From add578fdeb71874884af1fa7b0b663970ed201d3 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Fri, 2 Oct 2026 21:30:30 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(ai):=20=E4=BF=AE=E6=AD=A3=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=E7=BC=93=E5=AD=98=E9=94=AE=E8=AF=AD=E4=B9=89=E8=BE=93?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/CMakeLists.txt | 2 + server/src/ai/ai_assistant.cpp | 12 +--- server/src/ai/ai_assistant_cache_key.cpp | 18 ++++++ server/src/ai/ai_assistant_cache_key.h | 10 ++++ server/tests/CMakeLists.txt | 2 + .../tests/src/test_ai_assistant_cache_key.cpp | 57 +++++++++++++++++++ 6 files changed, 92 insertions(+), 9 deletions(-) create mode 100644 server/src/ai/ai_assistant_cache_key.cpp create mode 100644 server/src/ai/ai_assistant_cache_key.h create mode 100644 server/tests/src/test_ai_assistant_cache_key.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 911a86b2..5166b10b 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -147,6 +147,8 @@ endif() set(SRC_FILES "./src/ai/ai_assistant.cpp" "./src/ai/ai_assistant.h" + "./src/ai/ai_assistant_cache_key.cpp" + "./src/ai/ai_assistant_cache_key.h" "./src/clipboard/clipboard_history.cpp" "./src/clipboard/clipboard_history.h" "./src/cloud/cloud_ime.cpp" diff --git a/server/src/ai/ai_assistant.cpp b/server/src/ai/ai_assistant.cpp index fc107111..4a7841d8 100644 --- a/server/src/ai/ai_assistant.cpp +++ b/server/src/ai/ai_assistant.cpp @@ -1,4 +1,5 @@ #include "ai_assistant.h" +#include "ai_assistant_cache_key.h" #include #include @@ -24,13 +25,6 @@ std::chrono::steady_clock::time_point g_last_input; AiAssistant::ApplyCallback g_callback; std::unordered_map g_candidate_cache; -std::string BuildCacheKey(const AiAssistant::Request &request) -{ - nlohmann::json segments = request.pinyin_segments; - return request.config.provider + "\n" + request.config.endpoint + "\n" + request.config.model + "\n" + - segments.dump(); -} - size_t WriteResponse(char *data, size_t size, size_t count, void *user) { static_cast(user)->append(data, size * count); @@ -135,7 +129,7 @@ void WorkerLoop() break; observed = g_generation.load(); auto request = g_latest; - const auto cached = g_candidate_cache.find(BuildCacheKey(request)); + const auto cached = g_candidate_cache.find(AiAssistant::detail::BuildSuggestionCacheKey(request)); if (!request.identity.empty() && cached != g_candidate_cache.end()) { const std::string candidate = cached->second; @@ -164,7 +158,7 @@ void WorkerLoop() { { std::lock_guard cache_lock(g_mutex); - g_candidate_cache[BuildCacheKey(request)] = candidate; + g_candidate_cache[AiAssistant::detail::BuildSuggestionCacheKey(request)] = candidate; } (void)0; g_callback(candidate, request.identity, observed); diff --git a/server/src/ai/ai_assistant_cache_key.cpp b/server/src/ai/ai_assistant_cache_key.cpp new file mode 100644 index 00000000..a2eef900 --- /dev/null +++ b/server/src/ai/ai_assistant_cache_key.cpp @@ -0,0 +1,18 @@ +#include "ai_assistant_cache_key.h" + +#include + +namespace AiAssistant::detail +{ +std::string BuildSuggestionCacheKey(const Request &request) +{ + return nlohmann::json{{"provider", request.config.provider}, + {"endpoint", request.config.endpoint}, + {"model", request.config.model}, + {"prompt", request.config.prompt}, + {"pinyin_segments", request.pinyin_segments}, + {"context", request.context}, + {"candidate_limit", request.config.candidate_limit}} + .dump(); +} +} // namespace AiAssistant::detail diff --git a/server/src/ai/ai_assistant_cache_key.h b/server/src/ai/ai_assistant_cache_key.h new file mode 100644 index 00000000..43871705 --- /dev/null +++ b/server/src/ai/ai_assistant_cache_key.h @@ -0,0 +1,10 @@ +#pragma once + +#include "ai_assistant.h" + +#include + +namespace AiAssistant::detail +{ +std::string BuildSuggestionCacheKey(const Request &request); +} // namespace AiAssistant::detail diff --git a/server/tests/CMakeLists.txt b/server/tests/CMakeLists.txt index 923846ad..85eed85d 100644 --- a/server/tests/CMakeLists.txt +++ b/server/tests/CMakeLists.txt @@ -36,6 +36,8 @@ set(TEST_SOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/src/test_input_key_policy.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_owner.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/test_async_request_origin.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/src/test_ai_assistant_cache_key.cpp" + "${CMAKE_SOURCE_DIR}/src/ai/ai_assistant_cache_key.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_action_policy.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_translation_policy.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_state.cpp" diff --git a/server/tests/src/test_ai_assistant_cache_key.cpp b/server/tests/src/test_ai_assistant_cache_key.cpp new file mode 100644 index 00000000..d87a8f37 --- /dev/null +++ b/server/tests/src/test_ai_assistant_cache_key.cpp @@ -0,0 +1,57 @@ +#include "ai/ai_assistant_cache_key.h" +#include "tests/includes/test_framework.h" + +namespace +{ +AiAssistant::Request MakeRequest() +{ + AiAssistant::Request request; + request.pinyin_segments = {"shang", "hai"}; + request.context = "我准备去"; + request.config.provider = "deepseek"; + request.config.endpoint = "https://api.example.com/chat/completions"; + request.config.model = "example-model"; + request.config.prompt = "Return the best candidate."; + request.config.candidate_limit = 3; + return request; +} +} // namespace + +TEST_CASE(ai_suggestion_cache_hits_for_the_same_semantic_request) +{ + const auto request = MakeRequest(); + const auto same_request = request; + + REQUIRE(AiAssistant::detail::BuildSuggestionCacheKey(request) == + AiAssistant::detail::BuildSuggestionCacheKey(same_request)); +} + +TEST_CASE(ai_suggestion_cache_misses_when_context_changes) +{ + const auto request = MakeRequest(); + auto changed_request = request; + changed_request.context = "他受到了严重"; + + REQUIRE(AiAssistant::detail::BuildSuggestionCacheKey(request) != + AiAssistant::detail::BuildSuggestionCacheKey(changed_request)); +} + +TEST_CASE(ai_suggestion_cache_misses_when_prompt_changes) +{ + const auto request = MakeRequest(); + auto changed_request = request; + changed_request.config.prompt = "Prefer place names."; + + REQUIRE(AiAssistant::detail::BuildSuggestionCacheKey(request) != + AiAssistant::detail::BuildSuggestionCacheKey(changed_request)); +} + +TEST_CASE(ai_suggestion_cache_misses_when_candidate_limit_changes) +{ + const auto request = MakeRequest(); + auto changed_request = request; + changed_request.config.candidate_limit = 1; + + REQUIRE(AiAssistant::detail::BuildSuggestionCacheKey(request) != + AiAssistant::detail::BuildSuggestionCacheKey(changed_request)); +} From 5e100de92f2183c0b3b80de73303a778ed7d839b Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Sat, 3 Oct 2026 00:29:19 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(server):=20=E5=BB=BA=E8=AE=AE=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E9=94=AE=E5=AE=B9=E5=BF=8D=E9=9D=9E=E6=B3=95=20UTF-8?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E5=B7=A5=E4=BD=9C=E7=BA=BF=E7=A8=8B?= =?UTF-8?q?=E6=8A=9B=E5=87=BA=E7=BB=88=E6=AD=A2=E8=BF=9B=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/ai/ai_assistant_cache_key.cpp | 3 ++- server/tests/src/test_ai_assistant_cache_key.cpp | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/server/src/ai/ai_assistant_cache_key.cpp b/server/src/ai/ai_assistant_cache_key.cpp index a2eef900..36c832be 100644 --- a/server/src/ai/ai_assistant_cache_key.cpp +++ b/server/src/ai/ai_assistant_cache_key.cpp @@ -13,6 +13,7 @@ std::string BuildSuggestionCacheKey(const Request &request) {"pinyin_segments", request.pinyin_segments}, {"context", request.context}, {"candidate_limit", request.config.candidate_limit}} - .dump(); + // context 与 prompt 是自由文本;键在无 try 的工作线程里构建,非法 UTF-8 不能抛出 + .dump(-1, ' ', false, nlohmann::json::error_handler_t::replace); } } // namespace AiAssistant::detail diff --git a/server/tests/src/test_ai_assistant_cache_key.cpp b/server/tests/src/test_ai_assistant_cache_key.cpp index d87a8f37..7b652237 100644 --- a/server/tests/src/test_ai_assistant_cache_key.cpp +++ b/server/tests/src/test_ai_assistant_cache_key.cpp @@ -46,6 +46,15 @@ TEST_CASE(ai_suggestion_cache_misses_when_prompt_changes) AiAssistant::detail::BuildSuggestionCacheKey(changed_request)); } +TEST_CASE(ai_suggestion_cache_key_tolerates_invalid_utf8) +{ + auto request = MakeRequest(); + request.context = std::string("\xE6\x88", 2); + request.config.prompt = std::string("\xFF", 1); + + REQUIRE(!AiAssistant::detail::BuildSuggestionCacheKey(request).empty()); +} + TEST_CASE(ai_suggestion_cache_misses_when_candidate_limit_changes) { const auto request = MakeRequest();