改写回合推演技能合同,并接上画面工作流库。

旧产物名仍能读到,避免改名后上下文对不上。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-14 08:29:28 +08:00
co-authored by Cursor
parent e4c3141be5
commit bc77bc4a3c
42 changed files with 1153 additions and 628 deletions
+22
View File
@@ -0,0 +1,22 @@
/**
* 工作流库 → 创作提示里的短目录。不含 ComfyUI 节点图。
*/
import { listImageWorkflows, type ImageWorkflowEntry } from "./store.js";
export function formatImageWorkflowCatalogForPrompt(
entries?: ImageWorkflowEntry[],
): string {
const workflows = entries ?? listImageWorkflows();
if (!workflows.length) return "";
const lines = workflows.map(
(entry) => `- ${entry.id}|${entry.name}|${entry.summary}`,
);
return `## 【工作流库 · 可选用】
本机已导出的画面工作流。选定一个 id 写入本步产物。这里只有短名和一句画面说明。
${lines.join("\n")}`;
}
+7
View File
@@ -0,0 +1,7 @@
export {
getImageWorkflow,
getImageWorkflowsDir,
listImageWorkflows,
type ImageWorkflowEntry,
} from "./store.js";
export { formatImageWorkflowCatalogForPrompt } from "./catalog.js";
+102
View File
@@ -0,0 +1,102 @@
/**
* 本机 ComfyUI 工作流库。
* 目录:{用户数据}/image-workflows/<id>/
* meta.json 短名、一句画面说明、提示词节点
* workflow.json ComfyUI「API 格式」导出的节点图
* 创作模型只看短目录;节点图留在磁盘,提交生图时才读。
*/
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
import path from "node:path";
import { getUserDataDir } from "../config/user-data-dir.js";
export type ImageWorkflowEntry = {
id: string;
name: string;
/** 给创作模型看的一句画面说明 */
summary: string;
/** ComfyUI API JSON 里的节点 id */
promptNode: string;
/** 该节点上要覆盖的字段,CLIP 文本一般为 text */
promptField: string;
/** 可选:这种风格的提示词习惯,仍不是节点图 */
promptNotes?: string;
};
const DIR_NAME = "image-workflows";
export function getImageWorkflowsDir(): string {
return path.join(getUserDataDir(), DIR_NAME);
}
function asString(v: unknown): string | undefined {
if (typeof v !== "string") return undefined;
const t = v.trim();
return t || undefined;
}
function readJson(file: string): unknown | null {
try {
return JSON.parse(readFileSync(file, "utf8")) as unknown;
} catch {
return null;
}
}
function parseEntry(folderName: string, dir: string): ImageWorkflowEntry | null {
const metaPath = path.join(dir, "meta.json");
const workflowPath = path.join(dir, "workflow.json");
if (!existsSync(metaPath) || !existsSync(workflowPath)) return null;
const meta = readJson(metaPath);
const workflow = readJson(workflowPath);
if (!meta || typeof meta !== "object" || Array.isArray(meta)) return null;
if (!workflow || typeof workflow !== "object" || Array.isArray(workflow)) return null;
const row = meta as Record<string, unknown>;
const id = asString(row.id) ?? folderName.trim();
const name = asString(row.name);
const summary = asString(row.summary);
const promptNode = asString(row.prompt_node) ?? asString(row.promptNode);
const promptField = asString(row.prompt_field) ?? asString(row.promptField);
if (!id || !name || !summary || !promptNode || !promptField) return null;
const promptNotes = asString(row.prompt_notes) ?? asString(row.promptNotes);
return {
id,
name,
summary,
promptNode,
promptField,
...(promptNotes ? { promptNotes } : {}),
};
}
/** 扫描目录。缺文件或字段不全的文件夹跳过。 */
export function listImageWorkflows(): ImageWorkflowEntry[] {
const root = getImageWorkflowsDir();
if (!existsSync(root)) return [];
const out: ImageWorkflowEntry[] = [];
let names: string[] = [];
try {
names = readdirSync(root);
} catch {
return [];
}
for (const name of names) {
if (!name || name.startsWith(".")) continue;
const dir = path.join(root, name);
try {
if (!statSync(dir).isDirectory()) continue;
} catch {
continue;
}
const entry = parseEntry(name, dir);
if (entry) out.push(entry);
}
out.sort((a, b) => a.name.localeCompare(b.name, "zh"));
return out;
}
export function getImageWorkflow(id: string): ImageWorkflowEntry | null {
const key = id.trim();
if (!key) return null;
return listImageWorkflows().find((entry) => entry.id === key) ?? null;
}
+7
View File
@@ -3,6 +3,7 @@
* 新库:实现 formatForPrompt,在本文件 register。
*/
import { formatImageWorkflowCatalogForPrompt } from "../image-workflow/catalog.js";
import { formatPreferenceCatalogForPrompt } from "../preference/catalog.js";
import { formatStylePackCatalogForPrompt } from "../style-pack/catalog.js";
@@ -74,6 +75,12 @@ registerLibrary({
formatForPrompt: formatPreferenceCatalogForPrompt,
});
registerLibrary({
id: "image-workflows",
label: "工作流库",
formatForPrompt: formatImageWorkflowCatalogForPrompt,
});
/**
* 从模块列表收集绑定的库 id(去重、保序)。
* 对话落盘:通常传入 intake≠recipe 的模块。
+20 -9
View File
@@ -91,6 +91,8 @@ import {
isInstanceStep,
findStepByRef,
findModuleByName,
legacyTagsRetiredByWrite,
readModuleArtifactContent,
hasAcceptedCloserStep,
parseCreationFlow,
DESIGN_FLOW_WORKER_ID,
@@ -1127,15 +1129,18 @@ export class PhaseRuntime {
const other = findStepByRef(flow, item);
return other?.name === step.name;
}).length;
const tag = findModuleByName(catalog, step.name)?.artifact?.trim();
if (tag) {
const stored = readModuleArtifactContent(
findModuleByName(catalog, step.name),
(tag) => this.blackboard.getContentByTag(tag),
);
if (stored?.content) {
const pruned = pruneRepeatableArtifactContent({
raw: this.blackboard.getContentByTag(tag) ?? "",
raw: stored.content,
step,
remainingSameModule,
});
this.blackboard.write({
tag,
tag: stored.tag,
content: pruned,
source: "runtime",
});
@@ -1710,11 +1715,12 @@ export class PhaseRuntime {
}
const pendingModule = findModuleByName(catalog, step.name);
const storedArtifact = readModuleArtifactContent(pendingModule, (tag) =>
this.blackboard.getContentByTag(tag),
);
if (reenter) {
const artTag = pendingModule?.artifact?.trim();
const hasArt = Boolean(
artTag && this.blackboard.getContentByTag(artTag)?.trim(),
);
const artTag = storedArtifact?.tag;
const hasArt = Boolean(storedArtifact?.content);
if (
!isStepAccepted(step, accepted) &&
!hasArt &&
@@ -1764,7 +1770,7 @@ export class PhaseRuntime {
if (reenter) {
const existing = this.ensureReenterArtifact(
pinned.id,
pendingModule?.artifact,
storedArtifact?.tag ?? pendingModule?.artifact,
);
if (existing) {
this.reopenArtifactForRevision(existing, pinned);
@@ -2719,6 +2725,11 @@ export class PhaseRuntime {
content: toWrite,
source: workerId,
});
if (toWrite.trim()) {
for (const oldTag of legacyTagsRetiredByWrite(tag)) {
this.blackboard.deleteByTag(oldTag);
}
}
if (
inferLifecycleStage(this.session) === "play" &&
(tag === "输出.用户展示" || tag === "输出.开场白")
+14 -2
View File
@@ -36,11 +36,12 @@ const WORKER_LABELS: Record<string, string> = {
};
const FIXED_TOPIC_LABELS: Record<string, string> = {
"aesthetics-interaction": "美学纲领与交互范式",
"aesthetics-interaction": "美学纲领",
interaction: "交互范式",
narrative_guide: "叙事指南与故事推进",
"narrative-guide": "叙事指南",
"story-progression": "故事推进",
"image-prompt": "画面与生图",
"story-progression": "故事推进与交互范式",
input_protocol: "输入协议",
core_premise: "核心前提",
aesthetics: "美学纲领",
@@ -58,6 +59,11 @@ const CREATION_MODE_LABELS: Record<string, string> = {
listen_serial: "听书连载",
};
/** 顶栏产品面(与作品对话并列) */
const PRODUCT_SURFACE_LABELS: Record<string, string> = {
"direct-chat": "直聊",
};
const SKILL_PACK_LABELS: Record<string, string> = {
"world-simulator": "回合推演",
"expand-assistant": "扩写助手",
@@ -74,6 +80,12 @@ export function displayStageLabel(id: string | undefined | null): string {
return STAGE_LABELS[id] ?? id;
}
/** 顶栏产品面展示名 */
export function displayProductSurfaceLabel(id: string | undefined | null): string {
if (!id) return "";
return PRODUCT_SURFACE_LABELS[id] ?? id;
}
/** 进料方式展示名(新建对话框 / 工作面 kicker) */
export function displayCreationModeLabel(id: string | undefined | null): string {
if (!id) return "";
+4 -2
View File
@@ -496,6 +496,7 @@ const SYNTH_FALLBACK_CONTEXT: Record<
"设计.变量设计与更新规则",
"设计.生成规则",
"设计.变量控制上下文",
"设计.画面与生图",
],
dynamic: [
"输出.用户展示",
@@ -508,13 +509,14 @@ const SYNTH_FALLBACK_CONTEXT: Record<
"world-simulator": {
static: [
"设计.舞台骨架",
"设计.世界法则",
"设计.实现机制",
"设计.变量设计与更新规则",
"设计.生成规则",
"设计.叙事指南与故事推进",
"设计.叙事指南",
"设计.故事推进",
"设计.正文组成",
"设计.故事推进与交互范式",
"设计.监控栏",
],
dynamic: [
@@ -529,7 +531,7 @@ const SYNTH_FALLBACK_CONTEXT: Record<
"设计.叙事指南与故事推进",
"设计.叙事指南",
"设计.故事推进",
"设计.正文组成",
"设计.故事推进与交互范式",
"设计.监控栏",
],
dynamic: ["输出.用户展示"],
+16
View File
@@ -12,6 +12,7 @@ import {
formatAcceptedUnitsForPrompt,
} from "./creation-units.js";
import { projectFragmentContent } from "./context-fragment.js";
import { tagReadFallbacks } from "./creation-flow.js";
import {
buildHistoryFallback,
isDialogueHistoryRef,
@@ -79,6 +80,21 @@ function readTagContent(
inputs: Record<string, string>,
blackboard: Blackboard,
inputMerge: BlackboardInputMerge,
): string {
const direct = readOneTag(tag, inputs, blackboard, inputMerge);
if (direct) return direct;
for (const alt of tagReadFallbacks(tag)) {
const found = readOneTag(alt, inputs, blackboard, inputMerge);
if (found) return found;
}
return "";
}
function readOneTag(
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);
+101 -13
View File
@@ -1309,16 +1309,95 @@ export function creationFlowFromRecipeSeed(
};
}
/**
* 改名后,旧步骤名和旧产物 tag 仍对上同一能力。
* 新写入用目录里的现名和现 tag;读取时两边都认。
*/
const MODULE_LEGACY: Record<
string,
{ names: string[]; artifacts: string[]; current: string }
> = {
mechanism: {
names: ["实现机制"],
artifacts: ["设计.实现机制"],
current: "设计.世界法则",
},
};
/** 读上下文时,现用 tag 与旧 tag 互为兜底。 */
export function tagReadFallbacks(tag: string): string[] {
const t = tag.trim();
for (const legacy of Object.values(MODULE_LEGACY)) {
if (t === legacy.current) return [...legacy.artifacts];
if (legacy.artifacts.includes(t)) return [legacy.current];
}
return [];
}
/** 写成现用 tag 后,旧 tag 应清掉,避免两份法则同时进上下文。 */
export function legacyTagsRetiredByWrite(tag: string): string[] {
const t = tag.trim();
for (const legacy of Object.values(MODULE_LEGACY)) {
if (t === legacy.current) return [...legacy.artifacts];
}
return [];
}
/** 现用产物 tag 在前,旧 tag 在后。 */
export function moduleArtifactTags(
mod: Pick<ModuleCatalogEntry, "id" | "artifact">,
): string[] {
const current = mod.artifact?.trim();
const legacy = MODULE_LEGACY[mod.id]?.artifacts ?? [];
return [...new Set([...(current ? [current] : []), ...legacy])];
}
export function moduleOwnsArtifactTag(
mod: Pick<ModuleCatalogEntry, "id" | "artifact">,
tag: string,
): boolean {
const bare = tag.trim();
if (!bare) return false;
return moduleArtifactTags(mod).some(
(artifact) => bare === artifact || bare.startsWith(`${artifact}#`),
);
}
function filledOwnsModule(
filled: ReadonlySet<string>,
mod: Pick<ModuleCatalogEntry, "id" | "artifact">,
): boolean {
return moduleArtifactTags(mod).some((tag) => filled.has(tag));
}
/** 黑板里已有内容的那条 tag;都空时返回现用 tag。 */
export function readModuleArtifactContent(
mod: Pick<ModuleCatalogEntry, "id" | "artifact"> | null | undefined,
read: (tag: string) => string | undefined,
): { tag: string; content: string } | null {
if (!mod) return null;
const tags = moduleArtifactTags(mod);
for (const tag of tags) {
const content = read(tag)?.trim() || "";
if (content) return { tag, content };
}
const current = tags[0];
return current ? { tag: current, content: "" } : null;
}
/** 无 id 时的兜底(目录仍应显式写 id) */
function slugFromName(name: string): string {
const map: Record<string, string> = {
美学纲领与交互范式: "aesthetics-interaction",
交互范式: "interaction",
美学纲领: "aesthetics",
美学纲领: "aesthetics-interaction",
故事推进与交互范式: "story-progression",
叙事指南: "narrative-guide",
画面与生图: "image-prompt",
叙事指南与故事推进: "narrative",
故事推进: "story-progression",
实现机制: "mechanism",
世界法则: "mechanism",
实现机制: "mechanism", // 旧称
舞台骨架: "world-blueprint",
世界蓝图与人文地理: "world-blueprint", // 旧称
世界蓝图: "world-blueprint", // 旧简称
@@ -1958,8 +2037,8 @@ export function formatFlowProgressForAgent(params: {
for (const step of flow.steps) {
if (isPrototypeStep(step)) continue;
const acceptedHere = isStepAccepted(step, accepted);
const art = catalog ? artifactTagForStep(step.name, catalog) : null;
const hasArtifact = Boolean(art && filled.has(art));
const modForArt = catalog ? findModuleByName(catalog, step.name) : null;
const hasArtifact = Boolean(modForArt && filledOwnsModule(filled, modForArt));
const repeatable = repeatableNames.has(step.name);
if (acceptedHere || (!repeatable && hasArtifact)) {
markDone(
@@ -1981,7 +2060,7 @@ export function formatFlowProgressForAgent(params: {
if (catalog) {
for (const mod of catalogModulesForIntake(catalog, "recipe")) {
if (mod.repeatable === true) continue;
if (!mod.artifact || !filled.has(mod.artifact)) continue;
if (!filledOwnsModule(filled, mod)) continue;
if (doneNames.has(mod.name)) continue;
markDone(mod.name, undefined, "产物已在黑板", false);
}
@@ -2032,7 +2111,7 @@ export function formatFlowProgressForAgent(params: {
catalog && flow
? flow.steps.filter((step) => {
if (isPrototypeStep(step)) return false;
const mod = catalog.modules.find((m) => m.name === step.name);
const mod = findModuleByName(catalog, step.name);
return Boolean(mod?.closer) && !isStepAccepted(step, accepted);
})
: [];
@@ -2131,7 +2210,10 @@ export function validateCreationFlow(
const seenIds = new Set<string>();
const allowed = catalog
? new Set(
catalogModulesForIntake(catalog, "recipe").map((m) => m.name),
catalogModulesForIntake(catalog, "recipe").flatMap((m) => [
m.name,
...(MODULE_LEGACY[m.id]?.names ?? []),
]),
)
: null;
const reportedFreshDup = new Set<string>();
@@ -2147,7 +2229,7 @@ export function validateCreationFlow(
errors.push(`「${step.name}」不在模块目录中`);
}
const mod = catalog?.modules.find((m) => m.name === step.name);
const mod = catalog ? findModuleByName(catalog, step.name) : null;
if (isReviseStep(step)) {
const ref = step.revises?.trim() ?? "";
if (!ref) {
@@ -2220,7 +2302,7 @@ export function validateStepReadyToRun(
step: CreationFlowStep,
catalog: ModuleCatalog | null,
): CreationFlowValidation {
const mod = catalog?.modules.find((m) => m.name === step.name);
const mod = findModuleByName(catalog, step.name);
if (!mod?.params?.length) return { ok: true, errors: [] };
const missing = missingRequiredStepParams(step, mod);
if (missing.length === 0) return { ok: true, errors: [] };
@@ -2260,7 +2342,7 @@ export function artifactTagForStep(
name: string,
catalog: ModuleCatalog | null,
): string | null {
return catalog?.modules.find((m) => m.name === name)?.artifact ?? null;
return findModuleByName(catalog, name)?.artifact ?? null;
}
/**
@@ -2458,7 +2540,7 @@ export function listCallableCatalogModules(params: {
for (const mod of catalogModulesForIntake(catalog, "recipe")) {
if (scheduled.has(mod.name)) continue;
if (!mod.repeatable) {
if (mod.artifact && filled.has(mod.artifact)) continue;
if (filledOwnsModule(filled, mod)) continue;
if (accepted.has(mod.name)) continue;
}
out.push({
@@ -2566,7 +2648,7 @@ export function formatCreationFlowForUser(
...(blockedBy?.length ? { blockedBy } : {}),
runState,
...(mod?.artifact ? { artifactTag: mod.artifact } : {}),
...(mod?.artifact && filled.has(mod.artifact) ? { hasArtifact: true } : {}),
...(mod && filledOwnsModule(filled, mod) ? { hasArtifact: true } : {}),
...(!proto && s.params ? { params: s.params } : {}),
...(paramsMissing.length > 0 ? { paramsMissing } : {}),
};
@@ -2579,7 +2661,13 @@ export function findModuleByName(
name: string,
): ModuleCatalogEntry | null {
if (!catalog) return null;
return catalog.modules.find((m) => m.name === name) ?? null;
const exact = catalog.modules.find((m) => m.name === name);
if (exact) return exact;
const legacyId = Object.entries(MODULE_LEGACY).find(([, legacy]) =>
legacy.names.includes(name),
)?.[0];
if (!legacyId) return null;
return catalog.modules.find((m) => m.id === legacyId) ?? null;
}
/** 已验收步骤名列表(JSON 数组或换行文本) */
+2 -2
View File
@@ -240,7 +240,7 @@ const DEFAULT_WORKER_CONTRACTS: Record<
"设计.叙事指南与故事推进",
"设计.叙事指南",
"设计.故事推进",
"设计.正文组成",
"设计.故事推进与交互范式",
"设计.监控栏",
],
dynamic: [
@@ -268,7 +268,7 @@ const DEFAULT_WORKER_CONTRACTS: Record<
"设计.叙事指南与故事推进",
"设计.叙事指南",
"设计.故事推进",
"设计.正文组成",
"设计.故事推进与交互范式",
"设计.监控栏",
],
dynamic: ["输出.用户展示", "用户.最新输入", "变量.当前"],