@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 +1 -1
- package/README.zh.md +1 -1
- package/dist/src/slash-commands.js +306 -25
- package/dist/src/utils/pkg-version.js +19 -9
- package/package.json +4 -1
- package/scripts/upgrade-via-npm.sh +131 -24
- package/scripts/upgrade-via-source.sh +89 -9
- package/src/slash-commands.ts +300 -24
- package/src/utils/pkg-version.ts +18 -8
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.
|
|
13
|
+
### 🚀 Current Version: `v1.6.7`
|
|
14
14
|
|
|
15
15
|
[](./LICENSE)
|
|
16
16
|
[](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.
|
|
12
|
+
### 🚀 当前版本: `v1.6.7`
|
|
13
13
|
|
|
14
14
|
[](./LICENSE)
|
|
15
15
|
[](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
|
-
|
|
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
|
-
|
|
54
|
-
return _frameworkVersion;
|
|
49
|
+
return out;
|
|
55
50
|
}
|
|
56
51
|
}
|
|
57
52
|
}
|
|
58
53
|
catch {
|
|
59
54
|
// fallback
|
|
60
55
|
}
|
|
61
|
-
|
|
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(`❌
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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:
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
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
|
-
|
|
9
|
+
/** 已定位到的 package.json 路径,避免重复遍历目录树 */
|
|
10
|
+
let _resolvedPkgPath = null;
|
|
10
11
|
export function getPackageVersion(metaUrl) {
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
25
|
-
return
|
|
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
|
-
|
|
42
|
-
return _cached;
|
|
53
|
+
return pkg.version;
|
|
43
54
|
}
|
|
44
55
|
}
|
|
45
56
|
catch { /* next */ }
|
|
46
57
|
}
|
|
47
58
|
}
|
|
48
59
|
catch { /* fallback */ }
|
|
49
|
-
|
|
50
|
-
return _cached;
|
|
60
|
+
return "unknown";
|
|
51
61
|
}
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
228
|
+
NEEDS_TEMP_CONFIG=false
|
|
184
229
|
|
|
185
230
|
if [ -f "$CONFIG_FILE" ]; then
|
|
186
|
-
|
|
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
|
-
|
|
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 [ "$
|
|
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
|
-
|
|
201
|
-
if (
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
444
|
-
#
|
|
445
|
-
# (
|
|
446
|
-
# 新版已修复,此处通过阈值判断:包数量 > 50 才触发清理,避免对新版做无用操作。
|
|
473
|
+
# ── 复制 bundledDependencies 到插件 node_modules ──
|
|
474
|
+
# 由于安装前移走了源码的 node_modules,extensions 中的插件目录没有依赖。
|
|
475
|
+
# 只需复制 bundledDependencies(ws, 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
|
-
|
|
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
|
package/src/slash-commands.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
56
|
-
return _frameworkVersion;
|
|
52
|
+
return out;
|
|
57
53
|
}
|
|
58
54
|
}
|
|
59
55
|
} catch {
|
|
60
56
|
// fallback
|
|
61
57
|
}
|
|
62
|
-
|
|
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(`❌
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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:
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
|
package/src/utils/pkg-version.ts
CHANGED
|
@@ -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
|
-
|
|
11
|
+
/** 已定位到的 package.json 路径,避免重复遍历目录树 */
|
|
12
|
+
let _resolvedPkgPath: string | null = null;
|
|
12
13
|
|
|
13
14
|
export function getPackageVersion(metaUrl?: string): string {
|
|
14
|
-
|
|
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
|
-
|
|
29
|
-
return
|
|
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
|
-
|
|
46
|
-
return _cached;
|
|
57
|
+
return pkg.version as string;
|
|
47
58
|
}
|
|
48
59
|
} catch { /* next */ }
|
|
49
60
|
}
|
|
50
61
|
} catch { /* fallback */ }
|
|
51
62
|
|
|
52
|
-
|
|
53
|
-
return _cached;
|
|
63
|
+
return "unknown";
|
|
54
64
|
}
|