完善配方驱动的创作编排

为可重复技能补充参数校验与展示,统一配方、编排器和执行单元术语。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
moran
2026-07-30 17:59:20 +08:00
parent e670a5129c
commit 6392af54b4
49 changed files with 1671 additions and 626 deletions

View File

@@ -27,12 +27,12 @@ const MSG_CLASS = {
const MSG_LABEL = {
user_input: "你",
agent_tool: "工具",
orchestrator_decision: "导演",
orchestrator_thinking: "导演 · 思考",
orchestrator_prompt: "导演",
orchestrator_assessment: "导演 · 内容评价",
worker_running: "Worker",
worker_output: "Worker",
orchestrator_decision: "编排器",
orchestrator_thinking: "编排器 · 思考",
orchestrator_prompt: "编排器",
orchestrator_assessment: "编排器 · 内容评价",
worker_running: "执行单元",
worker_output: "执行单元",
worker_questions: "提问",
error: "错误",
system_info: "系统",
@@ -924,7 +924,7 @@ function renderCreationFlowView(flowView) {
return `<div class="review-parse-error" role="alert">
<div class="review-parse-error-title">流程无法解析</div>
<p>${esc(flowView.parseError)}</p>
<p class="review-parse-error-hint">需要 JSONsteps 数组,每步含 name与可选 iddepends_on可含 status=open|closed。</p>
<p class="review-parse-error-hint">需要 JSONsteps 数组,每步含 name与可选 iddepends_on、可选 params;可含 status=open|closed。</p>
</div>`;
}
if (!flowView.steps?.length) return "";
@@ -957,9 +957,19 @@ function renderCreationFlowView(flowView) {
s.id && s.id !== s.name
? `${esc(s.name)} <span class="flow-id">(${esc(s.id)})</span>`
: esc(s.name);
const paramsText = formatFlowParams(s.params);
const paramsMissing =
s.paramsMissing?.length > 0
? `<span class="flow-params-missing">缺参:${esc(s.paramsMissing.join("、"))}</span>`
: "";
const paramsHtml = paramsText
? `<span class="flow-params">${esc(paramsText)}</span>`
: "";
return `<li class="flow-step">
<span class="flow-order">${esc(String(s.order))}</span>
<span class="flow-name">${nameLabel}${occ}</span>
${paramsHtml}
${paramsMissing}
<span class="flow-deps">依赖:${deps}</span>
</li>`;
})
@@ -973,6 +983,14 @@ function renderCreationFlowView(flowView) {
</section>`;
}
function formatFlowParams(params) {
if (!params || typeof params !== "object" || Array.isArray(params)) return "";
const parts = Object.entries(params)
.filter(([, v]) => v != null && String(v).trim() !== "")
.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
return parts.length ? parts.join(" · ") : "";
}
function renderReviewFeedCard(review) {
const flowHtml = review.creationFlowView
? renderCreationFlowView(review.creationFlowView)
@@ -1133,9 +1151,9 @@ export function renderMessageFeed(view, loading, handlers = {}) {
p.textContent = "游玩模式Agent 将按 Worker 集调度,推进世界与叙事。";
} else if (view.uiPrompt) {
const recipeLine = view.selectedRecipe?.name
? `\n\n已选导演${view.selectedRecipe.name}`
? `\n\n已选配方${view.selectedRecipe.name}`
: view.recipes?.length
? "\n\n请先在新建作品时选定导演"
? "\n\n请先在新建作品时选定配方"
: "";
p.textContent = `${view.uiPrompt}${recipeLine}`;
p.classList.add("empty-intake");

View File

@@ -28,7 +28,7 @@ const $ = (id) => document.getElementById(id);
const PHASE = { idle: "待命", running: "执行中", waiting_user: "等待你", done: "已完成", error: "出错" };
const REASON = {
skill_selection: "选择能力包",
skill_selection: "选择配方",
intake: "补充信息",
input: "等待输入",
approve_step: "确认执行",
@@ -748,7 +748,7 @@ function renderHeader(view, loading) {
$("work-title").textContent = view.bookTitle ?? "未命名作品";
const skill = displaySkillPackLabel(view.activeSkill) ||
view.selectedRecipe?.name ||
"导演";
"配方";
const phase = view.waitingReason
? REASON[view.waitingReason.kind] ?? view.phase
: PHASE[view.phase] ?? view.phase;
@@ -988,10 +988,10 @@ async function populateDirectorSelect() {
const directors = data.directors ?? [];
sel.innerHTML = "";
if (!directors.length) {
sel.innerHTML = `<option value="">暂无导演</option>`;
sel.innerHTML = `<option value="">暂无配方</option>`;
if (desc) {
desc.hidden = false;
desc.textContent = "尚未配置导演选项recipes/catalog.yaml。";
desc.textContent = "尚未配置配方选项recipes/catalog.yaml。";
}
return;
}
@@ -1027,7 +1027,7 @@ async function createBook() {
const title = $("input-book-title").value.trim() || "未命名作品";
const recipeId = $("select-director")?.value?.trim();
if (!recipeId) {
alert("请选择导演");
alert("请选择配方");
return;
}
$("btn-create-book").disabled = true;

View File

@@ -1,6 +1,6 @@
/**
* 与 docs/ui-glossary.md、src/server/display-labels.ts 保持同步。
* 用户侧拍摄术语:导演 / 剧本 / 演员 / 能力
* 用户侧口径:配方 / 编排器 / 技能 / 工作流计划 / 运行规格 / 执行单元
*/
const STAGE_LABELS = {
@@ -15,15 +15,15 @@ const STAGE_LABELS = {
const WORKER_LABELS = {
"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": "世界推演",
@@ -72,11 +72,11 @@ export function displayWorkerLabel(id) {
}
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)}`;
@@ -85,7 +85,7 @@ export function displayWorkerLabel(id) {
}
export function formatWorkerDisplayTitle(workerId, action = null) {
const base = displayWorkerLabel(workerId) || "演员";
const base = displayWorkerLabel(workerId) || "执行单元";
if (action === "output") return `${base} · 产出`;
if (action === "questions") return `${base} · 提问`;
if (action === "stub") return `${base} · 占位`;

View File

@@ -13,12 +13,12 @@ function formatExportTime(iso) {
const KIND_LABELS = {
user_input: "用户输入",
orchestrator_decision: "导演决策",
orchestrator_prompt: "导演询问",
agent_tool: "Agent Tool",
worker_running: "Worker 执行",
worker_output: "Worker 产出",
worker_stub: "Worker 占位",
orchestrator_decision: "编排器决策",
orchestrator_prompt: "编排器询问",
agent_tool: "工具",
worker_running: "执行单元 · 执行",
worker_output: "执行单元 · 产出",
worker_stub: "执行单元 · 占位",
system_info: "系统",
error: "错误",
};
@@ -39,7 +39,7 @@ export function sessionToMarkdown(view) {
if (view.skillCatalog?.length) {
const stageLabel = view.lifecycleStage === "play" ? "游玩" : "创作";
lines.push(`## ${stageLabel}清单`);
lines.push(`## ${stageLabel}能清单`);
lines.push("");
for (const skill of view.skillCatalog) {
const mark =

View File

@@ -33,7 +33,7 @@
</div>
</div>
<div class="main-head-right">
<a class="link" href="/modules.html"></a>
<a class="link" href="/modules.html"></a>
<a class="link" href="/stats.html">Token</a>
<a class="link" href="/settings.html">设置</a>
<button type="button" class="btn-sm" id="btn-save-instance" hidden>保存定稿</button>
@@ -88,9 +88,9 @@
<dialog id="dialog-new-book" class="dialog">
<form method="dialog" id="form-new-book">
<h2>新建作品</h2>
<p class="dialog-desc">选择导演,再起名。导演是本局方法起点,之后按对话谈成剧本(可调味)。</p>
<p class="dialog-desc">选择配方,再起名。配方是本局方法起点,之后按对话谈成工作流计划与运行规格(可调味)。</p>
<label class="field">
导演
配方
<select id="select-director" required>
<option value="">加载中…</option>
</select>

View File

@@ -1,4 +1,4 @@
/* 能池浏览页 — 叠在 settings.css 壳上 */
/* 能池浏览页 — 叠在 settings.css 壳上 */
.mod-shell {
max-width: 1280px;

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>池 · Writing Agent</title>
<title>能池 · Writing Agent</title>
<link rel="stylesheet" href="/settings.css" />
<link rel="stylesheet" href="/modules.css" />
</head>
@@ -12,15 +12,15 @@
<a class="st-brand" href="/">Writing Agent</a>
<nav class="st-nav" aria-label="主导航">
<a href="/">对话</a>
<a href="/modules.html" aria-current="page"></a>
<a href="/modules.html" aria-current="page"></a>
<a href="/stats.html">Token</a>
<a href="/settings.html">设置</a>
</nav>
</header>
<div class="st-shell mod-shell">
<aside class="st-rail mod-rail" aria-label="能列表">
<p class="st-rail-label">备选能</p>
<aside class="st-rail mod-rail" aria-label="能列表">
<p class="st-rail-label">备选</p>
<p class="mod-pack" id="pack-label">加载中…</p>
<div class="mod-filter">
<input
@@ -38,14 +38,14 @@
</div>
<nav class="mod-list" id="mod-list"></nav>
<p class="st-rail-note">
导演编排剧本时从这里选型;本页只读浏览方法正文。
编排器编排工作流计划时从这里选型;本页只读浏览方法正文。
</p>
</aside>
<main class="st-main mod-main">
<header class="st-main-head">
<div>
<h1 id="panel-title"></h1>
<h1 id="panel-title">能池</h1>
<p class="st-sub" id="panel-subtitle">选择左侧一项查看方法块</p>
</div>
<div class="st-main-actions">
@@ -57,7 +57,7 @@
<p class="mod-error" id="mod-error" hidden></p>
<div class="st-body mod-body" id="mod-detail">
<p class="mod-empty">从左侧选择一项能</p>
<p class="mod-empty">从左侧选择一项</p>
</div>
</main>
</div>

View File

@@ -193,7 +193,7 @@ async function boot() {
const data = await res.json();
modulesCache = data.modules || [];
document.getElementById("pack-label").textContent =
`导演包 · ${data.skillPackId || "—"} · ${modulesCache.length}`;
`配方包 · ${data.skillPackId || "—"} · ${modulesCache.length}`;
renderList();
const fromHash = decodeURIComponent((location.hash || "").replace(/^#/, ""));

View File

@@ -11,7 +11,7 @@
<a class="st-brand" href="/">Writing Agent</a>
<nav class="st-nav" aria-label="主导航">
<a href="/">对话</a>
<a href="/modules.html"></a>
<a href="/modules.html"></a>
<a href="/stats.html">Token</a>
<a href="/settings.html" aria-current="page">设置</a>
</nav>

View File

@@ -67,7 +67,7 @@
<body>
<div class="stats-page">
<header>
<p><a href="/">← 对话</a> · <a href="/modules.html"></a> · <a href="/settings.html">设置</a></p>
<p><a href="/">← 对话</a> · <a href="/modules.html"></a> · <a href="/settings.html">设置</a></p>
<h1>Token 统计</h1>
<p class="stats-muted">按时间戳筛选调用记录(存于本地 stats/*.jsonl</p>
</header>

View File

@@ -569,10 +569,16 @@ body[data-lifecycle="play"] .panel-agent { background: #f7fef9; }
.flow-name {
font-weight: 600;
}
.flow-params,
.flow-params-missing,
.flow-deps {
grid-column: 2;
font-size: 12px;
color: var(--muted, #666);
}
.flow-params-missing {
color: var(--danger, #b42318);
}
.board-panel {
flex: 1;