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:
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
|
||||
Reference in New Issue
Block a user