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:
289
plugins/context/src/context_plugin.cpp
Normal file
289
plugins/context/src/context_plugin.cpp
Normal file
@@ -0,0 +1,289 @@
|
||||
// plugin-context: 上下文管理服务插件
|
||||
// 提供 dstalk_context_service_t vtable 实现
|
||||
// 依赖: session (获取历史消息做 token 计数)
|
||||
#include "dstalk/dstalk_host.h"
|
||||
#include "dstalk/dstalk_types.h"
|
||||
#include "dstalk/dstalk_services.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// ============================================================
|
||||
// 全局状态
|
||||
// ============================================================
|
||||
|
||||
static const dstalk_host_api_t* g_host = nullptr;
|
||||
static const dstalk_session_service_t* g_session = nullptr;
|
||||
static size_t g_max_tokens = 4096;
|
||||
|
||||
// ============================================================
|
||||
// 内部 C++ 辅助:token 计数
|
||||
// ============================================================
|
||||
|
||||
static bool cjk_is_ascii(unsigned char c) { return c < 0x80; }
|
||||
|
||||
static bool cjk_starts_cjk(unsigned char c) {
|
||||
// U+4E00-U+9FFF 在 UTF-8 中编码为 0xE4-0xE9 开头的三字节
|
||||
return c >= 0xE4 && c <= 0xE9;
|
||||
}
|
||||
|
||||
static size_t count_tokens_one_message(const dstalk_message_t& msg) {
|
||||
const char* text = msg.content;
|
||||
if (!text) return 4; // 只有 overhead
|
||||
|
||||
size_t ascii_chars = 0;
|
||||
size_t chinese_chars = 0;
|
||||
size_t other_chars = 0;
|
||||
|
||||
size_t i = 0;
|
||||
while (text[i] != '\0') {
|
||||
unsigned char c = static_cast<unsigned char>(text[i]);
|
||||
|
||||
if (cjk_is_ascii(c)) {
|
||||
ascii_chars++;
|
||||
i += 1;
|
||||
} else if (cjk_starts_cjk(c)) {
|
||||
chinese_chars++;
|
||||
i += 3;
|
||||
} else if (c >= 0xC0 && c < 0xE0) {
|
||||
other_chars++;
|
||||
i += 2;
|
||||
} else if (c >= 0xE0 && c < 0xF0) {
|
||||
other_chars++;
|
||||
i += 3;
|
||||
} else if (c >= 0xF0 && c < 0xF8) {
|
||||
other_chars++;
|
||||
i += 4;
|
||||
} else {
|
||||
other_chars++;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
size_t content_tokens = (ascii_chars / 4) + (chinese_chars / 2) + (other_chars / 3);
|
||||
return content_tokens + 4; // +4 条消息开销 (role + separators)
|
||||
}
|
||||
|
||||
static size_t count_tokens_all(const dstalk_message_t* msgs, int count) {
|
||||
size_t total = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
total += count_tokens_one_message(msgs[i]);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 内部 trim 逻辑
|
||||
// ============================================================
|
||||
|
||||
// 为 trim 操作将 C 消息数组复制到内部 struct
|
||||
struct TrimMessage {
|
||||
std::string role;
|
||||
std::string content;
|
||||
std::string tool_call_id;
|
||||
std::string tool_calls_json;
|
||||
};
|
||||
|
||||
static size_t count_tokens_trim(const TrimMessage& msg) {
|
||||
if (msg.content.empty()) return 4;
|
||||
const std::string& text = msg.content;
|
||||
size_t ascii_chars = 0, chinese_chars = 0, other_chars = 0;
|
||||
size_t i = 0;
|
||||
while (i < text.size()) {
|
||||
unsigned char c = static_cast<unsigned char>(text[i]);
|
||||
if (cjk_is_ascii(c)) { ascii_chars++; i += 1; }
|
||||
else if (cjk_starts_cjk(c)) { chinese_chars++; i += 3; }
|
||||
else if (c >= 0xC0 && c < 0xE0) { other_chars++; i += 2; }
|
||||
else if (c >= 0xE0 && c < 0xF0) { other_chars++; i += 3; }
|
||||
else if (c >= 0xF0 && c < 0xF8) { other_chars++; i += 4; }
|
||||
else { other_chars++; i += 1; }
|
||||
}
|
||||
return (ascii_chars / 4) + (chinese_chars / 2) + (other_chars / 3) + 4;
|
||||
}
|
||||
|
||||
static size_t count_tokens_trim_vec(const std::vector<TrimMessage>& msgs) {
|
||||
size_t total = 0;
|
||||
for (const auto& m : msgs) total += count_tokens_trim(m);
|
||||
return total;
|
||||
}
|
||||
|
||||
static int trim_impl(const dstalk_message_t* in, int in_count,
|
||||
dstalk_message_t** out, int* out_count,
|
||||
size_t max_tokens) {
|
||||
if (!in || in_count <= 0 || !out || !out_count) return -1;
|
||||
|
||||
// 将 C 数组转换为内部 vector
|
||||
std::vector<TrimMessage> messages;
|
||||
messages.reserve(in_count);
|
||||
for (int i = 0; i < in_count; ++i) {
|
||||
TrimMessage tm;
|
||||
if (in[i].role) tm.role = in[i].role;
|
||||
if (in[i].content) tm.content = in[i].content;
|
||||
if (in[i].tool_call_id) tm.tool_call_id = in[i].tool_call_id;
|
||||
if (in[i].tool_calls_json) tm.tool_calls_json = in[i].tool_calls_json;
|
||||
messages.push_back(std::move(tm));
|
||||
}
|
||||
|
||||
// 如果已在限制内,直接返回完整副本
|
||||
size_t current = count_tokens_trim_vec(messages);
|
||||
if (current <= max_tokens) {
|
||||
*out_count = in_count;
|
||||
*out = static_cast<dstalk_message_t*>(g_host->alloc(sizeof(dstalk_message_t) * in_count));
|
||||
if (!*out) return -1;
|
||||
for (int i = 0; i < in_count; ++i) {
|
||||
(*out)[i].role = messages[i].role.empty() ? nullptr : g_host->strdup(messages[i].role.c_str());
|
||||
(*out)[i].content = messages[i].content.empty() ? nullptr : g_host->strdup(messages[i].content.c_str());
|
||||
(*out)[i].tool_call_id = messages[i].tool_call_id.empty() ? nullptr : g_host->strdup(messages[i].tool_call_id.c_str());
|
||||
(*out)[i].tool_calls_json = messages[i].tool_calls_json.empty() ? nullptr : g_host->strdup(messages[i].tool_calls_json.c_str());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 分离 system 消息和非 system 消息
|
||||
std::vector<TrimMessage> system_msgs;
|
||||
std::vector<TrimMessage> non_system_msgs;
|
||||
for (const auto& msg : messages) {
|
||||
if (msg.role == "system") {
|
||||
system_msgs.push_back(msg);
|
||||
} else {
|
||||
non_system_msgs.push_back(msg);
|
||||
}
|
||||
}
|
||||
|
||||
size_t system_tokens = count_tokens_trim_vec(system_msgs);
|
||||
if (system_tokens > max_tokens) {
|
||||
std::fprintf(stderr, "[context] WARNING: system messages alone "
|
||||
"(%zu tokens) exceed max_context_tokens (%zu)\n",
|
||||
system_tokens, max_tokens);
|
||||
}
|
||||
|
||||
// 检查是否有单条消息超过限制
|
||||
for (const auto& msg : non_system_msgs) {
|
||||
size_t msg_tokens = count_tokens_trim(msg);
|
||||
if (msg_tokens > max_tokens) {
|
||||
std::fprintf(stderr, "[context] WARNING: single message "
|
||||
"(%s, %zu tokens) exceeds max_context_tokens (%zu). "
|
||||
"Returning empty list.\n",
|
||||
msg.role.c_str(), msg_tokens, max_tokens);
|
||||
*out = nullptr;
|
||||
*out_count = 0;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// 从最早的非 system 消息开始裁剪,确保 user/assistant 成对移除
|
||||
while (!non_system_msgs.empty()) {
|
||||
current = system_tokens + count_tokens_trim_vec(non_system_msgs);
|
||||
if (current <= max_tokens) break;
|
||||
|
||||
// 找第一个 "user" 消息
|
||||
auto user_it = non_system_msgs.begin();
|
||||
while (user_it != non_system_msgs.end() && user_it->role != "user") {
|
||||
++user_it;
|
||||
}
|
||||
if (user_it == non_system_msgs.end()) break;
|
||||
|
||||
// 找下一个 "assistant"
|
||||
auto assistant_it = user_it + 1;
|
||||
while (assistant_it != non_system_msgs.end() && assistant_it->role != "assistant") {
|
||||
++assistant_it;
|
||||
}
|
||||
|
||||
if (assistant_it == non_system_msgs.end()) {
|
||||
non_system_msgs.erase(user_it);
|
||||
} else {
|
||||
// 先删 assistant 再删 user 避免迭代器失效
|
||||
non_system_msgs.erase(assistant_it);
|
||||
user_it = non_system_msgs.begin();
|
||||
while (user_it != non_system_msgs.end() && user_it->role != "user") ++user_it;
|
||||
if (user_it != non_system_msgs.end()) non_system_msgs.erase(user_it);
|
||||
}
|
||||
}
|
||||
|
||||
// 组装结果
|
||||
std::vector<TrimMessage> result;
|
||||
result.reserve(system_msgs.size() + non_system_msgs.size());
|
||||
result.insert(result.end(), system_msgs.begin(), system_msgs.end());
|
||||
result.insert(result.end(), non_system_msgs.begin(), non_system_msgs.end());
|
||||
|
||||
int result_count = static_cast<int>(result.size());
|
||||
*out_count = result_count;
|
||||
*out = static_cast<dstalk_message_t*>(g_host->alloc(sizeof(dstalk_message_t) * result_count));
|
||||
if (!*out) return -1;
|
||||
|
||||
for (int i = 0; i < result_count; ++i) {
|
||||
(*out)[i].role = result[i].role.empty() ? nullptr : g_host->strdup(result[i].role.c_str());
|
||||
(*out)[i].content = result[i].content.empty() ? nullptr : g_host->strdup(result[i].content.c_str());
|
||||
(*out)[i].tool_call_id = result[i].tool_call_id.empty() ? nullptr : g_host->strdup(result[i].tool_call_id.c_str());
|
||||
(*out)[i].tool_calls_json = result[i].tool_calls_json.empty() ? nullptr : g_host->strdup(result[i].tool_calls_json.c_str());
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Context 服务 vtable 实现
|
||||
// ============================================================
|
||||
|
||||
static size_t context_count_tokens(const dstalk_message_t* msgs, int count) {
|
||||
if (!msgs || count <= 0) return 0;
|
||||
return count_tokens_all(msgs, count);
|
||||
}
|
||||
|
||||
static int context_trim(const dstalk_message_t* in, int in_count,
|
||||
dstalk_message_t** out, int* out_count,
|
||||
size_t max_tokens) {
|
||||
return trim_impl(in, in_count, out, out_count, max_tokens);
|
||||
}
|
||||
|
||||
static void context_set_max_tokens(size_t max) {
|
||||
g_max_tokens = max;
|
||||
}
|
||||
|
||||
static dstalk_context_service_t g_context_service = {
|
||||
context_count_tokens,
|
||||
context_trim,
|
||||
context_set_max_tokens
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// 插件生命周期
|
||||
// ============================================================
|
||||
|
||||
static int on_init(const dstalk_host_api_t* host) {
|
||||
g_host = host;
|
||||
|
||||
// 查询依赖服务: session
|
||||
void* raw = host->query_service("session", 1);
|
||||
if (!raw) {
|
||||
host->log(DSTALK_LOG_ERROR, "[plugin-context] required service 'session' not found");
|
||||
return -1;
|
||||
}
|
||||
g_session = static_cast<const dstalk_session_service_t*>(raw);
|
||||
|
||||
return host->register_service("context", 1, &g_context_service);
|
||||
}
|
||||
|
||||
static void on_shutdown() {
|
||||
g_session = nullptr;
|
||||
g_host = nullptr;
|
||||
}
|
||||
|
||||
static dstalk_plugin_info_t g_info = {
|
||||
"context",
|
||||
"1.0.0",
|
||||
"Context management plugin with token counting and trim support",
|
||||
DSTALK_API_VERSION,
|
||||
{"session", nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr},
|
||||
on_init,
|
||||
on_shutdown,
|
||||
nullptr
|
||||
};
|
||||
|
||||
extern "C" DSTALK_PLUGIN_EXPORT dstalk_plugin_info_t* dstalk_plugin_init(void) {
|
||||
return &g_info;
|
||||
}
|
||||
Reference in New Issue
Block a user