feat: let master-agent dispatch real threads

This commit is contained in:
kris
2026-04-03 05:29:38 +08:00
parent ad7dd94d95
commit 354c8b1f0b
12 changed files with 495 additions and 21 deletions

View File

@@ -2,6 +2,17 @@ import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { confirmDispatchPlanAndCreateExecutions } from "@/lib/boss-data";
function confirmFailureMessage(error?: string) {
switch (error) {
case "DISPATCH_TARGET_DEVICE_OFFLINE":
return "目标线程所在设备当前不在线,请先让设备上线后再确认下发。";
case "DISPATCH_TARGET_THREAD_BINDING_REQUIRED":
return "目标线程还没有绑定真实 Codex 线程,请先修复群成员或重新导入线程后再试。";
default:
return error ?? "UNKNOWN_ERROR";
}
}
export async function POST(
request: NextRequest,
context: { params: Promise<{ projectId: string; planId: string }> },
@@ -36,8 +47,13 @@ export async function POST(
collaborationGate: result.collaborationGate,
});
} catch (error) {
const reason = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{
ok: false,
code: reason,
message: confirmFailureMessage(reason),
},
{ status: 400 },
);
}

View File

@@ -50,7 +50,7 @@ export async function POST(
if (!project) {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
if (!project.isGroup) {
if (!project.isGroup && project.id !== "master-agent") {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_GROUP_CHAT" }, { status: 400 });
}

View File

@@ -5,6 +5,7 @@ import {
queueGroupDispatchPlan,
queueThreadConversationReplyTask,
replyToMasterAgentUserMessage,
shouldRecommendMasterAgentDispatchPlan,
} from "@/lib/boss-master-agent";
import { evaluatePermissionPolicy } from "@/lib/execution/permission-policy";
@@ -19,6 +20,17 @@ function dispatchFailureNotice(error?: string) {
}
}
function threadConversationFailureMessage(error?: string) {
switch (error) {
case "THREAD_BINDING_REQUIRED":
return "当前线程还没有绑定真实 Codex 线程,请先重新导入该线程后再试。";
case "THREAD_TARGET_DEVICE_OFFLINE":
return "当前线程所在设备不在线,请先让对应设备上线后再试。";
default:
return error ?? "UNKNOWN_ERROR";
}
}
export async function POST(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
@@ -37,8 +49,10 @@ export async function POST(
const state = await readState();
const project = state.projects.find((item) => item.id === projectId);
const shouldCreateDispatchPlan =
project?.isGroup &&
project.id !== "master-agent" &&
Boolean(project) &&
((project?.isGroup && project.id !== "master-agent") ||
(project?.id === "master-agent" &&
shouldRecommendMasterAgentDispatchPlan(state, (body.body ?? "").trim()))) &&
(body.kind ?? "text") === "text" &&
(body.body ?? "").trim().length > 0;
@@ -195,8 +209,13 @@ export async function POST(
collaborationGate,
});
} catch (error) {
const reason = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{
ok: false,
code: reason,
message: threadConversationFailureMessage(reason),
},
{ status: 400 },
);
}

View File

@@ -5188,7 +5188,7 @@ function upsertDispatchPlanInState(
if (!requestedBy) throw new Error("DISPATCH_PLAN_REQUESTED_BY_REQUIRED");
const groupProject = state.projects.find((item) => item.id === groupProjectId);
if (!groupProject) throw new Error("DISPATCH_PLAN_GROUP_PROJECT_NOT_FOUND");
if (!groupProject.isGroup) throw new Error("DISPATCH_PLAN_GROUP_PROJECT_INVALID");
if (!canOwnDispatchPlans(groupProject)) throw new Error("DISPATCH_PLAN_GROUP_PROJECT_INVALID");
const validatedTargets = normalizeDispatchPlanTargetsForCreate(state, input.targets);
const existing = state.dispatchPlans.find(
@@ -5255,6 +5255,10 @@ export async function listDispatchPlansByProject(groupProjectId: string) {
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}
function canOwnDispatchPlans(project: Project) {
return project.isGroup || project.id === "master-agent";
}
function applyDispatchPlanConfirmationInState(
state: BossState,
input: {
@@ -5315,7 +5319,7 @@ export async function rejectDispatchPlan(input: {
if (!groupProjectId) throw new Error("PROJECT_NOT_FOUND");
const groupProject = state.projects.find((item) => item.id === groupProjectId);
if (!groupProject) throw new Error("PROJECT_NOT_FOUND");
if (!groupProject.isGroup) throw new Error("PROJECT_NOT_GROUP_CHAT");
if (!canOwnDispatchPlans(groupProject)) throw new Error("PROJECT_NOT_GROUP_CHAT");
requireDispatchActorSession(state, input.rejectedBy);
const plan = state.dispatchPlans.find((item) => item.planId === input.planId);
@@ -5533,6 +5537,23 @@ function ensureDispatchExecutionTasksInState(
return executions.map((execution) => ensureDispatchExecutionTaskInState(state, plan, execution));
}
function validateDispatchExecutionTarget(
state: BossState,
target: DispatchPlanTarget,
) {
const project = state.projects.find((item) => item.id === target.projectId);
if (!project || project.isGroup) {
throw new Error("DISPATCH_TARGET_PROJECT_NOT_FOUND");
}
if (!project.threadMeta.codexThreadRef?.trim()) {
throw new Error("DISPATCH_TARGET_THREAD_BINDING_REQUIRED");
}
const device = state.devices.find((item) => item.id === target.deviceId);
if (!device || device.status !== "online") {
throw new Error("DISPATCH_TARGET_DEVICE_OFFLINE");
}
}
export async function confirmDispatchPlanAndCreateExecutions(input: {
groupProjectId: string;
planId: string;
@@ -5544,7 +5565,7 @@ export async function confirmDispatchPlanAndCreateExecutions(input: {
if (!groupProjectId) throw new Error("PROJECT_NOT_FOUND");
const groupProject = state.projects.find((item) => item.id === groupProjectId);
if (!groupProject) throw new Error("PROJECT_NOT_FOUND");
if (!groupProject.isGroup) throw new Error("PROJECT_NOT_GROUP_CHAT");
if (!canOwnDispatchPlans(groupProject)) throw new Error("PROJECT_NOT_GROUP_CHAT");
const plan = applyDispatchPlanConfirmationInState(state, {
planId: input.planId,
@@ -5582,6 +5603,7 @@ export async function confirmDispatchPlanAndCreateExecutions(input: {
if (targets.length === 0) {
throw new Error("DISPATCH_EXECUTION_TARGETS_REQUIRED");
}
targets.forEach((target) => validateDispatchExecutionTarget(state, target));
const createdAt = nowIso();
executions = targets.map((target) => {
const execution: DispatchExecution = {

View File

@@ -35,6 +35,7 @@ import {
getClawBackendSelectionState,
} from "@/lib/execution/backends/claw-backend";
import { getOmxTeamBackendSelectionState } from "@/lib/execution/backends/omx-team-backend";
import type { OrchestrationBackendId } from "@/lib/execution/orchestration-backend";
import { listExecutionBackendChoices, selectExecutionBackend } from "@/lib/execution/backend-selector";
import { selectOrchestrationBackend } from "@/lib/execution/orchestration-backend-selector";
import { resolveRuntimeRelevantMemories } from "@/lib/execution/memory-resolver";
@@ -1133,6 +1134,73 @@ function summarizeDispatchRequest(requestText: string) {
return `${compact.slice(0, 33)}...`;
}
const MASTER_AGENT_DISPATCH_KEYWORDS = [
"线程",
"项目",
"文件夹",
"codex",
"操作",
"处理",
"执行",
"修复",
"同步",
"部署",
"查看",
"检查",
"分析",
"回复",
"下发",
"让",
"继续",
];
function normalizeDispatchLookupText(value: string) {
return value.trim().toLowerCase();
}
function scoreMasterAgentDispatchCandidate(project: Project, requestText: string) {
const request = normalizeDispatchLookupText(requestText);
if (!request) {
return 0;
}
let score = 0;
const fields = [
project.name,
project.threadMeta.threadDisplayName,
project.threadMeta.folderName,
project.threadMeta.codexFolderRef?.split("/").filter(Boolean).pop(),
]
.map((value) => value?.trim())
.filter((value): value is string => Boolean(value && value.length >= 2));
for (const field of fields) {
if (request.includes(field.toLowerCase())) {
score += field === project.threadMeta.threadDisplayName ? 8 : field === project.threadMeta.folderName ? 6 : 4;
}
}
return score;
}
export function shouldRecommendMasterAgentDispatchPlan(
state: Awaited<ReturnType<typeof readState>>,
requestText: string,
) {
const request = normalizeDispatchLookupText(requestText);
if (!request) {
return false;
}
if (MASTER_AGENT_DISPATCH_KEYWORDS.some((keyword) => request.includes(keyword))) {
return true;
}
return state.projects
.filter((project) => isDispatchableThreadProject(project))
.some((project) => scoreMasterAgentDispatchCandidate(project, requestText) > 0);
}
function collectGroupDispatchTargets(
state: Awaited<ReturnType<typeof readState>>,
project: Project,
@@ -1174,11 +1242,53 @@ function collectGroupDispatchTargets(
});
}
function collectMasterAgentDispatchTargets(
state: Awaited<ReturnType<typeof readState>>,
requestText: string,
): DispatchPlanTarget[] {
const onlineDeviceIds = new Set(
state.devices.filter((device) => device.status === "online").map((device) => device.id),
);
const candidates = state.projects
.filter((project) => isDispatchableThreadProject(project))
.filter((project) => project.deviceIds.some((deviceId) => onlineDeviceIds.has(deviceId)))
.map((project) => ({
project,
score: scoreMasterAgentDispatchCandidate(project, requestText),
}))
.sort((left, right) => {
if (right.score !== left.score) {
return right.score - left.score;
}
return right.project.updatedAt.localeCompare(left.project.updatedAt);
});
const picked = candidates.some((candidate) => candidate.score > 0)
? candidates.filter((candidate) => candidate.score > 0).slice(0, 5)
: candidates.slice(0, 3);
return picked.map(({ project }) => ({
deviceId: project.deviceIds[0] ?? project.id,
projectId: project.id,
threadId: project.threadMeta.threadId,
threadDisplayName: project.threadMeta.threadDisplayName,
folderName: project.threadMeta.folderName,
codexFolderRef: project.threadMeta.codexFolderRef,
codexThreadRef: project.threadMeta.codexThreadRef,
reason: `主 Agent 会话“${summarizeDispatchRequest(requestText)}”需要该线程补充状态或执行建议。`,
}));
}
function summarizeGroupDispatchPlan(requestText: string, targets: DispatchPlanTarget[]) {
const targetLabels = targets.map((target) => target.threadDisplayName).filter(Boolean);
return `主 Agent 建议先按线程分发这条群聊消息:${summarizeDispatchRequest(requestText)}${targetLabels.length > 0 ? `。建议目标:${targetLabels.join("、")}` : ""}`;
}
function summarizeMasterAgentDispatchPlan(requestText: string, targets: DispatchPlanTarget[]) {
const targetLabels = targets.map((target) => target.threadDisplayName).filter(Boolean);
return `主 Agent 建议先把这条请求分发给以下线程:${summarizeDispatchRequest(requestText)}${targetLabels.length > 0 ? `。建议目标:${targetLabels.join("、")}` : ""}`;
}
function buildGroupDispatchPlanPrompt(project: Project, requestText: string) {
const memberDigest = (project.groupMembers.length > 0
? project.groupMembers
@@ -1208,6 +1318,29 @@ function buildGroupDispatchPlanPrompt(project: Project, requestText: string) {
].join("\n");
}
function buildMasterAgentDispatchPlanPrompt(
state: Awaited<ReturnType<typeof readState>>,
requestText: string,
) {
const candidateDigest = state.projects
.filter((project) => isDispatchableThreadProject(project))
.slice(0, 12)
.map(
(project) =>
`${project.id} / ${project.threadMeta.threadDisplayName} / ${project.threadMeta.folderName} / device=${project.deviceIds[0] ?? "unknown"}`,
)
.join("\n");
return [
"你正在处理 Boss 控制台的主 Agent 线程调度建议任务。",
"目标不是直接回复用户,而是为这条主 Agent 消息推荐下一步应分发到哪些真实线程。",
`projectId: master-agent`,
`requestText: ${requestText}`,
"dispatchableThreads:",
candidateDigest || "无",
].join("\n");
}
type GroupDispatchRecommendationResult =
| {
ok: true;
@@ -1244,6 +1377,20 @@ async function resolveGroupOrchestrationBackend(project: Project) {
};
}
function resolveNativeMasterAgentOrchestrationBackend(): {
requestedBackendId: undefined;
orchestrationBackendId: OrchestrationBackendId;
orchestrationBackendLabel: string;
orchestrationFallbackReason: undefined;
} {
return {
requestedBackendId: undefined,
orchestrationBackendId: "boss-native-orchestrator",
orchestrationBackendLabel: "Boss Native Orchestrator",
orchestrationFallbackReason: undefined,
};
}
async function resolveGroupDispatchPlanTask(taskId: string): Promise<GroupDispatchRecommendationResult> {
const task = await getMasterAgentTask(taskId);
if (!task) {
@@ -1259,22 +1406,29 @@ async function resolveGroupDispatchPlanTask(taskId: string): Promise<GroupDispat
if (!project) {
throw new Error("PROJECT_NOT_FOUND");
}
if (!project.isGroup) {
const isMasterAgentProject = project.id === "master-agent";
if (!project.isGroup && !isMasterAgentProject) {
throw new Error("PROJECT_NOT_GROUP_CHAT");
}
const targets = collectGroupDispatchTargets(state, project, task.requestText);
const targets = isMasterAgentProject
? collectMasterAgentDispatchTargets(state, task.requestText)
: collectGroupDispatchTargets(state, project, task.requestText);
if (targets.length === 0) {
throw new Error("GROUP_DISPATCH_TARGETS_REQUIRED");
}
const orchestrationBackend = await resolveGroupOrchestrationBackend(project);
const orchestrationBackend = isMasterAgentProject
? resolveNativeMasterAgentOrchestrationBackend()
: await resolveGroupOrchestrationBackend(project);
const completedTask = await completeMasterAgentTask({
taskId: task.taskId,
deviceId: task.deviceId,
status: "completed",
dispatchPlan: {
summary: summarizeGroupDispatchPlan(task.requestText, targets),
summary: isMasterAgentProject
? summarizeMasterAgentDispatchPlan(task.requestText, targets)
: summarizeGroupDispatchPlan(task.requestText, targets),
targets,
requestedOrchestrationBackendId: orchestrationBackend.requestedBackendId,
orchestrationBackendId: orchestrationBackend.orchestrationBackendId,
@@ -1318,18 +1472,23 @@ export async function queueGroupDispatchPlan(params: {
if (!project) {
throw new Error("PROJECT_NOT_FOUND");
}
if (!project.isGroup) {
if (!project.isGroup && project.id !== "master-agent") {
throw new Error("PROJECT_NOT_GROUP_CHAT");
}
const orchestrationBackend = await resolveGroupOrchestrationBackend(project);
const isMasterAgentProject = project.id === "master-agent";
const orchestrationBackend = isMasterAgentProject
? resolveNativeMasterAgentOrchestrationBackend()
: await resolveGroupOrchestrationBackend(project);
const task = await queueMasterAgentTask({
projectId: project.id,
taskType: "group_dispatch_plan",
requestMessageId: params.requestMessageId,
requestText: params.requestText,
executionPrompt: buildGroupDispatchPlanPrompt(project, params.requestText),
executionPrompt: isMasterAgentProject
? buildMasterAgentDispatchPlanPrompt(state, params.requestText)
: buildGroupDispatchPlanPrompt(project, params.requestText),
requestedBy: params.requestedBy,
requestedByAccount: params.requestedBy,
deviceId: state.user.boundDeviceId || "mac-studio",
@@ -1358,8 +1517,15 @@ export async function queueThreadConversationReplyTask(params: {
if (project.id === "master-agent") {
throw new Error("PROJECT_NOT_THREAD_CONVERSATION");
}
if (!project.threadMeta.codexThreadRef?.trim()) {
throw new Error("THREAD_BINDING_REQUIRED");
}
const deviceId = project.deviceIds[0] || state.user.boundDeviceId || "mac-studio";
const device = state.devices.find((item) => item.id === deviceId);
if (!device || device.status !== "online") {
throw new Error("THREAD_TARGET_DEVICE_OFFLINE");
}
return queueMasterAgentTask({
projectId: project.id,
taskType: "conversation_reply",