297 lines
10 KiB
TypeScript
297 lines
10 KiB
TypeScript
import { createHash } from "node:crypto";
|
||
import {
|
||
getModuleSection,
|
||
loadModulePrompt,
|
||
parseModulePromptSections,
|
||
type ModuleCatalog,
|
||
type ModuleCatalogEntry,
|
||
} from "../skills/creation-flow.js";
|
||
import {
|
||
loadShellRefCatalog,
|
||
type ShellRefEntry,
|
||
} from "../skills/shell-ref-catalog.js";
|
||
import {
|
||
formatMarkdownSafeSubsetForPrompt,
|
||
loadMarkdownSafeSubsetCatalog,
|
||
} from "../skills/markdown-safe-subset.js";
|
||
import { getLibrary } from "../libraries/registry.js";
|
||
import {
|
||
getPreference,
|
||
listActivePreferences,
|
||
} from "../preference/store.js";
|
||
import {
|
||
getImageWorkflow,
|
||
listImageWorkflows,
|
||
} from "../image-workflow/store.js";
|
||
import {
|
||
getStylePack,
|
||
listActiveStylePacks,
|
||
} from "../style-pack/store.js";
|
||
|
||
export const DICTATE_MODULE_READ_STATE_TAG = "运行.对话落盘.已读能力";
|
||
|
||
/** 改名后旧目录名仍能 read_module,已有对话不用重学 id。 */
|
||
const MODULE_READ_NAME_ALIASES: Record<string, readonly string[]> = {
|
||
"aesthetics-interaction": ["美学纲领与交互范式"],
|
||
"story-progression": ["故事推进"],
|
||
mechanism: ["实现机制"],
|
||
};
|
||
|
||
export type DictateModuleReadPayload = {
|
||
module_id: string;
|
||
name: string;
|
||
artifact: string;
|
||
version: string;
|
||
task: string;
|
||
principles: string;
|
||
output: string;
|
||
score: string;
|
||
score_dimensions: string[];
|
||
libraries: Array<{
|
||
library_id: string;
|
||
label: string;
|
||
entries: Array<{ id: string; name?: string; summary: string }>;
|
||
}>;
|
||
constraints: string[];
|
||
presentation_references?: {
|
||
shells: ShellRefEntry[];
|
||
markdown_safe_subset: string;
|
||
iframe_contract: string;
|
||
};
|
||
};
|
||
|
||
export type DictateModuleReadState = {
|
||
schema: "dictate-module-read.v1";
|
||
modules: Record<string, string>;
|
||
active_module_id?: string;
|
||
};
|
||
|
||
export async function buildDictateModuleReadIndex(params: {
|
||
skillPackRoot: string;
|
||
catalog: ModuleCatalog;
|
||
}): Promise<Map<string, DictateModuleReadPayload>> {
|
||
const index = new Map<string, DictateModuleReadPayload>();
|
||
const [shells, markdown] = await Promise.all([
|
||
loadShellRefCatalog(params.skillPackRoot),
|
||
loadMarkdownSafeSubsetCatalog(params.skillPackRoot),
|
||
]);
|
||
for (const module of params.catalog.modules) {
|
||
const prompt = await loadModulePrompt(params.skillPackRoot, module.id);
|
||
if (!prompt) continue;
|
||
const sections = parseModulePromptSections(prompt);
|
||
const payload: DictateModuleReadPayload = {
|
||
module_id: module.id,
|
||
name: module.name,
|
||
artifact: module.artifact,
|
||
version: modulePromptVersion(sections.blocks),
|
||
task: getModuleSection(sections, "task") ?? "",
|
||
principles: getModuleSection(sections, "principles") ?? "",
|
||
output: getModuleSection(sections, "output") ?? "",
|
||
score: getModuleSection(sections, "score") ?? "",
|
||
score_dimensions: extractScoreDimensions(
|
||
getModuleSection(sections, "score") ?? "",
|
||
),
|
||
libraries: libraryDirectories(module),
|
||
constraints: moduleConstraints(module),
|
||
...(module.id === "reply-format" || module.id === "zero-layer-reply-format"
|
||
? {
|
||
presentation_references: {
|
||
shells,
|
||
markdown_safe_subset: markdown
|
||
? formatMarkdownSafeSubsetForPrompt(markdown)
|
||
: "",
|
||
iframe_contract:
|
||
module.id === "reply-format"
|
||
? "正文组成生成一次 frontend 模板与数据契约;游玩每轮只传 present.v1 blocks,由程序装进已固定的壳。iframe 使用 sandbox=\"allow-scripts\",不含 allow-same-origin;模板只通过 present.onData(cb) 收数据,不提供 action 回传。html+css+js 合计不超过 60 KB;未知 blocks 键必须保留。"
|
||
: "0层正文组成生成一次持久前端;之后每轮只提交 slot_updates。模板通过 zeroLayer.onInit 与 zeroLayer.onPatch 收数据,不提供 action 回传。html+css+js 合计不超过 60 KB。",
|
||
},
|
||
}
|
||
: {}),
|
||
};
|
||
index.set(module.id, payload);
|
||
index.set(module.name, payload);
|
||
for (const alias of MODULE_READ_NAME_ALIASES[module.id] ?? []) {
|
||
if (!index.has(alias)) index.set(alias, payload);
|
||
}
|
||
}
|
||
return index;
|
||
}
|
||
|
||
export function parseDictateModuleReadState(
|
||
raw: string | undefined | null,
|
||
): DictateModuleReadState {
|
||
if (raw?.trim()) {
|
||
try {
|
||
const row = JSON.parse(raw) as DictateModuleReadState;
|
||
if (
|
||
row?.schema === "dictate-module-read.v1" &&
|
||
row.modules &&
|
||
typeof row.modules === "object"
|
||
) {
|
||
return row;
|
||
}
|
||
} catch {
|
||
// Reset malformed internal state.
|
||
}
|
||
}
|
||
return { schema: "dictate-module-read.v1", modules: {} };
|
||
}
|
||
|
||
export function serializeDictateModuleReadState(
|
||
state: DictateModuleReadState,
|
||
): string {
|
||
return JSON.stringify(state);
|
||
}
|
||
|
||
export function readLibraryEntries(params: {
|
||
module: DictateModuleReadPayload;
|
||
libraryId: string;
|
||
entryIds: string[];
|
||
reason?: string;
|
||
}): { entries?: unknown[]; error?: string } {
|
||
const libraryId = params.libraryId.trim();
|
||
const bound = params.module.libraries.some(
|
||
(library) => library.library_id === libraryId,
|
||
);
|
||
if (!bound) {
|
||
return { error: `能力 ${params.module.module_id} 未绑定库 ${libraryId}` };
|
||
}
|
||
const ids = [...new Set(params.entryIds.map((id) => id.trim()).filter(Boolean))];
|
||
if (ids.length === 0) return { error: "entry_ids 不能为空" };
|
||
if (ids.length > 2 && !params.reason?.trim()) {
|
||
return { error: "默认最多读取两个库条目;读取更多时必须提供 reason" };
|
||
}
|
||
if (libraryId === "preferences") {
|
||
const entries = ids.map(getPreference);
|
||
const missing = ids.filter((_, index) => !entries[index]);
|
||
if (missing.length) return { error: `偏好库无条目:${missing.join("、")}` };
|
||
return {
|
||
entries: entries.map((entry) => ({
|
||
id: entry!.id,
|
||
content: entry!.content,
|
||
})),
|
||
};
|
||
}
|
||
if (libraryId === "style-packs") {
|
||
const entries = ids.map(getStylePack);
|
||
const missing = ids.filter((_, index) => !entries[index]);
|
||
if (missing.length) return { error: `文风库无条目:${missing.join("、")}` };
|
||
return {
|
||
entries: entries.map((entry) => ({
|
||
id: entry!.id,
|
||
name: entry!.name,
|
||
content: entry!.content,
|
||
...(entry!.samples ? { samples: entry!.samples } : {}),
|
||
})),
|
||
};
|
||
}
|
||
if (libraryId === "image-workflows") {
|
||
const entries = ids.map(getImageWorkflow);
|
||
const missing = ids.filter((_, index) => !entries[index]);
|
||
if (missing.length) return { error: `工作流库无条目:${missing.join("、")}` };
|
||
return {
|
||
entries: entries.map((entry) => ({
|
||
id: entry!.id,
|
||
name: entry!.name,
|
||
summary: entry!.summary,
|
||
...(entry!.promptNotes ? { prompt_notes: entry!.promptNotes } : {}),
|
||
})),
|
||
};
|
||
}
|
||
return { error: `库 ${libraryId} 尚不支持按条目读取` };
|
||
}
|
||
|
||
function modulePromptVersion(blocks: Record<string, string>): string {
|
||
const source = ["task", "principles", "output", "score"]
|
||
.map((id) => `${id}\n${blocks[id]?.trim() ?? ""}`)
|
||
.join("\n---\n");
|
||
return createHash("sha256").update(source).digest("hex").slice(0, 16);
|
||
}
|
||
|
||
function libraryDirectories(module: ModuleCatalogEntry): DictateModuleReadPayload["libraries"] {
|
||
return (module.libraries ?? []).map((id) => {
|
||
const provider = getLibrary(id);
|
||
if (id === "preferences") {
|
||
return {
|
||
library_id: id,
|
||
label: provider?.label ?? id,
|
||
entries: listActivePreferences().map((entry) => ({
|
||
id: entry.id,
|
||
summary: clip(entry.content),
|
||
})),
|
||
};
|
||
}
|
||
if (id === "style-packs") {
|
||
return {
|
||
library_id: id,
|
||
label: provider?.label ?? id,
|
||
entries: listActiveStylePacks().map((entry) => ({
|
||
id: entry.id,
|
||
name: entry.name,
|
||
summary: clip(entry.content),
|
||
})),
|
||
};
|
||
}
|
||
if (id === "image-workflows") {
|
||
return {
|
||
library_id: id,
|
||
label: provider?.label ?? id,
|
||
entries: listImageWorkflows().map((entry) => ({
|
||
id: entry.id,
|
||
name: entry.name,
|
||
summary: entry.summary,
|
||
})),
|
||
};
|
||
}
|
||
return { library_id: id, label: provider?.label ?? id, entries: [] };
|
||
});
|
||
}
|
||
|
||
function repeatableLandLine(module: ModuleCatalogEntry): string {
|
||
if (module.id === "opening-setup") {
|
||
return "产物可反复:每条开场使用 设计.开场白#场景短码;同完整 tag 才覆盖。游玩时左右切换,选中的一条作为 0 层";
|
||
}
|
||
if (module.id === "zero-layer-opening-setup") {
|
||
return "产物可反复:每条开场使用 设计.0层开场白与初态#场景短码;同完整 tag 才覆盖。游玩时左右切换,选中的一条作为 0 层";
|
||
}
|
||
if (module.id === "generation-rules") {
|
||
return "产物可反复:每条规则单独落盘,使用 设计.生成规则#规则名;同完整 tag 才覆盖";
|
||
}
|
||
if (module.id === "concrete-instances") {
|
||
return "产物可反复:每条实例单独落盘,使用 设计.具体实例#名称;同完整 tag 才覆盖";
|
||
}
|
||
if (module.repeatable) {
|
||
return `产物可反复:每条单独落盘,使用 ${module.artifact}#唯一id;同完整 tag 才覆盖`;
|
||
}
|
||
return `产物 tag:${module.artifact}`;
|
||
}
|
||
|
||
function moduleConstraints(module: ModuleCatalogEntry): string[] {
|
||
const constraints = [repeatableLandLine(module)];
|
||
if (module.id === "generation-rules") {
|
||
constraints.push(
|
||
"产物层由生命周期决定:只预生成=intermediate;仅游玩期 / 预生成并游玩期=final",
|
||
);
|
||
constraints.push("每个产物只写一条规则");
|
||
} else if (module.layer) {
|
||
constraints.push(`默认产物层:${module.layer}`);
|
||
}
|
||
if (module.mount?.length) constraints.push(`默认挂载:${module.mount.join("、")}`);
|
||
if (module.auto) constraints.push("机械程序节点,不受读取门禁");
|
||
return constraints;
|
||
}
|
||
|
||
function clip(text: string, max = 140): string {
|
||
const compact = text.replace(/\s+/g, " ").trim();
|
||
return compact.length <= max ? compact : `${compact.slice(0, max - 1)}…`;
|
||
}
|
||
|
||
function extractScoreDimensions(score: string): string[] {
|
||
const names: string[] = [];
|
||
for (const match of score.matchAll(/^([^#\-\s][^:\n]{0,30}):\s*$/gm)) {
|
||
const name = match[1]?.trim();
|
||
if (name && !names.includes(name)) names.push(name);
|
||
}
|
||
return names;
|
||
}
|