@vrs-soft/wecom-aibot-mcp 1.0.0 → 1.0.2
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 +7 -0
- package/dist/client.js +2 -2
- package/dist/config-wizard.js +42 -9
- package/package.json +3 -2
- package/skills/headless-mode/SKILL.md +123 -0
package/README.md
CHANGED
|
@@ -58,6 +58,12 @@ PermissionRequest Hook 拦截
|
|
|
58
58
|
执行或拒绝操作
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
### 审批卡片示例
|
|
62
|
+
|
|
63
|
+

|
|
64
|
+
|
|
65
|
+
用户在企业微信中收到审批卡片,点击按钮即可远程决策:
|
|
66
|
+
|
|
61
67
|
## 安装
|
|
62
68
|
|
|
63
69
|
### 前置要求
|
|
@@ -142,6 +148,7 @@ npm link
|
|
|
142
148
|
- 检查配置完整性
|
|
143
149
|
- 注册权限预授权
|
|
144
150
|
- 生成审批 Hook 脚本
|
|
151
|
+
- 安装 headless-mode skill 文件
|
|
145
152
|
|
|
146
153
|
### 第五步:验证连接
|
|
147
154
|
|
package/dist/client.js
CHANGED
|
@@ -175,8 +175,8 @@ class WecomClient {
|
|
|
175
175
|
main_title: { title },
|
|
176
176
|
sub_title_text: description,
|
|
177
177
|
button_list: [
|
|
178
|
-
{ text: '
|
|
179
|
-
{ text: '
|
|
178
|
+
{ text: '允许', key: 'allow-once', style: 1 },
|
|
179
|
+
{ text: '默认', key: 'allow-always', style: 1 },
|
|
180
180
|
{ text: '拒绝', key: 'deny', style: 2 },
|
|
181
181
|
],
|
|
182
182
|
task_id: taskId,
|
package/dist/config-wizard.js
CHANGED
|
@@ -139,12 +139,10 @@ if [[ -z "$TASK_ID" ]]; then
|
|
|
139
139
|
exit 0
|
|
140
140
|
fi
|
|
141
141
|
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
POLL_COUNT=0
|
|
145
|
-
while [[ $POLL_COUNT -lt $MAX_POLL ]]; do
|
|
142
|
+
# 轮询审批结果(无限等待,适合 headless 模式)
|
|
143
|
+
while true; do
|
|
146
144
|
sleep 2
|
|
147
|
-
STATUS=$(curl -s "http://127.0.0.1:$PORT/approval_status/$TASK_ID" 2>/dev/null)
|
|
145
|
+
STATUS=$(curl -s -m 5 "http://127.0.0.1:$PORT/approval_status/$TASK_ID" 2>/dev/null)
|
|
148
146
|
RESULT=$(echo "$STATUS" | jq -r '.result // empty')
|
|
149
147
|
|
|
150
148
|
if [[ "$RESULT" == "allow-once" || "$RESULT" == "allow-always" ]]; then
|
|
@@ -154,11 +152,7 @@ while [[ $POLL_COUNT -lt $MAX_POLL ]]; do
|
|
|
154
152
|
printf '%s\\n' '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"deny","message":"用户拒绝"}}}'
|
|
155
153
|
exit 0
|
|
156
154
|
fi
|
|
157
|
-
|
|
158
|
-
POLL_COUNT=$((POLL_COUNT + 1))
|
|
159
155
|
done
|
|
160
|
-
|
|
161
|
-
exit 0
|
|
162
156
|
`;
|
|
163
157
|
ensureConfigDir();
|
|
164
158
|
fs.writeFileSync(HOOK_SCRIPT_PATH, script, { mode: 0o755 });
|
|
@@ -214,6 +208,44 @@ function writeMcpServerConfig(config) {
|
|
|
214
208
|
return false;
|
|
215
209
|
}
|
|
216
210
|
}
|
|
211
|
+
// 安装 skill 文件到 ~/.claude/skills/
|
|
212
|
+
function installSkills() {
|
|
213
|
+
try {
|
|
214
|
+
const claudeSkillsDir = path.join(os.homedir(), '.claude', 'skills', 'headless-mode');
|
|
215
|
+
const skillFile = path.join(claudeSkillsDir, 'SKILL.md');
|
|
216
|
+
// 检查是否已存在
|
|
217
|
+
if (fs.existsSync(skillFile)) {
|
|
218
|
+
console.log('[config] skill 文件已存在,跳过安装');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// 确保目录存在
|
|
222
|
+
if (!fs.existsSync(claudeSkillsDir)) {
|
|
223
|
+
fs.mkdirSync(claudeSkillsDir, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
// 从包内复制 skill 文件
|
|
226
|
+
// 包安装后 skills 目录在包根目录下
|
|
227
|
+
const packageDir = path.dirname(require.main?.filename || __dirname);
|
|
228
|
+
const sourceSkillFile = path.join(packageDir, '..', 'skills', 'headless-mode', 'SKILL.md');
|
|
229
|
+
if (fs.existsSync(sourceSkillFile)) {
|
|
230
|
+
fs.copyFileSync(sourceSkillFile, skillFile);
|
|
231
|
+
console.log(`[config] skill 文件已安装: ${skillFile}`);
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// 开发模式:从源码目录复制
|
|
235
|
+
const devSkillFile = path.join(process.cwd(), 'skills', 'headless-mode', 'SKILL.md');
|
|
236
|
+
if (fs.existsSync(devSkillFile)) {
|
|
237
|
+
fs.copyFileSync(devSkillFile, skillFile);
|
|
238
|
+
console.log(`[config] skill 文件已安装: ${skillFile}`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
console.log('[config] ⚠️ skill 文件未找到,请手动创建 ~/.claude/skills/headless-mode/SKILL.md');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
console.error('[config] 安装 skill 文件失败:', err);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
217
249
|
// 写入 MCP 工具权限 + 注册 PermissionRequest hook 到 Claude settings
|
|
218
250
|
function writeMcpPermissions() {
|
|
219
251
|
try {
|
|
@@ -259,6 +291,7 @@ function writeMcpPermissions() {
|
|
|
259
291
|
// 确保 hook 已安装(幂等,可多次调用)
|
|
260
292
|
export function ensureHookInstalled() {
|
|
261
293
|
writeMcpPermissions();
|
|
294
|
+
installSkills();
|
|
262
295
|
}
|
|
263
296
|
// 保存配置(并自动写入 MCP 权限和 Server 配置)
|
|
264
297
|
export function saveConfig(config) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vrs-soft/wecom-aibot-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "企业微信智能机器人 MCP 服务 - Claude Code 审批通道",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
-
"README.md"
|
|
12
|
+
"README.md",
|
|
13
|
+
"skills"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"build": "tsc",
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: headless-mode
|
|
3
|
+
description: 当用户说「现在开始通过微信联系」、「我要离开电脑前」、「切换到微信模式」时触发。进入微信模式,不间断轮询 get_pending_messages,只有审批时阻塞。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Headless 微信交互模式
|
|
7
|
+
|
|
8
|
+
**触发条件**:「现在开始通过微信联系」、「我要离开电脑前」、「切换到微信模式」
|
|
9
|
+
|
|
10
|
+
## 激活后立即执行(必须按顺序)
|
|
11
|
+
|
|
12
|
+
**Step 1**:进入 headless 模式(写入状态文件,让 hook 知道要发微信审批)
|
|
13
|
+
```
|
|
14
|
+
mcp__wecom-aibot__enter_headless_mode
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Step 2**:发送确认消息,明确已进入微信模式
|
|
18
|
+
```
|
|
19
|
+
mcp__wecom-aibot__send_message:
|
|
20
|
+
content: "【进度】已进入微信模式,所有交互将通过企业微信进行。请直接发消息给我。"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Step 3**:立即进入不间断轮询循环
|
|
24
|
+
```
|
|
25
|
+
loop (永不退出):
|
|
26
|
+
调用 mcp__wecom-aibot__get_pending_messages
|
|
27
|
+
有消息 → 处理 → 汇报结果 → 继续轮询
|
|
28
|
+
无消息 → sleep(间隔动态调整)→ 继续轮询
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 核心规则
|
|
32
|
+
|
|
33
|
+
1. **不间断轮询** `get_pending_messages`,间隔动态调整:
|
|
34
|
+
- **正常模式**:5 秒间隔(用户活跃时)
|
|
35
|
+
- **省电模式**:用户无响应超过 20 分钟后,间隔逐渐放大到 2 分钟
|
|
36
|
+
- 用户发新消息后,恢复 5 秒间隔
|
|
37
|
+
2. **只有审批时阻塞**:hook 自动拦截敏感操作推送审批卡片,Clause 阻塞等待
|
|
38
|
+
3. **审批结束后立即恢复轮询**
|
|
39
|
+
4. 轮询永不退出,除非收到结束指令
|
|
40
|
+
|
|
41
|
+
### 轮询间隔策略
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
开始轮询(5秒间隔)
|
|
45
|
+
↓
|
|
46
|
+
记录最后一次消息时间
|
|
47
|
+
↓
|
|
48
|
+
每次轮询检查:
|
|
49
|
+
- 有新消息 → 处理 → 重置计时 → 继续 5 秒间隔
|
|
50
|
+
- 无消息 → 检查空闲时间
|
|
51
|
+
- 空闲 < 20分钟 → 5 秒间隔
|
|
52
|
+
- 空闲 20-30分钟 → 30 秒间隔
|
|
53
|
+
- 空闲 30-60分钟 → 1 分钟间隔
|
|
54
|
+
- 空闲 > 60分钟 → 2 分钟间隔
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Why**:用户可能在休息、开会、睡觉,降低轮询频率减少 token 消耗,同时保持响应能力。
|
|
58
|
+
|
|
59
|
+
## 处理用户消息
|
|
60
|
+
|
|
61
|
+
1. 理解用户意图
|
|
62
|
+
2. 直接执行所需工具(Bash、Edit、Write 等)
|
|
63
|
+
- hook 自动处理审批
|
|
64
|
+
3. 执行完毕后用 `send_message` 汇报结果
|
|
65
|
+
4. **立即继续轮询**
|
|
66
|
+
|
|
67
|
+
## 群聊消息处理
|
|
68
|
+
|
|
69
|
+
`get_pending_messages` 返回的消息包含:
|
|
70
|
+
- `chattype`: "single"(单聊)或 "group"(群聊)
|
|
71
|
+
- `chatid`: 单聊=用户ID,群聊=群ID
|
|
72
|
+
|
|
73
|
+
**回复群聊消息**:
|
|
74
|
+
```
|
|
75
|
+
mcp__wecom-aibot__send_message:
|
|
76
|
+
content: "回复内容"
|
|
77
|
+
target_user: "消息中的chatid" # 群聊时传入 chatid,回复会发到群里
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**注意**:群聊消息回复到群里,所有人可见。
|
|
81
|
+
|
|
82
|
+
## 通讯工具
|
|
83
|
+
|
|
84
|
+
- **发消息**:`mcp__wecom-aibot__send_message`
|
|
85
|
+
- **接消息**:`mcp__wecom-aibot__get_pending_messages`(动态间隔轮询)
|
|
86
|
+
- **不要**手动调用 `send_approval_request`
|
|
87
|
+
|
|
88
|
+
## 消息格式
|
|
89
|
+
|
|
90
|
+
- `【需要确认】` — 需要决策
|
|
91
|
+
- `【问题】` — 阻塞错误
|
|
92
|
+
- `【进度】` — 里程碑
|
|
93
|
+
- `【完成】` — 当前任务完成,继续等待新消息(**不是退出轮询**)
|
|
94
|
+
|
|
95
|
+
## 结束模式
|
|
96
|
+
|
|
97
|
+
### 用户意图识别
|
|
98
|
+
|
|
99
|
+
在轮询过程中,如果收到用户消息,需判断是否要结束微信模式:
|
|
100
|
+
|
|
101
|
+
| 用户消息 | 判断 | 动作 |
|
|
102
|
+
|---------|------|------|
|
|
103
|
+
| 「结束微信模式」、「停止微信模式」、「退出微信模式」 | 明确终止 | 直接停止 |
|
|
104
|
+
| 「我回来了」、「我回电脑了」 | 明确终止 | 直接停止 |
|
|
105
|
+
| 「我已经回到电脑旁了」、「我在电脑前了」 | 可能终止 | **询问确认** |
|
|
106
|
+
| 「休息一下」、「暂停一下」 | 不终止 | 继续轮询 |
|
|
107
|
+
|
|
108
|
+
### 询问确认格式
|
|
109
|
+
|
|
110
|
+
如果用户消息暗示可能在电脑前但不确定:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
【需要确认】检测到您可能在电脑前了,是否结束微信模式?
|
|
114
|
+
- 回复「是」或「结束」→ 退出微信模式
|
|
115
|
+
- 回复「否」或「继续」→ 保持微信模式
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 结束流程
|
|
119
|
+
|
|
120
|
+
确认结束后:
|
|
121
|
+
- 调用 `mcp__wecom-aibot__exit_headless_mode`(删除状态文件)
|
|
122
|
+
- 发送:`【进度】已退出微信模式,恢复终端交互。`
|
|
123
|
+
- 停止轮询
|