Wave 9: fix audit findings, harden ABI, deduplicate config (W12.1-W12.6)
- W12.1 context_plugin (engineer-zhou): wrap C ABI surface in try/catch, add OOM-safe strdup_message_fields helper, make g_max_tokens drive message-count trim (option A). - W12.2 config refactor (architect-lin): introduce plugins/config/include/toml_parse.h to eliminate 74-line parser duplication; config_plugin delegates to host->config_get/set, collapsing the dual-store data island; ConfigStore::get() now copies via thread_local std::string to remove c_str() dangling under concurrent set(). Zero ABI changes. - W12.3 CLI command parsing (engineer-zhao): guard /clear and /context on missing session service; refactor /file dispatch so bare /file write hits usage instead of unknown-command. - W12.4 build path unification (devops-hu): set per-target RUNTIME_OUTPUT_DIRECTORY on dstalk-cli; remove stale build/dstalk-cli/dstalk-cli.exe so build/bin/ is the sole binary. - W12.5 STATUS.md auto-refresh (engineer-li): run W11.6 script to regenerate STATUS from live profile/group data. - W12.6 plugin-abi.md (writer-deng): add §8 exception safety across ABI boundary and §9 string return lifetime; reference real audit-found violations as anti-examples. Verified: cmake build 0 error 0 warning, ctest 4/4 pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,28 +1,28 @@
|
||||
# dstalk 实时编制状态
|
||||
|
||||
> **最后更新**: 2026-05-27
|
||||
> **数据来源**: 由 W10.2 任务自动整理(扫描全部 16 个 `agents/*/profile.md` + 5 个 `agents/groups/*.md` + `git log`)。CEO 可命令任意员工执行同样任务刷新。
|
||||
> **数据来源**: 由 `scripts/refresh_status.py` 自动扫描全部 16 个 `agents/*/profile.md` + 5 个 `agents/groups/*.md` 生成。
|
||||
|
||||
## 表 1:员工状态(16 人)
|
||||
|
||||
| Agent ID | 姓名 | 角色 | 最近一次贡献 | perf_log | 当前小组 | 状态 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| architect-lin | 林深 | 架构师 | W9.4 撰写 plugin-abi.md (7 点 ABI 契约) | 6 | grp-quality-core, grp-ai-plugins | idle |
|
||||
| architect-yang | 杨帆 | 架构师 | W10.1 WORKFLOW §11-§13 状态机 + 验收清单 + 失败回退 | 2 | -- | idle |
|
||||
| architect-huang | 黄岭 | 架构师 | W9.8 initialize_all 容错修复 (fail-fast → fail-continue) | 2 | -- | idle |
|
||||
| engineer-zhao | 赵码 | 工程师 | W9.6 CLI /history 命令 + /status 集成 | 6 | grp-ai-plugins, grp-cli-ux | idle |
|
||||
| engineer-chen | 陈风 | 工程师 | W2.1 跨 DLL 堆释放修复 (unify host->alloc/free) | 2 | -- | idle |
|
||||
| engineer-li | 李明 | 工程师 | W10.2 STATUS.md 实时编制状态文档 (63 行) | 2 | -- | idle |
|
||||
| engineer-zhou | 周岩 | 工程师 | W5.1 SSE buffer_body 优化 (峰值内存 -67%) | 2 | -- | idle |
|
||||
| engineer-sun | 孙宇 | 工程师 | W6.1 LSP reader_loop 协议合规修复 (状态机解析) | 2 | -- | idle |
|
||||
| qa-wang | 王测 | 质量工程师 | W7 smoke test 9 插件加载修复 + Boost.JSON 链接 | 5 | grp-quality-core, grp-cli-ux | idle |
|
||||
| qa-liu | 刘静 | 质量工程师 | W9.10 host_api 单元测试 (8 cases) | 2 | grp-security-audit | idle |
|
||||
| qa-xu | 徐磊 | 质量工程师 | W10.4 POSTMORTEM.md 踩坑记录 (5 事故 7 规则) | 3 | grp-security-audit | idle |
|
||||
| devops-ma | 马奔 | DevOps | W4 CI pipeline GitHub Actions (Linux/Windows 双矩阵) | 2 | grp-build-matrix | idle |
|
||||
| devops-hu | 胡桐 | DevOps | W3 CMake 4 项现代化 (target-based + Boost config) | 2 | grp-build-matrix | idle |
|
||||
| designer-zhu | 朱晴 | UX/CLI 设计师 | W10.3 PROMPT_TEMPLATE.md 子代理模板 (骨架+反模式+正模式) | 2 | grp-cli-ux | idle |
|
||||
| writer-deng | 邓书 | 技术作家 | W9.1 Diátaxis Explanation 文档 (architecture + plugin-lifecycle) | 2 | -- | idle |
|
||||
| security-cao | 曹武 | 安全工程师 | W9.3 日志凭证泄露审计 (8 文件 0 真实漏洞) | 3 | grp-security-audit | idle |
|
||||
| architect-huang | 黄岭 | 架构师 | W11.1 审计 context_plugin.cpp (289行,零Wave覆盖) | 2 | -- | idle |
|
||||
| architect-lin | 林深 | 架构师 | W9.4 撰写 docs/reference/plugin-abi.md Plugin ABI 契约文档(200行) | 6 | grp-ai-plugins, grp-quality-core | idle |
|
||||
| architect-yang | 杨帆 | 架构师 | W10.1 设计协作状态机 + 验收清单 + 失败回退协议,追加 WORKFLOW.md §11–§13 | 2 | -- | idle |
|
||||
| designer-zhu | 朱晴 | UX/CLI 设计师 | W10.3 创建 agents/PROMPT_TEMPLATE.md 子代理 prompt 模板(约 170 行) | 2 | grp-cli-ux | idle |
|
||||
| devops-hu | 胡桐 | DevOps 工程师 | 落地 4 项 CMake 改进 (审查报告 C1-C4) | 2 | grp-build-matrix | idle |
|
||||
| devops-ma | 马奔 | DevOps 工程师 | 落地 CI pipeline (GitHub Actions) | 2 | grp-build-matrix | idle |
|
||||
| engineer-chen | 陈风 | 工程师 | W11.2 审计 config_plugin / ConfigStore 职责划分与跨 DLL 堆合规 | 3 | -- | idle |
|
||||
| engineer-li | 李明 | 工程师 | W11.6 编写 scripts/refresh_status.py 自动扫描 agents/*/profile.md 重新生成 agents/STA... | 3 | -- | idle |
|
||||
| engineer-sun | 孙宇 | 工程师 | W6.1 修复 LSP reader_loop 协议合规 bug(Content-Length 状态机解析) | 2 | -- | idle |
|
||||
| engineer-zhao | 赵码 | 工程师 | W9.6 CLI新增/history[N]命令,含三种边界处理;/status增加history count | 6 | grp-ai-plugins, grp-cli-ux | idle |
|
||||
| engineer-zhou | 周岩 | 工程师 | W5.1 network_plugin SSE 改 buffer_body | 2 | -- | idle |
|
||||
| qa-liu | 刘静 | 质量工程师 | W11.3 event_bus 单元测试 (6 cases, tests/event_bus_test.cpp) + service_registry... | 3 | grp-security-audit | idle |
|
||||
| qa-wang | 王测 | 质量工程师 | W7 smoke test 插件加载修复 | 5 | grp-cli-ux, grp-quality-core | idle |
|
||||
| qa-xu | 徐磊 | 质量工程师 | W11.7 破坏性输入测试:build/dstalk-cli/dstalk-cli.exe (commit 004a81d) 10 场景全 PASS 零崩溃 | 4 | grp-security-audit | idle |
|
||||
| security-cao | 曹武 | 安全工程师 | W9.3 错误日志凭证泄露审计(8文件,0真实漏洞) | 3 | grp-security-audit | idle |
|
||||
| writer-deng | 邓书 | 技术作家 | Diátaxis 第二刀: 补充 Explanation 类文档 — architecture.md (插件架构哲学/三层模型/C ABI... | 2 | -- | idle |
|
||||
|
||||
> **状态判定规则**: 基于 `performance_log` 最后一条的 `rating`——`ongoing` 视为 `working`,其余 (`A/A+/B/completed/done/success/good`) 视为 `idle`。
|
||||
|
||||
@@ -30,38 +30,17 @@
|
||||
|
||||
| group_id | 名称 | lead | members | mission | active_tasks | 状态 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| grp-quality-core | 核心质量小组 | 王测 | 林深, 王测 | 确保插件化架构的代码质量和测试覆盖 | -- (C2 已交付) | 待命 |
|
||||
| grp-ai-plugins | AI 插件小组 | 赵码 | 赵码, 林深 | AI 相关插件的功能完善和架构优化 | -- | 待命 |
|
||||
| grp-cli-ux | CLI 体验小组 | 赵码 | 赵码, 朱晴, 王测 | 改进 dstalk-cli 的交互体验、健壮性和可测试性 | B3: CLI 交互增强 (信号处理/状态命令/退出码/管道输入) | 执行中 |
|
||||
| grp-build-matrix | 构建矩阵小组 | 马奔 | 马奔, 胡桐 | 完善 CI 跨平台构建矩阵 (Linux/Windows, Clang/MSVC) | -- | 待命 |
|
||||
| grp-security-audit | 安全审计小组 | 曹武 | 曹武, 徐磊, 刘静 | 全面审计内存安全、API 密钥处理、反序列化路径 | -- | 待命 |
|
||||
| grp-ai-plugins | AI插件小组 | 赵码 | 赵码, 林深 | AI相关插件的功能完善和架构优化 | -- | 待命 |
|
||||
| grp-build-matrix | 构建矩阵小组 | 马奔 | 马奔, 胡桐 | 完善 CI 跨平台构建矩阵(Linux/Windows,Clang/MSVC),缩短构建时间 | -- | 待命 |
|
||||
| grp-cli-ux | CLI 体验小组 | 赵码 | 赵码, 朱晴, 王测 | 改进 dstalk-cli 的交互体验、健壮性和可测试性 | B3: CLI 交互增强(信号处理、状态命令、退出码语义、管道输入) | 执行中 |
|
||||
| grp-quality-core | 核心质量小组 | 王测 | 林深, 王测 | 确保插件化架构的代码质量和测试覆盖 | -- | 待命 |
|
||||
| grp-security-audit | 安全审计小组 | 曹武 | 曹武, 徐磊, 刘静 | 全面审计 dstalk 的内存安全、API 密钥处理、反序列化路径 | -- | 待命 |
|
||||
|
||||
> **成员列来源**: 以 `agents/groups/*.md` 为准(部分成员 profile 未同步更新 `current_groups`)。
|
||||
|
||||
## Wave 进度
|
||||
|
||||
**已完成高水位**: W9.10(已 commit `5766938`)
|
||||
**已完成高水位**: W11.7(基于 16 份 profile.md 的 performance_log 聚合)
|
||||
|
||||
**已完成 Wave 清单** (WORKFLOW.md §7 + commit message):
|
||||
W1.1, W2.1, W2.2, W3, W4, W5.1, W6.1, W7, W8.1, W9.1, W9.3, W9.4, W9.6, W9.8, W9.10
|
||||
**已发现 Wave 编号**: W1.1, W2.1, W2.2, W5.1, W6.1, W7, W9.3, W9.4, W9.6, W9.10, W10.1, W10.2, W10.3, W10.4, W11.1, W11.2, W11.3, W11.6, W11.7
|
||||
|
||||
**In-flight (W10.x,尚未 commit)**:
|
||||
|
||||
| 任务 | 负责人 | 交付物 | 状态 |
|
||||
|---|---|---|---|
|
||||
| W10.1 | architect-yang (杨帆) | `agents/WORKFLOW.md` §11-§13 (状态机+验收清单+失败回退, +227 行) | 已完成,未 commit |
|
||||
| W10.2 | engineer-li (李明) | `agents/STATUS.md` (本文件, 65 行) | 已完成,未 commit |
|
||||
| W10.3 | designer-zhu (朱晴) | `agents/PROMPT_TEMPLATE.md` (子代理 prompt 模板, 193 行) | 已完成,未 commit |
|
||||
| W10.4 | qa-xu (徐磊) | `agents/POSTMORTEM.md` (项目踩坑记录, 172 行) | 已完成,未 commit |
|
||||
|
||||
**未提交变更摘要**:
|
||||
- `agents/WORKFLOW.md` — §9 追加模板迁移链接 + §11-§13 杨帆扩展
|
||||
- `agents/architect-yang/profile.md` — 追加 W10.1
|
||||
- `agents/designer-zhu/profile.md` — 追加 W10.3
|
||||
- `agents/qa-xu/profile.md` — 追加 W10.4
|
||||
- `agents/engineer-li/profile.md` — 追加 W10.2
|
||||
- `agents/PROMPT_TEMPLATE.md` — 新增 (untracked)
|
||||
- `agents/POSTMORTEM.md` — 新增 (untracked)
|
||||
- `agents/STATUS.md` — 新增 (untracked, 本文件)
|
||||
|
||||
**下一波建议起点**: W11.x(CEO 先 commit W10.1~W10.4,再派新任务;当前 16/16 员工 idle,产能充足)
|
||||
|
||||
@@ -36,6 +36,9 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "W9.4 完成:撰写 docs/reference/plugin-abi.md Plugin ABI 契约文档(200行),涵盖 DSTALK_API_VERSION、内存所有权、跨DLL堆纪律、register_service、on_init/on_shutdown、回调线程安全、依赖声明共7个契约要点。更新 docs/README.md reference 区追加入口"
|
||||
rating: A
|
||||
- date: 2026-05-27
|
||||
event: "W12.2 完成:消除 config_plugin 与 ConfigStore 的 TOML 解析代码重复(提取共享头 toml_parse.h),消除双 store 数据孤岛(config plugin 委托 host store),修复 c_str() 悬垂指针(thread_local 缓存)。build 0 error,4/4 test pass"
|
||||
rating: completed
|
||||
current_groups:
|
||||
- grp-quality-core (成员)
|
||||
- grp-ai-plugins (待命)
|
||||
|
||||
@@ -33,4 +33,14 @@ performance_log:
|
||||
5 个插件缺少 #include <boost/json/src.hpp> (Boost 1.86 不再识别 HEADER_ONLY)。
|
||||
rating: done
|
||||
current_groups: []
|
||||
- date: 2026-05-27
|
||||
event: "W12.4 修复 build 产物路径不一致 (BUG-1)"
|
||||
detail: >
|
||||
根因: build/dstalk-cli/dstalk-cli.exe 是 W3 设置 CMAKE_RUNTIME_OUTPUT_DIRECTORY=bin
|
||||
之前的陈旧产物 (MD5 803ca2ea),W3 后 ninja 链接行已正确输出到 build/bin/dstalk-cli.exe,
|
||||
但旧文件未被 ninja 自动清理,导致两路径存在不同二进制。
|
||||
修复: dstalk-cli/CMakeLists.txt L9-11 新增 set_target_properties RUNTIME_OUTPUT_DIRECTORY
|
||||
${CMAKE_BINARY_DIR}/bin 作为防御性显式声明;删除陈旧 build/dstalk-cli/dstalk-cli.exe。
|
||||
验证: clean rebuild 后仅 build/bin/dstalk-cli.exe 存在,ctest 4/4 pass。
|
||||
rating: done
|
||||
---
|
||||
|
||||
@@ -26,5 +26,8 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "W11.6: 编写 scripts/refresh_status.py 自动扫描 agents/*/profile.md 重新生成 agents/STATUS.md,支持 --dry-run,Python 3.8+ 标准库零依赖"
|
||||
rating: completed
|
||||
- date: 2026-05-27
|
||||
event: "W12.5: 使用 scripts/refresh_status.py 重新生成 agents/STATUS.md (46行),验证脚本对 W11.x (已 commit) 和 W12.x (in-flight) 数据正确解析,dry-run 确认 16 行表1 + 5 行表2 + 高水位 W11.7,build + ctest 全 PASS"
|
||||
rating: completed
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
@@ -42,3 +42,6 @@ current_groups:
|
||||
- date: 2026-05-27
|
||||
event: "W11.4: 实现管道输入支持(grp-cli-ux B3),pipe_mode检测_isatty→读取全部stdin→单次chat→退出;空输入返回1提示empty prompt;0 error 0 warning编译通过;4/4测试100% pass"
|
||||
rating: A
|
||||
- date: 2026-05-27
|
||||
event: "W12.3: 修复3个命令解析bug(BUG-2 /clear空session谎报成功→stderr守卫; BUG-3 /context空session静默→else分支stderr; BUG-4 /file write裸命令→统一token解析入口),build 0 error 0 warning,4/4 test pass"
|
||||
rating: completed
|
||||
|
||||
@@ -20,6 +20,12 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "入职 dstalk 团队"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "W12.1 - context_plugin 三处 bug 修复 (W11.1 audit)"
|
||||
detail: |
|
||||
修复项: (1) C++ 异常穿越 ABI: trim_impl/context_count_tokens/context_trim/on_init 包裹 try/catch, 异常时 int 返回 -1, size_t 返回 0. (2) strdup null check: 引入 strdup_message_fields() + free_msg_strs() 辅助函数, 两处循环逐一检查返回值, OOM 时回滚已分配字段. (3) g_max_tokens 死变量: 选项A — trim_impl max_tokens==0 时用全局值; 结果组装前按 ceil(g_max_tokens/100) 裁剪消息数(粗略 ~100 token/msg).
|
||||
编译 0 error 0 warning, ctest 4/4 pass.
|
||||
rating: completed
|
||||
- date: 2026-05-27
|
||||
event: "W5.1 - network_plugin SSE 改 buffer_body"
|
||||
detail: |
|
||||
|
||||
@@ -23,5 +23,8 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "Diátaxis 第二刀: 补充 Explanation 类文档 — architecture.md (插件架构哲学/三层模型/C ABI) + plugin-lifecycle.md (生命周期/拓扑排序/on_init on_shutdown 契约/ABI 纪律), 更新 docs/README.md 导航"
|
||||
rating: completed
|
||||
- date: 2026-05-27
|
||||
event: "W12.6 ABI 文档缺口填补: plugin-abi.md 追加 §8 异常安全(涵盖 service vtable 函数, 反例来自 context_plugin.cpp L114-226) + §9 字符串返回值生命周期(反例来自 config_store.cpp:72 / config_plugin.cpp:77 的锁外 c_str() 悬垂), 并更新 §2.3/§5.3/§6.4 交叉引用"
|
||||
rating: completed
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
@@ -60,7 +60,7 @@ Core rule: **谁分配,谁释放。分配函数必须与释放函数配对。*
|
||||
分配,**调用方**(即查询该服务的插件)负责 `dstalk_free`。
|
||||
|
||||
**反例**: 插件直接返回 `std::string::c_str()` 或栈上 buffer —— 因为服务调用完成后插件栈帧
|
||||
可能已销毁。
|
||||
可能已销毁。有关字符串返回值的完整规则见 **§9 字符串返回值生命周期**。
|
||||
|
||||
---
|
||||
|
||||
@@ -163,6 +163,9 @@ void (*on_shutdown)(void);
|
||||
- 所有可能抛异常的 C++ 逻辑用 `try { ... } catch (...) { return -1; }` 包裹
|
||||
- `on_shutdown` 同理,即使 void 也不能抛异常
|
||||
|
||||
> **延伸**: 本节仅覆盖 `on_init` / `on_shutdown` 两个生命周期回调。**所有**通过 C ABI 导出的函数
|
||||
> (包括 service vtable 中的函数指针)均适用同样规则,详见 **§8 异常安全——穿越 ABI 边界**。
|
||||
|
||||
---
|
||||
|
||||
## 6. 回调线程安全
|
||||
@@ -192,7 +195,7 @@ handler —— handler 内**不得调用 subscribe/unsubscribe**(会尝试 uni
|
||||
### 6.4 配置 (ConfigStore)
|
||||
|
||||
使用 `std::mutex`:get/set 串行化。`config_get` 返回的指针指向内部 `std::string`;在并发
|
||||
`config_set` 同一 key 后指针可能悬垂——调用方应复制。
|
||||
`config_set` 同一 key 后指针可能悬垂——调用方应复制。详细规则见 **§9 字符串返回值生命周期**。
|
||||
|
||||
### 6.5 Plugin Loader
|
||||
|
||||
@@ -229,8 +232,291 @@ const char* dependencies[DSTALK_MAX_DEPS]; // DSTALK_MAX_DEPS = 8
|
||||
|
||||
---
|
||||
|
||||
## 8. 异常安全——穿越 ABI 边界
|
||||
|
||||
> **强制规则**: 所有导出给 host 的 C 函数(包括 `on_init` / `on_shutdown` 以及 service vtable
|
||||
> 中的每一个函数指针)**严禁**让 C++ 异常穿越函数边界。
|
||||
|
||||
### 8.1 适用范围
|
||||
|
||||
本规则覆盖以下所有通过 C ABI 调用的函数入口:
|
||||
|
||||
1. `dstalk_plugin_info_t` 中的 `on_init` / `on_shutdown` / `on_event`(已在 §5.3 覆盖)
|
||||
2. 所有 `*_service_t` vtable 中的函数指针(如 `context_trim`、`config_get`、
|
||||
`ai_chat` 等)
|
||||
3. `EventBus` 中注册的 `dstalk_event_handler_fn` 回调
|
||||
4. 任何通过 `host->register_service` 注册的 vtable 函数
|
||||
|
||||
**为什么 service vtable 函数也受约束**: vtable 函数指针的类型签名为纯 C(如
|
||||
`int (*)(const dstalk_message_t*, int, dstalk_message_t**, int*, size_t)`),
|
||||
调用方 `host` / 其他插件通过该指针直接调用,调用路径上无 `try/catch` 保护。
|
||||
C++ 异常在此路径上传播 → `std::terminate()` → 进程崩溃。
|
||||
|
||||
### 8.2 实施要求
|
||||
|
||||
**每个**使用以下 C++ 类型的函数外层,必须包裹异常保护:
|
||||
|
||||
- `std::string` / `std::wstring`
|
||||
- `std::vector` / `std::map` / `std::unordered_map` 等 STL 容器
|
||||
- `std::unique_ptr` / `std::shared_ptr` / `std::make_unique` / `std::make_shared`
|
||||
- `std::stringstream` / `std::ifstream` / `std::ofstream`
|
||||
- 任何可能抛出 `std::bad_alloc`、`std::out_of_range`、`std::system_error` 的操作
|
||||
|
||||
**标准包裹模式**:
|
||||
|
||||
```c
|
||||
static int service_function(/* 参数 */) {
|
||||
try {
|
||||
// 主逻辑:可以使用 std::string / std::vector 等
|
||||
std::vector<std::string> items;
|
||||
// ...
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
g_host->log(DSTALK_LOG_ERROR, "service_function: %s", e.what());
|
||||
return -1;
|
||||
} catch (...) {
|
||||
g_host->log(DSTALK_LOG_ERROR, "service_function: unknown exception");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 错误返回约定
|
||||
|
||||
| 返回类型 | 错误值 | 示例函数 |
|
||||
|----------|--------|----------|
|
||||
| `int` | 非零(通常 `-1`) | `on_init`, `context_trim`, `config_set` |
|
||||
| `const char*` / `char*` | `nullptr` | `config_get`, `strdup` |
|
||||
| `bool` | `false` | (当前无 bool 返回值的 vtable 函数) |
|
||||
| `size_t` | `0` | `context_count_tokens` |
|
||||
| `void` | 无返回值;仅记日志,禁止抛异常 | `on_shutdown`, `context_set_max_tokens` |
|
||||
|
||||
失败时推荐调用 `host->log(DSTALK_LOG_ERROR, "function_name: <reason>")` 记录原因,
|
||||
便于诊断。`void` 返回类型的函数即使无法向调用方报告错误,也必须记录日志。
|
||||
|
||||
### 8.4 反例:未保护的 service vtable 函数
|
||||
|
||||
以下代码来自 `context_plugin.cpp`(`trim_impl`, L114-226)。该函数由 vtable 中的
|
||||
`context_trim` 直接调用,底层使用 `std::vector` / `std::string`,无 `try/catch`:
|
||||
|
||||
```c
|
||||
// 反例:C++ 异常可穿越 ABI 边界 → std::terminate()
|
||||
static int trim_impl(const dstalk_message_t* in, int in_count,
|
||||
dstalk_message_t** out, int* out_count,
|
||||
size_t max_tokens) {
|
||||
// 无 try/catch 包裹!
|
||||
|
||||
// ▼ std::vector::reserve / push_back 可抛 std::bad_alloc
|
||||
std::vector<TrimMessage> messages;
|
||||
messages.reserve(in_count);
|
||||
for (int i = 0; i < in_count; ++i) {
|
||||
TrimMessage tm;
|
||||
tm.role = in[i].role; // std::string::operator= → bad_alloc
|
||||
tm.content = in[i].content; // 同上
|
||||
// ...
|
||||
messages.push_back(std::move(tm)); // push_back → bad_alloc
|
||||
}
|
||||
|
||||
// ▼ count_tokens_trim_vec 内对每个元素调用 count_tokens_trim,
|
||||
// 后者访问 std::string::operator[] → 不抛异常(已 bounds-checked)
|
||||
// 但 vector 迭代器可能被异常干扰
|
||||
size_t current = count_tokens_trim_vec(messages);
|
||||
|
||||
// ... 更多 std::vector 操作 ...
|
||||
|
||||
return 0; // ← 若以上任何操作抛异常,永远不会到达这里
|
||||
// 异常沿 C 函数指针返回 host,触发 std::terminate()
|
||||
}
|
||||
|
||||
// vtable 绑定——通过 C 函数指针直接暴露
|
||||
static int context_trim(const dstalk_message_t* in, int in_count,
|
||||
dstalk_message_t** out, int* out_count,
|
||||
size_t max_tokens) {
|
||||
return trim_impl(in, in_count, out, out_count, max_tokens);
|
||||
}
|
||||
```
|
||||
|
||||
**违反代价**: OOM 或任何 STL 异常发生时,进程直接 `std::terminate()`,无任何恢复机会。
|
||||
|
||||
### 8.5 正例:异常安全的 service 函数
|
||||
|
||||
```c
|
||||
// 正例:try/catch 包裹所有 C++ 操作,异常转换为错误码
|
||||
static int trim_impl_safe(const dstalk_message_t* in, int in_count,
|
||||
dstalk_message_t** out, int* out_count,
|
||||
size_t max_tokens) {
|
||||
try {
|
||||
if (!in || in_count <= 0 || !out || !out_count) return -1;
|
||||
|
||||
std::vector<TrimMessage> messages;
|
||||
messages.reserve(in_count);
|
||||
for (int i = 0; i < in_count; ++i) {
|
||||
TrimMessage tm;
|
||||
if (in[i].role) tm.role = in[i].role;
|
||||
if (in[i].content) tm.content = in[i].content;
|
||||
if (in[i].tool_call_id) tm.tool_call_id = in[i].tool_call_id;
|
||||
if (in[i].tool_calls_json) tm.tool_calls_json = in[i].tool_calls_json;
|
||||
messages.push_back(std::move(tm));
|
||||
}
|
||||
|
||||
// ... 裁剪逻辑 ...
|
||||
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
const dstalk_host_api_t* host = g_host; // 原子读或以其他安全方式获取
|
||||
if (host) {
|
||||
host->log(DSTALK_LOG_ERROR, "trim_impl: %s", e.what());
|
||||
}
|
||||
return -1;
|
||||
} catch (...) {
|
||||
if (g_host) {
|
||||
g_host->log(DSTALK_LOG_ERROR, "trim_impl: unknown exception");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- `try/catch` 覆盖整个函数体——所有 STL 操作均在保护范围内
|
||||
- 两个 `catch` 子句:先捕获 `std::exception`(可取 `what()` 消息),再兜底 `...`
|
||||
- 日志中标注函数名,便于问题定位
|
||||
- 返回值转换为 `-1`,调用方可安全处理错误
|
||||
|
||||
---
|
||||
|
||||
## 9. 字符串返回值生命周期
|
||||
|
||||
> **强制规则**: 通过 ABI 返回的 `const char*` 必须满足以下二选一,禁止任何其他模式。
|
||||
|
||||
### 9.1 问题根源
|
||||
|
||||
C ABI 只能传递裸指针 `const char*`。指针指向的内存在何时释放、由谁释放,没有
|
||||
类型系统保障。错误的生命周期管理导致两大类 bug:
|
||||
|
||||
1. **悬垂指针 (use-after-free)**: 指针指向的 `std::string` 内部 buffer 在锁释放后
|
||||
被并发 `set()` 的 realloc 回收,或函数返回后栈帧销毁。
|
||||
2. **跨堆释放 (heap corruption)**: 调用方尝试 `free()` 插件 CRT 堆分配的指针,
|
||||
或反之。
|
||||
|
||||
§2 已覆盖内存所有权归属;§3 已覆盖跨 DLL 堆纪律。本节是对字符串返回值**生命周期**的
|
||||
专项强制条款,与 §2、§3 构成完整的指针契约体系。
|
||||
|
||||
### 9.2 规则:两种合法模式
|
||||
|
||||
#### 模式 A:拥有权转移(推荐)
|
||||
|
||||
**约定**: 返回的 `char*` 由函数分配(通过 `host->strdup` 或 `host->alloc`),
|
||||
**调用方**负责用 `host->free` 释放。
|
||||
|
||||
```c
|
||||
// 返回由 host 堆分配、调用方负责释放的字符串
|
||||
static char* service_get_owned_value(const char* key) {
|
||||
const char* raw = g_host->config_get(key);
|
||||
if (!raw) return nullptr;
|
||||
return g_host->strdup(raw); // 在 host 堆上复制,调用方 host->free 释放
|
||||
}
|
||||
```
|
||||
|
||||
**适用场景**: 需要将数据传出函数作用域、调用方需持有的情况。service vtable 函数返回
|
||||
动态内容时应使用此模式。
|
||||
|
||||
**文档要求**: 函数注释必须明确声明 "caller must free with `host->free`" 或等效说明。
|
||||
|
||||
#### 模式 B:静态 / 全局生命周期
|
||||
|
||||
**约定**: 返回的 `const char*` 指向编译期确定的内存,调用方**不得**释放。
|
||||
|
||||
允许的来源:
|
||||
- 字符串字面量: `return "hello";`
|
||||
- `static` 字符串数组: `static const char buf[] = "value"; return buf;`
|
||||
- 全局变量的 `c_str()`: **仅当**文档明确约束 "下次调用同函数前有效" 且无并发写入
|
||||
|
||||
```c
|
||||
// OK: 字符串字面量——程序生命周期内有效
|
||||
static const char* plugin_get_name(void) {
|
||||
return "context";
|
||||
}
|
||||
|
||||
// OK: static buffer——程序生命周期内有效
|
||||
static const char* plugin_get_version(void) {
|
||||
static const char version[] = "1.0.0";
|
||||
return version;
|
||||
}
|
||||
```
|
||||
|
||||
**不推荐的模式**: `static thread_local std::string` 虽然技术上满足 "下次调用前有效",
|
||||
但语义晦涩,调用方容易误用。仅在性能热点且调用方可证明不会跨调用持有指针时使用。
|
||||
|
||||
### 9.3 反例:锁外返回 `c_str()` (悬垂指针)
|
||||
|
||||
以下代码来自 `config_store.cpp:66-73` 和 `config_plugin.cpp:72-78`。
|
||||
`get()` 在锁内获取 `std::string::c_str()`,锁释放后返回:
|
||||
|
||||
```c
|
||||
// 反例:锁释放后 c_str() 指针悬垂
|
||||
const char* ConfigStore::get(const char* key) const {
|
||||
if (!key) return nullptr;
|
||||
std::lock_guard<std::mutex> lock(mutex_); // 获取锁
|
||||
auto it = data_.find(key);
|
||||
if (it == data_.end()) return nullptr;
|
||||
return it->second.c_str(); // 获取内部指针
|
||||
} // ← lock 在此释放。指针仍指向 map 内 string 的 buffer。
|
||||
|
||||
// 并发场景:
|
||||
// T1: const char* v = store.get("api_key"); // 得到指针 p → "sk-abc123"
|
||||
// T2: store.set("api_key", "sk-very-long-new-key-..."); // realloc! p 悬垂
|
||||
// T1: printf("%s\n", v); // ← 可能读取已释放内存、乱码或 crash
|
||||
```
|
||||
|
||||
**问题分析**:
|
||||
1. `std::string` 内部 buffer 由 `std::unordered_map` 的值对象拥有。
|
||||
2. 并发 `set()` 同一 key 触发 `operator=` → 可能 realloc → 旧 buffer 被释放。
|
||||
3. 外部持有的 `c_str()` 指针已悬垂,访问即未定义行为。
|
||||
|
||||
W11.2 审计已确认此问题存在于两个独立 ConfigStore 实现中
|
||||
(`config_store.cpp:72` 和 `config_plugin.cpp:77`)。
|
||||
|
||||
### 9.4 正例:用 `host->strdup` 返回安全副本
|
||||
|
||||
```c
|
||||
// 正例:在锁内用 host->strdup 复制,调用方负责释放
|
||||
static char* config_get_safe(const char* key) {
|
||||
if (!key) return nullptr;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = data_.find(key);
|
||||
if (it == data_.end()) return nullptr;
|
||||
|
||||
// ★ 在锁内完成复制——指针从"内部 buffer"变为"独立分配的内存"
|
||||
return g_host->strdup(it->second.c_str());
|
||||
// 调用方用 host->free(ptr) 释放
|
||||
}
|
||||
```
|
||||
|
||||
**为什么这是安全的**:
|
||||
1. `host->strdup` 调回 host CRT 堆,分配和未来的释放都在同一堆(符 §3)。
|
||||
2. 锁内完成复制——即使 T2 随后 `set()` 同一 key,新分配的内存与旧 buffer 无关。
|
||||
3. 调用方拥有返回指针的所有权——不再依赖 map 内部状态。
|
||||
|
||||
**权衡**: 每次 `get()` 多一次堆分配。对于高频调用的 key(如配置读取),调用方应在
|
||||
获取后立即消费并释放;对于需长期持有的指针,这是唯一安全的方式。
|
||||
|
||||
### 9.5 违规代价
|
||||
|
||||
| 违规模式 | 后果 |
|
||||
|----------|------|
|
||||
| 返回锁外 `c_str()` | 并发 set 后悬垂指针 → crash 或静默数据损坏 |
|
||||
| 返回栈上 buffer 指针 | 函数返回后栈帧销毁 → use-after-free |
|
||||
| 返回 `std::string{...}.c_str()` | 临时对象在语句结束时析构 → 立即悬垂 |
|
||||
| 调用方 `free()` 模式 B 的指针 | 跨堆释放 → heap corruption / crash |
|
||||
| 调用方不释放模式 A 的指针 | 内存泄漏 |
|
||||
|
||||
---
|
||||
|
||||
## 变更历史
|
||||
|
||||
| 日期 | 版本 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-05-27 | 1.1 | W12.6 追加 §8 异常安全 (涵盖 service vtable 函数) 和 §9 字符串返回值生命周期;§2.3 / §5.3 / §6.4 添加交叉引用 |
|
||||
| 2026-05-27 | 1.0 | 初始版本。W9.4 交付。基于 DSTALK_API_VERSION=1 的当前实现。 |
|
||||
|
||||
@@ -6,6 +6,10 @@ add_executable(dstalk-cli
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
set_target_properties(dstalk-cli PROPERTIES
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
|
||||
)
|
||||
|
||||
target_link_libraries(dstalk-cli
|
||||
PRIVATE dstalk
|
||||
)
|
||||
|
||||
@@ -166,8 +166,12 @@ static void handle_command(const char* line)
|
||||
|
||||
// /clear
|
||||
if (std::strcmp(line, "/clear") == 0) {
|
||||
if (g_session) g_session->clear();
|
||||
if (g_session) {
|
||||
g_session->clear();
|
||||
std::printf(CLR_GREEN "[OK] 会话已清空\n" CLR_RESET);
|
||||
} else {
|
||||
std::fprintf(stderr, CLR_RED "[ERROR] session service not available\n" CLR_RESET);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -180,6 +184,8 @@ static void handle_command(const char* line)
|
||||
std::printf(CLR_DIM "消息条数: " CLR_RESET "%d | "
|
||||
CLR_DIM "Token 估算: " CLR_RESET "%d\n",
|
||||
count, tokens);
|
||||
} else {
|
||||
std::fprintf(stderr, CLR_RED "[ERROR] context service not available\n" CLR_RESET);
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -226,37 +232,57 @@ static void handle_command(const char* line)
|
||||
return;
|
||||
}
|
||||
|
||||
// /file list [path]
|
||||
if (std::strcmp(line, "/file list") == 0 || std::strncmp(line, "/file list ", 11) == 0) {
|
||||
const char* path = line + 10;
|
||||
list_files(path);
|
||||
return;
|
||||
}
|
||||
|
||||
// /file show <path>
|
||||
if (std::strncmp(line, "/file show ", 11) == 0) {
|
||||
print_file(line + 11);
|
||||
return;
|
||||
}
|
||||
|
||||
// /file read <path>
|
||||
if (std::strncmp(line, "/file read ", 11) == 0) {
|
||||
print_file(line + 11);
|
||||
return;
|
||||
}
|
||||
|
||||
// /file write <path> <content...>
|
||||
if (std::strncmp(line, "/file write ", 12) == 0) {
|
||||
const char* rest = line + 12;
|
||||
// /file <subcommand> [args...] —— 统一入口,避免 strncmp 空格匹配遗漏
|
||||
if (std::strncmp(line, "/file", 5) == 0) {
|
||||
const char* rest = line + 5;
|
||||
while (*rest == ' ') rest++;
|
||||
const char* space = std::strchr(rest, ' ');
|
||||
if (!space) {
|
||||
|
||||
const char* sub_end = rest;
|
||||
while (*sub_end != ' ' && *sub_end != '\0') sub_end++;
|
||||
size_t sub_len = sub_end - rest;
|
||||
if (sub_len == 0) {
|
||||
std::printf(CLR_RED "[ERROR] 用法: /file <list|show|read|write> ...\n" CLR_RESET);
|
||||
return;
|
||||
}
|
||||
|
||||
const char* args = sub_end;
|
||||
while (*args == ' ') args++;
|
||||
|
||||
// /file list [path]
|
||||
if (sub_len == 4 && std::strncmp(rest, "list", 4) == 0) {
|
||||
list_files(args);
|
||||
return;
|
||||
}
|
||||
|
||||
// /file show <path> | /file read <path>
|
||||
if ((sub_len == 4 && std::strncmp(rest, "show", 4) == 0) ||
|
||||
(sub_len == 4 && std::strncmp(rest, "read", 4) == 0)) {
|
||||
while (*args == ' ') args++;
|
||||
if (*args == '\0') {
|
||||
std::printf(CLR_RED "[ERROR] 用法: /file %.*s <path>\n" CLR_RESET,
|
||||
static_cast<int>(sub_len), rest);
|
||||
return;
|
||||
}
|
||||
print_file(args);
|
||||
return;
|
||||
}
|
||||
|
||||
// /file write <path> <content>
|
||||
if (sub_len == 5 && std::strncmp(rest, "write", 5) == 0) {
|
||||
while (*args == ' ') args++;
|
||||
if (*args == '\0') {
|
||||
std::printf(CLR_RED "[ERROR] 用法: /file write <path> <content>\n" CLR_RESET);
|
||||
return;
|
||||
}
|
||||
std::string path(rest, space - rest);
|
||||
const char* content = space + 1;
|
||||
const char* path_end = args;
|
||||
while (*path_end != ' ' && *path_end != '\0') path_end++;
|
||||
std::string path(args, path_end - args);
|
||||
const char* content = path_end;
|
||||
while (*content == ' ') content++;
|
||||
if (*content == '\0') {
|
||||
std::printf(CLR_RED "[ERROR] 用法: /file write <path> <content>\n" CLR_RESET);
|
||||
return;
|
||||
}
|
||||
if (g_file_io && g_file_io->write(path.c_str(), content) == 0) {
|
||||
std::printf(CLR_GREEN "[OK] 已写入: %s\n" CLR_RESET, path.c_str());
|
||||
} else {
|
||||
@@ -265,6 +291,11 @@ static void handle_command(const char* line)
|
||||
return;
|
||||
}
|
||||
|
||||
std::printf(CLR_RED "[ERROR] 未知 /file 子命令: %.*s (可用: list, show, read, write)\n" CLR_RESET,
|
||||
static_cast<int>(sub_len), rest);
|
||||
return;
|
||||
}
|
||||
|
||||
// /history [N]
|
||||
if (std::strcmp(line, "/history") == 0 || std::strncmp(line, "/history ", 9) == 0) {
|
||||
if (!g_session) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "config_store.hpp"
|
||||
#include "../../plugins/config/include/toml_parse.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
@@ -18,47 +19,11 @@ int ConfigStore::load_file(const char* path)
|
||||
ss << file.rdbuf();
|
||||
std::string data = ss.str();
|
||||
|
||||
// 简易 TOML 解析:只处理 [section] 和 key = "value"
|
||||
std::string current_section;
|
||||
size_t pos = 0;
|
||||
while (pos < data.size()) {
|
||||
while (pos < data.size() && (data[pos] == ' ' || data[pos] == '\t'))
|
||||
pos++;
|
||||
if (pos >= data.size()) break;
|
||||
|
||||
size_t nl = data.find('\n', pos);
|
||||
std::string line = (nl != std::string::npos)
|
||||
? data.substr(pos, nl - pos) : data.substr(pos);
|
||||
pos = (nl != std::string::npos) ? nl + 1 : data.size();
|
||||
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
|
||||
line.pop_back();
|
||||
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
if (line[0] == '[' && line.back() == ']') {
|
||||
current_section = line.substr(1, line.size() - 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
|
||||
std::string key = line.substr(0, eq);
|
||||
while (!key.empty() && key.back() == ' ') key.pop_back();
|
||||
if (key.empty()) continue;
|
||||
|
||||
std::string val = line.substr(eq + 1);
|
||||
while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
|
||||
val.erase(0, 1);
|
||||
if (val.size() >= 2 && val.front() == '"' && val.back() == '"')
|
||||
val = val.substr(1, val.size() - 2);
|
||||
|
||||
// W12.2: Use shared TOML parser (de-duplicated from config_plugin.cpp)
|
||||
toml::parse(data, [this](const std::string& key, const std::string& value) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::string full_key = current_section.empty()
|
||||
? key : current_section + "." + key;
|
||||
data_[full_key] = val;
|
||||
}
|
||||
data_[key] = value;
|
||||
});
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -69,7 +34,22 @@ const char* ConfigStore::get(const char* key) const
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = data_.find(key);
|
||||
if (it == data_.end()) return nullptr;
|
||||
return it->second.c_str();
|
||||
|
||||
// W12.2: Copy to thread-local buffer before releasing lock.
|
||||
// Prevents c_str() dangling when concurrent set() on the same key
|
||||
// triggers std::string reallocation (W11.2 audit Finding 3).
|
||||
thread_local std::string tls_cached;
|
||||
tls_cached = it->second;
|
||||
return tls_cached.c_str();
|
||||
}
|
||||
|
||||
std::string ConfigStore::get_copy(const char* key) const
|
||||
{
|
||||
if (!key) return {};
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = data_.find(key);
|
||||
if (it == data_.end()) return {};
|
||||
return it->second; // copy-constructed under lock, always safe
|
||||
}
|
||||
|
||||
int ConfigStore::set(const char* key, const char* value)
|
||||
|
||||
@@ -11,13 +11,22 @@ public:
|
||||
ConfigStore() = default;
|
||||
~ConfigStore() = default;
|
||||
|
||||
// 从 TOML 文件加载配置
|
||||
// Load key-value pairs from a TOML file.
|
||||
// Returns 0 on success, -1 if file not found or path is null.
|
||||
int load_file(const char* path);
|
||||
|
||||
// 获取配置值(返回内部指针,线程安全)
|
||||
// Get config value (returns internal pointer, thread-safe).
|
||||
// W12.2: Returned pointer is now backed by a thread-local copy;
|
||||
// safe against concurrent set() on the same key from other threads.
|
||||
// Caller should still consume immediately — next get() on same
|
||||
// thread will overwrite the buffer.
|
||||
const char* get(const char* key) const;
|
||||
|
||||
// 设置配置值
|
||||
// Get a safe by-value copy of a config entry (no dangling risk).
|
||||
// Returns empty string if key not found.
|
||||
std::string get_copy(const char* key) const;
|
||||
|
||||
// Set config value. Returns 0 on success, -1 on null arguments.
|
||||
int set(const char* key, const char* value);
|
||||
|
||||
private:
|
||||
|
||||
67
plugins/config/include/toml_parse.h
Normal file
67
plugins/config/include/toml_parse.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
// Shared TOML parser — used by both ConfigStore (core) and config plugin.
|
||||
// W12.2: Extracted from config_store.cpp:23-61 and config_plugin.cpp:28-66
|
||||
// to eliminate the 74-line code duplication (W11.2 audit Finding 1).
|
||||
// Does NOT support: inline tables, arrays, multi-line strings, escape sequences.
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace dstalk {
|
||||
namespace toml {
|
||||
|
||||
/// Parse a TOML string, calling on_kv(full_key, value) for each key-value pair.
|
||||
/// Supports [section] headers, key = "value" pairs, # comments, blank lines.
|
||||
template<typename F>
|
||||
inline void parse(const std::string& content, F&& on_kv)
|
||||
{
|
||||
std::string current_section;
|
||||
size_t pos = 0;
|
||||
|
||||
while (pos < content.size()) {
|
||||
// Trim left whitespace
|
||||
while (pos < content.size() && (content[pos] == ' ' || content[pos] == '\t'))
|
||||
pos++;
|
||||
if (pos >= content.size()) break;
|
||||
|
||||
// Extract next line
|
||||
size_t nl = content.find('\n', pos);
|
||||
std::string line = (nl != std::string::npos)
|
||||
? content.substr(pos, nl - pos) : content.substr(pos);
|
||||
pos = (nl != std::string::npos) ? nl + 1 : content.size();
|
||||
|
||||
// Trim right whitespace (including \r)
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
|
||||
line.pop_back();
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
// Section header: [section_name]
|
||||
if (line[0] == '[' && line.back() == ']') {
|
||||
current_section = line.substr(1, line.size() - 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Key = value
|
||||
size_t eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
|
||||
std::string key = line.substr(0, eq);
|
||||
while (!key.empty() && key.back() == ' ') key.pop_back();
|
||||
if (key.empty()) continue;
|
||||
|
||||
std::string val = line.substr(eq + 1);
|
||||
while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
|
||||
val.erase(0, 1);
|
||||
if (val.size() >= 2 && val.front() == '"' && val.back() == '"')
|
||||
val = val.substr(1, val.size() - 2);
|
||||
|
||||
std::string full_key = current_section.empty()
|
||||
? key : current_section + "." + key;
|
||||
|
||||
on_kv(full_key, val);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace toml
|
||||
} // namespace dstalk
|
||||
@@ -1,22 +1,37 @@
|
||||
#include "dstalk/dstalk_host.h"
|
||||
#include "dstalk/dstalk_services.h"
|
||||
#include "../include/toml_parse.h"
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstdio>
|
||||
|
||||
// ============================================================
|
||||
// ConfigStore - independent TOML key-value store
|
||||
// Global state
|
||||
// ============================================================
|
||||
namespace {
|
||||
static const dstalk_host_api_t* g_host = nullptr;
|
||||
|
||||
class ConfigStore {
|
||||
public:
|
||||
int load_file(const char* path) {
|
||||
if (!path) return -1;
|
||||
// ============================================================
|
||||
// Service implementations
|
||||
//
|
||||
// W12.2: Eliminated private ConfigStore (was 90 lines duplicating core).
|
||||
// All get/set/load_file now delegate to the host store via g_host->config_get
|
||||
// and g_host->config_set, making the host store the single source of truth.
|
||||
// TOML parsing uses the shared dstalk::toml::parse() from toml_parse.h.
|
||||
// ============================================================
|
||||
|
||||
static const char* config_get(const char* key) {
|
||||
if (!g_host) return nullptr;
|
||||
return g_host->config_get(key);
|
||||
}
|
||||
|
||||
static int config_set(const char* key, const char* value) {
|
||||
if (!g_host) return -1;
|
||||
return g_host->config_set(key, value);
|
||||
}
|
||||
|
||||
static int config_load_file(const char* path) {
|
||||
if (!g_host || !path) return -1;
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) return -1;
|
||||
@@ -25,91 +40,15 @@ public:
|
||||
ss << file.rdbuf();
|
||||
std::string data = ss.str();
|
||||
|
||||
std::string current_section;
|
||||
size_t pos = 0;
|
||||
while (pos < data.size()) {
|
||||
while (pos < data.size() && (data[pos] == ' ' || data[pos] == '\t'))
|
||||
pos++;
|
||||
if (pos >= data.size()) break;
|
||||
|
||||
size_t nl = data.find('\n', pos);
|
||||
std::string line = (nl != std::string::npos)
|
||||
? data.substr(pos, nl - pos) : data.substr(pos);
|
||||
pos = (nl != std::string::npos) ? nl + 1 : data.size();
|
||||
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
|
||||
line.pop_back();
|
||||
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
if (line[0] == '[' && line.back() == ']') {
|
||||
current_section = line.substr(1, line.size() - 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
|
||||
std::string key = line.substr(0, eq);
|
||||
while (!key.empty() && key.back() == ' ') key.pop_back();
|
||||
if (key.empty()) continue;
|
||||
|
||||
std::string val = line.substr(eq + 1);
|
||||
while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
|
||||
val.erase(0, 1);
|
||||
if (val.size() >= 2 && val.front() == '"' && val.back() == '"')
|
||||
val = val.substr(1, val.size() - 2);
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::string full_key = current_section.empty()
|
||||
? key : current_section + "." + key;
|
||||
data_[full_key] = val;
|
||||
}
|
||||
int count = 0;
|
||||
dstalk::toml::parse(data, [&](const std::string& key, const std::string& value) {
|
||||
g_host->config_set(key.c_str(), value.c_str());
|
||||
++count;
|
||||
});
|
||||
|
||||
g_host->log(DSTALK_LOG_INFO,
|
||||
"config: loaded %d entries from %s into host store", count, path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* get(const char* key) const {
|
||||
if (!key) return nullptr;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = data_.find(key);
|
||||
if (it == data_.end()) return nullptr;
|
||||
return it->second.c_str();
|
||||
}
|
||||
|
||||
int set(const char* key, const char* value) {
|
||||
if (!key || !value) return -1;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
data_[key] = value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex mutex_;
|
||||
std::unordered_map<std::string, std::string> data_;
|
||||
};
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ============================================================
|
||||
// Global state
|
||||
// ============================================================
|
||||
static const dstalk_host_api_t* g_host = nullptr;
|
||||
static ConfigStore g_config;
|
||||
|
||||
// ============================================================
|
||||
// Service implementations
|
||||
// ============================================================
|
||||
static const char* config_get(const char* key) {
|
||||
return g_config.get(key);
|
||||
}
|
||||
|
||||
static int config_set(const char* key, const char* value) {
|
||||
return g_config.set(key, value);
|
||||
}
|
||||
|
||||
static int config_load_file(const char* path) {
|
||||
return g_config.load_file(path);
|
||||
}
|
||||
|
||||
static dstalk_config_service_t g_service = {
|
||||
@@ -123,17 +62,28 @@ static dstalk_config_service_t g_service = {
|
||||
// ============================================================
|
||||
static int on_init(const dstalk_host_api_t* host) {
|
||||
g_host = host;
|
||||
return host->register_service("config", 1, &g_service);
|
||||
|
||||
// W12.2: This service is now a thin wrapper around host->config_get/set.
|
||||
// Direct host API calls are preferred.
|
||||
host->log(DSTALK_LOG_INFO,
|
||||
"plugin config service is deprecated, prefer host->config_get/set");
|
||||
|
||||
int rc = host->register_service("config", 1, &g_service);
|
||||
if (rc != 0) {
|
||||
host->log(DSTALK_LOG_WARN,
|
||||
"config: register_service failed (rc=%d), service name may conflict", rc);
|
||||
}
|
||||
return (rc >= 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
static void on_shutdown() {
|
||||
// nothing to clean up
|
||||
// W12.2: No local store to clean up — all data lives in host store.
|
||||
}
|
||||
|
||||
static dstalk_plugin_info_t g_info = {
|
||||
"config", // name
|
||||
"1.0.0", // version
|
||||
"Configuration service with TOML file support", // description
|
||||
"Configuration service with TOML file support (deprecated: use host->config_get/set)",
|
||||
DSTALK_API_VERSION, // api_version
|
||||
{nullptr}, // dependencies (none)
|
||||
on_init, // on_init
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -111,11 +112,52 @@ static size_t count_tokens_trim_vec(const std::vector<TrimMessage>& msgs) {
|
||||
return total;
|
||||
}
|
||||
|
||||
// 释放单条消息中所有已分配的字符串字段(用于 OOM 回滚)
|
||||
static void free_msg_strs(dstalk_message_t* msg) {
|
||||
if (msg->role) { g_host->free((void*)msg->role); msg->role = nullptr; }
|
||||
if (msg->content) { g_host->free((void*)msg->content); msg->content = nullptr; }
|
||||
if (msg->tool_call_id) { g_host->free((void*)msg->tool_call_id); msg->tool_call_id = nullptr; }
|
||||
if (msg->tool_calls_json) { g_host->free((void*)msg->tool_calls_json); msg->tool_calls_json = nullptr; }
|
||||
}
|
||||
|
||||
// 将 TrimMessage 的字符串字段通过 g_host->strdup 复制到 dstalk_message_t。
|
||||
// 成功返回 0;OOM 时释放当前消息已分配字段并返回 -1。
|
||||
static int strdup_message_fields(dstalk_message_t* dst, const TrimMessage& src) {
|
||||
memset(dst, 0, sizeof(dstalk_message_t));
|
||||
|
||||
if (!src.role.empty()) {
|
||||
dst->role = g_host->strdup(src.role.c_str());
|
||||
if (!dst->role) goto oom;
|
||||
}
|
||||
if (!src.content.empty()) {
|
||||
dst->content = g_host->strdup(src.content.c_str());
|
||||
if (!dst->content) goto oom;
|
||||
}
|
||||
if (!src.tool_call_id.empty()) {
|
||||
dst->tool_call_id = g_host->strdup(src.tool_call_id.c_str());
|
||||
if (!dst->tool_call_id) goto oom;
|
||||
}
|
||||
if (!src.tool_calls_json.empty()) {
|
||||
dst->tool_calls_json = g_host->strdup(src.tool_calls_json.c_str());
|
||||
if (!dst->tool_calls_json) goto oom;
|
||||
}
|
||||
return 0;
|
||||
|
||||
oom:
|
||||
free_msg_strs(dst);
|
||||
return -1;
|
||||
}
|
||||
|
||||
// W12.1 修复:trim_impl 包裹 try/catch 防止 C++ 异常穿越 ABI 边界 (§5.3)
|
||||
static int trim_impl(const dstalk_message_t* in, int in_count,
|
||||
dstalk_message_t** out, int* out_count,
|
||||
size_t max_tokens) {
|
||||
try {
|
||||
if (!in || in_count <= 0 || !out || !out_count) return -1;
|
||||
|
||||
// W12.1: 调用方传 0 时使用 g_max_tokens 作为默认限制
|
||||
if (max_tokens == 0) max_tokens = g_max_tokens;
|
||||
|
||||
// 将 C 数组转换为内部 vector
|
||||
std::vector<TrimMessage> messages;
|
||||
messages.reserve(in_count);
|
||||
@@ -134,11 +176,14 @@ static int trim_impl(const dstalk_message_t* in, int in_count,
|
||||
*out_count = in_count;
|
||||
*out = static_cast<dstalk_message_t*>(g_host->alloc(sizeof(dstalk_message_t) * in_count));
|
||||
if (!*out) return -1;
|
||||
// W12.1: strdup 返回值逐一检查,OOM 时回滚已分配消息
|
||||
for (int i = 0; i < in_count; ++i) {
|
||||
(*out)[i].role = messages[i].role.empty() ? nullptr : g_host->strdup(messages[i].role.c_str());
|
||||
(*out)[i].content = messages[i].content.empty() ? nullptr : g_host->strdup(messages[i].content.c_str());
|
||||
(*out)[i].tool_call_id = messages[i].tool_call_id.empty() ? nullptr : g_host->strdup(messages[i].tool_call_id.c_str());
|
||||
(*out)[i].tool_calls_json = messages[i].tool_calls_json.empty() ? nullptr : g_host->strdup(messages[i].tool_calls_json.c_str());
|
||||
if (strdup_message_fields(&(*out)[i], messages[i]) != 0) {
|
||||
for (int j = 0; j < i; ++j) free_msg_strs(&(*out)[j]);
|
||||
g_host->free(*out);
|
||||
*out = nullptr;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -204,6 +249,15 @@ static int trim_impl(const dstalk_message_t* in, int in_count,
|
||||
}
|
||||
}
|
||||
|
||||
// W12.1: 消息数量上限粗略估算(每消息 ~100 token),利用 g_max_tokens 防止消息泛滥
|
||||
{
|
||||
size_t max_msg_count = (g_max_tokens + 99) / 100; // ceil(g_max_tokens / 100)
|
||||
if (max_msg_count < 1) max_msg_count = 1;
|
||||
while (non_system_msgs.size() > max_msg_count) {
|
||||
non_system_msgs.erase(non_system_msgs.begin());
|
||||
}
|
||||
}
|
||||
|
||||
// 组装结果
|
||||
std::vector<TrimMessage> result;
|
||||
result.reserve(system_msgs.size() + non_system_msgs.size());
|
||||
@@ -215,29 +269,50 @@ static int trim_impl(const dstalk_message_t* in, int in_count,
|
||||
*out = static_cast<dstalk_message_t*>(g_host->alloc(sizeof(dstalk_message_t) * result_count));
|
||||
if (!*out) return -1;
|
||||
|
||||
// W12.1: strdup 返回值逐一检查,OOM 时回滚已分配消息
|
||||
for (int i = 0; i < result_count; ++i) {
|
||||
(*out)[i].role = result[i].role.empty() ? nullptr : g_host->strdup(result[i].role.c_str());
|
||||
(*out)[i].content = result[i].content.empty() ? nullptr : g_host->strdup(result[i].content.c_str());
|
||||
(*out)[i].tool_call_id = result[i].tool_call_id.empty() ? nullptr : g_host->strdup(result[i].tool_call_id.c_str());
|
||||
(*out)[i].tool_calls_json = result[i].tool_calls_json.empty() ? nullptr : g_host->strdup(result[i].tool_calls_json.c_str());
|
||||
if (strdup_message_fields(&(*out)[i], result[i]) != 0) {
|
||||
for (int j = 0; j < i; ++j) free_msg_strs(&(*out)[j]);
|
||||
g_host->free(*out);
|
||||
*out = nullptr;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
// W12.1: 防止 std::bad_alloc 等 C++ 异常穿越 C ABI 边界 → std::terminate()
|
||||
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[context] trim_impl exception: %s", e.what());
|
||||
return -1;
|
||||
} catch (...) {
|
||||
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[context] trim_impl unknown exception");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Context 服务 vtable 实现
|
||||
// ============================================================
|
||||
|
||||
// W12.1: 包裹 try/catch 防止异常穿越 C ABI 边界 → std::terminate()
|
||||
static size_t context_count_tokens(const dstalk_message_t* msgs, int count) {
|
||||
try {
|
||||
if (!msgs || count <= 0) return 0;
|
||||
return count_tokens_all(msgs, count);
|
||||
} catch (...) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// W12.1: 包裹 try/catch 防止异常穿越 C ABI 边界
|
||||
static int context_trim(const dstalk_message_t* in, int in_count,
|
||||
dstalk_message_t** out, int* out_count,
|
||||
size_t max_tokens) {
|
||||
try {
|
||||
return trim_impl(in, in_count, out, out_count, max_tokens);
|
||||
} catch (...) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static void context_set_max_tokens(size_t max) {
|
||||
@@ -254,7 +329,9 @@ static dstalk_context_service_t g_context_service = {
|
||||
// 插件生命周期
|
||||
// ============================================================
|
||||
|
||||
// W12.1: 包裹 try/catch 防止异常穿越 C ABI 边界
|
||||
static int on_init(const dstalk_host_api_t* host) {
|
||||
try {
|
||||
g_host = host;
|
||||
|
||||
// 查询依赖服务: session
|
||||
@@ -266,6 +343,13 @@ static int on_init(const dstalk_host_api_t* host) {
|
||||
g_session = static_cast<const dstalk_session_service_t*>(raw);
|
||||
|
||||
return host->register_service("context", 1, &g_context_service);
|
||||
} catch (const std::exception& e) {
|
||||
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[plugin-context] on_init exception: %s", e.what());
|
||||
return -1;
|
||||
} catch (...) {
|
||||
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[plugin-context] on_init unknown exception");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_shutdown() {
|
||||
|
||||
Reference in New Issue
Block a user