完善配方驱动的创作编排
为可重复技能补充参数校验与展示,统一配方、编排器和执行单元术语。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,7 +23,7 @@ function buildMainAgentSystemPrompt(
|
||||
? workers.map((w) => `- ${w.id}:${w.description}`).join("\n")
|
||||
: "- (当前 skill 未加载 worker 列表)";
|
||||
|
||||
return `你是写作系统的导演(Main Agent)。你的职责是调度演员(worker),而不是直接创作正文。
|
||||
return `你是写作系统的编排器(Main Agent)。你的职责是调度执行单元(worker),而不是直接创作正文。
|
||||
|
||||
规则:
|
||||
1. 你不能直接生成小说/文章正文。
|
||||
@@ -34,27 +34,27 @@ function buildMainAgentSystemPrompt(
|
||||
6. requiresApproval 表示运行 worker 前是否需要用户确认。代笔模式通常为 true。
|
||||
7. run_worker 可选 workerContext:{ "roleId": "A" },用于 role-decide 等指定当前决策角色(Runtime 写入 世界.当前角色.id)。
|
||||
8. 你不能把未验收内容当作事实。
|
||||
9. 向用户提问是 worker 的能力(ask_user tool),不是独立 worker。导演只在调度层提问。
|
||||
9. 向用户提问是 worker 的技能(ask_user tool),不是独立 worker。编排器只在调度层提问。
|
||||
10. blackboardIndex 只有 tag 索引,不含正文 content。
|
||||
|
||||
## 调度思维
|
||||
用需求正推:「用户需要 [具体体验/能力] → 调用 [worker/skill] 来 [生成/补充/调整] [什么],以便更好满足用户。」
|
||||
用需求正推:「用户需要 [具体体验/技能] → 调用 [worker/skill] 来 [生成/补充/调整] [什么],以便更好满足用户。」
|
||||
禁止否定式路由:「某模式 / 背景形态 → 不需要某步骤 / 跳过某 worker」。
|
||||
|
||||
反例(禁止):「背景为单一角色,无需世界构建」「不是规则怪谈,跳过 write-rules」「默认上世界模拟套件」
|
||||
正例:「用户已选世界模拟器导演且要网恋对话 → design-flow 排出近期增量步骤(可调味、可后补)→ 用户认可后反复 design-step;需要再补规则/实例 → 再 design-flow 追加同能力 → 收成后若需开局 → opening-generator;用户手动进 play」
|
||||
正例:「用户已选世界模拟器配方且要网恋对话 → design-flow 排出近期增量步骤(可调味、可后补;有编排参数的步骤须钉 params)→ 用户认可后反复 design-step;需要再补规则/实例 → 再 design-flow 追加同技能并写齐 params → 收成后若需开局 → opening-generator;用户手动进 play」
|
||||
|
||||
当前 skill 可用 worker(workerId 必须与下列 id 完全一致):
|
||||
${workerLines}
|
||||
|
||||
## design 优先顺序
|
||||
- 尚无「设计.创作流程」→ **design-flow**(近期 horizon;status=open;未选导演则先请用户选)
|
||||
- 流程已验收且还有未完成步骤 → **design-step**(一次一步;能力由程序注入)
|
||||
- 尚无「设计.创作流程」→ **design-flow**(近期 horizon;status=open;未选配方则先请用户选)
|
||||
- 流程已验收且还有未完成步骤 → **design-step**(一次一步;技能由程序注入)
|
||||
- 已列步骤都验收但流程仍 **status=open** → 再 **design-flow**(追加反复步或 closed)
|
||||
- 禁止调度已废弃的 design-core / design-fixed / design-worker / design-refine
|
||||
- 终稿已 accept 且可用 opening-generator、尚无开场产物 → opening-generator
|
||||
- 进 play 由用户手动决定
|
||||
- **禁止**替用户猜测或改选导演
|
||||
- **禁止**替用户猜测或改选配方
|
||||
- **禁止**一次编排排死全程固定 DAG
|
||||
|
||||
输出必须是 JSON 对象,字段:
|
||||
|
||||
@@ -68,29 +68,30 @@ function buildToolLoopSystemPrompt(
|
||||
「不是规则怪谈,跳过 write-rules。」
|
||||
|
||||
示例(好):
|
||||
- 「用户已选导演且要 1v1 网恋 → design-flow 排出近期增量步骤(可调味)→ 认可后反复 design-step;不够再追加。」
|
||||
- 「用户已选配方且要 1v1 网恋 → design-flow 排出近期增量步骤(可调味)→ 认可后反复 design-step;不够再追加。」
|
||||
- 「尚无设计.创作流程 → run_worker(design-flow);有未完成步骤 → design-step;steps 完但 status=open → 再 design-flow。」
|
||||
- 「Worker 集已 accept 且声明需要开局 → 调用 opening-generator 写开场白。」
|
||||
- 「运行规格已 accept 且声明需要开局 → 调用 opening-generator 写开场白。」
|
||||
|
||||
示例(坏):
|
||||
- 「调度 design-core / design-fixed(已废弃)。」
|
||||
- 「一次 design-flow 排死全程固定 DAG。」
|
||||
- 「跳过流程编排直接写满 Worker 集。」
|
||||
- 「跳过流程编排直接写满运行规格。」
|
||||
- 「默认先上世界模拟全套再问用户要什么。」
|
||||
- 「替用户改选 / 猜测导演。」
|
||||
- 「替用户改选 / 猜测配方。」
|
||||
|
||||
在 reasoning / 可见 content 中请用 **正推句式** 写出调度理由;终止 tool 的 reason 字段同样用正推表述。
|
||||
|
||||
## design 阶段提示
|
||||
- 尚无「设计.创作流程」→ **优先 design-flow**(近期 horizon;status=open;未选则先请用户选)。
|
||||
- 流程已验收、还有未完成步骤 → **design-step**(一次一步)。
|
||||
- 已列步骤全验收但 **status=open** → 再 **design-flow**(追加生成规则/具体实例等,或设 closed)。
|
||||
- 已列步骤全验收但 **status=open** → 再 **design-flow**(追加生成规则/具体实例等并钉齐 params,或设 closed)。
|
||||
- 有编排参数的步骤缺必填 params → 先让 design-flow askUser,不要空壳跑 design-step。
|
||||
- 禁止 run_worker(design-core|design-fixed|design-worker|design-refine)——已废弃。
|
||||
- 不要重复问用户「想做什么」——意图已在 用户.需求。
|
||||
- Worker 集已 accept,且可用 worker 含 opening-generator,尚无开场产物 → opening-generator。
|
||||
- 运行规格已 accept,且可用 worker 含 opening-generator,尚无开场产物 → opening-generator。
|
||||
- 进 play 由用户手动决定。
|
||||
- 未声明开局、用户也未要求开场时,不要硬调 opening-generator。
|
||||
- **禁止**替用户猜测或改选导演。
|
||||
- **禁止**替用户猜测或改选配方。
|
||||
- **禁止**一次编排排死全程固定长链。
|
||||
当前 skill 可用 worker:
|
||||
${workerLines}
|
||||
|
||||
@@ -177,7 +177,7 @@ export async function handleBooksApi(
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 新建作品:导演一层选型(内部 = recipes catalog) */
|
||||
/** 新建作品:配方一层选型(内部 = recipes catalog) */
|
||||
if (pathname === "/api/directors" && req.method === "GET") {
|
||||
const { DEFAULT_ORCHESTRATOR_ID } = await import(
|
||||
"../config/default-orchestrator.js"
|
||||
@@ -202,7 +202,7 @@ export async function handleBooksApi(
|
||||
});
|
||||
} catch (err) {
|
||||
json(res, 404, {
|
||||
error: err instanceof Error ? err.message : "未找到导演列表",
|
||||
error: err instanceof Error ? err.message : "未找到配方列表",
|
||||
});
|
||||
}
|
||||
return true;
|
||||
@@ -229,13 +229,13 @@ export async function handleBooksApi(
|
||||
});
|
||||
} catch (err) {
|
||||
json(res, 404, {
|
||||
error: err instanceof Error ? err.message : "未找到能力包",
|
||||
error: err instanceof Error ? err.message : "未找到技能包",
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 默认导演包的能力池(编排备选) */
|
||||
/** 默认配方包的技能池(编排备选) */
|
||||
if (pathname === "/api/modules" && req.method === "GET") {
|
||||
const { DEFAULT_ORCHESTRATOR_ID } = await import(
|
||||
"../config/default-orchestrator.js"
|
||||
@@ -320,9 +320,9 @@ export async function handleBooksApi(
|
||||
if (pathname === "/api/books" && req.method === "POST") {
|
||||
const body = JSON.parse(await readBody(req)) as {
|
||||
title?: string;
|
||||
/** 导演 Skill / 能力包 id(registry name) */
|
||||
/** 编排器包 id(registry name) */
|
||||
orchestratorId?: string;
|
||||
/** 用户手动选定的导演 id(内部 recipe) */
|
||||
/** 用户手动选定的配方 id(内部 recipe) */
|
||||
recipeId?: string;
|
||||
};
|
||||
const skills = await listSkills();
|
||||
@@ -332,7 +332,7 @@ export async function handleBooksApi(
|
||||
skills.find((s) => s.name === "world-simulator") ||
|
||||
skills[0];
|
||||
if (!director) {
|
||||
json(res, 400, { error: "没有可用的导演 Skill(能力包)" });
|
||||
json(res, 400, { error: "没有可用的编排器技能包" });
|
||||
return true;
|
||||
}
|
||||
const book = createBook({ title: body.title });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 用户可见中文标签(与 docs/ui-glossary.md 同步)。
|
||||
* 内部 id 不变;仅展示层映射。
|
||||
* 口径:配方 / 编排器 / 技能 / 工作流计划 / 运行规格 / 执行单元
|
||||
*/
|
||||
|
||||
const STAGE_LABELS: Record<string, string> = {
|
||||
@@ -15,15 +16,15 @@ const STAGE_LABELS: Record<string, string> = {
|
||||
|
||||
const WORKER_LABELS: Record<string, string> = {
|
||||
"design-core": "创作 · 核心",
|
||||
"design-worker": "创作 · 演员规格",
|
||||
"design-worker": "创作 · 执行单元规格",
|
||||
"design-fixed": "创作 · 固定上下文",
|
||||
"design-refine": "创作 · 细化与终稿",
|
||||
"design-intake": "创作 · 综合收口",
|
||||
"design-flow": "创作 · 流程编排",
|
||||
"design-step": "创作 · 执行步骤",
|
||||
"opening-generator": "开局 · 开场白",
|
||||
orchestrator: "导演",
|
||||
"agent-burst": "导演调度",
|
||||
orchestrator: "编排器",
|
||||
"agent-burst": "编排器调度",
|
||||
narrator: "叙事转述",
|
||||
"role-decide": "角色决策",
|
||||
"world-simulator": "世界推演",
|
||||
@@ -57,14 +58,14 @@ export function displayStageLabel(id: string | undefined | null): string {
|
||||
return STAGE_LABELS[id] ?? id;
|
||||
}
|
||||
|
||||
/** 导演选项 / skill pack 展示名 */
|
||||
/** 配方选项 / skill pack 展示名 */
|
||||
export function displaySkillPackLabel(id: string | undefined | null): string {
|
||||
if (!id) return "";
|
||||
return SKILL_PACK_LABELS[id] ?? id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 演员 / 单位 / 能力 id → 用户可见名(不含动作后缀)。
|
||||
* 执行单元 / 单位 / 技能 id → 用户可见名(不含动作后缀)。
|
||||
*/
|
||||
export function displayWorkerLabel(id: string | undefined | null): string {
|
||||
if (!id) return "";
|
||||
@@ -79,11 +80,11 @@ export function displayWorkerLabel(id: string | undefined | null): string {
|
||||
}
|
||||
if (trimmed.startsWith("worker:")) {
|
||||
const ref = trimmed.slice("worker:".length);
|
||||
return `演员 · ${displayWorkerLabel(ref)}`;
|
||||
return `执行单元 · ${displayWorkerLabel(ref)}`;
|
||||
}
|
||||
if (trimmed.startsWith("fixed:")) {
|
||||
const topic = trimmed.slice("fixed:".length);
|
||||
return `能力 · ${FIXED_TOPIC_LABELS[topic] ?? topic}`;
|
||||
return `技能 · ${FIXED_TOPIC_LABELS[topic] ?? topic}`;
|
||||
}
|
||||
if (trimmed.startsWith("resident:")) {
|
||||
return `常驻 · ${trimmed.slice("resident:".length)}`;
|
||||
@@ -99,7 +100,7 @@ export function formatWorkerDisplayTitle(
|
||||
workerId: string | undefined | null,
|
||||
action: WorkerTitleAction = null,
|
||||
): string {
|
||||
const base = displayWorkerLabel(workerId) || "演员";
|
||||
const base = displayWorkerLabel(workerId) || "执行单元";
|
||||
switch (action) {
|
||||
case "output":
|
||||
return `${base} · 产出`;
|
||||
@@ -114,8 +115,8 @@ export function formatWorkerDisplayTitle(
|
||||
}
|
||||
}
|
||||
|
||||
/** 导演(调度 Agent)相关标题 */
|
||||
/** 编排器(调度 Agent)相关标题 */
|
||||
export function formatAgentDisplayTitle(detail?: string): string {
|
||||
if (detail?.trim()) return `导演 · ${detail.trim()}`;
|
||||
return "导演";
|
||||
if (detail?.trim()) return `编排器 · ${detail.trim()}`;
|
||||
return "编排器";
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* 可变增量 DAG:有序 steps + 每步 id/中文名 + depends_on;可追加、可同能力多次。
|
||||
*
|
||||
* 两层内容(作者细写,运行时只搭骨架):
|
||||
* - recipes/:初始配方(给总管 / design-flow 的参考起点,可调味)
|
||||
* - modules/:共用组件池(步骤名与方法正文;配方与总管都从这里选型)
|
||||
* - recipes/:配方方法论(适用、核心思路、设计流程、原则)+ 近期起点 steps
|
||||
* - modules/:共用能力池;编排注入 meta 选型字段,执行注入方法全文
|
||||
*/
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
@@ -25,6 +25,9 @@ const DEFAULT_SKILLS_ROOT = path.resolve(
|
||||
"../../skills",
|
||||
);
|
||||
|
||||
/** 步骤调用参数(编排期钉死;执行期只读) */
|
||||
export type CreationFlowStepParams = Record<string, unknown>;
|
||||
|
||||
export type CreationFlowStep = {
|
||||
/**
|
||||
* 本局步骤唯一 id(验收与 depends_on 用这个)。
|
||||
@@ -35,6 +38,11 @@ export type CreationFlowStep = {
|
||||
name: string;
|
||||
/** 依赖的其它步骤 id(旧稿若 name 唯一也可写 name) */
|
||||
depends_on: string[];
|
||||
/**
|
||||
* 本步调用参数。有「编排参数」声明的能力必须在进 design-step 前钉齐必填项。
|
||||
* 例:生成规则 → target;具体实例 → rule_id。
|
||||
*/
|
||||
params?: CreationFlowStepParams;
|
||||
};
|
||||
|
||||
export type CreationFlowStatus = "open" | "closed";
|
||||
@@ -51,6 +59,17 @@ export type CreationFlow = {
|
||||
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;
|
||||
@@ -69,6 +88,17 @@ export type ModuleCatalogEntry = {
|
||||
* 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 看见) */
|
||||
@@ -95,14 +125,24 @@ export type RecipeCatalog = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 单份初始配方详情。
|
||||
* seed = 建议步骤(可为空;name 须 ∈ 模块池);编排时允许增删改。
|
||||
* 单份配方详情。
|
||||
* 方法论字段供编排选型;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;
|
||||
};
|
||||
@@ -125,6 +165,10 @@ export type CreationFlowUserView = {
|
||||
/** 目录里的短声明(有则展示) */
|
||||
declaration?: string;
|
||||
repeatable?: boolean;
|
||||
/** 本步调用参数(编排期钉死) */
|
||||
params?: CreationFlowStepParams;
|
||||
/** 参数缺必填项时的提示(给人看) */
|
||||
paramsMissing?: string[];
|
||||
}>;
|
||||
parseError?: string;
|
||||
};
|
||||
@@ -150,9 +194,65 @@ export function extractJsonObject(raw: string): unknown | 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<string, unknown>)) {
|
||||
const key = k.trim();
|
||||
if (!key) continue;
|
||||
out[key] = v;
|
||||
}
|
||||
return Object.keys(out).length > 0 ? out : undefined;
|
||||
}
|
||||
|
||||
/** 目录声明的必填参数中,本步仍缺失的 key(按声明顺序) */
|
||||
export function missingRequiredStepParams(
|
||||
step: Pick<CreationFlowStep, "params">,
|
||||
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[] }>,
|
||||
steps: Array<{
|
||||
id?: string;
|
||||
name: string;
|
||||
depends_on: string[];
|
||||
params?: CreationFlowStepParams;
|
||||
}>,
|
||||
): CreationFlowStep[] {
|
||||
const used = new Set<string>();
|
||||
const nameCount = new Map<string, number>();
|
||||
@@ -177,6 +277,7 @@ export function ensureCreationFlowStepIds(
|
||||
id,
|
||||
name,
|
||||
depends_on: raw.depends_on.map((d) => d.trim()).filter(Boolean),
|
||||
...(raw.params ? { params: raw.params } : {}),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
@@ -208,7 +309,12 @@ export function parseCreationFlow(raw: string | undefined | null): CreationFlow
|
||||
const stepsRaw = row.steps;
|
||||
if (!Array.isArray(stepsRaw) || stepsRaw.length === 0) return null;
|
||||
|
||||
const drafted: Array<{ id?: string; name: string; depends_on: string[] }> = [];
|
||||
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<string, unknown>;
|
||||
@@ -219,7 +325,8 @@ export function parseCreationFlow(raw: string | undefined | null): CreationFlow
|
||||
const depends_on = Array.isArray(depsRaw)
|
||||
? depsRaw.map((d) => String(d).trim()).filter(Boolean)
|
||||
: [];
|
||||
drafted.push({ id, name, depends_on });
|
||||
const params = normalizeStepParams(s.params);
|
||||
drafted.push({ id, name, depends_on, ...(params ? { params } : {}) });
|
||||
}
|
||||
|
||||
const steps = ensureCreationFlowStepIds(drafted);
|
||||
@@ -264,6 +371,7 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null {
|
||||
? m.opening.trim()
|
||||
: undefined;
|
||||
const repeatable = m.repeatable === true;
|
||||
const params = parseModuleParamSpecs(m.params);
|
||||
modules.push({
|
||||
id,
|
||||
name,
|
||||
@@ -271,12 +379,36 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null {
|
||||
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<string, unknown>;
|
||||
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。
|
||||
@@ -327,6 +459,78 @@ export function getModuleSection(
|
||||
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<string, unknown>;
|
||||
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<ModuleCatalog> {
|
||||
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 … ```(能力标准块)。
|
||||
@@ -416,19 +620,49 @@ export async function loadModuleCatalog(
|
||||
const fullPath = path.join(skillsRoot, skillPackRoot, MODULE_CATALOG_FILENAME);
|
||||
try {
|
||||
const raw = await readFile(fullPath, "utf8");
|
||||
return parseModuleCatalog(raw);
|
||||
const catalog = parseModuleCatalog(raw);
|
||||
if (!catalog) return null;
|
||||
return enrichModuleCatalogWithMeta(catalog, skillPackRoot, skillsRoot);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 注入 design-flow 的短目录(非全文 prompt) */
|
||||
/** 注入 design-flow:能力名 + 选型字段(meta)+ 编排参数;非执行全文 */
|
||||
export function formatModuleCatalogForAgent(catalog: ModuleCatalog): string {
|
||||
const lines = catalog.modules.map((m) => {
|
||||
const flags = m.repeatable ? "〔可反复〕" : "";
|
||||
return `- ${m.name}${flags}:${m.declaration}`;
|
||||
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 `【能力 · 可选工序】(按需选用,勿默认全选;步骤名只能从这里选;标〔可反复〕的可多次编入)\n${lines.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 {
|
||||
@@ -476,8 +710,8 @@ export async function loadRecipeCatalog(
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单份 recipe.yaml(when / hint / brief / steps)。
|
||||
* steps 空或缺失 → seed 为 null(仍可作选型参考)。
|
||||
* 解析单份 recipe.yaml(when / core / process / principles / brief / steps)。
|
||||
* 兼容旧字段 hint。steps 空或缺失 → seed 为 null(仍可作选型参考)。
|
||||
*/
|
||||
export function parseRecipeYaml(
|
||||
raw: string,
|
||||
@@ -507,6 +741,9 @@ export function parseRecipeYaml(
|
||||
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()
|
||||
@@ -528,11 +765,28 @@ export function parseRecipeYaml(
|
||||
name,
|
||||
declaration: meta.declaration,
|
||||
when,
|
||||
hint,
|
||||
...(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,
|
||||
@@ -610,21 +864,36 @@ export function formatRecipeCatalogForAgent(catalog: RecipeCatalog): string {
|
||||
const lines = catalog.recipes.map(
|
||||
(r) => `- ${r.name}:${r.declaration}`,
|
||||
);
|
||||
return `【可选导演】(须由用户手动选择)\n${lines.join("\n")}`;
|
||||
return `【可选配方】(须由用户手动选择)\n${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
/** 注入 design-flow:用户已选导演(内部 recipe) */
|
||||
/** 注入 design-flow:用户已选配方(方法论 + 近期起点) */
|
||||
export function formatSelectedRecipeForAgent(detail: RecipeDetail): string {
|
||||
const lines: string[] = [
|
||||
`【用户已选导演 · ${detail.name}】`,
|
||||
"这是用户手动选定的方法起点,不是锁死流水线。",
|
||||
"产出**增量 DAG**:只排近期要做的步骤;已验收步保留,可追加同能力多次调用(如生成规则 / 具体实例)。",
|
||||
"按用户表述增删改未验收步骤与依赖(像现场改戏 / 调味);步骤名只能从【能力】选。",
|
||||
"禁止改选其它导演;若用户要换导演,须等用户重新选定后再编排。",
|
||||
`【用户已选配方 · ${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.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(
|
||||
{
|
||||
@@ -645,7 +914,14 @@ export function formatSelectedRecipeForAgent(detail: RecipeDetail): string {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** design-flow 一次注入:已选配方 + 组件池 */
|
||||
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;
|
||||
@@ -655,9 +931,9 @@ export function formatDesignFlowContentBlocks(params: {
|
||||
if (params.missingSelection) {
|
||||
blocks.push(
|
||||
[
|
||||
"【导演】用户尚未手动选择。",
|
||||
"禁止自行猜测或替用户选定导演。",
|
||||
"请 askUser 请用户从可用导演中选择,或等待用户在界面选定后再编排。",
|
||||
"【配方】用户尚未手动选择。",
|
||||
"禁止自行猜测或替用户选定配方。",
|
||||
"请 askUser 请用户从可用配方中选择,或等待用户在界面选定后再编排。",
|
||||
].join("\n"),
|
||||
);
|
||||
} else if (params.selectedRecipe) {
|
||||
@@ -735,6 +1011,21 @@ export function validateCreationFlow(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
@@ -762,6 +1053,7 @@ export function formatCreationFlowForUser(
|
||||
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,
|
||||
@@ -770,6 +1062,8 @@ export function formatCreationFlowForUser(
|
||||
occurrence: n,
|
||||
declaration: mod?.declaration,
|
||||
repeatable: mod?.repeatable,
|
||||
...(s.params ? { params: s.params } : {}),
|
||||
...(paramsMissing.length > 0 ? { paramsMissing } : {}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -510,6 +510,7 @@ export async function loadWorkerSkillWithContext(
|
||||
resolveDesignStepBinding,
|
||||
CREATION_CURRENT_STEP_TAG,
|
||||
CREATION_MODULE_OPENING_TAG,
|
||||
formatStepParamsForPrompt,
|
||||
} = await import("./creation-flow.js");
|
||||
const binding = await resolveDesignStepBinding({
|
||||
skillPackRoot: skill.skillPackRoot,
|
||||
@@ -522,7 +523,8 @@ export async function loadWorkerSkillWithContext(
|
||||
const openingNote = binding.opening
|
||||
? `\n\n【程序开场】若黑板有「${CREATION_MODULE_OPENING_TAG}」,该默认问题已由程序发给用户(不经 LLM);用户首答在「用户.worker答复」。勿重复同一开场白,在其答复与提示词基础上继续追问或产出。`
|
||||
: "";
|
||||
modulePromptBlock = `## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}`;
|
||||
const paramsBlock = `## 【本步参数】(编排期已钉;直接按此执行,勿再问「生成什么 / 调用哪个规则」)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
|
||||
modulePromptBlock = `${paramsBlock}\n\n## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}`;
|
||||
const baseInputs = [
|
||||
"用户.需求",
|
||||
"book.brief",
|
||||
|
||||
Reference in New Issue
Block a user