Wave 5+6: plugin ABI hardening, build modernization, ABI/security docs
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>
This commit is contained in:
75
.github/workflows/ci.yml
vendored
Normal file
75
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
# ── 动态矩阵 ──────────────────────────────────────────────
|
||||
# PR 仅跑 Ubuntu 节省 minutes;push master 跑全矩阵 Ubuntu + Windows
|
||||
matrix:
|
||||
name: Determine matrix
|
||||
runs-on: ubuntu-24.04
|
||||
outputs:
|
||||
os: ${{ steps.set-matrix.outputs.os }}
|
||||
steps:
|
||||
- id: set-matrix
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "push" ] && [ "${{ github.ref }}" = "refs/heads/master" ]; then
|
||||
echo "os=[\"ubuntu-24.04\",\"windows-2025\"]" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "os=[\"ubuntu-24.04\"]" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# ── 构建 & 测试 ───────────────────────────────────────────
|
||||
build:
|
||||
name: ${{ matrix.os }} / ${{ matrix.build_type }}
|
||||
needs: matrix
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ${{ fromJSON(needs.matrix.outputs.os) }}
|
||||
build_type: [Release]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Conan
|
||||
run: pip install conan
|
||||
|
||||
- name: Cache Conan
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.conan2
|
||||
~/.conan2/p
|
||||
key: ${{ runner.os }}-conan-${{ matrix.build_type }}-${{ hashFiles('deps/conanfile.txt') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-conan-${{ matrix.build_type }}-
|
||||
${{ runner.os }}-conan-
|
||||
|
||||
- name: Install Conan dependencies
|
||||
run: |
|
||||
conan profile detect --force
|
||||
conan install deps --build=missing -s build_type=${{ matrix.build_type }}
|
||||
|
||||
- name: Configure CMake
|
||||
run: cmake --preset conan-release
|
||||
|
||||
- name: Build
|
||||
run: cmake --build --preset conan-release --config ${{ matrix.build_type }}
|
||||
|
||||
- name: Test
|
||||
run: ctest --preset conan-release -C ${{ matrix.build_type }} --output-on-failure
|
||||
@@ -1,14 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.21)
|
||||
project(dstalk VERSION 0.1.0 LANGUAGES C CXX)
|
||||
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||
|
||||
set(CMAKE_C_STANDARD 11)
|
||||
if(MSVC_VERSION LESS 1920)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
else()
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
endif()
|
||||
set(CMAKE_C_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
option(DSTALK_BUILD_GUI "Build the SDL3 GUI frontend" OFF)
|
||||
option(DSTALK_BUILD_TESTS "Build dstalk tests" ON)
|
||||
|
||||
54
CMakePresets.json
Normal file
54
CMakePresets.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"version": 3,
|
||||
"vendor": {
|
||||
"conan": {}
|
||||
},
|
||||
"cmakeMinimumRequired": {
|
||||
"major": 3,
|
||||
"minor": 15,
|
||||
"patch": 0
|
||||
},
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "conan-release",
|
||||
"displayName": "'conan-release' config",
|
||||
"description": "'conan-release' configure using 'Ninja' generator",
|
||||
"generator": "Ninja",
|
||||
"cacheVariables": {
|
||||
"CMAKE_POLICY_DEFAULT_CMP0091": "NEW",
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"CMAKE_C_COMPILER": "cl",
|
||||
"CMAKE_CXX_COMPILER": "cl"
|
||||
},
|
||||
"toolset": {
|
||||
"value": "v145",
|
||||
"strategy": "external"
|
||||
},
|
||||
"architecture": {
|
||||
"value": "x64",
|
||||
"strategy": "external"
|
||||
},
|
||||
"toolchainFile": "generators\\conan_toolchain.cmake",
|
||||
"binaryDir": "E:\\Prj2026\\AIGen2026\\dstalk\\build\\build\\Release"
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{
|
||||
"name": "conan-release",
|
||||
"configurePreset": "conan-release",
|
||||
"jobs": 32
|
||||
}
|
||||
],
|
||||
"testPresets": [
|
||||
{
|
||||
"name": "conan-release",
|
||||
"configurePreset": "conan-release",
|
||||
"execution": {
|
||||
"jobs": 32
|
||||
},
|
||||
"environment": {
|
||||
"OPENSSL_MODULES": "C:\\Users\\Administrator\\.conan2\\p\\b\\opens4d81e45a1d5f5\\p\\lib\\ossl-modules"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
359
README.md
359
README.md
@@ -1,6 +1,6 @@
|
||||
# dstalk
|
||||
|
||||
> 基于 DeepSeek V4 大模型、兼容 OpenAI / Anthropic API 的 AI 编程 CLI
|
||||
> AI 编程 CLI —— 基于 DeepSeek V4, 兼容 OpenAI / Anthropic API
|
||||
>
|
||||
> 官网: [dstalk.top](https://dstalk.top)
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
|
||||
## 这是什么?
|
||||
|
||||
dstalk 是一款 AI 编程助手命令行工具。通过调用 DeepSeek V4 大模型(兼容 OpenAI 和 Anthropic API),在终端里用自然语言完成代码编写、重构、调试和文件操作。功能对标 Claude Code、OpenCode、KiloCode。
|
||||
dstalk 是一款 AI 编程助手命令行工具, 通过调用大模型在终端里完成代码编写、重构、调试和文件操作。
|
||||
|
||||
核心设计为 **插件化 CDLL + 多前端解耦**:
|
||||
核心设计为 **插件化 CDLL + 多前端解耦**:
|
||||
|
||||
```text
|
||||
┌───────────────────────────────────────────────────────────┐
|
||||
@@ -18,9 +18,7 @@ dstalk 是一款 AI 编程助手命令行工具。通过调用 DeepSeek V4 大
|
||||
│ ┌──────────────────┐ ┌──────────────────────────┐ │
|
||||
│ │ dstalk-cli │ │ dstalk-gui │ │
|
||||
│ │ ANSI 终端 UI │ │ SDL3 图形化 UI │ │
|
||||
│ │ exe → dstalk.dll│ │ exe → dstalk.dll │ │
|
||||
│ └────────┬─────────┘ └─────────────┬─────────────┘ │
|
||||
│ │ │ │
|
||||
│ └──────────────┬───────────────┘ │
|
||||
│ │ C ABI │
|
||||
└──────────────────────────┼─────────────────────────────────┘
|
||||
@@ -30,7 +28,6 @@ dstalk 是一款 AI 编程助手命令行工具。通过调用 DeepSeek V4 大
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ Host: 插件加载 · 服务注册 · 事件总线 · 配置管理 │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
│ │ 服务查询 │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
||||
│ │ deepseek │ │ anthropic│ │ network │ │ lsp │ │
|
||||
│ │ (ai) │ │ (ai) │ │ (http) │ │ 客户端 │ │
|
||||
@@ -41,350 +38,48 @@ dstalk 是一款 AI 编程助手命令行工具。通过调用 DeepSeek V4 大
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **`dstalk-core`** —— C11/C++20 插件化宿主 DLL,负责插件加载、服务注册/查询、事件总线、配置管理。
|
||||
- **`dstalk-cli`** —— 命令行前端,ANSI 转义码实现,调用 `dstalk.dll`。
|
||||
- **`dstalk-gui`** —— 图形化前端,SDL3 跨平台窗口,调用 `dstalk.dll`。
|
||||
- **`plugins/`** —— 9 个功能插件,编译为独立 DLL,通过 C ABI 动态注册服务。
|
||||
- **dstalk-core** —— C11/C++20 插件化宿主 DLL, 负责插件加载、服务注册、事件总线、配置管理
|
||||
- **dstalk-cli** —— 命令行前端, ANSI 终端 UI
|
||||
- **dstalk-gui** —— 图形化前端, SDL3 跨平台窗口
|
||||
- **plugins/** —— 9 个功能插件, 编译为独立 DLL, 通过 C ABI 动态注册服务
|
||||
|
||||
核心与界面完全解耦,可以轻松编写自己的前端,或把 AI 能力嵌入到现有工具中。所有功能通过插件实现,插件只需引用 `dstalk.dll` 即可。
|
||||
核心与界面完全解耦, 可编写自己的前端或把 AI 能力嵌入到现有工具中。
|
||||
|
||||
---
|
||||
|
||||
## 核心功能
|
||||
## 支持的 AI 模型
|
||||
|
||||
| 功能 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| **多后端 AI 支持** | 已完成 | DeepSeek V4 和 Anthropic Claude 通过插件独立加载,`config.toml` 中 `ai.provider` 一键切换 |
|
||||
| **流式输出** | 已完成 | SSE 流式响应,终端逐字打印 AI 思考过程 |
|
||||
| **多轮会话** | 已完成 | 上下文窗口连续对话,支持 `/clear` 清空、`/save` `/load` 持久化 |
|
||||
| **文件读写工具** | 已完成 | 内置 `/file` 命令集,支持列目录、查看、读取、写入文件 |
|
||||
| **LSP 集成** | 已完成 | 完整 LSP 客户端(子进程管理、JSON-RPC 2.0),支持诊断、悬停、补全 |
|
||||
| **插件系统** | 已完成 | 9 个功能插件,拓扑排序依赖管理,DLL 动态加载,服务注册/查询 |
|
||||
| **GUI 前端** | 已完成 | SDL3 跨平台图形界面,流式输出、会话管理、输入历史、剪贴板 |
|
||||
| 提供商 | 模型 | 插件 |
|
||||
|--------|------|------|
|
||||
| DeepSeek | deepseek-v4-pro | `ai.deepseek` |
|
||||
| Anthropic | claude-opus-4 | `ai.anthropic` |
|
||||
| OpenAI 兼容 | GPT 系列 | `ai.deepseek` (兼容) |
|
||||
|
||||
---
|
||||
|
||||
## 为什么用 C/C++ 实现?
|
||||
|
||||
| 维度 | dstalk (C/C++) | 典型竞品 (TypeScript/Node.js) |
|
||||
|------|----------------|------------------------------|
|
||||
| 启动速度 | 毫秒级 | 秒级 |
|
||||
| 内存占用 | 数十 MB | 数百 MB 起 |
|
||||
| 运行时依赖 | 零(单文件 DLL) | 需要 Node.js 运行时 |
|
||||
| 嵌入能力 | 任意语言通过 C ABI 调用 | 困难 |
|
||||
| GC 影响 | 无 GC 停顿 | 可能内存膨胀 |
|
||||
|
||||
AI 编程助手需要长期驻留、频繁交互,性能特征值得用系统级语言重新思考。
|
||||
|
||||
---
|
||||
|
||||
## 与竞品的差异化
|
||||
|
||||
| 特性 | dstalk | Claude Code | OpenCode | KiloCode |
|
||||
|------|--------|-------------|----------|----------|
|
||||
| 实现语言 | C11 / C++20 | TypeScript | TypeScript | TypeScript |
|
||||
| 运行时 | 零依赖 CDLL | Node.js | Node.js | Node.js |
|
||||
| 前端形态 | CLI + GUI 双前端 | 终端集成 | VS Code 插件 | VS Code 插件 |
|
||||
| 模型 | DeepSeek / OpenAI / Anthropic | Claude | 多模型 | 多模型 |
|
||||
| 嵌入第三方 | C ABI,极易 | 困难 | 困难 | 困难 |
|
||||
|
||||
### DLL 架构优势
|
||||
|
||||
- **语言无关** —— C ABI 意味着 C/C++、Python、Rust、C#、Go 都能直接调用
|
||||
- **进程内集成** —— 无需 HTTP 通信、零 IPC 开销,直接函数调用
|
||||
- **前端零状态** —— CLI 和 GUI 不持有业务逻辑,只负责渲染和输入
|
||||
通过 `config.toml` 中 `ai.provider` 一键切换。
|
||||
|
||||
---
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装工具链(全自动,存入 tools\,需系统已安装 Python 3.10+)
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
setup.bat # 下载 CMake + Ninja + LLVM/Clang,并在 tools/.venv 安装 Conan2
|
||||
cd tools && setup.bat # 1. 安装工具链 (CMake / Ninja / LLVM / Conan2)
|
||||
build.bat # 2. 编译
|
||||
# 3. 创建 config.toml # 见教程: docs/tutorial/quick-start.md
|
||||
build/dstalk-cli/dstalk-cli.exe # 4. 运行
|
||||
# 5. 输入自然语言 # "帮我写一个 C 程序"
|
||||
```
|
||||
|
||||
> 网络不畅时可手动下载放入对应目录:[Ninja](https://github.com/ninja-build/ninja/releases) | [CMake](https://cmake.org/download/) | [LLVM](https://github.com/llvm/llvm-project/releases)
|
||||
>
|
||||
> 目录结构要求: `tools/cmake/bin/cmake.exe` / `tools/ninja/ninja.exe` / `tools/llvm/bin/clang.exe` / `tools/.venv/Scripts/conan.exe`
|
||||
|
||||
### 2. 编译
|
||||
|
||||
```bash
|
||||
build.bat # 一键: Conan拉依赖 → CMake配置 → Ninja编译
|
||||
```
|
||||
|
||||
### 3. 运行
|
||||
|
||||
```bash
|
||||
build/dstalk-cli/dstalk-cli.exe # 命令行模式
|
||||
# 图形模式默认关闭;需要 SDL3 时用 -DDSTALK_BUILD_GUI=ON 重新配置
|
||||
```
|
||||
> 详细 5 步教程 (含 config.toml 模板与对话示例): [docs/tutorial/quick-start.md](docs/tutorial/quick-start.md)
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
## 文档
|
||||
|
||||
```text
|
||||
$ dstalk-cli
|
||||
|
||||
dstalk v0.1.0 | 模型: deepseek-v4-pro | /help 查看帮助
|
||||
|
||||
> 帮我写一个读取 CSV 并计算平均值的 C 程序
|
||||
|
||||
[dstalk] 正在思考...
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "用法: %s <csv文件>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
FILE *fp = fopen(argv[1], "r");
|
||||
if (!fp) { perror("fopen"); return 1; }
|
||||
|
||||
double sum = 0.0;
|
||||
int count = 0;
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
sum += atof(line);
|
||||
count++;
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
printf("平均值: %.2f (共 %d 行)\n", sum / count, count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
已写入 csv_avg.c。需要我帮你编译测试吗?
|
||||
|
||||
> 把这段代码改成支持表头的
|
||||
|
||||
[dstalk] 已更新 csv_avg.c——跳过第一行表头,增加列选择功能。
|
||||
|
||||
> /file show csv_avg.c
|
||||
|
||||
[dstalk] 已显示 csv_avg.c 内容。
|
||||
```
|
||||
|
||||
### 常用命令
|
||||
|
||||
| 命令 | 说明 |
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| `/file list [path]` | 列出目录内容 |
|
||||
| `/file show <path>` | 查看文件内容 |
|
||||
| `/file read <path>` | 读取文件内容 |
|
||||
| `/file write <path> <content>` | 写入文件 |
|
||||
| `/model <name>` | 切换 AI 模型 |
|
||||
| `/clear` | 清空会话上下文 |
|
||||
| `/save <path>` | 保存会话 |
|
||||
| `/load <path>` | 恢复会话 |
|
||||
| `/help` | 显示帮助 |
|
||||
|
||||
---
|
||||
|
||||
## 工程结构
|
||||
|
||||
```text
|
||||
dstalk/
|
||||
├── deps/
|
||||
│ └── conanfile.txt # Conan2 依赖声明 (Boost, OpenSSL, SDL3)
|
||||
├── dstalk-core/ # 核心 DLL — 插件宿主
|
||||
│ ├── include/dstalk/
|
||||
│ │ ├── dstalk_host.h # 公开 API: 宏定义、宿主API、插件生命周期
|
||||
│ │ ├── dstalk_services.h # 服务接口 vtable 定义 (AI/Session/Context/HTTP/FileIO/Config/Tools/LSP)
|
||||
│ │ ├── dstalk_types.h # 共享类型: 消息、结果、事件、日志等级
|
||||
│ │ └── dstalk_lsp.h # LSP 便捷函数 (委托给 lsp 插件)
|
||||
│ ├── src/
|
||||
│ │ ├── host.cpp # 宿主: 初始化、服务查询、LSP 便捷函数
|
||||
│ │ ├── config_store.cpp/.hpp # 配置管理 (TOML 解析)
|
||||
│ │ ├── event_bus.cpp/.hpp # 事件总线 (发布/订阅)
|
||||
│ │ ├── service_registry.cpp/.hpp # 服务注册表 (名称→vtable)
|
||||
│ │ ├── plugin_loader.cpp/.hpp # 插件加载器 (DLL 加载、拓扑排序、依赖管理)
|
||||
│ │ └── boost_json.cpp # Boost.JSON 编译单元
|
||||
│ └── CMakeLists.txt
|
||||
├── plugins/ # 功能插件 (每个编译为独立 DLL)
|
||||
│ ├── deepseek/ # DeepSeek AI (服务名: ai.deepseek)
|
||||
│ ├── anthropic/ # Anthropic Claude (服务名: ai.anthropic)
|
||||
│ ├── network/ # HTTP/HTTPS 客户端 (服务名: http)
|
||||
│ ├── session/ # 会话管理 (服务名: session)
|
||||
│ ├── context/ # 上下文/Token 管理 (服务名: context)
|
||||
│ ├── file-io/ # 文件读写 (服务名: file_io)
|
||||
│ ├── tools/ # 工具注册/执行 (服务名: tools)
|
||||
│ ├── lsp/ # LSP 客户端 (服务名: lsp)
|
||||
│ ├── config/ # 配置服务 (服务名: config)
|
||||
│ └── CMakeLists.txt # 插件构建 (按依赖顺序)
|
||||
├── dstalk-cli/ # 命令行前端 (ANSI)
|
||||
│ ├── src/main.cpp
|
||||
│ └── CMakeLists.txt
|
||||
├── dstalk-gui/ # 图形化前端 (SDL3)
|
||||
│ ├── src/main.cpp
|
||||
│ └── CMakeLists.txt
|
||||
├── examples/ # 示例代码
|
||||
│ └── example_plugin/
|
||||
│ └── example_plugin.cpp # 插件开发示例
|
||||
├── tests/ # 集成测试
|
||||
│ └── smoke_test.cpp
|
||||
├── CMakeLists.txt # 根 CMake
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 公开 API
|
||||
|
||||
头文件:
|
||||
- [dstalk_host.h](dstalk-core/include/dstalk/dstalk_host.h) — 宿主 API、插件生命周期
|
||||
- [dstalk_services.h](dstalk-core/include/dstalk/dstalk_services.h) — 服务接口 vtable 定义
|
||||
- [dstalk_types.h](dstalk-core/include/dstalk/dstalk_types.h) — 共享类型
|
||||
- [dstalk_lsp.h](dstalk-core/include/dstalk/dstalk_lsp.h) — LSP 便捷函数
|
||||
|
||||
```c
|
||||
/* 宿主生命周期 */
|
||||
int dstalk_init(const char* config_path);
|
||||
void dstalk_shutdown(void);
|
||||
|
||||
/* 插件管理 */
|
||||
int dstalk_plugin_load(const char* path);
|
||||
int dstalk_plugin_unload(int plugin_id);
|
||||
int dstalk_plugin_list(char** output_json);
|
||||
|
||||
/* 服务查询 —— 通过名称获取插件注册的 vtable */
|
||||
void* dstalk_service_query(const char* service_name, int min_version);
|
||||
|
||||
/* 事件总线 */
|
||||
int dstalk_event_subscribe(int event_type, dstalk_event_handler_fn handler, void* userdata);
|
||||
int dstalk_event_emit(int event_type, const void* data);
|
||||
void dstalk_event_unsubscribe(int subscription_id);
|
||||
|
||||
/* 配置 */
|
||||
const char* dstalk_config_get(const char* key);
|
||||
int dstalk_config_set(const char* key, const char* value);
|
||||
|
||||
/* 内存管理 */
|
||||
void* dstalk_alloc(size_t size);
|
||||
void dstalk_free(void* ptr);
|
||||
char* dstalk_strdup(const char* s);
|
||||
|
||||
/* LSP 便捷函数 (委托给 lsp 插件) */
|
||||
int dstalk_lsp_start(const char* server_cmd, const char* language);
|
||||
void dstalk_lsp_stop(void);
|
||||
int dstalk_lsp_open(const char* uri, const char* content, const char* language_id);
|
||||
int dstalk_lsp_close(const char* uri);
|
||||
int dstalk_lsp_diagnostics(const char* uri, char** output);
|
||||
int dstalk_lsp_hover(const char* uri, int line, int character, char** output);
|
||||
int dstalk_lsp_completion(const char* uri, int line, int character, char** output);
|
||||
```
|
||||
|
||||
**调用约定:**
|
||||
|
||||
- 所有字符串均为 UTF-8 编码
|
||||
- 通过 `dstalk_service_query` 获取服务 vtable,再通过函数指针调用具体功能
|
||||
- `dstalk_free` 释放所有 API 返回的堆内存
|
||||
- 返回 `0` 成功,负数表示错误码
|
||||
|
||||
**跨语言调用示例:**
|
||||
|
||||
```c
|
||||
#include "dstalk/dstalk_host.h"
|
||||
#include "dstalk/dstalk_services.h"
|
||||
#include <stdio.h>
|
||||
|
||||
int main(void) {
|
||||
if (dstalk_init("config.toml") != 0) {
|
||||
fprintf(stderr, "初始化失败\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 查询 AI 服务
|
||||
const char* provider = dstalk_config_get("ai.provider");
|
||||
if (!provider) provider = "ai.deepseek";
|
||||
const dstalk_ai_service_t* ai = dstalk_service_query(provider, 1);
|
||||
if (ai) {
|
||||
ai->configure(provider, "https://api.deepseek.com/v1", "sk-xxx",
|
||||
"deepseek-v4-pro", 4096, 0.7);
|
||||
}
|
||||
|
||||
dstalk_shutdown();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
**Q: 为什么不用 Rust?**
|
||||
A: 团队对 C/C++ 生态更熟悉,C++20 的现代特性已能让我们写出安全高效的代码。且 CDLL 需要稳定的 C ABI,C/C++ 最直接。
|
||||
|
||||
**Q: 支持哪些模型?**
|
||||
A: 主要支持 DeepSeek V4,同时兼容 OpenAI GPT 系列和 Anthropic Claude 系列的 API。通过配置文件切换 API 基地址和密钥即可。
|
||||
|
||||
**Q: 为什么同时做 CLI 和 GUI?**
|
||||
A: CLI 适合终端/SSH/CI 环境,GUI 适合需要富文本和鼠标交互的场景。两者共享同一核心 DLL,功能一致。
|
||||
|
||||
**Q: 如何配置 API Key?**
|
||||
A: 首次运行前,手动创建项目目录下的 `config.toml`,按需选择后端:
|
||||
|
||||
```toml
|
||||
# 选择 AI 后端插件: ai.deepseek 或 ai.anthropic
|
||||
ai.provider = "ai.deepseek"
|
||||
|
||||
# DeepSeek
|
||||
api.base_url = "https://api.deepseek.com/v1"
|
||||
api.api_key = "sk-xxxxxxxx"
|
||||
api.model = "deepseek-v4-pro"
|
||||
|
||||
# Anthropic Claude (切换 ai.provider 为 "ai.anthropic" 即可)
|
||||
# api.base_url = "https://api.anthropic.com/v1"
|
||||
# api.api_key = "sk-ant-xxxxxxxx"
|
||||
# api.model = "claude-opus-4-20250514"
|
||||
```
|
||||
|
||||
修改 `ai.provider` 字段即可在不同后端间切换,无需改动代码。
|
||||
|
||||
---
|
||||
|
||||
## 路线图
|
||||
|
||||
| 阶段 | 内容 |
|
||||
|------|------|
|
||||
| **Phase 1** | 项目骨架、CMake 构建、DLL 导出、CLI 前端主循环 |
|
||||
| **Phase 2** | HTTPS 网络层、DeepSeek API 对接、基本对话 |
|
||||
| **Phase 3** | ~~流式输出、多轮会话、文件读写工具、CLI 体验对齐~~ |
|
||||
| **Phase 4** (当前) | ~~插件化架构重构、多后端 AI、LSP 客户端、SDL3 GUI~~ |
|
||||
| **Phase 5** | GUI 完善、工具调用(Function Calling)、插件生态、多语言扩展 |
|
||||
|
||||
---
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. Fork 仓库并克隆到本地
|
||||
2. 创建分支: `git checkout -b feature/功能名`
|
||||
3. 编码: C 代码 K&R 风格,C++ 代码 LLVM 风格
|
||||
4. 确保 `cmake --build build` 通过
|
||||
5. 提交 PR,描述改动内容和动机
|
||||
|
||||
### 代码规范
|
||||
|
||||
- C: C11 标准,头文件 `#pragma once`
|
||||
- C++: C++20 标准,优先标准库,必要时引入 Boost
|
||||
- 内存: C++ 优先 RAII;C 代码显式管理
|
||||
- 对外接口: `extern "C"` 纯 C 函数,不抛异常
|
||||
|
||||
---
|
||||
|
||||
## 技术风险与对策
|
||||
|
||||
| 风险 | 对策 |
|
||||
|------|------|
|
||||
| C++ 开发效率低于脚本语言 | Boost 库弥补;核心 API 稳定后开发速度不会慢于竞品 |
|
||||
| OpenSSL 跨平台兼容性 | Conan2 锁定版本,统一 Windows/Linux/macOS HTTPS 后端 |
|
||||
| SDL3 依赖体积较大 | GUI 默认关闭,需要图形前端时再启用 `DSTALK_BUILD_GUI` |
|
||||
| AI API 协议变更 | 适配层独立模块,变更时只改一处 |
|
||||
| [教程: 快速入门](docs/tutorial/quick-start.md) | 5 步上手, 从安装到第一个对话 |
|
||||
| [参考: CLI 命令](docs/reference/commands.md) | 完整命令速查表 |
|
||||
| [文档导航](docs/README.md) | 全部文档索引与未来计划 |
|
||||
|
||||
---
|
||||
|
||||
@@ -394,4 +89,4 @@ GNU General Public License v3. Copyright (c) 2026 dstalk contributors.
|
||||
|
||||
---
|
||||
|
||||
[dstalk.top](https://dstalk.top) | [GitHub](https://github.com/dstalk/dstalk) | [Issue 反馈](https://github.com/dstalk/dstalk/issues)
|
||||
[dstalk.top](https://dstalk.top) | [GitHub](https://github.com/dstalk/dstalk)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# dstalk 多 Agents 协作系统
|
||||
|
||||
> **新会话恢复 CEO 模式?先读 [WORKFLOW.md](WORKFLOW.md)** —— 工作流、波次节奏、6 阶段协作、子代理 prompt 模板都在那里。
|
||||
|
||||
## 公司宗旨
|
||||
|
||||
- **第一性原理**:从问题本质出发,不被既有结构和惯性束缚
|
||||
|
||||
118
agents/WORKFLOW.md
Normal file
118
agents/WORKFLOW.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# dstalk 团队工作流(CEO 多代理协作模式)
|
||||
|
||||
> 本文档供 CEO(主会话)和所有员工子代理参考。新会话恢复 CEO 模式时**第一件事**就是读这里。
|
||||
|
||||
## 1. 编制与并行度
|
||||
|
||||
- **团队规模 = CPU 物理核数(dstalk = 16 人)**:1 CEO + 3 架构师 + 5 工程师 + 3 QA + 2 DevOps + 1 设计师 + 1 作家 + 1 安全
|
||||
- 每个员工有 `agents/<id>/profile.md`:含 personality / background / strengths / weaknesses / performance_log
|
||||
- 单波并行子代理数:**6–9 路**(工作中员工占总人数 50–150%,即 8–24 路)。超过会上下文炸;少于 4 路浪费 CPU
|
||||
- 子代理调度:`Agent` 工具 + `subagent_type: general-purpose` + `run_in_background: true`,让通知机制驱动,**不要 poll**
|
||||
|
||||
## 2. 6 阶段协作流程
|
||||
|
||||
每个 Wave 都按这个流程:
|
||||
|
||||
1. **propose(提案)** — CEO 把需求拆成 N 个候选任务(`W<wave>.<seq>` 编号,如 W2.1、W9.4)
|
||||
2. **vote(投票)** — 派 3–4 个子代理读代码独立评估、投票同意/反对/优化
|
||||
3. **optimize(优化)** — 被反对的提案,提案方根据反对意见迭代
|
||||
4. **integrate(整合)** — CEO 汇总成统一可执行计划
|
||||
5. **execute(执行)** — 派对应专长的员工子代理实施
|
||||
6. **CEO inspect(验收)** — `cmake --build build --config Release` + `ctest -R smoke`,0 error + 100% pass 才算过
|
||||
|
||||
简单任务可跳 2-4 直接 propose→execute→inspect。**有架构争议**的必须走完 6 阶段。
|
||||
|
||||
## 3. 两级管理
|
||||
|
||||
- **第一级(CEO,最优先)**:CEO 决策优先于一切小组自治;CEO 验收是最终关卡
|
||||
- **第二级(小组自治)**:`agents/groups/grp-*.md` 定义工作小组,组内事务自决
|
||||
- 冲突时 **CEO > group lead > 个人**
|
||||
|
||||
## 4. 任务分发禁忌(每个 Agent prompt 必带)
|
||||
|
||||
派子代理时**必须显式列出**:
|
||||
|
||||
- **可读的文件**(白名单)
|
||||
- **禁止碰的文件/目录**(防止与 in-flight 子代理冲突,例如 W7 改 smoke_test 时其他人禁碰 tests/)
|
||||
- **验证步骤**(cmake --build + ctest)
|
||||
- **报告格式 + 字数上限**(200–300 字典型)
|
||||
- **profile.md 更新要求**(每个任务必须追加 performance_log)
|
||||
|
||||
不写禁忌 → 多个子代理同时改同一个文件 → 冲突回滚 → 时间浪费。
|
||||
|
||||
## 5. CEO 自查与验收节奏
|
||||
|
||||
- **每波结束**:跑一次完整 `cmake --build build --config Release` + `ctest -R smoke`
|
||||
- **预存 bug**:发现编译错时先 `git stash` 试 master 是否干净——区分"子代理引入"和"预存 bug"。预存 bug CEO 自己改不甩锅
|
||||
- **stale obj 问题**:clang-cl 增量构建偶尔不识别源文件修改(lsp_plugin/tools_plugin 踩过坑),症状是编译报告的行号源码内容与磁盘不符。解法:`rm -f build/**/<file>.cpp.obj` 强制重编
|
||||
- **CEO 验收通过后立刻 commit + push**(用户硬规则:"别忘了每次通过ceo验收的提交git并推送")
|
||||
|
||||
## 6. 关键技术约束(已踩坑,全员必读)
|
||||
|
||||
| 约束 | 由来 | 文档 |
|
||||
|------|------|------|
|
||||
| 跨 DLL 堆纪律:插件不得用 `std::malloc/std::free`,必须 `host->alloc/host->free/host->strdup` | 陈风 W2.1 | `docs/reference/plugin-abi.md` |
|
||||
| Boost.JSON header-only:每个使用 `<boost/json.hpp>` 的翻译单元必须 `#include <boost/json/src.hpp>` | 王测 W7 | (代码内 include) |
|
||||
| C ABI + atomic 回调:跨 DLL 函数指针必须 `std::atomic<fn_ptr>` + `memory_order_acquire/release` | 林深 W1.1 | `docs/reference/plugin-abi.md` §6 |
|
||||
| api_key 安全清零:on_shutdown 必须 `volatile char*` 写零循环 + clear | 曹武 W2.2 | `docs/explanation/security-logging.md` |
|
||||
| CRT:`/MD` 动态 CRT(`CMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL`) | 胡桐 W3 | (build 配置) |
|
||||
| `initialize_all` 容错:单插件失败不阻断其他插件,返回失败计数 | 黄岭 W9.8 | `dstalk-core/src/plugin_loader.cpp` |
|
||||
|
||||
## 7. Wave 编号与历史
|
||||
|
||||
进入项目时**第一件事**:读 `agents/*/profile.md` 的 `performance_log`,从最大 W 号继续下一波。
|
||||
|
||||
**已完成的高水位**(截至 2026-05-27):W1.1、W2.1、W2.2、W3、W4、W5.1、W6.1、W7、W8.1、W9.1、W9.3、W9.4、W9.6、W9.8。Wave 6 还有 W9.10 in-flight。
|
||||
|
||||
下一个新提案应从 **W10.x** 开始。
|
||||
|
||||
## 8. 用户消息模式识别(CEO 行为指南)
|
||||
|
||||
| 用户说 | CEO 行动 |
|
||||
|--------|----------|
|
||||
| "团队不能闲着" / "去开会安排工作" | 立刻派下一波,**不要解释 / 不要问"派什么"**——基于 profile.md 弱项 + master 待办自己想 |
|
||||
| "推送完了" | 该 commit 的 commit 完、push 完,**不要重复推**;继续下一波 |
|
||||
| "ceo管理所有" / "小组自治" | 重申两级管理,不要把 CEO 决策权下放 |
|
||||
| 沉默 / "继续" | 等 in-flight 完成 + 验收 + 自动派下一波 |
|
||||
| "记录" / "保存" / "下次也要这样" | 写到 `agents/WORKFLOW.md`(公共)+ CEO 私有 memory(双轨) |
|
||||
|
||||
## 9. 子代理 prompt 模板(复制即用)
|
||||
|
||||
```
|
||||
你扮演 dstalk 团队成员 <姓名> (<agent-id>) — <角色>。读 `agents/<agent-id>/profile.md` 了解你的人设。
|
||||
|
||||
**W<n>.<m> 任务**:<一句话目标>
|
||||
|
||||
**背景**:<2-4 句,说明为什么做这个、依赖什么前置工作>
|
||||
|
||||
请读:
|
||||
- <文件 1>
|
||||
- <文件 2>
|
||||
- ...
|
||||
|
||||
**交付**:
|
||||
1. <可执行步骤 1>
|
||||
2. <可执行步骤 2>
|
||||
|
||||
**禁忌**:
|
||||
- 不要碰 <某目录>(in-flight 中)
|
||||
- 不要 <某行为>(与设计原则冲突)
|
||||
|
||||
实现完成后必须 cmake --build build --config Release 验证 0 error,并跑 ctest -R smoke 验证 100% pass。
|
||||
|
||||
完成后更新 `agents/<agent-id>/profile.md` performance_log 追加 W<n>.<m> 项。
|
||||
|
||||
**报告格式**(不超过 N 字):
|
||||
1. 改的代码行号 + 关键 diff
|
||||
2. 测试输出最后几行
|
||||
3. profile.md 已更新
|
||||
```
|
||||
|
||||
## 10. 关联文档
|
||||
|
||||
- [README.md](README.md) — 团队花名册 + 公司宗旨
|
||||
- [groups/](groups/) — 工作小组定义
|
||||
- [../docs/reference/plugin-abi.md](../docs/reference/plugin-abi.md) — Plugin ABI 契约
|
||||
- [../docs/explanation/architecture.md](../docs/explanation/architecture.md) — 架构哲学
|
||||
- [../docs/explanation/plugin-lifecycle.md](../docs/explanation/plugin-lifecycle.md) — 插件生命周期
|
||||
- [../docs/explanation/security-logging.md](../docs/explanation/security-logging.md) — 日志安全审计
|
||||
@@ -20,5 +20,15 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "入职 dstalk 团队"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "W9.8 修复 plugin_loader initialize_all() 首插件失败即终止缺陷"
|
||||
detail: |
|
||||
将 initialize_all() 从 fail-fast 改为 fail-continue:
|
||||
- 单插件 init 失败不再返回 -1,而是 log error + 标记失败 + 继续初始化其他插件
|
||||
- 依赖了 failed 插件的插件自动跳过,log warning
|
||||
- 拓扑序不变(Kahn 算法未修改)
|
||||
- 返回值语义: 0=全部成功, >0=失败插件数, <0=严重错误(循环依赖/host_api null)
|
||||
- 编译 0 error, smoke test 100% pass
|
||||
rating: completed
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
@@ -30,6 +30,12 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "加入核心质量小组(grp-quality-core),与王测协作执行C2"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "W1.1 修复完成:shutdown_all 新增 DLL 句柄释放(FreeLibrary/dlclose);g_diag_callback 改为 std::atomic 消除 UB。host.cpp 和 plugin_loader.cpp 均编译通过,0 error 0 warning"
|
||||
rating: A
|
||||
- date: 2026-05-27
|
||||
event: "W9.4 完成:撰写 docs/reference/plugin-abi.md Plugin ABI 契约文档(200行),涵盖 DSTALK_API_VERSION、内存所有权、跨DLL堆纪律、register_service、on_init/on_shutdown、回调线程安全、依赖声明共7个契约要点。更新 docs/README.md reference 区追加入口"
|
||||
rating: A
|
||||
current_groups:
|
||||
- grp-quality-core (成员)
|
||||
- grp-ai-plugins (待命)
|
||||
|
||||
@@ -20,5 +20,17 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "入职 dstalk 团队"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "落地 4 项 CMake 改进 (审查报告 C1-C4)"
|
||||
detail: >
|
||||
1. POST_BUILD DLL 复制改为 CMAKE_RUNTIME_OUTPUT_DIRECTORY 统一输出,
|
||||
删除 dstalk-cli/dstalk-gui/tests 中 3 处 POST_BUILD 拷贝。
|
||||
2. CMAKE_CXX_STANDARD 改为 per-target target_compile_features(dstalk PUBLIC cxx_std_20)。
|
||||
3. 删除 9 个插件中冗余的 target_include_directories(... dstalk-core/include)。
|
||||
4. 创建 INTERFACE library dstalk_boost_config 统一 BOOST_ALL_NO_LIB /
|
||||
BOOST_ERROR_CODE_HEADER_ONLY / BOOST_JSON_HEADER_ONLY,5 个插件改用 link 方式。
|
||||
顺带修复: tools_plugin.cpp 缺少前向声明、lsp_plugin.cpp 函数签名 mismatch、
|
||||
5 个插件缺少 #include <boost/json/src.hpp> (Boost 1.86 不再识别 HEADER_ONLY)。
|
||||
rating: done
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
@@ -20,5 +20,12 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "入职 dstalk 团队"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "落地 CI pipeline (GitHub Actions)"
|
||||
detail: >
|
||||
创建 .github/workflows/ci.yml,实现双矩阵构建 (ubuntu-24.04 + windows-2025)。
|
||||
PR 仅跑 Ubuntu,push master 跑全矩阵节省 minutes。
|
||||
集成 Conan 依赖缓存 (含 ~/.conan2/p),checkout fetch-depth=1 提速。
|
||||
rating: done
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
@@ -20,5 +20,18 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "入职 dstalk 团队"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "W2.1 - 修复 file_io_plugin 跨 DLL 堆释放风险 (曹武安全审计 + B3 评审)"
|
||||
rating: success
|
||||
details:
|
||||
- "问题: file_io_plugin.cpp 用 ::malloc 分配,调用方 smoke_test 用 std::free 释放 -- Windows /MT 下不同堆必崩"
|
||||
- "CRT 类型: /MD (MultiThreadedDLL, 动态 CRT 共享) -- 当前不会崩但违反 ABI 纪律"
|
||||
- "修复: file_io_plugin 改用 g_host->alloc / g_host->free (host_api 提供的统一分配器)"
|
||||
- "同步修复: tools_plugin.cpp:58 std::free -> g_host->free (同模式)"
|
||||
- "同步修复: session_plugin.cpp:166 std::free -> g_host->free (同模式)"
|
||||
- "同步修复: smoke_test.cpp 三处 std::free -> dstalk_free (与 main.cpp:110 一致)"
|
||||
- "编译: 0 error; 测试: smoke test passed"
|
||||
- "发现: initialize_all() 在首个插件失败时停止,使后续插件无法初始化 (预存 bug, 非本次引入)"
|
||||
- "发现: deepseek/session 插件 Boost JSON 链接错误 (预存问题, 与本次修复无关)"
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
@@ -20,5 +20,14 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "入职 dstalk 团队"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "W6.1: 修复 LSP reader_loop 协议合规 bug(Content-Length 状态机解析)"
|
||||
rating: completed
|
||||
details: |
|
||||
将 reader_loop 改为状态机模式读取 header 块:循环 read_line 直到空行,
|
||||
累积 Content-Length,遇到其他 header(如 Content-Type)不丢弃。
|
||||
修复前:第一行非 Content-Length 时 continue 丢弃该行,导致 header 解析偏移错位。
|
||||
修复后:正确遍历所有 header 行,空行后若仍未找到 Content-Length 则记录错误并跳过帧。
|
||||
编译通过,smoke test 通过。
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
@@ -33,6 +33,9 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "独立执行 C3 CI编译脚本任务"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "W9.6: CLI新增/history[N]命令,含三种边界处理;/status增加history count;build 0 error 0 warning;已用batch模式验证空history和无效N场景"
|
||||
rating: A
|
||||
current_groups:
|
||||
- grp-ai-plugins (待命)
|
||||
---
|
||||
|
||||
@@ -20,5 +20,14 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "入职 dstalk 团队"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "W5.1 - network_plugin SSE 改 buffer_body"
|
||||
detail: |
|
||||
do_post_stream response parser: http::string_body -> http::buffer_body.
|
||||
消除 parser 完整 body + fragment + result_body 三份冗余。
|
||||
编译 0 error 0 warning, smoke test 通过.
|
||||
峰值内存: -67% (~360KB -> ~120KB), 无额外拷贝.
|
||||
留待真实 API 压测验证 end-to-end.
|
||||
rating: good
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
@@ -20,5 +20,8 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "入职 dstalk 团队"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "W9.10: host_api 单元测试 (8 cases, tests/host_api_test.cpp)"
|
||||
rating: completed
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
@@ -31,8 +31,8 @@ performance_log:
|
||||
event: "C1 编码完成:6项修改全部正确,竞态彻底消除"
|
||||
rating: A+
|
||||
- date: 2026-05-27
|
||||
event: "晋升为核心质量小组(grp-quality-core)组长,与林深协作执行C2"
|
||||
rating: ongoing
|
||||
event: "W7: smoke test 插件加载修复。tests/CMakeLists.txt 增加 POST_BUILD 拷贝逻辑,修复5个插件 Boost.JSON header-only 链接,实现 9/9 插件加载,所有 [Block] 零 WARN,100% pass"
|
||||
rating: A+
|
||||
current_groups:
|
||||
- grp-quality-core (组长)
|
||||
---
|
||||
|
||||
@@ -21,5 +21,21 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "入职 dstalk 团队"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "W2.2: api_key 在 on_shutdown 时安全清零 (deepseek + anthropic)"
|
||||
rating: done
|
||||
detail: |
|
||||
在 deepseek_plugin.cpp 和 anthropic_plugin.cpp 的 anonymous namespace 内
|
||||
新增 secure_zero(void*, size_t),通过 volatile 写零循环对 g_cfg.api_key
|
||||
执行安全擦除后 clear。编译:0 error 0 warning(与改动相关的文件)。
|
||||
- date: 2026-05-27
|
||||
event: "W9.3: 错误日志凭证泄露审计(8文件,0真实漏洞)"
|
||||
rating: done
|
||||
detail: |
|
||||
审计了 8 个文件的所有 host->log / printf / fprintf(stderr) / std::cerr 调用。
|
||||
0 真实可利用漏洞。deepseek/anthropic 的 configure 日志有意排除了 api_key;
|
||||
build_headers_json() 产生的凭证字符串仅通过内存传递给 Beast HTTP,未经过日志管道。
|
||||
低风险/假阳性 2 项(lsp server_cmd 日志 + network e.what() 异常信息),无需代码修改。
|
||||
审计报告写入 docs/explanation/security-logging.md。CVSS: N/A(无可利用漏洞)。
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
@@ -20,5 +20,8 @@ performance_log:
|
||||
- date: 2026-05-27
|
||||
event: "入职 dstalk 团队"
|
||||
rating: ongoing
|
||||
- date: 2026-05-27
|
||||
event: "Diátaxis 第二刀: 补充 Explanation 类文档 — architecture.md (插件架构哲学/三层模型/C ABI) + plugin-lifecycle.md (生命周期/拓扑排序/on_init on_shutdown 契约/ABI 纪律), 更新 docs/README.md 导航"
|
||||
rating: completed
|
||||
current_groups: []
|
||||
---
|
||||
|
||||
72
docs/README.md
Normal file
72
docs/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# dstalk 文档
|
||||
|
||||
> 文档采用 [Diataxis](https://diataxis.fr/) 框架组织: 教程 / 操作指南 / 解释 / 参考 四类分离。
|
||||
|
||||
---
|
||||
|
||||
## 已有文档
|
||||
|
||||
### 教程 (Tutorials) — 从零开始的学习路径
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [快速入门](tutorial/quick-start.md) | 5 步上手: 安装工具链、编译、配置、运行、第一个对话 |
|
||||
|
||||
### 解释 (Explanation) — 背景与设计决策
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [架构哲学](explanation/architecture.md) | 为什么是插件架构, host/plugin/service 三层模型, service registry 和 event bus 的职责, C ABI 的选择理由 |
|
||||
| [插件生命周期](explanation/plugin-lifecycle.md) | DLL 加载与拓扑排序, on_init/on_shutdown 契约, 跨 DLL 堆分配与 ABI 纪律 |
|
||||
|
||||
### 参考 (Reference) — 精确的技术描述
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| [CLI 命令速查](reference/commands.md) | 全部 CLI 命令的别名、作用与示例 |
|
||||
| [Plugin ABI 契约](reference/plugin-abi.md) | 跨 DLL 通信的 C ABI 规范:内存所有权、堆纪律、回调线程安全 |
|
||||
|
||||
---
|
||||
|
||||
## 未来计划
|
||||
|
||||
### 教程
|
||||
|
||||
- [ ] **插件开发入门** (`tutorial/plugin-dev.md`) — TODO: 写一个 Hello World 插件, 注册服务并调用
|
||||
- [ ] **LSP 集成教程** (`tutorial/lsp-setup.md`) — TODO: 配置 clangd 并在 dstalk 中使用诊断/补全
|
||||
|
||||
### 操作指南 (How-to Guides) — 解决具体问题
|
||||
|
||||
- [ ] **切换 AI 后端** (`how-to/switch-provider.md`) — TODO: 在 DeepSeek / Anthropic / OpenAI 间切换
|
||||
- [ ] **配置 API Key** (`how-to/config-api-key.md`) — TODO: config.toml 各项配置说明与安全建议
|
||||
- [ ] **使用 File 命令管理项目文件** (`how-to/file-commands.md`) — TODO: 列目录、查看、读写文件的工作流
|
||||
- [ ] **自定义前端开发** (`how-to/custom-frontend.md`) — TODO: 基于 C ABI 编写自己的前端程序
|
||||
- [ ] **批处理模式** (`how-to/batch-mode.md`) — TODO: `--batch` 标志与非交互式使用
|
||||
|
||||
### 解释 (Explanation) — 背景与设计决策
|
||||
|
||||
- [x] **架构哲学** (`explanation/architecture.md`) — 插件架构、三层模型、service registry/event bus、C ABI 选择
|
||||
- [x] **插件生命周期** (`explanation/plugin-lifecycle.md`) — DLL 加载、拓扑排序、on_init/on_shutdown 契约、ABI 纪律
|
||||
- [ ] **为什么用 C/C++** (`explanation/why-cpp.md`) — TODO: 启动速度、内存占用、运行时依赖、嵌入能力的对比分析
|
||||
- [ ] **Diataxis 文档框架** (`explanation/diataxis.md`) — TODO: 本文档组织方式的设计思路
|
||||
|
||||
### 参考
|
||||
|
||||
- [ ] **API 参考** (`reference/api.md`) — TODO: dstalk_host.h 完整 API 说明与调用示例
|
||||
- [ ] **配置参考** (`reference/config.md`) — TODO: config.toml 所有字段的详细说明
|
||||
- [ ] **服务接口参考** (`reference/services.md`) — TODO: dstalk_services.h 中所有 vtable 接口定义
|
||||
|
||||
---
|
||||
|
||||
## 文档约定
|
||||
|
||||
- 命令以 `$ ` 前缀表示, 在终端中运行
|
||||
- 代码块标注语言 (```c, ```toml, ```bash 等)
|
||||
- [ ] 表示计划中未完成的文档
|
||||
|
||||
---
|
||||
|
||||
## 贡献文档
|
||||
|
||||
文档使用 Markdown 编写, 存放在 `docs/` 目录下。
|
||||
欢迎通过 PR 补充或修正文档内容。
|
||||
138
docs/explanation/architecture.md
Normal file
138
docs/explanation/architecture.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# 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 边界。
|
||||
167
docs/explanation/plugin-lifecycle.md
Normal file
167
docs/explanation/plugin-lifecycle.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# 插件生命周期
|
||||
|
||||
> **本文属于**: Explanation -- 解释插件从加载到卸载的完整生命周期和关键契约。
|
||||
> 不涉及具体 API 签名细节,那属于 Reference (`reference/api.md`) 的职责。
|
||||
|
||||
---
|
||||
|
||||
## 总览
|
||||
|
||||
一个 dstalk 插件从磁盘上的 DLL 文件到进程中活跃的服务提供者,经历了四个阶段:
|
||||
|
||||
```
|
||||
加载 DLL → 依赖解析 → on_init → on_shutdown
|
||||
(load_plugin) (topo sort) (初始化) (逆序清理)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1:DLL 加载
|
||||
|
||||
Host 调用 `load_plugin(path)`,由 `PluginLoader` 执行:
|
||||
|
||||
1. **加载动态库**。Windows 用 `LoadLibraryA`;Linux 用 `dlopen(..., RTLD_NOW | RTLD_LOCAL)`。注意 Linux 端使用了 `RTLD_LOCAL` 而非 `RTLD_GLOBAL`,这意味着每个插件的符号默认对其他插件不可见——跨插件通信必须通过 Host API,不能通过直接符号引用。
|
||||
|
||||
2. **查找入口函数**。Host 在 DLL 中搜索名为 `dstalk_plugin_init` 的符号,调用它获取 `dstalk_plugin_info_t*`。
|
||||
|
||||
3. **版本校验**。Host 检查 `info->api_version` 是否等于 `DSTALK_API_VERSION`。不匹配则拒绝加载——这是防止 ABI 断裂的第一道防线。
|
||||
|
||||
4. **存储元数据**。`PluginInfo` 被存入 `plugins_` map,此时 `initialized = false`,插件尚未初始化。
|
||||
|
||||
**关键点**: 加载阶段只做符号获取和版本校验,不调用 `on_init`。这意味着插件代码此时尚未运行,不持有任何资源。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 2:依赖解析(拓扑排序)
|
||||
|
||||
所有插件加载完毕后,`initialize_all` 在调用 `on_init` 之前先执行拓扑排序。
|
||||
|
||||
**为什么需要排序?** 如果 deepseek 插件依赖 `http` 和 `config`,那么 `http` 和 `config` 插件的 `on_init` 必须先于 deepseek 执行——否则 deepseek 在 `on_init` 中调用 `query_service("http")` 会得到 `nullptr`。
|
||||
|
||||
**算法**:Kahn 算法(BFS 拓扑排序)。
|
||||
|
||||
1. 构建 `name → id` 的映射表。
|
||||
2. 为每个插件计算入度(in-degree = 被多少个其他插件依赖)。
|
||||
3. 从入度为 0 的节点(无依赖或依赖已满足的插件)开始,逐层输出。
|
||||
4. 最终检查:若输出节点数不等于总节点数,说明存在**循环依赖**,抛出异常阻止初始化。
|
||||
|
||||
依赖声明在 `dstalk_plugin_info_t.dependencies` 中,以 NULL 结尾的字符串数组。最多 `DSTALK_MAX_DEPS` (8) 个依赖。
|
||||
|
||||
```
|
||||
// 示例:deepseek 插件声明依赖 http 和 config
|
||||
{ "http", "config", NULL }
|
||||
```
|
||||
|
||||
依赖名称与目标插件的 `info->name` 字段匹配(如 `"file-io"` 不是 DLL 的文件名),因此依赖声明和插件命名必须一致。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 3:on_init 的契约
|
||||
|
||||
`on_init` 是插件获得生命信号的地方。Host 按拓扑排序的结果依次调用每个插件的 `on_init`,传入 `dstalk_host_api_t*`。
|
||||
|
||||
### 契约条款
|
||||
|
||||
**契约 1:host_api 在 on_init 执行期间完全可用。**
|
||||
|
||||
`dstalk_host_api_t` 是插件访问一切 Host 能力的唯一通道。它的所有字段(`register_service`、`query_service`、`log`、`alloc`、`free` 等)在 `on_init` 调用时已经有效。插件应当将这个指针保存为全局变量——每次跨 API 调用都需要它。
|
||||
|
||||
```c
|
||||
static const dstalk_host_api_t* g_host = nullptr;
|
||||
|
||||
static int on_init(const dstalk_host_api_t* host) {
|
||||
g_host = host; // 保存,此后的所有 API 调用都用它
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**契约 2:query_service 查找依赖。** 插件在 `on_init` 中查询它声明的每一个依赖。如果某个依赖不存在,`on_init` 应当返回非零值,告知 Host 初始化失败。
|
||||
|
||||
```c
|
||||
g_http = (dstalk_http_service_t*)host->query_service("http", 1);
|
||||
if (!g_http) return -1; // 依赖缺失,初始化失败
|
||||
```
|
||||
|
||||
Host 收到非零返回值后,会跳过后续插件的初始化并报告警告。
|
||||
|
||||
**契约 3:register_service 注册自己的服务。** 插件将自己的 vtable 注册到服务注册表后,其他依赖它的插件才能在后续的 `on_init` 中通过 `query_service` 找到它。
|
||||
|
||||
```c
|
||||
return host->register_service("ai.deepseek", 1, &g_service);
|
||||
```
|
||||
|
||||
注册表内的 vtable 是原始指针,不拷贝。因此 vtable 指向的结构体必须是**静态生命周期**(全局变量或 static 局部变量)。
|
||||
|
||||
**契约 4:不要在 on_init 中做阻塞操作。** 当前 Host 是单线程初始化,阻塞一个插件的 `on_init` 会阻塞整个启动流程。如果需要异步初始化(如连接远程服务),在 `on_init` 中仅做最基本的 vtable 注册,把长连接放到首次服务调用时再建立。
|
||||
|
||||
**契约 5:所有内存分配通过 host_api->alloc/free。** 见下文"ABI 纪律"节。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 4:on_shutdown 的契约
|
||||
|
||||
Host 关闭时,按拓扑排序的**逆序**调用 `on_shutdown`——这保证了被依赖者后卸载。
|
||||
|
||||
```
|
||||
加载顺序: config → http → deepseek
|
||||
卸载顺序: deepseek → http → config
|
||||
```
|
||||
|
||||
注意:如果拓扑排序失败(如循环依赖),`shutdown_all` 会退化为任意顺序,仅保证所有插件的 `on_shutdown` 都被调用、所有 DLL 句柄都被释放。
|
||||
|
||||
### 契约条款
|
||||
|
||||
**契约 1:逆序卸载,释放持久的服务引用。** 在 `on_init` 中保存的服务指针(如 `g_http`)应在 `on_shutdown` 中置为 `nullptr`。这防止插件在卸载后仍持有悬垂指针——虽然当前实现是在 `on_shutdown` 之后才释放 DLL,但防御性置空是好习惯。
|
||||
|
||||
```c
|
||||
static void on_shutdown() {
|
||||
g_http = nullptr;
|
||||
g_config = nullptr;
|
||||
g_host = nullptr;
|
||||
}
|
||||
```
|
||||
|
||||
**契约 2:不能跨 DLL 堆边界释放。** 在插件 A 的 `on_shutdown` 中,如果还持有插件 B 分配的内存,不能简单地调用 `g_host->free`——这会触发跨堆释放的未定义行为。正确做法是调用提供方的专属释放函数(如 AI 服务的 `free_result`),或让提供方在 `on_shutdown` 中清理自己分配的资源。
|
||||
|
||||
**契约 3:删除已注册的服务。** 当前 `ServiceRegistry` 不自动清理。如果一个服务 remove 了对应的插件,应该在卸载期间调用 `unregister_service`。当前实现未强制这一点,但关闭过程会销毁整个 `ServiceRegistry`,所以注销是可选的(若不注销,重启 host 时不会残留)。
|
||||
|
||||
---
|
||||
|
||||
## ABI 纪律:为什么 Host 提供 alloc / free
|
||||
|
||||
这是插件架构中最容易被忽视但最容易出错的问题。
|
||||
|
||||
**问题**:在 Windows 上,每个 DLL 可能链接不同的 CRT(C 运行时库)。DLL A 用 MSVC 2022 的 `malloc` 分配内存,DLL B 用 MSVC 2019 的 `free` 释放——两个 `free` 管理不同的堆,导致崩溃或堆损坏。
|
||||
|
||||
**解决**:dstalk 要求所有跨 DLL 边界的内存操作使用 Host 提供的统一分配/释放函数:
|
||||
|
||||
| 操作 | 用这个 |
|
||||
|------|--------|
|
||||
| 分配内存 | `host_api->alloc(size)` → 调用 host 启动时链接的 `malloc` |
|
||||
| 释放内存 | `host_api->free(ptr)` → 调用 host 启动时链接的 `free` |
|
||||
| 复制字符串 | `host_api->strdup(s)` → 用 host 的 `malloc` + `memcpy` |
|
||||
|
||||
```c
|
||||
// 正确:跨 DLL 返回的字符串用 host 分配
|
||||
r.content = g_host->strdup(ctx.accumulated.c_str());
|
||||
|
||||
// 正确:调用方释放跨 DLL 返回的数据用 host 释放
|
||||
g_host->free((void*)result->content);
|
||||
|
||||
// 错误:用本地 malloc 分配跨 DLL 边界的数据
|
||||
// r.content = strdup(...); // 消费者的 free() 和此 strdup 的 malloc() 可能不同堆!
|
||||
```
|
||||
|
||||
**规则记忆**: 谁分配,谁负责提供释放手段。dstalk 的选择是让 Host 统一分配——所有 `alloc/free` 调用走同一个 CRT 的堆。
|
||||
|
||||
---
|
||||
|
||||
## 生命周期速查
|
||||
|
||||
| 阶段 | 谁触发 | 插件状态 | 关键函数 |
|
||||
|------|--------|----------|----------|
|
||||
| DLL 加载 | `dstalk_init` 或 `dstalk_plugin_load` | 未初始化 | `load_plugin` → `dstalk_plugin_init()` |
|
||||
| 依赖排序 | `initialize_all` | 等待初始化 | `topological_sort()` (Kahn) |
|
||||
| 初始化 | Host 按序调用 | 运行中 | `on_init(host_api)` |
|
||||
| 服务调用 | 任意插件/CLI 前端 | 运行中 | `query_service` → vtable 调用 |
|
||||
| 卸载 | `dstalk_plugin_unload` 或 `dstalk_shutdown` | 关闭中 | `on_shutdown()` → `FreeLibrary`/`dlclose` |
|
||||
109
docs/explanation/security-logging.md
Normal file
109
docs/explanation/security-logging.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Security Logging Audit (W9.3): 错误日志凭证泄露审计
|
||||
|
||||
**审计人**: 曹武 (security-cao)
|
||||
**日期**: 2026-05-27
|
||||
**审计范围**: 8 个核心/插件源码文件
|
||||
**结论**: 未发现真实凭证泄露漏洞(CVSS 不适用,零高危/中危/低危可利用漏洞)
|
||||
|
||||
---
|
||||
|
||||
## 审计方法论
|
||||
|
||||
对每个文件搜索以下输出调用:
|
||||
- `host->log(...)` / `g_host->log(...)` (插件日志 API)
|
||||
- `printf` / `fprintf(stderr, ...)` (C 标准输出)
|
||||
- `std::cerr` / `std::cout` (C++ 标准流)
|
||||
|
||||
对每个匹配项检查其格式字符串和参数是否包含 `api_key`、`Authorization` header、`token` 或原始 request body/headers。
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 1. `plugins/deepseek/src/deepseek_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" | 无 |
|
||||
| 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 -- 全程无日志记录。
|
||||
|
||||
**parse_response() 错误路径 (行 135-151)**: HTTP 错误响应体仅用于提取 JSON `error.message` 字段放入 `r.error`,不会输出到日志。原始 response_body 在解析后被 `g_host->free()` 释放。
|
||||
|
||||
### 2. `plugins/anthropic/src/anthropic_plugin.cpp` -- 安全
|
||||
|
||||
| 行号 | 调用 | 内容 | 风险 |
|
||||
|------|------|------|------|
|
||||
| 247-250 | `g_host->log(INFO, ...)` | 输出 model / base_url / max_tokens / temperature | 无 -- api_key 被有意排除 |
|
||||
| 453 | `g_host->log(ERROR, ...)` | 静态字符串 "http service not found" | 无 |
|
||||
| 457 | `g_host->log(INFO, ...)` | 静态字符串 "initializing Anthropic AI plugin" | 无 |
|
||||
| 464 | `g_host->log(INFO, ...)` | 静态字符串 "shutdown" | 无 |
|
||||
|
||||
**build_headers_json() (行 59-65)**: 构建 `{"x-api-key":"<key>","anthropic-version":"2023-06-01"}` 仅用于 HTTP 请求,不经过日志路径。
|
||||
|
||||
### 3. `plugins/network/src/network_plugin.cpp` -- 安全
|
||||
|
||||
| 行号 | 调用 | 内容 | 风险 |
|
||||
|------|------|------|------|
|
||||
| -- | 无任何 `host->log` / `printf` / `cerr` 调用 | -- | 无 |
|
||||
|
||||
**parse_headers_json() (行 40-80)**: 解析包含 api_key 的 headers JSON 字符串。解析结果仅用于 `req.set(h.first, h.second)` (行 176) 设置 HTTP header,不输出日志。
|
||||
|
||||
**do_post_stream() 异常路径 (行 280-282)**: `catch (std::exception& e)` 将 `e.what()` 赋值给 `result_body`。Beast/ASIO 异常消息为 OS 级别错误描述(如 "Connection refused"),不含 HTTP header/body 内容。
|
||||
|
||||
### 4. `plugins/config/src/config_plugin.cpp` -- 安全
|
||||
|
||||
| 行号 | 调用 | 内容 | 风险 |
|
||||
|------|------|------|------|
|
||||
| -- | 无任何日志调用 | -- | 无 |
|
||||
|
||||
ConfigStore 仅提供 get/set/load_file,无日志输出。
|
||||
|
||||
### 5. `dstalk-core/src/host.cpp` -- 基础设施(不动)
|
||||
|
||||
| 行号 | 调用 | 内容 | 风险 |
|
||||
|------|------|------|------|
|
||||
| 48,51,52 | `fprintf(stderr, ...)` | 日志前缀 + vfprintf(格式,参数) + 换行 | 无 -- 基础设施自身不包含业务数据 |
|
||||
|
||||
该文件是日志基础设施 (`host_log_impl`),仅负责格式化输出。安全性依赖于调用方不传敏感数据(本审计已确认所有调用方均安全)。按 W9.3 禁忌不修改此文件。
|
||||
|
||||
### 6. `dstalk-core/src/plugin_loader.cpp` -- 安全
|
||||
|
||||
| 行号 | 调用 | 内容 | 风险 |
|
||||
|------|------|------|------|
|
||||
| -- | 无任何日志调用 | -- | 无 |
|
||||
|
||||
### 7. `plugins/session/src/session_plugin.cpp` -- 安全
|
||||
|
||||
| 行号 | 调用 | 内容 | 风险 |
|
||||
|------|------|------|------|
|
||||
| 233 | `host->log(ERROR, ...)` | 静态字符串 "required service 'file_io' not found" | 无 |
|
||||
|
||||
该插件处理消息内容(role/content)但不记录任何消息数据到日志。
|
||||
|
||||
### 8. `plugins/lsp/src/lsp_plugin.cpp` -- 低风险
|
||||
|
||||
| 行号 | 调用 | 内容 | 风险 |
|
||||
|------|------|------|------|
|
||||
| 446 | `g_host->log(ERROR, ...)` | 静态字符串 "Invalid LSP frame..." | 无 |
|
||||
| 479 | `g_host->log(ERROR, ...)` | `"failed to start: %s", server_cmd` | 低 -- 理论上 server_cmd 可能含令牌,但实际为 LSP 服务器路径(如 `clangd`),不属于 API 密钥范畴 |
|
||||
| 525 | `g_host->log(ERROR, ...)` | 静态字符串 "initialize timed out" | 无 |
|
||||
| 535 | `g_host->log(INFO, ...)` | `"server started: %s", server_cmd` | 低 -- 同上 |
|
||||
| 565 | `g_host->log(INFO, ...)` | 静态字符串 "server stopped" | 无 |
|
||||
| 720 | `g_host->log(INFO, ...)` | 静态字符串 "initializing LSP service plugin" | 无 |
|
||||
| 728 | `g_host->log(INFO, ...)` | 静态字符串 "shutdown" | 无 |
|
||||
|
||||
`server_cmd` 是启动 LSP 子进程的命令行(例如 `clangd --log=error`),不是 API 密钥或 token。若用户将 API key 嵌入命令行(极不寻常且不安全的使用方式),则存在理论泄漏风险。判断为**低风险/假阳性**,不做代码修改。
|
||||
|
||||
## 总结
|
||||
|
||||
| 风险等级 | 数量 | 说明 |
|
||||
|----------|------|------|
|
||||
| 严重 (CVSS 9.0+) | 0 | 无 |
|
||||
| 高危 (CVSS 7.0-8.9) | 0 | 无 |
|
||||
| 中危 (CVSS 4.0-6.9) | 0 | 无 |
|
||||
| 低危 (CVSS 0.1-3.9) | 0 | 无真实可利用漏洞 |
|
||||
| 低风险/假阳性 | 2 | 仅 lsp `server_cmd` 日志和 network `e.what()` 理论上可能暴露非凭证信息 |
|
||||
|
||||
**审计结论**: 所有日志输出路径均已检查,证实 DeepSeek 和 Anthropic 插件的 `my_configure()` 日志有意排除了 `api_key` 字段。HTTP headers 中的凭证仅通过内存传递至 Beast HTTP 请求对象,从未进入日志管道。代码库对此攻击面防御充分,无需修改。
|
||||
39
docs/reference/commands.md
Normal file
39
docs/reference/commands.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# CLI 命令速查
|
||||
|
||||
dstalk 所有内置命令。在对话中直接输入 `/help` 或 `/h` 也可查看此列表。
|
||||
|
||||
---
|
||||
|
||||
## 命令表
|
||||
|
||||
| 命令 | 别名 | 作用 | 示例 |
|
||||
|------|------|------|------|
|
||||
| `/help` | `/h` | 显示命令列表 | `/help` |
|
||||
| `/quit` | `/q` | 退出程序 | `/quit` |
|
||||
| `/clear` | — | 清空当前会话上下文 | `/clear` |
|
||||
| `/context` | — | 显示当前 Token 数和消息条数 | `/context` |
|
||||
| `/status` | — | 显示当前运行状态 (脱敏: 不打印完整 API Key) | `/status` |
|
||||
| `/model <name>` | — | 切换 AI 模型 | `/model deepseek-v4-pro` |
|
||||
| `/file list [path]` | — | 列出目录内容, 不填 path 列出当前目录 | `/file list src/` |
|
||||
| `/file show <path>` | — | 查看文件内容 | `/file show main.cpp` |
|
||||
| `/file read <path>` | — | 读取文件内容 (同 `/file show`) | `/file read config.toml` |
|
||||
| `/file write <path> <content>` | — | 写入文件内容 | `/file write hello.c #include <stdio.h>...` |
|
||||
| `/save <path>` | — | 保存当前会话到文件 | `/save session.json` |
|
||||
| `/load <path>` | — | 从文件恢复会话 | `/load session.json` |
|
||||
|
||||
---
|
||||
|
||||
## 命令规则
|
||||
|
||||
- 所有命令以 `/` 开头
|
||||
- 不以下划线开头的输入视为 AI 对话内容, 由 AI 服务处理
|
||||
- 模型切换即时生效, 不影响已累积的会话历史
|
||||
- `/file show` 和 `/file read` 功能相同, 均为读取并打印文件内容
|
||||
- `/status` 显示脱敏信息: API Key 只显示 "已设置" 或 "未设置", 不暴露完整值
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [快速入门教程](../tutorial/quick-start.md) — 5 步上手
|
||||
- [文档导航](../README.md) — 全部文档索引
|
||||
236
docs/reference/plugin-abi.md
Normal file
236
docs/reference/plugin-abi.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# dstalk Plugin ABI 契约
|
||||
|
||||
> **面向**: 插件作者、host 维护者
|
||||
> **性质**: 规范性文档。违反任何条目 = 未定义行为。
|
||||
|
||||
---
|
||||
|
||||
## 1. DSTALK_API_VERSION
|
||||
|
||||
```c
|
||||
#define DSTALK_API_VERSION 1 // dstalk_host.h
|
||||
```
|
||||
|
||||
**语义**: 主版本号。当且仅当 `dstalk_host_api_t` 的字段布局、`dstalk_plugin_info_t` 的结构、或任意
|
||||
`*_service_t` vtable 的函数签名发生**不兼容变更**时 bump。
|
||||
|
||||
**不 bump 的情况**: 新增 event type 枚举值、新增日志级别、在 vtable 末尾追加函数指针(注意:
|
||||
不能改变已有字段的偏移)。
|
||||
|
||||
**匹配规则**: `plugin::api_version` 必须**精确等于** `DSTALK_API_VERSION`。host 拒绝加载不匹配的
|
||||
插件(`plugin_loader.cpp:68`),不提供后向兼容。这是硬断点——旧插件在新 host 上重新编译即可适配,
|
||||
二进制兼容不做保证。
|
||||
|
||||
**版本协商被拒绝的理由**: 早期版本刻意保持简单。如果未来需要协商(如 min/max version range),
|
||||
通过 bump `DSTALK_API_VERSION` 到 2 并定义协商结构体即可。W1.x 阶段不需要。
|
||||
|
||||
---
|
||||
|
||||
## 2. 内存所有权契约
|
||||
|
||||
Core rule: **谁分配,谁释放。分配函数必须与释放函数配对。**
|
||||
|
||||
### 2.1 Host 分配的字符串
|
||||
|
||||
以下函数返回的 `const char*` / `char*` **由 host 拥有**,调用方不得释放:
|
||||
|
||||
| 来源 | 示例 | 生命周期 |
|
||||
|------|------|----------|
|
||||
| `host->config_get(key)` | 配置值字符串 | 随 config 条目存在,或下次 `config_set` 覆盖前有效 |
|
||||
| `dstalk_chat_result_t.content` | AI 回复 | 必须在读取后立即复制;下一轮 `chat()` 调用可能覆盖 |
|
||||
|
||||
以下函数返回的 `char*` **由调用方释放**(用 `dstalk_free` / `host->free`):
|
||||
|
||||
| 函数 | 释放方式 |
|
||||
|------|----------|
|
||||
| `dstalk_strdup(s)` / `host->strdup(s)` | `dstalk_free(ptr)` / `host->free(ptr)` |
|
||||
| `dstalk_plugin_list(output_json)` | `dstalk_free(*output_json)` |
|
||||
| LSP 便捷函数 (`dstalk_lsp_*`) 的 `char** output` 参数 | `dstalk_free(*output)` |
|
||||
|
||||
### 2.2 插件返回的字符串
|
||||
|
||||
`dstalk_plugin_info_t` 中的 `name`、`version`、`description` 由插件的 `dstalk_plugin_init()`
|
||||
返回。host 在加载时读取并内部分配 `std::string` 副本(`plugin_loader.cpp:81-83`),随后不再
|
||||
引用原始指针。因此这些字符串的**生命周期只需覆盖 `init_fn()` 调用期间**——可以是静态字面量、
|
||||
栈上字符串、或插件内部分配的内存。
|
||||
|
||||
### 2.3 服务 vtable 的返回值
|
||||
|
||||
`dstalk_chat_result_t.content`、`.error`、`.tool_calls_json` 按结构体注释约定由 `dstalk_strdup`
|
||||
分配,**调用方**(即查询该服务的插件)负责 `dstalk_free`。
|
||||
|
||||
**反例**: 插件直接返回 `std::string::c_str()` 或栈上 buffer —— 因为服务调用完成后插件栈帧
|
||||
可能已销毁。
|
||||
|
||||
---
|
||||
|
||||
## 3. 跨 DLL 堆纪律
|
||||
|
||||
### 3.1 问题
|
||||
|
||||
Windows 上每个 DLL 拥有独立的 CRT 堆(取决于链接方式:/MD 共享 或 /MT 静态)。插件在其
|
||||
CRT 中调用 `malloc` 得到的指针,host 调用 `free` 时访问的是 host CRT 的堆——行为未定义。
|
||||
|
||||
Linux/macOS 通常共享 libc,但静态链接或不同 libc 版本时同样可能 crash。
|
||||
|
||||
### 3.2 硬性规则
|
||||
|
||||
> **严禁插件直接调用 `malloc`/`free`/`strdup`/`new`/`delete` 处理 host 传入或传出的数据。**
|
||||
|
||||
正确做法:
|
||||
|
||||
| 场景 | 错误做法 | 正确做法 |
|
||||
|------|----------|----------|
|
||||
| 释放 host 给的字符串 | `free(host->config_get("key"))` | 不释放;只读后丢弃 |
|
||||
| 分配传给 host 的缓冲区 | `malloc(256)` | `host->alloc(256)` 或 `dstalk_alloc(256)` |
|
||||
| 释放 host 分配的内存 | `delete ptr` / `std::free(ptr)` | `host->free(ptr)` 或 `dstalk_free(ptr)` |
|
||||
| 复制 host 给的字符串 | `strdup(host->config_get("k"))` | `host->strdup(...)` 或 `dstalk_strdup(...)` |
|
||||
| 在插件 DLL 内分配/释放私有数据 | 可以用任何方式 | 只要不跨越 DLL 边界 |
|
||||
|
||||
### 3.3 设计理由
|
||||
|
||||
`host->alloc` / `host->free` / `host->strdup` 是通过函数指针调回 host DLL 的 `malloc`/`free`,
|
||||
保证分配和释放发生在**同一个堆**上。`g_host_api` 表的 `api_alloc`/`api_free` 直接就是 `malloc`
|
||||
/`free`(`host.cpp:111-112`),所以 "host 分配 → host 释放" 总是在同一个 CRT 堆内。
|
||||
|
||||
---
|
||||
|
||||
## 4. register_service 契约
|
||||
|
||||
### 4.1 调用时机
|
||||
|
||||
`host->register_service(name, version, vtable)` **仅可在 `on_init` 回调期间调用**。
|
||||
|
||||
原因:
|
||||
- 服务注册表 `ServiceRegistry` 在 `dstalk_init()` 内创建,`on_init` 之前已存在。
|
||||
- 初始化顺序由拓扑排序保证:依赖方的 `on_init` 在被依赖方之后调用,因此被依赖方注册的服务
|
||||
在依赖方调用时已可用。
|
||||
- `on_shutdown` 期间不应注册新服务(该阶段仅做清理)。
|
||||
|
||||
### 4.2 重复注册
|
||||
|
||||
同一 `name` 不可重复注册:第二次调用返回 `-2`(`service_registry.cpp:13`)。插件应检查返回
|
||||
值,在共享服务名(如 `"ai.deepseek"`)的场景中避免冲突。
|
||||
|
||||
### 4.3 版本协商
|
||||
|
||||
`register_service` 的 `version` 参数声明了该 vtable 实现的版本。`query_service` 的
|
||||
`min_version` 允许调用方声明最低需求版本。当前实现为整型比较:`registered_version >=
|
||||
min_version` 即可。
|
||||
|
||||
**插件作者约定**: version 从 1 开始。vtable 新增函数指针(追加到末尾)→ bump version。
|
||||
vtable 重排字段或删除函数 → 改 service name(如 `"lsp"` → `"lsp2"`)。
|
||||
|
||||
### 4.4 vtable 生命周期
|
||||
|
||||
注册的 vtable 指针在 `ServiceRegistry` 中存储,随插件存在。插件卸载时,`ServiceRegistry::
|
||||
unregister_service` 会被调用。**插件不得在卸载后继续持有该 vtable 指针**。
|
||||
|
||||
---
|
||||
|
||||
## 5. on_init / on_shutdown 契约
|
||||
|
||||
### 5.1 调用顺序保证
|
||||
|
||||
- **on_init**: 按拓扑顺序调用(依赖项先初始化)。由 `PluginLoader::topological_sort()`
|
||||
使用 Kahn 算法计算(`plugin_loader.cpp:144-198`)。
|
||||
- **on_shutdown**: 按拓扑顺序的**逆序**调用(依赖项后销毁)。如果拓扑排序因循环依赖失败,
|
||||
降级为任意顺序(`plugin_loader.cpp:263-267`)。
|
||||
- **增量加载**: `dstalk_plugin_load()` 只初始化新加载的插件(`initialize_pending`),已
|
||||
初始化的插件不受影响。
|
||||
|
||||
### 5.2 返回值
|
||||
|
||||
`on_init` 返回 `0` = 成功;非零 = 失败。失败导致 `initialize_all` 返回 `-1`,整体初始化
|
||||
流程中止。失败插件的 `on_shutdown` **不会被调用**(`plugin.initialized` 保持 false)。
|
||||
|
||||
### 5.3 异常安全 (C++ ABI)
|
||||
|
||||
> **C++ 异常不得穿越 C ABI 边界。**
|
||||
|
||||
`on_init` 和 `on_shutdown` 定义为 C 函数指针:
|
||||
|
||||
```c
|
||||
int (*on_init)(const dstalk_host_api_t* host);
|
||||
void (*on_shutdown)(void);
|
||||
```
|
||||
|
||||
调用方(PluginLoader)**不设置** `try/catch` 保护(`plugin_loader.cpp:212-214`)。
|
||||
如果 `on_init` 由 C++ 实现且抛出异常,将导致 `std::terminate` / 未定义行为。
|
||||
|
||||
**防护规则**:
|
||||
- 用 `extern "C"` 声明实现
|
||||
- 所有可能抛异常的 C++ 逻辑用 `try { ... } catch (...) { return -1; }` 包裹
|
||||
- `on_shutdown` 同理,即使 void 也不能抛异常
|
||||
|
||||
---
|
||||
|
||||
## 6. 回调线程安全
|
||||
|
||||
### 6.1 诊断回调 (diag_callback)
|
||||
|
||||
`g_diag_callback` 为 `std::atomic<dstalk_diag_cb>`,使用 `memory_order_acquire` / `release`
|
||||
(`host.cpp:28,54,305`)。**多线程同时调用 `dstalk_log` 安全**——每个线程独立读取原子指针,
|
||||
无数据竞争。
|
||||
|
||||
### 6.2 事件总线 (EventBus)
|
||||
|
||||
`EventBus` 使用 `std::shared_mutex`:
|
||||
- `subscribe` / `unsubscribe` 持有 `unique_lock`
|
||||
- `emit` 持有 `shared_lock`
|
||||
|
||||
**结论**: 多线程 subscribe/unsubscribe/emit 安全。但 emit 持有 shared_lock 期间直接调用
|
||||
handler —— handler 内**不得调用 subscribe/unsubscribe**(会尝试 unique_lock 导致死锁)。
|
||||
|
||||
`on_event` 回调与 `on_init` / `on_shutdown` 之间无互斥保护,因此**不应在 `on_event` 回调
|
||||
内调用 host API 执行插件的 load/unload**(PluginLoader 无内部锁)。
|
||||
|
||||
### 6.3 服务注册表 (ServiceRegistry)
|
||||
|
||||
使用 `std::shared_mutex`:register/unregister 持写锁,query 持读锁。并发安全。
|
||||
|
||||
### 6.4 配置 (ConfigStore)
|
||||
|
||||
使用 `std::mutex`:get/set 串行化。`config_get` 返回的指针指向内部 `std::string`;在并发
|
||||
`config_set` 同一 key 后指针可能悬垂——调用方应复制。
|
||||
|
||||
### 6.5 Plugin Loader
|
||||
|
||||
`PluginLoader` **无内部互斥**(`plugin_loader.hpp` 无 mutex 成员)。load/unload 不应在
|
||||
多线程中并发调用。这是 host 层的设计假设——仅 `dstalk_init` / `dstalk_shutdown` 和显式
|
||||
CLI 命令使用。
|
||||
|
||||
---
|
||||
|
||||
## 7. 依赖声明 (dependencies)
|
||||
|
||||
### 7.1 语法
|
||||
|
||||
```c
|
||||
const char* dependencies[DSTALK_MAX_DEPS]; // DSTALK_MAX_DEPS = 8
|
||||
```
|
||||
|
||||
以 `NULL` 终止的字符串数组,每个元素为被依赖插件的 `name`。
|
||||
|
||||
### 7.2 语义
|
||||
|
||||
- 被依赖项必须先于依赖方初始化(拓扑排序保证)
|
||||
- 被依赖项必须后于依赖方销毁(逆序 shutdown)
|
||||
- 如果被依赖项不存在:初始化时不影响拓扑排序(仅计算已加载插件间的依赖关系),但插件的
|
||||
`on_init` 自己应检查所需服务是否可用(通过 `host->query_service`)
|
||||
- 循环依赖:拓扑排序失败,`topological_sort()` 抛出 `std::runtime_error("Circular
|
||||
dependency detected")`,被 `initialize_all` 捕获并返回 `-1`
|
||||
- 最大 8 个依赖,超出部分截断(`plugin_loader.cpp:90`)
|
||||
|
||||
### 7.3 最佳实践
|
||||
|
||||
`on_init` 内通过 `host->query_service("service_name", min_version)` 验证依赖可用。
|
||||
不做假设。这也是一种隐式版本协商:如果 service version 不足,`on_init` 返回错误。
|
||||
|
||||
---
|
||||
|
||||
## 变更历史
|
||||
|
||||
| 日期 | 版本 | 变更 |
|
||||
|------|------|------|
|
||||
| 2026-05-27 | 1.0 | 初始版本。W9.4 交付。基于 DSTALK_API_VERSION=1 的当前实现。 |
|
||||
140
docs/tutorial/quick-start.md
Normal file
140
docs/tutorial/quick-start.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 快速入门
|
||||
|
||||
5 步上手 dstalk, 从安装工具链到完成第一个 AI 对话。
|
||||
|
||||
---
|
||||
|
||||
## 1. 安装工具链
|
||||
|
||||
dstalk 需要 CMake、Ninja、LLVM/Clang 和 Conan2。`setup.bat` 全自动下载安装到 `tools/` 目录。
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
setup.bat
|
||||
```
|
||||
|
||||
> **前提**: 系统需已安装 Python 3.10+。
|
||||
>
|
||||
> 网络不畅时, 可手动下载放入对应目录:
|
||||
> - [Ninja](https://github.com/ninja-build/ninja/releases) → `tools/ninja/ninja.exe`
|
||||
> - [CMake](https://cmake.org/download/) → `tools/cmake/bin/cmake.exe`
|
||||
> - [LLVM](https://github.com/llvm/llvm-project/releases) → `tools/llvm/bin/clang.exe`
|
||||
> - Conan2 通过 pip 安装到 `tools/.venv/Scripts/conan.exe`
|
||||
|
||||
---
|
||||
|
||||
## 2. 编译
|
||||
|
||||
项目根目录提供 `build.bat`, 一键完成: Conan 拉取依赖 -> CMake 配置 -> Ninja 编译。
|
||||
|
||||
```bash
|
||||
build.bat
|
||||
```
|
||||
|
||||
编译产物输出到 `build/` 目录。核心产物:
|
||||
- `build/dstalk-core/dstalk.dll` —— 核心 DLL
|
||||
- `build/dstalk-cli/dstalk-cli.exe` —— 命令行前端
|
||||
- `build/plugins/*.dll` —— 功能插件
|
||||
|
||||
---
|
||||
|
||||
## 3. 创建 config.toml
|
||||
|
||||
在项目根目录创建 `config.toml`, 配置 AI 后端和 API Key。
|
||||
|
||||
```bash
|
||||
# 在项目根目录手动创建 config.toml
|
||||
```
|
||||
|
||||
**config.toml 示例:**
|
||||
|
||||
```toml
|
||||
# 选择 AI 后端插件: ai.deepseek 或 ai.anthropic
|
||||
ai.provider = "ai.deepseek"
|
||||
|
||||
# DeepSeek
|
||||
api.base_url = "https://api.deepseek.com/v1"
|
||||
api.api_key = "sk-xxxxxxxx"
|
||||
api.model = "deepseek-v4-pro"
|
||||
|
||||
# Anthropic Claude (切换 ai.provider 为 "ai.anthropic" 即可)
|
||||
# api.base_url = "https://api.anthropic.com/v1"
|
||||
# api.api_key = "sk-ant-xxxxxxxx"
|
||||
# api.model = "claude-opus-4-20250514"
|
||||
```
|
||||
|
||||
> **关键**: 修改 `ai.provider` 字段即可在不同后端间切换, 无需改动代码。
|
||||
>
|
||||
> API Key 可从 [DeepSeek 开放平台](https://platform.deepseek.com/) 或 [Anthropic Console](https://console.anthropic.com/) 获取。
|
||||
|
||||
---
|
||||
|
||||
## 4. 运行 dstalk-cli
|
||||
|
||||
```bash
|
||||
build/dstalk-cli/dstalk-cli.exe
|
||||
```
|
||||
|
||||
启动后显示欢迎横幅:
|
||||
|
||||
```text
|
||||
dstalk v0.1.0 | dstalk AI | /help 查看帮助 | /quit 退出
|
||||
|
||||
[deepseek-v4-pro] >
|
||||
```
|
||||
|
||||
> 图形模式默认关闭。需要 SDL3 GUI 时, 用 `-DDSTALK_BUILD_GUI=ON` 重新配置 CMake。
|
||||
|
||||
---
|
||||
|
||||
## 5. 第一个对话
|
||||
|
||||
在提示符 `>` 后输入自然语言, 即可与 AI 对话。
|
||||
|
||||
```text
|
||||
[deepseek-v4-pro] > 帮我写一个读取 CSV 并计算平均值的 C 程序
|
||||
|
||||
[dstalk] 正在思考...
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "用法: %s <csv文件>\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
FILE *fp = fopen(argv[1], "r");
|
||||
if (!fp) { perror("fopen"); return 1; }
|
||||
|
||||
double sum = 0.0;
|
||||
int count = 0;
|
||||
char line[1024];
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
sum += atof(line);
|
||||
count++;
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
printf("平均值: %.2f (共 %d 行)\n", sum / count, count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
已写入 csv_avg.c。需要我帮你编译测试吗?
|
||||
|
||||
[deepseek-v4-pro] > 把这段代码改成支持表头的
|
||||
|
||||
[dstalk] 已更新 csv_avg.c——跳过第一行表头, 增加列选择功能。
|
||||
|
||||
[deepseek-v4-pro] > /file show csv_avg.c
|
||||
|
||||
[dstalk] 已显示 csv_avg.c 内容。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
- 查看 [CLI 命令速查表](../reference/commands.md) 了解全部命令
|
||||
- 输入 `/help` 在 dstalk 内查看命令列表
|
||||
- 输入 `/status` 查看当前运行状态
|
||||
@@ -9,9 +9,3 @@ add_executable(dstalk-cli
|
||||
target_link_libraries(dstalk-cli
|
||||
PRIVATE dstalk
|
||||
)
|
||||
|
||||
add_custom_command(TARGET dstalk-cli POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_FILE:dstalk>
|
||||
$<TARGET_FILE_DIR:dstalk-cli>
|
||||
)
|
||||
|
||||
@@ -92,6 +92,7 @@ static void print_help()
|
||||
std::printf(" %s/file write <p> <c>%s 写入文件\n", CLR_YELLOW, CLR_RESET);
|
||||
std::printf(" %s/save <path>%s 保存会话\n", CLR_YELLOW, CLR_RESET);
|
||||
std::printf(" %s/load <path>%s 恢复会话\n", CLR_YELLOW, CLR_RESET);
|
||||
std::printf(" %s/history [N]%s 查看会话历史(默认全部,可指定最近N条)\n", CLR_YELLOW, CLR_RESET);
|
||||
std::printf("\n直接输入问题即可与 AI 对话。\n\n");
|
||||
}
|
||||
|
||||
@@ -197,6 +198,9 @@ static void handle_command(const char* line)
|
||||
std::printf(" provider: %s\n", provider);
|
||||
std::printf(" AI 服务: %s\n", g_ai ? "就绪" : "不可用");
|
||||
std::printf(" Session 服务: %s\n", g_session ? "就绪" : "不可用");
|
||||
int hc = 0;
|
||||
if (g_session) g_session->history(&hc);
|
||||
std::printf(" history count: %d\n", hc);
|
||||
std::printf(" File IO 服务: %s\n", g_file_io ? "就绪" : "不可用");
|
||||
const dstalk_tools_service_t* tools = static_cast<const dstalk_tools_service_t*>(
|
||||
dstalk_service_query("tools", 1));
|
||||
@@ -261,6 +265,39 @@ static void handle_command(const char* line)
|
||||
return;
|
||||
}
|
||||
|
||||
// /history [N]
|
||||
if (std::strcmp(line, "/history") == 0 || std::strncmp(line, "/history ", 9) == 0) {
|
||||
if (!g_session) {
|
||||
std::printf(CLR_RED "[ERROR] session service unavailable\n" CLR_RESET);
|
||||
return;
|
||||
}
|
||||
int count = 0;
|
||||
const dstalk_message_t* history = g_session->history(&count);
|
||||
if (count == 0 || !history) {
|
||||
std::printf(CLR_DIM "(history is empty)\n" CLR_RESET);
|
||||
return;
|
||||
}
|
||||
int limit = count;
|
||||
const char* arg = line + 8;
|
||||
while (*arg == ' ') arg++;
|
||||
if (*arg != '\0') {
|
||||
char* end = nullptr;
|
||||
long n = std::strtol(arg, &end, 10);
|
||||
if (*end != '\0' || n <= 0) {
|
||||
std::printf(CLR_RED "[ERROR] /history N: N must be a positive integer\n" CLR_RESET);
|
||||
return;
|
||||
}
|
||||
limit = (n < count) ? static_cast<int>(n) : count;
|
||||
}
|
||||
int start = count - limit;
|
||||
for (int i = start; i < count; i++) {
|
||||
std::printf("[%s] %s\n",
|
||||
history[i].role ? history[i].role : "?",
|
||||
history[i].content ? history[i].content : "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// /save <path>
|
||||
if (std::strncmp(line, "/save ", 6) == 0) {
|
||||
const char* path = line + 6;
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
find_package(Boost REQUIRED CONFIG)
|
||||
find_package(OpenSSL REQUIRED CONFIG)
|
||||
|
||||
# 统一的 Boost 编译宏 (header-only 模式)
|
||||
add_library(dstalk_boost_config INTERFACE)
|
||||
target_compile_definitions(dstalk_boost_config INTERFACE
|
||||
BOOST_ALL_NO_LIB
|
||||
BOOST_ERROR_CODE_HEADER_ONLY
|
||||
BOOST_JSON_HEADER_ONLY
|
||||
)
|
||||
|
||||
add_library(dstalk SHARED
|
||||
src/host.cpp
|
||||
src/config_store.cpp
|
||||
@@ -20,8 +28,11 @@ target_include_directories(dstalk
|
||||
PRIVATE src
|
||||
)
|
||||
|
||||
target_compile_features(dstalk PUBLIC cxx_std_20)
|
||||
|
||||
target_link_libraries(dstalk
|
||||
PRIVATE
|
||||
dstalk_boost_config
|
||||
boost::boost
|
||||
openssl::openssl
|
||||
)
|
||||
@@ -35,9 +46,6 @@ endif()
|
||||
target_compile_definitions(dstalk
|
||||
PRIVATE
|
||||
DSTALK_BUILD_DLL
|
||||
BOOST_ALL_NO_LIB
|
||||
BOOST_ERROR_CODE_HEADER_ONLY
|
||||
BOOST_JSON_HEADER_ONLY
|
||||
INTERFACE
|
||||
DSTALK_USE_DLL
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "service_registry.hpp"
|
||||
#include "plugin_loader.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
@@ -24,7 +25,7 @@ namespace {
|
||||
dstalk::EventBus* g_event_bus = nullptr;
|
||||
dstalk::ServiceRegistry* g_service_registry = nullptr;
|
||||
dstalk::PluginLoader* g_plugin_loader = nullptr;
|
||||
static dstalk_diag_cb g_diag_callback = nullptr;
|
||||
static std::atomic<dstalk_diag_cb> g_diag_callback{nullptr};
|
||||
|
||||
// ---- 内部辅助 ----
|
||||
|
||||
@@ -50,10 +51,11 @@ namespace {
|
||||
vfprintf(stderr, fmt, args);
|
||||
fprintf(stderr, "\n");
|
||||
// 转发到诊断回调
|
||||
if (g_diag_callback) {
|
||||
auto cb = g_diag_callback.load(std::memory_order_acquire);
|
||||
if (cb) {
|
||||
char buf[1024];
|
||||
vsnprintf(buf, sizeof(buf), fmt, args_copy);
|
||||
g_diag_callback(level, nullptr, 0, nullptr, buf);
|
||||
cb(level, nullptr, 0, nullptr, buf);
|
||||
}
|
||||
va_end(args_copy);
|
||||
}
|
||||
@@ -300,7 +302,7 @@ DSTALK_API void dstalk_free(void* ptr) { free(ptr); }
|
||||
DSTALK_API char* dstalk_strdup(const char* s) { return host_strdup(s); }
|
||||
|
||||
DSTALK_API void dstalk_set_diag_callback(dstalk_diag_cb cb) {
|
||||
g_diag_callback = cb;
|
||||
g_diag_callback.store(cb, std::memory_order_release);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -9,8 +9,10 @@
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <queue>
|
||||
#include <stdexcept>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace dstalk {
|
||||
|
||||
@@ -199,9 +201,14 @@ std::vector<int> PluginLoader::topological_sort() const
|
||||
|
||||
int PluginLoader::initialize_all(const dstalk_host_api_t* host_api)
|
||||
{
|
||||
if (!host_api) return -1;
|
||||
|
||||
try {
|
||||
std::vector<int> order = topological_sort();
|
||||
|
||||
std::unordered_set<std::string> failed_names;
|
||||
int failed_count = 0;
|
||||
|
||||
for (int id : order) {
|
||||
auto it = plugins_.find(id);
|
||||
if (it == plugins_.end()) continue;
|
||||
@@ -209,16 +216,40 @@ int PluginLoader::initialize_all(const dstalk_host_api_t* host_api)
|
||||
PluginInfo& plugin = it->second;
|
||||
if (plugin.initialized) continue;
|
||||
|
||||
// 检查依赖是否已失败
|
||||
bool dep_unavailable = false;
|
||||
for (const auto& dep_name : plugin.dependencies) {
|
||||
if (failed_names.count(dep_name)) {
|
||||
dep_unavailable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dep_unavailable) {
|
||||
fprintf(stderr, "[WARN] Plugin '%s' skipped: dependency unavailable\n",
|
||||
plugin.name.c_str());
|
||||
failed_names.insert(plugin.name);
|
||||
failed_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (plugin.info->on_init) {
|
||||
int result = plugin.info->on_init(host_api);
|
||||
if (result != 0) {
|
||||
return -1;
|
||||
fprintf(stderr, "[ERROR] Plugin '%s' init failed (code %d)\n",
|
||||
plugin.name.c_str(), result);
|
||||
failed_names.insert(plugin.name);
|
||||
failed_count++;
|
||||
continue; // 不设置 initialized=true
|
||||
}
|
||||
}
|
||||
plugin.initialized = true;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return failed_count;
|
||||
} catch (const std::runtime_error&) {
|
||||
// 循环依赖
|
||||
return -1;
|
||||
} catch (const std::exception&) {
|
||||
return -1;
|
||||
}
|
||||
@@ -272,13 +303,24 @@ void PluginLoader::shutdown_all()
|
||||
if (it == plugins_.end()) continue;
|
||||
|
||||
PluginInfo& plugin = it->second;
|
||||
if (!plugin.initialized) continue;
|
||||
|
||||
if (plugin.info->on_shutdown) {
|
||||
if (plugin.initialized && plugin.info->on_shutdown) {
|
||||
plugin.info->on_shutdown();
|
||||
}
|
||||
plugin.initialized = false;
|
||||
}
|
||||
|
||||
// 释放所有 DLL 句柄
|
||||
for (auto& [id, plugin] : plugins_) {
|
||||
if (plugin.handle) {
|
||||
#ifdef _WIN32
|
||||
FreeLibrary((HMODULE)plugin.handle);
|
||||
#else
|
||||
dlclose(plugin.handle);
|
||||
#endif
|
||||
plugin.handle = nullptr;
|
||||
}
|
||||
}
|
||||
plugins_.clear();
|
||||
}
|
||||
|
||||
const PluginInfo* PluginLoader::get_plugin(int plugin_id) const
|
||||
|
||||
@@ -14,9 +14,3 @@ target_link_libraries(dstalk-gui
|
||||
dstalk
|
||||
SDL3::SDL3
|
||||
)
|
||||
|
||||
add_custom_command(TARGET dstalk-gui POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_FILE:dstalk>
|
||||
$<TARGET_FILE_DIR:dstalk-gui>
|
||||
)
|
||||
|
||||
@@ -9,21 +9,11 @@ add_library(plugin-anthropic SHARED
|
||||
src/anthropic_plugin.cpp
|
||||
)
|
||||
|
||||
target_include_directories(plugin-anthropic PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/include
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-anthropic PRIVATE dstalk)
|
||||
|
||||
# Boost.JSON 用于构建/解析请求和响应
|
||||
find_package(Boost REQUIRED CONFIG)
|
||||
target_link_libraries(plugin-anthropic PRIVATE boost::boost)
|
||||
|
||||
target_compile_definitions(plugin-anthropic PRIVATE
|
||||
BOOST_ALL_NO_LIB
|
||||
BOOST_ERROR_CODE_HEADER_ONLY
|
||||
BOOST_JSON_HEADER_ONLY
|
||||
)
|
||||
target_link_libraries(plugin-anthropic PRIVATE boost::boost dstalk_boost_config)
|
||||
|
||||
set_target_properties(plugin-anthropic PROPERTIES
|
||||
PREFIX ""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "dstalk/dstalk_services.h"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
@@ -27,6 +28,14 @@ struct PluginConfig {
|
||||
};
|
||||
static PluginConfig g_cfg;
|
||||
|
||||
// ============================================================================
|
||||
// 安全擦除:用 volatile 写零循环防止编译器优化
|
||||
// ============================================================================
|
||||
static void secure_zero(void* p, size_t n) {
|
||||
volatile char* vp = (volatile char*)p;
|
||||
while (n--) *vp++ = 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助:提取 host / target
|
||||
// ============================================================================
|
||||
@@ -461,6 +470,8 @@ static int on_init(const dstalk_host_api_t* host)
|
||||
static void on_shutdown()
|
||||
{
|
||||
if (g_host) g_host->log(DSTALK_LOG_INFO, "[anthropic] shutdown");
|
||||
secure_zero(g_cfg.api_key.data(), g_cfg.api_key.size());
|
||||
g_cfg.api_key.clear();
|
||||
g_http = nullptr;
|
||||
g_config = nullptr;
|
||||
g_host = nullptr;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
add_library(plugin-config SHARED src/config_plugin.cpp)
|
||||
|
||||
target_include_directories(plugin-config PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/include
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-config PRIVATE dstalk)
|
||||
|
||||
set_target_properties(plugin-config PROPERTIES
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
add_library(plugin-context SHARED src/context_plugin.cpp)
|
||||
|
||||
target_include_directories(plugin-context PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/include
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-context PRIVATE dstalk)
|
||||
|
||||
set_target_properties(plugin-context PROPERTIES
|
||||
|
||||
@@ -9,21 +9,11 @@ add_library(plugin-deepseek SHARED
|
||||
src/deepseek_plugin.cpp
|
||||
)
|
||||
|
||||
target_include_directories(plugin-deepseek PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/include
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-deepseek PRIVATE dstalk)
|
||||
|
||||
# Boost.JSON 用于构建/解析请求和响应
|
||||
find_package(Boost REQUIRED CONFIG)
|
||||
target_link_libraries(plugin-deepseek PRIVATE boost::boost)
|
||||
|
||||
target_compile_definitions(plugin-deepseek PRIVATE
|
||||
BOOST_ALL_NO_LIB
|
||||
BOOST_ERROR_CODE_HEADER_ONLY
|
||||
BOOST_JSON_HEADER_ONLY
|
||||
)
|
||||
target_link_libraries(plugin-deepseek PRIVATE boost::boost dstalk_boost_config)
|
||||
|
||||
set_target_properties(plugin-deepseek PROPERTIES
|
||||
PREFIX ""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "dstalk/dstalk_services.h"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
@@ -27,6 +28,14 @@ struct PluginConfig {
|
||||
};
|
||||
static PluginConfig g_cfg;
|
||||
|
||||
// ============================================================================
|
||||
// 安全擦除:用 volatile 写零循环防止编译器优化
|
||||
// ============================================================================
|
||||
static void secure_zero(void* p, size_t n) {
|
||||
volatile char* vp = (volatile char*)p;
|
||||
while (n--) *vp++ = 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 辅助:从 base_url 提取 host 和 target
|
||||
// ============================================================================
|
||||
@@ -450,6 +459,8 @@ static int on_init(const dstalk_host_api_t* host)
|
||||
static void on_shutdown()
|
||||
{
|
||||
if (g_host) g_host->log(DSTALK_LOG_INFO, "[deepseek] shutdown");
|
||||
secure_zero(g_cfg.api_key.data(), g_cfg.api_key.size());
|
||||
g_cfg.api_key.clear();
|
||||
g_http = nullptr;
|
||||
g_config = nullptr;
|
||||
g_host = nullptr;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
add_library(plugin-file-io SHARED src/file_io_plugin.cpp)
|
||||
|
||||
target_include_directories(plugin-file-io PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/include
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-file-io PRIVATE dstalk)
|
||||
|
||||
set_target_properties(plugin-file-io PROPERTIES
|
||||
|
||||
@@ -29,8 +29,8 @@ static int file_read(const char* path, char** content) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Allocate buffer (+1 for null terminator)
|
||||
char* buf = (char*)malloc((size_t)fsize + 1);
|
||||
// Allocate buffer via host allocator (+1 for null terminator)
|
||||
char* buf = (char*)g_host->alloc((size_t)fsize + 1);
|
||||
if (!buf) {
|
||||
fclose(fp);
|
||||
return -1;
|
||||
@@ -40,7 +40,7 @@ static int file_read(const char* path, char** content) {
|
||||
fclose(fp);
|
||||
|
||||
if (read_bytes != (size_t)fsize) {
|
||||
free(buf);
|
||||
g_host->free(buf);
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,21 +9,11 @@ add_library(plugin-lsp SHARED
|
||||
src/lsp_plugin.cpp
|
||||
)
|
||||
|
||||
target_include_directories(plugin-lsp PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/include
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-lsp PRIVATE dstalk)
|
||||
|
||||
# Boost.JSON 用于 JSON-RPC 消息构建/解析
|
||||
find_package(Boost REQUIRED CONFIG)
|
||||
target_link_libraries(plugin-lsp PRIVATE boost::boost)
|
||||
|
||||
target_compile_definitions(plugin-lsp PRIVATE
|
||||
BOOST_ALL_NO_LIB
|
||||
BOOST_ERROR_CODE_HEADER_ONLY
|
||||
BOOST_JSON_HEADER_ONLY
|
||||
)
|
||||
target_link_libraries(plugin-lsp PRIVATE boost::boost dstalk_boost_config)
|
||||
|
||||
# POSIX 平台需要 pthread (用于 std::thread)
|
||||
if(NOT WIN32)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "dstalk/dstalk_services.h"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
@@ -417,18 +418,33 @@ static void handle_message(const std::string& body) {
|
||||
|
||||
static void reader_loop() {
|
||||
while (g_lsp.running) {
|
||||
std::string header_line;
|
||||
if (!g_lsp.proc.read_line(header_line)) break;
|
||||
int content_length = -1;
|
||||
bool pipe_ok = true;
|
||||
|
||||
int content_length = parse_content_length(header_line);
|
||||
if (content_length < 0) continue;
|
||||
|
||||
// 跳过后续头直到空行 (\r\n 换行被视为非空行,只检查空行)
|
||||
while (true) {
|
||||
// 状态机式读取 header 块:循环 read_line 直到读到空行
|
||||
// LSP 3.17: header 块以空行(\r\n)结束,允许 Content-Type 等其他 header
|
||||
while (pipe_ok) {
|
||||
std::string line;
|
||||
if (!g_lsp.proc.read_line(line)) break;
|
||||
if (!g_lsp.proc.read_line(line)) {
|
||||
pipe_ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// header 块以空行结束
|
||||
auto sv = trim(std::string_view(line));
|
||||
if (sv.empty()) break;
|
||||
|
||||
// 累积 Content-Length;遇到其他 header 不丢弃,继续读取下一行
|
||||
int len = parse_content_length(line);
|
||||
if (len >= 0) content_length = len;
|
||||
}
|
||||
|
||||
if (!pipe_ok) break;
|
||||
|
||||
// 空行前都没读到 Content-Length,协议错误——记日志并跳过这一帧
|
||||
if (content_length < 0) {
|
||||
if (g_host) g_host->log(DSTALK_LOG_ERROR, "[lsp] Invalid LSP frame: missing Content-Length header");
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string body;
|
||||
@@ -446,7 +462,7 @@ static void reader_loop() {
|
||||
// LSP 服务 vtable 实现 (定义在 vtable 变量之前)
|
||||
// ============================================================================
|
||||
|
||||
static int g_lsp_impl_stop();
|
||||
static void g_lsp_impl_stop();
|
||||
|
||||
static int g_lsp_impl_start(const char* server_cmd, const char* language) {
|
||||
if (!server_cmd || !server_cmd[0]) return -1;
|
||||
|
||||
@@ -3,10 +3,6 @@ find_package(OpenSSL REQUIRED CONFIG)
|
||||
|
||||
add_library(plugin-network SHARED src/network_plugin.cpp)
|
||||
|
||||
target_include_directories(plugin-network PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/include
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-network PRIVATE
|
||||
dstalk
|
||||
boost::boost
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
add_library(plugin-session SHARED src/session_plugin.cpp)
|
||||
|
||||
target_include_directories(plugin-session PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/include
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-session PRIVATE dstalk)
|
||||
|
||||
find_package(Boost REQUIRED CONFIG)
|
||||
target_link_libraries(plugin-session PRIVATE boost::boost)
|
||||
target_compile_definitions(plugin-session PRIVATE
|
||||
BOOST_ALL_NO_LIB BOOST_ERROR_CODE_HEADER_ONLY BOOST_JSON_HEADER_ONLY)
|
||||
target_link_libraries(plugin-session PRIVATE boost::boost dstalk_boost_config)
|
||||
|
||||
set_target_properties(plugin-session PROPERTIES
|
||||
PREFIX ""
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "dstalk/dstalk_services.h"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
@@ -163,7 +164,7 @@ static int session_load(const char* path) {
|
||||
if (ret != 0 || !content) return -1;
|
||||
|
||||
std::string data(content);
|
||||
std::free(content);
|
||||
g_host->free(content);
|
||||
|
||||
std::vector<InternalMessage> parsed;
|
||||
size_t pos = 0;
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
add_library(plugin-tools SHARED src/tools_plugin.cpp)
|
||||
|
||||
target_include_directories(plugin-tools PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/include
|
||||
)
|
||||
|
||||
target_link_libraries(plugin-tools PRIVATE dstalk)
|
||||
|
||||
find_package(Boost REQUIRED CONFIG)
|
||||
target_link_libraries(plugin-tools PRIVATE boost::boost)
|
||||
target_compile_definitions(plugin-tools PRIVATE
|
||||
BOOST_ALL_NO_LIB BOOST_ERROR_CODE_HEADER_ONLY BOOST_JSON_HEADER_ONLY)
|
||||
target_link_libraries(plugin-tools PRIVATE boost::boost dstalk_boost_config)
|
||||
|
||||
set_target_properties(plugin-tools PROPERTIES
|
||||
PREFIX ""
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "dstalk/dstalk_services.h"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <boost/json/src.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -55,7 +56,7 @@ static char* builtin_file_read(const char* args_json) {
|
||||
}
|
||||
|
||||
std::string escaped_content = json::serialize(json::string(content));
|
||||
std::free(content);
|
||||
g_host->free(content);
|
||||
|
||||
std::string result = "{\"content\":" + escaped_content + "}";
|
||||
return g_host->strdup(result.c_str());
|
||||
@@ -100,6 +101,8 @@ static char* builtin_file_write(const char* args_json) {
|
||||
// Tools 服务 vtable 实现
|
||||
// ============================================================
|
||||
|
||||
static void tools_unregister_tool(const char* name);
|
||||
|
||||
static int tools_register_tool(const char* name, const char* desc,
|
||||
const char* params_schema,
|
||||
dstalk_tool_handler_fn handler) {
|
||||
|
||||
@@ -10,10 +10,27 @@ target_link_libraries(dstalk-smoke-test
|
||||
PRIVATE dstalk
|
||||
)
|
||||
|
||||
add_custom_command(TARGET dstalk-smoke-test POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
$<TARGET_FILE:dstalk>
|
||||
$<TARGET_FILE_DIR:dstalk-smoke-test>
|
||||
add_test(NAME dstalk-smoke-test COMMAND dstalk-smoke-test)
|
||||
|
||||
# ============================================================
|
||||
# dstalk-host-api-test — host API 单元测试
|
||||
# ============================================================
|
||||
|
||||
add_executable(dstalk-host-api-test
|
||||
host_api_test.cpp
|
||||
${CMAKE_SOURCE_DIR}/dstalk-core/src/service_registry.cpp
|
||||
)
|
||||
|
||||
add_test(NAME dstalk-smoke-test COMMAND dstalk-smoke-test)
|
||||
target_include_directories(dstalk-host-api-test
|
||||
PRIVATE ${CMAKE_SOURCE_DIR}/dstalk-core/src
|
||||
)
|
||||
|
||||
target_compile_features(dstalk-host-api-test
|
||||
PRIVATE cxx_std_17
|
||||
)
|
||||
|
||||
target_link_libraries(dstalk-host-api-test
|
||||
PRIVATE dstalk
|
||||
)
|
||||
|
||||
add_test(NAME dstalk-host-api-test COMMAND dstalk-host-api-test)
|
||||
|
||||
180
tests/host_api_test.cpp
Normal file
180
tests/host_api_test.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
// ============================================================================
|
||||
// host_api_test.cpp — host API 单元测试 (独立于 smoke_test)
|
||||
// ============================================================================
|
||||
// 测试: register_service / query_service / alloc / free / log / init / shutdown
|
||||
// ============================================================================
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
// 引入 ServiceRegistry 实现做纯单元测试
|
||||
#include "service_registry.hpp"
|
||||
|
||||
#include "dstalk/dstalk_host.h"
|
||||
|
||||
// ---- 轻量断言 ----
|
||||
static int g_failures = 0;
|
||||
#define TCHECK(cond, msg) do { \
|
||||
if (cond) { \
|
||||
std::cout << "[OK] " << (msg) << "\n"; \
|
||||
} else { \
|
||||
std::cerr << "[FAIL] " << (msg) << "\n"; \
|
||||
g_failures++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
// ---- 辅助: 创建临时配置文件 ----
|
||||
static std::string make_temp_config(const std::string& tag) {
|
||||
auto dir = std::filesystem::temp_directory_path() / ("dstalk-host-api-" + tag);
|
||||
std::filesystem::create_directories(dir);
|
||||
auto config_path = dir / "config.toml";
|
||||
{
|
||||
std::ofstream c(config_path);
|
||||
// 指向不存在的插件目录,避免加载任何 .dll
|
||||
c << "plugin_dir = \"__no_such_plugins_dir__\"\n";
|
||||
}
|
||||
return config_path.string();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
int main()
|
||||
{
|
||||
std::cout << "=== dstalk host_api unit tests ===\n\n";
|
||||
|
||||
// ====================================================================
|
||||
// Test 1: register_service 重复注册 同名+同版本 → 应返回 -2
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
void* dummy_vtable = reinterpret_cast<void*>(0x1);
|
||||
int r1 = reg.register_service("echo", 1, dummy_vtable);
|
||||
TCHECK(r1 == 0, "register_service(\"echo\",1) first call returns 0");
|
||||
|
||||
int r2 = reg.register_service("echo", 1, dummy_vtable);
|
||||
TCHECK(r2 == -2,
|
||||
"register_service(\"echo\",1) duplicate same-version returns -2");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 2: register_service 同名+不同版本 → 应返回 -2
|
||||
// 名称已占用,与版本无关
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
void* dummy_vtable = reinterpret_cast<void*>(0x1);
|
||||
int r1 = reg.register_service("calc", 1, dummy_vtable);
|
||||
TCHECK(r1 == 0, "register_service(\"calc\",1) first call returns 0");
|
||||
|
||||
int r2 = reg.register_service("calc", 99, dummy_vtable);
|
||||
TCHECK(r2 == -2,
|
||||
"register_service(\"calc\",99) diff-version duplicate returns -2");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 3: query_service 不存在的 name → nullptr
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
void* q = reg.query_service("ghost_service", 1);
|
||||
TCHECK(q == nullptr, "query_service(\"ghost_service\",1) returns nullptr");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 4: query_service 错误版本号 → nullptr
|
||||
// 注册 v=1, 查询 min_version=2 → 不满足 → nullptr
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk::ServiceRegistry reg;
|
||||
void* dummy_vtable = reinterpret_cast<void*>(0x2);
|
||||
reg.register_service("solo", 1, dummy_vtable);
|
||||
|
||||
void* q = reg.query_service("solo", 2);
|
||||
TCHECK(q == nullptr, "query_service(\"solo\",2) with only v1 available returns nullptr");
|
||||
|
||||
// 确证以正确版本查询能拿到
|
||||
void* q2 = reg.query_service("solo", 1);
|
||||
TCHECK(q2 == dummy_vtable, "query_service(\"solo\",1) with v1 available returns vtable");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 5: dstalk_init 多次调用 → 第二次应返回 -1 (幂等拒绝)
|
||||
// ====================================================================
|
||||
{
|
||||
std::string cfg = make_temp_config("init-twice");
|
||||
int r1 = dstalk_init(cfg.c_str());
|
||||
TCHECK(r1 == 0, "dstalk_init first call returns 0");
|
||||
|
||||
int r2 = dstalk_init(cfg.c_str());
|
||||
TCHECK(r2 == -1, "dstalk_init second call returns -1 (idempotent guard)");
|
||||
|
||||
dstalk_shutdown();
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 6: alloc(0) / free(nullptr) 行为
|
||||
// malloc(0) 可返回 null 或合法指针; 两者都可 free
|
||||
// free(nullptr) 是安全空操作
|
||||
// ====================================================================
|
||||
{
|
||||
void* p = dstalk_alloc(0);
|
||||
std::cout << "[OK] dstalk_alloc(0) returned " << p
|
||||
<< " (null or valid, both acceptable)\n";
|
||||
dstalk_free(p);
|
||||
std::cout << "[OK] dstalk_free(alloc(0)) did not crash\n";
|
||||
|
||||
dstalk_free(nullptr);
|
||||
std::cout << "[OK] dstalk_free(nullptr) did not crash\n";
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 7: log 各 level 不崩溃 (DEBUG / INFO / WARN / ERROR)
|
||||
// ====================================================================
|
||||
{
|
||||
dstalk_log(DSTALK_LOG_DEBUG, "host_api_test: debug level message");
|
||||
std::cout << "[OK] dstalk_log(DEBUG) no crash\n";
|
||||
|
||||
dstalk_log(DSTALK_LOG_INFO, "host_api_test: info level message");
|
||||
std::cout << "[OK] dstalk_log(INFO) no crash\n";
|
||||
|
||||
dstalk_log(DSTALK_LOG_WARN, "host_api_test: warn level message");
|
||||
std::cout << "[OK] dstalk_log(WARN) no crash\n";
|
||||
|
||||
dstalk_log(DSTALK_LOG_ERROR, "host_api_test: error level message");
|
||||
std::cout << "[OK] dstalk_log(ERROR) no crash\n";
|
||||
|
||||
// 带格式参数
|
||||
dstalk_log(DSTALK_LOG_INFO, "formatted: %s %d", "answer", 42);
|
||||
std::cout << "[OK] dstalk_log with format args no crash\n";
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// Test 8: dstalk_shutdown 后 query_service → nullptr
|
||||
// g_service_registry 已被 delete 置空
|
||||
// ====================================================================
|
||||
{
|
||||
std::string cfg = make_temp_config("after-shutdown");
|
||||
dstalk_init(cfg.c_str());
|
||||
dstalk_shutdown();
|
||||
|
||||
void* q = dstalk_service_query("any_service", 1);
|
||||
TCHECK(q == nullptr, "dstalk_service_query after shutdown returns nullptr");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
// 结果
|
||||
// ====================================================================
|
||||
std::cout << "\n";
|
||||
if (g_failures == 0) {
|
||||
std::cout << "=== All host_api tests passed ===\n";
|
||||
return 0;
|
||||
} else {
|
||||
std::cerr << "=== " << g_failures << " host_api test(s) FAILED ===\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,7 @@ int main()
|
||||
char* content = nullptr;
|
||||
if (file_io->read(file_path.string().c_str(), &content) == 0 && content) {
|
||||
bool ok = std::strcmp(content, sample_content) == 0;
|
||||
std::free(content);
|
||||
dstalk_free(content);
|
||||
if (ok) {
|
||||
std::cout << "[OK] file_io->read content matches\n";
|
||||
} else {
|
||||
@@ -123,7 +123,7 @@ int main()
|
||||
char* saved = nullptr;
|
||||
if (file_io->read(saved_path.string().c_str(), &saved) == 0 && saved) {
|
||||
bool session_ok = std::strcmp(saved, session_content) == 0;
|
||||
std::free(saved);
|
||||
dstalk_free(saved);
|
||||
if (session_ok) {
|
||||
std::cout << "[OK] session content matches after save/load\n";
|
||||
} else {
|
||||
@@ -331,7 +331,7 @@ int main()
|
||||
<< " expected length: " << std::strlen(escape_content) << "\n"
|
||||
<< " got length: " << std::strlen(read_back) << "\n";
|
||||
}
|
||||
std::free(read_back);
|
||||
dstalk_free(read_back);
|
||||
} else {
|
||||
std::cerr << "[FAIL] escape content read-back failed\n";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user