feat: refine mobile master agent sync and chat rendering
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
59
src/app/api/v1/accounts/validate-draft/route.ts
Normal file
59
src/app/api/v1/accounts/validate-draft/route.ts
Normal 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 } : {}),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user