fix(canvas): 生成失败提示 - 统一转换用户可读错误 (#669)

Co-authored-by: linxi <linxi@enjoysoftware.cn>
This commit is contained in:
naidalao
2026-10-05 13:27:39 +08:00
committed by GitHub
parent dc8fe0ce22
commit 0c58d8d361
7 changed files with 98 additions and 29 deletions

View File

@@ -4,6 +4,7 @@ import { modelCapabilityConfigFor } from "@/lib/model-capabilities";
import { selectableModelsByCapability, type AiConfig } from "@/stores/use-config-store";
import { resourceFileUrl, resourceIdFromStorageKey } from "@/services/api/resources";
import type { ResponseInputMessage } from "@/services/api/image";
import { generationErrorMessage } from "@/lib/generation-error";
import { dynamicCreativePlan, type CreativeDynamicPlan } from "./creative-plan";
import { normalizeCreativeField } from "./creative-agent-contract";
import { CREATIVE_SCENARIOS, type CreativeAnswers, type CreativeBrief, type CreativeGenerationItem, type CreativePlan, type CreativeProposal, type CreativeQuestionRequest, type CreativeScenarioId } from "./creative-agent-contract";
@@ -185,6 +186,6 @@ export function creativePlan(state: CreativeAgentState): CreativePlan | undefine
return { id: state.proposal?.id ?? state.questions!.interactionId, steps: [
{ id: "brief", title: "了解需求", status: state.questions?.status === "pending" ? "waiting" : "completed" },
{ id: "proposal", title: "确认创意方案", status: state.canvasApplied ? "completed" : state.proposal ? "waiting" : "pending" },
...state.media.map((item) => ({ id: item.ref, title: state.proposal?.workflow.nodes.find((node) => node.ref === item.ref)?.title || item.ref, status: item.status === "ready" ? "completed" as const : item.status === "failed" || item.status === "write_failed" ? "failed" as const : item.status === "running" || item.status === "queued" ? "running" as const : item.submissionId ? "waiting" as const : "pending" as const, nodeIds: [item.nodeId], detail: item.error })),
...state.media.map((item) => ({ id: item.ref, title: state.proposal?.workflow.nodes.find((node) => node.ref === item.ref)?.title || item.ref, status: item.status === "ready" ? "completed" as const : item.status === "failed" || item.status === "write_failed" ? "failed" as const : item.status === "running" || item.status === "queued" ? "running" as const : item.submissionId ? "waiting" as const : "pending" as const, nodeIds: [item.nodeId], detail: item.error ? generationErrorMessage(item.error) : undefined })),
] };
}

View File

@@ -1,5 +1,6 @@
import type { CreativePlan, CreativePlanStep } from "./creative-agent-contract";
import type { CreativeAgentState } from "./creative-agent-state";
import { generationErrorMessage } from "@/lib/generation-error";
export type CreativePlannedStep = {
id: string;
@@ -58,7 +59,7 @@ export function dynamicCreativePlan(state: CreativeAgentState): CreativePlan | u
else if (media.some((item) => item.submissionId)) status = "waiting";
}
}
return { id: step.id, title: step.title, status, nodeIds, detail: step.phase === "media" ? state.media.filter((item) => step.mediaRefs.includes(item.ref)).map((item) => item.error).filter(Boolean).join(";") || undefined : undefined };
return { id: step.id, title: step.title, status, nodeIds, detail: step.phase === "media" ? state.media.filter((item) => step.mediaRefs.includes(item.ref)).map((item) => item.error ? generationErrorMessage(item.error) : "").filter(Boolean).join(";") || undefined : undefined };
}) };
}

View File

@@ -4,6 +4,28 @@ export const CONTENT_MODERATION_MESSAGE = "内容审核未通过,本次平台
const DEFAULT_GENERATION_ERROR_MESSAGE = "生成失败,请稍后重试。";
const NETWORK_ERROR_MESSAGE = "网络异常。";
const MODEL_CAPABILITY_ERROR_MESSAGE = "所选模型不支持当前生成方式或输入,请切换模型或调整输入后重试。";
const MODEL_PARAMETER_ERROR_MESSAGE = "当前模型不支持这组参数或参考素材,请调整输入或切换模型后重试。";
const MODEL_SERVICE_ERROR_MESSAGE = "模型服务处理失败,请稍后重试或换用其他模型。";
const REASON_MESSAGES: Record<string, string> = {
model_price_not_configured: "当前模型暂未配置价格,请换用其他模型或联系管理员。",
model_route_unavailable: "当前模型暂时没有可用渠道,请稍后重试或换用其他模型。",
provider_request_failed: MODEL_SERVICE_ERROR_MESSAGE,
model_catalog_mismatch: "模型配置已更新,请重新选择模型后再试。",
invalid_model_selection: "模型选择无效,请重新选择模型后再试。",
quota_exceeded: "积分或额度不足,请充值后再试。",
rate_limited: "请求过于频繁,请稍后再试。",
timeout: "模型服务响应超时,请稍后再试。",
bad_gateway: "模型服务暂时不可用,请稍后再试。",
unavailable: "模型服务暂时不可用,请稍后再试。",
upstream_dns_failed: "模型服务暂时不可用,请稍后再试。",
unauthorized: "当前登录状态或权限不足,请重新登录后再试。",
forbidden: "当前账号没有执行此操作的权限。",
failed_precondition: "当前请求条件不满足,请检查输入或换用其他模型。",
conflict: "请求状态已发生变化,请刷新后再试。",
internal: "系统暂时无法完成生成,请稍后再试。",
};
export type GenerationFailureMetadata = {
errorDetails: string;
@@ -25,21 +47,29 @@ export function generationErrorMessage(error: unknown) {
const raw = rawGenerationError(error);
if (isContentModerationError(raw)) return contentModerationMessage(raw);
const reason = structuredErrorReason(error);
if (reason === "model_capability_not_supported") return MODEL_CAPABILITY_ERROR_MESSAGE;
if (reason === "invalid_argument" && isModelCapabilityFailure(raw)) return MODEL_CAPABILITY_ERROR_MESSAGE;
if (reason === "invalid_argument" && isModelParameterFailure(raw)) return MODEL_PARAMETER_ERROR_MESSAGE;
if (reason && REASON_MESSAGES[reason]) return REASON_MESSAGES[reason];
const providerMessage = extractStructuredProviderMessage(raw) || extractWrappedProviderMessage(raw);
const displayMessage = providerMessage || raw;
if (isContentModerationError(displayMessage)) return contentModerationMessage(displayMessage);
const resourceStorageMessage = resourceStorageFailureMessage(raw) || resourceStorageFailureMessage(displayMessage);
if (resourceStorageMessage) return resourceStorageMessage;
if (isNetworkFailure(displayMessage)) return NETWORK_ERROR_MESSAGE;
if (!providerMessage) {
if (!displayMessage.includes(";上游:")) {
if (hasHttpStatus(raw, 429)) return "服务当前繁忙,请稍后重试。";
if (hasHttpStatus(raw, 401, 403)) return "生成服务鉴权失败,请检查渠道配置。";
if (hasHttpStatus(raw, 404)) return "生成服务地址不可用,请检查渠道配置。";
if (hasHttpStatus(raw, 500, 502, 503, 504)) return NETWORK_ERROR_MESSAGE;
}
if (!providerMessage && !keepsProviderDetail(displayMessage)) {
if (hasHttpStatus(raw, 429)) return "服务当前繁忙,请稍后重试。";
if (hasHttpStatus(raw, 401, 403)) return "生成服务鉴权失败,请检查渠道配置。";
if (hasHttpStatus(raw, 404)) return "生成服务地址不可用,请检查渠道配置。";
if (hasHttpStatus(raw, 500, 502, 503, 504)) return NETWORK_ERROR_MESSAGE;
if (containsInfrastructureDetails(raw)) return NETWORK_ERROR_MESSAGE;
}
if (isModelCapabilityFailure(displayMessage)) return MODEL_CAPABILITY_ERROR_MESSAGE;
if (isModelParameterFailure(displayMessage)) return MODEL_PARAMETER_ERROR_MESSAGE;
if (keepsProviderDetail(displayMessage)) return displayMessage;
if (isTechnicalProviderMessage(displayMessage)) return MODEL_SERVICE_ERROR_MESSAGE;
return displayMessage || DEFAULT_GENERATION_ERROR_MESSAGE;
}
@@ -88,6 +118,39 @@ function rawGenerationError(error: unknown) {
return providerPayloadMessage(error);
}
function structuredErrorReason(error: unknown): string | undefined {
if (!error || typeof error !== "object") return undefined;
const record = error as Record<string, unknown>;
if (typeof record.reason === "string" && record.reason.trim()) return record.reason.trim().toLowerCase();
if (record.cause && record.cause !== error) return structuredErrorReason(record.cause);
return undefined;
}
function isModelCapabilityFailure(value: string) {
return /所选模型不支持当前请求|模型不支持当前请求|不支持操作\s+|能力类型不匹配/i.test(value);
}
function isModelParameterFailure(value: string) {
return /不支持参数|超出支持范围|数量需在|至少需要\s+\d+\s+个|暂时无法满足这组输入和参数/i.test(value);
}
function isTechnicalProviderMessage(value: string) {
const text = value.trim();
if (!text) return false;
if (/(?:provider request failed|invalid_request_error|internal_server_error|bad_request|unauthorized|forbidden|not_found|upstream_error|request failed with status code|http\s*\d{3})/i.test(text)) return true;
// 纯英文的 SDK/网关错误通常不是面向终端用户的说明;中文供应商原因仍允许展示。
return !/[\u3400-\u9fff]/.test(text) && /^[\w .,:;_/'"()\-]+$/.test(text) && text.length > 18;
}
function keepsProviderDetail(value: string) {
const marker = ";上游:";
const index = value.indexOf(marker);
if (index < 0) return false;
const detail = value.slice(index + marker.length).trim();
if (!detail || isNetworkFailure(detail) || containsInfrastructureDetails(detail)) return false;
return !/\bHTTP\s*5\d{2}\b/i.test(detail);
}
function extractStructuredProviderMessage(raw: string) {
for (let index = raw.indexOf("{"); index >= 0; index = raw.indexOf("{", index + 1)) {
try {