feat: queue master-agent chat replies

This commit is contained in:
kris
2026-03-31 19:59:08 +08:00
parent e741952295
commit 013d9566be
8 changed files with 930 additions and 17 deletions

View File

@@ -7,6 +7,7 @@ import {
completeMasterAgentTask,
getProjectAttachment,
getAttachmentStorageConfig,
getProjectAgentControls,
getLatestDeviceImportDraft,
getRuntimeAiAccountById,
getMasterAgentRuntimeAccount,
@@ -18,11 +19,38 @@ import {
updateAttachmentAnalysisResult,
updateAiAccountHealth,
} from "@/lib/boss-data";
import type { DispatchPlanTarget, Project } from "@/lib/boss-data";
import type { DispatchPlanTarget, Project, ProjectAgentControls, ReasoningEffort } from "@/lib/boss-data";
import { canInlineAttachmentText, extractAttachmentTextExcerpt } from "@/lib/boss-attachments";
import { readAliyunOssObjectBuffer } from "@/lib/boss-storage-aliyun-oss";
import { readServerFileAttachmentBuffer } from "@/lib/boss-storage-server-file";
type MasterAgentReplyState = "queued" | "running" | "completed";
const OPENAI_MASTER_AGENT_DEVICE_ID = "master-agent-openai";
type QueuedMasterAgentReplyEnvelope = {
ok: true;
accountId: string;
taskId: string;
masterReplyState: MasterAgentReplyState;
task: {
taskId: string;
taskType: "conversation_reply";
status: MasterAgentReplyState;
};
};
function buildAgentControlsDigest(agentControls?: ProjectAgentControls | null) {
if (!agentControls) {
return "当前对话覆盖:无";
}
return [
"当前对话覆盖:",
`model=${agentControls.modelOverride ?? "默认"}`,
`reasoning=${agentControls.reasoningEffortOverride ?? "默认"}`,
].join(" ");
}
function buildMasterAgentInstructions() {
return [
"你是 Boss 控制台的主 Agent。",
@@ -53,6 +81,7 @@ function buildRuntimeDigest(
state: Awaited<ReturnType<typeof readState>>,
requestText: string,
currentSessionExpiresAt?: string,
agentControls?: ProjectAgentControls | null,
) {
const recentMessages = state.projects
.find((project) => project.id === "master-agent")
@@ -91,6 +120,7 @@ function buildRuntimeDigest(
`登录会话策略:成功登录后默认保持 ${Math.round(AUTH_SESSION_TTL_MS / 24 / 60 / 60_000)} 天。`,
"Cookie Max-Age2592000 秒。",
currentSessionExpiresAt ? `当前请求会话到期时间:${currentSessionExpiresAt}` : undefined,
buildAgentControlsDigest(agentControls),
]
.filter(Boolean)
.join("\n");
@@ -210,6 +240,7 @@ async function replyViaOpenAiAccount(params: {
requestText: string;
currentSessionExpiresAt?: string;
senderLabel: string;
agentControls?: ProjectAgentControls | null;
}) {
if (!params.account?.apiKey?.trim()) {
throw new Error("OPENAI_ACCOUNT_NOT_CONFIGURED");
@@ -217,9 +248,11 @@ async function replyViaOpenAiAccount(params: {
const generated = await generateOpenAiReply({
apiKey: params.account.apiKey,
model: params.account.model || "gpt-5.4",
model: params.agentControls?.modelOverride || params.account.model || "gpt-5.4",
reasoningEffort: params.agentControls?.reasoningEffortOverride || "medium",
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls: params.agentControls,
});
await appendMasterAgentSystemReply(generated.content, params.senderLabel);
@@ -240,8 +273,10 @@ async function replyViaOpenAiAccount(params: {
async function generateOpenAiReply(params: {
apiKey: string;
model: string;
reasoningEffort: ReasoningEffort;
requestText: string;
currentSessionExpiresAt?: string;
agentControls?: ProjectAgentControls | null;
}) {
const state = await readState();
let response: Response;
@@ -254,9 +289,14 @@ async function generateOpenAiReply(params: {
},
body: JSON.stringify({
model: params.model,
reasoning: { effort: "medium" },
reasoning: { effort: params.reasoningEffort },
instructions: buildMasterAgentInstructions(),
input: buildRuntimeDigest(state, params.requestText, params.currentSessionExpiresAt),
input: buildRuntimeDigest(
state,
params.requestText,
params.currentSessionExpiresAt,
params.agentControls,
),
}),
signal: AbortSignal.timeout(45_000),
});
@@ -296,6 +336,120 @@ async function generateOpenAiReply(params: {
};
}
function buildMasterOpenAiReplyPrompt(
state: Awaited<ReturnType<typeof readState>>,
requestText: string,
currentSessionExpiresAt?: string,
agentControls?: ProjectAgentControls | null,
) {
return [
buildMasterAgentInstructions(),
"",
buildRuntimeDigest(state, requestText, currentSessionExpiresAt, agentControls),
].join("\n");
}
async function queueAndStartOpenAiMasterAgentReply(params: {
taskId: string;
deviceId: string;
requestText: string;
currentSessionExpiresAt?: string;
apiKey: string;
model: string;
reasoningEffort: ReasoningEffort;
agentControls?: ProjectAgentControls | null;
}) {
const timer = setTimeout(() => {
void (async () => {
const task = await getMasterAgentTask(params.taskId);
if (!task || task.status !== "queued") {
return;
}
try {
const generated = await generateOpenAiReply({
apiKey: params.apiKey,
model: params.model,
reasoningEffort: params.reasoningEffort,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls: params.agentControls,
});
await completeMasterAgentTask({
taskId: params.taskId,
deviceId: params.deviceId,
status: "completed",
replyBody: generated.content,
requestId: generated.requestId,
});
} catch (error) {
await completeMasterAgentTask({
taskId: params.taskId,
deviceId: params.deviceId,
status: "failed",
errorMessage: error instanceof Error ? error.message : "主 Agent 当前调用模型失败。",
});
}
})();
}, 0);
timer.unref?.();
}
async function enqueueOpenAiMasterAgentReply(params: {
accountId: string;
accountLabel: string;
requestMessageId?: string;
requestText: string;
requestedBy: string;
requestedByAccount: string;
currentSessionExpiresAt?: string;
apiKey: string;
model: string;
reasoningEffort: ReasoningEffort;
agentControls?: ProjectAgentControls | null;
}) {
const state = await readState();
const task = await queueMasterAgentTask({
requestMessageId: params.requestMessageId ?? "master-agent-manual",
requestText: params.requestText,
executionPrompt: buildMasterOpenAiReplyPrompt(
state,
params.requestText,
params.currentSessionExpiresAt,
params.agentControls,
),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
deviceId: OPENAI_MASTER_AGENT_DEVICE_ID,
accountId: params.accountId,
accountLabel: params.accountLabel,
});
void queueAndStartOpenAiMasterAgentReply({
taskId: task.taskId,
deviceId: OPENAI_MASTER_AGENT_DEVICE_ID,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
apiKey: params.apiKey,
model: params.model,
reasoningEffort: params.reasoningEffort,
agentControls: params.agentControls,
});
const queuedReply: QueuedMasterAgentReplyEnvelope = {
ok: true as const,
accountId: params.accountId,
taskId: task.taskId,
masterReplyState: "queued" as const,
task: {
taskId: task.taskId,
taskType: "conversation_reply" as const,
status: "queued" as const,
},
};
return queuedReply;
}
export async function probeOpenAiApiAccount(params: {
apiKey: string;
model?: string;
@@ -366,14 +520,16 @@ function buildMasterCodexNodePrompt(
state: Awaited<ReturnType<typeof readState>>,
requestText: string,
currentSessionExpiresAt?: string,
agentControls?: ProjectAgentControls | null,
) {
return [
"你是 Boss 控制台的主 Agent运行在用户自己的 Master Codex Node 上。",
"请结合下面的运行时状态和用户消息,直接给出中文回复。",
"如果你认为需要继续在当前仓库里推进实现、排障或验证,可以直接说明你下一步会做什么;如果必须先做交接或收尾,也要明确说出原因。",
"保持简洁,优先给出结论、动作、验证点。",
buildAgentControlsDigest(agentControls),
"",
buildRuntimeDigest(state, requestText, currentSessionExpiresAt),
buildRuntimeDigest(state, requestText, currentSessionExpiresAt, agentControls),
].join("\n");
}
@@ -1039,8 +1195,10 @@ export async function replyToMasterAgentUserMessage(params: {
requestedBy: string;
requestedByAccount: string;
currentSessionExpiresAt?: string;
mode?: "wait" | "enqueue";
}) {
const runtime = await getMasterAgentRuntimeAccount();
const agentControls = await getProjectAgentControls("master-agent");
if (!runtime?.account) {
await appendMasterAgentSystemReply(
@@ -1049,6 +1207,96 @@ export async function replyToMasterAgentUserMessage(params: {
return { ok: false as const, reason: "NO_AI_ACCOUNT" };
}
if (params.mode === "enqueue") {
if (runtime.account.provider === "master_codex_node") {
const state = await readState();
const deviceId = runtime.account.nodeId || state.user.boundDeviceId || "mac-studio";
const boundDevice = state.devices.find((device) => device.id === deviceId);
const boundNodeLabel =
runtime.account.nodeLabel?.trim() ||
boundDevice?.name ||
state.user.boundCodexNodeLabel ||
deviceId;
if (!boundDevice || boundDevice.status !== "online") {
await updateAiAccountHealth({
accountId: runtime.account.accountId,
status: "degraded",
lastError: !boundDevice ? "MASTER_CODEX_NODE_DEVICE_NOT_FOUND" : "MASTER_CODEX_NODE_DEVICE_OFFLINE",
lastValidatedAt: new Date().toISOString(),
});
const fallbackAccount = await findFallbackOpenAiAccount(runtime.account.accountId);
if (fallbackAccount?.apiKey?.trim()) {
return enqueueOpenAiMasterAgentReply({
accountId: fallbackAccount.accountId,
accountLabel: fallbackAccount.label || aiRoleLabel(fallbackAccount.role),
requestMessageId: params.requestMessageId,
requestText: params.requestText,
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
currentSessionExpiresAt: params.currentSessionExpiresAt,
apiKey: fallbackAccount.apiKey,
model: agentControls?.modelOverride || fallbackAccount.model || "gpt-5.4",
reasoningEffort: agentControls?.reasoningEffortOverride || "medium",
agentControls,
});
}
await appendMasterAgentSystemReply(
`主 GPT 不在手机里直接登录。当前绑定设备 ${boundNodeLabel}${boundDevice ? " 不在线" : " 未找到"},主 Agent 暂时无法通过这台设备对话。请先在该设备上登录 Codex / ChatGPT Plus并确保 local-agent 在线后再重试。`,
`主 Agent · ${runtime.summary.roleLabel}`,
);
return { ok: false as const, reason: "MASTER_NODE_OFFLINE" };
}
const task = await queueMasterAgentTask({
requestMessageId: params.requestMessageId ?? "master-agent-manual",
requestText: params.requestText,
executionPrompt: buildMasterCodexNodePrompt(
state,
params.requestText,
params.currentSessionExpiresAt,
agentControls,
),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
deviceId,
accountId: runtime.account.accountId,
accountLabel: runtime.account.label || runtime.summary.roleLabel,
});
const queuedReply: QueuedMasterAgentReplyEnvelope = {
ok: true as const,
accountId: runtime.account.accountId,
taskId: task.taskId,
masterReplyState: "queued" as const,
task: {
taskId: task.taskId,
taskType: "conversation_reply" as const,
status: "queued" as const,
},
};
return queuedReply;
}
if (runtime.account.provider === "openai_api" && runtime.account.apiKey?.trim()) {
return enqueueOpenAiMasterAgentReply({
accountId: runtime.account.accountId,
accountLabel: runtime.account.label || runtime.summary.roleLabel,
requestMessageId: params.requestMessageId,
requestText: params.requestText,
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
currentSessionExpiresAt: params.currentSessionExpiresAt,
apiKey: runtime.account.apiKey,
model: agentControls?.modelOverride || runtime.account.model || "gpt-5.4",
reasoningEffort: agentControls?.reasoningEffortOverride || "medium",
agentControls,
});
}
}
if (runtime.account.provider === "master_codex_node") {
const state = await readState();
const deviceId = runtime.account.nodeId || state.user.boundDeviceId || "mac-studio";
@@ -1074,6 +1322,7 @@ export async function replyToMasterAgentUserMessage(params: {
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
senderLabel: `主 Agent · ${fallbackAccount.label || aiRoleLabel(fallbackAccount.role)}`,
agentControls,
});
} catch {
// Fall through to the original offline guidance when the fallback API account cannot respond.
@@ -1093,6 +1342,7 @@ export async function replyToMasterAgentUserMessage(params: {
state,
params.requestText,
params.currentSessionExpiresAt,
agentControls,
),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
@@ -1118,6 +1368,7 @@ export async function replyToMasterAgentUserMessage(params: {
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
senderLabel: `主 Agent · ${fallbackAccount.label || aiRoleLabel(fallbackAccount.role)}`,
agentControls,
});
} catch {
// Preserve the original execution failure below if the fallback account also fails.
@@ -1156,9 +1407,11 @@ export async function replyToMasterAgentUserMessage(params: {
try {
const generated = await generateOpenAiReply({
apiKey: runtime.account.apiKey,
model: runtime.account.model || "gpt-5.4",
model: agentControls?.modelOverride || runtime.account.model || "gpt-5.4",
reasoningEffort: agentControls?.reasoningEffortOverride || "medium",
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
agentControls,
});
await appendMasterAgentSystemReply(