feat: add OpenAI-compatible AI provider plugin with SSE streaming support

- Implemented the OpenAI-compatible AI provider plugin, including configuration, chat, and chat_stream functionalities.
- Added support for SSE streaming and tool calls.
- Integrated Boost.JSON for JSON handling.
- Created CMake configuration for the plugin.
- Added error handling and logging throughout the plugin.
This commit is contained in:
2026-05-31 05:37:04 +08:00
parent f6cb51b40a
commit ba7382db2a
61 changed files with 163 additions and 147 deletions

17
dstalk_cli/CMakeLists.txt Normal file
View File

@@ -0,0 +1,17 @@
# ============================================================
# dstalk_cli — 命令行前端 (ANSI 转义码)
# ============================================================
add_executable(dstalk_cli
src/main.cpp
)
set_target_properties(dstalk_cli PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
find_package(Boost REQUIRED CONFIG)
target_link_libraries(dstalk_cli
PRIVATE dstalk boost::boost dstalk_boost_config
)

764
dstalk_cli/src/main.cpp Normal file
View File

@@ -0,0 +1,764 @@
/*
* @file main.cpp
* @brief CLI frontend for dstalk: ANSI terminal UI, command parsing, streaming chat, tool calling loop, batch/pipe mode.
* dstalk 命令行前端ANSI 终端界面、命令解析、流式对话、工具调用循环、批处理/管道模式。
* Copyright (c) 2026 dstalk contributors. GPLv3.
*/
#include <algorithm>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <string>
#include <system_error>
#include <vector>
#include <boost/json.hpp>
#include <boost/json/src.hpp>
#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 简写 / ANSI shorthand macros ----
#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"
// ---- 退出码 / Exit codes ----
// 0=正常退出 1=用户中断(SIGINT/Ctrl+C) 2=致命错误 3=配置错误
// 0=normal 1=user interrupt (SIGINT/Ctrl+C) 2=fatal error 3=config error
#define EXIT_OK 0
#define EXIT_INTERRUPT 1
#define EXIT_FATAL 2
#define EXIT_CONFIG 3
// ---- 服务 vtable 指针 / Service vtable pointers ----
// Global pointers to plugin service vtables, queried from the host on startup.
// 插件服务 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 const dstalk_tools_service_t* g_tools = nullptr;
// ---- 运行时状态 / Runtime state ----
// g_current_model tracks the active model name for display in the prompt.
// g_quit_requested signals the main loop to exit (set by /quit or Ctrl+C).
// g_quit_via_signal distinguishes SIGINT-triggered exit from normal /quit.
// g_current_model 记录当前模型名称,用于提示符显示。
// g_quit_requested 通知主循环退出(由 /quit 或 Ctrl+C 设置)。
// g_quit_via_signal 区分 SIGINT 触发的退出和正常的 /quit 退出。
static std::string g_current_model;
static std::atomic<bool> g_quit_requested{false};
static std::atomic<bool> g_quit_via_signal{false};
// ---- Ctrl+C 信号处理 / Ctrl+C signal handlers ----
// Windows console event handler (CTRL_C_EVENT / CTRL_BREAK_EVENT).
// Windows 控制台事件处理CTRL_C_EVENT / CTRL_BREAK_EVENT
#ifdef _WIN32
static BOOL WINAPI on_console_event(DWORD event)
{
if (event == CTRL_C_EVENT || event == CTRL_BREAK_EVENT) {
g_quit_via_signal = true;
g_quit_requested = true;
return TRUE;
}
return FALSE;
}
// Unix signal handler (SIGINT).
// Unix 信号处理SIGINT
#else
static void on_signal(int /*sig*/)
{
g_quit_via_signal = true;
g_quit_requested = true;
}
#endif
// ---- 工具函数 / Utility functions ----
// 打印启动横幅 / Print the dstalk CLI banner with version, AI indicator, and quick command hints.
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);
}
// 打印帮助文本 / Print the full help text listing all available slash commands.
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");
}
// 通过 file_io 服务读取并显示文件内容 / Read and display the contents of the file at the given path via the file_io service.
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);
}
}
// 列出目录内容,按文件名排序,子目录以青色高亮 / List directory entries sorted by filename, highlighting subdirectories in cyan.
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 : "");
}
}
// 分发斜杠命令 / Dispatch a slash-command string: /quit, /help, /clear, /context, /status, /model, /file, /history, /save, /load.
static void handle_command(const char* line)
{
if (!line || line[0] != '/') return;
// /quit —— 设置退出标志,让控制流自然回到 main 末尾 / Set quit flag to let control flow naturally return to end of main
if (std::strcmp(line, "/quit") == 0 || std::strcmp(line, "/q") == 0) {
g_quit_requested = true;
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);
} else {
std::fprintf(stderr, CLR_RED "[ERROR] session service not available\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);
} else {
std::fprintf(stderr, CLR_RED "[ERROR] No active session\n" CLR_RESET);
}
return;
}
// /status —— 脱敏显示当前运行状态 / Display current runtime status (desensitized)
if (std::strcmp(line, "/status") == 0) {
const char* provider = dstalk_config_get("ai.provider");
if (!provider) provider = "ai.openai";
const char* base_url = dstalk_config_get("api.base_url");
if (!base_url) base_url = "https://api.openai.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);
if (g_ai && !g_current_model.empty()) {
std::printf(" 连接状态: 已连接 (%s, %s)\n", provider, g_current_model.c_str());
} else if (g_ai) {
std::printf(" 连接状态: 插件已加载,模型未配置\n");
} else {
std::printf(" 连接状态: 未连接\n");
}
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 <subcommand> [args...] —— 统一入口,避免 strncmp 空格匹配遗漏 / Unified entry to avoid strncmp space matching issues
if (std::strncmp(line, "/file", 5) == 0) {
const char* rest = line + 5;
while (*rest == ' ') rest++;
const char* sub_end = rest;
while (*sub_end != ' ' && *sub_end != '\0') sub_end++;
size_t sub_len = sub_end - rest;
if (sub_len == 0) {
std::printf(CLR_RED "[ERROR] 用法: /file <list|show|read|write> ...\n" CLR_RESET);
return;
}
const char* args = sub_end;
while (*args == ' ') args++;
// /file list [path]
if (sub_len == 4 && std::strncmp(rest, "list", 4) == 0) {
list_files(args);
return;
}
// /file show <path> | /file read <path>
if ((sub_len == 4 && std::strncmp(rest, "show", 4) == 0) ||
(sub_len == 4 && std::strncmp(rest, "read", 4) == 0)) {
while (*args == ' ') args++;
if (*args == '\0') {
std::printf(CLR_RED "[ERROR] 用法: /file %.*s <path>\n" CLR_RESET,
static_cast<int>(sub_len), rest);
return;
}
print_file(args);
return;
}
// /file write <path> <content>
if (sub_len == 5 && std::strncmp(rest, "write", 5) == 0) {
while (*args == ' ') args++;
if (*args == '\0') {
std::printf(CLR_RED "[ERROR] 用法: /file write <path> <content>\n" CLR_RESET);
return;
}
const char* path_end = args;
while (*path_end != ' ' && *path_end != '\0') path_end++;
std::string path(args, path_end - args);
const char* content = path_end;
while (*content == ' ') content++;
if (*content == '\0') {
std::printf(CLR_RED "[ERROR] 用法: /file write <path> <content>\n" CLR_RESET);
return;
}
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;
}
std::printf(CLR_RED "[ERROR] 未知 /file 子命令: %.*s (可用: list, show, read, write)\n" CLR_RESET,
static_cast<int>(sub_len), rest);
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);
}
// ---- 流式回调 / Streaming callback ----
// 流式输出回调:每收到一个 token 打印到 stdout 并刷新 / Callback invoked for each token during streaming chat; prints the token to stdout and flushes.
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;
}
// ---- 主程序 / Main entry point ----
// 入口:初始化 dstalk host查询插件服务处理 batch/pipe/交互模式。
// Entry point: initializes dstalk host, queries plugin services, handles batch/pipe/interactive modes.
int main(int argc, char* argv[])
{
// Windows: 启用 ANSI 转义码支持 / Windows: enable ANSI escape code support
#ifdef _WIN32
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD mode = 0;
GetConsoleMode(hOut, &mode);
SetConsoleMode(hOut, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
#endif
// ---- C1: batch/pipe 模式检测 / batch/pipe mode detection ----
#ifdef _WIN32
bool pipe_mode = (_isatty(_fileno(stdin)) == 0);
#else
bool pipe_mode = (isatty(fileno(stdin)) == 0);
#endif
bool batch_mode = false;
const char* prompt_arg = nullptr;
if (!pipe_mode) {
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--batch") == 0) {
batch_mode = true;
} else if (std::strcmp(argv[i], "--prompt") == 0) {
batch_mode = true;
if (i + 1 < argc && argv[i+1][0] != '-') {
prompt_arg = argv[++i];
} else if (i + 1 < argc && std::strcmp(argv[i+1], "-") == 0) {
++i;
prompt_arg = "-"; // stdin sentinel
} else {
prompt_arg = "-"; // --prompt without value → read stdin
}
}
}
}
if (pipe_mode) batch_mode = true;
// ---- B1: 安装 Ctrl+C 处理 / Install Ctrl+C handlers ----
#ifdef _WIN32
SetConsoleCtrlHandler(on_console_event, TRUE);
#else
signal(SIGINT, on_signal);
#endif
// 查找配置文件 / Locate config file
const char* config_path = nullptr;
if (argc >= 2) {
// 跳过 --batch / --prompt 标志 / Skip --batch / --prompt flags
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--batch") != 0 && std::strcmp(argv[i], "--prompt") != 0) {
config_path = argv[i];
break;
}
if (std::strcmp(argv[i], "--prompt") == 0 && i + 1 < argc) ++i;
}
}
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_base/middle/upper 目录加载插件) / Init host: load config + auto-scan plugins_base/middle/upper directories
if (dstalk_init(config_path) != 0) {
std::fprintf(stderr, CLR_RED "[dstalk] 初始化失败\n" CLR_RESET);
return EXIT_CONFIG;
}
// 查询插件服务 / Query plugin services
const char* ai_provider = dstalk_config_get("ai.provider");
if (!ai_provider) ai_provider = "ai.openai";
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));
g_tools = static_cast<const dstalk_tools_service_t*>(dstalk_service_query("tools", 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 设置 / Auto-load AI settings from config
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.openai.com/v1";
if (!model) model = "gpt-4o";
g_ai->configure(ai_provider, base_url, api_key ? api_key : "", model, 4096, 0.7);
g_current_model = model; // A1: 记录当前模型名 / Record current model name
}
if (!batch_mode) {
std::printf("\n");
print_banner();
std::printf("\n");
}
// ---- B3: 管道输入模式 (非交互) / Pipe input mode (non-interactive) ----
if (pipe_mode) {
std::string input;
char buf[4096];
while (std::fgets(buf, sizeof(buf), stdin)) {
input += buf;
}
if (input.empty()) {
std::fprintf(stderr, "empty prompt\n");
dstalk_shutdown();
return EXIT_FATAL;
}
if (!g_ai || !g_session) {
std::fprintf(stderr, CLR_RED "[ERROR] AI or session service unavailable\n" CLR_RESET);
dstalk_shutdown();
return EXIT_CONFIG;
}
int history_count = 0;
const dstalk_message_t* history = g_session->history(&history_count);
dstalk_chat_result_t result = g_ai->chat(history, history_count, input.c_str(), nullptr);
if (result.ok) {
std::printf("%s\n", result.content ? result.content : "");
g_ai->free_result(&result);
dstalk_shutdown();
return EXIT_OK;
} else {
std::fprintf(stderr, CLR_RED "[ERROR] AI error: %s\n" CLR_RESET,
result.error ? result.error : "unknown");
g_ai->free_result(&result);
dstalk_shutdown();
return EXIT_FATAL;
}
}
// ---- --prompt 批处理模式 (非交互) / --prompt batch mode (non-interactive) ----
if (prompt_arg) {
std::string prompt_text;
if (std::strcmp(prompt_arg, "-") == 0) {
// --prompt - or --prompt (no arg): read prompt from stdin / --prompt - 或 --prompt无参数从 stdin 读取提示
char buf[4096];
while (std::fgets(buf, sizeof(buf), stdin)) {
prompt_text += buf;
}
if (prompt_text.empty()) {
std::fprintf(stderr, "empty prompt\n");
dstalk_shutdown();
return EXIT_FATAL;
}
} else {
if (prompt_arg[0] == '\0') {
std::fprintf(stderr, "empty prompt\n");
dstalk_shutdown();
return EXIT_FATAL;
}
prompt_text = prompt_arg;
}
if (!g_ai || !g_session) {
std::fprintf(stderr, CLR_RED "[ERROR] AI or session service unavailable\n" CLR_RESET);
dstalk_shutdown();
return EXIT_CONFIG;
}
int history_count = 0;
const dstalk_message_t* history = g_session->history(&history_count);
dstalk_chat_result_t result = g_ai->chat(history, history_count, prompt_text.c_str(), nullptr);
if (result.ok) {
std::printf("%s\n", result.content ? result.content : "");
g_ai->free_result(&result);
dstalk_shutdown();
return EXIT_OK;
} else {
std::fprintf(stderr, CLR_RED "[ERROR] AI error: %s\n" CLR_RESET,
result.error ? result.error : "unknown");
g_ai->free_result(&result);
dstalk_shutdown();
return EXIT_FATAL;
}
}
char buffer[8192];
while (true) {
// B1: 检查退出标志 / Check quit flag
if (g_quit_requested) {
std::printf("再见!\n");
break;
}
// A1: 提示符带模型名batch 模式不打印) / Prompt shows model name (not printed in batch mode)
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 截断检测 / fgets truncation detection
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) {}
}
// 去除末尾换行 / Strip trailing newline
size_t len = std::strlen(buffer);
while (len > 0 && (buffer[len-1] == '\n' || buffer[len-1] == '\r')) {
buffer[--len] = '\0';
}
if (len == 0) continue;
// 命令处理 / Command dispatch
if (buffer[0] == '/') {
handle_command(buffer);
continue;
}
// AI 对话(通过插件服务 vtable / AI chat (via plugin service vtable)
if (!g_ai || !g_session) {
std::printf(CLR_RED "[ERROR] AI 或 Session 服务不可用\n" CLR_RESET);
continue;
}
// 获取会话历史 / Get session history
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 回复添加到会话 / Add user message and AI reply to session
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);
// W20.1: Tool Calling 闭环 / Tool calling closed loop
// 若 AI 返回了 tool_calls自动执行工具并将结果追加到 history再调 AI / If AI returns tool_calls, auto-execute tools, append results to history, then call AI again
bool has_tool_calls = (result.tool_calls_json && result.tool_calls_json[0] != '\0');
const int MAX_TOOL_ROUNDS = 5;
int tool_round = 0;
while (has_tool_calls && g_tools && tool_round < MAX_TOOL_ROUNDS) {
tool_round++;
has_tool_calls = false;
// 保存 tool_calls_jsonfree_result 前必须拷贝) / Save tool_calls_json (must copy before free_result)
std::string tc_json(result.tool_calls_json);
// 解析 [{"id":"...", "function":{"name":"...", "arguments":"..."}}] / Parse tool calls JSON array
boost::system::error_code ec;
auto tc_val = boost::json::parse(tc_json, ec);
if (ec.failed() || !tc_val.is_array()) break;
const auto& tc_array = tc_val.as_array();
if (tc_array.empty()) break; // 空数组 → 终止 / empty array → stop
bool any_executed = false;
for (const auto& tc : tc_array) {
if (!tc.is_object()) continue;
const auto& obj = tc.as_object();
const auto* func_j = obj.if_contains("function");
if (!func_j || !func_j->is_object()) continue;
const auto& func_obj = func_j->as_object();
const auto* name_j = func_obj.if_contains("name");
const auto* args_j = func_obj.if_contains("arguments");
if (!name_j || !name_j->is_string()) continue;
std::string tool_name = boost::json::value_to<std::string>(*name_j);
std::string tool_args = (args_j && args_j->is_string())
? boost::json::value_to<std::string>(*args_j) : "{}";
const auto* id_j = obj.if_contains("id");
std::string call_id = (id_j && id_j->is_string())
? boost::json::value_to<std::string>(*id_j) : "";
// 执行工具 / Execute tool
std::printf(CLR_DIM "[工具调用] %s...\n" CLR_RESET, tool_name.c_str());
char* exec_result = g_tools->execute(tool_name.c_str(), tool_args.c_str());
if (exec_result) {
std::printf(CLR_DIM "[工具结果] ok\n" CLR_RESET);
dstalk_message_t tool_msg = {
"tool",
exec_result,
call_id.empty() ? nullptr : call_id.c_str(),
nullptr
};
g_session->add(&tool_msg);
dstalk_free(exec_result);
any_executed = true;
} else {
std::printf(CLR_DIM "[工具结果] fail\n" CLR_RESET);
// 单工具失败log + skip / Single tool failure: log + skip
std::fprintf(stderr, CLR_YELLOW "[WARN] tool '%s' returned null, skipping\n" CLR_RESET,
tool_name.c_str());
}
}
if (!any_executed) break;
// 重新调用 AIchat_stream 流式,此时 history 已包含工具结果) / Re-invoke AI (chat_stream streaming, history now includes tool results)
history_count = 0;
history = g_session->history(&history_count);
g_ai->free_result(&result);
bool tool_stream_first = true;
result = g_ai->chat_stream(history, history_count, nullptr, on_stream_token, &tool_stream_first);
if (result.ok) {
std::printf(CLR_RESET "\n");
dstalk_message_t ai_followup = {
"assistant",
result.content,
nullptr,
result.tool_calls_json
};
g_session->add(&ai_followup);
has_tool_calls = (result.tool_calls_json && result.tool_calls_json[0] != '\0');
} else {
std::printf(CLR_RED "[ERROR] AI 调用失败: %s\n" CLR_RESET,
result.error ? result.error : "unknown error");
break;
}
}
if (tool_round >= MAX_TOOL_ROUNDS && has_tool_calls) {
std::fprintf(stderr, CLR_YELLOW "[WARN] 已达最大工具调用轮次(%d),停止\n" CLR_RESET, MAX_TOOL_ROUNDS);
}
} else {
// A3: error 路径下需 NULL 保护;当前只取 result.errorcontent 未涉及 / Error path needs NULL guard; currently only reads result.error, content not involved
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 只在此调用(交互模式下) / Single exit point, dstalk_shutdown only called here (in interactive mode)
dstalk_shutdown();
return g_quit_via_signal ? EXIT_INTERRUPT : EXIT_OK;
}