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 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

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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 '<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")
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 <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:

View File

@@ -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
"""

View File

@@ -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 '<json>'` 读取 `port` 作为起始端口
- `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" # 自定义访问地址
```
---