Steer generation rules toward direction pools and readable fills.

Direction-first pools, recipes, and grade manifestation replace enum-tag stuffing; spawned products can be deleted and in-flight runs aborted.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-08 01:02:32 +08:00
co-authored by Cursor
parent 3c751988de
commit e2446b0d97
23 changed files with 2023 additions and 292 deletions
+2 -2
View File
@@ -88,12 +88,12 @@ export class Blackboard {
this.writeSeq += 1;
const existing = this.getLatestByTag(input.tag);
const item: BlackboardItem = {
id: randomUUID(),
id: existing?.id ?? randomUUID(),
tag: input.tag,
content: input.content,
source: input.source,
scope: input.scope ?? existing?.scope,
createdAt: now,
createdAt: existing?.createdAt ?? now,
updatedAt: `${now}#${this.writeSeq}`,
dependencies: input.dependencies,
metadata: input.metadata,
+2 -1
View File
@@ -421,7 +421,8 @@ export function canApplyEvent(
reason?.kind === "approve_step" ||
reason?.kind === "worker_questions" ||
reason?.kind === "input" ||
reason?.kind === "revision"
reason?.kind === "revision" ||
reason?.kind === "review_artifact"
);
case "creation_step_pick_awaited":
return session.phase === "running";
+346 -71
View File
@@ -55,7 +55,10 @@ import {
extractUnitContentFromDraft,
isDesignDiskWorker,
isDesignUnitArtifact,
normalizeCreationUnitId,
parseAcceptedContentStore,
parseAcceptedUnits,
dropAcceptedUnitContent,
upsertAcceptedUnitContent,
} from "../skills/creation-units.js";
import {
@@ -80,6 +83,8 @@ import {
spawnRepeatableCreationStep,
spawnInstanceFromPrototype,
removeUnstartedInstance,
removeRepeatableInstance,
pruneRepeatableArtifactContent,
isPrototypeStep,
isPrototypeSelectable,
isInstanceStep,
@@ -93,7 +98,6 @@ import {
patchCreationFlowStepParams,
pickRecordedStepId,
resolveDesignStepBinding,
shouldSkipModuleOpening,
isProgressPointerTag,
missingRequiredStepParams,
stepUnitId,
@@ -101,6 +105,7 @@ import {
stringifyModuleOpeningState,
finalizeCreationFlow,
type CreationFlow,
type CreationFlowStep,
type CreationFlowStepParams,
type ModuleCatalog,
type ModuleCatalogEntry,
@@ -185,6 +190,7 @@ import type {
AcceptanceMode,
ActiveSkillSnapshot,
ApplyEventResult,
ArtifactRecord,
MainAgentDecision,
PhaseEffect,
RuntimeEvent,
@@ -339,6 +345,71 @@ export class PhaseRuntime {
this.lastWorkerRunSnapshot = null;
}
/**
* 提前停止当前生成:还原开跑快照,回到生成前的等待面(或分层图)。
* 不重跑。
*/
abortCurrentRun(): RuntimeSession {
const snap = this.lastWorkerRunSnapshot;
if (snap) {
this.restoreFromCheckpoint(snap.runtimeSession, snap.blackboardItems);
}
return this.settleAfterAbort();
}
/** 停止后落到可操作等待态,避免停在 running 空转。 */
private settleAfterAbort(): RuntimeSession {
const s = this.session;
if (s.waitingReason) return s;
const pendingId = s.pendingArtifactId?.trim();
const revisionTarget =
typeof s.slots.revisionTargetArtifactId === "string"
? s.slots.revisionTargetArtifactId.trim()
: "";
const reviewId =
pendingId && s.artifacts.some((a) => a.id === pendingId)
? pendingId
: revisionTarget && s.artifacts.some((a) => a.id === revisionTarget)
? revisionTarget
: "";
if (reviewId) {
this.session = {
...s,
phase: "waiting_user",
waitingReason: { kind: "review_artifact", artifactId: reviewId },
pendingArtifactId: reviewId,
currentWorkerId: undefined,
pendingDecision: undefined,
updatedAt: new Date().toISOString(),
};
return this.session;
}
if (this.hasCreationFlow()) {
this.clearProposedStep();
this.session = {
...s,
phase: "waiting_user",
waitingReason: { kind: "pick_creation_step" },
pendingDecision: undefined,
currentWorkerId: undefined,
updatedAt: new Date().toISOString(),
};
return this.session;
}
if (this.recoverOrphanedRun()) return this.session;
if (s.phase === "running") {
this.session = {
...s,
phase: "waiting_user",
waitingReason: { kind: "input", message: "已停止生成,可继续说明。" },
currentWorkerId: undefined,
pendingDecision: undefined,
updatedAt: new Date().toISOString(),
};
}
return this.session;
}
/**
* 执行中卡住:有进行中的 worker 则按开跑快照重跑;否则重跑总管。
*/
@@ -711,12 +782,19 @@ export class PhaseRuntime {
* 可增殖节点刚增殖出、尚未验收的空实例从图上撤掉。
*/
async leaveCreationStep(): Promise<RuntimeSession> {
if (this.session.phase === "running" && !this.session.waitingReason) {
this.abortCurrentRun();
}
if (this.session.waitingReason?.kind === "pick_creation_step") {
return this.session;
}
const waiting = this.session.waitingReason?.kind;
const allowed =
waiting === "worker_questions" ||
waiting === "input" ||
waiting === "revision" ||
waiting === "approve_step";
waiting === "approve_step" ||
waiting === "review_artifact";
if (!allowed) {
throw new Error("当前不能返回节点选择");
}
@@ -724,6 +802,7 @@ export class PhaseRuntime {
throw new Error("游玩中不能返回创作节点");
}
const stepId = this.readPinnedStepId();
this.forgetUnansweredModuleOpening(stepId);
const removed = this.unspawnCurrentInstanceIfEmpty();
this.clearProposedStep();
this.clearCurrentStepPointers();
@@ -768,6 +847,42 @@ export class PhaseRuntime {
return { id: step.id, name: step.name };
}
/** 离开节点时若默认问题只展示过、还没答:清状态,下次点进再出引导。 */
private forgetUnansweredModuleOpening(stepId: string): void {
const id = stepId.trim();
if (!id) return;
const flow = parseCreationFlow(
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
);
const step = flow ? findStepByRef(flow, id) : null;
const key = step ? stepUnitId(step) : id;
const state = parseModuleOpeningState(
this.session.slots[SLOT_CREATION_MODULE_OPENING_STATE] ??
this.blackboard.getContentByTag(CREATION_MODULE_OPENING_STATE_TAG),
);
if (state[key] !== "shown") return;
delete state[key];
const raw = stringifyModuleOpeningState(state);
this.blackboard.write({
tag: CREATION_MODULE_OPENING_STATE_TAG,
content: raw,
source: "runtime",
});
this.blackboard.write({
tag: CREATION_MODULE_OPENING_TAG,
content: "",
source: "runtime",
});
this.session = {
...this.session,
slots: {
...this.session.slots,
[SLOT_CREATION_MODULE_OPENING_STATE]: raw,
[CREATION_MODULE_OPENING_TAG]: undefined,
},
};
}
private clearCurrentStepPointers(): void {
this.blackboard.write({
tag: CREATION_CURRENT_STEP_TAG,
@@ -856,6 +971,85 @@ export class PhaseRuntime {
return this.session;
}
/** 图上删除可增殖产物(已验收也可)。 */
async deleteCreationStep(stepId: string): Promise<RuntimeSession> {
const id = stepId.trim();
if (!id) throw new Error("请指定要删除的产物");
this.syncSlotsToBlackboard(this.session);
const catalog = await this.loadActiveModuleCatalog();
const flow = this.readNormalizedFlow(catalog);
if (!flow) throw new Error("还没有工作流计划");
const result = removeRepeatableInstance({ flow, stepId: id, catalog });
if ("error" in result) throw new Error(result.error);
this.writeCreationFlow(result.flow);
this.forgetDeletedCreationStep(result.removed, result.flow, catalog);
this.onMessage(`已删除「${result.removed.name}」产物 ${result.removed.id}。`);
debugLog("step", `删除产物 ${result.removed.id}`);
return this.session;
}
private forgetDeletedCreationStep(
step: CreationFlowStep,
flow: CreationFlow,
catalog: ModuleCatalog | null,
): void {
const unitId = step.id;
const accepted = this.readAcceptedStepIds().filter((item) => item !== unitId);
const acceptedRaw = JSON.stringify(accepted);
this.blackboard.write({
tag: CREATION_ACCEPTED_UNITS_TAG,
content: acceptedRaw,
source: "runtime",
});
const contentRaw = dropAcceptedUnitContent(
this.blackboard.getContentByTag(CREATION_ACCEPTED_CONTENT_TAG),
unitId,
);
this.blackboard.write({
tag: CREATION_ACCEPTED_CONTENT_TAG,
content: contentRaw,
source: "runtime",
});
const now = new Date().toISOString();
const artifacts = this.session.artifacts.map((a) =>
a.stepId === unitId && a.status !== "rejected"
? { ...a, status: "rejected" as const, updatedAt: now }
: a,
);
const remainingSameModule = accepted.filter((item) => {
const other = findStepByRef(flow, item);
return other?.name === step.name;
}).length;
const tag = findModuleByName(catalog, step.name)?.artifact?.trim();
if (tag) {
const pruned = pruneRepeatableArtifactContent({
raw: this.blackboard.getContentByTag(tag) ?? "",
step,
remainingSameModule,
});
this.blackboard.write({
tag,
content: pruned,
source: "runtime",
});
}
const pinned = this.readPinnedStepId();
this.session = {
...this.session,
artifacts,
currentStepId: pinned === unitId ? undefined : this.session.currentStepId,
slots: {
...this.session.slots,
[SLOT_CREATION_ACCEPTED_UNITS]: accepted,
[CREATION_ACCEPTED_UNITS_TAG]: acceptedRaw,
[CREATION_ACCEPTED_CONTENT_TAG]: contentRaw,
...(tag ? { [tag]: this.blackboard.getContentByTag(tag) ?? "" } : {}),
},
updatedAt: now,
};
if (pinned === unitId) this.clearCurrentStepPointers();
}
private writeCreationFlow(flow: CreationFlow): void {
const content = stringifyCreationFlow(flow);
this.blackboard.write({
@@ -1164,7 +1358,7 @@ export class PhaseRuntime {
case "run_worker":
if (
effect.workerId === DESIGN_STEP_WORKER_ID &&
!this.hasPinnedReadyStep()
!this.canRunPinnedDesignStep()
) {
await this.awaitCreationStepPick();
break;
@@ -1273,6 +1467,29 @@ export class PhaseRuntime {
return isStepReady(flow, step, this.readAcceptedStepIds());
}
/** 已验收节点回头改:isStepReady 为 false,但仍须跑 worker,不能弹回分层图。 */
private canRunPinnedDesignStep(): boolean {
if (this.hasPinnedReadyStep()) return true;
if (typeof this.session.slots.revisionTargetArtifactId === "string") {
return Boolean(this.session.slots.revisionTargetArtifactId.trim());
}
if (
isInheritExistingFlag(
this.blackboard.getContentByTag(CREATION_INHERIT_EXISTING_TAG) ??
this.session.slots[CREATION_INHERIT_EXISTING_TAG],
)
) {
return true;
}
const flow = parseCreationFlow(
this.blackboard.getContentByTag(CREATION_FLOW_TAG),
);
const step = flow
? findStepByRef(flow, this.readPinnedStepId())
: null;
return Boolean(step && isReviseStep(step));
}
private pinCreationStepSnapshot(params: {
stepId: string;
name: string;
@@ -1371,15 +1588,24 @@ export class PhaseRuntime {
};
flow = spawned.flow;
step = spawned.step;
} else if (reenter) {
if (!isStepAccepted(step, accepted)) {
throw new Error(`「${step.name}」还没完成,请直接进入而不是回头修改`);
}
} else if (!isStepReady(flow, step, accepted)) {
} else if (!reenter && !isStepReady(flow, step, accepted)) {
throw new Error(`「${step.name}」的前置节点还没完成,不能开始`);
}
const pendingModule = findModuleByName(catalog, step.name);
if (reenter) {
const artTag = pendingModule?.artifact?.trim();
const hasArt = Boolean(
artTag && this.blackboard.getContentByTag(artTag)?.trim(),
);
if (
!isStepAccepted(step, accepted) &&
!hasArt &&
!this.findReenterArtifact(step.id, artTag)
) {
throw new Error(`「${step.name}」还没完成,请直接进入而不是回头修改`);
}
}
const paramSpecs = pendingModule?.params ?? [];
const auto = pendingModule?.auto === true;
if (opts?.stepParams && Object.keys(opts.stepParams).length > 0) {
@@ -1417,6 +1643,21 @@ export class PhaseRuntime {
const waitingPick =
this.session.waitingReason?.kind === "pick_creation_step";
const needsParamForm = paramsMissing.length > 0 && !auto;
if (reenter) {
const existing = this.ensureReenterArtifact(
pinned.id,
pendingModule?.artifact,
);
if (existing) {
this.reopenArtifactForRevision(existing, pinned);
return;
}
throw new Error(
`找不到「${pinned.name}」的已有产物,无法回头修改。要新开一条请点原型上的「增殖」。`,
);
}
if (needsParamForm) {
const decision = createDecision({
action: "run_worker",
@@ -1453,6 +1694,101 @@ export class PhaseRuntime {
});
}
/** 回头修改:打开该步已有产物,让用户像未验收时一样写意见再改。 */
private findReenterArtifact(
stepId: string,
artifactTag?: string,
): ArtifactRecord | undefined {
const id = stepId.trim();
const tag = artifactTag?.trim();
if (!id && !tag) return undefined;
const usable = this.session.artifacts.filter(
(a) => a.workerId === DESIGN_STEP_WORKER_ID && a.status !== "rejected",
);
const byStep = id ? usable.filter((a) => a.stepId === id) : [];
const byTag = tag
? usable.filter((a) => a.outputTags.includes(tag))
: [];
const matches = byStep.length ? byStep : byTag;
const accepted = matches.filter((a) => a.status === "accepted");
const pool = accepted.length ? accepted : matches;
return pool.at(-1);
}
private ensureReenterArtifact(
stepId: string,
artifactTag?: string,
): ArtifactRecord | undefined {
const found = this.findReenterArtifact(stepId, artifactTag);
if (found) return found;
const tag = artifactTag?.trim();
let content = tag
? this.blackboard.getContentByTag(tag)?.trim() || ""
: "";
if (!content) {
content = this.readAcceptedUnitSlice(stepId);
if (content && tag) {
this.blackboard.write({
tag,
content,
source: "runtime",
});
}
}
if (!tag || !content) return undefined;
const artifact = createArtifact({
workerId: DESIGN_STEP_WORKER_ID,
stepId,
outputTags: [tag],
summary: "既有产物",
});
this.session = {
...this.session,
artifacts: [...this.session.artifacts, artifact],
};
return artifact;
}
private readAcceptedUnitSlice(stepId: string): string {
const store = parseAcceptedContentStore(
this.blackboard.getContentByTag(CREATION_ACCEPTED_CONTENT_TAG),
);
const entry = store.units[normalizeCreationUnitId(stepId)];
if (!entry || entry.content == null) return "";
return typeof entry.content === "string"
? entry.content.trim()
: JSON.stringify(entry.content, null, 2).trim();
}
private reopenArtifactForRevision(
artifact: ArtifactRecord,
step: { id: string; name: string },
): void {
const now = new Date().toISOString();
this.session = {
...this.session,
phase: "waiting_user",
waitingReason: { kind: "review_artifact", artifactId: artifact.id },
pendingArtifactId: artifact.id,
currentWorkerId: DESIGN_STEP_WORKER_ID,
currentStepId: step.id,
pendingDecision: undefined,
artifacts: this.session.artifacts.map((a) =>
a.id === artifact.id
? { ...a, status: "under_review", updatedAt: now }
: a,
),
slots: {
...this.session.slots,
revisionTargetArtifactId: artifact.id,
},
updatedAt: now,
};
this.onMessage(
`已回到「${step.name}」,可按意见修改现有产物(继承原文),或返回节点再选别的步骤。`,
);
}
/**
* 验收后回到分层图:点节点即确认并进入,底栏写意见再发即改编排。
*/
@@ -2204,7 +2540,7 @@ export class PhaseRuntime {
});
if (!result) {
this.onMessage(
`[旁观维护] need_generate=${need.rule_id}:未找到可抽样的池(请检查设计.生成规则)`,
`[旁观维护] need_generate=${need.rule_id}:无可程序抽样的元素池(方向池由填写方按规则自创)`,
);
return;
}
@@ -2350,48 +2686,6 @@ export class PhaseRuntime {
if (phase === "answered") return false;
// 配方开局模块 + 用户首句已在「用户.需求」→ 默认视为 opening 已做完,直接进 LLM
const demand = (
this.blackboard.getContentByTag("用户.需求") ??
this.session.slots["用户.需求"] ??
""
)
.toString()
.trim();
if (
shouldSkipModuleOpening({
demand,
dependsOn: binding.step.depends_on,
phase,
})
) {
state[stepKey] = "answered";
const raw = stringifyModuleOpeningState(state);
this.blackboard.write({
tag: CREATION_MODULE_OPENING_STATE_TAG,
content: raw,
source: "runtime",
});
this.blackboard.write({
tag: CREATION_MODULE_OPENING_TAG,
content: "(已跳过默认问题:用户首句见「用户.需求」)",
source: "runtime",
});
this.session = {
...this.session,
slots: {
...this.session.slots,
[SLOT_CREATION_MODULE_OPENING_STATE]: raw,
[CREATION_MODULE_OPENING_TAG]:
"(已跳过默认问题:用户首句见「用户.需求」)",
},
};
this.onMessage(
`[系统] ${binding.module.name} · 已坐在配方开局步,首句见「用户.需求」,跳过默认问题`,
);
return false;
}
if (phase === "shown" && this.session.resumeContext) {
// 用户刚答完默认问题 → 标记已答,继续走 LLM
state[stepKey] = "answered";
@@ -2412,26 +2706,7 @@ export class PhaseRuntime {
return false;
}
if (phase === "shown" && !this.session.resumeContext) {
// 异常重入:开场已发过但未走 resume;避免死循环,直接进 LLM
state[stepKey] = "answered";
const raw = stringifyModuleOpeningState(state);
this.blackboard.write({
tag: CREATION_MODULE_OPENING_STATE_TAG,
content: raw,
source: "runtime",
});
this.session = {
...this.session,
slots: {
...this.session.slots,
[SLOT_CREATION_MODULE_OPENING_STATE]: raw,
},
};
return false;
}
// 首次:发出默认问题
// 首次,或开场已发出但用户还没答(离开节点 / 恢复丢了 resume):展示引导,等用户先说一轮
state[stepKey] = "shown";
const raw = stringifyModuleOpeningState(state);
this.blackboard.write({
+58 -1
View File
@@ -1410,6 +1410,31 @@ export class SessionManager {
return this.toView(id);
}
/** 中止当前生成,不重跑;回到生成前的等待面或分层图。 */
async abortRun(id: string): Promise<SessionView> {
const s = this.require(id);
const session = s.runtime.getSession();
if (session.phase !== "running" || session.waitingReason) {
return this.toView(id);
}
const cutoff = s.runMessageCutoff ?? s.messages.length;
s.messages = s.messages.slice(0, cutoff);
try {
const owned = await this.runExclusive(id, s, async () => {
s.runtime.abortCurrentRun();
});
if (!owned) return this.toView(id);
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
console.error("[会话] 停止生成失败", detail);
await this.recoverFailedRun(s, detail);
}
s.messages.push(this.msg("system", "已停止生成。"));
this.syncCreationDialogue(s);
this.persist(s);
return this.toView(id);
}
/** 删除消息及其后的对话 */
async deleteMessage(id: string, messageId: string): Promise<SessionView> {
const s = this.require(id);
@@ -1686,6 +1711,27 @@ export class SessionManager {
return this.toView(id);
}
async deleteStep(id: string, stepId: string): Promise<SessionView> {
const s = this.require(id);
try {
const owned = await this.runExclusive(id, s, async () => {
await this.confirmPendingFlowPlan(s);
const waiting = s.runtime.getSession().waitingReason?.kind;
if (waiting !== "pick_creation_step") {
throw new Error("当前不是删除产物的时机");
}
await s.runtime.deleteCreationStep(stepId);
});
if (!owned) return this.toView(id);
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
console.error("[会话] 删除产物失败", detail);
s.messages.push(this.msg("system", formatRuntimeError(detail)));
}
this.persist(s);
return this.toView(id);
}
/** 已点进技能步、尚未验收:可退回分层图(误点可增殖节点时用) */
private canOfferLeaveCreationStep(
s: ManagedSession,
@@ -1699,10 +1745,21 @@ export class SessionManager {
return false;
}
const kind = reason?.kind;
if (session.phase === "running" && !kind) {
return Boolean(
s.runtime.getBlackboard().getContentByTag(CREATION_FLOW_TAG)?.trim(),
);
}
if (kind === "review_artifact") {
return Boolean(
s.runtime.getBlackboard().getContentByTag(CREATION_FLOW_TAG)?.trim(),
);
}
if (
kind !== "worker_questions" &&
kind !== "input" &&
kind !== "revision"
kind !== "revision" &&
kind !== "approve_step"
) {
return false;
}
+9
View File
@@ -515,6 +515,12 @@ const server = createServer(async (req, res) => {
typeof body.moduleName === "string" ? body.moduleName : "",
);
break;
case "delete_step":
view = await sessionManager.deleteStep(
sessionId,
typeof body.stepId === "string" ? body.stepId : "",
);
break;
case "run_outline":
view = await sessionManager.runOutline(sessionId);
break;
@@ -524,6 +530,9 @@ const server = createServer(async (req, res) => {
case "retry_run":
view = await sessionManager.abortAndRetry(sessionId);
break;
case "abort_run":
view = await sessionManager.abortRun(sessionId);
break;
default:
json(res, 400, { error: "未知 action" });
return;
+95 -17
View File
@@ -1224,21 +1224,6 @@ export function creationFlowFromRecipeSeed(
};
}
/**
* 已坐在配方开局步(无 depends_on)且「用户.需求」有首句时:
* 跳过程序再抛模块 opening——首句即该步对话,不是「先开场再进 DAG」。
*/
export function shouldSkipModuleOpening(opts: {
demand: string;
dependsOn?: string[] | null;
phase?: "shown" | "answered" | null;
}): boolean {
if (opts.phase === "shown" || opts.phase === "answered") return false;
if (!String(opts.demand ?? "").trim()) return false;
const deps = opts.dependsOn ?? [];
return deps.length === 0;
}
/** 无 id 时的兜底(目录仍应显式写 id) */
function slugFromName(name: string): string {
const map: Record<string, string> = {
@@ -2623,17 +2608,110 @@ export function removeUnstartedInstance(
if (!step || !isInstanceStep(step) || isStepAccepted(step, acceptedStepIds)) {
return flow;
}
return dropInstanceFromFlow(flow, step.id);
}
function dropInstanceFromFlow(flow: CreationFlow, stepId: string): CreationFlow {
return {
...flow,
steps: flow.steps
.filter((s) => s.id !== step.id)
.filter((s) => s.id !== stepId)
.map((s) => ({
...s,
depends_on: s.depends_on.filter((d) => d !== step.id),
depends_on: s.depends_on.filter((d) => d !== stepId),
})),
};
}
function repeatableInstanceLabel(step: CreationFlowStep): string {
const target = titleFromStepParams(step.params);
if (target && target !== step.name) return `${step.name} · ${target}`;
return step.id !== step.name ? `${step.name}(${step.id})` : step.name;
}
/**
* 图上删除可增殖产物:已验收也可删。
* 另有实例依赖这条时拒绝;收口等一次性节点只解开依赖。
*/
export function removeRepeatableInstance(params: {
flow: CreationFlow;
stepId: string;
catalog?: ModuleCatalog | null;
}): { flow: CreationFlow; removed: CreationFlowStep } | { error: string } {
const step = findStepByRef(params.flow, params.stepId.trim());
if (!step) return { error: "找不到节点" };
if (!isInstanceStep(step)) {
return { error: "只能删除可增殖能力长出来的产物" };
}
const mod = findModuleByName(params.catalog, step.name);
if (mod && mod.repeatable !== true) {
return { error: `「${step.name}」不是可增殖能力,不能从图上删` };
}
if (!mod && !step.from) {
return { error: "只能删除可增殖能力长出来的产物" };
}
const blockers = params.flow.steps.filter(
(s) =>
s.id !== step.id &&
isInstanceStep(s) &&
s.depends_on.includes(step.id),
);
if (blockers.length) {
return {
error: `还有产物依赖这条:${blockers.map(repeatableInstanceLabel).join("、")}。请先删那些产物。`,
};
}
return { flow: dropInstanceFromFlow(params.flow, step.id), removed: step };
}
/** 共享产物 tag 里按 rule_id 抽掉已删实例;同能力已无实例则清空。 */
export function pruneRepeatableArtifactContent(params: {
raw: string;
step: CreationFlowStep;
remainingSameModule: number;
}): string {
if (params.remainingSameModule <= 0) return "";
const stripped = stripFragmentRowsMatchingStep(params.raw, params.step);
return stripped ?? params.raw;
}
function stripFragmentRowsMatchingStep(
raw: string,
step: CreationFlowStep,
): string | null {
const parsed = tryParseJsonDoc(raw);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return null;
}
const doc = structuredClone(parsed) as Record<string, unknown>;
const bodyRaw = doc.正文;
if (!bodyRaw || typeof bodyRaw !== "object" || Array.isArray(bodyRaw)) {
return null;
}
const body = bodyRaw as Record<string, unknown>;
const ruleId = flowParamText(step.params, "rule_id");
if (!ruleId) return null;
let changed = false;
for (const key of ["rules", "records"] as const) {
const rows = body[key];
if (!Array.isArray(rows)) continue;
const next = rows.filter((item) => {
if (!item || typeof item !== "object" || Array.isArray(item)) return true;
return (
String((item as Record<string, unknown>).rule_id ?? "").trim() !==
ruleId
);
});
if (next.length !== rows.length) {
body[key] = next;
changed = true;
}
}
if (!changed) return null;
doc.正文 = body;
return JSON.stringify(doc, null, 2);
}
/** 实例步 depends_on:具体实例钉最近已验收规则实例;其余跟原型。 */
function resolveInstanceDependsOn(params: {
flow: CreationFlow;
+11
View File
@@ -622,6 +622,17 @@ export function extractUnitContentFromDraft(
return null;
}
/** 从图上删除产物时拿掉该单位的验收切片 */
export function dropAcceptedUnitContent(
existingRaw: unknown,
unitId: string,
): string {
const store = parseAcceptedContentStore(existingRaw);
const id = normalizeCreationUnitId(unitId);
if (id) delete store.units[id];
return JSON.stringify(store, null, 2);
}
/** 写入/覆盖某一单位的最后验收内容 */
export function upsertAcceptedUnitContent(
existingRaw: unknown,
+31
View File
@@ -13,6 +13,36 @@ export const MAINTAIN_GENERATE_TAG = "运行.本轮.旁观.生成抽样";
type PoolEntry = { id?: string; 内容?: string; 权重?: number; weight?: number };
const DIRECTION_KINDS = new Set(["方向", "方向池", "type", "类型", "类型池"]);
const ELEMENT_KINDS = new Set(["元素", "元素池", "enum", "枚举", "枚举池"]);
function poolKindToken(pool: Record<string, unknown>): string | undefined {
return (
asString(pool.池型) ??
asString(pool.kind) ??
asString(pool.类型) ??
asString(pool.pool_type)
);
}
/** 方向池给 LLM 当创作范围,不能程序加权抽。 */
export function isElementPool(pool: Record<string, unknown>): boolean {
const kind = poolKindToken(pool);
if (kind && DIRECTION_KINDS.has(kind)) return false;
if (kind && ELEMENT_KINDS.has(kind)) return true;
const dirs = pool.方向;
const hasDirection =
(dirs != null &&
typeof dirs === "object" &&
!Array.isArray(dirs) &&
Object.keys(dirs as object).length > 0) ||
(Array.isArray(dirs) && dirs.length > 0);
const entries = pool.条目 ?? pool.entries;
const hasEntries = Array.isArray(entries) && entries.length > 0;
if (hasDirection && !hasEntries) return false;
return hasEntries;
}
function asString(v: unknown): string | undefined {
if (typeof v !== "string") return undefined;
const t = v.trim();
@@ -61,6 +91,7 @@ export function buildChanceBatchFromGenerationRule(
for (const poolRaw of pools) {
if (!poolRaw || typeof poolRaw !== "object" || Array.isArray(poolRaw)) continue;
const pool = poolRaw as Record<string, unknown>;
if (!isElementPool(pool)) continue;
const poolId =
asString(pool.pool_id) ?? asString(pool.池id) ?? asString(pool.名称);
const entries = (pool.条目 ?? pool.entries) as PoolEntry[] | undefined;