Archived
契约:三个端与路由归属(access 必填)、Principal / 角色→端 / 名册同步 schema、退役路由标 @deprecated(ADR-012)
- packages/contracts/src/api/http.ts:新增 SURFACES / surfaceSchema / Surface / SURFACE_LABEL / RouteAccess;RouteContract.access 必填;API_ERROR_CODES 增 'gone'。 - packages/contracts/src/org.ts:staffSchema 补 email? / roles(realm 角色快照);新增 principalSchema/Principal、roleAccessSchema/RoleAccess、rosterSyncOutcomeSchema/RosterSyncOutcome、 inFlightWorkSchema/InFlightWork;旧岗位授权(grant* / governanceFunction*)与 sessionSchema/Session 全部标 @deprecated 留一轮(web 编译不受影响,下个迭代删)。 - workbench.ts / governance.ts / ingest.ts:每条既有路由补 access(按所属端/两端共用/退役 public); 新增 meRoute、authTicketRoute(access:'any')与 syncStaffRoute、setStaffPositionRoute、 staffInFlightRoute、listRoleAccessRoute、setRoleAccessRoute、deleteRoleAccessRoute(access:'ops')。 - apps/api 的 org-service.ts 两处 staff.insert 补 roles: [](staffSchema 新必填字段,该文件下个 迭代会整份替换,这里只是让本次 typecheck 过)。 - 新增 packages/contracts/src/__tests__/route-access.test.ts:95 条路由的 access 归属断言。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kAzS35kMnZNt4YGgJXtfw
This commit is contained in:
@@ -4,7 +4,17 @@
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import type { RouteContract } from './http.ts';
|
||||
import { disableCheckSchema, grantSchema, positionSchema, staffSchema, workGroupSchema } from '../org.ts';
|
||||
import {
|
||||
disableCheckSchema,
|
||||
grantSchema,
|
||||
inFlightWorkSchema,
|
||||
positionSchema,
|
||||
roleAccessSchema,
|
||||
rosterSyncOutcomeSchema,
|
||||
staffSchema,
|
||||
workGroupSchema,
|
||||
} from '../org.ts';
|
||||
import { surfaceSchema } from './http.ts';
|
||||
import { positionIdSchema, staffIdSchema, taskIdSchema } from '../id.ts';
|
||||
import { ledgerObjectKindSchema } from '../ledger.ts';
|
||||
import { approvalTemplateSchema, closureReportSchema, processPackageSchema, triggerConfigSchema } from '../process-package.ts';
|
||||
@@ -17,15 +27,18 @@ import { reportMetricsSchema, reportPeriodSchema } from './workbench.ts';
|
||||
export const listStaffRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/staff',
|
||||
access: ['design', 'ops'],
|
||||
output: z.object({
|
||||
staff: z.array(staffSchema.extend({ position: positionSchema })),
|
||||
positions: z.array(positionSchema),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;人来自 Keycloak,改用 syncStaffRoute */
|
||||
export const createStaffRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/staff',
|
||||
access: 'public',
|
||||
body: z.object({
|
||||
account: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
@@ -34,35 +47,103 @@ export const createStaffRoute = {
|
||||
output: staffSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;改用 staffInFlightRoute */
|
||||
export const disableStaffCheckRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/staff/:staffId/disable-check',
|
||||
access: 'public',
|
||||
params: z.object({ staffId: staffIdSchema }),
|
||||
output: disableCheckSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;停用改在 Keycloak 做,名册同步据此落 disabled */
|
||||
export const disableStaffRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/staff/:staffId/disable',
|
||||
access: 'public',
|
||||
params: z.object({ staffId: staffIdSchema }),
|
||||
output: z.object({ ok: z.boolean(), check: disableCheckSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;权限只到端,改用 listRoleAccessRoute */
|
||||
export const listGrantsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/positions/:positionId/grants',
|
||||
access: 'public',
|
||||
params: z.object({ positionId: positionIdSchema }),
|
||||
output: z.object({ grants: z.array(grantSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;权限只到端,改用 setRoleAccessRoute */
|
||||
export const setGrantsRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/positions/:positionId/grants',
|
||||
access: 'public',
|
||||
params: z.object({ positionId: positionIdSchema }),
|
||||
body: z.object({ grants: z.array(grantSchema) }),
|
||||
output: z.object({ grants: z.array(grantSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── 员工管理 · 名册与岗位(ADR-012:人来自鉴权中心,岗位本地指派) ────── */
|
||||
|
||||
/** 从 Keycloak Admin API 同步名册;没配 provisioning 服务账号就如实回 configured:false */
|
||||
export const syncStaffRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/staff/sync',
|
||||
access: 'ops',
|
||||
output: rosterSyncOutcomeSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 指派岗位;指回哨兵岗 NONE = 取消指派 */
|
||||
export const setStaffPositionRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/staff/:staffId/position',
|
||||
access: 'ops',
|
||||
params: z.object({ staffId: staffIdSchema }),
|
||||
body: z.object({ positionId: positionIdSchema }),
|
||||
output: staffSchema.extend({ position: positionSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 此人身上还挂着什么(只读)—— 原「停用前置检查」;停用本身在 Keycloak 做 */
|
||||
export const staffInFlightRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/staff/:staffId/in-flight',
|
||||
access: 'ops',
|
||||
params: z.object({ staffId: staffIdSchema }),
|
||||
output: inFlightWorkSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── 角色 → 端(ADR-012:权限只到端,这张表运营中心可改) ─────────────── */
|
||||
|
||||
export const listRoleAccessRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/access/roles',
|
||||
access: 'ops',
|
||||
output: z.object({
|
||||
roles: z.array(roleAccessSchema),
|
||||
/** 名册上见过的角色 ∪ 已配置的角色 —— 配置前它等于「无端可进」 */
|
||||
knownRoles: z.array(z.string()),
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 锁死不变量不过(改完没人能进运营中心)→ 409 conflict */
|
||||
export const setRoleAccessRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/access/roles/:role',
|
||||
access: 'ops',
|
||||
params: z.object({ role: z.string().min(1) }),
|
||||
body: z.object({ ends: z.array(surfaceSchema) }),
|
||||
output: roleAccessSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const deleteRoleAccessRoute = {
|
||||
method: 'DELETE',
|
||||
path: '/api/gov/access/roles/:role',
|
||||
access: 'ops',
|
||||
params: z.object({ role: z.string().min(1) }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── 系统管理 · LLM 实例(四元绑定) ────────────────────────────────── */
|
||||
|
||||
export const LLM_PROVIDERS = ['openai-compatible', 'openai', 'deepseek', 'anthropic', 'qwen'] as const;
|
||||
@@ -92,6 +173,7 @@ export type LlmInstanceView = z.infer<typeof llmInstanceViewSchema>;
|
||||
export const listLlmProvidersRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/llm/providers',
|
||||
access: 'design',
|
||||
output: z.object({
|
||||
providers: z.array(
|
||||
z.object({
|
||||
@@ -109,12 +191,14 @@ export const listLlmProvidersRoute = {
|
||||
export const listLlmInstancesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/llm',
|
||||
access: 'design',
|
||||
output: z.object({ instances: z.array(llmInstanceViewSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const upsertLlmInstanceRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/llm',
|
||||
access: 'design',
|
||||
body: z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().min(1),
|
||||
@@ -142,6 +226,7 @@ export const upsertLlmInstanceRoute = {
|
||||
export const deleteLlmInstanceRoute = {
|
||||
method: 'DELETE',
|
||||
path: '/api/gov/llm/:id',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -149,6 +234,7 @@ export const deleteLlmInstanceRoute = {
|
||||
export const probeLlmDraftRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/llm/probe',
|
||||
access: 'design',
|
||||
body: z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
@@ -163,6 +249,7 @@ export const probeLlmDraftRoute = {
|
||||
export const probeLlmInstanceRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/llm/:id/probe',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ ok: z.boolean(), latencyMs: z.number().int().optional(), error: z.string().optional() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -186,6 +273,7 @@ export type GovTaskRow = z.infer<typeof govTaskRowSchema>;
|
||||
export const listGovTasksRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/tasks',
|
||||
access: 'ops',
|
||||
output: z.object({
|
||||
tasks: z.array(govTaskRowSchema),
|
||||
workflows: z.array(
|
||||
@@ -204,6 +292,7 @@ export const listGovTasksRoute = {
|
||||
export const controlTaskRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/tasks/:taskId/control',
|
||||
access: 'ops',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
body: z.object({ semantic: controlSemanticSchema, reason: z.string().optional() }),
|
||||
output: z.object({ ok: z.boolean(), resumable: z.boolean() }),
|
||||
@@ -212,6 +301,7 @@ export const controlTaskRoute = {
|
||||
export const resumeTaskRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/tasks/:taskId/resume',
|
||||
access: 'ops',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
output: z.object({ ok: z.boolean(), runtime: taskRuntimeStateSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -220,6 +310,7 @@ export const resumeTaskRoute = {
|
||||
export const reassignTaskRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/tasks/:taskId/reassign',
|
||||
access: 'ops',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
body: z.object({ to: staffIdSchema }),
|
||||
output: z.object({ ok: z.boolean(), ownerId: staffIdSchema }),
|
||||
@@ -229,6 +320,7 @@ export const reassignTaskRoute = {
|
||||
export const govTaskLogRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/tasks/:taskId/log',
|
||||
access: 'ops',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
output: z.object({
|
||||
// 与工作台同一份日志:章是四字动作,内容带主语(见 workbench 的 log 项)
|
||||
@@ -243,6 +335,7 @@ export const govTaskLogRoute = {
|
||||
export const listProcessPackagesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/process',
|
||||
access: ['design', 'ops'],
|
||||
output: z.object({ packages: z.array(processPackageSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -257,6 +350,7 @@ export const listProcessPackagesRoute = {
|
||||
export const saveProcessRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/process',
|
||||
access: 'design',
|
||||
/**
|
||||
* 编辑即其身:pkg.version 已存在 → 就地写回该版(号不变)。
|
||||
* `asNew: true`(或版本不存在,如新流程首版)→ 产生新版本号(服务端算,不生效)。
|
||||
@@ -275,6 +369,7 @@ export const saveProcessRoute = {
|
||||
export const setProcessStateRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/process/:id/:version/state',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string(), version: z.string() }),
|
||||
body: z.object({ state: z.enum(['active', 'retired']) }),
|
||||
output: z.object({ packages: z.array(processPackageSchema) }),
|
||||
@@ -288,6 +383,7 @@ export const setProcessStateRoute = {
|
||||
export const deleteProcessVersionRoute = {
|
||||
method: 'DELETE',
|
||||
path: '/api/gov/process/:id/:version',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string(), version: z.string() }),
|
||||
output: z.object({ packages: z.array(processPackageSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -298,6 +394,7 @@ export const deleteProcessVersionRoute = {
|
||||
export const listApprovalTemplatesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/approval-templates',
|
||||
access: 'design',
|
||||
output: z.object({
|
||||
templates: z.array(
|
||||
approvalTemplateSchema.extend({
|
||||
@@ -312,6 +409,7 @@ export const listApprovalTemplatesRoute = {
|
||||
export const upsertApprovalTemplateRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/approval-templates',
|
||||
access: 'design',
|
||||
body: z.object({ template: approvalTemplateSchema }),
|
||||
output: z.object({ template: approvalTemplateSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -320,6 +418,7 @@ export const upsertApprovalTemplateRoute = {
|
||||
export const deleteApprovalTemplateRoute = {
|
||||
method: 'DELETE',
|
||||
path: '/api/gov/approval-templates/:id',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -332,6 +431,7 @@ export const deleteApprovalTemplateRoute = {
|
||||
export const deleteProcessFlowRoute = {
|
||||
method: 'DELETE',
|
||||
path: '/api/gov/process/:id',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ packages: z.array(processPackageSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -347,6 +447,7 @@ export const deleteProcessFlowRoute = {
|
||||
export const editProcessVersionRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/process/:id/:version/edit',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string(), version: z.string() }),
|
||||
body: z.object({
|
||||
version: z.number().int().min(1).max(9999).optional(),
|
||||
@@ -360,6 +461,7 @@ export const editProcessVersionRoute = {
|
||||
export const validateProcessRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/process/validate',
|
||||
access: 'design',
|
||||
body: z.object({ pkg: processPackageSchema }),
|
||||
output: closureReportSchema,
|
||||
} as const satisfies RouteContract;
|
||||
@@ -369,12 +471,14 @@ export const validateProcessRoute = {
|
||||
export const listTriggersRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/triggers',
|
||||
access: ['design', 'ops'],
|
||||
output: z.object({ triggers: z.array(triggerConfigSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const toggleTriggerRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/triggers/:id/toggle',
|
||||
access: 'ops',
|
||||
params: z.object({ id: z.string() }),
|
||||
body: z.object({ enabled: z.boolean() }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
@@ -390,6 +494,7 @@ export const toggleTriggerRoute = {
|
||||
export const upsertTriggerRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/triggers',
|
||||
access: 'design',
|
||||
body: z.object({ trigger: triggerConfigSchema }),
|
||||
output: z.object({ trigger: triggerConfigSchema }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -405,6 +510,7 @@ export const upsertTriggerRoute = {
|
||||
export const listWorkGroupsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/work-groups',
|
||||
access: 'ops',
|
||||
/** `ownerName` 是组长姓名的呈现补充,读表时一并带出,免得前端再对一次 id */
|
||||
output: z.object({ groups: z.array(workGroupSchema.extend({ ownerName: z.string() })) }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -413,6 +519,7 @@ export const listWorkGroupsRoute = {
|
||||
export const saveWorkGroupRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/work-groups',
|
||||
access: 'ops',
|
||||
body: z.object({
|
||||
ownerId: staffIdSchema,
|
||||
name: z.string().min(1),
|
||||
@@ -434,6 +541,7 @@ export const saveWorkGroupRoute = {
|
||||
export const installActivityRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/activities',
|
||||
access: 'design',
|
||||
body: z.object({
|
||||
/** 清单六区:身份 / 七件契约 / 声明 / 资产初始版 / 样例集 / 代码入口 */
|
||||
manifest: activityManifestSchema,
|
||||
@@ -465,6 +573,7 @@ export const installActivityRoute = {
|
||||
export const registerToolRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/tools',
|
||||
access: 'design',
|
||||
body: z.object({
|
||||
contract: z.object({
|
||||
name: z.string().min(1),
|
||||
@@ -500,6 +609,7 @@ export const registerToolRoute = {
|
||||
export const listActivitiesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/activities',
|
||||
access: 'design',
|
||||
output: z.object({
|
||||
activities: z.array(
|
||||
z.object({
|
||||
@@ -562,6 +672,7 @@ export const listActivitiesRoute = {
|
||||
export const listToolsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/tools',
|
||||
access: 'design',
|
||||
output: z.object({
|
||||
tools: z.array(
|
||||
z.object({
|
||||
@@ -583,6 +694,7 @@ export const listToolsRoute = {
|
||||
export const listLedgerObjectsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/data/objects',
|
||||
access: ['design', 'ops'],
|
||||
/** `label` 是人读名(落在表注释上);缺省即没登记,呈现退回对象键 */
|
||||
output: z.object({
|
||||
objects: z.array(
|
||||
@@ -623,6 +735,7 @@ export const listLedgerObjectsRoute = {
|
||||
export const ledgerByActivityRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/data/by-activity',
|
||||
access: ['design', 'ops'],
|
||||
output: z.object({
|
||||
activities: z.array(
|
||||
z.object({
|
||||
@@ -667,6 +780,7 @@ export const ledgerByActivityRoute = {
|
||||
export const browseLedgerRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/data/:domain/:object',
|
||||
access: ['design', 'ops'],
|
||||
params: z.object({ domain: z.string(), object: z.string() }),
|
||||
output: z.object({
|
||||
label: z.string().optional(),
|
||||
@@ -682,6 +796,7 @@ export const browseLedgerRoute = {
|
||||
export const listActivityMemoryRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/memory/:activityId',
|
||||
access: 'design',
|
||||
params: z.object({ activityId: z.string() }),
|
||||
output: z.object({
|
||||
entries: z.array(
|
||||
@@ -703,6 +818,7 @@ export const listActivityMemoryRoute = {
|
||||
export const listChannelsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/channels',
|
||||
access: ['design', 'ops'],
|
||||
output: z.object({
|
||||
channels: z.array(
|
||||
z.object({
|
||||
@@ -733,6 +849,7 @@ export const listChannelsRoute = {
|
||||
export const listApprovalsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/approvals',
|
||||
access: 'ops',
|
||||
output: z.object({
|
||||
approvals: z.array(
|
||||
z.object({
|
||||
@@ -763,6 +880,7 @@ export const listApprovalsRoute = {
|
||||
export const setTriggerConfirmRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/triggers/:id/confirm',
|
||||
access: 'ops',
|
||||
params: z.object({ id: z.string() }),
|
||||
body: z.object({ required: z.boolean(), staffId: z.string().optional() }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
@@ -772,6 +890,7 @@ export const setTriggerConfirmRoute = {
|
||||
export const listNodeOwnersRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/packages/:id/owners',
|
||||
access: 'ops',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ owners: z.array(z.object({ nodeId: z.string(), staffId: z.string() })) }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -779,6 +898,7 @@ export const listNodeOwnersRoute = {
|
||||
export const setNodeOwnerRoute = {
|
||||
method: 'PUT',
|
||||
path: '/api/gov/packages/:id/owners/:nodeId',
|
||||
access: 'ops',
|
||||
params: z.object({ id: z.string(), nodeId: z.string() }),
|
||||
/** null = 撤掉运营指定,回到轮值 */
|
||||
body: z.object({ staffId: z.string().nullable() }),
|
||||
@@ -788,6 +908,7 @@ export const setNodeOwnerRoute = {
|
||||
export const testRunRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/testbed/run',
|
||||
access: 'design',
|
||||
body: z.object({
|
||||
packageId: z.string(),
|
||||
/**
|
||||
@@ -827,6 +948,7 @@ export const testRunRoute = {
|
||||
export const testRunActivityRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/testbed/run-activity',
|
||||
access: 'design',
|
||||
body: z.object({
|
||||
activityId: z.string().min(1),
|
||||
/** 起单入参 —— 按该活动契约的 `input.data` 填;可用包自带样例预填 */
|
||||
@@ -860,6 +982,7 @@ export const testRunActivityRoute = {
|
||||
export const testStopRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/testbed/stop',
|
||||
access: 'design',
|
||||
/** 两种试法各有其拔法:流程包给 workflowId,单活动给 taskId;都不给 = 拔掉所有试跑残留 */
|
||||
body: z.object({ workflowId: z.string().default(''), taskId: z.string().default('') }),
|
||||
output: z.object({ destroyed: z.boolean(), tasks: z.number().int() }),
|
||||
@@ -877,6 +1000,7 @@ export const testStopRoute = {
|
||||
export const testCurrentRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/testbed/current',
|
||||
access: 'design',
|
||||
output: z.object({
|
||||
workflowId: z.string().optional(),
|
||||
subject: z.string().optional(),
|
||||
@@ -900,6 +1024,7 @@ export const testCurrentRoute = {
|
||||
export const testActivityStateRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/testbed/activity/:taskId',
|
||||
access: 'design',
|
||||
params: z.object({ taskId: z.string() }),
|
||||
output: z.object({
|
||||
taskId: z.string(),
|
||||
@@ -954,6 +1079,7 @@ export const testActivityStateRoute = {
|
||||
export const uploadFileRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/gov/upload',
|
||||
access: 'design',
|
||||
body: z.object({
|
||||
name: z.string().min(1),
|
||||
contentType: z.string().default(''),
|
||||
@@ -976,6 +1102,7 @@ export type UploadedFile = z.infer<typeof uploadFileRoute.output>;
|
||||
export const downloadFileRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/upload/:id',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ name: z.string(), contentType: z.string(), dataBase64: z.string() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -1013,6 +1140,7 @@ export type TestLogRow = z.infer<typeof testLogRowSchema>;
|
||||
export const listTestLogsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/testbed/logs',
|
||||
access: 'design',
|
||||
output: z.object({ runs: z.array(testLogRowSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -1020,6 +1148,7 @@ export const listTestLogsRoute = {
|
||||
export const testLogDetailRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/testbed/logs/:id',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: testLogRowSchema.extend({
|
||||
trail: z.array(
|
||||
@@ -1053,6 +1182,7 @@ export const testLogDetailRoute = {
|
||||
export const deleteTestLogRoute = {
|
||||
method: 'DELETE',
|
||||
path: '/api/gov/testbed/logs/:id',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ deleted: z.number().int() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -1060,6 +1190,7 @@ export const deleteTestLogRoute = {
|
||||
export const clearTestLogsRoute = {
|
||||
method: 'DELETE',
|
||||
path: '/api/gov/testbed/logs',
|
||||
access: 'design',
|
||||
output: z.object({ deleted: z.number().int() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -1067,6 +1198,7 @@ export const clearTestLogsRoute = {
|
||||
export const traceOfWorkflowRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/workflows/:id/trace',
|
||||
access: 'design',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({
|
||||
spans: z.array(
|
||||
@@ -1092,6 +1224,7 @@ export const traceOfWorkflowRoute = {
|
||||
export const govWorkflowRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/workflows/:id',
|
||||
access: ['design', 'ops'],
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({
|
||||
workflow: z.object({
|
||||
@@ -1131,6 +1264,7 @@ export const govWorkflowRoute = {
|
||||
export const govMetricsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/gov/metrics',
|
||||
access: 'ops',
|
||||
/** 统计窗口 —— 不传按月;驾驶舱可切日/周/月/年 */
|
||||
query: z.object({ period: reportPeriodSchema.optional() }),
|
||||
/** `period` 原样回声:客户端不传时,由它知道服务端用的是哪个窗口 */
|
||||
|
||||
@@ -4,14 +4,35 @@
|
||||
* 前端只经 `shared/typed-fetch` + 本目录的路由契约与服务端交互;组件内禁止裸 fetch。
|
||||
* **禁止 Hono RPC 客户端** —— 它要求前端 import 后端类型,违反 §5.1。
|
||||
*/
|
||||
import type { z } from 'zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
|
||||
export type HttpMethod = (typeof HTTP_METHODS)[number];
|
||||
|
||||
/**
|
||||
* 三个端(ADR-012):设计中心 / 运营中心 / 工作台。
|
||||
* 权限只到端 —— 一个角色能进哪几个端由运营中心的「角色 → 端」表定,不写死在代码里。
|
||||
*/
|
||||
export const SURFACES = ['design', 'ops', 'workbench'] as const;
|
||||
export const surfaceSchema = z.enum(SURFACES);
|
||||
export type Surface = z.infer<typeof surfaceSchema>;
|
||||
|
||||
export const SURFACE_LABEL: Readonly<Record<Surface, string>> = {
|
||||
design: '设计中心',
|
||||
ops: '运营中心',
|
||||
workbench: '工作台',
|
||||
};
|
||||
|
||||
/**
|
||||
* 一条路由归哪个端(ADR-012):
|
||||
* `public` 不需要凭据;`any` 登录即可、不看端;端名 / 端名数组 —— 能进其中任一端即可。
|
||||
* 鉴权中间件按它统一判,路由里不再各写守卫;漏写编译不过(`satisfies RouteContract`)。
|
||||
*/
|
||||
export type RouteAccess = 'public' | 'any' | Surface | readonly Surface[];
|
||||
|
||||
/**
|
||||
* 一条路由契约。`path` 用 `:name` 表示路径参数,与 `params` 的键一一对应。
|
||||
* body 只在 POST/PATCH 上有意义;GET 用 query。
|
||||
* body 只在 POST/PUT/PATCH 上有意义;GET 用 query。
|
||||
*/
|
||||
export type RouteContract<
|
||||
TParams extends z.ZodTypeAny = z.ZodTypeAny,
|
||||
@@ -21,6 +42,7 @@ export type RouteContract<
|
||||
> = {
|
||||
readonly method: HttpMethod;
|
||||
readonly path: string;
|
||||
readonly access: RouteAccess;
|
||||
readonly params?: TParams;
|
||||
readonly query?: TQuery;
|
||||
readonly body?: TBody;
|
||||
@@ -32,7 +54,7 @@ export type RouteBody<R extends RouteContract> = R['body'] extends z.ZodTypeAny
|
||||
? z.infer<R['body']>
|
||||
: never;
|
||||
|
||||
/** 结构化错误:前端按 code 分辨,不解析文案 */
|
||||
/** 结构化错误:前端按 code 分辨,不解析文案。`gone` = 410,路由已随统一鉴权中心退役 */
|
||||
export const API_ERROR_CODES = [
|
||||
'unauthenticated',
|
||||
'forbidden',
|
||||
@@ -41,6 +63,7 @@ export const API_ERROR_CODES = [
|
||||
'conflict',
|
||||
'precondition-failed',
|
||||
'internal',
|
||||
'gone',
|
||||
] as const;
|
||||
export type ApiErrorCode = (typeof API_ERROR_CODES)[number];
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ export type InboundMail = z.infer<typeof inboundMailSchema>;
|
||||
export const ingestMailRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/ingest/channel/:channelId/mail',
|
||||
access: 'public',
|
||||
params: z.object({ channelId: channelIdSchema }),
|
||||
body: inboundMailSchema,
|
||||
output: z.object({
|
||||
|
||||
@@ -4,39 +4,40 @@
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
import type { RouteContract } from './http.ts';
|
||||
import { sessionSchema } from '../org.ts';
|
||||
import { principalSchema, sessionSchema } from '../org.ts';
|
||||
import { chatMessageSchema, deliverableSchema, fileDocSchema, formFieldSchema, stageSchema, viewBlockSchema, workOrderSchema } from '../card.ts';
|
||||
import { attributionSideSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts';
|
||||
import { staffIdSchema, taskIdSchema } from '../id.ts';
|
||||
|
||||
/** 我是谁 —— 无会话即 401,前端据此落登录页(ADR-010) */
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;改用 meRoute */
|
||||
export const sessionRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/session',
|
||||
access: 'public',
|
||||
output: sessionSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 登录 —— 单组织 cookie session(编码规范 §4 / ADR-010)。
|
||||
* 成功即种 `jcai_session`(HttpOnly);失败只回一句话,不区分账号不存在还是口令错。
|
||||
*/
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;改用 meRoute */
|
||||
export const loginRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/login',
|
||||
access: 'public',
|
||||
body: z.object({ account: z.string().min(1), password: z.string().min(1) }),
|
||||
output: sessionSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 登出 —— 服务端会话立刻作废,不等 cookie 到期 */
|
||||
/** @deprecated 随 ADR-012 退役,后端回 410;改用 meRoute */
|
||||
export const logoutRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/logout',
|
||||
access: 'public',
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const healthRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/health',
|
||||
access: 'public',
|
||||
output: z.object({
|
||||
ok: z.boolean(),
|
||||
version: z.string(),
|
||||
@@ -44,6 +45,25 @@ export const healthRoute = {
|
||||
}),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/** 我是谁、我能进哪几个端(ADR-012)—— 中间件解好 principal 放进上下文,这里只回投影 */
|
||||
export const meRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/me',
|
||||
access: 'any',
|
||||
output: principalSchema,
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/**
|
||||
* 60 秒票据(ADR-012 §9.3)—— 只为两条带不了 Authorization 头的路:
|
||||
* `EventSource('/api/stream')` 与 `<a href="…&raw=1" download>`。GET 上 `?ticket=` 认它。
|
||||
*/
|
||||
export const authTicketRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/auth/ticket',
|
||||
access: 'any',
|
||||
output: z.object({ ticket: z.string().min(1), expiresAt: z.string() }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
/* ── 任务要素:四区(任务列表 / 标题区 / 工作区 / 对话区) ─────────────── */
|
||||
|
||||
/** 任务卡 —— 清单里一条 = 一个活动的一次运行,不是一整条工作流 */
|
||||
@@ -77,6 +97,7 @@ export type TaskCard = z.infer<typeof taskCardSchema>;
|
||||
export const listTasksRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/tasks',
|
||||
access: 'workbench',
|
||||
output: z.object({ tasks: z.array(taskCardSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -229,6 +250,7 @@ export type TaskDetail = z.infer<typeof taskDetailSchema>;
|
||||
export const taskDetailRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/tasks/:taskId',
|
||||
access: 'workbench',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
output: taskDetailSchema,
|
||||
} as const satisfies RouteContract;
|
||||
@@ -237,6 +259,7 @@ export const taskDetailRoute = {
|
||||
export const replyWorkOrderRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/work-orders/reply',
|
||||
access: 'workbench',
|
||||
body: z.object({ reply: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({ ok: z.boolean(), producesCompletion: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -248,6 +271,7 @@ export const replyWorkOrderRoute = {
|
||||
export const replyChatCardRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/chat-cards/reply',
|
||||
access: 'workbench',
|
||||
body: z.object({ reply: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -259,6 +283,7 @@ export const replyChatCardRoute = {
|
||||
export const markCommReadRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/comms/:commId/read',
|
||||
access: 'workbench',
|
||||
params: z.object({ commId: z.string() }),
|
||||
body: z.object({ read: z.boolean() }),
|
||||
output: z.object({ ok: z.boolean(), unread: z.boolean() }),
|
||||
@@ -301,6 +326,7 @@ export const markCommReadRoute = {
|
||||
export const saveTableRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/tasks/:taskId/table',
|
||||
access: 'workbench',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
body: z.object({
|
||||
/** `{ '域.对象': [ { 台账列名: 值 } ] }` —— 主键列用来认行,不参与改行;台账里没有的行就是**新增** */
|
||||
@@ -347,6 +373,7 @@ export const saveTableRoute = {
|
||||
export const openAssetFileRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/tasks/:taskId/file',
|
||||
access: 'workbench',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
query: z.object({
|
||||
/** `域.对象` —— 必须是活动清单里声明过 `file` 段的那张表(授权面) */
|
||||
@@ -362,6 +389,7 @@ export const openAssetFileRoute = {
|
||||
export const sayRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/tasks/:taskId/say',
|
||||
access: 'workbench',
|
||||
params: z.object({ taskId: taskIdSchema }),
|
||||
body: z.object({ text: z.string().min(1) }),
|
||||
output: chatMessageSchema,
|
||||
@@ -402,12 +430,14 @@ export type SystemMessageView = z.infer<typeof systemMessageSchema>;
|
||||
export const listMessagesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/desk/messages',
|
||||
access: 'workbench',
|
||||
output: z.object({ messages: z.array(systemMessageSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const markMessageReadRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/messages/:messageId/read',
|
||||
access: 'workbench',
|
||||
params: z.object({ messageId: z.string() }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -416,6 +446,7 @@ export const markMessageReadRoute = {
|
||||
export const answerMessageRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/messages/:messageId/answer',
|
||||
access: 'workbench',
|
||||
params: z.object({ messageId: z.string() }),
|
||||
body: z.object({ answer: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({ ok: z.boolean() }),
|
||||
@@ -425,6 +456,7 @@ export const answerMessageRoute = {
|
||||
export const confirmTriggerPendingRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/desk/trigger-pending/:id/confirm',
|
||||
access: 'workbench',
|
||||
params: z.object({ id: z.string() }),
|
||||
output: z.object({ workflowId: z.string(), alreadyStarted: z.boolean() }),
|
||||
} as const satisfies RouteContract;
|
||||
@@ -460,12 +492,14 @@ export type MinuteView = z.infer<typeof minuteSchema>;
|
||||
export const listMinutesRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/minutes',
|
||||
access: 'workbench',
|
||||
output: z.object({ minutes: z.array(minuteSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
export const createMinuteRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/minutes',
|
||||
access: 'workbench',
|
||||
body: z.object({
|
||||
title: z.string().min(1),
|
||||
body: z.string(),
|
||||
@@ -479,6 +513,7 @@ export const createMinuteRoute = {
|
||||
export const appendResolutionRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/minutes/:minuteId/resolutions',
|
||||
access: 'workbench',
|
||||
params: z.object({ minuteId: z.string() }),
|
||||
body: z.object({ text: z.string().min(1), ownerId: staffIdSchema.optional() }),
|
||||
output: minuteSchema,
|
||||
@@ -488,6 +523,7 @@ export const appendResolutionRoute = {
|
||||
export const registerResolutionRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/minutes/resolutions/:resolutionId/register',
|
||||
access: 'workbench',
|
||||
params: z.object({ resolutionId: z.string() }),
|
||||
body: z.object({ processPackageId: z.string().min(1), data: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({ workflowId: z.string(), resolutionId: z.string() }),
|
||||
@@ -542,6 +578,7 @@ export type ReportView = z.infer<typeof reportSchema>;
|
||||
export const draftReportRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/reports/:period',
|
||||
access: 'workbench',
|
||||
params: z.object({ period: reportPeriodSchema }),
|
||||
output: reportSchema,
|
||||
} as const satisfies RouteContract;
|
||||
@@ -549,6 +586,7 @@ export const draftReportRoute = {
|
||||
export const listReportsRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/reports',
|
||||
access: 'workbench',
|
||||
output: z.object({ reports: z.array(reportSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -556,6 +594,7 @@ export const listReportsRoute = {
|
||||
export const submitReportRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/reports/:reportId/submit',
|
||||
access: 'workbench',
|
||||
params: z.object({ reportId: z.string() }),
|
||||
body: z.object({ note: z.string().optional() }),
|
||||
output: reportSchema,
|
||||
@@ -585,6 +624,7 @@ export type ApprovalItem = z.infer<typeof approvalItemSchema>;
|
||||
export const listApprovalsForMeRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/approvals',
|
||||
access: 'workbench',
|
||||
output: z.object({ approvals: z.array(approvalItemSchema) }),
|
||||
} as const satisfies RouteContract;
|
||||
|
||||
@@ -592,6 +632,7 @@ export const listApprovalsForMeRoute = {
|
||||
export const decideApprovalRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/approvals/:workflowId/:nodeId/decide',
|
||||
access: 'workbench',
|
||||
params: z.object({ workflowId: z.string(), nodeId: z.string() }),
|
||||
body: z.object({ verdict: z.enum(['pass', 'reject']), comment: z.string().optional() }),
|
||||
output: z.object({ ok: z.boolean(), workflowState: workflowStateSchema }),
|
||||
@@ -608,6 +649,7 @@ export type ListApprovalsForMeOutput = z.infer<typeof listApprovalsForMeRoute.ou
|
||||
export const listRegistrationTriggersRoute = {
|
||||
method: 'GET',
|
||||
path: '/api/triggers/registration',
|
||||
access: 'workbench',
|
||||
output: z.object({
|
||||
triggers: z.array(
|
||||
z.object({
|
||||
@@ -625,6 +667,7 @@ export const listRegistrationTriggersRoute = {
|
||||
export const fireRegistrationRoute = {
|
||||
method: 'POST',
|
||||
path: '/api/triggers/:triggerId/register',
|
||||
access: 'workbench',
|
||||
params: z.object({ triggerId: z.string() }),
|
||||
body: z.object({ data: z.record(z.string(), z.unknown()) }),
|
||||
output: z.object({
|
||||
|
||||
Reference in New Issue
Block a user