diff --git a/.gitignore b/.gitignore index 6f22d20..1357140 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ tools/conan2/ .vscode/ .idea/ *.user +config.toml +*.local.toml diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d359f7..068e699 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,17 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_C_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-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() diff --git a/README.md b/README.md index 528102a..379e5d8 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ build.bat # 一键: Conan拉依赖 → CMake配置 → Ninja编译 ```bash 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——跳过第一行表头,增加列选择功能。 -> /edit csv_avg.c:15 把 atof 改成 strtod +> /file show csv_avg.c - [dstalk] 已应用修改。 + [dstalk] 已显示 csv_avg.c 内容。 ``` ### 常用命令 | 命令 | 说明 | |------|------| -| `/file list` | 列出当前会话关联的文件 | -| `/file show ` | 查看文件内容(语法高亮) | -| `/edit : <描述>` | 让 AI 修改指定位置 | -| `/model` | 切换 AI 模型 | +| `/file list [path]` | 列出目录内容 | +| `/file show ` | 查看文件内容 | +| `/file read ` | 读取文件内容 | +| `/file write ` | 写入文件 | +| `/model ` | 切换 AI 模型 | | `/clear` | 清空会话上下文 | +| `/save ` | 保存会话 | +| `/load ` | 恢复会话 | | `/help` | 显示帮助 | --- @@ -165,7 +168,7 @@ $ dstalk-cli ## 工程结构 ```text -dstalk2026/ +dstalk/ ├── deps/ │ └── conanfile.txt # Conan2 依赖声明 ├── dstalk-core/ # 核心 DLL @@ -252,7 +255,7 @@ A: 主要支持 DeepSeek V4,同时兼容 OpenAI GPT 系列和 Anthropic Claude A: CLI 适合终端/SSH/CI 环境,GUI 适合需要富文本和鼠标交互的场景。两者共享同一核心 DLL,功能一致。 **Q: 如何配置 API Key?** -A: 首次运行提示输入,或手动创建 `~/.config/dstalk/config.toml`: +A: 首次运行前,手动创建项目目录下的 `config.toml`: ```toml [api] @@ -268,9 +271,9 @@ model = "deepseek-v4" | 阶段 | 内容 | |------|------| -| **Phase 1** (当前) | 项目骨架、CMake 构建、DLL 导出、前端主循环 | +| **Phase 1** | 项目骨架、CMake 构建、DLL 导出、CLI 前端主循环 | | **Phase 2** | HTTPS 网络层、DeepSeek API 对接、基本对话 | -| **Phase 3** | 流式输出、多轮会话、文件读写工具、CLI 体验对齐 | +| **Phase 3** (当前) | 流式输出、多轮会话、文件读写工具、CLI 体验对齐 | | **Phase 4** | SDL3 GUI 完善、插件系统、LSP 集成 | --- diff --git a/deps/conanfile.txt b/deps/conanfile.txt index ef50fa6..4a4522d 100644 --- a/deps/conanfile.txt +++ b/deps/conanfile.txt @@ -1,6 +1,7 @@ [requires] boost/1.86.0 openssl/3.4.1 +sdl/3.2.10 [generators] CMakeDeps diff --git a/dstalk-cli/CMakeLists.txt b/dstalk-cli/CMakeLists.txt index 35ad2af..b3b18c8 100644 --- a/dstalk-cli/CMakeLists.txt +++ b/dstalk-cli/CMakeLists.txt @@ -9,3 +9,9 @@ add_executable(dstalk-cli target_link_libraries(dstalk-cli PRIVATE dstalk ) + +add_custom_command(TARGET dstalk-cli POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ +) diff --git a/dstalk-cli/src/main.cpp b/dstalk-cli/src/main.cpp index 5db73b4..3de9806 100644 --- a/dstalk-cli/src/main.cpp +++ b/dstalk-cli/src/main.cpp @@ -1,7 +1,11 @@ +#include #include #include #include +#include #include +#include +#include #ifdef _WIN32 #include @@ -36,17 +40,66 @@ static void print_banner() 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/model %s 切换模型\n", CLR_YELLOW, CLR_RESET); - std::printf(" %s/file read %s 读取文件内容\n", CLR_YELLOW, CLR_RESET); - std::printf(" %s/file write

%s写入文件\n", CLR_YELLOW, CLR_RESET); - std::printf(" %s/save %s 保存会话\n", CLR_YELLOW, CLR_RESET); - std::printf(" %s/load %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/clear%s 清空当前会话上下文\n", CLR_YELLOW, CLR_RESET); + std::printf(" %s/model %s 切换模型\n", CLR_YELLOW, CLR_RESET); + std::printf(" %s/file list [path]%s 列出目录内容\n", CLR_YELLOW, CLR_RESET); + std::printf(" %s/file show %s 查看文件内容\n", CLR_YELLOW, CLR_RESET); + std::printf(" %s/file read %s 读取文件内容\n", CLR_YELLOW, CLR_RESET); + std::printf(" %s/file write

%s 写入文件\n", CLR_YELLOW, CLR_RESET); + std::printf(" %s/save %s 保存会话\n", CLR_YELLOW, CLR_RESET); + std::printf(" %s/load %s 恢复会话\n", CLR_YELLOW, CLR_RESET); 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 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) { if (!line || line[0] != '/') return; @@ -80,19 +133,22 @@ 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 + if (std::strncmp(line, "/file show ", 11) == 0) { + print_file(line + 11); + return; + } + // /file read if (std::strncmp(line, "/file read ", 11) == 0) { - const char* path = 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); - } + print_file(line + 11); return; } diff --git a/dstalk-core/CMakeLists.txt b/dstalk-core/CMakeLists.txt index 02141c4..945592d 100644 --- a/dstalk-core/CMakeLists.txt +++ b/dstalk-core/CMakeLists.txt @@ -32,10 +32,3 @@ target_compile_definitions(dstalk PRIVATE DSTALK_BUILD_DLL INTERFACE DSTALK_USE_DLL ) - -# Windows: 生成 .lib 导入库和 .dll -if(WIN32) - set_target_properties(dstalk PROPERTIES - WINDOWS_EXPORT_ALL_SYMBOLS ON - ) -endif() diff --git a/dstalk-core/src/ai/deepseek_api.hpp b/dstalk-core/src/ai/deepseek_api.hpp index e554c2a..e502a07 100644 --- a/dstalk-core/src/ai/deepseek_api.hpp +++ b/dstalk-core/src/ai/deepseek_api.hpp @@ -14,6 +14,7 @@ struct Message { // API 配置 struct ApiConfig { + std::string provider; // 默认 "deepseek" std::string base_url; // 默认 "https://api.deepseek.com/v1" std::string api_key; std::string model; // 默认 "deepseek-chat" diff --git a/dstalk-core/src/api.cpp b/dstalk-core/src/api.cpp index 719820e..59609bd 100644 --- a/dstalk-core/src/api.cpp +++ b/dstalk-core/src/api.cpp @@ -19,6 +19,7 @@ dstalk::ai::ApiConfig g_config; std::vector g_history; // 默认配置 +const char* DEFAULT_PROVIDER = "deepseek"; const char* DEFAULT_BASE_URL = "https://api.deepseek.com/v1"; const char* DEFAULT_MODEL = "deepseek-chat"; @@ -79,7 +80,9 @@ void parse_config_file(const char* path) val = val.substr(1, val.size() - 2); 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; else if (key == "base_url") g_config.base_url = val; @@ -98,6 +101,7 @@ DSTALK_API int dstalk_init(const char* config_path) if (g_initialized) return -1; // 设置默认值 + g_config.provider = DEFAULT_PROVIDER; g_config.base_url = DEFAULT_BASE_URL; g_config.model = DEFAULT_MODEL; g_config.max_tokens = 4096; diff --git a/dstalk-core/src/net/http_client_win.cpp b/dstalk-core/src/net/http_client_win.cpp index 64d4950..aadc331 100644 --- a/dstalk-core/src/net/http_client_win.cpp +++ b/dstalk-core/src/net/http_client_win.cpp @@ -219,5 +219,5 @@ HttpResponse HttpClient::post_stream( #else // 非 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 diff --git a/dstalk-gui/CMakeLists.txt b/dstalk-gui/CMakeLists.txt index 47d8175..9f534eb 100644 --- a/dstalk-gui/CMakeLists.txt +++ b/dstalk-gui/CMakeLists.txt @@ -13,3 +13,9 @@ target_link_libraries(dstalk-gui dstalk SDL3::SDL3 ) + +add_custom_command(TARGET dstalk-gui POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ +) diff --git a/dstalk-gui/src/main.cpp b/dstalk-gui/src/main.cpp index fee00e4..c5d9046 100644 --- a/dstalk-gui/src/main.cpp +++ b/dstalk-gui/src/main.cpp @@ -35,7 +35,6 @@ int main(int argc, char* argv[]) return 1; } - /* TODO: 主循环 — SDL 事件处理, UI 渲染, 调用 dstalk_chat */ bool running = true; SDL_Event event; while (running) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 61861c8..2f1f9ad 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -2,6 +2,18 @@ # tests — 单元测试 # ============================================================ -# TODO: 引入测试框架后启用 -# enable_testing() -# add_subdirectory(...) +add_executable(dstalk-smoke-test + smoke_test.cpp +) + +target_link_libraries(dstalk-smoke-test + PRIVATE dstalk +) + +add_custom_command(TARGET dstalk-smoke-test POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ +) + +add_test(NAME dstalk-smoke-test COMMAND dstalk-smoke-test) diff --git a/tests/smoke_test.cpp b/tests/smoke_test.cpp new file mode 100644 index 0000000..e0388fe --- /dev/null +++ b/tests/smoke_test.cpp @@ -0,0 +1,52 @@ +#include "dstalk/dstalk_api.h" + +#include +#include +#include +#include +#include + +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; +} diff --git a/tools/env.bat b/tools/env.bat index cc5b33e..330a065 100644 --- a/tools/env.bat +++ b/tools/env.bat @@ -8,6 +8,8 @@ set "TOOLS=%~dp0" if exist "%TOOLS%cmake\bin" set "PATH=%TOOLS%cmake\bin;%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\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%" echo [dstalk] tools\ 工具链已加载 diff --git a/说明.txt b/说明.txt index 90b63f8..1431179 100644 --- a/说明.txt +++ b/说明.txt @@ -52,19 +52,12 @@ cmake,Ninja,llvm,clang,lld cmake->ninja->cdll & exe -测试1密钥和连接网址: -sk-DWiHMg4T3cIxWUSwRGtjLuPe1c8FuwM0FiGyoyuNFWGpkhjY -anthropic或openai api:https://api.ai.pulsareon.com +测试连接网址: +openai api:https://api.deepseek.com +anthropic api:https://api.deepseek.com/anthropic -测试1用的模型: +测试用模型: deepseek-v4-pro deepseek-v4-flash -测试2deepseek的网址和密钥: -anthropic api:https://api.deepseek.com/anthropic -openai api:https://api.deepseek.com -sk-fabe6677bbfb4f119b29c54b0150bc0b - -测试2用的模型: -deepseek-v4-pro -deepseek-v4-flash \ No newline at end of file +密钥请通过本地 config.toml 配置,不要提交到仓库。