From 65613de3b4e5bbae0eabcd1ce5b32f2880610af4 Mon Sep 17 00:00:00 2001 From: ddCat <2695092045@qq.com> Date: Tue, 15 Sep 2026 14:21:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(canvas):=20Agent=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=BD=A8=E8=BF=B9=20-=20=E5=90=88=E5=B9=B6=E5=AA=92=E4=BD=93?= =?UTF-8?q?=E7=94=9F=E6=88=90=E9=87=8D=E5=A4=8D=E4=BA=8B=E4=BB=B6=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E5=BE=85=E6=B5=8B=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/content/docs/progress/pending-test.mdx | 32 +++++++ .../canvas/canvas-cloud-agent-panel.tsx | 89 ++++++++++++++++++- 2 files changed, 119 insertions(+), 2 deletions(-) diff --git a/docs/content/docs/progress/pending-test.mdx b/docs/content/docs/progress/pending-test.mdx index 5eea03be6..3b649c853 100644 --- a/docs/content/docs/progress/pending-test.mdx +++ b/docs/content/docs/progress/pending-test.mdx @@ -5,6 +5,28 @@ description: 当前项目已实现功能的手工验证清单与未确认边界 # 待测试 +## 思考模型 Agent 工具选择兼容 + +- 当前 Cloud Agent 和首页创造规划入口均使用自动工具选择,不固定发送 `tool_choice=required`。后端仍防御旧任务、旧客户端和其他结构化调用携带的强制选择。 +- 首页创造规划提示词只声明真实注册的 `creative_respond`,并把画布、模型和素材上下文明确标记为只读快照;不再诱导模型调用并不存在的 `canvas_*` 工具或假报节点已创建、分镜已生成。 +- OpenAI Chat Completion 兼容协议的自动选择在首个上游请求中直接省略 `tool_choice`;显式思考模式还会移除旧任务携带的 `required` 或指定函数对象,内置协议和声明式已知协议均有回归覆盖。非思考模式保留强制函数选择,上游隐式思考仍保留 `required` → `auto` → 省略字段的失败后兜底。 +- 已通过 Agent 工具选择专项后端测试;待用百炼 `qwen3.7-flash` 与 DeepSeek 思考模型真实渠道分别验收首请求正文,确认不再出现强制工具选择参数错误,且模型仍能自行调用画布工具。 + +## Agent 模型能力筛选与跨轮草稿续用 + +- 模型工具支持按生成模式和真实参考节点筛选,复用统一模型目录的能力匹配;多图视频不再依赖模型名称猜能力。具体时长、画幅、音频和价格仍由目录配置与任务准入共同约束,不提供静默换模型兜底。 +- 可续用未提交的空白媒体占位节点、当前运行草稿和已结束且清理完成的同用户同画布草稿,保持原节点 ID;仍在审批、清理中、已提交任务、已有成品、锁定节点和跨用户 / 画布草稿不可接管。续用重新审批,不跳过收费授权。 +- 媒体策略明确用户要求生成且参数齐备时直接调用生成工具进入审批;仅改词不擅自收费。此行为仍需真实模型验收,不能以策略文本测试代替模型实际执行。 +- 已通过后端 app / prompts 两包完整测试;最终引用连线调整后再次通过 Agent 专项回归。覆盖双图筛选、无候选不兜底、非法引用与跨用户拒绝、草稿状态与权限边界、跨轮原节点续用、新审批前无媒体任务、审批后只提交一次及移除过期引用连线。未调用真实收费模型。 +- 待验收:配置文生视频和多图视频模型,带两张参考图选择正确候选;上一轮取消后在原节点继续生成并出现新审批卡;确认不重复建节点、只填词不假报已提交、拒绝审批不收费。 + +## Agent 节点能力卡与结构化分镜 + +- 节点能力 registry 已升级,文本、Markdown、图片、视频、音频、背板和分镜脚本均向 Agent 返回用途、适用场景、不适用场景和维护代价;媒体能力卡还区分单图、单镜头文生视频、图生视频、多图参考视频与音频用途,并明确仍须按真实参考节点调用 `model_list` 筛选模型。 +- 节点选择采用软路由:单画面和临时试验可使用轻量节点;多镜头、连续性、逐镜审查 / 生成、资产关联或交接维护时优先结构化分镜。系统不强制所有任务创建分镜,但禁止把普通文本宣称为具备镜头级维护能力的分镜表。 +- 新增分镜脚本创建、分页读取和单行追加 / 修改 / 删除工具。镜头行 ID 由服务端生成;修改和删除必须使用最新读取的真实 `rowId` 与 `snapshotHash`,写入继续经过现有权限、审批、快照冲突和画布持久化路径。 +- 后端相关专项测试、前端生产构建和文档类型检查已通过。待真实模型和浏览器验收:简单单镜头自主选择轻量节点,多镜头任务创建结构化分镜;审批前画布不变化,审批后可在分镜 UI 中逐镜编辑,连续对话不改用 Markdown 重建整表。 + ## 混合节点画布 Agent - 未注册 Agent 能力的节点(包括插件、配置、绘图节点)不再阻断整张画布的 Agent 摘要与分页读取;返回基础信息和 `agentSupported: false`,不读取任意 metadata,也不授予创建、修改或媒体引用能力。 @@ -1854,3 +1876,13 @@ SSE 现在按 revision 变化发送紧凑快照,无变化只 heartbeat;客 仍待:隔离 PostgreSQL + Redis + mock 上游的 50/100/500 活跃运行压测、多实例断线重连、worker kill/restart、DB/Redis 短暂不可用、接单后响应丢失,以及取消落库后账务 / 上游取消收尾窗口的端到端验收。当前默认 Agent worker 并发和渠道并发仍需按线上资源、模型额度和连接池共同压测,不能据本轮单测承诺 500 用户可接受的 p95 或队列时延。 迁移 v14 发布前备份数据库并先在副本演练;新程序写入的 pending 控制字段不保证旧程序理解,回滚旧程序前必须确认没有待收尾运行。若迁移因损坏运行中止,应先导出并核对该运行及关联任务,再修复或人工处理,禁止跳过校验强行上线。 + +## 云端 Agent 审批拒绝与节点能力路由(2026-09-15) + +已实施,需真实环境验收: + +- [x] 审批拒绝改为独立终态 `rejected`,不再把用户决定包装为 `tool_failed`,也不会继续推进模型调用、画布写入、收费生成或扣费;前端展示“已拒绝”并保留可继续对话的上下文。 +- [x] 新轮次首个 system prompt 直接注入由服务端 Capability Registry 生成的节点能力速查,说明轻量节点、媒体节点与分镜脚本的适用场景、限制和维护取舍;模型自主决策,不强制调用能力查询或分镜工具。 +- [x] 审批预览支持创建/修改分镜脚本的操作标签;后端能力校验仍是最终约束。 +- [ ] 真实待审批轮次点击“拒绝”,确认只出现一次“已拒绝本次操作,未写入画布”,没有“更新画布内容失败”或随后“执行失败”,SSE 在 `rejected` 终态正常关闭。 +- [ ] 真实多镜头请求确认 Agent 能基于能力速查选择结构化分镜;单画面快速试验不应被强制创建分镜脚本。 diff --git a/web/src/components/canvas/canvas-cloud-agent-panel.tsx b/web/src/components/canvas/canvas-cloud-agent-panel.tsx index d5353c042..48d4c83af 100644 --- a/web/src/components/canvas/canvas-cloud-agent-panel.tsx +++ b/web/src/components/canvas/canvas-cloud-agent-panel.tsx @@ -1131,12 +1131,97 @@ function applyAgentEvent(event: AgentEvent, setMessages: Dispatch appendUniqueMessage(current, { id, role: "tool", title: "canvas_apply_ops", text: text || "画布操作已完成", detail: { ...payload, eventType: event.type } })); return; } - if (event.type.startsWith("tool_") || event.type === "generation_task_created") { - setMessages((current) => appendUniqueMessage(current, { id: event.eventId, role: "tool", title: String(payload.toolName || payload.title || "工具执行"), text: text || event.type, detail: { ...payload, eventType: event.type } })); + if (event.type === "generation_task_created") { + const message: CloudAgentChatMessage = { id: event.eventId, role: "tool", title: "generate_media", text: text || event.type, detail: { ...payload, eventType: event.type } }; + setMessages((current) => upsertMediaToolTrace(current, message)); + return; + } + if (event.type.startsWith("tool_")) { + const message: CloudAgentChatMessage = { id: event.eventId, role: "tool", title: String(payload.toolName || payload.title || "工具执行"), text: text || event.type, detail: { ...payload, eventType: event.type } }; + if (payload.toolName === "generate_media") { + setMessages((current) => upsertMediaToolTrace(current, message)); + } else { + setMessages((current) => appendUniqueMessage(current, message)); + } return; } if (event.type === "run_failed" || event.type === "error") setMessages((current) => appendAgentError(current, event.eventId, text || "Agent 执行失败")); } +function toolDetailRecord(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; +} + +function toolDetailNodeIds(detail: unknown): Set { + const payload = toolDetailRecord(detail); + const ids = new Set(); + for (const value of [payload.nodeId, toolDetailRecord(payload.result).nodeId]) { + if (typeof value === "string" && value) ids.add(value); + } + if (Array.isArray(payload.actions)) { + for (const action of payload.actions) { + const nodeId = toolDetailRecord(action).nodeId; + if (typeof nodeId === "string" && nodeId) ids.add(nodeId); + } + } + if (typeof payload.arguments === "string") { + try { + const args = toolDetailRecord(JSON.parse(payload.arguments)); + if (Array.isArray(args.ops)) { + for (const op of args.ops) { + const nodeId = toolDetailRecord(op).id; + if (typeof nodeId === "string" && nodeId) ids.add(nodeId); + } + } + } catch { + // Tool arguments are diagnostic data; a malformed value must not break the event feed. + } + } + return ids; +} + +function toolDetailTaskIds(detail: unknown): Set { + const payload = toolDetailRecord(detail); + const ids = new Set(); + for (const value of [payload.taskId, toolDetailRecord(payload.result).taskId]) { + if (typeof value === "string" && value) ids.add(value); + } + return ids; +} + +function mergeToolDetails(previous: unknown, next: unknown): Record { + const previousDetail = toolDetailRecord(previous); + const nextDetail = toolDetailRecord(next); + return { + ...previousDetail, + ...nextDetail, + actions: Array.isArray(nextDetail.actions) ? nextDetail.actions : previousDetail.actions, + arguments: nextDetail.arguments || previousDetail.arguments, + }; +} + +function upsertMediaToolTrace(current: CloudAgentChatMessage[], message: CloudAgentChatMessage): CloudAgentChatMessage[] { + const nextNodeIds = toolDetailNodeIds(message.detail); + const nextTaskIds = toolDetailTaskIds(message.detail); + const index = current.findIndex((item) => { + if (item.role !== "tool") return false; + const itemToolName = item.title || ""; + if (itemToolName !== "canvas_apply_ops" && itemToolName !== "generate_media") return false; + const itemNodeIds = toolDetailNodeIds(item.detail); + const itemTaskIds = toolDetailTaskIds(item.detail); + return [...nextNodeIds].some((id) => itemNodeIds.has(id)) || [...nextTaskIds].some((id) => itemTaskIds.has(id)); + }); + if (index < 0) return appendUniqueMessage(current, message); + const next = [...current]; + const previous = next[index]; + next[index] = { + ...previous, + ...message, + id: previous.id, + detail: mergeToolDetails(previous.detail, message.detail), + }; + return next; +} + function appendUniqueMessage(current: CloudAgentChatMessage[], message: CloudAgentChatMessage) { return current.some((item) => item.id === message.id) ? current : [...current, message]; }