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