Files
writing-agent/src/dictate/insert-feedback.ts
T
chenmoranzhiandCursor dd0a9e87d8 接通主角绑定、开场封印与游玩辅助,让世界模拟器从创作连续落到本局。
直聘插入按模块回馈,并记录请求骨架便于排查。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-22 07:40:08 +08:00

211 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* insert(tag) → 反查能力 → 独立「回复模块」规范,塞进 tool result。
* 一轮可多次 insert:每个模块各自一份;末尾回复须分段全部附带。
* 不照搬 schema / mount;只抽 probe、交互原则、自评关注点。
*/
import {
getModuleSection,
loadModulePrompt,
parseModulePromptSections,
type ModuleCatalog,
type ModuleCatalogEntry,
} from "../skills/creation-flow.js";
/** 对话落盘常用简化 tag → 能力 id(catalog.artifact 之外) */
export const DICTATE_INSERT_TAG_ALIASES: Readonly<Record<string, string>> = {
"设计.开场白": "opening-setup",
"设计.美学纲领": "aesthetics-interaction",
"设计.文风": "narrative-guide",
"设计.监控栏": "status-bar",
};
/** 多模块同轮:写进每条 insert tool result 的 reply_hint */
export const DICTATE_MULTI_MODULE_REPLY_HINT =
"本轮带回了多个 reply_module。末尾可见回复:每个模块各写一块 markdown,标题用 ## 模块 · {名称}(与该 reply_module 一致);块间空一行;每块只写该模块的确认 / 薄弱点 / 追问。全部模块写完后,可另起一段写 1 条「建议下一刀」(条件须已接近成立)。缺某一块则补上后再结束本轮。";
const MAX_PROBE_CHARS = 1200;
const MAX_PRINCIPLES_CHARS = 900;
const MAX_BRIEF_CHARS = 2800;
export type DictateInsertFeedback = {
moduleId: string;
moduleName: string;
tag: string;
/** 给模型的独立模块 markdown */
text: string;
};
export function resolveModuleForInsertTag(
tag: string,
catalog: ModuleCatalog | null | undefined,
): ModuleCatalogEntry | null {
const t = tag.trim();
if (!t || !catalog?.modules?.length) return null;
const byArtifact = catalog.modules.find((m) => m.artifact?.trim() === t);
if (byArtifact) return byArtifact;
// 可增殖拆分:设计.生成规则#slug → 仍反查 设计.生成规则
const hash = t.indexOf("#");
if (hash > 0) {
const family = t.slice(0, hash).trim();
const byFamily = catalog.modules.find((m) => m.artifact?.trim() === family);
if (byFamily) return byFamily;
}
const aliasId = DICTATE_INSERT_TAG_ALIASES[t];
if (aliasId) {
return catalog.modules.find((m) => m.id === aliasId) ?? null;
}
return null;
}
/** 从 output 块里抽自评维度名(用户向;不交 JSON) */
export function extractSelfScoreDimensionNames(outputFence: string): string[] {
const names: string[] = [];
const re = /"名"\s*:\s*"([^"]+)"/g;
let m: RegExpExecArray | null;
while ((m = re.exec(outputFence)) !== null) {
const name = m[1]!.trim();
if (name && !names.includes(name)) names.push(name);
}
return names.slice(0, 8);
}
function clip(text: string, max: number): string {
const t = text.trim();
if (t.length <= max) return t;
return `${t.slice(0, max - 1)}…`;
}
/**
* 从能力 prompt.md 抽「回答时附带本模块」的 markdown(无匹配块则空串)。
*/
export function formatDictateUserFacingBrief(params: {
tag: string;
module: ModuleCatalogEntry;
promptMd: string;
}): string {
const sections = parseModulePromptSections(params.promptMd);
const probe = getModuleSection(sections, "probe");
const principles = getModuleSection(sections, "principles");
const output = getModuleSection(sections, "output");
const dims = output ? extractSelfScoreDimensionNames(output) : [];
if (!probe && !principles && dims.length === 0) return "";
const includes: string[] = [];
if (probe) includes.push("探测与追问口径");
if (principles) includes.push("交互原则");
if (dims.length) includes.push(`自评关注(${dims.join("、")})`);
const parts: string[] = [
`# 模块 · ${params.module.name}`,
"",
`回答时请附带本模块(落盘 tag:\`${params.tag}\`)。`,
"",
"本模块内容包括:",
...includes.map((x) => `- ${x}`),
"",
"## 写入用户回复时做什么",
`1. 用二级标题 \`## 模块 · ${params.module.name}\` 开一段(多模块时本段只写本模块)。`,
"2. 确认:2~4 句复述本模块已落要点,让用户能判断对不对。",
"3. 薄弱点:对照下方「自评关注」口头点 1 处不够硬的地方(不必交打分表或 JSON)。",
"4. 追问:1~2 点;每点给建议选项或短场景,用户可直接采用或改写。材料已够则追问可空,改写「建议下一刀」。",
"5. 不粘贴产物全文;不复述 tool JSON。",
];
if (probe) {
parts.push("", "## 探测与追问(本模块口径)", clip(probe, MAX_PROBE_CHARS));
}
if (principles) {
parts.push("", "## 交互原则(摘)", clip(principles, MAX_PRINCIPLES_CHARS));
}
if (dims.length) {
parts.push("", "## 自评关注", dims.map((d) => `- ${d}`).join("\n"));
}
return clip(parts.join("\n"), MAX_BRIEF_CHARS);
}
export function buildDictateInsertFeedback(params: {
tag: string;
catalog: ModuleCatalog | null | undefined;
promptMd: string | null | undefined;
}): DictateInsertFeedback | null {
const module = resolveModuleForInsertTag(params.tag, params.catalog);
if (!module || !params.promptMd?.trim()) return null;
const text = formatDictateUserFacingBrief({
tag: params.tag.trim(),
module,
promptMd: params.promptMd,
});
if (!text.trim()) return null;
return {
moduleId: module.id,
moduleName: module.name,
tag: params.tag.trim(),
text,
};
}
/**
* 预加载 catalog 全部能力的 insert 反馈索引(tag → 模块 markdown)。
* 含 artifact 与别名 key。
*/
export async function buildDictateInsertFeedbackIndex(params: {
catalog: ModuleCatalog;
skillPackRoot: string;
skillsRoot?: string;
}): Promise<Map<string, string>> {
const index = new Map<string, string>();
const { catalog, skillPackRoot } = params;
await Promise.all(
catalog.modules.map(async (module) => {
const promptMd = await loadModulePrompt(
skillPackRoot,
module.id,
params.skillsRoot,
);
if (!promptMd) return;
const artifact = module.artifact?.trim();
if (artifact) {
const fb = buildDictateInsertFeedback({
tag: artifact,
catalog,
promptMd,
});
if (fb) index.set(artifact, fb.text);
}
for (const [aliasTag, moduleId] of Object.entries(
DICTATE_INSERT_TAG_ALIASES,
)) {
if (moduleId !== module.id) continue;
const fb = buildDictateInsertFeedback({
tag: aliasTag,
catalog,
promptMd,
});
if (fb) index.set(aliasTag, fb.text);
}
}),
);
return index;
}
export function lookupDictateInsertFeedback(
index: Map<string, string> | null | undefined,
tag: string,
): string | undefined {
const t = tag.trim();
if (!t || !index?.size) return undefined;
const direct = index.get(t);
if (direct) return direct;
const hash = t.indexOf("#");
if (hash > 0) return index.get(t.slice(0, hash).trim());
return undefined;
}