@goplus123/core-api 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,158 @@
7
7
  - WS 推送订阅与本地事件分发:`notify(type)`
8
8
  - 统一错误归一化与中间件:`apiError.use(...)`
9
9
 
10
- 入口文件:[`src/index.ts`](file:///d:/working/GX/tsup/packages/api/src/index.ts)
10
+ 入口文件:[`src/index.ts`](./src/index.ts)
11
+
12
+ ---
13
+
14
+ ## 设计方案与思路
15
+
16
+ ### 这是什么
17
+
18
+ `core-api` 的定位是“前端/Node 侧统一 API SDK 装配点(bootstrapper)”:
19
+
20
+ - 对外只暴露少量稳定入口:`initSDK / requestApi / notify / apiError / fetchGuid`
21
+ - 对内把 HTTP / WS / gRPC 三套通信能力收敛到一个统一调用模型(EndpointSpec + UnifiedApiClient)
22
+ - 让业务侧用“service + functionName”或“类型安全的 service client”去调用,而不是直接耦合具体传输协议
23
+
24
+ ### 为什么要这样设计(问题拆解)
25
+
26
+ 这个 SDK 解决的核心矛盾是:同一套业务接口在不同环境、不同阶段可能走不同 transport,同时又希望具备统一的鉴权/日志/错误处理/调试能力。
27
+
28
+ 设计上做了三层解耦:
29
+
30
+ - “接口长什么样”(spec 层:EndpointSpec/EndpointGroup,见 [`src/spec/endpoint.ts`](./src/spec/endpoint.ts))
31
+ - “怎么路由到哪个 transport”(routing 层:registry 构建 + pickEndpoint + fallback,见 [`src/routing/router.ts`](./src/routing/router.ts))
32
+ - “具体怎么发请求”(client 层:HttpClient/WsClient/GrpcClient + UnifiedApiClient,见 [`src/client/*`](./src/client/))
33
+
34
+ 这样做带来的直接收益:
35
+
36
+ - 业务调用方式稳定:接口迁移 transport(例如 ws → grpc)时,调用方不需要全量改造
37
+ - 横切能力可复用:鉴权、日志、错误归一化、灰度、重试等可以在统一链路上实现一次
38
+ - 便于渐进式改造:spec 驱动与 legacy modules 并存,允许逐步把旧调用迁移进 spec
39
+
40
+ ### 关键设计原则(对后续重构/AI 重设计很重要)
41
+
42
+ - 单一装配点:只允许通过 `initSDK()` 构建/覆盖全局状态,避免“到处 new client”导致的状态分裂
43
+ - 显式路由表:所有“能被 requestApi 路由”的能力必须可在 spec 中静态看到(便于审计、自动化生成、AI 重构)
44
+ - transport 无关的调用链:上层只关心 EndpointSpec 与请求参数;transport 细节由 UnifiedApiClient 分发
45
+ - 横切能力可插拔:拦截器链(interceptors)是统一入口,避免散落在各 transport 内部
46
+ - 向后兼容优先:spec 未命中时允许落到 legacy serviceInstances 或 raw WS 兜底(便于迁移期稳定)
47
+
48
+ ---
49
+
50
+ ## 架构分层与职责
51
+
52
+ ### 1) SDK 装配层(initSDK)
53
+
54
+ 入口:[`initSDK`](./src/index.ts#L372-L579)
55
+
56
+ `initSDK` 做的事情可以概括为“装配 + 建联 + 暴露能力”:
57
+
58
+ - 创建三种 transport client(HTTP/WS/gRPC):[`RequestApi`](./src/client/requestApi.ts)
59
+ - 构建路由 registry(从 `apiMap`):[`rebuildEndpointRegistry`](./src/routing/router.ts#L45-L84)
60
+ - 初始化 UnifiedApiClient,并注入 interceptors:[`UnifiedApiClient`](./src/client/unifiedApiClient.ts#L139-L274)
61
+ - 初始化 WS(仅在这里 connect):避免重复建连与多实例订阅
62
+ - 暴露两类 client:
63
+ - spec 驱动的 service clients:`sdk.cms/sdk.admin/sdk.platform`(由 [`createApiServiceClient`](./src/index.ts#L592-L683) 生成)
64
+ - legacy modules:`sdk.auth/sdk.player/...`(见 [`src/modules/*`](./src/modules/))
65
+
66
+ ### 2) Spec 层(接口描述与路由表)
67
+
68
+ spec 的职责是提供“可路由的接口清单”,而不是提供具体实现。
69
+
70
+ - EndpointSpec:一个接口在某一种 transport 下的“可调用描述”
71
+ - EndpointGroup:同一业务接口的多 transport 变体(可指定 defaultTransport)
72
+ - apiMap:按 service 聚合 endpoints,用于构建 registry:[`src/spec/index.ts`](./src/spec/index.ts)
73
+
74
+ 这层的价值在于:它是静态、可读、可生成的“接口真相来源”(source of truth)。
75
+
76
+ ### 3) Routing 层(requestApi 路由与兜底)
77
+
78
+ 路由入口:[`requestApi`](./src/routing/router.ts#L203-L263)
79
+
80
+ 核心逻辑:
81
+
82
+ 1. 组装 key:`<service>:<functionName>`
83
+ 2. 查 registry(由 `apiMap` 构建)
84
+ 3. 根据 `args.transport / specDefaultTransport / initSDK.defaultTransport` 选出 endpoint([`pickEndpoint`](./src/routing/router.ts#L267-L296))
85
+ 4. 交给 `unifiedClient.call(endpoint, requestParam, meta)` 分发到具体 transport
86
+ 5. 若 spec 未命中:
87
+ - 尝试 legacy `serviceInstances[service][functionName]`
88
+ - 再兜底到 raw WS:`rpcClient.call(functionName, service, payload, wsOptions)`
89
+
90
+ ### 4) Client 层(具体 transport + 统一分发)
91
+
92
+ - HttpClient:基于 `fetch`,提供 baseURL 拼接、timeout、header/token 注入、错误结构化:[`src/client/httpClient.ts`](./src/client/httpClient.ts)
93
+ - WsClient:管理连接/心跳/重连/请求响应匹配/notify 分发:[`src/client/wsClient.ts`](./src/client/wsClient.ts)
94
+ - WsRpcClient:在 WsClient 基础上做“业务 envelope 解包 + 错误归一化”:[`src/client/wsRpcClient.ts`](./src/client/wsRpcClient.ts)
95
+ - GrpcClient:基于 ConnectRPC 的 web transport,统一注入 headers/token:[`src/client/grpcClient.ts`](./src/client/grpcClient.ts)
96
+ - UnifiedApiClient:把 EndpointSpec + requestParam 统一分发到 http/ws/grpc,并在入口统一承接 interceptors:[`src/client/unifiedApiClient.ts`](./src/client/unifiedApiClient.ts)
97
+
98
+ ---
99
+
100
+ ## 横切能力(鉴权 / 日志 / 错误)
101
+
102
+ ### 拦截器模型(Interceptors)
103
+
104
+ 定义与组合见:[`src/client/interceptors.ts`](./src/client/interceptors.ts)
105
+
106
+ - `ApiCallContext` 是拦截器的“单次调用上下文”,包含:endpoint / request / headers / meta
107
+ - `createAuthInterceptor`:把 `auth.getToken()` 注入为 `Authorization: Bearer <token>`
108
+ - `createLogInterceptor`:围绕一次 endpoint 调用输出 request/response/biz_error/transport_error
109
+
110
+ 拦截器的设计意图是把“与业务接口无关的能力”统一实现一次,三种 transport 复用同一条链路。
111
+
112
+ ### token/header 注入策略
113
+
114
+ `initSDK({ auth })` 提供的 `getToken/getHeaders` 会被注入到:
115
+
116
+ - HTTP:自动补 `Authorization` 与公共头(调用方显式传入同名 header 时不覆盖)
117
+ - gRPC:统一加 `Authorization: Bearer ...`,并合并 callOptions.headers
118
+ - WS:浏览器限制无法加自定义 header,因此在 WS 层通过 URL/query 或消息体(headerConfig)携带
119
+
120
+ 对应实现入口:
121
+
122
+ - HTTP token/header:[`attachToken`](./src/client/httpClient.ts#L107-L134)
123
+ - gRPC token/header:[`createAuthInterceptor`](./src/client/grpcClient.ts#L203-L229)
124
+ - WS headerConfig:[`WsClient`](./src/client/wsClient.ts#L51-L60)
125
+
126
+ ### 错误归一化(ApiError)
127
+
128
+ 统一错误类型:[`ApiError`](./src/client/unifiedApiClient.ts#L19-L31)
129
+
130
+ 归一化目标:
131
+
132
+ - 让上层(UI/日志/监控)用一致结构处理错误,不必分辨来自 HTTP/WS/gRPC
133
+ - 把错误分为三类:`biz / transport / unknown`,用于更合理的告警与重试策略
134
+
135
+ ---
136
+
137
+ ## 依赖与技术方案清单
138
+
139
+ 以下清单以 `@goplus123/core-api` 的 `package.json` 为准:[`package.json`](./package.json)
140
+
141
+ ### 运行时依赖(dependencies)
142
+
143
+ | 依赖包 | 用途 | 在本仓库的落点 |
144
+ |---|---|---|
145
+ | `@connectrpc/connect` | ConnectRPC 核心:client、interceptor、CallOptions 等类型 | [`src/client/grpcClient.ts`](./src/client/grpcClient.ts), [`src/index.ts`](./src/index.ts) |
146
+ | `@connectrpc/connect-web` | 浏览器侧 transport(Connect/GrpcWeb),通过 fetch 发起 gRPC-web 调用 | [`src/client/grpcClient.ts`](./src/client/grpcClient.ts) |
147
+ | `@bufbuild/protobuf` | protobuf message 创建、schema/descriptor、生成代码运行时 | [`src/client/unifiedApiClient.ts`](./src/client/unifiedApiClient.ts), `src/grpcWeb/**` |
148
+ | `axios` | 预留/历史依赖:当前 `src/` 未直接使用(HTTP 目前基于 fetch 实现) | 目前无引用(可评估是否移除) |
149
+
150
+ ### 开发/构建依赖(devDependencies)
151
+
152
+ | 依赖包 | 用途 |
153
+ |---|---|
154
+ | `tsup` | 打包与产物格式(esm/cjs)+ dts 生成 |
155
+ | `typescript` | 类型检查与 dts 生成基础 |
156
+
157
+ ### 内建浏览器能力(不需要额外依赖)
158
+
159
+ - `fetch`:HTTP transport 的基础能力(并提供超时 AbortController 封装)
160
+ - `WebSocket`:WS transport 的基础能力(心跳/重连/订阅在 WsClient 内实现)
161
+ - `AbortController`:HTTP 超时/取消
11
162
 
12
163
  ---
13
164
 
@@ -37,7 +188,7 @@ packages/api
37
188
 
38
189
  1. `sdk.<service>.<method>(payload, options?)`
39
190
  2. `requestApi({ service, functionName, requestParam, wsOptions/callOptions })`
40
- 3. 路由选择与兜底:[`src/routing/router.ts`](file:///d:/working/GX/tsup/packages/api/src/routing/router.ts)
191
+ 3. 路由选择与兜底:[`src/routing/router.ts`](./src/routing/router.ts)
41
192
  - 先查 spec registry(由 `rebuildEndpointRegistry()` 从 `apiMap` 构建)
42
193
  - 命中后交给 `UnifiedApiClient` 分发到 `grpc/ws/http`
43
194
  - 未命中则尝试 legacy `serviceInstances`
@@ -47,7 +198,7 @@ packages/api
47
198
 
48
199
  - 新增 gRPC endpoint:先确认 `src/grpcWeb/grpc/**` 有对应 service/schema,再在 `src/spec/<service>.ts` 增加 `defineEndpoint`
49
200
  - 给同一方法加多协议:使用 `defineEndpointGroup`,同时提供 `grpc/ws/http` 变体
50
- - 改默认 transport 或自动选择策略:调整 [`pickEndpoint`](file:///d:/working/GX/tsup/packages/api/src/routing/router.ts#L267-L296)
201
+ - 改默认 transport 或自动选择策略:调整 [`pickEndpoint`](./src/routing/router.ts#L267-L296)
51
202
  - 新增全局鉴权/日志:通过 `initSDK({ auth, logger })`,或改 `src/client/interceptors.ts`
52
203
 
53
204
  ---
@@ -107,7 +258,7 @@ const sdk = initSDK({
107
258
  - `sdk.cms / sdk.admin / sdk.platform`:类型安全的 service client(由 spec 生成)
108
259
  - `sdk.apiError / sdk.notify`:全局错误与通知中心(同名导出也可直接使用)
109
260
 
110
- 对应实现:[`initSDK`](file:///d:/working/GX/tsup/packages/api/src/index.ts#L372-L578)
261
+ 对应实现:[`initSDK`](./src/index.ts#L372-L578)
111
262
 
112
263
  ### 2) 直接调用(推荐)
113
264
 
@@ -142,7 +293,7 @@ await requestApi({
142
293
  3. `initSDK({ defaultTransport })`
143
294
  4. `'auto'`(默认):按当前 SDK 初始化状态优先选择 `grpc → ws → http`
144
295
 
145
- 对应逻辑:[`pickEndpoint`](file:///d:/working/GX/tsup/packages/api/src/routing/router.ts#L267-L296)
296
+ 对应逻辑:[`pickEndpoint`](./src/routing/router.ts#L267-L296)
146
297
 
147
298
  ---
148
299
 
@@ -198,12 +349,12 @@ apiError.use((error, ctx, next) => {
198
349
 
199
350
  ## API 参考(对外导出)
200
351
 
201
- 来自:[`src/index.ts`](file:///d:/working/GX/tsup/packages/api/src/index.ts)
352
+ 来自:[`src/index.ts`](./src/index.ts)
202
353
 
203
354
  ### initSDK(config)
204
355
 
205
356
  - 作用:初始化(或重置)SDK 单例,并按配置装配 HTTP/WS/gRPC、路由、拦截器与 service clients
206
- - 入参类型:`RequestApiConfig`(见 [`RequestApiConfig`](file:///d:/working/GX/tsup/packages/api/src/client/requestApi.ts#L7-L36))
357
+ - 入参类型:`RequestApiConfig`(见 [`RequestApiConfig`](./src/client/requestApi.ts#L7-L36))
207
358
  - 返回:`{ api, rpc?, client?, wsNotify?, apiError, notify, ...services }`
208
359
 
209
360
  ### destroySDK()
@@ -239,19 +390,77 @@ apiError.use((error, ctx, next) => {
239
390
 
240
391
  ### fetchGuid()
241
392
 
242
- - 作用:生成一个短 id(playground 用于 WS `protocols`)
393
+ - 作用:生成/读取一个稳定的设备唯一标识(unikey),用于跨请求/跨连接的设备识别
394
+ - 行为:
395
+ - 读取存储:优先从当前 `KeyValueStorage` 读取 `uKey`
396
+ - 解析兼容:支持三种历史值格式(UUID / 32 位 hex / base64(bytes))
397
+ - 缓存写入:若不存在或不可解析,会生成新的 UUID 并写回存储
398
+ - 默认输出:返回 UUID(带 `-`),例如 `xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx`
399
+ - 默认存储策略(见 [`utils/uniqueKey.ts`](./src/utils/uniqueKey.ts)):
400
+ - Web 环境:使用 `localStorage`
401
+ - 非 Web 或 `localStorage` 不可用:使用进程内 memory storage 兜底(保证同进程稳定)
402
+
403
+ ### createFetchGuid(storage, options?)
404
+
405
+ - 作用:基于指定的 `KeyValueStorage` 创建一个 `fetchGuid()` 函数(推荐方式,便于按平台定制)
406
+ - options:
407
+ - `key?: string`:存储 key,默认 `uKey`
408
+ - `format?: 'uuid' | 'hex' | 'base64'`:返回格式,默认 `'uuid'`
409
+ - `prefix?: string`:输出前缀(用于区分来源端,例如 `'pc_' / 'h5_'`),默认 `''`
410
+
411
+ 示例:
412
+
413
+ ```ts
414
+ import { createFetchGuid, webStorage } from '@goplus123/core-api'
415
+
416
+ export const fetchGuidPc = createFetchGuid(webStorage, { format: 'uuid', prefix: 'pc_' })
417
+ export const fetchGuidH5 = createFetchGuid(webStorage, { format: 'hex', prefix: 'h5_' })
418
+ ```
419
+
420
+ ### setUniqueKeyStorage(adapter)
421
+
422
+ - 作用:设置 `fetchGuid()` 的全局存储适配器(用于 App/小程序等非 Web 环境注入 native storage)
423
+ - 建议:尽量在 `initSDK()` 之前调用,避免同一进程内出现“先生成、后换存储”的多份 id
424
+
425
+ 示例(伪代码):
426
+
427
+ ```ts
428
+ import type { KeyValueStorage } from '@goplus123/core-api'
429
+ import { setUniqueKeyStorage } from '@goplus123/core-api'
430
+
431
+ const appStorage: KeyValueStorage = {
432
+ get(key) {
433
+ return NativeStorage.get(key)
434
+ },
435
+ set(key, val) {
436
+ NativeStorage.set(key, val)
437
+ },
438
+ }
439
+
440
+ setUniqueKeyStorage(appStorage)
441
+ ```
442
+
443
+ ### webStorage
444
+
445
+ - 作用:浏览器端 `KeyValueStorage` 适配器,基于 `window.localStorage`
446
+ - 兼容:SSR/Node 下安全返回 `null`;浏览器隐私模式/禁用存储时会吞掉异常(不抛错)
447
+
448
+ ### KeyValueStorage
449
+
450
+ - 作用:unikey 存储抽象(只要求 `get/set` 两个方法)
451
+ - 定义:[`utils/storage.ts`](./src/utils/storage.ts)
243
452
 
244
453
  ---
245
454
 
246
455
  ## spec 驱动的 Service 列表
247
456
 
248
- spec 定义入口:[`spec/index.ts`](file:///d:/working/GX/tsup/packages/api/src/spec/index.ts)
457
+ spec 定义入口:[`spec/index.ts`](./src/spec/index.ts)
249
458
 
250
459
  按 service 拆分的落点文件:
251
460
 
252
- - cms:[`spec/cms.ts`](file:///d:/working/GX/tsup/packages/api/src/spec/cms.ts)
253
- - admin:[`spec/admin.ts`](file:///d:/working/GX/tsup/packages/api/src/spec/admin.ts)
254
- - platform:[`spec/platform.ts`](file:///d:/working/GX/tsup/packages/api/src/spec/platform.ts)
461
+ - cms:[`spec/cms.ts`](./src/spec/cms.ts)
462
+ - admin:[`spec/admin.ts`](./src/spec/admin.ts)
463
+ - platform:[`spec/platform.ts`](./src/spec/platform.ts)
255
464
 
256
465
  ### cms(gRPC 为主,部分接口可多协议)
257
466
 
@@ -291,22 +500,22 @@ spec 定义入口:[`spec/index.ts`](file:///d:/working/GX/tsup/packages/api/sr
291
500
 
292
501
  ## 为接口新增/扩展 transport(spec 写法)
293
502
 
294
- spec 工具位于:[`spec/endpoint.ts`](file:///d:/working/GX/tsup/packages/api/src/spec/endpoint.ts)
503
+ spec 工具位于:[`spec/endpoint.ts`](./src/spec/endpoint.ts)
295
504
 
296
505
  ## 开发指南:如何补充 spec 路由
297
506
 
298
507
  spec 的职责是给 `requestApi` 提供“路由表”:
299
508
 
300
- - runtime 上它会被加载进 `apiMap`,并在 `initSDK()` 时构建 registry(见 [`rebuildEndpointRegistry`](file:///d:/working/GX/tsup/packages/api/src/routing/router.ts#L84-L132))
509
+ - runtime 上它会被加载进 `apiMap`,并在 `initSDK()` 时构建 registry(见 [`rebuildEndpointRegistry`](./src/routing/router.ts#L84-L132))
301
510
  - 调用方可以通过 `sdk.<service>.<method>()` 或 `requestApi({ service, functionName })` 走到对应 transport
302
511
  - 当调用方显式指定 `transport` 时,如果 spec 中没有对应变体,会在路由选择阶段同步抛错
303
512
 
304
513
  ### 1) 选择落点文件
305
514
 
306
- - cms 的 spec:[`src/spec/cms.ts`](file:///d:/working/GX/tsup/packages/api/src/spec/cms.ts)
307
- - admin 的 spec:[`src/spec/admin.ts`](file:///d:/working/GX/tsup/packages/api/src/spec/admin.ts)
308
- - platform(WS)spec:[`src/spec/platform.ts`](file:///d:/working/GX/tsup/packages/api/src/spec/platform.ts)
309
- - 聚合与 apiMap:[`src/spec/index.ts`](file:///d:/working/GX/tsup/packages/api/src/spec/index.ts)
515
+ - cms 的 spec:[`src/spec/cms.ts`](./src/spec/cms.ts)
516
+ - admin 的 spec:[`src/spec/admin.ts`](./src/spec/admin.ts)
517
+ - platform(WS)spec:[`src/spec/platform.ts`](./src/spec/platform.ts)
518
+ - 聚合与 apiMap:[`src/spec/index.ts`](./src/spec/index.ts)
310
519
 
311
520
  ### 2) 约定:id / service / functionName
312
521
 
@@ -432,7 +641,7 @@ await requestApi({
432
641
  3. 在 `src/index.ts` 的 `initSDK` 返回类型里补充 `foo?: ApiServiceClient<'foo'>`
433
642
  4. 在 `initSDK` 里按 transport 条件创建 `sdk.foo = createApiServiceClient('foo')`
434
643
 
435
- `createApiServiceClient` 的实现位置:[`createApiServiceClient`](file:///d:/working/GX/tsup/packages/api/src/index.ts#L592-L683)
644
+ `createApiServiceClient` 的实现位置:[`createApiServiceClient`](./src/index.ts#L592-L683)
436
645
 
437
646
  ### 单协议(defineEndpoint)
438
647
 
@@ -460,3 +669,39 @@ export const demo = defineEndpointGroup({
460
669
  },
461
670
  })
462
671
  ```
672
+
673
+ ---
674
+
675
+ ## 扩展与改造思路(给后续技术升级/AI 重设计用)
676
+
677
+ ### 推荐的扩展入口(优先级从高到低)
678
+
679
+ 1. **扩接口/补路由**:新增或调整 `src/spec/*`,保持 `id` 与 `service/functionName` 约定一致(最安全、最不影响业务)
680
+ 2. **扩横切能力**:在 `initSDK` 注入 interceptors(或扩展内置 interceptors),实现鉴权/日志/重试/缓存/灰度/追踪
681
+ 3. **扩 transport 能力**:增强 HttpClient/WsClient/GrpcClient(例如:统一重试、断路器、requestId 透传、metrics)
682
+ 4. **迁移 legacy modules → spec**:逐个把 `src/modules/*` 中的接口补到 spec,并在业务侧切到 `requestApi`/service client
683
+
684
+ ### 适合做技术改造的“稳定边界”
685
+
686
+ 如果未来要用 AI 或进行架构升级,建议把以下内容视为“对外契约”,优先保持兼容:
687
+
688
+ - `initSDK(config)` 的输入语义:装配 transport、注入 auth/logger、建立 WS 连接、构建 registry、暴露 service clients
689
+ - `requestApi(args)` 的语义:按 spec 路由 + fallback(spec → legacy → raw WS)
690
+ - `EndpointSpec/EndpointGroup` 的数据结构:能静态表达“接口可调用方式”
691
+ - `ApiError` 的归一化结构:上层可用同一结构做提示/告警/埋点
692
+
693
+ 在不破坏上述契约的前提下,内部实现可以替换,例如:
694
+
695
+ - 把 WS 实现从当前协议替换为新协议,只要 `WsRpcClient.call` 语义不变
696
+ - 把 gRPC transport 从 grpc-web 切到 connect,只要 `GrpcClient.getService().method(req, opt)` 语义可映射
697
+ - 把路由策略从“静态 registry + pickEndpoint”升级为“策略引擎”,只要输入输出一致
698
+
699
+ ### 常见改造需求与落点建议
700
+
701
+ | 需求 | 推荐落点 | 原因 |
702
+ |---|---|---|
703
+ | 全局请求链路追踪(traceId/spanId) | interceptor + auth.getHeaders | 不侵入每个接口定义,可统一注入 |
704
+ | 统一重试/退避 | interceptor(基于 isTransportError) | 在统一入口实现一次,三 transport 复用 |
705
+ | gRPC cookie/credentials 支持 | GrpcClient transport 配置 | 属于 transport 细节,不应泄漏到业务侧 |
706
+ | WS 请求响应匹配策略调整 | WsClient/WsRpcClient | 直接影响 requestId、responseFunctionName 等行为 |
707
+ | 业务错误码映射为用户提示 | normalizeApiError / onApiError | 保持 transport 无关,方便统一处理 |