@vrs-soft/wecom-aibot-mcp 2.4.8 → 2.4.9

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.
@@ -448,15 +448,42 @@ case "$TOOL_NAME" in
448
448
  ;;
449
449
  esac
450
450
 
451
- # 检查项目目录的微信模式配置文件
452
- PROJECT_DIR=$(pwd)
453
- CONFIG_FILE="$PROJECT_DIR/.claude/wecom-aibot.json"
451
+ # 通过进程树匹配活跃项目(以 Claude 进程为准,不依赖 pwd)
452
+ ACTIVE_INDEX="$HOME/.wecom-aibot-mcp/active-projects.json"
453
+ log_debug "[$(date)] Checking active-projects index via PID tree (pid=$$, ppid=$PPID)"
454
454
 
455
- log_debug "[$(date)] Checking config: $CONFIG_FILE"
455
+ if [[ ! -f "$ACTIVE_INDEX" ]]; then
456
+ log_debug "[$(date)] No active-projects index, exit 0"
457
+ exit 0
458
+ fi
459
+
460
+ # 沿进程树向上查找,深度 8 层
461
+ PROJECT_DIR=""
462
+ SEARCH_PID=$PPID
463
+ for i in {1..8}; do
464
+ if [[ -z "$SEARCH_PID" ]] || [[ "$SEARCH_PID" -le 1 ]]; then
465
+ break
466
+ fi
467
+ MATCH=$(jq -r --argjson p "$SEARCH_PID" '.[] | select(.pid==$p) | .projectDir' "$ACTIVE_INDEX" 2>/dev/null)
468
+ if [[ -n "$MATCH" ]]; then
469
+ PROJECT_DIR="$MATCH"
470
+ log_debug "[$(date)] Found project via PID $SEARCH_PID (depth $i): $PROJECT_DIR"
471
+ break
472
+ fi
473
+ SEARCH_PID=$(ps -o ppid= -p "$SEARCH_PID" 2>/dev/null | tr -d ' ')
474
+ done
475
+
476
+ if [[ -z "$PROJECT_DIR" ]]; then
477
+ log_debug "[$(date)] No PID match in process tree, exit 0"
478
+ exit 0
479
+ fi
480
+
481
+ CONFIG_FILE="$PROJECT_DIR/.claude/wecom-aibot.json"
482
+ log_debug "[$(date)] Found project: $PROJECT_DIR"
456
483
 
457
484
  # 配置文件不存在,不在微信模式
458
485
  if [[ ! -f "$CONFIG_FILE" ]]; then
459
- log_debug "[$(date)] No config file, exit 0"
486
+ log_debug "[$(date)] No wecom-aibot.json config, exit 0"
460
487
  exit 0
461
488
  fi
462
489
 
@@ -468,37 +495,48 @@ if [[ "$WECHAT_MODE" != "true" ]]; then
468
495
  exit 0
469
496
  fi
470
497
 
471
- # 确定 MCP Server 地址(本地优先,失败则尝试远程 channel 配置)
498
+ # 确定 MCP Server 地址
499
+ # channel 模式直接使用远程地址,http 模式先试本地再回退远程
500
+ MODE=$(jq -r '.mode // "http"' "$CONFIG_FILE" 2>/dev/null)
472
501
  MCP_BASE_URL="http://127.0.0.1:$MCP_PORT"
473
502
  AUTH_ARGS=()
474
503
 
475
- HEALTH=$(curl -s -m 2 "$MCP_BASE_URL/health" 2>/dev/null)
476
- log_debug "[$(date)] Local health check: $HEALTH"
477
- if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
478
- log_debug "[$(date)] Local server not available, trying remote channel config..."
504
+ _try_remote() {
479
505
  CLAUDE_JSON="$HOME/.claude.json"
480
- if [[ -f "$CLAUDE_JSON" ]]; then
481
- REMOTE_URL=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_URL // empty' "$CLAUDE_JSON" 2>/dev/null)
482
- REMOTE_TOKEN=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_AUTH_TOKEN // empty' "$CLAUDE_JSON" 2>/dev/null)
483
- if [[ -n "$REMOTE_URL" ]]; then
484
- REMOTE_HEALTH=$(curl -s -m 5 \${REMOTE_TOKEN:+-H "Authorization: Bearer $REMOTE_TOKEN"} "$REMOTE_URL/health" 2>/dev/null)
485
- log_debug "[$(date)] Remote health check ($REMOTE_URL): $REMOTE_HEALTH"
486
- if echo "$REMOTE_HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
487
- MCP_BASE_URL="$REMOTE_URL"
488
- [[ -n "$REMOTE_TOKEN" ]] && AUTH_ARGS=(-H "Authorization: Bearer $REMOTE_TOKEN")
489
- log_debug "[$(date)] Using remote server: $MCP_BASE_URL"
490
- else
491
- log_debug "[$(date)] Remote health check failed, exit 0"
492
- exit 0
493
- fi
494
- else
495
- log_debug "[$(date)] No remote URL configured, exit 0"
496
- exit 0
497
- fi
498
- else
506
+ if [[ ! -f "$CLAUDE_JSON" ]]; then
499
507
  log_debug "[$(date)] No ~/.claude.json found, exit 0"
500
508
  exit 0
501
509
  fi
510
+ REMOTE_URL=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_URL // empty' "$CLAUDE_JSON" 2>/dev/null)
511
+ REMOTE_TOKEN=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_AUTH_TOKEN // empty' "$CLAUDE_JSON" 2>/dev/null)
512
+ if [[ -z "$REMOTE_URL" ]]; then
513
+ log_debug "[$(date)] No remote URL configured, exit 0"
514
+ exit 0
515
+ fi
516
+ REMOTE_HEALTH=$(curl -s -m 5 \${REMOTE_TOKEN:+-H "Authorization: Bearer $REMOTE_TOKEN"} "$REMOTE_URL/health" 2>/dev/null)
517
+ log_debug "[$(date)] Remote health check ($REMOTE_URL): $REMOTE_HEALTH"
518
+ if echo "$REMOTE_HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
519
+ MCP_BASE_URL="$REMOTE_URL"
520
+ [[ -n "$REMOTE_TOKEN" ]] && AUTH_ARGS=(-H "Authorization: Bearer $REMOTE_TOKEN")
521
+ log_debug "[$(date)] Using remote server: $MCP_BASE_URL"
522
+ else
523
+ log_debug "[$(date)] Remote health check failed, exit 0"
524
+ exit 0
525
+ fi
526
+ }
527
+
528
+ if [[ "$MODE" == "channel" ]]; then
529
+ # channel 模式:直接使用远程地址,跳过本地检查
530
+ log_debug "[$(date)] Channel mode, using remote server directly"
531
+ _try_remote
532
+ else
533
+ # http 模式:本地优先,失败则尝试远程
534
+ HEALTH=$(curl -s -m 2 "$MCP_BASE_URL/health" 2>/dev/null)
535
+ log_debug "[$(date)] Local health check: $HEALTH"
536
+ if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
537
+ log_debug "[$(date)] Local server not available, trying remote channel config..."
538
+ _try_remote
539
+ fi
502
540
  fi
503
541
 
504
542
  # 读取当前项目使用的机器人名称和 ccId
@@ -588,12 +626,14 @@ fi
588
626
  # 规则:删除命令→拒绝,项目内操作→允许,项目外操作→拒绝
589
627
  log_debug "[$(date)] Executing smart auto-approval"
590
628
 
591
- # 检查是否是删除命令
629
+ # 检查是否是删除命令(仅匹配命令行本身,不匹配 heredoc 内容)
592
630
  IS_DELETE=0
593
631
  if [[ "$TOOL_NAME" == "Bash" ]]; then
594
- CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
595
- log_debug "[$(date)] Checking delete: CMD=$CMD"
596
- if [[ "$CMD" == rm* ]] || [[ "$CMD" == *" rm "* ]] || [[ "$CMD" == *"-rf"* ]]; then
632
+ # 只取命令的第一行(避免 heredoc 内容干扰)
633
+ FIRST_LINE=$(echo "$TOOL_INPUT" | jq -r '.command // empty' | head -1)
634
+ log_debug "[$(date)] Checking delete: FIRST_LINE=$FIRST_LINE"
635
+ if [[ "$FIRST_LINE" == rm\\ * ]] || [[ "$FIRST_LINE" == rm ]] \\
636
+ || echo "$FIRST_LINE" | grep -qE '(^|[;&|(] *)(rm |rmdir )'; then
597
637
  IS_DELETE=1
598
638
  fi
599
639
  fi
@@ -615,10 +655,32 @@ IS_IN_PROJECT=0
615
655
  case "$TOOL_NAME" in
616
656
  Bash)
617
657
  CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
618
- # 只有明确在项目目录内操作才认为是项目内操作
619
- # 相对路径 ./ 或包含项目目录路径
620
- if [[ "$CMD" == *"$PROJECT_DIR"* ]] || [[ "$CMD" == ./* ]]; then
658
+ EXEC_CWD=$(pwd)
659
+ log_debug "[$(date)] Bash CMD=$CMD, EXEC_CWD=$EXEC_CWD"
660
+ if [[ "$CMD" == *"$PROJECT_DIR"* ]]; then
661
+ # 明确包含项目路径 → 项目内
621
662
  IS_IN_PROJECT=1
663
+ elif echo "$CMD" | grep -qE '(^|[ \t])/[a-zA-Z0-9]'; then
664
+ # 含有绝对路径:过滤掉项目路径和安全系统目录,看是否还有真正的项目外路径
665
+ OUTSIDE=$(echo "$CMD" | grep -oE '(^| )/[a-zA-Z0-9][^ \t>|;&]*' | tr -d ' ' \
666
+ | grep -v "^$PROJECT_DIR" \
667
+ | grep -vE '^(/tmp/|/var/tmp/|/dev/null|/dev/stdin|/dev/stdout|/dev/stderr|/dev/fd/)')
668
+ if [[ -z "$OUTSIDE" ]]; then
669
+ # 绝对路径全是项目内或安全临时目录 → 以执行位置为准
670
+ log_debug "[$(date)] Only safe abs paths, checking EXEC_CWD: $EXEC_CWD"
671
+ if [[ "$EXEC_CWD" == "$PROJECT_DIR"* ]]; then
672
+ IS_IN_PROJECT=1
673
+ fi
674
+ else
675
+ log_debug "[$(date)] Outside abs path detected: $OUTSIDE"
676
+ IS_IN_PROJECT=0
677
+ fi
678
+ else
679
+ # 无绝对路径(相对路径或纯命令如 npm/git)→ 以执行位置为准
680
+ log_debug "[$(date)] No absolute path, checking EXEC_CWD: $EXEC_CWD"
681
+ if [[ "$EXEC_CWD" == "$PROJECT_DIR"* ]]; then
682
+ IS_IN_PROJECT=1
683
+ fi
622
684
  fi
623
685
  ;;
624
686
  Write|Edit)
@@ -20,6 +20,7 @@ export interface WechatModeConfig {
20
20
  autoApprove?: boolean;
21
21
  autoApproveTimeout?: number;
22
22
  heartbeatJobId?: string;
23
+ mode?: 'channel' | 'http';
23
24
  }
24
25
  /**
25
26
  * 获取项目配置文件路径
@@ -147,3 +148,7 @@ export declare function removeStopHook(projectDir: string): {
147
148
  path: string;
148
149
  existed: boolean;
149
150
  };
151
+ /** 进入微信模式时注册 PID → projectDir */
152
+ export declare function registerActiveProject(claudePid: number, projectDir: string): void;
153
+ /** 退出微信模式时注销 */
154
+ export declare function unregisterActiveProject(projectDir: string): void;
@@ -405,3 +405,36 @@ export function removeStopHook(projectDir) {
405
405
  return { success: false, path: settingsPath, existed: false };
406
406
  }
407
407
  }
408
+ // ============================================================
409
+ // 活跃项目索引(PID → projectDir,供 permission hook 使用)
410
+ // ============================================================
411
+ const ACTIVE_PROJECTS_FILE = path.join(os.homedir(), '.wecom-aibot-mcp', 'active-projects.json');
412
+ function readActiveProjects() {
413
+ if (!fs.existsSync(ACTIVE_PROJECTS_FILE))
414
+ return [];
415
+ try {
416
+ return JSON.parse(fs.readFileSync(ACTIVE_PROJECTS_FILE, 'utf-8'));
417
+ }
418
+ catch {
419
+ return [];
420
+ }
421
+ }
422
+ function writeActiveProjects(entries) {
423
+ const dir = path.dirname(ACTIVE_PROJECTS_FILE);
424
+ if (!fs.existsSync(dir))
425
+ fs.mkdirSync(dir, { recursive: true });
426
+ fs.writeFileSync(ACTIVE_PROJECTS_FILE, JSON.stringify(entries, null, 2));
427
+ }
428
+ /** 进入微信模式时注册 PID → projectDir */
429
+ export function registerActiveProject(claudePid, projectDir) {
430
+ const entries = readActiveProjects().filter(e => e.projectDir !== projectDir);
431
+ entries.push({ pid: claudePid, projectDir });
432
+ writeActiveProjects(entries);
433
+ logger.log(`[project-config] 注册活跃项目: pid=${claudePid} projectDir=${projectDir}`);
434
+ }
435
+ /** 退出微信模式时注销 */
436
+ export function unregisterActiveProject(projectDir) {
437
+ const entries = readActiveProjects().filter(e => e.projectDir !== projectDir);
438
+ writeActiveProjects(entries);
439
+ logger.log(`[project-config] 注销活跃项目: ${projectDir}`);
440
+ }
@@ -25,7 +25,7 @@ import { callDocTool } from '../doc-proxy.js';
25
25
  import { connectRobot, disconnectRobot, getClient, getConnectionState, } from '../connection-manager.js';
26
26
  import { registerCcId, unregisterCcId, getRobotByCcId, getProjectDirByCcId, generateCcId, } from '../http-server.js';
27
27
  import { subscribeWecomMessageByCcId } from '../message-bus.js';
28
- import { updateWechatModeConfig, loadWechatModeConfig, addPermissionHook, removePermissionHook, addStopHook, removeStopHook } from '../project-config.js';
28
+ import { updateWechatModeConfig, loadWechatModeConfig, addPermissionHook, removePermissionHook, addStopHook, removeStopHook, registerActiveProject, unregisterActiveProject } from '../project-config.js';
29
29
  import { logger } from '../logger.js';
30
30
  // 辅助函数:从 ccId 获取客户端
31
31
  async function getConnectedClient(ccId) {
@@ -431,7 +431,10 @@ npx @vrs-soft/wecom-aibot-mcp
431
431
  ccId: finalCcId,
432
432
  autoApprove: auto_approve,
433
433
  autoApproveTimeout: auto_approve_timeout,
434
+ mode,
434
435
  });
436
+ // 注册 PID → projectDir(供 permission hook 通过进程树匹配项目)
437
+ registerActiveProject(process.ppid ?? process.pid, projectDir);
435
438
  // 安装 skill 到项目本地(支持远程部署 MCP)
436
439
  const skillResult = installSkill(projectDir);
437
440
  // 添加 PermissionRequest hook 到项目 settings.json
@@ -486,9 +489,10 @@ npx @vrs-soft/wecom-aibot-mcp
486
489
  }
487
490
  // 断开连接
488
491
  disconnectRobot(robotName);
489
- // 更新项目配置文件中的 wechatMode 为 false
492
+ // 更新项目配置文件中的 wechatMode 为 false,注销 PID 索引
490
493
  const projectDir = project_dir || process.cwd();
491
494
  updateWechatModeConfig(projectDir, { wechatMode: false });
495
+ unregisterActiveProject(projectDir);
492
496
  // 删除 PermissionRequest hook 从项目 settings.json
493
497
  const hookResult = removePermissionHook(projectDir);
494
498
  // 删除 Stop hook 从项目 settings.json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vrs-soft/wecom-aibot-mcp",
3
- "version": "2.4.8",
3
+ "version": "2.4.9",
4
4
  "description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",