/** * 工作台路由契约(编码规范 §8.1 / §9.1)。 * 工作台五要素:任务 / 审批 / 消息 / 纪要 / 汇报 —— 同一份过程事实的五种投影。 */ import { z } from 'zod'; import type { RouteContract } from './http.ts'; import { 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) */ export const sessionRoute = { method: 'GET', path: '/api/session', output: sessionSchema, } as const satisfies RouteContract; /** * 登录 —— 单组织 cookie session(编码规范 §4 / ADR-010)。 * 成功即种 `jcai_session`(HttpOnly);失败只回一句话,不区分账号不存在还是口令错。 */ export const loginRoute = { method: 'POST', path: '/api/login', body: z.object({ account: z.string().min(1), password: z.string().min(1) }), output: sessionSchema, } as const satisfies RouteContract; /** 登出 —— 服务端会话立刻作废,不等 cookie 到期 */ export const logoutRoute = { method: 'POST', path: '/api/logout', output: z.object({ ok: z.boolean() }), } as const satisfies RouteContract; export const healthRoute = { method: 'GET', path: '/api/health', output: z.object({ ok: z.boolean(), version: z.string(), db: z.enum(['up', 'down']), }), } as const satisfies RouteContract; /* ── 任务要素:四区(任务列表 / 标题区 / 工作区 / 对话区) ─────────────── */ /** 任务卡 —— 清单里一条 = 一个活动的一次运行,不是一整条工作流 */ export const taskCardSchema = z.object({ id: taskIdSchema, activity: z.string(), title: z.string(), runtime: taskRuntimeStateSchema, terminal: terminalStateSchema.optional(), /** 它在工作流上的序位 */ position: z.number().int().min(1), total: z.number().int().min(1), workflowSubject: z.string(), workflowState: workflowStateSchema, startedAt: z.string(), }); export type TaskCard = z.infer; export const listTasksRoute = { method: 'GET', path: '/api/desk/tasks', output: z.object({ tasks: z.array(taskCardSchema) }), } as const satisfies RouteContract; /** * 流程链上的一环 —— 「一条 = 一个活动的一次运行」之外,人还要看见自己在整条流程的哪儿。 * 别人负责的节点也在链上(只读),但不进任务清单。 */ export const chainNodeSchema = z.object({ nodeId: z.string(), label: z.string(), kind: z.enum(['activity', 'approval']), /** 谁做:姓名 · 岗位;岗位未落到具体人时只给岗位名 */ ownerName: z.string(), ownerRole: z.string(), state: z.enum(['done', 'current', 'todo']), }); export type ChainNode = z.infer; /** 任务详情 —— 工作区与对话区的服务端投影(前端零过程状态,刷新即重建) */ export const taskDetailSchema = z.object({ card: taskCardSchema, goal: z.string(), /** 本单负责人 —— 标题区落款与报告署名 */ owner: z.object({ name: z.string(), role: z.string() }), /** 整条流程的节点链(含他人节点,只读) */ chain: z.array(chainNodeSchema), budget: z.object({ steps: z.string(), tokens: z.string(), activeMinutes: z.string(), humanCalls: z.string(), }), stages: z.array(stageSchema), deliverables: z.array(deliverableSchema), /** * 工作成果 = 一组**文件**,大部分是 HTML 汇报报告。 * 一行一份、点进去看;文件本体随投影一并下发,前端不另取。 */ results: z.array( z.object({ label: z.string(), desc: z.string(), at: z.string().optional(), file: fileDocSchema, }), ), workOrders: z.array(workOrderSchema), chat: z.array(chatMessageSchema), view: z.array(viewBlockSchema), /** 活动推送的视图 HTML —— 容器沙箱渲染,无脚本执行权(AM 07) */ viewHtml: z.string().optional(), log: z.array( z.object({ id: z.string(), at: z.string(), side: attributionSideSchema, sideLabel: z.string(), title: z.string(), summary: z.string().optional(), }), ), }); export type TaskDetail = z.infer; export const taskDetailRoute = { method: 'GET', path: '/api/desk/tasks/:taskId', params: z.object({ taskId: taskIdSchema }), output: taskDetailSchema, } as const satisfies RouteContract; /** 回传工单 —— 与后端回传校验同 schema(§9.2) */ export const replyWorkOrderRoute = { method: 'POST', path: '/api/desk/work-orders/reply', body: z.object({ reply: z.record(z.string(), z.unknown()) }), output: z.object({ ok: z.boolean(), producesCompletion: z.boolean() }), } as const satisfies RouteContract; /** * 对话卡回传(决策 / 补全 / 评价)—— 与后端回传校验**同 schema**。 * 对话流是一切回环输入的唯一入口(OS 4.1):卡就长在对话流上,答也就地答。 */ export const replyChatCardRoute = { method: 'POST', path: '/api/desk/chat-cards/reply', body: z.object({ reply: z.record(z.string(), z.unknown()) }), output: z.object({ ok: z.boolean() }), } as const satisfies RouteContract; /** 负责人的自由输入 —— 人事件的第二形态 */ export const sayRoute = { method: 'POST', path: '/api/desk/tasks/:taskId/say', params: z.object({ taskId: taskIdSchema }), body: z.object({ text: z.string().min(1) }), output: chatMessageSchema, } as const satisfies RouteContract; /** 消息要素 */ /** 消息是**信息**,不产生责任;跨任务征答卡以系统消息送达,**就地作答** */ export const systemMessageSchema = z.object({ id: z.string(), kind: z.string(), title: z.string(), body: z.string(), at: z.string(), read: z.boolean(), /** 跨任务征答:卡本体与来源任务 —— 作答权限只及卡面,不开放任务对话流 */ card: z .object({ cardId: z.string(), sourceTaskId: z.string(), /** 按 schema 渲染的表单字段;与后端回传校验同 schema */ fields: z.array(formFieldSchema).default([]), ask: z.string().default(''), answered: z.boolean(), rejectedReason: z.string().optional(), supersededBy: z.string().optional(), }) .optional(), }); export type SystemMessageView = z.infer; export const listMessagesRoute = { method: 'GET', path: '/api/desk/messages', output: z.object({ messages: z.array(systemMessageSchema) }), } as const satisfies RouteContract; export const markMessageReadRoute = { method: 'POST', path: '/api/desk/messages/:messageId/read', params: z.object({ messageId: z.string() }), output: z.object({ ok: z.boolean() }), } as const satisfies RouteContract; /** 就地作答 —— 回传经统一入口写回来源任务,调用与回传全程在任务侧留痕 */ export const answerMessageRoute = { method: 'POST', path: '/api/desk/messages/:messageId/answer', params: z.object({ messageId: z.string() }), body: z.object({ answer: z.record(z.string(), z.unknown()) }), output: z.object({ ok: z.boolean() }), } as const satisfies RouteContract; export type ListMessagesOutput = z.infer; /* ── 纪要:工作台五要素里唯一的主动动作 ─────────────────────────────── */ export const minuteResolutionSchema = z.object({ id: z.string(), text: z.string(), ownerId: staffIdSchema.optional(), state: z.enum(['open', 'registered']), workflowId: z.string().optional(), registeredById: staffIdSchema.optional(), registeredAt: z.string().optional(), }); export const minuteSchema = z.object({ id: z.string(), title: z.string(), body: z.string(), authorId: staffIdSchema, authorName: z.string(), participants: z.array(z.string()), occurredOn: z.string(), resolutions: z.array(minuteResolutionSchema), createdAt: z.string(), updatedAt: z.string(), }); export type MinuteView = z.infer; export const listMinutesRoute = { method: 'GET', path: '/api/minutes', output: z.object({ minutes: z.array(minuteSchema) }), } as const satisfies RouteContract; export const createMinuteRoute = { method: 'POST', path: '/api/minutes', body: z.object({ title: z.string().min(1), body: z.string(), occurredOn: z.string().min(1), participants: z.array(staffIdSchema).default([]), resolutions: z.array(z.object({ text: z.string().min(1), ownerId: staffIdSchema.optional() })).default([]), }), output: minuteSchema, } as const satisfies RouteContract; export const appendResolutionRoute = { method: 'POST', path: '/api/minutes/:minuteId/resolutions', params: z.object({ minuteId: z.string() }), body: z.object({ text: z.string().min(1), ownerId: staffIdSchema.optional() }), output: minuteSchema, } as const satisfies RouteContract; /** 决议 → 登记起单:登记人即来源 */ export const registerResolutionRoute = { method: 'POST', path: '/api/minutes/resolutions/:resolutionId/register', 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() }), } as const satisfies RouteContract; /* ── 汇报:系统代劳的自动产出 ───────────────────────────────────────── */ export const reportPeriodSchema = z.enum(['day', 'week', 'month', 'year']); export type ReportPeriodName = z.infer; export const REPORT_PERIOD_LABEL: Readonly> = { day: '日报', week: '周报', month: '月报', year: '年报', }; export const reportMetricsSchema = z.object({ autonomy: z.number(), correction: z.number(), effectiveAutonomy: z.number(), interruption: z.number(), breakpoint: z.number(), dmnCoverage: z.number(), unitCost: z.object({ tokens: z.number(), minutes: z.number(), humanCalls: z.number() }), terminalMix: z.record(z.string(), z.number()), }); export const reportSchema = z.object({ id: z.string(), period: reportPeriodSchema, title: z.string(), window: z.object({ from: z.string(), to: z.string() }), sections: z.array( z.object({ taskId: z.string(), title: z.string(), activityId: z.string(), terminalLabel: z.string().optional(), lines: z.array(z.string()), }), ), metrics: reportMetricsSchema, counts: z.object({ touched: z.number().int(), released: z.number().int(), logLines: z.number().int() }), note: z.string().optional(), submittedAt: z.string().optional(), generatedAt: z.string(), }); export type ReportView = z.infer; /** 未提交的草案每次取回都重算 —— 汇报是投影,不是账本 */ export const draftReportRoute = { method: 'GET', path: '/api/reports/:period', params: z.object({ period: reportPeriodSchema }), output: reportSchema, } as const satisfies RouteContract; export const listReportsRoute = { method: 'GET', path: '/api/reports', output: z.object({ reports: z.array(reportSchema) }), } as const satisfies RouteContract; /** 确认即提交,提交后不再改 */ export const submitReportRoute = { method: 'POST', path: '/api/reports/:reportId/submit', params: z.object({ reportId: z.string() }), body: z.object({ note: z.string().optional() }), output: reportSchema, } as const satisfies RouteContract; export type ListMinutesOutput = z.infer; export type ListReportsOutput = z.infer; /* ── 审批:工作流层的审批点卡到审批人工位 ───────────────────────────── */ export const approvalItemSchema = z.object({ id: z.string(), workflowId: z.string(), workflowSubject: z.string(), nodeId: z.string(), title: z.string(), /** 依据快照 —— 调用必附上下文,裸调用不得发出 */ basis: z.array(z.object({ k: z.string(), v: z.string() })), ask: z.string(), state: z.enum(['pending', 'passed', 'rejected']), /** 驳回走向:声明式,不由临场决定 */ rejectRoute: z.string(), raisedAt: z.string(), }); export type ApprovalItem = z.infer; export const listApprovalsForMeRoute = { method: 'GET', path: '/api/approvals', output: z.object({ approvals: z.array(approvalItemSchema) }), } as const satisfies RouteContract; /** 通过 / 驳回,附依据 —— 驳回必附意见 */ export const decideApprovalRoute = { method: 'POST', path: '/api/approvals/:workflowId/:nodeId/decide', 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 }), } as const satisfies RouteContract; export type ListApprovalsForMeOutput = z.infer; /* ── 登记触发:人工起单的统一形态(OS 3.1) ─────────────────────────── */ /** * 「登记触发 —— 人工起单的统一形态:登记人选择流程包、按调用接口 schema 填写输入 * (表单由 schema 自动渲染)、提交即起单。」**登记人即来源**。 */ export const listRegistrationTriggersRoute = { method: 'GET', path: '/api/triggers/registration', output: z.object({ triggers: z.array( z.object({ id: z.string(), name: z.string(), processPackageId: z.string(), enabled: z.boolean(), /** 调用接口的入参 schema —— 登记表单据此渲染 */ inputSchema: z.record(z.string(), z.unknown()), }), ), }), } as const satisfies RouteContract; export const fireRegistrationRoute = { method: 'POST', path: '/api/triggers/:triggerId/register', params: z.object({ triggerId: z.string() }), body: z.object({ data: z.record(z.string(), z.unknown()) }), output: z.object({ ok: z.boolean(), workflowId: z.string().optional(), skipped: z.string().optional(), missing: z.array(z.string()).optional(), }), } as const satisfies RouteContract; export type ListRegistrationTriggersOutput = z.infer;