@stalfh233/omc-cli 0.4.55 → 0.4.57

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.
@@ -319,6 +319,14 @@ export function compileMethodConfigPlain(adapterCode, methodConfig) {
319
319
  config.requestBodyType = encodeEnumIndex(config.requestBodyType, RESTFUL_REQUEST_BODY_TYPES, "requestBodyType");
320
320
  if (typeof config.url !== "string" || !config.url)
321
321
  refuse(BIZSERVICE_CONFIG_BLOCKERS.methodConfigInvalid, "RESTFUL 方法 url 必须是非空字符串(相对路径与服务 commonlist.url 拼接)", { key: "url" });
322
+ // Prefix × auth matrix (live 2026-10-18, SubCon): `/openapi/**` is spliced
323
+ // to `/api/openapi/**` (single layer) and authenticates with the OAuth2
324
+ // client (`xclient`); `/api/**` becomes `/api/api/**` (double layer) and
325
+ // authenticates as the USER — mixing them answers 401 权限不够. Two hard
326
+ // rules are unambiguous on their own: an unauthenticated prefix must never
327
+ // carry a 二开 interface, and externalLink is never a business endpoint.
328
+ if (/^\/?externalLink/i.test(config.url))
329
+ refuse(BIZSERVICE_CONFIG_BLOCKERS.methodConfigInvalid, "禁止用 /externalLink 等免鉴权前缀承载二开接口(能通但方向错误);二开接口走 /openapi/** + xclient", { key: "url", url: config.url });
322
330
  // Designer parity: the rule designer's RESTFUL method config also carries
323
331
  // `connectTime` / `retryTimes` (string seconds/count, designer defaults
324
332
  // "10"/"3"); the 绑定业务方法 dialog and the runtime read them, so a
@@ -401,7 +409,19 @@ export function compileParameterFace(adapterCode, parameters, label) {
401
409
  if (!Array.isArray(parameters))
402
410
  refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${label} 必须是结构化参数数组(ParameterModel[])`, { label });
403
411
  const defaultRequired = isOutputFace(label) ? false : undefined;
404
- const normalized = parameters.map((parameter, index) => compileParameter(adapterCode, parameter, label, `${label}[${index}]`, index, defaultRequired));
412
+ // Live 2026-10-18 (SubCon): a 7-method registration failed one parameter at a
413
+ // time across two rounds — the whole batch was rewritten for a single enum
414
+ // typo, then again for a missing output position. Collect EVERY violation and
415
+ // refuse once with the full list, so one pass fixes the whole face.
416
+ const violations = [];
417
+ const normalized = parameters.map((parameter, index) => compileParameter(adapterCode, parameter, label, `${label}[${index}]`, index, defaultRequired, violations));
418
+ if (violations.length > 0)
419
+ throw new ToolRefusalError({
420
+ status: "refused",
421
+ blocker: BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid,
422
+ message: `${label} 参数面有 ${violations.length} 处非法项(一次性列出,逐项修正后一次重试):` + violations.map((v) => `${v.path}${v.code ? ` (${v.code})` : ""}: ${v.message}`).join(";"),
423
+ issues: violations,
424
+ });
405
425
  return JSON.stringify(normalized);
406
426
  }
407
427
  /** True when `label` names the RETURN parameter face (outputParametersJson). */
@@ -417,32 +437,51 @@ function isOutputFace(label) {
417
437
  * leaf → 1000 PROPERTY, a node with subParameters → 1002 OBJECT) and
418
438
  * `description`. RESTFUL additionally requires configJson.position.
419
439
  */
420
- function compileParameter(adapterCode, parameter, label, path, index, defaultRequired) {
421
- if (!isRecord(parameter))
422
- refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${path} 必须是对象`, { label, index });
440
+ function compileParameter(adapterCode, parameter, label, path, index, defaultRequired, violations) {
441
+ const collect = (message, code) => {
442
+ if (violations)
443
+ violations.push({ path, ...(code ? { code } : {}), message });
444
+ else
445
+ refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${path}${code ? ` (${code})` : ""}: ${message}`, { label, index, ...(code ? { code } : {}) });
446
+ };
447
+ if (!isRecord(parameter)) {
448
+ collect("必须是对象");
449
+ return {};
450
+ }
423
451
  if (typeof parameter.code !== "string" || !parameter.code)
424
- refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${path} 缺少 code(绑定校验按 code 对账)`, { label, index });
452
+ collect("缺少 code(绑定校验按 code 对账)");
425
453
  if (parameter.required !== undefined && typeof parameter.required !== "boolean")
426
- refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${path}.required 必须是布尔(缺席=未声明必填,不会被补成 false)`, { label, index });
454
+ collect("required 必须是布尔(缺席=未声明必填,不会被补成 false)");
427
455
  const position = parameterPosition(parameter);
456
+ const validPositions = Object.values(RESTFUL_PARAMETER_POSITIONS);
457
+ const positionIsValid = (typeof position === "number" && Number.isInteger(position) && position in RESTFUL_PARAMETER_POSITIONS) ||
458
+ (typeof position === "string" && validPositions.includes(position));
428
459
  if (adapterCode === "RESTFUL") {
429
460
  if (position === undefined)
430
- refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${path} (${parameter.code}) 缺 configJson.position —— RESTFUL 参数无 position 在 invoke 时 NPE(RestProtocolAdapterRuntimeServiceImpl.java:197)`, { label, index, code: parameter.code });
431
- if (position === 1 || position === "URL_VARIABLE")
432
- refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${path} (${parameter.code}) position=URL_VARIABLE 被运行时拒绝(501007,无对应分支);用 QUERY_PARAMS/REQUEST_HEADER/REQUEST_BODY/PATH_PARAMS`, { label, index, code: parameter.code });
461
+ collect("缺 configJson.position —— RESTFUL 参数无 position 在 invoke 时 NPE(RestProtocolAdapterRuntimeServiceImpl.java:197)", typeof parameter.code === "string" ? parameter.code : undefined);
462
+ else if (!positionIsValid)
463
+ collect(`position 枚举名 ${String(position)} 不在已取证集合 ${validPositions.join("|")} 内(REQUEST_PARAM 不存在)`, typeof parameter.code === "string" ? parameter.code : undefined);
464
+ else if (position === 1 || position === "URL_VARIABLE")
465
+ collect("position=URL_VARIABLE 被运行时拒绝(501007,无对应分支);用 QUERY_PARAMS/REQUEST_HEADER/REQUEST_BODY/PATH_PARAMS", typeof parameter.code === "string" ? parameter.code : undefined);
433
466
  }
434
467
  // Platform canonical: `ParameterModel.configJson` is a JSON STRING column
435
468
  // whose content is the nested config OBJECT with the WIRE INTEGER position
436
469
  // (platform built-ins: "configJson":{"position":3}). Encode the enum and
437
470
  // emit the object form; a string input is parsed so we never double-encode.
438
471
  const configInput = isRecord(parameter.configJson) ? parameter.configJson : typeof parameter.configJson === "string" ? safeParse(parameter.configJson) : undefined;
439
- const configJson = adapterCode === "RESTFUL"
440
- ? { ...(isRecord(configInput) ? configInput : {}), position: encodeEnumIndex(position, RESTFUL_PARAMETER_POSITIONS, "position") }
441
- : parameter.configJson;
442
- const bizPropertyType = resolveBizPropertyType(parameter, label, index, path);
443
- const externParameterType = resolveExternParameterType(parameter, label, index);
472
+ let configJson = parameter.configJson;
473
+ if (adapterCode === "RESTFUL") {
474
+ const usable = positionIsValid && position !== 1 && position !== "URL_VARIABLE";
475
+ configJson = usable
476
+ ? { ...(isRecord(configInput) ? configInput : {}), position: encodeEnumIndex(position, RESTFUL_PARAMETER_POSITIONS, "position") }
477
+ : isRecord(configInput)
478
+ ? configInput
479
+ : parameter.configJson;
480
+ }
481
+ const bizPropertyType = resolveBizPropertyType(parameter, label, index, path, collect);
482
+ const externParameterType = resolveExternParameterType(parameter, label, index, collect);
444
483
  const subParameters = Array.isArray(parameter.subParameters)
445
- ? parameter.subParameters.map((sub, subIndex) => compileParameter(adapterCode, sub, label, `${path}.subParameters[${subIndex}]`, subIndex, defaultRequired))
484
+ ? parameter.subParameters.map((sub, subIndex) => compileParameter(adapterCode, sub, label, `${path}.subParameters[${subIndex}]`, subIndex, defaultRequired, violations))
446
485
  : parameter.subParameters;
447
486
  return {
448
487
  ...parameter,
@@ -457,24 +496,35 @@ function compileParameter(adapterCode, parameter, label, path, index, defaultReq
457
496
  };
458
497
  }
459
498
  /** Resolve/validate a parameter's `bizPropertyType` (int index or enum name). Absent → refuse (fail-closed). */
460
- function resolveBizPropertyType(parameter, label, index, path) {
499
+ function resolveBizPropertyType(parameter, label, index, path, collect) {
461
500
  const raw = parameter.bizPropertyType;
501
+ const fail = (message) => {
502
+ if (collect) {
503
+ collect(message);
504
+ return BIZ_PROPERTY_TYPE_NAMES.SHORT_TEXT;
505
+ }
506
+ refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${path} (${parameter.code}) ${message}`, { label, index, code: parameter.code });
507
+ };
462
508
  if (raw === undefined || raw === null)
463
- refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${path} (${parameter.code}) 缺 bizPropertyType(参数类型)——业务集成参数必须有类型,否则业务规则按类型绑定失败、参数无法映射;用 BIZ_PROPERTY_TYPES 索引或枚举名(SHORT_TEXT/NUMERICAL/RADIO/DROPDOWN_BOX…)`, { label, index, code: parameter.code });
509
+ return fail("缺 bizPropertyType(参数类型)——业务集成参数必须有类型,否则业务规则按类型绑定失败、参数无法映射;用 BIZ_PROPERTY_TYPES 索引或枚举名(SHORT_TEXT/NUMERICAL/RADIO/DROPDOWN_BOX…)");
464
510
  if (typeof raw === "number" && Number.isInteger(raw) && raw in BIZ_PROPERTY_TYPES)
465
511
  return raw;
466
512
  if (typeof raw === "string" && raw in BIZ_PROPERTY_TYPE_NAMES)
467
513
  return BIZ_PROPERTY_TYPE_NAMES[raw];
468
- refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${path} (${parameter.code}) bizPropertyType 必须是合法字段类型(整数索引或枚举名,如 SHORT_TEXT/NUMERICAL/RADIO/DROPDOWN_BOX)——缺失或有误会破坏业务规则按类型绑定`, { label, index, code: parameter.code });
514
+ return fail("bizPropertyType 必须是合法字段类型(整数索引或枚举名,如 SHORT_TEXT/NUMERICAL/RADIO/DROPDOWN_BOX)——缺失或有误会破坏业务规则按类型绑定");
469
515
  }
470
516
  /** Resolve/validate a parameter's `externParameterType` (int index or name); default leaf=PROPERTY(1000)/parent=OBJECT(1002). */
471
- function resolveExternParameterType(parameter, label, index) {
517
+ function resolveExternParameterType(parameter, label, index, collect) {
472
518
  const raw = parameter.externParameterType;
473
519
  if (raw !== undefined) {
474
520
  if (typeof raw === "number" && Number.isInteger(raw) && raw in EXTERN_PARAMETER_TYPES)
475
521
  return raw;
476
522
  if (typeof raw === "string" && raw in EXTERN_PARAMETER_NAMES)
477
523
  return EXTERN_PARAMETER_NAMES[raw];
524
+ if (collect) {
525
+ collect("externParameterType 必须是 PROPERTY(1000)/LIST(1001)/OBJECT(1002) 的整数索引或枚举名");
526
+ return EXTERN_PARAMETER_NAMES.PROPERTY;
527
+ }
478
528
  refuse(BIZSERVICE_CONFIG_BLOCKERS.parameterFaceInvalid, `${label}[${index}] (${parameter.code}) externParameterType 必须是 PROPERTY(1000)/LIST(1001)/OBJECT(1002) 的整数索引或枚举名`, { label, index, code: parameter.code });
479
529
  }
480
530
  const hasChildren = Array.isArray(parameter.subParameters) && parameter.subParameters.length > 0;
package/dist/cli.js CHANGED
@@ -209,7 +209,24 @@ async function cmdDoctor(store, environment, serviceCode) {
209
209
  }
210
210
  catch (error) {
211
211
  ok = false;
212
- steps.push(`自检 FAIL ${redactText(error.message, [])}`);
212
+ // Distinguish "the project has not pinned an environment" (usable once you
213
+ // name one) from "the platform is unreachable" — conflating them sent a
214
+ // sandbox probe guessing environment names five times (r18).
215
+ const message = redactText(error.message, []);
216
+ let configured = "";
217
+ try {
218
+ configured = store.listEnvironments().map((item) => item.name).join("、");
219
+ }
220
+ catch {
221
+ configured = "";
222
+ }
223
+ const kind = /no default environment|not configured|unknown environment|not configured in omc\.config\.json/i.test(message)
224
+ ? "env-not-pinned"
225
+ : "env-unreachable";
226
+ steps.push(`自检 FAIL ${message}`);
227
+ steps.push(kind === "env-not-pinned"
228
+ ? `环境 ${kind} 已配置环境:${configured || "(none)"};用 omc env list 查看,再以 --env <name> 指定或 omc setup 绑定;对象不存在(modelPresent:false)不是环境错误`
229
+ : `环境 ${kind} 环境已配置但平台不可达;先查网络/服务,不要改环境名试错(已配置:${configured || "(none)"})`);
213
230
  }
214
231
  for (const line of steps)
215
232
  console.error(line);
@@ -935,6 +952,25 @@ async function main() {
935
952
  command.command("setup").option("--list").option("--edit <environment>").option("--remove <environment>").action(async (options) => {
936
953
  commandExitCode = await cmdSetup(new ConfigStore(defaultPaths()), [...(options.list ? ["--list"] : []), ...(options.edit ? ["--edit", options.edit] : []), ...(options.remove ? ["--remove", options.remove] : [])]);
937
954
  });
955
+ command.command("env [action]").option("--json").action(async (action, options) => {
956
+ // Read-only inventory of configured environment names. r18: the sandbox
957
+ // guessed names instead of listing them; doctor's wording now points here.
958
+ const store = new ConfigStore(defaultPaths());
959
+ if (action !== undefined && action !== "list") {
960
+ console.error("用法:omc env list [--json]");
961
+ commandExitCode = CLI_EXIT_CODES.invalidInput;
962
+ return;
963
+ }
964
+ 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 }));
965
+ if (options.json)
966
+ process.stdout.write(JSON.stringify({ ok: true, environments: rows }, null, 2) + "\n");
967
+ else if (rows.length === 0)
968
+ console.error("(none configured; run omc setup)");
969
+ else
970
+ for (const row of rows)
971
+ console.error(`${row.name}${row.isDefault ? " (default)" : ""} ${row.baseUrl} app=${row.applicationCode}${row.writable ? "" : " [read-only]"}`);
972
+ commandExitCode = CLI_EXIT_CODES.ok;
973
+ });
938
974
  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); });
939
975
  command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--max-output-bytes <bytes>").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
940
976
  const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs !== undefined ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
@@ -129,11 +129,11 @@ export const B2_LANE_CONTRACT = Object.freeze({
129
129
  }),
130
130
  "method.update": Object.freeze({
131
131
  scope: "method update readback",
132
- description: "原地更新业务集成方法(`/bizservice/method/update`):方法 configJson 由工具 AES 加密,参数面结构化序列化并回读;保留方法身份与规则引用。",
132
+ description: "原地更新业务集成方法(`/bizservice/method/update`),RESTFUL 参数 position 仅 QUERY_PARAMS|URL_VARIABLE|REQUEST_HEADER|REQUEST_BODY|PATH_PARAMS(写入名字、读回整数索引),非法项一次性全列。方法 configJson 由工具 AES 加密,参数面结构化序列化并回读;保留方法身份与规则引用。",
133
133
  }),
134
134
  "method.create": Object.freeze({
135
135
  scope: "method create readback",
136
- description: "在**已存在的业务服务**下新增方法(`/bizservice/method/create`):新写一个扩展接口通常不是新建服务,而是往已有服务里加方法。服务不存在 → refused(新增服务用 `service.register`);方法已存在 → refused(改用 `method.update`)。configJson 由工具 AES 加密,参数面结构化序列化并回读。**RESTFUL 参数面**:入参与出参的每个参数都要 `configJson.position`,合法值只有 `QUERY_PARAMS | URL_VARIABLE | REQUEST_HEADER | REQUEST_BODY | PATH_PARAMS`(`REQUEST_PARAM` 不存在;`URL_VARIABLE` 运行时 501007 被拒)。平台注入的常量头(如 X-LowCode-Corpid)标 `required:false`,否则 method.test 会按缺参 fail-closed。写入用枚举名,读回是整数索引(0 QUERY_PARAMS / 1 URL_VARIABLE / 2 REQUEST_HEADER / 3 REQUEST_BODY / 4 PATH_PARAMS)。",
136
+ description: "在**已存在的业务服务**下新增方法(`/bizservice/method/create`),RESTFUL 参数 position 仅 QUERY_PARAMS|URL_VARIABLE|REQUEST_HEADER|REQUEST_BODY|PATH_PARAMS,入参与出参都必带,常量头 required:false,非法项一次性全列。新写一个扩展接口通常不是新建服务,而是往已有服务里加方法。服务不存在 → refused(新增服务用 `service.register`);方法已存在 → refused(改用 `method.update`)。configJson 由工具 AES 加密,参数面结构化序列化并回读。**RESTFUL 参数面**:入参与出参的每个参数都要 `configJson.position`,合法值只有 `QUERY_PARAMS | URL_VARIABLE | REQUEST_HEADER | REQUEST_BODY | PATH_PARAMS`(`REQUEST_PARAM` 不存在;`URL_VARIABLE` 运行时 501007 被拒)。平台注入的常量头(如 X-LowCode-Corpid)标 `required:false`,否则 method.test 会按缺参 fail-closed。写入用枚举名,读回是整数索引(0 QUERY_PARAMS / 1 URL_VARIABLE / 2 REQUEST_HEADER / 3 REQUEST_BODY / 4 PATH_PARAMS)。",
137
137
  }),
138
138
  "method.delete": Object.freeze({
139
139
  scope: "method delete readback",
@@ -61,9 +61,10 @@ function safeParseArray(value) {
61
61
  function parseDeclaredParameterCodes(inputParametersJson) {
62
62
  const parsed = typeof inputParametersJson === "string" ? safeParseArray(inputParametersJson) : Array.isArray(inputParametersJson) ? inputParametersJson : undefined;
63
63
  if (!parsed)
64
- return { all: [], required: [] };
64
+ return { all: [], required: [], constants: [] };
65
65
  const all = [];
66
66
  const required = [];
67
+ const constants = [];
67
68
  for (const entry of parsed) {
68
69
  if (!entry || typeof entry !== "object")
69
70
  continue;
@@ -73,8 +74,12 @@ function parseDeclaredParameterCodes(inputParametersJson) {
73
74
  all.push(code);
74
75
  if (entry.required === true)
75
76
  required.push(code);
77
+ // codeType 0 = CONST: the platform injects the value; a null/blank test
78
+ // value is a declaration problem, not a missing business input.
79
+ if (entry.codeType === 0 || entry.codeType === "0")
80
+ constants.push(code);
76
81
  }
77
- return { all, required };
82
+ return { all, required, constants };
78
83
  }
79
84
  /**
80
85
  * Ticket T08 — readback face for a service LIST row: an explicit field
@@ -1280,6 +1285,18 @@ export function registerB2Lane(tools) {
1280
1285
  const missing = face.required.filter((code) => !(code in args.testInputParametersMap));
1281
1286
  if (missing.length > 0)
1282
1287
  throw new ToolRefusalError({ status: "refused", blocker: "bizservice-test-param-missing", message: `试跑缺必填入参:${missing.join("、")}(按方法声明参数面 fail-closed,零试跑调用)`, missing });
1288
+ // Live 2026-10-18 (SubCon): the test readback echoed
1289
+ // {"x-lowcode-enginecode":null,...} and the caller only understood it
1290
+ // after eating a 401. Flag declared CONST parameters supplied as
1291
+ // null/blank BEFORE the wire: the platform injects them, so a null here
1292
+ // means "the declaration/injection is wrong", not "the test input is
1293
+ // missing".
1294
+ const blankConstants = face.constants.filter((code) => {
1295
+ if (!(code in args.testInputParametersMap))
1296
+ return true;
1297
+ const value = args.testInputParametersMap[code];
1298
+ return value === null || value === undefined || (typeof value === "string" && value.trim() === "");
1299
+ });
1283
1300
  const unknown = supplied.filter((code) => !face.all.includes(code));
1284
1301
  if (unknown.length > 0)
1285
1302
  throw new ToolRefusalError({ status: "refused", blocker: "bizservice-test-param-unknown", message: `试跑携带未声明参数:${unknown.join("、")}(方法参数面只有 ${face.all.join("、") || "(空)"};不猜不透传,零试跑调用)`, unknown });
@@ -1329,6 +1346,7 @@ export function registerB2Lane(tools) {
1329
1346
  result: performed.data,
1330
1347
  configUnchanged,
1331
1348
  externalSuccess,
1349
+ ...(blankConstants.length ? { constantHeaderWarnings: blankConstants, constantHeaderHint: "平台注入的常量参数在试跑回显为 null/空——先确认参数声明 codeType:0 + bizCode(并为它们标 required:false);出现 401/EngineCode 空头时优先查这里,而不是改业务入参" } : {}),
1332
1350
  ...(resultBodyErrcode === undefined ? {} : { resultBodyErrcode }),
1333
1351
  ...(externalSuccess ? {} : { externalFailure }),
1334
1352
  },
@@ -74,7 +74,8 @@ export function registerBusinessRuleLane(tools) {
74
74
  inputSchema: {
75
75
  environment: environmentArg,
76
76
  schemaCode: z.string().min(1),
77
- full: z.boolean().optional().describe("行内联完整字段(id/createdTime/creator/…,并跳过 detailFile 外置);默认行只有 code/name/enabled/status/ruleVersion/defaultRule/latestPublished——平台预置触发行(defaultRule:true)客户端自滤"),
77
+ onlyCustom: z.boolean().optional().describe("只回自建规则(defaultRule!==true),滤掉平台预置触发行——读面收窄,省 token"),
78
+ full: z.boolean().optional().describe("行内联完整字段(id/createdTime/creator/…,并跳过 detailFile 外置);默认行只有 code/name/enabled/status/ruleVersion/defaultRule/latestPublished"),
78
79
  capability: capabilityArg,
79
80
  },
80
81
  handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
@@ -85,9 +86,11 @@ export function registerBusinessRuleLane(tools) {
85
86
  // with full:true (the old 14-key row). rule.get resolves by business
86
87
  // key (schemaCode+ruleCode), so the row id is rarely needed.
87
88
  const fullRows = args.full === true;
89
+ const visible = args.onlyCustom === true ? rows.filter((row) => row.defaultRule !== true) : rows;
88
90
  return {
89
91
  schemaCode: args.schemaCode,
90
- rules: rows.map((row) => ({
92
+ ...(args.onlyCustom === true ? { filtered: { onlyCustom: true, total: rows.length, returned: visible.length } } : {}),
93
+ rules: visible.map((row) => ({
91
94
  code: row.code,
92
95
  name: row.name,
93
96
  enabled: row.enabled,
@@ -499,8 +499,16 @@ export function registerRuntimeDataLane(tools) {
499
499
  const selected = fields ?? Object.keys(data).filter((field) => !["id", "bizObjectId", "schemaCode", "sequenceStatus", "createdTime", "updatedTime", "creator", "updater"].includes(field));
500
500
  return { id: row.bizObjectId ?? row.id ?? data.id, fields: Object.fromEntries(selected.map((field) => [field, data[field] ?? null])) };
501
501
  });
502
+ // Live 2026-10-19: when a model has NO configured list view, the
503
+ // platform projects only system columns — every business field comes
504
+ // back null while `data.load` shows the real values. That reads as
505
+ // "the write failed". Detect an all-null business projection (items
506
+ // present, zero non-null business values) and say so.
507
+ const businessKeys = fields ?? Object.keys(items[0]?.fields ?? {}).filter((field) => !["id", "bizObjectId", "schemaCode", "sequenceStatus", "createdTime", "updatedTime", "creator", "updater", "name"].includes(field));
508
+ const projectionEmpty = items.length > 0 && businessKeys.length > 0 && items.every((item) => businessKeys.every((key) => item.fields[key] === null || item.fields[key] === undefined));
502
509
  const readback = {
503
510
  schemaCode: args.schemaCode,
511
+ ...(projectionEmpty ? { projectionWarning: { reason: "business-columns-null", hint: "模型可能未配置列表视图:data.list 只投影列表列,业务列会全为 null;核对写入结果请用 data.load(或先建列表视图)" } } : {}),
504
512
  ...(args.queryCode !== undefined ? { queryCode: args.queryCode } : {}),
505
513
  total: args.showTotal === false ? null : page.total,
506
514
  // This branch is the non-`all` read: the platform's default filter
@@ -914,7 +914,21 @@ async function childTableFieldsOf(session, schemaCode) {
914
914
  };
915
915
  const rows = (await read("false")).length > 0 ? await read("false") : await read("true");
916
916
  return rows
917
- .map((row) => ({ code: String(row.code ?? row.propertyCode ?? ""), name: String(row.name ?? row.code ?? ""), isChild: Number(row.type ?? row.propertyType ?? 0) === 8 || String(row.type ?? row.propertyType ?? "").toUpperCase() === "CHILD_TABLE" || row.subSchema !== undefined }))
917
+ .map((row) => {
918
+ const type = row.propertyType ?? row.type ?? 0;
919
+ // Live 2026-10-19: the list face carries `subSchema: null` on EVERY row,
920
+ // so `subSchema !== undefined` classified every scalar/relation field as
921
+ // a child table and refused `required` with a false reason. Only a
922
+ // NON-EMPTY subSchema marks a child table (plus the real type code:
923
+ // propertyType 8 = CHILD_TABLE / SHEET).
924
+ const subSchema = row.subSchema;
925
+ const hasSubSchema = subSchema !== undefined && subSchema !== null && String(subSchema).trim() !== "";
926
+ const isChild = Number(type) === 8 ||
927
+ String(type).toUpperCase() === "CHILD_TABLE" ||
928
+ String(type).toUpperCase() === "SHEET" ||
929
+ hasSubSchema;
930
+ return { code: String(row.code ?? row.propertyCode ?? ""), name: String(row.name ?? row.code ?? ""), isChild };
931
+ })
918
932
  .filter((row) => row.isChild && row.code !== "");
919
933
  }
920
934
  const CHILD_TABLE_LIST_BUTTONS = [
@@ -80,8 +80,77 @@ const bizserviceSelfCallCorp = Object.freeze({
80
80
  ],
81
81
  requiresEnvironmentDerivedIds: false,
82
82
  });
83
+ const methodCreateRestfulGet = Object.freeze({
84
+ id: "method-create-restful-get",
85
+ title: "给既有服务加一个 RESTFUL GET 方法(query 参数 + 常量头 + 出参 position)",
86
+ status: "offline-valid",
87
+ tool: "method.create",
88
+ args: {
89
+ environment,
90
+ serviceCode: "<service_code>",
91
+ methodCode: "querySupplier",
92
+ name: "供应商查询",
93
+ methodConfig: { httpType: "Get", requestBodyType: "JSON", url: "/openapi/supplier/query", frontEndExtensionEnable: true },
94
+ methodInputParameters: [
95
+ { code: "keyword", name: "关键字", required: true, configJson: { position: "QUERY_PARAMS" } },
96
+ { code: "x-lowcode-corpId", name: "企业ID(平台注入常量)", required: false, codeType: 0, bizCode: "<corpId>", configJson: { position: "REQUEST_HEADER" } },
97
+ ],
98
+ methodOutputParameters: [{ code: "name", name: "供应商名称", configJson: { position: "REQUEST_BODY" } }],
99
+ },
100
+ preconditions: ["服务已存在(method.create 只加方法);methodCode 未存在;RESTFUL 每个入参与出参都带 configJson.position"],
101
+ conditionalRequirements: [
102
+ "position 仅 QUERY_PARAMS/URL_VARIABLE/REQUEST_HEADER/REQUEST_BODY/PATH_PARAMS;URL_VARIABLE 运行时 501007",
103
+ "平台注入常量头标 required:false,否则 method.test 缺参 fail-closed;常量值放 codeType:0 + bizCode",
104
+ "方法 url 走 /openapi/**(单层 + xclient);/api/** 是双层用户态,混用鉴权即 401",
105
+ ],
106
+ priorToolReferences: [
107
+ { tool: "service.get", args: { environment, serviceCode: "<service_code>" }, purpose: "确认服务存在、adapterCode=RESTFUL,并照抄既有方法的 url/auth 形状" },
108
+ ],
109
+ dryRun: { supported: true, command: `omc call method.create --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 只做参数面本地预检,零 wire" },
110
+ expectedReadback: { performed: { errcode: 0 }, readback: { methodPresent: true }, next: ["method.get 复核参数面(position 读回为整数索引)"] },
111
+ refusalRecovery: [
112
+ { code: "bizservice-parameter-face-invalid", action: "按 issues 一次性修完全部非法项;position 枚举名拼写见 summary" },
113
+ { code: "duplicate-method", action: "方法已存在;改用 method.update" },
114
+ ...commonRecovery,
115
+ ],
116
+ requiresEnvironmentDerivedIds: true,
117
+ prerequisiteData: { source: "service.get", fields: ["serviceCode", "adapterCode"] },
118
+ });
119
+ const methodCreateRestfulPost = Object.freeze({
120
+ id: "method-create-restful-post",
121
+ title: "给既有服务加一个 RESTFUL POST 方法(JSON body)",
122
+ status: "offline-valid",
123
+ tool: "method.create",
124
+ args: {
125
+ environment,
126
+ serviceCode: "<service_code>",
127
+ methodCode: "saveSupplier",
128
+ name: "供应商保存",
129
+ methodConfig: { httpType: "Post", requestBodyType: "JSON", url: "/openapi/supplier/save", frontEndExtensionEnable: true },
130
+ methodInputParameters: [{ code: "body", name: "供应商对象", required: true, configJson: { position: "REQUEST_BODY" } }],
131
+ methodOutputParameters: [{ code: "id", name: "主键", configJson: { position: "REQUEST_BODY" } }],
132
+ },
133
+ preconditions: ["服务已存在;methodCode 未存在;POST 入参 position=REQUEST_BODY,requestBodyType=JSON"],
134
+ conditionalRequirements: [
135
+ "出参也必须带 position(缺它会写成功但 invoke NPE)",
136
+ "方法 url 走 /openapi/** + xclient;不要用 /externalLink 免鉴权前缀",
137
+ ],
138
+ priorToolReferences: [
139
+ { tool: "service.get", args: { environment, serviceCode: "<service_code>" }, purpose: "照抄同 adapter 既有方法的 url 前缀与鉴权" },
140
+ ],
141
+ dryRun: { supported: true, command: `omc call method.create --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 零 wire" },
142
+ expectedReadback: { performed: { errcode: 0 }, readback: { methodPresent: true }, next: ["method.test 跑一条最小实参"] },
143
+ refusalRecovery: [
144
+ { code: "bizservice-parameter-face-invalid", action: "按 issues 一次性修全" },
145
+ { code: "duplicate-method", action: "改用 method.update" },
146
+ ...commonRecovery,
147
+ ],
148
+ requiresEnvironmentDerivedIds: true,
149
+ prerequisiteData: { source: "service.get", fields: ["serviceCode", "adapterCode"] },
150
+ });
83
151
  export const B2_METADATA = Object.freeze({
84
152
  "service.register": Object.freeze({ recipes: [restfulQueryService], relatedTools: ["service.category.list", "service.adapter_config", "service.get", "method.get"] }),
85
153
  "method.update": Object.freeze({ recipes: [bizserviceSelfCallCorp], relatedTools: ["method.get", "method.create", "service.get"] }),
154
+ "method.create": Object.freeze({ recipes: [methodCreateRestfulGet, methodCreateRestfulPost], relatedTools: ["method.get", "method.update", "service.get", "method.test"] }),
86
155
  });
87
156
  //# sourceMappingURL=b2-registration.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.55",
3
+ "version": "0.4.57",
4
4
  "description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,9 +50,9 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@stalfh233/omc-fake-platform": "0.4.55",
53
+ "@stalfh233/omc-fake-platform": "0.4.57",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.55",
55
+ "omc-core": "0.4.57",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",
@@ -238,7 +238,8 @@ omc call service.get --env local --args '{"serviceCode":"omc_demo_restful"}'
238
238
  - **写侧用名字、读侧是整数索引**(`{"position":2}` = `REQUEST_HEADER`)——核对时不要因类型变化误判"没写进去"。映射以写入名为准。
239
239
  - **常量头(平台注入,如 `X-LowCode-Corpid`/`X-LowCode-EngineCode`)标 `required:false`**:标成必填会让 `method.test` 判缺参而 fail-closed;常量值放 `codeType:0` + `bizCode`。
240
240
  - `method.test` 缺常量头时拒跑是设计:要么先补齐实参,要么把常量声明为 `required:false`。
241
- - 一次性把**全部**非法参数项改完再重试;逐项失败会按方法数放大往返。
241
+ - 一次性把**全部**非法参数项改完再重试;0.4.56 起校验会一次列出全部非法项(`issues[]`)。
242
+ - **请求头与查询参数都写在方法参数面**(`REQUEST_HEADER` / `QUERY_PARAMS`),不要另外写代码拦截器注入;平台按参数面注入,声明处就是事实源。
242
243
 
243
244
  ### 方法 URL 前缀 × 鉴权方式(混用即 401)
244
245
 
@@ -105,11 +105,12 @@ process exits), then retry. This guards concurrency, not approval.
105
105
 
106
106
  ## 8. Runtime / gateway errors (not tool refusals, same discipline)
107
107
 
108
- - `401 权限不够` (bizservice invoke / manual call) → check **URL prefix × auth** first: `/openapi/**` uses `xclient` (Client Credentials); `/api/api/**` uses user-identity token. Mixing them is a guaranteed 401. Compare with an existing successful service under the same adapter (`service.get` → method url).
109
- - `500` with no errcode, body only `header:X-LowCode-EngineCode should not be empty!` → add the multi-tenant header; not a business error.
110
- - `404 Not Found ... openresty` / `当前业务方法绑定的API在服务端找不到` → the method url prefix/layer count is wrong (double/triple `/api`); fix per the matrix, don't keep re-editing the url.
111
- - `position` read back as an integer while written as an enum name → read-side encoding, not a failed write.
112
- - **Version evidence missing**:先区分 **`pcServerUrl` 缺失**(读取耦合缺陷,0.4.54 已修,不再 fail-closed)与 **`systemVersion` 真缺失**(此时 fail-closed 正确)。不要伪造 8.6 版本。
108
+ - `401 权限不够`(业务集成)→ 先查**前缀×鉴权**:`/openapi/**` 用 `xclient`,`/api/api/**` 用用户 token;混用必 401。对照同 adapter 既有成功服务(`service.get` 的方法 url)。
109
+ - `500` 且仅 `X-LowCode-EngineCode should not be empty` → 补多租户头,不是业务错误。
110
+ - `404 openresty` / `API 在服务端找不到` → url 层数不对(双层/三层 `/api`),按矩阵改一次,别反复试。
111
+ - `position` 读回整数、写入枚举名 → 读侧编码,不是没写进去。
112
+ - 版本证据缺失:先分 `pcServerUrl` 缺失(读取耦合,0.4.54 已修)与 `systemVersion` 真缺失(fail-closed 正确);不要伪造 8.6。
113
+ - `401/403` 且 body 是 HTML/空 → 网关层,不是业务拒绝;停手索要一条可运行样例,别继续改载荷。
113
114
 
114
115
  ## 9. Input / target
115
116
 
@@ -38,6 +38,14 @@ omc help omc-write-hazards
38
38
  - **运行时写值的最小形状**(组合出错只会得到一个不透明的 `50000`/NPE,逐一最小化再合):单选关联(9) → id 字符串 `"<id>"`;**人员/组织(50/51/60/61) → `[{"id":"<id>","type":3,"name":"<名>"}]`,`type` 必填且为整数**(缺/非整数 → `BizField.parseFromClient` 拆箱 NPE→50000;用户=3、部门=1);日期(3) → 字符串 `"2026-09-30 10:00:00"`;子表(8) → 行对象数组 `[{"<子列code>":<值>, ...}]`。先各写一条最小行通过,再组合。
39
39
  - **DATE(3) 字段的 `options.format` 必须是平台大写枚举**(`YYYY-MM-DD`、`YYYY-MM-DD HH:mm:ss`、`YYYY年MM月DD日`…),**不是 Java 的 `yyyy-MM-dd`**:平台 `DateBizField` 按字面比较,Java 写法能创建字段但**首次运行时 save 稳定 `50000`(`不支持的format`)**(2026-09-30 R5 实测)。`field.create`/`intent` 会就地拒绝非枚举值。
40
40
 
41
+ ## 业务规则与流程事件(源码实证,1.1.25)
42
+
43
+ - **`Create` 的触发判定是「旧状态→新状态」**:旧 `DRAFT` 且新 `PROCESSING` 或 `COMPLETED`(`DomainServiceImpl#getDefaultRuleCode`)。所以无流程模型一调 `data.submit`(→COMPLETED)**和流程表单提交(DRAFT→PROCESSING)都触发 Create**;`data.save` 存草稿不触发。不要因为"目标表没变化"就断言规则没触发。
44
+ - **副作用缺失归因三步**:先查执行记录(`h_log_biz_service` 按服务/方法/时间,或容器日志)→ ①无调用=规则/触发问题 ②有调用但失败=参数/权限/业务异常 ③调用成功但不可见=视图投影/事务未提交/查询条件。禁止只看目标表。
45
+ - **BIZ_ACTION 判成功只看传输**:REST 适配器解析响应后直接返回,**不校验 `errcode`**。扩展接口回 `{"errcode":50000}` + HTTP 200 会被当成节点成功(日志 `biz action node execute successful`)。要阻断必须走表单 `onValidate` 或数据规则,不能靠业务返回码。
46
+ - **流程事件处理器吞异常**:`start`/`end`/`cancel`/`activate` 中的 BIZ_ACTION 抛异常时,`InstanceContext#processingBusiness` 只记 `processingBusiness error.` 并返回失败结果,**流程继续**。事件处理器不能做"提交前拦截"。
47
+ - **禁写当前单据**:BIZ_ACTION 是 HTTP 自调用,外层事务已持行锁;同事务更新当前单据会等到 15s 超时并整条回滚。副作用写独立表;需要留痕用独立事务/异步重建 `RequestContext`。
48
+
41
49
  ## 运行数据读取的默认过滤
42
50
 
43
51
  - `data.list` **默认不含 DRAFT**(`total` 也只算 COMPLETED):清测试数据时 `total:0` 不代表没有残留。核对/清理一律显式 `{"all":true,"showTotal":true}`,或读回体里的过滤标注。`data.load` 用于单行核对。