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

@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { readState, saveAiAccount } from "@/lib/boss-data";
import { validateAiAccountConnection } from "@/lib/boss-master-agent";
function chooseMasterPrimaryAccountId(state: Awaited<ReturnType<typeof readState>>) {
return (
state.aiAccounts.find((item) => item.provider === "master_codex_node" && item.role === "primary")
?.accountId || "master-codex-primary"
);
}
export async function POST(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const body = (await request.json().catch(() => ({}))) as {
label?: string;
displayName?: string;
accountIdentifier?: string;
nodeId?: string;
nodeLabel?: string;
model?: string;
setActive?: boolean;
};
if (!body.displayName?.trim()) {
return NextResponse.json({ ok: false, message: "显示名称不能为空。" }, { status: 400 });
}
if (!body.nodeId?.trim()) {
return NextResponse.json({ ok: false, message: "请先填写节点 ID。" }, { status: 400 });
}
try {
const state = await readState();
const accountId = chooseMasterPrimaryAccountId(state);
const account = await saveAiAccount({
accountId,
label: body.label?.trim() || "主 GPT",
role: "primary",
provider: "master_codex_node",
displayName: body.displayName.trim(),
accountIdentifier: body.accountIdentifier?.trim() || undefined,
nodeId: body.nodeId.trim(),
nodeLabel: body.nodeLabel?.trim() || undefined,
model: body.model?.trim() || "gpt-5.4",
enabled: true,
setActive: body.setActive !== false,
loginStatusNote: "节点绑定信息已保存,请在绑定设备上的 Codex / ChatGPT Plus 会话里完成登录。",
});
const validation = await validateAiAccountConnection(account.accountId);
return NextResponse.json({
ok: true,
accountId: account.accountId,
account,
validation,
message:
body.setActive === false
? "Master Codex Node 绑定信息已保存。"
: "Master Codex Node 已绑定,并设为当前主控。",
});
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "MASTER_NODE_ONBOARD_FAILED" },
{ status: 400 },
);
}
}

View File

@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { getMasterIdentitySummaryFromState, readState, saveAiAccount, updateAiAccountHealth } from "@/lib/boss-data";
import { probeOpenAiApiAccount } from "@/lib/boss-master-agent";
function chooseOpenAiPrimaryAccountId(state: Awaited<ReturnType<typeof readState>>) {
return (
state.aiAccounts.find((item) => item.provider === "openai_api" && item.role === "primary")?.accountId ||
"openai-api-primary"
);
}
export async function POST(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const body = (await request.json().catch(() => ({}))) as {
label?: string;
displayName?: string;
accountIdentifier?: string;
model?: string;
apiKey?: string;
};
if (!body.displayName?.trim()) {
return NextResponse.json({ ok: false, message: "显示名称不能为空。" }, { status: 400 });
}
if (!body.apiKey?.trim()) {
return NextResponse.json({ ok: false, message: "请先填写 OpenAI API Key。" }, { status: 400 });
}
try {
const probe = await probeOpenAiApiAccount({
apiKey: body.apiKey,
model: body.model,
});
const state = await readState();
const accountId = chooseOpenAiPrimaryAccountId(state);
const account = await saveAiAccount({
accountId,
label: body.label?.trim() || "主 GPT",
role: "primary",
provider: "openai_api",
displayName: body.displayName.trim(),
accountIdentifier: body.accountIdentifier?.trim() || undefined,
model: probe.model,
apiKey: body.apiKey.trim(),
enabled: true,
setActive: true,
loginStatusNote: "已在手机端登录 OpenAI 平台账号,可直接作为当前主控。",
});
await updateAiAccountHealth({
accountId: account.accountId,
status: "ready",
lastError: undefined,
lastValidatedAt: new Date().toISOString(),
lastUsedAt: new Date().toISOString(),
});
const nextState = await readState();
return NextResponse.json({
ok: true,
accountId: account.accountId,
account,
activeIdentity: getMasterIdentitySummaryFromState(nextState),
requestId: probe.requestId,
message: "OpenAI 平台账号已登录,并设为当前主控。",
});
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "OPENAI_ONBOARD_FAILED" },
{ status: 400 },
);
}
}

View File

@@ -3,9 +3,21 @@ import { requireRequestSession } from "@/lib/boss-auth";
import { appendProjectMessage, readState } from "@/lib/boss-data";
import {
queueGroupDispatchPlan,
queueThreadConversationReplyTask,
replyToMasterAgentUserMessage,
} from "@/lib/boss-master-agent";
function dispatchFailureNotice(error?: string) {
switch (error) {
case "GROUP_DISPATCH_TARGETS_REQUIRED":
return "当前群聊里还没有可下发的真实线程,请先在群资料里重新添加线程后再试。";
case "DISPATCH_TARGET_PROJECT_NOT_FOUND":
return "当前群聊里有失效的线程引用,请重新整理群成员后再试。";
default:
return error ? `主 Agent 暂时无法生成推荐线程:${error}` : "主 Agent 暂时无法生成推荐线程,请稍后重试。";
}
}
export async function POST(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
@@ -37,8 +49,22 @@ export async function POST(
}
| null = null;
let masterReply:
| { ok: boolean; reason?: string; message?: string; accountId?: string; requestId?: string }
| {
ok: boolean;
reason?: string;
message?: string;
accountId?: string;
requestId?: string;
taskId?: string;
}
| undefined;
let task:
| {
taskId: string;
taskType: "conversation_reply";
status: "queued" | "completed";
}
| null = null;
const state = await readState();
const project = state.projects.find((item) => item.id === projectId);
@@ -58,13 +84,42 @@ export async function POST(
});
dispatchRecommendation = recommendation;
dispatchPlan = recommendation.dispatchPlan;
if (!recommendation.ok) {
await appendProjectMessage({
projectId,
sender: "master",
senderLabel: "主 Agent",
body: dispatchFailureNotice(recommendation.error),
kind: "system_notice",
});
}
} catch (error) {
dispatchRecommendation = {
ok: false,
status: "failed",
error: error instanceof Error ? error.message : "GROUP_DISPATCH_PLAN_FAILED",
};
await appendProjectMessage({
projectId,
sender: "master",
senderLabel: "主 Agent",
body: dispatchFailureNotice(dispatchRecommendation.error),
kind: "system_notice",
});
}
} else if (project && projectId !== "master-agent" && !project.isGroup && message.body.trim().length > 0) {
const queuedTask = await queueThreadConversationReplyTask({
projectId,
requestMessageId: message.id,
requestText: message.body,
requestedBy: session.displayName || session.account,
requestedByAccount: session.account,
});
task = {
taskId: queuedTask.taskId,
taskType: "conversation_reply",
status: "queued",
};
} else {
dispatchRecommendation = {
ok: false,
@@ -80,6 +135,13 @@ export async function POST(
requestedByAccount: session.account,
currentSessionExpiresAt: session.expiresAt,
});
if (masterReply?.ok && masterReply.taskId) {
task = {
taskId: masterReply.taskId,
taskType: "conversation_reply",
status: masterReply.requestId ? "completed" : "queued",
};
}
}
const nextState = shouldCreateDispatchPlan ? await readState() : state;
@@ -103,6 +165,7 @@ export async function POST(
ok: true,
message,
masterReply,
task,
dispatchPlan,
dispatchRecommendation,
collaborationGate,

View File

@@ -314,12 +314,13 @@ export function AiAccountsClient({
nodeId: draft.nodeId.trim(),
nodeLabel: draft.nodeLabel.trim(),
model: draft.model.trim() || "gpt-5.4",
setActive: true,
}),
});
const result = (await response.json()) as { ok: boolean; message?: string };
setBusyKey(null);
if (result.ok) {
setMessage(result.message || "Master Codex Node 已绑定。");
setMessage(result.message || "Master Codex Node 已绑定,并设为当前主控。");
setMasterNodeOnboardDraft(defaultMasterNodeOnboardDraft());
closeOnboarding();
router.refresh();

View File

@@ -327,6 +327,15 @@ export interface DeviceImportCandidate {
suggestedImport: boolean;
}
export function isDispatchableThreadProject(project: Project) {
return (
project.id !== "master-agent" &&
!project.isGroup &&
Boolean(project.threadMeta.codexThreadRef?.trim()) &&
project.deviceIds.length > 0
);
}
export interface DeviceImportDraft {
draftId: string;
deviceId: string;
@@ -521,6 +530,8 @@ export interface MasterAgentTask {
targetProjectId?: string;
targetThreadId?: string;
targetThreadDisplayName?: string;
targetCodexThreadRef?: string;
targetCodexFolderRef?: string;
deviceImportDraftId?: string;
status: MasterAgentTaskStatus;
requestedAt: string;
@@ -3812,7 +3823,10 @@ export async function saveAiAccount(payload: {
const existing = payload.accountId
? state.aiAccounts.find((item) => item.accountId === payload.accountId)
: null;
const accountId = existing?.accountId ?? `ai-${slugify(`${payload.label}-${payload.displayName}`)}`;
const accountId =
existing?.accountId ??
payload.accountId?.trim() ??
`ai-${slugify(`${payload.label}-${payload.displayName}`)}`;
const next: AiAccount = normalizeAiAccount({
accountId,
label: payload.label.trim() || aiRoleLabel(payload.role),
@@ -3989,6 +4003,8 @@ export async function queueMasterAgentTask(payload: {
targetProjectId?: string;
targetThreadId?: string;
targetThreadDisplayName?: string;
targetCodexThreadRef?: string;
targetCodexFolderRef?: string;
}) {
const task = await mutateState((state) => {
const task: MasterAgentTask = {
@@ -4014,6 +4030,8 @@ export async function queueMasterAgentTask(payload: {
targetProjectId: payload.targetProjectId,
targetThreadId: payload.targetThreadId,
targetThreadDisplayName: payload.targetThreadDisplayName,
targetCodexThreadRef: payload.targetCodexThreadRef,
targetCodexFolderRef: payload.targetCodexFolderRef,
status: "queued",
requestedAt: nowIso(),
};
@@ -4281,6 +4299,8 @@ function ensureDispatchExecutionTaskInState(
existing.targetProjectId = existing.targetProjectId ?? execution.targetProjectId;
existing.targetThreadId = existing.targetThreadId ?? execution.targetThreadId;
existing.targetThreadDisplayName = existing.targetThreadDisplayName ?? target.threadDisplayName;
existing.targetCodexThreadRef = existing.targetCodexThreadRef ?? target.codexThreadRef;
existing.targetCodexFolderRef = existing.targetCodexFolderRef ?? target.codexFolderRef;
existing.executionPrompt =
existing.executionPrompt ||
buildDispatchExecutionPrompt({
@@ -4311,6 +4331,8 @@ function ensureDispatchExecutionTaskInState(
targetProjectId: execution.targetProjectId,
targetThreadId: execution.targetThreadId,
targetThreadDisplayName: target.threadDisplayName,
targetCodexThreadRef: target.codexThreadRef,
targetCodexFolderRef: target.codexFolderRef,
status: "queued",
requestedAt: nowIso(),
};
@@ -4790,17 +4812,48 @@ export async function completeMasterAgentTask(payload: {
masterSummary: payload.replyBody?.trim(),
});
} else if (!attachmentProjectId && payload.status === "completed" && task.replyBody) {
pushProjectLedgerMessage(state, task.projectId, {
sender: "master",
senderLabel: task.accountLabel ? `主 Agent · ${task.accountLabel}` : "主 Agent",
body: task.replyBody,
kind: "text",
});
const isThreadConversationReply =
task.taskType === "conversation_reply" &&
task.projectId !== "master-agent" &&
Boolean(task.targetProjectId && task.targetThreadId);
if (isThreadConversationReply) {
const threadProject = state.projects.find(
(item) => item.id === (task.targetProjectId ?? task.projectId),
);
const device = state.devices.find((item) => item.id === payload.deviceId);
pushProjectLedgerMessage(state, threadProject?.id ?? task.projectId, {
sender: "device",
senderLabel:
task.targetThreadDisplayName?.trim() ||
threadProject?.threadMeta.threadDisplayName ||
device?.name ||
"线程",
body: task.replyBody,
kind: "text",
});
} else {
pushProjectLedgerMessage(state, task.projectId, {
sender: "master",
senderLabel: task.accountLabel ? `主 Agent · ${task.accountLabel}` : "主 Agent",
body: task.replyBody,
kind: "text",
});
}
} else if (!attachmentProjectId && payload.status === "failed") {
const isThreadConversationReply =
task.taskType === "conversation_reply" &&
task.projectId !== "master-agent" &&
Boolean(task.targetProjectId && task.targetThreadId);
pushProjectLedgerMessage(state, task.projectId, {
sender: "ops",
senderLabel: task.accountLabel ? `主 Agent Relay · ${task.accountLabel}` : "主 Agent Relay",
body: `Master Codex Node 执行失败:${task.errorMessage ?? "UNKNOWN_ERROR"}`,
senderLabel: isThreadConversationReply
? "线程执行失败"
: task.accountLabel
? `主 Agent Relay · ${task.accountLabel}`
: "主 Agent Relay",
body: isThreadConversationReply
? `${task.targetThreadDisplayName ?? "当前线程"} 执行失败:${task.errorMessage ?? "UNKNOWN_ERROR"}`
: `Master Codex Node 执行失败:${task.errorMessage ?? "UNKNOWN_ERROR"}`,
kind: "text",
});
}
@@ -6233,6 +6286,9 @@ function createGroupChatFromProjectIds(
if (!memberProject) {
throw new Error("GROUP_CHAT_MEMBER_NOT_FOUND");
}
if (!isDispatchableThreadProject(memberProject)) {
throw new Error("GROUP_CHAT_MEMBER_NOT_THREAD");
}
memberProjects.push(memberProject);
}
if (memberProjects.length < 2) {

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",