feat: ship native boss android console
This commit is contained in:
29
src/app/api/v1/accounts/[accountId]/activate/route.ts
Normal file
29
src/app/api/v1/accounts/[accountId]/activate/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { activateAiAccount } from "@/lib/boss-data";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ accountId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
if (session.role !== "highest_admin") {
|
||||
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = (await request.json().catch(() => ({}))) as { reason?: string };
|
||||
const { accountId } = await context.params;
|
||||
|
||||
try {
|
||||
const result = await activateAiAccount(accountId, body.reason?.trim() || "手动切换主控身份");
|
||||
return NextResponse.json({ ok: true, ...result });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
116
src/app/api/v1/accounts/[accountId]/route.ts
Normal file
116
src/app/api/v1/accounts/[accountId]/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { deleteAiAccount, getAiAccount, saveAiAccount } from "@/lib/boss-data";
|
||||
|
||||
function isValidRole(value: string): value is "primary" | "backup" | "api_fallback" {
|
||||
return value === "primary" || value === "backup" || value === "api_fallback";
|
||||
}
|
||||
|
||||
function isValidProvider(value: string): value is "master_codex_node" | "openai_api" {
|
||||
return value === "master_codex_node" || value === "openai_api";
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ accountId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const { accountId } = await context.params;
|
||||
const account = await getAiAccount(accountId);
|
||||
if (!account) {
|
||||
return NextResponse.json({ ok: false, message: "AI_ACCOUNT_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json({ ok: true, account });
|
||||
}
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ accountId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
if (session.role !== "highest_admin") {
|
||||
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { accountId } = await context.params;
|
||||
const body = (await request.json()) as {
|
||||
label?: string;
|
||||
role?: string;
|
||||
provider?: string;
|
||||
displayName?: string;
|
||||
accountIdentifier?: string;
|
||||
nodeId?: string;
|
||||
nodeLabel?: string;
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
enabled?: boolean;
|
||||
setActive?: boolean;
|
||||
loginStatusNote?: string;
|
||||
};
|
||||
|
||||
if (!body.label?.trim() || !body.displayName?.trim()) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: "AI 账号至少需要填写显示名称和账号名称。" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
if (!body.role || !isValidRole(body.role)) {
|
||||
return NextResponse.json({ ok: false, message: "AI 账号角色不合法。" }, { status: 400 });
|
||||
}
|
||||
if (!body.provider || !isValidProvider(body.provider)) {
|
||||
return NextResponse.json({ ok: false, message: "AI 账号类型不合法。" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const account = await saveAiAccount({
|
||||
accountId,
|
||||
label: body.label,
|
||||
role: body.role,
|
||||
provider: body.provider,
|
||||
displayName: body.displayName,
|
||||
accountIdentifier: body.accountIdentifier,
|
||||
nodeId: body.nodeId,
|
||||
nodeLabel: body.nodeLabel,
|
||||
model: body.model,
|
||||
apiKey: body.apiKey,
|
||||
enabled: body.enabled,
|
||||
setActive: body.setActive,
|
||||
loginStatusNote: body.loginStatusNote,
|
||||
});
|
||||
return NextResponse.json({ ok: true, account });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ accountId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
if (session.role !== "highest_admin") {
|
||||
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||||
}
|
||||
const { accountId } = await context.params;
|
||||
try {
|
||||
await deleteAiAccount(accountId);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
27
src/app/api/v1/accounts/[accountId]/validate/route.ts
Normal file
27
src/app/api/v1/accounts/[accountId]/validate/route.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { validateAiAccountConnection } from "@/lib/boss-master-agent";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ accountId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
if (session.role !== "highest_admin") {
|
||||
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { accountId } = await context.params;
|
||||
const result = await validateAiAccountConnection(accountId);
|
||||
return NextResponse.json(result, { status: result.ok ? 200 : 400 });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
74
src/app/api/v1/accounts/route.ts
Normal file
74
src/app/api/v1/accounts/route.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { listAiAccounts, saveAiAccount } from "@/lib/boss-data";
|
||||
|
||||
function isValidRole(value: string): value is "primary" | "backup" | "api_fallback" {
|
||||
return value === "primary" || value === "backup" || value === "api_fallback";
|
||||
}
|
||||
|
||||
function isValidProvider(value: string): value is "master_codex_node" | "openai_api" {
|
||||
return value === "master_codex_node" || value === "openai_api";
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const result = await listAiAccounts();
|
||||
return NextResponse.json({ ok: true, ...result });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
if (session.role !== "highest_admin") {
|
||||
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = (await request.json()) as {
|
||||
label?: string;
|
||||
role?: string;
|
||||
provider?: string;
|
||||
displayName?: string;
|
||||
accountIdentifier?: string;
|
||||
nodeId?: string;
|
||||
nodeLabel?: string;
|
||||
model?: string;
|
||||
apiKey?: string;
|
||||
enabled?: boolean;
|
||||
setActive?: boolean;
|
||||
loginStatusNote?: string;
|
||||
};
|
||||
|
||||
if (!body.label?.trim() || !body.displayName?.trim()) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: "AI 账号至少需要填写显示名称和账号名称。" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
if (!body.role || !isValidRole(body.role)) {
|
||||
return NextResponse.json({ ok: false, message: "AI 账号角色不合法。" }, { status: 400 });
|
||||
}
|
||||
if (!body.provider || !isValidProvider(body.provider)) {
|
||||
return NextResponse.json({ ok: false, message: "AI 账号类型不合法。" }, { status: 400 });
|
||||
}
|
||||
|
||||
const account = await saveAiAccount({
|
||||
label: body.label,
|
||||
role: body.role,
|
||||
provider: body.provider,
|
||||
displayName: body.displayName,
|
||||
accountIdentifier: body.accountIdentifier,
|
||||
nodeId: body.nodeId,
|
||||
nodeLabel: body.nodeLabel,
|
||||
model: body.model,
|
||||
apiKey: body.apiKey,
|
||||
enabled: body.enabled,
|
||||
setActive: body.setActive,
|
||||
loginStatusNote: body.loginStatusNote,
|
||||
});
|
||||
return NextResponse.json({ ok: true, account });
|
||||
}
|
||||
93
src/app/api/v1/app-logs/route.ts
Normal file
93
src/app/api/v1/app-logs/route.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth";
|
||||
import { appendAppLog, readState } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const limit = Math.min(Math.max(Number(request.nextUrl.searchParams.get("limit") ?? "20"), 1), 100);
|
||||
const cursor = request.nextUrl.searchParams.get("cursor");
|
||||
const deviceId = request.nextUrl.searchParams.get("deviceId") ?? undefined;
|
||||
const projectId = request.nextUrl.searchParams.get("projectId") ?? undefined;
|
||||
const level = request.nextUrl.searchParams.get("level") ?? undefined;
|
||||
const category = request.nextUrl.searchParams.get("category") ?? undefined;
|
||||
const source = request.nextUrl.searchParams.get("source") ?? undefined;
|
||||
const state = await readState();
|
||||
|
||||
const filtered = state.appLogs
|
||||
.filter((entry) => {
|
||||
const device = state.devices.find((item) => item.id === entry.deviceId);
|
||||
if (
|
||||
session.role !== "highest_admin" &&
|
||||
device?.account !== session.account
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (deviceId && entry.deviceId !== deviceId) return false;
|
||||
if (projectId && entry.projectId !== projectId) return false;
|
||||
if (level && entry.level !== level) return false;
|
||||
if (category && entry.category !== category) return false;
|
||||
if (source && entry.source !== source) return false;
|
||||
if (cursor && entry.createdAt >= cursor) return false;
|
||||
return true;
|
||||
})
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
|
||||
const entries = filtered.slice(0, limit);
|
||||
const nextCursor = filtered.length > limit ? entries.at(-1)?.createdAt : null;
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
entries,
|
||||
nextCursor,
|
||||
hasMore: Boolean(nextCursor),
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = (await request.json()) as {
|
||||
deviceId?: string;
|
||||
projectId?: string;
|
||||
level?: "info" | "warn" | "error";
|
||||
source?: "app_client" | "local_agent";
|
||||
category?: string;
|
||||
message?: string;
|
||||
detail?: string;
|
||||
mirrorToMaster?: boolean;
|
||||
};
|
||||
|
||||
if (!body.deviceId || !body.level || !body.category || !body.message) {
|
||||
return NextResponse.json({ ok: false, message: "APP 日志字段不完整。" }, { status: 400 });
|
||||
}
|
||||
|
||||
const authorization = await authorizeDeviceWriteRequest(request, body.deviceId);
|
||||
if (!authorization.ok) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED_DEVICE_WRITE" }, { status: 401 });
|
||||
}
|
||||
if (!authorization.device) {
|
||||
return NextResponse.json({ ok: false, message: "DEVICE_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = await appendAppLog({
|
||||
deviceId: body.deviceId,
|
||||
projectId: body.projectId,
|
||||
level: body.level,
|
||||
source: body.source ?? "app_client",
|
||||
category: body.category,
|
||||
message: body.message,
|
||||
detail: body.detail,
|
||||
mirrorToMaster: body.mirrorToMaster,
|
||||
});
|
||||
return NextResponse.json({ ok: true, entry });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
13
src/app/api/v1/audits/summary/route.ts
Normal file
13
src/app/api/v1/audits/summary/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { getAuditSummaryView } from "@/lib/boss-projections";
|
||||
import { readState } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const state = await readState();
|
||||
return NextResponse.json({ ok: true, ...getAuditSummaryView(state) });
|
||||
}
|
||||
29
src/app/api/v1/conversations/[projectId]/actions/route.ts
Normal file
29
src/app/api/v1/conversations/[projectId]/actions/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { updateConversationAction } from "@/lib/boss-data";
|
||||
|
||||
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()) as { action?: "toggle_pin" | "mark_read" };
|
||||
|
||||
if (!body.action) {
|
||||
return NextResponse.json({ ok: false, message: "缺少 action" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const project = await updateConversationAction(projectId, body.action);
|
||||
return NextResponse.json({ ok: true, project });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
16
src/app/api/v1/conversations/route.ts
Normal file
16
src/app/api/v1/conversations/route.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { getConversationItems } from "@/lib/boss-projections";
|
||||
import { readState } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const state = await readState();
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
conversations: getConversationItems(state),
|
||||
});
|
||||
}
|
||||
33
src/app/api/v1/devices/[deviceId]/route.ts
Normal file
33
src/app/api/v1/devices/[deviceId]/route.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { updateDevice } from "@/lib/boss-data";
|
||||
|
||||
export async function PATCH(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ deviceId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const { deviceId } = await context.params;
|
||||
const body = (await request.json()) as {
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
account?: string;
|
||||
status?: "online" | "abnormal" | "offline";
|
||||
endpoint?: string;
|
||||
note?: string;
|
||||
projects?: string[];
|
||||
};
|
||||
|
||||
try {
|
||||
const device = await updateDevice(deviceId, body);
|
||||
return NextResponse.json({ ok: true, device });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
77
src/app/api/v1/devices/[deviceId]/skills/route.ts
Normal file
77
src/app/api/v1/devices/[deviceId]/skills/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth";
|
||||
import { readState, upsertDeviceSkills } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ deviceId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const { deviceId } = await context.params;
|
||||
const state = await readState();
|
||||
const device = state.devices.find((item) => item.id === deviceId);
|
||||
if (!device) {
|
||||
return NextResponse.json({ ok: false, message: "DEVICE_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
device,
|
||||
skills: state.deviceSkills
|
||||
.filter((item) => item.deviceId === deviceId)
|
||||
.sort((a, b) => a.name.localeCompare(b.name, "zh-CN")),
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ deviceId: string }> },
|
||||
) {
|
||||
const { deviceId } = await context.params;
|
||||
const body = (await request.json()) as {
|
||||
skills?: Array<{
|
||||
name?: string;
|
||||
description?: string;
|
||||
path?: string;
|
||||
invocation?: string;
|
||||
category?: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
if (!Array.isArray(body.skills)) {
|
||||
return NextResponse.json({ ok: false, message: "缺少技能列表。" }, { status: 400 });
|
||||
}
|
||||
|
||||
const authorization = await authorizeDeviceWriteRequest(request, deviceId);
|
||||
if (!authorization.ok) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED_DEVICE_WRITE" }, { status: 401 });
|
||||
}
|
||||
if (!authorization.device) {
|
||||
return NextResponse.json({ ok: false, message: "DEVICE_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
try {
|
||||
const skills = await upsertDeviceSkills({
|
||||
deviceId,
|
||||
skills: body.skills
|
||||
.filter((skill) => skill.name && skill.path)
|
||||
.map((skill) => ({
|
||||
name: skill.name as string,
|
||||
description: skill.description,
|
||||
path: skill.path as string,
|
||||
invocation: skill.invocation,
|
||||
category: skill.category,
|
||||
})),
|
||||
});
|
||||
return NextResponse.json({ ok: true, skills, count: skills.length });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
51
src/app/api/v1/devices/enrollments/route.ts
Normal file
51
src/app/api/v1/devices/enrollments/route.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { createDeviceEnrollment, readState } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const state = await readState();
|
||||
return NextResponse.json({ ok: true, enrollments: state.deviceEnrollments });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const body = (await request.json()) as {
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
account?: string;
|
||||
endpoint?: string;
|
||||
note?: string;
|
||||
projects?: string[];
|
||||
};
|
||||
|
||||
if (!body.name || !body.avatar || !body.account) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: "缺少设备名、头像或账号" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createDeviceEnrollment({
|
||||
name: body.name,
|
||||
avatar: body.avatar,
|
||||
account: body.account,
|
||||
endpoint: body.endpoint,
|
||||
note: body.note,
|
||||
projects: body.projects ?? [],
|
||||
});
|
||||
return NextResponse.json({ ok: true, ...result });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
21
src/app/api/v1/devices/route.ts
Normal file
21
src/app/api/v1/devices/route.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { getDeviceWorkspaceView } from "@/lib/boss-projections";
|
||||
import { readState } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const url = new URL(request.url);
|
||||
const deviceId = url.searchParams.get("device");
|
||||
const state = await readState();
|
||||
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
devices: state.devices,
|
||||
enrollments: state.deviceEnrollments,
|
||||
workspace: getDeviceWorkspaceView(state, deviceId ?? undefined),
|
||||
});
|
||||
}
|
||||
80
src/app/api/v1/events/route.ts
Normal file
80
src/app/api/v1/events/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { NextRequest } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { subscribeBossEvents } from "@/lib/boss-events";
|
||||
import { getAuditSummaryView, getConversationItems, getOpsSummaryView } from "@/lib/boss-projections";
|
||||
import { readState } from "@/lib/boss-data";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function sseEvent(event: string, data: unknown) {
|
||||
return `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return new Response(JSON.stringify({ ok: false, message: "UNAUTHORIZED" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
const encoder = new TextEncoder();
|
||||
let heartbeatTimer: ReturnType<typeof setInterval> | undefined;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const publishSnapshots = async () => {
|
||||
const state = await readState();
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
sseEvent("conversation.context_indicator.updated", {
|
||||
at: new Date().toISOString(),
|
||||
conversations: getConversationItems(state),
|
||||
}),
|
||||
),
|
||||
);
|
||||
controller.enqueue(
|
||||
encoder.encode(
|
||||
sseEvent("project.context_risk.updated", {
|
||||
at: new Date().toISOString(),
|
||||
ops: getOpsSummaryView(state),
|
||||
audits: getAuditSummaryView(state),
|
||||
}),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
await publishSnapshots();
|
||||
|
||||
unsubscribe = subscribeBossEvents((event, payload) => {
|
||||
try {
|
||||
controller.enqueue(encoder.encode(sseEvent(event, payload)));
|
||||
} catch {
|
||||
unsubscribe?.();
|
||||
}
|
||||
});
|
||||
|
||||
heartbeatTimer = setInterval(() => {
|
||||
try {
|
||||
controller.enqueue(encoder.encode(": keepalive\n\n"));
|
||||
} catch {
|
||||
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
||||
unsubscribe?.();
|
||||
}
|
||||
}, 20_000);
|
||||
},
|
||||
cancel() {
|
||||
if (heartbeatTimer) clearInterval(heartbeatTimer);
|
||||
unsubscribe?.();
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream; charset=utf-8",
|
||||
"Cache-Control": "no-cache, no-transform",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
44
src/app/api/v1/master-agent/tasks/[taskId]/complete/route.ts
Normal file
44
src/app/api/v1/master-agent/tasks/[taskId]/complete/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth";
|
||||
import { completeMasterAgentTask } 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?: "completed" | "failed";
|
||||
replyBody?: string;
|
||||
errorMessage?: string;
|
||||
requestId?: string;
|
||||
};
|
||||
|
||||
if (!body.deviceId?.trim()) {
|
||||
return NextResponse.json({ ok: false, message: "DEVICE_ID_REQUIRED" }, { status: 400 });
|
||||
}
|
||||
|
||||
const auth = await authorizeDeviceWriteRequest(request, body.deviceId.trim());
|
||||
if (!auth.ok) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
|
||||
const { taskId } = await context.params;
|
||||
|
||||
try {
|
||||
const task = await completeMasterAgentTask({
|
||||
taskId,
|
||||
deviceId: body.deviceId.trim(),
|
||||
status: body.status === "failed" ? "failed" : "completed",
|
||||
replyBody: body.replyBody,
|
||||
errorMessage: body.errorMessage,
|
||||
requestId: body.requestId,
|
||||
});
|
||||
return NextResponse.json({ ok: true, task });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
19
src/app/api/v1/master-agent/tasks/claim/route.ts
Normal file
19
src/app/api/v1/master-agent/tasks/claim/route.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth";
|
||||
import { claimNextMasterAgentTask } from "@/lib/boss-data";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = (await request.json().catch(() => ({}))) as { deviceId?: string };
|
||||
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 task = await claimNextMasterAgentTask(deviceId);
|
||||
return NextResponse.json({ ok: true, task });
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { approveRepairTicket } from "@/lib/boss-data";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ ticketId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const { ticketId } = await context.params;
|
||||
try {
|
||||
const ticket = await approveRepairTicket(ticketId);
|
||||
return NextResponse.json({ ok: true, ticket });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
23
src/app/api/v1/ops/repair-tickets/[ticketId]/verify/route.ts
Normal file
23
src/app/api/v1/ops/repair-tickets/[ticketId]/verify/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { verifyRepairTicket } from "@/lib/boss-data";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ ticketId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const { ticketId } = await context.params;
|
||||
try {
|
||||
const ticket = await verifyRepairTicket(ticketId);
|
||||
return NextResponse.json({ ok: true, ticket });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
13
src/app/api/v1/ops/summary/route.ts
Normal file
13
src/app/api/v1/ops/summary/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { getOpsSummaryView } from "@/lib/boss-projections";
|
||||
import { readState } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const state = await readState();
|
||||
return NextResponse.json({ ok: true, ...getOpsSummaryView(state) });
|
||||
}
|
||||
39
src/app/api/v1/projects/[projectId]/forwards/route.ts
Normal file
39
src/app/api/v1/projects/[projectId]/forwards/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { forwardProjectMessage } from "@/lib/boss-data";
|
||||
|
||||
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()) as {
|
||||
targetProjectId?: string;
|
||||
note?: string;
|
||||
};
|
||||
|
||||
if (!body.targetProjectId || !body.note) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: "缺少 targetProjectId 或 note" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const message = await forwardProjectMessage({
|
||||
sourceProjectId: projectId,
|
||||
targetProjectId: body.targetProjectId,
|
||||
note: body.note,
|
||||
});
|
||||
return NextResponse.json({ ok: true, message });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
29
src/app/api/v1/projects/[projectId]/goals/route.ts
Normal file
29
src/app/api/v1/projects/[projectId]/goals/route.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { createGoal } from "@/lib/boss-data";
|
||||
|
||||
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()) as { text?: string };
|
||||
|
||||
if (!body.text) {
|
||||
return NextResponse.json({ ok: false, message: "缺少 text" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const goal = await createGoal(projectId, body.text);
|
||||
return NextResponse.json({ ok: true, goal });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
48
src/app/api/v1/projects/[projectId]/messages/route.ts
Normal file
48
src/app/api/v1/projects/[projectId]/messages/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { appendProjectMessage } from "@/lib/boss-data";
|
||||
import { replyToMasterAgentUserMessage } from "@/lib/boss-master-agent";
|
||||
|
||||
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()) as {
|
||||
body?: string;
|
||||
kind?: "text" | "voice_intent" | "image_intent" | "video_intent";
|
||||
};
|
||||
|
||||
try {
|
||||
const message = await appendProjectMessage({
|
||||
projectId,
|
||||
senderLabel: session.displayName || "你",
|
||||
body: body.body,
|
||||
kind: body.kind ?? "text",
|
||||
});
|
||||
let masterReply:
|
||||
| { ok: boolean; reason?: string; message?: string; accountId?: string; requestId?: string }
|
||||
| undefined;
|
||||
|
||||
if (projectId === "master-agent" && (body.kind ?? "text") === "text" && message.body.trim()) {
|
||||
masterReply = await replyToMasterAgentUserMessage({
|
||||
requestMessageId: message.id,
|
||||
requestText: message.body,
|
||||
requestedBy: session.displayName,
|
||||
requestedByAccount: session.account,
|
||||
currentSessionExpiresAt: session.expiresAt,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, message, masterReply });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
23
src/app/api/v1/projects/[projectId]/route.ts
Normal file
23
src/app/api/v1/projects/[projectId]/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { getProjectDetailView } from "@/lib/boss-projections";
|
||||
import { readState } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(
|
||||
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 state = await readState();
|
||||
const detail = getProjectDetailView(state, projectId);
|
||||
|
||||
if (!detail) {
|
||||
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, ...detail });
|
||||
}
|
||||
39
src/app/api/v1/settings/route.ts
Normal file
39
src/app/api/v1/settings/route.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { readState, updateUserSettings } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const state = await readState();
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
settings: state.user.settings,
|
||||
user: state.user,
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const body = (await request.json()) as {
|
||||
liveUpdates?: boolean;
|
||||
showRiskBadges?: boolean;
|
||||
confirmDangerousActions?: boolean;
|
||||
preferredEntryPoint?: "conversations" | "devices" | "me";
|
||||
};
|
||||
|
||||
try {
|
||||
const settings = await updateUserSettings(body);
|
||||
return NextResponse.json({ ok: true, settings });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
23
src/app/api/v1/threads/[threadId]/context-budget/route.ts
Normal file
23
src/app/api/v1/threads/[threadId]/context-budget/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { getThreadContextDetailView } from "@/lib/boss-projections";
|
||||
import { readState } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ threadId: string }> },
|
||||
) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const { threadId } = await context.params;
|
||||
const state = await readState();
|
||||
const detail = getThreadContextDetailView(state, threadId);
|
||||
|
||||
if (!detail) {
|
||||
return NextResponse.json({ ok: false, message: "THREAD_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, ...detail });
|
||||
}
|
||||
35
src/app/api/v1/user/ota/package/route.ts
Normal file
35
src/app/api/v1/user/ota/package/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import { NextRequest } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { getPublishedOtaAsset } from "@/lib/boss-ota";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return new Response(JSON.stringify({ ok: false, message: "UNAUTHORIZED" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
const asset = await getPublishedOtaAsset();
|
||||
if (!asset) {
|
||||
return new Response(JSON.stringify({ ok: false, message: "OTA_PACKAGE_NOT_FOUND" }), {
|
||||
status: 404,
|
||||
headers: { "Content-Type": "application/json; charset=utf-8" },
|
||||
});
|
||||
}
|
||||
|
||||
const content = await fs.readFile(asset.absolutePath);
|
||||
return new Response(content, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.android.package-archive",
|
||||
"Content-Length": String(asset.sizeBytes),
|
||||
"Content-Disposition": `attachment; filename=\"${asset.fileName}\"`,
|
||||
ETag: asset.sha256,
|
||||
"X-Boss-Ota-Sha256": asset.sha256,
|
||||
"X-Boss-Ota-Updated-At": asset.updatedAt,
|
||||
},
|
||||
});
|
||||
}
|
||||
45
src/app/api/v1/user/ota/route.ts
Normal file
45
src/app/api/v1/user/ota/route.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { requireRequestSession } from "@/lib/boss-auth";
|
||||
import { checkForOta, getOtaStatus, performOta } from "@/lib/boss-data";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const status = await getOtaStatus();
|
||||
return NextResponse.json({ ok: true, ...status });
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await requireRequestSession(request);
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||
}
|
||||
const raw = await request.text();
|
||||
const body = raw.trim() ? (JSON.parse(raw) as { action?: "check" | "apply" }) : {};
|
||||
const action = body.action ?? "apply";
|
||||
|
||||
try {
|
||||
if (action === "check") {
|
||||
const result = await checkForOta();
|
||||
return NextResponse.json({ ok: true, ...result, message: result.hasOta ? "发现新的 OTA 版本。" : "当前已经是最新版本。" });
|
||||
}
|
||||
|
||||
const result = await performOta();
|
||||
return NextResponse.json({
|
||||
ok: true,
|
||||
version: result.version,
|
||||
summary: result.summary,
|
||||
downloadUrl: result.downloadUrl,
|
||||
packageFileName: result.packageFileName,
|
||||
packageSizeBytes: result.packageSizeBytes,
|
||||
packageSha256: result.packageSha256,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
98
src/app/api/v1/workers/[workerId]/thread-context/route.ts
Normal file
98
src/app/api/v1/workers/[workerId]/thread-context/route.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth";
|
||||
import { upsertThreadContextSnapshot } from "@/lib/boss-data";
|
||||
|
||||
export async function POST(
|
||||
request: NextRequest,
|
||||
context: { params: Promise<{ workerId: string }> },
|
||||
) {
|
||||
const { workerId } = await context.params;
|
||||
const body = (await request.json()) as {
|
||||
nodeId?: string;
|
||||
threadId?: string;
|
||||
projectId?: string;
|
||||
taskId?: string;
|
||||
title?: string;
|
||||
summary?: string;
|
||||
sourceKind?: "codex_app_server" | "codex_sdk" | "worker_estimator";
|
||||
status?: string;
|
||||
contextBudgetRemainingPct?: number;
|
||||
contextBudgetLevel?: "safe" | "watch" | "urgent" | "critical";
|
||||
compactionExpectedAt?: string;
|
||||
mustFinishBeforeCompaction?: boolean;
|
||||
estimatedRemainingTurns?: number;
|
||||
estimatedRemainingLargeMessages?: number;
|
||||
lastCompactionAt?: string;
|
||||
compactionCount?: number;
|
||||
patchPending?: boolean;
|
||||
testsPending?: boolean;
|
||||
evidencePending?: boolean;
|
||||
checklist?: string[];
|
||||
capturedAt?: string;
|
||||
};
|
||||
|
||||
if (
|
||||
!body.nodeId ||
|
||||
!body.threadId ||
|
||||
!body.projectId ||
|
||||
!body.taskId ||
|
||||
!body.title ||
|
||||
!body.summary ||
|
||||
typeof body.contextBudgetRemainingPct !== "number"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: "thread-context 字段不完整" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const authorization = await authorizeDeviceWriteRequest(request, body.nodeId);
|
||||
if (!authorization.ok) {
|
||||
return NextResponse.json({ ok: false, message: "UNAUTHORIZED_DEVICE_WRITE" }, { status: 401 });
|
||||
}
|
||||
if (!authorization.device) {
|
||||
return NextResponse.json({ ok: false, message: "DEVICE_NOT_FOUND" }, { status: 404 });
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await upsertThreadContextSnapshot(workerId, {
|
||||
nodeId: body.nodeId,
|
||||
threadId: body.threadId,
|
||||
projectId: body.projectId,
|
||||
taskId: body.taskId,
|
||||
title: body.title,
|
||||
summary: body.summary,
|
||||
sourceKind: body.sourceKind ?? "worker_estimator",
|
||||
status: (body.status as
|
||||
| "idle"
|
||||
| "running"
|
||||
| "waiting_input"
|
||||
| "waiting_approval"
|
||||
| "context_watch"
|
||||
| "context_urgent"
|
||||
| "compacted"
|
||||
| "handoff_pending"
|
||||
| "completed"
|
||||
| "failed") ?? "running",
|
||||
contextBudgetRemainingPct: body.contextBudgetRemainingPct,
|
||||
contextBudgetLevel: body.contextBudgetLevel,
|
||||
compactionExpectedAt: body.compactionExpectedAt,
|
||||
mustFinishBeforeCompaction: Boolean(body.mustFinishBeforeCompaction),
|
||||
estimatedRemainingTurns: body.estimatedRemainingTurns ?? 0,
|
||||
estimatedRemainingLargeMessages: body.estimatedRemainingLargeMessages ?? 0,
|
||||
lastCompactionAt: body.lastCompactionAt,
|
||||
compactionCount: body.compactionCount ?? 0,
|
||||
patchPending: Boolean(body.patchPending),
|
||||
testsPending: Boolean(body.testsPending),
|
||||
evidencePending: Boolean(body.evidencePending),
|
||||
checklist: body.checklist ?? [],
|
||||
capturedAt: body.capturedAt ?? new Date().toISOString(),
|
||||
});
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user