重构世界模拟器为模块化配方架构,完善创作编排、会话运行时与 Web UI,并清理过时技能。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-30 00:39:32 +08:00
parent 2b74c30d36
commit e670a5129c
167 changed files with 22955 additions and 5659 deletions

View File

@@ -0,0 +1,165 @@
/**
* Worker contextSegments 拼装:按 tier 顺序把黑板 tag 拼成 Markdown。
* 见 docs/context-assembly.md
*/
import type { Blackboard } from "../blackboard/blackboard.js";
import type { BlackboardInputMerge } from "../types/blackboard.js";
import { CONTEXT_BRIEF_TAG } from "../runtime/compress-after-worker.js";
import {
CREATION_ACCEPTED_CONTENT_TAG,
formatAcceptedContentForPrompt,
} from "./creation-units.js";
export type ContextSegmentTier = "static" | "dynamic";
export type ContextSegmentDef = {
id: string;
tier: ContextSegmentTier;
tags: string[];
label?: string;
/** latest | concat | tail_lines_N */
policy?: string;
};
export function parseContextSegments(raw: unknown): ContextSegmentDef[] {
if (!Array.isArray(raw)) return [];
const out: ContextSegmentDef[] = [];
for (const row of raw) {
if (!row || typeof row !== "object" || Array.isArray(row)) continue;
const r = row as Record<string, unknown>;
const id = typeof r.id === "string" ? r.id.trim() : "";
const tier = r.tier === "dynamic" ? "dynamic" : "static";
const tags = Array.isArray(r.tags)
? r.tags.filter((t): t is string => typeof t === "string" && t.trim()).map((t) => t.trim())
: [];
if (!id || tags.length === 0) continue;
out.push({
id,
tier,
tags,
label: typeof r.label === "string" ? r.label.trim() : undefined,
policy: typeof r.policy === "string" ? r.policy.trim() : undefined,
});
}
return out;
}
function applyPolicy(content: string, policy?: string): string {
if (!policy || policy === "latest" || policy === "concat") return content;
const m = /^tail_lines_(\d+)$/.exec(policy);
if (m) {
const n = Number(m[1]);
if (Number.isFinite(n) && n > 0) {
const lines = content.split(/\r?\n/);
return lines.slice(-n).join("\n");
}
}
return content;
}
function readTagContent(
tag: string,
inputs: Record<string, string>,
blackboard: Blackboard,
inputMerge: BlackboardInputMerge,
): string {
if (inputs[tag]?.trim()) return inputs[tag]!.trim();
const items = blackboard.queryByPatterns([tag], inputMerge);
if (items.length === 0) return "";
if (inputMerge === "concat") {
return items.map((i) => i.content).filter(Boolean).join("\n\n");
}
return items[items.length - 1]?.content?.trim() ?? "";
}
function formatSegmentBody(
segment: ContextSegmentDef,
inputs: Record<string, string>,
blackboard: Blackboard,
inputMerge: BlackboardInputMerge,
): string {
const parts: string[] = [];
for (const tag of segment.tags) {
let content = readTagContent(tag, inputs, blackboard, inputMerge);
if (!content) continue;
if (tag === CREATION_ACCEPTED_CONTENT_TAG) {
content = formatAcceptedContentForPrompt(content);
}
content = applyPolicy(content, segment.policy);
if (!content.trim()) continue;
parts.push(content.trim());
}
return parts.join("\n\n");
}
/**
* 按 contextSegments 拼装 user 侧上下文。
* 无 segments 时回退为 JSON inputs兼容旧 skill
*/
export function assembleWorkerContext(params: {
inputs: Record<string, string>;
segments?: ContextSegmentDef[] | null;
blackboard: Blackboard;
inputMerge?: BlackboardInputMerge;
workerId: string;
workerName: string;
outputTags: string[];
}): string {
const inputMerge = params.inputMerge ?? "latest";
const segments = params.segments ?? [];
if (segments.length === 0) {
return JSON.stringify(
{
workerId: params.workerId,
workerName: params.workerName,
outputTags: params.outputTags,
inputs: params.inputs,
instruction:
"根据 SKILL 说明完成任务。若 inputs 含「上下文.定稿摘要」,以终产物为准。" +
"设计.worker集 / 草稿须为 JSON 对象文本。",
},
null,
2,
);
}
const staticSegs = segments.filter((s) => s.tier === "static");
const dynamicSegs = segments.filter((s) => s.tier === "dynamic");
const blocks: string[] = [];
const render = (seg: ContextSegmentDef) => {
const body = formatSegmentBody(seg, params.inputs, params.blackboard, inputMerge);
if (!body) return;
if (seg.label) {
blocks.push(`${seg.label}\n\n${body}`);
} else {
blocks.push(body);
}
};
for (const seg of staticSegs) render(seg);
for (const seg of dynamicSegs) render(seg);
// 定稿摘要:若未在 segments 中声明,仍附在末尾
const brief = params.inputs[CONTEXT_BRIEF_TAG]?.trim();
const briefInSegments = segments.some((s) => s.tags.includes(CONTEXT_BRIEF_TAG));
if (brief && !briefInSegments) {
blocks.push(`## 上下文.定稿摘要\n\n${brief}`);
}
blocks.push(
[
"## 本步任务",
"",
`- workerId: \`${params.workerId}\``,
`- workerName: ${params.workerName}`,
`- outputTags: ${params.outputTags.map((t) => `\`${t}\``).join("、") || "(无)"}`,
"",
"按 SKILL 与上方分区完成任务。标为「只读 / 已定稿」的分区不要擅自改写;只改【本单位】范围。",
"设计.worker集 / 草稿须为 JSON 对象文本(以 `{` 开头)。",
].join("\n"),
);
return blocks.join("\n\n---\n\n");
}

964
src/skills/creation-flow.ts Normal file
View File

@@ -0,0 +1,964 @@
/**
* 创作流程:编排产物(设计.创作流程)。
* 可变增量 DAG有序 steps + 每步 id/中文名 + depends_on可追加、可同能力多次。
*
* 两层内容(作者细写,运行时只搭骨架):
* - recipes/:初始配方(给总管 / design-flow 的参考起点,可调味)
* - modules/:共用组件池(步骤名与方法正文;配方与总管都从这里选型)
*/
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { parse as parseYaml } from "yaml";
export const CREATION_FLOW_TAG = "设计.创作流程";
export const CREATION_CURRENT_STEP_TAG = "创作.当前步骤";
/** 用户手动选定的初始配方(存 recipe id或 JSON {id,name} */
export const CREATION_SELECTED_RECIPE_TAG = "创作.选用配方";
export const MODULE_CATALOG_FILENAME = "modules/catalog.yaml";
export const RECIPE_CATALOG_FILENAME = "recipes/catalog.yaml";
export const DESIGN_STEP_WORKER_ID = "design-step";
export const DESIGN_FLOW_WORKER_ID = "design-flow";
const DEFAULT_SKILLS_ROOT = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"../../skills",
);
export type CreationFlowStep = {
/**
* 本局步骤唯一 id验收与 depends_on 用这个)。
* 同能力可多次出现时必须不同;缺省时程序按 name / name#n 补齐。
*/
id: string;
/** 固定中文名,须 ∈ 模块目录(可重复) */
name: string;
/** 依赖的其它步骤 id旧稿若 name 唯一也可写 name */
depends_on: string[];
};
export type CreationFlowStatus = "open" | "closed";
export type CreationFlow = {
version: 1;
/** 可选一句体验复述(给人看) */
brief?: string;
/**
* open = 当前只是近期 horizon还可增量追加 / 反复编排同能力;
* closed = 不再扩步(可走收成)。缺省按 closed兼容旧固定 DAG
*/
status?: CreationFlowStatus;
steps: CreationFlowStep[];
};
export type ModuleCatalogEntry = {
/** 目录文件夹名,如 aesthetics-interaction */
id: string;
name: string;
/** 给 agent 的短声明:用来决定要不要调度这一步 */
declaration: string;
/** 执行期产物 tag流程 JSON 不写;程序映射) */
artifact: string;
/**
* 可选:默认可反复编排进流程(如生成规则、具体实例)。
* 程序不硬拦;给编排与校验提示。
*/
repeatable?: boolean;
/**
* 可选:默认问题(开场白)。优先用 prompt.md 的 ```opening 块;
* catalog 写了则作覆盖。程序发出,不经 LLM。
*/
opening?: string;
};
/** 本步程序开场白正文design-step 发出后写入,供 LLM 看见) */
export const CREATION_MODULE_OPENING_TAG = "创作.能力开场白";
/** JSON{ [步骤中文名]: "shown" | "answered" } */
export const CREATION_MODULE_OPENING_STATE_TAG = "创作.能力开场状态";
export const SLOT_CREATION_MODULE_OPENING_STATE = "creationModuleOpeningState";
export type ModuleOpeningState = Record<string, "shown" | "answered">;
export type ModuleCatalog = {
modules: ModuleCatalogEntry[];
};
/** 初始配方目录条目(短声明,给选型) */
export type RecipeCatalogEntry = {
id: string;
name: string;
declaration: string;
};
export type RecipeCatalog = {
recipes: RecipeCatalogEntry[];
};
/**
* 单份初始配方详情。
* seed = 建议步骤可为空name 须 ∈ 模块池);编排时允许增删改。
*/
export type RecipeDetail = {
id: string;
name: string;
declaration: string;
when?: string;
hint?: string;
seed: CreationFlow | null;
};
export type CreationFlowValidation = {
ok: boolean;
errors: string[];
};
export type CreationFlowUserView = {
brief?: string;
status?: CreationFlowStatus;
steps: Array<{
order: number;
id: string;
name: string;
depends_on: string[];
/** 同能力第几次(>1 时 UI 可标「再来」) */
occurrence?: number;
/** 目录里的短声明(有则展示) */
declaration?: string;
repeatable?: boolean;
}>;
parseError?: string;
};
/** 从任意正文抽取 JSON 对象 */
export function extractJsonObject(raw: string): unknown | null {
const trimmed = raw.trim();
if (!trimmed) return null;
try {
return JSON.parse(trimmed);
} catch {
/* try slice */
}
const start = trimmed.indexOf("{");
const end = trimmed.lastIndexOf("}");
if (start >= 0 && end > start) {
try {
return JSON.parse(trimmed.slice(start, end + 1));
} catch {
return null;
}
}
return null;
}
/** 为缺 id 的步骤补齐唯一 id同 name 多次 → name#2、name#3… */
export function ensureCreationFlowStepIds(
steps: Array<{ id?: string; name: string; depends_on: string[] }>,
): CreationFlowStep[] {
const used = new Set<string>();
const nameCount = new Map<string, number>();
const out: CreationFlowStep[] = [];
for (const raw of steps) {
const name = raw.name.trim();
const n = (nameCount.get(name) ?? 0) + 1;
nameCount.set(name, n);
let id = typeof raw.id === "string" ? raw.id.trim() : "";
if (!id) {
id = n === 1 ? name : `${name}#${n}`;
}
if (used.has(id)) {
let i = 2;
while (used.has(`${id}#${i}`)) i++;
id = `${id}#${i}`;
}
used.add(id);
out.push({
id,
name,
depends_on: raw.depends_on.map((d) => d.trim()).filter(Boolean),
});
}
return out;
}
/** 按 id 或唯一name 解析步骤引用 */
export function findStepByRef(
flow: CreationFlow,
ref: string,
): CreationFlowStep | null {
const key = ref.trim();
if (!key) return null;
const byId = flow.steps.find((s) => s.id === key);
if (byId) return byId;
const byName = flow.steps.filter((s) => s.name === key);
return byName.length === 1 ? byName[0]! : null;
}
/** 验收 / 当前步骤用的单位 id优先 step.id */
export function stepUnitId(step: CreationFlowStep): string {
return step.id || step.name;
}
export function parseCreationFlow(raw: string | undefined | null): CreationFlow | null {
if (!raw?.trim()) return null;
const doc = extractJsonObject(raw);
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
const row = doc as Record<string, unknown>;
const stepsRaw = row.steps;
if (!Array.isArray(stepsRaw) || stepsRaw.length === 0) return null;
const drafted: Array<{ id?: string; name: string; depends_on: string[] }> = [];
for (const item of stepsRaw) {
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
const s = item as Record<string, unknown>;
const name = typeof s.name === "string" ? s.name.trim() : "";
if (!name) return null;
const id = typeof s.id === "string" && s.id.trim() ? s.id.trim() : undefined;
const depsRaw = s.depends_on ?? s.dependsOn ?? [];
const depends_on = Array.isArray(depsRaw)
? depsRaw.map((d) => String(d).trim()).filter(Boolean)
: [];
drafted.push({ id, name, depends_on });
}
const steps = ensureCreationFlowStepIds(drafted);
const brief =
typeof row.brief === "string" && row.brief.trim() ? row.brief.trim() : undefined;
const statusRaw = typeof row.status === "string" ? row.status.trim() : "";
const status: CreationFlowStatus | undefined =
statusRaw === "open" || statusRaw === "closed" ? statusRaw : undefined;
return { version: 1, brief, status, steps };
}
export function parseModuleCatalog(raw: string): ModuleCatalog | null {
let doc: unknown;
try {
doc = parseYaml(raw);
} catch {
return null;
}
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
const modulesRaw = (doc as Record<string, unknown>).modules;
if (!Array.isArray(modulesRaw)) return null;
const modules: ModuleCatalogEntry[] = [];
for (const item of modulesRaw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const m = item as Record<string, unknown>;
const name = typeof m.name === "string" ? m.name.trim() : "";
const declaration =
typeof m.declaration === "string" ? m.declaration.trim() : "";
const artifact = typeof m.artifact === "string" ? m.artifact.trim() : "";
const id =
typeof m.id === "string" && m.id.trim()
? m.id.trim()
: name
? slugFromName(name)
: "";
if (!name || !declaration || !artifact || !id) continue;
const opening =
typeof m.opening === "string" && m.opening.trim()
? m.opening.trim()
: undefined;
const repeatable = m.repeatable === true;
modules.push({
id,
name,
declaration,
artifact,
...(repeatable ? { repeatable: true } : {}),
...(opening ? { opening } : {}),
});
}
if (modules.length === 0) return null;
return { modules };
}
/**
* 能力 prompt.md 可切割块fence 语言标签 = 块 id
* 标准块见 MODULE_SECTION_IDS程序只认 ```id … ```,不认散文标题 alone。
*/
export const MODULE_SECTION_IDS = [
"meta",
"opening",
"task",
"principles",
"probe",
"output",
"checklist",
"examples",
] as const;
export type ModuleSectionId = (typeof MODULE_SECTION_IDS)[number];
export type ModulePromptSections = {
/** 原文 */
raw: string;
/** 按 fence 标签切出的块;缺块则为空串 */
blocks: Partial<Record<ModuleSectionId, string>> & Record<string, string>;
};
/**
* 切割能力文档:抽取全部 ```lang … ``` 块。
* 同一 lang 多次出现时拼接(中间空行)。
*/
export function parseModulePromptSections(promptMd: string): ModulePromptSections {
const blocks: Record<string, string> = {};
if (!promptMd?.trim()) return { raw: promptMd ?? "", blocks };
const re = /```([a-zA-Z][\w-]*)\s*\r?\n([\s\S]*?)```/g;
let m: RegExpExecArray | null;
while ((m = re.exec(promptMd)) !== null) {
const id = m[1]!.toLowerCase();
const body = m[2]!.trim();
if (!body) continue;
blocks[id] = blocks[id] ? `${blocks[id]}\n\n${body}` : body;
}
return { raw: promptMd, blocks };
}
export function getModuleSection(
sections: ModulePromptSections,
id: ModuleSectionId | string,
): string | null {
const body = sections.blocks[id.toLowerCase()]?.trim();
return body || null;
}
/**
* 从能力 prompt.md 抽取默认问题(开场白)。
* 只认 ```opening … ```(能力标准块)。
*/
export function extractModuleOpening(promptMd: string): string | null {
return getModuleSection(parseModulePromptSections(promptMd), "opening");
}
/**
* 拼给 LLM 的方法正文:标准块按固定顺序;无标准块时回退全文。
* 不含 opening开场已由程序发出
*/
export function formatModulePromptForLlm(promptMd: string): string {
const { blocks } = parseModulePromptSections(promptMd);
const order: ModuleSectionId[] = [
"meta",
"task",
"principles",
"probe",
"output",
"checklist",
"examples",
];
const parts: string[] = [];
for (const id of order) {
const body = blocks[id]?.trim();
if (body) parts.push(`## ${id}\n\n\`\`\`${id}\n${body}\n\`\`\``);
}
if (parts.length === 0) return promptMd.trim();
return parts.join("\n\n");
}
export function parseModuleOpeningState(
raw: string | unknown | null | undefined,
): ModuleOpeningState {
if (raw == null) return {};
const text =
typeof raw === "string"
? raw.trim()
: typeof raw === "object"
? JSON.stringify(raw)
: String(raw);
if (!text) return {};
try {
const doc = JSON.parse(text) as unknown;
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return {};
const out: ModuleOpeningState = {};
for (const [k, v] of Object.entries(doc as Record<string, unknown>)) {
if (v === "shown" || v === "answered") out[k] = v;
}
return out;
} catch {
return {};
}
}
export function stringifyModuleOpeningState(state: ModuleOpeningState): string {
return JSON.stringify(state);
}
/** 无 id 时的兜底(目录仍应显式写 id */
function slugFromName(name: string): string {
const map: Record<string, string> = {
: "aesthetics-interaction",
: "interaction",
: "aesthetics",
: "narrative",
: "mechanism",
: "world-blueprint",
: "generation-rules",
: "concrete-instances",
: "topology",
: "status-bar",
: "variable-design",
: "variable-context",
: "reply-format",
"Worker 规格": "worker-spec",
稿: "refine",
};
return map[name] ?? name;
}
export async function loadModuleCatalog(
skillPackRoot: string,
skillsRoot = DEFAULT_SKILLS_ROOT,
): Promise<ModuleCatalog | null> {
const fullPath = path.join(skillsRoot, skillPackRoot, MODULE_CATALOG_FILENAME);
try {
const raw = await readFile(fullPath, "utf8");
return parseModuleCatalog(raw);
} catch {
return null;
}
}
/** 注入 design-flow 的短目录(非全文 prompt */
export function formatModuleCatalogForAgent(catalog: ModuleCatalog): string {
const lines = catalog.modules.map((m) => {
const flags = m.repeatable ? "〔可反复〕" : "";
return `- ${m.name}${flags}${m.declaration}`;
});
return `【能力 · 可选工序】(按需选用,勿默认全选;步骤名只能从这里选;标〔可反复〕的可多次编入)\n${lines.join("\n")}`;
}
export function parseRecipeCatalog(raw: string): RecipeCatalog | null {
let doc: unknown;
try {
doc = parseYaml(raw);
} catch {
return null;
}
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null;
const recipesRaw = (doc as Record<string, unknown>).recipes;
if (!Array.isArray(recipesRaw)) return null;
const recipes: RecipeCatalogEntry[] = [];
for (const item of recipesRaw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const r = item as Record<string, unknown>;
const name = typeof r.name === "string" ? r.name.trim() : "";
const declaration =
typeof r.declaration === "string" ? r.declaration.trim() : "";
const id =
typeof r.id === "string" && r.id.trim()
? r.id.trim()
: name
? slugFromName(name)
: "";
if (!name || !declaration || !id) continue;
recipes.push({ id, name, declaration });
}
if (recipes.length === 0) return null;
return { recipes };
}
export async function loadRecipeCatalog(
skillPackRoot: string,
skillsRoot = DEFAULT_SKILLS_ROOT,
): Promise<RecipeCatalog | null> {
const fullPath = path.join(skillsRoot, skillPackRoot, RECIPE_CATALOG_FILENAME);
try {
const raw = await readFile(fullPath, "utf8");
return parseRecipeCatalog(raw);
} catch {
return null;
}
}
/**
* 解析单份 recipe.yamlwhen / hint / brief / steps
* steps 空或缺失 → seed 为 null仍可作选型参考
*/
export function parseRecipeYaml(
raw: string,
meta: RecipeCatalogEntry,
): RecipeDetail {
let doc: unknown;
try {
doc = parseYaml(raw);
} catch {
return {
id: meta.id,
name: meta.name,
declaration: meta.declaration,
seed: null,
};
}
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
return {
id: meta.id,
name: meta.name,
declaration: meta.declaration,
seed: null,
};
}
const row = doc as Record<string, unknown>;
const when =
typeof row.when === "string" && row.when.trim()
? row.when.trim()
: undefined;
const hint =
typeof row.hint === "string" && row.hint.trim()
? row.hint.trim()
: undefined;
const name =
typeof row.name === "string" && row.name.trim()
? row.name.trim()
: meta.name;
const seedParsed = parseCreationFlow(JSON.stringify({
brief: typeof row.brief === "string" ? row.brief : undefined,
status: "open",
steps: Array.isArray(row.steps) ? row.steps : [],
}));
const seed = seedParsed;
return {
id: meta.id,
name,
declaration: meta.declaration,
when,
hint,
seed,
};
}
export async function loadRecipeDetail(
skillPackRoot: string,
entry: RecipeCatalogEntry,
skillsRoot = DEFAULT_SKILLS_ROOT,
): Promise<RecipeDetail> {
const fullPath = path.join(
skillsRoot,
skillPackRoot,
"recipes",
entry.id,
"recipe.yaml",
);
try {
const raw = await readFile(fullPath, "utf8");
return parseRecipeYaml(raw, entry);
} catch {
return {
id: entry.id,
name: entry.name,
declaration: entry.declaration,
seed: null,
};
}
}
export async function loadAllRecipeDetails(
skillPackRoot: string,
skillsRoot = DEFAULT_SKILLS_ROOT,
): Promise<RecipeDetail[]> {
const catalog = await loadRecipeCatalog(skillPackRoot, skillsRoot);
if (!catalog) return [];
const out: RecipeDetail[] = [];
for (const entry of catalog.recipes) {
out.push(await loadRecipeDetail(skillPackRoot, entry, skillsRoot));
}
return out;
}
/**
* 解析用户选定的配方引用。
* 接受纯 id / 中文名,或 JSON `{ "id": "…" }` / `{ "name": "…" }`。
*/
export function parseSelectedRecipeRef(
raw: string | null | undefined,
): string | null {
if (!raw?.trim()) return null;
const trimmed = raw.trim();
try {
const doc = JSON.parse(trimmed) as unknown;
if (doc && typeof doc === "object" && !Array.isArray(doc)) {
const row = doc as Record<string, unknown>;
const id = typeof row.id === "string" ? row.id.trim() : "";
const name = typeof row.name === "string" ? row.name.trim() : "";
return id || name || null;
}
} catch {
/* plain string */
}
return trimmed;
}
export function findRecipeCatalogEntry(
catalog: RecipeCatalog | null | undefined,
ref: string,
): RecipeCatalogEntry | null {
if (!catalog || !ref.trim()) return null;
const key = ref.trim();
return (
catalog.recipes.find((r) => r.id === key || r.name === key) ?? null
);
}
/** 给人 / API 看的配方目录短列表(不是给 agent 选型) */
export function formatRecipeCatalogForAgent(catalog: RecipeCatalog): string {
const lines = catalog.recipes.map(
(r) => `- ${r.name}${r.declaration}`,
);
return `【可选导演】(须由用户手动选择)\n${lines.join("\n")}`;
}
/** 注入 design-flow用户已选导演内部 recipe */
export function formatSelectedRecipeForAgent(detail: RecipeDetail): string {
const lines: string[] = [
`【用户已选导演 · ${detail.name}`,
"这是用户手动选定的方法起点,不是锁死流水线。",
"产出**增量 DAG**:只排近期要做的步骤;已验收步保留,可追加同能力多次调用(如生成规则 / 具体实例)。",
"按用户表述增删改未验收步骤与依赖(像现场改戏 / 调味);步骤名只能从【能力】选。",
"禁止改选其它导演;若用户要换导演,须等用户重新选定后再编排。",
];
if (detail.declaration) lines.push(`简介:${detail.declaration}`);
if (detail.when) lines.push(`适用:${detail.when}`);
if (detail.hint) lines.push(`调味提示:${detail.hint}`);
if (detail.seed?.steps.length) {
const stepsJson = JSON.stringify(
{
brief: detail.seed.brief,
status: detail.seed.status ?? "open",
steps: detail.seed.steps,
},
null,
2,
);
lines.push("建议近期 steps增量起点可改勿一次排完全程");
lines.push("```json");
lines.push(stepsJson);
lines.push("```");
} else {
lines.push("建议 steps待作者完善 recipe.yaml可从【能力】自行编排近期 horizon");
}
return lines.join("\n");
}
/** design-flow 一次注入:已选配方 + 组件池 */
export function formatDesignFlowContentBlocks(params: {
selectedRecipe?: RecipeDetail | null;
modules?: ModuleCatalog | null;
missingSelection?: boolean;
}): string[] {
const blocks: string[] = [];
if (params.missingSelection) {
blocks.push(
[
"【导演】用户尚未手动选择。",
"禁止自行猜测或替用户选定导演。",
"请 askUser 请用户从可用导演中选择,或等待用户在界面选定后再编排。",
].join("\n"),
);
} else if (params.selectedRecipe) {
blocks.push(formatSelectedRecipeForAgent(params.selectedRecipe));
}
if (params.modules) {
blocks.push(formatModuleCatalogForAgent(params.modules));
}
return blocks;
}
/** 解析并加载用户已选配方详情 */
export async function resolveSelectedRecipeDetail(params: {
skillPackRoot: string;
selectedRecipeRef?: string | null;
skillsRoot?: string;
}): Promise<RecipeDetail | null> {
const ref = parseSelectedRecipeRef(params.selectedRecipeRef);
if (!ref) return null;
const skillsRoot = params.skillsRoot ?? DEFAULT_SKILLS_ROOT;
const catalog = await loadRecipeCatalog(params.skillPackRoot, skillsRoot);
const entry = findRecipeCatalogEntry(catalog, ref);
if (!entry) return null;
return loadRecipeDetail(params.skillPackRoot, entry, skillsRoot);
}
export function validateCreationFlow(
flow: CreationFlow,
catalog: ModuleCatalog | null,
): CreationFlowValidation {
const errors: string[] = [];
const ids = flow.steps.map((s) => s.id);
const seenIds = new Set<string>();
const allowed = catalog
? new Set(catalog.modules.map((m) => m.name))
: null;
const nameCount = new Map<string, number>();
for (const step of flow.steps) {
nameCount.set(step.name, (nameCount.get(step.name) ?? 0) + 1);
}
for (let i = 0; i < flow.steps.length; i++) {
const step = flow.steps[i]!;
if (seenIds.has(step.id)) {
errors.push(`步骤 id「${step.id}」重复`);
}
seenIds.add(step.id);
if (allowed && !allowed.has(step.name)) {
errors.push(`${step.name}」不在模块目录中`);
}
const mod = catalog?.modules.find((m) => m.name === step.name);
if (
catalog &&
(nameCount.get(step.name) ?? 0) > 1 &&
mod &&
mod.repeatable !== true
) {
errors.push(
`${step.name}」出现多次,但目录未标 repeatable仅可反复能力可同名多次`,
);
}
for (const dep of step.depends_on) {
const depStep = findStepByRef(flow, dep);
if (!depStep) {
errors.push(`${step.id}」依赖「${dep}」,但流程中没有该步骤`);
continue;
}
const depIndex = flow.steps.findIndex((s) => s.id === depStep.id);
if (depIndex >= i) {
errors.push(
`${step.id}」依赖「${depStep.id}」,但「${depStep.id}」未排在其前面`,
);
}
}
}
return { ok: errors.length === 0, errors };
}
export function artifactTagForStep(
name: string,
catalog: ModuleCatalog | null,
): string | null {
return catalog?.modules.find((m) => m.name === name)?.artifact ?? null;
}
export function formatCreationFlowForUser(
flow: CreationFlow,
catalog?: ModuleCatalog | null,
): CreationFlowUserView {
const decl = new Map(
(catalog?.modules ?? []).map((m) => [m.name, m] as const),
);
const seenName = new Map<string, number>();
return {
brief: flow.brief,
status: flow.status,
steps: flow.steps.map((s, i) => {
const n = (seenName.get(s.name) ?? 0) + 1;
seenName.set(s.name, n);
const mod = decl.get(s.name);
return {
order: i + 1,
id: s.id,
name: s.name,
depends_on: s.depends_on,
occurrence: n,
declaration: mod?.declaration,
repeatable: mod?.repeatable,
};
}),
};
}
export function findModuleByName(
catalog: ModuleCatalog | null | undefined,
name: string,
): ModuleCatalogEntry | null {
if (!catalog) return null;
return catalog.modules.find((m) => m.name === name) ?? null;
}
/** 已验收步骤名列表JSON 数组或换行文本) */
export function parseAcceptedSteps(raw: unknown): string[] {
if (Array.isArray(raw)) {
return raw.map((x) => String(x).trim()).filter(Boolean);
}
if (typeof raw !== "string" || !raw.trim()) return [];
try {
const doc = JSON.parse(raw);
if (Array.isArray(doc)) {
return doc.map((x) => String(x).trim()).filter(Boolean);
}
} catch {
/* fall through */
}
return raw
.split(/[\n,]/)
.map((s) => s.trim())
.filter(Boolean);
}
/**
* 某步是否已验收:认 step.id兼容旧会话只记了中文 name且当时 name 唯一)。
*/
export function isStepAccepted(
step: CreationFlowStep,
acceptedStepIds: readonly string[],
): boolean {
const done = new Set(acceptedStepIds);
if (done.has(step.id)) return true;
// 旧稿:验收列表里是中文名,且 id 就是 name
if (step.id === step.name && done.has(step.name)) return true;
return false;
}
/**
* 流程中下一个待做步骤:未验收,且 depends_on 均已验收。
*/
export function nextPendingStep(
flow: CreationFlow | null,
acceptedStepIds: readonly string[],
): CreationFlowStep | null {
if (!flow?.steps.length) return null;
for (const step of flow.steps) {
if (isStepAccepted(step, acceptedStepIds)) continue;
const depsOk = step.depends_on.every((dep) => {
const depStep = findStepByRef(flow, dep);
if (!depStep) return false;
return isStepAccepted(depStep, acceptedStepIds);
});
if (depsOk) return step;
}
return null;
}
/** 当前已列出的步骤是否都已验收(不管 status */
export function areListedStepsAccepted(
flow: CreationFlow | null,
acceptedStepIds: readonly string[],
): boolean {
if (!flow?.steps.length) return false;
return flow.steps.every((s) => isStepAccepted(s, acceptedStepIds));
}
/**
* 流程是否收束完毕listed steps 全验收,且 status 非 open。
* status=open 或缺省但还要扩步 → 应再调 design-flow。
* 缺省 status兼容旧固定 DAG视为 closed。
*/
export function isCreationFlowComplete(
flow: CreationFlow | null,
acceptedStepIds: readonly string[],
): boolean {
if (!areListedStepsAccepted(flow, acceptedStepIds)) return false;
if (flow?.status === "open") return false;
return true;
}
/**
* 当前步骤做完、但 DAG 仍 open → 需要再编排(追加 / 关闭)。
*/
export function needsFlowExpansion(
flow: CreationFlow | null,
acceptedStepIds: readonly string[],
): boolean {
if (!flow?.steps.length) return true;
if (nextPendingStep(flow, acceptedStepIds)) return false;
return flow.status === "open";
}
export async function loadModulePrompt(
skillPackRoot: string,
moduleId: string,
skillsRoot = DEFAULT_SKILLS_ROOT,
): Promise<string | null> {
const fullPath = path.join(
skillsRoot,
skillPackRoot,
"modules",
moduleId,
"prompt.md",
);
try {
return await readFile(fullPath, "utf8");
} catch {
return null;
}
}
/** 依赖步骤 → 产物 tag执行期注入按依赖步的能力 name 映射) */
export function dependencyArtifactTags(
step: CreationFlowStep,
catalog: ModuleCatalog | null,
flow?: CreationFlow | null,
): string[] {
if (!catalog) return [];
const tags: string[] = [];
for (const dep of step.depends_on) {
const depName = flow
? findStepByRef(flow, dep)?.name ?? dep
: dep;
const art = artifactTagForStep(depName, catalog);
if (art) tags.push(art);
}
return [...new Set(tags)];
}
export type DesignStepBinding = {
step: CreationFlowStep;
module: ModuleCatalogEntry;
depTags: string[];
modulePrompt: string;
/** 程序开场白;无则本步直接调 LLM */
opening: string | null;
};
/**
* 解析 design-step 本轮绑定:当前步骤、模块 prompt、依赖 tag、产物 tag。
*/
export async function resolveDesignStepBinding(params: {
skillPackRoot: string;
flowRaw: string | null | undefined;
currentStepName?: string | null;
acceptedStepNames?: readonly string[];
skillsRoot?: string;
}): Promise<DesignStepBinding | null> {
const skillsRoot = params.skillsRoot ?? DEFAULT_SKILLS_ROOT;
const catalog = await loadModuleCatalog(params.skillPackRoot, skillsRoot);
const flow = parseCreationFlow(params.flowRaw);
if (!catalog || !flow) return null;
const accepted = params.acceptedStepNames ?? [];
let step: CreationFlowStep | null = null;
const named = params.currentStepName?.trim();
if (named) {
step = findStepByRef(flow, named);
}
if (!step) {
step = nextPendingStep(flow, accepted);
}
if (!step) return null;
const module = findModuleByName(catalog, step.name);
if (!module) return null;
const modulePromptRaw =
(await loadModulePrompt(params.skillPackRoot, module.id, skillsRoot)) ??
`# ${module.name}\n\n模块 prompt.md 缺失,请补充 skills/.../modules/${module.id}/prompt.md`;
const opening =
module.opening?.trim() || extractModuleOpening(modulePromptRaw) || null;
return {
step,
module,
depTags: dependencyArtifactTags(step, catalog, flow),
modulePrompt: formatModulePromptForLlm(modulePromptRaw),
opening,
};
}

View File

@@ -0,0 +1,694 @@
/**
* 创作单位:与 run worker 调度正交。
*
* 两大族(同级):
* - worker独立 LLM 工序
* - fixed已写入规格的固定上下文不上 worker ≠ 不重要)
*
* FIXED_CONTEXT_CATALOG = 给 design 的「可向用户询问的示例话题」提示目录,
* 不是填空表;默认 listCreationUnits 只列出**已有内容**的固定块。
*
* 见 docs/design-orchestrator-guide.md §6、§7.2
*/
import type { ParsedWorkerSet } from "./worker-set-parse.js";
import { parseResidentContext } from "./resident-context.js";
export const WORKER_SET_FINAL_TAG = "设计.worker集";
export const WORKER_SET_DRAFT_TAG = "设计.worker集.草稿";
export const CREATION_CURRENT_UNIT_TAG = "创作.当前单位";
export const CREATION_ACCEPTED_UNITS_TAG = "创作.已验收单位";
/** 各单位最后一次验收时的内容切片JSON store → 拼装时格式化为前情提要) */
export const CREATION_ACCEPTED_CONTENT_TAG = "创作.已验收内容";
export const SLOT_CREATION_CURRENT_UNIT = "creationCurrentUnitId";
export const SLOT_CREATION_ACCEPTED_UNITS = "creationAcceptedUnits";
export const SLOT_CREATION_UNIT_ANCHOR_AT = "creationUnitAnchorAt";
/** 创作阶段磁盘 skill新流程编排 + 按步执行) */
export const DESIGN_DISK_WORKERS = [
"design-flow",
"design-step",
] as const;
export type DesignDiskWorkerId = (typeof DESIGN_DISK_WORKERS)[number];
/** @deprecated 旧分步 skill已废弃仅兼容读旧会话 */
const LEGACY_DESIGN_DISK_WORKERS = [
"design-core",
"design-worker",
"design-fixed",
"design-refine",
"design-intake",
] as const;
/** 含历史 id便于读旧会话 */
export function isDesignDiskWorker(workerId: string): boolean {
const id = workerId.trim();
return (
(DESIGN_DISK_WORKERS as readonly string[]).includes(
id as (typeof DESIGN_DISK_WORKERS)[number],
) ||
(LEGACY_DESIGN_DISK_WORKERS as readonly string[]).includes(
id as (typeof LEGACY_DESIGN_DISK_WORKERS)[number],
)
);
}
/** 新流程不再注入 design-common.md */
export function usesDesignCommon(_workerId: string): boolean {
return false;
}
/** worker = 工序fixed = 固定上下文phase = A/C 阶段单位 */
export type CreationUnitKind = "worker" | "fixed" | "phase";
/**
* 固定上下文示例话题 → 规格落点(提示用,不是必填问卷)。
* 仅当草稿里已有对应内容时,才作为创作单位列出(除非显式 includeCatalogFixed
*/
export type FixedContextFlavor =
| "interaction"
| "narrative_guide"
| "aesthetics"
| "input_protocol"
| "core_premises"
| "resident";
export type CreationUnitView = {
id: string;
kind: CreationUnitKind;
label: string;
/** 固定上下文族内的细分worker / phase 无此字段 */
flavor?: FixedContextFlavor;
/** 规格里是否已有实质内容 */
filled: boolean;
/** session 是否已验收本单位 */
accepted?: boolean;
/** 是否为当前正在谈的单位 */
current?: boolean;
/**
* 创作时是否应优先谈清(纲领/范式类默认真)。
* 杂项 resident 可为 false但不代表可忽略——由体验决定。
*/
weighty?: boolean;
detail?: string;
};
/** 示例话题目录:告诉 design 可以问用户类似内容;非 UI 填空项 */
export const FIXED_CONTEXT_CATALOG: Array<{
id: string;
flavor: FixedContextFlavor;
label: string;
weighty: boolean;
hint: string;
}> = [
{
id: "fixed:interaction",
flavor: "interaction",
label: "交互范式",
weighty: true,
hint: "站位、系统扮演、输出形态、与用户怎么轮转(旧称交互骨架;现多由 phase:core 收)",
},
{
id: "fixed:narrative_guide",
flavor: "narrative_guide",
label: "叙事指南",
weighty: true,
hint: "世界态度与体验边界(残酷/不有求必应/随机危险等);≠ 文风",
},
{
id: "fixed:aesthetics",
flavor: "aesthetics",
label: "美学纲领",
weighty: true,
hint: "可读终稿的呈现气质;转述 presentation 或常驻美学块",
},
{
id: "fixed:input_protocol",
flavor: "input_protocol",
label: "输入协议",
weighty: true,
hint: "() 元要求、\"\" 对白、无包裹=事实等",
},
{
id: "fixed:core_premises",
flavor: "core_premises",
label: "核心实现前提",
weighty: true,
hint: "不能瞎发挥又关键的硬前提",
},
];
/** @deprecated 旧 id读进度时兼容 */
const LEGACY_UNIT_ALIASES: Record<string, string> = {
"skeleton:interaction": "fixed:interaction",
"fixed:interaction": "phase:core",
};
export function normalizeCreationUnitId(id: string): string {
let cur = id;
const seen = new Set<string>();
while (LEGACY_UNIT_ALIASES[cur] && !seen.has(cur)) {
seen.add(cur);
cur = LEGACY_UNIT_ALIASES[cur]!;
}
return cur;
}
/** 验收判定:旧 fixed:interaction 与 phase:core 互通 */
function unitIdMatches(candidate: string, target: string): boolean {
const a = normalizeCreationUnitId(candidate);
const b = normalizeCreationUnitId(target);
if (a === b) return true;
// 双向:未规范化的旧 id 也要对上
if (
(candidate === "fixed:interaction" || candidate === "phase:core") &&
(target === "fixed:interaction" || target === "phase:core")
) {
return true;
}
return false;
}
function workerLabel(
ref: string | null,
name?: string,
role?: string,
duty?: string,
): string {
if (name?.trim()) return name.trim();
// role 若是 taxonomycore/auxiliary…不当作展示名
const roleTrim = role?.trim();
if (roleTrim && !/^(core|auxiliary|transcription)$/i.test(roleTrim)) {
return roleTrim;
}
if (ref?.trim()) return ref.trim();
if (duty?.trim()) return duty.trim().slice(0, 40);
return "(未命名 worker";
}
function workerFilled(entry: {
ref: string | null;
duty?: string;
rationale?: string;
gap?: string | null;
}): boolean {
if (entry.gap) return false;
return Boolean(entry.ref?.trim() && (entry.duty?.trim() || entry.rationale?.trim()));
}
function interactionFilled(parsed: ParsedWorkerSet): boolean {
const i = parsed.interaction;
if (!i) return false;
return Boolean(
String(i.user_stance ?? "").trim() &&
String(i.system_role ?? "").trim() &&
String(i.output ?? "").trim(),
);
}
function experienceCheckFilled(parsed: ParsedWorkerSet): boolean {
const e = parsed.experience_check;
if (!e || typeof e !== "object") return false;
return Object.values(e).some((v) => typeof v === "string" && v.trim());
}
function corePhaseFilled(parsed: ParsedWorkerSet): boolean {
return interactionFilled(parsed) || experienceCheckFilled(parsed);
}
function refinePhaseFilled(parsed: ParsedWorkerSet): boolean {
const tables = parsed.tables;
if (!tables || typeof tables !== "object") return false;
const schemas = (tables as { schemas?: unknown }).schemas;
const effects = (tables as { side_effects?: unknown }).side_effects;
return (
(Array.isArray(schemas) && schemas.length > 0) ||
(Array.isArray(effects) && effects.length > 0)
);
}
function textFilled(v: unknown): boolean {
return typeof v === "string" && v.trim().length > 0;
}
function aestheticsFilled(parsed: ParsedWorkerSet): boolean {
for (const w of parsed.workers) {
const p = w.presentation;
if (!p || typeof p !== "object") continue;
if (Object.values(p).some((x) => (typeof x === "string" ? x.trim() : x != null))) {
return true;
}
}
// 仅显式美学 id不把普通 tone/文风常驻当成「美学纲领」填空项
const residents = parseResidentContext(parsed.resident_context);
return residents.some((r) => /^(美学|aesthetics|presentation)$/i.test(r.id));
}
function inputProtocolFilled(parsed: ParsedWorkerSet): boolean {
const p = parsed.input_protocol;
if (!p || typeof p !== "object") return false;
return Object.values(p).some((v) => typeof v === "string" && v.trim());
}
function corePremisesFilled(parsed: ParsedWorkerSet): boolean {
return (parsed.core_premises?.length ?? 0) > 0;
}
function fixedFilled(flavor: FixedContextFlavor, parsed: ParsedWorkerSet): boolean {
switch (flavor) {
case "interaction":
return interactionFilled(parsed);
case "narrative_guide":
return textFilled(parsed.narrative_guide);
case "aesthetics":
return aestheticsFilled(parsed);
case "input_protocol":
return inputProtocolFilled(parsed);
case "core_premises":
return corePremisesFilled(parsed);
default:
return false;
}
}
function annotate(
unit: CreationUnitView,
accepted: Set<string>,
current: string | null,
): CreationUnitView {
const acceptedHit = [...accepted].some((id) => unitIdMatches(id, unit.id));
const currentHit =
current != null &&
(unitIdMatches(current, unit.id) || current === unit.id);
return {
...unit,
accepted: acceptedHit,
current: currentHit,
};
}
export type ListCreationUnitsOptions = {
acceptedUnitIds?: string[];
currentUnitId?: string | null;
/**
* true列出全部示例话题调试用
* 默认 false只列草稿里**已有内容**的固定块 + workers + resident——避免当成填空表。
*/
includeCatalogFixed?: boolean;
};
/**
* 列出创作单位:已写出的固定上下文 + workers + resident同级
* 空的示例话题默认不出现在列表里。
*/
export function listCreationUnits(
parsed: ParsedWorkerSet | null | undefined,
options: ListCreationUnitsOptions = {},
): CreationUnitView[] {
if (!parsed) return [];
const accepted = new Set(
(options.acceptedUnitIds ?? []).map(normalizeCreationUnitId),
);
const currentRaw = options.currentUnitId?.trim() || null;
const current = currentRaw ? normalizeCreationUnitId(currentRaw) : null;
const includeCatalog = options.includeCatalogFixed === true;
const units: CreationUnitView[] = [];
const coreFilled = corePhaseFilled(parsed);
if (includeCatalog || coreFilled) {
units.push(
annotate(
{
id: "phase:core",
kind: "phase",
label: "A · 核心",
filled: coreFilled,
weighty: true,
detail: "站位 / 系统扮演 / 体验骨架",
},
accepted,
current,
),
);
}
for (const cat of FIXED_CONTEXT_CATALOG) {
// interaction 已并入 phase:core避免重复列出
if (cat.id === "fixed:interaction") continue;
const filled = fixedFilled(cat.flavor, parsed);
if (!includeCatalog && !filled) continue;
units.push(
annotate(
{
id: cat.id,
kind: "fixed",
flavor: cat.flavor,
label: cat.label,
filled,
weighty: cat.weighty,
detail: cat.hint,
},
accepted,
current,
),
);
}
for (let i = 0; i < parsed.workers.length; i++) {
const w = parsed.workers[i]!;
const ref = w.ref?.trim() || null;
const id = ref ? `worker:${ref}` : `worker:#${i + 1}`;
units.push(
annotate(
{
id,
kind: "worker",
label: workerLabel(ref, w.name, w.role, w.duty),
filled: workerFilled(w),
weighty: true,
detail: w.duty?.trim() || w.rationale?.trim(),
},
accepted,
current,
),
);
}
const residents = parseResidentContext(parsed.resident_context);
for (const r of residents) {
// 已由 aesthetics 启发式覆盖的 tone 类仍单独列出(挂载/正文可单独验收)
const id = `resident:${r.id}`;
units.push(
annotate(
{
id,
kind: "fixed",
flavor: "resident",
label: r.id,
filled: Boolean(r.content.trim()),
weighty: false,
detail: r.content.trim().slice(0, 80),
},
accepted,
current,
),
);
}
const refineFilled = refinePhaseFilled(parsed);
if (includeCatalog || refineFilled) {
units.push(
annotate(
{
id: "phase:refine",
kind: "phase",
label: "C · 细化",
filled: refineFilled,
weighty: true,
detail: "表 / 副作用 / 数据拓扑",
},
accepted,
current,
),
);
}
return units;
}
/**
* 下一个未验收单位(软启发,非硬闸):
* 核心 → 已开写的固定块 → 纲领类固定上下文 → worker → 其余 → 细化
*
* 固定上下文先于 worker便于同一份风格/叙事/美学挂到多个 worker
* 避免「先写完 worker 再逐个填上下文」。
* 已开写的 fixed/phase 仍优先续完filled pending
*/
export function nextCreationUnitId(
parsed: ParsedWorkerSet | null | undefined,
acceptedUnitIds: string[] = [],
): string | null {
if (!parsed) return "phase:core";
const units = listCreationUnits(parsed, {
acceptedUnitIds,
includeCatalogFixed: true,
});
const core = units.find((u) => u.id === "phase:core");
if (core && !core.accepted) return "phase:core";
const filledFixedPending = units.find(
(u) =>
(u.kind === "fixed" || u.kind === "phase") &&
u.id !== "phase:core" &&
!u.accepted &&
u.filled,
);
if (filledFixedPending) return filledFixedPending.id;
const foundationFixed = units.find(
(u) => u.kind === "fixed" && u.weighty && !u.accepted,
);
if (foundationFixed) return foundationFixed.id;
const filledWorkerPending = units.find(
(u) => u.kind === "worker" && !u.accepted && u.filled,
);
if (filledWorkerPending) return filledWorkerPending.id;
const worker = units.find((u) => u.kind === "worker" && !u.accepted);
if (worker) return worker.id;
const otherFixed = units.find(
(u) => u.kind === "fixed" && !u.weighty && !u.accepted,
);
if (otherFixed) return otherFixed.id;
const refine = units.find((u) => u.id === "phase:refine");
if (refine && !refine.accepted) return "phase:refine";
return null;
}
export function parseAcceptedUnits(raw: unknown): string[] {
if (Array.isArray(raw)) {
return raw.map((x) => String(x).trim()).filter(Boolean).map(normalizeCreationUnitId);
}
if (typeof raw === "string" && raw.trim()) {
try {
const doc = JSON.parse(raw) as unknown;
if (Array.isArray(doc)) {
return doc.map((x) => String(x).trim()).filter(Boolean).map(normalizeCreationUnitId);
}
} catch {
return raw
.split(/[,\n]/)
.map((s) => s.trim())
.filter(Boolean)
.map(normalizeCreationUnitId);
}
}
return [];
}
export function isFinalWorkerSetArtifact(artifact: {
workerId: string;
outputTags: string[];
}): boolean {
return artifact.outputTags.includes(WORKER_SET_FINAL_TAG);
}
/** 创作磁盘 skill 产出但尚未写终稿 tag → 单位验收 */
export function isDesignUnitArtifact(artifact: {
workerId: string;
outputTags: string[];
}): boolean {
return (
isDesignDiskWorker(artifact.workerId) && !isFinalWorkerSetArtifact(artifact)
);
}
/** 总管选 skill有流程待执行 → design-step否则 design-flow */
export function designWorkerForUnit(unitId: string | null | undefined): DesignDiskWorkerId {
const id = (unitId ?? "").trim();
if (id === "flow" || !id) return "design-flow";
return "design-step";
}
export type AcceptedUnitContentEntry = {
unitId: string;
acceptedAt: string;
summary?: string;
/** 该单位验收时的内容切片 */
content: unknown;
};
export type AcceptedContentStore = {
version: 1;
units: Record<string, AcceptedUnitContentEntry>;
};
export function parseAcceptedContentStore(raw: unknown): AcceptedContentStore {
if (typeof raw === "string" && raw.trim()) {
try {
return parseAcceptedContentStore(JSON.parse(raw) as unknown);
} catch {
return { version: 1, units: {} };
}
}
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
return { version: 1, units: {} };
}
const row = raw as Record<string, unknown>;
const unitsRaw = row.units;
const units: Record<string, AcceptedUnitContentEntry> = {};
if (unitsRaw && typeof unitsRaw === "object" && !Array.isArray(unitsRaw)) {
for (const [k, v] of Object.entries(unitsRaw as Record<string, unknown>)) {
if (!v || typeof v !== "object" || Array.isArray(v)) continue;
const e = v as Record<string, unknown>;
const unitId = normalizeCreationUnitId(
typeof e.unitId === "string" ? e.unitId : k,
);
units[unitId] = {
unitId,
acceptedAt:
typeof e.acceptedAt === "string" ? e.acceptedAt : new Date().toISOString(),
summary: typeof e.summary === "string" ? e.summary : undefined,
content: e.content,
};
}
}
return { version: 1, units };
}
/** 从草稿抽出某一创作单位在验收时的内容 */
export function extractUnitContentFromDraft(
parsed: ParsedWorkerSet | null | undefined,
unitId: string,
): unknown | null {
if (!parsed) return null;
const id = normalizeCreationUnitId(unitId.trim());
if (!id) return null;
if (id === "phase:core") {
const slice: Record<string, unknown> = {};
if (parsed.interaction) slice.interaction = parsed.interaction;
if (parsed.experience_check) slice.experience_check = parsed.experience_check;
return Object.keys(slice).length ? slice : null;
}
if (id === "phase:refine") {
return parsed.tables ? { tables: parsed.tables } : null;
}
if (id.startsWith("worker:")) {
const ref = id.slice("worker:".length);
const entry = parsed.workers.find((w) => (w.ref?.trim() || "") === ref);
return entry ?? null;
}
if (id === "fixed:narrative_guide") {
return textFilled(parsed.narrative_guide)
? { narrative_guide: parsed.narrative_guide }
: null;
}
if (id === "fixed:input_protocol") {
return parsed.input_protocol ? { input_protocol: parsed.input_protocol } : null;
}
if (id === "fixed:core_premises") {
return (parsed.core_premises?.length ?? 0) > 0
? { core_premises: parsed.core_premises }
: null;
}
if (id === "fixed:aesthetics") {
const presentations = parsed.workers
.filter((w) => w.presentation)
.map((w) => ({ ref: w.ref, presentation: w.presentation }));
return presentations.length ? { presentations } : null;
}
if (id === "fixed:interaction") {
return parsed.interaction ? { interaction: parsed.interaction } : null;
}
if (id.startsWith("resident:")) {
const rid = id.slice("resident:".length);
const residents = parseResidentContext(parsed.resident_context);
const hit = residents.find((r) => r.id === rid);
return hit ?? null;
}
return null;
}
/** 写入/覆盖某一单位的最后验收内容 */
export function upsertAcceptedUnitContent(
existingRaw: unknown,
entry: {
unitId: string;
content: unknown;
summary?: string;
acceptedAt?: string;
},
): string {
const store = parseAcceptedContentStore(existingRaw);
const unitId = normalizeCreationUnitId(entry.unitId);
store.units[unitId] = {
unitId,
acceptedAt: entry.acceptedAt ?? new Date().toISOString(),
summary: entry.summary,
content: entry.content,
};
return JSON.stringify(store, null, 2);
}
/** 前情提要:把已验收单位内容格式化为只读 Markdown */
export function formatAcceptedContentForPrompt(raw: unknown): string {
const store = parseAcceptedContentStore(raw);
const entries = Object.values(store.units).sort((a, b) =>
a.acceptedAt.localeCompare(b.acceptedAt),
);
if (entries.length === 0) {
return "(尚无已验收单位)";
}
return entries
.map((e) => {
const head = `### ${e.unitId}${e.summary ? ` · ${e.summary}` : ""}`;
const meta = `验收于 ${e.acceptedAt} · **只读,勿擅自改写**`;
const body =
typeof e.content === "string"
? e.content
: JSON.stringify(e.content ?? null, null, 2);
return `${head}\n${meta}\n\n\`\`\`json\n${body}\n\`\`\``;
})
.join("\n\n");
}
/**
* 完整 Worker 集是否可交终稿。
* 具名 weighty 固定单位:已填的必须已验收;至少 1 个 filled worker 已验收。
*/
export function isWorkerSetReadyForFinal(
parsed: ParsedWorkerSet | null | undefined,
acceptedUnitIds: string[],
): { ready: boolean; missing: string[] } {
if (!parsed) return { ready: false, missing: ["(无草稿)"] };
const accepted = acceptedUnitIds.map(normalizeCreationUnitId);
const units = listCreationUnits(parsed, { acceptedUnitIds: accepted });
const missing = units
.filter((u) => u.weighty && u.filled && !u.accepted)
.map((u) => u.id);
const acceptedWorkers = units.filter(
(u) => u.kind === "worker" && u.accepted && u.filled,
);
const interaction = units.find((u) => u.id === "fixed:interaction");
const core = units.find((u) => u.id === "phase:core");
if (core && core.filled && !core.accepted) {
return { ready: false, missing: missing.length ? missing : [core.id] };
}
if (interaction && !interaction.accepted && interaction.filled) {
return { ready: false, missing: missing.length ? missing : [interaction.id] };
}
if (acceptedWorkers.length === 0) {
return {
ready: false,
missing: missing.length ? missing : ["(至少一个 worker 单位)"],
};
}
if (missing.length) return { ready: false, missing };
return { ready: true, missing: [] };
}

View File

@@ -0,0 +1,260 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import { parse as parseYaml } from "yaml";
import type { Blackboard } from "../blackboard/blackboard.js";
import type { RuntimeSession } from "../types/runtime.js";
import type { AcceptanceMode } from "../types/runtime.js";
import type { ParsedWorkerSkill } from "./types.js";
import {
parseWorkerSetYaml,
type ParsedWorkerSet,
type WorkerSetEntry,
} from "./worker-set-parse.js";
import {
buildInstanceWorkerDeclaration,
inferLifecycleStage,
readWorkerSetYamlForDeclaration,
} from "./worker-declaration.js";
import { loadWorkerSkill, SKILLS_ROOT } from "./loader.js";
import { CONTEXT_BRIEF_TAG } from "../runtime/compress-after-worker.js";
import { isDesignDiskWorker } from "./creation-units.js";
import {
entriesForWorker,
formatResidentPromptSection,
parseResidentContext,
residentTagFor,
} from "./resident-context.js";
type WorkerTemplateDoc = {
id?: string;
label?: string;
duty?: string;
prompt_excerpt?: string;
suggested_context?: { static?: string[]; dynamic?: string[] };
suggested_outputs?: string[];
};
/**
* 解析本次 worker 的验收模式(创作 / run 共用入口)。
* - design-* / opening-generator 磁盘创作 worker → 始终 user_confirmed
* - Worker 集 acceptance: review → user_confirmedcontinue → no_confirmation
* - 未声明 acceptancedesign 生命周期默认确认play 缺省按 review稳妥
*/
export function resolveAcceptanceModeForWorker(params: {
session: RuntimeSession;
blackboard: Blackboard;
workerId: string;
}): AcceptanceMode {
const workerId = params.workerId.trim();
if (isDesignDiskWorker(workerId) || workerId === "opening-generator") {
return "user_confirmed";
}
const decl = buildInstanceWorkerDeclaration(
params.session,
params.blackboard,
inferLifecycleStage(params.session),
);
const entry = decl.parsed?.workers.find((w) => w.ref?.trim() === workerId);
if (entry?.acceptance === "continue") return "no_confirmation";
if (entry?.acceptance === "review") return "user_confirmed";
// 未写明:创作阶段默认验收;游玩缺省也验收(避免静默连跑)
return "user_confirmed";
}
/** 从声明 + 可选模板构建可执行 worker有磁盘 SKILL 时优先磁盘design-intake */
export async function resolveRunnableWorker(params: {
skillPackName: string;
workerId: string;
session: RuntimeSession;
blackboard: Blackboard;
skillsRoot?: string;
}): Promise<{
worker: ParsedWorkerSkill;
promptBody: string;
source: "disk" | "declaration";
}> {
const root = params.skillsRoot ?? SKILLS_ROOT;
try {
const { loadWorkerSkillWithContext } = await import("./loader.js");
const flowRaw = params.blackboard.getContentByTag("设计.创作流程");
const currentStepName = params.blackboard.getContentByTag("创作.当前步骤");
const selectedRecipeRef = params.blackboard.getContentByTag("创作.选用配方");
const acceptedRaw =
params.session.slots?.creationAcceptedUnits ??
params.blackboard.getContentByTag("创作.已验收单位");
const { parseAcceptedSteps } = await import("./creation-flow.js");
const withCtx = await loadWorkerSkillWithContext(
params.skillPackName,
params.workerId,
root,
{
flowRaw,
currentStepName,
acceptedStepNames: parseAcceptedSteps(acceptedRaw),
selectedRecipeRef,
},
);
return {
worker: withCtx.worker,
promptBody: withCtx.promptBody,
source: "disk",
};
} catch {
// fall through to declaration
}
const raw = readWorkerSetYamlForDeclaration(params.blackboard, params.session);
const parsed = raw ? parseWorkerSetYaml(raw.yaml) : null;
const entry = parsed?.workers.find(
(w) => w.ref?.trim() === params.workerId.trim(),
);
if (!entry?.ref) {
throw new Error(
`未找到 worker「${params.workerId}」的磁盘 SKILL且 设计.worker集 中无对应声明`,
);
}
const template = await loadWorkerTemplate(
params.skillPackName,
entry.ref,
root,
);
const built = buildDeclaredWorkerSkill({
skillPackName: params.skillPackName,
entry,
template,
workerSet: parsed,
});
return { ...built, source: "declaration" };
}
async function loadWorkerTemplate(
skillPackName: string,
workerId: string,
skillsRoot: string,
): Promise<WorkerTemplateDoc | null> {
const { loadSkill } = await import("./loader.js");
let packRoot: string | undefined;
try {
const skill = await loadSkill(skillPackName, skillsRoot);
packRoot = skill.skillPackRoot;
} catch {
return null;
}
if (!packRoot) return null;
const file = path.join(
skillsRoot,
packRoot,
"worker-templates",
`${workerId}.yaml`,
);
try {
const raw = await readFile(file, "utf8");
const doc = parseYaml(raw) as WorkerTemplateDoc;
return doc && typeof doc === "object" ? doc : null;
} catch {
return null;
}
}
export function buildDeclaredWorkerSkill(params: {
skillPackName: string;
entry: WorkerSetEntry;
template: WorkerTemplateDoc | null;
workerSet: ParsedWorkerSet | null;
}): { worker: ParsedWorkerSkill; promptBody: string } {
const id = params.entry.ref!.trim();
const staticTags =
params.entry.context?.static ??
params.template?.suggested_context?.static ??
["设计.worker集", CONTEXT_BRIEF_TAG];
const dynamicTags =
params.entry.context?.dynamic ??
params.template?.suggested_context?.dynamic ??
["用户.最新输入"];
const outputTags =
params.entry.outputs?.length
? params.entry.outputs
: params.template?.suggested_outputs?.length
? params.template.suggested_outputs
: ["输出.用户展示"];
const resident = parseResidentContext(params.workerSet?.resident_context);
const residentForWorker = entriesForWorker(resident, id);
const residentStaticTags = residentForWorker
.filter((e) => e.position !== "dynamic")
.map(residentTagFor);
const residentDynamicTags = residentForWorker
.filter((e) => e.position === "dynamic")
.map(residentTagFor);
const inputTags = [
...new Set([
...staticTags,
...residentStaticTags,
...dynamicTags,
...residentDynamicTags,
CONTEXT_BRIEF_TAG,
]),
];
const duty =
params.entry.duty?.trim() ||
params.template?.duty?.trim() ||
`执行 ${id}`;
const excerpt = params.template?.prompt_excerpt?.trim() || "";
const presentation = params.entry.presentation
? JSON.stringify(params.entry.presentation, null, 2)
: "";
const narrative = params.workerSet?.narrative_guide?.trim() || "";
const premises = (params.workerSet?.core_premises ?? []).filter(Boolean);
const residentSection = formatResidentPromptSection(resident, id);
const body = [
`# ${params.template?.label ?? id}`,
"",
"## 角色与职责",
"",
duty,
"",
excerpt ? `## 写法要点\n\n${excerpt}` : "",
presentation ? `## presentation实例\n\n\`\`\`json\n${presentation}\n\`\`\`` : "",
narrative ? `## 叙事指南\n\n${narrative}` : "",
premises.length
? `## 核心实现前提\n\n${premises.map((p) => `- ${p}`).join("\n")}`
: "",
residentSection,
params.entry.rationale
? `## 为何需要本 worker\n\n${params.entry.rationale}`
: "",
"",
"## 输出",
"",
`写入 outputTags${outputTags.join("、")}`,
]
.filter(Boolean)
.join("\n");
const worker: ParsedWorkerSkill = {
id,
skill: params.skillPackName,
name: params.template?.label ?? id,
description: duty.slice(0, 200),
version: 1,
inputTags,
outputTags,
inputMerge: "latest",
path: `declaration:${id}`,
body,
};
const promptBody = [
"(本 worker 由 设计.worker集 声明驱动,无独立磁盘 SKILL。",
"",
body,
].join("\n");
return { worker, promptBody };
}

View File

@@ -2,7 +2,7 @@ import { readFile, readdir, stat } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { parse as parseYaml } from "yaml";
import type { BookKind } from "../types/runtime.js";
import type { BookKind, SkillStartupMode } from "../types/runtime.js";
import type {
ParsedSkill,
ParsedWorkerSkill,
@@ -10,13 +10,17 @@ import type {
SkillWorkerLlmBindings,
StartupInquiry,
} from "./types.js";
import { parseContextSegments } from "./context-segments.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const SKILLS_ROOT = path.resolve(__dirname, "../../skills");
export const ORCHESTRATOR_FILENAME = "orchestrator.md";
export const WORKER_SKILL_FILENAME = "SKILL.md";
/** 部分环境写 SKILL.md 会损坏非 ASCII允许同目录 body.md 作为回退 */
export const WORKER_SKILL_FALLBACK_FILENAME = "body.md";
export const DEFAULT_SHARED_CONTEXT_FILENAME = "shared-context.md";
export const DESIGN_COMMON_FILENAME = "design-common.md";
export const LLM_BINDINGS_FILENAME = "llm-bindings.yaml";
/** 按 Book 形态分文件夹skill 可为平铺 .md 或 {name}/orchestrator.md 包 */
@@ -28,11 +32,10 @@ type RegistryDoc = {
>;
};
/** 解析 YAML frontmatter仅支持本项目用到的简单字段 */
function parseFrontmatter(raw: string): {
meta: Record<string, string | string[] | number>;
body: string;
} {
type FrontmatterMeta = Record<string, unknown>;
/** 解析 YAML frontmatter使用 yaml 包,支持折叠标量与列表) */
function parseFrontmatter(raw: string): { meta: FrontmatterMeta; body: string } {
if (!raw.startsWith("---")) {
return { meta: {}, body: raw };
}
@@ -40,44 +43,17 @@ function parseFrontmatter(raw: string): {
if (end === -1) {
return { meta: {}, body: raw };
}
const yaml = raw.slice(3, end).trim();
const yamlText = raw.slice(3, end).trim();
const body = raw.slice(end + 4).trim();
const meta: Record<string, string | string[] | number> = {};
let currentKey = "";
let listItems: string[] = [];
let inList = false;
const flushList = () => {
if (inList && currentKey) {
meta[currentKey] = listItems;
listItems = [];
inList = false;
}
};
for (const line of yaml.split("\n")) {
const listMatch = line.match(/^\s+-\s+(.+)$/);
if (listMatch && inList) {
listItems.push(listMatch[1].trim());
continue;
}
flushList();
const kv = line.match(/^([\w-]+):\s*(.*)$/);
if (!kv) continue;
const [, key, value] = kv;
currentKey = key;
if (value === "" || value === ">-" || value === "|") {
inList = true;
listItems = [];
} else if (value === ">-" || value.startsWith(">")) {
meta[key] = value;
} else {
meta[key] = value.trim();
inList = false;
let meta: FrontmatterMeta = {};
try {
const parsed = parseYaml(yamlText);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
meta = parsed as FrontmatterMeta;
}
} catch {
meta = {};
}
flushList();
return { meta, body };
}
@@ -119,14 +95,17 @@ function parseStartupInquiry(section: string): StartupInquiry {
return { prompt, targetKey, requiredFields: required, optionalFields: optional };
}
function metaString(meta: Record<string, string | string[] | number>, key: string): string {
function metaString(meta: FrontmatterMeta, key: string): string {
const v = meta[key];
return typeof v === "string" ? v : "";
if (typeof v === "string") return v;
if (typeof v === "number") return String(v);
return "";
}
function metaStringArray(meta: Record<string, string | string[] | number>, key: string): string[] {
function metaStringArray(meta: FrontmatterMeta, key: string): string[] {
const v = meta[key];
return Array.isArray(v) ? v : [];
if (!Array.isArray(v)) return [];
return v.map((item) => String(item));
}
function parseBookKind(value: string): BookKind | undefined {
@@ -147,7 +126,7 @@ function skillPackRootFromPath(relativePath: string): string | undefined {
return undefined;
}
function workerIdsFromMeta(meta: Record<string, string | string[] | number>): string[] {
function workerIdsFromMeta(meta: FrontmatterMeta): string[] {
const workers = metaStringArray(meta, "workers");
if (workers.length > 0) return workers;
return metaStringArray(meta, "suggestedWorkers");
@@ -227,7 +206,10 @@ async function parseSkillFile(
const fullPath = path.join(skillsRoot, relativePath);
const raw = await readFile(fullPath, "utf8");
const { meta, body } = parseFrontmatter(raw);
const startupSection = extractSection(body, "启动询问");
const startupSection =
extractSection(body, "启动询问") ||
extractSection(body, "启动agent-first") ||
extractSection(body, "启动");
const folderBookKind = bookKindFromRelativePath(relativePath);
const category = metaString(meta, "category") || folderBookKind || "custom";
const bookKind =
@@ -248,19 +230,35 @@ async function parseSkillFile(
bookKind,
path: normalizedPath,
skillPackRoot: packRoot,
version: typeof meta.version === "number" ? meta.version : Number(meta.version) || 1,
version:
typeof meta.version === "number"
? meta.version
: Number(metaString(meta, "version")) || 1,
defaultFlowId: metaString(meta, "defaultFlowId") || undefined,
suggestedWorkers: workerIdsFromMeta(meta),
tags: metaStringArray(meta, "tags"),
sharedContextPath: resolveSharedContextPath(meta, packRoot),
workerLlmBindings,
startupInquiry: parseStartupInquiry(startupSection),
startupInquiry: {
...parseStartupInquiry(startupSection),
targetKey:
metaString(meta, "demandTag") ||
parseStartupInquiry(startupSection).targetKey,
},
startupMode: parseStartupMode(metaString(meta, "startupMode")),
uiPrompt: metaString(meta, "uiPrompt") || undefined,
body,
};
}
function parseStartupMode(raw: string): SkillStartupMode | undefined {
if (raw === "agent-first" || raw === "design-intake") return "agent-first";
if (raw === "intake") return "intake";
return undefined;
}
function resolveSharedContextPath(
meta: Record<string, string | string[] | number>,
meta: FrontmatterMeta,
packRoot: string | undefined,
): string | undefined {
if (!packRoot) return undefined;
@@ -291,17 +289,22 @@ async function parseWorkerSkillFile(
? inputMergeRaw
: undefined;
const llmProfileId = metaString(meta, "llmProfileId") || undefined;
const contextSegments = parseContextSegments(meta.contextSegments);
return {
id: metaString(meta, "id") || idFromPath,
skill: metaString(meta, "skill"),
name: metaString(meta, "name") || idFromPath,
description: metaString(meta, "description"),
version: typeof meta.version === "number" ? meta.version : Number(meta.version) || 1,
version:
typeof meta.version === "number"
? meta.version
: Number(metaString(meta, "version")) || 1,
inputTags,
outputTags,
inputMerge,
llmProfileId,
contextSegments: contextSegments.length ? contextSegments : undefined,
path: normalizedPath,
body,
};
@@ -394,7 +397,7 @@ export async function loadSkill(
return parseSkillFile(relativePath, skillsRoot);
}
/** 解析 skill 包内 worker 的 SKILL.md 相对路径 */
/** 解析 skill 包内 worker 的 SKILL.md(或 body.md 回退)相对路径 */
export async function resolveWorkerSkillPath(
skillIdOrName: string,
workerId: string,
@@ -404,13 +407,25 @@ export async function resolveWorkerSkillPath(
if (!skill.skillPackRoot) {
return null;
}
const relativePath = `${skill.skillPackRoot}/workers/${workerId}/${WORKER_SKILL_FILENAME}`;
try {
await readFile(path.join(skillsRoot, relativePath), "utf8");
return relativePath;
} catch {
return null;
const base = `${skill.skillPackRoot}/workers/${workerId}`;
for (const filename of [WORKER_SKILL_FILENAME, WORKER_SKILL_FALLBACK_FILENAME]) {
const relativePath = `${base}/${filename}`;
try {
const raw = await readFile(path.join(skillsRoot, relativePath), "utf8");
// 损坏的 SKILL.md中文变 ?)时跳过,改用 body.md
if (
filename === WORKER_SKILL_FILENAME &&
/\?\?/.test(raw) &&
!/[\u4e00-\u9fff]/.test(raw)
) {
continue;
}
return relativePath;
} catch {
/* try next */
}
}
return null;
}
/** 加载 skill 包固定上下文(注入所有 worker prompt 开头) */
@@ -428,18 +443,146 @@ export async function loadSkillSharedContext(
}
}
/** 加载 worker skill 正文,可选拼接 skill 包固定上下文 */
/** 创作分步 skill 的共同开头(仅 design-* */
export async function loadDesignCommon(
skillIdOrName: string,
skillsRoot = SKILLS_ROOT,
): Promise<string | null> {
const skill = await loadSkill(skillIdOrName, skillsRoot);
if (!skill.skillPackRoot) return null;
const fullPath = path.join(
skillsRoot,
skill.skillPackRoot,
DESIGN_COMMON_FILENAME,
);
try {
return await readFile(fullPath, "utf8");
} catch {
return null;
}
}
/** 加载 worker skill 正文design-flow 注入目录design-step 注入模块 prompt + 动态 tag */
export async function loadWorkerSkillWithContext(
skillIdOrName: string,
workerId: string,
skillsRoot = SKILLS_ROOT,
opts?: {
flowRaw?: string | null;
currentStepName?: string | null;
acceptedStepNames?: readonly string[];
/** 用户手动选定的配方 id / 名 */
selectedRecipeRef?: string | null;
},
): Promise<{ worker: ParsedWorkerSkill; sharedContext: string | null; promptBody: string }> {
const worker = await loadWorkerSkill(skillIdOrName, workerId, skillsRoot);
const sharedContext = await loadSkillSharedContext(skillIdOrName, skillsRoot);
const promptBody = sharedContext
? `# 固定创作上下文\n\n${sharedContext}\n\n---\n\n${worker.body}`
: worker.body;
return { worker, sharedContext, promptBody };
const skill = await loadSkill(skillIdOrName, skillsRoot);
let moduleCatalogBlock: string | null = null;
let modulePromptBlock: string | null = null;
let patchedWorker = worker;
if (workerId.trim() === "design-flow" && skill.skillPackRoot) {
const {
loadModuleCatalog,
resolveSelectedRecipeDetail,
formatDesignFlowContentBlocks,
} = await import("./creation-flow.js");
const modules = await loadModuleCatalog(skill.skillPackRoot, skillsRoot);
const selectedRecipe = await resolveSelectedRecipeDetail({
skillPackRoot: skill.skillPackRoot,
selectedRecipeRef: opts?.selectedRecipeRef,
skillsRoot,
});
const blocks = formatDesignFlowContentBlocks({
selectedRecipe,
modules,
missingSelection: !selectedRecipe,
});
if (blocks.length) {
moduleCatalogBlock = blocks.join("\n\n");
}
}
if (workerId.trim() === "design-step" && skill.skillPackRoot) {
const {
resolveDesignStepBinding,
CREATION_CURRENT_STEP_TAG,
CREATION_MODULE_OPENING_TAG,
} = await import("./creation-flow.js");
const binding = await resolveDesignStepBinding({
skillPackRoot: skill.skillPackRoot,
flowRaw: opts?.flowRaw,
currentStepName: opts?.currentStepName,
acceptedStepNames: opts?.acceptedStepNames,
skillsRoot,
});
if (binding) {
const openingNote = binding.opening
? `\n\n【程序开场】若黑板有「${CREATION_MODULE_OPENING_TAG}」,该默认问题已由程序发给用户(不经 LLM用户首答在「用户.worker答复」。勿重复同一开场白在其答复与提示词基础上继续追问或产出。`
: "";
modulePromptBlock = `## 【本步方法 · ${binding.module.name}\n\n${binding.modulePrompt.trim()}${openingNote}`;
const baseInputs = [
"用户.需求",
"book.brief",
"用户.最新输入",
"用户.worker答复",
"用户.修订说明",
"设计.创作流程",
CREATION_CURRENT_STEP_TAG,
CREATION_MODULE_OPENING_TAG,
...binding.depTags,
];
const inputTags = [...new Set(baseInputs)];
const outputTags = [
binding.module.artifact,
CREATION_CURRENT_STEP_TAG,
];
const depSegments = binding.depTags.map((tag, i) => ({
id: `dep-${i}`,
tier: "static" as const,
tags: [tag],
label: `## 【依赖产物 · ${tag}】只读`,
}));
const openingSegment = binding.opening
? [
{
id: "module-opening",
tier: "static" as const,
tags: [CREATION_MODULE_OPENING_TAG],
label: "## 【本步默认问题 · 程序已发出】只读",
},
]
: [];
patchedWorker = {
...worker,
name: `创作 · ${binding.module.name}`,
description: binding.module.declaration,
inputTags,
outputTags,
contextSegments: [
...(worker.contextSegments ?? []),
...openingSegment,
...depSegments,
],
};
}
}
const parts: string[] = [];
if (sharedContext?.trim()) {
parts.push(`# 固定创作上下文\n\n${sharedContext.trim()}`);
}
if (moduleCatalogBlock?.trim()) {
parts.push(moduleCatalogBlock.trim());
}
if (modulePromptBlock?.trim()) {
parts.push(modulePromptBlock.trim());
}
parts.push(patchedWorker.body);
const promptBody = parts.join("\n\n---\n\n");
return { worker: patchedWorker, sharedContext, promptBody };
}
/** 加载 skill 包内专属 worker skill */
@@ -473,9 +616,10 @@ export async function listWorkerSkills(
}
const workers: ParsedWorkerSkill[] = [];
for (const entry of entries.sort()) {
const skillPath = `${skill.skillPackRoot}/workers/${entry}/${WORKER_SKILL_FILENAME}`;
const relativePath = await resolveWorkerSkillPath(skillIdOrName, entry, skillsRoot);
if (!relativePath) continue;
try {
workers.push(await parseWorkerSkillFile(skillPath, skillsRoot));
workers.push(await parseWorkerSkillFile(relativePath, skillsRoot));
} catch {
// skip
}

View File

@@ -0,0 +1,134 @@
/**
* Worker / Agent 结构化追问协议。
* UI左右分页选项卡经 composer 发送payload 须含问+答(可附自由补充)。
*/
import type {
QuestionAnswer,
QuestionItem,
QuestionOption,
} from "../types/questions.js";
export type { QuestionAnswer, QuestionItem, QuestionOption };
function letterId(i: number): string {
return String.fromCharCode(65 + (i % 26));
}
/** 把 askUser 原始值规范成 QuestionItem[](兼容纯 string[] */
export function normalizeQuestions(raw: unknown): QuestionItem[] {
if (!Array.isArray(raw)) return [];
const out: QuestionItem[] = [];
for (let i = 0; i < raw.length; i++) {
const item = raw[i];
if (typeof item === "string") {
const prompt = item.trim();
if (!prompt) continue;
out.push({
id: `q${i + 1}`,
prompt,
allowOther: true,
required: true,
});
continue;
}
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const prompt =
typeof row.prompt === "string"
? row.prompt.trim()
: typeof row.question === "string"
? row.question.trim()
: typeof row.text === "string"
? row.text.trim()
: "";
if (!prompt) continue;
const id =
typeof row.id === "string" && row.id.trim()
? row.id.trim()
: `q${i + 1}`;
let options: QuestionOption[] | undefined;
if (Array.isArray(row.options)) {
options = [];
for (let j = 0; j < row.options.length; j++) {
const opt = row.options[j];
if (typeof opt === "string") {
const label = opt.trim();
if (!label) continue;
options.push({ id: letterId(j), label, editable: true });
continue;
}
if (!opt || typeof opt !== "object" || Array.isArray(opt)) continue;
const o = opt as Record<string, unknown>;
const label =
typeof o.label === "string"
? o.label.trim()
: typeof o.text === "string"
? o.text.trim()
: "";
if (!label) continue;
options.push({
id:
typeof o.id === "string" && o.id.trim()
? o.id.trim()
: letterId(j),
label,
editable: o.editable === false ? false : true,
});
}
if (options.length === 0) options = undefined;
}
out.push({
id,
prompt,
options,
allowOther: row.allowOther === false ? false : true,
required: row.required === false ? false : true,
});
}
return out;
}
/** 发给 AI必须含问题与答案可选自由补充 */
export function formatQuestionAnswersForAi(
questions: QuestionItem[],
answers: QuestionAnswer[],
note?: string,
): string {
const byId = new Map(answers.map((a) => [a.questionId, a]));
const lines: string[] = ["【追问作答】"];
for (const q of questions) {
const a = byId.get(q.id);
const answer = a?.text?.trim() || "(未答)";
lines.push(`问:${q.prompt}`);
lines.push(`答:${answer}`);
lines.push("");
}
const trimmedNote = note?.trim();
if (trimmedNote) {
lines.push("【补充】");
lines.push(trimmedNote);
}
return lines.join("\n").trim();
}
/** 气泡摘要:答句 + 可选补充 */
export function formatQuestionAnswersForDisplay(
questions: QuestionItem[],
answers: QuestionAnswer[],
note?: string,
): string {
const byId = new Map(answers.map((a) => [a.questionId, a]));
const parts: string[] = [];
for (const q of questions) {
const a = byId.get(q.id);
if (!a?.text?.trim()) continue;
parts.push(a.text.trim());
}
const trimmedNote = note?.trim();
if (trimmedNote) parts.push(trimmedNote);
return parts.length ? parts.join("\n") : "(已提交追问作答)";
}
export function questionPrompts(questions: QuestionItem[]): string[] {
return questions.map((q) => q.prompt).filter(Boolean);
}

View File

@@ -0,0 +1,128 @@
/**
* 常驻上下文:按 Worker 集 resident_context 挂载到指定 worker。
* 写入黑板 tag并并入该 worker 的 static 输入。
*/
import type { Blackboard } from "../blackboard/blackboard.js";
export type ResidentContextEntry = {
id: string;
/** 注入位提示static | dynamic拼装分层 */
position?: "static" | "dynamic";
importance?: number;
content: string;
/** 挂载到哪些 worker ref空 = 全部声明 worker */
mount?: string[];
/** 显式 tag默认 上下文.常驻.{id} */
tag?: string;
};
export function parseResidentContext(raw: unknown): ResidentContextEntry[] {
if (!Array.isArray(raw)) return [];
const out: ResidentContextEntry[] = [];
for (const item of raw) {
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
const row = item as Record<string, unknown>;
const id =
typeof row.id === "string"
? row.id.trim()
: typeof row.key === "string"
? row.key.trim()
: "";
const content =
typeof row.content === "string"
? row.content
: typeof row.text === "string"
? row.text
: typeof row.summary === "string"
? row.summary
: "";
if (!id || !content.trim()) continue;
const mount = Array.isArray(row.mount)
? row.mount
.filter((m): m is string => typeof m === "string" && m.trim().length > 0)
.map((m) => m.trim())
: Array.isArray(row.workers)
? row.workers
.filter((m): m is string => typeof m === "string" && m.trim().length > 0)
.map((m) => m.trim())
: undefined;
const position =
row.position === "dynamic" || row.tier === "dynamic"
? "dynamic"
: row.position === "static" || row.tier === "static"
? "static"
: "static";
out.push({
id,
position,
importance:
typeof row.importance === "number" ? row.importance : undefined,
content: content.trim(),
mount,
tag: typeof row.tag === "string" && row.tag.trim() ? row.tag.trim() : undefined,
});
}
return out.sort((a, b) => (b.importance ?? 0) - (a.importance ?? 0));
}
export function residentTagFor(entry: ResidentContextEntry): string {
return entry.tag ?? `上下文.常驻.${entry.id}`;
}
export function entriesForWorker(
entries: ResidentContextEntry[],
workerId: string,
): ResidentContextEntry[] {
const id = workerId.trim();
return entries.filter((e) => {
if (!e.mount || e.mount.length === 0) return true;
return e.mount.includes(id);
});
}
/**
* 把匹配本 worker 的常驻块写入黑板,返回应并入 inputTags 的 tag 列表(按 position
*/
export function mountResidentContextForWorker(params: {
blackboard: Blackboard;
entries: ResidentContextEntry[];
workerId: string;
source?: string;
}): { staticTags: string[]; dynamicTags: string[]; written: string[] } {
const matched = entriesForWorker(params.entries, params.workerId);
const staticTags: string[] = [];
const dynamicTags: string[] = [];
const written: string[] = [];
const source = params.source ?? "system:resident-context";
for (const entry of matched) {
const tag = residentTagFor(entry);
const existing = params.blackboard.getContentByTag(tag);
if (existing !== entry.content) {
params.blackboard.write({
tag,
content: entry.content,
source,
});
written.push(tag);
}
if (entry.position === "dynamic") dynamicTags.push(tag);
else staticTags.push(tag);
}
return { staticTags, dynamicTags, written };
}
/** 拼进声明驱动 prompt 的常驻段(无黑板时也可纯文本注入) */
export function formatResidentPromptSection(
entries: ResidentContextEntry[],
workerId: string,
): string {
const matched = entriesForWorker(entries, workerId);
if (matched.length === 0) return "";
const blocks = matched.map((e) => {
const label = e.id;
return `### ${label}\n\n${e.content}`;
});
return `## 常驻上下文(本 worker 挂载)\n\n${blocks.join("\n\n")}`;
}

View File

@@ -12,6 +12,8 @@ export function toActiveSkillSnapshot(skill: ParsedSkill): ActiveSkillSnapshot {
bookKind: skill.bookKind,
defaultFlowId: skill.defaultFlowId,
suggestedWorkers: skill.suggestedWorkers,
startupMode: skill.startupMode,
uiPrompt: skill.uiPrompt,
startupPrompt: skill.startupInquiry.prompt,
startupTargetKey: skill.startupInquiry.targetKey,
intakeFields,

View File

@@ -1,5 +1,6 @@
import type { BlackboardInputMerge } from "../types/blackboard.js";
import type { AdvancePolicy } from "../types/runtime.js";
import type { AdvancePolicy, SkillStartupMode } from "../types/runtime.js";
import type { ContextSegmentDef } from "./context-segments.js";
export type WorkerLlmBinding = {
/** 固定 ApiProfile.id省略 = 会话默认 */
@@ -53,6 +54,10 @@ export type ParsedSkill = {
/** llm-bindings.yaml可选见 docs/worker-skill-format.md §9 */
workerLlmBindings?: SkillWorkerLlmBindings;
startupInquiry: StartupInquiry;
/** intakelegacy 包)或 agent-first默认UI 引导后 Agent 调度) */
startupMode?: SkillStartupMode;
/** agent-first 首屏固定引导文案 */
uiPrompt?: string;
/** 推进策略预留。loader 第一版不解析 orchestrator ## 推进策略 */
advancePolicy?: AdvancePolicy;
body: string;
@@ -70,6 +75,8 @@ export type ParsedWorkerSkill = {
inputMerge?: BlackboardInputMerge;
/** ApiProfile.id省略 = 走 llm-bindings 或会话默认 */
llmProfileId?: string;
/** 上下半拼装;缺省则 executor 回退 JSON inputs */
contextSegments?: ContextSegmentDef[];
/** 相对 skills/ 的路径 */
path: string;
body: string;

View File

@@ -0,0 +1,166 @@
import type { Blackboard } from "../blackboard/blackboard.js";
import type { RuntimeSession } from "../types/runtime.js";
import {
deriveDesignStageScope,
deriveRunWorkerScope,
parseWorkerSetYaml,
runWorkerMeta,
type ParsedWorkerSet,
} from "./worker-set-parse.js";
export type LifecycleStage = "design" | "play";
/** 实例 Worker 声明:由创作阶段 设计.worker集 动态定义play 只调度声明内的 id */
export type InstanceWorkerDeclaration = {
/** 声明正文来源 tagnull 表示尚无 Worker 集 */
sourceTag: "设计.worker集" | "设计.worker集.草稿" | null;
/** Worker 集是否已通过用户验收 */
accepted: boolean;
parsed: ParsedWorkerSet | null;
/** 当前 lifecycle 下总管可 run_worker 的 id 列表 */
activeWorkerIds: string[];
/** play 阶段声明deriveRunWorkerScope */
playWorkerIds: string[];
/** 创作末尾声明(如 opening-generator */
designEndWorkerIds: string[];
};
const WORKER_SET_ACCEPTED_TAG = "设计.worker集";
const WORKER_SET_DRAFT_TAG = "设计.worker集.草稿";
export function hasAcceptedWorkerSet(session: RuntimeSession): boolean {
if (Boolean(session.slots.designInstanceReady)) return true;
return session.artifacts.some(
(a) =>
a.status === "accepted" &&
a.outputTags.some((tag) => tag === WORKER_SET_ACCEPTED_TAG),
);
}
export function canEnterPlay(session: RuntimeSession): boolean {
return hasAcceptedWorkerSet(session);
}
export function inferLifecycleStage(session: RuntimeSession): LifecycleStage {
const override = session.slots.uiLifecycleStage;
if (override === "play" && canEnterPlay(session)) return "play";
return "design";
}
/** 读取用于构建声明的 Worker 集 YAML */
export function readWorkerSetYamlForDeclaration(
blackboard: Blackboard,
session: RuntimeSession,
): { yaml: string; sourceTag: InstanceWorkerDeclaration["sourceTag"] } | null {
const accepted = blackboard.getContentByTag(WORKER_SET_ACCEPTED_TAG)?.trim();
const draft = blackboard.getContentByTag(WORKER_SET_DRAFT_TAG)?.trim();
const workerSetAccepted = hasAcceptedWorkerSet(session);
if (workerSetAccepted && accepted) {
return { yaml: accepted, sourceTag: WORKER_SET_ACCEPTED_TAG };
}
if (draft) {
return { yaml: draft, sourceTag: WORKER_SET_DRAFT_TAG };
}
if (accepted) {
return { yaml: accepted, sourceTag: WORKER_SET_ACCEPTED_TAG };
}
return null;
}
/**
* 构建实例 Worker 声明。
* world-simulatorplay 仅 activeWorkerIdsdesign 验收前为分步 design-*。
*/
export function buildInstanceWorkerDeclaration(
session: RuntimeSession,
blackboard: Blackboard,
lifecycle: LifecycleStage = inferLifecycleStage(session),
): InstanceWorkerDeclaration {
const accepted = hasAcceptedWorkerSet(session);
const raw = readWorkerSetYamlForDeclaration(blackboard, session);
const parsed = raw ? parseWorkerSetYaml(raw.yaml) : null;
const playWorkerIds = accepted && parsed ? deriveRunWorkerScope(parsed) : [];
const designEndWorkerIds =
accepted && parsed ? deriveDesignStageScope(parsed) : [];
const designStepIds = ["design-flow", "design-step"];
let activeWorkerIds: string[];
if (lifecycle === "play") {
activeWorkerIds = [...playWorkerIds];
} else if (!accepted) {
activeWorkerIds = [...designStepIds];
} else {
activeWorkerIds = [...designStepIds, ...designEndWorkerIds];
// 去重保序
const seen = new Set<string>();
activeWorkerIds = activeWorkerIds.filter((id) => {
if (seen.has(id)) return false;
seen.add(id);
return true;
});
}
return {
sourceTag: raw?.sourceTag ?? null,
accepted,
parsed,
activeWorkerIds,
playWorkerIds,
designEndWorkerIds,
};
}
export function isWorkerDeclared(
declaration: InstanceWorkerDeclaration,
workerId: string,
): boolean {
const id = workerId.trim();
return declaration.activeWorkerIds.includes(id);
}
export function formatUndeclaredWorkerError(
workerId: string,
declaration: InstanceWorkerDeclaration,
): string {
const allowed =
declaration.activeWorkerIds.length > 0
? declaration.activeWorkerIds.join("、")
: "(尚无)";
return (
`Worker「${workerId}」不在本实例声明内。` +
`当前可调度:${allowed}` +
(declaration.accepted
? " play 阶段仅允许 设计.worker集 中 ref 列出的 Worker。"
: " 请先完成 design-flow → design-step并验收终稿 Worker 集。")
);
}
/** 合并磁盘已安装 SKILL 与声明中的 gap id供 list_workers 展示) */
export function mergeDeclaredWorkersForAgent(
installed: Array<{ id: string; description: string }>,
declaration: InstanceWorkerDeclaration,
): Array<{ id: string; description: string }> {
const byId = new Map(installed.map((w) => [w.id, w]));
const out: Array<{ id: string; description: string }> = [];
for (const id of declaration.activeWorkerIds) {
const found = byId.get(id);
if (found) {
out.push(found);
} else {
const meta = runWorkerMeta(id);
out.push({
id,
description: `[声明已启用 · SKILL 待补] ${meta.purpose}`,
});
}
}
return out;
}
export function shouldEnforceWorkerDeclaration(skillPackName?: string): boolean {
return skillPackName === "world-simulator";
}

View File

@@ -0,0 +1,523 @@
import { parse as parseYaml } from "yaml";
export type WorkerSetPresentation = {
tone?: string;
pacing?: string;
information_layers?: string[];
avoid?: string[];
[key: string]: unknown;
};
export type WorkerSetContext = {
static?: string[];
dynamic?: string[];
};
export type WorkerAcceptance = "review" | "continue";
export type WorkerSetEntry = {
ref: string | null;
/**
* 用户可见中文名(创造 worker 时优先填写)。
* 与 `ref` 分离:`ref` 仍为英文机器 id调度 / 模板 / mount
*/
name?: string;
role?: string;
duty?: string;
when?: string;
rationale?: string;
/**
* run 验收点:本 worker 完成后是否停下来给人读。
* review = 用户验收continue = 可连跑下一 worker。
*/
acceptance?: WorkerAcceptance;
merge_considered?: string;
gap?: string | null;
presentation?: WorkerSetPresentation | null;
/** play 阶段冻结的上下文插入顺序design-intake 设计) */
context?: WorkerSetContext;
/** 本 worker 写入黑板的 tag */
outputs?: string[];
};
export type ParsedWorkerSetInteraction = {
user_stance?: string;
system_role?: string;
output?: string;
turn_shape?: string;
[key: string]: unknown;
};
export type ParsedWorkerSet = {
version?: number;
form_summary?: string;
interaction_paradigm?: string;
/** 新规格:站位 / 系统扮演 / 输出 / 轮转 */
interaction?: ParsedWorkerSetInteraction;
experience_check?: Record<string, unknown>;
core_worker?: string;
reasoning?: string;
play_morphology?: string;
input_protocol?: Record<string, string>;
workers: WorkerSetEntry[];
tag_flow?: string[];
resident_context?: unknown[];
tables?: Record<string, unknown>;
narrative_guide?: string;
core_premises?: string[];
design_end?: Record<string, unknown>;
/** @deprecated 旧字段;新规格用 design_end */
instantiate_hints?: {
invoke?: string[];
skip?: string[];
skip_reason?: string;
notes?: string;
};
open_questions?: string[];
notes?: string;
parseError?: string;
};
/** 创作阶段可选 skill 元数据accept Worker 集之后、play 之前) */
export const DESIGN_STAGE_SKILL_META: Record<
string,
{ label: string; purpose: string }
> = {
"opening-generator": {
label: "开局 · 开场白",
purpose:
"创作末尾:结合已定世界/故事写开场白(主);表初值与开场对齐,能推则推。",
},
};
/**
* @deprecated 旧「run worker → instantiate 管道」映射。Worker 集即实例规格,不再用于推导 design 进度。
*/
export const RUN_TO_INSTANTIATE: Record<string, string[]> = {
"world-simulator": ["world-blueprint"],
narrator: ["narrative-guide"],
"variable-update": ["variable-catalog"],
"input-expand": ["narrative-guide"],
"plot-continue": ["narrative-guide"],
"role-decide": ["generation-rules"],
};
/** @deprecated 旧管道 skill 元数据;仅兼容旧 Worker 集 YAML 展示 */
export const INSTANTIATE_SKILL_META: Record<
string,
{ label: string; purpose: string }
> = {
"world-blueprint": {
label: "世界蓝图",
purpose: "背景板与核心设定,供 world-simulator 等读取。",
},
topology: {
label: "拓扑 / 关系",
purpose: "地图、关系网或进阶路径(按需多次)。",
},
"generation-rules": {
label: "生成规则",
purpose: "元规则:如何生成 NPC、物品等实例内容。",
},
"narrative-guide": {
label: "叙事 / 描写指南",
purpose: "正文 POV、时态、文风narrator 等 static 上下文)。",
},
"variable-catalog": {
label: "变量目录",
purpose: "要跟踪的状态与变化规则variable-update 用)。",
},
corpus: {
label: "语料 / 场景策略",
purpose: "口吻样例、场景模板、描写与节奏策略。",
},
};
export const RUN_WORKER_META: Record<string, { label: string; purpose: string }> =
{
"world-simulator": {
label: "世界模拟",
purpose: "裁决规则、更新事件流与可见信息。",
},
narrator: {
label: "转述 / 展示",
purpose: "把核心/世界层干巴输出转为用户可读回复(文学化或 Markdown 等)。",
},
"variable-update": {
label: "变量更新",
purpose: "跟踪等级、资源、职业等状态变量。",
},
"input-expand": {
label: "输入拓写",
purpose: "把用户裸输入拓写为场景内行动/意图。",
},
"plot-continue": {
label: "剧情续写",
purpose: "基于拓写结果续写剧情片段。",
},
"role-decide": {
label: "角色决策",
purpose: "单个重要角色独立决策(信息隔绝时用)。",
},
"round-present": {
label: "回合陈述",
purpose: "结构化陈述本轮事件与各方行动/思考摘要。",
},
};
function asString(value: unknown): string | undefined {
if (value == null) return undefined;
if (typeof value === "string") return value.trim() || undefined;
return String(value).trim() || undefined;
}
function asStringList(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value
.map((item) => asString(item))
.filter((item): item is string => Boolean(item));
}
function parsePresentation(raw: unknown): WorkerSetPresentation | null | undefined {
if (raw == null) return raw === null ? null : undefined;
if (typeof raw !== "object" || Array.isArray(raw)) return undefined;
return raw as WorkerSetPresentation;
}
function parseContext(raw: unknown): WorkerSetContext | undefined {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
const row = raw as Record<string, unknown>;
const staticTags = asStringList(row.static);
const dynamicTags = asStringList(row.dynamic);
if (!staticTags.length && !dynamicTags.length) return undefined;
return {
static: staticTags.length ? staticTags : undefined,
dynamic: dynamicTags.length ? dynamicTags : undefined,
};
}
function parseWorkers(raw: unknown): WorkerSetEntry[] {
if (!Array.isArray(raw)) return [];
return raw.map((item) => {
if (!item || typeof item !== "object" || Array.isArray(item)) {
return { ref: null };
}
const row = item as Record<string, unknown>;
const refRaw = row.ref;
const ref =
refRaw == null
? null
: asString(refRaw) ?? (typeof refRaw === "string" ? refRaw : null);
const outputs = asStringList(row.outputs);
const acceptanceRaw = asString(row.acceptance);
const acceptance: WorkerAcceptance | undefined =
acceptanceRaw === "review" || acceptanceRaw === "continue"
? acceptanceRaw
: undefined;
return {
ref,
name: asString(row.name),
role: asString(row.role),
duty: asString(row.duty),
when: asString(row.when),
rationale: asString(row.rationale),
acceptance,
merge_considered: asString(row.merge_considered),
gap: ref == null ? asString(row.gap) ?? null : asString(row.gap) ?? null,
presentation: parsePresentation(row.presentation),
context: parseContext(row.context),
outputs: outputs.length ? outputs : undefined,
};
});
}
function parseInstantiateHints(raw: unknown): ParsedWorkerSet["instantiate_hints"] {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
const row = raw as Record<string, unknown>;
const invoke = asStringList(row.invoke);
const skip = asStringList(row.skip);
const skip_reason = asString(row.skip_reason);
const notes = asString(row.notes);
if (invoke.length === 0 && skip.length === 0 && !skip_reason && !notes) return undefined;
return {
invoke: invoke.length ? invoke : undefined,
skip: skip.length ? skip : undefined,
skip_reason,
notes,
};
}
function parseWorkerSetObject(row: Record<string, unknown>): ParsedWorkerSet {
const interactionRaw = row.interaction;
const interaction =
interactionRaw &&
typeof interactionRaw === "object" &&
!Array.isArray(interactionRaw)
? (interactionRaw as ParsedWorkerSetInteraction)
: undefined;
return {
version: typeof row.version === "number" ? row.version : undefined,
form_summary: asString(row.form_summary),
interaction_paradigm: asString(row.interaction_paradigm),
interaction,
experience_check:
row.experience_check &&
typeof row.experience_check === "object" &&
!Array.isArray(row.experience_check)
? (row.experience_check as Record<string, unknown>)
: undefined,
core_worker: asString(row.core_worker),
reasoning: asString(row.reasoning),
play_morphology: asString(row.play_morphology),
input_protocol:
row.input_protocol &&
typeof row.input_protocol === "object" &&
!Array.isArray(row.input_protocol)
? Object.fromEntries(
Object.entries(row.input_protocol as Record<string, unknown>)
.map(([k, v]) => [k, asString(v) ?? ""])
.filter(([, v]) => v),
)
: undefined,
workers: parseWorkers(row.workers),
tag_flow: asStringList(row.tag_flow),
resident_context: Array.isArray(row.resident_context)
? row.resident_context
: undefined,
tables:
row.tables && typeof row.tables === "object" && !Array.isArray(row.tables)
? (row.tables as Record<string, unknown>)
: undefined,
narrative_guide: asString(row.narrative_guide),
core_premises: asStringList(row.core_premises),
design_end:
row.design_end &&
typeof row.design_end === "object" &&
!Array.isArray(row.design_end)
? (row.design_end as Record<string, unknown>)
: undefined,
instantiate_hints: parseInstantiateHints(row.instantiate_hints),
open_questions: asStringList(row.open_questions),
notes: asString(row.notes),
};
}
/**
* 解析 `设计.worker集`:优先 JSON含从说明文字中抽取 `{...}`),失败再试 YAML兼容旧草稿
* 新产出应为 JSON。散文/提问文字会得到明确的 parseError而不是晦涩的 YAML 报错。
*/
export function parseWorkerSetYaml(raw: string | undefined): ParsedWorkerSet | null {
const text = raw?.trim();
if (!text) return null;
const jsonCandidate = extractJsonObjectText(text);
if (jsonCandidate) {
try {
const doc = JSON.parse(jsonCandidate) as unknown;
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
return { workers: [], parseError: "Worker 集不是有效的 JSON 对象" };
}
return parseWorkerSetObject(doc as Record<string, unknown>);
} catch (err) {
return {
workers: [],
parseError: err instanceof Error ? err.message : "JSON 解析失败",
};
}
}
// 明显是中文说明/提问,不要丢给 YAML会报 Implicit keys…
if (looksLikeProseNotSpec(text)) {
return {
workers: [],
parseError:
"内容不是 JSON 规格(像是说明或提问文字)。提问应走 askUser规格字段只能是 {…} JSON。",
};
}
try {
const doc = parseYaml(text);
if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
return { workers: [], parseError: "Worker 集不是有效的对象" };
}
return parseWorkerSetObject(doc as Record<string, unknown>);
} catch (err) {
return {
workers: [],
parseError: err instanceof Error ? err.message : "Worker 集解析失败",
};
}
}
/** 从纯 JSON、```json 围栏或夹杂说明的文本中抽出对象字面量 */
export function extractJsonObjectText(raw: string): string | null {
const text = raw.trim();
if (!text) return null;
if (text.startsWith("{")) {
try {
JSON.parse(text);
return text;
} catch {
/* fall through to brace scan */
}
}
const fence = text.match(/```(?:json)?\s*(\{[\s\S]*?\})\s*```/i);
if (fence?.[1]) {
try {
JSON.parse(fence[1]);
return fence[1].trim();
} catch {
/* continue */
}
}
const first = text.indexOf("{");
const last = text.lastIndexOf("}");
if (first >= 0 && last > first) {
const slice = text.slice(first, last + 1);
try {
JSON.parse(slice);
return slice;
} catch {
return null;
}
}
return null;
}
export function looksLikeProseNotSpec(text: string): boolean {
const t = text.trim();
if (!t) return false;
if (t.startsWith("{") || t.startsWith("[")) return false;
// YAML 文档常见开头
if (/^(---|version:|workers:|interaction:|form_summary:)/m.test(t)) return false;
// 中文叙述 / 明显自然语言
if (/[\u4e00-\u9fff]{8,}/.test(t) && !/^\s*[\w.-]+\s*:/.test(t)) return true;
if (/^(我们|首先|根据|请|用户需求|我(?:们)?被要求)/.test(t)) return true;
return false;
}
/** 是否像一份可用的 Worker 集(而非空壳 / 仅 parseError */
export function isUsableWorkerSet(parsed: ParsedWorkerSet | null | undefined): boolean {
if (!parsed || parsed.parseError) return false;
if ((parsed.workers?.length ?? 0) > 0) return true;
if (parsed.interaction && Object.keys(parsed.interaction).length > 0) return true;
if (parsed.form_summary?.trim() || parsed.interaction_paradigm?.trim()) return true;
if (parsed.narrative_guide?.trim() || (parsed.core_premises?.length ?? 0) > 0) {
return true;
}
return false;
}
/**
* 从 Worker 集推导创作阶段收尾可选 skill如 opening-generator
* 读 design_end / instantiate_hints.invoke + workers[].ref。
*/
export function deriveDesignStageScope(workerSet: ParsedWorkerSet | null): string[] {
if (!workerSet) return [];
const seen = new Set<string>();
const ordered: string[] = [];
const add = (id: string) => {
const key = id.trim();
if (!key || seen.has(key)) return;
seen.add(key);
ordered.push(key);
};
const designEnd = workerSet.design_end;
if (designEnd) {
const opening = designEnd.opening;
if (
opening === "optional" ||
opening === true ||
opening === "opening-generator"
) {
add("opening-generator");
}
for (const id of asStringList(designEnd.invoke)) add(id);
}
for (const id of workerSet.instantiate_hints?.invoke ?? []) add(id);
for (const worker of workerSet.workers) {
const ref = worker.ref?.trim();
if (ref && DESIGN_STAGE_SKILL_META[ref]) add(ref);
}
for (const id of workerSet.instantiate_hints?.skip ?? []) {
seen.delete(id);
const idx = ordered.indexOf(id);
if (idx >= 0) ordered.splice(idx, 1);
}
return ordered;
}
/** @deprecated 请用 deriveDesignStageScope */
export function deriveInstantiateScope(workerSet: ParsedWorkerSet | null): string[] {
return deriveDesignStageScope(workerSet);
}
/** play 阶段启用的 run worker ref 列表(保序、去重;不含 opening-generator 等 design-end skill */
export function deriveRunWorkerScope(workerSet: ParsedWorkerSet | null): string[] {
if (!workerSet) return [];
const seen = new Set<string>();
const ordered: string[] = [];
for (const worker of workerSet.workers) {
const ref = worker.ref?.trim();
if (!ref || seen.has(ref)) continue;
if (DESIGN_STAGE_SKILL_META[ref]) continue;
seen.add(ref);
ordered.push(ref);
}
return ordered;
}
/**
* run 阶段需用户验收的 worker ref 列表acceptance === review
* 未写 acceptance 的不列入(创作时应写全;运行时缺省策略另议)。
*/
export function deriveReviewWorkerScope(workerSet: ParsedWorkerSet | null): string[] {
if (!workerSet) return [];
const seen = new Set<string>();
const ordered: string[] = [];
for (const worker of workerSet.workers) {
const ref = worker.ref?.trim();
if (!ref || seen.has(ref)) continue;
if (DESIGN_STAGE_SKILL_META[ref]) continue;
if (worker.acceptance !== "review") continue;
seen.add(ref);
ordered.push(ref);
}
return ordered;
}
/** 该 run worker 完成后是否应停下来给人验收 */
export function workerRequiresReview(
workerSet: ParsedWorkerSet | null,
workerId: string,
): boolean {
if (!workerSet) return false;
const id = workerId.trim();
const entry = workerSet.workers.find((w) => w.ref?.trim() === id);
return entry?.acceptance === "review";
}
export function instantiateMeta(id: string): { label: string; purpose: string } {
return (
DESIGN_STAGE_SKILL_META[id] ??
INSTANTIATE_SKILL_META[id] ?? {
label: id,
purpose: "创作阶段可选 skill。",
}
);
}
export function runWorkerMeta(id: string): { label: string; purpose: string } {
return (
RUN_WORKER_META[id] ?? {
label: id,
purpose: "play 阶段按需 invoke。",
}
);
}

View File

@@ -0,0 +1,720 @@
import type {
ParsedWorkerSet,
WorkerSetEntry,
WorkerSetPresentation,
} from "./worker-set-parse.js";
import { instantiateMeta, runWorkerMeta } from "./worker-set-parse.js";
import { listCreationUnits, extractUnitContentFromDraft } from "./creation-units.js";
import type { CreationUnitView } from "./creation-units.js";
import {
parseResidentContext,
residentTagFor,
type ResidentContextEntry,
} from "./resident-context.js";
export type TagLineView = {
tag: string;
note?: string;
filled?: boolean;
};
export type ContextLayerView = {
staticTags: TagLineView[];
dynamicTags: TagLineView[];
/** false = 部分来自包内默认模板design-intake 未写全 context */
explicit: boolean;
};
export type PresentationLineView = {
label: string;
value: string;
};
/** tag / 固定上下文 → 会塞进哪些 worker与 worker 卡片解耦) */
export type ContextTagMountView = {
workerId: string | null;
workerName: string;
/** prompt_body=写入该 worker 提示词正文input_tag=声明为黑板读入resident=常驻挂载 */
how: "prompt_body" | "input_tag" | "resident";
tier?: "static" | "dynamic";
};
export type ContextTagCardView = {
/** fixed:… / resident:… / board:tag名 */
id: string;
label: string;
kind: "fixed" | "resident" | "board";
/** 内容预览(截断) */
preview?: string;
filled: boolean;
mounts: ContextTagMountView[];
/** 给人看的挂载摘要,如「全部 Worker」「叙事转述 · 世界模拟」 */
mountSummary: string;
};
export type WorkerCardView = {
order: number;
id: string | null;
displayName: string;
roleLabel?: string;
status: "ready" | "gap";
gapNote?: string;
purpose: string;
invokeWhen: string;
rationale?: string;
/** run完成后是否停下来给人验收 */
acceptance?: "review" | "continue";
acceptanceLabel?: string;
mergeConsidered?: string;
/** @deprecated UI 以 contextTags 独立区为准;卡片上只展示 readsSummary */
context: ContextLayerView;
/** 本 worker 会读/注入的上下文短摘要(不含完整 tag 列表) */
readsSummary?: string;
writes: string[];
presentation?: PresentationLineView[];
};
export type TagFlowEdgeView = {
from: string[];
to: string[];
};
export type DesignTaskView = {
id: string;
label: string;
status: "planned" | "skipped" | "filled";
skipReason?: string;
};
export type WorkerSetUserView = {
headline?: string;
interactionParadigm?: string;
coreWorker?: string;
reasoning?: string;
playModeLabel?: string;
playModeHint?: string;
inputProtocol?: PresentationLineView[];
workers: WorkerCardView[];
/**
* 固定 / 常驻 / 黑板上下文(独立于 worker 卡)。
* 每张卡标明会塞进哪些 worker。
*/
contextTags?: ContextTagCardView[];
tagFlow: TagFlowEdgeView[];
designTasks: DesignTaskView[];
/** 创作单位worker 规格 + 固定插入上下文(同级) */
creationUnits?: CreationUnitView[];
/**
* 当前创作单位:用于验收主舞台聚焦。
* fixed/resident → 高亮「本块上下文」正文worker → 高亮该卡。
*/
focusUnit?: {
id: string;
kind: CreationUnitView["kind"];
label: string;
/** fixed / resident / phase 时抽出的本块内容(给 UI 高亮展示) */
body?: unknown;
};
openQuestions: string[];
notes?: string;
parseError?: string;
};
export type FormatWorkerSetOptions = {
/** 黑板上已有内容的 tag 名(用于设计任务 / static 是否已填) */
filledTags?: string[];
acceptedUnitIds?: string[];
currentUnitId?: string | null;
};
const PLAY_MORPHOLOGY: Record<string, { label: string; hint: string }> = {
action_reaction_loop: {
label: "行动–反应循环",
hint: "你输入一句,世界推进并回复一段",
},
chain: {
label: "链式输出",
hint: "按顺序生成,通常无多轮世界机",
},
single: {
label: "单次生成",
hint: "一次产出成稿,非多轮交互",
},
fork_review: {
label: "分叉验收",
hint: "生成后分角色验收,非交互小说",
},
multi_actor_sim: {
label: "多角色模拟",
hint: "多个角色各自决策,世界裁决后汇总",
},
};
const PRESENTATION_LABELS: Record<string, string> = {
mode: "输出模式",
tone: "语气",
pacing: "节奏",
information_layers: "信息层",
avoid: "避免",
layers: "信息层",
};
const TAG_NOTES: Record<string, string> = {
"设计.worker集": "实例分工与展示要求(本 worker 读切片)",
"世界.蓝图.确认稿": "设计阶段写入,游玩时只读",
"变量.目录.确认稿": "设计阶段写入,游玩时只读",
"变量.变化规则.确认稿": "设计阶段写入,游玩时只读",
"叙事.指南.确认稿": "设计阶段写入,游玩时只读",
"语料.场景策略集.确认稿": "设计阶段写入,游玩时只读",
"输出.回复格式.规范": "设计阶段写入,游玩时只读",
"变量.当前": "每轮更新",
"运行.事件流": "每轮追加",
"用户.最新输入": "每轮用户输入",
};
/** 包内 run worker 默认上下文契约design-intake 未写 context 时合并) */
const DEFAULT_WORKER_CONTRACTS: Record<
string,
{ context: { static: string[]; dynamic: string[] }; outputs: string[] }
> = {
"world-simulator": {
context: {
static: [
"设计.worker集",
"世界.蓝图.确认稿",
"世界.拓扑.*",
"变量.目录.确认稿",
"变量.变化规则.确认稿",
],
dynamic: ["变量.当前", "运行.事件流", "用户.最新输入"],
},
outputs: ["运行.本轮.裁决", "运行.事件流"],
},
"variable-update": {
context: {
static: ["变量.目录.确认稿", "变量.变化规则.确认稿"],
dynamic: ["运行.本轮.裁决", "变量.当前"],
},
outputs: ["运行.本轮.变量变更", "变量.当前"],
},
narrator: {
context: {
static: [
"设计.worker集",
"叙事.指南.确认稿",
"语料.场景策略集.确认稿",
"输出.回复格式.规范",
],
dynamic: ["运行.事件流", "变量.当前", "运行.本轮.变量变更"],
},
outputs: ["输出.用户展示"],
},
"input-expand": {
context: {
static: ["设计.worker集", "叙事.指南.确认稿", "用户.需求"],
dynamic: ["用户.最新输入"],
},
outputs: ["运行.本轮.拓写"],
},
"plot-continue": {
context: {
static: ["设计.worker集", "叙事.指南.确认稿", "世界.蓝图.确认稿"],
dynamic: ["运行.本轮.拓写", "运行.事件流"],
},
outputs: ["运行.本轮.续写"],
},
"role-decide": {
context: {
static: ["设计.worker集", "世界.生成规则.*", "实例.角色.*"],
dynamic: ["运行.事件流", "可见信息"],
},
outputs: ["运行.本轮.角色决策"],
},
};
const INPUT_PROTOCOL_LABELS: Record<string, string> = {
parens: "()圆括号",
quotes: "「」/ \"\" 台词",
brackets: "【】行动",
default: "默认规则",
};
const WORKER_ROLE_LABELS: Record<string, string> = {
core: "核心",
auxiliary: "辅助",
transcription: "转述",
};
function tagMatchesPattern(tag: string, pattern: string): boolean {
if (pattern.endsWith(".*")) {
const prefix = pattern.slice(0, -2);
return tag === prefix || tag.startsWith(`${prefix}.`);
}
return tag === pattern;
}
function tagFilled(tag: string, filledTags: string[]): boolean {
if (filledTags.some((t) => tagMatchesPattern(t, tag))) return true;
if (tag.endsWith(".*")) {
const prefix = tag.slice(0, -2);
return filledTags.some((t) => t === prefix || t.startsWith(`${prefix}.`));
}
return filledTags.includes(tag);
}
function resolveContract(entry: WorkerSetEntry): {
context: { static: string[]; dynamic: string[] };
outputs: string[];
explicit: boolean;
} {
const defaults = entry.ref ? DEFAULT_WORKER_CONTRACTS[entry.ref] : undefined;
const ctx = entry.context;
const hasExplicitContext =
Boolean(ctx?.static?.length) || Boolean(ctx?.dynamic?.length);
const hasExplicitOutputs = Boolean(entry.outputs?.length);
return {
context: {
static: hasExplicitContext
? (ctx?.static ?? [])
: (defaults?.context.static ?? []),
dynamic: hasExplicitContext
? (ctx?.dynamic ?? [])
: (defaults?.context.dynamic ?? []),
},
outputs: hasExplicitOutputs
? (entry.outputs ?? [])
: (defaults?.outputs ?? []),
explicit: hasExplicitContext || hasExplicitOutputs,
};
}
function formatPresentation(pres: WorkerSetPresentation): PresentationLineView[] {
const lines: PresentationLineView[] = [];
for (const [key, value] of Object.entries(pres)) {
if (value == null || value === "") continue;
const label = PRESENTATION_LABELS[key] ?? key;
const text = Array.isArray(value) ? value.join("、") : String(value);
lines.push({ label, value: text });
}
return lines;
}
function parseTagFlow(lines: string[] | undefined): TagFlowEdgeView[] {
if (!lines?.length) return [];
return lines
.map((line) => {
const arrow = line.includes("→") ? "→" : "->";
const parts = line.split(arrow);
if (parts.length < 2) return null;
const from = parts[0]
.split("+")
.map((s) => s.trim())
.filter(Boolean);
const to = parts
.slice(1)
.join(arrow)
.split("+")
.map((s) => s.trim())
.filter(Boolean);
return { from, to };
})
.filter((e): e is TagFlowEdgeView => e != null && e.to.length > 0);
}
function toTagLines(
tags: string[],
filledTags: string[],
tier: "static" | "dynamic",
): TagLineView[] {
return tags.map((tag) => ({
tag,
note:
TAG_NOTES[tag] ??
(tier === "static" ? "设计阶段写入,游玩时只读" : "每轮读写"),
filled: filledTags.length ? tagFilled(tag, filledTags) : undefined,
}));
}
function previewText(raw: string | undefined, max = 120): string | undefined {
const t = raw?.trim();
if (!t) return undefined;
return t.length > max ? `${t.slice(0, max)}` : t;
}
function formatMountSummary(
mounts: ContextTagMountView[],
allWorkerCount: number,
): string {
if (mounts.length === 0) return "尚未指定 Worker";
const names = [...new Set(mounts.map((m) => m.workerName))];
if (allWorkerCount > 0 && names.length >= allWorkerCount) {
return "全部 Worker";
}
if (names.length <= 3) return names.join(" · ");
return `${names.slice(0, 2).join(" · ")}${names.length}`;
}
function workerDisplayName(entry: WorkerSetEntry, index: number): string {
const meta = entry.ref ? runWorkerMeta(entry.ref) : null;
return (
entry.name?.trim() ||
(entry.ref == null
? entry.duty?.slice(0, 24) || `待命名 Worker ${index + 1}`
: meta!.label)
);
}
/** 从规格反查:每块固定/常驻/黑板 tag 会塞进哪些 worker */
function buildContextTagCards(
parsed: ParsedWorkerSet,
workers: WorkerCardView[],
filledTags: string[],
): ContextTagCardView[] {
const cards: ContextTagCardView[] = [];
const allWorkerMounts: ContextTagMountView[] = workers
.filter((w) => w.id)
.map((w) => ({
workerId: w.id,
workerName: w.displayName,
how: "prompt_body" as const,
}));
const workerCount = allWorkerMounts.length;
const narrative = parsed.narrative_guide?.trim();
if (narrative) {
const mounts = allWorkerMounts.map((m) => ({ ...m, how: "prompt_body" as const }));
cards.push({
id: "fixed:narrative_guide",
label: "叙事指南",
kind: "fixed",
preview: previewText(narrative),
filled: true,
mounts,
mountSummary: formatMountSummary(mounts, workerCount),
});
}
const premises = (parsed.core_premises ?? []).filter(
(p): p is string => typeof p === "string" && Boolean(p.trim()),
);
if (premises.length) {
const mounts = allWorkerMounts.map((m) => ({ ...m, how: "prompt_body" as const }));
cards.push({
id: "fixed:core_premises",
label: "核心实现前提",
kind: "fixed",
preview: previewText(premises.join("")),
filled: true,
mounts,
mountSummary: formatMountSummary(mounts, workerCount),
});
}
if (parsed.input_protocol && Object.values(parsed.input_protocol).some(Boolean)) {
const mounts = allWorkerMounts.map((m) => ({ ...m, how: "prompt_body" as const }));
const bits = Object.entries(parsed.input_protocol)
.filter(([, v]) => v)
.map(([k, v]) => `${INPUT_PROTOCOL_LABELS[k] ?? k}${v}`);
cards.push({
id: "fixed:input_protocol",
label: "输入协议",
kind: "fixed",
preview: previewText(bits.join("")),
filled: true,
mounts,
mountSummary: formatMountSummary(mounts, workerCount),
});
}
// 美学:各 worker 自己的 presentation → 只挂该 worker
for (const w of workers) {
if (!w.presentation?.length) continue;
const preview = w.presentation.map((p) => `${p.label}${p.value}`).join("");
const mounts: ContextTagMountView[] = [
{
workerId: w.id,
workerName: w.displayName,
how: "prompt_body",
},
];
cards.push({
id: `fixed:aesthetics:${w.id ?? w.order}`,
label: `美学纲领 · ${w.displayName}`,
kind: "fixed",
preview: previewText(preview),
filled: true,
mounts,
mountSummary: formatMountSummary(mounts, workerCount),
});
}
const residents = parseResidentContext(parsed.resident_context);
for (const entry of residents) {
const mounts = resolveResidentMounts(entry, workers);
cards.push({
id: `resident:${entry.id}`,
label: `常驻 · ${entry.id}`,
kind: "resident",
preview: previewText(entry.content),
filled: true,
mounts,
mountSummary: formatMountSummary(mounts, workerCount),
});
}
// 黑板 tag按「谁读」反查与固定正文解耦
const boardMap = new Map<
string,
{ mounts: ContextTagMountView[]; tiers: Set<string> }
>();
for (const w of workers) {
for (const t of w.context.staticTags) {
const cur = boardMap.get(t.tag) ?? { mounts: [], tiers: new Set() };
cur.mounts.push({
workerId: w.id,
workerName: w.displayName,
how: "input_tag",
tier: "static",
});
cur.tiers.add("static");
boardMap.set(t.tag, cur);
}
for (const t of w.context.dynamicTags) {
const cur = boardMap.get(t.tag) ?? { mounts: [], tiers: new Set() };
cur.mounts.push({
workerId: w.id,
workerName: w.displayName,
how: "input_tag",
tier: "dynamic",
});
cur.tiers.add("dynamic");
boardMap.set(t.tag, cur);
}
}
// 跳过已由 resident 显式 tag 覆盖的
const residentBoardTags = new Set(residents.map((e) => residentTagFor(e)));
for (const [tag, info] of boardMap) {
if (residentBoardTags.has(tag)) continue;
if (tag === "设计.worker集") continue; // 规格本身,不当「上下文块」
const mounts = info.mounts;
cards.push({
id: `board:${tag}`,
label: tag,
kind: "board",
filled: filledTags.length ? tagFilled(tag, filledTags) : false,
mounts,
mountSummary: formatMountSummary(mounts, workerCount),
});
}
return cards;
}
function resolveResidentMounts(
entry: ResidentContextEntry,
workers: WorkerCardView[],
): ContextTagMountView[] {
const withIds = workers.filter((w) => w.id);
if (!entry.mount || entry.mount.length === 0) {
return withIds.map((w) => ({
workerId: w.id,
workerName: w.displayName,
how: "resident" as const,
tier: entry.position === "dynamic" ? ("dynamic" as const) : ("static" as const),
}));
}
const wanted = new Set(entry.mount);
return withIds
.filter((w) => w.id && wanted.has(w.id))
.map((w) => ({
workerId: w.id,
workerName: w.displayName,
how: "resident" as const,
tier: entry.position === "dynamic" ? ("dynamic" as const) : ("static" as const),
}));
}
function summarizeWorkerReads(
w: WorkerCardView,
contextTags: ContextTagCardView[],
): string {
const injected = contextTags
.filter(
(c) =>
(c.kind === "fixed" || c.kind === "resident") &&
c.mounts.some((m) => m.workerId === w.id || (!m.workerId && !w.id)),
)
.map((c) => c.label);
// 「全部 Worker」挂载的 fixed 也算
const allInjected = contextTags
.filter(
(c) =>
(c.kind === "fixed" || c.kind === "resident") &&
(c.mountSummary === "全部 Worker" ||
c.mounts.some((m) => m.workerId === w.id)),
)
.map((c) => c.label);
const labels = [...new Set(allInjected.length ? allInjected : injected)];
const boardCount =
w.context.staticTags.length + w.context.dynamicTags.length;
const parts: string[] = [];
if (labels.length) parts.push(labels.slice(0, 3).join("、") + (labels.length > 3 ? "…" : ""));
if (boardCount) parts.push(`黑板 ${boardCount}`);
return parts.length ? parts.join(" · ") : "(未声明上下文)";
}
/** 将解析后的 Worker 集格式化为用户可读视图 */
export function formatWorkerSetForUser(
parsed: ParsedWorkerSet | null,
options: FormatWorkerSetOptions = {},
): WorkerSetUserView | null {
if (!parsed) return null;
const filledTags = options.filledTags ?? [];
if (parsed.parseError) {
return {
workers: [],
contextTags: [],
tagFlow: [],
designTasks: [],
openQuestions: parsed.open_questions ?? [],
parseError: parsed.parseError,
};
}
const morphKey = parsed.play_morphology?.trim();
const morph = morphKey ? PLAY_MORPHOLOGY[morphKey] : undefined;
const workers: WorkerCardView[] = parsed.workers.map((entry, index) => {
const meta = entry.ref ? runWorkerMeta(entry.ref) : null;
const contract = resolveContract(entry);
const displayName = workerDisplayName(entry, index);
return {
order: index + 1,
id: entry.ref,
displayName,
roleLabel: entry.role ? WORKER_ROLE_LABELS[entry.role] ?? entry.role : undefined,
status: entry.ref == null ? "gap" : "ready",
gapNote: entry.gap ?? (entry.ref == null ? "尚未有对应 SKILL" : undefined),
purpose: entry.duty?.trim() || meta?.purpose || "—",
invokeWhen: entry.when?.trim() || "由 Agent 按本轮状态决定",
rationale: entry.rationale,
acceptance: entry.acceptance,
acceptanceLabel:
entry.acceptance === "review"
? "完成后验收"
: entry.acceptance === "continue"
? "可连跑"
: undefined,
mergeConsidered: entry.merge_considered,
context: {
staticTags: toTagLines(contract.context.static, filledTags, "static"),
dynamicTags: toTagLines(contract.context.dynamic, filledTags, "dynamic"),
explicit: contract.explicit,
},
writes: contract.outputs,
presentation: entry.presentation
? formatPresentation(entry.presentation)
: undefined,
};
});
const contextTags = buildContextTagCards(parsed, workers, filledTags);
for (const w of workers) {
w.readsSummary = summarizeWorkerReads(w, contextTags);
}
const invoke = parsed.instantiate_hints?.invoke ?? [];
const designTasks: DesignTaskView[] = [];
for (const id of invoke) {
const meta = instantiateMeta(id);
const relatedTags = instantiateTagsForSkill(id);
const filled = relatedTags.some((t) => tagFilledOnBoard(t, filledTags));
designTasks.push({
id,
label: meta.label,
status: filled ? "filled" : "planned",
});
}
for (const id of parsed.instantiate_hints?.skip ?? []) {
designTasks.push({
id,
label: instantiateMeta(id).label,
status: "skipped",
skipReason: parsed.instantiate_hints?.skip_reason,
});
}
const inputProtocol = parsed.input_protocol
? Object.entries(parsed.input_protocol)
.filter(([, v]) => v)
.map(([key, value]) => ({
label: INPUT_PROTOCOL_LABELS[key] ?? key,
value,
}))
: undefined;
const creationUnits = listCreationUnits(parsed, {
acceptedUnitIds: options.acceptedUnitIds,
currentUnitId: options.currentUnitId,
});
const currentUnit =
creationUnits.find((u) => u.current) ??
(options.currentUnitId
? creationUnits.find((u) => u.id === options.currentUnitId)
: undefined);
let focusUnit: WorkerSetUserView["focusUnit"];
if (currentUnit) {
const body =
currentUnit.kind === "fixed" || currentUnit.kind === "phase"
? extractUnitContentFromDraft(parsed, currentUnit.id)
: currentUnit.kind === "worker"
? extractUnitContentFromDraft(parsed, currentUnit.id)
: null;
focusUnit = {
id: currentUnit.id,
kind: currentUnit.kind,
label: currentUnit.label,
body: body ?? undefined,
};
}
return {
headline: parsed.form_summary,
interactionParadigm: parsed.interaction_paradigm,
coreWorker: parsed.core_worker,
reasoning: parsed.reasoning,
playModeLabel: morph?.label ?? morphKey,
playModeHint: morph?.hint,
inputProtocol,
workers,
contextTags,
tagFlow: parseTagFlow(parsed.tag_flow),
designTasks,
creationUnits,
focusUnit,
openQuestions: parsed.open_questions ?? [],
notes: parsed.notes,
};
}
function tagFilledOnBoard(tagPattern: string, filledTags: string[]): boolean {
if (tagPattern.endsWith(".")) {
return filledTags.some((t) => t.startsWith(tagPattern));
}
return tagFilled(tagPattern, filledTags);
}
/** 创作阶段收尾 skill 典型写入 tag用于设计任务「已填」检测 */
function instantiateTagsForSkill(skillId: string): string[] {
if (skillId === "opening-generator") {
return ["输出.开场白", "运行.初始变量"];
}
return [];
}