@stalfh233/omc-cli 0.0.0-stage → 0.4.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/LICENSE +21 -0
- package/README.md +293 -3
- package/coverage/m1-coverage-manifest-v1.json +1278 -0
- package/dist/approval-token.js +102 -0
- package/dist/args.js +25 -0
- package/dist/artifacts.js +119 -0
- package/dist/bench/call-face-eval.js +256 -0
- package/dist/bench/context-attribution.js +151 -0
- package/dist/bench/discovery-cost-eval.js +230 -0
- package/dist/bench/driver.js +81 -0
- package/dist/bench/evals.js +236 -0
- package/dist/bench/fake-http-server.js +65 -0
- package/dist/bench/instrument.js +87 -0
- package/dist/bench/intent-face-eval.js +343 -0
- package/dist/bench/run.js +166 -0
- package/dist/bench/scenario.js +343 -0
- package/dist/bench/types.js +76 -0
- package/dist/bi-wire.js +41 -0
- package/dist/bizservice-config.js +581 -0
- package/dist/call.js +153 -0
- package/dist/capability-absences.js +23 -0
- package/dist/capability-overview.js +492 -0
- package/dist/capability-shape.js +154 -0
- package/dist/cli-contract.js +70 -0
- package/dist/cli-output.js +73 -0
- package/dist/cli.js +1418 -0
- package/dist/code-rules.js +69 -0
- package/dist/command-transport.js +155 -0
- package/dist/config-store.js +195 -0
- package/dist/context.js +21 -0
- package/dist/contract-consistency.js +66 -0
- package/dist/contract-resources.js +62 -0
- package/dist/coverage-consistency.js +62 -0
- package/dist/coverage-registry.js +76 -0
- package/dist/coverage.js +130 -0
- package/dist/data-list-filter.js +114 -0
- package/dist/discovery.js +390 -0
- package/dist/endpoints.js +154 -0
- package/dist/environment-policy.js +26 -0
- package/dist/execution-metadata.js +1092 -0
- package/dist/fake/app.js +45 -0
- package/dist/fake/b2-registration.js +594 -0
- package/dist/fake/businessrule.js +242 -0
- package/dist/fake/datarule.js +72 -0
- package/dist/fake/dictionary.js +108 -0
- package/dist/fake/environment.js +30 -0
- package/dist/fake/field.js +167 -0
- package/dist/fake/form.js +101 -0
- package/dist/fake/index.js +121 -0
- package/dist/fake/list-view.js +289 -0
- package/dist/fake/model.js +136 -0
- package/dist/fake/online-js.js +18 -0
- package/dist/fake/report.js +253 -0
- package/dist/fake/routes.js +47 -0
- package/dist/fake/rule-lifecycle.js +37 -0
- package/dist/fake/runtime-data.js +367 -0
- package/dist/fake/state.js +67 -0
- package/dist/fake/workflow.js +364 -0
- package/dist/field-change.js +200 -0
- package/dist/field-families.js +896 -0
- package/dist/form-layout.js +111 -0
- package/dist/form-support.js +821 -0
- package/dist/goal-routes.js +468 -0
- package/dist/governed-execution.js +87 -0
- package/dist/human-summary.js +212 -0
- package/dist/identity.js +62 -0
- package/dist/intent/baseline.js +57 -0
- package/dist/intent/capabilities/bizservice.js +274 -0
- package/dist/intent/capabilities/businessrule.js +493 -0
- package/dist/intent/capabilities/datarule.js +187 -0
- package/dist/intent/capabilities/field.js +545 -0
- package/dist/intent/capabilities/form.js +136 -0
- package/dist/intent/capabilities/index.js +64 -0
- package/dist/intent/capabilities/listview.js +157 -0
- package/dist/intent/capabilities/model.js +100 -0
- package/dist/intent/capabilities/onlinejs.js +108 -0
- package/dist/intent/capabilities/report.js +355 -0
- package/dist/intent/capabilities/workflow.js +458 -0
- package/dist/intent/capability.js +6 -0
- package/dist/intent/cli.js +91 -0
- package/dist/intent/compare.js +56 -0
- package/dist/intent/compiler.js +79 -0
- package/dist/intent/dsl.js +129 -0
- package/dist/intent/plan-file.js +63 -0
- package/dist/intent/readback.js +65 -0
- package/dist/intent/schema.js +158 -0
- package/dist/intent/validation.js +30 -0
- package/dist/intent/yaml.js +315 -0
- package/dist/json-column.js +68 -0
- package/dist/lanes/app-contract.js +95 -0
- package/dist/lanes/app-coverage.js +16 -0
- package/dist/lanes/app.js +174 -0
- package/dist/lanes/apply-changes.js +231 -0
- package/dist/lanes/b2-registration-contract.js +292 -0
- package/dist/lanes/b2-registration-coverage.js +48 -0
- package/dist/lanes/b2-registration.js +1187 -0
- package/dist/lanes/businessrule-contract.js +232 -0
- package/dist/lanes/businessrule-coverage.js +16 -0
- package/dist/lanes/businessrule.js +221 -0
- package/dist/lanes/contract-support.js +65 -0
- package/dist/lanes/coverage-declaration.js +9 -0
- package/dist/lanes/datarule-contract.js +155 -0
- package/dist/lanes/datarule-coverage.js +19 -0
- package/dist/lanes/datarule-protocol.js +308 -0
- package/dist/lanes/datarule.js +818 -0
- package/dist/lanes/dictionary-contract.js +78 -0
- package/dist/lanes/dictionary-coverage.js +24 -0
- package/dist/lanes/dictionary.js +235 -0
- package/dist/lanes/environment-contract.js +70 -0
- package/dist/lanes/environment-coverage.js +14 -0
- package/dist/lanes/environment.js +163 -0
- package/dist/lanes/field-contract.js +223 -0
- package/dist/lanes/field-coverage.js +27 -0
- package/dist/lanes/field.js +374 -0
- package/dist/lanes/form-contract.js +97 -0
- package/dist/lanes/form-coverage.js +16 -0
- package/dist/lanes/form.js +185 -0
- package/dist/lanes/lane-ids.js +34 -0
- package/dist/lanes/list-view-contract.js +175 -0
- package/dist/lanes/list-view-coverage.js +20 -0
- package/dist/lanes/list-view-shapes.js +1207 -0
- package/dist/lanes/list-view.js +578 -0
- package/dist/lanes/meta-contract.js +85 -0
- package/dist/lanes/meta.js +255 -0
- package/dist/lanes/model-contract.js +168 -0
- package/dist/lanes/model-coverage.js +20 -0
- package/dist/lanes/model.js +986 -0
- package/dist/lanes/online-js-contract.js +99 -0
- package/dist/lanes/online-js-coverage.js +28 -0
- package/dist/lanes/online-js.js +127 -0
- package/dist/lanes/report-contract.js +144 -0
- package/dist/lanes/report-coverage.js +21 -0
- package/dist/lanes/report.js +476 -0
- package/dist/lanes/rule-graph.js +1846 -0
- package/dist/lanes/rule-lifecycle-contract.js +92 -0
- package/dist/lanes/rule-lifecycle-coverage.js +20 -0
- package/dist/lanes/rule-lifecycle.js +176 -0
- package/dist/lanes/runtime-data-contract.js +264 -0
- package/dist/lanes/runtime-data-coverage.js +25 -0
- package/dist/lanes/runtime-data.js +1054 -0
- package/dist/lanes/workflow-contract.js +223 -0
- package/dist/lanes/workflow-coverage.js +40 -0
- package/dist/lanes/workflow.js +1813 -0
- package/dist/online-js-layout.js +58 -0
- package/dist/online-js-source.js +276 -0
- package/dist/package-tool.js +51 -0
- package/dist/package.js +73 -0
- package/dist/plan.js +73 -0
- package/dist/read.js +144 -0
- package/dist/redact.js +28 -0
- package/dist/rule-support.js +587 -0
- package/dist/runtime-support.js +134 -0
- package/dist/server.js +92 -0
- package/dist/session-manager.js +30 -0
- package/dist/session.js +149 -0
- package/dist/skills.js +112 -0
- package/dist/support.js +98 -0
- package/dist/tool-types.js +127 -0
- package/dist/tools.js +59 -0
- package/dist/usage-log.js +197 -0
- package/dist/wire.js +287 -0
- package/dist/workflow-support.js +99 -0
- package/dist/write-lease.js +26 -0
- package/dist/write-lock.js +109 -0
- package/dist/write.js +176 -0
- package/dist/zip.js +156 -0
- package/docs/tool-surface-map.md +42 -0
- package/package.json +71 -6
- package/skills/omc-acceptance-criteria.md +47 -0
- package/skills/omc-business-configuration.md +257 -0
- package/skills/omc-capabilities.md +197 -0
- package/skills/omc-capability-scouting.md +55 -0
- package/skills/omc-config-draft-review.md +171 -0
- package/skills/omc-five-piece-flow.md +35 -0
- package/skills/omc-glossary.md +86 -0
- package/skills/omc-refusals.md +110 -0
- package/skills/omc-requirement-analysis.md +161 -0
- package/skills/omc-requirement-vocabulary.md +51 -0
- package/skills/omc-start-here.md +82 -0
- package/skills/omc-tool-selection.md +119 -0
- package/skills/omc-write-hazards.md +87 -0
|
@@ -0,0 +1,1187 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { ENDPOINTS } from "../endpoints.js";
|
|
6
|
+
import { capabilityArg, environmentArg } from "../args.js";
|
|
7
|
+
import { governedRead } from "../read.js";
|
|
8
|
+
import { writeTool } from "../write.js";
|
|
9
|
+
import { decodeJsonColumn as decodeSharedJsonColumn, JSON_COLUMN_NULL_COALESCE } from "../json-column.js";
|
|
10
|
+
import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
11
|
+
import { wireCall, wireRows, wireDownloadBuffer, wireUploadFile, WireError } from "../wire.js";
|
|
12
|
+
import { ZipFormatError, readZipEntries, zipEntryByBasename } from "../zip.js";
|
|
13
|
+
import { B2_LANE_CONTRACT } from "./b2-registration-contract.js";
|
|
14
|
+
import { BIZSERVICE_WRITE_ADAPTERS, assertBizServiceAdapterWhitelisted, assertStructuredConfig, compileMethodConfigPlain, compileParameterFace, compileServiceConfig, maskServiceConfigSecrets, normalizeMethodConfigReadback, normalizeParameterFaceReadback, parseIeManifest, iePackageSummary, } from "../bizservice-config.js";
|
|
15
|
+
import { encryptBizServiceMethodConfigJson, encryptBizDbPoolPassword } from "omc-core";
|
|
16
|
+
const B2_LANE = B2_LANE_CONTRACT.lane;
|
|
17
|
+
const B2_TOOLS = B2_LANE_CONTRACT.tools;
|
|
18
|
+
/** Decode a JSON-string column (configJson/inputParametersJson/…); policy: non-strings null-coalesce, unparseable passes through. */
|
|
19
|
+
function decodeJsonColumn(value) {
|
|
20
|
+
return decodeSharedJsonColumn(value, JSON_COLUMN_NULL_COALESCE);
|
|
21
|
+
}
|
|
22
|
+
function adapterCodeOf(serviceRow) {
|
|
23
|
+
return typeof serviceRow?.adapterCode === "string" ? serviceRow.adapterCode : "";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Ticket 103 — fail-closed dangling-category preflight. The platform writes
|
|
27
|
+
* serviceCategoryId verbatim with NO existence validation
|
|
28
|
+
* (BizServiceServiceImpl.update:251; create likewise), so an orphan id would
|
|
29
|
+
* silently strand the service outside every real directory. The tool refuses
|
|
30
|
+
* before any write unless the id resolves to a non-virtual category row
|
|
31
|
+
* (the「全部」row carries an empty id and can never be one).
|
|
32
|
+
*/
|
|
33
|
+
const ALL_ZERO_ID_RE = /^0{16,}$/;
|
|
34
|
+
async function assertCategoryResolves(session, serviceCategoryId) {
|
|
35
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
36
|
+
const resolved = rows.find((row) => row.id === serviceCategoryId && typeof row.id === "string" && row.id !== "");
|
|
37
|
+
if (!resolved)
|
|
38
|
+
throw new ToolRefusalError({
|
|
39
|
+
status: "refused",
|
|
40
|
+
blocker: "bizservice-category-dangling",
|
|
41
|
+
message: `serviceCategoryId ${serviceCategoryId} 不存在于 /bizservice/category/list(虚拟「全部」id 为空,同样不可用)——先用 service.category.list 解析(取 usable[].id 真实值)或 service.category.create 新建;平台写面不校验该 id,悬空即拒绝,零写入`,
|
|
42
|
+
hint: ALL_ZERO_ID_RE.test(serviceCategoryId) ? "该 id 是全 0 掩码/占位值(r5-crm 实证:agent 一直用全 0 占位当真实 categoryId,四连拒后误判为环境限制)。真实 id 只能取 service.category.list 返回的 usable[].id——回显/兜底拼接出的全 0 串不是任何分类的 id。" : undefined,
|
|
43
|
+
serviceCategoryId,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** A JSON-string parameter face parses to an array when valid; otherwise undefined so the compile layer reports it. */
|
|
47
|
+
function safeParseArray(value) {
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(value);
|
|
50
|
+
return Array.isArray(parsed) ? parsed : undefined;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Ticket 107 — the DECLARED input parameter face of a method row, as
|
|
58
|
+
* (all codes, required codes). An unparsable/absent face decodes to no
|
|
59
|
+
* constraints (the platform itself quotes `args` by parameter code only).
|
|
60
|
+
*/
|
|
61
|
+
function parseDeclaredParameterCodes(inputParametersJson) {
|
|
62
|
+
const parsed = typeof inputParametersJson === "string" ? safeParseArray(inputParametersJson) : Array.isArray(inputParametersJson) ? inputParametersJson : undefined;
|
|
63
|
+
if (!parsed)
|
|
64
|
+
return { all: [], required: [] };
|
|
65
|
+
const all = [];
|
|
66
|
+
const required = [];
|
|
67
|
+
for (const entry of parsed) {
|
|
68
|
+
if (!entry || typeof entry !== "object")
|
|
69
|
+
continue;
|
|
70
|
+
const code = entry.code;
|
|
71
|
+
if (typeof code !== "string" || !code.trim())
|
|
72
|
+
continue;
|
|
73
|
+
all.push(code);
|
|
74
|
+
if (entry.required === true)
|
|
75
|
+
required.push(code);
|
|
76
|
+
}
|
|
77
|
+
return { all, required };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Ticket 104 — readback face for a connection-pool row: an explicit field
|
|
81
|
+
* whitelist WITHOUT `password`. The platform's raw list does not mask the
|
|
82
|
+
* credential column, so the strip lives in the tool boundary.
|
|
83
|
+
*/
|
|
84
|
+
function poolReadbackFace(pool) {
|
|
85
|
+
return {
|
|
86
|
+
id: pool.id,
|
|
87
|
+
code: pool.code,
|
|
88
|
+
name: pool.name,
|
|
89
|
+
...(pool.description !== undefined && pool.description !== null ? { description: pool.description } : {}),
|
|
90
|
+
databaseType: pool.databaseType,
|
|
91
|
+
datasourceType: pool.datasourceType,
|
|
92
|
+
jdbcUrl: pool.jdbcUrl,
|
|
93
|
+
...(pool.username !== undefined && pool.username !== null ? { username: pool.username } : {}),
|
|
94
|
+
...(pool.externInfo !== undefined && pool.externInfo !== null ? { externInfo: pool.externInfo } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Ticket 104 — fail-closed DATA_SQL/DATA_TABLE binding preflight. The
|
|
99
|
+
* platform stores commonlist.databaseConnectionPoolCode with no existence
|
|
100
|
+
* validation; a dangling code strands the service against a pool that cannot
|
|
101
|
+
* resolve. Refused before any write (read-only list resolution precedes).
|
|
102
|
+
*/
|
|
103
|
+
async function assertDbConnPoolResolves(session, poolCode) {
|
|
104
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
105
|
+
const resolved = rows.find((row) => row.code === poolCode);
|
|
106
|
+
if (!resolved)
|
|
107
|
+
throw new ToolRefusalError({
|
|
108
|
+
status: "refused",
|
|
109
|
+
blocker: "bizservice-dbconnpool-dangling",
|
|
110
|
+
message: `databaseConnectionPoolCode ${poolCode} 不存在于 /bizservice/dbconnpool/list——先用 service.dbconnpool.list 解析或 service.dbconnpool.create 新建;平台写面不校验该 code,悬空即拒绝,零写入`,
|
|
111
|
+
databaseConnectionPoolCode: poolCode,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** Ticket 104 — extract a DATA_SQL/DATA_TABLE service config's pool code (undefined when absent). */
|
|
115
|
+
function dbConnPoolCodeOf(serviceConfig) {
|
|
116
|
+
const commonlist = serviceConfig.commonlist;
|
|
117
|
+
if (!Array.isArray(commonlist))
|
|
118
|
+
return undefined;
|
|
119
|
+
for (const entry of commonlist) {
|
|
120
|
+
if (entry && typeof entry === "object" && entry.code === "databaseConnectionPoolCode") {
|
|
121
|
+
const value = entry.value;
|
|
122
|
+
if (typeof value === "string" && value)
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
/** get_by_code answers 500001 业务服务不存在 when the service is gone (BizServiceController). */
|
|
129
|
+
function isServiceAbsent(error) {
|
|
130
|
+
return error instanceof WireError && error.kind === "business" && error.errcode === 500001;
|
|
131
|
+
}
|
|
132
|
+
export function registerB2Lane(tools) {
|
|
133
|
+
tools.push(defineTool({
|
|
134
|
+
name: "service.get",
|
|
135
|
+
layer: "low",
|
|
136
|
+
kind: "read",
|
|
137
|
+
lane: B2_LANE,
|
|
138
|
+
scope: B2_TOOLS["service.get"].scope,
|
|
139
|
+
description: B2_TOOLS["service.get"].description,
|
|
140
|
+
inputSchema: { environment: environmentArg, serviceCode: z.string(), capability: capabilityArg },
|
|
141
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
142
|
+
let service;
|
|
143
|
+
try {
|
|
144
|
+
service = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
// A deleted service answers the wire-business error 500001; reading a
|
|
148
|
+
// just-cleaned-up service back to VERIFY the cleanup is a normal
|
|
149
|
+
// success, not a `refused` envelope (dogfood R3). Absence is a fact.
|
|
150
|
+
if (isServiceAbsent(error))
|
|
151
|
+
return { serviceCode: args.serviceCode, present: false, absent: true, service: null, adapterCode: null, methods: [], methodsError: null };
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
const serviceRow = service.data;
|
|
155
|
+
let methods = null;
|
|
156
|
+
let methodsError = null;
|
|
157
|
+
try {
|
|
158
|
+
methods = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data).map((row) => ({
|
|
159
|
+
code: row.code,
|
|
160
|
+
name: row.name,
|
|
161
|
+
serviceCode: row.serviceCode ?? args.serviceCode,
|
|
162
|
+
configJson: maskServiceConfigSecrets(normalizeMethodConfigReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.configJson))),
|
|
163
|
+
inputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.inputParametersJson)),
|
|
164
|
+
outputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.outputParametersJson)),
|
|
165
|
+
referenced: row.referenced === true,
|
|
166
|
+
}));
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
methodsError = error instanceof Error ? error.message : String(error);
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
serviceCode: args.serviceCode,
|
|
173
|
+
present: serviceRow !== null,
|
|
174
|
+
// Ticket 115: the READ face masks auth parameterlist VALUES (handoff
|
|
175
|
+
// §5 — credentials never echo); structure stays readable.
|
|
176
|
+
service: serviceRow === null ? null : { ...serviceRow, ...(serviceRow.configJson !== undefined ? { configJson: maskServiceConfigSecrets(decodeJsonColumn(serviceRow.configJson)) } : {}) },
|
|
177
|
+
adapterCode: serviceRow?.adapterCode ?? null,
|
|
178
|
+
methods,
|
|
179
|
+
methodsError,
|
|
180
|
+
};
|
|
181
|
+
}),
|
|
182
|
+
}));
|
|
183
|
+
tools.push(defineTool({
|
|
184
|
+
name: "method.get",
|
|
185
|
+
layer: "low",
|
|
186
|
+
kind: "read",
|
|
187
|
+
lane: B2_LANE,
|
|
188
|
+
scope: B2_TOOLS["method.get"].scope,
|
|
189
|
+
description: B2_TOOLS["method.get"].description,
|
|
190
|
+
inputSchema: { environment: environmentArg, serviceCode: z.string().min(1), methodCode: z.string().min(1), capability: capabilityArg },
|
|
191
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
192
|
+
let service = null;
|
|
193
|
+
try {
|
|
194
|
+
service = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
// Same absence semantics as service.get: a deleted service (and
|
|
198
|
+
// therefore its method) is `absent:true`, not a refused envelope.
|
|
199
|
+
if (isServiceAbsent(error))
|
|
200
|
+
return { serviceCode: args.serviceCode, methodCode: args.methodCode, present: false, absent: true, method: null };
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
const serviceRow = service.data;
|
|
204
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
205
|
+
const listed = rows.find((candidate) => candidate.code === args.methodCode);
|
|
206
|
+
if (!listed || typeof listed.id !== "string" || !listed.id)
|
|
207
|
+
return { serviceCode: args.serviceCode, methodCode: args.methodCode, present: false, absent: true, method: null };
|
|
208
|
+
const fetched = await wireCall(session, { path: ENDPOINTS.bizServiceMethodGet, query: { id: listed.id } });
|
|
209
|
+
const row = fetched.data;
|
|
210
|
+
if (!row)
|
|
211
|
+
return { serviceCode: args.serviceCode, methodCode: args.methodCode, present: false, absent: true, method: null };
|
|
212
|
+
const method = {
|
|
213
|
+
...row,
|
|
214
|
+
configJson: maskServiceConfigSecrets(normalizeMethodConfigReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.configJson))),
|
|
215
|
+
inputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.inputParametersJson)),
|
|
216
|
+
outputParameters: normalizeParameterFaceReadback(adapterCodeOf(serviceRow), decodeJsonColumn(row.outputParametersJson)),
|
|
217
|
+
};
|
|
218
|
+
return { serviceCode: args.serviceCode, methodCode: args.methodCode, present: true, method };
|
|
219
|
+
}),
|
|
220
|
+
}));
|
|
221
|
+
writeTool(tools, "method.update", B2_LANE, B2_TOOLS["method.update"].scope, B2_TOOLS["method.update"].description, {
|
|
222
|
+
serviceCode: z.string().min(1),
|
|
223
|
+
methodCode: z.string().min(1),
|
|
224
|
+
name: z.string().optional(),
|
|
225
|
+
methodConfig: z.record(z.string(), z.unknown()).optional(),
|
|
226
|
+
methodInputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional(),
|
|
227
|
+
methodOutputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional(),
|
|
228
|
+
}, async (session, args) => {
|
|
229
|
+
const service = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
230
|
+
const serviceRow = service.data;
|
|
231
|
+
const adapterCode = assertBizServiceAdapterWhitelisted(adapterCodeOf(serviceRow));
|
|
232
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
233
|
+
const current = rows.find((row) => row.code === args.methodCode);
|
|
234
|
+
if (!current)
|
|
235
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-business", message: `业务方法不存在:${args.serviceCode}/${args.methodCode}`, errcode: 500001 });
|
|
236
|
+
const currentRecord = current;
|
|
237
|
+
const inputJson = args.methodInputParameters === undefined ? currentRecord.inputParametersJson : compileParameterFace(adapterCode, typeof args.methodInputParameters === "string" ? safeParseArray(args.methodInputParameters) : args.methodInputParameters, "methodInputParameters");
|
|
238
|
+
const outputJson = args.methodOutputParameters === undefined ? currentRecord.outputParametersJson : compileParameterFace(adapterCode, typeof args.methodOutputParameters === "string" ? safeParseArray(args.methodOutputParameters) : args.methodOutputParameters, "methodOutputParameters");
|
|
239
|
+
const methodConfigPlain = args.methodConfig === undefined ? undefined : compileMethodConfigPlain(adapterCode, args.methodConfig);
|
|
240
|
+
const currentConfig = decodeJsonColumn(currentRecord.configJson);
|
|
241
|
+
const currentConfigPlain = currentConfig === undefined || currentConfig === null ? undefined : compileMethodConfigPlain(adapterCode, currentConfig);
|
|
242
|
+
const performed = await wireCall(session, {
|
|
243
|
+
path: ENDPOINTS.bizServiceMethodUpdate,
|
|
244
|
+
method: "POST",
|
|
245
|
+
// Live 2026-09-15 + source evidence (BizServiceMethodModel.java:88-146):
|
|
246
|
+
// a body carrying the transient `inputParameterModels`/`inputParameters`
|
|
247
|
+
// keys makes the model setter RE-SERIALIZE and filter out parameters
|
|
248
|
+
// lacking bizPropertyType/externParameterType, overwriting
|
|
249
|
+
// inputParametersJson with "[]". The update body must therefore carry
|
|
250
|
+
// ONLY the persisted columns — never the read-model's transient keys.
|
|
251
|
+
body: {
|
|
252
|
+
id: currentRecord.id,
|
|
253
|
+
serviceCode: args.serviceCode,
|
|
254
|
+
code: args.methodCode,
|
|
255
|
+
name: args.name ?? (typeof currentRecord.name === "string" ? currentRecord.name : args.methodCode),
|
|
256
|
+
...(methodConfigPlain !== undefined ? { configJson: encryptBizServiceMethodConfigJson(methodConfigPlain) } : currentConfigPlain !== undefined ? { configJson: encryptBizServiceMethodConfigJson(currentConfigPlain) } : {}),
|
|
257
|
+
...(inputJson !== undefined && inputJson !== null ? { inputParametersJson: inputJson } : {}),
|
|
258
|
+
...(outputJson !== undefined && outputJson !== null ? { outputParametersJson: outputJson } : {}),
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
262
|
+
const after = afterRows.find((row) => row.code === args.methodCode);
|
|
263
|
+
return { performed: { errcode: performed.errcode }, readback: { methodPresent: after != null, nameMatches: args.name === undefined || after?.name === args.name, configMatches: methodConfigPlain === undefined || after?.configJson === methodConfigPlain, inputParameters: decodeJsonColumn(after?.inputParametersJson), outputParameters: decodeJsonColumn(after?.outputParametersJson) } };
|
|
264
|
+
}, (args) => {
|
|
265
|
+
assertStructuredConfig(args.methodConfig, "methodConfig");
|
|
266
|
+
});
|
|
267
|
+
writeTool(tools, "method.create", B2_LANE, B2_TOOLS["method.create"].scope, B2_TOOLS["method.create"].description, {
|
|
268
|
+
serviceCode: z.string().min(1).describe("已存在的业务服务编码(在其下新增方法)"),
|
|
269
|
+
methodCode: z.string().min(1),
|
|
270
|
+
name: z.string().min(1).describe("方法名称(设计器必填,中文≤50)"),
|
|
271
|
+
methodConfig: z.record(z.string(), z.unknown()).optional(),
|
|
272
|
+
methodInputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional(),
|
|
273
|
+
methodOutputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional(),
|
|
274
|
+
}, async (session, args) => {
|
|
275
|
+
// Ticket 116: a new extension endpoint does NOT always mean a new
|
|
276
|
+
// service — the common case is ADDING a method to an existing service.
|
|
277
|
+
// service.register refuses a duplicate serviceCode (duplicate-service)
|
|
278
|
+
// and method.update refuses an absent method, so without this tool the
|
|
279
|
+
// only path was delete-and-recreate (which drops the service and its
|
|
280
|
+
// other methods/rule bindings). Platform face: POST
|
|
281
|
+
// /bizservice/method/create with BizServiceMethodModel{serviceCode,code,
|
|
282
|
+
// name,configJson,inputParametersJson,outputParametersJson}
|
|
283
|
+
// (BizServiceMethodController.java:133-170); name/code/serviceCode are
|
|
284
|
+
// validated non-blank, name ≤50 Chinese chars.
|
|
285
|
+
const service = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
286
|
+
const serviceRow = service.data;
|
|
287
|
+
if (!serviceRow)
|
|
288
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-business", message: `业务服务不存在:${args.serviceCode};在已有服务下加方法前先确认服务存在(新增服务用 service.register)`, errcode: 500001 });
|
|
289
|
+
const adapterCode = assertBizServiceAdapterWhitelisted(adapterCodeOf(serviceRow));
|
|
290
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
291
|
+
if (rows.some((row) => row.code === args.methodCode))
|
|
292
|
+
throw new ToolRefusalError({ status: "refused", blocker: "duplicate-method", serviceCode: args.serviceCode, methodCode: args.methodCode, message: `业务方法 ${args.serviceCode}/${args.methodCode} 已存在;改方法用 method.update` });
|
|
293
|
+
const inputJson = args.methodInputParameters === undefined ? undefined : compileParameterFace(adapterCode, typeof args.methodInputParameters === "string" ? safeParseArray(args.methodInputParameters) : args.methodInputParameters, "methodInputParameters");
|
|
294
|
+
const outputJson = args.methodOutputParameters === undefined ? undefined : compileParameterFace(adapterCode, typeof args.methodOutputParameters === "string" ? safeParseArray(args.methodOutputParameters) : args.methodOutputParameters, "methodOutputParameters");
|
|
295
|
+
const methodConfigPlain = args.methodConfig === undefined ? undefined : compileMethodConfigPlain(adapterCode, args.methodConfig);
|
|
296
|
+
const performed = await wireCall(session, {
|
|
297
|
+
path: ENDPOINTS.bizServiceMethodCreate,
|
|
298
|
+
method: "POST",
|
|
299
|
+
body: {
|
|
300
|
+
serviceCode: args.serviceCode,
|
|
301
|
+
code: args.methodCode,
|
|
302
|
+
name: args.name,
|
|
303
|
+
...(methodConfigPlain !== undefined ? { configJson: encryptBizServiceMethodConfigJson(methodConfigPlain) } : {}),
|
|
304
|
+
...(inputJson !== undefined && inputJson !== null ? { inputParametersJson: inputJson } : {}),
|
|
305
|
+
...(outputJson !== undefined && outputJson !== null ? { outputParametersJson: outputJson } : {}),
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
309
|
+
const after = afterRows.find((row) => row.code === args.methodCode);
|
|
310
|
+
return { performed: { errcode: performed.errcode }, readback: { methodPresent: after != null, nameMatches: after?.name === args.name, configMatches: methodConfigPlain === undefined || after?.configJson === methodConfigPlain, inputParameters: decodeJsonColumn(after?.inputParametersJson), outputParameters: decodeJsonColumn(after?.outputParametersJson) } };
|
|
311
|
+
}, (args) => {
|
|
312
|
+
assertStructuredConfig(args.methodConfig, "methodConfig");
|
|
313
|
+
});
|
|
314
|
+
writeTool(tools, "method.delete", B2_LANE, B2_TOOLS["method.delete"].scope, B2_TOOLS["method.delete"].description, { serviceCode: z.string().min(1), methodCode: z.string().min(1) }, async (session, args) => {
|
|
315
|
+
// Id resolution mirrors service.delete: the delete endpoint keys on the
|
|
316
|
+
// row id (query, comma-separated on the platform face — this tool stays
|
|
317
|
+
// single-target), resolved via list_by_service_code + method code.
|
|
318
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
319
|
+
const current = rows.find((row) => row.code === args.methodCode);
|
|
320
|
+
const id = current?.id;
|
|
321
|
+
if (typeof id !== "string" || !id)
|
|
322
|
+
return { performed: { errcode: 0 }, readback: { absent: true, note: "method already absent" } };
|
|
323
|
+
// A referenced method answers 500108 业务方法已绑定默认方法,不可删除
|
|
324
|
+
// (BizServiceMethodController.java:246-272) — governedWrite surfaces the
|
|
325
|
+
// platform refusal verbatim with errcode + message; the rule and its
|
|
326
|
+
// binding stay untouched.
|
|
327
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceMethodDelete, method: "POST", query: { id } });
|
|
328
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
329
|
+
const after = afterRows.find((row) => row.code === args.methodCode);
|
|
330
|
+
return { performed: { errcode: performed.errcode }, readback: { absent: after == null, ...(after ? { method: after } : {}) } };
|
|
331
|
+
});
|
|
332
|
+
tools.push(defineTool({
|
|
333
|
+
name: "service.category.list",
|
|
334
|
+
layer: "low",
|
|
335
|
+
kind: "read",
|
|
336
|
+
lane: B2_LANE,
|
|
337
|
+
scope: B2_TOOLS["service.category.list"].scope,
|
|
338
|
+
description: B2_TOOLS["service.category.list"].description,
|
|
339
|
+
inputSchema: { environment: environmentArg, capability: capabilityArg },
|
|
340
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
341
|
+
// Standard face: /bizservice/category/list returns [{id,name}]; the
|
|
342
|
+
// virtual「全部」row carries an empty id and is flagged — it is NOT a
|
|
343
|
+
// valid 所属目录 for register/update.
|
|
344
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
345
|
+
const categories = rows.map((row) => ({ id: typeof row.id === "string" ? row.id : "", name: row.name }));
|
|
346
|
+
return {
|
|
347
|
+
categories,
|
|
348
|
+
usable: categories.filter((row) => row.id !== ""),
|
|
349
|
+
virtual: categories.filter((row) => row.id === "").map((row) => row.name),
|
|
350
|
+
};
|
|
351
|
+
}),
|
|
352
|
+
}));
|
|
353
|
+
writeTool(tools, "service.category.create", B2_LANE, B2_TOOLS["service.category.create"].scope, B2_TOOLS["service.category.create"].description, {
|
|
354
|
+
name: z.string().min(1).describe("分类名称(非空)"),
|
|
355
|
+
}, async (session, args) => {
|
|
356
|
+
const beforeRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
357
|
+
if (beforeRows.some((row) => row.name === args.name))
|
|
358
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-category-duplicate", message: `业务服务分类 ${args.name} 已存在;拒绝重复 POST` });
|
|
359
|
+
// Standard face: category/create {name} (BizServiceCategoryController
|
|
360
|
+
// .java:43-176); the readback re-lists and resolves the new row by name.
|
|
361
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceCategoryCreate, method: "POST", body: { name: args.name } });
|
|
362
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
363
|
+
const created = afterRows.find((row) => row.name === args.name);
|
|
364
|
+
return {
|
|
365
|
+
performed: { errcode: performed.errcode },
|
|
366
|
+
readback: { categoryPresent: created != null, ...(created ? { id: created.id, name: created.name } : {}) },
|
|
367
|
+
};
|
|
368
|
+
}, (args) => {
|
|
369
|
+
if (typeof args.name !== "string" || !args.name.trim())
|
|
370
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-required-field-missing", message: "分类名称 name 是必填项;缺失即拒绝,零 wire", field: "name" });
|
|
371
|
+
});
|
|
372
|
+
writeTool(tools, "service.category.update", B2_LANE, B2_TOOLS["service.category.update"].scope, B2_TOOLS["service.category.update"].description, {
|
|
373
|
+
id: z.string().min(1).describe("分类 id(service.category.list 解析)"),
|
|
374
|
+
name: z.string().min(1).describe("新分类名称(非空,中文≤50)"),
|
|
375
|
+
}, async (session, args) => {
|
|
376
|
+
const beforeRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
377
|
+
const current = beforeRows.find((row) => row.id === args.id);
|
|
378
|
+
if (!current)
|
|
379
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-category-absent", errcode: 501052, message: `业务服务分类 ${args.id} 不存在;拒绝 category/update POST` });
|
|
380
|
+
if (beforeRows.some((row) => row.id !== args.id && row.name === args.name))
|
|
381
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-category-duplicate", errcode: 501004, message: `业务服务分类 ${args.name} 已存在;拒绝 category/update POST` });
|
|
382
|
+
// Standard face: category/update {id,name} (BizServiceCategoryController
|
|
383
|
+
// .java:109-140). A missing id answers the 501052 envelope; a duplicate
|
|
384
|
+
// name answers 501004 (BizServiceCategoryServiceImpl.save:54-63) — both
|
|
385
|
+
// surface verbatim. Readback re-lists and pins the row by id.
|
|
386
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceCategoryUpdate, method: "POST", body: { id: args.id, name: args.name } });
|
|
387
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
388
|
+
const after = afterRows.find((row) => row.id === args.id);
|
|
389
|
+
return {
|
|
390
|
+
performed: { errcode: performed.errcode },
|
|
391
|
+
readback: { categoryPresent: after != null, ...(after ? { id: after.id, name: after.name } : {}) },
|
|
392
|
+
};
|
|
393
|
+
}, (args) => {
|
|
394
|
+
for (const [key, label] of [["id", "分类 id"], ["name", "分类名称 name"]])
|
|
395
|
+
if (typeof args[key] !== "string" || !args[key].trim())
|
|
396
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-required-field-missing", message: `${label} 是必填项;缺失即拒绝,零 wire`, field: key });
|
|
397
|
+
if (args.name.length > 50)
|
|
398
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-name-too-long", message: "分类名称中文长度不能超过 50(服务端同样强制)" });
|
|
399
|
+
});
|
|
400
|
+
writeTool(tools, "service.category.delete", B2_LANE, B2_TOOLS["service.category.delete"].scope, B2_TOOLS["service.category.delete"].description, {
|
|
401
|
+
id: z.string().min(1).describe("分类 id(service.category.list 解析;虚拟「全部」id 为空不可删)"),
|
|
402
|
+
}, async (session, args) => {
|
|
403
|
+
const beforeRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
404
|
+
if (!beforeRows.some((row) => row.id === args.id))
|
|
405
|
+
return { performed: { skipped: "already-absent" }, readback: { absent: true } };
|
|
406
|
+
// Standard face: category/delete?id= (BizServiceCategoryController
|
|
407
|
+
// .java:142-165). A category still holding services answers 501003
|
|
408
|
+
// 业务服务目录下存在集成服务,不可删除 (BizServiceFacadeImpl:196-207) —
|
|
409
|
+
// deletion is BLOCKED, no cascade; the refusal surfaces verbatim.
|
|
410
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceCategoryDelete, method: "POST", query: { id: args.id } });
|
|
411
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceCategoryList })).data);
|
|
412
|
+
const after = afterRows.find((row) => row.id === args.id);
|
|
413
|
+
return {
|
|
414
|
+
performed: { errcode: performed.errcode },
|
|
415
|
+
readback: { absent: after == null, ...(after ? { category: after } : {}) },
|
|
416
|
+
};
|
|
417
|
+
});
|
|
418
|
+
tools.push(defineTool({
|
|
419
|
+
name: "service.dbconnpool.list",
|
|
420
|
+
layer: "low",
|
|
421
|
+
kind: "read",
|
|
422
|
+
lane: B2_LANE,
|
|
423
|
+
scope: B2_TOOLS["service.dbconnpool.list"].scope,
|
|
424
|
+
description: B2_TOOLS["service.dbconnpool.list"].description,
|
|
425
|
+
inputSchema: { environment: environmentArg, capability: capabilityArg },
|
|
426
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
427
|
+
// Standard face: /bizservice/dbconnpool/list. The platform returns
|
|
428
|
+
// the password column unmasked — the TOOL strips it (credentials
|
|
429
|
+
// never echo); only the structural face is read back.
|
|
430
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
431
|
+
return { pools: rows.map((row) => poolReadbackFace(row)), codes: rows.map((row) => row.code) };
|
|
432
|
+
}),
|
|
433
|
+
}));
|
|
434
|
+
writeTool(tools, "service.dbconnpool.create", B2_LANE, B2_TOOLS["service.dbconnpool.create"].scope, B2_TOOLS["service.dbconnpool.create"].description, {
|
|
435
|
+
code: z.string().min(1).describe("连接池编码(唯一;无效 → 502003,重复 → 502002)"),
|
|
436
|
+
name: z.string().min(1).describe("连接池名称(非空,中文≤50;空 → 502004)"),
|
|
437
|
+
databaseType: z.string().min(1).describe("数据库类型(如 MySQL/Oracle/SQLServer;设计器必选)"),
|
|
438
|
+
jdbcUrl: z.string().min(1).describe("JDBC 连接串(服务端白名单校验;Kafka 类型由 externInfo url:port 合成)"),
|
|
439
|
+
username: z.string().optional(),
|
|
440
|
+
password: z.string().optional().describe("连接口令(只上送,读回永不回显)"),
|
|
441
|
+
description: z.string().optional(),
|
|
442
|
+
datasourceType: z.string().optional().describe("DATABASE(缺省)/Kafka"),
|
|
443
|
+
externInfo: z.string().optional().describe("扩展信息 JSON(Kafka 需 {url,port})"),
|
|
444
|
+
}, async (session, args) => {
|
|
445
|
+
// Standard face: /bizservice/dbconnpool/create (BizDatabaseConnection
|
|
446
|
+
// PoolModel body; controller validates the jdbcUrl whitelist, service
|
|
447
|
+
// validates name/code/uniqueness and initializes the datasource).
|
|
448
|
+
// Password RSA boundary (live 504011): the entity decrypts the stored
|
|
449
|
+
// column with its hardcoded keypair — plaintext fails to decrypt and
|
|
450
|
+
// the pool connects with NO password, so the tool always sends the
|
|
451
|
+
// encrypted form (encryptBizDbPoolPassword).
|
|
452
|
+
const performed = await wireCall(session, {
|
|
453
|
+
path: ENDPOINTS.bizDbConnPoolCreate,
|
|
454
|
+
method: "POST",
|
|
455
|
+
body: {
|
|
456
|
+
code: args.code,
|
|
457
|
+
name: args.name,
|
|
458
|
+
databaseType: args.databaseType,
|
|
459
|
+
jdbcUrl: args.jdbcUrl,
|
|
460
|
+
...(args.username !== undefined ? { username: args.username } : {}),
|
|
461
|
+
...(args.password !== undefined ? { password: encryptBizDbPoolPassword(args.password) } : {}),
|
|
462
|
+
...(args.description !== undefined ? { description: args.description } : {}),
|
|
463
|
+
...(args.datasourceType !== undefined ? { datasourceType: args.datasourceType } : {}),
|
|
464
|
+
...(args.externInfo !== undefined ? { externInfo: args.externInfo } : {}),
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
468
|
+
const created = rows.find((row) => row.code === args.code);
|
|
469
|
+
return {
|
|
470
|
+
performed: { errcode: performed.errcode },
|
|
471
|
+
readback: { poolPresent: created != null, ...(created ? poolReadbackFace(created) : {}) },
|
|
472
|
+
};
|
|
473
|
+
}, (args) => {
|
|
474
|
+
for (const [key, label] of [["code", "连接池编码 code"], ["name", "连接池名称 name"], ["databaseType", "数据库类型 databaseType"], ["jdbcUrl", "JDBC 连接串 jdbcUrl"]])
|
|
475
|
+
if (typeof args[key] !== "string" || !args[key].trim())
|
|
476
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-required-field-missing", message: `${label} 是必填项;缺失即拒绝,零 wire`, field: key });
|
|
477
|
+
if (args.datasourceType !== undefined && !["DATABASE", "Kafka"].includes(String(args.datasourceType)))
|
|
478
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-dbconnpool-invalid", message: "datasourceType 只接受 DATABASE(缺省)或 Kafka(8.6.26 服务端分支)" });
|
|
479
|
+
});
|
|
480
|
+
writeTool(tools, "service.dbconnpool.update", B2_LANE, B2_TOOLS["service.dbconnpool.update"].scope, B2_TOOLS["service.dbconnpool.update"].description, {
|
|
481
|
+
code: z.string().min(1).describe("连接池编码(解析行 id 与当前列)"),
|
|
482
|
+
name: z.string().optional(),
|
|
483
|
+
databaseType: z.string().optional(),
|
|
484
|
+
jdbcUrl: z.string().optional(),
|
|
485
|
+
username: z.string().optional(),
|
|
486
|
+
password: z.string().optional().describe("新口令(只上送;缺省回显存量密文保持覆写不抹列)"),
|
|
487
|
+
description: z.string().optional(),
|
|
488
|
+
datasourceType: z.string().optional(),
|
|
489
|
+
externInfo: z.string().optional(),
|
|
490
|
+
}, async (session, args) => {
|
|
491
|
+
// The platform update is a FULL-ROW overwrite
|
|
492
|
+
// (BizDatabaseConnectionPoolServiceImpl.update:135-168 — only
|
|
493
|
+
// createdTime is carried), so the tool echoes every untouched column
|
|
494
|
+
// (including the stored password ciphertext, like the designer) and a
|
|
495
|
+
// name-only update cannot wipe jdbcUrl/username/credentials.
|
|
496
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
497
|
+
const current = rows.find((row) => row.code === args.code);
|
|
498
|
+
if (!current)
|
|
499
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-business", message: `数据库连接池不存在:${args.code}`, errcode: 502001 });
|
|
500
|
+
const currentRecord = current;
|
|
501
|
+
const performed = await wireCall(session, {
|
|
502
|
+
path: ENDPOINTS.bizDbConnPoolUpdate,
|
|
503
|
+
method: "POST",
|
|
504
|
+
body: {
|
|
505
|
+
id: currentRecord.id,
|
|
506
|
+
code: args.code,
|
|
507
|
+
name: args.name ?? currentRecord.name,
|
|
508
|
+
databaseType: args.databaseType ?? currentRecord.databaseType,
|
|
509
|
+
jdbcUrl: args.jdbcUrl ?? currentRecord.jdbcUrl,
|
|
510
|
+
username: args.username !== undefined ? args.username : currentRecord.username ?? "",
|
|
511
|
+
// New passwords ride the RSA boundary; the echoed stored value is
|
|
512
|
+
// already ciphertext (designer parity — the overwrite keeps the
|
|
513
|
+
// credential column intact when the caller does not rotate it).
|
|
514
|
+
password: args.password !== undefined ? encryptBizDbPoolPassword(args.password) : currentRecord.password ?? "",
|
|
515
|
+
description: args.description !== undefined ? args.description : currentRecord.description ?? "",
|
|
516
|
+
datasourceType: args.datasourceType ?? currentRecord.datasourceType ?? "DATABASE",
|
|
517
|
+
externInfo: args.externInfo !== undefined ? args.externInfo : currentRecord.externInfo ?? "",
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
521
|
+
const after = afterRows.find((row) => row.code === args.code);
|
|
522
|
+
return {
|
|
523
|
+
performed: { errcode: performed.errcode },
|
|
524
|
+
readback: { poolPresent: after != null, ...(after ? poolReadbackFace(after) : {}) },
|
|
525
|
+
};
|
|
526
|
+
});
|
|
527
|
+
writeTool(tools, "service.dbconnpool.delete", B2_LANE, B2_TOOLS["service.dbconnpool.delete"].scope, B2_TOOLS["service.dbconnpool.delete"].description, { code: z.string().min(1).describe("连接池编码") }, async (session, args) => {
|
|
528
|
+
// Standard face: /bizservice/dbconnpool/delete?id=. A pool referenced
|
|
529
|
+
// by any service config's dbConnPoolCode answers 502006
|
|
530
|
+
// 数据库连接池已经被业务服务引用 (BizServiceFacadeImpl delete:173-189)
|
|
531
|
+
// — blocked, no cascade; the refusal surfaces verbatim.
|
|
532
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
533
|
+
const current = rows.find((row) => row.code === args.code);
|
|
534
|
+
if (!current)
|
|
535
|
+
return { performed: { errcode: 0 }, readback: { absent: true, note: "pool already absent" } };
|
|
536
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizDbConnPoolDelete, method: "POST", query: { id: String(current.id) } });
|
|
537
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizDbConnPoolList })).data);
|
|
538
|
+
const after = afterRows.find((row) => row.code === args.code);
|
|
539
|
+
return { performed: { errcode: performed.errcode }, readback: { absent: after == null } };
|
|
540
|
+
});
|
|
541
|
+
tools.push(defineTool({
|
|
542
|
+
name: "service.adapter_config",
|
|
543
|
+
layer: "low",
|
|
544
|
+
kind: "read",
|
|
545
|
+
lane: B2_LANE,
|
|
546
|
+
scope: B2_TOOLS["service.adapter_config"].scope,
|
|
547
|
+
description: B2_TOOLS["service.adapter_config"].description,
|
|
548
|
+
inputSchema: { environment: environmentArg, capability: capabilityArg },
|
|
549
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
550
|
+
// Standard face: /bizservice/service/adapter_config answers a JSON
|
|
551
|
+
// STRING of the registered protocol adapters (BizServiceFacadeImpl
|
|
552
|
+
// .java:322-328 JSON.toJSONString(list); each entry
|
|
553
|
+
// {code,name,config:{authList,commonList,serviceActionList,
|
|
554
|
+
// methodActionList}} per AdapterConfigParam.java:16-181). Decoded to
|
|
555
|
+
// the same faces service.register's adapter preflight enforces —
|
|
556
|
+
// one source, no drift. Note: with sqlConfigEnabled the platform
|
|
557
|
+
// REMOVES DATA_SQL/DATA_TABLE from the list (readback verbatim).
|
|
558
|
+
const result = await wireCall(session, { path: ENDPOINTS.bizServiceAdapterConfig });
|
|
559
|
+
let adapters = null;
|
|
560
|
+
if (typeof result.data === "string" && result.data.trim()) {
|
|
561
|
+
try {
|
|
562
|
+
adapters = JSON.parse(result.data);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
adapters = null;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return { adapters, rawPresent: typeof result.data === "string" };
|
|
569
|
+
}),
|
|
570
|
+
}));
|
|
571
|
+
tools.push(defineTool({
|
|
572
|
+
name: "service.table_list",
|
|
573
|
+
layer: "low",
|
|
574
|
+
kind: "read",
|
|
575
|
+
lane: B2_LANE,
|
|
576
|
+
scope: B2_TOOLS["service.table_list"].scope,
|
|
577
|
+
description: B2_TOOLS["service.table_list"].description,
|
|
578
|
+
inputSchema: {
|
|
579
|
+
environment: environmentArg,
|
|
580
|
+
serviceCode: z.string().min(1).describe("DATA_TABLE 业务服务编码"),
|
|
581
|
+
page: z.number().int().min(1).optional(),
|
|
582
|
+
size: z.number().int().min(1).optional(),
|
|
583
|
+
keyword: z.string().optional(),
|
|
584
|
+
capability: capabilityArg,
|
|
585
|
+
},
|
|
586
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
587
|
+
// Standard face: /bizservice/method/get_tablename_list (defaults
|
|
588
|
+
// page=1 / size=MAX when omitted, BizServiceMethodController
|
|
589
|
+
// .java:503-537). DATA_TABLE only — a DATA_SQL service answers 501010
|
|
590
|
+
// 业务服务不为DataTable类型 (evidence correction: the research doc's
|
|
591
|
+
// "DATA_SQL table discovery" reading was wrong).
|
|
592
|
+
const result = await wireCall(session, {
|
|
593
|
+
path: ENDPOINTS.bizServiceTableList,
|
|
594
|
+
query: { serviceCode: args.serviceCode, ...(args.page !== undefined ? { page: String(args.page) } : {}), ...(args.size !== undefined ? { size: String(args.size) } : {}), ...(args.keyword !== undefined ? { keyword: args.keyword } : {}) },
|
|
595
|
+
});
|
|
596
|
+
return { serviceCode: args.serviceCode, tables: result.data };
|
|
597
|
+
}),
|
|
598
|
+
}));
|
|
599
|
+
tools.push(defineTool({
|
|
600
|
+
name: "service.table_sql",
|
|
601
|
+
layer: "low",
|
|
602
|
+
kind: "read",
|
|
603
|
+
lane: B2_LANE,
|
|
604
|
+
scope: B2_TOOLS["service.table_sql"].scope,
|
|
605
|
+
description: B2_TOOLS["service.table_sql"].description,
|
|
606
|
+
inputSchema: { environment: environmentArg, serviceCode: z.string().min(1), tableName: z.string().min(1), capability: capabilityArg },
|
|
607
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
608
|
+
// Standard face: /bizservice/method/get_table_column
|
|
609
|
+
// (BizServiceMethodController.java:538-560). Despite the name it
|
|
610
|
+
// answers GENERATED SQL TEMPLATES (DataTableSqlModel{sql,sqlType}),
|
|
611
|
+
// not column metadata (BizServiceFacadeImpl.getDataTableSql:889-910).
|
|
612
|
+
const result = await wireCall(session, { path: ENDPOINTS.bizServiceTableSql, query: { serviceCode: args.serviceCode, tableName: args.tableName } });
|
|
613
|
+
return { serviceCode: args.serviceCode, tableName: args.tableName, rows: result.data };
|
|
614
|
+
}),
|
|
615
|
+
}));
|
|
616
|
+
writeTool(tools, "service.register", B2_LANE, B2_TOOLS["service.register"].scope, B2_TOOLS["service.register"].description, {
|
|
617
|
+
serviceCode: z.string().min(1),
|
|
618
|
+
name: z.string().min(1).describe("服务名称(设计器必填;非空,中文≤50,服务端同样强制)"),
|
|
619
|
+
adapterCode: z.string().min(1).describe("适配器编码(设计器必填):RESTFUL/DATA_SQL/DATA_TABLE/SAP/KAFKA/SOAP。自定义 Java 方法体(扩展接口)的常规做法是用 RESTFUL 指向扩展端点,不要用 INTERNAL/custom_protocol"),
|
|
620
|
+
serviceCategoryId: z.string().min(1).describe("所属目录 id(设计器必填;用 service.category.list 解析,「全部」是空 id 虚拟项不可用,必要时 service.category.create 新建)"),
|
|
621
|
+
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 只能为空对象或省略"),
|
|
622
|
+
methodName: z.string().optional(),
|
|
623
|
+
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"),
|
|
624
|
+
methodInputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional().describe("方法输入参数声明([{code,name,…}] 数组或 JSON 字符串)→ inputParametersJson;绑定校验按 code 对账;RESTFUL 每参数需 configJson.position"),
|
|
625
|
+
methodOutputParameters: z.union([z.array(z.record(z.string(), z.unknown())), z.string()]).optional().describe("方法输出参数声明 → outputParametersJson"),
|
|
626
|
+
description: z.string().optional(),
|
|
627
|
+
}, async (session, args) => {
|
|
628
|
+
// Live evidence (ticket 35): service/create with an empty adapterCode is
|
|
629
|
+
// refused 500005 业务服务关联适配器编码为空; INTERNAL(6) is the local
|
|
630
|
+
// extension adapter a 二开 registration shell binds to. A structured
|
|
631
|
+
// write (serviceConfig/methodConfig) targets the designer write face and
|
|
632
|
+
// requires a whitelisted adapterCode (ticket 92). Designer-parity
|
|
633
|
+
// requireds (user directive 2026-09-14): name/adapterCode/
|
|
634
|
+
// serviceCategoryId are enforced by the schema AND the preflight, and a
|
|
635
|
+
// whitelisted adapter must carry serviceConfig (+ methodConfig with a
|
|
636
|
+
// methodName).
|
|
637
|
+
const adapterCode = args.adapterCode;
|
|
638
|
+
let existing = null;
|
|
639
|
+
try {
|
|
640
|
+
existing = (await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } })).data;
|
|
641
|
+
}
|
|
642
|
+
catch (error) {
|
|
643
|
+
if (!(error instanceof WireError && error.kind === "business"))
|
|
644
|
+
throw error;
|
|
645
|
+
}
|
|
646
|
+
if (existing)
|
|
647
|
+
throw new ToolRefusalError({ status: "refused", blocker: "duplicate-service", serviceCode: args.serviceCode, message: `业务服务 ${args.serviceCode} 已存在;拒绝重复 service/create POST` });
|
|
648
|
+
if (adapterCode === "SOAP" && (args.methodName !== undefined || args.methodConfig !== undefined))
|
|
649
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-method-config-invalid", message: "SOAP 方法由 WSDL 动态发现,没有固定 methodConfig;请使用平台原生 SOAP refresh/方法发现接口,不伪造方法体" });
|
|
650
|
+
// Ticket 103: the platform never validates serviceCategoryId — refuse a
|
|
651
|
+
// dangling id before any write (read-only resolution precedes this).
|
|
652
|
+
await assertCategoryResolves(session, args.serviceCategoryId);
|
|
653
|
+
const serviceConfigJson = args.serviceConfig !== undefined ? compileServiceConfig(adapterCode, args.serviceConfig) : undefined;
|
|
654
|
+
// Ticket 104: a DATA_SQL/DATA_TABLE binding must reference an existing
|
|
655
|
+
// connection pool before any write (the platform stores it unvalidated).
|
|
656
|
+
if (serviceConfigJson !== undefined && (adapterCode === "DATA_SQL" || adapterCode === "DATA_TABLE")) {
|
|
657
|
+
const poolCode = dbConnPoolCodeOf(args.serviceConfig);
|
|
658
|
+
if (poolCode !== undefined)
|
|
659
|
+
await assertDbConnPoolResolves(session, poolCode);
|
|
660
|
+
}
|
|
661
|
+
const methodConfigPlain = args.methodConfig !== undefined ? compileMethodConfigPlain(adapterCode, args.methodConfig) : undefined;
|
|
662
|
+
const performed = await wireCall(session, {
|
|
663
|
+
path: ENDPOINTS.bizServiceCreate,
|
|
664
|
+
method: "POST",
|
|
665
|
+
body: {
|
|
666
|
+
code: args.serviceCode,
|
|
667
|
+
name: args.name,
|
|
668
|
+
adapterCode,
|
|
669
|
+
description: args.description ?? "",
|
|
670
|
+
serviceCategoryId: args.serviceCategoryId,
|
|
671
|
+
...(serviceConfigJson !== undefined ? { configJson: serviceConfigJson } : {}),
|
|
672
|
+
},
|
|
673
|
+
});
|
|
674
|
+
if (args.methodName) {
|
|
675
|
+
// The parameter face is adapter-checked only when this is a structured
|
|
676
|
+
// write; a plain registration shell keeps the ticket-35 face (INTERNAL).
|
|
677
|
+
const faceAdapter = serviceConfigJson !== undefined || methodConfigPlain !== undefined ? assertBizServiceAdapterWhitelisted(adapterCode) : "INTERNAL";
|
|
678
|
+
const serializeFace = (value, label) => value === undefined ? undefined : compileParameterFace(faceAdapter, typeof value === "string" ? (safeParseArray(value) ?? value) : value, label);
|
|
679
|
+
const inputJson = serializeFace(args.methodInputParameters, "methodInputParameters");
|
|
680
|
+
const outputJson = serializeFace(args.methodOutputParameters, "methodOutputParameters");
|
|
681
|
+
await wireCall(session, {
|
|
682
|
+
path: ENDPOINTS.bizServiceMethodCreate,
|
|
683
|
+
method: "POST",
|
|
684
|
+
body: {
|
|
685
|
+
serviceCode: args.serviceCode, code: args.methodName, name: args.methodName,
|
|
686
|
+
...(methodConfigPlain !== undefined ? { configJson: encryptBizServiceMethodConfigJson(methodConfigPlain) } : {}),
|
|
687
|
+
...(inputJson !== undefined ? { inputParametersJson: inputJson } : {}),
|
|
688
|
+
...(outputJson !== undefined ? { outputParametersJson: outputJson } : {}),
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
const readback = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
693
|
+
const methodRows = readback.data != null
|
|
694
|
+
? wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data)
|
|
695
|
+
: [];
|
|
696
|
+
const registered = args.methodName ? methodRows.some((row) => row.code === args.methodName) : undefined;
|
|
697
|
+
return {
|
|
698
|
+
performed: { errcode: performed.errcode },
|
|
699
|
+
readback: { servicePresent: readback.data != null, ...(registered !== undefined ? { methodPresent: registered, methodCount: methodRows.length } : {}) },
|
|
700
|
+
};
|
|
701
|
+
}, (args) => {
|
|
702
|
+
// Dry-lint (zero wire). `omc call` bypasses zod, so every schema-level
|
|
703
|
+
// required is re-enforced here BEFORE the write permission gate.
|
|
704
|
+
for (const [key, label] of [["name", "服务名称 name"], ["adapterCode", "适配器编码 adapterCode"], ["serviceCategoryId", "所属目录 serviceCategoryId"]])
|
|
705
|
+
if (typeof args[key] !== "string" || !args[key].trim())
|
|
706
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-required-field-missing", message: `${label} 是设计器必填项(designer parity);缺失即拒绝,零 wire。所属目录用 service.category.list 解析、service.category.create 新建`, field: key });
|
|
707
|
+
if (args.name.length > 50)
|
|
708
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-name-too-long", message: "服务名称中文长度不能超过 50(BizServiceController.java:84-112 服务端同样强制)" });
|
|
709
|
+
assertStructuredConfig(args.serviceConfig, "serviceConfig");
|
|
710
|
+
assertStructuredConfig(args.methodConfig, "methodConfig");
|
|
711
|
+
const adapterCode = String(args.adapterCode);
|
|
712
|
+
if (BIZSERVICE_WRITE_ADAPTERS.includes(adapterCode)) {
|
|
713
|
+
// A source-evidenced native adapter is a DESIGNER configuration: the
|
|
714
|
+
// service config is required for every adapter. SOAP deliberately has
|
|
715
|
+
// no methodName/methodConfig here because the platform discovers SOAP
|
|
716
|
+
// methods from WSDL via its native refresh path.
|
|
717
|
+
if (args.serviceConfig === undefined)
|
|
718
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-service-config-invalid", message: `${adapterCode} 服务必须携带结构化 serviceConfig(设计器同款必填)` });
|
|
719
|
+
compileServiceConfig(adapterCode, args.serviceConfig);
|
|
720
|
+
if (args.methodName !== undefined && adapterCode === "SOAP")
|
|
721
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-method-config-invalid", message: "SOAP 方法由 WSDL 动态发现,没有固定 methodConfig;请使用平台原生 SOAP refresh/方法发现接口,不伪造方法体" });
|
|
722
|
+
if (args.methodName !== undefined && args.methodConfig === undefined)
|
|
723
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-method-config-invalid", message: `${adapterCode} 方法必须携带结构化 methodConfig——只有名字/参数面的注册壳不是设计器配置的方法` });
|
|
724
|
+
if (args.methodConfig !== undefined)
|
|
725
|
+
compileMethodConfigPlain(adapterCode, args.methodConfig);
|
|
726
|
+
for (const [key, label] of [["methodInputParameters", "methodInputParameters"], ["methodOutputParameters", "methodOutputParameters"]])
|
|
727
|
+
if (args[key] !== undefined)
|
|
728
|
+
compileParameterFace(adapterCode, typeof args[key] === "string" ? safeParseArray(args[key]) : args[key], label);
|
|
729
|
+
}
|
|
730
|
+
else if (args.serviceConfig !== undefined || args.methodConfig !== undefined) {
|
|
731
|
+
// Structured configs on a non-whitelisted adapter (e.g. INTERNAL) are
|
|
732
|
+
// refused with the support face named (ADR 0001).
|
|
733
|
+
assertBizServiceAdapterWhitelisted(adapterCode);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
writeTool(tools, "service.update", B2_LANE, B2_TOOLS["service.update"].scope, B2_TOOLS["service.update"].description, {
|
|
737
|
+
serviceCode: z.string().min(1),
|
|
738
|
+
name: z.string().optional(),
|
|
739
|
+
serviceConfig: z.record(z.string(), z.unknown()).optional().describe("结构化服务 configJson(明文上送;按服务 adapterCode 做必需键预检,同 service.register)"),
|
|
740
|
+
description: z.string().optional(),
|
|
741
|
+
serviceCategoryId: z.string().optional().describe("服务分类 id(通用透传,不静默丢弃)"),
|
|
742
|
+
shared: z.boolean().optional().describe("共享服务(通用透传)"),
|
|
743
|
+
userIdList: z.array(z.string()).optional().describe("可见用户列表(通用透传)"),
|
|
744
|
+
modifierList: z.array(z.string()).optional().describe("修改者列表(通用透传)"),
|
|
745
|
+
}, async (session, args) => {
|
|
746
|
+
// Ticket 93 — patch an existing service without delete/recreate. The id
|
|
747
|
+
// is resolved via get_by_code first (BizServiceController update path
|
|
748
|
+
// requires it); metadata keys pass through verbatim.
|
|
749
|
+
const found = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
750
|
+
const row = found.data;
|
|
751
|
+
const id = row?.id;
|
|
752
|
+
if (typeof id !== "string" || !id)
|
|
753
|
+
return { performed: { errcode: 500001 }, readback: { absent: true }, summary: `业务服务不存在:${args.serviceCode}` };
|
|
754
|
+
const adapterCode = typeof row.adapterCode === "string" ? row.adapterCode : "";
|
|
755
|
+
// Ticket 103: a NEW serviceCategoryId must resolve before the update
|
|
756
|
+
// write (the platform writes it verbatim without validation).
|
|
757
|
+
if (args.serviceCategoryId !== undefined)
|
|
758
|
+
await assertCategoryResolves(session, args.serviceCategoryId);
|
|
759
|
+
// A structured serviceConfig targets the designer write face: the same
|
|
760
|
+
// adapter whitelist + required-key preflight as register (ticket 92).
|
|
761
|
+
// Refusal here precedes the update WRITE (only the read-only id
|
|
762
|
+
// resolution may have preceded it).
|
|
763
|
+
const serviceConfigJson = args.serviceConfig !== undefined ? compileServiceConfig(assertBizServiceAdapterWhitelisted(adapterCode), args.serviceConfig) : undefined;
|
|
764
|
+
// Ticket 104: a DATA_SQL/DATA_TABLE binding must reference an existing
|
|
765
|
+
// connection pool before the update write.
|
|
766
|
+
if (serviceConfigJson !== undefined && (adapterCode === "DATA_SQL" || adapterCode === "DATA_TABLE")) {
|
|
767
|
+
const poolCode = dbConnPoolCodeOf(args.serviceConfig);
|
|
768
|
+
if (poolCode !== undefined)
|
|
769
|
+
await assertDbConnPoolResolves(session, poolCode);
|
|
770
|
+
}
|
|
771
|
+
// Live incident 2026-09-14 (user-visible): the platform update path is a
|
|
772
|
+
// FULL-ROW overwrite (ModelUtility.toEntity → save/update) — a body that
|
|
773
|
+
// omits configJson/serviceCategoryId wipes them to NULL on the platform
|
|
774
|
+
// even though the caller never asked to change them. Patch semantics are
|
|
775
|
+
// therefore implemented by ECHOING the row's current values for every
|
|
776
|
+
// nullable managed column the caller did not override.
|
|
777
|
+
const echo = (override, current) => (override !== undefined ? override : current ?? null);
|
|
778
|
+
const performed = await wireCall(session, {
|
|
779
|
+
path: ENDPOINTS.bizServiceUpdate,
|
|
780
|
+
method: "POST",
|
|
781
|
+
body: {
|
|
782
|
+
id,
|
|
783
|
+
code: args.serviceCode,
|
|
784
|
+
name: args.name ?? row.name ?? args.serviceCode,
|
|
785
|
+
adapterCode,
|
|
786
|
+
description: args.description ?? (typeof row.description === "string" ? row.description : ""),
|
|
787
|
+
configJson: echo(serviceConfigJson, typeof row.configJson === "string" ? row.configJson : null),
|
|
788
|
+
serviceCategoryId: echo(args.serviceCategoryId, typeof row.serviceCategoryId === "string" ? row.serviceCategoryId : null),
|
|
789
|
+
shared: echo(args.shared, row.shared === true),
|
|
790
|
+
userIdList: echo(args.userIdList, Array.isArray(row.userIdList) ? row.userIdList : []),
|
|
791
|
+
modifierList: echo(args.modifierList, Array.isArray(row.modifierList) ? row.modifierList : []),
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
const readback = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
795
|
+
const after = readback.data;
|
|
796
|
+
const nameMatches = args.name === undefined || after?.name === args.name;
|
|
797
|
+
const configMatches = serviceConfigJson === undefined || (typeof after?.configJson === "string" && after.configJson === serviceConfigJson);
|
|
798
|
+
// Regression guard against the full-row-overwrite wipe: when the caller
|
|
799
|
+
// did NOT touch the config, an existing config must survive verbatim.
|
|
800
|
+
const configPreserved = serviceConfigJson === undefined && typeof row.configJson === "string" ? after?.configJson === row.configJson : undefined;
|
|
801
|
+
return {
|
|
802
|
+
performed: { errcode: performed.errcode },
|
|
803
|
+
readback: { servicePresent: after != null, nameMatches, configMatches, ...(configPreserved !== undefined ? { configPreserved } : {}), ...(adapterCode ? { adapterCode } : {}) },
|
|
804
|
+
};
|
|
805
|
+
}, (args) => {
|
|
806
|
+
// Zero-wire refusal: the raw-string configJson face. `omc call` bypasses
|
|
807
|
+
// zod, so the string form must be refused here too. The adapter
|
|
808
|
+
// whitelist/required-key preflight needs the service row's adapterCode
|
|
809
|
+
// and therefore runs after the read-only id resolution — still before
|
|
810
|
+
// any update write (see the execute body).
|
|
811
|
+
assertStructuredConfig(args.serviceConfig, "serviceConfig");
|
|
812
|
+
});
|
|
813
|
+
writeTool(tools, "service.delete", B2_LANE, B2_TOOLS["service.delete"].scope, B2_TOOLS["service.delete"].description, { serviceCode: z.string().min(1) }, async (session, args) => {
|
|
814
|
+
const found = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
815
|
+
const id = found.data?.id;
|
|
816
|
+
if (typeof id !== "string" || !id)
|
|
817
|
+
return { performed: { errcode: 0 }, readback: { absent: true, note: "service already absent" } };
|
|
818
|
+
const performed = await wireCall(session, { path: ENDPOINTS.bizServiceDelete, method: "POST", query: { id } });
|
|
819
|
+
let absent = false;
|
|
820
|
+
try {
|
|
821
|
+
const readback = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
822
|
+
absent = readback.data == null;
|
|
823
|
+
}
|
|
824
|
+
catch {
|
|
825
|
+
// get_by_code answers 500001 业务服务不存在 when the service is gone.
|
|
826
|
+
absent = true;
|
|
827
|
+
}
|
|
828
|
+
// Ticket 97 audit fix: the service-row absence alone does not prove the
|
|
829
|
+
// METHOD rows are gone (DB-level cascade was never evidenced) — read the
|
|
830
|
+
// method list by serviceCode and require it empty before absent:true.
|
|
831
|
+
let methodsAbsent = false;
|
|
832
|
+
try {
|
|
833
|
+
const methods = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
834
|
+
methodsAbsent = methods.length === 0;
|
|
835
|
+
}
|
|
836
|
+
catch {
|
|
837
|
+
// The list face refuses once the owning service row is gone — the
|
|
838
|
+
// methods are unreachable, which is the observable absence.
|
|
839
|
+
methodsAbsent = true;
|
|
840
|
+
}
|
|
841
|
+
return { performed: { errcode: performed.errcode }, readback: { absent: absent && methodsAbsent, serviceRowAbsent: absent, methodsAbsent } };
|
|
842
|
+
});
|
|
843
|
+
// -------------------------------------------------------------------------
|
|
844
|
+
// Ticket 106 — the IE (import/export) face
|
|
845
|
+
// -------------------------------------------------------------------------
|
|
846
|
+
/**
|
|
847
|
+
* Read + validate a local IE package BEFORE any wire call: a missing file
|
|
848
|
+
* (bizservice-ie-zip-missing) or an off-shape zip/manifest
|
|
849
|
+
* (bizservice-ie-zip-invalid) refuses with zero uploads. The manifest is
|
|
850
|
+
* parsed once here and reused by check (reporting) and import (readback
|
|
851
|
+
* targets).
|
|
852
|
+
*/
|
|
853
|
+
function readIeZipFile(zipPath) {
|
|
854
|
+
if (!existsSync(zipPath))
|
|
855
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-ie-zip-missing", message: `集成服务包不存在:${zipPath}(先用 service.export 导出,或确认路径)`, zipPath });
|
|
856
|
+
let bytes;
|
|
857
|
+
try {
|
|
858
|
+
bytes = readFileSync(zipPath);
|
|
859
|
+
}
|
|
860
|
+
catch (error) {
|
|
861
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-ie-zip-missing", message: `集成服务包不可读:${zipPath}(${error.message})`, zipPath });
|
|
862
|
+
}
|
|
863
|
+
const fail = (reason) => new ToolRefusalError({ status: "refused", blocker: "bizservice-ie-zip-invalid", message: `集成服务包无效:${reason}(需要平台 export 形状的 zip:manifest.json + version.md)`, zipPath });
|
|
864
|
+
let manifestRaw;
|
|
865
|
+
try {
|
|
866
|
+
const entries = readZipEntries(bytes);
|
|
867
|
+
const manifestEntry = zipEntryByBasename(entries, "manifest.json");
|
|
868
|
+
if (manifestEntry === undefined)
|
|
869
|
+
throw new Error("缺少 manifest.json");
|
|
870
|
+
manifestRaw = JSON.parse(manifestEntry.toString("utf8"));
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
throw fail(error instanceof ZipFormatError || error instanceof SyntaxError ? error.message : error.message);
|
|
874
|
+
}
|
|
875
|
+
let manifest;
|
|
876
|
+
try {
|
|
877
|
+
manifest = parseIeManifest(manifestRaw);
|
|
878
|
+
}
|
|
879
|
+
catch (error) {
|
|
880
|
+
throw fail(error.message);
|
|
881
|
+
}
|
|
882
|
+
return { bytes, sha256: createHash("sha256").update(bytes).digest("hex"), manifest };
|
|
883
|
+
}
|
|
884
|
+
/** Upload a fresh copy of the package; every refId is one-shot (platform design). */
|
|
885
|
+
async function uploadIeZip(session, zip, zipPath) {
|
|
886
|
+
return wireUploadFile(session, ENDPOINTS.runtimeQueryUploadFile, basename(zipPath) || "bizservice-package.zip", zip.bytes);
|
|
887
|
+
}
|
|
888
|
+
async function ieImportScope(session, manifest, args) {
|
|
889
|
+
const rename = new Map((args.modifyServiceCodes ?? []).map((entry) => [entry.oldServiceCode, entry.newServiceCode]));
|
|
890
|
+
const scope = {
|
|
891
|
+
services: manifest.services.map((service) => rename.get(service.code) ?? service.code),
|
|
892
|
+
methods: manifest.services.map((service) => ({
|
|
893
|
+
serviceCode: rename.get(service.code) ?? service.code,
|
|
894
|
+
methodCodes: (manifest.methodsByService[service.code] ?? []).map((method) => method.code),
|
|
895
|
+
})),
|
|
896
|
+
};
|
|
897
|
+
if (args.overWrite)
|
|
898
|
+
return scope;
|
|
899
|
+
const conflicts = { services: [], methods: [] };
|
|
900
|
+
for (const service of scope.services) {
|
|
901
|
+
let row;
|
|
902
|
+
try {
|
|
903
|
+
row = (await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: service } })).data;
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 500001)
|
|
907
|
+
row = null;
|
|
908
|
+
else
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
911
|
+
if (!row)
|
|
912
|
+
continue;
|
|
913
|
+
conflicts.services.push(service);
|
|
914
|
+
const methods = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: service, filterFlag: "false" } })).data);
|
|
915
|
+
const packaged = scope.methods.find((entry) => entry.serviceCode === service)?.methodCodes ?? [];
|
|
916
|
+
const methodCodes = methods.map((method) => method.code).filter((code) => typeof code === "string" && packaged.includes(code));
|
|
917
|
+
if (methodCodes.length > 0)
|
|
918
|
+
conflicts.methods.push({ serviceCode: service, methodCodes });
|
|
919
|
+
}
|
|
920
|
+
if (conflicts.services.length > 0 || conflicts.methods.length > 0)
|
|
921
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-import-conflict", message: `导入包与现有业务服务/方法编码冲突;overWrite=false 时拒绝上传与 import`, conflicts });
|
|
922
|
+
return scope;
|
|
923
|
+
}
|
|
924
|
+
tools.push(defineTool({
|
|
925
|
+
name: "service.export",
|
|
926
|
+
layer: "low",
|
|
927
|
+
kind: "read",
|
|
928
|
+
lane: B2_LANE,
|
|
929
|
+
scope: B2_TOOLS["service.export"].scope,
|
|
930
|
+
description: B2_TOOLS["service.export"].description,
|
|
931
|
+
inputSchema: {
|
|
932
|
+
environment: environmentArg,
|
|
933
|
+
serviceCodes: z.array(z.string().min(1)).min(1).describe("要导出的业务集成服务编码列表"),
|
|
934
|
+
capability: capabilityArg,
|
|
935
|
+
},
|
|
936
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
937
|
+
// Fail-closed existence preflight: the platform export SILENTLY SKIPS
|
|
938
|
+
// unknown codes (BizServiceIEFacadeImpl.java:135-148 getByCode null →
|
|
939
|
+
// not added), so an unresolved code would hand back a short package.
|
|
940
|
+
const missing = [];
|
|
941
|
+
for (const code of args.serviceCodes) {
|
|
942
|
+
try {
|
|
943
|
+
const found = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: code } });
|
|
944
|
+
if (found.data == null)
|
|
945
|
+
missing.push(code);
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
missing.push(code);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (missing.length > 0)
|
|
952
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-export-code-unresolved", message: `集成服务编码不存在:${missing.join("、")}——平台导出会静默跳过未知编码,缺码即拒绝(零导出调用)`, serviceCodes: missing });
|
|
953
|
+
const zip = await wireDownloadBuffer(session, { path: ENDPOINTS.bizServiceExport, body: { serviceCodes: args.serviceCodes } });
|
|
954
|
+
mkdirSync(ctx.artifactsDir, { recursive: true });
|
|
955
|
+
const filePath = `${ctx.artifactsDir.replaceAll(/\\/g, "/")}/bizservice-export-${randomUUID().slice(0, 8)}.zip`;
|
|
956
|
+
writeFileSync(filePath, zip);
|
|
957
|
+
// Secret-free package summary. The zip itself keeps the platform
|
|
958
|
+
// shape (service configJson incl. auth values, method configJson in
|
|
959
|
+
// the stored plaintext form) — the import round-trip requires it; the
|
|
960
|
+
// RESPONSE never inlines package contents.
|
|
961
|
+
let packageSummary = null;
|
|
962
|
+
let packageParseError = null;
|
|
963
|
+
try {
|
|
964
|
+
const manifestEntry = zipEntryByBasename(readZipEntries(zip), "manifest.json");
|
|
965
|
+
if (manifestEntry === undefined)
|
|
966
|
+
throw new Error("manifest.json 缺失");
|
|
967
|
+
packageSummary = iePackageSummary(parseIeManifest(JSON.parse(manifestEntry.toString("utf8"))));
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
packageParseError = error.message;
|
|
971
|
+
}
|
|
972
|
+
return {
|
|
973
|
+
serviceCodes: args.serviceCodes,
|
|
974
|
+
artifact: { path: filePath, bytes: zip.length, sha256: createHash("sha256").update(zip).digest("hex") },
|
|
975
|
+
package: packageSummary,
|
|
976
|
+
...(packageParseError !== null ? { packageParseError } : {}),
|
|
977
|
+
};
|
|
978
|
+
}),
|
|
979
|
+
}));
|
|
980
|
+
tools.push(defineTool({
|
|
981
|
+
name: "service.check",
|
|
982
|
+
layer: "low",
|
|
983
|
+
kind: "read",
|
|
984
|
+
lane: B2_LANE,
|
|
985
|
+
scope: B2_TOOLS["service.check"].scope,
|
|
986
|
+
description: B2_TOOLS["service.check"].description,
|
|
987
|
+
inputSchema: {
|
|
988
|
+
environment: environmentArg,
|
|
989
|
+
zipPath: z.string().min(1).describe("本地集成服务包路径(service.export 的 artifact.path)"),
|
|
990
|
+
overWrite: z.boolean().describe("是否覆盖已存在的同码服务"),
|
|
991
|
+
modifyServiceCodes: z.array(z.object({ oldServiceCode: z.string().min(1), newServiceCode: z.string().min(1) })).optional().describe("重码改名映射(overWrite=false 时解冲突;newCode 需匹配 [a-zA-Z][a-zA-Z0-9_]{0,27})"),
|
|
992
|
+
capability: capabilityArg,
|
|
993
|
+
},
|
|
994
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
995
|
+
const zip = readIeZipFile(args.zipPath);
|
|
996
|
+
const refId = await uploadIeZip(session, zip, args.zipPath);
|
|
997
|
+
// Validation-only: check reads the uploaded package and reports
|
|
998
|
+
// repeated codes / renames; it writes nothing (the refId it consumed
|
|
999
|
+
// is its own fresh upload).
|
|
1000
|
+
const result = await wireCall(session, {
|
|
1001
|
+
path: ENDPOINTS.bizServiceCheck,
|
|
1002
|
+
method: "POST",
|
|
1003
|
+
body: {
|
|
1004
|
+
fileName: refId,
|
|
1005
|
+
overWrite: args.overWrite,
|
|
1006
|
+
...(args.modifyServiceCodes !== undefined ? { modifyServiceCodes: args.modifyServiceCodes } : {}),
|
|
1007
|
+
},
|
|
1008
|
+
});
|
|
1009
|
+
return {
|
|
1010
|
+
zip: { path: args.zipPath, bytes: zip.bytes.length, sha256: zip.sha256 },
|
|
1011
|
+
overWrite: args.overWrite,
|
|
1012
|
+
report: result.data,
|
|
1013
|
+
package: iePackageSummary(zip.manifest),
|
|
1014
|
+
};
|
|
1015
|
+
}),
|
|
1016
|
+
}));
|
|
1017
|
+
writeTool(tools, "service.import", B2_LANE, B2_TOOLS["service.import"].scope, B2_TOOLS["service.import"].description, {
|
|
1018
|
+
zipPath: z.string().min(1).describe("本地集成服务包路径(service.export 的 artifact.path)"),
|
|
1019
|
+
overWrite: z.boolean().describe("覆盖已存在的同码服务(true 时忽略重码改名)"),
|
|
1020
|
+
modifyServiceCodes: z.array(z.object({ oldServiceCode: z.string().min(1), newServiceCode: z.string().min(1) })).optional().describe("重码改名映射(overWrite=false 时解冲突)"),
|
|
1021
|
+
groupId: z.string().optional().describe("导入目标分类 id(service.category.list 解析;缺省沿用包内分类)"),
|
|
1022
|
+
}, async (session, args) => {
|
|
1023
|
+
const zip = readIeZipFile(args.zipPath);
|
|
1024
|
+
const overwriteScope = await ieImportScope(session, zip.manifest, args);
|
|
1025
|
+
const refId = await uploadIeZip(session, zip, args.zipPath);
|
|
1026
|
+
const performed = await wireCall(session, {
|
|
1027
|
+
path: ENDPOINTS.bizServiceImport,
|
|
1028
|
+
method: "POST",
|
|
1029
|
+
body: {
|
|
1030
|
+
fileName: refId,
|
|
1031
|
+
overWrite: args.overWrite,
|
|
1032
|
+
...(args.modifyServiceCodes !== undefined ? { modifyServiceCodes: args.modifyServiceCodes } : {}),
|
|
1033
|
+
...(args.groupId !== undefined ? { groupId: args.groupId } : {}),
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
1036
|
+
// Authoritative readback: every manifest service under its FINAL code
|
|
1037
|
+
// (rename applied), with its method face enumerated.
|
|
1038
|
+
const rename = new Map((args.modifyServiceCodes ?? []).map((entry) => [entry.oldServiceCode, entry.newServiceCode]));
|
|
1039
|
+
const services = [];
|
|
1040
|
+
for (const service of zip.manifest.services) {
|
|
1041
|
+
const finalCode = rename.get(service.code) ?? service.code;
|
|
1042
|
+
const entry = { code: finalCode, packagedCode: service.code };
|
|
1043
|
+
try {
|
|
1044
|
+
const found = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: finalCode } });
|
|
1045
|
+
const row = found.data;
|
|
1046
|
+
entry.present = row != null;
|
|
1047
|
+
if (row) {
|
|
1048
|
+
entry.name = row.name;
|
|
1049
|
+
entry.adapterCode = row.adapterCode;
|
|
1050
|
+
}
|
|
1051
|
+
const methods = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: finalCode, filterFlag: "false" } })).data);
|
|
1052
|
+
const packagedMethods = zip.manifest.methodsByService[service.code] ?? [];
|
|
1053
|
+
entry.methods = methods.map((method) => ({ code: method.code, name: method.name }));
|
|
1054
|
+
entry.parameterFaces = packagedMethods.map((packaged) => {
|
|
1055
|
+
const actual = methods.find((method) => method.code === packaged.code);
|
|
1056
|
+
return {
|
|
1057
|
+
methodCode: packaged.code,
|
|
1058
|
+
inputParametersMatch: actual !== undefined && String(actual.inputParametersJson ?? "") === String(packaged.raw.inputParametersJson ?? ""),
|
|
1059
|
+
outputParametersMatch: actual !== undefined && String(actual.outputParametersJson ?? "") === String(packaged.raw.outputParametersJson ?? ""),
|
|
1060
|
+
};
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
catch (error) {
|
|
1064
|
+
throw new ToolRefusalError({
|
|
1065
|
+
status: "refused",
|
|
1066
|
+
blocker: "bizservice-import-readback-failed",
|
|
1067
|
+
message: `导入后读回 ${finalCode} 失败,已完成部分已保留在报告中:${error instanceof Error ? error.message : String(error)}`,
|
|
1068
|
+
completedServices: services,
|
|
1069
|
+
failedService: finalCode,
|
|
1070
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
services.push(entry);
|
|
1074
|
+
}
|
|
1075
|
+
return {
|
|
1076
|
+
performed: { errcode: performed.errcode, overWrite: args.overWrite, ...(args.overWrite ? { overwriteScope } : {}), ...(args.modifyServiceCodes !== undefined ? { modifyServiceCodes: args.modifyServiceCodes } : {}), ...(args.groupId !== undefined ? { groupId: args.groupId } : {}) },
|
|
1077
|
+
readback: { services },
|
|
1078
|
+
};
|
|
1079
|
+
}, (args) => {
|
|
1080
|
+
// Zero-wire rename lint: the platform checks newCode against
|
|
1081
|
+
// [a-zA-Z][a-zA-Z0-9_]{0,27} (BizServiceIEFacadeImpl.checkSingleCodeLegal,
|
|
1082
|
+
// INVALID_NAME 580001) only AFTER the upload; refuse earlier here.
|
|
1083
|
+
for (const entry of args.modifyServiceCodes ?? [])
|
|
1084
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_]{0,27}$/.test(entry.newServiceCode))
|
|
1085
|
+
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 });
|
|
1086
|
+
});
|
|
1087
|
+
// -------------------------------------------------------------------------
|
|
1088
|
+
// Ticket 107 — the designer TEST face
|
|
1089
|
+
// -------------------------------------------------------------------------
|
|
1090
|
+
/** Adapters whose service_test runtimes are source-evidenced (8.6.26). */
|
|
1091
|
+
const TEST_EVIDENCED_ADAPTERS = new Set(["RESTFUL", "DATA_SQL", "DATA_TABLE", "SOAP", "INTERNAL"]);
|
|
1092
|
+
/** Adapters whose method configJson must be non-null to be executable (SOAP methods are WSDL-discovered and carry none). */
|
|
1093
|
+
const TEST_CONFIG_REQUIRED_ADAPTERS = new Set(["RESTFUL", "DATA_SQL", "DATA_TABLE", "SAP", "KAFKA", "INTERNAL"]);
|
|
1094
|
+
writeTool(tools, "method.test", B2_LANE, B2_TOOLS["method.test"].scope, B2_TOOLS["method.test"].description, {
|
|
1095
|
+
serviceCode: z.string().min(1),
|
|
1096
|
+
methodCode: z.string().min(1),
|
|
1097
|
+
testInputParametersMap: z.record(z.string(), z.unknown()).describe("试跑入参:参数 code → JSON 值(BizServiceMethodVO Map<String,Object>;按方法声明参数面校验)"),
|
|
1098
|
+
}, async (session, args) => {
|
|
1099
|
+
// Ticket 107 — a REAL one-shot execution of the configured method
|
|
1100
|
+
// (BizServiceMethodController.java:398-412 → BizServiceFacadeImpl:426-464
|
|
1101
|
+
// → the service's ProtocolAdapterRuntimeService). The chain writes NO
|
|
1102
|
+
// config rows (only async integration logs); the readback below proves
|
|
1103
|
+
// the stored method row survived the test untouched.
|
|
1104
|
+
const service = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
1105
|
+
const serviceRow = service.data;
|
|
1106
|
+
if (serviceRow === null)
|
|
1107
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-business", message: `业务服务不存在:${args.serviceCode}`, errcode: 500001 });
|
|
1108
|
+
const adapterCode = adapterCodeOf(serviceRow);
|
|
1109
|
+
if (!TEST_EVIDENCED_ADAPTERS.has(adapterCode))
|
|
1110
|
+
throw new ToolRefusalError({
|
|
1111
|
+
status: "refused",
|
|
1112
|
+
blocker: "bizservice-test-adapter-not-evidenced",
|
|
1113
|
+
message: `适配器 ${adapterCode || "(空)"} 在设计器测试面(service_test)无运行时证据(8.6.26 已取证:RESTFUL/DATA_SQL/DATA_TABLE/SOAP/INTERNAL)——不猜不伪造,拒绝试跑,零 service_test 调用`,
|
|
1114
|
+
adapterCode,
|
|
1115
|
+
});
|
|
1116
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
1117
|
+
const method = rows.find((row) => row.code === args.methodCode);
|
|
1118
|
+
if (!method)
|
|
1119
|
+
throw new ToolRefusalError({ status: "refused", blocker: "wire-business", message: `业务方法不存在:${args.serviceCode}/${args.methodCode}`, errcode: 500001 });
|
|
1120
|
+
// Registration-shell boundary: a method with no stored config on a
|
|
1121
|
+
// config-carrying adapter has nothing executable to test.
|
|
1122
|
+
if (TEST_CONFIG_REQUIRED_ADAPTERS.has(adapterCode) && (method.configJson === null || method.configJson === undefined || method.configJson === ""))
|
|
1123
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-test-shell", message: `方法 ${args.methodCode} 是注册壳(无 configJson)——不是可执行 BizMethod,试跑拒绝,零 service_test 调用`, methodCode: args.methodCode });
|
|
1124
|
+
// Parameter-face gate (fail closed, zero service_test wire on violation):
|
|
1125
|
+
// every declared required input must be supplied; unknown keys are
|
|
1126
|
+
// refused instead of guessed onto the wire.
|
|
1127
|
+
const face = parseDeclaredParameterCodes(method.inputParametersJson);
|
|
1128
|
+
const supplied = Object.keys(args.testInputParametersMap);
|
|
1129
|
+
const missing = face.required.filter((code) => !(code in args.testInputParametersMap));
|
|
1130
|
+
if (missing.length > 0)
|
|
1131
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-test-param-missing", message: `试跑缺必填入参:${missing.join("、")}(按方法声明参数面 fail-closed,零试跑调用)`, missing });
|
|
1132
|
+
const unknown = supplied.filter((code) => !face.all.includes(code));
|
|
1133
|
+
if (unknown.length > 0)
|
|
1134
|
+
throw new ToolRefusalError({ status: "refused", blocker: "bizservice-test-param-unknown", message: `试跑携带未声明参数:${unknown.join("、")}(方法参数面只有 ${face.all.join("、") || "(空)"};不猜不透传,零试跑调用)`, unknown });
|
|
1135
|
+
const performed = await wireCall(session, {
|
|
1136
|
+
path: ENDPOINTS.bizServiceMethodTest,
|
|
1137
|
+
method: "POST",
|
|
1138
|
+
body: { serviceCode: args.serviceCode, code: args.methodCode, testInputParametersMap: args.testInputParametersMap },
|
|
1139
|
+
});
|
|
1140
|
+
// Zero-config-write proof: re-read the method row and compare the
|
|
1141
|
+
// stored configJson verbatim.
|
|
1142
|
+
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
1143
|
+
const after = afterRows.find((row) => row.code === args.methodCode);
|
|
1144
|
+
const configUnchanged = after !== undefined && String(after.configJson ?? "") === String(method.configJson ?? "");
|
|
1145
|
+
// The platform test answer nests its own success flag
|
|
1146
|
+
// (`{result:{success:false,msg:"..."}}`) while the outer envelope is
|
|
1147
|
+
// errcode 0 — so a business-level test FAILURE would otherwise read as
|
|
1148
|
+
// `ok:true`. Surface it as a first-class `externalSuccess` field (dogfood
|
|
1149
|
+
// R3) so "the call completed" is never confused with "the endpoint
|
|
1150
|
+
// answered successfully". The outer status stays `ok` because the test
|
|
1151
|
+
// itself did run; the field carries the external verdict.
|
|
1152
|
+
const external = (performed.data ?? {});
|
|
1153
|
+
// The platform answers `{success,msg,data}` where `data` is the adapter's
|
|
1154
|
+
// own result. For a same-instance RESTFUL call the HTTP layer can be a
|
|
1155
|
+
// 200 while the target answered a CloudPivot error envelope
|
|
1156
|
+
// (`{errcode:50000,errmsg:"..."}`) — e.g. `corpId can not be blank`. The
|
|
1157
|
+
// platform still sets `success:true` (the call was made), so a business
|
|
1158
|
+
// failure would otherwise read as externalSuccess:true. Surface the
|
|
1159
|
+
// nested business errcode and fold it into the verdict (dogfood round20).
|
|
1160
|
+
const body = external.data;
|
|
1161
|
+
const isEnvelope = body !== null && typeof body === "object" && !Array.isArray(body) && typeof body.errcode === "number";
|
|
1162
|
+
const resultBodyErrcode = isEnvelope ? body.errcode : undefined;
|
|
1163
|
+
const businessOk = resultBodyErrcode === undefined || resultBodyErrcode === 0;
|
|
1164
|
+
const externalSuccess = external.success === true && businessOk;
|
|
1165
|
+
const externalFailure = externalSuccess
|
|
1166
|
+
? {}
|
|
1167
|
+
: {
|
|
1168
|
+
...(typeof external.msg === "string" && external.msg ? { msg: external.msg } : {}),
|
|
1169
|
+
...(resultBodyErrcode === undefined ? {} : { resultBodyErrcode }),
|
|
1170
|
+
...(isEnvelope && typeof body.errmsg === "string" ? { resultBodyMsg: body.errmsg } : {}),
|
|
1171
|
+
note: businessOk
|
|
1172
|
+
? "外层 status:ok 只表示试跑调用完成;被测接口本身失败(externalSuccess:false)——按 message/result 诊断外部资源,不要当作方法可用"
|
|
1173
|
+
: `外层 status:ok 只表示试跑调用完成;被测接口本身回了业务错误 errcode=${resultBodyErrcode}(如同实例自调缺 X-LowCode-Corpid 头会回 50000 corpId can not be blank)——按 errmsg/result 诊断,不要当作方法可用`,
|
|
1174
|
+
};
|
|
1175
|
+
return {
|
|
1176
|
+
performed: { errcode: performed.errcode, adapterCode, ...(adapterCode === "RESTFUL" || adapterCode === "DATA_SQL" || adapterCode === "DATA_TABLE" ? { externalSideEffect: "real" } : {}) },
|
|
1177
|
+
readback: {
|
|
1178
|
+
result: performed.data,
|
|
1179
|
+
configUnchanged,
|
|
1180
|
+
externalSuccess,
|
|
1181
|
+
...(resultBodyErrcode === undefined ? {} : { resultBodyErrcode }),
|
|
1182
|
+
...(externalSuccess ? {} : { externalFailure }),
|
|
1183
|
+
},
|
|
1184
|
+
};
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
//# sourceMappingURL=b2-registration.js.map
|