diff --git a/.memory/worklog.json b/.memory/worklog.json index cef7798..8b9ff85 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -2348,6 +2348,19 @@ "message": "auto-save 2026-05-13 20:40 (~4)", "hash": "66f2495", "files_changed": 4 + }, + { + "ts": "2026-05-13T20:45:53+08:00", + "type": "commit", + "message": "auto-save 2026-05-13 20:45 (~6)", + "hash": "700fa24", + "files_changed": 6 + }, + { + "ts": "2026-05-13T12:49:30Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 7 项未提交变更 · 最近提交:auto-save 2026-05-13 20:45 (~6)", + "files_changed": 7 } ] } diff --git a/api/main.py b/api/main.py index 6646556..c3a8ea5 100644 --- a/api/main.py +++ b/api/main.py @@ -216,7 +216,7 @@ def ensure_video_api_configured() -> None: if not VIDEO_API_BASE_URL and "ai.skg.com/ezlink" in base: raise HTTPException( 503, - "当前 SKG ezlink 网关未开通生视频 /videos 端点;请配置 VIDEO_API_BASE_URL/VIDEO_API_KEY 接 Seedance、Kling 或 Veo 3 的真实视频 API 后再生成。", + "当前 SKG ezlink baseurl 已连通,但这把 key 未开通生视频 /videos 端点;需要 IT 给该分组开通视频端点,或改用真实 Seedance/Kling/Veo 3 视频 API。", ) if not video_api_key(): raise HTTPException(503, "VIDEO_API_KEY 或 LLM_API_KEY 未配置,无法调用生视频 API") diff --git a/docs/source-analysis.html b/docs/source-analysis.html index 47101ad..b3d420e 100644 --- a/docs/source-analysis.html +++ b/docs/source-analysis.html @@ -830,6 +830,30 @@ api/main.py

变更记录

这个记录不是 git log 的替代品。它记录“产品理解发生了什么变化、影响了哪些源码、你以后描述需求时该怎么说”。后续每次改功能都要补一条。

+
+
+

2026-05-13 · 视频 API 未开通时前置禁用按钮

+ StoryboardWorkbench + Health +
+
+

问题:当前 SKG ezlink 未开 /videos,用户点生成后才看到失败 toast,容易误以为是某个分镜或模型选择错误。

+

改动:前端启动时读取 /healthmodels.video_configured;若为 false,分镜编排的生视频区域直接显示“视频 API 未开通”,并禁用提交按钮。

+

影响:web/lib/api.tsweb/app/page.tsxweb/components/storyboard-workbench.tsx

+
+
+
+
+

2026-05-13 · 生视频错误提示改为可读原因

+ VideoGenNode + API +
+
+

问题:提交生视频失败时,前端把 generateStoryboardVideo 503 {"detail": ...} 原样展示,用户无法快速判断是配置、端点还是 UI 问题。

+

改动:generateStoryboardVideo 解析后端 JSON 的 detail 后再抛错;后端 503 文案改为“SKG ezlink 已连通但当前 key 未开 /videos”;Video Gen 失败卡把 /videos 404 长错误压缩成一句可读原因。

+

影响:web/lib/api.tsweb/components/nodes/index.tsxapi/main.py

+
+

2026-05-13 · Video Gen 卡片增加复制和删除

diff --git a/web/app/page.tsx b/web/app/page.tsx index 2b75b44..dfe7fcf 100644 --- a/web/app/page.tsx +++ b/web/app/page.tsx @@ -16,9 +16,9 @@ import { ThemeToggle } from "@/components/theme-toggle" import { StoryboardBar } from "@/components/storyboard-bar" import { StoryboardWorkbench } from "@/components/storyboard-workbench" import { - addManualFrame, analyzeJob, createJob, getJob, uploadJob, deleteFrame, deleteGeneratedImage, + addManualFrame, analyzeJob, createJob, getHealth, getJob, uploadJob, deleteFrame, deleteGeneratedImage, deleteGeneratedVideo, generateStoryboardVideo, - type Job, type ImageRef, type StoryboardScene, + type BackendHealth, type Job, type ImageRef, type StoryboardScene, } from "@/lib/api" import { VideoLightbox } from "@/components/video-lightbox" @@ -77,6 +77,7 @@ export default function Home() { const [storyboardFrame, setStoryboardFrame] = useState(null) const [workbenchOpen, setWorkbenchOpen] = useState(false) const [clipboard, setClipboard] = useState(null) + const [backendHealth, setBackendHealth] = useState(null) const flowRef = useRef(null) // 把 setJob(prev=>...) 翻译成 setJobs 里更新当前 active @@ -228,6 +229,10 @@ export default function Home() { const handleQuickGenerateVideo = useCallback(async (frameIdx: number, scene: StoryboardScene, model: string) => { if (!job) return + if (backendHealth?.models?.video_configured === false) { + toast.error("当前 SKG ezlink 未开通生视频端点,不能提交视频任务") + return + } const frame = job.frames.find((f) => f.index === frameIdx) if (!frame) return @@ -265,7 +270,13 @@ export default function Home() { } catch (e) { toast.error("提交视频失败:" + (e instanceof Error ? e.message : String(e))) } - }, [job, setJob]) + }, [job, setJob, backendHealth?.models?.video_configured]) + + useEffect(() => { + getHealth() + .then(setBackendHealth) + .catch(() => setBackendHealth(null)) + }, []) // URL ?job=xxx,yyy 自动恢复多个 job useEffect(() => { @@ -487,6 +498,7 @@ export default function Home() { onJobUpdate={setJob as any} clipboard={clipboard} focusedFrame={storyboardFrame} + videoConfigured={backendHealth?.models?.video_configured} onGenerateVideo={handleQuickGenerateVideo} />
diff --git a/web/components/nodes/index.tsx b/web/components/nodes/index.tsx index 3171ce6..de304ac 100644 --- a/web/components/nodes/index.tsx +++ b/web/components/nodes/index.tsx @@ -960,6 +960,13 @@ export function VideoGenNode({ data, selected }: any) { if (m.includes("seedance")) return "Seedance" return model || "Video" } + const readableVideoError = (error?: string) => { + const e = error || "生成失败" + if (e.includes("/videos") && e.includes("404")) { + return "当前 SKG ezlink 未开通生视频 /videos 端点" + } + return e + } return (
{videos.length > 0 && ( @@ -1075,7 +1082,7 @@ export function VideoGenNode({ data, selected }: any) { {modelLabel(v.model)} · {v.status}
- {v.status === "failed" ? (v.error || "生成失败") : v.prompt} + {v.status === "failed" ? readableVideoError(v.error) : v.prompt}
diff --git a/web/components/storyboard-workbench.tsx b/web/components/storyboard-workbench.tsx index 7bb41bc..1f2167c 100644 --- a/web/components/storyboard-workbench.tsx +++ b/web/components/storyboard-workbench.tsx @@ -15,6 +15,7 @@ interface Props { onJobUpdate?: (j: Job) => void clipboard: ImageRef | null // 全局剪贴板(page.tsx 提供) focusedFrame: number | null + videoConfigured?: boolean onGenerateVideo?: (frameIdx: number, scene: StoryboardScene, model: string) => Promise | void } @@ -28,7 +29,7 @@ const VIDEO_MODELS = [ { value: "veo3", label: "Veo 3" }, ] as const -export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobUpdate, clipboard, focusedFrame, onGenerateVideo }: Props) { +export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobUpdate, clipboard, focusedFrame, videoConfigured, onGenerateVideo }: Props) { const [mounted, setMounted] = useState(false) useEffect(() => setMounted(true), []) @@ -124,6 +125,7 @@ export function StoryboardWorkbench({ job, selectedFrames, open, onClose, onJobU const hasVideoRefs = !!(form.subject_image || form.scene_image || form.product_image || form.action_image) const currentModelLabel = VIDEO_MODELS.find((m) => m.value === videoModel)?.label ?? "Seedance" + const videoUnavailable = videoConfigured === false return (
+ {videoUnavailable && ( +
+ 当前 SKG ezlink 只连通基础网关,但未开通生视频 /videos 端点;这里先禁用提交,避免继续产生失败任务。 +
+ )}
- 用当前 4 图槽、改造目标和时长提交生视频 API;生成中的进度和完成后的 MP4 会显示在 Video Gen 节点。 + {videoUnavailable + ? "需要 IT 给当前 key 分组开通 /videos,或配置外部 VIDEO_API_BASE_URL/VIDEO_API_KEY 后再生成。" + : "用当前 4 图槽、改造目标和时长提交生视频 API;生成中的进度和完成后的 MP4 会显示在 Video Gen 节点。"}
diff --git a/web/lib/api.ts b/web/lib/api.ts index 9a629bd..c21f890 100644 --- a/web/lib/api.ts +++ b/web/lib/api.ts @@ -146,12 +146,33 @@ export interface Job { error?: string } +export interface BackendHealth { + ok: boolean + llm_configured: boolean + base_url: string + models?: { + asr?: string + translate?: string + rewrite?: string + video?: string + video_aliases?: Record + video_base_url?: string + video_configured?: boolean + } +} + export function apiAssetUrl(path?: string | null): string { if (!path) return "" if (/^https?:\/\//i.test(path)) return path return `${API_BASE}${path.startsWith("/") ? "" : "/"}${path}` } +export async function getHealth(): Promise { + const res = await fetch(`${API_BASE}/health`) + if (!res.ok) throw new Error(`health ${res.status}`) + return res.json() +} + export async function createJob(tkUrl: string): Promise { const res = await fetch(`${API_BASE}/jobs`, { method: "POST", @@ -372,7 +393,12 @@ export async function generateStoryboardVideo( }) if (!res.ok) { const txt = await res.text().catch(() => "") - throw new Error(`generateStoryboardVideo ${res.status} ${txt.slice(0, 300)}`) + let detail = txt + try { + const parsed = JSON.parse(txt) + detail = parsed?.detail || txt + } catch {} + throw new Error(detail || `generateStoryboardVideo ${res.status}`) } return res.json() }