@yss123/cage-cli 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/approval-tree.d.ts +88 -0
  2. package/dist/approval-tree.js +18 -0
  3. package/dist/approval-tree.js.map +1 -0
  4. package/dist/capabilities.js +16 -1
  5. package/dist/capabilities.js.map +1 -1
  6. package/dist/graph-approvals.d.ts +11 -1
  7. package/dist/graph-approvals.js +18 -5
  8. package/dist/graph-approvals.js.map +1 -1
  9. package/dist/graph-relations.js +10 -8
  10. package/dist/graph-relations.js.map +1 -1
  11. package/dist/graph.js +2 -0
  12. package/dist/graph.js.map +1 -1
  13. package/dist/ontology.js +8 -5
  14. package/dist/ontology.js.map +1 -1
  15. package/dist/sync.js +1 -1
  16. package/graph-ui/approval-flow.js +28 -8
  17. package/graph-ui/approval-parallel.js +110 -0
  18. package/graph-ui/approval-tree.js +17 -0
  19. package/graph-ui/business.css +26 -0
  20. package/graph-ui/business.js +30 -13
  21. package/graph-ui/graph-approvals.js +18 -5
  22. package/graph-ui/graph-relations.js +10 -8
  23. package/package.json +6 -6
  24. package/templates/app/AGENTS.md +2 -2
  25. package/templates/app/README.md +5 -4
  26. package/templates/app/apps/api/package.json +2 -2
  27. package/templates/app/apps/web/package.json +1 -1
  28. package/templates/app/apps/web/src/modules/approval/components/task-operation-dialog.tsx +61 -0
  29. package/templates/app/apps/web/src/modules/approval/hooks/use-approval-tasks.ts +6 -9
  30. package/templates/app/apps/web/src/modules/approval/pages/approval-tasks-page.tsx +81 -49
  31. package/templates/app/apps/web/src/modules/approval/pages/approval-tasks.css +18 -0
  32. package/templates/app/apps/web/src/modules/purchase/components/approval-history.tsx +39 -4
  33. package/templates/app/apps/web/src/modules/purchase/components/order-reviewers.tsx +38 -0
  34. package/templates/app/apps/web/src/modules/purchase/components/purchase-order-form.tsx +9 -2
  35. package/templates/app/apps/web/src/modules/purchase/pages/purchase-intelligence.css +2 -0
  36. package/templates/app/apps/web/src/modules/purchase/pages/purchase-order-detail-page.tsx +2 -0
  37. package/templates/app/apps/web/src/modules/purchase/pages/purchase-orders-page.tsx +2 -0
  38. package/templates/app/apps/web/src/shared/lib/error-message.ts +6 -1
  39. package/templates/app/apps/web/src/shared/ui/approval-member-picker.css +10 -0
  40. package/templates/app/apps/web/src/shared/ui/approval-member-picker.tsx +51 -0
  41. package/templates/app/cage.config.json +14 -0
  42. package/templates/app/db/migrations/0004_draft.sql +27 -0
  43. package/templates/app/docs/ai/README.md +7 -2
  44. package/templates/app/docs/ai/approvals.md +260 -0
  45. package/templates/app/docs/ai/primitives.md +1 -1
  46. package/templates/app/docs/ai/purchase-example.md +19 -5
  47. package/templates/app/docs/ai/recipes.md +1 -1
  48. package/templates/app/docs/errors/CAGE-AI-001.md +2 -2
  49. package/templates/app/docs/errors/CAGE-APPR-001.md +12 -9
  50. package/templates/app/docs/errors/CAGE-APPR-005.md +4 -4
  51. package/templates/app/docs/errors/CAGE-APPR-006.md +24 -9
  52. package/templates/app/docs/errors/CAGE-APPR-007.md +8 -6
  53. package/templates/app/docs/errors/CAGE-EVENT-001.md +1 -1
  54. package/templates/app/docs/errors/CAGE-REG-001.md +1 -1
  55. package/templates/app/docs/errors/README.md +2 -2
  56. package/templates/app/package.json +3 -3
  57. package/templates/app/packages/contracts/package.json +1 -1
  58. package/templates/app/packages/contracts/src/aggregates/approval-member/index.ts +70 -0
  59. package/templates/app/packages/contracts/src/aggregates/purchase-order/index.ts +10 -2
  60. package/templates/app/packages/contracts/src/index.ts +9 -0
  61. package/templates/app/packages/domain/package.json +5 -5
  62. package/templates/app/packages/domain/src/modules/purchase/approval-members.ts +54 -0
  63. package/templates/app/packages/domain/src/modules/purchase/approvals.ts +19 -8
  64. package/templates/app/packages/domain/src/modules/purchase/index.ts +2 -1
  65. package/templates/app/packages/domain/src/modules/purchase/intelligence.test.ts +169 -7
  66. package/templates/app/packages/domain/src/modules/purchase/purchase-order.ts +56 -5
  67. package/templates/blank/AGENTS.md +2 -2
  68. package/templates/blank/apps/api/package.json +1 -1
  69. package/templates/blank/apps/web/package.json +1 -1
  70. package/templates/blank/docs/ai/README.md +7 -2
  71. package/templates/blank/docs/ai/approvals.md +260 -0
  72. package/templates/blank/docs/ai/primitives.md +1 -1
  73. package/templates/blank/docs/ai/purchase-example.md +19 -5
  74. package/templates/blank/docs/ai/recipes.md +1 -1
  75. package/templates/blank/docs/errors/CAGE-AI-001.md +2 -2
  76. package/templates/blank/docs/errors/CAGE-APPR-001.md +12 -9
  77. package/templates/blank/docs/errors/CAGE-APPR-005.md +4 -4
  78. package/templates/blank/docs/errors/CAGE-APPR-006.md +24 -9
  79. package/templates/blank/docs/errors/CAGE-APPR-007.md +8 -6
  80. package/templates/blank/docs/errors/CAGE-EVENT-001.md +1 -1
  81. package/templates/blank/docs/errors/CAGE-REG-001.md +1 -1
  82. package/templates/blank/docs/errors/README.md +2 -2
  83. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/components/task-operation-dialog.tsx +61 -0
  84. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/hooks/use-approval-tasks.ts +6 -9
  85. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/pages/approval-tasks-page.tsx +81 -49
  86. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/pages/approval-tasks.css +18 -0
  87. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/components/approval-history.tsx +39 -4
  88. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/components/order-reviewers.tsx +38 -0
  89. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/components/purchase-order-form.tsx +9 -2
  90. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/pages/purchase-intelligence.css +2 -0
  91. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/pages/purchase-order-detail-page.tsx +2 -0
  92. package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/pages/purchase-orders-page.tsx +2 -0
  93. package/templates/blank/docs/examples/purchase-admin/apps/web/src/shared/lib/error-message.ts +6 -1
  94. package/templates/blank/docs/examples/purchase-admin/apps/web/src/shared/ui/approval-member-picker.css +10 -0
  95. package/templates/blank/docs/examples/purchase-admin/apps/web/src/shared/ui/approval-member-picker.tsx +51 -0
  96. package/templates/blank/docs/examples/purchase-admin/cage.config.json +14 -0
  97. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/aggregates/approval-member/index.ts +70 -0
  98. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/aggregates/purchase-order/index.ts +10 -2
  99. package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/index.ts +9 -0
  100. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/approval-members.ts +54 -0
  101. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/approvals.ts +19 -8
  102. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/index.ts +2 -1
  103. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/intelligence.test.ts +169 -7
  104. package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/purchase-order.ts +56 -5
  105. package/templates/blank/package.json +3 -3
  106. package/templates/blank/packages/contracts/package.json +1 -1
  107. package/templates/blank/packages/domain/package.json +5 -5
@@ -1,7 +1,11 @@
1
- // 采购单声明集中列出状态、范围、不变量、命令和事件。
2
- // handler 只读声明的依赖;数据库写入、版本检查、审计由内核完成。
1
+ /**
2
+ * 采购单声明集中列出申请材料、专业复核名单、状态、范围、命令和事件,由 API、worker 和生成器统一消费。
3
+ * 草稿可编辑专业复核名单,提交重新核对协作名录;handler 仅读取声明依赖,数据库写入、版本与审计由内核完成。
4
+ * 审批结论仍以 process 命令执行职责分离、额度和库存检查;选入名录或路由不授予权限。
5
+ */
3
6
  import {
4
7
  aggregate,
8
+ bool,
5
9
  ceiling,
6
10
  command,
7
11
  emit,
@@ -17,9 +21,11 @@ import {
17
21
  sod,
18
22
  states,
19
23
  text,
24
+ type ReadRepo,
20
25
  } from '@yss123/cage-kernel';
21
26
  import { z } from 'zod';
22
27
  import { Inventory } from '../inventory/index.js';
28
+ import { ApprovalMember } from './approval-members.js';
23
29
 
24
30
  /**
25
31
  * 采购单生命周期:草稿提交后等待审批,通过后等待外部收货;退回修改重新进入草稿。
@@ -46,6 +52,10 @@ export const PurchaseOrder = aggregate('PurchaseOrder', {
46
52
  amount: money(),
47
53
  /** 可为空的用途说明;创建可先存草稿,AI 预审会判断是否足以支撑审批。 */
48
54
  reason: text(2000).optional(),
55
+ /** 是否增加专业会签;旧记录 null/缺省按 false,不改变已经启动的审批实例。 */
56
+ needsSpecialistReview: bool().optional(),
57
+ /** 提交快照的专业复核用户 UUID;旧记录可为空,启用复核时必须为 1–5 个不同且非创建人的有效名录用户。 */
58
+ reviewerIds: list(id()).optional(),
49
59
  /** 申请明细快照:sku 为物料编码,qty 为整数件数;输入额外要求每行数量为正。 */
50
60
  items: list({ sku: text(64), qty: int() }),
51
61
  /** ERP 成功响应后回填的外部单号,尚未同步时为空;不作为采购单生命周期状态。 */
@@ -76,6 +86,22 @@ export const OrderReceived = event('OrderReceived', { orderId: id() });
76
86
  /** 命令输入比持久化 int 原语更具体:至少一行且每行数量为正,避免零数量申请进入审批。 */
77
87
  const itemsInput = z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })).min(1);
78
88
 
89
+ /**
90
+ * 在设置和提交时核对专业复核名单,防止界面被绕过、名录退役或重复账号减少会签人数。
91
+ * disabled/范围/批准能力仍由审批运行时处理;此处只按当前命令声明的名录读依赖判断业务成员 active。
92
+ * 关闭专业复核必须传空名单;旧记录 null 等价 false/[]。最多五人使逐人查询有明确上限。
93
+ */
94
+ async function validateReviewers(needsReview: boolean | null | undefined, ids: string[] | null | undefined, creator: string, members: ReadRepo<typeof ApprovalMember>) {
95
+ const selected = ids ?? [];
96
+ if (!needsReview) return selected.length ? fail('REVIEWERS_NOT_REQUIRED', '未启用专业复核时请清空复核人员') : undefined;
97
+ if (selected.length < 1 || selected.length > 5) return fail('REVIEWERS_REQUIRED', '专业复核需选择 1–5 位协作人');
98
+ if (new Set(selected).size !== selected.length) return fail('DUPLICATE_REVIEWERS', '同一人不能重复承担专业会签');
99
+ if (selected.includes(creator)) return fail('REVIEWER_IS_APPLICANT', '提单人不能担任本单专业复核人');
100
+ for (const userId of selected) {
101
+ if (!await members.findOne({ userId, status: 'active' })) return fail('REVIEWER_UNAVAILABLE', '所选专业复核人不在有效协作名录中');
102
+ }
103
+ }
104
+
79
105
  /**
80
106
  * 保存草稿;金额格式、字段长度和不变量由内核随后统一校验,handler 无额外副作用。
81
107
  * deptId 可指定但仍须满足创建范围;attachments 为已完成上传 ID,省略时初始化为空集合。
@@ -89,10 +115,28 @@ export const create = command(PurchaseOrder, {
89
115
  items: itemsInput,
90
116
  supplierPhone: z.string().optional(),
91
117
  reason: z.string().max(2000).optional(),
118
+ needsSpecialistReview: z.boolean().optional(),
119
+ reviewerIds: z.array(z.string().uuid()).max(5).optional(),
92
120
  deptId: z.string().uuid().optional(),
93
121
  attachments: z.array(z.string().uuid()).max(10).optional(),
94
122
  }),
95
- handler: (o) => o,
123
+ // 草稿允许先保存未选完的复核需求;提交才要求完整名单,新记录统一 false/[],旧记录兼容 null。
124
+ handler: (o) => ({ ...o, needsSpecialistReview: o.needsSpecialistReview ?? false, reviewerIds: o.reviewerIds ?? [] }),
125
+ });
126
+
127
+ /**
128
+ * 在 draft 中调整本单专业复核需求和名单;需要 PurchaseOrder.setReviewers,不能直接修改待审批快照。
129
+ * 输入保存前核对 1–5 位不同且非提单人的 active 名录成员;关闭时用 false/[],失败整次回滚。
130
+ * 提交还会重新查询名录,以拒绝保存后退役的成员;名录不授予对单据的读取、批准能力或额度。
131
+ */
132
+ export const setReviewers = command(PurchaseOrder, {
133
+ name: 'setReviewers', description: '设置采购单专业复核需求和人员', from: 'draft', to: null,
134
+ input: z.object({ id: z.string().uuid(), needsSpecialistReview: z.boolean(), reviewerIds: z.array(z.string().uuid()).max(5) }),
135
+ reads: { members: ApprovalMember },
136
+ handler: async (draft, input, { members }) => {
137
+ const invalid = await validateReviewers(input.needsSpecialistReview, input.reviewerIds, draft.createdBy, members);
138
+ return invalid ?? { needsSpecialistReview: input.needsSpecialistReview, reviewerIds: input.reviewerIds };
139
+ },
96
140
  });
97
141
 
98
142
  // 退回后的说明只在草稿状态修改,保留提交时的版本校验和原始审计。
@@ -119,12 +163,19 @@ export const setAttachments = command(PurchaseOrder, {
119
163
  handler: (_draft, input) => ({ attachments: input.attachments }),
120
164
  });
121
165
 
122
- /** 提交只改变状态并记录 OrderSubmitted;审批实例由事件消费启动,HTTP 成功不代表审批已完成。 */
166
+ /**
167
+ * 提交前重查专业复核名单,避免草稿保存后退役的成员进入新流程;缺省旧字段按无需复核处理。
168
+ * 成功只改变状态并记录 OrderSubmitted,实例由 worker 消费后创建;HTTP 成功不表示审批完成。
169
+ */
123
170
  export const submit = command(PurchaseOrder, {
124
171
  from: 'draft',
125
172
  to: 'pending_approval',
126
173
  emits: [OrderSubmitted],
127
- handler: (o) => (o.items.length === 0 ? fail('NO_ITEMS') : o),
174
+ reads: { members: ApprovalMember },
175
+ handler: async (o, _input, { members }) => {
176
+ if (o.items.length === 0) return fail('NO_ITEMS');
177
+ return await validateReviewers(o.needsSpecialistReview, o.reviewerIds, o.createdBy, members) ?? o;
178
+ },
128
179
  });
129
180
 
130
181
  /**
@@ -10,7 +10,7 @@
10
10
 
11
11
  本节位于托管区之外,cage sync 会保留。下面的托管区提供框架工作方法。
12
12
 
13
- <!-- cage:begin v0.3.0 -->
13
+ <!-- cage:begin v0.3.1 -->
14
14
  ## Cage 业务开发约定(框架托管)
15
15
 
16
16
  ### 先读,再按需求改业务
@@ -85,7 +85,7 @@
85
85
  - 多页面业务必须有独立 URL,不用 useState/tab 模拟整页导航;详情从 URL 取 ID 并独立加载。前端路由集中在 app,页面从模块公开入口接入 Outlet。登录后保留原路径/query/hash,401 回登录,403 展示权限提示;/admin/ 使用普通链接进入独立系统管理。前端路由不替代后端授权。实现、SPA 部署回退和浏览器配方见 docs/ai/routing.md。
86
86
  - 跨聚合写入通过 reaction 调用命令。`tx: 'same'` 共同提交/回滚,默认 outbox 异步执行。资源 acquires/releases 是图规则标签,实际占用/释放仍由命令完成;离开持有状态要检查每条释放路径。
87
87
  - 批量调用同一命令使用 reactionEach,声明 maxItems、唯一排序字段 orderBy、items 和 map;稳定键排序后串行执行。步骤依赖用 order,同事件同事务分组不得混用有序/无序或重复顺序。保持同事务回滚边界,见 docs/ai/primitives.md。
88
- - 审批通过 approval + route 配置,结论是 process 命令;选中审批人不等于已具备能力、范围和额度。第三方 IO 通过连接器,回调验签后调用 external 命令。
88
+ - 审批通过 approval + route 配置,结论是 process 命令;选中审批人不等于已具备能力、范围和额度。parallel(title, ...branch(...)) 声明结构化 AND:分支同时开始,内部顺序独立,全部活动分支完成后汇合;when 支持嵌套并仍按提交快照求值,声明有深度/数量边界,不支持任意跳转。currentSteps 表示当前活动节点集合,兼容字段 currentStep 只是最小活动索引,不能据此隐藏其他分支。人工节点 assignment: 'candidate' 开放候选领取/释放,approver.field 从 routable 字段选人;actions 开放转办、委派归还和前后/并行必签。任务操作与决定携带 revision,waiting 任务只能查看。approvalOperations() 为框架通用时效运行时:step.sla 配置自然分钟节点总时限,route.reminders 配置申请人催办;迁移并运行 worker 后提供持久站内提醒、已读与 Admin 通用页面,不自动批准或授予权限。政策、SDK 和边界见 docs/ai/approvals.md。第三方 IO 通过连接器,回调验签后调用 external 命令。
89
89
  - job 的读取与调用必须列入 reads/calls。业务导入/导出放在需要的业务页面调用通用任务接口,202 只代表任务已接受;worker 必须运行。
90
90
  - 已部署事件只做兼容扩展;新增字段设为 optional,不兼容含义用新事件。已执行/已发布迁移不能改写,结构更新新增迁移。
91
91
  - 不手改生成物,不关闭门禁掩盖错误,不因方便自动授予生产角色新能力。unsafe 必须写明 reason/owner,不能替代普通业务声明。
@@ -10,6 +10,6 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@app/domain": "workspace:*",
13
- "@yss123/cage-http": "0.3.0"
13
+ "@yss123/cage-http": "0.3.1"
14
14
  }
15
15
  }
@@ -10,7 +10,7 @@
10
10
  "dependencies": {
11
11
  "@app/contracts": "workspace:*",
12
12
  "@tanstack/react-query": "^5.87.1",
13
- "@yss123/cage-client": "0.3.0",
13
+ "@yss123/cage-client": "0.3.1",
14
14
  "react": "^19.1.1",
15
15
  "react-dom": "^19.1.1",
16
16
  "react-markdown": "^10.1.0",
@@ -29,6 +29,7 @@
29
29
  | 开发前 | [本体查询与 AI 上下文](ontology-context.md) | 按需求查哪些代码?为什么关联?候选、证据与子图边界怎么判断? |
30
30
  | 开发与排错 | [能力发现与状态诊断](diagnostics.md) | 公开 API 有什么?注册/契约/进程哪里过期?如何查执行时间线和授权拒绝? |
31
31
  | 按需 | [对象关系](relations.md) | 如何在 fields 中声明一对一、一对多、多对多,修改关联并分页读取? |
32
+ | 复杂审批 | [审批流转、并行与流程时效](approvals.md) | 如何声明 AND 并行与汇合,按字段选人、候选领取、转办/委派/加签,并使用框架节点时限、站内提醒、催办和已读? |
32
33
  | 前端必读 | [业务路由与浏览器验收](routing.md) | 列表/详情如何获得独立 URL?登录回跳、404、/admin 和部署回退怎样验收? |
33
34
  | 原生 AI | [智能助理与 AI 审批](intelligence.md) | 如何启用后端、配置三态审核、生成工具、传入页面快照和接入 SDK? |
34
35
  | 4 | [标准采购范例导读](purchase-example.md) | 一次提交如何进入审批、锁库存、推 ERP、释放资源? |
@@ -55,7 +56,9 @@
55
56
  | 增加操作按钮 | 领域命令 + `apps/web/src/modules/<业务>` | 状态迁移、调用身份、错误展示、提交幂等键 |
56
57
  | 增加列表、详情或新页面 | 模块 pages + app 路由表 | URL、查询条件、深链接、刷新、前进后退、登录回跳;见[路由指南](routing.md) |
57
58
  | 增加报表 | 所属领域模块 `queries.ts` | `reads`、`safe.*`、参数化、调用人 read 权限 |
58
- | 修改审批顺序/条件/选人 | 所属领域模块 `approvals.ts` | `routable`、角色/负责人数据、结论命令约束 |
59
+ | 修改审批顺序/条件/并行/选人 | 所属领域模块 `approvals.ts` | `parallel/branch` 的 AND 汇合与完整 currentSteps、`routable`、`approver.field` 的 UUID/数组、角色/负责人数据、同租户 active 用户及结论命令约束;见[任务流转](approvals.md) |
60
+ | 候选领取、转办、委派和前后加签 | 人工 `step` 的 `assignment/actions` + 客户端任务接口 | 候选规则与权限实时检查、waiting 依赖、任务 revision、旧实例快照、系统迁移;Admin/采购已有领取、释放、归还,采购还提供协作名录选人、转办/委派/三种加签及分页人工历史 |
61
+ | 节点时限、流程提醒与申请人催办 | `step.sla`、`route.reminders` + 导出的 `approvalOperations()` | 自然分钟、逐节点开启计时、同实例共享催办冷却;并行催办携带 expectedSteps;沿用框架 Admin 申请/提醒页或 SDK,见[流程时效](approvals.md) |
59
62
  | AI 预审、退回修改、转人工 | 现有 `step` 的 `kind: 'ai'` + `approval` 绑定 | 策略、取证字段/查询、`automatedBy` 角色、结论命令及 `handoff`;见[智能化指南](intelligence.md) |
60
63
  | 业务助理、随页悬浮窗 | domain 导出 `ai()`;页面接 `useAssistant` 与 `useAiPageContext` | 业务权限、页面快照、流式 Markdown、写入确认;界面由应用实现 |
61
64
  | 模型动态配置、Responses、性格 | `managedModel` 与框架 Admin 页面 | 部署加密密钥、管理权限、实际网关能力、性格仅影响表达 |
@@ -86,7 +89,9 @@
86
89
  | 本体抽取、中文检索/节点/路径/AI 上下文、图规则、图谱、生成、影响分析与门禁 | 上下文注明证据、源码和覆盖边界;名称与测试匹配是候选线索,不能证明任意 JS 正确;跳过用例仍需补足 |
87
90
  | 有界集合 reactionEach、显式步骤 order | 稳定唯一键排序后串行调用目标命令;同事务整体回滚,异步链按 outbox 恢复;不是并行写表接口 |
88
91
  | capabilities、doctor、timeline、explain、snapshot | 实际运行诊断需要受控会话与权限;未取得的证据为 unknown,授权解释不执行 handler |
89
- | 代码审批路由与适配接口 | 顺序步骤、条件步骤、步骤内任一/全部审批;没有通用可视化流程设计器、任意 JS 选人、转签/加签 API |
92
+ | 代码审批路由与任务流转 | 顺序/条件、`parallel/branch` 结构化 AND 分支与汇合、any/all、字段选人、候选领取/释放、转办、单层委派归还、前后/并行必签和动作历史;候选池仅支持 any,追加票全部必签。条件和依赖随提交冻结,currentStep 仅为最小活动索引;尚无任意连线/循环/OR 网关、岗位/代理规则、通用恢复、任意 JS 选人或在途迁移,见[任务流转](approvals.md) |
93
+ | 审批时限、站内提醒与催办 | 人工节点按实际开启时间计算自然分钟 SLA,worker 持久投递临期/超时提醒;申请人可按完整活动集合催办,同实例共享冷却,接收人可标已读。旧快照不自动获得政策;尚无工作日历、自动升级、抄送或邮件/短信通知 |
94
+ | 审批界面与 SDK | Admin/采购待办可领取、释放、归还受托任务并查看等待加签;采购另有协作名录选人器、转办/委派/三种加签及分页人工历史,view 查询参数保留视图。启用运营后通用 Admin 提供我的申请、催办和流程提醒;页面仍需应用启用、真实授权与浏览器验收。AI 助理目前只查询本人待办及提交决定,无人员管理写工具 |
90
95
  | 原生 AI 审批与领域助理 | 现有 `step` 支持 `kind: 'ai'`;三态审核、自动工具、会话及偏好记忆可复用;需显式启用、授权并做模型质量验收 |
91
96
  | Admin 模型与性格、助理 SDK | 动态模型连接和性格管理;Chat Completions / Responses 兼容性以实际服务为准;SDK 提供流式状态与页面快照,悬浮窗和 Markdown 由应用实现 |
92
97
  | 连接器 fake、出站、入站验签、同步记录 | fake 不等于真实 ERP 联调,也不等于完整请求录制/回放平台 |
@@ -0,0 +1,260 @@
1
+ # 审批任务流转、并行分支与流程时效
2
+
3
+ [返回学习入口](README.md) · [审批原语](primitives.md) · [AI 审批](intelligence.md)
4
+
5
+ 本页描述 Cage 原生审批引擎的顺序/条件/结构化 AND 并行、字段选人、候选领取/释放、转办、委派归还、前后/并行必签、操作记录,以及框架通用的自然分钟时限、站内提醒和申请人催办。它参考 Flowable 的任务责任划分和并行汇合语义,不表示支持全部 BPMN 或与 Flowable XML 兼容。
6
+
7
+ ## 声明选人和节点操作
8
+
9
+ 人工步骤默认 `assignment: 'direct'`,为每位选中的人分别建立已分配任务。`mode: 'any'` 表示至少一位原审批人通过,`mode: 'all'` 表示全部原审批人通过。转办、委派和加签通过 `actions` 逐项开放,未写的动作关闭。
10
+
11
+ 以下是局部步骤片段,需放进应用已经导出的 `route`,并与自己的 `approval` 绑定配合:
12
+
13
+ ```ts
14
+ import { approver, step } from '@yss123/cage-approval';
15
+
16
+ // 为角色中的每位审批人直派一票;任一原票通过后仍需等待所有追加的必签票。
17
+ step('部门审核', approver.role('reviewer'), {
18
+ mode: 'any',
19
+ actions: { transfer: true, delegate: true, addSign: true },
20
+ });
21
+
22
+ // reviewers 必须是本聚合的公开字段并列入 approval.routable;领取成功不产生新的业务权限。
23
+ step('专业审核', approver.field('reviewers'), {
24
+ assignment: 'candidate',
25
+ actions: { transfer: true, delegate: true, addSign: true },
26
+ });
27
+ ```
28
+
29
+ `assignment: 'candidate'` 只创建一项 `assigneeId: null` 的共享任务,候选人必须先领取才能决定。它独立开放 `claim/unclaim`,无需在 `actions` 中增加字段;即使没有其他动作政策也能领取/释放。候选池只支持 `any`,`candidate + all` 在声明时拒绝;逐人会签使用默认的 `direct`。
30
+
31
+ 流程启动时保存选人规则、分配方式和动作政策的步骤快照。新 `step` 在 `actions.addSign: true` 时自动记录 `signPositions: ['parallel', 'before', 'after']`,应用无需另传签序配置。修改代码不会改变在途实例政策,也不会把旧直派任务合并为候选池。角色和部门规则在候选列表及领取时查询当前组织数据,字段选人使用提交时保存的路由变量;规则快照不等于冻结一份候选成员名单。AI 步骤的人工接管仍使用原来的直派方式,不开放任务管理动作。
32
+
33
+ ### 按单据字段选人
34
+
35
+ `approver.field('reviewers')` 支持单个 UUID 字符串或原数组长度为 1–200 的 UUID 数组,重复人员去重。字段必须存在、不是 hidden,并列入对应 `approval.routable`;不满足时声明检查报 `CAGE-APPR-001`。数组可用 `list(id())` 声明,单人可用已有的 UUID 或用户引用字段;选人器不执行任意 JS、SQL 或字符串表达式。
36
+
37
+ 字段缺失或为 `null` 时解析为空候选,开启节点报 `CAGE-APPR-007`;空数组、非法 UUID、数组中包含非字符串或超过 200 项报 `INVALID_INPUT`。开启字段选人或候选节点时,所有选中的 ID 必须属于当前租户且账号 active,否则整批失败,不静默去掉无效人员或默认批准。旧的固定用户/部门负责人直派规则保持原有开启行为。
38
+
39
+ 开启节点的账号校验不会替用户授予批准/read 能力或数据范围。领取、转办、委派、加签和实际决定仍按当时的权限核验;配置了有效用户也不代表其最终额度、职责分离等业务硬约束一定满足。
40
+
41
+ ## 结构化并行分支与 AND 汇合
42
+
43
+ `parallel(title, ...branches)` 声明同时推进的分支组,`branch(title, ...nodes)` 声明其中一条具名分支。分支内部可以有多个顺序步骤、条件和嵌套并行组;所有实际展开的分支都完成后,才开启组后面的节点。下例是应用路由片段,需要从 domain 入口导出,并绑定自己的 `approval`;`requiresLegal` 必须列入该绑定的 `routable`,角色与相应业务权限由应用明确配置。
44
+
45
+ ```ts
46
+ import { approver, branch, eq, parallel, route, step, when } from '@yss123/cage-approval';
47
+
48
+ /**
49
+ * 材料审核后同时推进业务与专业两条线;专业线的法务条件在提交时求值。
50
+ * 各线均完成才创建最终确认待办,结论仍由 approval 绑定的领域命令落实。
51
+ */
52
+ export const documentRoute = route('document-review', [
53
+ step('材料审核', approver.role('reviewer')),
54
+ parallel('联合审查',
55
+ branch('业务线',
56
+ step('业务复核', approver.role('business-reviewer')),
57
+ step('预算核对', approver.role('budget-reviewer')),
58
+ ),
59
+ branch('专业线',
60
+ step('专业评估', approver.role('specialist')),
61
+ when(eq('requiresLegal', true),
62
+ step('法务审查', approver.role('legal-reviewer')),
63
+ ),
64
+ ),
65
+ ),
66
+ step('最终确认', approver.role('final-reviewer')),
67
+ ]);
68
+ ```
69
+
70
+ 业务复核通过后即可进入预算核对,无需等待专业评估;专业线也独立前进。只有两条线均通过,最终确认才出现。节点内 `mode: 'all'` 是同一节点的多人会签,`addSign` 的 `placement: 'parallel'` 是同一节点的追加必签,两者都不会创建流程分支;它们可以继续用于某条分支中的人工节点。
71
+
72
+ 条件只在实例启动时依据提交变量求值,执行中不重新读取单据来改变路线。声明中的容器不能为空;如果某条分支的全部条件均不满足,展开后该分支直接通过汇合,不生成虚假任务。如果整个路由没有展开出任何办理节点,则明确拒绝启动。每个并行组要求 2–16 条名称不重复的分支;整份声明最多 128 个人工/AI 办理节点、512 个结构节点和 8 层容器。条件不满足不会绕过声明结构校验。
73
+
74
+ 拒绝、退回或取消结束整个审批轮次,关闭其他分支中的活动节点和 pending/waiting 任务;尚未开启的后续节点不再创建。正常推进在实例锁内检查持久前驱状态,节点唯一键防止并发分支重复创建同一汇合后继。若同一次启动或推进中的任一新节点无法开启,整个本次事务回滚,不留下部分成功的分支入口。已经在较早事务中通过的票仍保留。
75
+
76
+ 最终业务结论仍以实际推进完成的办理人身份调用领域命令。并行组后没有明确的最终审批节点时,最后完成分支的人可能承担该身份;额度、职责分离或状态约束不满足会回滚本次决定。需要固定最终责任人时,应像上例一样在 AND 汇合后声明明确的最终节点。
77
+
78
+ ### 快照、历史和界面定位
79
+
80
+ 执行计划保存在既有 `flow.approval_instance.steps` JSON 中,每个叶子步骤附带 `execution.schema/dependsOn/branchPath/definitionVersion`。`dependsOn` 是本实例内的前驱索引,全部前驱 approved 才能开启当前节点;数组中的声明遍历顺序不是实际完成顺序。`branchPath` 按外层到内层记录并行组及分支名称,普通主线为空数组。无需为这次并行能力新增 DDL 迁移;仍需完成当前版本已发布的 AI、任务流转和时效迁移,并让 API/worker 使用同一版本。
81
+
82
+ `definitionVersion` 是流程名、路由节点和催办政策等路由声明的 SHA256 内容指纹,不包含领域命令实现。它用于识别实例启动时保存的路线,不能替代完整的定义发布、版本回滚、领域命令版本绑定或在途迁移。代码更新不重算已有实例的依赖和政策;没有 `execution` 的历史快照仍按相邻顺序执行,历史版本指纹为 null,不追溯伪造。
83
+
84
+ 任务摘要提供 `branchPath`;`client.approvalHistory(aggregate, id)` 提供 `currentSteps`、`definitionVersion` 及各步骤的 `branchPath/dependsOn`,仍遵守当前单据与详情读取权限。`currentSteps` 是完整活动节点集合,包含排队、运行、人工接管和待诊断的失败节点;只有通过状态满足后继依赖。`currentStep` 只保留最小活动索引作为旧客户端摘要,不能代表整轮只有一个节点,也不能据此判断其他分支是否前进。申请列表另有 `activeSteps`,提供各活动节点的名称、截止时间和分支路径。
85
+
86
+ `cage graph` 递归展示条件和 AND 分叉/汇合;节点编号只定位声明,图中分列的分支才表达并发关系。它展示已声明的结构,不是运行时设计器,也不直接证明某个实例已完成。当前只支持这种有界结构树,不支持任意连线、循环、OR/包容网关、事件竞争、指定节点跳转或在途实例迁移。
87
+
88
+ ## 迁移和部署
89
+
90
+ 通过应用原有 `pnpm migrate` 执行系统迁移,再将 API 与 worker 切换到同一版本:
91
+
92
+ | 系统迁移 | 数据含义 |
93
+ | --- | --- |
94
+ | `cage:approval_task_actions_001` | 任务 revision、委派责任、original/additional 和追加式动作历史;旧任务保持 original、revision=0 |
95
+ | `cage:approval_task_stages_002` | 新增 assignment、父任务和签序;仅候选任务允许持有人为空,旧任务保持 direct |
96
+ | `cage:approval_deadlines_003` | 节点截止/临期/扫描时间、实例催办政策与冷却、持久站内提醒及已读;旧值为 null,不自动开始计时 |
97
+
98
+ 已发布的审批 DDL 和旧迁移没有改写。旧原票及旧版并行加签的 `parentTaskId/signPosition` 为空;升级不伪造旧签序或历史操作。升级后新创建的加签票保存父任务和签序。旧快照不会自动增加 actions、assignment 或 signPositions;升级前已经保存 `addSign: true` 但没有 signPositions 的节点只允许 parallel,before/after 返回 FORBIDDEN。没有 addSign 政策的旧实例仍完全禁止加签。不能把旧库基线测试中可创建直派任务理解为支持新版 API/worker 在未迁移数据库上运行。
99
+
100
+ ## 六种操作的区别
101
+
102
+ | 操作 | 结果 | 边界 |
103
+ | --- | --- | --- |
104
+ | claim 领取 | 未分配候选任务归当前用户处理,离开可领取列表 | 实时检查候选资格、active、批准/read 和单据范围;一个任务只能有一位持有人 |
105
+ | unclaim 释放 | 当前持有人清空分配,任务重新进入候选池 | 仅 candidate;委派未归还或存在未完成子票时拒绝;保留已完成子票和历史 |
106
+ | transfer 转办 | 接收人成为当前审批人,原审批人失去该任务决定权 | 双方须有单据 read/范围及批准能力;不转移角色授权 |
107
+ | delegate 委派 | 保存原负责人,将待办交受托人处理 | 受托人只能 resolve;不能批准、拒绝、退回、再次转办/委派/加签或释放 |
108
+ | resolve 归还 | 返回服务端保存的原负责人,等待其审批 | 不推进节点;原负责人停用或失去批准/read 时拒绝 |
109
+ | addSign 加签 | 当前节点添加独立必签任务,按 placement 控制签署顺序 | 仅原始任务可发起,每次 1–20 人、每节点累计最多 20 人;不创建新流程节点 |
110
+
111
+ 领取后移出候选角色不会自动取消已经承担的任务责任。只要仍有当前批准/read 权限和单据范围,该持有人可以正常决定或释放;失去这些业务权限后会拒绝操作。候选任务可以在动作政策允许时转办给有业务权限的非候选人,转办后仍保留 candidate 属性,新持有人也能按规则释放。没有管理员强制释放或无可用人员的通用恢复入口,不能在文档中以直接改表代替恢复功能。
112
+
113
+ 接收人不能是本人或同节点其他任务的当前/保留负责人,包括已经决定的人。候选领取也不能领取本人已经承担另一票的节点原票;例如完成前签的人不能在原票被释放后再领原票。转办或释放不能把独立会签名额压缩为同人多票。
114
+
115
+ ## 前签、后签和并行必签
116
+
117
+ `addSign` 的 `placement` 省略时为 `parallel`,已有客户端保持原来的并行含义。请求的位置还必须包含在当前实例的 signPositions 中,客户端不能仅凭新版声明替旧实例开放前后签。同一次追加的多位签署人彼此并行,不表示按 `userIds` 顺序逐人串签。
118
+
119
+ | placement | 创建时 | 何时恢复或激活 |
120
+ | --- | --- | --- |
121
+ | `parallel` | 原票和新加签票均可处理 | 原票通过仍须等待全部追加票 |
122
+ | `before` | 父任务变为 waiting,新加签票为 pending | 全部前签批准后,仍在 waiting 的父任务恢复为 pending |
123
+ | `after` | 父任务可处理,新加签票为 waiting | all 等自己的父票批准;any 等本节点任意原票批准 |
124
+
125
+ waiting 任务只能查看,不能决定或执行任务管理动作。前签完成恢复父票、后签从 waiting 激活为 pending 时都会增加该任务的 revision;原来等待页面保存的版本不可直接提交。
126
+
127
+ `any` 需要至少一张原票和全部追加票通过。其他原审批人先批准时,其余 pending/waiting 原票会 skipped,已经创建的前签、并行签、后签全部保留为必签;全部 waiting 后签此时激活。前签随后完成不会复活已经 skipped 的父票。`all` 需要全部原票和追加票通过,每张父票只激活自己的后签。
128
+
129
+ 任一当前可决定的任务拒绝或退回,会调用原有领域结论命令;成功后关闭同轮次剩余 pending 和 waiting。取消或重新提交也会关闭旧轮次两种状态的任务。不能通过释放仍带未完成前/后/并行子票的候选任务来丢弃必签责任。
130
+
131
+ 最终推进流程的实际决定人必须通过领域结论命令的额度、职责分离、状态和版本约束。后签人可能成为最终签署人;若其额度不足,最后决定、版本、阶段变化及本次历史共同回滚,之前已提交的父票保留。引擎不会自动改用更早审批人的身份。
132
+
133
+ ## 客户端调用
134
+
135
+ ```ts
136
+ import { client } from '@app/contracts';
137
+
138
+ const candidates = await client.approvalCandidateTasks();
139
+ if (candidates.ok && candidates.data.length) {
140
+ const task = candidates.data[0];
141
+ // 展示候选不代表任务已被占用;服务端在领取事务中再次检查候选规则、权限和版本。
142
+ const claimed = await client.approvalTaskAction(task.id, {
143
+ action: 'claim', expectedRevision: task.revision, reason: '由我负责核查材料',
144
+ });
145
+ if (claimed.ok) {
146
+ // 领取已增加任务版本,后续界面读取本人待办并使用新版本;此处不自动替用户批准。
147
+ await client.approvalTasks();
148
+ }
149
+ }
150
+ ```
151
+
152
+ 其他已提供的调用如下,目标用户 ID 由应用选人界面提供,不在页面写死员工 ID:
153
+
154
+ - `client.approvalTasks()` 读取本人 pending;`client.approvalTasks('waiting')` 查看等待签署依赖的本人任务。
155
+ - `client.approvalCandidateTasks()` 读取当前可领取的共享任务,不把它们混入本人的可决定清单。
156
+ - `approvalTaskAction(id, { action: 'claim' | 'unclaim' | 'resolve', expectedRevision, reason })` 不接受目标用户字段。
157
+ - 转办/委派为 `{ action: 'transfer' | 'delegate', targetUserId, expectedRevision, reason }`。
158
+ - 加签为 `{ action: 'addSign', userIds, placement?: 'parallel' | 'before' | 'after', expectedRevision, reason }`;目标数组不能重复。
159
+ - `client.decideApprovalTask(id, 'approve' | 'reject' | 'return', { expectedRevision, comment? })` 提交审批决定。
160
+ - `client.approvalTaskHistory(process, aggregate, id, { after?, limit? })` 返回 `items/nextCursor`;默认 50,最多 200 条。
161
+
162
+ 摘要中的 `assigneeId` 是当前持有人,未领取时为 null;`assignment` 区分 direct/candidate,`parentTaskId/signPosition` 表示加签来源和顺序,`signPositions` 提供本实例允许发起的签序,`delegation: 'pending'` 表示只能归还。`actions/signPositions` 仅提示实例开放的管理政策,不能代替当前用户权限或任务状态校验。
163
+
164
+ 任务 `revision` 与单据 `version` 不同。candidate 节点或启用了任一任务管理动作的节点,决定也必须携带 `expectedRevision`;其他旧直派节点保留不带版本的引擎/HTTP 兼容调用,SDK 始终要求版本。领取、释放、转办、决定、激活或关闭任务都会使相应版本递增。冲突后重新读取并让用户核对;接口没有跨请求成功回执缓存,超时不表示服务端未提交,不可盲目重发。
165
+
166
+ 助理的 `approval_tasks` 只查询本人 pending 待办,`approval_decide` 可以保存并提交读取到的 `expectedRevision`,决定时仍检查委派状态、归属和权限。助理目前没有候选领取/释放、转办、委派归还或加签的管理写工具;这些功能由页面直接调用 SDK。
167
+
168
+ ## HTTP、可见性与操作记录
169
+
170
+ | HTTP | 输入/行为 |
171
+ | --- | --- |
172
+ | GET `/api/approval/tasks` | `status` 默认 pending,可用 waiting 查看本人等待任务 |
173
+ | GET `/api/approval/candidate-tasks` | 当前未领取、规则和权限均符合的任务 |
174
+ | POST `/api/approval/tasks/:id/claim`、`unclaim` | `expectedRevision/reason` |
175
+ | POST `/api/approval/tasks/:id/transfer`、`delegate` | `targetUserId/expectedRevision/reason` |
176
+ | POST `/api/approval/tasks/:id/resolve` | `expectedRevision/reason` |
177
+ | POST `/api/approval/tasks/:id/add-sign` | `userIds/placement?/expectedRevision/reason` |
178
+ | POST `/api/approval/tasks/:id/approve`、`reject`、`return` | `expectedRevision/comment` |
179
+ | GET `/api/approval/task-history/:process/:aggregate/:id` | `after` 记录 ID 字符串,`limit` 分页大小 |
180
+
181
+ 所有写入沿用会话与 CSRF。意见/原因最多 1000 字,管理动作原因去首尾空白后必须非空;未知字段、伪造 action 和非法 placement 拒绝。任务不在当前租户、不是本人持有,或领取时已经归别人,返回 NOT_FOUND;策略未开放、候选资格或能力不符返回 FORBIDDEN;仍可操作的任务版本过期返回 CONFLICT;waiting、已关闭、委派状态不允许或非当前步骤返回 INVALID_TRANSITION。并发抢领失败不保证都返回版本冲突,另一个人已持有时会返回 NOT_FOUND。
182
+
183
+ 每个流程分别扫描本人对应状态最近 200 条任务,或最近 200 条未领取候选任务,再按当前单据 read/范围过滤;候选还检查批准能力、当前选人规则和同节点是否已经承担其他票。停用或失去当前读取权限后不会继续披露相应摘要。过滤后可能不足 200 条,当前没有分页游标,不能据此断言更早没有符合条件的任务。
184
+
185
+ `vars` 保留提交时路由变量的含义,但始终去 hidden 并对敏感字段掩码。即使具备明文能力,也需另经正式业务读取接口获得带审计的明文。
186
+
187
+ 领取、释放、转办、委派、归还、加签、激活、人工决定、跳过和取消写入独立动作记录,与本次任务变更共同事务提交或回滚。应用账号只有记录 INSERT/SELECT 权限;这不是数据库管理员也无法修改的防篡改存储。当前单据授权读者可分页查看人工意见,处理过任务不产生永久读权。不要把凭据或无权共享的额外明文写入原因;迁移前发生的操作不会自动补录。
188
+
189
+ ## 当前界面和验收边界
190
+
191
+ 框架 Admin 用同一收件箱表格展示本人、可领取和等待加签任务及各类数量;采购范例提供“待我处理 / 可领取 / 等待加签”三个视图。两者都有领取、释放、受托任务归还及正常决定入口。waiting 仅展示,不显示决定按钮;候选和受托任务分别显示领取、归还入口。采购 `/approvals` 用 `view=candidates`、`view=waiting` 保存视图,默认本人待办不需要 view 参数。界面可提示操作,未完成子票等动态限制仍由服务端拒绝。
192
+
193
+ 采购范例后续已接入协作名录选人、转办/委派/三种加签及分页人工操作历史,详见 [采购导读](purchase-example.md)。框架 Admin 保留通用任务收件箱,启用审批运营后新增“我的申请”和“流程提醒”,不依赖采购的页面、名录或额外业务字段。框架支持、应用路由启用、浏览器验收仍是不同状态。
194
+
195
+ 本批真实 PostgreSQL/HTTP 定向回归共 144 项通过、最终 0 跳过:新增候选/签序 38 项、既有人员动作 27 项和内核/HTTP/AI/恢复 79 项。先执行六文件 143 项,再补旧实例签序权限用例并复跑两个审批文件 65 项,重复用例不累加。包含 8 人抢领、超过五连接池容量的并发、字段非法/停用/跨租户、动态候选身份、前后签 any/all、取消、领域命令失败回滚和真实旧库升级。首次内嵌测试数据库短暂拒绝连接造成的跳过,已由后续完整执行覆盖;该次跳过不计入通过。
196
+
197
+ 上述第二批基础页面曾使用真实构建和内存 HTTP 夹具验收;后续采购场景已另做真实隔离 PostgreSQL、API、worker 和账号登录的浏览器验证。各次记录不累加为全部功能验收;本页 API 说明也不能代替部署后的实际验证。
198
+
199
+ 尚未实现受限岗位规则、代理时间段、嵌套委派、无可用人员的通用恢复、减签/撤销加签、任意串行加签链、指定节点退回、任意流程图/循环/OR 网关、工作日计时、自动升级、抄送、邮件/短信通知和可视化运行时设计器。应用接入仍需检查真实角色、路由、迁移与浏览器路径;上述历史测试数量不作为本次 AND 并行的验证数量。
200
+
201
+ ## 框架通用时限、提醒与催办
202
+
203
+ 这组能力实现在 `@yss123/cage-approval`,复用内核的 runtime/worker 扩展,不需要业务项目创建通知表或编写定时任务。应用只需声明政策,沿用正常迁移、API 与 worker 启动流程。旧实例未声明时限或催办政策时不会被升级自动开放。
204
+
205
+ ```ts
206
+ import { approvalOperations, approver, route, step } from '@yss123/cage-approval';
207
+
208
+ /** 通用站内运营服务,从 domain 入口导出,worker 才会扫描到期节点。 */
209
+ export const approvalOps = approvalOperations();
210
+
211
+ /** 局部接入示意:对应 approval 必须已绑定,并把 createdBy 列入 routable。 */
212
+ export const documentRoute = route('document-review', [
213
+ step('部门审核', approver.role('reviewer'), {
214
+ sla: { durationMinutes: 1440, remindBeforeMinutes: 60 },
215
+ actions: { transfer: true, delegate: true, addSign: true },
216
+ }),
217
+ ], { reminders: { requesterField: 'createdBy', cooldownMinutes: 15 } });
218
+ ```
219
+
220
+ `durationMinutes` 是进入人工节点后可用的**自然分钟总时限**,`remindBeforeMinutes` 是截止前多少分钟进入临期提醒。两者允许小数分钟,换算后至少一毫秒、至多 525600 分钟,提前量严格小于时限;省略提前量则只发超时提醒。每条并行分支的节点在实际开启时独立开始计时,尚未满足前驱的节点不提前计时。时限以数据库持久时间计算,加签、转办、委派、领取和释放都不重置;后签激活时该节点可能已经超时。AI 节点及其人工接管不使用此人工节点 SLA,已有人工接管待办仍可接收申请人催办。没有工作日历、节假日扣除或按员工时区计算的工作时间。
221
+
222
+ 临期/超时由 worker 扫描各分支的持久活动节点并写入站内提醒。同一节点阶段、任务、收件人去重;worker 重启或并发扫描不重复投递同一项。已经跨过截止时间时只补超时,不补过时临期。转办或后签激活后的新办理人可在下一次扫描收到当前阶段提醒。waiting 票不会收到“请处理”提醒,取消、完成和不再活动的步骤停止投递;提醒不会自动批准、自动转办或替人取得能力。
223
+
224
+ 每次 tick 最多扫描 25 个节点,每个节点最多投递 100 个尚未收到当前阶段提醒的任务/接收人配对,后续扫描继续补齐;扫描位置保存在数据库并轮转,完成投递的旧节点不会永久占满批次。角色选人仍读取当前完整候选集合,尚未验证大组织生产规模性能。手动催办最多 200 个合格配对,超出明确返回 INVALID_TRANSITION 且不写通知或冷却。提醒实际到达时间受 worker 轮询和队列积压影响,不是精确到毫秒的送达承诺。
225
+
226
+ 接收人根据当前 pending 任务的持有人、未归还委派的原负责人或未领取候选规则计算,并实时过滤 active、批准/read 能力与单据范围。候选人已承担本节点另一票时不接收可领取提醒。通知读取与标已读仍核对本人、当前权限及接收关系,转出或撤权后不能凭旧通知继续查看原因。它是当前协作收件箱;永久操作追溯仍用 `approvalTaskHistory`。
227
+
228
+ 申请人由路由 `reminders.requesterField` 指定,必须是公开且进入 `routable` 的单个用户 UUID 字段,并随提交冻结。仅能读取单据不等于能催办。催办绑定实例 UUID 与 `expectedStep`,并行实例还必须提交完整且按升序排列的 `expectedSteps`,其值使用最近一次申请摘要中的 `currentSteps`。即使并行实例暂时只剩一个活动节点,也不能省略该集合;否则返回 INVALID_INPUT。最小索引未变而另一分支已推进时,旧集合仍会返回 CONFLICT,页面应刷新后由用户重新核对。
229
+
230
+ 催办事务同时检查活动集合、业务提交版本、申请人和冷却时间,向全部活动节点的合格任务/接收人配对投递。同实例跨分支、跨节点共享冷却,冷却中或旧业务版本返回 CONFLICT;无合格接收人明确失败且不消耗冷却。催办只增加提醒和操作记录,不修改业务 version 或待办 revision。本人申请从最近 200 个匹配实例中再按当前可见性过滤,未提供分页,不能把返回数量当作完整历史总数。
231
+
232
+ 公共 SDK 使用与其他命令相同的会话和 CSRF:
233
+
234
+ | 调用 | 用途 |
235
+ | --- | --- |
236
+ | `client.approvalRequests()` | 本人发起且开放催办的运行实例,含 currentSteps、逐节点 activeSteps、最近截止 dueAt 和 nextUrgeAt |
237
+ | `client.urgeApproval(instanceId, { expectedStep, expectedSteps, reason })` | 并行必填完整 expectedSteps;原因 1–1000 字;返回任务/接收人配对数及再次可催办时间 |
238
+ | `client.approvalNotices({ after?, limit?, unreadOnly? })` | 本人当前可见提醒,返回 items/nextCursor,游标始终为字符串 |
239
+ | `client.readApprovalNotice(id)` | 标已读;重复请求保留首次已读时间 |
240
+
241
+ ```ts
242
+ const requests = await client.approvalRequests();
243
+ if (requests.ok && requests.data.length) {
244
+ const selected = requests.data[0];
245
+ // 页面应先让申请人选择实例并填写原因;同时提交最小索引与完整集合,防止旧页面误催已变化的分支。
246
+ const result = await client.urgeApproval(selected.id, {
247
+ expectedStep: selected.currentStep,
248
+ expectedSteps: selected.currentSteps,
249
+ reason: '材料已补齐,请继续审核',
250
+ });
251
+ if (result.ok) {
252
+ // recipientCount 按任务与接收人配对计数;同一人处理两条分支可收到两条提醒,不是去重人数。
253
+ console.log(`已发送 ${result.data.recipientCount} 条站内催办提醒`);
254
+ }
255
+ }
256
+ ```
257
+
258
+ 对应接口为 GET `/api/approval/requests`、POST `/api/approval/instances/:id/urge`、GET `/api/approval/notices`、POST `/api/approval/notices/:id/read`。只有启用运行时才注册,并写入 OpenAPI。提醒分页按底层稳定记录游标推进,授权过滤后空页也可能有 nextCursor,应继续加载直到 null;单条通知不包含整份路由变量。
259
+
260
+ 框架 Admin 的 `/admin/#/requests` 可填写原因并催办,`/admin/#/notices?view=unread` 可筛选、翻页和标已读;`view=all` 展示当前可见的全部提醒,`#/tasks` 展示截止时间。业务应用可以直接使用这些页面,也可以消费公共 SDK 构建自己的界面。采购仅作为接入验收载体,框架功能不依赖采购业务代码。
@@ -261,7 +261,7 @@ export const poRoute = route('po-approval', [
261
261
 
262
262
  `approval` 绑定事件与结论命令;`route` 决定步骤与人;`routable` 限制交给路由的数据字段。`onApproved/onRejected` 必须属于同一聚合,且允许 process 调用。
263
263
 
264
- 内置选人器是 `approver.deptLeader(字段名)`、`approver.role(角色code)`、`approver.user(用户id)`。条件使用 `gt/gte/lt/lte/eq/ne/isIn` 等封闭表达式。`mode: 'any'/'all'` 描述一步内的人如何完成,不表示任意并行分支。可以在代码组合这些能力;任意函数选人、转签和加签需框架扩展或外部审批适配器,当前不能直接写成现成 API。
264
+ 内置选人器是 `approver.deptLeader(字段名)`、`approver.role(角色code)`、`approver.user(用户id)`。条件使用 `gt/gte/lt/lte/eq/ne/isIn` 等封闭表达式。`mode: 'any'/'all'` 描述一步内的人如何完成,不表示任意并行分支。人工节点可通过 `actions: { transfer: true, delegate: true, addSign: true }` 显式开放转办、委派归还和当前节点并行必签,默认关闭;任务版本、权限、SDK 与边界见[审批任务流转](approvals.md)。任意函数选人、前后加签和并行流程分支仍需扩展。
265
265
 
266
266
  被选中的审批人还要具备结论命令所需能力、范围和额度。部门负责人为空会产生 `CAGE-APPR-007`,详见[排错](troubleshooting.md)。
267
267
 
@@ -11,8 +11,9 @@
11
11
  | `packages/domain/src/index.ts` | 哪些声明真正被注册 | 保留组织模型,导出新增业务,避免导出名冲突 |
12
12
  | `packages/domain/src/modules/inventory/inventory.ts` | 最简字段、创建命令、资源锁聚合 | scope.none 不等于无需授权;创建与释放都是命令 |
13
13
  | `packages/domain/src/modules/purchase/purchase-order.ts` | 单据生命周期、金额、明细、约束、事件 | 哪些字段由输入提供,哪些由内核维护;每个状态的允许操作 |
14
+ | `packages/domain/src/modules/purchase/approval-members.ts` | 采购协作名录与公开业务姓名 | 名录读权限与系统用户资料分离;成员身份不授予审批权限 |
14
15
  | `packages/domain/src/workflows/purchase-inventory.ts` | 同事务建锁、异步释放 | acquires/releases 的配对及 map 返回 null 的原因 |
15
- | `packages/domain/src/modules/purchase/approvals.ts` | 启动/取消审批、条件步骤与选人 | routable、部门负责人、角色 code、结论命令 |
16
+ | `packages/domain/src/modules/purchase/approvals.ts` | 启动/取消审批、专业会签、财务候选与任务动作 | routable、部门负责人、字段选人、候选领取、结论命令 |
16
17
  | `packages/domain/src/integrations/erp.ts` | 出站推单、成功回写、入站回调 | 白名单、secret 名、schema、幂等与 HMAC |
17
18
  | `packages/domain/src/integrations/intelligence.ts` | 消费框架托管模型和本体助理 | Admin 配置连接,通用工具由框架生成 |
18
19
  | `packages/domain/src/modules/purchase/queries.ts` | 按状态汇总 | reads、安全视图、命名参数 |
@@ -32,7 +33,9 @@
32
33
  → 材料不足:returnForChanges → draft,补充用途后重新提交、从头审核
33
34
  → 疑难材料:在本节点创建人工接管待办
34
35
  → 材料清楚:通过本节点,创建经理待办
35
- → 经理审批;金额 > 50000 时还有财务步骤
36
+ → 部门经理审批
37
+ → 申请启用专业复核时:所选专业人员全部会签
38
+ → 金额 > 50000 时:财务共享候选任务,由 finance 成员领取后处理
36
39
  → 流程完成 → 调用 approve
37
40
  → 职责分离、额度、库存检查
38
41
  → approved + OrderApproved
@@ -45,11 +48,12 @@
45
48
 
46
49
  submit 返回成功只表示单据和事件已提交,不表示待办、ERP 或其他异步工作已经完成。worker 未运行时,单据可以已经处于待审批,但没有待办。
47
50
 
48
- ### 为什么分三个聚合
51
+ ### 为什么分开单据、库存与协作名录
49
52
 
50
53
  - `PurchaseOrder` 管单据、审批结论和采购状态。
51
54
  - `Inventory` 是物料台账,提供库存检查的数据。
52
55
  - `InventoryLock` 记录某张订单的资源占用,使用订单 id 作为锁 id,让释放目标明确。
56
+ - `ApprovalMember` 保存采购参与者可读取的公开业务姓名和专业说明。它引用真实用户,但不开放 `User.name` 等系统用户资料,也不代替角色授权。
53
57
 
54
58
  审批通过和建锁放在同一事务,是为了避免“单据已批准却没拿到资源”。ERP 网络请求放到 outbox,是因为远端调用无法与本地 PostgreSQL 共同回滚。
55
59
 
@@ -71,19 +75,29 @@ submit 返回成功只表示单据和事件已提交,不表示待办、ERP 或
71
75
 
72
76
  | 对象 | 配置要点 |
73
77
  | --- | --- |
74
- | 采购员 | 创建/查看/提交/取消采购单等所需能力,所属部门,以及采购单数据范围 |
78
+ | 采购员 | 创建/查看/提交/取消采购单等所需能力,所属部门,以及采购单数据范围;配置专业复核需 `PurchaseOrder.setReviewers` 和 `ApprovalMember.read` |
75
79
  | 智能助理使用人 | 显式授予 `Sys.ai.use`;编辑用途还需 `PurchaseOrder.explainPurpose` |
76
80
  | 模型管理者 | `Sys.ai.models.manage`;在 `/admin/#/models` 配置并启用 `main`,部署主密钥见[智能能力指南](intelligence.md) |
77
81
  | 助理性格管理者 | `Sys.ai.personalities.manage`;在 `/admin/#/personalities` 编辑预设、自定义性格、设置默认并试聊 |
78
82
  | AI 预审角色 | 创建 code 为 `purchase_ai_reviewer` 的角色,授予 `PurchaseOrder.read/returnForChanges` 及覆盖申请范围的数据权限;本例 AI 仅预审,不执行最终 approve |
79
83
  | 经理 | `PurchaseOrder.read/approve/reject/returnForChanges`,适当数据范围,`approve` 的 `ceiling` 额度参数 |
84
+ | 专业协作人 | 真实账号及同样的审批能力、范围、额度;由名录管理员通过 `ApprovalMember.create` 登记公开业务姓名、专业说明;需要在任务中选人时授予 `ApprovalMember.read` |
85
+ | 名录管理员 | 显式授予 `ApprovalMember.create/update/retire/read`;登记、更新资料或退役不修改用户角色,不自动给采购参与者授予 `User.read` |
80
86
  | 部门 | 将采购员所属部门的负责人设为经理;不能为空 |
81
- | 财务 | 角色 **code** 为 `finance`,给实际用户分配;配置审批所需权限、范围及额度 |
87
+ | 财务 | 角色 **code** 为 `finance`,给至少两位实际用户分配以体验共享候选池;配置审批所需权限、范围及足够额度;领取后才能处理 |
82
88
  | 库存台账 | 预先登记与采购明细相同的 sku,数量满足演示检查 |
83
89
  | 连接器环境 | fake 阶段使用 worker 注册的 fake;真实环境另配 host 对应密钥及回调签名密钥 |
84
90
 
85
91
  经理与提单人必须是不同用户。配置“负责人”决定选谁;配置角色能力与范围决定此人能否完成结论命令,两者都需要。金额 **大于** 50,000 才追加财务步骤,恰好 50,000 不满足 gt 条件。
86
92
 
93
+ 专业复核默认关闭,兼容旧单据的空字段;开启后需选择 1–5 位不同且非提单人的有效名录成员。`setReviewers` 只允许编辑草稿;创建可暂存未选完整的草稿,但 `submit` 会重新核对名单,拒绝保存后退役的成员。名录退役不修改已经提交的名单快照,也不撤销在途任务;账号停用和实际审批权限继续由审批引擎核验。名录的用户引用不能换绑,已退役记录保留且不支持重新启用。
94
+
95
+ `ApprovalMember` 使用 `scope.none()`,没有部门或本人字段。名录管理员和需要选人的采购参与者都必须显式获得该聚合的 `all` 数据范围;仅授予 `ApprovalMember.read` 仍看不到成员,草稿的专业名单校验也会因读范围不足拒绝。这里的 `all` 始终受当前租户隔离,不要求也不授予 `User.read`。
96
+
97
+ 经理、专业会签和财务节点均显式启用转办、委派与前置/后置/并行加签,节点政策随每次提交保存。转办交接处理责任;委派处理完成后归还原审批人;加签增加必须完成的签署任务,前置签署完成前原任务等待,后置签署在原任务通过后才开放。任务动作和决定都应携带页面当前 `revision`,冲突后刷新待办,不自动重试旧决定。AI 人工接管继续采用 AI 节点自身政策,不能因为后续经理节点启用动作而越权操作。
98
+
99
+ 逐项体验时用不同采购单分别记录:普通经理审批、专业双人会签、恰好 50,000 元跳过财务、超过 50,000 元财务领取/释放/再领取、转办、委派归还、三种加签,以及退回重提和申请人取消。每一步同时查看采购状态、当前可操作/等待任务及动作历史;专业会签只通过一人或必签尚未完成时采购单应仍待审批,最终通过才产生库存锁。浏览器验收需切换真实账号进行,自动测试通过不能替代页面验证。
100
+
87
101
  可分别准备“采购办公用品”、说明实际使用场景的完整用途、含“紧急采购”的用途,体验退回、通过本节点和转人工。真实模型的判定应按实际样例验收;故障默认转人工,不能当作正常审查通过。详情页显示历史理由、修改建议及审核轮次;用途编辑框的未保存内容会经 SDK 一起传给助理,服务端重新核对当前单据与权限。
88
102
 
89
103
  拥有 `Sys.ai.use` 的用户在所有业务页面右下角都有助理入口,点击打开悬浮对话窗,收起后继续执行当前任务。工作台、采购列表/详情、库存和待办分别提供页面语义;列表带筛选与可见记录,详情带单据引用和未保存用途。应用壳只挂载一个 `AssistantScope`,业务页面通过 `AssistantPage` 注册上下文,所以站内切页保留对话,但下一条消息使用新页面的快照。不同页面拥有独立的 `AiPageContext` 身份,旧任务要求补充页面时不能提交新页面代替。登录页和独立的 `/admin/` 不挂载业务助理。
@@ -42,7 +42,7 @@
42
42
  4. 配置实际部门负责人、角色或指定用户;给审批人配置能力、范围和额度。
43
43
  5. 页面通过待办接口操作。通过后需占用资源时用反应调用资源命令,按业务一致性选择同事务。
44
44
 
45
- 完成标准:提交生成待办、正确的人审批、越权和自审批拒绝、取消能结束待办、资源配对完整。任意流程设计器、动态 JS 选人、转签/加签不属于当前现成能力,需要明确扩展范围。
45
+ 完成标准:提交生成待办、正确的人审批、越权和自审批拒绝、取消能结束待办、资源配对完整。转办、委派归还和当前节点并行必签见[审批任务流转](approvals.md),须显式启用政策并传任务版本。任意流程设计器、动态 JS 选人、前后加签和指定节点退回仍需明确扩展范围。
46
46
 
47
47
  ## 加入 AI 审批或随页助理
48
48
 
@@ -47,8 +47,8 @@ ai({ models: {}, assistant: { model: 'missing', limits: { maxSteps: 0 } } })
47
47
  - `packages/ai/src/index.ts:94`
48
48
  - `packages/ai/src/index.ts:96`
49
49
  - `packages/ai/src/index.ts:126`
50
- - `packages/ai/src/index.ts:130`
51
- - `packages/ai/src/index.ts:132`
50
+ - `packages/ai/src/index.ts:133`
51
+ - `packages/ai/src/index.ts:135`
52
52
 
53
53
  实现片段(位置随框架版本更新):
54
54
 
@@ -1,4 +1,4 @@
1
- # CAGE-APPR-001:可路由字段不存在
1
+ # CAGE-APPR-001:审批字段声明无效
2
2
 
3
3
  [返回错误码册](README.md) · [业务排错](../ai/troubleshooting.md)
4
4
 
@@ -8,11 +8,12 @@
8
8
 
9
9
  阶段:声明、图规则或 worker。
10
10
 
11
- approval.routable 引用了所属聚合没有声明的字段。
11
+ approval.routable 引用了不存在字段,或 approver.field 指向未声明、隐藏、未进入 routable 的字段。
12
12
 
13
13
  ## 修复步骤
14
14
 
15
- 1. 先在聚合声明实际字段,再将需要给审批路由的字段放入 routable;不要把任意输入直接交给流程。
15
+ 1. 先在聚合声明实际字段,再将需要给审批路由的非隐藏字段放入 routable;不要把任意输入直接交给流程。
16
+ 2. 字段选人值应为 UUID 或非空 UUID 数组,见 docs/ai/approvals.md;字段公开并不自动授予审批能力。
16
17
 
17
18
  ## 正例
18
19
 
@@ -34,15 +35,17 @@ routable: ['managerName'] // 聚合没有该字段
34
35
 
35
36
  以下路径相对框架源码仓库,不是要求业务项目创建这些文件。当前项目先修业务声明、配置或授权;明确属于框架缺口时反馈错误码和脱敏上下文。
36
37
 
37
- - `packages/approval/src/route.ts:159`
38
+ - `packages/approval/src/route.ts:215`
39
+ - `packages/approval/src/route.ts:220`
40
+ - `packages/approval/src/route.ts:229`
38
41
  - `packages/kernel/src/primitives/flow.ts:245`
39
42
 
40
43
  实现片段(位置随框架版本更新):
41
44
 
42
45
  ```ts
43
- for (const field of s.context.fields) if (!binding.agg.fields[field] || binding.agg.fields[field].isHidden)
44
- throw declError('CAGE-APPR-001', `AI 上下文字段 ${field} 不存在或不可公开`);
45
- for (const query of s.context.queries ?? []) if (!registry.queries.some(q => q.name === query))
46
- throw declError('CAGE-REG-001', `AI 步骤引用的查询 ${query} 未导出`);
47
- }
46
+ if (reminders && (!routeBinding?.routable.includes(reminders.requesterField) || !routeBinding.agg.fields[reminders.requesterField] || routeBinding.agg.fields[reminders.requesterField].isHidden))
47
+ throw declError('CAGE-APPR-001', `申请人字段 ${reminders.requesterField} 必须存在、可公开且进入 routable`);
48
+ for (const node of declaredSteps) {
49
+ const selector = node.kind === 'ai' ? node.handoff : node.approver;
50
+ // 新字段选择器必须能从 routable 快照取值;不追溯改变历史部门选人的声明校验行为。
48
51
  ```