@stalfh233/omc-cli 0.6.24 → 0.6.25
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/capability-overview.js +4 -2
- package/dist/cli.js +5 -3
- package/dist/field-families.js +4 -0
- package/dist/form-support.js +13 -0
- package/dist/goal-routes.js +2 -2
- package/dist/lanes/businessrule-contract.js +1 -0
- package/dist/lanes/datarule.js +6 -3
- package/dist/lanes/field.js +17 -2
- package/dist/lanes/form.js +1 -0
- package/dist/lanes/list-view.js +16 -1
- package/dist/lanes/model.js +12 -2
- package/dist/lanes/organization.js +3 -3
- package/dist/lanes/rule-graph.js +16 -0
- package/dist/lanes/rulelog.js +1 -1
- package/dist/lanes/workflow.js +68 -0
- package/dist/model-scaffold.js +25 -7
- package/dist/read.js +6 -2
- package/dist/recipes/field.js +2 -2
- package/dist/recipes/online-js.js +14 -1
- package/dist/residue.js +6 -2
- package/dist/rule-support.js +16 -2
- package/dist/write.js +11 -0
- package/package.json +3 -3
- package/skills/omc-capabilities.md +6 -3
- package/skills/omc-playbook.md +15 -7
- package/skills/omc-refusals.md +7 -0
|
@@ -238,8 +238,10 @@ export const CAPABILITY_CUES = Object.freeze({
|
|
|
238
238
|
"不早于", "不能早于", "不晚于", "不能晚于", "早于今天", "晚于今天", "日期校验", "提交校验", "校验不通过", "必填校验", "正则校验", "重复校验", "长度校验"],
|
|
239
239
|
// NOTE: bare "自动" was a cue (round 10 bug: "自动识别"/"自动发邮件" matched
|
|
240
240
|
// businessrule and routed an UNSUPPORTED ask onto the write ladder). A business
|
|
241
|
-
// rule is named by 业务规则/触发器/规则图, not by "automatic".
|
|
242
|
-
|
|
241
|
+
// rule is named by 业务规则/触发器/规则图, not by "automatic". r44fc(2026-11-08)
|
|
242
|
+
// 重犯过一次(round10 测试当场抓住)——复合词(累加/联动/审计/规则触发/自动累加)才是
|
|
243
|
+
// 规则面线索,裸「自动」永远不是。
|
|
244
|
+
businessrule: ["业务规则", "触发器", "规则图", "校验", "事件", "按钮", "定时任务", "定时触发", "定时器", "每天", "每日", "周期执行", "调度", "SCHEDULER", "TIMING_TASK", "累加", "联动", "审计", "规则触发", "自动累加", "自动回写"],
|
|
243
245
|
"rule-lifecycle": ["启用规则", "停用规则", "删除规则", "规则启停", "触发规则", "执行规则", "跑规则", "按钮规则", "规则验收"],
|
|
244
246
|
// r28h F1/F7: narrow org cues so "管理组织:部门/角色/角色组/用户/权限范围"
|
|
245
247
|
// hits workflow (where org.* lives) instead of field→workflow. Keep them
|
package/dist/cli.js
CHANGED
|
@@ -2225,7 +2225,7 @@ async function main() {
|
|
|
2225
2225
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
2226
2226
|
});
|
|
2227
2227
|
command.command("doctor").option("--environment <environment>").option("--env <environment>").option("--service <serviceCode>", "可选:体检单个业务集成服务的鉴权参数形状(掩码/占位/全 0 检出,WARN 非致命)").action(async (options) => { commandExitCode = await cmdDoctor(new ConfigStore(defaultPaths()), options.environment ?? options.env, options.service); });
|
|
2228
|
-
command.command("call <tool>").option("--env <environment>", "环境名;项目白名单按当前 cwd 解析,须在含 .omc-env 的目录内运行").option("--environment <environment>").option("--args <json>").option("--args-file <path>", "从文件读参数 JSON。**必须位于配置根内**(数组批量请改用 --args-list;跨目录/系统临时文件会被拒);免落盘用 `--args -`(stdin)").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象,最多 200 条);超过上限请分批,或用 omc script 读文件。逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>", "最终 stdout 信封预算;默认 65536(投影/--jq 之后再判);读面 diet(~2048) 独立外置 detailArtifactId,抬高本参数不会把已 diet 的大表重新内联;artifact get 内联上限 8192").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
|
|
2228
|
+
command.command("call <tool>").option("--env <environment>", "环境名;项目白名单按当前 cwd 解析,须在含 .omc-env 的目录内运行").option("--environment <environment>").option("--args <json>").option("--args-file <path>", "从文件读参数 JSON。**必须位于配置根内**(数组批量请改用 --args-list;跨目录/系统临时文件会被拒);免落盘用 `--args -`(stdin)").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象,最多 200 条);超过上限请分批,或用 omc script 读文件。逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>", "最终 stdout 信封预算;默认 65536(投影/--jq 之后再判);读面 diet(~2048) 独立外置 detailArtifactId,抬高本参数不会把已 diet 的大表重新内联;artifact get 内联上限 8192").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>", "仅 `omc call` / `omc intent apply` 支持;写锁冲突时有界排队(0=立刻拒绝)").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
|
|
2229
2229
|
const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.argsList !== undefined ? ["--args-list", options.argsList] : []), ...(options.preflight ? ["--preflight"] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(Array.isArray(options.jq) ? options.jq.flatMap((path) => path === true ? ["--jq"] : ["--jq", path]) : []), ...(options.extractKeys ? ["--extract-keys"] : []),
|
|
2230
2230
|
...(options.keys ? ["--keys"] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs !== undefined ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
|
|
2231
2231
|
commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
|
|
@@ -2254,7 +2254,7 @@ async function main() {
|
|
|
2254
2254
|
.option("--env <environment>")
|
|
2255
2255
|
.option("--environment <environment>")
|
|
2256
2256
|
.option("--apply")
|
|
2257
|
-
.addHelpText("after", "\n一条命令按依赖倒序清掉一个模型:列表视图 → 业务规则 → 计算规则 → 流程实例 → 流程 →
|
|
2257
|
+
.addHelpText("after", "\n一条命令按依赖倒序清掉一个模型:列表视图 → 业务规则 → 计算规则 → 流程实例 → 流程 → 模型(字段/表单随模型级联,并级联删除流程模板 <code>_wf 及其发起权限绑定)。默认 dry-run。\n业务数据:--purge-instances 删流程实例;普通业务行(无流程)不会自动带走——先 data.delete 清(sweep-all-readback 配方),再 purge,否则留孤儿数据。\n")
|
|
2258
2258
|
.action(async (options) => {
|
|
2259
2259
|
const rest = ["model-purge",
|
|
2260
2260
|
...(options.code ? ["--code", options.code] : []),
|
|
@@ -2371,6 +2371,7 @@ async function main() {
|
|
|
2371
2371
|
.option("--name <name>", "模型名")
|
|
2372
2372
|
.option("--fields <json>", "JSON 数组,每条是一个 field.create 参数;子表加 subFields")
|
|
2373
2373
|
.option("--fields-json <json>")
|
|
2374
|
+
.option("--fields-file <path>", "从配置根内文件读字段 JSON 数组;与 --fields 互斥")
|
|
2374
2375
|
.option("--folder <folderId>", "建在哪个应用目录(省略=默认目录)")
|
|
2375
2376
|
.option("--app <applicationCode>")
|
|
2376
2377
|
.option("--env <environment>")
|
|
@@ -2396,6 +2397,7 @@ async function main() {
|
|
|
2396
2397
|
...(options.name ? ["--name", options.name] : []),
|
|
2397
2398
|
...(options.fields !== undefined ? ["--fields", options.fields] : []),
|
|
2398
2399
|
...(options.fieldsJson !== undefined ? ["--fields-json", options.fieldsJson] : []),
|
|
2400
|
+
...(options.fieldsFile !== undefined ? ["--fields-file", options.fieldsFile] : []),
|
|
2399
2401
|
...(options.folder ? ["--folder", options.folder] : []),
|
|
2400
2402
|
...(options.app ? ["--app", options.app] : []),
|
|
2401
2403
|
...(options.env ? ["--env", options.env] : []),
|
|
@@ -2530,7 +2532,7 @@ async function main() {
|
|
|
2530
2532
|
pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
|
|
2531
2533
|
});
|
|
2532
2534
|
command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
|
|
2533
|
-
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)); });
|
|
2535
|
+
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("--all-faces", "列出全部应用×面(含 count:0;默认只回非零面+byFace/userResidues 紧凑形)").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 } : {}), ...(options.allFaces ? { allFaces: true } : {}) }, transport)); });
|
|
2534
2536
|
const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
|
|
2535
2537
|
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 } : {}) })); });
|
|
2536
2538
|
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/field-families.js
CHANGED
|
@@ -625,6 +625,8 @@ export function normalizeMappingsOption(options) {
|
|
|
625
625
|
const target = record.target;
|
|
626
626
|
if (typeof source !== "string" || source.trim() === "" || typeof target !== "string" || target.trim() === "")
|
|
627
627
|
mappingsError("每项的 source/target 必须是非空字符串");
|
|
628
|
+
if (source.includes(".") || target.includes("."))
|
|
629
|
+
mappingsError("source/target 应为目标模型字段 code,不带模型前缀(不要使用 模型.字段)");
|
|
628
630
|
return { source: source.trim(), target: target.trim() };
|
|
629
631
|
});
|
|
630
632
|
normalized = JSON.stringify(rows);
|
|
@@ -644,6 +646,8 @@ export function normalizeMappingsOption(options) {
|
|
|
644
646
|
}
|
|
645
647
|
if (!Array.isArray(parsed) || parsed.some((row) => row === null || typeof row !== "object" || Array.isArray(row) || typeof row.source !== "string" || typeof row.target !== "string" || row.source === "" || row.target === ""))
|
|
646
648
|
mappingsError("解析结果必须是 [{source,target}] 形状(source/target 非空字符串)");
|
|
649
|
+
if (parsed.some((row) => String(row.source).includes(".") || String(row.target).includes(".")))
|
|
650
|
+
mappingsError("source/target 应为目标模型字段 code,不带模型前缀(不要使用 模型.字段)");
|
|
647
651
|
normalized = JSON.stringify(parsed.map(({ source, target }) => ({ source: source.trim(), target: target.trim() })));
|
|
648
652
|
}
|
|
649
653
|
}
|
package/dist/form-support.js
CHANGED
|
@@ -1338,6 +1338,7 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
1338
1338
|
attributes = undefined;
|
|
1339
1339
|
}
|
|
1340
1340
|
let rebuildPreservedControls = [];
|
|
1341
|
+
let controlsUpdated = 0;
|
|
1341
1342
|
if (typeof view !== "string" || typeof attributes !== "string") {
|
|
1342
1343
|
// Ticket 48: an explicit fieldCodes selection picks (and orders) the
|
|
1343
1344
|
// scaffolded business fields; omitted scaffolds every published field.
|
|
@@ -1364,6 +1365,7 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
1364
1365
|
attributes = JSON.stringify(scaffold.attributes);
|
|
1365
1366
|
}
|
|
1366
1367
|
if (args.draftAttributesJson === undefined && typeof attributes === "string") {
|
|
1368
|
+
const beforeDerived = attributes;
|
|
1367
1369
|
attributes = propagateRelationMappings(attributes, publishedFields);
|
|
1368
1370
|
// Ticket 69: repair/densify org-selection controls on the existing draft.
|
|
1369
1371
|
attributes = normalizeOrgSelectorControls(attributes, publishedFields);
|
|
@@ -1374,6 +1376,16 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
1374
1376
|
// control even when the form already existed — a non-rebuild draft keeps
|
|
1375
1377
|
// controls verbatim, so a later field.update never propagated (bench20).
|
|
1376
1378
|
attributes = syncFieldDerivedControlOptions(attributes, publishedFields);
|
|
1379
|
+
if (args.refreshFromFields === true && attributes !== beforeDerived) {
|
|
1380
|
+
try {
|
|
1381
|
+
const before = JSON.parse(beforeDerived);
|
|
1382
|
+
const after = JSON.parse(attributes);
|
|
1383
|
+
controlsUpdated = Object.keys(after).filter((code) => JSON.stringify(before[code]) !== JSON.stringify(after[code])).length;
|
|
1384
|
+
}
|
|
1385
|
+
catch {
|
|
1386
|
+
controlsUpdated = 0;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1377
1389
|
}
|
|
1378
1390
|
// Fail-closed: a rebuild that would drop a designer formula must never reach
|
|
1379
1391
|
// the wire. The merge above normally carries them, but it cannot when the
|
|
@@ -1428,6 +1440,7 @@ export async function executeFormDraftWrite(session, args) {
|
|
|
1428
1440
|
}
|
|
1429
1441
|
: {}),
|
|
1430
1442
|
...(args.rebuild === true && previousAttributes ? { rebuildPreservedControls } : {}),
|
|
1443
|
+
...(args.refreshFromFields === true ? { refreshFromFields: true, controlsUpdated } : {}),
|
|
1431
1444
|
// r36(2026-11-01):rebuild 后草稿与重建前逐字节一致时明示——rebuild 的
|
|
1432
1445
|
// 语义就是"按已发布字段重搭",一致通常意味着预期的新控件没进来(字段未
|
|
1433
1446
|
// 发布/写错槽位),静默 no-op 会让"新增字段后刷新表单"看起来成功实则没变。
|
package/dist/goal-routes.js
CHANGED
|
@@ -973,7 +973,7 @@ export function goalRoute(registry, goal) {
|
|
|
973
973
|
goal,
|
|
974
974
|
confident: true,
|
|
975
975
|
mode: whole ? "whole-package" : mode,
|
|
976
|
-
match: { kind: readish ? "read-ladder" : "ladder", ref: ordered.join("→")
|
|
976
|
+
match: { kind: readish ? "read-ladder" : "ladder", ref: `${ordered.join("→")}${ordered.includes("businessrule") && /累加|联动|审计|规则触发|自动累加|自动回写/.test(goal) ? "(规则面不在本阶梯)" : ""}`, capabilities: ordered },
|
|
977
977
|
route: {
|
|
978
978
|
...(whole ? { primary: "intent", atomicSupplement } : { ladder }),
|
|
979
979
|
// r28e F2(真机复现 2026-11-05):`next "建表单布局、在线JS、表单按钮"`
|
|
@@ -1059,7 +1059,7 @@ export function goalRoute(registry, goal) {
|
|
|
1059
1059
|
mode: whole ? "whole-package" : mode,
|
|
1060
1060
|
match: {
|
|
1061
1061
|
kind: readish ? "read-capability" : recipe ? "recipe" : "capability",
|
|
1062
|
-
ref: recipe ? recipe.recipe : capabilityId
|
|
1062
|
+
ref: recipe ? recipe.recipe : `${capabilityId}${capabilityId === "businessrule" && /累加|联动|审计|规则触发|自动累加|自动回写/.test(goal) ? "(规则面不在本阶梯)" : ""}`,
|
|
1063
1063
|
title: firstSentence(recipe ? recipe.title : route.title),
|
|
1064
1064
|
capability: capabilityId,
|
|
1065
1065
|
...(recipe ? { tool: recipe.tool, recipe: recipe.recipe } : {}),
|
|
@@ -72,6 +72,7 @@ const RULE_SAVE_DESCRIPTION = describeTool("保存业务规则(upsert):`ru
|
|
|
72
72
|
"**条件执行**:① 单分支→动作节点自身 `filterCondition`(UPDATE/DELETE/ASSIGN 的 `mainConditions`:身份匹配 DYNAMIC + 固定条件 FIXED,同叶组 AND)。② 多路分支→`BRANCH` 节点:出口叶(DataFilterCondition)键为 `{currentSchemaDataItem(左值),ruleDataConditionType(操作符),ruleValueType,value}`,出路由 `routeCondition:true` 走条件、`false` 兜底 else。**别照抄其它条件组的 targetSchemaDataItem+ruleConditionType——未知键 save+publish 过但运行时 50000(lint fail closed `rule-graph-branch-condition-keys`)**。按数据走流程流转用 `ruleConditionGroup`。",
|
|
73
73
|
"**生命周期铁律(2026-09-11 live 取证)**:update 只写 DRAFT 工作行,get/list 读 PUBLISH 面因此看不到变化——必须再 `rule.enable`(publish 出新版本 + 翻 DRAFT 启用开关)才会生效;数据事件分发:DRAFT 保存不触发,submit(DRAFT→COMPLETED)触发 Create。保存 ≠ 设计器可见:readback 的 `published:false` + nextAction 标明尚未发布;传 `publish:true` 可在同一次调用内发布新版本(仅发布,启用仍走 `rule.enable`)。",
|
|
74
74
|
"**BizMethod 绑定(ticket 35/96)**:BIZ_ACTION / GET_LIST(BIZ_SERVICE) 的 methodMapping 须逐键对 8.6.26 DTO(codeType int 0=CONST/1=PROPERTY…;非语义键原样透传);写前 wire 校验服务+方法真实存在(get_by_code + list_by_service_code),悬空即 refused `rule-graph-bizmethod-unresolved`——平台只拦服务不拦方法,绝不用注册壳冒充已发布 BizMethod;RESTFUL/DATA_SQL 方法必须带适配器必需 configJson 键,只有名字/参数面的注册壳 refused `rule-graph-bizmethod-registration-shell`;映射 `required` 以方法参数面为权威镜像(省略→按面填入,面缺席→保持缺席,显式矛盾→本地拒绝零写入)。",
|
|
75
|
+
"**EXPRESSION 值形状**:`ruleValueType:EXPRESSION` 时 `value` 必须是 {\"formula\":\"{字段A}+{字段B}\"}(大括号裸 code);门户设计器的 [code] 方括号式在这里不认。",
|
|
75
76
|
"**幻影 schema 门(ticket 96 live 事故)**:rule.save 写前先以 bizmodels/get_all 探测模型存在(缺失 → refused `rule-schema-unresolved`,零规则面 wire 调用)——平台的 list/save 面会对任意 schemaCode 自动补建 7 条预置规则且 API 删除不可达(list 即重灌),幻影 schema 必须在写前拒绝。",
|
|
76
77
|
"**输出回写排序(ticket 36 live 铁证)**:BIZ_ACTION 的 outputMappings 由 SYSTEM_CREATE/SYSTEM_UPDATE 持久化节点消费,动作节点必须排在系统节点之前(`Start→BizAction→SYSTEM_CREATE→End`);工具默认把系统节点补在 START 之后,有回写动作时须显式提供系统节点并置于动作之后,否则合并结果不落库(echoResult 恒 null)。",
|
|
77
78
|
]);
|
package/dist/lanes/datarule.js
CHANGED
|
@@ -745,14 +745,17 @@ const FORM_CONDITION_RULE_TYPES = Object.freeze({
|
|
|
745
745
|
export async function syncFormConditionDataRules(session, schemaCode, fieldCode, prevOptions, nextOptions) {
|
|
746
746
|
const result = { created: [], updated: [], deleted: [], skipped: [] };
|
|
747
747
|
const text = (options, key) => (typeof options[key] === "string" ? options[key].trim() : "");
|
|
748
|
-
const changedKeys = Object.keys(FORM_CONDITION_RULE_TYPES).filter((key) => text(prevOptions, key) !== text(nextOptions, key));
|
|
749
|
-
if (changedKeys.length === 0)
|
|
750
|
-
return result;
|
|
751
748
|
const existing = await listDataRules(session, schemaCode);
|
|
752
749
|
// The designer's rule rows live on the PUBLISHED face (like datarule.create's
|
|
753
750
|
// own guard); a draft-only field cannot carry a designer-visible row yet.
|
|
754
751
|
const publishedCodes = await readPublishedFieldCodesOrNull(session, schemaCode);
|
|
755
752
|
const published = publishedCodes === null ? true : publishedCodes.has(fieldCode);
|
|
753
|
+
const changedKeys = Object.keys(FORM_CONDITION_RULE_TYPES).filter((key) => {
|
|
754
|
+
const changed = text(prevOptions, key) !== text(nextOptions, key);
|
|
755
|
+
const desired = text(nextOptions, key);
|
|
756
|
+
const present = existing.some((row) => Number(row.dataRuleType) === FORM_CONDITION_RULE_TYPES[key] && storedTargetCodes(row).includes(fieldCode));
|
|
757
|
+
return changed || (desired.length > 0 && !present);
|
|
758
|
+
});
|
|
756
759
|
for (const key of changedKeys) {
|
|
757
760
|
const dataRuleType = FORM_CONDITION_RULE_TYPES[key];
|
|
758
761
|
const next = text(nextOptions, key);
|
package/dist/lanes/field.js
CHANGED
|
@@ -266,7 +266,7 @@ export function registerFieldLane(tools) {
|
|
|
266
266
|
// 不带这个键,于是不会出现;write-only 的确认走 field.update 的
|
|
267
267
|
// readback.committed。
|
|
268
268
|
...(Object.prototype.hasOwnProperty.call(row, "propertyEmpty") ? { propertyEmpty: row.propertyEmpty } : {}),
|
|
269
|
-
...(options !== undefined ? { options } : {}),
|
|
269
|
+
...(options !== undefined ? { options, optionsPresent: true } : { optionsPresent: false }),
|
|
270
270
|
...(scaleWarning !== undefined ? { shapeWarning: scaleWarning, ...(system ? { origin: "system" } : {}) } : {}),
|
|
271
271
|
...(system && scaleWarning === undefined ? { origin: "system" } : {}),
|
|
272
272
|
};
|
|
@@ -292,7 +292,7 @@ export function registerFieldLane(tools) {
|
|
|
292
292
|
const projected = detailed
|
|
293
293
|
? visible
|
|
294
294
|
: visible.map((field) => {
|
|
295
|
-
const options = (field.options ?? {}
|
|
295
|
+
const options = ("options" in field ? field.options : undefined) ?? {};
|
|
296
296
|
const conditions = Object.keys(FORM_CONDITION_OPTION_KEYS).filter((key) => {
|
|
297
297
|
const value = options[key];
|
|
298
298
|
return key === "readonlyFormula" ? value === true : typeof value === "string" && value.trim() !== "";
|
|
@@ -314,6 +314,7 @@ export function registerFieldLane(tools) {
|
|
|
314
314
|
...(conditions.length > 0 ? { conditions } : {}),
|
|
315
315
|
...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
|
|
316
316
|
...(field.origin !== undefined ? { origin: field.origin } : {}),
|
|
317
|
+
...(args.full === true && "options" in field && field.options !== undefined ? { options: field.options, optionsPresent: true } : {}),
|
|
317
318
|
};
|
|
318
319
|
});
|
|
319
320
|
return {
|
|
@@ -1117,6 +1118,19 @@ async function runFieldUpdate(session, args) {
|
|
|
1117
1118
|
if (args.choices !== undefined && !same(choiceCodesOf(currentOptions) ?? [], [...args.choices]))
|
|
1118
1119
|
requestedChanges.push("choices");
|
|
1119
1120
|
if (requestedChanges.length === 0) {
|
|
1121
|
+
let unchangedSync;
|
|
1122
|
+
let unchangedSyncError;
|
|
1123
|
+
const conditionKeys = new Set(Object.keys(FORM_CONDITION_OPTION_KEYS));
|
|
1124
|
+
if (effectiveOptions !== undefined && [...conditionKeys].some((key) => typeof prepared.options[key] === "string" && String(prepared.options[key]).trim() !== "")) {
|
|
1125
|
+
try {
|
|
1126
|
+
const sync = await syncFormConditionDataRules(session, args.schemaCode, args.code, currentOptions, prepared.options);
|
|
1127
|
+
if (sync.created.length + sync.updated.length + sync.deleted.length + sync.skipped.length > 0)
|
|
1128
|
+
unchangedSync = sync;
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
unchangedSyncError = error instanceof Error ? error.message : String(error);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1120
1134
|
return {
|
|
1121
1135
|
performed: { errcode: 0, unchanged: true },
|
|
1122
1136
|
readback: {
|
|
@@ -1126,6 +1140,7 @@ async function runFieldUpdate(session, args) {
|
|
|
1126
1140
|
...(record.name !== undefined ? { name: record.name } : {}),
|
|
1127
1141
|
...(record.name_i18n !== undefined ? { name_i18n: record.name_i18n } : {}),
|
|
1128
1142
|
availableFields: ["name", "name_i18n", "options", "choices", "relativeCode", "propertyEmpty"],
|
|
1143
|
+
...(unchangedSync !== undefined ? { formConditionSync: unchangedSync } : unchangedSyncError !== undefined ? { formConditionSync: { error: unchangedSyncError } } : {}),
|
|
1129
1144
|
},
|
|
1130
1145
|
};
|
|
1131
1146
|
}
|
package/dist/lanes/form.js
CHANGED
|
@@ -296,6 +296,7 @@ export function registerFormLane(tools) {
|
|
|
296
296
|
.optional()
|
|
297
297
|
.describe("把子表列绑进**已有** Sheet201 控件的 columns(issue #24):[{parent:\"items\",fieldCodes:[\"category\"]}]。只追加列,不动其它控件/布局行;无已存布局时退回 scaffold"),
|
|
298
298
|
rebuild: z.boolean().optional().describe("true 时丢弃现有草稿布局,按已发布字段重新生成(新增字段/子表要用它;默认保留现有草稿)"),
|
|
299
|
+
refreshFromFields: z.boolean().optional().describe("true 时不重建布局,仅按当前已发布字段 options 刷新现有控件派生配置"),
|
|
299
300
|
fromReadback: z.boolean().optional().describe("true = 本次 draftViewJson/draftAttributesJson 来自 form.get 的**逐字未改动**原文(平台自身布局可能含子表列行/系统行超宽,逐字回写放行)。语义是「回写」不是「我读过」:读过之后改过一个字符就不再是回读,会被拒(去掉本参数按普通写面校验)。只加子表列请用 configure_form 的 childColumns"),
|
|
300
301
|
allowEmptyLayout: z.boolean().optional().describe("显式放行空布局(\"[]\"/\"{}\")写入;默认拒绝(form-empty-layout),防设计器 Save 清空后照写丢控件"),
|
|
301
302
|
buttons: z
|
package/dist/lanes/list-view.js
CHANGED
|
@@ -457,7 +457,22 @@ export function registerListViewLane(tools) {
|
|
|
457
457
|
description: LIST_VIEW_TOOLS["listview.list"].description,
|
|
458
458
|
inputSchema: { environment: environmentArg, schemaCode: z.string().min(1), capability: capabilityArg },
|
|
459
459
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
460
|
-
|
|
460
|
+
let rows;
|
|
461
|
+
try {
|
|
462
|
+
rows = wireRows((await wireCall(session, { path: ENDPOINTS.listQueryList, query: { schemaCode: args.schemaCode, clientType: "PC" } })).data);
|
|
463
|
+
}
|
|
464
|
+
catch (error) {
|
|
465
|
+
if (error instanceof WireError && error.errcode === 50000) {
|
|
466
|
+
try {
|
|
467
|
+
await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: args.schemaCode, isPublish: "true" } });
|
|
468
|
+
}
|
|
469
|
+
catch (probe) {
|
|
470
|
+
if (probe instanceof WireError && probe.errcode === 300004)
|
|
471
|
+
return { absent: true, schemaCode: args.schemaCode };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
461
476
|
return {
|
|
462
477
|
schemaCode: args.schemaCode,
|
|
463
478
|
count: rows.length,
|
package/dist/lanes/model.js
CHANGED
|
@@ -419,7 +419,14 @@ export function registerModelLane(tools) {
|
|
|
419
419
|
// 产物缺失/损坏时保持空摘要——不因解引用失败拖垮整个 inspect
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
|
-
const datarules = dataruleRows.map((rule) =>
|
|
422
|
+
const datarules = dataruleRows.map((rule) => {
|
|
423
|
+
const hasCondition = ["condition", "conditions", "conditionJson", "ruleCondition", "formula"].some((key) => {
|
|
424
|
+
const value = rule[key];
|
|
425
|
+
return value !== undefined && value !== null && value !== "" && !(Array.isArray(value) && value.length === 0);
|
|
426
|
+
});
|
|
427
|
+
const auto = rule.name == null && Number(rule.checkType) === 4 && !hasCondition;
|
|
428
|
+
return { propertyCode: rule.propertyCode, propertyCodes: rule.propertyCodes, dataRuleType: rule.dataRuleType, name: rule.name, checkType: rule.checkType, origin: auto ? "auto" : "user" };
|
|
429
|
+
});
|
|
423
430
|
// Workflow summary: use the PUBLISHED face for the `published` flag. The
|
|
424
431
|
// draft face's templateType is ALWAYS "DRAFT" (even for a published
|
|
425
432
|
// workflow — draft and published are separate faces), so deriving
|
|
@@ -496,7 +503,10 @@ export function registerModelLane(tools) {
|
|
|
496
503
|
: {}),
|
|
497
504
|
form: result.form,
|
|
498
505
|
listview: result.listview ? { code: result.listview.code, columnCount: result.listview.columnCount } : undefined,
|
|
499
|
-
ruleCodes: (result.rules
|
|
506
|
+
ruleCodes: Array.isArray(result.rules) ? result.rules.map((rule) => rule.code) : [],
|
|
507
|
+
...(Array.isArray(result.rules) && result.rules.length > 0
|
|
508
|
+
? { rules: { truncated: true, totalRules: result.rules.length, nextCommand: `omc call rule.list --args {"schemaCode":"${String(result.schemaCode ?? "")}"}` } }
|
|
509
|
+
: {}),
|
|
500
510
|
datarules: result.datarules,
|
|
501
511
|
// r35a-F2:workflow 摘要必须恒有 exists 布尔——compact 投影此前把它丢了,
|
|
502
512
|
// 真机(走 compact 的分支)于是永远读不到 exists(探针只能靠
|
|
@@ -1042,11 +1042,11 @@ export function registerOrganizationLane(tools) {
|
|
|
1042
1042
|
name: "org.role.delete",
|
|
1043
1043
|
lane: LANE,
|
|
1044
1044
|
scope: SCOPES.roleDelete,
|
|
1045
|
-
description: "删除角色(`GET organization/role/strike_out_role?ids=`)。平台**拒绝删除仍有成员的角色**(`-1 角色下已经包含用户数据!`,实测 1.1.25
|
|
1045
|
+
description: "删除角色(`GET organization/role/strike_out_role?ids=`)。平台**拒绝删除仍有成员的角色**(`-1 角色下已经包含用户数据!`,实测 1.1.25):默认先读成员并拒绝;拒绝回执带可直接粘贴的 `org.role.removeUsers` 命令。`force:true` 的语义是**先逐人移出全部成员,再删除角色**(不是强行绕过平台校验)。**可用 roleCode 代替 roleId**(org.role.list 的码表只有 code)。",
|
|
1046
1046
|
inputSchema: {
|
|
1047
1047
|
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
1048
1048
|
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表,如 qc_manager)或名称"),
|
|
1049
|
-
force: z.boolean().optional().describe("true =
|
|
1049
|
+
force: z.boolean().optional().describe("true = 先逐人调用 removeUsers 移出全部成员,再删角色;不会保留成员"),
|
|
1050
1050
|
},
|
|
1051
1051
|
execute: async (session, args) => {
|
|
1052
1052
|
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.delete");
|
|
@@ -1054,7 +1054,7 @@ export function registerOrganizationLane(tools) {
|
|
|
1054
1054
|
if (rows.length > 0 && args.force !== true)
|
|
1055
1055
|
throw new ToolRefusalError({
|
|
1056
1056
|
status: "refused", blocker: "role-has-members", roleId, members: rows.length,
|
|
1057
|
-
nextCommand: `omc call org.role.
|
|
1057
|
+
nextCommand: `omc call org.role.removeUsers --args '${JSON.stringify({ roleId, ids: rows.map(roleUserId) })}'`,
|
|
1058
1058
|
message: `角色下还有 ${rows.length} 名成员,平台拒绝删除(errmsg:角色下已经包含用户数据)。先 org.role.removeUsers,或传 force:true 让工具移出后再删。`,
|
|
1059
1059
|
});
|
|
1060
1060
|
if (rows.length > 0)
|
package/dist/lanes/rule-graph.js
CHANGED
|
@@ -1517,6 +1517,22 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
|
|
|
1517
1517
|
});
|
|
1518
1518
|
}
|
|
1519
1519
|
}
|
|
1520
|
+
// F11 (r44): `_input` is the trigger input binding, not a writable model.
|
|
1521
|
+
// QUERY may legitimately use inputParam:"_input"; only UPDATE targets are
|
|
1522
|
+
// rejected here because the platform accepts/publishes them and then throws
|
|
1523
|
+
// at runtime, rolling back the entire rule (including SYSTEM_CREATE).
|
|
1524
|
+
for (const node of nodeRecords) {
|
|
1525
|
+
if (String(node.nodeType ?? "") !== "UPDATE")
|
|
1526
|
+
continue;
|
|
1527
|
+
if (String(node.targetObjectCode ?? "").trim() !== "_input")
|
|
1528
|
+
continue;
|
|
1529
|
+
blockers.push({
|
|
1530
|
+
code: "rule-graph-update-target-input",
|
|
1531
|
+
nodeCode: String(node.nodeCode ?? ""),
|
|
1532
|
+
key: "targetObjectCode",
|
|
1533
|
+
message: 'UPDATE 的 targetObjectCode 不接受 `_input`(1.1.25 运行时 EXCEPTION 并回滚全规则);自更新写本模型 schemaCode;QUERY 的 inputParam 用 `_input` 不受影响',
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1520
1536
|
// Cross-model write-back: SUPPORTED (verified live 2026-09-28). An UPDATE whose
|
|
1521
1537
|
// `targetObjectCode` is a DIFFERENT model than the rule's own model writes back
|
|
1522
1538
|
// correctly once TWO prerequisites hold — a 2026-09-27 probe that reported
|
package/dist/lanes/rulelog.js
CHANGED
|
@@ -61,7 +61,7 @@ export function registerRuleLogLane(tools) {
|
|
|
61
61
|
}
|
|
62
62
|
return log;
|
|
63
63
|
}));
|
|
64
|
-
return { index: { schemaCode: args.schemaCode, page: requestedPage, size: requestedSize, totalElements: data.totalElements ?? logs.length, totalPages: data.totalPages ?? 0, ...(rowFilter ? { rowFilter } : {}) }, logs, notes: [...notes, ...(args.includeNodes === true ? ["节点 VO 不提供 targetBizObjectId,节点行级 id 不可得;仅前 3 条命中日志拉取节点链"] : [])] };
|
|
64
|
+
return { index: { schemaCode: args.schemaCode, page: requestedPage, size: requestedSize, totalElements: data.totalElements ?? logs.length, totalPages: data.totalPages ?? 0, ...(rowFilter ? { rowFilter } : {}) }, logs, notes: ["status=true 表示成功,status=false 表示失败", ...notes, ...(args.includeNodes === true ? ["节点 VO 不提供 targetBizObjectId,节点行级 id 不可得;仅前 3 条命中日志拉取节点链"] : [])] };
|
|
65
65
|
}, { target: `rulelog.search:${args.schemaCode}:${JSON.stringify(args)}` }),
|
|
66
66
|
}));
|
|
67
67
|
}
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -779,6 +779,55 @@ export function approvalByOriginatorWarnings(template) {
|
|
|
779
779
|
message: `所有参与人节点(${activityCodes.join("、")})都指向 {Originator}:该流程会由发起人自审自批;这是允许的单人流程,但请确认该行为符合业务意图。`,
|
|
780
780
|
}];
|
|
781
781
|
}
|
|
782
|
+
/**
|
|
783
|
+
* The runtime deliberately treats an empty role resolution as a successful
|
|
784
|
+
* activation owned by the originator. Keep this advisory: the originator
|
|
785
|
+
* may legitimately be a member of the configured role. The platform's
|
|
786
|
+
* participant chain is `ouScope ∩ originator department chain`; when that
|
|
787
|
+
* intersection is empty it silently takes this fallback path.
|
|
788
|
+
*/
|
|
789
|
+
export function participantFallbackWarnings(input) {
|
|
790
|
+
if (!input.originator)
|
|
791
|
+
return [];
|
|
792
|
+
const configured = new Map();
|
|
793
|
+
const rows = Array.isArray(input.template?.activities) ? input.template.activities : [];
|
|
794
|
+
for (const row of rows) {
|
|
795
|
+
if (String(row.activityType ?? "") !== "PARTICIPANT")
|
|
796
|
+
continue;
|
|
797
|
+
const code = String(row.activityCode ?? row.code ?? "");
|
|
798
|
+
const participant = typeof row.participant === "string" ? row.participant.trim() : "";
|
|
799
|
+
if (!code || !participant || /^(?:\{Originator\}|Originator)$/i.test(participant))
|
|
800
|
+
continue;
|
|
801
|
+
let role;
|
|
802
|
+
const fn = /FindUserByRole\s*\(\s*[^,]+,\s*(["'])(.*?)\1\s*,/i.exec(participant);
|
|
803
|
+
if (fn?.[2])
|
|
804
|
+
role = fn[2];
|
|
805
|
+
if (!role && participant.startsWith("[")) {
|
|
806
|
+
try {
|
|
807
|
+
const entries = JSON.parse(participant);
|
|
808
|
+
if (Array.isArray(entries)) {
|
|
809
|
+
const candidate = entries.find((entry) => entry && typeof entry === "object" && String(entry.type ?? "") === "2");
|
|
810
|
+
if (candidate)
|
|
811
|
+
role = String(candidate.code ?? candidate.name ?? candidate.id ?? "") || undefined;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
catch { /* advisory only */ }
|
|
815
|
+
}
|
|
816
|
+
if (role)
|
|
817
|
+
configured.set(code, role);
|
|
818
|
+
}
|
|
819
|
+
return input.activities
|
|
820
|
+
.filter((activity) => activity.status === 1 && activity.activityType === "PARTICIPANT" && activity.participantCount > 0)
|
|
821
|
+
.filter((activity) => activity.participants.every((person) => person.id === input.originator))
|
|
822
|
+
.map((activity) => ({
|
|
823
|
+
suspect: true,
|
|
824
|
+
nodeCode: activity.activityCode ?? "?",
|
|
825
|
+
...(configured.get(activity.activityCode ?? "") ? { configuredRole: configured.get(activity.activityCode ?? "") } : {}),
|
|
826
|
+
note: configured.has(activity.activityCode ?? "")
|
|
827
|
+
? "角色成员可能不覆盖发起人 OU,平台已退回发起人——核对 org.role.userScope"
|
|
828
|
+
: "参与者与发起人相同;若该节点配置的是角色,可能因成员 scope 不覆盖发起人 OU 而被平台退回发起人——核对 org.role.userScope",
|
|
829
|
+
}));
|
|
830
|
+
}
|
|
782
831
|
/**
|
|
783
832
|
* Role participants are filtered by `ouScope ∩ originator's department chain`.
|
|
784
833
|
* A role whose members all have an empty scope therefore resolves to NOBODY —
|
|
@@ -3371,6 +3420,24 @@ export function registerWorkflowLane(tools) {
|
|
|
3371
3420
|
// activity that currently holds tasks. No active activity while the
|
|
3372
3421
|
// instance is still PROCESSING is exactly the silent-stall shape.
|
|
3373
3422
|
const diagnosis = diagnoseInstanceActivity({ instanceStatus, endStatus: header ? wireString(header.endStatus) : null, originator: header ? wireString(header.originator) : null, activities });
|
|
3423
|
+
// The instance face gives the resolved participant, while the
|
|
3424
|
+
// published template gives the configured role. Compare both faces
|
|
3425
|
+
// here (and only here): data.submit wiring intentionally remains in
|
|
3426
|
+
// runtime-data.ts's owning batch. If the template cannot be read,
|
|
3427
|
+
// participantFallbackWarnings degrades to a participant==originator
|
|
3428
|
+
// hint rather than claiming a role mismatch.
|
|
3429
|
+
let publishedTemplate = null;
|
|
3430
|
+
const workflowCode = header ? wireString(header.workflowCode) ?? wireString(header.workflowCode_) : null;
|
|
3431
|
+
if (workflowCode) {
|
|
3432
|
+
try {
|
|
3433
|
+
const published = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode } });
|
|
3434
|
+
publishedTemplate = published.data && typeof published.data === "object" && !Array.isArray(published.data) ? published.data : null;
|
|
3435
|
+
}
|
|
3436
|
+
catch {
|
|
3437
|
+
publishedTemplate = null;
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
const participantFallback = participantFallbackWarnings({ activities, template: publishedTemplate, originator: header ? wireString(header.originator) : null });
|
|
3374
3441
|
return {
|
|
3375
3442
|
workflowInstanceId: args.workflowInstanceId,
|
|
3376
3443
|
...(instanceStatus ? { instanceStatus } : {}),
|
|
@@ -3379,6 +3446,7 @@ export function registerWorkflowLane(tools) {
|
|
|
3379
3446
|
activeCount: active.length,
|
|
3380
3447
|
reachedCount: reached.length,
|
|
3381
3448
|
activities,
|
|
3449
|
+
...(participantFallback.length ? { participantFallback } : {}),
|
|
3382
3450
|
diagnosis,
|
|
3383
3451
|
};
|
|
3384
3452
|
}),
|
package/dist/model-scaffold.js
CHANGED
|
@@ -20,6 +20,9 @@ import { argvValue } from "./args.js";
|
|
|
20
20
|
import { runToolCall } from "./call.js";
|
|
21
21
|
import { CLI_EXIT_CODES, classifyResult, envelope, refusalText, requestId } from "./cli-contract.js";
|
|
22
22
|
import { writeEnvelope } from "./cli-output.js";
|
|
23
|
+
import { assertSafeInputPath } from "./artifacts.js";
|
|
24
|
+
import { readFileSync } from "node:fs";
|
|
25
|
+
import { resolve } from "node:path";
|
|
23
26
|
/** Validate the caller's `--fields` payload (fail before any write). */
|
|
24
27
|
export function validateScaffoldFields(fields) {
|
|
25
28
|
const problems = [];
|
|
@@ -64,7 +67,10 @@ export async function cmdModelScaffold(args) {
|
|
|
64
67
|
const flag = (name) => argvValue(args, name);
|
|
65
68
|
const code = flag("--code") ?? flag("--schema") ?? flag("--schema-code");
|
|
66
69
|
const name = flag("--name");
|
|
67
|
-
const
|
|
70
|
+
const fieldsFile = flag("--fields-file");
|
|
71
|
+
const fieldsFlag = flag("--fields");
|
|
72
|
+
const fieldsStdin = fieldsFlag === "-";
|
|
73
|
+
const fieldsJson = fieldsStdin ? readFileSync(0, "utf8") : fieldsFlag ?? flag("--fields-json");
|
|
68
74
|
const folderId = flag("--folder") ?? flag("--folder-id");
|
|
69
75
|
const applicationCode = flag("--app");
|
|
70
76
|
const environment = flag("--env") ?? flag("--environment");
|
|
@@ -72,13 +78,13 @@ export async function cmdModelScaffold(args) {
|
|
|
72
78
|
const publishForm = !args.includes("--no-form");
|
|
73
79
|
const forcePublish = args.includes("--force-publish");
|
|
74
80
|
const usage = [
|
|
75
|
-
"用法:omc model-scaffold --code <模型code> --name <模型名> --fields '<JSON 数组>' [--folder <目录id>] [--apply]",
|
|
81
|
+
"用法:omc model-scaffold --code <模型code> --name <模型名> --fields '<JSON 数组>' [--fields-file <路径>|--fields -] [--folder <目录id>] [--apply]",
|
|
76
82
|
" --fields 每条是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...};子表加 \"subFields\":[...]。",
|
|
77
83
|
" 子表键名统一用 subFields(field.create 平台键);childFields 仍作别名接受,但 --help 只展示 subFields。",
|
|
78
84
|
" 默认 dry-run(列出将要做什么);--apply 依次执行 建模型→建字段→建子列→发布字段→发布表单。",
|
|
79
85
|
" 幂等:模型/字段已存在记为 skipped 继续;有真失败时默认不发布(--force-publish 可强发)。",
|
|
80
86
|
" 每个子调用使用独立 requestId(避免同 id 不同参数被幂等门拒绝);重入已存在字段记 skipped。",
|
|
81
|
-
"
|
|
87
|
+
" --fields-file 从配置根内文件读 JSON 数组;--fields - 从 stdin 读 JSON 数组。",
|
|
82
88
|
].join("\n");
|
|
83
89
|
const invalid = (message) => {
|
|
84
90
|
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
|
|
@@ -88,11 +94,23 @@ export async function cmdModelScaffold(args) {
|
|
|
88
94
|
return invalid("--code 必填");
|
|
89
95
|
if (!name)
|
|
90
96
|
return invalid("--name 必填");
|
|
91
|
-
if (
|
|
92
|
-
return invalid("--fields
|
|
97
|
+
if (fieldsFile !== undefined && (fieldsFlag !== undefined || flag("--fields-json") !== undefined))
|
|
98
|
+
return invalid("--fields-file 不能与 --fields/--fields-json 同时使用");
|
|
99
|
+
let inputJson = fieldsJson;
|
|
100
|
+
if (fieldsFile !== undefined) {
|
|
101
|
+
try {
|
|
102
|
+
const root = resolve(process.env.OMC_CONFIG_HOME ?? process.cwd());
|
|
103
|
+
inputJson = readFileSync(assertSafeInputPath(root, fieldsFile, "--fields-file"), "utf8");
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
return invalid(`--fields-file 读取失败:${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (inputJson === undefined)
|
|
110
|
+
return invalid("--fields 必填(JSON 数组;或使用 --fields-file/--fields -)");
|
|
93
111
|
let fields;
|
|
94
112
|
try {
|
|
95
|
-
const parsed = JSON.parse(
|
|
113
|
+
const parsed = JSON.parse(inputJson);
|
|
96
114
|
if (!Array.isArray(parsed))
|
|
97
115
|
throw new Error("--fields 必须是 JSON 数组");
|
|
98
116
|
fields = parsed;
|
|
@@ -161,7 +179,7 @@ export async function cmdModelScaffold(args) {
|
|
|
161
179
|
status: "dry-run",
|
|
162
180
|
...plan,
|
|
163
181
|
preflight: { checked: fields.length, passed: true },
|
|
164
|
-
note: "dry-run 只列计划;已对每个字段跑过本地形状预检(零 wire
|
|
182
|
+
note: "dry-run 只列计划;已对每个字段跑过本地形状预检(零 wire)。dry-run 只校参数与计划,不代表运行时可达/规则健康(模型可见性、规则节点运行时异常都不在探测面)。--apply 依次执行 建模型 → 建字段 → 建子列 → 发布字段 → 发布表单。",
|
|
165
183
|
}, request));
|
|
166
184
|
return CLI_EXIT_CODES.ok;
|
|
167
185
|
}
|
package/dist/read.js
CHANGED
|
@@ -59,7 +59,11 @@ export async function governedRead(ctx, tool, args, execute, options = {}) {
|
|
|
59
59
|
// 2026-11-01 预算批 v2(r38 usage 实证):1-4KB 中结果 59 次/135KB≈37% 全内联,
|
|
60
60
|
// model.inspect 17 次全部卡在旧门下。降到 2048;compact 对配置清单保形为码表
|
|
61
61
|
// (CODE_LIST_KEYS),完整正文仍在 detailArtifactId。
|
|
62
|
-
|
|
62
|
+
// r44fb: keep small row sets in the first envelope even when each row has
|
|
63
|
+
// useful detail (data.list items or rulelog nodes).
|
|
64
|
+
const smallRowSet = (Array.isArray(result.items) && result.items.length <= 10)
|
|
65
|
+
|| (Array.isArray(result.logs) && result.logs.length <= 10);
|
|
66
|
+
const large = !smallRowSet && jsonBytes(result) > 2048;
|
|
63
67
|
const fileName = `${tool.name.replace(/[^a-zA-Z0-9._-]/g, "_")}-${sha256(target).slice(0, 16)}.json`;
|
|
64
68
|
const artifactRef = writeArtifact(ctx, fileName, detail);
|
|
65
69
|
const cacheEntry = { environmentFingerprint, targetFingerprint, detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256 };
|
|
@@ -68,7 +72,7 @@ export async function governedRead(ctx, tool, args, execute, options = {}) {
|
|
|
68
72
|
// Small raw bodies keep the old inline behaviour; large ones are dieted
|
|
69
73
|
// (see FULL_INLINE_CEILING_BYTES). The raw body stays fully reachable via
|
|
70
74
|
// the artifact, so no capability is lost — only the inline copy is skipped.
|
|
71
|
-
if (rawBytes <= FULL_INLINE_CEILING_BYTES) {
|
|
75
|
+
if (rawBytes <= FULL_INLINE_CEILING_BYTES || smallRowSet || rawBytes <= 32768) {
|
|
72
76
|
ctx.readbackCache.set(target, cacheEntry);
|
|
73
77
|
return { ...detail, detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256 };
|
|
74
78
|
}
|
package/dist/recipes/field.js
CHANGED
|
@@ -357,7 +357,7 @@ const externalLookupModel = Object.freeze({
|
|
|
357
357
|
schemaCode: "<映射模型code>",
|
|
358
358
|
queryCode: "<映射模型code>",
|
|
359
359
|
displayField: "<映射模型显示字段>",
|
|
360
|
-
mappings: [{ source: "
|
|
360
|
+
mappings: [{ source: "<目标模型字段>", target: "<本模型字段>" }],
|
|
361
361
|
},
|
|
362
362
|
},
|
|
363
363
|
preconditions: [
|
|
@@ -370,7 +370,7 @@ const externalLookupModel = Object.freeze({
|
|
|
370
370
|
"下拉框形态在 form.draft 控件层配置;映射表要有数据下拉才有选项。",
|
|
371
371
|
],
|
|
372
372
|
priorToolReferences: [
|
|
373
|
-
{ tool: "model.create", args: { environment }, purpose: "
|
|
373
|
+
{ tool: "model.create", args: { environment }, purpose: "建映射模型(字段与接口返回一一对应;mappings 的 source/target 只填裸字段 code,不带模型前缀)" },
|
|
374
374
|
{ tool: "rule.save", args: { environment }, purpose: "数据段规则(三选一,见 conditionalRequirements)" },
|
|
375
375
|
],
|
|
376
376
|
dryRun: { supported: true, command: `omc call field.create --env ${environment} --dry-run --args '<recipe.args>'`, notes: "校验 WORK_SHEET options 与 mappings 形状" },
|
|
@@ -27,6 +27,19 @@ const onlinejsHighlightScript = Object.freeze({
|
|
|
27
27
|
requiresEnvironmentDerivedIds: false,
|
|
28
28
|
});
|
|
29
29
|
export const ONLINEJS_METADATA = Object.freeze({
|
|
30
|
-
"onlinejs.compose": Object.freeze({ recipes: [onlinejsHighlightScript
|
|
30
|
+
"onlinejs.compose": Object.freeze({ recipes: [onlinejsHighlightScript, Object.freeze({
|
|
31
|
+
id: "current-user-dept",
|
|
32
|
+
title: "读取当前用户与部门成员(门户已实证请求)",
|
|
33
|
+
status: "offline-valid",
|
|
34
|
+
tool: "onlinejs.compose",
|
|
35
|
+
args: { environment, schemaCode: model, sheetCode: "<默认表单 code == 模型 code>", replace: true, source: "(function(form){ h3form.axios.get(config.apiHost + '/api/organization/user/info', {params:{id:'<userId>'}}).then(function(res){ var dept = {id:res.data.departmentId, name:res.data.departmentName}; return h3form.axios.get(config.apiHost + '/api/organization/department/users', {params:{deptId:dept.id,page:0,size:300,filterType:'admin'}}); }).then(function(res){ var members = res.data.content || []; }); })" },
|
|
36
|
+
preconditions: ["门户取证:GET /api/organization/user/info 回 data.departmentId/departmentName;GET /api/organization/department/users 回 data.content,成员含 id/name"],
|
|
37
|
+
conditionalRequirements: ["这是已实证的门户请求形状;userId、schemaCode、字段使用前替换为真实值", "脚本含引号/较长时用 --args-file"],
|
|
38
|
+
refusalRecovery: [{ code: "onlinejs-source-invalid", action: "完整外壳改 `replace:true`(六元素模板保留,仅整体替换 customScript)" }],
|
|
39
|
+
priorToolReferences: [{ tool: "form.get", args: { environment, schemaCode: model }, purpose: "确认表单已发布与控件编码" }],
|
|
40
|
+
dryRun: { supported: true, command: `omc call onlinejs.compose --env ${environment} --dry-run --args-file compose.json`, notes: "dry-run 校验外壳/合并模式,不写平台" },
|
|
41
|
+
expectedReadback: { readback: { deliveryState: "DELIVERED", published: true, scriptElementPresent: true } },
|
|
42
|
+
requiresEnvironmentDerivedIds: false,
|
|
43
|
+
})], relatedTools: ["form.get", "form.draft", "form.publish"] }),
|
|
31
44
|
});
|
|
32
45
|
//# sourceMappingURL=online-js.js.map
|
package/dist/residue.js
CHANGED
|
@@ -108,10 +108,14 @@ export async function scanResidues(session, prefix, options = {}) {
|
|
|
108
108
|
const hasModelResidue = modelFace.some((item) => item.count > 0);
|
|
109
109
|
const hasOtherResidue = faces.some((item) => item.face !== "model" && item.count > 0);
|
|
110
110
|
const pendingCascade = !hasModelResidue && hasOtherResidue;
|
|
111
|
+
const nonZeroFaces = faces.filter((item) => item.count > 0);
|
|
112
|
+
const compact = options.allFaces === true ? faces : nonZeroFaces;
|
|
111
113
|
return {
|
|
112
114
|
prefix,
|
|
113
|
-
faces,
|
|
115
|
+
faces: compact,
|
|
116
|
+
...(options.allFaces === true ? {} : { byFace: compact }),
|
|
114
117
|
totalResidues,
|
|
118
|
+
userResidues,
|
|
115
119
|
clean: userResidues === 0 && !pendingCascade,
|
|
116
120
|
...(systemPresetCount > 0 ? { systemPresetCount } : {}),
|
|
117
121
|
...(pendingCascade ? { pendingCascade: true } : {}),
|
|
@@ -124,7 +128,7 @@ export async function cmdResidue(prefix, options, transport) {
|
|
|
124
128
|
try {
|
|
125
129
|
const context = makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
126
130
|
const session = await context.sessions.session(options.env);
|
|
127
|
-
return emitResult(await scanResidues(session, prefix, { apps: options.apps, deep: options.noDeep !== true }), transport);
|
|
131
|
+
return emitResult(await scanResidues(session, prefix, { apps: options.apps, deep: options.noDeep !== true, allFaces: options.allFaces }), transport);
|
|
128
132
|
}
|
|
129
133
|
catch (error) {
|
|
130
134
|
return emitResult({ status: "error", blocker: "residue-scan-failed", message: error instanceof Error ? error.message : String(error) }, transport);
|
package/dist/rule-support.js
CHANGED
|
@@ -968,6 +968,10 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
968
968
|
? "保存≠生效:本次只写了 DRAFT。规则虽 enabled:true,运行时仍跑上一版已发布图——请再 rule.enable(内部 publish + 翻 DRAFT 启用)或 rule.save 传 publish:true 后再 rule.enable,用 data.submit/data.load 核对新图副作用"
|
|
969
969
|
: "保存≠生效:本次只写了 DRAFT。rule.enable(businessrule/publish 出新版本 + 翻 DRAFT 启用开关)后规则才会分发;或 rule.save 传 publish:true 只发布(启用仍走 rule.enable)";
|
|
970
970
|
const currentRow = async () => wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } })).data).find((item) => item.code === args.ruleCode);
|
|
971
|
+
// Keep warnings tied to the submitted graph; old node-specific warnings are
|
|
972
|
+
// stale inventory noise after an upsert replaces the graph.
|
|
973
|
+
const graphNodeCodes = new Set(nodes.map((node) => String(node.nodeCode ?? "")).filter(Boolean));
|
|
974
|
+
const currentLintWarnings = lintWarnings.filter((warning) => warning.nodeCode === undefined || graphNodeCodes.has(warning.nodeCode) || warning.code === "rule-graph-system-node-tool-owned");
|
|
971
975
|
if (args.publish === true) {
|
|
972
976
|
const row = await currentRow();
|
|
973
977
|
if (row?.id) {
|
|
@@ -1003,6 +1007,9 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
1003
1007
|
// hardcoded true made a draft rule look live and a workflow end-event
|
|
1004
1008
|
// binding on it was silently dead. Liveness itself is `rule.enable`.
|
|
1005
1009
|
const published = effective?.status === "ENABLED" || effective?.latestPublished === true || effective?.templateType === "PUBLISH";
|
|
1010
|
+
const effectiveNow = published && enabled;
|
|
1011
|
+
const effectiveBasis = effectiveNow ? "本次图已发布且规则 enabled:true,当前运行面可用" : "本次图未同时满足 publish + enabled;仍需 rule.enable 才是当前运行图";
|
|
1012
|
+
const previousRuleVersion = existing?.ruleVersion;
|
|
1006
1013
|
return {
|
|
1007
1014
|
performed: { errcode: performed.errcode, mode: existing?.id ? "update" : "create", publishRequested: true, ...(bizMethodChecks > 0 ? { bizMethodChecks } : {}) },
|
|
1008
1015
|
readback: {
|
|
@@ -1010,7 +1017,7 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
1010
1017
|
published,
|
|
1011
1018
|
enabled,
|
|
1012
1019
|
status: effective?.status,
|
|
1013
|
-
...(
|
|
1020
|
+
...(currentLintWarnings.length > 0 ? { lintWarnings: currentLintWarnings } : {}),
|
|
1014
1021
|
...(lintHazards.length > 0 ? { lintHazards } : {}),
|
|
1015
1022
|
customTrigger: customTriggerApplied,
|
|
1016
1023
|
// publish only mints the PUBLISH row; the runtime gate
|
|
@@ -1024,6 +1031,9 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
1024
1031
|
...(customTriggerApplied ? {} : { customTriggerHint: "图中没有 CUSTOM_NODE 触发节点:门户按钮/流程按钮调用该规则时会按各自的触发面执行;rule.run 也能执行(它不要求 CUSTOM_NODE),执行时以 BUTTON 触发类型跑同一张图" }),
|
|
1025
1032
|
id: effective?.id ?? undefined,
|
|
1026
1033
|
ruleVersion: effective?.ruleVersion,
|
|
1034
|
+
previousRuleVersion,
|
|
1035
|
+
effectiveNow,
|
|
1036
|
+
effectiveBasis,
|
|
1027
1037
|
latestPublished: effective?.latestPublished,
|
|
1028
1038
|
...(existing?.id ? { versionNote } : {}),
|
|
1029
1039
|
...(enabled ? {} : { nextAction: "rule.enable(businessrule/publish + businessrule/enable 两步)后规则才会分发并对流程/按钮可见;publish:true 只写入版本" }),
|
|
@@ -1031,6 +1041,7 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
1031
1041
|
};
|
|
1032
1042
|
}
|
|
1033
1043
|
const effective = await currentRow();
|
|
1044
|
+
const previousRuleVersion = existing?.ruleVersion;
|
|
1034
1045
|
return {
|
|
1035
1046
|
performed: { errcode: performed.errcode, mode: existing?.id ? "update" : "create", ...(bizMethodChecks > 0 ? { bizMethodChecks } : {}) },
|
|
1036
1047
|
readback: {
|
|
@@ -1042,12 +1053,15 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
1042
1053
|
effective: false,
|
|
1043
1054
|
requiresPublishAction: "rule.enable",
|
|
1044
1055
|
...(existing?.defaultRule === true || PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode) ? { presetRule: true } : {}),
|
|
1045
|
-
...(
|
|
1056
|
+
...(currentLintWarnings.length > 0 ? { lintWarnings: currentLintWarnings } : {}),
|
|
1046
1057
|
...(lintHazards.length > 0 ? { lintHazards } : {}),
|
|
1047
1058
|
customTrigger: customTriggerApplied,
|
|
1048
1059
|
...(customTriggerApplied ? {} : { customTriggerHint: "图中没有 CUSTOM_NODE 触发节点:门户按钮/流程按钮调用该规则时会按各自的触发面执行;rule.run 也能执行(它不要求 CUSTOM_NODE),执行时以 BUTTON 触发类型跑同一张图" }),
|
|
1049
1060
|
id: effective?.id ?? undefined,
|
|
1050
1061
|
ruleVersion: effective?.ruleVersion,
|
|
1062
|
+
previousRuleVersion,
|
|
1063
|
+
effectiveNow: false,
|
|
1064
|
+
effectiveBasis: "本次只写 DRAFT,未发布;rule.enable(或 publish:true 后再 enable)才会让本次图成为当前运行图",
|
|
1051
1065
|
latestPublished: effective?.latestPublished,
|
|
1052
1066
|
...(existing?.id ? { versionNote } : {}),
|
|
1053
1067
|
nextAction: saveNotEffectiveNextAction,
|
package/dist/write.js
CHANGED
|
@@ -93,6 +93,17 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload, readOnlyEs
|
|
|
93
93
|
if (error instanceof ToolRefusalError)
|
|
94
94
|
throw error;
|
|
95
95
|
if (error instanceof WireError) {
|
|
96
|
+
// F3: rule-node runtime failures are masked by the platform as 300004.
|
|
97
|
+
if (error.errcode === 300004) {
|
|
98
|
+
return {
|
|
99
|
+
status: "refused",
|
|
100
|
+
blocker: `wire-${error.kind}`,
|
|
101
|
+
errcode: 300004,
|
|
102
|
+
message: `${error.message}——若 modelPresent:true 且 form 已发布,这个 300004 大概率是业务规则节点运行时异常,不是模型缺失;先查 rulelog 的 EXCEPTION/ROLLBACK,再看最近改动的规则。`,
|
|
103
|
+
nextCommand: "omc help omc-refusals",
|
|
104
|
+
diagnostic: "omc call rulelog.search --args '{\"schemaCode\":\"<model>\",\"includeNodes\":true}';再用 rule.get 检查最近改动规则,隔离法是暂时删/禁规则后重试",
|
|
105
|
+
};
|
|
106
|
+
}
|
|
96
107
|
// authine-orm Serializer.update/delete throws "Object <id> is not
|
|
97
108
|
// serialized." when the entity handle was never loaded from the store.
|
|
98
109
|
// That is a PLATFORM object-state error, not a shape error — reported as
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.25",
|
|
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.25",
|
|
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.25"
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -31,8 +31,8 @@ only the section for the capability you need —
|
|
|
31
31
|
| 字段 `field` | 字段族 + 子表 + **计算字段** | 字段、属性、子表、明细、下拉、单选、多选、附件、人员、部门、日期、数值、关联、**公式/计算/求和/汇总/合计/回主表** | [字段](#字段-field) |
|
|
32
32
|
| 默认表单 `form` | 表单布局与发布 | 表单、页面、布局、控件、只读、显示 | [默认表单](#默认表单-form) |
|
|
33
33
|
| 列表视图 `listview` | 列表表头与列 | 列表、视图、查询列、筛选、表头、甘特 | [列表视图](#列表视图-listview) |
|
|
34
|
-
|
|
|
35
|
-
| 业务规则 `businessrule` | 图形规则图 + **定时任务** | 业务规则、触发器、规则图、事件、BRANCH、BIZ_ACTION
|
|
34
|
+
| 字段级引擎规则(非表单控件) `datarule` | 字段级引擎读面 | 字段级规则、校验规则、计算拼接、显示规则、条件必填/只读/显示 | [字段级规则](#字段级规则-datarule) |
|
|
35
|
+
| 业务规则 `businessrule` | 图形规则图 + **定时任务** + 审计 | 业务规则、触发器、规则图、事件、BRANCH、BIZ_ACTION、**定时任务/定时/每天/周期/调度**、触发日志审计 | [业务规则](#业务规则-businessrule) |
|
|
36
36
|
| 规则生命周期 `rule-lifecycle` | 规则启停删 | 启用规则、停用规则、删除规则、规则启停 | [规则生命周期](#规则生命周期-rule-lifecycle) |
|
|
37
37
|
| 流程模板 `workflow` | 审批流程模板 | 流程、工作流、审批、节点、待办、流转、参与人、审批人、表单操作按钮 | [流程模板](#流程模板-workflow) |
|
|
38
38
|
| 表单在线 JS `onlinejs` | 表单脚本发布/回读 | 在线js、脚本、联动、onload、onvalidate | [表单在线 JS](#表单在线-js-onlinejs) |
|
|
@@ -121,6 +121,8 @@ only the section for the capability you need —
|
|
|
121
121
|
|
|
122
122
|
## 字段级规则 `datarule`
|
|
123
123
|
|
|
124
|
+
> **字段级引擎规则(非表单控件)**:表单条件必填/显示/只读走 `field.update options`;本面是设计器引擎读面。
|
|
125
|
+
|
|
124
126
|
- **能做哪些事**:字段级数据规则(bizdatarule)create/update/delete;条件必填(type4)、条件只读(type5)、条件显示(type3)、计算拼接(type2)等,按平台校验器闭环。
|
|
125
127
|
- **工具**:`datarule.list`、`datarule.create`、`datarule.update`、`datarule.delete`
|
|
126
128
|
- **recipe**:
|
|
@@ -132,7 +134,7 @@ only the section for the capability you need —
|
|
|
132
134
|
## 业务规则 `businessrule`
|
|
133
135
|
|
|
134
136
|
- **能做哪些事**:图形规则图(nodeGraph)——数据事件(Create/Update/Delete)触发、按钮/流程触发、动作节点(UPDATE/CREATE/DELETE/QUERY/ASSIGN/MESSAGE/BIZ_ACTION/GET_LIST…)、BRANCH 条件分支、**定时任务 TIMING_TASK**(每天/周期,SCHEDULER 节点)。
|
|
135
|
-
- **工具**:`rule.list`、`rule.get`、`rule.references`(跨规则引用检索:谁引用了服务/模型/字段)、`rule.save`、`rule.node.update`、`rulelog.search
|
|
137
|
+
- **工具**:`rule.list`、`rule.get`、`rule.references`(跨规则引用检索:谁引用了服务/模型/字段)、`rule.save`、`rule.node.update`、`rulelog.search`(**规则执行日志审计入口**:规则触发没触发/哪行/节点链——data.update 后查 F31)
|
|
136
138
|
- **recipe**:
|
|
137
139
|
- `omc describe rule.save --example standard-business-rule-graph`
|
|
138
140
|
- `omc describe rule.save --example business-rule-branch-conditional`
|
|
@@ -160,6 +162,7 @@ only the section for the capability you need —
|
|
|
160
162
|
|
|
161
163
|
- **能做哪些事**:表单在线 JS 的 compose 六元素骨架、`draftHtmlJson.customScript`、发布与 SHA-256 回读;**读现存脚本**(`onlinejs.get` 抽 customScript innerHTML 原文+SHA-256,不必拉 form.get --full 六份 JSON)(脚本**编写**不在 CLI 范围,遵循项目在线 JS 规范或向用户索取定稿 source)。
|
|
162
164
|
- **工具**:`onlinejs.get`、`onlinejs.compose`
|
|
165
|
+
- **常用运行时 API(已实证)**:门户可用 `form.getControl(code)`、`form.on('onRendered', fn)` 与控件 `valueChange.subscribe`;当前用户/部门取证走 `h3form.axios.get(config.apiHost + '/api/organization/user/info', {params:{id:userId}})`,响应字段含 `data.departmentId`/`data.departmentName`;部门成员走 `/api/organization/department/users`,列表在 `data.content`,成员含 `id`/`name`。配方:`omc describe onlinejs.compose --example current-user-dept`。
|
|
163
166
|
- **边界 / 陷阱**:仅表单在线 JS;后端在线开发(`/api/api/programming` Java 源码)是独立 `programming` lane(`programming.get/save/compile/publish`),与在线 JS 不互换。INCREMENTAL 合并对同内容重发幂等;同头注释的整文件修订会被拒(`onlinejs-revision-replace-required`),走 `onlinejs.get` 取回 → 改 → `replace:true`。
|
|
164
167
|
|
|
165
168
|
## 后端在线开发 `programming`
|
package/skills/omc-playbook.md
CHANGED
|
@@ -11,16 +11,13 @@ description: 一页纸业务场景步骤清单(采购申请 / 请假审批 /
|
|
|
11
11
|
|
|
12
12
|
## 采购申请(主子表 + 合计 + 条件审批 + 列表 + 按钮)
|
|
13
13
|
|
|
14
|
-
1. `omc next "从零做采购申请:主子表、合计、条件审批、列表、按钮"`
|
|
15
|
-
2. `omc
|
|
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。
|
|
14
|
+
1. `omc next "从零做采购申请:主子表、合计、条件审批、列表、按钮"` → `omc intent schema`
|
|
15
|
+
2. `omc model-scaffold --app <app> --code <code> --name 采购申请 --fields '<JSON数组>' --apply`(字段含 `amount` NUMERICAL scale=2、`<主表code>_details` CHILD_TABLE 及 item/qty/price/lineAmount 子列;子表 code 必须全局唯一)
|
|
18
16
|
4. `omc compute --schema <code> --target amount --formula 'SUM({<主表code>_details.lineAmount})' --apply`(⚠️ 只在门户算,REST 不回填)
|
|
19
17
|
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`。
|
|
20
18
|
6. 发起权限:`omc call workflow.addPermission --args '{"workflowCode":"<code>_wf","unitId":"<deptOrRoleId>","unitType":1}'`(这是**谁能发起**,不是行级可见范围)
|
|
21
|
-
7. 列表:`omc
|
|
22
|
-
|
|
23
|
-
8. 表单按钮:`omc call listview.configure --args '{"schemaCode":"<code>","buttons":[{"name":"提交审批","useLocation":"FORM","bindAction":"FLOW"}]}'`(形状以 describe 为准)
|
|
19
|
+
7. 列表:`listview.configure` 写 `fieldCodes`/`conditions`;条件形状以 `omc describe listview.configure --param conditions` 为准。
|
|
20
|
+
8. 按钮:`listview.configure` 写 `buttons:[{name,useLocation:"FORM",bindAction:"FLOW"}]`。
|
|
24
21
|
9. ⚠️ **行级「申请人只看自己」当前工具面不支持**(`workflow.permissions` / `org.role.setUserScope` 都不是行级数据权限)
|
|
25
22
|
|
|
26
23
|
## 请假审批(线性流)
|
|
@@ -36,6 +33,17 @@ description: 一页纸业务场景步骤清单(采购申请 / 请假审批 /
|
|
|
36
33
|
2. `omc call listview.configure --args '{"schemaCode":"vendor","fieldCodes":["name","code"]}'`
|
|
37
34
|
3. 造数:`omc describe data.submit --example plain-data-submit`
|
|
38
35
|
|
|
36
|
+
## 条件表单 + 关联带出
|
|
37
|
+
|
|
38
|
+
1. 条件必填/显示/只读写 `field.update options`(不要用 datarule);子表条件列同法,建在父字段 code 下。
|
|
39
|
+
2. 关联带出映射用**裸 code**(无 `schema.` 前缀);发布链 `field.publish → form.draft rebuild:true → form.publish`,`form.get` 验收。
|
|
40
|
+
|
|
41
|
+
## 数据事件规则(跨模型联动 + 执行日志)
|
|
42
|
+
|
|
43
|
+
1. 联动挂预置 `Create`/`Update`(submit 触发 Create、update 触发 Update)→ `rule.enable`;`data.load` 验收目标行。
|
|
44
|
+
2. 取证:`omc call rulelog.search --args '{"schemaCode":"<模型>","bizObjectId":"<行id>"}'——触发/行/节点链一次问清。
|
|
45
|
+
|
|
46
|
+
|
|
39
47
|
## 读法
|
|
40
48
|
|
|
41
49
|
- 默认索引:`omc help omc-playbook`(看节摘要)
|
package/skills/omc-refusals.md
CHANGED
|
@@ -68,6 +68,13 @@ condition/expression/query shape is off. Compare against
|
|
|
68
68
|
Escalate to 二开 **only** for a real `capability-out-of-coverage` (§2).
|
|
69
69
|
|
|
70
70
|
`40000 调用数据库发生异常` is the **most opaque** shape code — no detail at all.
|
|
71
|
+
|
|
72
|
+
### 3d. 300004 during form/submit can be a rule-node failure
|
|
73
|
+
|
|
74
|
+
Model present + form published yet submit says `300004 数据模型不存在`? Often a
|
|
75
|
+
rule-node exception in disguise (r44b): `rulelog.search {schemaCode,includeNodes:true}`
|
|
76
|
+
→ `EXCEPTION`/`ROLLBACK` nodes → `rule.get` the changed rule; disable/delete it to
|
|
77
|
+
isolate. Never recreate a healthy model.
|
|
71
78
|
Line up the model's field-level config first: an `AGGREGATION` FORMULA field whose
|
|
72
79
|
`aggregation.schemaCode` points at a **child-table schema** publishes fine and then
|
|
73
80
|
makes every `data.submit`/`data.update` on that model 40000 (r18 2026-10-26 live;
|