@tencent-connect/openclaw-qqbot 1.5.7 → 1.6.0-alpha.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.
Files changed (63) hide show
  1. package/README.md +9 -2
  2. package/README.zh.md +7 -2
  3. package/package.json +1 -1
  4. package/scripts/upgrade-via-npm.sh +85 -115
  5. package/scripts/upgrade-via-source.sh +203 -35
  6. package/skills/qqbot-cron/SKILL.md +46 -423
  7. package/skills/qqbot-media/SKILL.md +29 -182
  8. package/src/api.ts +16 -5
  9. package/src/channel.ts +6 -7
  10. package/src/gateway.ts +510 -525
  11. package/src/image-server.ts +72 -10
  12. package/src/openclaw-plugin-sdk.d.ts +1 -1
  13. package/src/outbound.ts +571 -611
  14. package/src/ref-index-store.ts +1 -1
  15. package/src/slash-commands.ts +425 -0
  16. package/src/types.ts +18 -1
  17. package/src/update-checker.ts +102 -0
  18. package/src/user-messages.ts +73 -0
  19. package/src/utils/audio-convert.ts +69 -4
  20. package/src/utils/media-tags.ts +46 -4
  21. package/dist/AI/345/210/233/346/226/260/345/272/224/347/224/250/345/245/226_/347/224/263/346/212/245/344/271/246.md +0 -211
  22. package/dist/index.d.ts +0 -17
  23. package/dist/index.js +0 -22
  24. package/dist/src/api.d.ts +0 -138
  25. package/dist/src/api.js +0 -525
  26. package/dist/src/channel.d.ts +0 -3
  27. package/dist/src/channel.js +0 -337
  28. package/dist/src/config.d.ts +0 -25
  29. package/dist/src/config.js +0 -161
  30. package/dist/src/gateway.d.ts +0 -18
  31. package/dist/src/gateway.js +0 -2468
  32. package/dist/src/image-server.d.ts +0 -62
  33. package/dist/src/image-server.js +0 -401
  34. package/dist/src/known-users.d.ts +0 -100
  35. package/dist/src/known-users.js +0 -263
  36. package/dist/src/onboarding.d.ts +0 -10
  37. package/dist/src/onboarding.js +0 -203
  38. package/dist/src/outbound.d.ts +0 -150
  39. package/dist/src/outbound.js +0 -1175
  40. package/dist/src/proactive.d.ts +0 -170
  41. package/dist/src/proactive.js +0 -399
  42. package/dist/src/runtime.d.ts +0 -3
  43. package/dist/src/runtime.js +0 -10
  44. package/dist/src/session-store.d.ts +0 -52
  45. package/dist/src/session-store.js +0 -254
  46. package/dist/src/slash-commands.d.ts +0 -48
  47. package/dist/src/slash-commands.js +0 -212
  48. package/dist/src/types.d.ts +0 -146
  49. package/dist/src/types.js +0 -1
  50. package/dist/src/utils/audio-convert.d.ts +0 -73
  51. package/dist/src/utils/audio-convert.js +0 -645
  52. package/dist/src/utils/file-utils.d.ts +0 -46
  53. package/dist/src/utils/file-utils.js +0 -107
  54. package/dist/src/utils/image-size.d.ts +0 -51
  55. package/dist/src/utils/image-size.js +0 -234
  56. package/dist/src/utils/media-tags.d.ts +0 -14
  57. package/dist/src/utils/media-tags.js +0 -120
  58. package/dist/src/utils/payload.d.ts +0 -112
  59. package/dist/src/utils/payload.js +0 -186
  60. package/dist/src/utils/platform.d.ts +0 -126
  61. package/dist/src/utils/platform.js +0 -358
  62. package/dist/src/utils/upload-cache.d.ts +0 -34
  63. package/dist/src/utils/upload-cache.js +0 -93
package/README.md CHANGED
@@ -6,14 +6,19 @@
6
6
 
7
7
  # QQ Bot Channel Plugin for OpenClaw
8
8
 
9
+
10
+
9
11
  **Connect your AI assistant to QQ — private chat, group chat, and rich media, all in one plugin.**
10
12
 
13
+ ### 🚀 Current Version: `v1.5.7`
14
+
11
15
  [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
12
16
  [![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](https://bot.q.qq.com/wiki/)
13
17
  [![Platform](https://img.shields.io/badge/platform-OpenClaw-orange)](https://github.com/tencent-connect/openclaw-qqbot)
14
18
  [![Node.js](https://img.shields.io/badge/Node.js->=18-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
15
19
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
16
20
 
21
+
17
22
  <br/>
18
23
 
19
24
  **[简体中文](README.zh.md) | English**
@@ -56,6 +61,8 @@ QQ quote events carry index keys (e.g. `REFIDX_xxx`) instead of full original me
56
61
  - Store path: `~/.openclaw/qqbot/data/ref-index.jsonl` (survives gateway restart).
57
62
  - Quote body may include text + media summary (image/voice/video/file).
58
63
 
64
+ <img width="360" src="docs/images/ref_msg.png" alt="Quoted Message Context Demo" />
65
+
59
66
  ### 🎙️ Voice Messages (STT)
60
67
 
61
68
  With STT configured, the plugin automatically transcribes voice messages to text before passing them to AI. The whole process is transparent to the user — sending voice feels as natural as sending text.
@@ -86,7 +93,7 @@ If your main model supports vision (e.g. Tencent Hunyuan `hunyuan-vision`), AI c
86
93
 
87
94
  <img width="360" src="docs/images/59d421891f813b0d3c0cbe12574b6a72_720.jpg" alt="Image Understanding Demo" />
88
95
 
89
- ### 🎨 Image Generation
96
+ ### 🎨 Image Sending
90
97
 
91
98
  > **You**: Draw me a cat
92
99
  >
@@ -96,7 +103,7 @@ AI sends images via `<qqimg>path</qqimg>`. Supports local paths and URLs. Format
96
103
 
97
104
  <img width="360" src="docs/images/4645f2b3a20822b7f8d6664a708529eb_720.jpg" alt="Image Generation Demo" />
98
105
 
99
- ### 🔊 Voice Reply (TTS)
106
+ ### 🔊 Voice Sending
100
107
 
101
108
  > **You**: Tell me a joke in voice
102
109
  >
package/README.zh.md CHANGED
@@ -6,8 +6,11 @@
6
6
 
7
7
  # QQ Bot — OpenClaw 渠道插件
8
8
 
9
+
9
10
  **让你的 AI 助手接入 QQ — 私聊、群聊、富媒体,一个插件全搞定。**
10
11
 
12
+ ### 🚀 当前版本: `v1.5.7`
13
+
11
14
  [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
12
15
  [![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](https://bot.q.qq.com/wiki/)
13
16
  [![Platform](https://img.shields.io/badge/platform-OpenClaw-orange)](https://github.com/tencent-connect/openclaw-qqbot)
@@ -53,6 +56,8 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
53
56
  - 存储位置:`~/.openclaw/qqbot/data/ref-index.jsonl`(网关重启后仍可恢复)。
54
57
  - 引用内容支持文本 + 媒体摘要(图片/语音/视频/文件)。
55
58
 
59
+ <img width="360" src="docs/images/ref_msg.png" alt="引用消息上下文演示" />
60
+
56
61
  ### 🎙️ 语音消息(STT)
57
62
 
58
63
  配置 STT 后,插件会自动将语音转录为文字再交给 AI 处理。整个过程对用户完全透明——发语音就像发文字一样自然,AI 听得懂你在说什么。
@@ -83,7 +88,7 @@ QQ 的引用事件通常只携带索引键(如 `REFIDX_xxx`),不直接返
83
88
 
84
89
  <img width="360" src="docs/images/59d421891f813b0d3c0cbe12574b6a72_720.jpg" alt="图片理解演示" />
85
90
 
86
- ### 🎨 AI 画图
91
+ ### 🎨 图片发送
87
92
 
88
93
  > **你**:画一只猫咪
89
94
  >
@@ -93,7 +98,7 @@ AI 通过 `<qqimg>路径</qqimg>` 发送图片,支持本地文件路径和网
93
98
 
94
99
  <img width="360" src="docs/images/4645f2b3a20822b7f8d6664a708529eb_720.jpg" alt="发图片演示" />
95
100
 
96
- ### 🔊 语音回复(TTS)
101
+ ### 🔊 语音发送
97
102
 
98
103
  > **你**:给我讲一个笑话
99
104
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.5.7",
3
+ "version": "1.6.0-alpha.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,6 +1,10 @@
1
1
  #!/bin/bash
2
2
 
3
- # qqbot 通过 npm 包升级
3
+ # qqbot 通过 npm 包升级(纯文件操作版本)
4
+ #
5
+ # 重要:此脚本不修改 openclaw.json 配置文件!
6
+ # 配置更新由调用方(TS handler)在脚本完成后统一处理,
7
+ # 避免 gateway config watcher 在安装过程中触发 SIGUSR1 重启导致竞态。
4
8
  #
5
9
  # 用法:
6
10
  # upgrade-via-npm.sh # 升级到 latest(默认)
@@ -11,8 +15,6 @@ set -eo pipefail
11
15
 
12
16
  PKG_NAME="@tencent-connect/openclaw-qqbot"
13
17
  INSTALL_SRC=""
14
- APPID=""
15
- SECRET=""
16
18
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
17
19
  PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
18
20
 
@@ -30,16 +32,11 @@ print_usage() {
30
32
  echo "用法:"
31
33
  echo " upgrade-via-npm.sh # 升级到 latest(默认)"
32
34
  echo " upgrade-via-npm.sh --version <版本号> # 升级到指定版本"
33
- echo " upgrade-via-npm.sh --appid <appid> --secret <secret> # 配置通道并启动"
34
35
  if [ -n "$LOCAL_VERSION" ]; then
35
36
  echo " upgrade-via-npm.sh --self-version # 升级到当前仓库版本($LOCAL_VERSION)"
36
37
  else
37
38
  echo " upgrade-via-npm.sh --self-version # 升级到当前仓库版本"
38
39
  fi
39
- echo ""
40
- echo "也可以通过环境变量设置:"
41
- echo " QQBOT_APPID QQ机器人 appid"
42
- echo " QQBOT_SECRET QQ机器人 secret"
43
40
  }
44
41
 
45
42
  while [[ $# -gt 0 ]]; do
@@ -59,16 +56,6 @@ while [[ $# -gt 0 ]]; do
59
56
  INSTALL_SRC="${PKG_NAME}@${LOCAL_VERSION}"
60
57
  shift 1
61
58
  ;;
62
- --appid)
63
- [ -z "$2" ] && echo "❌ --appid 需要参数" && exit 1
64
- APPID="$2"
65
- shift 2
66
- ;;
67
- --secret)
68
- [ -z "$2" ] && echo "❌ --secret 需要参数" && exit 1
69
- SECRET="$2"
70
- shift 2
71
- ;;
72
59
  -h|--help)
73
60
  print_usage
74
61
  exit 0
@@ -78,18 +65,13 @@ while [[ $# -gt 0 ]]; do
78
65
  done
79
66
  INSTALL_SRC="${INSTALL_SRC:-${PKG_NAME}@latest}"
80
67
 
81
- # 使用命令行参数或环境变量
82
- APPID="${APPID:-$QQBOT_APPID}"
83
- SECRET="${SECRET:-$QQBOT_SECRET}"
84
-
85
- # 检测 CLI
68
+ # 检测 CLI(仅用于确定 extensions 目录路径)
86
69
  CMD=""
87
70
  for name in openclaw clawdbot moltbot; do
88
71
  command -v "$name" &>/dev/null && CMD="$name" && break
89
72
  done
90
73
  [ -z "$CMD" ] && echo "❌ 未找到 openclaw / clawdbot / moltbot" && exit 1
91
74
 
92
- APP_CONFIG="$HOME/.$CMD/$CMD.json"
93
75
  EXTENSIONS_DIR="$HOME/.$CMD/extensions"
94
76
 
95
77
  echo "==========================================="
@@ -97,108 +79,96 @@ echo " qqbot npm 升级: $INSTALL_SRC"
97
79
  echo "==========================================="
98
80
  echo ""
99
81
 
100
- # [1/4] 备份并临时移除通道配置(避免 plugins install 因 unknown channel 拒绝执行)
101
- echo "[1/4] 备份通道配置..."
102
- if [ -f "$APP_CONFIG" ]; then
103
- node -e "
104
- const fs = require('fs');
105
- const cfg = JSON.parse(fs.readFileSync('$APP_CONFIG', 'utf8'));
106
- const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
107
- let saved = null;
108
- for (const key of keys) {
109
- const ch = cfg.channels && cfg.channels[key];
110
- if (ch) { saved = ch; delete cfg.channels[key]; break; }
111
- }
112
- // 清理 plugins.entries 中的旧记录(避免 stale config entry 告警)
113
- if (cfg.plugins && cfg.plugins.entries) {
114
- delete cfg.plugins.entries['openclaw-qqbot'];
115
- }
116
- if (saved) {
117
- fs.writeFileSync('$APP_CONFIG.qqbot-backup.json', JSON.stringify(saved, null, 2));
118
- fs.writeFileSync('$APP_CONFIG', JSON.stringify(cfg, null, 4) + '\n');
119
- console.log(' ✅ 已备份并临时移除 channels.qqbot');
120
- } else {
121
- console.log(' ℹ️ 无已有通道配置');
122
- }
123
- " 2>/dev/null || echo " ⚠️ 备份失败"
82
+ # [1/3] 下载并安装新版本到临时目录
83
+ echo "[1/3] 下载新版本..."
84
+ TMPDIR_PACK=$(mktemp -d)
85
+ EXTRACT_DIR=$(mktemp -d)
86
+ trap "rm -rf '$TMPDIR_PACK' '$EXTRACT_DIR'" EXIT
87
+
88
+ cd "$TMPDIR_PACK"
89
+ npm pack "$INSTALL_SRC" --quiet 2>&1 || { echo "❌ npm pack 失败"; exit 1; }
90
+ TGZ_FILE=$(ls -1 *.tgz 2>/dev/null | head -1)
91
+ [ -z "$TGZ_FILE" ] && echo "❌ 未找到下载的 tgz 文件" && exit 1
92
+ echo " 已下载: $TGZ_FILE"
93
+
94
+ tar xzf "$TGZ_FILE" -C "$EXTRACT_DIR"
95
+ PACKAGE_DIR="$EXTRACT_DIR/package"
96
+ [ ! -d "$PACKAGE_DIR" ] && echo "❌ 解压失败,未找到 package 目录" && exit 1
97
+
98
+ # 准备 staging 目录:放在 ~/.openclaw/ 下(extensions 的父目录),
99
+ # 同一文件系统保证 mv 原子操作,同时避免 OpenClaw 扫描 extensions/ 时发现它。
100
+ STAGING_DIR="$(dirname "$EXTENSIONS_DIR")/.qqbot-upgrade-staging"
101
+ rm -rf "$STAGING_DIR"
102
+ mkdir -p "$STAGING_DIR"
103
+ cp -R "$PACKAGE_DIR/"* "$STAGING_DIR/"
104
+
105
+ # 依赖处理:所有 production dependencies 都声明为 bundledDependencies,
106
+ # npm pack 时已打包进 tgz,解压后 node_modules/ 已包含全部依赖,无需 npm install。
107
+ # 注意:不能执行 npm install,否则会安装 peerDependencies(openclaw 平台及其 400+ 传递依赖),
108
+ # 导致插件目录膨胀到 900MB+,而这些依赖在运行时由宿主 openclaw 提供。
109
+ if [ -d "$STAGING_DIR/node_modules" ]; then
110
+ BUNDLED_COUNT=$(ls -d "$STAGING_DIR/node_modules"/*/ "$STAGING_DIR/node_modules"/@*/*/ 2>/dev/null | wc -l | tr -d ' ')
111
+ echo " bundled 依赖已就绪(${BUNDLED_COUNT} 个包)"
124
112
  else
125
- echo " ℹ️ 无配置文件"
113
+ echo " ⚠️ 未找到 bundled node_modules,尝试安装依赖..."
114
+ NPM_TMP_CACHE=$(mktemp -d)
115
+ (cd "$STAGING_DIR" && npm install --omit=dev --omit=peer --ignore-scripts --cache="$NPM_TMP_CACHE" --quiet 2>&1) || echo " ⚠️ 依赖安装失败"
116
+ rm -rf "$NPM_TMP_CACHE"
126
117
  fi
127
118
 
128
- # [2/4] 清理旧插件目录
129
- echo ""
130
- echo "[2/4] 清理旧插件..."
131
- for old_plugin in openclaw-qqbot qqbot openclaw-qq @sliverp/qqbot @tencent-connect/qqbot @tencent-connect/openclaw-qq @tencent-connect/openclaw-qqbot; do
132
- $CMD plugins uninstall "$old_plugin" 2>/dev/null && echo " 已卸载: $old_plugin" || true
133
- done
134
- for dir_name in openclaw-qqbot qqbot openclaw-qq; do
135
- if [ -d "$EXTENSIONS_DIR/$dir_name" ]; then
136
- rm -rf "$EXTENSIONS_DIR/$dir_name"
137
- echo " 已清理残留目录: $dir_name"
138
- fi
139
- done
119
+ # 清理下载临时文件
120
+ rm -rf "$TMPDIR_PACK" "$EXTRACT_DIR"
121
+ cd "$HOME"
140
122
 
141
- # [3/4] 安装新版本
123
+ # [2/3] 原子替换:先移走旧目录,再把 staging 目录 rename 过去
124
+ # 这样 extensions/openclaw-qqbot 只有极短的不存在时间窗口
142
125
  echo ""
143
- echo "[3/4] 安装新版本..."
144
- $CMD plugins install "$INSTALL_SRC" 2>&1
145
-
146
- # 恢复通道配置
147
- BACKUP_FILE="$APP_CONFIG.qqbot-backup.json"
148
- if [ -f "$BACKUP_FILE" ] && [ -f "$APP_CONFIG" ]; then
149
- node -e "
150
- const fs = require('fs');
151
- const cfg = JSON.parse(fs.readFileSync('$APP_CONFIG', 'utf8'));
152
- const saved = JSON.parse(fs.readFileSync('$BACKUP_FILE', 'utf8'));
153
- cfg.channels = cfg.channels || {};
154
- cfg.channels.qqbot = saved;
155
- fs.writeFileSync('$APP_CONFIG', JSON.stringify(cfg, null, 4) + '\n');
156
- fs.unlinkSync('$BACKUP_FILE');
157
- console.log(' ✅ 通道配置已恢复');
158
- " 2>/dev/null || echo " ⚠️ 通道配置恢复失败,请手动检查: $APP_CONFIG"
126
+ echo "[2/3] 原子替换插件目录..."
127
+ TARGET_DIR="$EXTENSIONS_DIR/openclaw-qqbot"
128
+ OLD_DIR="$(dirname "$EXTENSIONS_DIR")/.qqbot-upgrade-old"
129
+
130
+ rm -rf "$OLD_DIR"
131
+ if [ -d "$TARGET_DIR" ]; then
132
+ mv "$TARGET_DIR" "$OLD_DIR"
159
133
  fi
134
+ mv "$STAGING_DIR" "$TARGET_DIR"
135
+ rm -rf "$OLD_DIR"
160
136
 
161
- # 配置通道(如果提供了 appid secret)
162
- if [ -n "$APPID" ] && [ -n "$SECRET" ]; then
163
- echo ""
164
- echo "配置机器人通道..."
165
- DESIRED_TOKEN="${APPID}:${SECRET}"
166
-
167
- # 读取当前配置中的 token
168
- CURRENT_TOKEN=""
169
- if [ -f "$APP_CONFIG" ]; then
170
- CURRENT_TOKEN=$(node -e "
171
- const cfg = JSON.parse(require('fs').readFileSync('$APP_CONFIG', 'utf8'));
172
- const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
173
- for (const key of keys) {
174
- const ch = cfg.channels && cfg.channels[key];
175
- if (!ch) continue;
176
- if (ch.token) { process.stdout.write(ch.token); process.exit(0); }
177
- if (ch.appId && ch.clientSecret) { process.stdout.write(ch.appId + ':' + ch.clientSecret); process.exit(0); }
178
- }
179
- " 2>/dev/null || true)
180
- fi
137
+ # 清理可能残留的旧版 staging 目录(extensions 内外都清理)
138
+ rm -rf "$EXTENSIONS_DIR/openclaw-qqbot.staging"
139
+ rm -rf "$EXTENSIONS_DIR/.qqbot-upgrade-staging"
140
+ rm -rf "$EXTENSIONS_DIR/.qqbot-upgrade-old"
181
141
 
182
- if [ "$CURRENT_TOKEN" = "$DESIRED_TOKEN" ]; then
183
- echo " ✅ 当前配置已是目标值,跳过写入"
184
- elif $CMD channels add --channel qqbot --token "$DESIRED_TOKEN" 2>&1; then
185
- echo " ✅ 机器人通道配置成功"
186
- else
187
- echo " ⚠️ 通道配置失败,请手动执行: $CMD channels add --channel qqbot --token \"$DESIRED_TOKEN\""
188
- fi
189
- fi
142
+ # 同时清理历史遗留的其他目录名
143
+ for dir_name in qqbot openclaw-qq; do
144
+ [ -d "$EXTENSIONS_DIR/$dir_name" ] && rm -rf "$EXTENSIONS_DIR/$dir_name"
145
+ done
146
+ echo " 已安装到: $TARGET_DIR"
190
147
 
191
- # [4/4] 重启网关
148
+ # [3/3] 输出新版本号和升级报告(供调用方解析)
192
149
  echo ""
193
- echo "[4/4] 重启网关..."
194
- $CMD gateway restart 2>&1 || true
150
+ echo "[3/3] 验证安装..."
151
+ NEW_VERSION="$(node -e "
152
+ try {
153
+ const fs = require('fs');
154
+ const path = require('path');
155
+ const p = path.join('$EXTENSIONS_DIR', 'openclaw-qqbot', 'package.json');
156
+ if (fs.existsSync(p)) {
157
+ const v = JSON.parse(fs.readFileSync(p, 'utf8')).version;
158
+ if (v) { process.stdout.write(v); process.exit(0); }
159
+ }
160
+ } catch {}
161
+ " 2>/dev/null || true)"
162
+ echo "QQBOT_NEW_VERSION=${NEW_VERSION:-unknown}"
163
+
164
+ # 输出结构化升级报告(QQBOT_REPORT=...),供 TS handler 解析后直接回复用户
165
+ if [ -n "$NEW_VERSION" ] && [ "$NEW_VERSION" != "unknown" ]; then
166
+ echo "QQBOT_REPORT=✅ QQBot 升级完成: v${NEW_VERSION}"
167
+ else
168
+ echo "QQBOT_REPORT=⚠️ QQBot 升级异常,无法确认新版本"
169
+ fi
195
170
 
196
171
  echo ""
197
172
  echo "==========================================="
198
- echo " ✅ 升级完成"
173
+ echo " ✅ 文件安装完成"
199
174
  echo "==========================================="
200
- echo ""
201
- echo "常用命令:"
202
- echo " $CMD logs --follow # 跟踪日志"
203
- echo " $CMD gateway restart # 重启服务"
204
- echo " $CMD plugins list # 查看插件列表"
@@ -186,6 +186,19 @@ INSTALL_LOG="/tmp/openclaw-install-$(date +%s).log"
186
186
  echo "安装日志文件: $INSTALL_LOG"
187
187
  echo "详细信息将记录到日志文件中..."
188
188
 
189
+ # 安装前先 stop gateway,防止 chokidar 在 plugins install 写入配置的中间状态
190
+ # 触发 restart,导致 "unknown channel id: qqbot" 等错误
191
+ _gw_was_running=0
192
+ if lsof -i :18789 -sTCP:LISTEN >/dev/null 2>&1; then
193
+ _gw_was_running=1
194
+ echo " 暂停 gateway 服务(避免安装过程中中间状态 restart)..."
195
+ openclaw gateway stop 2>/dev/null || true
196
+ sleep 1
197
+ fi
198
+
199
+ # 清理之前可能残留的 staging 目录
200
+ find "$HOME/.openclaw/extensions/" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
201
+
189
202
  # 尝试安装并捕获详细输出
190
203
  if ! openclaw plugins install . 2>&1 | tee "$INSTALL_LOG"; then
191
204
  echo ""
@@ -243,15 +256,136 @@ if ! openclaw plugins install . 2>&1 | tee "$INSTALL_LOG"; then
243
256
  esac
244
257
  else
245
258
  echo ""
246
- echo "✅ 插件安装成功!"
259
+ echo "✅ 插件安装命令执行完成"
247
260
  echo "安装日志已保存到: $INSTALL_LOG"
248
261
 
249
- # plugins install 会修改 openclaw.json(plugins.allow/entries/installs),
250
- # gateway 检测到 config change 后会自动 SIGUSR1 重启(可能触发 1~2 次)。
251
- # 必须等这波自动重启完全结束,否则后续的 gateway restart 会叠加导致竞态。
252
- echo ""
253
- echo "等待 gateway 自动重启链完成(约 20 秒)..."
254
- sleep 20
262
+ # 验证插件目录是否真正创建(防止 "安装成功" 但目录缺失的情况)
263
+ _plugin_dir_ok=0
264
+ for _candidate_name in openclaw-qqbot qqbot openclaw-qq; do
265
+ if [ -d "$HOME/.openclaw/extensions/$_candidate_name" ] && \
266
+ [ -f "$HOME/.openclaw/extensions/$_candidate_name/package.json" ]; then
267
+ _plugin_dir_ok=1
268
+ echo " ✅ 插件目录验证通过: ~/.openclaw/extensions/$_candidate_name/"
269
+ break
270
+ fi
271
+ done
272
+ if [ "$_plugin_dir_ok" -eq 0 ]; then
273
+ echo ""
274
+ echo "⚠️ 警告: 插件目录不存在!安装命令返回成功但目录未创建"
275
+ echo " 可能原因: staging 目录未正确 rename(参考 openclaw/issues)"
276
+ echo " 检查 staging 残留:"
277
+ ls -la "$HOME/.openclaw/extensions/" 2>/dev/null
278
+ echo ""
279
+ echo " 尝试自动修复: 清理残留并重试安装..."
280
+ find "$HOME/.openclaw/extensions/" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
281
+ if openclaw plugins install . 2>&1 | tee -a "$INSTALL_LOG"; then
282
+ for _candidate_name in openclaw-qqbot qqbot openclaw-qq; do
283
+ if [ -d "$HOME/.openclaw/extensions/$_candidate_name" ]; then
284
+ _plugin_dir_ok=1
285
+ echo " ✅ 重试安装成功: ~/.openclaw/extensions/$_candidate_name/"
286
+ break
287
+ fi
288
+ done
289
+ fi
290
+ if [ "$_plugin_dir_ok" -eq 0 ]; then
291
+ echo " ❌ 重试安装仍失败,插件目录不存在"
292
+ echo " 请手动排查: ls -la ~/.openclaw/extensions/"
293
+ # 清理无效的配置条目,防止 gateway 启动时报错
294
+ echo " 清理无效的配置条目..."
295
+ node -e "
296
+ const fs = require('fs');
297
+ const path = require('path');
298
+ for (const app of ['openclaw', 'clawdbot', 'moltbot']) {
299
+ const f = path.join(process.env.HOME, '.' + app, app + '.json');
300
+ if (!fs.existsSync(f)) continue;
301
+ const cfg = JSON.parse(fs.readFileSync(f, 'utf8'));
302
+ let changed = false;
303
+ // 移除 channels.qqbot(插件未加载时此配置会导致 unknown channel id 错误)
304
+ if (cfg.channels && cfg.channels.qqbot) { delete cfg.channels.qqbot; changed = true; }
305
+ // 清理 plugins.allow 中的 openclaw-qqbot
306
+ if (cfg.plugins && Array.isArray(cfg.plugins.allow)) {
307
+ cfg.plugins.allow = cfg.plugins.allow.filter(x => x !== 'openclaw-qqbot');
308
+ changed = true;
309
+ }
310
+ if (changed) fs.writeFileSync(f, JSON.stringify(cfg, null, 4) + '\n');
311
+ break;
312
+ }
313
+ " 2>/dev/null || true
314
+ echo " 已清理无效配置,gateway 可正常启动(无 qqbot 插件状态)"
315
+ read -t 10 -p "是否继续? (y/N): " _cont || _cont="N"
316
+ case "$_cont" in
317
+ [Yy]* ) echo "继续..." ;;
318
+ * ) exit 1 ;;
319
+ esac
320
+ fi
321
+ fi
322
+
323
+ # 清理多余的 peerDependencies 传递依赖(兼容旧版 openclaw):
324
+ # openclaw v2026.3.4 之前的 plugins install 缺少 --omit=peer,会把 peerDeps
325
+ # (openclaw 平台及其 400+ 传递依赖)也安装到插件 node_modules 中。
326
+ # 新版已修复,此处通过阈值判断:包数量 > 50 才触发清理,避免对新版做无用操作。
327
+ PLUGIN_NM=""
328
+ for _candidate in openclaw-qqbot qqbot openclaw-qq; do
329
+ _nm="$HOME/.openclaw/extensions/$_candidate/node_modules"
330
+ [ -d "$_nm" ] && PLUGIN_NM="$_nm" && break
331
+ done
332
+ if [ -n "$PLUGIN_NM" ]; then
333
+ _before=$(ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null | wc -l | tr -d ' ')
334
+ if [ "$_before" -gt 50 ]; then
335
+ # 读取 bundledDependencies 列表,只保留这些包及其子依赖
336
+ _bundled_deps=$(node -e "
337
+ const fs = require('fs');
338
+ const path = require('path');
339
+ const pkgPath = path.join('$PLUGIN_NM', '..', 'package.json');
340
+ if (!fs.existsSync(pkgPath)) process.exit(0);
341
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
342
+ const bundled = pkg.bundledDependencies || pkg.bundleDependencies || [];
343
+ const keep = new Set();
344
+ const resolve = (name) => {
345
+ if (keep.has(name)) return;
346
+ keep.add(name);
347
+ const depPkg = path.join('$PLUGIN_NM', name, 'package.json');
348
+ if (!fs.existsSync(depPkg)) return;
349
+ const dep = JSON.parse(fs.readFileSync(depPkg, 'utf8'));
350
+ for (const d of Object.keys(dep.dependencies || {})) resolve(d);
351
+ };
352
+ bundled.forEach(resolve);
353
+ const installed = fs.readdirSync('$PLUGIN_NM').filter(n => !n.startsWith('.'));
354
+ const toRemove = [];
355
+ for (const item of installed) {
356
+ if (item.startsWith('@')) {
357
+ const scopeDir = path.join('$PLUGIN_NM', item);
358
+ const subs = fs.readdirSync(scopeDir);
359
+ const keepSubs = subs.filter(s => keep.has(item + '/' + s));
360
+ if (keepSubs.length === 0) toRemove.push(item);
361
+ else {
362
+ for (const s of subs) {
363
+ if (!keep.has(item + '/' + s)) toRemove.push(item + '/' + s);
364
+ }
365
+ }
366
+ } else {
367
+ if (!keep.has(item)) toRemove.push(item);
368
+ }
369
+ }
370
+ process.stdout.write(toRemove.join('\n'));
371
+ " 2>/dev/null || true)
372
+ if [ -n "$_bundled_deps" ]; then
373
+ echo ""
374
+ echo "检测到 ${_before} 个包(超过阈值 50),清理多余的 peerDep 传递依赖..."
375
+ echo "$_bundled_deps" | while IFS= read -r _pkg; do
376
+ rm -rf "$PLUGIN_NM/$_pkg"
377
+ done
378
+ find "$PLUGIN_NM" -maxdepth 1 -type d -name '@*' -empty -delete 2>/dev/null || true
379
+ _after=$(ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null | wc -l | tr -d ' ')
380
+ echo " 已清理: ${_before} → ${_after} 个包"
381
+ fi
382
+ else
383
+ echo " node_modules 包数量正常(${_before} 个),无需清理"
384
+ fi
385
+ fi
386
+
387
+ # gateway 已在安装前 stop,此时不会有自动 restart 的问题
388
+ # 所有配置写入完成后,在 Step 6 统一启动
255
389
 
256
390
  # 记录更新后的 qqbot 插件版本
257
391
  NEW_QQBOT_VERSION=$(node -e '
@@ -318,10 +452,9 @@ if [ -n "$DESIRED_QQBOT_TOKEN" ]; then
318
452
  else
319
453
  echo "✅ 机器人通道配置成功"
320
454
  _config_changed=1
321
- # 重要:配置写入后 gateway 会自动检测变化并热重载(SIGUSR1)
322
- # 必须等待热重载完成,否则后续的 gateway restart 会导致连续两次重启
323
- echo "等待 gateway 热重载完成..."
324
- sleep 5
455
+ # channels 配置变更在 reload plan 中匹配为 hot reload(非 restart),
456
+ # channel 插件热重载处理,通常 <1 秒完成,无需长时间等待。
457
+ sleep 1
325
458
  fi
326
459
  else
327
460
  # 未提供任何可用 token 时,检查是否已有可用配置
@@ -446,19 +579,37 @@ start_choice=$(printf '%s' "$start_choice" | tr '[:upper:]' '[:lower:]')
446
579
  case "$start_choice" in
447
580
  y|yes)
448
581
  echo ""
449
- # 不论配置是否变更,都显式 restart 一次,确保插件正确加载
450
- # (plugins install 触发的自动重启链已在第 3 步等待完成)
582
+ # gateway 在 Step 3 安装前已 stop,此处统一 restart
451
583
  echo "正在后台重启 openclaw 网关服务..."
452
- if ! openclaw gateway restart 2>&1; then
584
+ _restart_output=$(openclaw gateway restart 2>&1) || true
585
+ echo "$_restart_output"
586
+
587
+ if echo "$_restart_output" | grep -qi "not loaded\|not found\|not running\|not installed"; then
453
588
  echo ""
454
- echo "⚠️ 后台重启失败,可能服务未安装"
455
- echo "尝试: openclaw gateway install && openclaw gateway start"
589
+ echo "⚠️ gateway 服务未加载,尝试自动恢复..."
590
+ echo ""
591
+ echo " [1/2] 注册 gateway 服务..."
592
+ _install_out=$(openclaw gateway install 2>&1) || true
593
+ echo " $_install_out"
594
+ echo ""
595
+ echo " [2/2] 启动 gateway 服务..."
596
+ _start_out=$(openclaw gateway start 2>&1) || true
597
+ echo " $_start_out"
598
+ if echo "$_start_out" | grep -qi "restart\|started\|bootstrap"; then
599
+ echo ""
600
+ echo "✅ gateway 服务恢复成功"
601
+ else
602
+ echo ""
603
+ echo "⚠️ 自动恢复可能失败,请手动执行:"
604
+ echo " openclaw gateway install && openclaw gateway start"
605
+ fi
606
+ else
607
+ echo ""
608
+ echo "✅ openclaw 网关已在后台重启"
456
609
  fi
457
610
  echo ""
458
- echo "✅ openclaw 网关已在后台重启"
459
- echo ""
460
- # 等待 gateway 端口就绪(插件安装+自动重启可能需要 30-60 秒)
461
- echo "等待 gateway 就绪(插件安装中,可能需要 30-60 秒)..."
611
+ # 等待 gateway 端口就绪
612
+ echo "等待 gateway 就绪..."
462
613
  echo "========================================="
463
614
  _port_ready=0
464
615
  for i in $(seq 1 30); do
@@ -472,32 +623,49 @@ case "$start_choice" in
472
623
  echo ""
473
624
 
474
625
  if [ "$_port_ready" -eq 0 ]; then
475
- echo "⚠️ 等待超时,gateway 可能仍在启动中"
476
- echo "请手动检查: openclaw doctor"
477
- echo "或查看日志: tail -f /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log"
478
- else
626
+ echo "⚠️ 等待超时,尝试 openclaw doctor --fix 自动修复..."
627
+ _doctor_output=$(openclaw doctor --fix 2>&1) || true
628
+ echo "$_doctor_output"
629
+ # doctor --fix 后再尝试 restart 一次
630
+ echo ""
631
+ echo "doctor 修复后重试 gateway restart..."
632
+ openclaw gateway restart 2>&1 || true
633
+ sleep 5
634
+ if lsof -i :18789 -sTCP:LISTEN >/dev/null 2>&1; then
635
+ echo "✅ doctor --fix 后 gateway 启动成功"
636
+ _port_ready=1
637
+ else
638
+ echo "❌ 仍然无法启动,请手动排查:"
639
+ echo " openclaw doctor"
640
+ echo " tail -f /tmp/openclaw/openclaw-$(date +%Y-%m-%d).log"
641
+ fi
642
+ fi
643
+
644
+ # 端口就绪后:检查 qqbot 连接 + 跟踪日志
645
+ if [ "$_port_ready" -eq 1 ]; then
479
646
  echo "✅ Gateway 端口已就绪"
480
647
  echo ""
481
- # 检查 qqbot WS 是否连接成功(最多等 30 秒)
648
+ # 检查 qqbot WS 是否连接成功(最多等 20 秒)
482
649
  echo "检查 qqbot 插件连接状态..."
483
650
  _LOG_FILE="/tmp/openclaw/openclaw-$(date +%Y-%m-%d).log"
651
+ _restart_ts=$(date +%s)
484
652
  _qqbot_ready=0
485
- for _j in $(seq 1 15); do
486
- if grep -q "Gateway ready" "$_LOG_FILE" 2>/dev/null && \
487
- _last_ready_time=$(grep "Gateway ready" "$_LOG_FILE" | tail -1 | grep -o '"date":"[^"]*"' | tail -1) && \
488
- [ -n "$_last_ready_time" ]; then
489
- _qqbot_ready=1
490
- break
653
+ for _j in $(seq 1 10); do
654
+ if [ -f "$_LOG_FILE" ]; then
655
+ _last_line=$(grep "Gateway ready" "$_LOG_FILE" 2>/dev/null | tail -1 || true)
656
+ if [ -n "$_last_line" ]; then
657
+ _qqbot_ready=1
658
+ break
659
+ fi
491
660
  fi
492
- printf "\r 等待 qqbot WS 连接... (%d/15)" "$_j"
661
+ printf "\r 等待 qqbot WS 连接... (%d/10)" "$_j"
493
662
  sleep 2
494
663
  done
495
664
  echo ""
496
665
 
497
666
  if [ "$_qqbot_ready" -eq 0 ]; then
498
- echo "⚠️ qqbot 插件可能未正确加载,尝试再次重启..."
499
- openclaw gateway restart 2>&1 || true
500
- sleep 10
667
+ echo "⚠️ qqbot 插件可能未正确加载"
668
+ echo "请检查: openclaw doctor"
501
669
  else
502
670
  echo "✅ qqbot 插件已连接"
503
671
  fi