feat: 对aide decide进行部分调整

This commit is contained in:
2025-12-15 02:42:40 +08:00
parent 1381e8c7cd
commit ee1468492a
6 changed files with 120 additions and 19 deletions

View File

@@ -6,6 +6,7 @@ import json
import socket import socket
import time import time
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from aide.core import output from aide.core import output
from aide.core.config import ConfigManager from aide.core.config import ConfigManager
@@ -30,7 +31,10 @@ class DecideServer:
self.storage = storage self.storage = storage
self.port = 3721 self.port = 3721
self.timeout = 0 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.should_close = False
self.close_reason: str | None = None self.close_reason: str | None = None
self.httpd: DecideHTTPServer | None = None self.httpd: DecideHTTPServer | None = None
@@ -40,6 +44,8 @@ class DecideServer:
config = ConfigManager(self.root).load_config() config = ConfigManager(self.root).load_config()
start_port = _get_int(config, "decide", "port", default=3721) start_port = _get_int(config, "decide", "port", default=3721)
self.timeout = _get_int(config, "decide", "timeout", default=0) 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 end_port = start_port + 9
available = self._find_available_port(start_port) available = self._find_available_port(start_port)
if available is None: if available is None:
@@ -54,11 +60,17 @@ class DecideServer:
stop_callback=self.stop, stop_callback=self.stop,
) )
RequestHandler = self._build_request_handler(handlers) 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 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("Web 服务已启动")
output.info(f"请访问: http://localhost:{self.port}") output.info(f"请访问: {access_url}")
output.info("等待用户完成决策...") output.info("等待用户完成决策...")
self._serve_forever() self._serve_forever()
@@ -90,7 +102,7 @@ class DecideServer:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try: try:
sock.bind(("127.0.0.1", port)) sock.bind((self.bind, port))
return port return port
except OSError: except OSError:
continue continue
@@ -138,7 +150,7 @@ class DecideServer:
content_length = int(length) if length else 0 content_length = int(length) if length else 0
except ValueError: except ValueError:
self._send_response( 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 return
if content_length > 1024 * 1024: if content_length > 1024 * 1024:
@@ -146,7 +158,7 @@ class DecideServer:
( (
413, 413,
handlers._cors_headers({"Content-Type": "application/json; charset=utf-8"}), handlers._cors_headers({"Content-Type": "application/json; charset=utf-8"}),
b'{"error":"请求体过大","detail":"单次提交限制 1MB"}', '{"error":"请求体过大","detail":"单次提交限制 1MB"}'.encode("utf-8"),
) )
) )
return return
@@ -156,10 +168,10 @@ class DecideServer:
response = handlers.handle(method, self.path, body) response = handlers.handle(method, self.path, body)
except Exception as exc: # pragma: no cover - 兜底防御 except Exception as exc: # pragma: no cover - 兜底防御
payload = ( payload = (
b'{"error":"服务器内部错误","detail":' '{"error":"服务器内部错误","detail":'
+ json.dumps(str(exc), ensure_ascii=False).encode("utf-8") + json.dumps(str(exc), ensure_ascii=False)
+ b"}" + "}"
) ).encode("utf-8")
response = ( response = (
500, 500,
handlers._cors_headers({"Content-Type": "application/json; charset=utf-8"}), 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: except Exception:
return default return default
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

View File

@@ -14,6 +14,13 @@ async function init() {
AppState.source = data.source || ""; AppState.source = data.source || "";
AppState.items = Array.isArray(data.items) ? data.items : []; AppState.items = Array.isArray(data.items) ? data.items : [];
// 如果有推荐项,默认选中推荐项
AppState.items.forEach((item) => {
if (item.recommend) {
AppState.decisions[item.id] = item.recommend;
}
});
renderItems(data); renderItems(data);
bindEvents(); bindEvents();
} catch (error) { } catch (error) {

View File

@@ -265,6 +265,10 @@ body {
backdrop-filter: blur(2px); backdrop-filter: blur(2px);
} }
.success-overlay[hidden] {
display: none;
}
.success-message { .success-message {
background: var(--color-card); background: var(--color-card);
padding: var(--spacing-xl); padding: var(--spacing-xl);
@@ -294,6 +298,10 @@ body {
min-width: 220px; min-width: 220px;
} }
.error-toast[hidden] {
display: none;
}
.error-icon { .error-icon {
font-weight: 700; font-weight: 700;
} }

View File

@@ -132,8 +132,18 @@ def build_parser() -> argparse.ArgumentParser:
# aide decide # aide decide
decide_parser = subparsers.add_parser("decide", help="待定项确认与决策记录") decide_parser = subparsers.add_parser("decide", help="待定项确认与决策记录")
decide_parser.add_argument("data", help="待定项 JSON 数据或 result") decide_subparsers = decide_parser.add_subparsers(dest="decide_cmd")
decide_parser.set_defaults(func=handle_decide)
# aide decide submit '<json>'
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") parser.add_argument("--version", action="version", version="aide dev")
return parser return parser
@@ -288,8 +298,27 @@ def handle_flow_error(args: argparse.Namespace) -> bool:
return tracker.error(args.description) return tracker.error(args.description)
def handle_decide(args: argparse.Namespace) -> bool: def handle_decide_help(args: argparse.Namespace) -> bool:
return cmd_decide(args) print("usage: aide decide {submit,result} ...")
print("")
print("子命令:")
print(" submit <json> 提交待定项数据并启动 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: def _parse_value(raw: str) -> Any:

View File

@@ -119,24 +119,46 @@ stop
@enduml @enduml
``` ```
## 三、端口配置 ## 三、网络配置
### 3.1 端口探测策略 ### 3.1 配置项
| 配置项 | 默认值 | 说明 | | 配置项 | 默认值 | 说明 |
|--------|--------|------| |--------|--------|------|
| `decide.port` | 3721 | 起始端口 | | `decide.port` | 3721 | 起始端口 |
| `decide.bind` | `"127.0.0.1"` | 监听地址,设为 `"0.0.0.0"` 可允许外部访问 |
| `decide.url` | `""` | 自定义访问地址,为空时自动生成 `http://localhost:{port}` |
| 最大尝试次数 | 10 | 从起始端口开始尝试 | | 最大尝试次数 | 10 | 从起始端口开始尝试 |
### 3.2 配置示例
```toml
[decide]
port = 3721
bind = "0.0.0.0" # 监听所有网络接口
url = "http://example.dev.net:3721" # 自定义访问地址
```
### 3.3 端口探测策略
**探测逻辑** **探测逻辑**
1.`decide.port` 开始 1.`decide.port` 开始
2. 尝试绑定端口 2. 尝试绑定`decide.bind:port`
3. 若失败,尝试下一个端口 3. 若失败,尝试下一个端口
4. 最多尝试 10 次 4. 最多尝试 10 次
5. 全部失败则返回错误 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: check_port_available(port: int) -> bool:
@@ -144,7 +166,7 @@ check_port_available(port: int) -> bool:
检查端口是否可用 检查端口是否可用
1. 创建 socket 1. 创建 socket
2. 尝试绑定到 127.0.0.1:port 2. 尝试绑定到 {bind}:{port}
3. 成功则端口可用,关闭 socket 返回 True 3. 成功则端口可用,关闭 socket 返回 True
4. 失败则端口被占用,返回 False 4. 失败则端口被占用,返回 False
""" """

View File

@@ -167,10 +167,22 @@ manager = "pnpm"
|------|------|--------|------| |------|------|--------|------|
| `port` | int | `3721` | Web 服务起始端口,端口被占用时向后探测最多 10 次 | | `port` | int | `3721` | Web 服务起始端口,端口被占用时向后探测最多 10 次 |
| `timeout` | int | `0` | 服务超时时间0 表示不启用超时 | | `timeout` | int | `0` | 服务超时时间0 表示不启用超时 |
| `bind` | string | `"127.0.0.1"` | 服务监听地址,设为 `"0.0.0.0"` 可允许外部访问 |
| `url` | string | `""` | 自定义访问地址,为空时自动生成 `http://localhost:{port}` |
**使用场景** **使用场景**
- `aide decide '<json>'` 读取 `port` 作为起始端口 - `aide decide '<json>'` 读取 `port` 作为起始端口
- `aide decide '<json>'` 读取 `timeout` 控制服务最长等待时间 - `aide decide '<json>'` 读取 `timeout` 控制服务最长等待时间
- `aide decide '<json>'` 读取 `bind` 作为监听地址
- `aide decide '<json>'` 读取 `url` 作为输出的访问地址(支持自定义域名)
**示例配置**
```toml
[decide]
port = 3721
bind = "0.0.0.0" # 监听所有网络接口
url = "http://example.dev.net:3721" # 自定义访问地址
```
--- ---