@wfm-cn/dsh-mcp-connectors 0.2.2 → 0.3.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.
- package/README.md +1 -1
- package/client.js +17 -2
- package/index.js +38 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ DeepSeek Harness (DSH) 的 MCP 连接器中心插件:统一管理 MCP 连接
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
7
|
- **连接器面板**:卡片式添加 / 编辑 / 启停 MCP 连接器(streamable-http / stdio / 企业微信)
|
|
8
|
-
- **内置预设**:腾讯文档、金山文档(WPS 授权登录)、GitHub(PAT)、微信 (ClawBot)
|
|
8
|
+
- **内置预设**:腾讯文档、金山文档(WPS 授权登录)、GitHub(PAT)、微信 (ClawBot)、谷歌浏览器(Chrome DevTools MCP,本机 stdio 运行,无需令牌)
|
|
9
9
|
- **微信收发**:基于微信官方 ClawBot 机器人接口,扫码绑定后双向收发消息(文本 / 图片 / 文件)
|
|
10
10
|
- **微信自动回复**:守护轮询收消息(无 LLM 开销),新消息自动注入绑定的 DSH 会话,AI 基于该会话上下文回复
|
|
11
11
|
- **会话级门控**:每个 DSH 会话可选择启用哪些连接器的工具(以 `mcp__<server>__<tool>` 前缀注入)
|
package/client.js
CHANGED
|
@@ -53,6 +53,8 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
wxConfirmOnPhone: '已扫码,请在手机上确认登录…',
|
|
54
54
|
wxBindTimeout: '绑定未完成,请重新点击扫码绑定',
|
|
55
55
|
presetWechat: '微信 (ClawBot)',
|
|
56
|
+
presetChrome: '谷歌浏览器(Chrome DevTools · 本机运行)',
|
|
57
|
+
chromeHint: '无需令牌。保存后自动通过 npx 启动 chrome-devtools-mcp 并连接本机谷歌浏览器;首次使用请确保已安装 Chrome。',
|
|
56
58
|
testConn: '测试连接',
|
|
57
59
|
testing: '测试中',
|
|
58
60
|
fieldEnv: '环境变量(每行 KEY=value)',
|
|
@@ -108,6 +110,8 @@ window.__ModuleLoader__.load({
|
|
|
108
110
|
wxConfirmOnPhone: 'Scanned — confirm login on your phone…',
|
|
109
111
|
wxBindTimeout: 'Not completed. Click the button to try again',
|
|
110
112
|
presetWechat: 'WeChat (ClawBot)',
|
|
113
|
+
presetChrome: 'Google Chrome (Chrome DevTools · local)',
|
|
114
|
+
chromeHint: 'No token needed. Saving launches chrome-devtools-mcp via npx and connects to your local Chrome; make sure Chrome is installed.',
|
|
111
115
|
testConn: 'Test connection',
|
|
112
116
|
testing: 'Testing',
|
|
113
117
|
fieldEnv: 'Environment variables (KEY=value per line)',
|
|
@@ -186,6 +190,14 @@ window.__ModuleLoader__.load({
|
|
|
186
190
|
auth: 'wxqr',
|
|
187
191
|
},
|
|
188
192
|
},
|
|
193
|
+
{
|
|
194
|
+
id: 'chrome',
|
|
195
|
+
labelKey: 'presetChrome',
|
|
196
|
+
template: {
|
|
197
|
+
title: '谷歌浏览器', serverName: 'chrome-devtools', transport: 'stdio',
|
|
198
|
+
command: 'npx', args: ['-y', 'chrome-devtools-mcp@latest'],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
189
201
|
{
|
|
190
202
|
id: 'custom',
|
|
191
203
|
labelKey: 'presetCustom',
|
|
@@ -203,6 +215,7 @@ window.__ModuleLoader__.load({
|
|
|
203
215
|
if (c.type) return c.type;
|
|
204
216
|
const sn = String(c.serverName ?? '');
|
|
205
217
|
const url = String(c.url ?? '');
|
|
218
|
+
if (sn === 'chrome-devtools' || /chrome-devtools-mcp/.test(`${c.command ?? ''} ${(c.args ?? []).join(' ')}`)) return 'chrome';
|
|
206
219
|
if (c.auth === 'wxqr' || sn === 'weixin-mcp') return 'wechat';
|
|
207
220
|
if (c.auth === 'wps' || sn === 'kdocs-wps' || /mcp-center\.wps\.cn/.test(url)) return 'kdocs';
|
|
208
221
|
if (c.auth === 'pat' || sn === 'github' || /api\.githubcopilot\.com/.test(url)) return 'github';
|
|
@@ -421,6 +434,9 @@ window.__ModuleLoader__.load({
|
|
|
421
434
|
}, t('getTokenHint'))))
|
|
422
435
|
: h(React.Fragment, null,
|
|
423
436
|
// 预设类型:只显示该类型自己的字段
|
|
437
|
+
ctype === 'chrome'
|
|
438
|
+
? h('div', { className: 'mcx-auth-hint' }, t('chromeHint'))
|
|
439
|
+
: null,
|
|
424
440
|
ctype === 'github'
|
|
425
441
|
? h(React.Fragment, null,
|
|
426
442
|
h(Field, { label: t('fieldToken') }, h('input', { className: inputCls, type: 'password', value: draft.token === '●' ? '' : draft.token ?? '', onChange: set('token'), placeholder: initial?.token === '●' ? '已保存 · 留空保持不变' : 'github_pat_…' })),
|
|
@@ -431,8 +447,7 @@ window.__ModuleLoader__.load({
|
|
|
431
447
|
}, t('githubCreateBtn')))
|
|
432
448
|
: null,
|
|
433
449
|
ctype === 'wechat'
|
|
434
|
-
? h(React.Fragment, null,
|
|
435
|
-
h(WechatSessionPicker, { t, api, value: draft.targetSession ?? '', onChange: (v) => setDraft((d) => ({ ...d, targetSession: v })) }),
|
|
450
|
+
? h(React.Fragment, null, h(WechatSessionPicker, { t, api, value: draft.targetSession ?? '', onChange: (v) => setDraft((d) => ({ ...d, targetSession: v })) }),
|
|
436
451
|
h(WechatQrAuth, { t, api, onSaved, onSave: () => doSave(false), busy: authBusy, setBusy: setAuthBusy, msg: authMsg, setMsg: setAuthMsg, isNew, initialServerName: initial?.serverName }))
|
|
437
452
|
: null,
|
|
438
453
|
ctype === 'kdocs'
|
package/index.js
CHANGED
|
@@ -711,30 +711,59 @@ export async function apply(ctx, config = {}) {
|
|
|
711
711
|
for (const [sn, w] of want) {
|
|
712
712
|
if (!MOUNTED.has(sn)) await mount(w);
|
|
713
713
|
}
|
|
714
|
+
// 工具集变化后重新计算各会话的门控(deny 依赖已注册的工具名)
|
|
715
|
+
reapplyGates();
|
|
714
716
|
};
|
|
715
717
|
|
|
716
718
|
/* ---------------- 每会话工具门控 ---------------- */
|
|
717
719
|
|
|
718
|
-
const RESTRICTORS = new Map(); // agentId ->
|
|
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。 */
|
|
719
734
|
const applyGate = (agent) => {
|
|
720
735
|
const agentCtx = agent?.ctx;
|
|
721
736
|
if (!agentCtx?.tools?.restrict) return;
|
|
722
737
|
const sessionId = String(agent.session?.id ?? agent.id ?? '');
|
|
723
|
-
try { RESTRICTORS.get(agent.id)?.(); } catch { /* ignore */ }
|
|
724
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; }
|
|
725
748
|
try {
|
|
726
|
-
const dispose = agentCtx.tools.restrict(
|
|
727
|
-
const name = typeof tool === 'string' ? tool : tool?.name ?? tool?.definition?.name;
|
|
728
|
-
if (typeof name !== 'string') return true;
|
|
729
|
-
const m = /^mcp__([A-Za-z0-9_-]+)__/.exec(name);
|
|
730
|
-
return m === null || allowed.has(m[1]);
|
|
731
|
-
});
|
|
749
|
+
const dispose = agentCtx.tools.restrict({ deny });
|
|
732
750
|
RESTRICTORS.set(agent.id, dispose);
|
|
751
|
+
GATE_PENDING.delete(agent.id);
|
|
733
752
|
} catch (e) {
|
|
734
|
-
|
|
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
|
+
}
|
|
735
759
|
}
|
|
736
760
|
};
|
|
737
761
|
|
|
762
|
+
/** 挂载/卸载后重新应用所有 agent 的门控(门控依赖已注册的工具名)。 */
|
|
763
|
+
const reapplyGates = () => {
|
|
764
|
+
for (const agent of AGENTS?.list?.() ?? []) applyGate(agent);
|
|
765
|
+
};
|
|
766
|
+
|
|
738
767
|
/* ---------------- HTTP 路由 ---------------- */
|
|
739
768
|
|
|
740
769
|
const state = () => {
|