Files
yingqing/全能创作-契约-2026-09-02.md
T
2026-09-14 11:47:49 +08:00

15 KiB

全能创作 · 前后端契约(V1)

2026-09-02 定案。改这份文件 = 改前后端两侧,必须同步。 设计稿:影擎 - 欢迎回来.htmlomni-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: idle}(仅 planning);非 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} + DB agent_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 次计费生成(防「多做几版」烧积分)。

三条已固化进代码的铁律:

  1. ask_user 一被调用就中断循环等人回答 —— 继续跑等于自问自答。
  2. 生成工具只提交不等待,立刻发 task 事件收流,前端轮询回填。
  3. 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 即可,不用向量库)

  1. 滚动摘要 memory.summary —— 消息数超过 COMPRESS_AFTER_MESSAGES(24)时, 把最老的一批压成一段,只留最近 KEEP_RECENT_MESSAGES(12)条原文喂模型。 COMPRESS_MIN_BATCH(8)是关键:没有它的话过了阈值以后每一轮都要多花一次 模型调用去重压那么两三句话,长会话成本翻倍。压缩失败静默跳过,摘要是锦上添花。
  2. 实体锁定 pinned_refs —— 本会话引用过的商品/角色/场景 + 参考图,每轮无条件带上
  3. 产物索引 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 逐行照抄。

港页面时踩到的三个坑(以后港别的页面同样要查):

  1. 设计稿用 --black / --text,design-restraint.css 里没有 → background: var(--black) 解析失败变透明,白字白底看不见。已在 design-restraint.css 加别名指向 --accent-black首页 omni-create 之前就带着这个 bug。
  2. 设计稿的 .omni-chat-row.user 撞上 design-restraint.css 里的.user 全局类 (侧栏用户胶囊:999 圆角 + 灰底 + space-between),整套样式被传染。改用 .is-user
  3. 设计稿靠 id="omniSessionPrompt" 给输入框上样式;React 不写 id 就没样式。已补上 id (与 omni-create.tsx#omniStartPrompt 一致)。

另:这一版布局是整页滚 + 输入框 position: sticky,feed 没有自己的滚动条 —— 对 feed 调 scrollTo 是空操作,自动滚到底必须滚 window。


7 · 前端稳定性(2026-09-02 修)

对话页「条来跳去」的四个根因,改别的页面时同样要查:

  1. onNotify 是 App 里的内联箭头,每次 App 重渲染都是新身份。 放进 useEffect / useCallback 依赖 → 整条会话被反复重拉、消息数组被反复替换。 这是抖动的主因。 解法:存进 ref,回调身份稳定、值永远最新。
  2. 逐字 scrollIntoView({behavior:"smooth"}) —— 每个流式字符发一次平滑滚动, 动画互相打架。解法:流式期间用 instant + rAF 合帧;新消息落地才用 smooth; 且用户手动上滑看历史时不跟(距底 <120px 才跟)。
  3. 轮询整个替换 messages 数组 → 所有卡片重渲染。解法:按 id + payload 比对, 只替换真变了的那几条,其余保持引用不变让 React 跳过。
  4. 流式临时气泡带入场动画 → 逐字重渲染时一直在闪。解法:.is-live { animation: none }

另修:handleSend 原来先清输入框再判 streaming,流式期间敲回车会把打好的内容 清掉但消息没发出去。顺序反过来。