feat: org capacity policy admin surface

ADR-0022 / Spec.System.Capacity pins layered capacity limits: a platform
ceiling per dimension is unbreakable, and each organization may only set a
lower `organizationLimit`. The effective limit is the minimum of the two
(`LayeredLimit.effective`); dimensions with no org override fall back to
the platform ceiling. No dimension may be unlimited (a ceiling must exist
before an org limit can be set, `LayeredLimit.Valid`).

Add the backend: the pinned 23 CapacityDimension set + labels
(src/capacity/dimensions.ts), platform ceilings sourced from existing
runtime env vars plus `HUB_CEILING_<DIMENSION>` (src/capacity/ceilings.ts),
the OrganizationCapacityPolicy prisma model + migration, the
getCapacityPolicy/setCapacityPolicy service enforcing LayeredLimit.Valid,
and org-admin GET/PUT /api/org/:orgSlug/capacity-policy routes wired into
the org route tree.

Add the admin-web surface: CapacityDimension/CapacityPolicyView api client
types, capacityPolicy/setCapacityPolicy methods, a `容量` nav entry, and a
capacity page that lists every dimension with its platform ceiling (or
`未配置` when unset), an org-limit input (disabled until a ceiling exists),
and a live effective-value column. Saving sends the partial limits map;
the service rejects values above the ceiling or for unconfigured dimensions.
This commit is contained in:
2026-07-14 21:23:41 +08:00
parent adce8fb6f5
commit ab9dfad53a
10 changed files with 545 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
/**
* Org-admin capacity policy routes (ADR-0022).
*/
import type { PrismaClient } from "@prisma/client";
import type { FastifyInstance } from "fastify";
import { getCapacityPolicy, setCapacityPolicy } from "../../org/capacityPolicy.js";
import { isCapacityDimension } from "../../capacity/dimensions.js";
import { requireOrgRole, type GuardDeps } from "../auth/guards.js";
import { handleRouteError } from "../errors.js";
export async function registerCapacityRoutes(
app: FastifyInstance,
config: { readonly prisma: PrismaClient; readonly sessionSecret: string },
): Promise<void> {
const guardDeps: GuardDeps = { prisma: config.prisma, sessionSecret: config.sessionSecret };
app.get("/api/org/:orgSlug/capacity-policy", async (request, reply) => {
try {
const { orgSlug } = request.params as { orgSlug: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
return await getCapacityPolicy(config.prisma, auth.organization.id);
} catch (err) {
return handleRouteError(reply, err);
}
});
app.put("/api/org/:orgSlug/capacity-policy", async (request, reply) => {
try {
const { orgSlug } = request.params as { orgSlug: string };
const auth = await requireOrgRole(request, reply, guardDeps, { orgSlug });
if (auth === null) return;
const body = request.body as { limits?: unknown };
if (body?.limits === null || typeof body.limits !== "object") {
return reply.status(400).send({
error: { code: "bad_request", message: "limits object is required" },
});
}
const limits: Record<string, number | null> = {};
for (const [key, value] of Object.entries(body.limits as Record<string, unknown>)) {
if (!isCapacityDimension(key)) {
return reply.status(400).send({
error: { code: "bad_request", message: `unknown capacity dimension: ${key}` },
});
}
if (value === null) {
limits[key] = null;
} else if (typeof value === "number" && Number.isFinite(value)) {
limits[key] = value;
} else {
return reply.status(400).send({
error: { code: "bad_request", message: `limit for ${key} must be a number or null` },
});
}
}
return await setCapacityPolicy(config.prisma, {
organizationId: auth.organization.id,
limits,
});
} catch (err) {
return handleRouteError(reply, err);
}
});
}
+5
View File
@@ -15,6 +15,7 @@ import { registerMembersRoutes } from "./membersRoutes.js";
import { registerSessionsAndUsageRoutes } from "./sessionsRoutes.js";
import { registerTeamsRoutes } from "./teamsRoutes.js";
import { registerProviderConnectionRoutes } from "./providerConnectionRoutes.js";
import { registerCapacityRoutes } from "./capacityRoutes.js";
import type { LocalSecretEnvelope } from "../../security/secretEnvelope.js";
import type { ProviderReadinessProbe } from "../../connections/providerReadiness.js";
import type { FeishuReadinessProbe } from "../../connections/feishuReadiness.js";
@@ -105,6 +106,10 @@ export async function registerOrgRoutes(app: FastifyInstance, config: OrgRouteCo
prisma: config.prisma,
sessionSecret: config.sessionSecret,
});
await registerCapacityRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
});
await registerSessionsAndUsageRoutes(app, {
prisma: config.prisma,
sessionSecret: config.sessionSecret,
+78
View File
@@ -0,0 +1,78 @@
/**
* ADR-0022 platform ceilings (spec `LayeredLimit.platformCeiling`).
*
* Platform ceilings are the unbreakable upper bounds; org policy may only
* configure lower limits (see `LayeredLimit.Valid`). Exact numeric values are
* `OPEN` per spec and calibrated by capacity testing — this module is the
* single place to wire them.
*
* Seven dimensions are sourced from existing runtime env vars
* (`HUB_HTTP_BODY_LIMIT_BYTES`, `HUB_MAX_FILES_PER_MESSAGE`, `HUB_MAX_FILE_BYTES`,
* `HUB_HTTP_REQUESTS_PER_MINUTE`, `HUB_AGENT_MAX_CONCURRENT_RUNS`,
* `HUB_AGENT_MAX_RUN_SECONDS`, `HUB_AGENT_MAX_TURNS`). The remaining dimensions
* are sourced from `HUB_CEILING_<DIMENSION>` env vars; until those are set the
* dimension has no configured ceiling and the policy UI surfaces it as
* "未配置" (cannot enforce a limit without a ceiling).
*/
import type { CapacityDimension } from "./dimensions.js";
function positiveIntEnv(name: string): number | undefined {
const raw = process.env[name];
if (raw === undefined || raw === "") return undefined;
const n = Number.parseInt(raw, 10);
return Number.isFinite(n) && n > 0 ? n : undefined;
}
const ENV_BACKED_CEILINGS: Partial<Record<CapacityDimension, string>> = {
requestBodySize: "HUB_HTTP_BODY_LIMIT_BYTES",
attachmentCount: "HUB_MAX_FILES_PER_MESSAGE",
fileSize: "HUB_MAX_FILE_BYTES",
requestRate: "HUB_HTTP_REQUESTS_PER_MINUTE",
agentConcurrency: "HUB_AGENT_MAX_CONCURRENT_RUNS",
runWallTime: "HUB_AGENT_MAX_RUN_SECONDS",
runTurns: "HUB_AGENT_MAX_TURNS",
};
const CEILING_DIMENSIONS: readonly CapacityDimension[] = [
"requestRate",
"requestBodySize",
"agentConcurrency",
"admissionQueueLength",
"admissionQueueWait",
"fileSize",
"attachmentCount",
"archiveExpansion",
"projectStorage",
"organizationStorage",
"memberCount",
"projectCount",
"teamCount",
"folderCount",
"sessionCount",
"runWallTime",
"runTurns",
"runToolCalls",
"toolWallTime",
"runOutputSize",
"processMemory",
"processCpu",
"processCount",
];
function envNameFor(dimension: CapacityDimension): string {
return ENV_BACKED_CEILINGS[dimension] ?? `HUB_CEILING_${dimension.toUpperCase()}`;
}
/**
* Returns the configured platform ceilings (only the dimensions currently
* wired via env). Unconfigured dimensions are absent — callers must surface
* them, not assume unlimited (spec `LayeredLimit` forbids unlimited ceilings).
*/
export function readPlatformCeilings(): Partial<Record<CapacityDimension, number>> {
const ceilings: Partial<Record<CapacityDimension, number>> = {};
for (const dimension of CEILING_DIMENSIONS) {
const value = positiveIntEnv(envNameFor(dimension));
if (value !== undefined) ceilings[dimension] = value;
}
return ceilings;
}
+63
View File
@@ -0,0 +1,63 @@
/**
* ADR-0022 capacity dimensions (spec `Spec.System.Capacity.CapacityDimension`).
* The 23 PINNED dimensions; exact numeric ceilings are `OPEN` and calibrated by
* capacity testing. This module is the single source of the dimension set shared
* by the platform-ceiling config and the org capacity-policy service.
*/
export const CAPACITY_DIMENSIONS = [
"requestRate",
"requestBodySize",
"agentConcurrency",
"admissionQueueLength",
"admissionQueueWait",
"fileSize",
"attachmentCount",
"archiveExpansion",
"projectStorage",
"organizationStorage",
"memberCount",
"projectCount",
"teamCount",
"folderCount",
"sessionCount",
"runWallTime",
"runTurns",
"runToolCalls",
"toolWallTime",
"runOutputSize",
"processMemory",
"processCpu",
"processCount",
] as const;
export type CapacityDimension = (typeof CAPACITY_DIMENSIONS)[number];
export const CAPACITY_DIMENSION_LABELS: Record<CapacityDimension, string> = {
requestRate: "HTTP 请求速率",
requestBodySize: "请求体大小",
agentConcurrency: "智能体并发",
admissionQueueLength: "接纳队列长度",
admissionQueueWait: "接纳队列等待",
fileSize: "单文件大小",
attachmentCount: "每消息附件数",
archiveExpansion: "归档展开",
projectStorage: "项目存储",
organizationStorage: "组织存储",
memberCount: "成员数",
projectCount: "项目数",
teamCount: "团队数",
folderCount: "文件夹数",
sessionCount: "会话数",
runWallTime: "单次运行墙钟",
runTurns: "单次运行轮次",
runToolCalls: "单次运行工具调用",
toolWallTime: "工具墙钟",
runOutputSize: "运行输出大小",
processMemory: "进程内存",
processCpu: "进程 CPU",
processCount: "进程数",
};
export function isCapacityDimension(value: string): value is CapacityDimension {
return (CAPACITY_DIMENSIONS as readonly string[]).includes(value);
}
+102
View File
@@ -0,0 +1,102 @@
/**
* Org capacity policy service (ADR-0022 / spec `Spec.System.Capacity`).
*
* Stores per-Organization lower `organizationLimit` overrides per
* `CapacityDimension`. Enforces `LayeredLimit.Valid`: a set limit must be ≤ the
* platform ceiling for that dimension. `LayeredLimit.effective` (min of the two)
* is the value capacity admission should use; dimensions with no org override
* fall back to the platform ceiling.
*/
import type { PrismaClient } from "@prisma/client";
import { CAPACITY_DIMENSIONS, isCapacityDimension, type CapacityDimension } from "../capacity/dimensions.js";
import { readPlatformCeilings } from "../capacity/ceilings.js";
import { lockActiveOrganization } from "./status.js";
export interface CapacityPolicyView {
/** Per-dimension: platform ceiling (absent if not configured) + org limit. */
readonly dimensions: ReadonlyArray<{
readonly dimension: CapacityDimension;
readonly platformCeiling: number | null;
readonly organizationLimit: number | null;
readonly effective: number | null;
}>;
}
export async function getCapacityPolicy(
prisma: PrismaClient,
organizationId: string,
): Promise<CapacityPolicyView> {
const row = await prisma.organizationCapacityPolicy.findUnique({
where: { organizationId },
select: { limits: true },
});
const orgLimits = parseLimits(row?.limits);
const ceilings = readPlatformCeilings();
const dimensions = CAPACITY_DIMENSIONS.map((dimension) => {
const platformCeiling = ceilings[dimension] ?? null;
const organizationLimit = orgLimits[dimension] ?? null;
const effective = effectiveLimit(platformCeiling, organizationLimit);
return { dimension, platformCeiling, organizationLimit, effective };
});
return { dimensions };
}
export async function setCapacityPolicy(
prisma: PrismaClient,
input: {
readonly organizationId: string;
/** Partial map of dimension → limit. null/absent clears a dimension. */
readonly limits: Partial<Record<CapacityDimension, number | null>>;
},
): Promise<CapacityPolicyView> {
const ceilings = readPlatformCeilings();
const cleaned: Record<string, number> = {};
for (const [dimension, value] of Object.entries(input.limits)) {
if (!isCapacityDimension(dimension)) {
throw new Error(`unknown capacity dimension: ${dimension}`);
}
if (value === null || value === undefined) continue;
if (!Number.isFinite(value) || value <= 0 || !Number.isInteger(value)) {
throw new Error(`limit for ${dimension} must be a positive integer`);
}
const ceiling = ceilings[dimension];
if (ceiling === undefined) {
throw new Error(
`platform ceiling for ${dimension} is not configured; cannot set an organization limit (spec LayeredLimit.Valid)`,
);
}
if (value > ceiling) {
throw new Error(
`organization limit ${value} for ${dimension} exceeds platform ceiling ${ceiling} (spec LayeredLimit.Valid)`,
);
}
cleaned[dimension] = value;
}
await prisma.$transaction(async (tx) => {
await lockActiveOrganization(tx, input.organizationId);
await tx.organizationCapacityPolicy.upsert({
where: { organizationId: input.organizationId },
create: { organizationId: input.organizationId, limits: cleaned },
update: { limits: cleaned },
});
});
return getCapacityPolicy(prisma, input.organizationId);
}
function effectiveLimit(platformCeiling: number | null, organizationLimit: number | null): number | null {
if (platformCeiling === null) return null;
if (organizationLimit === null) return platformCeiling;
return Math.min(platformCeiling, organizationLimit);
}
function parseLimits(raw: unknown): Partial<Record<CapacityDimension, number>> {
if (raw === null || typeof raw !== "object") return {};
const result: Partial<Record<CapacityDimension, number>> = {};
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
if (isCapacityDimension(key) && typeof value === "number" && Number.isFinite(value)) {
result[key] = value;
}
}
return result;
}