Wave 9: fix audit findings, harden ABI, deduplicate config (W12.1-W12.6)
Some checks failed
CI / Determine matrix (push) Has been cancelled
CI / ${{ matrix.os }} / ${{ matrix.build_type }} (push) Has been cancelled

- W12.1 context_plugin (engineer-zhou): wrap C ABI surface in try/catch,
  add OOM-safe strdup_message_fields helper, make g_max_tokens drive
  message-count trim (option A).
- W12.2 config refactor (architect-lin): introduce
  plugins/config/include/toml_parse.h to eliminate 74-line parser
  duplication; config_plugin delegates to host->config_get/set,
  collapsing the dual-store data island; ConfigStore::get() now copies
  via thread_local std::string to remove c_str() dangling under
  concurrent set(). Zero ABI changes.
- W12.3 CLI command parsing (engineer-zhao): guard /clear and /context
  on missing session service; refactor /file dispatch so bare
  /file write hits usage instead of unknown-command.
- W12.4 build path unification (devops-hu): set per-target
  RUNTIME_OUTPUT_DIRECTORY on dstalk-cli; remove stale
  build/dstalk-cli/dstalk-cli.exe so build/bin/ is the sole binary.
- W12.5 STATUS.md auto-refresh (engineer-li): run W11.6 script to
  regenerate STATUS from live profile/group data.
- W12.6 plugin-abi.md (writer-deng): add §8 exception safety across
  ABI boundary and §9 string return lifetime; reference real
  audit-found violations as anti-examples.

Verified: cmake build 0 error 0 warning, ctest 4/4 pass.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-05-27 09:19:17 +08:00
parent bb2e8c0220
commit 58869abc15
15 changed files with 750 additions and 332 deletions

View File

@@ -6,6 +6,10 @@ add_executable(dstalk-cli
src/main.cpp
)
set_target_properties(dstalk-cli PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
target_link_libraries(dstalk-cli
PRIVATE dstalk
)

View File

@@ -166,8 +166,12 @@ static void handle_command(const char* line)
// /clear
if (std::strcmp(line, "/clear") == 0) {
if (g_session) g_session->clear();
std::printf(CLR_GREEN "[OK] 会话已清空\n" CLR_RESET);
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;
}
@@ -180,6 +184,8 @@ static void handle_command(const char* line)
std::printf(CLR_DIM "消息条数: " CLR_RESET "%d | "
CLR_DIM "Token 估算: " CLR_RESET "%d\n",
count, tokens);
} else {
std::fprintf(stderr, CLR_RED "[ERROR] context service not available\n" CLR_RESET);
}
return;
}
@@ -226,42 +232,67 @@ static void handle_command(const char* line)
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;
// /file <subcommand> [args...] —— 统一入口,避免 strncmp 空格匹配遗漏
if (std::strncmp(line, "/file", 5) == 0) {
const char* rest = line + 5;
while (*rest == ' ') rest++;
const char* space = std::strchr(rest, ' ');
if (!space) {
std::printf(CLR_RED "[ERROR] 用法: /file write <path> <content>\n" CLR_RESET);
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;
}
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());
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;
}