@stalfh233/omc-cli 0.6.20 → 0.6.22
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/cli.js +72 -11
- package/dist/command-transport.js +9 -0
- package/dist/graph.js +104 -29
- package/dist/human-summary.js +1 -1
- package/dist/lanes/field.js +12 -3
- package/dist/lanes/list-view-contract.js +1 -1
- package/dist/lanes/list-view.js +9 -1
- package/dist/lanes/online-js.js +37 -7
- package/dist/lanes/runtime-data-contract.js +1 -1
- package/dist/lanes/runtime-data.js +34 -17
- package/dist/lanes/workflow.js +10 -0
- package/dist/mcp-graph.js +46 -12
- package/dist/model-purge.js +1 -0
- package/dist/residue.js +21 -4
- package/dist/rule-when.js +72 -4
- package/package.json +3 -3
- package/skills/omc-capabilities.md +1 -0
- package/skills/omc-config-draft-review.md +2 -3
- package/skills/omc-glossary.md +2 -0
- package/skills/omc-playbook.md +5 -3
- package/skills/omc-start-here.md +3 -6
- package/skills/omc-write-hazards.md +9 -2
package/dist/lanes/online-js.js
CHANGED
|
@@ -5,7 +5,7 @@ import { assertHtmlRender, executeFormDraftWrite, executeFormPublishWrite, readF
|
|
|
5
5
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
6
6
|
import { writeTool } from "../write.js";
|
|
7
7
|
import { governedRead } from "../read.js";
|
|
8
|
-
import { defineTool } from "../tool-types.js";
|
|
8
|
+
import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
9
9
|
import { wireCall } from "../wire.js";
|
|
10
10
|
import { composeCustomScript, lintOnlineJsSource } from "../online-js-source.js";
|
|
11
11
|
import { composeLayoutElements } from "../online-js-layout.js";
|
|
@@ -51,7 +51,9 @@ function readCustomScriptSource(rawHtml) {
|
|
|
51
51
|
// real script when the find falls back to the tag name.
|
|
52
52
|
const element = elements.find((candidate) => candidate.id === "customScript");
|
|
53
53
|
const script = typeof element?.innerHTML === "string" && element.innerHTML.trim() !== "" ? element.innerHTML : null;
|
|
54
|
-
return
|
|
54
|
+
return script !== null
|
|
55
|
+
? { present: true, script, sha256: createHash("sha256").update(script, "utf8").digest("hex"), elementCount: elements.length }
|
|
56
|
+
: { present: false, script: null, sha256: "", elementCount: elements.length, notCollected: true, notCollectedReason: "customScript 不存在,无法计算脚本哈希" };
|
|
55
57
|
}
|
|
56
58
|
export function registerOnlineJsLane(tools) {
|
|
57
59
|
tools.push(defineTool({
|
|
@@ -79,11 +81,12 @@ export function registerOnlineJsLane(tools) {
|
|
|
79
81
|
sheetCode,
|
|
80
82
|
present: draft.present || published.present,
|
|
81
83
|
sheetPublished: sheet.published === true,
|
|
82
|
-
draft: { ...draft, ...(draft.script === null ? { note: "草稿无 customScript 脚本(尚未 compose 或被清空)" } : {}) },
|
|
83
|
-
published: { ...published, ...(published.script === null ? { note: "发布面无 customScript 脚本" } : {}) },
|
|
84
|
-
...(draft.sha256 !==
|
|
84
|
+
draft: { ...draft, scriptSha256: draft.sha256 || undefined, ...(draft.script === null ? { note: "草稿无 customScript 脚本(尚未 compose 或被清空)" } : {}) },
|
|
85
|
+
published: { ...published, scriptSha256: published.sha256 || undefined, ...(published.script === null ? { note: "发布面无 customScript 脚本" } : {}) },
|
|
86
|
+
...(draft.sha256 !== "" && published.sha256 !== ""
|
|
85
87
|
? { draftPublishedInSync: draft.sha256 === published.sha256 }
|
|
86
88
|
: {}),
|
|
89
|
+
scriptSha256: published.sha256 || draft.sha256 || undefined,
|
|
87
90
|
hint: "脚本是 innerHTML 原文(含平台 (function(form){…}) 生命周期包装——那就是平台存储形态);改脚本用 onlinejs.compose(默认 INCREMENTAL 并入,replace:true 整段替换);同文件修订直接 replace:true 传完整脚本",
|
|
88
91
|
};
|
|
89
92
|
}, {
|
|
@@ -109,6 +112,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
109
112
|
sheetCode: z.string().min(1).optional().describe("表单槽位码;省略时默认 = schemaCode(与 form.draft/form.publish 同口径)"),
|
|
110
113
|
source: z.string().optional().describe("在线 JS 代码。默认 INCREMENTAL:并入平台 (function(form){…}) 模板的既有脚本,模板与既有注册全部保留;配 replace:true 才是整段替换。脚本不得使用 export/import;事件名必须是平台事件(onLoad/onRendered/onValidate/onPreAction/onCustomAction/onActionDone/onPrintFormDataHandle/onPrintWorkflowLogDataHandle)。与 remove:true 二选一;source 为空字符串也表示移除 customScript"),
|
|
111
114
|
replace: z.boolean().optional().describe("true = 以 source 整体替换 customScript(含完整 (function(form){…}) 脚本)。默认 false = 增量并入既有脚本,保留平台模板与既有逻辑"),
|
|
115
|
+
confirmReplaceSha256: z.string().regex(/^[a-f0-9]{64}$/i).optional().describe("replace:true 覆盖既有脚本时必须等于当前脚本 SHA-256,防止静默覆盖线上改动"),
|
|
112
116
|
remove: z.boolean().optional().describe("true = 显式移除 customScript,使 onlinejs.get 返回 present:false;会写入并按 publish 设置发布。与 source 二选一"),
|
|
113
117
|
publish: z.boolean().optional().describe("默认 true:写草稿并推发布面;false = 只写草稿不推发布面"),
|
|
114
118
|
summary: z.boolean().optional().describe("true = 只回同步状态(sha/字节数/是否同步/渲染兼容),不回任何 HTML/JS 原文。确认「改动是否生效」用这个;要看原文用 onlinejs.get"),
|
|
@@ -122,10 +126,26 @@ export function registerOnlineJsLane(tools) {
|
|
|
122
126
|
throw new Error("remove:true 不能与非空 source 同时传入");
|
|
123
127
|
// r28e F9:和同 lane 其它工具一致——省略 sheetCode 即模型码槽位。
|
|
124
128
|
const targetSheetCode = args.sheetCode ?? args.schemaCode;
|
|
129
|
+
// Keep the human summary truthful when the caller relies on the documented
|
|
130
|
+
// default (sheetCode omitted): it is the model's default form slot.
|
|
125
131
|
// 1. Read the existing draftHtmlJson.
|
|
126
132
|
const current = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: targetSheetCode } });
|
|
127
133
|
const existingSheet = (current.data ?? null);
|
|
128
134
|
const existingRaw = typeof existingSheet?.draftHtmlJson === "string" ? existingSheet.draftHtmlJson : undefined;
|
|
135
|
+
const existingPublishedRaw = typeof existingSheet?.publishedHtmlJson === "string" ? existingSheet.publishedHtmlJson : undefined;
|
|
136
|
+
const existingScriptState = readCustomScriptSource(existingRaw ?? existingPublishedRaw);
|
|
137
|
+
if (args.replace === true && existingScriptState.present && args.confirmReplaceSha256 !== existingScriptState.sha256) {
|
|
138
|
+
throw new ToolRefusalError({
|
|
139
|
+
status: "refused",
|
|
140
|
+
blocker: "onlinejs-replace-confirmation-required",
|
|
141
|
+
schemaCode: args.schemaCode,
|
|
142
|
+
sheetCode: targetSheetCode,
|
|
143
|
+
previousSha256: existingScriptState.sha256,
|
|
144
|
+
...(args.confirmReplaceSha256 !== undefined ? { providedSha256: args.confirmReplaceSha256 } : {}),
|
|
145
|
+
message: "replace:true 将覆盖已有线上脚本;必须传 confirmReplaceSha256 且与当前脚本 SHA-256 完全一致。未写入任何内容。",
|
|
146
|
+
nextCommand: `omc call onlinejs.get --args '${JSON.stringify({ schemaCode: args.schemaCode, sheetCode: targetSheetCode })}'`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
129
149
|
const existingCompat = validateHtmlRender(existingRaw);
|
|
130
150
|
// A persisted legacy face may contain an object or malformed elements.
|
|
131
151
|
// Do not pass any of it through: discard the unproven face and rebuild
|
|
@@ -198,17 +218,23 @@ export function registerOnlineJsLane(tools) {
|
|
|
198
218
|
return {
|
|
199
219
|
performed: { errcode: 0, published },
|
|
200
220
|
readback: {
|
|
221
|
+
...(args.sheetCode === undefined ? { sheetCodeFromSchema: targetSheetCode } : {}),
|
|
201
222
|
publishedSha256,
|
|
202
223
|
draftSha256,
|
|
203
224
|
draftPublishedInSync,
|
|
204
225
|
publishedElementCount: publishedElements.length,
|
|
205
226
|
scriptElementPresent: publishedElements.some((element) => element.id === "customScript"),
|
|
206
|
-
|
|
227
|
+
scriptElementMeaning: "skeleton 空槽不等于存在脚本",
|
|
228
|
+
composeMode: args.replace === true ? "replace" : "incremental",
|
|
229
|
+
...(removing ? { removed: true } : {}),
|
|
230
|
+
scriptSha256: customScriptSha256,
|
|
207
231
|
customScriptSha256,
|
|
232
|
+
scriptSha256DeprecatedAlias: "customScriptSha256",
|
|
208
233
|
customScriptBytes: Buffer.byteLength(composedScript, "utf8"),
|
|
209
234
|
composedDraftBytes: Buffer.byteLength(composedDraftHtml, "utf8"),
|
|
210
235
|
publishedHtmlBytes: Buffer.byteLength(publishedRaw, "utf8"),
|
|
211
236
|
htmlRenderCompat,
|
|
237
|
+
verifyWith: "onlinejs.get → published.scriptSha256",
|
|
212
238
|
hint: "summary 模式不回原文;要脚本原文用 onlinejs.get,要完整布局用 artifact 或 form.get --full",
|
|
213
239
|
},
|
|
214
240
|
};
|
|
@@ -216,6 +242,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
216
242
|
return {
|
|
217
243
|
performed: { errcode: 0, published },
|
|
218
244
|
readback: {
|
|
245
|
+
...(args.sheetCode === undefined ? { sheetCodeFromSchema: targetSheetCode } : {}),
|
|
219
246
|
// Dedupe: after a successful publish the draft and the published
|
|
220
247
|
// face are the same bytes, and the script also rides inside them.
|
|
221
248
|
// Return the composed payload ONCE (composedDraftHtmlJson keeps the
|
|
@@ -230,10 +257,13 @@ export function registerOnlineJsLane(tools) {
|
|
|
230
257
|
draftPublishedInSync,
|
|
231
258
|
publishedElementCount: publishedElements.length,
|
|
232
259
|
scriptElementPresent: publishedElements.some((element) => element.id === "customScript"),
|
|
260
|
+
scriptElementMeaning: "skeleton 空槽不等于存在脚本",
|
|
233
261
|
customScript: composedScript,
|
|
234
262
|
customScriptMatchesPublished: draftPublishedInSync,
|
|
235
|
-
|
|
263
|
+
composeMode: args.replace === true ? "replace" : "incremental",
|
|
264
|
+
...(removing ? { removed: true } : {}),
|
|
236
265
|
htmlRenderCompat,
|
|
266
|
+
verifyWith: "onlinejs.get → published.scriptSha256",
|
|
237
267
|
portalVerificationPlan: ["在门户打开该表单", "触发已绑定的在线 JS 事件", "检查事件产生的副作用字段或校验提示"],
|
|
238
268
|
},
|
|
239
269
|
};
|
|
@@ -38,7 +38,7 @@ export function dataSubmitDescription() {
|
|
|
38
38
|
const wire = RUNTIME_DATA_WIRE;
|
|
39
39
|
return describeTool(`M2 runtime data SUBMIT (runtime/form/submit, WorkItemSubmitVO).`, [
|
|
40
40
|
"Distinct from data.save: data.save persists as DRAFT; data.submit advances the record to COMPLETED (no workflow) or drives the workflow instance.",
|
|
41
|
-
"
|
|
41
|
+
"**审批提交(带 workItemId)可省略 `formData`**:平台使用已存业务数据,且不要携带子表数组;新建记录(无 bizObjectId/workItemId)才必须传 formData。此前把两种场景混成 Required 是错误提示(2026-11-07 r40)。",
|
|
42
42
|
`就地语义:${wire.submitInPlace};无流程 readback 必须确认 objectId 与传入 bizObjectId 相同;无 bizObjectId 的 workflowCode 新建单 → PROCESSING + workflowInstanceId,无流程 → 该行 COMPLETED。`,
|
|
43
43
|
`既有草稿发起流程的唯一支持形状是两段式:先 data.save 带 workflowCode 暂存(${wire.workflowDraftSave}),再以返回的 workItemId 提交(issue#11 P6:带流程模型提交**必须**带 bizObjectId+workflowInstanceId+workItemId 三元——只带 bizObjectId 会答「流程实例无待办」、只缺 workItemId 会 50000;完整形状见 omc describe data.submit --example workflow-backed-data-submit,待办查询用 workitem.list)。`,
|
|
44
44
|
`仅无 workflowCode/workItemId/workflowInstanceId 且无 bizObjectId 时保留 save→submit 便捷链(新建记录);只要存在 workItemId 或 workflowInstanceId 就 fail-closed 拒绝(blocker submit-biz-object-id-required),零 runtime/form/submit wire。`,
|
|
@@ -33,8 +33,9 @@ function dataListItemPreview(items) {
|
|
|
33
33
|
return { ids, itemsPreview };
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* r40a B:`data.list` 的大结果被外置后不能回 `items:[]`,否则调用方会把
|
|
37
|
+
* 「有数据但在 artifact」误读成「空结果」。保留一个明确的 items 外置指针,
|
|
38
|
+
* 让通用读门不会再补回空数组占位。
|
|
38
39
|
*
|
|
39
40
|
* **只在确实要丢弃 items 时才补 `ids`/`itemsPreview`**:这段只在 governedRead
|
|
40
41
|
* 判定 `large`(>2048B)后才会被调用。早期版本把 ids/preview 无条件塞进 handler
|
|
@@ -55,8 +56,14 @@ function dataListCompactProjection(result) {
|
|
|
55
56
|
compact.itemsPreview = preview.ids.slice(0, 3).map((id) => ({ id }));
|
|
56
57
|
if (value.length > 0)
|
|
57
58
|
compact.listArtifactHint =
|
|
58
|
-
"items
|
|
59
|
-
|
|
59
|
+
"items 已外置:items 是指针而非空结果;完整行见 artifact(detailArtifactId / fetch)。ids=本页全部 id;itemsPreview=ids 前 3 个的引用(仅 {id},不是第二份行)。";
|
|
60
|
+
// The generic read gate adds [] for dropped array keys. Keep an explicit
|
|
61
|
+
// pointer under the original key so it cannot manufacture that false
|
|
62
|
+
// empty-result shape. The full rows remain in detailArtifactId.
|
|
63
|
+
compact.items = { externalized: true, count: value.length };
|
|
64
|
+
compact.truncatedKeys = ["items"];
|
|
65
|
+
compact.truncated = true;
|
|
66
|
+
compact.itemsExternalized = { count: value.length };
|
|
60
67
|
continue;
|
|
61
68
|
}
|
|
62
69
|
// 这两个键由本函数按 items 现算,handler 不再重复内联。
|
|
@@ -1867,9 +1874,9 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1867
1874
|
* gone, not that the model is clean — a submit can leave a separate draft row
|
|
1868
1875
|
* behind. After a delete, read the model's residue so "cleaned up" is a
|
|
1869
1876
|
* model-level fact (`total:0`). Best-effort: a query failure never fails the
|
|
1870
|
-
|
|
1877
|
+
* delete, but it IS surfaced as an explicit readback error.
|
|
1871
1878
|
*/
|
|
1872
|
-
async function
|
|
1879
|
+
async function remainingRows(session, schemaCode, sheetCode) {
|
|
1873
1880
|
try {
|
|
1874
1881
|
const result = await wireCall(session, {
|
|
1875
1882
|
path: ENDPOINTS.runtimeQueryList,
|
|
@@ -1877,12 +1884,12 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1877
1884
|
body: { schemaCode, queryCode: schemaCode, filters: [], mobile: false, page: 0, size: 1, queryVersion: 1, showTotal: true },
|
|
1878
1885
|
});
|
|
1879
1886
|
const page = extractBizObjectPage(result.data);
|
|
1880
|
-
return {
|
|
1887
|
+
return { remainingRows: page.total, clean: page.total === 0, sheetCode, cleanNote: "clean 表示删除该行后该表是否已为空" };
|
|
1881
1888
|
}
|
|
1882
1889
|
catch (error) {
|
|
1883
1890
|
if (error instanceof WireError && error.kind === "business" && error.errcode === RUNTIME_DATA_WIRE.emptyModelErrcode)
|
|
1884
|
-
return {
|
|
1885
|
-
return {
|
|
1891
|
+
return { remainingRows: 0, clean: true, sheetCode, cleanNote: "clean 表示删除该行后该表是否已为空" };
|
|
1892
|
+
return { remainingRows: null, clean: null, sheetCode, cleanNote: "clean 表示删除该行后该表是否已为空", error: error instanceof Error ? error.message : String(error) };
|
|
1886
1893
|
}
|
|
1887
1894
|
}
|
|
1888
1895
|
writeTool({
|
|
@@ -1981,13 +1988,14 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1981
1988
|
noop: true,
|
|
1982
1989
|
recoveryHint: "若另一进程刚刚删除,这是正常并发结果(already-absent),不是 id 拿错。否则:真实 id 必须取自 data.list 的 items[].id;工作流中心/门户页面回显的 id 会脱敏。",
|
|
1983
1990
|
thisRow: { absent: true, deleted: false },
|
|
1984
|
-
|
|
1991
|
+
thisRowDeleted: true,
|
|
1992
|
+
...(await remainingRows(session, args.schemaCode, sheetCode)),
|
|
1985
1993
|
},
|
|
1986
1994
|
};
|
|
1987
1995
|
}
|
|
1988
1996
|
return {
|
|
1989
1997
|
performed: { errcode: one.errcode, deleted: one.deleted === true, noop: false, sheetCode, resolvedFrom },
|
|
1990
|
-
readback: { absent: one.absent, deleted: one.deleted === true, thisRow: { absent: one.absent, deleted: one.deleted === true }, ...(await
|
|
1998
|
+
readback: { absent: one.absent, deleted: one.deleted === true, thisRowDeleted: one.deleted === true, thisRow: { absent: one.absent, deleted: one.deleted === true }, ...(await remainingRows(session, args.schemaCode, sheetCode)) },
|
|
1991
1999
|
};
|
|
1992
2000
|
}
|
|
1993
2001
|
const deleted = [];
|
|
@@ -1995,7 +2003,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1995
2003
|
deleted.push(await deleteOne(id));
|
|
1996
2004
|
// `deleted`/`count` ride `readback` — the write envelope surfaces only
|
|
1997
2005
|
// performed/readback inline (extra top-level keys are artifact-only).
|
|
1998
|
-
return { performed: { sheetCode, resolvedFrom }, readback: { absent: true, count: deleted.length, deleted, ...(await
|
|
2006
|
+
return { performed: { sheetCode, resolvedFrom }, readback: { absent: true, count: deleted.length, deleted, ...(await remainingRows(session, args.schemaCode, sheetCode)) } };
|
|
1999
2007
|
},
|
|
2000
2008
|
});
|
|
2001
2009
|
writeTool({
|
|
@@ -2007,7 +2015,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2007
2015
|
inputSchema: {
|
|
2008
2016
|
schemaCode: z.string().min(1),
|
|
2009
2017
|
sheetCode: z.string().optional(),
|
|
2010
|
-
formData: runtimeFormDataArg,
|
|
2018
|
+
formData: runtimeFormDataArg.optional().describe("business-object data keyed by field code;新建记录必填,审批提交(带 workItemId)可省略——平台使用已存业务数据"),
|
|
2011
2019
|
workflowCode: z.string().optional(),
|
|
2012
2020
|
workflowInstanceId: z.string().optional(),
|
|
2013
2021
|
workItemId: z.string().optional(),
|
|
@@ -2077,7 +2085,16 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2077
2085
|
});
|
|
2078
2086
|
}
|
|
2079
2087
|
const { sheetCode, resolvedFrom } = await resolveFormSheetCode(session, { schemaCode: args.schemaCode, sheetCode: args.sheetCode });
|
|
2080
|
-
|
|
2088
|
+
if (!hasWorkItem && args.formData === undefined) {
|
|
2089
|
+
throw new ToolInputError({
|
|
2090
|
+
status: "error",
|
|
2091
|
+
blocker: "tool-input-invalid",
|
|
2092
|
+
tool: "data.submit",
|
|
2093
|
+
message: "新建提交缺少 formData;审批提交可省略 formData(子表数组不要带),新建记录才必须传 formData。",
|
|
2094
|
+
nextCommand: "omc call data.submit --args '{\"schemaCode\":\"<schemaCode>\",\"formData\":{...}}'",
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
const stampedSubmit = await prepareRuntimeFormData(session, args.schemaCode, (args.formData ?? {}), { isUpdate: targetObjectId !== null });
|
|
2081
2098
|
const formData = stampedSubmit.formData;
|
|
2082
2099
|
// r5-asset 2026-10-20: the platform APPENDS child rows carried in an
|
|
2083
2100
|
// approval submit's formData (no upsert by row id, no error). Live: an
|
|
@@ -2089,7 +2106,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2089
2106
|
// {bizObject:{data:{...}}}, and a top-level-only check let those through
|
|
2090
2107
|
// (the platform appends whatever it finds). The child codes come from the
|
|
2091
2108
|
// model's own field list.
|
|
2092
|
-
const carried = collectChildArrayPaths(args.formData, new Set(stampedSubmit.childCodes));
|
|
2109
|
+
const carried = collectChildArrayPaths((args.formData ?? {}), new Set(stampedSubmit.childCodes));
|
|
2093
2110
|
if (carried.length > 0)
|
|
2094
2111
|
throw new ToolRefusalError({
|
|
2095
2112
|
status: "refused",
|
|
@@ -2124,7 +2141,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2124
2141
|
`确要在提交携带人员字段(个别平台构建接受),显式带 allowStaffOnSubmit:true 自担。`,
|
|
2125
2142
|
});
|
|
2126
2143
|
}
|
|
2127
|
-
const validation = await portalRequiredViolations(session, args.schemaCode, sheetCode, args.formData).catch(() => ({ requiredFields: [], missingRequired: [] }));
|
|
2144
|
+
const validation = await portalRequiredViolations(session, args.schemaCode, sheetCode, args.formData ?? {}).catch(() => ({ requiredFields: [], missingRequired: [] }));
|
|
2128
2145
|
// Directive: a workflow-backed model's submit must START the workflow, not
|
|
2129
2146
|
// merely save a sheet row. When the caller names no workflow params, detect
|
|
2130
2147
|
// the model's workflow and start it (formType=1 + workflowCode); a model
|
|
@@ -2178,7 +2195,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2178
2195
|
bizObject: {
|
|
2179
2196
|
schemaCode: args.schemaCode,
|
|
2180
2197
|
sheetCode,
|
|
2181
|
-
data: args.formData,
|
|
2198
|
+
data: args.formData ?? {},
|
|
2182
2199
|
},
|
|
2183
2200
|
agree: args.agree !== false,
|
|
2184
2201
|
};
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -3314,12 +3314,20 @@ export function registerWorkflowLane(tools) {
|
|
|
3314
3314
|
const purposeHint = args.workflowInstanceId === undefined && args.workflowCode === undefined
|
|
3315
3315
|
? "本调用用于发现待办并取 workItemId 做审批;只想看流程走到哪,用 workflow.instanceActivity;提交/审批后优先复用 data.submit 的 readback.nextSubmit,不要重复查询。"
|
|
3316
3316
|
: undefined;
|
|
3317
|
+
// r40a-E(2026-11-07,E2E F29):按实例查且本人口径 total:0 时,与「流程没生成」
|
|
3318
|
+
// 不可区分——探针差点误判「审批没生成」。存在范围外待办时显式说明口径差。
|
|
3319
|
+
const scopedToInstance = args.workflowInstanceId !== undefined && args.participant === undefined;
|
|
3320
|
+
const unfiltered = page.unfilteredTotal;
|
|
3321
|
+
const othersScopeNote = scopedToInstance && page.total === 0 && typeof unfiltered === "number" && unfiltered > 0
|
|
3322
|
+
? `total:0 是当前登录人口径——查询范围(unfilteredTotal=${unfiltered})内存在他人的待办。若该实例应有待办:待办按持有人过滤,用 participant:<持有人id> 复查(持有人见 data.submit 回执 flowWarning);不要据此判定「流程未生成」。`
|
|
3323
|
+
: undefined;
|
|
3317
3324
|
return {
|
|
3318
3325
|
total: page.total,
|
|
3319
3326
|
// The JPA-spec path reports no page count; only emit it when known.
|
|
3320
3327
|
...("totalPages" in page && page.totalPages !== undefined ? { totalPages: page.totalPages } : {}),
|
|
3321
3328
|
itemsCount: items.length,
|
|
3322
3329
|
...(purposeHint ? { purposeHint } : {}),
|
|
3330
|
+
...(othersScopeNote ? { scopeNote: othersScopeNote } : {}),
|
|
3323
3331
|
...(args.participant !== undefined ? { participant: args.participant, state: args.state ?? "PENDING", source: "list_workitems" } : {}),
|
|
3324
3332
|
...("unfilteredTotal" in page && page.unfilteredTotal !== undefined ? { unfilteredTotal: page.unfilteredTotal } : {}),
|
|
3325
3333
|
items,
|
|
@@ -3498,6 +3506,7 @@ export function registerWorkflowLane(tools) {
|
|
|
3498
3506
|
fullName: row.fullName ?? null,
|
|
3499
3507
|
corpId: row.corpId ?? null,
|
|
3500
3508
|
code: row.code ?? null,
|
|
3509
|
+
...(row.origin === "system-preset" || row.defaultPermission === true || row.preset === true ? { origin: "system-preset" } : row.origin === "user" ? { origin: "user" } : {}),
|
|
3501
3510
|
}));
|
|
3502
3511
|
const actor = await resolvePermissionActor(session);
|
|
3503
3512
|
const effectiveFor = {
|
|
@@ -3512,6 +3521,7 @@ export function registerWorkflowLane(tools) {
|
|
|
3512
3521
|
note: actor.isAdmin
|
|
3513
3522
|
? "当前登录人是平台管理员:发起成功不证明白名单生效。要用非管理员账号另建环境验证。"
|
|
3514
3523
|
: "本读面只回配置行;membership 命中需非管理员实发 data.submit 才能证伪。",
|
|
3524
|
+
...(actor.isAdmin ? { verifiable: false, verifyWith: `以普通账号凭据跑同查询:omc call workflow.permissions --env <env> --args '${JSON.stringify({ workflowCode })}'` } : {}),
|
|
3515
3525
|
};
|
|
3516
3526
|
return {
|
|
3517
3527
|
workflowCode,
|
package/dist/mcp-graph.js
CHANGED
|
@@ -47,7 +47,7 @@ const INSTRUCTIONS_INDEXED = [
|
|
|
47
47
|
"- 改名/删除/重构一个 字段(field)/模型(model)/服务(service) 前,先用 omc_graph_explore 查引用与影响面;查\"谁引用了 X\"也走它。",
|
|
48
48
|
"- 索引只覆盖:业务规则(methodMapping/targetObjectCode)、数据规则(propertyCodes 与公式 {字段})、视图(列/排序/条件)。**不含**规则条件叶子与流程绑定。",
|
|
49
49
|
"- search 语义:只匹配规则 code+name 的**词首**(中文子串不命中:搜「预算」匹配不到「增加预算」)。",
|
|
50
|
-
"答案带 freshness/pulledAtIso——索引是快照,平台侧改动后需 CLI 重建:omc graph pull --env <env>(只读远端,约 1 分钟)。",
|
|
50
|
+
"答案带 freshness/pulledAtIso——索引是快照,平台侧改动后需 CLI 重建:omc graph pull --env <env>(只读远端,约 1 分钟)。MCP 按注册时的 --path 读库;CLI pull 必须把 --path 指到本目录才能写入本库。",
|
|
51
51
|
"索引绑定单一环境(environmentName);查别的环境先重新 pull。",
|
|
52
52
|
].join("\n");
|
|
53
53
|
const INSTRUCTIONS_NO_INDEX = [
|
|
@@ -60,7 +60,7 @@ const EXPLORE_TOOL = {
|
|
|
60
60
|
description: [
|
|
61
61
|
"一次查全一个目标的引用与影响面(本地索引,毫秒级)。",
|
|
62
62
|
"输入 query 可为业务名(实体 name+code 子串)或规则 code+name 词首;也可直接给字段/模型/服务 code。kind 可选 field|model|service,不给则三种都查。",
|
|
63
|
-
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via}]}, impact:{kind:totals}, search:{matches:{model,field,service},rules}}。",
|
|
63
|
+
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via,referrer,isSelf}]}, impact:{kind:totals}, search:{matches:{model,field,service},rules}}。",
|
|
64
64
|
"references/impact 支持 offset+limit 分页;truncated 时按返回的 nextOffset 继续调用。实体分页结果带 nextCall;分页不会改变 resolved。字段同名时请用 schemaCode 消歧。",
|
|
65
65
|
"改/删/重命名任何字段、模型、服务之前必查;找\"关于 X 的规则\"拿 code 也走这里。",
|
|
66
66
|
"索引不含规则条件叶子与流程绑定;freshness=partial 时命中可能不完整(答案里有 gapWarning)。",
|
|
@@ -69,8 +69,11 @@ const EXPLORE_TOOL = {
|
|
|
69
69
|
type: "object",
|
|
70
70
|
properties: {
|
|
71
71
|
query: { type: "string", description: "业务名(实体 name+code 子串)、字段/模型/服务 code,或规则检索词(规则 code+name 词首匹配)" },
|
|
72
|
+
queries: { type: "array", items: { type: "string" }, description: "实体面多条件检索;与 query 并存时合并,match 默认 any" },
|
|
73
|
+
match: { type: "string", enum: ["all", "any"], description: "queries[] 的模型∪字段所属模型求交/并" },
|
|
72
74
|
kind: { type: "string", enum: [...KINDS], description: "限定一种引用种类;省略则 field/model/service 全查" },
|
|
73
75
|
schemaCode: { type: "string", description: "限定字段所属模型;也可用于校验模型。" },
|
|
76
|
+
applicationCode: { type: "string", description: "限定应用;仅返回该应用的实体与引用" },
|
|
74
77
|
env: { type: "string", description: "期望的环境名;与索引所属环境不一致时会拒绝(防跨环境误答)" },
|
|
75
78
|
offset: { type: "integer", minimum: 0, description: "引用/实体结果起始偏移;truncated 时使用 nextOffset" },
|
|
76
79
|
limit: { type: "integer", minimum: 1, maximum: MATCH_CAP, description: "每类结果最多返回多少条(默认 50)" },
|
|
@@ -125,8 +128,8 @@ function entityInventory() {
|
|
|
125
128
|
const db = new DatabaseSync(graphDbPath(), { readOnly: true });
|
|
126
129
|
try {
|
|
127
130
|
return [
|
|
128
|
-
...db.prepare("SELECT schema_code, name FROM models").all().map((r) => ({ kind: "model", schemaCode: r.schema_code, code: r.schema_code, name: r.name ?? "" })),
|
|
129
|
-
...db.prepare("SELECT schema_code, code, name FROM fields").all().map((r) => ({ kind: "field", schemaCode: r.schema_code, code: r.code, name: r.name ?? "" })),
|
|
131
|
+
...db.prepare("SELECT schema_code, name, application_code FROM models").all().map((r) => ({ kind: "model", schemaCode: r.schema_code, code: r.schema_code, name: r.name ?? "", applicationCode: r.application_code ?? undefined })),
|
|
132
|
+
...db.prepare("SELECT schema_code, code, name, application_code FROM fields").all().map((r) => ({ kind: "field", schemaCode: r.schema_code, code: r.code, name: r.name ?? "", applicationCode: r.application_code ?? undefined })),
|
|
130
133
|
...db.prepare("SELECT code, name FROM services").all().map((r) => ({ kind: "service", code: r.code, name: r.name ?? "" })),
|
|
131
134
|
];
|
|
132
135
|
}
|
|
@@ -159,12 +162,16 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
159
162
|
}
|
|
160
163
|
if (name === EXPLORE_TOOL.name) {
|
|
161
164
|
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
const queryList = Array.isArray(args.queries) ? args.queries.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean) : [];
|
|
166
|
+
const queries = queryList.length > 0 ? queryList : query ? [query] : [];
|
|
167
|
+
if (queries.length === 0)
|
|
168
|
+
return toolError("query 或 queries 不能为空");
|
|
169
|
+
const primaryQuery = queries[0];
|
|
164
170
|
const kind = args.kind === undefined ? undefined : String(args.kind);
|
|
165
171
|
if (kind !== undefined && !KINDS.includes(kind))
|
|
166
172
|
return toolError(`kind 必须是 ${KINDS.join("|")} 之一`);
|
|
167
173
|
const schemaCode = typeof args.schemaCode === "string" && args.schemaCode.trim() ? args.schemaCode.trim() : undefined;
|
|
174
|
+
const applicationCode = typeof args.applicationCode === "string" && args.applicationCode.trim() ? args.applicationCode.trim() : undefined;
|
|
168
175
|
if (schemaCode && kind !== "field" && kind !== "model")
|
|
169
176
|
return toolError("schemaCode 仅适用于 kind=field|model");
|
|
170
177
|
const status = readGraphStatus();
|
|
@@ -179,11 +186,34 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
179
186
|
const impact = {};
|
|
180
187
|
// Resolve from an unpaged search. The result page is presentation only;
|
|
181
188
|
// otherwise offset=2 can silently change the subject being inspected.
|
|
182
|
-
const search = searchGraphRules(
|
|
183
|
-
const
|
|
184
|
-
|
|
189
|
+
const search = searchGraphRules(primaryQuery, limit, offset);
|
|
190
|
+
const scopedSearch = applicationCode ? {
|
|
191
|
+
...search,
|
|
192
|
+
matches: {
|
|
193
|
+
...search.matches,
|
|
194
|
+
model: { ...search.matches.model, items: search.matches.model.items.filter((row) => row.applicationCode === applicationCode) },
|
|
195
|
+
field: { ...search.matches.field, items: search.matches.field.items.filter((row) => row.applicationCode === applicationCode) },
|
|
196
|
+
},
|
|
197
|
+
} : search;
|
|
198
|
+
const resolvedSearch = searchGraphRules(primaryQuery, MATCH_CAP, 0);
|
|
199
|
+
const loweredQuery = primaryQuery.toLocaleLowerCase();
|
|
185
200
|
const allCandidates = entityInventory().filter((r) => (r.code.toLocaleLowerCase().includes(loweredQuery) || r.name.toLocaleLowerCase().includes(loweredQuery)) &&
|
|
186
|
-
(!schemaCode || (r.kind !== "service" && r.schemaCode === schemaCode))
|
|
201
|
+
(!schemaCode || (r.kind !== "service" && r.schemaCode === schemaCode)) &&
|
|
202
|
+
(!applicationCode || r.kind === "service" || r.applicationCode === applicationCode));
|
|
203
|
+
const candidatesForQuery = (term) => entityInventory().filter((r) => (r.code.toLocaleLowerCase().includes(term.toLocaleLowerCase()) || r.name.toLocaleLowerCase().includes(term.toLocaleLowerCase())) &&
|
|
204
|
+
(!schemaCode || (r.kind !== "service" && r.schemaCode === schemaCode)) &&
|
|
205
|
+
(!applicationCode || r.kind === "service" || r.applicationCode === applicationCode));
|
|
206
|
+
const perQueryModels = queries.map((term) => new Set(candidatesForQuery(term).filter((r) => r.kind === "model").map((r) => r.schemaCode)));
|
|
207
|
+
const perQueryFieldModels = queries.map((term) => new Set(candidatesForQuery(term).filter((r) => r.kind === "field").map((r) => r.schemaCode)));
|
|
208
|
+
const combine = (sets) => {
|
|
209
|
+
if (sets.length === 0)
|
|
210
|
+
return new Set();
|
|
211
|
+
if (args.match === "all")
|
|
212
|
+
return new Set([...sets[0]].filter((value) => sets.every((set) => set.has(value))));
|
|
213
|
+
return new Set(sets.flatMap((set) => [...set]));
|
|
214
|
+
};
|
|
215
|
+
const hitModels = combine(perQueryModels);
|
|
216
|
+
const hitFieldModels = combine(perQueryFieldModels);
|
|
187
217
|
const typedCandidates = kind ? allCandidates.filter((r) => r.kind === kind) : allCandidates;
|
|
188
218
|
const fieldCandidates = kind === "field" ? typedCandidates : allCandidates.filter((r) => r.kind === "field");
|
|
189
219
|
const exactSame = fieldCandidates.filter((r) => r.code === query);
|
|
@@ -232,7 +262,7 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
232
262
|
}
|
|
233
263
|
}
|
|
234
264
|
const decorate = (row) => withMatchedOn({ ...row, ...(row.schemaCode && row.code === undefined ? { code: row.schemaCode } : {}) }, query);
|
|
235
|
-
const decoratedMatches = { ...
|
|
265
|
+
const decoratedMatches = { ...scopedSearch.matches, model: { ...scopedSearch.matches.model, items: scopedSearch.matches.model.items.map(decorate) }, field: { ...scopedSearch.matches.field, items: scopedSearch.matches.field.items.map(decorate) }, service: { ...scopedSearch.matches.service, items: scopedSearch.matches.service.items.map(decorate) } };
|
|
236
266
|
const noEntityHit = search.matches.model.total === 0 && search.matches.field.total === 0 && search.matches.service.total === 0;
|
|
237
267
|
const composite = /\s|\+|,|\b(?:AND|OR)\b/i.test(query);
|
|
238
268
|
const searchNote = "实体检索:业务名/实体 name+code 使用子串匹配。\n规则检索:规则 code+name 使用词首匹配(中文子串不命中)。";
|
|
@@ -242,8 +272,12 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
242
272
|
return toolResult({
|
|
243
273
|
status: "ok",
|
|
244
274
|
query,
|
|
275
|
+
...(queries.length > 1 ? { queries, match: args.match === "all" ? "all" : "any" } : {}),
|
|
276
|
+
hitModels: [...hitModels],
|
|
277
|
+
hitFields: [...hitFieldModels].flatMap((schema) => entityInventory().filter((r) => r.kind === "field" && r.schemaCode === schema).map((r) => ({ schemaCode: schema, code: r.code, name: r.name }))),
|
|
245
278
|
found: resolvedEntity !== undefined,
|
|
246
279
|
...(resolvedEntity ? { resolved: { kind: resolvedEntity.kind, schemaCode: "schemaCode" in resolvedEntity ? resolvedEntity.schemaCode : "", code: resolvedEntity.code, matchedOn: { on: resolvedEntity.name.includes(query) ? "name" : "code", fragment: query } } } : {}),
|
|
280
|
+
...(resolvedEntity ? { otherCandidates: allCandidates.filter((r) => r.code !== resolvedEntity.code).slice(0, 5).map((r) => ({ schemaCode: r.schemaCode ?? r.code, name: r.name })) } : {}),
|
|
247
281
|
...(ambiguous ? { ambiguous: true, candidates: exactSame.map((r) => ({ schemaCode: r.schemaCode, code: r.code, name: r.name })) } : {}),
|
|
248
282
|
...(kind !== undefined ? { kind } : {}),
|
|
249
283
|
index,
|
|
@@ -253,7 +287,7 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
253
287
|
...(nextCall ? { nextCall } : {}),
|
|
254
288
|
...(noEntityHit && composite ? { unsupportedQuerySyntax: true, hint: "组合检索暂不支持,请拆词单查后求交" } : {}),
|
|
255
289
|
...(noEntityHit ? { suggestions: suggestions(query), hint: "可尝试 suggestions 中的实体 code/name;引用明细请使用匹配 code 查 references/impact" } : {}),
|
|
256
|
-
searchNote: `${searchNote}${paginationNote}`,
|
|
290
|
+
searchNote: `${searchNote}${paginationNote}${queries.length > 1 ? " 规则面(词首匹配)不参与 queries[] 的实体求交。" : ""}`,
|
|
257
291
|
...(search.note ? { searchNote: `${searchNote}\n${search.note}` } : {}),
|
|
258
292
|
// 旧句「索引不含规则条件叶子与流程绑定」漏了表单与流程权限(r35a-F4 实证
|
|
259
293
|
// 探针据此差点误删被表单引用的字段)。改为指向结构化 coverage,不再靠散文。
|
package/dist/model-purge.js
CHANGED
|
@@ -324,6 +324,7 @@ export async function cmdModelPurge(args) {
|
|
|
324
324
|
const result = {
|
|
325
325
|
status,
|
|
326
326
|
schemaCode,
|
|
327
|
+
verdict: plan.modelExists && !failed ? "cascading" : "clean",
|
|
327
328
|
...(plan.workflowCode !== undefined ? { workflowCode: plan.workflowCode } : {}),
|
|
328
329
|
deletedInstances: steps.filter((step) => step.step.startsWith("workflow.delete_instance:") && step.status === "ok").length,
|
|
329
330
|
...(unremovableInstances.length > 0 ? { unremovableInstances } : {}),
|
package/dist/residue.js
CHANGED
|
@@ -7,6 +7,15 @@ import { SessionManager } from "./session-manager.js";
|
|
|
7
7
|
import { makeToolContext } from "./context.js";
|
|
8
8
|
import { emitInvalid, emitResult } from "./command-transport.js";
|
|
9
9
|
const MAX_ITEMS = 20;
|
|
10
|
+
const SYSTEM_RULE_NAMES = new Set(["Available", "Cancel", "Create", "Delete", "GetList", "Load", "Update"]);
|
|
11
|
+
function residueOrigin(row, faceName) {
|
|
12
|
+
if (row.defaultRule === true || row.preset === true || row.system === true || row.systemPreset === true)
|
|
13
|
+
return "system-preset";
|
|
14
|
+
if (faceName === "rule" && SYSTEM_RULE_NAMES.has(nameOf(row, codeOf(row))))
|
|
15
|
+
return "system-preset";
|
|
16
|
+
// Do not guess for datarules or otherwise unmarked rows.
|
|
17
|
+
return row.origin === "system-preset" || row.origin === "user" ? row.origin : undefined;
|
|
18
|
+
}
|
|
10
19
|
function face(faceName, rows, extra = {}) {
|
|
11
20
|
return { face: faceName, ...extra, count: rows.length, items: rows.slice(0, MAX_ITEMS) };
|
|
12
21
|
}
|
|
@@ -64,8 +73,8 @@ async function deepFaces(session, model) {
|
|
|
64
73
|
// A missing form is an absent online-js face, not a scan failure.
|
|
65
74
|
}
|
|
66
75
|
return [
|
|
67
|
-
face("rule", rules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { model: schemaCode }),
|
|
68
|
-
face("datarule", datarules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { model: schemaCode }),
|
|
76
|
+
face("rule", rules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)), ...(residueOrigin(row, "rule") ? { origin: residueOrigin(row, "rule") } : {}) })), { model: schemaCode }),
|
|
77
|
+
face("datarule", datarules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)), ...(residueOrigin(row, "datarule") ? { origin: residueOrigin(row, "datarule") } : {}) })), { model: schemaCode }),
|
|
69
78
|
face("onlinejs", online, { model: schemaCode }),
|
|
70
79
|
];
|
|
71
80
|
}
|
|
@@ -92,13 +101,21 @@ export async function scanResidues(session, prefix, options = {}) {
|
|
|
92
101
|
for (const model of models)
|
|
93
102
|
faces.push(...await deepFaces(session, model));
|
|
94
103
|
}
|
|
104
|
+
const systemPresetCount = faces.reduce((sum, item) => sum + item.items.filter((row) => row.origin === "system-preset").length, 0);
|
|
105
|
+
const userResidues = faces.reduce((sum, item) => sum + item.items.filter((row) => row.origin !== "system-preset").length, 0);
|
|
95
106
|
const totalResidues = faces.reduce((sum, item) => sum + item.count, 0);
|
|
107
|
+
const modelFace = faces.filter((item) => item.face === "model");
|
|
108
|
+
const hasModelResidue = modelFace.some((item) => item.count > 0);
|
|
109
|
+
const hasOtherResidue = faces.some((item) => item.face !== "model" && item.count > 0);
|
|
110
|
+
const pendingCascade = !hasModelResidue && hasOtherResidue;
|
|
96
111
|
return {
|
|
97
112
|
prefix,
|
|
98
113
|
faces,
|
|
99
114
|
totalResidues,
|
|
100
|
-
clean:
|
|
101
|
-
|
|
115
|
+
clean: userResidues === 0 && !pendingCascade,
|
|
116
|
+
...(systemPresetCount > 0 ? { systemPresetCount } : {}),
|
|
117
|
+
...(pendingCascade ? { pendingCascade: true } : {}),
|
|
118
|
+
note: pendingCascade ? "模型已不存在但仍有级联面命中:级联窗口内,等 40-60s 重跑,幂等" : "report 面暂停维护未扫描;workflow 模板跟模型走(模型删即删)",
|
|
102
119
|
};
|
|
103
120
|
}
|
|
104
121
|
export async function cmdResidue(prefix, options, transport) {
|
package/dist/rule-when.js
CHANGED
|
@@ -34,10 +34,65 @@ function parseWhen(raw) {
|
|
|
34
34
|
const value = /^-?\d+(\.\d+)?$/.test(rawValue.trim()) ? Number(rawValue.trim()) : rawValue.trim();
|
|
35
35
|
return { field, op, value };
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* 人读层只投影图中真正存在的节点;它不是另一套编译结果。
|
|
39
|
+
* `matchBy` 明确区分执行门(dataCondition)与选行(filterCondition),
|
|
40
|
+
* 避免 dry-run 只给 sha256 时把两者误认为同一种条件。
|
|
41
|
+
*/
|
|
42
|
+
export function compiledRuleSummary(nodeGraph) {
|
|
43
|
+
const nodes = Array.isArray(nodeGraph.nodes) ? nodeGraph.nodes : [];
|
|
44
|
+
return nodes
|
|
45
|
+
.filter((node) => !!node && typeof node === "object" && nodeTypeOf(node) !== undefined && nodeTypeOf(node) !== "START" && nodeTypeOf(node) !== "END")
|
|
46
|
+
.map((node) => {
|
|
47
|
+
const dataCondition = node.dataCondition;
|
|
48
|
+
const filterCondition = node.filterCondition;
|
|
49
|
+
const rawActions = Array.isArray(node.dataActions) ? node.dataActions : Array.isArray(node.actions) ? node.actions : [];
|
|
50
|
+
const currentRowFilter = filterCondition !== undefined && conditionText(filterCondition) === "id=id";
|
|
51
|
+
const matchBy = dataCondition !== undefined
|
|
52
|
+
? `dataCondition: ${conditionText(dataCondition)}`
|
|
53
|
+
: filterCondition !== undefined && !currentRowFilter
|
|
54
|
+
? `filterCondition: ${conditionText(filterCondition)}`
|
|
55
|
+
: "always";
|
|
56
|
+
const actions = rawActions
|
|
57
|
+
.filter((action) => !!action && typeof action === "object")
|
|
58
|
+
.map((action) => {
|
|
59
|
+
const field = action.targetDataItem ?? action.field ?? action.targetField;
|
|
60
|
+
const value = action.value ?? action.currentDataItemValue;
|
|
61
|
+
return {
|
|
62
|
+
...(field !== undefined ? { field } : {}),
|
|
63
|
+
...(value !== undefined ? { value } : {}),
|
|
64
|
+
...(action.ruleValueType !== undefined ? { valueType: action.ruleValueType } : {}),
|
|
65
|
+
...(action.ruleActionType !== undefined ? { actionType: action.ruleActionType } : {}),
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
return { nodeType: nodeTypeOf(node), matchBy, actions };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function nodeTypeOf(node) {
|
|
72
|
+
return typeof node.nodeType === "string" ? node.nodeType : undefined;
|
|
73
|
+
}
|
|
74
|
+
function conditionText(condition) {
|
|
75
|
+
if (!condition || typeof condition !== "object")
|
|
76
|
+
return "always";
|
|
77
|
+
const value = condition;
|
|
78
|
+
const groups = Array.isArray(value.conditionGroup) ? value.conditionGroup : Array.isArray(value.mainConditions) ? value.mainConditions : [];
|
|
79
|
+
const leaves = Array.isArray(groups[0]) ? groups[0] : groups;
|
|
80
|
+
const text = leaves
|
|
81
|
+
.filter((leaf) => !!leaf && typeof leaf === "object")
|
|
82
|
+
.map((leaf) => {
|
|
83
|
+
const field = leaf.currentSchemaDataItem ?? leaf.targetSchemaDataItem ?? "?";
|
|
84
|
+
const op = leaf.ruleDataConditionType ?? leaf.ruleConditionType ?? "?";
|
|
85
|
+
const operators = { EQ: "=", NEQ: "!=", GT: ">", LT: "<", GTEQ: ">=", LTEQ: "<=", CT: "~=", NCT: "!~" };
|
|
86
|
+
const right = leaf.value
|
|
87
|
+
?? (leaf.currentDataItemValue !== undefined ? `{${leaf.currentDataItemValue}}` : undefined)
|
|
88
|
+
?? (leaf.targetSchemaDataItem !== undefined ? leaf.currentSchemaDataItem : "?");
|
|
89
|
+
return `${String(field)}${operators[String(op)] ?? String(op)}${String(right)}`;
|
|
90
|
+
})
|
|
91
|
+
.join(" AND ");
|
|
92
|
+
return text || "always";
|
|
93
|
+
}
|
|
37
94
|
/** 编译 --when/--then DSL 为设计器验证形状的规则图(导出供测试断言)。 */
|
|
38
95
|
export function composeRuleWhenGraph(options) {
|
|
39
|
-
if (options.when.length === 0)
|
|
40
|
-
throw new Error("至少给一个 --when 条件;无条件更新整表是 bench23 病灶(空 mainConditions=无条件更新全部行)");
|
|
41
96
|
if (options.then.length === 0)
|
|
42
97
|
throw new Error("至少给一个 --then 动作");
|
|
43
98
|
const leaves = [];
|
|
@@ -81,7 +136,7 @@ export function composeRuleWhenGraph(options) {
|
|
|
81
136
|
// UPDATE.filterCondition selects rows; it is not an execution gate.
|
|
82
137
|
// Keep the trigger row as the default target and put --when on the
|
|
83
138
|
// platform's real execution-gate field (dataCondition).
|
|
84
|
-
dataCondition: { conditionGroup: [leaves] },
|
|
139
|
+
...(leaves.length > 0 ? { dataCondition: { conditionGroup: [leaves] } } : {}),
|
|
85
140
|
filterCondition: { mainConditions: [[{ targetSchemaDataItem: "id", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "id" }]], sheetConditions: [] },
|
|
86
141
|
dataActions: actions },
|
|
87
142
|
{ nodeCode: "End", nodeType: "END" },
|
|
@@ -126,7 +181,7 @@ export async function cmdRuleWhen(args) {
|
|
|
126
181
|
const name = flag("--name");
|
|
127
182
|
const environment = flag("--env") ?? flag("--environment");
|
|
128
183
|
const apply = args.includes("--apply");
|
|
129
|
-
const usage = '用法:omc rule-when --schema <模型> --on create|update --when "字段=值"
|
|
184
|
+
const usage = '用法:omc rule-when --schema <模型> --on create|update [--when "字段=值" ...] --then "set:字段=值|{来源字段}|{公式}" [--then ...] [--apply]\n触发时机只有 create|update(**没有 submit**;“提交时”用 create 或 update)。条件操作符:= != > < >= <= ~=(包含) !~(不包含)。省略 --when 表示 always(仍按当前 _input.id 定位,不更新整表)。默认 dry-run;--apply 保存并发布(rule.enable 仍需显式执行)。';
|
|
130
185
|
const invalid = (message) => {
|
|
131
186
|
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
|
|
132
187
|
return CLI_EXIT_CODES.invalidInput;
|
|
@@ -160,8 +215,21 @@ export async function cmdRuleWhen(args) {
|
|
|
160
215
|
const scopedResult = {
|
|
161
216
|
...result,
|
|
162
217
|
rowScope,
|
|
218
|
+
compiled: compiledRuleSummary(result && typeof result.nodeGraph === "object" && result.nodeGraph !== null
|
|
219
|
+
? result.nodeGraph
|
|
220
|
+
: createArgs.nodeGraph),
|
|
221
|
+
detailReadHint: {
|
|
222
|
+
detailFile: "cat <detailFile>",
|
|
223
|
+
detailArtifactId: "omc artifact get <detailArtifactId>",
|
|
224
|
+
},
|
|
163
225
|
};
|
|
164
226
|
const classified = classifyResult(result);
|
|
227
|
+
if (apply && classified.status === "ok") {
|
|
228
|
+
scopedResult.effectiveAfter = {
|
|
229
|
+
note: "规则保存后生效存在延迟;改数据后以读回的 changedFields 为准,不要以第一次读回判未生效",
|
|
230
|
+
verifyHint: "同一条 data.update 后 data.load 复核",
|
|
231
|
+
};
|
|
232
|
+
}
|
|
165
233
|
writeEnvelope(envelope(classified.status, scopedResult, request, classified.error));
|
|
166
234
|
if (apply && classified.status === "ok")
|
|
167
235
|
process.stderr.write("[omc] 前台生效验证清单(后端通过≠门户生效):1) omc call rule.enable 启用;2) 造一条命中 --when 的测试数据提交;3) 门户打开该记录核对 --then 字段已变更;4) 再造一条不命中的数据确认不误改;5) 设计器打开本规则确认节点/条件显示正常。\n");
|