@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 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 installMode = args.includes('--http-only') ? 'http-only' :
378
- args.includes('--channel-only') ? 'channel-only' : 'full';
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
- ensureGlobalConfigs(installMode);
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')) {
@@ -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
- const heartbeatInterval = setInterval(() => {
274
- logChannel('SSE heartbeat', { connected: sseConnected, messages: messageCount });
275
- }, 30000);
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
- clearInterval(heartbeatInterval);
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
- // content 成为 <channel> 标签正文,meta 成为标签属性(只允许字母/数字/下划线)
307
- const message = msg.message || {};
308
- const notification = {
309
- method: 'notifications/claude/channel',
310
- params: {
311
- content: message.content || JSON.stringify(msg),
312
- meta: {
313
- from: message.from || '',
314
- chatid: message.chatid || '',
315
- chattype: message.chattype || 'single',
316
- cc_id: msg.ccId || '',
317
- quote_content: message.quoteContent || '',
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
- logChannel('SSE event type', { type: line.slice(7) });
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
- clearInterval(heartbeatInterval);
398
+ clearWatchdog();
354
399
  }).catch((err) => {
400
+ clearWatchdog();
355
401
  logger.error('SSE error', { error: String(err) });
356
402
  sseConnected = false;
357
- // 非主动断开时自动重连
358
- if (!sseAbortController?.signal.aborted) {
359
- logger.info('SSE 出错,3 秒后重连', { ccId });
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;
@@ -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 declare function ensureGlobalConfigs(mode?: 'full' | 'http-only' | 'channel-only' | 'remote' | 'remote-channel', remoteOptions?: {
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
  }): {
@@ -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
- fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
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
- fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
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
- fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
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
- fs.writeFileSync(VERSION_FILE, JSON.stringify({ version: VERSION, installedAt: Date.now() }, null, 2));
1260
+ writeVersionFile(mode);
1215
1261
  console.log(`[config] 已记录版本号: ${VERSION}`);
1216
1262
  return { upgraded, previousVersion };
1217
1263
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};