feat: refine mobile master agent sync and chat rendering

This commit is contained in:
kris
2026-04-18 04:51:50 +08:00
parent e0c0ea1814
commit 449f84fcbc
61 changed files with 7051 additions and 1075 deletions

View File

@@ -7,8 +7,29 @@ function isValidRole(value: string): value is "primary" | "backup" | "api_fallba
return value === "primary" || value === "backup" || value === "api_fallback";
}
function isValidProvider(value: string): value is "master_codex_node" | "openai_api" | "aliyun_qwen_api" {
return value === "master_codex_node" || value === "openai_api" || value === "aliyun_qwen_api";
function isValidProvider(
value: string,
): value is
| "master_codex_node"
| "google_oauth"
| "chatgpt_oauth"
| "openai_api"
| "aliyun_qwen_api"
| "minimax_api"
| "glm_api"
| "hyzq_api"
| "custom_api" {
return (
value === "master_codex_node" ||
value === "google_oauth" ||
value === "chatgpt_oauth" ||
value === "openai_api" ||
value === "aliyun_qwen_api" ||
value === "minimax_api" ||
value === "glm_api" ||
value === "hyzq_api" ||
value === "custom_api"
);
}
export async function GET(
@@ -49,6 +70,7 @@ export async function PATCH(
nodeId?: string;
nodeLabel?: string;
model?: string;
apiBaseUrl?: string;
apiKey?: string;
enabled?: boolean;
setActive?: boolean;
@@ -79,6 +101,7 @@ export async function PATCH(
nodeId: body.nodeId,
nodeLabel: body.nodeLabel,
model: body.model,
apiBaseUrl: body.apiBaseUrl,
apiKey: body.apiKey,
enabled: body.enabled,
setActive: body.setActive,

View File

@@ -24,6 +24,7 @@ export async function POST(request: NextRequest) {
displayName?: string;
accountIdentifier?: string;
model?: string;
apiBaseUrl?: string;
apiKey?: string;
};
@@ -39,6 +40,7 @@ export async function POST(request: NextRequest) {
provider: "aliyun_qwen_api",
apiKey: body.apiKey,
model: body.model,
apiBaseUrl: body.apiBaseUrl,
});
const state = await readState();
@@ -51,6 +53,7 @@ export async function POST(request: NextRequest) {
displayName: body.displayName.trim(),
accountIdentifier: body.accountIdentifier?.trim() || undefined,
model: probe.model,
apiBaseUrl: body.apiBaseUrl,
apiKey: body.apiKey.trim(),
enabled: true,
setActive: false,

View File

@@ -24,6 +24,7 @@ export async function POST(request: NextRequest) {
displayName?: string;
accountIdentifier?: string;
model?: string;
apiBaseUrl?: string;
apiKey?: string;
};
@@ -38,6 +39,7 @@ export async function POST(request: NextRequest) {
const probe = await probeOpenAiApiAccount({
apiKey: body.apiKey,
model: body.model,
apiBaseUrl: body.apiBaseUrl,
});
const state = await readState();
@@ -50,6 +52,7 @@ export async function POST(request: NextRequest) {
displayName: body.displayName.trim(),
accountIdentifier: body.accountIdentifier?.trim() || undefined,
model: probe.model,
apiBaseUrl: body.apiBaseUrl,
apiKey: body.apiKey.trim(),
enabled: true,
setActive: true,

View File

@@ -7,8 +7,29 @@ function isValidRole(value: string): value is "primary" | "backup" | "api_fallba
return value === "primary" || value === "backup" || value === "api_fallback";
}
function isValidProvider(value: string): value is "master_codex_node" | "openai_api" | "aliyun_qwen_api" {
return value === "master_codex_node" || value === "openai_api" || value === "aliyun_qwen_api";
function isValidProvider(
value: string,
): value is
| "master_codex_node"
| "google_oauth"
| "chatgpt_oauth"
| "openai_api"
| "aliyun_qwen_api"
| "minimax_api"
| "glm_api"
| "hyzq_api"
| "custom_api" {
return (
value === "master_codex_node" ||
value === "google_oauth" ||
value === "chatgpt_oauth" ||
value === "openai_api" ||
value === "aliyun_qwen_api" ||
value === "minimax_api" ||
value === "glm_api" ||
value === "hyzq_api" ||
value === "custom_api"
);
}
export async function GET(request: NextRequest) {
@@ -38,6 +59,7 @@ export async function POST(request: NextRequest) {
nodeId?: string;
nodeLabel?: string;
model?: string;
apiBaseUrl?: string;
apiKey?: string;
enabled?: boolean;
setActive?: boolean;
@@ -66,6 +88,7 @@ export async function POST(request: NextRequest) {
nodeId: body.nodeId,
nodeLabel: body.nodeLabel,
model: body.model,
apiBaseUrl: body.apiBaseUrl,
apiKey: body.apiKey,
enabled: body.enabled,
setActive: body.setActive,

View File

@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { validateAiAccountDraftConnection } from "@/lib/boss-master-agent";
function isValidProvider(
value: string,
): value is
| "openai_api"
| "aliyun_qwen_api"
| "minimax_api"
| "glm_api"
| "hyzq_api"
| "custom_api" {
return (
value === "openai_api" ||
value === "aliyun_qwen_api" ||
value === "minimax_api" ||
value === "glm_api" ||
value === "hyzq_api" ||
value === "custom_api"
);
}
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()) as {
provider?: string;
apiKey?: string;
apiBaseUrl?: string;
};
if (!body.provider || !isValidProvider(body.provider)) {
return NextResponse.json({ ok: false, message: "API 接入商不合法。" }, { status: 400 });
}
if (!body.apiKey?.trim()) {
return NextResponse.json({ ok: false, message: "API Key 不能为空。" }, { status: 400 });
}
try {
const result = await validateAiAccountDraftConnection({
provider: body.provider,
apiKey: body.apiKey,
apiBaseUrl: body.apiBaseUrl,
});
return NextResponse.json(result, { status: 200 });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: 400 },
);
}
}

View File

@@ -56,6 +56,8 @@ export async function POST(
const payload = body as {
modelOverride?: unknown;
reasoningEffortOverride?: unknown;
fastModelOverride?: unknown;
deepModelOverride?: unknown;
promptOverride?: unknown;
backendOverride?: unknown;
takeoverEnabled?: unknown;
@@ -66,6 +68,8 @@ export async function POST(
payload,
"reasoningEffortOverride",
);
const hasFastModelOverride = Object.prototype.hasOwnProperty.call(payload, "fastModelOverride");
const hasDeepModelOverride = Object.prototype.hasOwnProperty.call(payload, "deepModelOverride");
const hasPromptOverride = Object.prototype.hasOwnProperty.call(payload, "promptOverride");
const hasBackendOverride = Object.prototype.hasOwnProperty.call(payload, "backendOverride");
const hasTakeoverEnabled = Object.prototype.hasOwnProperty.call(payload, "takeoverEnabled");
@@ -75,6 +79,8 @@ export async function POST(
? new Set([
"modelOverride",
"reasoningEffortOverride",
"fastModelOverride",
"deepModelOverride",
"promptOverride",
"backendOverride",
"globalTakeoverEnabled",
@@ -85,6 +91,8 @@ export async function POST(
(
!hasModelOverride &&
!hasReasoningEffortOverride &&
!hasFastModelOverride &&
!hasDeepModelOverride &&
!hasPromptOverride &&
!hasBackendOverride &&
!hasTakeoverEnabled &&
@@ -110,6 +118,12 @@ export async function POST(
{ status: 400 },
);
}
if (hasFastModelOverride && payload.fastModelOverride !== undefined && payload.fastModelOverride !== null && typeof payload.fastModelOverride !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_FAST_MODEL_OVERRIDE" }, { status: 400 });
}
if (hasDeepModelOverride && payload.deepModelOverride !== undefined && payload.deepModelOverride !== null && typeof payload.deepModelOverride !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_DEEP_MODEL_OVERRIDE" }, { status: 400 });
}
if (hasPromptOverride && payload.promptOverride !== undefined && payload.promptOverride !== null && typeof payload.promptOverride !== "string") {
return NextResponse.json({ ok: false, message: "INVALID_PROMPT_OVERRIDE" }, { status: 400 });
}
@@ -154,6 +168,8 @@ export async function POST(
{
...(hasModelOverride ? { modelOverride: payload.modelOverride } : {}),
...(hasReasoningEffortOverride ? { reasoningEffortOverride: payload.reasoningEffortOverride } : {}),
...(hasFastModelOverride ? { fastModelOverride: payload.fastModelOverride } : {}),
...(hasDeepModelOverride ? { deepModelOverride: payload.deepModelOverride } : {}),
...(hasPromptOverride ? { promptOverride: payload.promptOverride } : {}),
...(hasBackendOverride ? { backendOverride: payload.backendOverride } : {}),
...(hasTakeoverEnabled ? { takeoverEnabled: payload.takeoverEnabled } : {}),

View File

@@ -1,6 +1,13 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { appendProjectMessage, buildCollaborationGate, readState } from "@/lib/boss-data";
import {
appendProjectMessage,
appendProjectMessages,
buildCollaborationGate,
getProjectAgentControls,
readState,
requestProjectUnderstandingSyncForProject,
} from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
import { buildProjectMessagesRealtimePayload } from "@/lib/boss-projections";
import {
@@ -10,6 +17,7 @@ import {
replyToMasterAgentUserMessage,
shouldRecommendMasterAgentDispatchPlan,
ThreadConversationExecutionConflictError,
tryBuildLocalMasterAgentFastReply,
} from "@/lib/boss-master-agent";
import { evaluatePermissionPolicy } from "@/lib/execution/permission-policy";
@@ -105,14 +113,19 @@ export async function POST(
);
}
const singleThreadExecutionConflict =
project &&
const isSingleThreadTextMessage =
Boolean(project) &&
projectId !== "master-agent" &&
!project.isGroup &&
!project?.isGroup &&
(body.kind ?? "text") === "text" &&
(body.body ?? "").trim().length > 0
? await getThreadConversationExecutionConflict(projectId)
: null;
(body.body ?? "").trim().length > 0;
const singleThreadAgentControls = isSingleThreadTextMessage
? await getProjectAgentControls(projectId, session.account)
: null;
const singleThreadTakeoverEnabled = singleThreadAgentControls?.effectiveTakeoverEnabled === true;
const singleThreadExecutionConflict = isSingleThreadTextMessage && !singleThreadTakeoverEnabled
? await getThreadConversationExecutionConflict(projectId)
: null;
if (singleThreadExecutionConflict) {
return NextResponse.json(
@@ -126,6 +139,49 @@ export async function POST(
);
}
if (projectId === "master-agent" && (body.kind ?? "text") === "text" && (body.body ?? "").trim()) {
const localMasterReply = await tryBuildLocalMasterAgentFastReply({
requestText: (body.body ?? "").trim(),
requestedByAccount: session.account,
projectId,
state,
});
if (localMasterReply) {
const [message, replyMessage] = await appendProjectMessages({
projectId,
messages: [
{
senderLabel: session.displayName || "你",
body: body.body,
kind: body.kind ?? "text",
},
{
sender: "master",
senderLabel: localMasterReply.senderLabel,
body: localMasterReply.replyBody,
kind: "text",
},
],
});
return NextResponse.json({
ok: true,
message,
replyMessage,
masterReply: localMasterReply.masterReply,
task: null,
replyPresenter: "master",
masterReplyState: "completed",
dispatchPlan: null,
dispatchRecommendation: {
ok: false,
status: "skipped",
},
collaborationGate: buildCollaborationGate(project),
});
}
}
const message = await appendProjectMessage({
projectId,
senderLabel: session.displayName || "你",
@@ -155,8 +211,10 @@ export async function POST(
taskType: "conversation_reply";
status: "queued" | "running" | "completed";
};
replyMessage?: Awaited<ReturnType<typeof appendProjectMessage>>;
}
| undefined;
let replyMessage: Awaited<ReturnType<typeof appendProjectMessage>> | undefined;
let task:
| {
taskId: string;
@@ -169,6 +227,7 @@ export async function POST(
| "running"
| "completed"
| null = null;
let replyPresenter: "thread" | "master" | undefined;
if (shouldCreateDispatchPlan) {
try {
@@ -204,18 +263,49 @@ export async function POST(
});
}
} 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",
};
const relayViaMasterAgent = singleThreadTakeoverEnabled;
if (relayViaMasterAgent) {
if (shouldRequestVerifiedProjectSummarySync(message.body)) {
await requestProjectUnderstandingSyncForProject({
projectId,
observedActivityAt: message.sentAt,
reason: "thread_reply",
});
}
masterReply = await replyToMasterAgentUserMessage({
requestMessageId: message.id,
requestText: message.body,
requestedBy: session.displayName || session.account,
requestedByAccount: session.account,
currentSessionExpiresAt: session.expiresAt,
projectId,
interactionMode: "takeover_single_thread",
mode: "enqueue",
})
if (masterReply?.taskId) {
task = masterReply.task ?? {
taskId: masterReply.taskId,
taskType: "conversation_reply",
status: masterReply.masterReplyState ?? "queued",
};
masterReplyState = masterReply.masterReplyState ?? null;
}
replyMessage = masterReply?.replyMessage;
} else {
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",
};
}
replyPresenter = relayViaMasterAgent ? "master" : "thread";
} else {
dispatchRecommendation = {
ok: false,
@@ -230,11 +320,19 @@ export async function POST(
requestedBy: session.displayName,
requestedByAccount: session.account,
currentSessionExpiresAt: session.expiresAt,
mode: "enqueue",
mode: "smart",
});
if (masterReply?.ok && masterReply.taskId) {
task = masterReply.task ?? null;
masterReplyState = masterReply.masterReplyState ?? null;
if (masterReply?.ok) {
if (masterReply.taskId) {
task = masterReply.task ?? {
taskId: masterReply.taskId,
taskType: "conversation_reply",
status: masterReply.masterReplyState ?? "queued",
};
}
masterReplyState = masterReply.masterReplyState ?? (masterReply.taskId ? null : "completed");
replyPresenter = "master";
replyMessage = masterReply.replyMessage;
} else {
masterReplyState = null;
}
@@ -247,8 +345,10 @@ export async function POST(
return NextResponse.json({
ok: true,
message,
replyMessage,
masterReply,
task,
replyPresenter,
masterReplyState,
dispatchPlan,
dispatchRecommendation,
@@ -277,3 +377,14 @@ export async function POST(
);
}
}
function shouldRequestVerifiedProjectSummarySync(text: string) {
const normalized = text.trim();
if (!normalized) {
return false;
}
const mentionsGoal = /项目目标|目标/.test(normalized);
const mentionsVersion = /版本记录|版本迭代|版本/.test(normalized);
const mentionsReviewOrSync = /核对|确认|同步|更新|刷新|整理|汇总/.test(normalized);
return mentionsReviewOrSync && (mentionsGoal || mentionsVersion);
}