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