dsh-my-config 1.8.0 → 1.8.1

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 (3) hide show
  1. package/README.md +5 -0
  2. package/index.js +58 -24
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -12,6 +12,11 @@ DeepSeek Harness 插件:在 Web 侧边栏提供「我的技能 / 我的连接
12
12
 
13
13
  ## 更新日志
14
14
 
15
+ ### v1.8.1
16
+
17
+ - **修复**:streamable-http「测试连接」适配 stateful 服务端——带回 `mcp-session-id`(TS SDK 服务端强校验,缺失返回 400);响应支持 gzip / deflate / br 解码(部分 CDN 强制压缩导致解析失败);initialize 协议版本不匹配时自动降级重试。
18
+ - **修复**:Windows 下 stdio 探测无法启动 `npx` / `uvx` 等 `.cmd` shim 的问题(改经 cmd.exe 启动,与内核 MCP SDK 行为一致)。
19
+
15
20
  ### v1.8.0
16
21
 
17
22
  - **技能**:编辑器支持 license / compatibility / metadata / allowed-tools 全字段(对齐 agentskills.io 规范);保存与导入时执行规范校验,错误逐字段定位展示。
package/index.js CHANGED
@@ -13,7 +13,7 @@
13
13
  import { promises as fs } from 'node:fs';
14
14
  import os from 'node:os';
15
15
  import { join, resolve, sep, basename } from 'node:path';
16
- import { inflateRawSync } from 'node:zlib';
16
+ import { inflateRawSync, gunzipSync, inflateSync, brotliDecompressSync } from 'node:zlib';
17
17
  import { spawn } from 'node:child_process';
18
18
  import http from 'node:http';
19
19
  import https from 'node:https';
@@ -635,8 +635,12 @@ function normalizeToolList(list) {
635
635
  })).filter((t) => t.name);
636
636
  }
637
637
 
638
- /** streamable-http 单次 JSON-RPC POST;兼容 text/event-stream 响应。 */
639
- function httpJsonRpcPost(urlString, headers, payload, timeoutMs) {
638
+ /**
639
+ * streamable-http 单次 JSON-RPC POST;兼容 text/event-stream 与 gzip/deflate 响应。
640
+ * 返回 { msg, sessionId }:msg 为解析后的 JSON-RPC 消息;sessionId 取响应 mcp-session-id
641
+ * 头(stateful 服务端在 initialize 后要求后续请求带回,缺失会被 400 拒绝)。
642
+ */
643
+ function httpJsonRpcPost(urlString, headers, payload, timeoutMs, sessionId) {
640
644
  return new Promise((resolvePromise, rejectPromise) => {
641
645
  let target;
642
646
  try { target = new URL(urlString); } catch { rejectPromise(new Error('url 不是合法地址')); return; }
@@ -647,6 +651,8 @@ function httpJsonRpcPost(urlString, headers, payload, timeoutMs) {
647
651
  headers: {
648
652
  'content-type': 'application/json',
649
653
  accept: 'application/json, text/event-stream',
654
+ 'accept-encoding': 'identity', // 明示不压缩;部分 CDN 仍强推 gzip,响应侧再做兼容解码
655
+ ...(sessionId ? { 'mcp-session-id': sessionId } : {}),
650
656
  ...(headers && typeof headers === 'object' ? headers : {}),
651
657
  'content-length': Buffer.byteLength(body),
652
658
  },
@@ -655,20 +661,36 @@ function httpJsonRpcPost(urlString, headers, payload, timeoutMs) {
655
661
  const chunks = [];
656
662
  res.on('data', (chunk) => chunks.push(chunk));
657
663
  res.on('end', () => {
658
- const text = Buffer.concat(chunks).toString('utf8');
664
+ let raw = Buffer.concat(chunks);
665
+ try {
666
+ const enc = String(res.headers['content-encoding'] ?? '').toLowerCase();
667
+ if (enc === 'gzip') raw = gunzipSync(raw);
668
+ else if (enc === 'deflate') raw = inflateSync(raw);
669
+ else if (enc === 'br') raw = brotliDecompressSync(raw);
670
+ } catch (err) {
671
+ rejectPromise(new Error(`响应压缩解码失败(${String(res.headers['content-encoding'])}): ${err?.message ?? err}`));
672
+ return;
673
+ }
674
+ const text = raw.toString('utf8');
659
675
  if (res.statusCode < 200 || res.statusCode >= 300) {
660
676
  rejectPromise(new Error(`HTTP ${res.statusCode}: ${text.slice(0, 200)}`));
661
677
  return;
662
678
  }
679
+ const nextSid = res.headers['mcp-session-id'] ?? sessionId;
663
680
  try {
664
681
  if (String(res.headers['content-type'] ?? '').includes('text/event-stream')) {
665
- const dataLines = text.split(/\r?\n/).filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim());
666
- resolvePromise(JSON.parse(dataLines[dataLines.length - 1] ?? ''));
682
+ const dataLines = text.split(/\r?\n/).filter((l) => l.startsWith('data:')).map((l) => l.slice(5).trim()).filter(Boolean);
683
+ let msg;
684
+ for (let i = dataLines.length - 1; i >= 0; i--) {
685
+ try { msg = JSON.parse(dataLines[i]); break; } catch { /* 继续往前找 */ }
686
+ }
687
+ if (msg === undefined) throw new Error('no-json');
688
+ resolvePromise({ msg, sessionId: nextSid });
667
689
  } else {
668
- resolvePromise(JSON.parse(text));
690
+ resolvePromise({ msg: JSON.parse(text), sessionId: nextSid });
669
691
  }
670
692
  } catch {
671
- rejectPromise(new Error('响应不是合法 JSON'));
693
+ rejectPromise(new Error(`响应不是合法 JSON:${text.slice(0, 120)}`));
672
694
  }
673
695
  });
674
696
  });
@@ -683,24 +705,32 @@ function jsonRpcErrorMessage(msg) {
683
705
  return msg?.error?.message ? String(msg.error.message) : 'JSON-RPC 返回错误';
684
706
  }
685
707
 
686
- /** streamable-http 探测:initialize → notifications/initialized → tools/list。 */
708
+ /** streamable-http 探测:initialize → notifications/initialized → tools/list。
709
+ * 带回 mcp-session-id(stateful 服务端强校验);initialize 报协议版本错误时降级重试旧版本。 */
687
710
  async function probeHttpConnector(record, timeoutMs) {
688
- const initMsg = {
689
- jsonrpc: '2.0', id: 1, method: 'initialize',
690
- params: {
691
- protocolVersion: '2025-06-18',
692
- capabilities: {},
693
- clientInfo: { name: 'dsh-manager-center', version: '1.8.0' },
694
- },
711
+ const tryInit = async (protocolVersion) => {
712
+ const init = await httpJsonRpcPost(record.url, record.headers, {
713
+ jsonrpc: '2.0', id: 1, method: 'initialize',
714
+ params: {
715
+ protocolVersion,
716
+ capabilities: {},
717
+ clientInfo: { name: 'dsh-manager-center', version: '1.8.1' },
718
+ },
719
+ }, timeoutMs);
720
+ return init;
695
721
  };
696
- const init = await httpJsonRpcPost(record.url, record.headers, initMsg, timeoutMs);
697
- if (init?.error) throw new Error(jsonRpcErrorMessage(init));
722
+ let init = await tryInit('2025-06-18');
723
+ if (init.msg?.error && /protocol/i.test(String(init.msg.error.message ?? ''))) {
724
+ init = await tryInit('2024-11-05');
725
+ }
726
+ if (init.msg?.error) throw new Error(jsonRpcErrorMessage(init.msg));
727
+ const sessionId = init.sessionId; // stateful 服务端在 initialize 响应头发 mcp-session-id
698
728
  try {
699
- await httpJsonRpcPost(record.url, record.headers, { jsonrpc: '2.0', method: 'notifications/initialized' }, timeoutMs);
729
+ await httpJsonRpcPost(record.url, record.headers, { jsonrpc: '2.0', method: 'notifications/initialized' }, timeoutMs, sessionId);
700
730
  } catch { /* initialized 通知失败不阻断(部分服务端不要求) */ }
701
- const list = await httpJsonRpcPost(record.url, record.headers, { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }, timeoutMs);
702
- if (list?.error) throw new Error(jsonRpcErrorMessage(list));
703
- return normalizeToolList(list?.result?.tools);
731
+ const list = await httpJsonRpcPost(record.url, record.headers, { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }, timeoutMs, sessionId);
732
+ if (list.msg?.error) throw new Error(jsonRpcErrorMessage(list.msg));
733
+ return normalizeToolList(list.msg?.result?.tools);
704
734
  }
705
735
 
706
736
  /**
@@ -761,13 +791,17 @@ function probeStdioTransport(child, timeoutMs) {
761
791
  });
762
792
  }
763
793
 
764
- /** stdio 连接器探测:spawn 子进程(env 合并 process.env),再走统一传输层。绝不同步抛出。 */
794
+ /** stdio 连接器探测:spawn 子进程(env 合并 process.env),再走统一传输层。绝不同步抛出。
795
+ * Windows 兼容:npx/uvx 等是 .cmd 脚本 shim,CreateProcess 无法直接执行,
796
+ * 无扩展名命令统一经 shell(cmd.exe /d /s /c)启动,与内核 MCP SDK 行为一致。 */
765
797
  function probeStdioConnector(record, timeoutMs) {
766
798
  let child;
767
799
  try {
768
- child = spawn(record.command, Array.isArray(record.args) ? record.args : [], {
800
+ const command = String(record.command ?? '');
801
+ child = spawn(command, Array.isArray(record.args) ? record.args : [], {
769
802
  env: { ...process.env, ...(record.env && typeof record.env === 'object' ? record.env : {}) },
770
803
  ...(record.cwd ? { cwd: record.cwd } : {}),
804
+ ...(process.platform === 'win32' && !/\.[a-z0-9]+$/i.test(command) ? { shell: true } : {}),
771
805
  stdio: ['pipe', 'pipe', 'pipe'],
772
806
  });
773
807
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-my-config",
3
- "version": "1.8.0",
3
+ "version": "1.8.1",
4
4
  "description": "My Config (我的配置): manage skills (with external skill markets and zip import), MCP connectors and agent experts from dedicated Web sidebar panels.",
5
5
  "type": "module",
6
6
  "main": "index.js",