接通主角绑定、开场封印与游玩辅助,让世界模拟器从创作连续落到本局。
直聘插入按模块回馈,并记录请求骨架便于排查。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -56,6 +56,7 @@ async function loadModulesPayload(skillId: string): Promise<{
|
||||
loadModuleCatalog,
|
||||
loadModulePrompt,
|
||||
parseModulePromptSections,
|
||||
catalogModulesForIntake,
|
||||
MODULE_SECTION_IDS,
|
||||
} = await import("../skills/creation-flow.js");
|
||||
const skill = await loadSkill(skillId);
|
||||
@@ -64,7 +65,7 @@ async function loadModulesPayload(skillId: string): Promise<{
|
||||
}
|
||||
const catalog = await loadModuleCatalog(skill.skillPackRoot);
|
||||
const modules = [];
|
||||
for (const m of catalog?.modules ?? []) {
|
||||
for (const m of catalogModulesForIntake(catalog ?? { modules: [] }, "recipe")) {
|
||||
const prompt = await loadModulePrompt(skill.skillPackRoot, m.id);
|
||||
const sections = prompt
|
||||
? parseModulePromptSections(prompt)
|
||||
@@ -423,16 +424,27 @@ export async function handleBooksApi(
|
||||
}
|
||||
try {
|
||||
let instanceId: string | undefined;
|
||||
let useOpeningPersona: boolean | undefined;
|
||||
try {
|
||||
const raw = await readBody(req);
|
||||
if (raw.trim()) {
|
||||
const body = JSON.parse(raw) as { instanceId?: string };
|
||||
const body = JSON.parse(raw) as {
|
||||
instanceId?: string;
|
||||
useOpeningPersona?: boolean;
|
||||
};
|
||||
instanceId = body.instanceId?.trim() || undefined;
|
||||
useOpeningPersona =
|
||||
typeof body.useOpeningPersona === "boolean"
|
||||
? body.useOpeningPersona
|
||||
: undefined;
|
||||
}
|
||||
} catch {
|
||||
instanceId = undefined;
|
||||
useOpeningPersona = undefined;
|
||||
}
|
||||
const session = await sessionManager.startNewPlayRun(active.id, instanceId);
|
||||
const session = await sessionManager.startNewPlayRun(active.id, instanceId, {
|
||||
useOpeningPersona,
|
||||
});
|
||||
json(res, 200, { session });
|
||||
} catch (err) {
|
||||
json(res, 400, {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* 用户可见中文标签(与 docs/ui-glossary.md 同步)。
|
||||
* 内部 id 不变;仅展示层映射。
|
||||
* 口径:配方 / 编排器 / 技能 / 工作流计划 / 运行规格 / 执行单元
|
||||
* 口径:进料方式 / 配方 / 编排器 / 技能 / 工作流计划 / 运行规格 / 执行单元
|
||||
*/
|
||||
|
||||
const STAGE_LABELS: Record<string, string> = {
|
||||
@@ -39,9 +39,12 @@ const FIXED_TOPIC_LABELS: Record<string, string> = {
|
||||
"aesthetics-interaction": "美学纲领与交互范式",
|
||||
interaction: "交互范式",
|
||||
narrative_guide: "叙事指南与故事推进",
|
||||
"narrative-guide": "叙事指南",
|
||||
"story-progression": "故事推进",
|
||||
input_protocol: "输入协议",
|
||||
core_premise: "核心前提",
|
||||
aesthetics: "美学纲领",
|
||||
protagonist: "主角设定",
|
||||
};
|
||||
|
||||
const PHASE_UNIT_LABELS: Record<string, string> = {
|
||||
@@ -49,9 +52,15 @@ const PHASE_UNIT_LABELS: Record<string, string> = {
|
||||
refine: "细化",
|
||||
};
|
||||
|
||||
const CREATION_MODE_LABELS: Record<string, string> = {
|
||||
recipe: "工序编排",
|
||||
dictate: "对话落盘",
|
||||
};
|
||||
|
||||
const SKILL_PACK_LABELS: Record<string, string> = {
|
||||
"world-simulator": "世界模拟器",
|
||||
"world-simulator": "回合推演",
|
||||
"expand-assistant": "扩写助手",
|
||||
快穿短局: "快穿短局",
|
||||
文本生成器: "文本生成器",
|
||||
"text-generator": "文本生成器",
|
||||
交互式长文生成器: "交互式长文生成器",
|
||||
@@ -64,6 +73,12 @@ export function displayStageLabel(id: string | undefined | null): string {
|
||||
return STAGE_LABELS[id] ?? id;
|
||||
}
|
||||
|
||||
/** 进料方式展示名(新建对话框 / 工作面 kicker) */
|
||||
export function displayCreationModeLabel(id: string | undefined | null): string {
|
||||
if (!id) return "";
|
||||
return CREATION_MODE_LABELS[id] ?? id;
|
||||
}
|
||||
|
||||
/** 配方选项 / skill pack 展示名 */
|
||||
export function displaySkillPackLabel(id: string | undefined | null): string {
|
||||
if (!id) return "";
|
||||
|
||||
+230
-21
@@ -18,6 +18,13 @@ import {
|
||||
import {
|
||||
INSTANCE_OPENING_SNAPSHOT_LABEL,
|
||||
OPENING_OUTPUT_TAG,
|
||||
OPENING_PERSONA_CHOICE_TAG,
|
||||
OPENING_PERSONA_TAG,
|
||||
SLOT_OPENING_SELECTED_INDEX,
|
||||
parseOpeningSealPayload,
|
||||
readSealedOpeningPersona,
|
||||
serializeOpeningPersona,
|
||||
splitOpeningDocument,
|
||||
} from "../skills/opening-seal.js";
|
||||
import {
|
||||
PLAY_WORKING_SNAPSHOT_ID,
|
||||
@@ -150,9 +157,11 @@ import {
|
||||
CREATION_INTAKE_MODE_TAG,
|
||||
DICTATE_MODE_VALUE,
|
||||
DICTATE_ORDER_META_KEY,
|
||||
buildDictateInsertFeedbackIndex,
|
||||
defaultDictateOrder,
|
||||
extractDictateDialogue,
|
||||
isDictateModeValue,
|
||||
lookupDictateInsertFeedback,
|
||||
runDictateTurn,
|
||||
sortDictateProducts,
|
||||
type DictateProduct,
|
||||
@@ -173,6 +182,12 @@ import {
|
||||
listPersonas,
|
||||
personaForLifecycle,
|
||||
} from "../persona/store.js";
|
||||
import {
|
||||
overlayPlayPersona,
|
||||
readSessionProtagonist,
|
||||
} from "../persona/protagonist-bind.js";
|
||||
import { buildPlayAuxView } from "../skills/play-aux.js";
|
||||
import type { TableCellPatch } from "../blackboard/table-cells.js";
|
||||
import {
|
||||
appendBranchVariant,
|
||||
cloneCheckpoint,
|
||||
@@ -290,6 +305,11 @@ export type SessionView = {
|
||||
agentThinking?: string;
|
||||
/** Agent / Worker 流式输出(轮询用) */
|
||||
liveStream?: LiveStreamView;
|
||||
/**
|
||||
* 会话是否有进行中的排他运行(send/action 的 runExclusive)。
|
||||
* 前端 loading 应对齐此标志,避免 POST 挂住时底栏永远「生成中」。
|
||||
*/
|
||||
runActive?: boolean;
|
||||
resumed?: boolean;
|
||||
/** 创作进料:recipe=配方节点流;dictate=转述整理直填 */
|
||||
creationMode?: "recipe" | "dictate";
|
||||
@@ -310,6 +330,12 @@ export type SessionView = {
|
||||
personas?: Array<{ id: string; name: string }>;
|
||||
/** 当前选中的用户角色 */
|
||||
activePersona?: { id: string; name: string; description: string } | null;
|
||||
/** 本局开场自带的用户角色预设(名字+简介);关掉这局就不加载 */
|
||||
sessionProtagonist?: { name: string; description: string; summary: string } | null;
|
||||
/** 用户是否选用开场预设:opening / global */
|
||||
openingPersonaChoice?: "opening" | "global" | null;
|
||||
/** 游玩辅助 Tab:表格 / 开场与角色 */
|
||||
playAux?: import("../skills/play-aux.js").PlayAuxView;
|
||||
};
|
||||
|
||||
export type SessionAction =
|
||||
@@ -419,14 +445,21 @@ export class SessionManager {
|
||||
return this.createForBook();
|
||||
}
|
||||
|
||||
setLifecycleStage(sessionId: string, stage: LifecycleStage): SessionView {
|
||||
setLifecycleStage(
|
||||
sessionId: string,
|
||||
stage: LifecycleStage,
|
||||
opts?: { useOpeningPersona?: boolean },
|
||||
): SessionView {
|
||||
const s = this.require(sessionId);
|
||||
if (stage === "play") {
|
||||
s.runtime.prepareEnterPlay();
|
||||
if (!canEnterPlay(s.runtime.getSession())) {
|
||||
throw new Error("实例尚未就绪,无法进入游玩");
|
||||
}
|
||||
this.enterPlayLayer(s, { resume: true });
|
||||
this.enterPlayLayer(s, {
|
||||
resume: true,
|
||||
useOpeningPersona: opts?.useOpeningPersona,
|
||||
});
|
||||
} else {
|
||||
this.enterDesignLayer(s);
|
||||
}
|
||||
@@ -452,6 +485,65 @@ export class SessionManager {
|
||||
return this.toView(sessionId);
|
||||
}
|
||||
|
||||
/** 游玩表按格手改。冲突格在 tablePatch.skipped,整表不会被旧内容盖掉。 */
|
||||
patchPlayTable(
|
||||
sessionId: string,
|
||||
tag: string,
|
||||
cells: TableCellPatch[],
|
||||
): SessionView & {
|
||||
tablePatch: ReturnType<PhaseRuntime["patchUserTable"]>;
|
||||
} {
|
||||
const s = this.require(sessionId);
|
||||
const tablePatch = s.runtime.patchUserTable(tag.trim() || "变量.当前", cells);
|
||||
if (s.bookId) this.persist(s);
|
||||
return { ...this.toView(sessionId), tablePatch };
|
||||
}
|
||||
|
||||
/**
|
||||
* 游玩辅助面点选开场。usePersona=true 用该开场自带角色;false 保持当前用户角色卡。
|
||||
*/
|
||||
selectPlayOpening(
|
||||
sessionId: string,
|
||||
index: number,
|
||||
usePersona?: boolean,
|
||||
): SessionView {
|
||||
const s = this.require(sessionId);
|
||||
const board = s.runtime.getBlackboard();
|
||||
const aux = buildPlayAuxView(board, s.runtime.getSession().slots);
|
||||
if (!aux.openings.length) throw new Error("没有可选开场");
|
||||
const idx = Math.min(Math.max(0, Math.trunc(index)), aux.openings.length - 1);
|
||||
const picked = aux.openings[idx]!;
|
||||
s.runtime.setOpeningSelectedIndex(idx);
|
||||
|
||||
const persona = picked.persona;
|
||||
if (usePersona === false) {
|
||||
this.writeOpeningPersonaChoice(s, false);
|
||||
} else if (persona) {
|
||||
board.write({
|
||||
tag: OPENING_PERSONA_TAG,
|
||||
content: serializeOpeningPersona(persona),
|
||||
source: "user",
|
||||
});
|
||||
this.writeOpeningPersonaChoice(s, true);
|
||||
}
|
||||
|
||||
const expanded = this.expandDirectivesText(
|
||||
picked.text,
|
||||
this.playDirectiveContext(s),
|
||||
);
|
||||
board.write({
|
||||
tag: OPENING_OUTPUT_TAG,
|
||||
content: expanded,
|
||||
source: "user",
|
||||
});
|
||||
const first = s.messages[0];
|
||||
if (first?.title === "开场白") {
|
||||
s.messages[0] = this.playOpeningMessage(expanded);
|
||||
}
|
||||
if (s.bookId) this.persist(s);
|
||||
return this.toView(sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 编排上下文投影排序:写入 设计.worker集.context_order(若有规格),
|
||||
* 并同步 设计.上下文投影排序。下次声明驱动拼装即按新序。
|
||||
@@ -625,7 +717,7 @@ export class SessionManager {
|
||||
return this.toView(id);
|
||||
}
|
||||
|
||||
/** 切换到转述整理进料(与配方并行;不跑 design-flow) */
|
||||
/** 切换到对话落盘进料(与工序编排并行;不跑 design-flow) */
|
||||
private enterDictateMode(s: ManagedSession): void {
|
||||
const board = s.runtime.getBlackboard();
|
||||
board.write({
|
||||
@@ -641,7 +733,7 @@ export class SessionManager {
|
||||
}。`
|
||||
: "";
|
||||
const welcome =
|
||||
`Boss 直聘模式:直接说你想要的体验与设定。我会整理进产物;变量与映射用工具钉死。对话过长时可清空对话(产物保留)。${recipeLine}`;
|
||||
`对话落盘:直接说你想要的体验与设定。我会整理进产物;变量与映射用工具钉死。对话过长时可清空对话(产物保留)。${recipeLine}`;
|
||||
s.messages.push({
|
||||
id: randomUUID(),
|
||||
role: "system",
|
||||
@@ -649,7 +741,7 @@ export class SessionManager {
|
||||
createdAt: new Date().toISOString(),
|
||||
kind: "orchestrator_prompt",
|
||||
actor: "orchestrator",
|
||||
title: "Boss直聘",
|
||||
title: "对话落盘",
|
||||
body: welcome,
|
||||
});
|
||||
}
|
||||
@@ -726,6 +818,13 @@ export class SessionManager {
|
||||
skillName: s.runtime.getActiveSkill()?.name,
|
||||
moduleCatalog: s.moduleCatalog,
|
||||
});
|
||||
const insertFeedbackIndex = await resolveDictateInsertFeedbackIndexForSession({
|
||||
skillName: s.runtime.getActiveSkill()?.name,
|
||||
moduleCatalog: s.moduleCatalog,
|
||||
});
|
||||
const repeatableFamilies = (s.moduleCatalog?.modules ?? [])
|
||||
.filter((m) => m.repeatable === true && m.artifact?.trim())
|
||||
.map((m) => m.artifact!.trim());
|
||||
|
||||
const result = await runDictateTurn({
|
||||
llm,
|
||||
@@ -733,6 +832,7 @@ export class SessionManager {
|
||||
recipeName: recipeView?.name,
|
||||
recipeBrief,
|
||||
moduleGuide,
|
||||
repeatableFamilies,
|
||||
handlers: {
|
||||
listProducts: () => this.listDictateProducts(s),
|
||||
writeProduct: (tag, content, order) => {
|
||||
@@ -755,6 +855,7 @@ export class SessionManager {
|
||||
},
|
||||
});
|
||||
},
|
||||
deleteProduct: (tag) => s.runtime.getBlackboard().deleteByTag(tag),
|
||||
readTag: (tag) => s.runtime.getBlackboard().getContentByTag(tag),
|
||||
writeTag: (tag, content) => {
|
||||
const board = s.runtime.getBlackboard();
|
||||
@@ -768,6 +869,7 @@ export class SessionManager {
|
||||
},
|
||||
});
|
||||
},
|
||||
deleteTag: (tag) => s.runtime.getBlackboard().deleteByTag(tag),
|
||||
clearDialogue: () => {
|
||||
const kept = this.listDictateProducts(s);
|
||||
const lastUser = [...s.messages]
|
||||
@@ -813,6 +915,8 @@ export class SessionManager {
|
||||
if (text.trim()) this.agentThinkingLive.set(id, text);
|
||||
this.agentThinkingClosed.add(id);
|
||||
},
|
||||
lookupInsertFeedback: (tag) =>
|
||||
lookupDictateInsertFeedback(insertFeedbackIndex, tag),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -949,6 +1053,7 @@ export class SessionManager {
|
||||
async startNewPlayRun(
|
||||
sessionId: string,
|
||||
instanceId?: string,
|
||||
opts?: { useOpeningPersona?: boolean },
|
||||
): Promise<SessionView> {
|
||||
const s = this.require(sessionId);
|
||||
if (!s.bookId) throw new Error("仅绑定作品时可新建游玩");
|
||||
@@ -958,14 +1063,22 @@ export class SessionManager {
|
||||
throw new Error("须先验收 Worker 集、完成收口,或(转述)先写入产物并落档,才能开始游玩");
|
||||
}
|
||||
this.archivePlayWorking(s.bookId);
|
||||
this.enterPlayLayer(s, { resume: false, instanceId });
|
||||
this.enterPlayLayer(s, {
|
||||
resume: false,
|
||||
instanceId,
|
||||
useOpeningPersona: opts?.useOpeningPersona,
|
||||
});
|
||||
return this.toView(sessionId);
|
||||
}
|
||||
|
||||
/** 进入游玩层:resume 时恢复「当前游玩」,否则从产物新开一条聊天 */
|
||||
private enterPlayLayer(
|
||||
s: ManagedSession,
|
||||
opts: { resume: boolean; instanceId?: string },
|
||||
opts: {
|
||||
resume: boolean;
|
||||
instanceId?: string;
|
||||
useOpeningPersona?: boolean;
|
||||
},
|
||||
): void {
|
||||
this.persistCreationIfDesign(s);
|
||||
s.runtime.prepareEnterPlay();
|
||||
@@ -978,6 +1091,7 @@ export class SessionManager {
|
||||
isPlayLayerActive(session.slots) &&
|
||||
inferLifecycleStage(session) === "play"
|
||||
) {
|
||||
this.writeOpeningPersonaChoice(s, opts.useOpeningPersona);
|
||||
this.sanitizePlayHitl(s);
|
||||
return;
|
||||
}
|
||||
@@ -986,10 +1100,11 @@ export class SessionManager {
|
||||
const working = loadRunSnapshotFile(s.bookId, PLAY_WORKING_SNAPSHOT_ID);
|
||||
if (working?.kind === "run") {
|
||||
this.restoreSnapshotInPlace(s, working, "play");
|
||||
this.writeOpeningPersonaChoice(s, opts.useOpeningPersona);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.beginNewPlayFromInstance(s, opts.instanceId);
|
||||
this.beginNewPlayFromInstance(s, opts.instanceId, opts.useOpeningPersona);
|
||||
if (s.bookId) this.savePlayWorking(s);
|
||||
}
|
||||
|
||||
@@ -1009,7 +1124,11 @@ export class SessionManager {
|
||||
this.restoreCreationFromDisk(s);
|
||||
}
|
||||
|
||||
private beginNewPlayFromInstance(s: ManagedSession, instanceId?: string): void {
|
||||
private beginNewPlayFromInstance(
|
||||
s: ManagedSession,
|
||||
instanceId?: string,
|
||||
useOpeningPersona?: boolean,
|
||||
): void {
|
||||
const keepId = s.runtime.getSession().id;
|
||||
// 换上 instance 快照前先记下当前创作里的开场白(可能比上次定稿更新)
|
||||
const liveDesignOpening =
|
||||
@@ -1055,6 +1174,7 @@ export class SessionManager {
|
||||
source: "runtime",
|
||||
});
|
||||
}
|
||||
this.writeOpeningPersonaChoice(s, useOpeningPersona);
|
||||
const opening = this.ensurePlayOpeningWritten(
|
||||
s,
|
||||
instance?.messages as ChatMessage[] | undefined,
|
||||
@@ -1072,6 +1192,19 @@ export class SessionManager {
|
||||
s.branchState = createMessageBranchState();
|
||||
}
|
||||
|
||||
/** 开玩时记下是否用这条开场自带的用户角色;须在恢复快照之后写,否则会被冲掉。 */
|
||||
private writeOpeningPersonaChoice(
|
||||
s: ManagedSession,
|
||||
useOpeningPersona?: boolean,
|
||||
): void {
|
||||
if (useOpeningPersona === undefined) return;
|
||||
s.runtime.getBlackboard().write({
|
||||
tag: OPENING_PERSONA_CHOICE_TAG,
|
||||
content: useOpeningPersona ? "opening" : "global",
|
||||
source: "user",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保证黑板上有 输出.开场白,供开玩主区展示。
|
||||
* 优先级:设计.开场白(转述产物)> 已有 输出.开场白 > 其它产物拼装 > 非转述聊天兜底。
|
||||
@@ -1082,6 +1215,7 @@ export class SessionManager {
|
||||
s: ManagedSession,
|
||||
messageSource?: ChatMessage[],
|
||||
): string {
|
||||
this.sealOpeningPersonaFromBoard(s);
|
||||
const board = s.runtime.getBlackboard();
|
||||
const fromDesign =
|
||||
board.getContentByTag("设计.开场白")?.trim() ||
|
||||
@@ -1089,9 +1223,10 @@ export class SessionManager {
|
||||
?.content?.trim() ||
|
||||
"";
|
||||
if (fromDesign) {
|
||||
const body = splitOpeningDocument(fromDesign).text || fromDesign;
|
||||
const expanded = this.expandDirectivesText(
|
||||
fromDesign,
|
||||
this.playDirectiveContext(),
|
||||
body,
|
||||
this.playDirectiveContext(s),
|
||||
);
|
||||
const existing = board.getContentByTag(OPENING_OUTPUT_TAG)?.trim() ?? "";
|
||||
if (existing !== expanded) {
|
||||
@@ -1108,7 +1243,7 @@ export class SessionManager {
|
||||
if (existing) {
|
||||
const expanded = this.expandDirectivesText(
|
||||
existing,
|
||||
this.playDirectiveContext(),
|
||||
this.playDirectiveContext(s),
|
||||
);
|
||||
if (expanded !== existing) {
|
||||
board.write({
|
||||
@@ -1129,7 +1264,7 @@ export class SessionManager {
|
||||
|
||||
const expanded = this.expandDirectivesText(
|
||||
opening,
|
||||
this.playDirectiveContext(),
|
||||
this.playDirectiveContext(s),
|
||||
);
|
||||
board.write({
|
||||
tag: OPENING_OUTPUT_TAG,
|
||||
@@ -1139,19 +1274,46 @@ export class SessionManager {
|
||||
return expanded;
|
||||
}
|
||||
|
||||
/** 开场 meta 里的用户角色:落到 运行.开场用户角色,不进全局列表。 */
|
||||
private sealOpeningPersonaFromBoard(s: ManagedSession): void {
|
||||
const board = s.runtime.getBlackboard();
|
||||
if (readSealedOpeningPersona(board.getContentByTag(OPENING_PERSONA_TAG))) {
|
||||
return;
|
||||
}
|
||||
const selectedRaw = s.runtime.getSession().slots[SLOT_OPENING_SELECTED_INDEX];
|
||||
const selectedIndex =
|
||||
typeof selectedRaw === "number"
|
||||
? selectedRaw
|
||||
: typeof selectedRaw === "string" && selectedRaw.trim()
|
||||
? Number(selectedRaw)
|
||||
: 0;
|
||||
const persona =
|
||||
parseOpeningSealPayload(
|
||||
board.getContentByTag("设计.开场白与开场变量"),
|
||||
selectedIndex,
|
||||
)?.persona ?? splitOpeningDocument(board.getContentByTag("设计.开场白")).persona;
|
||||
if (!persona) return;
|
||||
board.write({
|
||||
tag: OPENING_PERSONA_TAG,
|
||||
content: serializeOpeningPersona(persona),
|
||||
source: "runtime",
|
||||
});
|
||||
}
|
||||
|
||||
private personaForManaged(s: ManagedSession): ReturnType<
|
||||
typeof personaForLifecycle
|
||||
> {
|
||||
if (!s.runtime) return personaForLifecycle("design");
|
||||
return personaForLifecycle(inferLifecycleStage(s.runtime.getSession()));
|
||||
return overlayPlayPersona(
|
||||
inferLifecycleStage(s.runtime.getSession()),
|
||||
s.runtime.getBlackboard(),
|
||||
);
|
||||
}
|
||||
|
||||
private playDirectiveContext(): DirectiveContext {
|
||||
const p = getActivePersona();
|
||||
private playDirectiveContext(s: ManagedSession): DirectiveContext {
|
||||
const p = this.personaForManaged(s);
|
||||
if (!p) return {};
|
||||
return {
|
||||
persona: { name: p.name, description: p.description },
|
||||
};
|
||||
return { persona: p };
|
||||
}
|
||||
|
||||
private directiveContextFor(s: ManagedSession): DirectiveContext {
|
||||
@@ -3097,6 +3259,7 @@ export class SessionManager {
|
||||
private toView(id: string, resumed = false, resumeHint?: string): SessionView {
|
||||
const s = this.require(id);
|
||||
this.sanitizePlayHitl(s);
|
||||
this.sealOpeningPersonaFromBoard(s);
|
||||
const session = s.runtime.getSession();
|
||||
const reason = session.waitingReason;
|
||||
const book = s.bookId ? getBook(s.bookId) : null;
|
||||
@@ -3482,6 +3645,7 @@ export class SessionManager {
|
||||
: undefined,
|
||||
liveStream: this.buildLiveStreamView(id),
|
||||
agentThinking: this.agentThinkingLive.get(id),
|
||||
runActive: Boolean(s.runAbort),
|
||||
resumed: resumed || undefined,
|
||||
tokenStats: {
|
||||
sessionTotal: tokens.totalTokens,
|
||||
@@ -3498,6 +3662,19 @@ export class SessionManager {
|
||||
? { id: p.id, name: p.name, description: p.description }
|
||||
: null;
|
||||
})(),
|
||||
sessionProtagonist: readSessionProtagonist(s.runtime.getBlackboard()),
|
||||
openingPersonaChoice: ((): "opening" | "global" | null => {
|
||||
const raw = s.runtime
|
||||
.getBlackboard()
|
||||
.getContentByTag(OPENING_PERSONA_CHOICE_TAG)
|
||||
?.trim();
|
||||
if (raw === "opening" || raw === "global") return raw;
|
||||
return null;
|
||||
})(),
|
||||
playAux: buildPlayAuxView(
|
||||
s.runtime.getBlackboard(),
|
||||
s.runtime.getSession().slots,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3927,7 +4104,7 @@ function resolveSelectedRecipeView(
|
||||
return { id: ref, name: ref, declaration: "" };
|
||||
}
|
||||
|
||||
/** Boss 直聘:优先注入 recipe.yaml 方法论,缺则退回 catalog declaration */
|
||||
/** 对话落盘:优先注入 recipe.yaml 方法论,缺则退回 catalog declaration */
|
||||
async function resolveDictateRecipeBriefForSession(params: {
|
||||
skillName?: string | null;
|
||||
recipeView: { id: string; name: string; declaration: string } | null;
|
||||
@@ -3956,7 +4133,7 @@ async function resolveDictateRecipeBriefForSession(params: {
|
||||
return view.declaration?.trim() || undefined;
|
||||
}
|
||||
|
||||
/** Boss 直聘:注入能力何时落盘(modules meta);优先会话已载 catalog */
|
||||
/** 对话落盘:注入能力何时落盘(modules meta);优先会话已载 catalog */
|
||||
async function resolveDictateModuleGuideForSession(params: {
|
||||
skillName?: string | null;
|
||||
moduleCatalog?: ModuleCatalog | null;
|
||||
@@ -3979,6 +4156,38 @@ async function resolveDictateModuleGuideForSession(params: {
|
||||
return formatModuleCatalogForDictate(catalog);
|
||||
}
|
||||
|
||||
/** 对话落盘:insert(tag) → 用户可见规范索引 */
|
||||
async function resolveDictateInsertFeedbackIndexForSession(params: {
|
||||
skillName?: string | null;
|
||||
moduleCatalog?: ModuleCatalog | null;
|
||||
}): Promise<Map<string, string> | undefined> {
|
||||
let catalog = params.moduleCatalog ?? null;
|
||||
let packRoot: string | undefined;
|
||||
const skillName = params.skillName?.trim();
|
||||
if (skillName) {
|
||||
try {
|
||||
const skill = await loadSkill(skillName);
|
||||
packRoot = skill.skillPackRoot?.trim() || undefined;
|
||||
if (!catalog?.modules?.length && packRoot) {
|
||||
catalog = await loadModuleCatalog(packRoot);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[会话] 加载 insert 反馈索引失败", err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
if (!catalog?.modules?.length || !packRoot) return undefined;
|
||||
try {
|
||||
return await buildDictateInsertFeedbackIndex({
|
||||
catalog,
|
||||
skillPackRoot: packRoot,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("[会话] 构建 insert 反馈索引失败", err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function buildWorkerSetUserView(
|
||||
blackboard: Blackboard,
|
||||
session?: RuntimeSession,
|
||||
|
||||
@@ -136,7 +136,10 @@ export async function handleSettingsApi(
|
||||
|
||||
if (pathname === "/api/profiles" && req.method === "GET") {
|
||||
ensureActiveProfileDefault();
|
||||
json(res, 200, { profiles: listApiProfiles() });
|
||||
json(res, 200, {
|
||||
profiles: listApiProfiles(),
|
||||
activeProfileId: loadAppSettings().activeProfileId,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,14 @@ async function serveStatic(res: ServerResponse, filePath: string): Promise<void>
|
||||
".js": "application/javascript; charset=utf-8",
|
||||
};
|
||||
const content = await readFile(filePath);
|
||||
res.writeHead(200, { "Content-Type": types[ext] ?? "application/octet-stream" });
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": types[ext] ?? "application/octet-stream",
|
||||
};
|
||||
// 开发期:避免 ES module(agent-ui.js 等)被强缓存,改完 import 仍吃旧脚本导致主区空白
|
||||
if (ext === ".html" || ext === ".js" || ext === ".css") {
|
||||
headers["Cache-Control"] = "no-cache, must-revalidate";
|
||||
}
|
||||
res.writeHead(200, headers);
|
||||
res.end(content);
|
||||
}
|
||||
|
||||
@@ -339,13 +346,21 @@ const server = createServer(async (req, res) => {
|
||||
}
|
||||
|
||||
if (req.method === "POST" && sub === "/lifecycle") {
|
||||
const body = JSON.parse(await readBody(req)) as { stage?: string };
|
||||
const body = JSON.parse(await readBody(req)) as {
|
||||
stage?: string;
|
||||
useOpeningPersona?: boolean;
|
||||
};
|
||||
if (body.stage !== "design" && body.stage !== "play") {
|
||||
json(res, 400, { error: "stage 须为 design 或 play" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const view = sessionManager.setLifecycleStage(sessionId, body.stage);
|
||||
const view = sessionManager.setLifecycleStage(sessionId, body.stage, {
|
||||
useOpeningPersona:
|
||||
typeof body.useOpeningPersona === "boolean"
|
||||
? body.useOpeningPersona
|
||||
: undefined,
|
||||
});
|
||||
json(res, 200, view);
|
||||
} catch (err) {
|
||||
json(res, 400, {
|
||||
@@ -399,6 +414,68 @@ const server = createServer(async (req, res) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && sub === "/table") {
|
||||
const body = JSON.parse(await readBody(req)) as {
|
||||
tag?: string;
|
||||
cells?: Array<{ key?: string; value?: unknown; expectedRev?: number }>;
|
||||
};
|
||||
const cells = Array.isArray(body.cells)
|
||||
? body.cells
|
||||
.filter(
|
||||
(c) =>
|
||||
typeof c?.key === "string" &&
|
||||
c.key.trim() &&
|
||||
typeof c.expectedRev === "number",
|
||||
)
|
||||
.map((c) => ({
|
||||
key: String(c.key).trim(),
|
||||
value: c.value,
|
||||
expectedRev: c.expectedRev as number,
|
||||
}))
|
||||
: [];
|
||||
if (!cells.length) {
|
||||
json(res, 400, { error: "缺少 cells(每格需要 key 与 expectedRev)" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const view = sessionManager.patchPlayTable(
|
||||
sessionId,
|
||||
body.tag?.trim() || "变量.当前",
|
||||
cells,
|
||||
);
|
||||
json(res, 200, view);
|
||||
} catch (err) {
|
||||
json(res, 400, {
|
||||
error: err instanceof Error ? err.message : "表写入失败",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && sub === "/play/opening") {
|
||||
const body = JSON.parse(await readBody(req)) as {
|
||||
index?: number;
|
||||
usePersona?: boolean;
|
||||
};
|
||||
if (typeof body.index !== "number" || !Number.isFinite(body.index)) {
|
||||
json(res, 400, { error: "缺少 index" });
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const view = sessionManager.selectPlayOpening(
|
||||
sessionId,
|
||||
body.index,
|
||||
typeof body.usePersona === "boolean" ? body.usePersona : undefined,
|
||||
);
|
||||
json(res, 200, view);
|
||||
} catch (err) {
|
||||
json(res, 400, {
|
||||
error: err instanceof Error ? err.message : "无法选用开场",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && sub === "/context-order") {
|
||||
const body = JSON.parse(await readBody(req)) as {
|
||||
action?: string;
|
||||
|
||||
Reference in New Issue
Block a user