@leoqlin/openclaw-qqbot 1.6.9 → 1.6.11
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/src/api.d.ts +4 -2
- package/dist/src/api.js +15 -6
- package/dist/src/gateway.js +2 -2
- package/dist/src/runtime.js +3 -0
- package/package.json +1 -1
- package/scripts/upgrade-via-npm.sh +674 -504
- package/skills/qqbot-upgrade/SKILL.md +2 -2
- package/src/api.ts +14 -6
- package/src/gateway.ts +2 -2
- package/src/openclaw-plugin-sdk.d.ts +2 -0
- package/src/runtime.ts +3 -0
|
@@ -1,652 +1,822 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
|
|
3
|
-
# qqbot 通过 openclaw
|
|
3
|
+
# qqbot 通过 openclaw 原生插件指令升级(v4)
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# 1. 已安装(plugins.installs 有记录)→ openclaw plugins update
|
|
10
|
-
# 2. 未安装 / update 失败 → 删除旧目录 + openclaw plugins install
|
|
5
|
+
# 两级降级策略:
|
|
6
|
+
# Level 1: openclaw plugins install/update(原生命令,经 ClawHub → npm)
|
|
7
|
+
# Level 2: npm pack 下载 + 解压 + openclaw plugins install <本地目录>(绕过 ClawHub + 安全扫描 bug,保留原子部署)
|
|
8
|
+
# 全部失败 → 回滚到用户原有版本
|
|
11
9
|
#
|
|
12
10
|
# 用法:
|
|
13
|
-
# upgrade-via-npm.sh # 升级到 latest
|
|
11
|
+
# upgrade-via-npm.sh # 升级到 latest
|
|
14
12
|
# upgrade-via-npm.sh --version <version> # 升级到指定版本
|
|
15
13
|
# upgrade-via-npm.sh --self-version # 升级到当前仓库 package.json 版本
|
|
16
14
|
# upgrade-via-npm.sh --appid <appid> --secret <secret> # 首次安装时配置 appid/secret
|
|
17
|
-
# upgrade-via-npm.sh --no-restart # 只做文件替换,不重启 gateway
|
|
15
|
+
# upgrade-via-npm.sh --no-restart # 只做文件替换,不重启 gateway
|
|
16
|
+
# upgrade-via-npm.sh --timeout 600 # 自定义安装超时时间(秒)
|
|
18
17
|
|
|
19
18
|
set -eo pipefail
|
|
20
19
|
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
# ============================================================================
|
|
21
|
+
# 进程隔离 — 脱离 gateway 进程组
|
|
22
|
+
# ============================================================================
|
|
23
|
+
if [ -z "$_UPGRADE_ISOLATED" ] && [ -f "$0" ] && command -v setsid &>/dev/null; then
|
|
24
|
+
export _UPGRADE_ISOLATED=1
|
|
25
|
+
exec setsid "$0" "$@"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
# ============================================================================
|
|
29
|
+
# 环境准备
|
|
30
|
+
# ============================================================================
|
|
31
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)" || SCRIPT_DIR=""
|
|
32
|
+
PROJECT_DIR=""
|
|
33
|
+
[ -n "$SCRIPT_DIR" ] && PROJECT_DIR="$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)" || true
|
|
24
34
|
|
|
25
|
-
# 确保 cwd 是一个存在的目录。
|
|
26
|
-
# 当从 gateway 进程 fork 时,继承的 cwd 可能已被删除(如旧插件目录被 mv/rm),
|
|
27
|
-
# 导致 openclaw CLI 启动时 process.cwd() 报 ENOENT: uv_cwd 错误。
|
|
28
35
|
cd "$HOME" 2>/dev/null || cd / 2>/dev/null || true
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
local exit_code=$?
|
|
37
|
+
ensure_valid_cwd() {
|
|
38
|
+
stat . &>/dev/null 2>&1 || cd "$HOME" 2>/dev/null || cd / 2>/dev/null || true
|
|
39
|
+
}
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
41
|
+
read_pkg_version() {
|
|
42
|
+
node -e "try{process.stdout.write(JSON.parse(require('fs').readFileSync('$1','utf8')).version||'')}catch{}" 2>/dev/null || true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
version_gte() {
|
|
46
|
+
[ "$(printf '%s\n' "$1" "$2" | sort -V | head -1)" = "$2" ]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for _p in /usr/local/bin /usr/local/sbin /usr/bin /usr/sbin /bin /sbin; do
|
|
50
|
+
case ":$PATH:" in *":$_p:"*) ;; *) [ -d "$_p" ] && export PATH="$PATH:$_p" ;; esac
|
|
51
|
+
done
|
|
52
|
+
[ -z "$npm_config_registry" ] && export npm_config_registry="https://registry.npmjs.org"
|
|
53
|
+
|
|
54
|
+
NPM_REGISTRIES="https://registry.npmjs.org/ https://mirrors.cloud.tencent.com/npm/"
|
|
55
|
+
|
|
56
|
+
# ============================================================================
|
|
57
|
+
# 超时执行包装器(兼容 macOS 无 GNU timeout)
|
|
58
|
+
# ============================================================================
|
|
59
|
+
run_with_timeout() {
|
|
60
|
+
local timeout_secs="$1" description="$2"; shift 2
|
|
61
|
+
|
|
62
|
+
if command -v timeout &>/dev/null; then
|
|
63
|
+
timeout --kill-after=10 "$timeout_secs" "$@" && return 0
|
|
64
|
+
local rc=$?
|
|
65
|
+
[ $rc -eq 124 ] && echo " ⏰ ${description} 超时 (${timeout_secs}s)"
|
|
66
|
+
return $rc
|
|
55
67
|
fi
|
|
56
68
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
# macOS fallback
|
|
70
|
+
"$@" &
|
|
71
|
+
local cmd_pid=$!
|
|
72
|
+
( sleep "$timeout_secs" 2>/dev/null
|
|
73
|
+
kill -0 "$cmd_pid" 2>/dev/null && echo " ⏰ ${description} 超时 (${timeout_secs}s),终止中..." && \
|
|
74
|
+
kill -TERM "$cmd_pid" 2>/dev/null && sleep 5 && \
|
|
75
|
+
kill -0 "$cmd_pid" 2>/dev/null && kill -KILL "$cmd_pid" 2>/dev/null
|
|
76
|
+
) &
|
|
77
|
+
local wd=$!; disown "$wd" 2>/dev/null || true
|
|
78
|
+
wait "$cmd_pid" 2>/dev/null; local rc=$?
|
|
79
|
+
kill "$wd" 2>/dev/null || true; wait "$wd" 2>/dev/null 2>&1 || true
|
|
80
|
+
[ $rc -eq 143 ] || [ $rc -eq 137 ] && return 124
|
|
81
|
+
return $rc
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# ============================================================================
|
|
85
|
+
# 配置快照 / 回滚
|
|
86
|
+
# ============================================================================
|
|
87
|
+
CONFIG_SNAPSHOT_FILE=""
|
|
88
|
+
|
|
89
|
+
snapshot_config() {
|
|
90
|
+
[ -f "$CONFIG_FILE" ] || return 0
|
|
91
|
+
CONFIG_SNAPSHOT_FILE="$(mktemp "${TMPDIR:-/tmp}/.qqbot-config-snapshot-XXXXXX")"
|
|
92
|
+
cp -a "$CONFIG_FILE" "$CONFIG_SNAPSHOT_FILE"
|
|
93
|
+
echo " [快照] 已保存配置快照"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
restore_config_snapshot() {
|
|
97
|
+
[ -n "$CONFIG_SNAPSHOT_FILE" ] && [ -f "$CONFIG_SNAPSHOT_FILE" ] && [ -n "$CONFIG_FILE" ] && \
|
|
98
|
+
cp -a "$CONFIG_SNAPSHOT_FILE" "$CONFIG_FILE" && echo " ↩️ 已恢复配置到安装前状态"
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
cleanup_config_snapshot() {
|
|
103
|
+
[ -n "$CONFIG_SNAPSHOT_FILE" ] && rm -f "$CONFIG_SNAPSHOT_FILE" 2>/dev/null || true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
rollback_plugin_dir() {
|
|
107
|
+
local reason="${1:-未知原因}"
|
|
108
|
+
if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR/$PLUGIN_ID" ]; then
|
|
109
|
+
rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
|
|
110
|
+
mv "$BACKUP_DIR/$PLUGIN_ID" "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || \
|
|
111
|
+
cp -a "$BACKUP_DIR/$PLUGIN_ID" "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
|
|
112
|
+
[ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ] && \
|
|
113
|
+
echo " ↩️ 已回滚到旧版本 v$(read_pkg_version "$EXTENSIONS_DIR/$PLUGIN_ID/package.json")(原因: ${reason})" && return 0
|
|
114
|
+
echo " ❌ 回滚后插件目录仍不完整!"; return 1
|
|
115
|
+
fi
|
|
116
|
+
echo " ⚠️ 无备份可回滚(原因: ${reason})"; return 1
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# ============================================================================
|
|
120
|
+
# 升级锁
|
|
121
|
+
# ============================================================================
|
|
122
|
+
UPGRADE_LOCK_FILE=""
|
|
123
|
+
|
|
124
|
+
acquire_upgrade_lock() {
|
|
125
|
+
[ -z "$UPGRADE_LOCK_FILE" ] && return 0
|
|
126
|
+
if [ -f "$UPGRADE_LOCK_FILE" ]; then
|
|
127
|
+
local lock_pid="$(cat "$UPGRADE_LOCK_FILE" 2>/dev/null || true)"
|
|
128
|
+
if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
|
|
129
|
+
echo "❌ 另一个升级进程正在运行 (PID: $lock_pid)"; exit 1
|
|
68
130
|
fi
|
|
69
|
-
|
|
70
|
-
# 正常完成,清理备份
|
|
71
|
-
rm -rf "$BACKUP_DIR" 2>/dev/null || true
|
|
131
|
+
rm -f "$UPGRADE_LOCK_FILE" 2>/dev/null || true
|
|
72
132
|
fi
|
|
133
|
+
echo "$$" > "$UPGRADE_LOCK_FILE"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
release_upgrade_lock() {
|
|
137
|
+
[ -n "$UPGRADE_LOCK_FILE" ] && rm -f "$UPGRADE_LOCK_FILE" 2>/dev/null || true
|
|
138
|
+
}
|
|
73
139
|
|
|
74
|
-
|
|
140
|
+
# ============================================================================
|
|
141
|
+
# 临时配置副本(绕过 openclaw 3.23+ 配置校验)
|
|
142
|
+
# ============================================================================
|
|
143
|
+
setup_temp_config() {
|
|
144
|
+
[ -f "$CONFIG_FILE" ] || return 0
|
|
145
|
+
local need_temp
|
|
146
|
+
need_temp="$(node -e "
|
|
147
|
+
try {
|
|
148
|
+
const fs = require('fs');
|
|
149
|
+
const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
|
|
150
|
+
if (cfg.channels?.qqbot || cfg.plugins?.allow?.includes('$PLUGIN_ID') || cfg.plugins?.entries?.['$PLUGIN_ID'])
|
|
151
|
+
process.stdout.write('1');
|
|
152
|
+
} catch {}
|
|
153
|
+
" 2>/dev/null || true)"
|
|
154
|
+
[ "$need_temp" != "1" ] && return 0
|
|
155
|
+
|
|
156
|
+
TEMP_CONFIG_FILE="$(mktemp)"
|
|
157
|
+
if node -e "
|
|
158
|
+
const fs = require('fs');
|
|
159
|
+
const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
|
|
160
|
+
delete cfg.channels?.qqbot;
|
|
161
|
+
cfg.channels && Object.keys(cfg.channels).length === 0 && delete cfg.channels;
|
|
162
|
+
if (Array.isArray(cfg.plugins?.allow)) {
|
|
163
|
+
cfg.plugins.allow = cfg.plugins.allow.filter(p => p !== '$PLUGIN_ID');
|
|
164
|
+
cfg.plugins.allow.length === 0 && delete cfg.plugins.allow;
|
|
165
|
+
}
|
|
166
|
+
delete cfg.plugins?.entries?.['$PLUGIN_ID'];
|
|
167
|
+
cfg.plugins?.entries && Object.keys(cfg.plugins.entries).length === 0 && delete cfg.plugins.entries;
|
|
168
|
+
fs.writeFileSync('$TEMP_CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n');
|
|
169
|
+
" 2>/dev/null; then
|
|
170
|
+
echo " [兼容] 创建临时配置副本以通过 3.23+ 配置校验"
|
|
171
|
+
export OPENCLAW_CONFIG_PATH="$TEMP_CONFIG_FILE"
|
|
172
|
+
else
|
|
173
|
+
echo " ⚠️ 创建临时配置失败,继续使用原配置"
|
|
174
|
+
rm -f "$TEMP_CONFIG_FILE" 2>/dev/null || true; TEMP_CONFIG_FILE=""
|
|
175
|
+
fi
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
sync_temp_config() {
|
|
179
|
+
[ -n "$TEMP_CONFIG_FILE" ] && [ -f "$TEMP_CONFIG_FILE" ] || return 0
|
|
180
|
+
if [ ! -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ]; then
|
|
181
|
+
echo " ⚠️ 插件目录不完整,跳过配置同步"
|
|
182
|
+
rm -f "$TEMP_CONFIG_FILE"; unset OPENCLAW_CONFIG_PATH; return 1
|
|
183
|
+
fi
|
|
184
|
+
ensure_valid_cwd
|
|
185
|
+
node -e "
|
|
186
|
+
const fs = require('fs');
|
|
187
|
+
const tmp = JSON.parse(fs.readFileSync('$TEMP_CONFIG_FILE', 'utf8'));
|
|
188
|
+
const real = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
|
|
189
|
+
let c = false;
|
|
190
|
+
if (tmp.plugins?.installs) { (real.plugins ??= {}).installs = { ...real.plugins.installs, ...tmp.plugins.installs }; c = true; }
|
|
191
|
+
if (tmp.plugins?.entries) { (real.plugins ??= {}).entries = { ...real.plugins.entries, ...tmp.plugins.entries }; c = true; }
|
|
192
|
+
for (const id of tmp.plugins?.allow || []) {
|
|
193
|
+
if (!(real.plugins ??= {}).allow) real.plugins.allow = [];
|
|
194
|
+
if (!real.plugins.allow.includes(id)) { real.plugins.allow.push(id); c = true; }
|
|
195
|
+
}
|
|
196
|
+
if (c) fs.writeFileSync('$CONFIG_FILE', JSON.stringify(real, null, 4) + '\n');
|
|
197
|
+
" 2>/dev/null || true
|
|
198
|
+
rm -f "$TEMP_CONFIG_FILE"; unset OPENCLAW_CONFIG_PATH
|
|
199
|
+
echo " [兼容] 已同步配置并清理临时副本"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
# ============================================================================
|
|
203
|
+
# npm pack 下载 tarball(供 Level 2 使用)
|
|
204
|
+
# 成功后设置 PACK_TGZ_FILE 变量指向 tgz 文件路径
|
|
205
|
+
# ============================================================================
|
|
206
|
+
PACK_TMP_DIR=""
|
|
207
|
+
PACK_TGZ_FILE=""
|
|
208
|
+
|
|
209
|
+
npm_pack_download() {
|
|
210
|
+
for _cmd in npm tar node; do
|
|
211
|
+
command -v "$_cmd" &>/dev/null || { echo " ❌ $_cmd 不可用"; return 1; }
|
|
212
|
+
done
|
|
213
|
+
|
|
214
|
+
PACK_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/.qqbot-pack-XXXXXX")"
|
|
215
|
+
PACK_TGZ_FILE=""
|
|
216
|
+
local ok=false
|
|
217
|
+
ensure_valid_cwd
|
|
218
|
+
for registry in $NPM_REGISTRIES; do
|
|
219
|
+
echo " 尝试 registry: $registry"
|
|
220
|
+
if run_with_timeout "$INSTALL_TIMEOUT" "npm pack" npm pack "$INSTALL_SRC" \
|
|
221
|
+
--pack-destination "$PACK_TMP_DIR" --registry "$registry" 2>&1; then
|
|
222
|
+
ok=true; break
|
|
223
|
+
fi
|
|
224
|
+
done
|
|
225
|
+
if [ "$ok" != "true" ]; then
|
|
226
|
+
echo " ❌ npm pack 失败(所有 registry 均不可用)"
|
|
227
|
+
rm -rf "$PACK_TMP_DIR" 2>/dev/null; PACK_TMP_DIR=""; return 1
|
|
228
|
+
fi
|
|
229
|
+
PACK_TGZ_FILE="$(find "$PACK_TMP_DIR" -maxdepth 1 -name '*.tgz' -type f | head -1)"
|
|
230
|
+
if [ -z "$PACK_TGZ_FILE" ]; then
|
|
231
|
+
echo " ❌ 未找到 tgz 文件"
|
|
232
|
+
rm -rf "$PACK_TMP_DIR" 2>/dev/null; PACK_TMP_DIR=""; return 1
|
|
233
|
+
fi
|
|
234
|
+
echo " 已下载: $(basename "$PACK_TGZ_FILE")"
|
|
235
|
+
return 0
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
cleanup_pack() {
|
|
239
|
+
[ -n "$PACK_TMP_DIR" ] && rm -rf "$PACK_TMP_DIR" 2>/dev/null || true
|
|
240
|
+
PACK_TMP_DIR=""; PACK_TGZ_FILE=""
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
# ============================================================================
|
|
244
|
+
# Level 2: npm pack 下载 + 解压 + openclaw plugins install <目录>
|
|
245
|
+
# 绕过 ClawHub 下载,保留 openclaw CLI 的原子部署、验证、完整 install record
|
|
246
|
+
# 注意:传目录路径而非 tarball 路径,因为 openclaw 的 installPluginFromArchive
|
|
247
|
+
# 存在 bug(漏传 dangerouslyForceUnsafeInstall),而 installPluginFromDir 正确传递
|
|
248
|
+
# ============================================================================
|
|
249
|
+
npm_pack_native_install() {
|
|
250
|
+
echo ""
|
|
251
|
+
echo " ============================================"
|
|
252
|
+
echo " [Level 2] npm pack + openclaw install 本地目录"
|
|
253
|
+
echo " ============================================"
|
|
254
|
+
|
|
255
|
+
echo " [L2 1/3] 下载 tarball..."
|
|
256
|
+
npm_pack_download || return 1
|
|
257
|
+
|
|
258
|
+
# 先解压再传目录路径给 openclaw,而非直接传 tarball 路径
|
|
259
|
+
# 原因:openclaw installPluginFromArchive 漏传 --dangerously-force-unsafe-install,
|
|
260
|
+
# installPluginFromDir 正确传递,传目录可绕过此 bug
|
|
261
|
+
echo " [L2 2/3] 解压 tarball..."
|
|
262
|
+
local extract_dir
|
|
263
|
+
extract_dir="$(mktemp -d "${TMPDIR:-/tmp}/.qqbot-extract-XXXXXX")"
|
|
264
|
+
if ! tar xzf "$PACK_TGZ_FILE" -C "$extract_dir" 2>&1; then
|
|
265
|
+
echo " ❌ 解压失败"; cleanup_pack; rm -rf "$extract_dir"; return 1
|
|
266
|
+
fi
|
|
267
|
+
cleanup_pack
|
|
268
|
+
local package_dir="$extract_dir/package"
|
|
269
|
+
if [ ! -f "$package_dir/package.json" ]; then
|
|
270
|
+
echo " ❌ 解压后未找到 package.json"; rm -rf "$extract_dir"; return 1
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
echo " [L2 3/3] 用 openclaw 安装本地目录..."
|
|
274
|
+
ensure_valid_cwd
|
|
275
|
+
local rc=0
|
|
276
|
+
run_with_timeout "$INSTALL_TIMEOUT" "plugins install (local dir)" \
|
|
277
|
+
openclaw plugins install "$package_dir" $FORCE_UNSAFE_FLAG 2>&1 || rc=$?
|
|
278
|
+
|
|
279
|
+
rm -rf "$extract_dir" 2>/dev/null || true
|
|
280
|
+
|
|
281
|
+
if [ $rc -eq 0 ] && [ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ]; then
|
|
282
|
+
echo " ✅ Level 2 安装成功"
|
|
283
|
+
return 0
|
|
284
|
+
fi
|
|
285
|
+
echo " Level 2 失败 (exit=$rc)"
|
|
286
|
+
[ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && [ ! -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ] && \
|
|
287
|
+
rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
|
|
288
|
+
find "${EXTENSIONS_DIR:-/dev/null}" "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" \
|
|
289
|
+
-exec rm -rf {} + 2>/dev/null || true
|
|
290
|
+
return 1
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
# 降级入口:Level 2
|
|
294
|
+
run_fallback() {
|
|
295
|
+
npm_pack_native_install && return 0
|
|
296
|
+
return 1
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# ============================================================================
|
|
300
|
+
# 异常退出清理
|
|
301
|
+
# ============================================================================
|
|
302
|
+
INSTALL_COMPLETED=false
|
|
303
|
+
BACKUP_DIR=""
|
|
304
|
+
TEMP_CONFIG_FILE=""
|
|
305
|
+
|
|
306
|
+
cleanup_on_exit() {
|
|
307
|
+
local exit_code=$?
|
|
308
|
+
ensure_valid_cwd
|
|
309
|
+
|
|
310
|
+
if [ "$INSTALL_COMPLETED" != "true" ] && [ $exit_code -ne 0 ]; then
|
|
311
|
+
local reason="异常退出 (code=$exit_code)"
|
|
312
|
+
case $exit_code in 124) reason="安装超时";; 130) reason="用户中断";; 143) reason="SIGTERM";; 129) reason="SIGHUP";; esac
|
|
313
|
+
echo " ⚠️ ${reason}"
|
|
314
|
+
restore_config_snapshot
|
|
315
|
+
rollback_plugin_dir "$reason"
|
|
316
|
+
fi
|
|
317
|
+
|
|
318
|
+
[ -n "$TEMP_CONFIG_FILE" ] && rm -f "$TEMP_CONFIG_FILE" 2>/dev/null || true
|
|
319
|
+
[ -n "$BACKUP_DIR" ] && rm -rf "$BACKUP_DIR" 2>/dev/null || true
|
|
320
|
+
cleanup_config_snapshot
|
|
321
|
+
cleanup_pack
|
|
75
322
|
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-*" -
|
|
323
|
+
find "${TMPDIR:-/tmp}" -maxdepth 1 \( -name ".openclaw-install-stage-*" -o -name ".qqbot-pack-*" \
|
|
324
|
+
-o -name ".qqbot-extract-*" -o -name ".qqbot-upgrade-backup-*" \) -exec rm -rf {} + 2>/dev/null || true
|
|
325
|
+
release_upgrade_lock
|
|
326
|
+
exit $exit_code
|
|
77
327
|
}
|
|
78
328
|
trap cleanup_on_exit EXIT
|
|
329
|
+
trap 'exit 143' TERM
|
|
330
|
+
trap 'echo " 中断"; exit 130' INT
|
|
331
|
+
trap 'exit 129' HUP
|
|
79
332
|
|
|
80
|
-
#
|
|
81
|
-
find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".qqbot-upgrade-backup-*" -
|
|
333
|
+
# 清理上次升级遗留(>60min)
|
|
334
|
+
find "${TMPDIR:-/tmp}" -maxdepth 1 \( -name ".qqbot-upgrade-backup-*" -o -name ".qqbot-pack-*" \
|
|
335
|
+
-o -name ".qqbot-extract-*" \) -mmin +60 -exec rm -rf {} + 2>/dev/null || true
|
|
82
336
|
|
|
337
|
+
# ============================================================================
|
|
338
|
+
# 参数解析
|
|
339
|
+
# ============================================================================
|
|
83
340
|
PKG_NAME="@leoqlin/openclaw-qqbot"
|
|
84
341
|
PLUGIN_ID="openclaw-qqbot"
|
|
85
|
-
INSTALL_SRC=""
|
|
86
342
|
TARGET_VERSION=""
|
|
87
343
|
APPID=""
|
|
88
344
|
SECRET=""
|
|
89
345
|
NO_RESTART=false
|
|
346
|
+
DISABLE_BUILTIN=true
|
|
347
|
+
INSTALL_TIMEOUT=1000
|
|
348
|
+
LOCAL_VERSION="$(read_pkg_version "$PROJECT_DIR/package.json")"
|
|
90
349
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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)"
|
|
350
|
+
# 可能与我们冲突的内置/官方插件 ID 列表
|
|
351
|
+
# 如果 OpenClaw 未来内置了 qqbot 相关插件,在此列表中添加其 ID
|
|
352
|
+
BUILTIN_CONFLICT_IDS="qqbot openclaw-qq"
|
|
100
353
|
|
|
101
354
|
print_usage() {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
echo " QQBOT_SECRET QQ机器人 secret"
|
|
118
|
-
echo " QQBOT_TOKEN QQ机器人 token (appid:secret)"
|
|
355
|
+
cat <<EOF
|
|
356
|
+
用法:
|
|
357
|
+
upgrade-via-npm.sh # 升级到 latest
|
|
358
|
+
upgrade-via-npm.sh --version <版本号> # 升级到指定版本
|
|
359
|
+
upgrade-via-npm.sh --self-version # 升级到当前仓库版本${LOCAL_VERSION:+ ($LOCAL_VERSION)}
|
|
360
|
+
|
|
361
|
+
--pkg <scope/name> 指定 npm 包名
|
|
362
|
+
--appid <appid> QQ机器人 appid
|
|
363
|
+
--secret <secret> QQ机器人 secret
|
|
364
|
+
--no-restart 只做文件替换,不重启 gateway
|
|
365
|
+
--disable-builtin 额外删除内置冲突插件目录(配置禁用默认执行)
|
|
366
|
+
--timeout <秒> 自定义安装超时(默认1000)
|
|
367
|
+
|
|
368
|
+
环境变量: QQBOT_APPID / QQBOT_SECRET / QQBOT_TOKEN (appid:secret)
|
|
369
|
+
EOF
|
|
119
370
|
}
|
|
120
371
|
|
|
121
372
|
while [[ $# -gt 0 ]]; do
|
|
122
373
|
case "$1" in
|
|
123
|
-
--tag)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
--
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
;;
|
|
374
|
+
--tag|--version) [ -z "$2" ] && echo "❌ $1 需要参数" && exit 1; TARGET_VERSION="${2#v}"; shift 2 ;;
|
|
375
|
+
--self-version) [ -z "$LOCAL_VERSION" ] && echo "❌ 无法读取版本" && exit 1; TARGET_VERSION="$LOCAL_VERSION"; shift ;;
|
|
376
|
+
--appid) [ -z "$2" ] && echo "❌ --appid 需要参数" && exit 1; APPID="$2"; shift 2 ;;
|
|
377
|
+
--secret) [ -z "$2" ] && echo "❌ --secret 需要参数" && exit 1; SECRET="$2"; shift 2 ;;
|
|
378
|
+
--pkg) [ -z "$2" ] && echo "❌ --pkg 需要参数" && exit 1; _p="$2"; [[ "$_p" != @* ]] && _p="@$_p"; PKG_NAME="$_p"; shift 2 ;;
|
|
379
|
+
--no-restart) NO_RESTART=true; shift ;;
|
|
380
|
+
--disable-builtin) DISABLE_BUILTIN=true; shift ;;
|
|
381
|
+
--timeout) [ -z "$2" ] && echo "❌ --timeout 需要参数" && exit 1; INSTALL_TIMEOUT="$2"; shift 2 ;;
|
|
382
|
+
-h|--help) print_usage; exit 0 ;;
|
|
164
383
|
*) echo "未知选项: $1"; print_usage; exit 1 ;;
|
|
165
384
|
esac
|
|
166
385
|
done
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
INSTALL_SRC="${PKG_NAME}@${TARGET_VERSION}"
|
|
170
|
-
else
|
|
171
|
-
INSTALL_SRC="${PKG_NAME}@latest"
|
|
172
|
-
fi
|
|
386
|
+
|
|
387
|
+
INSTALL_SRC="${PKG_NAME}@${TARGET_VERSION:-latest}"
|
|
173
388
|
|
|
174
389
|
# 环境变量 fallback
|
|
175
|
-
APPID="${APPID:-$QQBOT_APPID}"
|
|
176
|
-
SECRET="${SECRET:-$QQBOT_SECRET}"
|
|
390
|
+
APPID="${APPID:-$QQBOT_APPID}"; SECRET="${SECRET:-$QQBOT_SECRET}"
|
|
177
391
|
if [ -z "$APPID" ] && [ -z "$SECRET" ] && [ -n "$QQBOT_TOKEN" ]; then
|
|
178
|
-
APPID="${QQBOT_TOKEN%%:*}"
|
|
179
|
-
SECRET="${QQBOT_TOKEN#*:}"
|
|
392
|
+
APPID="${QQBOT_TOKEN%%:*}"; SECRET="${QQBOT_TOKEN#*:}"
|
|
180
393
|
fi
|
|
181
394
|
|
|
182
|
-
# 检测
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
395
|
+
# 检测 openclaw
|
|
396
|
+
command -v openclaw &>/dev/null || { echo "❌ 未找到 openclaw"; exit 1; }
|
|
397
|
+
|
|
398
|
+
# 解析数据目录(支持 OPENCLAW_STATE_DIR 覆盖)
|
|
399
|
+
OPENCLAW_HOME="${OPENCLAW_STATE_DIR:-$HOME/.openclaw}"
|
|
400
|
+
EXTENSIONS_DIR="$OPENCLAW_HOME/extensions"
|
|
401
|
+
CONFIG_FILE="$OPENCLAW_HOME/openclaw.json"
|
|
188
402
|
|
|
189
|
-
|
|
403
|
+
UPGRADE_LOCK_FILE="$OPENCLAW_HOME/.upgrading"
|
|
404
|
+
acquire_upgrade_lock
|
|
190
405
|
|
|
191
|
-
|
|
192
|
-
|
|
406
|
+
OPENCLAW_VERSION="$(openclaw --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1 || true)"
|
|
407
|
+
|
|
408
|
+
# OpenClaw ≥2026.3.30 引入安全扫描阻断 + --dangerously-force-unsafe-install 参数
|
|
409
|
+
# 该参数仅 plugins install 支持,plugins update 不支持
|
|
410
|
+
FORCE_UNSAFE_FLAG=""
|
|
411
|
+
if [ -n "$OPENCLAW_VERSION" ] && version_gte "$OPENCLAW_VERSION" "2026.3.30"; then
|
|
412
|
+
FORCE_UNSAFE_FLAG="--dangerously-force-unsafe-install"
|
|
413
|
+
fi
|
|
193
414
|
|
|
194
415
|
echo "==========================================="
|
|
195
416
|
echo " qqbot 升级: $INSTALL_SRC"
|
|
196
|
-
echo " openclaw
|
|
417
|
+
echo " openclaw: v${OPENCLAW_VERSION:-unknown}"
|
|
418
|
+
echo " 隔离: ${_UPGRADE_ISOLATED:+✓ setsid}${_UPGRADE_ISOLATED:-✗} 超时: ${INSTALL_TIMEOUT}s"
|
|
197
419
|
echo "==========================================="
|
|
198
|
-
echo ""
|
|
199
420
|
|
|
200
|
-
#
|
|
421
|
+
# 记录旧版本
|
|
201
422
|
OLD_VERSION=""
|
|
202
423
|
OLD_PKG="$EXTENSIONS_DIR/$PLUGIN_ID/package.json"
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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 "
|
|
424
|
+
[ -f "$OLD_PKG" ] && OLD_VERSION="$(read_pkg_version "$OLD_PKG")"
|
|
425
|
+
[ -n "$OLD_VERSION" ] && echo " 当前版本: $OLD_VERSION"
|
|
426
|
+
|
|
427
|
+
# ============================================================================
|
|
428
|
+
# 禁用内置冲突插件(配置禁用 + 目录删除 + 验证)
|
|
429
|
+
# ============================================================================
|
|
430
|
+
disable_builtin_plugins() {
|
|
431
|
+
local found_any=false
|
|
432
|
+
for bid in $BUILTIN_CONFLICT_IDS; do
|
|
433
|
+
[ "$bid" = "$PLUGIN_ID" ] && continue
|
|
434
|
+
local _changed=""
|
|
435
|
+
_changed="$(node -e "
|
|
245
436
|
try {
|
|
246
437
|
const fs = require('fs');
|
|
247
438
|
const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
|
|
248
|
-
|
|
249
|
-
if (cfg.
|
|
250
|
-
|
|
251
|
-
|
|
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;
|
|
439
|
+
let c = [];
|
|
440
|
+
if (cfg.plugins?.entries?.['$bid']) { cfg.plugins.entries['$bid'].enabled = false; c.push('entries'); }
|
|
441
|
+
if (Array.isArray(cfg.plugins?.allow) && cfg.plugins.allow.includes('$bid')) {
|
|
442
|
+
cfg.plugins.allow = cfg.plugins.allow.filter(p => p !== '$bid'); c.push('allow');
|
|
257
443
|
}
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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=""
|
|
444
|
+
if (cfg.plugins?.installs?.['$bid']) { delete cfg.plugins.installs['$bid']; c.push('installs'); }
|
|
445
|
+
if (c.length) fs.writeFileSync('$CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n');
|
|
446
|
+
process.stdout.write(c.join(','));
|
|
447
|
+
} catch {}
|
|
448
|
+
" 2>/dev/null || true)"
|
|
449
|
+
[ -n "$_changed" ] && echo " [禁用内置] $bid: 已修改 $_changed" && found_any=true
|
|
450
|
+
if [ -d "$EXTENSIONS_DIR/$bid" ]; then
|
|
451
|
+
rm -rf "$EXTENSIONS_DIR/$bid"; echo " [禁用内置] 已删除 extensions/$bid"; found_any=true
|
|
272
452
|
fi
|
|
273
|
-
|
|
274
|
-
|
|
453
|
+
done
|
|
454
|
+
[ "$found_any" = "true" ] && echo " ✅ 内置冲突插件已禁用" || echo " ℹ️ 未发现需要禁用的内置冲突插件"
|
|
455
|
+
}
|
|
275
456
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
457
|
+
verify_builtin_disabled() {
|
|
458
|
+
for bid in $BUILTIN_CONFLICT_IDS; do
|
|
459
|
+
[ "$bid" = "$PLUGIN_ID" ] && continue
|
|
460
|
+
local _e="$(node -e "try{const c=JSON.parse(require('fs').readFileSync('$CONFIG_FILE','utf8'));if(c.plugins?.entries?.['$bid']?.enabled)process.stdout.write('1')}catch{}" 2>/dev/null || true)"
|
|
461
|
+
if [ "$_e" = "1" ]; then
|
|
462
|
+
echo " ⚠️ 内置插件 $bid 仍启用,再次禁用..."
|
|
463
|
+
node -e "try{const f=require('fs'),c=JSON.parse(f.readFileSync('$CONFIG_FILE','utf8'));if(c.plugins?.entries?.['$bid'])c.plugins.entries['$bid'].enabled=false;f.writeFileSync('$CONFIG_FILE',JSON.stringify(c,null,4)+'\n')}catch{}" 2>/dev/null || true
|
|
464
|
+
fi
|
|
465
|
+
done
|
|
307
466
|
}
|
|
308
467
|
|
|
309
|
-
|
|
468
|
+
# ============================================================================
|
|
469
|
+
# [1/4] 安装/升级插件
|
|
470
|
+
# ============================================================================
|
|
471
|
+
echo ""
|
|
472
|
+
|
|
473
|
+
# 默认禁用内置冲突插件(openclaw ≥2026.3.31 内置了 qqbot 插件,与我们的 openclaw-qqbot 冲突)
|
|
474
|
+
echo "[前置] 检查并禁用内置冲突插件..."
|
|
475
|
+
disable_builtin_plugins
|
|
476
|
+
echo ""
|
|
477
|
+
|
|
478
|
+
echo "[1/4] 安装/升级插件..."
|
|
479
|
+
snapshot_config
|
|
480
|
+
setup_temp_config
|
|
310
481
|
|
|
311
|
-
#
|
|
312
|
-
|
|
482
|
+
# 清理历史遗留 ID 的配置记录(qqbot/openclaw-qq 是旧版本使用的 ID,
|
|
483
|
+
# entries 中残留会导致 gateway 重复加载同一插件报 tool name conflict)
|
|
484
|
+
[ -f "$CONFIG_FILE" ] && node -e "
|
|
313
485
|
try {
|
|
314
486
|
const fs = require('fs');
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
487
|
+
const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
|
|
488
|
+
let c = false;
|
|
489
|
+
for (const old of ['qqbot', 'openclaw-qq']) {
|
|
490
|
+
if (cfg.plugins?.entries?.[old]) { delete cfg.plugins.entries[old]; c = true; }
|
|
491
|
+
if (cfg.plugins?.installs?.[old]) { delete cfg.plugins.installs[old]; c = true; }
|
|
492
|
+
if (Array.isArray(cfg.plugins?.allow)) {
|
|
493
|
+
const i = cfg.plugins.allow.indexOf(old);
|
|
494
|
+
if (i >= 0) { cfg.plugins.allow.splice(i, 1); c = true; }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (c) { fs.writeFileSync('$CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n'); process.stdout.write('1'); }
|
|
498
|
+
} catch {}
|
|
499
|
+
" 2>/dev/null | grep -q '1' && echo " [清理] 已移除历史遗留配置记录" || true
|
|
500
|
+
|
|
501
|
+
UPGRADE_OK=false
|
|
502
|
+
|
|
503
|
+
# 检测安装状态
|
|
504
|
+
INSTALL_RECORD_INFO="$(node -e "
|
|
505
|
+
try {
|
|
506
|
+
const cfg = JSON.parse(require('fs').readFileSync('$CONFIG_FILE', 'utf8'));
|
|
507
|
+
const inst = cfg.plugins?.installs?.['$PLUGIN_ID'];
|
|
508
|
+
if (inst) process.stdout.write('yes|' + (inst.spec || ''));
|
|
319
509
|
} catch {}
|
|
320
510
|
" 2>/dev/null || true)"
|
|
511
|
+
HAS_INSTALL_RECORD="${INSTALL_RECORD_INFO%%|*}"
|
|
512
|
+
INSTALL_SPEC="${INSTALL_RECORD_INFO#*|}"
|
|
321
513
|
HAS_PLUGIN_DIR=false
|
|
322
|
-
[ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && [ -f "$
|
|
323
|
-
|
|
324
|
-
# 决策矩阵:
|
|
325
|
-
# 配置有记录 + 目录存在 → update(最佳路径)
|
|
326
|
-
# 配置有记录 + 目录不存在 → 清理残留记录,走 install
|
|
327
|
-
# 配置无记录 + 目录存在 → 删目录,走 install(配置与文件不一致)
|
|
328
|
-
# 配置无记录 + 目录不存在 → 走 install(全新安装)
|
|
329
|
-
#
|
|
330
|
-
# 指定了具体版本(--version/--tag/--self-version)时:
|
|
331
|
-
# update 不支持指定版本,直接走 删除 + install
|
|
514
|
+
[ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && [ -f "$OLD_PKG" ] && HAS_PLUGIN_DIR=true
|
|
332
515
|
|
|
516
|
+
# 决策:配置有记录 + 目录存在 + 未指定版本 + <3.30 → update,其他 → install
|
|
517
|
+
# ≥3.30 的 update 会被安全扫描阻断(update 不支持 --dangerously-force-unsafe-install),直接走 install
|
|
333
518
|
USE_UPDATE=false
|
|
334
|
-
|
|
335
519
|
if [ "$HAS_INSTALL_RECORD" = "yes" ] && [ "$HAS_PLUGIN_DIR" = "true" ] && [ -z "$TARGET_VERSION" ]; then
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
520
|
+
if [ -n "$FORCE_UNSAFE_FLAG" ]; then
|
|
521
|
+
echo " [检测] 配置 ✓ | 目录 ✓ | openclaw ≥3.30 → 跳过 update,直接 install(安全扫描兼容)"
|
|
522
|
+
else
|
|
523
|
+
USE_UPDATE=true
|
|
524
|
+
echo " [检测] 配置 ✓ | 目录 ✓ | 未指定版本 → update"
|
|
525
|
+
# spec 解锁
|
|
526
|
+
if [ -n "$INSTALL_SPEC" ]; then
|
|
527
|
+
SPEC_SUFFIX="${INSTALL_SPEC##*@}"
|
|
528
|
+
if echo "$SPEC_SUFFIX" | grep -qE '^[0-9]+\.[0-9]+'; then
|
|
529
|
+
echo " [spec 解锁] '$INSTALL_SPEC' → @latest"
|
|
530
|
+
node -e "
|
|
531
|
+
try {
|
|
532
|
+
const fs = require('fs'), p = process.env.OPENCLAW_CONFIG_PATH || '$CONFIG_FILE';
|
|
533
|
+
const cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
534
|
+
if (cfg.plugins?.installs?.['$PLUGIN_ID']) {
|
|
535
|
+
cfg.plugins.installs['$PLUGIN_ID'].spec = '$PKG_NAME@latest';
|
|
536
|
+
fs.writeFileSync(p, JSON.stringify(cfg, null, 4) + '\n');
|
|
537
|
+
}
|
|
538
|
+
} catch {}
|
|
539
|
+
" 2>/dev/null || true
|
|
540
|
+
fi
|
|
541
|
+
fi
|
|
542
|
+
fi
|
|
343
543
|
elif [ "$HAS_PLUGIN_DIR" = "true" ]; then
|
|
344
|
-
echo " [检测]
|
|
544
|
+
echo " [检测] 目录 ✓ | 指定版本或无配置记录 → reinstall"
|
|
345
545
|
else
|
|
346
|
-
echo " [检测]
|
|
546
|
+
echo " [检测] 目录 ✗ → 全新安装"
|
|
347
547
|
fi
|
|
348
548
|
|
|
549
|
+
mark_success() {
|
|
550
|
+
UPGRADE_OK=true; INSTALL_COMPLETED=true
|
|
551
|
+
[ -n "$BACKUP_DIR" ] && rm -rf "$BACKUP_DIR" 2>/dev/null && BACKUP_DIR="" || true
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
# ── 更新路径(Level 1: 原生 update,仅 <3.30 版本) ──
|
|
349
555
|
if [ "$USE_UPDATE" = "true" ]; then
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
if [ -n "$POST_UPDATE_VERSION" ] && [ "$POST_UPDATE_VERSION" != "$OLD_VERSION" ]; then
|
|
363
|
-
UPGRADE_OK=true
|
|
364
|
-
echo " ✅ update 成功 ($OLD_VERSION → $POST_UPDATE_VERSION)"
|
|
556
|
+
ensure_valid_cwd
|
|
557
|
+
UPDATE_TIMEOUT=$((INSTALL_TIMEOUT < 180 ? INSTALL_TIMEOUT : 180))
|
|
558
|
+
echo " [Level 1] 尝试 openclaw plugins update...(${UPDATE_TIMEOUT}s 超时)"
|
|
559
|
+
UPDATE_RC=0
|
|
560
|
+
UPDATE_OUTPUT="$(run_with_timeout "$UPDATE_TIMEOUT" \
|
|
561
|
+
"plugins update" openclaw plugins update "$PLUGIN_ID" 2>&1)" || UPDATE_RC=$?
|
|
562
|
+
echo "$UPDATE_OUTPUT"
|
|
563
|
+
|
|
564
|
+
if [ $UPDATE_RC -eq 0 ]; then
|
|
565
|
+
POST_VER=""; [ -f "$OLD_PKG" ] && POST_VER="$(read_pkg_version "$OLD_PKG")"
|
|
566
|
+
if [ -n "$POST_VER" ] && [ "$POST_VER" != "$OLD_VERSION" ]; then
|
|
567
|
+
mark_success; echo " ✅ update 成功 ($OLD_VERSION → $POST_VER)"
|
|
365
568
|
elif [ -z "$OLD_VERSION" ]; then
|
|
366
|
-
|
|
367
|
-
UPGRADE_OK=true
|
|
368
|
-
echo " ✅ update 成功"
|
|
569
|
+
mark_success; echo " ✅ update 成功"
|
|
369
570
|
else
|
|
370
|
-
echo "
|
|
571
|
+
echo " ℹ️ 版本未变 ($POST_VER),查询 npm latest..."
|
|
572
|
+
NPM_LATEST="$(npm view "$PKG_NAME" version 2>/dev/null || true)"
|
|
573
|
+
if [ -n "$NPM_LATEST" ] && [ "$NPM_LATEST" = "$POST_VER" ]; then
|
|
574
|
+
mark_success; echo " ✅ 已是最新版本 $POST_VER"
|
|
575
|
+
else
|
|
576
|
+
echo " npm latest=${NPM_LATEST:-unknown},当前=$POST_VER"
|
|
577
|
+
fi
|
|
371
578
|
fi
|
|
372
579
|
else
|
|
373
|
-
echo "
|
|
580
|
+
[ $UPDATE_RC -eq 124 ] && echo " ⏰ update 超时" || echo " update 失败 (exit=$UPDATE_RC)"
|
|
581
|
+
fi
|
|
582
|
+
|
|
583
|
+
# Level 1 失败 → Level 2 降级
|
|
584
|
+
if [ "$UPGRADE_OK" != "true" ]; then
|
|
585
|
+
if [ -z "$BACKUP_DIR" ] && [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ]; then
|
|
586
|
+
BACKUP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/.qqbot-upgrade-backup-XXXXXX")"
|
|
587
|
+
cp -a "$EXTENSIONS_DIR/$PLUGIN_ID" "$BACKUP_DIR/$PLUGIN_ID"
|
|
588
|
+
fi
|
|
589
|
+
run_fallback && mark_success
|
|
374
590
|
fi
|
|
375
591
|
fi
|
|
376
592
|
|
|
593
|
+
# ── 安装路径(Level 1 → Level 2) ──
|
|
377
594
|
if [ "$UPGRADE_OK" != "true" ]; then
|
|
378
|
-
#
|
|
379
|
-
BACKUP_DIR=""
|
|
595
|
+
# 备份旧目录
|
|
380
596
|
if [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ]; then
|
|
381
597
|
BACKUP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/.qqbot-upgrade-backup-XXXXXX")"
|
|
382
|
-
|
|
383
|
-
echo "
|
|
598
|
+
cp -a "$EXTENSIONS_DIR/$PLUGIN_ID" "$BACKUP_DIR/$PLUGIN_ID"
|
|
599
|
+
echo " 已备份旧目录"
|
|
384
600
|
fi
|
|
385
601
|
|
|
386
|
-
#
|
|
387
|
-
for
|
|
388
|
-
[ -d "$EXTENSIONS_DIR/$
|
|
602
|
+
# 清理历史遗留
|
|
603
|
+
for d in qqbot openclaw-qq; do
|
|
604
|
+
[ -d "$EXTENSIONS_DIR/$d" ] && rm -rf "$EXTENSIONS_DIR/$d" && echo " 已清理: $d"
|
|
389
605
|
done
|
|
606
|
+
[ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID"
|
|
390
607
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
|
608
|
+
# 从配置中移除插件记录,防止 openclaw CLI 报 "already exists"
|
|
609
|
+
_install_cfg="${TEMP_CONFIG_FILE:-$CONFIG_FILE}"
|
|
610
|
+
[ -f "$_install_cfg" ] && node -e "
|
|
611
|
+
try {
|
|
612
|
+
const fs = require('fs');
|
|
613
|
+
const cfg = JSON.parse(fs.readFileSync('$_install_cfg', 'utf8'));
|
|
614
|
+
let c = false;
|
|
615
|
+
if (cfg.plugins?.installs?.['$PLUGIN_ID']) { delete cfg.plugins.installs['$PLUGIN_ID']; c = true; }
|
|
616
|
+
if (cfg.plugins?.entries?.['$PLUGIN_ID']) { delete cfg.plugins.entries['$PLUGIN_ID']; c = true; }
|
|
617
|
+
if (Array.isArray(cfg.plugins?.allow)) {
|
|
618
|
+
const i = cfg.plugins.allow.indexOf('$PLUGIN_ID');
|
|
619
|
+
if (i >= 0) { cfg.plugins.allow.splice(i, 1); c = true; }
|
|
620
|
+
}
|
|
621
|
+
if (c) fs.writeFileSync('$_install_cfg', JSON.stringify(cfg, null, 4) + '\n');
|
|
622
|
+
} catch {}
|
|
623
|
+
" 2>/dev/null || true
|
|
624
|
+
|
|
625
|
+
# Level 1: 原生 install(单次尝试,失败后由 Level 2 的 npm pack 多源重试接管)
|
|
626
|
+
echo " [Level 1] 尝试 openclaw plugins install..."
|
|
627
|
+
ensure_valid_cwd
|
|
628
|
+
RC=0
|
|
629
|
+
run_with_timeout "$INSTALL_TIMEOUT" \
|
|
630
|
+
"plugins install" openclaw plugins install "$INSTALL_SRC" --pin \
|
|
631
|
+
$FORCE_UNSAFE_FLAG 2>&1 || RC=$?
|
|
632
|
+
|
|
633
|
+
if [ $RC -eq 0 ] && [ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ]; then
|
|
634
|
+
mark_success; echo " ✅ Level 1 install 成功"
|
|
432
635
|
else
|
|
433
|
-
echo "
|
|
434
|
-
#
|
|
435
|
-
|
|
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
|
|
636
|
+
[ $RC -ne 0 ] && echo " Level 1 install 失败 (exit=$RC)"
|
|
637
|
+
# 清理不完整的目录和 stage
|
|
638
|
+
[ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && [ ! -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ] && \
|
|
439
639
|
rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
640
|
+
find "${EXTENSIONS_DIR:-/dev/null}" "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" \
|
|
641
|
+
-exec rm -rf {} + 2>/dev/null || true
|
|
642
|
+
|
|
643
|
+
echo " Level 1 失败,尝试 Level 2 降级..."
|
|
644
|
+
run_fallback && mark_success || {
|
|
645
|
+
rollback_plugin_dir "安装失败"; restore_config_snapshot
|
|
646
|
+
[ -n "$TEMP_CONFIG_FILE" ] && rm -f "$TEMP_CONFIG_FILE" 2>/dev/null || true
|
|
647
|
+
unset OPENCLAW_CONFIG_PATH 2>/dev/null || true
|
|
648
|
+
echo "QQBOT_NEW_VERSION=unknown"
|
|
649
|
+
echo "QQBOT_REPORT=❌ QQBot 安装失败(已回滚),请检查网络"
|
|
650
|
+
exit 1
|
|
651
|
+
}
|
|
451
652
|
fi
|
|
452
653
|
fi
|
|
453
654
|
|
|
454
|
-
|
|
455
|
-
|
|
655
|
+
sync_temp_config
|
|
656
|
+
cleanup_config_snapshot
|
|
657
|
+
INSTALL_COMPLETED=true
|
|
456
658
|
|
|
457
|
-
#
|
|
659
|
+
# ============================================================================
|
|
660
|
+
# [2/4] 验证安装
|
|
661
|
+
# ============================================================================
|
|
458
662
|
echo ""
|
|
459
663
|
echo "[2/4] 验证安装..."
|
|
460
664
|
|
|
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
665
|
TARGET_DIR="$EXTENSIONS_DIR/$PLUGIN_ID"
|
|
666
|
+
NEW_VERSION=""; [ -f "$TARGET_DIR/package.json" ] && NEW_VERSION="$(read_pkg_version "$TARGET_DIR/package.json")"
|
|
469
667
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
PREFLIGHT_OK=false
|
|
473
|
-
else
|
|
474
|
-
echo " ✅ 版本号: $NEW_VERSION"
|
|
475
|
-
fi
|
|
668
|
+
PREFLIGHT_OK=true
|
|
669
|
+
[ -z "$NEW_VERSION" ] && echo " ❌ 无法读取版本号" && PREFLIGHT_OK=false || echo " ✅ 版本: $NEW_VERSION"
|
|
476
670
|
|
|
477
|
-
|
|
478
|
-
|
|
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
|
|
671
|
+
ENTRY=""; for f in "dist/index.js" "index.js"; do [ -f "$TARGET_DIR/$f" ] && ENTRY="$f" && break; done
|
|
672
|
+
[ -z "$ENTRY" ] && echo " ❌ 缺少入口文件" && PREFLIGHT_OK=false || echo " ✅ 入口: $ENTRY"
|
|
491
673
|
|
|
492
|
-
# 核心目录
|
|
493
674
|
if [ -d "$TARGET_DIR/dist/src" ]; then
|
|
494
|
-
|
|
495
|
-
echo " ✅ dist/src/
|
|
496
|
-
|
|
497
|
-
echo " ❌ JS 文件数量异常偏少(预期 ≥ 5,实际 ${CORE_JS_COUNT})"
|
|
498
|
-
PREFLIGHT_OK=false
|
|
499
|
-
fi
|
|
675
|
+
JS_COUNT=$(find "$TARGET_DIR/dist/src" -name "*.js" -type f 2>/dev/null | wc -l | tr -d ' ')
|
|
676
|
+
echo " ✅ dist/src/ 含 ${JS_COUNT} 个 JS"
|
|
677
|
+
[ "$JS_COUNT" -lt 5 ] && echo " ❌ JS 数量异常偏少" && PREFLIGHT_OK=false
|
|
500
678
|
else
|
|
501
|
-
echo " ❌
|
|
502
|
-
PREFLIGHT_OK=false
|
|
679
|
+
echo " ❌ 缺少 dist/src/"; PREFLIGHT_OK=false
|
|
503
680
|
fi
|
|
504
681
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
if [ ! -f "$TARGET_DIR/$module" ]; then
|
|
509
|
-
MISSING_MODULES="$MISSING_MODULES $module"
|
|
510
|
-
fi
|
|
682
|
+
MISS=""
|
|
683
|
+
for m in "dist/src/gateway.js" "dist/src/api.js" "dist/src/admin-resolver.js"; do
|
|
684
|
+
[ ! -f "$TARGET_DIR/$m" ] && MISS="$MISS $m"
|
|
511
685
|
done
|
|
512
|
-
|
|
513
|
-
echo " ❌ 缺少关键模块:$MISSING_MODULES"
|
|
514
|
-
PREFLIGHT_OK=false
|
|
515
|
-
else
|
|
516
|
-
echo " ✅ 关键模块完整"
|
|
517
|
-
fi
|
|
686
|
+
[ -n "$MISS" ] && echo " ❌ 缺少:$MISS" && PREFLIGHT_OK=false || echo " ✅ 关键模块完整"
|
|
518
687
|
|
|
519
|
-
# bundled 依赖
|
|
520
688
|
if [ -d "$TARGET_DIR/node_modules" ]; then
|
|
521
|
-
|
|
522
|
-
for dep in
|
|
523
|
-
|
|
524
|
-
echo " ⚠️ bundled 依赖缺失: $dep"
|
|
525
|
-
BUNDLED_OK=false
|
|
526
|
-
fi
|
|
527
|
-
done
|
|
528
|
-
if $BUNDLED_OK; then
|
|
529
|
-
echo " ✅ 核心 bundled 依赖完整"
|
|
530
|
-
fi
|
|
689
|
+
BOK=true
|
|
690
|
+
for dep in ws silk-wasm; do [ ! -d "$TARGET_DIR/node_modules/$dep" ] && echo " ⚠️ 缺失: $dep" && BOK=false; done
|
|
691
|
+
$BOK && echo " ✅ bundled 依赖完整"
|
|
531
692
|
fi
|
|
532
693
|
|
|
533
694
|
if [ "$PREFLIGHT_OK" != "true" ]; then
|
|
534
|
-
echo ""
|
|
535
|
-
echo "
|
|
536
|
-
echo "QQBOT_NEW_VERSION=unknown"
|
|
537
|
-
echo "QQBOT_REPORT=⚠️ QQBot 升级异常,验证未通过"
|
|
695
|
+
echo ""; echo "❌ 验证未通过"
|
|
696
|
+
echo "QQBOT_NEW_VERSION=unknown"; echo "QQBOT_REPORT=⚠️ 验证未通过"
|
|
538
697
|
exit 1
|
|
539
698
|
fi
|
|
540
699
|
echo " ✅ 验证全部通过"
|
|
541
700
|
|
|
542
|
-
#
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
701
|
+
# 轻量健康检查
|
|
702
|
+
echo ""
|
|
703
|
+
echo " [健康检查] 确认插件注册..."
|
|
704
|
+
ensure_valid_cwd
|
|
705
|
+
PLIST="$(run_with_timeout 10 "plugins list" openclaw plugins list 2>&1 || true)"
|
|
706
|
+
echo "$PLIST" | grep -q "$PLUGIN_ID" && echo " ✅ 插件已注册" || \
|
|
707
|
+
echo " ⚠️ 未在 plugins list 中找到(重启后可能自动修复)"
|
|
708
|
+
|
|
709
|
+
# 安装后再次验证内置插件已禁用(install/update 过程中 openclaw 可能重新启用)
|
|
710
|
+
verify_builtin_disabled
|
|
711
|
+
|
|
712
|
+
# postinstall SDK link(update 路径不会执行 lifecycle scripts,这里统一补执行)
|
|
713
|
+
if [ -f "$TARGET_DIR/scripts/postinstall-link-sdk.js" ]; then
|
|
547
714
|
echo " 执行 postinstall-link-sdk..."
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
echo " ⚠️ postinstall-link-sdk 失败,插件可能无法加载"
|
|
552
|
-
fi
|
|
715
|
+
ensure_valid_cwd
|
|
716
|
+
node "$TARGET_DIR/scripts/postinstall-link-sdk.js" 2>&1 && echo " ✅ SDK 链接就绪" || \
|
|
717
|
+
echo " ⚠️ postinstall-link-sdk 失败(非致命)"
|
|
553
718
|
fi
|
|
554
719
|
|
|
555
|
-
#
|
|
720
|
+
# ============================================================================
|
|
721
|
+
# [3/4] 升级结果
|
|
722
|
+
# ============================================================================
|
|
556
723
|
echo ""
|
|
557
724
|
echo "[3/4] 升级结果..."
|
|
558
725
|
echo "QQBOT_NEW_VERSION=${NEW_VERSION:-unknown}"
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
echo "QQBOT_REPORT
|
|
562
|
-
else
|
|
563
|
-
echo "QQBOT_REPORT=⚠️ QQBot 升级异常,无法确认新版本"
|
|
564
|
-
fi
|
|
726
|
+
[ -n "$NEW_VERSION" ] && [ "$NEW_VERSION" != "unknown" ] && \
|
|
727
|
+
echo "QQBOT_REPORT=✅ QQBot 升级完成: v${NEW_VERSION}" || \
|
|
728
|
+
echo "QQBOT_REPORT=⚠️ 无法确认新版本"
|
|
565
729
|
|
|
566
730
|
echo ""
|
|
567
731
|
echo "==========================================="
|
|
568
732
|
echo " ✅ 安装完成"
|
|
569
733
|
echo "==========================================="
|
|
570
734
|
|
|
571
|
-
|
|
572
|
-
if [ "$NO_RESTART" = "true" ]; then
|
|
573
|
-
echo ""
|
|
574
|
-
echo "[跳过重启] --no-restart 已指定,脚本立即退出以便调用方触发 gateway restart"
|
|
575
|
-
exit 0
|
|
576
|
-
fi
|
|
577
|
-
|
|
578
|
-
# 以下步骤仅在非热更新(手动执行)场景中执行
|
|
735
|
+
[ "$NO_RESTART" = "true" ] && echo "" && echo "[跳过重启] --no-restart 已指定" && exit 0
|
|
579
736
|
|
|
580
|
-
#
|
|
737
|
+
# ============================================================================
|
|
738
|
+
# [配置] appid/secret
|
|
739
|
+
# ============================================================================
|
|
581
740
|
if [ -n "$APPID" ] && [ -n "$SECRET" ]; then
|
|
582
741
|
echo ""
|
|
583
742
|
echo "[配置] 写入 qqbot 通道配置..."
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
if [ "$CURRENT_TOKEN" = "$DESIRED_TOKEN" ]; then
|
|
606
|
-
echo " ✅ 当前配置已是目标值,跳过写入"
|
|
743
|
+
DESIRED="${APPID}:${SECRET}"
|
|
744
|
+
CURRENT=""
|
|
745
|
+
[ -f "$CONFIG_FILE" ] && CURRENT=$(node -e "
|
|
746
|
+
try {
|
|
747
|
+
const cfg = JSON.parse(require('fs').readFileSync('$CONFIG_FILE', 'utf8'));
|
|
748
|
+
for (const k of ['qqbot','openclaw-qqbot','openclaw-qq']) {
|
|
749
|
+
const ch = cfg.channels?.[k]; if (!ch) continue;
|
|
750
|
+
if (ch.token) { process.stdout.write(ch.token); break; }
|
|
751
|
+
if (ch.appId && ch.clientSecret) { process.stdout.write(ch.appId+':'+ch.clientSecret); break; }
|
|
752
|
+
}
|
|
753
|
+
} catch {}
|
|
754
|
+
" 2>/dev/null || true)
|
|
755
|
+
|
|
756
|
+
if [ "$CURRENT" = "$DESIRED" ]; then
|
|
757
|
+
echo " ✅ 配置已是目标值"
|
|
758
|
+
elif [ -f "$CONFIG_FILE" ] && node -e "
|
|
759
|
+
const fs = require('fs'), cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
|
|
760
|
+
(cfg.channels ??= {}).qqbot = { ...cfg.channels.qqbot, appId: '$APPID', clientSecret: '$SECRET' };
|
|
761
|
+
fs.writeFileSync('$CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n');
|
|
762
|
+
" 2>&1; then
|
|
763
|
+
echo " ✅ 通道配置写入成功"
|
|
607
764
|
else
|
|
608
|
-
|
|
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
|
|
765
|
+
echo " ❌ 写入失败,请手动编辑 $CONFIG_FILE"
|
|
625
766
|
fi
|
|
626
767
|
elif [ -n "$APPID" ] || [ -n "$SECRET" ]; then
|
|
627
|
-
echo ""
|
|
628
|
-
echo "⚠️ --appid 和 --secret 必须同时提供"
|
|
768
|
+
echo ""; echo "⚠️ --appid 和 --secret 必须同时提供"
|
|
629
769
|
fi
|
|
630
770
|
|
|
631
|
-
#
|
|
771
|
+
# ============================================================================
|
|
772
|
+
# [4/4] 重启 gateway
|
|
773
|
+
# ============================================================================
|
|
632
774
|
echo ""
|
|
633
775
|
|
|
634
|
-
#
|
|
776
|
+
# startup-marker 防重复通知
|
|
635
777
|
if [ -n "$NEW_VERSION" ] && [ "$NEW_VERSION" != "unknown" ]; then
|
|
636
|
-
MARKER_DIR="$
|
|
637
|
-
mkdir -p "$MARKER_DIR"
|
|
638
|
-
MARKER_FILE="$MARKER_DIR/startup-marker.json"
|
|
778
|
+
MARKER_DIR="$OPENCLAW_HOME/qqbot/data"; mkdir -p "$MARKER_DIR"
|
|
639
779
|
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\"}" > "$
|
|
780
|
+
echo "{\"version\":\"$NEW_VERSION\",\"startedAt\":\"$NOW\",\"greetedAt\":\"$NOW\"}" > "$MARKER_DIR/startup-marker.json"
|
|
641
781
|
fi
|
|
642
782
|
|
|
643
|
-
echo "[
|
|
644
|
-
|
|
783
|
+
echo "[4/4] 重启 gateway..."
|
|
784
|
+
ensure_valid_cwd
|
|
785
|
+
GW_RC=0; run_with_timeout 90 "gateway restart" openclaw gateway restart 2>&1 || GW_RC=$?
|
|
786
|
+
|
|
787
|
+
if [ $GW_RC -eq 0 ]; then
|
|
645
788
|
echo " ✅ gateway 已重启"
|
|
646
|
-
echo ""
|
|
647
|
-
if [ -n "$NEW_VERSION" ] && [ "$NEW_VERSION" != "unknown" ]; then
|
|
648
|
-
echo "🎉 QQBot 插件已更新至 v${NEW_VERSION},在线等候你的吩咐。"
|
|
649
|
-
fi
|
|
789
|
+
[ -n "$NEW_VERSION" ] && echo "" && echo "🎉 QQBot 插件已更新至 v${NEW_VERSION},在线等候你的吩咐。"
|
|
650
790
|
else
|
|
651
|
-
echo "
|
|
791
|
+
[ $GW_RC -eq 124 ] && echo " ⏰ gateway restart 超时"
|
|
792
|
+
echo " ⚠️ 重启失败,尝试 doctor --fix..."
|
|
793
|
+
ensure_valid_cwd
|
|
794
|
+
|
|
795
|
+
_bak=""; [ -f "$CONFIG_FILE" ] && _bak="$(mktemp "${TMPDIR:-/tmp}/.qqbot-pre-doctor-XXXXXX")" && cp -a "$CONFIG_FILE" "$_bak"
|
|
796
|
+
run_with_timeout 30 "doctor --fix" openclaw doctor --fix 2>&1 | head -20 | sed 's/^/ /' || true
|
|
797
|
+
|
|
798
|
+
if [ -n "$_bak" ] && [ -f "$_bak" ] && [ -f "$CONFIG_FILE" ]; then
|
|
799
|
+
_damaged=$(node -e "
|
|
800
|
+
try {
|
|
801
|
+
const fs = require('fs');
|
|
802
|
+
const b = JSON.parse(fs.readFileSync('$_bak','utf8')), a = JSON.parse(fs.readFileSync('$CONFIG_FILE','utf8'));
|
|
803
|
+
if (b.channels?.qqbot && !a.channels?.qqbot) process.stdout.write('channels.qqbot');
|
|
804
|
+
else if (b.plugins?.installs?.['$PLUGIN_ID'] && !a.plugins?.installs?.['$PLUGIN_ID']) process.stdout.write('installs');
|
|
805
|
+
else if (b.plugins?.entries?.['$PLUGIN_ID'] && !a.plugins?.entries?.['$PLUGIN_ID']) process.stdout.write('entries');
|
|
806
|
+
} catch {}
|
|
807
|
+
" 2>/dev/null || true)
|
|
808
|
+
[ -n "$_damaged" ] && echo " ⚠️ doctor 误删 $_damaged,恢复中..." && cp -a "$_bak" "$CONFIG_FILE" && echo " ✅ 已恢复"
|
|
809
|
+
rm -f "$_bak" 2>/dev/null || true
|
|
810
|
+
fi
|
|
811
|
+
|
|
812
|
+
echo ""; echo " [重试] gateway restart..."
|
|
813
|
+
ensure_valid_cwd
|
|
814
|
+
RR=0; run_with_timeout 90 "gateway restart (重试)" openclaw gateway restart 2>&1 || RR=$?
|
|
815
|
+
if [ $RR -eq 0 ]; then
|
|
816
|
+
echo " ✅ 重启成功"
|
|
817
|
+
[ -n "$NEW_VERSION" ] && echo "" && echo "🎉 QQBot 插件已更新至 v${NEW_VERSION},在线等候你的吩咐。"
|
|
818
|
+
else
|
|
819
|
+
echo " ❌ 仍无法重启,请手动排查:"
|
|
820
|
+
echo " openclaw doctor && openclaw gateway restart"
|
|
821
|
+
fi
|
|
652
822
|
fi
|