@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 CHANGED
@@ -58,6 +58,12 @@ PermissionRequest Hook 拦截
58
58
  执行或拒绝操作
59
59
  ```
60
60
 
61
+ ### 审批卡片示例
62
+
63
+ ![审批卡片](docs/approval-card.png)
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: '允许一次', key: 'allow-once', style: 1 },
179
- { text: '永久允许', key: 'allow-always', style: 1 },
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,
@@ -139,12 +139,10 @@ if [[ -z "$TASK_ID" ]]; then
139
139
  exit 0
140
140
  fi
141
141
 
142
- # 轮询审批结果
143
- MAX_POLL=300
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.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
+ - 停止轮询