重构世界模拟器为模块化配方架构,完善创作编排、会话运行时与 Web UI,并清理过时技能。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
260
src/skills/declared-worker.ts
Normal file
260
src/skills/declared-worker.ts
Normal 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_confirmed;continue → no_confirmation
|
||||
* - 未声明 acceptance:design 生命周期默认确认;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 };
|
||||
}
|
||||
Reference in New Issue
Block a user