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:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user