@stalfh233/omc-cli 0.6.11 → 0.6.13
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/coverage/m1-coverage-manifest-v1.json +2 -0
- package/dist/approval-flow.js +84 -3
- package/dist/batch-call.js +32 -2
- package/dist/bench/call-face-eval.js +2 -2
- package/dist/call.js +35 -2
- package/dist/capability-overview.js +21 -7
- package/dist/cli-contract.js +34 -4
- package/dist/cli-output.js +6 -2
- package/dist/cli.js +303 -69
- package/dist/describe-cache.js +8 -4
- package/dist/environment-policy.js +20 -0
- package/dist/field-families.js +62 -4
- package/dist/form-support.js +293 -6
- package/dist/goal-routes.js +137 -22
- package/dist/identity.js +63 -11
- package/dist/lanes/app-contract.js +4 -4
- package/dist/lanes/app.js +19 -1
- package/dist/lanes/dictionary-contract.js +16 -5
- package/dist/lanes/dictionary-coverage.js +1 -0
- package/dist/lanes/dictionary.js +200 -8
- package/dist/lanes/environment-contract.js +18 -6
- package/dist/lanes/environment.js +129 -16
- package/dist/lanes/field-contract.js +2 -1
- package/dist/lanes/field.js +707 -59
- package/dist/lanes/form.js +52 -6
- package/dist/lanes/list-view-contract.js +2 -2
- package/dist/lanes/list-view.js +98 -2
- package/dist/lanes/meta.js +36 -10
- package/dist/lanes/model-contract.js +7 -6
- package/dist/lanes/model.js +154 -10
- package/dist/lanes/organization.js +67 -7
- package/dist/lanes/runtime-data-contract.js +4 -2
- package/dist/lanes/runtime-data.js +395 -41
- package/dist/lanes/workflow-contract.js +2 -2
- package/dist/lanes/workflow.js +381 -11
- package/dist/model-scaffold.js +11 -3
- package/dist/path-projection.js +192 -32
- package/dist/read-shapes.js +31 -11
- package/dist/recipes/field.js +16 -3
- package/dist/recipes/form.js +2 -1
- package/dist/recipes/runtime-data.js +58 -8
- package/dist/recipes/workflow.js +86 -1
- package/dist/refusal-guidance.js +18 -6
- package/dist/rollback-spec.js +41 -2
- package/dist/rollback.js +49 -11
- package/dist/script.js +72 -12
- package/dist/skills.js +91 -3
- package/dist/tools.js +113 -1
- package/dist/wire.js +13 -1
- package/dist/write-lock.js +92 -4
- package/dist/write.js +129 -16
- package/package.json +5 -3
- package/skills/omc-business-configuration.md +1 -1
- package/skills/omc-capabilities.md +5 -4
- package/skills/omc-playbook.md +41 -0
- package/skills/omc-refusals.md +58 -6
- package/skills/omc-tool-selection.md +2 -0
- package/skills/omc-write-hazards.md +3 -2
package/dist/lanes/model.js
CHANGED
|
@@ -19,6 +19,56 @@ import { MODEL_CREATE_BODY_DEFAULTS, MODEL_LANE_CONTRACT } from "./model-contrac
|
|
|
19
19
|
import { flattenFolders } from "./folder.js";
|
|
20
20
|
const MODEL_LANE = MODEL_LANE_CONTRACT.lane;
|
|
21
21
|
const MODEL_TOOLS = MODEL_LANE_CONTRACT.tools;
|
|
22
|
+
/**
|
|
23
|
+
* r29a F12:`model.list` 的 diet 投影。默认 `compactReadback` 把 `models` 压成
|
|
24
|
+
* 「code|name」字符串,小结果又原样回对象——同一工具元素类型不稳定。
|
|
25
|
+
* 这里恒回 `{code,schemaCode,name,legacyString}` 对象。
|
|
26
|
+
*/
|
|
27
|
+
function modelListCompact(result) {
|
|
28
|
+
const compact = {};
|
|
29
|
+
for (const [key, value] of Object.entries(result)) {
|
|
30
|
+
if (key === "models" && Array.isArray(value)) {
|
|
31
|
+
compact.models = value.map((entry) => {
|
|
32
|
+
if (entry !== null && typeof entry === "object" && !Array.isArray(entry)) {
|
|
33
|
+
const row = entry;
|
|
34
|
+
const code = String(row.code ?? row.schemaCode ?? "");
|
|
35
|
+
const name = row.name == null ? null : String(row.name);
|
|
36
|
+
return {
|
|
37
|
+
code,
|
|
38
|
+
schemaCode: String(row.schemaCode ?? code),
|
|
39
|
+
name,
|
|
40
|
+
legacyString: name ? `${code}|${name}` : code,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (typeof entry === "string") {
|
|
44
|
+
const sep = entry.indexOf("|");
|
|
45
|
+
const code = sep >= 0 ? entry.slice(0, sep) : entry;
|
|
46
|
+
const name = sep >= 0 ? entry.slice(sep + 1) : null;
|
|
47
|
+
return { code, schemaCode: code, name, legacyString: entry };
|
|
48
|
+
}
|
|
49
|
+
return entry;
|
|
50
|
+
});
|
|
51
|
+
compact.modelsCount = value.length;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)
|
|
55
|
+
&& Object.keys(value).length <= 4
|
|
56
|
+
&& Buffer.byteLength(JSON.stringify(value), "utf8") <= 200) {
|
|
57
|
+
compact[key] = value;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
61
|
+
compact[key] = value;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(value)) {
|
|
65
|
+
compact[`${key}Count`] = value.length;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
compact[key] = value;
|
|
69
|
+
}
|
|
70
|
+
return compact;
|
|
71
|
+
}
|
|
22
72
|
/**
|
|
23
73
|
* Pending work items for every workflow attached to this model.
|
|
24
74
|
*
|
|
@@ -195,11 +245,34 @@ export function registerModelLane(tools) {
|
|
|
195
245
|
applicationCode: z.string().optional().describe("应用 code;省略用配置/默认应用"),
|
|
196
246
|
filter: z.string().optional().describe("子串过滤:code 或 name 含该词才保留"),
|
|
197
247
|
search: z.string().optional().describe("filter 的别名(集合读统一关键字手感;org.role.list/service.list 也用 search)"),
|
|
248
|
+
// r29b F12:探针传 `query` 被静默忽略(仅 ignoredArgs 提示)。接受为 filter 别名,
|
|
249
|
+
// 与 search 同义;三者都给时以 filter > search > query 优先。
|
|
250
|
+
query: z.string().optional().describe("filter 的别名(与 search 同义;子串过滤 code/name)"),
|
|
198
251
|
capability: capabilityArg,
|
|
199
252
|
},
|
|
200
253
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
201
254
|
const scope = await resolveApplicationScope(session, args.applicationCode);
|
|
202
255
|
const applicationCode = scope.code;
|
|
256
|
+
// r29f F6:显式/配置的 applicationCode 若不在 app.list 里,旧路径会把拼写错误
|
|
257
|
+
// 伪装成「该应用没有模型」。先核对租户应用表,未命中即拒(与 discover 的
|
|
258
|
+
// applicationResolution.status 同构)。
|
|
259
|
+
const availableApps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data)
|
|
260
|
+
.map((row) => String(row.appCode ?? row.code ?? ""))
|
|
261
|
+
.filter((code) => code !== "");
|
|
262
|
+
if (availableApps.length > 0 && !availableApps.includes(applicationCode)) {
|
|
263
|
+
throw new ToolRefusalError({
|
|
264
|
+
status: "refused",
|
|
265
|
+
blocker: "app-not-found",
|
|
266
|
+
applicationCode,
|
|
267
|
+
applicationResolution: {
|
|
268
|
+
status: "unresolved",
|
|
269
|
+
source: scope.source,
|
|
270
|
+
requested: scope.requested ?? applicationCode,
|
|
271
|
+
available: availableApps.slice(0, 40),
|
|
272
|
+
},
|
|
273
|
+
message: `应用「${applicationCode}」不在租户 app.list 里(不是「该应用没有模型」)。可用:${availableApps.slice(0, 12).join("、")}${availableApps.length > 12 ? " 等" : ""};先 app.list 取正确 applicationCode。`,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
203
276
|
const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
204
277
|
const all = tree
|
|
205
278
|
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code)
|
|
@@ -207,7 +280,7 @@ export function registerModelLane(tools) {
|
|
|
207
280
|
// the tree row's key. Echo both so `--jq 'models[].schemaCode'` works
|
|
208
281
|
// (r15 P6: the alias was missing and the path missed).
|
|
209
282
|
.map((row) => ({ code: row.code, schemaCode: row.code, name: row.name ?? row.text ?? null }));
|
|
210
|
-
const needle = (args.filter ?? args.search)?.trim();
|
|
283
|
+
const needle = (args.filter ?? args.search ?? args.query)?.trim();
|
|
211
284
|
const models = needle ? all.filter((model) => String(model.code).includes(needle) || String(model.name ?? "").includes(needle)) : all;
|
|
212
285
|
// `filterApplied` disambiguates an empty result: it distinguishes "the
|
|
213
286
|
// filter ran and matched nothing" from "the filter was not applied"
|
|
@@ -217,7 +290,7 @@ export function registerModelLane(tools) {
|
|
|
217
290
|
// 1-of-N, so `count: 0` must never read as tenant-wide absence.
|
|
218
291
|
return {
|
|
219
292
|
applicationCode,
|
|
220
|
-
applicationResolution: applicationResolutionOf(scope),
|
|
293
|
+
applicationResolution: { status: "resolved", ...applicationResolutionOf(scope) },
|
|
221
294
|
count: models.length,
|
|
222
295
|
totalBeforeFilter: all.length,
|
|
223
296
|
filterApplied: needle !== undefined && needle !== "",
|
|
@@ -226,7 +299,13 @@ export function registerModelLane(tools) {
|
|
|
226
299
|
models,
|
|
227
300
|
...(models.length === 0 ? { hint: applicationScopeHint(scope, "模型") } : {}),
|
|
228
301
|
};
|
|
229
|
-
}, {
|
|
302
|
+
}, {
|
|
303
|
+
target: `model.list:${args.applicationCode ?? ""}:${args.filter ?? args.search ?? args.query ?? ""}`,
|
|
304
|
+
// r29a F12:默认 compactReadback 把 models 压成「code|name」字符串,小应用
|
|
305
|
+
// 不过 diet 门仍回对象 → 同一工具元素类型不稳定。自定义 compact 恒回对象,
|
|
306
|
+
// 并附 legacyString 兼容旧解析。
|
|
307
|
+
compact: (result) => modelListCompact(result),
|
|
308
|
+
}),
|
|
230
309
|
}));
|
|
231
310
|
const siblings = tools;
|
|
232
311
|
tools.push(defineTool({
|
|
@@ -433,6 +512,8 @@ export function registerModelLane(tools) {
|
|
|
433
512
|
.describe("模型所属应用 code(应用包,不是目录)。省略用配置默认应用;folder.* 的 appCode 别名本工具不认,请用 applicationCode。"),
|
|
434
513
|
modelType: z.enum(["LIST", "TREE"]).default("LIST"),
|
|
435
514
|
},
|
|
515
|
+
// r29a F6:dry-run 必须做与实跑同样的 duplicate 探测,否则会假绿灯。
|
|
516
|
+
dryRunPayload: modelCreateDryRun,
|
|
436
517
|
execute: async (session, args) => {
|
|
437
518
|
const applicationCode = await resolveApplicationCode(session, args.applicationCode);
|
|
438
519
|
const existing = await resolveModelTreeNode(session, args.code, args.applicationCode);
|
|
@@ -540,6 +621,48 @@ export function registerModelLane(tools) {
|
|
|
540
621
|
},
|
|
541
622
|
rollback: modelRollbackSpec("model.create"),
|
|
542
623
|
});
|
|
624
|
+
/**
|
|
625
|
+
* r29a F6:model.create 的 session-aware dry-run。与实跑同一套 duplicate /
|
|
626
|
+
* orphan 探测(只读),命中时 wouldChange:false + blocker,避免假绿灯。
|
|
627
|
+
*/
|
|
628
|
+
async function modelCreateDryRun(session, args) {
|
|
629
|
+
const existing = await resolveModelTreeNode(session, args.code, args.applicationCode);
|
|
630
|
+
if (existing.node) {
|
|
631
|
+
return {
|
|
632
|
+
wouldChange: false,
|
|
633
|
+
blocker: "model-create-duplicate",
|
|
634
|
+
schemaCode: args.code,
|
|
635
|
+
applicationCode: existing.applicationCode,
|
|
636
|
+
message: `模型 ${args.code} 已存在于应用 ${existing.applicationCode} 的应用树中;create 拒绝重复 POST,不能把既有模型证明为本次创建结果`,
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
const record = await readModelRecord(session, args.code);
|
|
641
|
+
if (record !== null && typeof record === "object") {
|
|
642
|
+
return {
|
|
643
|
+
wouldChange: false,
|
|
644
|
+
blocker: "model-create-duplicate-orphan",
|
|
645
|
+
schemaCode: args.code,
|
|
646
|
+
applicationCode: args.applicationCode ?? null,
|
|
647
|
+
nextCommand: `omc call model.delete --env <env> --args '{"schemaCode":"${args.code}"}'`,
|
|
648
|
+
message: `模型 ${args.code} 在册(bizmodels/get_all 可读)但不在应用树中。**先跑一次 model.delete**——它现在会把两种成因分开:` +
|
|
649
|
+
`① 读缓存滞后(记录其实已删)→ 回 already-absent/stale-record-cache,随后可直接 create;` +
|
|
650
|
+
`② 真孤儿(删除级联中断留下的残骸)→ 回 model-orphaned-record 并给平台侧出路(回收站物理删除 / 换 code),因为 8.6 平台无法删除没有 AppFunction 行的业务模型。` +
|
|
651
|
+
`不要重复 create(会再造一条孤儿),也不要只信这一条拒绝信息。`,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
// Record-face read failure must not be read as absence; dry-run proceeds.
|
|
657
|
+
}
|
|
658
|
+
const applicationCode = await resolveApplicationCode(session, args.applicationCode).catch(() => args.applicationCode ?? null);
|
|
659
|
+
return {
|
|
660
|
+
wouldChange: true,
|
|
661
|
+
schemaCode: args.code,
|
|
662
|
+
applicationCode,
|
|
663
|
+
summary: `即将创建模型 ${args.name ?? args.code}(${args.code})${args.modelType === "TREE" ? ",树型" : ",列表型"}${applicationCode ? `(应用 ${applicationCode})` : ""}`,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
543
666
|
/** model.delete writeTool extra arg: a session-aware dry-run that scans inbound references (no write). */
|
|
544
667
|
async function modelDeleteDryRun(session, args) {
|
|
545
668
|
const record = await (async () => {
|
|
@@ -620,8 +743,9 @@ export function registerModelLane(tools) {
|
|
|
620
743
|
const applicationCode = resolved.applicationCode;
|
|
621
744
|
const treeRows = async () => applicationCode ? flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data)) : [];
|
|
622
745
|
if (!node) {
|
|
746
|
+
// r30a F22: mark noop so write.ts skips the rollback journal line.
|
|
623
747
|
if (record === null)
|
|
624
|
-
return { performed: { skipped: "already-absent" }, readback: { absent: true } };
|
|
748
|
+
return { performed: { skipped: "already-absent", noop: true }, readback: { absent: true } };
|
|
625
749
|
// r12 (2026-10-24): "record readable but no tree node" has TWO causes and
|
|
626
750
|
// the caller could not tell them apart — create said "go fix parentId",
|
|
627
751
|
// model.move said "there is no node", model.delete said "no node means
|
|
@@ -642,7 +766,8 @@ export function registerModelLane(tools) {
|
|
|
642
766
|
}
|
|
643
767
|
if (stillPresent === null) {
|
|
644
768
|
return {
|
|
645
|
-
|
|
769
|
+
// r30a F22: noop — do not journal a phantom delete.
|
|
770
|
+
performed: { skipped: "stale-record-cache", noop: true },
|
|
646
771
|
readback: {
|
|
647
772
|
absent: true,
|
|
648
773
|
note: "业务模型记录读取面(bizmodels/get_all)先前仍返回该 code,复查后已消失——这是读缓存滞后的残留,模型实际已删除。若刚跑过 model.create 又被拒「已在册」,现在可以重试。",
|
|
@@ -950,6 +1075,10 @@ export function registerModelLane(tools) {
|
|
|
950
1075
|
description: MODEL_TOOLS["model.update"].description,
|
|
951
1076
|
inputSchema: {
|
|
952
1077
|
schemaCode: z.string().min(1),
|
|
1078
|
+
// Accept+refuse: callers habitually send `code` thinking they can rename
|
|
1079
|
+
// the model identity. Keep it in schema so unknown-key suggestion does
|
|
1080
|
+
// NOT redirect them to schemaCode (schemaCode == code).
|
|
1081
|
+
code: z.string().min(1).optional().describe("恒拒(model-code-immutable):模型 code 即 schemaCode,创建后不可改;要换 code 请新建模型并迁移数据"),
|
|
953
1082
|
name: z.string().min(1).optional().describe("新显示名(bizmodels/update 补丁语义,读回 get_all + 应用树节点)"),
|
|
954
1083
|
modelType: z.enum(["LIST", "TREE"]).optional().describe("恒拒(model-type-immutable):模型类型创建时固化,bizmodels/update 只更新菜单节点显示"),
|
|
955
1084
|
},
|
|
@@ -1003,8 +1132,16 @@ export function registerModelLane(tools) {
|
|
|
1003
1132
|
};
|
|
1004
1133
|
},
|
|
1005
1134
|
preflight: (args) => {
|
|
1006
|
-
// `omc call` bypasses zod, so an extra modelType key still reaches here.
|
|
1135
|
+
// `omc call` bypasses zod, so an extra modelType/code key still reaches here.
|
|
1007
1136
|
const raw = args;
|
|
1137
|
+
// schemaCode == code:传 code 想改模型标识时,绝不能建议「改传 schemaCode」。
|
|
1138
|
+
if (raw.code !== undefined)
|
|
1139
|
+
throw new ToolRefusalError({
|
|
1140
|
+
status: "refused",
|
|
1141
|
+
blocker: "model-code-immutable",
|
|
1142
|
+
schemaCode: raw.schemaCode,
|
|
1143
|
+
message: "模型 code 是业务数据的标识(规则/表单/视图/数据都挂在 schemaCode 上),不可原地修改;要换 code 请新建模型并迁移数据",
|
|
1144
|
+
});
|
|
1008
1145
|
// Probe evidence (yunshu ledger + ticket 09): bizmodels/update only
|
|
1009
1146
|
// updates the app-menu node display — the underlying schema (type, tree
|
|
1010
1147
|
// system fields) is FIXED at create. A modelType patch here would look
|
|
@@ -1024,17 +1161,21 @@ export function registerModelLane(tools) {
|
|
|
1024
1161
|
name: "model.move",
|
|
1025
1162
|
lane: MODEL_LANE,
|
|
1026
1163
|
scope: "move a model's app-tree node to another folder (bizmodels/update parentId)",
|
|
1027
|
-
description: "
|
|
1164
|
+
description: "把模型在**同一应用内**移动到指定目录(分组):`POST bizmodels/update` 的 `parentId`(与 model.update 同一端点,AppFunctionController 补丁语义:bizSchemaCode/id/name/parentId 全量必填,工具从应用树解析)。" +
|
|
1165
|
+
"`folderId` 必须是目录节点 id(`folder.tree` 的 id),**不能直接给目录名**——重名目录会定位错。" +
|
|
1166
|
+
"移动后按应用树回读 `parentId/path` 确认;模型的 schema、字段、表单、视图都不动,只换菜单节点归属。" +
|
|
1167
|
+
"**模型不能跨应用移动**——目标目录必须与模型同应用;要在目标应用得到同名模型,请 `model.create --applicationCode <目标>`(换 code,或先 `model.delete` 源再同 code 重建)。" +
|
|
1168
|
+
"目标目录不存在时拒绝 `folder-not-found` 并列出目录树;目录属于其它应用时拒绝 `folder-other-application`。",
|
|
1028
1169
|
inputSchema: {
|
|
1029
1170
|
schemaCode: z.string().min(1),
|
|
1030
1171
|
folderId: z
|
|
1031
1172
|
.string()
|
|
1032
1173
|
.min(1)
|
|
1033
|
-
.describe("目标目录 id(必须是 folder.tree 里 type=Folder 的 id;顶层目录也是真实 id
|
|
1174
|
+
.describe("目标目录 id(必须是 folder.tree 里 type=Folder 的 id;顶层目录也是真实 id,不要传空或目录名;且必须与模型同应用)"),
|
|
1034
1175
|
applicationCode: z
|
|
1035
1176
|
.string()
|
|
1036
1177
|
.optional()
|
|
1037
|
-
.describe("模型所在应用 code
|
|
1178
|
+
.describe("模型所在应用 code(应用包)。用于定位源模型节点;**不支持跨应用移动**——目标目录必须同应用。folder.* 的 appCode 别名本工具不认。"),
|
|
1038
1179
|
},
|
|
1039
1180
|
execute: async (session, args) => {
|
|
1040
1181
|
const resolved = await resolveModelTreeNode(session, args.schemaCode, args.applicationCode);
|
|
@@ -1071,8 +1212,11 @@ export function registerModelLane(tools) {
|
|
|
1071
1212
|
applicationCode: appCode,
|
|
1072
1213
|
...(otherApp ? { folderApplicationCode: otherApp } : {}),
|
|
1073
1214
|
available: tree.filter((row) => row.type === "Folder").map((row) => ({ id: row.id, name: row.name })).slice(0, 40),
|
|
1215
|
+
// r29a F5:旧建议「先在源应用建同名目录再移动」永远到不了目标应用。
|
|
1074
1216
|
message: otherApp
|
|
1075
|
-
? `目录 ${args.folderId} 属于应用 ${otherApp},而模型 ${args.schemaCode} 在应用 ${appCode}
|
|
1217
|
+
? `目录 ${args.folderId} 属于应用 ${otherApp},而模型 ${args.schemaCode} 在应用 ${appCode}——模型不能跨应用移动。` +
|
|
1218
|
+
`要在 ${otherApp} 得到同名模型,请 \`model.create --applicationCode ${otherApp}\`(换 code,或先 \`model.delete\` 源再同 code 重建);` +
|
|
1219
|
+
`同应用内换目录请传 ${appCode} 下 folder.tree 的目录 id。`
|
|
1076
1220
|
: `应用 ${appCode} 没有目录 ${args.folderId}——folderId 必须用 folder.tree 的目录节点 id(不是目录名)`,
|
|
1077
1221
|
});
|
|
1078
1222
|
}
|
|
@@ -8,6 +8,43 @@ import { ENDPOINTS } from "../endpoints.js";
|
|
|
8
8
|
import { environmentArg, capabilityArg } from "../args.js";
|
|
9
9
|
import { LANES } from "./lane-ids.js";
|
|
10
10
|
import { collectOrgDepartments } from "./org-tree.js";
|
|
11
|
+
import { resolvePermissionActor } from "../identity.js";
|
|
12
|
+
/** Normalize get_ouscope rows; empty / null / non-array → [] (r30d F3). */
|
|
13
|
+
function decodeOuScopeRows(data) {
|
|
14
|
+
if (data === null || data === undefined)
|
|
15
|
+
return [];
|
|
16
|
+
if (Array.isArray(data) && data.length === 0)
|
|
17
|
+
return [];
|
|
18
|
+
try {
|
|
19
|
+
return wireRows(data);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// Non-member / no-scope answers null or a non-array — that is a legitimate
|
|
23
|
+
// empty set, not a wire bug (r30d F3). Only rethrow when the payload looks
|
|
24
|
+
// like a real envelope with non-row content.
|
|
25
|
+
if (error instanceof WireError && error.kind === "malformed-envelope")
|
|
26
|
+
return [];
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Structured scope face (r30d F4): never echo the caller's includeChildren
|
|
32
|
+
* guess. Platform get_ouscope does not return an includeChildren bit, so we
|
|
33
|
+
* surface that honestly instead of inventing one.
|
|
34
|
+
*/
|
|
35
|
+
function projectOuScopeRow(row) {
|
|
36
|
+
const deptId = row.id === undefined || row.id === null ? null : String(row.id);
|
|
37
|
+
const deptPath = row.deptPath === undefined || row.deptPath === null ? null : String(row.deptPath);
|
|
38
|
+
return {
|
|
39
|
+
id: deptId,
|
|
40
|
+
deptId,
|
|
41
|
+
name: row.name === undefined || row.name === null ? null : String(row.name),
|
|
42
|
+
type: row.type === undefined || row.type === null ? null : String(row.type),
|
|
43
|
+
deptPath,
|
|
44
|
+
includeChildren: "unknown(平台不返回)",
|
|
45
|
+
source: "configured",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
11
48
|
/**
|
|
12
49
|
* Organization WRITE faces (8.6.41 customer transcript, 2026-10-21).
|
|
13
50
|
*
|
|
@@ -384,7 +421,7 @@ export function registerOrganizationLane(tools) {
|
|
|
384
421
|
kind: "read",
|
|
385
422
|
lane: LANE,
|
|
386
423
|
scope: SCOPES.userScope,
|
|
387
|
-
description: "读成员的管理范围(`GET organization/role/get_ouscope
|
|
424
|
+
description: "读成员的管理范围(`GET organization/role/get_ouscope`)。**不是枚举档位**:scopes=`{deptId,name,type:\"1\",deptPath,includeChildren,source}`。`includeChildren` 平台不返回(`unknown`);`source:\"configured\"`=配置值、不证明生效。非成员/无范围→`{count:0,scopes:[]}`(合法空集,非 wire 错误)。**roleId/roleCode 二选一**。",
|
|
388
425
|
inputSchema: {
|
|
389
426
|
environment: environmentArg,
|
|
390
427
|
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
@@ -398,8 +435,19 @@ export function registerOrganizationLane(tools) {
|
|
|
398
435
|
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
399
436
|
query: { ...orgQuery(session), roleId, userId: String(args.userId) },
|
|
400
437
|
})).data;
|
|
401
|
-
const scopes =
|
|
402
|
-
|
|
438
|
+
const scopes = decodeOuScopeRows(data).map(projectOuScopeRow);
|
|
439
|
+
const actor = await resolvePermissionActor(session);
|
|
440
|
+
return {
|
|
441
|
+
roleId,
|
|
442
|
+
userId: String(args.userId),
|
|
443
|
+
count: scopes.length,
|
|
444
|
+
scopes,
|
|
445
|
+
readback: "config",
|
|
446
|
+
note: scopes.length === 0
|
|
447
|
+
? "该用户不在本角色或无管理范围(合法空集;不是读失败)"
|
|
448
|
+
: "本工具只回配置值(source:configured),不证明已参与 data.list/listview 过滤;includeChildren 平台不返回。",
|
|
449
|
+
actor,
|
|
450
|
+
};
|
|
403
451
|
}),
|
|
404
452
|
}));
|
|
405
453
|
writeTool({
|
|
@@ -1216,7 +1264,7 @@ export function registerOrganizationLane(tools) {
|
|
|
1216
1264
|
},
|
|
1217
1265
|
execute: async (session, args) => {
|
|
1218
1266
|
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.setUserScope");
|
|
1219
|
-
const before =
|
|
1267
|
+
const before = decodeOuScopeRows((await wireCall(session, {
|
|
1220
1268
|
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
1221
1269
|
query: { ...orgQuery(session), roleId, userId: args.userId },
|
|
1222
1270
|
})).data);
|
|
@@ -1236,13 +1284,25 @@ export function registerOrganizationLane(tools) {
|
|
|
1236
1284
|
query: orgQuery(session),
|
|
1237
1285
|
body: { roleId, userId: args.userId, unitType: 3, sortKey: 999, ouScopeList: merged },
|
|
1238
1286
|
});
|
|
1239
|
-
const
|
|
1287
|
+
const afterRows = decodeOuScopeRows((await wireCall(session, {
|
|
1240
1288
|
path: ENDPOINTS.organizationRoleGetOuScope,
|
|
1241
1289
|
query: { ...orgQuery(session), roleId, userId: args.userId },
|
|
1242
|
-
})).data)
|
|
1290
|
+
})).data);
|
|
1291
|
+
const after = afterRows.map(projectOuScopeRow);
|
|
1292
|
+
const actor = await resolvePermissionActor(session);
|
|
1243
1293
|
return {
|
|
1244
1294
|
performed: { errcode: 0 },
|
|
1245
|
-
readback: {
|
|
1295
|
+
readback: {
|
|
1296
|
+
roleId,
|
|
1297
|
+
userId: args.userId,
|
|
1298
|
+
mode: args.mode ?? "merge",
|
|
1299
|
+
before: before.length,
|
|
1300
|
+
after: after.length,
|
|
1301
|
+
scopes: after,
|
|
1302
|
+
...(removed.length > 0 ? { removedScopeIds: removed } : {}),
|
|
1303
|
+
readback: "config",
|
|
1304
|
+
actor,
|
|
1305
|
+
},
|
|
1246
1306
|
};
|
|
1247
1307
|
},
|
|
1248
1308
|
});
|
|
@@ -15,6 +15,7 @@ export function dataUpdateDescription() {
|
|
|
15
15
|
`${wire.updateMergeRule}。`,
|
|
16
16
|
`缺席保护:${wire.updateAbsentGuard}。`,
|
|
17
17
|
`子表行:${wire.childRowStatusNote}`,
|
|
18
|
+
"CHILD_TABLE 字段传对象数组:既有行带 id、追加行 rowStatus:\"Added\"、删除行 {id,rowStatus:\"Deleted\"}(只给 id);**少传行不会删除**(见 --example child-rows-upsert)。",
|
|
18
19
|
`计算字段:${wire.formulaFieldNote}`,
|
|
19
20
|
"成功后以 form/load 权威读回变更字段;审批门/生产拒绝/租约与其他 M2 写一致(governedWrite)。",
|
|
20
21
|
]);
|
|
@@ -25,6 +26,7 @@ export function dataSaveDescription() {
|
|
|
25
26
|
`带可选 workflowCode 时按门户暂存形状创建流程草稿:${wire.workflowDraftSave}。`,
|
|
26
27
|
"formData 禁止携带 id(带 id 的就地发起在 8.6.26 无 REST 面,会被拒绝;既有无实例草稿请改用两段式或 data.submit 的 workItemId 审批链)。",
|
|
27
28
|
"关联值形状(live 2026-09-24):**单选关联(propertyType 9)传 id 字符串或 { id };传数组会 50000**;多选关联(11)才用数组。**人员/组织(50/51/60/61)传选择对象数组 `[{\"id\":\"<id>\",\"type\":3,\"name\":\"<名>\"}]`——`type` 必填**(BizField.parseFromClient 把 `type` 强转 Integer 并 intValue(),缺 `type` 稳定 50000);`type` 取 UnitType 索引:用户=3、部门=1。字段 `mappings` 由表单运行时在用户选择时套用,**服务端 data.save 不会自动回填映射目标字段**——API 侧调用需自行写入被映射字段,否则读回为空(不要误判为映射配置失败)。",
|
|
29
|
+
"CHILD_TABLE 字段传对象数组(行键见 data.update --example child-rows-upsert);新建时可省略行 id(工具 stamp Added)。",
|
|
28
30
|
`不带 workflowCode 走纯表单保存(新行 DRAFT);formType 语义与 replayToken 门见 data.submit 契约(${wire.replayTokenErrcode})。`,
|
|
29
31
|
"根因提示(环境相关,ticket 78 观测):**某些环境**里模型所在应用未发布时运行时写稳定 50000(同调用在已发布应用成功),工具会反推并拒 app-not-published。注意该根因并非普遍规律——2026-09-19 实测另有一类环境未发布应用也能写运行时数据(详见 docs/issues/omc-cli-findings-2026-09-19.md B17)。遇 50000 时先用只读数据核对,不要仅凭“应用未发布”就放弃运行时验证。",
|
|
30
32
|
`子表行:${wire.childRowStatusNote}`,
|
|
@@ -139,8 +141,8 @@ function desc(lead) {
|
|
|
139
141
|
export function assertRuntimeDataContract() {
|
|
140
142
|
assertLaneContract(RUNTIME_DATA_LANE_CONTRACT);
|
|
141
143
|
assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.list", ["queryCondition", "propertyType", "rawCondition", "projectionHint", "listview.configure", "data.load", "items[].fields"]);
|
|
142
|
-
assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.save", ["bizObjectId", "sheetCode", "bizsheet/get", "workflowCode", "sequenceStatus", "workItem", "propertyType 9", "50000", "mappings", "rowStatus", "DRAFT", "submit:true"]);
|
|
143
|
-
assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.update", ["runtime/form/save", "bizObjectId", "COMPLETED", "304001", "form/load", "rowStatus"]);
|
|
144
|
+
assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.save", ["bizObjectId", "sheetCode", "bizsheet/get", "workflowCode", "sequenceStatus", "workItem", "propertyType 9", "50000", "mappings", "rowStatus", "DRAFT", "submit:true", "CHILD_TABLE"]);
|
|
145
|
+
assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.update", ["runtime/form/save", "bizObjectId", "COMPLETED", "304001", "form/load", "rowStatus", "child-rows-upsert"]);
|
|
144
146
|
assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.delete", ["absence", "sheetCode"]);
|
|
145
147
|
assertDescriptionCarries(RUNTIME_DATA_LANE_CONTRACT, "data.submit", ["getReplayToken", "replayToken", "bizObjectId", "objectId", "DRAFT", "COMPLETED", "10009", "100005", "NPE", "两段式", "finishStart=FALSE"]);
|
|
146
148
|
if (!RUNTIME_DATA_WIRE.workflowDraftSave.includes("finishStart=FALSE"))
|