Add unit tests for OpenAI plugin and establish coding standards

- Introduced comprehensive unit tests for the OpenAI plugin, covering SSE parsing, sentinel matching, delta extraction, request building, and more.
- Created a new markdown file detailing coding and naming conventions for the dstalk project, including guidelines for comments, naming rules, code organization, and memory management practices.
This commit is contained in:
2026-05-31 00:51:59 +08:00
parent f2da0f2ed4
commit f6cb51b40a
21 changed files with 343 additions and 131 deletions

View File

@@ -9,7 +9,7 @@
dstalk 选择的不是单体架构,而是**以 C ABI 为边界的插件架构**。这是几条需求推导出的必然结果:
1. **AI 后端会变**DeepSeek / OpenAI / Anthropic 各有不同的 HTTP 协议细节和模型参数。今天用 A明天切 B后天同时挂两个。单体应用内硬编码所有后端会导致每次新增后端都要改核心、重新编译、重新测试。
1. **AI 后端会变**OpenAI-compatible / OpenAI / Anthropic 各有不同的 HTTP 协议细节和模型参数。今天用 A明天切 B后天同时挂两个。单体应用内硬编码所有后端会导致每次新增后端都要改核心、重新编译、重新测试。
2. **能力会增长**。LSP 集成、文件管理、会话持久化、工具调用——这些能力不是 CLI 启动时必须加载的。使用者可能只需要聊天,不需要 LSP。插件架构让能力按需加载启动更快内存更省。
@@ -69,7 +69,7 @@ Host 启动时,按严格顺序执行:
插件在 `on_init` 中做三件事:
1. 保存 `host_api` 指针,这是它此后访问一切 Host 能力的唯一通道。
2. 通过 `host_api->query_service` 查找它依赖的服务(例如 deepseek 插件查询 `http``config`)。
2. 通过 `host_api->query_service` 查找它依赖的服务(例如 openai 插件查询 `http``config`)。
3. 通过 `host_api->register_service` 向注册表注册自己提供的服务 vtable。
关键设计:插件之间**不直接链接**。插件 A 不知道插件 B 是否在当前进程中。A 只对 Host 说"我需要名为 `http` 的服务"Host 从注册表里找出那个 vtable把指针交给 A。
@@ -90,20 +90,20 @@ struct dstalk_ai_service_t {
每种服务都有一个预定义的 vtable 结构体(定义在 `dstalk_services.h`)。第三方也可以扩展自己的服务 vtable版本号随注册一起提供允许消费者做最低版本检查。
服务注册采用 **name + version** 两要素:
- 全局唯一名称(如 `"ai.deepseek"``"http"``"file_io"`)。
- 全局唯一名称(如 `"ai.openai"``"http"``"file_io"`)。
- 版本号(消费者可以要求 `min_version`)。
---
## Service Registry 解决什么问题?
核心问题是 **耦合方向**。如果不使用注册表,deepseek 插件就得直接知道 http 插件的符号,通过 `dlsym` 或头文件耦合。换成注册表后:
核心问题是 **耦合方向**。如果不使用注册表,openai 插件就得直接知道 http 插件的符号,通过 `dlsym` 或头文件耦合。换成注册表后:
- 提供者说:"我注册一个名为 `http` 的 vtable"。
- 消费者说:"给我一个名为 `http`、版本 >= 1 的 vtable"。
- Host 做中间人查表。
**效果**: 你可以用任意方式实现 `http` 服务——用 libcurl、用 WinHTTP、用 mock——deepseek 插件一行代码都不需要改。它只依赖服务接口,不依赖服务实现。
**效果**: 你可以用任意方式实现 `http` 服务——用 libcurl、用 WinHTTP、用 mock——openai 插件一行代码都不需要改。它只依赖服务接口,不依赖服务实现。
---

View File

@@ -36,7 +36,7 @@ Host 调用 `load_plugin(path)`,由 `PluginLoader` 执行:
所有插件加载完毕后,`initialize_all` 在调用 `on_init` 之前先执行拓扑排序。
**为什么需要排序?** 如果 deepseek 插件依赖 `http``config`,那么 `http``config` 插件的 `on_init` 必须先于 deepseek 执行——否则 deepseek`on_init` 中调用 `query_service("http")` 会得到 `nullptr`
**为什么需要排序?** 如果 openai 插件依赖 `http``config`,那么 `http``config` 插件的 `on_init` 必须先于 openai 执行——否则 openai`on_init` 中调用 `query_service("http")` 会得到 `nullptr`
**算法**Kahn 算法BFS 拓扑排序)。
@@ -48,7 +48,7 @@ Host 调用 `load_plugin(path)`,由 `PluginLoader` 执行:
依赖声明在 `dstalk_plugin_info_t.dependencies` 中,以 NULL 结尾的字符串数组。最多 `DSTALK_MAX_DEPS` (8) 个依赖。
```
// 示例:deepseek 插件声明依赖 http 和 config
// 示例:openai 插件声明依赖 http 和 config
{ "http", "config", NULL }
```
@@ -87,7 +87,7 @@ Host 收到非零返回值后,会跳过后续插件的初始化并报告警告
**契约 3register_service 注册自己的服务。** 插件将自己的 vtable 注册到服务注册表后,其他依赖它的插件才能在后续的 `on_init` 中通过 `query_service` 找到它。
```c
return host->register_service("ai.deepseek", 1, &g_service);
return host->register_service("ai.openai", 1, &g_service);
```
注册表内的 vtable 是原始指针,不拷贝。因此 vtable 指向的结构体必须是**静态生命周期**(全局变量或 static 局部变量)。
@@ -103,8 +103,8 @@ return host->register_service("ai.deepseek", 1, &g_service);
Host 关闭时,按拓扑排序的**逆序**调用 `on_shutdown`——这保证了被依赖者后卸载。
```
加载顺序: config → http → deepseek
卸载顺序: deepseek → http → config
加载顺序: config → http → openai
卸载顺序: openai → http → config
```
注意:如果拓扑排序失败(如循环依赖),`shutdown_all` 会退化为任意顺序,仅保证所有插件的 `on_shutdown` 都被调用、所有 DLL 句柄都被释放。

View File

@@ -18,13 +18,13 @@
## 文件清单
### 1. `plugins/deepseek/src/deepseek_plugin.cpp` -- 安全
### 1. `plugins/openai/src/openai_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" | 无 |
| 446 | `g_host->log(INFO, ...)` | 静态字符串 "initializing OpenAI-compatible 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 -- 全程无日志记录。
@@ -106,4 +106,4 @@ ConfigStore 仅提供 get/set/load_file无日志输出。
| 低危 (CVSS 0.1-3.9) | 0 | 无真实可利用漏洞 |
| 低风险/假阳性 | 2 | 仅 lsp `server_cmd` 日志和 network `e.what()` 理论上可能暴露非凭证信息 |
**审计结论**: 所有日志输出路径均已检查,证实 DeepSeek 和 Anthropic 插件的 `my_configure()` 日志有意排除了 `api_key` 字段。HTTP headers 中的凭证仅通过内存传递至 Beast HTTP 请求对象,从未进入日志管道。代码库对此攻击面防御充分,无需修改。
**审计结论**: 所有日志输出路径均已检查,证实 OpenAI-compatible 和 Anthropic 插件的 `my_configure()` 日志有意排除了 `api_key` 字段。HTTP headers 中的凭证仅通过内存传递至 Beast HTTP 请求对象,从未进入日志管道。代码库对此攻击面防御充分,无需修改。

View File

@@ -13,7 +13,7 @@ dstalk 所有内置命令。在对话中直接输入 `/help` 或 `/h` 也可查
| `/clear` | — | 清空当前会话上下文 | `/clear` |
| `/context` | — | 显示当前 Token 数和消息条数 | `/context` |
| `/status` | — | 显示当前运行状态 (脱敏: 不打印完整 API Key) | `/status` |
| `/model <name>` | — | 切换 AI 模型 | `/model deepseek-v4-pro` |
| `/model <name>` | — | 切换 AI 模型 | `/model gpt-4o` |
| `/file list [path]` | — | 列出目录内容, 不填 path 列出当前目录 | `/file list src/` |
| `/file show <path>` | — | 查看文件内容 | `/file show main.cpp` |
| `/file read <path>` | — | 读取文件内容 (同 `/file show`) | `/file read config.toml` |

View File

@@ -110,7 +110,7 @@ Linux/macOS 通常共享 libc但静态链接或不同 libc 版本时同样可
### 4.2 重复注册
同一 `name` 不可重复注册:第二次调用返回 `-2``service_registry.cpp:13`)。插件应检查返回
值,在共享服务名(如 `"ai.deepseek"`)的场景中避免冲突。
值,在共享服务名(如 `"ai.openai"`)的场景中避免冲突。
### 4.3 版本协商

View File

@@ -49,13 +49,13 @@ build.bat
**config.toml 示例:**
```toml
# 选择 AI 后端插件: ai.deepseek 或 ai.anthropic
ai.provider = "ai.deepseek"
# 选择 AI 后端插件: ai.openai 或 ai.anthropic
ai.provider = "ai.openai"
# DeepSeek
api.base_url = "https://api.deepseek.com/v1"
# OpenAI-compatible
api.base_url = "https://api.openai.com/v1"
api.api_key = "sk-xxxxxxxx"
api.model = "deepseek-v4-pro"
api.model = "gpt-4o"
# Anthropic Claude (切换 ai.provider 为 "ai.anthropic" 即可)
# api.base_url = "https://api.anthropic.com/v1"
@@ -65,7 +65,7 @@ api.model = "deepseek-v4-pro"
> **关键**: 修改 `ai.provider` 字段即可在不同后端间切换, 无需改动代码。
>
> API Key 可从 [DeepSeek 开放平台](https://platform.deepseek.com/) 或 [Anthropic Console](https://console.anthropic.com/) 获取。
> API Key 可从 [OpenAI-compatible 开放平台](https://platform.openai.com/) 或 [Anthropic Console](https://console.anthropic.com/) 获取。
---
@@ -80,7 +80,7 @@ build/dstalk-cli/dstalk-cli.exe
```text
dstalk v0.1.0 | dstalk AI | /help 查看帮助 | /quit 退出
[deepseek-v4-pro] >
[gpt-4o] >
```
> 图形模式默认关闭。需要 SDL3 GUI 时, 用 `-DDSTALK_BUILD_GUI=ON` 重新配置 CMake。
@@ -92,7 +92,7 @@ dstalk v0.1.0 | dstalk AI | /help 查看帮助 | /quit 退出
在提示符 `>` 后输入自然语言, 即可与 AI 对话。
```text
[deepseek-v4-pro] > 帮我写一个读取 CSV 并计算平均值的 C 程序
[gpt-4o] > 帮我写一个读取 CSV 并计算平均值的 C 程序
[dstalk] 正在思考...
@@ -122,11 +122,11 @@ dstalk v0.1.0 | dstalk AI | /help 查看帮助 | /quit 退出
已写入 csv_avg.c。需要我帮你编译测试吗
[deepseek-v4-pro] > 把这段代码改成支持表头的
[gpt-4o] > 把这段代码改成支持表头的
[dstalk] 已更新 csv_avg.c——跳过第一行表头, 增加列选择功能。
[deepseek-v4-pro] > /file show csv_avg.c
[gpt-4o] > /file show csv_avg.c
[dstalk] 已显示 csv_avg.c 内容。
```