@stalfh233/omc-cli 0.6.2 → 0.6.4
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/README.md +1 -1
- package/dist/call.js +10 -1
- package/dist/capability-absences.js +15 -2
- package/dist/capability-overview.js +1 -1
- package/dist/cli-contract.js +23 -4
- package/dist/cli-output.js +28 -10
- package/dist/cli.js +127 -16
- package/dist/command-transport.js +7 -1
- package/dist/config-store.js +17 -0
- package/dist/discovery.js +32 -3
- package/dist/endpoints.js +5 -0
- package/dist/governed-execution.js +26 -1
- package/dist/lanes/b2-registration-contract.js +1 -1
- package/dist/lanes/b2-registration.js +205 -4
- package/dist/lanes/businessrule-contract.js +5 -1
- package/dist/lanes/businessrule.js +38 -3
- package/dist/lanes/form.js +20 -0
- package/dist/lanes/model.js +20 -4
- package/dist/lanes/rule-lifecycle.js +12 -2
- package/dist/lanes/runtime-data.js +153 -15
- package/dist/path-projection.js +32 -8
- package/dist/read.js +62 -9
- package/dist/recipes/businessrule.js +83 -1
- package/dist/rule-support.js +61 -1
- package/dist/support.js +12 -0
- package/dist/tool-types.js +18 -0
- package/dist/wire.js +35 -1
- package/dist/write.js +7 -5
- package/package.json +3 -3
- package/skills/omc-start-here.md +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { FilterDslError, injectPropertyTypes, toQueryCondition } from "../data-list-filter.js";
|
|
3
|
+
import { FilterDslError, injectPropertyTypes, SIMPLE_OP_MAP, toQueryCondition } from "../data-list-filter.js";
|
|
4
4
|
import { ENDPOINTS } from "../endpoints.js";
|
|
5
5
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
6
6
|
import { governedRead } from "../read.js";
|
|
@@ -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
|
/**
|
|
@@ -725,6 +759,9 @@ export function registerRuntimeDataLane(tools) {
|
|
|
725
759
|
lane: RUNTIME_LANE,
|
|
726
760
|
scope: RUNTIME_TOOLS["data.list"].scope,
|
|
727
761
|
description: RUNTIME_TOOLS["data.list"].description,
|
|
762
|
+
// T3(GitLab #17):op 的封闭取值来自 DSL 的 SIMPLE_OP_MAP(不是 zod enum——
|
|
763
|
+
// 那样会抢先拒绝、丢掉带 acceptedOps 的说明性拒绝)。默认 describe 内联一次。
|
|
764
|
+
paramEnums: { filter: Object.keys(SIMPLE_OP_MAP).join("|") },
|
|
728
765
|
inputSchema: {
|
|
729
766
|
environment: environmentArg,
|
|
730
767
|
schemaCode: z.string().min(1),
|
|
@@ -734,7 +771,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
734
771
|
// `Expected object, received array`). An array is the caller's AND-list,
|
|
735
772
|
// so accept it and treat it as `filters` rather than costing a retry
|
|
736
773
|
// (dogfood R1 round17).
|
|
737
|
-
filter: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).optional().describe('one clause { op, field, value? } (or an array of clauses = AND); op
|
|
774
|
+
filter: z.union([z.record(z.string(), z.unknown()), z.array(z.record(z.string(), z.unknown()))]).optional().describe('one clause { op, field, value? } (or an array of clauses = AND); op 取值见过滤语义:eq/neq/gt/lt/gte/lte/like/notlike/in/notin/between/notbetween/isnull/isnotnull(camelCase 别名 equally valid: equals/notEquals/contains/isNull/isNotNull)。**like/contains 是子串包含(String.includes),不是前缀匹配,`%` / `_` 是字面量而非通配符**;按前缀分片请用 gte/lt 区间或更长的前缀+本地去重,别假设 like:"0%" 只命中 0 开头(GitLab #18 P1-3 实测 4 个"前缀分片"互相重叠 2258 条)'),
|
|
738
775
|
filters: z.array(z.record(z.string(), z.unknown())).optional().describe("clauses combined with AND; a malformed clause is refused with blocker invalid-filter-DSL"),
|
|
739
776
|
filterGroup: z.record(z.string(), z.unknown()).optional().describe('nested group { op:"and"|"or", clauses:[clause|group] }'),
|
|
740
777
|
rawCondition: z.unknown().optional(),
|
|
@@ -745,7 +782,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
745
782
|
showTotal: z.boolean().default(true),
|
|
746
783
|
page: z.number().int().min(0).default(0),
|
|
747
784
|
size: z.number().int().min(1).max(200).default(10),
|
|
748
|
-
all: z.boolean().optional().describe("true 时自动翻页聚合全部结果(内部按 size=200 逐页拉取,安全上限 1000
|
|
785
|
+
all: z.boolean().optional().describe("true 时自动翻页聚合全部结果(内部按 size=200 逐页拉取,安全上限 1000 行;超过时回 truncated:true + missingCount,别当全集)——存量盘点/数据核对用,替代手动循环翻页。与 page 互斥(all 固定从第 0 页起)"),
|
|
749
786
|
full: z.boolean().optional().describe("内联完整列表结果;默认大结果只返回计数与 detailFile"),
|
|
750
787
|
// r31 (2026-10-31 real machine): agents pass `query: {field: value}` as a
|
|
751
788
|
// filter — zod would STRIP the unknown key silently and the server
|
|
@@ -782,13 +819,15 @@ export function registerRuntimeDataLane(tools) {
|
|
|
782
819
|
status: "refused",
|
|
783
820
|
blocker: "invalid-filter-DSL",
|
|
784
821
|
reason: error.code,
|
|
785
|
-
message: `${error.message};filter 的最小合法形状:{"filter":{"op":"eq","field":"<字段code>","value":"<值>"}}(op 取 eq/neq/gt/lt/gte/lte/like/notlike/in/notin/between/notbetween/isnull/isnotnull
|
|
822
|
+
message: `${error.message};filter 的最小合法形状:{"filter":{"op":"eq","field":"<字段code>","value":"<值>"}}(op 取 eq/neq/gt/lt/gte/lte/like/notlike/in/notin/between/notbetween/isnull/isnotnull;like/contains 是**子串包含**、% 非通配)。多条件用 filters:[clause,…](AND)或 filterGroup:{op:"and"|"or",clauses:[…]}。可运行示例:describe data.list --example list-filter-equality`,
|
|
786
823
|
minimalValidFilter: { filter: { op: "eq", field: "<fieldCode>", value: "<value>" } },
|
|
787
824
|
acceptedOps: ["eq", "neq", "gt", "lt", "gte", "lte", "like", "notlike", "in", "notin", "between", "notbetween", "isnull", "isnotnull"],
|
|
788
825
|
};
|
|
789
826
|
throw error;
|
|
790
827
|
}
|
|
791
828
|
return governedRead(ctx, self, args, async (session) => {
|
|
829
|
+
// A2:DSL 过滤命中的单选/字典字段(total:0 时给值写法提示)
|
|
830
|
+
let choiceFilterFields = [];
|
|
792
831
|
// Published field metadata is the sole source for filter propertyType
|
|
793
832
|
// injection AND for the projectionHint below. Fetch at most once per
|
|
794
833
|
// call: the DSL path needs it up front; the hint path asks only after
|
|
@@ -813,9 +852,27 @@ export function registerRuntimeDataLane(tools) {
|
|
|
813
852
|
status: "refused",
|
|
814
853
|
blocker: "unknown-filter-field",
|
|
815
854
|
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
|
|
855
|
+
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
856
|
};
|
|
818
857
|
condition = injected.condition;
|
|
858
|
+
// A2(GitLab #15):单选/字典字段按显示值过滤会静默 0 条(平台查询按
|
|
859
|
+
// 字典 key 匹配)。带出这些字段,total:0 时给写法提示,防"查不到数据"
|
|
860
|
+
// 误报(实测 313 条被 total:0 误读成"今天没有已审核单")。
|
|
861
|
+
choiceFilterFields = collectConditionLeafFields(condition).filter((code) => CHOICE_FILTER_PROPERTY_TYPES.has(typeByCode.get(code) ?? -1));
|
|
862
|
+
// P1-4(GitLab #18,2026-11-04):子表字段(CHILD_TABLE=8)的值是行数组,
|
|
863
|
+
// 运行面 query/list 只做标量过滤——拿它当 filter 字段会回一个没有明细的
|
|
864
|
+
// errcode 50000「服务异常」,被读成"平台挂了"。元数据已证它是子表,就在
|
|
865
|
+
// 发查询前拒绝并给替代路径(data.load 读主表后看子表数组)。
|
|
866
|
+
const childTableFields = collectConditionLeafFields(condition).filter((code) => typeByCode.get(code) === CHILD_TABLE_PROPERTY_TYPE);
|
|
867
|
+
if (childTableFields.length > 0)
|
|
868
|
+
return {
|
|
869
|
+
status: "refused",
|
|
870
|
+
blocker: "unsupported-filter-on-child-table",
|
|
871
|
+
schemaCode: args.schemaCode,
|
|
872
|
+
fields: childTableFields,
|
|
873
|
+
message: `这些字段是子表(propertyType=${CHILD_TABLE_PROPERTY_TYPE},值是行数组):运行面 query/list 只支持标量过滤,按子表字段过滤会回 errcode 50000 且无明细。改法:用 data.load 读主表记录(子表行随主记录返回),或在本地按子表字段筛选。`,
|
|
874
|
+
nextCommand: `omc call data.load --env <env> --args '{"schemaCode":"${args.schemaCode}","bizObjectId":"<主表 bizObjectId>"}'`,
|
|
875
|
+
};
|
|
819
876
|
}
|
|
820
877
|
const requestedFields = args.displayFields ?? args.columns;
|
|
821
878
|
const fields = requestedFields && requestedFields.length > 0 ? requestedFields : undefined;
|
|
@@ -835,12 +892,23 @@ export function registerRuntimeDataLane(tools) {
|
|
|
835
892
|
if (args.all === true) {
|
|
836
893
|
const CAP = 1000;
|
|
837
894
|
const pageSize = 200;
|
|
895
|
+
// GitLab #18 P1-2: `all:true` is "aggregate from page 0 up to the cap".
|
|
896
|
+
// A caller who ALSO passed page expected server-side paging, got page 0
|
|
897
|
+
// silently (page:6 → 0 rows, read as "no data"). Refuse the combo
|
|
898
|
+
// instead of guessing which one they meant.
|
|
899
|
+
if ((args.page ?? 0) !== 0)
|
|
900
|
+
throw new ToolInputError({
|
|
901
|
+
status: "error",
|
|
902
|
+
blocker: "invalid-input",
|
|
903
|
+
tool: "data.list",
|
|
904
|
+
message: `all:true 与 page 互斥:all 固定从第 0 页聚合到上限 ${CAP} 行;要指定页请去掉 all(page/size 手动翻页),要全集请用 filters 分片。`,
|
|
905
|
+
});
|
|
838
906
|
const firstBody = { ...body, page: 0, size: pageSize };
|
|
839
907
|
const first = await wireCall(session, { path: ENDPOINTS.runtimeQueryList, method: "POST", body: firstBody });
|
|
840
908
|
const firstPage = extractBizObjectPage(first.data);
|
|
841
909
|
const total = Number(firstPage.total ?? 0);
|
|
842
910
|
if (total === 0 || firstPage.content.length === 0)
|
|
843
|
-
return { schemaCode: args.schemaCode, total: 0, page: "all", size: 0, pagesFetched: 0, items: [] };
|
|
911
|
+
return { schemaCode: args.schemaCode, total: 0, page: "all", size: 0, pagesFetched: 0, items: [], ...(choiceFilterFields.length > 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}) };
|
|
844
912
|
const rows = [...firstPage.content];
|
|
845
913
|
const target = Math.min(total, CAP);
|
|
846
914
|
let pageIndex = 1;
|
|
@@ -852,15 +920,57 @@ export function registerRuntimeDataLane(tools) {
|
|
|
852
920
|
rows.push(...nextPage.content);
|
|
853
921
|
pageIndex += 1;
|
|
854
922
|
}
|
|
855
|
-
|
|
856
|
-
|
|
923
|
+
// 翻页按 size=200 走,最后一页可能越过 total(total=999 会拉到 1000 行);
|
|
924
|
+
// 先按 total 夹一次,itemsCount/items.length 才与 total 可比。
|
|
925
|
+
const collected = rows.length > total ? rows.slice(0, total) : rows;
|
|
926
|
+
// GitLab #18 P1-2:`capped = rows.length > CAP` 是死代码——target 已被
|
|
927
|
+
// Math.min(total, CAP) 夹住,rows 永远到不了 CAP+1,于是 total>1000 时
|
|
928
|
+
// 返回「total:1043 / items:1000 / 无任何标记」,43 条静默消失(实测被
|
|
929
|
+
// 读成"数据就这么多")。真正的截断判据是「拿到的比总量少」。
|
|
930
|
+
const truncatedByCap = collected.length < total;
|
|
931
|
+
const items = collected.map((row0) => {
|
|
857
932
|
const row = row0;
|
|
858
933
|
const rawData = row.data && typeof row.data === "object" ? row.data : {};
|
|
859
934
|
const data = args.full === true ? rawData : compactRuntimeRecord(rawData);
|
|
860
935
|
const selected = fields ?? Object.keys(data).filter((field) => !["id", "bizObjectId", "schemaCode", "sequenceStatus", "createdTime", "updatedTime", "creator", "updater"].includes(field));
|
|
861
936
|
return { id: row.bizObjectId ?? row.id ?? data.id, fields: Object.fromEntries(selected.map((field) => [field, data[field] ?? null])) };
|
|
862
937
|
});
|
|
863
|
-
|
|
938
|
+
// A6(GitLab #15):all:true 路径此前完全没有 projectionHint——未配置列
|
|
939
|
+
// 的 null/省略同样要点名,防「全量拉取却被投影坑」(实测 orderChange)。
|
|
940
|
+
let allHint;
|
|
941
|
+
if (items.length > 0) {
|
|
942
|
+
try {
|
|
943
|
+
const typeByCode = await publishedTypeByCode();
|
|
944
|
+
const returnedKeys = new Set(Object.keys(items[0]?.fields ?? {}));
|
|
945
|
+
const hintColumns = [...typeByCode.keys()].filter((field) => !SYSTEM_FIELD_CODES.has(field) && (!returnedKeys.has(field) || items.every((item) => item.fields[field] === null)));
|
|
946
|
+
if (hintColumns.length > 0)
|
|
947
|
+
allHint = `这些列在默认视图未配置(值可能存在但列表投影为 null 或整列省略):${hintColumns.join("、")};先用 listview.configure 配置列,或用 data.load 读单条权威值`;
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
// 元数据读失败不阻断数据返回
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return {
|
|
954
|
+
schemaCode: args.schemaCode,
|
|
955
|
+
total,
|
|
956
|
+
page: "all",
|
|
957
|
+
size: collected.length,
|
|
958
|
+
pagesFetched: pageIndex,
|
|
959
|
+
// 形状常量:脚本按 result.items 取数(与内联态一致),itemsCount 与
|
|
960
|
+
// total 的差就是被截断的条数——不再只靠 size 反推。
|
|
961
|
+
itemsCount: collected.length,
|
|
962
|
+
...(truncatedByCap
|
|
963
|
+
? {
|
|
964
|
+
truncated: true,
|
|
965
|
+
capped: true,
|
|
966
|
+
cap: CAP,
|
|
967
|
+
missingCount: total - collected.length,
|
|
968
|
+
capNotice: `已取前 ${collected.length} 条 / 共 ${total} 条(安全上限 ${CAP})——还差 ${total - collected.length} 条没返回。不要把它当全集:加 filters 分片(如按日期/业务键)分别拉取,或去掉 all 用 page/size 手动翻页。`,
|
|
969
|
+
}
|
|
970
|
+
: {}),
|
|
971
|
+
...(allHint ? { projectionHint: allHint } : {}),
|
|
972
|
+
items,
|
|
973
|
+
};
|
|
864
974
|
}
|
|
865
975
|
let result;
|
|
866
976
|
try {
|
|
@@ -868,7 +978,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
868
978
|
}
|
|
869
979
|
catch (error) {
|
|
870
980
|
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: [] };
|
|
981
|
+
return { schemaCode: args.schemaCode, total: 0, totalPages: 0, page: args.page ?? 0, size: args.size ?? 10, items: [], ...(choiceFilterFields.length > 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}) };
|
|
872
982
|
}
|
|
873
983
|
// r30: a MISSING model answers this face with a generic 50000
|
|
874
984
|
// 服务异常 — indistinguishable from a real outage. One field-face
|
|
@@ -890,6 +1000,13 @@ export function registerRuntimeDataLane(tools) {
|
|
|
890
1000
|
}
|
|
891
1001
|
}
|
|
892
1002
|
}
|
|
1003
|
+
// P1-4:其余 50000 保持原样抛出(真服务异常必须照实暴露),但给一条
|
|
1004
|
+
// 不改判定的排查提示——子表/子表 schema 的过滤与系统字段过滤是这个面
|
|
1005
|
+
// 最常见的"服务异常"来源(live: 子表 schema 加 parentId 过滤 → 50000,
|
|
1006
|
+
// 去掉 filter 就正常)。只加 hint,不猜 blocker。
|
|
1007
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 50000) {
|
|
1008
|
+
throw new WireError(error.kind, error.message, error.errcode, error.diagnostic, `若 filter/filters 里含子表字段或子表自身 schema 的 parentId 等系统字段:运行面 query/list 的标量过滤不适用于子表,去掉 filter 后按主表 id 用 data.load 读子表行。其余情况按真服务异常处理(同一 schemaCode 去掉 filter 可读即说明是过滤形状问题)。`);
|
|
1009
|
+
}
|
|
893
1010
|
throw error;
|
|
894
1011
|
}
|
|
895
1012
|
const page = extractBizObjectPage(result.data);
|
|
@@ -921,6 +1038,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
921
1038
|
// (live 2026-10-18: total:0 was read as "no residue" while 9 drafts
|
|
922
1039
|
// existed).
|
|
923
1040
|
filter: { excludedDraft: true, hint: "默认不含 DRAFT(total 亦仅计非草稿);核对/清理残留请加 all:true" },
|
|
1041
|
+
...(choiceFilterFields.length > 0 && Number(page.total ?? 0) === 0 ? { filterValueHint: choiceFilterValueHint(choiceFilterFields) } : {}),
|
|
924
1042
|
totalPages: page.totalPages,
|
|
925
1043
|
page: page.page ?? args.page ?? 0,
|
|
926
1044
|
size: page.size ?? args.size ?? 20,
|
|
@@ -942,6 +1060,26 @@ export function registerRuntimeDataLane(tools) {
|
|
|
942
1060
|
const typeByCode = await publishedTypeByCode();
|
|
943
1061
|
return candidates.filter((field) => typeByCode.has(field));
|
|
944
1062
|
};
|
|
1063
|
+
// A6(GitLab #15,2026-11-04):未配置列有两种投影形态——值为 null,或
|
|
1064
|
+
// **整键省略**(orderChange 实测:sequenceNo 在 data.list 里根本不出现,
|
|
1065
|
+
// data.load 却有值,被误读成「单号为空」)。省略检测需要发布元数据
|
|
1066
|
+
// (fieldList 有 60s wire 缓存,DSL 过滤路径本就已取);两种形态合并进 hint。
|
|
1067
|
+
const projectionHintFor = async () => {
|
|
1068
|
+
if (items.length === 0)
|
|
1069
|
+
return undefined;
|
|
1070
|
+
const typeByCode = await publishedTypeByCode();
|
|
1071
|
+
const returnedKeys = new Set(Object.keys(items[0]?.fields ?? {}));
|
|
1072
|
+
const nullColumns = fields && fields.length > 0
|
|
1073
|
+
? fields.filter((field) => typeByCode.has(field) && items.every((item) => item.fields[field] === null))
|
|
1074
|
+
: [...returnedKeys].filter((field) => !SYSTEM_FIELD_CODES.has(field) && typeByCode.has(field) && items.every((item) => item.fields[field] === null));
|
|
1075
|
+
const omittedColumns = !fields
|
|
1076
|
+
? [...typeByCode.keys()].filter((field) => !SYSTEM_FIELD_CODES.has(field) && !returnedKeys.has(field))
|
|
1077
|
+
: [];
|
|
1078
|
+
const hintColumns = [...new Set([...nullColumns, ...omittedColumns])];
|
|
1079
|
+
if (hintColumns.length === 0)
|
|
1080
|
+
return undefined;
|
|
1081
|
+
return `这些列在默认视图未配置(值可能存在但列表投影为 null 或整列省略):${hintColumns.join("、")};先用 listview.configure 配置列,或用 data.load 读单条权威值`;
|
|
1082
|
+
};
|
|
945
1083
|
if (fields &&
|
|
946
1084
|
fields.length > 0 &&
|
|
947
1085
|
items.length > 0 &&
|
|
@@ -955,9 +1093,9 @@ export function registerRuntimeDataLane(tools) {
|
|
|
955
1093
|
// business fields absent from the list view render as null in every
|
|
956
1094
|
// row with no explanation. Name the null'd business columns so the
|
|
957
1095
|
// agent reads data.load instead of misdiagnosing "data lost".
|
|
958
|
-
const
|
|
959
|
-
if (
|
|
960
|
-
readback.projectionHint =
|
|
1096
|
+
const hint = await projectionHintFor();
|
|
1097
|
+
if (hint)
|
|
1098
|
+
readback.projectionHint = hint;
|
|
961
1099
|
}
|
|
962
1100
|
return readback;
|
|
963
1101
|
}, { 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 })}` });
|
|
@@ -974,8 +1112,8 @@ export function registerRuntimeDataLane(tools) {
|
|
|
974
1112
|
environment: environmentArg,
|
|
975
1113
|
schemaCode: z.string(),
|
|
976
1114
|
bizObjectId: z.string({ required_error: "bizObjectId 必填(参数名就是 bizObjectId——不是 id 也不是 objectId,r44 两个 agent 都猜了 objectId;取值来自 data.submit 读回的 objectId/sequenceNo 或 data.list 的 row.bizObjectId)" }),
|
|
977
|
-
full: z.boolean().optional().describe("返回未裁剪的完整业务对象(含 owner/creater
|
|
978
|
-
fields: z.array(z.string().min(1)).optional().describe("
|
|
1115
|
+
full: z.boolean().optional().describe("返回未裁剪的完整业务对象(含 owner/creater 等系统字段;默认返回业务字段+身份的精简记录)。整条大对象(百 KB 级)会被外置成 artifact——只需要几列时改用 fields"),
|
|
1116
|
+
fields: z.array(z.string().min(1)).optional().describe("列投影:只保留列出的字段——核验状态时传 [\"sequenceStatus\",\"<业务状态字段>\"],别整条 data.load(r4-training 2026-10-20:同一对象被读 74 次);缺失字段回在 missingFields。这是本地裁剪、在 artifact 外置**之前**生效,所以只取 9 列不会先拉整包(GitLab #17 T5)。平台 form/load 无列参数,投影只能在这一层做"),
|
|
979
1117
|
capability: capabilityArg,
|
|
980
1118
|
},
|
|
981
1119
|
handler: async (ctx, args, self) => {
|
package/dist/path-projection.js
CHANGED
|
@@ -105,21 +105,23 @@ export function projectJsonPathFanout(value, path) {
|
|
|
105
105
|
if (marker < 0) {
|
|
106
106
|
const projected = projectJsonPath(value, path);
|
|
107
107
|
return projected.missed
|
|
108
|
-
? { values: [], missed: true, ...(projected.at !== undefined ? { at: projected.at } : {}), ...(projected.available !== undefined ? { available: projected.available } : {}), missedEntries: 0 }
|
|
109
|
-
: { values: [projected.value], missed: false, missedEntries: 0 };
|
|
108
|
+
? { values: [], missed: true, ...(projected.at !== undefined ? { at: projected.at } : {}), ...(projected.available !== undefined ? { available: projected.available } : {}), missedEntries: 0, nullEntries: 0 }
|
|
109
|
+
: { values: [projected.value], missed: false, missedEntries: 0, nullEntries: projected.value === null ? 1 : 0 };
|
|
110
110
|
}
|
|
111
111
|
const before = path.slice(0, marker);
|
|
112
112
|
const after = path.slice(marker + 2).replace(/^[./]+/, "");
|
|
113
113
|
const base = projectJsonPath(value, before);
|
|
114
114
|
if (base.missed)
|
|
115
|
-
return { values: [], missed: true, ...(base.at !== undefined ? { at: base.at } : {}), ...(base.available !== undefined ? { available: base.available } : {}), missedEntries: 0 };
|
|
115
|
+
return { values: [], missed: true, ...(base.at !== undefined ? { at: base.at } : {}), ...(base.available !== undefined ? { available: base.available } : {}), missedEntries: 0, nullEntries: 0 };
|
|
116
116
|
if (!Array.isArray(base.value))
|
|
117
|
-
return { values: [], missed: true, at: before || "$", available: `该层不是数组(是 ${base.value === null ? "null" : typeof base.value});[] 只能映射数组,如 items[].fields.planNo`, missedEntries: 0 };
|
|
117
|
+
return { values: [], missed: true, at: before || "$", available: `该层不是数组(是 ${base.value === null ? "null" : typeof base.value});[] 只能映射数组,如 items[].fields.planNo`, missedEntries: 0, nullEntries: 0 };
|
|
118
118
|
const values = [];
|
|
119
119
|
let missedEntries = 0;
|
|
120
|
+
let nullEntries = 0;
|
|
120
121
|
for (const entry of base.value) {
|
|
121
122
|
const projected = projectJsonPathFanout(entry, after);
|
|
122
123
|
values.push(...projected.values);
|
|
124
|
+
nullEntries += projected.nullEntries;
|
|
123
125
|
if (projected.missed)
|
|
124
126
|
missedEntries += 1;
|
|
125
127
|
}
|
|
@@ -127,9 +129,9 @@ export function projectJsonPathFanout(value, path) {
|
|
|
127
129
|
// array) — report the miss so the caller does not read an empty list as data.
|
|
128
130
|
if (values.length === 0 && missedEntries > 0) {
|
|
129
131
|
const first = projectJsonPathFanout(base.value[0], after);
|
|
130
|
-
return { values: [], missed: true, ...(first.at !== undefined ? { at: `${before}[].${first.at}` } : {}), ...(first.available !== undefined ? { available: first.available } : {}), missedEntries };
|
|
132
|
+
return { values: [], missed: true, ...(first.at !== undefined ? { at: `${before}[].${first.at}` } : {}), ...(first.available !== undefined ? { available: first.available } : {}), missedEntries, nullEntries: 0 };
|
|
131
133
|
}
|
|
132
|
-
return { values, missed: false, ...(missedEntries > 0 ? { missedEntries } : { missedEntries: 0 }) };
|
|
134
|
+
return { values, missed: false, ...(missedEntries > 0 ? { missedEntries } : { missedEntries: 0 }), nullEntries };
|
|
133
135
|
}
|
|
134
136
|
/** Collect every value of a repeatable flag (`--extract a --extract b`). */
|
|
135
137
|
function argvValues(args, name) {
|
|
@@ -144,7 +146,14 @@ function argvValues(args, name) {
|
|
|
144
146
|
return values;
|
|
145
147
|
}
|
|
146
148
|
/** 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
|
|
149
|
+
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。";
|
|
150
|
+
/**
|
|
151
|
+
* T6(GitLab #17):`mapped` 只是"路径命中的条目数",值为 null 也算命中。
|
|
152
|
+
* 全 null + ok:true 曾被读成"取到了 N 个值/台账只有 1 行"(真实 42 行)。
|
|
153
|
+
* 提示里把两种成因分开说:未投影(看 data.list 的 projectionHint / data.load)
|
|
154
|
+
* 与字段本身为空。
|
|
155
|
+
*/
|
|
156
|
+
const ALL_NULL_HINT = `路径全部命中但值都是 null——注意 mapped 只是命中条目数,不代表有值。常见成因是列表投影没配置该列(data.list 只投影视图列,null 或整键省略;看同一次 data.list 的 projectionHint,或用 data.load 读单条权威值)。若 projectionHint 也指向该列,就不是"字段为空"。`;
|
|
148
157
|
/**
|
|
149
158
|
* `call --extract/--extract-keys`: resolve paths inside the result's own
|
|
150
159
|
* artifact (or inside the result object when the tool returned no artifact).
|
|
@@ -186,6 +195,14 @@ export function projectResultExtract(result, paths, keysOnly) {
|
|
|
186
195
|
...(artifactId !== undefined ? { artifactId, sha256: artifact?.sha256 } : {}),
|
|
187
196
|
from: readFromArtifact ? "artifact" : "result",
|
|
188
197
|
};
|
|
198
|
+
// T6:列表投影里「未配置的列」也是 null 键——工具自己已经写了 projectionHint
|
|
199
|
+
// (#15 A6),但投影层此前把它丢了,于是 `--jq items[].fields.x` 全 null 时
|
|
200
|
+
// 调用方看不到任何线索。把源结果的提示原样透传(有才带,零额外体积)。
|
|
201
|
+
const sourceProjectionHint = source && typeof source === "object" && !Array.isArray(source) && typeof source.projectionHint === "string"
|
|
202
|
+
? source.projectionHint
|
|
203
|
+
: undefined;
|
|
204
|
+
if (sourceProjectionHint !== undefined)
|
|
205
|
+
out.projectionHint = sourceProjectionHint;
|
|
189
206
|
if (keysOnly) {
|
|
190
207
|
const record = source && typeof source === "object" && !Array.isArray(source) ? source : undefined;
|
|
191
208
|
out.keys = record
|
|
@@ -203,7 +220,14 @@ export function projectResultExtract(result, paths, keysOnly) {
|
|
|
203
220
|
const fanout = projectJsonPathFanout(source, path);
|
|
204
221
|
return fanout.missed
|
|
205
222
|
? { path, content: null, pathMiss: true, missedAt: fanout.at, available: fanout.available, missedEntries: fanout.missedEntries, hint: PATH_MISS_HINT }
|
|
206
|
-
: {
|
|
223
|
+
: {
|
|
224
|
+
path,
|
|
225
|
+
content: fanout.values,
|
|
226
|
+
mapped: fanout.values.length,
|
|
227
|
+
...(fanout.nullEntries > 0 ? { nullEntries: fanout.nullEntries } : {}),
|
|
228
|
+
...(fanout.nullEntries > 0 && fanout.nullEntries === fanout.values.length ? { hint: ALL_NULL_HINT } : {}),
|
|
229
|
+
...(fanout.missedEntries > 0 ? { missedEntries: fanout.missedEntries, hint: `有 ${fanout.missedEntries} 条记录在该路径下缺失,values 只含命中项` } : {}),
|
|
230
|
+
};
|
|
207
231
|
}
|
|
208
232
|
const projected = projectJsonPath(source, path);
|
|
209
233
|
return projected.missed
|
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
|
-
...passthroughScalars(result),
|
|
82
|
+
...passthroughScalars(result, omittedScalarKeys(compact)),
|
|
77
83
|
...compact,
|
|
84
|
+
...placeholders,
|
|
78
85
|
full: true,
|
|
79
86
|
fullOmitted: true,
|
|
80
87
|
fullBytes: rawBytes,
|
|
@@ -84,19 +91,39 @@ 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, omittedScalarKeys(compact)), ...compact, ...placeholders, environmentFingerprint, detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256 };
|
|
91
102
|
}
|
|
92
103
|
catch (error) {
|
|
93
104
|
if (isUncertainWireError(error))
|
|
94
105
|
return { status: "uncertain", frozen: true, reason: `wire-transport:${wireErrorKind(error)}`, detail: error.message };
|
|
95
106
|
if (error instanceof WireError)
|
|
96
|
-
return { status: "refused", blocker: `wire-${error.kind}`, errcode: error.errcode, message: error.message, ...(error.diagnostic ? { diagnostic: error.diagnostic } : {}) };
|
|
107
|
+
return { status: "refused", blocker: `wire-${error.kind}`, errcode: error.errcode, message: error.message, ...(error.diagnostic ? { diagnostic: error.diagnostic } : {}), ...(error.hint ? { hint: error.hint } : {}) };
|
|
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)) {
|
|
@@ -181,10 +208,10 @@ const BULKY_READ_KEYS = new Set(["data", "nodes", "routes", "items", "rules", "a
|
|
|
181
208
|
* `result.workflowCode` got `undefined` (round 10: live demo). Pass the scalar
|
|
182
209
|
* (non-bulky) top-level keys through; the compact's own keys still win.
|
|
183
210
|
*/
|
|
184
|
-
function passthroughScalars(result) {
|
|
211
|
+
function passthroughScalars(result, omit = []) {
|
|
185
212
|
const carried = {};
|
|
186
213
|
for (const [key, value] of Object.entries(result)) {
|
|
187
|
-
if (key === "status" || BULKY_READ_KEYS.has(key))
|
|
214
|
+
if (key === "status" || BULKY_READ_KEYS.has(key) || omit.includes(key))
|
|
188
215
|
continue;
|
|
189
216
|
const type = typeof value;
|
|
190
217
|
if (value === null || type === "string" || type === "number" || type === "boolean")
|
|
@@ -192,6 +219,32 @@ function passthroughScalars(result) {
|
|
|
192
219
|
}
|
|
193
220
|
return carried;
|
|
194
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* T1(GitLab #17):某个工具的自定义 compact 用 `<key>Omitted` 显式声明"这段正文
|
|
224
|
+
* 已外置"(form.get 的六份布局 JSON 是大**字符串**,不是 BULKY_READ_KEYS 里的
|
|
225
|
+
* 数组/对象)。passthroughScalars 会把所有字符串原样带回来,正好把外置又抵消掉
|
|
226
|
+
* (42KB 读回里 41KB 是刚声明省略的布局)。这里把省略声明翻译成跳过名单。
|
|
227
|
+
*
|
|
228
|
+
* 白名单化(审查 2026-11-04):只认 form 的六个布局面。否则任何 compact 恰好
|
|
229
|
+
* 产出的 `<key>Omitted:true`(如 `fullOmitted` → 派生 omit=`full`)都会误删同名
|
|
230
|
+
* 标量,靠后续展开掩盖太脆。
|
|
231
|
+
*/
|
|
232
|
+
const EXTERNALIZED_SCALAR_KEYS = new Set([
|
|
233
|
+
"draftViewJson",
|
|
234
|
+
"draftAttributesJson",
|
|
235
|
+
"draftHtmlJson",
|
|
236
|
+
"draftActionsJson",
|
|
237
|
+
"publishedViewJson",
|
|
238
|
+
"publishedAttributesJson",
|
|
239
|
+
"publishedHtmlJson",
|
|
240
|
+
"publishedActionsJson",
|
|
241
|
+
]);
|
|
242
|
+
function omittedScalarKeys(compact) {
|
|
243
|
+
return Object.keys(compact)
|
|
244
|
+
.filter((key) => key.endsWith("Omitted") && compact[key] === true)
|
|
245
|
+
.map((key) => key.slice(0, -"Omitted".length))
|
|
246
|
+
.filter((key) => EXTERNALIZED_SCALAR_KEYS.has(key));
|
|
247
|
+
}
|
|
195
248
|
function safeFingerprint(session) {
|
|
196
249
|
try {
|
|
197
250
|
return session.environmentFingerprint();
|
|
@@ -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
|