feat: ship enterprise control and desktop governance

This commit is contained in:
AI Bot
2026-05-11 14:59:26 +08:00
parent 0757d07521
commit a311280238
285 changed files with 48574 additions and 2428 deletions

View File

@@ -0,0 +1,614 @@
import { NextRequest } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { buildRequestAuditMeta } from "@/lib/boss-audit";
import { requireCsrfSafeMutation } from "@/lib/boss-csrf";
import { BOSS_PERMISSION_TEMPLATES } from "@/lib/boss-access-templates";
import {
appendPermissionAuditLog,
assignAccountCompanyByAdmin,
assignDeviceCompanyByAdmin,
bulkImportAuthAccountsByAdmin,
previewBulkImportAuthAccountsByAdmin,
readState,
reclaimAuthAccountByAdmin,
resetAuthAccountPasswordByAdmin,
revokeAccessGrant,
saveAccountDeviceGrant,
saveAccountProjectGrant,
saveAccountSkillGrant,
setAdminCompanyStatusByAdmin,
setAuthAccountMfaRequiredByAdmin,
setAuthAccountStatusByAdmin,
upsertAdminCompanyByAdmin,
upsertAuthAccountByAdmin,
} from "@/lib/boss-data";
import type { AuthAccount, AuthAccountStatus, AuthRole, BossPermission, Device, Project } from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
const validRoles = new Set<AuthRole>(["member", "admin", "highest_admin"]);
const validImportRoles = new Set<AuthRole>(["member", "admin"]);
const validAccountStatuses = new Set<AuthAccountStatus>(["active", "disabled"]);
const validCompanyStatuses = new Set(["active", "disabled"]);
const validCompanyPlanTiers = new Set(["trial", "standard", "enterprise"]);
const validPermissions = new Set<BossPermission>([
"device.view",
"device.manage",
"project.view",
"thread.chat",
"master_agent.ask",
"master_agent.takeover",
"computer.control",
"skill.view",
"skill.use",
"skill.manage",
"account.manage",
"audit.view",
]);
function publicAuthAccount(account: AuthAccount) {
const { passwordHash, mfaSecret, ...safeAccount } = account;
void passwordHash;
void mfaSecret;
return safeAccount;
}
function publicAdminDevice(device: Device) {
return {
id: device.id,
name: device.name,
avatar: device.avatar,
account: device.account,
status: device.status,
companyId: device.companyId,
source: device.source,
lastSeenAt: device.lastSeenAt,
preferredExecutionMode: device.preferredExecutionMode,
capabilities: device.capabilities,
};
}
function publicAdminProject(project: Project) {
return {
id: project.id,
name: project.name,
deviceIds: project.deviceIds,
threadMeta: project.threadMeta,
isGroup: project.isGroup,
updatedAt: project.updatedAt,
lastMessageAt: project.lastMessageAt,
collaborationMode: project.collaborationMode,
approvalState: project.approvalState,
riskLevel: project.riskLevel,
};
}
function buildSkillCatalog(skills: Array<{
skillId: string;
deviceId: string;
name: string;
description: string;
path: string;
invocation: string;
category: string;
updatedAt: string;
}>) {
const catalog = new Map<string, {
name: string;
invocation: string;
description: string;
deviceCount: number;
devices: Array<{ skillId: string; deviceId: string; path: string; category: string; updatedAt: string }>;
}>();
for (const skill of skills) {
const key = skill.name.trim() || skill.skillId;
const existing = catalog.get(key) ?? {
name: key,
invocation: skill.invocation,
description: skill.description,
deviceCount: 0,
devices: [],
};
existing.devices.push({
skillId: skill.skillId,
deviceId: skill.deviceId,
path: skill.path,
category: skill.category,
updatedAt: skill.updatedAt,
});
existing.deviceCount = existing.devices.length;
catalog.set(key, existing);
}
return [...catalog.values()].sort((left, right) => left.name.localeCompare(right.name, "zh-CN"));
}
function forbidden() {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
async function requireHighestAdmin(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return { ok: false as const, response: jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 }) };
}
if (session.role !== "highest_admin") {
return { ok: false as const, response: forbidden() };
}
return { ok: true as const, session };
}
function stringValue(value: unknown) {
return typeof value === "string" ? value.trim() : "";
}
function permissionValues(value: unknown): BossPermission[] {
if (!Array.isArray(value)) {
return [];
}
return value.filter((item): item is BossPermission =>
typeof item === "string" && validPermissions.has(item as BossPermission),
);
}
function stringArray(value: unknown) {
if (!Array.isArray(value)) {
return [];
}
return value.map(stringValue).filter(Boolean);
}
function accountImportValues(value: unknown) {
if (!Array.isArray(value)) {
return [];
}
return value
.filter((item): item is Record<string, unknown> => item !== null && typeof item === "object")
.map((item) => ({
account: stringValue(item.account),
displayName: stringValue(item.displayName),
role: validImportRoles.has(stringValue(item.role) as AuthRole) ? (stringValue(item.role) as AuthRole) : "member",
password: stringValue(item.password),
verificationEmail: stringValue(item.verificationEmail),
}))
.filter((item) => Boolean(item.account));
}
function idSegment(value: string) {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "item";
}
export async function GET(request: NextRequest) {
const auth = await requireHighestAdmin(request);
if (!auth.ok) {
return auth.response;
}
const state = await readState();
return jsonNoStore({
ok: true,
companies: state.adminCompanies,
accounts: state.authAccounts.map(publicAuthAccount),
devices: state.devices.map(publicAdminDevice),
projects: state.projects.map(publicAdminProject),
skills: state.deviceSkills,
skillCatalog: buildSkillCatalog(state.deviceSkills),
permissionTemplates: BOSS_PERMISSION_TEMPLATES,
grants: {
devices: state.accountDeviceGrants,
projects: state.accountProjectGrants,
skills: state.accountSkillGrants,
},
auditLogs: state.permissionAuditLogs,
});
}
export async function POST(request: NextRequest) {
const csrf = requireCsrfSafeMutation(request);
if (csrf) return csrf;
const auth = await requireHighestAdmin(request);
if (!auth.ok) {
return auth.response;
}
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
const action = stringValue(body.action);
const auditMeta = buildRequestAuditMeta(request);
if (action === "upsert_account") {
const account = stringValue(body.account);
const role = stringValue(body.role) as AuthRole;
if (!account || !validRoles.has(role)) {
return jsonNoStore({ ok: false, message: "ACCOUNT_OR_ROLE_INVALID" }, { status: 400 });
}
const saved = await upsertAuthAccountByAdmin({
account,
displayName: stringValue(body.displayName),
role,
password: stringValue(body.password),
verificationEmail: stringValue(body.verificationEmail),
companyId: stringValue(body.companyId) || undefined,
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({ ok: true, account: publicAuthAccount(saved) });
}
if (action === "set_account_status") {
const account = stringValue(body.account);
const status = stringValue(body.status) as AuthAccountStatus;
if (!account || !validAccountStatuses.has(status)) {
return jsonNoStore({ ok: false, message: "ACCOUNT_STATUS_INVALID" }, { status: 400 });
}
try {
const saved = await setAuthAccountStatusByAdmin({
account,
status,
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({ ok: true, account: publicAuthAccount(saved) });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore(
{ ok: false, message },
{ status: message === "ACCOUNT_NOT_FOUND" ? 404 : 400 },
);
}
}
if (action === "set_account_mfa_required") {
const account = stringValue(body.account);
if (!account) {
return jsonNoStore({ ok: false, message: "ACCOUNT_REQUIRED" }, { status: 400 });
}
try {
const saved = await setAuthAccountMfaRequiredByAdmin({
account,
required: body.required === true,
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({
ok: true,
account: publicAuthAccount(saved),
mfaSetupSecret: body.required === true ? saved.mfaSecret : undefined,
});
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore({ ok: false, message }, { status: message === "ACCOUNT_NOT_FOUND" ? 404 : 400 });
}
}
if (action === "reset_account_password") {
const account = stringValue(body.account);
const password = stringValue(body.password);
if (!account || !password) {
return jsonNoStore({ ok: false, message: "ACCOUNT_OR_PASSWORD_INVALID" }, { status: 400 });
}
try {
const saved = await resetAuthAccountPasswordByAdmin({
account,
password,
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({ ok: true, account: publicAuthAccount(saved) });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore(
{ ok: false, message },
{ status: message === "ACCOUNT_NOT_FOUND" ? 404 : 400 },
);
}
}
if (action === "upsert_company") {
const companyId = stringValue(body.companyId);
if (!companyId) {
return jsonNoStore({ ok: false, message: "COMPANY_ID_REQUIRED" }, { status: 400 });
}
const company = await upsertAdminCompanyByAdmin({
companyId,
name: stringValue(body.name),
ownerAccount: stringValue(body.ownerAccount),
successOwnerAccount: stringValue(body.successOwnerAccount),
planTier: validCompanyPlanTiers.has(stringValue(body.planTier))
? (stringValue(body.planTier) as "trial" | "standard" | "enterprise")
: undefined,
contractExpiresAt: stringValue(body.contractExpiresAt),
note: stringValue(body.note),
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({ ok: true, company });
}
if (action === "set_company_status") {
const companyId = stringValue(body.companyId);
const status = stringValue(body.status);
if (!companyId || !validCompanyStatuses.has(status)) {
return jsonNoStore({ ok: false, message: "COMPANY_STATUS_INVALID" }, { status: 400 });
}
try {
const result = await setAdminCompanyStatusByAdmin({
companyId,
status: status as "active" | "disabled",
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({ ok: true, ...result });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore({ ok: false, message }, { status: message === "COMPANY_NOT_FOUND" ? 404 : 400 });
}
}
if (action === "assign_account_company") {
const account = stringValue(body.account);
const companyId = stringValue(body.companyId);
if (!account || !companyId) {
return jsonNoStore({ ok: false, message: "ACCOUNT_OR_COMPANY_INVALID" }, { status: 400 });
}
try {
const saved = await assignAccountCompanyByAdmin({
account,
companyId,
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({ ok: true, account: publicAuthAccount(saved) });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore({ ok: false, message }, { status: message.endsWith("_NOT_FOUND") ? 404 : 400 });
}
}
if (action === "assign_device_company") {
const deviceId = stringValue(body.deviceId);
const companyId = stringValue(body.companyId);
if (!deviceId || !companyId) {
return jsonNoStore({ ok: false, message: "DEVICE_OR_COMPANY_INVALID" }, { status: 400 });
}
try {
const device = await assignDeviceCompanyByAdmin({
deviceId,
companyId,
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({ ok: true, device });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore({ ok: false, message }, { status: message.endsWith("_NOT_FOUND") ? 404 : 400 });
}
}
if (action === "bulk_import_accounts") {
const companyId = stringValue(body.companyId);
const accounts = accountImportValues(body.accounts);
if (!companyId || accounts.length === 0) {
return jsonNoStore({ ok: false, message: "BULK_IMPORT_INVALID" }, { status: 400 });
}
try {
const imported = await bulkImportAuthAccountsByAdmin({
companyId,
accounts,
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({ ok: true, imported: imported.map(publicAuthAccount) });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore({ ok: false, message }, { status: message.endsWith("_NOT_FOUND") ? 404 : 400 });
}
}
if (action === "preview_bulk_import_accounts") {
const companyId = stringValue(body.companyId);
const accounts = accountImportValues(body.accounts);
if (!companyId || accounts.length === 0) {
return jsonNoStore({ ok: false, message: "BULK_IMPORT_INVALID" }, { status: 400 });
}
try {
const preview = await previewBulkImportAuthAccountsByAdmin({
companyId,
accounts,
});
return jsonNoStore({ ok: true, preview });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore({ ok: false, message }, { status: message === "COMPANY_NOT_FOUND" ? 404 : 400 });
}
}
if (action === "reclaim_account") {
const account = stringValue(body.account);
if (!account) {
return jsonNoStore({ ok: false, message: "ACCOUNT_REQUIRED" }, { status: 400 });
}
try {
const result = await reclaimAuthAccountByAdmin({
account,
reason: stringValue(body.reason),
actorAccount: auth.session.account,
auditMeta,
});
return jsonNoStore({
ok: true,
account: publicAuthAccount(result.account),
removedGrants: result.removedGrants,
});
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore({ ok: false, message }, { status: message === "ACCOUNT_NOT_FOUND" ? 404 : 400 });
}
}
if (action === "grant_device") {
const account = stringValue(body.account);
const deviceId = stringValue(body.deviceId);
const permissions = permissionValues(body.permissions);
if (!account || !deviceId || permissions.length === 0) {
return jsonNoStore({ ok: false, message: "DEVICE_GRANT_INVALID" }, { status: 400 });
}
const grant = await saveAccountDeviceGrant({
grantId: stringValue(body.grantId) || undefined,
account,
deviceId,
permissions,
grantedBy: auth.session.account,
expiresAt: stringValue(body.expiresAt) || undefined,
note: stringValue(body.note) || undefined,
auditMeta,
});
return jsonNoStore({ ok: true, grant });
}
if (action === "grant_project") {
const account = stringValue(body.account);
const projectId = stringValue(body.projectId);
const permissions = permissionValues(body.permissions);
if (!account || !projectId || permissions.length === 0) {
return jsonNoStore({ ok: false, message: "PROJECT_GRANT_INVALID" }, { status: 400 });
}
const grant = await saveAccountProjectGrant({
grantId: stringValue(body.grantId) || undefined,
account,
projectId,
deviceId: stringValue(body.deviceId) || undefined,
permissions,
inheritFromDeviceGrant: body.inheritFromDeviceGrant === true,
grantedBy: auth.session.account,
expiresAt: stringValue(body.expiresAt) || undefined,
note: stringValue(body.note) || undefined,
auditMeta,
});
return jsonNoStore({ ok: true, grant });
}
if (action === "grant_skill") {
const account = stringValue(body.account);
const skillId = stringValue(body.skillId);
const permissions = permissionValues(body.permissions);
if (!account || !skillId || permissions.length === 0) {
return jsonNoStore({ ok: false, message: "SKILL_GRANT_INVALID" }, { status: 400 });
}
const grant = await saveAccountSkillGrant({
grantId: stringValue(body.grantId) || undefined,
account,
skillId,
deviceId: stringValue(body.deviceId) || undefined,
projectId: stringValue(body.projectId) || undefined,
permissions,
grantedBy: auth.session.account,
expiresAt: stringValue(body.expiresAt) || undefined,
note: stringValue(body.note) || undefined,
auditMeta,
});
return jsonNoStore({ ok: true, grant });
}
if (action === "apply_template") {
const account = stringValue(body.account);
const templateId = stringValue(body.templateId);
const template = BOSS_PERMISSION_TEMPLATES.find((item) => item.templateId === templateId);
const deviceIds = stringArray(body.deviceIds);
const projectIds = stringArray(body.projectIds);
const skillIds = stringArray(body.skillIds);
if (!account || !template || deviceIds.length + projectIds.length + skillIds.length === 0) {
return jsonNoStore({ ok: false, message: "ACCESS_TEMPLATE_INVALID" }, { status: 400 });
}
const state = await readState();
if (!state.authAccounts.some((item) => item.account === account)) {
return jsonNoStore({ ok: false, message: "ACCOUNT_NOT_FOUND" }, { status: 404 });
}
const missingDevice = deviceIds.find((deviceId) => !state.devices.some((device) => device.id === deviceId));
const missingProject = projectIds.find((projectId) => !state.projects.some((project) => project.id === projectId));
const missingSkill = skillIds.find((skillId) => !state.deviceSkills.some((skill) => skill.skillId === skillId));
if (missingDevice || missingProject || missingSkill) {
return jsonNoStore({
ok: false,
message: "ACCESS_TEMPLATE_TARGET_NOT_FOUND",
missingDevice,
missingProject,
missingSkill,
}, { status: 404 });
}
const deviceGrants = [];
for (const deviceId of deviceIds) {
deviceGrants.push(await saveAccountDeviceGrant({
grantId: `grant-template-${idSegment(templateId)}-device-${idSegment(account)}-${idSegment(deviceId)}`,
account,
deviceId,
permissions: template.devicePermissions,
grantedBy: auth.session.account,
note: template.name,
auditMeta,
}));
}
const projectGrants = [];
for (const projectId of projectIds) {
projectGrants.push(await saveAccountProjectGrant({
grantId: `grant-template-${idSegment(templateId)}-project-${idSegment(account)}-${idSegment(projectId)}`,
account,
projectId,
permissions: template.projectPermissions,
grantedBy: auth.session.account,
note: template.name,
auditMeta,
}));
}
const skillGrants = [];
for (const skillId of skillIds) {
const skill = state.deviceSkills.find((item) => item.skillId === skillId);
skillGrants.push(await saveAccountSkillGrant({
grantId: `grant-template-${idSegment(templateId)}-skill-${idSegment(account)}-${idSegment(skillId)}`,
account,
skillId,
deviceId: skill?.deviceId,
permissions: template.skillPermissions,
grantedBy: auth.session.account,
note: template.name,
auditMeta,
}));
}
await appendPermissionAuditLog({
actorAccount: auth.session.account,
action: "grant.updated",
targetAccount: account,
permissions: [
...template.devicePermissions,
...template.projectPermissions,
...template.skillPermissions,
],
detail: `template:${template.templateId}`,
...auditMeta,
});
return jsonNoStore({
ok: true,
template,
grants: {
devices: deviceGrants,
projects: projectGrants,
skills: skillGrants,
},
});
}
if (action === "revoke_grant") {
const grantId = stringValue(body.grantId);
if (!grantId) {
return jsonNoStore({ ok: false, message: "GRANT_ID_REQUIRED" }, { status: 400 });
}
const grant = await revokeAccessGrant(grantId, auth.session.account, auditMeta);
return jsonNoStore({ ok: true, grant });
}
return jsonNoStore({ ok: false, message: "UNKNOWN_ACTION" }, { status: 400 });
}

View File

@@ -0,0 +1,219 @@
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { requireRequestSession } from "@/lib/boss-auth";
import { BOSS_PERMISSION_TEMPLATES } from "@/lib/boss-access-templates";
import { buildAdminOverview } from "@/lib/boss-admin-overview";
import { readState, type BossState } from "@/lib/boss-data";
const MENU_TREE = [
{ key: "workbench", label: "工作台" },
{ key: "tenant", label: "租户管理" },
{ key: "user", label: "账号管理" },
{ key: "role", label: "角色权限" },
{
key: "resource",
label: "资源授权",
children: [
{ key: "resource.devices", label: "设备资源" },
{ key: "resource.projects", label: "项目线程" },
{ key: "resource.skills", label: "Skill 资源" },
],
},
{ key: "skills", label: "Skill 中心" },
{ key: "risk", label: "风险告警" },
{ key: "audit", label: "审计日志" },
{ key: "system", label: "系统设置" },
] as const;
function companyNameMap(state: BossState) {
return new Map(state.adminCompanies.map((company) => [company.companyId, company.name]));
}
function companyNameFor(state: BossState, companyId?: string) {
if (!companyId || companyId === "default") return "默认公司";
return companyNameMap(state).get(companyId) ?? companyId;
}
function safeUsers(state: BossState) {
return state.authAccounts.map((account) => ({
id: account.id,
account: account.account,
displayName: account.displayName,
role: account.role,
status: account.status ?? "active",
companyId: account.companyId ?? "default",
companyName: companyNameFor(state, account.companyId),
primaryDeviceId: account.primaryDeviceId,
codexNodeId: account.codexNodeId,
codexNodeLabel: account.codexNodeLabel,
lastLoginAt: account.lastLoginAt,
lastLoginMethod: account.lastLoginMethod,
mfaRequired: Boolean(account.mfaRequired),
createdAt: account.createdAt,
updatedAt: account.updatedAt,
}));
}
function skillResources(state: BossState) {
const byName = new Map<
string,
{
skillId: string;
name: string;
description: string;
category?: string;
invocation?: string;
sourceType: "device" | "catalog";
deviceCount: number;
devices: Array<{ deviceId: string; updatedAt: string }>;
updatedAt: string;
}
>();
for (const skill of state.deviceSkills) {
const existing = byName.get(skill.name);
if (existing) {
existing.deviceCount += existing.devices.some((device) => device.deviceId === skill.deviceId) ? 0 : 1;
existing.devices.push({ deviceId: skill.deviceId, updatedAt: skill.updatedAt });
if (skill.updatedAt.localeCompare(existing.updatedAt) > 0) {
existing.updatedAt = skill.updatedAt;
}
continue;
}
byName.set(skill.name, {
skillId: skill.skillId,
name: skill.name,
description: skill.description,
category: skill.category,
invocation: skill.invocation,
sourceType: "device",
deviceCount: 1,
devices: [{ deviceId: skill.deviceId, updatedAt: skill.updatedAt }],
updatedAt: skill.updatedAt,
});
}
for (const catalogItem of state.skillCatalog) {
if (byName.has(catalogItem.name)) continue;
byName.set(catalogItem.name, {
skillId: catalogItem.skillId,
name: catalogItem.name,
description: catalogItem.description,
category: catalogItem.category,
sourceType: "catalog",
deviceCount: 0,
devices: [],
updatedAt: catalogItem.updatedAt,
});
}
return [...byName.values()].sort(
(left, right) => right.deviceCount - left.deviceCount || left.name.localeCompare(right.name, "zh-CN"),
);
}
function projectResources(state: BossState) {
return state.projects.map((project) => ({
id: project.id,
name: project.name,
deviceIds: project.deviceIds,
deviceCount: project.deviceIds.length,
folderName: project.threadMeta.folderName,
threadId: project.threadMeta.threadId,
threadDisplayName: project.threadMeta.threadDisplayName,
isGroup: project.isGroup,
collaborationMode: project.collaborationMode,
unreadCount: project.unreadCount,
riskLevel: project.riskLevel,
updatedAt: project.updatedAt,
lastMessageAt: project.lastMessageAt,
}));
}
function rolesContract() {
return {
builtInRoles: [
{
role: "highest_admin",
label: "超级管理员",
description: "平台侧最高权限可管理全部公司、账号、设备、项目、Skill、风险和审计。",
},
{
role: "admin",
label: "企业管理员",
description: "企业内管理员,按授权范围管理本公司资源。",
},
{
role: "member",
label: "成员账号",
description: "企业子账号,只能访问已分配的电脑、项目和 Skill。",
},
],
permissionTemplates: BOSS_PERMISSION_TEMPLATES,
};
}
function buildBackofficePayload(state: BossState) {
const overview = buildAdminOverview(state);
const skills = skillResources(state);
return {
ok: true,
menuTree: MENU_TREE,
workbench: {
summary: overview.summary,
companies: overview.companies.slice(0, 10),
devices: overview.devices.slice(0, 20),
risks: overview.risks.slice(0, 20),
notifications: overview.notifications,
grantsSummary: overview.grantsSummary,
},
tenants: overview.companies.map((company) => ({
...company,
lifecycleStatus: company.status ?? "active",
})),
users: safeUsers(state),
roles: rolesContract(),
resourceGroups: {
devices: overview.devices,
projects: projectResources(state),
skills,
grants: {
devices: state.accountDeviceGrants,
projects: state.accountProjectGrants,
skills: state.accountSkillGrants,
},
},
audit: {
risks: overview.risks,
notifications: overview.notifications,
riskTimeline: overview.riskTimeline,
permissionLogs: state.permissionAuditLogs
.slice()
.sort((left, right) => right.createdAt.localeCompare(left.createdAt))
.slice(0, 100),
},
yudaoMapping: {
tenant: "adminCompanies",
user: "authAccounts",
role: "BOSS_PERMISSION_TEMPLATES",
menu: "menuTree",
operateLog: "permissionAuditLogs",
resource: "devices/projects/deviceSkills",
risk: "opsFaults/threadContextAlerts/masterAgentTasks/adminNotifications",
},
};
}
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const state = await readState();
return jsonNoStore(buildBackofficePayload(state));
}

View File

@@ -0,0 +1,29 @@
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { requireRequestSession } from "@/lib/boss-auth";
import { requireCsrfSafeMutation } from "@/lib/boss-csrf";
import { dispatchAdminRiskNotifications } from "@/lib/boss-data";
function forbidden() {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
export async function POST(request: NextRequest) {
const csrf = requireCsrfSafeMutation(request);
if (csrf) return csrf;
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return forbidden();
}
const body = (await request.json().catch(() => ({}))) as { max?: number };
const result = await dispatchAdminRiskNotifications({
actorAccount: session.account,
max: typeof body.max === "number" && Number.isFinite(body.max) ? body.max : undefined,
});
return jsonNoStore({ ok: true, ...result });
}

View File

@@ -0,0 +1,21 @@
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { requireRequestSession } from "@/lib/boss-auth";
import { buildAdminOverview } from "@/lib/boss-admin-overview";
import { readState } from "@/lib/boss-data";
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const state = await readState();
return jsonNoStore({
ok: true,
...buildAdminOverview(state),
});
}

View File

@@ -0,0 +1,67 @@
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { requireRequestSession } from "@/lib/boss-auth";
import { requireCsrfSafeMutation } from "@/lib/boss-csrf";
import { handleAdminRiskAction, type AdminRiskAction } from "@/lib/boss-data";
const validActions = new Set<AdminRiskAction>(["ack", "resolve", "create_repair_ticket", "assign_owner", "set_sla"]);
function stringValue(value: unknown) {
return typeof value === "string" ? value.trim() : "";
}
function statusForError(error: unknown) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
if (message === "RISK_TARGET_NOT_FOUND") return 404;
if (
message === "RISK_ACTION_UNSUPPORTED" ||
message === "RISK_ID_INVALID" ||
message === "RISK_OWNER_REQUIRED" ||
message === "RISK_SLA_REQUIRED"
) return 400;
return 500;
}
export async function POST(request: NextRequest) {
const csrf = requireCsrfSafeMutation(request);
if (csrf) return csrf;
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
const riskId = stringValue(body.riskId);
const action = stringValue(body.action) as AdminRiskAction;
if (!riskId || !validActions.has(action)) {
return jsonNoStore({ ok: false, message: "RISK_ACTION_INVALID" }, { status: 400 });
}
const ownerAccount = stringValue(body.ownerAccount);
const slaDueAt = stringValue(body.slaDueAt);
const note = stringValue(body.note);
if (action === "assign_owner" && !ownerAccount) {
return jsonNoStore({ ok: false, message: "RISK_OWNER_REQUIRED" }, { status: 400 });
}
if (action === "set_sla" && !slaDueAt) {
return jsonNoStore({ ok: false, message: "RISK_SLA_REQUIRED" }, { status: 400 });
}
try {
const result = await handleAdminRiskAction({
riskId,
action,
actorAccount: session.account,
ownerAccount,
slaDueAt,
note,
});
return jsonNoStore({ ok: true, riskId, action, ...result });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore({ ok: false, message }, { status: statusForError(error) });
}
}

View File

@@ -0,0 +1,27 @@
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { requireRequestSession } from "@/lib/boss-auth";
import { requireCsrfSafeMutation } from "@/lib/boss-csrf";
import { scanAdminRiskNotifications } from "@/lib/boss-data";
function forbidden() {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
export async function POST(request: NextRequest) {
const csrf = requireCsrfSafeMutation(request);
if (csrf) return csrf;
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return forbidden();
}
const result = await scanAdminRiskNotifications({
actorAccount: session.account,
});
return jsonNoStore({ ok: true, ...result });
}

View File

@@ -0,0 +1,110 @@
import { NextRequest } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { buildRequestAuditMeta } from "@/lib/boss-audit";
import { requireCsrfSafeMutation } from "@/lib/boss-csrf";
import {
createSkillLifecycleRequest,
readState,
type SkillLifecycleAction,
} from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
const validActions = new Set<SkillLifecycleAction>([
"install",
"update",
"uninstall",
"rollback",
"version_lock",
]);
function stringValue(value: unknown) {
return typeof value === "string" ? value.trim() : "";
}
function forbidden() {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
async function requireHighestAdmin(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return { ok: false as const, response: jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 }) };
}
if (session.role !== "highest_admin") {
return { ok: false as const, response: forbidden() };
}
return { ok: true as const, session };
}
export async function GET(request: NextRequest) {
const auth = await requireHighestAdmin(request);
if (!auth.ok) {
return auth.response;
}
const state = await readState();
return jsonNoStore({
ok: true,
requests: [...state.skillLifecycleRequests].sort((left, right) =>
right.requestedAt.localeCompare(left.requestedAt),
),
});
}
export async function POST(request: NextRequest) {
const csrf = requireCsrfSafeMutation(request);
if (csrf) return csrf;
const auth = await requireHighestAdmin(request);
if (!auth.ok) {
return auth.response;
}
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
const action = stringValue(body.action) as SkillLifecycleAction;
const deviceId = stringValue(body.deviceId);
const skillId = stringValue(body.skillId);
const sourceUrl = stringValue(body.sourceUrl);
const trustedSource = stringValue(body.trustedSource);
const trustedSourceId = stringValue(body.trustedSourceId);
if (!validActions.has(action)) {
return jsonNoStore({ ok: false, message: "SKILL_ACTION_INVALID" }, { status: 400 });
}
if (!deviceId) {
return jsonNoStore({ ok: false, message: "DEVICE_ID_REQUIRED" }, { status: 400 });
}
if (!skillId && !sourceUrl && !trustedSource && !trustedSourceId) {
return jsonNoStore({ ok: false, message: "SKILL_ID_OR_SOURCE_URL_REQUIRED" }, { status: 400 });
}
const state = await readState();
if (!state.devices.some((device) => device.id === deviceId)) {
return jsonNoStore({ ok: false, message: "DEVICE_NOT_FOUND" }, { status: 404 });
}
if (
skillId &&
!state.deviceSkills.some((skill) => skill.deviceId === deviceId && skill.skillId === skillId)
) {
return jsonNoStore({ ok: false, message: "SKILL_NOT_FOUND" }, { status: 404 });
}
const requestRecord = await createSkillLifecycleRequest({
action,
deviceId,
skillId: skillId || undefined,
sourceUrl: sourceUrl || undefined,
trustedSource: trustedSource || undefined,
trustedSourceId: trustedSourceId || undefined,
checksum: stringValue(body.checksum) || undefined,
expectedChecksum: stringValue(body.expectedChecksum) || undefined,
targetVersion: stringValue(body.targetVersion) || undefined,
rollbackToVersion: stringValue(body.rollbackToVersion) || undefined,
lockedVersion: stringValue(body.lockedVersion) || undefined,
requestedBy: auth.session.account,
note: stringValue(body.note) || undefined,
auditMeta: buildRequestAuditMeta(request),
});
return jsonNoStore({ ok: true, request: requestRecord });
}

View File

@@ -0,0 +1,24 @@
import { NextRequest } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { jsonNoStore } from "@/lib/api-response";
import { permissionAuditQueryFromSearchParams, queryPermissionAuditLogs, summarizePermissionAuditRisks } from "@/lib/boss-audit";
import { readState } from "@/lib/boss-data";
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const state = await readState();
const query = permissionAuditQueryFromSearchParams(request.nextUrl.searchParams);
const result = queryPermissionAuditLogs(state.permissionAuditLogs, query);
return jsonNoStore({
ok: true,
...result,
riskSummary: summarizePermissionAuditRisks(state),
});
}

View File

@@ -0,0 +1,75 @@
import { NextRequest } from "next/server";
import { AUTH_SESSION_COOKIE, requireRequestSession } from "@/lib/boss-auth";
import { readState, revokeAuthSessionById } from "@/lib/boss-data";
import type { AuthSession } from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
function isActiveSession(session: AuthSession) {
return !session.revokedAt && new Date(session.expiresAt).getTime() > Date.now();
}
function publicSession(session: AuthSession, currentToken?: string | null) {
return {
sessionId: session.sessionId,
account: session.account,
role: session.role,
displayName: session.displayName,
loginMethod: session.loginMethod,
createdAt: session.createdAt,
expiresAt: session.expiresAt,
lastSeenAt: session.lastSeenAt,
current: Boolean(currentToken && session.sessionToken === currentToken),
};
}
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const currentToken = request.cookies.get(AUTH_SESSION_COOKIE)?.value;
const state = await readState();
const sessions = state.authSessions
.filter(isActiveSession)
.filter((item) => session.role === "highest_admin" || item.account === session.account)
.sort((left, right) => right.lastSeenAt.localeCompare(left.lastSeenAt))
.map((item) => publicSession(item, currentToken));
return jsonNoStore({ ok: true, sessions });
}
export async function POST(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const body = (await request.json().catch(() => ({}))) as {
action?: string;
sessionId?: string;
};
if (body.action !== "revoke_session") {
return jsonNoStore({ ok: false, message: "UNKNOWN_ACTION" }, { status: 400 });
}
if (!body.sessionId?.trim()) {
return jsonNoStore({ ok: false, message: "SESSION_ID_REQUIRED" }, { status: 400 });
}
try {
const revoked = await revokeAuthSessionById({
sessionId: body.sessionId.trim(),
actorAccount: session.account,
actorRole: session.role,
});
if (!revoked) {
return jsonNoStore({ ok: false, message: "SESSION_NOT_FOUND" }, { status: 404 });
}
return jsonNoStore({ ok: true, session: publicSession(revoked) });
} catch (error) {
if (error instanceof Error && error.message === "FORBIDDEN_AUTH_SESSION") {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
throw error;
}
}

View File

@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { getConversationFolderView } from "@/lib/boss-projections";
import { getConversationFolderViewForSession } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
@@ -14,7 +14,7 @@ export async function GET(
}
const { folderKey } = await context.params;
const state = await readState();
const folder = getConversationFolderView(state, decodeURIComponent(folderKey));
const folder = getConversationFolderViewForSession(state, session, decodeURIComponent(folderKey));
if (!folder) {
return jsonNoStore({ ok: false, message: "FOLDER_NOT_FOUND" }, { status: 404 });
}

View File

@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { getConversationHomeItems } from "@/lib/boss-projections";
import { getConversationHomeItemsForSession } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
@@ -12,6 +12,6 @@ export async function GET(request: NextRequest) {
const state = await readState();
return jsonNoStore({
ok: true,
conversations: getConversationHomeItems(state),
conversations: getConversationHomeItemsForSession(state, session),
});
}

View File

@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { getConversationItems } from "@/lib/boss-projections";
import { getConversationItemsForSession } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
@@ -12,6 +12,6 @@ export async function GET(request: NextRequest) {
const state = await readState();
return jsonNoStore({
ok: true,
conversations: getConversationItems(state),
conversations: getConversationItemsForSession(state, session),
});
}

View File

@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from "next/server";
import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth";
import { completeSkillLifecycleRequest } from "@/lib/boss-data";
export async function POST(
request: NextRequest,
context: { params: Promise<{ deviceId: string; requestId: string }> },
) {
const { deviceId, requestId } = await context.params;
const auth = await authorizeDeviceWriteRequest(request, deviceId);
if (!auth.ok) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (!auth.device) {
return NextResponse.json({ ok: false, message: "DEVICE_NOT_FOUND" }, { status: 404 });
}
const body = (await request.json().catch(() => ({}))) as {
status?: "completed" | "failed";
resultSummary?: string;
error?: string;
};
try {
const skillRequest = await completeSkillLifecycleRequest({
requestId,
deviceId,
status: body.status === "failed" ? "failed" : "completed",
resultSummary: typeof body.resultSummary === "string" ? body.resultSummary : undefined,
error: typeof body.error === "string" ? body.error : undefined,
});
return NextResponse.json({ ok: true, request: skillRequest });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: 400 },
);
}
}

View File

@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from "next/server";
import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth";
import { claimNextSkillLifecycleRequest } from "@/lib/boss-data";
export async function POST(
request: NextRequest,
context: { params: Promise<{ deviceId: string }> },
) {
const { deviceId } = await context.params;
const auth = await authorizeDeviceWriteRequest(request, deviceId);
if (!auth.ok) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (!auth.device) {
return NextResponse.json({ ok: false, message: "DEVICE_NOT_FOUND" }, { status: 404 });
}
const skillRequest = await claimNextSkillLifecycleRequest(deviceId);
return NextResponse.json({ ok: true, request: skillRequest });
}

View File

@@ -3,6 +3,7 @@ import { requireRequestSession } from "@/lib/boss-auth";
import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth";
import { readState, upsertDeviceSkills } from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
import { canAccessDevice, canViewSkill } from "@/lib/boss-permissions";
export async function GET(
request: NextRequest,
@@ -18,13 +19,24 @@ export async function GET(
if (!device) {
return jsonNoStore({ ok: false, message: "DEVICE_NOT_FOUND" }, { status: 404 });
}
const deviceSkills = state.deviceSkills
.filter((item) => item.deviceId === deviceId)
.sort((a, b) => a.name.localeCompare(b.name, "zh-CN"));
const visibleSkills = session.role === "highest_admin"
? deviceSkills
: deviceSkills.filter((skill) => canViewSkill(state, session, skill.skillId, { deviceId }));
if (
session.role !== "highest_admin" &&
!canAccessDevice(state, session, deviceId, "device.view") &&
visibleSkills.length === 0
) {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
return jsonNoStore({
ok: true,
device,
skills: state.deviceSkills
.filter((item) => item.deviceId === deviceId)
.sort((a, b) => a.name.localeCompare(b.name, "zh-CN")),
skills: visibleSkills,
});
}

View File

@@ -1,8 +1,9 @@
import { NextRequest } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { getDeviceWorkspaceView } from "@/lib/boss-projections";
import { getDeviceWorkspaceViewForSession } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
import { filterDevicesForSession } from "@/lib/boss-permissions";
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
@@ -12,11 +13,13 @@ export async function GET(request: NextRequest) {
const url = new URL(request.url);
const deviceId = url.searchParams.get("device");
const state = await readState();
const devices = filterDevicesForSession(state, session);
const visibleDeviceIds = new Set(devices.map((device) => device.id));
return jsonNoStore({
ok: true,
devices: state.devices,
enrollments: state.deviceEnrollments,
workspace: getDeviceWorkspaceView(state, deviceId ?? undefined),
devices,
enrollments: state.deviceEnrollments.filter((item) => visibleDeviceIds.has(item.deviceId)),
workspace: getDeviceWorkspaceViewForSession(state, session, deviceId ?? undefined),
});
}

View File

@@ -0,0 +1,82 @@
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import { requireRequestSession } from "@/lib/boss-auth";
import { requireCsrfSafeMutation } from "@/lib/boss-csrf";
import {
readState,
resolveDialogGuardInterventionDecision,
type DialogGuardInterventionAction,
} from "@/lib/boss-data";
import { canAccessDevice, canAccessProject } from "@/lib/boss-permissions";
const validDecisions = new Set<DialogGuardInterventionAction>([
"allow_once",
"allow_for_device_dialog",
"deny",
"handled_on_device",
"cancel_task",
]);
function stringValue(value: unknown) {
return typeof value === "string" ? value.trim() : "";
}
function statusForError(error: unknown) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
if (message === "DIALOG_GUARD_INTERVENTION_NOT_FOUND") return 404;
if (
message === "DIALOG_GUARD_INTERVENTION_ALREADY_RESOLVED" ||
message === "DIALOG_GUARD_DECISION_NOT_AVAILABLE"
) {
return 409;
}
return 400;
}
export async function POST(
request: NextRequest,
context: { params: Promise<{ interventionId: string }> },
) {
const csrf = requireCsrfSafeMutation(request);
if (csrf) return csrf;
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const { interventionId } = await context.params;
const state = await readState();
const intervention = state.dialogGuardInterventions.find(
(item) => item.interventionId === interventionId,
);
if (!intervention) {
return jsonNoStore({ ok: false, message: "DIALOG_GUARD_INTERVENTION_NOT_FOUND" }, { status: 404 });
}
const canResolve =
session.role === "highest_admin" ||
canAccessProject(state, session, intervention.projectId, "computer.control") ||
canAccessDevice(state, session, intervention.deviceId, "device.manage");
if (!canResolve) {
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
const decision = stringValue(body.decision) as DialogGuardInterventionAction;
if (!validDecisions.has(decision)) {
return jsonNoStore({ ok: false, message: "DIALOG_GUARD_DECISION_INVALID" }, { status: 400 });
}
try {
const resolved = await resolveDialogGuardInterventionDecision({
interventionId,
actorAccount: session.account,
decision,
note: stringValue(body.note),
});
return jsonNoStore({ ok: true, intervention: resolved });
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return jsonNoStore({ ok: false, message }, { status: statusForError(error) });
}
}

View File

@@ -0,0 +1,111 @@
import { NextRequest } from "next/server";
import { jsonNoStore } from "@/lib/api-response";
import {
getAuthorizedTelegramConfigSession,
getTelegramIntegrationView,
probeTelegramBot,
saveTelegramIntegrationConfig,
syncTelegramWebhookRegistration,
} from "@/lib/telegram-gateway";
export async function GET(request: NextRequest) {
const session = await getAuthorizedTelegramConfigSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
return jsonNoStore({ ok: true, telegram: await getTelegramIntegrationView() });
}
export async function POST(request: NextRequest) {
const session = await getAuthorizedTelegramConfigSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const body = (await request.json().catch(() => ({}))) as {
enabled?: boolean;
mode?: "webhook" | "polling";
botToken?: string;
botUsername?: string;
dmPolicy?: "allowlist" | "open" | "disabled";
allowFrom?: string[];
groupPolicy?: "allowlist" | "open" | "disabled";
groups?: string[];
requireMentionInGroups?: boolean;
defaultProjectId?: string;
groupProjectRoutes?: Array<{ chatId: string; threadId?: number; projectId: string; label?: string }>;
webhookSecret?: string;
webhookUrl?: string;
testConnection?: boolean;
};
let telegram = await saveTelegramIntegrationConfig({
enabled: body.enabled === true,
mode: body.mode,
botToken: body.botToken,
botUsername: body.botUsername,
dmPolicy: body.dmPolicy,
allowFrom: body.allowFrom,
groupPolicy: body.groupPolicy,
groups: body.groups,
requireMentionInGroups: body.requireMentionInGroups,
defaultProjectId: body.defaultProjectId,
groupProjectRoutes: body.groupProjectRoutes,
webhookSecret: body.webhookSecret,
webhookUrl: body.webhookUrl,
configuredBy: session.account,
});
let webhookSync:
| { ok: true; action: "set_webhook" | "delete_webhook" | "skipped"; reason?: string }
| undefined;
try {
webhookSync = await syncTelegramWebhookRegistration();
} catch (error) {
return jsonNoStore(
{
ok: false,
telegram,
message: error instanceof Error ? error.message : "TELEGRAM_WEBHOOK_SYNC_FAILED",
},
{ status: 400 },
);
}
if (!body.testConnection) {
return jsonNoStore({ ok: true, telegram, webhookSync });
}
try {
const probe = await probeTelegramBot();
if (probe.username && probe.username !== telegram.botUsername) {
telegram = await saveTelegramIntegrationConfig({
enabled: body.enabled === true,
mode: body.mode,
botToken: body.botToken,
botUsername: probe.username,
dmPolicy: body.dmPolicy,
allowFrom: body.allowFrom,
groupPolicy: body.groupPolicy,
groups: body.groups,
requireMentionInGroups: body.requireMentionInGroups,
defaultProjectId: body.defaultProjectId,
groupProjectRoutes: body.groupProjectRoutes,
webhookSecret: body.webhookSecret,
webhookUrl: body.webhookUrl,
configuredBy: session.account,
});
}
return jsonNoStore({ ok: true, telegram, probe, webhookSync });
} catch (error) {
return jsonNoStore(
{
ok: false,
telegram,
webhookSync,
message: error instanceof Error ? error.message : "TELEGRAM_PROBE_FAILED",
},
{ status: 400 },
);
}
}

View File

@@ -0,0 +1,6 @@
import { NextRequest } from "next/server";
import { handleTelegramWebhookRequest } from "@/lib/telegram-gateway";
export async function POST(request: NextRequest) {
return handleTelegramWebhookRequest({ request });
}

View File

@@ -1,7 +1,9 @@
import { NextRequest, NextResponse } from "next/server";
import { authorizeDeviceWriteRequest } from "@/lib/boss-device-auth";
import type { ExecutionProgressInput } from "@/lib/boss-data";
import { completeMasterAgentTask } from "@/lib/boss-data";
import { normalizeRemoteExecutionResult } from "@/lib/execution/remote-runtime-adapter";
import { deliverTelegramReplyForCompletedTask } from "@/lib/telegram-gateway";
export async function POST(
request: NextRequest,
@@ -9,14 +11,25 @@ export async function POST(
) {
const body = (await request.json().catch(() => ({}))) as {
deviceId?: string;
status?: "completed" | "failed";
status?: "completed" | "failed" | "needs_user_action";
kind?: string;
replyBody?: string;
errorMessage?: string;
requestId?: string;
dialogId?: string;
appName?: string;
platform?: string;
risk?: "low" | "medium" | "high";
summary?: string;
recommendedAction?: "allow_once" | "allow_for_device_dialog" | "deny" | "handled_on_device" | "cancel_task";
availableActions?: Array<"allow_once" | "allow_for_device_dialog" | "deny" | "handled_on_device" | "cancel_task">;
dispatchExecutionId?: string;
targetProjectId?: string;
targetThreadId?: string;
targetUrl?: string;
targetApp?: string;
rawThreadReply?: string;
executionProgress?: ExecutionProgressInput;
};
if (!body.deviceId?.trim()) {
@@ -31,6 +44,24 @@ export async function POST(
const { taskId } = await context.params;
try {
if (body.status === "needs_user_action") {
const task = await completeMasterAgentTask({
taskId,
deviceId: body.deviceId.trim(),
status: "needs_user_action",
kind: body.kind,
requestId: body.requestId,
dialogId: body.dialogId,
appName: body.appName,
platform: body.platform,
risk: body.risk,
summary: body.summary,
recommendedAction: body.recommendedAction,
availableActions: Array.isArray(body.availableActions) ? body.availableActions : undefined,
});
return NextResponse.json({ ok: true, task });
}
const normalized = normalizeRemoteExecutionResult({
...body,
status: body.status === "failed" ? "failed" : "completed",
@@ -45,8 +76,12 @@ export async function POST(
dispatchExecutionId: normalized.dispatchExecutionId,
targetProjectId: normalized.targetProjectId,
targetThreadId: normalized.targetThreadId,
targetUrl: normalized.targetUrl,
targetApp: normalized.targetApp,
rawThreadReply: normalized.rawThreadReply,
executionProgress: normalized.executionProgress,
});
await deliverTelegramReplyForCompletedTask(task.taskId).catch(() => null);
return NextResponse.json({ ok: true, task });
} catch (error) {
return NextResponse.json(

View File

@@ -4,22 +4,30 @@ import {
appendProjectMessage,
appendProjectMessages,
buildCollaborationGate,
deleteProjectMessage,
getProjectAgentControls,
hasProjectSummarySyncNotifyPreferenceInState,
rememberProjectSummarySyncNotifyPreference,
readState,
requestProjectUnderstandingSyncForProject,
updateProjectAgentControls,
} from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
import { buildProjectMessagesRealtimePayload } from "@/lib/boss-projections";
import { buildProjectMessagesRealtimePayloadForSession } from "@/lib/boss-projections";
import { canAccessProject } from "@/lib/boss-permissions";
import {
buildMasterAgentProjectSummarySyncAck,
getThreadConversationExecutionConflict,
queueGroupDispatchPlan,
queueThreadConversationReplyTask,
replyToMasterAgentUserMessage,
resolveMasterAgentProjectSummarySyncTarget,
shouldRecommendMasterAgentDispatchPlan,
ThreadConversationExecutionConflictError,
tryBuildLocalMasterAgentFastReply,
} from "@/lib/boss-master-agent";
import { evaluatePermissionPolicy } from "@/lib/execution/permission-policy";
import type { Message } from "@/lib/boss-data";
function dispatchFailureNotice(error?: string) {
switch (error) {
@@ -45,6 +53,10 @@ function threadConversationFailureMessage(error?: string) {
}
}
function forbiddenResponse(message = "FORBIDDEN") {
return NextResponse.json({ ok: false, message }, { status: 403 });
}
export async function GET(
request: NextRequest,
context: { params: Promise<{ projectId: string }> },
@@ -56,7 +68,14 @@ export async function GET(
const { projectId } = await context.params;
const state = await readState();
const payload = buildProjectMessagesRealtimePayload(state, projectId);
const projectExists = state.projects.some((project) => project.id === projectId);
if (!canAccessProject(state, session, projectId, "project.view")) {
return jsonNoStore(
{ ok: false, message: projectExists ? "FORBIDDEN" : "PROJECT_NOT_FOUND" },
{ status: projectExists ? 403 : 404 },
);
}
const payload = buildProjectMessagesRealtimePayloadForSession(state, session, projectId);
if (!payload) {
return jsonNoStore({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
@@ -81,13 +100,38 @@ export async function POST(
try {
const state = await readState();
const project = state.projects.find((item) => item.id === projectId);
if (!project) {
return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
}
const requestKind = body.kind ?? "text";
const requestText = (body.body ?? "").trim();
const masterAgentMention =
project && projectId !== "master-agent" && requestKind === "text"
? parseMasterAgentMention(requestText)
: null;
if (!canAccessProject(state, session, projectId, "project.view")) {
return forbiddenResponse();
}
if (masterAgentMention || projectId === "master-agent") {
if (!canAccessProject(state, session, projectId, "master_agent.ask")) {
return forbiddenResponse("MASTER_AGENT_FORBIDDEN");
}
} else if (!canAccessProject(state, session, projectId, "thread.chat")) {
return forbiddenResponse("THREAD_CHAT_FORBIDDEN");
}
const masterAgentProjectSummarySyncTarget =
projectId === "master-agent" && requestKind === "text"
? resolveMasterAgentProjectSummarySyncTarget(state, requestText)
: null;
const shouldCreateDispatchPlan =
!masterAgentMention &&
!masterAgentProjectSummarySyncTarget &&
Boolean(project) &&
((project?.isGroup && project.id !== "master-agent") ||
(project?.id === "master-agent" &&
shouldRecommendMasterAgentDispatchPlan(state, (body.body ?? "").trim()))) &&
(body.kind ?? "text") === "text" &&
(body.body ?? "").trim().length > 0;
shouldRecommendMasterAgentDispatchPlan(state, requestText))) &&
requestKind === "text" &&
requestText.length > 0;
const pendingPlan = shouldCreateDispatchPlan
? [...state.dispatchPlans]
@@ -101,7 +145,7 @@ export async function POST(
hasPendingDispatchPlan: Boolean(pendingPlan),
});
if (!permission.allowed) {
if (!permission.allowed && !masterAgentMention) {
return NextResponse.json(
{
ok: false,
@@ -114,11 +158,12 @@ export async function POST(
}
const isSingleThreadTextMessage =
!masterAgentMention &&
Boolean(project) &&
projectId !== "master-agent" &&
!project?.isGroup &&
(body.kind ?? "text") === "text" &&
(body.body ?? "").trim().length > 0;
requestKind === "text" &&
requestText.length > 0;
const singleThreadAgentControls = isSingleThreadTextMessage
? await getProjectAgentControls(projectId, session.account)
: null;
@@ -139,9 +184,198 @@ export async function POST(
);
}
if (projectId === "master-agent" && (body.kind ?? "text") === "text" && (body.body ?? "").trim()) {
if (masterAgentMention && project) {
const message = await appendProjectMessage({
projectId,
senderLabel: session.displayName || "你",
body: body.body,
kind: requestKind,
});
if (shouldDisableCurrentProjectTakeoverFromMasterMention(masterAgentMention.requestText)) {
await updateProjectAgentControls(projectId, { takeoverEnabled: false }, session.account);
const nextState = await readState();
const nextProject = nextState.projects.find((item) => item.id === projectId) ?? project;
const replyMessage = await appendProjectMessage({
projectId,
sender: "master",
senderLabel: "主 Agent",
body: buildMasterMentionTakeoverDisabledReply(nextProject),
kind: "text",
});
return NextResponse.json({
ok: true,
message,
replyMessage,
masterReply: {
ok: true,
masterReplyState: "completed",
replyMessage,
},
task: null,
replyPresenter: "master",
masterReplyState: "completed",
dispatchPlan: null,
dispatchRecommendation: {
ok: false,
status: "skipped",
},
collaborationGate: buildCollaborationGate(nextProject),
});
}
if (shouldEnableCurrentProjectTakeoverFromMasterMention(masterAgentMention.requestText)) {
await updateProjectAgentControls(projectId, { takeoverEnabled: true }, session.account);
const nextState = await readState();
const nextProject = nextState.projects.find((item) => item.id === projectId) ?? project;
const replyMessage = await appendProjectMessage({
projectId,
sender: "master",
senderLabel: "主 Agent",
body: buildMasterMentionTakeoverEnabledReply(nextProject),
kind: "text",
});
return NextResponse.json({
ok: true,
message,
replyMessage,
masterReply: {
ok: true,
masterReplyState: "completed",
replyMessage,
},
task: null,
replyPresenter: "master",
masterReplyState: "completed",
dispatchPlan: null,
dispatchRecommendation: {
ok: false,
status: "skipped",
},
collaborationGate: buildCollaborationGate(nextProject),
});
}
if (!project.isGroup && shouldRequestVerifiedProjectSummarySync(masterAgentMention.requestText)) {
const explicitNotify = shouldNotifyAfterProjectSummarySync(masterAgentMention.requestText);
const rememberPreference =
explicitNotify && shouldRememberProjectSummarySyncNotice(masterAgentMention.requestText);
const rememberedPreference =
rememberPreference || hasProjectSummarySyncNotifyPreferenceInState(state, session.account);
if (rememberPreference) {
await rememberProjectSummarySyncNotifyPreference({
account: session.account,
sourceMessageId: message.id,
});
}
const syncTask = await requestProjectUnderstandingSyncForProject({
projectId,
observedActivityAt: message.sentAt,
reason: "thread_reply",
replyProjectId: projectId,
notifyOnCompletion: explicitNotify || rememberedPreference,
requestedByAccount: session.account,
});
const replyMessage = await appendProjectMessage({
projectId,
sender: "master",
senderLabel: "主 Agent",
body: buildMasterAgentProjectSummarySyncAck(project, {
notifyOnCompletion: explicitNotify || rememberedPreference,
rememberedPreference: rememberPreference,
}),
kind: "system_notice",
});
return NextResponse.json({
ok: true,
message,
replyMessage,
masterReply: {
ok: true,
taskId: syncTask?.taskId,
masterReplyState: syncTask ? "queued" : "completed",
task: syncTask
? {
taskId: syncTask.taskId,
taskType: "conversation_reply" as const,
status: "queued" as const,
}
: undefined,
replyMessage,
},
task: syncTask
? {
taskId: syncTask.taskId,
taskType: "conversation_reply" as const,
status: "queued" as const,
}
: null,
replyPresenter: "master",
masterReplyState: syncTask ? "queued" : "completed",
dispatchPlan: null,
dispatchRecommendation: {
ok: false,
status: "skipped",
},
collaborationGate: buildCollaborationGate(project),
});
}
const masterReply = await replyToMasterAgentUserMessage({
requestMessageId: message.id,
requestText: masterAgentMention.requestText,
requestedBy: session.displayName || session.account,
requestedByAccount: session.account,
currentSessionExpiresAt: session.expiresAt,
projectId,
interactionMode: "direct",
mode: "smart",
});
const mentionMasterReply = masterReply as {
ok?: boolean;
taskId?: string;
masterReplyState?: "queued" | "running" | "completed";
task?: {
taskId: string;
taskType: "conversation_reply";
status: "queued" | "running" | "completed";
};
replyMessage?: Awaited<ReturnType<typeof appendProjectMessage>>;
};
const task = mentionMasterReply.taskId
? mentionMasterReply.task ?? {
taskId: mentionMasterReply.taskId,
taskType: "conversation_reply" as const,
status: mentionMasterReply.masterReplyState ?? "queued",
}
: null;
return NextResponse.json({
ok: true,
message,
replyMessage: mentionMasterReply.replyMessage,
masterReply,
task,
replyPresenter: "master",
masterReplyState: mentionMasterReply.masterReplyState ?? (mentionMasterReply.ok ? "completed" : null),
executionMode: (mentionMasterReply as { executionMode?: "discussion" | "thread" | "development" | "browser" | "desktop" }).executionMode,
riskLevel: (mentionMasterReply as { riskLevel?: "low" | "medium" | "high" }).riskLevel,
requiresConfirmation: (mentionMasterReply as { requiresConfirmation?: boolean }).requiresConfirmation,
dispatchPlan: null,
dispatchRecommendation: {
ok: false,
status: "skipped",
},
collaborationGate: buildCollaborationGate(project),
});
}
if (projectId === "master-agent" && requestKind === "text" && requestText) {
const localMasterReply = await tryBuildLocalMasterAgentFastReply({
requestText: (body.body ?? "").trim(),
requestText,
requestedByAccount: session.account,
projectId,
state,
@@ -153,7 +387,7 @@ export async function POST(
{
senderLabel: session.displayName || "你",
body: body.body,
kind: body.kind ?? "text",
kind: requestKind,
},
{
sender: "master",
@@ -186,7 +420,7 @@ export async function POST(
projectId,
senderLabel: session.displayName || "你",
body: body.body,
kind: body.kind ?? "text",
kind: requestKind,
});
let dispatchPlan = null;
let dispatchRecommendation:
@@ -208,17 +442,20 @@ export async function POST(
masterReplyState?: "queued" | "running" | "completed";
task?: {
taskId: string;
taskType: "conversation_reply";
taskType: "conversation_reply" | "browser_control" | "desktop_control";
status: "queued" | "running" | "completed";
};
replyMessage?: Awaited<ReturnType<typeof appendProjectMessage>>;
replyMessage?: Message;
executionMode?: "discussion" | "thread" | "development" | "browser" | "desktop";
riskLevel?: "low" | "medium" | "high";
requiresConfirmation?: boolean;
}
| undefined;
let replyMessage: Awaited<ReturnType<typeof appendProjectMessage>> | undefined;
let task:
| {
taskId: string;
taskType: "conversation_reply";
taskType: "conversation_reply" | "browser_control" | "desktop_control";
status: "queued" | "running" | "completed";
}
| null = null;
@@ -228,8 +465,72 @@ export async function POST(
| "completed"
| null = null;
let replyPresenter: "thread" | "master" | undefined;
let executionMode:
| "discussion"
| "thread"
| "development"
| "browser"
| "desktop"
| undefined;
let riskLevel: "low" | "medium" | "high" | undefined;
let requiresConfirmation: boolean | undefined;
if (shouldCreateDispatchPlan) {
if (masterAgentProjectSummarySyncTarget && message.body.trim().length > 0) {
const explicitNotify = shouldNotifyAfterProjectSummarySync(message.body);
const rememberPreference = explicitNotify && shouldRememberProjectSummarySyncNotice(message.body);
const rememberedPreference =
rememberPreference || hasProjectSummarySyncNotifyPreferenceInState(state, session.account);
if (rememberPreference) {
await rememberProjectSummarySyncNotifyPreference({
account: session.account,
sourceMessageId: message.id,
});
}
const syncTask = await requestProjectUnderstandingSyncForProject({
projectId: masterAgentProjectSummarySyncTarget.id,
observedActivityAt: message.sentAt,
reason: "thread_reply",
replyProjectId: projectId,
notifyOnCompletion: explicitNotify || rememberedPreference,
requestedByAccount: session.account,
});
replyMessage = await appendProjectMessage({
projectId,
sender: "master",
senderLabel: "主 Agent",
body: buildMasterAgentProjectSummarySyncAck(masterAgentProjectSummarySyncTarget, {
notifyOnCompletion: explicitNotify || rememberedPreference,
rememberedPreference: rememberPreference,
}),
kind: "system_notice",
});
masterReply = {
ok: true,
taskId: syncTask?.taskId,
masterReplyState: syncTask ? "queued" : "completed",
task: syncTask
? {
taskId: syncTask.taskId,
taskType: "conversation_reply",
status: "queued",
}
: undefined,
replyMessage,
};
task = syncTask
? {
taskId: syncTask.taskId,
taskType: "conversation_reply",
status: "queued",
}
: null;
masterReplyState = syncTask ? "queued" : "completed";
replyPresenter = "master";
dispatchRecommendation = {
ok: false,
status: "skipped",
};
} else if (shouldCreateDispatchPlan) {
try {
const recommendation = await queueGroupDispatchPlan({
groupProjectId: projectId,
@@ -265,40 +566,140 @@ export async function POST(
} else if (project && projectId !== "master-agent" && !project.isGroup && message.body.trim().length > 0) {
const relayViaMasterAgent = singleThreadTakeoverEnabled;
if (relayViaMasterAgent) {
if (shouldRequestVerifiedProjectSummarySync(message.body)) {
await requestProjectUnderstandingSyncForProject({
if (shouldDisableCurrentProjectTakeoverFromMasterMention(message.body)) {
await updateProjectAgentControls(projectId, { takeoverEnabled: false }, session.account);
const nextState = await readState();
const nextProject = nextState.projects.find((item) => item.id === projectId) ?? project;
replyMessage = await appendProjectMessage({
projectId,
sender: "master",
senderLabel: "主 Agent",
body: buildMasterMentionTakeoverDisabledReply(nextProject),
kind: "text",
});
masterReply = {
ok: true,
masterReplyState: "completed",
replyMessage,
};
task = null;
masterReplyState = "completed";
} else if (shouldRequestVerifiedProjectSummarySync(message.body)) {
const explicitNotify = shouldNotifyAfterProjectSummarySync(message.body);
const rememberPreference = explicitNotify && shouldRememberProjectSummarySyncNotice(message.body);
const rememberedPreference =
rememberPreference || hasProjectSummarySyncNotifyPreferenceInState(state, session.account);
if (rememberPreference) {
await rememberProjectSummarySyncNotifyPreference({
account: session.account,
sourceMessageId: message.id,
});
}
const syncTask = await requestProjectUnderstandingSyncForProject({
projectId,
observedActivityAt: message.sentAt,
reason: "thread_reply",
});
}
masterReply = await replyToMasterAgentUserMessage({
requestMessageId: message.id,
requestText: message.body,
requestedBy: session.displayName || session.account,
replyProjectId: projectId,
notifyOnCompletion: explicitNotify || rememberedPreference,
requestedByAccount: session.account,
currentSessionExpiresAt: session.expiresAt,
});
replyMessage = await appendProjectMessage({
projectId,
interactionMode: "takeover_single_thread",
mode: "enqueue",
})
if (masterReply?.taskId) {
task = masterReply.task ?? {
taskId: masterReply.taskId,
taskType: "conversation_reply",
status: masterReply.masterReplyState ?? "queued",
sender: "master",
senderLabel: "主 Agent",
body: buildMasterAgentProjectSummarySyncAck(project, {
notifyOnCompletion: explicitNotify || rememberedPreference,
rememberedPreference: rememberPreference,
}),
kind: "system_notice",
});
masterReply = {
ok: true,
taskId: syncTask?.taskId,
masterReplyState: syncTask ? "queued" : "completed",
task: syncTask
? {
taskId: syncTask.taskId,
taskType: "conversation_reply",
status: "queued" as const,
}
: undefined,
replyMessage,
};
masterReplyState = masterReply.masterReplyState ?? null;
task = syncTask
? {
taskId: syncTask.taskId,
taskType: "conversation_reply",
status: "queued",
}
: null;
masterReplyState = syncTask ? "queued" : "completed";
} else {
try {
const queuedTask = await queueThreadConversationReplyTask({
projectId,
requestMessageId: message.id,
requestText: message.body,
sourceMessageId: message.id,
sourceMessageBody: message.body,
sourceMessageSentAt: message.sentAt,
requestedBy: session.displayName || session.account,
requestedByAccount: session.account,
relayViaMasterAgent: true,
});
task = {
taskId: queuedTask.taskId,
taskType: "conversation_reply",
status: "queued",
};
masterReply = {
ok: true,
taskId: queuedTask.taskId,
masterReplyState: "queued",
task,
};
masterReplyState = "queued";
} catch (error) {
if (!(error instanceof ThreadConversationExecutionConflictError)) {
throw error;
}
masterReply = await replyToMasterAgentUserMessage({
requestMessageId: message.id,
requestText: message.body,
requestedBy: session.displayName || session.account,
requestedByAccount: session.account,
currentSessionExpiresAt: session.expiresAt,
projectId,
interactionMode: "takeover_single_thread",
mode: "enqueue",
});
if (masterReply?.taskId) {
task = masterReply.task ?? {
taskId: masterReply.taskId,
taskType: "conversation_reply",
status: masterReply.masterReplyState ?? "queued",
};
masterReplyState = masterReply.masterReplyState ?? null;
}
replyMessage = masterReply?.replyMessage;
executionMode = (masterReply as { executionMode?: typeof executionMode }).executionMode;
riskLevel = (masterReply as { riskLevel?: typeof riskLevel }).riskLevel;
requiresConfirmation = (
masterReply as { requiresConfirmation?: typeof requiresConfirmation }
).requiresConfirmation;
}
}
replyMessage = masterReply?.replyMessage;
} else {
const queuedTask = await queueThreadConversationReplyTask({
projectId,
requestMessageId: message.id,
requestText: message.body,
requestedBy: session.displayName || session.account,
requestedByAccount: session.account,
});
projectId,
requestMessageId: message.id,
requestText: message.body,
sourceMessageId: message.id,
sourceMessageBody: message.body,
sourceMessageSentAt: message.sentAt,
requestedBy: session.displayName || session.account,
requestedByAccount: session.account,
});
task = {
taskId: queuedTask.taskId,
taskType: "conversation_reply",
@@ -313,7 +714,12 @@ export async function POST(
};
}
if (projectId === "master-agent" && (body.kind ?? "text") === "text" && message.body.trim()) {
if (
projectId === "master-agent" &&
!masterAgentProjectSummarySyncTarget &&
(body.kind ?? "text") === "text" &&
message.body.trim()
) {
masterReply = await replyToMasterAgentUserMessage({
requestMessageId: message.id,
requestText: message.body,
@@ -333,6 +739,11 @@ export async function POST(
masterReplyState = masterReply.masterReplyState ?? (masterReply.taskId ? null : "completed");
replyPresenter = "master";
replyMessage = masterReply.replyMessage;
executionMode = (masterReply as { executionMode?: typeof executionMode }).executionMode;
riskLevel = (masterReply as { riskLevel?: typeof riskLevel }).riskLevel;
requiresConfirmation = (
masterReply as { requiresConfirmation?: typeof requiresConfirmation }
).requiresConfirmation;
} else {
masterReplyState = null;
}
@@ -350,6 +761,9 @@ export async function POST(
task,
replyPresenter,
masterReplyState,
executionMode,
riskLevel,
requiresConfirmation,
dispatchPlan,
dispatchRecommendation,
collaborationGate,
@@ -378,6 +792,49 @@ export async function POST(
}
}
export async function DELETE(
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 messageIdFromQuery = request.nextUrl.searchParams.get("messageId")?.trim();
const body = messageIdFromQuery
? {}
: ((await request.json().catch(() => ({}))) as { messageId?: string });
const messageId = messageIdFromQuery || body.messageId?.trim();
if (!messageId) {
return NextResponse.json({ ok: false, message: "MESSAGE_ID_REQUIRED" }, { status: 400 });
}
try {
const state = await readState();
const projectExists = state.projects.some((project) => project.id === projectId);
if (!canAccessProject(state, session, projectId, "thread.chat")) {
return NextResponse.json(
{ ok: false, message: projectExists ? "THREAD_CHAT_FORBIDDEN" : "PROJECT_NOT_FOUND" },
{ status: projectExists ? 403 : 404 },
);
}
const result = await deleteProjectMessage({ projectId, messageId });
return NextResponse.json({
ok: true,
deletedMessage: result.deletedMessage,
remainingCount: result.remainingCount,
});
} catch (error) {
const message = error instanceof Error ? error.message : "UNKNOWN_ERROR";
return NextResponse.json(
{ ok: false, message },
{ status: message === "PROJECT_NOT_FOUND" || message === "MESSAGE_NOT_FOUND" ? 404 : 400 },
);
}
}
function shouldRequestVerifiedProjectSummarySync(text: string) {
const normalized = text.trim();
if (!normalized) {
@@ -385,6 +842,110 @@ function shouldRequestVerifiedProjectSummarySync(text: string) {
}
const mentionsGoal = /项目目标|目标/.test(normalized);
const mentionsVersion = /版本记录|版本迭代|版本/.test(normalized);
const mentionsReviewOrSync = /核对|确认|同步|更新|刷新|整理|汇总/.test(normalized);
const mentionsReviewOrSync = /核对|确认|同步|更新|刷新|整理|汇总|总结|梳理|概括|回写/.test(normalized);
return mentionsReviewOrSync && (mentionsGoal || mentionsVersion);
}
function shouldNotifyAfterProjectSummarySync(text: string) {
const normalized = text.trim();
if (!normalized) {
return false;
}
return /同步完成.*(告诉我|提醒我|回我|和我说|通知我)|完成后.*(告诉我|提醒我|回我|和我说|通知我)|记得和我说|记得告诉我|同步完.*说一声/.test(
normalized,
);
}
function shouldRememberProjectSummarySyncNotice(text: string) {
const normalized = text.trim();
if (!normalized) {
return false;
}
return /以后|后续|默认|都这样|也这样|记住|下次也这样/.test(normalized);
}
const INLINE_MASTER_AGENT_MENTION_REGEX =
/@+\s*(主\s*a(?:[\s"'`._-]*)g(?:[\s"'`._-]*)e(?:[\s"'`._-]*)n(?:[\s"'`._-]*)t|主\s*gpt|主控|master\s*agent|agent)(?=$|[\s:,,、。.!?;])/i;
function parseMasterAgentMention(text: string) {
const normalized = text.trim();
if (!normalized) {
return null;
}
const inlineMentionMatch = normalized.match(INLINE_MASTER_AGENT_MENTION_REGEX);
if (inlineMentionMatch) {
const requestText = normalized
.replace(INLINE_MASTER_AGENT_MENTION_REGEX, " ")
.replace(/\s+/g, " ")
.replace(/^[\s:,,、-]+/, "")
.trim();
return {
requestText: requestText || "你好",
};
}
if (!normalized.startsWith("@")) {
return null;
}
const stripped = normalized.slice(1).trim();
if (!stripped) {
return { requestText: "你好" };
}
const labelMatch = stripped.match(
/^(主\s*a(?:[\s"'`._-]*)g(?:[\s"'`._-]*)e(?:[\s"'`._-]*)n(?:[\s"'`._-]*)t|主\s*gpt|主控|master\s*agent|agent)/i,
);
const requestText = labelMatch
? stripped.slice(labelMatch[0].length).replace(/^[\s:,,、-]+/, "").trim()
: stripped;
return {
requestText: requestText || "你好",
};
}
function shouldEnableCurrentProjectTakeoverFromMasterMention(text: string) {
const normalized = text.trim().replace(/\s+/g, "");
if (!normalized) {
return false;
}
if (!/(托管|接管|协同接管)/.test(normalized)) {
return false;
}
if (/(关闭|取消|不要|别|停止|禁用|撤销|解除)/.test(normalized)) {
return false;
}
return /(开启|打开|启用|开始|进入|切到|切换到|交给|接手|让.+(托管|接管)|请.+(托管|接管)|帮我.+(托管|接管))/.test(
normalized,
);
}
function shouldDisableCurrentProjectTakeoverFromMasterMention(text: string) {
const normalized = text.trim().replace(/\s+/g, "");
if (!normalized) {
return false;
}
if (!/(托管|接管|协同接管)/.test(normalized)) {
return false;
}
if (!/(关闭|取消|退出|停止|禁用|撤销|解除)/.test(normalized)) {
return false;
}
return /(当前|本线程|这个线程|当前线程|接管模式|托管模式)/.test(normalized) || normalized.includes("退出当前的接管模式");
}
function buildMasterMentionTakeoverEnabledReply(project: { name?: string | null }) {
const projectName = project.name?.trim() || "当前会话";
return [
`已为《${projectName}》开启主 Agent 协同接管。`,
"后续你直接发普通消息时,我会先确认意图,再协调对应线程推进;如果只是单独和我说话,继续用 @ 开头即可。",
].join("\n");
}
function buildMasterMentionTakeoverDisabledReply(project: { name?: string | null }) {
const projectName = project.name?.trim() || "当前会话";
return [
`已为《${projectName}》关闭主 Agent 协同接管。`,
"后续这个线程会恢复为你直接和对应 Codex 线程对话;如果还想单独找我,继续用 @ 开头即可。",
].join("\n");
}

View File

@@ -1,6 +1,6 @@
import { NextRequest } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { getProjectDetailView } from "@/lib/boss-projections";
import { getProjectDetailViewForSession } from "@/lib/boss-projections";
import { readState } from "@/lib/boss-data";
import { jsonNoStore } from "@/lib/api-response";
@@ -14,10 +14,14 @@ export async function GET(
}
const { projectId } = await context.params;
const state = await readState();
const detail = getProjectDetailView(state, projectId, session.account);
const projectExists = state.projects.some((project) => project.id === projectId);
const detail = getProjectDetailViewForSession(state, projectId, session);
if (!detail) {
return jsonNoStore({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 });
return jsonNoStore(
{ ok: false, message: projectExists ? "FORBIDDEN" : "PROJECT_NOT_FOUND" },
{ status: projectExists ? 403 : 404 },
);
}
return jsonNoStore({