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:
@@ -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 插件一行代码都不需要改。它只依赖服务接口,不依赖服务实现。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 收到非零返回值后,会跳过后续插件的初始化并报告警告
|
||||
**契约 3:register_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 句柄都被释放。
|
||||
|
||||
@@ -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 请求对象,从未进入日志管道。代码库对此攻击面防御充分,无需修改。
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -110,7 +110,7 @@ Linux/macOS 通常共享 libc,但静态链接或不同 libc 版本时同样可
|
||||
### 4.2 重复注册
|
||||
|
||||
同一 `name` 不可重复注册:第二次调用返回 `-2`(`service_registry.cpp:13`)。插件应检查返回
|
||||
值,在共享服务名(如 `"ai.deepseek"`)的场景中避免冲突。
|
||||
值,在共享服务名(如 `"ai.openai"`)的场景中避免冲突。
|
||||
|
||||
### 4.3 版本协商
|
||||
|
||||
|
||||
@@ -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 内容。
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user