Wave 5 (9 parallel agents): - W1.1 atomic diag callback + DLL handle release on shutdown (lin) - W2.1 unify cross-DLL heap discipline (host->alloc/free/strdup) (chen) - W2.2 secure_zero api_key on shutdown for deepseek/anthropic (cao) - W3 CMake modernization: target-based cxx_std_20, dstalk_boost_config INTERFACE lib, root-level RUNTIME_OUTPUT_DIRECTORY (hu) - W4 GitHub Actions CI with dynamic Linux/Windows matrix (ma) - W5.1 SSE buffer_body to cut peak memory ~67% on 32K streams (zhou) - W6.1 LSP JSON-RPC frame parser hardened against header reordering (sun) - W7 smoke test: copy plugin DLLs post-build + Boost.JSON src.hpp fix for full 9-plugin load coverage (wang) - W8.1 README slimmed 398->92, Diataxis docs/ skeleton (deng) Wave 6 (6 parallel agents): - W9.1 docs/explanation: architecture + plugin-lifecycle (deng) - W9.3 log credential leak audit (0 vulns, audit trail in docs/explanation/security-logging.md) (cao) - W9.4 docs/reference/plugin-abi.md - 7-point ABI contract (lin) - W9.6 CLI /history command + status integration (zhao) - W9.8 plugin_loader fault tolerance: per-plugin failure no longer aborts dstalk_init (huang) - W9.10 host_api unit tests: tests/host_api_test.cpp, 8 cases (liu) CEO oversight (preexisting bugs fixed during Wave 5 verification): - lsp_plugin.cpp:449 forward decl mismatch (int vs void) - tools_plugin.cpp:109 missing forward decl Multi-agent collaboration framework: - agents/WORKFLOW.md: 6-stage protocol, two-tier governance, prompt template, technical constraints registry Build: cmake --build 0 error / 0 warning. Tests: 2/2 100% pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
139 lines
7.3 KiB
Markdown
139 lines
7.3 KiB
Markdown
# dstalk 架构哲学
|
||
|
||
> **本文属于**: Explanation -- 解释"为什么这么设计",建立心智模型。
|
||
> 不涉及具体 API 签名或配置字段,那属于 Reference 的职责。
|
||
|
||
---
|
||
|
||
## 为什么是插件架构?
|
||
|
||
dstalk 选择的不是单体架构,而是**以 C ABI 为边界的插件架构**。这是几条需求推导出的必然结果:
|
||
|
||
1. **AI 后端会变**。DeepSeek / OpenAI / Anthropic 各有不同的 HTTP 协议细节和模型参数。今天用 A,明天切 B,后天同时挂两个。单体应用内硬编码所有后端会导致每次新增后端都要改核心、重新编译、重新测试。
|
||
|
||
2. **能力会增长**。LSP 集成、文件管理、会话持久化、工具调用——这些能力不是 CLI 启动时必须加载的。使用者可能只需要聊天,不需要 LSP。插件架构让能力按需加载,启动更快,内存更省。
|
||
|
||
3. **插件作者不是核心团队**。第三方应该能用自己的编译器、自己的 C++ 标准库版本编写插件,而不必须链接 dstalk-core 的静态库。这要求 ABI 稳定。C ABI 是唯一具有跨编译器二进制兼容性的选择。
|
||
|
||
**一句话心智模型**: 不要想象一个胖二进制把所有功能静态链接在一起;想象一个内核 (host) + 一圈可插拔的服务单元 (plugin),内核只负责编排,不负责实现。
|
||
|
||
---
|
||
|
||
## 三层模型
|
||
|
||
dstalk 的架构由 3 层组成。从上到下看,每一层依赖下一层提供的抽象:
|
||
|
||
```
|
||
┌────────────────────────────────────────────┐
|
||
│ Plugin (DLL) │
|
||
│ 实现具体能力:AI 聊天、文件读写、LSP... │
|
||
│ 通过服务注册表向其他插件暴露自己的功能 │
|
||
├────────────────────────────────────────────┤
|
||
│ Host (dstalk-core) │
|
||
│ 拥有事件总线、服务注册表、插件加载器、配置 │
|
||
│ 提供一个 dstalk_host_api_t 接口给所有插件 │
|
||
├────────────────────────────────────────────┤
|
||
│ Service (C vtable) │
|
||
│ 抽象接口:ai / http / session / file_io... │
|
||
│ 消费者通过 query_service 查找,不看实现者是谁│
|
||
└────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Host 层
|
||
|
||
Host 是 dstalk 的 **编排内核**。它自己不做 AI 推理,不读写文件,不管理会话。
|
||
|
||
Host 拥有四样东西:
|
||
|
||
- **配置存储 (ConfigStore)**: 管理 `config.toml` 的加载和键值查询。
|
||
- **事件总线 (EventBus)**: 插件间松耦合通信的唯一通道。
|
||
- **服务注册表 (ServiceRegistry)**: 按名称 + 版本号存储和查找服务 vtable。
|
||
- **插件加载器 (PluginLoader)**: 扫描 `plugins/` 目录、加载 DLL、按依赖拓扑排序后调用初始化。
|
||
|
||
Host 启动时,按严格顺序执行:
|
||
1. 分配上述四个组件。
|
||
2. 加载 `config.toml`(如果提供了路径)。
|
||
3. 扫描 `plugin_dir` 目录下所有 `.dll` / `.so` 文件。
|
||
4. 调用每个插件的 `dstalk_plugin_init()` 获取插件描述符。
|
||
5. 按依赖拓扑排序执行 `on_init`。
|
||
|
||
### Plugin 层
|
||
|
||
每个插件是一个**独立的动态库**,导出一个唯一入口函数 `dstalk_plugin_init()`。
|
||
|
||
该入口返回一个 `dstalk_plugin_info_t`,声明了:
|
||
- 插件名称(依赖解析时的唯一标识)。
|
||
- 依赖列表(必须先于本插件初始化的其他插件名)。
|
||
- 生命周期回调(`on_init` / `on_shutdown`)。
|
||
- 可选的事件回调(`on_event`)。
|
||
|
||
插件在 `on_init` 中做三件事:
|
||
1. 保存 `host_api` 指针,这是它此后访问一切 Host 能力的唯一通道。
|
||
2. 通过 `host_api->query_service` 查找它依赖的服务(例如 deepseek 插件查询 `http` 和 `config`)。
|
||
3. 通过 `host_api->register_service` 向注册表注册自己提供的服务 vtable。
|
||
|
||
关键设计:插件之间**不直接链接**。插件 A 不知道插件 B 是否在当前进程中。A 只对 Host 说"我需要名为 `http` 的服务",Host 从注册表里找出那个 vtable,把指针交给 A。
|
||
|
||
### Service 层
|
||
|
||
Service 是**纯 C vtable**——一个包含函数指针的结构体。以 `dstalk_ai_service_t` 为例:
|
||
|
||
```
|
||
struct dstalk_ai_service_t {
|
||
int (*configure)(...);
|
||
dstalk_chat_result_t (*chat)(...);
|
||
dstalk_chat_result_t (*chat_stream)(...);
|
||
void (*free_result)(dstalk_chat_result_t*);
|
||
};
|
||
```
|
||
|
||
每种服务都有一个预定义的 vtable 结构体(定义在 `dstalk_services.h`)。第三方也可以扩展自己的服务 vtable,版本号随注册一起提供,允许消费者做最低版本检查。
|
||
|
||
服务注册采用 **name + version** 两要素:
|
||
- 全局唯一名称(如 `"ai.deepseek"`、`"http"`、`"file_io"`)。
|
||
- 版本号(消费者可以要求 `min_version`)。
|
||
|
||
---
|
||
|
||
## Service Registry 解决什么问题?
|
||
|
||
核心问题是 **耦合方向**。如果不使用注册表,deepseek 插件就得直接知道 http 插件的符号,通过 `dlsym` 或头文件耦合。换成注册表后:
|
||
|
||
- 提供者说:"我注册一个名为 `http` 的 vtable"。
|
||
- 消费者说:"给我一个名为 `http`、版本 >= 1 的 vtable"。
|
||
- Host 做中间人查表。
|
||
|
||
**效果**: 你可以用任意方式实现 `http` 服务——用 libcurl、用 WinHTTP、用 mock——deepseek 插件一行代码都不需要改。它只依赖服务接口,不依赖服务实现。
|
||
|
||
---
|
||
|
||
## Event Bus 解决什么问题?
|
||
|
||
服务注册表解决的是**调用链**:"我需要你,你给我提供服务"。但架构里还有另一类通信需求——**通知链**:"某件事发生了,关心的人自行处理"。
|
||
|
||
举例:
|
||
- 会话清空了 (`DSTALK_EVENT_SESSION_CLEAR`)——所有关心会话状态的插件应当知道。
|
||
- 配置更新了 (`DSTALK_EVENT_CONFIG_CHANGED`)——读取了配置的插件应当刷新。
|
||
- 插件加载/卸载——其他插件可能需要重新查询依赖。
|
||
|
||
Event Bus 把这些需求抽象为**发布-订阅**:
|
||
- 发布者不知道谁在听。
|
||
- 订阅者不知道谁在发布。
|
||
- 发布者和订阅者通过事件类型编号耦合(如 `DSTALK_EVENT_SESSION_CLEAR = 2`)。
|
||
|
||
**效果**: 新写一个监控插件,只需 `event_subscribe` 即可收到所有感兴趣的事件,不需要修改任何现有代码。
|
||
|
||
---
|
||
|
||
## 为什么所有插件用 C ABI?
|
||
|
||
C++ 没有稳定的 ABI。不同编译器(MSVC / GCC / Clang)、不同编译器版本、不同标准库版本(libstdc++ vs libc++)之间,C++ 类的内存布局、虚函数表布局、异常展开机制都不保证兼容。
|
||
|
||
C ABI 的保证:
|
||
- 编译单元之间传递的只有**结构体**和**函数指针**。
|
||
- 结构体布局确定(`dstalk_plugin_info_t` 的字段顺序不会变)。
|
||
- 函数调用约定确定(`extern "C"` 关闭 name mangling)。
|
||
- 所有内存分配通过 Host 提供的 `alloc/free`,而非各自调用 `malloc/free`(不同 DLL 使用不同运行时库的 `malloc` 会崩溃)。
|
||
|
||
**一句话心智模型**: C ABI 是唯一能保证"用 MSVC 编译的插件在 GCC 编译的 host 中运行"的接口层。代价是只能传递纯数据结构和函数指针,不能使用异常、重载或模板跨 DLL 边界。
|