feat: complete chat routing and openai onboarding

This commit is contained in:
kris
2026-03-31 03:31:22 +08:00
parent 5b590f7cc1
commit 9c02ebb574
25 changed files with 2241 additions and 133 deletions

View File

@@ -1,6 +1,7 @@
import { randomBytes } from "node:crypto";
import {
AUTH_SESSION_TTL_MS,
aiRoleLabel,
aiProviderLabel,
appendProjectMessage,
completeMasterAgentTask,
@@ -13,10 +14,11 @@ import {
previewDeviceImportResolution,
queueMasterAgentTask,
readState,
isDispatchableThreadProject,
updateAttachmentAnalysisResult,
updateAiAccountHealth,
} from "@/lib/boss-data";
import type { DispatchPlanTarget, GroupConversationMember, Project } from "@/lib/boss-data";
import type { DispatchPlanTarget, Project } 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";
@@ -32,6 +34,21 @@ function buildMasterAgentInstructions() {
].join("\n");
}
function buildThreadConversationReplyPrompt(project: Project, requestText: string) {
return [
"你正在代表某个 Codex 线程回复 Boss 控制台里的单线程会话。",
"你不是主 Agent不要使用“主 Agent”口吻不要写总结不要解释调度过程。",
"请直接像该线程本人一样,用中文回复用户当前这条消息。",
"如果信息不足,要明确说缺什么;不要假装已经执行过设备操作。",
"输出要求:只输出线程要回给用户的正文,不要输出 JSON、代码块或额外前缀。",
`threadProjectId: ${project.id}`,
`threadTitle: ${project.threadMeta.threadDisplayName}`,
`folderName: ${project.threadMeta.folderName}`,
`deviceIds: ${project.deviceIds.join(",")}`,
`requestText: ${requestText}`,
].join("\n");
}
function buildRuntimeDigest(
state: Awaited<ReturnType<typeof readState>>,
requestText: string,
@@ -133,11 +150,93 @@ function extractResponseText(payload: unknown): string {
function normalizeOpenAiError(message: string) {
const trimmed = message.trim();
const lowered = trimmed.toLowerCase();
if (lowered.includes("network is unreachable") || lowered.includes("enetunreach")) {
return "服务器当前无法访问 api.openai.com请先恢复服务器出网或先切回 Master Codex Node。";
}
if (lowered.includes("fetch failed") || lowered.includes("connect timeout") || lowered.includes("timed out")) {
return "服务器当前无法连接 OpenAI API请检查出网、代理或防火墙配置。";
}
if (!trimmed) return "主 Agent 当前调用模型失败。";
if (trimmed.length <= 240) return trimmed;
return `${trimmed.slice(0, 237)}...`;
}
function normalizeOpenAiFetchFailure(error: unknown) {
if (error instanceof Error) {
const causeCode =
typeof (error as Error & { cause?: { code?: string } }).cause?.code === "string"
? (error as Error & { cause?: { code?: string } }).cause?.code
: "";
const causeMessage =
(error as Error & { cause?: { message?: string } }).cause?.message?.trim() || "";
return normalizeOpenAiError([error.message, causeCode, causeMessage].filter(Boolean).join(" "));
}
return normalizeOpenAiError(String(error));
}
function fallbackAiRolePriority(role: "primary" | "backup" | "api_fallback") {
switch (role) {
case "primary":
return 0;
case "backup":
return 1;
case "api_fallback":
return 2;
default:
return 9;
}
}
async function findFallbackOpenAiAccount(excludedAccountId?: string) {
const state = await readState();
return [...state.aiAccounts]
.filter(
(account) =>
account.accountId !== excludedAccountId &&
account.enabled &&
account.provider === "openai_api" &&
Boolean(account.apiKey?.trim()),
)
.sort((left, right) => {
const roleDelta = fallbackAiRolePriority(left.role) - fallbackAiRolePriority(right.role);
if (roleDelta !== 0) return roleDelta;
return (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "");
})[0];
}
async function replyViaOpenAiAccount(params: {
account: Awaited<ReturnType<typeof findFallbackOpenAiAccount>>;
requestText: string;
currentSessionExpiresAt?: string;
senderLabel: string;
}) {
if (!params.account?.apiKey?.trim()) {
throw new Error("OPENAI_ACCOUNT_NOT_CONFIGURED");
}
const generated = await generateOpenAiReply({
apiKey: params.account.apiKey,
model: params.account.model || "gpt-5.4",
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
});
await appendMasterAgentSystemReply(generated.content, params.senderLabel);
await updateAiAccountHealth({
accountId: params.account.accountId,
status: "ready",
lastValidatedAt: new Date().toISOString(),
lastUsedAt: new Date().toISOString(),
});
return {
ok: true as const,
accountId: params.account.accountId,
requestId: generated.requestId,
};
}
async function generateOpenAiReply(params: {
apiKey: string;
model: string;
@@ -145,20 +244,25 @@ async function generateOpenAiReply(params: {
currentSessionExpiresAt?: string;
}) {
const state = await readState();
const response = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
Authorization: `Bearer ${params.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: params.model,
reasoning: { effort: "medium" },
instructions: buildMasterAgentInstructions(),
input: buildRuntimeDigest(state, params.requestText, params.currentSessionExpiresAt),
}),
signal: AbortSignal.timeout(45_000),
});
let response: Response;
try {
response = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
Authorization: `Bearer ${params.apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: params.model,
reasoning: { effort: "medium" },
instructions: buildMasterAgentInstructions(),
input: buildRuntimeDigest(state, params.requestText, params.currentSessionExpiresAt),
}),
signal: AbortSignal.timeout(45_000),
});
} catch (error) {
throw new Error(normalizeOpenAiFetchFailure(error));
}
const requestId = response.headers.get("x-request-id") ?? undefined;
const payload = (await response.json().catch(() => null)) as
@@ -192,6 +296,62 @@ async function generateOpenAiReply(params: {
};
}
export async function probeOpenAiApiAccount(params: {
apiKey: string;
model?: string;
}) {
const apiKey = params.apiKey.trim();
if (!apiKey) {
throw new Error("当前账号还没有可用的 OpenAI API Key。");
}
const model = params.model?.trim() || "gpt-5.4";
let response: Response;
try {
response = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
reasoning: { effort: "low" },
instructions: "你正在执行 OpenAI API 连接自检。请只回复“连接正常”。",
input: "请只回复“连接正常”。",
}),
signal: AbortSignal.timeout(15_000),
});
} catch (error) {
throw new Error(normalizeOpenAiFetchFailure(error));
}
const requestId = response.headers.get("x-request-id") ?? undefined;
const payload = (await response.json().catch(() => null)) as
| { error?: { message?: string } }
| null;
if (!response.ok) {
const apiError =
payload && typeof payload === "object" && "error" in payload
? payload.error?.message
: undefined;
throw new Error(
normalizeOpenAiError(
`${apiError ?? `OpenAI API ${response.status}`}${requestId ? ` (request_id=${requestId})` : ""}`,
),
);
}
const content = extractResponseText(payload) || "连接正常。";
return {
ok: true as const,
message: content,
requestId,
model,
};
}
async function appendMasterAgentSystemReply(body: string, senderLabel = "主 Agent") {
return appendProjectMessage({
projectId: "master-agent",
@@ -229,15 +389,11 @@ function summarizeDispatchRequest(requestText: string) {
}
function collectGroupDispatchTargets(
state: Awaited<ReturnType<typeof readState>>,
project: Project,
requestText: string,
): DispatchPlanTarget[] {
const members: Array<
Pick<
GroupConversationMember,
"deviceId" | "projectId" | "threadId" | "threadDisplayName" | "folderName"
>
> =
const members =
project.groupMembers.length > 0
? project.groupMembers
: project.deviceIds.map((deviceId) => ({
@@ -249,20 +405,27 @@ function collectGroupDispatchTargets(
}));
return members
.map((member) => ({
deviceId: member.deviceId,
projectId: member.projectId,
threadId: member.threadId,
threadDisplayName: member.threadDisplayName,
folderName: member.folderName,
.map((member) => {
const candidate = state.projects.find((projectCandidate) => projectCandidate.id === member.projectId);
if (!candidate) {
throw new Error("DISPATCH_TARGET_PROJECT_NOT_FOUND");
}
return candidate;
})
.filter((candidate) => isDispatchableThreadProject(candidate))
.map((candidate) => ({
deviceId: candidate.deviceIds[0] ?? candidate.id,
projectId: candidate.id,
threadId: candidate.threadMeta.threadId,
threadDisplayName: candidate.threadMeta.threadDisplayName,
folderName: candidate.threadMeta.folderName,
codexFolderRef: candidate.threadMeta.codexFolderRef,
codexThreadRef: candidate.threadMeta.codexThreadRef,
reason: `群聊消息“${summarizeDispatchRequest(requestText)}”需要该线程补充状态或执行建议。`,
}))
.filter((target, index, array) => {
const signature = `${target.projectId}::${target.deviceId}::${target.threadId}`;
return (
array.findIndex((item) => `${item.projectId}::${item.deviceId}::${item.threadId}` === signature) ===
index
);
return array.findIndex((item) => `${item.projectId}::${item.deviceId}::${item.threadId}` === signature) === index;
});
}
@@ -336,7 +499,7 @@ async function resolveGroupDispatchPlanTask(taskId: string): Promise<GroupDispat
throw new Error("PROJECT_NOT_GROUP_CHAT");
}
const targets = collectGroupDispatchTargets(project, task.requestText);
const targets = collectGroupDispatchTargets(state, project, task.requestText);
if (targets.length === 0) {
throw new Error("GROUP_DISPATCH_TARGETS_REQUIRED");
}
@@ -404,6 +567,43 @@ export async function queueGroupDispatchPlan(params: {
return resolveGroupDispatchPlanTask(task.taskId);
}
export async function queueThreadConversationReplyTask(params: {
projectId: string;
requestMessageId: string;
requestText: string;
requestedBy: string;
requestedByAccount: string;
}) {
const state = await readState();
const project = state.projects.find((item) => item.id === params.projectId);
if (!project) {
throw new Error("PROJECT_NOT_FOUND");
}
if (project.isGroup) {
throw new Error("PROJECT_NOT_SINGLE_THREAD");
}
if (project.id === "master-agent") {
throw new Error("PROJECT_NOT_THREAD_CONVERSATION");
}
const deviceId = project.deviceIds[0] || state.user.boundDeviceId || "mac-studio";
return queueMasterAgentTask({
projectId: project.id,
taskType: "conversation_reply",
requestMessageId: params.requestMessageId,
requestText: params.requestText,
executionPrompt: buildThreadConversationReplyPrompt(project, params.requestText),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedByAccount,
deviceId,
targetProjectId: project.id,
targetThreadId: project.threadMeta.threadId,
targetThreadDisplayName: project.threadMeta.threadDisplayName,
targetCodexThreadRef: project.threadMeta.codexThreadRef,
targetCodexFolderRef: project.threadMeta.codexFolderRef,
});
}
function buildDeviceImportResolutionPrompt(params: {
deviceName: string;
deviceId: string;
@@ -813,10 +1013,9 @@ export async function validateAiAccountConnection(accountId: string) {
};
}
const generated = await generateOpenAiReply({
const generated = await probeOpenAiApiAccount({
apiKey: account.apiKey,
model: account.model || "gpt-5.4",
requestText: "请只回复“连接正常”。",
});
await updateAiAccountHealth({
@@ -829,7 +1028,7 @@ export async function validateAiAccountConnection(accountId: string) {
return {
ok: true as const,
status: "ready",
message: generated.content,
message: generated.message,
requestId: generated.requestId,
};
}
@@ -867,6 +1066,19 @@ export async function replyToMasterAgentUserMessage(params: {
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) {
try {
return await replyViaOpenAiAccount({
account: fallbackAccount,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
senderLabel: `主 Agent · ${fallbackAccount.label || aiRoleLabel(fallbackAccount.role)}`,
});
} catch {
// Fall through to the original offline guidance when the fallback API account cannot respond.
}
}
await appendMasterAgentSystemReply(
`主 GPT 不在手机里直接登录。当前绑定设备 ${boundNodeLabel}${boundDevice ? " 不在线" : " 未找到"},主 Agent 暂时无法通过这台设备对话。请先在该设备上登录 Codex / ChatGPT Plus并确保 local-agent 在线后再重试。`,
`主 Agent · ${runtime.summary.roleLabel}`,
@@ -898,6 +1110,19 @@ export async function replyToMasterAgentUserMessage(params: {
};
}
if (completedTask?.status === "failed") {
const fallbackAccount = await findFallbackOpenAiAccount(runtime.account.accountId);
if (fallbackAccount) {
try {
return await replyViaOpenAiAccount({
account: fallbackAccount,
requestText: params.requestText,
currentSessionExpiresAt: params.currentSessionExpiresAt,
senderLabel: `主 Agent · ${fallbackAccount.label || aiRoleLabel(fallbackAccount.role)}`,
});
} catch {
// Preserve the original execution failure below if the fallback account also fails.
}
}
return {
ok: false as const,
reason: "MASTER_NODE_EXEC_FAILED",