@vrs-soft/wecom-aibot-mcp 3.3.2 → 3.4.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.
@@ -14,11 +14,14 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
14
14
  import { z } from 'zod';
15
15
  import * as fs from 'fs';
16
16
  import * as path from 'path';
17
- import { execSync } from 'child_process';
18
17
  import { VERSION, installSkill } from './config-wizard.js';
19
18
  import { addPermissionHook, registerActiveProject, unregisterActiveProject, updateWechatModeConfig, loadWechatModeConfig } from './project-config.js';
19
+ import { findClaudePid } from './platform.js';
20
20
  import { logger } from './logger.js';
21
21
  /**
22
+ * v3.4.0: findClaudePid 移到 src/platform.ts(跨平台 wmic / ps),下方旧实现保留为参考
23
+ * 但已经不再使用 —— 调用点 import 自 platform.js
24
+ *
22
25
  * 沿进程树向上查找 Claude Code TUI 的 PID。
23
26
  *
24
27
  * 背景:本地 dev (`command: "node"`) 时 channel-server 是 Claude TUI 的直接子进程,
@@ -32,32 +35,7 @@ import { logger } from './logger.js';
32
35
  * 此函数从 startPid 起向上遍历,找到第一个命令名为 "claude" 的进程,返回其 PID。
33
36
  * 找不到时回退到 startPid(保持旧行为,至少 dev 场景不退化)。
34
37
  */
35
- function findClaudePid(startPid) {
36
- let pid = startPid;
37
- for (let i = 0; i < 8; i++) {
38
- if (!pid || pid <= 1)
39
- break;
40
- try {
41
- const comm = execSync(`ps -p ${pid} -o comm=`, { stdio: ['ignore', 'pipe', 'ignore'] })
42
- .toString()
43
- .trim();
44
- // ps comm= 返回执行文件 basename。Claude Code TUI 安装名就是 "claude"
45
- if (comm === 'claude' || comm.endsWith('/claude'))
46
- return pid;
47
- const ppidStr = execSync(`ps -p ${pid} -o ppid=`, { stdio: ['ignore', 'pipe', 'ignore'] })
48
- .toString()
49
- .trim();
50
- const ppid = parseInt(ppidStr, 10);
51
- if (!ppid || ppid === pid)
52
- break;
53
- pid = ppid;
54
- }
55
- catch {
56
- break;
57
- }
58
- }
59
- return startPid;
60
- }
38
+ // 实现已迁移到 src/platform.ts(跨平台 wmic / ps),见 import { findClaudePid }
61
39
  const MCP_URL = process.env.MCP_URL || 'http://127.0.0.1:18963';
62
40
  const MCP_AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
63
41
  // ============================================
@@ -18,8 +18,13 @@ const VERSION_FILE = path.join(CONFIG_DIR, 'version.json');
18
18
  const SERVER_CONFIG_FILE = path.join(CONFIG_DIR, 'server.json');
19
19
  const CLAUDE_CONFIG_FILE = path.join(os.homedir(), '.claude.json');
20
20
  const CLAUDE_SETTINGS_FILE = path.join(os.homedir(), '.claude', 'settings.local.json');
21
- const HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'permission-hook.sh');
22
- const STOP_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'stop-hook.sh');
21
+ // v3.4.0: hook 改 Node.js 实现,安装为 .mjs(hook 安装目录无 package.json,
22
+ // .js 会被 node 当 CommonJS,源文件的 import 语法报错)
23
+ const HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'permission-hook.mjs');
24
+ const STOP_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'stop-hook.mjs');
25
+ // 旧路径(v3.3.x 及以前),安装时清理
26
+ const HOOK_SCRIPT_PATH_LEGACY = path.join(CONFIG_DIR, 'permission-hook.sh');
27
+ const STOP_HOOK_SCRIPT_PATH_LEGACY = path.join(CONFIG_DIR, 'stop-hook.sh');
23
28
  // Skill 模板路径(包内)- 使用 fileURLToPath 确保跨平台兼容
24
29
  const __filename = fileURLToPath(import.meta.url);
25
30
  const __dirname = path.dirname(__filename);
@@ -179,382 +184,48 @@ export function uninstall() {
179
184
  }
180
185
  // 生成并写入 hook 脚本(HTTP Transport 版本)
181
186
  function writeHookScript() {
182
- const script = `#!/bin/bash
183
- # wecom-aibot-mcp PermissionRequest hook
184
- # HTTP Transport 版本
185
- #
186
- # 固定端口: 18963
187
- # 通过 PID 树查 ~/.wecom-aibot-mcp/active-projects.json 匹配项目,读 wechatMode 开关
188
-
189
- MCP_PORT=18963
190
-
191
- # 先保存输入(只能读一次)
192
- INPUT=$(cat)
193
-
194
- # 日志输出:--debug 模式下输出到 stderr,否则静默
195
- DEBUG_FILE="$HOME/.wecom-aibot-mcp/debug"
196
- log_debug() {
197
- if [[ -f "$DEBUG_FILE" ]]; then
198
- echo "$1" >&2
199
- fi
200
- }
201
-
202
- log_debug "[$(date)] Hook called. TOOL_NAME: $(echo "$INPUT" | jq -r '.tool_name')"
203
-
204
- TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
205
-
206
- # MCP 工具本身不需要拦截
207
- if [[ "$TOOL_NAME" == mcp__* ]]; then
208
- log_debug "[$(date)] Allowed: MCP tool"
209
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
210
- exit 0
211
- fi
212
-
213
- # 只读工具不需要拦截
214
- case "$TOOL_NAME" in
215
- Read|Glob|Grep|LS|TaskList|TaskGet|TaskOutput|TaskStop|CronList|CronCreate|CronDelete|AskUserQuestion|Skill|ListMcpResourcesTool|EnterPlanMode|ExitPlanMode|WebSearch|WebFetch|NotebookEdit)
216
- log_debug "[$(date)] Allowed: read-only tool"
217
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
218
- exit 0
219
- ;;
220
- esac
221
-
222
- # 通过进程树匹配活跃项目(以 Claude 进程为准,不依赖 pwd)
223
- ACTIVE_INDEX="$HOME/.wecom-aibot-mcp/active-projects.json"
224
- log_debug "[$(date)] Checking active-projects index via PID tree (pid=$$, ppid=$PPID)"
225
-
226
- if [[ ! -f "$ACTIVE_INDEX" ]]; then
227
- log_debug "[$(date)] No active-projects index, exit 0"
228
- exit 0
229
- fi
230
-
231
- # 沿进程树向上查找,深度 8 层
232
- PROJECT_DIR=""
233
- SEARCH_PID=$PPID
234
- for i in {1..8}; do
235
- if [[ -z "$SEARCH_PID" ]] || [[ "$SEARCH_PID" -le 1 ]]; then
236
- break
237
- fi
238
- MATCH=$(jq -r --argjson p "$SEARCH_PID" '.[] | select(.pid==$p) | .projectDir' "$ACTIVE_INDEX" 2>/dev/null)
239
- if [[ -n "$MATCH" ]]; then
240
- PROJECT_DIR="$MATCH"
241
- log_debug "[$(date)] Found project via PID $SEARCH_PID (depth $i): $PROJECT_DIR"
242
- break
243
- fi
244
- SEARCH_PID=$(ps -o ppid= -p "$SEARCH_PID" 2>/dev/null | tr -d ' ')
245
- done
246
-
247
- if [[ -z "$PROJECT_DIR" ]]; then
248
- log_debug "[$(date)] No PID match in process tree, exit 0"
249
- exit 0
250
- fi
251
-
252
- CONFIG_FILE="$PROJECT_DIR/.claude/wecom-aibot.json"
253
- log_debug "[$(date)] Found project: $PROJECT_DIR"
254
-
255
- # 配置文件不存在,不在微信模式
256
- if [[ ! -f "$CONFIG_FILE" ]]; then
257
- log_debug "[$(date)] No wecom-aibot.json config, exit 0"
258
- exit 0
259
- fi
260
-
261
- # 检查 wechatMode 是否为 true(微信模式开关)
262
- WECHAT_MODE=$(jq -r '.wechatMode // false' "$CONFIG_FILE" 2>/dev/null)
263
- log_debug "[$(date)] wechatMode: $WECHAT_MODE"
264
- if [[ "$WECHAT_MODE" != "true" ]]; then
265
- log_debug "[$(date)] wechatMode not true, exit 0"
266
- exit 0
267
- fi
268
-
269
- # 确定 MCP Server 地址
270
- # channel 模式直接使用远程地址,http 模式先试本地再回退远程
271
- MODE=$(jq -r '.mode // "http"' "$CONFIG_FILE" 2>/dev/null)
272
- MCP_BASE_URL="http://127.0.0.1:$MCP_PORT"
273
- AUTH_ARGS=()
274
-
275
- _try_remote() {
276
- CLAUDE_JSON="$HOME/.claude.json"
277
- if [[ ! -f "$CLAUDE_JSON" ]]; then
278
- log_debug "[$(date)] No ~/.claude.json found, exit 0"
279
- exit 0
280
- fi
281
- REMOTE_URL=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_URL // empty' "$CLAUDE_JSON" 2>/dev/null)
282
- REMOTE_TOKEN=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_AUTH_TOKEN // empty' "$CLAUDE_JSON" 2>/dev/null)
283
- if [[ -z "$REMOTE_URL" ]]; then
284
- log_debug "[$(date)] No remote URL configured, exit 0"
285
- exit 0
286
- fi
287
- REMOTE_HEALTH=$(curl -s -m 5 \${REMOTE_TOKEN:+-H "Authorization: Bearer $REMOTE_TOKEN"} "$REMOTE_URL/health" 2>/dev/null)
288
- log_debug "[$(date)] Remote health check ($REMOTE_URL): $REMOTE_HEALTH"
289
- if echo "$REMOTE_HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
290
- MCP_BASE_URL="$REMOTE_URL"
291
- [[ -n "$REMOTE_TOKEN" ]] && AUTH_ARGS=(-H "Authorization: Bearer $REMOTE_TOKEN")
292
- log_debug "[$(date)] Using remote server: $MCP_BASE_URL"
293
- else
294
- log_debug "[$(date)] Remote health check failed, exit 0"
295
- exit 0
296
- fi
297
- }
298
-
299
- if [[ "$MODE" == "channel" ]]; then
300
- # channel 模式:直接使用远程地址,跳过本地检查
301
- log_debug "[$(date)] Channel mode, using remote server directly"
302
- _try_remote
303
- else
304
- # http 模式:本地优先,失败则尝试远程
305
- HEALTH=$(curl -s -m 2 "$MCP_BASE_URL/health" 2>/dev/null)
306
- log_debug "[$(date)] Local health check: $HEALTH"
307
- if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
308
- log_debug "[$(date)] Local server not available, trying remote channel config..."
309
- _try_remote
310
- fi
311
- fi
312
-
313
- # 读取当前项目使用的机器人名称和 ccId
314
- ROBOT_NAME=$(jq -r '.robotName // empty' "$CONFIG_FILE" 2>/dev/null)
315
- CC_ID=$(jq -r '.ccId // empty' "$CONFIG_FILE" 2>/dev/null)
316
-
317
- # 发送审批请求(使用 pwd 作为 projectDir)
318
- TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
319
- BODY=$(jq -n --arg tool_name "$TOOL_NAME" --argjson tool_input "$TOOL_INPUT" --arg project_dir "$PROJECT_DIR" --arg robot_name "$ROBOT_NAME" --arg cc_id "$CC_ID" \\
320
- '{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir,"robotName":$robot_name,"ccId":$cc_id}')
321
-
322
- log_debug "[$(date)] Sending approval request..."
323
- RESPONSE=$(curl -s -m 10 -X POST "$MCP_BASE_URL/approve" \\
324
- "\${AUTH_ARGS[@]}" \\
325
- -H "Content-Type: application/json" \\
326
- -d "$BODY")
327
-
328
- log_debug "[$(date)] Approval response: $RESPONSE"
329
- TASK_ID=$(echo "$RESPONSE" | jq -r '.taskId // empty')
330
- if [[ -z "$TASK_ID" ]]; then
331
- log_debug "[$(date)] No taskId, exit 0"
332
- exit 0
333
- fi
334
-
335
- log_debug "[$(date)] Waiting for approval, taskId: $TASK_ID"
336
-
337
- # 轮询审批结果(带超时:从配置读取)
338
- AUTO_APPROVE_TIMEOUT=$(jq -r '.autoApproveTimeout // 300' "$CONFIG_FILE" 2>/dev/null)
339
- # 超时时间(秒),转换为轮询次数(每次 sleep 2秒)
340
- # 使用向上取整补偿整数截断:MAX_POLL = ceil(timeout/2) = (timeout+1)/2
341
- MAX_POLL=$(( (AUTO_APPROVE_TIMEOUT + 1) / 2 ))
342
- if [[ $MAX_POLL -lt 1 ]]; then
343
- MAX_POLL=1
344
- fi
345
- POLL_COUNT=0
346
-
347
- log_debug "[$(date)] autoApproveTimeout: $AUTO_APPROVE_TIMEOUT seconds, MAX_POLL: $MAX_POLL (actual wait: ~$((MAX_POLL * 2))s)"
348
-
349
- while [[ $POLL_COUNT -lt $MAX_POLL ]]; do
350
- sleep 2
351
- POLL_COUNT=$((POLL_COUNT + 1))
352
-
353
- STATUS=$(curl -s -m 3 "\${AUTH_ARGS[@]}" "$MCP_BASE_URL/approval_status/$TASK_ID" 2>/dev/null)
354
- RESULT=$(echo "$STATUS" | jq -r '.result // empty')
355
- log_debug "[$(date)] Poll $POLL_COUNT/$MAX_POLL: result=$RESULT"
356
-
357
- if [[ "$RESULT" == "allow-once" || "$RESULT" == "allow-always" ]]; then
358
- log_debug "[$(date)] Approved by user"
359
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
360
- exit 0
361
- elif [[ "$RESULT" == "deny" ]]; then
362
- log_debug "[$(date)] Denied by user"
363
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"用户拒绝"}}}'
364
- exit 0
365
- fi
366
- done
367
-
368
- log_debug "[$(date)] Timeout reached, executing smart auto-approval"
369
-
370
- # 超时处理:必须立即决策,Claude Code 的 hook timeout 会杀掉阻塞进程。
371
- # 规则:删除命令→拒绝,项目内操作→允许,项目外操作→拒绝
372
-
373
- # 检查是否是删除命令(仅匹配命令行本身,不匹配 heredoc 内容)
374
- IS_DELETE=0
375
- if [[ "$TOOL_NAME" == "Bash" ]]; then
376
- # 只取命令的第一行(避免 heredoc 内容干扰)
377
- FIRST_LINE=$(echo "$TOOL_INPUT" | jq -r '.command // empty' | head -1)
378
- log_debug "[$(date)] Checking delete: FIRST_LINE=$FIRST_LINE"
379
- if [[ "$FIRST_LINE" == rm\\ * ]] || [[ "$FIRST_LINE" == rm ]] \\
380
- || echo "$FIRST_LINE" | grep -qE '(^|[;&|(] *)(rm |rmdir )'; then
381
- IS_DELETE=1
382
- fi
383
- fi
384
-
385
- log_debug "[$(date)] IS_DELETE: $IS_DELETE"
386
-
387
- # 删除操作 → 永远拒绝
388
- if [[ $IS_DELETE -eq 1 ]]; then
389
- log_debug "[$(date)] Auto-deny: delete operation"
390
- # 通知 MCP Server 发送微信消息
391
- curl -s -m 5 -X POST "$MCP_BASE_URL/approval_timeout/$TASK_ID" "\${AUTH_ARGS[@]}" -H "Content-Type: application/json" -d '{"result":"deny","reason":"超时自动拒绝:删除操作需人工确认"}' > /dev/null 2>&1 &
392
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"超时自动拒绝:删除操作需人工确认"}}}'
393
- exit 0
394
- fi
395
-
396
- # 检查操作路径是否在项目内
397
- IS_IN_PROJECT=0
398
-
399
- case "$TOOL_NAME" in
400
- Bash)
401
- CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
402
- EXEC_CWD=$(pwd)
403
- log_debug "[$(date)] Bash CMD=$CMD, EXEC_CWD=$EXEC_CWD"
404
- if [[ "$CMD" == *"$PROJECT_DIR"* ]]; then
405
- # 明确包含项目路径 → 项目内
406
- IS_IN_PROJECT=1
407
- elif echo "$CMD" | grep -qE '(^|[ \t])/[a-zA-Z0-9]'; then
408
- # 含有绝对路径:过滤掉项目路径和安全系统目录,看是否还有真正的项目外路径
409
- OUTSIDE=$(echo "$CMD" | grep -oE '(^| )/[a-zA-Z0-9][^ \t>|;&]*' | tr -d ' ' \
410
- | grep -v "^$PROJECT_DIR" \
411
- | grep -vE '^(/tmp/|/var/tmp/|/dev/null|/dev/stdin|/dev/stdout|/dev/stderr|/dev/fd/)')
412
- if [[ -z "$OUTSIDE" ]]; then
413
- # 绝对路径全是项目内或安全临时目录 → 以执行位置为准
414
- log_debug "[$(date)] Only safe abs paths, checking EXEC_CWD: $EXEC_CWD"
415
- if [[ "$EXEC_CWD" == "$PROJECT_DIR"* ]]; then
416
- IS_IN_PROJECT=1
417
- fi
418
- else
419
- log_debug "[$(date)] Outside abs path detected: $OUTSIDE"
420
- IS_IN_PROJECT=0
421
- fi
422
- else
423
- # 无绝对路径(相对路径或纯命令如 npm/git)→ 以执行位置为准
424
- log_debug "[$(date)] No absolute path, checking EXEC_CWD: $EXEC_CWD"
425
- if [[ "$EXEC_CWD" == "$PROJECT_DIR"* ]]; then
426
- IS_IN_PROJECT=1
427
- fi
428
- fi
429
- ;;
430
- Write|Edit)
431
- FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
432
- if [[ "$FILE_PATH" == "$PROJECT_DIR"* ]] || [[ "$FILE_PATH" != /* ]]; then
433
- IS_IN_PROJECT=1
434
- fi
435
- ;;
436
- *)
437
- FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .path // .directory // empty')
438
- if [[ -n "$FILE_PATH" ]]; then
439
- if [[ "$FILE_PATH" == "$PROJECT_DIR"* ]] || [[ "$FILE_PATH" != /* ]]; then
440
- IS_IN_PROJECT=1
441
- fi
442
- fi
443
- ;;
444
- esac
445
-
446
- log_debug "[$(date)] IS_IN_PROJECT: $IS_IN_PROJECT"
447
-
448
- # 根据项目内/外决策
449
- if [[ $IS_IN_PROJECT -eq 1 ]]; then
450
- log_debug "[$(date)] Auto-allow: project operation"
451
- # 通知 MCP Server 发送微信消息
452
- curl -s -m 5 -X POST "$MCP_BASE_URL/approval_timeout/$TASK_ID" "\${AUTH_ARGS[@]}" -H "Content-Type: application/json" -d '{"result":"allow-once","reason":"超时自动允许:项目内操作"}' > /dev/null 2>&1 &
453
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","message":"超时自动允许:项目内操作"}}}'
454
- else
455
- log_debug "[$(date)] Auto-deny: outside project"
456
- # 通知 MCP Server 发送微信消息
457
- curl -s -m 5 -X POST "$MCP_BASE_URL/approval_timeout/$TASK_ID" "\${AUTH_ARGS[@]}" -H "Content-Type: application/json" -d '{"result":"deny","reason":"超时自动拒绝:项目外操作需人工确认"}' > /dev/null 2>&1 &
458
- printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"超时自动拒绝:项目外操作需人工确认"}}}'
459
- fi
460
- `;
187
+ // v3.4.0: dist/hooks/ 拷贝预编译的 .js 替代旧 bash 模板
461
188
  ensureConfigDir();
462
- fs.writeFileSync(HOOK_SCRIPT_PATH, script, { mode: 0o755 });
189
+ const src = path.join(__dirname, 'hooks', 'permission-hook.js');
190
+ if (!fs.existsSync(src)) {
191
+ console.log('[config] ⚠️ 找不到 hook 源文件,跳过:', src);
192
+ return;
193
+ }
194
+ fs.copyFileSync(src, HOOK_SCRIPT_PATH);
195
+ try {
196
+ fs.chmodSync(HOOK_SCRIPT_PATH, 0o755);
197
+ }
198
+ catch { /* Windows 无 exec bit,忽略 */ }
199
+ // 清理 v3.3.x 及以前的 .sh 残留
200
+ if (fs.existsSync(HOOK_SCRIPT_PATH_LEGACY)) {
201
+ try {
202
+ fs.unlinkSync(HOOK_SCRIPT_PATH_LEGACY);
203
+ }
204
+ catch { /* ignore */ }
205
+ }
463
206
  console.log(`[config] Hook 脚本已写入: ${HOOK_SCRIPT_PATH}`);
464
207
  }
465
208
  // 生成并写入 Stop hook 脚本
466
209
  // HTTP 模式使用:阻止 Claude 停止,提示调用 get_pending_messages 恢复轮询
467
210
  function writeStopHookScript() {
468
- const script = `#!/bin/bash
469
- # wecom-aibot-mcp Stop hook
470
- # HTTP 模式使用:阻止 Claude 停止,提示调用 get_pending_messages 恢复轮询
471
- #
472
- # 固定端口: 18963
473
- # 只检查 $(pwd)/.claude/wecom-aibot.json 的 wechatMode 字段
474
-
475
- MCP_PORT=18963
476
-
477
- # 先保存输入(Stop 事件数据)
478
- INPUT=$(cat)
479
-
480
- # 日志输出:--debug 模式下输出到 stderr,否则静默
481
- DEBUG_FILE="$HOME/.wecom-aibot-mcp/debug"
482
- log_debug() {
483
- if [[ -f "$DEBUG_FILE" ]]; then
484
- echo "$1" >&2
485
- fi
486
- }
487
-
488
- log_debug "[$(date)] Stop hook called. INPUT: \${INPUT:0:200}"
489
-
490
- # 检查项目目录的微信模式配置文件
491
- PROJECT_DIR=$(pwd)
492
- CONFIG_FILE="$PROJECT_DIR/.claude/wecom-aibot.json"
493
-
494
- log_debug "[$(date)] Checking config: $CONFIG_FILE"
495
-
496
- # 配置文件不存在,不在微信模式,允许停止
497
- if [[ ! -f "$CONFIG_FILE" ]]; then
498
- log_debug "[$(date)] No config file, exit 0 (allow stop)"
499
- exit 0
500
- fi
501
-
502
- # 检查 wechatMode 是否为 true(微信模式开关)
503
- WECHAT_MODE=$(jq -r '.wechatMode // false' "$CONFIG_FILE" 2>/dev/null)
504
- log_debug "[$(date)] wechatMode: $WECHAT_MODE"
505
- if [[ "$WECHAT_MODE" != "true" ]]; then
506
- log_debug "[$(date)] wechatMode not true, exit 0 (allow stop)"
507
- exit 0
508
- fi
509
-
510
- # 确定 MCP Server 地址(本地优先,失败则尝试远程 channel 配置)
511
- MCP_BASE_URL="http://127.0.0.1:$MCP_PORT"
512
- AUTH_ARGS=()
513
-
514
- HEALTH=$(curl -s -m 2 "$MCP_BASE_URL/health" 2>/dev/null)
515
- log_debug "[$(date)] Local health check: $HEALTH"
516
- if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
517
- CLAUDE_JSON="$HOME/.claude.json"
518
- if [[ -f "$CLAUDE_JSON" ]]; then
519
- REMOTE_URL=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_URL // empty' "$CLAUDE_JSON" 2>/dev/null)
520
- REMOTE_TOKEN=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_AUTH_TOKEN // empty' "$CLAUDE_JSON" 2>/dev/null)
521
- if [[ -n "$REMOTE_URL" ]]; then
522
- REMOTE_HEALTH=$(curl -s -m 5 \${REMOTE_TOKEN:+-H "Authorization: Bearer $REMOTE_TOKEN"} "$REMOTE_URL/health" 2>/dev/null)
523
- if echo "$REMOTE_HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
524
- MCP_BASE_URL="$REMOTE_URL"
525
- [[ -n "$REMOTE_TOKEN" ]] && AUTH_ARGS=(-H "Authorization: Bearer $REMOTE_TOKEN")
526
- log_debug "[$(date)] Using remote server: $MCP_BASE_URL"
527
- else
528
- log_debug "[$(date)] MCP Server offline, exit 0 (allow stop)"
529
- exit 0
530
- fi
531
- else
532
- log_debug "[$(date)] MCP Server offline, exit 0 (allow stop)"
533
- exit 0
534
- fi
535
- else
536
- log_debug "[$(date)] MCP Server offline, exit 0 (allow stop)"
537
- exit 0
538
- fi
539
- fi
540
-
541
- # 获取 ccId
542
- CC_ID=$(jq -r '.ccId // empty' "$CONFIG_FILE" 2>/dev/null)
543
- log_debug "[$(date)] ccId: $CC_ID"
544
- if [[ -z "$CC_ID" ]]; then
545
- log_debug "[$(date)] No ccId in config, exit 0 (allow stop)"
546
- exit 0
547
- fi
548
-
549
- # 处于微信模式,需要恢复轮询
550
- # 使用 exit code 2 阻止停止,并提示 Claude 调用 MCP 工具
551
- log_debug "[$(date)] ✅ WeChat mode active, blocking stop to resume polling"
552
- log_debug "[$(date)] ccId=$CC_ID, will prompt Claude to call get_pending_messages"
553
- echo "任务已完成,请调用 mcp__wecom-aibot__get_pending_messages(cc_id=\"$CC_ID\", timeout_ms=30000) 恢复微信消息轮询" >&2
554
- exit 2
555
- `;
211
+ // v3.4.0: 同上,从 dist/hooks/ 拷贝预编译的 .js
556
212
  ensureConfigDir();
557
- fs.writeFileSync(STOP_HOOK_SCRIPT_PATH, script, { mode: 0o755 });
213
+ const src = path.join(__dirname, 'hooks', 'stop-hook.js');
214
+ if (!fs.existsSync(src)) {
215
+ console.log('[config] ⚠️ 找不到 stop-hook 源文件,跳过:', src);
216
+ return;
217
+ }
218
+ fs.copyFileSync(src, STOP_HOOK_SCRIPT_PATH);
219
+ try {
220
+ fs.chmodSync(STOP_HOOK_SCRIPT_PATH, 0o755);
221
+ }
222
+ catch { /* ignore */ }
223
+ if (fs.existsSync(STOP_HOOK_SCRIPT_PATH_LEGACY)) {
224
+ try {
225
+ fs.unlinkSync(STOP_HOOK_SCRIPT_PATH_LEGACY);
226
+ }
227
+ catch { /* ignore */ }
228
+ }
558
229
  console.log(`[config] Stop Hook 脚本已写入: ${STOP_HOOK_SCRIPT_PATH}`);
559
230
  }
560
231
  // 写入 MCP 工具权限到 Claude settings
@@ -16,7 +16,14 @@
16
16
  * 6. 超时(autoApproveTimeout)→ 智能策略:
17
17
  * - rm 命令 → 拒
18
18
  * - 项目内 → 允许,项目外 → 拒
19
+ *
20
+ * v3.4.0 起替代 permission-hook.sh:去掉 jq / bash / curl / ps / grep 外部依赖,
21
+ * 跨平台(Windows wmic / Unix ps),方便部署到没装 jq 的机器(如 jorry)。
19
22
  */
23
+ // v3.4.0: daemon HTTPS 用 Let's Encrypt 自签或过期时 fetch 会拒,hook 走 Bearer
24
+ // token 鉴权,TLS 只是传输层加密,跳过 cert 校验更稳。设到环境变量是为了影响
25
+ // 整个 node 进程的全局 fetch / undici 行为。
26
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
20
27
  import * as fs from 'fs';
21
28
  import * as path from 'path';
22
29
  import * as os from 'os';
@@ -4,7 +4,11 @@
4
4
  *
5
5
  * Claude 准备停止时触发。如果当前项目处于微信模式,输出 exit code 2 阻止停止,
6
6
  * 同时通过 stderr 提示 Claude 调用 get_pending_messages 恢复轮询。
7
+ *
8
+ * v3.4.0 起替代 stop-hook.sh:去掉 jq / bash / curl 依赖,跨平台。
7
9
  */
10
+ // v3.4.0: 同 permission-hook —— 跳过 TLS cert 校验(Bearer token 提供身份)
11
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
8
12
  import * as fs from 'fs';
9
13
  import * as path from 'path';
10
14
  import * as os from 'os';
package/dist/logger.js CHANGED
@@ -89,12 +89,14 @@ function write(level, msg, data) {
89
89
  // 写文件失败不抛
90
90
  }
91
91
  }
92
- // 控制台
92
+ // 控制台 —— **stdio MCP server 的 stdout 是 JSON-RPC 协议管道,绝不能写非协议内容**
93
+ // 否则 Claude TUI 解析失败 → 判定 disconnect → channel mcp 不断中断。
94
+ // 任何级别的日志一律走 stderr。
93
95
  if (level === 'error') {
94
96
  process.stderr.write(entry);
95
97
  }
96
98
  else if (debugMode) {
97
- process.stdout.write(entry);
99
+ process.stderr.write(entry);
98
100
  }
99
101
  }
100
102
  // 公共 API
@@ -116,6 +116,8 @@ export declare function validateConfig(config: Partial<ProjectConfig>): boolean;
116
116
  export declare function getProjectSettingsPath(projectDir: string): string;
117
117
  export declare const PERMISSION_HOOK_SCRIPT_PATH: string;
118
118
  export declare const STOP_HOOK_SCRIPT_PATH: string;
119
+ export declare const PERMISSION_HOOK_SCRIPT_PATH_LEGACY: string;
120
+ export declare const STOP_HOOK_SCRIPT_PATH_LEGACY: string;
119
121
  /**
120
122
  * 添加 PermissionRequest hook 到项目 settings.json
121
123
  */
@@ -251,16 +251,21 @@ export function getProjectSettingsPath(projectDir) {
251
251
  }
252
252
  // ============================================
253
253
  // Hook 脚本路径(统一定义)
254
+ // v3.4.0: hook 改为 Node.js 实现 —— 去 jq/bash/curl 外部依赖,跨平台
254
255
  // ============================================
255
256
  const CONFIG_DIR = path.join(os.homedir(), '.wecom-aibot-mcp');
256
- export const PERMISSION_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'permission-hook.sh');
257
- export const STOP_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'stop-hook.sh');
257
+ export const PERMISSION_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'permission-hook.mjs');
258
+ export const STOP_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'stop-hook.mjs');
259
+ // v3.4.0 前的旧路径,安装新版本时一并删掉残留
260
+ export const PERMISSION_HOOK_SCRIPT_PATH_LEGACY = path.join(CONFIG_DIR, 'permission-hook.sh');
261
+ export const STOP_HOOK_SCRIPT_PATH_LEGACY = path.join(CONFIG_DIR, 'stop-hook.sh');
258
262
  /**
259
263
  * PermissionRequest hook 配置
264
+ * v3.4.0+: command 改为 `node "..."`,双引号包路径以兼容含空格的 Windows 路径
260
265
  */
261
266
  const PERMISSION_HOOK = {
262
267
  matcher: '',
263
- hooks: [{ type: 'command', command: PERMISSION_HOOK_SCRIPT_PATH, timeout: 3600 }],
268
+ hooks: [{ type: 'command', command: `node "${PERMISSION_HOOK_SCRIPT_PATH}"`, timeout: 3600 }],
264
269
  };
265
270
  /**
266
271
  * Stop hook 配置
@@ -268,7 +273,7 @@ const PERMISSION_HOOK = {
268
273
  */
269
274
  const STOP_HOOK = {
270
275
  matcher: '',
271
- hooks: [{ type: 'command', command: STOP_HOOK_SCRIPT_PATH }],
276
+ hooks: [{ type: 'command', command: `node "${STOP_HOOK_SCRIPT_PATH}"` }],
272
277
  };
273
278
  /**
274
279
  * 进入微信模式时默认预批的 MCP 工具通配(避免每次都走 hook 增加延迟)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrs-soft/wecom-aibot-mcp",
3
- "version": "3.3.2",
3
+ "version": "3.4.0",
4
4
  "description": "企业微信智能机器人 MCP 客户端 - 连接 wecom-aibot-server daemon",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",