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:
@@ -29,7 +29,7 @@ dstalk 是一款 AI 编程助手命令行工具, 通过调用大模型在终端
|
||||
│ │ Host: 插件加载 · 服务注册 · 事件总线 · 配置管理 │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
||||
│ │ deepseek │ │ anthropic│ │ network │ │ lsp │ │
|
||||
│ │ openai │ │ anthropic│ │ network │ │ lsp │ │
|
||||
│ │ (ai) │ │ (ai) │ │ (http) │ │ 客户端 │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────────┘ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
||||
@@ -51,9 +51,9 @@ dstalk 是一款 AI 编程助手命令行工具, 通过调用大模型在终端
|
||||
|
||||
| 提供商 | 模型 | 插件 |
|
||||
|--------|------|------|
|
||||
| DeepSeek | deepseek-v4-pro | `ai.deepseek` |
|
||||
| OpenAI-compatible | gpt-4o | `ai.openai` |
|
||||
| Anthropic | claude-opus-4 | `ai.anthropic` |
|
||||
| OpenAI 兼容 | GPT 系列 | `ai.deepseek` (兼容) |
|
||||
| OpenAI 兼容 | GPT 系列 | `ai.openai` |
|
||||
|
||||
通过 `config.toml` 中 `ai.provider` 一键切换。
|
||||
|
||||
|
||||
@@ -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 内容。
|
||||
```
|
||||
|
||||
@@ -220,9 +220,9 @@ static void handle_command(const char* line)
|
||||
// /status —— 脱敏显示当前运行状态 / Display current runtime status (desensitized)
|
||||
if (std::strcmp(line, "/status") == 0) {
|
||||
const char* provider = dstalk_config_get("ai.provider");
|
||||
if (!provider) provider = "ai.deepseek";
|
||||
if (!provider) provider = "ai.openai";
|
||||
const char* base_url = dstalk_config_get("api.base_url");
|
||||
if (!base_url) base_url = "https://api.deepseek.com/v1";
|
||||
if (!base_url) base_url = "https://api.openai.com/v1";
|
||||
const char* api_key = dstalk_config_get("api.api_key");
|
||||
|
||||
std::printf(" 模型: %s\n", g_current_model.empty() ? "(未设置)" : g_current_model.c_str());
|
||||
@@ -488,7 +488,7 @@ int main(int argc, char* argv[])
|
||||
|
||||
// 查询插件服务 / Query plugin services
|
||||
const char* ai_provider = dstalk_config_get("ai.provider");
|
||||
if (!ai_provider) ai_provider = "ai.deepseek";
|
||||
if (!ai_provider) ai_provider = "ai.openai";
|
||||
g_ai = static_cast<const dstalk_ai_service_t*>(dstalk_service_query(ai_provider, 1));
|
||||
g_session = static_cast<const dstalk_session_service_t*>(dstalk_service_query("session", 1));
|
||||
g_file_io = static_cast<const dstalk_file_io_service_t*>(dstalk_service_query("file_io", 1));
|
||||
@@ -506,8 +506,8 @@ int main(int argc, char* argv[])
|
||||
const char* base_url = dstalk_config_get("api.base_url");
|
||||
const char* api_key = dstalk_config_get("api.api_key");
|
||||
const char* model = dstalk_config_get("api.model");
|
||||
if (!base_url) base_url = "https://api.deepseek.com/v1";
|
||||
if (!model) model = "deepseek-v4-pro";
|
||||
if (!base_url) base_url = "https://api.openai.com/v1";
|
||||
if (!model) model = "gpt-4o";
|
||||
g_ai->configure(ai_provider, base_url, api_key ? api_key : "", model, 4096, 0.7);
|
||||
g_current_model = model; // A1: 记录当前模型名 / Record current model name
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/* ---- AI 服务 vtable / AI service vtable ---- */
|
||||
/* 以名称如 "ai.deepseek" 或 "ai.anthropic" 注册 / Registered under names such as "ai.deepseek" or "ai.anthropic" */
|
||||
/* 以名称如 "ai.openai" 或 "ai.anthropic" 注册 / Registered under names such as "ai.openai" or "ai.anthropic" */
|
||||
typedef struct {
|
||||
/* 配置服务商连接 (base_url, api_key, model 等) / Configure provider connection (base_url, api_key, model, etc.) */
|
||||
int (*configure)(const char* provider, const char* base_url,
|
||||
|
||||
@@ -100,7 +100,7 @@ struct GuiState {
|
||||
int history_index = -1; // 当前历史位置(-1 = 新输入) / current history position (-1 = new input)
|
||||
std::string saved_input; // 浏览历史时暂存当前输入 / saved current input while browsing history
|
||||
bool sidebar_visible = true; // 侧边栏可见性 / sidebar visibility
|
||||
std::string model_name = "deepseek-chat";// 当前模型名 / current model name
|
||||
std::string model_name = "gpt-4o";// 当前模型名 / current model name
|
||||
};
|
||||
|
||||
// 将 GuiState 与 SDL 窗口/渲染器句柄及逐帧标志打包。
|
||||
@@ -814,7 +814,7 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
const char* ai_provider = dstalk_config_get("ai.provider");
|
||||
if (!ai_provider) ai_provider = "ai.deepseek";
|
||||
if (!ai_provider) ai_provider = "ai.openai";
|
||||
g_ai_svc = static_cast<const dstalk_ai_service_t*>(dstalk_service_query(ai_provider, 1));
|
||||
g_session_svc = static_cast<const dstalk_session_service_t*>(dstalk_service_query("session", 1));
|
||||
if (!g_ai_svc) dstalk_log(3, "AI service not found (check plugins directory)");
|
||||
|
||||
@@ -507,7 +507,7 @@ int main(int argc, char* argv[])
|
||||
|
||||
// 查询插件服务 / Query plugin services
|
||||
const char* ai_provider = dstalk_config_get("ai.provider");
|
||||
if (!ai_provider) ai_provider = "ai.deepseek";
|
||||
if (!ai_provider) ai_provider = "ai.openai";
|
||||
g_ai = static_cast<const dstalk_ai_service_t*>(dstalk_service_query(ai_provider, 1));
|
||||
g_session = static_cast<const dstalk_session_service_t*>(dstalk_service_query("session", 1));
|
||||
|
||||
@@ -523,8 +523,8 @@ int main(int argc, char* argv[])
|
||||
const char* base_url = dstalk_config_get("api.base_url");
|
||||
const char* api_key = dstalk_config_get("api.api_key");
|
||||
const char* model = dstalk_config_get("api.model");
|
||||
if (!base_url) base_url = "https://api.deepseek.com/v1";
|
||||
if (!model) model = "deepseek-v4-pro";
|
||||
if (!base_url) base_url = "https://api.openai.com/v1";
|
||||
if (!model) model = "gpt-4o";
|
||||
g_ai->configure(ai_provider, base_url, api_key ? api_key : "", model, 4096, 0.7);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
# ============================================================
|
||||
# 插件目录 — 所有功能插件
|
||||
# 插件目录 — 所有功能插件 / Plugin directory — all functional plugins
|
||||
# ============================================================
|
||||
|
||||
# 基础插件(无外部服务依赖)
|
||||
# 基础插件(无依赖) / Base plugins (no dependencies)
|
||||
add_subdirectory(config)
|
||||
add_subdirectory(file-io)
|
||||
add_subdirectory(network)
|
||||
|
||||
# 中间插件(依赖基础插件)
|
||||
add_subdirectory(session)
|
||||
add_subdirectory(context)
|
||||
|
||||
# 上层插件(依赖中间插件)
|
||||
add_subdirectory(deepseek)
|
||||
add_subdirectory(anthropic)
|
||||
add_subdirectory(tools)
|
||||
add_subdirectory(lsp)
|
||||
|
||||
# 依赖基础插件的插件 / Plugins depending on base plugins only
|
||||
add_subdirectory(network) # 依赖 config / depends on config
|
||||
add_subdirectory(session) # 依赖 file_io / depends on file_io
|
||||
add_subdirectory(tools) # 依赖 file_io / depends on file_io
|
||||
|
||||
# 依赖其他插件的插件 / Plugins depending on non-base plugins
|
||||
add_subdirectory(context) # 依赖 session / depends on session
|
||||
add_subdirectory(openai) # 依赖 http, config / depends on http, config
|
||||
add_subdirectory(anthropic) # 依赖 http, config / depends on http, config
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
|
||||
# ============================================================
|
||||
# plugin-deepseek — DeepSeek AI 服务 (OpenAI 兼容)
|
||||
# 依赖: http 服务 (查询), config 服务 (查询)
|
||||
# ============================================================
|
||||
|
||||
add_library(plugin-deepseek SHARED
|
||||
src/deepseek_plugin.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-deepseek PRIVATE dstalk)
|
||||
|
||||
# Boost.JSON 用于构建/解析请求和响应
|
||||
find_package(Boost REQUIRED CONFIG)
|
||||
target_link_libraries(plugin-deepseek PRIVATE boost::boost dstalk_boost_config)
|
||||
|
||||
set_target_properties(plugin-deepseek PROPERTIES
|
||||
PREFIX ""
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/plugins"
|
||||
)
|
||||
20
plugins/openai/CMakeLists.txt
Normal file
20
plugins/openai/CMakeLists.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
# ============================================================
|
||||
# plugin-openai — OpenAI 兼容 AI 服务 / OpenAI-compatible AI service
|
||||
# ============================================================
|
||||
|
||||
find_package(Boost REQUIRED CONFIG)
|
||||
|
||||
add_library(plugin-openai SHARED
|
||||
src/openai_plugin.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-openai PRIVATE dstalk)
|
||||
|
||||
# Boost.JSON (header-only)
|
||||
find_package(Boost REQUIRED CONFIG)
|
||||
target_link_libraries(plugin-openai PRIVATE boost::boost dstalk_boost_config)
|
||||
|
||||
set_target_properties(plugin-openai PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins
|
||||
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* @file deepseek_plugin.cpp
|
||||
* @brief DeepSeek/OpenAI-compatible AI provider plugin with SSE streaming and tool calls.
|
||||
* @file openai_plugin.cpp
|
||||
* @brief OpenAI-compatible AI provider plugin with SSE streaming and tool calls.
|
||||
* DeepSeek/OpenAI 兼容 AI 提供者插件,支持 SSE 流式输出和工具调用。
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*/
|
||||
@@ -351,18 +351,18 @@ static int my_configure(const char* provider, const char* base_url,
|
||||
}
|
||||
|
||||
host->log(DSTALK_LOG_INFO,
|
||||
"[deepseek] configured: model=%s base_url=%s max_tokens=%d temperature=%.2f",
|
||||
"[openai] configured: model=%s base_url=%s max_tokens=%d temperature=%.2f",
|
||||
g_cfg.model.c_str(), g_cfg.base_url.c_str(),
|
||||
g_cfg.max_tokens, g_cfg.temperature);
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[deepseek] my_configure exception: %s", e.what());
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_configure exception: %s", e.what());
|
||||
return -1;
|
||||
} catch (...) {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[deepseek] my_configure unknown exception");
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_configure unknown exception");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -417,14 +417,14 @@ static dstalk_chat_result_t my_chat(
|
||||
return r;
|
||||
} catch (const std::exception& e) {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[deepseek] my_chat exception: %s", e.what());
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_chat exception: %s", e.what());
|
||||
dstalk_chat_result_t r = {};
|
||||
r.ok = 0;
|
||||
r.error = host ? host->strdup(e.what()) : nullptr;
|
||||
return r;
|
||||
} catch (...) {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[deepseek] my_chat unknown exception");
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_chat unknown exception");
|
||||
dstalk_chat_result_t r = {};
|
||||
r.ok = 0;
|
||||
r.error = host ? host->strdup("unknown exception") : nullptr;
|
||||
@@ -458,11 +458,11 @@ static int sse_line_callback(const char* line, void* userdata)
|
||||
return 1; // 继续 / continue
|
||||
} catch (const std::exception& e) {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[deepseek] sse_line_callback exception: %s", e.what());
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] sse_line_callback exception: %s", e.what());
|
||||
return 0;
|
||||
} catch (...) {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[deepseek] sse_line_callback unknown exception");
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] sse_line_callback unknown exception");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -580,14 +580,14 @@ static dstalk_chat_result_t my_chat_stream(
|
||||
return r;
|
||||
} catch (const std::exception& e) {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[deepseek] my_chat_stream exception: %s", e.what());
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_chat_stream exception: %s", e.what());
|
||||
dstalk_chat_result_t r = {};
|
||||
r.ok = 0;
|
||||
r.error = host ? host->strdup(e.what()) : nullptr;
|
||||
return r;
|
||||
} catch (...) {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[deepseek] my_chat_stream unknown exception");
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] my_chat_stream unknown exception");
|
||||
dstalk_chat_result_t r = {};
|
||||
r.ok = 0;
|
||||
r.error = host ? host->strdup("unknown exception") : nullptr;
|
||||
@@ -621,7 +621,7 @@ static dstalk_ai_service_t g_service = {
|
||||
// ============================================================================
|
||||
// 生命周期 / Lifecycle
|
||||
// ============================================================================
|
||||
// 插件初始化:查询 http 和 config 服务,注册 ai.deepseek 服务 / Plugin init: query http and config services, register ai.deepseek service.
|
||||
// 插件初始化:查询 http 和 config 服务,注册 ai.openai 服务 / Plugin init: query http and config services, register ai.openai service.
|
||||
static int on_init(const dstalk_host_api_t* host)
|
||||
{
|
||||
try {
|
||||
@@ -632,20 +632,20 @@ static int on_init(const dstalk_host_api_t* host)
|
||||
g_config.store(cfg, std::memory_order_release);
|
||||
|
||||
if (!http) {
|
||||
if (host) host->log(DSTALK_LOG_ERROR, "[deepseek] http service not found");
|
||||
if (host) host->log(DSTALK_LOG_ERROR, "[openai] http service not found");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (host) host->log(DSTALK_LOG_INFO, "[deepseek] initializing DeepSeek AI plugin");
|
||||
if (host) host->log(DSTALK_LOG_INFO, "[openai] initializing OpenAI-compatible AI plugin");
|
||||
|
||||
return host->register_service("ai.deepseek", 1, &g_service);
|
||||
return host->register_service("ai.openai", 1, &g_service);
|
||||
} catch (const std::exception& e) {
|
||||
const dstalk_host_api_t* h = g_host.load(std::memory_order_acquire);
|
||||
if (h && h->log) h->log(DSTALK_LOG_ERROR, "[deepseek] on_init exception: %s", e.what());
|
||||
if (h && h->log) h->log(DSTALK_LOG_ERROR, "[openai] on_init exception: %s", e.what());
|
||||
return -1;
|
||||
} catch (...) {
|
||||
const dstalk_host_api_t* h = g_host.load(std::memory_order_acquire);
|
||||
if (h && h->log) h->log(DSTALK_LOG_ERROR, "[deepseek] on_init unknown exception");
|
||||
if (h && h->log) h->log(DSTALK_LOG_ERROR, "[openai] on_init unknown exception");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -655,7 +655,7 @@ static void on_shutdown()
|
||||
{
|
||||
try {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host) host->log(DSTALK_LOG_INFO, "[deepseek] shutdown");
|
||||
if (host) host->log(DSTALK_LOG_INFO, "[openai] shutdown");
|
||||
secure_zero(g_cfg.api_key.data(), g_cfg.api_key.size());
|
||||
g_cfg.api_key.clear();
|
||||
g_http.store(nullptr, std::memory_order_release);
|
||||
@@ -663,10 +663,10 @@ static void on_shutdown()
|
||||
g_host.store(nullptr, std::memory_order_release);
|
||||
} catch (const std::exception& e) {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[deepseek] on_shutdown exception: %s", e.what());
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] on_shutdown exception: %s", e.what());
|
||||
} catch (...) {
|
||||
const dstalk_host_api_t* host = g_host.load(std::memory_order_acquire);
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[deepseek] on_shutdown unknown exception");
|
||||
if (host && host->log) host->log(DSTALK_LOG_ERROR, "[openai] on_shutdown unknown exception");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,9 +674,9 @@ static void on_shutdown()
|
||||
// 插件描述符 / Plugin descriptor
|
||||
// ============================================================================
|
||||
static dstalk_plugin_info_t g_info = {
|
||||
/* .name = */ "deepseek-ai",
|
||||
/* .name = */ "openai-compat",
|
||||
/* .version = */ "1.0.0",
|
||||
/* .description = */ "DeepSeek AI provider (OpenAI-compatible API) / DeepSeek AI 提供者 (OpenAI 兼容 API)",
|
||||
/* .description = */ "OpenAI-compatible AI provider (OpenAI-compatible API) / OpenAI-compatible AI 提供者 (OpenAI 兼容 API)",
|
||||
/* .api_version = */ DSTALK_API_VERSION,
|
||||
/* .dependencies = */ { "http", "config", NULL },
|
||||
/* .on_init = */ on_init,
|
||||
@@ -152,31 +152,31 @@ target_link_libraries(dstalk-anthropic-plugin-test
|
||||
add_test(NAME dstalk-anthropic-plugin-test COMMAND dstalk-anthropic-plugin-test)
|
||||
|
||||
# ============================================================
|
||||
# dstalk-deepseek-plugin-test — DeepSeek AI 插件单元测试
|
||||
# dstalk-openai-plugin-test — OpenAI 兼容 AI 插件单元测试
|
||||
# W21.6 (qa-wang): 通过 #include source 访问 static 函数
|
||||
# ============================================================
|
||||
|
||||
add_executable(dstalk-deepseek-plugin-test
|
||||
deepseek_plugin_test.cpp
|
||||
add_executable(dstalk-openai-plugin-test
|
||||
openai_plugin_test.cpp
|
||||
)
|
||||
|
||||
target_include_directories(dstalk-deepseek-plugin-test
|
||||
target_include_directories(dstalk-openai-plugin-test
|
||||
PRIVATE ${CMAKE_SOURCE_DIR}/dstalk-core/include
|
||||
)
|
||||
|
||||
target_compile_definitions(dstalk-deepseek-plugin-test
|
||||
target_compile_definitions(dstalk-openai-plugin-test
|
||||
PRIVATE
|
||||
BOOST_JSON_HEADER_ONLY
|
||||
BOOST_ALL_NO_LIB
|
||||
)
|
||||
|
||||
target_link_libraries(dstalk-deepseek-plugin-test
|
||||
target_link_libraries(dstalk-openai-plugin-test
|
||||
PRIVATE
|
||||
dstalk
|
||||
boost::boost
|
||||
)
|
||||
|
||||
add_test(NAME dstalk-deepseek-plugin-test COMMAND dstalk-deepseek-plugin-test)
|
||||
add_test(NAME dstalk-openai-plugin-test COMMAND dstalk-openai-plugin-test)
|
||||
|
||||
# ============================================================
|
||||
# dstalk-network-plugin-test — Network 插件单元测试
|
||||
|
||||
@@ -40,10 +40,10 @@ int main()
|
||||
{
|
||||
std::ofstream config(config_path);
|
||||
config << "[api]\n"
|
||||
<< "provider = \"deepseek\"\n"
|
||||
<< "base_url = \"https://api.deepseek.com/v1\"\n"
|
||||
<< "provider = \"openai\"\n"
|
||||
<< "base_url = \"https://api.openai.com/v1\"\n"
|
||||
<< "api_key = \"test-key\"\n"
|
||||
<< "model = \"deepseek-v4-pro\"\n";
|
||||
<< "model = \"gpt-4o\"\n";
|
||||
}
|
||||
|
||||
if (dstalk_init(config_path.string().c_str()) != 0) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* @file deepseek_plugin_test.cpp
|
||||
* @brief DeepSeek AI plugin unit tests: SSE parsing (parse_sse_line edge cases),
|
||||
* @file openai_plugin_test.cpp
|
||||
* @brief OpenAI-compatible AI plugin unit tests: SSE parsing (parse_sse_line edge cases),
|
||||
* [DONE] sentinel matching, tool_calls delta extraction, request building,
|
||||
* append_history, extract_host_port, secure_zero, and null-safety.
|
||||
* DeepSeek AI 插件单元测试:SSE 解析(parse_sse_line 边界情况)、[DONE] 标记匹配、
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
#define BOOST_JSON_HEADER_ONLY
|
||||
#define BOOST_ALL_NO_LIB
|
||||
#include "../plugins/deepseek/src/deepseek_plugin.cpp"
|
||||
#include "../plugins/openai/src/openai_plugin.cpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
@@ -30,10 +30,10 @@ static int g_failures = 0;
|
||||
// Test helper: populate g_cfg with valid deepseek defaults before build_* tests
|
||||
// 测试辅助函数:为 build_* 测试填充 g_cfg 的有效 deepseek 默认值
|
||||
static void setup_config() {
|
||||
g_cfg.provider = "deepseek";
|
||||
g_cfg.base_url = "https://api.deepseek.com/v1";
|
||||
g_cfg.provider = "openai";
|
||||
g_cfg.base_url = "https://api.openai.com/v1";
|
||||
g_cfg.api_key = "sk-ds-test-key-67890";
|
||||
g_cfg.model = "deepseek-v4-pro";
|
||||
g_cfg.model = "gpt-4o";
|
||||
g_cfg.max_tokens = 4096;
|
||||
g_cfg.temperature = 0.7;
|
||||
}
|
||||
@@ -243,7 +243,7 @@ int main()
|
||||
"data: {\"id\":\"chatcmpl-xxx\","
|
||||
"\"object\":\"chat.completion.chunk\","
|
||||
"\"created\":1712345678,"
|
||||
"\"model\":\"deepseek-v4-pro\","
|
||||
"\"model\":\"gpt-4o\","
|
||||
"\"choices\":[{\"index\":0,"
|
||||
"\"delta\":{\"content\":\" World\"},"
|
||||
"\"finish_reason\":null}]}";
|
||||
@@ -368,7 +368,7 @@ int main()
|
||||
"T5.4: contains user input");
|
||||
CHECK(json.find("\"stream\":false") != std::string::npos,
|
||||
"T5.5: stream=false present");
|
||||
CHECK(json.find("\"model\":\"deepseek-v4-pro\"")
|
||||
CHECK(json.find("\"model\":\"gpt-4o\"")
|
||||
!= std::string::npos,
|
||||
"T5.6: model field present");
|
||||
CHECK(json.find("\"max_tokens\":4096") != std::string::npos,
|
||||
@@ -521,11 +521,11 @@ int main()
|
||||
{
|
||||
std::string scheme, host, port, target;
|
||||
bool ret = extract_host_port(
|
||||
"https://api.deepseek.com/v1/chat/completions",
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
scheme, host, port, target);
|
||||
CHECK(ret, "T8.1: valid HTTPS URL returns true");
|
||||
CHECK(scheme == "https", "T8.2: scheme is 'https'");
|
||||
CHECK(host == "api.deepseek.com", "T8.3: host extracted");
|
||||
CHECK(host == "api.openai.com", "T8.3: host extracted");
|
||||
CHECK(port == "443", "T8.4: default HTTPS port 443");
|
||||
CHECK(target == "/v1/chat/completions", "T8.5: target path extracted");
|
||||
}
|
||||
@@ -552,7 +552,7 @@ int main()
|
||||
{
|
||||
std::string scheme, host, port, target;
|
||||
bool ret = extract_host_port(
|
||||
"https://api.deepseek.com",
|
||||
"https://api.openai.com",
|
||||
scheme, host, port, target);
|
||||
CHECK(ret, "T8.11: URL without path returns true");
|
||||
CHECK(target == "/", "T8.12: target defaults to '/'");
|
||||
@@ -683,10 +683,10 @@ int main()
|
||||
|
||||
{
|
||||
int ret = my_configure(
|
||||
"deepseek", "https://api.deepseek.com/v1",
|
||||
"sk-key", "deepseek-v4", 2048, 0.5);
|
||||
"openai", "https://api.openai.com/v1",
|
||||
"sk-key", "gpt-4o", 2048, 0.5);
|
||||
CHECK(ret == 0, "T12.1: my_configure returns 0 with null host");
|
||||
CHECK(g_cfg.provider == "deepseek", "T12.2: provider stored");
|
||||
CHECK(g_cfg.provider == "openai", "T12.2: provider stored");
|
||||
CHECK(g_cfg.max_tokens == 2048, "T12.3: max_tokens stored");
|
||||
CHECK(g_cfg.temperature == 0.5, "T12.4: temperature stored");
|
||||
}
|
||||
@@ -701,7 +701,7 @@ int main()
|
||||
// ================================================================
|
||||
std::cout << "\n";
|
||||
if (g_failures == 0) {
|
||||
std::cout << "=== All deepseek plugin tests passed ===\n";
|
||||
std::cout << "=== All openai plugin tests passed ===\n";
|
||||
return 0;
|
||||
} else {
|
||||
std::cerr << "=== " << g_failures << " test(s) FAILED ===\n";
|
||||
@@ -52,10 +52,10 @@ int main()
|
||||
{
|
||||
std::ofstream config(config_path);
|
||||
config << "[api]\n"
|
||||
<< "provider = \"deepseek\"\n"
|
||||
<< "base_url = \"https://api.deepseek.com/v1\"\n"
|
||||
<< "provider = \"openai\"\n"
|
||||
<< "base_url = \"https://api.openai.com/v1\"\n"
|
||||
<< "api_key = \"test-key\"\n"
|
||||
<< "model = \"deepseek-v4-pro\"\n";
|
||||
<< "model = \"gpt-4o\"\n";
|
||||
}
|
||||
|
||||
// 初始化主机(会自动扫描 plugins/ 加载插件)/ Init host (auto-scans plugins/ to load plugins)
|
||||
@@ -186,7 +186,7 @@ int main()
|
||||
// 测试服务查询: ai(可能因为没有真实 API key 而失败,但服务应存在)
|
||||
// Test service query: ai (may fail without real API key, but service should exist)
|
||||
const char* ai_provider = dstalk_config_get("ai.provider");
|
||||
if (!ai_provider) ai_provider = "ai.deepseek";
|
||||
if (!ai_provider) ai_provider = "ai.openai";
|
||||
auto* ai = static_cast<const dstalk_ai_service_t*>(
|
||||
dstalk_service_query(ai_provider, 1));
|
||||
if (ai) {
|
||||
@@ -598,12 +598,12 @@ int main()
|
||||
std::cout << "[OK] R3: http error path, no response body (connection refused)\n";
|
||||
}
|
||||
} else {
|
||||
// 回退:测 AI 服务 (ai.deepseek) 错误路径
|
||||
// Fallback: test AI service (ai.deepseek) error path
|
||||
// 回退:测 AI 服务 (ai.openai) 错误路径
|
||||
// Fallback: test AI service (ai.openai) error path
|
||||
auto* ai_svc = static_cast<const dstalk_ai_service_t*>(
|
||||
dstalk_service_query("ai.deepseek", 1));
|
||||
dstalk_service_query("ai.openai", 1));
|
||||
if (ai_svc) {
|
||||
std::cout << "[OK] R3: ai.deepseek service found (http fallback)\n";
|
||||
std::cout << "[OK] R3: ai.openai service found (http fallback)\n";
|
||||
dstalk_message_t msg = {"user", "hi", nullptr, nullptr};
|
||||
dstalk_chat_result_t r = ai_svc->chat(&msg, 1, "", nullptr);
|
||||
// api_key="test-key" 为无效 key,应返回 error result 而非崩溃
|
||||
@@ -748,10 +748,10 @@ int main()
|
||||
{
|
||||
std::ofstream c(config_path);
|
||||
c << "[api]\n"
|
||||
<< "provider = \"deepseek\"\n"
|
||||
<< "base_url = \"https://api.deepseek.com/v1\"\n"
|
||||
<< "provider = \"openai\"\n"
|
||||
<< "base_url = \"https://api.openai.com/v1\"\n"
|
||||
<< "api_key = \"test-key\"\n"
|
||||
<< "model = \"deepseek-v4-pro\"\n";
|
||||
<< "model = \"gpt-4o\"\n";
|
||||
}
|
||||
|
||||
std::cout << "[R4] cycle " << (i + 1) << "/" << cycles << "\n";
|
||||
|
||||
76
模块目录和功能说明.md
76
模块目录和功能说明.md
@@ -0,0 +1,76 @@
|
||||
# dstalk 模块目录和功能说明
|
||||
|
||||
## 一、核心网关模块
|
||||
|
||||
| 目录 | 功能说明 |
|
||||
|------|---------|
|
||||
| `dstalk-core/` | 核心网关库(dstalk.dll / libdstalk.so),提供插件加载、服务注册、事件总线、配置存储、日志、内存管理 |
|
||||
|
||||
## 二、前端模块
|
||||
|
||||
| 目录 | 功能说明 |
|
||||
|------|---------|
|
||||
| `dstalk-cli/` | 命令行前端:ANSI 终端交互、命令解析、流式对话、工具调用、批处理/管道模式 |
|
||||
| `dstalk-gui/` | 图形界面前端:SDL3 窗口化界面、暗色主题、流式对话(默认关闭) |
|
||||
| `dstalk-web/` | Web 前端:Boost.Beast HTTP + SSE 流式推送、内嵌 HTML/CSS/JS 聊天界面(默认关闭) |
|
||||
|
||||
## 三、插件模块
|
||||
|
||||
所有插件位于 `plugins/` 目录下,通过 C ABI 与核心网关通信。
|
||||
|
||||
### 3.1 基础插件(无依赖)
|
||||
|
||||
不依赖任何其他插件,可独立加载运行。
|
||||
|
||||
| 目录 | 服务名 | 功能说明 |
|
||||
|------|--------|---------|
|
||||
| `plugins/config/` | `"config"` | TOML 配置文件解析、键值读写 |
|
||||
| `plugins/file-io/` | `"file_io"` | 文件读写服务 |
|
||||
| `plugins/lsp/` | `"lsp"` | 语言服务器协议 JSON-RPC 客户端(诊断、悬停、补全),自行管理子进程 |
|
||||
|
||||
### 3.2 只依赖基础插件的插件
|
||||
|
||||
仅依赖 3.1 中的基础插件,不依赖同级或上层插件。
|
||||
|
||||
| 目录 | 服务名 | 功能说明 | 依赖 |
|
||||
|------|--------|---------|------|
|
||||
| `plugins/network/` | `"http"` | HTTP/HTTPS POST 和流式请求(Boost.Beast + OpenSSL) | `config` |
|
||||
| `plugins/session/` | `"session"` | 会话消息历史管理、保存/加载、Token 计数 | `file_io` |
|
||||
| `plugins/tools/` | `"tools"` | 工具注册、Schema 管理、执行分发(内置 file_read/file_write) | `file_io` |
|
||||
|
||||
### 3.3 依赖其他插件的插件
|
||||
|
||||
依赖 3.2 或更多层级的插件。
|
||||
|
||||
| 目录 | 服务名 | 功能说明 | 依赖 |
|
||||
|------|--------|---------|------|
|
||||
| `plugins/context/` | `"context"` | Token 计数(UTF-8 字节估算)、上下文窗口裁剪 | `session` |
|
||||
| `plugins/openai/` | `"ai.openai"` | OpenAI 兼容格式 AI 接入(chat/stream/tools、SSE 解析) | `http`、`config` |
|
||||
| `plugins/anthropic/` | `"ai.anthropic"` | Anthropic Claude Messages API 接入(chat/stream、SSE 解析) | `http`、`config` |
|
||||
|
||||
### 3.4 规划中的插件(尚未实现)
|
||||
|
||||
| 模块 | 服务名(建议) | 功能说明 | 分类 |
|
||||
|------|---------------|---------|------|
|
||||
| Anthropic↔OpenAI 格式转换 | `"format.convert"` | 双向请求/响应/SSE 流格式转换 | 依赖其他插件 |
|
||||
| AI 自动识别接口 | `"ai.auto"` | 自动检测可用 AI 提供者并统一路由 | 依赖其他插件 |
|
||||
| 数据库 | `"database"` | 数据库连接和查询服务 | 基础插件 |
|
||||
|
||||
## 四、插件依赖关系图
|
||||
|
||||
```
|
||||
第一层(基础插件,无依赖):
|
||||
config file_io lsp
|
||||
|
||||
第二层(只依赖基础插件):
|
||||
network ← config
|
||||
session ← file_io
|
||||
tools ← file_io
|
||||
|
||||
第三层(依赖其他插件):
|
||||
context ← session
|
||||
openai ← http, config
|
||||
anthropic ← http, config
|
||||
```
|
||||
|
||||
加载顺序由 Kahn 拓扑排序自动计算。
|
||||
|
||||
138
编码规范和命名规范.md
Normal file
138
编码规范和命名规范.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# dstalk 编码规范和命名规范
|
||||
|
||||
## 一、编程语言
|
||||
|
||||
- 核心库和插件:C11 / C++20
|
||||
- 插件边界:纯 C ABI(`extern "C"`),禁止跨 DLL 传递 C++ 对象
|
||||
- 构建系统:CMake 3.21+,Ninja 生成器
|
||||
|
||||
## 二、注释规范
|
||||
|
||||
### 2.1 文件头注释(必须)
|
||||
|
||||
每个源文件(`.cpp` / `.hpp` / `.h`)开头必须有文件头注释,格式如下:
|
||||
|
||||
```cpp
|
||||
/*
|
||||
* @file 文件名.cpp
|
||||
* @brief 英文简述。
|
||||
* 中文简述。
|
||||
* @version 0.1.1
|
||||
* @date 2026-06-01
|
||||
* Copyright (c) 2026 dstalk contributors. GPLv3.
|
||||
*
|
||||
* @changelog
|
||||
* - 2026-06-01 v0.1.1: 修复线程安全问题 / Fix thread-safety issue
|
||||
* - 2026-05-31 v0.1.0: 初始版本 / Initial version
|
||||
*/
|
||||
```
|
||||
|
||||
**规则说明**:
|
||||
|
||||
- `@version`: 当前文件版本号,必须与 `@changelog` 最新条目一致
|
||||
- `@date`: 最后修改日期,与 `@version` 同步更新
|
||||
- `@changelog`: **每次修改代码时,必须在最上方新增一行**,格式为 `- 日期 版本: 中文说明 / English description`
|
||||
- 最新变更始终在第一行,方便快速查看文件当前版本和最近改动
|
||||
- 仅记录功能性变更(bug修复、新功能、重构等),纯注释或格式调整不需要记录
|
||||
- 版本号递增规则:修订号用于 bugfix,次版本号用于新功能,主版本号用于不兼容变更
|
||||
|
||||
### 2.2 函数注释(必须)
|
||||
|
||||
每个函数/方法定义上方必须有中英双语注释,格式:
|
||||
|
||||
```cpp
|
||||
// 从 TOML 文件加载键值对 / Load key-value pairs from a TOML file
|
||||
int ConfigStore::load_file(const char* path)
|
||||
```
|
||||
|
||||
### 2.3 行内注释
|
||||
|
||||
- 复杂逻辑分块必须有中英双语注释
|
||||
- 格式:`// 中文说明 / English description`
|
||||
- 简单代码不需要注释
|
||||
|
||||
### 2.4 段落注释
|
||||
|
||||
代码段落分隔使用:
|
||||
|
||||
```cpp
|
||||
// === 中文标题 / English Title ===
|
||||
```
|
||||
|
||||
### 2.5 版本变更
|
||||
|
||||
文件头的 `@brief` 行简述当前功能即可,详细变更历史通过 git log 追溯,不在文件内维护 changelog。
|
||||
|
||||
## 三、命名规范
|
||||
|
||||
### 3.1 文件和目录
|
||||
|
||||
| 类型 | 规则 | 示例 |
|
||||
|------|------|------|
|
||||
| 目录名 | 小写英文 + 数字 + 下划线/连字符,字母或下划线开头 | `dstalk-core`、`file-io`、`plugins` |
|
||||
| 源文件 | 小写英文 + 下划线,字母开头 | `config_store.cpp`、`openai_plugin.cpp` |
|
||||
| 头文件 | 同源文件规则 | `dstalk_host.h`、`event_bus.hpp` |
|
||||
| 测试文件 | `<模块名>_test.cpp` | `event_bus_test.cpp`、`openai_plugin_test.cpp` |
|
||||
|
||||
### 3.2 C 层(公共 API,跨 DLL)
|
||||
|
||||
| 类型 | 规则 | 示例 |
|
||||
|------|------|------|
|
||||
| 函数 | `dstalk_` 前缀 + snake_case | `dstalk_init()`、`dstalk_service_query()` |
|
||||
| 类型 | `dstalk_` 前缀 + snake_case + `_t` 后缀 | `dstalk_message_t`、`dstalk_ai_service_t` |
|
||||
| 宏 | `DSTALK_` 前缀 + UPPER_SNAKE_CASE | `DSTALK_API`、`DSTALK_API_VERSION` |
|
||||
| 枚举值 | `DSTALK_` 前缀 + UPPER_SNAKE_CASE | `DSTALK_LOG_ERROR`、`DSTALK_EVENT_MESSAGE` |
|
||||
| 回调类型 | `dstalk_` 前缀 + snake_case + `_fn` / `_cb` 后缀 | `dstalk_stream_cb`、`dstalk_tool_handler_fn` |
|
||||
|
||||
### 3.3 C++ 层(内部实现)
|
||||
|
||||
| 类型 | 规则 | 示例 |
|
||||
|------|------|------|
|
||||
| 命名空间 | 小写单词 | `dstalk` |
|
||||
| 类名 | PascalCase | `ConfigStore`、`EventBus`、`PluginLoader` |
|
||||
| 成员变量 | snake_case + `_` 后缀 | `mutex_`、`data_`、`next_id_` |
|
||||
| 局部变量 | snake_case | `history_count`、`plugin_dir` |
|
||||
| 全局变量 | `g_` 前缀 + snake_case | `g_ai`、`g_session`、`g_quit_requested` |
|
||||
| 常量 | `k` 前缀 + PascalCase | `kWebUiHtml` |
|
||||
|
||||
### 3.4 插件命名
|
||||
|
||||
| 类型 | 规则 | 示例 |
|
||||
|------|------|------|
|
||||
| 插件目录 | 功能名,小写 + 连字符 | `plugins/openai/`、`plugins/file-io/` |
|
||||
| 插件源文件 | `<功能名>_plugin.cpp` | `openai_plugin.cpp`、`session_plugin.cpp` |
|
||||
| CMake 目标 | `plugin-<功能名>` | `plugin-openai`、`plugin-network` |
|
||||
| 服务注册名 | 小写 + 点号分级 | `"ai.openai"`、`"http"`、`"session"` |
|
||||
| 插件入口函数 | 固定为 `dstalk_plugin_init` | — |
|
||||
|
||||
### 3.5 禁止项
|
||||
|
||||
- 禁止纯数字命名(如 `123.cpp`)
|
||||
- 禁止中文或特殊字符出现在文件名、变量名中
|
||||
- 禁止在 C ABI 函数中抛出 C++ 异常(必须 try/catch 包裹)
|
||||
- 禁止跨 DLL 边界传递 `std::string` 或其他 C++ 类型
|
||||
|
||||
## 四、代码组织
|
||||
|
||||
### 4.1 目录 README
|
||||
|
||||
- `dstalk/`(根目录)和所有二级目录(`dstalk-core/`、`dstalk-cli/`、`plugins/` 等)必须有 `README.md`
|
||||
- 三级目录(如 `plugins/openai/`)如内容简单可暂不添加
|
||||
|
||||
### 4.2 include 路径
|
||||
|
||||
- 公共头文件放在 `dstalk-core/include/dstalk/` 下
|
||||
- 私有实现头文件放在 `dstalk-core/src/` 下
|
||||
- 插件不得被 core 反向依赖(core 禁止 `#include` 插件目录下的文件)
|
||||
|
||||
### 4.3 内存管理
|
||||
|
||||
- 跨 DLL 的内存分配必须通过 `host->alloc` / `host->free` / `host->strdup`
|
||||
- 禁止在一个 DLL 中 `malloc`,在另一个 DLL 中 `free`
|
||||
- `dstalk_chat_result_t` 中的字符串字段由 `dstalk_strdup` 分配,调用方通过 `free_result()` 释放
|
||||
|
||||
## 五、构建规范
|
||||
|
||||
- 所有目标输出到 `${CMAKE_BINARY_DIR}/bin`(可执行文件)或 `${CMAKE_BINARY_DIR}/plugins`(插件 DLL)
|
||||
- 新前端通过 `option(DSTALK_BUILD_XXX)` 控制,默认 OFF
|
||||
- 插件在 `plugins/CMakeLists.txt` 中按依赖顺序 `add_subdirectory`
|
||||
Reference in New Issue
Block a user