接通主角绑定、开场封印与游玩辅助,让世界模拟器从创作连续落到本局。

直聘插入按模块回馈,并记录请求骨架便于排查。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-22 07:40:08 +08:00
co-authored by Cursor
parent 9306c8e659
commit dd0a9e87d8
88 changed files with 6869 additions and 786 deletions
+210
View File
@@ -0,0 +1,210 @@
/**
* 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;
}