@stalfh233/omc-cli 0.6.24 → 0.6.26
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 +5 -2
- package/dist/cli.js +7 -3
- package/dist/field-add.js +23 -5
- package/dist/field-families.js +6 -2
- package/dist/form-support.js +13 -0
- package/dist/formula-lint.js +3 -0
- package/dist/goal-routes.js +2 -2
- package/dist/lanes/businessrule-contract.js +2 -0
- package/dist/lanes/businessrule.js +1 -0
- package/dist/lanes/datarule.js +15 -3
- package/dist/lanes/field.js +83 -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 +7 -4
- package/dist/lanes/rule-graph.js +16 -0
- package/dist/lanes/rulelog.js +1 -1
- package/dist/lanes/workflow.js +78 -4
- package/dist/model-scaffold.js +25 -7
- package/dist/read.js +6 -2
- package/dist/recipes/businessrule.js +1 -0
- package/dist/recipes/field.js +3 -3
- package/dist/recipes/online-js.js +14 -1
- package/dist/residue.js +6 -2
- package/dist/rule-support.js +27 -2
- package/dist/skills.js +2 -0
- package/dist/write.js +11 -0
- package/package.json +3 -3
- package/skills/omc-capabilities.md +6 -3
- package/skills/omc-playbook.md +16 -7
- package/skills/omc-refusals.md +7 -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 —
|
|
@@ -829,7 +878,15 @@ async function assertRoleParticipantScopes(session, body) {
|
|
|
829
878
|
if (String(activity.activityType ?? "") !== "PARTICIPANT")
|
|
830
879
|
continue;
|
|
831
880
|
const participant = typeof activity.participant === "string" ? activity.participant.trim() : "";
|
|
832
|
-
|
|
881
|
+
if (/^FindUserByRole\s*\(/i.test(participant)) {
|
|
882
|
+
const strict = /^FindUserByRole\s*\(\s*\{Originator\.OU\}\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*\)\s*$/i.exec(participant);
|
|
883
|
+
if (!strict)
|
|
884
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-find-user-by-role-parameters-invalid", activityCode: String(activity.activityCode ?? ""), nextCommand: "使用 FindUserByRole({Originator.OU},\"<真实角色code>\",\"random\");组织范围不得手填 id", message: `节点 ${String(activity.activityCode ?? "")} 的 FindUserByRole 参数不合法:x 必须是单一 {Originator.OU},不得手填 id;y 必须是带引号的单一角色 code。` });
|
|
885
|
+
const order = strict[2].slice(1, -1);
|
|
886
|
+
if (!new Set(["dept", "sortKey", "random"]).has(order))
|
|
887
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-find-user-by-role-order-invalid", activityCode: String(activity.activityCode ?? ""), order, candidates: ["dept", "sortKey", "random"], nextCommand: "将第三参数改为 dept、sortKey 或 random", message: `FindUserByRole 第三参数 ${order} 不在允许枚举 dept/sortKey/random。` });
|
|
888
|
+
}
|
|
889
|
+
const match = /^FindUserByRole\s*\(\s*\{Originator\.OU\}\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*,\s*("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*')\s*\)\s*$/.exec(participant);
|
|
833
890
|
if (!match)
|
|
834
891
|
continue;
|
|
835
892
|
const quoted = match[1];
|
|
@@ -853,9 +910,7 @@ async function assertRoleParticipantScopes(session, body) {
|
|
|
853
910
|
roleListAvailable = true;
|
|
854
911
|
}
|
|
855
912
|
catch {
|
|
856
|
-
|
|
857
|
-
// Do not turn a transient read failure into a blanket workflow write outage.
|
|
858
|
-
roleListAvailable = false;
|
|
913
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-participant-role-read-failed", nextCommand: "omc call org.role.list --args '{}'", message: "无法读取真实角色树,不能确认 FindUserByRole 的 role code;为避免流程静默回退发起人,已 fail-closed 拒绝写入。请恢复组织读面后重试。" });
|
|
859
914
|
}
|
|
860
915
|
if (roleListAvailable) {
|
|
861
916
|
const byCode = new Map(listedRoles
|
|
@@ -3371,6 +3426,24 @@ export function registerWorkflowLane(tools) {
|
|
|
3371
3426
|
// activity that currently holds tasks. No active activity while the
|
|
3372
3427
|
// instance is still PROCESSING is exactly the silent-stall shape.
|
|
3373
3428
|
const diagnosis = diagnoseInstanceActivity({ instanceStatus, endStatus: header ? wireString(header.endStatus) : null, originator: header ? wireString(header.originator) : null, activities });
|
|
3429
|
+
// The instance face gives the resolved participant, while the
|
|
3430
|
+
// published template gives the configured role. Compare both faces
|
|
3431
|
+
// here (and only here): data.submit wiring intentionally remains in
|
|
3432
|
+
// runtime-data.ts's owning batch. If the template cannot be read,
|
|
3433
|
+
// participantFallbackWarnings degrades to a participant==originator
|
|
3434
|
+
// hint rather than claiming a role mismatch.
|
|
3435
|
+
let publishedTemplate = null;
|
|
3436
|
+
const workflowCode = header ? wireString(header.workflowCode) ?? wireString(header.workflowCode_) : null;
|
|
3437
|
+
if (workflowCode) {
|
|
3438
|
+
try {
|
|
3439
|
+
const published = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode } });
|
|
3440
|
+
publishedTemplate = published.data && typeof published.data === "object" && !Array.isArray(published.data) ? published.data : null;
|
|
3441
|
+
}
|
|
3442
|
+
catch {
|
|
3443
|
+
publishedTemplate = null;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
const participantFallback = participantFallbackWarnings({ activities, template: publishedTemplate, originator: header ? wireString(header.originator) : null });
|
|
3374
3447
|
return {
|
|
3375
3448
|
workflowInstanceId: args.workflowInstanceId,
|
|
3376
3449
|
...(instanceStatus ? { instanceStatus } : {}),
|
|
@@ -3379,6 +3452,7 @@ export function registerWorkflowLane(tools) {
|
|
|
3379
3452
|
activeCount: active.length,
|
|
3380
3453
|
reachedCount: reached.length,
|
|
3381
3454
|
activities,
|
|
3455
|
+
...(participantFallback.length ? { participantFallback } : {}),
|
|
3382
3456
|
diagnosis,
|
|
3383
3457
|
};
|
|
3384
3458
|
}),
|
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
|
}
|
|
@@ -385,6 +385,7 @@ const ruleCrossModelCreate = Object.freeze({
|
|
|
385
385
|
"**`originatorValueType:\"DYNAMIC\"` 的 `originatorValue` 必须指向人员字段**(STAFF_SELECTOR 等);指向文本/编号字段会致发起人错误或为空。",
|
|
386
386
|
"「当前时间」不是规则的 value 类型:`ruleValueType:\"SYSTEM\"` 不会填 now(真机 2026-10-08 落 null)。要让下游日期=生成时刻,把该**日期字段的默认值**设为「当前时间」(平台 `{value:\"current\"}`),或走二开;纯规则映射填不出 now。",
|
|
387
387
|
"上游是 QUERY 时 `inputParam=\"_<QUERY节点code>_output\"`(QUERY 查 N 行→CREATE 逐条生成即 N→N 扇出,真机验证支持,同/跨模型均可);否则 `\"_input\"`。",
|
|
388
|
+
"跨模型 UPDATE 三段:`inputParam`/`currentSchemaDataItem`=输入源(触发行或 QUERY 输出)→ `filterCondition` 查 `targetSchemaCode` 目标行 → `dataActions` 写入。QUERY 目标行非隐式输入源;child fan-out 保持 fail-closed。",
|
|
388
389
|
"`QUERY` 节点只能查**主模型行**:查子表 schema(`parent.child` 或子 schema 名)空条件返回 0 行、带条件 550018(真机 2026-10-08)——逐子表明细扇出才需要二开。",
|
|
389
390
|
"跨模型 CREATE/UPDATE 真机验证支持(2026-09-28 / 2026-10-08);承载规则的触发时机要对(Create 在提交时触发、Update 只在 data.update 触发、TIMING 定时触发)。",
|
|
390
391
|
"**CREATE 目标是已发布流程的模型时**(2026-10-27 真机四格矩阵):不传 startWorkflow → 建行**不发起流程**;要发起必须 startWorkflow:true + targetWorkflowCode:<该流程> + originatorValue 用户对象数组 [{\"id\":\"<userId>\",\"name\":\"<名>\"}](传对象 {} → 触发提交 50000 expect '[';无效 id → 50000 发起人不存在);设计器手工编辑可打开该开关,API 与设计器配置会漂移,rule.get 回读核对。",
|
package/dist/recipes/field.js
CHANGED
|
@@ -107,7 +107,7 @@ const numericTwoDecimals = Object.freeze({
|
|
|
107
107
|
priorToolReferences: [{ tool: "discover_environment", args: { environment }, purpose: "确认目标模型已存在" }],
|
|
108
108
|
dryRun: { supported: true, command: `omc call field.create --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 校验 propertyType/scale/治理,不创建" },
|
|
109
109
|
expectedReadback: { readback: { draftFieldPresent: true }, next: ["field.publish"] },
|
|
110
|
-
refusalRecovery: [{ code: "
|
|
110
|
+
refusalRecovery: [{ code: "field-options-shape", action: "NUMERICAL 必须带 options.scale;金额两位小数用 scale:2,整数用 scale:0" }, ...commonRecovery],
|
|
111
111
|
requiresEnvironmentDerivedIds: false,
|
|
112
112
|
});
|
|
113
113
|
const dictionarySelection = Object.freeze({
|
|
@@ -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
|
@@ -6,6 +6,7 @@ import { ToolRefusalError } from "./tool-types.js";
|
|
|
6
6
|
import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "./json-column.js";
|
|
7
7
|
import { RULE_BIZ_RULE_TYPE_DESCRIPTION, RULE_CREATE_BODY, RULE_ROUTES_DESCRIPTION, ruleNodeGraphDescription, schedulerSettingDescription, } from "./lanes/businessrule-contract.js";
|
|
8
8
|
import { validateRuleNodeGraph, prepareRuleGraph, PLATFORM_TRIGGER_RULE_CODES, DEFAULT_SYSTEM_NODE_BY_RULE_CODE } from "./lanes/rule-graph.js";
|
|
9
|
+
import { FORMULA_LINT_CODES } from "./formula-lint.js";
|
|
9
10
|
import { compileMethodConfigPlain } from "./bizservice-config.js";
|
|
10
11
|
import { RESERVED_CODES } from "./code-rules.js";
|
|
11
12
|
import { FIELD_PROPERTY_TYPES } from "./field-families.js";
|
|
@@ -757,6 +758,16 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
757
758
|
// plain advisory.
|
|
758
759
|
const lintWarnings = warningFindings.filter((blocker) => blocker.hazard !== true);
|
|
759
760
|
const lintHazards = warningFindings.filter((blocker) => blocker.hazard === true);
|
|
761
|
+
const formulaWarnings = lintWarnings.filter((finding) => FORMULA_LINT_CODES.has(finding.code));
|
|
762
|
+
if (formulaWarnings.length > 0 && args.allowAdvisory !== true) {
|
|
763
|
+
throw new ToolRefusalError({
|
|
764
|
+
status: "refused",
|
|
765
|
+
blocker: "formula-lint-failed",
|
|
766
|
+
validation: formulaWarnings,
|
|
767
|
+
nextCommand: "将未知函数改为平台注册函数(文本拼接使用 STRING),仅调查时显式传 allowAdvisory:true",
|
|
768
|
+
message: "公式写前校验拒绝:发现未知函数、平台别名或字符串 + 拼接。请修正公式;调查用途可显式 allowAdvisory:true(默认 fail-closed)。",
|
|
769
|
+
});
|
|
770
|
+
}
|
|
760
771
|
if (blockers.some((blocker) => blocker.severity !== "warning")) {
|
|
761
772
|
throw new ToolRefusalError({
|
|
762
773
|
status: "refused",
|
|
@@ -968,6 +979,10 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
968
979
|
? "保存≠生效:本次只写了 DRAFT。规则虽 enabled:true,运行时仍跑上一版已发布图——请再 rule.enable(内部 publish + 翻 DRAFT 启用)或 rule.save 传 publish:true 后再 rule.enable,用 data.submit/data.load 核对新图副作用"
|
|
969
980
|
: "保存≠生效:本次只写了 DRAFT。rule.enable(businessrule/publish 出新版本 + 翻 DRAFT 启用开关)后规则才会分发;或 rule.save 传 publish:true 只发布(启用仍走 rule.enable)";
|
|
970
981
|
const currentRow = async () => wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } })).data).find((item) => item.code === args.ruleCode);
|
|
982
|
+
// Keep warnings tied to the submitted graph; old node-specific warnings are
|
|
983
|
+
// stale inventory noise after an upsert replaces the graph.
|
|
984
|
+
const graphNodeCodes = new Set(nodes.map((node) => String(node.nodeCode ?? "")).filter(Boolean));
|
|
985
|
+
const currentLintWarnings = lintWarnings.filter((warning) => warning.nodeCode === undefined || graphNodeCodes.has(warning.nodeCode) || warning.code === "rule-graph-system-node-tool-owned");
|
|
971
986
|
if (args.publish === true) {
|
|
972
987
|
const row = await currentRow();
|
|
973
988
|
if (row?.id) {
|
|
@@ -1003,6 +1018,9 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
1003
1018
|
// hardcoded true made a draft rule look live and a workflow end-event
|
|
1004
1019
|
// binding on it was silently dead. Liveness itself is `rule.enable`.
|
|
1005
1020
|
const published = effective?.status === "ENABLED" || effective?.latestPublished === true || effective?.templateType === "PUBLISH";
|
|
1021
|
+
const effectiveNow = published && enabled;
|
|
1022
|
+
const effectiveBasis = effectiveNow ? "本次图已发布且规则 enabled:true,当前运行面可用" : "本次图未同时满足 publish + enabled;仍需 rule.enable 才是当前运行图";
|
|
1023
|
+
const previousRuleVersion = existing?.ruleVersion;
|
|
1006
1024
|
return {
|
|
1007
1025
|
performed: { errcode: performed.errcode, mode: existing?.id ? "update" : "create", publishRequested: true, ...(bizMethodChecks > 0 ? { bizMethodChecks } : {}) },
|
|
1008
1026
|
readback: {
|
|
@@ -1010,7 +1028,7 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
1010
1028
|
published,
|
|
1011
1029
|
enabled,
|
|
1012
1030
|
status: effective?.status,
|
|
1013
|
-
...(
|
|
1031
|
+
...(currentLintWarnings.length > 0 ? { lintWarnings: currentLintWarnings } : {}),
|
|
1014
1032
|
...(lintHazards.length > 0 ? { lintHazards } : {}),
|
|
1015
1033
|
customTrigger: customTriggerApplied,
|
|
1016
1034
|
// publish only mints the PUBLISH row; the runtime gate
|
|
@@ -1024,6 +1042,9 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
1024
1042
|
...(customTriggerApplied ? {} : { customTriggerHint: "图中没有 CUSTOM_NODE 触发节点:门户按钮/流程按钮调用该规则时会按各自的触发面执行;rule.run 也能执行(它不要求 CUSTOM_NODE),执行时以 BUTTON 触发类型跑同一张图" }),
|
|
1025
1043
|
id: effective?.id ?? undefined,
|
|
1026
1044
|
ruleVersion: effective?.ruleVersion,
|
|
1045
|
+
previousRuleVersion,
|
|
1046
|
+
effectiveNow,
|
|
1047
|
+
effectiveBasis,
|
|
1027
1048
|
latestPublished: effective?.latestPublished,
|
|
1028
1049
|
...(existing?.id ? { versionNote } : {}),
|
|
1029
1050
|
...(enabled ? {} : { nextAction: "rule.enable(businessrule/publish + businessrule/enable 两步)后规则才会分发并对流程/按钮可见;publish:true 只写入版本" }),
|
|
@@ -1031,6 +1052,7 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
1031
1052
|
};
|
|
1032
1053
|
}
|
|
1033
1054
|
const effective = await currentRow();
|
|
1055
|
+
const previousRuleVersion = existing?.ruleVersion;
|
|
1034
1056
|
return {
|
|
1035
1057
|
performed: { errcode: performed.errcode, mode: existing?.id ? "update" : "create", ...(bizMethodChecks > 0 ? { bizMethodChecks } : {}) },
|
|
1036
1058
|
readback: {
|
|
@@ -1042,12 +1064,15 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
1042
1064
|
effective: false,
|
|
1043
1065
|
requiresPublishAction: "rule.enable",
|
|
1044
1066
|
...(existing?.defaultRule === true || PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode) ? { presetRule: true } : {}),
|
|
1045
|
-
...(
|
|
1067
|
+
...(currentLintWarnings.length > 0 ? { lintWarnings: currentLintWarnings } : {}),
|
|
1046
1068
|
...(lintHazards.length > 0 ? { lintHazards } : {}),
|
|
1047
1069
|
customTrigger: customTriggerApplied,
|
|
1048
1070
|
...(customTriggerApplied ? {} : { customTriggerHint: "图中没有 CUSTOM_NODE 触发节点:门户按钮/流程按钮调用该规则时会按各自的触发面执行;rule.run 也能执行(它不要求 CUSTOM_NODE),执行时以 BUTTON 触发类型跑同一张图" }),
|
|
1049
1071
|
id: effective?.id ?? undefined,
|
|
1050
1072
|
ruleVersion: effective?.ruleVersion,
|
|
1073
|
+
previousRuleVersion,
|
|
1074
|
+
effectiveNow: false,
|
|
1075
|
+
effectiveBasis: "本次只写 DRAFT,未发布;rule.enable(或 publish:true 后再 enable)才会让本次图成为当前运行图",
|
|
1051
1076
|
latestPublished: effective?.latestPublished,
|
|
1052
1077
|
...(existing?.id ? { versionNote } : {}),
|
|
1053
1078
|
nextAction: saveNotEffectiveNextAction,
|
package/dist/skills.js
CHANGED
|
@@ -276,6 +276,8 @@ export function readSkillSection(name, requested) {
|
|
|
276
276
|
const matches = (heading) => {
|
|
277
277
|
if (heading === normalized)
|
|
278
278
|
return true;
|
|
279
|
+
if (heading.replace(/\s+`[^`]+`\s*$/, "").trim() === normalized)
|
|
280
|
+
return true;
|
|
279
281
|
if (heading.replace(/[^\p{L}\p{N}]+/gu, "-").toLowerCase() === normalized)
|
|
280
282
|
return true;
|
|
281
283
|
if (requestedOrdinal !== undefined) {
|
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.26",
|
|
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.26",
|
|
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.26"
|
|
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,14 @@ description: 一页纸业务场景步骤清单(采购申请 / 请假审批 /
|
|
|
11
11
|
|
|
12
12
|
## 采购申请(主子表 + 合计 + 条件审批 + 列表 + 按钮)
|
|
13
13
|
|
|
14
|
-
1. `omc next "从零做采购申请:主子表、合计、条件审批、列表、按钮"`
|
|
15
|
-
2. `omc
|
|
16
|
-
3.
|
|
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 必须全局唯一)
|
|
16
|
+
3. (`model-scaffold` 已在第 2 步完成字段发布与表单发布;无需另起命令)
|
|
18
17
|
4. `omc compute --schema <code> --target amount --formula 'SUM({<主表code>_details.lineAmount})' --apply`(⚠️ 只在门户算,REST 不回填)
|
|
19
18
|
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
19
|
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 为准)
|
|
20
|
+
7. 列表:`listview.configure` 写 `fieldCodes`/`conditions`;条件形状以 `omc describe listview.configure --param conditions` 为准。
|
|
21
|
+
8. 按钮:`listview.configure` 写 `buttons:[{name,useLocation:"FORM",bindAction:"FLOW"}]`。
|
|
24
22
|
9. ⚠️ **行级「申请人只看自己」当前工具面不支持**(`workflow.permissions` / `org.role.setUserScope` 都不是行级数据权限)
|
|
25
23
|
|
|
26
24
|
## 请假审批(线性流)
|
|
@@ -36,6 +34,17 @@ description: 一页纸业务场景步骤清单(采购申请 / 请假审批 /
|
|
|
36
34
|
2. `omc call listview.configure --args '{"schemaCode":"vendor","fieldCodes":["name","code"]}'`
|
|
37
35
|
3. 造数:`omc describe data.submit --example plain-data-submit`
|
|
38
36
|
|
|
37
|
+
## 条件表单 + 关联带出
|
|
38
|
+
|
|
39
|
+
1. 条件必填/显示/只读写 `field.update options`(不要用 datarule);子表条件列同法,建在父字段 code 下。
|
|
40
|
+
2. 关联带出映射用**裸 code**(无 `schema.` 前缀);发布链 `field.publish → form.draft rebuild:true → form.publish`,`form.get` 验收。
|
|
41
|
+
|
|
42
|
+
## 数据事件规则(跨模型联动 + 执行日志)
|
|
43
|
+
|
|
44
|
+
1. 联动挂预置 `Create`/`Update`(submit 触发 Create、update 触发 Update)→ `rule.enable`;`data.load` 验收目标行。
|
|
45
|
+
2. 取证:`omc call rulelog.search --args '{"schemaCode":"<模型>","bizObjectId":"<行id>"}'——触发/行/节点链一次问清。
|
|
46
|
+
|
|
47
|
+
|
|
39
48
|
## 读法
|
|
40
49
|
|
|
41
50
|
- 默认索引:`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;
|