diff --git a/aide-program/aide/decide/server.py b/aide-program/aide/decide/server.py index 6fa7c0c..451cef6 100644 --- a/aide-program/aide/decide/server.py +++ b/aide-program/aide/decide/server.py @@ -6,6 +6,7 @@ import json import socket import time from http.server import BaseHTTPRequestHandler, HTTPServer +from pathlib import Path from aide.core import output from aide.core.config import ConfigManager @@ -30,7 +31,10 @@ class DecideServer: self.storage = storage self.port = 3721 self.timeout = 0 - self.web_dir = root / "aide" / "decide" / "web" + self.bind = "127.0.0.1" + self.url = "" + # web 资源位于 aide 包目录下,而非项目根目录 + self.web_dir = Path(__file__).parent / "web" self.should_close = False self.close_reason: str | None = None self.httpd: DecideHTTPServer | None = None @@ -40,6 +44,8 @@ class DecideServer: config = ConfigManager(self.root).load_config() start_port = _get_int(config, "decide", "port", default=3721) self.timeout = _get_int(config, "decide", "timeout", default=0) + self.bind = _get_str(config, "decide", "bind", default="127.0.0.1") + self.url = _get_str(config, "decide", "url", default="") end_port = start_port + 9 available = self._find_available_port(start_port) if available is None: @@ -54,11 +60,17 @@ class DecideServer: stop_callback=self.stop, ) RequestHandler = self._build_request_handler(handlers) - self.httpd = DecideHTTPServer(("127.0.0.1", self.port), RequestHandler, handlers) + self.httpd = DecideHTTPServer((self.bind, self.port), RequestHandler, handlers) self.httpd.timeout = 1.0 + # 生成访问地址:优先使用自定义 url,否则自动生成 + if self.url: + access_url = self.url + else: + access_url = f"http://localhost:{self.port}" + output.info("Web 服务已启动") - output.info(f"请访问: http://localhost:{self.port}") + output.info(f"请访问: {access_url}") output.info("等待用户完成决策...") self._serve_forever() @@ -90,7 +102,7 @@ class DecideServer: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: - sock.bind(("127.0.0.1", port)) + sock.bind((self.bind, port)) return port except OSError: continue @@ -138,7 +150,7 @@ class DecideServer: content_length = int(length) if length else 0 except ValueError: self._send_response( - (400, handlers._cors_headers({"Content-Type": "application/json; charset=utf-8"}), b'{"error":"决策数据无效","detail":"无效的 Content-Length"}') + (400, handlers._cors_headers({"Content-Type": "application/json; charset=utf-8"}), '{"error":"决策数据无效","detail":"无效的 Content-Length"}'.encode("utf-8")) ) return if content_length > 1024 * 1024: @@ -146,7 +158,7 @@ class DecideServer: ( 413, handlers._cors_headers({"Content-Type": "application/json; charset=utf-8"}), - b'{"error":"请求体过大","detail":"单次提交限制 1MB"}', + '{"error":"请求体过大","detail":"单次提交限制 1MB"}'.encode("utf-8"), ) ) return @@ -156,10 +168,10 @@ class DecideServer: response = handlers.handle(method, self.path, body) except Exception as exc: # pragma: no cover - 兜底防御 payload = ( - b'{"error":"服务器内部错误","detail":' - + json.dumps(str(exc), ensure_ascii=False).encode("utf-8") - + b"}" - ) + '{"error":"服务器内部错误","detail":' + + json.dumps(str(exc), ensure_ascii=False) + + "}" + ).encode("utf-8") response = ( 500, handlers._cors_headers({"Content-Type": "application/json; charset=utf-8"}), @@ -200,3 +212,14 @@ def _get_int(config: dict, section: str, key: str, default: int) -> int: except Exception: return default return default + + +def _get_str(config: dict, section: str, key: str, default: str) -> str: + try: + section_data = config.get(section, {}) if isinstance(config, dict) else {} + value = section_data.get(key, default) + if isinstance(value, str): + return value + except Exception: + return default + return default diff --git a/aide-program/aide/decide/web/app.js b/aide-program/aide/decide/web/app.js index d7ed7d3..2473d29 100644 --- a/aide-program/aide/decide/web/app.js +++ b/aide-program/aide/decide/web/app.js @@ -14,6 +14,13 @@ async function init() { AppState.source = data.source || ""; AppState.items = Array.isArray(data.items) ? data.items : []; + // 如果有推荐项,默认选中推荐项 + AppState.items.forEach((item) => { + if (item.recommend) { + AppState.decisions[item.id] = item.recommend; + } + }); + renderItems(data); bindEvents(); } catch (error) { diff --git a/aide-program/aide/decide/web/style.css b/aide-program/aide/decide/web/style.css index 7bdef8d..50f6404 100644 --- a/aide-program/aide/decide/web/style.css +++ b/aide-program/aide/decide/web/style.css @@ -265,6 +265,10 @@ body { backdrop-filter: blur(2px); } +.success-overlay[hidden] { + display: none; +} + .success-message { background: var(--color-card); padding: var(--spacing-xl); @@ -294,6 +298,10 @@ body { min-width: 220px; } +.error-toast[hidden] { + display: none; +} + .error-icon { font-weight: 700; } diff --git a/aide-program/aide/main.py b/aide-program/aide/main.py index 9a97e2f..61e885b 100644 --- a/aide-program/aide/main.py +++ b/aide-program/aide/main.py @@ -132,8 +132,18 @@ def build_parser() -> argparse.ArgumentParser: # aide decide decide_parser = subparsers.add_parser("decide", help="待定项确认与决策记录") - decide_parser.add_argument("data", help="待定项 JSON 数据或 result") - decide_parser.set_defaults(func=handle_decide) + decide_subparsers = decide_parser.add_subparsers(dest="decide_cmd") + + # aide decide submit '' + decide_submit_parser = decide_subparsers.add_parser("submit", help="提交待定项数据并启动 Web 服务") + decide_submit_parser.add_argument("data", help="待定项 JSON 数据") + decide_submit_parser.set_defaults(func=handle_decide_submit) + + # aide decide result + decide_result_parser = decide_subparsers.add_parser("result", help="获取用户决策结果") + decide_result_parser.set_defaults(func=handle_decide_result) + + decide_parser.set_defaults(func=handle_decide_help) parser.add_argument("--version", action="version", version="aide dev") return parser @@ -288,8 +298,27 @@ def handle_flow_error(args: argparse.Namespace) -> bool: return tracker.error(args.description) -def handle_decide(args: argparse.Namespace) -> bool: - return cmd_decide(args) +def handle_decide_help(args: argparse.Namespace) -> bool: + print("usage: aide decide {submit,result} ...") + print("") + print("子命令:") + print(" submit 提交待定项数据并启动 Web 服务") + print(" result 获取用户决策结果") + print("") + print("示例:") + print(" aide decide submit '{\"task\":\"...\",\"source\":\"...\",\"items\":[...]}'") + print(" aide decide result") + return True + + +def handle_decide_submit(args: argparse.Namespace) -> bool: + from aide.decide import cmd_decide_submit + return cmd_decide_submit(args.data) + + +def handle_decide_result(args: argparse.Namespace) -> bool: + from aide.decide import cmd_decide_result + return cmd_decide_result() def _parse_value(raw: str) -> Any: diff --git a/aide-program/docs/commands/decide/server.md b/aide-program/docs/commands/decide/server.md index faa9d4c..4c6eee2 100644 --- a/aide-program/docs/commands/decide/server.md +++ b/aide-program/docs/commands/decide/server.md @@ -119,24 +119,46 @@ stop @enduml ``` -## 三、端口配置 +## 三、网络配置 -### 3.1 端口探测策略 +### 3.1 配置项 | 配置项 | 默认值 | 说明 | |--------|--------|------| | `decide.port` | 3721 | 起始端口 | +| `decide.bind` | `"127.0.0.1"` | 监听地址,设为 `"0.0.0.0"` 可允许外部访问 | +| `decide.url` | `""` | 自定义访问地址,为空时自动生成 `http://localhost:{port}` | | 最大尝试次数 | 10 | 从起始端口开始尝试 | +### 3.2 配置示例 + +```toml +[decide] +port = 3721 +bind = "0.0.0.0" # 监听所有网络接口 +url = "http://example.dev.net:3721" # 自定义访问地址 +``` + +### 3.3 端口探测策略 + **探测逻辑**: 1. 从 `decide.port` 开始 -2. 尝试绑定端口 +2. 尝试绑定到 `decide.bind:port` 3. 若失败,尝试下一个端口 4. 最多尝试 10 次 5. 全部失败则返回错误 -### 3.2 端口占用检测 +### 3.4 访问地址生成 + +``` +if decide.url 不为空: + access_url = decide.url +else: + access_url = f"http://localhost:{actual_port}" +``` + +### 3.5 端口占用检测 ``` check_port_available(port: int) -> bool: @@ -144,7 +166,7 @@ check_port_available(port: int) -> bool: 检查端口是否可用 1. 创建 socket - 2. 尝试绑定到 127.0.0.1:port + 2. 尝试绑定到 {bind}:{port} 3. 成功则端口可用,关闭 socket 返回 True 4. 失败则端口被占用,返回 False """ diff --git a/aide-program/docs/formats/config.md b/aide-program/docs/formats/config.md index 79037f6..eed0935 100644 --- a/aide-program/docs/formats/config.md +++ b/aide-program/docs/formats/config.md @@ -167,10 +167,22 @@ manager = "pnpm" |------|------|--------|------| | `port` | int | `3721` | Web 服务起始端口,端口被占用时向后探测最多 10 次 | | `timeout` | int | `0` | 服务超时时间(秒),0 表示不启用超时 | +| `bind` | string | `"127.0.0.1"` | 服务监听地址,设为 `"0.0.0.0"` 可允许外部访问 | +| `url` | string | `""` | 自定义访问地址,为空时自动生成 `http://localhost:{port}` | **使用场景**: - `aide decide ''` 读取 `port` 作为起始端口 - `aide decide ''` 读取 `timeout` 控制服务最长等待时间 +- `aide decide ''` 读取 `bind` 作为监听地址 +- `aide decide ''` 读取 `url` 作为输出的访问地址(支持自定义域名) + +**示例配置**: +```toml +[decide] +port = 3721 +bind = "0.0.0.0" # 监听所有网络接口 +url = "http://example.dev.net:3721" # 自定义访问地址 +``` ---