@stalfh233/omc-cli 0.4.74 → 0.4.76

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.
@@ -0,0 +1,129 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { readArtifact } from "./artifacts.js";
3
+ import { artifactRoot } from "./command-transport.js";
4
+ export function projectJsonPath(value, path) {
5
+ // 1.5 of issue 2026-10-23: an array-of-objects selection WITHOUT jq pipes.
6
+ // `.rules[code=Create]` (or `[code="Create"]`) picks the FIRST element whose
7
+ // key matches — exactly the shape callers reached for with
8
+ // `jq '.[] | select(...)'`, which then died on shell quoting.
9
+ const selector = /\[\s*([A-Za-z_][\w.]*)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\]]*?))\s*\]/.exec(path);
10
+ if (selector !== null) {
11
+ const [, key, dq, sq, bare] = selector;
12
+ const expected = String(dq ?? sq ?? bare ?? "").trim();
13
+ const before = path.slice(0, selector.index);
14
+ const after = path.slice(selector.index + selector[0].length);
15
+ const base = projectJsonPath(value, before);
16
+ if (base.missed)
17
+ return base;
18
+ const array = base.value;
19
+ if (!Array.isArray(array))
20
+ return { missed: true, at: before || "$", available: `该层不是数组(是 ${array === null ? "null" : typeof array});选择器 [${key}=${expected}] 需要数组` };
21
+ const readKey = (entry) => (entry !== null && typeof entry === "object" ? String(entry[key] ?? "") : "");
22
+ const index = array.findIndex((entry) => readKey(entry) === expected);
23
+ if (index < 0)
24
+ return { missed: true, at: `${before}[${key}=${expected}]`, available: `数组长度 ${array.length};${key} 的候选值:${[...new Set(array.map(readKey))].slice(0, 12).join("、") || "(无)"}` };
25
+ return after === "" ? { missed: false, value: array[index] } : projectJsonPath(array[index], after);
26
+ }
27
+ const normalized = path
28
+ .trim()
29
+ .replace(/^[.\/]+/, "")
30
+ .replace(/\[(\d+)\]/g, ".$1")
31
+ .replace(/\[(?:"([^"]+)"|'([^']+)')\]/g, (_all, dq, sq) => `.${dq || sq}`)
32
+ .replace(/\.{2,}/g, ".");
33
+ const segments = normalized.split(/[./]/).filter((segment) => segment.length > 0);
34
+ let current = value;
35
+ for (const segment of segments) {
36
+ if (Array.isArray(current)) {
37
+ const index = Number(segment);
38
+ if (!Number.isInteger(index) || index < 0 || index >= current.length)
39
+ return { missed: true, at: segment, available: `数组长度 ${current.length}(下标 0-${Math.max(current.length - 1, 0)})` };
40
+ current = current[index];
41
+ }
42
+ else if (current && typeof current === "object") {
43
+ const record = current;
44
+ if (!Object.prototype.hasOwnProperty.call(record, segment)) {
45
+ const keys = Object.keys(record);
46
+ return { missed: true, at: segment, available: keys.length > 0 ? keys.slice(0, 12) : "(该层是空对象)" };
47
+ }
48
+ current = record[segment];
49
+ }
50
+ else {
51
+ return { missed: true, at: segment, available: `路径在第 ${segments.indexOf(segment) + 1} 段之前已到标量/undefined(前段类型 ${current === null ? "null" : typeof current})` };
52
+ }
53
+ }
54
+ return { missed: false, value: current };
55
+ }
56
+ /** Collect every value of a repeatable flag (`--extract a --extract b`). */
57
+ function argvValues(args, name) {
58
+ const values = [];
59
+ for (let index = 0; index < args.length; index += 1) {
60
+ if (args[index] !== name)
61
+ continue;
62
+ const value = args[index + 1];
63
+ if (value !== undefined && !value.startsWith("--"))
64
+ values.push(value);
65
+ }
66
+ return values;
67
+ }
68
+ /**
69
+ * `call --extract/--extract-keys`: resolve paths inside the result's own
70
+ * artifact (or inside the result object when the tool returned no artifact).
71
+ * Returns undefined when there is nothing to project so the caller's result is
72
+ * emitted unchanged.
73
+ */
74
+ export function projectResultExtract(result, paths, keysOnly) {
75
+ const artifactId = typeof result.detailArtifactId === "string" ? result.detailArtifactId : typeof result.artifactId === "string" ? result.artifactId : undefined;
76
+ // Tools hand an in-process artifact as `detailFile` (a local path);
77
+ // command-transport later swaps it for detailArtifactId. Extraction runs
78
+ // BEFORE that swap, so accept both.
79
+ const detailFile = typeof result.detailFile === "string" ? result.detailFile : undefined;
80
+ let source = result;
81
+ let artifact;
82
+ let readFromArtifact = false;
83
+ try {
84
+ const content = artifactId !== undefined
85
+ ? (artifact = readArtifact(artifactRoot(), artifactId)).content
86
+ : detailFile !== undefined
87
+ ? readFileSync(detailFile)
88
+ : undefined;
89
+ if (content !== undefined) {
90
+ readFromArtifact = true;
91
+ const text = content.toString("utf8");
92
+ try {
93
+ source = JSON.parse(text);
94
+ }
95
+ catch {
96
+ source = text;
97
+ }
98
+ }
99
+ }
100
+ catch {
101
+ // A pruned/expired artifact must not mask the real tool answer.
102
+ return { ...result, extract: { artifactId: artifactId ?? null, error: "artifact not found", hint: "产物已过期或被清理;去掉 --extract 重新调用该工具即可重新生成" } };
103
+ }
104
+ const out = {
105
+ status: result.status ?? "ok",
106
+ ...(artifactId !== undefined ? { artifactId, sha256: artifact?.sha256 } : {}),
107
+ from: readFromArtifact ? "artifact" : "result",
108
+ };
109
+ if (keysOnly) {
110
+ const record = source && typeof source === "object" && !Array.isArray(source) ? source : undefined;
111
+ out.keys = record
112
+ ? Object.entries(record)
113
+ .map(([key, value]) => ({ key, bytes: Buffer.byteLength(JSON.stringify(value) ?? "null", "utf8"), kind: Array.isArray(value) ? `array[${value.length}]` : value === null ? "null" : typeof value }))
114
+ .sort((left, right) => right.bytes - left.bytes)
115
+ : [];
116
+ }
117
+ if (paths.length > 0) {
118
+ out.extract = paths.map((path) => {
119
+ const projected = projectJsonPath(source, path);
120
+ return projected.missed
121
+ ? { path, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }
122
+ : { path, content: projected.value ?? null };
123
+ });
124
+ if (paths.length === 1)
125
+ out.content = out.extract[0]?.content ?? null;
126
+ }
127
+ return out;
128
+ }
129
+ //# sourceMappingURL=path-projection.js.map
@@ -109,6 +109,22 @@ const VOLATILE_VERIFY_KEYS = new Set([
109
109
  "fixedPoint",
110
110
  "shortCode",
111
111
  ]);
112
+ /**
113
+ * Arrays whose ORDER is not part of the configuration's meaning. The platform
114
+ * reorders them on write (live 2026-10-23: a workflow.update restore read back
115
+ * `propertyPermissions` in a different order and rollback reported a phantom
116
+ * "mismatch" although every field/value matched). Canonicalize by a stable key
117
+ * before comparing.
118
+ */
119
+ const ORDER_INSENSITIVE_ARRAY_KEYS = {
120
+ propertyPermissions: (entry) => String(entry?.propertyCode ?? ""),
121
+ subPropertyPermissions: (entry) => String(entry?.propertyCode ?? ""),
122
+ activities: (entry) => String(entry?.activityCode ?? ""),
123
+ rules: (entry) => {
124
+ const rule = (entry ?? {});
125
+ return `${String(rule.preActivityCode ?? "")}->${String(rule.postActivityCode ?? "")}|${JSON.stringify(rule.ruleConditionGroup ?? rule.formula ?? null)}|${rule.utilizeElse === true ? "else" : ""}`;
126
+ },
127
+ };
112
128
  export function canonicalForVerify(value) {
113
129
  if (Array.isArray(value))
114
130
  return value.map(canonicalForVerify);
@@ -118,7 +134,22 @@ export function canonicalForVerify(value) {
118
134
  for (const key of Object.keys(record).sort()) {
119
135
  if (VOLATILE_VERIFY_KEYS.has(key))
120
136
  continue;
121
- out[key] = canonicalForVerify(record[key]);
137
+ // Workflow route conditions: 8.x stores `ruleConditionGroup: []` for a
138
+ // plain edge while the restore write drops the empty shape (semantically
139
+ // "no condition" either way). Treat both spellings as equal, else every
140
+ // workflow rollback on such a draft would report a phantom mismatch.
141
+ if (key === "ruleConditionGroup" && Array.isArray(record[key]) && record[key].length === 0)
142
+ continue;
143
+ const item = record[key];
144
+ const orderKey = ORDER_INSENSITIVE_ARRAY_KEYS[key];
145
+ if (orderKey && Array.isArray(item)) {
146
+ out[key] = item
147
+ .map((entry) => ({ key: orderKey(entry), value: canonicalForVerify(entry) }))
148
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
149
+ .map((wrapped) => wrapped.value);
150
+ continue;
151
+ }
152
+ out[key] = canonicalForVerify(item);
122
153
  }
123
154
  return out;
124
155
  }
package/dist/usage-log.js CHANGED
@@ -56,8 +56,17 @@ export function priorIdenticalSuccess(command, argsDigest, options = {}) {
56
56
  for (let index = tail.length - 1; index >= 0; index -= 1) {
57
57
  const record = tail[index];
58
58
  const recordCommand = String(record.command ?? "");
59
- if (options.invalidators?.includes(recordCommand) && sameKey(record))
60
- return count > 0 ? { count, requestIds } : undefined;
59
+ if (options.invalidators?.includes(recordCommand)) {
60
+ // `invalidateOnAny`: a write we cannot attribute to a business key must
61
+ // still invalidate. `rule.save --args-file` and `omc script` carry no
62
+ // schemaCode/ruleCode in the usage record, so key-scoped invalidation
63
+ // misses them — that let a stale memo answer `noop:true` AFTER a save and
64
+ // left the rule disabled (r10 F2, 0.4.74 regression).
65
+ if (options.invalidateOnAny === true)
66
+ return count > 0 ? { count, requestIds } : undefined;
67
+ if (sameKey(record))
68
+ return count > 0 ? { count, requestIds } : undefined;
69
+ }
61
70
  if (recordCommand !== command)
62
71
  continue;
63
72
  if (String(record.argsDigest ?? "") !== argsDigest)
package/dist/write.js CHANGED
@@ -22,7 +22,7 @@ import { plannableArgsRaw } from "./plan.js";
22
22
  import { defineTool, ToolRefusalError } from "./tool-types.js";
23
23
  import { assertFieldFamilyAllowed } from "./field-families.js";
24
24
  import { validateBusinessCode } from "./code-rules.js";
25
- import { validateFormLayout } from "./form-layout.js";
25
+ import { hardLayoutViolations, validateFormLayout } from "./form-layout.js";
26
26
  import { prepareRuleGraph, validateRuleNodeGraph } from "./lanes/rule-graph.js";
27
27
  import { WireError } from "./wire.js";
28
28
  /**
@@ -219,8 +219,14 @@ export function writeTool(tools, name, lane, scope, description, inputSchema, ex
219
219
  }
220
220
  // CFG-form-layout: a supplied draft layout must satisfy the platform's
221
221
  // span/row/coverage invariants before it reaches the environment.
222
- if (name === "form.draft" && typeof args.draftViewJson === "string" && typeof args.draftAttributesJson === "string") {
223
- const violations = validateFormLayout(parseJsonOrUndefined(args.draftViewJson), parseJsonOrUndefined(args.draftAttributesJson));
222
+ // `fromReadback:true` means "these faces came from form.get" — the strict
223
+ // shape gate cannot tell a platform layout from a hand-written one without
224
+ // the session, so defer to form.draft's session-aware comparison (which
225
+ // still refuses a modified face). See issue 2026-10-23 §1.3.
226
+ if (name === "form.draft" && args.fromReadback !== true && typeof args.draftViewJson === "string" && typeof args.draftAttributesJson === "string") {
227
+ // Only HARD violations refuse here: the platform's own scaffold/layouts
228
+ // carry overflow/full-row/child-column shapes (issue 2026-10-23 §1.3).
229
+ const violations = hardLayoutViolations(validateFormLayout(parseJsonOrUndefined(args.draftViewJson), parseJsonOrUndefined(args.draftAttributesJson)));
224
230
  if (violations.length > 0)
225
231
  throw new ToolRefusalError({ status: "refused", blocker: "form-layout-invalid", message: "表单布局未过出门前校验;按 validation 修正(span 白名单/行宽≤24/整行独占/控件覆盖率)", validation: violations });
226
232
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.74",
3
+ "version": "0.4.76",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,9 +50,9 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@stalfh233/omc-fake-platform": "0.4.74",
53
+ "@stalfh233/omc-fake-platform": "0.4.76",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.74",
55
+ "omc-core": "0.4.76",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",
@@ -219,6 +219,22 @@ rules:
219
219
  - 写完后 `field.publish` → `configure_form`(同步进控件)→ **门户打开表单改触发字段的值**,断言目标控件变 `required` / `disabled`(存储回读不算验证)。
220
220
  - 恒只读:`readonlyFormula: true`。
221
221
 
222
+ ### 日期类校验(不早于今天 / 截止日不早于开始日)
223
+
224
+ **这类需求走表单的「提交校验」,不要用规则图 BRANCH。** 平台有专门的日期值类型 `today`/`yesterday`/`tomorrow`(不是"没有今天的系统变量"):
225
+
226
+ - **字段 vs 今天**(`planDate` 不得早于今天)→ `datarule.create {dataRuleType:1, checkType:2}`:
227
+ ```json
228
+ {"schemaCode":"<m>","propertyCode":"planDate","dataRuleType":1,"checkType":2,
229
+ "name":"计划日期不早于今天",
230
+ "options":{"conditions":"{planDate} 大于等于 {\"valueType\":\"today\"}",
231
+ "prompt":"计划日期不能早于今天"}}
232
+ ```
233
+ - **字段 vs 字段**(结束日不早于开始日)→ 同一 `checkType:2`,右值必须写成值对象:
234
+ `{end_date} 大于等于 {"valueType":"currentModel","value":"start_date"}`(**裸 `{start_date}` 右值不会生效**)。
235
+ - **规则图里要按"今天"分支**(例如给逾期记录打标)→ 条件叶用 `ruleValueType:"today"` + `currentSchemaDataItem:"<offset>"`(`"0"`=今天、`"-7"`=7 天前、`"30"`=30 天后,可带单位 `"-1-week"`);**不要用 `ruleValueType:"DYNAMIC"` 去和另一个日期字段比**——那会 `data.submit` 稳定 50000(`SingletonDataset.getBizObjectProperty` NPE,r10 复现)。
236
+ - 校验规则的执行面:**门户前端提交时**执行;REST `data.submit`/`data.update` **不拦**(r9 已用越界数据实测)。要用 API 兜底就得二开。
237
+
222
238
  ### 规则图的触发条件(关键)
223
239
 
224
240
  规则支持**触发条件**,不要用「两条无条件规则」表达互斥结果:
@@ -232,6 +248,7 @@ rules:
232
248
  - **触发时机**:数据事件(`Create`/`Update`/`Delete`)**只分发到平台预置 code 行**;流程相关用预置 `Available`(流程生效)/`Cancel`(流程作废),或流程事件绑定 `workflow.bindRule` 的 `end`/`cancel`/`start`/`activate`。
233
249
  **"流程结束后执行"的两个正式入口(2026-10-16 真机反馈消歧)**:① **流程设计 → 流程结束后**(对应 `workflow.bindRule` 的 `end`,绑定自定义规则承载动作节点);② **平台自带业务规则的"流程生效"(预置 `Available`)行**。两者都在**流程实例走完归档/审批通过那一刻**触发,**不是进入流程时**——进入流程时刻是 `start`/`activate`。业务要"审批通过后执行某逻辑"时只用 ①或②,**禁止**绑 `start`/`activate`,也勿误用 `Create`(提交即触发,时序等同流程发起)。
234
250
  - **预置规则身份不可改**:`Create`/`Update`/`Delete`/`Load`/`GetList`/`Available`/`Cancel` 名称由平台维护(`Create`=数据新增);传 `name` 不生效(仅自定义规则名称可改),可改的是节点名 `nodeName`。
251
+ - **节点级事件**:`workflow.bindRule` 还可传 `activityCode`+`nodeEvent`(`endActivity`=提交离开该节点、`activityApproved`/`activityRejected`、`beforeActivate`/`afterActivate`、`cancelActivity`、`jobSubmitted`/`jobRejected`),与 `handler` 互斥。
235
252
  - **流程事件只绑自定义规则**:`workflow.bindRule` 的 `ruleCodes` 只能给自定义 code——设计器的事件规则选择器不提供预置触发行(绑 `Create` 到 `end` 非法,工具 fail closed)。要「审批通过后执行某逻辑」:新建自定义规则承载动作节点再绑 `end`。
236
253
  - **审批通过/驳回回写**:通过绑 `end`(或 `Available`)、驳回绑 `cancel`(或 `Cancel`)——**不要**把两条各自 FIXED 赋值的规则都绑到 `endEventHandler`(两条都会执行、后者覆盖前者;工具会在 `bizActions.length>1` 时回读 `advisory` 提醒)。真实生产只在 `endEventHandler` 绑**一条**多分支规则图。
237
254
 
@@ -283,14 +300,15 @@ BIZ_ACTION:
283
300
  - **常量入参用 `codeType: 0`(CONST)**(如固定请求头 `X-LowCode-Corpid`):值可放 `bizCode`,也可放 `value`(工具会自动移入 `bizCode`)。平台 CONST **只读 `bizCode`**——`value`/`expressionValue` 原样存着的话,运行时取到的是字面 `null`。
284
301
  - 参数面每参数必带 `bizPropertyType`(参数类型),否则业务规则按类型匹配字段会失败、映射不到(缺失即拒绝);`externParameterType`/`description` 工具补齐。**返回值参数默认不必填(`required:false`),入参按需必填**——返回值声明为必填会经规则 designer 的 `required` 镜像强制每条输出映射也必填,是绑定摩擦的常见来源。
285
302
  - `rule.save` 自动补 `serviceName`/`serviceMethodName`/节点 `nodeCategory:"actionNode"`/映射元素 `index`(弹窗异步加载,稍候再判空)。
286
- - 配规则后读回 methodMapping,再用按钮或流程实际触发一次。
303
+ - 配规则后读回 methodMapping,再用 `rule.run` 或流程实际触发一次。
287
304
  - 服务、方法、参数不确定时拒绝猜,先读证据。
288
305
 
289
306
  ## 按钮与在线 JS
290
307
 
291
- - 直接执行规则:`useLocation: ROW` + `operateType: WITH_OUT_VIEW`。
308
+ - 直接执行规则:`useLocation: ROW` + `operateType: WITH_OUT_VIEW`,`bindAction: BUSINESS_RULE` + `targetCode: <自定义 ruleCode>`。
292
309
  - `WITH_VIEW` 是打开表单。
293
310
  - FLOW 按钮需要 workflowCode;CLI 会补安全的 actionConfig。
311
+ - **按钮规则验收用 `rule.run`**(= 按钮点击的请求):副作用用 `data.load` 另读(返回是执行前快照);规则须自定义且 enabled,被更新行须 COMPLETED(引擎不查 DRAFT)。
294
312
  - **在线 JS 分两条**:本 CLI 只做**读写与发布**——**读现存脚本用 `onlinejs.get`**(抽 customScript innerHTML 原文+SHA-256,不必拉 form.get --full 六份 JSON;据此决定 replace 还是默认 INCREMENTAL 并入),用 `onlinejs.compose` 把已定稿的 `source` 组合进平台 skeleton 的 `customScript` 再 publish,回读 `publishedHtmlJson`+SHA-256。**确认是否落地用 `summary:true`**(只回 sha/字节数/`draftPublishedInSync`,不回原文,约 600B;默认回显也已去重,不再把同一段脚本回三次)。**脚本内容的编写/增量合并不在本 CLI**:若当前项目自带在线 JS 编写规范(例如其 `.cpdocs`/技能或项目文档),遵循它;否则向用户/设计者索取已定稿的 `source`。不要在 omc 侧手写或臆造 JS 逻辑。
295
313
 
296
314
  ## 运行验收与清理
@@ -33,7 +33,7 @@ only the section for the capability you need —
33
33
  | 字段级规则 `datarule` | 字段级控件规则 | 字段级规则、控件规则、校验规则、计算拼接、显示规则、条件必填/只读/显示 | [字段级规则](#字段级规则-datarule) |
34
34
  | 业务规则 `businessrule` | 图形规则图 + **定时任务** | 业务规则、触发器、规则图、事件、BRANCH、BIZ_ACTION、**定时任务/定时/每天/周期/调度** | [业务规则](#业务规则-businessrule) |
35
35
  | 规则生命周期 `rule-lifecycle` | 规则启停删 | 启用规则、停用规则、删除规则、规则启停 | [规则生命周期](#规则生命周期-rule-lifecycle) |
36
- | 流程模板 `workflow` | 审批流程模板 | 流程、工作流、审批、节点、待办、流转、参与人、审批人 | [流程模板](#流程模板-workflow) |
36
+ | 流程模板 `workflow` | 审批流程模板 | 流程、工作流、审批、节点、待办、流转、参与人、审批人、表单操作按钮 | [流程模板](#流程模板-workflow) |
37
37
  | 表单在线 JS `onlinejs` | 表单脚本发布/回读 | 在线js、脚本、联动、onload、onvalidate | [表单在线 JS](#表单在线-js-onlinejs) |
38
38
  | 业务集成 `bizservice` | 集成服务与方法 | 集成、接口、服务、回调、对接、外部系统、datasql、restful | [业务集成](#业务集成-bizservice) |
39
39
  | 报表 `report` | 报表页 + BI 图表(**仅显式要求时**) | 报表、图表、看板、bi、dashboard(用户点名的才做) | [报表](#报表-report) |
@@ -90,13 +90,14 @@ only the section for the capability you need —
90
90
 
91
91
  ## 默认表单 `form`
92
92
 
93
- - **能做哪些事**:表单 draft-first 布局、发布、`publishedHtmlJson` 绑定读回;把已发布字段绑进默认表单。
94
- - **工具**:`form.get`、`form.draft`、`form.publish`、`configure_form`
93
+ - **能做哪些事**:表单 draft-first 布局、发布、`publishedHtmlJson` 绑定读回;把已发布字段绑进默认表单;**表单工具栏按钮**(`options.customButtonJson` → publish 生成 FORM 按钮)与按钮执行。
94
+ - **工具**:`form.get`、`form.draft`、`form.publish`、`configure_form`、`form.runButton`
95
95
  - **recipe**:`omc describe configure_form --example publish-default-form`
96
96
  - **边界 / 陷阱**:默认表单已存在,用 `configure_form`/`form.draft(rebuild)`/`form.publish(rebuild:true)` 改它,不要新建;改字段形状后刷新表单一调用 `form.publish rebuild:true`。
97
97
  - **每个模型交付前其默认表单必须已发布**:只建草稿=用户无法录入/查看。用 `configure_form` 或 `form.draft(rebuild:true)`+`form.publish` 发布。
98
98
  - **发布态以 `form.get` 的 `published`/`existDraft` 布尔为准**,不要看 `publishedControlCodes` 是否非空(草稿字段也非空,会误判)。交付前列出每个模型的 `form.get.published` 核对。
99
99
  - **写业务数据的前提是先发布模型+表单**:正确顺序 = 建模型 → 发布字段(`field.publish`)→ 建并**发布**默认表单(`form.publish`)→ 才 `data.save`/`data.submit`。不要在建/未发布状态下写业务数据。
100
+ - **表单按钮**:`form.publish {buttons:[…]}` 写 `options.customButtonJson` 并生成 `h_biz_button(triggerType=FORM)`;表单页显示还取决于流程节点 `buttonPermission.customBtns[].enabled`(设计器「表单操作」勾选)。执行用 `form.runButton`(带整条记录,区别于列表按钮的 `rule.run`)。
100
101
 
101
102
  ## 列表视图 `listview`
102
103
 
@@ -131,14 +132,14 @@ only the section for the capability you need —
131
132
 
132
133
  ## 规则生命周期 `rule-lifecycle`
133
134
 
134
- - **能做哪些事**:业务规则的启用、停用、批量停用、删除。
135
- - **工具**:`rule.enable`、`rule.disable`、`rule.delete`、`rule.disableMany`、`configure_rule`
136
- - **边界 / 陷阱**:`enabled` 是运行时真相,`status` 可能滞后。`rule.disableMany` 默认含预置行,`customOnly:true` 只停自定义。
135
+ - **能做哪些事**:业务规则的启用、停用、批量停用、删除、手动执行(按钮规则验收)。
136
+ - **工具**:`rule.enable`、`rule.disable`、`rule.delete`、`rule.disableMany`、`rule.run`、`configure_rule`
137
+ - **边界 / 陷阱**:`enabled` 是运行时真相,`status` 可能滞后。`rule.disableMany` 默认含预置行,`customOnly:true` 只停自定义。`rule.run` 只跑自定义 code 且须 enabled(预置行被拒);返回 data 是执行前快照,副作用用 `data.load` 另读;被更新行须 COMPLETED(引擎不查 DRAFT)。
137
138
 
138
139
  ## 流程模板 `workflow`
139
140
 
140
141
  - **能做哪些事**:流程模板 get_draft/get_published、create、update、publish、bindRule(事件处理器规则绑定)、delete、workitem.list、workflow.instanceActivity(实例节点状态诊断);参与人用组织对象。
141
- - **工具**:`workflow.get_draft`、`workflow.get_published`、`workflow.validate`、`workflow.create`、`workflow.update`、`workflow.publish`、`workflow.bindRule`、`workflow.delete`、`configure_workflow`、`workitem.list`、`workflow.instanceActivity`、`org.role.list`、`org.department.list`、`org.user.search`、`org.role.users`、`org.role.userScope`、`org.roleGroup.create`、`org.role.create`、`org.role.update`、`org.role.addUsers`、`org.role.removeUsers`、`org.role.setUserScope`、`org.department.create`、`org.department.update`、`org.department.delete`、`org.department.users`、`org.user.create`、`org.user.update`、`org.user.delete`、`org.user.info`、`org.user.departments`、`org.role.delete`、`org.roleGroup.delete`、`org.user.transferDepartment`
142
+ - **工具**:`workflow.get_draft`、`workflow.setButtons`(节点「表单操作」按钮勾选)、`workflow.get_published`、`workflow.validate`、`workflow.create`、`workflow.update`、`workflow.publish`、`workflow.bindRule`、`workflow.delete`、`configure_workflow`、`workitem.list`、`workflow.instanceActivity`、`org.role.list`、`org.department.list`、`org.user.search`、`org.role.users`、`org.role.userScope`、`org.roleGroup.create`、`org.role.create`、`org.role.update`、`org.role.addUsers`、`org.role.removeUsers`、`org.role.setUserScope`、`org.department.create`、`org.department.update`、`org.department.delete`、`org.department.users`、`org.user.create`、`org.user.update`、`org.user.delete`、`org.user.info`、`org.user.departments`、`org.role.delete`、`org.roleGroup.delete`、`org.user.transferDepartment`
142
143
  - **recipe**:`omc describe workflow.bindRule --example workflow-handler-binding`
143
144
  - **边界 / 陷阱**:greenfield 首版;`publish`/`bindRule` 不得在 `ValidateMessage[]` 拒绝时报告成功。运行时 workitem(提交/审批)走 `data.submit`;想知道实例停在哪/是否错派,用 `workflow.instanceActivity`(无 status=1=真停,参与人全是发起人=解析为空退回)。
144
145
 
@@ -69,10 +69,10 @@ omc help omc-write-hazards
69
69
 
70
70
  ## 按钮
71
71
 
72
- - 列表按钮用 `listview.configure` 的 `buttons`(传 `code`/`name`/`useLocation`/`bindAction`/`targetModel`/`targetForm`…);**不要因为没找到就叫「CLI 无列表按钮能力」**(dogfood 实测该误判会假阻塞整条需求)。配完 `listview.get --full` 回读 `customButtons`。
73
- - `BUSINESS_RULE` 按钮需要 target rule 和 actionConfig 对象。
74
- - `FLOW`/`FORM`/`ADD_DATA` 的 actionConfig 必须有 `configureList: []`;FLOW 还需要 `WorkflowListData[0].workflowCode` 与 `schemaCodeVal`。CLI 只给 FLOW 的 targetCode 时会自动展开。
75
- - `WITH_VIEW` 是打开表单;`WITH_OUT_VIEW` 才是直接执行。行级按钮要用 `useLocation: ROW` 才能拿到当前行上下文。
72
+ - **列表/行级按钮**用 `listview.configure` 的 `buttons`(`useLocation: LIST|ROW`)。
73
+ - **表单按钮是另一层**:`h_biz_button.triggerType=FORM`(非 QUERY),由 `form/publish` 读 `biz_sheet.options.customButtonJson` 生成;表单页按**流程节点 `buttonPermission.customBtns[].enabled`**(「表单操作」勾选)过滤。`useLocation:FORM` 不显示。
74
+ - `SCRIPT` 需 `actionConfig.scriptData`;`BUSINESS_RULE`/`FLOW` 需 `targetCode`;`ADD_DATA`/`FORM` 目标在 `actionConfig`(`schemaCodeVal`+`formListData`+`configureList`)。
75
+ - `WITH_VIEW` 打开表单、`WITH_OUT_VIEW` 直接执行;行级按钮用 `ROW` 才有行上下文;列表按钮绑的规则用 `rule.run` 验收。
76
76
 
77
77
  ## 子表
78
78