fix(agent): 创作执行 - 修复会话恢复并校验画风与真实分镜行

This commit is contained in:
王志伟
2026-09-08 22:24:17 +08:00
parent 2856bc7f58
commit 45de256deb
13 changed files with 179 additions and 27 deletions

View File

@@ -15,7 +15,7 @@ import { runBackendToolGenerationTask } from "@/services/api/generation-task";
import { inspectAgentImage } from "@/services/agent-image-preview";
import { AGENT_CAPABILITY_GUIDANCE, buildAgentPluginOperations, listAgentCapabilities, readAgentPluginDocumentation, readAgentPluginNode } from "@/services/agent-capabilities";
import { getNodeDefinition } from "@/lib/canvas/node-registry";
import { buildAgentStoryboardOperations, readAgentStoryboard } from "@/lib/canvas/canvas-agent-storyboard";
import { buildAgentStoryboardOperations, readAgentStoryboard, validateAgentStoryboardCreation } from "@/lib/canvas/canvas-agent-storyboard";
import { canvasStylePresets, userStylePreset, type CanvasStylePreset } from "./canvas-style-picker-modal";
import { listStyleProfiles } from "@/services/api/style-profiles";
import { getActiveUserScope } from "@/lib/user-scope";
@@ -879,8 +879,8 @@ export function CanvasAssistantPanel({
}
if (name === "canvas_read_plugin_node") return { ok: true, message: "已读取插件节点状态。", data: readAgentPluginNode(current, requireString(args.nodeId, "nodeId")) };
if (name === "canvas_read_storyboard") {
const { node, rows } = readAgentStoryboard(current, requireString(args.nodeId, "nodeId"));
return { ok: true, message: "已读取分镜表。", data: { nodeId: node.id, title: node.title, rows } };
const { node, rows, readiness } = readAgentStoryboard(current, requireString(args.nodeId, "nodeId"));
return { ok: true, message: "已读取分镜表及画风前置条件。", data: { nodeId: node.id, title: node.title, rows, readiness } };
}
if (name === "canvas_list_styles" || name === "canvas_apply_style") {
const originScope = getActiveUserScope();
@@ -1564,13 +1564,13 @@ function parseToolArguments(value: string) {
export function onlineToolToOps(name: string, input: Record<string, unknown>, snapshot: CanvasAgentSnapshot, config: AiConfig): CanvasAgentOp[] {
if (name === "canvas_edit_storyboard") return buildAgentStoryboardOperations(snapshot, input);
if (name === "canvas_plugin_action") return buildAgentPluginOperations(requireString(input.pluginId, "pluginId"), requireString(input.actionId, "actionId"), recordOptional(input.input) || {}, snapshot);
if (name === "canvas_apply_ops") return requireOps(input.ops);
if (name === "canvas_apply_ops") return validateAgentStoryboardCreation(requireOps(input.ops));
if (name === "canvas_create_workflow") return buildCanvasWorkflowOps(input as unknown as CanvasWorkflowInput, snapshot, config);
if (name === "canvas_create_node") {
const nodeType = requireNodeType(input.nodeType);
const x = numberOr(input.x, nextCanvasX(snapshot));
const y = numberOr(input.y, 0);
return [{ type: "add_node", nodeType, title: stringOptional(input.title), position: { x, y }, width: numberOptional(input.width), height: numberOptional(input.height), metadata: recordOptional(input.metadata) as CanvasNodeData["metadata"] }];
return validateAgentStoryboardCreation([{ type: "add_node", nodeType, title: stringOptional(input.title), position: { x, y }, width: numberOptional(input.width), height: numberOptional(input.height), metadata: recordOptional(input.metadata) as CanvasNodeData["metadata"] }]);
}
if (name === "canvas_create_text_node") return [textNodeOp(input, numberOr(input.x, nextCanvasX(snapshot)), numberOr(input.y, 0))];
if (name === "canvas_create_text_nodes") {

View File

@@ -57,12 +57,14 @@ export function CanvasCreativeInteraction(props: Props) {
const quoteHistory = useRef<CreativeQuote[]>(detail.quotes || []);
const answersEdited = useRef(false);
const presentationAttempted = useRef(false);
const hadControl = useRef(false);
const scope = getActiveUserScope();
const invoke = (promise?: Promise<unknown>) => { if (promise) { setError(""); void promise.catch((cause) => { if (!(cause instanceof DOMException && cause.name === "AbortError")) setError(cause instanceof Error ? cause.message : "操作未完成"); }); } };
useEffect(() => {
let live = true;
presentationAttempted.current = false;
hadControl.current = false;
const original = canvasCreativeDetail(latest.current.message)!;
setView({ state: original.state, busy: false, hasControl: false });
const instance = new CreativeAgentController({
@@ -72,6 +74,9 @@ export function CanvasCreativeInteraction(props: Props) {
onOpenCanvas: () => setError("当前会话仅操作所在画布,请在对应画布打开历史会话。"),
onChange: (next) => {
if (!live) return;
// Recovery clears the controller error and the rejected action's local UI error together.
if (next.hasControl && !hadControl.current && !next.error) setError("");
hadControl.current = next.hasControl;
setView(next); latest.current.onBusy(props.message.id, next.busy);
if (!next.run || !latest.current.active) return;
if (next.quote) quoteHistory.current = [...quoteHistory.current.filter((quote) => quote.id !== next.quote!.id), next.quote];
@@ -126,7 +131,9 @@ export function CanvasCreativeInteraction(props: Props) {
props.onContinue("保留已确认的要求,在已授权范围内继续当前任务,自行处理常规选择和技术问题;确实阻塞时再询问。");
};
const rawError = proposalFailure || error || view.error;
const feedback = rawError ? /恢复|连接|网络|接管|执行权|代次/.test(rawError) ? "连接暂时中断,正在恢复。已有作品会保留。"
const failedGenerations = view.state.media.filter((item) => item.status === "failed" && item.failureKind !== "observation");
const onlyFailedRemaining = failedGenerations.length > 0 && !view.state.media.some((item) => ["queued", "running", "write_failed"].includes(item.status) || item.failureKind === "observation" && item.status === "failed");
const feedback = onlyFailedRemaining ? "部分作品生成失败。请在失败作品下方重新生成,先核对费用;已成功的作品会保留。" : rawError ? /恢复|连接|网络|接管|执行权|代次/.test(rawError) ? "状态读取未完成,可以继续处理重试。已有作品会保留。"
: /报价|费用|价格|过期/.test(rawError) ? "生成费用需要重新确认,继续后会显示最新价格。"
: /素材|参考|引用/.test(rawError) ? "参考素材还需要核对,助手会继续处理,必要时请你选择。"
: "这一步还没有完成。已有内容已保留,可以继续处理或修改要求。" : undefined;
@@ -147,23 +154,27 @@ export function CanvasCreativeInteraction(props: Props) {
{view.state.pendingEdits && <div className="creative-agent-card"><p>将调整 {view.state.pendingEdits.length} 个节点的指定字段。</p><Button disabled={blocked} onClick={() => invoke(controller.current?.approveEdits())}>确认节点调整</Button></div>}
{props.active && view.quote && <CreativeQuoteCard quote={view.quote} busy={view.busy} disabled={blocked} onApprove={() => invoke(controller.current?.approvePayment())} onRefresh={() => invoke(controller.current?.refreshQuotes())} />}
{(detail.quotes || []).filter((quote) => !props.active || quote.id !== view.quote?.id).map((quote) => <details key={quote.id} className="creative-agent-receipt"><summary>{quote.approvedQuantity ? "已确认生成" : "历史报价"} · {quote.amountLabel} · {quote.items.length} 项</summary><p>{quote.basis}</p><ul>{quote.items.map((item) => <li key={item.id}>{item.label} · {item.model} · {item.specification}</li>)}</ul></details>)}
{view.state.media.filter((item) => item.storageKey || item.error).map((item) => {
{view.state.media.filter((item) => item.taskId || item.storageKey || item.error).map((item) => {
const resource = resourceIdFromStorageKey(item.storageKey), url = resource ? resourceFileUrl(resource) : "";
const video = view.state.proposal?.generationItems.find((entry) => entry.ref === item.ref)?.mode === "video";
return <div className="creative-agent-card" key={item.ref}>
<strong>{view.state.proposal?.workflow.nodes.find((node) => node.ref === item.ref)?.title || item.ref}</strong>
{!item.error && !url && item.taskId && <p className="creative-agent-muted">{view.busy ? item.status === "running" ? "正在生成,完成后自动显示。" : "已提交,正在等待生成结果。" : "任务已提交。点击下方“继续处理”读取最新结果,无需重复确认费用。"}</p>}
{url && (video ? <video className="w-full max-h-80 object-contain" src={url} controls preload="metadata" /> : <img className="w-full max-h-80 object-contain" src={url} alt={item.ref} />)}
{item.error && <p className="creative-agent-muted">这项作品还需要处理,已有结果会保留。</p>}
{item.error && <details className="creative-agent-receipt"><summary>查看失败原因</summary><p>{item.error}</p></details>}
{props.active && item.status === "failed" && item.failureKind !== "observation" && <Button type="primary" disabled={blocked} onClick={() => invoke(controller.current?.redo(item.ref))}>重新生成此项 · 先确认费用</Button>}
<div className="creative-agent-actions">{url && <a href={url} target="_blank" rel="noreferrer">查看作品</a>}{props.active && <Dropdown trigger={["click"]} disabled={blocked} menu={{ items: [{ key: "adjust", label: "调整作品" }, { key: "redo", label: "重新生成(费用另行确认)" }], onClick: ({ key }) => key === "redo" ? invoke(controller.current?.redo(item.ref)) : modify(`请调整《${view.state.proposal?.workflow.nodes.find((entry) => entry.ref === item.ref)?.title || item.ref}》,保留其他作品:`) }}><Button type="text" disabled={blocked}>更多</Button></Dropdown>}</div>
</div>;
})}
{!props.active && rawError && <details className="creative-agent-receipt"><summary>这一步未完成,后续对话中可继续调整</summary><p>{feedback}</p></details>}
{props.active && <div className="creative-agent-next" role="status">
<p>{view.busy ? "正在处理,请稍候…" : !view.hasControl ? "正在恢复当前创作,请稍候…" : view.state.modificationRequested ? "在下方告诉我想改哪里,调整后再确认。" : feedback || (view.quote ? "确认费用后,就开始本批制作。" : awaitingProposal ? "看看方案是否符合你的想法,确认后继续。" : question?.status === "pending" ? "选择一个答案,也可以在下方直接补充。" : status === "completed" ? "本阶段已完成。可以查看作品,或告诉我想改哪里。" : "准备好了,可以继续。")}</p>
<p>{view.busy ? "正在处理,请稍候…" : !view.hasControl ? rawError ? "暂时无法恢复当前创作,可以重试连接。已有作品会保留。" : "正在恢复当前创作,请稍候…" : view.state.modificationRequested ? "在下方告诉我想改哪里,调整后再确认。" : feedback || (view.quote ? "确认费用后,就开始本批制作。" : awaitingProposal ? "看看方案是否符合你的想法,确认后继续。" : question?.status === "pending" ? "选择一个答案,也可以在下方直接补充。" : status === "completed" ? "本阶段已完成。可以查看作品,或告诉我想改哪里。" : "准备好了,可以继续。")}</p>
<div className="creative-agent-actions">
{!view.busy && !view.hasControl && <Button onClick={() => invoke(view.run ? controller.current?.takeControl() : controller.current?.load(detail.runId, detail.state))}>重试连接</Button>}
{view.busy && ["running", "waiting_task", "waiting_canvas"].includes(status || "") ? <Button onClick={() => invoke(controller.current?.pause())}>停止后续制作</Button>
: !view.busy && view.hasControl && !view.state.modificationRequested && !view.quote && !awaitingProposal && !view.state.pendingEdits && question?.status !== "pending" ? <>
<Button type="primary" onClick={() => status === "completed" && !feedback ? props.onReview() : invoke(continueWork())}>{status === "completed" && !feedback ? "继续创作" : "继续处理"}</Button>
{!onlyFailedRemaining && <Button type="primary" onClick={() => status === "completed" && !feedback ? props.onReview() : invoke(continueWork())}>{status === "completed" && !feedback ? "继续创作" : "继续处理"}</Button>}
<Button onClick={() => modify("我想调整当前创作要求:")}>修改</Button>
</> : null}
</div>