@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
|
@@ -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
|
|
@@ -347,6 +349,7 @@ const CAPABILITY_TOPICS = Object.freeze({
|
|
|
347
349
|
"rule-lifecycle": ["app-scope", "omc-business-configuration", "omc-refusals"],
|
|
348
350
|
workflow: ["omc-business-configuration", "omc-refusals"],
|
|
349
351
|
onlinejs: ["omc-business-configuration", "omc-write-hazards"],
|
|
352
|
+
programming: ["omc-backend-online-coding", "omc-business-configuration", "omc-write-hazards"],
|
|
350
353
|
bizservice: ["omc-business-configuration", "omc-write-hazards"],
|
|
351
354
|
report: ["omc-business-configuration", "omc-write-hazards"],
|
|
352
355
|
data: ["omc-business-configuration", "omc-refusals"],
|
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] : []),
|
|
@@ -2416,6 +2418,7 @@ async function main() {
|
|
|
2416
2418
|
.option("--code <schemaCode>")
|
|
2417
2419
|
.option("--fields <json>", "JSON 数组,每条是一个 field.create 参数(choices/字典 options 原样透传)")
|
|
2418
2420
|
.option("--fields-json <json>")
|
|
2421
|
+
.option("--fields-file <path>", "从配置根内文件读字段 JSON 数组;与 --fields 互斥")
|
|
2419
2422
|
.option("--env <environment>")
|
|
2420
2423
|
.option("--environment <environment>")
|
|
2421
2424
|
.option("--no-form", "跳过表单重建")
|
|
@@ -2434,6 +2437,7 @@ async function main() {
|
|
|
2434
2437
|
...(options.code ? ["--code", options.code] : []),
|
|
2435
2438
|
...(options.fields !== undefined ? ["--fields", options.fields] : []),
|
|
2436
2439
|
...(options.fieldsJson !== undefined ? ["--fields-json", options.fieldsJson] : []),
|
|
2440
|
+
...(options.fieldsFile !== undefined ? ["--fields-file", options.fieldsFile] : []),
|
|
2437
2441
|
...(options.env ? ["--env", options.env] : []),
|
|
2438
2442
|
...(options.environment ? ["--environment", options.environment] : []),
|
|
2439
2443
|
...(options.form === false ? ["--no-form"] : []),
|
|
@@ -2530,7 +2534,7 @@ async function main() {
|
|
|
2530
2534
|
pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
|
|
2531
2535
|
});
|
|
2532
2536
|
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)); });
|
|
2537
|
+
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
2538
|
const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
|
|
2535
2539
|
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
2540
|
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-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
|
@@ -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
|
}
|
|
@@ -934,14 +938,14 @@ export function assertSelectionChoicesAllowed(args) {
|
|
|
934
938
|
}
|
|
935
939
|
function refuseFieldOptions(reason, message) {
|
|
936
940
|
const citation = {
|
|
937
|
-
code: "
|
|
941
|
+
code: "field-options-shape",
|
|
938
942
|
lane: "field-families",
|
|
939
943
|
capability: "field family full chain",
|
|
940
944
|
status: "blocked",
|
|
941
945
|
reason,
|
|
942
946
|
message,
|
|
943
947
|
};
|
|
944
|
-
throw new ToolRefusalError({ status: "refused", blocker: "
|
|
948
|
+
throw new ToolRefusalError({ status: "refused", blocker: "field-options-shape", citation });
|
|
945
949
|
}
|
|
946
950
|
/**
|
|
947
951
|
* Aggregation query conditions are deserialized by Jackson into Java enums that
|
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/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
|
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,8 +72,10 @@ 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)。",
|
|
78
|
+
"**跨模型 UPDATE 三段语义(1.1.25 反编译定案)**:`inputParam`=输入源(触发行或 QUERY 上游输出,`currentSchemaDataItem` 属此侧)→ `filterCondition` 按 `targetSchemaCode` 查目标行 → `dataActions` 把输入源值写入目标行。QUERY 目标行不自动成为输入源;child fan-out 保持 fail-closed(用显式中间字段/映射/二开)。",
|
|
77
79
|
]);
|
|
78
80
|
export const BUSINESSRULE_LANE_CONTRACT = Object.freeze({
|
|
79
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;
|
|
@@ -745,14 +750,17 @@ const FORM_CONDITION_RULE_TYPES = Object.freeze({
|
|
|
745
750
|
export async function syncFormConditionDataRules(session, schemaCode, fieldCode, prevOptions, nextOptions) {
|
|
746
751
|
const result = { created: [], updated: [], deleted: [], skipped: [] };
|
|
747
752
|
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
753
|
const existing = await listDataRules(session, schemaCode);
|
|
752
754
|
// The designer's rule rows live on the PUBLISHED face (like datarule.create's
|
|
753
755
|
// own guard); a draft-only field cannot carry a designer-visible row yet.
|
|
754
756
|
const publishedCodes = await readPublishedFieldCodesOrNull(session, schemaCode);
|
|
755
757
|
const published = publishedCodes === null ? true : publishedCodes.has(fieldCode);
|
|
758
|
+
const changedKeys = Object.keys(FORM_CONDITION_RULE_TYPES).filter((key) => {
|
|
759
|
+
const changed = text(prevOptions, key) !== text(nextOptions, key);
|
|
760
|
+
const desired = text(nextOptions, key);
|
|
761
|
+
const present = existing.some((row) => Number(row.dataRuleType) === FORM_CONDITION_RULE_TYPES[key] && storedTargetCodes(row).includes(fieldCode));
|
|
762
|
+
return changed || (desired.length > 0 && !present);
|
|
763
|
+
});
|
|
756
764
|
for (const key of changedKeys) {
|
|
757
765
|
const dataRuleType = FORM_CONDITION_RULE_TYPES[key];
|
|
758
766
|
const next = text(nextOptions, key);
|
|
@@ -869,9 +877,11 @@ export function registerDataRuleLane(tools) {
|
|
|
869
877
|
childRule: z.boolean().optional().describe("true=子表列规则,propertyCode 为子表列 code;主表字段忽略"),
|
|
870
878
|
name: z.string().optional(),
|
|
871
879
|
sortKey: z.number().int().optional(),
|
|
880
|
+
allowAdvisory: z.boolean().optional().describe("调查逃生口;默认 false"),
|
|
872
881
|
},
|
|
873
882
|
execute: async (session, args) => {
|
|
874
883
|
assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
|
|
884
|
+
assertDataRuleFormulaLint(args.dataRuleType, args.options, args.allowAdvisory);
|
|
875
885
|
assertDataRuleCheckType(args.dataRuleType, args.checkType);
|
|
876
886
|
assertDataRuleTarget(args);
|
|
877
887
|
const codes = dataRuleTargetCodes(args);
|
|
@@ -954,6 +964,7 @@ export function registerDataRuleLane(tools) {
|
|
|
954
964
|
propertyCodes: propertyCodesArg,
|
|
955
965
|
dataRuleType: dataRuleTypeArg,
|
|
956
966
|
options: dataRuleOptionsArg,
|
|
967
|
+
allowAdvisory: z.boolean().optional().describe("调查逃生口;默认 false"),
|
|
957
968
|
checkType: checkTypeArg,
|
|
958
969
|
childRule: z.boolean().optional().describe("true=子表列规则,propertyCode 为子表列 code;主表字段忽略"),
|
|
959
970
|
name: z.string().optional(),
|
|
@@ -961,6 +972,7 @@ export function registerDataRuleLane(tools) {
|
|
|
961
972
|
},
|
|
962
973
|
execute: async (session, args) => {
|
|
963
974
|
assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
|
|
975
|
+
assertDataRuleFormulaLint(args.dataRuleType, args.options, args.allowAdvisory);
|
|
964
976
|
assertDataRuleCheckType(args.dataRuleType, args.checkType);
|
|
965
977
|
const id = await locateDataRuleId(session, args, "update POST");
|
|
966
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));
|
|
@@ -266,7 +321,7 @@ export function registerFieldLane(tools) {
|
|
|
266
321
|
// 不带这个键,于是不会出现;write-only 的确认走 field.update 的
|
|
267
322
|
// readback.committed。
|
|
268
323
|
...(Object.prototype.hasOwnProperty.call(row, "propertyEmpty") ? { propertyEmpty: row.propertyEmpty } : {}),
|
|
269
|
-
...(options !== undefined ? { options } : {}),
|
|
324
|
+
...(options !== undefined ? { options, optionsPresent: true } : { optionsPresent: false }),
|
|
270
325
|
...(scaleWarning !== undefined ? { shapeWarning: scaleWarning, ...(system ? { origin: "system" } : {}) } : {}),
|
|
271
326
|
...(system && scaleWarning === undefined ? { origin: "system" } : {}),
|
|
272
327
|
};
|
|
@@ -292,7 +347,7 @@ export function registerFieldLane(tools) {
|
|
|
292
347
|
const projected = detailed
|
|
293
348
|
? visible
|
|
294
349
|
: visible.map((field) => {
|
|
295
|
-
const options = (field.options ?? {}
|
|
350
|
+
const options = ("options" in field ? field.options : undefined) ?? {};
|
|
296
351
|
const conditions = Object.keys(FORM_CONDITION_OPTION_KEYS).filter((key) => {
|
|
297
352
|
const value = options[key];
|
|
298
353
|
return key === "readonlyFormula" ? value === true : typeof value === "string" && value.trim() !== "";
|
|
@@ -314,6 +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 } : {}),
|
|
372
|
+
...(args.full === true
|
|
373
|
+
? ("options" in field && field.options !== undefined
|
|
374
|
+
? { options: field.options, optionsPresent: true }
|
|
375
|
+
: { optionsPresent: false })
|
|
376
|
+
: {}),
|
|
317
377
|
};
|
|
318
378
|
});
|
|
319
379
|
return {
|
|
@@ -372,6 +432,11 @@ export function registerFieldLane(tools) {
|
|
|
372
432
|
...(conditions.length > 0 ? { conditions } : {}),
|
|
373
433
|
...(field.shapeWarning !== undefined ? { shapeWarning: field.shapeWarning } : {}),
|
|
374
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 } : {}),
|
|
375
440
|
};
|
|
376
441
|
}),
|
|
377
442
|
hint: "字段详情(options/subSchema)用 omc call field.list --args '{\"schemaCode\":\"...\",\"published\":true,\"code\":\"<field>\"}'",
|
|
@@ -417,6 +482,7 @@ export function registerFieldLane(tools) {
|
|
|
417
482
|
// (cpdocs issue #8:缺键让设计器选项弹窗硬崩)。
|
|
418
483
|
const targetVersion = session.detectedVersion();
|
|
419
484
|
const prepared = prepareFieldChange({ ...args, ...(targetVersion ? { targetVersion } : {}) });
|
|
485
|
+
await assertRelationOptionsFields(session, args.schemaCode, prepared.propertyType, args.relativeCode, prepared.options);
|
|
420
486
|
let performed;
|
|
421
487
|
try {
|
|
422
488
|
performed = await wireCall(session, { path: ENDPOINTS.fieldCreate, method: "POST", body: fieldChangeWireBody(args, prepared) });
|
|
@@ -729,6 +795,7 @@ async function fieldUpdateDryRun(session, args) {
|
|
|
729
795
|
...(args.propertyEmpty !== undefined ? { propertyEmpty: args.propertyEmpty } : {}),
|
|
730
796
|
};
|
|
731
797
|
const prepared = prepareFieldUpdate(update);
|
|
798
|
+
await assertRelationOptionsFields(session, args.schemaCode, prepared.propertyType, args.relativeCode ?? record.relativeCode, prepared.options);
|
|
732
799
|
const currentOptions = decodeJsonObjectColumn(record.options, { invalid: "throw", array: "accept", object: "accept" }) ?? {};
|
|
733
800
|
const changes = [];
|
|
734
801
|
const pushChange = (field, from, to) => {
|
|
@@ -1117,6 +1184,19 @@ async function runFieldUpdate(session, args) {
|
|
|
1117
1184
|
if (args.choices !== undefined && !same(choiceCodesOf(currentOptions) ?? [], [...args.choices]))
|
|
1118
1185
|
requestedChanges.push("choices");
|
|
1119
1186
|
if (requestedChanges.length === 0) {
|
|
1187
|
+
let unchangedSync;
|
|
1188
|
+
let unchangedSyncError;
|
|
1189
|
+
const conditionKeys = new Set(Object.keys(FORM_CONDITION_OPTION_KEYS));
|
|
1190
|
+
if (effectiveOptions !== undefined && [...conditionKeys].some((key) => typeof prepared.options[key] === "string" && String(prepared.options[key]).trim() !== "")) {
|
|
1191
|
+
try {
|
|
1192
|
+
const sync = await syncFormConditionDataRules(session, args.schemaCode, args.code, currentOptions, prepared.options);
|
|
1193
|
+
if (sync.created.length + sync.updated.length + sync.deleted.length + sync.skipped.length > 0)
|
|
1194
|
+
unchangedSync = sync;
|
|
1195
|
+
}
|
|
1196
|
+
catch (error) {
|
|
1197
|
+
unchangedSyncError = error instanceof Error ? error.message : String(error);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1120
1200
|
return {
|
|
1121
1201
|
performed: { errcode: 0, unchanged: true },
|
|
1122
1202
|
readback: {
|
|
@@ -1126,6 +1206,7 @@ async function runFieldUpdate(session, args) {
|
|
|
1126
1206
|
...(record.name !== undefined ? { name: record.name } : {}),
|
|
1127
1207
|
...(record.name_i18n !== undefined ? { name_i18n: record.name_i18n } : {}),
|
|
1128
1208
|
availableFields: ["name", "name_i18n", "options", "choices", "relativeCode", "propertyEmpty"],
|
|
1209
|
+
...(unchangedSync !== undefined ? { formConditionSync: unchangedSync } : unchangedSyncError !== undefined ? { formConditionSync: { error: unchangedSyncError } } : {}),
|
|
1129
1210
|
},
|
|
1130
1211
|
};
|
|
1131
1212
|
}
|
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(探针只能靠
|
|
@@ -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);
|
|
@@ -1042,11 +1045,11 @@ export function registerOrganizationLane(tools) {
|
|
|
1042
1045
|
name: "org.role.delete",
|
|
1043
1046
|
lane: LANE,
|
|
1044
1047
|
scope: SCOPES.roleDelete,
|
|
1045
|
-
description: "删除角色(`GET organization/role/strike_out_role?ids=`)。平台**拒绝删除仍有成员的角色**(`-1 角色下已经包含用户数据!`,实测 1.1.25
|
|
1048
|
+
description: "删除角色(`GET organization/role/strike_out_role?ids=`)。平台**拒绝删除仍有成员的角色**(`-1 角色下已经包含用户数据!`,实测 1.1.25):默认先读成员并拒绝;拒绝回执带可直接粘贴的 `org.role.removeUsers` 命令。`force:true` 的语义是**先逐人移出全部成员,再删除角色**(不是强行绕过平台校验)。**可用 roleCode 代替 roleId**(org.role.list 的码表只有 code)。",
|
|
1046
1049
|
inputSchema: {
|
|
1047
1050
|
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
1048
1051
|
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表,如 qc_manager)或名称"),
|
|
1049
|
-
force: z.boolean().optional().describe("true =
|
|
1052
|
+
force: z.boolean().optional().describe("true = 先逐人调用 removeUsers 移出全部成员,再删角色;不会保留成员"),
|
|
1050
1053
|
},
|
|
1051
1054
|
execute: async (session, args) => {
|
|
1052
1055
|
const roleId = await resolveRoleRef(session, { roleId: args.roleId, roleCode: args.roleCode }, "org.role.delete");
|
|
@@ -1054,7 +1057,7 @@ export function registerOrganizationLane(tools) {
|
|
|
1054
1057
|
if (rows.length > 0 && args.force !== true)
|
|
1055
1058
|
throw new ToolRefusalError({
|
|
1056
1059
|
status: "refused", blocker: "role-has-members", roleId, members: rows.length,
|
|
1057
|
-
nextCommand: `omc call org.role.
|
|
1060
|
+
nextCommand: `omc call org.role.removeUsers --args '${JSON.stringify({ roleId, ids: rows.map(roleUserId) })}'`,
|
|
1058
1061
|
message: `角色下还有 ${rows.length} 名成员,平台拒绝删除(errmsg:角色下已经包含用户数据)。先 org.role.removeUsers,或传 force:true 让工具移出后再删。`,
|
|
1059
1062
|
});
|
|
1060
1063
|
if (rows.length > 0)
|