@vrs-soft/wecom-aibot-mcp 2.4.8 → 2.4.10
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 +38 -0
- package/dist/config-wizard.js +99 -37
- package/dist/project-config.d.ts +5 -0
- package/dist/project-config.js +34 -1
- package/dist/tools/index.js +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,6 +12,44 @@
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## 效果预览
|
|
16
|
+
|
|
17
|
+
<img src="docs/approval-card.png" width="320" alt="微信审批卡片" />
|
|
18
|
+
|
|
19
|
+
每次 Claude 执行敏感操作(Bash 命令、编辑文件等)时,企业微信会推送审批卡片,点击**允许**或**拒绝**即可实时控制执行权限。超时未响应时,根据配置自动代批(允许项目内操作,拒绝删除命令)。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 使用场景
|
|
24
|
+
|
|
25
|
+
### 场景一:离开电脑,微信远程监督
|
|
26
|
+
|
|
27
|
+
出门开会或离开座位时,告诉 Claude「现在开始通过微信联系」,进入微信模式后:
|
|
28
|
+
|
|
29
|
+
- Claude 执行每一步操作前发送审批请求到微信
|
|
30
|
+
- 你在手机上点击**允许 / 拒绝**,Claude 实时响应
|
|
31
|
+
- 设置超时自动审批(`autoApproveTimeout`),无人值守时自动处理项目内操作
|
|
32
|
+
|
|
33
|
+
### 场景二:微信直接下达任务
|
|
34
|
+
|
|
35
|
+
不在电脑前,直接在企业微信给机器人发消息:
|
|
36
|
+
|
|
37
|
+
- 「帮我跑一下单元测试,把结果发给我」
|
|
38
|
+
- 「把 src/index.ts 里的 TODO 都处理掉」
|
|
39
|
+
- 「最近有什么错误日志?」
|
|
40
|
+
|
|
41
|
+
Claude 执行完成后自动回复进度和结果到微信。
|
|
42
|
+
|
|
43
|
+
### 场景三:团队共享机器人,群聊协作
|
|
44
|
+
|
|
45
|
+
在企业微信群中 @机器人,多个成员可以同时:
|
|
46
|
+
|
|
47
|
+
- 查询项目状态
|
|
48
|
+
- 触发 CI 任务
|
|
49
|
+
- 审批自己负责的操作(审批请求精确路由到对应 Claude 窗口)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
15
53
|
## 前置条件
|
|
16
54
|
|
|
17
55
|
企业微信管理后台创建智能机器人,连接方式选「使用长连接」,记录 **Bot ID** 和 **Secret** 以及 **DocURL**(文档url)。
|
package/dist/config-wizard.js
CHANGED
|
@@ -448,15 +448,42 @@ case "$TOOL_NAME" in
|
|
|
448
448
|
;;
|
|
449
449
|
esac
|
|
450
450
|
|
|
451
|
-
#
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -526,7 +564,7 @@ fi
|
|
|
526
564
|
log_debug "[$(date)] Waiting for approval, taskId: $TASK_ID"
|
|
527
565
|
|
|
528
566
|
# 轮询审批结果(带超时:从配置读取)
|
|
529
|
-
AUTO_APPROVE_TIMEOUT=$(jq -r '.autoApproveTimeout //
|
|
567
|
+
AUTO_APPROVE_TIMEOUT=$(jq -r '.autoApproveTimeout // 300' "$CONFIG_FILE" 2>/dev/null)
|
|
530
568
|
# 超时时间(秒),转换为轮询次数(每次 sleep 2秒)
|
|
531
569
|
# 使用向上取整补偿整数截断:MAX_POLL = ceil(timeout/2) = (timeout+1)/2
|
|
532
570
|
MAX_POLL=$(( (AUTO_APPROVE_TIMEOUT + 1) / 2 ))
|
|
@@ -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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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"* ]]
|
|
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)
|
package/dist/project-config.d.ts
CHANGED
|
@@ -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;
|
package/dist/project-config.js
CHANGED
|
@@ -260,7 +260,7 @@ export const STOP_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'stop-hook.sh');
|
|
|
260
260
|
*/
|
|
261
261
|
const PERMISSION_HOOK = {
|
|
262
262
|
matcher: '',
|
|
263
|
-
hooks: [{ type: 'command', command: PERMISSION_HOOK_SCRIPT_PATH }],
|
|
263
|
+
hooks: [{ type: 'command', command: PERMISSION_HOOK_SCRIPT_PATH, timeout: 3600 }],
|
|
264
264
|
};
|
|
265
265
|
/**
|
|
266
266
|
* Stop hook 配置
|
|
@@ -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
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -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) {
|
|
@@ -357,7 +357,7 @@ npx @vrs-soft/wecom-aibot-mcp
|
|
|
357
357
|
mode: z.enum(['channel', 'http']).optional().default('http')
|
|
358
358
|
.describe('运行模式:channel=SSE推送(推荐),http=轮询(兼容)'),
|
|
359
359
|
auto_approve: z.boolean().optional().default(true).describe('超时自动审批(默认 true)'),
|
|
360
|
-
auto_approve_timeout: z.number().optional().default(
|
|
360
|
+
auto_approve_timeout: z.number().optional().default(300).describe('自动审批超时时间(秒,默认 300 即 5 分钟)'),
|
|
361
361
|
}, async ({ agent_name, cc_id, robot_id, project_dir, mode, auto_approve, auto_approve_timeout }, extra) => {
|
|
362
362
|
// 获取项目目录
|
|
363
363
|
const projectDir = project_dir || process.cwd();
|
|
@@ -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
|