引入游玩回合与开场收口,抽出可替换 agent 司机并支持运行中止;完善预设洞位、世界书拼装与试跑,并打磨会话运行时与 Web 交互。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -163,12 +163,22 @@ export function projectFragmentContent(
|
||||
const frag = parseContextFragment(raw);
|
||||
const mode = projection.trim().toLowerCase();
|
||||
if (!frag) {
|
||||
if (mode === "summary" && raw.length > 800) return `${raw.slice(0, 800)}…`;
|
||||
if ((mode === "summary" || mode === "brief" || mode === "index") && raw.length > 800) {
|
||||
return `${raw.slice(0, 800)}…`;
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
if (mode === "summary" || mode === "brief") {
|
||||
return frag.brief || raw;
|
||||
}
|
||||
if (mode === "index") {
|
||||
const lines: string[] = [];
|
||||
if (frag.技能) lines.push(`技能:${frag.技能}`);
|
||||
if (frag.brief) lines.push(frag.brief);
|
||||
if (frag.mount.length) lines.push(`挂载:${frag.mount.join("、")}`);
|
||||
if (frag.稳变) lines.push(`稳变:${frag.稳变}`);
|
||||
return lines.join("\n") || frag.brief || raw;
|
||||
}
|
||||
if (mode === "fields") {
|
||||
if (frag.正文 && typeof frag.正文 === "object" && !Array.isArray(frag.正文)) {
|
||||
const keys = Object.keys(frag.正文 as object);
|
||||
@@ -237,9 +247,11 @@ export function contextFragmentToView(raw: unknown): ContextFragmentView {
|
||||
const probe = rawDoc?.追问;
|
||||
if (probe && typeof probe === "object" && !Array.isArray(probe)) {
|
||||
const row = probe as Record<string, unknown>;
|
||||
const lines: string[] = [];
|
||||
if (typeof row.导语 === "string" && row.导语.trim()) lines.push(row.导语.trim());
|
||||
const qs = Array.isArray(row.题目) ? row.题目 : [];
|
||||
const lines: string[] = [];
|
||||
if (qs.length && typeof row.导语 === "string" && row.导语.trim()) {
|
||||
lines.push(row.导语.trim());
|
||||
}
|
||||
qs.forEach((q, i) => {
|
||||
if (!q || typeof q !== "object") return;
|
||||
const r = q as Record<string, unknown>;
|
||||
@@ -282,7 +294,9 @@ export function extractFragmentAskSidecar(raw: unknown): {
|
||||
|
||||
const looksFragment =
|
||||
row.schema === CONTEXT_FRAGMENT_SCHEMA ||
|
||||
(typeof row.brief === "string" && ("正文" in row || "body" in row));
|
||||
typeof row.技能 === "string" ||
|
||||
(typeof row.brief === "string" &&
|
||||
("正文" in row || "body" in row || "追问" in row));
|
||||
if (!looksFragment) return { questions: [], assessment: "" };
|
||||
|
||||
const assessmentParts: string[] = [];
|
||||
@@ -358,7 +372,12 @@ export function extractFragmentAskSidecar(raw: unknown): {
|
||||
}
|
||||
|
||||
const questions = normalizeQuestions(rawQs);
|
||||
const assessment = [lead, ...assessmentParts].filter(Boolean).join("\n");
|
||||
const assessment = [
|
||||
questions.length ? lead : "",
|
||||
...assessmentParts,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
return { questions, assessment };
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,14 @@
|
||||
import type { ContextSegmentDef } from "./context-segments.js";
|
||||
import { extractJsonObjectText } from "./worker-set-parse.js";
|
||||
import {
|
||||
DEFAULT_ON_DEMAND_REFS,
|
||||
DEFAULT_PLAY_SLOT_REFS,
|
||||
defaultPlaySlots,
|
||||
parsePlaySlots,
|
||||
playSlotIdForRef,
|
||||
PLAY_SLOT_META,
|
||||
PLAY_SLOTS_WITHOUT_DIALOGUE_HISTORY,
|
||||
resolvePlayAgentId,
|
||||
type PlaySlotsConfig,
|
||||
} from "./play-slots.js";
|
||||
import {
|
||||
@@ -87,20 +92,53 @@ function parseInsert(raw: unknown): ContextOrderInsert | undefined {
|
||||
function parseSlot(raw: unknown): ContextOrderSlot | undefined {
|
||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
|
||||
const row = raw as Record<string, unknown>;
|
||||
const ref = asString(row.ref) ?? asString(row.id);
|
||||
if (!ref) return undefined;
|
||||
const rawRef = asString(row.ref) ?? asString(row.id);
|
||||
if (!rawRef) return undefined;
|
||||
const slotId = resolvePlayAgentId(rawRef);
|
||||
const ref = slotId
|
||||
? slotId === "chance"
|
||||
? DEFAULT_ON_DEMAND_REFS.chance
|
||||
: DEFAULT_PLAY_SLOT_REFS[slotId]
|
||||
: rawRef;
|
||||
const insertsRaw = Array.isArray(row.inserts) ? row.inserts : [];
|
||||
const inserts = insertsRaw
|
||||
.map(parseInsert)
|
||||
.filter((x): x is ContextOrderInsert => Boolean(x))
|
||||
.sort((a, b) => a.order - b.order);
|
||||
const label =
|
||||
asString(row.label) ??
|
||||
asString(row.name) ??
|
||||
(slotId && slotId !== "chance" ? PLAY_SLOT_META[slotId].label : undefined);
|
||||
return {
|
||||
ref,
|
||||
label: asString(row.label) ?? asString(row.name),
|
||||
label,
|
||||
inserts,
|
||||
};
|
||||
}
|
||||
|
||||
function agentEnabledFlag(row: Record<string, unknown>): boolean {
|
||||
if (typeof row.enabled === "boolean") return row.enabled;
|
||||
if (row.enabled === "false" || row.enabled === 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function playSlotsFromAgents(
|
||||
rows: Record<string, unknown>[],
|
||||
): PlaySlotsConfig {
|
||||
const base = defaultPlaySlots();
|
||||
for (const row of rows) {
|
||||
const rawId = asString(row.id) ?? asString(row.ref) ?? asString(row.label);
|
||||
if (!rawId) continue;
|
||||
const slotId = resolvePlayAgentId(rawId);
|
||||
if (!slotId) continue;
|
||||
const on = agentEnabledFlag(row);
|
||||
if (slotId === "chance") base.chance = on;
|
||||
else if (slotId === "auditor") base.auditor = on;
|
||||
else base[slotId] = on;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export function parseContextOrder(raw: unknown): ContextOrderDoc | undefined {
|
||||
let row: Record<string, unknown> | null = null;
|
||||
if (typeof raw === "string") {
|
||||
@@ -118,16 +156,32 @@ export function parseContextOrder(raw: unknown): ContextOrderDoc | undefined {
|
||||
}
|
||||
if (!row) return undefined;
|
||||
|
||||
const slotsRaw = Array.isArray(row.slots) ? row.slots : [];
|
||||
const slots = slotsRaw
|
||||
const agentsRaw = Array.isArray(row.agents) ? row.agents : [];
|
||||
const agentRows = agentsRaw.filter(
|
||||
(a): a is Record<string, unknown> =>
|
||||
Boolean(a) && typeof a === "object" && !Array.isArray(a),
|
||||
);
|
||||
const fromAgents = agentRows
|
||||
.filter(agentEnabledFlag)
|
||||
.map(parseSlot)
|
||||
.filter((x): x is ContextOrderSlot => Boolean(x));
|
||||
|
||||
const slotsRaw = Array.isArray(row.slots) ? row.slots : [];
|
||||
const fromSlots = slotsRaw
|
||||
.map(parseSlot)
|
||||
.filter((x): x is ContextOrderSlot => Boolean(x));
|
||||
|
||||
const slots = fromAgents.length ? fromAgents : fromSlots;
|
||||
if (!slots.length && row.schema !== CONTEXT_ORDER_SCHEMA) return undefined;
|
||||
|
||||
const play_slots = fromAgents.length
|
||||
? playSlotsFromAgents(agentRows)
|
||||
: parsePlaySlots(row.play_slots ?? row.playSlots);
|
||||
|
||||
return {
|
||||
schema: CONTEXT_ORDER_SCHEMA,
|
||||
brief: asString(row.brief),
|
||||
play_slots: parsePlaySlots(row.play_slots ?? row.playSlots),
|
||||
play_slots,
|
||||
slots,
|
||||
};
|
||||
}
|
||||
|
||||
+159
-39
@@ -89,17 +89,40 @@ function readTagContent(
|
||||
return items[items.length - 1]?.content?.trim() ?? "";
|
||||
}
|
||||
|
||||
/** 单段投影正文(不含 label)。供世界书 marker 填洞复用。 */
|
||||
export function renderContextSegmentBody(
|
||||
segment: ContextSegmentDef,
|
||||
inputs: Record<string, string>,
|
||||
blackboard: Blackboard,
|
||||
inputMerge: BlackboardInputMerge,
|
||||
skipExact?: Set<string>,
|
||||
skipTags?: Set<string>,
|
||||
): string {
|
||||
return formatSegmentBody(
|
||||
segment,
|
||||
inputs,
|
||||
blackboard,
|
||||
inputMerge,
|
||||
skipExact,
|
||||
skipTags,
|
||||
);
|
||||
}
|
||||
|
||||
function formatSegmentBody(
|
||||
segment: ContextSegmentDef,
|
||||
inputs: Record<string, string>,
|
||||
blackboard: Blackboard,
|
||||
inputMerge: BlackboardInputMerge,
|
||||
skipExact?: Set<string>,
|
||||
skipTags?: Set<string>,
|
||||
): string {
|
||||
if (segment.inline?.trim()) {
|
||||
return applyProjection(segment.inline.trim(), segment.projection);
|
||||
}
|
||||
const parts: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const tag of segment.tags) {
|
||||
if (skipTags?.has(tag)) continue;
|
||||
let content: string;
|
||||
if (isDialogueHistoryRef(tag)) {
|
||||
content = resolveDialogueHistory(inputs, blackboard, inputMerge);
|
||||
@@ -115,8 +138,22 @@ function formatSegmentBody(
|
||||
content = applyPolicy(content, segment.policy);
|
||||
content = applyProjection(content, segment.projection);
|
||||
}
|
||||
if (!content.trim()) continue;
|
||||
parts.push(content.trim());
|
||||
const trimmed = content.trim();
|
||||
if (!trimmed) continue;
|
||||
if (skipExact?.has(trimmed)) continue;
|
||||
if (seen.has(trimmed)) continue;
|
||||
if (
|
||||
parts.some(
|
||||
(p) =>
|
||||
p === trimmed ||
|
||||
p.includes(`\n\n${trimmed}`) ||
|
||||
p.startsWith(`${trimmed}\n\n`),
|
||||
)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
seen.add(trimmed);
|
||||
parts.push(trimmed);
|
||||
}
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
@@ -141,6 +178,61 @@ function applyProjection(content: string, projection?: string): string {
|
||||
return projectFragmentContent(content, projection);
|
||||
}
|
||||
|
||||
const USER_REQUIREMENT_TAGS = new Set([
|
||||
"用户.需求",
|
||||
"book.brief",
|
||||
"用户.最新输入",
|
||||
"用户.worker答复",
|
||||
"用户.修订说明",
|
||||
]);
|
||||
|
||||
const CREATION_REVIEW_WORKERS = new Set([
|
||||
"design-step",
|
||||
"design-flow",
|
||||
"opening-generator",
|
||||
]);
|
||||
|
||||
function alreadyCovered(parts: string[], next: string): boolean {
|
||||
const trimmed = next.trim();
|
||||
if (!trimmed) return true;
|
||||
return parts.some(
|
||||
(p) =>
|
||||
p === trimmed ||
|
||||
p.includes(`\n\n${trimmed}`) ||
|
||||
p.startsWith(`${trimmed}\n\n`),
|
||||
);
|
||||
}
|
||||
|
||||
function pushUnique(parts: string[], next: string): void {
|
||||
const trimmed = next.trim();
|
||||
if (!trimmed || alreadyCovered(parts, trimmed)) return;
|
||||
parts.push(trimmed);
|
||||
}
|
||||
|
||||
/** 创作步:用户要求按时间排;冲突只看较新,旧条未点名推翻则仍有效。 */
|
||||
function collectUserRequirementBlock(params: {
|
||||
inputs: Record<string, string>;
|
||||
blackboard: Blackboard;
|
||||
inputMerge: BlackboardInputMerge;
|
||||
}): string {
|
||||
const { inputs, blackboard, inputMerge } = params;
|
||||
const ordered = [
|
||||
readTagContent("用户.需求", inputs, blackboard, inputMerge),
|
||||
readTagContent("book.brief", inputs, blackboard, inputMerge),
|
||||
readTagContent("用户.worker答复", inputs, blackboard, inputMerge),
|
||||
readTagContent("用户.最新输入", inputs, blackboard, inputMerge),
|
||||
readTagContent("用户.修订说明", inputs, blackboard, inputMerge),
|
||||
];
|
||||
const parts: string[] = [];
|
||||
for (const item of ordered) pushUnique(parts, item);
|
||||
if (!parts.length) return "";
|
||||
return [
|
||||
"## 【用户要求】按先后排列。冲突时以较新的为准;未被较新要求改写的旧条仍有效。",
|
||||
"",
|
||||
...parts,
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 contextSegments 拼装 user 侧上下文。
|
||||
* 无 segments 时回退为 JSON inputs(兼容旧 skill)。
|
||||
@@ -178,10 +270,49 @@ export function assembleWorkerContext(params: {
|
||||
);
|
||||
}
|
||||
|
||||
const revisionNote = readTagContent(
|
||||
"用户.修订说明",
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
).trim();
|
||||
const inheritExisting = segments.some((s) => s.id === "inherit-existing");
|
||||
const splitUserReq = CREATION_REVIEW_WORKERS.has(params.workerId);
|
||||
|
||||
const draftBlocks: string[] = [];
|
||||
for (const tag of params.outputTags) {
|
||||
const alreadyInSegments = segments.some((s) => s.tags.includes(tag));
|
||||
if (alreadyInSegments) continue;
|
||||
const draft = readTagContent(
|
||||
tag,
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
).trim();
|
||||
if (!draft) continue;
|
||||
draftBlocks.push(`### \`${tag}\`\n\n${draft}`);
|
||||
}
|
||||
|
||||
// 只有本步真有底稿(或回头修改)才算修订;上一步残留的修订说明仍作为用户要求保留
|
||||
const inheritMode = inheritExisting || (Boolean(revisionNote) && draftBlocks.length > 0);
|
||||
|
||||
const skipTags = new Set<string>();
|
||||
if (splitUserReq) {
|
||||
for (const tag of USER_REQUIREMENT_TAGS) skipTags.add(tag);
|
||||
}
|
||||
|
||||
const blocks: string[] = [];
|
||||
|
||||
const render = (seg: ContextSegmentDef) => {
|
||||
const body = formatSegmentBody(seg, params.inputs, params.blackboard, inputMerge);
|
||||
if (inheritMode && seg.id === "module-opening") return;
|
||||
const body = formatSegmentBody(
|
||||
seg,
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
undefined,
|
||||
skipTags,
|
||||
);
|
||||
if (!body) return;
|
||||
if (seg.label) {
|
||||
blocks.push(`${seg.label}\n\n${body}`);
|
||||
@@ -193,6 +324,15 @@ export function assembleWorkerContext(params: {
|
||||
// 严格按 segments 数组顺序(投影排序表顺序);不再按 static/dynamic 重排
|
||||
for (const seg of segments) render(seg);
|
||||
|
||||
if (splitUserReq) {
|
||||
const reqBlock = collectUserRequirementBlock({
|
||||
inputs: params.inputs,
|
||||
blackboard: params.blackboard,
|
||||
inputMerge,
|
||||
});
|
||||
if (reqBlock) blocks.push(reqBlock);
|
||||
}
|
||||
|
||||
// 定稿摘要:若未在 segments 中声明,仍附在末尾
|
||||
const brief = params.inputs[CONTEXT_BRIEF_TAG]?.trim();
|
||||
const briefInSegments = segments.some((s) => s.tags.includes(CONTEXT_BRIEF_TAG));
|
||||
@@ -200,39 +340,17 @@ export function assembleWorkerContext(params: {
|
||||
blocks.push(`## 上下文.定稿摘要\n\n${brief}`);
|
||||
}
|
||||
|
||||
// 修订态:把本步 outputTags 上已有正文钉成「待改底稿」,禁止无故推倒重写
|
||||
const revisionNote = readTagContent(
|
||||
"用户.修订说明",
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
).trim();
|
||||
const draftBlocks: string[] = [];
|
||||
if (revisionNote) {
|
||||
for (const tag of params.outputTags) {
|
||||
const alreadyInSegments = segments.some((s) => s.tags.includes(tag));
|
||||
if (alreadyInSegments) continue;
|
||||
const draft = readTagContent(
|
||||
tag,
|
||||
params.inputs,
|
||||
params.blackboard,
|
||||
inputMerge,
|
||||
).trim();
|
||||
if (!draft) continue;
|
||||
draftBlocks.push(`### \`${tag}\`\n\n${draft}`);
|
||||
}
|
||||
if (draftBlocks.length) {
|
||||
blocks.push(
|
||||
[
|
||||
"## 【待改底稿】",
|
||||
"",
|
||||
"下列为**当前已有产物**。按「用户.修订说明」以及「用户.worker答复」中的追问作答(选项与补充)在其上修改,并写回同名 outputTags。",
|
||||
"节点方法(task / principles / probe / output)仍须遵守。保留未要求改动的结构与结论;禁止无故整份重写。",
|
||||
"",
|
||||
...draftBlocks,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
if (inheritMode && draftBlocks.length) {
|
||||
blocks.push(
|
||||
[
|
||||
"## 【待改底稿】",
|
||||
"",
|
||||
"下列为**当前已有产物**(只保留这一份最新稿)。在其上落实【用户要求】里较新的条目;与旧要求冲突时以较新的为准,未点名改写的旧条仍须保留在产物里。",
|
||||
"节点方法(task / principles / probe / output)仍须遵守。禁止无视较新要求原样交回。",
|
||||
"",
|
||||
...draftBlocks,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
const taskLines = [
|
||||
@@ -243,10 +361,12 @@ export function assembleWorkerContext(params: {
|
||||
`- outputTags: ${params.outputTags.map((t) => `\`${t}\``).join("、") || "(无)"}`,
|
||||
"",
|
||||
];
|
||||
if (revisionNote) {
|
||||
if (inheritMode) {
|
||||
taskLines.push(
|
||||
"**修订模式**:接着上方【待改底稿】(若有)与依赖产物,落实「用户.修订说明」以及「用户.worker答复」里的追问作答(选项与补充意见)。",
|
||||
"不要从零另起一份;不要扩大修改面。标为「只读 / 已定稿」的分区仍不可改。节点方法仍有效。",
|
||||
inheritExisting
|
||||
? "**回头修改**:接着上方【既有产物 · 继承修改】(以及【待改底稿】若有)继续改,写回同一产物。"
|
||||
: "**修订模式**:以【待改底稿】为底,落实【用户要求】;冲突时以较新的为准。",
|
||||
"不要从零另起一份。标为「只读 / 已定稿」的分区仍不可改。",
|
||||
);
|
||||
} else {
|
||||
taskLines.push(
|
||||
|
||||
+316
-63
@@ -53,9 +53,41 @@ const DEFAULT_SKILLS_ROOT = path.resolve(
|
||||
"../../skills",
|
||||
);
|
||||
|
||||
/** 步骤调用参数(编排期钉死;执行期只读) */
|
||||
/** 步骤调用参数。普通节点:编排期钉死、执行只读。〔先验产物〕:编排器规划的产物内容(可空),执行时注入为【规划产物】。 */
|
||||
export type CreationFlowStepParams = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* 节点特性。缺省 = 普通执行步。后续会加更多 kind。
|
||||
* prior-artifact = 先验产物:须先定「写什么」(生成规则、具体实例);编排器可提前规划,步内再钉/修订。
|
||||
*/
|
||||
export const MODULE_NODE_KINDS = ["prior-artifact"] as const;
|
||||
export type ModuleNodeKind = (typeof MODULE_NODE_KINDS)[number];
|
||||
|
||||
export const MODULE_NODE_KIND_FLAGS: Record<ModuleNodeKind, string> = {
|
||||
"prior-artifact": "〔先验产物〕",
|
||||
};
|
||||
|
||||
export function parseModuleNodeKind(raw: unknown): ModuleNodeKind | undefined {
|
||||
if (typeof raw !== "string") return undefined;
|
||||
const key = raw.trim();
|
||||
return (MODULE_NODE_KINDS as readonly string[]).includes(key)
|
||||
? (key as ModuleNodeKind)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function isPriorArtifactModule(
|
||||
module: { kind?: ModuleNodeKind } | null | undefined,
|
||||
): boolean {
|
||||
return module?.kind === "prior-artifact";
|
||||
}
|
||||
|
||||
/**
|
||||
* 本步怎么跑:
|
||||
* - fresh(缺省)= 从零产出。〔可反复〕再编入新 id 必须用这个——彻底新建,不改旧条。
|
||||
* - revise = 回头修改:继承 `revises` 所指那步的既有产物继续改。
|
||||
*/
|
||||
export type CreationFlowStepMode = "fresh" | "revise";
|
||||
|
||||
export type CreationFlowStep = {
|
||||
/**
|
||||
* 本局步骤唯一 id(验收与 depends_on 用这个)。
|
||||
@@ -67,10 +99,15 @@ export type CreationFlowStep = {
|
||||
/** 依赖的其它步骤 id(旧稿若 name 唯一也可写 name) */
|
||||
depends_on: string[];
|
||||
/**
|
||||
* 本步调用参数。有「编排参数」声明的能力必须在进 design-step 前钉齐必填项。
|
||||
* 例:生成规则 → target;具体实例 → rule_id。
|
||||
* 本步调用参数。
|
||||
* 普通节点:有必填声明时须在进执行前钉齐。
|
||||
* 〔先验产物〕:编排器规划的产物内容(可空);执行注入【规划产物】,步内可修订。
|
||||
*/
|
||||
params?: CreationFlowStepParams;
|
||||
/** 缺省 fresh。revise = 继承旧产物修改,不是再生成一条。 */
|
||||
mode?: CreationFlowStepMode;
|
||||
/** mode=revise 时:被改的既有步骤 id(须更前、同 name) */
|
||||
revises?: string;
|
||||
};
|
||||
|
||||
export type CreationFlowStatus = "open" | "closed";
|
||||
@@ -111,14 +148,29 @@ export type ModuleCatalogEntry = {
|
||||
* 程序不硬拦;给编排与校验提示。
|
||||
*/
|
||||
repeatable?: boolean;
|
||||
/**
|
||||
* 可选:创作终节点。选定后程序收口并保存,其后不要再追加步骤。
|
||||
*/
|
||||
closer?: boolean;
|
||||
/**
|
||||
* 可选:程序步。不抛默认问题、不经「同意并开始」,提案后直接执行;
|
||||
* 产物仍走验收。适合投影排序这类只排序、几乎不问用户的收成步。
|
||||
*/
|
||||
auto?: boolean;
|
||||
/**
|
||||
* 可选:节点特性。缺省为普通执行步。
|
||||
* prior-artifact = 先验产物,须先定写什么;params 是规划产物而非拦执行的必填项。
|
||||
*/
|
||||
kind?: ModuleNodeKind;
|
||||
/**
|
||||
* 可选:默认问题(开场白)。优先用 prompt.md 的 ```opening 块;
|
||||
* catalog 写了则作覆盖。程序发出,不经 LLM。
|
||||
*/
|
||||
opening?: string;
|
||||
/**
|
||||
* 可选:编排期步骤参数声明。
|
||||
* 有 required 项时,steps[].params 必须在进执行前钉齐;勿把选型推迟到 design-step。
|
||||
* 可选:步骤参数声明。
|
||||
* 普通节点:required 须在进执行前钉齐。
|
||||
* 〔先验产物〕:规划产物字段;可提前写入 params,空则步内钉,不拦确认开干。
|
||||
*/
|
||||
params?: ModuleParamSpec[];
|
||||
/** 来自 prompt.md ```meta:何时该选用(编排选型) */
|
||||
@@ -191,15 +243,21 @@ export type CreationFlowUserView = {
|
||||
id: string;
|
||||
name: string;
|
||||
depends_on: string[];
|
||||
/** 同能力第几次(>1 时 UI 可标「再来」) */
|
||||
/** 同能力第几次(>1 时 UI 可标「再来」;回头修改不计入「第 N 次新建」) */
|
||||
occurrence?: number;
|
||||
/** fresh=从零新建;revise=回头修改 */
|
||||
mode?: CreationFlowStepMode;
|
||||
/** 回头修改时:被改的既有步骤 id */
|
||||
revises?: string;
|
||||
/** 目录里的短声明(有则展示) */
|
||||
declaration?: string;
|
||||
repeatable?: boolean;
|
||||
/** 本步调用参数(编排期钉死) */
|
||||
/** 本步调用参数(普通节点编排钉死;先验产物=规划内容) */
|
||||
params?: CreationFlowStepParams;
|
||||
/** 参数缺必填项时的提示(给人看) */
|
||||
/** 参数缺必填项时的提示(给人看);先验产物不拦执行,不出现此项 */
|
||||
paramsMissing?: string[];
|
||||
/** 节点特性(如先验产物) */
|
||||
kind?: ModuleNodeKind;
|
||||
/** 已执行 / 将要执行 / 未执行 */
|
||||
runState: FlowStepRunState;
|
||||
}>;
|
||||
@@ -241,11 +299,12 @@ export function normalizeStepParams(
|
||||
return Object.keys(out).length > 0 ? out : undefined;
|
||||
}
|
||||
|
||||
/** 目录声明的必填参数中,本步仍缺失的 key(按声明顺序) */
|
||||
/** 目录声明的必填参数中,本步仍缺失的 key(按声明顺序)。〔先验产物〕不拦执行,恒为 []。 */
|
||||
export function missingRequiredStepParams(
|
||||
step: Pick<CreationFlowStep, "params">,
|
||||
module: ModuleCatalogEntry | null | undefined,
|
||||
): string[] {
|
||||
if (isPriorArtifactModule(module)) return [];
|
||||
const specs = module?.params ?? [];
|
||||
if (specs.length === 0) return [];
|
||||
const params = step.params ?? {};
|
||||
@@ -278,14 +337,120 @@ export function formatStepParamsForPrompt(
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/** 为缺 id 的步骤补齐唯一 id;同 name 多次 → name#2、name#3… */
|
||||
const PRIOR_ARTIFACT_CONTEXT_TITLE =
|
||||
"## 【规划产物】编排器预先为本步规划的产物内容(先验;缺则步内钉;步内可修订)";
|
||||
|
||||
/** 〔先验产物〕注入块:规划可空,身份写在片段自身。 */
|
||||
export function formatPriorArtifactContext(
|
||||
params: CreationFlowStepParams | null | undefined,
|
||||
): string {
|
||||
const body = formatStepParamsForPrompt(params);
|
||||
const note =
|
||||
!params || Object.keys(params).length === 0
|
||||
? "编排器尚未规划本步具体写什么。先在本步与用户钉「写什么」,再填产物。"
|
||||
: "以上为先验规划,不是锁死合同。用户改对象或范围时,以本步最新认定为准,并重钉对应英文 id。";
|
||||
return `${PRIOR_ARTIFACT_CONTEXT_TITLE}\n\n${body}\n\n${note}`;
|
||||
}
|
||||
|
||||
const REVISE_MODE_ALIASES = new Set([
|
||||
"revise",
|
||||
"amend",
|
||||
"edit",
|
||||
"修改",
|
||||
"回头修改",
|
||||
"继承修改",
|
||||
]);
|
||||
const FRESH_MODE_ALIASES = new Set([
|
||||
"fresh",
|
||||
"new",
|
||||
"create",
|
||||
"新建",
|
||||
"再来",
|
||||
"再来一次",
|
||||
]);
|
||||
|
||||
/** 解析 DAG 步骤的 mode;无法识别则视为缺省(fresh) */
|
||||
export function parseCreationFlowStepMode(raw: unknown): CreationFlowStepMode | undefined {
|
||||
if (typeof raw !== "string") return undefined;
|
||||
const key = raw.trim().toLowerCase();
|
||||
if (!key) return undefined;
|
||||
if (REVISE_MODE_ALIASES.has(key) || REVISE_MODE_ALIASES.has(raw.trim())) {
|
||||
return "revise";
|
||||
}
|
||||
if (FRESH_MODE_ALIASES.has(key) || FRESH_MODE_ALIASES.has(raw.trim())) {
|
||||
return "fresh";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isReviseStep(
|
||||
step: Pick<CreationFlowStep, "mode"> | null | undefined,
|
||||
): boolean {
|
||||
return step?.mode === "revise";
|
||||
}
|
||||
|
||||
function parseStepRevisesRef(raw: Record<string, unknown>): string | undefined {
|
||||
for (const key of ["revises", "revise_of", "revises_id"] as const) {
|
||||
const v = raw[key];
|
||||
if (typeof v === "string" && v.trim()) return v.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
type DraftCreationFlowStep = {
|
||||
id?: string;
|
||||
name: string;
|
||||
depends_on: string[];
|
||||
params?: CreationFlowStepParams;
|
||||
mode?: CreationFlowStepMode;
|
||||
revises?: string;
|
||||
};
|
||||
|
||||
function withStepModeFields(
|
||||
step: DraftCreationFlowStep,
|
||||
): Pick<CreationFlowStep, "mode" | "revises"> {
|
||||
return {
|
||||
...(step.mode === "revise" ? { mode: "revise" as const } : {}),
|
||||
...(step.revises ? { revises: step.revises } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 回头修改步:补 revises(最近一条同名更前步),并继承其 params(本步已写的优先)。
|
||||
*/
|
||||
export function normalizeReviseSteps(
|
||||
steps: CreationFlowStep[],
|
||||
): CreationFlowStep[] {
|
||||
return steps.map((step, i) => {
|
||||
if (step.mode !== "revise") return step;
|
||||
let revises = step.revises?.trim() || "";
|
||||
if (!revises) {
|
||||
for (let j = i - 1; j >= 0; j--) {
|
||||
if (steps[j]?.name === step.name) {
|
||||
revises = steps[j]!.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
const origin = revises
|
||||
? steps.find((s) => s.id === revises || s.name === revises)
|
||||
: undefined;
|
||||
const inherited = origin?.params;
|
||||
const params =
|
||||
inherited && Object.keys(inherited).length
|
||||
? { ...inherited, ...(step.params ?? {}) }
|
||||
: step.params;
|
||||
return {
|
||||
...step,
|
||||
...(revises ? { revises } : {}),
|
||||
...(params && Object.keys(params).length ? { params } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** 为缺 id 的步骤补齐唯一 id;同 name 多次 → name#n;回头修改 → name·改 */
|
||||
export function ensureCreationFlowStepIds(
|
||||
steps: Array<{
|
||||
id?: string;
|
||||
name: string;
|
||||
depends_on: string[];
|
||||
params?: CreationFlowStepParams;
|
||||
}>,
|
||||
steps: Array<DraftCreationFlowStep>,
|
||||
): CreationFlowStep[] {
|
||||
const used = new Set<string>();
|
||||
const nameCount = new Map<string, number>();
|
||||
@@ -295,10 +460,20 @@ export function ensureCreationFlowStepIds(
|
||||
const name = raw.name.trim();
|
||||
const n = (nameCount.get(name) ?? 0) + 1;
|
||||
nameCount.set(name, n);
|
||||
const revise = raw.mode === "revise";
|
||||
|
||||
let id = typeof raw.id === "string" ? raw.id.trim() : "";
|
||||
if (!id) {
|
||||
id = n === 1 ? name : `${name}#${n}`;
|
||||
if (revise) {
|
||||
id = n === 1 ? `${name}·改` : `${name}·改`;
|
||||
if (used.has(id)) {
|
||||
let i = 2;
|
||||
while (used.has(`${name}·改#${i}`)) i++;
|
||||
id = `${name}·改#${i}`;
|
||||
}
|
||||
} else {
|
||||
id = n === 1 ? name : `${name}#${n}`;
|
||||
}
|
||||
}
|
||||
if (used.has(id)) {
|
||||
let i = 2;
|
||||
@@ -311,9 +486,10 @@ export function ensureCreationFlowStepIds(
|
||||
name,
|
||||
depends_on: raw.depends_on.map((d) => d.trim()).filter(Boolean),
|
||||
...(raw.params ? { params: raw.params } : {}),
|
||||
...withStepModeFields(raw),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
return normalizeReviseSteps(out);
|
||||
}
|
||||
|
||||
/** 按 id 或(唯一)name 解析步骤引用 */
|
||||
@@ -378,12 +554,7 @@ 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[];
|
||||
params?: CreationFlowStepParams;
|
||||
}> = [];
|
||||
const drafted: DraftCreationFlowStep[] = [];
|
||||
for (const item of stepsRaw) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
||||
const s = item as Record<string, unknown>;
|
||||
@@ -395,7 +566,17 @@ export function parseCreationFlow(raw: string | undefined | null): CreationFlow
|
||||
? depsRaw.map((d) => String(d).trim()).filter(Boolean)
|
||||
: [];
|
||||
const params = normalizeStepParams(s.params);
|
||||
drafted.push({ id, name, depends_on, ...(params ? { params } : {}) });
|
||||
const mode =
|
||||
parseCreationFlowStepMode(s.mode) ?? parseCreationFlowStepMode(s.intent);
|
||||
const revises = parseStepRevisesRef(s);
|
||||
drafted.push({
|
||||
id,
|
||||
name,
|
||||
depends_on,
|
||||
...(params ? { params } : {}),
|
||||
...(mode ? { mode } : {}),
|
||||
...(revises ? { revises } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
const steps = ensureCreationFlowStepIds(drafted);
|
||||
@@ -440,6 +621,9 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null {
|
||||
? m.opening.trim()
|
||||
: undefined;
|
||||
const repeatable = m.repeatable === true;
|
||||
const closer = m.closer === true;
|
||||
const auto = m.auto === true;
|
||||
const kind = parseModuleNodeKind(m.kind);
|
||||
const params = parseModuleParamSpecs(m.params);
|
||||
modules.push({
|
||||
id,
|
||||
@@ -447,6 +631,9 @@ export function parseModuleCatalog(raw: string): ModuleCatalog | null {
|
||||
declaration,
|
||||
artifact,
|
||||
...(repeatable ? { repeatable: true } : {}),
|
||||
...(closer ? { closer: true } : {}),
|
||||
...(auto ? { auto: true } : {}),
|
||||
...(kind ? { kind } : {}),
|
||||
...(opening ? { opening } : {}),
|
||||
...(params ? { params } : {}),
|
||||
});
|
||||
@@ -702,7 +889,9 @@ export function mergeCreationFlowPreservingAccepted(params: {
|
||||
const changed =
|
||||
current.name !== frozen.name ||
|
||||
JSON.stringify(current.depends_on) !== JSON.stringify(frozen.depends_on) ||
|
||||
JSON.stringify(current.params ?? null) !== JSON.stringify(frozen.params ?? null);
|
||||
JSON.stringify(current.params ?? null) !== JSON.stringify(frozen.params ?? null) ||
|
||||
(current.mode ?? "fresh") !== (frozen.mode ?? "fresh") ||
|
||||
(current.revises ?? "") !== (frozen.revises ?? "");
|
||||
if (changed) {
|
||||
steps[at] = frozen;
|
||||
restored.push(frozen.id);
|
||||
@@ -795,29 +984,49 @@ export async function loadModuleCatalog(
|
||||
/** 注入 design-flow:能力名 + 选型字段(meta)+ 编排参数;非执行全文 */
|
||||
export function formatModuleCatalogForAgent(catalog: ModuleCatalog): string {
|
||||
const lines = catalog.modules.map((m) => {
|
||||
const flags = m.repeatable ? "〔可反复〕" : "";
|
||||
const flags = [
|
||||
m.repeatable ? "〔可反复〕" : "",
|
||||
m.closer ? "〔收口〕" : "",
|
||||
m.auto ? "〔程序步〕" : "",
|
||||
m.kind ? MODULE_NODE_KIND_FLAGS[m.kind] : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("");
|
||||
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(";")}`,
|
||||
);
|
||||
if (isPriorArtifactModule(m)) {
|
||||
parts.push(
|
||||
` 规划产物字段(可提前写入 params;空则步内钉):${m.params
|
||||
.map((p) => {
|
||||
const hint = p.hint ? `,${p.hint}` : "";
|
||||
return `${p.key}(${p.label}${hint})`;
|
||||
})
|
||||
.join(";")}`,
|
||||
);
|
||||
} else {
|
||||
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 选项+其它,禁止空壳进执行。",
|
||||
"生成规则:改 target 时必须同步改 rule_id(英文 kebab-case)与步骤 id(生成规则·{对象});禁止只改中文对象沿用旧后台 id。",
|
||||
"按需选用,勿默认全选;步骤名只能从这里选;标〔可反复〕的可多次编入;标〔收口〕的是终节点:选定后结束创作并保存,排在细化终稿之后,其后不要再追加步骤;标〔程序步〕的确认编排后直接执行(不抛默认问题、不经同意并开始),产物仍验收。",
|
||||
"标〔先验产物〕的步骤(如生成规则、具体实例):须先定「写什么」。编排器可以提前规划多条并写入 params——这是正确的;执行时程序注入【规划产物】。规划可空,「生成什么」在步内钉。禁止为钉对象而 askUser 拦在确认开干前。规划或修订对象时同步改 rule_id(英文 kebab-case)与步骤 id(如 生成规则·{对象}),禁止只改中文沿用旧后台 id。",
|
||||
"其它有「编排参数」的步骤:确认开干前写齐必填 params;缺参时用 askUser 选项+其它。",
|
||||
"选型依据是下方「何时用 / 何时不用 / 边界」(来自各能力 meta)。",
|
||||
"两种「再来一次」必须分开:〔可反复〕再编入新 id(mode 缺省/fresh)= 彻底新建一条;回头修改已完成节点 = mode=revise + revises=原步 id,继承旧产物继续改。禁止把回头修改写成又一条 fresh。",
|
||||
"不要把能力执行全文塞进本步;执行由 design-step 注入。",
|
||||
lines.join("\n"),
|
||||
].join("\n");
|
||||
@@ -1153,8 +1362,8 @@ export function formatFlowProgressForAgent(params: {
|
||||
repeatable: boolean,
|
||||
) => {
|
||||
const flag = repeatable
|
||||
? "〔可反复:可再追加不同 id〕"
|
||||
: "〔不可反复:禁止再排一次执行〕";
|
||||
? "〔可反复:mode=fresh 再追加 = 彻底新建;要改旧条用 mode=revise〕"
|
||||
: "〔不可反复:禁止再排新建;要改已完成节点用 mode=revise 回头修改〕";
|
||||
const idBit = id ? `(id: ${id})` : "";
|
||||
doneLines.push(`- ${name}${idBit} · ${why} ${flag}`);
|
||||
doneNames.add(name);
|
||||
@@ -1196,9 +1405,12 @@ export function formatFlowProgressForAgent(params: {
|
||||
const draftLines: string[] = [];
|
||||
if (flow) {
|
||||
for (const step of flow.steps) {
|
||||
if (doneIds.has(step.id) || doneNames.has(step.name)) continue;
|
||||
if (doneIds.has(step.id) || isStepAccepted(step, accepted)) continue;
|
||||
const reviseBit = isReviseStep(step)
|
||||
? ` · 回头修改${step.revises ? `(revises: ${step.revises})` : ""}`
|
||||
: "";
|
||||
draftLines.push(
|
||||
`- ${step.name}(id: ${step.id})· 已在草案,保留原 id;不要当作新规划再写一遍`,
|
||||
`- ${step.name}(id: ${step.id})${reviseBit} · 已在草案,保留原 id;不要当作新规划再写一遍`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1210,9 +1422,9 @@ export function formatFlowProgressForAgent(params: {
|
||||
const lines = [
|
||||
"【流程进度】(程序钉死,必须遵守)",
|
||||
"你的任务是追加缺口,不是从头规划。",
|
||||
"已完成的非反复技能:只保留原 id 作 depends_on 锚点,禁止再作为将要执行的一步。",
|
||||
"草案里已有的步骤:原样保留,不要重排一遍开局。",
|
||||
"只有能力目录标了〔可反复〕的(如生成规则、具体实例)才允许再编入新 id。",
|
||||
"已完成的非反复技能:禁止再排一次「新建」(mode=fresh)。用户要改已完成节点 → 追加新 id,mode=revise,revises=原步 id;程序会继承既有产物。",
|
||||
"〔可反复〕再编入新 id 且 mode 缺省/fresh = 彻底新建一条(新对象/新批次),不是改旧的。要改已有那一条 → 同样用 mode=revise。",
|
||||
"草案里已有的步骤:原样保留,不要重排一遍开局。回头修改步插在最近已验收步之后、尚未执行的步之前。",
|
||||
];
|
||||
if (doneLines.length) {
|
||||
lines.push("", "已完成:", ...doneLines);
|
||||
@@ -1258,10 +1470,7 @@ export function validateCreationFlow(
|
||||
const allowed = catalog
|
||||
? new Set(catalog.modules.map((m) => m.name))
|
||||
: null;
|
||||
const nameCount = new Map<string, number>();
|
||||
for (const step of flow.steps) {
|
||||
nameCount.set(step.name, (nameCount.get(step.name) ?? 0) + 1);
|
||||
}
|
||||
const reportedFreshDup = new Set<string>();
|
||||
|
||||
for (let i = 0; i < flow.steps.length; i++) {
|
||||
const step = flow.steps[i]!;
|
||||
@@ -1275,14 +1484,44 @@ export function validateCreationFlow(
|
||||
}
|
||||
|
||||
const mod = catalog?.modules.find((m) => m.name === step.name);
|
||||
if (
|
||||
if (isReviseStep(step)) {
|
||||
const ref = step.revises?.trim() ?? "";
|
||||
if (!ref) {
|
||||
errors.push(
|
||||
`「${step.id}」是回头修改,但未标明 revises(应指向更前的同名步骤 id)`,
|
||||
);
|
||||
} else {
|
||||
const origin = findStepByRef(flow, ref);
|
||||
if (!origin) {
|
||||
errors.push(
|
||||
`「${step.id}」回头修改「${ref}」,但流程中没有该步骤`,
|
||||
);
|
||||
} else if (origin.id === step.id) {
|
||||
errors.push(`「${step.id}」不能回头修改自己`);
|
||||
} else if (origin.name !== step.name) {
|
||||
errors.push(
|
||||
`「${step.id}」回头修改「${origin.id}」,但能力名不同(${step.name} ≠ ${origin.name})`,
|
||||
);
|
||||
} else {
|
||||
const originIndex = flow.steps.findIndex((s) => s.id === origin.id);
|
||||
if (originIndex >= i) {
|
||||
errors.push(
|
||||
`「${step.id}」回头修改「${origin.id}」,但「${origin.id}」未排在其前面`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
catalog &&
|
||||
(nameCount.get(step.name) ?? 0) > 1 &&
|
||||
mod &&
|
||||
mod.repeatable !== true
|
||||
mod.repeatable !== true &&
|
||||
!reportedFreshDup.has(step.name) &&
|
||||
flow.steps.filter((s) => s.name === step.name && !isReviseStep(s)).length >
|
||||
1
|
||||
) {
|
||||
reportedFreshDup.add(step.name);
|
||||
errors.push(
|
||||
`「${step.name}」出现多次,但目录未标 repeatable(仅可反复能力可同名多次)`,
|
||||
`「${step.name}」出现多次新建,但目录未标 repeatable(非反复技能要改旧稿请用 mode=revise)`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1306,7 +1545,7 @@ export function validateCreationFlow(
|
||||
return { ok: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
/** 进 design-step 前:本步必填编排参数是否已齐 */
|
||||
/** 进 design-step 前:本步必填编排参数是否已齐。〔先验产物〕不拦。 */
|
||||
export function validateStepReadyToRun(
|
||||
step: CreationFlowStep,
|
||||
catalog: ModuleCatalog | null,
|
||||
@@ -1362,14 +1601,19 @@ export function formatCreationFlowForUser(
|
||||
const decl = new Map(
|
||||
(catalog?.modules ?? []).map((m) => [m.name, m] as const),
|
||||
);
|
||||
const seenName = new Map<string, number>();
|
||||
const seenFreshName = new Map<string, number>();
|
||||
const next = nextPendingStep(flow, acceptedStepIds);
|
||||
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 revise = isReviseStep(s);
|
||||
let occurrence: number | undefined;
|
||||
if (!revise) {
|
||||
const n = (seenFreshName.get(s.name) ?? 0) + 1;
|
||||
seenFreshName.set(s.name, n);
|
||||
occurrence = n;
|
||||
}
|
||||
const mod = decl.get(s.name);
|
||||
const paramsMissing = missingRequiredStepParams(s, mod);
|
||||
const runState: FlowStepRunState = isStepAccepted(s, acceptedStepIds)
|
||||
@@ -1382,9 +1626,12 @@ export function formatCreationFlowForUser(
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
depends_on: s.depends_on,
|
||||
occurrence: n,
|
||||
occurrence,
|
||||
...(revise ? { mode: "revise" as const } : {}),
|
||||
...(s.revises ? { revises: s.revises } : {}),
|
||||
declaration: mod?.declaration,
|
||||
repeatable: mod?.repeatable,
|
||||
...(mod?.kind ? { kind: mod.kind } : {}),
|
||||
runState,
|
||||
...(s.params ? { params: s.params } : {}),
|
||||
...(paramsMissing.length > 0 ? { paramsMissing } : {}),
|
||||
@@ -1443,6 +1690,7 @@ export function nextPendingStep(
|
||||
acceptedStepIds: readonly string[],
|
||||
): CreationFlowStep | null {
|
||||
if (!flow?.steps.length) return null;
|
||||
const ready: CreationFlowStep[] = [];
|
||||
for (const step of flow.steps) {
|
||||
if (isStepAccepted(step, acceptedStepIds)) continue;
|
||||
const depsOk = step.depends_on.every((dep) => {
|
||||
@@ -1450,9 +1698,9 @@ export function nextPendingStep(
|
||||
if (!depStep) return false;
|
||||
return isStepAccepted(depStep, acceptedStepIds);
|
||||
});
|
||||
if (depsOk) return step;
|
||||
if (depsOk) ready.push(step);
|
||||
}
|
||||
return null;
|
||||
return ready.find((s) => isReviseStep(s)) ?? ready[0] ?? null;
|
||||
}
|
||||
|
||||
/** 当前已列出的步骤是否都已验收(不管 status) */
|
||||
@@ -1532,8 +1780,10 @@ export type DesignStepBinding = {
|
||||
module: ModuleCatalogEntry;
|
||||
depTags: string[];
|
||||
modulePrompt: string;
|
||||
/** 程序开场白;无则本步直接调 LLM */
|
||||
/** 程序开场白;无则本步直接调 LLM。回头修改不抛 opening。 */
|
||||
opening: string | null;
|
||||
/** 回头修改时注入的既有产物 tag */
|
||||
inheritTag: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1570,7 +1820,9 @@ export async function resolveDesignStepBinding(params: {
|
||||
`# ${module.name}\n\n(模块 prompt.md 缺失,请补充 skills/.../modules/${module.id}/prompt.md)`;
|
||||
|
||||
const opening =
|
||||
module.opening?.trim() || extractModuleOpening(modulePromptRaw) || null;
|
||||
isReviseStep(step) || module.auto
|
||||
? null
|
||||
: module.opening?.trim() || extractModuleOpening(modulePromptRaw) || null;
|
||||
|
||||
return {
|
||||
step,
|
||||
@@ -1578,5 +1830,6 @@ export async function resolveDesignStepBinding(params: {
|
||||
depTags: dependencyArtifactTags(step, catalog, flow),
|
||||
modulePrompt: formatModulePromptForLlm(modulePromptRaw),
|
||||
opening,
|
||||
inheritTag: isReviseStep(step) ? module.artifact : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
slotOrderForRef,
|
||||
} from "./context-order.js";
|
||||
import type { ContextSegmentDef } from "./context-segments.js";
|
||||
import { PLAY_VISIBLE_BODY_INSTRUCTION } from "./present-packet.js";
|
||||
|
||||
type WorkerTemplateDoc = {
|
||||
id?: string;
|
||||
@@ -44,8 +45,9 @@ type WorkerTemplateDoc = {
|
||||
/**
|
||||
* 解析本次 worker 的验收模式(创作 / run 共用入口)。
|
||||
* - design-* / opening-generator 磁盘创作 worker → 始终 user_confirmed
|
||||
* - 游玩回合:一律 no_confirmation(一句输入连跑,不穿插验收)
|
||||
* - Worker 集 acceptance: review → user_confirmed;continue → no_confirmation
|
||||
* - 未声明 acceptance:design 生命周期默认确认;play 缺省按 review(稳妥)
|
||||
* - 未声明 acceptance:创作默认确认
|
||||
*/
|
||||
export function resolveAcceptanceModeForWorker(params: {
|
||||
session: RuntimeSession;
|
||||
@@ -56,6 +58,9 @@ export function resolveAcceptanceModeForWorker(params: {
|
||||
if (isDesignDiskWorker(workerId) || workerId === "opening-generator") {
|
||||
return "user_confirmed";
|
||||
}
|
||||
if (inferLifecycleStage(params.session) === "play") {
|
||||
return "no_confirmation";
|
||||
}
|
||||
|
||||
const decl = buildInstanceWorkerDeclaration(
|
||||
params.session,
|
||||
@@ -66,7 +71,6 @@ export function resolveAcceptanceModeForWorker(params: {
|
||||
if (entry?.acceptance === "continue") return "no_confirmation";
|
||||
if (entry?.acceptance === "review") return "user_confirmed";
|
||||
|
||||
// 未写明:创作阶段默认验收;游玩缺省也验收(避免静默连跑)
|
||||
return "user_confirmed";
|
||||
}
|
||||
|
||||
@@ -210,6 +214,11 @@ export function buildDeclaredWorkerSkill(params: {
|
||||
params.template?.duty?.trim() ||
|
||||
`执行 ${id}`;
|
||||
const excerpt = params.template?.prompt_excerpt?.trim() || "";
|
||||
const visibleBodyRule =
|
||||
id === "narrator" || id === "round-present"
|
||||
? PLAY_VISIBLE_BODY_INSTRUCTION
|
||||
: "";
|
||||
const excerptWithRule = [excerpt, visibleBodyRule].filter(Boolean).join("\n\n");
|
||||
const presentation = params.entry.presentation
|
||||
? JSON.stringify(params.entry.presentation, null, 2)
|
||||
: "";
|
||||
@@ -217,7 +226,7 @@ export function buildDeclaredWorkerSkill(params: {
|
||||
const premises = (params.workerSet?.core_premises ?? []).filter(Boolean);
|
||||
const residentSection = formatResidentPromptSection(resident, id);
|
||||
|
||||
const personaText = [duty, excerpt].filter(Boolean).join("\n\n");
|
||||
const personaText = [duty, excerptWithRule].filter(Boolean).join("\n\n");
|
||||
const orderDoc = parseContextOrder(params.workerSet?.context_order);
|
||||
const orderSlot = slotOrderForRef(orderDoc, id);
|
||||
let contextSegments: ContextSegmentDef[] | undefined;
|
||||
@@ -277,7 +286,7 @@ export function buildDeclaredWorkerSkill(params: {
|
||||
"",
|
||||
duty,
|
||||
"",
|
||||
excerpt ? `## 写法要点\n\n${excerpt}` : "",
|
||||
excerptWithRule ? `## 写法要点\n\n${excerptWithRule}` : "",
|
||||
presentation ? `## presentation(实例)\n\n\`\`\`json\n${presentation}\n\`\`\`` : "",
|
||||
narrative ? `## 叙事指南\n\n${narrative}` : "",
|
||||
premises.length
|
||||
|
||||
+40
-6
@@ -516,7 +516,9 @@ export async function loadWorkerSkillWithContext(
|
||||
resolveDesignStepBinding,
|
||||
CREATION_CURRENT_STEP_TAG,
|
||||
CREATION_MODULE_OPENING_TAG,
|
||||
formatPriorArtifactContext,
|
||||
formatStepParamsForPrompt,
|
||||
isPriorArtifactModule,
|
||||
} = await import("./creation-flow.js");
|
||||
const binding = await resolveDesignStepBinding({
|
||||
skillPackRoot: skill.skillPackRoot,
|
||||
@@ -526,11 +528,27 @@ export async function loadWorkerSkillWithContext(
|
||||
skillsRoot,
|
||||
});
|
||||
if (binding) {
|
||||
const inheritTag = binding.inheritTag?.trim() || "";
|
||||
const inheritNote = inheritTag
|
||||
? `\n\n【回头修改】本步继承既有产物继续改,不是从零再生成。程序已注入【既有产物 · 继承修改】。保留未点名要改的部分,写回同一产物。禁止推倒重写,禁止再走一遍开场询问。`
|
||||
: "";
|
||||
const openingNote = binding.opening
|
||||
? `\n\n【程序开场】若黑板有「${CREATION_MODULE_OPENING_TAG}」,该默认问题已由程序发给用户(不经 LLM);用户首答在「用户.worker答复」。勿重复同一开场白,在其答复与提示词基础上继续追问或产出。`
|
||||
: "";
|
||||
const paramsBlock = `## 【本步参数】(编排期已钉;直接按此执行,勿再问「生成什么 / 调用哪个规则」)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
|
||||
modulePromptBlock = `${paramsBlock}\n\n## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}`;
|
||||
const paramsBlock = isPriorArtifactModule(binding.module)
|
||||
? formatPriorArtifactContext(binding.step.params)
|
||||
: `## 【本步参数】(编排期已钉;直接按此执行,勿再问「生成什么 / 调用哪个规则」)\n\n${formatStepParamsForPrompt(binding.step.params)}`;
|
||||
let rosterBlock = "";
|
||||
const depProjection =
|
||||
binding.module.id === "context-order" ? "index" : undefined;
|
||||
if (binding.module.id === "context-order") {
|
||||
const { formatPlayAgentRosterForPrompt } = await import("./play-slots.js");
|
||||
rosterBlock = `${formatPlayAgentRosterForPrompt()}\n\n`;
|
||||
}
|
||||
modulePromptBlock = `${rosterBlock}${paramsBlock}\n\n## 【本步方法 · ${binding.module.name}】\n\n${binding.modulePrompt.trim()}${openingNote}${inheritNote}`;
|
||||
const depTags = inheritTag
|
||||
? binding.depTags.filter((tag) => tag !== inheritTag)
|
||||
: binding.depTags;
|
||||
const baseInputs = [
|
||||
"用户.需求",
|
||||
"book.brief",
|
||||
@@ -540,16 +558,29 @@ export async function loadWorkerSkillWithContext(
|
||||
"设计.创作流程",
|
||||
CREATION_CURRENT_STEP_TAG,
|
||||
CREATION_MODULE_OPENING_TAG,
|
||||
...binding.depTags,
|
||||
...depTags,
|
||||
...(inheritTag ? [inheritTag] : []),
|
||||
];
|
||||
const inputTags = [...new Set(baseInputs)];
|
||||
const outputTags = [binding.module.artifact];
|
||||
const depSegments = binding.depTags.map((tag, i) => ({
|
||||
const depSegments = depTags.map((tag, i) => ({
|
||||
id: `dep-${i}`,
|
||||
tier: "static" as const,
|
||||
tags: [tag],
|
||||
label: `## 【依赖产物 · ${tag}】只读`,
|
||||
label: `## 【依赖产物 · ${tag}】只读${depProjection === "index" ? "(仅 brief / 挂载 / 稳变)" : ""}`,
|
||||
...(depProjection ? { projection: depProjection } : {}),
|
||||
}));
|
||||
const inheritSegment = inheritTag
|
||||
? [
|
||||
{
|
||||
id: "inherit-existing",
|
||||
tier: "static" as const,
|
||||
tags: [inheritTag],
|
||||
label:
|
||||
"## 【既有产物 · 继承修改】在下列正文上继续改,写回同一产物;禁止从零另起一份",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const openingSegment = binding.opening
|
||||
? [
|
||||
{
|
||||
@@ -562,13 +593,16 @@ export async function loadWorkerSkillWithContext(
|
||||
: [];
|
||||
patchedWorker = {
|
||||
...worker,
|
||||
name: `创作 · ${binding.module.name}`,
|
||||
name: inheritTag
|
||||
? `创作 · 回头修改 · ${binding.module.name}`
|
||||
: `创作 · ${binding.module.name}`,
|
||||
description: binding.module.declaration,
|
||||
inputTags,
|
||||
outputTags,
|
||||
contextSegments: [
|
||||
...(worker.contextSegments ?? []),
|
||||
...openingSegment,
|
||||
...inheritSegment,
|
||||
...depSegments,
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 开场白终节点:选定后落库、关 DAG。
|
||||
* 能力 `opening-setup` 与磁盘 worker `opening-generator` 共用这条收口。
|
||||
*/
|
||||
import {
|
||||
parseTableDoc,
|
||||
stringifyTableDoc,
|
||||
type TableDoc,
|
||||
} from "../blackboard/table-cells.js";
|
||||
import {
|
||||
parseCreationFlow,
|
||||
stringifyCreationFlow,
|
||||
} from "./creation-flow.js";
|
||||
import { parseContextFragment } from "./context-fragment.js";
|
||||
|
||||
export const OPENING_SETUP_ARTIFACT_TAG = "设计.开场白与开场变量";
|
||||
export const OPENING_OUTPUT_TAG = "输出.开场白";
|
||||
export const OPENING_INITIAL_VARS_TAG = "运行.初始变量";
|
||||
export const OPENING_CURRENT_VARS_TAG = "变量.当前";
|
||||
|
||||
export const SLOT_CREATION_SEALED_BY_OPENING = "creationSealedByOpening";
|
||||
export const SLOT_OPENING_SELECTED_INDEX = "openingSelectedIndex";
|
||||
|
||||
export const CREATION_SEALED_WAITING_MESSAGE =
|
||||
"开场已选定,创作已收口并保存。可切换到「游玩」。";
|
||||
|
||||
export const INSTANCE_OPENING_SNAPSHOT_LABEL = "创作定稿(开场)";
|
||||
|
||||
export function isOpeningLockTag(tag: string): boolean {
|
||||
const t = tag.trim();
|
||||
return t === OPENING_OUTPUT_TAG || t === OPENING_INITIAL_VARS_TAG;
|
||||
}
|
||||
|
||||
export function isOpeningSealArtifact(artifact: {
|
||||
workerId?: string;
|
||||
outputTags?: readonly string[];
|
||||
}): boolean {
|
||||
if ((artifact.workerId ?? "").trim() === "opening-generator") return true;
|
||||
return (artifact.outputTags ?? []).includes(OPENING_SETUP_ARTIFACT_TAG);
|
||||
}
|
||||
|
||||
export type OpeningVariable = {
|
||||
name: string;
|
||||
value: unknown;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
export type OpeningSealPayload = {
|
||||
candidates: string[];
|
||||
selectedIndex: number;
|
||||
selectedText: string;
|
||||
variables: OpeningVariable[];
|
||||
};
|
||||
|
||||
function asTrimmed(v: unknown): string | undefined {
|
||||
if (typeof v !== "string") return undefined;
|
||||
const t = v.trim();
|
||||
return t || undefined;
|
||||
}
|
||||
|
||||
function collectCandidateTexts(raw: unknown): string[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
const out: string[] = [];
|
||||
for (const item of raw) {
|
||||
if (typeof item === "string" && item.trim()) {
|
||||
out.push(item.trim());
|
||||
continue;
|
||||
}
|
||||
if (item && typeof item === "object" && !Array.isArray(item)) {
|
||||
const row = item as Record<string, unknown>;
|
||||
const text =
|
||||
asTrimmed(row.全文) ||
|
||||
asTrimmed(row.text) ||
|
||||
asTrimmed(row.开场白) ||
|
||||
asTrimmed(row.内容);
|
||||
if (text) out.push(text);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function collectVariables(raw: unknown): OpeningVariable[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
const out: OpeningVariable[] = [];
|
||||
for (const item of raw) {
|
||||
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
|
||||
const row = item as Record<string, unknown>;
|
||||
const name =
|
||||
asTrimmed(row.名) || asTrimmed(row.name) || asTrimmed(row.key) || "";
|
||||
if (!name) continue;
|
||||
const note = asTrimmed(row.依据) || asTrimmed(row.note);
|
||||
out.push({
|
||||
name,
|
||||
value: row.值 !== undefined ? row.值 : row.value,
|
||||
...(note ? { note } : {}),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function bodyRecord(fragmentBody: unknown): Record<string, unknown> | null {
|
||||
if (!fragmentBody || typeof fragmentBody !== "object" || Array.isArray(fragmentBody)) {
|
||||
return null;
|
||||
}
|
||||
return fragmentBody as Record<string, unknown>;
|
||||
}
|
||||
|
||||
/** 从开场白 fragment(或裸 JSON)抽出候选与变量。 */
|
||||
export function parseOpeningSealPayload(
|
||||
raw: string | null | undefined,
|
||||
selectedIndex = 0,
|
||||
): OpeningSealPayload | null {
|
||||
if (!raw?.trim()) return null;
|
||||
const fragment = parseContextFragment(raw);
|
||||
const body = bodyRecord(fragment?.正文) ?? (() => {
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
return bodyRecord(parsed) ?? bodyRecord((parsed as { 正文?: unknown })?.正文);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const primary =
|
||||
asTrimmed(body?.开场白全文) ||
|
||||
asTrimmed(body?.["输出.开场白"]) ||
|
||||
(typeof fragment?.正文 === "string" ? asTrimmed(fragment.正文) : undefined);
|
||||
|
||||
const fromList = collectCandidateTexts(body?.开场白候选);
|
||||
const candidates: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const push = (text: string | undefined) => {
|
||||
if (!text || seen.has(text)) return;
|
||||
seen.add(text);
|
||||
candidates.push(text);
|
||||
};
|
||||
push(primary);
|
||||
for (const t of fromList) push(t);
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
const max = candidates.length - 1;
|
||||
const idx = Number.isFinite(selectedIndex)
|
||||
? Math.min(max, Math.max(0, Math.trunc(selectedIndex)))
|
||||
: 0;
|
||||
|
||||
return {
|
||||
candidates,
|
||||
selectedIndex: idx,
|
||||
selectedText: candidates[idx] ?? candidates[0]!,
|
||||
variables: collectVariables(body?.开场变量),
|
||||
};
|
||||
}
|
||||
|
||||
export function openingVariablesToTableDoc(
|
||||
variables: readonly OpeningVariable[],
|
||||
source = "worker:opening-setup",
|
||||
): TableDoc {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
rows: variables.map((v) => ({
|
||||
key: v.name,
|
||||
value: v.value,
|
||||
rev: 1,
|
||||
updatedAt: now,
|
||||
source,
|
||||
visibility: "visible" as const,
|
||||
...(v.note ? { note: v.note } : {}),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeOpeningTablePatch(
|
||||
currentRaw: string | null | undefined,
|
||||
variables: readonly OpeningVariable[],
|
||||
source = "worker:opening-setup",
|
||||
): string | null {
|
||||
if (!variables.length) return null;
|
||||
const patch = openingVariablesToTableDoc(variables, source);
|
||||
const current = parseTableDoc(currentRaw);
|
||||
if (!current) return stringifyTableDoc(patch);
|
||||
const byKey = new Map(current.rows.map((r) => [r.key, r]));
|
||||
const now = new Date().toISOString();
|
||||
for (const row of patch.rows) {
|
||||
const prev = byKey.get(row.key);
|
||||
if (prev && String(prev.source).startsWith("user")) continue;
|
||||
byKey.set(row.key, {
|
||||
...row,
|
||||
rev: (prev?.rev ?? 0) + 1,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
return stringifyTableDoc({ rows: [...byKey.values()] });
|
||||
}
|
||||
|
||||
export function closeCreationFlowRaw(raw: string | null | undefined): string | null {
|
||||
const flow = parseCreationFlow(raw);
|
||||
if (!flow) return raw?.trim() ? raw : null;
|
||||
if (flow.status === "closed") return stringifyCreationFlow(flow);
|
||||
return stringifyCreationFlow({ ...flow, status: "closed" });
|
||||
}
|
||||
@@ -319,3 +319,54 @@ export function inferPlaySlotsFromWorkers(
|
||||
chance: hasChance,
|
||||
};
|
||||
}
|
||||
|
||||
const AGENT_ID_ALIASES: Record<string, PlaySlotId | OnDemandSlotId> = {
|
||||
auditor: "auditor",
|
||||
旁观维护: "auditor",
|
||||
表格管理: "auditor",
|
||||
gm: "gm",
|
||||
"world-simulator": "gm",
|
||||
主世界层: "gm",
|
||||
narrator: "narrator",
|
||||
叙事转述: "narrator",
|
||||
转述: "narrator",
|
||||
perspective: "perspective",
|
||||
"role-decide": "perspective",
|
||||
角色视角: "perspective",
|
||||
chance: "chance",
|
||||
机遇裁定: "chance",
|
||||
};
|
||||
|
||||
/** 把模型写的 id / 中文名 / worker ref 收成槽键 */
|
||||
export function resolvePlayAgentId(
|
||||
raw: string,
|
||||
): PlaySlotId | OnDemandSlotId | undefined {
|
||||
const t = raw.trim();
|
||||
return AGENT_ID_ALIASES[t] ?? AGENT_ID_ALIASES[t.toLowerCase()];
|
||||
}
|
||||
|
||||
/** 注入投影排序等晚段步骤:名册由程序钉死,模型只抄不发明。 */
|
||||
export function formatPlayAgentRosterForPrompt(): string {
|
||||
const lines = [
|
||||
"## 【本局固定执行单元 · 程序钉死】",
|
||||
"",
|
||||
"禁止增删改名,禁止发明未入白名单的槽(如画图)。先抄名册,再给每个已启用者分配上下文 inserts。",
|
||||
"",
|
||||
"每轮管线(顺序固定):",
|
||||
];
|
||||
for (const id of PLAY_SLOT_ORDER) {
|
||||
const meta = PLAY_SLOT_META[id];
|
||||
const def = id === "perspective" ? "默认关" : "默认开";
|
||||
lines.push(
|
||||
`- \`${id}\` → ref=\`${DEFAULT_PLAY_SLOT_REFS[id]}\` **${meta.label}**(${def}):${meta.purpose}`,
|
||||
);
|
||||
}
|
||||
lines.push("", "按需(不进每轮;enabled 时可空 inserts):");
|
||||
for (const id of Object.keys(DEFAULT_ON_DEMAND_REFS) as OnDemandSlotId[]) {
|
||||
const meta = ON_DEMAND_SLOT_META[id];
|
||||
lines.push(
|
||||
`- \`${id}\` → ref=\`${DEFAULT_ON_DEMAND_REFS[id]}\` **${meta.label}**(默认关):${meta.purpose}`,
|
||||
);
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 游玩回合:按运行规格 playWorkerIds 连跑(旁观 → 视角? → 主世界层 → 转述)。
|
||||
* 与创作总管无关;收口后的输入走这里。
|
||||
*/
|
||||
export const SLOT_PLAY_TURN_QUEUE = "playTurnQueue";
|
||||
export const SLOT_PLAY_LAYER_ACTIVE = "playLayerActive";
|
||||
|
||||
export const PLAY_WORKING_SNAPSHOT_ID = "play-working";
|
||||
export const PLAY_WORKING_SNAPSHOT_LABEL = "当前游玩";
|
||||
|
||||
export function isPlayLayerActive(slots: Record<string, unknown> | undefined): boolean {
|
||||
return Boolean(slots?.[SLOT_PLAY_LAYER_ACTIVE]);
|
||||
}
|
||||
|
||||
export function readPlayTurnQueue(
|
||||
slots: Record<string, unknown> | undefined,
|
||||
): string[] {
|
||||
const raw = slots?.[SLOT_PLAY_TURN_QUEUE];
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw
|
||||
.filter((id): id is string => typeof id === "string" && Boolean(id.trim()))
|
||||
.map((id) => id.trim());
|
||||
}
|
||||
|
||||
export function withPlayTurnQueue(
|
||||
slots: Record<string, unknown>,
|
||||
queue: string[] | undefined,
|
||||
): Record<string, unknown> {
|
||||
return {
|
||||
...slots,
|
||||
[SLOT_PLAY_TURN_QUEUE]: queue?.length ? queue : undefined,
|
||||
};
|
||||
}
|
||||
@@ -132,6 +132,9 @@ function normalizeBlocks(raw: unknown): PresentBlocks {
|
||||
if (out.monitor === undefined && row.监控 !== undefined) {
|
||||
out.monitor = row.监控 as PresentBlocks["monitor"];
|
||||
}
|
||||
if (typeof out.body === "string") {
|
||||
out.body = stripPresentSourceFences(out.body);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -170,15 +173,35 @@ export function presentFromPlainText(
|
||||
return {
|
||||
schema: PRESENT_SCHEMA,
|
||||
shell,
|
||||
blocks: { body: text },
|
||||
blocks: { body: stripPresentSourceFences(text) },
|
||||
};
|
||||
}
|
||||
|
||||
/** 模型常把 tag 名写进正文开头;展示前剥掉 */
|
||||
export function stripPresentSourceFences(raw: string): string {
|
||||
let s = raw.trim();
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const next = s
|
||||
.replace(/^(#{1,6}\s*)?输出[.::]?\s*(用户展示|开场白)(?:\s*\n+|\s*$)/u, "")
|
||||
.replace(/^【\s*输出[.::]?\s*(用户展示|开场白)\s*】(?:\s*\n+|\s*$)/u, "")
|
||||
.trim();
|
||||
if (next === s) break;
|
||||
s = next;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
export const PLAY_VISIBLE_BODY_INSTRUCTION = [
|
||||
"用户可见正文(present.v1 的 blocks.body,或纯 Markdown 主读)默认 **500~2000 字**(按汉字计)。",
|
||||
"不要写成几句气泡短信就结束,除非用户明确要求极短。",
|
||||
"禁止把 tag 名(如「输出.用户展示」)、压缩摘要、过程日志写进正文。",
|
||||
].join("\n");
|
||||
|
||||
export function parsePresentPacket(
|
||||
raw: string | undefined | null,
|
||||
fallbackShell: PresentShellId = "prose",
|
||||
): PresentPacketView {
|
||||
const text = raw?.trim() ?? "";
|
||||
const text = stripPresentSourceFences(raw?.trim() ?? "");
|
||||
if (!text) {
|
||||
return {
|
||||
ok: true,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Blackboard } from "../blackboard/blackboard.js";
|
||||
import type { RuntimeSession } from "../types/runtime.js";
|
||||
import { isPlayLayerActive } from "./play-turn.js";
|
||||
import {
|
||||
deriveDesignStageScope,
|
||||
deriveOnDemandWorkerScope,
|
||||
@@ -45,8 +46,9 @@ export function canEnterPlay(session: RuntimeSession): boolean {
|
||||
}
|
||||
|
||||
export function inferLifecycleStage(session: RuntimeSession): LifecycleStage {
|
||||
const override = session.slots.uiLifecycleStage;
|
||||
if (override === "play" && canEnterPlay(session)) return "play";
|
||||
if (!canEnterPlay(session)) return "design";
|
||||
if (session.slots.uiLifecycleStage === "play") return "play";
|
||||
if (isPlayLayerActive(session.slots)) return "play";
|
||||
return "design";
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,13 @@ import {
|
||||
OpenAiCompatibleProvider,
|
||||
type LlmProvider,
|
||||
} from "../llm/client.js";
|
||||
import { PresetLlmProvider } from "../llm/preset-wrapper.js";
|
||||
import { resolveActivePreset } from "../preset/store.js";
|
||||
import { loadAppSettings } from "../config/settings.js";
|
||||
import {
|
||||
TokenTrackingProvider,
|
||||
type LlmTrackingContext,
|
||||
} from "../llm/token-tracker.js";
|
||||
wrapLlmForSession,
|
||||
type LlmTrackingRef,
|
||||
} from "../llm/preset-wrapper.js";
|
||||
import type { ParsedWorkerSkill, SkillWorkerLlmBindings } from "./types.js";
|
||||
|
||||
type LlmTrackingRef = { current: LlmTrackingContext };
|
||||
export type { LlmTrackingContext } from "../llm/token-tracker.js";
|
||||
|
||||
/** 按 ApiProfile.id 构建 LLM;找不到 profile 时回退 fallback */
|
||||
export function createLlmForProfileId(
|
||||
@@ -25,17 +22,10 @@ export function createLlmForProfileId(
|
||||
return fallback;
|
||||
}
|
||||
|
||||
let inner: LlmProvider = new OpenAiCompatibleProvider(
|
||||
profileToLlmConfig(profile),
|
||||
return wrapLlmForSession(
|
||||
new OpenAiCompatibleProvider(profileToLlmConfig(profile)),
|
||||
trackingRef,
|
||||
);
|
||||
const preset = resolveActivePreset(loadAppSettings().activePresetId);
|
||||
if (preset) {
|
||||
inner = new PresetLlmProvider(inner, () =>
|
||||
resolveActivePreset(loadAppSettings().activePresetId),
|
||||
);
|
||||
}
|
||||
if (!trackingRef) return inner;
|
||||
return new TokenTrackingProvider(inner, () => trackingRef.current);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,5 +132,3 @@ export function filterInputsForRolePerspective(
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
export type { LlmTrackingContext };
|
||||
|
||||
Reference in New Issue
Block a user