From 6f1f2bd296e9b5bfdb9b856d9c99299da5b7e5b4 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Thu, 20 Aug 2026 11:43:45 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B8=85=E7=90=86=E5=B7=B2=E8=90=BD?= =?UTF-8?q?=E5=9C=B0=E7=9A=84=E5=BC=80=E5=8F=91=E8=AE=A1=E5=88=92=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=B9=B6=E4=BF=AE=E5=A4=8D=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除 10 篇已完成/已实施的开发计划文档(认证独立化、captcha/confs C++ 重写、services.yaml、pybot 迁移、aibox --once/工具分层/CLI 化/ 重构快照、actpred cv2.LUT 优化),同步修复 ca/captcha2/pybot/aibox README、代码注释、安全审计与报告中的 12 处引用,MODULES.md 文档 计数更新为 67 篇。 --- actpred/mt_parser.py | 3 +- aibox/README.md | 2 +- aibox/aibox.py | 2 +- ca/README.md | 2 +- captcha2/README.md | 1 - docs/MODULES.md | 2 +- ...tpred_cv2_lut_benchmark_report_20260817.md | 3 +- ...o_preprocess_optimization_plan_20260817.md | 154 ---- docs/reports/aibox_cli_once_mode_dev_doc.md | 358 -------- .../aibox_file_network_cli_refactor.md | 183 ---- .../reports/aibox_orchestrator_worker_plan.md | 184 ---- .../aibox_refactor_progress_20260814.md | 167 ---- docs/reports/auth_service_plan.md | 355 -------- docs/reports/blackbird_project_profile.md | 6 +- .../bot4cj_python_migration_dev_doc.md | 406 --------- docs/reports/captcha_cpp_rewrite_dev_doc.md | 750 ---------------- docs/reports/confs2_cpp_rewrite_dev_doc.md | 740 ---------------- docs/reports/core_services_yaml_dev_doc.md | 806 ------------------ docs/security/ca_SECURITY_AUDIT.md | 2 +- .../captcha_SECURITY_AUDIT_20260801.md | 1 - pybot/README.md | 3 - pybot/pybot/__init__.py | 2 - pybot/pybot/parser.py | 2 +- 23 files changed, 11 insertions(+), 4123 deletions(-) delete mode 100644 docs/reports/actpred_hailo_preprocess_optimization_plan_20260817.md delete mode 100644 docs/reports/aibox_cli_once_mode_dev_doc.md delete mode 100644 docs/reports/aibox_file_network_cli_refactor.md delete mode 100644 docs/reports/aibox_orchestrator_worker_plan.md delete mode 100644 docs/reports/aibox_refactor_progress_20260814.md delete mode 100644 docs/reports/auth_service_plan.md delete mode 100644 docs/reports/bot4cj_python_migration_dev_doc.md delete mode 100644 docs/reports/captcha_cpp_rewrite_dev_doc.md delete mode 100644 docs/reports/confs2_cpp_rewrite_dev_doc.md delete mode 100644 docs/reports/core_services_yaml_dev_doc.md diff --git a/actpred/mt_parser.py b/actpred/mt_parser.py index e9496d5..5165629 100644 --- a/actpred/mt_parser.py +++ b/actpred/mt_parser.py @@ -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 diff --git a/aibox/README.md b/aibox/README.md index 471ea3b..3b97766 100644 --- a/aibox/README.md +++ b/aibox/README.md @@ -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) diff --git a/aibox/aibox.py b/aibox/aibox.py index 5bcc4ef..d423ab7 100644 --- a/aibox/aibox.py +++ b/aibox/aibox.py @@ -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() diff --git a/ca/README.md b/ca/README.md index 599c224..927beaf 100644 --- a/ca/README.md +++ b/ca/README.md @@ -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` diff --git a/captcha2/README.md b/captcha2/README.md index 438c3dc..e358a8c 100644 --- a/captcha2/README.md +++ b/captcha2/README.md @@ -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) ## 许可证 diff --git a/docs/MODULES.md b/docs/MODULES.md index 8ef1f14..6d90826 100644 --- a/docs/MODULES.md +++ b/docs/MODULES.md @@ -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 | ## 九、根级构建与部署脚本 diff --git a/docs/reports/actpred_cv2_lut_benchmark_report_20260817.md b/docs/reports/actpred_cv2_lut_benchmark_report_20260817.md index 760f397..29a3994 100644 --- a/docs/reports/actpred_cv2_lut_benchmark_report_20260817.md +++ b/docs/reports/actpred_cv2_lut_benchmark_report_20260817.md @@ -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`(旧基准) --- diff --git a/docs/reports/actpred_hailo_preprocess_optimization_plan_20260817.md b/docs/reports/actpred_hailo_preprocess_optimization_plan_20260817.md deleted file mode 100644 index 3d9bfa6..0000000 --- a/docs/reports/actpred_hailo_preprocess_optimization_plan_20260817.md +++ /dev/null @@ -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° 余量充足) diff --git a/docs/reports/aibox_cli_once_mode_dev_doc.md b/docs/reports/aibox_cli_once_mode_dev_doc.md deleted file mode 100644 index 72ff512..0000000 --- a/docs/reports/aibox_cli_once_mode_dev_doc.md +++ /dev/null @@ -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 模式行为对齐,减少心智负担 | diff --git a/docs/reports/aibox_file_network_cli_refactor.md b/docs/reports/aibox_file_network_cli_refactor.md deleted file mode 100644 index 3b31477..0000000 --- a/docs/reports/aibox_file_network_cli_refactor.md +++ /dev/null @@ -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 '\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 瓶颈远大于进程开销 | \ No newline at end of file diff --git a/docs/reports/aibox_orchestrator_worker_plan.md b/docs/reports/aibox_orchestrator_worker_plan.md deleted file mode 100644 index bca0318..0000000 --- a/docs/reports/aibox_orchestrator_worker_plan.md +++ /dev/null @@ -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` 已有广播通道)。 diff --git a/docs/reports/aibox_refactor_progress_20260814.md b/docs/reports/aibox_refactor_progress_20260814.md deleted file mode 100644 index abd886c..0000000 --- a/docs/reports/aibox_refactor_progress_20260814.md +++ /dev/null @@ -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) diff --git a/docs/reports/auth_service_plan.md b/docs/reports/auth_service_plan.md deleted file mode 100644 index 4b8c535..0000000 --- a/docs/reports/auth_service_plan.md +++ /dev/null @@ -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(非验签,仅防线编程,防本地误直连/SSR​​F 兜底)。 -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 步同步更新 diff --git a/docs/reports/blackbird_project_profile.md b/docs/reports/blackbird_project_profile.md index 7506627..7dcd5d6 100644 --- a/docs/reports/blackbird_project_profile.md +++ b/docs/reports/blackbird_project_profile.md @@ -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 安全评级演进 diff --git a/docs/reports/bot4cj_python_migration_dev_doc.md b/docs/reports/bot4cj_python_migration_dev_doc.md deleted file mode 100644 index fd3a67b..0000000 --- a/docs/reports/bot4cj_python_migration_dev_doc.md +++ /dev/null @@ -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 解析器先例) - ---- - -*本文档为开发计划,实施前需结合实际硬件环境评估。* diff --git a/docs/reports/captcha_cpp_rewrite_dev_doc.md b/docs/reports/captcha_cpp_rewrite_dev_doc.md deleted file mode 100644 index 1f68bc1..0000000 --- a/docs/reports/captcha_cpp_rewrite_dev_doc.md +++ /dev/null @@ -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` 字段。 diff --git a/docs/reports/confs2_cpp_rewrite_dev_doc.md b/docs/reports/confs2_cpp_rewrite_dev_doc.md deleted file mode 100644 index 4377bc6..0000000 --- a/docs/reports/confs2_cpp_rewrite_dev_doc.md +++ /dev/null @@ -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(); -``` diff --git a/docs/reports/core_services_yaml_dev_doc.md b/docs/reports/core_services_yaml_dev_doc.md deleted file mode 100644 index 7e2e13e..0000000 --- a/docs/reports/core_services_yaml_dev_doc.md +++ /dev/null @@ -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 字段来源 | diff --git a/docs/security/ca_SECURITY_AUDIT.md b/docs/security/ca_SECURITY_AUDIT.md index 4812f3a..41068ee 100644 --- a/docs/security/ca_SECURITY_AUDIT.md +++ b/docs/security/ca_SECURITY_AUDIT.md @@ -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)。 --- diff --git a/docs/security/captcha_SECURITY_AUDIT_20260801.md b/docs/security/captcha_SECURITY_AUDIT_20260801.md index a37bad8..8c21da3 100644 --- a/docs/security/captcha_SECURITY_AUDIT_20260801.md +++ b/docs/security/captcha_SECURITY_AUDIT_20260801.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++ 版中的状态: diff --git a/pybot/README.md b/pybot/README.md index faffbd1..5307d8a 100644 --- a/pybot/README.md +++ b/pybot/README.md @@ -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), diff --git a/pybot/pybot/__init__.py b/pybot/pybot/__init__.py index d7be04d..9067dab 100644 --- a/pybot/pybot/__init__.py +++ b/pybot/pybot/__init__.py @@ -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" diff --git a/pybot/pybot/parser.py b/pybot/pybot/parser.py index 26c41da..dd79c87 100644 --- a/pybot/pybot/parser.py +++ b/pybot/pybot/parser.py @@ -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),递归取上一行同列值"""