feat: let master-agent dispatch real threads
This commit is contained in:
@@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user