/** * 创作流程:编排产物(设计.创作流程)。 * 可变增量 DAG:有序 steps + 每步 id/中文名 + depends_on;可追加、可同能力多次。 * * 两层内容(作者细写,运行时只搭骨架): * - recipes/:配方方法论(适用、核心思路、设计流程、原则)+ 近期起点 steps * - modules/:共用能力池;编排注入 meta 选型字段,执行注入方法全文 */ 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 CreationFlowStepParams = Record; export type CreationFlowStep = { /** * 本局步骤唯一 id(验收与 depends_on 用这个)。 * 同能力可多次出现时必须不同;缺省时程序按 name / name#n 补齐。 */ id: string; /** 固定中文名,须 ∈ 模块目录(可重复) */ name: string; /** 依赖的其它步骤 id(旧稿若 name 唯一也可写 name) */ depends_on: string[]; /** * 本步调用参数。有「编排参数」声明的能力必须在进 design-step 前钉齐必填项。 * 例:生成规则 → target;具体实例 → rule_id。 */ params?: CreationFlowStepParams; }; export type CreationFlowStatus = "open" | "closed"; export type CreationFlow = { version: 1; /** 可选一句体验复述(给人看) */ brief?: string; /** * open = 当前只是近期 horizon,还可增量追加 / 反复编排同能力; * closed = 不再扩步(可走收成)。缺省按 closed(兼容旧固定 DAG)。 */ status?: CreationFlowStatus; steps: CreationFlowStep[]; }; /** catalog 声明:编排进 DAG 时本步需要哪些调用参数 */ export type ModuleParamSpec = { key: string; /** 给人看的中文名 */ label: string; /** 缺省 false;true = validateCreationFlow / 编排必须钉齐 */ required?: boolean; /** 给编排器的短提示(选项从何来、可否其它) */ hint?: string; }; 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; /** * 可选:编排期步骤参数声明。 * 有 required 项时,steps[].params 必须在进执行前钉齐;勿把选型推迟到 design-step。 */ params?: ModuleParamSpec[]; /** 来自 prompt.md ```meta:何时该选用(编排选型) */ when?: string; /** 来自 prompt.md ```meta:何时不该选用 */ when_not?: string; /** 来自 prompt.md ```meta:与其它能力的边界 */ boundary?: 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; export type ModuleCatalog = { modules: ModuleCatalogEntry[]; }; /** 初始配方目录条目(短声明,给选型) */ export type RecipeCatalogEntry = { id: string; name: string; declaration: string; }; export type RecipeCatalog = { recipes: RecipeCatalogEntry[]; }; /** * 单份配方详情。 * 方法论字段供编排选型;seed = 近期起点 steps(可为空;name 须 ∈ 模块池)。 */ export type RecipeDetail = { id: string; name: string; declaration: string; /** 适用什么体验/任务 */ when?: string; /** 整套设计方法的核心思路与最终目标 */ core?: string; /** 设计流程:如何增量选型、何时收成(字符串或条目列表) */ process?: string | string[]; /** 配方特有取舍原则 */ principles?: string | string[]; /** * @deprecated 旧字段;新配方用 core/process/principles。解析仍可读,格式化时作兜底。 */ 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; /** 本步调用参数(编排期钉死) */ params?: CreationFlowStepParams; /** 参数缺必填项时的提示(给人看) */ paramsMissing?: string[]; }>; 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; } /** 规范化 steps[].params:仅接受普通对象 */ export function normalizeStepParams( raw: unknown, ): CreationFlowStepParams | undefined { if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined; const out: CreationFlowStepParams = {}; for (const [k, v] of Object.entries(raw as Record)) { const key = k.trim(); if (!key) continue; out[key] = v; } return Object.keys(out).length > 0 ? out : undefined; } /** 目录声明的必填参数中,本步仍缺失的 key(按声明顺序) */ export function missingRequiredStepParams( step: Pick, module: ModuleCatalogEntry | null | undefined, ): string[] { const specs = module?.params ?? []; if (specs.length === 0) return []; const params = step.params ?? {}; const missing: string[] = []; for (const spec of specs) { if (!spec.required) continue; const v = params[spec.key]; if (v == null) { missing.push(spec.key); continue; } if (typeof v === "string" && !v.trim()) { missing.push(spec.key); } } return missing; } /** 给人 / LLM 看的参数摘要 */ export function formatStepParamsForPrompt( params: CreationFlowStepParams | null | undefined, ): string { if (!params || Object.keys(params).length === 0) return "(无)"; return Object.entries(params) .map(([k, v]) => { const rendered = typeof v === "string" ? v : JSON.stringify(v, null, 0); return `- ${k}: ${rendered}`; }) .join("\n"); } /** 为缺 id 的步骤补齐唯一 id;同 name 多次 → name#2、name#3… */ export function ensureCreationFlowStepIds( steps: Array<{ id?: string; name: string; depends_on: string[]; params?: CreationFlowStepParams; }>, ): CreationFlowStep[] { const used = new Set(); const nameCount = new Map(); 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), ...(raw.params ? { params: raw.params } : {}), }); } 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; const stepsRaw = row.steps; if (!Array.isArray(stepsRaw) || stepsRaw.length === 0) return null; const drafted: Array<{ id?: string; name: string; depends_on: string[]; params?: CreationFlowStepParams; }> = []; for (const item of stepsRaw) { if (!item || typeof item !== "object" || Array.isArray(item)) return null; const s = item as Record; 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) : []; const params = normalizeStepParams(s.params); drafted.push({ id, name, depends_on, ...(params ? { params } : {}) }); } 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).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; 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; const params = parseModuleParamSpecs(m.params); modules.push({ id, name, declaration, artifact, ...(repeatable ? { repeatable: true } : {}), ...(opening ? { opening } : {}), ...(params ? { params } : {}), }); } if (modules.length === 0) return null; return { modules }; } function parseModuleParamSpecs(raw: unknown): ModuleParamSpec[] | undefined { if (!Array.isArray(raw) || raw.length === 0) return undefined; const out: ModuleParamSpec[] = []; for (const item of raw) { if (!item || typeof item !== "object" || Array.isArray(item)) continue; const row = item as Record; const key = typeof row.key === "string" ? row.key.trim() : ""; const label = typeof row.label === "string" ? row.label.trim() : ""; if (!key || !label) continue; const hint = typeof row.hint === "string" && row.hint.trim() ? row.hint.trim() : undefined; out.push({ key, label, ...(row.required === true ? { required: true } : {}), ...(hint ? { hint } : {}), }); } return out.length > 0 ? out : undefined; } /** * 能力 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; }; /** * 切割能力文档:抽取全部 ```lang … ``` 块。 * 同一 lang 多次出现时拼接(中间空行)。 */ export function parseModulePromptSections(promptMd: string): ModulePromptSections { const blocks: Record = {}; 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; } /** 把 YAML 字段收成可展示的字符串(支持 string / string[]) */ export function coerceYamlTextField(raw: unknown): string | undefined { if (typeof raw === "string" && raw.trim()) return raw.trim(); if (Array.isArray(raw)) { const lines = raw .map((x) => (typeof x === "string" ? x.trim() : "")) .filter(Boolean); return lines.length ? lines.map((l) => `- ${l}`).join("\n") : undefined; } return undefined; } /** * 从 prompt.md 的 ```meta 块解析选型字段。 * catalog 负责索引/params;when/when_not/boundary 以 meta 为准。 */ export function parseModuleMetaFromPrompt(promptMd: string): { declaration?: string; when?: string; when_not?: string; boundary?: string; } | null { const metaRaw = getModuleSection(parseModulePromptSections(promptMd), "meta"); if (!metaRaw) return null; let doc: unknown; try { doc = parseYaml(metaRaw); } catch { return null; } if (!doc || typeof doc !== "object" || Array.isArray(doc)) return null; const row = doc as Record; const declaration = coerceYamlTextField(row.declaration); const when = coerceYamlTextField(row.when); const when_not = coerceYamlTextField(row.when_not); const boundary = coerceYamlTextField(row.boundary); if (!declaration && !when && !when_not && !boundary) return null; return { ...(declaration ? { declaration } : {}), ...(when ? { when } : {}), ...(when_not ? { when_not } : {}), ...(boundary ? { boundary } : {}), }; } /** * 用各能力 prompt.md 的 meta 充实目录条目(编排选型用)。 * declaration:meta 有则覆盖 catalog;when/when_not/boundary:仅来自 meta。 */ export async function enrichModuleCatalogWithMeta( catalog: ModuleCatalog, skillPackRoot: string, skillsRoot = DEFAULT_SKILLS_ROOT, ): Promise { const modules = await Promise.all( catalog.modules.map(async (m) => { const prompt = await loadModulePrompt(skillPackRoot, m.id, skillsRoot); if (!prompt) return m; const meta = parseModuleMetaFromPrompt(prompt); if (!meta) return m; return { ...m, ...(meta.declaration ? { declaration: meta.declaration } : {}), ...(meta.when ? { when: meta.when } : {}), ...(meta.when_not ? { when_not: meta.when_not } : {}), ...(meta.boundary ? { boundary: meta.boundary } : {}), }; }), ); return { modules }; } /** * 从能力 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)) { 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 = { 美学纲领与交互范式: "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 { const fullPath = path.join(skillsRoot, skillPackRoot, MODULE_CATALOG_FILENAME); try { const raw = await readFile(fullPath, "utf8"); const catalog = parseModuleCatalog(raw); if (!catalog) return null; return enrichModuleCatalogWithMeta(catalog, skillPackRoot, skillsRoot); } catch { return null; } } /** 注入 design-flow:能力名 + 选型字段(meta)+ 编排参数;非执行全文 */ export function formatModuleCatalogForAgent(catalog: ModuleCatalog): string { const lines = catalog.modules.map((m) => { const flags = m.repeatable ? "〔可反复〕" : ""; const parts: string[] = [`- ${m.name}${flags}:${m.declaration}`]; if (m.when) parts.push(` 何时用:${indentMultiline(m.when, " ")}`); if (m.when_not) parts.push(` 何时不用:${indentMultiline(m.when_not, " ")}`); if (m.boundary) parts.push(` 边界:${indentMultiline(m.boundary, " ")}`); if (m.params && m.params.length > 0) { parts.push( ` 编排参数:${m.params .map((p) => { const req = p.required ? "必填" : "可选"; const hint = p.hint ? `,${p.hint}` : ""; return `${p.key}(${p.label},${req}${hint})`; }) .join(";")}`, ); } return parts.join("\n"); }); return [ "【能力 · 可选工序】", "按需选用,勿默认全选;步骤名只能从这里选;标〔可反复〕的可多次编入。", "选型依据是下方「何时用 / 何时不用 / 边界」(来自各能力 meta);有「编排参数」的步骤必须在 DAG 里写齐 params,缺参时用 askUser 选项+其它,禁止空壳进执行。", "不要把能力执行全文塞进本步;执行由 design-step 注入。", lines.join("\n"), ].join("\n"); } /** 多行字段:首行接在标签后,续行缩进 */ function indentMultiline(text: string, indent: string): string { const lines = text.split(/\r?\n/); if (lines.length <= 1) return text; return [lines[0], ...lines.slice(1).map((l) => `${indent}${l}`)].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).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; 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 { const fullPath = path.join(skillsRoot, skillPackRoot, RECIPE_CATALOG_FILENAME); try { const raw = await readFile(fullPath, "utf8"); return parseRecipeCatalog(raw); } catch { return null; } } /** * 解析单份 recipe.yaml(when / core / process / principles / brief / steps)。 * 兼容旧字段 hint。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; const when = typeof row.when === "string" && row.when.trim() ? row.when.trim() : undefined; const core = coerceYamlTextField(row.core); const process = normalizeRecipeListOrText(row.process); const principles = normalizeRecipeListOrText(row.principles); 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, ...(core ? { core } : {}), ...(process ? { process } : {}), ...(principles ? { principles } : {}), ...(hint ? { hint } : {}), seed, }; } /** process / principles:保留数组,或收成单字符串 */ function normalizeRecipeListOrText( raw: unknown, ): string | string[] | undefined { if (typeof raw === "string" && raw.trim()) return raw.trim(); if (Array.isArray(raw)) { const items = raw .map((x) => (typeof x === "string" ? x.trim() : "")) .filter(Boolean); return items.length ? items : undefined; } return undefined; } export async function loadRecipeDetail( skillPackRoot: string, entry: RecipeCatalogEntry, skillsRoot = DEFAULT_SKILLS_ROOT, ): Promise { 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 { 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; 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:用户已选配方(方法论 + 近期起点) */ 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.core) { lines.push("核心思路:"); lines.push(detail.core); } if (detail.process) { lines.push("设计流程:"); lines.push(formatRecipeFieldBlock(detail.process)); } if (detail.principles) { lines.push("原则:"); lines.push(formatRecipeFieldBlock(detail.principles)); } if (!detail.core && !detail.process && !detail.principles && 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"); } function formatRecipeFieldBlock(value: string | string[]): string { if (Array.isArray(value)) { return value.map((l) => `- ${l}`).join("\n"); } return value; } /** 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 { 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(); const allowed = catalog ? new Set(catalog.modules.map((m) => m.name)) : null; const nameCount = new Map(); 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}」未排在其前面`, ); } } if (mod?.params?.length) { const missing = missingRequiredStepParams(step, mod); if (missing.length > 0) { const labels = missing .map((key) => { const spec = mod.params!.find((p) => p.key === key); return spec ? `${key}(${spec.label})` : key; }) .join("、"); errors.push( `「${step.id}」缺少必填编排参数:${labels}(须在 design-flow 钉齐后再执行)`, ); } } } 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(); 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); const paramsMissing = missingRequiredStepParams(s, mod); return { order: i + 1, id: s.id, name: s.name, depends_on: s.depends_on, occurrence: n, declaration: mod?.declaration, repeatable: mod?.repeatable, ...(s.params ? { params: s.params } : {}), ...(paramsMissing.length > 0 ? { paramsMissing } : {}), }; }), }; } 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 { 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 { 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, }; }