Files
msime-windows/server/tests/src/test_voice_providers.cpp
T
fanlusky 8097aa7df2 feat(voice): 豆包语音识别支持新版控制台单 API Key 鉴权
新版豆包控制台只签发一个 API Key,以 X-Api-Key 发送,不再需要 App ID。
原先这条路径是靠「asr_app_key 是否为空」隐式推断的,而随包配置里
asr_app_key 带有占位符(<YOUR_OWN_DOUBAO_APP_ID>)且非空,全新安装会被
判定为旧版控制台,带着占位符发送 X-Api-App-Key 导致鉴权失败。

改为显式配置项 voice_input.doubao_auth_mode(api_key | legacy):

- voice_providers 新增 NormalizeDoubaoAuthMode()/UsesDoubaoLegacyAuth(),
  集中处理模式解析。该项缺失时按 asr_app_key 是否为「可用值」推断,占位符
  不计入,因此既有配置升级后行为不变。
- DoubaoAsrClient 构造函数改收显式 legacy_auth,新版模式下忽略配置中残留
  的 App ID;连接失败提示按模式区分文案。旧版 App ID + Access Token 路径
  完整保留。
- 设置界面新增「豆包鉴权方式」下拉框,新版模式下隐藏 App ID 输入框并把
  Token 标签改为「API Key」。

同时把官方推荐用于输入法场景的 bigmodel_nostream 整句流式接口做成设置里的
可选项,与原有「接口地址」文本框双向同步。默认端点仍为 bigmodel_async,避免
改变既有用户的实时上屏行为。

新版文档将 payload_msg.result 描述为列表,而原 ParseResponse 只按对象读取,
接上该端点会安静地返回空转写。新增 ExtractTranscript() 同时接受对象与数组两
种结构,一套解析覆盖两个端点。
2026-09-13 18:58:39 +08:00

102 lines
4.8 KiB
C++

#ifndef NOMINMAX
#define NOMINMAX
#endif
#include "voice-input/voice_providers.h"
#include "tests/includes/test_framework.h"
TEST_CASE(voice_providers_resolve_openai_compatible_defaults)
{
REQUIRE(VoiceInput::IsDoubaoAsrProvider("doubao"));
REQUIRE(VoiceInput::IsDoubaoAsrProvider("Doubao"));
REQUIRE(!VoiceInput::IsDoubaoAsrProvider("openai"));
REQUIRE_EQ(VoiceInput::DefaultAsrEndpoint("openai"), "https://api.openai.com/v1/audio/transcriptions");
REQUIRE_EQ(VoiceInput::DefaultAsrModel("openai"), "whisper-1");
REQUIRE_EQ(VoiceInput::DefaultAsrModel("siliconflow"), "FunAudioLLM/SenseVoiceSmall");
REQUIRE_EQ(VoiceInput::AsrTokenSlotKey("siliconflow"), "asr_token_siliconflow");
REQUIRE(VoiceInput::IsPlaceholderToken("<YOUR_OWN_DOUBAO_ACCESS_TOKEN>"));
{
VoiceInputConfig config;
config.asr_provider = "doubao";
config.asr_token = "sk-should-not-be-used";
config.asr_tokens["doubao"] = "doubao-access";
config.asr_tokens["siliconflow"] = "sk-siliconflow";
REQUIRE_EQ(VoiceInput::ResolveAsrToken(config), "doubao-access");
config.asr_provider = "siliconflow";
REQUIRE_EQ(VoiceInput::ResolveAsrToken(config), "sk-siliconflow");
config.asr_provider = "groq";
REQUIRE_EQ(VoiceInput::ResolveAsrToken(config), "");
config.asr_provider = "siliconflow";
REQUIRE_EQ(VoiceInput::ResolveAsrModel(config), "FunAudioLLM/SenseVoiceSmall");
config.asr_model = "custom/asr-model";
REQUIRE_EQ(VoiceInput::ResolveAsrModel(config), "custom/asr-model");
}
{
VoiceInputConfig config;
config.polish_provider = "deepseek";
config.polish_token = "sk-should-not-be-used";
config.polish_tokens["siliconflow"] = "sk-sf-polish";
config.polish_tokens["deepseek"] = "sk-deepseek";
REQUIRE_EQ(VoiceInput::ResolvePolishToken(config), "sk-deepseek");
config.polish_provider = "siliconflow";
REQUIRE_EQ(VoiceInput::ResolvePolishToken(config), "sk-sf-polish");
}
REQUIRE_EQ(VoiceInput::DefaultPolishEndpoint("deepseek"), "https://api.deepseek.com/chat/completions");
REQUIRE_EQ(VoiceInput::DefaultPolishModel("openai"), "gpt-4o-mini");
}
// The new console issues one API Key sent as X-Api-Key; the legacy console needs App ID +
// Access Token. Picking the wrong header set fails authentication, so the fallback for configs
// written before doubao_auth_mode existed has to key off a *usable* App ID, not a placeholder.
TEST_CASE(voice_providers_select_doubao_auth_mode)
{
REQUIRE_EQ(VoiceInput::NormalizeDoubaoAuthMode("api_key", "real-app-id"), "api_key");
REQUIRE_EQ(VoiceInput::NormalizeDoubaoAuthMode("legacy", ""), "legacy");
REQUIRE_EQ(VoiceInput::NormalizeDoubaoAuthMode("LEGACY", ""), "legacy");
// Absent or unrecognised setting: infer from the App ID.
REQUIRE_EQ(VoiceInput::NormalizeDoubaoAuthMode("", "real-app-id"), "legacy");
REQUIRE_EQ(VoiceInput::NormalizeDoubaoAuthMode("", ""), "api_key");
REQUIRE_EQ(VoiceInput::NormalizeDoubaoAuthMode("nonsense", "real-app-id"), "legacy");
// The shipped placeholders must not be mistaken for a configured legacy console.
REQUIRE_EQ(VoiceInput::NormalizeDoubaoAuthMode("", "<YOUR_OWN_DOUBAO_APP_ID>"), "api_key");
REQUIRE_EQ(VoiceInput::NormalizeDoubaoAuthMode("", "<YOUR_ASR_APP_KEY>"), "api_key");
VoiceInputConfig config;
config.doubao_auth_mode = "api_key";
config.asr_app_key = "real-app-id";
REQUIRE(!VoiceInput::UsesDoubaoLegacyAuth(config));
config.doubao_auth_mode = "legacy";
REQUIRE(VoiceInput::UsesDoubaoLegacyAuth(config));
config.doubao_auth_mode.clear();
REQUIRE(VoiceInput::UsesDoubaoLegacyAuth(config));
config.asr_app_key.clear();
REQUIRE(!VoiceInput::UsesDoubaoLegacyAuth(config));
}
TEST_CASE(voice_providers_use_builtin_prompt_until_overridden)
{
VoiceInputConfig config;
config.polish_prompt_id = "faithful";
const std::string faithful = VoiceInput::ResolvePolishSystemPrompt(config);
REQUIRE(faithful.find("校对") != std::string::npos);
REQUIRE(faithful.find("<asr_text>") != std::string::npos);
config.polish_prompt = "只用这一句。";
REQUIRE_EQ(VoiceInput::ResolvePolishSystemPrompt(config), "只用这一句。");
REQUIRE_EQ(VoiceInput::WrapAsrUserMessage("明天开会"), "<asr_text>\n明天开会\n</asr_text>");
}
TEST_CASE(voice_providers_empty_custom_falls_back_to_cleanup)
{
VoiceInputConfig config;
config.polish_prompt_id = "custom_2";
config.polish_prompt.clear();
config.polish_prompt_custom_2 = "第二个自定义提示词";
REQUIRE_EQ(VoiceInput::ResolvePolishSystemPrompt(config), "第二个自定义提示词");
config.polish_prompt_custom_2.clear();
const std::string prompt = VoiceInput::ResolvePolishSystemPrompt(config);
REQUIRE(prompt.find("整理助手") != std::string::npos);
}