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

@@ -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