W22: coverage metric + network tests + Tool stream feedback + stdin pipe + session path + dependency check (W22.1-W22.6)
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled
CI / Sanitizer (ASan+UBSan) / ubuntu-24.04 (push) Has been cancelled
CI / Coverage (gcovr) / ubuntu-24.04 (push) Has been cancelled

- W22.1: gcovr 覆盖率度量 + CI coverage job(40% 阈值 warning)
- W22.2: network_plugin 单元测试(parse_headers_json/extract_host_port/SSE/异常保护)
- W22.3: Tool Calling 流式反馈(chat_stream + "[工具调用]/[工具结果]" 状态行)
- W22.4: --prompt stdin pipe(--prompt - 从 stdin 读取)
- W22.5: session 路径健壮化(static 缓存 + mkdir + fallback)
- W22.6: 插件依赖拓扑静态校验(validate_dependencies 循环/缺失检测)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 21:21:24 +08:00
parent b2b381b9b3
commit df3bf298ee
13 changed files with 753 additions and 23 deletions

View File

@@ -406,9 +406,16 @@ int main(int argc, char* argv[])
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 && i + 1 < argc && argv[i+1][0] != '-') {
prompt_arg = argv[++i];
} 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
}
}
}
}
@@ -524,10 +531,25 @@ int main(int argc, char* argv[])
// ---- --prompt 批处理模式 (非交互) ----
if (prompt_arg) {
if (prompt_arg[0] == '\0') {
std::fprintf(stderr, "empty prompt\n");
dstalk_shutdown();
return EXIT_FATAL;
std::string prompt_text;
if (std::strcmp(prompt_arg, "-") == 0) {
// --prompt - or --prompt (no arg): read prompt from 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);
@@ -536,7 +558,7 @@ int main(int argc, char* argv[])
}
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_arg, nullptr);
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);
@@ -654,8 +676,10 @@ int main(int argc, char* argv[])
? boost::json::value_to<std::string>(*id_j) : "";
// 执行工具
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,
@@ -666,6 +690,7 @@ int main(int argc, char* argv[])
dstalk_free(exec_result);
any_executed = true;
} else {
std::printf(CLR_DIM "[工具结果] fail\n" CLR_RESET);
// 单工具失败log + skip
std::fprintf(stderr, CLR_YELLOW "[WARN] tool '%s' returned null, skipping\n" CLR_RESET,
tool_name.c_str());
@@ -674,20 +699,16 @@ int main(int argc, char* argv[])
if (!any_executed) break;
// 重新调用 AIchat 流式,此时 history 已包含工具结果)
// 重新调用 AIchat_stream 流式,此时 history 已包含工具结果)
history_count = 0;
history = g_session->history(&history_count);
char* tools_json = g_tools->get_tools_json();
g_ai->free_result(&result);
result = g_ai->chat(history, history_count, nullptr, tools_json);
if (tools_json) dstalk_free(tools_json);
bool tool_stream_first = true;
result = g_ai->chat_stream(history, history_count, nullptr, on_stream_token, &tool_stream_first);
if (result.ok) {
if (result.content && result.content[0]) {
std::printf("%s\n", result.content);
}
std::printf(CLR_RESET "\n");
dstalk_message_t ai_followup = {
"assistant",
result.content,