feat: adapt codex app-server protocol updates

This commit is contained in:
AI Bot
2026-05-31 03:25:30 +08:00
parent e1aed590f8
commit b9d3cca2e7
820 changed files with 108070 additions and 71 deletions

View 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 },
);
}
}

View File

@@ -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 },
);
}
}

View File

@@ -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;

View File

@@ -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;