From 0253072232a2df129cf6f8bbfdd2ea870845d4c3 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Sun, 2 Aug 2026 22:29:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(*):=20=E5=88=9B=E4=BD=9C=E4=BD=93=E9=AA=8C?= =?UTF-8?q?=20-=20=E6=94=AF=E6=8C=81=E8=B5=84=E6=BA=90=E6=8A=80=E8=83=BD?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E5=B9=B6=E7=BB=9F=E4=B8=80=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=8F=8D=E9=A6=88=E4=B8=8E=E7=94=BB=E5=B8=83=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../canvas/canvas-assistant-panel.tsx | 6 +- .../canvas/canvas-node-hover-toolbar.tsx | 4 +- web/src/components/canvas/canvas-node.tsx | 4 +- .../components/canvas/canvas-project-card.tsx | 49 ++--- .../components/canvas/canvas-script-node.tsx | 6 +- web/src/components/layout/workspace-page.tsx | 4 +- web/src/components/model-picker.tsx | 6 +- web/src/lib/generation-error.ts | 79 +++++++- .../canvas/canvas-project-status-dialogs.tsx | 4 +- web/src/pages/canvas/index.tsx | 5 +- .../canvas/use-canvas-generation-executor.ts | 4 +- .../pages/canvas/use-canvas-media-tools.ts | 7 +- web/src/pages/canvas/use-canvas-storyboard.ts | 9 +- web/src/pages/create/creation-references.ts | 162 ++++++++++++++++ web/src/pages/create/index.tsx | 176 ++++++++++++++---- web/src/pages/settings/model-default-grid.tsx | 1 - web/src/pages/tasks/index.tsx | 14 +- web/src/pages/wallet/index.tsx | 16 +- web/src/stores/use-config-store.ts | 4 +- web/src/styles/globals.css | 159 +++++++++++++--- 20 files changed, 589 insertions(+), 130 deletions(-) create mode 100644 web/src/pages/create/creation-references.ts diff --git a/web/src/components/canvas/canvas-assistant-panel.tsx b/web/src/components/canvas/canvas-assistant-panel.tsx index b51959dea..147d742b8 100644 --- a/web/src/components/canvas/canvas-assistant-panel.tsx +++ b/web/src/components/canvas/canvas-assistant-panel.tsx @@ -886,16 +886,16 @@ function AgentTextModelPicker({ config, value, onChange }: { config: AiConfig; v value={current || undefined} className="agent-text-model-select w-full" popupMatchSelectWidth={288} - options={options.map((model) => ({ value: model, label: `${modelDisplayName(config, model)} ${modelOptionName(model)} ${resolveModelChannel(config, model).name}` }))} + options={options.map((model) => ({ value: model, label: `${modelDisplayName(config, model)} ${resolveModelChannel(config, model).name}` }))} notFoundContent={暂无文本模型} optionRender={(option) => { const model = String(option.value); - return {modelDisplayName(config, model)}{modelOptionName(model)}{resolveModelChannel(config, model).name}; + return {modelDisplayName(config, model)}{resolveModelChannel(config, model).name}; }} labelRender={() => {current ? modelDisplayName(config, current) : "选择文本模型"}{current ? {resolveModelChannel(config, current).name} : null}} onChange={onChange} aria-label="选择 Agent 文本模型" - title={current ? `${modelDisplayName(config, current)} · ${modelOptionName(current)} · ${resolveModelChannel(config, current).name}` : "选择文本模型"} + title={current ? `${modelDisplayName(config, current)} · ${resolveModelChannel(config, current).name}` : "选择文本模型"} /> ); diff --git a/web/src/components/canvas/canvas-node-hover-toolbar.tsx b/web/src/components/canvas/canvas-node-hover-toolbar.tsx index b4974eae3..d46f01b28 100644 --- a/web/src/components/canvas/canvas-node-hover-toolbar.tsx +++ b/web/src/components/canvas/canvas-node-hover-toolbar.tsx @@ -7,7 +7,7 @@ import { canvasDockStyle } from "@/lib/canvas/canvas-aceternity-style"; import { subscribeCanvasViewportPreview } from "@/lib/canvas/canvas-live-viewport"; import { canvasNodeAssetCategory } from "@/lib/canvas/canvas-node-asset"; import { formatBytes, getDataUrlByteSize } from "@/lib/image-utils"; -import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error"; +import { CONTENT_MODERATION_ERROR_CODE, generationErrorMessage, isContentModerationError } from "@/lib/generation-error"; import { useCopyText } from "@/hooks/use-copy-text"; import { useThemeStore } from "@/stores/use-theme-store"; import { FloatingDock, type FloatingDockEntry } from "@/components/ui/aceternity/floating-dock"; @@ -546,7 +546,7 @@ export function CanvasNodeInfoModal({ node, open, onClose, onMetadataChange, rea {node.metadata?.errorDetails ? (
- {node.metadata.errorDetails} + {generationErrorMessage(node.metadata.errorDetails)}
) : null} diff --git a/web/src/components/canvas/canvas-node.tsx b/web/src/components/canvas/canvas-node.tsx index 816e69bba..3e0ec360d 100644 --- a/web/src/components/canvas/canvas-node.tsx +++ b/web/src/components/canvas/canvas-node.tsx @@ -7,7 +7,7 @@ import { CometCard } from "@/components/ui/aceternity/comet-card"; import { resourceStorageLabel, resourceStorageLocation, resourceStorageTitle } from "@/lib/canvas/resource-storage-status"; import { canvasRichTextHTML } from "@/lib/canvas/canvas-rich-text"; import { formatBytes } from "@/lib/image-utils"; -import { CONTENT_MODERATION_ERROR_CODE, isContentModerationError } from "@/lib/generation-error"; +import { CONTENT_MODERATION_ERROR_CODE, generationErrorMessage, isContentModerationError } from "@/lib/generation-error"; import { useThemeStore } from "@/stores/use-theme-store"; import { resourceIdFromStorageKey } from "@/services/api/resources"; import { cacheResourceObjectUrl, getCachedResourceObjectUrl } from "@/services/resource-blob-cache"; @@ -633,7 +633,7 @@ function ErrorContent({ node, theme, onRetry }: Pick -
{node.metadata?.errorDetails || "生成失败"}
+
{generationErrorMessage(node.metadata?.errorDetails)}
{moderationFailure ? (
修改节点提示词后,可重新点击生成。 diff --git a/web/src/components/canvas/canvas-project-card.tsx b/web/src/components/canvas/canvas-project-card.tsx index 97d171bfb..5dd137453 100644 --- a/web/src/components/canvas/canvas-project-card.tsx +++ b/web/src/components/canvas/canvas-project-card.tsx @@ -32,11 +32,11 @@ export function CanvasProjectCard({ project, projectName, variant = "library" }: const compact = variant === "recent"; return ( -
!editing && open()}> +
!editing && open()}>
-
-
+
+
{editing ? ( event.stopPropagation()} onChange={(event) => setEditingTitle(event.target.value)} onKeyDown={(event) => event.key === "Enter" && saveTitle()} autoFocus /> ) : ( )} {editing ? ( @@ -70,27 +70,30 @@ export function CanvasProjectCard({ project, projectName, variant = "library" }:
) : ( - domEvent.stopPropagation(), - items: [ - { key: "export", icon: , label: "导出画布", onClick: () => void exportCanvasProjects([project], project.title || "影策画布") }, - { key: "rename", icon: , label: "重命名", onClick: () => startEditing(project.id, project.title) }, - { type: "divider" }, - { key: "delete", danger: true, icon: , label: "删除", onClick: () => setDeleteIds([project.id]) }, - ], - }} - > - - +
event.stopPropagation()}> + + domEvent.stopPropagation(), + items: [ + { key: "export", icon: , label: "导出画布", onClick: () => void exportCanvasProjects([project], project.title || "影策画布") }, + { type: "divider" }, + { key: "delete", danger: true, icon: , label: "删除", onClick: () => setDeleteIds([project.id]) }, + ], + }} + > + + +
)}
-
+
{projectName || "自由画布"} - {project.connections.length} 条连线 + + {project.connections.length} 条连线
-

更新于 {formatProjectTime(project.updatedAt)}

+

更新于 {formatProjectTime(project.updatedAt)}

); diff --git a/web/src/components/canvas/canvas-script-node.tsx b/web/src/components/canvas/canvas-script-node.tsx index 168eebcd1..f676549af 100644 --- a/web/src/components/canvas/canvas-script-node.tsx +++ b/web/src/components/canvas/canvas-script-node.tsx @@ -8,7 +8,7 @@ import { ModelPicker } from "@/components/model-picker"; import { buildGenerationConfig } from "@/lib/canvas/canvas-project-generation"; import type { CanvasResourceReference } from "@/lib/canvas/canvas-resource-references"; import { pipelineStatusLabel, type CanvasStoryboardPipelineProgress, type StoryboardPipelineStage } from "@/lib/canvas/canvas-storyboard-progress"; -import { isContentModerationError } from "@/lib/generation-error"; +import { generationErrorMessage, isContentModerationError } from "@/lib/generation-error"; import { navigateToSettings } from "@/lib/settings-navigation"; import { canvasThemes } from "@/lib/canvas-theme"; import { useEffectiveConfig } from "@/stores/use-config-store"; @@ -106,7 +106,7 @@ export function CanvasScriptNodeContent({ node, batch, pipeline, scale, mentionR const hasActiveBatchItems = Boolean(batch?.items.some((item) => item.status === "waiting" || item.status === "submitting" || item.status === "queued" || item.status === "running")); const taskFeedback = node.metadata?.status === "loading" ? `${node.metadata.taskStage || "正在创建任务"}${typeof node.metadata.taskProgress === "number" ? ` · ${node.metadata.taskProgress}%` : ""}` - : node.metadata?.status === "error" ? node.metadata.errorDetails : ""; + : node.metadata?.status === "error" ? generationErrorMessage(node.metadata.errorDetails) : ""; const submitPrompt = () => { const value = prompt.trim(); if (value && node.metadata?.status !== "loading") onGenerateScript(value); @@ -344,7 +344,7 @@ function GenerationBatchDetails({ batch, rows, onRetryItem, onCancelItem }: { ba const requiresPromptChange = isContentModerationError(item.errorDetails); return
镜头 {shotByRowId.get(item.rowId) || "--"} - {generationBatchItemLabel(item)}{item.retryCount ? ` · 重试 ${item.retryCount}` : ""} + {generationBatchItemLabel(item)}{item.retryCount ? ` · 重试 ${item.retryCount}` : ""} {item.status === "failed" ? : null} {cancellable ? : null}
; diff --git a/web/src/components/layout/workspace-page.tsx b/web/src/components/layout/workspace-page.tsx index daeb56798..7bab30864 100644 --- a/web/src/components/layout/workspace-page.tsx +++ b/web/src/components/layout/workspace-page.tsx @@ -31,9 +31,9 @@ export function PageHeader({ title, description, meta, actions, icon }: { title: ); } -export function ListToolbar({ children, trailing, active, onReset }: { children: ReactNode; trailing?: ReactNode; active?: boolean; onReset?: () => void }) { +export function ListToolbar({ children, trailing, active, onReset, className }: { children: ReactNode; trailing?: ReactNode; active?: boolean; onReset?: () => void; className?: string }) { return ( -
+
{children}
{active && onReset ? : null} diff --git a/web/src/components/model-picker.tsx b/web/src/components/model-picker.tsx index c07f13852..e0bde0fb1 100644 --- a/web/src/components/model-picker.tsx +++ b/web/src/components/model-picker.tsx @@ -124,7 +124,7 @@ export function ModelPicker({ config, value, onChange, capability, className, fu window.requestAnimationFrame(() => triggerRef.current?.focus()); }} > - + {selected ? : null} ); @@ -182,7 +182,7 @@ function emptyModelLabel(config: AiConfig, capability?: ModelCapability) { return config.models.length ? `暂无匹配的${label}模型` : "请先到配置里添加渠道和模型"; } -function ModelLabel({ config, model, capability, theme, creationVariant }: { config: AiConfig; model: string; capability?: ModelCapability; theme: (typeof canvasThemes)[keyof typeof canvasThemes]; creationVariant: boolean }) { +function ModelLabel({ config, model, capability, theme }: { config: AiConfig; model: string; capability?: ModelCapability; theme: (typeof canvasThemes)[keyof typeof canvasThemes] }) { const meta = modelMenuMeta(model, capability); return ( @@ -191,7 +191,7 @@ function ModelLabel({ config, model, capability, theme, creationVariant }: { con {modelDisplayName(config, model)} - {creationVariant ? meta.description : `${modelOptionName(model)} · ${meta.description}`} + {meta.description} {!creationVariant && meta.time ? {meta.time} : null} diff --git a/web/src/lib/generation-error.ts b/web/src/lib/generation-error.ts index 994d390f2..edc522116 100644 --- a/web/src/lib/generation-error.ts +++ b/web/src/lib/generation-error.ts @@ -2,6 +2,9 @@ export const CONTENT_MODERATION_ERROR_CODE = "sensitive_words_detected"; export const CONTENT_MODERATION_MESSAGE = "内容审核未通过,本次平台积分未扣除或已退还。请修改提示词后重新生成。"; +const DEFAULT_GENERATION_ERROR_MESSAGE = "生成失败,请稍后重试。"; +const NETWORK_ERROR_MESSAGE = "网络异常。"; + export type GenerationFailureMetadata = { errorDetails: string; generationErrorCode?: string; @@ -9,8 +12,8 @@ export type GenerationFailureMetadata = { }; export function generationFailureMetadata(error: unknown, prompt: string): GenerationFailureMetadata { - const raw = error instanceof Error ? error.message : String(error || "生成失败"); - if (!isContentModerationError(raw)) return { errorDetails: raw || "生成失败" }; + const raw = rawGenerationError(error); + if (!isContentModerationError(raw)) return { errorDetails: generationErrorMessage(error) }; return { errorDetails: CONTENT_MODERATION_MESSAGE, generationErrorCode: CONTENT_MODERATION_ERROR_CODE, @@ -19,8 +22,20 @@ export function generationFailureMetadata(error: unknown, prompt: string): Gener } export function generationErrorMessage(error: unknown) { - const raw = error instanceof Error ? error.message : String(error || "生成失败"); - return isContentModerationError(raw) ? CONTENT_MODERATION_MESSAGE : raw || "生成失败"; + const raw = rawGenerationError(error); + if (isContentModerationError(raw)) return CONTENT_MODERATION_MESSAGE; + + const providerMessage = extractStructuredProviderMessage(raw) || extractWrappedProviderMessage(raw); + const displayMessage = providerMessage || raw; + if (isContentModerationError(displayMessage)) return CONTENT_MODERATION_MESSAGE; + if (isNetworkFailure(displayMessage)) return NETWORK_ERROR_MESSAGE; + if (!providerMessage) { + if (hasHttpStatus(raw, 429)) return "服务当前繁忙,请稍后重试。"; + if (hasHttpStatus(raw, 401, 403)) return "生成服务鉴权失败,请检查渠道配置。"; + if (hasHttpStatus(raw, 404)) return "生成服务地址不可用,请检查渠道配置。"; + if (hasHttpStatus(raw, 500, 502, 503, 504) || containsInfrastructureDetails(raw)) return NETWORK_ERROR_MESSAGE; + } + return displayMessage || DEFAULT_GENERATION_ERROR_MESSAGE; } export function isContentModerationError(value: unknown) { @@ -45,3 +60,59 @@ export function generationPromptFingerprint(value: string) { } return `${normalized.length}:${(hash >>> 0).toString(36)}`; } + +function rawGenerationError(error: unknown) { + if (error instanceof Error) return error.message.trim(); + if (typeof error === "string") return error.trim(); + return providerPayloadMessage(error); +} + +function extractStructuredProviderMessage(raw: string) { + for (let index = raw.indexOf("{"); index >= 0; index = raw.indexOf("{", index + 1)) { + try { + const message = providerPayloadMessage(JSON.parse(raw.slice(index).trim())); + if (message) return message; + } catch { + // 上游常把 JSON 拼在 HTTP 状态后;不是完整 JSON 时继续尝试下一个对象起点。 + } + } + return ""; +} + +function extractWrappedProviderMessage(raw: string) { + const interfaceFailure = raw.match(/^接口请求失败[::]\s*(.*)$/s); + const requestFailure = raw.match(/^Request failed with status code \d{3}\s*[::-]?\s*(.+)$/is); + const wrapped = interfaceFailure?.[1] ?? requestFailure?.[1]; + if (!wrapped) return ""; + const message = wrapped + .replace(/^\d{3}(?:\s+(?:Bad Gateway|Service Unavailable|Gateway Timeout|Internal Server Error|Not Found|Unauthorized|Forbidden|Too Many Requests))?\s*[::-]?\s*/i, "") + .trim(); + return message && !containsInfrastructureDetails(message) ? message : ""; +} + +function providerPayloadMessage(payload: unknown): string { + if (typeof payload === "string") return payload.trim(); + if (!payload || typeof payload !== "object" || Array.isArray(payload)) return ""; + const record = payload as Record; + if (record.error && typeof record.error === "object") { + const nested = providerPayloadMessage(record.error); + if (nested) return nested; + } + for (const key of ["message", "msg", "detail"] as const) { + const value = record[key]; + if (typeof value === "string" && value.trim()) return value.trim(); + } + return typeof record.error === "string" ? record.error.trim() : ""; +} + +function isNetworkFailure(value: string) { + return /\b(?:dial tcp|connection refused|connection reset|no such host|i\/o timeout|context deadline exceeded|network error|failed to fetch|fetch failed|socket hang up|econnrefused|econnreset|etimedout)\b/i.test(value); +} + +function hasHttpStatus(value: string, ...statuses: number[]) { + return statuses.some((status) => new RegExp(`\\b${status}\\b`).test(value)); +} + +function containsInfrastructureDetails(value: string) { + return /(?:接口请求失败|Request failed with status code|https?:\/\/|\b(?:GET|POST|PUT|PATCH|DELETE)\s+["']?|Bad Gateway|Service Unavailable|Gateway Timeout|upstream_error)/i.test(value); +} diff --git a/web/src/pages/canvas/canvas-project-status-dialogs.tsx b/web/src/pages/canvas/canvas-project-status-dialogs.tsx index 716c16d05..e389ad92f 100644 --- a/web/src/pages/canvas/canvas-project-status-dialogs.tsx +++ b/web/src/pages/canvas/canvas-project-status-dialogs.tsx @@ -4,6 +4,7 @@ import { TaskDetailItem, taskStatusText } from "./canvas-project-feedback"; import type { GenerationTask, TaskLog } from "@/services/api/task-center"; import { CanvasNodeType, type CanvasNodeData } from "@/types/canvas"; import { VideoPlayer } from "@/components/video-player"; +import { modelDisplayName, useEffectiveConfig } from "@/stores/use-config-store"; type CanvasProjectStatusDialogsProps = { theme: { node: { stroke: string; panel: string; muted: string; fill: string } }; @@ -21,6 +22,7 @@ type CanvasProjectStatusDialogsProps = { }; export function CanvasProjectStatusDialogs({ theme, task, taskLogs, taskLoading, superResolveNode, previewNode, clearConfirmOpen, onCloseTask, onCloseSuperResolve, onClosePreview, onCancelClear, onConfirmClear }: CanvasProjectStatusDialogsProps) { + const config = useEffectiveConfig(); return ( <> @@ -29,7 +31,7 @@ export function CanvasProjectStatusDialogs({ theme, task, taskLogs, taskLoading,
- +
diff --git a/web/src/pages/canvas/index.tsx b/web/src/pages/canvas/index.tsx index da3f73e8d..809cb93eb 100644 --- a/web/src/pages/canvas/index.tsx +++ b/web/src/pages/canvas/index.tsx @@ -154,11 +154,12 @@ export default function CanvasPage() { /> 显示 {visibleProjects.length} / {filteredProjects.length}} onReset={() => { setKeyword(""); setProjectFilter("all"); setSort("updated"); setPage(1); }} > -
+
} value={keyword} placeholder="搜索画布" aria-label="搜索画布" onChange={(event) => { setKeyword(event.target.value); setPage(1); }} />
- +
-