2025-12-15 01:37:19 +08:00
|
|
|
|
# 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
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2025-12-15 02:42:40 +08:00
|
|
|
|
## 三、网络配置
|
2025-12-15 01:37:19 +08:00
|
|
|
|
|
2025-12-15 02:42:40 +08:00
|
|
|
|
### 3.1 配置项
|
2025-12-15 01:37:19 +08:00
|
|
|
|
|
|
|
|
|
|
| 配置项 | 默认值 | 说明 |
|
|
|
|
|
|
|--------|--------|------|
|
|
|
|
|
|
| `decide.port` | 3721 | 起始端口 |
|
2025-12-15 02:42:40 +08:00
|
|
|
|
| `decide.bind` | `"127.0.0.1"` | 监听地址,设为 `"0.0.0.0"` 可允许外部访问 |
|
|
|
|
|
|
| `decide.url` | `""` | 自定义访问地址,为空时自动生成 `http://localhost:{port}` |
|
2025-12-15 01:37:19 +08:00
|
|
|
|
| 最大尝试次数 | 10 | 从起始端口开始尝试 |
|
|
|
|
|
|
|
2025-12-15 02:42:40 +08:00
|
|
|
|
### 3.2 配置示例
|
|
|
|
|
|
|
|
|
|
|
|
```toml
|
|
|
|
|
|
[decide]
|
|
|
|
|
|
port = 3721
|
|
|
|
|
|
bind = "0.0.0.0" # 监听所有网络接口
|
|
|
|
|
|
url = "http://example.dev.net:3721" # 自定义访问地址
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.3 端口探测策略
|
|
|
|
|
|
|
2025-12-15 01:37:19 +08:00
|
|
|
|
**探测逻辑**:
|
|
|
|
|
|
|
|
|
|
|
|
1. 从 `decide.port` 开始
|
2025-12-15 02:42:40 +08:00
|
|
|
|
2. 尝试绑定到 `decide.bind:port`
|
2025-12-15 01:37:19 +08:00
|
|
|
|
3. 若失败,尝试下一个端口
|
|
|
|
|
|
4. 最多尝试 10 次
|
|
|
|
|
|
5. 全部失败则返回错误
|
|
|
|
|
|
|
2025-12-15 02:42:40 +08:00
|
|
|
|
### 3.4 访问地址生成
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
if decide.url 不为空:
|
|
|
|
|
|
access_url = decide.url
|
|
|
|
|
|
else:
|
|
|
|
|
|
access_url = f"http://localhost:{actual_port}"
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 3.5 端口占用检测
|
2025-12-15 01:37:19 +08:00
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
check_port_available(port: int) -> bool:
|
|
|
|
|
|
"""
|
|
|
|
|
|
检查端口是否可用
|
|
|
|
|
|
|
|
|
|
|
|
1. 创建 socket
|
2025-12-15 02:42:40 +08:00
|
|
|
|
2. 尝试绑定到 {bind}:{port}
|
2025-12-15 01:37:19 +08:00
|
|
|
|
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)
|