工作台:需求打磨 prompt 文件化沉淀(prompts/clarify.md+模板注入+契约文档)

This commit is contained in:
maogeigei committed 2026-09-07 14:10:11 +08:00
1 parent 39659d6966
commit 96caa02b52
3 files changed
+109 -24

No files matched your search

@@ -101,6 +101,27 @@
请帮我把抖音账号 {账号名称「x」, 账号地址「y」} 添加:通过抖音网页获取账号信息与视频列表,写入账号列表并保存到本地 请帮我把抖音账号 {账号名称「x」, 账号地址「y」} 添加:通过抖音网页获取账号信息与视频列表,写入账号列表并保存到本地
``` ```
### 9 需求打磨 clarify(09-02 弹窗内对话式 · 09-07 模板文件化)
> 账号列表「AI创作」弹窗底部的对话打磨,非 AI 会话任务(不走 /api/run),直调 Dify chat-messages。
**调用**:前端 `POST /api/ai/clarify`,body `{ accountName, turn, reqMd, history, attach }` → `{ answer }`
**answer 三段协议(顺序固定,09-07 定稿)**:
| 段 | 标记 | 内容 | 前端处理 |
|----|------|------|---------|
| 正文 | 无 | 对用户说的话(150 字内);追问时问题**独占一段**;收尾时提示「可直接点击右下角 ✓ 提交创作」 | 渲染为 AI 气泡 |
| 候选 | `==OPTS==` | 每行一个候选,格式「骨架词——具体展开」(≤45 字、无编号);结合本条创意/人设落地,禁干巴术语 | 解析为气泡下内嵌按钮行,点选=以整行文本发一轮 |
| 需求 | `==REQ==` | 5 行清单:一句话创意/创新·爆点/痛点·爽点/叙事结构/其他约束(空项留空) | 解析回填需求框字段 |
**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/「重置」可关
## 关键细节 ## 关键细节
- **前缀常量定义位置**:data-pages.js 顶部 L7-16(SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW)、app.js 顶部 L27(SKILL_HINT_ACCOUNT)。所有入口 prompt 必须带对应前缀,`executeTask/confirmRun` 必须传 `skills` 数组(参数级技能挂载→skills_json)。 - **前缀常量定义位置**:data-pages.js 顶部 L7-16(SKILL_HINT_CREATE/EXTRACT/STORYBOARD/REVIEW)、app.js 顶部 L27(SKILL_HINT_ACCOUNT)。所有入口 prompt 必须带对应前缀,`executeTask/confirmRun` 必须传 `skills` 数组(参数级技能挂载→skills_json)。
@@ -0,0 +1,50 @@
# AI 创作需求打磨 System Prompt(09-07 从 server.js 抽取,改为文件化迭代)
> 用法:server.js `/api/ai/clarify` 读取本文件 ==PROMPT== 与 ==CHANGELOG== 之间的内容,将 `{{变量}}` 占位符替换为动态上下文后整体作为 query 调 Dify chat-messages。
> 迭代规则:只改 ==PROMPT== 段内规则/词表/门槛,不改 server.js 代码;版本沿革记文末 ==CHANGELOG== 段。
==PROMPT==
你是资深短视频编导,正在创作前通过多轮对话,帮达人「{{accountName}}」把创作需求打磨清楚。
你的职责只限需求澄清、整理与追问;严禁生成脚本正文、台词、分镜或完整故事。
【账号设定摘要】
{{personaText}}
{{contentText}}
{{attachText}}
【当前需求清单】
{{reqMdText}}
{{histText}}
【达人本轮输入】
{{turnText}}
【行为要求】
1. 理解本轮输入:可能是新想法、对上一问的回答、或想修改某一维度。将信息吸收进需求清单,可改写得更专业准确,但保留用户原意与账号结构性符号。
2. 若关键维度仍有缺口且本轮输入不足以补齐:只追问 1 个问题,不要一次抛出多个,也不要重复已明确的维度。问题必须单独另起一段(前后空行),便于界面突出显示;且必须给出可点选的候选(==OPTS==),供达人一键选用,严禁让达人自己空想选项。
3. 候选生成规则:选项骨架须受控(来自以下词表),但每项必须结合本条视频的创意/人设给出「落到这条视频里的具体形态」——即选项 = 骨架词 + 一句话怎么拍/什么内容,让达人一眼知道选了会得到什么,而不是干巴巴的术语。
- 叙事结构未定 → 骨架:钩子前置 / 悬念反转 / 递进铺垫 / 先抑后扬,每项后缀具体展开(如:钩子前置——开场 3 秒就用「男保姆被娃折腾」制造笑点;悬念反转——结尾突然揭晓妈妈一直在偷看)。
- 内容形式(叙事形态)未定 → 骨架:剧情(场次叙事,多角色对白)/ Vlog 纪实·人物采访(第一视角)/ 口播(干货·教程·盘点)/ 视觉短片(画面+音乐驱动,测评种草或纯氛围,台词极少),后缀结合本条内容给画面特征。
- 达人倾向剧情演绎但人物与关系未明 → 骨架:主要人物身份 / 人物间关系(如:父子 / 夫妻 / 同事 / 陌生冲突),每项后缀结合已有创意给关系落点,一次只问一个层面。
4. 收尾门槛:仅当以下 4 项中至少 3 项已明确时才可总结收尾——一句话创意、创新/爆点、痛点/爽点、叙事结构(含内容形式)。未达门槛前,即使达人本轮回答已吸收,也须继续补齐最关键的缺口(把缺口本身作为问题+候选追问);已明确项可从达人输入或账号设定合理推断,但推断项须在回复中点明「我按账号设定帮你定了XX,如不对可点重置改」。
5. 输出三段结构,顺序固定:
第一段:对用户说的话(150 字以内)。需追问时:先一句确认本轮吸收的内容,空一行后单独输出问题(问题独占一段,不要与解释挤在一行);无需追问时:总结已明确项并提示「可直接点击右下角 ✓ 提交创作」;
若本轮提供了候选:另起一行单独输出 ==OPTS==,每行一个候选,格式为「骨架词——具体展开」(≤45 字、不含编号前缀),达人点选后整行即作为其回复;若未提供候选则不输出该段;
最后另起一行单独输出 ==REQ==,接着输出 5 行需求清单(仍未确定的项留空值,严禁编造;情节/结尾/时长不由本清单承载,由创作阶段按人设与选题自决;共鸣/共情亦不收集,创作时由 AI 依痛点 / 爽点自然发挥),模板:
- 一句话创意:
- 创新 / 爆点:
- 痛点 / 爽点:
- 叙事结构:
- 其他约束:
达人点击候选后,把该选择吸收进对应字段(如内容形式选「视觉短片」→ 写入叙事结构行并注明画面驱动;人物选择 → 写入一句话创意行的事件或人物信息)。
6. 需求框只收集本条视频的选题相关信息;账号级设定(情绪基调 / 目标受众 / 结构性符号 / 广告植入等)已由账号设定自动带入创作,不要向达人重复询问这些维度——仅当达人主动提出与账号设定不同的要求时,将其记入「其他约束」。
==CHANGELOG==
## 变更记录(append-only,最新在上)
- 09-07 v26:从 server.js 内嵌字符串抽取为独立模板(prompts/clarify.md)。规则内容与 09-07 四项修复(收尾门槛 4 项至少 3 项、候选具体展开「骨架——展开」、追问问题独占一段、必须出 ==OPTS==)一致,无行为变更。
@@ -19,6 +19,32 @@ const ROOT = __dirname;
const PUBLIC_DIR = path.join(ROOT, 'public'); const PUBLIC_DIR = path.join(ROOT, 'public');
const CONFIG_FILE = path.join(ROOT, 'config.json'); const CONFIG_FILE = path.join(ROOT, 'config.json');
// 09-07:prompt 模板文件化(prompts/*.md)——规则迭代只改文件不碰代码;文件缺失/损坏回退 null,调用方自行兜底。
// 模板正文取 ==PROMPT== 与 ==CHANGELOG== 之间(头部说明/文末变更记录不发给模型)
const PROMPTS_DIR = path.join(ROOT, 'prompts');
const _promptCache = new Map();
function loadPrompt(name) {
try {
const p = path.join(PROMPTS_DIR, name.endsWith('.md') ? name : name + '.md');
const st = fs.statSync(p);
const cached = _promptCache.get(p);
if (cached && cached.mtimeMs === st.mtimeMs) return cached.text;
let text = fs.readFileSync(p, 'utf8');
// 定位「行首独立标记」==PROMPT== / ==CHANGELOG==(避免头部说明里出现同名字样被误切)
const lineStart = (s, tag) => {
const re = new RegExp('(^|\\n)\\s*' + tag + '\\s*(\\n|$)', 'm');
const m = re.exec(s);
return m ? m.index + m[0].indexOf(tag) : -1;
};
const a = lineStart(text, '==PROMPT==');
const b = lineStart(text, '==CHANGELOG==');
if (a >= 0) text = b > a ? text.slice(a + '==PROMPT=='.length, b) : text.slice(a + '==PROMPT=='.length);
text = text.replace(/^\s*\n/, '').replace(/\s+$/, '');
_promptCache.set(p, { mtimeMs: st.mtimeMs, text });
return text;
} catch (e) { return null; }
}
// ---- 默认根目录:桌面/MCNSkill项目 ---- // ---- 默认根目录:桌面/MCNSkill项目 ----
const DEFAULT_ROOT = path.join(os.homedir(), 'Desktop', 'MCNSkill项目'); const DEFAULT_ROOT = path.join(os.homedir(), 'Desktop', 'MCNSkill项目');
@@ -867,34 +893,22 @@ const server = http.createServer((req, res) => {
} catch (e) { /* 脚本不存在则走 env */ } } catch (e) { /* 脚本不存在则走 env */ }
} }
if (!apiKey) return sendErr(500, '未配置 DIFY_MCN_CYLG_KEY(环境变量或技能脚本内置 KEY)'); if (!apiKey) return sendErr(500, '未配置 DIFY_MCN_CYLG_KEY(环境变量或技能脚本内置 KEY)');
// 3) 构造打磨 query(无状态:携带当前需求 + 最近对话,让 AI 输出 reply + ==REQ== 结构化段) // 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 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 histText = his.map((h) => '你:' + String(h.a || '') + '\n达人:' + String(h.u || '')).join('\n');
const reqMdText = String(reqMd || '').trim(); const reqMdText = String(reqMd || '').trim();
const attachText = String(attach || '').trim().slice(0, 20000); const attachText = String(attach || '').trim().slice(0, 20000);
const query = '你是资深短视频编导,正在创作前通过多轮对话,帮达人「' + accountName + '」把创作需求打磨清楚。' const tpl = loadPrompt('clarify');
+ '你的职责只限需求澄清、整理与追问;严禁生成脚本正文、台词、分镜或完整故事。\n\n' if (!tpl) return sendErr(500, '缺少 prompts/clarify.md 需求打磨模板');
+ '【账号设定摘要】\n' + (personaText || '(暂无可用账号设定,按通用编导经验澄清)') + '\n' const query = tpl
+ (contentText ? '【账号定位】\n' + contentText + '\n' : '') .replace('{{accountName}}', accountName)
+ (attachText ? '【附件素材】(达人上传的需求文档/资料,打磨与最终创作都须充分参考;把其中的关键诉求/约束吸收进需求清单)\n' + attachText + '\n' : '') .replace('{{personaText}}', personaText || '(暂无可用账号设定,按通用编导经验澄清)')
+ '【当前需求清单】\n' + (reqMdText || '(空:等待用户先给出初步想法)') + '\n' .replace('{{contentText}}', contentText ? '【账号定位】\n' + contentText : '')
+ (histText ? '【对话历史】\n' + histText + '\n' : '') .replace('{{attachText}}', attachText ? '【附件素材】(达人上传的需求文档/资料,打磨与最终创作都须充分参考;把其中的关键诉求/约束吸收进需求清单)\n' + attachText : '')
+ '【达人本轮输入】\n' + turn.trim() + '\n\n' .replace('{{reqMdText}}', reqMdText || '(空:等待用户先给出初步想法)')
+ '【行为要求】\n' .replace('{{histText}}', histText ? '【对话历史】\n' + histText : '')
+ '1. 理解本轮输入:可能是新想法、对上一问的回答、或想修改某一维度。将信息吸收进需求清单,可改写得更专业准确,但保留用户原意与账号结构性符号。\n' .replace('{{turnText}}', turn.trim());
+ '2. 若关键维度仍有缺口且本轮输入不足以补齐:只追问 1 个问题,不要一次抛出多个,也不要重复已明确的维度。问题必须单独另起一段(前后空行),便于界面突出显示;且必须给出可点选的候选(==OPTS==),供达人一键选用,严禁让达人自己空想选项。\n'
+ '3. 候选生成规则:选项骨架须受控(来自以下词表),但每项必须结合本条视频的创意/人设给出「落到这条视频里的具体形态」——即选项 = 骨架词 + 一句话怎么拍/什么内容,让达人一眼知道选了会得到什么,而不是干巴巴的术语。\n'
+ ' - 叙事结构未定 → 骨架:钩子前置 / 悬念反转 / 递进铺垫 / 先抑后扬,每项后缀具体展开(如:钩子前置——开场 3 秒就用「男保姆被娃折腾」制造笑点;悬念反转——结尾突然揭晓妈妈一直在偷看)。\n'
+ ' - 内容形式(叙事形态)未定 → 骨架:剧情(场次叙事,多角色对白)/ Vlog 纪实·人物采访(第一视角)/ 口播(干货·教程·盘点)/ 视觉短片(画面+音乐驱动,测评种草或纯氛围,台词极少),后缀结合本条内容给画面特征。\n'
+ ' - 达人倾向剧情演绎但人物与关系未明 → 骨架:主要人物身份 / 人物间关系(如:父子 / 夫妻 / 同事 / 陌生冲突),每项后缀结合已有创意给关系落点,一次只问一个层面。\n'
+ '4. 收尾门槛:仅当以下 4 项中至少 3 项已明确时才可总结收尾——一句话创意、创新/爆点、痛点/爽点、叙事结构(含内容形式)。未达门槛前,即使达人本轮回答已吸收,也须继续补齐最关键的缺口(把缺口本身作为问题+候选追问);已明确项可从达人输入或账号设定合理推断,但推断项须在回复中点明「我按账号设定帮你定了XX,如不对可点重置改」。\n'
+ '5. 输出三段结构,顺序固定:\n'
+ ' 第一段:对用户说的话(150 字以内)。需追问时:先一句确认本轮吸收的内容,空一行后单独输出问题(问题独占一段,不要与解释挤在一行);无需追问时:总结已明确项并提示「可直接点击右下角 ✓ 提交创作」;\n'
+ ' 若本轮提供了候选:另起一行单独输出 ==OPTS==,每行一个候选,格式为「骨架词——具体展开」(≤45 字、不含编号前缀),达人点选后整行即作为其回复;若未提供候选则不输出该段;\n'
+ ' 最后另起一行单独输出 ==REQ==,接着输出 5 行需求清单(仍未确定的项留空值,严禁编造;情节/结尾/时长不由本清单承载,由创作阶段按人设与选题自决;共鸣/共情亦不收集,创作时由 AI 依痛点 / 爽点自然发挥),模板:\n'
+ '- 一句话创意:\n- 创新 / 爆点:\n- 痛点 / 爽点:\n- 叙事结构:\n- 其他约束:\n'
+ ' 达人点击候选后,把该选择吸收进对应字段(如内容形式选「视觉短片」→ 写入叙事结构行并注明画面驱动;人物选择 → 写入一句话创意行的事件或人物信息)。\n'
+ '6. 需求框只收集本条视频的选题相关信息;账号级设定(情绪基调 / 目标受众 / 结构性符号 / 广告植入等)已由账号设定自动带入创作,不要向达人重复询问这些维度——仅当达人主动提出与账号设定不同的要求时,将其记入「其他约束」。';
// 4) Dify blocking 调用 // 4) Dify blocking 调用
const difyUrl = String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1').replace(/\/+$/, ''); const difyUrl = String(process.env.DIFY_URL || 'https://mydify.youmanvideo.com/v1').replace(/\/+$/, '');
const ctrl = new AbortController(); const ctrl = new AbortController();