@stalfh233/omc-cli 0.0.0-stage → 0.4.0

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.
Files changed (181) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +304 -3
  3. package/coverage/m1-coverage-manifest-v1.json +1278 -0
  4. package/dist/approval-token.js +102 -0
  5. package/dist/args.js +25 -0
  6. package/dist/artifacts.js +119 -0
  7. package/dist/bench/call-face-eval.js +256 -0
  8. package/dist/bench/context-attribution.js +151 -0
  9. package/dist/bench/discovery-cost-eval.js +230 -0
  10. package/dist/bench/driver.js +81 -0
  11. package/dist/bench/evals.js +236 -0
  12. package/dist/bench/fake-http-server.js +65 -0
  13. package/dist/bench/instrument.js +87 -0
  14. package/dist/bench/intent-face-eval.js +343 -0
  15. package/dist/bench/run.js +166 -0
  16. package/dist/bench/scenario.js +343 -0
  17. package/dist/bench/types.js +76 -0
  18. package/dist/bi-wire.js +41 -0
  19. package/dist/bizservice-config.js +581 -0
  20. package/dist/call.js +153 -0
  21. package/dist/capability-absences.js +23 -0
  22. package/dist/capability-overview.js +492 -0
  23. package/dist/capability-shape.js +154 -0
  24. package/dist/cli-contract.js +70 -0
  25. package/dist/cli-output.js +73 -0
  26. package/dist/cli.js +1418 -0
  27. package/dist/code-rules.js +69 -0
  28. package/dist/command-transport.js +155 -0
  29. package/dist/config-store.js +195 -0
  30. package/dist/context.js +21 -0
  31. package/dist/contract-consistency.js +66 -0
  32. package/dist/contract-resources.js +62 -0
  33. package/dist/coverage-consistency.js +62 -0
  34. package/dist/coverage-registry.js +76 -0
  35. package/dist/coverage.js +130 -0
  36. package/dist/data-list-filter.js +114 -0
  37. package/dist/discovery.js +390 -0
  38. package/dist/endpoints.js +154 -0
  39. package/dist/environment-policy.js +26 -0
  40. package/dist/execution-metadata.js +1092 -0
  41. package/dist/fake/app.js +45 -0
  42. package/dist/fake/b2-registration.js +594 -0
  43. package/dist/fake/businessrule.js +242 -0
  44. package/dist/fake/datarule.js +72 -0
  45. package/dist/fake/dictionary.js +108 -0
  46. package/dist/fake/environment.js +30 -0
  47. package/dist/fake/field.js +167 -0
  48. package/dist/fake/form.js +101 -0
  49. package/dist/fake/index.js +121 -0
  50. package/dist/fake/list-view.js +289 -0
  51. package/dist/fake/model.js +136 -0
  52. package/dist/fake/online-js.js +18 -0
  53. package/dist/fake/report.js +253 -0
  54. package/dist/fake/routes.js +47 -0
  55. package/dist/fake/rule-lifecycle.js +37 -0
  56. package/dist/fake/runtime-data.js +367 -0
  57. package/dist/fake/state.js +67 -0
  58. package/dist/fake/workflow.js +364 -0
  59. package/dist/field-change.js +200 -0
  60. package/dist/field-families.js +896 -0
  61. package/dist/form-layout.js +111 -0
  62. package/dist/form-support.js +821 -0
  63. package/dist/goal-routes.js +468 -0
  64. package/dist/governed-execution.js +87 -0
  65. package/dist/human-summary.js +212 -0
  66. package/dist/identity.js +62 -0
  67. package/dist/intent/baseline.js +57 -0
  68. package/dist/intent/capabilities/bizservice.js +274 -0
  69. package/dist/intent/capabilities/businessrule.js +493 -0
  70. package/dist/intent/capabilities/datarule.js +187 -0
  71. package/dist/intent/capabilities/field.js +545 -0
  72. package/dist/intent/capabilities/form.js +136 -0
  73. package/dist/intent/capabilities/index.js +64 -0
  74. package/dist/intent/capabilities/listview.js +157 -0
  75. package/dist/intent/capabilities/model.js +100 -0
  76. package/dist/intent/capabilities/onlinejs.js +108 -0
  77. package/dist/intent/capabilities/report.js +355 -0
  78. package/dist/intent/capabilities/workflow.js +458 -0
  79. package/dist/intent/capability.js +6 -0
  80. package/dist/intent/cli.js +91 -0
  81. package/dist/intent/compare.js +56 -0
  82. package/dist/intent/compiler.js +79 -0
  83. package/dist/intent/dsl.js +129 -0
  84. package/dist/intent/plan-file.js +63 -0
  85. package/dist/intent/readback.js +65 -0
  86. package/dist/intent/schema.js +158 -0
  87. package/dist/intent/validation.js +30 -0
  88. package/dist/intent/yaml.js +315 -0
  89. package/dist/json-column.js +68 -0
  90. package/dist/lanes/app-contract.js +95 -0
  91. package/dist/lanes/app-coverage.js +16 -0
  92. package/dist/lanes/app.js +174 -0
  93. package/dist/lanes/apply-changes.js +231 -0
  94. package/dist/lanes/b2-registration-contract.js +292 -0
  95. package/dist/lanes/b2-registration-coverage.js +48 -0
  96. package/dist/lanes/b2-registration.js +1187 -0
  97. package/dist/lanes/businessrule-contract.js +232 -0
  98. package/dist/lanes/businessrule-coverage.js +16 -0
  99. package/dist/lanes/businessrule.js +221 -0
  100. package/dist/lanes/contract-support.js +65 -0
  101. package/dist/lanes/coverage-declaration.js +9 -0
  102. package/dist/lanes/datarule-contract.js +155 -0
  103. package/dist/lanes/datarule-coverage.js +19 -0
  104. package/dist/lanes/datarule-protocol.js +308 -0
  105. package/dist/lanes/datarule.js +818 -0
  106. package/dist/lanes/dictionary-contract.js +78 -0
  107. package/dist/lanes/dictionary-coverage.js +24 -0
  108. package/dist/lanes/dictionary.js +235 -0
  109. package/dist/lanes/environment-contract.js +70 -0
  110. package/dist/lanes/environment-coverage.js +14 -0
  111. package/dist/lanes/environment.js +163 -0
  112. package/dist/lanes/field-contract.js +223 -0
  113. package/dist/lanes/field-coverage.js +27 -0
  114. package/dist/lanes/field.js +374 -0
  115. package/dist/lanes/form-contract.js +97 -0
  116. package/dist/lanes/form-coverage.js +16 -0
  117. package/dist/lanes/form.js +185 -0
  118. package/dist/lanes/lane-ids.js +34 -0
  119. package/dist/lanes/list-view-contract.js +175 -0
  120. package/dist/lanes/list-view-coverage.js +20 -0
  121. package/dist/lanes/list-view-shapes.js +1207 -0
  122. package/dist/lanes/list-view.js +578 -0
  123. package/dist/lanes/meta-contract.js +85 -0
  124. package/dist/lanes/meta.js +255 -0
  125. package/dist/lanes/model-contract.js +168 -0
  126. package/dist/lanes/model-coverage.js +20 -0
  127. package/dist/lanes/model.js +986 -0
  128. package/dist/lanes/online-js-contract.js +99 -0
  129. package/dist/lanes/online-js-coverage.js +28 -0
  130. package/dist/lanes/online-js.js +127 -0
  131. package/dist/lanes/report-contract.js +144 -0
  132. package/dist/lanes/report-coverage.js +21 -0
  133. package/dist/lanes/report.js +476 -0
  134. package/dist/lanes/rule-graph.js +1846 -0
  135. package/dist/lanes/rule-lifecycle-contract.js +92 -0
  136. package/dist/lanes/rule-lifecycle-coverage.js +20 -0
  137. package/dist/lanes/rule-lifecycle.js +176 -0
  138. package/dist/lanes/runtime-data-contract.js +264 -0
  139. package/dist/lanes/runtime-data-coverage.js +25 -0
  140. package/dist/lanes/runtime-data.js +1054 -0
  141. package/dist/lanes/workflow-contract.js +223 -0
  142. package/dist/lanes/workflow-coverage.js +40 -0
  143. package/dist/lanes/workflow.js +1813 -0
  144. package/dist/online-js-layout.js +58 -0
  145. package/dist/online-js-source.js +276 -0
  146. package/dist/package-tool.js +51 -0
  147. package/dist/package.js +73 -0
  148. package/dist/plan.js +73 -0
  149. package/dist/read.js +144 -0
  150. package/dist/redact.js +28 -0
  151. package/dist/rule-support.js +587 -0
  152. package/dist/runtime-support.js +134 -0
  153. package/dist/server.js +92 -0
  154. package/dist/session-manager.js +30 -0
  155. package/dist/session.js +149 -0
  156. package/dist/skills.js +112 -0
  157. package/dist/support.js +98 -0
  158. package/dist/tool-types.js +127 -0
  159. package/dist/tools.js +59 -0
  160. package/dist/usage-log.js +197 -0
  161. package/dist/wire.js +287 -0
  162. package/dist/workflow-support.js +99 -0
  163. package/dist/write-lease.js +26 -0
  164. package/dist/write-lock.js +109 -0
  165. package/dist/write.js +176 -0
  166. package/dist/zip.js +156 -0
  167. package/docs/tool-surface-map.md +42 -0
  168. package/package.json +71 -6
  169. package/skills/omc-acceptance-criteria.md +47 -0
  170. package/skills/omc-business-configuration.md +257 -0
  171. package/skills/omc-capabilities.md +197 -0
  172. package/skills/omc-capability-scouting.md +55 -0
  173. package/skills/omc-config-draft-review.md +171 -0
  174. package/skills/omc-five-piece-flow.md +35 -0
  175. package/skills/omc-glossary.md +86 -0
  176. package/skills/omc-refusals.md +110 -0
  177. package/skills/omc-requirement-analysis.md +161 -0
  178. package/skills/omc-requirement-vocabulary.md +51 -0
  179. package/skills/omc-start-here.md +82 -0
  180. package/skills/omc-tool-selection.md +119 -0
  181. 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