Initial commit

This commit is contained in:
2026-07-10 08:31:27 +08:00
commit 2b74c30d36
134 changed files with 21801 additions and 0 deletions
+829
View File
@@ -0,0 +1,829 @@
import { randomUUID } from "node:crypto";
import { appendBookSession, getBook, updateBook } from "../book/store.js";
import { loadBookSession, saveBookSession } from "../book/session-store.js";
import {
deleteRunSnapshot as deleteRunSnapshotFile,
listRunSnapshots,
loadRunSnapshot as loadRunSnapshotFile,
saveRunSnapshot as saveRunSnapshotFile,
} from "../book/run-snapshot-store.js";
import type { RunSnapshot, RunSnapshotMeta, SnapshotKind } from "../types/run-snapshot.js";
import { toRunSnapshotMeta } from "../types/run-snapshot.js";
import { materializeInstanceSnapshotPayload } from "../book/snapshot-filters.js";
import { PhaseRuntime, createDecision } from "../runtime/phase-runtime.js";
import { createSession } from "../runtime/phase-machine.js";
import {
createDefaultMainAgentLlm,
hasRealLlmConfig,
reloadDefaultMainAgentLlm,
type LlmTrackingRef,
} from "../runtime/llm-factory.js";
import { getSessionTokenTotals, type MessageTokenUsage } from "../stats/token-store.js";
import type { PersistedBookSession } from "../types/book-session.js";
import type { RuntimeSession, WaitingReason } from "../types/runtime.js";
import type { IntakeProgress } from "../types/intake.js";
import {
buildFocus,
buildPipeline,
buildToolTrace,
buildBurstState,
buildSkillCatalog,
inferLifecycleStage,
canEnterPlay,
classifyAgentMessage,
type AgentMessageKind,
type SessionFocus,
type StageStep,
type LifecycleStage,
type SkillCatalogEntry,
type ToolTraceEntry,
type BurstState,
} from "./agent-view.js";
import {
buildIntakeProgress,
readIntakeValues,
} from "../intake/intake.js";
import {
bookSkillPackId,
persistedSkillPackId,
runSnapshotSkillPackId,
sessionSkillPackId,
skillPacksMatch,
} from "../book/skill-id.js";
import type { ActiveSkillSnapshot } from "../types/runtime.js";
export type ChatMessage = {
id: string;
role: "system" | "user";
text: string;
createdAt: string;
kind?: AgentMessageKind;
actor?: string;
title?: string;
body?: string;
/** LLM 思维链 / reasoning_content */
thinking?: string;
tokenUsage?: MessageTokenUsage;
};
export type SessionView = {
id: string;
bookId?: string;
bookTitle?: string;
activeSkill?: string;
phase: RuntimeSession["phase"];
waitingReason?: WaitingReason;
startupCompleted: boolean;
skills: Array<{ name: string; description: string; category: string }>;
messages: ChatMessage[];
hints: string[];
actions: SessionAction[];
/** @deprecated 用 skillCatalog */
pipeline: StageStep[];
lifecycleStage: LifecycleStage;
playReady: boolean;
skillCatalog: SkillCatalogEntry[];
toolTrace: ToolTraceEntry[];
burst: BurstState;
focus: SessionFocus;
/** 启动填空进度(waitingReason.kind === intake 时有值) */
intake?: IntakeProgress;
intakePrompt?: string;
resumed?: boolean;
tokenStats?: {
sessionTotal: number;
sessionCached?: number;
sessionCacheMiss?: number;
lastCaller?: string;
lastTotal?: number;
byCaller?: Record<
string,
{ totalTokens: number; cachedTokens: number; cacheMissTokens: number; calls: number }
>;
};
};
export type SessionAction =
| { type: "send_message"; label: string; placeholder: string }
| { type: "confirm_intake"; label: string }
| { type: "approve"; label: string }
| { type: "accept"; label: string }
| { type: "reject"; label: string }
| { type: "run_outline"; label: string }
| { type: "finish"; label: string };
type ManagedSession = {
runtime: PhaseRuntime;
messages: ChatMessage[];
bookId?: string;
trackingRef: LlmTrackingRef;
};
export class SessionManager {
private readonly sessions = new Map<string, ManagedSession>();
/** bookId → 当前内存中的 sessionId */
private readonly activeBookSessions = new Map<string, string>();
async create(): Promise<SessionView> {
return this.createForBook();
}
setLifecycleStage(sessionId: string, stage: LifecycleStage): SessionView {
const s = this.require(sessionId);
const session = s.runtime.getSession();
if (stage === "play" && !canEnterPlay(session)) {
throw new Error("实例尚未就绪,无法进入游玩");
}
s.runtime.setLifecycleStage(stage);
if (s.bookId) this.persist(s);
return this.toView(sessionId);
}
/**
* 打开 Book:优先恢复磁盘快照;无快照则新建 Session。
* 若该 Book 已在内存中,直接返回当前视图。
*/
async openBook(bookId: string): Promise<SessionView> {
const book = getBook(bookId);
if (!book) throw new Error("Book 不存在");
const inMemory = this.getActiveSessionForBook(bookId);
if (inMemory) return inMemory;
const snapshot = loadBookSession(bookId);
if (snapshot && skillPacksMatch(book, persistedSkillPackId(snapshot))) {
return this.restoreFromSnapshot(snapshot);
}
const legacyPreselect = bookSkillPackId(book);
return this.createForBook(bookId, legacyPreselect);
}
async createForBook(
bookId?: string,
preselectSkillId?: string,
): Promise<SessionView> {
const id = randomUUID();
const messages: ChatMessage[] = [];
const book = bookId ? getBook(bookId) : null;
const initialSession = createSession("default");
initialSession.id = id;
const trackingRef: LlmTrackingRef = {
current: {
sessionId: id,
bookId,
bookTitle: book?.title,
orchestratorId: preselectSkillId ?? (book ? bookSkillPackId(book) : undefined),
},
};
const managed: ManagedSession = {
runtime: null as unknown as PhaseRuntime,
messages,
bookId,
trackingRef,
};
const onMessage = this.buildOnMessageHandler(() => managed);
const llm = createDefaultMainAgentLlm(trackingRef);
const runtime = new PhaseRuntime({
autoStubWorker: !hasRealLlmConfig(),
llm,
onMessage,
initialSession,
});
managed.runtime = runtime;
if (preselectSkillId) {
await runtime.startWithOrchestrator(preselectSkillId);
} else {
await runtime.start();
}
this.sessions.set(id, managed);
if (bookId) {
this.activeBookSessions.set(bookId, id);
appendBookSession(bookId, id);
}
return this.toView(id);
}
get(id: string): SessionView | null {
if (!this.sessions.has(id)) return null;
return this.toView(id);
}
getActiveSessionForBook(bookId: string): SessionView | null {
const sessionId = this.activeBookSessions.get(bookId);
if (!sessionId) return null;
return this.get(sessionId);
}
/**
* 保存快照。
* - instance:实例化后的对象(情境/规则/角色设定等),不含 run 轮次状态
* - run:运行存档,完整进度
*/
saveGameSnapshot(
sessionId: string,
label: string,
kind: SnapshotKind = "run",
note?: string,
): RunSnapshotMeta {
const s = this.require(sessionId);
if (!s.bookId) throw new Error("仅绑定作品时可存档");
const book = getBook(s.bookId);
if (!book) throw new Error("Book 不存在");
const trimmed = label.trim();
if (!trimmed) throw new Error("请输入存档名称");
let runtimeSession = structuredClone(s.runtime.getSession());
let blackboardItems = s.runtime.getBlackboard().exportItems();
if (kind === "instance") {
if (!runtimeSession.slots.startupCompleted) {
throw new Error("实例尚未完成(需先完成启动与 setup 验收),无法保存实例快照");
}
({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({
runtimeSession,
blackboardItems,
}));
}
const skillPackId =
sessionSkillPackId(runtimeSession) ?? bookSkillPackId(book) ?? "";
const snapshot: RunSnapshot = {
version: 1,
id: randomUUID(),
bookId: s.bookId,
label: trimmed,
kind,
orchestratorId: skillPackId,
runtimeSession,
blackboardItems,
messages: s.messages.map((m) => ({ ...m })),
createdAt: new Date().toISOString(),
note: note?.trim() || undefined,
};
saveRunSnapshotFile(snapshot);
return toRunSnapshotMeta(snapshot);
}
/** 列出某 Book 的全部存档 */
listGameSnapshots(bookId: string): RunSnapshotMeta[] {
if (!getBook(bookId)) throw new Error("Book 不存在");
return listRunSnapshots(bookId);
}
/** 从存档读档:替换当前作品进度,可继续创作 */
async loadGameSnapshot(bookId: string, snapshotId: string): Promise<SessionView> {
const snapshot = loadRunSnapshotFile(bookId, snapshotId);
if (!snapshot) throw new Error("存档不存在");
const book = getBook(bookId);
if (!book) throw new Error("Book 不存在");
if (!skillPacksMatch(book, runSnapshotSkillPackId(snapshot))) {
throw new Error("存档与当前作品 skill 包不匹配,无法读档");
}
this.dropBookSessions(bookId);
const newSessionId = randomUUID();
const messages: ChatMessage[] = snapshot.messages.map((m) => ({ ...m })) as ChatMessage[];
let runtimeSession = structuredClone(snapshot.runtimeSession);
let blackboardItems = snapshot.blackboardItems;
if (snapshot.kind === "instance") {
({ runtimeSession, blackboardItems } = materializeInstanceSnapshotPayload({
runtimeSession,
blackboardItems,
}));
}
runtimeSession.id = newSessionId;
const resumeHint =
snapshot.kind === "instance"
? `已加载实例「${snapshot.label}」。可开始运行,或调整角色设定后再模拟。`
: `已从存档「${snapshot.label}」读档,可继续创作。`;
return this.mountRestoredSession({
sessionId: newSessionId,
bookId,
skillPackId: runSnapshotSkillPackId(snapshot) ?? "",
runtimeSession,
blackboardItems,
messages,
resumeHint,
});
}
/** 删除单个存档 */
deleteGameSnapshot(bookId: string, snapshotId: string): void {
if (!getBook(bookId)) throw new Error("Book 不存在");
if (!deleteRunSnapshotFile(bookId, snapshotId)) {
throw new Error("存档不存在");
}
}
/** 删除 Book 时清理内存中的会话 */
dropBookSessions(bookId: string): void {
for (const [sessionId, managed] of this.sessions) {
if (managed.bookId === bookId) {
this.sessions.delete(sessionId);
}
}
this.activeBookSessions.delete(bookId);
}
/** 将当前 API / 预设设置应用到所有活跃会话 */
reloadAllLlms(): number {
let count = 0;
for (const s of this.sessions.values()) {
const llm = reloadDefaultMainAgentLlm(s.trackingRef);
s.runtime.reloadLlm(llm, !hasRealLlmConfig());
count += 1;
}
return count;
}
async sendMessage(id: string, text: string): Promise<SessionView> {
const s = this.require(id);
s.messages.push(this.msg("user", text));
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
const reason = s.runtime.getSession().waitingReason;
try {
if (reason?.kind === "approve_step") {
await s.runtime.rejectStep(text);
} else if (reason?.kind === "review_artifact") {
await s.runtime.rejectArtifact(text);
} else {
await s.runtime.submitInput(text);
}
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
console.error("[session] sendMessage failed:", detail);
s.messages.push(this.msg("system", formatRuntimeError(detail)));
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
}
return this.toView(id);
}
async approve(id: string): Promise<SessionView> {
const s = this.require(id);
try {
await s.runtime.approve();
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
console.error("[session] approve failed:", detail);
s.messages.push(this.msg("system", formatRuntimeError(detail)));
}
return this.toView(id);
}
async confirmIntake(id: string): Promise<SessionView> {
const s = this.require(id);
try {
await s.runtime.confirmIntake();
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
console.error("[session] confirmIntake failed:", detail);
s.messages.push(this.msg("system", formatRuntimeError(detail)));
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
}
return this.toView(id);
}
async accept(id: string): Promise<SessionView> {
const s = this.require(id);
await s.runtime.acceptArtifact();
return this.toView(id);
}
/** 拒绝当前待确认步骤或待验收产物(无说明时触发重新来 / 回到总管) */
async reject(id: string, reason?: string): Promise<SessionView> {
const s = this.require(id);
const waiting = s.runtime.getSession().waitingReason;
try {
if (waiting?.kind === "approve_step") {
await s.runtime.rejectStep(reason ?? "用户暂不执行");
} else if (waiting?.kind === "review_artifact") {
await s.runtime.rejectArtifact(reason ?? "用户要求重新来");
} else {
throw new Error("当前没有可拒绝的确认或验收");
}
} catch (err) {
const detail = err instanceof Error ? err.message : String(err);
console.error("[session] reject failed:", detail);
s.messages.push(this.msg("system", formatRuntimeError(detail)));
if (s.bookId) this.syncBookPreview(s.bookId, s.messages);
}
return this.toView(id);
}
async runOutline(id: string): Promise<SessionView> {
const s = this.require(id);
await s.runtime.submitDecision(
createDecision({
action: "run_worker",
reason: "根据创作简报生成大纲",
workerId: "outline",
requiresApproval: true,
}),
);
return this.toView(id);
}
async finish(id: string): Promise<SessionView> {
const s = this.require(id);
await s.runtime.submitDecision(
createDecision({
action: "finish",
reason: "创作流程结束",
requiresApproval: false,
}),
);
return this.toView(id);
}
private async restoreFromSnapshot(
snapshot: PersistedBookSession,
): Promise<SessionView> {
return this.mountRestoredSession({
sessionId: snapshot.sessionId,
bookId: snapshot.bookId,
skillPackId: persistedSkillPackId(snapshot) ?? "",
runtimeSession: snapshot.runtimeSession,
blackboardItems: snapshot.blackboardItems,
messages: snapshot.messages.map((m) => ({ ...m })) as ChatMessage[],
resumeHint: "已从上次进度恢复,可继续创作。",
});
}
private async mountRestoredSession(params: {
sessionId: string;
bookId: string;
skillPackId: string;
runtimeSession: RuntimeSession;
blackboardItems: import("../types/blackboard.js").BlackboardItem[];
messages: ChatMessage[];
resumeHint: string;
}): Promise<SessionView> {
const book = getBook(params.bookId);
const trackingRef: LlmTrackingRef = {
current: {
sessionId: params.sessionId,
bookId: params.bookId,
bookTitle: book?.title,
orchestratorId: params.skillPackId || undefined,
},
};
let managedRef: ManagedSession | null = null;
const onMessage = this.buildOnMessageHandler(() => managedRef);
const runtime = new PhaseRuntime({
autoStubWorker: !hasRealLlmConfig(),
llm: createDefaultMainAgentLlm(trackingRef),
onMessage,
initialSession: params.runtimeSession,
initialBlackboardItems: params.blackboardItems,
});
await runtime.ensureAvailableSkills();
managedRef = {
runtime,
messages: params.messages,
bookId: params.bookId,
trackingRef,
};
this.sessions.set(params.sessionId, managedRef);
this.activeBookSessions.set(params.bookId, params.sessionId);
appendBookSession(params.bookId, params.sessionId);
this.persist(managedRef);
return this.toView(params.sessionId, true, params.resumeHint);
}
private buildOnMessageHandler(getManaged: () => ManagedSession | null) {
return (text: string) => {
const managed = getManaged();
if (!managed) return;
const usage = managed.trackingRef.current.pendingUsage;
const thinking = managed.trackingRef.current.pendingReasoning;
managed.trackingRef.current.pendingUsage = undefined;
managed.trackingRef.current.pendingReasoning = undefined;
managed.messages.push(this.msg("system", text, usage, thinking));
if (managed.bookId) {
this.syncBookPreview(managed.bookId, managed.messages);
this.persist(managed);
}
};
}
private persist(s: ManagedSession): void {
if (!s.bookId) return;
const book = getBook(s.bookId);
if (!book) return;
const session = s.runtime.getSession();
const skillPackId =
sessionSkillPackId(session) ?? (book ? bookSkillPackId(book) : undefined) ?? "";
const snapshot: PersistedBookSession = {
version: 1,
sessionId: session.id,
bookId: s.bookId,
orchestratorId: skillPackId || undefined,
runtimeSession: session,
blackboardItems: s.runtime.getBlackboard().exportItems(),
messages: s.messages,
savedAt: new Date().toISOString(),
};
saveBookSession(snapshot);
this.syncBookSkill(s.bookId, session);
updateBook(s.bookId, { activeSessionId: session.id });
}
private syncBookSkill(bookId: string, session: RuntimeSession): void {
const snap = session.slots.activeSkill as ActiveSkillSnapshot | undefined;
if (!snap?.name) return;
try {
updateBook(bookId, {
activeSkillId: snap.name,
activeSkillName: snap.name,
});
} catch {
/* book may have been deleted */
}
}
private syncBookPreview(bookId: string, messages: ChatMessage[]): void {
const preview = this.previewFromMessages(messages);
try {
updateBook(bookId, { preview });
} catch {
/* book may have been deleted */
}
}
private previewFromMessages(messages: ChatMessage[]): string {
for (let i = messages.length - 1; i >= 0; i--) {
const t = messages[i].text?.trim();
if (t) return t.slice(0, 80);
}
return "等待开始…";
}
private require(id: string): ManagedSession {
const s = this.sessions.get(id);
if (!s) throw new Error("会话不存在");
return s;
}
private enrichDisplayMessages(
session: RuntimeSession,
messages: ChatMessage[],
): ChatMessage[] {
const reason = session.waitingReason;
if (reason?.kind === "intake" && !session.slots.startupCompleted) {
const prompt = reason.prompt?.trim();
if (!prompt) return messages;
const needle = prompt.slice(0, 48);
const hasStartup = messages.some(
(m) =>
m.kind === "orchestrator_prompt" ||
(typeof m.text === "string" && m.text.includes(needle)),
);
if (hasStartup) return messages;
return [...messages, this.msg("system", prompt)];
}
if (reason?.kind !== "input" || session.slots.startupCompleted) {
return messages;
}
const prompt = reason.message?.trim();
if (!prompt) return messages;
const needle = prompt.slice(0, 48);
const hasStartup = messages.some(
(m) =>
m.kind === "orchestrator_prompt" ||
(typeof m.text === "string" && m.text.includes(needle)),
);
if (hasStartup) return messages;
return [...messages, this.msg("system", prompt)];
}
private msg(
role: ChatMessage["role"],
text: string,
tokenUsage?: MessageTokenUsage,
thinking?: string,
): ChatMessage {
const base = {
id: randomUUID(),
role,
text,
createdAt: new Date().toISOString(),
...(tokenUsage ? { tokenUsage } : {}),
...(thinking ? { thinking } : {}),
};
if (role === "user") {
return {
...base,
kind: "user_input",
title: "你的输入",
body: text,
};
}
const classified = classifyAgentMessage(text);
return { ...base, ...classified };
}
private toView(id: string, resumed = false, resumeHint?: string): SessionView {
const s = this.require(id);
const session = s.runtime.getSession();
const reason = session.waitingReason;
const book = s.bookId ? getBook(s.bookId) : null;
const hints: string[] = [];
const actions: SessionAction[] = [];
let intake: IntakeProgress | undefined;
let intakePrompt: string | undefined;
const activeSnap = session.slots.activeSkill as ActiveSkillSnapshot | undefined;
if (resumeHint) {
hints.push(resumeHint);
} else if (resumed) {
hints.push("已从上次进度恢复,可继续创作。");
}
if (reason?.kind === "intake" && activeSnap?.intakeFields?.length) {
intake = buildIntakeProgress(
activeSnap.intakeFields,
readIntakeValues(session.slots),
);
intakePrompt = reason.prompt;
if (intake.ready) {
hints.push("必要项已齐,可确认进入实例化;也可继续补充可选项。");
actions.push({
type: "confirm_intake",
label: "确认,进入实例化",
});
actions.push({
type: "send_message",
label: "继续补充",
placeholder: "补充可选项或修正已填内容…",
});
} else {
hints.push(
`填写必要项(${intake.requiredFilled}/${intake.requiredTotal})后可确认进入实例化`,
);
actions.push({
type: "send_message",
label: "发送",
placeholder: "按上方说明补充信息,可一次说多项…",
});
}
} else if (reason?.kind === "skill_selection") {
hints.push("请选择创作类型(输入 skill 名称或编号)");
actions.push({
type: "send_message",
label: "发送",
placeholder: "例如:basic 或 1",
});
} else if (reason?.kind === "intake") {
hints.push(reason.prompt?.trim() ?? "请按填空项补充创作信息");
actions.push({
type: "send_message",
label: "发送",
placeholder: "描述你想写什么…",
});
} else if (reason?.kind === "input") {
if (reason.message?.trim()) {
hints.push(reason.message.trim());
} else {
hints.push("请回答启动问题,或补充创作目标");
}
actions.push({
type: "send_message",
label: "发送",
placeholder: "描述你想写什么…",
});
} else if (reason?.kind === "worker_questions") {
const qs = reason.questions.filter((q) => q?.trim());
if (qs.length) {
hints.push(`Worker · ${reason.workerId} 提问:${qs.join(" ")}`);
} else {
hints.push(`Worker · ${reason.workerId} 需要更多信息,请补充说明`);
}
const placeholder =
qs[0]?.slice(0, 120) ?? "回答 Worker 的问题,或补充情境与参数…";
actions.push({
type: "send_message",
label: "发送",
placeholder,
});
} else if (reason?.kind === "approve_step") {
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: "不接受,重新来" });
} else if (
session.phase === "running" &&
s.runtime.needsMainAgentDecision() &&
!s.runtime.hasMainAgent()
) {
hints.push("简报已就绪,可手动生成大纲(未配置 Agent LLM)");
actions.push({ type: "run_outline", label: "生成大纲" });
actions.push({
type: "send_message",
label: "发送",
placeholder: "补充说明…",
});
} else if (session.phase === "running" && s.runtime.needsMainAgentDecision()) {
hints.push("Agent 正在调度…");
} else if (session.phase === "running" && session.pendingArtifactId) {
/* worker 运行中 */
} else if (
session.phase === "running" &&
session.slots.startupCompleted &&
!session.pendingDecision
) {
actions.push({ type: "finish", label: "结束流程" });
} else if (session.phase === "done") {
hints.push("流程已完成");
} else {
actions.push({
type: "send_message",
label: "发送",
placeholder: "输入消息…",
});
}
if (s.bookId) {
this.persist(s);
}
const tokens = getSessionTokenTotals(id);
const messages = this.enrichDisplayMessages(session, [...s.messages]);
const lifecycleStage = inferLifecycleStage(session);
const skillPackId =
sessionSkillPackId(session) ?? (book ? bookSkillPackId(book) : undefined);
const skillCatalog = buildSkillCatalog(
session,
skillPackId,
lifecycleStage,
);
return {
id,
bookId: s.bookId,
bookTitle: book?.title,
activeSkill: s.runtime.getActiveSkill()?.name ?? skillPackId,
phase: session.phase,
waitingReason: reason,
startupCompleted: Boolean(session.slots.startupCompleted),
skills: s.runtime.getAvailableSkills(),
messages,
hints,
actions,
pipeline: buildPipeline(session),
lifecycleStage,
playReady: canEnterPlay(session),
skillCatalog,
toolTrace: buildToolTrace(messages),
burst: buildBurstState(messages, session),
focus: buildFocus(session, reason, intake, lifecycleStage),
intake,
intakePrompt,
resumed: resumed || undefined,
tokenStats: {
sessionTotal: tokens.totalTokens,
sessionCached: tokens.totalCached || undefined,
sessionCacheMiss: tokens.totalCacheMiss || undefined,
lastCaller: tokens.last?.caller,
lastTotal: tokens.last?.totalTokens,
byCaller: tokens.byCaller,
},
};
}
}
export const sessionManager = new SessionManager();
function formatRuntimeError(detail: string): string {
if (detail.includes("401") || detail.toLowerCase().includes("authentication")) {
return (
"[请求失败] API Key 无效或未授权。请到「设置 → API 配置」检查 Key 与 Base URL," +
"或点击「测试连接」验证。"
);
}
if (detail.startsWith("LLM request failed")) {
return `[请求失败] ${detail.replace(/^LLM request failed \(\d+\): /, "").slice(0, 300)}`;
}
if (detail.startsWith("Main Agent returned invalid JSON")) {
return "[请求失败] 总管返回了无效 JSON,请检查模型是否支持 json 输出,或暂时关闭预设后重试。";
}
return `[请求失败] ${detail.slice(0, 300)}`;
}