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:
@@ -1,16 +1,17 @@
|
||||
# ============================================================
|
||||
# dstalk-core — 核心 DLL
|
||||
# 包含: 网络通讯 / AI接口 / 文件读写
|
||||
# dstalk-core — 核心 DLL (插件宿主)
|
||||
# 包含: 插件管理 / 服务注册 / 事件总线 / 配置存储
|
||||
# ============================================================
|
||||
|
||||
find_package(Boost REQUIRED CONFIG)
|
||||
find_package(OpenSSL REQUIRED CONFIG)
|
||||
|
||||
add_library(dstalk SHARED
|
||||
src/api.cpp
|
||||
src/file/file_io.cpp
|
||||
src/net/http_client.cpp
|
||||
src/ai/deepseek_api.cpp
|
||||
src/host.cpp
|
||||
src/config_store.cpp
|
||||
src/event_bus.cpp
|
||||
src/service_registry.cpp
|
||||
src/plugin_loader.cpp
|
||||
src/boost_json.cpp
|
||||
)
|
||||
|
||||
@@ -25,6 +26,11 @@ target_link_libraries(dstalk
|
||||
openssl::openssl
|
||||
)
|
||||
|
||||
# dlopen / dlclose / dlsym on Linux and macOS
|
||||
if(NOT WIN32)
|
||||
target_link_libraries(dstalk PRIVATE ${CMAKE_DL_LIBS})
|
||||
endif()
|
||||
|
||||
# 导出 DLL 符号宏
|
||||
target_compile_definitions(dstalk
|
||||
PRIVATE
|
||||
|
||||
@@ -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 */
|
||||
132
dstalk-core/include/dstalk/dstalk_host.h
Normal file
132
dstalk-core/include/dstalk/dstalk_host.h
Normal 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
|
||||
91
dstalk-core/include/dstalk/dstalk_lsp.h
Normal file
91
dstalk-core/include/dstalk/dstalk_lsp.h
Normal 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 */
|
||||
97
dstalk-core/include/dstalk/dstalk_services.h
Normal file
97
dstalk-core/include/dstalk/dstalk_services.h
Normal 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
|
||||
52
dstalk-core/include/dstalk/dstalk_types.h
Normal file
52
dstalk-core/include/dstalk/dstalk_types.h
Normal 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
|
||||
@@ -1,226 +0,0 @@
|
||||
#include "ai/deepseek_api.hpp"
|
||||
#include "net/http_client.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
|
||||
namespace json = boost::json;
|
||||
|
||||
namespace dstalk {
|
||||
namespace ai {
|
||||
|
||||
// ---- JSON 构造 ----
|
||||
static std::string build_request_json(
|
||||
const ApiConfig& cfg,
|
||||
const std::vector<Message>& history,
|
||||
const std::string& user_input,
|
||||
bool stream)
|
||||
{
|
||||
json::object root;
|
||||
root["model"] = cfg.model;
|
||||
root["max_tokens"] = cfg.max_tokens;
|
||||
root["temperature"] = cfg.temperature;
|
||||
root["stream"] = stream;
|
||||
|
||||
json::array msgs;
|
||||
for (const auto& m : history) {
|
||||
json::object obj;
|
||||
obj["role"] = m.role;
|
||||
obj["content"] = m.content;
|
||||
msgs.push_back(obj);
|
||||
}
|
||||
// 追加当前用户输入
|
||||
{
|
||||
json::object obj;
|
||||
obj["role"] = "user";
|
||||
obj["content"] = user_input;
|
||||
msgs.push_back(obj);
|
||||
}
|
||||
root["messages"] = msgs;
|
||||
|
||||
return json::serialize(root);
|
||||
}
|
||||
|
||||
// ---- JSON 响应解析 ----
|
||||
static ChatResult parse_response(const std::string& body, int http_status)
|
||||
{
|
||||
ChatResult r;
|
||||
r.http_status = http_status;
|
||||
|
||||
if (http_status < 200 || http_status >= 300) {
|
||||
r.ok = false;
|
||||
// 尝试提取错误信息
|
||||
try {
|
||||
auto jv = json::parse(body);
|
||||
auto obj = jv.as_object();
|
||||
if (obj.contains("error")) {
|
||||
auto err = obj["error"].as_object();
|
||||
r.error = json::value_to<std::string>(err["message"]);
|
||||
}
|
||||
} catch (...) {
|
||||
r.error = "HTTP " + std::to_string(http_status);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
try {
|
||||
auto jv = json::parse(body);
|
||||
auto obj = jv.as_object();
|
||||
auto choices = obj["choices"].as_array();
|
||||
if (!choices.empty()) {
|
||||
auto msg = choices[0].as_object()["message"].as_object();
|
||||
r.content = json::value_to<std::string>(msg["content"]);
|
||||
r.ok = true;
|
||||
} else {
|
||||
r.ok = false;
|
||||
r.error = "empty response";
|
||||
}
|
||||
} catch (std::exception& e) {
|
||||
r.ok = false;
|
||||
r.error = std::string("json parse: ") + e.what();
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
// ---- SSE 行解析 ----
|
||||
static bool parse_sse_line(const std::string& line, std::string& token_out)
|
||||
{
|
||||
// SSE 格式: "data: <json>" 或 "data: [DONE]"
|
||||
if (line.rfind("data: ", 0) != 0) return false;
|
||||
std::string data = line.substr(6);
|
||||
if (data == "[DONE]") {
|
||||
token_out.clear();
|
||||
return true; // 流结束信号
|
||||
}
|
||||
|
||||
try {
|
||||
auto jv = json::parse(data);
|
||||
auto obj = jv.as_object();
|
||||
auto choices = obj["choices"].as_array();
|
||||
if (!choices.empty()) {
|
||||
auto delta = choices[0].as_object()["delta"].as_object();
|
||||
if (delta.contains("content")) {
|
||||
token_out = json::value_to<std::string>(delta["content"]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
// 忽略解析失败的行
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Impl ----
|
||||
struct DeepSeekClient::Impl {
|
||||
net::HttpClient http;
|
||||
ApiConfig config;
|
||||
|
||||
std::string extract_host_port(std::string& target) {
|
||||
// base_url 例如 "https://api.deepseek.com/v1"
|
||||
// 提取 host: "api.deepseek.com"
|
||||
// 提取 target 前缀: "/v1"
|
||||
std::string url = config.base_url;
|
||||
if (url.rfind("https://", 0) == 0) url = url.substr(8);
|
||||
else if (url.rfind("http://", 0) == 0) url = url.substr(7);
|
||||
|
||||
size_t slash = url.find('/');
|
||||
if (slash != std::string::npos) {
|
||||
target = url.substr(slash);
|
||||
return url.substr(0, slash);
|
||||
}
|
||||
target = "/";
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
DeepSeekClient::DeepSeekClient() : impl_(new Impl{}) {}
|
||||
DeepSeekClient::~DeepSeekClient() { delete impl_; }
|
||||
|
||||
void DeepSeekClient::configure(const ApiConfig& config)
|
||||
{
|
||||
impl_->config = config;
|
||||
}
|
||||
|
||||
ChatResult DeepSeekClient::chat(
|
||||
const std::vector<Message>& history,
|
||||
const std::string& user_input)
|
||||
{
|
||||
std::string target;
|
||||
std::string host = impl_->extract_host_port(target);
|
||||
std::string target_path = target + "/chat/completions";
|
||||
|
||||
std::string body = build_request_json(
|
||||
impl_->config, history, user_input, false);
|
||||
|
||||
std::unordered_map<std::string, std::string> headers;
|
||||
headers["Authorization"] = "Bearer " + impl_->config.api_key;
|
||||
|
||||
auto resp = impl_->http.post_json(host, "443", target_path, body, headers);
|
||||
return parse_response(resp.body, resp.status_code);
|
||||
}
|
||||
|
||||
ChatResult DeepSeekClient::chat_stream(
|
||||
const std::vector<Message>& history,
|
||||
const std::string& user_input,
|
||||
bool (*on_token)(const std::string& token, void* userdata),
|
||||
void* userdata)
|
||||
{
|
||||
std::string target;
|
||||
std::string host = impl_->extract_host_port(target);
|
||||
std::string target_path = target + "/chat/completions";
|
||||
|
||||
std::string body = build_request_json(
|
||||
impl_->config, history, user_input, true);
|
||||
|
||||
std::unordered_map<std::string, std::string> headers;
|
||||
headers["Authorization"] = "Bearer " + impl_->config.api_key;
|
||||
|
||||
ChatResult result;
|
||||
|
||||
auto resp = impl_->http.post_stream(host, "443", target_path, body, headers,
|
||||
[&](const std::string& line) -> bool {
|
||||
if (line.empty()) return true;
|
||||
std::string token;
|
||||
if (!parse_sse_line(line, token)) return true;
|
||||
if (token.empty()) return false; // [DONE]
|
||||
result.content += token;
|
||||
return on_token ? on_token(token, userdata) : true;
|
||||
});
|
||||
|
||||
result.http_status = resp.status_code;
|
||||
|
||||
// 检查传输层错误或非 2xx 状态
|
||||
if (resp.status_code < 200 || resp.status_code >= 300) {
|
||||
result.ok = false;
|
||||
// 尝试从响应 body 提取错误信息(与 parse_response 等同逻辑)
|
||||
try {
|
||||
auto jv = json::parse(resp.body);
|
||||
auto obj = jv.as_object();
|
||||
if (obj.contains("error")) {
|
||||
auto err = obj["error"].as_object();
|
||||
result.error = json::value_to<std::string>(err["message"]);
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
if (result.error.empty()) {
|
||||
if (resp.status_code <= 0) {
|
||||
result.error = "transport error";
|
||||
} else {
|
||||
result.error = "HTTP " + std::to_string(resp.status_code);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (result.content.empty()) {
|
||||
result.ok = false;
|
||||
result.error = "no content received";
|
||||
} else {
|
||||
result.ok = true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace ai
|
||||
} // namespace dstalk
|
||||
@@ -1,65 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dstalk {
|
||||
namespace ai {
|
||||
|
||||
// 单条消息
|
||||
struct Message {
|
||||
std::string role; // "system", "user", "assistant"
|
||||
std::string content;
|
||||
};
|
||||
|
||||
// API 配置
|
||||
struct ApiConfig {
|
||||
std::string provider; // 默认 "deepseek"
|
||||
std::string base_url; // 默认 "https://api.deepseek.com/v1"
|
||||
std::string api_key;
|
||||
std::string model; // 默认 "deepseek-chat"
|
||||
int max_tokens = 4096;
|
||||
double temperature = 0.7;
|
||||
};
|
||||
|
||||
// 对话补全结果
|
||||
struct ChatResult {
|
||||
bool ok = false;
|
||||
std::string content;
|
||||
std::string error;
|
||||
int http_status = 0;
|
||||
};
|
||||
|
||||
/*
|
||||
* DeepSeek API 客户端 (OpenAI 兼容)
|
||||
* 内部使用 HttpClient 进行 HTTPS 通信
|
||||
*/
|
||||
class DeepSeekClient {
|
||||
public:
|
||||
DeepSeekClient();
|
||||
~DeepSeekClient();
|
||||
|
||||
// 配置 API 参数
|
||||
void configure(const ApiConfig& config);
|
||||
|
||||
// 同步对话 (发送全部历史 + 新消息, 返回完整回复)
|
||||
ChatResult chat(
|
||||
const std::vector<Message>& history,
|
||||
const std::string& user_input
|
||||
);
|
||||
|
||||
// 流式对话, 每收到一个 token 调用 on_token, 返回 true 继续 / false 取消
|
||||
ChatResult chat_stream(
|
||||
const std::vector<Message>& history,
|
||||
const std::string& user_input,
|
||||
bool (*on_token)(const std::string& token, void* userdata),
|
||||
void* userdata = nullptr
|
||||
);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
Impl* impl_;
|
||||
};
|
||||
|
||||
} // namespace ai
|
||||
} // namespace dstalk
|
||||
@@ -1,306 +0,0 @@
|
||||
#include "dstalk/dstalk_api.h"
|
||||
#include "ai/deepseek_api.hpp"
|
||||
#include "file/file_io.hpp"
|
||||
#include "net/http_client.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace json = boost::json;
|
||||
|
||||
namespace {
|
||||
|
||||
bool g_initialized = false;
|
||||
dstalk::ai::DeepSeekClient g_ai;
|
||||
dstalk::ai::ApiConfig g_config;
|
||||
std::vector<dstalk::ai::Message> g_history;
|
||||
|
||||
// 默认配置
|
||||
const char* DEFAULT_PROVIDER = "deepseek";
|
||||
const char* DEFAULT_BASE_URL = "https://api.deepseek.com/v1";
|
||||
const char* DEFAULT_MODEL = "deepseek-chat";
|
||||
|
||||
/*
|
||||
* 简易 TOML 解析 (只处理 [api] 段中的 key = "value")
|
||||
* 足够读取 dstalk 配置文件,不引入第三方 TOML 库
|
||||
*/
|
||||
void parse_config_file(const char* path)
|
||||
{
|
||||
if (!path) return;
|
||||
size_t len = 0;
|
||||
char* content = file_read_all(path, &len);
|
||||
if (!content) return;
|
||||
|
||||
std::string data(content, len);
|
||||
std::free(content);
|
||||
|
||||
std::string current_section;
|
||||
size_t pos = 0;
|
||||
while (pos < data.size()) {
|
||||
// 跳过空白
|
||||
while (pos < data.size() && (data[pos] == ' ' || data[pos] == '\t'))
|
||||
pos++;
|
||||
if (pos >= data.size()) break;
|
||||
|
||||
// 找行尾
|
||||
size_t nl = data.find('\n', pos);
|
||||
std::string line = (nl != std::string::npos)
|
||||
? data.substr(pos, nl - pos) : data.substr(pos);
|
||||
pos = (nl != std::string::npos) ? nl + 1 : data.size();
|
||||
|
||||
// 去尾随 \r 和空白
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
|
||||
line.pop_back();
|
||||
|
||||
// 跳过空行和注释
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
// [section]
|
||||
if (line[0] == '[' && line.back() == ']') {
|
||||
current_section = line.substr(1, line.size() - 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
// key = "value" 或 key = value
|
||||
size_t eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
|
||||
std::string key = line.substr(0, eq);
|
||||
while (!key.empty() && key.back() == ' ') key.pop_back();
|
||||
if (key.empty()) continue;
|
||||
|
||||
std::string val = line.substr(eq + 1);
|
||||
while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
|
||||
val.erase(0, 1);
|
||||
// 去引号
|
||||
if (val.size() >= 2 && val.front() == '"' && val.back() == '"')
|
||||
val = val.substr(1, val.size() - 2);
|
||||
|
||||
if (current_section == "api") {
|
||||
if (key == "provider")
|
||||
g_config.provider = val;
|
||||
else if (key == "api_key" || key == "apikey")
|
||||
g_config.api_key = val;
|
||||
else if (key == "base_url")
|
||||
g_config.base_url = val;
|
||||
else if (key == "model")
|
||||
g_config.model = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
char* copy_to_c_string(const std::string& value)
|
||||
{
|
||||
char* output = static_cast<char*>(std::malloc(value.size() + 1));
|
||||
if (!output) return nullptr;
|
||||
std::memcpy(output, value.c_str(), value.size() + 1);
|
||||
return output;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// ---- 初始化 / 销毁 ----
|
||||
|
||||
DSTALK_API int dstalk_init(const char* config_path)
|
||||
{
|
||||
if (g_initialized) return -1;
|
||||
|
||||
// 设置默认值
|
||||
g_config.provider = DEFAULT_PROVIDER;
|
||||
g_config.base_url = DEFAULT_BASE_URL;
|
||||
g_config.model = DEFAULT_MODEL;
|
||||
g_config.max_tokens = 4096;
|
||||
g_config.temperature = 0.7;
|
||||
g_history.clear();
|
||||
|
||||
// 读取配置文件
|
||||
if (config_path) {
|
||||
parse_config_file(config_path);
|
||||
}
|
||||
|
||||
g_ai.configure(g_config);
|
||||
g_initialized = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
DSTALK_API void dstalk_destroy(void)
|
||||
{
|
||||
if (!g_initialized) return;
|
||||
g_history.clear();
|
||||
g_initialized = false;
|
||||
}
|
||||
|
||||
// ---- 配置 ----
|
||||
|
||||
DSTALK_API void dstalk_set_api_key(const char* api_key)
|
||||
{
|
||||
if (!g_initialized || !api_key) return;
|
||||
g_config.api_key = api_key;
|
||||
g_ai.configure(g_config);
|
||||
}
|
||||
|
||||
DSTALK_API void dstalk_set_base_url(const char* base_url)
|
||||
{
|
||||
if (!g_initialized || !base_url) return;
|
||||
g_config.base_url = base_url;
|
||||
g_ai.configure(g_config);
|
||||
}
|
||||
|
||||
DSTALK_API void dstalk_set_model(const char* model)
|
||||
{
|
||||
if (!g_initialized || !model) return;
|
||||
g_config.model = model;
|
||||
g_ai.configure(g_config);
|
||||
}
|
||||
|
||||
// ---- AI 对话 ----
|
||||
|
||||
DSTALK_API int dstalk_chat(const char* input, char** output)
|
||||
{
|
||||
if (!g_initialized || !input || !output) return -1;
|
||||
*output = nullptr;
|
||||
|
||||
auto result = g_ai.chat(g_history, input);
|
||||
if (!result.ok) {
|
||||
*output = copy_to_c_string(result.error);
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* reply = copy_to_c_string(result.content);
|
||||
if (!reply) return -1;
|
||||
|
||||
g_history.push_back({"user", input});
|
||||
g_history.push_back({"assistant", result.content});
|
||||
*output = reply;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
// 流式回调上下文
|
||||
struct StreamCtx {
|
||||
std::string* buf;
|
||||
dstalk_stream_cb cb;
|
||||
void* ud;
|
||||
bool cancelled;
|
||||
};
|
||||
|
||||
static bool on_token_proxy(const std::string& token, void* userdata)
|
||||
{
|
||||
auto* ctx = static_cast<StreamCtx*>(userdata);
|
||||
*ctx->buf += token;
|
||||
int ret = ctx->cb(token.c_str(), ctx->ud);
|
||||
if (ret == 0) return true;
|
||||
ctx->cancelled = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_chat_stream(const char* input,
|
||||
dstalk_stream_cb cb, void* userdata)
|
||||
{
|
||||
if (!g_initialized || !input || !cb) return -1;
|
||||
|
||||
std::string full_reply;
|
||||
StreamCtx ctx{&full_reply, cb, userdata, false};
|
||||
auto result = g_ai.chat_stream(g_history, input, on_token_proxy, &ctx);
|
||||
|
||||
if (!result.ok && !ctx.cancelled) return -1;
|
||||
|
||||
// 更新历史
|
||||
g_history.push_back({"user", input});
|
||||
g_history.push_back({"assistant", full_reply});
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
DSTALK_API void dstalk_free_string(char* str)
|
||||
{
|
||||
std::free(str);
|
||||
}
|
||||
|
||||
// ---- 会话管理 ----
|
||||
|
||||
DSTALK_API void dstalk_session_clear(void)
|
||||
{
|
||||
if (!g_initialized) return;
|
||||
g_history.clear();
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_session_save(const char* path)
|
||||
{
|
||||
if (!g_initialized || !path) return -1;
|
||||
|
||||
std::string data;
|
||||
for (const auto& m : g_history) {
|
||||
json::object entry;
|
||||
entry["role"] = m.role;
|
||||
entry["content"] = m.content;
|
||||
data += json::serialize(entry);
|
||||
data += '\n';
|
||||
}
|
||||
return file_write_all(path, data.c_str());
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_session_load(const char* path)
|
||||
{
|
||||
if (!g_initialized || !path) return -1;
|
||||
size_t len = 0;
|
||||
char* content = file_read_all(path, &len);
|
||||
if (!content) return -1;
|
||||
|
||||
std::string data(content, len);
|
||||
std::free(content);
|
||||
|
||||
std::vector<dstalk::ai::Message> parsed;
|
||||
|
||||
size_t pos = 0;
|
||||
while (pos < data.size()) {
|
||||
size_t nl = data.find('\n', pos);
|
||||
std::string line = (nl != std::string::npos)
|
||||
? data.substr(pos, nl - pos) : data.substr(pos);
|
||||
pos = (nl != std::string::npos) ? nl + 1 : data.size();
|
||||
if (line.empty()) continue;
|
||||
|
||||
try {
|
||||
auto obj = json::parse(line).as_object();
|
||||
auto* role = obj.if_contains("role");
|
||||
auto* content_val = obj.if_contains("content");
|
||||
if (role && content_val && role->is_string() && content_val->is_string()) {
|
||||
parsed.push_back({json::value_to<std::string>(*role),
|
||||
json::value_to<std::string>(*content_val)});
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed.empty()) return -1;
|
||||
g_history = std::move(parsed);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- 文件操作 ----
|
||||
|
||||
DSTALK_API int dstalk_file_read(const char* path, char** content)
|
||||
{
|
||||
if (!g_initialized || !path || !content) return -1;
|
||||
*content = nullptr;
|
||||
|
||||
size_t len = 0;
|
||||
char* buf = file_read_all(path, &len);
|
||||
if (!buf) return -1;
|
||||
|
||||
*content = buf;
|
||||
return 0;
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_file_write(const char* path, const char* content)
|
||||
{
|
||||
if (!g_initialized || !path || !content) return -1;
|
||||
return file_write_all(path, content);
|
||||
}
|
||||
83
dstalk-core/src/config_store.cpp
Normal file
83
dstalk-core/src/config_store.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include "config_store.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace dstalk {
|
||||
|
||||
int ConfigStore::load_file(const char* path)
|
||||
{
|
||||
if (!path) return -1;
|
||||
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) return -1;
|
||||
|
||||
std::stringstream ss;
|
||||
ss << file.rdbuf();
|
||||
std::string data = ss.str();
|
||||
|
||||
// 简易 TOML 解析:只处理 [section] 和 key = "value"
|
||||
std::string current_section;
|
||||
size_t pos = 0;
|
||||
while (pos < data.size()) {
|
||||
while (pos < data.size() && (data[pos] == ' ' || data[pos] == '\t'))
|
||||
pos++;
|
||||
if (pos >= data.size()) break;
|
||||
|
||||
size_t nl = data.find('\n', pos);
|
||||
std::string line = (nl != std::string::npos)
|
||||
? data.substr(pos, nl - pos) : data.substr(pos);
|
||||
pos = (nl != std::string::npos) ? nl + 1 : data.size();
|
||||
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' '))
|
||||
line.pop_back();
|
||||
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
if (line[0] == '[' && line.back() == ']') {
|
||||
current_section = line.substr(1, line.size() - 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t eq = line.find('=');
|
||||
if (eq == std::string::npos) continue;
|
||||
|
||||
std::string key = line.substr(0, eq);
|
||||
while (!key.empty() && key.back() == ' ') key.pop_back();
|
||||
if (key.empty()) continue;
|
||||
|
||||
std::string val = line.substr(eq + 1);
|
||||
while (!val.empty() && (val.front() == ' ' || val.front() == '\t'))
|
||||
val.erase(0, 1);
|
||||
if (val.size() >= 2 && val.front() == '"' && val.back() == '"')
|
||||
val = val.substr(1, val.size() - 2);
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
std::string full_key = current_section.empty()
|
||||
? key : current_section + "." + key;
|
||||
data_[full_key] = val;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* ConfigStore::get(const char* key) const
|
||||
{
|
||||
if (!key) return nullptr;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
auto it = data_.find(key);
|
||||
if (it == data_.end()) return nullptr;
|
||||
return it->second.c_str();
|
||||
}
|
||||
|
||||
int ConfigStore::set(const char* key, const char* value)
|
||||
{
|
||||
if (!key || !value) return -1;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
data_[key] = value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace dstalk
|
||||
28
dstalk-core/src/config_store.hpp
Normal file
28
dstalk-core/src/config_store.hpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace dstalk {
|
||||
|
||||
class ConfigStore {
|
||||
public:
|
||||
ConfigStore() = default;
|
||||
~ConfigStore() = default;
|
||||
|
||||
// 从 TOML 文件加载配置
|
||||
int load_file(const char* path);
|
||||
|
||||
// 获取配置值(返回内部指针,线程安全)
|
||||
const char* get(const char* key) const;
|
||||
|
||||
// 设置配置值
|
||||
int set(const char* key, const char* value);
|
||||
|
||||
private:
|
||||
mutable std::mutex mutex_;
|
||||
std::unordered_map<std::string, std::string> data_;
|
||||
};
|
||||
|
||||
} // namespace dstalk
|
||||
39
dstalk-core/src/event_bus.cpp
Normal file
39
dstalk-core/src/event_bus.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "event_bus.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dstalk {
|
||||
|
||||
int EventBus::subscribe(int event_type, EventHandler handler)
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
int id = next_id_++;
|
||||
subscriptions_.push_back({id, event_type, std::move(handler)});
|
||||
return id;
|
||||
}
|
||||
|
||||
void EventBus::unsubscribe(int subscription_id)
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
subscriptions_.erase(
|
||||
std::remove_if(subscriptions_.begin(), subscriptions_.end(),
|
||||
[subscription_id](const Subscription& s) {
|
||||
return s.id == subscription_id;
|
||||
}),
|
||||
subscriptions_.end());
|
||||
}
|
||||
|
||||
int EventBus::emit(int event_type, const void* data)
|
||||
{
|
||||
std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
int count = 0;
|
||||
for (const auto& sub : subscriptions_) {
|
||||
if (sub.event_type == event_type) {
|
||||
sub.handler(event_type, data);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace dstalk
|
||||
39
dstalk-core/src/event_bus.hpp
Normal file
39
dstalk-core/src/event_bus.hpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dstalk {
|
||||
|
||||
using EventHandler = std::function<void(int event_type, const void* data)>;
|
||||
|
||||
class EventBus {
|
||||
public:
|
||||
EventBus() = default;
|
||||
~EventBus() = default;
|
||||
|
||||
// 订阅事件,返回订阅ID
|
||||
int subscribe(int event_type, EventHandler handler);
|
||||
|
||||
// 取消订阅
|
||||
void unsubscribe(int subscription_id);
|
||||
|
||||
// 发布事件
|
||||
int emit(int event_type, const void* data);
|
||||
|
||||
private:
|
||||
struct Subscription {
|
||||
int id;
|
||||
int event_type;
|
||||
EventHandler handler;
|
||||
};
|
||||
|
||||
mutable std::shared_mutex mutex_;
|
||||
std::vector<Subscription> subscriptions_;
|
||||
int next_id_ = 1;
|
||||
};
|
||||
|
||||
} // namespace dstalk
|
||||
@@ -1,81 +0,0 @@
|
||||
#include "file/file_io.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <io.h>
|
||||
#define STDIN_FILENO _fileno(stdin)
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
char* file_read_all(const char* path, size_t* out_len)
|
||||
{
|
||||
if (!path || !out_len) return nullptr;
|
||||
|
||||
FILE* f = nullptr;
|
||||
#ifdef _WIN32
|
||||
fopen_s(&f, path, "rb");
|
||||
#else
|
||||
f = fopen(path, "rb");
|
||||
#endif
|
||||
if (!f) {
|
||||
*out_len = 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
long sz = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
if (sz < 0) {
|
||||
fclose(f);
|
||||
*out_len = 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (sz == 0) {
|
||||
fclose(f);
|
||||
char* buf = (char*)std::malloc(1);
|
||||
if (!buf) {
|
||||
*out_len = 0;
|
||||
return nullptr;
|
||||
}
|
||||
buf[0] = '\0';
|
||||
*out_len = 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
char* buf = (char*)std::malloc(static_cast<size_t>(sz) + 1);
|
||||
if (!buf) {
|
||||
fclose(f);
|
||||
*out_len = 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
size_t n = fread(buf, 1, static_cast<size_t>(sz), f);
|
||||
fclose(f);
|
||||
buf[n] = '\0';
|
||||
*out_len = n;
|
||||
return buf;
|
||||
}
|
||||
|
||||
int file_write_all(const char* path, const char* content)
|
||||
{
|
||||
if (!path || !content) return -1;
|
||||
|
||||
FILE* f = nullptr;
|
||||
#ifdef _WIN32
|
||||
fopen_s(&f, path, "wb");
|
||||
#else
|
||||
f = fopen(path, "wb");
|
||||
#endif
|
||||
if (!f) return -1;
|
||||
|
||||
size_t len = strlen(content);
|
||||
size_t written = fwrite(content, 1, len, f);
|
||||
fclose(f);
|
||||
|
||||
return (written == len) ? 0 : -1;
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/*
|
||||
* 内部文件 IO 实现
|
||||
* 读取整个文件到内存,返回 malloc 分配的 C 字符串
|
||||
* 调用方负责 free
|
||||
*/
|
||||
char* file_read_all(const char* path, size_t* out_len);
|
||||
|
||||
/*
|
||||
* 将内容写入文件(覆盖模式)
|
||||
* 返回 0 成功,-1 失败
|
||||
*/
|
||||
int file_write_all(const char* path, const char* content);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
362
dstalk-core/src/host.cpp
Normal file
362
dstalk-core/src/host.cpp
Normal file
@@ -0,0 +1,362 @@
|
||||
#include "dstalk/dstalk_host.h"
|
||||
#include "config_store.hpp"
|
||||
#include "event_bus.hpp"
|
||||
#include "service_registry.hpp"
|
||||
#include "plugin_loader.hpp"
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// ============================================================
|
||||
// 全局主机上下文
|
||||
// ============================================================
|
||||
namespace {
|
||||
std::mutex g_init_mutex;
|
||||
bool g_initialized = false;
|
||||
|
||||
dstalk::ConfigStore* g_config = nullptr;
|
||||
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;
|
||||
|
||||
// ---- 内部辅助 ----
|
||||
|
||||
char* host_strdup(const char* s) {
|
||||
if (!s) return nullptr;
|
||||
size_t len = strlen(s);
|
||||
char* copy = (char*)malloc(len + 1);
|
||||
if (copy) memcpy(copy, s, len + 1);
|
||||
return copy;
|
||||
}
|
||||
|
||||
void host_log_impl(int level, const char* fmt, va_list args) {
|
||||
const char* prefix = "";
|
||||
switch (level) {
|
||||
case DSTALK_LOG_DEBUG: prefix = "[DEBUG] "; break;
|
||||
case DSTALK_LOG_INFO: prefix = "[INFO] "; break;
|
||||
case DSTALK_LOG_WARN: prefix = "[WARN] "; break;
|
||||
case DSTALK_LOG_ERROR: prefix = "[ERROR] "; break;
|
||||
}
|
||||
fprintf(stderr, "%s", prefix);
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
vfprintf(stderr, fmt, args);
|
||||
fprintf(stderr, "\n");
|
||||
// 转发到诊断回调
|
||||
if (g_diag_callback) {
|
||||
char buf[1024];
|
||||
vsnprintf(buf, sizeof(buf), fmt, args_copy);
|
||||
g_diag_callback(level, nullptr, 0, nullptr, buf);
|
||||
}
|
||||
va_end(args_copy);
|
||||
}
|
||||
|
||||
void host_log(int level, const char* fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
host_log_impl(level, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
// ---- Host API 表回调 ----
|
||||
|
||||
int api_register_service(const char* name, int version, void* vtable) {
|
||||
return g_service_registry ? g_service_registry->register_service(name, version, vtable) : -1;
|
||||
}
|
||||
|
||||
void* api_query_service(const char* name, int min_version) {
|
||||
return g_service_registry ? g_service_registry->query_service(name, min_version) : nullptr;
|
||||
}
|
||||
|
||||
int api_event_subscribe(int event_type, dstalk_event_handler_fn handler, void* userdata) {
|
||||
if (!g_event_bus || !handler) return -1;
|
||||
return g_event_bus->subscribe(event_type,
|
||||
[handler, userdata](int type, const void* data) {
|
||||
handler(type, data, userdata);
|
||||
});
|
||||
}
|
||||
|
||||
int api_event_emit(int event_type, const void* data) {
|
||||
return g_event_bus ? g_event_bus->emit(event_type, data) : -1;
|
||||
}
|
||||
|
||||
void api_event_unsubscribe(int sub_id) {
|
||||
if (g_event_bus) g_event_bus->unsubscribe(sub_id);
|
||||
}
|
||||
|
||||
const char* api_config_get(const char* key) {
|
||||
return g_config ? g_config->get(key) : nullptr;
|
||||
}
|
||||
|
||||
int api_config_set(const char* key, const char* value) {
|
||||
return g_config ? g_config->set(key, value) : -1;
|
||||
}
|
||||
|
||||
void api_log(int level, const char* fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
host_log_impl(level, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
void* api_alloc(size_t size) { return malloc(size); }
|
||||
void api_free(void* ptr) { free(ptr); }
|
||||
|
||||
char* api_strdup(const char* s) { return host_strdup(s); }
|
||||
|
||||
dstalk_host_api_t g_host_api = {
|
||||
api_register_service,
|
||||
api_query_service,
|
||||
api_event_subscribe,
|
||||
api_event_emit,
|
||||
api_event_unsubscribe,
|
||||
api_config_get,
|
||||
api_config_set,
|
||||
api_log,
|
||||
api_alloc,
|
||||
api_free,
|
||||
api_strdup
|
||||
};
|
||||
|
||||
// ---- 插件目录扫描 ----
|
||||
|
||||
int load_plugins_from_directory(const char* plugin_dir) {
|
||||
if (!plugin_dir) return -1;
|
||||
|
||||
try {
|
||||
fs::path dir(plugin_dir);
|
||||
if (!fs::exists(dir) || !fs::is_directory(dir)) return -1;
|
||||
|
||||
int loaded = 0;
|
||||
for (const auto& entry : fs::directory_iterator(dir)) {
|
||||
if (!entry.is_regular_file()) continue;
|
||||
|
||||
std::string ext = entry.path().extension().string();
|
||||
#ifdef _WIN32
|
||||
if (ext != ".dll") continue;
|
||||
#else
|
||||
if (ext != ".so" && ext != ".dylib") continue;
|
||||
#endif
|
||||
|
||||
int id = g_plugin_loader->load_plugin(entry.path().string().c_str());
|
||||
if (id >= 0) {
|
||||
loaded++;
|
||||
host_log(DSTALK_LOG_INFO, "Loaded plugin: %s",
|
||||
entry.path().filename().string().c_str());
|
||||
}
|
||||
}
|
||||
return loaded;
|
||||
} catch (const std::exception& e) {
|
||||
host_log(DSTALK_LOG_ERROR, "Failed to scan plugin directory: %s", e.what());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 公共 API
|
||||
// ============================================================
|
||||
|
||||
DSTALK_API int dstalk_init(const char* config_path)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_init_mutex);
|
||||
|
||||
if (g_initialized) return -1;
|
||||
|
||||
try {
|
||||
g_config = new dstalk::ConfigStore();
|
||||
g_event_bus = new dstalk::EventBus();
|
||||
g_service_registry = new dstalk::ServiceRegistry();
|
||||
g_plugin_loader = new dstalk::PluginLoader();
|
||||
|
||||
// 加载配置
|
||||
if (config_path && config_path[0]) {
|
||||
if (g_config->load_file(config_path) != 0) {
|
||||
host_log(DSTALK_LOG_WARN, "Failed to load config: %s", config_path);
|
||||
}
|
||||
}
|
||||
|
||||
// 扫描插件目录
|
||||
const char* plugin_dir = g_config->get("plugin_dir");
|
||||
if (!plugin_dir) plugin_dir = "plugins";
|
||||
load_plugins_from_directory(plugin_dir);
|
||||
|
||||
// 初始化所有插件
|
||||
if (g_plugin_loader->initialize_all(&g_host_api) != 0) {
|
||||
host_log(DSTALK_LOG_WARN, "Some plugins failed to initialize");
|
||||
}
|
||||
|
||||
g_initialized = true;
|
||||
host_log(DSTALK_LOG_INFO, "dstalk host initialized");
|
||||
return 0;
|
||||
|
||||
} catch (const std::exception& e) {
|
||||
host_log(DSTALK_LOG_ERROR, "Init failed: %s", e.what());
|
||||
delete g_plugin_loader; g_plugin_loader = nullptr;
|
||||
delete g_service_registry; g_service_registry = nullptr;
|
||||
delete g_event_bus; g_event_bus = nullptr;
|
||||
delete g_config; g_config = nullptr;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
DSTALK_API void dstalk_shutdown(void)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_init_mutex);
|
||||
if (!g_initialized) return;
|
||||
|
||||
host_log(DSTALK_LOG_INFO, "dstalk shutting down...");
|
||||
|
||||
if (g_plugin_loader) {
|
||||
g_plugin_loader->shutdown_all();
|
||||
delete g_plugin_loader;
|
||||
g_plugin_loader = nullptr;
|
||||
}
|
||||
|
||||
delete g_service_registry; g_service_registry = nullptr;
|
||||
delete g_event_bus; g_event_bus = nullptr;
|
||||
delete g_config; g_config = nullptr;
|
||||
|
||||
g_initialized = false;
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_plugin_load(const char* path)
|
||||
{
|
||||
if (!g_initialized || !g_plugin_loader) return -1;
|
||||
int id = g_plugin_loader->load_plugin(path);
|
||||
if (id >= 0) {
|
||||
g_plugin_loader->initialize_pending(&g_host_api);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_plugin_unload(int plugin_id)
|
||||
{
|
||||
if (!g_initialized || !g_plugin_loader) return -1;
|
||||
return g_plugin_loader->unload_plugin(plugin_id);
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_plugin_list(char** output_json)
|
||||
{
|
||||
if (!g_initialized || !g_plugin_loader || !output_json) return -1;
|
||||
*output_json = host_strdup(g_plugin_loader->list_plugins().c_str());
|
||||
return *output_json ? 0 : -1;
|
||||
}
|
||||
|
||||
DSTALK_API void* dstalk_service_query(const char* service_name, int min_version)
|
||||
{
|
||||
if (!g_initialized || !g_service_registry) return nullptr;
|
||||
return g_service_registry->query_service(service_name, min_version);
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_event_subscribe(int event_type, dstalk_event_handler_fn handler, void* userdata)
|
||||
{
|
||||
if (!g_initialized || !g_event_bus || !handler) return -1;
|
||||
return g_event_bus->subscribe(event_type,
|
||||
[handler, userdata](int type, const void* data) { handler(type, data, userdata); });
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_event_emit(int event_type, const void* data)
|
||||
{
|
||||
if (!g_initialized || !g_event_bus) return -1;
|
||||
return g_event_bus->emit(event_type, data);
|
||||
}
|
||||
|
||||
DSTALK_API void dstalk_event_unsubscribe(int subscription_id)
|
||||
{
|
||||
if (!g_initialized || !g_event_bus) return;
|
||||
g_event_bus->unsubscribe(subscription_id);
|
||||
}
|
||||
|
||||
DSTALK_API const char* dstalk_config_get(const char* key)
|
||||
{
|
||||
if (!g_initialized || !g_config) return nullptr;
|
||||
return g_config->get(key);
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_config_set(const char* key, const char* value)
|
||||
{
|
||||
if (!g_initialized || !g_config) return -1;
|
||||
return g_config->set(key, value);
|
||||
}
|
||||
|
||||
DSTALK_API void dstalk_log(int level, const char* fmt, ...)
|
||||
{
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
host_log_impl(level, fmt, args);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
DSTALK_API void* dstalk_alloc(size_t size) { return malloc(size); }
|
||||
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;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// LSP 便捷函数 (委托给 "lsp" 服务插件)
|
||||
// ============================================================
|
||||
|
||||
static const dstalk_lsp_service_t* get_lsp_service() {
|
||||
if (!g_initialized || !g_service_registry) return nullptr;
|
||||
return static_cast<const dstalk_lsp_service_t*>(
|
||||
g_service_registry->query_service("lsp", 1));
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_lsp_start(const char* server_cmd, const char* language)
|
||||
{
|
||||
auto* svc = get_lsp_service();
|
||||
if (!svc || !svc->start) return -1;
|
||||
return svc->start(server_cmd, language);
|
||||
}
|
||||
|
||||
DSTALK_API void dstalk_lsp_stop(void)
|
||||
{
|
||||
auto* svc = get_lsp_service();
|
||||
if (svc && svc->stop) svc->stop();
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_lsp_open(const char* uri, const char* content, const char* language_id)
|
||||
{
|
||||
auto* svc = get_lsp_service();
|
||||
if (!svc || !svc->open_document) return -1;
|
||||
return svc->open_document(uri, content, language_id);
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_lsp_close(const char* uri)
|
||||
{
|
||||
auto* svc = get_lsp_service();
|
||||
if (!svc || !svc->close_document) return -1;
|
||||
return svc->close_document(uri);
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_lsp_diagnostics(const char* uri, char** output)
|
||||
{
|
||||
auto* svc = get_lsp_service();
|
||||
if (!svc || !svc->get_diagnostics) return -1;
|
||||
return svc->get_diagnostics(uri, output);
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_lsp_hover(const char* uri, int line, int character, char** output)
|
||||
{
|
||||
auto* svc = get_lsp_service();
|
||||
if (!svc || !svc->get_hover) return -1;
|
||||
return svc->get_hover(uri, line, character, output);
|
||||
}
|
||||
|
||||
DSTALK_API int dstalk_lsp_completion(const char* uri, int line, int character, char** output)
|
||||
{
|
||||
auto* svc = get_lsp_service();
|
||||
if (!svc || !svc->get_completion) return -1;
|
||||
return svc->get_completion(uri, line, character, output);
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
// MSVC 14.16 (VS 2017) doesn't provide std::to_address (C++20)
|
||||
#define BOOST_ASIO_DISABLE_STD_TO_ADDRESS
|
||||
|
||||
#include "net/http_client.hpp"
|
||||
|
||||
#include <boost/asio/connect.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/ssl.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace beast = boost::beast;
|
||||
namespace http = beast::http;
|
||||
namespace asio = boost::asio;
|
||||
namespace ssl = boost::asio::ssl;
|
||||
using tcp = asio::ip::tcp;
|
||||
|
||||
namespace dstalk {
|
||||
namespace net {
|
||||
|
||||
struct HttpClient::Impl {
|
||||
asio::io_context ioc;
|
||||
ssl::context ssl_ctx{ssl::context::tlsv12_client};
|
||||
int connect_timeout = 30;
|
||||
int request_timeout = 120;
|
||||
|
||||
Impl() {
|
||||
ssl_ctx.set_default_verify_paths();
|
||||
}
|
||||
};
|
||||
|
||||
HttpClient::HttpClient() : impl_(new Impl{}) {}
|
||||
HttpClient::~HttpClient() { delete impl_; }
|
||||
|
||||
void HttpClient::set_timeout(int connect_sec, int request_sec)
|
||||
{
|
||||
impl_->connect_timeout = connect_sec;
|
||||
impl_->request_timeout = request_sec;
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::post_json(
|
||||
const std::string& host,
|
||||
const std::string& port,
|
||||
const std::string& target,
|
||||
const std::string& json_body,
|
||||
const std::unordered_map<std::string, std::string>& extra_headers)
|
||||
{
|
||||
return post_stream(host, port, target, json_body, extra_headers, nullptr);
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::post_stream(
|
||||
const std::string& host,
|
||||
const std::string& port,
|
||||
const std::string& target,
|
||||
const std::string& json_body,
|
||||
const std::unordered_map<std::string, std::string>& extra_headers,
|
||||
std::function<bool(const std::string&)> on_line)
|
||||
{
|
||||
HttpResponse result;
|
||||
|
||||
try {
|
||||
tcp::resolver resolver(impl_->ioc);
|
||||
auto endpoints = resolver.resolve(host, port);
|
||||
|
||||
ssl::stream<tcp::socket> stream(impl_->ioc, impl_->ssl_ctx);
|
||||
beast::flat_buffer buffer;
|
||||
|
||||
// SNI hostname (required for HTTPS)
|
||||
if (!SSL_set_tlsext_host_name(stream.native_handle(), host.c_str())) {
|
||||
result.status_code = -1;
|
||||
return result;
|
||||
}
|
||||
|
||||
asio::connect(beast::get_lowest_layer(stream), endpoints);
|
||||
stream.handshake(ssl::stream_base::client);
|
||||
|
||||
// Build HTTP POST request
|
||||
http::request<http::string_body> req{http::verb::post, target, 11};
|
||||
req.set(http::field::host, host);
|
||||
req.set(http::field::user_agent, "dstalk/0.1");
|
||||
req.set(http::field::content_type, "application/json");
|
||||
req.body() = json_body;
|
||||
req.prepare_payload();
|
||||
|
||||
for (const auto& h : extra_headers) {
|
||||
req.set(h.first, h.second);
|
||||
}
|
||||
|
||||
// Send
|
||||
http::write(stream, req);
|
||||
|
||||
// Read response
|
||||
http::response_parser<http::string_body> parser;
|
||||
parser.body_limit(16 * 1024 * 1024);
|
||||
http::read_header(stream, buffer, parser);
|
||||
|
||||
result.status_code = parser.get().result_int();
|
||||
|
||||
beast::error_code ec;
|
||||
|
||||
if (on_line) {
|
||||
std::string fragment = parser.get().body();
|
||||
auto emit_lines = [&]() -> bool {
|
||||
size_t pos = 0;
|
||||
while (pos < fragment.size()) {
|
||||
size_t nl = fragment.find('\n', pos);
|
||||
if (nl == std::string::npos) break;
|
||||
std::string line = fragment.substr(pos, nl - pos);
|
||||
if (!line.empty() && line.back() == '\r')
|
||||
line.pop_back();
|
||||
if (!on_line(line)) return false;
|
||||
pos = nl + 1;
|
||||
}
|
||||
if (pos > 0)
|
||||
fragment = fragment.substr(pos);
|
||||
return true;
|
||||
};
|
||||
if (!emit_lines()) goto done;
|
||||
|
||||
size_t processed = parser.get().body().size();
|
||||
while (!parser.is_done()) {
|
||||
http::read_some(stream, buffer, parser, ec);
|
||||
if (ec) break;
|
||||
|
||||
const std::string& full_body = parser.get().body();
|
||||
if (full_body.size() > processed) {
|
||||
std::string_view new_data(full_body.data() + processed,
|
||||
full_body.size() - processed);
|
||||
processed = full_body.size();
|
||||
|
||||
fragment.append(new_data.data(), new_data.size());
|
||||
if (!emit_lines()) goto done;
|
||||
}
|
||||
}
|
||||
if (!fragment.empty()) {
|
||||
if (fragment.back() == '\r')
|
||||
fragment.pop_back();
|
||||
if (!fragment.empty())
|
||||
on_line(fragment);
|
||||
}
|
||||
} else {
|
||||
while (!parser.is_done()) {
|
||||
http::read_some(stream, buffer, parser, ec);
|
||||
if (ec) break;
|
||||
}
|
||||
}
|
||||
done:
|
||||
result.body = parser.get().body();
|
||||
beast::get_lowest_layer(stream).cancel();
|
||||
stream.shutdown(ec);
|
||||
} catch (std::exception& e) {
|
||||
result.status_code = -1;
|
||||
result.body = e.what();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dstalk
|
||||
@@ -1,53 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace dstalk {
|
||||
namespace net {
|
||||
|
||||
struct HttpResponse {
|
||||
int status_code = 0;
|
||||
std::string body;
|
||||
std::unordered_map<std::string, std::string> headers;
|
||||
};
|
||||
|
||||
/*
|
||||
* HTTPS 客户端统一接口
|
||||
* 所有平台统一使用 Boost.Beast + OpenSSL 实现
|
||||
*/
|
||||
class HttpClient {
|
||||
public:
|
||||
HttpClient();
|
||||
~HttpClient();
|
||||
|
||||
void set_timeout(int connect_sec, int request_sec);
|
||||
|
||||
// 同步 POST JSON, 返回完整响应
|
||||
HttpResponse post_json(
|
||||
const std::string& host,
|
||||
const std::string& port,
|
||||
const std::string& target,
|
||||
const std::string& json_body,
|
||||
const std::unordered_map<std::string, std::string>& extra_headers
|
||||
);
|
||||
|
||||
// 流式 POST (SSE 逐行回调), on_line 返回 false 提前终止
|
||||
using StreamCallback = std::function<bool(const std::string& line)>;
|
||||
HttpResponse post_stream(
|
||||
const std::string& host,
|
||||
const std::string& port,
|
||||
const std::string& target,
|
||||
const std::string& json_body,
|
||||
const std::unordered_map<std::string, std::string>& extra_headers,
|
||||
StreamCallback on_line
|
||||
);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
Impl* impl_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dstalk
|
||||
@@ -1,223 +0,0 @@
|
||||
#include "net/http_client.hpp"
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <winhttp.h>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#pragma comment(lib, "winhttp.lib")
|
||||
|
||||
namespace dstalk {
|
||||
namespace net {
|
||||
|
||||
// ---- 宽字符转换 ----
|
||||
static std::wstring to_w(const std::string& s)
|
||||
{
|
||||
if (s.empty()) return L"";
|
||||
int len = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, nullptr, 0);
|
||||
std::wstring out(len - 1, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), -1, &out[0], len);
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- 读取全部 body ----
|
||||
static std::string read_all(HINTERNET hRequest, DWORD& status_code)
|
||||
{
|
||||
DWORD status = 0, statusSize = sizeof(status);
|
||||
WinHttpQueryHeaders(hRequest,
|
||||
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusSize,
|
||||
WINHTTP_NO_HEADER_INDEX);
|
||||
status_code = status;
|
||||
|
||||
std::string body;
|
||||
char buf[4096];
|
||||
DWORD bytesRead = 0;
|
||||
while (WinHttpReadData(hRequest, buf, sizeof(buf), &bytesRead)) {
|
||||
if (bytesRead == 0) break;
|
||||
body.append(buf, bytesRead);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
// ---- 流式读取 (SSE 逐行回调) ----
|
||||
static std::string read_stream(HINTERNET hRequest, DWORD& status_code,
|
||||
HttpClient::StreamCallback on_line)
|
||||
{
|
||||
DWORD status = 0, statusSize = sizeof(status);
|
||||
WinHttpQueryHeaders(hRequest,
|
||||
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusSize,
|
||||
WINHTTP_NO_HEADER_INDEX);
|
||||
status_code = status;
|
||||
|
||||
if (status < 200 || status >= 300) {
|
||||
return read_all(hRequest, status_code);
|
||||
}
|
||||
|
||||
std::string body;
|
||||
std::string lineBuf;
|
||||
char buf[1024];
|
||||
DWORD bytesRead = 0;
|
||||
|
||||
while (WinHttpReadData(hRequest, buf, sizeof(buf), &bytesRead)) {
|
||||
if (bytesRead == 0) break;
|
||||
|
||||
for (DWORD i = 0; i < bytesRead; i++) {
|
||||
char c = buf[i];
|
||||
body += c;
|
||||
if (c == '\n') {
|
||||
while (!lineBuf.empty() && lineBuf.back() == '\r')
|
||||
lineBuf.pop_back();
|
||||
if (!lineBuf.empty()) {
|
||||
if (!on_line(lineBuf)) return body;
|
||||
}
|
||||
lineBuf.clear();
|
||||
} else if (c != '\r') {
|
||||
lineBuf += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (!lineBuf.empty() && lineBuf.back() == '\r')
|
||||
lineBuf.pop_back();
|
||||
if (!lineBuf.empty()) on_line(lineBuf);
|
||||
return body;
|
||||
}
|
||||
|
||||
// ---- Impl ----
|
||||
struct HttpClient::Impl {
|
||||
HINTERNET hSession = nullptr;
|
||||
int connect_timeout = 30;
|
||||
int request_timeout = 120;
|
||||
};
|
||||
|
||||
HttpClient::HttpClient() : impl_(new Impl{})
|
||||
{
|
||||
impl_->hSession = WinHttpOpen(
|
||||
L"dstalk/0.1",
|
||||
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME,
|
||||
WINHTTP_NO_PROXY_BYPASS, 0);
|
||||
}
|
||||
|
||||
HttpClient::~HttpClient()
|
||||
{
|
||||
if (impl_->hSession) WinHttpCloseHandle(impl_->hSession);
|
||||
delete impl_;
|
||||
}
|
||||
|
||||
void HttpClient::set_timeout(int connect_sec, int request_sec)
|
||||
{
|
||||
impl_->connect_timeout = connect_sec;
|
||||
impl_->request_timeout = request_sec;
|
||||
}
|
||||
|
||||
// ---- 核心请求逻辑 ----
|
||||
static HttpResponse do_request(
|
||||
HINTERNET hSession,
|
||||
const std::string& host,
|
||||
const std::string& port,
|
||||
const std::string& target,
|
||||
const std::string& json_body,
|
||||
const std::unordered_map<std::string, std::string>& extra_headers,
|
||||
int connect_timeout,
|
||||
int request_timeout,
|
||||
HttpClient::StreamCallback on_line)
|
||||
{
|
||||
HttpResponse result;
|
||||
|
||||
int nPort = port.empty() ? 443 : std::stoi(port);
|
||||
DWORD flags = (nPort == 443) ? WINHTTP_FLAG_SECURE : 0;
|
||||
|
||||
std::wstring wHost = to_w(host);
|
||||
std::wstring wPath = to_w(target);
|
||||
HINTERNET hConnect = WinHttpConnect(hSession, wHost.c_str(), (WORD)nPort, 0);
|
||||
if (!hConnect) { result.status_code = -1; return result; }
|
||||
|
||||
LPCWSTR acceptTypes[] = { L"application/json", nullptr };
|
||||
HINTERNET hRequest = WinHttpOpenRequest(
|
||||
hConnect, L"POST", wPath.c_str(),
|
||||
nullptr, WINHTTP_NO_REFERER, acceptTypes, flags);
|
||||
if (!hRequest) {
|
||||
WinHttpCloseHandle(hConnect);
|
||||
result.status_code = -1;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Headers
|
||||
WinHttpAddRequestHeaders(hRequest,
|
||||
L"Content-Type: application/json\r\n", -1,
|
||||
WINHTTP_ADDREQ_FLAG_ADD);
|
||||
for (const auto& h : extra_headers) {
|
||||
std::string hdr = h.first + ": " + h.second + "\r\n";
|
||||
std::wstring whdr = to_w(hdr);
|
||||
WinHttpAddRequestHeaders(hRequest, whdr.c_str(), -1,
|
||||
WINHTTP_ADDREQ_FLAG_ADD);
|
||||
}
|
||||
|
||||
// Timeouts
|
||||
WinHttpSetTimeouts(hRequest,
|
||||
connect_timeout * 1000, connect_timeout * 1000,
|
||||
request_timeout * 1000, request_timeout * 1000);
|
||||
|
||||
// Send
|
||||
DWORD bodyLen = (DWORD)json_body.size();
|
||||
BOOL sent = WinHttpSendRequest(
|
||||
hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
|
||||
(LPVOID)json_body.data(), bodyLen, bodyLen, 0);
|
||||
if (!sent) {
|
||||
WinHttpCloseHandle(hRequest);
|
||||
WinHttpCloseHandle(hConnect);
|
||||
result.status_code = -1;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Receive
|
||||
WinHttpReceiveResponse(hRequest, nullptr);
|
||||
DWORD status = 0;
|
||||
if (on_line) {
|
||||
result.body = read_stream(hRequest, status, on_line);
|
||||
} else {
|
||||
result.body = read_all(hRequest, status);
|
||||
}
|
||||
result.status_code = (int)status;
|
||||
|
||||
WinHttpCloseHandle(hRequest);
|
||||
WinHttpCloseHandle(hConnect);
|
||||
return result;
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::post_json(
|
||||
const std::string& host,
|
||||
const std::string& port,
|
||||
const std::string& target,
|
||||
const std::string& json_body,
|
||||
const std::unordered_map<std::string, std::string>& extra_headers)
|
||||
{
|
||||
return post_stream(host, port, target, json_body, extra_headers, nullptr);
|
||||
}
|
||||
|
||||
HttpResponse HttpClient::post_stream(
|
||||
const std::string& host,
|
||||
const std::string& port,
|
||||
const std::string& target,
|
||||
const std::string& json_body,
|
||||
const std::unordered_map<std::string, std::string>& extra_headers,
|
||||
StreamCallback on_line)
|
||||
{
|
||||
return do_request(impl_->hSession, host, port, target, json_body,
|
||||
extra_headers,
|
||||
impl_->connect_timeout, impl_->request_timeout,
|
||||
on_line);
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dstalk
|
||||
|
||||
#else
|
||||
// 非 Windows: 需要 Boost.Beast 实现 (编译时会报错提示)
|
||||
# error "WinHTTP backend is Windows-only. Use net/http_client.cpp for non-Windows builds."
|
||||
#endif
|
||||
291
dstalk-core/src/plugin_loader.cpp
Normal file
291
dstalk-core/src/plugin_loader.cpp
Normal file
@@ -0,0 +1,291 @@
|
||||
#include "plugin_loader.hpp"
|
||||
|
||||
#include <boost/json.hpp>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <dlfcn.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <queue>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace dstalk {
|
||||
|
||||
namespace json = boost::json;
|
||||
|
||||
PluginLoader::~PluginLoader()
|
||||
{
|
||||
shutdown_all();
|
||||
}
|
||||
|
||||
int PluginLoader::load_plugin(const char* path)
|
||||
{
|
||||
if (!path) return -1;
|
||||
|
||||
// 加载DLL
|
||||
#ifdef _WIN32
|
||||
void* handle = LoadLibraryA(path);
|
||||
#else
|
||||
void* handle = dlopen(path, RTLD_NOW | RTLD_LOCAL);
|
||||
#endif
|
||||
|
||||
if (!handle) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 获取入口函数
|
||||
#ifdef _WIN32
|
||||
auto init_fn = (dstalk_plugin_init_fn)GetProcAddress(
|
||||
(HMODULE)handle, "dstalk_plugin_init");
|
||||
#else
|
||||
auto init_fn = (dstalk_plugin_init_fn)dlsym(handle, "dstalk_plugin_init");
|
||||
#endif
|
||||
|
||||
if (!init_fn) {
|
||||
#ifdef _WIN32
|
||||
FreeLibrary((HMODULE)handle);
|
||||
#else
|
||||
dlclose(handle);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 调用入口函数获取插件信息
|
||||
dstalk_plugin_info_t* info = init_fn();
|
||||
if (!info) {
|
||||
#ifdef _WIN32
|
||||
FreeLibrary((HMODULE)handle);
|
||||
#else
|
||||
dlclose(handle);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 检查API版本兼容性
|
||||
if (info->api_version != DSTALK_API_VERSION) {
|
||||
#ifdef _WIN32
|
||||
FreeLibrary((HMODULE)handle);
|
||||
#else
|
||||
dlclose(handle);
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 创建插件信息
|
||||
int id = next_id_++;
|
||||
PluginInfo plugin;
|
||||
plugin.id = id;
|
||||
plugin.name = info->name ? info->name : "";
|
||||
plugin.version = info->version ? info->version : "";
|
||||
plugin.description = info->description ? info->description : "";
|
||||
plugin.api_version = info->api_version;
|
||||
plugin.handle = handle;
|
||||
plugin.info = info;
|
||||
plugin.initialized = false;
|
||||
|
||||
// 解析依赖
|
||||
for (int i = 0; i < DSTALK_MAX_DEPS && info->dependencies[i]; i++) {
|
||||
plugin.dependencies.push_back(info->dependencies[i]);
|
||||
}
|
||||
|
||||
plugins_[id] = std::move(plugin);
|
||||
return id;
|
||||
}
|
||||
|
||||
int PluginLoader::unload_plugin(int plugin_id)
|
||||
{
|
||||
auto it = plugins_.find(plugin_id);
|
||||
if (it == plugins_.end()) return -1;
|
||||
|
||||
PluginInfo& plugin = it->second;
|
||||
|
||||
// 调用关闭回调
|
||||
if (plugin.initialized && plugin.info->on_shutdown) {
|
||||
plugin.info->on_shutdown();
|
||||
}
|
||||
|
||||
// 卸载DLL
|
||||
#ifdef _WIN32
|
||||
FreeLibrary((HMODULE)plugin.handle);
|
||||
#else
|
||||
dlclose(plugin.handle);
|
||||
#endif
|
||||
|
||||
plugins_.erase(it);
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string PluginLoader::list_plugins() const
|
||||
{
|
||||
json::array arr;
|
||||
for (const auto& [id, plugin] : plugins_) {
|
||||
json::object obj;
|
||||
obj["id"] = id;
|
||||
obj["name"] = plugin.name;
|
||||
obj["version"] = plugin.version;
|
||||
obj["description"] = plugin.description;
|
||||
obj["api_version"] = plugin.api_version;
|
||||
obj["initialized"] = plugin.initialized;
|
||||
|
||||
json::array deps;
|
||||
for (const auto& dep : plugin.dependencies) {
|
||||
deps.push_back(json::value(dep));
|
||||
}
|
||||
obj["dependencies"] = std::move(deps);
|
||||
|
||||
arr.push_back(std::move(obj));
|
||||
}
|
||||
return json::serialize(arr);
|
||||
}
|
||||
|
||||
std::vector<int> PluginLoader::topological_sort() const
|
||||
{
|
||||
// 构建名称到ID的映射
|
||||
std::unordered_map<std::string, int> name_to_id;
|
||||
for (const auto& [id, plugin] : plugins_) {
|
||||
name_to_id[plugin.name] = id;
|
||||
}
|
||||
|
||||
// 计算入度
|
||||
std::unordered_map<int, int> in_degree;
|
||||
std::unordered_map<int, std::vector<int>> dependents;
|
||||
|
||||
for (const auto& [id, plugin] : plugins_) {
|
||||
in_degree[id] = 0;
|
||||
}
|
||||
|
||||
for (const auto& [id, plugin] : plugins_) {
|
||||
for (const auto& dep_name : plugin.dependencies) {
|
||||
auto it = name_to_id.find(dep_name);
|
||||
if (it != name_to_id.end()) {
|
||||
int dep_id = it->second;
|
||||
dependents[dep_id].push_back(id);
|
||||
in_degree[id]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 拓扑排序(Kahn算法)
|
||||
std::queue<int> queue;
|
||||
for (const auto& [id, degree] : in_degree) {
|
||||
if (degree == 0) {
|
||||
queue.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int> sorted;
|
||||
while (!queue.empty()) {
|
||||
int id = queue.front();
|
||||
queue.pop();
|
||||
sorted.push_back(id);
|
||||
|
||||
for (int dependent : dependents[id]) {
|
||||
if (--in_degree[dependent] == 0) {
|
||||
queue.push(dependent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查循环依赖
|
||||
if (sorted.size() != plugins_.size()) {
|
||||
throw std::runtime_error("Circular dependency detected");
|
||||
}
|
||||
|
||||
return sorted;
|
||||
}
|
||||
|
||||
int PluginLoader::initialize_all(const dstalk_host_api_t* host_api)
|
||||
{
|
||||
try {
|
||||
std::vector<int> order = topological_sort();
|
||||
|
||||
for (int id : order) {
|
||||
auto it = plugins_.find(id);
|
||||
if (it == plugins_.end()) continue;
|
||||
|
||||
PluginInfo& plugin = it->second;
|
||||
if (plugin.initialized) continue;
|
||||
|
||||
if (plugin.info->on_init) {
|
||||
int result = plugin.info->on_init(host_api);
|
||||
if (result != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
plugin.initialized = true;
|
||||
}
|
||||
|
||||
return 0;
|
||||
} catch (const std::exception&) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int PluginLoader::initialize_pending(const dstalk_host_api_t* host_api)
|
||||
{
|
||||
try {
|
||||
std::vector<int> order = topological_sort();
|
||||
|
||||
int count = 0;
|
||||
for (int id : order) {
|
||||
auto it = plugins_.find(id);
|
||||
if (it == plugins_.end()) continue;
|
||||
|
||||
PluginInfo& plugin = it->second;
|
||||
if (plugin.initialized) continue;
|
||||
|
||||
if (plugin.info->on_init) {
|
||||
int result = plugin.info->on_init(host_api);
|
||||
if (result != 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
plugin.initialized = true;
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
} catch (const std::exception&) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
void PluginLoader::shutdown_all()
|
||||
{
|
||||
// 按逆序关闭
|
||||
std::vector<int> order;
|
||||
try {
|
||||
order = topological_sort();
|
||||
std::reverse(order.begin(), order.end());
|
||||
} catch (...) {
|
||||
// 如果排序失败,按任意顺序关闭
|
||||
for (const auto& [id, _] : plugins_) {
|
||||
order.push_back(id);
|
||||
}
|
||||
}
|
||||
|
||||
for (int id : order) {
|
||||
auto it = plugins_.find(id);
|
||||
if (it == plugins_.end()) continue;
|
||||
|
||||
PluginInfo& plugin = it->second;
|
||||
if (!plugin.initialized) continue;
|
||||
|
||||
if (plugin.info->on_shutdown) {
|
||||
plugin.info->on_shutdown();
|
||||
}
|
||||
plugin.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
const PluginInfo* PluginLoader::get_plugin(int plugin_id) const
|
||||
{
|
||||
auto it = plugins_.find(plugin_id);
|
||||
if (it == plugins_.end()) return nullptr;
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
} // namespace dstalk
|
||||
57
dstalk-core/src/plugin_loader.hpp
Normal file
57
dstalk-core/src/plugin_loader.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "dstalk/dstalk_host.h"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace dstalk {
|
||||
|
||||
struct PluginInfo {
|
||||
int id;
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::string description;
|
||||
int api_version;
|
||||
std::vector<std::string> dependencies;
|
||||
|
||||
void* handle; // DLL handle
|
||||
dstalk_plugin_info_t* info;
|
||||
bool initialized;
|
||||
};
|
||||
|
||||
class PluginLoader {
|
||||
public:
|
||||
PluginLoader() = default;
|
||||
~PluginLoader();
|
||||
|
||||
// 加载插件(返回插件ID,失败返回-1)
|
||||
int load_plugin(const char* path);
|
||||
|
||||
// 卸载插件
|
||||
int unload_plugin(int plugin_id);
|
||||
|
||||
// 获取插件列表(JSON格式)
|
||||
std::string list_plugins() const;
|
||||
|
||||
// 按依赖顺序初始化所有插件
|
||||
int initialize_all(const dstalk_host_api_t* host_api);
|
||||
|
||||
// 仅初始化尚未初始化的插件(增量加载场景)
|
||||
int initialize_pending(const dstalk_host_api_t* host_api);
|
||||
|
||||
// 关闭所有插件
|
||||
void shutdown_all();
|
||||
|
||||
// 获取插件信息
|
||||
const PluginInfo* get_plugin(int plugin_id) const;
|
||||
|
||||
private:
|
||||
// 拓扑排序(按依赖顺序)
|
||||
std::vector<int> topological_sort() const;
|
||||
|
||||
std::unordered_map<int, PluginInfo> plugins_;
|
||||
int next_id_ = 1;
|
||||
};
|
||||
|
||||
} // namespace dstalk
|
||||
42
dstalk-core/src/service_registry.cpp
Normal file
42
dstalk-core/src/service_registry.cpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#include "service_registry.hpp"
|
||||
|
||||
namespace dstalk {
|
||||
|
||||
int ServiceRegistry::register_service(const char* name, int version, void* vtable)
|
||||
{
|
||||
if (!name || !vtable) return -1;
|
||||
|
||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
|
||||
// 检查是否已注册
|
||||
if (services_.find(name) != services_.end()) {
|
||||
return -2; // 已存在
|
||||
}
|
||||
|
||||
services_[name] = {name, version, vtable};
|
||||
return 0;
|
||||
}
|
||||
|
||||
void* ServiceRegistry::query_service(const char* name, int min_version) const
|
||||
{
|
||||
if (!name) return nullptr;
|
||||
|
||||
std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||
|
||||
auto it = services_.find(name);
|
||||
if (it == services_.end()) return nullptr;
|
||||
|
||||
if (it->second.version < min_version) return nullptr;
|
||||
|
||||
return it->second.vtable;
|
||||
}
|
||||
|
||||
void ServiceRegistry::unregister_service(const char* name)
|
||||
{
|
||||
if (!name) return;
|
||||
|
||||
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||
services_.erase(name);
|
||||
}
|
||||
|
||||
} // namespace dstalk
|
||||
35
dstalk-core/src/service_registry.hpp
Normal file
35
dstalk-core/src/service_registry.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace dstalk {
|
||||
|
||||
class ServiceRegistry {
|
||||
public:
|
||||
ServiceRegistry() = default;
|
||||
~ServiceRegistry() = default;
|
||||
|
||||
// 注册服务
|
||||
int register_service(const char* name, int version, void* vtable);
|
||||
|
||||
// 查询服务(返回 vtable 指针,或 nullptr)
|
||||
void* query_service(const char* name, int min_version) const;
|
||||
|
||||
// 注销服务
|
||||
void unregister_service(const char* name);
|
||||
|
||||
private:
|
||||
struct ServiceEntry {
|
||||
std::string name;
|
||||
int version;
|
||||
void* vtable;
|
||||
};
|
||||
|
||||
mutable std::shared_mutex mutex_;
|
||||
std::unordered_map<std::string, ServiceEntry> services_;
|
||||
};
|
||||
|
||||
} // namespace dstalk
|
||||
Reference in New Issue
Block a user