mcp-multi-bridge 2.1.2 → 2.1.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/bin.js CHANGED
@@ -221,10 +221,15 @@ function textResult(text, isError = false) {
221
221
  *
222
222
  * 这段文字是给模型看的指令:告诉它当前登录态不可用,以及该执行什么命令。
223
223
  * 之所以写这么具体,是因为模型拿不到本文件的上下文,
224
- * 只说「请登录」它不知道登录入口在哪、也不知道要等待人机验证。
224
+ * 只说「请登录」它不知道登录入口在哪、也不知道要等待人机操作。
225
+ *
226
+ * 注意:这里**不描述登录页上有什么**。验证码、密码策略、二次验证之类的差异
227
+ * 属于登录包和页面的事(由 protocol.adapter 与页面自己决定),而且页面本身
228
+ * 就有提示。桥接层写死「需要图形验证码」会让无验证码的系统(如 EBS-Plus)
229
+ * 收到错误指令,模型据此让人去点一个根本不存在的验证码。
225
230
  *
226
231
  * 命令里必须带 --home:同一个客户端可能同时接多套系统,各自一个配置目录。
227
- * 只说 `mes-login` 会让模型登到另一套系统去,那边看起来成功了、
232
+ * 只说 `mcp-multi-login` 会让模型登到另一套系统去,那边看起来成功了、
228
233
  * 这边的 token 却没变,表现是「登录了但还是未登录」,非常难查。
229
234
  *
230
235
  * @param {string} status 状态描述,如「未登录」/「登录已失效(code=613:token 已过期)」
@@ -253,7 +258,7 @@ ${L.loginCommand}${homeArg}${orgArg}
253
258
 
254
259
  执行说明:
255
260
  1. 该命令会启动一个临时登录页并在浏览器中打开
256
- 2. 需要在页面上输入用户名密码,并完成图形点选验证码(由人操作,AI 无法代替)
261
+ 2. 在页面上完成登录。需要填什么(账号密码、图形验证码等)以页面本身的提示为准,由人操作,AI 无法代替
257
262
  3. 登录成功后 token 会自动写入本地文件,本服务**下一次调用即可生效,无需重启**
258
263
  4. 命令会一直运行到登录完成才退出,**请以后台方式运行**(run_in_background),避免阻塞
259
264
  5. \`--home\` 不能省:本服务读的是 \`${CONFIG_DIR}\`,命令指向别的目录会写错位置
@@ -642,6 +647,12 @@ async function main() {
642
647
  });
643
648
 
644
649
  const payload = extractPayload(result);
650
+
651
+ // 失效判定有两个维度:
652
+ // invalidCodes 业务码,绝大多数系统走这条(HTTP 200 + code)
653
+ // invalidHttpStatus 响应体里的 status 字段,给那些把认证失败兜成 HTTP 错误的系统用
654
+ // 两者字段不重叠:业务响应用 code,框架默认错误页用 status,
655
+ // 所以简单的 or 关系不会互相误伤。
645
656
  if (payload && S.invalidCodes.includes(payload.code)) {
646
657
  const why = S.invalidReasons[payload.code] || payload.message || '凭据不可用';
647
658
  console.error(`[bridge] ${S.tenantLabel} ${org.key} 的凭据不可用(code=${payload.code})`);
@@ -650,6 +661,20 @@ async function main() {
650
661
  true
651
662
  );
652
663
  }
664
+ if (payload && S.invalidHttpStatus.includes(payload.status)) {
665
+ const why = S.invalidReasons[payload.status] || 'HTTP ' + payload.status;
666
+ console.error(
667
+ `[bridge] ${S.tenantLabel} ${org.key} 的凭据不可用(HTTP ${payload.status})`
668
+ );
669
+ return textResult(
670
+ loginGuide(
671
+ `${S.systemName}${S.nameGap}登录已失效(HTTP ${payload.status}:${why})`,
672
+ org,
673
+ cfg
674
+ ),
675
+ true
676
+ );
677
+ }
653
678
 
654
679
  // 正常结果:附一条组织说明,让模型知道这份数据属于哪个组织
655
680
  const L = S;
package/lib/higress.js CHANGED
@@ -31,19 +31,50 @@ export async function rpc(cfg, org, method, params) {
31
31
 
32
32
  /**
33
33
  * 从 MCP 工具返回里取出业务 JSON
34
- * 网关可能在正文前拼一段说明文字,需从第一个 { 截取
34
+ *
35
+ * 网关会在正文前拼一段说明文字(responseTemplate.prependBody),业务 JSON 在最后。
36
+ * 不能简单取第一个 `{`:说明文字里可能带花括号,比如描述字段结构时写的
37
+ * 「data 为 [{value, label}] 数组」,那样会把提示文字当成 JSON 解析,失败后
38
+ * 返回 code=-1,把一个正常的业务响应报成异常。
39
+ *
40
+ * 所以从后往前找:JSON 一定在末尾,而提示文字在前。优先试整段,
41
+ * 再按 `{` 位置倒序尝试,命中即止。最多试 MAX_TRIES 次,避免长文本下反复解析。
35
42
  */
43
+ const MAX_TRIES = 12;
44
+
36
45
  export function extractPayload(rpcResult) {
37
46
  const content = rpcResult?.result?.content;
38
47
  if (!Array.isArray(content) || !content.length) {
39
48
  return rpcResult?.result?.isError ? { code: -1, message: '调用失败' } : null;
40
49
  }
41
50
  const text = content[0].text || '';
42
- const start = text.indexOf('{');
43
- if (start === -1) return { code: -1, message: text.trim() || '响应无 JSON 内容' };
44
- try {
45
- return JSON.parse(text.slice(start));
46
- } catch (e) {
47
- return { code: -1, message: text.slice(0, 200) };
51
+
52
+ // 常见情况:整段就是 JSON
53
+ const whole = text.trim();
54
+ if (whole.startsWith('{')) {
55
+ try {
56
+ return JSON.parse(whole);
57
+ } catch (e) {
58
+ /* 继续往下找 */
59
+ }
60
+ }
61
+
62
+ // 从后往前,逐个 `{` 尝试
63
+ let idx = text.length;
64
+ for (let n = 0; n < MAX_TRIES; n++) {
65
+ const start = text.lastIndexOf('{', idx - 1);
66
+ if (start === -1) break;
67
+ try {
68
+ const parsed = JSON.parse(text.slice(start));
69
+ if (parsed && typeof parsed === 'object') return parsed;
70
+ } catch (e) {
71
+ /* 这个位置不是 JSON 起点,继续往前 */
72
+ }
73
+ idx = start;
48
74
  }
75
+
76
+ // 兜底:仍然取第一个 `{`,保持与旧行为一致的错误信息
77
+ const first = text.indexOf('{');
78
+ if (first === -1) return { code: -1, message: text.trim() || '响应无 JSON 内容' };
79
+ return { code: -1, message: text.slice(first, first + 200) };
49
80
  }
package/lib/settings.js CHANGED
@@ -9,6 +9,7 @@
9
9
  * credentialField 注入到工具参数里的凭据字段名 默认 access_token
10
10
  * hiddenArgs 对模型隐藏的参数(凭据与租户 ID 由本层填) 默认 access_token / orgId
11
11
  * invalidCodes 表示登录态失效的业务错误码 默认 613/614/615/616
12
+ * invalidHttpStatus 表示登录态失效的 HTTP 状态码 默认空(不启用)
12
13
  * loginCommand 登录引导里让模型执行的命令 默认 mcp-multi-login
13
14
  *
14
15
  * 全部有默认值,不配置时行为与改造前完全一致。
@@ -27,6 +28,16 @@ export const DEFAULT_TENANT_ID_FIELD = 'orgId';
27
28
  export const DEFAULT_HIDDEN_ARGS = [DEFAULT_CREDENTIAL_FIELD, DEFAULT_TENANT_ID_FIELD];
28
29
  /** 默认的登录失效错误码 */
29
30
  export const DEFAULT_INVALID_CODES = [613, 614, 615, 616];
31
+ /**
32
+ * 默认的登录失效 HTTP 状态码
33
+ *
34
+ * 默认空数组——大多数系统(如 MES)认证失败也返回 HTTP 200 + 业务码,
35
+ * 靠 invalidCodes 判定就够了。默认开某个状态码会误伤正常的业务异常。
36
+ *
37
+ * 只有明确知道「某状态码在此系统上等价于认证失败」时才配。
38
+ * 典型场景:EBS-Plus 的网关把认证失败兜成了 500,拿不到业务码。
39
+ */
40
+ export const DEFAULT_INVALID_HTTP_STATUS = [];
30
41
  /**
31
42
  * 默认登录命令
32
43
  *
@@ -92,7 +103,7 @@ function reasonMap(v) {
92
103
  * @returns {{
93
104
  * systemName:string, tenantLabel:string, nameGap:string,
94
105
  * credentialField:string, hiddenArgs:string[],
95
- * invalidCodes:number[], invalidReasons:Record<number,string>,
106
+ * invalidCodes:number[], invalidHttpStatus:number[], invalidReasons:Record<number,string>,
96
107
  * loginCommand:string
97
108
  * }}
98
109
  */
@@ -119,6 +130,7 @@ export function settings(cfg) {
119
130
  tenantIdField,
120
131
  hiddenArgs,
121
132
  invalidCodes: codeList(cfg?.invalidCodes, DEFAULT_INVALID_CODES),
133
+ invalidHttpStatus: codeList(cfg?.invalidHttpStatus, DEFAULT_INVALID_HTTP_STATUS),
122
134
  invalidReasons: reasonMap(cfg?.invalidCodeReasons),
123
135
  loginCommand: isNonEmptyString(cfg?.loginCommand) ? cfg.loginCommand.trim() : DEFAULT_LOGIN_COMMAND
124
136
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-multi-bridge",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "多后端 MCP 桥接:把网关后多个后端、多个租户的工具集合并成一个 MCP 服务,凭据由登录 CLI 提供",
5
5
  "type": "module",
6
6
  "bin": {