feat: queue master-agent chat replies
This commit is contained in:
@@ -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-Age:2592000 秒。",
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user