Wave 5+6: plugin ABI hardening, build modernization, ABI/security docs
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled

Wave 5 (9 parallel agents):
- W1.1 atomic diag callback + DLL handle release on shutdown (lin)
- W2.1 unify cross-DLL heap discipline (host->alloc/free/strdup) (chen)
- W2.2 secure_zero api_key on shutdown for deepseek/anthropic (cao)
- W3 CMake modernization: target-based cxx_std_20, dstalk_boost_config
  INTERFACE lib, root-level RUNTIME_OUTPUT_DIRECTORY (hu)
- W4 GitHub Actions CI with dynamic Linux/Windows matrix (ma)
- W5.1 SSE buffer_body to cut peak memory ~67% on 32K streams (zhou)
- W6.1 LSP JSON-RPC frame parser hardened against header reordering (sun)
- W7 smoke test: copy plugin DLLs post-build + Boost.JSON src.hpp fix
  for full 9-plugin load coverage (wang)
- W8.1 README slimmed 398->92, Diataxis docs/ skeleton (deng)

Wave 6 (6 parallel agents):
- W9.1 docs/explanation: architecture + plugin-lifecycle (deng)
- W9.3 log credential leak audit (0 vulns, audit trail in
  docs/explanation/security-logging.md) (cao)
- W9.4 docs/reference/plugin-abi.md - 7-point ABI contract (lin)
- W9.6 CLI /history command + status integration (zhao)
- W9.8 plugin_loader fault tolerance: per-plugin failure no longer
  aborts dstalk_init (huang)
- W9.10 host_api unit tests: tests/host_api_test.cpp, 8 cases (liu)

CEO oversight (preexisting bugs fixed during Wave 5 verification):
- lsp_plugin.cpp:449 forward decl mismatch (int vs void)
- tools_plugin.cpp:109 missing forward decl

Multi-agent collaboration framework:
- agents/WORKFLOW.md: 6-stage protocol, two-tier governance,
  prompt template, technical constraints registry

Build: cmake --build 0 error / 0 warning. Tests: 2/2 100% pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-27 05:39:10 +08:00
parent 4433218853
commit 5766938524
49 changed files with 1640 additions and 449 deletions

72
docs/README.md Normal file
View File

@@ -0,0 +1,72 @@
# dstalk 文档
> 文档采用 [Diataxis](https://diataxis.fr/) 框架组织: 教程 / 操作指南 / 解释 / 参考 四类分离。
---
## 已有文档
### 教程 (Tutorials) — 从零开始的学习路径
| 文档 | 说明 |
|------|------|
| [快速入门](tutorial/quick-start.md) | 5 步上手: 安装工具链、编译、配置、运行、第一个对话 |
### 解释 (Explanation) — 背景与设计决策
| 文档 | 说明 |
|------|------|
| [架构哲学](explanation/architecture.md) | 为什么是插件架构, host/plugin/service 三层模型, service registry 和 event bus 的职责, C ABI 的选择理由 |
| [插件生命周期](explanation/plugin-lifecycle.md) | DLL 加载与拓扑排序, on_init/on_shutdown 契约, 跨 DLL 堆分配与 ABI 纪律 |
### 参考 (Reference) — 精确的技术描述
| 文档 | 说明 |
|------|------|
| [CLI 命令速查](reference/commands.md) | 全部 CLI 命令的别名、作用与示例 |
| [Plugin ABI 契约](reference/plugin-abi.md) | 跨 DLL 通信的 C ABI 规范:内存所有权、堆纪律、回调线程安全 |
---
## 未来计划
### 教程
- [ ] **插件开发入门** (`tutorial/plugin-dev.md`) — TODO: 写一个 Hello World 插件, 注册服务并调用
- [ ] **LSP 集成教程** (`tutorial/lsp-setup.md`) — TODO: 配置 clangd 并在 dstalk 中使用诊断/补全
### 操作指南 (How-to Guides) — 解决具体问题
- [ ] **切换 AI 后端** (`how-to/switch-provider.md`) — TODO: 在 DeepSeek / Anthropic / OpenAI 间切换
- [ ] **配置 API Key** (`how-to/config-api-key.md`) — TODO: config.toml 各项配置说明与安全建议
- [ ] **使用 File 命令管理项目文件** (`how-to/file-commands.md`) — TODO: 列目录、查看、读写文件的工作流
- [ ] **自定义前端开发** (`how-to/custom-frontend.md`) — TODO: 基于 C ABI 编写自己的前端程序
- [ ] **批处理模式** (`how-to/batch-mode.md`) — TODO: `--batch` 标志与非交互式使用
### 解释 (Explanation) — 背景与设计决策
- [x] **架构哲学** (`explanation/architecture.md`) — 插件架构、三层模型、service registry/event bus、C ABI 选择
- [x] **插件生命周期** (`explanation/plugin-lifecycle.md`) — DLL 加载、拓扑排序、on_init/on_shutdown 契约、ABI 纪律
- [ ] **为什么用 C/C++** (`explanation/why-cpp.md`) — TODO: 启动速度、内存占用、运行时依赖、嵌入能力的对比分析
- [ ] **Diataxis 文档框架** (`explanation/diataxis.md`) — TODO: 本文档组织方式的设计思路
### 参考
- [ ] **API 参考** (`reference/api.md`) — TODO: dstalk_host.h 完整 API 说明与调用示例
- [ ] **配置参考** (`reference/config.md`) — TODO: config.toml 所有字段的详细说明
- [ ] **服务接口参考** (`reference/services.md`) — TODO: dstalk_services.h 中所有 vtable 接口定义
---
## 文档约定
- 命令以 `$ ` 前缀表示, 在终端中运行
- 代码块标注语言 (```c, ```toml, ```bash 等)
- [ ] 表示计划中未完成的文档
---
## 贡献文档
文档使用 Markdown 编写, 存放在 `docs/` 目录下。
欢迎通过 PR 补充或修正文档内容。

View File

@@ -0,0 +1,138 @@
# dstalk 架构哲学
> **本文属于**: Explanation -- 解释"为什么这么设计",建立心智模型。
> 不涉及具体 API 签名或配置字段,那属于 Reference 的职责。
---
## 为什么是插件架构?
dstalk 选择的不是单体架构,而是**以 C ABI 为边界的插件架构**。这是几条需求推导出的必然结果:
1. **AI 后端会变**。DeepSeek / OpenAI / Anthropic 各有不同的 HTTP 协议细节和模型参数。今天用 A明天切 B后天同时挂两个。单体应用内硬编码所有后端会导致每次新增后端都要改核心、重新编译、重新测试。
2. **能力会增长**。LSP 集成、文件管理、会话持久化、工具调用——这些能力不是 CLI 启动时必须加载的。使用者可能只需要聊天,不需要 LSP。插件架构让能力按需加载启动更快内存更省。
3. **插件作者不是核心团队**。第三方应该能用自己的编译器、自己的 C++ 标准库版本编写插件,而不必须链接 dstalk-core 的静态库。这要求 ABI 稳定。C ABI 是唯一具有跨编译器二进制兼容性的选择。
**一句话心智模型**: 不要想象一个胖二进制把所有功能静态链接在一起;想象一个内核 (host) + 一圈可插拔的服务单元 (plugin),内核只负责编排,不负责实现。
---
## 三层模型
dstalk 的架构由 3 层组成。从上到下看,每一层依赖下一层提供的抽象:
```
┌────────────────────────────────────────────┐
│ Plugin (DLL) │
│ 实现具体能力AI 聊天、文件读写、LSP... │
│ 通过服务注册表向其他插件暴露自己的功能 │
├────────────────────────────────────────────┤
│ Host (dstalk-core) │
│ 拥有事件总线、服务注册表、插件加载器、配置 │
│ 提供一个 dstalk_host_api_t 接口给所有插件 │
├────────────────────────────────────────────┤
│ Service (C vtable) │
│ 抽象接口ai / http / session / file_io... │
│ 消费者通过 query_service 查找,不看实现者是谁│
└────────────────────────────────────────────┘
```
### Host 层
Host 是 dstalk 的 **编排内核**。它自己不做 AI 推理,不读写文件,不管理会话。
Host 拥有四样东西:
- **配置存储 (ConfigStore)**: 管理 `config.toml` 的加载和键值查询。
- **事件总线 (EventBus)**: 插件间松耦合通信的唯一通道。
- **服务注册表 (ServiceRegistry)**: 按名称 + 版本号存储和查找服务 vtable。
- **插件加载器 (PluginLoader)**: 扫描 `plugins/` 目录、加载 DLL、按依赖拓扑排序后调用初始化。
Host 启动时,按严格顺序执行:
1. 分配上述四个组件。
2. 加载 `config.toml`(如果提供了路径)。
3. 扫描 `plugin_dir` 目录下所有 `.dll` / `.so` 文件。
4. 调用每个插件的 `dstalk_plugin_init()` 获取插件描述符。
5. 按依赖拓扑排序执行 `on_init`
### Plugin 层
每个插件是一个**独立的动态库**,导出一个唯一入口函数 `dstalk_plugin_init()`
该入口返回一个 `dstalk_plugin_info_t`,声明了:
- 插件名称(依赖解析时的唯一标识)。
- 依赖列表(必须先于本插件初始化的其他插件名)。
- 生命周期回调(`on_init` / `on_shutdown`)。
- 可选的事件回调(`on_event`)。
插件在 `on_init` 中做三件事:
1. 保存 `host_api` 指针,这是它此后访问一切 Host 能力的唯一通道。
2. 通过 `host_api->query_service` 查找它依赖的服务(例如 deepseek 插件查询 `http``config`)。
3. 通过 `host_api->register_service` 向注册表注册自己提供的服务 vtable。
关键设计:插件之间**不直接链接**。插件 A 不知道插件 B 是否在当前进程中。A 只对 Host 说"我需要名为 `http` 的服务"Host 从注册表里找出那个 vtable把指针交给 A。
### Service 层
Service 是**纯 C vtable**——一个包含函数指针的结构体。以 `dstalk_ai_service_t` 为例:
```
struct dstalk_ai_service_t {
int (*configure)(...);
dstalk_chat_result_t (*chat)(...);
dstalk_chat_result_t (*chat_stream)(...);
void (*free_result)(dstalk_chat_result_t*);
};
```
每种服务都有一个预定义的 vtable 结构体(定义在 `dstalk_services.h`)。第三方也可以扩展自己的服务 vtable版本号随注册一起提供允许消费者做最低版本检查。
服务注册采用 **name + version** 两要素:
- 全局唯一名称(如 `"ai.deepseek"``"http"``"file_io"`)。
- 版本号(消费者可以要求 `min_version`)。
---
## Service Registry 解决什么问题?
核心问题是 **耦合方向**。如果不使用注册表deepseek 插件就得直接知道 http 插件的符号,通过 `dlsym` 或头文件耦合。换成注册表后:
- 提供者说:"我注册一个名为 `http` 的 vtable"。
- 消费者说:"给我一个名为 `http`、版本 >= 1 的 vtable"。
- Host 做中间人查表。
**效果**: 你可以用任意方式实现 `http` 服务——用 libcurl、用 WinHTTP、用 mock——deepseek 插件一行代码都不需要改。它只依赖服务接口,不依赖服务实现。
---
## Event Bus 解决什么问题?
服务注册表解决的是**调用链**"我需要你,你给我提供服务"。但架构里还有另一类通信需求——**通知链**"某件事发生了,关心的人自行处理"。
举例:
- 会话清空了 (`DSTALK_EVENT_SESSION_CLEAR`)——所有关心会话状态的插件应当知道。
- 配置更新了 (`DSTALK_EVENT_CONFIG_CHANGED`)——读取了配置的插件应当刷新。
- 插件加载/卸载——其他插件可能需要重新查询依赖。
Event Bus 把这些需求抽象为**发布-订阅**
- 发布者不知道谁在听。
- 订阅者不知道谁在发布。
- 发布者和订阅者通过事件类型编号耦合(如 `DSTALK_EVENT_SESSION_CLEAR = 2`)。
**效果**: 新写一个监控插件,只需 `event_subscribe` 即可收到所有感兴趣的事件,不需要修改任何现有代码。
---
## 为什么所有插件用 C ABI
C++ 没有稳定的 ABI。不同编译器MSVC / GCC / Clang、不同编译器版本、不同标准库版本libstdc++ vs libc++之间C++ 类的内存布局、虚函数表布局、异常展开机制都不保证兼容。
C ABI 的保证:
- 编译单元之间传递的只有**结构体**和**函数指针**。
- 结构体布局确定(`dstalk_plugin_info_t` 的字段顺序不会变)。
- 函数调用约定确定(`extern "C"` 关闭 name mangling
- 所有内存分配通过 Host 提供的 `alloc/free`,而非各自调用 `malloc/free`(不同 DLL 使用不同运行时库的 `malloc` 会崩溃)。
**一句话心智模型**: C ABI 是唯一能保证"用 MSVC 编译的插件在 GCC 编译的 host 中运行"的接口层。代价是只能传递纯数据结构和函数指针,不能使用异常、重载或模板跨 DLL 边界。

View File

@@ -0,0 +1,167 @@
# 插件生命周期
> **本文属于**: Explanation -- 解释插件从加载到卸载的完整生命周期和关键契约。
> 不涉及具体 API 签名细节,那属于 Reference (`reference/api.md`) 的职责。
---
## 总览
一个 dstalk 插件从磁盘上的 DLL 文件到进程中活跃的服务提供者,经历了四个阶段:
```
加载 DLL → 依赖解析 → on_init → on_shutdown
(load_plugin) (topo sort) (初始化) (逆序清理)
```
---
## 阶段 1DLL 加载
Host 调用 `load_plugin(path)`,由 `PluginLoader` 执行:
1. **加载动态库**。Windows 用 `LoadLibraryA`Linux 用 `dlopen(..., RTLD_NOW | RTLD_LOCAL)`。注意 Linux 端使用了 `RTLD_LOCAL` 而非 `RTLD_GLOBAL`,这意味着每个插件的符号默认对其他插件不可见——跨插件通信必须通过 Host API不能通过直接符号引用。
2. **查找入口函数**。Host 在 DLL 中搜索名为 `dstalk_plugin_init` 的符号,调用它获取 `dstalk_plugin_info_t*`
3. **版本校验**。Host 检查 `info->api_version` 是否等于 `DSTALK_API_VERSION`。不匹配则拒绝加载——这是防止 ABI 断裂的第一道防线。
4. **存储元数据**`PluginInfo` 被存入 `plugins_` map此时 `initialized = false`,插件尚未初始化。
**关键点**: 加载阶段只做符号获取和版本校验,不调用 `on_init`。这意味着插件代码此时尚未运行,不持有任何资源。
---
## 阶段 2依赖解析拓扑排序
所有插件加载完毕后,`initialize_all` 在调用 `on_init` 之前先执行拓扑排序。
**为什么需要排序?** 如果 deepseek 插件依赖 `http``config`,那么 `http``config` 插件的 `on_init` 必须先于 deepseek 执行——否则 deepseek 在 `on_init` 中调用 `query_service("http")` 会得到 `nullptr`
**算法**Kahn 算法BFS 拓扑排序)。
1. 构建 `name → id` 的映射表。
2. 为每个插件计算入度in-degree = 被多少个其他插件依赖)。
3. 从入度为 0 的节点(无依赖或依赖已满足的插件)开始,逐层输出。
4. 最终检查:若输出节点数不等于总节点数,说明存在**循环依赖**,抛出异常阻止初始化。
依赖声明在 `dstalk_plugin_info_t.dependencies` 中,以 NULL 结尾的字符串数组。最多 `DSTALK_MAX_DEPS` (8) 个依赖。
```
// 示例deepseek 插件声明依赖 http 和 config
{ "http", "config", NULL }
```
依赖名称与目标插件的 `info->name` 字段匹配(如 `"file-io"` 不是 DLL 的文件名),因此依赖声明和插件命名必须一致。
---
## 阶段 3on_init 的契约
`on_init` 是插件获得生命信号的地方。Host 按拓扑排序的结果依次调用每个插件的 `on_init`,传入 `dstalk_host_api_t*`
### 契约条款
**契约 1host_api 在 on_init 执行期间完全可用。**
`dstalk_host_api_t` 是插件访问一切 Host 能力的唯一通道。它的所有字段(`register_service``query_service``log``alloc``free` 等)在 `on_init` 调用时已经有效。插件应当将这个指针保存为全局变量——每次跨 API 调用都需要它。
```c
static const dstalk_host_api_t* g_host = nullptr;
static int on_init(const dstalk_host_api_t* host) {
g_host = host; // 保存,此后的所有 API 调用都用它
// ...
}
```
**契约 2query_service 查找依赖。** 插件在 `on_init` 中查询它声明的每一个依赖。如果某个依赖不存在,`on_init` 应当返回非零值,告知 Host 初始化失败。
```c
g_http = (dstalk_http_service_t*)host->query_service("http", 1);
if (!g_http) return -1; // 依赖缺失,初始化失败
```
Host 收到非零返回值后,会跳过后续插件的初始化并报告警告。
**契约 3register_service 注册自己的服务。** 插件将自己的 vtable 注册到服务注册表后,其他依赖它的插件才能在后续的 `on_init` 中通过 `query_service` 找到它。
```c
return host->register_service("ai.deepseek", 1, &g_service);
```
注册表内的 vtable 是原始指针,不拷贝。因此 vtable 指向的结构体必须是**静态生命周期**(全局变量或 static 局部变量)。
**契约 4不要在 on_init 中做阻塞操作。** 当前 Host 是单线程初始化,阻塞一个插件的 `on_init` 会阻塞整个启动流程。如果需要异步初始化(如连接远程服务),在 `on_init` 中仅做最基本的 vtable 注册,把长连接放到首次服务调用时再建立。
**契约 5所有内存分配通过 host_api->alloc/free。** 见下文"ABI 纪律"节。
---
## 阶段 4on_shutdown 的契约
Host 关闭时,按拓扑排序的**逆序**调用 `on_shutdown`——这保证了被依赖者后卸载。
```
加载顺序: config → http → deepseek
卸载顺序: deepseek → http → config
```
注意:如果拓扑排序失败(如循环依赖),`shutdown_all` 会退化为任意顺序,仅保证所有插件的 `on_shutdown` 都被调用、所有 DLL 句柄都被释放。
### 契约条款
**契约 1逆序卸载释放持久的服务引用。**`on_init` 中保存的服务指针(如 `g_http`)应在 `on_shutdown` 中置为 `nullptr`。这防止插件在卸载后仍持有悬垂指针——虽然当前实现是在 `on_shutdown` 之后才释放 DLL但防御性置空是好习惯。
```c
static void on_shutdown() {
g_http = nullptr;
g_config = nullptr;
g_host = nullptr;
}
```
**契约 2不能跨 DLL 堆边界释放。** 在插件 A 的 `on_shutdown` 中,如果还持有插件 B 分配的内存,不能简单地调用 `g_host->free`——这会触发跨堆释放的未定义行为。正确做法是调用提供方的专属释放函数(如 AI 服务的 `free_result`),或让提供方在 `on_shutdown` 中清理自己分配的资源。
**契约 3删除已注册的服务。** 当前 `ServiceRegistry` 不自动清理。如果一个服务 remove 了对应的插件,应该在卸载期间调用 `unregister_service`。当前实现未强制这一点,但关闭过程会销毁整个 `ServiceRegistry`,所以注销是可选的(若不注销,重启 host 时不会残留)。
---
## ABI 纪律:为什么 Host 提供 alloc / free
这是插件架构中最容易被忽视但最容易出错的问题。
**问题**:在 Windows 上,每个 DLL 可能链接不同的 CRTC 运行时库。DLL A 用 MSVC 2022 的 `malloc` 分配内存DLL B 用 MSVC 2019 的 `free` 释放——两个 `free` 管理不同的堆,导致崩溃或堆损坏。
**解决**dstalk 要求所有跨 DLL 边界的内存操作使用 Host 提供的统一分配/释放函数:
| 操作 | 用这个 |
|------|--------|
| 分配内存 | `host_api->alloc(size)` → 调用 host 启动时链接的 `malloc` |
| 释放内存 | `host_api->free(ptr)` → 调用 host 启动时链接的 `free` |
| 复制字符串 | `host_api->strdup(s)` → 用 host 的 `malloc` + `memcpy` |
```c
// 正确:跨 DLL 返回的字符串用 host 分配
r.content = g_host->strdup(ctx.accumulated.c_str());
// 正确:调用方释放跨 DLL 返回的数据用 host 释放
g_host->free((void*)result->content);
// 错误:用本地 malloc 分配跨 DLL 边界的数据
// r.content = strdup(...); // 消费者的 free() 和此 strdup 的 malloc() 可能不同堆!
```
**规则记忆**: 谁分配谁负责提供释放手段。dstalk 的选择是让 Host 统一分配——所有 `alloc/free` 调用走同一个 CRT 的堆。
---
## 生命周期速查
| 阶段 | 谁触发 | 插件状态 | 关键函数 |
|------|--------|----------|----------|
| DLL 加载 | `dstalk_init``dstalk_plugin_load` | 未初始化 | `load_plugin``dstalk_plugin_init()` |
| 依赖排序 | `initialize_all` | 等待初始化 | `topological_sort()` (Kahn) |
| 初始化 | Host 按序调用 | 运行中 | `on_init(host_api)` |
| 服务调用 | 任意插件/CLI 前端 | 运行中 | `query_service` → vtable 调用 |
| 卸载 | `dstalk_plugin_unload``dstalk_shutdown` | 关闭中 | `on_shutdown()``FreeLibrary`/`dlclose` |

View File

@@ -0,0 +1,109 @@
# Security Logging Audit (W9.3): 错误日志凭证泄露审计
**审计人**: 曹武 (security-cao)
**日期**: 2026-05-27
**审计范围**: 8 个核心/插件源码文件
**结论**: 未发现真实凭证泄露漏洞CVSS 不适用,零高危/中危/低危可利用漏洞)
---
## 审计方法论
对每个文件搜索以下输出调用:
- `host->log(...)` / `g_host->log(...)` (插件日志 API)
- `printf` / `fprintf(stderr, ...)` (C 标准输出)
- `std::cerr` / `std::cout` (C++ 标准流)
对每个匹配项检查其格式字符串和参数是否包含 `api_key``Authorization` header、`token` 或原始 request body/headers。
## 文件清单
### 1. `plugins/deepseek/src/deepseek_plugin.cpp` -- 安全
| 行号 | 调用 | 内容 | 风险 |
|------|------|------|------|
| 242-245 | `g_host->log(INFO, ...)` | 输出 model / base_url / max_tokens / temperature | 无 -- api_key 被有意排除在格式字符串外 |
| 442 | `g_host->log(ERROR, ...)` | 静态字符串 "http service not found" | 无 |
| 446 | `g_host->log(INFO, ...)` | 静态字符串 "initializing DeepSeek AI plugin" | 无 |
| 453 | `g_host->log(INFO, ...)` | 静态字符串 "shutdown" | 无 |
**build_headers_json() (行 59-63)**: 构建 `{"Authorization":"Bearer <key>"}` 并传给 HTTP 服务。该字符串从未传递给任何 log 调用,仅在 `http_post_json()` / `http_post_stream()` 的参数链中使用,最终由 Beast 直接设置到 HTTP request headers -- 全程无日志记录。
**parse_response() 错误路径 (行 135-151)**: HTTP 错误响应体仅用于提取 JSON `error.message` 字段放入 `r.error`,不会输出到日志。原始 response_body 在解析后被 `g_host->free()` 释放。
### 2. `plugins/anthropic/src/anthropic_plugin.cpp` -- 安全
| 行号 | 调用 | 内容 | 风险 |
|------|------|------|------|
| 247-250 | `g_host->log(INFO, ...)` | 输出 model / base_url / max_tokens / temperature | 无 -- api_key 被有意排除 |
| 453 | `g_host->log(ERROR, ...)` | 静态字符串 "http service not found" | 无 |
| 457 | `g_host->log(INFO, ...)` | 静态字符串 "initializing Anthropic AI plugin" | 无 |
| 464 | `g_host->log(INFO, ...)` | 静态字符串 "shutdown" | 无 |
**build_headers_json() (行 59-65)**: 构建 `{"x-api-key":"<key>","anthropic-version":"2023-06-01"}` 仅用于 HTTP 请求,不经过日志路径。
### 3. `plugins/network/src/network_plugin.cpp` -- 安全
| 行号 | 调用 | 内容 | 风险 |
|------|------|------|------|
| -- | 无任何 `host->log` / `printf` / `cerr` 调用 | -- | 无 |
**parse_headers_json() (行 40-80)**: 解析包含 api_key 的 headers JSON 字符串。解析结果仅用于 `req.set(h.first, h.second)` (行 176) 设置 HTTP header不输出日志。
**do_post_stream() 异常路径 (行 280-282)**: `catch (std::exception& e)``e.what()` 赋值给 `result_body`。Beast/ASIO 异常消息为 OS 级别错误描述(如 "Connection refused"),不含 HTTP header/body 内容。
### 4. `plugins/config/src/config_plugin.cpp` -- 安全
| 行号 | 调用 | 内容 | 风险 |
|------|------|------|------|
| -- | 无任何日志调用 | -- | 无 |
ConfigStore 仅提供 get/set/load_file无日志输出。
### 5. `dstalk-core/src/host.cpp` -- 基础设施(不动)
| 行号 | 调用 | 内容 | 风险 |
|------|------|------|------|
| 48,51,52 | `fprintf(stderr, ...)` | 日志前缀 + vfprintf(格式,参数) + 换行 | 无 -- 基础设施自身不包含业务数据 |
该文件是日志基础设施 (`host_log_impl`),仅负责格式化输出。安全性依赖于调用方不传敏感数据(本审计已确认所有调用方均安全)。按 W9.3 禁忌不修改此文件。
### 6. `dstalk-core/src/plugin_loader.cpp` -- 安全
| 行号 | 调用 | 内容 | 风险 |
|------|------|------|------|
| -- | 无任何日志调用 | -- | 无 |
### 7. `plugins/session/src/session_plugin.cpp` -- 安全
| 行号 | 调用 | 内容 | 风险 |
|------|------|------|------|
| 233 | `host->log(ERROR, ...)` | 静态字符串 "required service 'file_io' not found" | 无 |
该插件处理消息内容role/content但不记录任何消息数据到日志。
### 8. `plugins/lsp/src/lsp_plugin.cpp` -- 低风险
| 行号 | 调用 | 内容 | 风险 |
|------|------|------|------|
| 446 | `g_host->log(ERROR, ...)` | 静态字符串 "Invalid LSP frame..." | 无 |
| 479 | `g_host->log(ERROR, ...)` | `"failed to start: %s", server_cmd` | 低 -- 理论上 server_cmd 可能含令牌,但实际为 LSP 服务器路径(如 `clangd`),不属于 API 密钥范畴 |
| 525 | `g_host->log(ERROR, ...)` | 静态字符串 "initialize timed out" | 无 |
| 535 | `g_host->log(INFO, ...)` | `"server started: %s", server_cmd` | 低 -- 同上 |
| 565 | `g_host->log(INFO, ...)` | 静态字符串 "server stopped" | 无 |
| 720 | `g_host->log(INFO, ...)` | 静态字符串 "initializing LSP service plugin" | 无 |
| 728 | `g_host->log(INFO, ...)` | 静态字符串 "shutdown" | 无 |
`server_cmd` 是启动 LSP 子进程的命令行(例如 `clangd --log=error`),不是 API 密钥或 token。若用户将 API key 嵌入命令行(极不寻常且不安全的使用方式),则存在理论泄漏风险。判断为**低风险/假阳性**,不做代码修改。
## 总结
| 风险等级 | 数量 | 说明 |
|----------|------|------|
| 严重 (CVSS 9.0+) | 0 | 无 |
| 高危 (CVSS 7.0-8.9) | 0 | 无 |
| 中危 (CVSS 4.0-6.9) | 0 | 无 |
| 低危 (CVSS 0.1-3.9) | 0 | 无真实可利用漏洞 |
| 低风险/假阳性 | 2 | 仅 lsp `server_cmd` 日志和 network `e.what()` 理论上可能暴露非凭证信息 |
**审计结论**: 所有日志输出路径均已检查,证实 DeepSeek 和 Anthropic 插件的 `my_configure()` 日志有意排除了 `api_key` 字段。HTTP headers 中的凭证仅通过内存传递至 Beast HTTP 请求对象,从未进入日志管道。代码库对此攻击面防御充分,无需修改。

View File

@@ -0,0 +1,39 @@
# CLI 命令速查
dstalk 所有内置命令。在对话中直接输入 `/help``/h` 也可查看此列表。
---
## 命令表
| 命令 | 别名 | 作用 | 示例 |
|------|------|------|------|
| `/help` | `/h` | 显示命令列表 | `/help` |
| `/quit` | `/q` | 退出程序 | `/quit` |
| `/clear` | — | 清空当前会话上下文 | `/clear` |
| `/context` | — | 显示当前 Token 数和消息条数 | `/context` |
| `/status` | — | 显示当前运行状态 (脱敏: 不打印完整 API Key) | `/status` |
| `/model <name>` | — | 切换 AI 模型 | `/model deepseek-v4-pro` |
| `/file list [path]` | — | 列出目录内容, 不填 path 列出当前目录 | `/file list src/` |
| `/file show <path>` | — | 查看文件内容 | `/file show main.cpp` |
| `/file read <path>` | — | 读取文件内容 (同 `/file show`) | `/file read config.toml` |
| `/file write <path> <content>` | — | 写入文件内容 | `/file write hello.c #include <stdio.h>...` |
| `/save <path>` | — | 保存当前会话到文件 | `/save session.json` |
| `/load <path>` | — | 从文件恢复会话 | `/load session.json` |
---
## 命令规则
- 所有命令以 `/` 开头
- 不以下划线开头的输入视为 AI 对话内容, 由 AI 服务处理
- 模型切换即时生效, 不影响已累积的会话历史
- `/file show``/file read` 功能相同, 均为读取并打印文件内容
- `/status` 显示脱敏信息: API Key 只显示 "已设置" 或 "未设置", 不暴露完整值
---
## 相关文档
- [快速入门教程](../tutorial/quick-start.md) — 5 步上手
- [文档导航](../README.md) — 全部文档索引

View File

@@ -0,0 +1,236 @@
# dstalk Plugin ABI 契约
> **面向**: 插件作者、host 维护者
> **性质**: 规范性文档。违反任何条目 = 未定义行为。
---
## 1. DSTALK_API_VERSION
```c
#define DSTALK_API_VERSION 1 // dstalk_host.h
```
**语义**: 主版本号。当且仅当 `dstalk_host_api_t` 的字段布局、`dstalk_plugin_info_t` 的结构、或任意
`*_service_t` vtable 的函数签名发生**不兼容变更**时 bump。
**不 bump 的情况**: 新增 event type 枚举值、新增日志级别、在 vtable 末尾追加函数指针(注意:
不能改变已有字段的偏移)。
**匹配规则**: `plugin::api_version` 必须**精确等于** `DSTALK_API_VERSION`。host 拒绝加载不匹配的
插件(`plugin_loader.cpp:68`),不提供后向兼容。这是硬断点——旧插件在新 host 上重新编译即可适配,
二进制兼容不做保证。
**版本协商被拒绝的理由**: 早期版本刻意保持简单。如果未来需要协商(如 min/max version range
通过 bump `DSTALK_API_VERSION` 到 2 并定义协商结构体即可。W1.x 阶段不需要。
---
## 2. 内存所有权契约
Core rule: **谁分配,谁释放。分配函数必须与释放函数配对。**
### 2.1 Host 分配的字符串
以下函数返回的 `const char*` / `char*` **由 host 拥有**,调用方不得释放:
| 来源 | 示例 | 生命周期 |
|------|------|----------|
| `host->config_get(key)` | 配置值字符串 | 随 config 条目存在,或下次 `config_set` 覆盖前有效 |
| `dstalk_chat_result_t.content` | AI 回复 | 必须在读取后立即复制;下一轮 `chat()` 调用可能覆盖 |
以下函数返回的 `char*` **由调用方释放**(用 `dstalk_free` / `host->free`
| 函数 | 释放方式 |
|------|----------|
| `dstalk_strdup(s)` / `host->strdup(s)` | `dstalk_free(ptr)` / `host->free(ptr)` |
| `dstalk_plugin_list(output_json)` | `dstalk_free(*output_json)` |
| LSP 便捷函数 (`dstalk_lsp_*`) 的 `char** output` 参数 | `dstalk_free(*output)` |
### 2.2 插件返回的字符串
`dstalk_plugin_info_t` 中的 `name``version``description` 由插件的 `dstalk_plugin_init()`
返回。host 在加载时读取并内部分配 `std::string` 副本(`plugin_loader.cpp:81-83`),随后不再
引用原始指针。因此这些字符串的**生命周期只需覆盖 `init_fn()` 调用期间**——可以是静态字面量、
栈上字符串、或插件内部分配的内存。
### 2.3 服务 vtable 的返回值
`dstalk_chat_result_t.content``.error``.tool_calls_json` 按结构体注释约定由 `dstalk_strdup`
分配,**调用方**(即查询该服务的插件)负责 `dstalk_free`
**反例**: 插件直接返回 `std::string::c_str()` 或栈上 buffer —— 因为服务调用完成后插件栈帧
可能已销毁。
---
## 3. 跨 DLL 堆纪律
### 3.1 问题
Windows 上每个 DLL 拥有独立的 CRT 堆(取决于链接方式:/MD 共享 或 /MT 静态)。插件在其
CRT 中调用 `malloc` 得到的指针host 调用 `free` 时访问的是 host CRT 的堆——行为未定义。
Linux/macOS 通常共享 libc但静态链接或不同 libc 版本时同样可能 crash。
### 3.2 硬性规则
> **严禁插件直接调用 `malloc`/`free`/`strdup`/`new`/`delete` 处理 host 传入或传出的数据。**
正确做法:
| 场景 | 错误做法 | 正确做法 |
|------|----------|----------|
| 释放 host 给的字符串 | `free(host->config_get("key"))` | 不释放;只读后丢弃 |
| 分配传给 host 的缓冲区 | `malloc(256)` | `host->alloc(256)``dstalk_alloc(256)` |
| 释放 host 分配的内存 | `delete ptr` / `std::free(ptr)` | `host->free(ptr)``dstalk_free(ptr)` |
| 复制 host 给的字符串 | `strdup(host->config_get("k"))` | `host->strdup(...)``dstalk_strdup(...)` |
| 在插件 DLL 内分配/释放私有数据 | 可以用任何方式 | 只要不跨越 DLL 边界 |
### 3.3 设计理由
`host->alloc` / `host->free` / `host->strdup` 是通过函数指针调回 host DLL 的 `malloc`/`free`
保证分配和释放发生在**同一个堆**上。`g_host_api` 表的 `api_alloc`/`api_free` 直接就是 `malloc`
/`free``host.cpp:111-112`),所以 "host 分配 → host 释放" 总是在同一个 CRT 堆内。
---
## 4. register_service 契约
### 4.1 调用时机
`host->register_service(name, version, vtable)` **仅可在 `on_init` 回调期间调用**
原因:
- 服务注册表 `ServiceRegistry``dstalk_init()` 内创建,`on_init` 之前已存在。
- 初始化顺序由拓扑排序保证:依赖方的 `on_init` 在被依赖方之后调用,因此被依赖方注册的服务
在依赖方调用时已可用。
- `on_shutdown` 期间不应注册新服务(该阶段仅做清理)。
### 4.2 重复注册
同一 `name` 不可重复注册:第二次调用返回 `-2``service_registry.cpp:13`)。插件应检查返回
值,在共享服务名(如 `"ai.deepseek"`)的场景中避免冲突。
### 4.3 版本协商
`register_service``version` 参数声明了该 vtable 实现的版本。`query_service`
`min_version` 允许调用方声明最低需求版本。当前实现为整型比较:`registered_version >=
min_version` 即可。
**插件作者约定**: version 从 1 开始。vtable 新增函数指针(追加到末尾)→ bump version。
vtable 重排字段或删除函数 → 改 service name`"lsp"``"lsp2"`)。
### 4.4 vtable 生命周期
注册的 vtable 指针在 `ServiceRegistry` 中存储,随插件存在。插件卸载时,`ServiceRegistry::
unregister_service` 会被调用。**插件不得在卸载后继续持有该 vtable 指针**。
---
## 5. on_init / on_shutdown 契约
### 5.1 调用顺序保证
- **on_init**: 按拓扑顺序调用(依赖项先初始化)。由 `PluginLoader::topological_sort()`
使用 Kahn 算法计算(`plugin_loader.cpp:144-198`)。
- **on_shutdown**: 按拓扑顺序的**逆序**调用(依赖项后销毁)。如果拓扑排序因循环依赖失败,
降级为任意顺序(`plugin_loader.cpp:263-267`)。
- **增量加载**: `dstalk_plugin_load()` 只初始化新加载的插件(`initialize_pending`),已
初始化的插件不受影响。
### 5.2 返回值
`on_init` 返回 `0` = 成功;非零 = 失败。失败导致 `initialize_all` 返回 `-1`,整体初始化
流程中止。失败插件的 `on_shutdown` **不会被调用**`plugin.initialized` 保持 false
### 5.3 异常安全 (C++ ABI)
> **C++ 异常不得穿越 C ABI 边界。**
`on_init``on_shutdown` 定义为 C 函数指针:
```c
int (*on_init)(const dstalk_host_api_t* host);
void (*on_shutdown)(void);
```
调用方PluginLoader**不设置** `try/catch` 保护(`plugin_loader.cpp:212-214`)。
如果 `on_init` 由 C++ 实现且抛出异常,将导致 `std::terminate` / 未定义行为。
**防护规则**:
-`extern "C"` 声明实现
- 所有可能抛异常的 C++ 逻辑用 `try { ... } catch (...) { return -1; }` 包裹
- `on_shutdown` 同理,即使 void 也不能抛异常
---
## 6. 回调线程安全
### 6.1 诊断回调 (diag_callback)
`g_diag_callback``std::atomic<dstalk_diag_cb>`,使用 `memory_order_acquire` / `release`
`host.cpp:28,54,305`)。**多线程同时调用 `dstalk_log` 安全**——每个线程独立读取原子指针,
无数据竞争。
### 6.2 事件总线 (EventBus)
`EventBus` 使用 `std::shared_mutex`
- `subscribe` / `unsubscribe` 持有 `unique_lock`
- `emit` 持有 `shared_lock`
**结论**: 多线程 subscribe/unsubscribe/emit 安全。但 emit 持有 shared_lock 期间直接调用
handler —— handler 内**不得调用 subscribe/unsubscribe**(会尝试 unique_lock 导致死锁)。
`on_event` 回调与 `on_init` / `on_shutdown` 之间无互斥保护,因此**不应在 `on_event` 回调
内调用 host API 执行插件的 load/unload**PluginLoader 无内部锁)。
### 6.3 服务注册表 (ServiceRegistry)
使用 `std::shared_mutex`register/unregister 持写锁query 持读锁。并发安全。
### 6.4 配置 (ConfigStore)
使用 `std::mutex`get/set 串行化。`config_get` 返回的指针指向内部 `std::string`;在并发
`config_set` 同一 key 后指针可能悬垂——调用方应复制。
### 6.5 Plugin Loader
`PluginLoader` **无内部互斥**`plugin_loader.hpp` 无 mutex 成员。load/unload 不应在
多线程中并发调用。这是 host 层的设计假设——仅 `dstalk_init` / `dstalk_shutdown` 和显式
CLI 命令使用。
---
## 7. 依赖声明 (dependencies)
### 7.1 语法
```c
const char* dependencies[DSTALK_MAX_DEPS]; // DSTALK_MAX_DEPS = 8
```
`NULL` 终止的字符串数组,每个元素为被依赖插件的 `name`
### 7.2 语义
- 被依赖项必须先于依赖方初始化(拓扑排序保证)
- 被依赖项必须后于依赖方销毁(逆序 shutdown
- 如果被依赖项不存在:初始化时不影响拓扑排序(仅计算已加载插件间的依赖关系),但插件的
`on_init` 自己应检查所需服务是否可用(通过 `host->query_service`
- 循环依赖:拓扑排序失败,`topological_sort()` 抛出 `std::runtime_error("Circular
dependency detected")`,被 `initialize_all` 捕获并返回 `-1`
- 最大 8 个依赖,超出部分截断(`plugin_loader.cpp:90`
### 7.3 最佳实践
`on_init` 内通过 `host->query_service("service_name", min_version)` 验证依赖可用。
不做假设。这也是一种隐式版本协商:如果 service version 不足,`on_init` 返回错误。
---
## 变更历史
| 日期 | 版本 | 变更 |
|------|------|------|
| 2026-05-27 | 1.0 | 初始版本。W9.4 交付。基于 DSTALK_API_VERSION=1 的当前实现。 |

View File

@@ -0,0 +1,140 @@
# 快速入门
5 步上手 dstalk, 从安装工具链到完成第一个 AI 对话。
---
## 1. 安装工具链
dstalk 需要 CMake、Ninja、LLVM/Clang 和 Conan2。`setup.bat` 全自动下载安装到 `tools/` 目录。
```bash
cd tools
setup.bat
```
> **前提**: 系统需已安装 Python 3.10+。
>
> 网络不畅时, 可手动下载放入对应目录:
> - [Ninja](https://github.com/ninja-build/ninja/releases) → `tools/ninja/ninja.exe`
> - [CMake](https://cmake.org/download/) → `tools/cmake/bin/cmake.exe`
> - [LLVM](https://github.com/llvm/llvm-project/releases) → `tools/llvm/bin/clang.exe`
> - Conan2 通过 pip 安装到 `tools/.venv/Scripts/conan.exe`
---
## 2. 编译
项目根目录提供 `build.bat`, 一键完成: Conan 拉取依赖 -> CMake 配置 -> Ninja 编译。
```bash
build.bat
```
编译产物输出到 `build/` 目录。核心产物:
- `build/dstalk-core/dstalk.dll` —— 核心 DLL
- `build/dstalk-cli/dstalk-cli.exe` —— 命令行前端
- `build/plugins/*.dll` —— 功能插件
---
## 3. 创建 config.toml
在项目根目录创建 `config.toml`, 配置 AI 后端和 API Key。
```bash
# 在项目根目录手动创建 config.toml
```
**config.toml 示例:**
```toml
# 选择 AI 后端插件: ai.deepseek 或 ai.anthropic
ai.provider = "ai.deepseek"
# DeepSeek
api.base_url = "https://api.deepseek.com/v1"
api.api_key = "sk-xxxxxxxx"
api.model = "deepseek-v4-pro"
# Anthropic Claude (切换 ai.provider 为 "ai.anthropic" 即可)
# api.base_url = "https://api.anthropic.com/v1"
# api.api_key = "sk-ant-xxxxxxxx"
# api.model = "claude-opus-4-20250514"
```
> **关键**: 修改 `ai.provider` 字段即可在不同后端间切换, 无需改动代码。
>
> API Key 可从 [DeepSeek 开放平台](https://platform.deepseek.com/) 或 [Anthropic Console](https://console.anthropic.com/) 获取。
---
## 4. 运行 dstalk-cli
```bash
build/dstalk-cli/dstalk-cli.exe
```
启动后显示欢迎横幅:
```text
dstalk v0.1.0 | dstalk AI | /help 查看帮助 | /quit 退出
[deepseek-v4-pro] >
```
> 图形模式默认关闭。需要 SDL3 GUI 时, 用 `-DDSTALK_BUILD_GUI=ON` 重新配置 CMake。
---
## 5. 第一个对话
在提示符 `>` 后输入自然语言, 即可与 AI 对话。
```text
[deepseek-v4-pro] > 帮我写一个读取 CSV 并计算平均值的 C 程序
[dstalk] 正在思考...
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "用法: %s <csv文件>\n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "r");
if (!fp) { perror("fopen"); return 1; }
double sum = 0.0;
int count = 0;
char line[1024];
while (fgets(line, sizeof(line), fp)) {
sum += atof(line);
count++;
}
fclose(fp);
printf("平均值: %.2f (共 %d 行)\n", sum / count, count);
return 0;
}
已写入 csv_avg.c。需要我帮你编译测试吗
[deepseek-v4-pro] > 把这段代码改成支持表头的
[dstalk] 已更新 csv_avg.c——跳过第一行表头, 增加列选择功能。
[deepseek-v4-pro] > /file show csv_avg.c
[dstalk] 已显示 csv_avg.c 内容。
```
---
## 下一步
- 查看 [CLI 命令速查表](../reference/commands.md) 了解全部命令
- 输入 `/help` 在 dstalk 内查看命令列表
- 输入 `/status` 查看当前运行状态