@stalfh233/omc-cli 0.6.2 → 0.6.3
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 -1
- package/dist/cli-output.js +5 -0
- package/dist/cli.js +37 -6
- package/dist/config-store.js +17 -0
- package/dist/endpoints.js +5 -0
- package/dist/lanes/b2-registration.js +29 -3
- package/dist/lanes/businessrule-contract.js +5 -1
- package/dist/lanes/businessrule.js +38 -3
- package/dist/lanes/rule-lifecycle.js +12 -2
- package/dist/lanes/runtime-data.js +83 -7
- package/dist/path-projection.js +1 -1
- package/dist/read.js +32 -5
- package/dist/recipes/businessrule.js +83 -1
- package/dist/rule-support.js +61 -1
- package/dist/wire.js +26 -0
- package/package.json +3 -3
|
@@ -107,7 +107,7 @@ const SURFACES = [
|
|
|
107
107
|
operations: ["list", "get", "save"],
|
|
108
108
|
requires: ["field"],
|
|
109
109
|
intent: { level: "partial", note: "声明式支持数据事件规则与定时任务:紧凑 actions 或 nodeGraph 透传(BRANCH/BIZ_ACTION/定时图用透传);写 route.save + rule.enable/disable。" },
|
|
110
|
-
notes: "数据事件只分发到平台预置 code 行(Create/Update/Delete);自定义 ruleCode 需按钮/流程/定时/显式触发。**「每天定时执行」= 定时任务 TIMING_TASK(平台原生,非外部定时器、非在线 JS)**:`rule.save` 传含 SCHEDULER 节点 + `schedulerSetting`(`bizRuleType:2`)的 nodeGraph
|
|
110
|
+
notes: "数据事件只分发到平台预置 code 行(Create/Update/Delete);自定义 ruleCode 需按钮/流程/定时/显式触发。**「每天定时执行」= 定时任务 TIMING_TASK(平台原生,非外部定时器、非在线 JS)**:`rule.save` 传含 SCHEDULER 节点 + `schedulerSetting`(`bizRuleType:2`)的 nodeGraph。**定时改数据**(GET_LIST + UPDATE 动作节点)见 `--example rule-timing-task`;**定时调业务集成方法**(SCHEDULER-only + `dataSourceType:BIZ_SERVICE` + `methodMapping` 含应用编码 methodCode)见 `--example rule-timing-bizservice`。保存≠可见,需 rule.enable;绑定 BizMethod 前先有已发布业务集成方法。",
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
id: "rule-lifecycle",
|
package/dist/cli-output.js
CHANGED
|
@@ -110,6 +110,11 @@ function withRepeatWarning(value) {
|
|
|
110
110
|
const status = String(envelope.status ?? "");
|
|
111
111
|
if (status !== "refused" && status !== "error" && status !== "uncertain")
|
|
112
112
|
return value;
|
|
113
|
+
// A5(GitLab #15):auth-config(凭据缺失/配置问题)不是参数形状问题——
|
|
114
|
+
// 「不要微调参数重试」的提示会把排查方向带偏,实测连续 6 次失败全在改形状。
|
|
115
|
+
const errorRecord = envelope.error && typeof envelope.error === "object" ? envelope.error : undefined;
|
|
116
|
+
if (typeof errorRecord?.code === "string" && errorRecord.code === "auth-config")
|
|
117
|
+
return value;
|
|
113
118
|
const result = envelope.result && typeof envelope.result === "object" ? envelope.result : undefined;
|
|
114
119
|
const blocker = typeof result?.blocker === "string" ? result.blocker : typeof envelope.blocker === "string" ? envelope.blocker : undefined;
|
|
115
120
|
const prior = priorRepeatedFailures(currentUsageCommand(), blocker);
|
package/dist/cli.js
CHANGED
|
@@ -232,6 +232,18 @@ async function cmdDoctor(store, environment, serviceCode) {
|
|
|
232
232
|
steps.push(`许可证 FAIL 许可证面探测失败:${redactText(error.message, [])}`);
|
|
233
233
|
}
|
|
234
234
|
}
|
|
235
|
+
// A5(GitLab #15):doctor 此前只体检当前环境,其他环境「列得出、没凭据」
|
|
236
|
+
// 完全不可见——补一行全环境凭据矩阵(存在性探测,不触碰值)。
|
|
237
|
+
try {
|
|
238
|
+
const envRows = store.listEnvironments().map((item) => ({ name: item.name, credential: store.hasCredential(item.name) }));
|
|
239
|
+
const missingEnvs = envRows.filter((row) => !row.credential);
|
|
240
|
+
steps.push(missingEnvs.length === 0
|
|
241
|
+
? `环境凭据 ok ${envRows.map((row) => row.name).join("、")} 全部在位`
|
|
242
|
+
: `环境凭据 WARN ${missingEnvs.map((row) => row.name).join("、")} 无凭据——调用会回 auth-config(不是参数问题);omc env list 可核对,omc setup 补`);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// 环境清单读不到不阻断体检主链路
|
|
246
|
+
}
|
|
235
247
|
// T09 — optional per-service credential shape check (--service <code>).
|
|
236
248
|
// Non-fatal: a masked/placeholder credential does not make the ENVIRONMENT
|
|
237
249
|
// unhealthy; it explains why that service's calls would 401/500. Findings
|
|
@@ -450,7 +462,12 @@ async function cmdCall(store, args) {
|
|
|
450
462
|
// every list read that only wanted one column paid a second artifact round.
|
|
451
463
|
// `--jq` is the same path projection plus `[]` fan-out; a bare `--jq` is the
|
|
452
464
|
// key inventory (the same muscle memory as `artifact get <id> --jq`).
|
|
453
|
-
const extractPaths = [...argvValues(args, "--extract"), ...argvValues(args, "--jq")]
|
|
465
|
+
const extractPaths = [...argvValues(args, "--extract"), ...argvValues(args, "--jq")]
|
|
466
|
+
// A3(GitLab #15):逗号多路径——一张 9 列表此前要 9 次 artifact get。
|
|
467
|
+
// 路径语法(点式/下标/切片/[]/按键)不含逗号,拆分无歧义。
|
|
468
|
+
.flatMap((path) => path.split(","))
|
|
469
|
+
.map((path) => path.trim())
|
|
470
|
+
.filter(Boolean);
|
|
454
471
|
// `artifact get` uses `--keys`; `call` only had `--extract-keys`, so the muscle
|
|
455
472
|
// memory failed with "unknown option '--keys'" (r16 P7). Accept both.
|
|
456
473
|
const extractKeys = args.includes("--extract-keys") || args.includes("--keys") || args.some((token, index) => token === "--jq" && (args[index + 1] === undefined || args[index + 1].startsWith("--")));
|
|
@@ -520,7 +537,7 @@ async function cmdCall(store, args) {
|
|
|
520
537
|
// Keep the REJECTED path on stderr only: the machine envelope must not leak
|
|
521
538
|
// host layout (ticket 103), so it carries the root plus the bare file name.
|
|
522
539
|
const rejectedName = typeof argsFile === "string" ? basename(resolve(configRoot, argsFile)) : "";
|
|
523
|
-
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: outOfRoot ? `--args-file 必须位于配置根内(配置根 ${configRoot}${rejectedName ? `;本次文件 ${rejectedName}` : ""}
|
|
540
|
+
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: outOfRoot ? `--args-file 必须位于配置根内(配置根 ${configRoot}${rejectedName ? `;本次文件 ${rejectedName}` : ""})。免落盘替代:--args - 从 stdin 读 JSON(echo '<json>' | omc call <tool> --args -),或把文件放进配置根后再引用;限制是为了防参数文件携带凭据被收进会话产物` : detail, retryable: false, ...(outOfRoot ? { configurationRoot: configRoot } : {}) }));
|
|
524
541
|
return CLI_EXIT_CODES.invalidInput;
|
|
525
542
|
}
|
|
526
543
|
const registry = buildToolRegistry();
|
|
@@ -713,9 +730,21 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
713
730
|
return emitTransportResult(result, transport);
|
|
714
731
|
}
|
|
715
732
|
if (options.jq !== undefined) {
|
|
716
|
-
|
|
733
|
+
// A3(GitLab #15):逗号多路径——一次取多列(projections 数组),单路径形态不变。
|
|
734
|
+
const jqPaths = options.jq.split(",").map((path) => path.trim()).filter(Boolean);
|
|
735
|
+
const artifactContent = artifact.content.toString("utf8") === "" ? undefined : content;
|
|
736
|
+
if (jqPaths.length > 1) {
|
|
737
|
+
const projections = jqPaths.map((path) => {
|
|
738
|
+
const projected = projectArtifactPath(artifactContent, path);
|
|
739
|
+
return projected.missed
|
|
740
|
+
? { path, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }
|
|
741
|
+
: { path, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}) };
|
|
742
|
+
});
|
|
743
|
+
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, projections, ...(projections.some((entry) => entry.pathMiss) ? { unanswered: true, hint: "逗号多路径中部分路径未命中(见各 projection.pathMiss/missedAt/available);路径语法:点式 a.b、下标 a[0]、切片 a[0:6]、数组映射 items[].fields.x、按键选择 rules[code=Create]" } : {}) }, transport);
|
|
744
|
+
}
|
|
745
|
+
const projected = projectArtifactPath(artifactContent, options.jq);
|
|
717
746
|
return emitTransportResult(projected.missed
|
|
718
|
-
? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: `路径未命中段 "${projected.at}"——available 是该层可用的键/范围;方括号下标(a[0])与点式(a.0)等价;[] 映射数组(items[].fields.x);按键选择 rules[code=Create]
|
|
747
|
+
? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: `路径未命中段 "${projected.at}"——available 是该层可用的键/范围;方括号下标(a[0])与点式(a.0)等价;[] 映射数组(items[].fields.x);按键选择 rules[code=Create];一次取多列用逗号分隔路径(--jq "a.b,c.d"),本参数是路径选择器、不是 jq 语言(不支持 | 与对象构造)` }
|
|
719
748
|
: { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}) }, transport);
|
|
720
749
|
}
|
|
721
750
|
// r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
|
|
@@ -1606,14 +1635,16 @@ async function main() {
|
|
|
1606
1635
|
commandExitCode = CLI_EXIT_CODES.invalidInput;
|
|
1607
1636
|
return;
|
|
1608
1637
|
}
|
|
1609
|
-
const rows = store.listEnvironments().map((item) => ({ name: item.name, baseUrl: item.profile.baseUrl, applicationCode: item.profile.applicationCode, isDefault: item.profile.isDefault === true, writable: item.profile.writable !== false }));
|
|
1638
|
+
const rows = store.listEnvironments().map((item) => ({ name: item.name, baseUrl: item.profile.baseUrl, applicationCode: item.profile.applicationCode, isDefault: item.profile.isDefault === true, writable: item.profile.writable !== false, credential: store.hasCredential(item.name) ? "ok" : "missing" }));
|
|
1610
1639
|
if (options.json)
|
|
1611
1640
|
process.stdout.write(JSON.stringify({ ok: true, environments: rows }, null, 2) + "\n");
|
|
1612
1641
|
else if (rows.length === 0)
|
|
1613
1642
|
console.error("(none configured; run omc setup)");
|
|
1643
|
+
// A5(GitLab #15):credential: missing 的环境列得出、调不通(auth-config)——
|
|
1644
|
+
// 标注出来,防把"没凭据"误判成"参数写错"。
|
|
1614
1645
|
else
|
|
1615
1646
|
for (const row of rows)
|
|
1616
|
-
console.error(`${row.name}${row.isDefault ? " (default)" : ""} ${row.baseUrl} app=${row.applicationCode}${row.writable ? "" : " [read-only]"}`);
|
|
1647
|
+
console.error(`${row.name}${row.isDefault ? " (default)" : ""} ${row.baseUrl} app=${row.applicationCode}${row.writable ? "" : " [read-only]"}${row.credential === "ok" ? "" : " [credential: missing → omc setup]"}`);
|
|
1617
1648
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
1618
1649
|
});
|
|
1619
1650
|
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); });
|
package/dist/config-store.js
CHANGED
|
@@ -195,6 +195,23 @@ export class ConfigStore {
|
|
|
195
195
|
throw new ConfigurationError(`credentials store has no entry for environment ${name}`);
|
|
196
196
|
return credential;
|
|
197
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* A5(GitLab #15):凭据**存在性**探测(只回 boolean,永不触碰值——凭据卫生)。
|
|
200
|
+
* env list / doctor 用它标注哪些环境真的可用,防「列得出、调不通」的方向性误判。
|
|
201
|
+
*/
|
|
202
|
+
hasCredential(name) {
|
|
203
|
+
if (process.env.OMC_CLOUDPIVOT_CREDENTIAL)
|
|
204
|
+
return true;
|
|
205
|
+
if (!existsSync(this.paths.credentialsPath))
|
|
206
|
+
return false;
|
|
207
|
+
try {
|
|
208
|
+
const parsed = JSON.parse(readFileSync(this.paths.credentialsPath, "utf8"));
|
|
209
|
+
return typeof parsed[name]?.username === "string" && typeof parsed[name]?.password === "string";
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
198
215
|
writeCredential(name, credential) {
|
|
199
216
|
mkdirSync(dirname(this.paths.credentialsPath), { recursive: true });
|
|
200
217
|
const existing = existsSync(this.paths.credentialsPath)
|
package/dist/endpoints.js
CHANGED
|
@@ -91,6 +91,11 @@ export const ENDPOINTS = {
|
|
|
91
91
|
businessRulePublish: "/api/api/app/businessrule/publish",
|
|
92
92
|
businessRuleNodeRunMap: "/api/api/app/businessrule/node_run_map",
|
|
93
93
|
businessRuleEnable: "/api/api/app/businessrule/enable",
|
|
94
|
+
// 8.6.26 BusinessRuleController.java:517 与 8.1.108 均提供 disable 端点
|
|
95
|
+
// (body={schemaCode,ruleCode})。rule.disable 直打此端点——此前走
|
|
96
|
+
// businessrule/update 拼载荷,正是 2026-11-03 issue #13 在 8.1 上踩出的
|
|
97
|
+
// 形状脆弱路径(残缺实体 → 无明细 50000)。
|
|
98
|
+
businessRuleDisable: "/api/api/app/businessrule/disable",
|
|
94
99
|
businessRuleDelete: "/api/api/app/businessrule/delete",
|
|
95
100
|
/**
|
|
96
101
|
* Manual execution of a CUSTOM-trigger rule (the list/form button path).
|
|
@@ -365,11 +365,37 @@ export function registerB2Lane(tools) {
|
|
|
365
365
|
const row = fetched.data;
|
|
366
366
|
if (!row)
|
|
367
367
|
return { serviceCode: args.serviceCode, methodCode: args.methodCode, present: false, absent: true, method: null };
|
|
368
|
+
const adapterCode = adapterCodeOf(serviceRow);
|
|
369
|
+
const inputParameters = normalizeParameterFaceReadback(adapterCode, decodeJsonColumn(row.inputParametersJson));
|
|
370
|
+
const outputParameters = normalizeParameterFaceReadback(adapterCode, decodeJsonColumn(row.outputParametersJson));
|
|
371
|
+
// A2 双面交叉校验(2026-11-03 issue,8.1.108 实测):get 面(按 id)可能把
|
|
372
|
+
// DB 实有的参数面读成 "[]"(同服务短参数正常、637 字节 3 参数读空——长度
|
|
373
|
+
// 相关的平台读缺陷)。list 面同列非空而 get 面为空时,标记
|
|
374
|
+
// parametersUnreadable 并附 list 面参数,禁止下游按「无参数」处理(曾导致
|
|
375
|
+
// 把配置正确的入参误判为缺失并重配)。
|
|
376
|
+
const crossCheck = (column, face, fromListKey) => {
|
|
377
|
+
const listFace = decodeJsonColumn(listed[column]);
|
|
378
|
+
const listCount = Array.isArray(listFace) ? listFace.length : 0;
|
|
379
|
+
if (listCount === 0 || face.length > 0)
|
|
380
|
+
return null;
|
|
381
|
+
return {
|
|
382
|
+
parametersUnreadable: true,
|
|
383
|
+
listFaceCount: listCount,
|
|
384
|
+
getFaceCount: face.length,
|
|
385
|
+
hint: `${column} 的 get 面(按 id)读回为空,但 list 面实有 ${listCount} 个参数——平台读缺陷(8.1.108 实测),勿按「无参数」处理;${fromListKey} 是 list 面读回的参数,以其为准复核`,
|
|
386
|
+
[fromListKey]: normalizeParameterFaceReadback(adapterCode, listFace),
|
|
387
|
+
};
|
|
388
|
+
};
|
|
389
|
+
const inputCross = crossCheck("inputParametersJson", Array.isArray(inputParameters) ? inputParameters : [], "inputParametersFromList");
|
|
390
|
+
const outputCross = crossCheck("outputParametersJson", Array.isArray(outputParameters) ? outputParameters : [], "outputParametersFromList");
|
|
368
391
|
const method = {
|
|
369
392
|
...row,
|
|
370
|
-
configJson: maskServiceConfigSecrets(normalizeMethodConfigReadback(
|
|
371
|
-
inputParameters
|
|
372
|
-
outputParameters
|
|
393
|
+
configJson: maskServiceConfigSecrets(normalizeMethodConfigReadback(adapterCode, decodeJsonColumn(row.configJson))),
|
|
394
|
+
inputParameters,
|
|
395
|
+
outputParameters,
|
|
396
|
+
...(inputCross || outputCross
|
|
397
|
+
? { readbackCaveat: { ...(inputCross ?? {}), ...(outputCross ?? {}) } }
|
|
398
|
+
: {}),
|
|
373
399
|
};
|
|
374
400
|
return { serviceCode: args.serviceCode, methodCode: args.methodCode, present: true, method };
|
|
375
401
|
}),
|
|
@@ -12,8 +12,12 @@ export const RULE_CREATE_BODY = Object.freeze({
|
|
|
12
12
|
status: RULE_STATUS.disabled,
|
|
13
13
|
defaultRule: false,
|
|
14
14
|
developmentMode: 0,
|
|
15
|
+
// 8.1 族实体校验要求结构化默认值(缺席 → 平台落 "",update 回无明细 50000);
|
|
16
|
+
// 8.6 同样接受。与设计器新建规则的落库形状一致(2026-11-03 issue A8)。
|
|
17
|
+
options: '{"ruleVariable":[]}',
|
|
18
|
+
extensionSetting: "{}",
|
|
15
19
|
});
|
|
16
|
-
export const RULE_PLATFORM_QUIRK = "enabled is the dispatch truth; status may still read DISABLED after enable — never infer enable state from status";
|
|
20
|
+
export const RULE_PLATFORM_QUIRK = "enabled is the dispatch truth; status may still read DISABLED after enable — never infer enable state from status. Reverse state exists too: enable returning errcode 0 while enabled stays 0 means the ENTITY is incomplete (missing schedulerSetting.methodMapping.methodCode or empty-string options/extensionSetting break the 8.1 update validation) — fix the entity via rule.save, not by retrying enable";
|
|
17
21
|
const RULE_NODE_GRAPH_EXAMPLE = '[{"nodeCode":"Start","nodeName":"开始","nodeType":"START",x,y,height,width},{"nodeCode":"UPDATE_1","nodeType":"UPDATE","targetObjectCode":"<target schemaCode>","inputParam":"_input","updateNodeType":"UPDATE_MODEL","execFormMapping":false,"filterCondition":{"mainConditions":[[{"targetSchemaDataItem":"<target field>","ruleConditionType":"EQ","ruleValueType":"DYNAMIC","currentSchemaDataItem":"<trigger field>"}]],"sheetConditions":[]},"dataActions":[{"targetDataItem":"<target field>","ruleValueType":"DYNAMIC","value":"","ruleActionType":"EQUALS","currentDataItemValue":"<trigger field>"}],x,y,height,width},{"nodeCode":"End","nodeName":"结束","nodeType":"END",x,y,height,width}]';
|
|
18
22
|
export const RULE_NODE_GRAPH_WIRE = Object.freeze({
|
|
19
23
|
inputParam: "_input",
|
|
@@ -8,7 +8,7 @@ import { applicationResolutionOf, applicationScopeHint, flattenTreeRows, resolve
|
|
|
8
8
|
import { executeRuleSaveWrite, ruleNodeGraphArg } from "../rule-support.js";
|
|
9
9
|
import { defineTool, ToolInputError, ToolRefusalError } from "../tool-types.js";
|
|
10
10
|
import { resolveRuleRowId } from "../identity.js";
|
|
11
|
-
import { wireCall, wireRows } from "../wire.js";
|
|
11
|
+
import { wireCall, wireCallWith404Fallback, wireRows } from "../wire.js";
|
|
12
12
|
import { BUSINESSRULE_LANE_CONTRACT } from "./businessrule-contract.js";
|
|
13
13
|
const RULE_LANE = BUSINESSRULE_LANE_CONTRACT.lane;
|
|
14
14
|
const RULE_TOOLS = BUSINESSRULE_LANE_CONTRACT.tools;
|
|
@@ -109,7 +109,16 @@ export function registerBusinessRuleLane(tools) {
|
|
|
109
109
|
capability: capabilityArg,
|
|
110
110
|
},
|
|
111
111
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
112
|
-
|
|
112
|
+
// 版本族备选(2026-11-03 issue A1):8.6 锚点走 runtime 面;8.1 族
|
|
113
|
+
// runtime 端点缺席(404)时下探 app 面 `businessrule/list_by_schemacode`。
|
|
114
|
+
// 仅 404 下探,401/500/业务码拒绝原样上抛。rule.save / rule.enable 的
|
|
115
|
+
// 读回本就走 app 面,两族都可用。下探发生时在结果里标注命中的端点族,
|
|
116
|
+
// 让调用方能看见自己不在锚点路径上(排查兼容问题时的关键信号)。
|
|
117
|
+
let endpointFamily = null;
|
|
118
|
+
const rows = wireRows((await wireCallWith404Fallback(session, [
|
|
119
|
+
{ path: ENDPOINTS.runtimeBusinessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } },
|
|
120
|
+
{ path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } },
|
|
121
|
+
], (failed, next) => { endpointFamily = { used: next.path, reason: `${failed.path} HTTP 404(端点在本版本族缺席,已下探)` }; })).data);
|
|
113
122
|
// N2 (r10 noise pass): the default row is the inventory face —
|
|
114
123
|
// code/name/state/publish-version/is-preset. The audit columns
|
|
115
124
|
// (id/createdTime/creator/developmentMode/templateType/…) only ride
|
|
@@ -286,6 +295,8 @@ export function registerBusinessRuleLane(tools) {
|
|
|
286
295
|
schemaCode: z.string().min(1).optional().describe("业务键定位:模型 code(与 ruleCode 同传,免去先 rule.list 取 id)"),
|
|
287
296
|
ruleCode: z.string().min(1).optional().describe("业务键定位:规则 code(如 Create/Update/自定义),与 schemaCode 同传"),
|
|
288
297
|
full: z.boolean().optional().describe("返回原始存储图(含确定性布局 x/y/width/height 与路由 points);默认返回语义图,确定性布局已投影去掉(可直接重存,保存时会重新生成布局)"),
|
|
298
|
+
keys: z.array(z.string().min(1)).min(1).optional().describe("工具经济(A6):只返回列出的顶层键(identity 键 id/code/schemaCode 恒在)。例 ['schedulerSetting'] 只看调度配置,不内联整图——对照生产规则形状时用"),
|
|
299
|
+
summary: z.boolean().optional().describe("工具经济(A6):只返回身份与体量摘要(节点数/路由数/schedulerSetting 是否存在),不含 nodes/routes 正文"),
|
|
289
300
|
capability: capabilityArg,
|
|
290
301
|
},
|
|
291
302
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
@@ -375,7 +386,7 @@ export function registerBusinessRuleLane(tools) {
|
|
|
375
386
|
catch {
|
|
376
387
|
// 体检失败不破坏读回本身
|
|
377
388
|
}
|
|
378
|
-
|
|
389
|
+
const payload = {
|
|
379
390
|
id: ruleId,
|
|
380
391
|
absent: false,
|
|
381
392
|
code: record.code,
|
|
@@ -402,6 +413,30 @@ export function registerBusinessRuleLane(tools) {
|
|
|
402
413
|
...(semanticFindings ? { semanticFindings } : {}),
|
|
403
414
|
...(staleRow ? { staleRow, effectiveId, staleRowMessage } : {}),
|
|
404
415
|
};
|
|
416
|
+
// A6 工具经济(2026-11-03 issue):rule.get 默认内联整图(nodes/routes/
|
|
417
|
+
// schedulerSetting 全量),对照生产规则形状时往往只要一个键。summary 给
|
|
418
|
+
// 身份+体量;keys 保留列出的键 + 恒定的 identity 键。两者都作用于投影后
|
|
419
|
+
// 的返回体,full 语义不变。
|
|
420
|
+
if (args.summary === true) {
|
|
421
|
+
return {
|
|
422
|
+
id: payload.id, code: payload.code, name: payload.name, schemaCode: payload.schemaCode,
|
|
423
|
+
bizRuleType: payload.bizRuleType, enabled: payload.enabled, ruleStatus: payload.ruleStatus,
|
|
424
|
+
defaultRule: payload.defaultRule, ruleVersion: payload.ruleVersion,
|
|
425
|
+
nodeCount: Array.isArray(payload.nodes) ? payload.nodes.length : 0,
|
|
426
|
+
routeCount: Array.isArray(payload.routes) ? payload.routes.length : 0,
|
|
427
|
+
schedulerSettingPresent: payload.schedulerSetting !== null && payload.schedulerSetting !== undefined,
|
|
428
|
+
schedulerSettingDataSourceType: payload.schedulerSetting && typeof payload.schedulerSetting === "object" && !Array.isArray(payload.schedulerSetting)
|
|
429
|
+
? payload.schedulerSetting.dataSourceType
|
|
430
|
+
: undefined,
|
|
431
|
+
...(payload.statusLag ? { statusLag: payload.statusLag, statusNote: payload.statusNote } : {}),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (args.keys !== undefined) {
|
|
435
|
+
const identity = new Set(["id", "code", "schemaCode"]);
|
|
436
|
+
const wanted = new Set([...args.keys, ...identity]);
|
|
437
|
+
return Object.fromEntries(Object.entries(payload).filter(([key]) => wanted.has(key)));
|
|
438
|
+
}
|
|
439
|
+
return payload;
|
|
405
440
|
}, { full: args.full === true, compact: ruleCompactProjection, target: `rule.get:${args.id ?? `${args.schemaCode}/${args.ruleCode}`}` }),
|
|
406
441
|
}));
|
|
407
442
|
writeTool(tools, "rule.save", RULE_LANE, RULE_TOOLS["rule.save"].scope, RULE_TOOLS["rule.save"].description, {
|
|
@@ -119,7 +119,14 @@ export async function executeRuleEnableWrite(session, args) {
|
|
|
119
119
|
message: `规则 ${args.ruleCode} 启用时被平台自检拒绝(550018)。${detail.nodeChecks.length > 0 ? `问题节点:${JSON.stringify(detail.nodeChecks).slice(0, 600)}` : "先用 field.list(all:true) 核对引用的字段是否已发布。"}`,
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
-
|
|
122
|
+
// 版本族备选(2026-11-03 issue A1 更正,8.1.108 实测):8.1 族没有
|
|
123
|
+
// `businessrule/publish` 端点(HTTP 404)。已验证的等价路径是
|
|
124
|
+
// `businessrule/update` 携完整实体 + enabled:true —— 实体完整时一次调用
|
|
125
|
+
// 同时翻转 enabled 与 status(见 issue 复现脚本)。仅 404 下探,其余原样抛。
|
|
126
|
+
const publishEndpointAbsent = error instanceof WireError && error.kind === "http-status" && error.diagnostic?.httpStatus === 404;
|
|
127
|
+
if (!publishEndpointAbsent)
|
|
128
|
+
throw error;
|
|
129
|
+
published = await wireCall(session, { path: ENDPOINTS.businessRuleUpdate, method: "POST", body: { ...full, enabled: true, status: "ENABLED" } });
|
|
123
130
|
}
|
|
124
131
|
let enableErrcode = 0;
|
|
125
132
|
let alreadyEnabled = false;
|
|
@@ -147,7 +154,10 @@ export function registerRuleLifecycleLane(tools) {
|
|
|
147
154
|
const row = wireRows(list.data).find((item) => item.code === args.ruleCode);
|
|
148
155
|
if (!row?.id)
|
|
149
156
|
throw new WireError("business", `rule ${args.ruleCode} not found for disable`, 302024);
|
|
150
|
-
|
|
157
|
+
// 2026-11-04(源码实证,GitLab #13):8.6 与 8.1 都有 businessrule/disable
|
|
158
|
+
// 端点(body={schemaCode,ruleCode}),UI 开关同款——直打,不再用 update
|
|
159
|
+
// 拼载荷(残缺实体在 8.1 会回无明细 50000,正是 issue 的 disable 故障形态)。
|
|
160
|
+
const performed = await wireCall(session, { path: ENDPOINTS.businessRuleDisable, method: "POST", body: { schemaCode: args.schemaCode, ruleCode: args.ruleCode } });
|
|
151
161
|
const readback = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode: args.schemaCode } });
|
|
152
162
|
const after = wireRows(readback.data).find((item) => item.code === args.ruleCode);
|
|
153
163
|
return { performed: { errcode: performed.errcode }, readback: { enabled: after?.enabled === true } };
|
|
@@ -12,6 +12,40 @@ import { CHILD_TABLE_PROPERTY_TYPE, diagnoseApprovalSubmitIdentity, runtimeWrite
|
|
|
12
12
|
import { getReplayToken, searchWorkItems, wireString } from "../workflow-support.js";
|
|
13
13
|
import { ENGINE_OWNED_KEYS, RUNTIME_DATA_LANE_CONTRACT, RUNTIME_DATA_WIRE, runtimeFormDataArg, } from "./runtime-data-contract.js";
|
|
14
14
|
import { SYSTEM_FIELD_CODES } from "../field-families.js";
|
|
15
|
+
// A2(GitLab #15,2026-11-04):单选/字典字段(RADIO/CHECKBOX/DROPDOWN_BOX/
|
|
16
|
+
// DROPDOWN_MULTI_BOX)在运行面查询按**字典 key**匹配,不按显示值——按「已审核」
|
|
17
|
+
// 这类显示值过滤会静默 0 条;加 `_key` 后缀的字段名则不在发布元数据里,直接拒。
|
|
18
|
+
// 两个方向都给写法提示,防"total:0 被读成查不到数据"(实测 313 条被误读)。
|
|
19
|
+
const CHOICE_FILTER_PROPERTY_TYPES = new Set([12, 13, 14, 15]);
|
|
20
|
+
function collectConditionLeafFields(condition) {
|
|
21
|
+
const codes = [];
|
|
22
|
+
const walk = (node) => {
|
|
23
|
+
if (Array.isArray(node)) {
|
|
24
|
+
node.forEach(walk);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (node === null || typeof node !== "object")
|
|
28
|
+
return;
|
|
29
|
+
const leaf = node;
|
|
30
|
+
if (typeof leaf.propertyCode === "string" && leaf.propertyCode)
|
|
31
|
+
codes.push(leaf.propertyCode);
|
|
32
|
+
Object.values(leaf).forEach(walk);
|
|
33
|
+
};
|
|
34
|
+
walk(condition);
|
|
35
|
+
return [...new Set(codes)];
|
|
36
|
+
}
|
|
37
|
+
function choiceKeySuffixHint(missing, typeByCode) {
|
|
38
|
+
const bases = missing
|
|
39
|
+
.filter((code) => code.endsWith("_key"))
|
|
40
|
+
.map((code) => code.slice(0, -"_key".length))
|
|
41
|
+
.filter((base) => CHOICE_FILTER_PROPERTY_TYPES.has(typeByCode.get(base) ?? -1));
|
|
42
|
+
if (bases.length === 0)
|
|
43
|
+
return "";
|
|
44
|
+
return `;${bases.join("、")} 是单选/多选(字典来源)字段:列表虽同时返回 code 与 code_key,但过滤要用原字段名 + 字典 key 值(不是 _key 后缀的字段名、也不是显示值);key 可用 dictionary.names 查,或改 rawCondition 绕过字段元数据校验`;
|
|
45
|
+
}
|
|
46
|
+
function choiceFilterValueHint(fields) {
|
|
47
|
+
return `total:0 且过滤字段 ${fields.join("、")} 是单选/多选(字典来源):平台查询按字典 key 匹配而非显示值——若 value 用的是显示值(如「已审核」),换字典 key(如 YSH,dictionary.names 可查)再试;确认确无数据再下「查不到」结论;也可用 rawCondition 或先无过滤查询复核 total`;
|
|
48
|
+
}
|
|
15
49
|
const RUNTIME_LANE = RUNTIME_DATA_LANE_CONTRACT.lane;
|
|
16
50
|
const RUNTIME_TOOLS = RUNTIME_DATA_LANE_CONTRACT.tools;
|
|
17
51
|
/**
|
|
@@ -789,6 +823,8 @@ export function registerRuntimeDataLane(tools) {
|
|
|
789
823
|
throw error;
|
|
790
824
|
}
|
|
791
825
|
return governedRead(ctx, self, args, async (session) => {
|
|
826
|
+
// A2:DSL 过滤命中的单选/字典字段(total:0 时给值写法提示)
|
|
827
|
+
let choiceFilterFields = [];
|
|
792
828
|
// Published field metadata is the sole source for filter propertyType
|
|
793
829
|
// injection AND for the projectionHint below. Fetch at most once per
|
|
794
830
|
// call: the DSL path needs it up front; the hint path asks only after
|
|
@@ -813,9 +849,13 @@ export function registerRuntimeDataLane(tools) {
|
|
|
813
849
|
status: "refused",
|
|
814
850
|
blocker: "unknown-filter-field",
|
|
815
851
|
missing: injected.missing,
|
|
816
|
-
message: `filter fields not found in published field metadata: ${injected.missing.join(", ")}; the runtime query requires each leaf's propertyType from field metadata (no guessing); system/foreign fields may use rawCondition
|
|
852
|
+
message: `filter fields not found in published field metadata: ${injected.missing.join(", ")}; the runtime query requires each leaf's propertyType from field metadata (no guessing); system/foreign fields may use rawCondition` + choiceKeySuffixHint(injected.missing, typeByCode),
|
|
817
853
|
};
|
|
818
854
|
condition = injected.condition;
|
|
855
|
+
// A2(GitLab #15):单选/字典字段按显示值过滤会静默 0 条(平台查询按
|
|
856
|
+
// 字典 key 匹配)。带出这些字段,total:0 时给写法提示,防"查不到数据"
|
|
857
|
+
// 误报(实测 313 条被 total:0 误读成"今天没有已审核单")。
|
|
858
|
+
choiceFilterFields = collectConditionLeafFields(condition).filter((code) => CHOICE_FILTER_PROPERTY_TYPES.has(typeByCode.get(code) ?? -1));
|
|
819
859
|
}
|
|
820
860
|
const requestedFields = args.displayFields ?? args.columns;
|
|
821
861
|
const fields = requestedFields && requestedFields.length > 0 ? requestedFields : undefined;
|
|
@@ -840,7 +880,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
840
880
|
const firstPage = extractBizObjectPage(first.data);
|
|
841
881
|
const total = Number(firstPage.total ?? 0);
|
|
842
882
|
if (total === 0 || firstPage.content.length === 0)
|
|
843
|
-
return { schemaCode: args.schemaCode, total: 0, page: "all", size: 0, pagesFetched: 0, items: [] };
|
|
883
|
+
return { schemaCode: args.schemaCode, total: 0, page: "all", size: 0, pagesFetched: 0, items: [], ...(choiceFilterFields.length > 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}) };
|
|
844
884
|
const rows = [...firstPage.content];
|
|
845
885
|
const target = Math.min(total, CAP);
|
|
846
886
|
let pageIndex = 1;
|
|
@@ -860,7 +900,22 @@ export function registerRuntimeDataLane(tools) {
|
|
|
860
900
|
const selected = fields ?? Object.keys(data).filter((field) => !["id", "bizObjectId", "schemaCode", "sequenceStatus", "createdTime", "updatedTime", "creator", "updater"].includes(field));
|
|
861
901
|
return { id: row.bizObjectId ?? row.id ?? data.id, fields: Object.fromEntries(selected.map((field) => [field, data[field] ?? null])) };
|
|
862
902
|
});
|
|
863
|
-
|
|
903
|
+
// A6(GitLab #15):all:true 路径此前完全没有 projectionHint——未配置列
|
|
904
|
+
// 的 null/省略同样要点名,防「全量拉取却被投影坑」(实测 orderChange)。
|
|
905
|
+
let allHint;
|
|
906
|
+
if (items.length > 0) {
|
|
907
|
+
try {
|
|
908
|
+
const typeByCode = await publishedTypeByCode();
|
|
909
|
+
const returnedKeys = new Set(Object.keys(items[0]?.fields ?? {}));
|
|
910
|
+
const hintColumns = [...typeByCode.keys()].filter((field) => !SYSTEM_FIELD_CODES.has(field) && (!returnedKeys.has(field) || items.every((item) => item.fields[field] === null)));
|
|
911
|
+
if (hintColumns.length > 0)
|
|
912
|
+
allHint = `这些列在默认视图未配置(值可能存在但列表投影为 null 或整列省略):${hintColumns.join("、")};先用 listview.configure 配置列,或用 data.load 读单条权威值`;
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
// 元数据读失败不阻断数据返回
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return { schemaCode: args.schemaCode, total, page: "all", size: rows.length, pagesFetched: pageIndex, ...(capped ? { capped: true, capNotice: `结果超过安全上限 ${CAP},已截断;请加 filters 分批` } : {}), ...(allHint ? { projectionHint: allHint } : {}), items };
|
|
864
919
|
}
|
|
865
920
|
let result;
|
|
866
921
|
try {
|
|
@@ -868,7 +923,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
868
923
|
}
|
|
869
924
|
catch (error) {
|
|
870
925
|
if (error instanceof WireError && error.kind === "business" && error.errcode === RUNTIME_DATA_WIRE.emptyModelErrcode) {
|
|
871
|
-
return { schemaCode: args.schemaCode, total: 0, totalPages: 0, page: args.page ?? 0, size: args.size ?? 10, items: [] };
|
|
926
|
+
return { schemaCode: args.schemaCode, total: 0, totalPages: 0, page: args.page ?? 0, size: args.size ?? 10, items: [], ...(choiceFilterFields.length > 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}) };
|
|
872
927
|
}
|
|
873
928
|
// r30: a MISSING model answers this face with a generic 50000
|
|
874
929
|
// 服务异常 — indistinguishable from a real outage. One field-face
|
|
@@ -921,6 +976,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
921
976
|
// (live 2026-10-18: total:0 was read as "no residue" while 9 drafts
|
|
922
977
|
// existed).
|
|
923
978
|
filter: { excludedDraft: true, hint: "默认不含 DRAFT(total 亦仅计非草稿);核对/清理残留请加 all:true" },
|
|
979
|
+
...(choiceFilterFields.length > 0 && Number(page.total ?? 0) === 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}),
|
|
924
980
|
totalPages: page.totalPages,
|
|
925
981
|
page: page.page ?? args.page ?? 0,
|
|
926
982
|
size: page.size ?? args.size ?? 20,
|
|
@@ -942,6 +998,26 @@ export function registerRuntimeDataLane(tools) {
|
|
|
942
998
|
const typeByCode = await publishedTypeByCode();
|
|
943
999
|
return candidates.filter((field) => typeByCode.has(field));
|
|
944
1000
|
};
|
|
1001
|
+
// A6(GitLab #15,2026-11-04):未配置列有两种投影形态——值为 null,或
|
|
1002
|
+
// **整键省略**(orderChange 实测:sequenceNo 在 data.list 里根本不出现,
|
|
1003
|
+
// data.load 却有值,被误读成「单号为空」)。省略检测需要发布元数据
|
|
1004
|
+
// (fieldList 有 60s wire 缓存,DSL 过滤路径本就已取);两种形态合并进 hint。
|
|
1005
|
+
const projectionHintFor = async () => {
|
|
1006
|
+
if (items.length === 0)
|
|
1007
|
+
return undefined;
|
|
1008
|
+
const typeByCode = await publishedTypeByCode();
|
|
1009
|
+
const returnedKeys = new Set(Object.keys(items[0]?.fields ?? {}));
|
|
1010
|
+
const nullColumns = fields && fields.length > 0
|
|
1011
|
+
? fields.filter((field) => typeByCode.has(field) && items.every((item) => item.fields[field] === null))
|
|
1012
|
+
: [...returnedKeys].filter((field) => !SYSTEM_FIELD_CODES.has(field) && typeByCode.has(field) && items.every((item) => item.fields[field] === null));
|
|
1013
|
+
const omittedColumns = !fields
|
|
1014
|
+
? [...typeByCode.keys()].filter((field) => !SYSTEM_FIELD_CODES.has(field) && !returnedKeys.has(field))
|
|
1015
|
+
: [];
|
|
1016
|
+
const hintColumns = [...new Set([...nullColumns, ...omittedColumns])];
|
|
1017
|
+
if (hintColumns.length === 0)
|
|
1018
|
+
return undefined;
|
|
1019
|
+
return `这些列在默认视图未配置(值可能存在但列表投影为 null 或整列省略):${hintColumns.join("、")};先用 listview.configure 配置列,或用 data.load 读单条权威值`;
|
|
1020
|
+
};
|
|
945
1021
|
if (fields &&
|
|
946
1022
|
fields.length > 0 &&
|
|
947
1023
|
items.length > 0 &&
|
|
@@ -955,9 +1031,9 @@ export function registerRuntimeDataLane(tools) {
|
|
|
955
1031
|
// business fields absent from the list view render as null in every
|
|
956
1032
|
// row with no explanation. Name the null'd business columns so the
|
|
957
1033
|
// agent reads data.load instead of misdiagnosing "data lost".
|
|
958
|
-
const
|
|
959
|
-
if (
|
|
960
|
-
readback.projectionHint =
|
|
1034
|
+
const hint = await projectionHintFor();
|
|
1035
|
+
if (hint)
|
|
1036
|
+
readback.projectionHint = hint;
|
|
961
1037
|
}
|
|
962
1038
|
return readback;
|
|
963
1039
|
}, { full: args.full === true, target: `data.list:${JSON.stringify({ schemaCode: args.schemaCode, queryCode: args.queryCode ?? args.schemaCode, filter: args.filter, filters: args.filters, filterGroup: args.filterGroup, rawCondition: args.rawCondition, displayFields: args.displayFields, columns: args.columns, page: args.page ?? 0, size: args.size ?? 10 })}` });
|
package/dist/path-projection.js
CHANGED
|
@@ -144,7 +144,7 @@ function argvValues(args, name) {
|
|
|
144
144
|
return values;
|
|
145
145
|
}
|
|
146
146
|
/** Shared wording for every projection miss, so all three faces teach the same syntax. */
|
|
147
|
-
const PATH_MISS_HINT = "路径未命中(这一层没有该段)。可用写法:点式 a.b、下标 a[0]、切片 a[0:6]、数组映射 items[].fields.x、按键选择 rules[code=Create] 或 data.activities[code=A1](数组与 index→条目对象都支持;键名可省 activityCode/nodeCode 等别名;返回第一个匹配项)。available
|
|
147
|
+
const PATH_MISS_HINT = "路径未命中(这一层没有该段)。可用写法:点式 a.b、下标 a[0]、切片 a[0:6]、数组映射 items[].fields.x、按键选择 rules[code=Create] 或 data.activities[code=A1](数组与 index→条目对象都支持;键名可省 activityCode/nodeCode 等别名;返回第一个匹配项)。available 是该层真实存在的键。注意:本参数是路径选择器、不是 jq 语言(不支持 | 管道与对象构造);一次取多列用逗号分隔(--jq \"a.b,c.d\")或重复传 --jq。";
|
|
148
148
|
/**
|
|
149
149
|
* `call --extract/--extract-keys`: resolve paths inside the result's own
|
|
150
150
|
* artifact (or inside the result object when the tool returned no artifact).
|
package/dist/read.js
CHANGED
|
@@ -54,7 +54,7 @@ export async function governedRead(ctx, tool, args, execute, options = {}) {
|
|
|
54
54
|
const targetFingerprint = sha256(result);
|
|
55
55
|
const cached = ctx.readbackCache.get(target);
|
|
56
56
|
if (ctx.readbackCacheBypass === 0 && cached && cached.environmentFingerprint === environmentFingerprint && cached.targetFingerprint === targetFingerprint && options.full !== true)
|
|
57
|
-
return { status: "ok", unchanged: true, detailFile: cached.detailFile, detailSha256: cached.detailSha256, environmentFingerprint };
|
|
57
|
+
return { status: "ok", unchanged: true, detailFile: cached.detailFile, detailSha256: cached.detailSha256, ...(cached.shapePlaceholders ?? {}), environmentFingerprint };
|
|
58
58
|
const detail = { status: "ok", ...result, environmentFingerprint };
|
|
59
59
|
// 2026-11-01 预算批 v2(r38 usage 实证):1-4KB 中结果 59 次/135KB≈37% 全内联,
|
|
60
60
|
// model.inspect 17 次全部卡在旧门下。降到 2048;compact 对配置清单保形为码表
|
|
@@ -62,19 +62,26 @@ export async function governedRead(ctx, tool, args, execute, options = {}) {
|
|
|
62
62
|
const large = jsonBytes(result) > 2048;
|
|
63
63
|
const fileName = `${tool.name.replace(/[^a-zA-Z0-9._-]/g, "_")}-${sha256(target).slice(0, 16)}.json`;
|
|
64
64
|
const artifactRef = writeArtifact(ctx, fileName, detail);
|
|
65
|
-
|
|
65
|
+
const cacheEntry = { environmentFingerprint, targetFingerprint, detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256 };
|
|
66
66
|
if (options.full === true) {
|
|
67
67
|
const rawBytes = jsonBytes(result);
|
|
68
68
|
// Small raw bodies keep the old inline behaviour; large ones are dieted
|
|
69
69
|
// (see FULL_INLINE_CEILING_BYTES). The raw body stays fully reachable via
|
|
70
70
|
// the artifact, so no capability is lost — only the inline copy is skipped.
|
|
71
|
-
if (rawBytes <= FULL_INLINE_CEILING_BYTES)
|
|
71
|
+
if (rawBytes <= FULL_INLINE_CEILING_BYTES) {
|
|
72
|
+
ctx.readbackCache.set(target, cacheEntry);
|
|
72
73
|
return { ...detail, detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256 };
|
|
74
|
+
}
|
|
73
75
|
const compact = options.compact ? options.compact(result, session) : compactReadback(result);
|
|
76
|
+
// A1:外置与内联同形——被折叠的 payload 键补回空占位;缓存同样记占位,
|
|
77
|
+
// unchanged 二次读回时不丢形状(GitLab #15)。
|
|
78
|
+
const placeholders = externalizedShapePlaceholders(result, droppedShapeKeys(result, compact));
|
|
79
|
+
ctx.readbackCache.set(target, { ...cacheEntry, shapePlaceholders: placeholders });
|
|
74
80
|
return {
|
|
75
81
|
status: "ok",
|
|
76
82
|
...passthroughScalars(result),
|
|
77
83
|
...compact,
|
|
84
|
+
...placeholders,
|
|
78
85
|
full: true,
|
|
79
86
|
fullOmitted: true,
|
|
80
87
|
fullBytes: rawBytes,
|
|
@@ -84,10 +91,14 @@ export async function governedRead(ctx, tool, args, execute, options = {}) {
|
|
|
84
91
|
detailSha256: artifactRef.detailSha256,
|
|
85
92
|
};
|
|
86
93
|
}
|
|
87
|
-
if (!large)
|
|
94
|
+
if (!large) {
|
|
95
|
+
ctx.readbackCache.set(target, cacheEntry);
|
|
88
96
|
return { status: "ok", ...result, environmentFingerprint };
|
|
97
|
+
}
|
|
89
98
|
const compact = options.compact ? options.compact(result, session) : compactReadback(result);
|
|
90
|
-
|
|
99
|
+
const placeholders = externalizedShapePlaceholders(result, droppedShapeKeys(result, compact));
|
|
100
|
+
ctx.readbackCache.set(target, { ...cacheEntry, shapePlaceholders: placeholders });
|
|
101
|
+
return { status: "ok", ...passthroughScalars(result), ...compact, ...placeholders, environmentFingerprint, detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256 };
|
|
91
102
|
}
|
|
92
103
|
catch (error) {
|
|
93
104
|
if (isUncertainWireError(error))
|
|
@@ -97,6 +108,22 @@ export async function governedRead(ctx, tool, args, execute, options = {}) {
|
|
|
97
108
|
throw error;
|
|
98
109
|
}
|
|
99
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* A1(GitLab #15,2026-11-04):外置结果与内联同形。compact 把 BULKY_READ_KEYS
|
|
113
|
+
* (items/data/…)折叠成 `*Count` 后,按内联形状取数的脚本读 `result.items` /
|
|
114
|
+
* `result.data` 直接 TypeError——外置只是把数据搬进 artifact,不应改变返回形状。
|
|
115
|
+
* 被折叠的键补回为空占位(原值是数组→[],否则 null)并标记 truncatedKeys;
|
|
116
|
+
* 真实数据在 detailFile 指向的 artifact 里,根键与内联一致(list=items、load=data)。
|
|
117
|
+
*/
|
|
118
|
+
function externalizedShapePlaceholders(result, dropped) {
|
|
119
|
+
const placeholders = { truncatedKeys: [...dropped], truncated: true };
|
|
120
|
+
for (const key of dropped)
|
|
121
|
+
placeholders[key] = Array.isArray(result[key]) ? [] : null;
|
|
122
|
+
return placeholders;
|
|
123
|
+
}
|
|
124
|
+
function droppedShapeKeys(result, payload) {
|
|
125
|
+
return Object.keys(result).filter((key) => BULKY_READ_KEYS.has(key) && !(key in payload));
|
|
126
|
+
}
|
|
100
127
|
function compactReadback(result) {
|
|
101
128
|
const compact = {};
|
|
102
129
|
for (const [key, value] of Object.entries(result)) {
|
|
@@ -167,6 +167,88 @@ const ruleTimingTask = Object.freeze({
|
|
|
167
167
|
],
|
|
168
168
|
requiresEnvironmentDerivedIds: false,
|
|
169
169
|
});
|
|
170
|
+
const ruleTimingBizService = Object.freeze({
|
|
171
|
+
id: "rule-timing-bizservice",
|
|
172
|
+
title: "定时任务:定时调用业务方法(SCHEDULER-only 图 + BIZ_SERVICE,生产 106 条实证形状)",
|
|
173
|
+
aliases: ["定时调服务", "定时调接口", "定时调用方法", "BIZ_SERVICE", "定时调方法"],
|
|
174
|
+
status: "requires-prior-readback",
|
|
175
|
+
tool: "rule.save",
|
|
176
|
+
args: {
|
|
177
|
+
environment,
|
|
178
|
+
schemaCode: model,
|
|
179
|
+
ruleCode: "<customRuleCode e.g. nightly_ledger_rebuild>",
|
|
180
|
+
name: "每日台账重建",
|
|
181
|
+
nodeGraph: {
|
|
182
|
+
bizRuleType: 2,
|
|
183
|
+
// 与 rule-timing-task 的关键差异:**无动作节点**——方法挂在 schedulerSetting
|
|
184
|
+
// 上,不是 SCHEDULER 后面再接 UPDATE/BIZ_ACTION。生产 106 条「定时调服务」
|
|
185
|
+
// 全部是这个形状(Start → SCHEDULER → End)。
|
|
186
|
+
nodes: [
|
|
187
|
+
{ nodeCode: "Start", nodeName: "开始", nodeType: "START" },
|
|
188
|
+
{ nodeCode: "SCHEDULER", nodeName: "定时作业", nodeType: "SCHEDULER", isSystemNode: true, nodeCategory: "systemNode" },
|
|
189
|
+
{ nodeCode: "End", nodeName: "结束", nodeType: "END" },
|
|
190
|
+
],
|
|
191
|
+
routes: [
|
|
192
|
+
{ preNode: "Start", postNode: "SCHEDULER", routeCondition: true },
|
|
193
|
+
{ preNode: "SCHEDULER", postNode: "End", routeCondition: true },
|
|
194
|
+
],
|
|
195
|
+
schedulerSetting: {
|
|
196
|
+
triggerTimeType: "SELECTED_TIME",
|
|
197
|
+
cycleType: "DAY",
|
|
198
|
+
executeType: "CYCLE",
|
|
199
|
+
execCycle: 1,
|
|
200
|
+
hour: 2,
|
|
201
|
+
minute: 0,
|
|
202
|
+
second: 0,
|
|
203
|
+
startTime: "<yyyy-MM-dd HH:mm:ss,未来时刻>",
|
|
204
|
+
endType: "NEVER",
|
|
205
|
+
// 定时调用业务集成方法:以方法为数据源,调度到点直接调用。
|
|
206
|
+
dataSourceType: "BIZ_SERVICE",
|
|
207
|
+
// methodCode 是**应用编码(appCode)**,不是方法 code——serviceCode+
|
|
208
|
+
// serviceMethodCode 定位方法,methodCode 定位应用。缺它:8.6 create 容忍、
|
|
209
|
+
// 8.1 update 回无明细 50000 且 enable 只翻 status 不翻 enabled
|
|
210
|
+
// (2026-11-03 issue A7/A8 实证;写前 gate 会直接拒绝缺失形态)。
|
|
211
|
+
methodMapping: {
|
|
212
|
+
schemaCode: model,
|
|
213
|
+
serviceCode: "<已注册服务 code>",
|
|
214
|
+
serviceMethodCode: "<方法 code>",
|
|
215
|
+
methodCode: "<应用编码 appCode>",
|
|
216
|
+
inputMappings: [
|
|
217
|
+
{ serviceMethodParameterCode: "<参数 code,与方法声明逐字一致>", codeType: 0, bizCode: "<常量值>", required: false },
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
latestVersion: true,
|
|
221
|
+
consolidation: false,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
publish: true,
|
|
225
|
+
},
|
|
226
|
+
preconditions: [
|
|
227
|
+
"目标业务集成服务与方法**已注册且已发布**(service.list / service.get 核对)",
|
|
228
|
+
"`methodCode`(应用编码)已知——不确定时先 `rule.get --keys schedulerSetting` 一条同族生产定时规则对照 methodMapping 形状",
|
|
229
|
+
"本环境需 `cloudpivot.task.enabled=true`(否则平台不建调度任务)",
|
|
230
|
+
],
|
|
231
|
+
conditionalRequirements: [
|
|
232
|
+
"**不要**套用 `rule-timing-task`(GET_LIST + UPDATE 动作节点)的形状——那是「定时改数据」,本配方是「定时调方法」;两者 schedulerSetting.dataSourceType 与节点图都不同。",
|
|
233
|
+
"`methodMapping.inputMappings[]` 的 `serviceMethodParameterCode` 必须与方法声明的参数 code 逐字一致(method.get 读参数面核对,readbackCaveat 出现时以 inputParametersFromList 为准);无参方法传 `[]`。",
|
|
234
|
+
"`latestVersion` 必须 true(false 走旧 builder → publish/update 50000,与 rule-timing-task 同坑)。",
|
|
235
|
+
"保存≠生效:`rule.save` 后必须 `rule.enable`;enable 回成功但 DB `enabled` 仍为 0 = 实体残缺(methodCode 缺失/options 空串),回 rule.save 修实体,不是重试 enable。",
|
|
236
|
+
],
|
|
237
|
+
priorToolReferences: [
|
|
238
|
+
{ tool: "service.list", args: { environment }, purpose: "确认服务已注册" },
|
|
239
|
+
{ tool: "method.get", args: { environment, serviceCode: "<svc>", methodCode: "<method>" }, purpose: "读参数面拿 serviceMethodParameterCode(readbackCaveat.parametersUnreadable 出现时以 inputParametersFromList 为准)" },
|
|
240
|
+
{ tool: "rule.get", args: { environment, schemaCode: model, ruleCode: "<同族生产规则>", keys: ["schedulerSetting"] }, purpose: "不确定 methodMapping 形状时对照生产实证形状" },
|
|
241
|
+
],
|
|
242
|
+
dryRun: { supported: true, command: `omc call rule.save --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 跑图校验 + BIZ_SERVICE 写前 gate(methodMapping 三键非空),不写不发布" },
|
|
243
|
+
expectedReadback: { readback: { published: true, enabled: false, next: "rule.enable;到点后以平台日志/方法侧效果核验,不认返回值" } },
|
|
244
|
+
refusalRecovery: [
|
|
245
|
+
{ code: "rule-scheduler-bizservice-mapping", action: "dataSourceType:BIZ_SERVICE 必须带 methodMapping{serviceCode,serviceMethodCode,methodCode};methodCode 是应用编码(appCode)。对照 rule.get 的生产规则形状补齐" },
|
|
246
|
+
{ code: "rule-name-invalid", action: "规则名仅支持中文/字母/数字、≤50 字符(平台设计器约束)" },
|
|
247
|
+
{ code: "rule-graph-scheduler-setting-shape", action: "schedulerSetting 必须是 nodeGraph.schedulerSetting 上的对象,不是 SCHEDULER 节点字段" },
|
|
248
|
+
...commonRecovery,
|
|
249
|
+
],
|
|
250
|
+
requiresEnvironmentDerivedIds: true,
|
|
251
|
+
});
|
|
170
252
|
const dataDuplicateFlag = Object.freeze({
|
|
171
253
|
id: "data-duplicate-flag",
|
|
172
254
|
title: "字段唯一性防重打标(8.6.26 实证唯一可靠路径):预置 Create 规则挂 BIZ_ACTION 自调二开 markDuplicates(规则图内 QUERY→UPDATE 打标会静默空转)",
|
|
@@ -328,6 +410,6 @@ const ruleBizactionInvokeService = Object.freeze({
|
|
|
328
410
|
requiresEnvironmentDerivedIds: true,
|
|
329
411
|
});
|
|
330
412
|
export const BUSINESSRULE_METADATA = Object.freeze({
|
|
331
|
-
"rule.save": Object.freeze({ recipes: [ruleGraph, ruleBranchGraph, ruleTimingTask, dataDuplicateFlag, ruleCrossModelCreate, ruleBizactionInvokeService], relatedTools: ["field.list", "rule.get", "rule.enable", "service.get"] }),
|
|
413
|
+
"rule.save": Object.freeze({ recipes: [ruleGraph, ruleBranchGraph, ruleTimingTask, ruleTimingBizService, dataDuplicateFlag, ruleCrossModelCreate, ruleBizactionInvokeService], relatedTools: ["field.list", "rule.get", "rule.enable", "service.get"] }),
|
|
332
414
|
});
|
|
333
415
|
//# sourceMappingURL=businessrule.js.map
|
package/dist/rule-support.js
CHANGED
|
@@ -838,6 +838,34 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
838
838
|
// SCHEDULER rules keep the schedule on the rule row (schedulerSetting), not
|
|
839
839
|
// on the node: normalize object → JSON string exactly like node/route.
|
|
840
840
|
const schedulerInput = preparedGraph.schedulerSetting ?? preparedGraph.scheduler;
|
|
841
|
+
// A7 写前 gate(2026-11-03 issue,8.1.108 实测):dataSourceType:"BIZ_SERVICE" 的
|
|
842
|
+
// 调度必须携带 methodMapping{serviceCode,serviceMethodCode,methodCode}。缺
|
|
843
|
+
// methodCode(应用编码)时平台 create 容忍、update 回**无明细 50000**,且 enable
|
|
844
|
+
// 只翻 status 不翻 enabled(实体残缺装不起调度链)。在本地拒绝,不发到 wire。
|
|
845
|
+
const schedulerForGate = typeof schedulerInput === "string" ? parseJsonColumn(schedulerInput) : schedulerInput;
|
|
846
|
+
if (schedulerForGate !== null && typeof schedulerForGate === "object" && !Array.isArray(schedulerForGate)
|
|
847
|
+
&& String(schedulerForGate.dataSourceType ?? "") === "BIZ_SERVICE") {
|
|
848
|
+
const mapping = schedulerForGate.methodMapping;
|
|
849
|
+
const missing = [];
|
|
850
|
+
if (mapping === null || typeof mapping !== "object" || Array.isArray(mapping)) {
|
|
851
|
+
missing.push("methodMapping{serviceCode,serviceMethodCode,methodCode}");
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
for (const key of ["serviceCode", "serviceMethodCode", "methodCode"]) {
|
|
855
|
+
const value = mapping[key];
|
|
856
|
+
if (typeof value !== "string" || !value.trim())
|
|
857
|
+
missing.push(`methodMapping.${key}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (missing.length > 0) {
|
|
861
|
+
throw new ToolRefusalError({
|
|
862
|
+
status: "refused",
|
|
863
|
+
blocker: "rule-scheduler-bizservice-mapping",
|
|
864
|
+
message: `schedulerSetting.dataSourceType=BIZ_SERVICE 但 ${missing.join("、")} 缺失(methodCode 是应用编码 appCode,不是方法 code——serviceCode+serviceMethodCode 定位方法,methodCode 定位应用)。缺 methodCode 的实体平台 create 容忍、update 回无明细 50000,且 enable 只翻 status 不翻 enabled。可先 rule.get 一条同族生产规则对照 methodMapping 形状。`,
|
|
865
|
+
missing,
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
841
869
|
const schedulerSetting = schedulerInput === undefined || schedulerInput === null
|
|
842
870
|
? undefined
|
|
843
871
|
: typeof schedulerInput === "string"
|
|
@@ -885,8 +913,40 @@ export async function executeRuleSaveWrite(session, args) {
|
|
|
885
913
|
// Only CUSTOM rule codes take the caller's name. `existing` already carries
|
|
886
914
|
// the platform name, so the spread preserves it.
|
|
887
915
|
const isPresetRule = PLATFORM_TRIGGER_RULE_CODES.has(args.ruleCode);
|
|
916
|
+
// A5 写前 gate(2026-11-03 issue):平台设计器只接受 中文/字母/数字、≤50 字符的
|
|
917
|
+
// 规则名。违规名 create 能落库(平台不拦),但设计器打开即报「规则名称仅支持
|
|
918
|
+
// 输入中文、字母和数字」且无法编辑——产出一个 UI 不可维护的 landmine。
|
|
919
|
+
// 只约束自定义规则(预置触发行的 name 属平台,见上方 isPresetRule 分支)。
|
|
920
|
+
if (!isPresetRule && typeof args.name === "string" && !/^[\u4e00-\u9fa5A-Za-z0-9]{1,50}$/.test(args.name)) {
|
|
921
|
+
throw new ToolRefusalError({
|
|
922
|
+
status: "refused",
|
|
923
|
+
blocker: "rule-name-invalid",
|
|
924
|
+
message: `规则 name「${args.name.slice(0, 60)}」不符合平台约束:仅支持中文/字母/数字,长度 1-50(设计器校验;违规名虽能落库,但设计器无法再编辑该规则)。建议改为纯中文或字母数字命名。`,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
// A8(2026-11-03 issue,8.1.108 实测):`options` / `extensionSetting` 缺席或空串
|
|
928
|
+
// 时平台落 "",8.1 族实体校验要求结构化的默认值——update 回无明细 50000。写前
|
|
929
|
+
// 归一:空串/缺席补平台默认结构(与设计器保存行为一致)。
|
|
930
|
+
const normalizeRuleJsonColumn = (value, fallback) => {
|
|
931
|
+
if (typeof value === "string" && value.trim())
|
|
932
|
+
return value;
|
|
933
|
+
if (value !== null && typeof value === "object")
|
|
934
|
+
return JSON.stringify(value);
|
|
935
|
+
return fallback;
|
|
936
|
+
};
|
|
888
937
|
const performed = existing?.id
|
|
889
|
-
? await wireCall(session, { path: ENDPOINTS.businessRuleUpdate, method: "POST", body: {
|
|
938
|
+
? await wireCall(session, { path: ENDPOINTS.businessRuleUpdate, method: "POST", body: {
|
|
939
|
+
...existing,
|
|
940
|
+
...(isPresetRule ? {} : { name: args.name }),
|
|
941
|
+
options: normalizeRuleJsonColumn(existing.options, '{"ruleVariable":[]}'),
|
|
942
|
+
extensionSetting: normalizeRuleJsonColumn(existing.extensionSetting, "{}"),
|
|
943
|
+
node: nodeJson,
|
|
944
|
+
route: routeJson,
|
|
945
|
+
bizRuleType,
|
|
946
|
+
...schedulerField,
|
|
947
|
+
enabled: existing.enabled === true,
|
|
948
|
+
status: existing.status ?? "DISABLED",
|
|
949
|
+
} })
|
|
890
950
|
: await wireCall(session, { path: ENDPOINTS.businessRuleCreate, method: "POST", body: { code: args.ruleCode, name: args.name, schemaCode: args.schemaCode, bizRuleType, node: nodeJson, route: routeJson, schedulerSetting: schedulerSetting ?? null, ...RULE_CREATE_BODY } });
|
|
891
951
|
// Ticket 62: a bare save only writes the DRAFT working row — the designer
|
|
892
952
|
// and the runtime read the PUBLISHED version, so "saved" ≠ "visible". An
|
package/dist/wire.js
CHANGED
|
@@ -232,6 +232,32 @@ export async function wireCall(session, call) {
|
|
|
232
232
|
return result;
|
|
233
233
|
}
|
|
234
234
|
/** List rows from the standard list envelope (array or {data:[]}). */
|
|
235
|
+
/**
|
|
236
|
+
* 版本族端点备选链(2026-11-03 issue A1,8.1.108 实测):部分端点只存在于
|
|
237
|
+
* 某个版本族——8.6 的 `runtime/business_rule/list_by_schemacode` 在 8.1 族不存在
|
|
238
|
+
* (404),8.1 用 `app/businessrule/list_by_schemacode`。按序尝试 candidates,
|
|
239
|
+
* **仅 HTTP 404(端点缺席)下探下一个**:401/500/transport/业务码拒绝原样抛出,
|
|
240
|
+
* 不吞真错误。全部 404 时重抛最后一个。
|
|
241
|
+
*/
|
|
242
|
+
export async function wireCallWith404Fallback(session, candidates, onFallback) {
|
|
243
|
+
let lastError;
|
|
244
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
245
|
+
const call = candidates[index];
|
|
246
|
+
try {
|
|
247
|
+
return await wireCall(session, call);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
const endpointAbsent = error instanceof WireError && error.kind === "http-status" && error.diagnostic?.httpStatus === 404;
|
|
251
|
+
if (!endpointAbsent)
|
|
252
|
+
throw error;
|
|
253
|
+
lastError = error;
|
|
254
|
+
const next = candidates[index + 1];
|
|
255
|
+
if (next && onFallback)
|
|
256
|
+
onFallback(call, next);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
throw lastError;
|
|
260
|
+
}
|
|
235
261
|
export function wireRows(value) {
|
|
236
262
|
const rows = Array.isArray(value)
|
|
237
263
|
? value
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@stalfh233/omc-fake-platform": "0.6.
|
|
55
|
+
"@stalfh233/omc-fake-platform": "0.6.3",
|
|
56
56
|
"commander": "14.0.3",
|
|
57
|
-
"omc-core": "0.6.
|
|
57
|
+
"omc-core": "0.6.3",
|
|
58
58
|
"proper-lockfile": "4.1.2",
|
|
59
59
|
"write-file-atomic": "6.0.0",
|
|
60
60
|
"zod": "^3.23.8",
|