feat: ship native boss android console

This commit is contained in:
kris
2026-03-26 23:16:56 +08:00
parent 90e904814d
commit 90cb6b7ff1
261 changed files with 40051 additions and 135 deletions

View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from "next/server";
import { resetAccountPassword } from "@/lib/boss-data";
function messageForResetError(code: string) {
switch (code) {
case "INVALID_VERIFICATION_CODE":
return "验证码不正确或已失效,请重新获取。";
case "ACCOUNT_NOT_FOUND":
return "账号不存在,请先确认输入是否正确。";
default:
return code;
}
}
export async function POST(request: NextRequest) {
const body = (await request.json()) as {
account?: string;
password?: string;
confirmPassword?: string;
code?: string;
};
if (!body.account || !body.password || !body.confirmPassword || !body.code) {
return NextResponse.json({ ok: false, message: "忘记密码页字段不完整。" }, { status: 400 });
}
if (body.password !== body.confirmPassword) {
return NextResponse.json({ ok: false, message: "两次输入的新密码不一致。" }, { status: 400 });
}
try {
await resetAccountPassword(body.account, body.password, body.code);
return NextResponse.json({
ok: true,
message: "密码重置成功。旧设备登录态后续应被要求重新校验。",
});
} catch (error) {
const message =
error instanceof Error ? messageForResetError(error.message) : "密码重置失败,请稍后再试。";
return NextResponse.json({ ok: false, message }, { status: 400 });
}
}

View File

@@ -0,0 +1,97 @@
import { NextRequest, NextResponse } from "next/server";
import { createPrimaryAdminSession, loginAccount } from "@/lib/boss-data";
import { setAuthSessionCookie } from "@/lib/boss-auth";
function shouldAllowTemporaryAutoLogin() {
return process.env.BOSS_AUTH_AUTO_LOGIN !== "0";
}
function messageForLoginError(code: string) {
switch (code) {
case "ACCOUNT_NOT_FOUND":
return "账号不存在,请先注册或确认输入是否正确。";
case "PASSWORD_REQUIRED":
return "账号密码登录时必须填写密码。";
case "INVALID_ACCOUNT_OR_PASSWORD":
return "账号或密码不正确。";
case "VERIFICATION_CODE_REQUIRED":
return "验证码登录时必须填写验证码。";
case "INVALID_VERIFICATION_CODE":
return "验证码不正确或已失效,请重新获取。";
default:
return code;
}
}
export async function POST(request: NextRequest) {
const body = (await request.json()) as {
account?: string;
password?: string;
code?: string;
method?: "password" | "code";
};
if (shouldAllowTemporaryAutoLogin()) {
const result = await createPrimaryAdminSession();
const response = NextResponse.json({
ok: true,
account: result.account,
displayName: result.displayName,
loginMethod: result.loginMethod,
role: result.role,
sessionExpiresAt: result.sessionExpiresAt,
...(request.headers.get("x-boss-native-app") === "1"
? { restoreToken: result.restoreToken }
: {}),
message: "临时免验证登录已启用,正在进入会话首页。",
});
return setAuthSessionCookie(response, result.sessionToken, request);
}
if (!body.account) {
return NextResponse.json({ ok: false, message: "登录页至少需要填写账号。" }, { status: 400 });
}
const method = body.method ?? (body.password ? "password" : "code");
if (method === "password" && !body.password) {
return NextResponse.json({ ok: false, message: "账号密码登录时必须填写密码。" }, { status: 400 });
}
if (method === "code" && !body.code) {
return NextResponse.json({ ok: false, message: "验证码登录时必须填写验证码。" }, { status: 400 });
}
try {
const result = await loginAccount({
account: body.account,
password: body.password,
code: body.code,
method,
});
const response = NextResponse.json({
ok: true,
account: result.account,
displayName: result.displayName,
loginMethod: result.loginMethod,
role: result.role,
sessionExpiresAt: result.sessionExpiresAt,
...(request.headers.get("x-boss-native-app") === "1"
? { restoreToken: result.restoreToken }
: {}),
message:
method === "password"
? "账号密码校验通过,正在进入会话首页。"
: "验证码校验通过,正在进入会话首页。",
});
return setAuthSessionCookie(response, result.sessionToken, request);
} catch (error) {
const message = error instanceof Error ? error.message : "LOGIN_FAILED";
if (message === "LOGIN_TEMPORARILY_LOCKED") {
return NextResponse.json(
{ ok: false, message: "当前账号登录失败次数过多,请 10 分钟后再试。" },
{ status: 429 },
);
}
return NextResponse.json({ ok: false, message: messageForLoginError(message) }, { status: 400 });
}
}

View File

@@ -0,0 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { clearAuthSessionCookie } from "@/lib/boss-auth";
import { revokeAuthSession } from "@/lib/boss-data";
export async function POST(request: NextRequest) {
await revokeAuthSession(request.cookies.get("boss_session")?.value);
const response = NextResponse.json({ ok: true, message: "已退出当前账号。" });
return clearAuthSessionCookie(response, request);
}

View File

@@ -0,0 +1,42 @@
import { NextRequest, NextResponse } from "next/server";
import { registerAccount } from "@/lib/boss-data";
function messageForRegisterError(code: string) {
switch (code) {
case "INVALID_VERIFICATION_CODE":
return "验证码不正确或已失效,请重新获取。";
case "ACCOUNT_ALREADY_EXISTS":
return "该账号已经注册,可以直接去登录。";
default:
return code;
}
}
export async function POST(request: NextRequest) {
const body = (await request.json()) as {
account?: string;
password?: string;
confirmPassword?: string;
code?: string;
};
if (!body.account || !body.password || !body.confirmPassword || !body.code) {
return NextResponse.json({ ok: false, message: "注册页字段不完整。" }, { status: 400 });
}
if (body.password !== body.confirmPassword) {
return NextResponse.json({ ok: false, message: "两次输入的密码不一致。" }, { status: 400 });
}
try {
await registerAccount(body.account, body.password, body.code);
return NextResponse.json({
ok: true,
message: "注册成功。现在可以回到登录页完成登录。",
});
} catch (error) {
const message =
error instanceof Error ? messageForRegisterError(error.message) : "注册失败,请稍后再试。";
return NextResponse.json({ ok: false, message }, { status: 400 });
}
}

View File

@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from "next/server";
import { setAuthSessionCookie } from "@/lib/boss-auth";
import { restoreAuthSession } from "@/lib/boss-data";
export async function POST(request: NextRequest) {
const body = (await request.json().catch(() => ({}))) as {
restoreToken?: string;
};
if (!body.restoreToken?.trim()) {
return NextResponse.json({ ok: false, message: "RESTORE_TOKEN_REQUIRED" }, { status: 400 });
}
const session = await restoreAuthSession(body.restoreToken.trim());
if (!session) {
return NextResponse.json({ ok: false, message: "RESTORE_SESSION_NOT_FOUND" }, { status: 401 });
}
const response = NextResponse.json({
ok: true,
session: {
account: session.account,
role: session.role,
displayName: session.displayName,
loginMethod: session.loginMethod,
expiresAt: session.expiresAt,
lastSeenAt: session.lastSeenAt,
restoreToken: session.restoreToken,
},
});
return setAuthSessionCookie(response, session.sessionToken, request);
}

View File

@@ -0,0 +1,62 @@
import { NextRequest, NextResponse } from "next/server";
import {
getVerificationDeliveryTarget,
issueVerificationCode,
recordVerificationDelivery,
} from "@/lib/boss-data";
import { deliverVerificationCode } from "@/lib/boss-mail";
export async function POST(request: NextRequest) {
const body = (await request.json()) as {
account?: string;
purpose?: "login" | "register" | "forgot-password";
};
if (!body.account || !body.purpose) {
return NextResponse.json({ ok: false, message: "缺少账号或验证码用途。" }, { status: 400 });
}
try {
const record = await issueVerificationCode(body.account, body.purpose);
const recipient = await getVerificationDeliveryTarget(body.account);
const delivery = await deliverVerificationCode({
account: body.account,
recipient,
purpose: body.purpose,
code: record.code,
});
await recordVerificationDelivery(
body.account,
body.purpose,
delivery.delivered ? "delivered" : "failed",
delivery.message,
);
if (!delivery.delivered) {
return NextResponse.json({ ok: false, message: delivery.message }, { status: delivery.status });
}
return NextResponse.json({
ok: true,
message: `验证码已生成。${delivery.message}`,
});
} catch (error) {
const message = error instanceof Error ? error.message : "SEND_CODE_FAILED";
if (message === "VERIFICATION_CODE_COOLDOWN") {
await recordVerificationDelivery(body.account, body.purpose, "rate_limited", "60 秒冷却中");
return NextResponse.json({ ok: false, message: "验证码刚发送过,请 60 秒后再试。" }, { status: 429 });
}
if (message === "VERIFICATION_RATE_LIMITED") {
await recordVerificationDelivery(body.account, body.purpose, "rate_limited", "15 分钟窗口达到上限");
return NextResponse.json({ ok: false, message: "当前账号验证码请求过于频繁,请 15 分钟后再试。" }, { status: 429 });
}
if (message === "ACCOUNT_NOT_FOUND") {
return NextResponse.json({ ok: false, message: "当前账号不存在,无法发送验证码。" }, { status: 404 });
}
if (message === "ACCOUNT_ALREADY_EXISTS") {
return NextResponse.json({ ok: false, message: "当前账号已注册,请直接登录。" }, { status: 409 });
}
return NextResponse.json({ ok: false, message }, { status: 400 });
}
}

View File

@@ -0,0 +1,23 @@
import { NextRequest, NextResponse } from "next/server";
import { getRequestSession } from "@/lib/boss-auth";
export async function GET(request: NextRequest) {
const session = await getRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
return NextResponse.json({
ok: true,
session: {
account: session.account,
role: session.role,
displayName: session.displayName,
loginMethod: session.loginMethod,
expiresAt: session.expiresAt,
lastSeenAt: session.lastSeenAt,
...(request.headers.get("x-boss-native-app") === "1"
? { restoreToken: session.restoreToken }
: {}),
},
});
}

View File

@@ -0,0 +1,45 @@
import { NextRequest, NextResponse } from "next/server";
import { upsertDeviceHeartbeat } from "@/lib/boss-data";
export async function POST(request: NextRequest) {
const body = (await request.json()) as {
deviceId?: string;
token?: string;
pairingCode?: string;
name?: string;
avatar?: string;
account?: string;
status?: "online" | "abnormal" | "offline";
quota5h?: number;
quota7d?: number;
projects?: string[];
endpoint?: string;
};
if (
!body.deviceId ||
!body.name ||
!body.avatar ||
!body.account ||
!body.status ||
!body.projects
) {
return NextResponse.json({ ok: false, message: "heartbeat 字段不完整" }, { status: 400 });
}
const device = await upsertDeviceHeartbeat({
deviceId: body.deviceId,
token: body.token,
pairingCode: body.pairingCode,
name: body.name,
avatar: body.avatar,
account: body.account,
status: body.status,
quota5h: body.quota5h ?? 0,
quota7d: body.quota7d ?? 0,
projects: body.projects,
endpoint: body.endpoint,
});
return NextResponse.json({ ok: true, ...device });
}

View File

@@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({
ok: true,
service: "boss-web",
now: new Date().toISOString(),
});
}

View File

@@ -0,0 +1,19 @@
import { NextResponse } from "next/server";
import { toggleGoal } from "@/lib/boss-data";
export async function POST(
_request: Request,
context: { params: Promise<{ projectId: string; goalId: string }> },
) {
const { projectId, goalId } = await context.params;
try {
const goal = await toggleGoal(projectId, goalId);
return NextResponse.json({ ok: true, goal });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: 400 },
);
}
}

View File

@@ -0,0 +1,37 @@
import { NextRequest, NextResponse } from "next/server";
import { createGoal, updateGoalText } from "@/lib/boss-data";
export async function POST(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
) {
const { projectId } = await context.params;
const body = (await request.json()) as {
goalId?: string;
text?: string;
action?: "create" | "update";
};
if (!body.text) {
return NextResponse.json({ ok: false, message: "缺少 text" }, { status: 400 });
}
try {
if (body.action === "create") {
const goal = await createGoal(projectId, body.text);
return NextResponse.json({ ok: true, goal });
}
if (!body.goalId) {
return NextResponse.json({ ok: false, message: "缺少 goalId" }, { status: 400 });
}
const goal = await updateGoalText(projectId, body.goalId, 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 },
);
}
}

View File

@@ -0,0 +1,18 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
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({
...state,
aiAccounts: state.aiAccounts.map(({ apiKey, ...account }) => ({
...account,
apiKey: apiKey ? "[REDACTED]" : undefined,
})),
});
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -0,0 +1,17 @@
import { AppShell, AuthForm, PageNav, StatusBar } from "@/components/app-ui";
import { redirectIfAuthenticated } from "@/lib/boss-auth";
export default async function ForgotPasswordPage() {
await redirectIfAuthenticated();
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="忘记密码" backHref="/auth/login" />
<AuthForm
mode="forgot-password"
title="重置登录密码"
description="通过验证码确认身份后,才能设置新的登录密码。验证码带有效期,使用后会立即失效。"
/>
</AppShell>
);
}

View File

@@ -0,0 +1,26 @@
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
export default function AuthHelpPage() {
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="登录帮助" backHref="/auth/login" />
<div className="space-y-3 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
`17600003315`
<br />
`boss123456`
<br />
`最高管理员`
</div>
<div className="rounded-2xl bg-[#EAF7F0] px-4 py-4 text-[13px] leading-6 text-[#215B39]">
`000000` MVP
/
</div>
</div>
</AppShell>
);
}

View File

@@ -0,0 +1,17 @@
import { AppShell, AuthForm, PageNav, StatusBar } from "@/components/app-ui";
import { redirectIfAuthenticated } from "@/lib/boss-auth";
export default async function LoginPage() {
await redirectIfAuthenticated();
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="登录" rightLabel="帮助" rightHref="/auth/help" />
<AuthForm
mode="login"
title="登录 Codex 协同"
description="当前已临时切到免验证登录模式,点击登录会直接进入会话首页。账号密码和验证码输入暂时不作为拦截条件。"
/>
</AppShell>
);
}

View File

@@ -0,0 +1,17 @@
import { AppShell, AuthForm, PageNav, StatusBar } from "@/components/app-ui";
import { redirectIfAuthenticated } from "@/lib/boss-auth";
export default async function RegisterPage() {
await redirectIfAuthenticated();
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="注册" backHref="/auth/login" />
<AuthForm
mode="register"
title="创建账号"
description="注册后可绑定自己的 Mac、Windows 或云端 Codex 节点。注册仍通过验证码确认身份,验证码会校验发送频率并在使用后失效。"
/>
</AppShell>
);
}

View File

@@ -0,0 +1,34 @@
import { notFound } from "next/navigation";
import { AppShell, ForwardComposer, PageNav, StatusBar } from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getProject, readState } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function ForwardPage({
params,
}: {
params: Promise<{ projectId: string }>;
}) {
await requirePageSession();
const { projectId } = await params;
const [project, state] = await Promise.all([getProject(projectId), readState()]);
if (!project) notFound();
const targets = state.projects
.filter((item) => item.id !== projectId)
.map((item) => ({ id: item.id, name: item.name }));
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="转发到项目" backHref={`/conversations/${projectId}`} />
<div className="space-y-3 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
{project.name}
</div>
<ForwardComposer sourceProjectId={projectId} targets={targets} />
</div>
</AppShell>
);
}

View File

@@ -0,0 +1,53 @@
import { notFound } from "next/navigation";
import {
AppShell,
GoalChecklist,
PageNav,
StatusBar,
} from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getProject } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function GoalsPage({
params,
}: {
params: Promise<{ projectId: string }>;
}) {
await requirePageSession();
const { projectId } = await params;
const project = await getProject(projectId);
if (!project) notFound();
const completedCount = project.goals.filter((item) => item.state === "completed").length;
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="项目目标" backHref={`/conversations/${projectId}`} />
<div className="flex flex-col gap-3 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[15px] font-semibold text-[#111111]">
Agent · {completedCount}/{project.goals.length}
</div>
<div className="mt-1 text-[12px] leading-5 text-[#8C8C8C]">
09:18 · · 线
</div>
</div>
<GoalChecklist projectId={projectId} goals={project.goals} />
<div className="rounded-2xl bg-[#EAF7F0] px-4 py-4">
<div className="text-[14px] font-semibold text-[#215B39]"></div>
<div className="mt-2 text-[13px] leading-6 text-[#215B39]">
使
<br />
<br />
Agent
</div>
</div>
</div>
</AppShell>
);
}

View File

@@ -0,0 +1,153 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { RealtimeRefresh } from "@/components/app-runtime";
import {
AppShell,
ChatBubble,
ChatComposer,
MasterIdentityPill,
PageNav,
ProjectHeaderActions,
StatusBar,
} from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { formatTimestampLabel, getProjectDetailView } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function ProjectChatPage({
params,
}: {
params: Promise<{ projectId: string }>;
}) {
await requirePageSession();
const { projectId } = await params;
const state = await readState();
const detail = getProjectDetailView(state, projectId);
if (!detail) notFound();
return (
<AppShell bottomNav={false}>
<RealtimeRefresh
events={["project.messages.updated", "app.logs.updated", "project.context_risk.updated", "ota.updated"]}
/>
<StatusBar />
<PageNav
title={detail.project.name}
backHref="/conversations"
rightNode={detail.masterIdentity ? <MasterIdentityPill identity={detail.masterIdentity} /> : undefined}
/>
<div className="flex min-h-0 flex-1 flex-col px-[18px] pb-0">
<div className="text-[12px] text-[#8C8C8C]">
{detail.devices.map((item) => item.name).join(" / ")}
</div>
{detail.masterIdentity ? (
<div className="mt-2 rounded-2xl border border-[#E5E5EA] bg-white px-4 py-3 text-[12px] leading-6 text-[#57606A]">
<span className="font-semibold text-[#111111]">{detail.masterIdentity.roleLabel}</span>
{" · "}
{detail.masterIdentity.displayName}
{detail.masterIdentity.nodeLabel ? ` · ${detail.masterIdentity.nodeLabel}` : ""}
<br />
{detail.masterIdentity.statusLabel}
{detail.masterIdentity.model ? ` · 模型 ${detail.masterIdentity.model}` : ""}
{detail.masterIdentity.lastSwitchedAt
? ` · 最近切换 ${formatTimestampLabel(detail.masterIdentity.lastSwitchedAt)}`
: ""}
</div>
) : null}
<div className="pt-3">
<ProjectHeaderActions projectId={detail.project.id} />
</div>
<div className="mt-4 space-y-3">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[14px] font-semibold text-[#111111]"> Agent </div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">
{detail.masterContextStrategySummary}
</div>
</div>
{detail.activeThreadContexts.map((thread) => (
<Link
key={thread.snapshot.threadId}
href={`/threads/${thread.snapshot.threadId}`}
className="block rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4"
>
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[14px] font-semibold text-[#111111]">
{thread.snapshot.title}
</div>
<div className="mt-1 text-[12px] text-[#8C8C8C]">
{thread.snapshot.workerId} · {thread.snapshot.nodeId}
</div>
</div>
<div className="text-right">
<div className="text-[14px] font-semibold text-[#111111]">
{thread.snapshot.contextBudgetRemainingPct}%
</div>
<div className="text-[12px] text-[#8C8C8C]">
{thread.snapshot.contextBudgetLevel}
</div>
</div>
</div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">
{thread.snapshot.summary}
</div>
{thread.handoffPackage ? (
<div className="mt-2 rounded-xl bg-[#FFF7E6] px-3 py-2 text-[12px] text-[#D46B08]">
handoff: {thread.handoffPackage.packageStatus}
</div>
) : null}
</Link>
))}
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[14px] font-semibold text-[#111111]"> APP </div>
<div className="text-[12px] text-[#8C8C8C]">
{detail.recentAppLogs.length ? "已同步到服务器" : "等待客户端日志"}
</div>
</div>
<div className="mt-3 space-y-3">
{detail.recentAppLogs.length ? (
detail.recentAppLogs.map((log) => (
<div key={log.logId} className="rounded-2xl bg-[#F7F8FA] px-3 py-3">
<div className="flex items-center justify-between gap-3 text-[12px] text-[#8C8C8C]">
<span>
{log.deviceId} · {log.category}
</span>
<span>{formatTimestampLabel(log.createdAt)}</span>
</div>
<div className="mt-2 text-[13px] leading-6 text-[#111111]">{log.message}</div>
{log.detail ? (
<div className="mt-1 text-[12px] leading-5 text-[#57606A]">{log.detail}</div>
) : null}
</div>
))
) : (
<div className="rounded-2xl bg-[#FFF7E6] px-3 py-3 text-[12px] leading-6 text-[#D46B08]">
APP Skill OTA
</div>
)}
</div>
</div>
</div>
<div className="mt-4 flex-1 space-y-4 overflow-y-auto pb-6">
{detail.project.messages.map((message) => (
<ChatBubble key={message.id} message={message} />
))}
<div className="rounded-2xl bg-white px-4 py-3 text-[13px] leading-6 text-[#57606A]">
MVP
</div>
<Link
href={`/conversations/${detail.project.id}/versions`}
className="block rounded-2xl bg-[#EAF7F0] px-4 py-3 text-[13px] leading-6 text-[#215B39]"
>
Agent
</Link>
</div>
</div>
<ChatComposer projectId={detail.project.id} />
</AppShell>
);
}

View File

@@ -0,0 +1,43 @@
import { notFound } from "next/navigation";
import {
AppShell,
PageNav,
StatusBar,
} from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getProject } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function VersionsPage({
params,
}: {
params: Promise<{ projectId: string }>;
}) {
await requirePageSession();
const { projectId } = await params;
const project = await getProject(projectId);
if (!project) notFound();
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="版本迭代记录" backHref={`/conversations/${projectId}`} />
<div className="flex flex-col gap-3 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
Agent 线
</div>
{project.versions.map((item) => (
<div key={item.version} className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[16px] font-semibold text-[#111111]">{item.version}</div>
<div className="text-[12px] text-[#8C8C8C]">{item.createdAt}</div>
</div>
<div className="mt-2 text-[14px] leading-6 text-[#57606A]">{item.summary}</div>
</div>
))}
</div>
</AppShell>
);
}

View File

@@ -0,0 +1,33 @@
import { RealtimeRefresh } from "@/components/app-runtime";
import {
AppShell,
ConversationList,
HeaderTitle,
LogoutButton,
StatusBar,
} from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getConversationItems } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function ConversationsPage() {
await requirePageSession();
const state = await readState();
const conversations = getConversationItems(state);
return (
<AppShell>
<RealtimeRefresh events={["conversation.updated", "project.messages.updated", "app.logs.updated"]} />
<StatusBar />
<HeaderTitle
title="会话"
extra={
<LogoutButton label="退出" compact />
}
/>
<ConversationList conversations={conversations} />
</AppShell>
);
}

View File

@@ -0,0 +1,21 @@
import { AppShell, DeviceEnrollmentBuilder, PageNav, StatusBar } from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
export default async function AddDevicePage() {
await requirePageSession();
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="添加设备" backHref="/devices" />
<div className="space-y-3 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold"></div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">
pairing code token MacWindows
</div>
</div>
<DeviceEnrollmentBuilder />
</div>
</AppShell>
);
}

62
src/app/devices/page.tsx Normal file
View File

@@ -0,0 +1,62 @@
import Link from "next/link";
import { RealtimeRefresh } from "@/components/app-runtime";
import {
AppShell,
DeviceEditorCard,
DeviceList,
HeaderTitle,
StatusBar,
} from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getDeviceWorkspaceView } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function DevicesPage({
searchParams,
}: {
searchParams: Promise<{ device?: string }>;
}) {
await requirePageSession();
const state = await readState();
const resolvedSearchParams = await searchParams;
const selectedDeviceId = resolvedSearchParams.device;
const workspace = getDeviceWorkspaceView(state, selectedDeviceId);
return (
<AppShell>
<RealtimeRefresh events={["devices.updated", "devices.skills.updated", "project.context_risk.updated"]} />
<StatusBar />
<HeaderTitle
title="设备"
extra={
<Link href="/devices/add" className="text-[20px] font-semibold text-[#07C160]">
</Link>
}
/>
<div className="px-[18px] pb-3 text-[12px] text-[#8C8C8C]">
</div>
{state.devices.length ? (
<DeviceList devices={state.devices} />
) : (
<div className="px-[18px] pb-5">
<div className="rounded-2xl border border-dashed border-[#D9D9D9] bg-white px-4 py-5 text-[13px] leading-6 text-[#57606A]">
`local-agent` pairing code
</div>
</div>
)}
{workspace.selectedDevice ? (
<div className="px-[18px] pb-5">
<DeviceEditorCard
device={workspace.selectedDevice}
relatedThreads={workspace.relatedThreads}
activeEnrollment={workspace.activeEnrollment}
/>
</div>
) : null}
</AppShell>
);
}

View File

@@ -1,26 +1,58 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
--boss-bg: #f5f5f7;
--boss-card: #ffffff;
--boss-border: #e5e5ea;
--boss-text: #111111;
--boss-subtle: #8c8c8c;
--boss-green: #07c160;
--boss-green-soft: #eaf7f0;
--boss-abnormal: #ff4d4f;
--boss-offline: #b8b8b8;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
* {
box-sizing: border-box;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
html,
body {
min-height: 100%;
height: 100%;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
margin: 0;
min-height: 100dvh;
background-color: #eef2eb;
background-image: url("/boss-app-bg.svg");
background-position: center top;
background-repeat: no-repeat;
background-size: cover;
color: var(--boss-text);
font-family:
-apple-system, BlinkMacSystemFont, "PingFang SC", "Helvetica Neue",
Helvetica, Arial, sans-serif;
overflow-x: hidden;
}
a {
color: inherit;
text-decoration: none;
}
button,
input,
textarea {
font: inherit;
}
.boss-scrollbar::-webkit-scrollbar {
width: 6px;
}
.boss-scrollbar::-webkit-scrollbar-thumb {
background: rgba(17, 17, 17, 0.12);
border-radius: 999px;
}

View File

@@ -1,33 +1,49 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import type { Metadata, Viewport } from "next";
import { Suspense } from "react";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
import { AppLogBridge, NativeAppBridge } from "@/components/app-runtime";
import { getCurrentPageSession } from "@/lib/boss-auth";
import { getPreferredDeviceIdForAccountFromState, readState } from "@/lib/boss-data";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Boss 控制台",
description: "多设备 Codex 协作控制台 MVP",
};
export default function RootLayout({
export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
userScalable: false,
themeColor: "#eef2eb",
viewportFit: "cover",
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const session = await getCurrentPageSession();
const state = session ? await readState() : null;
const boundDeviceId =
session && state
? getPreferredDeviceIdForAccountFromState(
state,
session.account,
state.user.boundDeviceId,
)
: undefined;
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<html lang="zh-CN">
<body data-bound-device-id={boundDeviceId ?? ""}>
<Suspense fallback={null}>
<NativeAppBridge />
</Suspense>
<AppLogBridge deviceId={boundDeviceId} />
{children}
</body>
</html>
);
}

37
src/app/me/about/page.tsx Normal file
View File

@@ -0,0 +1,37 @@
import { AppShell, OtaCenterCard, PageNav, StatusBar } from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getOtaStatus, readState } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function AboutPage() {
await requirePageSession();
const [state, otaStatus] = await Promise.all([readState(), getOtaStatus()]);
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="关于 / OTA" backHref="/me" />
<div className="flex flex-col gap-3 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[16px] font-semibold"></div>
<div className="mt-2 text-[14px] leading-6 text-[#57606A]">
{state.user.version}
<br />
{state.user.account}
<br />
Codex{state.user.boundCodexNodeLabel ?? "未绑定"}
</div>
</div>
<OtaCenterCard
currentVersion={state.user.version}
availableRelease={otaStatus.availableRelease}
logs={otaStatus.logs}
boundCodexNodeLabel={state.user.boundCodexNodeLabel}
roleLabel={state.user.roleLabel}
canApply={state.user.role === "highest_admin"}
/>
</div>
</AppShell>
);
}

View File

@@ -0,0 +1,25 @@
import { AppShell, HeaderTitle, PageNav, StatusBar } from "@/components/app-ui";
import { AiAccountsClient } from "@/components/ai-accounts-client";
import { requirePageSession } from "@/lib/boss-auth";
import { listAiAccounts } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function AiAccountsPage() {
const session = await requirePageSession();
const data = await listAiAccounts();
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="AI 账号" backHref="/me" />
<HeaderTitle title="主控与账号状态" />
<AiAccountsClient
accounts={data.accounts}
activeIdentity={data.activeIdentity}
switchHistory={data.switchHistory}
canManage={session.role === "highest_admin"}
/>
</AppShell>
);
}

View File

@@ -0,0 +1,82 @@
import Link from "next/link";
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getAuditSummaryView } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function AuditPage() {
await requirePageSession();
const state = await readState();
const summary = getAuditSummaryView(state);
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="运维与修复" backHref="/me/ops" />
<div className="flex min-h-0 flex-1 flex-col px-[18px] pb-0">
<div className="grid grid-cols-2 gap-3">
<Link
href="/me/ops"
className="flex h-10 items-center justify-center rounded-full bg-white text-[14px] font-semibold text-[#111111]"
>
</Link>
<Link
href="/me/ops/audit"
className="flex h-10 items-center justify-center rounded-full bg-[#07C160] text-[14px] font-semibold text-white"
>
</Link>
</div>
<div className="mt-4 flex-1 space-y-4 overflow-y-auto pb-6">
{summary.pendingRequests.map((request) => (
<div
key={request.auditRequestId}
className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4"
>
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[14px] font-semibold text-[#111111]">
{request.auditType} audit
</div>
<div className="mt-1 text-[12px] text-[#8C8C8C]">
{request.projectName} · priority {request.priority}
</div>
</div>
<div className="text-[12px] text-[#8C8C8C]">{request.trigger}</div>
</div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">
{request.objective}
</div>
</div>
))}
{summary.latestResults.map((result) => (
<div
key={result.auditRequestId}
className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4"
>
<div className="flex items-start justify-between gap-3">
<div className="text-[14px] font-semibold text-[#111111]">
{result.decision}
</div>
<div className="text-[12px] text-[#8C8C8C]">{result.status}</div>
</div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">{result.summary}</div>
<div className="mt-2 text-[12px] leading-5 text-[#8C8C8C]">
findings{result.findings.join("")}
</div>
</div>
))}
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[14px] font-semibold text-[#111111]"></div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">
{summary.capabilities.map((capability) => `${capability.displayName} (${capability.status})`).join("")}
</div>
</div>
</div>
</div>
</AppShell>
);
}

72
src/app/me/ops/page.tsx Normal file
View File

@@ -0,0 +1,72 @@
import Link from "next/link";
import { AppShell, PageNav, RepairTicketActions, StatusBar } from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getOpsSummaryView } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function OpsPage() {
await requirePageSession();
const state = await readState();
const summary = getOpsSummaryView(state);
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="运维与修复" backHref="/me" />
<div className="flex min-h-0 flex-1 flex-col px-[18px] pb-0">
<div className="grid grid-cols-2 gap-3">
<Link
href="/me/ops"
className="flex h-10 items-center justify-center rounded-full bg-[#07C160] text-[14px] font-semibold text-white"
>
</Link>
<Link
href="/me/ops/audit"
className="flex h-10 items-center justify-center rounded-full bg-white text-[14px] font-semibold text-[#111111]"
>
</Link>
</div>
<div className="mt-4 flex-1 space-y-4 overflow-y-auto pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[14px] font-semibold text-[#111111]"></div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">
{summary.mode === "active"
? "高频 active当前存在风险线程或未关闭运维工单。"
: "idle当前没有高风险工单保持低频巡检。"}
</div>
</div>
{summary.faults.map((fault) => (
<div key={fault.faultId} className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[14px] font-semibold text-[#111111]">{fault.faultKey}</div>
<div className="mt-1 text-[12px] text-[#8C8C8C]">
{fault.nodeId} · {fault.serviceName}
</div>
</div>
<div className="text-[12px] text-[#8C8C8C]">{fault.status}</div>
</div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">{fault.summary}</div>
<div className="mt-2 text-[12px] leading-5 text-[#8C8C8C]">
{fault.suggestedNextAction}
</div>
{summary.tickets
.filter((ticket) => ticket.faultId === fault.faultId)
.map((ticket) => (
<RepairTicketActions
key={ticket.ticketId}
ticket={ticket}
verification={ticket.verification}
/>
))}
</div>
))}
</div>
</div>
</AppShell>
);
}

63
src/app/me/page.tsx Normal file
View File

@@ -0,0 +1,63 @@
import {
AppShell,
HeaderTitle,
MenuRow,
OtaCenterCard,
ProfileHero,
StatusBar,
} from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getOtaStatus, readState } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function MePage() {
await requirePageSession();
const [state, otaStatus] = await Promise.all([readState(), getOtaStatus()]);
return (
<AppShell>
<StatusBar />
<HeaderTitle title="我的" />
<div className="flex flex-col gap-3 px-[18px] pb-5">
<ProfileHero user={state.user} />
<MenuRow
href="/me/security"
title="账号与安全"
description="修改登录密码、设备安全与身份校验"
/>
<MenuRow href="/me/settings" title="设置" description="实时刷新、风险徽标和默认首页" />
<MenuRow
href="/me/ops"
title="运维与修复"
description="查看运维对话、审计对话、修复回放与 standby 切换"
/>
<MenuRow
href="/me/ai-accounts"
title="AI 账号"
description="管理主 GPT、备用 GPT、ChatGPT Plus / Codex 节点与 API 容灾"
/>
<MenuRow
href="/me/skills"
title="技能"
description="按绑定设备查看已同步 Skill并一键复制调用语句"
/>
<MenuRow
href="/me/about"
title="关于"
description={`当前版本 ${state.user.version}`}
badge={state.user.hasOta ? <span className="text-[12px] text-[#FF3B30]">OTA</span> : null}
/>
<OtaCenterCard
currentVersion={state.user.version}
availableRelease={otaStatus.availableRelease}
logs={otaStatus.logs}
boundCodexNodeLabel={state.user.boundCodexNodeLabel}
roleLabel={state.user.roleLabel}
canApply={state.user.role === "highest_admin"}
compact
/>
</div>
</AppShell>
);
}

View File

@@ -0,0 +1,29 @@
import { AppShell, LogoutButton, MenuRow, PageNav, StatusBar } from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
export default async function SecurityPage() {
const session = await requirePageSession();
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="账号与安全" backHref="/me" />
<div className="flex flex-col gap-3 px-[18px] pb-6">
<MenuRow href="/auth/forgot-password" title="修改账号密码" description="通过验证码重置登录密码" />
<MenuRow href="/devices" title="设备安全管理" description="查看设备绑定、登录状态和剩余额度" />
<div className="rounded-2xl bg-[#EAF7F0] px-4 py-4 text-[13px] leading-6 text-[#215B39]">
使
`17600003315` Codex
`000000`
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
<span className="font-semibold text-[#111111]">{session.account}</span>
<br />
{session.loginMethod === "password" ? "账号密码" : "验证码"}
<br />
{new Date(session.expiresAt).toLocaleString("zh-CN", { hour12: false })}
</div>
<LogoutButton />
</div>
</AppShell>
);
}

View File

@@ -0,0 +1,23 @@
import { AppShell, PageNav, SettingsForm, StatusBar } from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { readState } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function SettingsPage() {
await requirePageSession();
const state = await readState();
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="设置" backHref="/me" />
<div className="space-y-3 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
`data/boss-state.json`线
</div>
<SettingsForm settings={state.user.settings} />
</div>
</AppShell>
);
}

View File

@@ -0,0 +1,33 @@
import { PageNav, StatusBar } from "@/components/app-ui";
import { RealtimeRefresh, SkillInventoryPanel } from "@/components/app-runtime";
import { AppShell } from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getPreferredDeviceIdForAccount, readState } from "@/lib/boss-data";
import { getSkillInventoryViewForAccount } from "@/lib/boss-projections";
export const dynamic = "force-dynamic";
export default async function SkillsPage() {
const session = await requirePageSession();
const state = await readState();
const boundDeviceId = await getPreferredDeviceIdForAccount(
session.account,
state.user.boundDeviceId,
);
const inventory = getSkillInventoryViewForAccount(state, session.account, boundDeviceId);
return (
<AppShell>
<RealtimeRefresh events={["devices.skills.updated", "devices.updated", "app.logs.updated"]} />
<StatusBar />
<PageNav title="技能" backHref="/me" />
<div className="flex flex-col gap-4 px-[18px] pb-5">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
Skill Skill local-agent `~/.codex/skills`
Skill Skill
</div>
<SkillInventoryPanel groups={inventory.groups} boundDeviceId={inventory.boundDeviceId} />
</div>
</AppShell>
);
}

View File

@@ -1,65 +1,7 @@
import Image from "next/image";
import { redirect } from "next/navigation";
import { getCurrentPageSession } from "@/lib/boss-auth";
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
export default async function Home() {
const session = await getCurrentPageSession();
redirect(session ? "/conversations" : "/auth/login");
}

View File

@@ -0,0 +1,90 @@
import { notFound } from "next/navigation";
import Link from "next/link";
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
import { requirePageSession } from "@/lib/boss-auth";
import { getThreadContextDetailView } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
export const dynamic = "force-dynamic";
export default async function ThreadPage({
params,
}: {
params: Promise<{ threadId: string }>;
}) {
await requirePageSession();
const { threadId } = await params;
const state = await readState();
const detail = getThreadContextDetailView(state, threadId);
if (!detail) notFound();
return (
<AppShell bottomNav={false}>
<StatusBar />
<PageNav title="线程详情" backHref={`/conversations/${detail.snapshot.projectId}`} />
<div className="space-y-3 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[16px] font-semibold text-[#111111]">
{detail.snapshot.title}
</div>
<div className="mt-1 text-[12px] text-[#8C8C8C]">
{detail.snapshot.workerId} · {detail.snapshot.nodeId}
</div>
</div>
<div className="text-right">
<div className="text-[18px] font-semibold text-[#111111]">
{detail.snapshot.contextBudgetRemainingPct}%
</div>
<div className="text-[12px] text-[#8C8C8C]">
{detail.snapshot.contextBudgetLevel}
</div>
</div>
</div>
<div className="mt-3 text-[13px] leading-6 text-[#57606A]">
{detail.snapshot.summary}
</div>
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[14px] font-semibold text-[#111111]"></div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">
{detail.currentChecklist.map((item) => `${item}`).join("\n")}
</div>
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="text-[14px] font-semibold text-[#111111]"> Agent </div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">
{detail.masterActions.map((item) => `${item}`).join("\n") || "当前无额外动作。"}
</div>
</div>
{detail.handoffPackage ? (
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[14px] font-semibold text-[#111111]">handoff package</div>
<div className="text-[12px] text-[#8C8C8C]">{detail.handoffPackage.packageStatus}</div>
</div>
<div className="mt-2 text-[13px] leading-6 text-[#57606A]">
{detail.handoffPackage.summaryText}
</div>
<div className="mt-2 text-[12px] leading-5 text-[#8C8C8C]">
critical commands{detail.handoffPackage.criticalCommands.join("") || "暂无"}
</div>
</div>
) : null}
{detail.alerts.map((alert) => (
<div key={alert.alertId} className="rounded-2xl bg-[#FFF7E6] px-4 py-4 text-[13px] leading-6 text-[#D46B08]">
{alert.summary}
</div>
))}
<Link
href={`/api/v1/threads/${threadId}/context-budget`}
className="block rounded-2xl bg-[#F5F5F7] px-4 py-4 text-[12px] text-[#57606A]"
>
JSON /api/v1/threads/{threadId}/context-budget
</Link>
</div>
</AppShell>
);
}