@vrs-soft/wecom-aibot-mcp 3.3.3 → 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.
package/dist/channel-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
// ============================================
|
package/dist/config-wizard.js
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
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,389 +184,48 @@ export function uninstall() {
|
|
|
179
184
|
}
|
|
180
185
|
// 生成并写入 hook 脚本(HTTP Transport 版本)
|
|
181
186
|
function writeHookScript() {
|
|
182
|
-
|
|
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
|
-
# v3.3.3: 远端 daemon 用自签 / Let's Encrypt cert 时若过期或被信任链拒绝,会让 curl 静默失败。
|
|
275
|
-
# hook 已经用 Bearer token 鉴权,TLS 只是传输加密,因此 https URL 一律加 -k 跳过 cert 校验。
|
|
276
|
-
TLS_OPTS=()
|
|
277
|
-
|
|
278
|
-
_try_remote() {
|
|
279
|
-
CLAUDE_JSON="$HOME/.claude.json"
|
|
280
|
-
if [[ ! -f "$CLAUDE_JSON" ]]; then
|
|
281
|
-
log_debug "[$(date)] No ~/.claude.json found, exit 0"
|
|
282
|
-
exit 0
|
|
283
|
-
fi
|
|
284
|
-
REMOTE_URL=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_URL // empty' "$CLAUDE_JSON" 2>/dev/null)
|
|
285
|
-
REMOTE_TOKEN=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_AUTH_TOKEN // empty' "$CLAUDE_JSON" 2>/dev/null)
|
|
286
|
-
if [[ -z "$REMOTE_URL" ]]; then
|
|
287
|
-
log_debug "[$(date)] No remote URL configured, exit 0"
|
|
288
|
-
exit 0
|
|
289
|
-
fi
|
|
290
|
-
# v3.3.3: 远端 https 时 -k 跳 cert 校验(cert 过期/不可信不再卡死 hook)
|
|
291
|
-
if [[ "$REMOTE_URL" == https://* ]]; then
|
|
292
|
-
TLS_OPTS=(-k)
|
|
293
|
-
fi
|
|
294
|
-
REMOTE_HEALTH=$(curl -s -m 5 "\${TLS_OPTS[@]}" \${REMOTE_TOKEN:+-H "Authorization: Bearer $REMOTE_TOKEN"} "$REMOTE_URL/health" 2>/dev/null)
|
|
295
|
-
log_debug "[$(date)] Remote health check ($REMOTE_URL): $REMOTE_HEALTH"
|
|
296
|
-
if echo "$REMOTE_HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
|
|
297
|
-
MCP_BASE_URL="$REMOTE_URL"
|
|
298
|
-
[[ -n "$REMOTE_TOKEN" ]] && AUTH_ARGS=(-H "Authorization: Bearer $REMOTE_TOKEN")
|
|
299
|
-
log_debug "[$(date)] Using remote server: $MCP_BASE_URL"
|
|
300
|
-
else
|
|
301
|
-
log_debug "[$(date)] Remote health check failed, exit 0"
|
|
302
|
-
exit 0
|
|
303
|
-
fi
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if [[ "$MODE" == "channel" ]]; then
|
|
307
|
-
# channel 模式:直接使用远程地址,跳过本地检查
|
|
308
|
-
log_debug "[$(date)] Channel mode, using remote server directly"
|
|
309
|
-
_try_remote
|
|
310
|
-
else
|
|
311
|
-
# http 模式:本地优先,失败则尝试远程
|
|
312
|
-
HEALTH=$(curl -s -m 2 "$MCP_BASE_URL/health" 2>/dev/null)
|
|
313
|
-
log_debug "[$(date)] Local health check: $HEALTH"
|
|
314
|
-
if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
|
|
315
|
-
log_debug "[$(date)] Local server not available, trying remote channel config..."
|
|
316
|
-
_try_remote
|
|
317
|
-
fi
|
|
318
|
-
fi
|
|
319
|
-
|
|
320
|
-
# 读取当前项目使用的机器人名称和 ccId
|
|
321
|
-
ROBOT_NAME=$(jq -r '.robotName // empty' "$CONFIG_FILE" 2>/dev/null)
|
|
322
|
-
CC_ID=$(jq -r '.ccId // empty' "$CONFIG_FILE" 2>/dev/null)
|
|
323
|
-
|
|
324
|
-
# 发送审批请求(使用 pwd 作为 projectDir)
|
|
325
|
-
TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
|
|
326
|
-
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" \\
|
|
327
|
-
'{"tool_name":$tool_name,"tool_input":$tool_input,"projectDir":$project_dir,"robotName":$robot_name,"ccId":$cc_id}')
|
|
328
|
-
|
|
329
|
-
log_debug "[$(date)] Sending approval request..."
|
|
330
|
-
RESPONSE=$(curl -s -m 10 "\${TLS_OPTS[@]}" -X POST "$MCP_BASE_URL/approve" \\
|
|
331
|
-
"\${AUTH_ARGS[@]}" \\
|
|
332
|
-
-H "Content-Type: application/json" \\
|
|
333
|
-
-d "$BODY")
|
|
334
|
-
|
|
335
|
-
log_debug "[$(date)] Approval response: $RESPONSE"
|
|
336
|
-
TASK_ID=$(echo "$RESPONSE" | jq -r '.taskId // empty')
|
|
337
|
-
if [[ -z "$TASK_ID" ]]; then
|
|
338
|
-
log_debug "[$(date)] No taskId, exit 0"
|
|
339
|
-
exit 0
|
|
340
|
-
fi
|
|
341
|
-
|
|
342
|
-
log_debug "[$(date)] Waiting for approval, taskId: $TASK_ID"
|
|
343
|
-
|
|
344
|
-
# 轮询审批结果(带超时:从配置读取)
|
|
345
|
-
AUTO_APPROVE_TIMEOUT=$(jq -r '.autoApproveTimeout // 300' "$CONFIG_FILE" 2>/dev/null)
|
|
346
|
-
# 超时时间(秒),转换为轮询次数(每次 sleep 2秒)
|
|
347
|
-
# 使用向上取整补偿整数截断:MAX_POLL = ceil(timeout/2) = (timeout+1)/2
|
|
348
|
-
MAX_POLL=$(( (AUTO_APPROVE_TIMEOUT + 1) / 2 ))
|
|
349
|
-
if [[ $MAX_POLL -lt 1 ]]; then
|
|
350
|
-
MAX_POLL=1
|
|
351
|
-
fi
|
|
352
|
-
POLL_COUNT=0
|
|
353
|
-
|
|
354
|
-
log_debug "[$(date)] autoApproveTimeout: $AUTO_APPROVE_TIMEOUT seconds, MAX_POLL: $MAX_POLL (actual wait: ~$((MAX_POLL * 2))s)"
|
|
355
|
-
|
|
356
|
-
while [[ $POLL_COUNT -lt $MAX_POLL ]]; do
|
|
357
|
-
sleep 2
|
|
358
|
-
POLL_COUNT=$((POLL_COUNT + 1))
|
|
359
|
-
|
|
360
|
-
STATUS=$(curl -s -m 3 "\${TLS_OPTS[@]}" "\${AUTH_ARGS[@]}" "$MCP_BASE_URL/approval_status/$TASK_ID" 2>/dev/null)
|
|
361
|
-
RESULT=$(echo "$STATUS" | jq -r '.result // empty')
|
|
362
|
-
log_debug "[$(date)] Poll $POLL_COUNT/$MAX_POLL: result=$RESULT"
|
|
363
|
-
|
|
364
|
-
if [[ "$RESULT" == "allow-once" || "$RESULT" == "allow-always" ]]; then
|
|
365
|
-
log_debug "[$(date)] Approved by user"
|
|
366
|
-
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}'
|
|
367
|
-
exit 0
|
|
368
|
-
elif [[ "$RESULT" == "deny" ]]; then
|
|
369
|
-
log_debug "[$(date)] Denied by user"
|
|
370
|
-
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"用户拒绝"}}}'
|
|
371
|
-
exit 0
|
|
372
|
-
fi
|
|
373
|
-
done
|
|
374
|
-
|
|
375
|
-
log_debug "[$(date)] Timeout reached, executing smart auto-approval"
|
|
376
|
-
|
|
377
|
-
# 超时处理:必须立即决策,Claude Code 的 hook timeout 会杀掉阻塞进程。
|
|
378
|
-
# 规则:删除命令→拒绝,项目内操作→允许,项目外操作→拒绝
|
|
379
|
-
|
|
380
|
-
# 检查是否是删除命令(仅匹配命令行本身,不匹配 heredoc 内容)
|
|
381
|
-
IS_DELETE=0
|
|
382
|
-
if [[ "$TOOL_NAME" == "Bash" ]]; then
|
|
383
|
-
# 只取命令的第一行(避免 heredoc 内容干扰)
|
|
384
|
-
FIRST_LINE=$(echo "$TOOL_INPUT" | jq -r '.command // empty' | head -1)
|
|
385
|
-
log_debug "[$(date)] Checking delete: FIRST_LINE=$FIRST_LINE"
|
|
386
|
-
if [[ "$FIRST_LINE" == rm\\ * ]] || [[ "$FIRST_LINE" == rm ]] \\
|
|
387
|
-
|| echo "$FIRST_LINE" | grep -qE '(^|[;&|(] *)(rm |rmdir )'; then
|
|
388
|
-
IS_DELETE=1
|
|
389
|
-
fi
|
|
390
|
-
fi
|
|
391
|
-
|
|
392
|
-
log_debug "[$(date)] IS_DELETE: $IS_DELETE"
|
|
393
|
-
|
|
394
|
-
# 删除操作 → 永远拒绝
|
|
395
|
-
if [[ $IS_DELETE -eq 1 ]]; then
|
|
396
|
-
log_debug "[$(date)] Auto-deny: delete operation"
|
|
397
|
-
# 通知 MCP Server 发送微信消息
|
|
398
|
-
curl -s -m 5 "\${TLS_OPTS[@]}" -X POST "$MCP_BASE_URL/approval_timeout/$TASK_ID" "\${AUTH_ARGS[@]}" -H "Content-Type: application/json" -d '{"result":"deny","reason":"超时自动拒绝:删除操作需人工确认"}' > /dev/null 2>&1 &
|
|
399
|
-
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"超时自动拒绝:删除操作需人工确认"}}}'
|
|
400
|
-
exit 0
|
|
401
|
-
fi
|
|
402
|
-
|
|
403
|
-
# 检查操作路径是否在项目内
|
|
404
|
-
IS_IN_PROJECT=0
|
|
405
|
-
|
|
406
|
-
case "$TOOL_NAME" in
|
|
407
|
-
Bash)
|
|
408
|
-
CMD=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
|
|
409
|
-
EXEC_CWD=$(pwd)
|
|
410
|
-
log_debug "[$(date)] Bash CMD=$CMD, EXEC_CWD=$EXEC_CWD"
|
|
411
|
-
if [[ "$CMD" == *"$PROJECT_DIR"* ]]; then
|
|
412
|
-
# 明确包含项目路径 → 项目内
|
|
413
|
-
IS_IN_PROJECT=1
|
|
414
|
-
elif echo "$CMD" | grep -qE '(^|[ \t])/[a-zA-Z0-9]'; then
|
|
415
|
-
# 含有绝对路径:过滤掉项目路径和安全系统目录,看是否还有真正的项目外路径
|
|
416
|
-
OUTSIDE=$(echo "$CMD" | grep -oE '(^| )/[a-zA-Z0-9][^ \t>|;&]*' | tr -d ' ' \
|
|
417
|
-
| grep -v "^$PROJECT_DIR" \
|
|
418
|
-
| grep -vE '^(/tmp/|/var/tmp/|/dev/null|/dev/stdin|/dev/stdout|/dev/stderr|/dev/fd/)')
|
|
419
|
-
if [[ -z "$OUTSIDE" ]]; then
|
|
420
|
-
# 绝对路径全是项目内或安全临时目录 → 以执行位置为准
|
|
421
|
-
log_debug "[$(date)] Only safe abs paths, checking EXEC_CWD: $EXEC_CWD"
|
|
422
|
-
if [[ "$EXEC_CWD" == "$PROJECT_DIR"* ]]; then
|
|
423
|
-
IS_IN_PROJECT=1
|
|
424
|
-
fi
|
|
425
|
-
else
|
|
426
|
-
log_debug "[$(date)] Outside abs path detected: $OUTSIDE"
|
|
427
|
-
IS_IN_PROJECT=0
|
|
428
|
-
fi
|
|
429
|
-
else
|
|
430
|
-
# 无绝对路径(相对路径或纯命令如 npm/git)→ 以执行位置为准
|
|
431
|
-
log_debug "[$(date)] No absolute path, checking EXEC_CWD: $EXEC_CWD"
|
|
432
|
-
if [[ "$EXEC_CWD" == "$PROJECT_DIR"* ]]; then
|
|
433
|
-
IS_IN_PROJECT=1
|
|
434
|
-
fi
|
|
435
|
-
fi
|
|
436
|
-
;;
|
|
437
|
-
Write|Edit)
|
|
438
|
-
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
|
|
439
|
-
if [[ "$FILE_PATH" == "$PROJECT_DIR"* ]] || [[ "$FILE_PATH" != /* ]]; then
|
|
440
|
-
IS_IN_PROJECT=1
|
|
441
|
-
fi
|
|
442
|
-
;;
|
|
443
|
-
*)
|
|
444
|
-
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // .path // .directory // empty')
|
|
445
|
-
if [[ -n "$FILE_PATH" ]]; then
|
|
446
|
-
if [[ "$FILE_PATH" == "$PROJECT_DIR"* ]] || [[ "$FILE_PATH" != /* ]]; then
|
|
447
|
-
IS_IN_PROJECT=1
|
|
448
|
-
fi
|
|
449
|
-
fi
|
|
450
|
-
;;
|
|
451
|
-
esac
|
|
452
|
-
|
|
453
|
-
log_debug "[$(date)] IS_IN_PROJECT: $IS_IN_PROJECT"
|
|
454
|
-
|
|
455
|
-
# 根据项目内/外决策
|
|
456
|
-
if [[ $IS_IN_PROJECT -eq 1 ]]; then
|
|
457
|
-
log_debug "[$(date)] Auto-allow: project operation"
|
|
458
|
-
# 通知 MCP Server 发送微信消息
|
|
459
|
-
curl -s -m 5 "\${TLS_OPTS[@]}" -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 &
|
|
460
|
-
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","message":"超时自动允许:项目内操作"}}}'
|
|
461
|
-
else
|
|
462
|
-
log_debug "[$(date)] Auto-deny: outside project"
|
|
463
|
-
# 通知 MCP Server 发送微信消息
|
|
464
|
-
curl -s -m 5 "\${TLS_OPTS[@]}" -X POST "$MCP_BASE_URL/approval_timeout/$TASK_ID" "\${AUTH_ARGS[@]}" -H "Content-Type: application/json" -d '{"result":"deny","reason":"超时自动拒绝:项目外操作需人工确认"}' > /dev/null 2>&1 &
|
|
465
|
-
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"超时自动拒绝:项目外操作需人工确认"}}}'
|
|
466
|
-
fi
|
|
467
|
-
`;
|
|
187
|
+
// v3.4.0: 从 dist/hooks/ 拷贝预编译的 .js 替代旧 bash 模板
|
|
468
188
|
ensureConfigDir();
|
|
469
|
-
|
|
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
|
+
}
|
|
470
206
|
console.log(`[config] Hook 脚本已写入: ${HOOK_SCRIPT_PATH}`);
|
|
471
207
|
}
|
|
472
208
|
// 生成并写入 Stop hook 脚本
|
|
473
209
|
// HTTP 模式使用:阻止 Claude 停止,提示调用 get_pending_messages 恢复轮询
|
|
474
210
|
function writeStopHookScript() {
|
|
475
|
-
|
|
476
|
-
# wecom-aibot-mcp Stop hook
|
|
477
|
-
# HTTP 模式使用:阻止 Claude 停止,提示调用 get_pending_messages 恢复轮询
|
|
478
|
-
#
|
|
479
|
-
# 固定端口: 18963
|
|
480
|
-
# 只检查 $(pwd)/.claude/wecom-aibot.json 的 wechatMode 字段
|
|
481
|
-
|
|
482
|
-
MCP_PORT=18963
|
|
483
|
-
|
|
484
|
-
# 先保存输入(Stop 事件数据)
|
|
485
|
-
INPUT=$(cat)
|
|
486
|
-
|
|
487
|
-
# 日志输出:--debug 模式下输出到 stderr,否则静默
|
|
488
|
-
DEBUG_FILE="$HOME/.wecom-aibot-mcp/debug"
|
|
489
|
-
log_debug() {
|
|
490
|
-
if [[ -f "$DEBUG_FILE" ]]; then
|
|
491
|
-
echo "$1" >&2
|
|
492
|
-
fi
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
log_debug "[$(date)] Stop hook called. INPUT: \${INPUT:0:200}"
|
|
496
|
-
|
|
497
|
-
# 检查项目目录的微信模式配置文件
|
|
498
|
-
PROJECT_DIR=$(pwd)
|
|
499
|
-
CONFIG_FILE="$PROJECT_DIR/.claude/wecom-aibot.json"
|
|
500
|
-
|
|
501
|
-
log_debug "[$(date)] Checking config: $CONFIG_FILE"
|
|
502
|
-
|
|
503
|
-
# 配置文件不存在,不在微信模式,允许停止
|
|
504
|
-
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
505
|
-
log_debug "[$(date)] No config file, exit 0 (allow stop)"
|
|
506
|
-
exit 0
|
|
507
|
-
fi
|
|
508
|
-
|
|
509
|
-
# 检查 wechatMode 是否为 true(微信模式开关)
|
|
510
|
-
WECHAT_MODE=$(jq -r '.wechatMode // false' "$CONFIG_FILE" 2>/dev/null)
|
|
511
|
-
log_debug "[$(date)] wechatMode: $WECHAT_MODE"
|
|
512
|
-
if [[ "$WECHAT_MODE" != "true" ]]; then
|
|
513
|
-
log_debug "[$(date)] wechatMode not true, exit 0 (allow stop)"
|
|
514
|
-
exit 0
|
|
515
|
-
fi
|
|
516
|
-
|
|
517
|
-
# 确定 MCP Server 地址(本地优先,失败则尝试远程 channel 配置)
|
|
518
|
-
MCP_BASE_URL="http://127.0.0.1:$MCP_PORT"
|
|
519
|
-
AUTH_ARGS=()
|
|
520
|
-
|
|
521
|
-
HEALTH=$(curl -s -m 2 "$MCP_BASE_URL/health" 2>/dev/null)
|
|
522
|
-
log_debug "[$(date)] Local health check: $HEALTH"
|
|
523
|
-
if ! echo "$HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
|
|
524
|
-
CLAUDE_JSON="$HOME/.claude.json"
|
|
525
|
-
if [[ -f "$CLAUDE_JSON" ]]; then
|
|
526
|
-
REMOTE_URL=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_URL // empty' "$CLAUDE_JSON" 2>/dev/null)
|
|
527
|
-
REMOTE_TOKEN=$(jq -r '.mcpServers["wecom-aibot-channel"].env.MCP_AUTH_TOKEN // empty' "$CLAUDE_JSON" 2>/dev/null)
|
|
528
|
-
if [[ -n "$REMOTE_URL" ]]; then
|
|
529
|
-
REMOTE_HEALTH=$(curl -s -m 5 \${REMOTE_TOKEN:+-H "Authorization: Bearer $REMOTE_TOKEN"} "$REMOTE_URL/health" 2>/dev/null)
|
|
530
|
-
if echo "$REMOTE_HEALTH" | jq -e '.status == "ok"' > /dev/null 2>&1; then
|
|
531
|
-
MCP_BASE_URL="$REMOTE_URL"
|
|
532
|
-
[[ -n "$REMOTE_TOKEN" ]] && AUTH_ARGS=(-H "Authorization: Bearer $REMOTE_TOKEN")
|
|
533
|
-
log_debug "[$(date)] Using remote server: $MCP_BASE_URL"
|
|
534
|
-
else
|
|
535
|
-
log_debug "[$(date)] MCP Server offline, exit 0 (allow stop)"
|
|
536
|
-
exit 0
|
|
537
|
-
fi
|
|
538
|
-
else
|
|
539
|
-
log_debug "[$(date)] MCP Server offline, exit 0 (allow stop)"
|
|
540
|
-
exit 0
|
|
541
|
-
fi
|
|
542
|
-
else
|
|
543
|
-
log_debug "[$(date)] MCP Server offline, exit 0 (allow stop)"
|
|
544
|
-
exit 0
|
|
545
|
-
fi
|
|
546
|
-
fi
|
|
547
|
-
|
|
548
|
-
# 获取 ccId
|
|
549
|
-
CC_ID=$(jq -r '.ccId // empty' "$CONFIG_FILE" 2>/dev/null)
|
|
550
|
-
log_debug "[$(date)] ccId: $CC_ID"
|
|
551
|
-
if [[ -z "$CC_ID" ]]; then
|
|
552
|
-
log_debug "[$(date)] No ccId in config, exit 0 (allow stop)"
|
|
553
|
-
exit 0
|
|
554
|
-
fi
|
|
555
|
-
|
|
556
|
-
# 处于微信模式,需要恢复轮询
|
|
557
|
-
# 使用 exit code 2 阻止停止,并提示 Claude 调用 MCP 工具
|
|
558
|
-
log_debug "[$(date)] ✅ WeChat mode active, blocking stop to resume polling"
|
|
559
|
-
log_debug "[$(date)] ccId=$CC_ID, will prompt Claude to call get_pending_messages"
|
|
560
|
-
echo "任务已完成,请调用 mcp__wecom-aibot__get_pending_messages(cc_id=\"$CC_ID\", timeout_ms=30000) 恢复微信消息轮询" >&2
|
|
561
|
-
exit 2
|
|
562
|
-
`;
|
|
211
|
+
// v3.4.0: 同上,从 dist/hooks/ 拷贝预编译的 .js
|
|
563
212
|
ensureConfigDir();
|
|
564
|
-
|
|
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
|
+
}
|
|
565
229
|
console.log(`[config] Stop Hook 脚本已写入: ${STOP_HOOK_SCRIPT_PATH}`);
|
|
566
230
|
}
|
|
567
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';
|
package/dist/hooks/stop-hook.js
CHANGED
|
@@ -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/project-config.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/project-config.js
CHANGED
|
@@ -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.
|
|
257
|
-
export const STOP_HOOK_SCRIPT_PATH = path.join(CONFIG_DIR, 'stop-hook.
|
|
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
|
|
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 增加延迟)
|