@stalfh233/omc-cli 0.6.20 → 0.6.21
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 +19 -1
- package/dist/lanes/online-js.js +26 -5
- package/dist/lanes/runtime-data-contract.js +1 -1
- package/dist/lanes/runtime-data.js +34 -17
- package/dist/lanes/workflow.js +8 -0
- 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 +2 -2
- package/skills/omc-write-hazards.md +9 -2
package/dist/cli.js
CHANGED
|
@@ -802,6 +802,24 @@ async function cmdCall(store, args) {
|
|
|
802
802
|
// Do not also copy the single-path value to top-level content.
|
|
803
803
|
if (extractPaths.length > 0 && !extractKeys && typeof result === "object" && result !== null)
|
|
804
804
|
delete result.content;
|
|
805
|
+
// r40a: unsupported jq language is an input error, never an `ok:true`
|
|
806
|
+
// projection with a null value. Keep the path engine's detailed hint but
|
|
807
|
+
// make the CLI contract actionable and machine-classifiable.
|
|
808
|
+
if (typeof result === "object" && result !== null && Array.isArray(result.extract)) {
|
|
809
|
+
for (const entry of result.extract)
|
|
810
|
+
delete entry.autoStripped;
|
|
811
|
+
const unsupported = result.extract.filter((entry) => entry.unsupportedSyntax === true);
|
|
812
|
+
if (unsupported.length > 0) {
|
|
813
|
+
const first = unsupported[0];
|
|
814
|
+
const message = `--jq 只接受路径选择器,不支持对象构造或管道语法(${String(first?.path ?? "")})。可运行示例:--jq 'fields[].code,fields[].typeName'`;
|
|
815
|
+
const invalid = { status: "error", blocker: "tool-input-invalid", unsupportedSyntax: true, paths: unsupported.map((entry) => entry.path), message, nextCommand: "omc call <tool> --args '<json>' --jq 'fields[].code,fields[].typeName'" };
|
|
816
|
+
return emit("error", invalid, CLI_EXIT_CODES.invalidInput);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
// `extract[]` is the canonical multi-path result. Do not emit the second
|
|
820
|
+
// row-alignment copy: callers can align the explicitly named path values.
|
|
821
|
+
if (typeof result === "object" && result !== null && extractPaths.length > 1)
|
|
822
|
+
delete result.rows;
|
|
805
823
|
}
|
|
806
824
|
// 读面未知键的非阻断回显:放在投影**之后**,否则会被 --jq 的窄投影吞掉。
|
|
807
825
|
if (ignoredReadKeys.length > 0 && typeof result === "object" && result !== null) {
|
|
@@ -2470,7 +2488,7 @@ async function main() {
|
|
|
2470
2488
|
pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
|
|
2471
2489
|
});
|
|
2472
2490
|
command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
|
|
2473
|
-
command.command("residue <prefix>").description("跨面前缀残留扫描(model 全租户/folder/custompage;默认 deep 再查命中模型的 rule/datarule/onlinejs)——收尾「删净」一次证明,clean:true
|
|
2491
|
+
command.command("residue <prefix>").description("跨面前缀残留扫描(model 全租户/folder/custompage;默认 deep 再查命中模型的 rule/datarule/onlinejs)——收尾「删净」一次证明,clean:true 即零残留。清理入口:omc model-purge --code <schemaCode>(按依赖倒序,默认 dry-run)。只读零写入;report 面暂停维护未扫描").option("--env <environment>", "环境名(默认按 cwd 的 .omc-env 解析)").option("--apps <codes>", "all(默认,全租户)或逗号分隔应用码").option("--no-deep", "只扫 model/folder/custompage,不深查命中模型").option("--max-output-bytes <bytes>").action(async (prefix, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdResidue(prefix, { env: options.env, apps: options.apps, ...(options.deep === false ? { noDeep: true } : {}) }, transport)); });
|
|
2474
2492
|
const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
|
|
2475
2493
|
command.command("commands").option("--json").option("--compact").option("--filter <keyword>").option("--lane <id>").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("commands", transport, undefined, { json: options.json === true, compact: options.compact === true, ...(options.filter ? { filter: options.filter } : {}), ...(options.lane ? { lane: options.lane } : {}), ...(options.jq !== undefined ? { jq: options.jq } : {}) })); });
|
|
2476
2494
|
command.command("describe <tool>").option("--full").option("--cache-ok", "允许 --full 在同会话重复读时短路为 artifact 指针(默认每次回全文)").option("--schema-only").option("--param <name>").option("--example [id]").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (tool, options) => {
|
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({
|
|
@@ -81,7 +83,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
81
83
|
sheetPublished: sheet.published === true,
|
|
82
84
|
draft: { ...draft, ...(draft.script === null ? { note: "草稿无 customScript 脚本(尚未 compose 或被清空)" } : {}) },
|
|
83
85
|
published: { ...published, ...(published.script === null ? { note: "发布面无 customScript 脚本" } : {}) },
|
|
84
|
-
...(draft.sha256 !==
|
|
86
|
+
...(draft.sha256 !== "" && published.sha256 !== ""
|
|
85
87
|
? { draftPublishedInSync: draft.sha256 === published.sha256 }
|
|
86
88
|
: {}),
|
|
87
89
|
hint: "脚本是 innerHTML 原文(含平台 (function(form){…}) 生命周期包装——那就是平台存储形态);改脚本用 onlinejs.compose(默认 INCREMENTAL 并入,replace:true 整段替换);同文件修订直接 replace:true 传完整脚本",
|
|
@@ -109,6 +111,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
109
111
|
sheetCode: z.string().min(1).optional().describe("表单槽位码;省略时默认 = schemaCode(与 form.draft/form.publish 同口径)"),
|
|
110
112
|
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
113
|
replace: z.boolean().optional().describe("true = 以 source 整体替换 customScript(含完整 (function(form){…}) 脚本)。默认 false = 增量并入既有脚本,保留平台模板与既有逻辑"),
|
|
114
|
+
confirmReplaceSha256: z.string().regex(/^[a-f0-9]{64}$/i).optional().describe("replace:true 覆盖既有脚本时必须等于当前脚本 SHA-256,防止静默覆盖线上改动"),
|
|
112
115
|
remove: z.boolean().optional().describe("true = 显式移除 customScript,使 onlinejs.get 返回 present:false;会写入并按 publish 设置发布。与 source 二选一"),
|
|
113
116
|
publish: z.boolean().optional().describe("默认 true:写草稿并推发布面;false = 只写草稿不推发布面"),
|
|
114
117
|
summary: z.boolean().optional().describe("true = 只回同步状态(sha/字节数/是否同步/渲染兼容),不回任何 HTML/JS 原文。确认「改动是否生效」用这个;要看原文用 onlinejs.get"),
|
|
@@ -126,6 +129,20 @@ export function registerOnlineJsLane(tools) {
|
|
|
126
129
|
const current = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: targetSheetCode } });
|
|
127
130
|
const existingSheet = (current.data ?? null);
|
|
128
131
|
const existingRaw = typeof existingSheet?.draftHtmlJson === "string" ? existingSheet.draftHtmlJson : undefined;
|
|
132
|
+
const existingPublishedRaw = typeof existingSheet?.publishedHtmlJson === "string" ? existingSheet.publishedHtmlJson : undefined;
|
|
133
|
+
const existingScriptState = readCustomScriptSource(existingRaw ?? existingPublishedRaw);
|
|
134
|
+
if (args.replace === true && existingScriptState.present && args.confirmReplaceSha256 !== existingScriptState.sha256) {
|
|
135
|
+
throw new ToolRefusalError({
|
|
136
|
+
status: "refused",
|
|
137
|
+
blocker: "onlinejs-replace-confirmation-required",
|
|
138
|
+
schemaCode: args.schemaCode,
|
|
139
|
+
sheetCode: targetSheetCode,
|
|
140
|
+
previousSha256: existingScriptState.sha256,
|
|
141
|
+
...(args.confirmReplaceSha256 !== undefined ? { providedSha256: args.confirmReplaceSha256 } : {}),
|
|
142
|
+
message: "replace:true 将覆盖已有线上脚本;必须传 confirmReplaceSha256 且与当前脚本 SHA-256 完全一致。未写入任何内容。",
|
|
143
|
+
nextCommand: `omc call onlinejs.get --args '${JSON.stringify({ schemaCode: args.schemaCode, sheetCode: targetSheetCode })}'`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
129
146
|
const existingCompat = validateHtmlRender(existingRaw);
|
|
130
147
|
// A persisted legacy face may contain an object or malformed elements.
|
|
131
148
|
// Do not pass any of it through: discard the unproven face and rebuild
|
|
@@ -203,7 +220,9 @@ export function registerOnlineJsLane(tools) {
|
|
|
203
220
|
draftPublishedInSync,
|
|
204
221
|
publishedElementCount: publishedElements.length,
|
|
205
222
|
scriptElementPresent: publishedElements.some((element) => element.id === "customScript"),
|
|
206
|
-
|
|
223
|
+
scriptElementMeaning: "skeleton 空槽不等于存在脚本",
|
|
224
|
+
composeMode: args.replace === true ? "replace" : "incremental",
|
|
225
|
+
...(removing ? { removed: true } : {}),
|
|
207
226
|
customScriptSha256,
|
|
208
227
|
customScriptBytes: Buffer.byteLength(composedScript, "utf8"),
|
|
209
228
|
composedDraftBytes: Buffer.byteLength(composedDraftHtml, "utf8"),
|
|
@@ -230,9 +249,11 @@ export function registerOnlineJsLane(tools) {
|
|
|
230
249
|
draftPublishedInSync,
|
|
231
250
|
publishedElementCount: publishedElements.length,
|
|
232
251
|
scriptElementPresent: publishedElements.some((element) => element.id === "customScript"),
|
|
252
|
+
scriptElementMeaning: "skeleton 空槽不等于存在脚本",
|
|
233
253
|
customScript: composedScript,
|
|
234
254
|
customScriptMatchesPublished: draftPublishedInSync,
|
|
235
|
-
|
|
255
|
+
composeMode: args.replace === true ? "replace" : "incremental",
|
|
256
|
+
...(removing ? { removed: true } : {}),
|
|
236
257
|
htmlRenderCompat,
|
|
237
258
|
portalVerificationPlan: ["在门户打开该表单", "触发已绑定的在线 JS 事件", "检查事件产生的副作用字段或校验提示"],
|
|
238
259
|
},
|
|
@@ -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,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.21",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"commander": "14.0.3",
|
|
58
|
-
"omc-core": "0.6.
|
|
58
|
+
"omc-core": "0.6.21",
|
|
59
59
|
"proper-lockfile": "4.1.2",
|
|
60
60
|
"write-file-atomic": "6.0.0",
|
|
61
61
|
"zod": "^3.23.8",
|
|
@@ -73,6 +73,6 @@
|
|
|
73
73
|
"directory": "packages/omc-cli"
|
|
74
74
|
},
|
|
75
75
|
"optionalDependencies": {
|
|
76
|
-
"@stalfh233/omc-fake-platform": "0.6.
|
|
76
|
+
"@stalfh233/omc-fake-platform": "0.6.21"
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -117,6 +117,7 @@ only the section for the capability you need —
|
|
|
117
117
|
- **按钮脚本(JS)读写**:`listview.get --buttonCode <code>`(或 `--buttons`)直接回 `scriptData`/`scriptBytes`/`scriptSha256`,不用翻 artifact;写用 `listview.configure --buttonScripts [{code,script}]` 只改脚本(保留其它字段),或在 `buttons[]` 里用 `script` 简写(= `actionConfig.scriptData`)。
|
|
118
118
|
- **recipe**:`omc describe listview.configure --example configure-default-list`
|
|
119
119
|
- **边界 / 陷阱**:默认视图已存在,`listview.configure` 省略 code 即配置默认视图。**按字段分组(queryLayer)未取证**——别猜写。
|
|
120
|
+
- **行级条件标红当前不支持**:`rowColorSetting` 在 8.6.26 没有平台类映射,配置会被拒绝。替代方案是增加/展示状态字段,再用视图筛选或分组表达重点记录;不要把列表行颜色当作已覆盖能力承诺。
|
|
120
121
|
|
|
121
122
|
## 字段级规则 `datarule`
|
|
122
123
|
|
|
@@ -144,10 +144,9 @@ you can wait for.
|
|
|
144
144
|
范围以任务书为界,任务书之外的增删改仍需新草稿。指纹重核与写后读回照旧。
|
|
145
145
|
此豁免**要求任务书显式声明预授权**;agent 不得自行把交互会话当成自治上下文。
|
|
146
146
|
|
|
147
|
-
## 7. Cleanup
|
|
147
|
+
## 7. Cleanup / 非交互 run
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
**清单外的对象仍要单独出草稿。** 清理只碰自有 `omc_mcp_*` 夹具。
|
|
149
|
+
清理授权捆在创建授权里:创建草稿写明「验收后删除这些」,确认即一并授权;**清单外对象仍单独出草稿**;清理只碰自有 `omc_mcp_*` 夹具。非交互 run 无 question 通道时按上文「任务书即草稿即批准」执行,另加两条:推定内容逐项写入草稿 `assumptions[]`;**超出任务书范围的写入不能推定**——停下,收口报告标红等新授权。
|
|
151
150
|
|
|
152
151
|
## 8. Canonical configuration order(规范配置顺序)
|
|
153
152
|
|
package/skills/omc-glossary.md
CHANGED
|
@@ -76,6 +76,8 @@ Terminology for the `omc` configuration CLI. Built on CloudPivot 8.6 (anchor
|
|
|
76
76
|
tools `data.save` / `data.load` / `data.submit` / `data.update` / `data.list`).
|
|
77
77
|
`data.load` is the authoritative custom-field readback; list projection
|
|
78
78
|
is a pinned boundary (custom fields keys-present-values-null).
|
|
79
|
+
- **记录状态 `sequenceStatus`** — 系统流转态枚举:`DRAFT`(草稿)、`PROCESSING`(流程处理中)、`COMPLETED`(已完成/提交态)。判态用 `data.load`,不要用 `data.list` 的入口统计;`data.list` 的 `draftPolicy` 只影响列表计入口径。无流程模型提交用 `data.submit`;有流程模型在 workitem 审批通过后才进入 `COMPLETED`。它不是业务状态字段,业务状态应另建字段并由规则维护。
|
|
80
|
+
- **业务状态** — 平台无内置业务状态字段;需求里的「状态改成大额维修」要显式建业务字段(如 `repairStatus`)+ `rule-when --then 'set:<状态字段>=<值>'` 维护;不要拿 `sequenceStatus` 承载业务枚举。
|
|
79
81
|
- **B2 service (bizservice)** — service + method registration face.
|
|
80
82
|
- **List view (query)** — design-time list-view header lifecycle with
|
|
81
83
|
forced LIST/showOnPc/unpublished defaults.
|
package/skills/omc-playbook.md
CHANGED
|
@@ -13,11 +13,13 @@ description: 一页纸业务场景步骤清单(采购申请 / 请假审批 /
|
|
|
13
13
|
|
|
14
14
|
1. `omc next "从零做采购申请:主子表、合计、条件审批、列表、按钮"`
|
|
15
15
|
2. `omc intent schema`(或直接按下方拼装器链)
|
|
16
|
-
3. `omc model-scaffold --app <app> --code <code> --name 采购申请 --fields '[{"code":"title","name":"标题","propertyType":"SHORT_TEXT"},{"code":"amount","name":"合计","propertyType":"NUMERICAL","options":{"scale":2}},{"code":"
|
|
17
|
-
|
|
16
|
+
3. `omc model-scaffold --app <app> --code <code> --name 采购申请 --fields '[{"code":"title","name":"标题","propertyType":"SHORT_TEXT"},{"code":"amount","name":"合计","propertyType":"NUMERICAL","options":{"scale":2}},{"code":"<主表code>_details","name":"明细","propertyType":"CHILD_TABLE","subFields":[{"code":"item","name":"品名","propertyType":"SHORT_TEXT"},{"code":"qty","name":"数量","propertyType":"NUMERICAL","options":{"scale":0}},{"code":"price","name":"单价","propertyType":"NUMERICAL","options":{"scale":2}},{"code":"lineAmount","name":"金额","propertyType":"NUMERICAL","options":{"scale":2}}]}]' --apply`
|
|
17
|
+
子表 code 是全局子 schema code,必须全局唯一;不要使用 `details`、`items` 这类通用名,按 `<主表code>_details` 这类带主表前缀的形式命名,避免撞名 40000。
|
|
18
|
+
4. `omc compute --schema <code> --target amount --formula 'SUM({<主表code>_details.lineAmount})' --apply`(⚠️ 只在门户算,REST 不回填)
|
|
18
19
|
5. 条件审批(金额>50000 两级):**不要** `approval-flow --condition`(无此参数)。线性先:`omc approval-flow --schema <code> --steps '经理@role:<mgr>' --steps '总监@role:<dir>' --apply`;条件分流改 `omc describe workflow.update --example workflow-conditional-branch` 后 `workflow.update`。
|
|
19
20
|
6. 发起权限:`omc call workflow.addPermission --args '{"workflowCode":"<code>_wf","unitId":"<deptOrRoleId>","unitType":1}'`(这是**谁能发起**,不是行级可见范围)
|
|
20
|
-
7. 列表:`omc call listview.configure --args '{"schemaCode":"<code>","fieldCodes":["title","amount"]}'`
|
|
21
|
+
7. 列表:`omc call listview.configure --args '{"schemaCode":"<code>","fieldCodes":["title","amount"],"conditions":[{"propertyCode":"assetNo","filterType":"Eq","defaultValue":"<设备编号>"}]}'`
|
|
22
|
+
按设备编号等条件查询时用 `conditions`;形状以 `omc describe listview.configure --param conditions` 为准(至少提供 `propertyCode`,再按字段补 `filterType`/`defaultValue`)。
|
|
21
23
|
8. 表单按钮:`omc call listview.configure --args '{"schemaCode":"<code>","buttons":[{"name":"提交审批","useLocation":"FORM","bindAction":"FLOW"}]}'`(形状以 describe 为准)
|
|
22
24
|
9. ⚠️ **行级「申请人只看自己」当前工具面不支持**(`workflow.permissions` / `org.role.setUserScope` 都不是行级数据权限)
|
|
23
25
|
|
package/skills/omc-start-here.md
CHANGED
|
@@ -12,9 +12,9 @@ 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
|
|
15
|
+
0. **调用与检索(跨平台)**:`omc call ... --args '<json>'` 的内联 JSON 在 Windows PowerShell 下因引号转义不可靠——**PS 环境优先 `--args-file <path>`**(把 JSON 写临时文件再引用;解析失败的报错里也有此提示)。长 JSON、含引号或中文换行的负载统一可走 stdin:`echo '<json>' | omc call <tool> --args -`;`--args-file <path>` 则必须位于项目配置根内。读命令输出默认把中文转义为 `\uXXXX`(防乱码)——**这是有意为之**:stdout 恒为「恰好一个合法 JSON envelope」,在任何代码页下都能被 `JSON.parse`/`ConvertFrom-Json` 还原(GitLab #17 T2 实证:曾让 `--utf8` 关掉转义,GBK 管道把信封结构解码坏、`ConvertFrom-Json` 中途失败)。`--utf8` 只让**人类诊断文本**(stderr/散文)按 UTF-8 原文输出;要连 stdout 也是中文明文,必须先让管道按 UTF-8 解码(`chcp 65001`)——只设 `OMC_ASCII_STDOUT=0` 是关写出端转义、不改管道解码,在 cp936 管道下会把信封解码坏。对输出做内容检索时**先解 JSON 再检索**(或 `--jq` 取值),"grep 搜不到中文"≠"没有配置"(静默假阴性坑)。stdout 恒为纯 JSON、人读进度在 stderr——脚本解析勿 `2>&1` 合流。
|
|
16
16
|
|
|
17
|
-
0. **拼装器优先(高频写面一条命令)**:整模型建模(模型+N 字段+子表+发布+表单)`omc model-scaffold`;**存量模型加字段(建→发布→重建表单→列表加列)`omc field-add`**;字段计算 `omc compute`;条件→动作 `omc rule-when`;线性审批流 `omc approval-flow`;表单默认值 `omc form-default`;存量盘点 `data.list all:true
|
|
17
|
+
0. **拼装器优先(高频写面一条命令)**:整模型建模(模型+N 字段+子表+发布+表单)`omc model-scaffold`;**存量模型加字段(建→发布→重建表单→列表加列)`omc field-add`**;字段计算 `omc compute`;条件→动作 `omc rule-when`;线性审批流 `omc approval-flow`;表单默认值 `omc form-default`;存量盘点 `data.list all:true`;收尾清理 `omc model-purge --code <schema>`(依赖倒序,默认 dry-run)+ `omc residue <prefix>` 证删净——命中这些形态先看 `--help`,比 recipe 手拼省 10+ 轮。
|
|
18
18
|
|
|
19
19
|
0d. **改线上已有配置:按范围选路**——单点(字段/视图/规则/布局)用 `omc call` 只传要改的参数;存量加字段用 `omc field-add`;整包收敛写**最小** `intent.yaml`(只声明要管的)再 `validate`/`plan`/`apply`;整面正文用 `form.get` + `artifact get --jq` 取回再写。`intent readback` 只做**等价校验**、不是导出可编辑草稿;结构化配置没有 pull/apply(仅规则源码走 `omc programming`)。
|
|
20
20
|
|
|
@@ -80,6 +80,10 @@ omc help omc-write-hazards
|
|
|
80
80
|
- 新模型先配列表视图再依赖 `data.list`。部分 8.6.26 读面在没有 query/view 配置时会把业务列投影为 null;单行核对可用 `data.load`。1.1.25 复证(2026-10-31):脚手架默认视图只含 createdTime/creater/name/sequenceNo 四个系统列,`data.list` 对不在列面的业务字段一律回 null——**null ≠ 数据丢失**,先 `listview.get` 看列面;修复=`listview.configure {fieldCodes:[…业务列…], replaceColumns:true}`,配完立即回值。
|
|
81
81
|
- **默认列表视图不能单独 `listview.delete`**(平台回 `303025 列表是默认列表`);要清掉它就删模型(`model.delete` 级联)。清理时别把它当失败。
|
|
82
82
|
|
|
83
|
+
## 删除链
|
|
84
|
+
|
|
85
|
+
`model.delete` 带走:默认表单/在线 JS/流程/列表视图/平台预置规则与数据规则;**不带走**:自定义业务规则、业务数据(须先删)。顺序:`onlinejs` → `data.delete` 逐条清数据 → 删自定义规则 → `omc model-purge --code <schema>`(依赖倒序,默认 dry-run)。漏传 `bizObjectId(s)` 会被 `tool-input-invalid` 拒绝并指路 `data.list` 取 id(1.1.25 批量 `bizObjectIds` 被拒,须逐个单删)——无「漏传=全删」暗门(真机实测)。清完 `data.load` 复核。
|
|
86
|
+
|
|
83
87
|
## 按钮
|
|
84
88
|
|
|
85
89
|
- **列表/行级按钮**用 `listview.configure` 的 `buttons`(`useLocation: LIST|ROW`)。
|
|
@@ -164,9 +168,13 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
164
168
|
|
|
165
169
|
- **本 CLI 只负责发布,不负责写 JS**。脚本内容的创作与增量合并不在本 CLI:若当前项目自带在线 JS 编写规范(例如其 `.cpdocs`/技能或项目文档),遵循它;否则向用户/设计者索取已定稿 `source`。不要在 omc 技能里手写/臆造 JS 逻辑。
|
|
166
170
|
- 经 omc 发布时用 `onlinejs.compose`:把已定稿的 `source` 组合进平台 skeleton 的 `customScript`,再 `publish`,并回读 `publishedHtmlJson` + SHA-256。不要用裸脚本覆盖平台 skeleton。
|
|
167
|
-
- **`replace
|
|
171
|
+
- **`replace`/`merge` 拒绝形状是设计**:`replace` 必须传完整 `(function(form){…})`(裸代码会写坏脚本槽);`merge` 传完整外层脚本也被拒(避免嵌套丢内容)。远端脚本为空时:完整脚本 + `replace:true`。
|
|
168
172
|
- 源码不能有 import/export,事件名必须在支持集合内;compose 后读回 draft/published HTML,浏览器不是默认步骤。
|
|
169
173
|
|
|
174
|
+
### 规范查找顺序
|
|
175
|
+
|
|
176
|
+
项目规范:沙盒/项目 `.cpdocs/` → 项目根技能/文档 → 任务书指定路径。cpdocs 的 `--confirm-sha256` 流水线与 omc 互不替代:compose 已内置 dry-run 指纹,等价=replace 前用 `onlinejs.get` 记当前 SHA。
|
|
177
|
+
|
|
170
178
|
## 固定验收顺序
|
|
171
179
|
|
|
172
180
|
执行阶段以 `omc-five-piece-flow` 为准;写面安全只额外强调两点:写前先 `commands/describe/contract` + dry-run 静态校验,写后权威 readback,必要时再用 CLI 触发运行验证。
|
|
@@ -237,4 +245,3 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
237
245
|
2. **来源子表字段是集合,不是逐行扇出**:`currentSchemaDataItem:"items.asset"` 会被解析成该子表所有行的值集合,目标主表仍只查询一次。写这个形状能存能发布,但运行 0 行命中(r5-asset 真机)。OMC 0.4.64 起 `rule-graph-cross-model-child-source-fanout` **拒绝**。
|
|
238
246
|
3. **一张单多行逐件回写**(每条明细各改一条另一模型的主表行)没有可用的纯配置形状——走 BIZ_ACTION 自调二开接口,一次处理多行;单行回写用主表字段做 DYNAMIC 来源。
|
|
239
247
|
4. **BRANCH 后接两个跨表 UPDATE** 真机报 50000;改用单个 UPDATE + 条件动作,或二开。
|
|
240
|
-
|