M6(4/n): 工作台五要素齐活 —— 审批 / 消息 / 纪要 / 汇报

四个 desk-* feature 自 workbench 原型 ModuleWorkspace.tsx 平移(jc-* 类名原样),
共用件落 shared/desk/module-kit;mock 数据换成服务端投影:
  · desk-approval:审批点卡到审批人工位;依据快照、驳回走向(声明式)、驳回必附意见
  · desk-message:通知 + 跨任务征答**就地作答**;卡面表单由 card.ts schema 驱动,
    与后端回传校验同 schema;跨人卡不占任务清单,不开放任务对话流
  · desk-minute:纪要维护 + 决议登记起单;正文可续、决议只增
  · desk-report:四周期草案 + 度量八指标 + 确认即提交
左轨角标常驻(待批 / 未读),责任面要一眼看见什么在等我。

修一个真 bug:process.start 从不校验流程包的 callInterface,
先插工作流再由 task.start 撞活动契约 —— 失败就留下一条永远推不动的在途单。
改为**先校验后建单**(CallInterfaceViolationError),一条工作流都不留;
路由回 400 并带上缺哪个字段。已清掉这次触发出的两条孤儿单。

判据 ⑩ 纪要一环跑通:决议 → 登记触发 → 工作流 → A1 落到 BD 冯兵,
登记人即来源已记在单上;同一决议重复登记被 412 挡下。

gates: tsc / eslint / depcruise(316 模块)/ api-report / 380 测试 全绿

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
This commit is contained in:
2026-07-27 03:53:36 +08:00
co-authored by Claude Opus 5
parent a60acb2e02
commit 3b9587563a
25 changed files with 1680 additions and 49 deletions
+79 -13
View File
@@ -5,7 +5,7 @@
import { z } from 'zod';
import type { RouteContract } from './http.ts';
import { sessionSchema } from '../org.ts';
import { chatMessageSchema, deliverableSchema, stageSchema, viewBlockSchema, workOrderSchema } from '../card.ts';
import { chatMessageSchema, deliverableSchema, formFieldSchema, stageSchema, viewBlockSchema, workOrderSchema } from '../card.ts';
import { attributionSideSchema, taskRuntimeStateSchema, terminalStateSchema, workflowStateSchema } from '../terminal.ts';
import { staffIdSchema, taskIdSchema } from '../id.ts';
@@ -102,23 +102,54 @@ export const sayRoute = {
} 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<typeof systemMessageSchema>;
export const listMessagesRoute = {
method: 'GET',
path: '/api/desk/messages',
output: z.object({
messages: z.array(
z.object({
id: z.string(),
kind: z.string(),
title: z.string(),
body: z.string(),
at: z.string(),
read: z.boolean(),
}),
),
}),
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<typeof listMessagesRoute.output>;
/* ── 纪要:工作台五要素里唯一的主动动作 ─────────────────────────────── */
export const minuteResolutionSchema = z.object({
@@ -251,3 +282,38 @@ export const submitReportRoute = {
export type ListMinutesOutput = z.infer<typeof listMinutesRoute.output>;
export type ListReportsOutput = z.infer<typeof listReportsRoute.output>;
/* ── 审批:工作流层的审批点卡到审批人工位 ───────────────────────────── */
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<typeof approvalItemSchema>;
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<typeof listApprovalsForMeRoute.output>;