feat: add aliyun qwen backup provider

This commit is contained in:
kris
2026-04-01 05:33:35 +08:00
parent ba01ae5393
commit 60d69eb222
15 changed files with 708 additions and 87 deletions

View File

@@ -6,8 +6,8 @@ function isValidRole(value: string): value is "primary" | "backup" | "api_fallba
return value === "primary" || value === "backup" || value === "api_fallback";
}
function isValidProvider(value: string): value is "master_codex_node" | "openai_api" {
return value === "master_codex_node" || value === "openai_api";
function isValidProvider(value: string): value is "master_codex_node" | "openai_api" | "aliyun_qwen_api" {
return value === "master_codex_node" || value === "openai_api" || value === "aliyun_qwen_api";
}
export async function GET(

View File

@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { getMasterIdentitySummaryFromState, readState, saveAiAccount, updateAiAccountHealth } from "@/lib/boss-data";
import { probeApiCompatibleAccount } from "@/lib/boss-master-agent";
function chooseAliyunBackupAccountId(state: Awaited<ReturnType<typeof readState>>) {
return (
state.aiAccounts.find((item) => item.provider === "aliyun_qwen_api" && item.role === "backup")?.accountId ||
"aliyun-qwen-backup"
);
}
export async function POST(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const body = (await request.json().catch(() => ({}))) as {
label?: string;
displayName?: string;
accountIdentifier?: string;
model?: string;
apiKey?: string;
};
if (!body.displayName?.trim()) {
return NextResponse.json({ ok: false, message: "显示名称不能为空。" }, { status: 400 });
}
if (!body.apiKey?.trim()) {
return NextResponse.json({ ok: false, message: "请先填写阿里百炼 API Key。" }, { status: 400 });
}
try {
const probe = await probeApiCompatibleAccount({
provider: "aliyun_qwen_api",
apiKey: body.apiKey,
model: body.model,
});
const state = await readState();
const accountId = chooseAliyunBackupAccountId(state);
const account = await saveAiAccount({
accountId,
label: body.label?.trim() || "备用 GPT",
role: "backup",
provider: "aliyun_qwen_api",
displayName: body.displayName.trim(),
accountIdentifier: body.accountIdentifier?.trim() || undefined,
model: probe.model,
apiKey: body.apiKey.trim(),
enabled: true,
setActive: false,
loginStatusNote: "已接入阿里百炼 Qwen 兼容接口,可作为主 Agent 的备用模型链路。",
});
await updateAiAccountHealth({
accountId: account.accountId,
status: "ready",
lastError: undefined,
lastValidatedAt: new Date().toISOString(),
});
const nextState = await readState();
return NextResponse.json({
ok: true,
accountId: account.accountId,
account,
activeIdentity: getMasterIdentitySummaryFromState(nextState),
requestId: probe.requestId,
message: "阿里百炼备用账号已接入,可作为主 Agent 的备用链路。",
});
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "ALIYUN_QWEN_ONBOARD_FAILED" },
{ status: 400 },
);
}
}

View File

@@ -6,8 +6,8 @@ function isValidRole(value: string): value is "primary" | "backup" | "api_fallba
return value === "primary" || value === "backup" || value === "api_fallback";
}
function isValidProvider(value: string): value is "master_codex_node" | "openai_api" {
return value === "master_codex_node" || value === "openai_api";
function isValidProvider(value: string): value is "master_codex_node" | "openai_api" | "aliyun_qwen_api" {
return value === "master_codex_node" || value === "openai_api" || value === "aliyun_qwen_api";
}
export async function GET(request: NextRequest) {

View File

@@ -26,7 +26,7 @@ type AccountDraft = {
loginStatusNote: string;
};
type OnboardingMode = "openai_api" | "master_codex_node" | null;
type OnboardingMode = "openai_api" | "aliyun_qwen_api" | "master_codex_node" | null;
type OpenAiOnboardDraft = {
label: string;
@@ -36,6 +36,14 @@ type OpenAiOnboardDraft = {
apiKey: string;
};
type AliyunQwenOnboardDraft = {
label: string;
displayName: string;
accountIdentifier: string;
model: string;
apiKey: string;
};
type MasterNodeOnboardDraft = {
label: string;
displayName: string;
@@ -56,6 +64,7 @@ function roleOptions() {
function providerOptions() {
return [
{ value: "openai_api", label: "OpenAI API" },
{ value: "aliyun_qwen_api", label: "阿里百炼 Qwen" },
{ value: "master_codex_node", label: "Master Codex Node / ChatGPT Plus 节点" },
] as const;
}
@@ -70,6 +79,16 @@ function defaultOpenAiOnboardDraft(): OpenAiOnboardDraft {
};
}
function defaultAliyunQwenOnboardDraft(): AliyunQwenOnboardDraft {
return {
label: "备用 GPT",
displayName: "阿里百炼备用账号",
accountIdentifier: "",
model: "qwen3.5-plus",
apiKey: "",
};
}
function defaultMasterNodeOnboardDraft(): MasterNodeOnboardDraft {
return {
label: "主 GPT",
@@ -184,6 +203,8 @@ export function AiAccountsClient({
const [guideAccountId, setGuideAccountId] = useState<string | null>(null);
const [onboardingMode, setOnboardingMode] = useState<OnboardingMode>(null);
const [openAiOnboardDraft, setOpenAiOnboardDraft] = useState<OpenAiOnboardDraft>(defaultOpenAiOnboardDraft());
const [aliyunQwenOnboardDraft, setAliyunQwenOnboardDraft] =
useState<AliyunQwenOnboardDraft>(defaultAliyunQwenOnboardDraft());
const [masterNodeOnboardDraft, setMasterNodeOnboardDraft] = useState<MasterNodeOnboardDraft>(
defaultMasterNodeOnboardDraft(),
);
@@ -296,6 +317,37 @@ export function AiAccountsClient({
setMessage(result.message || "OpenAI 平台账号登录失败。");
}
async function submitAliyunQwenOnboarding() {
const draft = aliyunQwenOnboardDraft;
if (!draft.displayName.trim() || !draft.apiKey.trim()) {
setMessage("请先填写阿里百炼备用账号名称和 API Key。");
return;
}
setBusyKey("onboard:aliyun_qwen_api");
const response = await fetch("/api/v1/accounts/onboard/aliyun-qwen", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
label: draft.label.trim() || "备用 GPT",
displayName: draft.displayName.trim(),
accountIdentifier: draft.accountIdentifier.trim(),
model: draft.model.trim() || "qwen3.5-plus",
apiKey: draft.apiKey.trim(),
}),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
if (result.ok) {
setMessage(result.message || "阿里百炼备用账号已接入。");
setAliyunQwenOnboardDraft(defaultAliyunQwenOnboardDraft());
closeOnboarding();
router.refresh();
return;
}
setMessage(result.message || "阿里百炼备用账号接入失败。");
}
async function submitMasterNodeOnboarding() {
const draft = masterNodeOnboardDraft;
if (!draft.displayName.trim() || !draft.nodeId.trim()) {
@@ -330,7 +382,9 @@ export function AiAccountsClient({
}
function onboardingTitle() {
return onboardingMode === "openai_api" ? "登录 OpenAI 平台账号" : "绑定电脑上的 Codex 节点";
if (onboardingMode === "openai_api") return "登录 OpenAI 平台账号";
if (onboardingMode === "aliyun_qwen_api") return "接入阿里百炼备用账号";
return "绑定电脑上的 Codex 节点";
}
return (
@@ -399,6 +453,30 @@ export function AiAccountsClient({
API Key
</div>
</button>
<button
type="button"
onClick={() => {
setOnboardingMode("aliyun_qwen_api");
setMessage("");
}}
disabled={!canManage}
className="rounded-3xl border border-[#E5E5EA] bg-gradient-to-br from-[#FFF8EE] to-white p-4 text-left shadow-sm transition hover:-translate-y-0.5 disabled:cursor-not-allowed disabled:opacity-60"
>
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-2 text-[12px] leading-6 text-[#57606A]">
Agent qwen3.5-plus
</div>
</div>
<span className="rounded-full bg-[#FFF5E8] px-3 py-1 text-[11px] font-semibold text-[#B54708]">
</span>
</div>
<div className="mt-3 rounded-2xl bg-white/80 px-3 py-3 text-[12px] leading-6 text-[#57606A]">
API Key
</div>
</button>
<button
type="button"
onClick={() => {
@@ -545,7 +623,7 @@ export function AiAccountsClient({
onChange={(value) => updateDraft(account.accountId, (current) => ({ ...current, model: value }))}
placeholder="例如gpt-5.4"
/>
{draft.provider === "openai_api" ? (
{draft.provider === "openai_api" || draft.provider === "aliyun_qwen_api" ? (
<div className="col-span-2">
<AccountField
label={`API Key${account.apiKeyMasked ? `(已配置 ${account.apiKeyMasked}` : ""}`}
@@ -553,7 +631,13 @@ export function AiAccountsClient({
onChange={(value) =>
updateDraft(account.accountId, (current) => ({ ...current, apiKey: value }))
}
placeholder={account.apiKeyConfigured ? "留空则保持现有 Key" : "输入 OpenAI API Key"}
placeholder={
account.apiKeyConfigured
? "留空则保持现有 Key"
: draft.provider === "aliyun_qwen_api"
? "输入阿里百炼 API Key"
: "输入 OpenAI API Key"
}
type="password"
/>
</div>
@@ -679,6 +763,8 @@ export function AiAccountsClient({
<div className="mt-1 text-[12px] leading-6 text-[#8C8C8C]">
{onboardingMode === "openai_api"
? "填写 API Key 后会立即校验,并将这个账号设为当前主控。"
: onboardingMode === "aliyun_qwen_api"
? "填写阿里百炼 API Key 后会立即校验,并保存为备用模型链路。"
: "这个入口只负责绑定电脑节点,真正登录仍发生在那台电脑上。"}
</div>
</div>
@@ -746,6 +832,63 @@ export function AiAccountsClient({
</button>
</div>
</div>
) : onboardingMode === "aliyun_qwen_api" ? (
<div className="mt-4 space-y-3">
<AccountField
label="标签"
value={aliyunQwenOnboardDraft.label}
onChange={(value) => setAliyunQwenOnboardDraft((current) => ({ ...current, label: value }))}
placeholder="例如:备用 GPT"
/>
<AccountField
label="显示名称"
value={aliyunQwenOnboardDraft.displayName}
onChange={(value) =>
setAliyunQwenOnboardDraft((current) => ({ ...current, displayName: value }))
}
placeholder="例如:阿里百炼备用账号"
/>
<AccountField
label="账号标识"
value={aliyunQwenOnboardDraft.accountIdentifier}
onChange={(value) =>
setAliyunQwenOnboardDraft((current) => ({ ...current, accountIdentifier: value }))
}
placeholder="例如dashscope 账号备注"
/>
<AccountField
label="模型"
value={aliyunQwenOnboardDraft.model}
onChange={(value) =>
setAliyunQwenOnboardDraft((current) => ({ ...current, model: value }))
}
placeholder="例如qwen3.5-plus"
/>
<AccountField
label="API Key"
value={aliyunQwenOnboardDraft.apiKey}
onChange={(value) => setAliyunQwenOnboardDraft((current) => ({ ...current, apiKey: value }))}
placeholder="输入阿里百炼 API Key"
type="password"
/>
<div className="flex items-center gap-3 pt-1">
<button
type="button"
onClick={submitAliyunQwenOnboarding}
disabled={!canManage || busyKey === "onboard:aliyun_qwen_api"}
className="flex-1 rounded-full bg-[#111111] px-4 py-3 text-[13px] font-semibold text-white disabled:opacity-50"
>
{busyKey === "onboard:aliyun_qwen_api" ? "接入中" : "接入备用账号"}
</button>
<button
type="button"
onClick={closeOnboarding}
className="rounded-full border border-[#E5E5EA] px-4 py-3 text-[13px] font-semibold text-[#57606A]"
>
</button>
</div>
</div>
) : (
<div className="mt-4 space-y-3">
<AccountField

View File

@@ -127,7 +127,7 @@ export type LoginMethod = "password" | "code";
export type OtaUpdateStatus = "available" | "scheduled" | "applied" | "skipped";
export type OtaLogStatus = "checked" | "applied" | "skipped";
export type AppLogLevel = "info" | "warn" | "error";
export type AiProvider = "master_codex_node" | "openai_api";
export type AiProvider = "master_codex_node" | "openai_api" | "aliyun_qwen_api";
export type AiAccountRole = "primary" | "backup" | "api_fallback";
export type AiAccountStatus = "ready" | "needs_login" | "needs_api_key" | "degraded" | "disabled";
export type MasterAgentTaskStatus = "queued" | "running" | "completed" | "failed";
@@ -2254,6 +2254,8 @@ export function aiProviderLabel(provider: AiProvider) {
return "Master Codex Node / ChatGPT Plus 节点";
case "openai_api":
return "OpenAI API";
case "aliyun_qwen_api":
return "阿里百炼 Qwen";
default:
return provider;
}
@@ -2283,9 +2285,13 @@ function maskApiKey(value?: string) {
return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
}
function isApiKeyProvider(provider: AiProvider) {
return provider === "openai_api" || provider === "aliyun_qwen_api";
}
function deriveAiAccountStatus(account: AiAccount): AiAccountStatus {
if (!account.enabled) return "disabled";
if (account.provider === "openai_api") {
if (isApiKeyProvider(account.provider)) {
if (!account.apiKey?.trim()) return "needs_api_key";
return account.status === "disabled" ? "ready" : account.status;
}
@@ -2458,7 +2464,7 @@ export function getMasterIdentitySummaryFromState(state: BossState): MasterIdent
status: "needs_api_key",
statusLabel: aiStatusLabel("needs_api_key"),
canGenerate: false,
note: "请到“我的 > AI 账号”至少配置一个可用的 Master Codex NodeOpenAI API 账号。",
note: "请到“我的 > AI 账号”至少配置一个可用的 Master Codex NodeOpenAI API 或阿里百炼 Qwen 账号。",
};
}
@@ -4662,6 +4668,12 @@ export async function saveAiAccount(payload: {
existing?.accountId ??
payload.accountId?.trim() ??
`ai-${slugify(`${payload.label}-${payload.displayName}`)}`;
const defaultModel =
payload.provider === "aliyun_qwen_api"
? "qwen3.5-plus"
: payload.provider === "openai_api"
? "gpt-5.4"
: undefined;
const next: AiAccount = normalizeAiAccount({
accountId,
label: payload.label.trim() || aiRoleLabel(payload.role),
@@ -4671,21 +4683,21 @@ export async function saveAiAccount(payload: {
accountIdentifier: payload.accountIdentifier?.trim() || undefined,
nodeId: payload.nodeId?.trim() || undefined,
nodeLabel: payload.nodeLabel?.trim() || undefined,
model: payload.model?.trim() || (payload.provider === "openai_api" ? "gpt-5.4" : undefined),
model: payload.model?.trim() || defaultModel,
apiKey:
payload.provider === "openai_api"
isApiKeyProvider(payload.provider)
? payload.apiKey?.trim()
? payload.apiKey.trim()
: existing?.apiKey
: undefined,
apiKeyMasked:
payload.provider === "openai_api"
isApiKeyProvider(payload.provider)
? maskApiKey(payload.apiKey?.trim() || existing?.apiKey)
: undefined,
enabled: payload.enabled ?? existing?.enabled ?? true,
isActive: existing?.isActive ?? false,
status:
payload.provider === "openai_api"
isApiKeyProvider(payload.provider)
? payload.apiKey?.trim() || existing?.apiKey
? existing?.status === "degraded"
? "degraded"

View File

@@ -19,7 +19,7 @@ import {
updateAttachmentAnalysisResult,
updateAiAccountHealth,
} from "@/lib/boss-data";
import type { DispatchPlanTarget, Project, ProjectAgentControls, ReasoningEffort } from "@/lib/boss-data";
import type { AiProvider, DispatchPlanTarget, Project, ProjectAgentControls, ReasoningEffort } from "@/lib/boss-data";
import { canInlineAttachmentText, extractAttachmentTextExcerpt } from "@/lib/boss-attachments";
import { readAliyunOssObjectBuffer } from "@/lib/boss-storage-aliyun-oss";
import { readServerFileAttachmentBuffer } from "@/lib/boss-storage-server-file";
@@ -31,6 +31,32 @@ import {
type MasterAgentReplyState = "queued" | "running" | "completed";
const OPENAI_MASTER_AGENT_DEVICE_ID = "master-agent-openai";
const ALIYUN_QWEN_DEVICE_ID = "master-agent-aliyun-qwen";
type ApiCompatibleProvider = Extract<AiProvider, "openai_api" | "aliyun_qwen_api">;
const API_PROVIDER_CONFIG: Record<
ApiCompatibleProvider,
{
label: string;
endpoint: string;
defaultModel: string;
loginLabel: string;
}
> = {
openai_api: {
label: "OpenAI API",
endpoint: "https://api.openai.com/v1/responses",
defaultModel: "gpt-5.4",
loginLabel: "OpenAI API Key",
},
aliyun_qwen_api: {
label: "阿里百炼 Qwen",
endpoint: "https://dashscope.aliyuncs.com/compatible-mode/v1/responses",
defaultModel: "qwen3.5-plus",
loginLabel: "阿里百炼 API Key",
},
};
type QueuedMasterAgentReplyEnvelope = {
ok: true;
@@ -378,6 +404,51 @@ function normalizeOpenAiFetchFailure(error: unknown) {
return normalizeOpenAiError(String(error));
}
function isApiCompatibleProvider(provider: AiProvider): provider is ApiCompatibleProvider {
return provider === "openai_api" || provider === "aliyun_qwen_api";
}
function apiProviderConfig(provider: ApiCompatibleProvider) {
return API_PROVIDER_CONFIG[provider];
}
function normalizeApiProviderError(provider: ApiCompatibleProvider, message: string) {
if (provider === "openai_api") {
return normalizeOpenAiError(message);
}
const trimmed = message.trim();
const lowered = trimmed.toLowerCase();
if (
lowered.includes("network is unreachable") ||
lowered.includes("enetunreach") ||
lowered.includes("timed out") ||
lowered.includes("fetch failed") ||
lowered.includes("connect timeout")
) {
return "服务器当前无法连接阿里百炼兼容接口,请检查出网、代理或防火墙配置。";
}
if (!trimmed) return "主 Agent 当前调用阿里百炼模型失败。";
if (trimmed.length <= 240) return trimmed;
return `${trimmed.slice(0, 237)}...`;
}
function normalizeApiProviderFetchFailure(provider: ApiCompatibleProvider, error: unknown) {
if (provider === "openai_api") {
return normalizeOpenAiFetchFailure(error);
}
if (error instanceof Error) {
const causeCode =
typeof (error as Error & { cause?: { code?: string } }).cause?.code === "string"
? (error as Error & { cause?: { code?: string } }).cause?.code
: "";
const causeMessage =
(error as Error & { cause?: { message?: string } }).cause?.message?.trim() || "";
return normalizeApiProviderError(provider, [error.message, causeCode, causeMessage].filter(Boolean).join(" "));
}
return normalizeApiProviderError(provider, String(error));
}
function fallbackAiRolePriority(role: "primary" | "backup" | "api_fallback") {
switch (role) {
case "primary":
@@ -391,14 +462,14 @@ function fallbackAiRolePriority(role: "primary" | "backup" | "api_fallback") {
}
}
async function findFallbackOpenAiAccount(excludedAccountId?: string) {
async function findFallbackApiAccount(excludedAccountId?: string) {
const state = await readState();
return [...state.aiAccounts]
.filter(
(account) =>
account.accountId !== excludedAccountId &&
account.enabled &&
account.provider === "openai_api" &&
isApiCompatibleProvider(account.provider) &&
Boolean(account.apiKey?.trim()),
)
.sort((left, right) => {
@@ -409,7 +480,7 @@ async function findFallbackOpenAiAccount(excludedAccountId?: string) {
}
async function replyViaOpenAiAccount(params: {
account: Awaited<ReturnType<typeof findFallbackOpenAiAccount>>;
account: Awaited<ReturnType<typeof findFallbackApiAccount>>;
requestText: string;
currentSessionExpiresAt?: string;
senderLabel: string;
@@ -419,13 +490,17 @@ async function replyViaOpenAiAccount(params: {
projectMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
userMemories?: Awaited<ReturnType<typeof listUserMasterMemoriesView>>;
}) {
if (!params.account?.apiKey?.trim()) {
if (!params.account?.apiKey?.trim() || !isApiCompatibleProvider(params.account.provider)) {
throw new Error("OPENAI_ACCOUNT_NOT_CONFIGURED");
}
const generated = await generateOpenAiReply({
const generated = await generateApiProviderReply({
provider: params.account.provider,
apiKey: params.account.apiKey,
model: params.agentControls?.modelOverride || params.account.model || "gpt-5.4",
model:
params.agentControls?.modelOverride ||
params.account.model ||
apiProviderConfig(params.account.provider).defaultModel,
reasoningEffort: params.agentControls?.reasoningEffortOverride || "medium",
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
@@ -451,7 +526,8 @@ async function replyViaOpenAiAccount(params: {
};
}
async function generateOpenAiReply(params: {
async function generateApiProviderReply(params: {
provider: ApiCompatibleProvider;
apiKey: string;
model: string;
reasoningEffort: ReasoningEffort;
@@ -475,34 +551,38 @@ async function generateOpenAiReply(params: {
params.requestText,
);
let response: Response;
const config = apiProviderConfig(params.provider);
const requestBody: Record<string, unknown> = {
model: params.model,
instructions: buildMasterAgentExecutionPrompt({
state,
projectId: "master-agent",
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls: params.agentControls,
accountId: "master-agent",
promptPolicy: params.promptPolicy ?? null,
userPrompt: params.userPrompt ?? null,
projectMemories: effectiveProjectMemories,
userMemories: params.userMemories ?? [],
}),
input: params.requestText,
};
if (params.provider === "openai_api") {
requestBody.reasoning = { effort: params.reasoningEffort };
}
try {
response = await fetch("https://api.openai.com/v1/responses", {
response = await fetch(config.endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${params.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: params.model,
reasoning: { effort: params.reasoningEffort },
instructions: buildMasterAgentExecutionPrompt({
state,
projectId: "master-agent",
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls: params.agentControls,
accountId: "master-agent",
promptPolicy: params.promptPolicy ?? null,
userPrompt: params.userPrompt ?? null,
projectMemories: effectiveProjectMemories,
userMemories: params.userMemories ?? [],
}),
input: params.requestText,
}),
body: JSON.stringify(requestBody),
signal: AbortSignal.timeout(45_000),
});
} catch (error) {
throw new Error(normalizeOpenAiFetchFailure(error));
throw new Error(normalizeApiProviderFetchFailure(params.provider, error));
}
const requestId = response.headers.get("x-request-id") ?? undefined;
@@ -516,8 +596,9 @@ async function generateOpenAiReply(params: {
? payload.error?.message
: undefined;
throw new Error(
normalizeOpenAiError(
`${apiError ?? `OpenAI API ${response.status}`}${requestId ? ` (request_id=${requestId})` : ""}`,
normalizeApiProviderError(
params.provider,
`${apiError ?? `${config.label} ${response.status}`}${requestId ? ` (request_id=${requestId})` : ""}`,
),
);
}
@@ -525,7 +606,8 @@ async function generateOpenAiReply(params: {
const content = extractResponseText(payload);
if (!content) {
throw new Error(
normalizeOpenAiError(
normalizeApiProviderError(
params.provider,
`模型已返回成功状态,但没有可用文本输出${requestId ? ` (request_id=${requestId})` : ""}`,
),
);
@@ -562,6 +644,7 @@ function buildMasterOpenAiReplyPrompt(
}
async function queueAndStartOpenAiMasterAgentReply(params: {
provider: ApiCompatibleProvider;
taskId: string;
deviceId: string;
requestText: string;
@@ -583,7 +666,8 @@ async function queueAndStartOpenAiMasterAgentReply(params: {
}
try {
const generated = await generateOpenAiReply({
const generated = await generateApiProviderReply({
provider: params.provider,
apiKey: params.apiKey,
model: params.model,
reasoningEffort: params.reasoningEffort,
@@ -617,6 +701,7 @@ async function queueAndStartOpenAiMasterAgentReply(params: {
}
async function enqueueOpenAiMasterAgentReply(params: {
provider: ApiCompatibleProvider;
accountId: string;
accountLabel: string;
requestMessageId?: string;
@@ -649,13 +734,14 @@ async function enqueueOpenAiMasterAgentReply(params: {
),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
deviceId: OPENAI_MASTER_AGENT_DEVICE_ID,
deviceId: params.provider === "aliyun_qwen_api" ? ALIYUN_QWEN_DEVICE_ID : OPENAI_MASTER_AGENT_DEVICE_ID,
accountId: params.accountId,
accountLabel: params.accountLabel,
});
void queueAndStartOpenAiMasterAgentReply({
provider: params.provider,
taskId: task.taskId,
deviceId: OPENAI_MASTER_AGENT_DEVICE_ID,
deviceId: params.provider === "aliyun_qwen_api" ? ALIYUN_QWEN_DEVICE_ID : OPENAI_MASTER_AGENT_DEVICE_ID,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
apiKey: params.apiKey,
@@ -682,34 +768,39 @@ async function enqueueOpenAiMasterAgentReply(params: {
return queuedReply;
}
export async function probeOpenAiApiAccount(params: {
export async function probeApiCompatibleAccount(params: {
provider: ApiCompatibleProvider;
apiKey: string;
model?: string;
}) {
const apiKey = params.apiKey.trim();
if (!apiKey) {
throw new Error("当前账号还没有可用的 OpenAI API Key。");
throw new Error(`当前账号还没有可用的 ${apiProviderConfig(params.provider).loginLabel}`);
}
const model = params.model?.trim() || "gpt-5.4";
const config = apiProviderConfig(params.provider);
const model = params.model?.trim() || config.defaultModel;
let response: Response;
const body: Record<string, unknown> = {
model,
instructions: `你正在执行${config.label}连接自检。请只回复“连接正常”。`,
input: "请只回复“连接正常”。",
};
if (params.provider === "openai_api") {
body.reasoning = { effort: "low" };
}
try {
response = await fetch("https://api.openai.com/v1/responses", {
response = await fetch(config.endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
reasoning: { effort: "low" },
instructions: "你正在执行 OpenAI API 连接自检。请只回复“连接正常”。",
input: "请只回复“连接正常”。",
}),
body: JSON.stringify(body),
signal: AbortSignal.timeout(15_000),
});
} catch (error) {
throw new Error(normalizeOpenAiFetchFailure(error));
throw new Error(normalizeApiProviderFetchFailure(params.provider, error));
}
const requestId = response.headers.get("x-request-id") ?? undefined;
@@ -723,8 +814,9 @@ export async function probeOpenAiApiAccount(params: {
? payload.error?.message
: undefined;
throw new Error(
normalizeOpenAiError(
`${apiError ?? `OpenAI API ${response.status}`}${requestId ? ` (request_id=${requestId})` : ""}`,
normalizeApiProviderError(
params.provider,
`${apiError ?? `${config.label} ${response.status}`}${requestId ? ` (request_id=${requestId})` : ""}`,
),
);
}
@@ -738,6 +830,13 @@ export async function probeOpenAiApiAccount(params: {
};
}
export async function probeOpenAiApiAccount(params: { apiKey: string; model?: string }) {
return probeApiCompatibleAccount({
provider: "openai_api",
...params,
});
}
async function appendMasterAgentSystemReply(body: string, senderLabel = "主 Agent") {
return appendProjectMessage({
projectId: "master-agent",
@@ -1332,17 +1431,18 @@ export async function validateAiAccountConnection(accountId: string) {
};
}
if (account.provider !== "openai_api" || !account.apiKey?.trim()) {
if (!isApiCompatibleProvider(account.provider) || !account.apiKey?.trim()) {
return {
ok: false as const,
status: "needs_api_key",
message: "当前账号还没有可用的 OpenAI API Key。",
message: `当前账号还没有可用的${isApiCompatibleProvider(account.provider) ? apiProviderConfig(account.provider).loginLabel : " API Key"}`,
};
}
const generated = await probeOpenAiApiAccount({
const generated = await probeApiCompatibleAccount({
provider: account.provider,
apiKey: account.apiKey,
model: account.model || "gpt-5.4",
model: account.model || apiProviderConfig(account.provider).defaultModel,
});
await updateAiAccountHealth({
@@ -1372,7 +1472,7 @@ export async function replyToMasterAgentUserMessage(params: {
if (!runtime?.account) {
await appendMasterAgentSystemReply(
"我已经收到你的消息,但当前没有可用的主控 AI 账号。请到“我的 > AI 账号”至少配置一个可用的 OpenAI API 账号,再继续对话。",
"我已经收到你的消息,但当前没有可用的主控 AI 账号。请到“我的 > AI 账号”至少配置一个可用的 OpenAI API、阿里百炼 Qwen或接回 Master Codex Node 后,再继续对话。",
);
return { ok: false as const, reason: "NO_AI_ACCOUNT" };
}
@@ -1403,9 +1503,10 @@ export async function replyToMasterAgentUserMessage(params: {
lastValidatedAt: new Date().toISOString(),
});
const fallbackAccount = await findFallbackOpenAiAccount(runtime.account.accountId);
if (fallbackAccount?.apiKey?.trim()) {
const fallbackAccount = await findFallbackApiAccount(runtime.account.accountId);
if (fallbackAccount?.apiKey?.trim() && isApiCompatibleProvider(fallbackAccount.provider)) {
return enqueueOpenAiMasterAgentReply({
provider: fallbackAccount.provider,
accountId: fallbackAccount.accountId,
accountLabel: fallbackAccount.label || aiRoleLabel(fallbackAccount.role),
requestMessageId: params.requestMessageId,
@@ -1414,7 +1515,10 @@ export async function replyToMasterAgentUserMessage(params: {
requestedByAccount: params.requestedByAccount,
currentSessionExpiresAt: params.currentSessionExpiresAt,
apiKey: fallbackAccount.apiKey,
model: agentControls?.modelOverride || fallbackAccount.model || "gpt-5.4",
model:
agentControls?.modelOverride ||
fallbackAccount.model ||
apiProviderConfig(fallbackAccount.provider).defaultModel,
reasoningEffort: agentControls?.reasoningEffortOverride || "medium",
agentControls,
promptPolicy: executionConfig.promptPolicy,
@@ -1465,8 +1569,9 @@ export async function replyToMasterAgentUserMessage(params: {
return queuedReply;
}
if (runtime.account.provider === "openai_api" && runtime.account.apiKey?.trim()) {
if (isApiCompatibleProvider(runtime.account.provider) && runtime.account.apiKey?.trim()) {
return enqueueOpenAiMasterAgentReply({
provider: runtime.account.provider,
accountId: runtime.account.accountId,
accountLabel: runtime.account.label || runtime.summary.roleLabel,
requestMessageId: params.requestMessageId,
@@ -1503,7 +1608,7 @@ export async function replyToMasterAgentUserMessage(params: {
lastError: !boundDevice ? "MASTER_CODEX_NODE_DEVICE_NOT_FOUND" : "MASTER_CODEX_NODE_DEVICE_OFFLINE",
lastValidatedAt: new Date().toISOString(),
});
const fallbackAccount = await findFallbackOpenAiAccount(runtime.account.accountId);
const fallbackAccount = await findFallbackApiAccount(runtime.account.accountId);
if (fallbackAccount) {
try {
return await replyViaOpenAiAccount({
@@ -1557,7 +1662,7 @@ export async function replyToMasterAgentUserMessage(params: {
};
}
if (completedTask?.status === "failed") {
const fallbackAccount = await findFallbackOpenAiAccount(runtime.account.accountId);
const fallbackAccount = await findFallbackApiAccount(runtime.account.accountId);
if (fallbackAccount) {
try {
return await replyViaOpenAiAccount({
@@ -1593,12 +1698,12 @@ export async function replyToMasterAgentUserMessage(params: {
return { ok: true as const, accountId: runtime.account.accountId, taskId: task.taskId };
}
if (runtime.account.provider !== "openai_api" || !runtime.account.apiKey?.trim()) {
if (!isApiCompatibleProvider(runtime.account.provider) || !runtime.account.apiKey?.trim()) {
await appendMasterAgentSystemReply(
[
`当前主控身份是 ${runtime.summary.roleLabel},来源 ${aiProviderLabel(runtime.account.provider)}`,
"当前账号既没有接入 Master Codex Node 执行器,也没有可用的 OpenAI API Key。",
"请到“我的 > AI 账号”补一个可用的 OpenAI API 账号,或者把当前节点接回 Master Codex Node relay。",
"当前账号既没有接入 Master Codex Node 执行器,也没有可用的 API 兼容账号。",
"请到“我的 > AI 账号”补一个可用的 OpenAI API 或阿里百炼 Qwen 账号,或者把当前节点接回 Master Codex Node relay。",
].join(""),
`主 Agent · ${runtime.summary.roleLabel}`,
);
@@ -1606,7 +1711,8 @@ export async function replyToMasterAgentUserMessage(params: {
}
try {
const generated = await generateOpenAiReply({
const generated = await generateApiProviderReply({
provider: runtime.account.provider,
apiKey: runtime.account.apiKey,
model: executionConfig.model,
reasoningEffort: executionConfig.reasoningEffort,

View File

@@ -234,6 +234,8 @@ function aiProviderLabel(provider: AiProvider) {
return "Master Codex Node";
case "openai_api":
return "OpenAI API";
case "aliyun_qwen_api":
return "阿里百炼 Qwen";
default:
return provider;
}