把代入名片从开场白拆到主角设定短码槽,并收紧对话落盘写法。
多开场可各自绑定名片;开玩挂载跳过开场产物并认用户需求类标签。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -19,6 +19,8 @@ import {
|
||||
} from "./present-packet.js";
|
||||
|
||||
export const OPENING_SETUP_ARTIFACT_TAG = "设计.开场白与开场变量";
|
||||
/** 对话落盘开场正文族(可 设计.开场白#槽位 增殖) */
|
||||
export const OPENING_PRODUCT_FAMILY = "设计.开场白";
|
||||
export const OPENING_OUTPUT_TAG = "输出.开场白";
|
||||
export const OPENING_INITIAL_VARS_TAG = "运行.初始变量";
|
||||
export const OPENING_CURRENT_VARS_TAG = "变量.当前";
|
||||
@@ -222,20 +224,75 @@ export function parseUserPersonaField(raw: unknown): OpeningUserPersona | null {
|
||||
return { name, description };
|
||||
}
|
||||
|
||||
/** 去掉文首用户角色 meta 行(含无 --- 篱笆的旧稿),避免 meta 进开场正文。 */
|
||||
export function stripLeadingPersonaMeta(raw: string): string {
|
||||
const lines = raw.split(/\r?\n/);
|
||||
let i = 0;
|
||||
while (i < lines.length && !lines[i]!.trim()) i++;
|
||||
let saw = false;
|
||||
let inUserRoleBlock = false;
|
||||
while (i < lines.length) {
|
||||
const line = lines[i]!;
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
if (saw) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (/^---+$/.test(trimmed) && saw) {
|
||||
i++;
|
||||
break;
|
||||
}
|
||||
if (/^用户角色\s*:\s*$/.test(trimmed)) {
|
||||
saw = true;
|
||||
inUserRoleBlock = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
inUserRoleBlock &&
|
||||
/^\s+(?:名字|简介|name|description|人设|描述)\s*[::]/.test(line)
|
||||
) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
inUserRoleBlock = false;
|
||||
if (/^(?:用户角色\.)?(?:名字|简介)[::]/.test(trimmed)) {
|
||||
saw = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return lines.slice(i).join("\n").trim();
|
||||
}
|
||||
|
||||
function splitYamlFrontmatter(raw: string): {
|
||||
text: string;
|
||||
persona: OpeningUserPersona | null;
|
||||
} {
|
||||
const text = raw.trim();
|
||||
const fence = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||
if (!fence) return { text, persona: parseOpeningUserPersona(text) };
|
||||
if (!fence) {
|
||||
const persona = parseOpeningUserPersona(text);
|
||||
if (!persona) return { text, persona: null };
|
||||
const stripped = stripLeadingPersonaMeta(text);
|
||||
// 能认出文首 meta 才剥;正文里偶然出现「名字:」不剥
|
||||
if (stripped !== text.trim()) return { text: stripped, persona };
|
||||
if (/^(?:用户角色\.|用户角色\s*:)/m.test(text)) {
|
||||
return { text: stripped, persona };
|
||||
}
|
||||
return { text, persona };
|
||||
}
|
||||
const persona =
|
||||
parseUserPersonaField(parseSimpleYamlMap(fence[1] ?? "")) ||
|
||||
parseOpeningUserPersona(fence[1] ?? "");
|
||||
return { text: (fence[2] ?? "").trim(), persona };
|
||||
}
|
||||
|
||||
/** 开场正文前的 YAML 式 meta:用户角色名字+简介,其余当正文。 */
|
||||
/** 开场正文与用户角色 meta 分离;meta 不应进「系统 开场白」。 */
|
||||
export function splitOpeningDocument(raw: string | null | undefined): {
|
||||
text: string;
|
||||
persona: OpeningUserPersona | null;
|
||||
|
||||
+56
-7
@@ -6,8 +6,17 @@ import {
|
||||
isTableCellEditable,
|
||||
parseTableDoc,
|
||||
} from "../blackboard/table-cells.js";
|
||||
import {
|
||||
dictateProductFamily,
|
||||
dictateProductSlot,
|
||||
} from "../dictate/repeatable-tags.js";
|
||||
import {
|
||||
DICTATE_ORDER_META_KEY,
|
||||
effectiveDictateOrder,
|
||||
} from "../dictate/types.js";
|
||||
import {
|
||||
OPENING_PERSONA_CHOICE_TAG,
|
||||
OPENING_PRODUCT_FAMILY,
|
||||
OPENING_SETUP_ARTIFACT_TAG,
|
||||
SLOT_OPENING_SELECTED_INDEX,
|
||||
parseOpeningSealPayload,
|
||||
@@ -15,7 +24,10 @@ import {
|
||||
type OpeningUserPersona,
|
||||
} from "./opening-seal.js";
|
||||
import { CURRENT_VARS_TAG } from "./variable-catalog.js";
|
||||
import { readSessionProtagonist } from "../persona/protagonist-bind.js";
|
||||
import {
|
||||
personaBoundToOpening,
|
||||
readSessionProtagonist,
|
||||
} from "../persona/protagonist-bind.js";
|
||||
import { presentReadablePlain } from "./present-packet.js";
|
||||
|
||||
export type PlayAuxTableRow = {
|
||||
@@ -29,6 +41,9 @@ export type PlayAuxTableRow = {
|
||||
|
||||
export type PlayAuxOpening = {
|
||||
index: number;
|
||||
/** 产物 tag:设计.开场白 或 设计.开场白#槽位;opening-setup 候选为假想 tag */
|
||||
tag: string;
|
||||
slot: string;
|
||||
text: string;
|
||||
persona: OpeningUserPersona | null;
|
||||
};
|
||||
@@ -51,7 +66,8 @@ function selectedOpeningIndex(slots: Record<string, unknown> | undefined): numbe
|
||||
return 0;
|
||||
}
|
||||
|
||||
function collectOpenings(board: Blackboard): PlayAuxOpening[] {
|
||||
/** 黑板上全部可选用开场(工序候选优先;否则对话落盘 设计.开场白[#槽]) */
|
||||
export function collectOpenings(board: Blackboard): PlayAuxOpening[] {
|
||||
const payload = parseOpeningSealPayload(
|
||||
board.getContentByTag(OPENING_SETUP_ARTIFACT_TAG),
|
||||
0,
|
||||
@@ -62,18 +78,50 @@ function collectOpenings(board: Blackboard): PlayAuxOpening[] {
|
||||
board.getContentByTag(OPENING_SETUP_ARTIFACT_TAG),
|
||||
index,
|
||||
);
|
||||
const tag = `${OPENING_SETUP_ARTIFACT_TAG}#${index + 1}`;
|
||||
return {
|
||||
index,
|
||||
tag,
|
||||
slot: String(index + 1),
|
||||
text: presentReadablePlain(text) || text,
|
||||
persona: one?.persona ?? payload.persona,
|
||||
};
|
||||
});
|
||||
}
|
||||
const split = splitOpeningDocument(board.getContentByTag("设计.开场白"));
|
||||
if (split.text) {
|
||||
return [{ index: 0, text: split.text, persona: split.persona }];
|
||||
|
||||
const rows: Array<{
|
||||
tag: string;
|
||||
content: string;
|
||||
order: number;
|
||||
}> = [];
|
||||
for (const e of board.listTagIndex()) {
|
||||
if (dictateProductFamily(e.tag) !== OPENING_PRODUCT_FAMILY) continue;
|
||||
const item = board.getLatestByTag(e.tag);
|
||||
const content = item?.content?.trim() ?? "";
|
||||
if (!content) continue;
|
||||
const rawOrder = item?.metadata?.[DICTATE_ORDER_META_KEY];
|
||||
const order =
|
||||
typeof rawOrder === "number" && Number.isFinite(rawOrder)
|
||||
? rawOrder
|
||||
: effectiveDictateOrder({ tag: e.tag });
|
||||
rows.push({ tag: e.tag, content, order });
|
||||
}
|
||||
return [];
|
||||
rows.sort((a, b) => {
|
||||
if (a.order !== b.order) return a.order - b.order;
|
||||
return a.tag.localeCompare(b.tag, "zh");
|
||||
});
|
||||
|
||||
return rows.map((row, index) => {
|
||||
const split = splitOpeningDocument(row.content);
|
||||
const text = split.text.trim() || row.content;
|
||||
return {
|
||||
index,
|
||||
tag: row.tag,
|
||||
slot: dictateProductSlot(row.tag),
|
||||
text: presentReadablePlain(text) || text,
|
||||
persona: personaBoundToOpening(board, row.tag, row.content),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function buildPlayAuxView(
|
||||
@@ -100,6 +148,7 @@ export function buildPlayAuxView(
|
||||
const selectedOpeningIndexClamped = openings.length
|
||||
? Math.min(idx, openings.length - 1)
|
||||
: 0;
|
||||
const picked = openings[selectedOpeningIndexClamped];
|
||||
const sealed = readSessionProtagonist(board);
|
||||
const choiceRaw = board.getContentByTag(OPENING_PERSONA_CHOICE_TAG)?.trim();
|
||||
const openingPersonaChoice =
|
||||
@@ -111,7 +160,7 @@ export function buildPlayAuxView(
|
||||
selectedOpeningIndex: selectedOpeningIndexClamped,
|
||||
openingPersona: sealed
|
||||
? { name: sealed.name, description: sealed.description }
|
||||
: openings[selectedOpeningIndexClamped]?.persona ?? null,
|
||||
: picked?.persona ?? null,
|
||||
openingPersonaChoice,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user