把代入名片从开场白拆到主角设定短码槽,并收紧对话落盘写法。

多开场可各自绑定名片;开玩挂载跳过开场产物并认用户需求类标签。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-24 00:59:05 +08:00
co-authored by Cursor
parent da459fb34c
commit a4ad552aff
17 changed files with 417 additions and 58 deletions
+2
View File
@@ -18,6 +18,8 @@ describe("dictate play-bind", () => {
expect(isDictateStyleBindTag("设计.模仿要点")).toBe(true);
expect(isDictateStyleBindTag("设计.叙事指南")).toBe(true);
expect(isDictateStyleBindTag("设计.故事推进")).toBe(true);
expect(isDictateStyleBindTag("设计.用户需求")).toBe(true);
expect(isDictateStyleBindTag("设计.用户约束")).toBe(true);
expect(isDictateStyleBindTag("用户.需求")).toBe(false);
});
+30
View File
@@ -259,6 +259,36 @@ describe("dictate context", () => {
});
expect("error" in overwriteBare).toBe(true);
const openingFamilies = new Set(["设计.开场白", "设计.主角设定"]);
const firstOpen = resolveRepeatableInsertTag({
tag: "设计.开场白",
content: "@玩家推开门。",
existingTags: [],
repeatableFamilies: openingFamilies,
});
expect("tag" in firstOpen && firstOpen.tag).toBe("设计.开场白");
const overwriteFirst = resolveRepeatableInsertTag({
tag: "设计.开场白",
content: "@玩家改写第一幕。",
existingTags: ["设计.开场白"],
repeatableFamilies: openingFamilies,
});
expect("tag" in overwriteFirst && overwriteFirst.tag).toBe("设计.开场白");
const secondOpen = resolveRepeatableInsertTag({
tag: "设计.开场白#gate",
content: "@玩家在闸机前。",
existingTags: ["设计.开场白"],
repeatableFamilies: openingFamilies,
});
expect("tag" in secondOpen && secondOpen.tag).toBe("设计.开场白#gate");
const bareOpenConflict = resolveRepeatableInsertTag({
tag: "设计.开场白",
content: "又写一条",
existingTags: ["设计.开场白", "设计.开场白#gate"],
repeatableFamilies: openingFamilies,
});
expect("error" in bareOpenConflict).toBe(true);
const catalog = await loadModuleCatalog("dialogue/world-simulator");
const index = await buildDictateInsertFeedbackIndex({
catalog: catalog!,
+18
View File
@@ -175,6 +175,24 @@ describe("opening-seal", () => {
expect(dotted.text).toBe("门外有人喊 @玩家。");
});
it("strips bare 用户角色 meta lines without YAML fence", () => {
const bare = splitOpeningDocument(`用户角色.名字: 李宗
用户角色.简介: 18cm 男大学生,家境贫穷
……@玩家推开宿舍门。
`);
expect(bare.persona).toEqual({
name: "李宗",
description: "18cm 男大学生,家境贫穷",
});
expect(bare.text).toBe("……@玩家推开宿舍门。");
const onlyMeta = splitOpeningDocument(`用户角色.名字: 李宗
用户角色.简介: 男大学生
`);
expect(onlyMeta.persona?.name).toBe("李宗");
expect(onlyMeta.text).toBe("");
});
it("builds table rows from opening variables", () => {
const payload = parseOpeningSealPayload(fragment, 0);
const doc = openingVariablesToTableDoc(payload!.variables);
+35
View File
@@ -73,4 +73,39 @@ describe("play-aux view", () => {
expect(view.openings[0]?.text).toBe("@玩家站在债契前,堂屋很静。");
expect(view.openings[0]?.text).not.toContain("present.v1");
});
it("lists dictate 设计.开场白#slot openings with same-slot 主角设定", () => {
const board = new Blackboard();
board.write({
tag: "设计.开场白#dorm",
content: "@玩家推开宿舍门。",
source: "agent",
});
board.write({
tag: "设计.开场白#gate",
content: "@玩家停在闸机前。",
source: "agent",
});
board.write({
tag: "设计.主角设定#dorm",
content: JSON.stringify({
正文: { 名字: "李宗", 背景: "男大学生", 特殊设定: { 身份: "无", 金手指: "无", 其它: "无" } },
}),
source: "agent",
});
board.write({
tag: "设计.主角设定#gate",
content: JSON.stringify({
正文: { 名字: "阿泽", 背景: "社畜", 特殊设定: { 身份: "无", 金手指: "无", 其它: "无" } },
}),
source: "agent",
});
const view = buildPlayAuxView(board, { [SLOT_OPENING_SELECTED_INDEX]: 0 });
expect(view.openings).toHaveLength(2);
expect(view.openings.map((o) => o.slot).sort()).toEqual(["dorm", "gate"]);
const dorm = view.openings.find((o) => o.slot === "dorm");
const gate = view.openings.find((o) => o.slot === "gate");
expect(dorm?.persona?.name).toBe("李宗");
expect(gate?.persona?.name).toBe("阿泽");
});
});
+48
View File
@@ -155,6 +155,54 @@ describe("protagonist overlay", () => {
});
});
it("prefers 设计.主角设定 over legacy opening meta", async () => {
const {
PROTAGONIST_TAG,
readSessionProtagonist,
ensureProtagonistFromPersona,
protagonistFragmentFromPersona,
protagonistTagForOpeningSlot,
} = await import("../src/persona/protagonist-bind.js");
const board = new Blackboard();
board.write({
tag: "设计.开场白",
content: `用户角色.名字: 旧名
用户角色.简介: 旧简介
@玩家推开门。`,
source: "agent",
});
board.write({
tag: PROTAGONIST_TAG,
content: protagonistFragmentFromPersona({
name: "李宗",
description: "男大学生",
}),
source: "agent",
});
expect(readSessionProtagonist(board)?.name).toBe("李宗");
const empty = new Blackboard();
expect(
ensureProtagonistFromPersona(empty, {
name: "阿泽",
description: "社畜",
}),
).toBe(true);
expect(readSessionProtagonist(empty)?.name).toBe("阿泽");
expect(
ensureProtagonistFromPersona(empty, {
name: "另一个",
description: "不覆盖",
}),
).toBe(false);
expect(protagonistTagForOpeningSlot("设计.开场白#dorm")).toBe(
"设计.主角设定#dorm",
);
expect(protagonistTagForOpeningSlot("设计.开场白")).toBe(PROTAGONIST_TAG);
});
it("overlays opening meta 用户角色 when the user switches to that preset", async () => {
const { readSessionProtagonist, overlayPlayPersona } = await import(
"../src/persona/protagonist-bind.js"