工作台:需求打磨 prompt 文件化沉淀(prompts/clarify.md+模板注入+契约文档)
This commit is contained in:
1 parent
89b453ef6a
commit
c3727d8e6f
3 files changed
+109
-24
No files matched your search
@@ -19,6 +19,32 @@ const ROOT = __dirname;
|
||||
const PUBLIC_DIR = path.join(ROOT, 'public');
|
||||
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项目 ----
|
||||
const DEFAULT_ROOT = path.join(os.homedir(), 'Desktop', 'MCNSkill项目');
|
||||
|
||||
@@ -867,34 +893,22 @@ const server = http.createServer((req, res) => {
|
||||
} catch (e) { /* 脚本不存在则走 env */ }
|
||||
}
|
||||
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 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 query = '你是资深短视频编导,正在创作前通过多轮对话,帮达人「' + accountName + '」把创作需求打磨清楚。'
|
||||
+ '你的职责只限需求澄清、整理与追问;严禁生成脚本正文、台词、分镜或完整故事。\n\n'
|
||||
+ '【账号设定摘要】\n' + (personaText || '(暂无可用账号设定,按通用编导经验澄清)') + '\n'
|
||||
+ (contentText ? '【账号定位】\n' + contentText + '\n' : '')
|
||||
+ (attachText ? '【附件素材】(达人上传的需求文档/资料,打磨与最终创作都须充分参考;把其中的关键诉求/约束吸收进需求清单)\n' + attachText + '\n' : '')
|
||||
+ '【当前需求清单】\n' + (reqMdText || '(空:等待用户先给出初步想法)') + '\n'
|
||||
+ (histText ? '【对话历史】\n' + histText + '\n' : '')
|
||||
+ '【达人本轮输入】\n' + turn.trim() + '\n\n'
|
||||
+ '【行为要求】\n'
|
||||
+ '1. 理解本轮输入:可能是新想法、对上一问的回答、或想修改某一维度。将信息吸收进需求清单,可改写得更专业准确,但保留用户原意与账号结构性符号。\n'
|
||||
+ '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. 需求框只收集本条视频的选题相关信息;账号级设定(情绪基调 / 目标受众 / 结构性符号 / 广告植入等)已由账号设定自动带入创作,不要向达人重复询问这些维度——仅当达人主动提出与账号设定不同的要求时,将其记入「其他约束」。';
|
||||
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(/\/+$/, '');
|
||||
const ctrl = new AbortController();
|
||||
|
||||
Reference in new issue
Block a user