完善创作节点循环与等待态交互,并大幅打磨 Web 壳与会话运行时。
- 节点循环:配方开局直坐 DAG 第一步;验收后先问下一步意向,再确认开干并可钉编排参数;「按意见修改」只重跑当前节点。 - 询问分流:能力 opening 走说话面引导,可跳过追问按题干去重;追问与产物同轮挂载,避免拆成两段历史。 - 运行时:补强 revision 重跑、创作流程合并/进度指针、工具循环与 worker 执行;新增运行日志与 web:watch。 - 前端:统一等待态文案与底栏主按钮,完善询问卡/产物验收/呈现壳样式与交互。 - 同步世界模拟器模块提示、编排文档与相关测试。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,7 +12,12 @@ import {
|
||||
displayWorkerLabel,
|
||||
formatAgentDisplayTitle,
|
||||
formatWorkerDisplayTitle,
|
||||
reviewComposerCopy,
|
||||
} from "./display-labels.js";
|
||||
import {
|
||||
isModuleOpeningQuestions,
|
||||
normalizeQuestions,
|
||||
} from "../skills/question-protocol.js";
|
||||
|
||||
export type AgentMessageKind =
|
||||
| "user_input"
|
||||
@@ -167,6 +172,20 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
|
||||
};
|
||||
}
|
||||
|
||||
// 能力默认问题:正文在主栏说话面,调度消息只留短提示
|
||||
const moduleOpeningMsg = trimmed.match(
|
||||
/^\[Worker\]\s*(\S+)\s*·\s*默认问题/,
|
||||
);
|
||||
if (moduleOpeningMsg) {
|
||||
return {
|
||||
kind: "orchestrator_prompt",
|
||||
actor: moduleOpeningMsg[1],
|
||||
title: formatWorkerDisplayTitle(moduleOpeningMsg[1], "questions"),
|
||||
body: "默认问题(请在主栏按引导作答)",
|
||||
text: trimmed,
|
||||
};
|
||||
}
|
||||
|
||||
const workerAskTagged = trimmed.match(
|
||||
/^\[Worker\]\s*(\S+)\s*提问[::]\s*\n?([\s\S]*)$/,
|
||||
);
|
||||
@@ -240,7 +259,7 @@ export function classifyAgentMessage(text: string): EnrichedMessage {
|
||||
title: "可选追问",
|
||||
body: formatWorkerQuestionBody(
|
||||
trimmed.replace(
|
||||
/^\[Worker\]\s*可选追问(可跳过,直接接受(?:目前)?产物)[::]\s*/,
|
||||
/^\[Worker\]\s*可选追问(可跳过(?:,直接[^)]+)?)[::]\s*/,
|
||||
"",
|
||||
),
|
||||
),
|
||||
@@ -387,19 +406,52 @@ export function buildFocus(
|
||||
|
||||
if (reason?.kind === "approve_step") {
|
||||
const worker = session.pendingDecision?.workerId ?? "skill";
|
||||
const proposed =
|
||||
typeof session.slots["创作.待确认步骤"] === "string"
|
||||
? (() => {
|
||||
try {
|
||||
return JSON.parse(String(session.slots["创作.待确认步骤"])) as {
|
||||
name?: string;
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})()
|
||||
: null;
|
||||
return {
|
||||
actorType: "orchestrator",
|
||||
actorId: "orchestrator",
|
||||
actorLabel: "总管",
|
||||
action: `建议调用 ${displayWorkerLabel(worker)}`,
|
||||
actorLabel: "编排",
|
||||
action: proposed?.name
|
||||
? `确认开始 · ${proposed.name}`
|
||||
: `建议调用 ${displayWorkerLabel(worker)}`,
|
||||
detail: session.pendingDecision?.reason,
|
||||
};
|
||||
}
|
||||
|
||||
if (reason?.kind === "next_intent") {
|
||||
return {
|
||||
actorType: "user",
|
||||
actorLabel: "你",
|
||||
action: "下一步想写什么",
|
||||
detail: "可留空;发送后会展示下一节点供确认开干",
|
||||
};
|
||||
}
|
||||
|
||||
if (reason?.kind === "worker_questions") {
|
||||
const qs = normalizeQuestions(reason.questions);
|
||||
if (isModuleOpeningQuestions(qs)) {
|
||||
return {
|
||||
actorType: "user",
|
||||
actorId: reason.workerId,
|
||||
actorLabel: "你",
|
||||
action: "按引导先说几句",
|
||||
detail: (qs[0]?.prompt ?? "").slice(0, 200) || "想到什么写什么",
|
||||
};
|
||||
}
|
||||
const q =
|
||||
reason.questions
|
||||
?.map((item) => item.prompt)
|
||||
qs
|
||||
.map((item) => item.prompt)
|
||||
.filter((s) => s?.trim())
|
||||
.join(";") ?? "";
|
||||
return {
|
||||
@@ -413,13 +465,16 @@ export function buildFocus(
|
||||
|
||||
if (reason?.kind === "review_artifact") {
|
||||
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
|
||||
const copy = reviewComposerCopy(art?.workerId, {
|
||||
hasQuestions: Boolean(reason.questions?.length),
|
||||
});
|
||||
const optionalQs = reason.questions?.length
|
||||
? `;另有 ${reason.questions.length} 道可选追问`
|
||||
: "";
|
||||
return {
|
||||
actorType: "user",
|
||||
actorLabel: "你",
|
||||
action: "验收产物",
|
||||
action: copy.taskTitle,
|
||||
detail: `${art?.summary ?? displayWorkerLabel(art?.workerId) ?? ""}${optionalQs}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -122,3 +122,84 @@ export function formatAgentDisplayTitle(detail?: string): string {
|
||||
if (detail?.trim()) return `编排器 · ${detail.trim()}`;
|
||||
return "编排器";
|
||||
}
|
||||
|
||||
/** 底栏双按钮 + 工作面 kicker:随 waiting 产物种类变化 */
|
||||
export type ReviewComposerCopy = {
|
||||
/** 底栏短芯片 */
|
||||
chip: string;
|
||||
/** 工作面左上角 */
|
||||
kicker: string;
|
||||
taskLabel: string;
|
||||
taskTitle: string;
|
||||
hint: string;
|
||||
submitLabel: string;
|
||||
acceptLabel: string;
|
||||
placeholder: string;
|
||||
emptyEnterHint: string;
|
||||
/** plan=编排确认(主色);accept=产物验收(绿) */
|
||||
tone: "plan" | "accept";
|
||||
kind: "flow" | "opening" | "accept";
|
||||
};
|
||||
|
||||
/**
|
||||
* `review_artifact` 的用户文案。流程编排核对的是工作流计划,不是「验收通过」。
|
||||
*/
|
||||
export function reviewComposerCopy(
|
||||
workerId: string | undefined | null,
|
||||
opts?: { hasQuestions?: boolean },
|
||||
): ReviewComposerCopy {
|
||||
const hasQuestions = Boolean(opts?.hasQuestions);
|
||||
const id = (workerId ?? "").trim();
|
||||
|
||||
if (id === "design-flow") {
|
||||
return {
|
||||
chip: "编排",
|
||||
kicker: "核对编排",
|
||||
taskLabel: "编排",
|
||||
taskTitle: "核对工作流计划",
|
||||
hint: hasQuestions
|
||||
? "同意就按这个排;要改就写意见,在现有计划上改。下方追问可选答。"
|
||||
: "同意就按这个排;要改就写意见,在现有计划上改。",
|
||||
submitLabel: "按意见改编排",
|
||||
acceptLabel: "确认编排",
|
||||
placeholder: "改编排意见…",
|
||||
emptyEnterHint: "空 Enter=确认编排",
|
||||
tone: "plan",
|
||||
kind: "flow",
|
||||
};
|
||||
}
|
||||
|
||||
if (id === "opening-generator") {
|
||||
return {
|
||||
chip: "开场",
|
||||
kicker: "待选定",
|
||||
taskLabel: "开场",
|
||||
taskTitle: "选定开场白",
|
||||
hint: hasQuestions
|
||||
? "同意就选定此开场;要改就写意见。下方追问可选答。"
|
||||
: "同意就选定此开场;要改就写意见。",
|
||||
submitLabel: "按意见修改",
|
||||
acceptLabel: "选定此开场",
|
||||
placeholder: "修改意见…",
|
||||
emptyEnterHint: "空 Enter=选定此开场",
|
||||
tone: "plan",
|
||||
kind: "opening",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
chip: "验收",
|
||||
kicker: "待验收",
|
||||
taskLabel: "验收",
|
||||
taskTitle: "验收产物",
|
||||
hint: hasQuestions
|
||||
? "同意就接受;要改就写意见,在现有产物上改。下方追问可选答。"
|
||||
: "同意就接受;要改就写意见,在现有产物上改。",
|
||||
submitLabel: "按意见修改",
|
||||
acceptLabel: "接受",
|
||||
placeholder: "修改意见…",
|
||||
emptyEnterHint: "空 Enter=接受",
|
||||
tone: "accept",
|
||||
kind: "accept",
|
||||
};
|
||||
}
|
||||
|
||||
+428
-48
@@ -63,7 +63,12 @@ import type { BlackboardItem } from "../types/blackboard.js";
|
||||
import { Blackboard } from "../blackboard/blackboard.js";
|
||||
import { parseWorkerSetYaml } from "../skills/worker-set-parse.js";
|
||||
import { parseShellAdaptationFromReplyFormat } from "../skills/present-packet.js";
|
||||
import { parseAcceptedUnits, isDesignDiskWorker } from "../skills/creation-units.js";
|
||||
import {
|
||||
CREATION_ACCEPTED_UNITS_TAG,
|
||||
isDesignDiskWorker,
|
||||
parseAcceptedUnits,
|
||||
SLOT_CREATION_ACCEPTED_UNITS,
|
||||
} from "../skills/creation-units.js";
|
||||
import {
|
||||
formatWorkerSetForUser,
|
||||
type WorkerSetUserView,
|
||||
@@ -78,13 +83,21 @@ import {
|
||||
type ContextOrderEdit,
|
||||
} from "../skills/context-order.js";
|
||||
import {
|
||||
CREATION_CURRENT_STEP_TAG,
|
||||
CREATION_FLOW_TAG,
|
||||
CREATION_PROPOSED_STEP_TAG,
|
||||
CREATION_SELECTED_RECIPE_TAG,
|
||||
creationFlowFromRecipeSeed,
|
||||
findRecipeCatalogEntry,
|
||||
formatCreationFlowForUser,
|
||||
isProgressPointerTag,
|
||||
loadRecipeCatalog,
|
||||
loadRecipeDetail,
|
||||
parseCreationFlow,
|
||||
parseSelectedRecipeRef,
|
||||
resolveDesignStepBinding,
|
||||
stepUnitId,
|
||||
stringifyCreationFlow,
|
||||
type CreationFlowUserView,
|
||||
type RecipeCatalogEntry,
|
||||
} from "../skills/creation-flow.js";
|
||||
@@ -94,6 +107,7 @@ import { buildBoardPanel } from "../runtime/compress-after-worker.js";
|
||||
import {
|
||||
displayWorkerLabel,
|
||||
formatWorkerDisplayTitle,
|
||||
reviewComposerCopy,
|
||||
} from "./display-labels.js";
|
||||
import {
|
||||
buildCreationDialogueTranscript,
|
||||
@@ -102,6 +116,7 @@ import {
|
||||
import {
|
||||
formatQuestionAnswersForAi,
|
||||
formatQuestionAnswersForDisplay,
|
||||
isModuleOpeningQuestions,
|
||||
normalizeQuestions,
|
||||
} from "../skills/question-protocol.js";
|
||||
import type { QuestionAnswer } from "../types/questions.js";
|
||||
@@ -180,10 +195,16 @@ export type SessionView = {
|
||||
workerSetView?: WorkerSetUserView;
|
||||
/** 创作流程用户视图(步骤顺序 + 依赖) */
|
||||
creationFlowView?: CreationFlowUserView;
|
||||
/** 确认开干:下一步提案(含可编辑编排参数) */
|
||||
proposedNextStep?: ProposedNextStepView;
|
||||
/** 能力包内可选初始配方(用户手动选) */
|
||||
recipes?: Array<{ id: string; name: string; declaration: string }>;
|
||||
/** 用户已选初始配方 */
|
||||
selectedRecipe?: { id: string; name: string; declaration: string } | null;
|
||||
/**
|
||||
* 开局引导:配方起点或当前能力默认问题的 opening(说话面主柱;对齐美学纲领)。
|
||||
*/
|
||||
openingGuide?: { stepId: string; stepName: string; text: string } | null;
|
||||
/** 黑板与定稿上下文(强可读) */
|
||||
boardPanel?: BoardPanelView;
|
||||
/** 游玩呈现壳微调(来自设计.正文组成) */
|
||||
@@ -238,6 +259,21 @@ export type BoardPanelView = {
|
||||
brief?: string;
|
||||
};
|
||||
|
||||
export type ProposedNextStepView = {
|
||||
stepId: string;
|
||||
name: string;
|
||||
declaration?: string;
|
||||
params: Record<string, unknown>;
|
||||
paramsMissing: string[];
|
||||
paramSpecs: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
required: boolean;
|
||||
hint?: string;
|
||||
}>;
|
||||
intent?: string;
|
||||
};
|
||||
|
||||
export type ReviewArtifactView = {
|
||||
id: string;
|
||||
workerId: string;
|
||||
@@ -263,14 +299,18 @@ type ManagedSession = {
|
||||
branchState: MessageBranchState;
|
||||
/** 当前能力包可选初始配方(用户手动选) */
|
||||
recipeOptions?: RecipeCatalogEntry[];
|
||||
/** 开局模块 opening,缓存给意图页 */
|
||||
openingGuide?: { stepId: string; stepName: string; text: string } | null;
|
||||
};
|
||||
|
||||
export class SessionManager {
|
||||
private readonly sessions = new Map<string, ManagedSession>();
|
||||
/** bookId → 当前内存中的 sessionId */
|
||||
private readonly activeBookSessions = new Map<string, string>();
|
||||
/** sessionId → 流式 thinking 缓冲 */
|
||||
/** sessionId → 流式 thinking 缓冲(思考结束后仍保留,直到本轮结束) */
|
||||
private readonly agentThinkingLive = new Map<string, string>();
|
||||
/** 本轮思维链已收束;下一次 delta 起新一轮 */
|
||||
private readonly agentThinkingClosed = new Set<string>();
|
||||
/** sessionId → worker 流式缓冲 */
|
||||
private readonly workerLive = new Map<
|
||||
string,
|
||||
@@ -679,7 +719,11 @@ export class SessionManager {
|
||||
return count;
|
||||
}
|
||||
|
||||
async sendMessage(id: string, text: string): Promise<SessionView> {
|
||||
async sendMessage(
|
||||
id: string,
|
||||
text: string,
|
||||
opts?: { answers?: QuestionAnswer[] },
|
||||
): Promise<SessionView> {
|
||||
const s = this.require(id);
|
||||
this.clearAgentThinking(id);
|
||||
recordPreMessageCheckpoint(
|
||||
@@ -687,21 +731,62 @@ export class SessionManager {
|
||||
s.messages.length,
|
||||
this.captureCheckpoint(s),
|
||||
);
|
||||
s.messages.push(this.msg("user", text));
|
||||
this.syncCreationDialogue(s);
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
const reason = s.runtime.getSession().waitingReason;
|
||||
const trimmed = text.trim();
|
||||
const sidecarAnswers =
|
||||
reason?.kind === "review_artifact" &&
|
||||
reason.questions?.length &&
|
||||
opts?.answers?.length
|
||||
? opts.answers.filter((a) => a.text?.trim() && a.text !== "(未答)")
|
||||
: [];
|
||||
|
||||
// next_intent 允许留空:不塞空白气泡
|
||||
if (trimmed || reason?.kind !== "next_intent" || sidecarAnswers.length) {
|
||||
let recordText = trimmed || text;
|
||||
let displayBody: string | undefined;
|
||||
if (sidecarAnswers.length && reason?.kind === "review_artifact") {
|
||||
const qs = normalizeQuestions(reason.questions ?? []);
|
||||
recordText = formatQuestionAnswersForAi(
|
||||
qs,
|
||||
sidecarAnswers,
|
||||
trimmed || undefined,
|
||||
);
|
||||
displayBody = formatQuestionAnswersForDisplay(
|
||||
qs,
|
||||
sidecarAnswers,
|
||||
trimmed || undefined,
|
||||
);
|
||||
}
|
||||
const userMsg = this.msg("user", recordText);
|
||||
if (displayBody) userMsg.body = displayBody;
|
||||
s.messages.push(userMsg);
|
||||
this.syncCreationDialogue(s);
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
}
|
||||
try {
|
||||
if (reason?.kind === "approve_step") {
|
||||
await s.runtime.rejectStep(text);
|
||||
await s.runtime.rejectStep(trimmed || text);
|
||||
} else if (reason?.kind === "review_artifact") {
|
||||
await s.runtime.rejectArtifact(text);
|
||||
// 先把卡上追问(问+选项+补充)写入「用户.worker答复」,再按意见改产物
|
||||
if (sidecarAnswers.length && reason.questions?.length) {
|
||||
const qaText = formatQuestionAnswersForAi(
|
||||
normalizeQuestions(reason.questions),
|
||||
sidecarAnswers,
|
||||
trimmed || undefined,
|
||||
);
|
||||
await s.runtime.resolveSidecarQuestions(qaText);
|
||||
}
|
||||
if (trimmed || sidecarAnswers.length) {
|
||||
await s.runtime.rejectArtifact(
|
||||
trimmed || "按追问作答更新产物",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await s.runtime.submitInput(text);
|
||||
await s.runtime.submitInput(trimmed || text);
|
||||
}
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[session] sendMessage failed:", detail);
|
||||
console.error("[会话] 发送消息失败", detail);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
} finally {
|
||||
@@ -755,12 +840,14 @@ export class SessionManager {
|
||||
try {
|
||||
if (isSidecar) {
|
||||
await s.runtime.resolveSidecarQuestions(aiText);
|
||||
// 作答必须写回产物,否则验收后进度空转、追问在美学/交互间打转
|
||||
await s.runtime.rejectArtifact("按追问作答更新产物");
|
||||
} else {
|
||||
await s.runtime.submitInput(aiText);
|
||||
}
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[session] answerQuestions failed:", detail);
|
||||
console.error("[会话] 回答追问失败", detail);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
} finally {
|
||||
@@ -960,13 +1047,16 @@ export class SessionManager {
|
||||
return this.toView(id);
|
||||
}
|
||||
|
||||
async approve(id: string): Promise<SessionView> {
|
||||
async approve(
|
||||
id: string,
|
||||
stepParams?: Record<string, unknown>,
|
||||
): Promise<SessionView> {
|
||||
const s = this.require(id);
|
||||
try {
|
||||
await s.runtime.approve();
|
||||
await s.runtime.approve(stepParams);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[session] approve failed:", detail);
|
||||
console.error("[会话] 确认开干失败", detail);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
}
|
||||
return this.toView(id);
|
||||
@@ -978,7 +1068,7 @@ export class SessionManager {
|
||||
await s.runtime.confirmIntake();
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[session] confirmIntake failed:", detail);
|
||||
console.error("[会话] 确认需求失败", detail);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
}
|
||||
@@ -1028,7 +1118,7 @@ export class SessionManager {
|
||||
}
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
console.error("[session] reject failed:", detail);
|
||||
console.error("[会话] 打回失败", detail);
|
||||
s.messages.push(this.msg("system", formatRuntimeError(detail)));
|
||||
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
|
||||
}
|
||||
@@ -1154,7 +1244,7 @@ export class SessionManager {
|
||||
payload: { text },
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[session] maybeKickAgentAfterDemand failed:", err);
|
||||
console.error("[会话] 需求后唤醒总管失败", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1214,7 +1304,7 @@ export class SessionManager {
|
||||
const transcript = buildCreationDialogueTranscript(s.messages);
|
||||
s.runtime.writeCreationDialogue(transcript);
|
||||
} catch (err) {
|
||||
console.warn("[session] syncCreationDialogue:", err);
|
||||
console.warn("[会话] 同步创作对话失败", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1274,6 +1364,7 @@ export class SessionManager {
|
||||
|
||||
private clearAgentThinking(sessionId: string): void {
|
||||
this.agentThinkingLive.delete(sessionId);
|
||||
this.agentThinkingClosed.delete(sessionId);
|
||||
this.workerLive.delete(sessionId);
|
||||
}
|
||||
|
||||
@@ -1287,15 +1378,20 @@ export class SessionManager {
|
||||
| "onWorkerStreamStart"
|
||||
| "onWorkerThinkingDelta"
|
||||
| "onWorkerOutputDelta"
|
||||
| "onWorkerStreamDone"
|
||||
> {
|
||||
return {
|
||||
onAgentThinkingDelta: (delta) => {
|
||||
const prev = this.agentThinkingLive.get(sessionId) ?? "";
|
||||
const prev = this.agentThinkingClosed.has(sessionId)
|
||||
? ""
|
||||
: (this.agentThinkingLive.get(sessionId) ?? "");
|
||||
this.agentThinkingClosed.delete(sessionId);
|
||||
this.agentThinkingLive.set(sessionId, prev + delta);
|
||||
},
|
||||
onAgentThinkingDone: () => {
|
||||
this.agentThinkingLive.delete(sessionId);
|
||||
onAgentThinkingDone: (text) => {
|
||||
// 正文仍等完整后再渲染;思考收束后继续挂在 liveStream,避免中间只剩「…」
|
||||
this.agentThinkingClosed.add(sessionId);
|
||||
const done = text.trim();
|
||||
if (done) this.agentThinkingLive.set(sessionId, done);
|
||||
},
|
||||
onWorkerStreamStart: (workerId) => {
|
||||
this.workerLive.set(sessionId, { workerId, thinking: "", output: "" });
|
||||
@@ -1322,9 +1418,6 @@ export class SessionManager {
|
||||
prev.output += delta;
|
||||
this.workerLive.set(sessionId, prev);
|
||||
},
|
||||
onWorkerStreamDone: () => {
|
||||
this.workerLive.delete(sessionId);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1603,47 +1696,94 @@ export class SessionManager {
|
||||
}
|
||||
} else if (reason?.kind === "worker_questions") {
|
||||
const qs = normalizeQuestions(reason.questions);
|
||||
if (qs.length) {
|
||||
if (isModuleOpeningQuestions(qs)) {
|
||||
// 能力默认问题:对齐美学纲领 → 自由书写,不是可跳过追问卡
|
||||
hints.push("按主栏引导先说几句(想到什么写什么,不必整齐)");
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: "发送",
|
||||
placeholder: "想到什么写什么…",
|
||||
});
|
||||
} else if (qs.length) {
|
||||
hints.push(
|
||||
`${displayWorkerLabel(reason.workerId)} · 提问(请在询问卡作答):${qs.map((q) => q.prompt).join(" ")}`,
|
||||
);
|
||||
actions.push({ type: "answer_questions", label: "提交作答" });
|
||||
actions.push({ type: "skip_questions", label: "跳过追问" });
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: "自由补充",
|
||||
placeholder: "也可在此自由补充…",
|
||||
});
|
||||
} else {
|
||||
hints.push(
|
||||
`${displayWorkerLabel(reason.workerId)} 需要更多信息,请补充说明`,
|
||||
);
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: "发送",
|
||||
placeholder: "补充说明…",
|
||||
});
|
||||
}
|
||||
actions.push({ type: "answer_questions", label: "提交作答" });
|
||||
actions.push({ type: "skip_questions", label: "跳过追问" });
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: "自由补充",
|
||||
placeholder: "也可在此自由补充…",
|
||||
});
|
||||
} else if (reason?.kind === "approve_step") {
|
||||
actions.push({ type: "approve", label: "确认执行" });
|
||||
const proposed = readProposedNextStep(
|
||||
s.runtime.getSession().slots,
|
||||
s.runtime.getBlackboard(),
|
||||
);
|
||||
if (proposed) {
|
||||
hints.push(
|
||||
`确认开始「${proposed.name}」${
|
||||
proposed.paramsMissing.length
|
||||
? `(请先补齐:${proposed.paramsMissing.join("、")})`
|
||||
: ""
|
||||
}`,
|
||||
);
|
||||
actions.push({
|
||||
type: "approve",
|
||||
label: proposed.paramsMissing.length ? "补参后开始" : "同意并开始",
|
||||
});
|
||||
} else {
|
||||
actions.push({ type: "approve", label: "确认执行" });
|
||||
}
|
||||
actions.push({ type: "reject", label: "暂不执行" });
|
||||
} else if (reason?.kind === "review_artifact") {
|
||||
actions.push({ type: "accept", label: "接受目前产物" });
|
||||
actions.push({ type: "reject", label: "不接受,重新来" });
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: "发送修改意见",
|
||||
placeholder: "输入修改意见后发送…",
|
||||
label: "说明意见",
|
||||
placeholder: "不同意可说明要改成什么…",
|
||||
});
|
||||
} else if (reason?.kind === "next_intent") {
|
||||
hints.push("下一步想写什么?可留空,发送后会展示下一节点供你确认。");
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: "继续",
|
||||
placeholder: "下一步想写什么?(可留空)…",
|
||||
});
|
||||
} else if (reason?.kind === "review_artifact") {
|
||||
const art = session.artifacts.find((a) => a.id === session.pendingArtifactId);
|
||||
const copy = reviewComposerCopy(art?.workerId, {
|
||||
hasQuestions: Boolean(reason.questions?.length),
|
||||
});
|
||||
actions.push({ type: "accept", label: copy.acceptLabel });
|
||||
actions.push({ type: "reject", label: "不接受" });
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: copy.submitLabel,
|
||||
placeholder: `${copy.placeholder};在现有产物上改,不整份重做…`,
|
||||
});
|
||||
if (reason.questions?.length) {
|
||||
hints.push("下方追问可选;接受产物即收起(表示无需再完善)");
|
||||
hints.push(`下方追问可选;${copy.acceptLabel}即收起(表示无需再完善)`);
|
||||
actions.push({ type: "answer_questions", label: "提交作答" });
|
||||
actions.push({ type: "skip_questions", label: "跳过追问" });
|
||||
}
|
||||
} else if (reason?.kind === "revision") {
|
||||
hints.push(
|
||||
reason.instruction?.trim() ||
|
||||
"需要按反馈修改后继续;请在下方说明或直接发送继续",
|
||||
"请说明要改哪里;发送后在现有产物上修改",
|
||||
);
|
||||
actions.push({
|
||||
type: "send_message",
|
||||
label: "发送",
|
||||
placeholder: "说明修改意见,或直接发送让总管继续…",
|
||||
label: "按意见修改",
|
||||
placeholder: "说明要改哪里…",
|
||||
});
|
||||
} else if (session.phase === "error") {
|
||||
hints.push("上一轮执行出错。可直接发消息让总管重试,或刷新作品后继续。");
|
||||
@@ -1737,7 +1877,17 @@ export class SessionManager {
|
||||
s.runtime.getBlackboard(),
|
||||
s.runtime.getSession(),
|
||||
),
|
||||
creationFlowView: buildCreationFlowUserView(s.runtime.getBlackboard()),
|
||||
creationFlowView: buildCreationFlowUserView(
|
||||
s.runtime.getBlackboard(),
|
||||
session.slots[SLOT_CREATION_ACCEPTED_UNITS],
|
||||
),
|
||||
proposedNextStep:
|
||||
reason?.kind === "approve_step"
|
||||
? readProposedNextStep(
|
||||
session.slots,
|
||||
s.runtime.getBlackboard(),
|
||||
)
|
||||
: undefined,
|
||||
recipes: s.recipeOptions?.map((r) => ({
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
@@ -1747,6 +1897,7 @@ export class SessionManager {
|
||||
s.runtime.getBlackboard(),
|
||||
s.recipeOptions,
|
||||
),
|
||||
openingGuide: this.resolveOpeningGuideForView(s, reason),
|
||||
boardPanel: buildBoardPanelFromRuntime(s.runtime.getBlackboard()),
|
||||
presentationTweaks: shellAdapt
|
||||
? {
|
||||
@@ -1839,12 +1990,14 @@ export class SessionManager {
|
||||
const skillName = s.runtime.getActiveSkill()?.name;
|
||||
if (!skillName) {
|
||||
s.recipeOptions = [];
|
||||
s.openingGuide = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const skill = await loadSkill(skillName);
|
||||
if (!skill.skillPackRoot) {
|
||||
s.recipeOptions = [];
|
||||
s.openingGuide = null;
|
||||
return;
|
||||
}
|
||||
const catalog = await loadRecipeCatalog(skill.skillPackRoot);
|
||||
@@ -1852,6 +2005,125 @@ export class SessionManager {
|
||||
} catch {
|
||||
s.recipeOptions = [];
|
||||
}
|
||||
await this.ensureRecipeSeededFlow(s);
|
||||
await this.refreshOpeningGuide(s);
|
||||
}
|
||||
|
||||
/** 已选配方但尚无流程 → 写入配方近期起点(兼容旧会话) */
|
||||
private async ensureRecipeSeededFlow(s: ManagedSession): Promise<void> {
|
||||
const board = s.runtime.getBlackboard();
|
||||
if (board.getContentByTag(CREATION_FLOW_TAG)?.trim()) return;
|
||||
const ref = parseSelectedRecipeRef(
|
||||
board.getContentByTag(CREATION_SELECTED_RECIPE_TAG),
|
||||
);
|
||||
if (!ref) return;
|
||||
const entry = findRecipeCatalogEntry(
|
||||
s.recipeOptions?.length ? { recipes: s.recipeOptions } : null,
|
||||
ref,
|
||||
);
|
||||
if (!entry) return;
|
||||
const skillName = s.runtime.getActiveSkill()?.name;
|
||||
if (!skillName) return;
|
||||
try {
|
||||
const skill = await loadSkill(skillName);
|
||||
const packRoot = skill.skillPackRoot?.trim();
|
||||
if (!packRoot) return;
|
||||
const detail = await loadRecipeDetail(packRoot, entry);
|
||||
const seeded = creationFlowFromRecipeSeed(detail);
|
||||
if (!seeded) return;
|
||||
board.write({
|
||||
tag: CREATION_FLOW_TAG,
|
||||
content: stringifyCreationFlow(seeded),
|
||||
source: "runtime",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[会话] 配方预置失败", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下一待做开局步(无 depends_on)的 opening → 意图页引导。
|
||||
* 有起点模块就展示;无则 null(再走 design-flow 生成)。
|
||||
* 中段步骤的默认问题改由 resolveOpeningGuideForView 在 waiting 时覆盖。
|
||||
*/
|
||||
private async refreshOpeningGuide(s: ManagedSession): Promise<void> {
|
||||
const skillName = s.runtime.getActiveSkill()?.name;
|
||||
if (!skillName) {
|
||||
s.openingGuide = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const skill = await loadSkill(skillName);
|
||||
const packRoot = skill.skillPackRoot?.trim();
|
||||
if (!packRoot) {
|
||||
s.openingGuide = null;
|
||||
return;
|
||||
}
|
||||
const board = s.runtime.getBlackboard();
|
||||
const session = s.runtime.getSession();
|
||||
const accepted = parseAcceptedUnits(
|
||||
session.slots[SLOT_CREATION_ACCEPTED_UNITS] ??
|
||||
board.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
||||
);
|
||||
const binding = await resolveDesignStepBinding({
|
||||
skillPackRoot: packRoot,
|
||||
flowRaw: board.getContentByTag(CREATION_FLOW_TAG),
|
||||
acceptedStepNames: accepted,
|
||||
});
|
||||
if (
|
||||
!binding?.opening?.trim() ||
|
||||
(binding.step.depends_on?.length ?? 0) > 0
|
||||
) {
|
||||
s.openingGuide = null;
|
||||
return;
|
||||
}
|
||||
s.openingGuide = {
|
||||
stepId: binding.step.id,
|
||||
stepName: binding.module.name || binding.step.name,
|
||||
text: binding.opening.trim(),
|
||||
};
|
||||
} catch {
|
||||
s.openingGuide = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图用开场引导:等待能力默认问题时用题干(对齐美学纲领说话面);
|
||||
* 中段步骤(有 depends_on)也会覆盖,避免只剩空答题壳。
|
||||
*/
|
||||
private resolveOpeningGuideForView(
|
||||
s: ManagedSession,
|
||||
reason: WaitingReason | undefined,
|
||||
): { stepId: string; stepName: string; text: string } | null {
|
||||
if (reason?.kind === "worker_questions") {
|
||||
const qs = normalizeQuestions(reason.questions);
|
||||
if (isModuleOpeningQuestions(qs)) {
|
||||
const text = qs[0]?.prompt?.trim() ?? "";
|
||||
if (text) {
|
||||
const board = s.runtime.getBlackboard();
|
||||
const stepKey =
|
||||
board.getContentByTag(CREATION_CURRENT_STEP_TAG)?.trim() || "";
|
||||
const flow = parseCreationFlow(board.getContentByTag(CREATION_FLOW_TAG));
|
||||
const step = flow?.steps?.find(
|
||||
(st) =>
|
||||
stepUnitId(st) === stepKey ||
|
||||
st.id === stepKey ||
|
||||
st.name === stepKey,
|
||||
);
|
||||
const stepName =
|
||||
step?.name?.trim() ||
|
||||
s.openingGuide?.stepName?.trim() ||
|
||||
stepKey ||
|
||||
"按引导先说几句";
|
||||
return {
|
||||
stepId: qs[0]!.id,
|
||||
stepName,
|
||||
text,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return s.openingGuide ?? null;
|
||||
}
|
||||
|
||||
private async applySelectedRecipe(
|
||||
@@ -1866,7 +2138,8 @@ export class SessionManager {
|
||||
if (!entry) {
|
||||
throw new Error(`未知初始配方:${recipeId}`);
|
||||
}
|
||||
s.runtime.getBlackboard().write({
|
||||
const board = s.runtime.getBlackboard();
|
||||
board.write({
|
||||
tag: CREATION_SELECTED_RECIPE_TAG,
|
||||
content: JSON.stringify({
|
||||
id: entry.id,
|
||||
@@ -1874,6 +2147,33 @@ export class SessionManager {
|
||||
}),
|
||||
source: "user",
|
||||
});
|
||||
|
||||
// 剧本已钉死开局模块 → 默认写入近期起点,开局即坐在 DAG 第一步(不必先跑 design-flow)
|
||||
const existingFlow = board.getContentByTag(CREATION_FLOW_TAG)?.trim();
|
||||
if (!existingFlow) {
|
||||
const skillName = s.runtime.getActiveSkill()?.name;
|
||||
if (skillName) {
|
||||
try {
|
||||
const skill = await loadSkill(skillName);
|
||||
const packRoot = skill.skillPackRoot?.trim();
|
||||
if (packRoot) {
|
||||
const detail = await loadRecipeDetail(packRoot, entry);
|
||||
const seeded = creationFlowFromRecipeSeed(detail);
|
||||
if (seeded) {
|
||||
board.write({
|
||||
tag: CREATION_FLOW_TAG,
|
||||
content: stringifyCreationFlow(seeded),
|
||||
source: "runtime",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[会话] 从配方写入创作流程失败", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.refreshOpeningGuide(s);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1893,6 +2193,7 @@ function collectFilledTags(blackboard: Blackboard): string[] {
|
||||
|
||||
function buildCreationFlowUserView(
|
||||
blackboard: Blackboard,
|
||||
acceptedRaw?: unknown,
|
||||
): CreationFlowUserView | undefined {
|
||||
const raw = blackboard.getContentByTag(CREATION_FLOW_TAG)?.trim();
|
||||
if (!raw) return undefined;
|
||||
@@ -1900,7 +2201,56 @@ function buildCreationFlowUserView(
|
||||
if (!flow) {
|
||||
return { steps: [], parseError: "创作流程无法解析为 JSON(需要 steps 数组)" };
|
||||
}
|
||||
return formatCreationFlowForUser(flow);
|
||||
const accepted = parseAcceptedUnits(
|
||||
acceptedRaw ?? blackboard.getContentByTag(CREATION_ACCEPTED_UNITS_TAG),
|
||||
);
|
||||
return formatCreationFlowForUser(flow, undefined, accepted);
|
||||
}
|
||||
|
||||
function readProposedNextStep(
|
||||
slots: Record<string, unknown>,
|
||||
blackboard: Blackboard,
|
||||
): ProposedNextStepView | undefined {
|
||||
const raw =
|
||||
(typeof slots[CREATION_PROPOSED_STEP_TAG] === "string"
|
||||
? String(slots[CREATION_PROPOSED_STEP_TAG])
|
||||
: null) || blackboard.getContentByTag(CREATION_PROPOSED_STEP_TAG);
|
||||
if (!raw?.trim()) return undefined;
|
||||
try {
|
||||
const doc = JSON.parse(raw) as Partial<ProposedNextStepView>;
|
||||
if (typeof doc.stepId !== "string" || typeof doc.name !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
stepId: doc.stepId,
|
||||
name: doc.name,
|
||||
declaration:
|
||||
typeof doc.declaration === "string" ? doc.declaration : undefined,
|
||||
params:
|
||||
doc.params && typeof doc.params === "object" && !Array.isArray(doc.params)
|
||||
? (doc.params as Record<string, unknown>)
|
||||
: {},
|
||||
paramsMissing: Array.isArray(doc.paramsMissing)
|
||||
? doc.paramsMissing.map(String)
|
||||
: [],
|
||||
paramSpecs: Array.isArray(doc.paramSpecs)
|
||||
? doc.paramSpecs
|
||||
.map((p) => ({
|
||||
key: String((p as { key?: string }).key ?? ""),
|
||||
label: String((p as { label?: string }).label ?? ""),
|
||||
required: Boolean((p as { required?: boolean }).required),
|
||||
hint:
|
||||
typeof (p as { hint?: string }).hint === "string"
|
||||
? (p as { hint: string }).hint
|
||||
: undefined,
|
||||
}))
|
||||
.filter((p) => p.key)
|
||||
: [],
|
||||
intent: typeof doc.intent === "string" ? doc.intent : undefined,
|
||||
};
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveSelectedRecipeView(
|
||||
@@ -1972,19 +2322,46 @@ function buildReviewArtifactView(
|
||||
|
||||
for (const tag of art.outputTags) {
|
||||
if (workerSetTags.includes(tag)) continue;
|
||||
if (isProgressPointerTag(tag)) continue;
|
||||
const content = blackboard.getContentByTag(tag);
|
||||
if (content?.trim()) {
|
||||
tagBodies.push(`## ${tag}\n\n${content.trim()}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 回退消息里也可能夹带「创作.当前步骤」段落,验收正文里去掉
|
||||
const stripPointerSections = (text: string): string => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return "";
|
||||
const re = /^##\s+(.+?)\s*$/gm;
|
||||
const hits: Array<{ title: string; index: number; headerEnd: number }> = [];
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(trimmed))) {
|
||||
hits.push({
|
||||
title: m[1]!.trim(),
|
||||
index: m.index,
|
||||
headerEnd: m.index + m[0].length,
|
||||
});
|
||||
}
|
||||
if (!hits.length) return trimmed;
|
||||
return hits
|
||||
.filter((h) => !isProgressPointerTag(h.title) && !/当前步骤|当前单位/.test(h.title))
|
||||
.map((h, i) => {
|
||||
const end = i + 1 < hits.length ? hits[i + 1]!.index : trimmed.length;
|
||||
const chunk = trimmed.slice(h.headerEnd, end).trim();
|
||||
return chunk ? `## ${h.title}\n\n${chunk}` : "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
};
|
||||
|
||||
const outputMsg = [...messages]
|
||||
.reverse()
|
||||
.find((m) => m.kind === "worker_output" && m.actor === art.workerId);
|
||||
|
||||
const body =
|
||||
tagBodies.join("\n\n") ||
|
||||
(outputMsg?.body ?? outputMsg?.text ?? "").trim() ||
|
||||
stripPointerSections((outputMsg?.body ?? outputMsg?.text ?? "").trim()) ||
|
||||
art.summary?.trim() ||
|
||||
"(Worker 未写入可读正文,请检查黑板 tag 或重试创作 skill)";
|
||||
|
||||
@@ -2028,7 +2405,10 @@ function buildReviewArtifactView(
|
||||
: undefined;
|
||||
|
||||
const creationFlowView = isFlowArt
|
||||
? buildCreationFlowUserView(blackboard)
|
||||
? buildCreationFlowUserView(
|
||||
blackboard,
|
||||
session.slots[SLOT_CREATION_ACCEPTED_UNITS],
|
||||
)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
|
||||
@@ -7,9 +7,11 @@ import { sessionManager } from "./session-manager.js";
|
||||
import { handleSettingsApi } from "./settings-handlers.js";
|
||||
import { handleBooksApi } from "./book-handlers.js";
|
||||
import { handleStatsApi } from "./stats-handlers.js";
|
||||
import { ensureConsoleUtf8, getRuntimeLogPath } from "../log.js";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "../..");
|
||||
ensureConsoleUtf8();
|
||||
loadDotEnv(PROJECT_ROOT);
|
||||
const WEB_ROOT = path.resolve(__dirname, "../../web");
|
||||
const PORT = Number(process.env.PORT ?? 23337);
|
||||
@@ -122,12 +124,45 @@ const server = createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
if (req.method === "POST" && sub === "/messages") {
|
||||
const body = JSON.parse(await readBody(req)) as { text?: string };
|
||||
if (!body.text?.trim()) {
|
||||
json(res, 400, { error: "text 不能为空" });
|
||||
return;
|
||||
const body = JSON.parse(await readBody(req)) as {
|
||||
text?: string;
|
||||
answers?: Array<{
|
||||
questionId?: string;
|
||||
optionId?: string;
|
||||
text?: string;
|
||||
}>;
|
||||
};
|
||||
const text = typeof body.text === "string" ? body.text : "";
|
||||
const trimmed = text.trim();
|
||||
const answers = (body.answers ?? [])
|
||||
.filter(
|
||||
(a) =>
|
||||
typeof a?.questionId === "string" &&
|
||||
a.questionId.trim() &&
|
||||
typeof a?.text === "string" &&
|
||||
a.text.trim(),
|
||||
)
|
||||
.map((a) => ({
|
||||
questionId: a.questionId!.trim(),
|
||||
optionId:
|
||||
typeof a.optionId === "string" && a.optionId.trim()
|
||||
? a.optionId.trim()
|
||||
: undefined,
|
||||
text: a.text!.trim(),
|
||||
}));
|
||||
// next_intent 明确可留空;其余态仍要求有字(除非顺带提交了追问)
|
||||
if (!trimmed && !answers.length) {
|
||||
const current = sessionManager.get(sessionId);
|
||||
if (current?.waitingReason?.kind !== "next_intent") {
|
||||
json(res, 400, { error: "text 不能为空" });
|
||||
return;
|
||||
}
|
||||
}
|
||||
const view = await sessionManager.sendMessage(sessionId, body.text.trim());
|
||||
const view = await sessionManager.sendMessage(
|
||||
sessionId,
|
||||
trimmed,
|
||||
answers.length ? { answers } : undefined,
|
||||
);
|
||||
json(res, 200, view);
|
||||
return;
|
||||
}
|
||||
@@ -406,11 +441,19 @@ const server = createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
if (req.method === "POST" && sub === "/actions") {
|
||||
const body = JSON.parse(await readBody(req)) as { action?: string };
|
||||
const body = JSON.parse(await readBody(req)) as {
|
||||
action?: string;
|
||||
stepParams?: Record<string, unknown>;
|
||||
};
|
||||
let view;
|
||||
switch (body.action) {
|
||||
case "approve":
|
||||
view = await sessionManager.approve(sessionId);
|
||||
view = await sessionManager.approve(
|
||||
sessionId,
|
||||
body.stepParams && typeof body.stepParams === "object"
|
||||
? body.stepParams
|
||||
: undefined,
|
||||
);
|
||||
break;
|
||||
case "confirm_intake":
|
||||
view = await sessionManager.confirmIntake(sessionId);
|
||||
@@ -452,7 +495,7 @@ const server = createServer(async (req, res) => {
|
||||
json(res, 404, { error: "Not found" });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[api]", req.method, url.pathname, err);
|
||||
console.error("[接口]", req.method, url.pathname, err);
|
||||
json(res, 500, {
|
||||
error: err instanceof Error ? err.message : "服务器错误",
|
||||
});
|
||||
@@ -460,5 +503,6 @@ const server = createServer(async (req, res) => {
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Writing Agent 对话页: http://localhost:${PORT}`);
|
||||
console.log(`对话页 http://localhost:${PORT}`);
|
||||
console.log(`运行日志(UTF-8) ${getRuntimeLogPath()}`);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user