@tencent-connect/openclaw-qqbot 1.7.1 → 1.7.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 (38) hide show
  1. package/README.md +188 -3
  2. package/README.zh.md +190 -3
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/src/api.d.ts +2 -0
  6. package/dist/src/api.js +16 -3
  7. package/dist/src/config.d.ts +5 -1
  8. package/dist/src/config.js +12 -2
  9. package/dist/src/gateway.js +131 -169
  10. package/dist/src/slash-commands.js +119 -3
  11. package/dist/src/tools/channel.js +1 -4
  12. package/dist/src/tools/remind.js +0 -1
  13. package/dist/src/transport/index.d.ts +10 -0
  14. package/dist/src/transport/index.js +9 -0
  15. package/dist/src/transport/webhook-transport.d.ts +67 -0
  16. package/dist/src/transport/webhook-transport.js +245 -0
  17. package/dist/src/transport/webhook-verify.d.ts +48 -0
  18. package/dist/src/transport/webhook-verify.js +98 -0
  19. package/dist/src/types.d.ts +19 -0
  20. package/dist/src/utils/audio-convert.js +37 -9
  21. package/index.ts +1 -0
  22. package/package.json +1 -1
  23. package/scripts/postinstall-link-sdk.js +44 -0
  24. package/scripts/upgrade-via-npm.sh +358 -62
  25. package/scripts/upgrade-via-source.sh +122 -85
  26. package/src/api.ts +18 -4
  27. package/src/config.ts +15 -2
  28. package/src/gateway.ts +135 -167
  29. package/src/onboarding.ts +8 -0
  30. package/src/slash-commands.ts +137 -3
  31. package/src/tools/channel.ts +1 -7
  32. package/src/tools/remind.ts +0 -2
  33. package/src/transport/index.ts +11 -0
  34. package/src/transport/webhook-transport.ts +332 -0
  35. package/src/transport/webhook-verify.ts +119 -0
  36. package/src/types.ts +22 -1
  37. package/src/typings/openclaw-webhook-ingress.d.ts +66 -0
  38. package/src/utils/audio-convert.ts +37 -9
@@ -17,6 +17,9 @@
17
17
 
18
18
  set -eo pipefail
19
19
 
20
+ # 忽略 SIGTERM:gateway restart 时可能向进程组发送 SIGTERM,不能让它中断升级
21
+ trap 'echo " ⚠️ 收到 SIGTERM,已忽略(升级进行中)"' SIGTERM
22
+
20
23
  # ============================================================================
21
24
  # 进程隔离 — 脱离 gateway 进程组
22
25
  # ============================================================================
@@ -28,6 +31,8 @@ fi
28
31
  # ============================================================================
29
32
  # 环境准备
30
33
  # ============================================================================
34
+ _SCRIPT_START_MS="$(node -e "process.stdout.write(String(Date.now()))" 2>/dev/null || echo "$(date +%s)000")"
35
+
31
36
  SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)" || SCRIPT_DIR=""
32
37
  PROJECT_DIR=""
33
38
  [ -n "$SCRIPT_DIR" ] && PROJECT_DIR="$(cd "$SCRIPT_DIR/.." 2>/dev/null && pwd)" || true
@@ -53,6 +58,132 @@ done
53
58
 
54
59
  NPM_REGISTRIES="https://registry.npmjs.org/ https://mirrors.cloud.tencent.com/npm/"
55
60
 
61
+ # ============================================================================
62
+ # CLS 日志上报(腾讯云日志服务)
63
+ # ============================================================================
64
+ CLS_HOST="ap-guangzhou.cls.tencentcs.com"
65
+ CLS_TOPIC_ID="${CLS_TOPIC_ID:-845a0802-ec56-49a0-afa0-fe686b0a16f2}"
66
+ CLS_ENABLED="${CLS_ENABLED:-true}"
67
+
68
+ # 静态字段(全局变量,启动时采集一次)
69
+ _STATIC_FIELDS=""
70
+ _SESSION_ID=""
71
+
72
+ # 采集静态字段(只执行一次)
73
+ init_track_log() {
74
+ [ "$CLS_ENABLED" != "true" ] && return 0
75
+ [ -n "$_STATIC_FIELDS" ] && return 0 # 已初始化
76
+
77
+ _SESSION_ID="$(node -e "try{process.stdout.write(require('crypto').randomUUID())}catch{process.stdout.write(Date.now().toString())}" 2>/dev/null || echo "$$-$(date +%s)")"
78
+
79
+ local node_ver="${NODE_VERSION:-$(node --version 2>/dev/null || echo "")}"
80
+ local os_type="$(uname -s 2>/dev/null || echo "unknown")"
81
+
82
+ # 构造静态字段 JSON(转义处理)
83
+ _STATIC_FIELDS=$(node -e "
84
+ const fields = {
85
+ session_id: '$_SESSION_ID',
86
+ script_version: 'v4',
87
+ node_version: '$node_ver',
88
+ os: '$os_type'
89
+ };
90
+ process.stdout.write(JSON.stringify(fields));
91
+ " 2>/dev/null || echo "{}")
92
+ }
93
+
94
+ # 上报日志到 CLS
95
+ # 用法: track_log <event> <result> <log_message> [extra_key1=val1] [extra_key2=val2] ...
96
+ track_log() {
97
+ [ "$CLS_ENABLED" != "true" ] && return 0
98
+ [ -z "$CLS_TOPIC_ID" ] && return 0
99
+
100
+ local event="$1"
101
+ local result="$2"
102
+ local log_msg="$3"
103
+ shift 3
104
+
105
+ # 确保静态字段已初始化
106
+ [ -z "$_STATIC_FIELDS" ] && init_track_log
107
+
108
+ # 计算耗时(毫秒)
109
+ local _now_ms="$(node -e "process.stdout.write(String(Date.now()))" 2>/dev/null || echo "$(date +%s)000")"
110
+ local _elapsed_ms="$(( _now_ms - _SCRIPT_START_MS ))"
111
+
112
+ # 构造额外字段
113
+ local extra_fields=",\"elapsed_ms\":\"$_elapsed_ms\""
114
+ for arg in "$@"; do
115
+ if [[ "$arg" == *=* ]]; then
116
+ local key="${arg%%=*}"
117
+ local val="${arg#*=}"
118
+ extra_fields="$extra_fields,\"$key\":\"$val\""
119
+ fi
120
+ done
121
+
122
+ # 后台异步上报(不阻塞主流程)
123
+ (
124
+ node -e "
125
+ (() => {
126
+ try {
127
+ const https = require('https');
128
+ const staticFields = $_STATIC_FIELDS;
129
+ const contents = {
130
+ ...staticFields,
131
+ event: '$event',
132
+ result: '$result',
133
+ log: $(node -e "process.stdout.write(JSON.stringify('$log_msg'))" 2>/dev/null || echo "'$log_msg'"),
134
+ openclaw_version: '${OPENCLAW_VERSION:-}',
135
+ old_version: '${OLD_VERSION:-}',
136
+ new_version: '${NEW_VERSION:-}',
137
+ target_version: '${TARGET_VERSION:-}'$extra_fields
138
+ };
139
+
140
+ const body = JSON.stringify({
141
+ logs: [{
142
+ contents: contents,
143
+ time: Math.floor(Date.now() / 1000)
144
+ }],
145
+ source: 'qqbot-upgrade-script'
146
+ });
147
+
148
+ const req = https.request({
149
+ hostname: '$CLS_HOST',
150
+ path: '/tracklog?topic_id=$CLS_TOPIC_ID',
151
+ method: 'POST',
152
+ headers: {
153
+ 'Content-Type': 'application/json',
154
+ 'Content-Length': Buffer.byteLength(body)
155
+ },
156
+ timeout: 5000
157
+ }, (res) => { res.resume(); });
158
+
159
+ req.on('error', () => {});
160
+ req.on('timeout', () => req.destroy());
161
+ req.end(body);
162
+ } catch {}
163
+ })();
164
+ " 2>/dev/null
165
+ ) &
166
+ }
167
+
168
+ # 封装 echo:同时输出到终端 + 上报 CLS
169
+ # 用法: log <message> → event=log, result=info
170
+ # log <event> <result> <message> → 自定义 event/result
171
+ # log <event> <result> <message> k=v → 带额外字段
172
+ log() {
173
+ if [ $# -eq 1 ]; then
174
+ echo "$1"
175
+ track_log "log" "info" "$1"
176
+ elif [ $# -ge 3 ]; then
177
+ local _event="$1" _result="$2" _msg="$3"
178
+ shift 3
179
+ echo "$_msg"
180
+ track_log "$_event" "$_result" "$_msg" "$@"
181
+ else
182
+ # 2 个参数:当普通 echo,不上报
183
+ echo "$@"
184
+ fi
185
+ }
186
+
56
187
  # ============================================================================
57
188
  # 超时执行包装器(兼容 macOS 无 GNU timeout)
58
189
  # ============================================================================
@@ -62,7 +193,11 @@ run_with_timeout() {
62
193
  if command -v timeout &>/dev/null; then
63
194
  timeout --kill-after=10 "$timeout_secs" "$@" && return 0
64
195
  local rc=$?
65
- [ $rc -eq 124 ] && echo " ⏰ ${description} 超时 (${timeout_secs}s)"
196
+ # GNU coreutils timeout 超时返回 124;uutils coreutils 返回 125
197
+ if [ $rc -eq 124 ] || [ $rc -eq 125 ]; then
198
+ echo " ⏰ ${description} 超时 (${timeout_secs}s)"
199
+ return 124 # 统一返回 124 表示超时
200
+ fi
66
201
  return $rc
67
202
  fi
68
203
 
@@ -117,15 +252,21 @@ restore_reload_mode() {
117
252
 
118
253
  rollback_plugin_dir() {
119
254
  local reason="${1:-未知原因}"
255
+ log "rollback" "start" " 开始回滚..." "reason=$reason"
120
256
  if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR/$PLUGIN_ID" ]; then
121
257
  rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
122
258
  mv "$BACKUP_DIR/$PLUGIN_ID" "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || \
123
259
  cp -a "$BACKUP_DIR/$PLUGIN_ID" "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
124
- [ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ] && \
125
- echo " ↩️ 已回滚到旧版本 v$(read_pkg_version "$EXTENSIONS_DIR/$PLUGIN_ID/package.json")(原因: ${reason})" && return 0
126
- echo " 回滚后插件目录仍不完整!"; return 1
260
+ if [ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ]; then
261
+ local rollback_ver="$(read_pkg_version "$EXTENSIONS_DIR/$PLUGIN_ID/package.json")"
262
+ log "rollback" "success" " ↩️ 已回滚到旧版本 v${rollback_ver}(原因: ${reason})" "rollback_version=$rollback_ver"
263
+ return 0
264
+ fi
265
+ log "rollback" "fail" " ❌ 回滚后插件目录仍不完整!"
266
+ return 1
127
267
  fi
128
- echo " ⚠️ 无备份可回滚(原因: ${reason})"; return 1
268
+ log "rollback" "fail" " ⚠️ 无备份可回滚(原因: ${reason})" "reason=$reason"
269
+ return 1
129
270
  }
130
271
 
131
272
  # ============================================================================
@@ -138,7 +279,7 @@ acquire_upgrade_lock() {
138
279
  if [ -f "$UPGRADE_LOCK_FILE" ]; then
139
280
  local lock_pid="$(cat "$UPGRADE_LOCK_FILE" 2>/dev/null || true)"
140
281
  if [ -n "$lock_pid" ] && kill -0 "$lock_pid" 2>/dev/null; then
141
- echo "❌ 另一个升级进程正在运行 (PID: $lock_pid)"; exit 1
282
+ log "lock_conflict" "fail" "❌ 另一个升级进程正在运行 (PID: $lock_pid)" "lock_pid=$lock_pid"; exit 1
142
283
  fi
143
284
  rm -f "$UPGRADE_LOCK_FILE" 2>/dev/null || true
144
285
  fi
@@ -165,7 +306,9 @@ setup_temp_config() {
165
306
  " 2>/dev/null || true)"
166
307
  [ "$need_temp" != "1" ] && return 0
167
308
 
168
- TEMP_CONFIG_FILE="$(mktemp)"
309
+ # 临时配置必须放在 $OPENCLAW_HOME 下,避免 openclaw ≥2026.4.9 将其父目录当作 CONFIG_DIR
310
+ # (2026-04-07 新增逻辑:OPENCLAW_CONFIG_PATH 的 dirname 会被用作 CONFIG_DIR)
311
+ TEMP_CONFIG_FILE="$(mktemp "$OPENCLAW_HOME/.qqbot-temp-config-XXXXXX")"
169
312
  if node -e "
170
313
  const fs = require('fs');
171
314
  const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
@@ -179,10 +322,10 @@ setup_temp_config() {
179
322
  cfg.plugins?.entries && Object.keys(cfg.plugins.entries).length === 0 && delete cfg.plugins.entries;
180
323
  fs.writeFileSync('$TEMP_CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n');
181
324
  " 2>/dev/null; then
182
- echo " [兼容] 创建临时配置副本以通过 3.23+ 配置校验"
325
+ log "temp_config" "success" " [兼容] 创建临时配置副本以通过 3.23+ 配置校验"
183
326
  export OPENCLAW_CONFIG_PATH="$TEMP_CONFIG_FILE"
184
327
  else
185
- echo " ⚠️ 创建临时配置失败,继续使用原配置"
328
+ log "temp_config" "fail" " ⚠️ 创建临时配置失败,继续使用原配置"
186
329
  rm -f "$TEMP_CONFIG_FILE" 2>/dev/null || true; TEMP_CONFIG_FILE=""
187
330
  fi
188
331
  }
@@ -235,15 +378,15 @@ npm_pack_download() {
235
378
  fi
236
379
  done
237
380
  if [ "$ok" != "true" ]; then
238
- echo " ❌ npm pack 失败(所有 registry 均不可用)"
381
+ log "npm_pack" "fail" " ❌ npm pack 失败(所有 registry 均不可用)"
239
382
  rm -rf "$PACK_TMP_DIR" 2>/dev/null; PACK_TMP_DIR=""; return 1
240
383
  fi
241
384
  PACK_TGZ_FILE="$(find "$PACK_TMP_DIR" -maxdepth 1 -name '*.tgz' -type f | head -1)"
242
385
  if [ -z "$PACK_TGZ_FILE" ]; then
243
- echo " ❌ 未找到 tgz 文件"
386
+ log "npm_pack" "fail" " ❌ 未找到 tgz 文件"
244
387
  rm -rf "$PACK_TMP_DIR" 2>/dev/null; PACK_TMP_DIR=""; return 1
245
388
  fi
246
- echo " 已下载: $(basename "$PACK_TGZ_FILE")"
389
+ log "npm_pack" "success" " 已下载: $(basename "$PACK_TGZ_FILE")"
247
390
  return 0
248
391
  }
249
392
 
@@ -264,25 +407,47 @@ npm_pack_native_install() {
264
407
  echo " [Level 2] npm pack + openclaw install 本地目录"
265
408
  echo " ============================================"
266
409
 
267
- echo " [L2 1/3] 下载 tarball..."
410
+ echo " [L2 1/4] 下载 tarball..."
268
411
  npm_pack_download || return 1
269
412
 
270
413
  # 先解压再传目录路径给 openclaw,而非直接传 tarball 路径
271
414
  # 原因:openclaw installPluginFromArchive 漏传 --dangerously-force-unsafe-install,
272
415
  # installPluginFromDir 正确传递,传目录可绕过此 bug
273
- echo " [L2 2/3] 解压 tarball..."
416
+ echo " [L2 2/4] 解压 tarball..."
274
417
  local extract_dir
275
418
  extract_dir="$(mktemp -d "${TMPDIR:-/tmp}/.qqbot-extract-XXXXXX")"
276
419
  if ! tar xzf "$PACK_TGZ_FILE" -C "$extract_dir" 2>&1; then
277
- echo " ❌ 解压失败"; cleanup_pack; rm -rf "$extract_dir"; return 1
420
+ log "l2_extract" "fail" " ❌ 解压失败"; cleanup_pack; rm -rf "$extract_dir"; return 1
278
421
  fi
279
422
  cleanup_pack
280
423
  local package_dir="$extract_dir/package"
281
424
  if [ ! -f "$package_dir/package.json" ]; then
282
- echo " ❌ 解压后未找到 package.json"; rm -rf "$extract_dir"; return 1
425
+ log "l2_extract" "fail" " ❌ 解压后未找到 package.json"; rm -rf "$extract_dir"; return 1
283
426
  fi
284
427
 
285
- echo " [L2 3/3] openclaw 安装本地目录..."
428
+ # L1 失败可能留下残缺目录或 stage,L2 安装前再次清理
429
+ echo " [L2 3/4] 清理残留..."
430
+ [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
431
+ find "${EXTENSIONS_DIR:-/dev/null}" "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" \
432
+ -exec rm -rf {} + 2>/dev/null || true
433
+ # 从配置中移除插件记录,防止 openclaw CLI 报 "already exists"
434
+ local _l2_cfg="${TEMP_CONFIG_FILE:-$CONFIG_FILE}"
435
+ [ -f "$_l2_cfg" ] && node -e "
436
+ try {
437
+ const fs = require('fs');
438
+ const cfg = JSON.parse(fs.readFileSync('$_l2_cfg', 'utf8'));
439
+ let c = false;
440
+ if (cfg.plugins?.installs?.['$PLUGIN_ID']) { delete cfg.plugins.installs['$PLUGIN_ID']; c = true; }
441
+ if (cfg.plugins?.entries?.['$PLUGIN_ID']) { delete cfg.plugins.entries['$PLUGIN_ID']; c = true; }
442
+ if (Array.isArray(cfg.plugins?.allow)) {
443
+ const i = cfg.plugins.allow.indexOf('$PLUGIN_ID');
444
+ if (i >= 0) { cfg.plugins.allow.splice(i, 1); c = true; }
445
+ }
446
+ if (c) fs.writeFileSync('$_l2_cfg', JSON.stringify(cfg, null, 4) + '\n');
447
+ } catch {}
448
+ " 2>/dev/null || true
449
+
450
+ echo " [L2 4/4] 用 openclaw 安装本地目录..."
286
451
  ensure_valid_cwd
287
452
  local rc=0
288
453
  run_with_timeout "$INSTALL_TIMEOUT" "plugins install (local dir)" \
@@ -291,10 +456,10 @@ npm_pack_native_install() {
291
456
  rm -rf "$extract_dir" 2>/dev/null || true
292
457
 
293
458
  if [ $rc -eq 0 ] && [ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ]; then
294
- echo " ✅ Level 2 安装成功"
459
+ log "l2_install" "success" " ✅ Level 2 安装成功"
295
460
  return 0
296
461
  fi
297
- echo " Level 2 失败 (exit=$rc)"
462
+ log "l2_install" "fail" " Level 2 失败 (exit=$rc)" "exit_code=$rc"
298
463
  [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && [ ! -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ] && \
299
464
  rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
300
465
  find "${EXTENSIONS_DIR:-/dev/null}" "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" \
@@ -321,8 +486,8 @@ cleanup_on_exit() {
321
486
 
322
487
  if [ "$INSTALL_COMPLETED" != "true" ] && [ $exit_code -ne 0 ]; then
323
488
  local reason="异常退出 (code=$exit_code)"
324
- case $exit_code in 124) reason="安装超时";; 130) reason="用户中断";; 143) reason="SIGTERM";; 129) reason="SIGHUP";; esac
325
- echo " ⚠️ ${reason}"
489
+ case $exit_code in 124|125) reason="安装超时";; 130) reason="用户中断";; 143) reason="SIGTERM";; 129) reason="SIGHUP";; esac
490
+ log "abnormal_exit" "fail" " ⚠️ ${reason}" "reason=$reason" "exit_code=$exit_code"
326
491
  restore_config_snapshot
327
492
  rollback_plugin_dir "$reason"
328
493
  fi
@@ -335,17 +500,18 @@ cleanup_on_exit() {
335
500
  find "${EXTENSIONS_DIR:-/dev/null}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
336
501
  find "${TMPDIR:-/tmp}" -maxdepth 1 \( -name ".openclaw-install-stage-*" -o -name ".qqbot-pack-*" \
337
502
  -o -name ".qqbot-extract-*" -o -name ".qqbot-upgrade-backup-*" \) -exec rm -rf {} + 2>/dev/null || true
503
+ find "${OPENCLAW_HOME:-/dev/null}" -maxdepth 1 -name ".qqbot-temp-config-*" -exec rm -f {} + 2>/dev/null || true
338
504
  release_upgrade_lock
339
505
  exit $exit_code
340
506
  }
341
507
  trap cleanup_on_exit EXIT
342
- trap 'exit 143' TERM
343
508
  trap 'echo " 中断"; exit 130' INT
344
509
  trap 'exit 129' HUP
345
510
 
346
511
  # 清理上次升级遗留(>60min)
347
512
  find "${TMPDIR:-/tmp}" -maxdepth 1 \( -name ".qqbot-upgrade-backup-*" -o -name ".qqbot-pack-*" \
348
513
  -o -name ".qqbot-extract-*" \) -mmin +60 -exec rm -rf {} + 2>/dev/null || true
514
+ find "${OPENCLAW_HOME:-/dev/null}" -maxdepth 1 -name ".qqbot-temp-config-*" -mmin +60 -exec rm -f {} + 2>/dev/null || true
349
515
 
350
516
  # ============================================================================
351
517
  # 参数解析
@@ -425,7 +591,7 @@ if [ -n "$OPENCLAW_VERSION" ] && version_gte "$OPENCLAW_VERSION" "2026.3.30"; th
425
591
  FORCE_UNSAFE_FLAG="--dangerously-force-unsafe-install"
426
592
  fi
427
593
 
428
- echo "==========================================="
594
+ log "upgrade_start" "start" "==========================================="
429
595
  echo " qqbot 升级: $INSTALL_SRC"
430
596
  echo " openclaw: v${OPENCLAW_VERSION:-unknown}"
431
597
  echo " 隔离: ${_UPGRADE_ISOLATED:+✓ setsid}${_UPGRADE_ISOLATED:-✗} 超时: ${INSTALL_TIMEOUT}s"
@@ -437,20 +603,60 @@ OLD_PKG="$EXTENSIONS_DIR/$PLUGIN_ID/package.json"
437
603
  [ -f "$OLD_PKG" ] && OLD_VERSION="$(read_pkg_version "$OLD_PKG")"
438
604
  [ -n "$OLD_VERSION" ] && echo " 当前版本: $OLD_VERSION"
439
605
 
606
+ # 初始化日志上报
607
+ init_track_log
608
+
440
609
  # ============================================================================
441
- # 禁用内置冲突插件(配置禁用 + 目录删除 + 验证)
610
+ # 禁用内置冲突插件(配置禁用 + 验证)
442
611
  # ============================================================================
612
+ # 记录已确认存在的内置冲突插件 ID,供 verify_builtin_disabled 复用
613
+ CONFIRMED_BUILTIN_IDS=""
614
+
443
615
  disable_builtin_plugins() {
444
616
  local found_any=false
617
+ CONFIRMED_BUILTIN_IDS=""
618
+
619
+ # 一次性获取 openclaw 已知的所有插件 ID(stock + global)
620
+ local _known_ids=""
621
+ ensure_valid_cwd
622
+ _known_ids="$(run_with_timeout 15 "plugins list" openclaw plugins list 2>/dev/null \
623
+ | sed -n 's/^│[^│]*│[[:space:]]*\([a-zA-Z0-9_-]*\)[[:space:]]*│.*/\1/p' || true)"
624
+
445
625
  for bid in $BUILTIN_CONFLICT_IDS; do
446
626
  [ "$bid" = "$PLUGIN_ID" ] && continue
627
+
628
+ # 判断该内置插件是否存在:plugins list 中有 / 配置中有记录 / user extensions 目录有
629
+ local _bid_exists=false
630
+ echo "$_known_ids" | grep -qx "$bid" 2>/dev/null && _bid_exists=true
631
+ [ "$_bid_exists" != "true" ] && [ -d "$EXTENSIONS_DIR/$bid" ] && _bid_exists=true
632
+ [ "$_bid_exists" != "true" ] && node -e "
633
+ try {
634
+ const c = JSON.parse(require('fs').readFileSync('$CONFIG_FILE', 'utf8'));
635
+ if (c.plugins?.entries?.['$bid'] || c.plugins?.installs?.['$bid'] ||
636
+ (Array.isArray(c.plugins?.allow) && c.plugins.allow.includes('$bid')))
637
+ process.stdout.write('1');
638
+ } catch {}
639
+ " 2>/dev/null | grep -q '1' && _bid_exists=true
640
+
641
+ if [ "$_bid_exists" != "true" ]; then
642
+ echo " [禁用内置] $bid: 未检测到,跳过"
643
+ continue
644
+ fi
645
+
646
+ CONFIRMED_BUILTIN_IDS="$CONFIRMED_BUILTIN_IDS $bid"
647
+
447
648
  local _changed=""
448
649
  _changed="$(node -e "
449
650
  try {
450
651
  const fs = require('fs');
451
652
  const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
452
653
  let c = [];
453
- if (cfg.plugins?.entries?.['$bid']) { cfg.plugins.entries['$bid'].enabled = false; c.push('entries'); }
654
+ // 显式写入 enabled:false,内置插件即使没有 entries 记录也会自动加载
655
+ (cfg.plugins ??= {}).entries ??= {};
656
+ if (!cfg.plugins.entries['$bid'] || cfg.plugins.entries['$bid'].enabled !== false) {
657
+ cfg.plugins.entries['$bid'] = { ...cfg.plugins.entries['$bid'], enabled: false };
658
+ c.push('entries');
659
+ }
454
660
  if (Array.isArray(cfg.plugins?.allow) && cfg.plugins.allow.includes('$bid')) {
455
661
  cfg.plugins.allow = cfg.plugins.allow.filter(p => p !== '$bid'); c.push('allow');
456
662
  }
@@ -460,20 +666,36 @@ disable_builtin_plugins() {
460
666
  } catch {}
461
667
  " 2>/dev/null || true)"
462
668
  [ -n "$_changed" ] && echo " [禁用内置] $bid: 已修改 $_changed" && found_any=true
463
- if [ -d "$EXTENSIONS_DIR/$bid" ]; then
464
- rm -rf "$EXTENSIONS_DIR/$bid"; echo " [禁用内置] 已删除 extensions/$bid"; found_any=true
465
- fi
466
669
  done
467
- [ "$found_any" = "true" ] && echo " ✅ 内置冲突插件已禁用" || echo " ℹ️ 未发现需要禁用的内置冲突插件"
670
+ if [ "$found_any" = "true" ]; then
671
+ log "disable_builtin" "success" " ✅ 内置冲突插件已禁用" "confirmed_ids=$CONFIRMED_BUILTIN_IDS"
672
+ else
673
+ log "disable_builtin" "skip" " ℹ️ 未发现需要禁用的内置冲突插件"
674
+ fi
468
675
  }
469
676
 
470
677
  verify_builtin_disabled() {
471
- for bid in $BUILTIN_CONFLICT_IDS; do
472
- [ "$bid" = "$PLUGIN_ID" ] && continue
473
- 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)"
678
+ [ -z "$CONFIRMED_BUILTIN_IDS" ] && return 0
679
+ for bid in $CONFIRMED_BUILTIN_IDS; do
680
+ local _e="$(node -e "
681
+ try {
682
+ const c = JSON.parse(require('fs').readFileSync('$CONFIG_FILE', 'utf8'));
683
+ const e = c.plugins?.entries?.['$bid'];
684
+ // 不存在或 enabled 不为 false 都需要修复
685
+ if (!e || e.enabled !== false) process.stdout.write('1');
686
+ } catch {}
687
+ " 2>/dev/null || true)"
474
688
  if [ "$_e" = "1" ]; then
475
- echo " ⚠️ 内置插件 $bid 仍启用,再次禁用..."
476
- 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
689
+ log "verify_builtin" "fix" " ⚠️ 内置插件 $bid 未禁用,写入 entries..." "bid=$bid"
690
+ node -e "
691
+ try {
692
+ const f = require('fs');
693
+ const c = JSON.parse(f.readFileSync('$CONFIG_FILE', 'utf8'));
694
+ (c.plugins ??= {}).entries ??= {};
695
+ c.plugins.entries['$bid'] = { ...c.plugins.entries['$bid'], enabled: false };
696
+ f.writeFileSync('$CONFIG_FILE', JSON.stringify(c, null, 4) + '\n');
697
+ } catch {}
698
+ " 2>/dev/null || true
477
699
  fi
478
700
  done
479
701
  }
@@ -502,13 +724,16 @@ setup_temp_config
502
724
 
503
725
  # 清理历史遗留 ID 的配置记录(qqbot/openclaw-qq 是旧版本使用的 ID,
504
726
  # entries 中残留会导致 gateway 重复加载同一插件报 tool name conflict)
727
+ # 注意:跳过 enabled===false 的 entries,那是 disable_builtin_plugins 写入的禁用记录
505
728
  [ -f "$CONFIG_FILE" ] && node -e "
506
729
  try {
507
730
  const fs = require('fs');
508
731
  const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
509
732
  let c = false;
510
733
  for (const old of ['qqbot', 'openclaw-qq']) {
511
- if (cfg.plugins?.entries?.[old]) { delete cfg.plugins.entries[old]; c = true; }
734
+ if (cfg.plugins?.entries?.[old] && cfg.plugins.entries[old].enabled !== false) {
735
+ delete cfg.plugins.entries[old]; c = true;
736
+ }
512
737
  if (cfg.plugins?.installs?.[old]) { delete cfg.plugins.installs[old]; c = true; }
513
738
  if (Array.isArray(cfg.plugins?.allow)) {
514
739
  const i = cfg.plugins.allow.indexOf(old);
@@ -539,10 +764,10 @@ HAS_PLUGIN_DIR=false
539
764
  USE_UPDATE=false
540
765
  if [ "$HAS_INSTALL_RECORD" = "yes" ] && [ "$HAS_PLUGIN_DIR" = "true" ] && [ -z "$TARGET_VERSION" ]; then
541
766
  if [ -n "$FORCE_UNSAFE_FLAG" ]; then
542
- echo " [检测] 配置 ✓ | 目录 ✓ | openclaw ≥3.30 → 跳过 update,直接 install(安全扫描兼容)"
767
+ log "install_decision" "info" " [检测] 配置 ✓ | 目录 ✓ | openclaw ≥3.30 → 跳过 update,直接 install(安全扫描兼容)" "decision=install" "reason=force_unsafe"
543
768
  else
544
769
  USE_UPDATE=true
545
- echo " [检测] 配置 ✓ | 目录 ✓ | 未指定版本 → update"
770
+ log "install_decision" "info" " [检测] 配置 ✓ | 目录 ✓ | 未指定版本 → update" "decision=update"
546
771
  # spec 解锁
547
772
  if [ -n "$INSTALL_SPEC" ]; then
548
773
  SPEC_SUFFIX="${INSTALL_SPEC##*@}"
@@ -562,9 +787,9 @@ if [ "$HAS_INSTALL_RECORD" = "yes" ] && [ "$HAS_PLUGIN_DIR" = "true" ] && [ -z "
562
787
  fi
563
788
  fi
564
789
  elif [ "$HAS_PLUGIN_DIR" = "true" ]; then
565
- echo " [检测] 目录 ✓ | 指定版本或无配置记录 → reinstall"
790
+ log "install_decision" "info" " [检测] 目录 ✓ | 指定版本或无配置记录 → reinstall" "decision=reinstall"
566
791
  else
567
- echo " [检测] 目录 ✗ → 全新安装"
792
+ log "install_decision" "info" " [检测] 目录 ✗ → 全新安装" "decision=fresh_install"
568
793
  fi
569
794
 
570
795
  mark_success() {
@@ -585,29 +810,39 @@ if [ "$USE_UPDATE" = "true" ]; then
585
810
  if [ $UPDATE_RC -eq 0 ]; then
586
811
  POST_VER=""; [ -f "$OLD_PKG" ] && POST_VER="$(read_pkg_version "$OLD_PKG")"
587
812
  if [ -n "$POST_VER" ] && [ "$POST_VER" != "$OLD_VERSION" ]; then
588
- mark_success; echo " ✅ update 成功 ($OLD_VERSION → $POST_VER)"
813
+ mark_success; log "level1_update" "success" " ✅ update 成功 ($OLD_VERSION → $POST_VER)" "method=update" "post_version=$POST_VER"
589
814
  elif [ -z "$OLD_VERSION" ]; then
590
- mark_success; echo " ✅ update 成功"
815
+ mark_success; log "level1_update" "success" " ✅ update 成功" "method=update"
591
816
  else
592
817
  echo " ℹ️ 版本未变 ($POST_VER),查询 npm latest..."
593
818
  NPM_LATEST="$(npm view "$PKG_NAME" version 2>/dev/null || true)"
594
819
  if [ -n "$NPM_LATEST" ] && [ "$NPM_LATEST" = "$POST_VER" ]; then
595
- mark_success; echo " ✅ 已是最新版本 $POST_VER"
820
+ mark_success; log "level1_update" "success" " ✅ 已是最新版本 $POST_VER" "method=update" "version=$POST_VER"
596
821
  else
597
- echo " npm latest=${NPM_LATEST:-unknown},当前=$POST_VER"
822
+ log "level1_update" "fail" " npm latest=${NPM_LATEST:-unknown},当前=$POST_VER" "npm_latest=$NPM_LATEST" "current=$POST_VER"
598
823
  fi
599
824
  fi
600
825
  else
601
- [ $UPDATE_RC -eq 124 ] && echo " ⏰ update 超时" || echo " update 失败 (exit=$UPDATE_RC)"
826
+ if [ $UPDATE_RC -eq 124 ]; then
827
+ log "level1_update" "fail" " ⏰ update 超时" "method=update" "exit_code=$UPDATE_RC"
828
+ else
829
+ log "level1_update" "fail" " update 失败 (exit=$UPDATE_RC)" "method=update" "exit_code=$UPDATE_RC"
830
+ fi
602
831
  fi
603
832
 
604
833
  # Level 1 失败 → Level 2 降级
605
834
  if [ "$UPGRADE_OK" != "true" ]; then
835
+ log "level2_fallback" "start" " 尝试 Level 2 降级..." "reason=level1_failed"
606
836
  if [ -z "$BACKUP_DIR" ] && [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ]; then
607
837
  BACKUP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/.qqbot-upgrade-backup-XXXXXX")"
608
838
  cp -a "$EXTENSIONS_DIR/$PLUGIN_ID" "$BACKUP_DIR/$PLUGIN_ID"
609
839
  fi
610
- run_fallback && mark_success
840
+ if run_fallback; then
841
+ mark_success
842
+ log "level2_fallback" "success" " ✅ Level 2 降级成功"
843
+ else
844
+ log "level2_fallback" "fail" " ❌ Level 2 降级失败"
845
+ fi
611
846
  fi
612
847
  fi
613
848
 
@@ -644,7 +879,7 @@ if [ "$UPGRADE_OK" != "true" ]; then
644
879
  " 2>/dev/null || true
645
880
 
646
881
  # Level 1: 原生 install(单次尝试,失败后由 Level 2 的 npm pack 多源重试接管)
647
- echo " [Level 1] 尝试 openclaw plugins install..."
882
+ log "level1_install" "start" " [Level 1] 尝试 openclaw plugins install..." "method=install"
648
883
  ensure_valid_cwd
649
884
  RC=0
650
885
  run_with_timeout "$INSTALL_TIMEOUT" \
@@ -652,24 +887,29 @@ if [ "$UPGRADE_OK" != "true" ]; then
652
887
  $FORCE_UNSAFE_FLAG 2>&1 || RC=$?
653
888
 
654
889
  if [ $RC -eq 0 ] && [ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ]; then
655
- mark_success; echo " ✅ Level 1 install 成功"
890
+ mark_success; log "level1_install" "success" " ✅ Level 1 install 成功" "method=install"
656
891
  else
657
- [ $RC -ne 0 ] && echo " Level 1 install 失败 (exit=$RC)"
892
+ log "level1_install" "fail" " Level 1 install 失败 (exit=$RC)" "method=install" "exit_code=$RC"
658
893
  # 清理不完整的目录和 stage
659
894
  [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && [ ! -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ] && \
660
895
  rm -rf "$EXTENSIONS_DIR/$PLUGIN_ID" 2>/dev/null || true
661
896
  find "${EXTENSIONS_DIR:-/dev/null}" "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" \
662
897
  -exec rm -rf {} + 2>/dev/null || true
663
898
 
664
- echo " Level 1 失败,尝试 Level 2 降级..."
665
- run_fallback && mark_success || {
899
+ log "level2_fallback" "start" " Level 1 失败,尝试 Level 2 降级..." "reason=level1_install_failed"
900
+ if run_fallback; then
901
+ mark_success
902
+ log "level2_fallback" "success" " ✅ Level 2 降级成功"
903
+ else
904
+ log "level2_fallback" "fail" " ❌ Level 2 降级失败"
905
+ log "upgrade_complete" "fail" " 升级完全失败,已回滚"
666
906
  rollback_plugin_dir "安装失败"; restore_config_snapshot
667
907
  [ -n "$TEMP_CONFIG_FILE" ] && rm -f "$TEMP_CONFIG_FILE" 2>/dev/null || true
668
908
  unset OPENCLAW_CONFIG_PATH 2>/dev/null || true
669
909
  echo "QQBOT_NEW_VERSION=unknown"
670
910
  echo "QQBOT_REPORT=❌ QQBot 安装失败(已回滚),请检查网络"
671
911
  exit 1
672
- }
912
+ fi
673
913
  fi
674
914
  fi
675
915
 
@@ -713,11 +953,12 @@ if [ -d "$TARGET_DIR/node_modules" ]; then
713
953
  fi
714
954
 
715
955
  if [ "$PREFLIGHT_OK" != "true" ]; then
716
- echo ""; echo "❌ 验证未通过"
956
+ echo ""
957
+ log "validation" "fail" "❌ 验证未通过" "missing=$MISS"
717
958
  echo "QQBOT_NEW_VERSION=unknown"; echo "QQBOT_REPORT=⚠️ 验证未通过"
718
959
  exit 1
719
960
  fi
720
- echo " ✅ 验证全部通过"
961
+ log "validation" "success" " ✅ 验证全部通过"
721
962
 
722
963
  # 轻量健康检查
723
964
  echo ""
@@ -749,7 +990,7 @@ echo "QQBOT_NEW_VERSION=${NEW_VERSION:-unknown}"
749
990
  echo "QQBOT_REPORT=⚠️ 无法确认新版本"
750
991
 
751
992
  echo ""
752
- echo "==========================================="
993
+ log "upgrade_complete" "success" "===========================================" "new_version=${NEW_VERSION:-unknown}"
753
994
  echo " ✅ 安装完成"
754
995
  echo "==========================================="
755
996
 
@@ -805,16 +1046,71 @@ fi
805
1046
 
806
1047
  echo "[4/4] 重启 gateway..."
807
1048
  ensure_valid_cwd
808
- GW_RC=0; run_with_timeout 90 "gateway restart" openclaw gateway restart 2>&1 || GW_RC=$?
1049
+ GW_RC=0; GW_OUTPUT="$(run_with_timeout 90 "gateway restart" openclaw gateway restart 2>&1)" || GW_RC=$?
1050
+ echo "$GW_OUTPUT"
809
1051
 
810
1052
  if [ $GW_RC -eq 0 ]; then
811
- echo " ✅ gateway 已重启"
1053
+ log "gateway_restart" "success" " ✅ gateway 已重启"
812
1054
  [ -n "$NEW_VERSION" ] && echo "" && echo "🎉 QQBot 插件已更新至 v${NEW_VERSION},在线等候你的吩咐。"
813
1055
  else
814
- [ $GW_RC -eq 124 ] && echo " ⏰ gateway restart 超时"
815
- echo " ⚠️ 重启失败,尝试 doctor --fix..."
816
- ensure_valid_cwd
1056
+ if [ $GW_RC -eq 124 ]; then
1057
+ log "gateway_restart" "fail" " gateway restart 超时" "exit_code=$GW_RC"
1058
+ else
1059
+ log "gateway_restart" "fail" " ⚠️ 重启失败" "exit_code=$GW_RC"
1060
+ fi
1061
+
1062
+ # 检测是否是 qqbot 通道配置的 additional properties 校验错误
1063
+ if echo "$GW_OUTPUT" | grep -q "additional properties"; then
1064
+ echo ""
1065
+ log "config_fix" "start" " [配置修复] 检测到 QQBot 通道配置包含不支持的字段"
1066
+
1067
+ # 备份当前配置
1068
+ _cfg_backup="${CONFIG_FILE}.pre-fix.$(date +%s)"
1069
+ cp -a "$CONFIG_FILE" "$_cfg_backup" 2>/dev/null && \
1070
+ echo " [配置修复] 已备份当前配置到: $_cfg_backup"
817
1071
 
1072
+ # 只保留合法字段: enabled/appId/clientSecret/allowFrom/accounts
1073
+ # accounts 内每个条目也只保留: enabled/appId/clientSecret/allowFrom
1074
+ node -e "
1075
+ try {
1076
+ const fs = require('fs');
1077
+ const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
1078
+ const ch = cfg.channels?.qqbot;
1079
+ if (!ch) process.exit(0);
1080
+
1081
+ const ALLOWED_ROOT = new Set(['enabled', 'appId', 'clientSecret', 'allowFrom', 'accounts']);
1082
+ const ALLOWED_ACCOUNT = new Set(['enabled', 'appId', 'clientSecret', 'allowFrom']);
1083
+
1084
+ const cleaned = {};
1085
+ for (const k of Object.keys(ch)) {
1086
+ if (!ALLOWED_ROOT.has(k)) continue;
1087
+ if (k === 'accounts' && typeof ch.accounts === 'object' && ch.accounts !== null) {
1088
+ cleaned.accounts = {};
1089
+ for (const [accId, acc] of Object.entries(ch.accounts)) {
1090
+ if (typeof acc !== 'object' || acc === null) continue;
1091
+ const cleanedAcc = {};
1092
+ for (const ak of Object.keys(acc)) {
1093
+ if (ALLOWED_ACCOUNT.has(ak)) cleanedAcc[ak] = acc[ak];
1094
+ }
1095
+ if (Object.keys(cleanedAcc).length > 0) cleaned.accounts[accId] = cleanedAcc;
1096
+ }
1097
+ if (Object.keys(cleaned.accounts).length === 0) delete cleaned.accounts;
1098
+ } else {
1099
+ cleaned[k] = ch[k];
1100
+ }
1101
+ }
1102
+
1103
+ cfg.channels.qqbot = cleaned;
1104
+ fs.writeFileSync('$CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n');
1105
+ process.stdout.write('fixed');
1106
+ } catch (e) { process.stderr.write(String(e)); }
1107
+ " 2>/dev/null && log "config_fix" "success" " [配置修复] ✅ 已清理 channels.qqbot 中不支持的字段" || \
1108
+ log "config_fix" "fail" " [配置修复] ⚠️ 自动修复失败,请手动编辑 $CONFIG_FILE"
1109
+
1110
+ echo " [配置修复] 如需恢复原配置: cp $_cfg_backup $CONFIG_FILE"
1111
+ fi
1112
+
1113
+ ensure_valid_cwd
818
1114
  _bak=""; [ -f "$CONFIG_FILE" ] && _bak="$(mktemp "${TMPDIR:-/tmp}/.qqbot-pre-doctor-XXXXXX")" && cp -a "$CONFIG_FILE" "$_bak"
819
1115
  run_with_timeout 30 "doctor --fix" openclaw doctor --fix 2>&1 | head -20 | sed 's/^/ /' || true
820
1116
 
@@ -828,7 +1124,7 @@ else
828
1124
  else if (b.plugins?.entries?.['$PLUGIN_ID'] && !a.plugins?.entries?.['$PLUGIN_ID']) process.stdout.write('entries');
829
1125
  } catch {}
830
1126
  " 2>/dev/null || true)
831
- [ -n "$_damaged" ] && echo " ⚠️ doctor 误删 $_damaged,恢复中..." && cp -a "$_bak" "$CONFIG_FILE" && echo " ✅ 已恢复"
1127
+ [ -n "$_damaged" ] && log "doctor_fix" "warn" " ⚠️ doctor 误删 $_damaged,恢复中..." "damaged=$_damaged" && cp -a "$_bak" "$CONFIG_FILE" && echo " ✅ 已恢复"
832
1128
  rm -f "$_bak" 2>/dev/null || true
833
1129
  fi
834
1130
 
@@ -836,10 +1132,10 @@ else
836
1132
  ensure_valid_cwd
837
1133
  RR=0; run_with_timeout 90 "gateway restart (重试)" openclaw gateway restart 2>&1 || RR=$?
838
1134
  if [ $RR -eq 0 ]; then
839
- echo " ✅ 重启成功"
1135
+ log "gateway_retry" "success" " ✅ 重启成功"
840
1136
  [ -n "$NEW_VERSION" ] && echo "" && echo "🎉 QQBot 插件已更新至 v${NEW_VERSION},在线等候你的吩咐。"
841
1137
  else
842
- echo " ❌ 仍无法重启,请手动排查:"
1138
+ log "gateway_retry" "fail" " ❌ 仍无法重启,请手动排查:" "exit_code=$RR"
843
1139
  echo " openclaw doctor && openclaw gateway restart"
844
1140
  fi
845
1141
  fi