术语统一:视觉脚本→视觉短片、预设钩子→开场钩子(技能层全量)+ clarify v30 维度口径衔接

- P0: D·视觉脚本→D·视觉短片 20 处/5 文件(SKILL.md S9、6_框架映射表、9_脚本、创作流程规范、07_格式指南);F07 纯视觉短片保留
- P2a: 预设钩子→开场钩子 10 处/3 文件(5_选题、7_大纲、04_爆款开场口径说明)
- P1a/P1b: clarify.md 新增第 7 条维度口径衔接(内容形式4选→S6细化、故事框架倾向词→S6定型、开场钩子同口径) CHANGELOG v30
- SKILL.md frontmatter updated_at=2026-09-07; references-add/变更日志.md 归档 09-07 条目
- 需求框 v29(内容形式置首/Vlog去人物采访/叙事结构→故事框架) 已于上轮提交范围
This commit is contained in:
maogeigei committed 2026-09-07 15:03:21 +08:00
1 parent 96caa02b52
commit d8e83ad368
15 files changed
+376 -172

No files matched your search

+3 -3
View File
@@ -8,8 +8,8 @@ description: >
触发场景:写脚本、创作短视频、生成脚本、拆解对标视频、分析爆款视频、
诊断脚本质量、设计账号人设、选题策划、生成分镜、广告植入设计、生成创作回顾。
version: "1.0"
updated_at: "2026-09-04"
last_change: "09-04 技能树体量优化(R73/R74):frontmatter 历史明细迁 references-add/变更日志.md、流程总览压缩为行式 Gate 链、新增阅读分层说明;本技能近期实质变更(09-04 子技能索引登记+外部依赖路径修正、09-03 更名短视频工作台、09-02 AI任务入口契约+执行铁律)完整历史见 references-add/变更日志.md;browser-harness 2 个 14B 占位 SKILL.md 删除(防宿主误注册,外部依赖段补维护提示)"
updated_at: "2026-09-07"
last_change: "09-07 术语统一:D·视觉脚本→D·视觉短片、预设钩子→开场钩子(S5/S7 字段全量对齐,明细见 references-add/变更日志.md)"
deployment: "source"
agent_created: true
---
@@ -273,7 +273,7 @@ S1 意图识别 → S2 需求完善 → [G0 分析确认]
### 步骤 9:生成短视频脚本(情绪演绎层)
**目的:** 基于 S6 情绪规划(场次结构表+情绪设计卡)和 S7 结构骨架(模板+分段),按叙事形态选择对应脚本格式(A·场次叙事/B·Vlog双轨/C·口播要点/D·视觉脚本,参见知识库 `03_框架节奏/07_脚本格式选择指南.md`),具象化为完整脚本——台词、动作、多维度情绪表达,不做任何情绪维度的重新规划。
**目的:** 基于 S6 情绪规划(场次结构表+情绪设计卡)和 S7 结构骨架(模板+分段),按叙事形态选择对应脚本格式(A·场次叙事/B·Vlog双轨/C·口播要点/D·视觉短片,参见知识库 `03_框架节奏/07_脚本格式选择指南.md`),具象化为完整脚本——台词、动作、多维度情绪表达,不做任何情绪维度的重新规划。
**输出:** 脚本概览 + 对应格式脚本(逐场故事演绎+情绪表达+口播台词)+ 广告植入对齐检查 + 自检清单。
@@ -101,7 +101,7 @@
请帮我把抖音账号 {账号名称「x」, 账号地址「y」} 添加:通过抖音网页获取账号信息与视频列表,写入账号列表并保存到本地
```
### 9 需求打磨 clarify(09-02 弹窗内对话式 · 09-07 模板文件化)
### 9 需求打磨 clarify(09-02 弹窗内对话式 · 09-07 模板文件化 · v29 八维编导名)
> 账号列表「AI创作」弹窗底部的对话打磨,非 AI 会话任务(不走 /api/run),直调 Dify chat-messages。
@@ -113,14 +113,14 @@
|----|------|------|---------|
| 正文 | 无 | 对用户说的话(150 字内);追问时问题**独占一段**;收尾时提示「可直接点击右下角 ✓ 提交创作」 | 渲染为 AI 气泡 |
| 候选 | `==OPTS==` | 每行一个候选,格式「骨架词——具体展开」(≤45 字、无编号);结合本条创意/人设落地,禁干巴术语 | 解析为气泡下内嵌按钮行,点选=以整行文本发一轮 |
| 需求 | `==REQ==` | 5 行清单:一句话创意/创新·爆点/痛点·爽点/叙事结构/其他约束(空项留空) | 解析回填需求框字段 |
| 需求 | `==REQ==` | 8 行清单:内容形式/一句话故事/开场钩子/故事爆点/故事爽点/故事框架/人物关系/特别要求(空项留空) | 解析回填需求框字段 |
**System Prompt 已抽为文件**:`mcn-work-shop/prompts/clarify.md`(==PROMPT== 与 ==CHANGELOG== 之间为正文,头尾说明不发模型)。**迭代 prompt 只改该文件**,server.js 启动按 mtime 缓存读取、占位符注入(`{{accountName}}/{{personaText}}/{{contentText}}/{{attachText}}/{{reqMdText}}/{{histText}}/{{turnText}}`),不再改 server.js 字符串。
**关键规则(clarify.md 内固化)**:
- 候选骨架受控词表:叙事结构(钩子前置/悬念反转/递进铺垫/先抑后扬)、内容形式(剧情/Vlog/口播/**视觉短片**)、剧情人物关系(一次一层);每项必须结合本条创意给「具体形态」
- 收尾门槛:创意/爆点/爽点/叙事结构 **至少 3 项明确**才可提示提交,否则继续补缺口
- 前端草稿缓存:`localStorage aicreate_draft_{账号}`(w/convo/attach,每轮回复后存、提交后删);点遮罩外不关闭,仅 X/「重置」可关
- 候选骨架受控词表:内容形式(剧情/Vlog/口播/**视觉短片**)、故事框架(钩子前置/悬念反转/递进铺垫/先抑后扬)、开场钩子(悬念提问/反差画面/直击痛点/身份共鸣/利益承诺)、剧情人物关系(一次一层);每项必须结合本条创意给「具体形态」
- 收尾门槛(09-07 v29):内容形式/一句话故事/开场钩子/故事爆点/故事爽点/故事框架 **核心 6 项至少 4 项明确**才可提示提交;且内容形式=剧情时「人物关系」也须明确。未达门槛继续补最关键缺口
- 前端草稿缓存:`localStorage aicreate_draft_{账号}`(w/convo/attach,每轮回复后存、提交后删);点遮罩外不关闭,仅 X/「重置」可关;旧草稿缺 v28 新字段(hook/ftype/cast)自动为空,兼容不报错
## 关键细节
@@ -1,6 +1,6 @@
# 「对话式选题完善」用户模拟使用场景案例
> 版本:09-07 OPTS 候选 chips 版(视觉短片/口播等编导词定稿)|入口:账号列表 → 任一行「AI创作」→ 弹窗底部输入框
> 版本:09-07 v29 八维编导名版(内容形式/一句话故事/开场钩子/故事爆点/故事爽点/故事框架/人物关系/特别要求)|入口:账号列表 → 任一行「AI创作」→ 弹窗底部输入框
> 一句话:用户在弹窗里像聊天一样逐步说出想法,AI 每轮把缺口整理成**可点选候选**,点一下即回填,补齐后一键提交创作。
---
@@ -9,14 +9,17 @@
| # | 场景 | 模拟目标 | 关键验证点 |
|---|------|---------|-----------|
| 1 | 从零想法 → 提交创作 | 主流程闭环 | 5 字段逐步点亮、叙事结构 chips 点选、提交按钮 |
| 2 | 想拍"视觉短片" | 内容形式四选项分支 | D·视觉短片词条出现、点击后画面驱动进需求 |
| 1 | 从零想法 → 提交创作 | 主流程闭环 | 8 字段逐步点亮、故事框架/开场钩子 chips 点选、提交按钮 |
| 2 | 想拍"视觉短片" | 内容形式四选项分支 | 视觉短片词条出现、点击后画面驱动进内容形式行 |
| 3 | 想做剧情演绎但人物没想好 | 人物/关系分支追问 | AI 追问人物关系、chips 提供关系候选 |
| 4 | 想法已经很完整 | 快速收尾 | AI 直接总结、提示提交、不再弹 chips |
| 5 | 上传需求文档起步 | 附件 + 对话结合 | 附件被通读、诉求进清单 |
> 术语对照(文档内统一用编导词):
> 内容形式 = 剧情(场次叙事)/ Vlog 纪实·人物采访 / 口播(干货·教程·盘点) / **视觉短片**(画面+音乐驱动,测评种草或纯氛围)
> 术语对照(文档内统一用 v29 编导词):
> 内容形式 = 剧情(场次叙事)/ Vlog / 口播(干货·教程·盘点) / **视觉短片**(画面+音乐驱动,测评种草或纯氛围) → 独立字段「内容形式」,**置需求框首位**
> 故事框架 = 钩子前置 / 悬念反转 / 递进铺垫 / 先抑后扬 → 独立字段「故事框架」
> 开场钩子 = 悬念提问 / 反差画面 / 直击痛点 / 身份共鸣 / 利益承诺 → 独立字段「开场钩子」(前 3 秒留人)
> 收尾门槛 = 核心 6 项(内容形式/一句话故事/开场钩子/故事爆点/故事爽点/故事框架)取 4;内容形式=剧情时「人物关系」也须明确
---
@@ -27,29 +30,29 @@
**① 用户输入:**
> 想拍一条亲子互动的搞笑短剧
**AI 行为:** 吸收"亲子 + 搞笑 + 短剧"→ 填入一句话创意;判断内容形式可定为剧情(场次叙事);发现叙事结构缺口 → 追问并给 4 个候选 chips。
**AI 行为:** 吸收"亲子 + 搞笑 + 短剧"→ 内容形式可定为剧情(场次叙事)写入内容形式行(首位) → 判断一句话故事可推断"天使宝宝 × 打工人妈妈 反差互动"填入 → 发现故事框架缺口 → 追问并给 4 个候选 chips。
**AI 气泡(实测同款):**
> 没问题!"天使宝宝 × 打工人妈妈"的日常反差互动,精准戳中宝妈笑点。既然定了剧情短剧,这条视频您更倾向哪种叙事结构?
> 没问题!"天使宝宝 × 打工人妈妈"的日常反差互动,精准戳中宝妈笑点。既然定了剧情短剧,这条视频您更倾向哪种故事框架?
>
> 〔钩子前置〕〔悬念反转〕〔递进铺垫〕〔先抑后扬〕
**需求框变化:** `2/5 已填`(一句话创意 ✅、叙事结构=剧情·场次叙事 ✅)
**需求框变化:** `2/8 已填`(内容形式=剧情·场次叙事 ✅、一句话故事 ✅)
**② 用户点击:** 〔钩子前置〕
**用户侧效果:** 点击文本以"钩子前置"作为用户输入发一轮,chips 即时收起(防重复点)。
**AI 行为(下一轮):** 吸收选择 → 叙事结构行更新为"钩子前置(开场即抛钩子)" → 发现爆点/痛点仍有缺口 → 继续追问并给候选。
**AI 行为(下一轮):** 吸收选择 → 故事框架行更新为"钩子前置(开场即抛钩子)" → 发现开场钩子缺口(v28 新增追问层) → 追问开场钩子并给候选(悬念提问/反差画面/直击痛点/身份共鸣/利益承诺 × 亲子落点)。
**需求框变化:** `3/5 已填`
**需求框变化:** `3/8 已填`
**③ 用户继续:** (示例)点击爆点候选或直接输入 "反转的点是爸爸藏私房钱被妈妈抓包"。
**③ 用户点击:** 〔反差画面——妈妈躲门后偷看,爸爸独自带娃鸡飞狗跳〕
**AI 行为:** 吸收 → 补爆点 → 若痛点/爽点仍空 → 依账号设定(妈妈带娃痛点)给爽点候选。
**AI 行为:** 吸收 → 开场钩子行写入"反差画面:妈妈躲门后偷看、爸爸带娃鸡飞狗跳(前 3 秒)" → 故事爆点/故事爽点若仍空 → 依账号设定(妈妈带娃痛点)给爽点候选。
**④ 直到需求框 ≥4/5,AI 回复:**
> 需求已齐:剧情短剧 × 钩子前置,笑点结构完整。可以直接点右下角 ✓ 提交创作了!
**④ 直到核心 6 项 ≥4 项明确(内容形式/一句话故事/故事框架/开场钩子已满 4),AI 回复:**
> 需求已齐:剧情短剧 × 钩子前置 × 反差开场,笑点结构完整。可以直接点右下角 ✓ 提交创作了!
**⑤ 用户点击** ✓ 提交创作 → 确认弹窗展示需求清单 → 确认 → 创建 S1-S11 全流程创作任务。
@@ -73,17 +76,17 @@
**AI 气泡(预期):**
> 明白了,画面优先、台词极少——这属于**视觉短片**。确认一下内容形式:
>
> 〔剧情(多角色对白)〕〔Vlog 纪实·人物采访〕〔口播(干货·教程·盘点)〕〔视觉短片(画面+音乐驱动)〕
> 〔剧情(多角色对白)〕〔Vlog〕〔口播(干货·教程·盘点)〕〔视觉短片(画面+音乐驱动)〕
**② 用户点击:** 〔视觉短片(画面+音乐驱动)〕
**AI 行为(下一轮):** 叙事结构行写入"视觉短片(画面驱动、配乐承载情绪)" → 追问音乐氛围倾向/时长 → 给候选(如〔治愈氛围〕〔氛围卡点〕)。
**AI 行为(下一轮):** 内容形式行写入"视觉短片(画面驱动、配乐承载情绪)" → 追问故事框架/开场钩子(氛围类常用递进铺垫或氛围卡点开场) → 给候选(如〔治愈氛围〕〔氛围卡点〕)。
**需求框变化:** 叙事结构字段呈现 **视觉短片** 字样 ✅(验证本轮术语定稿生效)
**需求框变化:** 内容形式字段呈现 **视觉短片** 字样 ✅(验证 v28 内容形式独立字段生效)
**验收清单:**
- [ ] 选项文案是否为「视觉短片」而非旧词「视觉脚本」
- [ ] 点击后叙事结构行是否注明"画面驱动"
- [ ] 点击后**内容形式**行是否写入"视觉短片"并注明"画面驱动"(勿再落入结构类字段)
---
@@ -103,14 +106,14 @@
**② 用户点击:** 〔闺蜜反目(多年挚友)〕
**AI 行为(下一轮):** 把人物关系吸收进创意行(如"十年闺蜜因…反目")→ 继续追问决裂导火索或爆点 → 给候选。
**AI 行为(下一轮):** 把人物关系吸收进**人物关系行**(如"闺蜜反目(多年挚友):十年闺蜜因…反目")→ 继续追问决裂导火索或故事爆点 → 给候选。
**需求框变化:** 一句话创意行包含人物关系信息 ✅
**需求框变化:** 人物关系行包含"闺蜜反目(多年挚友)"信息 ✅(v28 独立字段,不再挤在创意行)
**验收清单:**
- [ ] 剧情向输入是否触发"人物/关系"追问(此前 flova 缺口)
- [ ] 剧情向输入是否触发"人物关系"追问(此前 flova 缺口)
- [ ] 追问是否一次一层、不堆问题
- [ ] 点击关系候选后是否写入创意行
- [ ] 点击关系候选后是否写入**人物关系行**(而非一句话故事行)
---
@@ -126,7 +129,7 @@
**AI 气泡(预期):**
> 需求已经很完整:美妆测评、30 秒、妆前妆后反差开场、口播节奏快、结尾挂优惠。直接点 ✓ 提交创作即可。
**需求框变化:** `5/5 已填`
**需求框变化:** 核心 6 项已明确 4 项(内容形式=口播、一句话故事=美妆测评种草、开场钩子=妆前妆后反差、故事框架=反差直给)→ AI 不再追问 ✅
**验收清单:**
- [ ] 信息齐备时 AI 是否**不**输出候选(chips 不出现 = 正常)
@@ -149,7 +152,7 @@
>
> 〔营养早餐的一天〕〔挑食宝宝秒喝光〕〔妈妈偷喝被发现〕
**需求框变化:** 其他约束行出现"儿童牛奶 · 早餐场景 · 禁功效词" ✅
**需求框变化:** 特别要求行出现"儿童牛奶 · 早餐场景 · 禁功效词" ✅
**验收清单:**
- [ ] 附件诉求是否进入需求框对应字段
@@ -27,24 +27,35 @@
1. 理解本轮输入:可能是新想法、对上一问的回答、或想修改某一维度。将信息吸收进需求清单,可改写得更专业准确,但保留用户原意与账号结构性符号。
2. 若关键维度仍有缺口且本轮输入不足以补齐:只追问 1 个问题,不要一次抛出多个,也不要重复已明确的维度。问题必须单独另起一段(前后空行),便于界面突出显示;且必须给出可点选的候选(==OPTS==),供达人一键选用,严禁让达人自己空想选项。
3. 候选生成规则:选项骨架须受控(来自以下词表),但每项必须结合本条视频的创意/人设给出「落到这条视频里的具体形态」——即选项 = 骨架词 + 一句话怎么拍/什么内容,让达人一眼知道选了会得到什么,而不是干巴巴的术语。
- 叙事结构未定 → 骨架:钩子前置 / 悬念反转 / 递进铺垫 / 先抑后扬,每项后缀具体展开(如:钩子前置——开场 3 秒就用「男保姆被娃折腾」制造笑点;悬念反转——结尾突然揭晓妈妈一直在偷看)。
- 内容形式(叙事形态)未定 → 骨架:剧情(场次叙事,多角色对白)/ Vlog 纪实·人物采访(第一视角)/ 口播(干货·教程·盘点)/ 视觉短片(画面+音乐驱动,测评种草或纯氛围,台词极少),后缀结合本条内容给画面特征。
- 达人倾向剧情演绎但人物与关系未明 → 骨架:主要人物身份 / 人物间关系(如:父子 / 夫妻 / 同事 / 陌生冲突),每项后缀结合已有创意给关系落点,一次只问一个层面。
4. 收尾门槛:仅当以下 4 项中至少 3 项已明确时才可总结收尾——一句话创意、创新/爆点、痛点/爽点、叙事结构(含内容形式)。未达门槛前,即使达人本轮回答已吸收,也须继续补齐最关键的缺口(把缺口本身作为问题+候选追问);已明确项可从达人输入或账号设定合理推断,但推断项须在回复中点明「我按账号设定帮你定了XX,如不对可点重置改」。
- 内容形式未定(最先确认,放第一位)→ 骨架:剧情(场次叙事,多角色对白)/ Vlog(第一视角记录)/ 口播(干货·教程·盘点)/ 视觉短片(画面+音乐驱动,测评种草或纯氛围,台词极少),后缀结合本条内容给画面特征 → 写入「内容形式」行。
- 故事框架未定(内容形式已明确时追问)→ 骨架:钩子前置 / 悬念反转 / 递进铺垫 / 先抑后扬,每项后缀具体展开(如:钩子前置——开场 3 秒就用「男保姆被娃折腾」制造笑点;悬念反转——结尾突然揭晓妈妈一直在偷看)→ 写入「故事框架」行。
- 开场钩子未定(内容形式与框架已有时追问)→ 骨架:悬念提问 / 反差画面 / 直击痛点 / 身份共鸣 / 利益承诺(五选一),后缀结合本条内容给前 3 秒的具体画面或台词 → 写入「开场钩子」行。
- 达人倾向剧情演绎但人物与关系未明 → 骨架:主要人物身份 / 人物间关系(如:父子 / 夫妻 / 同事 / 陌生冲突),每项后缀结合已有创意给关系落点,一次只问一个层面 → 写入「人物关系」行。
4. 收尾门槛:仅当以下 6 项中至少 4 项已明确时才可总结收尾——内容形式、一句话故事、开场钩子、故事爆点、故事爽点、故事框架。且若内容形式=剧情(场次叙事),「人物关系」也须明确才可提示提交。未达门槛前,即使达人本轮回答已吸收,也须继续补齐最关键的缺口(把缺口本身作为问题+候选追问);已明确项可从达人输入或账号设定合理推断,但推断项须在回复中点明「我按账号设定帮你定了XX,如不对可点重置改」。
5. 输出三段结构,顺序固定:
第一段:对用户说的话(150 字以内)。需追问时:先一句确认本轮吸收的内容,空一行后单独输出问题(问题独占一段,不要与解释挤在一行);无需追问时:总结已明确项并提示「可直接点击右下角 ✓ 提交创作」;
若本轮提供了候选:另起一行单独输出 ==OPTS==,每行一个候选,格式为「骨架词——具体展开」(≤45 字、不含编号前缀),达人点选后整行即作为其回复;若未提供候选则不输出该段;
最后另起一行单独输出 ==REQ==,接着输出 5 行需求清单(仍未确定的项留空值,严禁编造;情节/结尾/时长不由本清单承载,由创作阶段按人设与选题自决;共鸣/共情亦不收集,创作时由 AI 依痛点 / 爽点自然发挥),模板:
- 一句话创意:
- 创新 / 爆点:
- 痛点 / 爽点:
- 叙事结构:
- 其他约束:
达人点击候选后,把该选择吸收进对应字段(如内容形式选「视觉短片」→ 写入叙事结构行并注明画面驱动;人物选择 → 写入一句话创意行的事件或人物信息)。
6. 需求框只收集本条视频的选题相关信息;账号级设定(情绪基调 / 目标受众 / 结构性符号 / 广告植入等)已由账号设定自动带入创作,不要向达人重复询问这些维度——仅当达人主动提出与账号设定不同的要求时,将其记入「其他约束」。
最后另起一行单独输出 ==REQ==,接着输出 8 行需求清单(仍未确定的项留空值,严禁编造;情节/结尾/时长不由本清单承载,由创作阶段按人设与选题自决),模板(顺序固定,内容形式在第一行):
- 内容形式:
- 一句话故事:
- 开场钩子:
- 故事爆点:
- 故事爽点:
- 故事框架:
- 人物关系:
- 特别要求:
达人点击候选后,把该选择吸收进对应字段(如内容形式选「视觉短片」→ 写入内容形式行并注明画面驱动;故事框架选「钩子前置」→ 写入故事框架行并给开场画面;人物选择 → 写入人物关系行;钩子候选 → 写入开场钩子行)。
6. 需求框只收集本条视频的选题相关信息;账号级设定(情绪基调 / 目标受众 / 结构性符号 / 广告植入等)已由账号设定自动带入创作,不要向达人重复询问这些维度——仅当达人主动提出与账号设定不同的要求时,将其记入「特别要求」。
7. 维度口径衔接(需求框词表是达人口径,落到创作由技能流程定型,本环节只记不细问):
- 内容形式:达人只需粗选 剧情 / Vlog / 口播 / 视觉短片;落到创作时由 S6 按映射细化(口播→教程 / 知识分享 / 盘点,视觉短片→测评种草 / 沉浸式),不要在本环节向达人追问更细品类——达人主动给出的品类细节照常记入该行。
- 故事框架:候选词(钩子前置 / 悬念反转 / 递进铺垫 / 先抑后扬)是达人倾向表达,仅记录倾向本身;具体框架由 S6 用叙事形态(F01-F08)+ 结构模板(三段式/五段式)落地定型,本环节不展开。
- 开场钩子:与 S5 选题方案的开场钩子同口径(10 种开场方式,抓眼元素前 3 秒内出现、开场段总时长 3-7 秒铁律)。
==CHANGELOG==
## 变更记录(append-only,最新在上)
- 09-07 v30:新增行为要求第 7 条「维度口径衔接」——内容形式 4 项为粗选(落创作由 S6 细化:口播→教程/知识分享/盘点,视觉短片→测评种草/沉浸式);故事框架四词=达人倾向词(由 S6 叙事形态 F01-F08 + 结构模板定型);开场钩子与 S5 选题方案同口径(10 种开场方式 + 3-7s 铁律)。随技能层术语统一(视觉短片/开场钩子改名)落地。
- 09-07 v29:内容形式置首位(最先确认的维度);Vlog 项去除「人物采访」(Vlog 就是 Vlog);叙事结构 → 故事框架(==REQ== 模板第 6 行、门槛枚举、候选规则同步更名;key 与草稿兼容不变)。
- 09-07 v28:需求框升级 8 维编导名(与前端 REQ_FIELDS 同步)——一句话故事/开场钩子/故事爆点/故事爽点/内容形式/叙事结构/人物关系/特别要求;内容形式与叙事结构拆为两行独立收集;开场钩子新增追问层(悬念提问/反差画面/直击痛点/身份共鸣/利益承诺 五选一);收尾门槛由「4 项取 3」升级为「核心 6 项取 4」(一句话故事/开场钩子/故事爆点/故事爽点/内容形式/叙事结构),且内容形式=剧情时人物关系也须明确才可提示提交;==REQ== 模板 5 行 → 8 行。
- 09-07 v26:从 server.js 内嵌字符串抽取为独立模板(prompts/clarify.md)。规则内容与 09-07 四项修复(收尾门槛 4 项至少 3 项、候选具体展开「骨架——展开」、追问问题独占一段、必须出 ==OPTS==)一致,无行为变更。
@@ -200,14 +200,34 @@ function bindBatchBar(container, opts) {
// 需求单字段顺序:弹窗需求框展示、AI 输出模板、创作 prompt 共用(值空 = 待 AI 按账号设定与选题自决)
// 09-02 v10:需求框定位=选题补充——只留本条视频选题侧字段 + 兜底「其他」;账号设定类(情绪基调/受众/结构性符号/广告植入)由人设卡提供,不在此重复收集
// 09-02 v11:dur 时长目标 移出需求栏 → 需求栏下方「功能选项」下拉(30/60/120/240/自定义),值由 w.dur 单独承载
// 09-02 v17:选题落点要素(痛点/爽点、共鸣/共情、爆点)加入需求框——创作流程需要但不是必填,空值由 AI 按账号设定与选题自决
// 09-02 v17:选题落点要素加入需求框——创作流程需要但不是必填,空值由 AI 按账号设定与选题自决
// 09-02 v18:叙事形态→叙事结构;删核心情节/结尾落点(情节与结尾由 AI 创作阶段自决,无需用户填写)
// 09-02 v19:爆点→创新/爆点,字段序调整至「一句话创意」之后
// 09-02 v20:删共鸣/共情(由 AI 按痛点/爽点自然发挥,无需用户单独填写)
// 09-02 v20:删共鸣/共情(由 AI 按爽点自然发挥,无需用户单独填写)
// 09-07 v28:8 维编导名定稿(用户确认)——idea 一句话故事 / hook 开场钩子 / blow 故事爆点 / pain 故事爽点 /
// ftype 内容形式 / form 叙事结构(不再含内容形式)/ cast 人物关系(剧情向)/ note 特别要求;
// 收尾门槛=核心 6 项取 4(clarify.md 同步);旧草稿缓存缺新字段自动为空,不报错
// 09-07 v29:内容形式(ftype)置首位;Vlog 项去掉「人物采访」(Vlog 就是 Vlog);叙事结构 → 故事框架(key form 不变,草稿兼容)
const REQ_FIELDS = [
['idea', '一句话创意'], ['blow', '创新 / 爆点'], ['pain', '痛点 / 爽点'],
['form', '叙事结构'], ['note', '其他约束'],
['ftype', '内容形式'],
['idea', '一句话故事'],
['hook', '开场钩子'],
['blow', '故事爆点'],
['pain', '故事爽点'],
['form', '故事框架'],
['cast', '人物关系'],
['note', '特别要求'],
];
// 字段 → 悬停提示(renderSheet 用;用编导语言解释落到本条视频要定什么,hover 标题可见)
const REQ_FIELD_HINTS = {
ftype: '内容形式:剧情 / Vlog / 口播 / 视觉短片(画面+音乐驱动)',
idea: '主角 + 事件 + 看点,一句话说清这条视频讲什么',
hook: '开场前 3 秒靠什么留人:悬念提问 / 反差画面 / 直击痛点 / 身份共鸣 / 利益承诺(S5 开场钩子场,3-7s 铁律)',
blow: '全片最炸的那一下 / 记忆点 / 反转高潮,观众看完记住什么',
pain: '观众为什么代入、看了爽在哪 / 被治愈在哪(目标体感:爽感 / 治愈感 / 获得感)',
form: '故事框架走向:钩子前置 / 悬念反转 / 递进铺垫 / 先抑后扬',
cast: '剧情向必补:主要人物是谁、彼此什么关系(父子 / 夫妻 / 同事 / 陌生冲突)',
note: '禁忌、广告植入、参考资料、与账号设定的差异要求等',
};
// 需求 md 行 → 向导字段映射(parseReqSheet 用)
const REQ_LABEL_TO_KEY = Object.fromEntries(REQ_FIELDS.map(([k, label]) => [label, k]));
// 需求单 markdown 解析 → 状态对象(仅解析已知字段,空值/待填写忽略)
@@ -246,7 +266,8 @@ function buildCreatePrompt(accountName, w, personaBlock, hasPersona, attachText)
}
/* AI创作 弹窗(09-02 v2:需求框 + 输入框 对话式打磨)——
顶部=创作需求(5 行选题字段:创意/创新爆点/痛点爽点/叙事结构/其他约束,共鸣共情由 AI 按痛点爽点发挥,随对话逐轮被 AI 更新,空值标「待完善」)+ 下方时长选项条
顶部=创作需求(09-07 v29:8 个选题字段——内容形式/一句话故事/开场钩子/故事爆点/故事爽点/故事框架/人物关系/特别要求;
内容形式置首位;Vlog 不含人物采访;人物关系剧情向补;空值标「待完善」)+ 下方时长选项条
中部=对话流(用户输入 vs AI 整理/追问),底部=输入框 + 提交创作 */
async function confirmAiScript(accountName, btn) {
const accId = btn && btn.dataset.accId ? btn.dataset.accId : '';
@@ -259,7 +280,7 @@ async function confirmAiScript(accountName, btn) {
} catch (e) { return null; }
};
const cache = readCache();
const w = { idea: '', pain: '', blow: '', form: '', dur: '', note: '', ...(cache && cache.w ? cache.w : {}) };
const w = { idea: '', hook: '', blow: '', pain: '', ftype: '', form: '', cast: '', note: '', dur: '', ...(cache && cache.w ? cache.w : {}) };
const convo = (cache && Array.isArray(cache.convo)) ? cache.convo.filter((c) => c && c.role && typeof c.text === 'string').map((c) => ({ role: c.role === 'ai' ? 'ai' : 'u', text: String(c.text), opts: Array.isArray(c.opts) ? c.opts.filter((o) => typeof o === 'string') : [] })) : []; // 对话流 { role: 'u' | 'ai', text, opts }
let pHas = false, pContent = '';
let attach = (cache && cache.attach && cache.attach.name)
@@ -343,11 +364,12 @@ async function confirmAiScript(accountName, btn) {
showToast('已重置,本次对话已清空');
};
/* —— 需求框渲染:5 个选题字段(落点要素可空;共鸣/情节/结尾由 AI 按痛点爽点自决),维度名作标题、内容在下方 —— */
/* —— 需求框渲染:8 个选题字段(内容形式置首位;开场钩子/人物关系 v28 新增、v29 故事框架命名),维度名作标题、内容在下方 —— */
const renderSheet = () => {
const rows = REQ_FIELDS.map(([k, label]) => {
const v = String(w[k] || '').trim();
return `<div class="req-field${v ? '' : ' empty'}"><div class="rf-title">${label}</div><div class="rf-val">${v ? esc(v) : '待完善'}</div></div>`;
const tip = REQ_FIELD_HINTS[k] ? ` title="${esc(REQ_FIELD_HINTS[k])}"` : '';
return `<div class="req-field${v ? '' : ' empty'}"${tip}><div class="rf-title">${label}</div><div class="rf-val">${v ? esc(v) : '待完善'}</div></div>`;
}).join('');
q('#reqSheet').innerHTML = rows;
const filled = REQ_FIELDS.filter(([k]) => String(w[k] || '').trim()).length;
@@ -380,18 +402,20 @@ async function confirmAiScript(accountName, btn) {
const n = Math.round(Number(durNum.value));
w.dur = (Number.isFinite(n) && n > 0) ? n + '秒' : '';
});
/* —— 对话流渲染(09-07:AI 消息可携带 ==OPTS== 候选,渲染为可点选 chips;点击即以该候选作为用户输入发送) —— */
/* —— 对话流渲染(09-07 v27:GPT 风格——用户主色实心圆角、AI 无背景 markdown;候选可点;支持流式追加与思考占位) —— */
const renderDialog = () => {
const el = q('#reqDialog');
el.innerHTML = convo.length
? convo.map((c, ci) => {
if (c.role === 'u') return `<div class="req-msg mine"><div class="req-bubble">${esc(c.text)}</div></div>`;
if (c.role === 'u') return `<div class="req-msg mine"><div class="req-bubble-mine">${esc(c.text)}</div></div>`;
const thinking = c.thinking ? `<div class="req-thinking" role="status" aria-live="polite"><span class="dot"></span><span class="dot"></span><span class="dot"></span><span class="req-thinking-label">正在思考</span></div>` : '';
const body = c.streaming ? '<div class="req-md req-md-stream" data-stream="' + ci + '"></div>' : (c.text ? `<div class="req-md">${esc(c.text)}</div>` : '');
const opts = (Array.isArray(c.opts) && c.opts.length)
? `<div class="req-opt-row">${c.opts.map((o) => `<button type="button" class="req-opt-chip" data-i="${ci}" data-opt="${esc(o)}">${esc(o)}</button>`).join('')}</div>`
: '';
return `<div class="req-msg ai"><div class="req-bubble">${esc(c.text)}</div>${opts}</div>`;
return `<div class="req-msg ai" data-ci="${ci}">${thinking}${body}${opts}</div>`;
}).join('')
: `<div class="req-dialog-empty">在这里输入一个初步想法,AI 会引导你补齐创意 / 创新·爆点 / 痛点爽点 / 叙事结构等需求(落点要素可留空,由 AI 按账号设定与选题补齐,共鸣与情节结尾由 AI 依痛点爽点发挥);每条输入都会即时更新上方的创作需求框,AI 追问时会给出可点选的候选。支持上传附件。Enter 发送,Shift+Enter 换行。</div>`;
: `<div class="req-dialog-empty">像聊天一样说出想法,AI 帮你把这条视频的需求聊到可开拍。</div>`;
el.querySelectorAll('.req-opt-chip').forEach((btn) => {
btn.addEventListener('click', () => {
if (busy) return;
@@ -458,7 +482,7 @@ async function confirmAiScript(accountName, btn) {
parsing = false; paperBtn.disabled = false; paperBtn.classList.remove('loading');
});
paperBtn.addEventListener('click', () => { if (!parsing) attachInput.click(); });
/* —— 发送一轮:调 /api/ai/clarify(同步打磨,返回 reply + ==REQ== 段) —— */
/* —— 发送一轮(09-07 v27:流式输出——首字节前显示"正在思考"占位,逐 chunk 追加到 AI 气泡;流结束后再解析 ==OPTS==/==REQ==) —— */
const sendTurn = async (raw) => {
let text = String(raw || '').trim();
if (busy) return;
@@ -469,46 +493,91 @@ async function confirmAiScript(accountName, btn) {
convo.push({ role: 'u', text });
q('#reqInput').value = '';
autoGrow();
// 先放占位 AI 气泡(thinking=true),再渲染:用户可见"正在思考"动画
const aiIdx = convo.length;
convo.push({ role: 'ai', text: '', opts: [], thinking: true });
renderDialog();
setBusy(true);
try {
// 带最近 2 轮问答做上下文(无状态:需求本身在 reqMd 中已完整携带)
const history = [];
for (let i = convo.length - 2; i >= 0 && history.length < 2; i--) {
if (convo[i].role === 'ai') {
for (let i = convo.length - 3; i >= 0 && history.length < 2; i--) {
if (convo[i].role === 'ai' && !convo[i].thinking) {
const u = convo[i - 1] && convo[i - 1].role === 'u' ? convo[i - 1].text : '';
history.unshift({ u, a: convo[i].text });
i--;
}
}
const r = await api('/api/ai/clarify', {
const resp = await fetch('/api/ai/clarify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ accountName, turn: text, reqMd: reqMdOf(w), history, attach: attach.text }),
body: JSON.stringify({ accountName, turn: text, reqMd: reqMdOf(w), history, attach: attach.text, stream: 1 }),
});
if (r.error) throw new Error(r.error);
const answer = String(r.answer || '').trim();
const ri = answer.lastIndexOf('==REQ==');
const head = (ri >= 0 ? answer.slice(0, ri) : answer).trim();
// 09-07:==OPTS== 候选段(位于正文与 ==REQ== 之间,每行一个可点选短句)→ 渲染为 chips
const oi = head.lastIndexOf('==OPTS==');
const reply = (oi >= 0 ? head.slice(0, oi) : head).trim();
const opts = oi >= 0
? head.slice(oi + '==OPTS=='.length).split('\n').map((s) => s.trim()).filter((s) => s && !s.startsWith('==')).slice(0, 6)
: [];
const fields = ri >= 0 ? parseReqSheet(answer.slice(ri + '==REQ=='.length)) : {};
let updated = 0;
for (const k of Object.keys(fields)) {
const v = String(fields[k] || '').trim();
if (v && w[k] !== v) { w[k] = v; updated++; }
if (!resp.ok) {
const t = await resp.text().catch(() => '');
throw new Error('HTTP ' + resp.status + (t ? ': ' + t.slice(0, 120) : ''));
}
if (reply || opts.length) convo.push({ role: 'ai', text: reply || '(已记录本轮信息,继续补充或点选上方候选)', opts });
saveCache(); // 09-07 v26:每轮成功后即时存草稿,防刷新/误关丢失
renderSheet();
renderDialog();
showToast(updated > 0 ? `✅ 需求已更新 ${updated} 项` : (ri >= 0 ? '✅ 已记录(本次无新变更)' : '⚠️ 本次未返回结构化需求,可换种说法再试'));
const reader = resp.body && resp.body.getReader ? resp.body.getReader() : null;
const ctype = resp.headers.get('content-type') || '';
// 若后端未真正流式(仍走 blocking),兼容降级:读全部 → 一次性写入
if (!reader || !ctype.includes('text/event-stream')) {
const t = await resp.text();
const buf = String(t || '');
// 兼容 data:{answer:"..."} JSON 包裹
let ans = buf;
try { const j = JSON.parse(buf); if (j && typeof j.answer === 'string') ans = j.answer; } catch (e) {}
convo[aiIdx] = { role: 'ai', text: ans, opts: [], thinking: false };
finishAi(aiIdx, ans);
saveCache();
renderDialog();
showToast('✅ 已记录');
return;
}
const dec = new TextDecoder('utf-8');
let buf2 = '';
let firstByte = false;
// 流式:首字节到达即关闭 thinking 占位,进入"边推边渲染"
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf2 += dec.decode(value, { stream: true });
// 按行切分(SSE 事件以空行分隔,每行以 data: 开头)
const lines = buf2.split('\n');
buf2 = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data:')) continue;
const payload = line.slice(5).trim();
if (payload === '[DONE]') continue;
let chunk = '';
try {
const j = JSON.parse(payload);
if (typeof j.answer === 'string') chunk = j.answer;
else if (typeof j.text === 'string') chunk = j.text;
else if (typeof j.delta === 'string') chunk = j.delta;
} catch (e) { continue; }
if (!chunk) continue;
if (!firstByte) {
firstByte = true;
convo[aiIdx].thinking = false;
convo[aiIdx].text = '';
convo[aiIdx].streaming = true;
renderDialog();
appendStreamChunk(aiIdx, chunk);
} else {
appendStreamChunk(aiIdx, chunk);
}
}
}
// 流结束
convo[aiIdx].streaming = false;
const finalText = convo[aiIdx].text || '';
finishAi(aiIdx, finalText);
saveCache();
showToast('✅ 已记录');
} catch (e) {
convo.pop(); // 移除失败的用户消息,恢复输入
// 移除失败的两条占位(用户消息 + 思考占位)
convo.splice(aiIdx, 1);
convo.pop();
q('#reqInput').value = text;
autoGrow();
renderDialog();
@@ -516,6 +585,41 @@ async function confirmAiScript(accountName, btn) {
}
setBusy(false);
};
// 流式过程中:把新片段追加到 convo[aiIdx].text,就地更新对应 .req-md-stream 容器(不重建整个对话流)
function appendStreamChunk(aiIdx, chunk) {
convo[aiIdx].text = (convo[aiIdx].text || '') + chunk;
const el = q('#reqDialog');
if (!el) return;
const node = el.querySelector('.req-md-stream[data-stream="' + aiIdx + '"]');
if (node) {
node.textContent = convo[aiIdx].text; // 纯文本直写,不做 HTML(防注入)
el.scrollTop = el.scrollHeight;
}
}
// 流结束后:解析 ==OPTS== / ==REQ==,回填需求框与候选
function finishAi(aiIdx, fullText) {
const ri = fullText.lastIndexOf('==REQ==');
const head = (ri >= 0 ? fullText.slice(0, ri) : fullText).trim();
const oi = head.lastIndexOf('==OPTS==');
const reply = (oi >= 0 ? head.slice(0, oi) : head).trim();
const opts = oi >= 0
? head.slice(oi + '==OPTS=='.length).split('\n').map((s) => s.trim()).filter((s) => s && !s.startsWith('==')).slice(0, 6)
: [];
const fields = ri >= 0 ? parseReqSheet(fullText.slice(ri + '==REQ=='.length)) : {};
let updated = 0;
for (const k of Object.keys(fields)) {
const v = String(fields[k] || '').trim();
if (v && w[k] !== v) { w[k] = v; updated++; }
}
convo[aiIdx].text = reply || '(已记录本轮信息,继续补充或点选上方候选)';
convo[aiIdx].opts = opts;
convo[aiIdx].thinking = false;
convo[aiIdx].streaming = false;
renderSheet();
renderDialog();
if (updated > 0) showToast(`✅ 需求已更新 ${updated} 项`);
else if (ri < 0) showToast('⚠️ 本次未返回结构化需求,可换种说法再试');
}
/* —— 事件 —— */
const inputEl = q('#reqInput');
/* 输入自动增高:textarea scrollHeight 动态写入 style.height,上限 100px(与 CSS max-height 一致) */
@@ -260,13 +260,38 @@ main { height: calc(100vh - 54px); }
.req-msg { display: flex; }
.req-msg.mine { justify-content: flex-end; }
.req-msg.ai { justify-content: flex-start; }
.req-bubble {
max-width: 88%; padding: 7px 11px; border-radius: 10px;
/* 09-07 v27:GPT 风格气泡——用户主色实心圆角右对齐,AI 无背景直接 markdown 铺排左对齐 */
.req-bubble-mine {
max-width: 80%;
background: var(--primary); color: #fff;
padding: 10px 14px; border-radius: 14px;
font-size: 14px; line-height: 1.55; white-space: pre-wrap; word-break: break-word;
}
/* 09-07 v26:AI 消息 column 布局下气泡宽度自适应文本(最长行限制在消息行宽内) */
.req-msg.ai { align-items: flex-start; }
.req-msg.ai .req-bubble { max-width: 100%; width: fit-content; min-width: 0; }
.req-md {
max-width: 100%;
color: var(--text); font-size: 14.5px; line-height: 1.7;
white-space: pre-wrap; word-break: break-word;
}
/* 流式占位容器(流结束时会被替换为最终 markdown 节点) */
.req-md-stream { min-height: 1.6em; }
/* 思考动画:三个点交替跳动 + "正在思考"提示(不展示模型思考内容) */
.req-thinking {
display: inline-flex; align-items: center; gap: 6px;
color: var(--dim); font-size: 13px;
padding: 4px 2px;
}
.req-thinking .dot {
width: 6px; height: 6px; border-radius: 50%;
background: var(--primary); display: inline-block;
animation: req-dot 1.2s ease-in-out infinite;
}
.req-thinking .dot:nth-child(2) { animation-delay: .15s; }
.req-thinking .dot:nth-child(3) { animation-delay: .30s; }
.req-thinking-label { margin-left: 2px; }
@keyframes req-dot {
0%, 80%, 100% { opacity: .25; transform: translateY(0); }
40% { opacity: 1; transform: translateY(-3px); }
}
.req-msg.mine .req-bubble { background: var(--primary); color: #fff; border-bottom-right-radius: 3px; }
.req-msg.ai .req-bubble { background: #f0f2f5; color: var(--text); border-bottom-left-radius: 3px; }
/* 09-07:AI 消息内嵌候选按钮行(flova 式:气泡下方紧跟可点选项,无额外提示语/虚线框) */
@@ -859,76 +859,130 @@ const server = http.createServer((req, res) => {
return;
}
// ---------- AI 创作需求打磨(09-02 弹窗内对话:Dify chat-messages 同步、无状态多轮) ----------
// body: { accountName, turn(本轮输入), reqMd(当前需求5行md), history(最近轮[{u,a}]) } → { answer }
// ---------- AI 创作需求打磨(09-02 弹窗内对话:阻塞/流式 两种模式;09-07 v27 新增 SSE 流式输出,前端可逐 chunk 渲染 + 思考占位) ----------
if (p === '/api/ai/clarify' && req.method === 'POST') {
let body = '';
req.on('data', (c) => { body += c; if (body.length > 1e6) req.destroy(); });
req.on('end', async () => {
let payload;
try { payload = JSON.parse(body || '{}'); } catch (e) { return sendErr(400, 'JSON 解析失败'); }
const { accountName, turn, reqMd, history, attach, stream } = payload;
if (!accountName || typeof accountName !== 'string') return sendErr(400, '缺少账号名');
if (!turn || typeof turn !== 'string' || !turn.trim()) return sendErr(400, '缺少用户输入');
// 1) 账号设定摘要(工作台库,与选题接口同源)
let personaText = '', contentText = '';
try {
const { accountName, turn, reqMd, history, attach } = JSON.parse(body || '{}');
if (!accountName || typeof accountName !== 'string') return sendErr(400, '缺少账号名');
if (!turn || typeof turn !== 'string' || !turn.trim()) return sendErr(400, '缺少用户输入');
// 1) 账号设定摘要(工作台库,与选题接口同源)
let personaText = '', contentText = '';
try {
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(path.join(ROOT, 'mcn-plugin.db'), { readOnly: true });
const acc = db.prepare('SELECT * FROM hot_accounts WHERE account_name=? LIMIT 1').get(accountName);
if (acc) {
contentText = String(acc.content || '').slice(0, 300);
const p = db.prepare('SELECT content_json, summary FROM account_persona WHERE account_id=? ORDER BY id DESC LIMIT 1').get(acc.id);
if (p) personaText = String(p.summary || p.content_json || '').slice(0, 900);
}
db.close();
} catch (e) { /* 库不可用不阻塞 */ }
// 2) Dify key:环境变量优先,其次技能脚本内置默认
let apiKey = process.env.DIFY_MCN_CYLG_KEY || '';
if (!apiKey) {
const scriptPath = path.join(ROOT, '..', 'scripts', 'MCN_CYLG_API.py');
try {
const src = fs.readFileSync(scriptPath, 'utf8');
const m = src.match(/DEFAULT_CYLG_KEY\s*=\s*"([^"]+)"/);
if (m) apiKey = m[1];
} catch (e) { /* 脚本不存在则走 env */ }
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(path.join(ROOT, 'mcn-plugin.db'), { readOnly: true });
const acc = db.prepare('SELECT * FROM hot_accounts WHERE account_name=? LIMIT 1').get(accountName);
if (acc) {
contentText = String(acc.content || '').slice(0, 300);
const p = db.prepare('SELECT content_json, summary FROM account_persona WHERE account_id=? ORDER BY id DESC LIMIT 1').get(acc.id);
if (p) personaText = String(p.summary || p.content_json || '').slice(0, 900);
}
if (!apiKey) return sendErr(500, '未配置 DIFY_MCN_CYLG_KEY(环境变量或技能脚本内置 KEY)');
// 3) 构造打磨 query(无状态:携带当前需求 + 最近对话,让 AI 输出 reply + ==OPTS==/==REQ== 结构化段)
// 09-07:prompt 规则已抽为 prompts/clarify.md(文件化迭代);此处只负责动态上下文注入
const his = Array.isArray(history) ? history.filter((h) => h && h.u).slice(-2) : [];
const histText = his.map((h) => '你:' + String(h.a || '') + '\n达人:' + String(h.u || '')).join('\n');
const reqMdText = String(reqMd || '').trim();
const attachText = String(attach || '').trim().slice(0, 20000);
const tpl = loadPrompt('clarify');
if (!tpl) return sendErr(500, '缺少 prompts/clarify.md 需求打磨模板');
const query = tpl
.replace('{{accountName}}', accountName)
.replace('{{personaText}}', personaText || '(暂无可用账号设定,按通用编导经验澄清)')
.replace('{{contentText}}', contentText ? '【账号定位】\n' + contentText : '')
.replace('{{attachText}}', attachText ? '【附件素材】(达人上传的需求文档/资料,打磨与最终创作都须充分参考;把其中的关键诉求/约束吸收进需求清单)\n' + attachText : '')
.replace('{{reqMdText}}', reqMdText || '(空:等待用户先给出初步想法)')
.replace('{{histText}}', histText ? '【对话历史】\n' + histText : '')
.replace('{{turnText}}', turn.trim());
// 4) Dify blocking 调用
const difyUrl = String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1').replace(/\/+$/, '');
db.close();
} catch (e) { /* 库不可用不阻塞 */ }
// 2) Dify key:环境变量优先,其次技能脚本内置默认
let apiKey = process.env.DIFY_MCN_CYLG_KEY || '';
if (!apiKey) {
const scriptPath = path.join(ROOT, '..', 'scripts', 'MCN_CYLG_API.py');
try {
const src = fs.readFileSync(scriptPath, 'utf8');
const m = src.match(/DEFAULT_CYLG_KEY\s*=\s*"([^"]+)"/);
if (m) apiKey = m[1];
} catch (e) { /* 脚本不存在则走 env */ }
}
if (!apiKey) return sendErr(500, '未配置 DIFY_MCN_CYLG_KEY(环境变量或技能脚本内置 KEY)');
// 3) 构造打磨 query(无状态:携带当前需求 + 最近对话;prompt 模板读自 prompts/clarify.md)
const his = Array.isArray(history) ? history.filter((h) => h && h.u).slice(-2) : [];
const histText = his.map((h) => '你:' + String(h.a || '') + '\n达人:' + String(h.u || '')).join('\n');
const reqMdText = String(reqMd || '').trim();
const attachText = String(attach || '').trim().slice(0, 20000);
const tpl = loadPrompt('clarify');
if (!tpl) return sendErr(500, '缺少 prompts/clarify.md 需求打磨模板');
const query = tpl
.replace('{{accountName}}', accountName)
.replace('{{personaText}}', personaText || '(暂无可用账号设定,按通用编导经验澄清)')
.replace('{{contentText}}', contentText ? '【账号定位】\n' + contentText : '')
.replace('{{attachText}}', attachText ? '【附件素材】(达人上传的需求文档/资料,打磨与最终创作都须充分参考;把其中的关键诉求/约束吸收进需求清单)\n' + attachText : '')
.replace('{{reqMdText}}', reqMdText || '(空:等待用户先给出初步想法)')
.replace('{{histText}}', histText ? '【对话历史】\n' + histText : '')
.replace('{{turnText}}', turn.trim());
const difyUrl = String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1').replace(/\/+$/, '');
// 4) 流式 / 阻塞 分支
if (stream) {
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no',
});
const write = (obj) => { try { res.write('data: ' + JSON.stringify(obj) + '\n\n'); } catch (e) {} };
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 100000);
let resp;
try {
resp = await fetch(difyUrl + '/chat-messages', {
const difyResp = await fetch(difyUrl + '/chat-messages', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-work-shop' }),
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'streaming', user: 'mcn-work-shop' }),
signal: ctrl.signal,
});
if (!difyResp.ok || !difyResp.body) {
write({ error: 'AI 接口错误: HTTP ' + difyResp.status });
res.write('data: [DONE]\n\n');
return res.end();
}
const reader = difyResp.body.getReader();
const dec = new TextDecoder('utf-8');
let buf = '';
let lastAnswer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const events = buf.split('\n\n');
buf = events.pop() || '';
for (const ev of events) {
const lines = ev.split('\n');
for (const line of lines) {
if (!line.startsWith('data:')) continue;
const pl = line.slice(5).trim();
if (!pl || pl === '[DONE]') continue;
let json = null;
try { json = JSON.parse(pl); } catch (e) { continue; }
if (json.event === 'message' && typeof json.answer === 'string') {
const delta = json.answer.slice(lastAnswer.length);
lastAnswer = json.answer;
if (delta) write({ delta });
}
// message_end / workflow_finished 等终态事件:暂不额外处理,依赖流关闭
}
}
}
log('AI 需求打磨(stream): ' + accountName);
res.write('data: [DONE]\n\n');
res.end();
} catch (e) {
try { write({ error: e.message }); res.write('data: [DONE]\n\n'); res.end(); } catch (e) {}
} finally { clearTimeout(timer); }
const data = await resp.json().catch(() => ({}));
if (!resp.ok) return sendErr(502, 'AI 接口错误: ' + (data.message || resp.status));
log('AI 需求打磨: ' + accountName);
return dshOk(200, { answer: data.answer || '' });
} catch (e) {
return sendErr(500, '需求打磨失败: ' + e.message);
return;
}
// 阻塞模式(兼容)
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 100000);
let difyResp;
try {
difyResp = await fetch(difyUrl + '/chat-messages', {
method: 'POST',
headers: { 'Authorization': 'Bearer ' + apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify({ inputs: { is_think: '0', is_online: '0', model: process.env.DIFY_MCN_MODEL || 'gemini' }, query, response_mode: 'blocking', user: 'mcn-work-shop' }),
signal: ctrl.signal,
});
} finally { clearTimeout(timer); }
const data = await difyResp.json().catch(() => ({}));
if (!difyResp.ok) return sendErr(502, 'AI 接口错误: ' + (data.message || difyResp.status));
log('AI 需求打磨: ' + accountName);
return dshOk(200, { answer: data.answer || '' });
});
return;
}
@@ -6,6 +6,13 @@
---
## 2026-09-07(术语统一:视觉短片 / 开场钩子 / 需求框-技能衔接)
- **D·视觉脚本 → D·视觉短片(全量)**:S9 格式列举(SKILL.md)、S6 内容形式映射表(F04 测评种草/F07 纯视觉短片)、S9 正文 6 处、创作流程规范 3 处、03_框架节奏/07_脚本格式选择指南 8 处——与工作台需求框/用户既定术语对齐,消除「纯视觉短片→D·视觉脚本」混用。
- **预设钩子 → 开场钩子(全量)**:S5 选题方案字段(含 10 种开场方式引用)+ S7 大纲回扣语义 + 04_爆款开场/00_口径说明——对外统称「开场钩子」(=S6 场1 段名、工作台需求框 hook 字段同名同义),S5 阶段语义由「预设」改为直接以开场钩子表达。
- **需求框-技能衔接注明(mcn-work-shop/prompts/clarify.md)**:内容形式 4 项为粗选方向,落创作时细化为 S6 口径(口播→教程/知识分享/盘点,视觉短片→测评/沉浸式);故事框架四词(钩子前置/悬念反转/递进铺垫/先抑后扬)=达人倾向词,由 S6 用叙事形态 F01-F08 + 结构模板(三段式/五段式)定型。
## 2026-09-04(R73/R74 技能树体量优化)
主 SKILL.md 压缩维护:frontmatter `last_change` 明细迁移至本文件(历史归档);11 步流程 ASCII 总览图压缩为行式 Gate 链(S1→[G0]→…→[G6]);文件底部新增「阅读分层」说明(🔑 导航行/🚫 红线/无标记正文三类)。
@@ -256,7 +256,7 @@
选题创作方法论:参照处理规则「二、选题创作方法论」中定义的 5 大方法及其推荐流程。方法论来源及更详细的创作技巧见知识库文件:
- `知识库/02_故事选题/` 全部文件(元素叠加/跨界重组/新鲜度维度/落地路径/**选题三环模型**)
- `知识库/04_爆款开场/` 全部文件(10种开场方式,用于选题的"预设钩子"环节)
- `知识库/04_爆款开场/` 全部文件(10种开场方式,用于选题的"开场钩子"环节)
- `知识库/07_情绪共鸣/` 分块4(网感坐标:赛道×叙事形态→目标体感映射表 + 体感兑现闭环)— 用于选题的"目标体感"确定
选题验证标准:参照处理规则「三、选题验证标准」中的硬性验证、三环校验和钩子预设要求。
@@ -314,7 +314,7 @@
| **人设匹配度** | {与达人身份/性格/价值的一致性说明} |
| **目标体感** | {从网感坐标选取的目标体感 + 情绪标签 + 避免体感} |
| **共鸣切入点** | {选题的「为什么」+ 切入点类型(缺失补偿/困境投射/身份反转/愿望实现)+ 一句共鸣测试语} |
| **预设钩子** | {钩子类型 + 开场方式(抓眼元素前3秒内出现;开场段总时长另守 3-7 秒铁律)} |
| **开场钩子** | {钩子类型 + 开场方式(抓眼元素前3秒内出现;开场段总时长另守 3-7 秒铁律)} |
| **故事前提** | 剧情/悬念/反转类必填:核心设定 / 核心秘密 / 秘密曝光节点 / 信息边界 / 来源标注([U]/[AI]/[对标]);非剧情类标"不适用" |
---
@@ -330,7 +330,7 @@
| **人设匹配度** | {与达人身份/性格/价值的一致性说明} |
| **目标体感** | {从网感坐标选取的目标体感 + 情绪标签 + 避免体感} |
| **共鸣切入点** | {选题的「为什么」+ 切入点类型 + 一句共鸣测试语} |
| **预设钩子** | {钩子类型 + 开场方式(抓眼元素前3秒内出现;开场段总时长另守 3-7 秒铁律)} |
| **开场钩子** | {钩子类型 + 开场方式(抓眼元素前3秒内出现;开场段总时长另守 3-7 秒铁律)} |
| **故事前提** | 剧情/悬念/反转类必填:核心设定 / 核心秘密 / 秘密曝光节点 / 信息边界 / 来源标注([U]/[AI]/[对标]);非剧情类标"不适用" |
---
@@ -346,7 +346,7 @@
| **人设匹配度** | {与达人身份/性格/价值的一致性说明} |
| **目标体感** | {从网感坐标选取的目标体感 + 情绪标签 + 避免体感} |
| **共鸣切入点** | {选题的「为什么」+ 切入点类型 + 一句共鸣测试语} |
| **预设钩子** | {钩子类型 + 开场方式(抓眼元素前3秒内出现;开场段总时长另守 3-7 秒铁律)} |
| **开场钩子** | {钩子类型 + 开场方式(抓眼元素前3秒内出现;开场段总时长另守 3-7 秒铁律)} |
| **故事前提** | 剧情/悬念/反转类必填:核心设定 / 核心秘密 / 秘密曝光节点 / 信息边界 / 来源标注([U]/[AI]/[对标]);非剧情类标"不适用" |
---
@@ -362,7 +362,7 @@
| **人设匹配度** | {与达人身份/性格/价值的一致性说明} |
| **目标体感** | {从网感坐标选取的目标体感 + 情绪标签 + 避免体感} |
| **共鸣切入点** | {选题的「为什么」+ 切入点类型 + 一句共鸣测试语} |
| **预设钩子** | {钩子类型 + 开场方式(抓眼元素前3秒内出现;开场段总时长另守 3-7 秒铁律)} |
| **开场钩子** | {钩子类型 + 开场方式(抓眼元素前3秒内出现;开场段总时长另守 3-7 秒铁律)} |
| **故事前提** | 剧情/悬念/反转类必填:核心设定 / 核心秘密 / 秘密曝光节点 / 信息边界 / 来源标注([U]/[AI]/[对标]);非剧情类标"不适用" |
---
@@ -378,7 +378,7 @@
| **人设匹配度** | {与达人身份/性格/价值的一致性说明} |
| **目标体感** | {从网感坐标选取的目标体感 + 情绪标签 + 避免体感} |
| **共鸣切入点** | {选题的「为什么」+ 切入点类型 + 一句共鸣测试语} |
| **预设钩子** | {钩子类型 + 开场方式(抓眼元素前3秒内出现;开场段总时长另守 3-7 秒铁律)} |
| **开场钩子** | {钩子类型 + 开场方式(抓眼元素前3秒内出现;开场段总时长另守 3-7 秒铁律)} |
| **故事前提** | 剧情/悬念/反转类必填:核心设定 / 核心秘密 / 秘密曝光节点 / 信息边界 / 来源标注([U]/[AI]/[对标]);非剧情类标"不适用" |
---
@@ -35,10 +35,10 @@
|---------|---------|------------|
| 剧情 | F01 剧情演绎 | A·场次叙事 |
| Vlog | F02 Vlog纪实 | B·Vlog双轨 |
| 测评 | F04 测评种草 | D·视觉脚本 |
| 测评 | F04 测评种草 | D·视觉短片 |
| 教程 | F05 教程教学 | C·口播要点 |
| 挑战 | F01 剧情演绎 | A·场次叙事 |
| 沉浸式 | F07 纯视觉短片 | D·视觉脚本 |
| 沉浸式 | F07 纯视觉短片 | D·视觉短片 |
| 知识分享 | F03 干货口播 | C·口播要点 |
> 补充叙事形态(无直接内容形式映射,需在框架阶段显式选择):
@@ -5,7 +5,7 @@
---
## 输入
- 步骤5输出:选题方案(主题/切入角度/爆款逻辑/预设钩子/差异化特征/目标体感/共鸣切入点)
- 步骤5输出:选题方案(主题/切入角度/爆款逻辑/开场钩子/差异化特征/目标体感/共鸣切入点)
- 步骤6输出:用户选定的框架方案(核心框架+内容形式+时长+主线)
- 步骤6输出:场次结构展开表(场次划分/时长区间/叙事节奏/情绪)
- 步骤6输出:情绪设计卡(弧线类型/情绪标注/情绪起伏/缺口自检)— 只引用不重做
@@ -206,7 +206,7 @@ S6 场次结构表「事件」列是规划概述(如「准备场景 → 核心
- **场景预演**:在展开每场事件序列之前,先在内部完成该场关键场景的文字预演(空间/时间/灯光/人物位置/最显眼的 3 个细节),再基于预演画面写出事件序列。预演内容不写入最终大纲
- **素材使用**:按知识库/05_极致事件/确定的极致类型调取对应素材内容填充大纲,而非从零推断。极致类型(感官/戏剧)映射到具体素材类型,参见素材库/分块定义/。所有具名内容必须有品牌/材质/型号等具体信息,禁止泛称
- 结构模板选择规则:知识分享/技能展示/15s内→三段式,Vlog/剧情/测评/30s+→五段式
- 钩子类型须回扣 S5 选题方案中的「预设钩子」,不另起炉灶
- 钩子类型须回扣 S5 选题方案中的「开场钩子」,不另起炉灶
- 叙事模型必须明确选择一种,须与框架方案的爆款逻辑一致
- 时长分配须参照 S6 场次结构展开表,不得自行改动
- 口播须符合达人人设的语言风格和口头禅
@@ -222,7 +222,7 @@ S6 场次结构表「事件」列是规划概述(如「准备场景 → 核心
- 不得用概要式对话替代完整台词(如"她表达了感谢""他说他很贴心"),必须展开为「角色名(语气):具体台词」,一组对话至少 2 句有来有回
- 不得用概括替代操作步骤链(如"认真做饭""收拾房间"),必须拆解为 ≥3 步连贯动作(做什么 → 怎么做 → 细节)
- 不得跳过密度自检直接提交
- 不得在已有选题预设钩子的情况下重新设计钩子方向
- 不得在已有选题开场钩子的情况下重新设计钩子方向
- 不得偏离 S6 场次结构展开表中的时长区间和情绪弧线
- 不得使用模糊描述(如"做一个有趣的钩子"),必须具体到台词和画面
- 不得跳过合规自检
@@ -21,7 +21,7 @@
| F01 剧情演绎 | A·场次叙事 | 场1/场2… | 多角色+对白驱动+场景转换 |
| F02/F08 | B·Vlog双轨 | 场1/场2… | 画面描述+旁白双轨,第一视角 |
| F03/F05/F06 | C·口播要点 | 场1/场2… | 口播主体+[B-roll]标注 |
| F04/F07 | D·视觉脚本 | 场1/场2… | 画面驱动,少台词 |
| F04/F07 | D·视觉短片 | 场1/场2… | 画面驱动,少台词 |
**格式间禁止混用**:F01 剧情类严禁旁白;F02/F08 不得套用剧情对白格式;F03/F05/F06 不得堆砌动作叙述;F07 不得写大段台词。
@@ -74,7 +74,7 @@
[B-roll:{画面描述}]
```
**_D·视觉脚本(F04/F07):_**
**_D·视觉短片(F04/F07):_**
```
**场1:画面主题(Xs-Xs)**
@@ -197,7 +197,7 @@
- F01 剧情演绎 → **A·场次叙事**(场1/场2…)
- F02 Vlog纪实 / F08 人物采访 → **B·Vlog双轨**(场1/场2…)
- F03 干货口播 / F05 教程教学 / F06 盘点解说 → **C·口播要点**(场1/场2…)
- F04 测评种草 / F07 纯视觉短片 → **D·视觉脚本**(场1/场2…)
- F04 测评种草 / F07 纯视觉短片 → **D·视觉短片**(场1/场2…)
3. 三大段结构(开场钩子/中间主体/结尾收口)+ 每段下设场次
4. 台词口语化、可直接朗读/配音,标注说话人和语气
5. 广告植入严格对齐广告植入大纲
@@ -228,7 +228,7 @@
- **B-roll 标注**:`[B-roll:画面描述]` 放在对应口播段落之后
- 结尾场以实用信息收尾,不强行升华
**_D·视觉脚本(F04/F07):_**
**_D·视觉短片(F04/F07):_**
- **场次标题**:**加粗**,`场N:画面主题(Xs-Xs)`
- **画面驱动**:每场核心是画面描述(构图/光影/色彩/运镜节奏)
- **配乐/音效标注**:`(配乐/音效:描述)`
@@ -326,7 +326,7 @@
→ F01 剧情演绎 → **A·场次叙事** 格式(场1/场2…)
→ F02/F08 → **B·Vlog双轨** 格式(场1/场2…)
→ F03/F05/F06 → **C·口播要点** 格式(场1/场2…)
→ F04/F07 → **D·视觉脚本** 格式(场1/场2…)
→ F04/F07 → **D·视觉短片** 格式(场1/场2…)
完整格式定义参见知识库 `03_框架节奏/07_脚本格式选择指南.md`。
@@ -419,7 +419,7 @@
**{角色名}**(口播·{语气}):{口播台词}
```
## D·视觉脚本(F04/F07)
## D·视觉短片(F04/F07)
```
**场1:{画面主题}({X}-{X}秒)**
@@ -199,7 +199,7 @@
| F01 剧情演绎 | **A·场次叙事** | 场1/场2… | 多角色+对白驱动+场景转换 |
| F02 Vlog纪实 / F08 人物采访 | **B·Vlog双轨** | 场1/场2… | 画面+旁白双轨,第一视角 |
| F03 干货口播 / F05 教程教学 / F06 盘点解说 | **C·口播要点** | 场1/场2… | 口播主体 + B-roll标注 |
| F04 测评种草 / F07 纯视觉短片 | **D·视觉脚本** | 场1/场2… | 画面驱动,少台词 |
| F04 测评种草 / F07 纯视觉短片 | **D·视觉短片** | 场1/场2… | 画面驱动,少台词 |
### 格式间禁止混用
@@ -310,12 +310,12 @@ F05 教程教学类脚本必须包含**完整步骤链**,缺任一段 = 格式
---
## D·视觉脚本格式(F04 测评种草 / F07 纯视觉短片)
## D·视觉短片格式(F04 测评种草 / F07 纯视觉短片)
```
**场1:画面主题(Xs-Xs)**
(画面描述——构图、光影、色彩、运镜节奏。视觉脚本的核心是画面本身。)
(画面描述——构图、光影、色彩、运镜节奏。视觉短片的核心是画面本身。)
(配乐/音效:氛围音乐或关键音效,可选。)
@@ -23,7 +23,7 @@
| **A·场次叙事** | 场1/场2… | F01 剧情演绎 | 多角色+对白驱动+场景转换,完整场次叙事 |
| **B·Vlog双轨** | 场1/场2… | F02 Vlog纪实 / F08 人物采访 | 画面+旁白/口播双层,第一视角沉浸 |
| **C·口播要点** | 场1/场2… | F03 干货口播 / F05 教程教学 / F06 盘点解说 | 口播主体 + B-roll辅助,信息密度优先 |
| **D·视觉脚本** | 场1/场2… | F04 测评种草 / F07 纯视觉短片 | 画面驱动为主,少量台词/字幕说明 |
| **D·视觉短片** | 场1/场2… | F04 测评种草 / F07 纯视觉短片 | 画面驱动为主,少量台词/字幕说明 |
---
@@ -167,7 +167,7 @@
---
### D·视觉脚本(测评/纯视觉专用)
### D·视觉短片(测评/纯视觉专用)
> 适用:F04 测评种草 / F07 纯视觉短片 | 编号:场1、场2、场3…
> 特征:画面优先、氛围驱动、少台词或无台词
@@ -179,16 +179,16 @@
**内容格式:**
```
(画面描述——自然段落,镜头中看到什么、镜头怎么运动、光影氛围。视觉脚本的核心就是画面本身。)
(画面描述——自然段落,镜头中看到什么、镜头怎么运动、光影氛围。视觉短片的核心就是画面本身。)
(配乐/音效标注——该场次的氛围音乐或关键音效,可选。)
(台词/字幕:如有口播或字幕说明,简短标注。视觉脚本以画面为主,台词极少。)
(台词/字幕:如有口播或字幕说明,简短标注。视觉短片以画面为主,台词极少。)
```
**核心规则:**
- 编号用阿拉伯数字:场1、场2、场3…
- 场标题:画面主题 + 时长,不标注地点·时间(视觉脚本的"地点"在画面描述中呈现)
- 场标题:画面主题 + 时长,不标注地点·时间(视觉短片的"地点"在画面描述中呈现)
- **画面驱动**:每个场次的核心是画面描述——构图、光影、色彩、运镜节奏
- **配乐/音效敏感**:因为台词少,音乐和音效承担情绪引导功能,必须标注
- 台词极少(或完全没有),字幕仅为必要信息说明
@@ -229,7 +229,7 @@
│ → 内容:口播主体 + [B-roll:画面描述] 标注
│
└─ 叙事形态 = F04 测评种草 或 F07 纯视觉短片
→ 使用 D·视觉脚本格式
→ 使用 D·视觉短片格式
→ 编号:场1、场2…
→ 内容:画面描述为主 + 配乐/音效 + 少量台词/字幕
```
@@ -243,7 +243,7 @@
| F01 剧情类使用旁白 | 剧情必须靠角色行为和对话演绎,严禁旁白驱动 |
| F02/F08 Vlog类使用角色对白格式 | Vlog的画面+旁白双轨≠剧情类的多角色对白 |
| F03/F05/F06 口播类堆砌动作叙述 | 口播主体是讲述,动作描述仅标注关键手势/走位 |
| F07 纯视觉类写大段台词 | 视觉脚本画面驱动,台词极少或无 |
| F07 纯视觉类写大段台词 | 视觉短片画面驱动,台词极少或无 |
| F04 测评类套用剧情格式 | 测评是画面+信息驱动,不需要角色扮演和冲突 |
---
@@ -255,4 +255,4 @@
| 有剧本、有多个人物对话、有冲突反转 | **A·场次叙事(场1/场2…)** |
| 第一视角边走边拍、画外音讲述感受 | **B·Vlog双轨(场1/场2…)** |
| 一个人面对镜头讲干货、步骤清晰 | **C·口播要点(场1/场2…)** |
| 靠画面说话、音乐很重要、台词很少 | **D·视觉脚本(场1/场2…)** |
| 靠画面说话、音乐很重要、台词很少 | **D·视觉短片(场1/场2…)** |
@@ -4,7 +4,7 @@
> 目标:将零散的爆款开场知识系统化重组为10个标准分类文件
> **开场钩子时长铁律:3-7秒,硬上限10秒**(绝对时长口径,不看总时长占比)——任何开场方式(下表10种)都必须在此时间内完成核心钩子动作。
> > 注意区分两个口径:本铁律管**开场段总时长**;流程文件中的「前3秒 / 3秒抓眼」(S5 预设钩子、S6 场1、S7 钩子有效性)指**抓眼元素呈现窗口**(刷到后3秒内必须出现),两者兼容不冲突。
> > 注意区分两个口径:本铁律管**开场段总时长**;流程文件中的「前3秒 / 3秒抓眼」(S5 开场钩子、S6 场1、S7 钩子有效性)指**抓眼元素呈现窗口**(刷到后3秒内必须出现),两者兼容不冲突。
---