Complete build wiring and CLI file commands

Align documented commands with the CLI, enable optional GUI/test targets, and remove committed API secrets so the project is safer to build and run.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 20:43:53 +08:00
parent e1b0abaf54
commit 330cd686db
16 changed files with 197 additions and 57 deletions

2
.gitignore vendored
View File

@@ -27,3 +27,5 @@ tools/conan2/
.vscode/ .vscode/
.idea/ .idea/
*.user *.user
config.toml
*.local.toml

View File

@@ -6,7 +6,17 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(DSTALK_BUILD_GUI "Build the SDL3 GUI frontend" ON)
option(DSTALK_BUILD_TESTS "Build dstalk tests" ON)
add_subdirectory(dstalk-core) add_subdirectory(dstalk-core)
add_subdirectory(dstalk-cli) add_subdirectory(dstalk-cli)
# add_subdirectory(dstalk-gui) # 等 SDL3 Conan 包可用后启用
# add_subdirectory(tests) # TODO: 引入测试框架后启用 if(DSTALK_BUILD_GUI)
add_subdirectory(dstalk-gui)
endif()
if(DSTALK_BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()

View File

@@ -98,7 +98,7 @@ build.bat # 一键: Conan拉依赖 → CMake配置 → Ninja编译
```bash ```bash
build/dstalk-cli/dstalk-cli.exe # 命令行模式 build/dstalk-cli/dstalk-cli.exe # 命令行模式
build/dstalk-gui/dstalk-gui.exe # 图形模式 build/dstalk-gui/dstalk-gui.exe # 图形模式(默认启用,可用 -DDSTALK_BUILD_GUI=OFF 关闭)
``` ```
--- ---
@@ -144,20 +144,23 @@ $ dstalk-cli
[dstalk] 已更新 csv_avg.c——跳过第一行表头增加列选择功能。 [dstalk] 已更新 csv_avg.c——跳过第一行表头增加列选择功能。
> /edit csv_avg.c:15 把 atof 改成 strtod > /file show csv_avg.c
[dstalk] 已应用修改 [dstalk] 已显示 csv_avg.c 内容
``` ```
### 常用命令 ### 常用命令
| 命令 | 说明 | | 命令 | 说明 |
|------|------| |------|------|
| `/file list` | 列出当前会话关联的文件 | | `/file list [path]` | 列出目录内容 |
| `/file show <path>` | 查看文件内容(语法高亮) | | `/file show <path>` | 查看文件内容 |
| `/edit <path>:<line> <描述>` | 让 AI 修改指定位置 | | `/file read <path>` | 读取文件内容 |
| `/model` | 切换 AI 模型 | | `/file write <path> <content>` | 写入文件 |
| `/model <name>` | 切换 AI 模型 |
| `/clear` | 清空会话上下文 | | `/clear` | 清空会话上下文 |
| `/save <path>` | 保存会话 |
| `/load <path>` | 恢复会话 |
| `/help` | 显示帮助 | | `/help` | 显示帮助 |
--- ---
@@ -165,7 +168,7 @@ $ dstalk-cli
## 工程结构 ## 工程结构
```text ```text
dstalk2026/ dstalk/
├── deps/ ├── deps/
│ └── conanfile.txt # Conan2 依赖声明 │ └── conanfile.txt # Conan2 依赖声明
├── dstalk-core/ # 核心 DLL ├── dstalk-core/ # 核心 DLL
@@ -252,7 +255,7 @@ A: 主要支持 DeepSeek V4同时兼容 OpenAI GPT 系列和 Anthropic Claude
A: CLI 适合终端/SSH/CI 环境GUI 适合需要富文本和鼠标交互的场景。两者共享同一核心 DLL功能一致。 A: CLI 适合终端/SSH/CI 环境GUI 适合需要富文本和鼠标交互的场景。两者共享同一核心 DLL功能一致。
**Q: 如何配置 API Key** **Q: 如何配置 API Key**
A: 首次运行提示输入,或手动创建 `~/.config/dstalk/config.toml` A: 首次运行前,手动创建项目目录下的 `config.toml`
```toml ```toml
[api] [api]
@@ -268,9 +271,9 @@ model = "deepseek-v4"
| 阶段 | 内容 | | 阶段 | 内容 |
|------|------| |------|------|
| **Phase 1** (当前) | 项目骨架、CMake 构建、DLL 导出、前端主循环 | | **Phase 1** | 项目骨架、CMake 构建、DLL 导出、CLI 前端主循环 |
| **Phase 2** | HTTPS 网络层、DeepSeek API 对接、基本对话 | | **Phase 2** | HTTPS 网络层、DeepSeek API 对接、基本对话 |
| **Phase 3** | 流式输出、多轮会话、文件读写工具、CLI 体验对齐 | | **Phase 3** (当前) | 流式输出、多轮会话、文件读写工具、CLI 体验对齐 |
| **Phase 4** | SDL3 GUI 完善、插件系统、LSP 集成 | | **Phase 4** | SDL3 GUI 完善、插件系统、LSP 集成 |
--- ---

1
deps/conanfile.txt vendored
View File

@@ -1,6 +1,7 @@
[requires] [requires]
boost/1.86.0 boost/1.86.0
openssl/3.4.1 openssl/3.4.1
sdl/3.2.10
[generators] [generators]
CMakeDeps CMakeDeps

View File

@@ -9,3 +9,9 @@ add_executable(dstalk-cli
target_link_libraries(dstalk-cli target_link_libraries(dstalk-cli
PRIVATE dstalk PRIVATE dstalk
) )
add_custom_command(TARGET dstalk-cli POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:dstalk>
$<TARGET_FILE_DIR:dstalk-cli>
)

View File

@@ -1,7 +1,11 @@
#include <algorithm>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <filesystem>
#include <string> #include <string>
#include <system_error>
#include <vector>
#ifdef _WIN32 #ifdef _WIN32
#include <windows.h> #include <windows.h>
@@ -36,17 +40,66 @@ static void print_banner()
static void print_help() static void print_help()
{ {
std::printf("\n%s命令列表:%s\n", CLR_BOLD, CLR_RESET); std::printf("\n%s命令列表:%s\n", CLR_BOLD, CLR_RESET);
std::printf(" %s/help%s 显示此帮助\n", CLR_YELLOW, 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/quit%s 退出程序\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/clear%s 清空当前会话上下文\n", CLR_YELLOW, CLR_RESET); std::printf(" %s/clear%s 清空当前会话上下文\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/model <name>%s 切换模型\n", CLR_YELLOW, CLR_RESET); std::printf(" %s/model <name>%s 切换模型\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/file read <path>%s 读取文件内容\n", CLR_YELLOW, CLR_RESET); std::printf(" %s/file list [path]%s 列出目录内容\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/file write <p> <c>%s写入文件\n", CLR_YELLOW, CLR_RESET); std::printf(" %s/file show <path>%s 查看文件内容\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/save <path>%s 保存会话\n", CLR_YELLOW, CLR_RESET); std::printf(" %s/file read <path>%s 读取文件内容\n", CLR_YELLOW, CLR_RESET);
std::printf(" %s/load <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("\n直接输入问题即可与 AI 对话。\n\n"); std::printf("\n直接输入问题即可与 AI 对话。\n\n");
} }
static void print_file(const char* path)
{
while (*path == ' ') path++;
char* content = nullptr;
if (dstalk_file_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_string(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) static void handle_command(const char* line)
{ {
if (!line || line[0] != '/') return; if (!line || line[0] != '/') return;
@@ -80,19 +133,22 @@ static void handle_command(const char* line)
return; 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> // /file read <path>
if (std::strncmp(line, "/file read ", 11) == 0) { if (std::strncmp(line, "/file read ", 11) == 0) {
const char* path = line + 11; print_file(line + 11);
while (*path == ' ') path++;
char* content = nullptr;
if (dstalk_file_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_string(content);
} else {
std::printf(CLR_RED "[ERROR] 无法读取: %s\n" CLR_RESET, path);
}
return; return;
} }

View File

@@ -32,10 +32,3 @@ target_compile_definitions(dstalk
PRIVATE DSTALK_BUILD_DLL PRIVATE DSTALK_BUILD_DLL
INTERFACE DSTALK_USE_DLL INTERFACE DSTALK_USE_DLL
) )
# Windows: 生成 .lib 导入库和 .dll
if(WIN32)
set_target_properties(dstalk PROPERTIES
WINDOWS_EXPORT_ALL_SYMBOLS ON
)
endif()

View File

@@ -14,6 +14,7 @@ struct Message {
// API 配置 // API 配置
struct ApiConfig { struct ApiConfig {
std::string provider; // 默认 "deepseek"
std::string base_url; // 默认 "https://api.deepseek.com/v1" std::string base_url; // 默认 "https://api.deepseek.com/v1"
std::string api_key; std::string api_key;
std::string model; // 默认 "deepseek-chat" std::string model; // 默认 "deepseek-chat"

View File

@@ -19,6 +19,7 @@ dstalk::ai::ApiConfig g_config;
std::vector<dstalk::ai::Message> g_history; std::vector<dstalk::ai::Message> g_history;
// 默认配置 // 默认配置
const char* DEFAULT_PROVIDER = "deepseek";
const char* DEFAULT_BASE_URL = "https://api.deepseek.com/v1"; const char* DEFAULT_BASE_URL = "https://api.deepseek.com/v1";
const char* DEFAULT_MODEL = "deepseek-chat"; const char* DEFAULT_MODEL = "deepseek-chat";
@@ -79,7 +80,9 @@ void parse_config_file(const char* path)
val = val.substr(1, val.size() - 2); val = val.substr(1, val.size() - 2);
if (current_section == "api") { if (current_section == "api") {
if (key == "api_key" || key == "apikey") if (key == "provider")
g_config.provider = val;
else if (key == "api_key" || key == "apikey")
g_config.api_key = val; g_config.api_key = val;
else if (key == "base_url") else if (key == "base_url")
g_config.base_url = val; g_config.base_url = val;
@@ -98,6 +101,7 @@ DSTALK_API int dstalk_init(const char* config_path)
if (g_initialized) return -1; if (g_initialized) return -1;
// 设置默认值 // 设置默认值
g_config.provider = DEFAULT_PROVIDER;
g_config.base_url = DEFAULT_BASE_URL; g_config.base_url = DEFAULT_BASE_URL;
g_config.model = DEFAULT_MODEL; g_config.model = DEFAULT_MODEL;
g_config.max_tokens = 4096; g_config.max_tokens = 4096;

View File

@@ -219,5 +219,5 @@ HttpResponse HttpClient::post_stream(
#else #else
// 非 Windows: 需要 Boost.Beast 实现 (编译时会报错提示) // 非 Windows: 需要 Boost.Beast 实现 (编译时会报错提示)
# error "Non-Windows HTTP client not implemented yet. Use Boost.Beast version." # error "WinHTTP backend is Windows-only. Use net/http_client.cpp for non-Windows builds."
#endif #endif

View File

@@ -13,3 +13,9 @@ target_link_libraries(dstalk-gui
dstalk dstalk
SDL3::SDL3 SDL3::SDL3
) )
add_custom_command(TARGET dstalk-gui POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:dstalk>
$<TARGET_FILE_DIR:dstalk-gui>
)

View File

@@ -35,7 +35,6 @@ int main(int argc, char* argv[])
return 1; return 1;
} }
/* TODO: 主循环 — SDL 事件处理, UI 渲染, 调用 dstalk_chat */
bool running = true; bool running = true;
SDL_Event event; SDL_Event event;
while (running) { while (running) {

View File

@@ -2,6 +2,18 @@
# tests — 单元测试 # tests — 单元测试
# ============================================================ # ============================================================
# TODO: 引入测试框架后启用 add_executable(dstalk-smoke-test
# enable_testing() smoke_test.cpp
# add_subdirectory(...) )
target_link_libraries(dstalk-smoke-test
PRIVATE dstalk
)
add_custom_command(TARGET dstalk-smoke-test POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:dstalk>
$<TARGET_FILE_DIR:dstalk-smoke-test>
)
add_test(NAME dstalk-smoke-test COMMAND dstalk-smoke-test)

52
tests/smoke_test.cpp Normal file
View File

@@ -0,0 +1,52 @@
#include "dstalk/dstalk_api.h"
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
int main()
{
const auto dir = std::filesystem::temp_directory_path() / "dstalk-smoke-test";
std::filesystem::create_directories(dir);
const auto config_path = dir / "config.toml";
{
std::ofstream config(config_path);
config << "[api]\n"
<< "provider = \"deepseek\"\n"
<< "base_url = \"https://api.deepseek.com/v1\"\n"
<< "api_key = \"test-key\"\n"
<< "model = \"deepseek-chat\"\n";
}
if (dstalk_init(config_path.string().c_str()) != 0) {
std::cerr << "dstalk_init failed\n";
return 1;
}
const auto file_path = dir / "sample.txt";
if (dstalk_file_write(file_path.string().c_str(), "hello dstalk") != 0) {
std::cerr << "dstalk_file_write failed\n";
dstalk_destroy();
return 1;
}
char* content = nullptr;
if (dstalk_file_read(file_path.string().c_str(), &content) != 0 || !content) {
std::cerr << "dstalk_file_read failed\n";
dstalk_destroy();
return 1;
}
const bool ok = std::strcmp(content, "hello dstalk") == 0;
dstalk_free_string(content);
dstalk_destroy();
if (!ok) {
std::cerr << "unexpected file content\n";
return 1;
}
return 0;
}

View File

@@ -8,6 +8,8 @@ set "TOOLS=%~dp0"
if exist "%TOOLS%cmake\bin" set "PATH=%TOOLS%cmake\bin;%PATH%" if exist "%TOOLS%cmake\bin" set "PATH=%TOOLS%cmake\bin;%PATH%"
if exist "%TOOLS%ninja" set "PATH=%TOOLS%ninja;%PATH%" if exist "%TOOLS%ninja" set "PATH=%TOOLS%ninja;%PATH%"
if exist "%TOOLS%llvm\bin" set "PATH=%TOOLS%llvm\bin;%PATH%" if exist "%TOOLS%llvm\bin" set "PATH=%TOOLS%llvm\bin;%PATH%"
if exist "%TOOLS%llvm\bin\clang.exe" set "CC=%TOOLS%llvm\bin\clang.exe"
if exist "%TOOLS%llvm\bin\clang++.exe" set "CXX=%TOOLS%llvm\bin\clang++.exe"
if exist "%TOOLS%conan2" set "PATH=%TOOLS%conan2;%PATH%" if exist "%TOOLS%conan2" set "PATH=%TOOLS%conan2;%PATH%"
echo [dstalk] tools\ 工具链已加载 echo [dstalk] tools\ 工具链已加载

View File

@@ -52,19 +52,12 @@ cmake,Ninja,llvm,clang,lld
cmake->ninja->cdll & exe cmake->ninja->cdll & exe
测试1密钥和连接网址: 测试连接网址:
sk-DWiHMg4T3cIxWUSwRGtjLuPe1c8FuwM0FiGyoyuNFWGpkhjY openai api:https://api.deepseek.com
anthropic或openai api:https://api.ai.pulsareon.com anthropic api:https://api.deepseek.com/anthropic
测试1用的模型: 测试模型:
deepseek-v4-pro deepseek-v4-pro
deepseek-v4-flash deepseek-v4-flash
测试2deepseek的网址和密钥 密钥请通过本地 config.toml 配置,不要提交到仓库。
anthropic api:https://api.deepseek.com/anthropic
openai api:https://api.deepseek.com
sk-fabe6677bbfb4f119b29c54b0150bc0b
测试2用的模型
deepseek-v4-pro
deepseek-v4-flash