为转述进料单独挂配方目录,避免与节点流程导演共用。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
# 导演选项目录(用户在新建作品时手动选择;对用户称「导演」/「配方」)
|
||||
# 节点流程 · 配方目录(与转述进料配方不共享)
|
||||
# id = recipes/{id}/ 文件夹
|
||||
# name = 固定中文名(下拉展示)
|
||||
# declaration = 给人看的短说明
|
||||
# 内部仍叫 recipe;勿对用户再说「配方」作第二层选项
|
||||
# recipe.yaml 写方法论:when / core / process / principles + 近期 steps
|
||||
# 步骤 name 必须 ∈ modules/catalog.yaml(【能力】)
|
||||
# 选定后写入黑板 tag 创作.选用配方
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# 转述进料 · 配方目录(与节点流程配方不共享)
|
||||
# id = recipes/转述/{id}/ 文件夹;对用户展示用 name(中文)
|
||||
# 选定后写入黑板 tag 创作.选用配方;转述模式不种节点 DAG
|
||||
|
||||
recipes:
|
||||
- id: 文本生成器
|
||||
name: 文本生成器
|
||||
declaration: 钉需求与文风;无世界推演。正文格式与开场白另步。
|
||||
- id: 正文组成
|
||||
name: 正文组成
|
||||
declaration: 钉呈现格式(壳与分区);开场白按该格式生成
|
||||
@@ -0,0 +1,25 @@
|
||||
# 文本生成器 · 转述进料配方
|
||||
# 钉体验与设定;不写开场叙事本身。正文组成(格式)与开场白(按格式生成)见另配方或同提示分阶段。
|
||||
|
||||
when: >-
|
||||
按用户需求钉清要写什么、给谁看、气质与禁忌;
|
||||
不要世界推演、不要跨轮状态机、不要把聊天确认段当成开场白
|
||||
|
||||
core: >-
|
||||
产物落需求与文风类上下文;默认无变量、无旁观、无主世界层。
|
||||
「正文」指展示格式(设计.正文组成),不是开场白;开场白按格式另生成。
|
||||
|
||||
process:
|
||||
- 听用户说清要写什么与交付形态;写入需求/文风等设定产物。
|
||||
- 缺关键信息就问;不要问卷式盘问。
|
||||
- 不要去搭世界模拟、变量、舞台骨架。
|
||||
- 不要把整章叙事只留在聊天确认段里。
|
||||
|
||||
principles:
|
||||
- 正推文本交付规格,勿套世界模拟全套。
|
||||
- 保留用户原意,少二次创作。
|
||||
- 格式与开场白分清:正文组成≠开场白。
|
||||
|
||||
brief: 文本生成:钉需求与文风(正文格式 / 开场白另步)
|
||||
|
||||
steps: []
|
||||
@@ -0,0 +1,25 @@
|
||||
# 正文组成 · 转述进料配方
|
||||
# 只钉「怎么给用户看」的格式;开场白按此格式另生成。
|
||||
|
||||
when: >-
|
||||
已大致知道要什么体验,需要定版式:选壳、分区、各区职责;
|
||||
或用户在谈呈现/版式/要不要选项坞/监控栏
|
||||
|
||||
core: >-
|
||||
写入设计.正文组成(格式契约)。
|
||||
这不是开场白、不是故事正文;开场白必须按本格式的可见块生成。
|
||||
|
||||
process:
|
||||
- 对照设定产物,选 shell_id(文本沉浸默认 prose)。
|
||||
- 写可见块挂到壳区域(至少 body)。
|
||||
- 需要时写微调(tone_chrome、block_labels、是否显示建议行动)。
|
||||
- 用户要第一幕时:再按本格式生成设计.开场白(内容),勿把格式 JSON 当开场白展示。
|
||||
|
||||
principles:
|
||||
- 格式权威在设计.正文组成;开场白服从格式。
|
||||
- 不重议文风遣词(归叙事/需求);本步只交展示契约。
|
||||
- 产物自洽:只读设计.正文组成就能知道开场/每轮终稿怎么灌数。
|
||||
|
||||
brief: 正文组成:钉呈现壳与可见块(格式)→ 开场白按格式生成
|
||||
|
||||
steps: []
|
||||
@@ -52,6 +52,8 @@ const PHASE_UNIT_LABELS: Record<string, string> = {
|
||||
const SKILL_PACK_LABELS: Record<string, string> = {
|
||||
"world-simulator": "世界模拟器",
|
||||
"expand-assistant": "扩写助手",
|
||||
文本生成器: "文本生成器",
|
||||
"text-generator": "文本生成器",
|
||||
};
|
||||
|
||||
/** 生命周期 / 相位 */
|
||||
|
||||
+43
-15
@@ -49,9 +49,15 @@ export function isRuntimePinnedTag(tag: string): boolean {
|
||||
}
|
||||
export const MODULE_CATALOG_FILENAME = "modules/catalog.yaml";
|
||||
export const RECIPE_CATALOG_FILENAME = "recipes/catalog.yaml";
|
||||
/** 转述进料专用配方目录(与节点流程 catalog 不共享) */
|
||||
export const DICTATE_RECIPE_CATALOG_FILENAME = "recipes/转述/catalog.yaml";
|
||||
export const DICTATE_RECIPE_DIR = "recipes/转述";
|
||||
export const DESIGN_STEP_WORKER_ID = "design-step";
|
||||
export const DESIGN_FLOW_WORKER_ID = "design-flow";
|
||||
|
||||
/** 配方所属进料族:节点流程 vs 转述 */
|
||||
export type RecipeIntakeFamily = "recipe" | "dictate";
|
||||
|
||||
const DEFAULT_SKILLS_ROOT = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../../skills",
|
||||
@@ -225,6 +231,8 @@ export type RecipeCatalogEntry = {
|
||||
id: string;
|
||||
name: string;
|
||||
declaration: string;
|
||||
/** 条目来自哪本目录;缺省视为节点流程 */
|
||||
family?: RecipeIntakeFamily;
|
||||
};
|
||||
|
||||
export type RecipeCatalog = {
|
||||
@@ -1329,7 +1337,10 @@ function indentMultiline(text: string, indent: string): string {
|
||||
return [lines[0], ...lines.slice(1).map((l) => `${indent}${l}`)].join("\n");
|
||||
}
|
||||
|
||||
export function parseRecipeCatalog(raw: string): RecipeCatalog | null {
|
||||
export function parseRecipeCatalog(
|
||||
raw: string,
|
||||
family: RecipeIntakeFamily = "recipe",
|
||||
): RecipeCatalog | null {
|
||||
let doc: unknown;
|
||||
try {
|
||||
doc = parseYaml(raw);
|
||||
@@ -1354,7 +1365,7 @@ export function parseRecipeCatalog(raw: string): RecipeCatalog | null {
|
||||
? slugFromName(name)
|
||||
: "";
|
||||
if (!name || !declaration || !id) continue;
|
||||
recipes.push({ id, name, declaration });
|
||||
recipes.push({ id, name, declaration, family });
|
||||
}
|
||||
if (recipes.length === 0) return null;
|
||||
return { recipes };
|
||||
@@ -1363,11 +1374,16 @@ export function parseRecipeCatalog(raw: string): RecipeCatalog | null {
|
||||
export async function loadRecipeCatalog(
|
||||
skillPackRoot: string,
|
||||
skillsRoot = DEFAULT_SKILLS_ROOT,
|
||||
family: RecipeIntakeFamily = "recipe",
|
||||
): Promise<RecipeCatalog | null> {
|
||||
const fullPath = path.join(skillsRoot, skillPackRoot, RECIPE_CATALOG_FILENAME);
|
||||
const filename =
|
||||
family === "dictate"
|
||||
? DICTATE_RECIPE_CATALOG_FILENAME
|
||||
: RECIPE_CATALOG_FILENAME;
|
||||
const fullPath = path.join(skillsRoot, skillPackRoot, filename);
|
||||
try {
|
||||
const raw = await readFile(fullPath, "utf8");
|
||||
return parseRecipeCatalog(raw);
|
||||
return parseRecipeCatalog(raw, family);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -1456,13 +1472,17 @@ export async function loadRecipeDetail(
|
||||
entry: RecipeCatalogEntry,
|
||||
skillsRoot = DEFAULT_SKILLS_ROOT,
|
||||
): Promise<RecipeDetail> {
|
||||
const fullPath = path.join(
|
||||
skillsRoot,
|
||||
skillPackRoot,
|
||||
"recipes",
|
||||
entry.id,
|
||||
"recipe.yaml",
|
||||
);
|
||||
const family = entry.family ?? "recipe";
|
||||
const fullPath =
|
||||
family === "dictate"
|
||||
? path.join(
|
||||
skillsRoot,
|
||||
skillPackRoot,
|
||||
DICTATE_RECIPE_DIR,
|
||||
entry.id,
|
||||
"recipe.yaml",
|
||||
)
|
||||
: path.join(skillsRoot, skillPackRoot, "recipes", entry.id, "recipe.yaml");
|
||||
try {
|
||||
const raw = await readFile(fullPath, "utf8");
|
||||
return parseRecipeYaml(raw, entry);
|
||||
@@ -1801,10 +1821,18 @@ export async function resolveSelectedRecipeDetail(params: {
|
||||
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);
|
||||
for (const family of ["recipe", "dictate"] as const) {
|
||||
const catalog = await loadRecipeCatalog(
|
||||
params.skillPackRoot,
|
||||
skillsRoot,
|
||||
family,
|
||||
);
|
||||
const entry = findRecipeCatalogEntry(catalog, ref);
|
||||
if (entry) {
|
||||
return loadRecipeDetail(params.skillPackRoot, entry, skillsRoot);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function validateCreationFlow(
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
loadModuleCatalog,
|
||||
loadModulePrompt,
|
||||
loadRecipeCatalog,
|
||||
loadRecipeDetail,
|
||||
mergeCreationFlowPreservingAccepted,
|
||||
needsFlowExpansion,
|
||||
nextPendingStep,
|
||||
@@ -484,6 +485,7 @@ describe("creation-flow", () => {
|
||||
const recipes = await loadRecipeCatalog("dialogue/world-simulator");
|
||||
expect(recipes?.recipes.some((r) => r.name === "世界模拟器")).toBe(true);
|
||||
expect(recipes?.recipes.some((r) => r.name === "扩写助手")).toBe(true);
|
||||
expect(recipes?.recipes.some((r) => r.name === "文本生成器")).toBe(false);
|
||||
const block = formatRecipeCatalogForAgent(recipes!);
|
||||
expect(block).toContain("须由用户手动选择");
|
||||
expect(block).toContain("世界模拟器");
|
||||
@@ -506,6 +508,22 @@ describe("creation-flow", () => {
|
||||
expect(formatted).not.toContain("调味提示");
|
||||
});
|
||||
|
||||
it("loads dictate recipe catalog separately with Chinese ids", async () => {
|
||||
const dictate = await loadRecipeCatalog(
|
||||
"dialogue/world-simulator",
|
||||
undefined,
|
||||
"dictate",
|
||||
);
|
||||
expect(dictate?.recipes.some((r) => r.id === "文本生成器")).toBe(true);
|
||||
expect(dictate?.recipes.some((r) => r.name === "文本生成器")).toBe(true);
|
||||
expect(dictate?.recipes.some((r) => r.name === "世界模拟器")).toBe(false);
|
||||
const entry = dictate!.recipes.find((r) => r.id === "文本生成器")!;
|
||||
expect(entry.family).toBe("dictate");
|
||||
const detail = await loadRecipeDetail("dialogue/world-simulator", entry);
|
||||
expect(detail.core).toBeTruthy();
|
||||
expect(detail.seed).toBeNull();
|
||||
});
|
||||
|
||||
it("parses selected recipe ref", () => {
|
||||
expect(parseSelectedRecipeRef("world-simulator")).toBe("world-simulator");
|
||||
expect(parseSelectedRecipeRef('{"id":"expand-assistant","name":"扩写助手"}')).toBe(
|
||||
|
||||
@@ -57,6 +57,8 @@ export function displaySkillPackLabel(id) {
|
||||
const map = {
|
||||
"world-simulator": "世界模拟器",
|
||||
"expand-assistant": "扩写助手",
|
||||
文本生成器: "文本生成器",
|
||||
"text-generator": "文本生成器",
|
||||
};
|
||||
return map[id] ?? id;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user