docs: 清理已落地的开发计划文档并修复引用
删除 10 篇已完成/已实施的开发计划文档(认证独立化、captcha/confs C++ 重写、services.yaml、pybot 迁移、aibox --once/工具分层/CLI 化/ 重构快照、actpred cv2.LUT 优化),同步修复 ca/captcha2/pybot/aibox README、代码注释、安全审计与报告中的 12 处引用,MODULES.md 文档 计数更新为 67 篇。
This commit is contained in:
1 parent
38be729509
commit
6f1f2bd296
23 files changed
+11
-4123
No files matched your search
@@ -2,8 +2,7 @@
|
||||
|
||||
从 .mt 文件中提取 @Action 块,解析为 (n_rows, n_cols) 的 float32 矩阵。
|
||||
支持 `-` 占位符(沿用上一行同列的值);首行 `-` 填 0,
|
||||
与 pybot/bot4cj 运行时语义一致(2026-08 统一,见
|
||||
docs/reports/bot4cj_python_migration_dev_doc.md §2.3)。
|
||||
与 pybot/bot4cj 运行时语义一致(2026-08 统一)。
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
+1
-1
@@ -980,7 +980,7 @@ elif command == "/mycommand":
|
||||
- ✅ **测试**:272 项全过(Batch E PoC 回归 + F1 配置脱敏 3 项 + F2 超时清理/线程收尾 5 项新增)
|
||||
|
||||
### v3.2
|
||||
- ✅ **CLI 一次性对话模式(`--once`)**(分三批落地,开发文档见 [docs/reports/aibox_cli_once_mode_dev_doc.md](../docs/reports/aibox_cli_once_mode_dev_doc.md)):
|
||||
- ✅ **CLI 一次性对话模式(`--once`)**(分三批落地):
|
||||
- **Batch A**:`--once QUERY` / `--quiet` / `--once-timeout`;不启动 WebSocket,执行后退出;回答走 stdout、过程走 stderr,可管道组合;SIGINT 与超时复用三层取消机制
|
||||
- **Batch B**:`--no-save-memory`(`enable_memory` 开关干净禁用 MemoryTool)、`--show-reasoning`(思考流默认静默);压缩提示改走 output_callback 不再污染 stdout;退出码 1(工具执行失败)
|
||||
- **Batch C**:SQLite WAL(多 CLI 进程并发写锁缓解)、`--worker-only`(纯执行面 + 精简 system prompt 防人格污染)、`--model` / `--temperature` 覆盖、子代理工具失败标记透传(`_SUBAGENT_TOOL_FAILED_MARKER` → orchestrator stats)
|
||||
|
||||
+1
-1
@@ -3343,7 +3343,7 @@ def handle_memo_commands(manager):
|
||||
def handle_once_mode(manager, args) -> bool:
|
||||
"""一次性对话模式:命中 --once 则执行单次 AI 对话并以退出码结束进程。
|
||||
|
||||
设计要点(详见 docs/reports/aibox_cli_once_mode_dev_doc.md):
|
||||
设计要点:
|
||||
- stdout 仅输出 AI 回答正文(chunk 流式),line/reasoning 过程行走 stderr,管道安全
|
||||
- 退出码:0 成功 / 2 API 或链路失败 / 3 用户中断(Ctrl+C) / 4 全局超时
|
||||
- 复用三层中断机制(Event + HTTP 关闭 + 子进程 kill):SIGINT 与超时 Timer 均走 cancel()
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ gateway 侧仅做**存在性不变量**检查(显式 `Target-Url` 请求必须
|
||||
|
||||
### `POST /register` / `POST /register/send_code` / `POST /reset/send_code` / `POST /reset_password`
|
||||
|
||||
与 gateway v0.8.0 契约完全一致(详见 `docs/reports/auth_service_plan.md` §5 与黑盒 tests/blackbox/test_gateway.py)。
|
||||
与 gateway v0.8.0 契约完全一致(详见黑盒 tests/blackbox/test_gateway.py)。
|
||||
|
||||
### `POST /logout` / `POST /refresh_token` / `POST /whoami` / `POST /admin/verify`
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ captcha2 :8009 (RpcServer)
|
||||
## 状态
|
||||
|
||||
- 生产运行中(services.yaml 拉起),Python captcha 已废弃
|
||||
- 开发文档见 [docs/reports/captcha_cpp_rewrite_dev_doc.md](../docs/reports/captcha_cpp_rewrite_dev_doc.md)
|
||||
- 安全审计见 [docs/security/captcha2_SECURITY_AUDIT_20260809.md](../docs/security/captcha2_SECURITY_AUDIT_20260809.md)
|
||||
|
||||
## 许可证
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@
|
||||
| 模块 | 说明 |
|
||||
|------|------|
|
||||
| tests | 黑盒测试套件(`test_db.py`/`test_gateway.py`/`test_waf.py`/`test_dnfs.py`/`test_captcha.py`/`test_smtp.py`/`test_ai_review.py`/`test_static.py`/`test_core_confs.py`),纯标准库 HTTP/TCP 用户旅程模拟,经 `test_runner.py` 统一调度(详见 [tests/blackbox/README.md](../tests/blackbox/README.md)) |
|
||||
| docs | 文档聚合(security/ 审计、reviews/ 审查、reports/ 分析),77 篇 md + 1 篇 html |
|
||||
| docs | 文档聚合(security/ 审计、reviews/ 审查、reports/ 分析),67 篇 md + 1 篇 html |
|
||||
|
||||
## 九、根级构建与部署脚本
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
> 模块:actpred(hailo_infer.py cv2.LUT 快路径)
|
||||
> 作者:自动生成
|
||||
> 日期:2026-08-17
|
||||
> 前置:`actpred_hailo_preprocess_optimization_plan_20260817.md`(优化方案,已实施)、
|
||||
> `actpred_inference_speed_benchmark_20260813.md`(旧基准)
|
||||
> 前置:`actpred_inference_speed_benchmark_20260813.md`(旧基准)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
# actpred Hailo 推理预处理优化:cv2.LUT 查表合并归一化与量化(已实施)
|
||||
|
||||
> 文档类型:开发方案(已实施)
|
||||
> 模块:actpred(部署链 hailo_infer.py)
|
||||
> 作者:自动生成
|
||||
> 日期:2026-08-17(原型实测 + 集成实施)
|
||||
> 前置:`actpred_inference_speed_benchmark_20260813.md`(速度基准)、
|
||||
> 2026-08-17 新 640×480 数据集重测(本方案 §2 数据)
|
||||
|
||||
> 📌 **版本历程**
|
||||
> 1. 初版:numpy 查表法为核心,预期端到端 18-20ms
|
||||
> 2. 原型实测(§3.4):**numpy 高级索引查表在 Pi5 上无加速**(fancy
|
||||
> indexing 的 gather 开销与逐元素 float 运算相当),预期不成立;
|
||||
> 合并系数乘加仅 -5ms;**cv2.LUT 实测 15.8× 加速**(§3.4)
|
||||
> 3. **已实施(§5)**:`hailo_infer.py` 集成 cv2.LUT 快路径(可选依赖,
|
||||
> 无 cv2 自动回退 float 路径),端到端 52.4 → **16.0 ms**,输出与
|
||||
> float 路径逐点一致(max_diff = 0)
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与动机
|
||||
|
||||
2026-08-17 用新录制的 640×480 数据集(免 resize 训练样本)重测 HEF 推理速度:
|
||||
|
||||
| 阶段 | 640×480 输入 | 224×224 输入(旧数据) |
|
||||
|---|---|---|
|
||||
| 端到端 predict | **52.4 ms** | 60.0 ms |
|
||||
| 预处理 | **35.9 ms** | 43.1 ms |
|
||||
| NPU run | 12.5 ms | 12.9 ms |
|
||||
| 后处理 | 0.6 ms | 0.6 ms |
|
||||
|
||||
输入尺寸已与模型一致(resize 变 no-op),**预处理仍是端到端延迟的主导项**(68%)。
|
||||
旧基准报告(2026-08-13)已记录同类问题:1080p 采集的预处理缩放约 45ms 主导端到端延迟,
|
||||
当时通过把默认分辨率改回 640×480 缓解;本次分析证明 640×480 下 Python 端预处理
|
||||
仍有约 36ms 的优化空间——但空间比初版估计小(见 §3.4 实测)。
|
||||
|
||||
## 2. 瓶颈分析(100 次均值分段计时)
|
||||
|
||||
| 子步骤 | 耗时 | 说明 |
|
||||
|---|---|---|
|
||||
| `_frame_float`(3 帧) | **27.2 ms** | PIL fromarray/convert + resize(同尺寸 no-op 仍有开销)+ `(x/255-mean)/std` float 运算(92 万像素×3 通道) |
|
||||
| `create_bindings` + 帧量化绑定 | **9.1 ms** | `_bind_frames`:float 归一化帧 → `round(x/q.scale+q.zp)` 再量化回 uint8 |
|
||||
| history/pred 绑定 | 0.07 ms | 可忽略 |
|
||||
|
||||
**关键冗余**:当前流程先做 float 归一化(`_frame_float`),随后又量化回 uint8(`_bind_frames`)——
|
||||
中间 float 帧是纯中间态,归一化与量化可以**合并为一步**。
|
||||
|
||||
## 3. 优化方案
|
||||
|
||||
### 3.1 主方案:合并系数乘加(纯 numpy,无新依赖)
|
||||
|
||||
HEF 帧输入是 uint8 量化值:`q = round(((x/255 - mean)/std) / q_scale + q_zp)`。
|
||||
把 4 步运算合并为**一次乘加**(系数预计算,按通道):
|
||||
|
||||
```
|
||||
q = round(x * A[c] + B[c])
|
||||
A[c] = 1 / (255 * std[c] * q_scale)
|
||||
B[c] = -mean[c] / (std[c] * q_scale) + q_zp
|
||||
```
|
||||
|
||||
- 现流程 4 次全数组运算(`/255`、`-mean`、`/std`、`/q_scale+q_zp`)→ 1 次乘加
|
||||
- 数值完全等价(实测 max_diff = 0 LSB,见 §3.4)
|
||||
- 直通模式(`_normalize_frames=False`)同理:`A=1/q_scale, B=q_zp`
|
||||
|
||||
### 3.2 附带优化(与主方案正交)
|
||||
|
||||
1. **免 PIL 路径**:输入已是 np.ndarray 时直接运算,跳过 `Image.fromarray/convert`
|
||||
2. **resize no-op 跳过**:`image.size == (W,H)` 时不调用 `resize()`
|
||||
3. **缓冲复用**:`np.multiply/np.add` 带 `out=` 参数,减少临时数组分配
|
||||
|
||||
### 3.3 已实施快路径:cv2.LUT(可选依赖,无 cv2 自动回退)
|
||||
|
||||
`cv2.LUT(img, lut)` 是 C 实现的 uint8→uint8 查表,单次遍历,无 float 中间态——
|
||||
把"归一化+量化"合并为一步。集成要点(`hailo_infer.py`):
|
||||
|
||||
- 惰性导入 cv2(`HAS_CV2`);**无 cv2 或非逐帧输入布局(ncfhw/nhwc_fused)自动回退 float 路径**
|
||||
- 加载时按每帧 `quant_info` 构建 256 项表(归一化/直通两种,`_normalize_frames` 判定)
|
||||
- **OpenCV 5.0 的 cv2.LUT 只接受单通道表**((256,3) 会断言失败),逐通道应用后 `cv2.merge`
|
||||
- 输入尺寸与模型不一致时先 PIL resize(仅此情况走 PIL)
|
||||
- 依赖安装:`opencv-python-headless`(5.0.0,清华镜像;官方源过慢)
|
||||
- 注:本方案实测后发现**合并系数乘加(§3.1)未实施**——cv2.LUT 收益已远超,
|
||||
避免冗余代码;§3.1 保留作为无 cv2 环境下的潜在备选
|
||||
|
||||
### 3.4 原型实测评估(2026-08-17,100-200 次均值,640×480×3 帧)
|
||||
|
||||
| 方案 | 3 帧耗时 | 对比现 float 28.5ms | 数值差异 |
|
||||
|---|---|---|---|
|
||||
| 现 float 路径 | 28.5 ms | 基线 | — |
|
||||
| numpy 高级索引查表 | 26.6 ms | **无加速(1.0x)** | 0 LSB |
|
||||
| 合并系数乘加 | 23.2 ms | 快 19%(-5ms) | 0 LSB |
|
||||
| **cv2.LUT(逐通道)** | **1.68 ms** | **快 15.8×** | **0 LSB** |
|
||||
|
||||
**结论**:
|
||||
- numpy fancy indexing 的 gather 开销与逐元素 float 运算相当,查表法**无收益**
|
||||
- 纯 numpy 可达的最佳是合并乘加(-5ms),受限于 92 万像素 float32 数组的内存吞吐
|
||||
- cv2.LUT 的 C 查表把帧预处理降一个数量级,且数值完全等价(0 LSB)
|
||||
|
||||
### 3.5 实测收益(已实施,集成后 NPU 实测)
|
||||
|
||||
| 指标 | 优化前 | 优化后(cv2.LUT) |
|
||||
|---|---|---|
|
||||
| 帧预处理(3 帧) | 26.5 ms | **3.0 ms(分段实测含绑定)** |
|
||||
| 端到端 predict | 52.4 ms | **16.1 ms(3.3×)**(200 次均值,见测试报告) |
|
||||
| 输出一致性 | — | 与 float 路径 max_diff = **0.0000**(7 类输入 + 8 seed 随机) |
|
||||
| HEF vs torch | 0.50° | 0.30°(4 角复验 PASS) |
|
||||
| vs PyTorch CPU | 2.1× | **6.7×**(16.1 vs 107.9 ms) |
|
||||
|
||||
闭环 10Hz(100ms 预算)占用率 52% → **16%**,20Hz 32%,30Hz(33ms)48%。
|
||||
|
||||
> 完整验收数据见 `actpred_cv2_lut_benchmark_report_20260817.md`(测试报告)。
|
||||
|
||||
## 4. 一致性约束(关键)
|
||||
|
||||
- **训练侧不动**:`dataset.py` 的 `Resize+ToTensor+Normalize` 保持 float 路径——
|
||||
合并乘加只是推理侧对"归一化+量化"的合并实现,数值等价(实测 0 LSB)
|
||||
- **三后端口径**:`predict.py`(PyTorch,float 输入不量化)、`onnx_infer.py`(ORT,float)
|
||||
不受影响;优化仅作用于 `hailo_infer.py` 的 uint8 量化输入路径
|
||||
- **帧输入判定不变**:`_normalize_frames`(量化网格负零偏判定)决定走
|
||||
归一化还是直通,逻辑与现状一致
|
||||
- 旧 5 输入 / 旧版单图像 HEF 布局同样适用(按各自 quant_info 算系数)
|
||||
|
||||
## 5. 实施步骤(已完成 ✅)
|
||||
|
||||
| 步骤 | 内容 | 状态 |
|
||||
|---|---|---|
|
||||
| 1 | `hailo_infer.py`:加载时按每帧 quant_info 构建 256 项 LUT(归一化/直通两种) | ✅ `_build_frame_luts` |
|
||||
| 2 | `predict` 的 nhwc_frames 分支走 cv2.LUT 快路径(尺寸不匹配时 PIL resize);无 cv2 回退 float | ✅ `_frame_lut_quant` + `HAS_CV2` |
|
||||
| 3 | NPU 实测:640×480 数据集 100 次均值 | ✅ 16.0 ms(52.4 → 16.0) |
|
||||
| 4 | `_npu_test.py` 4 角 + HEF vs torch 对比复验 | ✅ PASS,0.30°(≤1° 预算) |
|
||||
| 5 | 全套单测(TestHailoPredictor 等) | ✅ 188 tests OK |
|
||||
| 6 | 依赖安装:opencv-python-headless 5.0.0(清华镜像) | ✅ venv 已装 |
|
||||
|
||||
## 6. 风险与对策
|
||||
|
||||
| 风险 | 对策 |
|
||||
|---|---|
|
||||
| 查表取整与 float 路径存在差异 | 实测 0 LSB(表项 float32 与现路径同精度);NPU 输出 vs float 路径 max_diff=0,vs torch 0.30° |
|
||||
| 无 cv2 环境 | 惰性导入 + 自动回退 float 路径,行为与优化前完全一致 |
|
||||
| OpenCV 5.0 多通道 LUT 断言失败 | 逐通道单表 + `cv2.merge`(实测无性能损失) |
|
||||
| 非 nhwc_frames 布局(旧 HEF) | 仅该布局启用 cv2 路径,其余走原 float 路径 |
|
||||
| 未来输入尺寸变更 | LUT 与尺寸无关(逐像素映射);尺寸不一致时才走 PIL resize |
|
||||
|
||||
## 7. 决策记录
|
||||
|
||||
- **查表法(初版核心)实测证伪**:numpy 高级索引 gather 在 Pi5 上与逐元素 float
|
||||
运算同速(26.6 vs 28.5ms),"查表降为内存索引即快"的假设不成立——教训:
|
||||
性能方案须原型实测,不能凭直觉估计
|
||||
- **最终方案选 cv2.LUT**:C 实现查表把帧预处理从 26.5ms 降到 1.7ms(15.8×),
|
||||
端到端 52.4 → 16.0ms;数值 0 LSB 等价。合并系数乘加(-5ms)收益被碾压,
|
||||
不再实施(§3.1 保留为无 cv2 环境的备选)
|
||||
- **依赖取舍**:opencv-python-headless 作为可选依赖(惰性导入 + 自动回退),
|
||||
有 cv2 环境收益 3.3×,无 cv2 环境行为不变——收益远大于依赖成本
|
||||
- **训练侧不改**:训练用 float 归一化(增强/数值精度),推理侧查表实现——两侧
|
||||
口径在"归一化值"层面一致,量化差异由部署链固有预算吸收(HEF vs torch 0.30° 余量充足)
|
||||
@@ -1,358 +0,0 @@
|
||||
# aibox CLI 单次调用模式 · 开发文档
|
||||
|
||||
> 文档类型:功能开发计划
|
||||
> 模块:aibox(单 AI 智能体系统)
|
||||
> 日期:2026-08-17
|
||||
> 状态:方案研究(待评审)
|
||||
> 影响范围:`aibox/aibox.py`(仅增量改动,WebSocket 模式零回归)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 背景与动机
|
||||
|
||||
aibox 目前只有两种使用方式:
|
||||
|
||||
| 使用方式 | 入口 | 特点 |
|
||||
|----------|------|------|
|
||||
| WebSocket 服务器 | `python aibox.py` → `ws://localhost:8765` | 长连接、多轮对话、前端/BotOps 交互(核心场景) |
|
||||
| 备忘录 CLI | `--check-memo` / `--memo-add` / `--memo-list` / `--memo-complete` | 只做任务 CRUD,不触发 AI 对话 |
|
||||
|
||||
缺少一种**"问一次就退"**的 CLI 一次性对话模式。这导致以下场景无法覆盖:
|
||||
|
||||
1. **脚本化调用**:无法在 shell 脚本 / cron / CI 流水线中把 AI 当命令行工具用(如 `aibox --once "统计当前目录 py 文件行数" | grep ...`)
|
||||
2. **资源受限环境**:树莓派 / NPU 盒子上不想常驻一个 WebSocket 服务进程,只想按需拉起、用完即退
|
||||
3. **调试与测试**:复现某个 prompt 行为必须启动服务器 + 前端连接,链路太重
|
||||
4. **进程级隔离执行**:Worker 子代理当前是**同进程线程**(`run_full_subagent` → `DeepSeekChat`),子代理崩溃/死循环会影响主服务;有了 CLI 单次模式后,可演进为"独立子进程执行",OS 级隔离
|
||||
|
||||
### 1.2 设计目标
|
||||
|
||||
1. **新增 `--once QUERY` 参数**:执行一次性 AI 对话(含完整工具调用链),结果流式输出到 stdout,完成后进程退出
|
||||
2. **纯增量、零回归**:不改动 WebSocket 模式任何现有行为;不加参数时行为与现状完全一致
|
||||
3. **复用现有架构**:复用 `EnhancedAIManager` / `DeepSeekChat` / Orchestrator-Worker 工具分层 / 三层中断机制,不重写对话逻辑
|
||||
4. **管道友好**:stdout 只出 AI 回答正文,过程行(🔧 工具调用等)走 stderr,退出码语义化,可被 shell 脚本安全组合
|
||||
5. **状态隔离明确**:单次模式默认不加载历史对话、不写对话归档,记忆写入显式可控
|
||||
|
||||
### 1.3 非目标
|
||||
|
||||
- 不做多轮对话 CLI(交互式 REPL 已由 `client.py` 覆盖)
|
||||
- 不做 HTTP/REST 单次接口(如需可在后续批次评估,复用同一内核)
|
||||
- 不改变 Worker 子代理的线程内执行方式(进程化是后续可选演进,不在本期范围)
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状分析
|
||||
|
||||
### 2.1 入口结构(aibox.py)
|
||||
|
||||
```
|
||||
parse_args() # L2786: argparse 定义
|
||||
main() # L2927: 主入口
|
||||
├── 语言/配置/调试覆盖 # L2931-2959
|
||||
├── 目录创建 + 日志初始化 # L2961-2973
|
||||
├── SubprocessCommandExecutor # L2976
|
||||
├── EnhancedAIManager # L2978-2981
|
||||
├── handle_check_commands() # L2983: --check-memo 处理后 return
|
||||
├── handle_memo_commands() # L2986: --memo-* 处理后 return
|
||||
└── manager.run() # L2990: WebSocket 服务器(唯一终点)
|
||||
```
|
||||
|
||||
关键观察:
|
||||
|
||||
- `handle_check_commands` / `handle_memo_commands` 已经是**"处理后即退出"的 CLI 模式先例**,单次对话模式可完全沿用该模式:在 `manager.run()` 之前插入 `handle_once_mode(manager)`,命中则执行并 return
|
||||
- `EnhancedAIManager.add_ai()`(L2584)已封装完整的 AI 实例组装(memory_db / memo_db / history_manager / prompt_manager / command_executor 注入),单次模式可直接复用
|
||||
- `DeepSeekChat.think_and_respond(input_text, output_callback)`(L1308)是同步阻塞函数,天然适合 CLI 直接调用——不需要 asyncio 事件循环
|
||||
|
||||
### 2.2 output_callback 消息类型(流式输出契约)
|
||||
|
||||
`think_and_respond` 通过 `output_callback(msg_type, content)` 实时推送:
|
||||
|
||||
| msg_type | 语义 | CLI 模式去向 |
|
||||
|----------|------|--------------|
|
||||
| `chunk` | AI 回答增量片段(**权威内容来源**) | **stdout**(流式 write + flush) |
|
||||
| `complete` | 收尾信号(内容与 chunk 累积重复) | 仅作收尾标记,不重复输出 |
|
||||
| `reasoning_start/reasoning/reasoning_end` | 思考模式流式 | stderr(可用 `--show-reasoning` 控制) |
|
||||
| `line` | 过程行:🔧 工具调用、工具输出、💬 前缀 | stderr |
|
||||
| `ai_break` | 工具调用分段信号 | 忽略(CLI 无 widget 概念) |
|
||||
| `error` | API/解析错误 | stderr + 退出码 2 |
|
||||
| `dialogue_ended` | AI 主动结束对话 | 正常收尾 |
|
||||
|
||||
### 2.3 需要处理的技术要点
|
||||
|
||||
| 要点 | 现状 | 风险与对策 |
|
||||
|------|------|-----------|
|
||||
| **压缩输出污染** | `_think_and_respond_impl` L1346 中 `print("📦 对话压缩...")` 直接打 stdout | 单次模式默认不加载历史 → 消息极少 → 几乎不触发压缩;另需确认该 print 走 stderr 或抑制(Batch C 处理) |
|
||||
| **记忆写入并发** | 多个 CLI 进程并发写 `memories.db`(SQLite) | 默认 `--once` 模式 AI 侧仍可调用 `memory` 工具(orchestrator profile);SQLite 有文件锁兜底,低频并发无碍;提供 `--no-save-memory` 开关时通过提示词引导(见 §3.4) |
|
||||
| **Ctrl+C 中断** | 三层取消机制(Event + HTTP close + 子进程 kill)依赖 `cancel()` 被调用 | CLI 模式注册 `signal.SIGINT` handler 调 `manager.cancel()`,与 WebSocket `handle_interrupt` 路径等价 |
|
||||
| **子代理输出转发** | `SubAgentTool._subagent_output` 经 `send_line` → `output_queue` → 回调链 | CLI 模式的 `output_callback` 会自然收到子代理的 `line`/`reasoning`,全部导流 stderr,无需特殊处理 |
|
||||
| **对话归档写入** | `global_exit_handler` 会调 `history_manager.end_current_conversation()` | 单次模式传 `exit_callback=None`(与 TaskScheduler 的任务实例一致),AI 主动 `save_memory_and_end_conversation` 时不触发全局归档 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 设计方案
|
||||
|
||||
### 3.1 命令行接口
|
||||
|
||||
```bash
|
||||
# 基本用法:一次性对话,结果输出到 stdout
|
||||
python aibox.py --once "帮我统计当前目录下所有 py 文件的行数"
|
||||
|
||||
# 静默模式:只输出最终回答正文(工具过程完全静默,适合管道)
|
||||
python aibox.py --once "把 README.md 翻译成英文摘要" --quiet
|
||||
|
||||
# 显式控制
|
||||
python aibox.py --once "..." --no-save-memory # 不写入记忆数据库
|
||||
python aibox.py --once "..." --timeout 120 # 全局超时 120 秒(默认 300)
|
||||
python aibox.py --once "..." --show-reasoning # stderr 显示思考过程(默认不显示)
|
||||
```
|
||||
|
||||
新增参数(全部可选,`--once` 之外均有默认值):
|
||||
|
||||
| 参数 | 类型 | 默认 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `--once QUERY` | str | — | 一次性对话模式:执行 QUERY 后退出,不启动 WebSocket |
|
||||
| `--quiet` | flag | false | 仅 stdout 输出回答正文;过程行/thinking 全静默 |
|
||||
| `--no-save-memory` | flag | false | 禁止本次对话写入记忆数据库 |
|
||||
| `--timeout SEC` | int | 300 | 全局执行超时,超时 kill 并退出码 4 |
|
||||
| `--show-reasoning` | flag | false | 在 stderr 显示思考过程流 |
|
||||
|
||||
### 3.2 退出码语义
|
||||
|
||||
| 退出码 | 含义 |
|
||||
|--------|------|
|
||||
| 0 | 成功(AI 正常完成回答) |
|
||||
| 1 | 工具执行链路异常(如 run_command 失败且 AI 报告失败) |
|
||||
| 2 | API 请求失败(网络/HTTP/JSON 解析,服务端已回滚消息) |
|
||||
| 3 | 用户中断(Ctrl+C) |
|
||||
| 4 | 全局超时(--timeout 到期) |
|
||||
|
||||
> Batch A 只保证 0 / 2 / 3 / 4;退出码 1 的判定(从 AI 文本中识别失败)在 Batch B 实现,初版可统一归 0。
|
||||
|
||||
### 3.3 核心实现(Batch A,约 60~80 行增量)
|
||||
|
||||
**改动点 1:`parse_args()`(aibox.py L2786)** 追加 §3.1 的 5 个参数定义。
|
||||
|
||||
**改动点 2:新增 `handle_once_mode()`**,插入 `main()` 中 `handle_memo_commands()` 之后、`manager.run()` 之前:
|
||||
|
||||
```python
|
||||
def handle_once_mode(manager, args) -> bool:
|
||||
"""一次性对话模式:命中 --once 则执行并返回 True(进程退出)。"""
|
||||
if not args.once:
|
||||
return False
|
||||
|
||||
ai = manager.add_ai()
|
||||
ai.history_manager = None # 单次模式不加载/不写对话历史
|
||||
if args.no_save_memory:
|
||||
ai.memory_db = None # 工具注册时已注入;此处断引用防止运行期写入
|
||||
# 注:Batch A 中 memory 工具已在 _register_tools 注册;
|
||||
# 完整禁用见 §3.4 说明
|
||||
|
||||
exit_code = [0]
|
||||
|
||||
def cli_callback(msg_type, content):
|
||||
if msg_type == "chunk":
|
||||
sys.stdout.write(content)
|
||||
sys.stdout.flush()
|
||||
elif msg_type == "complete":
|
||||
sys.stdout.write("\n")
|
||||
sys.stdout.flush()
|
||||
elif msg_type == "error":
|
||||
print(content, file=sys.stderr)
|
||||
exit_code[0] = 2
|
||||
elif msg_type == "line" and not args.quiet:
|
||||
sys.stderr.write(content)
|
||||
sys.stderr.flush()
|
||||
elif msg_type.startswith("reasoning") and args.show_reasoning:
|
||||
sys.stderr.write(content)
|
||||
sys.stderr.flush()
|
||||
# ai_break / dialogue_ended 忽略
|
||||
|
||||
# Ctrl+C → 三层取消(与 WebSocket interrupt 路径等价)
|
||||
def _sigint(sig, frame):
|
||||
manager.cancel()
|
||||
signal.signal(signal.SIGINT, _sigint)
|
||||
|
||||
# 全局超时:守护线程到点 cancel
|
||||
timer = threading.Timer(args.timeout, manager.cancel)
|
||||
timer.daemon = True
|
||||
timer.start()
|
||||
try:
|
||||
response = ai.think_and_respond(args.once, cli_callback)
|
||||
if response is None or (isinstance(response, str)
|
||||
and response.startswith(f"\n{i18n.get('error_prefix')}")):
|
||||
exit_code[0] = max(exit_code[0], 2)
|
||||
# 被 cancel 打断时 think_and_respond 返回 interrupt 文案
|
||||
if ai._is_cancelled() and timer.is_alive() is False:
|
||||
exit_code[0] = 4
|
||||
finally:
|
||||
timer.cancel()
|
||||
ai.cancel() # 兜底清理子进程
|
||||
|
||||
sys.exit(exit_code[0])
|
||||
```
|
||||
|
||||
**改动点 3:`main()` 一行插入**:
|
||||
|
||||
```python
|
||||
if handle_memo_commands(manager):
|
||||
return
|
||||
|
||||
if handle_once_mode(manager, args): # 新增
|
||||
return
|
||||
|
||||
manager.run()
|
||||
```
|
||||
|
||||
> 注:`handle_once_mode` 内部 `sys.exit()`,return 分支实际不可达,保留 return 仅为与 `handle_check_commands` / `handle_memo_commands` 模式一致。
|
||||
|
||||
### 3.4 记忆写入的处理
|
||||
|
||||
`--no-save-memory` 的**完整实现**需要在工具注册层禁用 `MemoryTool`,但 `_register_tools` 依赖构造期注入的 `memory_db`。Batch A 采用轻量方案:
|
||||
|
||||
- **Batch A**:仅断 `ai.memory_db = None` 引用 + 文档声明"`--once` 场景不建议依赖记忆";AI 若调用 `memory` 工具会因注入的 db 为 None 由工具自身报错兜底(`MemoryTool.execute` 内部访问 db 返回错误字符串,不会崩溃)
|
||||
- **Batch B**:`DeepSeekChat.__init__` 增加 `enable_memory: bool = True` 形参,为 False 时 `_register_tools` 跳过 `MemoryTool` 注册,工具 schema 不出现在请求中,LLM 无从调用——这才是干净方案
|
||||
|
||||
### 3.5 与既有架构约束的对齐
|
||||
|
||||
| 项目硬约束 | 本方案如何满足 |
|
||||
|-----------|---------------|
|
||||
| WebSocket localhost:8765 通信 | 不受影响;`--once` 不启动 WebSocket,互斥使用 |
|
||||
| Worker 仅 7 种执行工具 | 不受影响;单次模式沿用 orchestrator profile,工具分层不变 |
|
||||
| 工具输出 ≤1MB + 超时中断 | 不受影响;复用 `SubprocessCommandExecutor` |
|
||||
| 单向依赖 common←subsystems←tools←aibox | 满足;改动全部在 aibox.py 聚合层 |
|
||||
| 新增/变更模块三处同步(根README/docs/MODULES/ARCHITECTURE) | 本功能非新模块,但落地时需同步更新 `aibox/README.md` 的"命令行参数"章节 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 分批次实施计划
|
||||
|
||||
遵循项目"增量重构、低风险先行"惯例:
|
||||
|
||||
### Batch A:最小可用版(核心,建议先做)
|
||||
|
||||
- [ ] `parse_args()` 新增 `--once` / `--quiet` / `--timeout` 三个参数
|
||||
- [ ] 新增 `handle_once_mode()`(§3.3 实现)
|
||||
- [ ] `main()` 插入调用点
|
||||
- [ ] SIGINT handler + 全局超时 Timer
|
||||
- [ ] 退出码 0/2/3/4
|
||||
- [ ] **验收**:`python aibox.py --once "现在几点了"` 正常输出并退出;`echo $?` 为 0;WebSocket 模式回归(启动服务器 + client.py 对话一轮)
|
||||
|
||||
### Batch B:体验与边界补齐
|
||||
|
||||
- [ ] `--no-save-memory` / `--show-reasoning` 参数落地
|
||||
- [ ] `DeepSeekChat.__init__` 增加 `enable_memory` 开关,干净禁用 MemoryTool
|
||||
- [ ] `_think_and_respond_impl` L1346 压缩提示 `print` 改走 stderr(或注入 print 回调)
|
||||
- [ ] 退出码 1(工具失败识别)
|
||||
- [ ] 补充 `tests/test_once_mode.py`:mock LLM 客户端,验证 stdout/stderr 分流、退出码、超时路径
|
||||
- [ ] 更新 `aibox/README.md` 命令行参数章节
|
||||
|
||||
### Batch C:安全加固(可选,按需)
|
||||
|
||||
- [ ] SQLite WAL 模式(`PRAGMA journal_mode=WAL`),缓解多 CLI 进程并发写锁等待
|
||||
- [ ] `--worker-only` 开关:`tool_profile=worker` 纯执行面模式,供其他模块以子进程方式调用 AI 执行器(为"Worker 进程级隔离"演进铺路)
|
||||
- [ ] `--model` / `--temperature` 覆盖参数
|
||||
|
||||
### 明确不做(本期)
|
||||
|
||||
- 多轮交互式 CLI REPL(`client.py` 已覆盖)
|
||||
- HTTP 单次接口
|
||||
- Worker 子进程化(架构演进议题,单独立项)
|
||||
|
||||
---
|
||||
|
||||
## 5. 风险清单
|
||||
|
||||
| 风险 | 等级 | 缓解 |
|
||||
|------|------|------|
|
||||
| 冷启动开销(PromptManager 加载 + SQLite 初始化 + LLM 握手 ≈1~3s) | 低 | 可接受取舍;批量场景文档引导用 WebSocket 长连接 |
|
||||
| CLI 模式与 WebSocket 模式代码 drift | 中 | 复用 `EnhancedAIManager.add_ai()`,单次模式新增代码控制在 80 行内,核心对话逻辑零复制 |
|
||||
| `print` 污染 stdout 破坏管道 | 中 | Batch A 用 `--quiet` 兜底;Batch B 根治压缩提示 print |
|
||||
| 并发进程 SQLite 写锁 | 低 | 低频无碍;Batch C WAL 加固 |
|
||||
| Ctrl+C 时子进程泄漏 | 低 | 复用现成三层取消;`finally` 中 `ai.cancel()` 兜底 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 测试方案
|
||||
|
||||
### 6.1 单元测试(tests/test_once_mode.py,Batch B)
|
||||
|
||||
| 用例 | 验证点 |
|
||||
|------|--------|
|
||||
| 正常问答 | mock LLM 返回纯文本 → stdout 含回答、退出码 0 |
|
||||
| 工具调用链 | mock LLM 返回 tool_calls → line 走 stderr、chunk 走 stdout |
|
||||
| API 失败 | mock LLM 抛 RequestException → 退出码 2、stderr 含错误 |
|
||||
| 超时 | mock LLM 阻塞 → Timer 触发 cancel → 退出码 4 |
|
||||
| SIGINT | 发送 SIGINT → 三层取消触发 → 退出码 3 |
|
||||
| `--quiet` | 过程行不输出到任何流 |
|
||||
| `--no-save-memory` | memories.db 无新增行 |
|
||||
|
||||
### 6.2 回归测试
|
||||
|
||||
- [ ] `py -m pytest aibox/tests -q` 现有 232 项全过
|
||||
- [ ] WebSocket 模式手测:启动服务器 + `client.py` 完成一轮对话 + Ctrl+C 中断
|
||||
|
||||
### 6.3 手动验收脚本
|
||||
|
||||
```bash
|
||||
# 基础问答(无工具)
|
||||
python aibox.py --once "用一句话介绍你自己" ; echo "exit=$?"
|
||||
|
||||
# 工具调用链(会派生 worker 子代理执行命令)
|
||||
python aibox.py --once "在 ~/.aibox/workspace 下创建一个 hello.txt 写入 hello" ; echo "exit=$?"
|
||||
|
||||
# 管道组合
|
||||
python aibox.py --once --quiet "列出当前目录 py 文件" | grep ".py" | wc -l
|
||||
|
||||
# 超时路径
|
||||
python aibox.py --once --timeout 5 "运行 sleep 60" ; echo "exit=$?" # 期望 4
|
||||
```
|
||||
|
||||
## 7. 实施状态(2026-08-17 落地记录)
|
||||
|
||||
> 本节为实施后回填,与第 3/4 节计划存在少量偏差,以本节为准。
|
||||
|
||||
### 7.1 提交与批次
|
||||
|
||||
| 批次 | 提交 | 内容 |
|
||||
|---|---|---|
|
||||
| Batch A | `ce3fb7f` | `--once` / `--quiet` / `--once-timeout`(300s 默认)+ `handle_once_mode` + SIGINT/超时复用三层取消 |
|
||||
| Batch B | `9e0d39c` | `--no-save-memory`(`enable_memory` 构造开关干净禁用 MemoryTool)、`--show-reasoning`(思考流默认静默走 stderr)、压缩提示改走 output_callback、退出码 1(orchestrator 层工具异常)+ `test_once_mode.py` 10 项 |
|
||||
| Batch C | `a5af4bb` | SQLite WAL、`--worker-only`(worker profile + 精简 system prompt 防人格污染)、`--model` / `--temperature`、子代理失败标记透传(`_SUBAGENT_TOOL_FAILED_MARKER` → orchestrator `stats.tool_failed`)+ 5 项测试 |
|
||||
| Batch D | `d03dbc7` | 多模型档案 `models` 配置节(本档案为实施中新增需求,超出原计划范围)+ `test_models_profile.py` 17 项 + README 更新 |
|
||||
|
||||
### 7.2 与计划的偏差
|
||||
|
||||
1. **超时参数名**:计划 `--timeout`,落地为 `--once-timeout`(避免与 `api.timeout` 语义混淆)
|
||||
2. **`models` 默认留空 `{}`**:计划未涉及;实施中发现默认档案硬编码 `DEEPSEEK_API_KEY` / 官方 URL 会覆盖用户自定义配置(本机为 `ARK_API_KEY` + 火山方舟端点,曾导致 401),故默认空 dict,未配置档案时零破坏回退旧 `api` 扁平节
|
||||
3. **`--worker-only` 需要精简 system prompt**:原计划仅切 tool_profile;实测 orchestrator 的人格/身份提示词会污染 worker 语义(自称"老板"、主动写文件、多轮不收敛),已对齐 `run_full_subagent` 的子代理语义修复
|
||||
4. **退出码 1 的边界**:仅覆盖 orchestrator 直接感知的工具异常;worker 子代理内的命令失败经 Batch C 的标记透传后同样计入。子命令失败但 AI 正常汇报时退出码为 0(AI 视角任务完成)
|
||||
|
||||
### 7.3 验证结果
|
||||
|
||||
- 单测 **264 项全过**(once 模式 15 + 多模型档案 17 新增)
|
||||
- 冒烟:基础问答 exit 0 / 工具调用链(子代理执行 echo)exit 0 / 超时路径 exit 4 / `--no-save-memory --quiet` stdout 干净 / `--show-reasoning` 思考流走 stderr / `--temperature` 透传 / 不存在的模型名 exit 2
|
||||
|
||||
### 7.4 明确不做(维持原判)
|
||||
|
||||
WebSocket 会话恢复、`--once` 结果缓存、批处理多 query——见第 4 节"明确不做"。
|
||||
|
||||
---
|
||||
|
||||
## 8. 后续演进方向(不在本期)
|
||||
|
||||
1. **Worker 进程级隔离**:`run_full_subagent` 的线程内 `DeepSeekChat` → `subprocess.run(["python", "aibox.py", "--once", "--worker-only", ...])`,子代理崩溃不波及主服务,可用 OS 用户/chroot 做权限隔离
|
||||
2. **HTTP 单次接口**:`POST /chat` 无状态接口,与 `--once` 共享内核
|
||||
3. **结构化输出**:`--json` 输出 `{answer, tool_calls, exit_reason}` JSON,供程序消费
|
||||
|
||||
---
|
||||
|
||||
## 9. 决策记录
|
||||
|
||||
| 决策点 | 结论 | 理由 |
|
||||
|--------|------|------|
|
||||
| 新入口文件还是复用 aibox.py | 复用 aibox.py + `--once` 参数 | 沿用 `--check-memo` 先例;避免维护两份入口 drift |
|
||||
| stdout 输出协议 | chunk 流式 + 末尾换行 | 与 WebSocket 的 chunk 权威语义一致;管道可逐段消费 |
|
||||
| 过程行去向 | stderr | Unix 管道惯例:stdout 是数据、stderr 是元信息 |
|
||||
| 单次模式是否加载历史 | 否 | "一次性"语义明确;避免历史污染导致行为不可复现 |
|
||||
| 是否默认写记忆 | 默认与 orchestrator 一致(可写),`--no-save-memory` 显式关闭 | 与 WebSocket 模式行为对齐,减少心智负担 |
|
||||
@@ -1,183 +0,0 @@
|
||||
# aibox 文件操作与网络操作命令行化 开发计划
|
||||
|
||||
## 目标
|
||||
|
||||
将 aibox 中所有 Python 原生实现的文件操作和网络操作改为通过系统命令行工具实现,统一工具执行模式,减少 Python 依赖。
|
||||
|
||||
## 涉及模块
|
||||
|
||||
| 现有类 | 作用 | 替换方案 |
|
||||
|--------|------|----------|
|
||||
| `FileToolBase` | 文件工具基类 | 简化为命令行工具基类 |
|
||||
| `ReadFileTool` | 读文件 | `cat` / `head` / `tail` / `wc` |
|
||||
| `WriteFileTool` | 写文件 | `mkdir -p` + shell 重定向 |
|
||||
| `ListFilesTool` | 列目录 | `ls` / `find` / `du` |
|
||||
| `SimpleWebClient` | HTTP 客户端 | `curl` |
|
||||
| `WebPage` | 网页解析对象 | 移除,curl + grep/sed 替代 |
|
||||
| `SimpleWebCrawlerTool` | 网页爬虫工具 | `curl` + 文本处理管道 |
|
||||
|
||||
## 阶段一:文件操作命令行化
|
||||
|
||||
> **实现说明(已落地)**:阶段一采用「工具返回 markdown 提示词」的方式而非工具内部调子进程。`read_file` / `write_file` / `list_files` 三个工具的 `execute()` 不再执行 Python 文件 I/O,而是返回一段 markdown 提示词,指导智能体改用 `run_command` 工具执行对应的命令行命令(`head`/`tail`/`sed`/`wc`/`file`/`cut`/`cat`/`ls`/`du`/`find`)。工具接口(名称与参数)保持不变,安全限制(500 行、10KB/行、10MB 文件、路径 `shlex.quote` 转义)在提示词中注明。
|
||||
|
||||
### 1.1 ReadFileTool 重构
|
||||
|
||||
用以下命令行工具替代 Python 文件 I/O:
|
||||
|
||||
| 原功能 | 命令行实现 |
|
||||
|--------|-----------|
|
||||
| `head=True` 读取开头 N 行 | `head -n N` |
|
||||
| `tail=True` 读取末尾 N 行 | `tail -n N` |
|
||||
| 指定行范围 `start_line ~ end_line` | `sed -n 'start,endp'` |
|
||||
| 统计行数 `_count_lines` | `wc -l` |
|
||||
| 文件编码检测 | `file -b --mime-encoding` |
|
||||
| 单行超长截断 | `cut -c 1-10000` |
|
||||
|
||||
保留的安全限制:
|
||||
- 文件大小限制 `MAX_FILE_SIZE`:通过 `wc -c` 或 `stat -f%z` 检查
|
||||
- 行数上限 500 行:`head -n 500` 作为最后一道防线
|
||||
- 单行 10KB 限制:`cut -c 1-10000`
|
||||
|
||||
### 1.2 WriteFileTool 重构
|
||||
|
||||
| 原功能 | 命令行实现 |
|
||||
|--------|-----------|
|
||||
| 创建目录 | `mkdir -p` |
|
||||
| 覆盖写入 `mode=w` | `content > filepath` |
|
||||
| 追加写入 `mode=a` | `content >> filepath` |
|
||||
| 检查文件大小 | `wc -c` 或 `stat -f%z` |
|
||||
| 路径安全检查 | `realpath` / `test -f` |
|
||||
|
||||
### 1.3 ListFilesTool 重构
|
||||
|
||||
| 原功能 | 命令行实现 |
|
||||
|--------|-----------|
|
||||
| 列出目录内容 | `ls -la` 或 `find . -maxdepth 1` |
|
||||
| 隐藏文件过滤 | `ls -la` 默认不显示 `.` 开头的隐藏文件(或 `find` 排除) |
|
||||
| 文件大小统计 | `du -sh *` 或 `ls -lh` |
|
||||
| 目录/文件区分 | `ls -F` 或 `find -type f/d` |
|
||||
|
||||
### 1.4 FileToolBase 简化
|
||||
|
||||
移除 `normalize_path()`、`check_path_safety()` 中 Python `os` 模块调用,改为:
|
||||
- 路径规范化:`realpath`
|
||||
- 文件存在性检查:`test -f` / `test -d`
|
||||
- 文件大小检查:`wc -c` 或 `stat`
|
||||
|
||||
### 1.5 安全边界保留
|
||||
|
||||
- `MAX_FILE_SIZE`(默认 10MB):通过命令检查
|
||||
- 行数上限 500 行:`head -n 500` 兜底
|
||||
- 单行长度上限 10KB:`cut -c 1-10000` 兜底
|
||||
- 所有路径参数 `shlex.quote()` 转义
|
||||
|
||||
## 阶段二:网络操作命令行化
|
||||
|
||||
> **实现说明(已落地)**:阶段二沿用阶段一的方式,`web` 工具(`SimpleWebCrawlerTool`)的 `execute()` 不再使用 `requests`/`BeautifulSoup`,而是返回 markdown 提示词,指导智能体改用 `run_command` 执行 `curl` 命令完成各 action(`get`/`text`/`title`/`links`/`search`)。`WebPage`、`SimpleWebClient` 类已删除;`urljoin`/`urlparse`/`BeautifulSoup` 导入已清理。代理通过 `get_proxy()` 读取配置并拼出 `--socks5` 参数。注意 `requests` 仍在 `aibox.py` 其他位置(API 调用、DeepSeek 请求)使用,故保留其依赖。
|
||||
|
||||
### 2.1 SimpleWebClient → curl
|
||||
|
||||
| 原功能 | 命令行实现 |
|
||||
|--------|-----------|
|
||||
| HTTP GET 请求 | `curl -sL --max-time N` |
|
||||
| 自定义 User-Agent | `curl -A "..."` |
|
||||
| SOCKS5 代理 | `curl --socks5 host:port` 或环境变量 `ALL_PROXY` |
|
||||
| 请求间隔控制 | `SubprocessCommandExecutor` 层面控制 |
|
||||
| 响应状态码 | `curl -sL -o /dev/null -w '%{http_code}'` |
|
||||
| 下载大小限制 | `curl --max-filesize N` 或管道 `head -c N` |
|
||||
|
||||
### 2.2 WebPage 移除
|
||||
|
||||
`WebPage` 类不再需要,其功能由命令行管道实现:
|
||||
|
||||
| 原属性/方法 | 命令行实现 |
|
||||
|------------|-----------|
|
||||
| `title` | `curl -sL | grep -oP '<title>\K[^<]+'` |
|
||||
| `text` | `curl -sL | lynx -dump -stdin` 或 `html2text` |
|
||||
| `links` | `curl -sL | grep -oP 'href="[^"]*"'` |
|
||||
| `status_code` | `curl -sL -o /dev/null -w '%{http_code}'` |
|
||||
| `headers` | `curl -sL -I` |
|
||||
|
||||
### 2.3 SimpleWebCrawlerTool 重构
|
||||
|
||||
| action | 命令行实现 |
|
||||
|--------|-----------|
|
||||
| `get` | `curl -sL` 获取完整页面信息 |
|
||||
| `text` | `curl -sL | lynx -dump -stdin` 或 `html2text` |
|
||||
| `title` | `curl -sL | grep -oP '<title>\K[^<]+'` |
|
||||
| `links` | `curl -sL | grep -oP 'href="[^"]*"'` |
|
||||
| `search` | `curl -sL | grep -c 'keyword'` 或 `grep -q` |
|
||||
|
||||
### 2.4 下载大小限制
|
||||
|
||||
- 响应体:`curl --max-filesize` 或 `curl | head -c 100M` 管道限制
|
||||
- 配置项 `web.max_download_size`(默认 100MB)
|
||||
|
||||
## 阶段三:清理与收尾
|
||||
|
||||
> **实现说明(已落地)**:
|
||||
> - 3.1 `requirements.txt` 已移除 `beautifulsoup4`;**`requests` 保留**(仍在 `aibox.py` 的 DeepSeek API 调用 `ConversationCompressor` 与 `DeepSeekChat` 中使用,属核心功能,无法移除)。
|
||||
> - 3.2 已统一:文件/网页工具改为返回 markdown 提示词,实际 I/O 全部委托给 `run_command`(`SubprocessCommandExecutor`),天然复用其 1MB 输出上限、超时与中断取消。
|
||||
> - 3.3 中断支持已随 `SubprocessCommandExecutor.cancel()` 覆盖。
|
||||
> - 3.4 默认提示词 `3_file_operations.md`、`4_web_access.md` 已更新为命令行走指导(`head`/`sed`/`cat`/`ls`/`curl` 等)。
|
||||
> - 3.5 `README.md` 工具表与安全说明已更新。
|
||||
|
||||
### 3.1 移除 Python 依赖
|
||||
|
||||
从 `requirements.txt` 删除:
|
||||
|
||||
```diff
|
||||
- requests
|
||||
- beautifulsoup4
|
||||
```
|
||||
|
||||
### 3.2 统一使用 SubprocessCommandExecutor
|
||||
|
||||
所有新工具统一通过 `SubprocessCommandExecutor` 执行命令,与 `CommandLineTool` 保持一致:
|
||||
- 输出上限 1MB(超出自动终止进程并截断)
|
||||
- 超时控制
|
||||
- 中断取消(`cancel()` → `process.kill()`)
|
||||
|
||||
### 3.3 中断支持
|
||||
|
||||
- 利用已有的 `SubprocessCommandExecutor.cancel()` 机制
|
||||
- 确保 Ctrl+C 可终止长时间运行的 curl 下载或文件操作
|
||||
|
||||
### 3.4 更新提示词
|
||||
|
||||
`~/.aibox/prompts/` 中相关文件更新:
|
||||
|
||||
| 文件 | 更新内容 |
|
||||
|------|---------|
|
||||
| `3_file_operations.md` | 文件操作描述改为命令行方式 |
|
||||
| `4_web_access.md` | 网络访问描述改为 curl 方式 |
|
||||
|
||||
### 3.5 更新文档
|
||||
|
||||
- `SKILLS_FORMAT.md`:技能开发示例中文件/网络操作更新
|
||||
- `README.md`:更新依赖说明和架构图
|
||||
|
||||
## 不做的事
|
||||
|
||||
- **不改变工具接口**:`read_file`、`write_file`、`list_files`、`web` 四个工具名称和参数签名保持不变
|
||||
- **不改变 `CommandLineTool`(`run_command`)**:已有的命令执行工具不动
|
||||
- **不改变 `SubprocessCommandExecutor`**:已有安全限制(1MB 输出)保持不变
|
||||
- **不改变记忆/备忘录数据库**:`MemoryDatabase`、`MemoDatabase` 的 SQLite 操作保留
|
||||
|
||||
## 收益
|
||||
|
||||
- **减少依赖**:移除 `requests`、`beautifulsoup4` 两个 Python 库,降低供应链风险
|
||||
- **统一执行模式**:所有外部操作(文件、网络、命令)全部走 `SubprocessCommandExecutor`,统一超时控制、输出限制、中断取消
|
||||
- **更强的可审计性**:AI 操作日志中能看到完整的 `curl` / `cat` / `ls` 命令,更透明
|
||||
- **复用已有基础设施**:`CommandLineTool` 已有的安全防护直接复用
|
||||
|
||||
## 风险与注意事项
|
||||
|
||||
| 风险 | 缓解措施 |
|
||||
|------|----------|
|
||||
| `curl` 不存在 | 启动时检测 `curl` 可用性,缺失时尝试 `wget` 降级或报错 |
|
||||
| 跨平台 `sed`/`grep` 差异 | macOS (BSD) 和 Linux (GNU) 行为差异;使用 POSIX 兼容写法,必要时运行时检测 |
|
||||
| 网页解析精度下降 | `grep` 正则提取 HTML 不可靠;优先用 `lynx -dump` 或 `html2text` 替代 BeautifulSoup 解析 |
|
||||
| 命令注入风险 | 所有用户输入参数必须经过 `shlex.quote()` 转义 |
|
||||
| Proxy 配置 | `curl` 通过 `--socks5` 或 `ALL_PROXY` 环境变量传递,与现有 `set_proxy` 工具集成 |
|
||||
| 性能影响 | 文件操作每步 fork 新进程,但 aibox 不是高吞吐服务,IO 瓶颈远大于进程开销 |
|
||||
@@ -1,184 +0,0 @@
|
||||
# aibox 编排者-执行者工具分层实施方案
|
||||
|
||||
> 状态:待实施 · 日期:2026-08-13 · 基线:aibox v2.8(tools 包抽离后,aibox.py 7753 行)
|
||||
> 前置:`14db64f refactor(aibox): 工具基类 Tool/ToolRegistry 抽离至 tools 包`
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
当前 aibox 主对话与完整子代理(`run_full_subagent`)注册**同一份全量工具集**(21 个工具),带来三个问题:
|
||||
|
||||
1. **安全隐患**:子代理可调用 `update_soul` / `update_identity` 改写主人格文件,且携带 `create_subagent` 可无限递归派生。
|
||||
2. **上下文膨胀**:主对话 system prompt 携带全部 21 个工具的 schema 与使用说明,token 成本高。
|
||||
3. **职责混杂**:命令执行、记忆管理、情绪表达交织在同一上下文中,长任务易污染主对话。
|
||||
|
||||
**目标架构**(编排者-执行者分层):
|
||||
|
||||
- **主对话(orchestrator)**:只保留情绪管理、记忆管理、任务调度、子代理派生等"管理面"工具,**不直接接触命令行**。
|
||||
- **子代理(worker)**:唯一拥有命令行/文件/网页等"执行面"工具的角色,独立上下文,无记忆/人格/派生能力。
|
||||
- 轻量子代理(`run_light_subagent`,供 `express_emotion` 使用)保持不变。
|
||||
|
||||
## 2. 现状分析(关键代码位置)
|
||||
|
||||
| 位置 | 说明 |
|
||||
|------|------|
|
||||
| `aibox.py:5667` `DeepSeekChat.__init__` | 无工具集参数,构造即调 `_register_tools` |
|
||||
| `aibox.py:5766` `_register_tools()` | 无条件注册全部 21 个内置工具 + `search_skills` |
|
||||
| `aibox.py:5392` `run_full_subagent()` | 创建完整 `DeepSeekChat(system_prompt=sub_role, ...)`,子代理因此获得全量工具(含 `update_soul`/`create_subagent`)|
|
||||
| `aibox.py:5363` `run_light_subagent()` | 无工具,单次 LLM 补全,供 `ExpressEmotionTool`(:3408) 使用 |
|
||||
| `aibox.py:5524` `SubAgentTool` | `create_subagent` 工具,调 `run_full_subagent` |
|
||||
| `~/.aibox/prompts/` | 运行期提示词(7_extension_skills.md 等指导 AI 直接用 `run_command` 执行技能)|
|
||||
|
||||
## 3. 工具分配方案
|
||||
|
||||
### orchestrator(主对话)— 13 个工具
|
||||
|
||||
| 工具 | 保留理由 |
|
||||
|------|---------|
|
||||
| `memory` / `memo` / `save_memory_and_end_conversation` | 记忆与任务管理是主代理核心职责 |
|
||||
| `express_emotion` / `ask_user` | 情绪管理与用户交互只在主对话有意义 |
|
||||
| `create_subagent` | 唯一的执行入口 |
|
||||
| `search_skills` | 只查不执行,用于路由决策(查到技能后派 worker 执行)|
|
||||
| `reload_skills` / `install_skill` / `create_skill` | 技能管理面;后两者为指引型工具,实际文件写入通过 worker 完成 |
|
||||
| `update_soul` / `update_identity` | 人格进化收敛到主代理(同时消除子代理越权隐患)|
|
||||
| `set_proxy` / `get_proxy` | 代理配置影响 worker 的 web/curl,归管理面 |
|
||||
| `get_current_time` | 任务调度/记忆时间戳需要 |
|
||||
|
||||
### worker(子代理)— 7 个工具
|
||||
|
||||
| 工具 | 说明 |
|
||||
|------|------|
|
||||
| `run_command` | 命令行执行(含技能脚本执行)|
|
||||
| `read_file` / `write_file` / `list_files` | 文件操作三件套(指引型)|
|
||||
| `web` | 网页访问指引 |
|
||||
| `calculator` / `get_current_time` | 执行辅助 |
|
||||
|
||||
### 明确不给 worker 的工具
|
||||
|
||||
`memory` / `memo`(记忆隔离)、`update_soul` / `update_identity`(人格保护)、`create_subagent`(断递归)、`express_emotion`、`ask_user`(**worker 不得向用户提问**,疑问写入返回结果)。
|
||||
|
||||
## 4. 改动清单
|
||||
|
||||
### 4.1 `aibox.py` — 工具 profile 参数化(核心改动)
|
||||
|
||||
**a) 模块级常量**(`DeepSeekChat` 类定义前):
|
||||
|
||||
```python
|
||||
# 工具集 profile:orchestrator=主对话管理面,worker=子代理执行面
|
||||
TOOL_PROFILE_ORCHESTRATOR = "orchestrator"
|
||||
TOOL_PROFILE_WORKER = "worker"
|
||||
```
|
||||
|
||||
**b) `DeepSeekChat.__init__`(:5667)** 增加参数 `tool_profile: str = TOOL_PROFILE_ORCHESTRATOR`,保存为 `self.tool_profile`,`_register_tools` 调用处透传。
|
||||
|
||||
**c) `_register_tools()`(:5766)** 按 profile 分支装配:
|
||||
|
||||
```python
|
||||
def _register_tools(self, exit_callback=None, command_executor=None):
|
||||
if self.tool_profile == TOOL_PROFILE_WORKER:
|
||||
builtin_tools = [
|
||||
CommandLineTool(command_executor or SubprocessCommandExecutor(self.logger), self.logger),
|
||||
ReadFileTool(self.logger),
|
||||
WriteFileTool(self.logger),
|
||||
ListFilesTool(self.logger),
|
||||
SimpleWebCrawlerTool(self.logger),
|
||||
CalculatorTool(self.logger),
|
||||
GetCurrentTimeTool(self.logger),
|
||||
]
|
||||
self.tool_registry.register_many(builtin_tools, is_builtin=True)
|
||||
return # worker 不需要技能管理器与 search_skills
|
||||
|
||||
# —— 以下为 orchestrator 分支,即现有装配逻辑删去执行面 7 工具 ——
|
||||
builtin_tools = [GetCurrentTimeTool(self.logger)]
|
||||
# memory/memo/update_soul/update_identity/save_memory/ask_user/
|
||||
# express_emotion/reload_skills/install_skill/create_skill/
|
||||
# set_proxy/get_proxy/SubAgentTool 保持现有条件注册逻辑
|
||||
...
|
||||
```
|
||||
|
||||
注意:orchestrator 分支从现有列表(:5768-5807)中移除 `CalculatorTool`、`CommandLineTool`、`ReadFileTool`、`WriteFileTool`、`ListFilesTool`、`SimpleWebCrawlerTool`,其余不变。
|
||||
|
||||
**d) `run_full_subagent()`(:5392)** 创建子实例时传 `tool_profile=TOOL_PROFILE_WORKER`:
|
||||
|
||||
```python
|
||||
sub_chat = DeepSeekChat(
|
||||
system_prompt=sub_role,
|
||||
tool_profile=TOOL_PROFILE_WORKER, # 新增
|
||||
command_executor=SubprocessCommandExecutor(),
|
||||
...
|
||||
)
|
||||
```
|
||||
|
||||
### 4.2 子代理角色提示词补强(`run_full_subagent` 内 `sub_role` 拼接)
|
||||
|
||||
现有结尾指令("任务完成后直接给出最终回答")追加 worker 行为约束:
|
||||
|
||||
```
|
||||
【约束】你没有记忆/提问/派生能力。遇到不确定的信息,用工具自行查证;
|
||||
确实无法完成时,在最终回答中明确说明原因与已完成的部分。
|
||||
产物文件一律写入 ~/.aibox/workspace/ 并在回答中给出绝对路径。
|
||||
```
|
||||
|
||||
### 4.3 运行期提示词调整(`~/.aibox/prompts/`,部署后手工更新)
|
||||
|
||||
- `99_SOUL.md`:角色定位改为"编排者"——拆解任务、派生子代理执行、汇总结果;明确自己**不直接执行命令**。
|
||||
- `7_extension_skills.md`:技能执行路径从"直接 run_command"改为"`search_skills` 查到技能 → `create_subagent` 派 worker 执行 → 汇总结论"。
|
||||
- `6_command_execution.md`:改为说明命令执行须经子代理(或直接停用该提示词)。
|
||||
- `10_task_handling.md`:定时任务执行同样走子代理路径。
|
||||
|
||||
> 提示词为运行期数据不在仓库内,实施时在部署机 `~/.aibox/prompts/` 修改,并用 `/reload_prompts` 生效。
|
||||
|
||||
### 4.4 文档更新
|
||||
|
||||
- `aibox/README.md`:工具表按 orchestrator/worker 重新分组;"架构流程"图补子代理层;更新日志加 v2.9 条目。
|
||||
|
||||
## 5. 已知限制与风险
|
||||
|
||||
| 风险 | 说明 | 对策 |
|
||||
|------|------|------|
|
||||
| 延迟与成本上升 | 每次执行多一跳 LLM + 结束摘要(`_summarize_subagent_conversation`) | 简单问答主代理直接回答不派生;提示词明确"仅在需要执行时派生" |
|
||||
| 信息失真 | 主代理只见摘要(8000 字符截断) | worker 产物落盘 workspace,主代理用 `search_skills`/派生复查;后续可提升摘要上限 |
|
||||
| 中断不彻底 | worker 自建 `SubprocessCommandExecutor`(:5392 内),主代理 Ctrl+C 无法 kill worker 的子进程 | 现状已有该缺陷;后续改进:worker executor 注册到主 `_command_executor` 链或全局进程表 |
|
||||
| 任务调度器 | TaskScheduler 触发的对话为主代理,执行类任务须学会派生 | 提示词 10_task_handling.md 明确路径;上线后观察任务执行日志 |
|
||||
| 回退 | 行为变更较大 | 单提交实现,`git revert` 即可完整回退 |
|
||||
|
||||
## 6. 验证方案
|
||||
|
||||
```bash
|
||||
cd /opt/blackbird/aibox
|
||||
|
||||
# 1. 导入冒烟(系统 python 缺 websockets 时用桩模块)
|
||||
python3 -m py_compile aibox.py
|
||||
|
||||
# 2. profile 装配断言(桩掉 websockets/requests 后)
|
||||
python3 - <<'EOF'
|
||||
import sys, types
|
||||
for n in ("websockets",): sys.modules.setdefault(n, types.ModuleType(n))
|
||||
import aibox
|
||||
o = aibox.DeepSeekChat(tool_profile="orchestrator")
|
||||
w = aibox.DeepSeekChat(tool_profile="worker")
|
||||
on = sorted(t.get_name() for t in o.tool_registry.get_all_tools())
|
||||
wn = sorted(t.get_name() for t in w.tool_registry.get_all_tools())
|
||||
assert "run_command" not in on and "create_subagent" in on
|
||||
assert set(wn) == {"run_command","read_file","write_file","list_files",
|
||||
"web","calculator","get_current_time"}
|
||||
assert "update_soul" not in wn and "create_subagent" not in wn
|
||||
print("orchestrator:", on); print("worker:", wn)
|
||||
EOF
|
||||
```
|
||||
|
||||
(注:构造 `DeepSeekChat` 需 `DEEPSEEK_API_KEY`,`_setenv.sh` 已从 `~/.cert/deepseek.key` 导出。)
|
||||
|
||||
**端到端**(部署机):
|
||||
|
||||
1. `bash _run.sh` 启动,BotOps 连接。
|
||||
2. 问"帮我查看系统内存"→ 预期:主代理调 `create_subagent`,任务侧栏见 worker 执行 `free -h`,主代理汇总回答;主对话流中不出现 `run_command` 工具调用。
|
||||
3. 问"记住我喜欢喝绿茶"→ 预期:主代理直接调 `memory`,不派生子代理。
|
||||
4. 问"使用 nmapskills 技能扫描本机"→ 预期:`search_skills` → `create_subagent` 执行技能脚本。
|
||||
5. Ctrl+C 中断一个执行中的子代理任务,确认 `interrupt_ack` 与回滚正常。
|
||||
6. `/tasks` 添加即时任务"统计 ~/.aibox/workspace 文件数",确认调度器路径走子代理执行。
|
||||
|
||||
## 7. 后续可选扩展(不在本期)
|
||||
|
||||
- `config.json` 增加 `agent.worker_model`,worker 用更廉价模型降本。
|
||||
- worker 进程注册到全局取消链,实现彻底中断。
|
||||
- 摘要失真问题严重时可改为 worker 结果直通主对话(`task_process` 已有广播通道)。
|
||||
@@ -1,167 +0,0 @@
|
||||
# aibox 模块化重构进度报告
|
||||
|
||||
> 状态:A/B 批工具 + Wave 1 子系统已抽离 · 日期:2026-08-14 · 基线:aibox v3.0
|
||||
> 测试:171 项全过(10.79s)
|
||||
> 关联文档:
|
||||
> - [aibox_orchestrator_worker_plan.md](aibox_orchestrator_worker_plan.md)(v2.9 编排者-执行者分层方案)
|
||||
> - [aibox_open_dev_plan.md](aibox_open_dev_plan.md)(开放版安全裁剪方案)
|
||||
> - [aibox_SECURITY_AUDIT_20260801.md](../security/aibox_SECURITY_AUDIT_20260801.md)(安全审计基线)
|
||||
|
||||
## 1. 重构目标
|
||||
|
||||
将单文件 8450 行的 `aibox.py` 按职责拆分为多个内聚包,建立清晰的依赖方向与测试基线,降低维护成本、为后续工具/子系统扩展提供稳定骨架。
|
||||
|
||||
**目标架构**(单向依赖,禁止反向引用):
|
||||
|
||||
```
|
||||
common ← subsystems ← tools ← aibox.py
|
||||
```
|
||||
|
||||
| 层 | 职责 | 依赖 |
|
||||
|---|---|---|
|
||||
| `common/` | 配置、常量、i18n、日志 | 标准库 |
|
||||
| `subsystems/` | 命令执行器、提示词、数据库、技能管理 | `common` |
|
||||
| `tools/` | Tool 基类 + 17 个工具类 | `common` + `subsystems` |
|
||||
| `aibox.py` | 核心类聚合(DeepSeekChat/WebSocketHandler/EnhancedAIManager + 调度/压缩/历史) | 上述三包 |
|
||||
|
||||
## 2. 已完成工作
|
||||
|
||||
### 2.1 A 批基础工具抽离(`tools/basic_tools.py`,342 行)
|
||||
|
||||
7 个无外部子系统依赖的工具类:
|
||||
|
||||
| 类 | 工具名 | 说明 |
|
||||
|---|---|---|
|
||||
| `FileToolBase` | — | 文件操作基类(normalize_path / check_path_safety) |
|
||||
| `ReadFileTool` | `read_file` | 读文件指引(head/tail/sed) |
|
||||
| `WriteFileTool` | `write_file` | 写文件指引(heredoc) |
|
||||
| `ListFilesTool` | `list_files` | 列目录指引(ls) |
|
||||
| `SimpleWebCrawlerTool` | `web` | 网页访问指引(curl + 代理) |
|
||||
| `GetCurrentTimeTool` | `get_current_time` | 时间获取 |
|
||||
| `CalculatorTool` | `calculator` | 四则运算 |
|
||||
|
||||
**测试**:[tests/test_basic_tools.py](../../aibox/tests/test_basic_tools.py) 43 项全过
|
||||
|
||||
### 2.2 B 批管理面工具抽离(`tools/` 目录,共 13 类)
|
||||
|
||||
| 文件 | 类 | 工具名 |
|
||||
|---|---|---|
|
||||
| `command_tool.py` | `CommandLineTool` | `run_command` |
|
||||
| `skill_tools.py` | `SearchSkillsTool` / `ReloadSkillsTool` / `InstallSkillTool` / `CreateSkillTool` | `search_skills` / `reload_skills` / `install_skill` / `create_skill` |
|
||||
| `db_tools.py` | `MemoryTool` / `MemoTool` | `memory` / `memo` |
|
||||
| `prompt_tools.py` | `UpdateIdentityTool` / `UpdateSoulTool` / `SaveMemoryAndEndConversationTool` | `update_identity` / `update_soul` / `save_memory_and_end_conversation` |
|
||||
| `proxy_tools.py` | `SetProxyTool` / `GetProxyTool` | `set_proxy` / `get_proxy` |
|
||||
| `misc_tools.py` | `AskUserTool` | `ask_user` |
|
||||
|
||||
**测试**:[tests/test_b_tools.py](../../aibox/tests/test_b_tools.py) 58 项全过
|
||||
|
||||
### 2.3 Wave 1 子系统抽离(`subsystems/` 目录)
|
||||
|
||||
| 文件 | 类 | 行数 |
|
||||
|---|---|---|
|
||||
| `executors.py` | `CommandExecutor` + `SubprocessCommandExecutor` | 279 |
|
||||
| `prompt_manager.py` | `PromptManager`(单例,数字前缀排序加载) | 241 |
|
||||
| `db.py` | `MemoryDatabase` + `MemoDatabase`(SQLite) | 530 |
|
||||
| `skill_manager.py` | `SkillManager` | 186 |
|
||||
|
||||
**测试**:
|
||||
- [tests/test_executors.py](../../aibox/tests/test_executors.py)
|
||||
- [tests/test_prompt_manager.py](../../aibox/tests/test_prompt_manager.py)
|
||||
- [tests/test_db.py](../../aibox/tests/test_db.py)
|
||||
- [tests/test_skill_manager.py](../../aibox/tests/test_skill_manager.py)
|
||||
|
||||
共 70 项全过。
|
||||
|
||||
### 2.4 工具系统基础设施
|
||||
|
||||
- [tools/base.py](../../aibox/tools/base.py)(115 行):`Tool` 抽象基类,定义 `get_name` / `get_description` / `get_parameters` / `execute` / `execute_streaming` / `validate_params` / `format_result` 接口
|
||||
- [tools/registry.py](../../aibox/tools/registry.py)(135 行):`ToolRegistry` 注册器,支持热重载与流式输出分发
|
||||
- [tools/__init__.py](../../aibox/tools/__init__.py) / [subsystems/__init__.py](../../aibox/subsystems/__init__.py):采用"重导出"策略,外部统一用 `from tools import Tool, ReadFileTool`,调用点无需关心子模块位置
|
||||
|
||||
### 2.5 关键 Bug 修复
|
||||
|
||||
- **`subsystems/db.py` 的 `get_recent_memories(limit=None)` SQL datatype mismatch**:`LIMIT ?` 传 `None` 时 SQLite 报错;修复为 `limit` 为 `None` 时移除 `LIMIT` 子句
|
||||
- **`test_basic_tools.py` 路径漂移**(2026-08-14 修复):4 个测试用 `filepath="aibox.py"` 假设 pytest 从 `aibox/` 目录运行;改用 pytest `tmp_path` fixture 创建临时文件,测试不再依赖 cwd
|
||||
|
||||
## 3. 量化指标
|
||||
|
||||
| 指标 | 起点(v2.8) | 当前(v3.0) | 变化 |
|
||||
|---|---|---|---|
|
||||
| `aibox.py` 行数 | 8450 | 3706 | -56% |
|
||||
| 抽离工具类数 | 0 | 17 | +17 |
|
||||
| 抽离子系统数 | 0 | 4 | +4 |
|
||||
| 测试用例数 | 0 | 171 | +171 |
|
||||
| 测试运行时长 | — | 10.79s | — |
|
||||
|
||||
## 4. 测试套件现状
|
||||
|
||||
```
|
||||
aibox/tests/ (171 项,10.79s)
|
||||
├── test_basic_tools.py 43 项 A 批工具(文件/网页/时间/计算器)
|
||||
├── test_b_tools.py 58 项 B 批工具(13 个管理面工具)
|
||||
├── test_executors.py — 命令执行器
|
||||
├── test_prompt_manager.py — 提示词管理器
|
||||
├── test_db.py — 记忆/备忘录数据库
|
||||
└── test_skill_manager.py — 技能管理器
|
||||
```
|
||||
|
||||
运行命令:
|
||||
|
||||
```bash
|
||||
# 从仓库根目录
|
||||
py -m pytest aibox/tests -q
|
||||
```
|
||||
|
||||
## 5. 剩余工作(未抽离)
|
||||
|
||||
### 5.1 aibox.py 剩余类清单(3706 行)
|
||||
|
||||
| 类 | 起始行 | 批次 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `ConversationCompressor` | L70 | Wave 3 | 对话压缩器,依赖 `llm_client` |
|
||||
| `TaskScheduler` | L570 | Wave 2 | 任务调度器,依赖 `MemoDatabase` |
|
||||
| `ConversationHistory` | L986 | Wave 2 | 对话历史管理 |
|
||||
| `ExpressEmotionTool` | L1309 | C 批 | 情绪表达工具,依赖 `run_light_subagent` |
|
||||
| `SubAgentTool` | L1574 | C 批 | 子代理派生工具,依赖 `run_full_subagent` |
|
||||
| `DeepSeekChat` | L1700 | 核心类 | 保留入口(编排逻辑) |
|
||||
| `WebSocketHandler` | L2715 | 核心类 | 保留入口 |
|
||||
| `EnhancedAIManager` | L3377 | 核心类 | 保留入口 |
|
||||
|
||||
### 5.2 建议优先级
|
||||
|
||||
| 优先级 | 批次 | 内容 | 风险 |
|
||||
|---|---|---|---|
|
||||
| 高 | Wave 2 | `TaskScheduler` + `ConversationHistory` 抽离至 `subsystems/` | 低(依赖已抽离的 `MemoDatabase`) |
|
||||
| 中 | Wave 3 | `ConversationCompressor` 抽离至 `subsystems/` | 中(需评估与 `DeepSeekChat` 的循环依赖,参考 lessons learned) |
|
||||
| 中 | C 批 | `ExpressEmotionTool` + `SubAgentTool` 抽离至 `tools/` | 中(依赖 `DeepSeekChat` 的 `run_light_subagent` / `run_full_subagent`,需通过依赖注入或协议接口打破循环) |
|
||||
| 低 | 核心类 | `DeepSeekChat` / `WebSocketHandler` / `EnhancedAIManager` | 保留在 `aibox.py`,作为聚合入口 |
|
||||
|
||||
### 5.3 风险点
|
||||
|
||||
1. **`ConversationCompressor` 与 `DeepSeekChat` 的循环依赖**:压缩器需调用 LLM,而 `DeepSeekChat` 持有压缩器;需通过依赖注入或协议接口打破(参考 project_memory 中 lessons learned)
|
||||
2. **C 批工具依赖 `DeepSeekChat` 内部方法**:`run_light_subagent` / `run_full_subagent` 是 `DeepSeekChat` 的方法,抽离工具时需先抽离这些方法或改为函数式调用
|
||||
3. **测试覆盖盲区**:C 批工具(`ExpressEmotionTool` / `SubAgentTool`)和 Wave 2/3 子系统尚无单元测试,抽离时应同步补测
|
||||
|
||||
## 6. 文档同步状态
|
||||
|
||||
| 文档 | 状态 | 备注 |
|
||||
|---|---|---|
|
||||
| [aibox/README.md](../../aibox/README.md) | ✅ 已同步 v3.0 | 行数、目录结构、开发指南、v3.0 更新日志已更新 |
|
||||
| 本报告 | ✅ 新建 | 归档已完成工作与剩余规划 |
|
||||
| [aibox_orchestrator_worker_plan.md](aibox_orchestrator_worker_plan.md) | 历史 | v2.9 分层方案(已实施) |
|
||||
| [aibox_open_dev_plan.md](aibox_open_dev_plan.md) | 待评估 | 开放版裁剪方案,与本次模块化正交 |
|
||||
| [aibox_file_network_cli_refactor.md](aibox_file_network_cli_refactor.md) | 待评估 | 文件/网络/CLI 重构计划 |
|
||||
|
||||
## 7. 验证记录
|
||||
|
||||
```
|
||||
$ py -m pytest aibox/tests -q
|
||||
........................................................................ [ 42%]
|
||||
........................................................................ [ 84%]
|
||||
........................... [100%]
|
||||
171 passed in 10.79s
|
||||
```
|
||||
|
||||
- 测试基线:2026-08-14
|
||||
- 运行环境:Windows + PowerShell 5
|
||||
- Python:通过 `py` 启动器调用(`python` 不在 PATH)
|
||||
@@ -1,355 +0,0 @@
|
||||
# 认证独立化(CA 模块)开发文档
|
||||
|
||||
**日期**: 2026-08-09(v2 重写)
|
||||
**背景**: gateway v0.8.0 单文件 [main.cpp](../../gateway/src/main.cpp)(2470 行),认证逻辑(约 65%)与反向代理耦合;core/dnfs2 通过 HTTP 回调 gateway 验签,gateway 既是流量入口又是认证权威,形成环形依赖
|
||||
**范围**: `ca`(新模块)、`gateway`(瘦身为纯代理);**WAF / core / dnfs2 / static / 前端零改动**
|
||||
**前版**: 2026-08-08 初版计划书(auth 双栈方案)。本版按已选定拓扑重写:**数据流 `waf → ca → gateway`,gateway 不再验签**。原双栈/后置方案见 §2.2 决策记录
|
||||
|
||||
---
|
||||
|
||||
## 0. 摘要
|
||||
|
||||
新建 CA(Central Auth)模块:C++20 服务,接管 gateway 当前监听的 `127.0.0.1:8443`(WAF upstream 不变),承载**全部认证面**(登录/注册/Token 生命周期/CAPTCHA 代理/限流/WS 代理),并作为**认证前置层**:对业务请求本地验签后注入可信身份头,转发给退居 `127.0.0.1:8445` 的 gateway。gateway 删除全部认证代码,不再验签,仅保留白名单反代。
|
||||
|
||||
核心收益:验签在 CA 本地完成(RSA 密钥同进程,**零新增网络跳**);core/dnfs2 仍调 `8443` 的 `/admin/verify`、`/whoami`,**无感知切换**;前端 URL 不变;现有黑盒 34 断言原样充当验收门。
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状分析
|
||||
|
||||
### 1.1 当前调用链
|
||||
|
||||
```
|
||||
客户端 ──HTTPS──▶ WAF:443 ──HTTP──▶ gateway:8443 ──HTTP──▶ static:9200 / dnfs2:8006 / core:5200
|
||||
│ ├─ 本地验签(RSA 内存密钥)+ Cookie 回退 + Authorization 注入
|
||||
│ ├─ /login /register /logout /refresh_token /whoami /admin/verify
|
||||
│ ├─ /register/send_code /reset/send_code /reset_password
|
||||
│ ├─ GET /captcha/image(pymsg 代理 captcha2:8009)
|
||||
│ └─ WS 代理 /ws/aibox(握手验签 is_admin)
|
||||
└── pymsg: db:8005 / captcha2:8009 / smtp3:8008 / confs:8010
|
||||
|
||||
下游验签回调(HTTP 回环):
|
||||
core:5200 ──HTTP──▶ gateway:8443/admin/verify (HttpTask.cpp,GATEWAY_HOST/GATEWAY_PORT 可配,fail-closed)
|
||||
dnfs2:8006 ──HTTP──▶ gateway:8443/whoami (gateway_url 配置项,fail-closed)
|
||||
```
|
||||
|
||||
### 1.2 gateway 职责盘点与归属
|
||||
|
||||
| 职责域 | 函数/组件 | 归属 |
|
||||
|--------|-----------|------|
|
||||
| 用户认证 | `handleLogin` / `handleRegister` / `handleLogout` / `handleResetPassword` / `handleSendEmailCode` / `handleResetSendCode` | → CA |
|
||||
| Token 管理 | `verify_token` / `generate_jti` / `revoke_jti` / `revoke_all_user_tokens` / `handleRefreshToken` / `handleWhoami` / `handleAdminVerify` / jti 黑名单 / 用户版本号表 / RSA 密钥对 | → CA |
|
||||
| 密码体系 | `PBKDF2Hasher` / `PasswordValidator` / 防枚举 dummy 校验 | → CA |
|
||||
| 验证码/邮箱 | `handleCaptchaImage` / `verify_captcha_remote` / `verify_email_code_remote` | → CA |
|
||||
| 速率限制 | 全部 RateLimiter 实例(登录 5/5min、注册 3/10min、发码 5/10min + 邮箱级 3/h+10/d、验证码 10/min 等) | → CA |
|
||||
| WS 代理 | `verifyWsAdminToken` + `_WS_PROXY_ROUTES` 注册循环 | → CA(验签+桥接一体) |
|
||||
| Cookie | `q4_token` 签发/清除、`extract_token_from_request`(Cookie 回退)、代理时 `Authorization: Bearer` 注入 | → CA |
|
||||
| HTTP 反代 | `handleProxy`(白名单/路径安全/头转发/XFF) | 留 gateway(去验签化) |
|
||||
| 头卫生 | `is_special_header` / `is_safe_target` | 留 gateway |
|
||||
| 基础设施 | `wrapperHttp` / `ConfIni` / `DbC` / `LogC` / 信号处理 / 优雅关闭 | 两侧各持一份(模板见 smtp3) |
|
||||
|
||||
### 1.3 核心问题
|
||||
|
||||
1. **职责耦合**:认证与代理同进程,任一域改动爆炸半径为全文件
|
||||
2. **环形依赖**:gateway 是流量入口又是验签权威,core/dnfs2 须 HTTP 回调入口
|
||||
3. **敏感面混杂**:RSA 私钥、密码哈希校验、jti 黑名单与转发逻辑共用内存空间
|
||||
4. **不可独立扩展**:认证密集与代理密集负载无法分别扩容;认证状态纯内存,绑定单实例
|
||||
|
||||
---
|
||||
|
||||
## 2. 目标架构
|
||||
|
||||
### 2.1 拓扑
|
||||
|
||||
```
|
||||
客户端 ──HTTPS──▶ WAF:443 ──HTTP──▶ CA:8443 ──HTTP──▶ gateway:8445 ──▶ static:9200 / dnfs2:8006 / core:5200
|
||||
│
|
||||
├─ 本地处理:/login /register /logout /refresh_token /whoami /admin/verify
|
||||
│ /register/send_code /reset/send_code /reset_password
|
||||
│ GET /captcha/image
|
||||
├─ WS:/ws/aibox 握手验签(is_admin) → 直接桥接 ws://127.0.0.1:8765
|
||||
├─ 其余路径:本地验签 → 注入 X-Auth-User/Authorization → 转发 gateway:8445
|
||||
└─ pymsg: db:8005 / captcha2:8009 / smtp3:8008 / confs:8010
|
||||
|
||||
端口安排:CA 接管 8443(WAF upstream 配置不变);gateway 迁 8445(仅 loopback,仅 CA 可达)。
|
||||
```
|
||||
|
||||
职责划分:**CA = 认证权威 + 认证前置代理**;**gateway = 纯路由/转发策略**(opened 白名单、路径安全、XFF 透传)。
|
||||
|
||||
### 2.2 决策记录
|
||||
|
||||
| 方案 | 拓扑 | 结论 |
|
||||
|------|------|------|
|
||||
| **选定:CA 前置** | `waf → ca → gateway` | ✅ 验签在 CA 本地(零网络跳);core/dnfs2 调 8443 不变;WAF/前端不变;gateway 无需新增路径路由机制 |
|
||||
| 否决 A:CA 后置双栈 | `waf → gateway → ca`(HTTP+pymsg 双栈) | gateway 需新增路径级路由表;CORS/Set-Cookie/错误映射下沉 CA;两服务改动面都大 |
|
||||
| 否决 B:CA 后置纯 pymsg | `waf → gateway →(pymsg) ca` | gateway 保留 HTTP↔pymsg 翻译层与 Cookie 职责,认证面仍在入口进程内,瘦身不彻底;且每请求多一跳 pymsg 验签或引入吊销延迟的缓存 |
|
||||
| 否决 C:验签缓存 | gateway 缓存验签结果 60s | logout/改密后 Token 在窗口内仍可用,吊销语义弱化(随拓扑选定自然消失) |
|
||||
|
||||
### 2.3 信任模型(重要)
|
||||
|
||||
1. **loopback 信任基线不变**:本架构 loopback 内部服务本就不互鉴权(db:8005 execute 无认证即先例)。gateway:8445 仅绑 `127.0.0.1`,与本基线一致。
|
||||
2. **身份头契约(strip-then-set)**:CA 在边缘剥离客户端伪造的 `X-Auth-User` / `X-Auth-Admin`(Beast fields 大小写不敏感,`erase` 后 `set`),验签通过才注入。模式 A 下 CA 的唯一上游是 WAF(loopback)。
|
||||
3. **gateway 存在性不变量**:显式 `Target-Url` 请求必须携带 `X-Auth-User`,否则 401(非验签,仅防线编程,防本地误直连/SSRF 兜底)。
|
||||
4. **模式 B 警示迁移**:CA 直连公网(无 WAF)时 XFF 可被伪造、限流可绕过——原 gateway README 的 A4 降级提示随限流一并迁入 CA README;根治方案 [libhttp_remote_addr_dev_doc.md](libhttp_remote_addr_dev_doc.md) 中的 `trust_xff`/`rate_limit_key()` 配套改在 CA 落地。
|
||||
|
||||
---
|
||||
|
||||
## 3. CA 模块设计
|
||||
|
||||
### 3.1 目录与构建(沿用 smtp3/captcha2 约定)
|
||||
|
||||
```
|
||||
ca/
|
||||
├── src/main.cpp # 入口 + HTTP 路由 + 认证逻辑 + 前置转发(迁移代码为主)
|
||||
├── inc/
|
||||
│ ├── Conf.hpp # ConfIni(confs 拉取 + env 覆盖 + 完整性校验)
|
||||
│ ├── DbC.hpp # 自 gateway 随迁(已基于 libmsg us/Msg.hpp;client 名 "gateway"→"ca")
|
||||
│ ├── LogC.hpp # 异步日志(smtp3 长连接版)
|
||||
│ ├── ExpBase.hpp # 异常基类
|
||||
│ └── LocalTime.hpp # 时间工具
|
||||
├── Makefile # 链接 us_http / us_msg / us_crypt / ssl / crypto / boost_json / curl
|
||||
├── _set.sh / _run.sh
|
||||
└── README.md
|
||||
```
|
||||
|
||||
- 二进制 `ca.bin`;版本从 `1.0.0` 起;日志服务名 `ca`
|
||||
- 依赖:libhttp(HttpServer/HttpClient/ws_proxy)、libmsg(MsgClient/RpcClient)、libcrypt(MyRSA/Base64)、OpenSSL(PBKDF2/RAND_bytes)、boost::json/program_options/property_tree、libcurl
|
||||
- **不依赖 libsqlite3**:Users 表访问全部经 db:8005(现状即如此)
|
||||
|
||||
### 3.2 配置(`confs/configs/ca.ini` 新增)
|
||||
|
||||
```ini
|
||||
[Server]
|
||||
address = 127.0.0.1
|
||||
port = 8443 # 接管 gateway 现端口,WAF upstream 不变
|
||||
|
||||
[Upstream]
|
||||
gateway = http://127.0.0.1:8445 # 业务请求唯一上游
|
||||
|
||||
[Token] # 自 gateway.ini 原样迁入
|
||||
keep_time = 3600
|
||||
refresh_grace_multiplier = 2
|
||||
max_refresh = 10
|
||||
|
||||
[Admin] # 原样迁入
|
||||
users = 1776177662@qq.com;277665039@qq.com;
|
||||
|
||||
[WsProxy] # 自 gateway.ini 迁入
|
||||
routes = /ws/aibox=ws://127.0.0.1:8765;
|
||||
|
||||
[CORS] # 原样迁入(P2-3:预检反射由 libhttp 处理,白名单暂仍不生效)
|
||||
allowed_origins = https://qsont.me;https://www.qsont.me;http://127.0.0.1:8443;http://localhost:8443
|
||||
|
||||
[Downstream] # 原样迁入
|
||||
db_host = 127.0.0.1
|
||||
db_port = 8005
|
||||
captcha_host = 127.0.0.1
|
||||
captcha_port = 8009
|
||||
smtp_host = 127.0.0.1
|
||||
smtp_port = 8008
|
||||
```
|
||||
|
||||
环境变量覆盖(与 gateway 现机制一致):`LISTEN_ADDR`/`LISTEN_PORT`、`UPSTREAM_GATEWAY`、`DB_*`、`CAPTCHA_*`、`SMTP_*`、`CONFS_HOST`/`CONFS_PORT`。配置加载:confs `get` 拉取(retry_on_refused 阻塞)→ env 覆盖 → 完整性校验。
|
||||
|
||||
### 3.3 HTTP 路由表(对外契约与原 gateway 完全一致)
|
||||
|
||||
| 路由 | 处理 | 说明 |
|
||||
|------|------|------|
|
||||
| POST `/login` | 本地 | 强制远端 CAPTCHA;dummy PBKDF2 防枚举;签发 Token + `q4_token` Cookie |
|
||||
| POST `/register` | 本地 | 密码强度→邮箱格式→CAPTCHA→邮箱验证码(smtp fail-closed)→写 Users |
|
||||
| POST `/register/send_code`、`/reset/send_code` | 本地 | IP+邮箱级限流、强制 CAPTCHA、smtp 60s 冷却 429 透传 |
|
||||
| POST `/reset_password` | 本地 | 防枚举恒成功文案;改密后版本号递增全设备吊销 |
|
||||
| POST `/logout` | 本地 | jti 吊销 + 清 Cookie |
|
||||
| POST `/refresh_token` | 本地 | 宽限期刷新、旧 jti 吊销、max_refresh 上限 |
|
||||
| POST `/whoami` | 本地 | 恒 200,`{login:false}` 不泄露细节;core/dnfs2 依赖此契约 |
|
||||
| POST `/admin/verify` | 本地 | fail-closed 401;core 依赖此契约 |
|
||||
| GET `/captcha/image` | 本地 | pymsg 调 captcha2,PNG + `X-Captcha-Token` + `no-store`,10/min/IP |
|
||||
| WS `[WsProxy]` 路径 | 本地 | 握手验签 `is_admin=true` → 桥接后端(ws/wss),双向透传 |
|
||||
| GET/POST `*`(通配) | **转发** | 见 §3.4 |
|
||||
|
||||
### 3.4 前置转发层(通配路由 → gateway:8445)
|
||||
|
||||
迁移 `handleProxy` 的转发骨架,认证语义前置:
|
||||
|
||||
1. **路径安全**:`is_safe_target()` 校验(`..`/`%2e`/`%2f`/`%5c`/空字节 → 400),CA/gateway 双层各执行一次(防线叠加,gateway 侧保留原校验)。
|
||||
2. **公开 vs 受保护**:无 `Target-Url` 头 → 公开目标(Proxy.default 语义在 gateway),**剥离**客户端 `X-Auth-User`/`X-Auth-Admin` 后直接转发,不注入任何凭证。
|
||||
3. **受保护**:有 `Target-Url` → 提取 Token(`Authorization` 优先,回退 `q4_token` Cookie)→ `verify_token` 本地验签(RSA→过期→jti→版本号)→ 失败按现契约 400/401,**不转发**;成功则注入:
|
||||
- `Authorization: Bearer <token>`(Cookie 来源也统一补齐,dnfs2 等下游零改动)
|
||||
- `X-Auth-User: <email>`、`X-Auth-Admin: true`(仅管理员;供 gateway 不变量与下游未来免回调使用)
|
||||
4. **头卫生**:剥离客户端自带的 `X-Auth-User`/`X-Auth-Admin`(strip-then-set);XFF 透传 WAF 值(缺省 `unknown`);`Host`/`connection`/`Accept-Encoding` 等逐跳头处理沿用现 `is_special_header` 集合 + `x-auth-user`/`x-auth-admin` 新增。
|
||||
5. **转发失败语义**:gateway:8445 不可达 → 502(与现 captcha/smtp fail-closed 风格一致),记 warning 日志。
|
||||
6. **响应**:状态码与响应头原样回传(现 `handleProxy` 行为),`Connection: close`。
|
||||
|
||||
### 3.5 迁移资产清单(gateway `main.cpp` → CA `main.cpp`)
|
||||
|
||||
代码以**整体搬迁**为主,保持实现逐字不变(降低回归面),仅做两处适配:日志服务名 `gateway`→`ca`;DbC client 名同上。
|
||||
|
||||
| 资产 | 备注 |
|
||||
|------|------|
|
||||
| PBKDF2Hasher / PasswordValidator / 常数时间比较 | 含 SHA256 旧哈希迁移注释块(保持注释状态) |
|
||||
| TokenInfo / verify_token / generate_jti / revoke_jti / revoke_all_user_tokens / jti 黑名单(5000 上限清理)/ 用户版本号表 | RSA 密钥对启动内存生成(重启即全吊销,语义不变) |
|
||||
| 8 个 RateLimiter 实例 + 邮箱级双窗口限流 | 限流键仍取 XFF(模式 A 可信;模式 B 警示见 §2.3-4) |
|
||||
| handleLogin ~ handleResetPassword 全部 handler | 错误文案/状态码契约逐字保留(黑盒断言依赖) |
|
||||
| handleWhoami / handleAdminVerify / handleRefreshToken / handleLogout | 同上 |
|
||||
| handleCaptchaImage / verify_captcha_remote / verify_email_code_remote | fail-closed 语义保留 |
|
||||
| verifyWsAdminToken + ws_proxy 注册 | 桥接目标直接为后端(不再经 gateway) |
|
||||
| handleProxy 转发骨架 / is_safe_target / is_special_header / get_cookie_value / extract_token_from_request | 按 §3.4 改造 |
|
||||
| wrapperHttp / 信号处理 / 优雅关闭(8s+3s 超时)/ ExpBase / ConfIni / LocalTime / DbC / LogC | 基础设施随迁 |
|
||||
|
||||
### 3.6 不变量自检(启动时)
|
||||
|
||||
- `[Upstream] gateway` 非空且为 `http://127.0.0.1:*`(防误配到公网)
|
||||
- 模式 B 检测:`Server.address` 非 loopback 时启动日志打印 A4 安全降级警告
|
||||
|
||||
---
|
||||
|
||||
## 4. gateway 瘦身设计(v0.9.0)
|
||||
|
||||
### 4.1 删除清单(预计 -1700 行,2470 → ~750 行)
|
||||
|
||||
- 全部认证 handler 与 Token/PBKDF2/RSA/jti/版本号/限流/CAPTCHA/邮箱验证码代码
|
||||
- `q4_token` Cookie 读写、`extract_token_from_request`、代理内验签与 Authorization 注入
|
||||
- `[Token]`/`[Admin]`/`[WsProxy]`/`[Database]` 配置读取;下游 captcha/smtp 配置(db 保留供 LogC)
|
||||
- `/login` 等 9 条认证路由 + `/captcha/image` 路由 + WS 代理注册循环
|
||||
|
||||
### 4.2 保留清单
|
||||
|
||||
- `handleProxy`(去验签化)+ `is_safe_target` + `is_special_header` + `wrapperHttp`
|
||||
- 存在性不变量:显式 `Target-Url` 且无 `X-Auth-User` → 401(§2.3-3)
|
||||
- 公开目标规则:无 `Target-Url` → `Proxy.default`,不校验身份头
|
||||
- XFF 处理:透传 CA 值(缺省 `unknown`)
|
||||
- 配置加载、信号处理、优雅关闭、LogC
|
||||
|
||||
### 4.3 精简后 `confs/configs/gateway.ini`
|
||||
|
||||
```ini
|
||||
[Server]
|
||||
address = 127.0.0.1
|
||||
port = 8445 # 迁出 8443;仅 CA 可达
|
||||
tls = off
|
||||
|
||||
[Proxy]
|
||||
default = http://127.0.0.1:9200
|
||||
closed = http://127.0.0.1:8005;http://127.0.0.1:8007;
|
||||
opened = http://127.0.0.1:9200;http://127.0.0.1:8006;http://127.0.0.1:5200;
|
||||
|
||||
[Downstream] # 仅供 LogC 日志落库
|
||||
db_host = 127.0.0.1
|
||||
db_port = 8005
|
||||
```
|
||||
|
||||
### 4.4 启动自检
|
||||
|
||||
- `Server.address` 必须为 loopback(`127.0.0.1`/`::1`):gateway 不再验签后,绑定公网等于裸奔,**非 loopback 直接拒绝启动**并打印致命错误。
|
||||
|
||||
---
|
||||
|
||||
## 5. 接口契约
|
||||
|
||||
### 5.1 对外 HTTP(不变量)
|
||||
|
||||
`https://<host>/login`、`/register`、`/whoami` 等全部 URL、请求/响应 JSON 结构、状态码(400/401/403/429/500/502)、`q4_token` Cookie 属性、`X-Captcha-Token` 头——**逐字不变**。前端、WAF、core、dnfs2 零改动的依据即此。
|
||||
|
||||
### 5.2 内部头契约(CA ↔ gateway ↔ 下游)
|
||||
|
||||
| 头 | 方向 | 语义 |
|
||||
|----|------|------|
|
||||
| `X-Auth-User` | CA→gateway→下游 | 已验签身份;CA strip-then-set;gateway 对显式 Target-Url 要求其存在 |
|
||||
| `X-Auth-Admin` | CA→gateway→下游 | `true` 表示管理员;供下游免回调豁免(可选消费) |
|
||||
| `Authorization` | CA→gateway→下游 | Cookie 来源统一补齐为 Bearer;下游解析不变 |
|
||||
| `X-Forwarded-For` | WAF→CA→gateway→下游 | 逐层透传,缺省 `unknown` |
|
||||
| `Target-Url` | 客户端→CA→gateway | 路由策略头,语义不变 |
|
||||
|
||||
### 5.3 Token 格式(不变)
|
||||
|
||||
`Base64(payload)_RSA-Sign(payload)`;payload 字段(user/jti/ver/iat/created_at/keep_time/is_admin/refresh_count/max_refresh)不变。切换瞬间旧 Token 全部失效(CA 新密钥),用户重新登录——一次性事件,cutover 通告中说明。
|
||||
|
||||
### 5.4 下游零改动验证点
|
||||
|
||||
| 消费方 | 现状调用 | 切换后 |
|
||||
|--------|----------|--------|
|
||||
| core `verify_admin_token` | `http://127.0.0.1:8443/admin/verify` | 同 URL 由 CA 服务,契约不变 |
|
||||
| dnfs2 Token 验签 | `http://127.0.0.1:8443/whoami` | 同上 |
|
||||
| static `/api/check_user` | 直连 db 查 Users(账号旁路) | 不受影响;遗留项另行收口 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 上线步骤(cutover 与回滚)
|
||||
|
||||
| 步骤 | 内容 |
|
||||
|------|------|
|
||||
| 1 | CA 开发完成,`LISTEN_PORT=18443` 并行实例联调(不影响现网 8443) |
|
||||
| 2 | gateway 瘦身版(v0.9.0)构建,`gateway.ini` 改 8445 |
|
||||
| 3 | cutover(秒级窗口):停 gateway:8443 → 启 CA:8443 → 启 gateway:8445 → 黑盒全量验证 |
|
||||
| 4 | `core/init.txt` 增加 `ca/_run.sh`(gateway 之前);README/INDEX 文档同步 |
|
||||
| 5 | 观察 24h:CA/gateway 日志、db Logs 表 4xx/5xx 基线对比 |
|
||||
|
||||
**回滚**:停 CA → 旧 gateway 二进制 + 旧配置回 8443 → 重启。Token 全部重签(用户重新登录),无数据迁移。
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试计划
|
||||
|
||||
### 7.1 验收门(零改动复用)
|
||||
|
||||
- `tests/blackbox/test_gateway.py` 34 断言**原样**对 8443 执行:认证路径打到 CA、代理路径经 CA→gateway,外部契约不变则全绿(当前基线 32 passed / 2 env-skipped)
|
||||
- 修复期两个红测试(A3 缺字段 400)已在 v0.8.0 转绿,迁移后必须保持
|
||||
|
||||
### 7.2 新增探针(test_ca.py,黑盒补充)
|
||||
|
||||
| 探针 | 预期 |
|
||||
|------|------|
|
||||
| 客户端伪造 `X-Auth-User: admin@x` + 显式 Target-Url(无 Token) | CA 剥离后 401,不到下游 |
|
||||
| 伪造 `X-Auth-User` + 有效非管理员 Token 访问 WS 路由 | 403(以验签结果为准,不以头为准) |
|
||||
| 直连 gateway:8445,显式 Target-Url、无 `X-Auth-User` | 401(存在性不变量) |
|
||||
| 经 CA 全链路(8443)带 Token 访问 dnfs2 | 下游收到 `X-Auth-User` 与 `Authorization` |
|
||||
| CA 存活、gateway:8445 停止 | 业务路径 502(不静默放行),认证路径正常 |
|
||||
| gateway 存活、CA 停止 | WAF 502(入口不可达,符合预期 fail-closed) |
|
||||
| 公开目标(无 Target-Url)带伪造身份头 | 下游收不到 `X-Auth-User`/`Authorization` |
|
||||
|
||||
### 7.3 集成回归
|
||||
|
||||
- core HTTP 管理接口(`/admin/verify` 链路)实测;dnfs2 笔记 CRUD(`/whoami` 链路 + AI 审核)实测
|
||||
- WS 代理端到端:管理员 Token 升级 `/ws/aibox` 双向帧;非管理员 403
|
||||
- 限流全谱在 CA 复测(随机 XFF 技巧沿用);cutover/回滚演练各一次
|
||||
|
||||
### 7.4 测试资产演进(后续)
|
||||
|
||||
- `test_gateway.py` 按进程边界拆分为 `test_ca.py`(A/B/C/E 段)+ 精简 `test_gateway.py`(D 段代理矩阵 + 8445 直连探针),README/INDEX 同步
|
||||
- 认证 happy path 仍依赖 captcha2/smtp 测试钩子(既有遗留,CA 落地后可仅在 CA 层加 `[Security] captcha_required` 开关,单点解锁)
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险与缓解
|
||||
|
||||
| 风险 | 等级 | 缓解 |
|
||||
|------|------|------|
|
||||
| CA 不可用 → 全站入口中断 | 高 | CA 职责内聚后可用性等同于现 gateway;core 守护 + 黑盒监控;回滚方案秒级 |
|
||||
| 迁移遗漏导致认证行为漂移 | 中 | 代码逐字搬迁 + 34 断言验收门 + §7.2 探针矩阵 |
|
||||
| 身份头伪造/泄露 | 中 | strip-then-set + gateway 存在性不变量 + 公开目标不注入 + 直连探针回归 |
|
||||
| gateway:8445 被公网暴露 | 高 | 启动强制 loopback 自检(§4.4);部署审计 checklist 增加端口扫描项 |
|
||||
| cutover 窗口 Token 全失效 | 低 | 预期内(RSA 换钥),通告用户重新登录 |
|
||||
| 双重转发延迟 | 低 | 本机 HTTP 转发 <1ms;CA→gateway 短连接可后续优化为 keep-alive |
|
||||
| 模式 B 限流绕过(A4 迁移) | 中 | CA README 降级警示;根治按 libhttp_remote_addr_dev_doc(配套改落 CA) |
|
||||
|
||||
---
|
||||
|
||||
## 9. 工作量估算
|
||||
|
||||
| 部分 | 规模 | 估时 |
|
||||
|------|------|------|
|
||||
| CA 脚手架(目录/Makefile/_set.sh/_run.sh/LogC 等) | 模板复制 + 适配 | 0.5 天 |
|
||||
| 认证资产搬迁 + 前置转发层改造 | ~1700 行搬迁 + ~150 行改造 | 1.5 天 |
|
||||
| gateway 瘦身 + 启动自检 | -1700 行 + ~20 行 | 0.5 天 |
|
||||
| 配置/init.txt/文档(README×2、INDEX、confs) | — | 0.5 天 |
|
||||
| 黑盒验收 + §7.2 探针 + 集成回归 + cutover 演练 | — | 1 天 |
|
||||
| **合计** | | **4 天**(含缓冲) |
|
||||
|
||||
---
|
||||
|
||||
## 10. 与既有文档的关系
|
||||
|
||||
- 本文档**替代** 2026-08-08 初版计划书中的双栈拓扑与阶段划分(初版的职责盘点/迁移清单仍然有效,已并入 §1.2/§3.5)
|
||||
- [libhttp_remote_addr_dev_doc.md](libhttp_remote_addr_dev_doc.md)(A4 根治):实施时 `trust_xff`/`rate_limit_key()` 配套由 gateway 改为落 CA;`is_special_header` 两侧同步加 `x-remote-addr`
|
||||
- [gateway_SECURITY_AUDIT.md](../security/gateway_SECURITY_AUDIT.md):A2(白名单 403)断言、P2-3(CORS 白名单)随转发层/配置迁入 CA;P1-7(CSRF Token)在 CA 层评估
|
||||
- gateway README / 项目 README 的架构图、端口表、请求链路在上线步骤第 4 步同步更新
|
||||
@@ -312,13 +312,13 @@ systemctl start Nidus # 或 bash core/_run.sh
|
||||
| **core ConfsTask** | C++ 内嵌配置任务(557 行) |
|
||||
| **gateway 精简** | `src/main.cpp` 大幅精简(-1980 行),认证逻辑下沉至 ca |
|
||||
|
||||
### 8.2 认证独立化(auth_service_plan)
|
||||
### 8.2 认证独立化(ca 模块)
|
||||
|
||||
`waf → ca → gateway` 前置拓扑:ca 统一鉴权并前置转发,gateway 退化为纯转发(仅 loopback),下游 / WAF / 前端零改动。对应开发文档见 `docs/reports/auth_service_plan.md`。
|
||||
`waf → ca → gateway` 前置拓扑:ca 统一鉴权并前置转发,gateway 退化为纯转发(仅 loopback),下游 / WAF / 前端零改动。
|
||||
|
||||
### 8.3 关键 dev docs 索引(`docs/reports/`)
|
||||
|
||||
core_service_registry_dev_doc(注册中心)、auth_service_plan(CA 模块)、aibox_open_dev_plan / aibox_file_network_cli_refactor(aibox 安全沙箱与 CLI 化)、libcrypt_CBC_deprecation_dev_doc(AES-GCM 迁移)、libhttp_remote_addr_dev_doc(X-Remote-Addr 透传)、captcha_cpp_rewrite / smtp_cpp_rewrite / confs2_cpp_rewrite(C++ 重写方案)。
|
||||
core_service_registry_dev_doc(注册中心)、aibox_open_dev_plan(aibox 安全沙箱与 CLI 化)、libcrypt_CBC_deprecation_dev_doc(AES-GCM 迁移)、libhttp_remote_addr_dev_doc(X-Remote-Addr 透传)、smtp_cpp_rewrite(C++ 重写方案)。
|
||||
|
||||
### 8.4 安全评级演进
|
||||
|
||||
|
||||
@@ -1,406 +0,0 @@
|
||||
# bot4cj 模块 Python 迁移开发方案
|
||||
|
||||
> 文档类型:功能开发计划
|
||||
> 模块:bot4cj → pybot(Python 迁移)
|
||||
> 作者:自动生成
|
||||
> 日期:2026-08-09
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 背景与动机
|
||||
|
||||
`bot4cj` 是基于仓颉(Cangjie)编程语言和 Linux I2C 接口的机器人硬件脚本解析器,通过 `.mt` 矩阵脚本描述机器人硬件构成与动作序列,驱动 PCA9685 舵机控制板、MPU6050 传感器等真实硬件(宿主机为树莓派 aarch64)。
|
||||
|
||||
将其迁移到 Python 的动机:
|
||||
|
||||
| 收益 | 说明 |
|
||||
|------|------|
|
||||
| 工具链门槛消除 | 仓颉编译器(cjc 1.1.3)+ cjpm + 4 个仓颉扩展包(ctype4cj/pthread4cj/spi4cj/lua4cj)→ 树莓派系统自带 Python3,零编译 |
|
||||
| 与 actpred 生态打通 | actpred(动作预测,PyTorch)已有 Python 版 `.mt` 解析器(`mt_parser.py`),迁移后"大脑(actpred)↔ 身体(bot4cj)"共享同一语言与数据结构,DRL 闭环开发成本大幅降低 |
|
||||
| Lua 扩展层可消除 | 仓颉版引入 Lua 是为了运行时可扩展硬件驱动;Python 本身即脚本语言,硬件驱动直接写成 Python 插件模块,砍掉 lua4cj + liblua5.4 + C 函数注册整层 |
|
||||
| C/C++ 共享库大部分可消除 | libi2c4cj.so 的功能可用 `fcntl.ioctl` + `os.read/write` 纯 Python 实现;矩阵/FFT/CNN 由 numpy/PyTorch(actpred 已有)承担 |
|
||||
| 审计遗留一并修复 | [bot4cj 代码审计](../reviews/bot4cj_CODE_AUDIT_20260808.md) 15 项发现(3 P0 + 5 P1 + 7 P2)全部待修复,迁移时逐项闭环 |
|
||||
| 与项目主流技术栈对齐 | micro_servers 业务服务以 Python/C++20 为主,仓颉是唯一的孤立技术栈,维护人手受限 |
|
||||
|
||||
**性能评估**:bot4cj 的执行节奏由硬件决定(舵机 50Hz PWM、I2C 100kHz~400kHz、动作行间 20~500ms 延时),解析与调度不是瓶颈,Python 的性能完全够用;且 Python 插件替代 Lua 桥接后,单次硬件调用的框架开销反而下降约两个数量级(详见附录 A 的量化分析)。这与 db2/captcha2 等"Python→C++ 求性能"的方向相反,是"仓颉→Python 求生态与可维护性"。
|
||||
|
||||
### 1.2 设计目标
|
||||
|
||||
1. **`.mt` 脚本零改动**:现有 `scripts/*.mt` 全部原样可用,块语法(`@Alias`/`@I2C`/`@PCA9685`/`@MPU6050_BIND`/`@Speaker`/`@Servo`/`@Action`)、`-` 占位符语义、`#` 注释、`;` 行结束符完全兼容。
|
||||
2. **CLI 兼容**:保持 `cat scripts/std.mt | ./bot` 的 stdin 管道用法与 `--help`/`--test` 分支。
|
||||
3. **零第三方依赖的核心**:解析器 + 硬件层仅用标准库(`fcntl`/`os`/`struct`/`threading`/`json`/`argparse`),树莓派上无需 venv 即可运行;numpy 仅测试/工具链可选。
|
||||
4. **硬件插件化(Python 替代 Lua)**:未知 `@Block` 路由到 `modules/<Name>.py` 插件,保留 build/call/destroy 三段生命周期协议。
|
||||
5. **审计 15 项发现全部修复**:见 §5 修复映射表。
|
||||
6. **可测试**:pytest 单元测试覆盖解析器、占位符语义、Servo 校准、插件加载;硬件层可用 Mock I2C 在无硬件的 x86 上跑通全流程。
|
||||
|
||||
### 1.3 不做什么
|
||||
|
||||
- **不迁移 math/NN/CNN/FFT/CUDA 层**(`src/math/*.cj`、`src/cpp/cnn/`、`src/cpp/fft*`、`src/cuda/`):这部分是为 DRL 预留的自研神经网络库,功能已被 actpred(PyTorch)完全覆盖且质量更高。DRL 闭环未来直接调 actpred,不重复造轮子。相应地 `--test nn/cnn/cnn1` 分支不迁移。
|
||||
- **不迁移 `.bot` 旧版脚本支持**:`scripts/*.bot` 是历史遗留,仓颉版本身也未解析它。
|
||||
- **不改 `.mt` 语法**:`@Action_Auto` 等语法演进(旧分析 D11)留待后续版本。
|
||||
- **不引入 smbus2/RPi.GPIO 等三方库**:I2C 用 `fcntl.ioctl(fd, I2C_SLAVE=0x0703, addr)` 直接实现,与仓颉版 `I2C.cj`/`I2C.c` 行为逐字节对齐。
|
||||
- **不删除 bot4cj 目录**:遵循项目"原版与重写版并存 → 验证通过后切换"的渐进策略(同 db→db2、captcha→captcha2、smtp→smtp3)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状分析
|
||||
|
||||
### 2.1 仓颉版架构分层
|
||||
|
||||
```
|
||||
.mt 矩阵脚本(硬件定义 + 动作序列)
|
||||
↓
|
||||
MatrixParser(P.cj,FSM 逐字符解析)→ MatrixObject 链表
|
||||
↓
|
||||
RobotStruct(r.cj)createHardVars():按 @块名分发到硬件变量工厂
|
||||
MatrixVars_Hardware 基类 + 三闭包:rise/things/free
|
||||
↓
|
||||
硬件实现 hd/*.cj:I2C(20 槽锁数组)、Servo、PCA9685、MPU6050、Speaker
|
||||
+ Lua 扩展 modules/*.lua(未知 @Block 的通用驱动,经 C 函数注册调 I2C/SPI)
|
||||
↓
|
||||
系统库 .so:libi2c4cj(I2C glibc 封装)、libsnd4cj(音频)、libmt4cj/libfft4cj/libcnn4cj(数学)
|
||||
```
|
||||
|
||||
### 2.2 迁移范围源码清单
|
||||
|
||||
| 仓颉源文件 | 行数 | 职责 | 迁移去向 |
|
||||
|-----------|------|------|---------|
|
||||
| `src/main.cj` | 178 | CLI 入口、stdin 管道、--test 分支 | `pybot/main.py` |
|
||||
| `src/P.cj` | 264 | MatrixParser(FSM)+ MatrixObject + MatrixSaver + 解析异常 | `pybot/parser.py` |
|
||||
| `src/r.cj` | 1072 | RobotStruct + 硬件变量基类 + 6 个工厂 + Lua 状态机桥接 | `pybot/robot.py` + `pybot/hardvars.py` |
|
||||
| `src/hd/I2C.cj` + `src/cpp/I2C.c` | 340+ | I2C glibc 封装 + 每 fd 互斥锁 | `pybot/hd/i2c.py` |
|
||||
| `src/hd/Servo.cj` | 31 | max/min/offset 校准 + 负 max 角度翻转 | `pybot/hd/servo.py` |
|
||||
| `src/hd/MPU6050.cj` | 244 | 6 轴传感器 + 后台采样线程 + 卡尔曼 | `pybot/hd/mpu6050.py` |
|
||||
| `src/hd/snd.cj` + `src/cpp/snd.cpp` | 23+ | 扬声器(仓颉侧仅 sleep 占位) | `pybot/hd/speaker.py` |
|
||||
| `src/hd/PCA9685.cj` | — | 已标记"由 Lua 取代" | 不迁移(走插件) |
|
||||
| `modules/*.lua`(PCA9685/ADS1115/ADS1256/SPI/TEST) | — | Lua 硬件驱动 | `pybot/modules/*.py` 插件 |
|
||||
| `src/math/*.cj`、`src/cpp/cnn|fft|Matrix|kalman`、`src/cuda/` | ~2000 | 自研 NN/数学库 | **不迁移**(actpred 承担);卡尔曼滤波例外,MPU6050 需要,~40 行纯 Python |
|
||||
|
||||
### 2.3 关键语义(必须逐项对齐)
|
||||
|
||||
1. **占位符 `-`**:递归沿用上一行同列值;首行为 `-` 时,`parseMt` 解析为 `0`(数值列),`getMt` 返回字面 `-`。
|
||||
actpred `mt_parser.py` 曾与此不同(首行 `-` 用 `@Servo` max 列填充),2026-08 已统一为本语义(填 0):运行时 Servo 有 max/min 钳位保护,且 max 列是钳位上限而非初始位姿(反装舵机 max 为负值,会填出指令空间 0~180 之外的非法值)。
|
||||
2. **@Action 列名 `dev.param`**:点号拆分为硬件名 + 参数名;参数名经 `@Alias` 表转换(`A`→`angle`、`F`→`frequency`)后传入硬件。
|
||||
3. **每行执行**:同一行按硬件聚合参数字典,逐硬件调用一次 `play()`;行间时延由 `spk.delay` 参数承担。
|
||||
4. **硬件生命周期**:`active()` 按创建顺序 build,`deactive()` 倒序 destroy,且必须在 `try/finally` 中保证执行。
|
||||
5. **Servo 角度算法**:`a = angle + offset; a = clamp(a, |min|, |max|); if max < 0: a = 180 - a`。
|
||||
6. **PCA9685 脉宽映射**:50Hz、0°→0.5ms、180°→2.5ms、`offval = width * 4096 / 20`、寄存器 `0x06 + 4*channel` 写 4 字节。
|
||||
7. **MPU6050**:±500°/s、±8g、10ms 采样周期、卡尔曼滤波(Q=20, R=200)、roll/pitch 由加速度计反正切求得。
|
||||
8. **I2C 互斥**:同一 fd 上"设地址 + 读/写寄存器"必须是原子序列(多设备共享总线)。
|
||||
|
||||
### 2.4 审计遗留(迁移时修复的输入清单)
|
||||
|
||||
来自 [bot4cj_CODE_AUDIT_20260808.md](../reviews/bot4cj_CODE_AUDIT_20260808.md):3 项 P0(S01 列数校验恒假 / S02 调试 print 污染 stdout / S03 扬声器无实声)、5 项 P1(S04 转义不一致 / S05 MPU6050 地址重复 / S06 有符号读寄存器 / S07 Lua 路径硬编码 / S08 文档滞后)、7 项 P2(无测试、注释代码、锁扫描 O(n)、无编译告警、消费端报错不可读、线程退出、GC 析构不确定)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 目标架构
|
||||
|
||||
### 3.1 目录结构
|
||||
|
||||
```
|
||||
pybot/
|
||||
├── pybot/
|
||||
│ ├── __init__.py
|
||||
│ ├── main.py # CLI 入口(argparse:--help / --test / -i / -o 预留)
|
||||
│ ├── parser.py # MatrixParser + MatrixObject + MatrixSaver + 解析异常
|
||||
│ ├── robot.py # RobotStruct(createHardVars / active / run / deactive / save)
|
||||
│ ├── hardvars.py # HardwareVar 基类 + Alias/I2C/Servo/Speaker/MPU6050_BIND 工厂
|
||||
│ ├── plugin.py # Python 插件加载器(替代 Lua 状态机桥接)
|
||||
│ └── hd/
|
||||
│ ├── __init__.py
|
||||
│ ├── i2c.py # fcntl.ioctl I2C + 每 fd 锁(dict + threading.Lock)
|
||||
│ ├── servo.py # 舵机校准与翻转
|
||||
│ ├── mpu6050.py # 传感器 + threading.Thread 采样 + kalman
|
||||
│ ├── kalman.py # 一维卡尔曼滤波(~40 行)
|
||||
│ └── speaker.py # 正弦波发声(可选 alsaaudio,默认 aplay 子进程 / 静默降级)
|
||||
├── modules/ # 硬件驱动插件(Python 替代 Lua)
|
||||
│ ├── PCA9685.py
|
||||
│ ├── ADS1115.py
|
||||
│ ├── ADS1256.py
|
||||
│ └── SPI.py
|
||||
├── scripts -> ../bot4cj/scripts # 软链或复制,.mt 脚本共享
|
||||
├── tests/
|
||||
│ ├── test_parser.py
|
||||
│ ├── test_hardvars.py
|
||||
│ ├── test_servo.py
|
||||
│ └── mock_i2c.py # Mock I2C 总线(记录寄存器写序列,供断言)
|
||||
├── _run.sh # cat x.mt | bash _run.sh(exec python3 -m pybot)
|
||||
├── _set.sh # 无编译;可选建 venv 装 pytest
|
||||
├── requirements.txt # 核心为空;[dev] pytest
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 3.2 数据流(与仓颉版一致)
|
||||
|
||||
```
|
||||
stdin → MatrixParser.parse()(逐块产出 MatrixObject)
|
||||
→ RobotStruct.create_hardvars()(按块名分发工厂;未知块 → plugin.load("modules/<Name>.py"))
|
||||
→ robot.active()(按序 build)
|
||||
→ robot.run()(逐行 @Action:聚合参数 → 逐硬件 play(dict))
|
||||
→ robot.deactive()(倒序 destroy,try/finally 保证)
|
||||
```
|
||||
|
||||
### 3.3 关键映射决策
|
||||
|
||||
| 仓颉版机制 | Python 版方案 | 理由 |
|
||||
|-----------|--------------|------|
|
||||
| `JsonValue` 统一通信协议 | 原生 `dict`(进程内直接传递) | 无跨语言边界后无需序列化;插件协议文档化 dict 键约定 |
|
||||
| 三闭包 rise/things/free | `HardwareVar` 基类三方法 `build()/play(params)/destroy()`,子类覆写 | Python 惯用法;闭包捕获改为实例属性 |
|
||||
| Lua 状态机 + C 函数注册(i2c_open 等 11 个) | `importlib` 动态加载 `modules/<Name>.py`,插件直接 `from pybot.hd import i2c` | 消除整个 FFI 层;插件与核心同语言 |
|
||||
| 20 槽 I2C 锁数组(O(n) 线性扫描) | `dict[fd → threading.Lock]` + 全局注册锁 | 修复审计 Q03;容量不再受限 |
|
||||
| pthread_create/pthread_cancel(MPU6050 采样) | `threading.Thread(daemon=True)` + `threading.Event` 停止标志 | 修复审计 Q06/D3,安全退出 |
|
||||
| 仓颉 GC 终结器不确定 | 上下文管理器 `with RobotStruct(...) as robot:` + `atexit` 兜底 | 修复审计 Q07/D1,确定性释放 fd |
|
||||
| cjpm build + Makefile .so | 无构建步骤 | 树莓派上 `git pull` 即部署 |
|
||||
|
||||
### 3.4 插件协议(modules/*.py,替代 Lua build/call/destroy)
|
||||
|
||||
```python
|
||||
# modules/PCA9685.py —— 与 PCA9685.lua 逐语义对齐
|
||||
def build(ctx: dict) -> dict:
|
||||
"""ctx: 元数据(fd/address/... 来自 @PCA9685 块行 + 引用硬件的 play() 展开)
|
||||
初始化:重启 → SLEEP → 设 prescale(50Hz) → 唤醒"""
|
||||
def call(ctx: dict) -> dict:
|
||||
"""ctx 额外含实时参数(channel/angle);写 4 个 PWM 寄存器"""
|
||||
def destroy(ctx: dict) -> dict:
|
||||
"""复位 PCA9685"""
|
||||
```
|
||||
|
||||
- 插件查找顺序(修复审计 S07):`$PYBOT_MODULES_PATH`(冒号分隔多路径)→ 包内置 `modules/` → 当前工作目录 `./modules/`。
|
||||
- 加载失败抛出含插件名与搜索路径的 `PluginNotFound` 异常。
|
||||
|
||||
---
|
||||
|
||||
## 4. 详细设计
|
||||
|
||||
### 4.1 parser.py
|
||||
|
||||
- **MatrixObject**:`name: str` + `rows: list[list[str]]`;提供 `__getitem__((row, col))`、`index_of_col(row, key)`、`save(writer)`。越界访问抛 `MatrixIndexError(块名, 行, 列)`(修复审计 Q05 的报错可读性)。
|
||||
- **MatrixParser**:保持 FSM 逐字符解析(非正则),状态与仓颉版一致(nl/cm/nm 三标志),原因:`@` 块嵌套注释、行内注释、任意空白分隔的兼容性已被现有脚本验证。
|
||||
- 修复 S01:`submit_line` 用 `len(grp) != len(rows[-1])` 校验列数,不一致抛 `DoesNotSync(行号, 期望列数, 实际列数)`。
|
||||
- 修复 S02:删除逐字符 debug 打印;提供 `--verbose` 时输出到 **stderr**(stdout 保留给未来管道通信)。
|
||||
- 修复 S04:Python 无 Rune/String 混淆问题,换行统一 `"\n"` 字符比较;补充单测覆盖"漏写分号跨行"场景。
|
||||
- **占位符语义**(§2.3-1):`get_mt(obj, row, col)` 递归向上;`parse_mt(obj, row, col, type)` 首行 `-` 返回 `type("0")`。
|
||||
- **MatrixSaver**:保持与仓颉版相同的输出格式(`@Name` / 空格分隔 / `;` 结尾 / `@` 收尾),保证 `--test matrix_o/matrix_2` 的解析↔序列化往返可用。
|
||||
|
||||
### 4.2 hd/i2c.py
|
||||
|
||||
```python
|
||||
I2C_SLAVE = 0x0703
|
||||
class I2CBus:
|
||||
def __init__(self, path): self.fd = os.open(path, os.O_RDWR); _register_lock(self.fd)
|
||||
def set_addr(self, addr): fcntl.ioctl(self.fd, I2C_SLAVE, addr)
|
||||
def write_reg(self, reg, val): os.write(self.fd, bytes([reg, val]))
|
||||
def read_reg(self, reg) -> int: # 返回 0..255 无符号(修复 S06)
|
||||
os.write(self.fd, bytes([reg])); return os.read(self.fd, 1)[0]
|
||||
def transaction(self): # with bus.transaction(): 原子序列(设地址+读写)
|
||||
```
|
||||
|
||||
- 锁注册表:`_LOCKS: dict[int, threading.Lock]` + `_MAIN_LOCK`;`transaction()` 上下文管理器持锁,替代仓颉版 `I2cLock_lock/unlock` 手动配对。
|
||||
- `read_reg` 天然返回无符号 int(`bytes[0]` ∈ 0..255),16 位拼接 `(hh << 8) | ll` 后用 `struct.unpack('>h')` 转有符号——修复 S06 的符号扩展错误且保留传感器数据的正确符号。
|
||||
|
||||
### 4.3 hd/servo.py
|
||||
|
||||
与 `Servo.cj` 逐行对齐(§2.3-5),构造参数 `max/min/offset/set_angle_callback`。补单测:正装(max=90)、反装(max=-180 翻转)、offset 漂移、越界钳位。
|
||||
|
||||
### 4.4 hd/mpu6050.py
|
||||
|
||||
- 寄存器常量与 `MPU6050.cj` 一致;**修复 S05**:`ADDR_0 = 0x68`、`ADDR_1 = 0x69`。
|
||||
- 采样线程:`threading.Thread` + `self._stop = threading.Event()`,循环 `while not self._stop.wait(0.01)`(10ms 周期且可即时打断)——修复 Q06。
|
||||
- 读传感器整体包在 `bus.transaction()` 中(设地址 + 14 字节连读),比仓颉版逐寄存器 12 次 I2C 往返更高效且原子。
|
||||
- 卡尔曼滤波参数保持 Q=20、R=200;roll/pitch 公式不变。
|
||||
- 数据出口:`latest()` 返回带 `threading.Lock` 保护的最近一帧滤波值(仓颉版 mutex 字段从未真正使用,此处补全)。
|
||||
|
||||
### 4.5 hd/speaker.py(决策 S03)
|
||||
|
||||
仓颉版 `play()` 仅 sleep(实声 C++ 函数被注释)。Python 版决策:**实现真实发声,带静默降级**。
|
||||
|
||||
- 首选:标准库 `wave` + `subprocess.run(["aplay", "-q", tmp.wav])`——树莓派自带 alsa-utils,零 pip 依赖;正弦波生成用 `math.sin`(每次 ≤ 数秒 @48kHz,纯 Python 可接受),保留 5ms 淡入淡出。
|
||||
- `aplay` 不存在或播放失败:log 一次警告后仅 sleep(delay),行为与仓颉版一致(延时职责不丢失)。
|
||||
- `delay` 语义不变:`play()` 阻塞 `delay` 毫秒(发声与延时并行时取延时为准)。
|
||||
|
||||
### 4.6 robot.py / hardvars.py
|
||||
|
||||
- `RobotStruct` 保持仓颉版结构:`C: list[MatrixObject]` + `M: dict[str, HardwareVar]`(Python dict 有序,天然满足"倒序 destroy"需求)。
|
||||
- `create_hardvars()` 分发表:`{"I2C": I2CVar, "Alias": AliasVar, "Servo": ServoVar, "Speaker": SpeakerVar, "MPU6050_BIND": MPU6050Var}`;`Action` 跳过;未知块 → `plugin.PluginVar`。
|
||||
- 重名键检查(仓颉版 FIXME r.cj:911):同名硬件变量重复定义时,先 `destroy()` 旧值再覆盖,并输出 stderr 警告。
|
||||
- `run()` 逐行执行 `@Action`;列名合法性(必须含一个 `.`)在**执行前**整表校验一次,报错含列号与列名(配合 Q05 修复)。
|
||||
- 上下文管理器:`__enter__`→`active()`,`__exit__`→`deactive()`;`main.py` 用 `with` 替代仓颉版 try/finally。
|
||||
|
||||
### 4.7 main.py
|
||||
|
||||
| 仓颉版 CLI | Python 版 | 说明 |
|
||||
|-----------|-----------|------|
|
||||
| (无参)stdin 管道 | 同 | `cat std.mt | ./bot` 不变;另支持 `pybot path/to/x.mt` 位置参数(增强,非破坏) |
|
||||
| `--help` | argparse 自动生成 | 文案更新 |
|
||||
| `--test nn/cnn/cnn1` | **移除**(见 §1.3) | NN 由 actpred 承担 |
|
||||
| `--test matrix_o/matrix_2` | `--test roundtrip <in.mt> <out.mt>` | 解析↔序列化往返测试,路径改为参数(仓颉版硬编码 `ss/test.mt`) |
|
||||
| `-i` / `-o`(DRL 预留) | 保留参数位,标注 reserved | 未来对接 actpred |
|
||||
| (新增)`--dry-run` | 解析 + 构建硬件变量但不开 I2C,Mock 总线打印动作序列 | 无硬件调试 .mt 脚本 |
|
||||
| (新增)`--verbose` | FSM 与硬件调用日志到 stderr | 替代 S02 删掉的调试打印 |
|
||||
|
||||
### 4.8 日志规范
|
||||
|
||||
- 所有诊断输出走 `logging`(stderr),stdout 保留纯净——为未来 actpred 管道集成预留(审计 §六 的阻塞项 S02 根治)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 审计发现修复映射表
|
||||
|
||||
| 审计 ID | 级别 | 仓颉版问题 | Python 版修复方式 | 落点 |
|
||||
|---------|------|-----------|------------------|------|
|
||||
| S01 | P0 | submitLine 列数校验恒假 | `len(grp) != len(rows[-1])` 显式校验 + DoesNotSync 带期望/实际列数 | parser.py |
|
||||
| S02 | P0 | parse() 逐字符 print 污染 stdout | 移除;--verbose 走 stderr logging | parser.py / main.py |
|
||||
| S03 | P0 | 扬声器仅 sleep 无实声 | wave + aplay 实现发声,缺 aplay 静默降级为 sleep | hd/speaker.py |
|
||||
| S04 | P1 | r"\n" 与 r'\n' 转义不一致 | Python 无此坑;补"漏分号跨行"单测防回归 | tests/test_parser.py |
|
||||
| S05 | P1 | MPU6050 ADDR_1 应为 0x69 | `ADDR_1 = 0x69` | hd/mpu6050.py |
|
||||
| S06 | P1 | readI2cReg 返回有符号 Int8 | read_reg 返回 0..255;16 位拼接后 struct 显式转有符号 | hd/i2c.py |
|
||||
| S07 | P1 | Lua 路径硬编码 ./modules/ | `$PYBOT_MODULES_PATH` → 包内置 → cwd 三级查找 | plugin.py |
|
||||
| S08 | P1 | README 已知问题表滞后 | pybot README 重写;bot4cj README 标注维护状态 | README.md |
|
||||
| Q01 | P2 | 无单元测试 | pytest:解析器/占位符/Servo/插件/Mock I2C 全流程 | tests/ |
|
||||
| Q02 | P2 | 大量注释旧代码 | 新代码库不携带 | — |
|
||||
| Q03 | P2 | I2C 锁 O(n) 线性扫描 | dict[fd → Lock] | hd/i2c.py |
|
||||
| Q04 | P2 | Makefile 告警关闭 | 无 C 编译;CI 跑 `python -m pyflakes`(可选) | _set.sh |
|
||||
| Q05 | P2 | 列数不匹配报错不可读 | MatrixIndexError 带块名/行/列 + run() 前整表校验 | parser.py / robot.py |
|
||||
| Q06 | P2 | pthread_cancel 非安全点退出 | threading.Event 优雅停止 | hd/mpu6050.py |
|
||||
| Q07 | P2 | GC 析构不确定,fd 泄漏 | with 上下文管理器 + atexit 兜底 | robot.py / main.py |
|
||||
|
||||
---
|
||||
|
||||
## 6. 测试计划
|
||||
|
||||
### 6.1 单元测试(无硬件,x86 可跑)
|
||||
|
||||
| 用例组 | 覆盖 |
|
||||
|--------|------|
|
||||
| test_parser | 全部 `scripts/*.mt` 解析成功;列数不一致抛 DoesNotSync;漏分号跨行;注释/空行/CRLF;roundtrip(解析→save→再解析等价) |
|
||||
| test_placeholder | `-` 递归沿用;首行 `-` 数值列=0;actpred mt_parser 已统一为同一语义(2026-08),两侧测试互为语义锚点 |
|
||||
| test_servo | 正装/反装翻转/offset/钳位边界 |
|
||||
| test_hardvars | @Alias 转换;@Action 列名拆分;同行参数聚合;重名覆盖警告 |
|
||||
| test_plugin | 插件三级路径查找;PluginNotFound 报错内容;PCA9685.py 用 Mock I2C 断言寄存器写序列(prescale=121、reg=0x06+4*ch、offval 字节序) |
|
||||
| test_mpu6050 | Mock 总线注入 14 字节样本 → 有符号拼接正确(高字节 ≥ 0x80 场景,回归 S06);Event 停止 1s 内退出 |
|
||||
|
||||
### 6.2 实机验证(树莓派 + Dashun 蜘蛛机器人)
|
||||
|
||||
1. `cat scripts/std.mt | bash _run.sh`:站立姿态与仓颉版目测一致(10 舵机角度逐一对照 --verbose 日志)。
|
||||
2. `jump.mt` / `lie-down.mt` / `tilt-*.mt` 逐个回放,动作时序无可感知差异。
|
||||
3. 双 MPU6050(0x68 + 0x69)同总线并行采样(回归 S05),roll/pitch 数值与仓颉版单设备输出同量级。
|
||||
4. 扬声器 `spk.F` 发声可闻(回归 S03),无爆音(淡入淡出生效)。
|
||||
5. Ctrl-C 中断:舵机停止、I2C fd 全部释放(`ls /proc/<pid>/fd` 验证)、MPU6050 线程退出。
|
||||
|
||||
### 6.3 交叉验证
|
||||
|
||||
- `--dry-run` 模式下用 Mock 总线记录全部 I2C 写序列,与仓颉版(打补丁记录)同脚本输出逐字节 diff——作为迁移等价性的最强证据(可参考 captcha2 迁移的像素级验证经验)。
|
||||
|
||||
---
|
||||
|
||||
## 7. 里程碑
|
||||
|
||||
| 阶段 | 内容 | 产出 |
|
||||
|------|------|------|
|
||||
| M1 解析器 | parser.py + 全量单测 + roundtrip | 全部 scripts/*.mt 解析通过 |
|
||||
| M2 硬件抽象 | hardvars.py + robot.py + plugin.py + Mock I2C | --dry-run 全流程可跑(x86) |
|
||||
| M3 硬件落地 | i2c.py / servo.py / mpu6050.py / speaker.py + modules/*.py | 树莓派实机 std.mt 站立 |
|
||||
| M4 等价性验证 | §6.2 实机回放 + §6.3 写序列 diff | 验证报告 |
|
||||
| M5 切换 | README/INDEX 更新、bot4cj 标注 legacy、aibox/BotOps 若有引用改指 pybot | 并存期结束评估 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 风险与对策
|
||||
|
||||
| 风险 | 影响 | 对策 |
|
||||
|------|------|------|
|
||||
| Python GIL + 解释器抖动影响动作时序 | 舵机动作节奏轻微不均 | 动作行间延时 ≥ 20ms,抖动 <1ms 不可感知;MPU6050 采样用 Event.wait 定时,不忙等;量化依据见附录 A |
|
||||
| aplay 子进程首次启动延迟 | 首个音符滞后 | build() 时预热(播放 0 长度静音);或后续可选接 alsaaudio |
|
||||
| 仓颉版行为的未记录细节 | 等价性缺口 | §6.3 写序列逐字节 diff 兜底 |
|
||||
| .mt 脚本方言分裂(pybot vs actpred 解析器) | 语义漂移 | 占位符语义已统一(2026-08,首行 `-` 均填 0);长期可评估 actpred 复用 pybot parser 的只读模式 |
|
||||
| 树莓派 Python 版本参差 | 语法兼容 | 目标 Python ≥ 3.9(树莓派 OS Bullseye 起自带 3.9) |
|
||||
|
||||
---
|
||||
|
||||
## 9. 与 actpred 的集成(已实现:进程内闭环)
|
||||
|
||||
原规划的"大脑-身体"闭环已在 `run_loop()` 中落地,采用进程内调用替代 FIFO 序列化:
|
||||
|
||||
```
|
||||
camera 帧 → actpred.predict(F帧, H历史) → 动作向量(18) → pybot 下发执行
|
||||
↑ ↓
|
||||
history deque ←────────── 回写实际动作 ←────────────────────┘
|
||||
```
|
||||
|
||||
**用法**:`python3 -m pybot --loop CKPT --hz 10 scripts/std.mt`
|
||||
|
||||
- checkpoint 后缀 `.hef` → Hailo NPU 后端,否则 → PyTorch 后端
|
||||
- `@Action` 表头列数 ≤ `action_dim`(默认 18),列索引即动作向量索引
|
||||
- 闭环中剔除 `spk.delay` 阻塞(发声但不延时),节拍由 `hz` 主循环掌控
|
||||
- 冷启动:F 帧重复同帧 + H 步零动作历史
|
||||
- `--loop-steps N` 限步数调试;`-i/-o` FIFO 保留用于未来跨进程隔离
|
||||
|
||||
**与录制的衔接**:`--record DIR` 产出 actpred 格式 episode → actpred 训练 → 推理 checkpoint → `--loop` 闭环执行,形成完整数据飞轮。
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:性能评估依据
|
||||
|
||||
迁移评审中最常见的两个疑虑是"Python 做驱动会不会慢"与"Python 替代 Lua 会不会引入开销"。本附录给出量化分析,结论:**两者均不构成风险,后者反而是性能收益**。
|
||||
|
||||
### A.1 Python 做硬件驱动的时间尺度分析
|
||||
|
||||
bot4cj 架构中所有微秒级时序都已下沉到专用硬件芯片(PCA9685 自主生成 PWM 波形),主机侧只发送设定值,该路径的瓶颈是 I2C 总线物理速率,与宿主语言无关:
|
||||
|
||||
| 环节 | 耗时量级 | 由谁承担 |
|
||||
|------|---------|---------|
|
||||
| 舵机 PWM 波形(50Hz、0.5~2.5ms 脉宽) | µs 级精度 | PCA9685 硬件芯片,主机不参与 |
|
||||
| I2C 写一个寄存器(100kHz 标准模式) | ~200–300 µs/次 | 总线物理速率,语言无关 |
|
||||
| Python 一次 `os.write`/`ioctl` 调用开销 | ~2–5 µs | 解释器 + 系统调用 |
|
||||
| 设一个舵机角度(4 次寄存器写) | ~1 ms | 总线 ≈ 99%,Python ≈ 1% |
|
||||
| 舵机机械转动 60° | ~100–200 ms | 物理惯性 |
|
||||
| `.mt` 动作行间延时 | 20–500 ms | 脚本显式指定 |
|
||||
|
||||
Python 解释开销比总线时间低约两个数量级,比机械动作低四五个数量级。逐项检查潜在慢点:
|
||||
|
||||
- **MPU6050 10ms 采样循环**:14 字节连读 ≈ 1.5–2ms 总线时间 + 卡尔曼滤波 6 次浮点(纯 Python 数 µs),周期内富余充足;且连读比仓颉版逐寄存器 12 次往返(每次重发从机地址)更快。
|
||||
- **正弦波生成**:48kHz 下每秒音频约 4.8 万次 `math.sin`,纯 Python 10–30ms 算完,发声前一次性生成,可接受。
|
||||
- **GIL/GC**:仅主线程 + 1 个采样线程,GIL 无竞争;GC 停顿亚毫秒级,低于 `sleep()` 本身的 ms 级精度。
|
||||
|
||||
Python 不适用的边界(bot4cj 均不涉及):主机 GPIO bit-bang 生成 PWM/WS2812 单线协议、>1kHz 闭环控制、硬实时保证(Linux 非 RTOS,仓颉版同样做不到)。若未来 DRL 需要高频控制环,正确做法是下沉到协处理器(如 RP2040),属架构问题而非语言问题。
|
||||
|
||||
### A.2 Python 插件替代 Lua 桥接的开销对比
|
||||
|
||||
单论解释器速度 Lua 5.4 快于 CPython,但现有 Lua 路径的开销主要在跨语言桥接而非驱动代码本身。仓颉版每次硬件调用([r.cj MtVars_Lua.things](../../bot4cj/src/r.cj#L311-L331))的实际成本:
|
||||
|
||||
| 步骤 | 实现 | 开销量级 |
|
||||
|------|------|---------|
|
||||
| 元数据深拷贝 | `JsonValue.fromStr(metadata.toJsonString())`——JSON 序列化+反序列化一整轮 | ~10–50 µs |
|
||||
| 参数推入 Lua 表 | 每键 `mallocCString` + `Lua_pushstring/setfield` + `free`,跨仓颉→C→Lua 三层 FFI | ~几 µs/键 |
|
||||
| `Lua_pcallk` + 返回表遍历 | `Lua_next` 循环 + 每键值 `Lua_tostring` 转仓颉 String | ~几 µs |
|
||||
| 强制调试打印 | 仓颉侧 2 次 `println` + Lua 侧 `print_table` 全表打印 | ~几十 µs 至 ms 级(终端 I/O) |
|
||||
| Lua 内 4 次 `i2c_write_reg` | 每次跨 Lua→C→仓颉 @C 三层 | 桥接 ~1 µs + 总线 ~250 µs |
|
||||
|
||||
合计框架开销 ~0.1–1 ms/次,与 I2C 总线时间同量级。Python 插件路径的对应成本:
|
||||
|
||||
| 步骤 | 实现 | 开销量级 |
|
||||
|------|------|---------|
|
||||
| 上下文构造 | `{**metadata, **params}` dict 合并(同进程传引用,无序列化) | ~0.5 µs |
|
||||
| 插件调用 | 同解释器直接函数调用 | ~0.1 µs |
|
||||
| I2C 写 | 插件直接 `i2c.write_reg()`,一层 `os.write` 到底 | 系统调用 ~2 µs + 总线 ~250 µs |
|
||||
| 诊断输出 | 默认关闭,`--verbose` 时走 stderr logging(即审计 S02 修复) | 0 |
|
||||
|
||||
框架开销 ~1–2 µs/次,比 Lua 桥接低约两个数量级。插件内部计算(脉宽换算、移位掩码)在两种解释器上的差距为百纳秒级,相对 1ms 总线时间可忽略。
|
||||
|
||||
**结论**:Lua 层在仓颉版存在的唯一理由是"编译型宿主做不了运行时扩展",为此付出 JSON 往返 + 五层 FFI(仓颉→C→Lua→C→仓颉→glibc)的税;宿主换成 Python 后调用链缩短为 Python→glibc 一层,此项迁移是删除一整层间接性,而非语言互换。
|
||||
|
||||
---
|
||||
|
||||
## 参考文档
|
||||
|
||||
- [bot4cj 代码审计报告](../reviews/bot4cj_CODE_AUDIT_20260808.md)(15 项发现)
|
||||
- [bot4cj 深度分析报告](bot4cj_analysis_20260525_00.md)(D1-D29)
|
||||
- [bot4cj README.robot.md](../../bot4cj/README.robot.md)
|
||||
- [actpred mt_parser.py](../../actpred/mt_parser.py)(Python 解析器先例)
|
||||
|
||||
---
|
||||
|
||||
*本文档为开发计划,实施前需结合实际硬件环境评估。*
|
||||
@@ -1,750 +0,0 @@
|
||||
# captcha 模块 C++20 重写 · 开发方案
|
||||
|
||||
> 文档类型:模块重写开发计划
|
||||
> 模块:captcha(验证码服务)
|
||||
> 日期:2026-08-09
|
||||
> 参照:dnfs2 / db2 / fsys 的 Python → C++20 重写路径
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 背景与动机
|
||||
|
||||
captcha 模块是 micro_servers 中唯一仍未 C++ 化的纯 Python 内部基础服务(smtp 同为 Python 但依赖外部 SMTP 会话,重写收益有限)。当前 Python 版核心只有 299 行,依赖仅 Pillow + pymsg,属于**小而精的理想重写目标**。
|
||||
|
||||
| 维度 | Python 版现状 | 重写收益 |
|
||||
|------|-------------|----------|
|
||||
| 依赖 | Python venv + Pillow + pymsg 本地包 | 消除 Python 运行时依赖,编译为单二进制 |
|
||||
| 启动 | `_run.sh` 激活 venv 后 `exec python3` | 与 db2/dnfs2/fsys 一致的 `./captcha2.bin` 直接启动 |
|
||||
| 性能 | PIL 图片生成是 CPU 密集型,单线程 GIL 受限 | 原生 C++ 无 GIL,`workers>0` 时真实并发 |
|
||||
| 审计遗留 | P2 `random.choices` 非安全随机、NEW-2 无速率限制 | 一并用 `std::mt19937` + `random_device` 修复 |
|
||||
| 一致性 | 项目已有 3 个成功的 C++ 重写案例(db2/dnfs2/fsys) | 与 gateway/db2/dnfs2/fsys 统一技术栈 |
|
||||
|
||||
### 1.2 设计目标
|
||||
|
||||
1. **协议完全兼容**:pymsg 三条路由(`image` / `verify` / `health`)请求/响应格式与 Python 版字节级一致,gateway 零改动
|
||||
2. **图片效果一致**:160×50 PNG,相同干扰策略(旋转字符 + 干扰线 + 噪点),视觉效果等同,机器识别难度不降低
|
||||
3. **安全对齐**:修复审计报告中的 `random` → 安全随机、添加生成速率限制
|
||||
4. **零新增重型依赖**:仅引入 FreeType(libfreetype-dev),已经是 Linux 标配;PNG 编码使用单头文件 stb_image_write.h(公共领域)
|
||||
5. **基础设施复用**:libmsg(RpcServer/AsyncQueue)、boost::json、Conf.hpp 模式,所有已在项目内的组件
|
||||
|
||||
### 1.3 与 gateway 内置 CAPTCHA 的关系
|
||||
|
||||
gateway 内部保留了一套旧版本地 CAPTCHA(`generate_captcha_text()` + `verify_captcha()` + `_CAPTCHA_STORE`),与 captcha 服务通过 pymsg 调用的 `verify_captcha_remote()` **并存**。本次重写仅涉及独立的 captcha 服务进程,不影响 gateway 内部逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状分析
|
||||
|
||||
### 2.1 源码结构
|
||||
|
||||
```
|
||||
captcha/
|
||||
├── captcha_service.py # 299 行,主程序
|
||||
├── log_client.py # 159 行,异步日志客户端(项目通用 SDK)
|
||||
├── _set.sh # venv 创建 + pip install
|
||||
├── _run.sh # venv 激活 + exec python3
|
||||
└── requirements.txt # ../pymsg + Pillow
|
||||
```
|
||||
|
||||
### 2.2 功能清单
|
||||
|
||||
| 功能 | 代码位置 | 行数 | C++ 等效方案 |
|
||||
|------|---------|------|-------------|
|
||||
| 配置加载(confs → env) | L40–134 | 95 行 | 复用 gateway `Conf.hpp` 的 confs 拉取 + env 覆盖模式 |
|
||||
| `generate_text()` | L145–146 | 2 行 | `std::mt19937` + 字符集数组(审计 NEW-1 修复:用 `random_device` 种子) |
|
||||
| `generate_image()` | L149–182 | 34 行 | FreeType 渲染 + 像素旋转合成 + stb_image_write PNG 编码(约 180 行 C++) |
|
||||
| `CaptchaServer.__init__` | L192–198 | 7 行 | `RpcServer` 构造 + `add_route` 注册三个路由 |
|
||||
| `_handle_image` | L215–241 | 27 行 | 逻辑完全相同:生成 text → 惰性清理 → 容量检查 → 存 store → 调 generate_image → base64 编码返回 |
|
||||
| `_handle_verify` | L243–269 | 27 行 | 逻辑完全相同:pop token → 不存在返回 invalid → XOR 恒定时间比较 |
|
||||
| `_handle_health` | L271–272 | 2 行 | 返回 `{"status":"ok","service":"captcha"}` |
|
||||
| `main` / argparse | L279–299 | 21 行 | boost::program_options(与 gateway 一致) |
|
||||
| log_client | 整个文件 | 159 行 | 基于 libmsg `AsyncQueue`,参照 gateway `LogC.hpp`(约 60 行) |
|
||||
|
||||
### 2.3 `generate_image()` 详细分析
|
||||
|
||||
当前 Python 版图片生成流程(这是 C++ 重写的核心难点):
|
||||
|
||||
```
|
||||
1. Image.new('RGB', (160, 50), random_bg_color)
|
||||
→ 创建 RGB 画布,随机背景色
|
||||
|
||||
2. for each char in text (4 chars):
|
||||
a. random_color → 随机前景色
|
||||
b. random_y (2~12) → 垂直偏移
|
||||
c. random_x (15 + i*32 ± 3) → 水平分布在 160px 宽画布上
|
||||
d. ImageFont.truetype(font_path, random_size 20~38) → 可变字号
|
||||
e. 创建 64×64 透明子画布 → draw_char.text((20,20), char)
|
||||
f. char.rotate(random_angle ±30°) → 旋转字符
|
||||
g. img.paste(rotated_char, (x-32, y-32), mask=rotated_char)
|
||||
→ Alpha 合成到主画布
|
||||
|
||||
3. 4 条随机干扰线(Bresenham)
|
||||
|
||||
4. 50 个随机噪点(单像素)
|
||||
|
||||
5. img.save(buf, format='PNG') → PNG 编码
|
||||
```
|
||||
|
||||
每个步骤的 C++ 等价实现:
|
||||
|
||||
| 步骤 | Python (Pillow) | C++ (FreeType + 手写) |
|
||||
|------|----------------|----------------------|
|
||||
| 1. 创建画布 | `Image.new('RGB', 160, 50)` | `std::vector<uint8_t> pixels(160*50*3)` |
|
||||
| 2a. 随机颜色 | `random.randint(0,256)` | `std::uniform_int_distribution<int>(0, 255)` |
|
||||
| 2d. 字体加载 | `ImageFont.truetype(path, size)` | `FT_New_Face` + `FT_Set_Char_Size` |
|
||||
| 2e. 字符渲染 | `draw_char.text((20,20), ch)` | `FT_Load_Char(face, ch, FT_LOAD_RENDER)` → 灰度位图 |
|
||||
| 2f. 旋转 | `char.rotate(angle, expand=True)` | 手写最近邻插值旋转(~40 行) |
|
||||
| 2g. Alpha 合成 | `img.paste(char, pos, mask=char)` | 像素级 alpha blend(灰度值 → 透明度) |
|
||||
| 3. 干扰线 | `draw.line([...])` | Bresenham 画线算法 |
|
||||
| 4. 噪点 | `draw.point((x, y))` | 直接写像素 |
|
||||
| 5. PNG 编码 | `img.save(buf, format='PNG')` | `stbi_write_png_to_func` |
|
||||
|
||||
### 2.4 依赖替换矩阵
|
||||
|
||||
| Python 依赖 | C++ 替代 | 引入方式 |
|
||||
|------------|---------|----------|
|
||||
| `Pillow` (PIL) | FreeType 2 + stb_image_write.h | `apt install libfreetype-dev` + 单头文件放入 `inc/` |
|
||||
| `pymsg` | libmsg (`RpcServer` / `AsyncQueue`) | 系统已安装 (`libus_msg.so`) |
|
||||
| `log_client.py` | 独立 `LogC.hpp`(基于 `AsyncQueue`) | 参照 gateway `LogC.hpp` 简化版 |
|
||||
| `configparser` | boost::property_tree(`Conf.hpp` 模式) | 系统已安装 (`libboost-all-dev`) |
|
||||
| `argparse` | boost::program_options | 系统已安装 |
|
||||
| `secrets` / `random` | `<random>` (`mt19937` + `random_device`) | C++11 标准库 |
|
||||
| `base64` | libcrypt `Base64` 或手写(~20 行) | 系统已安装 (`libus_crypt.so`) 或内联 |
|
||||
| `threading.Lock` | `std::mutex` + `std::lock_guard` | C++11 标准库 |
|
||||
| `json` | `boost::json` | 系统已安装 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 整体设计
|
||||
|
||||
### 3.1 模块结构
|
||||
|
||||
```
|
||||
captcha2/
|
||||
├── src/
|
||||
│ └── main.cpp # ~420 行,主程序
|
||||
│ ├── 配置加载 # confs captcha.ini → env override(~80 行)
|
||||
│ ├── CaptchaStore # 线程安全 Token 存储(~40 行)
|
||||
│ ├── generate_text() # 安全随机文本生成(~15 行)
|
||||
│ ├── generate_image() # FreeType + 像素操作 + PNG 编码(~180 行)
|
||||
│ ├── base64_encode() # 手写或复用 libcrypt(~20 行)
|
||||
│ └── main() # RpcServer 路由注册 + 启动(~85 行)
|
||||
├── inc/
|
||||
│ ├── stb_image_write.h # 单头文件 PNG 编码器(公共领域,~1800 行)
|
||||
│ └── LogC.hpp # 简化版异步日志客户端(~60 行)
|
||||
├── Makefile # 构建(~30 行)
|
||||
├── _set.sh # make + install(~10 行)
|
||||
├── _run.sh # 启动脚本(~5 行)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 3.2 与现有 C++ 基础库的关系
|
||||
|
||||
```
|
||||
captcha2.bin
|
||||
│
|
||||
├── libmsg (RpcServer) ← pymsg 协议 C++ 实现,路由分发 + 响应包装
|
||||
│ └── libus_msg.so (系统已安装)
|
||||
│
|
||||
├── libmsg (AsyncQueue) ← 异步批量日志推送
|
||||
│ └── libus_msg.so
|
||||
│
|
||||
├── boost::json ← JSON 解析/序列化
|
||||
│ └── libboost_json (系统已安装)
|
||||
│
|
||||
├── boost::program_options ← 命令行参数解析
|
||||
│ └── libboost_program_options(系统已安装)
|
||||
│
|
||||
├── FreeType 2 ← 字体渲染
|
||||
│ └── libfreetype.so (需新增,apt install libfreetype-dev)
|
||||
│
|
||||
└── stb_image_write.h ← PNG 编码(header-only,直接放入 inc/)
|
||||
```
|
||||
|
||||
### 3.3 配置体系
|
||||
|
||||
与 gateway/db2/dnfs2/fsys 一致的**两层加载**模式:
|
||||
|
||||
1. **confs 统一配置** → 启动时 pymsg `get` 路由拉取 `captcha.ini`(`retry_on_refused=true` 阻塞等待 confs 就绪)
|
||||
2. **环境变量覆盖**(最高优先级,Docker 兼容)
|
||||
|
||||
新增可选配置项(审计 NEW-2 修复):
|
||||
|
||||
```ini
|
||||
[captcha]
|
||||
len = 4 # 验证码字符数(不变)
|
||||
ttl = 300 # Token 有效期,秒(不变)
|
||||
font_path = /usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf # 字体路径(不变)
|
||||
max_store = 10000 # 最大 Token 存储数(Python 版已有)
|
||||
rate_limit_per_min = 60 # [新增] 每分钟最多生成次数,0=不限制,默认 60
|
||||
```
|
||||
|
||||
配置白名单合并(复用 Python 版的 `_merge_ini` 策略,仅覆盖已知 section/key):
|
||||
|
||||
```cpp
|
||||
struct CaptchaConfig {
|
||||
// [server]
|
||||
std::string host = "127.0.0.1";
|
||||
int port = 8009;
|
||||
int workers = 4; // RpcServer 工作线程数
|
||||
|
||||
// [captcha]
|
||||
int len = 4;
|
||||
int ttl = 300;
|
||||
std::string font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf";
|
||||
int max_store = 10000;
|
||||
int rate_limit_per_min = 60; // 0 = 不限制
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 详细设计
|
||||
|
||||
### 4.1 CaptchaStore — 线程安全 Token 存储
|
||||
|
||||
```cpp
|
||||
class CaptchaStore {
|
||||
public:
|
||||
struct Entry {
|
||||
std::string answer; // 小写答案
|
||||
std::chrono::steady_clock::time_point created;
|
||||
};
|
||||
|
||||
// 生成新 Token,返回 token 字符串;容量满时抛出 std::runtime_error
|
||||
std::string generate(const std::string& answer, int ttl_seconds, int max_size);
|
||||
|
||||
// 校验并消费 Token(一次性);返回 true 表示答案正确
|
||||
// 无效/过期 Token 返回 false
|
||||
bool verify(const std::string& token, const std::string& answer, int ttl_seconds);
|
||||
|
||||
// 当前存储数量
|
||||
size_t size() const;
|
||||
|
||||
private:
|
||||
void expire_locked(int ttl_seconds); // 惰性清理过期项
|
||||
|
||||
mutable std::mutex mtx_;
|
||||
std::unordered_map<std::string, Entry> store_;
|
||||
};
|
||||
```
|
||||
|
||||
**与 Python 版的关键差异:**
|
||||
- `generate()` 和 `verify()` 都在锁内先清理过期项
|
||||
- Token 生成使用 256-bit 安全随机(`std::random_device` + `std::mt19937` + hex 编码),替代 Python 版 `secrets.token_hex(16)` 的 128-bit → 升级为 256-bit
|
||||
- 容量检查同 Python 版:清理过期后仍超限则拒绝生成
|
||||
|
||||
### 4.2 generate_text() — 安全随机验证码文本
|
||||
|
||||
```cpp
|
||||
std::string generate_text(int len = 4) {
|
||||
// 剔除易混淆字符:l I 1 O 0 o(与 Python 版完全一致)
|
||||
static const char kChars[] =
|
||||
"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
|
||||
static constexpr size_t kNumChars = sizeof(kChars) - 1;
|
||||
|
||||
// thread_local 保证多线程安全(RpcServer workers>0 时并发回调)
|
||||
thread_local std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<size_t> dist(0, kNumChars - 1);
|
||||
|
||||
std::string text;
|
||||
text.reserve(len);
|
||||
for (int i = 0; i < len; ++i)
|
||||
text.push_back(kChars[dist(rng)]);
|
||||
return text;
|
||||
}
|
||||
```
|
||||
|
||||
**审计修复**:Python 版使用 `random.choices`(确定性伪随机),C++ 版用 `std::random_device` 作为种子源,消除可预测性风险。
|
||||
|
||||
### 4.3 generate_image() — 核心图片生成(FreeType + 像素操作)
|
||||
|
||||
这是重写的**技术核心**,约 180 行 C++。以下为伪代码级设计:
|
||||
|
||||
```cpp
|
||||
// 返回值:PNG 字节流
|
||||
std::vector<uint8_t> generate_image(const std::string& text,
|
||||
const std::string& font_path) {
|
||||
constexpr int kWidth = 160, kHeight = 50, kChannels = 3;
|
||||
std::vector<uint8_t> pixels(kWidth * kHeight * kChannels);
|
||||
|
||||
// 1. 随机背景色
|
||||
fill_random_color(pixels, kWidth, kHeight);
|
||||
|
||||
// 2. 初始化 FreeType
|
||||
FT_Library ft;
|
||||
FT_Init_FreeType(&ft);
|
||||
FT_Face face;
|
||||
// 字体缺失时回退逻辑:先尝试指定路径 → 再尝试默认 DejaVu → 失败则报错退出
|
||||
if (FT_New_Face(ft, font_path.c_str(), 0, &face) != 0)
|
||||
FT_New_Face(ft, "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 0, &face);
|
||||
|
||||
// 3. 逐字符渲染
|
||||
thread_local std::mt19937 rng(std::random_device{}());
|
||||
std::uniform_int_distribution<int> angle_dist(-30, 30);
|
||||
std::uniform_int_distribution<int> size_dist(20, 38);
|
||||
std::uniform_int_distribution<int> y_dist(2, 12);
|
||||
std::uniform_int_distribution<int> x_jitter(-3, 3);
|
||||
|
||||
for (size_t i = 0; i < text.size(); ++i) {
|
||||
// 随机颜色、字号、旋转角度、位置
|
||||
auto color = random_color(rng);
|
||||
int font_size = size_dist(rng);
|
||||
FT_Set_Char_Size(face, 0, font_size * 64, 96, 96);
|
||||
|
||||
int angle = angle_dist(rng);
|
||||
int y = y_dist(rng);
|
||||
int x = 15 + static_cast<int>(i) * 32 + x_jitter(rng);
|
||||
|
||||
// FT_Load_Char 渲染为 8-bit 灰度位图
|
||||
FT_Load_Char(face, text[i], FT_LOAD_RENDER);
|
||||
|
||||
// 旋转灰度位图(最近邻插值,expand 模式)
|
||||
auto rotated = rotate_bitmap(face->glyph->bitmap, angle);
|
||||
|
||||
// Alpha 合成到主画布
|
||||
composite_char(pixels, kWidth, kHeight, rotated, x, y, color);
|
||||
}
|
||||
|
||||
FT_Done_Face(face);
|
||||
FT_Done_FreeType(ft);
|
||||
|
||||
// 4. 4 条随机干扰线
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
draw_line(pixels, kWidth, kHeight,
|
||||
random_point(rng), random_point(rng),
|
||||
random_color(rng));
|
||||
}
|
||||
|
||||
// 5. 50 个随机噪点
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
set_pixel(pixels, kWidth, kHeight,
|
||||
random_point(rng), random_color(rng));
|
||||
}
|
||||
|
||||
// 6. PNG 编码
|
||||
std::vector<uint8_t> png_buf;
|
||||
stbi_write_png_to_func(
|
||||
[](void* ctx, void* data, int size) {
|
||||
auto* buf = static_cast<std::vector<uint8_t>*>(ctx);
|
||||
buf->insert(buf->end(), (uint8_t*)data, (uint8_t*)data + size);
|
||||
},
|
||||
&png_buf, kWidth, kHeight, kChannels,
|
||||
pixels.data(), kWidth * kChannels);
|
||||
|
||||
return png_buf;
|
||||
}
|
||||
```
|
||||
|
||||
**关键辅助函数**:
|
||||
|
||||
| 函数 | 说明 | 预估行数 |
|
||||
|------|------|----------|
|
||||
| `fill_random_color(pixels, w, h)` | 填充随机背景色 | 10 |
|
||||
| `random_color(rng)` | 返回 `(R, G, B)` 结构体 | 5 |
|
||||
| `rotate_bitmap(bitmap, angle)` | 最近邻插值旋转灰度位图,expand 模式 | 40 |
|
||||
| `composite_char(pixels, w, h, rotated, x, y, color)` | 将旋转后字符位图以指定颜色 Alpha 合成到主画布;灰度值作为透明度 | 25 |
|
||||
| `draw_line(pixels, w, h, p1, p2, color)` | Bresenham 画线 | 20 |
|
||||
| `set_pixel(pixels, w, h, point, color)` | 设置单像素(带边界检查) | 8 |
|
||||
| `random_point(rng)` | 返回画布内随机点 `(x, y)` | 5 |
|
||||
| `stbi_write_png_to_func` 的回调封装 | 将 stb 输出收集到 `vector<uint8_t>` | 10 |
|
||||
|
||||
**FreeType 字体回退策略**(与 Pillow 行为对齐):
|
||||
|
||||
```
|
||||
1. 尝试 FT_New_Face(font_path) → 成功则使用
|
||||
2. 失败 → 尝试 DejaVuSans-Bold.ttf 默认路径
|
||||
3. 失败 → 打印错误到 stderr,退出进程(验证码无字体无法工作)
|
||||
(Python 版 Pillow 回退到 load_default() 位图字体无抗锯齿视觉效果差;
|
||||
C++ 版选择快速失败,在部署阶段确保字体安装)
|
||||
```
|
||||
|
||||
### 4.4 RpcServer 路由注册
|
||||
|
||||
```cpp
|
||||
int main(int argc, char* argv[]) {
|
||||
// 1. 命令行解析(boost::program_options)
|
||||
// 2. 配置加载:confs captcha.ini → 环境变量覆盖 → 完整性校验
|
||||
// 3. 初始化 CaptchaStore、字体路径
|
||||
// 4. 初始化日志客户端(AsyncQueue → db Logs 表)
|
||||
|
||||
pmc::net::RpcServer rpc(config.host, config.port,
|
||||
{.workers = config.workers});
|
||||
|
||||
CaptchaStore store;
|
||||
|
||||
// 路由
|
||||
rpc.add_route("image", [&](const json::object&) -> json::value {
|
||||
// 速率限制检查(审计 NEW-2)
|
||||
std::string text = generate_text(config.len);
|
||||
std::string token = store.generate(text, config.ttl, config.max_store);
|
||||
auto png = generate_image(text, config.font_path);
|
||||
json::object data;
|
||||
data["token"] = token;
|
||||
data["image"] = base64_encode(png);
|
||||
return data;
|
||||
});
|
||||
|
||||
rpc.add_route("verify", [&](const json::object& req) -> json::value {
|
||||
std::string token = json::value_to<std::string>(req.at("token"));
|
||||
std::string answer = json::value_to<std::string>(req.at("answer"));
|
||||
// 小写化
|
||||
std::transform(answer.begin(), answer.end(), answer.begin(), ::tolower);
|
||||
bool valid = store.verify(token, answer, config.ttl);
|
||||
json::object data;
|
||||
data["valid"] = valid;
|
||||
return data;
|
||||
});
|
||||
|
||||
rpc.add_route("health", [](const json::object&) -> json::value {
|
||||
json::object data;
|
||||
data["status"] = "ok";
|
||||
data["service"] = "captcha";
|
||||
return data;
|
||||
});
|
||||
|
||||
logs.info("captcha", "开始监听 tcp://" + config.host + ":" + std::to_string(config.port));
|
||||
rpc.start(); // 阻塞
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 LogC.hpp — 简化版异步日志客户端
|
||||
|
||||
参照 gateway `LogC.hpp`,基于 libmsg `AsyncQueue<LogEntry>`:
|
||||
|
||||
```cpp
|
||||
// ~60 行,与 log_client.py 功能等价
|
||||
// - 异步批量推送(攒满 50 条或 2 秒 flush)
|
||||
// - fail-safe(db 不可用静默丢弃)
|
||||
// - push()/info()/warning()/error() API 不变
|
||||
// - 环境变量 DB_HOST/DB_PORT 覆盖 db 地址
|
||||
```
|
||||
|
||||
### 4.6 速率限制(审计 NEW-2 修复)
|
||||
|
||||
在 `image` 路由中增加滑动窗口限流:
|
||||
|
||||
```cpp
|
||||
class RateLimiter {
|
||||
public:
|
||||
// 返回 true 表示允许,false 表示超限
|
||||
bool allow(int max_per_minute = 60) {
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
|
||||
// 清理超过 60 秒的旧记录
|
||||
while (!timestamps_.empty() &&
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
now - timestamps_.front()).count() > 60) {
|
||||
timestamps_.pop_front();
|
||||
}
|
||||
|
||||
if ((int)timestamps_.size() >= max_per_minute)
|
||||
return false;
|
||||
|
||||
timestamps_.push_back(now);
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
std::deque<std::chrono::steady_clock::time_point> timestamps_;
|
||||
std::mutex mtx_;
|
||||
};
|
||||
```
|
||||
|
||||
通过在 `_handle_image` 入口处调用 `rate_limiter.allow()` 实现,超限时返回:
|
||||
|
||||
```json
|
||||
{"success": false, "error": "rate limit exceeded, please try again later"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 实现计划
|
||||
|
||||
### 5.1 分阶段交付
|
||||
|
||||
| 阶段 | 里程碑 | 关键产出 | 预估工时 |
|
||||
|------|--------|----------|----------|
|
||||
| **P0 — 基础设施** | 目录骨架 + Makefile + stb 引入 + LogC.hpp | `captcha2/` 结构搭建,能编译出空 `main()` 链接 libmsg + FreeType | 1h |
|
||||
| **P1 — 图片生成核心** | `generate_image()` 全链路 | FreeType 渲染 + 旋转 + 合成 + 干扰线 + 噪点 + PNG 编码,视觉效果与 Python 版对比 | 5h |
|
||||
| **P2 — CaptchaStore** | Token 存储 + 过期清理 + 容量保护 | 线程安全 `generate()` / `verify()`,单元测试 | 1.5h |
|
||||
| **P3 — RPC 服务集成** | `RpcServer` 三路由 + 命令行参数 | 能与 gateway 联调,pymsg 协议互通 | 2h |
|
||||
| **P4 — 配置加载** | confs 拉取 + env 覆盖 + 配置完整性校验 | 与现有 `captcha.ini` 完全兼容 | 1.5h |
|
||||
| **P5 — 安全加固** | 速率限制 + `mt19937` 安全种子 + 审计遗留修复 | 审计报告 NEW-1/NEW-2 全部修复 | 1h |
|
||||
| **P6 — 日志集成** | LogC.hpp 异步推送日志到 db | 结构化日志(生成/校验/错误),与 Python 版日志格式对齐 | 1h |
|
||||
| **P7 — 测试** | 黑盒测试 + 图片对比 + 压力测试 | `tests/test_captcha2.py` | 2.5h |
|
||||
| **P8 — 文档 + 部署** | README + _set.sh/_run.sh + init.txt 更新 | 完整文档 + 一键安装 + core 自启动 | 0.5h |
|
||||
|
||||
**总计预估:~16 小时**
|
||||
|
||||
### 5.2 P1 图片生成是高风险路径
|
||||
|
||||
P1 阶段需要逐像素对比 Python/C++ 版本的输出,验证视觉效果一致。策略:
|
||||
|
||||
1. **单元级对比**:对同一输入文本,分别运行 Python 和 C++ 版 `generate_image()`,保存 PNG 文件进行人工对比
|
||||
2. **关键差异容忍**:由于 `random` vs `mt19937` 的伪随机序列不同、FreeType 与 Pillow 的字体 hinting 差异,逐像素精确匹配不可能。目标是**同等干扰级别、同等人类可读性、同等机器识别难度**
|
||||
3. **灰度验证**:将两张图片都转灰度后,对比字符位置分布、旋转角度范围、噪点密度等统计特征
|
||||
|
||||
### 5.3 构建系统
|
||||
|
||||
```makefile
|
||||
# captcha2/Makefile
|
||||
CXX = g++
|
||||
CXXFLAGS = -std=c++20 -Wall -O2
|
||||
INCS = -I./inc -I/usr/include/freetype2
|
||||
LIBS = -lus_msg -lboost_json -lboost_program_options -lfreetype -lpthread
|
||||
|
||||
TARGET = captcha2.bin
|
||||
SRC = src/main.cpp
|
||||
|
||||
$(TARGET): $(SRC)
|
||||
$(CXX) $(CXXFLAGS) $(INCS) $^ $(LIBS) -o $@
|
||||
|
||||
install: $(TARGET)
|
||||
cp $(TARGET) /usr/local/bin/
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET)
|
||||
```
|
||||
|
||||
> **说明**:stb_image_write.h 是单头文件(header-only),无需编译链接,直接 `#include` 并在一个翻译单元中 `#define STB_IMAGE_WRITE_IMPLEMENTATION`。FreeType 需链接 `-lfreetype`。
|
||||
|
||||
---
|
||||
|
||||
## 6. 测试计划
|
||||
|
||||
### 6.1 黑盒测试(`tests/test_captcha2.py`)
|
||||
|
||||
复用现有测试框架(`tests/blackbox/helpers.py` 中的 `PymsgClient`),覆盖:
|
||||
|
||||
| 测试用例 | 说明 |
|
||||
|----------|------|
|
||||
| `test_image_generation` | 调 `image` 路由,验证返回合法 token + base64 PNG(解码后为有效 PNG,尺寸 160×50) |
|
||||
| `test_verify_correct` | `image` → `verify(correct_answer)` → `{"valid": true}` |
|
||||
| `test_verify_wrong` | `image` → `verify(wrong_answer)` → `{"valid": false}` |
|
||||
| `test_verify_replay` | `image` → `verify(correct)` → `verify(correct)` → 第二次应返回 `invalid token`(一次性消费) |
|
||||
| `test_verify_expired` | 生成后等待 CAPTCHA_TTL+1 秒再校验 → `invalid token` |
|
||||
| `test_verify_no_token` | `verify` 缺少 token 字段 → `{"valid": false, "error": "token and answer required"}` |
|
||||
| `test_health` | 调 `health` 路由 → `{"status": "ok", "service": "captcha"}` |
|
||||
| `test_max_store` | 连续生成超过 `max_store` 个 → 后续返回 `too many pending captchas` |
|
||||
| `test_rate_limit` | 连续生成超过 `rate_limit_per_min` 次 → 返回 `rate limit exceeded`(与 Python 版的区别:Python 版暂无限流) |
|
||||
|
||||
### 6.2 图片质量验证
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| 人工 A/B 对比 | 同一文本(固定随机种子),分别用 Python 和 C++ 生成,人工对比 |
|
||||
| 统计特征 | 字符分布范围、旋转角度范围、干扰线数量、噪点密度 → 与 Python 版统计对齐 |
|
||||
| OCR 挑战 | 用简单 OCR 工具(如 tesseract)分别尝试识别两版验证码,识别率应相近 |
|
||||
|
||||
### 6.3 gateway 联调
|
||||
|
||||
1. 启动 captcha2 服务替代 Python captcha
|
||||
2. `GET /captcha/image` → 浏览器正常显示验证码图片
|
||||
3. 登录/注册流程完整走通
|
||||
4. `X-Captcha-Token` 响应头正常
|
||||
|
||||
---
|
||||
|
||||
## 7. 迁移计划
|
||||
|
||||
### 7.1 渐进切换
|
||||
|
||||
```
|
||||
阶段 1: captcha2.bin 开发完成,通过全部测试
|
||||
↓
|
||||
阶段 2: 在测试环境与 Python captcha 并行运行(不同端口),验证
|
||||
↓
|
||||
阶段 3: 修改 core/init.txt,将 captcha/_run.sh 替换为 captcha2/_run.sh
|
||||
↓
|
||||
阶段 4: 生产切换,观察 1 周无异常后删除 Python captcha 目录
|
||||
```
|
||||
|
||||
### 7.2 gateway 零改动保证
|
||||
|
||||
- pymsg 协议不变(JSON RPC 约定不变)
|
||||
- 监听地址不变(`127.0.0.1:8009`)
|
||||
- `captcha.ini` 配置不变
|
||||
- gateway 的 `verify_captcha_remote()` 和 `handleCaptchaImage()` 通过 pymsg `MsgClient` 调用,与 Python 版字节级兼容
|
||||
|
||||
### 7.3 回滚方案
|
||||
|
||||
- `init.txt` 中恢复 `bash /opt/micro_servers/captcha/_run.sh`
|
||||
- Python venv 保持不变(不删除),可立即回滚
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全考量
|
||||
|
||||
### 8.1 审计遗留修复
|
||||
|
||||
| 编号 | 问题 | 修复方式 |
|
||||
|------|------|----------|
|
||||
| NEW-1 | `generate_text()` 使用 `random.choices` 非安全随机 | C++ 版使用 `std::mt19937` 以 `std::random_device` 种子,Token 使用 256-bit `random_device` 直接生成 |
|
||||
| NEW-2 | 无生成速率限制,可被用于 CPU 资源消耗攻击 | 新增滑动窗口限流(默认 60 次/分钟,可配 `rate_limit_per_min`) |
|
||||
|
||||
### 8.2 已有安全机制保持
|
||||
|
||||
| 机制 | Python 版 | C++ 版 |
|
||||
|------|----------|--------|
|
||||
| Token 一次性使用 | `captcha_store.pop(token)` | `store_.erase(it)` 后返回 |
|
||||
| 5 分钟过期 | 惰性清理 | 惰性清理(与 Python 版一致) |
|
||||
| 常数时间比较 | XOR 逐字符 | XOR 逐字符(与 Python 版一致) |
|
||||
| 仅监听 127.0.0.1 | `RpcServer("127.0.0.1", ...)` | 同 |
|
||||
| 容量上限 | `MAX_STORE_SIZE = 10000` | 同 |
|
||||
| PNG base64 编码 | `base64.b64encode` | 手写或 libcrypt `Base64` |
|
||||
| 配置白名单合并 | `_merge_ini` 仅覆盖已知 key | `CaptchaConfig` 仅解析已知字段 |
|
||||
| 字体缺失回退 | Pillow `load_default()` | 尝试默认路径 → 失败则退出(快速失败优于静默降级为无抗锯齿位图字体) |
|
||||
|
||||
### 8.3 RpcServer 安全特性(继承自 libmsg)
|
||||
|
||||
- JSON 解析嵌套深度限制 32 层
|
||||
- 错误信息脱敏(不泄露内部路径/类型名)
|
||||
- `recv_timeout` 防 Slowloris(默认 10s)
|
||||
- Handler 异常不导致服务崩溃
|
||||
|
||||
---
|
||||
|
||||
## 9. 新增文件清单
|
||||
|
||||
```
|
||||
captcha2/
|
||||
├── src/
|
||||
│ └── main.cpp # ~420 行,全部实现
|
||||
├── inc/
|
||||
│ ├── stb_image_write.h # 公共领域单头 PNG 编码器(直接引入)
|
||||
│ └── LogC.hpp # ~60 行,简化版异步日志客户端
|
||||
├── Makefile # ~30 行
|
||||
├── _set.sh # make + install(~10 行)
|
||||
├── _run.sh # 启动脚本(~5 行)
|
||||
└── README.md # 模块文档
|
||||
```
|
||||
|
||||
**0 个新增文件需修改现有代码。** gateway / core / waf / init.txt 均零改动。
|
||||
|
||||
---
|
||||
|
||||
## 10. 依赖清单
|
||||
|
||||
| 依赖 | 用途 | 是否新增 | 安装方式 |
|
||||
|------|------|----------|----------|
|
||||
| `libus_msg.so` (libmsg) | RPC 服务端 + 异步日志 | 否 | 已通过 `_inst.sh` 安装 |
|
||||
| `libboost_json` | JSON 解析 | 否 | 已通过 `_setup.deb.sh` 安装 |
|
||||
| `libboost_program_options` | 命令行参数 | 否 | 已通过 `_setup.deb.sh` 安装 |
|
||||
| `libfreetype-dev` | TrueType 字体渲染 | **是** | `apt install libfreetype-dev`(已在 `_setup.deb.sh` 中添加) |
|
||||
| `stb_image_write.h` | PNG 编码 | 形式上是 | 单头文件直接放入 `inc/`,无编译依赖 |
|
||||
|
||||
### 10.1 `_setup.deb.sh` 新增依赖
|
||||
|
||||
在根目录 `_setup.deb.sh` 的 `apt install` 命令中追加 `libfreetype-dev`:
|
||||
|
||||
```bash
|
||||
sudo apt install -y ... libfreetype-dev
|
||||
```
|
||||
|
||||
该包在 Ubuntu 24.04 中预装,大小约 1.2MB(含 `.so`)。
|
||||
|
||||
---
|
||||
|
||||
## 11. 与 Python 版的差异汇总
|
||||
|
||||
| 项目 | Python 版 | C++ 版 | 影响 |
|
||||
|------|----------|--------|------|
|
||||
| 运行时 | Python 3 + venv + Pillow | 单二进制 `captcha2.bin` | 消除 Python 依赖 |
|
||||
| 随机数 | `random.choices` + `secrets.token_hex(16)` | `mt19937(random_device)` + 256-bit random_device | 安全增强 |
|
||||
| 速率限制 | 无 | 滑动窗口 60 次/分钟(可配) | 审计 NEW-2 修复 |
|
||||
| 字体回退 | Pillow `load_default()`(无抗锯齿位图字体) | 快速失败(打印错误退出) | 部署时确保字体安装 |
|
||||
| Token 长度 | 128-bit hex (32 字符) | 256-bit hex (64 字符) | 对调用方透明(Token 只做 key 用) |
|
||||
| log_client | Python `AsyncRpcQueue` | C++ `AsyncQueue<LogEntry>` | 功能等价 |
|
||||
| 进程模型 | Python GIL | 原生多线程(`workers` 配置) | 真实并发图片生成 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 风险与应对
|
||||
|
||||
| 风险 | 概率 | 影响 | 应对 |
|
||||
|------|------|------|------|
|
||||
| FreeType 与 Pillow 字体渲染差异大,验证码视觉效果不一致 | 中 | 中 | P1 阶段人工对比 + 统计特征验证;必要时调整旋转/位置参数 |
|
||||
| 像素旋转合成逻辑有 bug,字符变形/丢失 | 低 | 高 | 大量单元测试 + 固定种子生成保存图片对比 |
|
||||
| FreeType 字体缺失导致进程退出 | 低 | 高 | `_setup.deb.sh` 确保 `fonts-dejavu-core` 安装;启动时显式检查字体文件存在 |
|
||||
| gateway 联调时 pymsg 协议兼容性问题 | 极低 | 高 | libmsg 已在 db2/dnfs2/fsys 中经过充分验证;协议极简 |
|
||||
| `max_store` 容量保护逻辑与 Python 版不一致 | 低 | 低 | 黑盒测试覆盖边界条件 |
|
||||
|
||||
---
|
||||
|
||||
## A. 附录:stb_image_write.h 许可说明
|
||||
|
||||
`stb_image_write.h` 是 Sean Barrett 编写的单头文件公共领域库(public domain / MIT),广泛用于游戏引擎和图形工具。将其放入 `captcha2/inc/` 目录即可使用,无需额外安装。文件约 1800 行,提供:
|
||||
|
||||
```c
|
||||
// 主要 API
|
||||
STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride);
|
||||
STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride);
|
||||
```
|
||||
|
||||
本项目使用 `stbi_write_png_to_func`(内存输出),在 `main.cpp` 中:
|
||||
|
||||
```cpp
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "stb_image_write.h"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. 实现后记 · 审计与修复
|
||||
|
||||
### 13.1 实现完成
|
||||
|
||||
2026-08-09,captcha2 按本方案完成全部开发(P0~P6),编译为 434KB 动态链接 ELF 二进制。
|
||||
目录结构:
|
||||
|
||||
```
|
||||
captcha2/
|
||||
├── Makefile # g++ -std=c++20 -O2
|
||||
├── _run.sh / _set.sh # 启动/编译脚本
|
||||
├── inc/
|
||||
│ ├── LogC.hpp # 异步日志客户端(AsyncQueue → db2:8005)
|
||||
│ └── stb_image_write.h # PNG 编码(1724 行,public domain)
|
||||
├── src/
|
||||
│ └── main.cpp # 主程序(672 行)
|
||||
├── test_gen.cpp # 图片生成独立测试
|
||||
├── test_rpc.py # pymsg RPC 全功能测试(7 项自动化)
|
||||
└── captcha2.bin # 编译产物
|
||||
```
|
||||
|
||||
### 13.2 安全审计
|
||||
|
||||
同日完成全量安全审计(参见 `docs/security/captcha2_SECURITY_AUDIT_20260809.md`),
|
||||
发现 11 项问题(P0×1 / P1×1 / P2×3 / P3×6),其中:
|
||||
|
||||
| 编号 | 等级 | 问题 | 状态 |
|
||||
|------|------|------|------|
|
||||
| P0-1 | 🔴 | 存储答案未小写化,含大写字母的验证码约 90% 永远校验失败 | ✅ 已修复 |
|
||||
| P2-2 | 🟡 | `::tolower(char)` 对非 ASCII 输入为 UB | ✅ 已修复 |
|
||||
| P1-1 | 🟠 | 全局速率限制可被单一匿名用户 DoS | ❌ 待修复(需 gateway 层按 IP 限流) |
|
||||
| P2-1 | 🟡 | 答案生成用 mt19937 非密码学安全随机 | ❌ 遗留(与 Python 版同源) |
|
||||
|
||||
### 13.3 P0-1 修复详情
|
||||
|
||||
**根因**: Python 原版存储时即小写化答案(`text.lower()`),C++ 初版直接存原始混合大小写文本,
|
||||
verify 路由仅对用户输入做 lowercase,导致大小写不匹配。
|
||||
|
||||
**修复** (`src/main.cpp` L588-597):
|
||||
```cpp
|
||||
std::string text = generate_text(g_config.len);
|
||||
// 小写化存储,与 Python 版行为对齐(大小写不敏感校验)
|
||||
std::string answer_lc = text;
|
||||
std::transform(answer_lc.begin(), answer_lc.end(), answer_lc.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
token = g_store.generate(answer_lc, g_config.ttl, g_config.max_store);
|
||||
```
|
||||
|
||||
同时修复 `::tolower` UB(L633-636),改用 `unsigned char` lambda 包装。
|
||||
|
||||
### 13.4 init.txt 集成
|
||||
|
||||
`core/init.txt` 已将 `captcha/_run.sh` 替换为 `captcha2/_run.sh`;
|
||||
`confs/configs/captcha.ini` 已补充 `max_store=10000` 和 `rate_limit_per_min=60` 字段。
|
||||
@@ -1,740 +0,0 @@
|
||||
# confs 模块 C++20 重写开发方案
|
||||
|
||||
> 文档类型:功能开发计划
|
||||
> 模块:confs → confs2(C++20 重写)
|
||||
> 作者:自动生成
|
||||
> 日期:2026-08-09
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 背景与动机
|
||||
|
||||
`confs` 统一配置服务当前以 Python 实现,基于 `pymsg RpcServer` 在 `127.0.0.1:8010` 提供配置文件集中托管与分发。5 个内部服务(static、dnfs、db、captcha、redirect80)通过 `pymsg RpcClient` 消费配置。
|
||||
|
||||
本项目已完成 db2、captcha2、smtp3、dnfs2 等 C++20 重写,遵循"原版与重写版并存 → 重写版验证通过后切换"的渐进策略。confs 作为最后一个纯 Python 基础设施服务,其 C++ 重写可:
|
||||
|
||||
| 收益 | 说明 |
|
||||
|------|------|
|
||||
| 内存占用降低 | Python 进程 ~50-100MB RSS → C++ 预估 < 5MB |
|
||||
| 启动速度提升 | Python 解释器启动 ~200ms → C++ 瞬时 |
|
||||
| 依赖链简化 | 消除 Python venv / pip / nuitka 工具链 |
|
||||
| 安全审计遗留修复 | 重写时一并修复 Python 版 7 项遗留审计发现 |
|
||||
| 未来合入 core 的基础 | C++ 同构后,未来可评估合入 core 注册中心 |
|
||||
|
||||
### 1.2 设计目标
|
||||
|
||||
1. **协议零改动**:pymsg RPC 协议、端口 `8010`、路由名(`get`/`list`/`reload`/`health`)、响应格式全部不变,5 个消费方零改动。
|
||||
2. **功能完全对等**:ConfigStore 内存缓存 + mtime 自动重载、INI/JSON 按键取值、扩展名白名单、TOCTOU 防护、文件大小限制——逐项 C++ 实现。
|
||||
3. **零新增系统依赖**:仅链接 `libmsg`(`-lus_msg`)、`boost_json`、`boost_program_options`、`pthread`——均为系统已安装库。
|
||||
4. **安全审计遗留修复**:Python 版 7 项未修复发现(P2-3/P2-4/P3-1~P3-6)在重写中一并解决。
|
||||
5. **配置目录兼容**:复用现有 `confs/configs/` 目录,配置文件零迁移。
|
||||
6. **可演进**:保留向 core 注册中心注册的接口预留,未来合入评估时改动最小。
|
||||
|
||||
### 1.3 不做什么
|
||||
|
||||
- **不改协议**:不引入 HTTP,不改变 pymsg 帧格式,不增减路由。
|
||||
- **不改消费方**:5 个 Python 服务的 `RpcClient` 调用代码不改。
|
||||
- **不引入新解析库**:INI 解析手写(与 captcha2 的 `merge_ini` 风格一致),不引入 `boost::property_tree::ini_parser`(其大小写语义与 Python ConfigParser 不一致)。
|
||||
- **不实现 YAML/TOML 按键取值**:与 Python 版一致,仅返回全文。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状分析
|
||||
|
||||
### 2.1 Python confs 源码结构
|
||||
|
||||
```
|
||||
confs/
|
||||
├── confs.py # 服务主程序(ConfigStore + ConfsServer,361 行)
|
||||
├── log_client.py # 异步日志客户端 SDK(160 行)
|
||||
├── configs/ # 配置文件存放目录(9 个 .ini 文件)
|
||||
│ ├── ca.ini
|
||||
│ ├── captcha.ini
|
||||
│ ├── db.ini
|
||||
│ ├── dnfs.ini
|
||||
│ ├── example.ini
|
||||
│ ├── gateway.ini
|
||||
│ ├── redirect80.ini
|
||||
│ ├── smtp.ini
|
||||
│ └── static.ini
|
||||
├── _run.sh # 启动脚本(venv 中 exec python3 confs.py)
|
||||
├── _set.sh # 初始化脚本(建 venv + pip install)
|
||||
├── requirements.txt # 依赖(pymsg + nuitka)
|
||||
└── README.md
|
||||
```
|
||||
|
||||
### 2.2 关键组件
|
||||
|
||||
| 组件 | 职责 | 行数 |
|
||||
|------|------|------|
|
||||
| `ConfigStore` | 磁盘 configs/ 目录 + 内存缓存 + mtime 自动重载 + 路径安全 | ~120 行 |
|
||||
| `ConfsServer` | RpcServer 入口,路由分发(get/list/reload/health) | ~90 行 |
|
||||
| `_extract_key()` | INI `Section.option` 取值 + JSON 点路径取值 | ~40 行 |
|
||||
| `log_client.py` | 异步批量推送日志到 db:8005 | 160 行 |
|
||||
|
||||
### 2.3 现有路由
|
||||
|
||||
| 路由 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `get` | `{"name": "db.ini"}` | 取配置全文;带 `key` 时返回单键值(仅 ini/json) |
|
||||
| `list` | `{}` | 列出配置目录全部文件及元信息 |
|
||||
| `reload` | `{"name": "db.ini"}` 或 `{}` | 重载指定配置;省略 name 清空全部缓存 |
|
||||
| `health` | `{}` | 健康检查 |
|
||||
|
||||
### 2.4 消费方清单
|
||||
|
||||
| 服务 | 配置文件 | 接入方式 |
|
||||
|------|---------|---------|
|
||||
| static | `static.ini` | pymsg `RpcClient` + `retry_on_refused=True` |
|
||||
| dnfs | `dnfs.ini` | pymsg `RpcClient` + `retry_on_refused=True` |
|
||||
| db | `db.ini` | pymsg `RpcClient` + `retry_on_refused=True` |
|
||||
| captcha | `captcha.ini` | pymsg `RpcClient` + `retry_on_refused=True` |
|
||||
| redirect80 | `redirect80.ini` | pymsg `RpcClient` + `retry_on_refused=True` |
|
||||
|
||||
### 2.5 Python 版安全审计遗留(7 项)
|
||||
|
||||
| 编号 | 级别 | 问题 | 重写修复方案 |
|
||||
|------|------|------|-------------|
|
||||
| P2-3 | P2 | 配置名/key 直接拼入日志,未过滤控制字符 | 日志消息过滤 `\n`/`\r`/ANSI 转义 |
|
||||
| P2-4 | P2 | `list()` 持锁遍历目录+stat | 拷贝文件名列表后释放锁再 stat |
|
||||
| P3-1 | P3 | `_run.sh` 有 `sleep` hack | 移除 sleep,靠 init.txt 启动顺序 |
|
||||
| P3-2 | P3 | `_set.sh` 硬编码 pip 镜像源 | C++ 重写无需 pip,`_set.sh` 改为 `make` |
|
||||
| P3-3 | P3 | `_set.sh` 缺少 `set -e` | `_set.sh` 加 `set -euo pipefail` |
|
||||
| P3-4 | P3 | requirements.txt 含运行时不需要的 nuitka | C++ 无 requirements.txt |
|
||||
| P3-5 | P3 | ConfigParser 大小写隐式行为未文档化 | INI 解析器明确定义大小写策略 |
|
||||
| P3-6 | P3 | 错误消息暴露底层 IO 异常 | 异常消息脱敏,不返回系统路径 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 整体设计
|
||||
|
||||
### 3.1 架构总览
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ confs2 (C++20, :8010) │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ ConfigStore (线程安全) │ │
|
||||
│ │ ├── 磁盘: configs/ 目录 │ │
|
||||
│ │ ├── 内存缓存: {name → {content, mtime, size}} │ │
|
||||
│ │ ├── mtime 自动重载 │ │
|
||||
│ │ └── 安全: 路径遍历/TOCTOU/扩展名白名单/大小限制 │ │
|
||||
│ └───────────────────────┬────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌───────────────────────▼────────────────────────────┐ │
|
||||
│ │ RpcServer (libmsg, :8010, pymsg 协议) │ │
|
||||
│ │ ├── get → ConfigStore.get() + _extract_key() │ │
|
||||
│ │ ├── list → ConfigStore.list() │ │
|
||||
│ │ ├── reload → ConfigStore.reload() │ │
|
||||
│ │ └── health → 静态响应 │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────────────────────────────────────────┐ │
|
||||
│ │ LogC (异步日志, libmsg AsyncQueue → db:8005) │ │
|
||||
│ └────────────────────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────┘
|
||||
▲
|
||||
│ pymsg RPC (TCP, 127.0.0.1:8010)
|
||||
│
|
||||
┌─────┴─────┬──────┬──────┬──────┬──────┐
|
||||
▼ ▼ ▼ ▼ ▼ ▼
|
||||
static dnfs db captcha redirect80
|
||||
(RpcClient, 消费方零改动)
|
||||
```
|
||||
|
||||
### 3.2 核心组件
|
||||
|
||||
| 组件 | 职责 | 对应 Python |
|
||||
|------|------|-------------|
|
||||
| **ConfigStore** | 配置文件仓库(磁盘+缓存+安全) | `ConfigStore` 类 |
|
||||
| **IniParser** | INI 解析(Section.option 取值) | `configparser.ConfigParser` |
|
||||
| **JsonKeyExtractor** | JSON 点路径取值 | `_extract_key` JSON 分支 |
|
||||
| **LogC** | 异步日志推送 | `log_client.py` |
|
||||
| **RpcServer** | pymsg RPC 服务端(libmsg) | `pymsg.RpcServer` |
|
||||
|
||||
### 3.3 目录结构
|
||||
|
||||
```
|
||||
confs2/
|
||||
├── src/
|
||||
│ └── main.cpp # 全部源码(ConfigStore + 路由 + main)
|
||||
├── inc/
|
||||
│ └── LogC.hpp # 异步日志客户端(从 captcha2 改编)
|
||||
├── configs/ # → 符号链接到 ../confs/configs/(配置文件零迁移)
|
||||
├── Makefile # 构建系统(g++ -std=c++20)
|
||||
├── _run.sh # 启动脚本
|
||||
├── _set.sh # 编译脚本
|
||||
└── README.md # 模块文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 详细设计
|
||||
|
||||
### 4.1 ConfigStore — 配置文件仓库
|
||||
|
||||
```cpp
|
||||
// 核心数据结构
|
||||
struct CacheEntry {
|
||||
std::string content;
|
||||
std::filesystem::file_time_type mtime;
|
||||
std::size_t size;
|
||||
};
|
||||
|
||||
class ConfigStore {
|
||||
public:
|
||||
explicit ConfigStore(const std::string& root);
|
||||
|
||||
// 取配置全文(带缓存 + mtime 自动重载)
|
||||
// 返回 CacheEntry 的拷贝(防缓存污染,与 Python 版 dict(entry) 语义一致)
|
||||
CacheEntry get(const std::string& name);
|
||||
|
||||
// 列出配置目录全部文件元信息
|
||||
std::vector<ConfigMeta> list();
|
||||
|
||||
// 重载指定配置 / 清空全部缓存
|
||||
std::vector<std::string> reload(const std::string* name);
|
||||
|
||||
std::size_t cached_count() const;
|
||||
|
||||
private:
|
||||
// 扩展名白名单校验(纯字符串操作,不访问文件系统)
|
||||
static void validate_name(const std::string& name);
|
||||
|
||||
// 持锁解析路径(realpath 前缀校验 + 文件存在性)
|
||||
std::string resolve_locked(const std::string& name);
|
||||
|
||||
std::filesystem::path root_;
|
||||
std::unordered_map<std::string, CacheEntry> cache_;
|
||||
mutable std::mutex mtx_;
|
||||
};
|
||||
```
|
||||
|
||||
#### 安全机制(逐项 C++ 实现)
|
||||
|
||||
| 安全机制 | Python 实现 | C++ 实现 |
|
||||
|----------|-------------|----------|
|
||||
| 扩展名白名单 | `ALLOWED_EXT = {".ini", ...}` | `static const std::unordered_set<std::string> kAllowedExt` |
|
||||
| 路径遍历防护 | `os.path.basename` + `os.path.realpath` 前缀校验 | `std::filesystem::path::filename()` + `canonical()` 前缀校验 |
|
||||
| TOCTOU 防护 | 锁内 `realpath→stat→open` | 锁内 `canonical→file_size→ifstream` |
|
||||
| 文件大小限制 | `MAX_CONFIG_SIZE = 1MiB`,stat 后校验 | `file_size()` 后校验 `kMaxConfigSize` |
|
||||
| 缓存防污染 | 返回 `dict(entry)` 浅拷贝 | 返回 `CacheEntry` 值拷贝 |
|
||||
| mtime 自动重载 | `os.stat().st_mtime` 比对 | `last_write_time()` 比对 |
|
||||
| list 符号链接防护 | 遍历时 `realpath` 前缀校验 | 遍历时 `canonical()` 前缀校验,逃逸链接跳过 |
|
||||
| 日志注入防护 | **未实现(P2-3 遗留)** | **新增**:日志消息过滤 `\n`/`\r`/`\x1b` |
|
||||
|
||||
#### TOCTOU 防护流程(锁内原子操作)
|
||||
|
||||
```cpp
|
||||
CacheEntry ConfigStore::get(const std::string& name) {
|
||||
validate_name(name); // 锁外快速拒绝非法名
|
||||
std::lock_guard<std::mutex> lock(mtx_);
|
||||
|
||||
auto path = resolve_locked(name); // 锁内: canonical() 解析符号链接 + 前缀校验
|
||||
|
||||
// 锁内: stat → mtime 比对 → open → read,无 TOCTOU 窗口
|
||||
auto mtime = std::filesystem::last_write_time(path);
|
||||
auto size = std::filesystem::file_size(path);
|
||||
|
||||
auto it = cache_.find(name);
|
||||
if (it != cache_.end() && it->second.mtime == mtime)
|
||||
return it->second; // 缓存命中,返回拷贝
|
||||
|
||||
if (size > kMaxConfigSize)
|
||||
throw ConfigError("配置文件过大");
|
||||
|
||||
std::ifstream f(path);
|
||||
std::string content((std::istreambuf_iterator<char>(f)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
CacheEntry entry{content, mtime, content.size()};
|
||||
cache_[name] = entry;
|
||||
return entry; // 返回拷贝
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 IniParser — INI 按键取值
|
||||
|
||||
与 Python `ConfigParser(interpolation=None)` 行为对等的 C++ 手写解析器:
|
||||
|
||||
| Python ConfigParser 行为 | C++ 实现策略 |
|
||||
|--------------------------|-------------|
|
||||
| Section 大小写敏感 | `std::string` 原样比较 |
|
||||
| Option 大小写不敏感 | 比较前 `std::tolower` 归一化 |
|
||||
| `;` 和 `#` 注释 | 行首匹配跳过 |
|
||||
| `=` 和 `:` 分隔符 | 仅支持 `=`(与现有 configs/ 一致) |
|
||||
| 禁用 `%(key)s` 插值 | 不实现插值,原样返回 |
|
||||
| 多行续行(前导空格) | 支持:下一行以空格/Tab 开头则追加 |
|
||||
|
||||
```cpp
|
||||
// 从 INI 文本中提取 Section.option 的值
|
||||
// 键格式: "Section.option"
|
||||
// 大小写: section 敏感, option 不敏感(与 Python 一致)
|
||||
// 插值: 禁用,原样返回(与 Python interpolation=None 一致)
|
||||
static std::optional<std::string> extract_ini_key(
|
||||
const std::string& content, const std::string& key);
|
||||
```
|
||||
|
||||
### 4.3 JsonKeyExtractor — JSON 点路径取值
|
||||
|
||||
```cpp
|
||||
// 从 JSON 文本中按点路径提取值
|
||||
// 键格式: "server.port" 或 "servers.0.host"(数字下标)
|
||||
// 返回 boost::json::value(可为 string/int/double/bool/null/array/object)
|
||||
static json::value extract_json_key(
|
||||
const std::string& content, const std::string& key);
|
||||
```
|
||||
|
||||
### 4.4 路由实现
|
||||
|
||||
#### `get` — 取配置
|
||||
|
||||
```cpp
|
||||
rpc.add_route("get", [](const json::object& req) -> json::value {
|
||||
// 参数校验
|
||||
auto name_it = req.find("name");
|
||||
if (name_it == req.end() || !name_it->value().is_string())
|
||||
throw ConfigError("缺少 name 字段");
|
||||
|
||||
std::string name = json::value_to<std::string>(name_it->value());
|
||||
auto entry = g_store.get(name);
|
||||
|
||||
// 无 key:返回全文
|
||||
auto key_it = req.find("key");
|
||||
if (key_it == req.end())
|
||||
return json::object{
|
||||
{"name", name},
|
||||
{"content", entry.content},
|
||||
{"size", (std::int64_t)entry.size},
|
||||
{"mtime", iso_timestamp(entry.mtime)}
|
||||
};
|
||||
|
||||
// 有 key:按键取值
|
||||
std::string key = json::value_to<std::string>(key_it->value());
|
||||
auto ext = get_extension(name);
|
||||
|
||||
if (ext == ".ini" || ext == ".conf" || ext == ".cfg") {
|
||||
auto val = extract_ini_key(entry.content, key);
|
||||
if (!val) throw KeyNotFound("键不存在: " + name + " -> " + key);
|
||||
return json::object{{"name", name}, {"key", key}, {"value", *val}};
|
||||
}
|
||||
if (ext == ".json") {
|
||||
auto val = extract_json_key(entry.content, key);
|
||||
return json::object{{"name", name}, {"key", key}, {"value", val}};
|
||||
}
|
||||
throw ConfigError(get_extension(name) + " 类型不支持按键查询");
|
||||
});
|
||||
```
|
||||
|
||||
#### `list` — 列出配置
|
||||
|
||||
```cpp
|
||||
rpc.add_route("list", [](const json::object&) -> json::value {
|
||||
auto configs = g_store.list();
|
||||
json::array arr;
|
||||
for (const auto& c : configs)
|
||||
arr.push_back(json::object{
|
||||
{"name", c.name}, {"size", (std::int64_t)c.size},
|
||||
{"mtime", c.mtime_iso}, {"cached", c.cached}
|
||||
});
|
||||
return json::object{
|
||||
{"config_dir", g_store.root_string()},
|
||||
{"configs", arr}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
#### `reload` — 重载配置
|
||||
|
||||
```cpp
|
||||
rpc.add_route("reload", [](const json::object& req) -> json::value {
|
||||
auto it = req.find("name");
|
||||
|
||||
// 省略 name 或 null → 清空全部缓存
|
||||
if (it == req.end() || it->value().is_null()) {
|
||||
auto names = g_store.reload(nullptr);
|
||||
return json::object{{"reloaded", make_json_array(names)}};
|
||||
}
|
||||
|
||||
// 非字符串 → 报错
|
||||
if (!it->value().is_string())
|
||||
throw ConfigError("name 字段必须是字符串");
|
||||
|
||||
std::string name = json::value_to<std::string>(it->value());
|
||||
// 空字符串 → 报错(不误清全部缓存,与 Python 版一致)
|
||||
if (name.empty())
|
||||
throw ConfigError("name 字段不能为空(要清空全部缓存请省略 name 字段)");
|
||||
|
||||
auto names = g_store.reload(&name);
|
||||
return json::object{{"reloaded", make_json_array(names)}};
|
||||
});
|
||||
```
|
||||
|
||||
#### `health` — 健康检查
|
||||
|
||||
```cpp
|
||||
rpc.add_route("health", [](const json::object&) -> json::value {
|
||||
return json::object{
|
||||
{"status", "ok"},
|
||||
{"service", "confs"},
|
||||
{"config_dir", g_store.root_string()},
|
||||
{"cached", (std::int64_t)g_store.cached_count()},
|
||||
{"uptime", uptime_seconds()}
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 4.5 配置加载(confs 自身的引导配置)
|
||||
|
||||
confs 自身不需要从 confs 获取配置(避免循环依赖),使用硬编码默认值 + 环境变量覆盖 + 命令行参数覆盖:
|
||||
|
||||
```cpp
|
||||
struct ConfsConfig {
|
||||
std::string host = "127.0.0.1";
|
||||
int port = 8010;
|
||||
int workers = 4;
|
||||
std::string config_dir; // 默认: 同级 configs/ 目录
|
||||
};
|
||||
|
||||
// 加载顺序: 硬编码默认 → 环境变量覆盖 → 命令行覆盖
|
||||
// 环境变量: LISTEN_ADDR, LISTEN_PORT, WORKERS, CONFS_DIR
|
||||
```
|
||||
|
||||
### 4.6 日志客户端(LogC.hpp)
|
||||
|
||||
从 [liblogc/inc/us/LogC.hpp](../../liblogc/inc/us/LogC.hpp)(原 captcha2/inc/LogC.hpp,后独立为 liblogc 模块)改编,仅改 namespace 和服务名:
|
||||
|
||||
```cpp
|
||||
namespace confs2 {
|
||||
// ... 与 captcha2::LogC 结构完全一致
|
||||
// 仅改动:
|
||||
// - namespace: captcha2 → confs2
|
||||
// - 日志中服务名: "captcha2" → "confs"
|
||||
// - 客户端名: "captcha2-log" → "confs-log"
|
||||
}
|
||||
```
|
||||
|
||||
#### 日志注入防护(修复 P2-3)
|
||||
|
||||
```cpp
|
||||
// 过滤日志消息中的控制字符(\n, \r, \x1b ANSI 转义等)
|
||||
static std::string sanitize_for_log(std::string s) {
|
||||
s.erase(std::remove_if(s.begin(), s.end(), [](unsigned char c) {
|
||||
return c < 0x20 && c != '\t'; // 保留 Tab,过滤其他控制字符
|
||||
}), s.end());
|
||||
return s;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.7 异常消息脱敏(修复 P3-6)
|
||||
|
||||
```cpp
|
||||
// 异常消息脱敏:不返回系统路径、权限错误等底层信息
|
||||
static std::string sanitize_error_msg(const std::string& msg) {
|
||||
// 替换绝对路径为 <redacted>
|
||||
static const std::regex path_re(R"(/\S+(?:/\S*)*)");
|
||||
std::string result = std::regex_replace(msg, path_re, "<redacted>");
|
||||
// 过滤 Permission denied 等系统错误
|
||||
static const std::regex perm_re(R"(Permission denied|No such file)");
|
||||
result = std::regex_replace(result, perm_re, "access error");
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
> 注意:RpcServer 已内置 `_sanitize_error()` 方法(见 [RpcServer.hpp](../../libmsg/inc/us/RpcServer.hpp#L188-L197)),会自动替换绝对路径。confs2 的 handler 抛出的异常会先经 RpcServer 脱敏。但 confs2 仍应在业务层对底层 IO 异常做一层包装,将 `Permission denied` 等系统错误转为业务语义错误。
|
||||
|
||||
---
|
||||
|
||||
## 5. 实现计划
|
||||
|
||||
### 5.1 分阶段交付
|
||||
|
||||
| 阶段 | 里程碑 | 涉及文件 | 预估工时 |
|
||||
|------|--------|----------|----------|
|
||||
| **P0 — 骨架搭建** | Makefile + _run.sh + _set.sh + 空 main.cpp(RpcServer 启动 + health 路由) | 新增 `confs2/Makefile`、`_run.sh`、`_set.sh`、`src/main.cpp`、`inc/LogC.hpp` | 1h |
|
||||
| **P1 — ConfigStore 核心** | ConfigStore 类:磁盘读取 + 内存缓存 + mtime 重载 + 安全机制 | `src/main.cpp` | 3h |
|
||||
| **P2 — 路由实现** | get / list / reload / health 四路由完整实现 | `src/main.cpp` | 2h |
|
||||
| **P3 — INI 按键取值** | IniParser 手写 + JSON 点路径取值 | `src/main.cpp` | 2h |
|
||||
| **P4 — 日志客户端** | LogC.hpp 改编 + 日志注入防护 | `inc/LogC.hpp`、`src/main.cpp` | 1h |
|
||||
| **P5 — 配置加载** | 环境变量 + 命令行参数解析 | `src/main.cpp` | 1h |
|
||||
| **P6 — 编译验证** | 零警告编译 + ldd 依赖检查 + --help 验证 | `Makefile` | 0.5h |
|
||||
| **P7 — 功能测试** | 逐路由对比 Python 版响应一致性 | `tests/` 新增 | 2h |
|
||||
| **P8 — 切换 init.txt** | init.txt 改为 confs2/_run.sh + 生产验证 | `core/init.txt` | 0.5h |
|
||||
|
||||
**总计预估:~13 小时**
|
||||
|
||||
### 5.2 Makefile
|
||||
|
||||
```makefile
|
||||
CXX := g++
|
||||
CXXFLAGS := -std=c++20 -Wall -Wextra -O2
|
||||
INCS := -I./inc
|
||||
LIBS := -lus_msg -lboost_json -lboost_program_options -lpthread
|
||||
|
||||
TARGET := confs2.bin
|
||||
SRC := src/main.cpp
|
||||
|
||||
.PHONY: all clean install
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(SRC)
|
||||
$(CXX) $(CXXFLAGS) $(INCS) $^ $(LIBS) -o $@
|
||||
|
||||
install: $(TARGET)
|
||||
cp $(TARGET) /usr/local/bin/
|
||||
|
||||
clean:
|
||||
rm -f $(TARGET)
|
||||
```
|
||||
|
||||
### 5.3 _run.sh
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")" || exit
|
||||
sleep 1
|
||||
exec ./confs2.bin
|
||||
```
|
||||
|
||||
### 5.4 _set.sh
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")" || exit
|
||||
|
||||
make clean 2>/dev/null || true
|
||||
make
|
||||
echo "[confs2] 编译完成 → ./confs2.bin"
|
||||
|
||||
# 创建 configs 符号链接(指向 ../confs/configs/,配置文件零迁移)
|
||||
if [ ! -e configs ]; then
|
||||
ln -s ../confs/configs configs
|
||||
echo "[confs2] 已创建 configs → ../confs/configs 符号链接"
|
||||
fi
|
||||
```
|
||||
|
||||
### 5.5 新增文件清单
|
||||
|
||||
```
|
||||
confs2/
|
||||
├── src/
|
||||
│ └── main.cpp # 全部业务源码(预估 ~500 行)
|
||||
├── inc/
|
||||
│ └── LogC.hpp # 异步日志客户端(从 captcha2 改编,~110 行)
|
||||
├── configs # → 符号链接到 ../confs/configs/
|
||||
├── Makefile # 构建系统
|
||||
├── _run.sh # 启动脚本
|
||||
├── _set.sh # 编译 + 链接 configs/
|
||||
└── README.md # 模块文档
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键依赖
|
||||
|
||||
| 依赖 | 用途 | 已安装 |
|
||||
|------|------|--------|
|
||||
| `libmsg` (`-lus_msg`) | RpcServer / RpcClient / MsgServer / AsyncQueue | ✅ |
|
||||
| `boost::json` (`-lboost_json`) | JSON 序列化/反序列化 | ✅ |
|
||||
| `boost::program_options` (`-lboost_program_options`) | 命令行参数解析 | ✅ |
|
||||
| `pthread` | 多线程(mutex) | ✅ |
|
||||
| `std::filesystem` | 文件系统操作(C++20 标准库) | ✅ |
|
||||
|
||||
**零新增系统依赖**——所有库均已被 captcha2 / smtp3 等模块使用。
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试计划
|
||||
|
||||
### 7.1 功能测试(对比 Python 版响应一致性)
|
||||
|
||||
| 测试项 | 方法 | 通过标准 |
|
||||
|--------|------|----------|
|
||||
| `get` 取全文 | `RpcClient.call("get", {"name":"db.ini"})` | content/size/mtime 与 Python 版一致 |
|
||||
| `get` INI 按键 | `RpcClient.call("get", {"name":"db.ini","key":"server.port"})` | value="8005" |
|
||||
| `get` JSON 按键 | 构造 .json 配置,取 `server.port` | 值一致 |
|
||||
| `get` 不存在 | `RpcClient.call("get", {"name":"nonexist.ini"})` | `success=false`,error 含"不存在" |
|
||||
| `get` 路径遍历 | `{"name":"../../etc/passwd"}` | `success=false`,拒绝 |
|
||||
| `get` 非法扩展名 | `{"name":"test.exe"}` | `success=false`,拒绝 |
|
||||
| `list` | `RpcClient.call("list", {})` | 配置列表与 Python 版一致 |
|
||||
| `reload` 指定 | `RpcClient.call("reload", {"name":"db.ini"})` | `success=true`,reloaded=["db.ini"] |
|
||||
| `reload` 全部 | `RpcClient.call("reload", {})` | `success=true`,reloaded 含全部缓存名 |
|
||||
| `reload` 空字符串 | `RpcClient.call("reload", {"name":""})` | `success=false`,error 含"不能为空" |
|
||||
| `reload` 非字符串 | `RpcClient.call("reload", {"name":123})` | `success=false`,error 含"必须是字符串" |
|
||||
| `health` | `RpcClient.call("health", {})` | status="ok",service="confs" |
|
||||
| mtime 重载 | 修改配置文件内容,再 `get` | 返回新内容 |
|
||||
| 大文件拒绝 | 创建 > 1MiB 文件,`get` | `success=false`,error 含"过大" |
|
||||
| 符号链接逃逸 | configs/ 内放逃逸符号链接 | `list` 中不出现 |
|
||||
|
||||
### 7.2 消费方回归测试
|
||||
|
||||
| 服务 | 测试方法 |
|
||||
|------|----------|
|
||||
| static | 启动 confs2 + static,验证静态文件服务正常(从 confs2 获取 static.ini) |
|
||||
| dnfs | 启动 confs2 + dnfs,验证树状菜单 API 正常 |
|
||||
| db | 启动 confs2 + db,验证数据库服务正常(从 confs2 获取 db.ini) |
|
||||
| captcha | 启动 confs2 + captcha2,验证验证码生成正常(从 confs2 获取 captcha.ini) |
|
||||
| redirect80 | 启动 confs2 + redirect80,验证重定向服务正常 |
|
||||
|
||||
### 7.3 安全测试
|
||||
|
||||
| 测试项 | 方法 |
|
||||
|--------|------|
|
||||
| 路径遍历 | `{"name":"../../../etc/passwd"}` → 拒绝 |
|
||||
| 绝对路径 | `{"name":"/etc/passwd"}` → 拒绝 |
|
||||
| 非法扩展名 | `{"name":"malicious.sh"}` → 拒绝 |
|
||||
| 符号链接逃逸 | configs/ 内符号链接指向 /etc/ → list 跳过,get 拒绝 |
|
||||
| 大文件 OOM | 创建 2MiB 配置 → 拒绝 |
|
||||
| 日志注入 | 配置名含 `\n[FAKE]` → 日志中无伪造条目 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 安全考量
|
||||
|
||||
### 8.1 安全机制对照表
|
||||
|
||||
| 机制 | Python 版状态 | C++ 版方案 |
|
||||
|------|---------------|-------------|
|
||||
| 扩展名白名单 | ✅ | ✅ `kAllowedExt` |
|
||||
| 路径遍历防护 | ✅ basename + realpath 前缀 | ✅ `filename()` + `canonical()` 前缀 |
|
||||
| TOCTOU 防护 | ✅ 锁内 realpath→stat→open | ✅ 锁内 canonical→file_size→ifstream |
|
||||
| 文件大小限制 | ✅ 1MiB | ✅ `kMaxConfigSize = 1MiB` |
|
||||
| Slowloris 防护 | ✅ pymsg 10s recv 超时 | ✅ libmsg MsgServer 同策略 |
|
||||
| 缓存防污染 | ✅ 返回浅拷贝 | ✅ 返回值拷贝 |
|
||||
| list 符号链接防护 | ✅ realpath 前缀校验 | ✅ canonical 前缀校验 |
|
||||
| 参数类型校验 | ✅ 非字符串/空值报错 | ✅ 逐字段 is_string 校验 |
|
||||
| INI 安全 | ✅ 禁用插值 | ✅ 不实现插值 |
|
||||
| 仅监听本机 | ✅ 127.0.0.1 | ✅ 默认 127.0.0.1 |
|
||||
| 异常隔离 | ✅ RpcServer 包装 | ✅ libmsg RpcServer 同机制 |
|
||||
| 日志注入防护 | ❌ P2-3 遗留 | ✅ **新增** sanitize_for_log |
|
||||
| 异常消息脱敏 | ❌ P3-6 遗留 | ✅ **新增** RpcServer 内置脱敏 + 业务层包装 |
|
||||
| 鉴权 | ❌ 无 | ❌ 无(依赖仅本机信任边界,与 Python 版一致) |
|
||||
|
||||
### 8.2 INI 大小写策略文档化(修复 P3-5)
|
||||
|
||||
在代码注释和 README 中明确文档化:
|
||||
|
||||
```
|
||||
INI 解析大小写策略(与 Python configparser(interpolation=None) 一致):
|
||||
- Section 名:大小写敏感([Server] ≠ [server])
|
||||
- Option 名:大小写不敏感(port = Port = PORT)
|
||||
- 值:原样返回,不做任何转换
|
||||
- 插值:禁用,%(key)s 原样返回
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 兼容性与迁移
|
||||
|
||||
### 9.1 零改动兼容
|
||||
|
||||
- **消费方零改动**:5 个 Python 服务继续用 `RpcClient("127.0.0.1", 8010, ...)` 调用,协议/端口/路由/响应格式全部不变。
|
||||
- **配置文件零迁移**:`confs2/configs/` 符号链接到 `../confs/configs/`,现有 9 个 .ini 文件不动。
|
||||
- **启动方式一致**:`init.txt` 中 `bash .../confs/_run.sh` 改为 `bash .../confs2/_run.sh`,一行改动。
|
||||
|
||||
### 9.2 迁移路径
|
||||
|
||||
```
|
||||
阶段 1: 编译 confs2 + 功能测试(confs2 与 confs 并存)
|
||||
↓
|
||||
阶段 2: 消费方回归测试(逐个服务切到 confs2 验证)
|
||||
↓
|
||||
阶段 3: init.txt 切换 confs → confs2(一行改动)
|
||||
↓
|
||||
阶段 4: 生产运行观察 72h,确认无异常
|
||||
↓
|
||||
阶段 5: Python confs 目录归档(不立即删除,保留回退能力)
|
||||
```
|
||||
|
||||
### 9.3 init.txt 切换
|
||||
|
||||
```
|
||||
# 切换前
|
||||
bash /opt/micro_servers/confs/_run.sh
|
||||
|
||||
# 切换后
|
||||
bash /opt/micro_servers/confs2/_run.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 未来扩展:core 注册中心联动
|
||||
|
||||
当 core 注册中心([core_service_registry_dev_doc](core_service_registry_dev_doc.md))落地后,confs2 可作为注册中心的第一个自注册服务:
|
||||
|
||||
```cpp
|
||||
// main() 启动后,RpcServer.start() 之前
|
||||
register_to_core("confs", g_config.port, "pymsg", "health",
|
||||
"bash /opt/micro_servers/confs2/_run.sh");
|
||||
```
|
||||
|
||||
进一步地,当未来评估 confs2 合入 core 时(方案 A: HTTP 合入),因 confs2 已是 C++ 同构,迁移成本仅为:
|
||||
1. 将 ConfigStore 代码移入 core 进程
|
||||
2. HTTP :5200 增加 `/confs/*` 路由
|
||||
3. 5 个消费方从 pymsg 改 HTTP(~50 行改动)
|
||||
|
||||
详见前序分析中的"confs2 合入 core 注册中心"可行性评估。
|
||||
|
||||
---
|
||||
|
||||
## 11. 附录
|
||||
|
||||
### A. 与已有 C++ 重写的模式对照
|
||||
|
||||
| 维度 | captcha2 | smtp3 | **confs2** |
|
||||
|------|----------|-------|-----------|
|
||||
| 原版语言 | Python | Python | Python |
|
||||
| 重写语言 | C++20 | C++20 | C++20 |
|
||||
| 协议 | pymsg(libmsg) | pymsg(libmsg) | pymsg(libmsg) |
|
||||
| 端口 | 8009 | 8008 | 8010 |
|
||||
| libmsg | ✅ RpcServer | ✅ RpcServer | ✅ RpcServer |
|
||||
| boost_json | ✅ | ✅ | ✅ |
|
||||
| LogC.hpp | ✅ 原创 | ✅ 改编 | ✅ 改编 |
|
||||
| Makefile 模式 | g++ -std=c++20 | g++ -std=c++20 | g++ -std=c++20 |
|
||||
| _run.sh 模式 | sleep 1 + exec .bin | sleep 1 + exec .bin | sleep 1 + exec .bin |
|
||||
| _set.sh 模式 | set -euo + make | set -euo + make | set -euo + make + ln -s |
|
||||
| 特殊依赖 | freetype, stb | openssl | 无(仅 libmsg + boost) |
|
||||
|
||||
### B. 配置文件目录现状
|
||||
|
||||
| 文件名 | 消费方 | 大小 |
|
||||
|--------|--------|------|
|
||||
| ca.ini | ca 模块 | 51 行 |
|
||||
| captcha.ini | captcha2 | 11 行 |
|
||||
| db.ini | db | 16 行 |
|
||||
| dnfs.ini | dnfs | 16 行 |
|
||||
| example.ini | 示例 | 10 行 |
|
||||
| gateway.ini | gateway | 20 行 |
|
||||
| redirect80.ini | redirect80 | 5 行 |
|
||||
| smtp.ini | smtp3 | 19 行 |
|
||||
| static.ini | static | 17 行 |
|
||||
|
||||
### C. libmsg RpcServer API 速查
|
||||
|
||||
```cpp
|
||||
// 构造
|
||||
pmc::net::RpcServer rpc("127.0.0.1", 8010,
|
||||
pmc::net::MsgServerConfig{.workers = 4},
|
||||
[](const std::string& route, const std::exception& e) {
|
||||
// on_error 回调
|
||||
});
|
||||
|
||||
// 注册路由
|
||||
rpc.add_route("get", [](const json::object& req) -> json::value {
|
||||
// handler 签名: json::value(const json::object& req)
|
||||
// 返回值自动包装为 {"success": true, "data": ...}
|
||||
// 抛异常自动包装为 {"success": false, "error": "<脱敏>"}
|
||||
return json::object{{"content", "..."}};
|
||||
});
|
||||
|
||||
// 启动(阻塞)
|
||||
rpc.start();
|
||||
|
||||
// 停止(另一线程)
|
||||
rpc.stop();
|
||||
```
|
||||
@@ -1,806 +0,0 @@
|
||||
# Core 启动配置 YAML 化与 CLI 参数标准化 · 开发文档
|
||||
|
||||
> 文档类型:功能开发计划
|
||||
> 模块:core(PMC / Nidus)+ 全部子服务
|
||||
> 作者:自动生成
|
||||
> 日期:2026-08-10
|
||||
> 前置关系:本文档是 `core_service_registry_dev_doc.md` P7 阶段(services.yaml)的前置拆分,聚焦"配置格式迁移 + 参数透传 + CLI 标准化"三步基础设施改造
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 背景与动机
|
||||
|
||||
当前 core 通过 `init.txt`(纯命令列表)拉起 11 个子服务,存在以下问题:
|
||||
|
||||
| 现状 | 问题 |
|
||||
|------|------|
|
||||
| `init.txt` 每行一条 bash 命令,无结构化字段 | 无法为每个服务指定命令行参数、环境变量、依赖关系 |
|
||||
| 5/11 个 `_run.sh` 不转发 `"$@"` | core 即便想传参也传不进去 |
|
||||
| `sleep 2` 等依赖等待逻辑散落在各 `_run.sh` 中 | 无统一管理,调整需逐个脚本修改 |
|
||||
| waf 的 `-c config.yaml` 硬编码在 `_run.sh` | 参数不可外部覆盖 |
|
||||
| 各服务 CLI 接口不统一 | ca/gateway 仅有 `--help/--version`,waf 有 `-c/-list/-test/-check`,其余 8 个服务零 CLI 支持 |
|
||||
|
||||
**本方案目标**:通过三步改造,让 core 能够以结构化 YAML 声明每个服务的启动命令和参数,参数能透传到各服务二进制/脚本,并建立统一的 CLI 参数规范——为注册中心 `services.yaml` + `depends_on` DAG 拓扑排序打下基础。
|
||||
|
||||
### 1.2 设计目标
|
||||
|
||||
1. **结构化启动声明**:`init.txt` → `services.yaml`,每个服务可声明 `cmd`、`args`、`env`、`depends_on`、`sleep_before`
|
||||
2. **参数透传**:全部 11 个 `_run.sh` 统一转发 `"$@"`,消除硬编码参数
|
||||
3. **CLI 标准化**:定义通用参数约定(`--host`/`--port`/`--config`/`--log-level`/`--instance`),各服务逐步适配
|
||||
4. **向后兼容**:`services.yaml` 不存在时回退 `init.txt`,现有部署零中断
|
||||
5. **yaml-cpp 引入**:vendored 到 `core/third_party/yaml-cpp/`,不依赖系统包管理器
|
||||
|
||||
### 1.3 与注册中心计划的关系
|
||||
|
||||
```
|
||||
本文档(三步改造) 注册中心计划(core_service_registry_dev_doc.md)
|
||||
┌─────────────────────┐ ┌─────────────────────────────┐
|
||||
│ Step 1: services.yaml│ ──────▶ │ P7: services.yaml DAG 排序 │
|
||||
│ Step 2: _run.sh 透传 │ ──────▶ │ P2: init 解析后自动填充注册表 │
|
||||
│ Step 3: CLI 标准化 │ ──────▶ │ P5: RegistrationClient SDK │
|
||||
└─────────────────────┘ └─────────────────────────────┘
|
||||
```
|
||||
|
||||
本方案是注册中心的**基础设施前置**——做完这三步,P2/P5/P7 的工作量直接减半。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状分析
|
||||
|
||||
### 2.1 init.txt 当前内容
|
||||
|
||||
```
|
||||
bash /opt/micro_servers/db2/_run.sh
|
||||
bash /opt/micro_servers/ai_review/_run.sh
|
||||
bash /opt/micro_servers/dnfs2/_run.sh
|
||||
bash /opt/micro_servers/smtp/_run.sh
|
||||
bash /opt/micro_servers/fsys/_run.sh
|
||||
bash /opt/micro_servers/waf/_run.sh
|
||||
bash /opt/micro_servers/ca/_run.sh
|
||||
bash /opt/micro_servers/gateway/_run.sh
|
||||
bash /opt/micro_servers/captcha2/_run.sh
|
||||
bash /opt/micro_servers/aibox/_run.sh
|
||||
bash /opt/micro_servers/redirect80/_run.sh
|
||||
```
|
||||
|
||||
纯命令列表,无参数、无元数据、无依赖声明。
|
||||
|
||||
### 2.2 各服务 _run.sh 现状
|
||||
|
||||
| 服务 | 语言 | `"$@"` 转发 | 硬编码参数 | 特殊处理 | sleep 等待 |
|
||||
|------|------|-------------|-----------|----------|------------|
|
||||
| db2 | C++ | ✅ `"$@"` | — | `set -e` | — |
|
||||
| dnfs2 | C++ | ✅ `"$@"` | — | `set -e` | — |
|
||||
| fsys | C++ | ✅ `"$@"` | — | `set -e` | — |
|
||||
| ca | C++ | ✅ `"$@"` | — | — | `sleep 2` |
|
||||
| gateway | C++ | ✅ `"$@"` | — | — | `sleep 2` |
|
||||
| waf | Go | ❌ | `-c config.yaml` | — | — |
|
||||
| captcha2 | C++ | ❌ | — | — | `sleep 1` |
|
||||
| aibox | Python | ⚠️ `$@`(未加引号) | — | `_setenv.sh` + venv | — |
|
||||
| ai_review | Python | ❌ | — | venv | — |
|
||||
| smtp | Python | ❌ | — | venv | `sleep 2` |
|
||||
| redirect80 | Python | ❌ | — | setcap + venv | — |
|
||||
|
||||
**统计**:5 个已转发、1 个半转发(未加引号)、5 个未转发。3 个脚本含 `sleep` 硬编码。
|
||||
|
||||
### 2.3 各服务 CLI 参数支持现状
|
||||
|
||||
| 服务 | 语言 | CLI 框架 | 实际参数 | 配置来源 |
|
||||
|------|------|----------|----------|----------|
|
||||
| db2 | C++ | 无 | 无 | 硬编码常量 |
|
||||
| dnfs2 | C++ | 无 | 无 | 硬编码 + confs |
|
||||
| fsys | C++ | 无 | 无 | 硬编码 |
|
||||
| captcha2 | C++ | 无 | 无 | 硬编码 |
|
||||
| ca | C++ | `boost::program_options` | `--help` `--version`(仅此) | confs (ca.ini) + 环境变量 |
|
||||
| gateway | C++ | `boost::program_options` | `--help` `--version`(仅此) | confs (gateway.ini) + 环境变量 |
|
||||
| waf | Go | `flag` | `-c` `-list` `-test` `-check` | YAML 配置文件 |
|
||||
| aibox | Python | `argparse`(已 import 未使用) | 无 | `_setenv.sh` + 硬编码 |
|
||||
| ai_review | Python | 无 | 无 | 硬编码 |
|
||||
| smtp | Python | 无 | 无 | 硬编码 |
|
||||
| redirect80 | Python | 无 | 无 | 硬编码 |
|
||||
|
||||
**关键发现**:
|
||||
- ca 和 gateway 已引入 `boost::program_options` 但只实现了 `--help`/`--version`,实际配置全走 confs + 环境变量
|
||||
- aibox 已 `import argparse` 但从未调用,是死代码
|
||||
- waf 是唯一有完整 CLI 接口的服务(Go `flag` 包)
|
||||
- 8/11 个服务零 CLI 参数支持
|
||||
|
||||
### 2.4 配置加载模式分类
|
||||
|
||||
当前各服务的配置获取有三种模式:
|
||||
|
||||
| 模式 | 服务 | 说明 |
|
||||
|------|------|------|
|
||||
| **confs + 环境变量** | ca, gateway, dnfs2 | 从 confs 服务拉取 ini 配置,环境变量可覆盖;`CONFS_HOST`/`CONFS_PORT` 引导 |
|
||||
| **本地配置文件** | waf | `-c config.yaml` 指定 YAML 配置 |
|
||||
| **硬编码** | db2, fsys, captcha2, aibox, ai_review, smtp, redirect80 | 常量直接写在源码中 |
|
||||
|
||||
CLI 标准化后,三种模式可统一为:**CLI 参数(最高优先级)> 环境变量 > confs/配置文件 > 硬编码默认值**。
|
||||
|
||||
---
|
||||
|
||||
## 3. 三步改造方案
|
||||
|
||||
### 3.1 总览
|
||||
|
||||
```
|
||||
Step 1: init.txt → services.yaml Step 2: _run.sh 参数透传 Step 3: CLI 参数标准化
|
||||
┌──────────────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ • 引入 yaml-cpp (vendored) │ │ • 11 个 _run.sh 统一 │ │ • 定义通用参数约定 │
|
||||
│ • 设计 services.yaml 格式 │ ──▶ │ 末尾 exec 加 "$@" │ ──▶ │ • _run.sh 注入默认值 │
|
||||
│ • core 解析 YAML 并启动 │ │ • 移除硬编码参数 │ │ • 各服务逐步适配 │
|
||||
│ • 回退兼容 init.txt │ │ • sleep 移至 YAML │ │ • 环境变量桥接 │
|
||||
└──────────────────────────────┘ └──────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Step 1:init.txt → services.yaml
|
||||
|
||||
### 4.1 yaml-cpp 引入
|
||||
|
||||
采用系统包安装方式(已写入 `_setup.deb.sh` 和 `_setup.rhel.sh`):
|
||||
|
||||
| 发行版 | 包名 | 安装命令 |
|
||||
|--------|------|----------|
|
||||
| Debian/Ubuntu | `libyaml-cpp-dev` | `apt install libyaml-cpp-dev` |
|
||||
| RHEL/CentOS/Rocky | `yaml-cpp-devel` | `dnf install yaml-cpp-devel` |
|
||||
|
||||
**Makefile 集成**(已完成):
|
||||
|
||||
```makefile
|
||||
# 链接 yaml-cpp 系统库
|
||||
LIBS := ... -lyaml-cpp
|
||||
|
||||
# InitParser 编译目标
|
||||
$(BUILD_DIR)/InitParser.o: src/registry/InitParser.cpp
|
||||
$(CPP) $(CPPFLAGS) -c $^ $(INCS) -o $@
|
||||
|
||||
# 链接时追加
|
||||
$(APP_NAME).out: ... $(BUILD_DIR)/InitParser.o
|
||||
```
|
||||
|
||||
### 4.2 services.yaml 格式设计
|
||||
|
||||
```yaml
|
||||
# core/services.yaml
|
||||
# 声明式服务启动配置(替代 init.txt)
|
||||
# 如果本文件存在则优先使用,否则回退 init.txt
|
||||
|
||||
services:
|
||||
# --- 数据层 ---
|
||||
- name: db2
|
||||
cmd: bash /opt/micro_servers/db2/_run.sh
|
||||
args: []
|
||||
env: {}
|
||||
depends_on: []
|
||||
sleep_before: 0
|
||||
|
||||
# --- AI 服务 ---
|
||||
- name: ai_review
|
||||
cmd: bash /opt/micro_servers/ai_review/_run.sh
|
||||
args: ["--port", "8007", "--log-level", "info"]
|
||||
env:
|
||||
PYTHONUNBUFFERED: "1"
|
||||
depends_on: [db2]
|
||||
sleep_before: 2
|
||||
|
||||
# --- 文件服务 ---
|
||||
- name: dnfs2
|
||||
cmd: bash /opt/micro_servers/dnfs2/_run.sh
|
||||
args: []
|
||||
env: {}
|
||||
depends_on: [db2, ai_review]
|
||||
sleep_before: 0
|
||||
|
||||
# --- 邮件服务 ---
|
||||
- name: smtp
|
||||
cmd: bash /opt/micro_servers/smtp/_run.sh
|
||||
args: []
|
||||
env: {}
|
||||
depends_on: []
|
||||
sleep_before: 2
|
||||
|
||||
# --- 前端静态资源 ---
|
||||
- name: fsys
|
||||
cmd: bash /opt/micro_servers/fsys/_run.sh
|
||||
args: []
|
||||
env: {}
|
||||
depends_on: []
|
||||
sleep_before: 0
|
||||
|
||||
# --- 安全链路 ---
|
||||
- name: waf
|
||||
cmd: bash /opt/micro_servers/waf/_run.sh
|
||||
args: ["-c", "config.yaml"]
|
||||
env: {}
|
||||
depends_on: [ca, gateway]
|
||||
sleep_before: 0
|
||||
|
||||
- name: ca
|
||||
cmd: bash /opt/micro_servers/ca/_run.sh
|
||||
args: []
|
||||
env: {}
|
||||
depends_on: [db2]
|
||||
sleep_before: 2
|
||||
|
||||
- name: gateway
|
||||
cmd: bash /opt/micro_servers/gateway/_run.sh
|
||||
args: []
|
||||
env: {}
|
||||
depends_on: [db2]
|
||||
sleep_before: 2
|
||||
|
||||
# --- 验证码 ---
|
||||
- name: captcha2
|
||||
cmd: bash /opt/micro_servers/captcha2/_run.sh
|
||||
args: []
|
||||
env: {}
|
||||
depends_on: []
|
||||
sleep_before: 1
|
||||
|
||||
# --- AI 工具箱 ---
|
||||
- name: aibox
|
||||
cmd: bash /opt/micro_servers/aibox/_run.sh
|
||||
args: []
|
||||
env:
|
||||
PATH: "${PATH}:~/.aibox/skills"
|
||||
depends_on: []
|
||||
sleep_before: 0
|
||||
|
||||
# --- 端口重定向 ---
|
||||
- name: redirect80
|
||||
cmd: bash /opt/micro_servers/redirect80/_run.sh
|
||||
args: []
|
||||
env: {}
|
||||
depends_on: [waf]
|
||||
sleep_before: 0
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `name` | string | 是 | 服务唯一标识,用于日志和未来注册表 |
|
||||
| `cmd` | string | 是 | 启动命令(与 init.txt 每行格式一致) |
|
||||
| `args` | string[] | 否 | 传递给 `_run.sh` 的命令行参数列表 |
|
||||
| `env` | map<string,string> | 否 | 启动前设置的环境变量 |
|
||||
| `depends_on` | string[] | 否 | 依赖的服务名列表(未来 DAG 排序用,当前仅记录) |
|
||||
| `sleep_before` | int | 否 | 启动前等待秒数(替代散落在 _run.sh 中的 sleep) |
|
||||
|
||||
### 4.3 core 解析逻辑
|
||||
|
||||
新增 `include/registry/InitParser.hh` + `src/registry/InitParser.cpp`:
|
||||
|
||||
```cpp
|
||||
// include/registry/InitParser.hh
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
namespace qing::registry {
|
||||
|
||||
struct ServiceConfig {
|
||||
std::string name;
|
||||
std::string cmd;
|
||||
std::vector<std::string> args;
|
||||
std::map<std::string, std::string> env;
|
||||
std::vector<std::string> depends_on;
|
||||
int sleep_before = 0;
|
||||
};
|
||||
|
||||
class InitParser {
|
||||
public:
|
||||
// 优先读 services.yaml,不存在则回退 init.txt
|
||||
static std::vector<ServiceConfig> load(const std::string& conf_dir);
|
||||
|
||||
// 解析 services.yaml
|
||||
static std::vector<ServiceConfig> parseYaml(const std::string& path);
|
||||
|
||||
// 解析 init.txt(向后兼容)
|
||||
static std::vector<ServiceConfig> parseTxt(const std::string& path);
|
||||
|
||||
// 将 ServiceConfig 拼成完整命令字符串(cmd + " " + args.join(" "))
|
||||
static std::string toCommand(const ServiceConfig& svc);
|
||||
};
|
||||
|
||||
} // namespace qing::registry
|
||||
```
|
||||
|
||||
**解析流程**:
|
||||
|
||||
```
|
||||
load(conf_dir):
|
||||
1. 检查 conf_dir/services.yaml 是否存在
|
||||
2. 存在 → parseYaml() → 返回 ServiceConfig 列表
|
||||
3. 不存在 → 检查 conf_dir/init.txt
|
||||
4. 存在 → parseTxt() → 每行作为 ServiceConfig{name="", cmd=line, args=[]}
|
||||
5. 都不存在 → 报错退出
|
||||
```
|
||||
|
||||
**parseYaml 核心逻辑**(使用 yaml-cpp):
|
||||
|
||||
```cpp
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
std::vector<ServiceConfig> InitParser::parseYaml(const std::string& path) {
|
||||
YAML::Node root = YAML::LoadFile(path);
|
||||
std::vector<ServiceConfig> result;
|
||||
|
||||
for (const auto& node : root["services"]) {
|
||||
ServiceConfig svc;
|
||||
svc.name = node["name"].as<std::string>("");
|
||||
svc.cmd = node["cmd"].as<std::string>();
|
||||
|
||||
if (node["args"])
|
||||
for (const auto& a : node["args"])
|
||||
svc.args.push_back(a.as<std::string>());
|
||||
|
||||
if (node["env"])
|
||||
for (const auto& kv : node["env"])
|
||||
svc.env[kv.first.as<std::string>()] = kv.second.as<std::string>();
|
||||
|
||||
if (node["depends_on"])
|
||||
for (const auto& d : node["depends_on"])
|
||||
svc.depends_on.push_back(d.as<std::string>());
|
||||
|
||||
if (node["sleep_before"])
|
||||
svc.sleep_before = node["sleep_before"].as<int>(0);
|
||||
|
||||
result.push_back(std::move(svc));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**启动执行逻辑**(改动 `pmc_mtd::parse_self_init_list`):
|
||||
|
||||
```cpp
|
||||
// pmc_mtd.cpp 中改造
|
||||
void pmc_mtd::init_from_config(const std::string& conf_dir) {
|
||||
auto services = InitParser::load(conf_dir);
|
||||
|
||||
for (const auto& svc : services) {
|
||||
// 1. sleep_before 等待
|
||||
if (svc.sleep_before > 0)
|
||||
std::this_thread::sleep_for(std::chrono::seconds(svc.sleep_before));
|
||||
|
||||
// 2. 设置环境变量
|
||||
for (const auto& [k, v] : svc.env)
|
||||
setenv(k.c_str(), v.c_str(), 1);
|
||||
|
||||
// 3. 拼接完整命令并启动
|
||||
std::string full_cmd = InitParser::toCommand(svc);
|
||||
// full_cmd = "bash /opt/micro_servers/db2/_run.sh --port 8007 --log-level info"
|
||||
ppool.crtp(full_cmd);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 向后兼容策略
|
||||
|
||||
```
|
||||
services.yaml 存在?
|
||||
│
|
||||
┌─── 是 ──┤── 否 ───┐
|
||||
▼ ▼
|
||||
parseYaml() init.txt 存在?
|
||||
│ │
|
||||
│ ┌── 是 ──┤── 否 ───┐
|
||||
│ ▼ ▼
|
||||
│ parseTxt() 报错退出
|
||||
│ │
|
||||
└─────┬─────┘
|
||||
▼
|
||||
ServiceConfig[]
|
||||
逐个启动
|
||||
```
|
||||
|
||||
- `init.txt` 模式下,每行解析为 `{name="", cmd=line, args=[], sleep_before=0}`,行为与当前完全一致
|
||||
- `services.yaml` 模式下,享受参数透传和 sleep 管理能力
|
||||
- Makefile 的 `CONF_NAME` 可逐步从 `init.txt` 切换为 `services.yaml`
|
||||
|
||||
---
|
||||
|
||||
## 5. Step 2:_run.sh 参数透传
|
||||
|
||||
### 5.1 改造规则
|
||||
|
||||
每个 `_run.sh` 的末尾 `exec` 行必须带 `"$@"`,同时移除硬编码参数和散落的 `sleep`。
|
||||
|
||||
**改造模板**:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# <service>/_run.sh — 标准化启动脚本
|
||||
cd "$(dirname "$0")" || exit
|
||||
|
||||
# --- 服务特有前置(venv / setcap 等)保持不变 ---
|
||||
|
||||
# 启动:"$@" 透传 core/services.yaml 传入的参数
|
||||
exec <binary_or_python> "$@"
|
||||
```
|
||||
|
||||
### 5.2 逐服务改造清单
|
||||
|
||||
| 服务 | 当前 exec 行 | 改造后 | 额外改动 |
|
||||
|------|-------------|--------|----------|
|
||||
| db2 | `exec ./build/db2 "$@"` | ✅ 无需改 | — |
|
||||
| dnfs2 | `exec ./build/dnfs2 "$@"` | ✅ 无需改 | — |
|
||||
| fsys | `exec ./build/fsys "$@"` | ✅ 无需改 | — |
|
||||
| ca | `exec ./build/ca.out "$@"` | ✅ 无需改 | 移除 `sleep 2`(移交 YAML) |
|
||||
| gateway | `exec ./build/gateway.out "$@"` | ✅ 无需改 | 移除 `sleep 2`(移交 YAML) |
|
||||
| waf | `exec ./waf.bin -c config.yaml` | `exec ./waf.bin -c config.yaml "$@"` | 保留 `-c config.yaml` 作为脚本默认值,services.yaml `args: []` |
|
||||
| captcha2 | `exec ./captcha2.bin` | `exec ./captcha2.bin "$@"` | 移除 `sleep 1`(移交 YAML) |
|
||||
| aibox | `exec python aibox.py $@` | `exec python aibox.py "$@"` | `$@` → `"$@"` 加引号 |
|
||||
| ai_review | `exec python3 ai_review.py` | `exec python3 ai_review.py "$@"` | — |
|
||||
| smtp | `exec python3 smtp_v2.py` | `exec python3 smtp_v2.py "$@"` | 移除 `sleep 2`(移交 YAML) |
|
||||
| redirect80 | `exec python3 redirect80.py` | `exec python3 redirect80.py "$@"` | — |
|
||||
|
||||
**统计**:6 个无需改、5 个需改 exec 行、4 个需移除 sleep。
|
||||
|
||||
### 5.3 sleep 迁移对照
|
||||
|
||||
| 服务 | 当前 sleep 位置 | 迁移到 services.yaml |
|
||||
|------|----------------|---------------------|
|
||||
| ca | `_run.sh` 中 `sleep 2` | `sleep_before: 2` |
|
||||
| gateway | `_run.sh` 中 `sleep 2` | `sleep_before: 2` |
|
||||
| captcha2 | `_run.sh` 中 `sleep 1` | `sleep_before: 1` |
|
||||
| smtp | `_run.sh` 中 `sleep 2` | `sleep_before: 2` |
|
||||
|
||||
移除后 `_run.sh` 只负责"切换目录 + 前置准备 + exec",不负责时序控制。
|
||||
|
||||
---
|
||||
|
||||
## 6. Step 3:CLI 参数标准化
|
||||
|
||||
### 6.1 通用参数约定
|
||||
|
||||
定义一组所有服务都应支持的通用 CLI 参数:
|
||||
|
||||
| 参数 | 短选项 | 类型 | 默认值 | 说明 |
|
||||
|------|--------|------|--------|------|
|
||||
| `--host` | `-H` | string | `127.0.0.1` | 绑定地址 |
|
||||
| `--port` | `-p` | int | 服务特定 | 监听端口 |
|
||||
| `--config` | `-c` | string | 服务特定 | 配置文件路径 |
|
||||
| `--log-level` | `-l` | enum | `info` | `debug\|info\|warn\|error` |
|
||||
| `--instance` | `-i` | string | 服务名 | 实例标识(未来多实例/注册中心用) |
|
||||
| `--help` | `-h` | flag | — | 输出帮助 |
|
||||
| `--version` | `-v` | flag | — | 输出版本 |
|
||||
|
||||
**优先级规则**:`CLI 参数 > 环境变量 > confs/配置文件 > 硬编码默认值`
|
||||
|
||||
### 6.2 _run.sh 默认值注入
|
||||
|
||||
在服务二进制还不支持所有参数的过渡期,`_run.sh` 可注入默认值:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# waf/_run.sh — 标准化后
|
||||
cd "$(dirname "$0")" || exit
|
||||
|
||||
# 默认参数(可被 services.yaml args 覆盖)
|
||||
DEFAULT_ARGS=(-c config.yaml)
|
||||
|
||||
exec ./waf.bin "${DEFAULT_ARGS[@]}" "$@"
|
||||
```
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# db2/_run.sh — 标准化后
|
||||
cd "$(dirname "$0")" || exit
|
||||
set -e
|
||||
|
||||
# 默认参数
|
||||
DEFAULT_ARGS=(--host 127.0.0.1 --port 8005 --log-level info)
|
||||
|
||||
exec ./build/db2 "${DEFAULT_ARGS[@]}" "$@"
|
||||
```
|
||||
|
||||
**原理**:`"$@"` 追加在 `DEFAULT_ARGS` 之后,如果用户通过 services.yaml 传了同名参数,取决于各服务的解析逻辑(`boost::program_options` 默认后者覆盖前者;Python `argparse` 同理)。
|
||||
|
||||
### 6.3 各语言适配指南
|
||||
|
||||
#### C++ 服务(db2, dnfs2, fsys, captcha2)
|
||||
|
||||
当前这 4 个服务无 CLI 框架。建议引入 `boost::program_options`(ca/gateway 已有先例):
|
||||
|
||||
```cpp
|
||||
// db2/src/db2.cpp — 新增 CLI 解析
|
||||
#include <boost/program_options.hpp>
|
||||
namespace po = boost::program_options;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
po::options_description desc("db2 — database RPC service");
|
||||
desc.add_options()
|
||||
("help,h", "output help message")
|
||||
("version,v", "output version message")
|
||||
("host,H", po::value<std::string>()->default_value("127.0.0.1"), "bind address")
|
||||
("port,p", po::value<int>()->default_value(8005), "listen port")
|
||||
("log-level,l", po::value<std::string>()->default_value("info"),
|
||||
"debug|info|warn|error")
|
||||
("instance,i", po::value<std::string>()->default_value("db2"), "instance name")
|
||||
;
|
||||
|
||||
po::variables_map vm;
|
||||
try {
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
po::notify(vm);
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "ERROR: " << e.what() << std::endl;
|
||||
std::cerr << desc << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (vm.count("help")) { std::cout << desc << std::endl; return 0; }
|
||||
if (vm.count("version")) { std::cout << "db2 1.0.0" << std::endl; return 0; }
|
||||
|
||||
std::string host = vm["host"].as<std::string>();
|
||||
int port = vm["port"].as<int>();
|
||||
std::string log_level = vm["log-level"].as<std::string>();
|
||||
|
||||
// 后续使用 host/port/log_level 启动服务...
|
||||
}
|
||||
```
|
||||
|
||||
#### C++ 服务(ca, gateway)— 扩展现有 program_options
|
||||
|
||||
ca 和 gateway已有 `po::options_description` 但只注册了 `--help`/`--version`,需追加通用参数:
|
||||
|
||||
```cpp
|
||||
// ca/src/main.cpp — 扩展
|
||||
desc.add_options()
|
||||
("help,h", "output help message")
|
||||
("version,v", "output version message")
|
||||
// 新增通用参数
|
||||
("host,H", po::value<std::string>()->default_value("127.0.0.1"), "bind address")
|
||||
("port,p", po::value<int>()->default_value(8443), "listen port")
|
||||
("log-level,l", po::value<std::string>()->default_value("info"),
|
||||
"debug|info|warn|error")
|
||||
("instance,i", po::value<std::string>()->default_value("ca"), "instance name")
|
||||
;
|
||||
```
|
||||
|
||||
注意:ca/gateway 当前配置走 confs + 环境变量,CLI 参数应作为**覆盖层**,不替换 confs 机制。
|
||||
|
||||
#### Go 服务(waf)— 扩展现有 flag
|
||||
|
||||
waf 已有 `flag` 包,追加通用参数:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
var (
|
||||
configFile = flag.String("c", "config.yaml", "配置文件路径")
|
||||
host = flag.String("H", "127.0.0.1", "绑定地址")
|
||||
port = flag.Int("p", 443, "监听端口")
|
||||
logLevel = flag.String("l", "info", "debug|info|warn|error")
|
||||
instance = flag.String("i", "waf", "实例名")
|
||||
doList = flag.Bool("list", false, "列出所有规则后退出")
|
||||
doTest = flag.String("test", "", "校验单个规则文件语法后退出")
|
||||
doCheck = flag.String("check", "", "用当前规则测试输入字符串后退出")
|
||||
)
|
||||
flag.Parse()
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
注意:Go `flag` 包不支持短选项连写(`-H 127.0.0.1`),与 C++ `program_options` 的 `-H` 语法兼容。
|
||||
|
||||
#### Python 服务(aibox, ai_review, smtp, redirect80)
|
||||
|
||||
建议统一使用 `argparse`:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""ai_review — AI 审核服务"""
|
||||
import argparse
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description="ai_review — AI 审核服务")
|
||||
parser.add_argument("-H", "--host", default="127.0.0.1", help="绑定地址")
|
||||
parser.add_argument("-p", "--port", type=int, default=8007, help="监听端口")
|
||||
parser.add_argument("-l", "--log-level", default="info",
|
||||
choices=["debug", "info", "warn", "error"], help="日志级别")
|
||||
parser.add_argument("-i", "--instance", default="ai_review", help="实例名")
|
||||
return parser.parse_args()
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = parse_args()
|
||||
# 后续使用 args.host, args.port, args.log_level...
|
||||
```
|
||||
|
||||
aibox 已有 `import argparse`(死代码),激活即可。
|
||||
|
||||
### 6.4 环境变量桥接
|
||||
|
||||
对于走 confs + 环境变量的服务(ca, gateway, dnfs2),CLI 参数应映射到环境变量,保持与现有 confs 机制兼容:
|
||||
|
||||
```bash
|
||||
# ca/_run.sh — 环境变量桥接模式
|
||||
cd "$(dirname "$0")" || exit
|
||||
|
||||
# 从命令行参数提取并导出环境变量(供 confs 加载逻辑使用)
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-H|--host) export CA_HOST="$2"; shift 2 ;;
|
||||
-p|--port) export CA_PORT="$2"; shift 2 ;;
|
||||
-l|--log-level) export LOG_LEVEL="$2"; shift 2 ;;
|
||||
*) EXTRA_ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
exec ./build/ca.out "${EXTRA_ARGS[@]}"
|
||||
```
|
||||
|
||||
这种桥接方式不需要立刻改 ca 的 C++ 源码——`_run.sh` 先把 CLI 参数翻译成环境变量,ca 现有的环境变量读取逻辑自然生效。
|
||||
|
||||
---
|
||||
|
||||
## 7. 实施计划
|
||||
|
||||
### 7.1 分步交付
|
||||
|
||||
| 步骤 | 改动范围 | 涉及文件 | 预估工时 | 风险 |
|
||||
|------|----------|----------|----------|------|
|
||||
| **S1-a** | yaml-cpp vendored + Makefile 集成 | `core/third_party/yaml-cpp/`、`core/Makefile` | 1h | 低 |
|
||||
| **S1-b** | InitParser 实现 | `core/include/registry/InitParser.hh`、`core/src/registry/InitParser.cpp` | 2h | 低 |
|
||||
| **S1-c** | pmc_mtd 改造(调用 InitParser) | `core/src/main/pmc_mtd.cpp` | 1h | 中(改核心启动路径) |
|
||||
| **S1-d** | 创建 services.yaml | `core/services.yaml` | 0.5h | 低 |
|
||||
| **S1-e** | 回退测试(删除 services.yaml 验证 init.txt 兼容) | — | 0.5h | 低 |
|
||||
| **S2-a** | 5 个 _run.sh 改 exec 行 | `waf/`、`captcha2/`、`aibox/`、`ai_review/`、`smtp/`、`redirect80/` 的 `_run.sh` | 0.5h | 极低 |
|
||||
| **S2-b** | 4 个 _run.sh 移除 sleep | `ca/`、`gateway/`、`captcha2/`、`smtp/` 的 `_run.sh` | 0.2h | 低(sleep 已移交 YAML) |
|
||||
| **S3-a** | 定义 CLI 标准文档(本文档 §6) | — | 0h(已完成) | — |
|
||||
| **S3-b** | C++ 服务适配(ca/gateway 扩展,db2/dnfs2/fsys/captcha2 新增) | 各服务 `src/*.cpp` | 4h | 中 |
|
||||
| **S3-c** | Python 服务适配(argparse) | `aibox.py`、`ai_review.py`、`smtp_v2.py`、`redirect80.py` | 2h | 低 |
|
||||
| **S3-d** | Go 服务适配(waf flag 扩展) | `waf/waf.go` | 0.5h | 低 |
|
||||
| **S3-e** | _run.sh 默认值注入 | 各服务 `_run.sh` | 0.5h | 低 |
|
||||
| **S4** | 端到端测试 | — | 1h | — |
|
||||
|
||||
**总计预估:~14 小时**
|
||||
|
||||
### 7.2 推荐实施顺序
|
||||
|
||||
```
|
||||
S1-a (yaml-cpp 集成)
|
||||
└─▶ S1-b (InitParser)
|
||||
└─▶ S1-c (pmc_mtd 改造)
|
||||
└─▶ S1-d (services.yaml)
|
||||
└─▶ S1-e (回退测试) ──▶ Step 1 完成
|
||||
│
|
||||
▼
|
||||
S2-a + S2-b ( _run.sh 改造) ──▶ Step 2 完成
|
||||
│
|
||||
▼
|
||||
S3-b/c/d (各服务 CLI 适配)
|
||||
└─▶ S3-e (默认值注入) ──▶ Step 3 完成
|
||||
│
|
||||
▼
|
||||
S4 (端到端测试)
|
||||
```
|
||||
|
||||
### 7.3 新增/改动文件清单
|
||||
|
||||
```
|
||||
core/
|
||||
├── include/registry/
|
||||
│ └── InitParser.hh # 【新增】YAML/TXT 解析器头文件
|
||||
├── src/registry/
|
||||
│ └── InitParser.cpp # 【新增】YAML/TXT 解析器实现
|
||||
├── src/main/pmc_mtd.cpp # 【改动】调用 InitParser 替代 parse_self_init_list
|
||||
├── src/main.cpp # 【改动】--init 帮助文本更新
|
||||
├── Makefile # 【改动】追加 InitParser.o 编译目标 + -lyaml-cpp 链接
|
||||
├── _run.sh # 【改动】-i init.txt → -i .(目录模式自动查找)
|
||||
├── services.yaml # 【新增】声明式服务启动配置
|
||||
└── init.txt # 【保留】向后兼容回退
|
||||
|
||||
项目根目录/
|
||||
├── _setup.deb.sh # 【改动】追加 libyaml-cpp-dev
|
||||
└── _setup.rhel.sh # 【改动】追加 yaml-cpp-devel
|
||||
|
||||
各服务 _run.sh(11 个) # 【已完成】Step 2 统一 "$@" 透传 + 移除 sleep
|
||||
|
||||
C++ 服务源码(6 个) # 【待改】Step 3 新增/扩展 program_options
|
||||
Python 服务源码(4 个) # 【待改】Step 3 新增 argparse
|
||||
Go 服务源码(1 个) # 【待改】Step 3 扩展 flag
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 验证清单
|
||||
|
||||
### 8.1 Step 1 验证
|
||||
|
||||
- [ ] `make` 编译成功,yaml-cpp 源文件正确编译链接
|
||||
- [ ] `services.yaml` 存在时,core 按 YAML 声明启动全部 11 个服务
|
||||
- [ ] `services.yaml` 不存在时,core 回退 `init.txt`,行为与改造前完全一致
|
||||
- [ ] `args` 字段的参数正确透传到 `_run.sh`
|
||||
- [ ] `env` 字段的环境变量在子进程启动前生效
|
||||
- [ ] `sleep_before` 字段正确控制启动间隔
|
||||
|
||||
### 8.2 Step 2 验证
|
||||
|
||||
- [x] 全部 11 个 `_run.sh` 末尾 `exec` 行带 `"$@"`
|
||||
- [x] waf 保留 `-c config.yaml` 作为脚本默认值,追加 `"$@"` 透传,services.yaml `args: []`
|
||||
- [x] aibox 的 `$@` 改为 `"$@"`(加引号,防空格路径问题)
|
||||
- [x] ca/gateway/captcha2/smtp 的 `sleep` 从 `_run.sh` 移除,由 `services.yaml` 的 `sleep_before` 管理
|
||||
- [ ] 手动执行 `_run.sh --help` 能透传到二进制(依赖 Step 3 CLI 适配)
|
||||
|
||||
### 8.3 Step 3 验证
|
||||
|
||||
- [ ] 全部服务支持 `--help` 输出用法
|
||||
- [ ] 全部服务支持 `--version` 输出版本
|
||||
- [ ] `--port` 能覆盖默认端口并生效
|
||||
- [ ] `--log-level` 能控制日志输出级别
|
||||
- [ ] `_run.sh` 默认值注入 + `services.yaml` 覆盖,两者叠加不冲突
|
||||
- [ ] ca/gateway 的环境变量桥接模式正确工作
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与缓解
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| yaml-cpp 编译增加构建时间 | 低(~10s) | 可接受;如需优化可预编译为静态库 |
|
||||
| pmc_mtd 改造影响核心启动路径 | 高 | 保留 `init.txt` 回退路径;先在测试环境验证 |
|
||||
| _run.sh 移除 sleep 后启动时序变化 | 中 | `services.yaml` 的 `sleep_before` 提供等价等待 |
|
||||
| 各服务 CLI 参数适配工作量大 | 中 | 分批适配;_run.sh 默认值注入可过渡 |
|
||||
| Go flag 与 C++ program_options 语法差异 | 低 | 两者都支持 `-H value` 和 `--host value` 格式 |
|
||||
| Python argparse 与 C++ program_options 行为差异 | 低 | 测试覆盖;argparse 默认不覆盖已设默认值 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 附录
|
||||
|
||||
### A. 各服务端口与配置对照
|
||||
|
||||
| 服务 | 端口 | 协议 | 配置来源 | CLI 框架现状 |
|
||||
|------|------|------|----------|-------------|
|
||||
| db2 | 8005 | pymsg | 硬编码 | 无 |
|
||||
| ai_review | 8007 | http | 硬编码 | 无 |
|
||||
| dnfs2 | 8006 | http | 硬编码 + confs | 无 |
|
||||
| smtp | 8008 | pymsg | 硬编码 | 无 |
|
||||
| fsys | 8090 | http | 硬编码 | 无 |
|
||||
| waf | 443 | https | YAML 配置文件 | Go flag ✅ |
|
||||
| ca | 8443 | https | confs + 环境变量 | program_options(仅 --help/--version) |
|
||||
| gateway | 8445 | http | confs + 环境变量 | program_options(仅 --help/--version) |
|
||||
| captcha2 | 8009 | pymsg | 硬编码 | 无 |
|
||||
| aibox | 8765 | websocket | _setenv.sh + 硬编码 | argparse(已 import 未使用) |
|
||||
| redirect80 | 80 | http | 硬编码 | 无 |
|
||||
|
||||
### B. yaml-cpp 集成验证
|
||||
|
||||
已采用系统包方式(非 vendored),在 `_setup.deb.sh` / `_setup.rhel.sh` 中声明依赖:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install libyaml-cpp-dev
|
||||
|
||||
# RHEL/CentOS
|
||||
sudo dnf install yaml-cpp-devel
|
||||
```
|
||||
|
||||
Makefile 中 `-lyaml-cpp` 链接,`InitParser.cpp` 中 `#include <yaml-cpp/yaml.h>` 引用。
|
||||
|
||||
### C. 环境变量约定(现有 + 新增)
|
||||
|
||||
| 环境变量 | 使用方 | 当前/新增 | 说明 |
|
||||
|----------|--------|-----------|------|
|
||||
| `CONFS_HOST` | ca, gateway, dnfs2 | 现有 | confs 服务地址,默认 `127.0.0.1` |
|
||||
| `CONFS_PORT` | ca, gateway, dnfs2 | 现有 | confs 服务端口,默认 `8010` |
|
||||
| `CORE_HOST` | 未来 RegistrationClient | 新增 | core 注册中心地址,默认 `127.0.0.1` |
|
||||
| `CORE_PORT` | 未来 RegistrationClient | 新增 | core 注册中心端口,默认 `5200` |
|
||||
| `LOG_LEVEL` | 全部服务(可选) | 新增 | 日志级别,可被 `--log-level` 覆盖 |
|
||||
|
||||
### D. 与注册中心计划的衔接点
|
||||
|
||||
| 本方案产出 | 注册中心计划消费点 |
|
||||
|-----------|-------------------|
|
||||
| `services.yaml` 格式 | P7 直接复用,追加 `health_endpoint`/`protocol`/`restart_max` 等字段 |
|
||||
| `InitParser` 解析器 | P2 调用 `parseYaml()` 后自动填充 `ServiceRegistry` |
|
||||
| `ServiceConfig.depends_on` | P7 DAG 拓扑排序的输入 |
|
||||
| `ServiceConfig.args` | 各服务通过 CLI 参数接收注册中心分配的端口/实例名 |
|
||||
| CLI `--instance` 参数 | P5 RegistrationClient SDK 用此值作为注册服务名 |
|
||||
| CLI `--port` 参数 | P0 ServiceEntry 的 port 字段来源 |
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> ca(Central Auth,C++20,v1.0.0)是 UniServe Framework 的中央认证服务,承接原 gateway v0.8.0 的全部认证职责。
|
||||
> 本索引汇总:① 从 gateway v0.9.0 认证独立化**迁移接收**的审计项(已在 ca 闭环);② **ca 相关开放项**(与 gateway 转发层/配置共享,需 ca 侧处理);③ ca 原生安全态势基线。
|
||||
> 关联文档:gateway 合并审计 [`gateway_SECURITY_AUDIT.md`](gateway_SECURITY_AUDIT.md);认证迁移设计 [`../reports/auth_service_plan.md`](../reports/auth_service_plan.md)。
|
||||
> 关联文档:gateway 合并审计 [`gateway_SECURITY_AUDIT.md`](gateway_SECURITY_AUDIT.md)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -109,7 +109,6 @@
|
||||
替代 Pillow,协议完全兼容,gateway 零改动。
|
||||
|
||||
- 重写审计报告: [captcha2_SECURITY_AUDIT_20260809.md](captcha2_SECURITY_AUDIT_20260809.md)
|
||||
- 开发方案文档: [../reports/captcha_cpp_rewrite_dev_doc.md](../reports/captcha_cpp_rewrite_dev_doc.md)
|
||||
- `core/init.txt` 已切换为 `captcha2/_run.sh`
|
||||
|
||||
本报告中的遗留问题在 C++ 版中的状态:
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
驱动大顺(Dashun)蜘蛛机器人(树莓派 aarch64,10 舵机 + 2 云台,
|
||||
PCA9685 PWM 控制器 + MPU6050 姿态传感器)。
|
||||
|
||||
迁移方案与审计修复映射见
|
||||
[docs/reports/bot4cj_python_migration_dev_doc.md](../docs/reports/bot4cj_python_migration_dev_doc.md)。
|
||||
|
||||
## 特性
|
||||
|
||||
- **零第三方依赖**:纯标准库(fcntl / struct / threading / wave / argparse),
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
|
||||
硬件驱动插件(modules/<Name>.py)替代仓颉版的 Lua 扩展层,
|
||||
协议为 build(ctx) / call(ctx) / destroy(ctx) 三段生命周期。
|
||||
|
||||
迁移方案见 docs/reports/bot4cj_python_migration_dev_doc.md。
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
@@ -215,7 +215,7 @@ class MatrixSaver:
|
||||
#
|
||||
# 首行 '-' 数值列解析为 0(Servo 有 max/min 钳位保护)。
|
||||
# actpred/mt_parser.py 曾用 @Servo max 列填充首行 '-',2026-08 已统一
|
||||
# 为本语义(填 0),见 docs/reports/bot4cj_python_migration_dev_doc.md §2.3。
|
||||
# 为本语义(填 0)。
|
||||
|
||||
def get_mt(obj, row, col):
|
||||
"""获得项目值:原始值是占位符且非首行(row>1),递归取上一行同列值"""
|
||||
|
||||
Reference in new issue
Block a user