From f6cb51b40abfa8eab0edc8a84733a3572412283a Mon Sep 17 00:00:00 2001 From: XiuChengWu <732857315@qq.com> Date: Sun, 31 May 2026 00:51:59 +0800 Subject: [PATCH] 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. --- README.md | 6 +- docs/explanation/architecture.md | 10 +- docs/explanation/plugin-lifecycle.md | 10 +- docs/explanation/security-logging.md | 6 +- docs/reference/commands.md | 2 +- docs/reference/plugin-abi.md | 2 +- docs/tutorial/quick-start.md | 20 +-- dstalk-cli/src/main.cpp | 10 +- dstalk-core/include/dstalk/dstalk_services.h | 2 +- dstalk-gui/src/main.cpp | 4 +- dstalk-web/src/main.cpp | 6 +- plugins/CMakeLists.txt | 24 +-- plugins/deepseek/CMakeLists.txt | 22 --- plugins/openai/CMakeLists.txt | 20 +++ .../src/openai_plugin.cpp} | 44 +++--- tests/CMakeLists.txt | 14 +- tests/context_plugin_test.cpp | 6 +- ...plugin_test.cpp => openai_plugin_test.cpp} | 30 ++-- tests/smoke_test.cpp | 22 +-- 模块目录和功能说明.md | 76 ++++++++++ 编码规范和命名规范.md | 138 ++++++++++++++++++ 21 files changed, 343 insertions(+), 131 deletions(-) delete mode 100644 plugins/deepseek/CMakeLists.txt create mode 100644 plugins/openai/CMakeLists.txt rename plugins/{deepseek/src/deepseek_plugin.cpp => openai/src/openai_plugin.cpp} (93%) rename tests/{deepseek_plugin_test.cpp => openai_plugin_test.cpp} (97%) create mode 100644 编码规范和命名规范.md diff --git a/README.md b/README.md index 0efae0f..9683643 100644 --- a/README.md +++ b/README.md @@ -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` 一键切换。 diff --git a/docs/explanation/architecture.md b/docs/explanation/architecture.md index ac5dbc9..fb06424 100644 --- a/docs/explanation/architecture.md +++ b/docs/explanation/architecture.md @@ -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 插件一行代码都不需要改。它只依赖服务接口,不依赖服务实现。 --- diff --git a/docs/explanation/plugin-lifecycle.md b/docs/explanation/plugin-lifecycle.md index 2858ff0..2ea440c 100644 --- a/docs/explanation/plugin-lifecycle.md +++ b/docs/explanation/plugin-lifecycle.md @@ -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 句柄都被释放。 diff --git a/docs/explanation/security-logging.md b/docs/explanation/security-logging.md index 507b7c2..f07a48c 100644 --- a/docs/explanation/security-logging.md +++ b/docs/explanation/security-logging.md @@ -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 "}` 并传给 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 请求对象,从未进入日志管道。代码库对此攻击面防御充分,无需修改。 diff --git a/docs/reference/commands.md b/docs/reference/commands.md index dacf7da..b7c4835 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -13,7 +13,7 @@ dstalk 所有内置命令。在对话中直接输入 `/help` 或 `/h` 也可查 | `/clear` | — | 清空当前会话上下文 | `/clear` | | `/context` | — | 显示当前 Token 数和消息条数 | `/context` | | `/status` | — | 显示当前运行状态 (脱敏: 不打印完整 API Key) | `/status` | -| `/model ` | — | 切换 AI 模型 | `/model deepseek-v4-pro` | +| `/model ` | — | 切换 AI 模型 | `/model gpt-4o` | | `/file list [path]` | — | 列出目录内容, 不填 path 列出当前目录 | `/file list src/` | | `/file show ` | — | 查看文件内容 | `/file show main.cpp` | | `/file read ` | — | 读取文件内容 (同 `/file show`) | `/file read config.toml` | diff --git a/docs/reference/plugin-abi.md b/docs/reference/plugin-abi.md index 80add0e..b4c025c 100644 --- a/docs/reference/plugin-abi.md +++ b/docs/reference/plugin-abi.md @@ -110,7 +110,7 @@ Linux/macOS 通常共享 libc,但静态链接或不同 libc 版本时同样可 ### 4.2 重复注册 同一 `name` 不可重复注册:第二次调用返回 `-2`(`service_registry.cpp:13`)。插件应检查返回 -值,在共享服务名(如 `"ai.deepseek"`)的场景中避免冲突。 +值,在共享服务名(如 `"ai.openai"`)的场景中避免冲突。 ### 4.3 版本协商 diff --git a/docs/tutorial/quick-start.md b/docs/tutorial/quick-start.md index 01f91ed..ce8da84 100644 --- a/docs/tutorial/quick-start.md +++ b/docs/tutorial/quick-start.md @@ -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 内容。 ``` diff --git a/dstalk-cli/src/main.cpp b/dstalk-cli/src/main.cpp index b38f86d..df795f5 100644 --- a/dstalk-cli/src/main.cpp +++ b/dstalk-cli/src/main.cpp @@ -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(dstalk_service_query(ai_provider, 1)); g_session = static_cast(dstalk_service_query("session", 1)); g_file_io = static_cast(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 } diff --git a/dstalk-core/include/dstalk/dstalk_services.h b/dstalk-core/include/dstalk/dstalk_services.h index 292f9c6..a0b3b14 100644 --- a/dstalk-core/include/dstalk/dstalk_services.h +++ b/dstalk-core/include/dstalk/dstalk_services.h @@ -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, diff --git a/dstalk-gui/src/main.cpp b/dstalk-gui/src/main.cpp index 404a8f9..b6d6bbc 100644 --- a/dstalk-gui/src/main.cpp +++ b/dstalk-gui/src/main.cpp @@ -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(dstalk_service_query(ai_provider, 1)); g_session_svc = static_cast(dstalk_service_query("session", 1)); if (!g_ai_svc) dstalk_log(3, "AI service not found (check plugins directory)"); diff --git a/dstalk-web/src/main.cpp b/dstalk-web/src/main.cpp index ed542a8..e2f463b 100644 --- a/dstalk-web/src/main.cpp +++ b/dstalk-web/src/main.cpp @@ -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(dstalk_service_query(ai_provider, 1)); g_session = static_cast(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); } diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 386e243..2a4f7da 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -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 diff --git a/plugins/deepseek/CMakeLists.txt b/plugins/deepseek/CMakeLists.txt deleted file mode 100644 index 6be765a..0000000 --- a/plugins/deepseek/CMakeLists.txt +++ /dev/null @@ -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" -) diff --git a/plugins/openai/CMakeLists.txt b/plugins/openai/CMakeLists.txt new file mode 100644 index 0000000..9ce1216 --- /dev/null +++ b/plugins/openai/CMakeLists.txt @@ -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 +) diff --git a/plugins/deepseek/src/deepseek_plugin.cpp b/plugins/openai/src/openai_plugin.cpp similarity index 93% rename from plugins/deepseek/src/deepseek_plugin.cpp rename to plugins/openai/src/openai_plugin.cpp index 0abc6ab..3aff8d9 100644 --- a/plugins/deepseek/src/deepseek_plugin.cpp +++ b/plugins/openai/src/openai_plugin.cpp @@ -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, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f795d3d..b0c4d2d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 插件单元测试 diff --git a/tests/context_plugin_test.cpp b/tests/context_plugin_test.cpp index 14ac83d..606ea55 100644 --- a/tests/context_plugin_test.cpp +++ b/tests/context_plugin_test.cpp @@ -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) { diff --git a/tests/deepseek_plugin_test.cpp b/tests/openai_plugin_test.cpp similarity index 97% rename from tests/deepseek_plugin_test.cpp rename to tests/openai_plugin_test.cpp index 5a4afc0..90034e0 100644 --- a/tests/deepseek_plugin_test.cpp +++ b/tests/openai_plugin_test.cpp @@ -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 #include @@ -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"; diff --git a/tests/smoke_test.cpp b/tests/smoke_test.cpp index 90265ec..bc8de32 100644 --- a/tests/smoke_test.cpp +++ b/tests/smoke_test.cpp @@ -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( 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( - 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"; diff --git a/模块目录和功能说明.md b/模块目录和功能说明.md index e69de29..514b017 100644 --- a/模块目录和功能说明.md +++ b/模块目录和功能说明.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 拓扑排序自动计算。 diff --git a/编码规范和命名规范.md b/编码规范和命名规范.md new file mode 100644 index 0000000..cc4ac21 --- /dev/null +++ b/编码规范和命名规范.md @@ -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`