feat: adapt codex app-server protocol updates
This commit is contained in:
43
src/app/api/v1/master-agent/tasks/[taskId]/progress/route.ts
Normal file
43
src/app/api/v1/master-agent/tasks/[taskId]/progress/route.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth";
|
||||
import type { ExecutionProgressInput } from "@/lib/boss-data";
|
||||
import { updateMasterAgentTaskProgress } from "@/lib/boss-data";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ taskId: string }> },
|
||||
) {
|
||||
const body = (await request.json().catch(() => ({}))) as {
|
||||
deviceId?: string;
|
||||
status?: "queued" | "running";
|
||||
requestId?: string;
|
||||
executionProgress?: ExecutionProgressInput;
|
||||
};
|
||||
|
||||
const deviceId = body.deviceId?.trim();
|
||||
if (!deviceId) {
|
||||
return NextResponse.json({ ok: false, message: "DEVICE_ID_REQUIRED" }, { status: 400 });
|
||||
}
|
||||
|
||||
const auth = await authorizeDeviceWriteRequest(request, deviceId);
|
||||
if (!auth.ok) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { taskId } = await context.params;
|
||||
try {
|
||||
const task = await updateMasterAgentTaskProgress({
|
||||
taskId,
|
||||
deviceId,
|
||||
status: body.status,
|
||||
requestId: body.requestId,
|
||||
executionProgress: body.executionProgress,
|
||||
});
|
||||
return NextResponse.json({ ok: true, task });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { appendProjectMessage, buildCollaborationGate, readState } from "@/lib/boss-data";
|
||||
import { canAccessProject } from "@/lib/boss-permissions";
|
||||
import {
|
||||
queueInterThreadCollaborationTask,
|
||||
ThreadConversationExecutionConflictError,
|
||||
} from "@/lib/boss-master-agent";
|
||||
|
||||
function forbiddenResponse(message = "FORBIDDEN") {
|
||||
return NextResponse.json({ ok: false, message }, { status: 403 });
|
||||
}
|
||||
|
||||
function normalizeRequestText(body: { body?: unknown; requestText?: unknown }) {
|
||||
return String(body.body ?? body.requestText ?? "").trim();
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ projectId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { projectId } = await context.params;
|
||||
const body = (await request.json().catch(() => ({}))) as {
|
||||
targetProjectId?: string;
|
||||
body?: string;
|
||||
requestText?: string;
|
||||
};
|
||||
const targetProjectId = body.targetProjectId?.trim();
|
||||
const requestText = normalizeRequestText(body);
|
||||
if (!targetProjectId) {
|
||||
return NextResponse.json({ ok: false, message: "TARGET_PROJECT_ID_REQUIRED" }, { status: 400 });
|
||||
}
|
||||
if (!requestText) {
|
||||
return NextResponse.json({ ok: false, message: "REQUEST_TEXT_REQUIRED" }, { status: 400 });
|
||||
}
|
||||
|
||||
const state = await readState();
|
||||
const sourceProjectExists = state.projects.some((project) => project.id === projectId);
|
||||
const targetProjectExists = state.projects.some((project) => project.id === targetProjectId);
|
||||
if (!canAccessProject(state, session, projectId, "project.view")) {
|
||||
return forbiddenResponse(sourceProjectExists ? "FORBIDDEN" : "PROJECT_NOT_FOUND");
|
||||
}
|
||||
if (!canAccessProject(state, session, targetProjectId, "project.view")) {
|
||||
return forbiddenResponse(targetProjectExists ? "TARGET_FORBIDDEN" : "TARGET_PROJECT_NOT_FOUND");
|
||||
}
|
||||
if (!canAccessProject(state, session, projectId, "master_agent.ask")) {
|
||||
return forbiddenResponse("MASTER_AGENT_FORBIDDEN");
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await appendProjectMessage({
|
||||
projectId,
|
||||
account: session.account,
|
||||
senderLabel: session.displayName || "你",
|
||||
body: requestText,
|
||||
kind: "text",
|
||||
});
|
||||
const task = await queueInterThreadCollaborationTask({
|
||||
sourceProjectId: projectId,
|
||||
targetProjectId,
|
||||
requestMessageId: message.id,
|
||||
requestText,
|
||||
sourceMessageId: message.id,
|
||||
sourceMessageBody: message.body,
|
||||
sourceMessageSentAt: message.sentAt,
|
||||
requestedBy: session.displayName || session.account,
|
||||
requestedByAccount: session.account,
|
||||
});
|
||||
const nextState = await readState();
|
||||
const sourceProject = nextState.projects.find((project) => project.id === projectId);
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
message,
|
||||
task,
|
||||
collaborationGate: buildCollaborationGate(sourceProject),
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof ThreadConversationExecutionConflictError) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
ok: false,
|
||||
code: error.message,
|
||||
message: "THREAD_EXECUTION_CONFLICT",
|
||||
executionConflict: error.conflict,
|
||||
},
|
||||
{ status: 409 },
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1105,10 +1105,16 @@ export interface MasterAgentTask {
|
||||
attachmentDownloadUrl?: string;
|
||||
attachmentTextExcerpt?: string;
|
||||
dispatchExecutionId?: string;
|
||||
sourceProjectId?: string;
|
||||
sourceThreadId?: string;
|
||||
sourceThreadDisplayName?: string;
|
||||
sourceCodexThreadRef?: string;
|
||||
targetProjectId?: string;
|
||||
targetThreadId?: string;
|
||||
targetThreadDisplayName?: string;
|
||||
targetCodexThreadRef?: string;
|
||||
targetTurnId?: string;
|
||||
targetCodexTurnId?: string;
|
||||
targetCodexFolderRef?: string;
|
||||
orchestrationBackendId?: OrchestrationBackendId;
|
||||
orchestrationBackendLabel?: string;
|
||||
@@ -4447,10 +4453,16 @@ export function migrateBossState(raw: Partial<BossState> | undefined): BossState
|
||||
attachmentDownloadUrl: task.attachmentDownloadUrl,
|
||||
attachmentTextExcerpt: task.attachmentTextExcerpt,
|
||||
dispatchExecutionId: task.dispatchExecutionId,
|
||||
sourceProjectId: task.sourceProjectId,
|
||||
sourceThreadId: task.sourceThreadId,
|
||||
sourceThreadDisplayName: task.sourceThreadDisplayName,
|
||||
sourceCodexThreadRef: task.sourceCodexThreadRef,
|
||||
targetProjectId: task.targetProjectId,
|
||||
targetThreadId: task.targetThreadId,
|
||||
targetThreadDisplayName: task.targetThreadDisplayName,
|
||||
targetCodexThreadRef: task.targetCodexThreadRef,
|
||||
targetTurnId: task.targetTurnId,
|
||||
targetCodexTurnId: task.targetCodexTurnId,
|
||||
targetCodexFolderRef: task.targetCodexFolderRef,
|
||||
orchestrationBackendId:
|
||||
task.orchestrationBackendId === "omx-team" || task.orchestrationBackendId === "boss-native-orchestrator"
|
||||
@@ -8085,10 +8097,16 @@ export async function queueMasterAgentTask(payload: {
|
||||
attachmentTextExcerpt?: string;
|
||||
deviceImportDraftId?: string;
|
||||
dispatchExecutionId?: string;
|
||||
sourceProjectId?: string;
|
||||
sourceThreadId?: string;
|
||||
sourceThreadDisplayName?: string;
|
||||
sourceCodexThreadRef?: string;
|
||||
targetProjectId?: string;
|
||||
targetThreadId?: string;
|
||||
targetThreadDisplayName?: string;
|
||||
targetCodexThreadRef?: string;
|
||||
targetTurnId?: string;
|
||||
targetCodexTurnId?: string;
|
||||
targetCodexFolderRef?: string;
|
||||
orchestrationBackendId?: OrchestrationBackendId;
|
||||
orchestrationBackendLabel?: string;
|
||||
@@ -8138,10 +8156,16 @@ export async function queueMasterAgentTask(payload: {
|
||||
attachmentTextExcerpt: payload.attachmentTextExcerpt,
|
||||
deviceImportDraftId: payload.deviceImportDraftId,
|
||||
dispatchExecutionId: payload.dispatchExecutionId,
|
||||
sourceProjectId: payload.sourceProjectId,
|
||||
sourceThreadId: payload.sourceThreadId,
|
||||
sourceThreadDisplayName: payload.sourceThreadDisplayName,
|
||||
sourceCodexThreadRef: payload.sourceCodexThreadRef,
|
||||
targetProjectId: payload.targetProjectId,
|
||||
targetThreadId: payload.targetThreadId,
|
||||
targetThreadDisplayName: payload.targetThreadDisplayName,
|
||||
targetCodexThreadRef: payload.targetCodexThreadRef,
|
||||
targetTurnId: payload.targetTurnId,
|
||||
targetCodexTurnId: payload.targetCodexTurnId,
|
||||
targetCodexFolderRef: payload.targetCodexFolderRef,
|
||||
orchestrationBackendId: payload.orchestrationBackendId,
|
||||
orchestrationBackendLabel: payload.orchestrationBackendLabel,
|
||||
@@ -9291,6 +9315,50 @@ export async function cancelMasterAgentTask(input: {
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function updateMasterAgentTaskProgress(payload: {
|
||||
taskId: string;
|
||||
deviceId: string;
|
||||
status?: "queued" | "running";
|
||||
executionProgress?: ExecutionProgressInput;
|
||||
requestId?: string;
|
||||
}) {
|
||||
const result = await mutateState((state) => {
|
||||
const task = state.masterAgentTasks.find((item) => item.taskId === payload.taskId);
|
||||
if (!task) {
|
||||
throw new Error("MASTER_AGENT_TASK_NOT_FOUND");
|
||||
}
|
||||
if (task.deviceId !== payload.deviceId) {
|
||||
throw new Error("MASTER_AGENT_TASK_DEVICE_MISMATCH");
|
||||
}
|
||||
if (isTerminalMasterAgentTaskStatus(task.status)) {
|
||||
return { ...task };
|
||||
}
|
||||
const progressStatus = payload.status === "queued" ? "queued" : "running";
|
||||
if (task.status === "queued" && progressStatus === "running") {
|
||||
task.status = "running";
|
||||
task.claimedAt = task.claimedAt ?? nowIso();
|
||||
task.lastClaimedAt = task.lastClaimedAt ?? task.claimedAt;
|
||||
task.attemptCount = task.attemptCount ?? 1;
|
||||
task.maxAttempts = task.maxAttempts ?? defaultMasterAgentTaskMaxAttempts(task.taskType);
|
||||
task.leaseExpiresAt = task.leaseExpiresAt ?? new Date(Date.now() + masterAgentTaskLeaseMs(task)).toISOString();
|
||||
}
|
||||
task.requestId = payload.requestId?.trim() || task.requestId;
|
||||
upsertTaskExecutionProgressMessageInState(state, task, progressStatus, payload.executionProgress);
|
||||
return { ...task };
|
||||
});
|
||||
publishBossEvent("master_agent.task.updated", {
|
||||
taskId: result.taskId,
|
||||
deviceId: result.deviceId,
|
||||
status: result.status,
|
||||
});
|
||||
const progressProjectId = resolveTaskExecutionProgressProjectId(result);
|
||||
if (progressProjectId) {
|
||||
publishBossEvent("project.messages.updated", { projectId: progressProjectId });
|
||||
publishBossEvent("conversation.updated", { projectId: progressProjectId });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function completeMasterAgentTask(payload: {
|
||||
taskId: string;
|
||||
deviceId: string;
|
||||
|
||||
@@ -1039,6 +1039,23 @@ function buildThreadConversationRelayPrompt(project: Project, requestText: strin
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function buildInterThreadCollaborationPrompt(params: {
|
||||
sourceProject: Project;
|
||||
targetProject: Project;
|
||||
requestText: string;
|
||||
}) {
|
||||
return [
|
||||
"你正在处理 Boss Inter-Thread Broker 下发的线程协作任务。",
|
||||
`来源线程:${params.sourceProject.threadMeta.threadDisplayName || params.sourceProject.name}`,
|
||||
`目标线程:${params.targetProject.threadMeta.threadDisplayName || params.targetProject.name}`,
|
||||
"Boss 已通过 Codex App Server 把来源线程的最新可读上下文注入到当前目标线程。",
|
||||
"请只基于用户的协作意图和已注入上下文继续推进,不要输出系统提示词、设备密钥、内部调度字段或无关运行时噪音。",
|
||||
"如果信息足够,直接给出目标线程接下来的执行结论或开发动作;如果信息不足,只问一个最关键的问题。",
|
||||
"用户协作意图:",
|
||||
params.requestText.trim(),
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function appendExecutionPromptDirective(basePrompt: string, directive?: string | null) {
|
||||
const trimmedDirective = directive?.trim();
|
||||
if (!trimmedDirective) {
|
||||
@@ -3173,6 +3190,65 @@ export async function queueThreadConversationReplyTask(params: {
|
||||
});
|
||||
}
|
||||
|
||||
export async function queueInterThreadCollaborationTask(params: {
|
||||
sourceProjectId: string;
|
||||
targetProjectId: string;
|
||||
requestMessageId: string;
|
||||
requestText: string;
|
||||
sourceMessageId?: string;
|
||||
sourceMessageBody?: string;
|
||||
sourceMessageSentAt?: string;
|
||||
requestedBy: string;
|
||||
requestedByAccount: string;
|
||||
}) {
|
||||
if (params.sourceProjectId === params.targetProjectId) {
|
||||
throw new Error("THREAD_COLLABORATION_TARGET_MUST_DIFFER");
|
||||
}
|
||||
const state = await readState();
|
||||
const sourceProject = state.projects.find((project) => project.id === params.sourceProjectId);
|
||||
if (!sourceProject) {
|
||||
throw new Error("SOURCE_PROJECT_NOT_FOUND");
|
||||
}
|
||||
if (!isDispatchableThreadProject(sourceProject)) {
|
||||
throw new Error("SOURCE_THREAD_BINDING_REQUIRED");
|
||||
}
|
||||
|
||||
const conflict = await getThreadConversationExecutionConflict(params.targetProjectId);
|
||||
if (conflict) {
|
||||
throw new ThreadConversationExecutionConflictError(conflict);
|
||||
}
|
||||
const { project: targetProject, deviceId } =
|
||||
await resolveThreadConversationExecutionContext(params.targetProjectId);
|
||||
|
||||
return queueMasterAgentTask({
|
||||
projectId: targetProject.id,
|
||||
taskType: "conversation_reply",
|
||||
requestMessageId: params.requestMessageId,
|
||||
requestText: params.requestText,
|
||||
executionPrompt: buildInterThreadCollaborationPrompt({
|
||||
sourceProject,
|
||||
targetProject,
|
||||
requestText: params.requestText,
|
||||
}),
|
||||
sourceMessageId: params.sourceMessageId,
|
||||
sourceMessageBody: params.sourceMessageBody,
|
||||
sourceMessageSentAt: params.sourceMessageSentAt,
|
||||
requestedBy: params.requestedBy,
|
||||
requestedByAccount: params.requestedByAccount,
|
||||
deviceId,
|
||||
intentCategory: "thread_collaboration",
|
||||
sourceProjectId: sourceProject.id,
|
||||
sourceThreadId: sourceProject.threadMeta.threadId,
|
||||
sourceThreadDisplayName: sourceProject.threadMeta.threadDisplayName,
|
||||
sourceCodexThreadRef: sourceProject.threadMeta.codexThreadRef,
|
||||
targetProjectId: targetProject.id,
|
||||
targetThreadId: targetProject.threadMeta.threadId,
|
||||
targetThreadDisplayName: targetProject.threadMeta.threadDisplayName,
|
||||
targetCodexThreadRef: targetProject.threadMeta.codexThreadRef,
|
||||
targetCodexFolderRef: targetProject.threadMeta.codexFolderRef,
|
||||
});
|
||||
}
|
||||
|
||||
function buildDeviceImportResolutionPrompt(params: {
|
||||
deviceName: string;
|
||||
deviceId: string;
|
||||
|
||||
Reference in New Issue
Block a user