@stalfh233/omc-cli 0.5.7 → 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/dist/field-add.js CHANGED
@@ -17,6 +17,70 @@ import { argvValue } from "./args.js";
17
17
  import { runToolCall } from "./call.js";
18
18
  import { CLI_EXIT_CODES, classifyResult, envelope, refusalText, requestId } from "./cli-contract.js";
19
19
  import { writeEnvelope } from "./cli-output.js";
20
+ /**
21
+ * 字典简写展开(r30/r36 实证:字典字段要先 names 查 id、records 取记录、
22
+ * 再拼 optionsSet.dictionary 三段 4–5 调;`dictCode` 把它折进 field-add 一步):
23
+ * `{"code":"x","propertyType":"DROPDOWN_BOX","dictCode":"purchase_coop_status"}`
24
+ * → 内部解析 checkedDictionary(id)+checkedDictionaryCode+useDictionariesData(records)
25
+ * 按 dictionary-backed-single-select recipe 形状组装 options.optionsSet。
26
+ */
27
+ async function expandDictShorthand(fields, environment, contextForTest, request) {
28
+ const problems = [];
29
+ const expanded = [];
30
+ const call = async (tool, args) => {
31
+ const outcome = await runToolCall(tool, {
32
+ args,
33
+ requestId: request,
34
+ ...(environment !== undefined ? { environment } : {}),
35
+ ...(contextForTest !== undefined ? { context: contextForTest } : {}),
36
+ print: () => undefined,
37
+ });
38
+ return outcome.exitCode === 0 ? outcome.result : null;
39
+ };
40
+ const out = [];
41
+ for (const field of fields) {
42
+ const dictCode = field.dictCode;
43
+ if (typeof dictCode !== "string" || dictCode.length === 0) {
44
+ out.push({ ...field });
45
+ continue;
46
+ }
47
+ const rest = { ...field };
48
+ delete rest.dictCode;
49
+ // dictionary.list 是真假两平台都稳的清单面(names 的 getDictionaryIdAndNames
50
+ // 在 fake 上未实现);list 的回执可能是 {dictionaries:{content:[...]}} 分页
51
+ // 或裸数组,两种都吃。
52
+ const listResult = await call("dictionary.list", { limit: 200 });
53
+ const listPayload = listResult === null ? null : (listResult.dictionaries ?? listResult.names ?? listResult.content ?? listResult);
54
+ const names = Array.isArray(listPayload)
55
+ ? listPayload
56
+ : (Array.isArray(listPayload?.content) ? listPayload.content : []);
57
+ const hit = names.find((row) => String(row.code ?? row.dictionaryCode ?? "") === dictCode);
58
+ if (hit === undefined) {
59
+ problems.push(`${String(field.code)}: 字典 ${dictCode} 不存在(dictionary.names 未命中)——先 dictionary.save 或修正 dictCode`);
60
+ continue;
61
+ }
62
+ const dictId = String(hit.id ?? hit.dictionaryId ?? "");
63
+ const recordsResult = await call("dictionary.records", { code: dictCode, enabledOnly: true });
64
+ const recordsRaw = recordsResult === null ? [] : (recordsResult.records ?? {});
65
+ const recordRows = (Array.isArray(recordsRaw) ? recordsRaw : (recordsRaw.content ?? []));
66
+ if (recordRows.length === 0) {
67
+ problems.push(`${String(field.code)}: 字典 ${dictCode} 无启用记录——先补 dictionary.records`);
68
+ continue;
69
+ }
70
+ const useDictionariesData = recordRows.map((row) => ({ id: String(row.id ?? ""), code: String(row.code ?? ""), name: String(row.name ?? row.code ?? "") }));
71
+ out.push({
72
+ ...rest,
73
+ options: {
74
+ optionsSet: {
75
+ optionsType: "dictionary",
76
+ dictionary: { checkedDictionary: dictId, checkedDictionaryCode: dictCode, useDictionariesData },
77
+ },
78
+ },
79
+ });
80
+ expanded.push(String(field.code));
81
+ }
82
+ return { fields: out, expanded, problems };
83
+ }
20
84
  export function validateFields(schemaCode, fields) {
21
85
  const problems = [];
22
86
  const seen = new Set();
@@ -52,7 +116,8 @@ export async function cmdFieldAdd(args, contextForTest) {
52
116
  const forcePublish = args.includes("--force-publish");
53
117
  const usage = [
54
118
  "用法:omc field-add --schema <模型code> --fields '<JSON 数组>' [--apply]",
55
- " --fields 每项是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...}(choices/字典 options 原样透传)。",
119
+ " --fields 每项是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...}(choices/字典 options 原样透传);",
120
+ " 字典简写:{\"dictCode\":\"<字典code>\"} 代替手拼 options.optionsSet(内部解析字典 id+启用记录,recipe 同形状)。",
56
121
  " 默认 dry-run;--apply 依次执行 建字段 → 发布字段 → 重建表单 → 列表追加列。",
57
122
  " 幂等:字段已存在记 skipped 继续;有真失败默认不发布/不重建(--force-publish 强发)。",
58
123
  " --no-form 跳过表单重建;--no-list 跳过列表加列(默认两者都做)。",
@@ -78,6 +143,16 @@ export async function cmdFieldAdd(args, contextForTest) {
78
143
  const problems = validateFields(schemaCode, fields);
79
144
  if (problems.length > 0)
80
145
  return invalid(`--fields 有 ${problems.length} 处问题:\n - ${problems.join("\n - ")}`);
146
+ // 字典简写展开(dictCode → optionsSet.dictionary,含 id/记录解析)——展开失败
147
+ // 在任何写之前拒绝(名字对不上/无启用记录)。
148
+ let dictExpanded = 0;
149
+ if (fields.some((field) => typeof field.dictCode === "string")) {
150
+ const expandedResult = await expandDictShorthand(fields, environment, contextForTest, request);
151
+ if (expandedResult.problems.length > 0)
152
+ return invalid(`dictCode 展开失败(未写入):\n - ${expandedResult.problems.join("\n - ")}`);
153
+ fields = expandedResult.fields;
154
+ dictExpanded = expandedResult.expanded.length;
155
+ }
81
156
  const options = {
82
157
  schemaCode, fields, apply, publishForm, addToList, forcePublish,
83
158
  ...(environment !== undefined ? { environment } : {}),
@@ -115,6 +190,7 @@ export async function cmdFieldAdd(args, contextForTest) {
115
190
  status: "dry-run",
116
191
  schema: schemaCode,
117
192
  fields: fields.map((field) => String(field.code)),
193
+ ...(dictExpanded > 0 ? { dictExpanded } : {}),
118
194
  steps: [
119
195
  `field.create ×${fields.length}`,
120
196
  "field.publish(发布失败/有字段失败则停)",
@@ -1147,6 +1147,15 @@ export async function executeFormDraftWrite(session, args) {
1147
1147
  }
1148
1148
  : {}),
1149
1149
  ...(args.rebuild === true && previousAttributes ? { rebuildPreservedControls } : {}),
1150
+ // r36(2026-11-01):rebuild 后草稿与重建前逐字节一致时明示——rebuild 的
1151
+ // 语义就是"按已发布字段重搭",一致通常意味着预期的新控件没进来(字段未
1152
+ // 发布/写错槽位),静默 no-op 会让"新增字段后刷新表单"看起来成功实则没变。
1153
+ ...(args.rebuild === true && previousAttributes !== undefined && typeof attributes === "string" && attributes.trim() === JSON.stringify(previousAttributes).trim()
1154
+ ? {
1155
+ rebuildUnchanged: true,
1156
+ rebuildNote: "rebuild 后草稿与重建前完全一致——若你预期新增控件(新字段上表单),检查:① 字段是否已 field.publish;② 写入槽位 sheetCode 是否正确(省略=模型 code 槽位);r36 实测同序列下重建静默 no-op。",
1157
+ }
1158
+ : {}),
1150
1159
  },
1151
1160
  };
1152
1161
  }
@@ -126,7 +126,7 @@ function phraseOf(tool, args, outcome) {
126
126
  case "field.publish":
127
127
  return `发布模型 ${schemaCode ?? "?"} 的全部草稿字段(模型级一次发布)`;
128
128
  case "form.draft":
129
- return `保存表单 ${stringArg(args, "sheetCode") ?? "?"}(模型 ${schemaCode ?? "?"})的草稿布局`;
129
+ return `保存表单 ${stringArg(args, "sheetCode") ?? (schemaCode !== undefined ? String(schemaCode) : "?")}(模型 ${schemaCode ?? "?"})的草稿布局——sheetCode 省略时实际写入 = 模型 code 槽位`;
130
130
  case "form.publish":
131
131
  // r26 finding: sheetCode is optional (resolved from schemaCode for the
132
132
  // default form) — the summary showed "?" when the agent omitted it.
@@ -2,6 +2,24 @@ import { readFileSync } from "node:fs";
2
2
  import { readArtifact } from "./artifacts.js";
3
3
  import { artifactRoot } from "./command-transport.js";
4
4
  export function projectJsonPath(value, path) {
5
+ // 2026-11-01(r36 维护者实测):jq 切片 `a[0:6]`/`a[:n]`/`a[n:]` 此前
6
+ // pathMiss 且 hint 的 available 还把数组列成"可用键",误导下一步。数组
7
+ // 切片与 jq 语义一致(含头不含尾)。
8
+ const sliceSel = /\[(\d*):(\d*)\]/.exec(path);
9
+ if (sliceSel !== null) {
10
+ const [, rawFrom, rawTo] = sliceSel;
11
+ const before = path.slice(0, sliceSel.index);
12
+ const after = path.slice(sliceSel.index + sliceSel[0].length);
13
+ const base = projectJsonPath(value, before);
14
+ if (base.missed)
15
+ return base;
16
+ if (!Array.isArray(base.value))
17
+ return { missed: true, at: before || "$", available: `该层不是数组(是 ${base.value === null ? "null" : typeof base.value});切片 [from:to] 需要数组` };
18
+ const from = rawFrom === "" ? undefined : Number(rawFrom);
19
+ const to = rawTo === "" ? undefined : Number(rawTo);
20
+ const sliced = base.value.slice(from, to);
21
+ return after === "" ? { missed: false, value: sliced } : projectJsonPath(sliced, after);
22
+ }
5
23
  // 1.5 of issue 2026-10-23: an array-of-objects selection WITHOUT jq pipes.
6
24
  // `.rules[code=Create]` (or `[code="Create"]`) picks the FIRST element whose
7
25
  // key matches — exactly the shape callers reached for with
@@ -97,7 +115,7 @@ function argvValues(args, name) {
97
115
  return values;
98
116
  }
99
117
  /** Shared wording for every projection miss, so all three faces teach the same syntax. */
100
- const PATH_MISS_HINT = "路径未命中(这一层没有该段)。可用写法:点式 a.b、下标 a[0]、数组映射 items[].fields.x、按键选择 rules[code=Create](返回第一个匹配项)。available 是该层真实存在的键。";
118
+ const PATH_MISS_HINT = "路径未命中(这一层没有该段)。可用写法:点式 a.b、下标 a[0]、切片 a[0:6]、数组映射 items[].fields.x、按键选择 rules[code=Create](返回第一个匹配项)。available 是该层真实存在的键。";
101
119
  /**
102
120
  * `call --extract/--extract-keys`: resolve paths inside the result's own
103
121
  * artifact (or inside the result object when the tool returned no artifact).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,9 +52,9 @@
52
52
  "access": "public"
53
53
  },
54
54
  "dependencies": {
55
- "@stalfh233/omc-fake-platform": "0.5.7",
55
+ "@stalfh233/omc-fake-platform": "0.5.8",
56
56
  "commander": "14.0.3",
57
- "omc-core": "0.5.7",
57
+ "omc-core": "0.5.8",
58
58
  "proper-lockfile": "4.1.2",
59
59
  "write-file-atomic": "6.0.0",
60
60
  "zod": "^3.23.8",
@@ -68,7 +68,7 @@ only the section for the capability you need —
68
68
  - **能做哪些事**:字典与字典记录全生命周期(键值型/树型、启停);作为选择字段的选项来源;字典分类。
69
69
  - **工具**:`dictionary.list`、`dictionary.type`、`dictionary.names`、`dictionary.get`、`dictionary.records`、`dictionary.save`、`dictionary.status`、`dictionary.usage`、`dictionary.parent`、`dictionary.classes`、`dictionary.class.save`、`dictionary.class.delete`
70
70
  - **recipe**:`omc describe dictionary.save --example dictionary-key-value`
71
- - **边界 / 陷阱**:选择字段有三种选项来源——`custom` / `dictionary` / `businessModel`;关联单选/多选支持 `searchFormula` 与 `conditions` JSON 筛选。
71
+ - **边界 / 陷阱**:选择字段有三种选项来源——`custom` / `dictionary` / `businessModel`;关联单选/多选支持 `searchFormula` 与 `conditions` JSON 筛选。**字典没有删除**——平台只有停用/启用(`dictionary.status`,target= dictionary|record);不要找 dictionary.delete(分类有 `class.delete`,字典本体没有),清理测试残留用停用即可。
72
72
 
73
73
  ## 业务模型 `model`
74
74
 
@@ -12,7 +12,7 @@ be treated as a second source. Read this, then follow the topics it names.
12
12
 
13
13
  ## 最小路径(先读这段就能开始)
14
14
 
15
- 0. **调用与检索(跨平台)**:`omc call ... --args '<json>'` 的内联 JSON 在 Windows PowerShell 下因引号转义不可靠——**PS 环境优先 `--args-file <path>`**(把 JSON 写临时文件再引用;解析失败的报错里也有此提示)。读命令输出默认把中文转义为 `\uXXXX`(防乱码)——**对输出做内容检索前先加 `--utf8`(单次明文)或先解 JSON,"grep 搜不到中文"≠"没有配置"**(静默假阴性坑)。**stdout 恒为纯 JSON 信封,人读进度(「已执行 xxx」等)走 stderr**——脚本解析别 `2>&1` 合流(r33/r34 两轮探针同坑),合流后从首个 `{` 截断。
15
+ 0. **调用与检索(跨平台)**:`omc call ... --args '<json>'` 的内联 JSON 在 Windows PowerShell 下因引号转义不可靠——**PS 环境优先 `--args-file <path>`**(把 JSON 写临时文件再引用;解析失败的报错里也有此提示)。读命令输出默认把中文转义为 `\uXXXX`(防乱码)——**对输出做内容检索前先加 `--utf8`(单次明文)或先解 JSON,"grep 搜不到中文"≠"没有配置"**(静默假阴性坑)。stdout 恒为纯 JSON、人读进度在 stderr——脚本解析勿 `2>&1` 合流。
16
16
 
17
17
  0. **拼装器优先(高频写面一条命令)**:整模型建模(模型+N 字段+子表+发布+表单)`omc model-scaffold`;**存量模型加字段(建→发布→重建表单→列表加列)`omc field-add`**;字段计算 `omc compute`;条件→动作 `omc rule-when`;线性审批流 `omc approval-flow`;表单默认值 `omc form-default`;存量盘点 `data.list all:true`——命中这些形态先看 `--help`,比 recipe 手拼省 10+ 轮。
18
18
  0c. **清理一个模型用 `omc model-purge --code <x> [--purge-instances] --apply`**——按依赖倒序(实例 → 模型级联带走流程/视图/字段/表单),**不要手工逐层删**:删部门/字段留下缓存或孤儿,会让后续调用出现「假 40009 / 假孤儿 / 全局待办 50000」这类幽灵故障(r12–r14 三次实测)。若返回 `unremovableInstances`(业务对象已删的停滞实例,平台 `delete_instance` NPE),按返回的 `maintainerAction` 让维护者清 `biz_workflow_instance`;**不要改用 model.delete 硬来**。
@@ -190,15 +190,15 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
190
190
 
191
191
  - `workitem.list` 只回**当前登录人**的待办:审批人配别人 → 看不到待办,`data.submit` 被 `submit-workitem-not-mine` 拒(40005)。演示闭环要么节点参与者用 `{Originator}`,要么把登录人加进审批角色。
192
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`(`userStatus:DISABLE`)、部门树也可能仍见节点——验证口径用「列表/状态」,别声称物理清除。
193
+ - **`transferDepartment` 迁人不迁作用域**(r33 真机):换部门后 `role.userScope` 仍指旧部门——迁移后复核成员作用域。
194
+ - **`user.delete` 是软删**(r33 真机):回 `deleted:true` 但 `user.info` 仍 `exists:true`(DISABLE)——验证口径用列表/状态,别称物理清除。
195
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 关联删八表+收藏(维护者);见此报错先查收藏夹。
196
197
 
197
198
  ## workflow.update 的行级替换语义(r32 真机 2026-10-31)
198
199
 
199
200
  - **`btnConfigList` 局部行=整行替换**:只传 `{code, remark}` 会把该按钮的 `enabled` 静默置 `false`、`commentPermission` 置 `null`("驳回意见必填"丢失),同提交未提及的其他属性也被重置。改前先 `workflow.get_published`/`get_draft` 取整行,改目标属性后**整条回写**;`propertyPermissions` 同粒度。
200
- - 只改 `participant`/`btnConfigList` 的 activity 行**必须显式带 `type`**——省略 type 的豁免仅限带 `propertyPermissions` 的权限编辑行(拒绝信封已注明)。
201
- - **`SINGLE_PARTICIPANT` 配多角色只取第一个解析出的成员**(`MULTI_PARTICIPANT` 则全员会签)——演示/测试时"配置没生效"常是它;回读确认实际解析人。
201
+ - **`SINGLE_PARTICIPANT` 多角色只取第一个解析成员**(MULTI 则全员会签)——"配置没生效"常是它,回读确认解析人。
202
202
 
203
203
  ## 跨表回写的真实语义(1.1.25 源码 + 真机 2026-10-20)
204
204