Archived
建组/改组:逐岗位选人,留空即回落四策略。绑定先于策略,起单时快照进工作流; 实测改绑定后新起单的 work_group 快照即含新成员表,在途不受影响。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
169 lines
7.5 KiB
TypeScript
169 lines
7.5 KiB
TypeScript
/**
|
|
* 治理台路由组装(编码规范 §3:治理台九功能不设后端 governance 模块 ——
|
|
* 每个功能是对应模块的管理用例 + 本文件的路由组装)。
|
|
*/
|
|
import type { Hono } from 'hono';
|
|
import type { ApiError, GovernanceFunction } from '@contracts';
|
|
import {
|
|
createStaffRoute,
|
|
disableStaffCheckRoute,
|
|
disableStaffRoute,
|
|
listGrantsRoute,
|
|
listLlmInstancesRoute,
|
|
listStaffRoute,
|
|
listWorkGroupsRoute,
|
|
positionIdSchema,
|
|
probeLlmInstanceRoute,
|
|
saveWorkGroupRoute,
|
|
setGrantsRoute,
|
|
staffIdSchema,
|
|
upsertLlmInstanceRoute,
|
|
} from '@contracts';
|
|
import { DisableBlockedError } from '@modules/org';
|
|
import type { Container } from '../container.ts';
|
|
import type { AppEnv } from '../http-env.ts';
|
|
|
|
/** 治理动作要过岗位授权(客体 = 治理功能,动作 = 治理) */
|
|
function requireGovern(container: Container, fn: GovernanceFunction) {
|
|
return async (account: string): Promise<ApiError | undefined> => {
|
|
const staff = await container.org.findByAccount(account);
|
|
if (staff === undefined) return { code: 'unauthenticated', message: '会话无效' };
|
|
const ok = await container.org.authorize(staff.id, { kind: 'governance', function: fn }, 'govern');
|
|
return ok ? undefined : { code: 'forbidden', message: `岗位 ${staff.position.code} 无「${fn}」治理权` };
|
|
};
|
|
}
|
|
|
|
export function registerGovernanceRoutes(app: Hono<AppEnv>, container: Container): void {
|
|
const guardStaff = requireGovern(container, 'staff');
|
|
const guardSystem = requireGovern(container, 'system');
|
|
|
|
/* ── 员工管理 ─────────────────────────────────────────────────────── */
|
|
|
|
app.get(listStaffRoute.path, async (c) => {
|
|
const denied = await guardStaff(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
return c.json(await container.org.listStaff());
|
|
});
|
|
|
|
app.post(createStaffRoute.path, async (c) => {
|
|
const denied = await guardStaff(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
const parsed = createStaffRoute.body.safeParse(await c.req.json());
|
|
if (!parsed.success) {
|
|
const body: ApiError = { code: 'validation-failed', message: '入参不合法', detail: parsed.error.issues };
|
|
return c.json(body, 400);
|
|
}
|
|
return c.json(await container.org.createStaff(parsed.data));
|
|
});
|
|
|
|
app.get(disableStaffCheckRoute.path, async (c) => {
|
|
const denied = await guardStaff(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
const staffId = staffIdSchema.parse(c.req.param('staffId'));
|
|
return c.json(await container.org.checkDisable(staffId));
|
|
});
|
|
|
|
app.post(disableStaffRoute.path, async (c) => {
|
|
const denied = await guardStaff(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
const staffId = staffIdSchema.parse(c.req.param('staffId'));
|
|
try {
|
|
await container.org.disableStaff(staffId);
|
|
return c.json({ ok: true, check: await container.org.checkDisable(staffId) });
|
|
} catch (error) {
|
|
if (error instanceof DisableBlockedError) {
|
|
// 停用前置检查未过:在途任务与待答卡须先改派完毕(OS 02)
|
|
const body: ApiError = { code: 'precondition-failed', message: error.message, detail: error.check };
|
|
return c.json(body, 412);
|
|
}
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
app.get(listGrantsRoute.path, async (c) => {
|
|
const denied = await guardStaff(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
const positionId = positionIdSchema.parse(c.req.param('positionId'));
|
|
return c.json({ grants: await container.org.listGrants(positionId) });
|
|
});
|
|
|
|
app.post(setGrantsRoute.path, async (c) => {
|
|
const denied = await guardStaff(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
const positionId = positionIdSchema.parse(c.req.param('positionId'));
|
|
const parsed = setGrantsRoute.body.safeParse(await c.req.json());
|
|
if (!parsed.success) {
|
|
const body: ApiError = { code: 'validation-failed', message: '入参不合法', detail: parsed.error.issues };
|
|
return c.json(body, 400);
|
|
}
|
|
return c.json({ grants: await container.org.setGrants(positionId, parsed.data.grants) });
|
|
});
|
|
|
|
/* ── 员工管理 · 工作组(ADR-011) ──────────────────────────────────── */
|
|
|
|
/**
|
|
* 全部工作组 —— 一人至多一组,按员工遍历取。
|
|
* `ownerName` 一并带出:前端要显示的是「柴浩的组」,不是一串 id。
|
|
*/
|
|
app.get(listWorkGroupsRoute.path, async (c) => {
|
|
const denied = await guardStaff(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
const { staff } = await container.org.listStaff();
|
|
const found = await Promise.all(
|
|
staff.map(async (s) => {
|
|
const group = await container.org.workGroupOf(s.id);
|
|
return group === undefined ? undefined : { ...group, ownerName: s.name };
|
|
}),
|
|
);
|
|
return c.json({ groups: found.filter((g) => g !== undefined) });
|
|
});
|
|
|
|
/**
|
|
* 建组 / 改组 —— 按 `ownerId` 落:已有即更新(复用 id),没有即新建。
|
|
* 与 `workGroupOf` 的取组口径一致(一人一组),不给同一个人建出两个组来。
|
|
* 改组**不影响在途**:装进工作流的是起单时的快照。
|
|
*/
|
|
app.put(saveWorkGroupRoute.path, async (c) => {
|
|
const denied = await guardStaff(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
const parsed = saveWorkGroupRoute.body.safeParse(await c.req.json());
|
|
if (!parsed.success) {
|
|
const body: ApiError = { code: 'validation-failed', message: '工作组不合 schema', detail: parsed.error.issues };
|
|
return c.json(body, 400);
|
|
}
|
|
const existing = await container.org.workGroupOf(parsed.data.ownerId);
|
|
const group = await container.org.saveWorkGroup({
|
|
...(existing === undefined ? {} : { id: existing.id }),
|
|
name: parsed.data.name,
|
|
ownerId: parsed.data.ownerId,
|
|
members: parsed.data.members,
|
|
});
|
|
return c.json({ group });
|
|
});
|
|
|
|
/* ── 系统管理 · LLM 实例 ──────────────────────────────────────────── */
|
|
|
|
app.get(listLlmInstancesRoute.path, async (c) => {
|
|
const denied = await guardSystem(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
return c.json({ instances: await container.llm.list() });
|
|
});
|
|
|
|
app.post(upsertLlmInstanceRoute.path, async (c) => {
|
|
const denied = await guardSystem(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
const parsed = upsertLlmInstanceRoute.body.safeParse(await c.req.json());
|
|
if (!parsed.success) {
|
|
const body: ApiError = { code: 'validation-failed', message: '入参不合法', detail: parsed.error.issues };
|
|
return c.json(body, 400);
|
|
}
|
|
return c.json(await container.llm.upsert(parsed.data));
|
|
});
|
|
|
|
app.post(probeLlmInstanceRoute.path, async (c) => {
|
|
const denied = await guardSystem(c.get('account'));
|
|
if (denied) return c.json(denied, denied.code === 'forbidden' ? 403 : 401);
|
|
return c.json(await container.llm.probe(c.req.param('id')));
|
|
});
|
|
}
|