@stalfh233/omc-cli 0.5.6 → 0.5.8
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.
- package/LICENSE +21 -21
- package/README.md +359 -359
- package/coverage/m1-coverage-manifest-v1.json +1414 -1414
- package/dist/approval-flow.js +0 -0
- package/dist/args.js +0 -0
- package/dist/artifacts.js +0 -0
- package/dist/batch-call.js +0 -0
- package/dist/bench/call-face-eval.js +0 -0
- package/dist/bench/context-attribution.js +0 -0
- package/dist/bench/discovery-cost-eval.js +0 -0
- package/dist/bench/driver.js +0 -0
- package/dist/bench/evals.js +0 -0
- package/dist/bench/instrument.js +0 -0
- package/dist/bench/intent-face-eval.js +40 -40
- package/dist/bench/run.js +0 -0
- package/dist/bench/scenario.js +0 -0
- package/dist/bench/types.js +0 -0
- package/dist/bi-wire.js +0 -0
- package/dist/bizservice-config.js +0 -0
- package/dist/call.js +0 -0
- package/dist/capability-absences.js +0 -0
- package/dist/capability-overview.js +0 -0
- package/dist/capability-shape.js +0 -0
- package/dist/cli-contract.js +0 -0
- package/dist/cli-output.js +0 -0
- package/dist/cli.js +31 -1
- package/dist/closest-match.js +0 -0
- package/dist/code-rules.js +0 -0
- package/dist/command-transport.js +0 -0
- package/dist/compound-writes.js +0 -0
- package/dist/compute.js +0 -0
- package/dist/config-store.js +0 -0
- package/dist/context.js +0 -0
- package/dist/contract-consistency.js +0 -0
- package/dist/contract-resources.js +0 -0
- package/dist/coverage-consistency.js +0 -0
- package/dist/coverage-registry.js +0 -0
- package/dist/coverage.js +0 -0
- package/dist/data-list-filter.js +0 -0
- package/dist/describe-cache.js +0 -0
- package/dist/discovery.js +0 -0
- package/dist/endpoints.js +0 -0
- package/dist/environment-policy.js +0 -0
- package/dist/execution-metadata.js +0 -0
- package/dist/field-add.js +314 -0
- package/dist/field-change.js +0 -0
- package/dist/field-families.js +0 -0
- package/dist/form-default.js +0 -0
- package/dist/form-layout.js +0 -0
- package/dist/form-support.js +9 -0
- package/dist/formula-lint.js +0 -0
- package/dist/goal-routes.js +28 -16
- package/dist/governed-execution.js +0 -0
- package/dist/graph.js +50 -33
- package/dist/human-summary.js +1 -1
- package/dist/identity.js +0 -0
- package/dist/intent/baseline.js +0 -0
- package/dist/intent/capabilities/bizservice.js +0 -0
- package/dist/intent/capabilities/businessrule.js +0 -0
- package/dist/intent/capabilities/datarule.js +0 -0
- package/dist/intent/capabilities/field.js +0 -0
- package/dist/intent/capabilities/form.js +0 -0
- package/dist/intent/capabilities/index.js +0 -0
- package/dist/intent/capabilities/listview.js +0 -0
- package/dist/intent/capabilities/model.js +0 -0
- package/dist/intent/capabilities/onlinejs.js +0 -0
- package/dist/intent/capabilities/report.js +0 -0
- package/dist/intent/capabilities/workflow.js +0 -0
- package/dist/intent/capability.js +0 -0
- package/dist/intent/cli.js +0 -0
- package/dist/intent/compare.js +0 -0
- package/dist/intent/compiler.js +0 -0
- package/dist/intent/dsl.js +0 -0
- package/dist/intent/plan-apply.js +0 -0
- package/dist/intent/plan-file.js +0 -0
- package/dist/intent/readback.js +0 -0
- package/dist/intent/schema.js +0 -0
- package/dist/intent/validation.js +0 -0
- package/dist/intent/yaml.js +0 -0
- package/dist/journal.js +0 -0
- package/dist/json-column.js +0 -0
- package/dist/lanes/app-contract.js +0 -0
- package/dist/lanes/app-coverage.js +0 -0
- package/dist/lanes/app.js +0 -0
- package/dist/lanes/apply-changes.js +0 -0
- package/dist/lanes/b2-registration-contract.js +0 -0
- package/dist/lanes/b2-registration-coverage.js +0 -0
- package/dist/lanes/b2-registration.js +0 -0
- package/dist/lanes/businessrule-contract.js +0 -0
- package/dist/lanes/businessrule-coverage.js +0 -0
- package/dist/lanes/businessrule.js +11 -3
- package/dist/lanes/contract-support.js +0 -0
- package/dist/lanes/coverage-declaration.js +0 -0
- package/dist/lanes/custom-page.js +0 -0
- package/dist/lanes/datarule-contract.js +0 -0
- package/dist/lanes/datarule-coverage.js +0 -0
- package/dist/lanes/datarule-protocol.js +0 -0
- package/dist/lanes/datarule.js +6 -0
- package/dist/lanes/dictionary-contract.js +0 -0
- package/dist/lanes/dictionary-coverage.js +0 -0
- package/dist/lanes/dictionary.js +0 -0
- package/dist/lanes/environment-contract.js +0 -0
- package/dist/lanes/environment-coverage.js +0 -0
- package/dist/lanes/environment.js +0 -0
- package/dist/lanes/field-contract.js +0 -0
- package/dist/lanes/field-coverage.js +0 -0
- package/dist/lanes/field.js +0 -0
- package/dist/lanes/folder-contract.js +0 -0
- package/dist/lanes/folder-coverage.js +0 -0
- package/dist/lanes/folder.js +0 -0
- package/dist/lanes/form-contract.js +0 -0
- package/dist/lanes/form-coverage.js +0 -0
- package/dist/lanes/form.js +0 -0
- package/dist/lanes/lane-ids.js +0 -0
- package/dist/lanes/list-view-contract.js +0 -0
- package/dist/lanes/list-view-coverage.js +0 -0
- package/dist/lanes/list-view-shapes.js +0 -0
- package/dist/lanes/list-view.js +6 -0
- package/dist/lanes/meta-contract.js +0 -0
- package/dist/lanes/meta.js +0 -0
- package/dist/lanes/model-contract.js +0 -0
- package/dist/lanes/model-coverage.js +0 -0
- package/dist/lanes/model.js +0 -0
- package/dist/lanes/online-js-contract.js +0 -0
- package/dist/lanes/online-js-coverage.js +0 -0
- package/dist/lanes/online-js.js +0 -0
- package/dist/lanes/org-tree.js +0 -0
- package/dist/lanes/organization-coverage.js +27 -0
- package/dist/lanes/organization.js +10 -1
- package/dist/lanes/report-contract.js +0 -0
- package/dist/lanes/report-coverage.js +0 -0
- package/dist/lanes/report.js +0 -0
- package/dist/lanes/rule-graph.js +0 -0
- package/dist/lanes/rule-lifecycle-contract.js +0 -0
- package/dist/lanes/rule-lifecycle-coverage.js +0 -0
- package/dist/lanes/rule-lifecycle.js +0 -0
- package/dist/lanes/runtime-data-contract.js +0 -0
- package/dist/lanes/runtime-data-coverage.js +0 -0
- package/dist/lanes/runtime-data.js +24 -0
- package/dist/lanes/workflow-contract.js +0 -0
- package/dist/lanes/workflow-coverage.js +0 -0
- package/dist/lanes/workflow.js +7 -2
- package/dist/mcp-graph.js +0 -0
- package/dist/model-purge.js +0 -0
- package/dist/model-record.js +0 -0
- package/dist/model-scaffold.js +0 -0
- package/dist/omc-core-lazy.js +0 -0
- package/dist/online-js-layout.js +0 -0
- package/dist/online-js-source.js +28 -28
- package/dist/package-tool.js +0 -0
- package/dist/package.js +0 -0
- package/dist/path-projection.js +19 -1
- package/dist/plan.js +0 -0
- package/dist/read.js +0 -0
- package/dist/recipes/b2-registration.js +0 -0
- package/dist/recipes/businessrule.js +0 -0
- package/dist/recipes/datarule.js +0 -0
- package/dist/recipes/dictionary.js +0 -0
- package/dist/recipes/field.js +0 -0
- package/dist/recipes/form.js +0 -0
- package/dist/recipes/list-view.js +0 -0
- package/dist/recipes/model.js +0 -0
- package/dist/recipes/online-js.js +0 -0
- package/dist/recipes/runtime-data.js +0 -0
- package/dist/recipes/shared.js +0 -0
- package/dist/recipes/workflow.js +0 -0
- package/dist/redact.js +0 -0
- package/dist/refusal-guidance.js +0 -0
- package/dist/rollback-spec.js +0 -0
- package/dist/rollback.js +0 -0
- package/dist/rule-support.js +0 -0
- package/dist/rule-when.js +4 -3
- package/dist/runtime-support.js +0 -0
- package/dist/script.js +0 -0
- package/dist/service-doctor.js +0 -0
- package/dist/session-manager.js +0 -0
- package/dist/session.js +0 -0
- package/dist/skills.js +0 -0
- package/dist/sqlite-warning-filter.js +0 -0
- package/dist/support.js +0 -0
- package/dist/token-cache.js +0 -0
- package/dist/tool-types.js +0 -0
- package/dist/tools.js +0 -0
- package/dist/usage-log.js +0 -0
- package/dist/wire.js +0 -0
- package/dist/workflow-support.js +0 -0
- package/dist/write-lease.js +0 -0
- package/dist/write-lock.js +0 -0
- package/dist/write.js +0 -0
- package/dist/zip.js +0 -0
- package/docs/images/omc-nixi/README.md +41 -41
- package/docs/omc-introduction.md +851 -851
- package/docs/tool-surface-map.md +42 -42
- package/package.json +3 -3
- package/skills/omc-acceptance-criteria.md +50 -50
- package/skills/omc-business-configuration.md +336 -335
- package/skills/omc-capabilities.md +214 -214
- package/skills/omc-capability-scouting.md +55 -55
- package/skills/omc-config-draft-review.md +175 -175
- package/skills/omc-five-piece-flow.md +35 -35
- package/skills/omc-glossary.md +86 -86
- package/skills/omc-refusals.md +129 -129
- package/skills/omc-requirement-analysis.md +161 -161
- package/skills/omc-requirement-vocabulary.md +61 -61
- package/skills/omc-start-here.md +107 -106
- package/skills/omc-tool-selection.md +119 -119
- package/skills/omc-write-hazards.md +222 -200
- package/dist/approval-token.js +0 -102
- package/dist/bench/fake-http-server.js +0 -65
- package/dist/fake/app.js +0 -45
- package/dist/fake/b2-registration.js +0 -594
- package/dist/fake/businessrule.js +0 -242
- package/dist/fake/datarule.js +0 -72
- package/dist/fake/dictionary.js +0 -108
- package/dist/fake/environment.js +0 -30
- package/dist/fake/field.js +0 -167
- package/dist/fake/form.js +0 -101
- package/dist/fake/index.js +0 -121
- package/dist/fake/list-view.js +0 -289
- package/dist/fake/model.js +0 -136
- package/dist/fake/online-js.js +0 -18
- package/dist/fake/report.js +0 -253
- package/dist/fake/routes.js +0 -47
- package/dist/fake/rule-lifecycle.js +0 -37
- package/dist/fake/runtime-data.js +0 -367
- package/dist/fake/state.js +0 -67
- package/dist/fake/workflow.js +0 -364
- package/dist/server.js +0 -92
|
@@ -1,200 +1,222 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: omc-write-hazards
|
|
3
|
-
description: 分发版 CLI 的底层写面安全形状。配置表单、视图、按钮、子表、关联、规则或在线 JS 时使用;CLI dry-lint 是主防线,浏览器不是常规验收步骤。
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# CLI 写面安全形状
|
|
7
|
-
|
|
8
|
-
本技能随 `omc-cli` 包分发,可用:
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
omc help omc-write-hazards
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
优先依赖 CLI 的 schema、contract、dry-lint、writable/生产确认和权威读回。浏览器只在用户报告明确的 UI 回归时使用。
|
|
15
|
-
|
|
16
|
-
## 表单与 Sheet201
|
|
17
|
-
|
|
18
|
-
- 控件 `options` 必须是 **object**,不能是 JSON 字符串。8.6.34 渲染器会写 `options.name_i18n`;字符串会抛 `Cannot create property 'name_i18n' on string` 并白屏。
|
|
19
|
-
- 子表父控件是 `type: 201`,业务子列放 `columns`;子列 key 不得进入 `viewJson`。
|
|
20
|
-
- 调用方显式传 `draftAttributesJson`/`draftViewJson` 时,CLI 会在写前拒绝字符串 options、非字符串 viewJson 单元、子列混入 viewJson。
|
|
21
|
-
- `form.draft rebuild:true` 会重建布局,但会保留已存在的非结构性控件 options;Sheet201 columns 以最新 subSchema 为准。
|
|
22
|
-
- 空布局 `[]`/`{}` 默认拒绝;只有显式 `allowEmptyLayout:true` 才能清空。
|
|
23
|
-
- **列数由 viewJson 行分组决定,不是 span**(2026-10-27 r24 真机实证):`viewJson` 是行的数组,一行内 N 个 code 即 N 列;`span` 只是 24 栅格下的控件宽度。只把字段 `span` 改 8 而不动行分组 → 控件变窄但每行仍两个,不是三列。改列数必须同时改行分组(三列=每行 3 个 code,字段 span=8)→ `form.publish` → `form.get` 回读 viewJson 核对。系统字段行与业务字段行分开调整。
|
|
24
|
-
- **下拉/单选/复选的选项在表单控件有两份独立副本**(2026-10-29 治豪 issue 同源事实,8.6/1.1.25 机制一致):数据项 `options.optionsSet.custom` 之外,表单控件的 `options.options.custom` 与 `options.optionsSet.custom` 是独立副本(× draft/published 双版本)——**只改字段不重存表单,门户/设计器不会变**。标准生效链:`field.update`(选项)→ `field.publish` → `form.draft`(rebuild 重建控件同步副本)→ `form.publish`;验收以表单面回读为准,数据项一致 ≠ 表单生效。
|
|
25
|
-
- **模型不支持跨应用迁移**(1.1.25 真机实证:`folder-other-application` 拒绝):模型建错应用时走「新应用重建 + 关联引用改指 + 旧模型删除」,不存在物理迁移命令。
|
|
26
|
-
- **流程编排器是线性的,无并行会签**(1.1.25 实证):多部门会签只能建成顺序节点链;需求里的「并行会签」按顺序节点落地并在交付说明里披露。
|
|
27
|
-
- **BIZ_ACTION 服务出参只认响应 JSON 根级平铺键**(p7/r26 双实证):嵌套在信封里(如 `{data:{level}}`)静默映射不上、目标字段全空——先把这类判断列进方案:接口形态要在「平铺 vs 信封」间做选择时,需要回写本平台的出参一律平铺。外部系统本身回信封的,扩展整形转发或放弃出参回写。
|
|
28
|
-
- **`{Originator.OU}` 参与人变量在 1.1.25 本环境不解析成部门**(r26 实证 4 轮误判):发起人部门参与人写法先小样本验证解析,失败改用显式部门/角色参与人,不要反复重配角色。
|
|
29
|
-
- **BRANCH 条件分支与运行时规则次序互斥**(r26 实证 3 轮):该版本 BRANCH 节点的工具校验与运行时执行规则冲突时,改用按钮触发或顺序节点路由条件,不硬调 BRANCH。
|
|
30
|
-
|
|
31
|
-
## 模型字段与关联
|
|
32
|
-
|
|
33
|
-
- **SQL 保留字预检**:字段 code 命中数据库关键字(如 `order`)时 wire 才回 `10033 编码[order]为SQL关键字`。建字段前先检查常见保留字(order/group/user/key/index/desc/asc/select/from/where/table),命中就换语义后缀(`orderRef`/`orderNo`),不要建了再改。
|
|
34
|
-
- **跨读面包目录键**:`apppackage/list` 投影的目录键是 `h_app_function.code`,`apppackage/trees` 的键是 `h_app_function.id`。把前者的 code 当 id 传给 `bizmodels/create.parentId` 会落成孤儿菜单行(模型读面可达、应用树/门户失踪)。0.4.54 起 `model.create` 的显式 `folderId` 会按 id/code 双口径归一;其它写面仍要自己归一。
|
|
35
|
-
- **`readback.present=false` 是硬失败信号**:平台可能接受请求但对象不在权威面(典型=孤儿挂载)。0.4.54 起交付状态降级为 `WRITE_UNVERIFIED`;禁止用"平台最终一致性"经验豁免。
|
|
36
|
-
|
|
37
|
-
- **code 定位写前先核在册**:`field.update` / `rule.node.update` 等按 code 定位的精确写,先 `model.inspect` / `field.list` 确认 code 在册;缺码即停并报"目标不存在 + 当前字段清单"——**别凭用户措辞假定 code 正确**(用户可能记错字段名),**也别擅自替换成语义相近的字段**(改错对象比不改更糟)。缺码 `field.update` 本就拒 301001,但写前先核能省一轮失败往返,并在草稿阶段就把"字段不存在"与"需求理解偏差"分清。
|
|
38
|
-
- **改名核对以字段读为准**:`field.update` 改名后,`model.inspect` 聚合面的 `fields[].name` 可能仍是旧值(发布态快照/聚合缓存);核对显示名用 `field.list`(默认清单面有 name);核对 options 用 `field.list` 的 `code` 选择器 / `field.get` 或 `form.publish` 回读——两处读数不一致时以字段读为准,别误判"写入没生效"而重复写。
|
|
39
|
-
- `propertyEmpty` 默认 `false`。模型层保持可空;必填、只读、可见性在表单和流程节点配置。
|
|
40
|
-
- 无映射关联单选 `WORK_SHEET` 使用 `mappings: ""`,不要用 `"[]"`。`"[]"` 是非空字符串,设计器会显示“已设置”,但弹窗没有映射行。
|
|
41
|
-
- 有映射时传非空 JSON 数组字符串,并在 draft/published 两面读回。
|
|
42
|
-
- **运行时写关联值是另一种形状**(`data.save`/`data.update` 的 formData):单选关联(`propertyType 9`)传 **id 字符串或 `{ id }`**,传**数组会稳定 50000**;**多选关联(11)传分号分隔的 id 字符串 `"id1;id2"`(或 `{id:"id1;id2"}`),传数组稳定 50000**;**人员/组织(50/51/60/61)传 `[{"id","type","name"}]`,`type` 必填**(缺它稳定 50000)——`type` 是 UnitType 索引:**用户=3、部门=1**。用错形状只会拿到一个不透明的 50000。
|
|
43
|
-
- **`mappings` 由表单运行时在用户选择时套用**,服务端 `data.save` **不会**自动回填映射目标字段。API 侧要自己写被映射字段,否则读回为空——别把它误判成“映射配置失败”。
|
|
44
|
-
- **关联映射的事实源在字段行 `options.mappings`,不在表单控件属性**(真机 2026-10-14 实证:改表单 attributes 里的映射会被平台按字段重算覆盖、回读不生效)。改映射走:`field.update` 按完整 options 提交(只改 mappings,其余键原值保留)→ `field.publish` → `form.draft`(**rebuild:false**,保留现有布局与在线JS)重存 → `form.publish` → 回读核对。**别用 cascadeRepublish 刷带布局/在线JS 的表单**:rebuild 会清空设计器公式(已做写前拒绝)。
|
|
45
|
-
- **运行时写值的最小形状**(组合出错只会得到一个不透明的 `50000`/NPE,逐一最小化再合):单选关联(9) → id 字符串 `"<id>"`;**多选关联(11) → 分号串 `"<id1>;<id2>"`**;**人员/组织(50/51/60/61) → `[{"id":"<id>","type":3,"name":"<名>"}]`,`type` 必填且为整数**(缺/非整数 → `BizField.parseFromClient` 拆箱 NPE→50000;用户=3、部门=1);日期(3) → 字符串 `"2026-09-30 10:00:00"`;子表(8) → 行对象数组 `[{"<子列code>":<值>, ...}]`。先各写一条最小行通过,再组合。
|
|
46
|
-
其中三类已有**写前拒绝**(r17b 2026-10-26 起):日期收到毫秒时间戳(平台接受但落库 NULL)、日期字符串不匹配字段声明 format(50000)、单值关联收到数组(50000)——blocker `submit-date-shape` / `submit-relation-shape`。
|
|
47
|
-
- **DATE(3) 字段的 `options.format` 必须是平台大写枚举**(`YYYY-MM-DD`、`YYYY-MM-DD HH:mm:ss`、`YYYY年MM月DD日`…),**不是 Java 的 `yyyy-MM-dd`**:平台 `DateBizField` 按字面比较,Java 写法能创建字段但**首次运行时 save 稳定 `50000`(`不支持的format`)**(2026-09-30 R5 实测)。`field.create`/`intent` 会就地拒绝非枚举值。
|
|
48
|
-
|
|
49
|
-
- **多选关联(11) 的写入源码依据**(1.1.25 实机 jar):`WorkSheetMultiBizField.contentFromClient` 只接受 `String` 或 `Map`,其它类型直接 `AppAsserts.isTrue(false, "Incorrect value type, should be string or map.")` → 表现为 50000;`contentToDbValue()` 返回 `map.get("id")`,而读回时按 `";"` 切分(`MULTI_WORK_SHEET_SPILT`)并组装 `items` 数组。所以**数组是读写不对称的读侧形状,不能拿来写**;分号串才是写侧形状。实测三种形状:`"<id>"`/`"<id>;<id>"` 成功、`["<id>"]` 50000。
|
|
50
|
-
|
|
51
|
-
## 业务规则与流程事件(源码实证,1.1.25)
|
|
52
|
-
|
|
53
|
-
- **`Create` 的触发判定是「旧状态→新状态」**:旧 `DRAFT` 且新 `PROCESSING` 或 `COMPLETED`(`DomainServiceImpl#getDefaultRuleCode`)。所以无流程模型一调 `data.submit`(→COMPLETED)**和流程表单提交(DRAFT→PROCESSING)都触发 Create**;`data.save` 存草稿不触发。不要因为"目标表没变化"就断言规则没触发。
|
|
54
|
-
- **副作用缺失归因三步**:先查执行记录(`h_log_biz_service` 按服务/方法/时间,或容器日志)→ ①无调用=规则/触发问题 ②有调用但失败=参数/权限/业务异常 ③调用成功但不可见=视图投影/事务未提交/查询条件。禁止只看目标表。
|
|
55
|
-
- **BIZ_ACTION 判成功只看传输**:REST 适配器解析响应后直接返回,**不校验 `errcode`**。扩展接口回 `{"errcode":50000}` + HTTP 200 会被当成节点成功(日志 `biz action node execute successful`)。要阻断必须走表单 `onValidate` 或数据规则,不能靠业务返回码。
|
|
56
|
-
- **`rule.run`/自定义规则不回写出参**(r26 实证 2 轮):出参合并只发生在预置 Update 规则的执行上下文;`rule.run` 手动触发与自定义规则不把服务出参写回本模型——出参落表要走「BIZ_ACTION 节点 outputMappings + 数据事件触发」,验收用 data.load 看字段而非 rule.run 回执。
|
|
57
|
-
- **出参回写三前提(r28b 8 组实验定案,单点总纲)**:① 响应 JSON 根级平铺出参键 ② 预置 Create/Update 规则上下文 ③ data.submit/data.update 触发(data.save 不触发)。三者缺一出参恒 null 且节点日志仍 successful——外部接口回信封的(`{code,message,data:[...]}`),先落平铺整形端点(平台扩展)再配 outputMappings,不要反复换规则形状试。
|
|
58
|
-
- **布尔出参映射到文本字段 → ClassCastException 50000**(r28b 实证):服务出参 `bizPropertyType:LOGICAL` 的值映射到 SHORT_TEXT 字段会让规则执行炸 50000——布尔出参只能映射 LOGICAL 字段;要落文本先在扩展侧转字符串。
|
|
59
|
-
- **审批提交的 formData 带子表数组 = 追加行**(r28b 实证):approval submit 的 formData 里带子表数组会被平台当「追加行」处理,审批回执只回传主表字段——审批链路里要改子表行,用 data.update 单独写,不要塞进 formData。
|
|
60
|
-
- **workItemId 不在 instanceActivity 里**(r28b 实证):流程实例活动查询不返回 workItemId;办理待办要的 workItemId 从 `search_workitems?state=PENDING` 的 `content[].id` 取。
|
|
61
|
-
- **模型创建/删除非原子,会有半态残留**(r28b 实证 ~20min):model.create 在表单层撞错(如「表单已存在」)时 schema 可能已半落库——refused 后换名重试前先 model.list 核对半态并清理;同理模型删除后平台读面不可见但库里可能留僵尸行(引擎报 `appFunction not exist` 就是它在作祟),迁移/复用编码前先核对。
|
|
62
|
-
- **流程事件处理器吞异常**:`start`/`end`/`cancel`/`activate` 中的 BIZ_ACTION 抛异常时,`InstanceContext#processingBusiness` 只记 `processingBusiness error.` 并返回失败结果,**流程继续**。事件处理器不能做"提交前拦截"。
|
|
63
|
-
- **禁写当前单据**:BIZ_ACTION 是 HTTP 自调用,外层事务已持行锁;同事务更新当前单据会等到 15s 超时并整条回滚。副作用写独立表;需要留痕用独立事务/异步重建 `RequestContext`。
|
|
64
|
-
|
|
65
|
-
## 运行数据读取的默认过滤
|
|
66
|
-
|
|
67
|
-
- `data.list` **默认不含 DRAFT**(`total` 也只算 COMPLETED):清测试数据时 `total:0` 不代表没有残留。核对/清理一律显式 `{"all":true,"showTotal":true}`,或读回体里的过滤标注。`data.load` 用于单行核对。
|
|
68
|
-
|
|
69
|
-
## 报表数据集
|
|
70
|
-
|
|
71
|
-
- `report.datasource.link` 回 `DELIVERED` 但 `readback.linkedCount:0` = **假成功**:新模型尚未同步进 BI(`cloudpivot_report.i_datasource`/`i_modelschema` 无记录)。前置条件是先在设计器打开一次报表/触发同步;判定以 `linkedCount>0` 为准,不以 deliveryState 为准。0.4.54 起该形状直接被拒绝(`report-datasource-link-not-synced`)。
|
|
72
|
-
|
|
73
|
-
## 应用发布边界
|
|
74
|
-
|
|
75
|
-
- `app.create`/`configure_model` 的 `publishBoundary.inScope:false` + `hint:publish-in-designer` 表示**应用发布必须在设计器完成**。交付说明必须把它列进「剩余人工动作」,不得宣称"全部配好"。
|
|
76
|
-
|
|
77
|
-
## 视图
|
|
78
|
-
|
|
79
|
-
- `conditions[].visible` 未传时 CLI 默认 `true`;只有需求明确隐藏时传 `false`。
|
|
80
|
-
- `fieldCodes` 不要加入系统字段。平台可能在最终查询面保留默认系统列,那不是 agent 配置的业务列。
|
|
81
|
-
- 新模型先配列表视图再依赖 `data.list`。部分 8.6.26 读面在没有 query/view 配置时会把业务列投影为 null;单行核对可用 `data.load`。
|
|
82
|
-
- **默认列表视图不能单独 `listview.delete`**(平台回 `303025 列表是默认列表`);要清掉它就删模型(`model.delete` 级联)。清理时别把它当失败。
|
|
83
|
-
|
|
84
|
-
## 按钮
|
|
85
|
-
|
|
86
|
-
- **列表/行级按钮**用 `listview.configure` 的 `buttons`(`useLocation: LIST|ROW`)。
|
|
87
|
-
- **表单按钮是另一层**:`h_biz_button.triggerType=FORM`(非 QUERY),由 `form/publish` 读 `biz_sheet.options.customButtonJson` 生成;表单页按**流程节点 `buttonPermission.customBtns[].enabled`**(「表单操作」勾选)过滤。`useLocation:FORM` 不显示。
|
|
88
|
-
- `SCRIPT` 需 `actionConfig.scriptData`;`BUSINESS_RULE`/`FLOW` 需 `targetCode`;`ADD_DATA`/`FORM` 目标在 `actionConfig`(`schemaCodeVal`+`formListData`+`configureList`)。
|
|
89
|
-
- `WITH_VIEW` 打开表单、`WITH_OUT_VIEW` 直接执行;行级按钮用 `ROW` 才有行上下文;列表按钮绑的规则用 `rule.run` 验收。
|
|
90
|
-
|
|
91
|
-
## 子表
|
|
92
|
-
|
|
93
|
-
- 一个主表可以有多个 `CHILD_TABLE`,子表不能嵌套 `CHILD_TABLE`。
|
|
94
|
-
- **子表控件(type 201)的显示/只读条件(displayFormula/readonlyFormula)不在字段面**:`field.update` 对子表父控件必拒(只管子表**列**)。可行路径是表单 attributes:`form.get --controls` 读 `draftAttributesJson` 定位控件 → `form.draft` 改该控件条件 → `form.publish`(`omc next "子表控件显示条件"` 已路由到该链)。
|
|
95
|
-
- **直调 `runtime/form/save` 更新既有记录时主键放 `data.id`**:`bizObject.bizObjectId` 不被保存逻辑识别——接口仍回 `errcode:0` 但 `objectId` 是**新 id**(静默新增孤儿数据,券商 issue 2026-10-27 实证 2 条)。用 `omc call data.update` 则无此坑(`bizObjectId` 在顶层参数位)。
|
|
96
|
-
- 子列是子表字段 code 对应 schema 上的普通字段:
|
|
97
|
-
|
|
98
|
-
```bash
|
|
99
|
-
# --env 在单环境项目(.omc-env 在位)可省;下例显式写出仅为多环境演示
|
|
100
|
-
omc call field.create --env local --args '{"schemaCode":"expense_items","code":"item_note","name":"明细备注","propertyType":"LONG_TEXT"}'
|
|
101
|
-
omc call field.update --env local --args '{"schemaCode":"expense_items","code":"item_note","name":"备注"}'
|
|
102
|
-
omc call field.delete --env local --args '{"schemaCode":"expense_items","code":"item_note"}'
|
|
103
|
-
omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
父 `CHILD_TABLE` 行不接受 `options/choices/subFields` 更新;子列直接走上面的 CRUD 面。
|
|
107
|
-
|
|
108
|
-
- **更新既有记录时,子表行必须带 `id`**(r17b 2026-10-26 真机):`data.update`/`data.submit(bizObjectId)` 里无 `id` 也无显式 `rowStatus` 的子行,平台按**新增插入**(不判重)——同一条记录连发 4 次带 2 行无 id 子表的 update,子行从 2 变 8,聚合 SUM 翻倍。改既有行:先 `data.load` 拿子行 id 原样带回(→ rowStatus=Modified);有意追加新行显式写 `rowStatus:"Added"`。OMC 已写前拒绝 `submit-child-rows-missing-id`。
|
|
109
|
-
- **提交/审批都不要再回传子表数组**(真机 2026-10-19/20):`data.save` 建草稿后 `data.submit` 若带子表行(无 `id`),平台按新增追加(3→9);**审批提交同样**——`data.submit` 带 `workItemId`+`agree` 推进一步时回传子表,每级再 +1 行(1→2→3)。金额若显式传了合计不会翻倍,但行数与 SUM 口径已被污染,且没有报错。审批只回传标量字段(`agree`/意见/被改标量);确需改子行时带 `id`+`rowStatus:"Modified"`。验收加一条"审批后子表行数不变"。
|
|
110
|
-
|
|
111
|
-
## 写调用纪律
|
|
112
|
-
|
|
113
|
-
- **同一 `<env>` 的写调用必须串行发起**:`write-lock-held` 不只在"并行重放失败请求"时出现,也会在**并行发起两个无关写**时触发(实测:并行两条 `dictionary.save`、并行 `field.update`+`field.publish` 各失败一次)。读可并行,写不要。确实要并发进程时用 `--wait <ms>`(`omc call` / `omc intent apply` 都支持)让 CLI 内部排队,别把重试循环写在 shell 里。
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
| `
|
|
121
|
-
| `approval.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
154
|
-
-
|
|
155
|
-
-
|
|
156
|
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
- `
|
|
160
|
-
-
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
-
|
|
167
|
-
-
|
|
168
|
-
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
1
|
+
---
|
|
2
|
+
name: omc-write-hazards
|
|
3
|
+
description: 分发版 CLI 的底层写面安全形状。配置表单、视图、按钮、子表、关联、规则或在线 JS 时使用;CLI dry-lint 是主防线,浏览器不是常规验收步骤。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# CLI 写面安全形状
|
|
7
|
+
|
|
8
|
+
本技能随 `omc-cli` 包分发,可用:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
omc help omc-write-hazards
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
优先依赖 CLI 的 schema、contract、dry-lint、writable/生产确认和权威读回。浏览器只在用户报告明确的 UI 回归时使用。
|
|
15
|
+
|
|
16
|
+
## 表单与 Sheet201
|
|
17
|
+
|
|
18
|
+
- 控件 `options` 必须是 **object**,不能是 JSON 字符串。8.6.34 渲染器会写 `options.name_i18n`;字符串会抛 `Cannot create property 'name_i18n' on string` 并白屏。
|
|
19
|
+
- 子表父控件是 `type: 201`,业务子列放 `columns`;子列 key 不得进入 `viewJson`。
|
|
20
|
+
- 调用方显式传 `draftAttributesJson`/`draftViewJson` 时,CLI 会在写前拒绝字符串 options、非字符串 viewJson 单元、子列混入 viewJson。
|
|
21
|
+
- `form.draft rebuild:true` 会重建布局,但会保留已存在的非结构性控件 options;Sheet201 columns 以最新 subSchema 为准。
|
|
22
|
+
- 空布局 `[]`/`{}` 默认拒绝;只有显式 `allowEmptyLayout:true` 才能清空。
|
|
23
|
+
- **列数由 viewJson 行分组决定,不是 span**(2026-10-27 r24 真机实证):`viewJson` 是行的数组,一行内 N 个 code 即 N 列;`span` 只是 24 栅格下的控件宽度。只把字段 `span` 改 8 而不动行分组 → 控件变窄但每行仍两个,不是三列。改列数必须同时改行分组(三列=每行 3 个 code,字段 span=8)→ `form.publish` → `form.get` 回读 viewJson 核对。系统字段行与业务字段行分开调整。
|
|
24
|
+
- **下拉/单选/复选的选项在表单控件有两份独立副本**(2026-10-29 治豪 issue 同源事实,8.6/1.1.25 机制一致):数据项 `options.optionsSet.custom` 之外,表单控件的 `options.options.custom` 与 `options.optionsSet.custom` 是独立副本(× draft/published 双版本)——**只改字段不重存表单,门户/设计器不会变**。标准生效链:`field.update`(选项)→ `field.publish` → `form.draft`(rebuild 重建控件同步副本)→ `form.publish`;验收以表单面回读为准,数据项一致 ≠ 表单生效。
|
|
25
|
+
- **模型不支持跨应用迁移**(1.1.25 真机实证:`folder-other-application` 拒绝):模型建错应用时走「新应用重建 + 关联引用改指 + 旧模型删除」,不存在物理迁移命令。
|
|
26
|
+
- **流程编排器是线性的,无并行会签**(1.1.25 实证):多部门会签只能建成顺序节点链;需求里的「并行会签」按顺序节点落地并在交付说明里披露。
|
|
27
|
+
- **BIZ_ACTION 服务出参只认响应 JSON 根级平铺键**(p7/r26 双实证):嵌套在信封里(如 `{data:{level}}`)静默映射不上、目标字段全空——先把这类判断列进方案:接口形态要在「平铺 vs 信封」间做选择时,需要回写本平台的出参一律平铺。外部系统本身回信封的,扩展整形转发或放弃出参回写。
|
|
28
|
+
- **`{Originator.OU}` 参与人变量在 1.1.25 本环境不解析成部门**(r26 实证 4 轮误判):发起人部门参与人写法先小样本验证解析,失败改用显式部门/角色参与人,不要反复重配角色。
|
|
29
|
+
- **BRANCH 条件分支与运行时规则次序互斥**(r26 实证 3 轮):该版本 BRANCH 节点的工具校验与运行时执行规则冲突时,改用按钮触发或顺序节点路由条件,不硬调 BRANCH。
|
|
30
|
+
|
|
31
|
+
## 模型字段与关联
|
|
32
|
+
|
|
33
|
+
- **SQL 保留字预检**:字段 code 命中数据库关键字(如 `order`)时 wire 才回 `10033 编码[order]为SQL关键字`。建字段前先检查常见保留字(order/group/user/key/index/desc/asc/select/from/where/table),命中就换语义后缀(`orderRef`/`orderNo`),不要建了再改。
|
|
34
|
+
- **跨读面包目录键**:`apppackage/list` 投影的目录键是 `h_app_function.code`,`apppackage/trees` 的键是 `h_app_function.id`。把前者的 code 当 id 传给 `bizmodels/create.parentId` 会落成孤儿菜单行(模型读面可达、应用树/门户失踪)。0.4.54 起 `model.create` 的显式 `folderId` 会按 id/code 双口径归一;其它写面仍要自己归一。
|
|
35
|
+
- **`readback.present=false` 是硬失败信号**:平台可能接受请求但对象不在权威面(典型=孤儿挂载)。0.4.54 起交付状态降级为 `WRITE_UNVERIFIED`;禁止用"平台最终一致性"经验豁免。
|
|
36
|
+
|
|
37
|
+
- **code 定位写前先核在册**:`field.update` / `rule.node.update` 等按 code 定位的精确写,先 `model.inspect` / `field.list` 确认 code 在册;缺码即停并报"目标不存在 + 当前字段清单"——**别凭用户措辞假定 code 正确**(用户可能记错字段名),**也别擅自替换成语义相近的字段**(改错对象比不改更糟)。缺码 `field.update` 本就拒 301001,但写前先核能省一轮失败往返,并在草稿阶段就把"字段不存在"与"需求理解偏差"分清。
|
|
38
|
+
- **改名核对以字段读为准**:`field.update` 改名后,`model.inspect` 聚合面的 `fields[].name` 可能仍是旧值(发布态快照/聚合缓存);核对显示名用 `field.list`(默认清单面有 name);核对 options 用 `field.list` 的 `code` 选择器 / `field.get` 或 `form.publish` 回读——两处读数不一致时以字段读为准,别误判"写入没生效"而重复写。
|
|
39
|
+
- `propertyEmpty` 默认 `false`。模型层保持可空;必填、只读、可见性在表单和流程节点配置。
|
|
40
|
+
- 无映射关联单选 `WORK_SHEET` 使用 `mappings: ""`,不要用 `"[]"`。`"[]"` 是非空字符串,设计器会显示“已设置”,但弹窗没有映射行。
|
|
41
|
+
- 有映射时传非空 JSON 数组字符串,并在 draft/published 两面读回。
|
|
42
|
+
- **运行时写关联值是另一种形状**(`data.save`/`data.update` 的 formData):单选关联(`propertyType 9`)传 **id 字符串或 `{ id }`**,传**数组会稳定 50000**;**多选关联(11)传分号分隔的 id 字符串 `"id1;id2"`(或 `{id:"id1;id2"}`),传数组稳定 50000**;**人员/组织(50/51/60/61)传 `[{"id","type","name"}]`,`type` 必填**(缺它稳定 50000)——`type` 是 UnitType 索引:**用户=3、部门=1**。用错形状只会拿到一个不透明的 50000。
|
|
43
|
+
- **`mappings` 由表单运行时在用户选择时套用**,服务端 `data.save` **不会**自动回填映射目标字段。API 侧要自己写被映射字段,否则读回为空——别把它误判成“映射配置失败”。
|
|
44
|
+
- **关联映射的事实源在字段行 `options.mappings`,不在表单控件属性**(真机 2026-10-14 实证:改表单 attributes 里的映射会被平台按字段重算覆盖、回读不生效)。改映射走:`field.update` 按完整 options 提交(只改 mappings,其余键原值保留)→ `field.publish` → `form.draft`(**rebuild:false**,保留现有布局与在线JS)重存 → `form.publish` → 回读核对。**别用 cascadeRepublish 刷带布局/在线JS 的表单**:rebuild 会清空设计器公式(已做写前拒绝)。
|
|
45
|
+
- **运行时写值的最小形状**(组合出错只会得到一个不透明的 `50000`/NPE,逐一最小化再合):单选关联(9) → id 字符串 `"<id>"`;**多选关联(11) → 分号串 `"<id1>;<id2>"`**;**人员/组织(50/51/60/61) → `[{"id":"<id>","type":3,"name":"<名>"}]`,`type` 必填且为整数**(缺/非整数 → `BizField.parseFromClient` 拆箱 NPE→50000;用户=3、部门=1);日期(3) → 字符串 `"2026-09-30 10:00:00"`;子表(8) → 行对象数组 `[{"<子列code>":<值>, ...}]`。先各写一条最小行通过,再组合。
|
|
46
|
+
其中三类已有**写前拒绝**(r17b 2026-10-26 起):日期收到毫秒时间戳(平台接受但落库 NULL)、日期字符串不匹配字段声明 format(50000)、单值关联收到数组(50000)——blocker `submit-date-shape` / `submit-relation-shape`。
|
|
47
|
+
- **DATE(3) 字段的 `options.format` 必须是平台大写枚举**(`YYYY-MM-DD`、`YYYY-MM-DD HH:mm:ss`、`YYYY年MM月DD日`…),**不是 Java 的 `yyyy-MM-dd`**:平台 `DateBizField` 按字面比较,Java 写法能创建字段但**首次运行时 save 稳定 `50000`(`不支持的format`)**(2026-09-30 R5 实测)。`field.create`/`intent` 会就地拒绝非枚举值。
|
|
48
|
+
|
|
49
|
+
- **多选关联(11) 的写入源码依据**(1.1.25 实机 jar):`WorkSheetMultiBizField.contentFromClient` 只接受 `String` 或 `Map`,其它类型直接 `AppAsserts.isTrue(false, "Incorrect value type, should be string or map.")` → 表现为 50000;`contentToDbValue()` 返回 `map.get("id")`,而读回时按 `";"` 切分(`MULTI_WORK_SHEET_SPILT`)并组装 `items` 数组。所以**数组是读写不对称的读侧形状,不能拿来写**;分号串才是写侧形状。实测三种形状:`"<id>"`/`"<id>;<id>"` 成功、`["<id>"]` 50000。
|
|
50
|
+
|
|
51
|
+
## 业务规则与流程事件(源码实证,1.1.25)
|
|
52
|
+
|
|
53
|
+
- **`Create` 的触发判定是「旧状态→新状态」**:旧 `DRAFT` 且新 `PROCESSING` 或 `COMPLETED`(`DomainServiceImpl#getDefaultRuleCode`)。所以无流程模型一调 `data.submit`(→COMPLETED)**和流程表单提交(DRAFT→PROCESSING)都触发 Create**;`data.save` 存草稿不触发。不要因为"目标表没变化"就断言规则没触发。
|
|
54
|
+
- **副作用缺失归因三步**:先查执行记录(`h_log_biz_service` 按服务/方法/时间,或容器日志)→ ①无调用=规则/触发问题 ②有调用但失败=参数/权限/业务异常 ③调用成功但不可见=视图投影/事务未提交/查询条件。禁止只看目标表。
|
|
55
|
+
- **BIZ_ACTION 判成功只看传输**:REST 适配器解析响应后直接返回,**不校验 `errcode`**。扩展接口回 `{"errcode":50000}` + HTTP 200 会被当成节点成功(日志 `biz action node execute successful`)。要阻断必须走表单 `onValidate` 或数据规则,不能靠业务返回码。
|
|
56
|
+
- **`rule.run`/自定义规则不回写出参**(r26 实证 2 轮):出参合并只发生在预置 Update 规则的执行上下文;`rule.run` 手动触发与自定义规则不把服务出参写回本模型——出参落表要走「BIZ_ACTION 节点 outputMappings + 数据事件触发」,验收用 data.load 看字段而非 rule.run 回执。
|
|
57
|
+
- **出参回写三前提(r28b 8 组实验定案,单点总纲)**:① 响应 JSON 根级平铺出参键 ② 预置 Create/Update 规则上下文 ③ data.submit/data.update 触发(data.save 不触发)。三者缺一出参恒 null 且节点日志仍 successful——外部接口回信封的(`{code,message,data:[...]}`),先落平铺整形端点(平台扩展)再配 outputMappings,不要反复换规则形状试。
|
|
58
|
+
- **布尔出参映射到文本字段 → ClassCastException 50000**(r28b 实证):服务出参 `bizPropertyType:LOGICAL` 的值映射到 SHORT_TEXT 字段会让规则执行炸 50000——布尔出参只能映射 LOGICAL 字段;要落文本先在扩展侧转字符串。
|
|
59
|
+
- **审批提交的 formData 带子表数组 = 追加行**(r28b 实证):approval submit 的 formData 里带子表数组会被平台当「追加行」处理,审批回执只回传主表字段——审批链路里要改子表行,用 data.update 单独写,不要塞进 formData。
|
|
60
|
+
- **workItemId 不在 instanceActivity 里**(r28b 实证):流程实例活动查询不返回 workItemId;办理待办要的 workItemId 从 `search_workitems?state=PENDING` 的 `content[].id` 取。
|
|
61
|
+
- **模型创建/删除非原子,会有半态残留**(r28b 实证 ~20min):model.create 在表单层撞错(如「表单已存在」)时 schema 可能已半落库——refused 后换名重试前先 model.list 核对半态并清理;同理模型删除后平台读面不可见但库里可能留僵尸行(引擎报 `appFunction not exist` 就是它在作祟),迁移/复用编码前先核对。
|
|
62
|
+
- **流程事件处理器吞异常**:`start`/`end`/`cancel`/`activate` 中的 BIZ_ACTION 抛异常时,`InstanceContext#processingBusiness` 只记 `processingBusiness error.` 并返回失败结果,**流程继续**。事件处理器不能做"提交前拦截"。
|
|
63
|
+
- **禁写当前单据**:BIZ_ACTION 是 HTTP 自调用,外层事务已持行锁;同事务更新当前单据会等到 15s 超时并整条回滚。副作用写独立表;需要留痕用独立事务/异步重建 `RequestContext`。
|
|
64
|
+
|
|
65
|
+
## 运行数据读取的默认过滤
|
|
66
|
+
|
|
67
|
+
- `data.list` **默认不含 DRAFT**(`total` 也只算 COMPLETED):清测试数据时 `total:0` 不代表没有残留。核对/清理一律显式 `{"all":true,"showTotal":true}`,或读回体里的过滤标注。`data.load` 用于单行核对。
|
|
68
|
+
|
|
69
|
+
## 报表数据集
|
|
70
|
+
|
|
71
|
+
- `report.datasource.link` 回 `DELIVERED` 但 `readback.linkedCount:0` = **假成功**:新模型尚未同步进 BI(`cloudpivot_report.i_datasource`/`i_modelschema` 无记录)。前置条件是先在设计器打开一次报表/触发同步;判定以 `linkedCount>0` 为准,不以 deliveryState 为准。0.4.54 起该形状直接被拒绝(`report-datasource-link-not-synced`)。
|
|
72
|
+
|
|
73
|
+
## 应用发布边界
|
|
74
|
+
|
|
75
|
+
- `app.create`/`configure_model` 的 `publishBoundary.inScope:false` + `hint:publish-in-designer` 表示**应用发布必须在设计器完成**。交付说明必须把它列进「剩余人工动作」,不得宣称"全部配好"。
|
|
76
|
+
|
|
77
|
+
## 视图
|
|
78
|
+
|
|
79
|
+
- `conditions[].visible` 未传时 CLI 默认 `true`;只有需求明确隐藏时传 `false`。
|
|
80
|
+
- `fieldCodes` 不要加入系统字段。平台可能在最终查询面保留默认系统列,那不是 agent 配置的业务列。
|
|
81
|
+
- 新模型先配列表视图再依赖 `data.list`。部分 8.6.26 读面在没有 query/view 配置时会把业务列投影为 null;单行核对可用 `data.load`。1.1.25 复证(2026-10-31):脚手架默认视图只含 createdTime/creater/name/sequenceNo 四个系统列,`data.list` 对不在列面的业务字段一律回 null——**null ≠ 数据丢失**,先 `listview.get` 看列面;修复=`listview.configure {fieldCodes:[…业务列…], replaceColumns:true}`,配完立即回值。
|
|
82
|
+
- **默认列表视图不能单独 `listview.delete`**(平台回 `303025 列表是默认列表`);要清掉它就删模型(`model.delete` 级联)。清理时别把它当失败。
|
|
83
|
+
|
|
84
|
+
## 按钮
|
|
85
|
+
|
|
86
|
+
- **列表/行级按钮**用 `listview.configure` 的 `buttons`(`useLocation: LIST|ROW`)。
|
|
87
|
+
- **表单按钮是另一层**:`h_biz_button.triggerType=FORM`(非 QUERY),由 `form/publish` 读 `biz_sheet.options.customButtonJson` 生成;表单页按**流程节点 `buttonPermission.customBtns[].enabled`**(「表单操作」勾选)过滤。`useLocation:FORM` 不显示。
|
|
88
|
+
- `SCRIPT` 需 `actionConfig.scriptData`;`BUSINESS_RULE`/`FLOW` 需 `targetCode`;`ADD_DATA`/`FORM` 目标在 `actionConfig`(`schemaCodeVal`+`formListData`+`configureList`)。
|
|
89
|
+
- `WITH_VIEW` 打开表单、`WITH_OUT_VIEW` 直接执行;行级按钮用 `ROW` 才有行上下文;列表按钮绑的规则用 `rule.run` 验收。
|
|
90
|
+
|
|
91
|
+
## 子表
|
|
92
|
+
|
|
93
|
+
- 一个主表可以有多个 `CHILD_TABLE`,子表不能嵌套 `CHILD_TABLE`。
|
|
94
|
+
- **子表控件(type 201)的显示/只读条件(displayFormula/readonlyFormula)不在字段面**:`field.update` 对子表父控件必拒(只管子表**列**)。可行路径是表单 attributes:`form.get --controls` 读 `draftAttributesJson` 定位控件 → `form.draft` 改该控件条件 → `form.publish`(`omc next "子表控件显示条件"` 已路由到该链)。
|
|
95
|
+
- **直调 `runtime/form/save` 更新既有记录时主键放 `data.id`**:`bizObject.bizObjectId` 不被保存逻辑识别——接口仍回 `errcode:0` 但 `objectId` 是**新 id**(静默新增孤儿数据,券商 issue 2026-10-27 实证 2 条)。用 `omc call data.update` 则无此坑(`bizObjectId` 在顶层参数位)。
|
|
96
|
+
- 子列是子表字段 code 对应 schema 上的普通字段:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# --env 在单环境项目(.omc-env 在位)可省;下例显式写出仅为多环境演示
|
|
100
|
+
omc call field.create --env local --args '{"schemaCode":"expense_items","code":"item_note","name":"明细备注","propertyType":"LONG_TEXT"}'
|
|
101
|
+
omc call field.update --env local --args '{"schemaCode":"expense_items","code":"item_note","name":"备注"}'
|
|
102
|
+
omc call field.delete --env local --args '{"schemaCode":"expense_items","code":"item_note"}'
|
|
103
|
+
omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
父 `CHILD_TABLE` 行不接受 `options/choices/subFields` 更新;子列直接走上面的 CRUD 面。
|
|
107
|
+
|
|
108
|
+
- **更新既有记录时,子表行必须带 `id`**(r17b 2026-10-26 真机):`data.update`/`data.submit(bizObjectId)` 里无 `id` 也无显式 `rowStatus` 的子行,平台按**新增插入**(不判重)——同一条记录连发 4 次带 2 行无 id 子表的 update,子行从 2 变 8,聚合 SUM 翻倍。改既有行:先 `data.load` 拿子行 id 原样带回(→ rowStatus=Modified);有意追加新行显式写 `rowStatus:"Added"`。OMC 已写前拒绝 `submit-child-rows-missing-id`。
|
|
109
|
+
- **提交/审批都不要再回传子表数组**(真机 2026-10-19/20):`data.save` 建草稿后 `data.submit` 若带子表行(无 `id`),平台按新增追加(3→9);**审批提交同样**——`data.submit` 带 `workItemId`+`agree` 推进一步时回传子表,每级再 +1 行(1→2→3)。金额若显式传了合计不会翻倍,但行数与 SUM 口径已被污染,且没有报错。审批只回传标量字段(`agree`/意见/被改标量);确需改子行时带 `id`+`rowStatus:"Modified"`。验收加一条"审批后子表行数不变"。
|
|
110
|
+
|
|
111
|
+
## 写调用纪律
|
|
112
|
+
|
|
113
|
+
- **同一 `<env>` 的写调用必须串行发起**:`write-lock-held` 不只在"并行重放失败请求"时出现,也会在**并行发起两个无关写**时触发(实测:并行两条 `dictionary.save`、并行 `field.update`+`field.publish` 各失败一次)。读可并行,写不要。确实要并发进程时用 `--wait <ms>`(`omc call` / `omc intent apply` 都支持)让 CLI 内部排队,别把重试循环写在 shell 里。
|
|
114
|
+
- **`data.delete` 批量形态 1.1.25 假成功**(r30 2026-10-31 真机):`bizObjectIds` 数组回 `DELIVERED`+`absent:true`+`modelResidue.total` 递减但行仍在——OMC 已写前拒绝 `data-delete-batch-fake-success`。纪律不变:删除后用 `data.load exists:false`(或重查 `total`)复核,`absent:true` 不是删除成功的证明。
|
|
115
|
+
|
|
116
|
+
## 审批提交的真实形状(r18b 2026-10-26 真机,1.1.25)
|
|
117
|
+
|
|
118
|
+
| 参数 | 位置 | 语义 |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `agree` | **顶层** | true=通过 / false=驳回;**缺省=true**(驳回后实例仍 PROCESSING,别当成功) |
|
|
121
|
+
| `approval.content` / `relUsers` / `resources` | approval 对象 | 意见 / 下一步审批人 / 附件 |
|
|
122
|
+
| `approval.agree`、`approval.opinion` | — | **静默忽略**(真机:agree:false 仍 APPROVE 且走完流程;opinion 不落库)。OMC 已写前拒绝 |
|
|
123
|
+
|
|
124
|
+
链:`data.save`(draft→workItemId) → `data.submit`(顶层 agree) → 下一级待办。验收看 `sequenceStatus`。
|
|
125
|
+
|
|
126
|
+
## 计算规则(datarule)的取值时机
|
|
127
|
+
|
|
128
|
+
- **字段计算规则(`datarule.create` `dataRuleType:2`)在 REST 路径上不会执行、也不会回填**(r17 2026-10-25 复核,AI-Platform 1.1.25):
|
|
129
|
+
`data.submit` 与 `data.update` 写完目标字段**仍是调用方传入值**;`docker logs` 里没有任何 CalculateHandler/formula-recalc 记录。
|
|
130
|
+
- 实测:`SUM({rows.amt})` 的 type2 规则 + 两行明细(10+20),submit 传 `total:0` 回读仍是 `0`;`data.update` 传 `0` 也仍是 `0`。
|
|
131
|
+
(早前「覆盖传入值、API 可判定」的结论来自 r12,已被推翻——不要再用它做验收。)
|
|
132
|
+
- 源码门控:`CalculateHandler.java:50` 的 `forceExecuteCalculateRule` 只在 `BizObjectOptions.executeCalRule()` 为真时求值,而该 builder 全库 0 调用方(REST 路径从不置真)。
|
|
133
|
+
- 所以「计算规则是否生效」**不能用 API 判定**;正确性只能在门户表单里验。API 调用方请自己算好再写。
|
|
134
|
+
- **跨模型汇总可以走 FORMULA(100) + AGGREGATION,而且 REST 能回算**(r17 真机 100→105;r17b 2026-10-26 补齐行为图谱):
|
|
135
|
+
`aggregation.schemaCode` 指向**另一个模型**,`queryConditions` 必须给 DYNAMIC 条件
|
|
136
|
+
(`propertyCode`=来源模型字段、`currentPropertyCode`=目标模型字段)。配方:`omc describe field.create --example formula-aggregation-cross-model`。
|
|
137
|
+
已验证的形状:FIXED+DYNAMIC 混用、多组 OR、COUNTIF/MAX、子表行级条件(`propertyCode` 带 `.`)、来源行 DELETE 也会回算。
|
|
138
|
+
**回算可靠性(重要)**:① 目标行**自己 submit** 时镜像规则一定算(最可靠);② 来源行 submit/update/delete 的事件回算**只在目标模型发布后几分钟内可靠**,之后静默停止(无报错,源变了目标不更新)——重新 `field.publish --force` 目标模型恢复。验收:发布后立刻改来源行并读目标字段;长期对账别依赖事件回算。
|
|
139
|
+
- **同模型(含自己的子表)聚合不可用**:`field.publish` 必抛 305003 —— `publishFormula` 把相对码改写成子表父模型(= 本模型),
|
|
140
|
+
`checkCycleOfAggregation:1032` 判成环。与 payload 无关,8.6.26 与 1.1.25 代码相同。OMC 已写前拒绝 `aggregation-self-schema-cycle`。
|
|
141
|
+
- **聚合必须指向来源主表的标量列**(r18/r18b 真机):`schemaCode` 指向**子表 schema** 能发布,但目标模型从此整个不可写
|
|
142
|
+
(写入一律 `40000`,无明细),且子表无父单单号列、匹配恒 0;`dataItemCode` 写 `<子表>.<子列>` 则 publish 回 309004。
|
|
143
|
+
要子表合计:先在来源主表物化一个数值字段,再聚合该标量。写前拒绝:`aggregation-target-is-child-schema` / `aggregation-data-item-not-scalar`。
|
|
144
|
+
- **`40000` 无明细时**先怀疑字段级配置(上面的聚合、公式序列化),用「删/禁一个公式字段再写一次」二分,别从表单/权限查。
|
|
145
|
+
OMC 会转成 `runtime-write-db-exception` 并列出该模型的公式字段。
|
|
146
|
+
- **`LARGE/SMALL` 必须带 k**:`LARGE({来源.列},2)`(缺 k → NumberFormatException → 写入 50000);
|
|
147
|
+
**`UNION` 是文本拼接**,顶层 `resultType` 必须 0/1(写 2 会 50000)。拒绝:`aggregation-large-missing-k` / `aggregation-union-result-type`。
|
|
148
|
+
- **区别:`FORMULA`(计算公式,`propertyType:100`)字段的 EXPRESSION 模式仍只在前端算** —— API 写入不回填。
|
|
149
|
+
|
|
150
|
+
## 规则与业务集成
|
|
151
|
+
|
|
152
|
+
- 规则节点缺 `nodeName` 时 CLI 按平台 `RuleNodeType` 补默认名并给 warning;业务节点优先显式命名。
|
|
153
|
+
- BIZ_ACTION 先 `service.get` 读取现存方法参数面,再写 methodMapping。PROPERTY 输入使用 `codeType: 1`、`bizCode` 为业务字段 code、`propertyName` 为显示名、`serviceMethodParameterCode` 命中方法参数。
|
|
154
|
+
- **`codeType:1` 映射的 `bizCode` 必须是该模型上**已创建且已发布**的字段 code**:指向不存在/未发布的字段时,平台 publish 只回一个**无明细的 `550018 保存规则失败,规则存在异常`**(真因只经设计器「检测」/`POST /api/app/businessrule/node_run_map → notExistPropertyObjectList` 可见)。`rule.save` 现已本地前置校验并拒绝(`rule-graph-bizaction-mapping-field-missing`);改字段族类型也要同步映射。
|
|
155
|
+
- **把 BIZ_ACTION 挂到预置数据事件规则(`Create`/`Update`)上时**:映射的 `bizCode` 数据项必须真实存在于该模型,否则发布被拒 `550010 业务规则使用的数据项不存在`(典型:REST 短文本入参要映射一个关联字段——先在模型加一个短文本「快照」字段承接,再映射它)。若预置规则仍不接受,改挂**自定义按钮触发规则**(`customTrigger:true`)或 `Update` 规则,更稳;不要因此判定“平台不支持 BIZ_ACTION”。
|
|
156
|
+
- **BIZ_ACTION 的 `outputMappings` 按“响应 JSON 根级路径”取值**(源码 `MapOps.toMapByBizServiceJson`/`convertJsonObject`:keyMap 以 `serviceMethodParameterCode` 为键,与响应体**根级**各键匹配)。所以 `serviceMethodParameterCode` 必须等于 REST 响应**根级**的键名:
|
|
157
|
+
- **扩展接口若返回信封 `ResponseResult{errcode,data:{x}}`,平铺出参 `x` 在根级找不到 → 静默空回写**(真机 2026-10-01 实证:`h_log_biz_service` 显示 invoke SUCCESS、`data.amount:300`,目标字段仍空)。**正解:扩展接口在根级返回业务负载**(`GET` handler 直接 `return Map.of("amount", n)`,不分信封),出参 code=根键 `amount`。
|
|
158
|
+
- 也**不要**试图用输出参数的 `subParameters` 下沉取值:实测把出参声明为父 `data`+`subParameters[value]` 会把整个 `{value:99}` 子对象赋给标量字段,触发 `数据项【x】的值是未知的类型:com.alibaba.JSONObject`(50000)。嵌套出参结构只用于 OBJECT/子表类的整体赋值,不能用来“取 `data.x` 这个标量”。
|
|
159
|
+
- 对照:平台自带 `skb_risk_service` 出参平铺 `riskLevel` ↔ 响应根级键;真机三态验证:平铺根级→回写 42 ✓;信封+平铺出参→不回写 ✗;信封+嵌套出参→50000 ✗。
|
|
160
|
+
- `method.test` 的 `externalSuccess:true` **不代表**回写成功——回写要另看目标字段实际值(`data.submit` 读回)。
|
|
161
|
+
- 流程结束新增目标表单必须运行验证;配置发布不等于映射成功。检查目标记录的每个字段。
|
|
162
|
+
- **`model.delete` 不物理清理 `h_business_rule`**:预置规则(Create/Update/GetList/Load/Delete/Available/Cancel)与历史版本行会残留在规则表(`rule.list` 对已删 schema 也读不到,属物理孤儿)。平台写面没有“按 schema 物理清规则”的手段;验收“零残留”以**可见面**为准(`model.list`/`rule.list`/`service.get`/列表/按钮),物理行需 DB 侧清理。清理顺序让规则先于模型删能减少、但不消除预置规则行。
|
|
163
|
+
|
|
164
|
+
## 在线 JS
|
|
165
|
+
|
|
166
|
+
- **本 CLI 只负责发布,不负责写 JS**。脚本内容的创作与增量合并不在本 CLI:若当前项目自带在线 JS 编写规范(例如其 `.cpdocs`/技能或项目文档),遵循它;否则向用户/设计者索取已定稿 `source`。不要在 omc 技能里手写/臆造 JS 逻辑。
|
|
167
|
+
- 经 omc 发布时用 `onlinejs.compose`:把已定稿的 `source` 组合进平台 skeleton 的 `customScript`,再 `publish`,并回读 `publishedHtmlJson` + SHA-256。不要用裸脚本覆盖平台 skeleton。
|
|
168
|
+
- **`replace:true` 对裸代码被拒是设计**:`replace` 必须传完整的 `(function(form){…})`,否则会写坏脚本槽让表单空白;同样,`merge` 传完整带外层的脚本也会被拒(避免嵌套丢内容)。远端 `customScript` 为空时,定稿脚本带外层 + `replace:true` 即可,不要把它当成缺陷。
|
|
169
|
+
- 源码不能有 import/export,事件名必须在支持集合内;compose 后读回 draft/published HTML,浏览器不是默认步骤。
|
|
170
|
+
|
|
171
|
+
## 固定验收顺序
|
|
172
|
+
|
|
173
|
+
执行阶段以 `omc-five-piece-flow` 为准;写面安全只额外强调两点:写前先 `commands/describe/contract` + dry-run 静态校验,写后权威 readback,必要时再用 CLI 触发运行验证。
|
|
174
|
+
|
|
175
|
+
## 审批提交回传子表 = 追加行(真机 2026-10-20)
|
|
176
|
+
|
|
177
|
+
带 `workItemId` / `workflowInstanceId` 的 `data.submit` 里放子表数组,平台按**追加**处理,不按行 `id` upsert、也不报错:
|
|
178
|
+
2 行明细回传两次 → 6 行,`totalValue` 30000 → 90000,审批节点的 `editable:false` 拦不住服务端写入。
|
|
179
|
+
**只传主表字段**;确需追加行时显式 `allowChildRowAppend:true`(0.4.63 起默认拒绝,blocker `submit-approval-child-rows`)。
|
|
180
|
+
收尾时前后各读一次子表行数确认未变。
|
|
181
|
+
|
|
182
|
+
## 往有规则模型写入的反向副作用(r30 真机 2026-10-31)
|
|
183
|
+
|
|
184
|
+
影响评估不能只问"我的变更会不会破坏既有规则",还要问反方向:**目标模型的既有规则会不会在我写入之后执行并覆盖我写的字段**。
|
|
185
|
+
|
|
186
|
+
- 实测:往 `mcc_supplier` 建档(别模型的 CREATE 节点)→ 目标模型自己的 `Create` 规则(BIZ_ACTION 调企查查)随后执行,回写值**覆盖**映射进去的 `supplier_name`/`tax_id_number`;上游挂或喂占位名(`SDMTEST`)→ 覆盖成**空**(白跑两轮闭环)。**建成行的字段值取决于既有规则的回写结果,不是你映射了什么**。
|
|
187
|
+
- 写前必答:目标模型 `rule.list` 有没有会改写你目标字段的规则?执行顺序?喂给它的输入有效吗?依赖外部接口的(BIZ_ACTION)先 `method.test` 验上游。
|
|
188
|
+
|
|
189
|
+
## 审批身份与角色作用域(r30 真机 2026-10-31)
|
|
190
|
+
|
|
191
|
+
- `workitem.list` 只回**当前登录人**的待办:审批人配别人 → 看不到待办,`data.submit` 被 `submit-workitem-not-mine` 拒(40005)。演示闭环要么节点参与者用 `{Originator}`,要么把登录人加进审批角色。
|
|
192
|
+
- **`org.role.addUsers` 必须紧跟 `org.role.setUserScope`**:只加成员不布作用域,平台不报错(仅 `org-role-member-scope-empty` 警告),但成员**无法被流程解析为参与人**。一对调用别做半。
|
|
193
|
+
- **`transferDepartment` 迁人不迁作用域**(r33 真机):换部门后 `role.userScope` 仍指旧部门——迁移后复核成员作用域。
|
|
194
|
+
- **`user.delete` 是软删**(r33 真机):回 `deleted:true` 但 `user.info` 仍 `exists:true`(DISABLE)——验证口径用列表/状态,别称物理清除。
|
|
195
|
+
- **「名称重复 200001」≠ 失败**(r33 真机):多为首次已建成、重发才撞重复——先读回确认首建结果再决定。
|
|
196
|
+
- **删模型≠删干净(收藏夹死引用)**(2026-11-01 真机):模型删除后用户收藏(`h_user_favorites`,含 `_wf` 变体)与 h_biz_property/query/sheet/data_rule/button/method_mapping 等行残留——之后**每次**数据读写都在 launcher 日志抛 `appFunction not exist, schemaCode:<死模型>`(刷屏不阻断)。清理=按 schemaCode 关联删八表+收藏(维护者);见此报错先查收藏夹。
|
|
197
|
+
|
|
198
|
+
## workflow.update 的行级替换语义(r32 真机 2026-10-31)
|
|
199
|
+
|
|
200
|
+
- **`btnConfigList` 局部行=整行替换**:只传 `{code, remark}` 会把该按钮的 `enabled` 静默置 `false`、`commentPermission` 置 `null`("驳回意见必填"丢失),同提交未提及的其他属性也被重置。改前先 `workflow.get_published`/`get_draft` 取整行,改目标属性后**整条回写**;`propertyPermissions` 同粒度。
|
|
201
|
+
- **`SINGLE_PARTICIPANT` 多角色只取第一个解析成员**(MULTI 则全员会签)——"配置没生效"常是它,回读确认解析人。
|
|
202
|
+
|
|
203
|
+
## 跨表回写的真实语义(1.1.25 源码 + 真机 2026-10-20)
|
|
204
|
+
|
|
205
|
+
`targetObjectCode` 的**点号是目标侧路径**,不是来源字段路径:
|
|
206
|
+
|
|
207
|
+
| 写法 | 语义 | 执行 |
|
|
208
|
+
|------|------|------|
|
|
209
|
+
| `"asset_ledger"` | 目标主表 | 先按 `mainConditions` 查目标主表(**只查一次**),再逐行改 |
|
|
210
|
+
| `"asset_apply.asset_apply_items"` | 目标主表下的子表 | 先查目标主表,再用 `sheetConditions` 在**已查出的主表 ID 范围内**筛目标子表行 |
|
|
211
|
+
|
|
212
|
+
对应源码:`UpdateServiceExecutor.getUpdatedMainBizObject()` 只把 `mainConditions` 交给目标主表查询;`sheetConditions` 只在 `doUpdateChildData()` 里被读取。
|
|
213
|
+
|
|
214
|
+
两个前提(真机 2026-10-21):① **目标行必须已提交**——DRAFT 行不参与目标查询,`data.save` 建的行永远改不到,日志只有「查找范围不存在数据」;② **单跳关联字段可直接作 DYNAMIC 来源**(引擎取关联对象 id),点号路径按子表解析,`关联字段.id` 与两跳都不支持——用表单映射把目标 id 落到本模型字段再引用。
|
|
215
|
+
|
|
216
|
+
由此得到四条使用纪律:
|
|
217
|
+
|
|
218
|
+
1. **主表目标上的 `sheetConditions` 是死配置**——不会生效(OMC 0.4.64 起给 warning)。
|
|
219
|
+
2. **来源子表字段是集合,不是逐行扇出**:`currentSchemaDataItem:"items.asset"` 会被解析成该子表所有行的值集合,目标主表仍只查询一次。写这个形状能存能发布,但运行 0 行命中(r5-asset 真机)。OMC 0.4.64 起 `rule-graph-cross-model-child-source-fanout` **拒绝**。
|
|
220
|
+
3. **一张单多行逐件回写**(每条明细各改一条另一模型的主表行)没有可用的纯配置形状——走 BIZ_ACTION 自调二开接口,一次处理多行;单行回写用主表字段做 DYNAMIC 来源。
|
|
221
|
+
4. **BRANCH 后接两个跨表 UPDATE** 真机报 50000;改用单个 UPDATE + 条件动作,或二开。
|
|
222
|
+
|
package/dist/approval-token.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { createHash, createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
-
export class ApprovalTokenRejectedError extends Error {
|
|
3
|
-
reason;
|
|
4
|
-
constructor(reason) {
|
|
5
|
-
super(`approval token rejected: ${reason}`);
|
|
6
|
-
this.reason = reason;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
function canonical(value) {
|
|
10
|
-
return JSON.stringify(sort(value));
|
|
11
|
-
}
|
|
12
|
-
function sort(value) {
|
|
13
|
-
if (Array.isArray(value))
|
|
14
|
-
return value.map(sort);
|
|
15
|
-
if (value && typeof value === "object")
|
|
16
|
-
return Object.fromEntries(Object.entries(value)
|
|
17
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
18
|
-
.map(([k, v]) => [k, sort(v)]));
|
|
19
|
-
return value;
|
|
20
|
-
}
|
|
21
|
-
export function packageFingerprint(plan) {
|
|
22
|
-
return createHash("sha256")
|
|
23
|
-
.update(canonical({
|
|
24
|
-
environment: plan.environment,
|
|
25
|
-
environmentFingerprint: plan.environmentFingerprint,
|
|
26
|
-
actions: plan.actions.map((a) => ({ tool: a.tool, args: sort(a.args) })),
|
|
27
|
-
}))
|
|
28
|
-
.digest("hex");
|
|
29
|
-
}
|
|
30
|
-
export class ApprovalTokenService {
|
|
31
|
-
key;
|
|
32
|
-
ttlMs;
|
|
33
|
-
now;
|
|
34
|
-
used = new Set();
|
|
35
|
-
constructor(options) {
|
|
36
|
-
const key = Buffer.from(options.key, "base64");
|
|
37
|
-
if (key.length !== 32)
|
|
38
|
-
throw new Error("approval token key must be exactly 32 bytes (base64).");
|
|
39
|
-
this.key = key;
|
|
40
|
-
this.ttlMs = options.ttlMs ?? 10 * 60 * 1000;
|
|
41
|
-
this.now = options.now ?? Date.now;
|
|
42
|
-
}
|
|
43
|
-
requestApproval(plan) {
|
|
44
|
-
const approvalId = randomUUID();
|
|
45
|
-
const fingerprint = packageFingerprint(plan);
|
|
46
|
-
const issuedAt = this.now();
|
|
47
|
-
const expiresAt = issuedAt + this.ttlMs;
|
|
48
|
-
const payload = canonical({
|
|
49
|
-
v: 1,
|
|
50
|
-
approvalId,
|
|
51
|
-
packageFingerprint: fingerprint,
|
|
52
|
-
issuedAt,
|
|
53
|
-
expiresAt,
|
|
54
|
-
});
|
|
55
|
-
const sig = createHmac("sha256", this.key).update(payload).digest("base64url");
|
|
56
|
-
const token = Buffer.from(payload, "utf8").toString("base64url") + "." + sig;
|
|
57
|
-
return {
|
|
58
|
-
token,
|
|
59
|
-
approvalId,
|
|
60
|
-
packageFingerprint: fingerprint,
|
|
61
|
-
expiresAt: new Date(expiresAt).toISOString(),
|
|
62
|
-
ttlSeconds: Math.round(this.ttlMs / 1000),
|
|
63
|
-
singleUse: true,
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
verify(plan, token) {
|
|
67
|
-
const parts = token.split(".");
|
|
68
|
-
if (parts.length !== 2 || !parts[0] || !parts[1])
|
|
69
|
-
throw new ApprovalTokenRejectedError("token-malformed");
|
|
70
|
-
let payload;
|
|
71
|
-
try {
|
|
72
|
-
payload = Buffer.from(parts[0], "base64url").toString("utf8");
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
throw new ApprovalTokenRejectedError("token-malformed");
|
|
76
|
-
}
|
|
77
|
-
let decoded;
|
|
78
|
-
try {
|
|
79
|
-
decoded = JSON.parse(payload);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
throw new ApprovalTokenRejectedError("token-malformed");
|
|
83
|
-
}
|
|
84
|
-
const expectedSig = createHmac("sha256", this.key).update(payload).digest("base64url");
|
|
85
|
-
if (!timingSafeEqualB64(parts[1], expectedSig))
|
|
86
|
-
throw new ApprovalTokenRejectedError("token-signature-invalid");
|
|
87
|
-
if (this.used.has(String(decoded.approvalId)))
|
|
88
|
-
throw new ApprovalTokenRejectedError("token-already-used");
|
|
89
|
-
if (Number(decoded.expiresAt) <= this.now())
|
|
90
|
-
throw new ApprovalTokenRejectedError("token-expired");
|
|
91
|
-
if (decoded.packageFingerprint !== packageFingerprint(plan))
|
|
92
|
-
throw new ApprovalTokenRejectedError("package-fingerprint-mismatch");
|
|
93
|
-
this.used.add(String(decoded.approvalId));
|
|
94
|
-
return { approvalId: String(decoded.approvalId) };
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
function timingSafeEqualB64(a, b) {
|
|
98
|
-
const ba = Buffer.from(a);
|
|
99
|
-
const bb = Buffer.from(b);
|
|
100
|
-
return ba.length === bb.length && timingSafeEqual(ba, bb);
|
|
101
|
-
}
|
|
102
|
-
//# sourceMappingURL=approval-token.js.map
|