Files
dstalk/dstalk-cli/src/main.cpp
XiuChengWu 5766938524
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled
Wave 5+6: plugin ABI hardening, build modernization, ABI/security docs
Wave 5 (9 parallel agents):
- W1.1 atomic diag callback + DLL handle release on shutdown (lin)
- W2.1 unify cross-DLL heap discipline (host->alloc/free/strdup) (chen)
- W2.2 secure_zero api_key on shutdown for deepseek/anthropic (cao)
- W3 CMake modernization: target-based cxx_std_20, dstalk_boost_config
  INTERFACE lib, root-level RUNTIME_OUTPUT_DIRECTORY (hu)
- W4 GitHub Actions CI with dynamic Linux/Windows matrix (ma)
- W5.1 SSE buffer_body to cut peak memory ~67% on 32K streams (zhou)
- W6.1 LSP JSON-RPC frame parser hardened against header reordering (sun)
- W7 smoke test: copy plugin DLLs post-build + Boost.JSON src.hpp fix
  for full 9-plugin load coverage (wang)
- W8.1 README slimmed 398->92, Diataxis docs/ skeleton (deng)

Wave 6 (6 parallel agents):
- W9.1 docs/explanation: architecture + plugin-lifecycle (deng)
- W9.3 log credential leak audit (0 vulns, audit trail in
  docs/explanation/security-logging.md) (cao)
- W9.4 docs/reference/plugin-abi.md - 7-point ABI contract (lin)
- W9.6 CLI /history command + status integration (zhao)
- W9.8 plugin_loader fault tolerance: per-plugin failure no longer
  aborts dstalk_init (huang)
- W9.10 host_api unit tests: tests/host_api_test.cpp, 8 cases (liu)

CEO oversight (preexisting bugs fixed during Wave 5 verification):
- lsp_plugin.cpp:449 forward decl mismatch (int vs void)
- tools_plugin.cpp:109 missing forward decl

Multi-agent collaboration framework:
- agents/WORKFLOW.md: 6-stage protocol, two-tier governance,
  prompt template, technical constraints registry

Build: cmake --build 0 error / 0 warning. Tests: 2/2 100% pass.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-27 05:39:10 +08:00

506 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// dstalk-cli — 命令行前端 (使用插件化架构)
// ============================================================================
// 通过 dstalk_host.h API 初始化核心,然后查询插件服务 vtable 调用功能。
// ============================================================================
#include <algorithm>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <string>
#include <system_error>
#include <vector>
#ifdef _WIN32
#include <windows.h>
#include <io.h>
#else
#include <signal.h>
#include <termios.h>
#include <unistd.h>
#endif
#include "dstalk/dstalk_host.h"
// ---- ANSI 简写 ----
#define CLR_RESET "\033[0m"
#define CLR_CYAN "\033[36m"
#define CLR_YELLOW "\033[33m"
#define CLR_GREEN "\033[32m"
#define CLR_RED "\033[31m"
#define CLR_DIM "\033[2m"
#define CLR_BOLD "\033[1m"
// ---- 退出码 ----
#define EXIT_OK 0
#define EXIT_INIT_FAIL 1
#define EXIT_AI_ERROR 2
#define EXIT_SVC_UNAVAIL 3
// ---- 服务 vtable 指针 ----
static const dstalk_ai_service_t* g_ai = nullptr;
static const dstalk_session_service_t* g_session = nullptr;
static const dstalk_file_io_service_t* g_file_io = nullptr;
// ---- 运行时状态 ----
static std::string g_current_model;
static std::atomic<bool> g_quit_requested{false};
// ---- Ctrl+C 信号处理 ----
#ifdef _WIN32
static BOOL WINAPI on_console_event(DWORD event)
{
if (event == CTRL_C_EVENT || event == CTRL_BREAK_EVENT) {
g_quit_requested = true;
return TRUE;
}
return FALSE;
}
#else
static void on_signal(int /*sig*/)
{
g_quit_requested = true;
}
#endif
// ---- 工具函数 ----
static void print_banner()
{
std::printf("%sdstalk v0.1.0%s | %sdstalk AI%s | "
"%s/help%s 查看帮助 | %s/quit%s 退出\n",
CLR_CYAN CLR_BOLD, CLR_RESET,
CLR_GREEN, CLR_RESET,
CLR_DIM, CLR_RESET,
CLR_DIM, CLR_RESET);
}
static void print_help()
{
std::printf("\n%s命令列表:%s\n", CLR_BOLD, CLR_RESET);
std::printf(" %s/help%s 显示此帮助\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/quit%s 退出程序\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/clear%s 清空当前会话上下文\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/context%s 显示当前 Token 数和消息条数\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/status%s 显示当前运行状态(脱敏)\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/model <name>%s 切换模型\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/file list [path]%s 列出目录内容\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/file show <path>%s 查看文件内容\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/file read <path>%s 读取文件内容\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/file write <p> <c>%s 写入文件\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/save <path>%s 保存会话\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/load <path>%s 恢复会话\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/history [N]%s 查看会话历史默认全部可指定最近N条\n", CLR_YELLOW, CLR_RESET);
std::printf("\n直接输入问题即可与 AI 对话。\n\n");
}
static void print_file(const char* path)
{
while (*path == ' ') path++;
if (!g_file_io) {
std::printf(CLR_RED "[ERROR] file_io 服务不可用\n" CLR_RESET);
return;
}
char* content = nullptr;
if (g_file_io->read(path, &content) == 0 && content) {
std::printf("%s--- %s ---%s\n", CLR_DIM, path, CLR_RESET);
std::printf("%s\n", content);
std::printf(CLR_DIM "--- EOF ---\n" CLR_RESET);
dstalk_free(content);
} else {
std::printf(CLR_RED "[ERROR] 无法读取: %s\n" CLR_RESET, path);
}
}
static void list_files(const char* path)
{
while (*path == ' ') path++;
std::filesystem::path dir = *path ? std::filesystem::path(path) : std::filesystem::current_path();
std::error_code ec;
if (!std::filesystem::exists(dir, ec) || !std::filesystem::is_directory(dir, ec)) {
std::printf(CLR_RED "[ERROR] 不是有效目录: %s\n" CLR_RESET, dir.string().c_str());
return;
}
std::vector<std::filesystem::directory_entry> entries;
for (const auto& entry : std::filesystem::directory_iterator(dir, ec)) {
if (ec) break;
entries.push_back(entry);
}
if (ec) {
std::printf(CLR_RED "[ERROR] 无法列出目录: %s\n" CLR_RESET, dir.string().c_str());
return;
}
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
return a.path().filename().string() < b.path().filename().string();
});
std::printf("%s--- %s ---%s\n", CLR_DIM, dir.string().c_str(), CLR_RESET);
for (const auto& entry : entries) {
std::error_code status_ec;
const bool is_dir = entry.is_directory(status_ec);
std::printf(" %s%s%s\n", is_dir ? CLR_CYAN : "", entry.path().filename().string().c_str(), is_dir ? "/" CLR_RESET : "");
}
}
static void handle_command(const char* line)
{
if (!line || line[0] != '/') return;
// /quit —— 设置退出标志,让控制流自然回到 main 末尾
if (std::strcmp(line, "/quit") == 0 || std::strcmp(line, "/q") == 0) {
g_quit_requested = true;
std::printf("再见!\n");
return;
}
// /help
if (std::strcmp(line, "/help") == 0 || std::strcmp(line, "/h") == 0) {
print_help();
return;
}
// /clear
if (std::strcmp(line, "/clear") == 0) {
if (g_session) g_session->clear();
std::printf(CLR_GREEN "[OK] 会话已清空\n" CLR_RESET);
return;
}
// /context
if (std::strcmp(line, "/context") == 0) {
if (g_session) {
int count = 0;
g_session->history(&count);
int tokens = g_session->token_count();
std::printf(CLR_DIM "消息条数: " CLR_RESET "%d | "
CLR_DIM "Token 估算: " CLR_RESET "%d\n",
count, tokens);
}
return;
}
// /status —— 脱敏显示当前运行状态
if (std::strcmp(line, "/status") == 0) {
const char* provider = dstalk_config_get("ai.provider");
if (!provider) provider = "ai.deepseek";
const char* base_url = dstalk_config_get("api.base_url");
if (!base_url) base_url = "https://api.deepseek.com/v1";
const char* api_key = dstalk_config_get("api.api_key");
std::printf(" 模型: %s\n", g_current_model.empty() ? "(未设置)" : g_current_model.c_str());
std::printf(" base_url: %s\n", base_url ? base_url : "(未设置)");
std::printf(" api_key: %s\n", (api_key && api_key[0]) ? "已设置" : "未设置");
std::printf(" provider: %s\n", provider);
std::printf(" AI 服务: %s\n", g_ai ? "就绪" : "不可用");
std::printf(" Session 服务: %s\n", g_session ? "就绪" : "不可用");
int hc = 0;
if (g_session) g_session->history(&hc);
std::printf(" history count: %d\n", hc);
std::printf(" File IO 服务: %s\n", g_file_io ? "就绪" : "不可用");
const dstalk_tools_service_t* tools = static_cast<const dstalk_tools_service_t*>(
dstalk_service_query("tools", 1));
std::printf(" Tools 服务: %s\n", tools ? "就绪" : "不可用");
return;
}
// /model <name>
if (std::strncmp(line, "/model ", 7) == 0) {
const char* model = line + 7;
while (*model == ' ') model++;
if (*model == '\0') {
std::printf(CLR_RED "[ERROR] /model 需要模型名\n" CLR_RESET);
return;
}
if (g_ai) {
g_ai->configure(nullptr, nullptr, nullptr, model, 0, 0.0);
g_current_model = model;
std::printf(CLR_GREEN "[OK] 模型已切换: %s\n" CLR_RESET, model);
} else {
std::printf(CLR_RED "[ERROR] AI 服务不可用\n" CLR_RESET);
}
return;
}
// /file list [path]
if (std::strcmp(line, "/file list") == 0 || std::strncmp(line, "/file list ", 11) == 0) {
const char* path = line + 10;
list_files(path);
return;
}
// /file show <path>
if (std::strncmp(line, "/file show ", 11) == 0) {
print_file(line + 11);
return;
}
// /file read <path>
if (std::strncmp(line, "/file read ", 11) == 0) {
print_file(line + 11);
return;
}
// /file write <path> <content...>
if (std::strncmp(line, "/file write ", 12) == 0) {
const char* rest = line + 12;
while (*rest == ' ') rest++;
const char* space = std::strchr(rest, ' ');
if (!space) {
std::printf(CLR_RED "[ERROR] 用法: /file write <path> <content>\n" CLR_RESET);
return;
}
std::string path(rest, space - rest);
const char* content = space + 1;
while (*content == ' ') content++;
if (g_file_io && g_file_io->write(path.c_str(), content) == 0) {
std::printf(CLR_GREEN "[OK] 已写入: %s\n" CLR_RESET, path.c_str());
} else {
std::printf(CLR_RED "[ERROR] 写入失败: %s\n" CLR_RESET, path.c_str());
}
return;
}
// /history [N]
if (std::strcmp(line, "/history") == 0 || std::strncmp(line, "/history ", 9) == 0) {
if (!g_session) {
std::printf(CLR_RED "[ERROR] session service unavailable\n" CLR_RESET);
return;
}
int count = 0;
const dstalk_message_t* history = g_session->history(&count);
if (count == 0 || !history) {
std::printf(CLR_DIM "(history is empty)\n" CLR_RESET);
return;
}
int limit = count;
const char* arg = line + 8;
while (*arg == ' ') arg++;
if (*arg != '\0') {
char* end = nullptr;
long n = std::strtol(arg, &end, 10);
if (*end != '\0' || n <= 0) {
std::printf(CLR_RED "[ERROR] /history N: N must be a positive integer\n" CLR_RESET);
return;
}
limit = (n < count) ? static_cast<int>(n) : count;
}
int start = count - limit;
for (int i = start; i < count; i++) {
std::printf("[%s] %s\n",
history[i].role ? history[i].role : "?",
history[i].content ? history[i].content : "");
}
return;
}
// /save <path>
if (std::strncmp(line, "/save ", 6) == 0) {
const char* path = line + 6;
while (*path == ' ') path++;
if (g_session && g_session->save(path) == 0) {
std::printf(CLR_GREEN "[OK] 会话已保存: %s\n" CLR_RESET, path);
} else {
std::printf(CLR_RED "[ERROR] 保存失败: %s\n" CLR_RESET, path);
}
return;
}
// /load <path>
if (std::strncmp(line, "/load ", 6) == 0) {
const char* path = line + 6;
while (*path == ' ') path++;
if (g_session && g_session->load(path) == 0) {
std::printf(CLR_GREEN "[OK] 会话已恢复: %s\n" CLR_RESET, path);
} else {
std::printf(CLR_RED "[ERROR] 恢复失败: %s\n" CLR_RESET, path);
}
return;
}
std::printf(CLR_RED "未知命令: %s (输入 /help 查看帮助)\n" CLR_RESET, line);
}
// ---- 流式回调 ----
static int on_stream_token(const char* token, void* userdata)
{
bool* first = static_cast<bool*>(userdata);
if (*first) {
std::printf(CLR_GREEN);
*first = false;
}
std::printf("%s", token);
std::fflush(stdout);
return 0;
}
// ---- 主程序 ----
int main(int argc, char* argv[])
{
// Windows: 启用 ANSI 转义码支持
#ifdef _WIN32
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD mode = 0;
GetConsoleMode(hOut, &mode);
SetConsoleMode(hOut, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
#endif
// ---- C1: batch 模式检测 ----
bool batch_mode = false;
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--batch") == 0) {
batch_mode = true;
break;
}
}
#ifdef _WIN32
if (!batch_mode && _isatty(_fileno(stdin)) == 0) batch_mode = true;
#else
if (!batch_mode && isatty(fileno(stdin)) == 0) batch_mode = true;
#endif
// ---- B1: 安装 Ctrl+C 处理 ----
#ifdef _WIN32
SetConsoleCtrlHandler(on_console_event, TRUE);
#else
signal(SIGINT, on_signal);
#endif
// 查找配置文件
const char* config_path = nullptr;
if (argc >= 2) {
// 跳过 --batch 标志
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--batch") != 0) {
config_path = argv[i];
break;
}
}
}
if (!config_path) {
const char* default_configs[] = {"config.toml", nullptr};
for (int i = 0; default_configs[i]; i++) {
FILE* f = nullptr;
#ifdef _WIN32
fopen_s(&f, default_configs[i], "r");
#else
f = fopen(default_configs[i], "r");
#endif
if (f) {
fclose(f);
config_path = default_configs[i];
break;
}
}
}
// 初始化主机(加载配置 + 自动扫描 plugins/ 目录加载插件)
if (dstalk_init(config_path) != 0) {
std::fprintf(stderr, CLR_RED "[dstalk] 初始化失败\n" CLR_RESET);
return EXIT_INIT_FAIL;
}
// 查询插件服务
const char* ai_provider = dstalk_config_get("ai.provider");
if (!ai_provider) ai_provider = "ai.deepseek";
g_ai = static_cast<const dstalk_ai_service_t*>(dstalk_service_query(ai_provider, 1));
g_session = static_cast<const dstalk_session_service_t*>(dstalk_service_query("session", 1));
g_file_io = static_cast<const dstalk_file_io_service_t*>(dstalk_service_query("file_io", 1));
if (!g_ai) {
std::fprintf(stderr, CLR_RED "[dstalk] AI 服务未找到(请检查插件目录)\n" CLR_RESET);
}
if (!g_session) {
std::fprintf(stderr, CLR_RED "[dstalk] Session 服务未找到\n" CLR_RESET);
}
// 自动从配置加载 AI 设置
if (g_ai) {
const char* base_url = dstalk_config_get("api.base_url");
const char* api_key = dstalk_config_get("api.api_key");
const char* model = dstalk_config_get("api.model");
if (!base_url) base_url = "https://api.deepseek.com/v1";
if (!model) model = "deepseek-v4-pro";
g_ai->configure(ai_provider, base_url, api_key ? api_key : "", model, 4096, 0.7);
g_current_model = model; // A1: 记录当前模型名
}
if (!batch_mode) {
std::printf("\n");
print_banner();
std::printf("\n");
}
char buffer[8192];
while (true) {
// B1: 检查退出标志
if (g_quit_requested) break;
// A1: 提示符带模型名batch 模式不打印)
if (!batch_mode) {
std::printf(CLR_CYAN "[%s] " CLR_RESET CLR_YELLOW "> " CLR_RESET,
g_current_model.empty() ? "?" : g_current_model.c_str());
std::fflush(stdout);
}
if (!std::fgets(buffer, sizeof(buffer), stdin)) break;
// C3: fgets 截断检测
if (!std::strchr(buffer, '\n') && !feof(stdin)) {
std::fprintf(stderr, CLR_RED "[ERROR] 输入超过 8KB已截断。建议用文件方式dstalk --batch < file.txt\n" CLR_RESET);
int c;
while ((c = std::fgetc(stdin)) != '\n' && c != EOF) {}
}
// 去除末尾换行
size_t len = std::strlen(buffer);
while (len > 0 && (buffer[len-1] == '\n' || buffer[len-1] == '\r')) {
buffer[--len] = '\0';
}
if (len == 0) continue;
// 命令处理
if (buffer[0] == '/') {
handle_command(buffer);
continue;
}
// AI 对话(通过插件服务 vtable
if (!g_ai || !g_session) {
std::printf(CLR_RED "[ERROR] AI 或 Session 服务不可用\n" CLR_RESET);
continue;
}
// 获取会话历史
int history_count = 0;
const dstalk_message_t* history = g_session->history(&history_count);
bool first = true;
dstalk_chat_result_t result = g_ai->chat_stream(
history, history_count, buffer, on_stream_token, &first);
if (result.ok) {
std::printf(CLR_RESET "\n\n");
// 将用户消息和 AI 回复添加到会话
dstalk_message_t user_msg = {"user", buffer, nullptr, nullptr};
g_session->add(&user_msg);
dstalk_message_t ai_msg = {"assistant", result.content, nullptr, result.tool_calls_json};
g_session->add(&ai_msg);
} else {
// A3: error 路径下需 NULL 保护;当前只取 result.errorcontent 未涉及
std::printf(CLR_RESET "\n" CLR_RED "[ERROR] AI 调用失败: %s\n" CLR_RESET,
result.error ? result.error : "unknown error");
}
g_ai->free_result(&result);
}
// B2: 单一退出点dstalk_shutdown 只在此调用
dstalk_shutdown();
return EXIT_OK;
}