# dstalk 架构哲学 > **本文属于**: Explanation -- 解释"为什么这么设计",建立心智模型。 > 不涉及具体 API 签名或配置字段,那属于 Reference 的职责。 --- ## 为什么是插件架构? dstalk 选择的不是单体架构,而是**以 C ABI 为边界的插件架构**。这是几条需求推导出的必然结果: 1. **AI 后端会变**。OpenAI-compatible / 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_base/`、`plugins_middle/`、`plugins_upper/` 三层目录、加载 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` 查找它依赖的服务(例如 openai 插件查询 `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_openai"`、`"http"`、`"file_io"`)。 - 版本号(消费者可以要求 `min_version`)。 --- ## Service Registry 解决什么问题? 核心问题是 **耦合方向**。如果不使用注册表,openai 插件就得直接知道 http 插件的符号,通过 `dlsym` 或头文件耦合。换成注册表后: - 提供者说:"我注册一个名为 `http` 的 vtable"。 - 消费者说:"给我一个名为 `http`、版本 >= 1 的 vtable"。 - Host 做中间人查表。 **效果**: 你可以用任意方式实现 `http` 服务——用 libcurl、用 WinHTTP、用 mock——openai 插件一行代码都不需要改。它只依赖服务接口,不依赖服务实现。 --- ## 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 边界。