Files
agent-aide/aide-program/docs/commands/decide/server.md

495 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# aide decide HTTP 服务设计
## 一、概述
aide decide 使用内置 HTTP 服务器提供 Web 界面,供用户确认待定项。本文档定义服务器的设计规格。
### 1.1 技术选型
- 使用 Python 标准库 `http.server`
- 无需额外依赖
- 单线程阻塞模式(简化实现,满足单用户场景)
### 1.2 服务职责
| 职责 | 说明 |
|------|------|
| 静态资源服务 | 提供 HTML/CSS/JS 文件 |
| API 服务 | 提供数据获取和提交接口 |
| 生命周期管理 | 启动、等待、关闭 |
## 二、服务生命周期
### 2.1 状态机
```
@startuml
skinparam defaultFontName "PingFang SC"
[*] --> 初始化 : 调用 start()
初始化 --> 端口探测 : 配置加载完成
端口探测 --> 启动失败 : 无可用端口
端口探测 --> 运行中 : 找到可用端口
启动失败 --> [*]
运行中 --> 关闭中 : 收到提交/超时/中断
关闭中 --> 已关闭
已关闭 --> [*]
@enduml
```
### 2.2 启动流程
```
@startuml
skinparam defaultFontName "PingFang SC"
start
:加载配置;
note right: decide.port, decide.timeout
:确定起始端口;
if (配置了固定端口?) then (是)
:使用配置端口;
else (否)
:使用默认端口 3721;
endif
:端口探测循环;
repeat
:尝试绑定端口;
if (绑定成功?) then (是)
:记录实际端口;
break
else (否)
:端口 += 1;
endif
repeat while (尝试次数 < 10?)
if (找到可用端口?) then (是)
else (否)
:返回错误;
stop
endif
:创建 HTTP 服务器;
:注册请求处理器;
:输出访问链接;
:进入服务循环;
stop
@enduml
```
### 2.3 关闭流程
服务在以下情况下关闭:
| 触发条件 | 处理方式 |
|----------|----------|
| 用户提交决策 | 正常关闭,返回成功 |
| 超时(若配置) | 正常关闭,返回警告 |
| 键盘中断Ctrl+C | 正常关闭,返回中断 |
| 异常错误 | 异常关闭,返回错误 |
```
@startuml
skinparam defaultFontName "PingFang SC"
start
:收到关闭信号;
:停止接受新请求;
:等待当前请求完成;
note right: 最多等待 5 秒
:关闭 socket;
:清理资源;
:返回关闭原因;
stop
@enduml
```
## 三、网络配置
### 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. 尝试绑定到 `decide.bind:port`
3. 若失败,尝试下一个端口
4. 最多尝试 10 次
5. 全部失败则返回错误
### 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:
"""
检查端口是否可用
1. 创建 socket
2. 尝试绑定到 {bind}:{port}
3. 成功则端口可用,关闭 socket 返回 True
4. 失败则端口被占用,返回 False
"""
```
## 四、API 端点设计
### 4.1 端点一览
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/` | 返回主页面index.html |
| GET | `/style.css` | 返回样式文件 |
| GET | `/app.js` | 返回脚本文件 |
| GET | `/api/items` | 获取待定项数据 |
| POST | `/api/submit` | 提交决策结果 |
### 4.2 GET /api/items
**请求**:无参数
**响应**
成功200
```json
{
"task": "实现用户认证模块",
"source": "task-now.md",
"items": [
{
"id": 1,
"title": "认证方式选择",
"context": "任务描述中未明确指定认证方式",
"options": [
{"value": "jwt", "label": "JWT Token 认证", "score": 85, "pros": [...], "cons": [...]},
{"value": "session", "label": "Session 认证", "score": 70, "pros": [...], "cons": [...]}
],
"recommend": "jwt"
}
]
}
```
失败500
```json
{
"error": "无法读取待定项数据",
"detail": "文件不存在或格式错误"
}
```
### 4.3 POST /api/submit
**请求**
Content-Type: `application/json`
```json
{
"decisions": [
{"id": 1, "chosen": "jwt"},
{"id": 2, "chosen": "bcrypt", "note": "团队更熟悉 bcrypt"}
]
}
```
**响应**
成功200
```json
{
"success": true,
"message": "决策已保存"
}
```
校验失败400
```json
{
"error": "决策数据无效",
"detail": "缺少待定项 2 的决策"
}
```
服务器错误500
```json
{
"error": "保存失败",
"detail": "无法写入文件"
}
```
**提交后行为**
1. 验证决策数据完整性(所有待定项都有决策)
2. 保存决策结果到历史记录
3. 设置关闭标志
4. 返回成功响应
5. 服务器在响应发送后关闭
### 4.4 静态资源服务
| 路径 | 文件 | Content-Type |
|------|------|--------------|
| `/` | `web/index.html` | `text/html; charset=utf-8` |
| `/style.css` | `web/style.css` | `text/css; charset=utf-8` |
| `/app.js` | `web/app.js` | `application/javascript; charset=utf-8` |
**资源加载方式**
- 方案A从文件系统读取开发时便于修改
- 方案B嵌入 Python 代码(部署时无需额外文件)
建议使用方案A资源文件放在 `aide/decide/web/` 目录下。
## 五、请求处理
### 5.1 请求处理流程
```
@startuml
skinparam defaultFontName "PingFang SC"
start
:接收 HTTP 请求;
:解析请求路径和方法;
switch (路径)
case (/)
:返回 index.html;
case (/style.css)
:返回 style.css;
case (/app.js)
:返回 app.js;
case (/api/items)
if (方法 == GET?) then (是)
:读取 pending.json;
:返回 JSON 数据;
else (否)
:返回 405 Method Not Allowed;
endif
case (/api/submit)
if (方法 == POST?) then (是)
:解析请求体;
:验证决策数据;
:保存决策结果;
:设置关闭标志;
:返回成功响应;
else (否)
:返回 405 Method Not Allowed;
endif
case (其他)
:返回 404 Not Found;
endswitch
stop
@enduml
```
### 5.2 CORS 处理
由于前端和后端在同一服务器,通常不需要 CORS。但为了开发调试方便建议添加以下响应头
```
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type
```
### 5.3 错误处理
| HTTP 状态码 | 场景 |
|-------------|------|
| 200 | 请求成功 |
| 400 | 请求数据无效 |
| 404 | 路径不存在 |
| 405 | 方法不允许 |
| 500 | 服务器内部错误 |
## 六、超时处理
### 6.1 配置项
| 配置项 | 默认值 | 说明 |
|--------|--------|------|
| `decide.timeout` | 0 | 超时时间0 表示无超时 |
### 6.2 超时实现
```
@startuml
skinparam defaultFontName "PingFang SC"
start
fork
:服务循环;
:处理请求;
fork again
:超时计时器;
:等待 timeout 秒;
:设置超时标志;
end fork
if (超时?) then (是)
:关闭服务;
:输出警告;
else (否)
:正常关闭;
endif
stop
@enduml
```
**注意**:由于使用单线程模型,超时检测需要在请求处理间隙进行,或使用 `select` 实现非阻塞等待。
## 七、方法签名原型
```
class DecideServer:
"""HTTP 服务器"""
root: Path # 项目根目录
port: int # 实际使用的端口
timeout: int # 超时时间(秒)
pending_path: Path # pending.json 路径
web_dir: Path # 静态资源目录
should_close: bool # 关闭标志
close_reason: str # 关闭原因
def __init__(self, root: Path) -> None:
"""初始化服务器"""
def start(self) -> bool:
"""
启动服务器
1. 加载配置
2. 探测可用端口
3. 创建 HTTP 服务器
4. 输出访问链接
5. 进入服务循环
6. 返回是否成功完成
"""
def stop(self, reason: str) -> None:
"""
停止服务器
1. 设置关闭标志和原因
2. 关闭 socket
"""
def _find_available_port(self) -> int | None:
"""
探测可用端口
从配置端口开始,最多尝试 10 次
返回可用端口或 None
"""
def _serve_forever(self) -> None:
"""
服务循环
处理请求直到 should_close 为 True
"""
class DecideHandler:
"""请求处理器"""
server: DecideServer # 服务器引用
def handle_request(self, method: str, path: str, body: bytes) -> Response:
"""
处理请求
根据 method 和 path 分发到具体处理函数
"""
def handle_index(self) -> Response:
"""返回主页面"""
def handle_static(self, filename: str) -> Response:
"""返回静态资源"""
def handle_get_items(self) -> Response:
"""处理 GET /api/items"""
def handle_submit(self, body: bytes) -> Response:
"""处理 POST /api/submit"""
Response = tuple[int, dict[str, str], bytes]
# (状态码, 响应头, 响应体)
```
## 八、安全考虑
### 8.1 绑定地址
- 默认绑定到 `127.0.0.1`(仅本地访问)
- 不绑定到 `0.0.0.0`(避免外部访问)
### 8.2 输入验证
- 验证 JSON 格式
- 验证决策数据完整性
- 限制请求体大小(建议 1MB
### 8.3 路径遍历防护
- 静态资源只从 `web/` 目录提供
- 不允许 `..` 路径
## 九、相关文档
- [decide 详细设计入口](README.md)
- [CLI 规格](cli.md)
- [Web 前端设计](web.md)
- [数据存储设计](storage.md)