diff --git a/core/frontend/src/routes/pipeline.tsx b/core/frontend/src/routes/pipeline.tsx index 4d1035d..652a14b 100644 --- a/core/frontend/src/routes/pipeline.tsx +++ b/core/frontend/src/routes/pipeline.tsx @@ -391,7 +391,7 @@ type StepNode = { id: string; label: string; done?: boolean; think?: boolean }; // 行33 · 进度时间轴(参考主流 AI 应用的流式+推理体感): // 竖线串起每步;当前步状态字「光扫」+ 秒数;generate 步思考态用模型实时思考最新一句做状态字, // 右侧 › 折叠/展开思考全文(默认收起);完成后收成「已完成思考 · 用时 Xs」。非推理模型无思考节点,照常跑步骤。 -function ProgressTimeline({ steps, reasoning, stream, done, startedAt }: { steps?: StepNode[]; reasoning?: string; stream?: string; done?: boolean; startedAt?: number }) { +function ProgressTimeline({ steps, reasoning, stream, done, startedAt, thinkingElapsedSeconds, thinkingDurationReady }: { steps?: StepNode[]; reasoning?: string; stream?: string; done?: boolean; startedAt?: number; thinkingElapsedSeconds?: number; thinkingDurationReady?: boolean }) { const [openThink, setOpenThink] = useState(false); const [now, setNow] = useState(() => Date.now()); const bodyRef = useRef(null); @@ -403,7 +403,9 @@ function ProgressTimeline({ steps, reasoning, stream, done, startedAt }: { steps useEffect(() => { if (openThink && bodyRef.current) bodyRef.current.scrollTop = bodyRef.current.scrollHeight; }, [reasoning, openThink]); const list = steps ?? []; - const elapsed = startedAt ? Math.max(0, Math.round((now - startedAt) / 1000)) : 0; + const liveElapsed = startedAt ? Math.max(0, Math.round((now - startedAt) / 1000)) : 0; + // “已完成思考”只展示生成分镜阶段冻结下来的时长;历史记录重进页面时不能以当前时间重算。 + const elapsed = thinkingDurationReady && thinkingElapsedSeconds != null ? thinkingElapsedSeconds : liveElapsed; const activeId = !done ? ([...list].reverse().find((s) => !s.done)?.id ?? null) : null; // 思考流最新一句(按句末标点/换行切)= 动态状态字,不写死 const latestThought = (() => { @@ -420,7 +422,9 @@ function ProgressTimeline({ steps, reasoning, stream, done, startedAt }: { steps const hasThink = s.id === "generate" && (reasoning ?? "").length > 0; let text = s.label; if (s.id === "generate") { - if (rowDone) text = hasThink ? `已完成思考 · 用时 ${elapsed}s` : (preamble || s.label); + if (rowDone) text = hasThink + ? (thinkingDurationReady && thinkingElapsedSeconds != null ? `已完成思考 · 用时 ${elapsed}s` : "已完成思考") + : (preamble || s.label); else if (s.think) text = latestThought || "思考"; else text = preamble || s.label; } @@ -1247,8 +1251,9 @@ export function PipelinePage(props: { const chatBodyRef = useRef(null); // 对话记录(本地会话态):生成动作可追溯,不再是「点了按钮、对话区永远空着」 // kind=progress:进度提示流(行33),steps 逐条滚动出现,done 后折叠成一行结果 - type ChatMsg = { id: number; role: "ai" | "user"; text: string; time: string; kind?: "progress"; steps?: StepNode[]; stream?: string; reasoning?: string; done?: boolean; startedAt?: number; auto?: boolean }; + type ChatMsg = { id: number; role: "ai" | "user"; text: string; time: string; kind?: "progress"; steps?: StepNode[]; stream?: string; reasoning?: string; done?: boolean; startedAt?: number; thinkingElapsedSeconds?: number; thinkingDurationReady?: boolean; auto?: boolean }; const nowHm = () => new Date().toTimeString().slice(0, 5); + const elapsedSinceStart = (startedAt?: number) => startedAt ? Math.max(0, Math.round((Date.now() - startedAt) / 1000)) : 0; const msgIdRef = useRef(1); const nextMsgId = () => msgIdRef.current++; // 脚本助手记录按项目持久化:退出项目→重进仍能看到历史对话(行36) @@ -1260,7 +1265,13 @@ export function PipelinePage(props: { const saved = JSON.parse(raw) as ChatMsg[]; if (Array.isArray(saved) && saved.length) { msgIdRef.current = Math.max(...saved.map((m) => m.id || 0)) + 1; - return saved; + // 兼容此前已落盘的完成记录:旧数据没有生成完成时刻,无法还原真实时长; + // 标为历史记录后仅显示“已完成思考”,不展示被自然时间放大的伪造秒数。 + return saved.map((msg) => ( + msg.kind === "progress" && msg.done && msg.steps?.some((step) => step.id === "generate") && !msg.thinkingDurationReady + ? { ...msg, thinkingDurationReady: false } + : msg + )); } } } catch { /* 解析失败则回退默认 */ } @@ -1434,7 +1445,12 @@ export function PipelinePage(props: { const idx = steps.findIndex((s) => s.id === sid); if (idx === -1) steps.push({ id: sid, label: label ?? sid, done: !running }); else steps[idx] = { ...steps[idx], ...(label ? { label } : {}), ...(running ? {} : { done: true }) }; - return { ...m, steps }; + // “生成分镜”结束即表示思考结束;实体提取、自检、保存都不再计入该文案的用时。 + const thinkingElapsedSeconds = sid === "generate" && !running && m.thinkingElapsedSeconds == null + ? elapsedSinceStart(m.startedAt) + : m.thinkingElapsedSeconds; + const thinkingDurationReady = sid === "generate" && !running ? true : m.thinkingDurationReady; + return { ...m, steps, ...(thinkingElapsedSeconds != null ? { thinkingElapsedSeconds } : {}), ...(thinkingDurationReady != null ? { thinkingDurationReady } : {}) }; })); } else if (evt.type === "reasoning" && typeof evt.text === "string") { // 推理模型思考流:逐字累积,并把 generate 步置「思考态」(状态字 = 思考最新一句) @@ -1458,7 +1474,7 @@ export function PipelinePage(props: { } else if (evt.type === "saved") { ok = true; } else if (evt.type === "error") { - setChatMsgs((list) => list.map((m) => (m.id === progressId ? { ...m, done: true } : m))); + setChatMsgs((list) => list.map((m) => (m.id === progressId ? { ...m, done: true, thinkingElapsedSeconds: m.thinkingElapsedSeconds ?? elapsedSinceStart(m.startedAt), thinkingDurationReady: m.thinkingDurationReady ?? false } : m))); const publicError = isPublicGenerationError(evt.error) ? evt.error : null; if (publicError) { const presentation = presentGenerationError(publicError); @@ -1489,7 +1505,7 @@ export function PipelinePage(props: { setStreamBusy(false); if (targetShotId) { setBusyShot(null); setRewriteShot(null); } else setScriptBusy(false); } - setChatMsgs((list) => list.map((m) => (m.id === progressId ? { ...m, done: true } : m))); + setChatMsgs((list) => list.map((m) => (m.id === progressId ? { ...m, done: true, thinkingElapsedSeconds: m.thinkingElapsedSeconds ?? elapsedSinceStart(m.startedAt), thinkingDurationReady: m.thinkingDurationReady ?? false } : m))); if (ok) { await onRefreshProject(); // 模型写了收尾就用它(真 agent 感);没写则兜底默认句 @@ -2777,7 +2793,7 @@ export function PipelinePage(props: {
{msg.kind === "progress" ? <> - + : }
diff --git a/docs/bug_todo/视频项目-脚本时间持续增长-bug-todo.md b/docs/bug_todo/视频项目-脚本时间持续增长-bug-todo.md new file mode 100644 index 0000000..33b53c4 --- /dev/null +++ b/docs/bug_todo/视频项目-脚本时间持续增长-bug-todo.md @@ -0,0 +1,46 @@ +# 视频项目|脚本时间持续增长 Bug TODO + +> 状态:代码修复完成,待页面确认(2026-07-16) +> 范围:仅视频项目 Pipeline Stage 1「脚本助手」内的历史生成进度卡;不影响其他页面、任务或计时展示。 +> 协作约定:本待办建立时不修改任何业务代码;待确认现象与预期后,再逐步定位与实施。 + +## 1. 当前问题描述 + +视频项目的脚本助手中,已完成的生成进度卡展示“已完成思考 · 用时 xxxs”。 + +同一条已完成记录在重新进入项目页面后,展示用时会持续变大。例如先显示 `455s`,再次进入后变为 `721s`;任务实际并未重新执行。 + +## 2. 已确认根因 + +- 前端在发起脚本生成时记录了开始时间。 +- 生成完成后,该开始时间随脚本助手对话历史保存到浏览器本地。 +- 再次进入页面时,已完成卡片仍使用“当前时间 − 原开始时间”重新计算用时,因此经过的自然时间被误算进任务时长。 +- 当前计时还会覆盖脚本生成之后的实体提取、自检、保存等步骤;这与“已完成思考”的文案语义不一致。 + +## 3. 确认的目标效果 + +- “已完成思考 · 用时 xxxs”必须是一次脚本生成的固定历史结果,刷新、离开或重新进入项目后数值不变。 +- 当“按黄金结构生成分镜”完成、流程进入“提取实体”时,思考用时立即冻结。 +- 后续的提取实体、自检、落库与页面刷新不计入“思考用时”。 +- 修复仅作用于视频项目 Pipeline Stage 1 脚本助手的这类进度卡;不改动其他功能、页面或任务的计时逻辑。 + +## 4. 待实施方案(最小范围) + +1. 在脚本助手的单条进度记录中,增加独立的“思考结束时间”或已冻结的思考时长。 +2. 收到“生成分镜完成”事件时立即写入该固定值,并随该条历史对话保存。 +3. 已完成卡片优先展示固定值,禁止用重新打开页面时的当前时间重算。 +4. 验证首次生成、刷新页面、离开后重新进入、历史多条记录四种情况,确认数值均不会继续增长。 + +## 5. 非目标 + +- 不调整脚本生成、实体提取、自检、落库等业务执行顺序。 +- 不修改后端任务计费、任务状态或数据库历史数据。 +- 不改动其他页面、其他 Pipeline 阶段和其他 AI 任务的计时展示。 + +## 6. 实施结果(2026-07-16) + +1. 已仅在 Pipeline Stage 1 脚本助手的进度消息数据中新增冻结的思考时长字段。 +2. 后端流返回“生成分镜完成”时,前端立即记录并保存该秒数;后续实体提取、自检、保存不再改变它。 +3. 已完成的历史进度卡优先使用该固定秒数,不再以重新进入页面时的当前时间计算。 +4. 对修复前已保存在浏览器本地的历史记录,由于旧数据未保存真正的完成时刻,无法反推出其最初的准确耗时;这类卡片仅显示“已完成思考”,不再展示误导性的秒数。 +5. 前端生产构建已通过。待在页面新生成一次脚本后确认:进入“提取实体”时数字停止,刷新或重新进入项目后数字保持不变。