@stalfh233/omc-cli 0.6.25 → 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 +1 -0
- package/dist/cli.js +2 -0
- package/dist/field-add.js +23 -5
- package/dist/field-families.js +2 -2
- package/dist/formula-lint.js +3 -0
- package/dist/lanes/businessrule-contract.js +1 -0
- package/dist/lanes/businessrule.js +1 -0
- package/dist/lanes/datarule.js +9 -0
- package/dist/lanes/field.js +67 -1
- package/dist/lanes/organization.js +4 -1
- package/dist/lanes/workflow.js +10 -4
- package/dist/recipes/businessrule.js +1 -0
- package/dist/recipes/field.js +1 -1
- package/dist/rule-support.js +11 -0
- package/dist/skills.js +2 -0
- package/package.json +3 -3
- package/skills/omc-playbook.md +1 -0
|
@@ -349,6 +349,7 @@ const CAPABILITY_TOPICS = Object.freeze({
|
|
|
349
349
|
"rule-lifecycle": ["app-scope", "omc-business-configuration", "omc-refusals"],
|
|
350
350
|
workflow: ["omc-business-configuration", "omc-refusals"],
|
|
351
351
|
onlinejs: ["omc-business-configuration", "omc-write-hazards"],
|
|
352
|
+
programming: ["omc-backend-online-coding", "omc-business-configuration", "omc-write-hazards"],
|
|
352
353
|
bizservice: ["omc-business-configuration", "omc-write-hazards"],
|
|
353
354
|
report: ["omc-business-configuration", "omc-write-hazards"],
|
|
354
355
|
data: ["omc-business-configuration", "omc-refusals"],
|
package/dist/cli.js
CHANGED
|
@@ -2418,6 +2418,7 @@ async function main() {
|
|
|
2418
2418
|
.option("--code <schemaCode>")
|
|
2419
2419
|
.option("--fields <json>", "JSON 数组,每条是一个 field.create 参数(choices/字典 options 原样透传)")
|
|
2420
2420
|
.option("--fields-json <json>")
|
|
2421
|
+
.option("--fields-file <path>", "从配置根内文件读字段 JSON 数组;与 --fields 互斥")
|
|
2421
2422
|
.option("--env <environment>")
|
|
2422
2423
|
.option("--environment <environment>")
|
|
2423
2424
|
.option("--no-form", "跳过表单重建")
|
|
@@ -2436,6 +2437,7 @@ async function main() {
|
|
|
2436
2437
|
...(options.code ? ["--code", options.code] : []),
|
|
2437
2438
|
...(options.fields !== undefined ? ["--fields", options.fields] : []),
|
|
2438
2439
|
...(options.fieldsJson !== undefined ? ["--fields-json", options.fieldsJson] : []),
|
|
2440
|
+
...(options.fieldsFile !== undefined ? ["--fields-file", options.fieldsFile] : []),
|
|
2439
2441
|
...(options.env ? ["--env", options.env] : []),
|
|
2440
2442
|
...(options.environment ? ["--environment", options.environment] : []),
|
|
2441
2443
|
...(options.form === false ? ["--no-form"] : []),
|
package/dist/field-add.js
CHANGED
|
@@ -17,6 +17,9 @@ import { argvValue } from "./args.js";
|
|
|
17
17
|
import { runToolCall } from "./call.js";
|
|
18
18
|
import { CLI_EXIT_CODES, classifyResult, envelope, refusalText, requestId } from "./cli-contract.js";
|
|
19
19
|
import { writeEnvelope } from "./cli-output.js";
|
|
20
|
+
import { assertSafeInputPath } from "./artifacts.js";
|
|
21
|
+
import { readFileSync } from "node:fs";
|
|
22
|
+
import { resolve } from "node:path";
|
|
20
23
|
/**
|
|
21
24
|
* 字典简写展开(r30/r36 实证:字典字段要先 names 查 id、records 取记录、
|
|
22
25
|
* 再拼 optionsSet.dictionary 三段 4–5 调;`dictCode` 把它折进 field-add 一步):
|
|
@@ -108,7 +111,9 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
108
111
|
const request = requestId();
|
|
109
112
|
const flag = (name) => argvValue(args, name);
|
|
110
113
|
const schemaCode = flag("--schema") ?? flag("--schema-code") ?? flag("--code");
|
|
111
|
-
const
|
|
114
|
+
const fieldsFile = flag("--fields-file");
|
|
115
|
+
const fieldsFlag = flag("--fields");
|
|
116
|
+
const fieldsJson = fieldsFlag === "-" ? readFileSync(0, "utf8") : fieldsFlag ?? flag("--fields-json");
|
|
112
117
|
const environment = flag("--env") ?? flag("--environment");
|
|
113
118
|
const apply = args.includes("--apply");
|
|
114
119
|
const publishForm = !args.includes("--no-form");
|
|
@@ -117,7 +122,7 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
117
122
|
const conditionsJson = flag("--conditions") ?? flag("--conditions-json");
|
|
118
123
|
const demoRowsJson = flag("--demo-rows") ?? flag("--demo-rows-json");
|
|
119
124
|
const usage = [
|
|
120
|
-
"用法:omc field-add --schema <模型code> --fields '<JSON 数组>' [--apply]",
|
|
125
|
+
"用法:omc field-add --schema <模型code> --fields '<JSON 数组>' [--fields-file <路径>|--fields -] [--apply]",
|
|
121
126
|
" --fields 每项是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...}(choices/字典 options 原样透传);",
|
|
122
127
|
" 字典简写:{\"dictCode\":\"<字典code>\"} 代替手拼 options.optionsSet(内部解析字典 id+启用记录,recipe 同形状)。",
|
|
123
128
|
" 默认 dry-run;--apply 依次执行 建字段 → 发布字段 → 重建表单 → 列表追加列。",
|
|
@@ -125,6 +130,7 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
125
130
|
" --no-form 跳过表单重建;--no-list 跳过列表加列(默认两者都做)。",
|
|
126
131
|
" --conditions '<JSON 数组>' 列表固定过滤条件(listview.configure 契约;**整体替换**既有固定条件——要保留旧的须一并带上;不传则不动);需 --list(默认开)。",
|
|
127
132
|
" --demo-rows '<JSON 数组>' 演示数据行(每项一份 formData,逐行 data.submit);非幂等——重复执行会重复插行。",
|
|
133
|
+
" --fields-file 从配置根内文件读 JSON 数组;--fields - 从 stdin 读 JSON 数组。",
|
|
128
134
|
].join("\n");
|
|
129
135
|
const invalid = (message) => {
|
|
130
136
|
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
|
|
@@ -132,11 +138,23 @@ export async function cmdFieldAdd(args, contextForTest) {
|
|
|
132
138
|
};
|
|
133
139
|
if (!schemaCode)
|
|
134
140
|
return invalid("--schema 必填(存量模型 code)");
|
|
135
|
-
if (
|
|
136
|
-
return invalid("--fields
|
|
141
|
+
if (fieldsFile !== undefined && (fieldsFlag !== undefined || flag("--fields-json") !== undefined))
|
|
142
|
+
return invalid("--fields-file 不能与 --fields/--fields-json 同时使用");
|
|
143
|
+
let inputJson = fieldsJson;
|
|
144
|
+
if (fieldsFile !== undefined) {
|
|
145
|
+
try {
|
|
146
|
+
const root = resolve(process.env.OMC_CONFIG_HOME ?? process.cwd());
|
|
147
|
+
inputJson = readFileSync(assertSafeInputPath(root, fieldsFile, "--fields-file"), "utf8");
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return invalid(`--fields-file 读取失败:${error instanceof Error ? error.message : String(error)}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (inputJson === undefined)
|
|
154
|
+
return invalid("--fields 必填(JSON 数组;或使用 --fields-file/--fields -)");
|
|
137
155
|
let fields;
|
|
138
156
|
try {
|
|
139
|
-
const parsed = JSON.parse(
|
|
157
|
+
const parsed = JSON.parse(inputJson);
|
|
140
158
|
if (!Array.isArray(parsed) || parsed.length === 0)
|
|
141
159
|
throw new Error("--fields 必须是非空 JSON 数组");
|
|
142
160
|
fields = parsed;
|
package/dist/field-families.js
CHANGED
|
@@ -938,14 +938,14 @@ export function assertSelectionChoicesAllowed(args) {
|
|
|
938
938
|
}
|
|
939
939
|
function refuseFieldOptions(reason, message) {
|
|
940
940
|
const citation = {
|
|
941
|
-
code: "
|
|
941
|
+
code: "field-options-shape",
|
|
942
942
|
lane: "field-families",
|
|
943
943
|
capability: "field family full chain",
|
|
944
944
|
status: "blocked",
|
|
945
945
|
reason,
|
|
946
946
|
message,
|
|
947
947
|
};
|
|
948
|
-
throw new ToolRefusalError({ status: "refused", blocker: "
|
|
948
|
+
throw new ToolRefusalError({ status: "refused", blocker: "field-options-shape", citation });
|
|
949
949
|
}
|
|
950
950
|
/**
|
|
951
951
|
* Aggregation query conditions are deserialized by Jackson into Java enums that
|
package/dist/formula-lint.js
CHANGED
|
@@ -30,6 +30,9 @@ export const FORMULA_FUNCTION_ALIASES = Object.freeze({
|
|
|
30
30
|
STRCAT: "STRING",
|
|
31
31
|
JOIN: "STRING",
|
|
32
32
|
});
|
|
33
|
+
export const FORMULA_LINT_CODES = new Set([
|
|
34
|
+
"formula-function-unknown", "formula-function-alias", "formula-text-plus-concat",
|
|
35
|
+
]);
|
|
33
36
|
const TEXT_PLUS_CONCAT = /(\{[^{}]+\}|[A-Za-z_][A-Za-z0-9_]*\s*\([^()]*\))\s*\+\s*["']|["']\s*\+\s*(\{[^{}]+\}|[A-Za-z_][A-Za-z0-9_]*\s*\()/;
|
|
34
37
|
/**
|
|
35
38
|
* Lint one formula string. `where` names the carrier in the caller's own terms
|
|
@@ -75,6 +75,7 @@ const RULE_SAVE_DESCRIPTION = describeTool("保存业务规则(upsert):`ru
|
|
|
75
75
|
"**EXPRESSION 值形状**:`ruleValueType:EXPRESSION` 时 `value` 必须是 {\"formula\":\"{字段A}+{字段B}\"}(大括号裸 code);门户设计器的 [code] 方括号式在这里不认。",
|
|
76
76
|
"**幻影 schema 门(ticket 96 live 事故)**:rule.save 写前先以 bizmodels/get_all 探测模型存在(缺失 → refused `rule-schema-unresolved`,零规则面 wire 调用)——平台的 list/save 面会对任意 schemaCode 自动补建 7 条预置规则且 API 删除不可达(list 即重灌),幻影 schema 必须在写前拒绝。",
|
|
77
77
|
"**输出回写排序(ticket 36 live 铁证)**:BIZ_ACTION 的 outputMappings 由 SYSTEM_CREATE/SYSTEM_UPDATE 持久化节点消费,动作节点必须排在系统节点之前(`Start→BizAction→SYSTEM_CREATE→End`);工具默认把系统节点补在 START 之后,有回写动作时须显式提供系统节点并置于动作之后,否则合并结果不落库(echoResult 恒 null)。",
|
|
78
|
+
"**跨模型 UPDATE 三段语义(1.1.25 反编译定案)**:`inputParam`=输入源(触发行或 QUERY 上游输出,`currentSchemaDataItem` 属此侧)→ `filterCondition` 按 `targetSchemaCode` 查目标行 → `dataActions` 把输入源值写入目标行。QUERY 目标行不自动成为输入源;child fan-out 保持 fail-closed(用显式中间字段/映射/二开)。",
|
|
78
79
|
]);
|
|
79
80
|
export const BUSINESSRULE_LANE_CONTRACT = Object.freeze({
|
|
80
81
|
lane: LANES.businessrule,
|
|
@@ -479,6 +479,7 @@ export function registerBusinessRuleLane(tools) {
|
|
|
479
479
|
name: z.string().min(1),
|
|
480
480
|
nodeGraph: ruleNodeGraphArg,
|
|
481
481
|
publish: z.boolean().optional().describe("also publish the new version in this call (businessrule/publish) so the designer sees it; enabling stays with rule.enable — default false keeps the legacy draft-only save"),
|
|
482
|
+
allowAdvisory: z.boolean().optional().describe("调查逃生口;默认 false"),
|
|
482
483
|
customTrigger: z.boolean().optional().describe("默认开:自定义 ruleCode + bizRuleType 0(非平台触发码 Create/Update/Delete/GetList/Load/Available/Cancel)即按钮/流程规则,工具在 START 后合成平台标准 CUSTOM_NODE「自定义按钮/流程触发」系统触发节点(nodeCode=ruleCode,live 布局 x558/y233);调用方已写 CUSTOM_NODE 则原样保留;显式 false 退回"),
|
|
483
484
|
},
|
|
484
485
|
execute: async (session, args) => executeRuleSaveWrite(session, args),
|
package/dist/lanes/datarule.js
CHANGED
|
@@ -481,6 +481,11 @@ export function dataRuleFormulaWarnings(dataRuleType, options) {
|
|
|
481
481
|
return [];
|
|
482
482
|
return lintFormulaText(formula, "options.shortTextStitch.formula");
|
|
483
483
|
}
|
|
484
|
+
function assertDataRuleFormulaLint(dataRuleType, options, allowAdvisory) {
|
|
485
|
+
const findings = dataRuleFormulaWarnings(dataRuleType, options);
|
|
486
|
+
if (findings.length > 0 && allowAdvisory !== true)
|
|
487
|
+
throw new ToolRefusalError({ status: "refused", blocker: "formula-lint-failed", validation: findings, nextCommand: "修正公式函数/字符串拼接;调查用途显式传 allowAdvisory:true", message: "公式写前校验拒绝:未知函数、平台别名或字符串 + 拼接。文本合并请使用 STRING()。" });
|
|
488
|
+
}
|
|
484
489
|
/** Extract the expression of a type-2 CALCULATE rule (mirrors calcExpressionShapeError's carrier choice). */
|
|
485
490
|
function type2FormulaText(options) {
|
|
486
491
|
const raw = typeof options.shortTextStitch === "string" ? options.shortTextStitch : typeof options.value === "string" ? options.value : undefined;
|
|
@@ -872,9 +877,11 @@ export function registerDataRuleLane(tools) {
|
|
|
872
877
|
childRule: z.boolean().optional().describe("true=子表列规则,propertyCode 为子表列 code;主表字段忽略"),
|
|
873
878
|
name: z.string().optional(),
|
|
874
879
|
sortKey: z.number().int().optional(),
|
|
880
|
+
allowAdvisory: z.boolean().optional().describe("调查逃生口;默认 false"),
|
|
875
881
|
},
|
|
876
882
|
execute: async (session, args) => {
|
|
877
883
|
assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
|
|
884
|
+
assertDataRuleFormulaLint(args.dataRuleType, args.options, args.allowAdvisory);
|
|
878
885
|
assertDataRuleCheckType(args.dataRuleType, args.checkType);
|
|
879
886
|
assertDataRuleTarget(args);
|
|
880
887
|
const codes = dataRuleTargetCodes(args);
|
|
@@ -957,6 +964,7 @@ export function registerDataRuleLane(tools) {
|
|
|
957
964
|
propertyCodes: propertyCodesArg,
|
|
958
965
|
dataRuleType: dataRuleTypeArg,
|
|
959
966
|
options: dataRuleOptionsArg,
|
|
967
|
+
allowAdvisory: z.boolean().optional().describe("调查逃生口;默认 false"),
|
|
960
968
|
checkType: checkTypeArg,
|
|
961
969
|
childRule: z.boolean().optional().describe("true=子表列规则,propertyCode 为子表列 code;主表字段忽略"),
|
|
962
970
|
name: z.string().optional(),
|
|
@@ -964,6 +972,7 @@ export function registerDataRuleLane(tools) {
|
|
|
964
972
|
},
|
|
965
973
|
execute: async (session, args) => {
|
|
966
974
|
assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
|
|
975
|
+
assertDataRuleFormulaLint(args.dataRuleType, args.options, args.allowAdvisory);
|
|
967
976
|
assertDataRuleCheckType(args.dataRuleType, args.checkType);
|
|
968
977
|
const id = await locateDataRuleId(session, args, "update POST");
|
|
969
978
|
const before = await listDataRules(session, args.schemaCode);
|
package/dist/lanes/field.js
CHANGED
|
@@ -41,6 +41,61 @@ export async function modelSchemaCodes(session) {
|
|
|
41
41
|
.map((row) => String(row.schemaCode ?? row.bizSchemaCode ?? row.code ?? ""))
|
|
42
42
|
.filter((code) => code.length > 0);
|
|
43
43
|
}
|
|
44
|
+
function relationTypeOf(row) {
|
|
45
|
+
const value = Number(row.type ?? row.propertyType);
|
|
46
|
+
return Number.isInteger(value) ? value : undefined;
|
|
47
|
+
}
|
|
48
|
+
/** Semantic relation gate: the designer only offers published business fields. */
|
|
49
|
+
async function assertRelationOptionsFields(session, schemaCode, propertyType, relativeCode, options) {
|
|
50
|
+
const type = typeof propertyType === "number" ? propertyType : FIELD_PROPERTY_TYPES[String(propertyType ?? "")];
|
|
51
|
+
if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET && type !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
|
|
52
|
+
return;
|
|
53
|
+
const target = String(relativeCode ?? options.schemaCode ?? "").trim();
|
|
54
|
+
if (!target)
|
|
55
|
+
return;
|
|
56
|
+
const targetRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: target, isPublish: "true" } })).data);
|
|
57
|
+
const ownRows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
|
|
58
|
+
const business = (rows) => rows
|
|
59
|
+
.map((row) => ({ code: String(row.code ?? row.propertyCode ?? "").trim(), type: relationTypeOf(row) }))
|
|
60
|
+
.filter((row) => row.code && !SYSTEM_FIELD_CODES.has(row.code));
|
|
61
|
+
const targetFields = business(targetRows);
|
|
62
|
+
const ownFields = business(ownRows);
|
|
63
|
+
// A compatibility/fake transport may expose an empty field face even though
|
|
64
|
+
// the model exists. Empty metadata is not proof that a requested field is
|
|
65
|
+
// absent; preserve the existing relation write path rather than rejecting a
|
|
66
|
+
// valid payload on an unavailable read face.
|
|
67
|
+
if (targetRows.length === 0 || ownRows.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
const candidates = targetFields.map((row) => row.code).sort();
|
|
70
|
+
const display = typeof options.displayField === "string" ? options.displayField.trim() : "";
|
|
71
|
+
if (display) {
|
|
72
|
+
const field = targetFields.find((row) => row.code === display);
|
|
73
|
+
const declaredType = options.displayFieldType === undefined ? undefined : Number(options.displayFieldType);
|
|
74
|
+
if (!field || (declaredType !== undefined && Number.isInteger(declaredType) && field.type !== undefined && field.type !== declaredType)) {
|
|
75
|
+
throw new ToolRefusalError({ status: "refused", blocker: "relation-display-field-not-found", schemaCode, relationTarget: target, displayField: display, candidates, message: `关联显示字段 ${target}.${display} 不存在于发布面或类型不兼容;请从目标模型业务字段中选择。候选:${candidates.join("、") || "(无)"}` });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
let mappings = options.mappings;
|
|
79
|
+
if (typeof mappings === "string" && mappings) {
|
|
80
|
+
try {
|
|
81
|
+
mappings = JSON.parse(mappings);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
mappings = [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!Array.isArray(mappings))
|
|
88
|
+
return;
|
|
89
|
+
const sourceCandidates = targetFields.map((row) => row.code).sort();
|
|
90
|
+
const targetCandidates = ownFields.map((row) => row.code).sort();
|
|
91
|
+
for (const [index, row] of mappings.entries()) {
|
|
92
|
+
const source = row && typeof row === "object" ? String(row.source ?? "").trim() : "";
|
|
93
|
+
const mappedTarget = row && typeof row === "object" ? String(row.target ?? "").trim() : "";
|
|
94
|
+
if (!sourceCandidates.includes(source) || !targetCandidates.includes(mappedTarget)) {
|
|
95
|
+
throw new ToolRefusalError({ status: "refused", blocker: "relation-mapping-field-not-found", schemaCode, relationTarget: target, mappingIndex: index, source, targetField: mappedTarget, sourceCandidates, targetCandidates, message: `关联映射第 ${index + 1} 行字段不存在:source=${source || "(空)"}(目标模型候选:${sourceCandidates.join("、") || "(无)"}),target=${mappedTarget || "(空)"}(本模型候选:${targetCandidates.join("、") || "(无)"})。请使用发布面业务字段 code。` });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
44
99
|
async function readFieldRollback(session, args) {
|
|
45
100
|
for (const published of [false, true]) {
|
|
46
101
|
const row = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: String(args.schemaCode), isPublish: String(published) } })).data).find((entry) => String(entry.code ?? entry.propertyCode) === String(args.code));
|
|
@@ -314,7 +369,11 @@ export function registerFieldLane(tools) {
|
|
|
314
369
|
...(conditions.length > 0 ? { conditions } : {}),
|
|
315
370
|
...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
|
|
316
371
|
...(field.origin !== undefined ? { origin: field.origin } : {}),
|
|
317
|
-
...(args.full === true
|
|
372
|
+
...(args.full === true
|
|
373
|
+
? ("options" in field && field.options !== undefined
|
|
374
|
+
? { options: field.options, optionsPresent: true }
|
|
375
|
+
: { optionsPresent: false })
|
|
376
|
+
: {}),
|
|
318
377
|
};
|
|
319
378
|
});
|
|
320
379
|
return {
|
|
@@ -373,6 +432,11 @@ export function registerFieldLane(tools) {
|
|
|
373
432
|
...(conditions.length > 0 ? { conditions } : {}),
|
|
374
433
|
...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
|
|
375
434
|
...(field.origin !== undefined ? { origin: field.origin } : {}),
|
|
435
|
+
// r46a 接线(2026-11-08,r45 普查 R5a):压缩占位行保留**标量判别键**
|
|
436
|
+
// optionsPresent——不然 >10 行的 full 读回内联面完全看不到 presence
|
|
437
|
+
// 信号,agent 误判「条件没写进去」(S1-G3 预言命中:通用层把「有值
|
|
438
|
+
// 但正文外置」和「确实没有」混在一起)。正文仍在 artifact。
|
|
439
|
+
...(field.optionsPresent !== undefined ? { optionsPresent: field.optionsPresent } : {}),
|
|
376
440
|
};
|
|
377
441
|
}),
|
|
378
442
|
hint: "字段详情(options/subSchema)用 omc call field.list --args '{\"schemaCode\":\"...\",\"published\":true,\"code\":\"<field>\"}'",
|
|
@@ -418,6 +482,7 @@ export function registerFieldLane(tools) {
|
|
|
418
482
|
// (cpdocs issue #8:缺键让设计器选项弹窗硬崩)。
|
|
419
483
|
const targetVersion = session.detectedVersion();
|
|
420
484
|
const prepared = prepareFieldChange({ ...args, ...(targetVersion ? { targetVersion } : {}) });
|
|
485
|
+
await assertRelationOptionsFields(session, args.schemaCode, prepared.propertyType, args.relativeCode, prepared.options);
|
|
421
486
|
let performed;
|
|
422
487
|
try {
|
|
423
488
|
performed = await wireCall(session, { path: ENDPOINTS.fieldCreate, method: "POST", body: fieldChangeWireBody(args, prepared) });
|
|
@@ -730,6 +795,7 @@ async function fieldUpdateDryRun(session, args) {
|
|
|
730
795
|
...(args.propertyEmpty !== undefined ? { propertyEmpty: args.propertyEmpty } : {}),
|
|
731
796
|
};
|
|
732
797
|
const prepared = prepareFieldUpdate(update);
|
|
798
|
+
await assertRelationOptionsFields(session, args.schemaCode, prepared.propertyType, args.relativeCode ?? record.relativeCode, prepared.options);
|
|
733
799
|
const currentOptions = decodeJsonObjectColumn(record.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {};
|
|
734
800
|
const changes = [];
|
|
735
801
|
const pushChange = (field, from, to) => {
|
|
@@ -554,8 +554,11 @@ export function registerOrganizationLane(tools) {
|
|
|
554
554
|
// A 32-hex id absent from the tree may be a group created moments ago in
|
|
555
555
|
// this same session; let it through and rely on the visibility backstop.
|
|
556
556
|
if (!match && args.roleGroupCode) {
|
|
557
|
-
throw new
|
|
557
|
+
throw new ToolRefusalError({
|
|
558
|
+
status: "refused",
|
|
559
|
+
blocker: "role-group-not-found",
|
|
558
560
|
message: `角色组 ${requested} 不存在。可用组:${groups.map((group) => `${group.code}(id=${group.id},${group.name})`).join(";")}`,
|
|
561
|
+
candidates: groups.map((group) => ({ code: group.code, id: group.id, name: group.name })),
|
|
559
562
|
});
|
|
560
563
|
}
|
|
561
564
|
const roleGroupId = match?.id ?? String(requested);
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -878,7 +878,15 @@ async function assertRoleParticipantScopes(session, body) {
|
|
|
878
878
|
if (String(activity.activityType ?? "") !== "PARTICIPANT")
|
|
879
879
|
continue;
|
|
880
880
|
const participant = typeof activity.participant === "string" ? activity.participant.trim() : "";
|
|
881
|
-
|
|
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);
|
|
882
890
|
if (!match)
|
|
883
891
|
continue;
|
|
884
892
|
const quoted = match[1];
|
|
@@ -902,9 +910,7 @@ async function assertRoleParticipantScopes(session, body) {
|
|
|
902
910
|
roleListAvailable = true;
|
|
903
911
|
}
|
|
904
912
|
catch {
|
|
905
|
-
|
|
906
|
-
// Do not turn a transient read failure into a blanket workflow write outage.
|
|
907
|
-
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 拒绝写入。请恢复组织读面后重试。" });
|
|
908
914
|
}
|
|
909
915
|
if (roleListAvailable) {
|
|
910
916
|
const byCode = new Map(listedRoles
|
|
@@ -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({
|
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",
|
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/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
|
}
|
package/skills/omc-playbook.md
CHANGED
|
@@ -13,6 +13,7 @@ description: 一页纸业务场景步骤清单(采购申请 / 请假审批 /
|
|
|
13
13
|
|
|
14
14
|
1. `omc next "从零做采购申请:主子表、合计、条件审批、列表、按钮"` → `omc intent schema`
|
|
15
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 步完成字段发布与表单发布;无需另起命令)
|
|
16
17
|
4. `omc compute --schema <code> --target amount --formula 'SUM({<主表code>_details.lineAmount})' --apply`(⚠️ 只在门户算,REST 不回填)
|
|
17
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`。
|
|
18
19
|
6. 发起权限:`omc call workflow.addPermission --args '{"workflowCode":"<code>_wf","unitId":"<deptOrRoleId>","unitType":1}'`(这是**谁能发起**,不是行级可见范围)
|