增强进料配方注入,并拆分转述侧创作与跑团体验配方。

让文本生成器与数值跑团各走清晰进料路径,开玩后槽位与分工自动对齐。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-10 02:18:23 +08:00
co-authored by Cursor
parent 3d720f23fe
commit 92dd412e14
15 changed files with 292 additions and 36 deletions
+35 -8
View File
@@ -27,6 +27,10 @@ import {
type PlaySlotsConfig,
} from "../skills/play-slots.js";
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
import {
CREATION_SELECTED_RECIPE_TAG,
parseSelectedRecipeRef,
} from "../skills/creation-flow.js";
import {
VARIABLE_CATALOG_TAG,
} from "../skills/variable-catalog.js";
@@ -48,12 +52,17 @@ const SKIP_BIND_TAGS = new Set([
"设计.worker集.草稿",
CONTEXT_ORDER_TAG,
"创作.进料模式",
CREATION_SELECTED_RECIPE_TAG,
VARIABLE_CATALOG_TAG,
VALUE_MAP_TAG,
]);
/** 偏文风/呈现:有叙事转述时挂转述;否则仍挂主世界层 */
const STYLE_TAG_RE = /叙事指南|文风|美学|纲领|禁忌|示例|正文组成|回复格式|监控栏|篇幅|结构/;
const STYLE_TAG_RE =
/叙事指南|文风|美学|纲领|禁忌|示例|模仿|正文组成|回复格式|监控栏|篇幅|结构/;
/** 这些配方开玩时刚需叙事转述(主世界出内容、转述出文风) */
const RECIPES_REQUIRE_NARRATOR = new Set(["文本生成器"]);
export type DictatePlayBindResult = {
workerSetJson: string;
@@ -185,7 +194,7 @@ function buildNarratorInserts(products: DictateProduct[]): ContextOrderInsert[]
if (!isStyleProductTag(p.tag)) continue;
inserts.push(insert(o++, p.tag, "full", "文风/呈现参考"));
}
inserts.push(insert(o++, "运行.本轮.裁决", "full"));
inserts.push(insert(o++, "输出.用户展示", "full"));
return inserts;
}
@@ -204,7 +213,7 @@ function buildAuditorInserts(params: {
for (const tag of params.runtimeTags) {
inserts.push(insert(o++, tag, tag === "变量.当前" ? "fields" : "full"));
}
inserts.push(insert(o++, "运行.本轮.裁决", "full"));
inserts.push(insert(o++, "输出.用户展示", "full"));
inserts.push(insert(o++, "用户.最新输入", "full"));
return inserts;
}
@@ -261,7 +270,7 @@ export function buildDictateContextOrder(params: {
inserts: [
insert(0, WORKER_PERSONA_REF, "fixed", "槽位人设"),
insert(1, "用户.最新输入", "full"),
insert(2, "运行.本轮.裁决", "full"),
insert(2, "输出.用户展示", "full"),
],
});
}
@@ -275,15 +284,27 @@ export function buildDictateContextOrder(params: {
});
}
function resolvePlaySlots(existingRaw: string | undefined): PlaySlotsConfig {
function resolvePlaySlots(
existingRaw: string | undefined,
board: Blackboard,
): PlaySlotsConfig {
const parsed = existingRaw?.trim()
? parseWorkerSetYaml(existingRaw)
: null;
return parsed?.play_slots ?? defaultPlaySlots();
const slots = { ...(parsed?.play_slots ?? defaultPlaySlots()) };
const recipeRef = parseSelectedRecipeRef(
board.getContentByTag(CREATION_SELECTED_RECIPE_TAG),
);
if (recipeRef && RECIPES_REQUIRE_NARRATOR.has(recipeRef)) {
slots.gm = true;
slots.narrator = true;
}
return slots;
}
/**
* 生成/更新 设计.worker集:保留已有 play_slots,重写 context_order 为产物挂载表。
* 文本生成器等配方会强制打开叙事转述。
*/
export function bindDictateProductsToPlaySpec(board: Blackboard): DictatePlayBindResult {
const products = collectDictateBindProducts(board);
@@ -291,7 +312,7 @@ export function bindDictateProductsToPlaySpec(board: Blackboard): DictatePlayBin
board.getContentByTag(WORKER_SET_FINAL_TAG)?.trim() ||
board.getContentByTag(WORKER_SET_DRAFT_TAG)?.trim() ||
"";
const playSlots = resolvePlaySlots(existing || undefined);
const playSlots = resolvePlaySlots(existing || undefined, board);
const runtimeTags = collectRuntimeStateTags(board);
const contextOrder = buildDictateContextOrder({
products,
@@ -320,7 +341,13 @@ export function bindDictateProductsToPlaySpec(board: Blackboard): DictatePlayBin
} catch {
row = { play_slots: playSlots, brief: "Boss直聘落档自动生成" };
}
if (!row.play_slots) row.play_slots = playSlots;
row.play_slots = playSlots;
if (playSlots.narrator) {
row.brief =
typeof row.brief === "string" && String(row.brief).includes("转述")
? row.brief
: "Boss直聘:主世界出内容,叙事转述出文风";
}
const withSlots = JSON.stringify(row, null, 2);
const workerSetJson = mergeContextOrderIntoWorkerSetJson(withSlots, contextOrder);
+6 -1
View File
@@ -14,8 +14,9 @@ export function buildDictateSystemPrompt(opts?: {
? `
## 本局配方
名称:${opts.recipeName ?? "(未命名)"}
${opts.recipeBrief ? `要点:${opts.recipeBrief}` : ""}
${opts.recipeBrief ? `要点:\n${opts.recipeBrief}` : ""}
按上述适用与原则整理产物;材料只够做本配方时,就只做本配方范围内的事。
本配方若含跨轮数值:优先走 D(declare_variable / declare_map),不要只 insert 散文表。
`
: "";
@@ -45,6 +46,7 @@ ${opts.recipeBrief ? `要点:${opts.recipeBrief}` : ""}
**分清:**
- 正文组成(格式)→ insert \`设计.正文组成\`
- 开场白(内容)→ insert \`设计.开场白\`;落档/开玩同步 \`输出.开场白\`
- 多篇无关联短文(生产在游玩)→ 创作期钉 \`设计.模仿范例\` / \`设计.模仿要点\` / \`设计.叙事指南与故事推进\`(大纲扩写)
- 好感分档性格等 → declare_variable + declare_map,不要 insert 一整张表进设定散文
${recipeBlock}
## 按用户本轮意图择一为主
@@ -60,6 +62,9 @@ insert 设定类;order 偏小。回复:确认体验 + 建议补充。
### D · 钉变量与映射
用户提到跨轮状态、分档态度、章大纲切换时:declare_variable(含是否对用户可见)→ declare_map(分档正文)。可同轮多次调用。
### E · 文本生成器(创作收料 → 开玩挂载)
必须用 insert 写齐游玩上下文:\`设计.模仿范例\`、\`设计.模仿要点\`、\`设计.叙事指南与故事推进\`(用户输入用法:大纲扩写;禁止扮演停笔)。缺任一就先补产物,不要说可以开玩。禁止在创作期写完整短文。开玩后程序只挂载这些产物:主世界出内容,转述出文风。
## 常用序感
- 需求/美学等不变设定:靠前(负)
- 正文组成:中间(0)
+1 -1
View File
@@ -48,7 +48,7 @@ export type DictateTurnResult = {
};
/**
* 跑一轮 Boss 直聘整理:上下文 = 预设提示 + 按相对序的产物 + 全量对话。
* 跑一轮 Boss 直聘整理:任务 messages 由本模块拼;preset 夹心由 LLM 包装层统一装配。
*/
export async function runDictateTurn(params: {
llm: LlmProvider;
+3 -1
View File
@@ -38,8 +38,10 @@ export function defaultDictateOrder(tag: string): number {
const t = tag.trim();
if (t === "用户.需求") return -40;
if (t === "设计.变量目录" || t === "设计.变量映射") return -25;
if (/美学|纲领|禁忌|示例/.test(t)) return -30;
if (t === "设计.模仿范例" || t === "设计.模仿要点") return -28;
if (/美学|纲领|禁忌|示例|模仿/.test(t)) return -30;
if (/文风|叙事指南|篇幅|结构/.test(t)) return -20;
if (/短文集|短文\./.test(t)) return 10;
if (/正文组成|回复格式/.test(t)) return 0;
if (/开场白/.test(t)) return 20;
return 0;
+2
View File
@@ -54,6 +54,8 @@ const SKILL_PACK_LABELS: Record<string, string> = {
"expand-assistant": "扩写助手",
文本生成器: "文本生成器",
"text-generator": "文本生成器",
交互式长文生成器: "交互式长文生成器",
数据化跑团体验: "数据化跑团体验",
};
/** 生命周期 / 相位 */
+36 -3
View File
@@ -109,6 +109,7 @@ import {
creationFlowFromRecipeSeed,
findRecipeCatalogEntry,
formatCreationFlowForUser,
formatDictateRecipeBrief,
findModuleByName,
findStepByRef,
type FlowStepTitleHint,
@@ -682,14 +683,17 @@ export class SessionManager {
s.runtime.getBlackboard(),
s.recipeOptions,
);
const recipeBrief = await resolveDictateRecipeBriefForSession({
skillName: s.runtime.getActiveSkill()?.name,
recipeView,
recipeOptions: s.recipeOptions,
});
const result = await runDictateTurn({
llm,
dialogue,
recipeName: recipeView?.name,
recipeBrief: recipeView
? [recipeView.declaration].filter(Boolean).join(";")
: undefined,
recipeBrief,
handlers: {
listProducts: () => this.listDictateProducts(s),
writeProduct: (tag, content, order) => {
@@ -3586,6 +3590,35 @@ function resolveSelectedRecipeView(
return { id: ref, name: ref, declaration: "" };
}
/** Boss 直聘:优先注入 recipe.yaml 方法论,缺则退回 catalog declaration */
async function resolveDictateRecipeBriefForSession(params: {
skillName?: string | null;
recipeView: { id: string; name: string; declaration: string } | null;
recipeOptions?: RecipeCatalogEntry[];
}): Promise<string | undefined> {
const view = params.recipeView;
if (!view) return undefined;
const entry = findRecipeCatalogEntry(
params.recipeOptions?.length ? { recipes: params.recipeOptions } : null,
view.id,
);
const skillName = params.skillName?.trim();
if (entry && skillName) {
try {
const skill = await loadSkill(skillName);
const packRoot = skill.skillPackRoot?.trim();
if (packRoot) {
const detail = await loadRecipeDetail(packRoot, entry);
const brief = formatDictateRecipeBrief(detail);
if (brief) return brief;
}
} catch (err) {
console.error("[会话] 加载转述配方要点失败", err);
}
}
return view.declaration?.trim() || undefined;
}
function buildWorkerSetUserView(
blackboard: Blackboard,
session?: RuntimeSession,
+40 -1
View File
@@ -247,6 +247,8 @@ export type RecipeDetail = {
id: string;
name: string;
declaration: string;
/** 一句话方法摘要(recipe.yaml brief;转述进料优先注入) */
brief?: string;
/** 适用什么体验/任务 */
when?: string;
/** 整套设计方法的核心思路与最终目标 */
@@ -1433,8 +1435,12 @@ export function parseRecipeYaml(
? row.name.trim()
: meta.name;
const brief =
typeof row.brief === "string" && row.brief.trim()
? row.brief.trim()
: undefined;
const seedParsed = parseCreationFlow(JSON.stringify({
brief: typeof row.brief === "string" ? row.brief : undefined,
brief,
status: "open",
steps: Array.isArray(row.steps) ? row.steps : [],
}));
@@ -1444,6 +1450,7 @@ export function parseRecipeYaml(
id: meta.id,
name,
declaration: meta.declaration,
...(brief ? { brief } : {}),
when,
...(core ? { core } : {}),
...(process ? { process } : {}),
@@ -1551,6 +1558,38 @@ export function formatRecipeCatalogForAgent(catalog: RecipeCatalog): string {
return `【可选配方】(须由用户手动选择)\n${lines.join("\n")}`;
}
/**
* Boss 直聘 / 转述进料:把已选配方方法论收成短要点(进系统提示「本局配方」)。
* 不含 DAG steps / design-flow 选型话术。
*/
export function formatDictateRecipeBrief(detail: RecipeDetail): string {
const lines: string[] = [];
if (detail.brief?.trim()) lines.push(detail.brief.trim());
else if (detail.declaration?.trim()) lines.push(detail.declaration.trim());
if (detail.when?.trim()) lines.push(`适用:${detail.when.trim()}`);
if (detail.core?.trim()) {
lines.push("核心:");
lines.push(detail.core.trim());
}
if (detail.process) {
lines.push("怎么做:");
lines.push(formatRecipeFieldBlock(detail.process));
}
if (detail.principles) {
lines.push("原则:");
lines.push(formatRecipeFieldBlock(detail.principles));
}
if (
!detail.core &&
!detail.process &&
!detail.principles &&
detail.hint?.trim()
) {
lines.push(detail.hint.trim());
}
return lines.join("\n").trim();
}
/** 注入 design-flow:用户已选配方(方法论 + 开局起点) */
export function formatSelectedRecipeForAgent(detail: RecipeDetail): string {
const lines: string[] = [