@leoqlin/openclaw-qqbot 1.6.7-alpha1

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 (218) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +484 -0
  3. package/README.zh.md +479 -0
  4. package/bin/qqbot-cli.js +243 -0
  5. package/dist/index.d.ts +17 -0
  6. package/dist/index.js +26 -0
  7. package/dist/src/admin-resolver.d.ts +33 -0
  8. package/dist/src/admin-resolver.js +157 -0
  9. package/dist/src/api.d.ts +301 -0
  10. package/dist/src/api.js +890 -0
  11. package/dist/src/channel.d.ts +29 -0
  12. package/dist/src/channel.js +452 -0
  13. package/dist/src/config.d.ts +56 -0
  14. package/dist/src/config.js +278 -0
  15. package/dist/src/credential-backup.d.ts +31 -0
  16. package/dist/src/credential-backup.js +66 -0
  17. package/dist/src/deliver-debounce.d.ts +74 -0
  18. package/dist/src/deliver-debounce.js +174 -0
  19. package/dist/src/gateway.d.ts +18 -0
  20. package/dist/src/gateway.js +2005 -0
  21. package/dist/src/group-history.d.ts +136 -0
  22. package/dist/src/group-history.js +226 -0
  23. package/dist/src/image-server.d.ts +87 -0
  24. package/dist/src/image-server.js +570 -0
  25. package/dist/src/inbound-attachments.d.ts +60 -0
  26. package/dist/src/inbound-attachments.js +248 -0
  27. package/dist/src/known-users.d.ts +100 -0
  28. package/dist/src/known-users.js +263 -0
  29. package/dist/src/message-gating.d.ts +53 -0
  30. package/dist/src/message-gating.js +107 -0
  31. package/dist/src/message-queue.d.ts +89 -0
  32. package/dist/src/message-queue.js +257 -0
  33. package/dist/src/onboarding.d.ts +10 -0
  34. package/dist/src/onboarding.js +203 -0
  35. package/dist/src/outbound-deliver.d.ts +48 -0
  36. package/dist/src/outbound-deliver.js +392 -0
  37. package/dist/src/outbound.d.ts +205 -0
  38. package/dist/src/outbound.js +938 -0
  39. package/dist/src/proactive.d.ts +170 -0
  40. package/dist/src/proactive.js +399 -0
  41. package/dist/src/ref-index-store.d.ts +101 -0
  42. package/dist/src/ref-index-store.js +298 -0
  43. package/dist/src/reply-dispatcher.d.ts +35 -0
  44. package/dist/src/reply-dispatcher.js +311 -0
  45. package/dist/src/request-context.d.ts +25 -0
  46. package/dist/src/request-context.js +37 -0
  47. package/dist/src/runtime.d.ts +3 -0
  48. package/dist/src/runtime.js +10 -0
  49. package/dist/src/session-store.d.ts +52 -0
  50. package/dist/src/session-store.js +254 -0
  51. package/dist/src/slash-commands.d.ts +77 -0
  52. package/dist/src/slash-commands.js +1866 -0
  53. package/dist/src/startup-greeting.d.ts +30 -0
  54. package/dist/src/startup-greeting.js +97 -0
  55. package/dist/src/streaming.d.ts +247 -0
  56. package/dist/src/streaming.js +899 -0
  57. package/dist/src/stt.d.ts +21 -0
  58. package/dist/src/stt.js +70 -0
  59. package/dist/src/tools/channel.d.ts +16 -0
  60. package/dist/src/tools/channel.js +234 -0
  61. package/dist/src/tools/remind.d.ts +2 -0
  62. package/dist/src/tools/remind.js +256 -0
  63. package/dist/src/types.d.ts +367 -0
  64. package/dist/src/types.js +17 -0
  65. package/dist/src/typing-keepalive.d.ts +27 -0
  66. package/dist/src/typing-keepalive.js +64 -0
  67. package/dist/src/update-checker.d.ts +36 -0
  68. package/dist/src/update-checker.js +171 -0
  69. package/dist/src/utils/audio-convert.d.ts +98 -0
  70. package/dist/src/utils/audio-convert.js +755 -0
  71. package/dist/src/utils/chunked-upload.d.ts +68 -0
  72. package/dist/src/utils/chunked-upload.js +341 -0
  73. package/dist/src/utils/file-utils.d.ts +61 -0
  74. package/dist/src/utils/file-utils.js +172 -0
  75. package/dist/src/utils/image-size.d.ts +51 -0
  76. package/dist/src/utils/image-size.js +234 -0
  77. package/dist/src/utils/media-send.d.ts +158 -0
  78. package/dist/src/utils/media-send.js +499 -0
  79. package/dist/src/utils/media-tags.d.ts +14 -0
  80. package/dist/src/utils/media-tags.js +165 -0
  81. package/dist/src/utils/payload.d.ts +112 -0
  82. package/dist/src/utils/payload.js +186 -0
  83. package/dist/src/utils/pkg-version.d.ts +5 -0
  84. package/dist/src/utils/pkg-version.js +61 -0
  85. package/dist/src/utils/platform.d.ts +137 -0
  86. package/dist/src/utils/platform.js +390 -0
  87. package/dist/src/utils/ssrf-guard.d.ts +25 -0
  88. package/dist/src/utils/ssrf-guard.js +91 -0
  89. package/dist/src/utils/text-parsing.d.ts +36 -0
  90. package/dist/src/utils/text-parsing.js +75 -0
  91. package/dist/src/utils/upload-cache.d.ts +34 -0
  92. package/dist/src/utils/upload-cache.js +93 -0
  93. package/index.ts +31 -0
  94. package/node_modules/@eshaz/web-worker/LICENSE +201 -0
  95. package/node_modules/@eshaz/web-worker/README.md +134 -0
  96. package/node_modules/@eshaz/web-worker/browser.js +17 -0
  97. package/node_modules/@eshaz/web-worker/cjs/browser.js +16 -0
  98. package/node_modules/@eshaz/web-worker/cjs/node.js +219 -0
  99. package/node_modules/@eshaz/web-worker/index.d.ts +4 -0
  100. package/node_modules/@eshaz/web-worker/node.js +223 -0
  101. package/node_modules/@eshaz/web-worker/package.json +54 -0
  102. package/node_modules/@wasm-audio-decoders/common/index.js +5 -0
  103. package/node_modules/@wasm-audio-decoders/common/package.json +36 -0
  104. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +231 -0
  105. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +129 -0
  106. package/node_modules/@wasm-audio-decoders/common/src/puff/README +67 -0
  107. package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +31 -0
  108. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +863 -0
  109. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +35 -0
  110. package/node_modules/@wasm-audio-decoders/common/src/utilities.js +3 -0
  111. package/node_modules/@wasm-audio-decoders/common/types.d.ts +7 -0
  112. package/node_modules/mpg123-decoder/README.md +265 -0
  113. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +185 -0
  114. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +1 -0
  115. package/node_modules/mpg123-decoder/index.js +8 -0
  116. package/node_modules/mpg123-decoder/package.json +58 -0
  117. package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +464 -0
  118. package/node_modules/mpg123-decoder/src/MPEGDecoder.js +200 -0
  119. package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +21 -0
  120. package/node_modules/mpg123-decoder/types.d.ts +30 -0
  121. package/node_modules/silk-wasm/LICENSE +21 -0
  122. package/node_modules/silk-wasm/README.md +85 -0
  123. package/node_modules/silk-wasm/lib/index.cjs +16 -0
  124. package/node_modules/silk-wasm/lib/index.d.ts +70 -0
  125. package/node_modules/silk-wasm/lib/index.mjs +16 -0
  126. package/node_modules/silk-wasm/lib/silk.wasm +0 -0
  127. package/node_modules/silk-wasm/lib/utils.d.ts +4 -0
  128. package/node_modules/silk-wasm/package.json +39 -0
  129. package/node_modules/simple-yenc/.github/FUNDING.yml +1 -0
  130. package/node_modules/simple-yenc/.prettierignore +1 -0
  131. package/node_modules/simple-yenc/LICENSE +7 -0
  132. package/node_modules/simple-yenc/README.md +163 -0
  133. package/node_modules/simple-yenc/dist/esm.js +1 -0
  134. package/node_modules/simple-yenc/dist/index.js +1 -0
  135. package/node_modules/simple-yenc/package.json +50 -0
  136. package/node_modules/simple-yenc/rollup.config.js +27 -0
  137. package/node_modules/simple-yenc/src/simple-yenc.js +302 -0
  138. package/node_modules/ws/LICENSE +20 -0
  139. package/node_modules/ws/README.md +548 -0
  140. package/node_modules/ws/browser.js +8 -0
  141. package/node_modules/ws/index.js +22 -0
  142. package/node_modules/ws/lib/buffer-util.js +131 -0
  143. package/node_modules/ws/lib/constants.js +19 -0
  144. package/node_modules/ws/lib/event-target.js +292 -0
  145. package/node_modules/ws/lib/extension.js +203 -0
  146. package/node_modules/ws/lib/limiter.js +55 -0
  147. package/node_modules/ws/lib/permessage-deflate.js +528 -0
  148. package/node_modules/ws/lib/receiver.js +706 -0
  149. package/node_modules/ws/lib/sender.js +602 -0
  150. package/node_modules/ws/lib/stream.js +161 -0
  151. package/node_modules/ws/lib/subprotocol.js +62 -0
  152. package/node_modules/ws/lib/validation.js +152 -0
  153. package/node_modules/ws/lib/websocket-server.js +554 -0
  154. package/node_modules/ws/lib/websocket.js +1393 -0
  155. package/node_modules/ws/package.json +70 -0
  156. package/node_modules/ws/wrapper.mjs +21 -0
  157. package/openclaw.plugin.json +17 -0
  158. package/package.json +70 -0
  159. package/preload.cjs +33 -0
  160. package/scripts/cleanup-legacy-plugins.sh +124 -0
  161. package/scripts/link-sdk-core.cjs +185 -0
  162. package/scripts/postinstall-link-sdk.js +126 -0
  163. package/scripts/proactive-api-server.ts +369 -0
  164. package/scripts/send-proactive.ts +293 -0
  165. package/scripts/set-markdown.sh +156 -0
  166. package/scripts/test-sendmedia.ts +116 -0
  167. package/scripts/upgrade-via-npm.ps1 +460 -0
  168. package/scripts/upgrade-via-npm.sh +652 -0
  169. package/scripts/upgrade-via-source.sh +1026 -0
  170. package/skills/qqbot-channel/SKILL.md +263 -0
  171. package/skills/qqbot-channel/references/api_references.md +521 -0
  172. package/skills/qqbot-media/SKILL.md +60 -0
  173. package/skills/qqbot-remind/SKILL.md +159 -0
  174. package/src/admin-resolver.ts +181 -0
  175. package/src/api.ts +1284 -0
  176. package/src/channel.ts +477 -0
  177. package/src/config.ts +347 -0
  178. package/src/credential-backup.ts +72 -0
  179. package/src/deliver-debounce.ts +229 -0
  180. package/src/gateway.ts +2245 -0
  181. package/src/group-history.ts +328 -0
  182. package/src/image-server.ts +675 -0
  183. package/src/inbound-attachments.ts +321 -0
  184. package/src/known-users.ts +353 -0
  185. package/src/message-gating.ts +190 -0
  186. package/src/message-queue.ts +352 -0
  187. package/src/onboarding.ts +274 -0
  188. package/src/openclaw-plugin-sdk.d.ts +587 -0
  189. package/src/outbound-deliver.ts +473 -0
  190. package/src/outbound.ts +1131 -0
  191. package/src/proactive.ts +530 -0
  192. package/src/ref-index-store.ts +412 -0
  193. package/src/reply-dispatcher.ts +334 -0
  194. package/src/request-context.ts +49 -0
  195. package/src/runtime.ts +14 -0
  196. package/src/session-store.ts +303 -0
  197. package/src/slash-commands.ts +2030 -0
  198. package/src/startup-greeting.ts +120 -0
  199. package/src/streaming.ts +1077 -0
  200. package/src/stt.ts +86 -0
  201. package/src/tools/channel.ts +281 -0
  202. package/src/tools/remind.ts +308 -0
  203. package/src/types.ts +391 -0
  204. package/src/typing-keepalive.ts +59 -0
  205. package/src/update-checker.ts +186 -0
  206. package/src/utils/audio-convert.ts +859 -0
  207. package/src/utils/chunked-upload.ts +483 -0
  208. package/src/utils/file-utils.ts +193 -0
  209. package/src/utils/image-size.ts +266 -0
  210. package/src/utils/media-send.ts +631 -0
  211. package/src/utils/media-tags.ts +183 -0
  212. package/src/utils/payload.ts +265 -0
  213. package/src/utils/pkg-version.ts +64 -0
  214. package/src/utils/platform.ts +435 -0
  215. package/src/utils/ssrf-guard.ts +102 -0
  216. package/src/utils/text-parsing.ts +85 -0
  217. package/src/utils/upload-cache.ts +128 -0
  218. package/tsconfig.json +16 -0
@@ -0,0 +1,652 @@
1
+ #!/bin/bash
2
+
3
+ # qqbot 通过 openclaw 原生插件指令升级
4
+ #
5
+ # 使用 openclaw plugins install/update 原生命令进行安装和升级,
6
+ # 保留 appid/secret 配置写入、热更新 (--no-restart)、结构化输出等功能。
7
+ #
8
+ # 升级策略:
9
+ # 1. 已安装(plugins.installs 有记录)→ openclaw plugins update
10
+ # 2. 未安装 / update 失败 → 删除旧目录 + openclaw plugins install
11
+ #
12
+ # 用法:
13
+ # upgrade-via-npm.sh # 升级到 latest(默认)
14
+ # upgrade-via-npm.sh --version <version> # 升级到指定版本
15
+ # upgrade-via-npm.sh --self-version # 升级到当前仓库 package.json 版本
16
+ # upgrade-via-npm.sh --appid <appid> --secret <secret> # 首次安装时配置 appid/secret
17
+ # upgrade-via-npm.sh --no-restart # 只做文件替换,不重启 gateway(供热更指令使用)
18
+
19
+ set -eo pipefail
20
+
21
+ # ⚠️ 必须在 cd 之前解析脚本路径,否则相对路径的 $0 在 cd 后无法正确解析
22
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
23
+ PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
24
+
25
+ # 确保 cwd 是一个存在的目录。
26
+ # 当从 gateway 进程 fork 时,继承的 cwd 可能已被删除(如旧插件目录被 mv/rm),
27
+ # 导致 openclaw CLI 启动时 process.cwd() 报 ENOENT: uv_cwd 错误。
28
+ cd "$HOME" 2>/dev/null || cd / 2>/dev/null || true
29
+
30
+ # 异常退出时清理临时文件并回滚(防止泄露或残留)
31
+ INSTALL_COMPLETED=false # 标记 install 是否已完成(用于区分正常退出和异常退出)
32
+ cleanup_on_exit() {
33
+ local exit_code=$?
34
+
35
+ # 异常退出时同步临时配置中的 install 记录回真实配置
36
+ if [ -n "$TEMP_CONFIG_FILE" ] && [ -f "$TEMP_CONFIG_FILE" ]; then
37
+ # 尝试同步 install 记录(即使异常退出也要保留)
38
+ node -e "
39
+ try {
40
+ const fs = require('fs');
41
+ const tmp = JSON.parse(fs.readFileSync('$TEMP_CONFIG_FILE', 'utf8'));
42
+ const real = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
43
+ if (tmp.plugins && tmp.plugins.installs) {
44
+ if (!real.plugins) real.plugins = {};
45
+ real.plugins.installs = { ...(real.plugins.installs || {}), ...tmp.plugins.installs };
46
+ }
47
+ if (tmp.plugins && tmp.plugins.entries) {
48
+ if (!real.plugins) real.plugins = {};
49
+ real.plugins.entries = { ...(real.plugins.entries || {}), ...tmp.plugins.entries };
50
+ }
51
+ fs.writeFileSync('$CONFIG_FILE', JSON.stringify(real, null, 4) + '\n');
52
+ } catch {}
53
+ " 2>/dev/null || true
54
+ rm -f "$TEMP_CONFIG_FILE" 2>/dev/null || true
55
+ fi
56
+
57
+ # 异常退出且 install 未完成时,回滚备份目录(而非删除)
58
+ if [ "$INSTALL_COMPLETED" != "true" ] && [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
59
+ if [ ! -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] || [ ! -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ]; then
60
+ # 插件目录不存在或不完整,回滚
61
+ rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
62
+ mv "$BACKUP_DIR/$PLUGIN_ID" "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || \
63
+ mv "$BACKUP_DIR"/* "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
64
+ echo " ↩️ [cleanup] 异常退出,已回滚到旧版本"
65
+ else
66
+ # 插件目录完整,清理备份
67
+ rm -rf "$BACKUP_DIR" 2>/dev/null || true
68
+ fi
69
+ elif [ "$INSTALL_COMPLETED" = "true" ] && [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
70
+ # 正常完成,清理备份
71
+ rm -rf "$BACKUP_DIR" 2>/dev/null || true
72
+ fi
73
+
74
+ # 清理 openclaw install 可能残留的暂存目录(extensions 和 /tmp 中都可能存在)
75
+ find "${EXTENSIONS_DIR:-/dev/null}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
76
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
77
+ }
78
+ trap cleanup_on_exit EXIT
79
+
80
+ # 清理上次升级可能遗留的备份目录(如上次脚本被 kill 等极端情况)
81
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".qqbot-upgrade-backup-*" -exec rm -rf {} + 2>/dev/null || true
82
+
83
+ PKG_NAME="@tencent-connect/openclaw-qqbot"
84
+ PLUGIN_ID="openclaw-qqbot"
85
+ INSTALL_SRC=""
86
+ TARGET_VERSION=""
87
+ APPID=""
88
+ SECRET=""
89
+ NO_RESTART=false
90
+
91
+ LOCAL_VERSION="$(node -e "
92
+ try {
93
+ const fs = require('fs');
94
+ const path = require('path');
95
+ const p = path.join('$PROJECT_DIR', 'package.json');
96
+ const v = JSON.parse(fs.readFileSync(p, 'utf8')).version;
97
+ if (v) process.stdout.write(String(v));
98
+ } catch {}
99
+ " 2>/dev/null || true)"
100
+
101
+ print_usage() {
102
+ echo "用法:"
103
+ echo " upgrade-via-npm.sh # 升级到 latest(默认)"
104
+ echo " upgrade-via-npm.sh --version <版本号> # 升级到指定版本"
105
+ if [ -n "$LOCAL_VERSION" ]; then
106
+ echo " upgrade-via-npm.sh --self-version # 升级到当前仓库版本($LOCAL_VERSION)"
107
+ else
108
+ echo " upgrade-via-npm.sh --self-version # 升级到当前仓库版本"
109
+ fi
110
+ echo ""
111
+ echo " --pkg <scope/name> 指定 npm 包名(如 ryantest/openclaw-qqbot)"
112
+ echo " --appid <appid> QQ机器人 appid(首次安装时必填)"
113
+ echo " --secret <secret> QQ机器人 secret(首次安装时必填)"
114
+ echo ""
115
+ echo "也可以通过环境变量设置:"
116
+ echo " QQBOT_APPID QQ机器人 appid"
117
+ echo " QQBOT_SECRET QQ机器人 secret"
118
+ echo " QQBOT_TOKEN QQ机器人 token (appid:secret)"
119
+ }
120
+
121
+ while [[ $# -gt 0 ]]; do
122
+ case "$1" in
123
+ --tag)
124
+ [ -z "$2" ] && echo "❌ --tag 需要参数" && exit 1
125
+ TARGET_VERSION="${2#v}" # 去掉 v 前缀(npm 版本号不带 v)
126
+ shift 2
127
+ ;;
128
+ --version)
129
+ [ -z "$2" ] && echo "❌ --version 需要参数" && exit 1
130
+ TARGET_VERSION="${2#v}" # 去掉 v 前缀(npm 版本号不带 v)
131
+ shift 2
132
+ ;;
133
+ --self-version)
134
+ [ -z "$LOCAL_VERSION" ] && echo "❌ 无法从 package.json 读取版本" && exit 1
135
+ TARGET_VERSION="$LOCAL_VERSION"
136
+ shift 1
137
+ ;;
138
+ --appid)
139
+ [ -z "$2" ] && echo "❌ --appid 需要参数" && exit 1
140
+ APPID="$2"
141
+ shift 2
142
+ ;;
143
+ --secret)
144
+ [ -z "$2" ] && echo "❌ --secret 需要参数" && exit 1
145
+ SECRET="$2"
146
+ shift 2
147
+ ;;
148
+ --pkg)
149
+ [ -z "$2" ] && echo "❌ --pkg 需要参数" && exit 1
150
+ _pkg="$2"
151
+ # 支持 "scope/name" 自动补 @
152
+ if [[ "$_pkg" != @* ]]; then _pkg="@$_pkg"; fi
153
+ PKG_NAME="$_pkg"
154
+ shift 2
155
+ ;;
156
+ --no-restart)
157
+ NO_RESTART=true
158
+ shift 1
159
+ ;;
160
+ -h|--help)
161
+ print_usage
162
+ exit 0
163
+ ;;
164
+ *) echo "未知选项: $1"; print_usage; exit 1 ;;
165
+ esac
166
+ done
167
+ # 参数解析完毕后统一拼接 INSTALL_SRC(确保 --pkg 无论在 --version 前后都能生效)
168
+ if [ -n "$TARGET_VERSION" ]; then
169
+ INSTALL_SRC="${PKG_NAME}@${TARGET_VERSION}"
170
+ else
171
+ INSTALL_SRC="${PKG_NAME}@latest"
172
+ fi
173
+
174
+ # 环境变量 fallback
175
+ APPID="${APPID:-$QQBOT_APPID}"
176
+ SECRET="${SECRET:-$QQBOT_SECRET}"
177
+ if [ -z "$APPID" ] && [ -z "$SECRET" ] && [ -n "$QQBOT_TOKEN" ]; then
178
+ APPID="${QQBOT_TOKEN%%:*}"
179
+ SECRET="${QQBOT_TOKEN#*:}"
180
+ fi
181
+
182
+ # 检测 CLI
183
+ CMD=""
184
+ for name in openclaw clawdbot moltbot; do
185
+ command -v "$name" &>/dev/null && CMD="$name" && break
186
+ done
187
+ [ -z "$CMD" ] && echo "❌ 未找到 openclaw / clawdbot / moltbot" && exit 1
188
+
189
+ EXTENSIONS_DIR="$HOME/.$CMD/extensions"
190
+
191
+ # 检测 openclaw 版本
192
+ OPENCLAW_VERSION="$($CMD --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1 || true)"
193
+
194
+ echo "==========================================="
195
+ echo " qqbot 升级: $INSTALL_SRC"
196
+ echo " openclaw 版本: ${OPENCLAW_VERSION:-unknown}"
197
+ echo "==========================================="
198
+ echo ""
199
+
200
+ # 记录升级前的版本
201
+ OLD_VERSION=""
202
+ OLD_PKG="$EXTENSIONS_DIR/$PLUGIN_ID/package.json"
203
+ if [ -f "$OLD_PKG" ]; then
204
+ OLD_VERSION="$(node -e "
205
+ try {
206
+ const v = JSON.parse(require('fs').readFileSync('$OLD_PKG', 'utf8')).version;
207
+ if (v) process.stdout.write(String(v));
208
+ } catch {}
209
+ " 2>/dev/null || true)"
210
+ echo " 当前版本: ${OLD_VERSION:-unknown}"
211
+ fi
212
+
213
+ # [1/4] 通过 openclaw 原生指令安装/升级
214
+ echo ""
215
+ echo "[1/4] 安装/升级插件..."
216
+
217
+ # ── 兼容 openclaw 3.23+ 配置严格校验 ──
218
+ # 3.23+ 在 plugins install/update 时会校验整个配置文件,
219
+ # 如果 channels.qqbot 已存在但 qqbot 插件尚未加载,校验会失败。
220
+ #
221
+ # ⚠️ 关键:绝不能直接修改真实的 openclaw.json,否则 gateway 的 config file watcher
222
+ # 会检测到变更并触发 SIGUSR1 重启,导致正在执行的升级脚本被杀死。
223
+ #
224
+ # 解决:创建临时配置副本(不含 channels.qqbot),通过 OPENCLAW_CONFIG_PATH
225
+ # 环境变量让 plugins install/update 使用临时配置,真实配置文件不受影响。
226
+ CONFIG_FILE="$HOME/.$CMD/$CMD.json"
227
+ TEMP_CONFIG_FILE=""
228
+ NEEDS_TEMP_CONFIG=false
229
+
230
+ if [ -f "$CONFIG_FILE" ]; then
231
+ NEEDS_TEMP_CONFIG="$(node -e "
232
+ try {
233
+ const fs = require('fs');
234
+ const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
235
+ const hasChannel = !!(cfg.channels && cfg.channels.qqbot);
236
+ const hasAllow = Array.isArray(cfg.plugins?.allow) && cfg.plugins.allow.includes('$PLUGIN_ID');
237
+ const hasEntry = !!(cfg.plugins?.entries?.['$PLUGIN_ID']);
238
+ if (hasChannel || hasAllow || hasEntry) process.stdout.write('true');
239
+ } catch {}
240
+ " 2>/dev/null || true)"
241
+
242
+ if [ "$NEEDS_TEMP_CONFIG" = "true" ]; then
243
+ TEMP_CONFIG_FILE="$(mktemp)"
244
+ node -e "
245
+ try {
246
+ const fs = require('fs');
247
+ const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
248
+ // 移除 channels.qqbot(插件自定义通道,校验时会 unknown channel id)
249
+ if (cfg.channels?.qqbot) {
250
+ delete cfg.channels.qqbot;
251
+ if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
252
+ }
253
+ // 移除 plugins.allow 中的 openclaw-qqbot(插件目录被备份后校验找不到)
254
+ if (Array.isArray(cfg.plugins?.allow)) {
255
+ cfg.plugins.allow = cfg.plugins.allow.filter(p => p !== '$PLUGIN_ID');
256
+ if (cfg.plugins.allow.length === 0) delete cfg.plugins.allow;
257
+ }
258
+ // 移除 plugins.entries 中的 openclaw-qqbot(同理)
259
+ if (cfg.plugins?.entries?.['$PLUGIN_ID']) {
260
+ delete cfg.plugins.entries['$PLUGIN_ID'];
261
+ if (Object.keys(cfg.plugins.entries).length === 0) delete cfg.plugins.entries;
262
+ }
263
+ fs.writeFileSync('$TEMP_CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n');
264
+ } catch(e) { process.exit(1); }
265
+ " 2>/dev/null
266
+ if [ $? -eq 0 ]; then
267
+ echo " [兼容] 创建临时配置副本(不含 channels.qqbot / plugins.allow / plugins.entries)以通过配置校验"
268
+ export OPENCLAW_CONFIG_PATH="$TEMP_CONFIG_FILE"
269
+ else
270
+ echo " ⚠️ 创建临时配置失败,继续使用原配置"
271
+ TEMP_CONFIG_FILE=""
272
+ fi
273
+ fi
274
+ fi
275
+
276
+ # 清理临时配置的函数
277
+ # plugins install/update 可能把 install 记录写入了临时配置,需要同步回真实配置
278
+ restore_qqbot_channel() {
279
+ if [ -n "$TEMP_CONFIG_FILE" ] && [ -f "$TEMP_CONFIG_FILE" ]; then
280
+ # 将临时配置中 plugins.installs 和 plugins.entries 的变更同步回真实配置
281
+ node -e "
282
+ try {
283
+ const fs = require('fs');
284
+ const tmp = JSON.parse(fs.readFileSync('$TEMP_CONFIG_FILE', 'utf8'));
285
+ const real = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
286
+ let changed = false;
287
+ if (tmp.plugins && tmp.plugins.installs) {
288
+ if (!real.plugins) real.plugins = {};
289
+ real.plugins.installs = { ...(real.plugins.installs || {}), ...tmp.plugins.installs };
290
+ changed = true;
291
+ }
292
+ // 同步 plugins.entries(openclaw plugins install 会写入 entries)
293
+ if (tmp.plugins && tmp.plugins.entries) {
294
+ if (!real.plugins) real.plugins = {};
295
+ real.plugins.entries = { ...(real.plugins.entries || {}), ...tmp.plugins.entries };
296
+ changed = true;
297
+ }
298
+ if (changed) {
299
+ fs.writeFileSync('$CONFIG_FILE', JSON.stringify(real, null, 4) + '\n');
300
+ }
301
+ } catch {}
302
+ " 2>/dev/null || true
303
+ rm -f "$TEMP_CONFIG_FILE"
304
+ unset OPENCLAW_CONFIG_PATH
305
+ echo " [兼容] 已同步 install/entries 记录并清理临时配置副本"
306
+ fi
307
+ }
308
+
309
+ UPGRADE_OK=false
310
+
311
+ # 检测安装状态:同时检查配置记录和磁盘目录
312
+ HAS_INSTALL_RECORD="$(node -e "
313
+ try {
314
+ const fs = require('fs');
315
+ const p = '$HOME/.$CMD/$CMD.json';
316
+ const cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
317
+ const inst = cfg.plugins && cfg.plugins.installs && cfg.plugins.installs['$PLUGIN_ID'];
318
+ if (inst) process.stdout.write('yes');
319
+ } catch {}
320
+ " 2>/dev/null || true)"
321
+ HAS_PLUGIN_DIR=false
322
+ [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && [ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ] && HAS_PLUGIN_DIR=true
323
+
324
+ # 决策矩阵:
325
+ # 配置有记录 + 目录存在 → update(最佳路径)
326
+ # 配置有记录 + 目录不存在 → 清理残留记录,走 install
327
+ # 配置无记录 + 目录存在 → 删目录,走 install(配置与文件不一致)
328
+ # 配置无记录 + 目录不存在 → 走 install(全新安装)
329
+ #
330
+ # 指定了具体版本(--version/--tag/--self-version)时:
331
+ # update 不支持指定版本,直接走 删除 + install
332
+
333
+ USE_UPDATE=false
334
+
335
+ if [ "$HAS_INSTALL_RECORD" = "yes" ] && [ "$HAS_PLUGIN_DIR" = "true" ] && [ -z "$TARGET_VERSION" ]; then
336
+ # 配置和目录都齐全,且未指定版本 → 走 update
337
+ USE_UPDATE=true
338
+ echo " [检测] 配置记录 ✓ | 插件目录 ✓ | 未指定版本 → 使用 update"
339
+ elif [ "$HAS_INSTALL_RECORD" = "yes" ] && [ "$HAS_PLUGIN_DIR" = "true" ]; then
340
+ echo " [检测] 配置记录 ✓ | 插件目录 ✓ | 指定版本 $TARGET_VERSION → 使用 reinstall"
341
+ elif [ "$HAS_INSTALL_RECORD" = "yes" ]; then
342
+ echo " [检测] 配置记录 ✓ | 插件目录 ✗ → 配置与文件不一致,使用 install"
343
+ elif [ "$HAS_PLUGIN_DIR" = "true" ]; then
344
+ echo " [检测] 配置记录 ✗ | 插件目录 ✓ → 目录残留,清理后 install"
345
+ else
346
+ echo " [检测] 配置记录 ✗ | 插件目录 ✗ → 全新安装"
347
+ fi
348
+
349
+ if [ "$USE_UPDATE" = "true" ]; then
350
+ echo " 尝试 update..."
351
+ if $CMD plugins update "$PLUGIN_ID" 2>&1; then
352
+ # update 返回 0 不一定真的更新了,检查版本是否变化
353
+ POST_UPDATE_VERSION=""
354
+ if [ -f "$OLD_PKG" ]; then
355
+ POST_UPDATE_VERSION="$(node -e "
356
+ try {
357
+ const v = JSON.parse(require('fs').readFileSync('$OLD_PKG', 'utf8')).version;
358
+ if (v) process.stdout.write(String(v));
359
+ } catch {}
360
+ " 2>/dev/null || true)"
361
+ fi
362
+ if [ -n "$POST_UPDATE_VERSION" ] && [ "$POST_UPDATE_VERSION" != "$OLD_VERSION" ]; then
363
+ UPGRADE_OK=true
364
+ echo " ✅ update 成功 ($OLD_VERSION → $POST_UPDATE_VERSION)"
365
+ elif [ -z "$OLD_VERSION" ]; then
366
+ # 之前没有旧版本,无法比较,信任 update 结果
367
+ UPGRADE_OK=true
368
+ echo " ✅ update 成功"
369
+ else
370
+ echo " ⚠️ update 返回成功但版本未变 ($POST_UPDATE_VERSION),回退到 reinstall..."
371
+ fi
372
+ else
373
+ echo " ⚠️ update 失败,回退到 reinstall..."
374
+ fi
375
+ fi
376
+
377
+ if [ "$UPGRADE_OK" != "true" ]; then
378
+ # 备份旧目录(而非直接删除),install 失败时可回滚
379
+ BACKUP_DIR=""
380
+ if [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ]; then
381
+ BACKUP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/.qqbot-upgrade-backup-XXXXXX")"
382
+ mv "$EXTENSIONS_DIR/$PLUGIN_ID" "$BACKUP_DIR"
383
+ echo " 已备份旧目录: $BACKUP_DIR"
384
+ fi
385
+
386
+ # 清理历史遗留名称(这些不需要回滚)
387
+ for dir_name in qqbot openclaw-qq; do
388
+ [ -d "$EXTENSIONS_DIR/$dir_name" ] && rm -rf "$EXTENSIONS_DIR/$dir_name" && echo " 已清理历史目录: $EXTENSIONS_DIR/$dir_name"
389
+ done
390
+
391
+ echo " 执行 install: $INSTALL_SRC"
392
+
393
+ if $CMD plugins install "$INSTALL_SRC" --pin 2>&1; then
394
+ # install 返回 0,但需要验证插件目录是否真的存在且完整
395
+ if [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && [ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ]; then
396
+ UPGRADE_OK=true
397
+ INSTALL_COMPLETED=true
398
+ echo " ✅ install 成功"
399
+ # install 成功,清理备份
400
+ if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
401
+ rm -rf "$BACKUP_DIR"
402
+ echo " 已清理旧版备份"
403
+ fi
404
+ # 清理 openclaw CLI install 可能留下的额外 backup 目录(extensions 内遗留 + 新路径)
405
+ find "$EXTENSIONS_DIR" -maxdepth 1 -name ".openclaw-qqbot-backup-*" -exec rm -rf {} + 2>/dev/null || true
406
+ find "${EXTENSIONS_DIR:-/dev/null}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
407
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
408
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".qqbot-upgrade-backup-*" -exec rm -rf {} + 2>/dev/null || true
409
+ else
410
+ echo " ❌ install 命令返回成功但插件目录不完整"
411
+ echo " [诊断] 目录存在: $([ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && echo '是' || echo '否')"
412
+ echo " [诊断] package.json 存在: $([ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ] && echo '是' || echo '否')"
413
+ # 清理可能残留的暂存目录(extensions 和 /tmp 中都可能存在)
414
+ find "${EXTENSIONS_DIR:-/dev/null}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
415
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
416
+ # 回滚
417
+ if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
418
+ rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
419
+ # 备份目录内可能是 PLUGIN_ID 子目录或直接是内容
420
+ if [ -d "$BACKUP_DIR/$PLUGIN_ID" ]; then
421
+ mv "$BACKUP_DIR/$PLUGIN_ID" "$EXTENSIONS_DIR/$PLUGIN_ID"
422
+ else
423
+ mv "$BACKUP_DIR" "$EXTENSIONS_DIR/$PLUGIN_ID"
424
+ fi
425
+ echo " ↩️ 已回滚到旧版本"
426
+ fi
427
+ restore_qqbot_channel
428
+ echo "QQBOT_NEW_VERSION=unknown"
429
+ echo "QQBOT_REPORT=❌ QQBot 安装异常(目录不完整,已回滚),请重试或手动安装"
430
+ exit 1
431
+ fi
432
+ else
433
+ echo " ❌ install 失败"
434
+ # 清理可能残留的暂存目录(extensions 和 /tmp 中都可能存在)
435
+ find "${EXTENSIONS_DIR:-/dev/null}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
436
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
437
+ # 回滚:恢复旧目录
438
+ if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
439
+ rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
440
+ if [ -d "$BACKUP_DIR/$PLUGIN_ID" ]; then
441
+ mv "$BACKUP_DIR/$PLUGIN_ID" "$EXTENSIONS_DIR/$PLUGIN_ID"
442
+ else
443
+ mv "$BACKUP_DIR" "$EXTENSIONS_DIR/$PLUGIN_ID"
444
+ fi
445
+ echo " ↩️ 已回滚到旧版本"
446
+ fi
447
+ restore_qqbot_channel
448
+ echo "QQBOT_NEW_VERSION=unknown"
449
+ echo "QQBOT_REPORT=❌ QQBot 安装失败(已回滚到旧版本),请检查网络和 npm registry"
450
+ exit 1
451
+ fi
452
+ fi
453
+
454
+ # install/update 完成,恢复 channels.qqbot
455
+ restore_qqbot_channel
456
+
457
+ # [2/4] 验证安装
458
+ echo ""
459
+ echo "[2/4] 验证安装..."
460
+
461
+ PKG_JSON="$EXTENSIONS_DIR/$PLUGIN_ID/package.json"
462
+ if [ -f "$PKG_JSON" ]; then
463
+ NEW_VERSION="$(node -e "process.stdout.write(JSON.parse(require('fs').readFileSync(process.argv[1],'utf8')).version||'')" "$PKG_JSON" 2>/dev/null || true)"
464
+ fi
465
+
466
+ # Preflight 检查
467
+ PREFLIGHT_OK=true
468
+ TARGET_DIR="$EXTENSIONS_DIR/$PLUGIN_ID"
469
+
470
+ if [ -z "$NEW_VERSION" ]; then
471
+ echo " ❌ 无法读取新版本号"
472
+ PREFLIGHT_OK=false
473
+ else
474
+ echo " ✅ 版本号: $NEW_VERSION"
475
+ fi
476
+
477
+ # 入口文件
478
+ ENTRY_FILE=""
479
+ for candidate in "dist/index.js" "index.js"; do
480
+ if [ -f "$TARGET_DIR/$candidate" ]; then
481
+ ENTRY_FILE="$candidate"
482
+ break
483
+ fi
484
+ done
485
+ if [ -z "$ENTRY_FILE" ]; then
486
+ echo " ❌ 缺少入口文件(dist/index.js 或 index.js)"
487
+ PREFLIGHT_OK=false
488
+ else
489
+ echo " ✅ 入口文件: $ENTRY_FILE"
490
+ fi
491
+
492
+ # 核心目录
493
+ if [ -d "$TARGET_DIR/dist/src" ]; then
494
+ CORE_JS_COUNT=$(find "$TARGET_DIR/dist/src" -name "*.js" -type f 2>/dev/null | wc -l | tr -d ' ')
495
+ echo " ✅ dist/src/ 包含 ${CORE_JS_COUNT} 个 JS 文件"
496
+ if [ "$CORE_JS_COUNT" -lt 5 ]; then
497
+ echo " ❌ JS 文件数量异常偏少(预期 ≥ 5,实际 ${CORE_JS_COUNT})"
498
+ PREFLIGHT_OK=false
499
+ fi
500
+ else
501
+ echo " ❌ 缺少核心目录 dist/src/"
502
+ PREFLIGHT_OK=false
503
+ fi
504
+
505
+ # 关键模块
506
+ MISSING_MODULES=""
507
+ for module in "dist/src/gateway.js" "dist/src/api.js" "dist/src/admin-resolver.js"; do
508
+ if [ ! -f "$TARGET_DIR/$module" ]; then
509
+ MISSING_MODULES="$MISSING_MODULES $module"
510
+ fi
511
+ done
512
+ if [ -n "$MISSING_MODULES" ]; then
513
+ echo " ❌ 缺少关键模块:$MISSING_MODULES"
514
+ PREFLIGHT_OK=false
515
+ else
516
+ echo " ✅ 关键模块完整"
517
+ fi
518
+
519
+ # bundled 依赖
520
+ if [ -d "$TARGET_DIR/node_modules" ]; then
521
+ BUNDLED_OK=true
522
+ for dep in "ws" "silk-wasm"; do
523
+ if [ ! -d "$TARGET_DIR/node_modules/$dep" ]; then
524
+ echo " ⚠️ bundled 依赖缺失: $dep"
525
+ BUNDLED_OK=false
526
+ fi
527
+ done
528
+ if $BUNDLED_OK; then
529
+ echo " ✅ 核心 bundled 依赖完整"
530
+ fi
531
+ fi
532
+
533
+ if [ "$PREFLIGHT_OK" != "true" ]; then
534
+ echo ""
535
+ echo "❌ 验证未通过"
536
+ echo "QQBOT_NEW_VERSION=unknown"
537
+ echo "QQBOT_REPORT=⚠️ QQBot 升级异常,验证未通过"
538
+ exit 1
539
+ fi
540
+ echo " ✅ 验证全部通过"
541
+
542
+ # 确保 openclaw/plugin-sdk 可解析:
543
+ # openclaw plugins install 不会执行 npm lifecycle scripts,
544
+ # 需要手动调用 postinstall-link-sdk.js 创建 node_modules/openclaw → 全局 openclaw 的符号链接
545
+ POSTINSTALL_SCRIPT="$TARGET_DIR/scripts/postinstall-link-sdk.js"
546
+ if [ -f "$POSTINSTALL_SCRIPT" ]; then
547
+ echo " 执行 postinstall-link-sdk..."
548
+ if node "$POSTINSTALL_SCRIPT" 2>&1; then
549
+ echo " ✅ plugin-sdk 链接就绪"
550
+ else
551
+ echo " ⚠️ postinstall-link-sdk 失败,插件可能无法加载"
552
+ fi
553
+ fi
554
+
555
+ # [3/4] 输出结构化信息(供 TS handler 解析)
556
+ echo ""
557
+ echo "[3/4] 升级结果..."
558
+ echo "QQBOT_NEW_VERSION=${NEW_VERSION:-unknown}"
559
+
560
+ if [ -n "$NEW_VERSION" ] && [ "$NEW_VERSION" != "unknown" ]; then
561
+ echo "QQBOT_REPORT=✅ QQBot 升级完成: v${NEW_VERSION}"
562
+ else
563
+ echo "QQBOT_REPORT=⚠️ QQBot 升级异常,无法确认新版本"
564
+ fi
565
+
566
+ echo ""
567
+ echo "==========================================="
568
+ echo " ✅ 安装完成"
569
+ echo "==========================================="
570
+
571
+ # --no-restart 模式(热更新场景):立即退出,让调用方触发 gateway restart
572
+ if [ "$NO_RESTART" = "true" ]; then
573
+ echo ""
574
+ echo "[跳过重启] --no-restart 已指定,脚本立即退出以便调用方触发 gateway restart"
575
+ exit 0
576
+ fi
577
+
578
+ # 以下步骤仅在非热更新(手动执行)场景中执行
579
+
580
+ # [配置] appid/secret(仅在提供了参数时执行)
581
+ if [ -n "$APPID" ] && [ -n "$SECRET" ]; then
582
+ echo ""
583
+ echo "[配置] 写入 qqbot 通道配置..."
584
+ DESIRED_TOKEN="${APPID}:${SECRET}"
585
+
586
+ # 读取当前已有的 token
587
+ CURRENT_TOKEN=""
588
+ for _app in openclaw clawdbot moltbot; do
589
+ _cfg="$HOME/.$_app/$_app.json"
590
+ if [ -f "$_cfg" ]; then
591
+ CURRENT_TOKEN=$(node -e "
592
+ const cfg = JSON.parse(require('fs').readFileSync('$_cfg', 'utf8'));
593
+ const keys = ['qqbot', 'openclaw-qqbot', 'openclaw-qq'];
594
+ for (const key of keys) {
595
+ const ch = cfg.channels && cfg.channels[key];
596
+ if (!ch) continue;
597
+ if (ch.token) { process.stdout.write(ch.token); process.exit(0); }
598
+ if (ch.appId && ch.clientSecret) { process.stdout.write(ch.appId + ':' + ch.clientSecret); process.exit(0); }
599
+ }
600
+ " 2>/dev/null || true)
601
+ [ -n "$CURRENT_TOKEN" ] && break
602
+ fi
603
+ done
604
+
605
+ if [ "$CURRENT_TOKEN" = "$DESIRED_TOKEN" ]; then
606
+ echo " ✅ 当前配置已是目标值,跳过写入"
607
+ else
608
+ # qqbot 是插件自定义通道,openclaw channels add --channel 不支持,
609
+ # 直接编辑配置文件写入 channels.qqbot
610
+ CONFIG_FILE="$HOME/.$CMD/$CMD.json"
611
+ if [ -f "$CONFIG_FILE" ] && node -e "
612
+ const fs = require('fs');
613
+ const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
614
+ if (!cfg.channels) cfg.channels = {};
615
+ if (!cfg.channels.qqbot) cfg.channels.qqbot = {};
616
+ cfg.channels.qqbot.appId = '$APPID';
617
+ cfg.channels.qqbot.clientSecret = '$SECRET';
618
+ fs.writeFileSync('$CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n');
619
+ " 2>&1; then
620
+ echo " ✅ 通道配置写入成功"
621
+ else
622
+ echo " ❌ 配置写入失败,请手动编辑 $CONFIG_FILE 添加 channels.qqbot:"
623
+ echo " { \"channels\": { \"qqbot\": { \"appId\": \"$APPID\", \"clientSecret\": \"...\" } } }"
624
+ fi
625
+ fi
626
+ elif [ -n "$APPID" ] || [ -n "$SECRET" ]; then
627
+ echo ""
628
+ echo "⚠️ --appid 和 --secret 必须同时提供"
629
+ fi
630
+
631
+ # [4/4] 重启 gateway 使新版本生效
632
+ echo ""
633
+
634
+ # 手动升级场景:提前写入 startup-marker,阻止重启后 bot 重复推送升级通知
635
+ if [ -n "$NEW_VERSION" ] && [ "$NEW_VERSION" != "unknown" ]; then
636
+ MARKER_DIR="$HOME/.openclaw/qqbot/data"
637
+ mkdir -p "$MARKER_DIR"
638
+ MARKER_FILE="$MARKER_DIR/startup-marker.json"
639
+ NOW="$(date -u +%Y-%m-%dT%H:%M:%S.000Z 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)"
640
+ echo "{\"version\":\"$NEW_VERSION\",\"startedAt\":\"$NOW\",\"greetedAt\":\"$NOW\"}" > "$MARKER_FILE"
641
+ fi
642
+
643
+ echo "[重启] 重启 gateway 使新版本生效..."
644
+ if $CMD gateway restart 2>&1; then
645
+ echo " ✅ gateway 已重启"
646
+ echo ""
647
+ if [ -n "$NEW_VERSION" ] && [ "$NEW_VERSION" != "unknown" ]; then
648
+ echo "🎉 QQBot 插件已更新至 v${NEW_VERSION},在线等候你的吩咐。"
649
+ fi
650
+ else
651
+ echo " ⚠️ gateway 重启失败,请手动执行: $CMD gateway restart"
652
+ fi