From 1c1140b1fd08ea06eafe824c1d6c6619a7be488c Mon Sep 17 00:00:00 2001 From: AI Bot Date: Tue, 12 May 2026 12:52:47 +0800 Subject: [PATCH] feat: support deepseek api for master agent --- .../com/hyzq/boss/AiAccountsActivity.java | 15 +++- scripts/deploy-server.sh | 2 +- src/app/api/v1/accounts/[accountId]/route.ts | 2 + src/app/api/v1/accounts/route.ts | 2 + .../api/v1/accounts/validate-draft/route.ts | 2 + src/components/ai-accounts-client.tsx | 23 ++++- src/lib/boss-data.ts | 8 ++ src/lib/boss-master-agent.ts | 89 +++++++++++++++---- src/lib/boss-projections.ts | 2 + tests/ai-account-validation.test.ts | 62 +++++++++++++ tests/master-agent-openai-fallback.test.ts | 62 ++++++++++++- 11 files changed, 246 insertions(+), 23 deletions(-) diff --git a/android/app/src/main/java/com/hyzq/boss/AiAccountsActivity.java b/android/app/src/main/java/com/hyzq/boss/AiAccountsActivity.java index 6fe2d07..95ab4f0 100644 --- a/android/app/src/main/java/com/hyzq/boss/AiAccountsActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/AiAccountsActivity.java @@ -33,6 +33,7 @@ public class AiAccountsActivity extends BossScreenActivity { private static final String PROVIDER_CHATGPT_OAUTH = "chatgpt_oauth"; private static final String PROVIDER_OPENAI_API = "openai_api"; private static final String PROVIDER_ALIYUN_QWEN_API = "aliyun_qwen_api"; + private static final String PROVIDER_DEEPSEEK_API = "deepseek_api"; private static final String PROVIDER_MINIMAX_API = "minimax_api"; private static final String PROVIDER_GLM_API = "glm_api"; private static final String PROVIDER_HYZQ_API = "hyzq_api"; @@ -41,6 +42,7 @@ public class AiAccountsActivity extends BossScreenActivity { private static final String DEFAULT_MASTER_MODEL = "gpt-5.4"; private static final String OPENAI_API_BASE_URL = "https://api.openai.com/v1"; private static final String ALIYUN_QWEN_API_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"; + private static final String DEEPSEEK_API_BASE_URL = "https://api.deepseek.com"; private static final String MINIMAX_API_BASE_URL = "https://api.minimaxi.com/v1"; private static final String GLM_API_BASE_URL = "https://open.bigmodel.cn/api/paas/v4"; private static final String HYZQ_API_BASE_URL = "https://api.hyzq2046.com/v1"; @@ -54,6 +56,7 @@ public class AiAccountsActivity extends BossScreenActivity { private static final ProviderOption[] API_OPTIONS = new ProviderOption[] { new ProviderOption(PROVIDER_ALIYUN_QWEN_API, "阿里", "阿里百炼 API", "自动填好百炼兼容地址,直接填写模型和 Key。"), + new ProviderOption(PROVIDER_DEEPSEEK_API, "DeepSeek", "DeepSeek API", "自动填好 DeepSeek 地址,可选择 V4 Pro / Flash。"), new ProviderOption(PROVIDER_MINIMAX_API, "Minimax", "MiniMax API", "自动填好 MiniMax 地址,适合接成主链路或备用链路。"), new ProviderOption(PROVIDER_GLM_API, "GLM", "智谱 GLM API", "自动填好 GLM 地址,直接复用通用 API 保存流程。"), new ProviderOption(PROVIDER_HYZQ_API, "环宇智擎", "环宇智擎 API", "自动填好环宇智擎网关地址,直接填写模型和 Key。"), @@ -466,7 +469,7 @@ public class AiAccountsActivity extends BossScreenActivity { isPrimaryRole(targetRole) ? "主链路可直接接兼容 API。" : "备用链路可接兼容 API 做兜底。", - "支持阿里、Minimax、GLM、环宇智擎和自定义。", + "支持阿里、DeepSeek、Minimax、GLM、环宇智擎和自定义。", null, null )); @@ -575,6 +578,7 @@ public class AiAccountsActivity extends BossScreenActivity { private boolean isApiProvider(String provider) { return PROVIDER_OPENAI_API.equals(provider) || PROVIDER_ALIYUN_QWEN_API.equals(provider) + || PROVIDER_DEEPSEEK_API.equals(provider) || PROVIDER_MINIMAX_API.equals(provider) || PROVIDER_GLM_API.equals(provider) || PROVIDER_HYZQ_API.equals(provider) @@ -588,6 +592,9 @@ public class AiAccountsActivity extends BossScreenActivity { if (PROVIDER_ALIYUN_QWEN_API.equals(provider)) { return ALIYUN_QWEN_API_BASE_URL; } + if (PROVIDER_DEEPSEEK_API.equals(provider)) { + return DEEPSEEK_API_BASE_URL; + } if (PROVIDER_MINIMAX_API.equals(provider)) { return MINIMAX_API_BASE_URL; } @@ -604,6 +611,9 @@ public class AiAccountsActivity extends BossScreenActivity { if (PROVIDER_ALIYUN_QWEN_API.equals(provider)) { return "qwen3.5-plus"; } + if (PROVIDER_DEEPSEEK_API.equals(provider)) { + return "deepseek-v4-pro"; + } if (PROVIDER_MINIMAX_API.equals(provider)) { return "MiniMax-M1"; } @@ -629,6 +639,9 @@ public class AiAccountsActivity extends BossScreenActivity { if (PROVIDER_ALIYUN_QWEN_API.equals(provider)) { return "阿里"; } + if (PROVIDER_DEEPSEEK_API.equals(provider)) { + return "DeepSeek"; + } if (PROVIDER_MINIMAX_API.equals(provider)) { return "Minimax"; } diff --git a/scripts/deploy-server.sh b/scripts/deploy-server.sh index c6975ff..e88445a 100755 --- a/scripts/deploy-server.sh +++ b/scripts/deploy-server.sh @@ -6,7 +6,7 @@ REMOTE_USER="${BOSS_SERVER_USER:-ubuntu}" REMOTE_HOSTNAME="${BOSS_SERVER_HOST:-106.53.170.158}" REMOTE_HOST="${BOSS_REMOTE_HOST:-${REMOTE_USER}@${REMOTE_HOSTNAME}}" REMOTE_DIR="${BOSS_REMOTE_DIR:-/opt/boss}" -SSH_OPTS="-o StrictHostKeyChecking=no" +SSH_OPTS="-o StrictHostKeyChecking=no -o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1" KEYCHAIN_SERVICE="${BOSS_KEYCHAIN_SERVICE:-boss-server-debug-ssh}" BUILD_MODE="${BOSS_DEPLOY_BUILD_MODE:-auto}" diff --git a/src/app/api/v1/accounts/[accountId]/route.ts b/src/app/api/v1/accounts/[accountId]/route.ts index 1eec0c5..7a3fbef 100644 --- a/src/app/api/v1/accounts/[accountId]/route.ts +++ b/src/app/api/v1/accounts/[accountId]/route.ts @@ -15,6 +15,7 @@ function isValidProvider( | "chatgpt_oauth" | "openai_api" | "aliyun_qwen_api" + | "deepseek_api" | "minimax_api" | "glm_api" | "hyzq_api" @@ -25,6 +26,7 @@ function isValidProvider( value === "chatgpt_oauth" || value === "openai_api" || value === "aliyun_qwen_api" || + value === "deepseek_api" || value === "minimax_api" || value === "glm_api" || value === "hyzq_api" || diff --git a/src/app/api/v1/accounts/route.ts b/src/app/api/v1/accounts/route.ts index 5bb6f2b..8c9e058 100644 --- a/src/app/api/v1/accounts/route.ts +++ b/src/app/api/v1/accounts/route.ts @@ -15,6 +15,7 @@ function isValidProvider( | "chatgpt_oauth" | "openai_api" | "aliyun_qwen_api" + | "deepseek_api" | "minimax_api" | "glm_api" | "hyzq_api" @@ -25,6 +26,7 @@ function isValidProvider( value === "chatgpt_oauth" || value === "openai_api" || value === "aliyun_qwen_api" || + value === "deepseek_api" || value === "minimax_api" || value === "glm_api" || value === "hyzq_api" || diff --git a/src/app/api/v1/accounts/validate-draft/route.ts b/src/app/api/v1/accounts/validate-draft/route.ts index 83ef7ae..1793936 100644 --- a/src/app/api/v1/accounts/validate-draft/route.ts +++ b/src/app/api/v1/accounts/validate-draft/route.ts @@ -7,6 +7,7 @@ function isValidProvider( ): value is | "openai_api" | "aliyun_qwen_api" + | "deepseek_api" | "minimax_api" | "glm_api" | "hyzq_api" @@ -14,6 +15,7 @@ function isValidProvider( return ( value === "openai_api" || value === "aliyun_qwen_api" || + value === "deepseek_api" || value === "minimax_api" || value === "glm_api" || value === "hyzq_api" || diff --git a/src/components/ai-accounts-client.tsx b/src/components/ai-accounts-client.tsx index a3148c7..f6e2b4f 100644 --- a/src/components/ai-accounts-client.tsx +++ b/src/components/ai-accounts-client.tsx @@ -71,10 +71,27 @@ function providerOptions() { return [ { value: "openai_api", label: "OpenAI API" }, { value: "aliyun_qwen_api", label: "阿里百炼 Qwen" }, + { value: "deepseek_api", label: "DeepSeek API" }, + { value: "minimax_api", label: "MiniMax API" }, + { value: "glm_api", label: "GLM API" }, + { value: "hyzq_api", label: "环宇智擎 API" }, + { value: "custom_api", label: "自定义 API" }, { value: "master_codex_node", label: "Master Codex Node / ChatGPT Plus 节点" }, ] as const; } +function isApiKeyDraftProvider(provider: AiProvider) { + return ( + provider === "openai_api" || + provider === "aliyun_qwen_api" || + provider === "deepseek_api" || + provider === "minimax_api" || + provider === "glm_api" || + provider === "hyzq_api" || + provider === "custom_api" + ); +} + function defaultOpenAiOnboardDraft(): OpenAiOnboardDraft { return { label: "主 GPT", @@ -675,7 +692,7 @@ export function AiAccountsClient({ placeholder="例如:gpt-5.4" /> )} - {draft.provider === "openai_api" || draft.provider === "aliyun_qwen_api" ? ( + {isApiKeyDraftProvider(draft.provider) ? (
diff --git a/src/lib/boss-data.ts b/src/lib/boss-data.ts index 6468dfd..1e04510 100644 --- a/src/lib/boss-data.ts +++ b/src/lib/boss-data.ts @@ -226,6 +226,7 @@ export type AiProvider = | "chatgpt_oauth" | "openai_api" | "aliyun_qwen_api" + | "deepseek_api" | "minimax_api" | "glm_api" | "hyzq_api" @@ -3354,6 +3355,8 @@ export function aiProviderLabel(provider: AiProvider) { return "OpenAI API"; case "aliyun_qwen_api": return "阿里百炼 Qwen"; + case "deepseek_api": + return "DeepSeek API"; case "minimax_api": return "MiniMax API"; case "glm_api": @@ -3373,6 +3376,8 @@ export function aiProviderDefaultApiBaseUrl(provider: AiProvider) { return "https://api.openai.com/v1"; case "aliyun_qwen_api": return "https://dashscope.aliyuncs.com/compatible-mode/v1"; + case "deepseek_api": + return "https://api.deepseek.com"; case "minimax_api": return "https://api.minimaxi.com/v1"; case "glm_api": @@ -3390,6 +3395,8 @@ export function aiProviderDefaultModel(provider: AiProvider) { return "gpt-5.4"; case "aliyun_qwen_api": return "qwen3.5-plus"; + case "deepseek_api": + return "deepseek-v4-pro"; case "minimax_api": return "MiniMax-M1"; case "glm_api": @@ -3434,6 +3441,7 @@ export function isApiKeyProvider(provider: AiProvider) { return ( provider === "openai_api" || provider === "aliyun_qwen_api" || + provider === "deepseek_api" || provider === "minimax_api" || provider === "glm_api" || provider === "hyzq_api" || diff --git a/src/lib/boss-master-agent.ts b/src/lib/boss-master-agent.ts index 0387da0..b1e3598 100644 --- a/src/lib/boss-master-agent.ts +++ b/src/lib/boss-master-agent.ts @@ -1,7 +1,6 @@ import { randomBytes } from "node:crypto"; import { AUTH_SESSION_TTL_MS, - aiRoleLabel, aiProviderLabel, appendProjectMessage, completeMasterAgentTask, @@ -70,7 +69,7 @@ const CLAW_RUNTIME_DEVICE_ID = "master-agent-claw"; type ApiCompatibleProvider = Extract< AiProvider, - "openai_api" | "aliyun_qwen_api" | "minimax_api" | "glm_api" | "hyzq_api" | "custom_api" + "openai_api" | "aliyun_qwen_api" | "deepseek_api" | "minimax_api" | "glm_api" | "hyzq_api" | "custom_api" >; const API_PROVIDER_CONFIG: Record< @@ -97,6 +96,13 @@ const API_PROVIDER_CONFIG: Record< loginLabel: "阿里百炼 API Key", protocol: "responses", }, + deepseek_api: { + label: "DeepSeek API", + defaultBaseUrl: "https://api.deepseek.com", + defaultModel: "deepseek-v4-pro", + loginLabel: "DeepSeek API Key", + protocol: "chat_completions", + }, minimax_api: { label: "MiniMax API", defaultBaseUrl: "https://api.minimaxi.com/v1", @@ -130,6 +136,7 @@ const API_PROVIDER_CONFIG: Record< const API_PROVIDER_MODEL_OPTIONS: Record = { openai_api: ["gpt-5.4-mini", "gpt-5.4", "gpt-5.1", "gpt-4.1"], aliyun_qwen_api: ["qwen3.5-plus", "qwen3.5-flash"], + deepseek_api: ["deepseek-v4-pro", "deepseek-v4-flash"], minimax_api: ["MiniMax-M1"], glm_api: ["glm-4.5"], hyzq_api: ["gpt-5.4-mini", "gpt-5.4"], @@ -139,6 +146,7 @@ const API_PROVIDER_MODEL_OPTIONS: Record = { const API_EXECUTION_PROVIDER_PRIORITY: ApiCompatibleProvider[] = [ "hyzq_api", "openai_api", + "deepseek_api", "aliyun_qwen_api", "glm_api", "minimax_api", @@ -528,7 +536,7 @@ export async function resolveMasterAgentExecutionConfig( const state = await readState(); const resolvedAccountId = accountId?.trim() || state.user.account || runtime.account.accountId; const scopedAgentControls = await getProjectAgentControls(projectId, resolvedAccountId); - const modeResolution = resolveMasterAgentExecutionMode(scopedAgentControls, requestText); + const modeResolution = resolveMasterAgentExecutionMode(scopedAgentControls, requestText, runtime.account); const reasoningEffort = modeResolution.effectiveReasoningEffort || (runtime.account as typeof runtime.account & { reasoningEffort?: ReasoningEffort }).reasoningEffort || @@ -584,12 +592,44 @@ function normalizeAgentControlText(value?: string | null) { return trimmed ? trimmed : undefined; } -function resolveConfiguredFastModel(agentControls?: ProjectAgentControls | null) { - return normalizeAgentControlText(agentControls?.fastModelOverride) || DEFAULT_FAST_MODEL; +function isProviderCompatibleModel(model: string | undefined, account?: AiAccount | null) { + if (!model || !account || !isApiCompatibleProvider(account.provider)) { + return true; + } + if (account.provider !== "deepseek_api") { + return true; + } + const modelOptions = API_PROVIDER_MODEL_OPTIONS[account.provider]; + return modelOptions.length === 0 || modelOptions.includes(model); } -function resolveConfiguredDeepModel(agentControls?: ProjectAgentControls | null) { - return normalizeAgentControlText(agentControls?.deepModelOverride) || DEFAULT_DEEP_MODEL; +function normalizeAgentControlModelForAccount(value?: string | null, account?: AiAccount | null) { + const model = normalizeAgentControlText(value); + return isProviderCompatibleModel(model, account) ? model : undefined; +} + +function defaultFastModelForAccount(account?: AiAccount | null) { + if (account && isApiCompatibleProvider(account.provider)) { + if (account.provider === "deepseek_api") return "deepseek-v4-flash"; + if (account.provider === "aliyun_qwen_api") return "qwen3.5-flash"; + return apiProviderConfig(account.provider).defaultModel; + } + return DEFAULT_FAST_MODEL; +} + +function defaultDeepModelForAccount(account?: AiAccount | null) { + if (account && isApiCompatibleProvider(account.provider)) { + return account.model || apiProviderConfig(account.provider).defaultModel; + } + return DEFAULT_DEEP_MODEL; +} + +function resolveConfiguredFastModel(agentControls?: ProjectAgentControls | null, account?: AiAccount | null) { + return normalizeAgentControlModelForAccount(agentControls?.fastModelOverride, account) || defaultFastModelForAccount(account); +} + +function resolveConfiguredDeepModel(agentControls?: ProjectAgentControls | null, account?: AiAccount | null) { + return normalizeAgentControlModelForAccount(agentControls?.deepModelOverride, account) || defaultDeepModelForAccount(account); } export function shouldAutoEscalateMasterAgentRequest(requestText?: string | null) { @@ -623,12 +663,21 @@ export function shouldAutoEscalateMasterAgentRequest(requestText?: string | null function resolveMasterAgentExecutionMode( agentControls?: ProjectAgentControls | null, requestText?: string | null, + account?: AiAccount | null, ): MasterAgentExecutionModeResolution { const storedAgentControls = agentControls ?? null; - const currentModelOverride = normalizeAgentControlText(agentControls?.modelOverride); + const currentModelOverride = normalizeAgentControlModelForAccount(agentControls?.modelOverride, account); const currentReasoningEffort = agentControls?.reasoningEffortOverride; - const fastModelOverride = resolveConfiguredFastModel(agentControls); - const deepModelOverride = resolveConfiguredDeepModel(agentControls); + const fastModelOverride = resolveConfiguredFastModel(agentControls, account); + const deepModelOverride = resolveConfiguredDeepModel(agentControls, account); + const effectiveStoredAgentControls: ProjectAgentControls | null = agentControls + ? { + ...agentControls, + modelOverride: currentModelOverride, + fastModelOverride, + deepModelOverride, + } + : null; let activeMode: MasterAgentExecutionModeResolution["activeMode"] = "custom"; if (!currentModelOverride && !currentReasoningEffort) { @@ -663,7 +712,7 @@ function resolveMasterAgentExecutionMode( return { storedAgentControls, - effectiveAgentControls: storedAgentControls, + effectiveAgentControls: effectiveStoredAgentControls, activeMode, effectiveMode: activeMode, fastPathEligible: activeMode === "fast", @@ -1616,6 +1665,7 @@ function isApiCompatibleProvider(provider: AiProvider): provider is ApiCompatibl return ( provider === "openai_api" || provider === "aliyun_qwen_api" || + provider === "deepseek_api" || provider === "minimax_api" || provider === "glm_api" || provider === "hyzq_api" || @@ -2263,7 +2313,7 @@ async function queueAndStartOpenAiMasterAgentReply(params: { taskId: params.taskId, deviceId: candidate.deviceId, accountId: candidate.account.accountId, - accountLabel: candidate.account.label, + accountLabel: candidate.model, }); } @@ -2392,7 +2442,7 @@ async function enqueueOpenAiMasterAgentReply(params: { requestedByAccount: params.requestedByAccount, deviceId: primaryCandidate.deviceId, accountId: primaryCandidate.account.accountId, - accountLabel: primaryCandidate.account.label, + accountLabel: primaryCandidate.model, ...params.taskAuthorization, relayViaMasterAgent: params.relayViaMasterAgent, externalReplyTarget: params.externalReplyTarget, @@ -3683,6 +3733,8 @@ export async function replyToMasterAgentUserMessage(params: { const replyMetadata = buildMasterAgentModeMetadata(modeResolution); const controlIntent = classifyMasterAgentControlIntent(params.requestText); const relayViaMasterAgent = params.interactionMode === "takeover_single_thread"; + const forcePrimaryApiExecution = + isApiCompatibleProvider(runtime.account.provider) && Boolean(runtime.account.apiKey?.trim()); const selectedMasterAccount = await resolveMasterNodeExecutionCandidate({ backendChoices, runtimeAccount: runtime.account, @@ -3700,13 +3752,16 @@ export async function replyToMasterAgentUserMessage(params: { modeResolution, backendOverride: executionConfig.agentControls?.backendOverride, }); + const selectedReplyBackendId = forcePrimaryApiExecution && apiExecutionCandidates.length > 0 + ? "openai-api" + : selectedBackend.backendId; const replyMode = resolveMasterAgentReplyMode({ requestedMode: params.mode, selectedBackendId: preferApiExecutionForSmartMode && apiExecutionCandidates.length > 0 ? apiExecutionCandidates[0]?.provider === "aliyun_qwen_api" ? "aliyun-qwen" : "openai-api" - : selectedBackend.backendId, + : selectedReplyBackendId, apiCandidateCount: apiExecutionCandidates.length, modeResolution, }); @@ -4019,7 +4074,7 @@ export async function replyToMasterAgentUserMessage(params: { if ( apiExecutionCandidates.length > 0 && - (preferApiExecutionForSmartMode || selectedBackend.backendId !== "master-codex-node") + (forcePrimaryApiExecution || preferApiExecutionForSmartMode || selectedBackend.backendId !== "master-codex-node") ) { const queuedReply = await enqueueOpenAiMasterAgentReply({ candidates: apiExecutionCandidates, @@ -4080,7 +4135,7 @@ export async function replyToMasterAgentUserMessage(params: { } } - if (selectedBackend.backendId === "master-codex-node" && !preferApiExecutionForSmartMode) { + if (selectedBackend.backendId === "master-codex-node" && !preferApiExecutionForSmartMode && !forcePrimaryApiExecution) { return runMasterNodeExecution(); } @@ -4093,7 +4148,7 @@ export async function replyToMasterAgentUserMessage(params: { requestText: params.requestText, projectId: replyProjectId, currentSessionExpiresAt: params.currentSessionExpiresAt, - senderLabel: `主 Agent · ${candidate.account.label || aiRoleLabel(candidate.account.role)}`, + senderLabel: `主 Agent · ${candidate.model}`, agentControls, promptPolicy: executionConfig.promptPolicy, userPrompt: executionConfig.userPrompt, diff --git a/src/lib/boss-projections.ts b/src/lib/boss-projections.ts index aa08d1e..109c3c5 100644 --- a/src/lib/boss-projections.ts +++ b/src/lib/boss-projections.ts @@ -363,6 +363,8 @@ function aiProviderLabel(provider: AiProvider) { return "OpenAI API"; case "aliyun_qwen_api": return "阿里百炼 Qwen"; + case "deepseek_api": + return "DeepSeek API"; default: return provider; } diff --git a/tests/ai-account-validation.test.ts b/tests/ai-account-validation.test.ts index 11d5f5f..da07f7f 100644 --- a/tests/ai-account-validation.test.ts +++ b/tests/ai-account-validation.test.ts @@ -249,6 +249,68 @@ test("validateAiAccountConnection probes GLM accounts through chat completions", } }); +test("validateAiAccountConnection probes DeepSeek V4 accounts through chat completions", async () => { + await setup(); + + await saveAiAccount({ + accountId: "deepseek-primary", + label: "主要API", + role: "primary", + provider: "deepseek_api", + displayName: "DeepSeek V4 主链路", + accountIdentifier: "deepseek-demo", + model: "deepseek-v4-pro", + apiKey: "sk-deepseek-demo-123456", + enabled: true, + }); + + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + if (typeof input === "string" && input === "https://api.deepseek.com/chat/completions") { + return new Response(JSON.stringify({ + choices: [ + { + message: { + content: "连接正常", + }, + }, + ], + }), { + status: 200, + headers: { + "content-type": "application/json", + "x-request-id": "req-deepseek-validate", + }, + }); + } + if (typeof input === "string" && input === "https://api.deepseek.com/models") { + return new Response(JSON.stringify({ + data: [ + { id: "deepseek-v4-pro" }, + { id: "deepseek-v4-flash" }, + ], + }), { + status: 200, + headers: { + "content-type": "application/json", + }, + }); + } + throw new Error(`unexpected fetch: ${String(input)}`); + }) as typeof fetch; + + try { + const result = await validateAiAccountConnection("deepseek-primary"); + assert.equal(result.ok, true); + assert.equal(result.status, "ready"); + assert.equal(result.requestId, "req-deepseek-validate"); + assert.match(result.message, /连接正常/); + assert.deepEqual(result.availableModels, ["deepseek-v4-pro", "deepseek-v4-flash"]); + } finally { + globalThis.fetch = originalFetch; + } +}); + test("validateAiAccountConnection falls back to generic models for custom api accounts", async () => { await setup(); diff --git a/tests/master-agent-openai-fallback.test.ts b/tests/master-agent-openai-fallback.test.ts index 8b8853b..8bbe444 100644 --- a/tests/master-agent-openai-fallback.test.ts +++ b/tests/master-agent-openai-fallback.test.ts @@ -113,7 +113,7 @@ test("replyToMasterAgentUserMessage falls back to a runnable OpenAI API account const reply = masterProject?.messages.at(-1); assert.ok(reply, "expected a master-agent reply to be appended"); assert.equal(reply?.sender, "master"); - assert.equal(reply?.senderLabel, "主 Agent · 备用 GPT"); + assert.equal(reply?.senderLabel, "主 Agent · gpt-5.4"); assert.match(reply?.body ?? "", /主Agent链路正常/); } finally { globalThis.fetch = originalFetch; @@ -194,6 +194,64 @@ test("replyToMasterAgentUserMessage can retry the same degraded API account when } }); +test("replyToMasterAgentUserMessage uses active DeepSeek API accounts directly", async () => { + await saveAiAccount({ + accountId: "deepseek-primary", + label: "DeepSeek 主控", + role: "primary", + provider: "deepseek_api", + displayName: "DeepSeek V4 主链路", + model: "deepseek-v4-pro", + apiKey: "sk-deepseek-demo-123456", + enabled: true, + setActive: true, + loginStatusNote: "DeepSeek API 主链路。", + }); + + const originalFetch = globalThis.fetch; + globalThis.fetch = (async (input) => { + if (typeof input === "string" && input === "https://api.deepseek.com/chat/completions") { + return new Response(JSON.stringify({ + choices: [ + { + message: { + content: "DeepSeek 主链路正常。", + }, + }, + ], + }), { + status: 200, + headers: { + "content-type": "application/json", + "x-request-id": "req-deepseek-primary", + }, + }); + } + throw new Error(`unexpected fetch: ${String(input)}`); + }) as typeof fetch; + + try { + const result = await replyToMasterAgentUserMessage({ + requestMessageId: "msg-deepseek-primary", + requestText: "请只回复:DeepSeek 主链路正常。", + requestedBy: "Boss 超级管理员", + requestedByAccount: "krisolo", + mode: "wait", + }); + + assert.equal(result.ok, true); + assert.equal(result.accountId, "deepseek-primary"); + assert.equal(result.requestId, "req-deepseek-primary"); + + const state = await readState(); + const reply = state.projects.find((project) => project.id === "master-agent")?.messages.at(-1); + assert.equal(reply?.senderLabel, "主 Agent · deepseek-v4-pro"); + assert.match(reply?.body ?? "", /DeepSeek 主链路正常/); + } finally { + globalThis.fetch = originalFetch; + } +}); + test("replyToMasterAgentUserMessage falls back to a runnable aliyun qwen backup account when the master node is offline", async () => { await saveAiAccount({ accountId: "master-codex-primary", @@ -254,7 +312,7 @@ test("replyToMasterAgentUserMessage falls back to a runnable aliyun qwen backup const reply = masterProject?.messages.at(-1); assert.ok(reply, "expected a master-agent reply to be appended"); assert.equal(reply?.sender, "master"); - assert.equal(reply?.senderLabel, "主 Agent · 阿里备用"); + assert.equal(reply?.senderLabel, "主 Agent · qwen3.5-plus"); assert.match(reply?.body ?? "", /阿里备用链路正常/); } finally { globalThis.fetch = originalFetch;