15 KiB
全能创作 · 前后端契约(V1)
2026-09-02 定案。改这份文件 = 改前后端两侧,必须同步。 设计稿:
影擎 - 欢迎回来.html的omni-create-page/omni-session-page/omni-history-page。
0 · 定案(不再讨论)
| 项 | 定案 |
|---|---|
| 视频链路 | 单次出片:AI 把方案编译成一个长 prompt,一次 Seedance 2.5 调用(≤30 秒)。方案卡里的「镜头 1-4 矩阵」只是给用户看的说明,不拆成 4 个任务 |
| 会话模式 | 发起时选定 video / image,会话内不可切换。顶栏模型/分辨率/比例/时长跟着 mode 固定 |
| 追问形态 | 对话流里的卡片(进消息历史、可回看),支持 单选 / 多选 / 填空 / 选素材 |
| 策略卡 · 方案卡 | AI 真生成(新 skill omni-creative-strategy),不是模板 |
| @ 引用范围 | 商品(卖点+主图) · 角色/模特(锁脸) · 场景/资产库任意图。不含历史项目;不含本会话产物(产物靠记忆层自动带) |
| 图片会话 | 无确认闸门,聊完直接生成 |
| 视频会话 | 5 步闸门:澄清(ask_user) → 策略确认 → 方案确认 → Prompt 确认 → 参数+积分确认 → 生成 |
| 重新生成 | 对话流往下叠加新结果,旧的留在上面。历史页封面取最新一版 |
| 编排模型 | doubao-seed-2-1-pro-260628(火山直连),gpt-5.5(tokenssr)兜底 |
| 生成模型 | 图 doubao-seedream-5-0-260128 / gpt-image-2;视频 doubao-seedance-2-5-260628 |
改图 = 重新生成,不做局部编辑。带上一版当参考图重跑一次。
1 · 数据模型
CreationConversation(会话)
id uuid
team FK accounts.Team (TeamOwnedModel)
created_by FK accounts.User
title str(120) 首条用户消息前 20 字,可改名
mode "video" | "image" 发起时定死
preset str(64) 预设名,"" = 自由创作
params json {model, resolution, ratio, duration} 会话级参数
pinned_refs json [Ref] 本会话锁定的实体(每轮无条件带上 → 锁脸锁商品)
memory json {summary: str, artifacts: [ArtifactRef], turn_count: int, stage: clarify|strategy|plan|prompt|confirm|done, pending_video_prompt?: str}
status "running" | "completed" | "failed"
agent_status "idle" | "planning" | "awaiting_user" 整理方案态(与 status 正交);一团队同时只能有一个 planning
agent_started_at datetime|null planning 开始时间(超时清扫用)
last_active_at / is_deleted / purged_at
CreationMessage(消息)
id uuid
conversation FK CreationConversation
role "user" | "assistant" | "system"
kind 见 §2 消息类型
text str 纯文字内容(bubble 用)
payload json 按 kind 而定,见 §2
refs json [Ref] 本条消息引用的实体
task FK ai.AITask null 生成类消息挂的任务
seq int 会话内自增,渲染顺序
created_at
Ref(引用,结构化 —— 不许只存 "@净颜精华" 字符串)
{ "type": "product|model|character|scene|asset",
"id": "uuid",
"name": "净颜精华",
"cover": "https://..." }
后端 resolve_refs() 把 Ref → (文字事实, 参考图 URL 列表)。
参考图顺序固定:角色/模特 → 场景 → 商品(这是 @图N 的语义依据,不跟用户 @ 的先后),
最多 6 张、同图去重;真人图带 review_status/review_remote_id,视频路据此换成火山 asset://。
| type | 落到哪张表 | 参考图取哪张 |
|---|---|---|
product |
products.Product | 真实上传主图(排除 AI 生成图) |
model |
assets.Model(模特库) | 三视图优先,无则形象图 |
character |
Asset category=person | 角色定妆照 |
scene |
Asset category=scene | 场景图 |
asset |
Asset in_library=True | 该图本身 |
查不到的引用(被删/跨团队)进 missing,不抛异常 —— 由 agent 在对话里说明。
2 · 消息类型(kind)与 payload
| kind | 谁产 | payload | 对应设计稿 |
|---|---|---|---|
text |
双方 | — (用 text 字段) | .omni-chat-bubble |
elicit |
AI | {fields: [Field], submitted: bool, answers: {}};步骤确认另带 interaction:"step_confirm", step: strategy|plan|prompt |
追问卡 / 步骤确认条 |
strategy |
AI | {target, trust, belief, direction} |
.omni-strategy-card |
plan |
AI | 见下 | .omni-video-plan-card |
prompt_file |
AI | {title, body, ref_count} |
.omni-prompt-file-card |
confirm |
AI | {estimated_credits: int, label: "开始生成"} |
方案卡下的确认条 |
generating |
AI | {task_id, kind: "video"|"image"} |
.omni-process-card |
result |
AI | {task_id, assets: [{id,url,cover,type}], model, resolution, ratio} |
.omni-result-card |
error |
AI | {code, message, refunded: bool} |
气泡红态 |
Field(追问控件)
{ "key": "product",
"label": "这条视频推哪个商品?",
"type": "single|multi|text|asset",
"required": true,
"options": [{"value":"uuid","label":"净颜精华","cover":"https://..."}],
"asset_types": ["product","character"], // type=asset 时,决定弹哪个选择器
"placeholder": "比如:通勤补妆" // type=text 时
}
用户提交 → POST .../messages/ 带 {kind:"elicit_answer", reply_to: <msg_id>, answers:{...}}。
后端把 answers 塞回上下文,继续 agent 循环。
plan payload
{ "usp": "核心效果:一句话讲清为什么值得选",
"points": ["使用感受:质地与触感", "转化理由:限时优惠"],
"timeline": [{"start":0,"end":2.7,"stage":"Hook","desc":"..."}, ...],
"matrix": {"shots": 4, "rows": [{"point":"主打卖点 USP","hits":[1,3]}, ...]},
"voice_chars": [51, 60],
"ref_count": 3 }
3 · 接口
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/ai/mentions/?q=&types=product,model,character,scene,asset&limit= |
@ 检索,返回 {results:[Ref], type_labels:{}}。新增 |
| GET/POST | /api/ai/creations/ |
会话列表 / 新建(带首条消息即开聊) |
| GET/PATCH/DELETE | /api/ai/creations/{id}/ |
详情(含全部消息) / 改名 / 软删 |
| GET | /api/ai/creations/{id}/messages/?after_seq= |
增量拉消息(轮询时只补新的) |
| POST | /api/ai/creations/{id}/send/ |
发消息 → 202 {conversation_id, agent_status}(Celery 整理方案)+ poll;闸门短路径 200;kind=confirm → JSON 201;团队冲突 409 |
| POST | /api/ai/creations/{id}/cancel/ |
终止整理方案 → 200 {conversation_id, agent_status}(仅 planning);若取消时正处策略/方案/Prompt 闸门修订或推进,恢复该步 step_confirm(清 submitted)并返回 awaiting_user,否则 idle。非 planning 409。Redis 标 omni:agent:cancel:{id} + 立刻释放 omni:agent:{team_id};Celery 在工具轮间隙停跑,已落库消息保留。不影响 confirm 出片 |
| GET | /api/ai/creations/{id}/tasks/{task_id}/poll/ |
轮询生成任务(视频用) |
POST send/ 请求体:
{ "text": "帮我做一条净颜精华的口播种草",
"refs": [Ref],
"attachments": [{"asset_id":"uuid"}],
"kind": "text" | "elicit_answer" | "confirm",
"reply_to": "msg_uuid", // elicit_answer / confirm 时必填
"answers": {} // elicit_answer 时必填
}
SSE 事件(text/event-stream,每帧 data: {json}\n\n)
沿用 script_agent 的事件名,新增 3 个。DRF 必须挂 ServerSentEventRenderer,否则 406。
| event type | 载荷 | 前端动作 |
|---|---|---|
tool |
{id,label,status:running|done|error} |
工具卡(理解需求/查商品/写方案…) |
reasoning |
{text} |
思考流,纯展示 |
delta |
{text} |
逐字追加到当前气泡 |
message |
{message: CreationMessage} |
新增。一条完整消息落库了,整块渲染(策略卡/方案卡/追问卡/结果卡都走它) |
task |
{task_id, kind} |
新增。异步生成已提交,前端开始轮询 |
credits |
{estimated, balance} |
新增。确认按钮旁的积分 |
done |
{} |
收流 |
error |
{detail, error} |
已回滚积分 |
2026-09-11 更新:
kind=text|elicit_answer的整理方案循环已迁到 Celery(run_creation_agent_turn_task)。 离开/刷新页面不中断;前端按agent_status===planning轮询GET creations/{id}//messages?after_seq=。 团队级 Redis 锁omni:agent:{team_id}+ DBagent_status保证同时只有一个会话在整理方案;冲突返回 409「当前已有对话正在整理方案,请等待完成后再试」。 下方 SSE 事件表保留作stream_creation_agent兼容/单测说明;生产主路径不再对浏览器推 SSE。
铁律:视频 5–10 分钟,绝不在 SSE 里等。 工具立刻返回 task_id → 落一条 generating 消息 → 发 task 事件 → 收流。前端轮询完成后把该消息替换成 result。
4 · Agent 工具清单
| 工具 | 参数 | 复用 |
|---|---|---|
ask_user |
{fields: [Field]} |
澄清缺信息。一调即中断 |
search_library |
{query, types} |
复用 mentions 检索 |
write_strategy |
strategy payload | 出策略卡 + step_confirm,中断等确认 |
write_plan |
plan payload + video_prompt(先缓存) |
出方案卡 + step_confirm,中断;不同轮出 Prompt/积分卡 |
write_prompt |
{video_prompt} |
出 prompt_file + step_confirm,中断 |
generate_image |
{prompt, ref_asset_ids, count, ratio} |
现有 GenerateImageView 服务层 |
循环上限:单条用户消息最多 8 轮工具调用、最多 1 次计费生成(防「多做几版」烧积分)。
三条已固化进代码的铁律:
ask_user一被调用就中断循环等人回答 —— 继续跑等于自问自答。- 生成工具只提交不等待,立刻发
task事件收流,前端轮询回填。 - SSE 帧必须用
DjangoJSONEncoder—— 消息里带 UUID 和 datetime, 标准json.dumps会 TypeError 把整条流当场打断。
会话 mode 决定暴露哪些工具:图片会话只有 generate_image,视频会话只有
write_strategy / write_plan / write_prompt,互相看不见。
视频确认闸门(5 步 · 小云雀式)
聊 → ① ask_user?(缺信息则停)
→ ② write_strategy → 策略卡 + step_confirm(**停**;确认/改)
→ ③ write_plan → 方案卡 + step_confirm(**停**;确认/改)
· video_prompt 先写入 memory.pending_video_prompt
→ ④ 用户确认方案后平台(或 write_prompt)出 prompt_file + step_confirm(**停**)
→ ⑤ 用户确认 Prompt 后平台出积分 confirm 卡(模型/分辨率/比例/时长)
→ 用户点「开始生成」→ submit_free_video(不再跑编排模型)
memory.stage 取值:clarify|strategy|plan|prompt|confirm|done,用于 resume 与系统提示约束。
每一步确认用 elicit + interaction:"step_confirm" + step;前端「按这个继续」走
elicit_answer,我想改 打开输入框,打字反馈走 text(后端标 revise 并只重写该步)。
最终「开始生成」后不再跑编排模型。 video_prompt 在方案步缓存、Prompt 确认后写入
确认卡 payload,确认时照它提交 submit_free_video,返回同步 JSON。
积分确认卡是一次性的:submitted 置位后重复提交返回 409。
但提交失败要把闸门放回去(submitted 复位),否则积分不足改完也点不了了。
顶栏的模型 label(「Seedance 2.5」)要翻成火山真名才能提交;duration 从「15 秒」里
抠数字,「智能时长」回落 15,超过 30 秒夹住(火山单次上限)。
5 · 记忆三层(会话内 SQL 即可,不用向量库)
- 滚动摘要
memory.summary—— 消息数超过COMPRESS_AFTER_MESSAGES(24)时, 把最老的一批压成一段,只留最近KEEP_RECENT_MESSAGES(12)条原文喂模型。COMPRESS_MIN_BATCH(8)是关键:没有它的话过了阈值以后每一轮都要多花一次 模型调用去重压那么两三句话,长会话成本翻倍。压缩失败静默跳过,摘要是锦上添花。 - 实体锁定
pinned_refs—— 本会话引用过的商品/角色/场景 + 参考图,每轮无条件带上 - 产物索引
memory.artifacts——[{msg_id, asset_id, prompt, kind}],让"背景换成夜景"能定位到上一版
6 · 实施顺序
- 阶段 0 · 本契约
- 阶段 1 ·
CreationConversation/CreationMessage+ 迁移 0034 + 会话 CRUD - 阶段 2 ·
/mentions/检索 +resolve_refs()→apps/ai/mentions.py - 阶段 3 · agent 循环 + SSE →
apps/ai/creation_agent.py,已接ask_user/search_library/generate_image - 阶段 4 · 视频链路:
write_strategy/write_plan+ 确认闸门 +submit_free_video - 阶段 5 · 记忆压缩(
compress_memory)+ 历史页接后端 - 阶段 6 · 预设拍法 →
apps/ai/creation_presets.py(14 条,与前端卡片逐字对应)
前端 omni-session.tsx(对话页)已港,设计稿 omni-session-page 逐行照抄。
港页面时踩到的三个坑(以后港别的页面同样要查):
- 设计稿用
--black/--text,design-restraint.css里没有 →background: var(--black)解析失败变透明,白字白底看不见。已在 design-restraint.css 加别名指向--accent-black。 首页 omni-create 之前就带着这个 bug。 - 设计稿的
.omni-chat-row.user撞上design-restraint.css里的裸.user全局类 (侧栏用户胶囊:999 圆角 + 灰底 + space-between),整套样式被传染。改用.is-user。 - 设计稿靠
id="omniSessionPrompt"给输入框上样式;React 不写 id 就没样式。已补上 id (与omni-create.tsx的#omniStartPrompt一致)。
另:这一版布局是整页滚 + 输入框 position: sticky,feed 没有自己的滚动条 ——
对 feed 调 scrollTo 是空操作,自动滚到底必须滚 window。
7 · 前端稳定性(2026-09-02 修)
对话页「条来跳去」的四个根因,改别的页面时同样要查:
onNotify是 App 里的内联箭头,每次 App 重渲染都是新身份。 放进useEffect/useCallback依赖 → 整条会话被反复重拉、消息数组被反复替换。 这是抖动的主因。 解法:存进 ref,回调身份稳定、值永远最新。- 逐字
scrollIntoView({behavior:"smooth"})—— 每个流式字符发一次平滑滚动, 动画互相打架。解法:流式期间用instant+ rAF 合帧;新消息落地才用 smooth; 且用户手动上滑看历史时不跟(距底 <120px 才跟)。 - 轮询整个替换 messages 数组 → 所有卡片重渲染。解法:按 id + payload 比对, 只替换真变了的那几条,其余保持引用不变让 React 跳过。
- 流式临时气泡带入场动画 → 逐字重渲染时一直在闪。解法:
.is-live { animation: none }。
另修:handleSend 原来先清输入框再判 streaming,流式期间敲回车会把打好的内容
清掉但消息没发出去。顺序反过来。