/** * 员工、岗位与端(编码规范 §4 · `org`;语义出处 OS 02 员工管理;鉴权归统一鉴权中心,ADR-012)。 * * 身份来自 Keycloak:`Staff.id` 就是 token 的 `sub`。权限只到「端」(design / ops / workbench), * 角色 → 端由运营中心的表定;岗位仍是本系统的表(流程引擎按它解析负责人)。 * 旧的岗位授权(客体三类 × 动作三种)与口令会话已退役 —— 类型留一轮(@deprecated,下个迭代删)。 */ import { z } from 'zod'; import { positionIdSchema, staffIdSchema } from './id.ts'; import { surfaceSchema } from './api/http.ts'; export const positionSchema = z.object({ id: positionIdSchema, code: z.string().min(1), name: z.string().min(1), note: z.string().optional(), }); export type Position = z.infer; export const STAFF_STATES = ['active', 'disabled'] as const; export const staffStateSchema = z.enum(STAFF_STATES); export type StaffState = z.infer; export const staffSchema = z.object({ /** = Keycloak `sub`(手册硬约束 8:业务外键用 sub,不用用户名) */ id: staffIdSchema, /** = `preferred_username`,只作显示 —— 用户名允许改,别拿它当键 */ account: z.string().min(1), /** = `name` claim,缺则回退 account */ name: z.string().min(1), email: z.string().optional(), /** 未指派 = 哨兵岗 `NONE / 未分配` 的 id */ positionId: positionIdSchema, /** 只由名册同步置 `disabled`(Keycloak 停用或删除);有效 token 不改它 */ state: staffStateSchema.default('active'), /** realm 角色快照(token 签发时;噪声角色已滤) */ roles: z.array(z.string()).default([]), }); export type Staff = z.infer; /* ── 我是谁、能进哪几个端(ADR-012) ───────────────────────────────── */ export const principalSchema = z.object({ staff: staffSchema, position: positionSchema, roles: z.array(z.string()), ends: z.array(surfaceSchema), }); export type Principal = z.infer; /** 角色 → 端:一行一个 realm 角色 */ export const roleAccessSchema = z.object({ role: z.string().min(1), ends: z.array(surfaceSchema), }); export type RoleAccess = z.infer; /** * 名册同步的结果 —— 没配 provisioning 就如实说没配,不摆零。 * 三个计数说的是**真落进库里的条数**,不是打算写几条:单条写失败时它不进计数, * 而是点名进 `failed` —— 一次半途失败的同步不许报成一排绿数字。 */ export const rosterSyncOutcomeSchema = z.discriminatedUnion('configured', [ z.object({ configured: z.literal(false), message: z.string() }), z.object({ configured: z.literal(true), inserted: z.number().int().min(0), updated: z.number().int().min(0), disabled: z.number().int().min(0), total: z.number().int().min(0), /** 逐条落库时失败的人:账号 + 原话(厂商原文照抄,不改写成「操作失败」) */ failed: z.array(z.object({ account: z.string(), reason: z.string() })), }), ]); export type RosterSyncOutcome = z.infer; /* ── 在途工作(只读;OS 02:改派先于停用,停用本身已归鉴权中心) ───── */ export const inFlightWorkSchema = z.object({ staffId: staffIdSchema, blocked: z.boolean(), openTasks: z.array(z.object({ taskId: z.string(), title: z.string() })).default([]), openCards: z.array(z.object({ cardId: z.string(), title: z.string() })).default([]), openApprovals: z.array(z.object({ approvalId: z.string(), title: z.string() })).default([]), }); export type InFlightWork = z.infer; /** @deprecated 随 ADR-012 改名 `inFlightWorkSchema`;下个迭代删 */ export const disableCheckSchema = inFlightWorkSchema; /** @deprecated 改用 `InFlightWork` */ export type DisableCheck = InFlightWork; /* ── 岗位授权(已退役,ADR-012;类型留一轮给 web 编译,下个迭代删) ──── */ /** @deprecated 权限只到端,动作三分已退役 */ export const GRANT_ACTIONS = ['read', 'write', 'govern'] as const; /** @deprecated */ export const grantActionSchema = z.enum(GRANT_ACTIONS); /** @deprecated */ export type GrantAction = z.infer; /** @deprecated */ export const GRANT_ACTION_LABEL: Readonly> = { read: '读', write: '写', govern: '治理', }; /** @deprecated 治理台九功能不再是授权客体 */ export const GOVERNANCE_FUNCTIONS = [ 'task', 'staff', 'process', 'trigger', 'activity', 'tool', 'data', 'memory', 'system', ] as const; /** @deprecated */ export const governanceFunctionSchema = z.enum(GOVERNANCE_FUNCTIONS); /** @deprecated */ export type GovernanceFunction = z.infer; /** @deprecated */ export const GOVERNANCE_FUNCTION_LABEL: Readonly> = { task: '任务管理', staff: '员工管理', process: '工作流管理', trigger: '触发器配置', activity: '活动库管理', tool: '工具管理', data: '数据管理', memory: '记忆管理', system: '系统管理', }; /** @deprecated */ export const grantObjectSchema = z.discriminatedUnion('kind', [ z.object({ kind: z.literal('data'), domain: z.string().min(1), object: z.string().min(1), field: z.string().optional(), }), z.object({ kind: z.literal('governance'), function: governanceFunctionSchema }), z.object({ kind: z.literal('task'), activityId: z.string().optional() }), ]); /** @deprecated */ export type GrantObject = z.infer; /** @deprecated */ export const grantSchema = z.object({ positionId: positionIdSchema, object: grantObjectSchema, action: grantActionSchema, }); /** @deprecated */ export type Grant = z.infer; /* ── 会话(已退役:身份在 token 里,ADR-012) ──────────────────────── */ /** @deprecated 改用 `principalSchema` / `GET /api/me` */ export const sessionSchema = z.object({ staff: staffSchema, position: positionSchema, grants: z.array(grantSchema), }); /** @deprecated 改用 `Principal` */ export type Session = z.infer; /* ── 工作组(ADR-011) ────────────────────────────────────────────────── */ /** * 工作组 —— 一组「岗位 → 员工」的绑定,起单时装载、随工作流走。 * * **不是第五种负责人策略**(OS 03「策略封闭四选」不动): * 四策略回答的是「这个岗位该派给谁」,工作组直接给出了答案,策略于是无从发问。 * 键是**岗位 code**(BD / PM / FAE …),与流程包 `owner.position` 同一口径。 */ export const workGroupSchema = z.object({ id: z.string().min(1), name: z.string().min(1), /** 组归谁 —— 起单时按来源人取其组 */ ownerId: staffIdSchema, members: z.record(z.string().min(1), staffIdSchema), }); export type WorkGroup = z.infer; /** 装进工作流的**快照**:改组不改已起的单(留痕不被改写) */ export const workGroupBindingSchema = z.record(z.string().min(1), staffIdSchema); export type WorkGroupBinding = z.infer;