@stalfh233/omc-cli 0.6.7 → 0.6.10
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 +2 -1
- package/dist/approval-flow.js +5 -3
- package/dist/bizservice-config.js +26 -10
- package/dist/call.js +47 -0
- package/dist/capability-overview.js +47 -24
- package/dist/cli-contract.js +73 -26
- package/dist/cli-output.js +41 -2
- package/dist/cli-stdout.js +74 -0
- package/dist/cli.js +135 -52
- package/dist/command-transport.js +8 -1
- package/dist/config-store.js +17 -2
- package/dist/discovery.js +62 -3
- package/dist/field-families.js +35 -6
- package/dist/goal-routes.js +118 -11
- package/dist/human-summary.js +11 -0
- package/dist/lanes/app-contract.js +12 -5
- package/dist/lanes/app.js +221 -134
- package/dist/lanes/apply-changes.js +2 -1
- package/dist/lanes/b2-registration-contract.js +3 -3
- package/dist/lanes/b2-registration.js +890 -693
- package/dist/lanes/businessrule.js +75 -51
- package/dist/lanes/custom-page.js +166 -115
- package/dist/lanes/datarule-contract.js +2 -2
- package/dist/lanes/datarule.js +238 -154
- package/dist/lanes/dictionary.js +253 -110
- package/dist/lanes/environment.js +57 -9
- package/dist/lanes/field.js +160 -107
- package/dist/lanes/folder-contract.js +7 -3
- package/dist/lanes/folder.js +249 -118
- package/dist/lanes/form-contract.js +1 -1
- package/dist/lanes/form.js +188 -64
- package/dist/lanes/list-view.js +405 -369
- package/dist/lanes/meta.js +31 -6
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +610 -512
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +160 -146
- package/dist/lanes/organization.js +905 -640
- package/dist/lanes/programming.js +274 -136
- package/dist/lanes/report-contract.js +2 -2
- package/dist/lanes/report.js +354 -198
- package/dist/lanes/rule-lifecycle.js +294 -140
- package/dist/lanes/runtime-data-contract.js +4 -4
- package/dist/lanes/runtime-data.js +869 -745
- package/dist/lanes/workflow-contract.js +3 -3
- package/dist/lanes/workflow.js +732 -605
- package/dist/model-scaffold.js +26 -5
- package/dist/path-projection.js +196 -14
- package/dist/plan.js +25 -0
- package/dist/programming-workflow.js +55 -3
- package/dist/read-shapes.js +146 -0
- package/dist/read.js +40 -9
- package/dist/recipes/businessrule.js +74 -24
- package/dist/recipes/dictionary.js +7 -4
- package/dist/recipes/form.js +46 -0
- package/dist/recipes/runtime-data.js +3 -2
- package/dist/refusal-guidance.js +29 -5
- package/dist/rule-support.js +14 -1
- package/dist/rule-when.js +2 -2
- package/dist/tool-types.js +9 -3
- package/dist/version-family-gaps.js +101 -0
- package/dist/write.js +2 -1
- package/package.json +6 -4
- package/skills/omc-glossary.md +13 -0
- package/skills/omc-refusals.md +13 -5
- package/skills/omc-tool-selection.md +2 -1
- package/skills/omc-write-hazards.md +5 -2
|
@@ -409,7 +409,30 @@ export function registerB2Lane(tools) {
|
|
|
409
409
|
query: { categoryId: args.categoryId ?? "", ...(args.keyword !== undefined ? { keyword: args.keyword } : {}) },
|
|
410
410
|
})).data);
|
|
411
411
|
const services = rows.map((row) => serviceReadbackFace(row));
|
|
412
|
-
|
|
412
|
+
// r28f F1:选型要看 adapter 分布;默认 CODE_NAME 压成 code|name 会丢掉
|
|
413
|
+
// adapterCode。聚合一次,紧凑面也保留。
|
|
414
|
+
const adapterDistribution = {};
|
|
415
|
+
for (const row of services) {
|
|
416
|
+
const code = typeof row.adapterCode === "string" && row.adapterCode ? row.adapterCode : "(空)";
|
|
417
|
+
adapterDistribution[code] = (adapterDistribution[code] ?? 0) + 1;
|
|
418
|
+
}
|
|
419
|
+
return { services, codes: services.map((row) => row.code), adapterDistribution, servicesCount: services.length };
|
|
420
|
+
}, {
|
|
421
|
+
// r28f F1:>2048B 时默认 compact 把 services 压成 code|name——丢掉
|
|
422
|
+
// adapterCode。自定义紧凑面保留选型所需三列 + 分布聚合。
|
|
423
|
+
compact: (result) => {
|
|
424
|
+
const services = Array.isArray(result.services) ? result.services : [];
|
|
425
|
+
return {
|
|
426
|
+
services: services.map((row) => ({
|
|
427
|
+
code: row.code,
|
|
428
|
+
name: row.name,
|
|
429
|
+
...(row.adapterCode !== undefined && row.adapterCode !== null ? { adapterCode: row.adapterCode } : {}),
|
|
430
|
+
})),
|
|
431
|
+
codes: result.codes,
|
|
432
|
+
adapterDistribution: result.adapterDistribution,
|
|
433
|
+
servicesCount: result.servicesCount ?? services.length,
|
|
434
|
+
};
|
|
435
|
+
},
|
|
413
436
|
}),
|
|
414
437
|
}));
|
|
415
438
|
tools.push(defineTool({
|
|
@@ -474,119 +497,179 @@ export function registerB2Lane(tools) {
|
|
|
474
497
|
: {}),
|
|
475
498
|
};
|
|
476
499
|
return { serviceCode: args.serviceCode, methodCode: args.methodCode, present: true, method };
|
|
500
|
+
}, {
|
|
501
|
+
// r28f F3:单条读面。默认 compact 把整个 method 折成 {keys,sha256,omitted}
|
|
502
|
+
// ——参数面是本工具存在意义,必须内联;其余大字段仍可外置到 artifact。
|
|
503
|
+
compact: (result) => {
|
|
504
|
+
const method = result.method;
|
|
505
|
+
if (!method || typeof method !== "object" || Array.isArray(method)) {
|
|
506
|
+
return {
|
|
507
|
+
serviceCode: result.serviceCode,
|
|
508
|
+
methodCode: result.methodCode,
|
|
509
|
+
present: result.present,
|
|
510
|
+
...(result.absent !== undefined ? { absent: result.absent } : {}),
|
|
511
|
+
method,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const row = method;
|
|
515
|
+
return {
|
|
516
|
+
serviceCode: result.serviceCode,
|
|
517
|
+
methodCode: result.methodCode,
|
|
518
|
+
present: result.present,
|
|
519
|
+
method: {
|
|
520
|
+
code: row.code,
|
|
521
|
+
name: row.name,
|
|
522
|
+
configJson: row.configJson,
|
|
523
|
+
inputParameters: row.inputParameters,
|
|
524
|
+
outputParameters: row.outputParameters,
|
|
525
|
+
...(row.readbackCaveat !== undefined ? { readbackCaveat: row.readbackCaveat } : {}),
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
},
|
|
477
529
|
}),
|
|
478
530
|
}));
|
|
479
|
-
writeTool(
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
path
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
531
|
+
writeTool({
|
|
532
|
+
tools,
|
|
533
|
+
name: "method.update",
|
|
534
|
+
lane: B2_LANE,
|
|
535
|
+
scope: B2_TOOLS["method.update"].scope,
|
|
536
|
+
description: B2_TOOLS["method.update"].description,
|
|
537
|
+
inputSchema: {
|
|
538
|
+
serviceCode: z.string().min(1),
|
|
539
|
+
methodCode: z.string().min(1),
|
|
540
|
+
name: z.string().optional(),
|
|
541
|
+
methodConfig: z.record(z.string(), z.unknown()).optional(),
|
|
542
|
+
methodInputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional(),
|
|
543
|
+
methodOutputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional(),
|
|
544
|
+
},
|
|
545
|
+
execute: async (session, args) => {
|
|
546
|
+
const service = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
547
|
+
const serviceRow = service.data;
|
|
548
|
+
const adapterCode = assertBizServiceAdapterWhitelisted(adapterCodeOf(serviceRow));
|
|
549
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
550
|
+
const current = rows.find((row) => row.code === args.methodCode);
|
|
551
|
+
if (!current)
|
|
552
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-business", message: `业务方法不存在:${args.serviceCode}/${args.methodCode}`, errcode: 500001 });
|
|
553
|
+
const currentRecord = current;
|
|
554
|
+
const inputJson = args.methodInputParameters === undefined ? currentRecord.inputParametersJson : compileParameterFace(adapterCode, typeof args.methodInputParameters === "string" ? safeParseArray(args.methodInputParameters) : args.methodInputParameters, "methodInputParameters");
|
|
555
|
+
const outputJson = args.methodOutputParameters === undefined ? currentRecord.outputParametersJson : compileParameterFace(adapterCode, typeof args.methodOutputParameters === "string" ? safeParseArray(args.methodOutputParameters) : args.methodOutputParameters, "methodOutputParameters");
|
|
556
|
+
const methodConfigPlain = args.methodConfig === undefined ? undefined : compileMethodConfigPlain(adapterCode, args.methodConfig);
|
|
557
|
+
const currentConfig = decodeJsonColumn(currentRecord.configJson);
|
|
558
|
+
const currentConfigPlain = currentConfig === undefined || currentConfig === null ? undefined : compileMethodConfigPlain(adapterCode, currentConfig);
|
|
559
|
+
const performed = await wireCall(session, {
|
|
560
|
+
path: ENDPOINTS.bizServiceMethodUpdate,
|
|
561
|
+
method: "POST",
|
|
562
|
+
// Live 2026-09-15 + source evidence (BizServiceMethodModel.java:88-146):
|
|
563
|
+
// a body carrying the transient `inputParameterModels`/`inputParameters`
|
|
564
|
+
// keys makes the model setter RE-SERIALIZE and filter out parameters
|
|
565
|
+
// lacking bizPropertyType/externParameterType, overwriting
|
|
566
|
+
// inputParametersJson with "[]". The update body must therefore carry
|
|
567
|
+
// ONLY the persisted columns — never the read-model's transient keys.
|
|
568
|
+
body: {
|
|
569
|
+
id: currentRecord.id,
|
|
570
|
+
serviceCode: args.serviceCode,
|
|
571
|
+
code: args.methodCode,
|
|
572
|
+
name: args.name ?? (typeof currentRecord.name === "string" ? currentRecord.name : args.methodCode),
|
|
573
|
+
...(methodConfigPlain !== undefined ? { configJson: (await omcCore()).encryptBizServiceMethodConfigJson(methodConfigPlain) } : currentConfigPlain !== undefined ? { configJson: (await omcCore()).encryptBizServiceMethodConfigJson(currentConfigPlain) } : {}),
|
|
574
|
+
...(inputJson !== undefined && inputJson !== null ? { inputParametersJson: inputJson } : {}),
|
|
575
|
+
...(outputJson !== undefined && outputJson !== null ? { outputParametersJson: outputJson } : {}),
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
579
|
+
const after = afterRows.find((row) => row.code === args.methodCode);
|
|
580
|
+
// r28f F4:与 method.get 共用参数面解码(position + bizPropertyType → 名字)。
|
|
581
|
+
return { performed: { errcode: performed.errcode }, readback: { methodCode: args.methodCode, methodPresent: after != null, nameMatches: args.name === undefined || after?.name === args.name, configMatches: methodConfigPlain === undefined || after?.configJson === methodConfigPlain, inputParameters: normalizeParameterFaceReadback(adapterCode, decodeJsonColumn(after?.inputParametersJson)), outputParameters: normalizeParameterFaceReadback(adapterCode, decodeJsonColumn(after?.outputParametersJson)), mappingNote: "规则映射直接用这里的参数 code(bizPropertyType 供类型对账),不必再 service.get" } };
|
|
582
|
+
},
|
|
583
|
+
preflight: (args) => {
|
|
584
|
+
assertStructuredConfig(args.methodConfig, "methodConfig");
|
|
585
|
+
},
|
|
586
|
+
rollback: b2ServiceRollbackSpec("method.update"),
|
|
587
|
+
});
|
|
588
|
+
writeTool({
|
|
589
|
+
tools,
|
|
590
|
+
name: "method.create",
|
|
591
|
+
lane: B2_LANE,
|
|
592
|
+
scope: B2_TOOLS["method.create"].scope,
|
|
593
|
+
description: B2_TOOLS["method.create"].description,
|
|
594
|
+
inputSchema: {
|
|
595
|
+
serviceCode: z.string().min(1).describe("已存在的业务服务编码(在其下新增方法)"),
|
|
596
|
+
methodCode: z.string().min(1),
|
|
597
|
+
name: z.string().min(1).describe("方法名称(设计器必填,中文≤50)"),
|
|
598
|
+
methodConfig: z.record(z.string(), z.unknown()).optional(),
|
|
599
|
+
methodInputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional(),
|
|
600
|
+
methodOutputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional(),
|
|
601
|
+
},
|
|
602
|
+
execute: async (session, args) => {
|
|
603
|
+
// Ticket 116: a new extension endpoint does NOT always mean a new
|
|
604
|
+
// service — the common case is ADDING a method to an existing service.
|
|
605
|
+
// service.register refuses a duplicate serviceCode (duplicate-service)
|
|
606
|
+
// and method.update refuses an absent method, so without this tool the
|
|
607
|
+
// only path was delete-and-recreate (which drops the service and its
|
|
608
|
+
// other methods/rule bindings). Platform face: POST
|
|
609
|
+
// /bizservice/method/create with BizServiceMethodModel{serviceCode,code,
|
|
610
|
+
// name,configJson,inputParametersJson,outputParametersJson}
|
|
611
|
+
// (BizServiceMethodController.java:133-170); name/code/serviceCode are
|
|
612
|
+
// validated non-blank, name ≤50 Chinese chars.
|
|
613
|
+
const service = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
614
|
+
const serviceRow = service.data;
|
|
615
|
+
if (!serviceRow)
|
|
616
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-business", message: `业务服务不存在:${args.serviceCode};在已有服务下加方法前先确认服务存在(新增服务用 service.register)`, errcode: 500001 });
|
|
617
|
+
const adapterCode = assertBizServiceAdapterWhitelisted(adapterCodeOf(serviceRow));
|
|
618
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
619
|
+
if (rows.some((row) => row.code === args.methodCode))
|
|
620
|
+
throw new ToolRefusalError({ status: "refused", blocker: "duplicate-method", serviceCode: args.serviceCode, methodCode: args.methodCode, message: `业务方法 ${args.serviceCode}/${args.methodCode} 已存在;改方法用 method.update` });
|
|
621
|
+
const inputJson = args.methodInputParameters === undefined ? undefined : compileParameterFace(adapterCode, typeof args.methodInputParameters === "string" ? safeParseArray(args.methodInputParameters) : args.methodInputParameters, "methodInputParameters");
|
|
622
|
+
const outputJson = args.methodOutputParameters === undefined ? undefined : compileParameterFace(adapterCode, typeof args.methodOutputParameters === "string" ? safeParseArray(args.methodOutputParameters) : args.methodOutputParameters, "methodOutputParameters");
|
|
623
|
+
const methodConfigPlain = args.methodConfig === undefined ? undefined : compileMethodConfigPlain(adapterCode, args.methodConfig);
|
|
624
|
+
const performed = await wireCall(session, {
|
|
625
|
+
path: ENDPOINTS.bizServiceMethodCreate,
|
|
626
|
+
method: "POST",
|
|
627
|
+
body: {
|
|
628
|
+
serviceCode: args.serviceCode,
|
|
629
|
+
code: args.methodCode,
|
|
630
|
+
name: args.name,
|
|
631
|
+
...(methodConfigPlain !== undefined ? { configJson: (await omcCore()).encryptBizServiceMethodConfigJson(methodConfigPlain) } : {}),
|
|
632
|
+
...(inputJson !== undefined && inputJson !== null ? { inputParametersJson: inputJson } : {}),
|
|
633
|
+
...(outputJson !== undefined && outputJson !== null ? { outputParametersJson: outputJson } : {}),
|
|
634
|
+
},
|
|
635
|
+
});
|
|
636
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
637
|
+
const after = afterRows.find((row) => row.code === args.methodCode);
|
|
638
|
+
// r28f F4:与 method.get 共用参数面解码(position + bizPropertyType → 名字)。
|
|
639
|
+
return { performed: { errcode: performed.errcode }, readback: { methodCode: args.methodCode, methodPresent: after != null, nameMatches: after?.name === args.name, configMatches: methodConfigPlain === undefined || after?.configJson === methodConfigPlain, inputParameters: normalizeParameterFaceReadback(adapterCode, decodeJsonColumn(after?.inputParametersJson)), outputParameters: normalizeParameterFaceReadback(adapterCode, decodeJsonColumn(after?.outputParametersJson)), mappingNote: "规则映射直接用这里的参数 code(bizPropertyType 供类型对账),不必再 service.get" } };
|
|
640
|
+
},
|
|
641
|
+
preflight: (args) => {
|
|
642
|
+
assertStructuredConfig(args.methodConfig, "methodConfig");
|
|
643
|
+
},
|
|
644
|
+
rollback: b2ServiceRollbackSpec("method.create"),
|
|
645
|
+
});
|
|
646
|
+
writeTool({
|
|
647
|
+
tools,
|
|
648
|
+
name: "method.delete",
|
|
649
|
+
lane: B2_LANE,
|
|
650
|
+
scope: B2_TOOLS["method.delete"].scope,
|
|
651
|
+
description: B2_TOOLS["method.delete"].description,
|
|
652
|
+
inputSchema: { serviceCode: z.string().min(1), methodCode: z.string().min(1) },
|
|
653
|
+
execute: async (session, args) => {
|
|
654
|
+
// Id resolution mirrors service.delete: the delete endpoint keys on the
|
|
655
|
+
// row id (query, comma-separated on the platform face — this tool stays
|
|
656
|
+
// single-target), resolved via list_by_service_code + method code.
|
|
657
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
658
|
+
const current = rows.find((row) => row.code === args.methodCode);
|
|
659
|
+
const id = current?.id;
|
|
660
|
+
if (typeof id !== "string" || !id)
|
|
661
|
+
return { performed: { errcode: 0 }, readback: { absent: true, note: "method already absent" } };
|
|
662
|
+
// A referenced method answers 500108 业务方法已绑定默认方法,不可删除
|
|
663
|
+
// (BizServiceMethodController.java:246-272) — governedWrite surfaces the
|
|
664
|
+
// platform refusal verbatim with errcode + message; the rule and its
|
|
665
|
+
// binding stay untouched.
|
|
666
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceMethodDelete, method: "POST", query: { id } });
|
|
667
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
668
|
+
const after = afterRows.find((row) => row.code === args.methodCode);
|
|
669
|
+
return { performed: { errcode: performed.errcode }, readback: { absent: after == null, ...(after ? { method: after } : {}) } };
|
|
670
|
+
},
|
|
671
|
+
rollback: b2ServiceRollbackSpec("method.delete"),
|
|
672
|
+
});
|
|
590
673
|
tools.push(defineTool({
|
|
591
674
|
name: "service.category.list",
|
|
592
675
|
layer: "low",
|
|
@@ -608,70 +691,96 @@ export function registerB2Lane(tools) {
|
|
|
608
691
|
};
|
|
609
692
|
}),
|
|
610
693
|
}));
|
|
611
|
-
writeTool(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
694
|
+
writeTool({
|
|
695
|
+
tools,
|
|
696
|
+
name: "service.category.create",
|
|
697
|
+
lane: B2_LANE,
|
|
698
|
+
scope: B2_TOOLS["service.category.create"].scope,
|
|
699
|
+
description: B2_TOOLS["service.category.create"].description,
|
|
700
|
+
inputSchema: {
|
|
701
|
+
name: z.string().min(1).describe("分类名称(非空)"),
|
|
702
|
+
},
|
|
703
|
+
execute: async (session, args) => {
|
|
704
|
+
const beforeRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
705
|
+
if (beforeRows.some((row) => row.name === args.name))
|
|
706
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-category-duplicate", message: `业务服务分类 ${args.name} 已存在;拒绝重复 POST` });
|
|
707
|
+
// Standard face: category/create {name} (BizServiceCategoryController
|
|
708
|
+
// .java:43-176); the readback re-lists and resolves the new row by name.
|
|
709
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceCategoryCreate, method: "POST", body: { name: args.name } });
|
|
710
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
711
|
+
const created = afterRows.find((row) => row.name === args.name);
|
|
712
|
+
return {
|
|
713
|
+
performed: { errcode: performed.errcode },
|
|
714
|
+
readback: { categoryPresent: created != null, ...(created ? { id: created.id, name: created.name } : {}) },
|
|
715
|
+
};
|
|
716
|
+
},
|
|
717
|
+
preflight: (args) => {
|
|
718
|
+
if (typeof args.name !== "string" || !args.name.trim())
|
|
719
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-required-field-missing", message: "分类名称 name 是必填项;缺失即拒绝,零 wire", field: "name" });
|
|
720
|
+
},
|
|
629
721
|
});
|
|
630
|
-
writeTool(
|
|
631
|
-
|
|
632
|
-
name:
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
722
|
+
writeTool({
|
|
723
|
+
tools,
|
|
724
|
+
name: "service.category.update",
|
|
725
|
+
lane: B2_LANE,
|
|
726
|
+
scope: B2_TOOLS["service.category.update"].scope,
|
|
727
|
+
description: B2_TOOLS["service.category.update"].description,
|
|
728
|
+
inputSchema: {
|
|
729
|
+
id: z.string().min(1).describe("分类 id(service.category.list 解析)"),
|
|
730
|
+
name: z.string().min(1).describe("新分类名称(非空,中文≤50)"),
|
|
731
|
+
},
|
|
732
|
+
execute: async (session, args) => {
|
|
733
|
+
const beforeRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
734
|
+
const current = beforeRows.find((row) => row.id === args.id);
|
|
735
|
+
if (!current)
|
|
736
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-category-absent", errcode: 501052, message: `业务服务分类 ${args.id} 不存在;拒绝 category/update POST` });
|
|
737
|
+
if (beforeRows.some((row) => row.id !== args.id && row.name === args.name))
|
|
738
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-category-duplicate", errcode: 501004, message: `业务服务分类 ${args.name} 已存在;拒绝 category/update POST` });
|
|
739
|
+
// Standard face: category/update {id,name} (BizServiceCategoryController
|
|
740
|
+
// .java:109-140). A missing id answers the 501052 envelope; a duplicate
|
|
741
|
+
// name answers 501004 (BizServiceCategoryServiceImpl.save:54-63) — both
|
|
742
|
+
// surface verbatim. Readback re-lists and pins the row by id.
|
|
743
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceCategoryUpdate, method: "POST", body: { id: args.id, name: args.name } });
|
|
744
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
745
|
+
const after = afterRows.find((row) => row.id === args.id);
|
|
746
|
+
return {
|
|
747
|
+
performed: { errcode: performed.errcode },
|
|
748
|
+
readback: { categoryPresent: after != null, ...(after ? { id: after.id, name: after.name } : {}) },
|
|
749
|
+
};
|
|
750
|
+
},
|
|
751
|
+
preflight: (args) => {
|
|
752
|
+
for (const [key, label] of [["id", "分类 id"], ["name", "分类名称 name"]])
|
|
753
|
+
if (typeof args[key] !== "string" || !args[key].trim())
|
|
754
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-required-field-missing", message: `${label} 是必填项;缺失即拒绝,零 wire`, field: key });
|
|
755
|
+
if (args.name.length > 50)
|
|
756
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-name-too-long", message: "分类名称中文长度不能超过 50(服务端同样强制)" });
|
|
757
|
+
},
|
|
657
758
|
});
|
|
658
|
-
writeTool(
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
759
|
+
writeTool({
|
|
760
|
+
tools,
|
|
761
|
+
name: "service.category.delete",
|
|
762
|
+
lane: B2_LANE,
|
|
763
|
+
scope: B2_TOOLS["service.category.delete"].scope,
|
|
764
|
+
description: B2_TOOLS["service.category.delete"].description,
|
|
765
|
+
inputSchema: {
|
|
766
|
+
id: z.string().min(1).describe("分类 id(service.category.list 解析;虚拟「全部」id 为空不可删)"),
|
|
767
|
+
},
|
|
768
|
+
execute: async (session, args) => {
|
|
769
|
+
const beforeRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
770
|
+
if (!beforeRows.some((row) => row.id === args.id))
|
|
771
|
+
return { performed: { skipped: "already-absent" }, readback: { absent: true } };
|
|
772
|
+
// Standard face: category/delete?id= (BizServiceCategoryController
|
|
773
|
+
// .java:142-165). A category still holding services answers 501003
|
|
774
|
+
// 业务服务目录下存在集成服务,不可删除 (BizServiceFacadeImpl:196-207) —
|
|
775
|
+
// deletion is BLOCKED, no cascade; the refusal surfaces verbatim.
|
|
776
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceCategoryDelete, method: "POST", query: { id: args.id } });
|
|
777
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
778
|
+
const after = afterRows.find((row) => row.id === args.id);
|
|
779
|
+
return {
|
|
780
|
+
performed: { errcode: performed.errcode },
|
|
781
|
+
readback: { absent: after == null, ...(after ? { category: after } : {}) },
|
|
782
|
+
};
|
|
783
|
+
},
|
|
675
784
|
});
|
|
676
785
|
tools.push(defineTool({
|
|
677
786
|
name: "service.dbconnpool.list",
|
|
@@ -689,112 +798,137 @@ export function registerB2Lane(tools) {
|
|
|
689
798
|
return { pools: rows.map((row) => poolReadbackFace(row)), codes: rows.map((row) => row.code) };
|
|
690
799
|
}),
|
|
691
800
|
}));
|
|
692
|
-
writeTool(
|
|
693
|
-
|
|
694
|
-
name:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
801
|
+
writeTool({
|
|
802
|
+
tools,
|
|
803
|
+
name: "service.dbconnpool.create",
|
|
804
|
+
lane: B2_LANE,
|
|
805
|
+
scope: B2_TOOLS["service.dbconnpool.create"].scope,
|
|
806
|
+
description: B2_TOOLS["service.dbconnpool.create"].description,
|
|
807
|
+
inputSchema: {
|
|
808
|
+
code: z.string().min(1).describe("连接池编码(唯一;无效 → 502003,重复 → 502002)"),
|
|
809
|
+
name: z.string().min(1).describe("连接池名称(非空,中文≤50;空 → 502004)"),
|
|
810
|
+
databaseType: z.string().min(1).describe("数据库类型(如 MySQL/Oracle/SQLServer;设计器必选)"),
|
|
811
|
+
jdbcUrl: z.string().min(1).describe("JDBC 连接串(服务端白名单校验;Kafka 类型由 externInfo url:port 合成)"),
|
|
812
|
+
username: z.string().optional(),
|
|
813
|
+
password: z.string().optional().describe("连接口令(只上送,读回永不回显)"),
|
|
814
|
+
description: z.string().optional(),
|
|
815
|
+
datasourceType: z.string().optional().describe("DATABASE(缺省)/Kafka"),
|
|
816
|
+
externInfo: z.string().optional().describe("扩展信息 JSON(Kafka 需 {url,port})"),
|
|
817
|
+
},
|
|
818
|
+
execute: async (session, args) => {
|
|
819
|
+
// Standard face: /bizservice/dbconnpool/create (BizDatabaseConnection
|
|
820
|
+
// PoolModel body; controller validates the jdbcUrl whitelist, service
|
|
821
|
+
// validates name/code/uniqueness and initializes the datasource).
|
|
822
|
+
// Password RSA boundary (live 504011): the entity decrypts the stored
|
|
823
|
+
// column with its hardcoded keypair — plaintext fails to decrypt and
|
|
824
|
+
// the pool connects with NO password, so the tool always sends the
|
|
825
|
+
// encrypted form (encryptBizDbPoolPassword).
|
|
826
|
+
const performed = await wireCall(session, {
|
|
827
|
+
path: ENDPOINTS.bizDbConnPoolCreate,
|
|
828
|
+
method: "POST",
|
|
829
|
+
body: {
|
|
830
|
+
code: args.code,
|
|
831
|
+
name: args.name,
|
|
832
|
+
databaseType: args.databaseType,
|
|
833
|
+
jdbcUrl: args.jdbcUrl,
|
|
834
|
+
...(args.username !== undefined ? { username: args.username } : {}),
|
|
835
|
+
...(args.password !== undefined ? { password: (await omcCore()).encryptBizDbPoolPassword(args.password) } : {}),
|
|
836
|
+
...(args.description !== undefined ? { description: args.description } : {}),
|
|
837
|
+
...(args.datasourceType !== undefined ? { datasourceType: args.datasourceType } : {}),
|
|
838
|
+
...(args.externInfo !== undefined ? { externInfo: args.externInfo } : {}),
|
|
839
|
+
},
|
|
840
|
+
});
|
|
841
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
842
|
+
const created = rows.find((row) => row.code === args.code);
|
|
843
|
+
return {
|
|
844
|
+
performed: { errcode: performed.errcode },
|
|
845
|
+
readback: { poolPresent: created != null, ...(created ? poolReadbackFace(created) : {}) },
|
|
846
|
+
};
|
|
847
|
+
},
|
|
848
|
+
preflight: (args) => {
|
|
849
|
+
for (const [key, label] of [["code", "连接池编码 code"], ["name", "连接池名称 name"], ["databaseType", "数据库类型 databaseType"], ["jdbcUrl", "JDBC 连接串 jdbcUrl"]])
|
|
850
|
+
if (typeof args[key] !== "string" || !args[key].trim())
|
|
851
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-required-field-missing", message: `${label} 是必填项;缺失即拒绝,零 wire`, field: key });
|
|
852
|
+
if (args.datasourceType !== undefined && !["DATABASE", "Kafka"].includes(String(args.datasourceType)))
|
|
853
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-dbconnpool-invalid", message: "datasourceType 只接受 DATABASE(缺省)或 Kafka(8.6.26 服务端分支)" });
|
|
854
|
+
},
|
|
737
855
|
});
|
|
738
|
-
writeTool(
|
|
739
|
-
|
|
740
|
-
name:
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
856
|
+
writeTool({
|
|
857
|
+
tools,
|
|
858
|
+
name: "service.dbconnpool.update",
|
|
859
|
+
lane: B2_LANE,
|
|
860
|
+
scope: B2_TOOLS["service.dbconnpool.update"].scope,
|
|
861
|
+
description: B2_TOOLS["service.dbconnpool.update"].description,
|
|
862
|
+
inputSchema: {
|
|
863
|
+
code: z.string().min(1).describe("连接池编码(解析行 id 与当前列)"),
|
|
864
|
+
name: z.string().optional(),
|
|
865
|
+
databaseType: z.string().optional(),
|
|
866
|
+
jdbcUrl: z.string().optional(),
|
|
867
|
+
username: z.string().optional(),
|
|
868
|
+
password: z.string().optional().describe("新口令(只上送;缺省回显存量密文保持覆写不抹列)"),
|
|
869
|
+
description: z.string().optional(),
|
|
870
|
+
datasourceType: z.string().optional(),
|
|
871
|
+
externInfo: z.string().optional(),
|
|
872
|
+
},
|
|
873
|
+
execute: async (session, args) => {
|
|
874
|
+
// The platform update is a FULL-ROW overwrite
|
|
875
|
+
// (BizDatabaseConnectionPoolServiceImpl.update:135-168 — only
|
|
876
|
+
// createdTime is carried), so the tool echoes every untouched column
|
|
877
|
+
// (including the stored password ciphertext, like the designer) and a
|
|
878
|
+
// name-only update cannot wipe jdbcUrl/username/credentials.
|
|
879
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
880
|
+
const current = rows.find((row) => row.code === args.code);
|
|
881
|
+
if (!current)
|
|
882
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-business", message: `数据库连接池不存在:${args.code}`, errcode: 502001 });
|
|
883
|
+
const currentRecord = current;
|
|
884
|
+
const performed = await wireCall(session, {
|
|
885
|
+
path: ENDPOINTS.bizDbConnPoolUpdate,
|
|
886
|
+
method: "POST",
|
|
887
|
+
body: {
|
|
888
|
+
id: currentRecord.id,
|
|
889
|
+
code: args.code,
|
|
890
|
+
name: args.name ?? currentRecord.name,
|
|
891
|
+
databaseType: args.databaseType ?? currentRecord.databaseType,
|
|
892
|
+
jdbcUrl: args.jdbcUrl ?? currentRecord.jdbcUrl,
|
|
893
|
+
username: args.username !== undefined ? args.username : currentRecord.username ?? "",
|
|
894
|
+
// New passwords ride the RSA boundary; the echoed stored value is
|
|
895
|
+
// already ciphertext (designer parity — the overwrite keeps the
|
|
896
|
+
// credential column intact when the caller does not rotate it).
|
|
897
|
+
password: args.password !== undefined ? (await omcCore()).encryptBizDbPoolPassword(args.password) : currentRecord.password ?? "",
|
|
898
|
+
description: args.description !== undefined ? args.description : currentRecord.description ?? "",
|
|
899
|
+
datasourceType: args.datasourceType ?? currentRecord.datasourceType ?? "DATABASE",
|
|
900
|
+
externInfo: args.externInfo !== undefined ? args.externInfo : currentRecord.externInfo ?? "",
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
904
|
+
const after = afterRows.find((row) => row.code === args.code);
|
|
905
|
+
return {
|
|
906
|
+
performed: { errcode: performed.errcode },
|
|
907
|
+
readback: { poolPresent: after != null, ...(after ? poolReadbackFace(after) : {}) },
|
|
908
|
+
};
|
|
909
|
+
},
|
|
784
910
|
});
|
|
785
|
-
writeTool(
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
911
|
+
writeTool({
|
|
912
|
+
tools,
|
|
913
|
+
name: "service.dbconnpool.delete",
|
|
914
|
+
lane: B2_LANE,
|
|
915
|
+
scope: B2_TOOLS["service.dbconnpool.delete"].scope,
|
|
916
|
+
description: B2_TOOLS["service.dbconnpool.delete"].description,
|
|
917
|
+
inputSchema: { code: z.string().min(1).describe("连接池编码") },
|
|
918
|
+
execute: async (session, args) => {
|
|
919
|
+
// Standard face: /bizservice/dbconnpool/delete?id=. A pool referenced
|
|
920
|
+
// by any service config's dbConnPoolCode answers 502006
|
|
921
|
+
// 数据库连接池已经被业务服务引用 (BizServiceFacadeImpl delete:173-189)
|
|
922
|
+
// — blocked, no cascade; the refusal surfaces verbatim.
|
|
923
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
924
|
+
const current = rows.find((row) => row.code === args.code);
|
|
925
|
+
if (!current)
|
|
926
|
+
return { performed: { errcode: 0 }, readback: { absent: true, note: "pool already absent" } };
|
|
927
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizDbConnPoolDelete, method: "POST", query: { id: String(current.id) } });
|
|
928
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
929
|
+
const after = afterRows.find((row) => row.code === args.code);
|
|
930
|
+
return { performed: { errcode: performed.errcode }, readback: { absent: after == null } };
|
|
931
|
+
},
|
|
798
932
|
});
|
|
799
933
|
tools.push(defineTool({
|
|
800
934
|
name: "service.adapter_config",
|
|
@@ -803,7 +937,12 @@ export function registerB2Lane(tools) {
|
|
|
803
937
|
lane: B2_LANE,
|
|
804
938
|
scope: B2_TOOLS["service.adapter_config"].scope,
|
|
805
939
|
description: B2_TOOLS["service.adapter_config"].description,
|
|
806
|
-
inputSchema: {
|
|
940
|
+
inputSchema: {
|
|
941
|
+
environment: environmentArg,
|
|
942
|
+
// r28f F5:选型只要 code/name;完整 config(authList/commonList/…)按需。
|
|
943
|
+
compact: z.boolean().optional().describe("true=只回 code/name/description(选型摘要);默认/省略=完整 adapter 面(含 config)"),
|
|
944
|
+
capability: capabilityArg,
|
|
945
|
+
},
|
|
807
946
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
808
947
|
// Standard face: /bizservice/service/adapter_config answers a JSON
|
|
809
948
|
// STRING of the registered protocol adapters (BizServiceFacadeImpl
|
|
@@ -823,10 +962,21 @@ export function registerB2Lane(tools) {
|
|
|
823
962
|
adapters = null;
|
|
824
963
|
}
|
|
825
964
|
}
|
|
826
|
-
|
|
965
|
+
const rows = Array.isArray(adapters) ? adapters : [];
|
|
966
|
+
const adapterCodes = rows.map((row) => (typeof row.code === "string" ? row.code : "")).filter(Boolean);
|
|
967
|
+
// compact:true → 选型摘要;否则完整面(含 config)。顶栏始终带 adapterCodes。
|
|
968
|
+
const face = args.compact === true
|
|
969
|
+
? rows.map((row) => ({
|
|
970
|
+
code: row.code,
|
|
971
|
+
name: row.name,
|
|
972
|
+
...(row.description !== undefined ? { description: row.description } : {}),
|
|
973
|
+
}))
|
|
974
|
+
: adapters;
|
|
975
|
+
return { adapters: face, adapterCodes, rawPresent: typeof result.data === "string", ...(args.compact === true ? { compact: true } : {}) };
|
|
827
976
|
}, {
|
|
828
977
|
// 2026-11-01 读门 2048:适配器面本身就是这个工具的存在意义(注册预检的
|
|
829
|
-
// 同源依据)——compact 保留 adapters 本体,不折码表。
|
|
978
|
+
// 同源依据)——compact 保留 adapters 本体,不折码表。r28f F5:顶栏
|
|
979
|
+
// adapterCodes 始终内联,完整 config 仍可达。
|
|
830
980
|
compact: (result) => result,
|
|
831
981
|
}),
|
|
832
982
|
}));
|
|
@@ -875,249 +1025,278 @@ export function registerB2Lane(tools) {
|
|
|
875
1025
|
return { serviceCode: args.serviceCode, tableName: args.tableName, rows: result.data };
|
|
876
1026
|
}),
|
|
877
1027
|
}));
|
|
878
|
-
writeTool(
|
|
879
|
-
|
|
880
|
-
name:
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
...(serviceConfigJson !== undefined ? { configJson: serviceConfigJson } : {}),
|
|
934
|
-
},
|
|
935
|
-
});
|
|
936
|
-
if (args.methodName) {
|
|
937
|
-
// The parameter face is adapter-checked only when this is a structured
|
|
938
|
-
// write; a plain registration shell keeps the ticket-35 face (INTERNAL).
|
|
939
|
-
const faceAdapter = serviceConfigJson !== undefined || methodConfigPlain !== undefined ? assertBizServiceAdapterWhitelisted(adapterCode) : "INTERNAL";
|
|
940
|
-
const serializeFace = (value, label) => value === undefined ? undefined : compileParameterFace(faceAdapter, typeof value === "string" ? (safeParseArray(value) ?? value) : value, label);
|
|
941
|
-
const inputJson = serializeFace(args.methodInputParameters, "methodInputParameters");
|
|
942
|
-
const outputJson = serializeFace(args.methodOutputParameters, "methodOutputParameters");
|
|
943
|
-
await wireCall(session, {
|
|
944
|
-
path: ENDPOINTS.bizServiceMethodCreate,
|
|
1028
|
+
writeTool({
|
|
1029
|
+
tools,
|
|
1030
|
+
name: "service.register",
|
|
1031
|
+
lane: B2_LANE,
|
|
1032
|
+
scope: B2_TOOLS["service.register"].scope,
|
|
1033
|
+
description: B2_TOOLS["service.register"].description,
|
|
1034
|
+
inputSchema: {
|
|
1035
|
+
serviceCode: z.string().min(1),
|
|
1036
|
+
name: z.string().min(1).describe("服务名称(设计器必填;非空,中文≤50,服务端同样强制)"),
|
|
1037
|
+
adapterCode: z.string().min(1).describe("适配器编码(设计器必填):RESTFUL/DATA_SQL/DATA_TABLE/SAP/KAFKA/SOAP。自定义 Java 方法体(扩展接口)的常规做法是用 RESTFUL 指向扩展端点,不要用 INTERNAL/custom_protocol"),
|
|
1038
|
+
serviceCategoryId: z.string().min(1).describe("所属目录 id(设计器必填;用 service.category.list 解析,「全部」是空 id 虚拟项不可用,必要时 service.category.create 新建)"),
|
|
1039
|
+
serviceConfig: z.record(z.string(), z.unknown()).optional().describe("结构化服务 configJson(明文落库;原生适配器按 adapterCode 校验)。RESTFUL/SOAP 鉴权 auth.code 仅接受 customer(parameterlist=[])、basic(username/password)、oauth_password(client_id/client_secret/username/password/oauth_path)、oauth_client(client_id/client_secret/oauth_path);每项为 code/name?/value 字符串,值只透传不记录;DATA_SQL/DATA_TABLE/SAP/KAFKA 不使用服务级鉴权,auth 只能为空对象或省略"),
|
|
1040
|
+
methodName: z.string().optional(),
|
|
1041
|
+
methodConfig: z.record(z.string(), z.unknown()).optional().describe("结构化方法 configJson(工具 AES-128-ECB+Base64 加密后上送;明文会被平台静默存 NULL)。RESTFUL 必填 httpType/requestBodyType/url/frontEndExtensionEnable,枚举可用名字(httpType: Post/Get/Put/Patch/Delete;requestBodyType: Form-urlencoded/JSON/Text),工具会编码成平台整数索引。DATA_SQL/DATA_TABLE 需要 sql/returnType(returnType: Void/SingleObject/List)。RESTFUL/DATA_SQL/DATA_TABLE/SAP/KAFKA 带 methodName 时必填;SOAP 方法由 WSDL 动态发现、无固定方法 configJson"),
|
|
1042
|
+
methodInputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional().describe("方法输入参数声明([{code,name,…}] 数组或 JSON 字符串)→ inputParametersJson;绑定校验按 code 对账;RESTFUL 每参数需 configJson.position"),
|
|
1043
|
+
methodOutputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional().describe("方法输出参数声明 → outputParametersJson"),
|
|
1044
|
+
description: z.string().optional(),
|
|
1045
|
+
},
|
|
1046
|
+
execute: async (session, args) => {
|
|
1047
|
+
// Live evidence (ticket 35): service/create with an empty adapterCode is
|
|
1048
|
+
// refused 500005 业务服务关联适配器编码为空; INTERNAL(6) is the local
|
|
1049
|
+
// extension adapter a 二开 registration shell binds to. A structured
|
|
1050
|
+
// write (serviceConfig/methodConfig) targets the designer write face and
|
|
1051
|
+
// requires a whitelisted adapterCode (ticket 92). Designer-parity
|
|
1052
|
+
// requireds (user directive 2026-09-14): name/adapterCode/
|
|
1053
|
+
// serviceCategoryId are enforced by the schema AND the preflight, and a
|
|
1054
|
+
// whitelisted adapter must carry serviceConfig (+ methodConfig with a
|
|
1055
|
+
// methodName).
|
|
1056
|
+
const adapterCode = args.adapterCode;
|
|
1057
|
+
let existing = null;
|
|
1058
|
+
try {
|
|
1059
|
+
existing = (await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } })).data;
|
|
1060
|
+
}
|
|
1061
|
+
catch (error) {
|
|
1062
|
+
if (!(error instanceof WireError && error.kind === "business"))
|
|
1063
|
+
throw error;
|
|
1064
|
+
}
|
|
1065
|
+
if (existing)
|
|
1066
|
+
throw new ToolRefusalError({ status: "refused", blocker: "duplicate-service", serviceCode: args.serviceCode, message: `业务服务 ${args.serviceCode} 已存在;拒绝重复 service/create POST` });
|
|
1067
|
+
if (adapterCode === "SOAP" && (args.methodName !== undefined || args.methodConfig !== undefined))
|
|
1068
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-method-config-invalid", message: "SOAP 方法由 WSDL 动态发现,没有固定 methodConfig;请使用平台原生 SOAP refresh/方法发现接口,不伪造方法体" });
|
|
1069
|
+
// Ticket 103: the platform never validates serviceCategoryId — refuse a
|
|
1070
|
+
// dangling id before any write (read-only resolution precedes this).
|
|
1071
|
+
await assertCategoryResolves(session, args.serviceCategoryId);
|
|
1072
|
+
const serviceConfigJson = args.serviceConfig !== undefined ? compileServiceConfig(adapterCode, args.serviceConfig) : undefined;
|
|
1073
|
+
// Ticket 104: a DATA_SQL/DATA_TABLE binding must reference an existing
|
|
1074
|
+
// connection pool before any write (the platform stores it unvalidated).
|
|
1075
|
+
if (serviceConfigJson !== undefined && (adapterCode === "DATA_SQL" || adapterCode === "DATA_TABLE")) {
|
|
1076
|
+
const poolCode = dbConnPoolCodeOf(args.serviceConfig);
|
|
1077
|
+
if (poolCode !== undefined)
|
|
1078
|
+
await assertDbConnPoolResolves(session, poolCode);
|
|
1079
|
+
}
|
|
1080
|
+
const methodConfigPlain = args.methodConfig !== undefined ? compileMethodConfigPlain(adapterCode, args.methodConfig) : undefined;
|
|
1081
|
+
const performed = await wireCall(session, {
|
|
1082
|
+
path: ENDPOINTS.bizServiceCreate,
|
|
945
1083
|
method: "POST",
|
|
946
1084
|
body: {
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1085
|
+
code: args.serviceCode,
|
|
1086
|
+
name: args.name,
|
|
1087
|
+
adapterCode,
|
|
1088
|
+
description: args.description ?? "",
|
|
1089
|
+
serviceCategoryId: args.serviceCategoryId,
|
|
1090
|
+
...(serviceConfigJson !== undefined ? { configJson: serviceConfigJson } : {}),
|
|
951
1091
|
},
|
|
952
1092
|
});
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1093
|
+
if (args.methodName) {
|
|
1094
|
+
// The parameter face is adapter-checked only when this is a structured
|
|
1095
|
+
// write; a plain registration shell keeps the ticket-35 face (INTERNAL).
|
|
1096
|
+
const faceAdapter = serviceConfigJson !== undefined || methodConfigPlain !== undefined ? assertBizServiceAdapterWhitelisted(adapterCode) : "INTERNAL";
|
|
1097
|
+
const serializeFace = (value, label) => value === undefined ? undefined : compileParameterFace(faceAdapter, typeof value === "string" ? (safeParseArray(value) ?? value) : value, label);
|
|
1098
|
+
const inputJson = serializeFace(args.methodInputParameters, "methodInputParameters");
|
|
1099
|
+
const outputJson = serializeFace(args.methodOutputParameters, "methodOutputParameters");
|
|
1100
|
+
await wireCall(session, {
|
|
1101
|
+
path: ENDPOINTS.bizServiceMethodCreate,
|
|
1102
|
+
method: "POST",
|
|
1103
|
+
body: {
|
|
1104
|
+
serviceCode: args.serviceCode, code: args.methodName, name: args.methodName,
|
|
1105
|
+
...(methodConfigPlain !== undefined ? { configJson: (await omcCore()).encryptBizServiceMethodConfigJson(methodConfigPlain) } : {}),
|
|
1106
|
+
...(inputJson !== undefined ? { inputParametersJson: inputJson } : {}),
|
|
1107
|
+
...(outputJson !== undefined ? { outputParametersJson: outputJson } : {}),
|
|
1108
|
+
},
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
const readback = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
1112
|
+
const methodRows = readback.data != null
|
|
1113
|
+
? wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data)
|
|
1114
|
+
: [];
|
|
1115
|
+
const registered = args.methodName ? methodRows.some((row) => row.code === args.methodName) : undefined;
|
|
1116
|
+
// r19 p7:readback 只回布尔时,agent 只能再调 service.get 拿参数 code(12 次里
|
|
1117
|
+
// 6 次是这种写后验证读)。方法视图内联进 readback,rule.save 的 methodMapping
|
|
1118
|
+
// 直接用这里的 code,不必再读。
|
|
1119
|
+
const methodRow = args.methodName ? methodRows.find((row) => row.code === args.methodName) : undefined;
|
|
1120
|
+
// r28f F4:与 method.get 共用 decode 面(position + bizPropertyType → 名字)。
|
|
1121
|
+
const readbackAdapter = adapterCodeOf(readback.data ?? undefined) || adapterCode;
|
|
1122
|
+
return {
|
|
1123
|
+
performed: { errcode: performed.errcode },
|
|
1124
|
+
readback: {
|
|
1125
|
+
servicePresent: readback.data != null,
|
|
1126
|
+
...(registered !== undefined ? { methodPresent: registered, methodCount: methodRows.length } : {}),
|
|
1127
|
+
...(methodRow !== undefined
|
|
1128
|
+
? {
|
|
1129
|
+
method: {
|
|
1130
|
+
code: methodRow.code,
|
|
1131
|
+
inputParameters: normalizeParameterFaceReadback(readbackAdapter, decodeJsonColumn(methodRow.inputParametersJson)),
|
|
1132
|
+
outputParameters: normalizeParameterFaceReadback(readbackAdapter, decodeJsonColumn(methodRow.outputParametersJson)),
|
|
1133
|
+
},
|
|
1134
|
+
mappingNote: "rule.save 的 methodMapping 直接用上面的参数 code:inputMappings.bizCode=来源字段 code、serviceMethodParameterCode=参数 code;不必再 service.get",
|
|
1135
|
+
}
|
|
1136
|
+
: {}),
|
|
1137
|
+
},
|
|
1138
|
+
};
|
|
1139
|
+
},
|
|
1140
|
+
preflight: (args) => {
|
|
1141
|
+
// Dry-lint (zero wire). `omc call` bypasses zod, so every schema-level
|
|
1142
|
+
// required is re-enforced here BEFORE the write permission gate.
|
|
1143
|
+
for (const [key, label] of [["name", "服务名称 name"], ["adapterCode", "适配器编码 adapterCode"], ["serviceCategoryId", "所属目录 serviceCategoryId"]])
|
|
1144
|
+
if (typeof args[key] !== "string" || !args[key].trim())
|
|
1145
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-required-field-missing", message: `${label} 是设计器必填项(designer parity);缺失即拒绝,零 wire。所属目录用 service.category.list 解析、service.category.create 新建`, field: key });
|
|
1146
|
+
if (args.name.length > 50)
|
|
1147
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-name-too-long", message: "服务名称中文长度不能超过 50(BizServiceController.java:84-112 服务端同样强制)" });
|
|
1148
|
+
assertStructuredConfig(args.serviceConfig, "serviceConfig");
|
|
1149
|
+
assertStructuredConfig(args.methodConfig, "methodConfig");
|
|
1150
|
+
const adapterCode = String(args.adapterCode);
|
|
1151
|
+
if (BIZSERVICE_WRITE_ADAPTERS.includes(adapterCode)) {
|
|
1152
|
+
// A source-evidenced native adapter is a DESIGNER configuration: the
|
|
1153
|
+
// service config is required for every adapter. SOAP deliberately has
|
|
1154
|
+
// no methodName/methodConfig here because the platform discovers SOAP
|
|
1155
|
+
// methods from WSDL via its native refresh path.
|
|
1156
|
+
if (args.serviceConfig === undefined)
|
|
1157
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-service-config-invalid", message: `${adapterCode} 服务必须携带结构化 serviceConfig(设计器同款必填)` });
|
|
1158
|
+
compileServiceConfig(adapterCode, args.serviceConfig);
|
|
1159
|
+
if (args.methodName !== undefined && adapterCode === "SOAP")
|
|
1160
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-method-config-invalid", message: "SOAP 方法由 WSDL 动态发现,没有固定 methodConfig;请使用平台原生 SOAP refresh/方法发现接口,不伪造方法体" });
|
|
1161
|
+
if (args.methodName !== undefined && args.methodConfig === undefined)
|
|
1162
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-method-config-invalid", message: `${adapterCode} 方法必须携带结构化 methodConfig——只有名字/参数面的注册壳不是设计器配置的方法` });
|
|
1163
|
+
if (args.methodConfig !== undefined)
|
|
1164
|
+
compileMethodConfigPlain(adapterCode, args.methodConfig);
|
|
1165
|
+
for (const [key, label] of [["methodInputParameters", "methodInputParameters"], ["methodOutputParameters", "methodOutputParameters"]])
|
|
1166
|
+
if (args[key] !== undefined)
|
|
1167
|
+
compileParameterFace(adapterCode, typeof args[key] === "string" ? safeParseArray(args[key]) : args[key], label);
|
|
1168
|
+
}
|
|
1169
|
+
else if (args.serviceConfig !== undefined || args.methodConfig !== undefined) {
|
|
1170
|
+
// Structured configs on a non-whitelisted adapter (e.g. INTERNAL) are
|
|
1171
|
+
// refused with the support face named (ADR 0001).
|
|
1172
|
+
assertBizServiceAdapterWhitelisted(adapterCode);
|
|
1173
|
+
}
|
|
1174
|
+
},
|
|
1014
1175
|
});
|
|
1015
|
-
writeTool(
|
|
1016
|
-
|
|
1017
|
-
name:
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
return {
|
|
1081
|
-
performed: { errcode: performed.errcode },
|
|
1082
|
-
readback: { servicePresent: after != null, nameMatches, configMatches, ...(configPreserved !== undefined ? { configPreserved } : {}), ...(adapterCode ? { adapterCode } : {}) },
|
|
1083
|
-
};
|
|
1084
|
-
}, (args) => {
|
|
1085
|
-
// Zero-wire refusal: the raw-string configJson face. `omc call` bypasses
|
|
1086
|
-
// zod, so the string form must be refused here too. The adapter
|
|
1087
|
-
// whitelist/required-key preflight needs the service row's adapterCode
|
|
1088
|
-
// and therefore runs after the read-only id resolution — still before
|
|
1089
|
-
// any update write (see the execute body).
|
|
1090
|
-
assertStructuredConfig(args.serviceConfig, "serviceConfig");
|
|
1091
|
-
}, undefined, b2ServiceRollbackSpec("service.update"));
|
|
1092
|
-
writeTool(tools, "service.delete", B2_LANE, B2_TOOLS["service.delete"].scope, B2_TOOLS["service.delete"].description, { serviceCode: z.string().min(1) }, async (session, args) => {
|
|
1093
|
-
const found = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
1094
|
-
const id = found.data?.id;
|
|
1095
|
-
if (typeof id !== "string" || !id)
|
|
1096
|
-
return { performed: { errcode: 0 }, readback: { absent: true, note: "service already absent" } };
|
|
1097
|
-
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceDelete, method: "POST", query: { id } });
|
|
1098
|
-
let absent = false;
|
|
1099
|
-
try {
|
|
1176
|
+
writeTool({
|
|
1177
|
+
tools,
|
|
1178
|
+
name: "service.update",
|
|
1179
|
+
lane: B2_LANE,
|
|
1180
|
+
scope: B2_TOOLS["service.update"].scope,
|
|
1181
|
+
description: B2_TOOLS["service.update"].description,
|
|
1182
|
+
inputSchema: {
|
|
1183
|
+
serviceCode: z.string().min(1),
|
|
1184
|
+
name: z.string().optional(),
|
|
1185
|
+
serviceConfig: z.record(z.string(), z.unknown()).optional().describe("结构化服务 configJson(明文上送;按服务 adapterCode 做必需键预检,同 service.register)"),
|
|
1186
|
+
description: z.string().optional(),
|
|
1187
|
+
serviceCategoryId: z.string().optional().describe("服务分类 id(通用透传,不静默丢弃)"),
|
|
1188
|
+
shared: z.boolean().optional().describe("共享服务(通用透传)"),
|
|
1189
|
+
userIdList: z.array(z.string()).optional().describe("可见用户列表(通用透传)"),
|
|
1190
|
+
modifierList: z.array(z.string()).optional().describe("修改者列表(通用透传)"),
|
|
1191
|
+
},
|
|
1192
|
+
execute: async (session, args) => {
|
|
1193
|
+
// Ticket 93 — patch an existing service without delete/recreate. The id
|
|
1194
|
+
// is resolved via get_by_code first (BizServiceController update path
|
|
1195
|
+
// requires it); metadata keys pass through verbatim.
|
|
1196
|
+
const found = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
1197
|
+
const row = found.data;
|
|
1198
|
+
const id = row?.id;
|
|
1199
|
+
if (typeof id !== "string" || !id)
|
|
1200
|
+
return { performed: { errcode: 500001 }, readback: { absent: true }, summary: `业务服务不存在:${args.serviceCode}` };
|
|
1201
|
+
const adapterCode = typeof row.adapterCode === "string" ? row.adapterCode : "";
|
|
1202
|
+
// Ticket 103: a NEW serviceCategoryId must resolve before the update
|
|
1203
|
+
// write (the platform writes it verbatim without validation).
|
|
1204
|
+
if (args.serviceCategoryId !== undefined)
|
|
1205
|
+
await assertCategoryResolves(session, args.serviceCategoryId);
|
|
1206
|
+
// A structured serviceConfig targets the designer write face: the same
|
|
1207
|
+
// adapter whitelist + required-key preflight as register (ticket 92).
|
|
1208
|
+
// Refusal here precedes the update WRITE (only the read-only id
|
|
1209
|
+
// resolution may have preceded it).
|
|
1210
|
+
const serviceConfigJson = args.serviceConfig !== undefined ? compileServiceConfig(assertBizServiceAdapterWhitelisted(adapterCode), args.serviceConfig) : undefined;
|
|
1211
|
+
// Ticket 104: a DATA_SQL/DATA_TABLE binding must reference an existing
|
|
1212
|
+
// connection pool before the update write.
|
|
1213
|
+
if (serviceConfigJson !== undefined && (adapterCode === "DATA_SQL" || adapterCode === "DATA_TABLE")) {
|
|
1214
|
+
const poolCode = dbConnPoolCodeOf(args.serviceConfig);
|
|
1215
|
+
if (poolCode !== undefined)
|
|
1216
|
+
await assertDbConnPoolResolves(session, poolCode);
|
|
1217
|
+
}
|
|
1218
|
+
// Live incident 2026-09-14 (user-visible): the platform update path is a
|
|
1219
|
+
// FULL-ROW overwrite (ModelUtility.toEntity → save/update) — a body that
|
|
1220
|
+
// omits configJson/serviceCategoryId wipes them to NULL on the platform
|
|
1221
|
+
// even though the caller never asked to change them. Patch semantics are
|
|
1222
|
+
// therefore implemented by ECHOING the row's current values for every
|
|
1223
|
+
// nullable managed column the caller did not override.
|
|
1224
|
+
const echo = (override, current) => (override !== undefined ? override : current ?? null);
|
|
1225
|
+
const performed = await wireCall(session, {
|
|
1226
|
+
path: ENDPOINTS.bizServiceUpdate,
|
|
1227
|
+
method: "POST",
|
|
1228
|
+
body: {
|
|
1229
|
+
id,
|
|
1230
|
+
code: args.serviceCode,
|
|
1231
|
+
name: args.name ?? row.name ?? args.serviceCode,
|
|
1232
|
+
adapterCode,
|
|
1233
|
+
description: args.description ?? (typeof row.description === "string" ? row.description : ""),
|
|
1234
|
+
configJson: echo(serviceConfigJson, typeof row.configJson === "string" ? row.configJson : null),
|
|
1235
|
+
serviceCategoryId: echo(args.serviceCategoryId, typeof row.serviceCategoryId === "string" ? row.serviceCategoryId : null),
|
|
1236
|
+
shared: echo(args.shared, row.shared === true),
|
|
1237
|
+
userIdList: echo(args.userIdList, Array.isArray(row.userIdList) ? row.userIdList : []),
|
|
1238
|
+
modifierList: echo(args.modifierList, Array.isArray(row.modifierList) ? row.modifierList : []),
|
|
1239
|
+
},
|
|
1240
|
+
});
|
|
1100
1241
|
const readback = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
//
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
1118
|
-
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1242
|
+
const after = readback.data;
|
|
1243
|
+
const nameMatches = args.name === undefined || after?.name === args.name;
|
|
1244
|
+
const configMatches = serviceConfigJson === undefined || (typeof after?.configJson === "string" && after.configJson === serviceConfigJson);
|
|
1245
|
+
// Regression guard against the full-row-overwrite wipe: when the caller
|
|
1246
|
+
// did NOT touch the config, an existing config must survive verbatim.
|
|
1247
|
+
const configPreserved = serviceConfigJson === undefined && typeof row.configJson === "string" ? after?.configJson === row.configJson : undefined;
|
|
1248
|
+
return {
|
|
1249
|
+
performed: { errcode: performed.errcode },
|
|
1250
|
+
readback: { servicePresent: after != null, nameMatches, configMatches, ...(configPreserved !== undefined ? { configPreserved } : {}), ...(adapterCode ? { adapterCode } : {}) },
|
|
1251
|
+
};
|
|
1252
|
+
},
|
|
1253
|
+
preflight: (args) => {
|
|
1254
|
+
// Zero-wire refusal: the raw-string configJson face. `omc call` bypasses
|
|
1255
|
+
// zod, so the string form must be refused here too. The adapter
|
|
1256
|
+
// whitelist/required-key preflight needs the service row's adapterCode
|
|
1257
|
+
// and therefore runs after the read-only id resolution — still before
|
|
1258
|
+
// any update write (see the execute body).
|
|
1259
|
+
assertStructuredConfig(args.serviceConfig, "serviceConfig");
|
|
1260
|
+
},
|
|
1261
|
+
rollback: b2ServiceRollbackSpec("service.update"),
|
|
1262
|
+
});
|
|
1263
|
+
writeTool({
|
|
1264
|
+
tools,
|
|
1265
|
+
name: "service.delete",
|
|
1266
|
+
lane: B2_LANE,
|
|
1267
|
+
scope: B2_TOOLS["service.delete"].scope,
|
|
1268
|
+
description: B2_TOOLS["service.delete"].description,
|
|
1269
|
+
inputSchema: { serviceCode: z.string().min(1) },
|
|
1270
|
+
execute: async (session, args) => {
|
|
1271
|
+
const found = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
1272
|
+
const id = found.data?.id;
|
|
1273
|
+
if (typeof id !== "string" || !id)
|
|
1274
|
+
return { performed: { errcode: 0 }, readback: { absent: true, note: "service already absent" } };
|
|
1275
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceDelete, method: "POST", query: { id } });
|
|
1276
|
+
let absent = false;
|
|
1277
|
+
try {
|
|
1278
|
+
const readback = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
1279
|
+
absent = readback.data == null;
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
// get_by_code answers 500001 业务服务不存在 when the service is gone.
|
|
1283
|
+
absent = true;
|
|
1284
|
+
}
|
|
1285
|
+
// Ticket 97 audit fix: the service-row absence alone does not prove the
|
|
1286
|
+
// METHOD rows are gone (DB-level cascade was never evidenced) — read the
|
|
1287
|
+
// method list by serviceCode and require it empty before absent:true.
|
|
1288
|
+
let methodsAbsent = false;
|
|
1289
|
+
try {
|
|
1290
|
+
const methods = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
1291
|
+
methodsAbsent = methods.length === 0;
|
|
1292
|
+
}
|
|
1293
|
+
catch {
|
|
1294
|
+
// The list face refuses once the owning service row is gone — the
|
|
1295
|
+
// methods are unreachable, which is the observable absence.
|
|
1296
|
+
methodsAbsent = true;
|
|
1297
|
+
}
|
|
1298
|
+
return { performed: { errcode: performed.errcode }, readback: { absent: absent && methodsAbsent, serviceRowAbsent: absent, methodsAbsent } };
|
|
1299
|
+
},
|
|
1121
1300
|
});
|
|
1122
1301
|
// -------------------------------------------------------------------------
|
|
1123
1302
|
// Ticket 106 — the IE (import/export) face
|
|
@@ -1293,75 +1472,84 @@ export function registerB2Lane(tools) {
|
|
|
1293
1472
|
};
|
|
1294
1473
|
}),
|
|
1295
1474
|
}));
|
|
1296
|
-
writeTool(
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1475
|
+
writeTool({
|
|
1476
|
+
tools,
|
|
1477
|
+
name: "service.import",
|
|
1478
|
+
lane: B2_LANE,
|
|
1479
|
+
scope: B2_TOOLS["service.import"].scope,
|
|
1480
|
+
description: B2_TOOLS["service.import"].description,
|
|
1481
|
+
inputSchema: {
|
|
1482
|
+
zipPath: z.string().min(1).describe("本地集成服务包路径(service.export 的 artifact.path)"),
|
|
1483
|
+
overWrite: z.boolean().describe("覆盖已存在的同码服务(true 时忽略重码改名)"),
|
|
1484
|
+
modifyServiceCodes: z.array(z.object({ oldServiceCode: z.string().min(1), newServiceCode: z.string().min(1) })).optional().describe("重码改名映射(overWrite=false 时解冲突)"),
|
|
1485
|
+
groupId: z.string().optional().describe("导入目标分类 id(service.category.list 解析;缺省沿用包内分类)"),
|
|
1486
|
+
},
|
|
1487
|
+
execute: async (session, args) => {
|
|
1488
|
+
const zip = readIeZipFile(args.zipPath);
|
|
1489
|
+
const overwriteScope = await ieImportScope(session, zip.manifest, args);
|
|
1490
|
+
const refId = await uploadIeZip(session, zip, args.zipPath);
|
|
1491
|
+
const performed = await wireCall(session, {
|
|
1492
|
+
path: ENDPOINTS.bizServiceImport,
|
|
1493
|
+
method: "POST",
|
|
1494
|
+
body: {
|
|
1495
|
+
fileName: refId,
|
|
1496
|
+
overWrite: args.overWrite,
|
|
1497
|
+
...(args.modifyServiceCodes !== undefined ? { modifyServiceCodes: args.modifyServiceCodes } : {}),
|
|
1498
|
+
...(args.groupId !== undefined ? { groupId: args.groupId } : {}),
|
|
1499
|
+
},
|
|
1500
|
+
});
|
|
1501
|
+
// Authoritative readback: every manifest service under its FINAL code
|
|
1502
|
+
// (rename applied), with its method face enumerated.
|
|
1503
|
+
const rename = new Map((args.modifyServiceCodes ?? []).map((entry) => [entry.oldServiceCode, entry.newServiceCode]));
|
|
1504
|
+
const services = [];
|
|
1505
|
+
for (const service of zip.manifest.services) {
|
|
1506
|
+
const finalCode = rename.get(service.code) ?? service.code;
|
|
1507
|
+
const entry = { code: finalCode, packagedCode: service.code };
|
|
1508
|
+
try {
|
|
1509
|
+
const found = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: finalCode } });
|
|
1510
|
+
const row = found.data;
|
|
1511
|
+
entry.present = row != null;
|
|
1512
|
+
if (row) {
|
|
1513
|
+
entry.name = row.name;
|
|
1514
|
+
entry.adapterCode = row.adapterCode;
|
|
1515
|
+
}
|
|
1516
|
+
const methods = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: finalCode, filterFlag: "false" } })).data);
|
|
1517
|
+
const packagedMethods = zip.manifest.methodsByService[service.code] ?? [];
|
|
1518
|
+
entry.methods = methods.map((method) => ({ code: method.code, name: method.name }));
|
|
1519
|
+
entry.parameterFaces = packagedMethods.map((packaged) => {
|
|
1520
|
+
const actual = methods.find((method) => method.code === packaged.code);
|
|
1521
|
+
return {
|
|
1522
|
+
methodCode: packaged.code,
|
|
1523
|
+
inputParametersMatch: actual !== undefined && String(actual.inputParametersJson ?? "") === String(packaged.raw.inputParametersJson ?? ""),
|
|
1524
|
+
outputParametersMatch: actual !== undefined && String(actual.outputParametersJson ?? "") === String(packaged.raw.outputParametersJson ?? ""),
|
|
1525
|
+
};
|
|
1526
|
+
});
|
|
1329
1527
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
}
|
|
1342
|
-
catch (error) {
|
|
1343
|
-
throw new ToolRefusalError({
|
|
1344
|
-
status: "refused",
|
|
1345
|
-
blocker: "bizservice-import-readback-failed",
|
|
1346
|
-
message: `导入后读回 ${finalCode} 失败,已完成部分已保留在报告中:${error instanceof Error ? error.message : String(error)}`,
|
|
1347
|
-
completedServices: services,
|
|
1348
|
-
failedService: finalCode,
|
|
1349
|
-
error: error instanceof Error ? error.message : String(error),
|
|
1350
|
-
});
|
|
1528
|
+
catch (error) {
|
|
1529
|
+
throw new ToolRefusalError({
|
|
1530
|
+
status: "refused",
|
|
1531
|
+
blocker: "bizservice-import-readback-failed",
|
|
1532
|
+
message: `导入后读回 ${finalCode} 失败,已完成部分已保留在报告中:${error instanceof Error ? error.message : String(error)}`,
|
|
1533
|
+
completedServices: services,
|
|
1534
|
+
failedService: finalCode,
|
|
1535
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
services.push(entry);
|
|
1351
1539
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1540
|
+
return {
|
|
1541
|
+
performed: { errcode: performed.errcode, overWrite: args.overWrite, ...(args.overWrite ? { overwriteScope } : {}), ...(args.modifyServiceCodes !== undefined ? { modifyServiceCodes: args.modifyServiceCodes } : {}), ...(args.groupId !== undefined ? { groupId: args.groupId } : {}) },
|
|
1542
|
+
readback: { services },
|
|
1543
|
+
};
|
|
1544
|
+
},
|
|
1545
|
+
preflight: (args) => {
|
|
1546
|
+
// Zero-wire rename lint: the platform checks newCode against
|
|
1547
|
+
// [a-zA-Z][a-zA-Z0-9_]{0,27} (BizServiceIEFacadeImpl.checkSingleCodeLegal,
|
|
1548
|
+
// INVALID_NAME 580001) only AFTER the upload; refuse earlier here.
|
|
1549
|
+
for (const entry of args.modifyServiceCodes ?? [])
|
|
1550
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]{0,27}$/.test(entry.newServiceCode))
|
|
1551
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-ie-rename-invalid", message: `newServiceCode "${entry.newServiceCode}" 不匹配平台合法编码 [a-zA-Z][a-zA-Z0-9_]{0,27}(BizServiceIEFacadeImpl.checkSingleCodeLegal,580001)`, newServiceCode: entry.newServiceCode });
|
|
1552
|
+
},
|
|
1365
1553
|
});
|
|
1366
1554
|
// -------------------------------------------------------------------------
|
|
1367
1555
|
// Ticket 107 — the designer TEST face
|
|
@@ -1464,115 +1652,124 @@ export function registerB2Lane(tools) {
|
|
|
1464
1652
|
};
|
|
1465
1653
|
},
|
|
1466
1654
|
};
|
|
1467
|
-
writeTool(
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1655
|
+
writeTool({
|
|
1656
|
+
tools,
|
|
1657
|
+
name: "method.test",
|
|
1658
|
+
lane: B2_LANE,
|
|
1659
|
+
scope: B2_TOOLS["method.test"].scope,
|
|
1660
|
+
description: B2_TOOLS["method.test"].description,
|
|
1661
|
+
inputSchema: {
|
|
1662
|
+
serviceCode: z.string().min(1),
|
|
1663
|
+
methodCode: z.string().min(1),
|
|
1664
|
+
testInputParametersMap: z.record(z.string(), z.unknown()).describe("试跑入参:参数 code → JSON 值(BizServiceMethodVO Map<String,Object>;按方法声明参数面校验)"),
|
|
1665
|
+
// P2-5(GitLab #18):method.test 是治理写(会真调外部系统),只读环境下默认
|
|
1666
|
+
// 拒绝——但一条纯 SELECT 的 DATA_SQL 方法没有任何副作用,此前却必须先把
|
|
1667
|
+
// 生产环境改成 writable 才能读数据(安全语义倒置)。显式传本参数 + 工具
|
|
1668
|
+
// 用真实配置证明「DATA_SQL 且严格 SELECT-only」才放行,其余一律原写门。
|
|
1669
|
+
readOnlyOnReadOnlyEnv: z.boolean().optional().describe("只读环境(writable=false)里显式放行本次试跑:仅当服务是 DATA_SQL 且方法 SQL 是单条 SELECT 时生效;不传=按写门拒绝(默认)。仍会真执行 SELECT 并记集成日志,不写配置行"),
|
|
1670
|
+
},
|
|
1671
|
+
execute: async (session, args) => {
|
|
1672
|
+
// Ticket 107 — a REAL one-shot execution of the configured method
|
|
1673
|
+
// (BizServiceMethodController.java:398-412 → BizServiceFacadeImpl:426-464
|
|
1674
|
+
// → the service's ProtocolAdapterRuntimeService). The chain writes NO
|
|
1675
|
+
// config rows (only async integration logs); the readback below proves
|
|
1676
|
+
// the stored method row survived the test untouched.
|
|
1677
|
+
const service = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
1678
|
+
const serviceRow = service.data;
|
|
1679
|
+
if (serviceRow === null)
|
|
1680
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-business", message: `业务服务不存在:${args.serviceCode}`, errcode: 500001 });
|
|
1681
|
+
const adapterCode = adapterCodeOf(serviceRow);
|
|
1682
|
+
if (!TEST_EVIDENCED_ADAPTERS.has(adapterCode))
|
|
1683
|
+
throw new ToolRefusalError({
|
|
1684
|
+
status: "refused",
|
|
1685
|
+
blocker: "bizservice-test-adapter-not-evidenced",
|
|
1686
|
+
message: `适配器 ${adapterCode || "(空)"} 在设计器测试面(service_test)无运行时证据(8.6.26 已取证:RESTFUL/DATA_SQL/DATA_TABLE/SOAP/INTERNAL)——不猜不伪造,拒绝试跑,零 service_test 调用`,
|
|
1687
|
+
adapterCode,
|
|
1688
|
+
});
|
|
1689
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
1690
|
+
const method = rows.find((row) => row.code === args.methodCode);
|
|
1691
|
+
if (!method)
|
|
1692
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-business", message: `业务方法不存在:${args.serviceCode}/${args.methodCode}`, errcode: 500001 });
|
|
1693
|
+
// Registration-shell boundary: a method with no stored config on a
|
|
1694
|
+
// config-carrying adapter has nothing executable to test.
|
|
1695
|
+
if (TEST_CONFIG_REQUIRED_ADAPTERS.has(adapterCode) && (method.configJson === null || method.configJson === undefined || method.configJson === ""))
|
|
1696
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-test-shell", message: `方法 ${args.methodCode} 是注册壳(无 configJson)——不是可执行 BizMethod,试跑拒绝,零 service_test 调用`, methodCode: args.methodCode });
|
|
1697
|
+
// Parameter-face gate (fail closed, zero service_test wire on violation):
|
|
1698
|
+
// every declared required input must be supplied; unknown keys are
|
|
1699
|
+
// refused instead of guessed onto the wire.
|
|
1700
|
+
const face = parseDeclaredParameterCodes(method.inputParametersJson);
|
|
1701
|
+
const supplied = Object.keys(args.testInputParametersMap);
|
|
1702
|
+
const missing = face.required.filter((code) => !(code in args.testInputParametersMap));
|
|
1703
|
+
if (missing.length > 0)
|
|
1704
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-test-param-missing", message: `试跑缺必填入参:${missing.join("、")}(按方法声明参数面 fail-closed,零试跑调用)`, missing });
|
|
1705
|
+
// Live 2026-10-18 (SubCon): the test readback echoed
|
|
1706
|
+
// {"x-lowcode-enginecode":null,...} and the caller only understood it
|
|
1707
|
+
// after eating a 401. Flag declared CONST parameters supplied as
|
|
1708
|
+
// null/blank BEFORE the wire: the platform injects them, so a null here
|
|
1709
|
+
// means "the declaration/injection is wrong", not "the test input is
|
|
1710
|
+
// missing".
|
|
1711
|
+
const blankConstants = face.constants.filter((code) => {
|
|
1712
|
+
if (!(code in args.testInputParametersMap))
|
|
1713
|
+
return true;
|
|
1714
|
+
const value = args.testInputParametersMap[code];
|
|
1715
|
+
return value === null || value === undefined || (typeof value === "string" && value.trim() === "");
|
|
1493
1716
|
});
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
// platform still sets `success:true` (the call was made), so a business
|
|
1548
|
-
// failure would otherwise read as externalSuccess:true. Surface the
|
|
1549
|
-
// nested business errcode and fold it into the verdict (dogfood round20).
|
|
1550
|
-
const body = external.data;
|
|
1551
|
-
const isEnvelope = body !== null && typeof body === "object" && !Array.isArray(body) && typeof body.errcode === "number";
|
|
1552
|
-
const resultBodyErrcode = isEnvelope ? body.errcode : undefined;
|
|
1553
|
-
const businessOk = resultBodyErrcode === undefined || resultBodyErrcode === 0;
|
|
1554
|
-
const externalSuccess = external.success === true && businessOk;
|
|
1555
|
-
const externalFailure = externalSuccess
|
|
1556
|
-
? {}
|
|
1557
|
-
: {
|
|
1558
|
-
...(typeof external.msg === "string" && external.msg ? { msg: external.msg } : {}),
|
|
1559
|
-
...(resultBodyErrcode === undefined ? {} : { resultBodyErrcode }),
|
|
1560
|
-
...(isEnvelope && typeof body.errmsg === "string" ? { resultBodyMsg: body.errmsg } : {}),
|
|
1561
|
-
note: businessOk
|
|
1562
|
-
? "外层 status:ok 只表示试跑调用完成;被测接口本身失败(externalSuccess:false)——按 message/result 诊断外部资源,不要当作方法可用"
|
|
1563
|
-
: `外层 status:ok 只表示试跑调用完成;被测接口本身回了业务错误 errcode=${resultBodyErrcode}(如同实例自调缺 X-LowCode-Corpid 头会回 50000 corpId can not be blank)——按 errmsg/result 诊断,不要当作方法可用`,
|
|
1717
|
+
const unknown = supplied.filter((code) => !face.all.includes(code));
|
|
1718
|
+
if (unknown.length > 0)
|
|
1719
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-test-param-unknown", message: `试跑携带未声明参数:${unknown.join("、")}(方法参数面只有 ${face.all.join("、") || "(空)"};不猜不透传,零试跑调用)`, unknown });
|
|
1720
|
+
const performed = await wireCall(session, {
|
|
1721
|
+
path: ENDPOINTS.bizServiceMethodTest,
|
|
1722
|
+
method: "POST",
|
|
1723
|
+
body: { serviceCode: args.serviceCode, code: args.methodCode, testInputParametersMap: args.testInputParametersMap },
|
|
1724
|
+
});
|
|
1725
|
+
// Zero-config-write proof: re-read the method row and compare the
|
|
1726
|
+
// stored configJson verbatim.
|
|
1727
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
1728
|
+
const after = afterRows.find((row) => row.code === args.methodCode);
|
|
1729
|
+
const configUnchanged = after !== undefined && String(after.configJson ?? "") === String(method.configJson ?? "");
|
|
1730
|
+
// The platform test answer nests its own success flag
|
|
1731
|
+
// (`{result:{success:false,msg:"..."}}`) while the outer envelope is
|
|
1732
|
+
// errcode 0 — so a business-level test FAILURE would otherwise read as
|
|
1733
|
+
// `ok:true`. Surface it as a first-class `externalSuccess` field (dogfood
|
|
1734
|
+
// R3) so "the call completed" is never confused with "the endpoint
|
|
1735
|
+
// answered successfully". The outer status stays `ok` because the test
|
|
1736
|
+
// itself did run; the field carries the external verdict.
|
|
1737
|
+
const external = (performed.data ?? {});
|
|
1738
|
+
// The platform answers `{success,msg,data}` where `data` is the adapter's
|
|
1739
|
+
// own result. For a same-instance RESTFUL call the HTTP layer can be a
|
|
1740
|
+
// 200 while the target answered a CloudPivot error envelope
|
|
1741
|
+
// (`{errcode:50000,errmsg:"..."}`) — e.g. `corpId can not be blank`. The
|
|
1742
|
+
// platform still sets `success:true` (the call was made), so a business
|
|
1743
|
+
// failure would otherwise read as externalSuccess:true. Surface the
|
|
1744
|
+
// nested business errcode and fold it into the verdict (dogfood round20).
|
|
1745
|
+
const body = external.data;
|
|
1746
|
+
const isEnvelope = body !== null && typeof body === "object" && !Array.isArray(body) && typeof body.errcode === "number";
|
|
1747
|
+
const resultBodyErrcode = isEnvelope ? body.errcode : undefined;
|
|
1748
|
+
const businessOk = resultBodyErrcode === undefined || resultBodyErrcode === 0;
|
|
1749
|
+
const externalSuccess = external.success === true && businessOk;
|
|
1750
|
+
const externalFailure = externalSuccess
|
|
1751
|
+
? {}
|
|
1752
|
+
: {
|
|
1753
|
+
...(typeof external.msg === "string" && external.msg ? { msg: external.msg } : {}),
|
|
1754
|
+
...(resultBodyErrcode === undefined ? {} : { resultBodyErrcode }),
|
|
1755
|
+
...(isEnvelope && typeof body.errmsg === "string" ? { resultBodyMsg: body.errmsg } : {}),
|
|
1756
|
+
note: businessOk
|
|
1757
|
+
? "外层 status:ok 只表示试跑调用完成;被测接口本身失败(externalSuccess:false)——按 message/result 诊断外部资源,不要当作方法可用"
|
|
1758
|
+
: `外层 status:ok 只表示试跑调用完成;被测接口本身回了业务错误 errcode=${resultBodyErrcode}(如同实例自调缺 X-LowCode-Corpid 头会回 50000 corpId can not be blank)——按 errmsg/result 诊断,不要当作方法可用`,
|
|
1759
|
+
};
|
|
1760
|
+
return {
|
|
1761
|
+
performed: { errcode: performed.errcode, adapterCode, ...(adapterCode === "RESTFUL" || adapterCode === "DATA_SQL" || adapterCode === "DATA_TABLE" ? { externalSideEffect: "real" } : {}) },
|
|
1762
|
+
readback: {
|
|
1763
|
+
result: performed.data,
|
|
1764
|
+
configUnchanged,
|
|
1765
|
+
externalSuccess,
|
|
1766
|
+
...(blankConstants.length ? { constantHeaderWarnings: blankConstants, constantHeaderHint: "平台注入的常量参数在试跑回显为 null/空——先确认参数声明 codeType:0 + bizCode(并为它们标 required:false);出现 401/EngineCode 空头时优先查这里,而不是改业务入参" } : {}),
|
|
1767
|
+
...(resultBodyErrcode === undefined ? {} : { resultBodyErrcode }),
|
|
1768
|
+
...(externalSuccess ? {} : { externalFailure }),
|
|
1769
|
+
},
|
|
1564
1770
|
};
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
result: performed.data,
|
|
1569
|
-
configUnchanged,
|
|
1570
|
-
externalSuccess,
|
|
1571
|
-
...(blankConstants.length ? { constantHeaderWarnings: blankConstants, constantHeaderHint: "平台注入的常量参数在试跑回显为 null/空——先确认参数声明 codeType:0 + bizCode(并为它们标 required:false);出现 401/EngineCode 空头时优先查这里,而不是改业务入参" } : {}),
|
|
1572
|
-
...(resultBodyErrcode === undefined ? {} : { resultBodyErrcode }),
|
|
1573
|
-
...(externalSuccess ? {} : { externalFailure }),
|
|
1574
|
-
},
|
|
1575
|
-
};
|
|
1576
|
-
}, undefined, undefined, undefined, methodTestReadOnlyEscape);
|
|
1771
|
+
},
|
|
1772
|
+
readOnlyEscape: methodTestReadOnlyEscape,
|
|
1773
|
+
});
|
|
1577
1774
|
}
|
|
1578
1775
|
//# sourceMappingURL=b2-registration.js.map
|