Refactor to plugin architecture with B3 CLI UX, C2 smoke tests, C3 CI scripts

Architecture overhaul (Wave 1-4 collaborative work):
- Migrated dstalk-core from monolithic api.cpp to plugin-based design with
  host/service_registry/event_bus/plugin_loader and topological initialization.
- Split public headers into dstalk_host.h / dstalk_services.h /
  dstalk_lsp.h / dstalk_types.h; deleted obsolete dstalk_api.h and inlined
  TLS/file/net code now provided by plugins.
- Added 9 plugins: deepseek, anthropic, network, session, context, tools,
  config, file-io, lsp; AI plugins register as "ai.<provider>" services.

B3 CLI interaction enhancement:
- Prompt now shows current model name (A1).
- /status command prints model/base_url/api_key (sanitized: shown only
  as set/unset)/services readiness (A2).
- SIGINT/Ctrl+C handled on POSIX (signal) and Windows (SetConsoleCtrlHandler);
  /quit no longer std::exit(0) but sets a quit flag so dstalk_shutdown runs
  exactly once via natural control flow (B1+B2).
- Cross-DLL free fixed: print_file uses dstalk_free instead of std::free (B4).
- --batch mode plus isatty auto-detection for piped stdin (C1).
- fgets truncation detection with friendly error and stdin draining (C3).
- Distinct exit codes (init/AI/service-unavailable) (C4).
- /model rejects empty model name (C5).

C2 smoke test extension:
- 4 new test blocks: null-safety (file_io/session/tools/config),
  escape-boundary round-trip, tools->execute call chain, session robustness
  (add(nullptr), clear -> token_count == 0).

C3 CI build scripts:
- scripts/ci-build.sh and scripts/ci-build.bat invoke cmake configure +
  parallel build + ctest, suitable for GitHub Actions.

Build verified: dstalk-cli compiles, smoke test passes via ctest.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-27 05:12:56 +08:00
parent 3e9ba04df5
commit e6f24f00f1
53 changed files with 6450 additions and 1360 deletions

View File

@@ -1,52 +0,0 @@
#ifndef DSTALK_API_H
#define DSTALK_API_H
#ifdef __cplusplus
extern "C" {
#endif
/* ---- DLL 导出 / 导入宏 ---- */
#if defined(_WIN32)
#ifdef DSTALK_BUILD_DLL
#define DSTALK_API __declspec(dllexport)
#else
#define DSTALK_API __declspec(dllimport)
#endif
#else
#define DSTALK_API __attribute__((visibility("default")))
#endif
/* ---- 初始化和配置 ---- */
DSTALK_API int dstalk_init(const char* config_path);
DSTALK_API void dstalk_destroy(void);
/* 在 init 之后可修改 API 参数 (init 也会从配置文件读取) */
DSTALK_API void dstalk_set_api_key(const char* api_key);
DSTALK_API void dstalk_set_base_url(const char* base_url);
DSTALK_API void dstalk_set_model(const char* model);
/* ---- AI 对话 ---- */
/* 同步对话: 发送 input返回完整 AI 回复 (调用方通过 dstalk_free_string 释放) */
DSTALK_API int dstalk_chat(const char* input, char** output);
/* 流式对话: 每收到一个 token 调用回调,回调返回 0 继续,非 0 取消 */
typedef int (*dstalk_stream_cb)(const char* token, void* userdata);
DSTALK_API int dstalk_chat_stream(const char* input, dstalk_stream_cb cb, void* userdata);
/* 释放由 dstalk_chat / dstalk_file_read 分配的字符串 */
DSTALK_API void dstalk_free_string(char* str);
/* ---- 会话管理 ---- */
DSTALK_API void dstalk_session_clear(void); /* 清空对话历史 */
DSTALK_API int dstalk_session_save(const char* path); /* 保存会话到文件 */
DSTALK_API int dstalk_session_load(const char* path); /* 从文件恢复会话 */
/* ---- 文件操作 ---- */
DSTALK_API int dstalk_file_read(const char* path, char** content);
DSTALK_API int dstalk_file_write(const char* path, const char* content);
#ifdef __cplusplus
}
#endif
#endif /* DSTALK_API_H */

View File

@@ -0,0 +1,132 @@
#ifndef DSTALK_HOST_H
#define DSTALK_HOST_H
#include "dstalk_types.h"
#include "dstalk_services.h"
#ifdef __cplusplus
extern "C" {
#endif
// === 平台导出宏 ===
#ifndef DSTALK_API
#if defined(_WIN32)
#ifdef DSTALK_BUILD_DLL
#define DSTALK_API __declspec(dllexport)
#else
#define DSTALK_API __declspec(dllimport)
#endif
#else
#define DSTALK_API __attribute__((visibility("default")))
#endif
#endif
// === 插件导出宏 ===
#if defined(_WIN32)
#define DSTALK_PLUGIN_EXPORT __declspec(dllexport)
#else
#define DSTALK_PLUGIN_EXPORT __attribute__((visibility("default")))
#endif
// === API 版本 ===
#define DSTALK_API_VERSION 1
#define DSTALK_MAX_DEPS 8
// === 诊断 ===
typedef void (*dstalk_diag_cb)(int severity, const char* file,
int line, const char* func, const char* message);
#define DSTALK_ERROR_RETURN(expr, retval) do { \
if (!(expr)) { \
dstalk_log(DSTALK_LOG_ERROR, "[%s:%d] %s: assertion '%s' failed", \
__FILE__, __LINE__, __func__, #expr); \
return (retval); \
} \
} while(0)
DSTALK_API void dstalk_set_diag_callback(dstalk_diag_cb cb);
// === 事件处理器 ===
typedef void (*dstalk_event_handler_fn)(int event_type, const void* data, void* userdata);
// === Host 提供给插件的 API 表 ===
typedef struct {
// 服务注册/查询
int (*register_service)(const char* name, int version, void* vtable);
void*(*query_service)(const char* name, int min_version);
// 事件
int (*event_subscribe)(int event_type, dstalk_event_handler_fn handler, void* userdata);
int (*event_emit)(int event_type, const void* data);
void (*event_unsubscribe)(int sub_id);
// 配置
const char* (*config_get)(const char* key);
int (*config_set)(const char* key, const char* value);
// 日志
void (*log)(int level, const char* fmt, ...);
// 内存
void* (*alloc)(size_t size);
void (*free)(void* ptr);
char* (*strdup)(const char* s);
} dstalk_host_api_t;
// === 插件信息结构 ===
typedef struct {
const char* name; // 插件名称(唯一标识)
const char* version; // 语义化版本号,如 "1.0.0"
const char* description; // 描述
int api_version; // 必须 == DSTALK_API_VERSION
// 依赖声明(以 NULL 结尾)
const char* dependencies[DSTALK_MAX_DEPS];
// 生命周期回调
int (*on_init)(const dstalk_host_api_t* host);
void (*on_shutdown)(void);
// 事件处理(可选)
void (*on_event)(int event_type, const void* data);
} dstalk_plugin_info_t;
// === 插件入口函数 ===
typedef dstalk_plugin_info_t* (*dstalk_plugin_init_fn)(void);
// === Host 公共 API ===
// 初始化/销毁
DSTALK_API int dstalk_init(const char* config_path);
DSTALK_API void dstalk_shutdown(void);
// 插件管理
DSTALK_API int dstalk_plugin_load(const char* path);
DSTALK_API int dstalk_plugin_unload(int plugin_id);
DSTALK_API int dstalk_plugin_list(char** output_json);
// 服务查询
DSTALK_API void* dstalk_service_query(const char* service_name, int min_version);
// 事件系统
DSTALK_API int dstalk_event_subscribe(int event_type, dstalk_event_handler_fn handler, void* userdata);
DSTALK_API int dstalk_event_emit(int event_type, const void* data);
DSTALK_API void dstalk_event_unsubscribe(int subscription_id);
// 配置
DSTALK_API const char* dstalk_config_get(const char* key);
DSTALK_API int dstalk_config_set(const char* key, const char* value);
// 日志
DSTALK_API void dstalk_log(int level, const char* fmt, ...);
// 内存
DSTALK_API void* dstalk_alloc(size_t size);
DSTALK_API void dstalk_free(void* ptr);
DSTALK_API char* dstalk_strdup(const char* s);
#ifdef __cplusplus
}
#endif
#endif // DSTALK_HOST_H

View File

@@ -0,0 +1,91 @@
#ifndef DSTALK_LSP_H
#define DSTALK_LSP_H
#include "dstalk_host.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ---- LSP 服务器生命周期 ---- */
/*
* 启动语言服务器进程
* server_cmd: 命令字符串,例如 "clangd" 或 "pyright --stdio" 或完整路径
* language: 语言标识,例如 "c", "cpp", "python", "javascript", "rust"
* returns: 0 成功, -1 失败
*/
DSTALK_API int dstalk_lsp_start(const char* server_cmd, const char* language);
/*
* 停止语言服务器
* 发送 shutdown 请求,然后发送 exit 通知
* 关闭管道,终止子进程
*/
DSTALK_API void dstalk_lsp_stop(void);
/* ---- 文档管理 ---- */
/*
* 在语言服务器中打开一个文档
* uri: 文件 URI例如 "file:///path/to/file.c"
* content: 文件内容文本
* language_id: 语言 ID例如 "c", "cpp", "python", "javascript"
* returns: 0 成功, -1 失败
*/
DSTALK_API int dstalk_lsp_open(const char* uri, const char* content,
const char* language_id);
/*
* 关闭语言服务器中的文档
* uri: 文件 URI
* returns: 0 成功, -1 失败
*/
DSTALK_API int dstalk_lsp_close(const char* uri);
/* ---- 查询操作 ---- */
/*
* 获取诊断信息 (编译错误、警告等)
* uri: 文件 URI
* output: 输出参数JSON 格式的诊断列表 (调用方通过 dstalk_free 释放)
* returns: 0 成功, -1 失败
*
* JSON 输出格式示例:
* [
* {
* "range": { "start": {"line":0,"character":0}, "end":{"line":0,"character":5} },
* "severity": 1,
* "message": "error message"
* }
* ]
*/
DSTALK_API int dstalk_lsp_diagnostics(const char* uri, char** output);
/*
* 获取悬停信息 (类型、文档等)
* uri: 文件 URI
* line: 行号 (0-based)
* character: 列号 (0-based, UTF-16 code units)
* output: 输出参数JSON 格式的悬停信息 (调用方通过 dstalk_free 释放)
* returns: 0 成功, -1 失败
*/
DSTALK_API int dstalk_lsp_hover(const char* uri, int line, int character,
char** output);
/*
* 获取代码补全建议
* uri: 文件 URI
* line: 行号 (0-based)
* character: 列号 (0-based, UTF-16 code units)
* output: 输出参数JSON 格式的补全列表 (调用方通过 dstalk_free 释放)
* returns: 0 成功, -1 失败
*/
DSTALK_API int dstalk_lsp_completion(const char* uri, int line, int character,
char** output);
#ifdef __cplusplus
}
#endif
#endif /* DSTALK_LSP_H */

View File

@@ -0,0 +1,97 @@
#ifndef DSTALK_SERVICES_H
#define DSTALK_SERVICES_H
#include "dstalk_types.h"
#ifdef __cplusplus
extern "C" {
#endif
// === AI 服务 vtable (实际服务名由插件注册: "ai.deepseek" / "ai.anthropic") ===
typedef struct {
int (*configure)(const char* provider, const char* base_url,
const char* api_key, const char* model,
int max_tokens, double temperature);
dstalk_chat_result_t (*chat)(
const dstalk_message_t* history, int history_len,
const char* user_input,
const char* tools_json);
dstalk_chat_result_t (*chat_stream)(
const dstalk_message_t* history, int history_len,
const char* user_input,
dstalk_stream_cb cb, void* userdata);
void (*free_result)(dstalk_chat_result_t* result);
} dstalk_ai_service_t;
// === Session 服务 (service name: "session") ===
typedef struct {
void (*add)(const dstalk_message_t* msg);
void (*clear)(void);
int (*save)(const char* path);
int (*load)(const char* path);
const dstalk_message_t* (*history)(int* out_count);
int (*token_count)(void);
} dstalk_session_service_t;
// === Context 服务 (service name: "context") ===
typedef struct {
size_t (*count_tokens)(const dstalk_message_t* msgs, int count);
int (*trim)(const dstalk_message_t* in, int in_count,
dstalk_message_t** out, int* out_count,
size_t max_tokens);
void (*set_max_tokens)(size_t max);
} dstalk_context_service_t;
// === HTTP 服务 (service name: "http") ===
typedef struct {
int (*post_json)(const char* host, const char* port,
const char* target, const char* body,
const char* headers_json,
char** response_body, int* status_code);
int (*post_stream)(const char* host, const char* port,
const char* target, const char* body,
const char* headers_json,
dstalk_stream_cb cb, void* userdata,
char** response_body, int* status_code);
} dstalk_http_service_t;
// === File IO 服务 (service name: "file_io") ===
typedef struct {
int (*read)(const char* path, char** content);
int (*write)(const char* path, const char* content);
} dstalk_file_io_service_t;
// === Config 服务 (service name: "config") ===
typedef struct {
const char* (*get)(const char* key);
int (*set)(const char* key, const char* value);
int (*load_file)(const char* path);
} dstalk_config_service_t;
// === Tools 服务 (service name: "tools") ===
typedef char* (*dstalk_tool_handler_fn)(const char* args_json);
typedef struct {
int (*register_tool)(const char* name, const char* desc,
const char* params_schema,
dstalk_tool_handler_fn handler);
void (*unregister_tool)(const char* name);
char* (*get_tools_json)(void);
char* (*execute)(const char* name, const char* args_json);
} dstalk_tools_service_t;
// === LSP 服务 (service name: "lsp") ===
typedef struct {
int (*start)(const char* server_cmd, const char* language);
void (*stop)(void);
int (*open_document)(const char* uri, const char* content, const char* lang_id);
int (*close_document)(const char* uri);
int (*get_diagnostics)(const char* uri, char** json_out);
int (*get_hover)(const char* uri, int line, int col, char** json_out);
int (*get_completion)(const char* uri, int line, int col, char** json_out);
} dstalk_lsp_service_t;
#ifdef __cplusplus
}
#endif
#endif // DSTALK_SERVICES_H

View File

@@ -0,0 +1,52 @@
#ifndef DSTALK_TYPES_H
#define DSTALK_TYPES_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
// 消息结构(跨插件共享)
typedef struct {
const char* role; // "user", "assistant", "system", "tool"
const char* content; // 消息内容
const char* tool_call_id; // tool 响应时必填
const char* tool_calls_json;// assistant 返回的工具调用JSON 数组)
} dstalk_message_t;
// 聊天结果
typedef struct {
int ok;
const char* content; // dstalk_strdup 分配,调用方 dstalk_free
const char* error; // dstalk_strdup 分配
int http_status;
const char* tool_calls_json;// dstalk_strdup 分配
} dstalk_chat_result_t;
// 流式回调
typedef int (*dstalk_stream_cb)(const char* token, void* userdata);
// 事件类型
enum {
DSTALK_EVENT_MESSAGE = 1, // data = dstalk_message_t*
DSTALK_EVENT_SESSION_CLEAR,
DSTALK_EVENT_CONFIG_CHANGED,
DSTALK_EVENT_PLUGIN_LOADED, // data = plugin info JSON string
DSTALK_EVENT_PLUGIN_UNLOADED,
DSTALK_EVENT_CUSTOM = 1000, // 插件自定义事件起始值
};
// 日志级别
enum {
DSTALK_LOG_DEBUG = 0,
DSTALK_LOG_INFO = 1,
DSTALK_LOG_WARN = 2,
DSTALK_LOG_ERROR = 3,
};
#ifdef __cplusplus
}
#endif
#endif // DSTALK_TYPES_H