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 请求对象,从未进入日志管道。代码库对此攻击面防御充分,无需修改。