@vrs-soft/wecom-aibot-mcp 2.4.25 → 2.6.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/dist/bin.js +20 -5
- package/dist/channel-server.js +91 -28
- package/dist/client.d.ts +5 -0
- package/dist/client.js +49 -0
- package/dist/config-wizard.d.ts +9 -1
- package/dist/config-wizard.js +52 -6
- package/dist/hooks/permission-hook.d.ts +2 -0
- package/dist/hooks/permission-hook.js +325 -0
- package/dist/hooks/stop-hook.d.ts +2 -0
- package/dist/hooks/stop-hook.js +101 -0
- package/dist/http-server.d.ts +1 -0
- package/dist/http-server.js +64 -4
- package/dist/message-bus.d.ts +15 -1
- package/dist/message-bus.js +12 -1
- package/dist/platform.d.ts +14 -0
- package/dist/platform.js +107 -0
- package/dist/tools/index.js +104 -5
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -13,7 +13,7 @@ import { spawn, execSync } from 'child_process';
|
|
|
13
13
|
import * as fs from 'fs';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import * as os from 'os';
|
|
16
|
-
import { runConfigWizard, loadConfig, saveConfig, deleteRobotConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, listAllRobots, ensureGlobalConfigs, getAuthToken, setAuthToken, getHttpsConfig, setHttpsConfig, updateMcpAuthHeaders, runRemoteInstallWizard, VERSION, } from './config-wizard.js';
|
|
16
|
+
import { runConfigWizard, loadConfig, saveConfig, deleteRobotConfigInteractive, uninstall, addMcpConfig, detectUserIdFromMessage, ensureHookInstalled, listAllRobots, ensureGlobalConfigs, getInstalledMode, getAuthToken, setAuthToken, getHttpsConfig, setHttpsConfig, updateMcpAuthHeaders, runRemoteInstallWizard, VERSION, } from './config-wizard.js';
|
|
17
17
|
import { initClient } from './client.js';
|
|
18
18
|
import { registerTools } from './tools/index.js';
|
|
19
19
|
import { startHttpServer, stopHttpServer, HTTP_PORT } from './http-server.js';
|
|
@@ -373,9 +373,14 @@ function startMcpServerBackground() {
|
|
|
373
373
|
}
|
|
374
374
|
async function main() {
|
|
375
375
|
const args = process.argv.slice(2);
|
|
376
|
-
//
|
|
377
|
-
const
|
|
378
|
-
args.includes('--channel-only') ? 'channel-only' :
|
|
376
|
+
// 确定安装模式:优先 CLI flag,其次复用 version.json 里上次的 mode(保持 remote / channel-only 等模式不被 --upgrade 打回 full)
|
|
377
|
+
const explicitMode = args.includes('--http-only') ? 'http-only' :
|
|
378
|
+
args.includes('--channel-only') ? 'channel-only' : undefined;
|
|
379
|
+
const prior = getInstalledMode();
|
|
380
|
+
const installMode = explicitMode || prior.mode || 'full';
|
|
381
|
+
const remoteOptions = (installMode === 'remote' || installMode === 'remote-channel') && prior.remote?.url
|
|
382
|
+
? { url: prior.remote.url, token: prior.remote.token || '' }
|
|
383
|
+
: undefined;
|
|
379
384
|
// 以下命令跳过顶部 ensureGlobalConfigs,避免覆盖配置
|
|
380
385
|
// --setup: 向导完成后自己调用
|
|
381
386
|
// --channel: 作为 Channel MCP 代理运行,不应改写全局配置
|
|
@@ -390,7 +395,17 @@ async function main() {
|
|
|
390
395
|
args.includes('--clean-cache') || args.includes('--set-token') || args.includes('--config');
|
|
391
396
|
if (!skipEnsure) {
|
|
392
397
|
// 强制覆盖所有全局配置(不依赖智能体)
|
|
393
|
-
|
|
398
|
+
if (installMode === 'remote' || installMode === 'remote-channel') {
|
|
399
|
+
if (remoteOptions) {
|
|
400
|
+
ensureGlobalConfigs(installMode, remoteOptions);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
console.log(`[mcp] 检测到上次安装模式 ${installMode},但缺少远程参数;跳过配置写入。如需变更请使用 --setup`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
ensureGlobalConfigs(installMode);
|
|
408
|
+
}
|
|
394
409
|
}
|
|
395
410
|
// 解析命令行参数
|
|
396
411
|
if (args.includes('--help') || args.includes('-h')) {
|
package/dist/channel-server.js
CHANGED
|
@@ -225,6 +225,16 @@ function connectSSE(ccId) {
|
|
|
225
225
|
const sseUrl = ccId ? `${MCP_URL}/sse/${ccId}?ccId=${ccId}` : `${MCP_URL}/sse`;
|
|
226
226
|
logger.info('Connecting to SSE', { url: sseUrl, ccId, mcpServerReady: mcpServer ? 'yes' : 'no' });
|
|
227
227
|
sseAbortController = new AbortController();
|
|
228
|
+
// Watchdog:每 15s 检查最后一次收到 chunk 的时间,>45s 无数据则主动 abort 触发重连。
|
|
229
|
+
// 修复 daemon 端 SSE keep-alive 单向失效问题(NAT 在 client→daemon 方向闭合时
|
|
230
|
+
// daemon 写心跳失败把 entry 清掉,但 channel-server 的 fetch read 永不返回)。
|
|
231
|
+
let watchdogTimer = null;
|
|
232
|
+
const clearWatchdog = () => {
|
|
233
|
+
if (watchdogTimer) {
|
|
234
|
+
clearInterval(watchdogTimer);
|
|
235
|
+
watchdogTimer = null;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
228
238
|
// SSE fetch 配置:添加 keep-alive headers 确保连接稳定
|
|
229
239
|
fetch(sseUrl, {
|
|
230
240
|
method: 'GET',
|
|
@@ -269,15 +279,26 @@ function connectSSE(ccId) {
|
|
|
269
279
|
const decoder = new TextDecoder();
|
|
270
280
|
let buffer = '';
|
|
271
281
|
let messageCount = 0;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
let lastChunkAt = Date.now();
|
|
283
|
+
let currentEvent = 'message'; // SSE event type,由 `event: xxx` 行设置;空行复位
|
|
284
|
+
// Watchdog:>45s 没收到任何 chunk(含 daemon 端的 `: heartbeat` 注释)
|
|
285
|
+
// 视为单向 TCP 死链,主动 abort 让 catch 分支触发 reconnect。
|
|
286
|
+
watchdogTimer = setInterval(() => {
|
|
287
|
+
const idleMs = Date.now() - lastChunkAt;
|
|
288
|
+
logChannel('SSE watchdog', { connected: sseConnected, messages: messageCount, idleMs });
|
|
289
|
+
if (idleMs > 45000) {
|
|
290
|
+
logger.info('SSE 心跳超时(>45s 无数据),主动 abort 触发重连', { ccId, idleMs });
|
|
291
|
+
try {
|
|
292
|
+
sseAbortController?.abort();
|
|
293
|
+
}
|
|
294
|
+
catch { /* ignore */ }
|
|
295
|
+
}
|
|
296
|
+
}, 15000);
|
|
276
297
|
while (true) {
|
|
277
298
|
const { done, value } = await reader.read();
|
|
278
299
|
if (done) {
|
|
279
300
|
logChannel('SSE stream ended');
|
|
280
|
-
|
|
301
|
+
clearWatchdog();
|
|
281
302
|
sseConnected = false;
|
|
282
303
|
// 非主动断开时自动重连
|
|
283
304
|
if (!sseAbortController?.signal.aborted) {
|
|
@@ -286,6 +307,7 @@ function connectSSE(ccId) {
|
|
|
286
307
|
}
|
|
287
308
|
break;
|
|
288
309
|
}
|
|
310
|
+
lastChunkAt = Date.now();
|
|
289
311
|
const chunk = decoder.decode(value, { stream: true });
|
|
290
312
|
logChannel('SSE chunk received', { bytes: chunk.length, preview: chunk.slice(0, 100) });
|
|
291
313
|
buffer += chunk;
|
|
@@ -296,28 +318,49 @@ function connectSSE(ccId) {
|
|
|
296
318
|
logChannel('SSE line', { line: line.slice(0, 80) });
|
|
297
319
|
if (line.startsWith('data: ')) {
|
|
298
320
|
const data = line.slice(6);
|
|
299
|
-
logChannel('📩 SSE MESSAGE RECEIVED', { data: data.slice(0, 100) });
|
|
321
|
+
logChannel('📩 SSE MESSAGE RECEIVED', { data: data.slice(0, 100), event: currentEvent });
|
|
300
322
|
try {
|
|
301
323
|
const msg = JSON.parse(data);
|
|
302
324
|
messageCount++;
|
|
303
|
-
logChannel('✅ 消息解析成功', { messageNumber: messageCount, msg });
|
|
304
|
-
// 推送 notifications/claude/channel 唤醒 Claude agent
|
|
325
|
+
logChannel('✅ 消息解析成功', { messageNumber: messageCount, event: currentEvent, msg });
|
|
305
326
|
if (mcpServer) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
327
|
+
let notification;
|
|
328
|
+
if (currentEvent === 'cc_message') {
|
|
329
|
+
// CC 间消息:用 cc:<fromCc> 作为 source 前缀,便于 agent 区分非 wecom 来源
|
|
330
|
+
notification = {
|
|
331
|
+
method: 'notifications/claude/channel',
|
|
332
|
+
params: {
|
|
333
|
+
content: msg.content || '',
|
|
334
|
+
meta: {
|
|
335
|
+
source: `cc:${msg.fromCc || ''}`,
|
|
336
|
+
from_cc: msg.fromCc || '',
|
|
337
|
+
to_cc: msg.toCc || '',
|
|
338
|
+
chattype: 'cc',
|
|
339
|
+
cc_id: msg.toCc || '',
|
|
340
|
+
kind: msg.kind || 'notify',
|
|
341
|
+
reply_to: msg.replyTo || '',
|
|
342
|
+
msg_id: msg.msgId || '',
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
// 默认 wecom 消息(event: message 或无 event 头)
|
|
349
|
+
const message = msg.message || {};
|
|
350
|
+
notification = {
|
|
351
|
+
method: 'notifications/claude/channel',
|
|
352
|
+
params: {
|
|
353
|
+
content: message.content || JSON.stringify(msg),
|
|
354
|
+
meta: {
|
|
355
|
+
from: message.from || '',
|
|
356
|
+
chatid: message.chatid || '',
|
|
357
|
+
chattype: message.chattype || 'single',
|
|
358
|
+
cc_id: msg.ccId || '',
|
|
359
|
+
quote_content: message.quoteContent || '',
|
|
360
|
+
},
|
|
318
361
|
},
|
|
319
|
-
}
|
|
320
|
-
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
321
364
|
logChannel('📤 发送 notification', { notification });
|
|
322
365
|
try {
|
|
323
366
|
mcpServer.server.notification(notification);
|
|
@@ -336,10 +379,12 @@ function connectSSE(ccId) {
|
|
|
336
379
|
}
|
|
337
380
|
}
|
|
338
381
|
else if (line.startsWith('event: ')) {
|
|
339
|
-
|
|
382
|
+
currentEvent = line.slice(7).trim();
|
|
383
|
+
logChannel('SSE event type', { type: currentEvent });
|
|
340
384
|
}
|
|
341
385
|
else if (line === '') {
|
|
342
|
-
//
|
|
386
|
+
// 事件分隔符:复位 event type 到默认 'message'
|
|
387
|
+
currentEvent = 'message';
|
|
343
388
|
}
|
|
344
389
|
else if (line.startsWith(':')) {
|
|
345
390
|
// SSE 注释(如 ": heartbeat"),忽略,不要写回 buffer
|
|
@@ -350,13 +395,18 @@ function connectSSE(ccId) {
|
|
|
350
395
|
}
|
|
351
396
|
}
|
|
352
397
|
}
|
|
353
|
-
|
|
398
|
+
clearWatchdog();
|
|
354
399
|
}).catch((err) => {
|
|
400
|
+
clearWatchdog();
|
|
355
401
|
logger.error('SSE error', { error: String(err) });
|
|
356
402
|
sseConnected = false;
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
403
|
+
// watchdog abort 或网络异常都走这里:触发 reconnect
|
|
404
|
+
// 注意 abort() 后 signal.aborted=true,但这是 watchdog 自己造成的,仍需要重连
|
|
405
|
+
const isWatchdogAbort = sseAbortController?.signal.aborted && String(err).includes('aborted');
|
|
406
|
+
if (!sseAbortController?.signal.aborted || isWatchdogAbort) {
|
|
407
|
+
logger.info('SSE 出错,3 秒后重连', { ccId, watchdogAbort: isWatchdogAbort });
|
|
408
|
+
// watchdog abort 后需要新建 controller,否则下次 connectSSE 会立即被 abort 状态干扰
|
|
409
|
+
sseAbortController = null;
|
|
360
410
|
setTimeout(() => { httpSessionId = null; connectSSE(ccId); }, 3000);
|
|
361
411
|
}
|
|
362
412
|
});
|
|
@@ -402,6 +452,19 @@ function registerChannelTools(server) {
|
|
|
402
452
|
return forwardToHttpMcp('check_connection', {});
|
|
403
453
|
});
|
|
404
454
|
// ============================================
|
|
455
|
+
// 工具 4a: CC 间通信 — send_to_cc / list_active_ccs(v2.6.0+)
|
|
456
|
+
// ============================================
|
|
457
|
+
server.tool('send_to_cc', '向同一 daemon 上的另一个 CC 发送消息。目标 CC 收到时会作为 <channel source="cc:..."> 推送唤醒。仅支持同 daemon 间互通。', {
|
|
458
|
+
cc_id: z.string().describe('自己的 CC 标识'),
|
|
459
|
+
to_cc: z.string().describe('目标 CC 标识'),
|
|
460
|
+
content: z.string().describe('消息内容(支持 Markdown)'),
|
|
461
|
+
kind: z.enum(['request', 'reply', 'notify']).optional().default('notify').describe('消息语义'),
|
|
462
|
+
reply_to: z.string().optional().describe('可选:关联的请求 msgId'),
|
|
463
|
+
}, async (params) => forwardToHttpMcp('send_to_cc', params));
|
|
464
|
+
server.tool('list_active_ccs', '列出同一 daemon 上当前在线的所有 CC', {
|
|
465
|
+
cc_id: z.string().describe('自己的 CC 标识'),
|
|
466
|
+
}, async (params) => forwardToHttpMcp('list_active_ccs', params));
|
|
467
|
+
// ============================================
|
|
405
468
|
// 工具 4: 获取待处理消息
|
|
406
469
|
// ============================================
|
|
407
470
|
server.tool('get_pending_messages', '获取待处理的微信消息。支持长轮询:传入 timeout_ms 后阻塞等待,有消息立即返回,无消息等到超时。超时后继续轮询,不要停止。', {
|
package/dist/client.d.ts
CHANGED
|
@@ -46,6 +46,9 @@ declare class WecomClient extends EventEmitter {
|
|
|
46
46
|
private reconnectAttempt;
|
|
47
47
|
private lastDisconnectTime;
|
|
48
48
|
private disconnectNotifyCount;
|
|
49
|
+
private daemonReconnectTimer;
|
|
50
|
+
private daemonReconnectAttempts;
|
|
51
|
+
private intentionallyDisconnected;
|
|
49
52
|
constructor(botId: string, secret: string, targetUserId: string, robotName: string);
|
|
50
53
|
getAuthUrl(): string;
|
|
51
54
|
private setupEventHandlers;
|
|
@@ -54,6 +57,8 @@ declare class WecomClient extends EventEmitter {
|
|
|
54
57
|
private replyApprovalResult;
|
|
55
58
|
connect(): void;
|
|
56
59
|
disconnect(): void;
|
|
60
|
+
private scheduleDaemonReconnect;
|
|
61
|
+
private clearDaemonReconnect;
|
|
57
62
|
isConnected(): boolean;
|
|
58
63
|
getDefaultTargetUser(): string;
|
|
59
64
|
verifyTargetUser(userId?: string): Promise<{
|
package/dist/client.js
CHANGED
|
@@ -56,6 +56,11 @@ class WecomClient extends EventEmitter {
|
|
|
56
56
|
reconnectAttempt = 0; // 重连尝试次数
|
|
57
57
|
lastDisconnectTime = 0; // 最后断线时间
|
|
58
58
|
disconnectNotifyCount = 0; // 断线通知次数(最多1次)
|
|
59
|
+
// daemon-level 重连兜底:SDK 内部的 reconnect 偶尔会因被服务端踢断("New connection established")
|
|
60
|
+
// 而沉默卡住。下面这套是 daemon 层的 safety net,指数退避 5s/10s/30s/60s,最多 100 次。
|
|
61
|
+
daemonReconnectTimer = null;
|
|
62
|
+
daemonReconnectAttempts = 0;
|
|
63
|
+
intentionallyDisconnected = false;
|
|
59
64
|
constructor(botId, secret, targetUserId, robotName) {
|
|
60
65
|
super();
|
|
61
66
|
this.botId = botId;
|
|
@@ -87,6 +92,7 @@ class WecomClient extends EventEmitter {
|
|
|
87
92
|
this.wasReconnecting = false;
|
|
88
93
|
this.reconnectAttempt = 0;
|
|
89
94
|
this.disconnectNotifyCount = 0; // 重连成功后重置断线通知计数
|
|
95
|
+
this.clearDaemonReconnect(); // 重连成功后清理 daemon-level safety net
|
|
90
96
|
logAuthenticated();
|
|
91
97
|
// 重连成功后发送通知
|
|
92
98
|
if (wasReconnecting) {
|
|
@@ -109,6 +115,10 @@ class WecomClient extends EventEmitter {
|
|
|
109
115
|
logger.error('wecom', `发送断线通知失败: ${err}`);
|
|
110
116
|
});
|
|
111
117
|
}
|
|
118
|
+
// 兜底重连:SDK 重连失败时由 daemon 层接手
|
|
119
|
+
if (!this.intentionallyDisconnected) {
|
|
120
|
+
this.scheduleDaemonReconnect();
|
|
121
|
+
}
|
|
112
122
|
});
|
|
113
123
|
this.wsClient.on('reconnecting', (attempt) => {
|
|
114
124
|
this.reconnectAttempt = attempt;
|
|
@@ -285,12 +295,51 @@ class WecomClient extends EventEmitter {
|
|
|
285
295
|
}
|
|
286
296
|
// 连接
|
|
287
297
|
connect() {
|
|
298
|
+
this.intentionallyDisconnected = false;
|
|
288
299
|
this.wsClient.connect();
|
|
289
300
|
}
|
|
290
301
|
// 断开
|
|
291
302
|
disconnect() {
|
|
303
|
+
this.intentionallyDisconnected = true;
|
|
304
|
+
this.clearDaemonReconnect();
|
|
292
305
|
this.wsClient.disconnect();
|
|
293
306
|
}
|
|
307
|
+
// daemon-level 重连:SDK 内部 reconnect 卡住时由这里接手
|
|
308
|
+
// 指数退避:5s → 10s → 30s → 60s(封顶);最多 100 次
|
|
309
|
+
scheduleDaemonReconnect() {
|
|
310
|
+
if (this.daemonReconnectTimer)
|
|
311
|
+
return; // 已经在等待
|
|
312
|
+
if (this.daemonReconnectAttempts >= 100) {
|
|
313
|
+
logger.error('wecom', `[${this.robotName}] daemon-level reconnect 100 次仍未成功,放弃`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const backoff = [5000, 10000, 30000, 60000];
|
|
317
|
+
const delay = backoff[Math.min(this.daemonReconnectAttempts, backoff.length - 1)];
|
|
318
|
+
this.daemonReconnectTimer = setTimeout(() => {
|
|
319
|
+
this.daemonReconnectTimer = null;
|
|
320
|
+
if (this.connected || this.intentionallyDisconnected) {
|
|
321
|
+
// 已经恢复或已显式断开,不需要重连
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
this.daemonReconnectAttempts++;
|
|
325
|
+
logger.info('wecom', `[${this.robotName}] daemon-level reconnect attempt ${this.daemonReconnectAttempts}`);
|
|
326
|
+
try {
|
|
327
|
+
this.wsClient.connect();
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
logger.error('wecom', `[${this.robotName}] daemon-level reconnect failed: ${err.message}`);
|
|
331
|
+
}
|
|
332
|
+
// 无论成功失败都排下一轮,若已连上下次 timeout 触发时 connected=true 会直接 return
|
|
333
|
+
this.scheduleDaemonReconnect();
|
|
334
|
+
}, delay);
|
|
335
|
+
}
|
|
336
|
+
clearDaemonReconnect() {
|
|
337
|
+
if (this.daemonReconnectTimer) {
|
|
338
|
+
clearTimeout(this.daemonReconnectTimer);
|
|
339
|
+
this.daemonReconnectTimer = null;
|
|
340
|
+
}
|
|
341
|
+
this.daemonReconnectAttempts = 0;
|
|
342
|
+
}
|
|
294
343
|
// 检查连接状态
|
|
295
344
|
isConnected() {
|
|
296
345
|
return this.connected;
|
package/dist/config-wizard.d.ts
CHANGED
|
@@ -40,7 +40,15 @@ export declare function getDocMcpUrl(robotName?: string): {
|
|
|
40
40
|
error?: string;
|
|
41
41
|
};
|
|
42
42
|
export declare function ensureHookInstalled(): void;
|
|
43
|
-
export
|
|
43
|
+
export type InstallMode = 'full' | 'http-only' | 'channel-only' | 'remote' | 'remote-channel';
|
|
44
|
+
export declare function getInstalledMode(): {
|
|
45
|
+
mode?: InstallMode;
|
|
46
|
+
remote?: {
|
|
47
|
+
url: string;
|
|
48
|
+
token?: string;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export declare function ensureGlobalConfigs(mode?: InstallMode, remoteOptions?: {
|
|
44
52
|
url: string;
|
|
45
53
|
token: string;
|
|
46
54
|
}): {
|
package/dist/config-wizard.js
CHANGED
|
@@ -177,10 +177,19 @@ export function deleteConfig() {
|
|
|
177
177
|
if (fs.existsSync(CLAUDE_CONFIG_FILE)) {
|
|
178
178
|
const content = fs.readFileSync(CLAUDE_CONFIG_FILE, 'utf-8');
|
|
179
179
|
const claudeConfig = JSON.parse(content);
|
|
180
|
+
let changed = false;
|
|
180
181
|
if (claudeConfig.mcpServers?.['wecom-aibot']) {
|
|
181
182
|
delete claudeConfig.mcpServers['wecom-aibot'];
|
|
182
|
-
fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
|
|
183
183
|
console.log('[config] 已从 ~/.claude.json 删除 wecom-aibot 配置');
|
|
184
|
+
changed = true;
|
|
185
|
+
}
|
|
186
|
+
if (claudeConfig.mcpServers?.['wecom-aibot-channel']) {
|
|
187
|
+
delete claudeConfig.mcpServers['wecom-aibot-channel'];
|
|
188
|
+
console.log('[config] 已从 ~/.claude.json 删除 wecom-aibot-channel 配置');
|
|
189
|
+
changed = true;
|
|
190
|
+
}
|
|
191
|
+
if (changed) {
|
|
192
|
+
fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
|
|
184
193
|
}
|
|
185
194
|
}
|
|
186
195
|
}
|
|
@@ -194,14 +203,27 @@ export function deleteHook() {
|
|
|
194
203
|
if (fs.existsSync(CLAUDE_SETTINGS_FILE)) {
|
|
195
204
|
const content = fs.readFileSync(CLAUDE_SETTINGS_FILE, 'utf-8');
|
|
196
205
|
const settings = JSON.parse(content);
|
|
206
|
+
let changed = false;
|
|
197
207
|
if (settings.hooks && settings.hooks['PermissionRequest']) {
|
|
198
208
|
// 只删除 wecom-aibot 相关的 hook
|
|
199
209
|
settings.hooks['PermissionRequest'] = settings.hooks['PermissionRequest'].filter((hook) => !hook.hooks?.some?.((h) => h.command?.includes?.('wecom-aibot-mcp')));
|
|
200
210
|
if (settings.hooks['PermissionRequest'].length === 0) {
|
|
201
211
|
delete settings.hooks['PermissionRequest'];
|
|
202
212
|
}
|
|
203
|
-
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
204
213
|
console.log('[config] 已删除 PermissionRequest hook');
|
|
214
|
+
changed = true;
|
|
215
|
+
}
|
|
216
|
+
// 移除 wecom-aibot 相关的 MCP 权限
|
|
217
|
+
if (Array.isArray(settings.permissions?.allow)) {
|
|
218
|
+
const before = settings.permissions.allow.length;
|
|
219
|
+
settings.permissions.allow = settings.permissions.allow.filter((p) => !/^mcp__wecom-aibot(-channel)?__/.test(p));
|
|
220
|
+
if (settings.permissions.allow.length !== before) {
|
|
221
|
+
console.log(`[config] 已移除 ${before - settings.permissions.allow.length} 条 wecom-aibot MCP 权限`);
|
|
222
|
+
changed = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (changed) {
|
|
226
|
+
fs.writeFileSync(CLAUDE_SETTINGS_FILE, JSON.stringify(settings, null, 2));
|
|
205
227
|
}
|
|
206
228
|
// 删除 hook 脚本文件
|
|
207
229
|
if (fs.existsSync(HOOK_SCRIPT_PATH)) {
|
|
@@ -1064,6 +1086,30 @@ export function ensureHookInstalled() {
|
|
|
1064
1086
|
writeMcpPermissions();
|
|
1065
1087
|
writeStopHookScript();
|
|
1066
1088
|
}
|
|
1089
|
+
// 读取上次安装的模式 + 远程参数(来自 version.json)
|
|
1090
|
+
export function getInstalledMode() {
|
|
1091
|
+
if (!fs.existsSync(VERSION_FILE))
|
|
1092
|
+
return {};
|
|
1093
|
+
try {
|
|
1094
|
+
const data = JSON.parse(fs.readFileSync(VERSION_FILE, 'utf-8'));
|
|
1095
|
+
const result = {};
|
|
1096
|
+
if (data.mode)
|
|
1097
|
+
result.mode = data.mode;
|
|
1098
|
+
if (data.remote?.url)
|
|
1099
|
+
result.remote = { url: data.remote.url, token: data.remote.token };
|
|
1100
|
+
return result;
|
|
1101
|
+
}
|
|
1102
|
+
catch {
|
|
1103
|
+
return {};
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
// 写 version.json(统一入口,记录 mode + 远程参数,用于后续 --upgrade 复用)
|
|
1107
|
+
function writeVersionFile(mode, remoteOptions) {
|
|
1108
|
+
const payload = { version: VERSION, installedAt: Date.now(), mode };
|
|
1109
|
+
if (remoteOptions?.url)
|
|
1110
|
+
payload.remote = { url: remoteOptions.url, ...(remoteOptions.token ? { token: remoteOptions.token } : {}) };
|
|
1111
|
+
fs.writeFileSync(VERSION_FILE, JSON.stringify(payload, null, 2));
|
|
1112
|
+
}
|
|
1067
1113
|
// 确保所有全局配置已写入(强制覆盖,不依赖智能体)
|
|
1068
1114
|
export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
|
|
1069
1115
|
ensureConfigDir();
|
|
@@ -1085,7 +1131,7 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
|
|
|
1085
1131
|
// 只写权限配置和 Hook(可选,用于本地调试)
|
|
1086
1132
|
writeMcpPermissions();
|
|
1087
1133
|
console.log('[config] 已写入权限配置到 ~/.claude/settings.local.json');
|
|
1088
|
-
|
|
1134
|
+
writeVersionFile(mode);
|
|
1089
1135
|
return { upgraded, previousVersion };
|
|
1090
1136
|
}
|
|
1091
1137
|
// remote 模式:仅写入远程 HTTP MCP 配置(带 token headers),不装 Channel/Hook
|
|
@@ -1108,7 +1154,7 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
|
|
|
1108
1154
|
};
|
|
1109
1155
|
fs.writeFileSync(CLAUDE_CONFIG_FILE, JSON.stringify(claudeConfig, null, 2));
|
|
1110
1156
|
console.log('[config] remote 模式:已写入远程 HTTP MCP 配置(带 Token)');
|
|
1111
|
-
|
|
1157
|
+
writeVersionFile(mode, remoteOptions);
|
|
1112
1158
|
return { upgraded, previousVersion };
|
|
1113
1159
|
}
|
|
1114
1160
|
// remote-channel 模式:远程部署的 Channel 客户端——只写 Channel MCP,不写 HTTP MCP
|
|
@@ -1143,7 +1189,7 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
|
|
|
1143
1189
|
// Channel 模式需要权限配置
|
|
1144
1190
|
writeMcpPermissions();
|
|
1145
1191
|
console.log('[config] 已写入权限配置到 ~/.claude/settings.local.json');
|
|
1146
|
-
|
|
1192
|
+
writeVersionFile(mode, remoteOptions);
|
|
1147
1193
|
return { upgraded, previousVersion };
|
|
1148
1194
|
}
|
|
1149
1195
|
// 1. 强制写入 MCP 配置到 ~/.claude.json
|
|
@@ -1211,7 +1257,7 @@ export function ensureGlobalConfigs(mode = 'full', remoteOptions) {
|
|
|
1211
1257
|
writeMcpPermissions();
|
|
1212
1258
|
console.log('[config] 已写入权限配置到 ~/.claude/settings.local.json');
|
|
1213
1259
|
// 3. 写入版本号
|
|
1214
|
-
|
|
1260
|
+
writeVersionFile(mode);
|
|
1215
1261
|
console.log(`[config] 已记录版本号: ${VERSION}`);
|
|
1216
1262
|
return { upgraded, previousVersion };
|
|
1217
1263
|
}
|