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

9.2 KiB
Raw Blame History

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 起始端口
最大尝试次数 10 从起始端口开始尝试

探测逻辑

  1. decide.port 开始
  2. 尝试绑定端口
  3. 若失败,尝试下一个端口
  4. 最多尝试 10 次
  5. 全部失败则返回错误

3.2 端口占用检测

check_port_available(port: int) -> bool:
    """
    检查端口是否可用

    1. 创建 socket
    2. 尝试绑定到 127.0.0.1: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

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

{
  "error": "无法读取待定项数据",
  "detail": "文件不存在或格式错误"
}

4.3 POST /api/submit

请求

Content-Type: application/json

{
  "decisions": [
    {"id": 1, "chosen": "jwt"},
    {"id": 2, "chosen": "bcrypt", "note": "团队更熟悉 bcrypt"}
  ]
}

响应

成功200

{
  "success": true,
  "message": "决策已保存"
}

校验失败400

{
  "error": "决策数据无效",
  "detail": "缺少待定项 2 的决策"
}

服务器错误500

{
  "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/ 目录提供
  • 不允许 .. 路径

九、相关文档