feat: fork codex threads
This commit is contained in:
86
src/app/api/v1/projects/[projectId]/thread-fork/route.ts
Normal file
86
src/app/api/v1/projects/[projectId]/thread-fork/route.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
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 {
|
||||
queueThreadForkTask,
|
||||
ThreadConversationExecutionConflictError,
|
||||
} from "@/lib/boss-master-agent";
|
||||
|
||||
function forbiddenResponse(message = "FORBIDDEN") {
|
||||
return NextResponse.json({ ok: false, message }, { status: 403 });
|
||||
}
|
||||
|
||||
function normalizeReason(value: unknown) {
|
||||
const trimmed = String(value ?? "").trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
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 {
|
||||
reason?: unknown;
|
||||
ephemeral?: unknown;
|
||||
};
|
||||
const reason = normalizeReason(body.reason);
|
||||
const ephemeral = body.ephemeral === true;
|
||||
|
||||
const state = await readState();
|
||||
const projectExists = state.projects.some((project) => project.id === projectId);
|
||||
if (!canAccessProject(state, session, projectId, "project.view")) {
|
||||
return forbiddenResponse(projectExists ? "FORBIDDEN" : "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: reason || "分叉当前 Codex 线程。",
|
||||
kind: "text",
|
||||
});
|
||||
const task = await queueThreadForkTask({
|
||||
projectId,
|
||||
requestMessageId: message.id,
|
||||
ephemeral,
|
||||
reason,
|
||||
requestedBy: session.displayName || session.account,
|
||||
requestedByAccount: session.account,
|
||||
});
|
||||
const nextState = await readState();
|
||||
const project = nextState.projects.find((item) => item.id === projectId);
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
message,
|
||||
task,
|
||||
collaborationGate: buildCollaborationGate(project),
|
||||
});
|
||||
} 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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user