feat(canvas): 创作助手 - 合并创作对话、能力发现与画布接续

This commit is contained in:
ddCat
2026-09-09 22:46:45 +08:00
97 changed files with 8434 additions and 326 deletions

View File

@@ -0,0 +1,70 @@
---
title: Agent 能力发现与插件动作
description: 在线画布助手的节点发现、技能渐进读取和插件动作接入合同
---
# Agent 能力发现与插件动作
在线画布助手通过 `canvas_list_capabilities` 查询实时节点注册表和有效启用的插件。内置节点及插件贡献不再依赖助手中的固定节点类型枚举。发现返回名称、来源、输入约束、默认数据和插件提供的动作;发现到声明不代表专用执行器已经存在。
## 技能自动发现
`canvas_list_skills` 支持 `query`、`offset`、`limit`,默认每页 40 条、最多 100 条,返回 `items`、`total`、`nextOffset`。助手先根据任务检索元数据,再读取 `SKILL.md`,按入口引用搜索、读取关联文件。无需用户手动选中技能;是否选用由模型结合任务判断,不保证每次都调用技能。
在线技能工具执行时重新取得当前已加入技能目录,复用账号隔离缓存和安装、移除时的缓存失效机制。插件文档可通过 `canvas_read_plugin` 按已发现 ID 读取。文档和技能说明不扩大用户授权,也不意味着浏览器可以执行技能包中的任意脚本。
## 专业短剧节点
Agent 拆镜与分镜节点底部输入共用 `generateScriptRows`:调用 `canvas_generate_storyboard` 后,由现有 `agent_storyboard_rows` 任务结合画风、角色、画布资产和短剧技能生成真实行。原始剧本保存在 `composerContent`;没有节点时可用 `canvas_create_storyboard` 创建明确标为待拆镜的空表,此时不得宣称镜头已完成。此入口用于已获准的搭建任务,不取消创作方案确认;通用方案直接交付镜头时仍要求完整 `script.shots`。
受管模型先用 `creation-runs` 准备真实报价,用户确认后批准并提交;外部自定义渠道沿用明确说明外部费用的提交确认,不伪造价格。整表重拆确认会说明替换镜头行、保留原媒体;取消或报价过期不提交。局部修改继续使用 `canvas_edit_storyboard`。专业拆镜完成后 Agent 继续读取真实表格检查,再进行后续操作;取消时停止本轮。单镜时长选项不表示总片长,总片长应在剧本要求中说明。
创作方案和通用工作流支持 `styleboard`、`story_input`、`script`,复用手动短剧入口的数据结构。画风节点初始为空,显示系统已有的选择入口;故事节点使用专用编辑卡片;分镜节点使用真实表格。
`script.shots` 必须逐镜填写正数 `durationSeconds` 和非空 `videoMotionPrompt`,`dialogue` 按需填写,最多 100 行,写入稳定行 ID。仅有正文或提示词的分镜方案会返回校验错误供模型修正;通用节点创建工具也校验真实分镜行,不能以空表代替交付。方案连到分镜节点时使用 `storyboard:context` 输入。仅创建这些节点不产生媒体生成项;图像、视频仍沿用既有方案和费用确认。
`canvas_get_context.storyboardReadiness` 与 `canvas_read_storyboard.readiness` 复用原分镜生成前置校验,返回画风状态、阻塞原因、真实行数量、未填完整的行 ID 和实际总时长。助手先检查这些状态;缺画风时读取真实目录,结合已确认风格与用户授权应用,只有关键取舍尚不明确时才追问。已有空表通过逐行编辑补齐,保留原行 ID 和素材关联;有足够剧本信息时直接编写镜头,不要求用户重复手动生成分镜。提示词引导不替代生成入口的业务校验,写入镜头也不代表媒体已生成。
`canvas_list_styles` 按 `source=system|user` 查询系统预设或当前用户保存的风格。`canvas_apply_style` 只接受该目录返回的真实 ID,复用页面的异步画风保存入口:关联项目时先保存项目设置,再应用画布节点;不关联项目时走原画布保存机制。异步返回时校验画布、项目及用户上下文,避免写到新页面。该动作是原画风选择逻辑,不冒充普通节点撤销能够回滚服务端项目设置;需要改回时重新应用原风格。
`canvas_read_storyboard` 返回真实行 ID、镜头内容及素材关联。`canvas_edit_storyboard` 支持 `append`、`update`、`remove`,按行 ID 操作;仅允许镜头文本与正数时长字段,不允许伪造任务状态或素材 ID。移除行保留生成素材,剩余行保留 ID 并重排序号。分镜编辑进入原画布操作、撤销与历史记录链路,不自动重新生成媒体。
## 插件动作接入
已在宿主注册的 `RegisteredPlugin` 可提供 `agentActions`:
```ts
agentActions: [{
id: "create-analysis-node",
description: "创建图片分析节点",
inputSchema: { type: "object", properties: {}, additionalProperties: false },
buildOperations(input, snapshot) {
// 在此校验本动作入参,仅返回操作,禁止请求模型或产生其他副作用。
return [{ type: "add_node", nodeType: "my-analysis-node" }];
},
}]
```
模型根据目录通过 `canvas_plugin_action` 提交 `pluginId`、`actionId`、`input`。动作构建器负责业务入参校验;宿主核对插件有效启用状态、动作存在性、`canvas.write` 权限、操作数量及生成操作的 `generation.run` 声明,再交给原画布执行链路。工具调用和结果沿用原会话记录。
构建器必须是纯函数:输入和快照以副本传入,返回 1 至 100 个操作,不自行网络请求、收费或写入资源。`inputSchema` 用于向模型说明合同,不替代动作构建器的校验。仅上传带节点声明的 JSON 插件不会凭空获得 JavaScript 执行器;执行器需要通过宿主支持的插件运行时接入。本接口没有增加任意远程代码执行能力。
插件还可提供 `readAgentNode(node, snapshot)`,将节点状态转换为有限的结构化结果,通过 `canvas_read_plugin_node` 读取。必须校验插件启用及 `canvas.read` 声明,不返回内嵌媒体和密钥。审美批改、肖像排查已接入 `prepare-input` 动作和状态读取;同一图片已有对应节点时复用,不重复创建。审美报告仅在当前唯一输入与报告指纹一致时返回,否则标为过期。
节点类型冲突会拒绝注册。插件停用后不再出现在能力目录,新建节点和调用插件动作都会失败,旧作品保留。未知节点类型不能降级为普通文本。
## 当前边界与后续接入
审美分析的手动入口和 `canvas_start_art_critique` 共用原插件面板及 `art-critique-execution`。Agent 准备输入后可打开分析并准备报价;已有当前图片的完整报告直接复用。只有用户明确要求重新分析时传 `retry=true`。打开面板后暂停本轮工具循环,同批后续操作不执行,避免等待费用确认期间重复调用模型;分析完成后用户可让 Agent 读取报告继续。这次接入使用原插件弹窗展示费用卡,尚未把费用卡嵌入聊天消息或完成后自动唤醒会话。
`ApprovedToolExecution` 复用已有 `creation-runs` 报价、批准和幂等执行接口。各阶段先准备真实请求,再展示费用;确认按钮只批准当时费用卡列出的提交 ID,后来到达的阶段继续等待。首批为场景理解及四个独立视觉分析,后续聚合、定位、复核和提示词按前面的实际结果报价,不预先虚构后续输入。按用量收费的金额是估算,不是封顶;自定义渠道另行标注外部费用。
各阶段通过现有后台文本任务运行,原图先准备为资源引用,任务正文不内嵌媒体。每轮最多 9 个不同阶段;节点保存 `billingRunId`、`lastRunId`、`lastRunModel` 和 `stageTaskIds`,任务元数据记录节点、执行和阶段标识。底层 pipeline 注入批准后的阶段执行器,不复制审美判断与解析逻辑。工具调用仍进入原会话历史;费用记录由服务端创作运行保存,节点保存恢复入口。
关闭面板或停止观察不代表后台任务取消或未收费。重新打开后,在图片、节点和模型未变时可选择“继续上次分析”:同阶段相同请求找回原报价或任务,已提交阶段沿用原任务 ID,已完成结果重新参与本地管线计算,未批准阶段继续等确认。报价过期且未提交时更新报价并重新确认;刷新后旧费用卡不能批准新报价。请求内容变化时后端拒绝复用,需重新开始,不静默新增收费阶段。
执行控制或报价错误会中止其他待确认阶段,不能被当作部分模型结果降级后宣称成功。已提交阶段的模型失败保留原管线的部分报告处理,不自动付费重试;整轮重新分析走新报价。当前仍是浏览器驱动的管线,关页后已提交后台任务继续,尚未提交的后续阶段不会自动运行。画布输入或账号变化、插件停用会阻止继续提交或回写旧结果。
本批接入在线画布助手。首页尚未改造成通用工具循环,本机 Agent 仍使用原技能包交付机制。现有插件只有声明而没有 `agentActions` 时,可发现贡献并创建节点,不宣称能自动执行它的专用分析、编辑或导出。
已补齐画风预设的模型选择与项目同步、分镜专用增删改,以及 Agent 发起审美分析报价。肖像排查涉及本机任务,仍使用插件自己的执行入口;后续接通该执行器,以及首页与本机端的同一能力目录。不得以任意 `metadata` 更新替代具有业务校验和费用语义的执行接口。

View File

@@ -0,0 +1,404 @@
---
title: 通用创作 Agent 动态规划与画布协作需求设计
description: 基于原 Agent 会话与现有画布能力,设计专业角色指导、动态问答、计划、分批执行及方案和费用确认
---
# 通用创作 Agent 动态规划与画布协作需求设计
> 状态:待用户审阅,尚未实施。本文描述目标行为,不代表当前系统已经具备这些能力。
>
> 第一批实施进度:用户批准先做有限灵活性调整,已接入自定义问题字段、专业角色指导、动态计划和手动请求结果分析;继续保留现有完整方案与费用合同。原型视觉同步应用于原 Agent 卡片与输入框上方计划条。本文其余任务级 run、方案分层、统一批次执行等仍是后续设计,不应视为已实现。
>
> 本文是在第一期融合实现之上的调整设计。与旧设计有冲突时,以本文审阅通过的条目作为后续实施依据;未涉及的现有能力继续保留。配套旧原型仅供布局参考,不代表本方案的动态执行协议。
## 1. 产品目标
在当前画布的原 Agent 会话中,让用户通过自然语言、素材和问答卡片完成创作。Agent 根据任务扮演导演、设计师、电商美工、营销策划等专业角色,自主理解需求、读取画布、提出创意、规划步骤,并在授权范围内使用真实工具推进任务。
核心原则:创作路径由模型决定;真实能力、资源归属、执行结果、版本与费用授权由程序决定。
典型体验:
1. 用户描述目标,Agent 读取当前画布与已有资料。
2. 信息不足时按需提问;信息充分时直接给出创意方案。
3. 用户修改或确认方案,确认前不创建画布节点。
4. Agent 在批准范围内分批创建正式节点、整理布局与连线。
5. 涉及收费生成时展示真实费用,等待明确确认。
6. 工具完成后,Agent 根据结果决定继续、补问、修正或结束。
7. 计划、问答、批准与结果保存在原历史会话中,刷新后可核对恢复。
以上是可能经过的环节,不是每次都要执行的固定七步。普通咨询、纯文案、修改节点、制作整套作品采用不同路径。
## 2. 范围与非目标
### 2.1 本次包含
- 原在线 Agent 的动态规划和执行闭环。
- 专业角色提示词、按需加载已有 Skills 和可用插件能力。
- 模型自定义问题、选项和需求字段。
- 动态计划、可修改方案、按批次操作当前画布。
- 统一画布写入与收费授权检查。
- 失败反馈、有限自动修正、局部重做、历史及恢复。
- 短剧、设计、电商、营销及跨场景任务的共同运行机制。
### 2.2 本次不包含
- 不替换为 Harness,不建设完整服务端画布 MCP。
- 不新增独立智能创作聊天页面或第二套输入框。
- 不创建草稿节点;确认前的方案只存在于会话中。
- 首页接续继续暂缓,不扩大当前画布内的实施范围。
- 不建设通用角色/问卷低代码编辑器、公开插件市场或任意脚本执行平台。
- 不承诺模型能够临时发明可执行节点、编码安装插件或调用未注册工具。
- 不将浏览器关闭后的持续 Agent 思考纳入本期。已提交后端任务可以继续,后续编排需页面恢复。
- 不以本次改造直接承诺长剧、配音、字幕、剪辑合成全链路;具体交付受已接入能力约束。
- 不实现多用户同时编辑同一画布的完整协作协议。
## 3. 当前代码依据与差距
| 当前入口 | 已有能力 | 本次要调整的限制 |
| --- | --- | --- |
| `web/src/components/canvas/canvas-assistant-panel.tsx` | 原会话、模型选择、Skills、画布工具循环、上下文和工具确认 | 控制逻辑集中在组件;原生成工具与新创作链路需要统一授权 |
| `web/src/components/canvas/canvas-creative-interaction.tsx` | 消息内问答、方案、报价、进度、历史 detail | 当前控制器按结构化消息创建,需转为同一任务跨多轮持续运行 |
| `web/src/lib/creation/creative-scenarios.ts` | 四类场景角色、字段、参考问法、方案章节与节点规则 | 场景字段与流程建议偏强,跨场景组合不自然 |
| `web/src/lib/creation/creative-agent-contract.ts` | 问题规范化、已知字段过滤、答案校验 | 未配置字段会被过滤,每轮硬性最多三题 |
| `web/src/lib/creation/creative-agent-tools.ts` | `creative_respond` 结构化输出 | 一个工具承担问答、方案、节点和生成配置,技术字段过早绑定 |
| `web/src/lib/creation/creative-agent-state.ts` | 方案、规格、依赖校验与计划展示 | 媒体节点必须立即配齐生成项;计划由固定阶段拼接 |
| `web/src/services/creative-agent-controller.ts` | 批准、画布提交、媒体依赖、报价与恢复 | 结果处理以既定流程为主,需要回到原 Agent 决策 |
| `web/src/services/api/creation-runs.ts` | run、租约、版本、审批、报价、任务与画布提交 API | 当前方案审批绑定具体 ops,不能直接视为可支持任意后续动作授权 |
| `backend/internal/service/creation.go`、`creation_canvas.go` | 权威批准、幂等、受限提交与任务执行 | 需要补充任务级计划、执行批次与批准范围之间的校验 |
已观察到的实际失败:视频节点使用了图片模型;后续方案遗漏 `generationItems`。目前已补上失败方案反馈和手动修正入口,但这只是错误恢复,不等于本设计的动态运行闭环。
## 4. 方案选择
| 方案 | 优点 | 问题 | 结论 |
| --- | --- | --- | --- |
| 只丰富场景提示词和选项 | 改动最少 | 字段限制、固定计划和执行分支仍然存在 | 不足以达到目标 |
| 原 Agent 负责决策,现有控制器负责受控执行 | 复用会话、画布、任务、审批与计费,逐步调整合同 | 要统一不同工具的执行边界 | 推荐 |
| 替换整套 Agent 框架与画布接入 | 可重新定义运行架构 | 迁移大,无法替代产品侧权限、账务和资源工作 | 本期不采用 |
## 5. 专业角色、Skills 与插件
### 5.1 角色配置从流程模板改为专业指导
建议角色配置保留:标识、展示名称、适用任务、专业目标、判断原则、质量检查点、参考案例和推荐 Skills。现有字段及问题参考作为建议,不作为唯一允许集合;节点规则中的真实业务约束保留为质量与素材要求。
| 角色 | 指导重点 | 不强制规定 |
| --- | --- | --- |
| 导演 | 人物动机、冲突、镜头叙事、节奏、连续性、现有剧本理解 | 题材、时长、角色数量、必须先做定妆图 |
| 视觉设计师 | 信息层级、构图、色彩、排版、品牌一致性 | 固定三种风格、固定海报结构 |
| 电商美工 | 商品真实性、购买动机、主图/详情/场景用途、卖点表达 | 必须做全套详情页、必须生成模特 |
| 营销策划 | 受众、传播目标、渠道、创意切入、行动引导 | 必须剧情化、必须制作视频 |
用户可以直接指定角色,也可以由 Agent 推荐;角色变化不清空已有需求。商品广告片允许组合电商、美术和导演指导。角色组合仍由同一个 Agent 完成,本期不引入多 Agent 调度。
### 5.2 按需加载
- 初始上下文提供精简角色和 Skill 目录,相关内容按需读取。
- 用户明确指定的角色/Skill 优先于系统推荐,仍不能绕过产品权限和收费规则。
- Skill 提供方法与规范;插件必须有实际注册的工具,才能增加执行能力。
- 工具目录声明输入、输出、读写影响、是否收费、可用条件及限制。
- 未提供抽帧、元素替换、合成等工具时,Agent 应指出缺口或提出可执行替代,不创建假功能节点。
- 现有原生节点与语义化工作流逐项审计后接入。第一批沿用已支持的文本、图片、视频;音频及其他专用节点按真实合同分步开放。
## 6. 上下文与需求记忆
### 6.1 每轮决策所需信息
1. 当前用户指令与最近对话。
2. 结构化需求摘要:目标、事实、偏好、限制、待解决问题、自主决定授权。
3. 当前画布 ID、真实节点、选区、关联资源与近期变化。
4. 当前任务、动态计划、方案版本和已批准范围。
5. 当前可用模型、工具、Skills 目录及所需能力详情。
6. 最新工具结果、失败原因、未完成操作和未消费报价。
原始历史完整保存,模型上下文采用摘要和按需读取,不要求把全部节点、全部 Skills、全部历史每轮重复发送。
### 6.2 需求字段
保留目标、时长、数量、比例、素材等可执行标准字段,允许模型补充如“结尾情绪”“品牌气质”“包装允许变化范围”等自定义字段。
每个字段保存稳定标识、显示名称、值、来源、对应消息/节点、确定程度与修订关系。程序生成可信标识;模型可以建议字段名称,但不能直接写执行权限、账务字段或服务端状态。
“模型建议”“用户确认”“用户授权 Agent 选择”分开保存。用户同意自主创意不代表授权虚构商品参数、随意增加费用或改变交付目标。
同义问题先关联已有字段,避免换一种表述重复问。确需修改已确认内容时,应说明冲突并记录新的用户依据。
## 7. 动态问答需求
### 7.1 何时提问
- 缺失信息会影响交付目标、事实准确性、关键创意选择或可执行性。
- 用户资料之间冲突,无法可靠判断。
- 现有能力不支持目标,需要选择替代方案。
- 当前结果引出了必须由用户决定的新问题。
不因场景模板存在字段就追问。可以合理建议的非关键偏好先给推荐,用户已授权自主选择时直接推进。
### 7.2 卡片交互
- 复用单选、多选、自由文本、素材选择和分页组件。
- 问题标题、说明、候选项由模型结合当前任务生成。
- 默认推荐每轮 1~3 题;允许确需一起决定时到 6 题。6 是防止长问卷的产品保护上限,不是要求凑满的题数。
- 选项可以随场景、画布素材和已知需求变化,提供自由补充。
- 支持“由你建议”;必需的真实商品事实、素材身份等不能以自主建议替代。
- 用户可以不点击卡片,直接在原输入框回答;Agent 解析后更新原问题状态并记录来源。
- 用户改变目标时旧问题标记为已替代,不自动删除历史答案。
- 有能力产生缩略图选择时可以复用真实素材预览;不能为展示选项偷偷生成收费图片。
### 7.3 验收重点
已知 10 秒、横屏时不再问时长与比例;指定完整剧本时先读剧本;商品图与价格已提供时直接规划;没有任何必要问题时不显示问答卡。
## 8. 动态计划与 Plan 式体验
界面只参考第一期原型的交互与布局。配色、字体、圆角、阴影和控件状态以系统当前主题及主题 token 为准,不复制原型主题,不单独建立 Agent 皮肤。
### 8.1 计划的用途
计划描述用户可以理解的工作目标,例如“分析商品”“确定视觉方向”“制作首张图”“完成系列图”,而不是暴露 API 调用列表。
只有多步骤任务才显示紧凑计划条,展开后查看当前、已完成、等待和跳过步骤。普通聊天、单次咨询不显示计划。无需新增独立模式页面;规划/执行状态在原会话中自然体现。
### 8.2 计划可调整
- Agent 可以新增、重排、拆分、跳过未执行步骤,并用一句话说明有业务意义的变化。
- 已执行记录不能通过重写计划抹掉;删除未开始步骤也保留修订记录。
- 步骤总数显示当前计划数量,变更后允许从 2/5 变成 2/6,附“新增结果检查”等说明。
- 步骤完成必须有依据:已展示方案、用户回答、成功工具结果或真实资源。
- 失败、部分成功、等待用户和等待后端任务分别展示,不能统称处理中。
### 8.3 推荐状态
计划步骤采用待开始、进行中、等待用户、等待任务、完成、失败、跳过。整体任务继续尽量复用现有 CreationStatus;计划步骤状态单独保存,避免将业务阶段膨胀成大量 API 状态。
计划允许动态调整,不代表审批状态可以由模型自行改变。
## 9. 创意方案与执行批次
### 9.1 创意方案
用户确认的内容包括:目标与交付物、核心创意、主要内容、沿用素材、关键规格、本阶段准备创建/修改的节点范围、明显能力缺口与自主选择范围。
方案格式按场景调整:导演可以给剧本与镜头设计;设计师可以给构图与视觉方向;电商美工可以给逐张用途与卖点。复用 Markdown 与方案卡片,不强制所有场景显示同样章节。
方案可分阶段批准,例如先批准剧本与角色方向,角色图产生后再讨论分镜。无需一次规划整部作品的全部技术细节。
### 9.2 执行批次
每批次保存目标步骤、方案版本、操作清单、受影响节点、素材依赖、解析后的生成规格、预期结果和执行状态。
创意方案无需包含每一个未来任务的完整供应商参数。但确认前应做能力可行性检查,不允许明知无法生成 60 秒视频却承诺直接交付。
批准后可以创建本阶段正式节点,并将未准备好的生成参数标记为待配置。只有参数完整、依赖就绪、报价批准的生成项才能执行。
模型选择所需能力及效果偏好;程序从当前目录解析真实候选。存在显著价格、质量或时长差异时交用户决定,不能悄悄替换已批准模型。
### 9.3 批准范围如何约束动态动作
当前后端批准绑定具体 ops,后续不能直接扩展成“这个 run 已批准,所以所有操作都允许”。建议将批准记录同时绑定:
- 方案版本和内容摘要。
- 允许创建的业务对象及数量、类型。
- 允许修改的已有节点 ID 与字段范围。
- 交付规格、关键素材与禁止修改项。
- 当前具体操作清单或由受限解析器生成的批次摘要。
后续批次必须由程序检查属于该范围;不能机械判断归属时重新展示批次确认,不让模型自行声称属于已批准范围。
首批实施优先采用可枚举的节点与字段清单。跨范围的大幅动态扩展需要新批准,避免过早建设复杂通用授权语言。
## 10. 确认与费用规则
| 操作 | 默认规则 |
| --- | --- |
| 读取当前画布、查看已授权素材、读取 Skill | 可自主进行 |
| 编写需求摘要、问题、方案、调整未执行计划 | 可进行文本推理,沿用当前文本计费方式 |
| 创建或修改方案中的正式节点 | 先批准方案或对应变更;批准范围内不逐节点重复问 |
| 用户明确要求移动/重命名指定节点 | 作为明确操作指令处理,遵循原工具确认偏好,不强制完整创意方案 |
| 改变数量、关键素材、时长、创意方向 | 对受影响部分重新确认 |
| 图片/视频及其他独立收费生成 | 真实报价批准后提交 |
| 删除或覆盖未在批准范围内的用户内容 | 展示目标与影响并确认 |
| 已消费生成的重做 | 新生成项与新报价,重新批准 |
“继续”“你决定”不构成费用批准。原确认开关可以控制普通工具操作体验,不能关闭创作方案与收费的硬门槛。
文本模型用于问答、方案编写和执行后判断,不等到最终确认才调用。方案确认与费用确认针对画布执行及具体生成消费;界面不能声称所有规划过程免费。
费用卡绑定真实模型、规格、数量、参考输入、请求摘要、报价版本与有效期。条件变化使相应批准失效,未受影响的已完成任务保留。
批量费用批准只覆盖卡片列出的有限请求。依赖前序产物、尚无法形成确定请求的后续生成,在依赖就绪后报价;本期不引入无限预算授权。
## 11. 统一画布执行入口
### 11.1 复用原画布能力
原读取、搜索、选区、节点、连线、布局与后置校验继续复用。模型通过已有适配器使用当前页面画布,本期无需改为服务端 MCP。
写操作执行前检查:当前画布匹配、真实资源归属、目标节点存在、批准范围、当前状态版本、输入合同和必要依赖。
执行后记录操作 ID、受影响节点、真实结果、变更摘要及错误。通过当前节点实际状态确认变更,不以模型回复作为完成依据。
### 11.2 防止不同工具绕过确认
原 `canvas_apply_ops` 中的生成操作、`canvas_run_generation`、生成工具的 `autoRun`、工作流自动运行与新创作控制器都必须进入同一执行检查。
普通画布写入与收费生成可以采用不同执行器,但批准范围和费用提交的验证不能只写在提示词或前端按钮里。后端对创作生成请求强制关联有效批准;具体路由合同在实施时覆盖所有现有入口。
### 11.3 手动编辑与并发
执行前重读受影响节点。用户修改过内容时保留用户版本,重新计算差异;改变已批准的生成输入时重新报价。
同一页面继续串行化 Agent 提交与原自动保存。执行租约防止同一任务被多个执行器同时推进,但不宣称解决整个画布跨设备协作。
## 12. 工具结果驱动的循环
决策循环为:读取任务上下文 → 模型决定下一步 → 程序校验 → 必要时等待用户 → 执行已授权动作 → 保存结果 → 回到模型。
到达问答、方案或费用等待点时暂停执行。同一批模型回复中出现等待交互后,不能继续执行夹带的写入或收费操作。
批次完成后,Agent 可以继续、补问、更新尚未执行步骤或建议重做。成功产物必须保留;后续推理失败不能把整个作品标记为不存在。
视觉检查仅在当前模型及输入协议真正支持读取产物时进行。视频审阅可能需要可用抽帧或视频理解工具,缺失时只报告状态、尺寸、时长等实际检查结果。
## 13. 失败与恢复
| 失败类型 | 处理 |
| --- | --- |
| 工具 JSON、引用或能力选择错误 | 将具体错误和失败参数作为工具结果返回,允许最多两次自动修正;不创建节点或生成媒体 |
| 信息缺失或目标冲突 | 改为提问,不反复生成无效方案 |
| 没有匹配模型或工具 | 展示能力差距,询问替代,禁止编造模型 ID |
| 报价过期或输入改变 | 作废相应未消费报价,重新报价并确认 |
| 网络中断且提交结果未知 | 查询原操作/提交 ID,不新建重复任务 |
| 媒体任务失败 | 保留成功项,说明失败项;新尝试按原账务事实判断并重新报价 |
| 用户暂停 | 停止后续编排,明确已提交任务可能仍在执行 |
| 页面关闭 | 保存等待位置;重新进入后核对后端任务,再由用户继续 |
自动纠正限于未执行参数,不自动重跑收费媒体。文本自动修正也消耗推理资源,达到限制后提供“修正方案”“修改要求”等操作,不无限循环。
## 14. 原会话与历史持久化
一个原聊天会话可以包含多个创作任务,一个任务可以跨多轮问答、多个方案版本和多个执行批次。不能继续把每条结构化消息都当成独立任务事实源。
原消息保存轻量内容快照与 task/run、interaction、proposal、step、batch 引用。服务端 run 保存当前执行事实;历史消息保留当时展示内容,不能全部渲染为最新方案。
需要持久化:
- 用户输入、结构化答案和自主选择授权。
- 需求摘要与来源、问题提交/替代状态。
- 计划版本、步骤修订与执行依据。
- 方案版本、用户批准范围、变更说明。
- 操作批次、报价与批准、任务与产物引用。
- 错误、自动纠正次数、暂停和恢复位置。
按用户 scope 隔离。UI 区分本地缓存与服务端保存;不将媒体大对象写进 localStorage,不在历史中保存密钥。
恢复先读服务端事实。已消费的批准不重放,旧方案确认按钮不可再次执行,失效报价不可继续扣费。旧单消息 run 历史只读展示;未完成旧任务按旧协议继续或明确结束后重新规划,不静默升级授权。
## 15. 概念数据合同
以下是职责草案,不代表立即增加同名数据库表:
| 对象 | 核心内容 | 权威来源 |
| --- | --- | --- |
| CreativeTask | 会话、画布、需求摘要、角色、当前计划和等待点 | 程序管理,绑定现有 run |
| Requirement | 字段、值、来源、确定程度、修订 | 模型提取,程序校验 |
| QuestionInteraction | 问题、候选项、关联字段、答案、版本 | 模型提问,程序分配标识并保存提交 |
| AgentPlan / PlanStep | 步骤、依赖、修订、状态、完成证据 | 模型建议结构,程序维护实际状态 |
| CreativeProposal | 内容、交付、规格、素材、拟操作范围 | 模型提案,用户批准 |
| ExecutionBatch | 具体操作、目标节点、前置条件、方案版本 | 程序校验和执行 |
| Approval | 批准类型、范围、版本、有效性 | 用户动作与服务端记录 |
| Submission / Task | 真实请求、报价、消费及结果 | 复用现有后端 |
标准字段和自定义需求分别存储;模型输出不直接合并进程序配置。权限与账务相关字段不接受模型赋值。
当前 CreationStatus 和 run.state 尽量复用。若批次幂等、批准范围需要索引与事务保证,则新增明确持久结构并同步数据库文档,不能只依靠无约束 JSON 提供安全保证。
## 16. 代码调整建议
| 模块 | 调整职责 |
| --- | --- |
| 原 `CanvasAssistantPanel` | 保留会话 UI,将新增决策循环协调逐步提取,避免继续堆进页面组件 |
| 场景配置 | 改为专业指导与推荐 Skill;常见字段作为建议 |
| 问答合同与卡片 | 动态字段、自然语言回答关联、可变题数、历史替代状态 |
| 计划合同与组件 | 持久化模型计划及修订,实际执行状态驱动展示 |
| 方案校验 | 分开创意可行性与执行参数完整性检查 |
| 创作控制器 | 聚焦租约、批准、报价、提交、幂等与恢复,工具结果交回原 Agent |
| 新增有限执行适配层 | 聚合现有工具的读写/费用分类、批准检查与执行回执;不另建模型 SDK |
| 后端 creation service | 批次、范围、版本与收费校验,复用现有 handler/service/repository 分层 |
可以将模型交互区分为提问、更新计划、提出方案、提出执行批次四种明确动作;是否拆成四个工具在实施时结合供应商 schema 支持决定。不能再要求所有动作同时填写一份完整工作流。
## 17. 分期交付与人工验收
### A:动态规划与方案体验
交付角色指导、动态需求字段、动态计划、方案分层、原历史关联。未完成执行门槛改造前,不开放新的自主写入路径;不完整执行参数只用于规划展示。
验收:
- 完整需求直接出方案,不补无关问题。
- 用户通过普通输入框回答卡片,已回答字段不会再次提问。
- 一个商品广告需求可同时采用电商与导演方法。
- 问答数量和计划数量根据任务变化。
- 缺少后续媒体参数不阻塞创意方案阅读,无法完成的目标有明确说明。
### B:受控的动态画布执行闭环
交付任务级 run、执行批次、统一写入与费用门槛、结果回到模型、历史恢复。A+B 构成本次推荐最小完整交付。
验收:
- 方案确认前零新增节点、零媒体生成。
- 批准范围内分批创建节点,不每个节点重复确认。
- 从原工具、新创作入口及 autoRun 均不能绕过费用确认。
- 用户改动相关节点后,旧请求不能覆盖新内容或继续消费旧报价。
- 刷新、重复点击确认、网络超时均不重复创建或扣费。
- 成功与失败混合批次保留成功项,只处理失败项。
### C:产物驱动与专业能力增强
按实际可用视觉/视频能力接入检查,扩展更多已存在的节点工具与 Skills。缺失工具单独立项,不纳入基础闭环的隐含承诺。
验收:
- 根据真实产物提出下一步,而非固定跑完全部节点。
- 只修改指定海报或镜头,其他产物保留。
- 新增交付或重做产生新范围/费用确认。
- 无视觉读取能力时不输出虚构的画面检查结论。
本轮仅设计,不运行自动测试。实施后按用户要求进行实际使用调试;上线前涉及审批、账务、幂等和资源归属的验证需单独记录,不以浏览器截图代替这些行为的验证。
## 18. 三类端到端审阅示例
### 短剧
用户给出剧本和角色图。Agent 先读现有素材,提出镜头与节奏方案,不重复问题材和角色。用户批准本阶段后创建剧本文本与必要镜头节点,引用已有角色图。实际视频时长超出模型上限时先解释并询问拆段或缩短,不私自改变。生成费用确认后制作,产物完成后再决定是否需要下一镜头或局部重做。
### 电商
用户提供商品图并要求两张促销海报。Agent 确认真实促销信息与不可改包装范围,提出两种构图。用户批准后创建对应节点及素材引用,按真实模型与数量报价。第一张完成后,可在可用视觉能力下检查商品保留情况,继续第二张或提出调整;调整已批准请求时重新报价。
### 营销与普通设计
用户只要活动文案,Agent 直接给文字,不附带收费图片。用户随后要求海报时在同一任务或明确关联的新任务中扩展方案。用户仅要求将已有标题节点左移并改名时,走具体操作确认规则,不重新询问受众、风格、尺寸。
## 19. 建议审阅的产品决策
以下给出明确推荐默认值,审阅时可直接修改:
| 决策 | 推荐默认值 |
| --- | --- |
| 是否新增模式切换 | 不强制新增;原会话显示规划/执行状态 |
| 角色是否需要用户手动选 | 不需要,支持用户指定和模型按需组合 |
| 每轮问题数量 | 默认 1~3,确有需要最多 6,绝不凑题 |
| 参数错误自动修正 | 最多两次未执行参数纠正,不自动重跑媒体 |
| 画布节点何时创建 | 批准本阶段方案后创建正式节点 |
| 执行中是否可以改计划 | 可以;范围与费用变化单独重新确认 |
| 是否保留逐次媒体费用确认 | 保留,可批准明确列出的一个批次 |
| 本次最小交付 | A+B,C 按真实工具能力逐步加入 |
| 首页与跨画布 | 本次不扩大,当前画布内闭环优先 |
## 20. 交付判断
本次完成的判断不是“可以生成更多问题”,而是同一个 Agent 能结合真实画布与用户反馈,选择不同创作路径、维护动态计划、在批准范围内执行,并在结果、错误或需求变化后合理调整;同时,确认、费用、历史和恢复仍然可追溯。

View File

@@ -0,0 +1,49 @@
---
title: 创作 Agent 第一期版本基线
description: 已实现范围、用户体验反馈、代码核对与下一阶段边界
---
# 创作 Agent 第一期版本基线
用户已反馈基本流程验证通过。本基线保存当前可用实现,作为后续工作台扩展和问题定位的对照;这不代表全部模型协议、费用异常路径及部署环境均已通过专项验收。
## 本期实际范围
- 原画布在线助手支持动态问答、正式方案、按需执行计划、节点操作、分批报价、生成与结果接续,复用原会话、输入框、模型和技能。
- 普通画布工具按当前确认开关执行;模型承接当前任务授权,减少重复提问。结构化方案及费用仍以程序批准为准。
- 历史问答、方案和报价折叠;引用显示类型与标题;首页/创作页和画布在线、本机助手均采用回车发送、Shift+回车换行。
- 前端传输单份标准消息,后端路由后适配供应商;看图按需读取预览、原图或局部,派生图上传为资源,任务正文只保存资源引用。
- 上下文按预算保留当前要求和执行事实,旧会话按完整组省略。运行事实保存在创作 run/submission 中,历史消息保留展示快照。
## 收尾代码核对
| 边界 | 当前依据与结论 |
| --- | --- |
| 历史恢复 | `CanvasCreativeInteraction` 从原消息读取快照和 runId,controller 加载服务端 run;历史只读,恢复连接不自动批准 |
| 生成批准 | `ExecuteCreationSubmission` 在事务内检查批准、过期、撤销、方案范围及价格签名,再创建任务和额度预留;已有 TaskID 返回原任务 |
| 修改 | `proposal-invalidate` 撤销未提交生成的执行项;已提交任务不会由文字修改自动取消 |
| 资源 | 看图消息通过 `referenceImages` 提供资源清单;后端按用户读取 ready 资源,执行时还原;创作 run/submission 纳入资源删除引用检查 |
| 撤销 | 原画布撤销最多保留十批;用户后续修改导致快照不匹配时清空撤销链,避免覆盖手工编辑;不取消已提交生成或退费 |
| 隔离 | 浏览器缓存使用用户 scope,controller 检查账号与生命周期;服务端 run、submission、画布和资源按当前用户查询 |
以上为静态代码核对,不记为自动测试或浏览器全流程验证。本轮按用户要求未运行测试、类型检查、构建或收费模型请求。此前后端已通过开发启动编译,健康接口可用;它们仅证明启动入口可用。
## 保留的专项验收
- 四种模型协议的工具调用/结果及资源引用,尤其 Gemini 思考签名与协议插件。
- 刷新、切换会话、断线、暂停后恢复,以及修改前后旧报价不能再次消费。
- 原图复用、裁剪上传幂等、资源被引用时禁止删除、账号切换隔离。
- 明暗主题、窄屏、中文输入法、引用/技能候选与发送状态。
- PostgreSQL 迁移和部署验证。
## 版本与恢复边界
本次 Git 提交记录代码、设计和验收说明,不包含本地账号数据库、上传媒体、密钥、日志或构建产物。以该提交作为后续改动的对照点,必要时在独立检出中恢复代码,不覆盖正在使用的工作区。
本期数据库由 v8 增加到 v9。Git 回退不能恢复数据库、资源或账务;部署前应单独备份数据库和资源。数据库恢复或迁移回退需要另行核对影响,不随代码回退自动执行。
## 下一阶段
先设计首页进入画布时的同任务上下文接续、页面工具范围及角色/技能加载,再接通一个电商业务流程。当前仅首页发送快捷键统一,不宣称已实现首页到画布的同任务迁移。
本期不引入 Harness、完整服务端画布 MCP、动态插件节点注册、多人协作画布或完整长视频合成。这些能力继续作为独立阶段设计。

View File

@@ -0,0 +1,235 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>影策 · 第一期创作 Agent 交互设计</title>
<style>
:root { color-scheme:light; --yc-bg:#f1f1f1; --yc-surface:#fff; --yc-panel:#f7f7f7; --yc-hover:#efefef; --yc-text:#202020; --yc-muted:#727272; --yc-border:#e5e5e5; --yc-strong:#181818; --yc-on-strong:#fff; --yc-success:#26754a; --yc-success-bg:#eef8f1; --yc-warn:#976017; --yc-warn-bg:#fff7e9; --yc-shadow:0 16px 48px #0000000a; --yc-node-shadow:0 5px 18px #00000009; --yc-line:#b6b6b6; --yc-radius:14px; }
[data-theme="dark"] { color-scheme:dark; --yc-bg:#111112; --yc-surface:#1c1c1e; --yc-panel:#242426; --yc-hover:#303032; --yc-text:#eeeeef; --yc-muted:#a0a0a4; --yc-border:#363639; --yc-strong:#f5f5f5; --yc-on-strong:#191919; --yc-success:#82c9a0; --yc-success-bg:#203129; --yc-warn:#e1b974; --yc-warn-bg:#362e20; --yc-shadow:0 16px 48px #0004; --yc-node-shadow:0 5px 18px #0003; --yc-line:#626268; }
* { box-sizing:border-box; } body { margin:0; background:var(--yc-bg); color:var(--yc-text); font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei",sans-serif; }
button,input,textarea,select { font:inherit; } button { cursor:pointer; } button,input,textarea,select { color:inherit; } button:disabled { opacity:.38; cursor:not-allowed; } button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible { outline:2px solid var(--yc-strong); outline-offset:3px; } button { border:0; background:transparent; }
.review-bar { min-height:54px; padding:9px 22px; display:flex; align-items:center; justify-content:space-between; gap:15px; background:var(--yc-strong); color:var(--yc-on-strong); font-size:12px; }
.review-label { display:flex; align-items:center; gap:10px; flex-wrap:wrap; } .review-label b { font-size:12px; letter-spacing:.05em; } .review-label span { opacity:.76; }
.review-nav { display:flex; align-items:center; gap:5px; flex-wrap:wrap; } .review-nav select { max-width:155px; border:1px solid #8887; border-radius:7px; padding:4px 8px; background:transparent; color:inherit; } .review-nav option { color:var(--yc-text); background:var(--yc-surface); } .review-nav button { color:inherit; padding:4px 8px; border-radius:6px; }
.review-nav button:hover { background:#8884; } .app-shell { display:flex; padding:7px; gap:7px; height:calc(100dvh - 54px); min-height:640px; }
.app-sidebar { width:210px; flex-shrink:0; background:var(--yc-panel); border-radius:var(--yc-radius); padding:21px 13px 14px; display:flex; flex-direction:column; } .brand { display:flex; align-items:center; gap:10px; padding:0 10px 26px; font-size:19px; font-weight:650; letter-spacing:1px; } .brand-mark { font-size:33px; line-height:30px; letter-spacing:-3px; font-weight:500; } .brand small { font-size:10px; display:block; font-weight:400; color:var(--yc-muted); letter-spacing:.06em; margin-top:-3px; }
.side-action { width:100%; display:flex; align-items:center; gap:10px; padding:10px 12px; text-align:left; border-radius:9px; color:var(--yc-muted); margin-bottom:4px; } .side-action.active { color:var(--yc-text); background:var(--yc-surface); box-shadow:0 2px 6px #00000004; } .side-action:hover { background:var(--yc-hover); color:var(--yc-text); } .side-glyph { width:18px; text-align:center; font-size:17px; } .side-group { font-size:11px; color:var(--yc-muted); padding:25px 12px 8px; } .side-foot { margin-top:auto; padding:15px 10px 0; display:flex; align-items:center; gap:9px; } .avatar { display:grid; place-items:center; width:29px; height:29px; border-radius:50%; background:var(--yc-hover); font-size:11px; font-weight:600; } .side-foot small { display:block; color:var(--yc-muted); font-size:10px; }
.app-stage { position:relative; flex:1; min-width:0; display:flex; flex-direction:column; background:var(--yc-surface); border-radius:var(--yc-radius); overflow:hidden; box-shadow:var(--yc-shadow); } .app-topbar { height:57px; flex-shrink:0; display:flex; justify-content:space-between; align-items:center; padding:0 25px; gap:10px; } .breadcrumb { display:flex; align-items:center; gap:13px; font-size:12px; color:var(--yc-muted); } .breadcrumb strong { color:var(--yc-text); font-weight:500; } .top-actions { display:flex; align-items:center; gap:8px; } .mini-btn { display:inline-flex; justify-content:center; align-items:center; gap:6px; padding:6px 10px; border-radius:7px; font-size:12px; } .mini-btn:hover { background:var(--yc-hover); } .top-status { font-size:11px; color:var(--yc-muted); }
.content { flex:1; min-height:0; overflow:hidden; position:relative; } .home-page { overflow:auto; height:100%; padding:58px 30px 30px; } .home-inner { width:min(780px,100%); margin:0 auto; } .eyebrow { color:var(--yc-muted); font-size:11px; letter-spacing:.14em; } h1 { font-size:32px; font-weight:550; letter-spacing:-1px; margin:12px 0 8px; } .home-subtitle { font-size:14px; color:var(--yc-muted); margin:0 0 27px; } .scene-switch { display:flex; gap:7px; margin:26px 0 18px; flex-wrap:wrap; } .scene-switch button { padding:7px 16px; border-radius:22px; font-size:13px; color:var(--yc-muted); } .scene-switch button[aria-pressed="true"] { background:var(--yc-hover); color:var(--yc-text); font-weight:550; } .scene-switch button:hover { background:var(--yc-panel); }
.home-composer { border:1px solid var(--yc-border); border-radius:19px; background:var(--yc-surface); padding:18px; box-shadow:0 6px 24px #00000006; } .home-composer textarea { width:100%; height:86px; resize:none; border:0; outline:none; background:transparent; font-size:15px; line-height:1.8; } .composer-tools { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-top:9px; } .composer-tool-left { display:flex; align-items:center; gap:6px; flex-wrap:wrap; } .soft-pill { padding:4px 9px; border-radius:6px; background:var(--yc-panel); color:var(--yc-muted); font-size:11px; } .primary { border-radius:9px; background:var(--yc-strong); color:var(--yc-on-strong); padding:9px 15px; font-size:13px; font-weight:500; } .primary:hover { opacity:.88; } .secondary { padding:8px 13px; border:1px solid var(--yc-border); background:var(--yc-surface); border-radius:8px; font-size:12px; } .secondary:hover { background:var(--yc-hover); } .text-button { padding:4px 2px; font-size:12px; color:var(--yc-muted); text-decoration:underline; text-underline-offset:4px; }
.home-examples { display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin:36px 0; } .example { text-align:left; padding:15px 17px; min-height:118px; border:1px solid var(--yc-border); border-radius:12px; background:var(--yc-panel); } .example:hover { border-color:var(--yc-muted); } .example span { font-size:11px; color:var(--yc-muted); } .example strong { display:block; font-size:13px; font-weight:500; margin:9px 0 5px; } .example p { margin:0; color:var(--yc-muted); font-size:11px; } .home-note { color:var(--yc-muted); font-size:11px; text-align:center; }
.conversation-only { height:100%; display:flex; flex-direction:column; max-width:780px; margin:0 auto; padding:0 20px; } .chat-area { display:flex; flex-direction:column; flex:1; min-height:0; position:relative; } .chat-scroll { overflow-y:auto; flex:1; min-height:0; padding:25px 3px 18px; scrollbar-width:thin; } .message { margin-bottom:24px; } .message.user { display:flex; justify-content:flex-end; } .user-bubble { max-width:87%; background:var(--yc-panel); padding:12px 16px; border-radius:13px 13px 3px 13px; font-size:13px; white-space:pre-wrap; } .assistant-label { font-weight:550; font-size:12px; display:flex; gap:7px; align-items:center; margin-bottom:9px; } .assistant-dot { width:20px; height:20px; display:grid; place-items:center; border-radius:6px; background:var(--yc-strong); color:var(--yc-on-strong); font-size:15px; line-height:1; } .assistant-text { font-size:13px; margin:0 0 13px; line-height:1.85; } .facts { display:flex; gap:6px; flex-wrap:wrap; margin:12px 0 16px; } .fact { font-size:10px; border-radius:5px; padding:3px 7px; background:var(--yc-panel); color:var(--yc-muted); } .fact::before { content:"✓ "; color:var(--yc-success); }
.question-card,.plan-card,.quote-card,.receipt-card { border:1px solid var(--yc-border); border-radius:12px; padding:18px; margin-top:11px; background:var(--yc-surface); } .card-overline { display:flex; align-items:center; justify-content:space-between; font-size:10px; color:var(--yc-muted); margin-bottom:12px; gap:10px; } .step-dots { display:flex; gap:4px; } .step-dots i { width:17px; height:3px; background:var(--yc-border); border-radius:2px; } .step-dots i.current { background:var(--yc-strong); } .question-title { font-size:16px; line-height:1.5; margin:5px 0 5px; font-weight:550; } .question-help { color:var(--yc-muted); font-size:11px; margin:0 0 14px; } .options { display:grid; gap:7px; } .option { display:flex; text-align:left; gap:10px; align-items:flex-start; width:100%; border:1px solid var(--yc-border); border-radius:8px; padding:10px 12px; } .option[aria-pressed="true"] { background:var(--yc-panel); border-color:var(--yc-strong); } .option-radio { flex-shrink:0; margin-top:4px; height:12px; width:12px; border-radius:50%; border:1px solid var(--yc-muted); } .option[aria-pressed="true"] .option-radio { border:4px solid var(--yc-strong); } .option strong { display:block; font-size:12px; font-weight:500; } .option small { display:block; color:var(--yc-muted); font-size:10px; margin-top:2px; } .free-label { display:block; font-size:11px; color:var(--yc-muted); margin:14px 0 6px; } .field { width:100%; border:1px solid var(--yc-border); border-radius:8px; padding:9px 11px; background:var(--yc-surface); font-size:12px; resize:vertical; min-height:40px; } .card-footer { display:flex; align-items:center; justify-content:space-between; gap:8px; margin-top:15px; } .card-footer .left,.card-footer .right { display:flex; gap:7px; align-items:center; } .quiet { color:var(--yc-muted); font-size:10px; } .submitted { display:flex; justify-content:space-between; align-items:center; gap:8px; background:var(--yc-panel); border-radius:8px; padding:11px 13px; font-size:12px; } .submitted span { color:var(--yc-success); } .plan-card h3 { font-size:20px; font-weight:550; margin:5px 0 10px; } .plan-card .logline { font-size:13px; line-height:1.85; margin:0 0 15px; } .plan-summary { display:grid; grid-template-columns:1fr 1fr; gap:10px 18px; margin:14px 0 17px; font-size:11px; } .plan-summary strong { display:block; font-weight:500; color:var(--yc-text); font-size:12px; } .plan-summary span { color:var(--yc-muted); } .scope-note { padding:10px 12px; background:var(--yc-panel); border-radius:7px; font-size:11px; line-height:1.75; }
.composer-wrap { position:relative; flex-shrink:0; padding:6px 0 15px; background:var(--yc-surface); } .chat-composer { border:1px solid var(--yc-border); border-radius:12px; padding:10px 12px; background:var(--yc-surface); } .chat-composer textarea { width:100%; border:0; outline:none; background:transparent; height:47px; resize:none; font-size:12px; padding:0; } .chat-composer .composer-tools { margin:1px 0 0; } .chat-composer .primary { padding:5px 11px; font-size:12px; } .progress-trigger { width:100%; display:flex; justify-content:space-between; align-items:center; gap:10px; padding:11px 4px; text-align:left; font-size:11px; } .progress-trigger .progress-info { display:flex; align-items:center; gap:7px; } .progress-track { height:3px; width:64px; background:var(--yc-border); border-radius:4px; overflow:hidden; } .progress-track span { display:block; height:100%; background:var(--yc-strong); border-radius:4px; transition:width .25s; } .progress-count { font-variant-numeric:tabular-nums; color:var(--yc-muted); font-size:10px; } .plan-popover { position:absolute; z-index:20; left:0; right:0; bottom:calc(100% - 4px); border:1px solid var(--yc-border); background:var(--yc-surface); border-radius:12px; padding:17px; box-shadow:var(--yc-shadow); } .plan-popover h3 { font-size:13px; margin:0 0 14px; display:flex; justify-content:space-between; align-items:center; } .plan-popover ol { padding:0; margin:0; list-style:none; } .plan-popover li { display:flex; gap:10px; align-items:flex-start; padding:8px 0; font-size:11px; } .plan-popover li strong { display:block; font-size:12px; font-weight:500; } .plan-popover li small { color:var(--yc-muted); font-size:10px; } .plan-indicator { width:20px; height:20px; border-radius:50%; background:var(--yc-panel); color:var(--yc-muted); display:grid; place-items:center; font-size:10px; margin-top:1px; } .plan-indicator.done { color:var(--yc-success); background:var(--yc-success-bg); } .plan-indicator.active { background:var(--yc-strong); color:var(--yc-on-strong); }
.canvas-layout { display:flex; height:100%; } .canvas-pane { flex:1; min-width:0; position:relative; overflow:hidden; background-color:var(--yc-panel); background-image:radial-gradient(var(--yc-line) .65px,transparent .65px); background-size:19px 19px; } .canvas-heading { position:absolute; left:22px; top:17px; z-index:3; max-width:calc(100% - 44px); } .canvas-heading strong { font-size:13px; font-weight:550; } .canvas-heading span { display:block; color:var(--yc-muted); font-size:10px; margin-top:2px; } .canvas-scroll { overflow:auto; height:100%; padding-top:65px; padding-bottom:70px; scrollbar-width:thin; } .canvas-scaled { width:840px; height:675px; position:relative; transform-origin:top left; } .canvas-size { position:relative; margin:0 22px; } .edges { position:absolute; inset:0; width:840px; height:675px; pointer-events:none; overflow:visible; } .edges path { stroke:var(--yc-line); stroke-width:1.4; fill:none; } .canvas-node { position:absolute; width:210px; background:var(--yc-surface); border:1px solid var(--yc-border); border-radius:10px; box-shadow:var(--yc-node-shadow); } .canvas-node.selected { outline:1.5px solid var(--yc-strong); outline-offset:3px; } .node-header { display:flex; align-items:center; gap:6px; padding:9px 10px; font-size:10px; min-height:35px; } .node-type { color:var(--yc-muted); font-size:11px; } .node-title { flex:1; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .node-edit { padding:0 2px; font-size:12px; color:var(--yc-muted); } .node-edit:hover { color:var(--yc-text); } .node-port { position:absolute; border:2px solid var(--yc-surface); background:var(--yc-line); width:8px; height:8px; border-radius:50%; top:50%; left:-5px; } .node-port.out { right:-5px; left:auto; } .node-text { padding:0 12px 13px; font-size:11px; line-height:1.85; } .node-text h4 { font-size:14px; margin:1px 0 9px; font-weight:550; } .node-foot { padding:7px 10px; display:flex; justify-content:space-between; align-items:center; font-size:9px; color:var(--yc-muted); } .node-foot button { font-size:9px; color:var(--yc-text); } .node-empty { height:127px; margin:0 8px; border-radius:5px; background:var(--yc-panel); color:var(--yc-muted); display:flex; flex-direction:column; align-items:center; justify-content:center; font-size:10px; gap:5px; } .node-empty strong { font-size:17px; font-weight:400; color:var(--yc-line); } .node-empty.running { background:linear-gradient(110deg,var(--yc-panel),var(--yc-hover)); } .chat-pane { width:440px; flex-shrink:0; background:var(--yc-surface); padding:0 19px; border-left:1px solid var(--yc-border); display:flex; flex-direction:column; } .chat-pane-header { display:flex; justify-content:space-between; align-items:center; padding:16px 0 8px; font-size:12px; flex-shrink:0; } .chat-pane-header strong { font-weight:550; } .chat-pane .chat-scroll { padding-top:15px; } .chat-pane .question-card,.chat-pane .plan-card,.chat-pane .quote-card { padding:14px; } .chat-pane .message { margin-bottom:21px; } .canvas-tools { position:absolute; bottom:19px; left:20px; display:flex; gap:4px; padding:5px; border:1px solid var(--yc-border); border-radius:9px; background:var(--yc-surface); z-index:3; box-shadow:var(--yc-node-shadow); } .canvas-tools button { padding:5px 8px; font-size:11px; border-radius:5px; } .canvas-tools button:hover { background:var(--yc-hover); } .canvas-tools .divider { background:var(--yc-border); width:1px; margin:3px 5px; } .canvas-legend { position:absolute; bottom:21px; right:19px; font-size:10px; color:var(--yc-muted); padding:7px 9px; background:var(--yc-surface); border-radius:6px; } .mobile-toggle { display:none; }
.scene-art { height:127px; margin:0 8px; border-radius:5px; overflow:hidden; background:#24292b; position:relative; color:#d8dbd4; } .scene-art::before { content:""; position:absolute; left:58%; top:8%; width:26%; height:85%; background:linear-gradient(100deg,#526364,#b7bc9d); transform:skewY(-6deg); box-shadow:-4px 0 20px #a4baa32b; } .scene-art::after { content:""; position:absolute; width:43px; height:67px; background:#141b1f; left:35%; bottom:-5px; border-radius:42% 42% 0 0; box-shadow:2px 0 12px #0b1418; } .scene-art .head { position:absolute; z-index:1; height:20px; width:18px; border-radius:50%; left:calc(35% + 12px); bottom:57px; background:#131a1d; } .scene-art .clock-face { position:absolute; top:18%; left:14%; width:21px; height:21px; border:1px solid #a7b1a5; border-radius:50%; opacity:.7; } .clock-face::before { content:""; position:absolute; width:1px; height:7px; background:#a7b1a5; left:9px; top:3px; transform:rotate(35deg); transform-origin:bottom; } .scene-art .art-grain { position:absolute; inset:0; background:repeating-linear-gradient(0deg,#ffffff04 0,#ffffff04 1px,transparent 1px,transparent 3px); z-index:2; pointer-events:none; } .scene-art .art-label { position:absolute; z-index:3; left:7px; bottom:5px; font-size:8px; color:#e6e6df; background:#1118; padding:1px 5px; border-radius:3px; } .scene-art .play { position:absolute; z-index:4; left:calc(50% - 14px); top:calc(50% - 14px); width:28px; height:28px; border-radius:50%; background:#0007; color:#fff; display:grid; place-items:center; font-size:10px; } .scene-art.scene-2 { background:#353029; } .scene-art.scene-2::before { left:19%; top:48%; height:25%; width:60%; background:linear-gradient(10deg,#5e5943,#b5ae8d); transform:rotate(-8deg); } .scene-art.scene-2::after { left:59%; width:55px; height:85px; bottom:-10px; transform:rotate(-25deg); } .scene-art.scene-2 .head { display:none; } .scene-art.scene-2 .clock-face { width:32px; height:22px; left:38%; top:35%; border-radius:1px; transform:rotate(-9deg); border-color:#ddd7b8; } .scene-art.scene-3 { background:#232b2d; } .scene-art.scene-3::before { left:37%; width:31%; background:#b4bbb0; } .scene-art.scene-3::after { left:40%; bottom:-3px; height:68px; } .scene-art.scene-3 .head { left:calc(40% + 12px); bottom:61px; } .scene-art.scene-3 .clock-face { display:none; }
.quote-title { display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:14px; gap:10px; } .quote-title strong { font-size:15px; font-weight:550; } .quote-title b { font-size:20px; letter-spacing:-.5px; } .quote-title small { font-size:10px; font-weight:400; color:var(--yc-muted); } .quote-row { display:flex; justify-content:space-between; gap:10px; font-size:11px; margin:8px 0; } .quote-row span { color:var(--yc-muted); } .quote-total { padding-top:12px; margin-top:12px; border-top:1px solid var(--yc-border); } .quote-note { font-size:10px; color:var(--yc-muted); margin:11px 0; line-height:1.8; } .quote-check { display:flex; align-items:flex-start; gap:7px; font-size:11px; margin:14px 0; } .quote-check input { accent-color:var(--yc-strong); margin-top:4px; } .success-note { color:var(--yc-success); font-size:11px; } .warning { color:var(--yc-warn); background:var(--yc-warn-bg); padding:11px 12px; border-radius:8px; font-size:11px; margin:10px 0; } .result-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:7px; margin:12px 0; } .result-grid button { min-width:0; padding:0; text-align:left; } .result-grid .scene-art { margin:0; height:114px; } .result-grid small { display:block; font-size:9px; margin-top:5px; color:var(--yc-muted); } .receipt-card { font-size:11px; } .receipt-line { display:flex; justify-content:space-between; align-items:center; gap:10px; margin:8px 0; } .receipt-line span { color:var(--yc-muted); } .receipt-card .primary { font-size:11px; } .chat-more { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
.dialog { width:min(820px,calc(100% - 32px)); max-height:calc(100dvh - 44px); border:1px solid var(--yc-border); border-radius:16px; padding:0; background:var(--yc-surface); color:var(--yc-text); box-shadow:0 30px 120px #0003; } .dialog::backdrop { background:#1117; backdrop-filter:blur(3px); } .dialog-head { padding:19px 24px; display:flex; align-items:center; justify-content:space-between; gap:10px; border-bottom:1px solid var(--yc-border); } .dialog-head h2 { margin:0; font-size:15px; font-weight:550; } .dialog-body { overflow:auto; padding:23px 28px; max-height:calc(100dvh - 190px); } .dialog-body h3 { font-size:14px; font-weight:550; margin:23px 0 9px; } .dialog-body h3:first-child { margin-top:0; } .dialog-body p { font-size:12px; line-height:1.9; color:var(--yc-muted); } .dialog-foot { padding:16px 24px; display:flex; justify-content:space-between; gap:12px; align-items:center; border-top:1px solid var(--yc-border); } .dialog-foot div { display:flex; gap:8px; } .detail-meta { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px; } .detail-meta span { font-size:10px; background:var(--yc-panel); padding:4px 8px; border-radius:5px; } .detail-assets { display:grid; grid-template-columns:repeat(3,1fr); gap:9px; } .detail-asset { background:var(--yc-panel); border-radius:8px; padding:12px; font-size:11px; } .detail-asset strong { display:block; font-size:12px; font-weight:500; margin-bottom:5px; } .detail-asset span { color:var(--yc-muted); } .shot-table { width:100%; border-collapse:collapse; font-size:11px; line-height:1.8; } .shot-table th { text-align:left; font-weight:500; color:var(--yc-muted); background:var(--yc-panel); padding:8px; } .shot-table td { padding:10px 8px; border-bottom:1px solid var(--yc-border); vertical-align:top; } .shot-table th:first-child { width:65px; } .shot-table td b { display:block; font-weight:500; } .error-text { color:var(--yc-warn); font-size:11px; min-height:17px; } .large-art { max-width:270px; margin:auto; } .large-art .scene-art { height:420px; margin:0; } .large-art .scene-art::after { width:85px; height:170px; } .large-art .scene-art .head { width:36px; height:44px; bottom:160px; left:calc(35% + 24px); } .large-art .scene-art .art-label { font-size:11px; padding:4px 8px; } .toast { position:fixed; z-index:100; bottom:25px; left:50%; transform:translateX(-50%); background:var(--yc-strong); color:var(--yc-on-strong); padding:11px 17px; border-radius:9px; font-size:12px; max-width:90%; box-shadow:var(--yc-shadow); }
[hidden] { display:none!important; } .sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
@media(min-width:1700px) { .chat-pane { width:470px; padding:0 25px; } .home-page { padding-top:85px; } }
@media(max-width:1250px) { .app-sidebar { width:172px; } .chat-pane { width:420px; padding:0 16px; } .canvas-legend { display:none; } .home-page { padding-top:37px; } }
@media(max-width:1040px) { .app-sidebar { width:66px; padding:19px 7px 12px; } .brand { padding:0 7px 24px; } .brand-word,.side-action .nav-text,.side-group,.side-foot .account-text { display:none; } .side-action { justify-content:center; padding:10px; } .side-foot { padding-left:10px; } .chat-pane { width:405px; } .app-topbar { padding:0 18px; } }
@media(max-width:820px) { .review-bar { align-items:flex-start; padding:9px 13px; gap:7px; } .review-label { gap:4px; flex-direction:column; align-items:flex-start; } .review-label span { font-size:10px; } .review-nav { justify-content:flex-end; } .review-nav label { display:none; } .review-nav select { width:123px; font-size:11px; } .app-shell { height:calc(100dvh - 62px); min-height:590px; } .app-sidebar { display:none; } .app-topbar { height:50px; padding:0 13px; } .top-status { display:none; } .home-page { padding:35px 19px 24px; } h1 { font-size:27px; } .home-examples { gap:8px; } .example { padding:12px; min-height:133px; } .canvas-layout .chat-pane { width:100%; border:0; padding:0 20px; } .canvas-layout .canvas-pane { display:none; } .canvas-layout.show-canvas .canvas-pane { display:block; } .canvas-layout.show-canvas .chat-pane { display:none; } .mobile-toggle { display:inline-flex; } .conversation-only { padding:0 18px; } .canvas-heading { top:15px; } .dialog-body { padding:19px; } .detail-assets { grid-template-columns:1fr; } }
@media(max-width:480px) { .review-nav button { padding:4px 5px; font-size:11px; } .review-label b { font-size:10px; } .review-nav { gap:1px; } .home-page { padding-top:28px; } .home-examples { grid-template-columns:1fr; margin-top:22px; } .example { min-height:auto; padding:12px 14px; } .example strong { margin:3px 0; } .home-subtitle { font-size:12px; } .scene-switch { gap:3px; margin-top:19px; } .scene-switch button { padding:6px 12px; font-size:12px; } .composer-tool-left { gap:3px; } .home-composer { padding:14px; } .home-composer textarea { height:100px; font-size:13px; } .home-note { display:none; } .question-card,.plan-card { padding:14px; } .plan-summary { gap:8px; } .breadcrumb { gap:8px; } .breadcrumb .optional { display:none; } .card-footer { align-items:flex-start; } .dialog-foot { align-items:flex-start; padding:14px 18px; } .dialog-foot .quiet { max-width:42%; } .dialog-foot div { flex-wrap:wrap; justify-content:flex-end; } .shot-table { font-size:10px; } }
@media(prefers-reduced-motion:reduce) { *,*::before,*::after { transition:none!important; scroll-behavior:auto!important; } }
</style>
</head>
<body>
<div class="review-bar" aria-label="设计评审控制,与产品界面分开">
<div class="review-label"><b>交互原型 · 示例数据</b><span>不会调用模型或扣费</span></div>
<div class="review-nav"><label for="review-state">评审状态</label><select id="review-state" aria-label="跳转评审状态"><option value="home">从首页体验</option><option value="questions">需求问答</option><option value="proposal">方案待确认</option><option value="quote">画布与费用确认</option><option value="running">生成中</option><option value="complete">结果与局部调整</option><option value="failure">结果回写失败</option><option value="casual">普通聊天</option></select><button data-action="theme" aria-label="切换明暗主题">浅 / 深</button><button data-action="reset">重置</button></div>
</div>
<div id="app" class="app-shell"></div>
<dialog id="detail-dialog" class="dialog" aria-labelledby="dialog-title"></dialog>
<div id="toast" class="toast" role="status" hidden></div>
<script>
(() => {
'use strict';
const root = document.getElementById('app');
const dialog = document.getElementById('detail-dialog');
const defaultPrompt = '帮我做一集 15 秒、9:16 的悬疑推理短片。雨夜,一位年轻侦探发现一封写着自己名字的信。';
const questions = [
{id:'hook',title:'故事的悬念,你更喜欢哪种方向?',hint:'已知是悬疑推理短片,只补充影响故事走向的选择。',options:[['时间循环','那封信来自未来的自己,结尾留下循环线索。'],['身份谜局','寄信人与侦探身份交错,制造一次认知反转。'],['无声追踪','门外脚步声逐渐逼近,用环境制造压迫感。']]},
{id:'style',title:'画面希望呈现什么质感?',hint:'这会影响角色设定、镜头提示词和后续画面生成。',options:[['写实电影感','冷色雨夜、克制灯光、真实人物,突出悬疑氛围。'],['黑白侦探片','强烈明暗对比与胶片颗粒,经典黑色电影风格。'],['悬疑动画','风格化人物与场景,更适合系列化内容。']]},
{id:'ending',title:'这 15 秒,你希望如何收尾?',hint:'可以选一个方向,也可以在下面直接写你的想法。',options:[['留一个钩子','门外出现与侦探完全相同的身影,故事戛然而止。'],['揭示一次反转','侦探打开信,发现每句话都在描述眼前的动作。'],['交给你决定','根据已有设定,选择最适合短片节奏的结尾。']]}
];
const initial = () => ({stage:'home',scene:'通用',prompt:defaultPrompt,answers:['','',''],extras:['','',''],question:0,version:1,planOpen:false,quoteAgreed:false,textApproved:false,canvasCreated:false,mobileCanvas:false,phase:0,selectedNode:'',nodeNames:{},messages:[],editNote:'',retry:false,restored:false,zoom:0.8});
let s = initial(); let timer = null; let toastTimer = null; let previousFocus = null;
const esc = v => String(v ?? '').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
const btn = (text,action,cls='secondary',extra='') => `<button class="${cls}" data-action="${action}" ${extra}>${text}</button>`;
const referenceNames = ['角色参考 · 年轻侦探','场景参考 · 雨夜办公室','道具参考 · 神秘信件'];
const nodeName = id => s.nodeNames[id] || (id==='script'?'故事与视频方案':id.startsWith('video')?'雨夜来信 · 15 秒短片':referenceNames[Number(id.split('-')[1])-1]);
const art = (index=1,video=false) => `<div class="scene-art scene-${index}" role="img" aria-label="${video?'15 秒短片':referenceNames[index-1]}的抽象示意,并非真实生成素材"><i class="head"></i><i class="clock-face"></i><i class="art-grain"></i><span class="art-label">${video?'短片封面':'参考图片'}示意</span>${video?'<span class="play" aria-hidden="true">▶</span>':''}</div>`;
const toast = message => { clearTimeout(toastTimer); const el=document.getElementById('toast'); el.textContent=message; el.hidden=false; toastTimer=setTimeout(()=>el.hidden=true,2800); };
const stopTimer = () => {if(timer)clearInterval(timer);timer=null;};
function progress() {
if(s.stage==='questions')return {done:0,active:0,label:'完善创作需求'};
if(s.stage==='proposal')return {done:1,active:1,label:'等待确认创意方案'};
if(s.stage==='quote')return {done:s.retry?4:3,active:s.retry?4:3,label:s.retry?'等待视频重做授权':'等待确认生成费用'};
if(s.stage==='running')return {done:s.phase<2?3:s.phase<4?4:5,active:s.phase<2?3:s.phase<4?4:5,label:s.retry?'正在重做 15 秒短片':s.phase<2?'正在生成 3 张参考图片':s.phase<4?'正在生成 1 段 15 秒短片':'正在回写结果'};
if(s.stage==='failure')return {done:5,active:5,label:'产物已就绪 · 等待恢复回写'};
if(s.stage==='complete')return {done:6,active:-1,label:'创作已完成'};
return {done:0,active:-1,label:''};
}
function shell() {
const onCanvas=s.canvasCreated;
return `<aside class="app-sidebar"><div class="brand"><span class="brand-mark">∞</span><span class="brand-word">影策<small>创作工作台</small></span></div>
<button class="side-action ${!onCanvas?'active':''}" data-action="home"><span class="side-glyph">⌂</span><span class="nav-text">创作首页</span></button>
<button class="side-action" data-action="short-drama"><span class="side-glyph">▤</span><span class="nav-text">短剧工作台</span></button>
<button class="side-action ${onCanvas?'active':''}" data-action="canvas"><span class="side-glyph">▦</span><span class="nav-text">画布</span></button>
<button class="side-action" data-action="assets"><span class="side-glyph">▧</span><span class="nav-text">素材库</span></button>
<div class="side-group">最近创作</div><button class="side-action" data-action="resume"><span class="side-glyph">◷</span><span class="nav-text">雨夜来信</span></button>
<div class="side-foot"><span class="avatar">YC</span><span class="account-text">我的工作空间<small>个人创作</small></span></div></aside>
<main class="app-stage"><header class="app-topbar"><div class="breadcrumb"><span class="optional">影策</span><span class="optional">/</span><strong>${onCanvas?'雨夜来信':s.stage==='home'?'创作首页':'新创作'}</strong>${s.stage!=='home'?'<span class="soft-pill">悬疑短片</span>':''}</div><div class="top-actions">${onCanvas?'<span class="top-status">同一会话 · 5 个节点</span>':s.stage!=='home'?'<span class="top-status">画布节点 0</span>':''}${onCanvas?btn(s.mobileCanvas?'返回会话':'查看画布','mobile-canvas','mini-btn mobile-toggle'):''}${btn('新创作','new','mini-btn')}</div></header><div class="content">${s.stage==='home'?home():onCanvas?canvas():`<section class="conversation-only">${chat()}</section>`}</div></main>`;
}
function home() {
return `<section class="home-page"><div class="home-inner"><div class="eyebrow">从一个想法,到一份作品</div><h1>今天,想创作点什么?</h1><p class="home-subtitle">告诉我你的想法,我们一起把它变成画面。</p>
<div class="scene-switch" aria-label="创作场景">${['通用','短剧','营销','电商'].map(x=>`<button data-action="scene" data-scene="${x}" aria-pressed="${s.scene===x}">${x}</button>`).join('')}</div>
<form id="home-form" class="home-composer"><label for="home-prompt" class="sr-only">描述你的创作需求</label><textarea id="home-prompt" placeholder="描述你想创作的内容,也可以添加参考素材…">${esc(s.prompt)}</textarea><div class="composer-tools"><div class="composer-tool-left">${btn('+ 素材','attach','mini-btn')}<span class="soft-pill">Agent 自动</span><span class="soft-pill">技能 · 短片创作</span></div><button type="submit" class="primary">开始创作 ↑</button></div></form>
<div class="home-examples"><button class="example" data-action="example" data-example="short"><span>短剧 / 影视</span><strong>15 秒,讲一个悬疑故事</strong><p>从创意、参考素材到完整短片</p></button><button class="example" data-action="example" data-example="marketing"><span>营销创作</span><strong>把一个卖点讲得更动人</strong><p>从受众、文案到视觉方案</p></button><button class="example" data-action="example" data-example="commerce"><span>电商设计</span><strong>让商品成为画面的主角</strong><p>从商品素材到场景图组</p></button></div><p class="home-note">本次完整演示:悬疑短片。营销、电商入口展示后续复用位置。</p></div></section>`;
}
function userMessage(text) {return `<div class="message user"><div class="user-bubble">${esc(text)}</div></div>`;}
function assistant(body) {return `<div class="message assistant"><div class="assistant-label"><span class="assistant-dot">∞</span>影策</div>${body}</div>`;}
function questionCard() {
const q=questions[s.question];
return `<div class="question-card"><div class="card-overline"><span>补充创作需求 · ${s.question+1} / ${questions.length}</span><span class="step-dots">${questions.map((_,i)=>`<i class="${i<=s.question?'current':''}"></i>`).join('')}</span></div><h3 class="question-title">${q.title}</h3><p class="question-help">${q.hint}</p><div class="options">${q.options.map(([title,desc])=>`<button class="option" data-action="answer" data-value="${esc(title)}" aria-pressed="${s.answers[s.question]===title}"><i class="option-radio"></i><span><strong>${title}</strong><small>${desc}</small></span></button>`).join('')}</div><label class="free-label" for="answer-extra">自定义或补充要求</label><textarea class="field" id="answer-extra" rows="2" placeholder="也可以直接写你的想法…">${esc(s.extras[s.question])}</textarea><div id="question-error" class="error-text" aria-live="polite"></div><div class="card-footer"><div class="left">${btn('上一题','prev-question','mini-btn',s.question===0?'disabled':'')}</div><div class="right">${btn(s.question===2?'提交需求':'下一题 →',s.question===2?'submit-answers':'next-question','primary')}</div></div></div>`;
}
function submitted() {return `<div class="submitted"><div><span>✓ 已提交</span> · ${esc(s.answers[0]||'自定义悬念')} / ${esc(s.answers[1]||'自定义风格')}</div>${btn('查看 / 修改','edit-answers','text-button')}</div>`;}
function titleText(){ return s.answers[0]==='身份谜局'?'另一位侦探':s.answers[0]==='无声追踪'?'门外的脚步':'雨夜来信'; }
function story(){ return s.answers[0]==='身份谜局'?'雨夜,侦探收到一封署着自己名字的信。信中照片上的人正坐在同一张书桌前。门开了,对方称这里是自己的办公室。':s.answers[0]==='无声追踪'?'雨夜,侦探收到一封写着自己名字的信。拆信时,门外的脚步与他的心跳同步。信纸上只有一句:不要让门外的人知道你在这里。':'雨夜,年轻侦探收到一封写着自己名字的信。信里警告:不要开门。敲门声响起,他透过门缝看见的,却是另一个自己。'; }
function proposalCard(compact=false) {
return `<div class="plan-card"><div class="card-overline"><span>创意方案 · v${s.version}</span><span>${s.canvasCreated?'已确认版本可回看':'等待你的确认'}</span></div><h3>《${titleText()}》</h3><p class="logline">${story()}</p>${compact?'':`<div class="plan-summary"><div><strong>1 集 · 15 秒 · 9:16</strong><span>一段视频,分为 3 个叙事节奏</span></div><div><strong>${esc(s.answers[1]||'写实电影感')}</strong><span>冷色雨夜 / 克制表演</span></div><div><strong>角色、场景与道具</strong><span>年轻侦探 / 旧办公室 / 神秘信件</span></div><div><strong>预期产物</strong><span>3 张参考图 + 1 段 15 秒短片</span></div></div>`}${s.editNote?`<p class="quiet">本版调整:${esc(s.editNote)}</p>`:''}<div class="scope-note">${s.canvasCreated?'这是已确认的创意方向。修改后会生成新版本,再确认受影响的节点。':'确认后创建 1 个方案文本节点、3 个参考图片节点、1 个视频节点及连线。图片和视频生成费用另行确认。'}</div><div class="card-footer"><div class="left">${btn('查看完整方案','view-plan','text-button')}</div><div class="right">${!s.canvasCreated?btn('修改','edit-plan','secondary')+btn('确认方案','confirm-plan','primary'):''}</div></div></div>`;
}
function quote() {
const retry=s.retry;
return `<div class="quote-card"><div class="card-overline"><span>${retry?'视频重做授权':'生成授权'}</span><span>示例报价 · 非真实计价</span></div><div class="quote-title"><strong>${retry?'仅重做 15 秒短片':'生成参考图与 15 秒短片'}</strong><div><b>${retry?'72':'84'}</b><small> 示例积分</small></div></div>${retry?'<div class="quote-row"><span>复用 3 张已完成参考图</span><b>0 新增积分</b></div>':'<div class="quote-row"><span>角色 / 场景 / 道具参考图 · 示例图像模型</span><b>3 × 4 = 12</b></div>'}<div class="quote-row"><span>15 秒短片 · 示例视频模型</span><b>1 × 72 = 72</b></div><div class="quote-row quote-total"><span>本次授权上限</span><b>${retry?'72':'84'} 示例积分</b></div><p class="quote-note">${retry?'仅重新生成视频;原视频保留为上一版,3 张参考图保持原样。':'先生成角色、场景、道具参考图,再按视频模型已验证的参考输入能力,生成一段 15 秒视频。'} 不包含自动重试、额外变体或后续批次。模型、数量或费用变化后需要重新确认。</p><label class="quote-check"><input type="checkbox" id="quote-agree" ${s.quoteAgreed?'checked':''}>同意上述生成范围和积分上限</label><div class="card-footer"><span class="quiet">未授权前不提交生成任务</span>${btn('确认并生成','approve','primary',s.quoteAgreed?'':'disabled')}</div></div>`;
}
function result() {
return `<p class="assistant-text">${s.restored?'15 秒短片已恢复到原视频节点。没有重新请求模型,也没有新增生成费用。':s.retry?'15 秒短片的新版本已完成,原视频和 3 张参考图保留。':'3 张参考图和 1 段 15 秒短片已完成,结果已回写到对应节点。'}</p><button data-action="preview" data-kind="video" data-shot="1" style="display:block;width:100%;padding:0">${art(1,true)}</button><p class="quiet">《${titleText()}》· 15 秒 · 9:16${s.retry?' · v2':''} · 视频封面示意</p><div class="result-grid">${[1,2,3].map(i=>`<button data-action="preview" data-kind="image" data-shot="${i}">${art(i,false)}<small>${referenceNames[i-1]}</small></button>`).join('')}</div><div class="receipt-card"><div class="receipt-line"><b>本轮结果</b><span>示例数据</span></div><div class="receipt-line"><span>画布结果</span><strong>已回写 4 项产物</strong></div><div class="receipt-line"><span>本次示例积分</span><strong>${s.retry?'72':'84'}</strong></div><div class="receipt-line"><span>视频交付</span><strong>1 段可预览的 15 秒短片</strong></div></div><div class="chat-more">${btn('只重做视频','redo','secondary')}${btn('预览短片','preview','secondary','data-kind="video" data-shot="1"')}${btn('继续调整节点','edit-node-prompt','secondary')}</div>`;
}
function chat() {
let body='';
if(s.stage==='casual') {
body=userMessage('悬疑短片和恐怖短片有什么区别?')+assistant('<p class="assistant-text">悬疑短片主要让人想知道“真相是什么”,靠线索、信息差和反转推动;恐怖短片更强调威胁感与恐惧体验。</p><p class="assistant-text">你这类 15 秒短片可以先给一个异常线索,再用最后一个镜头改变观众的理解。比如,一封来自未来的信。</p>');
} else {
body=userMessage(s.prompt)+assistant(`<p class="assistant-text">我已经记下这几个条件,接下来只补充会影响创作方向的信息。</p><div class="facts"><span class="fact">悬疑推理</span><span class="fact">1 集 · 15 秒</span><span class="fact">9:16 竖屏</span><span class="fact">雨夜 · 年轻侦探</span></div>${s.stage==='questions'?questionCard():submitted()}`);
if(s.stage!=='questions') body+=assistant(`<p class="assistant-text">根据你的选择,我整理了一个适合 15 秒表达的故事。先看看方向是否符合预期。</p>${proposalCard(s.canvasCreated)}`);
if(s.canvasCreated)body+=assistant('<p class="assistant-text">方案已确认,画布已创建 5 个节点及 6 条连线:方案文本、角色参考、场景参考、道具参考,以及 15 秒短片。节点中的提示词可以继续调整。</p>');
if(s.stage==='quote')body+=assistant(quote());
if(s.stage==='running')body+=assistant(`<p class="assistant-text">已获得${s.retry?'仅重做视频':'本批次'}授权。${s.phase<2?'正在生成角色、场景、道具参考图。':s.phase<4?'参考图已就绪,正在生成一段 15 秒短片。':'媒体已生成,正在回写画布结果。'}</p><div class="receipt-card"><div class="receipt-line"><span>当前步骤</span><strong>${s.phase<2?'生成参考图片':s.phase<4?'生成 15 秒短片':'回写结果'}</strong></div><div class="receipt-line"><span>后续操作</span><strong>${s.phase<2?'参考图片成功后才开始视频':s.phase<4?'视频完成后检查并回写':'绑定已有产物到原节点'}</strong></div><p class="quiet">评审原型正在按预设状态演示;没有真实任务。</p>${btn('暂停演示','pause','secondary')}</div>`);
if(s.stage==='failure')body+=assistant(`<p class="assistant-text">3 张参考图和 1 段 15 秒短片已经生成,但视频产物未能回写到画布。</p><div class="warning">回写中断 · 已有产物和任务记录保留。恢复时只绑定已有产物,不重新生成。</div><div class="receipt-card"><div class="receipt-line"><span>生成任务</span><strong>4 / 4 已完成</strong></div><div class="receipt-line"><span>画布回写</span><strong>3 / 4 已完成</strong></div><div class="receipt-line"><span>本次恢复新增生成费</span><strong>0 示例积分</strong></div>${btn('恢复结果回写','restore','primary')}</div>`);
if(s.stage==='complete')body+=assistant(result());
}
body+=s.messages.map(m=>m.role==='user'?userMessage(m.text):assistant(`<p class="assistant-text">${esc(m.text)}</p>`)).join('');
return `<div class="chat-area"><div class="chat-scroll" id="chat-scroll">${body}</div>${composer()}</div>`;
}
function composer() {
const p=progress(),hasTask=s.stage!=='casual'&&s.stage!=='home';
return `<div class="composer-wrap">${hasTask?`${s.planOpen?planPopover():''}<button class="progress-trigger" data-action="toggle-plan" aria-expanded="${s.planOpen}" aria-controls="execution-plan"><span class="progress-info"><span>${p.done===6?'✓':'◷'}</span>${p.label}</span><span class="progress-info"><span class="progress-track"><span style="width:${p.done/6*100}%"></span></span><span class="progress-count">${p.done}/6</span><span>${s.planOpen?'⌄':'⌃'}</span></span></button>`:''}<form class="chat-composer" id="chat-form"><label class="sr-only" for="chat-input">继续补充或调整创作需求</label><textarea id="chat-input" placeholder="${s.stage==='questions'?'也可以直接补充你的想法…':s.stage==='complete'?'继续调整,例如:把场景节点改名为「雨夜旧办公室」':'继续补充想法,或调整已有方案…'}"></textarea><div class="composer-tools"><div class="composer-tool-left">${btn('+','attach','mini-btn','aria-label="添加素材"')}<span class="soft-pill">Agent · 自动</span></div><button class="primary" type="submit" aria-label="发送消息">↑</button></div></form></div>`;
}
function planPopover() {
const p=progress(); const labels=[['完善创作需求','从上下文提取已有条件,只补问缺失项'],['确认创意方案','故事、设定、叙事节奏与创建范围'],['创建画布节点','5 个节点及 6 条连线'],['生成参考图片','角色、场景、道具各 1 张'],['生成 15 秒短片','按视频能力引用参考素材,直接生成一段视频'],['检查并回写结果','按任务回执和节点结果确认完成']];
return `<section id="execution-plan" class="plan-popover" aria-label="创作执行计划"><h3>创作计划 <span class="quiet">${p.done}/6</span>${btn('收起','toggle-plan','mini-btn')}</h3><ol>${labels.map(([name,desc],i)=>`<li><span class="plan-indicator ${i<p.done?'done':i===p.active?'active':''}">${i<p.done?'✓':i+1}</span><span><strong>${name}${i===p.active&&s.stage==='failure'?' · 等待恢复':''}</strong><small>${desc}</small></span></li>`).join('')}</ol></section>`;
}
function nodeStatus(index,video) {
if(s.stage==='complete')return 'done';
if(s.stage==='failure')return video?'failure':'done';
if(s.stage==='running') {
if(s.retry&&!video)return 'done';
if(!video)return s.phase>=2?'done':'running';
return s.phase>=4?'done':s.phase>=2?'running':'empty';
}
if(s.retry)return 'done';
return 'empty';
}
function mediaNode(i,video) {
const id=`${video?'video':'image'}-${i}`,status=nodeStatus(i,video); const name=nodeName(id); const y=video?234:23+(i-1)*211;
return `<article class="canvas-node ${s.selectedNode===id?'selected':''}" data-node="${id}" style="left:${video?567:278}px;top:${y}px;width:210px"><span class="node-port"></span>${video?'':'<span class="node-port out"></span>'}<header class="node-header"><span class="node-type">${video?'▷':'▧'}</span><span class="node-title">${esc(name)}</span><button class="node-edit" data-action="rename" data-id="${id}" aria-label="修改${esc(name)}名称">✎</button></header>${status==='done'?`<button data-action="preview" data-kind="${video?'video':'image'}" data-shot="${i}" style="display:block;padding:0;width:100%">${art(i,video)}</button>`:`<div class="node-empty ${status==='running'?'running':''}"><strong>${status==='failure'?'!':status==='running'?'◷':video?'▷':'▧'}</strong><span>${status==='failure'?'已有结果,等待恢复回写':status==='running'?'生成中 · 演示状态':'等待授权生成'}</span><span class="quiet">${video?'15 秒 · 9:16 · 完整短片':'9:16 · 参考图片'}</span></div>`}<footer class="node-foot"><span>${status==='done'?'✓ 示例产物已回写':status==='failure'?'回写待恢复':status==='running'?'任务已接受':'提示词已填充'}</span><button data-action="node-detail" data-id="${id}">查看参数</button></footer></article>`;
}
function canvas() {
return `<section class="canvas-layout ${s.mobileCanvas?'show-canvas':''}"><div class="canvas-pane"><div class="canvas-heading"><strong>《${titleText()}》</strong><span>方案 v${s.version} · 方案文本 → 角色 / 场景 / 道具参考 → 15 秒短片</span></div><div class="canvas-scroll" id="canvas-scroll"><div class="canvas-size" id="canvas-size"><div class="canvas-scaled" id="canvas-scaled"><svg class="edges" viewBox="0 0 840 675" role="img" aria-label="方案文本连接角色、场景、道具参考图片;三个已支持的参考输入汇入同一个 15 秒视频节点"><path d="M220 332 C247 332 248 118 278 118"/><path d="M220 332 C247 332 248 329 278 329"/><path d="M220 332 C247 332 248 540 278 540"/>${[118,329,540].map(y=>`<path d="M488 ${y} C525 ${y} 530 329 567 329"/>`).join('')}</svg><article class="canvas-node" style="left:20px;top:223px;width:200px"><span class="node-port out"></span><header class="node-header"><span class="node-type">T</span><span class="node-title">${esc(nodeName('script'))}</span><button class="node-edit" data-action="rename" data-id="script" aria-label="修改方案节点名称">✎</button></header><div class="node-text"><h4>${titleText()}</h4>${story()}<div class="facts"><span class="fact">15 秒</span><span class="fact">9:16</span></div><span class="quiet">片内节奏:信件出现 → 信中警告 → 门外的自己</span></div><footer class="node-foot"><span>方案 v${s.version}</span><button data-action="view-plan">查看方案</button></footer></article>${[1,2,3].map(i=>mediaNode(i,false)).join('')}${mediaNode(1,true)}</div></div></div><div class="canvas-tools">${btn('-','zoom-out','')}${btn(`${Math.round(s.zoom*100)}%`,'fit','', 'id="zoom-label"')}${btn('+','zoom-in','')}<span class="divider"></span>${btn('适应画布','fit','')}${btn('整理节点','arrange','')}</div><span class="canvas-legend">图片、视频均为示意</span></div><aside class="chat-pane"><div class="chat-pane-header"><strong>创作助手</strong><span class="quiet">接续首页会话</span></div>${chat()}</aside></section>`;
}
function resizeCanvas(fit=false) {
const pane=root.querySelector('.canvas-pane'); const scaled=document.getElementById('canvas-scaled'); const size=document.getElementById('canvas-size');
if(!pane||!scaled||!size)return;
if(fit&&pane.clientWidth>0)s.zoom=Math.min(1,Math.max(.35,(pane.clientWidth-46)/840));
scaled.style.transform=`scale(${s.zoom})`;size.style.width=`${840*s.zoom}px`;size.style.height=`${675*s.zoom}px`;
const zoomLabel=document.getElementById('zoom-label');if(zoomLabel)zoomLabel.textContent=`${Math.round(s.zoom*100)}%`;
}
function render({bottom=false,fit=false}={}) {
const oldScroll=document.getElementById('chat-scroll'); const y=oldScroll?.scrollTop||0;root.innerHTML=shell();document.getElementById('review-state').value=s.stage;resizeCanvas(fit);
const scroll=document.getElementById('chat-scroll');if(scroll)scroll.scrollTop=bottom?scroll.scrollHeight:y;
}
function showDialog(title,body,foot='') {
previousFocus=document.activeElement;dialog.innerHTML=`<div class="dialog-head"><h2 id="dialog-title">${title}</h2>${btn('关闭 ×','close-dialog','mini-btn','aria-label="关闭弹窗"')}</div><div class="dialog-body">${body}</div>${foot?`<div class="dialog-foot">${foot}</div>`:''}`;if(!dialog.open)dialog.showModal();
}
function closeDialog(){dialog.close();previousFocus?.focus?.();}
function planDetail() {
showDialog(`《${titleText()}》· 创意方案 v${s.version}`,`<div class="detail-meta"><span>1 段视频 · 15 秒</span><span>9:16 竖屏</span><span>${esc(s.answers[1]||'写实电影感')}</span><span>3 个片内叙事节奏</span></div><h3>故事大纲</h3><p>${story()} 通过一封信和一次开门动作,在 15 秒内完成“发现异常—危险逼近—身份反转”。</p><h3>角色、场景与道具参考</h3><div class="detail-assets"><div class="detail-asset"><strong>角色 · 年轻侦探</strong><span>约 28 岁,短发、深色风衣,神情克制。全片沿用同一人物参考。</span></div><div class="detail-asset"><strong>场景 · 旧办公室</strong><span>雨夜、窄窗、木桌、走廊门,冷色环境中保留一盏暖灯。</span></div><div class="detail-asset"><strong>道具 · 神秘信件</strong><span>米白信封、黑色手写字迹。文字与声音的精确表现需以模型实际能力为准。</span></div></div><h3>一段 15 秒视频内部的节奏</h3><p>以下时间段共同写入一次视频生成的创作描述,属于同一段视频内部的叙事安排。</p><table class="shot-table"><thead><tr><th>时间</th><th>画面与动作</th><th>镜头语言 / 声音意图</th></tr></thead><tbody><tr><td>00–05s</td><td><b>01 · 信件出现</b>侦探在雨夜办公室发现写着自己名字的信封。</td><td>中景缓慢推进。雨声与室内钟声。</td></tr><tr><td>05–10s</td><td><b>02 · 信中警告</b>手部拆信,读到警告,敲门声突然响起。</td><td>信纸特写,焦点切到门口。</td></tr><tr><td>10–15s</td><td><b>03 · 门外的自己</b>门缓缓打开,同样衣着的身影站在门外,停在揭示瞬间。</td><td>越肩视角,保持两人外观一致。</td></tr></tbody></table><h3>本轮产物与确认范围</h3><p>创建 1 个方案文本节点、3 个参考图片节点、1 个视频节点及 6 条连线。先生成角色、场景、道具参考图,再引用已确认方案与模型支持的参考输入,直接生成 1 段可预览的 15 秒短片。</p><div class="scope-note">本例以已验证支持 15 秒和所需参考输入的视频模型为前提;具体参考数量、声音及文字效果以所选模型能力为准。未满足时先调整方案和报价,不提交不支持的任务。查看方案不会创建或更改画布节点。</div>${s.extras.some(Boolean)?`<h3>你的补充要求</h3>${s.extras.filter(Boolean).map(x=>`<p>${esc(x)}</p>`).join('')}`:''}${s.editNote?`<h3>本版调整</h3><p>${esc(s.editNote)}</p>`:''}`,`<span class="quiet">${s.canvasCreated?'节点已按已确认版本创建':'画布节点:0 · 等待确认'}</span><div>${btn('修改方案','edit-plan','secondary')}${!s.canvasCreated?btn('确认方案,进入画布','confirm-plan','primary'):btn('返回画布','close-dialog','primary')}</div>`);
}
function editPlan() { showDialog('调整创意方案',`<p>说明你想修改的部分。更新后会展示新版本,重新确认受影响的范围。</p><label class="free-label" for="plan-edit">修改要求</label><textarea id="plan-edit" class="field" rows="5" placeholder="例如:第二镜头突出信纸细节,结尾只露出门外人的轮廓。">${esc(s.editNote)}</textarea><p class="quiet">原型会记录调整说明并生成新版本;不会调用模型改写文本。</p>`,`<span class="quiet">暂不更改画布</span><div>${btn('取消','close-dialog','secondary')}${btn('更新方案','save-plan','primary')}</div>`); }
function seed(stage) {
stopTimer();s=initial();s.stage=stage;
if(['proposal','quote','running','complete','failure'].includes(stage)){s.answers=['时间循环','写实电影感','留一个钩子'];s.canvasCreated=['quote','running','complete','failure'].includes(stage);}
if(stage==='running')s.phase=2;
render({bottom:stage!=='home'&&stage!=='questions',fit:true});
}
function startRun() {stopTimer();s.stage='running';s.phase=s.retry?2:0;s.quoteAgreed=false;render({bottom:true});timer=setInterval(()=>{if(s.stage!=='running'){stopTimer();return;}s.phase+=1;if(s.phase>=5){stopTimer();s.stage='complete';s.planOpen=false;}render({bottom:true});},1900);}
function answerSave() {const el=document.getElementById('answer-extra');if(el)s.extras[s.question]=el.value.trim();}
function answerValid() {answerSave();if(s.answers[s.question]||s.extras[s.question])return true;document.getElementById('question-error').textContent='选择一个方向,或输入你的想法后继续。';return false;}
function sendChat(text) {
if(!text.trim())return;s.messages.push({role:'user',text:text.trim()});
if(s.stage==='questions') {s.extras[s.question]=text.trim();s.messages.push({role:'assistant',text:'已记录到当前问题的补充要求。你可以直接继续下一题。'});}
else if(s.canvasCreated&&/改名|命名/.test(text)){s.nodeNames['image-2']='场景参考 · 雨夜旧办公室';s.selectedNode='image-2';s.messages.push({role:'assistant',text:'已将场景参考节点改名为“场景参考 · 雨夜旧办公室”。这是节点文字调整,没有触发重新生成。'});}
else if(s.stage==='proposal') {s.editNote=text.trim();s.version+=1;s.messages.push({role:'assistant',text:`已记录方案调整,更新为 v${s.version},请查看并确认。原型仅演示版本与确认状态。`});}
else {s.messages.push({role:'assistant',text:s.canvasCreated?'已收到。这个原型可以演示节点改名、查看参数、局部重做和回写恢复;自由对话理解将在接入现有模型工具循环后实现。':'悬疑通常用线索和反转建立吸引力。普通聊天不创建任务,所以不会出现执行计划。'});}
render({bottom:true});
}
document.addEventListener('click',event=>{
const button=event.target.closest('[data-action]');if(!button)return;const a=button.dataset.action;
if(a==='theme'){const dark=document.documentElement.dataset.theme==='dark';document.documentElement.dataset.theme=dark?'light':'dark';return;}
if(a==='reset'||a==='new'){if(dialog.open)closeDialog();seed('home');return;}
if(a==='home'){stopTimer();s.stage='home';s.canvasCreated=false;s.messages=[];render();return;}
if(a==='scene'){s.prompt=document.getElementById('home-prompt')?.value||s.prompt;s.scene=button.dataset.scene;render();return;}
if(a==='example'){if(button.dataset.example==='short'){s.prompt=defaultPrompt;s.scene='短剧';render();document.getElementById('home-prompt').focus();}else toast('本次聚焦短片完整链路;该场景沿用同一输入和问答框架。');return;}
if(a==='short-drama'){toast('产品中沿用已有短剧工作台;本原型聚焦首页创作链路。');return;}
if(a==='assets'||a==='attach'){showDialog('添加参考素材',`<p>产品中复用现有素材库与上传选择器。此评审原型不上传文件。</p><div class="scope-note">素材会进入同一会话上下文。模型已经知道的角色、场景、比例和类型,应优先从素材与用户描述中提取。</div>`,`<span class="quiet">本例暂无附件</span><div>${btn('返回创作','close-dialog','primary')}</div>`);return;}
if(a==='canvas'){if(!s.canvasCreated)toast('方案尚未确认,画布保持 0 个节点。');else{s.mobileCanvas=true;render({fit:true});}return;}
if(a==='resume'){if(s.stage==='home')seed('questions');else toast('当前就是这段创作会话。');return;}
if(a==='answer'){answerSave();s.answers[s.question]=button.dataset.value;render();return;}
if(a==='prev-question'){answerSave();s.question=Math.max(0,s.question-1);render();return;}
if(a==='next-question'){if(answerValid()){s.question=Math.min(2,s.question+1);render({bottom:true});}return;}
if(a==='submit-answers'){if(answerValid()){s.stage='proposal';s.question=0;render({bottom:true});}return;}
if(a==='edit-answers'){if(s.canvasCreated){showDialog('已提交的创作需求',questions.map((q,i)=>`<h3>${q.title}</h3><p>${esc(s.answers[i]||'自定义')} ${esc(s.extras[i])}</p>`),`<span class="quiet">调整后生成新方案版本</span><div>${btn('修改方案','edit-plan','primary')}</div>`);}else{s.stage='questions';s.question=0;s.version+=1;render({bottom:true});}return;}
if(a==='view-plan'){planDetail();return;}
if(a==='edit-plan'){editPlan();return;}
if(a==='save-plan'){const note=document.getElementById('plan-edit').value.trim();if(!note){toast('请填写调整要求。');return;}s.editNote=note;s.version+=1;closeDialog();if(s.canvasCreated){s.messages.push({role:'assistant',text:`已记录方案 v${s.version} 的调整。现有节点暂不改变;原型本期以首次方案确认及节点局部编辑为主要评审路径。`});}else s.stage='proposal';render({bottom:true});return;}
if(a==='confirm-plan'){if(dialog.open)closeDialog();s.canvasCreated=true;s.stage='quote';s.planOpen=false;s.quoteAgreed=false;render({bottom:true,fit:true});return;}
if(a==='approve-text'){s.textApproved=true;s.stage='questions';closeDialog();render({bottom:true});return;}
if(a==='approve'){if(s.quoteAgreed)startRun();return;}
if(a==='toggle-plan'){s.planOpen=!s.planOpen;render();return;}
if(a==='pause'){stopTimer();toast('演示已暂停,可在顶部选择状态继续评审。');return;}
if(a==='restore'){s.restored=true;s.stage='complete';s.planOpen=false;render({bottom:true});toast('已仅恢复结果回写,新增生成费用为 0。');return;}
if(a==='redo'){s.retry=true;s.restored=false;s.stage='quote';s.quoteAgreed=false;s.selectedNode='video-1';render({bottom:true});return;}
if(a==='edit-node-prompt'){document.getElementById('chat-input').value='把场景参考节点改名为“场景参考 · 雨夜旧办公室”';document.getElementById('chat-input').focus();return;}
if(a==='rename'){const id=button.dataset.id;showDialog('修改节点名称',`<label class="free-label" for="node-name">节点名称</label><input id="node-name" class="field" value="${esc(nodeName(id))}"><p class="quiet">仅修改画布节点名称,不触发生成。</p>`,`<span></span><div>${btn('保存名称','save-name','primary',`data-id="${id}"`)}</div>`);return;}
if(a==='save-name'){const name=document.getElementById('node-name').value.trim();if(!name){toast('节点名称不能为空。');return;}s.nodeNames[button.dataset.id]=name;s.selectedNode=button.dataset.id;closeDialog();render();toast('节点名称已更新。');return;}
if(a==='node-detail'){const id=button.dataset.id;const video=id.startsWith('video');showDialog('节点参数',`<div class="detail-meta"><span>${video?'视频节点':'图片节点'}</span><span>9:16</span><span>${video?'15 秒 · 1 段完整短片':'单张参考图'}</span></div><h3>提示词</h3><p>${video?`${story()} 一段 15 秒视频依次表现信件出现、信中警告、门外身影三个叙事节奏,使用已确认的角色、场景、道具参考。`:id.endsWith('1')?'年轻侦探,约 28 岁,短发、深色风衣、克制神情。写实电影质感,清晰体现人物外观,用作全片人物参考。':id.endsWith('2')?'雨夜旧办公室,窄窗、木桌、走廊门。冷色窗光与桌面暖灯对比,用作全片场景参考。':'米白信封、黑色手写字迹及展开的信纸。道具细节清晰,用作视频中神秘信件的外观参考。'}</p><h3>参考关系</h3><p>${video?'本例选择已验证支持 15 秒及三类参考输入的视频模型。三个参考节点成功后才提交一次视频任务。实际接入时按模型能力校验数量与输入角色。':'引用已确认的创意方案中对应设定,生成后供同一个视频节点使用。'}</p><div class="scope-note">参数调整与重新生成分开。产生新生成费用时,需要新的批次授权。</div>`,`<span class="quiet">当前展示既有语义节点</span><div>${btn('关闭','close-dialog','primary')}</div>`);return;}
if(a==='preview'){const shot=Number(button.dataset.shot||1),video=button.dataset.kind==='video';showDialog(video?`《${titleText()}》· 15 秒短片示意`:`${referenceNames[shot-1]} · 图片示意`,`<div class="large-art">${art(shot,video)}</div><p style="text-align:center">${video?'示例封面 · 没有真实视频文件,播放按钮表示产品中的完整短片预览入口。':'抽象参考图片示意,不是模型真实生成素材。'}</p>`,`<span class="quiet">${video?'15 秒 · 1 段视频':'1 张参考图'} · 9:16</span><div>${btn('返回','close-dialog','primary')}</div>`);return;}
if(a==='zoom-in'||a==='zoom-out'){s.zoom=Math.min(1.5,Math.max(.35,s.zoom+(a==='zoom-in'?.1:-.1)));resizeCanvas();return;}
if(a==='fit'){resizeCanvas(true);return;}
if(a==='arrange'){s.selectedNode='';resizeCanvas(true);toast('已按“方案 → 角色 / 场景 / 道具参考 → 15 秒短片”整理并适应画布。');return;}
if(a==='mobile-canvas'){s.mobileCanvas=!s.mobileCanvas;render({fit:true});return;}
if(a==='close-dialog'){closeDialog();return;}
});
document.addEventListener('submit',event=>{if(event.target.id==='home-form'){event.preventDefault();const input=document.getElementById('home-prompt').value.trim();if(!input){toast('先写下你的创作想法。');return;}s.prompt=input;if(!/15|悬疑|侦探|雨夜/.test(input)){toast('本原型完整演示悬疑短片,请使用下方短片示例。');return;}s.canvasCreated=false;if(!s.textApproved){showDialog('确认本次创作规划预算',`<p>理解需求、补充问答和生成创意方案也可能产生文本模型费用。</p><div class="receipt-card"><div class="receipt-line"><span>授权范围</span><strong>本次短片需求梳理与方案生成</strong></div><div class="receipt-line"><span>文本请求上限</span><strong>6 次示例请求</strong></div><div class="receipt-line"><span>总费用上限</span><strong>2 示例积分</strong></div></div><p>范围内继续补问与规划,达到请求次数或费用上限后再次确认。图片和视频任务稍后单独报价。</p><div class="scope-note">这里的模型次数和积分仅用于设计评审,不是真实价格;点击确认不会请求模型或扣费。</div>`,`<span class="quiet">示例预算 · 本次会话有效</span><div>${btn('取消','close-dialog','secondary')}${btn('确认,开始梳理需求','approve-text','primary')}</div>`);}else{s.stage='questions';render({bottom:true});}}if(event.target.id==='chat-form'){event.preventDefault();sendChat(document.getElementById('chat-input').value);}});
document.addEventListener('change',event=>{if(event.target.id==='review-state')seed(event.target.value);if(event.target.id==='quote-agree'){s.quoteAgreed=event.target.checked;const approve=root.querySelector('[data-action="approve"]');if(approve)approve.disabled=!s.quoteAgreed;}});
dialog.addEventListener('click',event=>{if(event.target===dialog){const r=dialog.getBoundingClientRect();if(event.clientX<r.left||event.clientX>r.right||event.clientY<r.top||event.clientY>r.bottom)closeDialog();}});
window.addEventListener('resize',()=>resizeCanvas(true));
document.documentElement.dataset.theme=window.matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light';
render();
})();
</script>
</body>
</html>

View File

@@ -0,0 +1,357 @@
---
title: 通用创作 Agent 第一期体验与实现设计
description: 复核两段录屏及六张截图,以连续问答、方案确认、同会话画布执行和真实视频产出为第一期验收主线
---
# 通用创作 Agent 第一期体验与实现设计
> 最新入口调整:按用户确认,首页本期暂缓,撤回新增独立创作界面;问答、方案、确认、执行和历史融合进原画布 Agent,会话和输入框不另建。下文首页接续属于暂缓设计,当前实现以[运行说明](../content/docs/backend/creative-agent-runtime.mdx)为准。
> 状态:第一期代码已接入,待联调与用户验收。本文细化[完整需求设计](../content/docs/overview/creative-agent-design.mdx),本期交付边界以本文为准。实现与待验收差异见[待测试](../content/docs/progress/pending-test.mdx)。未调用真实收费模型,不能将自动化测试通过等同于达到录屏效果。
配套[交互原型](creative-agent-phase-one-prototype.html)使用示例数据,仅供查看布局、卡片和状态转换,不证明真实生成或保存能力。原型中的价格、资源和节点状态均为演示。
原型顶部“评审状态”可以直接切换普通聊天、问答、方案、费用、执行、结果与失败状态,这是设计查看工具,不是产品中的权限入口。原型不实现真实模型语义理解、通用素材上传、后端恢复或收费校验;这些按本文验收脚本实施。HTML 文件独立保留,便于后续评审。
## 1. 第一期要交付的体验
用户从创作首页输入想法,Agent 结合上下文补问,生成可以查看和修改的创意方案。用户批准后,原会话接续到画布,Agent 创建本阶段真实节点与连线,按授权范围生成素材和视频,结果同时出现在会话和画布。进度可展开,用户可以修改节点或局部重做。
第一期以“一集短片试作”为主要验收场景,目标接近第一个视频中的 15 秒、9:16、悬疑短片体验;同时以“两张商品营销海报”验证同一套问答、确认和计划合同能用于其他场景。系统不固定悬疑、15 秒或六道题。
本期的可用版本必须覆盖首页到画布、真实视频生成和结果预览。开发可先完成画布内部能力,但不能把仅有问答卡或只有图片生成的中间成果称为已达到视频参考效果。具体视频模型必须先验证时长、比例和素材输入支持;能力不足时说明差距,由用户选择可执行替代,不静默缩短视频。
固定边界:沿用当前在线 Agent 和后端模型中转;不接入 Harness,不建设完整服务端画布 MCP,不增加草稿节点,不全面重构,不改变浏览器关闭后 Agent 无法继续推进的运行边界。
## 2. 参考资料复核
### 2.1 第一个视频:主要交互目标
参考文件为用户上传的“录屏2026-09-07 12.46.02.mov”,长度约 54 秒。以下时间是录屏播放位置,不是生成任务的耗时。
| 时间点 | 可直接观察到的内容 | 转化为本期需求 |
| --- | --- | --- |
| 00:00 | 左侧画布已有创意文本、道具图、角色图、场景图、视频;右侧为“搭建短剧工作流”会话 | 画布和会话并排,产物具有业务名称;历史能回看 |
| 00:06 | 题材卡,选项含标题/说明、已选高亮、已提交状态及 1/6 导航 | 单卡分页、清晰选择、提交回执、历史可浏览 |
| 00:10 | 时长卡显示 2/6,后面是“我已确认以上信息” | 回答按结构化信息汇总,一批提交后连续推进 |
| 00:14 | 6/6 为自由补充;模型汇总悬疑、15 秒、试做一集、9:16、写实等信息 | 自定义输入与选项共存;需求摘要参与后续规划 |
| 00:21 | Creative Design 产物卡,带故事板标签、查看分镜入口;随后为确认方案卡 | 方案是可打开的内容对象,不能只是长段聊天或空缩略图 |
| 00:28~00:35 | “确认方案,开始制作”“修改剧本”“换一个方向” | 确认、局部修改、方向调整三种明确分支 |
| 00:41 | 会话内展示竖屏视频画面及 0:15,带相关操作入口 | 真实结果可以预览,并与画布中的结果对应 |
| 00:53 | 结果卡后仍出现“服务器繁忙,请稍后重试” | 生成结果和后续助手响应分别记录,后续失败不能抹掉已成功产物 |
证据边界:该录屏主要是滚动浏览已提交的历史,开头已有节点,因此不能据此认定全部节点在本段视频内实时创建,不能用 54 秒作为生成性能目标,也未观察到完整扣费确认过程。首页进入来自另行提供的截图。用户明确要求的“确认后才创建节点”“收费前确认”继续作为本项目规则。
### 2.2 第二个视频:补充阶段与画布关联
参考文件为“录屏2026-09-07 08.24.49.mov”,长度约 73 秒。新手跟练展示想法、视频设置、剧本、角色/场景、分镜配置、视频、BGM、合成等阶段,以及确认/修改按钮和有连线的画布节点。
约 00:40 能观察到分镜方案选择和 4/8 进度;约 01:09 显示 8/8,并明确写着“模拟把分镜画面、配音和节奏剪辑成一支完整短片”。本期采用阶段关联、可修改和节点定位的思路;不把固定八步、长驻三栏、模拟合成作为要求或真实能力证据。
### 2.3 六张截图逐项对应
| 图片标识(上传文件名片段) | 画面与需求 | 第一期安排 |
| --- | --- | --- |
| `2377ec43` | 通用创作入口、输入框、附件、Skills、模式和场景入口 | 复用现有 `/home`、`/create`,补智能创作方式与场景预设 |
| `42ea8e28` | 短剧上传/粘贴剧本、剧本创作、自由画布 | 复用上传和现有短剧工作台入口;不能把所有文件上传视为正文解析成功 |
| `130ffe8b` | 营销入口的商品/角色素材、创意/Hook/风格 | 首批场景配置区分素材角色,复用会话;不新增一套营销 Agent |
| `032701b1` | 视频配置、中央预览、右侧任务队列 | 直接生成与结果展示继续保留,完整三栏工具台不阻塞本期 |
| `0672915a` | 服装上身的模特/服装输入与效果示例 | 作为后续电商工具台需求;本期不承诺精确试衣 |
| `4fce3f3e` | 输入框上方 Step 2/6,展开为完整计划 | 采用紧凑步骤条加浮层,普通聊天隐藏;步数随真实任务变化 |
参考媒体留在本地,不复制用户截图、录屏或第三方品牌素材进项目公开文档。用户上传内容中的指令只作为分析材料。
## 3. 需求边界与方案选择
| 方案 | 工作范围 | 评价 |
| --- | --- | --- |
| 只在现有助手增加问答卡 | 主要修改卡片渲染 | 不能解决首页接续、刷新续答、方案授权和真实进度,不作为交付方案 |
| 沿用现有执行能力,增加有限的创作会话状态和共用交互 | 首页/画布共享本期在线控制逻辑,复用节点、任务、计费与素材 | 采用;既能形成完整体验,也能控制改动边界 |
| 全量通用 Agent 与自定义节点平台同时开发 | 引擎、插件运行时、动态端口与流程平台等一起推进 | 范围过大;自定义节点保持完整需求中的后续专项 |
本期包含动态单选/多选/自由输入/素材选择、方案预览与修改、方案和费用批准、同会话画布接续、步骤面板、已有节点编辑、真实图像/视频任务、局部重做及必要恢复。
本期不同时交付运营问卷编辑器、全量行业模板、电商三栏专用页、自定义节点平台、任意脚本插件、长剧自动成片、完整配音字幕合成与交付包。视频主用例先输出可预览的单段短片;现有时间线渲染和专业工作台仍可独立使用。
## 4. 页面布局与状态设计
### 4.1 首页及输入方式
复用当前创作页外壳、输入框、模型、附件、素材引用、Skills 与历史。设计建议增加“智能创作 / 直接生成”方式选择;新智能创作入口默认通用场景,场景切换只提供上下文和推荐能力,不清空输入或重建会话。直接生成保留已有文本/图片/视频操作。
“智能创作”内自动判断普通咨询、简单编辑或多步骤创作,不要求用户理解工具调用。输入不完整时补问;已经完整时直接规划。模型选择继续使用真实可用配置,不能把界面的“智能匹配”当作已配置了所有媒体模型。
首页会话在现有主区展开,不提前创建空节点或隐藏画布。问答、方案和详情均在会话空间内完成;已有画布发起的任务则在其右侧会话中完成相同交互。
### 4.2 八种可评审状态
| 状态 | 主区域 | 当前主操作 | 画布与进度 |
| --- | --- | --- | --- |
| S0 空闲 | 首页输入、场景入口、推荐技能 | 发送需求 | 无任务计划 |
| S1 理解/补问 | 简短说明与问答卡 | 回答并继续 | 多步骤任务有紧凑进度;无本轮节点 |
| S2 方案待确认 | 摘要、方案卡、详情 | 确认方案,创建节点 | 查看详情不建节点 |
| S3 画布接续 | 画布与同一会话 | 查看执行范围/准备生成 | 批准后创建本阶段节点与连线 |
| S4 费用待确认 | 数量、模型、规格、真实报价 | 批准本批生成 | 无对应获批任务时不显示生成中 |
| S5 执行 | 节点任务状态、会话产物 | 暂停后续执行/查看结果 | 紧凑进度可展开,依真实任务更新 |
| S6 完成 | 可播放/下载产物、结果摘要 | 局部修改或重做 | 计划收起,历史保留 |
| S7 异常/暂停 | 保留成功结果和待处理内容 | 针对当前失败继续或重试 | 区分生成失败、回写失败、助手响应失败 |
### 4.3 画布侧栏与步骤条
画布使用现有右侧 `AssistantPanelColumn`,默认宽度建议约 440px,服从现有按视口限制的拖拽范围。顶部显示会话名称与必要控制,中部滚动消息,底部为步骤条和输入框。主画布保留当前工具、缩放和节点编辑入口。
步骤条示例:“第 3/6 步 · 等待你确认参考图”。点击向上展开步骤浮层,点击外部或 Escape 关闭并恢复焦点。浮层高度受会话区约束,不遮挡输入主操作;节点执行不持续抢用户视口。
普通聊天不出现步骤条。任务生成计划后显示;完成/取消后移入结果消息,可再次展开。并行任务显示“已完成 2/6,运行中 2 项”,不以连续编号制造串行假象。问题卡的“第几题”、业务计划步数、模型请求轮数分别表达。
小于 768px 的设计采用会话/画布切换或已有抽屉,同会话不重置;不强行压成三栏。主题、主操作、普通选中、焦点和减少动效均沿用项目 token。
## 5. 上下文问答的详细行为
### 5.1 已知信息先入需求摘要
场景规则提供稳定字段、字段解释、必需条件、允许默认值和推荐选项;模型结合会话、用户引用素材、已有画布选择及 Skills 提取信息。程序校验提取结果与待问字段,不仅依赖模型记忆最近几句聊天。
用户输入“做一集 15 秒、9:16 的写实悬疑短片”时,不再询问题材、时长、集数、比例或风格。若故事内容未确定,可询问创意方向或直接在用户允许自主决定的范围内给方案;不能为了演示强制补齐六题。
每个字段记录来源与状态。明确答案优先于推断,最新明确修改覆盖旧值。已上传但未解析的资料不能当作已知内容,素材身份不明时使用素材选择卡补齐用途。
### 5.2 问答卡合同
每批默认不超过 3 个必要问题,配置允许调整;录屏的 6 题是表现参考,不是固定题数。卡片一次展示一题,包含标题、必要性说明、选项标题/描述、自定义输入、上/下一题与批次提交。
- 单选点击仅选择;多选不自动跳页;下一题只切换界面,不代表批准执行。
- 每题允许自定义时直接提供输入,不要求输入选项数字;可选题显示跳过。
- 最后一题提供“提交并继续”,缺少必需输入定位到对应问题;不把默认选中或放置不操作当作回答。
- 提交成功后显示“已提交”和答案摘要;可回看各题。修改通过新版本提交,不在旧批准记录中原地改答案。
- 用户在普通输入框补充信息时,先合并需求,重新筛选未提交问题;保留其他输入,对已解决的题显示已由补充信息回答。
- 必要信息存在冲突时只澄清冲突字段。已确认信息无需再次要求“确认摘要”,可直接进入方案。
设计示例(仅表示逻辑结构):
```json
{
"kind": "question_request",
"interactionId": "question-example-1",
"revision": 1,
"questions": [
{
"id": "story-direction",
"field": "story.direction",
"type": "single",
"title": "你更想从哪个悬念开始?",
"options": [
{ "id": "elevator", "label": "电梯里的陌生楼层", "description": "密闭空间,结尾出现异常楼层" },
{ "id": "phone", "label": "来自自己的电话", "description": "一个电话打断日常,留下反转" }
],
"allowCustom": true,
"required": false
}
]
}
```
可信 ID、revision、状态和来源由程序维护;模型仅提出问题内容与字段关联。同一份场景规则参与提示构造与程序校验,所选场景和 Skills 必须显式进入当前消息组装点。
## 6. 方案、大纲与批准
### 6.1 方案卡要有可阅读的内容
方案卡显示标题、版本、摘要、产物数量/规格和“查看方案”。短片详情包括故事大纲、角色/场景/道具设定、镜头或叙事节奏、已有素材与待生成素材、输出视频规格。营销详情替换为受众、卖点、文案、视觉和交付清单,使用同一确认合同。
详情通过受控抽屉或会话内展开呈现 Markdown 与结构化列表;无需新增故事板编辑器或在确认前创建 Script 节点。“方案 v1”是会话内容对象,不称为草稿画布。缩略图若没有真实素材,使用明确的内容图标,不伪造已生成图片。
短片示例方案可描述:主角进入电梯,指示灯跳到不存在的楼层,门缝出现异样手影,最后以主角反应收尾。具体文字由模型生成,方案应说明哪些画面来自同一段视频,不能把文本中的多个叙事节奏误称为已完成的多个视频镜头。
### 6.2 三个分支
| 操作 | 行为 |
| --- | --- |
| 确认方案,创建节点 | 批准明确的本阶段节点、连线和参数准备;不隐含收费任务批准 |
| 修改方案 | 指定要改的内容,生成新版本并标示受影响部分;保留未受影响的要求 |
| 换个方向 | 保留比例、时长、素材等共用信息,替换创意方向;旧未执行批准失效 |
报价与执行参数完整时可以提供“确认并生成”,一次提交记录方案与费用两类批准。报价尚不确定时不提供这个按钮,不以一句“开始制作”允许未限定的后续付费调用。
一期先允许对话修改方案、复用已有节点编辑;不额外实现完整富文本协作编辑器。已批准方案的文字、参数或执行映射不能静默变化;新的用户输入到来时,取消尚未使用的旧执行段并评估影响。
## 7. 首页接续与画布实际操作
### 7.1 同一运行记录跨页面
新增智能创作使用同一个 `CreationRun`(设计名)。首页和画布分别投影同一需求、待交互、方案、计划和任务引用,不把两个旧聊天列表互相复制。保留旧直接生成历史与旧画布会话,不进行全面迁移。
确认后若尚无画布,按稳定操作键创建一个实际画布,关联 run,再导航到已有画布路由。URL 只携带不含凭证的关联 ID;接收方按当前账号读取并校验。画布关联成功但跳转失败时可重试打开,不重新建画布;关联失效则进入待核对状态。
首页卸载前终止其主动执行段,画布挂载后获取控制权并恢复。节点写入需要挂载后的真实画布适配器;首页不能使用虚构快照或空项目 ID 提前执行。画布未就绪时显示“正在打开画布”,不标成已创建节点。
### 7.2 第一期复用哪些节点
| 内容 | 节点映射 | 更新方式 |
| --- | --- | --- |
| 创意大纲 | 现有 Markdown/文本节点 | 确认后写入方案内容,后续修改按范围更新 |
| 分镜表(确有需要时) | 已有 Script 节点 | 经现有镜头、风格和资产校验;不为单段视频强制造一个空表格 |
| 角色、场景、道具参考 | 现有 Image 节点及业务元数据 | 生成状态与真实资源回写;优先复用用户已有可用素材 |
| 短片输出 | 现有 Video 节点 | 任务与产物引用;生成失败与资源加载失败分开 |
| 辅助说明/分组 | 现有文本/背板 | 仅在布局和理解需要时创建 |
确认后可以创建等待执行的正式媒体节点,它们不是确认前的草稿层。所有节点要有真实用途、提示词/参数和对应状态,不用多个空文本卡片冒充工作流。
布局按“方案 → 参考素材 → 视频/图片结果”安排,同一批素材可并行排列。连线必须表达真实依赖;媒体模型支持几个参考输入、各自角色是什么,以能力校验结果为准,不只在画面上把所有图连接过去。必要时用户选择参考组合或另批生成合成参考图,新增费用需授权。
每批预分配稳定节点/连接 ID,验证输入、归属和批准范围后执行。结果回执分别记录内存已变更、本地已缓存和服务端已保存。用户拖动或编辑后重新读取快照,避免旧异步结果覆盖手工内容;失败只核对和恢复缺失部分。
## 8. 生成授权与分阶段推进
短片示例采用:文本理解/问答/规划 → 本阶段节点创建 → 参考素材生成 → 必要的素材选择 → 视频生成 → 可用结果交付。已有素材可以跳过参考生成;已明确选中的素材不再强制确认一次。
### 8.1 三种确认各自负责什么
| 确认 | 批准内容 | 触发时机 |
| --- | --- | --- |
| 问答提交 | 用户的需求与选择 | 信息缺失时,不代表节点写入或费用批准 |
| 方案批准 | 指定版本下的节点和执行范围 | 创作写入前;明确的小编辑可以直接由用户指令授权 |
| 费用批准 | 模型、规格、数量、计价依据、允许费用与操作范围 | 实际收费请求前;已批准范围内不重复弹相同确认 |
文本模型本身可能收费。首个收费请求前通过发送区的计费提示或费用卡批准有限调用范围,随后持续补问和规划。首批验证建议使用可确定计价的模型;按 token 计价只有在能执行上限时显示“最多”,否则明确估算与可执行限制。自定义渠道显示外部费用归属,不承诺平台能封顶第三方账单。
媒体费用卡显示“生成哪些产物、各几项、使用什么模型、什么规格、确定金额或估算、已批准数量”。参考图与视频参数尚未共同确定时分批授权;若范围和报价都明确,可以一次批准并按依赖推进。
### 8.2 后端强校验与重复提交
复用现有报价、`CreateTask` 与账务服务,在收费 admission 增加批准范围核验与稳定执行键。当前报价没有完整快照标识,设计需保存价格版本/依据、有效期和规范化参数;报价变化重新确认。
同一 run/批准/执行项重复提交应返回原任务;同一幂等键参数不同则冲突。批次先记录获批子项,逐项消费,部分成功只补未提交项。不能依靠按钮 disabled、内存 Map、`confirmTools` 或请求中的 `source: agent` 实现此保证。
通用工具执行器必须执行相同授权检查,模型没有直接调用旧收费入口的旁路;普通手工生成也必须具有可验证的用户提交语义。账务状态不确定时复用现有核对/重试门槛,不自动新建付费任务。
## 9. 计划、日志与真实状态
短片可以采用“了解需求、确认创意、准备参考素材、确认视频参数、生成短片、整理结果”六步作为示例,但模型可以按任务简化、拆分和调整,程序负责有效状态。
| 事实 | 计划/界面表现 |
| --- | --- |
| 问答等待 | 等待你补充,停止下一阶段副作用 |
| 方案待确认 | 等待你确认方案,不显示正在创建 |
| 提交生成获后端 task ID | 排队中或生成中,不提前打勾 |
| Provider 成功但资源未就绪 | 处理中,尚不可复用 |
| 资源与节点/会话引用保存完成 | 生成步骤成功,可预览/引用 |
| 视频已成功,后续助手总结失败 | 视频继续可用,提示总结失败,可单独继续 |
| 用户暂停或某一项失败 | 保留已完成部分,标明等待/失败项 |
会话可以展示简短活动摘要,如“读取 2 份参考资料”“已创建 5 个节点”,依据真实工具回执生成,详情默认折叠。产品主流程不展示命令、原始 JSON 或技能文件路径,不伪造“执行命令”“写入文件”来模仿参考。业务计划不是模型思考过程。
## 10. 最小代码改动边界
| 现有代码/符号 | 核查结果 | 本期改动 |
| --- | --- | --- |
| `web/src/pages/create/index.tsx` | `CreationConversation` 是直接生成历史 | 增加智能创作入口,接新 run;保留当前直接生成路径 |
| `web/src/components/canvas/canvas-assistant-panel.tsx` | 循环、工具、确认与 UI 集中;首轮强制工具调用 | 按模式选择工具;保留本机路径;提取本期首页/画布真实共享的在线控制逻辑 |
| `buildToolAgentMessages` | 保留最近 8 条普通消息,未保留完整跨回合工具上下文 | 显式拼场景规则、brief、方案及当前协议段;避免仅依赖聊天截断 |
| `pendingToolContextRef`、`approveOnlineTool` | 关键续接上下文只在内存;刷新后可报上下文不完整 | 持久化待交互和可恢复工具段,支持版本化提交 |
| `executeOnlineToolCalls` | 主要按错误中断工具批次 | 增加等待结果与屏障,阻止提问/待批准之后的写调用 |
| `web/src/lib/canvas/canvas-asset-handoff.ts` | 当前只转 asset,非整会话 | 新 run 关联采用明确交接;原素材转画布功能继续复用 |
| `web/src/pages/canvas/use-canvas-agent-operations.ts` | 已有操作及生成消费;默认 `resumeAgent` 为空实现 | 接真实继续规划回调、执行代次与停止信号 |
| `web/src/services/canvas-generation-consumer.ts` | 已有生成结果消费与持久化 | 复用任务/资源回写和消费键,补 run/step 关联 |
| `backend/internal/model/models_task.go` | 现有 Session 含影视会话与 CanvasOps 语义 | 不直接把旧 Session 变成全场景状态机;新增有限创作运行记录 |
| `backend/internal/service/task_creation.go`、`logical_model_quote.go`、`task_billing.go` | 已有收费入口,缺本期批准/请求幂等合同 | 扩展准入与价格快照,复用既有任务、账务和 Provider |
建议只形成少量新增边界:`web/src/lib/creation/` 的合同/纯校验,`web/src/components/creation/` 的共用问答/方案/费用/计划卡片,本期在线控制器,以及 `web/src/services/api/` 的创作运行接口。后端按 handler → service → repository/model 扩展。控制器提取仅覆盖实际共享部分,不为本机渠道、插件平台或全部历史重写架构。
## 11. 状态与恢复合同
逻辑结构建议如下,不要求每种记录独立建表;正式表和迁移在实施任务中对照现有数据库细化。
| 记录 | 最少字段 |
| --- | --- |
| CreationRun | 用户/会话关联、可选 canvasId 与 domainProjectId、场景版本、status、revision、executionEpoch、brief、proposal、plan、预算引用 |
| Interaction | interactionId、类型、revision、原 toolCallId、完整当前工具批次/已执行结果、待答问题或方案、提交结果与状态 |
| Approval | 用户、run、方案版本、范围指纹、报价依据与有效期、额度、获批子项及消费结果 |
| OperationReceipt | 稳定操作键、画布/节点/连接 ID、stepId、任务/资源 ID、本地/服务端保存结果、待核对状态 |
会话展示消息与模型协议消息可关联但不等同。持久化只保存必要的可恢复协议段和资源引用,不保存 API Key、Cookie、临时签名链接或整份包含凭证的模型配置;恢复时重新解析合法资源与当前配置。
```mermaid
stateDiagram-v2
[*] --> idle
idle --> running: 用户提交且满足收费授权
running --> waiting_answer: 缺少必要信息
waiting_answer --> running: 同一交互提交成功
running --> waiting_proposal: 方案准备完成
waiting_proposal --> running: 修改方案
waiting_proposal --> waiting_canvas: 方案批准
waiting_canvas --> waiting_payment: 画布就绪且需要费用授权
waiting_payment --> waiting_task: 批准并提交任务
waiting_canvas --> waiting_task: 已有有效费用授权并提交
waiting_task --> running: 结果就绪后继续规划
running --> completed: 交付条件满足
running --> paused: 用户暂停或请求异常
waiting_task --> paused: 任务或回写失败
paused --> running: 对账后用户继续
completed --> [*]
```
图示为主要路径,实际取消从任意未结束状态可进入 cancelled;任务终态使用现有 Task 枚举,不被创作 run 状态替代。等待素材选择、方案或费用时均释放本轮模型请求资源。
执行规则:
1. 同一模型响应包含“提问 + 创建节点 + 生成”时,交互工具形成执行屏障。此前合法只读结果保留,后续副作用暂不执行;答复后重新评估原写调用,过期内容不得重放。协议中的每个 toolCallId 最终都要有合法结果或取消结果。
2. 刷新先恢复 run、交互、批准与任务状态,再对账画布操作。等待用户继续等待,已提交任务继续观察;不因恢复自动新建未知状态的任务。
3. 当前 run 只允许一个执行方持有控制权;切换页面/标签页增加执行代次,旧回调不能继续提交。后端验证代次与批准,浏览器在节点写入前重新检查控制权和画布版本。
4. 暂停取消新的规划/提交;上游任务取消单独展示。成功生成但回写失败只恢复回写,成功视频后总结失败只继续总结。
5. 现有连续 8 次模型循环仍是保护,等待不消耗请求次数;新回答可开新段,但整任务调用/金额/失败预算持续累计,不能无限重置。
6. 关闭页面后不发起下一阶段,已接受的后端任务可继续;再进入显示结果和“继续任务”。退出时保存失败需明确区分仅本地恢复与云端已保存。
## 12. 第一期实施顺序与完成标准
| 内部步骤 | 具体结果 | 放行条件 |
| --- | --- | --- |
| I0 能力核对 | 确定可用文本、参考图和短片视频模型;对齐场景字段、费用与 run 合同 | 15 秒/比例/参考输入可验证;不满足时明确产品差距 |
| I1 交互与持久化 | 问答、方案、计划及待交互记录,普通聊天分支 | 重复问题被过滤,刷新可继续回答,未批准不写画布 |
| I2 画布与授权 | 方案批准、稳定节点/连接、报价批准、任务幂等与回写 | 一批真实图像任务可完成、可恢复、无未授权收费 |
| I3 首页同会话接续 | 共享本期在线控制逻辑,首页到画布、单执行方控制 | 不丢答案/方案/批准,刷新/重复跳转不重复创建 |
| I4 视频与完整验收 | 短片生成、预览、局部修改/重做;营销海报回归 | 真实短片可访问,辅助编辑可用,两场景同一合同 |
I1~I3 是内部中间结果;本期发布以 I4 的完整链路通过为准。没有真实模型验证时,只能交付界面/协议能力并明确视频尚未验收,不以原型替代。
## 13. 验收脚本
### 13.1 主用例:接近第一个视频
1. 在创作首页输入“帮我搭建短剧工作流,先试做一集”,完成首段文本费用授权。
2. Agent 补问缺失条件。选择悬疑、15 秒、9:16、写实,并可自由补充故事要求;已说“试做一集”不再询问集数。
3. 提交后显示需求摘要与可阅读的创意方案,查看详情、修改一个情节;此时本轮画布节点为零。
4. 确认最新方案,进入画布,右侧保留原对话、答案和方案;看到有意义的文本/素材/视频节点及真实依赖连线。
5. 按真实报价批准参考素材任务,观察排队、生成与可用状态。使用已有素材的情况应跳过对应生成。
6. 在视频所需参考/参数明确后批准视频费用,生成期间可以展开步骤、定位节点或暂停后续执行。
7. 获得实际可播放的竖屏短片,会话和画布指向同一资源;完成后步骤收起,用户可下载。
8. 要求“只修改这个提示词”时仅更新字段;要求重做视频时重新评估费用授权,保留其他成功结果。
### 13.2 必需回归与异常
| 编号 | 场景 | 预期 |
| --- | --- | --- |
| V01 | 输入已包含题材、时长、集数、比例、风格 | 跳过这些问题;不强制六题 |
| V02 | 自定义回答同时填多个字段 | 更新多个字段,后续已解决问题被移除或标记已回答 |
| V03 | 普通咨询/明确改名 | 无制作计划,无强制大纲;不因首轮工具要求而写画布 |
| V04 | 未确认方案但查看分镜详情 | 无本轮节点、连线或隐藏草稿 |
| V05 | 方案/参数改变后点击旧确认 | 拒绝过期版本,展示有效版本 |
| V06 | 同一模型响应同时提问和写画布 | 等待屏障阻止后续副作用 |
| V07 | 问答/确认中刷新 | 恢复同一交互与协议上下文,无“工具上下文不完整” |
| V08 | 重复批准、重复跳转、双标签页 | 不重复建画布/节点/任务或扣费,过期执行方不能写入 |
| V09 | 一批两项只提交成功一项 | 返回已成功任务,仅补未提交项 |
| V10 | Provider 成功,素材回写失败 | 不显示整步成功,不重新生成,只重试资源回写 |
| V11 | 视频可用但助手总结失败 | 视频仍可预览/下载,单独继续总结 |
| V12 | 用户在等待时移动、编辑、删除节点 | 不覆盖手工内容;生成产物仍可从任务/素材找到 |
| V13 | 超预算、报价过期、账户切换、越权资源 | 阻塞对应写入/收费,不兜底默认额度或用户 |
| V14 | 不支持 15 秒或多参考输入的模型 | 提示实际能力与可选方案,不静默变更规格 |
| V15 | 两张营销海报 | 复用同一问答/方案/授权/计划,使用营销字段,不出现短剧专属必问题 |
| V16 | 深浅主题、窄屏、长选项、中文输入、键盘 | 卡片和步骤可操作,滚动不误缩放画布,提交失败保留输入 |
运行验证需使用受控 Provider 和账务测试,随后在明确获批的真实额度内完成端到端验证。只做静态检查不能宣称上述用例通过。
## 14. 评审结论与实施前事项
本设计已选定首页同会话接续、右侧助手、分页问答、可阅读方案卡、紧凑步骤条、确认后创建现有节点、分批费用批准和真实短片交付的主线。自定义节点与插件注册扩展继续保留在完整需求,排在基础闭环之后。
进入实施前需核实部署中可用的模型与计价配置、短片参考输入限制,以及实际 run/批准记录落表方式。界面原型的数值和默认选项仅用于评审;最终 UI 使用真实能力、报价和用户上下文。