Files
writing-agent/src/skills/play-aux.ts
T

174 lines
5.1 KiB
TypeScript

/**
* 游玩辅助面:表格 / 开场与角色 等 Tab 的视图截面。
*/
import type { Blackboard } from "../blackboard/blackboard.js";
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,
ZERO_LAYER_OPENING_FAMILY,
SLOT_OPENING_SELECTED_INDEX,
parseOpeningSealPayload,
splitOpeningDocument,
type OpeningUserPersona,
} from "./opening-seal.js";
import { CURRENT_VARS_TAG } from "./variable-catalog.js";
import {
personaBoundToOpening,
readSessionProtagonist,
} from "../persona/protagonist-bind.js";
import { presentReadablePlain } from "./present-packet.js";
export type PlayAuxTableRow = {
key: string;
value: unknown;
rev: number;
visibility: "visible" | "hidden";
editable: boolean;
note?: string;
};
export type PlayAuxOpening = {
index: number;
/** 产物 tag:设计.开场白 或 设计.开场白#槽位;opening-setup 候选为假想 tag */
tag: string;
slot: string;
text: string;
persona: OpeningUserPersona | null;
};
export type PlayAuxView = {
table: { tag: string; rows: PlayAuxTableRow[] } | null;
openings: PlayAuxOpening[];
selectedOpeningIndex: number;
openingPersona: OpeningUserPersona | null;
openingPersonaChoice: "opening" | "global" | null;
};
function selectedOpeningIndex(slots: Record<string, unknown> | undefined): number {
const raw = slots?.[SLOT_OPENING_SELECTED_INDEX];
if (typeof raw === "number" && Number.isFinite(raw)) return Math.max(0, Math.trunc(raw));
if (typeof raw === "string" && raw.trim()) {
const n = Number(raw);
if (Number.isFinite(n)) return Math.max(0, Math.trunc(n));
}
return 0;
}
/** 黑板上全部可选用开场(工序候选优先;否则对话落盘 设计.开场白[#槽]) */
export function collectOpenings(board: Blackboard): PlayAuxOpening[] {
const payload = parseOpeningSealPayload(
board.getContentByTag(OPENING_SETUP_ARTIFACT_TAG),
0,
);
if (payload?.candidates.length) {
return payload.candidates.map((text, index) => {
const one = parseOpeningSealPayload(
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 rows: Array<{
tag: string;
content: string;
order: number;
}> = [];
for (const e of board.listTagIndex()) {
const family = dictateProductFamily(e.tag);
if (
family !== OPENING_PRODUCT_FAMILY &&
family !== ZERO_LAYER_OPENING_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 });
}
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(
board: Blackboard,
slots?: Record<string, unknown>,
): PlayAuxView {
const tableDoc = parseTableDoc(board.getContentByTag(CURRENT_VARS_TAG));
const table = tableDoc?.rows.length
? {
tag: CURRENT_VARS_TAG,
rows: tableDoc.rows.map((row) => ({
key: row.key,
value: row.value,
rev: row.rev,
visibility: row.visibility === "hidden" ? "hidden" : "visible",
editable: isTableCellEditable(row),
...(row.note ? { note: row.note } : {}),
})),
}
: null;
const openings = collectOpenings(board);
const idx = selectedOpeningIndex(slots);
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 =
choiceRaw === "opening" || choiceRaw === "global" ? choiceRaw : null;
return {
table,
openings,
selectedOpeningIndex: selectedOpeningIndexClamped,
openingPersona: sealed
? { name: sealed.name, description: sealed.description }
: picked?.persona ?? null,
openingPersonaChoice,
};
}