@ryantest/openclaw-qqbot 1.6.7-beta.15 → 1.6.7-beta.17

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.
@@ -694,131 +694,95 @@ function fireHotUpgrade(targetVersion, pkg, useLocal) {
694
694
  // openclaw plugins install/update 启动时会校验整个配置文件,
695
695
  // 如果 channels.qqbot 已存在但 qqbot 插件尚未加载,校验会报 "unknown channel id: qqbot"。
696
696
  //
697
- // 策略(双保险):
698
- // 1. 直接从真实 openclaw.json 中临时移除 channels.qqbot / plugins.entries.openclaw-qqbot,
699
- // 暂存到备份文件,脚本完成后恢复。这是最可靠的方式,因为 openclaw CLI 一定读取真实配置。
700
- // 移除后的配置是合法的,gateway 的 config file watcher 重新加载不会失败。
701
- // 2. 同时创建临时配置副本并通过 OPENCLAW_CONFIG_PATH 环境变量传递给子进程,
702
- // 作为额外保险(防止某些 openclaw 版本在 install 过程中多次加载配置)。
697
+ // ⚠️ 关键:绝不能直接修改真实的 openclaw.json!
698
+ // gateway config file watcher 会检测到变更并触发 SIGUSR1 重启,
699
+ // 导致当前进程被杀、execFile 回调(restoreConfigAndCleanup)永远不会执行,
700
+ // channels.qqbot 配置就此丢失。
701
+ //
702
+ // 策略:创建临时配置副本(不含 channels.qqbot),通过 OPENCLAW_CONFIG_PATH
703
+ // 环境变量传递给子进程,真实配置文件不受影响。
704
+ // shell 脚本(upgrade-via-npm.sh)内部也有同样的临时配置机制作为双保险。
703
705
  const homeDir = getHomeDir();
704
706
  const realConfigPath = path.join(homeDir, ".openclaw", "openclaw.json");
705
- const channelBackupPath = path.join(homeDir, ".openclaw", ".qqbot-upgrade-channel-stash.json");
706
- let stashedChannelConfig = null;
707
- let stashedPluginEntry = null;
708
- let configModified = false;
709
707
  let tempConfigPath = null;
710
708
  const childEnv = { ...process.env };
711
709
  try {
712
710
  if (fs.existsSync(realConfigPath)) {
713
711
  const cfg = JSON.parse(fs.readFileSync(realConfigPath, "utf8"));
714
- let needsModify = false;
715
- if (cfg.channels?.qqbot)
716
- needsModify = true;
717
- if (cfg.plugins?.entries?.["openclaw-qqbot"])
718
- needsModify = true;
719
- if (needsModify) {
720
- // 暂存需要移除的配置项
721
- const stash = {};
722
- if (cfg.channels?.qqbot) {
723
- stash.channelsQqbot = cfg.channels.qqbot;
724
- stashedChannelConfig = cfg.channels.qqbot;
725
- }
726
- if (cfg.plugins?.entries?.["openclaw-qqbot"]) {
727
- stash.pluginsEntry = cfg.plugins.entries["openclaw-qqbot"];
728
- stashedPluginEntry = cfg.plugins.entries["openclaw-qqbot"];
712
+ const needsTempConfig = !!(cfg.channels?.qqbot) ||
713
+ !!(cfg.plugins?.entries?.["openclaw-qqbot"]);
714
+ if (needsTempConfig) {
715
+ // 创建临时配置副本(移除 channels.qqbot 和 plugins.entries.openclaw-qqbot
716
+ const cleanCfg = JSON.parse(JSON.stringify(cfg)); // deep clone
717
+ if (cleanCfg.channels?.qqbot) {
718
+ delete cleanCfg.channels.qqbot;
719
+ if (Object.keys(cleanCfg.channels).length === 0)
720
+ delete cleanCfg.channels;
729
721
  }
730
- // 写入备份文件(防止进程异常退出时丢失暂存数据)
731
- fs.writeFileSync(channelBackupPath, JSON.stringify(stash, null, 2), "utf8");
732
- // 从真实配置中移除会导致校验失败的项
733
- if (cfg.channels?.qqbot) {
734
- delete cfg.channels.qqbot;
735
- if (Object.keys(cfg.channels).length === 0)
736
- delete cfg.channels;
722
+ if (cleanCfg.plugins?.entries?.["openclaw-qqbot"]) {
723
+ delete cleanCfg.plugins.entries["openclaw-qqbot"];
724
+ if (cleanCfg.plugins.entries && Object.keys(cleanCfg.plugins.entries).length === 0)
725
+ delete cleanCfg.plugins.entries;
737
726
  }
738
- if (cfg.plugins?.entries?.["openclaw-qqbot"]) {
739
- delete cfg.plugins.entries["openclaw-qqbot"];
740
- if (Object.keys(cfg.plugins.entries).length === 0)
741
- delete cfg.plugins.entries;
742
- }
743
- fs.writeFileSync(realConfigPath, JSON.stringify(cfg, null, 4) + "\n");
744
- configModified = true;
745
- console.log(`[qqbot] fireHotUpgrade: temporarily removed channels.qqbot & plugins.entries from real config (stash=${channelBackupPath})`);
746
- // 双保险:同时创建临时配置副本并通过 OPENCLAW_CONFIG_PATH 传递
747
- try {
748
- const tmpDir = path.join(homeDir, ".openclaw", ".qqbot-upgrade-tmp");
749
- fs.mkdirSync(tmpDir, { recursive: true });
750
- tempConfigPath = path.join(tmpDir, "openclaw-tmp.json");
751
- fs.writeFileSync(tempConfigPath, JSON.stringify(cfg, null, 4) + "\n");
752
- childEnv.OPENCLAW_CONFIG_PATH = tempConfigPath;
753
- }
754
- catch { /* 非关键,忽略 */ }
727
+ const tmpDir = path.join(homeDir, ".openclaw", ".qqbot-upgrade-tmp");
728
+ fs.mkdirSync(tmpDir, { recursive: true });
729
+ tempConfigPath = path.join(tmpDir, "openclaw-tmp.json");
730
+ fs.writeFileSync(tempConfigPath, JSON.stringify(cleanCfg, null, 4) + "\n");
731
+ childEnv.OPENCLAW_CONFIG_PATH = tempConfigPath;
732
+ console.log(`[qqbot] fireHotUpgrade: created temp config without channels.qqbot (OPENCLAW_CONFIG_PATH=${tempConfigPath}), real config untouched`);
755
733
  }
756
734
  }
757
735
  }
758
736
  catch (e) {
759
- console.warn(`[qqbot] fireHotUpgrade: failed to modify config for upgrade: ${e.message}, proceeding with original`);
760
- configModified = false;
761
- stashedChannelConfig = null;
762
- stashedPluginEntry = null;
737
+ console.warn(`[qqbot] fireHotUpgrade: failed to create temp config: ${e.message}, proceeding with original`);
738
+ tempConfigPath = null;
763
739
  }
764
740
  /**
765
- * 恢复暂存的 channels.qqbot plugins.entries 到真实配置,
766
- * 同时合并 openclaw plugins install 写入的 installs/entries 记录。
741
+ * openclaw plugins install 写入临时配置的 installs/entries 记录同步回真实配置,
742
+ * 然后清理临时文件。
743
+ *
744
+ * 注意:真实配置中的 channels.qqbot 从未被移除,无需恢复。
767
745
  */
768
- function restoreConfigAndCleanup() {
746
+ function syncTempConfigAndCleanup() {
769
747
  try {
770
- // 从备份文件恢复暂存数据(优先使用文件,防止内存中的数据因异常丢失)
771
- let stash = {};
772
- if (fs.existsSync(channelBackupPath)) {
773
- try {
774
- stash = JSON.parse(fs.readFileSync(channelBackupPath, "utf8"));
775
- }
776
- catch { /* 解析失败,使用内存中的数据 */ }
777
- }
778
- const channelCfg = stash.channelsQqbot ?? stashedChannelConfig;
779
- const entryCfg = stash.pluginsEntry ?? stashedPluginEntry;
780
- if (configModified && fs.existsSync(realConfigPath)) {
781
- const cfg = JSON.parse(fs.readFileSync(realConfigPath, "utf8"));
782
- // 恢复 channels.qqbot
783
- if (channelCfg) {
784
- if (!cfg.channels)
785
- cfg.channels = {};
786
- cfg.channels.qqbot = channelCfg;
787
- }
788
- // 恢复 plugins.entries.openclaw-qqbot(仅当 openclaw plugins install 没有写入新 entry 时)
789
- if (entryCfg) {
790
- if (!cfg.plugins)
791
- cfg.plugins = {};
792
- if (!cfg.plugins.entries)
793
- cfg.plugins.entries = {};
794
- if (!cfg.plugins.entries["openclaw-qqbot"]) {
795
- cfg.plugins.entries["openclaw-qqbot"] = entryCfg;
796
- }
748
+ if (tempConfigPath && fs.existsSync(tempConfigPath) && fs.existsSync(realConfigPath)) {
749
+ const tmp = JSON.parse(fs.readFileSync(tempConfigPath, "utf8"));
750
+ const real = JSON.parse(fs.readFileSync(realConfigPath, "utf8"));
751
+ let changed = false;
752
+ // 同步 plugins.installs(openclaw plugins install 会写入安装记录)
753
+ if (tmp.plugins?.installs) {
754
+ if (!real.plugins)
755
+ real.plugins = {};
756
+ real.plugins.installs = { ...(real.plugins.installs || {}), ...tmp.plugins.installs };
757
+ changed = true;
797
758
  }
798
- // 如果临时配置中有 plugins install 写入的新记录,也合并过来
799
- if (tempConfigPath && fs.existsSync(tempConfigPath)) {
800
- try {
801
- const tmp = JSON.parse(fs.readFileSync(tempConfigPath, "utf8"));
802
- if (tmp.plugins?.installs) {
803
- if (!cfg.plugins)
804
- cfg.plugins = {};
805
- cfg.plugins.installs = { ...(cfg.plugins.installs || {}), ...tmp.plugins.installs };
759
+ // 同步 plugins.entries(openclaw plugins install 会写入 entries)
760
+ // 注意:不同步 openclaw-qqbot 自身的 entry,因为插件通过 auto-discover 加载,
761
+ // 显式写入 entries 会导致 "duplicate plugin id" 警告刷屏。
762
+ if (tmp.plugins?.entries) {
763
+ if (!real.plugins)
764
+ real.plugins = {};
765
+ if (!real.plugins.entries)
766
+ real.plugins.entries = {};
767
+ for (const [k, v] of Object.entries(tmp.plugins.entries)) {
768
+ if (k === "openclaw-qqbot")
769
+ continue; // 跳过自身,避免 duplicate
770
+ if (!real.plugins.entries[k]) {
771
+ real.plugins.entries[k] = v;
772
+ changed = true;
806
773
  }
807
774
  }
808
- catch { /* 忽略 */ }
809
775
  }
810
- fs.writeFileSync(realConfigPath, JSON.stringify(cfg, null, 4) + "\n");
811
- console.log("[qqbot] fireHotUpgrade: restored channels.qqbot & plugins.entries to real config");
776
+ if (changed) {
777
+ fs.writeFileSync(realConfigPath, JSON.stringify(real, null, 4) + "\n");
778
+ console.log("[qqbot] fireHotUpgrade: synced install/entries records from temp config to real config");
779
+ }
812
780
  }
813
781
  }
814
782
  catch (e) {
815
- console.warn(`[qqbot] fireHotUpgrade: failed to restore config: ${e.message}`);
783
+ console.warn(`[qqbot] fireHotUpgrade: failed to sync temp config: ${e.message}`);
816
784
  }
817
- // 清理备份和临时文件
818
- try {
819
- fs.unlinkSync(channelBackupPath);
820
- }
821
- catch { /* ignore */ }
785
+ // 清理临时文件
822
786
  try {
823
787
  if (tempConfigPath)
824
788
  fs.unlinkSync(tempConfigPath);
@@ -841,7 +805,7 @@ function fireHotUpgrade(targetVersion, pkg, useLocal) {
841
805
  console.error(`[qqbot] fireHotUpgrade: stdout: ${stdout.slice(0, 2000)}`);
842
806
  if (_stderr)
843
807
  console.error(`[qqbot] fireHotUpgrade: stderr: ${_stderr.slice(0, 2000)}`);
844
- restoreConfigAndCleanup();
808
+ syncTempConfigAndCleanup();
845
809
  cleanupTempScript();
846
810
  _upgrading = false;
847
811
  return;
@@ -852,14 +816,14 @@ function fireHotUpgrade(targetVersion, pkg, useLocal) {
852
816
  const newVersion = versionMatch?.[1];
853
817
  if (newVersion === "unknown") {
854
818
  console.error(`[qqbot] fireHotUpgrade: script output QQBOT_NEW_VERSION=unknown, aborting restart`);
855
- restoreConfigAndCleanup();
819
+ syncTempConfigAndCleanup();
856
820
  cleanupTempScript();
857
821
  _upgrading = false;
858
822
  return;
859
823
  }
860
824
  console.log(`[qqbot] fireHotUpgrade: new version=${newVersion || "(not detected)"}, triggering restart...`);
861
- // 脚本执行成功,恢复暂存的配置并清理
862
- restoreConfigAndCleanup();
825
+ // 脚本执行成功,同步临时配置中的 install 记录并清理
826
+ syncTempConfigAndCleanup();
863
827
  cleanupTempScript();
864
828
  // 文件替换成功,在 restart 之前把 source 从 path 切换为 npm,
865
829
  // 确保新进程启动时读到的是 npm source,不会被本地源码覆盖。
@@ -935,12 +899,64 @@ CLI="${cliInvoke}"
935
899
  CONFIG="${configPath}"
936
900
  BACKUP="${qqbotChannelBackup}"
937
901
 
902
+ # ── 兼容 openclaw 3.23+ 配置严格校验 ──
903
+ # 所有 openclaw CLI 命令(包括 gateway stop/start)启动时都会 loadConfig 校验配置,
904
+ # 如果 channels.qqbot 存在但 qqbot 插件尚未加载,校验会报 "unknown channel id: qqbot"。
905
+ #
906
+ # 策略:
907
+ # 1. gateway stop:使用 OPENCLAW_CONFIG_PATH 临时配置(不含 channels.qqbot)
908
+ # 2. gateway start:先尝试直接启动(真实配置),如果 CLI 校验失败,
909
+ # 则临时修改真实配置(此时 gateway 已停止,无 config watcher),启动后恢复。
910
+ # 这样 gateway 进程读取的是完整配置(含 channels.qqbot)。
911
+
912
+ # 为 gateway stop 创建临时配置
913
+ TEMP_RESTART_CONFIG=""
914
+ if [ -f "$BACKUP" ]; then
915
+ TEMP_RESTART_CONFIG="\$(mktemp)"
916
+ node -e "
917
+ const fs = require('fs');
918
+ const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
919
+ if (cfg.channels && cfg.channels.qqbot) {
920
+ delete cfg.channels.qqbot;
921
+ if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
922
+ }
923
+ if (cfg.plugins && cfg.plugins.entries && cfg.plugins.entries['openclaw-qqbot']) {
924
+ delete cfg.plugins.entries['openclaw-qqbot'];
925
+ if (Object.keys(cfg.plugins.entries).length === 0) delete cfg.plugins.entries;
926
+ }
927
+ fs.writeFileSync(process.argv[2], JSON.stringify(cfg, null, 4) + '\\n');
928
+ " "$CONFIG" "$TEMP_RESTART_CONFIG" 2>/dev/null
929
+ if [ \$? -ne 0 ] || [ ! -s "$TEMP_RESTART_CONFIG" ]; then
930
+ echo "[qqbot-upgrade] WARNING: failed to create temp config"
931
+ TEMP_RESTART_CONFIG=""
932
+ fi
933
+ fi
934
+
938
935
  echo "[qqbot-upgrade] Stopping gateway..."
939
- $CLI gateway stop 2>/dev/null || true
936
+ if [ -n "$TEMP_RESTART_CONFIG" ]; then
937
+ OPENCLAW_CONFIG_PATH="$TEMP_RESTART_CONFIG" $CLI gateway stop 2>/dev/null || true
938
+ else
939
+ $CLI gateway stop 2>/dev/null || true
940
+ fi
940
941
  sleep 2
941
942
 
942
- if [ -f "$BACKUP" ]; then
943
- echo "[qqbot-upgrade] Temporarily removing channels.qqbot and plugins.entries for config validation bypass..."
943
+ # 清理临时配置(不再需要)
944
+ if [ -n "$TEMP_RESTART_CONFIG" ] && [ -f "$TEMP_RESTART_CONFIG" ]; then
945
+ rm -f "$TEMP_RESTART_CONFIG"
946
+ fi
947
+
948
+ echo "[qqbot-upgrade] Starting gateway..."
949
+ START_OK=false
950
+
951
+ # 先尝试直接启动(使用真实配置,含 channels.qqbot)
952
+ # 如果 openclaw 版本不做严格校验,或者插件已注册,这会直接成功
953
+ if $CLI gateway start 2>/dev/null; then
954
+ START_OK=true
955
+ echo "[qqbot-upgrade] Gateway started successfully (direct start)"
956
+ elif [ -f "$BACKUP" ]; then
957
+ # 直接启动失败(可能是 channels.qqbot 校验失败),
958
+ # 临时修改真实配置(此时 gateway 已停止,无 config watcher,安全)
959
+ echo "[qqbot-upgrade] Direct start failed, temporarily removing channels.qqbot from real config..."
944
960
  node -e "
945
961
  const fs = require('fs');
946
962
  const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
@@ -959,42 +975,44 @@ if [ -f "$BACKUP" ]; then
959
975
  fs.writeFileSync(process.argv[1], JSON.stringify(cfg, null, 4) + '\\n');
960
976
  }
961
977
  " "$CONFIG" 2>/dev/null
962
- echo "[qqbot-upgrade] channels.qqbot temporarily removed"
963
- fi
964
978
 
965
- echo "[qqbot-upgrade] Starting gateway..."
966
- START_OK=false
967
- if $CLI gateway start 2>/dev/null; then
968
- START_OK=true
969
- echo "[qqbot-upgrade] Gateway started successfully"
970
- else
971
- echo "[qqbot-upgrade] WARNING: gateway start failed, will still restore config"
972
- fi
979
+ if $CLI gateway start 2>/dev/null; then
980
+ START_OK=true
981
+ echo "[qqbot-upgrade] Gateway started successfully (after config fix)"
982
+ else
983
+ echo "[qqbot-upgrade] WARNING: gateway start still failed after config fix"
984
+ fi
973
985
 
974
- echo "[qqbot-upgrade] Waiting for plugin to load (8s)..."
975
- sleep 8
986
+ # 等待 gateway 进程启动并加载插件(插件注册 qqbot channel type)
987
+ echo "[qqbot-upgrade] Waiting for plugin to load (8s)..."
988
+ sleep 8
976
989
 
977
- if [ -f "$BACKUP" ]; then
978
- echo "[qqbot-upgrade] Restoring channels.qqbot and plugins.entries..."
990
+ # 恢复 channels.qqbot 到真实配置
991
+ # gateway config file watcher 会检测到变更并热加载
992
+ echo "[qqbot-upgrade] Restoring channels.qqbot to real config..."
979
993
  node -e "
994
+ const fs = require('fs');
980
995
  const fs = require('fs');
981
996
  const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
982
997
  const qqbot = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
983
998
  if (!cfg.channels) cfg.channels = {};
984
999
  cfg.channels.qqbot = qqbot;
985
- // 恢复 plugins.entries
986
- if (!cfg.plugins) cfg.plugins = {};
987
- if (!cfg.plugins.entries) cfg.plugins.entries = {};
988
- if (!cfg.plugins.entries['openclaw-qqbot']) {
989
- cfg.plugins.entries['openclaw-qqbot'] = { enabled: true };
990
- }
1000
+ // 注意:不写入 plugins.entries.openclaw-qqbot,
1001
+ // 插件通过 auto-discover 加载,显式 entry 会导致 duplicate plugin id 警告。
991
1002
  fs.writeFileSync(process.argv[1], JSON.stringify(cfg, null, 4) + '\\n');
992
1003
  " "$CONFIG" "$BACKUP" 2>/dev/null
993
1004
  rm -f "$BACKUP"
994
1005
  echo "[qqbot-upgrade] channels.qqbot restored"
1006
+ else
1007
+ echo "[qqbot-upgrade] WARNING: gateway start failed, no backup to restore"
1008
+ fi
1009
+
1010
+ # 直接启动成功的情况下,清理备份文件
1011
+ if [ "$START_OK" = "true" ] && [ -f "$BACKUP" ]; then
1012
+ rm -f "$BACKUP"
995
1013
  fi
996
1014
 
997
- # 如果 start 失败,尝试再次启动(此时 channels.qqbot 已恢复,插件可能已注册)
1015
+ # 如果 start 失败,尝试再次启动
998
1016
  if [ "$START_OK" != "true" ]; then
999
1017
  echo "[qqbot-upgrade] Retrying gateway start..."
1000
1018
  sleep 2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryantest/openclaw-qqbot",
3
- "version": "1.6.7-beta.15",
3
+ "version": "1.6.7-beta.17",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -796,125 +796,94 @@ function fireHotUpgrade(targetVersion?: string, pkg?: string, useLocal?: boolean
796
796
  // openclaw plugins install/update 启动时会校验整个配置文件,
797
797
  // 如果 channels.qqbot 已存在但 qqbot 插件尚未加载,校验会报 "unknown channel id: qqbot"。
798
798
  //
799
- // 策略(双保险):
800
- // 1. 直接从真实 openclaw.json 中临时移除 channels.qqbot / plugins.entries.openclaw-qqbot,
801
- // 暂存到备份文件,脚本完成后恢复。这是最可靠的方式,因为 openclaw CLI 一定读取真实配置。
802
- // 移除后的配置是合法的,gateway 的 config file watcher 重新加载不会失败。
803
- // 2. 同时创建临时配置副本并通过 OPENCLAW_CONFIG_PATH 环境变量传递给子进程,
804
- // 作为额外保险(防止某些 openclaw 版本在 install 过程中多次加载配置)。
799
+ // ⚠️ 关键:绝不能直接修改真实的 openclaw.json!
800
+ // gateway config file watcher 会检测到变更并触发 SIGUSR1 重启,
801
+ // 导致当前进程被杀、execFile 回调(restoreConfigAndCleanup)永远不会执行,
802
+ // channels.qqbot 配置就此丢失。
803
+ //
804
+ // 策略:创建临时配置副本(不含 channels.qqbot),通过 OPENCLAW_CONFIG_PATH
805
+ // 环境变量传递给子进程,真实配置文件不受影响。
806
+ // shell 脚本(upgrade-via-npm.sh)内部也有同样的临时配置机制作为双保险。
805
807
  const homeDir = getHomeDir();
806
808
  const realConfigPath = path.join(homeDir, ".openclaw", "openclaw.json");
807
- const channelBackupPath = path.join(homeDir, ".openclaw", ".qqbot-upgrade-channel-stash.json");
808
- let stashedChannelConfig: unknown = null;
809
- let stashedPluginEntry: unknown = null;
810
- let configModified = false;
811
809
  let tempConfigPath: string | null = null;
812
810
  const childEnv: NodeJS.ProcessEnv = { ...process.env };
813
811
 
814
812
  try {
815
813
  if (fs.existsSync(realConfigPath)) {
816
814
  const cfg = JSON.parse(fs.readFileSync(realConfigPath, "utf8"));
817
- let needsModify = false;
818
-
819
- if (cfg.channels?.qqbot) needsModify = true;
820
- if (cfg.plugins?.entries?.["openclaw-qqbot"]) needsModify = true;
821
-
822
- if (needsModify) {
823
- // 暂存需要移除的配置项
824
- const stash: Record<string, unknown> = {};
825
- if (cfg.channels?.qqbot) {
826
- stash.channelsQqbot = cfg.channels.qqbot;
827
- stashedChannelConfig = cfg.channels.qqbot;
815
+ const needsTempConfig =
816
+ !!(cfg.channels?.qqbot) ||
817
+ !!(cfg.plugins?.entries?.["openclaw-qqbot"]);
818
+
819
+ if (needsTempConfig) {
820
+ // 创建临时配置副本(移除 channels.qqbot 和 plugins.entries.openclaw-qqbot)
821
+ const cleanCfg = JSON.parse(JSON.stringify(cfg)); // deep clone
822
+ if (cleanCfg.channels?.qqbot) {
823
+ delete cleanCfg.channels.qqbot;
824
+ if (Object.keys(cleanCfg.channels).length === 0) delete cleanCfg.channels;
828
825
  }
829
- if (cfg.plugins?.entries?.["openclaw-qqbot"]) {
830
- stash.pluginsEntry = cfg.plugins.entries["openclaw-qqbot"];
831
- stashedPluginEntry = cfg.plugins.entries["openclaw-qqbot"];
826
+ if (cleanCfg.plugins?.entries?.["openclaw-qqbot"]) {
827
+ delete cleanCfg.plugins.entries["openclaw-qqbot"];
828
+ if (cleanCfg.plugins.entries && Object.keys(cleanCfg.plugins.entries).length === 0) delete cleanCfg.plugins.entries;
832
829
  }
833
- // 写入备份文件(防止进程异常退出时丢失暂存数据)
834
- fs.writeFileSync(channelBackupPath, JSON.stringify(stash, null, 2), "utf8");
835
830
 
836
- // 从真实配置中移除会导致校验失败的项
837
- if (cfg.channels?.qqbot) {
838
- delete cfg.channels.qqbot;
839
- if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
840
- }
841
- if (cfg.plugins?.entries?.["openclaw-qqbot"]) {
842
- delete cfg.plugins.entries["openclaw-qqbot"];
843
- if (Object.keys(cfg.plugins.entries).length === 0) delete cfg.plugins.entries;
844
- }
845
- fs.writeFileSync(realConfigPath, JSON.stringify(cfg, null, 4) + "\n");
846
- configModified = true;
847
- console.log(`[qqbot] fireHotUpgrade: temporarily removed channels.qqbot & plugins.entries from real config (stash=${channelBackupPath})`);
848
-
849
- // 双保险:同时创建临时配置副本并通过 OPENCLAW_CONFIG_PATH 传递
850
- try {
851
- const tmpDir = path.join(homeDir, ".openclaw", ".qqbot-upgrade-tmp");
852
- fs.mkdirSync(tmpDir, { recursive: true });
853
- tempConfigPath = path.join(tmpDir, "openclaw-tmp.json");
854
- fs.writeFileSync(tempConfigPath, JSON.stringify(cfg, null, 4) + "\n");
855
- childEnv.OPENCLAW_CONFIG_PATH = tempConfigPath;
856
- } catch { /* 非关键,忽略 */ }
831
+ const tmpDir = path.join(homeDir, ".openclaw", ".qqbot-upgrade-tmp");
832
+ fs.mkdirSync(tmpDir, { recursive: true });
833
+ tempConfigPath = path.join(tmpDir, "openclaw-tmp.json");
834
+ fs.writeFileSync(tempConfigPath, JSON.stringify(cleanCfg, null, 4) + "\n");
835
+ childEnv.OPENCLAW_CONFIG_PATH = tempConfigPath;
836
+ console.log(`[qqbot] fireHotUpgrade: created temp config without channels.qqbot (OPENCLAW_CONFIG_PATH=${tempConfigPath}), real config untouched`);
857
837
  }
858
838
  }
859
839
  } catch (e: any) {
860
- console.warn(`[qqbot] fireHotUpgrade: failed to modify config for upgrade: ${e.message}, proceeding with original`);
861
- configModified = false;
862
- stashedChannelConfig = null;
863
- stashedPluginEntry = null;
840
+ console.warn(`[qqbot] fireHotUpgrade: failed to create temp config: ${e.message}, proceeding with original`);
841
+ tempConfigPath = null;
864
842
  }
865
843
 
866
844
  /**
867
- * 恢复暂存的 channels.qqbot plugins.entries 到真实配置,
868
- * 同时合并 openclaw plugins install 写入的 installs/entries 记录。
845
+ * openclaw plugins install 写入临时配置的 installs/entries 记录同步回真实配置,
846
+ * 然后清理临时文件。
847
+ *
848
+ * 注意:真实配置中的 channels.qqbot 从未被移除,无需恢复。
869
849
  */
870
- function restoreConfigAndCleanup(): void {
850
+ function syncTempConfigAndCleanup(): void {
871
851
  try {
872
- // 从备份文件恢复暂存数据(优先使用文件,防止内存中的数据因异常丢失)
873
- let stash: Record<string, unknown> = {};
874
- if (fs.existsSync(channelBackupPath)) {
875
- try {
876
- stash = JSON.parse(fs.readFileSync(channelBackupPath, "utf8"));
877
- } catch { /* 解析失败,使用内存中的数据 */ }
878
- }
879
- const channelCfg = stash.channelsQqbot ?? stashedChannelConfig;
880
- const entryCfg = stash.pluginsEntry ?? stashedPluginEntry;
881
-
882
- if (configModified && fs.existsSync(realConfigPath)) {
883
- const cfg = JSON.parse(fs.readFileSync(realConfigPath, "utf8"));
884
-
885
- // 恢复 channels.qqbot
886
- if (channelCfg) {
887
- if (!cfg.channels) cfg.channels = {};
888
- cfg.channels.qqbot = channelCfg;
852
+ if (tempConfigPath && fs.existsSync(tempConfigPath) && fs.existsSync(realConfigPath)) {
853
+ const tmp = JSON.parse(fs.readFileSync(tempConfigPath, "utf8"));
854
+ const real = JSON.parse(fs.readFileSync(realConfigPath, "utf8"));
855
+ let changed = false;
856
+
857
+ // 同步 plugins.installs(openclaw plugins install 会写入安装记录)
858
+ if (tmp.plugins?.installs) {
859
+ if (!real.plugins) real.plugins = {};
860
+ real.plugins.installs = { ...(real.plugins.installs || {}), ...tmp.plugins.installs };
861
+ changed = true;
889
862
  }
890
- // 恢复 plugins.entries.openclaw-qqbot(仅当 openclaw plugins install 没有写入新 entry 时)
891
- if (entryCfg) {
892
- if (!cfg.plugins) cfg.plugins = {};
893
- if (!cfg.plugins.entries) cfg.plugins.entries = {};
894
- if (!cfg.plugins.entries["openclaw-qqbot"]) {
895
- cfg.plugins.entries["openclaw-qqbot"] = entryCfg;
863
+ // 同步 plugins.entriesopenclaw plugins install 会写入 entries)
864
+ // 注意:不同步 openclaw-qqbot 自身的 entry,因为插件通过 auto-discover 加载,
865
+ // 显式写入 entries 会导致 "duplicate plugin id" 警告刷屏。
866
+ if (tmp.plugins?.entries) {
867
+ if (!real.plugins) real.plugins = {};
868
+ if (!real.plugins.entries) real.plugins.entries = {};
869
+ for (const [k, v] of Object.entries(tmp.plugins.entries)) {
870
+ if (k === "openclaw-qqbot") continue; // 跳过自身,避免 duplicate
871
+ if (!real.plugins.entries[k]) {
872
+ real.plugins.entries[k] = v;
873
+ changed = true;
874
+ }
896
875
  }
897
876
  }
898
877
 
899
- // 如果临时配置中有 plugins install 写入的新记录,也合并过来
900
- if (tempConfigPath && fs.existsSync(tempConfigPath)) {
901
- try {
902
- const tmp = JSON.parse(fs.readFileSync(tempConfigPath, "utf8"));
903
- if (tmp.plugins?.installs) {
904
- if (!cfg.plugins) cfg.plugins = {};
905
- cfg.plugins.installs = { ...(cfg.plugins.installs || {}), ...tmp.plugins.installs };
906
- }
907
- } catch { /* 忽略 */ }
878
+ if (changed) {
879
+ fs.writeFileSync(realConfigPath, JSON.stringify(real, null, 4) + "\n");
880
+ console.log("[qqbot] fireHotUpgrade: synced install/entries records from temp config to real config");
908
881
  }
909
-
910
- fs.writeFileSync(realConfigPath, JSON.stringify(cfg, null, 4) + "\n");
911
- console.log("[qqbot] fireHotUpgrade: restored channels.qqbot & plugins.entries to real config");
912
882
  }
913
883
  } catch (e: any) {
914
- console.warn(`[qqbot] fireHotUpgrade: failed to restore config: ${e.message}`);
884
+ console.warn(`[qqbot] fireHotUpgrade: failed to sync temp config: ${e.message}`);
915
885
  }
916
- // 清理备份和临时文件
917
- try { fs.unlinkSync(channelBackupPath); } catch { /* ignore */ }
886
+ // 清理临时文件
918
887
  try { if (tempConfigPath) fs.unlinkSync(tempConfigPath); } catch { /* ignore */ }
919
888
  }
920
889
 
@@ -932,7 +901,7 @@ function fireHotUpgrade(targetVersion?: string, pkg?: string, useLocal?: boolean
932
901
  console.error(`[qqbot] fireHotUpgrade: script failed: ${error.message}`);
933
902
  if (stdout) console.error(`[qqbot] fireHotUpgrade: stdout: ${stdout.slice(0, 2000)}`);
934
903
  if (_stderr) console.error(`[qqbot] fireHotUpgrade: stderr: ${_stderr.slice(0, 2000)}`);
935
- restoreConfigAndCleanup();
904
+ syncTempConfigAndCleanup();
936
905
  cleanupTempScript();
937
906
  _upgrading = false;
938
907
  return;
@@ -945,7 +914,7 @@ function fireHotUpgrade(targetVersion?: string, pkg?: string, useLocal?: boolean
945
914
  const newVersion = versionMatch?.[1];
946
915
  if (newVersion === "unknown") {
947
916
  console.error(`[qqbot] fireHotUpgrade: script output QQBOT_NEW_VERSION=unknown, aborting restart`);
948
- restoreConfigAndCleanup();
917
+ syncTempConfigAndCleanup();
949
918
  cleanupTempScript();
950
919
  _upgrading = false;
951
920
  return;
@@ -953,8 +922,8 @@ function fireHotUpgrade(targetVersion?: string, pkg?: string, useLocal?: boolean
953
922
 
954
923
  console.log(`[qqbot] fireHotUpgrade: new version=${newVersion || "(not detected)"}, triggering restart...`);
955
924
 
956
- // 脚本执行成功,恢复暂存的配置并清理
957
- restoreConfigAndCleanup();
925
+ // 脚本执行成功,同步临时配置中的 install 记录并清理
926
+ syncTempConfigAndCleanup();
958
927
  cleanupTempScript();
959
928
 
960
929
  // 文件替换成功,在 restart 之前把 source 从 path 切换为 npm,
@@ -1031,12 +1000,64 @@ CLI="${cliInvoke}"
1031
1000
  CONFIG="${configPath}"
1032
1001
  BACKUP="${qqbotChannelBackup}"
1033
1002
 
1003
+ # ── 兼容 openclaw 3.23+ 配置严格校验 ──
1004
+ # 所有 openclaw CLI 命令(包括 gateway stop/start)启动时都会 loadConfig 校验配置,
1005
+ # 如果 channels.qqbot 存在但 qqbot 插件尚未加载,校验会报 "unknown channel id: qqbot"。
1006
+ #
1007
+ # 策略:
1008
+ # 1. gateway stop:使用 OPENCLAW_CONFIG_PATH 临时配置(不含 channels.qqbot)
1009
+ # 2. gateway start:先尝试直接启动(真实配置),如果 CLI 校验失败,
1010
+ # 则临时修改真实配置(此时 gateway 已停止,无 config watcher),启动后恢复。
1011
+ # 这样 gateway 进程读取的是完整配置(含 channels.qqbot)。
1012
+
1013
+ # 为 gateway stop 创建临时配置
1014
+ TEMP_RESTART_CONFIG=""
1015
+ if [ -f "$BACKUP" ]; then
1016
+ TEMP_RESTART_CONFIG="\$(mktemp)"
1017
+ node -e "
1018
+ const fs = require('fs');
1019
+ const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
1020
+ if (cfg.channels && cfg.channels.qqbot) {
1021
+ delete cfg.channels.qqbot;
1022
+ if (Object.keys(cfg.channels).length === 0) delete cfg.channels;
1023
+ }
1024
+ if (cfg.plugins && cfg.plugins.entries && cfg.plugins.entries['openclaw-qqbot']) {
1025
+ delete cfg.plugins.entries['openclaw-qqbot'];
1026
+ if (Object.keys(cfg.plugins.entries).length === 0) delete cfg.plugins.entries;
1027
+ }
1028
+ fs.writeFileSync(process.argv[2], JSON.stringify(cfg, null, 4) + '\\n');
1029
+ " "$CONFIG" "$TEMP_RESTART_CONFIG" 2>/dev/null
1030
+ if [ \$? -ne 0 ] || [ ! -s "$TEMP_RESTART_CONFIG" ]; then
1031
+ echo "[qqbot-upgrade] WARNING: failed to create temp config"
1032
+ TEMP_RESTART_CONFIG=""
1033
+ fi
1034
+ fi
1035
+
1034
1036
  echo "[qqbot-upgrade] Stopping gateway..."
1035
- $CLI gateway stop 2>/dev/null || true
1037
+ if [ -n "$TEMP_RESTART_CONFIG" ]; then
1038
+ OPENCLAW_CONFIG_PATH="$TEMP_RESTART_CONFIG" $CLI gateway stop 2>/dev/null || true
1039
+ else
1040
+ $CLI gateway stop 2>/dev/null || true
1041
+ fi
1036
1042
  sleep 2
1037
1043
 
1038
- if [ -f "$BACKUP" ]; then
1039
- echo "[qqbot-upgrade] Temporarily removing channels.qqbot and plugins.entries for config validation bypass..."
1044
+ # 清理临时配置(不再需要)
1045
+ if [ -n "$TEMP_RESTART_CONFIG" ] && [ -f "$TEMP_RESTART_CONFIG" ]; then
1046
+ rm -f "$TEMP_RESTART_CONFIG"
1047
+ fi
1048
+
1049
+ echo "[qqbot-upgrade] Starting gateway..."
1050
+ START_OK=false
1051
+
1052
+ # 先尝试直接启动(使用真实配置,含 channels.qqbot)
1053
+ # 如果 openclaw 版本不做严格校验,或者插件已注册,这会直接成功
1054
+ if $CLI gateway start 2>/dev/null; then
1055
+ START_OK=true
1056
+ echo "[qqbot-upgrade] Gateway started successfully (direct start)"
1057
+ elif [ -f "$BACKUP" ]; then
1058
+ # 直接启动失败(可能是 channels.qqbot 校验失败),
1059
+ # 临时修改真实配置(此时 gateway 已停止,无 config watcher,安全)
1060
+ echo "[qqbot-upgrade] Direct start failed, temporarily removing channels.qqbot from real config..."
1040
1061
  node -e "
1041
1062
  const fs = require('fs');
1042
1063
  const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
@@ -1055,42 +1076,44 @@ if [ -f "$BACKUP" ]; then
1055
1076
  fs.writeFileSync(process.argv[1], JSON.stringify(cfg, null, 4) + '\\n');
1056
1077
  }
1057
1078
  " "$CONFIG" 2>/dev/null
1058
- echo "[qqbot-upgrade] channels.qqbot temporarily removed"
1059
- fi
1060
1079
 
1061
- echo "[qqbot-upgrade] Starting gateway..."
1062
- START_OK=false
1063
- if $CLI gateway start 2>/dev/null; then
1064
- START_OK=true
1065
- echo "[qqbot-upgrade] Gateway started successfully"
1066
- else
1067
- echo "[qqbot-upgrade] WARNING: gateway start failed, will still restore config"
1068
- fi
1080
+ if $CLI gateway start 2>/dev/null; then
1081
+ START_OK=true
1082
+ echo "[qqbot-upgrade] Gateway started successfully (after config fix)"
1083
+ else
1084
+ echo "[qqbot-upgrade] WARNING: gateway start still failed after config fix"
1085
+ fi
1069
1086
 
1070
- echo "[qqbot-upgrade] Waiting for plugin to load (8s)..."
1071
- sleep 8
1087
+ # 等待 gateway 进程启动并加载插件(插件注册 qqbot channel type)
1088
+ echo "[qqbot-upgrade] Waiting for plugin to load (8s)..."
1089
+ sleep 8
1072
1090
 
1073
- if [ -f "$BACKUP" ]; then
1074
- echo "[qqbot-upgrade] Restoring channels.qqbot and plugins.entries..."
1091
+ # 恢复 channels.qqbot 到真实配置
1092
+ # gateway config file watcher 会检测到变更并热加载
1093
+ echo "[qqbot-upgrade] Restoring channels.qqbot to real config..."
1075
1094
  node -e "
1095
+ const fs = require('fs');
1076
1096
  const fs = require('fs');
1077
1097
  const cfg = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
1078
1098
  const qqbot = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
1079
1099
  if (!cfg.channels) cfg.channels = {};
1080
1100
  cfg.channels.qqbot = qqbot;
1081
- // 恢复 plugins.entries
1082
- if (!cfg.plugins) cfg.plugins = {};
1083
- if (!cfg.plugins.entries) cfg.plugins.entries = {};
1084
- if (!cfg.plugins.entries['openclaw-qqbot']) {
1085
- cfg.plugins.entries['openclaw-qqbot'] = { enabled: true };
1086
- }
1101
+ // 注意:不写入 plugins.entries.openclaw-qqbot,
1102
+ // 插件通过 auto-discover 加载,显式 entry 会导致 duplicate plugin id 警告。
1087
1103
  fs.writeFileSync(process.argv[1], JSON.stringify(cfg, null, 4) + '\\n');
1088
1104
  " "$CONFIG" "$BACKUP" 2>/dev/null
1089
1105
  rm -f "$BACKUP"
1090
1106
  echo "[qqbot-upgrade] channels.qqbot restored"
1107
+ else
1108
+ echo "[qqbot-upgrade] WARNING: gateway start failed, no backup to restore"
1109
+ fi
1110
+
1111
+ # 直接启动成功的情况下,清理备份文件
1112
+ if [ "$START_OK" = "true" ] && [ -f "$BACKUP" ]; then
1113
+ rm -f "$BACKUP"
1091
1114
  fi
1092
1115
 
1093
- # 如果 start 失败,尝试再次启动(此时 channels.qqbot 已恢复,插件可能已注册)
1116
+ # 如果 start 失败,尝试再次启动
1094
1117
  if [ "$START_OK" != "true" ]; then
1095
1118
  echo "[qqbot-upgrade] Retrying gateway start..."
1096
1119
  sleep 2