Add metadata validation script and module documentation

- Introduced a new Python script `check_agents_metadata.py` for validating agent metadata, including YAML parsing, rating ranges, and cross-references.
- Added usage instructions and exit codes for the script.
- Created a new markdown file `模块目录和功能说明.md` to outline the directory structure and functionality of the modules.
- Added a text file `说明此文件不可AI修改.txt` to specify that certain files should not be modified by AI, including important information about the `dstalk` framework and its modules.
This commit is contained in:
2026-05-31 00:00:58 +08:00
parent 3cc9ee95e4
commit f2da0f2ed4
43 changed files with 2467 additions and 800 deletions

View File

@@ -1,8 +1,9 @@
// ============================================================================
// dstalk-cli — 命令行前端 (使用插件化架构)
// ============================================================================
// 通过 dstalk_host.h API 初始化核心,然后查询插件服务 vtable 调用功能
// ============================================================================
/*
* @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>
@@ -28,7 +29,7 @@
#include "dstalk/dstalk_host.h"
// ---- ANSI 简写 ----
// ---- ANSI 简写 / ANSI shorthand macros ----
#define CLR_RESET "\033[0m"
#define CLR_CYAN "\033[36m"
#define CLR_YELLOW "\033[33m"
@@ -37,25 +38,36 @@
#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 指针 ----
// ---- 服务 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 信号处理 / 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)
{
@@ -66,6 +78,8 @@ static BOOL WINAPI on_console_event(DWORD event)
}
return FALSE;
}
// Unix signal handler (SIGINT).
// Unix 信号处理SIGINT
#else
static void on_signal(int /*sig*/)
{
@@ -74,7 +88,9 @@ static void on_signal(int /*sig*/)
}
#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 | "
@@ -85,6 +101,7 @@ static void print_banner()
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);
@@ -104,6 +121,7 @@ static void print_help()
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++;
@@ -122,6 +140,7 @@ static void print_file(const char* path)
}
}
// 列出目录内容,按文件名排序,子目录以青色高亮 / List directory entries sorted by filename, highlighting subdirectories in cyan.
static void list_files(const char* path)
{
while (*path == ' ') path++;
@@ -155,11 +174,12 @@ static void list_files(const char* path)
}
}
// 分发斜杠命令 / 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 末尾
// /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;
@@ -197,7 +217,7 @@ static void handle_command(const char* line)
return;
}
// /status —— 脱敏显示当前运行状态
// /status —— 脱敏显示当前运行状态 / Display current runtime status (desensitized)
if (std::strcmp(line, "/status") == 0) {
const char* provider = dstalk_config_get("ai.provider");
if (!provider) provider = "ai.deepseek";
@@ -246,7 +266,7 @@ static void handle_command(const char* line)
return;
}
// /file <subcommand> [args...] —— 统一入口,避免 strncmp 空格匹配遗漏
// /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++;
@@ -370,7 +390,8 @@ static void handle_command(const char* line)
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);
@@ -383,10 +404,12 @@ static int on_stream_token(const char* token, void* userdata)
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: 启用 ANSI 转义码支持 / Windows: enable ANSI escape code support
#ifdef _WIN32
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
DWORD mode = 0;
@@ -394,7 +417,7 @@ int main(int argc, char* argv[])
SetConsoleMode(hOut, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
#endif
// ---- C1: batch/pipe 模式检测 ----
// ---- C1: batch/pipe 模式检测 / batch/pipe mode detection ----
#ifdef _WIN32
bool pipe_mode = (_isatty(_fileno(stdin)) == 0);
#else
@@ -421,17 +444,17 @@ int main(int argc, char* argv[])
}
if (pipe_mode) batch_mode = true;
// ---- B1: 安装 Ctrl+C 处理 ----
// ---- 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 标志
// 跳过 --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];
@@ -457,13 +480,13 @@ int main(int argc, char* argv[])
}
}
// 初始化主机(加载配置 + 自动扫描 plugins/ 目录加载插件)
// 初始化主机(加载配置 + 自动扫描 plugins/ 目录加载插件) / Init host: load config + auto-scan plugins/ directory
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.deepseek";
g_ai = static_cast<const dstalk_ai_service_t*>(dstalk_service_query(ai_provider, 1));
@@ -478,7 +501,7 @@ int main(int argc, char* argv[])
std::fprintf(stderr, CLR_RED "[dstalk] Session 服务未找到\n" CLR_RESET);
}
// 自动从配置加载 AI 设置
// 自动从配置加载 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");
@@ -486,7 +509,7 @@ int main(int argc, char* argv[])
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: 记录当前模型名
g_current_model = model; // A1: 记录当前模型名 / Record current model name
}
if (!batch_mode) {
@@ -495,7 +518,7 @@ int main(int argc, char* argv[])
std::printf("\n");
}
// ---- B3: 管道输入模式 (非交互) ----
// ---- B3: 管道输入模式 (非交互) / Pipe input mode (non-interactive) ----
if (pipe_mode) {
std::string input;
char buf[4096];
@@ -529,11 +552,11 @@ int main(int argc, char* argv[])
}
}
// ---- --prompt 批处理模式 (非交互) ----
// ---- --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 - or --prompt (no arg): read prompt from stdin / --prompt - 或 --prompt无参数从 stdin 读取提示
char buf[4096];
while (std::fgets(buf, sizeof(buf), stdin)) {
prompt_text += buf;
@@ -575,13 +598,13 @@ int main(int argc, char* argv[])
char buffer[8192];
while (true) {
// B1: 检查退出标志
// B1: 检查退出标志 / Check quit flag
if (g_quit_requested) {
std::printf("再见!\n");
break;
}
// A1: 提示符带模型名batch 模式不打印)
// 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());
@@ -590,14 +613,14 @@ int main(int argc, char* argv[])
if (!std::fgets(buffer, sizeof(buffer), stdin)) break;
// C3: fgets 截断检测
// 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';
@@ -605,19 +628,19 @@ int main(int argc, char* argv[])
if (len == 0) continue;
// 命令处理
// 命令处理 / Command dispatch
if (buffer[0] == '/') {
handle_command(buffer);
continue;
}
// AI 对话(通过插件服务 vtable
// 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);
@@ -627,14 +650,14 @@ int main(int argc, char* argv[])
if (result.ok) {
std::printf(CLR_RESET "\n\n");
// 将用户消息和 AI 回复添加到会话
// 将用户消息和 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 闭环
// 若 AI 返回了 tool_calls自动执行工具并将结果追加到 history再调 AI
// 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;
@@ -643,15 +666,15 @@ int main(int argc, char* argv[])
tool_round++;
has_tool_calls = false;
// 保存 tool_calls_jsonfree_result 前必须拷贝)
// 保存 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":"..."}}]
// 解析 [{"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; // 空数组 → 终止
if (tc_array.empty()) break; // 空数组 → 终止 / empty array → stop
bool any_executed = false;
for (const auto& tc : tc_array) {
@@ -675,7 +698,7 @@ int main(int argc, char* argv[])
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) {
@@ -691,7 +714,7 @@ int main(int argc, char* argv[])
any_executed = true;
} else {
std::printf(CLR_DIM "[工具结果] fail\n" CLR_RESET);
// 单工具失败log + skip
// 单工具失败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());
}
@@ -699,7 +722,7 @@ int main(int argc, char* argv[])
if (!any_executed) break;
// 重新调用 AIchat_stream 流式,此时 history 已包含工具结果)
// 重新调用 AIchat_stream 流式,此时 history 已包含工具结果) / Re-invoke AI (chat_stream streaming, history now includes tool results)
history_count = 0;
history = g_session->history(&history_count);
@@ -728,14 +751,14 @@ int main(int argc, char* argv[])
std::fprintf(stderr, CLR_YELLOW "[WARN] 已达最大工具调用轮次(%d),停止\n" CLR_RESET, MAX_TOOL_ROUNDS);
}
} else {
// A3: error 路径下需 NULL 保护;当前只取 result.errorcontent 未涉及
// 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 只在此调用(交互模式下)
// 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;
}