@wfm-cn/dsh-mcp-connectors 0.2.1 → 0.2.3

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
@@ -21,6 +21,8 @@ dsh-mcp-connectors
21
21
 
22
22
  依赖:Node.js ≥ 18,微信功能需要最新版微信并开通「微信 ClawBot」插件。
23
23
 
24
+ 支持的平台:macOS / Linux / Windows(stdio 子进程在 Windows 上经 shell 启动 `npx.cmd`)。
25
+
24
26
  ## 使用
25
27
 
26
28
  - 侧边栏「连接器」面板 → 「+ 添加连接器」→ 选择类型
package/client.js CHANGED
@@ -226,7 +226,7 @@ window.__ModuleLoader__.load({
226
226
  const wsName = (s) => {
227
227
  const cwd = String(s.cwd ?? '');
228
228
  if (!cwd) return s.live ? '(活跃)' : '';
229
- const parts = cwd.split('/').filter(Boolean);
229
+ const parts = cwd.split(/[\\/]/).filter(Boolean);
230
230
  return parts[parts.length - 1] ?? cwd;
231
231
  };
232
232
  const label = (s) => s.title || s.id;
package/index.js CHANGED
@@ -231,9 +231,20 @@ export async function apply(ctx, config = {}) {
231
231
  function stdioClient(c) {
232
232
  let cl = STDIO_CLIENTS.get(c.serverName);
233
233
  if (cl) return cl;
234
- const child = spawn(c.command, c.args ?? [], {
234
+ // Windows 上 npx/pnpm 等命令是 .cmd shim,必须经 shell 才能启动;
235
+ // macOS/Linux 不加 shell,避免参数被 shell 二次解析
236
+ const isWin = process.platform === 'win32';
237
+ let command = c.command;
238
+ if (isWin) {
239
+ const s = String(command).trim();
240
+ if (/^(npx|npm|pnpm|yarn|bunx?|uvx?|python3?|node)$/i.test(s)) command = `${s}.cmd`;
241
+ else if (/[\\/ ]/.test(s) && !/\.(cmd|exe|bat|ps1)$/i.test(s)) command = s; // 完整路径交给 shell 解析
242
+ }
243
+ const child = spawn(command, c.args ?? [], {
235
244
  env: { ...process.env, ...(c.env ?? {}) },
236
245
  stdio: ['pipe', 'pipe', 'pipe'],
246
+ shell: isWin,
247
+ windowsHide: true,
237
248
  });
238
249
  child.on('exit', () => STDIO_CLIENTS.delete(c.serverName));
239
250
  const pending = new Map();
@@ -700,30 +711,59 @@ export async function apply(ctx, config = {}) {
700
711
  for (const [sn, w] of want) {
701
712
  if (!MOUNTED.has(sn)) await mount(w);
702
713
  }
714
+ // 工具集变化后重新计算各会话的门控(deny 依赖已注册的工具名)
715
+ reapplyGates();
703
716
  };
704
717
 
705
718
  /* ---------------- 每会话工具门控 ---------------- */
706
719
 
707
- const RESTRICTORS = new Map(); // agentId -> dispose
720
+ const RESTRICTORS = new Map(); // agentId -> disposer
721
+ const GATE_PENDING = new Set(); // 因工具尚未注册而待重试的 agentId
722
+
723
+ /** 当前已挂载连接器的公开工具名(含 mcp__server__ 前缀)。 */
724
+ function mountedToolNamesByServer() {
725
+ const out = new Map(); // serverName -> string[]
726
+ for (const [serverName, rec] of MOUNTED) {
727
+ const names = (rec.tools ?? []).map((raw) => publicToolName(serverName, raw));
728
+ if (names.length) out.set(serverName, names);
729
+ }
730
+ return out;
731
+ }
732
+
733
+ /** 对单个 agent 应用会话级门控:禁用连接器的工具全部 deny。 */
708
734
  const applyGate = (agent) => {
709
735
  const agentCtx = agent?.ctx;
710
736
  if (!agentCtx?.tools?.restrict) return;
711
737
  const sessionId = String(agent.session?.id ?? agent.id ?? '');
712
- try { RESTRICTORS.get(agent.id)?.(); } catch { /* ignore */ }
713
738
  const allowed = enabledServersOf(currentConfig(), sessionId);
739
+ const byServer = mountedToolNamesByServer();
740
+ // 还没挂载完(工具尚未注册)时调用 restrict 会因 unknown name 抛错:留到挂载后再试
741
+ const deny = [];
742
+ for (const [serverName, names] of byServer) {
743
+ if (!allowed.has(serverName)) deny.push(...names);
744
+ }
745
+ try { RESTRICTORS.get(agent.id)?.(); } catch { /* ignore */ }
746
+ RESTRICTORS.delete(agent.id);
747
+ if (deny.length === 0) { GATE_PENDING.delete(agent.id); return; }
714
748
  try {
715
- const dispose = agentCtx.tools.restrict((tool) => {
716
- const name = typeof tool === 'string' ? tool : tool?.name ?? tool?.definition?.name;
717
- if (typeof name !== 'string') return true;
718
- const m = /^mcp__([A-Za-z0-9_-]+)__/.exec(name);
719
- return m === null || allowed.has(m[1]);
720
- });
749
+ const dispose = agentCtx.tools.restrict({ deny });
721
750
  RESTRICTORS.set(agent.id, dispose);
751
+ GATE_PENDING.delete(agent.id);
722
752
  } catch (e) {
723
- warn(`会话 ${sessionId} 工具门控失败:`, e?.message ?? e);
753
+ // 工具名尚未生效(挂载中):记下,稍后重试
754
+ if (/unknown global tool/i.test(String(e?.message ?? ''))) {
755
+ GATE_PENDING.add(agent.id);
756
+ } else {
757
+ warn(`会话 ${sessionId} 工具门控失败:`, e?.message ?? e);
758
+ }
724
759
  }
725
760
  };
726
761
 
762
+ /** 挂载/卸载后重新应用所有 agent 的门控(门控依赖已注册的工具名)。 */
763
+ const reapplyGates = () => {
764
+ for (const agent of AGENTS?.list?.() ?? []) applyGate(agent);
765
+ };
766
+
727
767
  /* ---------------- HTTP 路由 ---------------- */
728
768
 
729
769
  const state = () => {
@@ -915,7 +955,7 @@ export async function apply(ctx, config = {}) {
915
955
  'POST /wx-auth/start': async (body) => {
916
956
  const serverName = String(body?.serverName ?? 'weixin-mcp');
917
957
  const WX_BASE = 'https://ilinkai.weixin.qq.com';
918
- const ACCOUNTS_DIR = `${process.env.HOME}/.weixin-mcp/accounts`;
958
+ const ACCOUNTS_DIR = `${homedir()}/.weixin-mcp/accounts`;
919
959
  const genQr = async (url) => {
920
960
  const { default: QR } = await import('qrcode');
921
961
  return QR.toDataURL(url, { width: 360, margin: 2 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wfm-cn/dsh-mcp-connectors",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",