@tencent-connect/openclaw-qqbot 1.6.7-beta.3 → 1.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  **Connect your AI assistant to QQ — private chat, group chat, and rich media, all in one plugin.**
12
12
 
13
- ### 🚀 Current Version: `v1.6.6`
13
+ ### 🚀 Current Version: `v1.6.7`
14
14
 
15
15
  [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
16
16
  [![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](https://bot.q.qq.com/wiki/)
package/README.zh.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  **让你的 AI 助手接入 QQ — 私聊、群聊、富媒体,一个插件全搞定。**
11
11
 
12
- ### 🚀 当前版本: `v1.6.6`
12
+ ### 🚀 当前版本: `v1.6.7`
13
13
 
14
14
  [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
15
15
  [![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](https://bot.q.qq.com/wiki/)
@@ -21,11 +21,8 @@ import { fileURLToPath } from "node:url";
21
21
  import { getPackageVersion } from "./utils/pkg-version.js";
22
22
  const require = createRequire(import.meta.url);
23
23
  let PLUGIN_VERSION = getPackageVersion(import.meta.url);
24
- // 获取 openclaw 框架版本(缓存结果,只执行一次)
25
- let _frameworkVersion = null;
24
+ // 获取 openclaw 框架版本(不缓存,每次实时获取)
26
25
  export function getFrameworkVersion() {
27
- if (_frameworkVersion !== null)
28
- return _frameworkVersion;
29
26
  try {
30
27
  // 先尝试 PATH 中的 CLI
31
28
  // Windows 上 npm 安装的 CLI 通常是 .cmd wrapper,execFileSync 需要 shell:true 才能执行
@@ -37,8 +34,7 @@ export function getFrameworkVersion() {
37
34
  }).trim();
38
35
  // 输出格式: "OpenClaw 2026.3.13 (61d171a)"
39
36
  if (out) {
40
- _frameworkVersion = out;
41
- return _frameworkVersion;
37
+ return out;
42
38
  }
43
39
  }
44
40
  catch {
@@ -50,16 +46,14 @@ export function getFrameworkVersion() {
50
46
  if (cliPath) {
51
47
  const out = execCliSync(cliPath, ["--version"]);
52
48
  if (out) {
53
- _frameworkVersion = out;
54
- return _frameworkVersion;
49
+ return out;
55
50
  }
56
51
  }
57
52
  }
58
53
  catch {
59
54
  // fallback
60
55
  }
61
- _frameworkVersion = "unknown";
62
- return _frameworkVersion;
56
+ return "unknown";
63
57
  }
64
58
  // ============ 热更新兼容性检查 ============
65
59
  /**
@@ -152,7 +146,7 @@ function checkUpgradeCompatibility() {
152
146
  // 3. 检查 Node.js 版本
153
147
  const nodeVer = process.version.replace(/^v/, "");
154
148
  if (compareSemver(nodeVer, req.minNodeVersion) < 0) {
155
- errors.push(`❌ NoVBNde.js 版本过低:当前 **v${nodeVer}**,热更新要求最低 **v${req.minNodeVersion}**`);
149
+ errors.push(`❌ Node.js 版本过低:当前 **v${nodeVer}**,热更新要求最低 **v${req.minNodeVersion}**`);
156
150
  }
157
151
  // 4. 检查系统架构(arm 等特殊架构提示)
158
152
  const arch = process.arch;
@@ -690,10 +684,118 @@ function fireHotUpgrade(targetVersion, pkg, useLocal) {
690
684
  shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : []), ...(pkg ? ["--pkg", pkg] : [])];
691
685
  }
692
686
  console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}, pkg=${pkg || "default"}`);
687
+ // ── 兼容 openclaw 3.23+ 配置严格校验 ──
688
+ // openclaw plugins install/update 启动时会校验整个配置文件,
689
+ // 如果 channels.qqbot 已存在但 qqbot 插件尚未加载,校验会报 "unknown channel id: qqbot"。
690
+ //
691
+ // ⚠️ 关键:绝不能直接修改真实的 openclaw.json!
692
+ // gateway 的 config file watcher 会检测到变更并触发 SIGUSR1 重启,
693
+ // 导致当前进程被杀、execFile 回调(restoreConfigAndCleanup)永远不会执行,
694
+ // channels.qqbot 配置就此丢失。
695
+ //
696
+ // 策略:创建临时配置副本(不含 channels.qqbot),通过 OPENCLAW_CONFIG_PATH
697
+ // 环境变量传递给子进程,真实配置文件不受影响。
698
+ // shell 脚本(upgrade-via-npm.sh)内部也有同样的临时配置机制作为双保险。
699
+ const homeDir = getHomeDir();
700
+ const realConfigPath = path.join(homeDir, ".openclaw", "openclaw.json");
701
+ let tempConfigPath = null;
702
+ const childEnv = { ...process.env };
703
+ try {
704
+ if (fs.existsSync(realConfigPath)) {
705
+ const cfg = JSON.parse(fs.readFileSync(realConfigPath, "utf8"));
706
+ const needsTempConfig = !!(cfg.channels?.qqbot) ||
707
+ !!(cfg.plugins?.entries?.["openclaw-qqbot"]);
708
+ if (needsTempConfig) {
709
+ // 创建临时配置副本(移除 channels.qqbot 和 plugins.entries.openclaw-qqbot)
710
+ const cleanCfg = JSON.parse(JSON.stringify(cfg)); // deep clone
711
+ if (cleanCfg.channels?.qqbot) {
712
+ delete cleanCfg.channels.qqbot;
713
+ if (Object.keys(cleanCfg.channels).length === 0)
714
+ delete cleanCfg.channels;
715
+ }
716
+ if (cleanCfg.plugins?.entries?.["openclaw-qqbot"]) {
717
+ delete cleanCfg.plugins.entries["openclaw-qqbot"];
718
+ if (cleanCfg.plugins.entries && Object.keys(cleanCfg.plugins.entries).length === 0)
719
+ delete cleanCfg.plugins.entries;
720
+ }
721
+ const tmpDir = path.join(homeDir, ".openclaw", ".qqbot-upgrade-tmp");
722
+ fs.mkdirSync(tmpDir, { recursive: true });
723
+ tempConfigPath = path.join(tmpDir, "openclaw-tmp.json");
724
+ fs.writeFileSync(tempConfigPath, JSON.stringify(cleanCfg, null, 4) + "\n");
725
+ childEnv.OPENCLAW_CONFIG_PATH = tempConfigPath;
726
+ console.log(`[qqbot] fireHotUpgrade: created temp config without channels.qqbot (OPENCLAW_CONFIG_PATH=${tempConfigPath}), real config untouched`);
727
+ }
728
+ }
729
+ }
730
+ catch (e) {
731
+ console.warn(`[qqbot] fireHotUpgrade: failed to create temp config: ${e.message}, proceeding with original`);
732
+ tempConfigPath = null;
733
+ }
734
+ /**
735
+ * 将 openclaw plugins install 写入临时配置的 installs/entries 记录同步回真实配置,
736
+ * 然后清理临时文件。
737
+ *
738
+ * 注意:真实配置中的 channels.qqbot 从未被移除,无需恢复。
739
+ */
740
+ function syncTempConfigAndCleanup() {
741
+ try {
742
+ if (tempConfigPath && fs.existsSync(tempConfigPath) && fs.existsSync(realConfigPath)) {
743
+ const tmp = JSON.parse(fs.readFileSync(tempConfigPath, "utf8"));
744
+ const real = JSON.parse(fs.readFileSync(realConfigPath, "utf8"));
745
+ let changed = false;
746
+ // 同步 plugins.installs(openclaw plugins install 会写入安装记录)
747
+ if (tmp.plugins?.installs) {
748
+ if (!real.plugins)
749
+ real.plugins = {};
750
+ real.plugins.installs = { ...(real.plugins.installs || {}), ...tmp.plugins.installs };
751
+ changed = true;
752
+ }
753
+ // 同步 plugins.entries(openclaw plugins install 会写入 entries)
754
+ // 注意:不同步 openclaw-qqbot 自身的 entry,因为插件通过 auto-discover 加载,
755
+ // 显式写入 entries 会导致 "duplicate plugin id" 警告刷屏。
756
+ if (tmp.plugins?.entries) {
757
+ if (!real.plugins)
758
+ real.plugins = {};
759
+ if (!real.plugins.entries)
760
+ real.plugins.entries = {};
761
+ for (const [k, v] of Object.entries(tmp.plugins.entries)) {
762
+ if (k === "openclaw-qqbot")
763
+ continue; // 跳过自身,避免 duplicate
764
+ if (!real.plugins.entries[k]) {
765
+ real.plugins.entries[k] = v;
766
+ changed = true;
767
+ }
768
+ }
769
+ }
770
+ if (changed) {
771
+ fs.writeFileSync(realConfigPath, JSON.stringify(real, null, 4) + "\n");
772
+ console.log("[qqbot] fireHotUpgrade: synced install/entries records from temp config to real config");
773
+ }
774
+ }
775
+ }
776
+ catch (e) {
777
+ console.warn(`[qqbot] fireHotUpgrade: failed to sync temp config: ${e.message}`);
778
+ }
779
+ // 清理临时文件
780
+ try {
781
+ if (tempConfigPath)
782
+ fs.unlinkSync(tempConfigPath);
783
+ }
784
+ catch { /* ignore */ }
785
+ }
693
786
  // 异步执行升级脚本
694
- execFile(shell, shellArgs, {
695
- timeout: 120_000,
696
- env: { ...process.env },
787
+ // 必须显式设置 cwd 为一个确定存在的目录(如 homeDir),
788
+ // 否则子进程继承 gateway 的 cwd,如果该目录在升级过程中被删除/移动,
789
+ // openclaw CLI 启动时 process.cwd() 会报 ENOENT: uv_cwd 错误。
790
+ // 超时设为 5 分钟:openclaw plugins install 需要下载 npm 包,
791
+ // 网络慢时(如国内访问 npm registry)可能需要 2-3 分钟。
792
+ // 120 秒超时会导致脚本被杀但 openclaw CLI 子进程继续运行,
793
+ // 同时 bash 的 cleanup_on_exit 回滚了备份目录,造成 "plugin already exists" 错误。
794
+ const child = execFile(shell, shellArgs, {
795
+ timeout: 300_000,
796
+ cwd: homeDir,
797
+ env: childEnv,
798
+ killSignal: "SIGTERM",
697
799
  ...(isWindows() ? { windowsHide: true } : {}),
698
800
  }, (error, stdout, _stderr) => {
699
801
  if (error) {
@@ -702,6 +804,25 @@ function fireHotUpgrade(targetVersion, pkg, useLocal) {
702
804
  console.error(`[qqbot] fireHotUpgrade: stdout: ${stdout.slice(0, 2000)}`);
703
805
  if (_stderr)
704
806
  console.error(`[qqbot] fireHotUpgrade: stderr: ${_stderr.slice(0, 2000)}`);
807
+ // 超时时确保子进程树被清理,防止 openclaw plugins install 继续运行
808
+ // 与 cleanup_on_exit 的回滚逻辑冲突(回滚恢复了旧目录,install 又尝试写入)
809
+ if (error.killed || error.message.includes("TIMEOUT")) {
810
+ try {
811
+ // 尝试杀掉子进程树(SIGKILL 确保立即终止)
812
+ child.kill("SIGKILL");
813
+ // 额外尝试通过 pkill 杀掉可能残留的 openclaw plugins install 子进程
814
+ if (!isWindows()) {
815
+ try {
816
+ execFileSync("pkill", ["-9", "-f", "openclaw.*plugins.*install"], { timeout: 3000, stdio: "pipe" });
817
+ }
818
+ catch { /* ignore */ }
819
+ }
820
+ }
821
+ catch {
822
+ // 进程可能已退出
823
+ }
824
+ }
825
+ syncTempConfigAndCleanup();
705
826
  cleanupTempScript();
706
827
  _upgrading = false;
707
828
  return;
@@ -712,12 +833,14 @@ function fireHotUpgrade(targetVersion, pkg, useLocal) {
712
833
  const newVersion = versionMatch?.[1];
713
834
  if (newVersion === "unknown") {
714
835
  console.error(`[qqbot] fireHotUpgrade: script output QQBOT_NEW_VERSION=unknown, aborting restart`);
836
+ syncTempConfigAndCleanup();
715
837
  cleanupTempScript();
716
838
  _upgrading = false;
717
839
  return;
718
840
  }
719
841
  console.log(`[qqbot] fireHotUpgrade: new version=${newVersion || "(not detected)"}, triggering restart...`);
720
- // 脚本执行成功,清理临时脚本副本
842
+ // 脚本执行成功,同步临时配置中的 install 记录并清理
843
+ syncTempConfigAndCleanup();
721
844
  cleanupTempScript();
722
845
  // 文件替换成功,在 restart 之前把 source 从 path 切换为 npm,
723
846
  // 确保新进程启动时读到的是 npm source,不会被本地源码覆盖。
@@ -760,17 +883,175 @@ function fireHotUpgrade(targetVersion, pkg, useLocal) {
760
883
  }
761
884
  }
762
885
  else {
763
- // Mac/Linux: 直接 restart(框架通常以 daemon 模式运行)
764
- execCliAsync(cli, ["gateway", "restart"], { timeout: 30_000 }, (restartErr) => {
765
- if (restartErr) {
766
- console.error(`[qqbot] fireHotUpgrade: restart failed: ${restartErr.message}, trying stop+start fallback`);
767
- execCliAsync(cli, ["gateway", "stop"], { timeout: 10_000 }, () => {
768
- setTimeout(() => {
769
- execCliAsync(cli, ["gateway", "start"], { timeout: 30_000 }, () => { });
770
- }, 1000);
771
- });
886
+ // Mac/Linux: 使用 detached shell 脚本执行 stop+start
887
+ //
888
+ // 兼容 openclaw 2026.3.24+ 配置严格校验:
889
+ // gateway restart openclaw 先校验配置(loadConfig)再加载插件。
890
+ // 如果 channels.qqbot 存在但 qqbot channel type 尚未注册,校验会失败。
891
+ // 解决:stop 后临时移除 channels.qqbot → start(插件加载、qqbot type 注册)→ 恢复。
892
+ const cliInvoke = cli.endsWith(".mjs")
893
+ ? `"${process.execPath}" "${cli}"`
894
+ : `"${cli}"`;
895
+ const homeDir = getHomeDir();
896
+ const configPath = path.join(homeDir, ".openclaw", "openclaw.json");
897
+ const qqbotChannelBackup = path.join(homeDir, ".openclaw", ".qqbot-channel-backup.json");
898
+ const restartScript = path.join(homeDir, ".openclaw", ".qqbot-restart.sh");
899
+ // 先保存 channels.qqbot 到临时文件(在当前进程中,JSON 处理更安全)
900
+ let hasChannel = false;
901
+ try {
902
+ const cfgRaw = fs.readFileSync(configPath, "utf8");
903
+ const cfg = JSON.parse(cfgRaw);
904
+ const qqbot = cfg?.channels?.qqbot;
905
+ if (qqbot) {
906
+ fs.writeFileSync(qqbotChannelBackup, JSON.stringify(qqbot, null, 2), "utf8");
907
+ hasChannel = true;
772
908
  }
773
- });
909
+ }
910
+ catch {
911
+ // 配置文件不存在或 JSON 解析失败,不做处理
912
+ }
913
+ const shContent = `#!/bin/bash
914
+ # 注意:不使用 set -e,因为 gateway start 失败时仍需恢复 channels.qqbot
915
+ CLI="${cliInvoke}"
916
+ CONFIG="${configPath}"
917
+ BACKUP="${qqbotChannelBackup}"
918
+
919
+ # ── 兼容 openclaw 3.23+ 配置严格校验 ──
920
+ # 所有 openclaw CLI 命令(包括 gateway stop/start)启动时都会 loadConfig 校验配置,
921
+ # 如果 channels.qqbot 存在但 qqbot 插件尚未加载,校验会报 "unknown channel id: qqbot"。
922
+ #
923
+ # 策略:
924
+ # 1. gateway stop:使用 OPENCLAW_CONFIG_PATH 临时配置(不含 channels.qqbot)
925
+ # 2. gateway start:先尝试直接启动(真实配置),如果 CLI 校验失败,
926
+ # 则临时修改真实配置(此时 gateway 已停止,无 config watcher),启动后恢复。
927
+ # 这样 gateway 进程读取的是完整配置(含 channels.qqbot)。
928
+
929
+ # 为 gateway stop 创建临时配置
930
+ TEMP_RESTART_CONFIG=""
931
+ if [ -f "$BACKUP" ]; then
932
+ TEMP_RESTART_CONFIG="\$(mktemp)"
933
+ node -e "
934
+ const fs = require('fs');
935
+ const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
936
+ if (cfg.channels && cfg.channels.qqbot) {
937
+ delete cfg.channels.qqbot;
938
+ if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
939
+ }
940
+ if (cfg.plugins && cfg.plugins.entries && cfg.plugins.entries['openclaw-qqbot']) {
941
+ delete cfg.plugins.entries['openclaw-qqbot'];
942
+ if (Object.keys(cfg.plugins.entries).length === 0) delete cfg.plugins.entries;
943
+ }
944
+ fs.writeFileSync(process.argv[2], JSON.stringify(cfg, null, 4) + '\\n');
945
+ " "$CONFIG" "$TEMP_RESTART_CONFIG" 2>/dev/null
946
+ if [ \$? -ne 0 ] || [ ! -s "$TEMP_RESTART_CONFIG" ]; then
947
+ echo "[qqbot-upgrade] WARNING: failed to create temp config"
948
+ TEMP_RESTART_CONFIG=""
949
+ fi
950
+ fi
951
+
952
+ echo "[qqbot-upgrade] Stopping gateway..."
953
+ if [ -n "$TEMP_RESTART_CONFIG" ]; then
954
+ OPENCLAW_CONFIG_PATH="$TEMP_RESTART_CONFIG" $CLI gateway stop 2>/dev/null || true
955
+ else
956
+ $CLI gateway stop 2>/dev/null || true
957
+ fi
958
+ sleep 2
959
+
960
+ # 清理临时配置(不再需要)
961
+ if [ -n "$TEMP_RESTART_CONFIG" ] && [ -f "$TEMP_RESTART_CONFIG" ]; then
962
+ rm -f "$TEMP_RESTART_CONFIG"
963
+ fi
964
+
965
+ echo "[qqbot-upgrade] Starting gateway..."
966
+ START_OK=false
967
+
968
+ # 先尝试直接启动(使用真实配置,含 channels.qqbot)
969
+ # 如果 openclaw 版本不做严格校验,或者插件已注册,这会直接成功
970
+ if $CLI gateway start 2>/dev/null; then
971
+ START_OK=true
972
+ echo "[qqbot-upgrade] Gateway started successfully (direct start)"
973
+ elif [ -f "$BACKUP" ]; then
974
+ # 直接启动失败(可能是 channels.qqbot 校验失败),
975
+ # 临时修改真实配置(此时 gateway 已停止,无 config watcher,安全)
976
+ echo "[qqbot-upgrade] Direct start failed, temporarily removing channels.qqbot from real config..."
977
+ node -e "
978
+ const fs = require('fs');
979
+ const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
980
+ let changed = false;
981
+ if (cfg.channels && cfg.channels.qqbot) {
982
+ delete cfg.channels.qqbot;
983
+ if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
984
+ changed = true;
985
+ }
986
+ if (cfg.plugins && cfg.plugins.entries && cfg.plugins.entries['openclaw-qqbot']) {
987
+ delete cfg.plugins.entries['openclaw-qqbot'];
988
+ if (Object.keys(cfg.plugins.entries).length === 0) delete cfg.plugins.entries;
989
+ changed = true;
990
+ }
991
+ if (changed) {
992
+ fs.writeFileSync(process.argv[1], JSON.stringify(cfg, null, 4) + '\\n');
993
+ }
994
+ " "$CONFIG" 2>/dev/null
995
+
996
+ if $CLI gateway start 2>/dev/null; then
997
+ START_OK=true
998
+ echo "[qqbot-upgrade] Gateway started successfully (after config fix)"
999
+ else
1000
+ echo "[qqbot-upgrade] WARNING: gateway start still failed after config fix"
1001
+ fi
1002
+
1003
+ # 等待 gateway 进程启动并加载插件(插件注册 qqbot channel type)
1004
+ echo "[qqbot-upgrade] Waiting for plugin to load (8s)..."
1005
+ sleep 8
1006
+
1007
+ # 恢复 channels.qqbot 到真实配置
1008
+ # gateway 的 config file watcher 会检测到变更并热加载
1009
+ echo "[qqbot-upgrade] Restoring channels.qqbot to real config..."
1010
+ node -e "
1011
+ const fs = require('fs');
1012
+ const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
1013
+ const qqbot = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
1014
+ if (!cfg.channels) cfg.channels = {};
1015
+ cfg.channels.qqbot = qqbot;
1016
+ // 注意:不写入 plugins.entries.openclaw-qqbot,
1017
+ // 插件通过 auto-discover 加载,显式 entry 会导致 duplicate plugin id 警告。
1018
+ fs.writeFileSync(process.argv[1], JSON.stringify(cfg, null, 4) + '\\n');
1019
+ " "$CONFIG" "$BACKUP" 2>/dev/null
1020
+ rm -f "$BACKUP"
1021
+ echo "[qqbot-upgrade] channels.qqbot restored"
1022
+ else
1023
+ echo "[qqbot-upgrade] WARNING: gateway start failed, no backup to restore"
1024
+ fi
1025
+
1026
+ # 直接启动成功的情况下,清理备份文件
1027
+ if [ "$START_OK" = "true" ] && [ -f "$BACKUP" ]; then
1028
+ rm -f "$BACKUP"
1029
+ fi
1030
+
1031
+ # 如果 start 失败,尝试再次启动
1032
+ if [ "$START_OK" != "true" ]; then
1033
+ echo "[qqbot-upgrade] Retrying gateway start..."
1034
+ sleep 2
1035
+ $CLI gateway start 2>/dev/null || echo "[qqbot-upgrade] WARNING: retry also failed"
1036
+ fi
1037
+
1038
+ # 清理自身
1039
+ rm -f "$0"
1040
+ echo "[qqbot-upgrade] Done."
1041
+ `;
1042
+ try {
1043
+ fs.writeFileSync(restartScript, shContent, { mode: 0o755 });
1044
+ const child = spawn("bash", [restartScript], {
1045
+ detached: true,
1046
+ stdio: "ignore",
1047
+ });
1048
+ child.unref();
1049
+ console.log(`[qqbot] fireHotUpgrade: launched detached restart script (pid=${child.pid}), hasChannel=${hasChannel}`);
1050
+ }
1051
+ catch (shErr) {
1052
+ console.error(`[qqbot] fireHotUpgrade: failed to launch restart script: ${shErr.message}, falling back to direct restart`);
1053
+ execCliAsync(cli, ["gateway", "restart"], { timeout: 30_000 }, () => { });
1054
+ }
774
1055
  }
775
1056
  });
776
1057
  return { ok: true };
@@ -6,10 +6,22 @@ import { fileURLToPath } from "node:url";
6
6
  import { createRequire } from "node:module";
7
7
  import path from "node:path";
8
8
  import fs from "node:fs";
9
- let _cached = null;
9
+ /** 已定位到的 package.json 路径,避免重复遍历目录树 */
10
+ let _resolvedPkgPath = null;
10
11
  export function getPackageVersion(metaUrl) {
11
- if (_cached !== null)
12
- return _cached;
12
+ // 如果之前已定位到 package.json 路径,直接重新读取(快速路径)
13
+ if (_resolvedPkgPath) {
14
+ try {
15
+ const pkg = JSON.parse(fs.readFileSync(_resolvedPkgPath, "utf8"));
16
+ if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
17
+ return pkg.version;
18
+ }
19
+ }
20
+ catch {
21
+ // 文件可能已被删除(升级过程中),清除路径缓存,走完整查找
22
+ _resolvedPkgPath = null;
23
+ }
24
+ }
13
25
  // Strategy 1: 从调用者的 import.meta.url(或本模块)向上遍历找 package.json
14
26
  const startFile = metaUrl ? fileURLToPath(metaUrl) : fileURLToPath(import.meta.url);
15
27
  let dir = path.dirname(startFile);
@@ -21,8 +33,8 @@ export function getPackageVersion(metaUrl) {
21
33
  const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
22
34
  // 确认是我们自己的包(避免找到其他 package.json)
23
35
  if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
24
- _cached = pkg.version;
25
- return _cached;
36
+ _resolvedPkgPath = candidate;
37
+ return pkg.version;
26
38
  }
27
39
  }
28
40
  }
@@ -38,14 +50,12 @@ export function getPackageVersion(metaUrl) {
38
50
  try {
39
51
  const pkg = require(rel);
40
52
  if (pkg?.version) {
41
- _cached = pkg.version;
42
- return _cached;
53
+ return pkg.version;
43
54
  }
44
55
  }
45
56
  catch { /* next */ }
46
57
  }
47
58
  }
48
59
  catch { /* fallback */ }
49
- _cached = "unknown";
50
- return _cached;
60
+ return "unknown";
51
61
  }
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.6.7-beta.3",
3
+ "version": "1.6.7",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
4
7
  "type": "module",
5
8
  "main": "dist/index.js",
6
9
  "types": "dist/index.d.ts",
@@ -18,15 +18,62 @@
18
18
 
19
19
  set -eo pipefail
20
20
 
21
- # 异常退出时清理临时文件(防止泄露或残留)
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 是否已完成(用于区分正常退出和异常退出)
22
32
  cleanup_on_exit() {
33
+ local exit_code=$?
34
+
35
+ # 异常退出时同步临时配置中的 install 记录回真实配置
23
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
24
54
  rm -f "$TEMP_CONFIG_FILE" 2>/dev/null || true
25
55
  fi
26
- # 异常退出时清理本次备份目录(正常流程中备份已被删除或回滚,这里是兜底)
27
- if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
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
+ # 正常完成,清理备份
28
71
  rm -rf "$BACKUP_DIR" 2>/dev/null || true
29
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
30
77
  }
31
78
  trap cleanup_on_exit EXIT
32
79
 
@@ -40,8 +87,6 @@ TARGET_VERSION=""
40
87
  APPID=""
41
88
  SECRET=""
42
89
  NO_RESTART=false
43
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
44
- PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
45
90
 
46
91
  LOCAL_VERSION="$(node -e "
47
92
  try {
@@ -180,30 +225,46 @@ echo "[1/4] 安装/升级插件..."
180
225
  # 环境变量让 plugins install/update 使用临时配置,真实配置文件不受影响。
181
226
  CONFIG_FILE="$HOME/.$CMD/$CMD.json"
182
227
  TEMP_CONFIG_FILE=""
183
- HAS_QQBOT_CHANNEL=false
228
+ NEEDS_TEMP_CONFIG=false
184
229
 
185
230
  if [ -f "$CONFIG_FILE" ]; then
186
- HAS_QQBOT_CHANNEL="$(node -e "
231
+ NEEDS_TEMP_CONFIG="$(node -e "
187
232
  try {
188
233
  const fs = require('fs');
189
234
  const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
190
- if (cfg.channels && cfg.channels.qqbot) process.stdout.write('true');
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');
191
239
  } catch {}
192
240
  " 2>/dev/null || true)"
193
241
 
194
- if [ "$HAS_QQBOT_CHANNEL" = "true" ]; then
242
+ if [ "$NEEDS_TEMP_CONFIG" = "true" ]; then
195
243
  TEMP_CONFIG_FILE="$(mktemp)"
196
244
  node -e "
197
245
  try {
198
246
  const fs = require('fs');
199
247
  const cfg = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
200
- delete cfg.channels.qqbot;
201
- if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
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
+ }
202
263
  fs.writeFileSync('$TEMP_CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n');
203
264
  } catch(e) { process.exit(1); }
204
265
  " 2>/dev/null
205
266
  if [ $? -eq 0 ]; then
206
- echo " [兼容] 创建临时配置副本(不含 channels.qqbot)以通过配置校验"
267
+ echo " [兼容] 创建临时配置副本(不含 channels.qqbot / plugins.allow / plugins.entries)以通过配置校验"
207
268
  export OPENCLAW_CONFIG_PATH="$TEMP_CONFIG_FILE"
208
269
  else
209
270
  echo " ⚠️ 创建临时配置失败,继续使用原配置"
@@ -216,22 +277,32 @@ fi
216
277
  # plugins install/update 可能把 install 记录写入了临时配置,需要同步回真实配置
217
278
  restore_qqbot_channel() {
218
279
  if [ -n "$TEMP_CONFIG_FILE" ] && [ -f "$TEMP_CONFIG_FILE" ]; then
219
- # 将临时配置中 plugins.installs 的变更同步回真实配置
280
+ # 将临时配置中 plugins.installs 和 plugins.entries 的变更同步回真实配置
220
281
  node -e "
221
282
  try {
222
283
  const fs = require('fs');
223
284
  const tmp = JSON.parse(fs.readFileSync('$TEMP_CONFIG_FILE', 'utf8'));
224
285
  const real = JSON.parse(fs.readFileSync('$CONFIG_FILE', 'utf8'));
286
+ let changed = false;
225
287
  if (tmp.plugins && tmp.plugins.installs) {
226
288
  if (!real.plugins) real.plugins = {};
227
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) {
228
299
  fs.writeFileSync('$CONFIG_FILE', JSON.stringify(real, null, 4) + '\n');
229
300
  }
230
301
  } catch {}
231
302
  " 2>/dev/null || true
232
303
  rm -f "$TEMP_CONFIG_FILE"
233
304
  unset OPENCLAW_CONFIG_PATH
234
- echo " [兼容] 已同步 install 记录并清理临时配置副本"
305
+ echo " [兼容] 已同步 install/entries 记录并清理临时配置副本"
235
306
  fi
236
307
  }
237
308
 
@@ -320,21 +391,57 @@ if [ "$UPGRADE_OK" != "true" ]; then
320
391
  echo " 执行 install: $INSTALL_SRC"
321
392
 
322
393
  if $CMD plugins install "$INSTALL_SRC" --pin 2>&1; then
323
- UPGRADE_OK=true
324
- echo " install 成功"
325
- # install 成功,清理备份
326
- if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
327
- rm -rf "$BACKUP_DIR"
328
- echo " 已清理旧版备份"
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
329
431
  fi
330
- # 清理 openclaw CLI install 可能留下的额外 backup 目录(extensions 内遗留 + 新路径)
331
- find "$EXTENSIONS_DIR" -maxdepth 1 -name ".openclaw-qqbot-backup-*" -exec rm -rf {} + 2>/dev/null || true
332
- find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".qqbot-upgrade-backup-*" -exec rm -rf {} + 2>/dev/null || true
333
432
  else
334
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
335
437
  # 回滚:恢复旧目录
336
438
  if [ -n "$BACKUP_DIR" ] && [ -d "$BACKUP_DIR" ]; then
337
- mv "$BACKUP_DIR" "$EXTENSIONS_DIR/$PLUGIN_ID"
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
338
445
  echo " ↩️ 已回滚到旧版本"
339
446
  fi
340
447
  restore_qqbot_channel
@@ -262,8 +262,9 @@ if lsof -i :18789 -sTCP:LISTEN >/dev/null 2>&1; then
262
262
  sleep 1
263
263
  fi
264
264
 
265
- # 清理之前可能残留的 staging 目录
265
+ # 清理之前可能残留的 staging 目录(extensions 和 /tmp 中都可能存在)
266
266
  find "$HOME/.openclaw/extensions/" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
267
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
267
268
 
268
269
  # ── 清空并重新构建 dist/ ──
269
270
  # openclaw plugins install . 只做文件复制,不执行 npm lifecycle scripts。
@@ -290,7 +291,25 @@ fi
290
291
  # 由于 CJS/ESM 混用问题(Node 22 ERR_INTERNAL_ASSERTION),验证阶段可能失败
291
292
  # 但文件已成功拷贝。因此不依赖退出码,而是检查安装目录中关键文件是否存在。
292
293
  _INSTALL_DIR="$HOME/.openclaw/extensions/openclaw-qqbot"
294
+
295
+ # ── 优化:安装前临时移走 node_modules ──
296
+ # openclaw plugins install . 会把整个项目目录(含 node_modules/)复制到 extensions,
297
+ # 但运行时只需要 bundledDependencies 中的 3 个包 + openclaw symlink。
298
+ # 移走 node_modules 可避免复制数百个不必要的包,大幅加速安装。
299
+ _NM_BACKUP=""
300
+ if [ -d "$PROJ_DIR/node_modules" ]; then
301
+ echo " 临时移走 node_modules(避免整体复制到 extensions)..."
302
+ _NM_BACKUP="$PROJ_DIR/.node_modules_install_bak"
303
+ mv "$PROJ_DIR/node_modules" "$_NM_BACKUP"
304
+ fi
305
+
293
306
  openclaw plugins install . 2>&1 | tee "$INSTALL_LOG" || true
307
+
308
+ # ── 恢复 node_modules ──
309
+ if [ -n "$_NM_BACKUP" ] && [ -d "$_NM_BACKUP" ]; then
310
+ mv "$_NM_BACKUP" "$PROJ_DIR/node_modules"
311
+ echo " 已恢复源码目录 node_modules"
312
+ fi
294
313
  if [ ! -f "$_INSTALL_DIR/dist/index.js" ] || [ ! -f "$_INSTALL_DIR/preload.cjs" ]; then
295
314
  echo ""
296
315
  echo "❌ 插件安装失败!"
@@ -398,6 +417,13 @@ else
398
417
  echo ""
399
418
  echo " 尝试自动修复: 清理残留并重试安装..."
400
419
  find "$HOME/.openclaw/extensions/" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
420
+ find "${TMPDIR:-/tmp}" -maxdepth 1 -name ".openclaw-install-stage-*" -exec rm -rf {} + 2>/dev/null || true
421
+ # 重试时同样移走 node_modules 避免整体复制
422
+ _NM_BACKUP=""
423
+ if [ -d "$PROJ_DIR/node_modules" ]; then
424
+ _NM_BACKUP="$PROJ_DIR/.node_modules_install_bak"
425
+ mv "$PROJ_DIR/node_modules" "$_NM_BACKUP"
426
+ fi
401
427
  if openclaw plugins install . 2>&1 | tee -a "$INSTALL_LOG"; then
402
428
  for _candidate_name in openclaw-qqbot qqbot openclaw-qq; do
403
429
  if [ -d "$HOME/.openclaw/extensions/$_candidate_name" ]; then
@@ -407,6 +433,10 @@ else
407
433
  fi
408
434
  done
409
435
  fi
436
+ # 恢复 node_modules
437
+ if [ -n "$_NM_BACKUP" ] && [ -d "$_NM_BACKUP" ]; then
438
+ mv "$_NM_BACKUP" "$PROJ_DIR/node_modules"
439
+ fi
410
440
  if [ "$_plugin_dir_ok" -eq 0 ]; then
411
441
  echo " ❌ 重试安装仍失败,插件目录不存在"
412
442
  echo " 请手动排查: ls -la ~/.openclaw/extensions/"
@@ -440,18 +470,70 @@ else
440
470
  fi
441
471
  fi
442
472
 
443
- # 清理多余的 peerDependencies 传递依赖(兼容旧版 openclaw):
444
- # openclaw v2026.3.4 之前的 plugins install 缺少 --omit=peer,会把 peerDeps
445
- # (openclaw 平台及其 400+ 传递依赖)也安装到插件 node_modules 中。
446
- # 新版已修复,此处通过阈值判断:包数量 > 50 才触发清理,避免对新版做无用操作。
473
+ # ── 复制 bundledDependencies 到插件 node_modules ──
474
+ # 由于安装前移走了源码的 node_modules,extensions 中的插件目录没有依赖。
475
+ # 只需复制 bundledDependenciesws, silk-wasm, mpg123-decoder)及其传递依赖即可。
447
476
  PLUGIN_NM=""
448
477
  for _candidate in openclaw-qqbot qqbot openclaw-qq; do
449
478
  _nm="$HOME/.openclaw/extensions/$_candidate/node_modules"
450
- [ -d "$_nm" ] && PLUGIN_NM="$_nm" && break
479
+ _ext_dir="$HOME/.openclaw/extensions/$_candidate"
480
+ [ -d "$_ext_dir" ] && PLUGIN_NM="$_nm" && break
451
481
  done
452
- if [ -n "$PLUGIN_NM" ]; then
482
+ if [ -n "$PLUGIN_NM" ] && [ -d "$PROJ_DIR/node_modules" ]; then
483
+ # 如果 extensions 中已有大量包(旧版 openclaw 安装的 peerDeps),先清理
484
+ if [ -d "$PLUGIN_NM" ]; then
485
+ _before=$(ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null | wc -l | tr -d ' ')
486
+ if [ "$_before" -gt 50 ]; then
487
+ echo ""
488
+ echo "检测到 ${_before} 个包(超过阈值 50),清理多余的 peerDep 传递依赖..."
489
+ rm -rf "$PLUGIN_NM"
490
+ fi
491
+ fi
492
+
493
+ # 读取 bundledDependencies 及其传递依赖列表,只复制这些包
494
+ _deps_to_copy=$(node -e "
495
+ const fs = require('fs');
496
+ const path = require('path');
497
+ const pkgPath = path.join('$PROJ_DIR', 'package.json');
498
+ if (!fs.existsSync(pkgPath)) process.exit(0);
499
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
500
+ const bundled = pkg.bundledDependencies || pkg.bundleDependencies || [];
501
+ const keep = new Set();
502
+ const resolve = (name) => {
503
+ if (keep.has(name)) return;
504
+ keep.add(name);
505
+ const depPkg = path.join('$PROJ_DIR', 'node_modules', name, 'package.json');
506
+ if (!fs.existsSync(depPkg)) return;
507
+ const dep = JSON.parse(fs.readFileSync(depPkg, 'utf8'));
508
+ for (const d of Object.keys(dep.dependencies || {})) resolve(d);
509
+ };
510
+ bundled.forEach(resolve);
511
+ process.stdout.write([...keep].join('\\n'));
512
+ " 2>/dev/null || true)
513
+
514
+ if [ -n "$_deps_to_copy" ]; then
515
+ mkdir -p "$PLUGIN_NM"
516
+ _copied=0
517
+ echo "$_deps_to_copy" | while IFS= read -r _dep; do
518
+ _src="$PROJ_DIR/node_modules/$_dep"
519
+ _dst="$PLUGIN_NM/$_dep"
520
+ if [ -d "$_src" ] && [ ! -d "$_dst" ]; then
521
+ # 处理 scoped 包(如 @scope/pkg)
522
+ mkdir -p "$(dirname "$_dst")"
523
+ cp -r "$_src" "$_dst"
524
+ fi
525
+ done
526
+ _after=$(ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null | wc -l | tr -d ' ')
527
+ echo " ✅ 已复制 bundled 依赖到插件目录(${_after} 个包)"
528
+ else
529
+ echo " ⚠️ 未读取到 bundledDependencies,跳过依赖复制"
530
+ fi
531
+ elif [ -n "$PLUGIN_NM" ] && [ -d "$PLUGIN_NM" ]; then
532
+ # node_modules 已存在(可能是旧版 openclaw 安装的),检查是否需要清理
453
533
  _before=$(ls -d "$PLUGIN_NM"/*/ "$PLUGIN_NM"/@*/*/ 2>/dev/null | wc -l | tr -d ' ')
454
534
  if [ "$_before" -gt 50 ]; then
535
+ echo ""
536
+ echo "检测到 ${_before} 个包(超过阈值 50),清理多余的 peerDep 传递依赖..."
455
537
  # 读取 bundledDependencies 列表,只保留这些包及其子依赖
456
538
  _bundled_deps=$(node -e "
457
539
  const fs = require('fs');
@@ -490,8 +572,6 @@ else
490
572
  process.stdout.write(toRemove.join('\n'));
491
573
  " 2>/dev/null || true)
492
574
  if [ -n "$_bundled_deps" ]; then
493
- echo ""
494
- echo "检测到 ${_before} 个包(超过阈值 50),清理多余的 peerDep 传递依赖..."
495
575
  echo "$_bundled_deps" | while IFS= read -r _pkg; do
496
576
  rm -rf "$PLUGIN_NM/$_pkg"
497
577
  done
@@ -25,10 +25,8 @@ const require = createRequire(import.meta.url);
25
25
 
26
26
  let PLUGIN_VERSION = getPackageVersion(import.meta.url);
27
27
 
28
- // 获取 openclaw 框架版本(缓存结果,只执行一次)
29
- let _frameworkVersion: string | null = null;
28
+ // 获取 openclaw 框架版本(不缓存,每次实时获取)
30
29
  export function getFrameworkVersion(): string {
31
- if (_frameworkVersion !== null) return _frameworkVersion;
32
30
  try {
33
31
  // 先尝试 PATH 中的 CLI
34
32
  // Windows 上 npm 安装的 CLI 通常是 .cmd wrapper,execFileSync 需要 shell:true 才能执行
@@ -40,8 +38,7 @@ export function getFrameworkVersion(): string {
40
38
  }).trim();
41
39
  // 输出格式: "OpenClaw 2026.3.13 (61d171a)"
42
40
  if (out) {
43
- _frameworkVersion = out;
44
- return _frameworkVersion;
41
+ return out;
45
42
  }
46
43
  } catch {
47
44
  continue;
@@ -52,15 +49,13 @@ export function getFrameworkVersion(): string {
52
49
  if (cliPath) {
53
50
  const out = execCliSync(cliPath, ["--version"]);
54
51
  if (out) {
55
- _frameworkVersion = out;
56
- return _frameworkVersion;
52
+ return out;
57
53
  }
58
54
  }
59
55
  } catch {
60
56
  // fallback
61
57
  }
62
- _frameworkVersion = "unknown";
63
- return _frameworkVersion;
58
+ return "unknown";
64
59
  }
65
60
 
66
61
  // ============ 热更新兼容性检查 ============
@@ -162,7 +157,7 @@ function checkUpgradeCompatibility(): UpgradeCompatResult {
162
157
  // 3. 检查 Node.js 版本
163
158
  const nodeVer = process.version.replace(/^v/, "");
164
159
  if (compareSemver(nodeVer, req.minNodeVersion) < 0) {
165
- errors.push(`❌ NoVBNde.js 版本过低:当前 **v${nodeVer}**,热更新要求最低 **v${req.minNodeVersion}**`);
160
+ errors.push(`❌ Node.js 版本过低:当前 **v${nodeVer}**,热更新要求最低 **v${req.minNodeVersion}**`);
166
161
  }
167
162
 
168
163
  // 4. 检查系统架构(arm 等特殊架构提示)
@@ -792,16 +787,137 @@ function fireHotUpgrade(targetVersion?: string, pkg?: string, useLocal?: boolean
792
787
 
793
788
  console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}, pkg=${pkg || "default"}`);
794
789
 
790
+ // ── 兼容 openclaw 3.23+ 配置严格校验 ──
791
+ // openclaw plugins install/update 启动时会校验整个配置文件,
792
+ // 如果 channels.qqbot 已存在但 qqbot 插件尚未加载,校验会报 "unknown channel id: qqbot"。
793
+ //
794
+ // ⚠️ 关键:绝不能直接修改真实的 openclaw.json!
795
+ // gateway 的 config file watcher 会检测到变更并触发 SIGUSR1 重启,
796
+ // 导致当前进程被杀、execFile 回调(restoreConfigAndCleanup)永远不会执行,
797
+ // channels.qqbot 配置就此丢失。
798
+ //
799
+ // 策略:创建临时配置副本(不含 channels.qqbot),通过 OPENCLAW_CONFIG_PATH
800
+ // 环境变量传递给子进程,真实配置文件不受影响。
801
+ // shell 脚本(upgrade-via-npm.sh)内部也有同样的临时配置机制作为双保险。
802
+ const homeDir = getHomeDir();
803
+ const realConfigPath = path.join(homeDir, ".openclaw", "openclaw.json");
804
+ let tempConfigPath: string | null = null;
805
+ const childEnv: NodeJS.ProcessEnv = { ...process.env };
806
+
807
+ try {
808
+ if (fs.existsSync(realConfigPath)) {
809
+ const cfg = JSON.parse(fs.readFileSync(realConfigPath, "utf8"));
810
+ const needsTempConfig =
811
+ !!(cfg.channels?.qqbot) ||
812
+ !!(cfg.plugins?.entries?.["openclaw-qqbot"]);
813
+
814
+ if (needsTempConfig) {
815
+ // 创建临时配置副本(移除 channels.qqbot 和 plugins.entries.openclaw-qqbot)
816
+ const cleanCfg = JSON.parse(JSON.stringify(cfg)); // deep clone
817
+ if (cleanCfg.channels?.qqbot) {
818
+ delete cleanCfg.channels.qqbot;
819
+ if (Object.keys(cleanCfg.channels).length === 0) delete cleanCfg.channels;
820
+ }
821
+ if (cleanCfg.plugins?.entries?.["openclaw-qqbot"]) {
822
+ delete cleanCfg.plugins.entries["openclaw-qqbot"];
823
+ if (cleanCfg.plugins.entries && Object.keys(cleanCfg.plugins.entries).length === 0) delete cleanCfg.plugins.entries;
824
+ }
825
+
826
+ const tmpDir = path.join(homeDir, ".openclaw", ".qqbot-upgrade-tmp");
827
+ fs.mkdirSync(tmpDir, { recursive: true });
828
+ tempConfigPath = path.join(tmpDir, "openclaw-tmp.json");
829
+ fs.writeFileSync(tempConfigPath, JSON.stringify(cleanCfg, null, 4) + "\n");
830
+ childEnv.OPENCLAW_CONFIG_PATH = tempConfigPath;
831
+ console.log(`[qqbot] fireHotUpgrade: created temp config without channels.qqbot (OPENCLAW_CONFIG_PATH=${tempConfigPath}), real config untouched`);
832
+ }
833
+ }
834
+ } catch (e: any) {
835
+ console.warn(`[qqbot] fireHotUpgrade: failed to create temp config: ${e.message}, proceeding with original`);
836
+ tempConfigPath = null;
837
+ }
838
+
839
+ /**
840
+ * 将 openclaw plugins install 写入临时配置的 installs/entries 记录同步回真实配置,
841
+ * 然后清理临时文件。
842
+ *
843
+ * 注意:真实配置中的 channels.qqbot 从未被移除,无需恢复。
844
+ */
845
+ function syncTempConfigAndCleanup(): void {
846
+ try {
847
+ if (tempConfigPath && fs.existsSync(tempConfigPath) && fs.existsSync(realConfigPath)) {
848
+ const tmp = JSON.parse(fs.readFileSync(tempConfigPath, "utf8"));
849
+ const real = JSON.parse(fs.readFileSync(realConfigPath, "utf8"));
850
+ let changed = false;
851
+
852
+ // 同步 plugins.installs(openclaw plugins install 会写入安装记录)
853
+ if (tmp.plugins?.installs) {
854
+ if (!real.plugins) real.plugins = {};
855
+ real.plugins.installs = { ...(real.plugins.installs || {}), ...tmp.plugins.installs };
856
+ changed = true;
857
+ }
858
+ // 同步 plugins.entries(openclaw plugins install 会写入 entries)
859
+ // 注意:不同步 openclaw-qqbot 自身的 entry,因为插件通过 auto-discover 加载,
860
+ // 显式写入 entries 会导致 "duplicate plugin id" 警告刷屏。
861
+ if (tmp.plugins?.entries) {
862
+ if (!real.plugins) real.plugins = {};
863
+ if (!real.plugins.entries) real.plugins.entries = {};
864
+ for (const [k, v] of Object.entries(tmp.plugins.entries)) {
865
+ if (k === "openclaw-qqbot") continue; // 跳过自身,避免 duplicate
866
+ if (!real.plugins.entries[k]) {
867
+ real.plugins.entries[k] = v;
868
+ changed = true;
869
+ }
870
+ }
871
+ }
872
+
873
+ if (changed) {
874
+ fs.writeFileSync(realConfigPath, JSON.stringify(real, null, 4) + "\n");
875
+ console.log("[qqbot] fireHotUpgrade: synced install/entries records from temp config to real config");
876
+ }
877
+ }
878
+ } catch (e: any) {
879
+ console.warn(`[qqbot] fireHotUpgrade: failed to sync temp config: ${e.message}`);
880
+ }
881
+ // 清理临时文件
882
+ try { if (tempConfigPath) fs.unlinkSync(tempConfigPath); } catch { /* ignore */ }
883
+ }
884
+
795
885
  // 异步执行升级脚本
796
- execFile(shell, shellArgs, {
797
- timeout: 120_000,
798
- env: { ...process.env },
886
+ // 必须显式设置 cwd 为一个确定存在的目录(如 homeDir),
887
+ // 否则子进程继承 gateway 的 cwd,如果该目录在升级过程中被删除/移动,
888
+ // openclaw CLI 启动时 process.cwd() 会报 ENOENT: uv_cwd 错误。
889
+ // 超时设为 5 分钟:openclaw plugins install 需要下载 npm 包,
890
+ // 网络慢时(如国内访问 npm registry)可能需要 2-3 分钟。
891
+ // 120 秒超时会导致脚本被杀但 openclaw CLI 子进程继续运行,
892
+ // 同时 bash 的 cleanup_on_exit 回滚了备份目录,造成 "plugin already exists" 错误。
893
+ const child = execFile(shell, shellArgs, {
894
+ timeout: 300_000,
895
+ cwd: homeDir,
896
+ env: childEnv,
897
+ killSignal: "SIGTERM",
799
898
  ...(isWindows() ? { windowsHide: true } : {}),
800
899
  }, (error, stdout, _stderr) => {
801
900
  if (error) {
802
901
  console.error(`[qqbot] fireHotUpgrade: script failed: ${error.message}`);
803
902
  if (stdout) console.error(`[qqbot] fireHotUpgrade: stdout: ${stdout.slice(0, 2000)}`);
804
903
  if (_stderr) console.error(`[qqbot] fireHotUpgrade: stderr: ${_stderr.slice(0, 2000)}`);
904
+
905
+ // 超时时确保子进程树被清理,防止 openclaw plugins install 继续运行
906
+ // 与 cleanup_on_exit 的回滚逻辑冲突(回滚恢复了旧目录,install 又尝试写入)
907
+ if ((error as any).killed || error.message.includes("TIMEOUT")) {
908
+ try {
909
+ // 尝试杀掉子进程树(SIGKILL 确保立即终止)
910
+ child.kill("SIGKILL");
911
+ // 额外尝试通过 pkill 杀掉可能残留的 openclaw plugins install 子进程
912
+ if (!isWindows()) {
913
+ try { execFileSync("pkill", ["-9", "-f", "openclaw.*plugins.*install"], { timeout: 3000, stdio: "pipe" }); } catch { /* ignore */ }
914
+ }
915
+ } catch {
916
+ // 进程可能已退出
917
+ }
918
+ }
919
+
920
+ syncTempConfigAndCleanup();
805
921
  cleanupTempScript();
806
922
  _upgrading = false;
807
923
  return;
@@ -814,6 +930,7 @@ function fireHotUpgrade(targetVersion?: string, pkg?: string, useLocal?: boolean
814
930
  const newVersion = versionMatch?.[1];
815
931
  if (newVersion === "unknown") {
816
932
  console.error(`[qqbot] fireHotUpgrade: script output QQBOT_NEW_VERSION=unknown, aborting restart`);
933
+ syncTempConfigAndCleanup();
817
934
  cleanupTempScript();
818
935
  _upgrading = false;
819
936
  return;
@@ -821,7 +938,8 @@ function fireHotUpgrade(targetVersion?: string, pkg?: string, useLocal?: boolean
821
938
 
822
939
  console.log(`[qqbot] fireHotUpgrade: new version=${newVersion || "(not detected)"}, triggering restart...`);
823
940
 
824
- // 脚本执行成功,清理临时脚本副本
941
+ // 脚本执行成功,同步临时配置中的 install 记录并清理
942
+ syncTempConfigAndCleanup();
825
943
  cleanupTempScript();
826
944
 
827
945
  // 文件替换成功,在 restart 之前把 source 从 path 切换为 npm,
@@ -864,17 +982,175 @@ function fireHotUpgrade(targetVersion?: string, pkg?: string, useLocal?: boolean
864
982
  execCliAsync(cli, ["gateway", "restart"], { timeout: 30_000 }, () => {});
865
983
  }
866
984
  } else {
867
- // Mac/Linux: 直接 restart(框架通常以 daemon 模式运行)
868
- execCliAsync(cli, ["gateway", "restart"], { timeout: 30_000 }, (restartErr) => {
869
- if (restartErr) {
870
- console.error(`[qqbot] fireHotUpgrade: restart failed: ${restartErr.message}, trying stop+start fallback`);
871
- execCliAsync(cli, ["gateway", "stop"], { timeout: 10_000 }, () => {
872
- setTimeout(() => {
873
- execCliAsync(cli, ["gateway", "start"], { timeout: 30_000 }, () => {});
874
- }, 1000);
875
- });
985
+ // Mac/Linux: 使用 detached shell 脚本执行 stop+start
986
+ //
987
+ // 兼容 openclaw 2026.3.24+ 配置严格校验:
988
+ // gateway restart openclaw 先校验配置(loadConfig)再加载插件。
989
+ // 如果 channels.qqbot 存在但 qqbot channel type 尚未注册,校验会失败。
990
+ // 解决:stop 后临时移除 channels.qqbot → start(插件加载、qqbot type 注册)→ 恢复。
991
+ const cliInvoke = cli.endsWith(".mjs")
992
+ ? `"${process.execPath}" "${cli}"`
993
+ : `"${cli}"`;
994
+ const homeDir = getHomeDir();
995
+ const configPath = path.join(homeDir, ".openclaw", "openclaw.json");
996
+ const qqbotChannelBackup = path.join(homeDir, ".openclaw", ".qqbot-channel-backup.json");
997
+ const restartScript = path.join(homeDir, ".openclaw", ".qqbot-restart.sh");
998
+
999
+ // 先保存 channels.qqbot 到临时文件(在当前进程中,JSON 处理更安全)
1000
+ let hasChannel = false;
1001
+ try {
1002
+ const cfgRaw = fs.readFileSync(configPath, "utf8");
1003
+ const cfg = JSON.parse(cfgRaw);
1004
+ const qqbot = cfg?.channels?.qqbot;
1005
+ if (qqbot) {
1006
+ fs.writeFileSync(qqbotChannelBackup, JSON.stringify(qqbot, null, 2), "utf8");
1007
+ hasChannel = true;
876
1008
  }
877
- });
1009
+ } catch {
1010
+ // 配置文件不存在或 JSON 解析失败,不做处理
1011
+ }
1012
+
1013
+ const shContent = `#!/bin/bash
1014
+ # 注意:不使用 set -e,因为 gateway start 失败时仍需恢复 channels.qqbot
1015
+ CLI="${cliInvoke}"
1016
+ CONFIG="${configPath}"
1017
+ BACKUP="${qqbotChannelBackup}"
1018
+
1019
+ # ── 兼容 openclaw 3.23+ 配置严格校验 ──
1020
+ # 所有 openclaw CLI 命令(包括 gateway stop/start)启动时都会 loadConfig 校验配置,
1021
+ # 如果 channels.qqbot 存在但 qqbot 插件尚未加载,校验会报 "unknown channel id: qqbot"。
1022
+ #
1023
+ # 策略:
1024
+ # 1. gateway stop:使用 OPENCLAW_CONFIG_PATH 临时配置(不含 channels.qqbot)
1025
+ # 2. gateway start:先尝试直接启动(真实配置),如果 CLI 校验失败,
1026
+ # 则临时修改真实配置(此时 gateway 已停止,无 config watcher),启动后恢复。
1027
+ # 这样 gateway 进程读取的是完整配置(含 channels.qqbot)。
1028
+
1029
+ # 为 gateway stop 创建临时配置
1030
+ TEMP_RESTART_CONFIG=""
1031
+ if [ -f "$BACKUP" ]; then
1032
+ TEMP_RESTART_CONFIG="\$(mktemp)"
1033
+ node -e "
1034
+ const fs = require('fs');
1035
+ const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
1036
+ if (cfg.channels && cfg.channels.qqbot) {
1037
+ delete cfg.channels.qqbot;
1038
+ if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
1039
+ }
1040
+ if (cfg.plugins && cfg.plugins.entries && cfg.plugins.entries['openclaw-qqbot']) {
1041
+ delete cfg.plugins.entries['openclaw-qqbot'];
1042
+ if (Object.keys(cfg.plugins.entries).length === 0) delete cfg.plugins.entries;
1043
+ }
1044
+ fs.writeFileSync(process.argv[2], JSON.stringify(cfg, null, 4) + '\\n');
1045
+ " "$CONFIG" "$TEMP_RESTART_CONFIG" 2>/dev/null
1046
+ if [ \$? -ne 0 ] || [ ! -s "$TEMP_RESTART_CONFIG" ]; then
1047
+ echo "[qqbot-upgrade] WARNING: failed to create temp config"
1048
+ TEMP_RESTART_CONFIG=""
1049
+ fi
1050
+ fi
1051
+
1052
+ echo "[qqbot-upgrade] Stopping gateway..."
1053
+ if [ -n "$TEMP_RESTART_CONFIG" ]; then
1054
+ OPENCLAW_CONFIG_PATH="$TEMP_RESTART_CONFIG" $CLI gateway stop 2>/dev/null || true
1055
+ else
1056
+ $CLI gateway stop 2>/dev/null || true
1057
+ fi
1058
+ sleep 2
1059
+
1060
+ # 清理临时配置(不再需要)
1061
+ if [ -n "$TEMP_RESTART_CONFIG" ] && [ -f "$TEMP_RESTART_CONFIG" ]; then
1062
+ rm -f "$TEMP_RESTART_CONFIG"
1063
+ fi
1064
+
1065
+ echo "[qqbot-upgrade] Starting gateway..."
1066
+ START_OK=false
1067
+
1068
+ # 先尝试直接启动(使用真实配置,含 channels.qqbot)
1069
+ # 如果 openclaw 版本不做严格校验,或者插件已注册,这会直接成功
1070
+ if $CLI gateway start 2>/dev/null; then
1071
+ START_OK=true
1072
+ echo "[qqbot-upgrade] Gateway started successfully (direct start)"
1073
+ elif [ -f "$BACKUP" ]; then
1074
+ # 直接启动失败(可能是 channels.qqbot 校验失败),
1075
+ # 临时修改真实配置(此时 gateway 已停止,无 config watcher,安全)
1076
+ echo "[qqbot-upgrade] Direct start failed, temporarily removing channels.qqbot from real config..."
1077
+ node -e "
1078
+ const fs = require('fs');
1079
+ const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
1080
+ let changed = false;
1081
+ if (cfg.channels && cfg.channels.qqbot) {
1082
+ delete cfg.channels.qqbot;
1083
+ if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
1084
+ changed = true;
1085
+ }
1086
+ if (cfg.plugins && cfg.plugins.entries && cfg.plugins.entries['openclaw-qqbot']) {
1087
+ delete cfg.plugins.entries['openclaw-qqbot'];
1088
+ if (Object.keys(cfg.plugins.entries).length === 0) delete cfg.plugins.entries;
1089
+ changed = true;
1090
+ }
1091
+ if (changed) {
1092
+ fs.writeFileSync(process.argv[1], JSON.stringify(cfg, null, 4) + '\\n');
1093
+ }
1094
+ " "$CONFIG" 2>/dev/null
1095
+
1096
+ if $CLI gateway start 2>/dev/null; then
1097
+ START_OK=true
1098
+ echo "[qqbot-upgrade] Gateway started successfully (after config fix)"
1099
+ else
1100
+ echo "[qqbot-upgrade] WARNING: gateway start still failed after config fix"
1101
+ fi
1102
+
1103
+ # 等待 gateway 进程启动并加载插件(插件注册 qqbot channel type)
1104
+ echo "[qqbot-upgrade] Waiting for plugin to load (8s)..."
1105
+ sleep 8
1106
+
1107
+ # 恢复 channels.qqbot 到真实配置
1108
+ # gateway 的 config file watcher 会检测到变更并热加载
1109
+ echo "[qqbot-upgrade] Restoring channels.qqbot to real config..."
1110
+ node -e "
1111
+ const fs = require('fs');
1112
+ const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
1113
+ const qqbot = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
1114
+ if (!cfg.channels) cfg.channels = {};
1115
+ cfg.channels.qqbot = qqbot;
1116
+ // 注意:不写入 plugins.entries.openclaw-qqbot,
1117
+ // 插件通过 auto-discover 加载,显式 entry 会导致 duplicate plugin id 警告。
1118
+ fs.writeFileSync(process.argv[1], JSON.stringify(cfg, null, 4) + '\\n');
1119
+ " "$CONFIG" "$BACKUP" 2>/dev/null
1120
+ rm -f "$BACKUP"
1121
+ echo "[qqbot-upgrade] channels.qqbot restored"
1122
+ else
1123
+ echo "[qqbot-upgrade] WARNING: gateway start failed, no backup to restore"
1124
+ fi
1125
+
1126
+ # 直接启动成功的情况下,清理备份文件
1127
+ if [ "$START_OK" = "true" ] && [ -f "$BACKUP" ]; then
1128
+ rm -f "$BACKUP"
1129
+ fi
1130
+
1131
+ # 如果 start 失败,尝试再次启动
1132
+ if [ "$START_OK" != "true" ]; then
1133
+ echo "[qqbot-upgrade] Retrying gateway start..."
1134
+ sleep 2
1135
+ $CLI gateway start 2>/dev/null || echo "[qqbot-upgrade] WARNING: retry also failed"
1136
+ fi
1137
+
1138
+ # 清理自身
1139
+ rm -f "$0"
1140
+ echo "[qqbot-upgrade] Done."
1141
+ `;
1142
+ try {
1143
+ fs.writeFileSync(restartScript, shContent, { mode: 0o755 });
1144
+ const child = spawn("bash", [restartScript], {
1145
+ detached: true,
1146
+ stdio: "ignore",
1147
+ });
1148
+ child.unref();
1149
+ console.log(`[qqbot] fireHotUpgrade: launched detached restart script (pid=${child.pid}), hasChannel=${hasChannel}`);
1150
+ } catch (shErr: any) {
1151
+ console.error(`[qqbot] fireHotUpgrade: failed to launch restart script: ${shErr.message}, falling back to direct restart`);
1152
+ execCliAsync(cli, ["gateway", "restart"], { timeout: 30_000 }, () => {});
1153
+ }
878
1154
  }
879
1155
  });
880
1156
 
@@ -8,10 +8,22 @@ import { createRequire } from "node:module";
8
8
  import path from "node:path";
9
9
  import fs from "node:fs";
10
10
 
11
- let _cached: string | null = null;
11
+ /** 已定位到的 package.json 路径,避免重复遍历目录树 */
12
+ let _resolvedPkgPath: string | null = null;
12
13
 
13
14
  export function getPackageVersion(metaUrl?: string): string {
14
- if (_cached !== null) return _cached;
15
+ // 如果之前已定位到 package.json 路径,直接重新读取(快速路径)
16
+ if (_resolvedPkgPath) {
17
+ try {
18
+ const pkg = JSON.parse(fs.readFileSync(_resolvedPkgPath, "utf8"));
19
+ if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
20
+ return pkg.version as string;
21
+ }
22
+ } catch {
23
+ // 文件可能已被删除(升级过程中),清除路径缓存,走完整查找
24
+ _resolvedPkgPath = null;
25
+ }
26
+ }
15
27
 
16
28
  // Strategy 1: 从调用者的 import.meta.url(或本模块)向上遍历找 package.json
17
29
  const startFile = metaUrl ? fileURLToPath(metaUrl) : fileURLToPath(import.meta.url);
@@ -25,8 +37,8 @@ export function getPackageVersion(metaUrl?: string): string {
25
37
  const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
26
38
  // 确认是我们自己的包(避免找到其他 package.json)
27
39
  if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
28
- _cached = pkg.version as string;
29
- return _cached;
40
+ _resolvedPkgPath = candidate;
41
+ return pkg.version as string;
30
42
  }
31
43
  }
32
44
  } catch {
@@ -42,13 +54,11 @@ export function getPackageVersion(metaUrl?: string): string {
42
54
  try {
43
55
  const pkg = require(rel);
44
56
  if (pkg?.version) {
45
- _cached = pkg.version as string;
46
- return _cached;
57
+ return pkg.version as string;
47
58
  }
48
59
  } catch { /* next */ }
49
60
  }
50
61
  } catch { /* fallback */ }
51
62
 
52
- _cached = "unknown";
53
- return _cached;
63
+ return "unknown";
54
64
  }