@tencent-connect/openclaw-qqbot 1.6.6 → 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 +6 -1
- package/README.zh.md +6 -1
- package/dist/src/api.js +1 -1
- package/dist/src/config.js +1 -1
- package/dist/src/gateway.js +1 -1
- package/dist/src/request-context.d.ts +7 -0
- package/dist/src/request-context.js +7 -0
- package/dist/src/slash-commands.js +362 -38
- package/dist/src/tools/remind.js +17 -9
- package/dist/src/types.d.ts +9 -2
- package/dist/src/update-checker.d.ts +3 -1
- package/dist/src/update-checker.js +13 -2
- package/dist/src/utils/pkg-version.js +19 -9
- package/package.json +4 -1
- package/scripts/postinstall-link-sdk.js +22 -9
- package/scripts/upgrade-via-npm.ps1 +9 -0
- package/scripts/upgrade-via-npm.sh +154 -30
- package/scripts/upgrade-via-source.sh +124 -38
- package/skills/qqbot-remind/SKILL.md +21 -11
- package/src/api.ts +1 -1
- package/src/config.ts +1 -1
- package/src/gateway.ts +1 -1
- package/src/request-context.ts +10 -0
- package/src/slash-commands.ts +354 -36
- package/src/tools/remind.ts +17 -9
- package/src/types.ts +9 -2
- package/src/update-checker.ts +14 -2
- package/src/utils/pkg-version.ts +18 -8
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
|
// ============ 热更新兼容性检查 ============
|
|
@@ -746,14 +741,22 @@ function cleanupTempScript(): void {
|
|
|
746
741
|
*
|
|
747
742
|
* 安全机制:脚本会被复制到临时目录再执行,避免升级过程中插件目录被操作导致脚本自身丢失。
|
|
748
743
|
*/
|
|
749
|
-
function fireHotUpgrade(targetVersion?: string): HotUpgradeStartResult {
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
744
|
+
function fireHotUpgrade(targetVersion?: string, pkg?: string, useLocal?: boolean): HotUpgradeStartResult {
|
|
745
|
+
// --local: 直接使用本地脚本,跳过远端下载
|
|
746
|
+
// 默认: 优先从远端下载升级脚本,避免使用本地可能过时的版本
|
|
747
|
+
const scriptPath = useLocal
|
|
748
|
+
? (() => {
|
|
749
|
+
const local = getUpgradeScriptPath();
|
|
750
|
+
if (!local) return null;
|
|
751
|
+
console.log(`[qqbot] fireHotUpgrade: --local specified, using local script: ${local}`);
|
|
752
|
+
return copyScriptToTemp(local) || local;
|
|
753
|
+
})()
|
|
754
|
+
: downloadRemoteUpgradeScript() || (() => {
|
|
755
|
+
const local = getUpgradeScriptPath();
|
|
756
|
+
if (!local) return null;
|
|
757
|
+
console.log(`[qqbot] fireHotUpgrade: remote download failed, falling back to local script: ${local}`);
|
|
758
|
+
return copyScriptToTemp(local) || local;
|
|
759
|
+
})();
|
|
757
760
|
if (!scriptPath) return { ok: false, reason: "no-script" };
|
|
758
761
|
|
|
759
762
|
const cli = findCli();
|
|
@@ -772,27 +775,149 @@ function fireHotUpgrade(targetVersion?: string): HotUpgradeStartResult {
|
|
|
772
775
|
"-File", scriptPath,
|
|
773
776
|
"-NoRestart",
|
|
774
777
|
...(targetVersion ? ["-Version", targetVersion] : []),
|
|
778
|
+
...(pkg ? ["-Pkg", pkg] : []),
|
|
775
779
|
];
|
|
776
780
|
} else {
|
|
777
781
|
// Mac / Linux: bash 执行 .sh
|
|
778
782
|
const bash = findBash();
|
|
779
783
|
if (!bash) return { ok: false, reason: "no-bash" };
|
|
780
784
|
shell = bash;
|
|
781
|
-
shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : [])];
|
|
785
|
+
shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : []), ...(pkg ? ["--pkg", pkg] : [])];
|
|
782
786
|
}
|
|
783
787
|
|
|
784
|
-
console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}`);
|
|
788
|
+
console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}, pkg=${pkg || "default"}`);
|
|
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
|
+
}
|
|
785
884
|
|
|
786
885
|
// 异步执行升级脚本
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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",
|
|
790
898
|
...(isWindows() ? { windowsHide: true } : {}),
|
|
791
899
|
}, (error, stdout, _stderr) => {
|
|
792
900
|
if (error) {
|
|
793
901
|
console.error(`[qqbot] fireHotUpgrade: script failed: ${error.message}`);
|
|
794
902
|
if (stdout) console.error(`[qqbot] fireHotUpgrade: stdout: ${stdout.slice(0, 2000)}`);
|
|
795
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();
|
|
796
921
|
cleanupTempScript();
|
|
797
922
|
_upgrading = false;
|
|
798
923
|
return;
|
|
@@ -805,6 +930,7 @@ function fireHotUpgrade(targetVersion?: string): HotUpgradeStartResult {
|
|
|
805
930
|
const newVersion = versionMatch?.[1];
|
|
806
931
|
if (newVersion === "unknown") {
|
|
807
932
|
console.error(`[qqbot] fireHotUpgrade: script output QQBOT_NEW_VERSION=unknown, aborting restart`);
|
|
933
|
+
syncTempConfigAndCleanup();
|
|
808
934
|
cleanupTempScript();
|
|
809
935
|
_upgrading = false;
|
|
810
936
|
return;
|
|
@@ -812,7 +938,8 @@ function fireHotUpgrade(targetVersion?: string): HotUpgradeStartResult {
|
|
|
812
938
|
|
|
813
939
|
console.log(`[qqbot] fireHotUpgrade: new version=${newVersion || "(not detected)"}, triggering restart...`);
|
|
814
940
|
|
|
815
|
-
//
|
|
941
|
+
// 脚本执行成功,同步临时配置中的 install 记录并清理
|
|
942
|
+
syncTempConfigAndCleanup();
|
|
816
943
|
cleanupTempScript();
|
|
817
944
|
|
|
818
945
|
// 文件替换成功,在 restart 之前把 source 从 path 切换为 npm,
|
|
@@ -855,17 +982,175 @@ function fireHotUpgrade(targetVersion?: string): HotUpgradeStartResult {
|
|
|
855
982
|
execCliAsync(cli, ["gateway", "restart"], { timeout: 30_000 }, () => {});
|
|
856
983
|
}
|
|
857
984
|
} else {
|
|
858
|
-
// Mac/Linux:
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
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;
|
|
867
1008
|
}
|
|
868
|
-
}
|
|
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
|
+
}
|
|
869
1154
|
}
|
|
870
1155
|
});
|
|
871
1156
|
|
|
@@ -894,11 +1179,13 @@ registerCommand({
|
|
|
894
1179
|
`/bot-upgrade 检查是否有新版本`,
|
|
895
1180
|
`/bot-upgrade --latest 确认升级到最新版本(需 upgradeMode=hot-reload)`,
|
|
896
1181
|
`/bot-upgrade --version X 升级到指定版本(需 upgradeMode=hot-reload)`,
|
|
1182
|
+
`/bot-upgrade --pkg scope/name 指定 npm 包(如 ryantest/openclaw-qqbot)`,
|
|
897
1183
|
`/bot-upgrade --force 强制重新安装当前版本(需 upgradeMode=hot-reload)`,
|
|
1184
|
+
`/bot-upgrade --local 使用本地升级脚本(跳过远端下载)`,
|
|
898
1185
|
].join("\n"),
|
|
899
1186
|
handler: async (ctx) => {
|
|
900
1187
|
const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
|
|
901
|
-
const upgradeMode = ctx.accountConfig?.upgradeMode || "
|
|
1188
|
+
const upgradeMode = ctx.accountConfig?.upgradeMode || "hot-reload";
|
|
902
1189
|
const args = ctx.args.trim();
|
|
903
1190
|
const info = await getUpdateInfo();
|
|
904
1191
|
|
|
@@ -949,7 +1236,9 @@ registerCommand({
|
|
|
949
1236
|
|
|
950
1237
|
let isForce = false;
|
|
951
1238
|
let isLatest = false;
|
|
1239
|
+
let isLocal = false;
|
|
952
1240
|
let versionArg: string | undefined;
|
|
1241
|
+
let pkgArg: string | undefined;
|
|
953
1242
|
const tokens = args ? args.split(/\s+/).filter(Boolean) : [];
|
|
954
1243
|
for (let i = 0; i < tokens.length; i += 1) {
|
|
955
1244
|
const t = tokens[i]!;
|
|
@@ -961,6 +1250,27 @@ registerCommand({
|
|
|
961
1250
|
isLatest = true;
|
|
962
1251
|
continue;
|
|
963
1252
|
}
|
|
1253
|
+
if (t === "--local") {
|
|
1254
|
+
isLocal = true;
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
if (t === "--pkg") {
|
|
1258
|
+
const next = tokens[i + 1];
|
|
1259
|
+
if (!next || next.startsWith("--")) {
|
|
1260
|
+
return `❌ 参数错误:--pkg 需要包名\n\n示例:/bot-upgrade --pkg ryantest/openclaw-qqbot`;
|
|
1261
|
+
}
|
|
1262
|
+
pkgArg = next;
|
|
1263
|
+
i += 1;
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
if (t.startsWith("--pkg=")) {
|
|
1267
|
+
const v = t.slice("--pkg=".length).trim();
|
|
1268
|
+
if (!v) {
|
|
1269
|
+
return `❌ 参数错误:--pkg 需要包名\n\n示例:/bot-upgrade --pkg ryantest/openclaw-qqbot`;
|
|
1270
|
+
}
|
|
1271
|
+
pkgArg = v;
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
964
1274
|
if (t === "--version") {
|
|
965
1275
|
const next = tokens[i + 1];
|
|
966
1276
|
if (!next || next.startsWith("--")) {
|
|
@@ -1022,9 +1332,17 @@ registerCommand({
|
|
|
1022
1332
|
].join("\n");
|
|
1023
1333
|
}
|
|
1024
1334
|
|
|
1335
|
+
// 解析 npm 包名:--pkg 参数 > 配置项 upgradePkg > 默认
|
|
1336
|
+
// 支持 "scope/name"(自动补 @)和 "@scope/name" 两种格式
|
|
1337
|
+
let upgradePkg = pkgArg || ctx.accountConfig?.upgradePkg;
|
|
1338
|
+
if (upgradePkg) {
|
|
1339
|
+
upgradePkg = upgradePkg.trim();
|
|
1340
|
+
if (!upgradePkg.startsWith("@")) upgradePkg = `@${upgradePkg}`;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1025
1343
|
// ── --version 指定版本:先校验版本号是否存在 ──
|
|
1026
1344
|
if (versionArg) {
|
|
1027
|
-
const exists = await checkVersionExists(versionArg);
|
|
1345
|
+
const exists = await checkVersionExists(versionArg, upgradePkg);
|
|
1028
1346
|
if (!exists) {
|
|
1029
1347
|
return `❌ 版本 ${versionArg} 不存在,请检查版本号`;
|
|
1030
1348
|
}
|
|
@@ -1077,7 +1395,7 @@ registerCommand({
|
|
|
1077
1395
|
preUpgradeCredentialBackup(ctx.accountId, ctx.appId);
|
|
1078
1396
|
|
|
1079
1397
|
// 异步执行升级
|
|
1080
|
-
const startResult = fireHotUpgrade(targetVersion);
|
|
1398
|
+
const startResult = fireHotUpgrade(targetVersion, upgradePkg, isLocal);
|
|
1081
1399
|
if (!startResult.ok) {
|
|
1082
1400
|
_upgrading = false;
|
|
1083
1401
|
if (startResult.reason === "no-script") {
|
package/src/tools/remind.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
-
import { getRequestTarget } from "../request-context.js";
|
|
2
|
+
import { getRequestTarget, getRequestAccountId } from "../request-context.js";
|
|
3
3
|
|
|
4
4
|
// ========== 类型定义 ==========
|
|
5
5
|
|
|
@@ -134,7 +134,7 @@ function generateJobName(content: string): string {
|
|
|
134
134
|
/**
|
|
135
135
|
* 构建一次性提醒的 cron 工具参数
|
|
136
136
|
*/
|
|
137
|
-
function buildOnceJob(params: RemindParams, delayMs: number, to: string) {
|
|
137
|
+
function buildOnceJob(params: RemindParams, delayMs: number, to: string, accountId: string) {
|
|
138
138
|
const atMs = Date.now() + delayMs;
|
|
139
139
|
const content = params.content!;
|
|
140
140
|
const name = params.name || generateJobName(content);
|
|
@@ -150,9 +150,12 @@ function buildOnceJob(params: RemindParams, delayMs: number, to: string) {
|
|
|
150
150
|
payload: {
|
|
151
151
|
kind: "agentTurn",
|
|
152
152
|
message: buildReminderPrompt(content),
|
|
153
|
-
|
|
153
|
+
},
|
|
154
|
+
delivery: {
|
|
155
|
+
mode: "announce",
|
|
154
156
|
channel: "qqbot",
|
|
155
157
|
to,
|
|
158
|
+
accountId,
|
|
156
159
|
},
|
|
157
160
|
},
|
|
158
161
|
};
|
|
@@ -161,7 +164,7 @@ function buildOnceJob(params: RemindParams, delayMs: number, to: string) {
|
|
|
161
164
|
/**
|
|
162
165
|
* 构建周期提醒的 cron 工具参数
|
|
163
166
|
*/
|
|
164
|
-
function buildCronJob(params: RemindParams, to: string) {
|
|
167
|
+
function buildCronJob(params: RemindParams, to: string, accountId: string) {
|
|
165
168
|
const content = params.content!;
|
|
166
169
|
const name = params.name || generateJobName(content);
|
|
167
170
|
const tz = params.timezone || "Asia/Shanghai";
|
|
@@ -176,9 +179,12 @@ function buildCronJob(params: RemindParams, to: string) {
|
|
|
176
179
|
payload: {
|
|
177
180
|
kind: "agentTurn",
|
|
178
181
|
message: buildReminderPrompt(content),
|
|
179
|
-
|
|
182
|
+
},
|
|
183
|
+
delivery: {
|
|
184
|
+
mode: "announce",
|
|
180
185
|
channel: "qqbot",
|
|
181
186
|
to,
|
|
187
|
+
accountId,
|
|
182
188
|
},
|
|
183
189
|
},
|
|
184
190
|
};
|
|
@@ -256,6 +262,8 @@ export function registerRemindTool(api: OpenClawPluginApi): void {
|
|
|
256
262
|
if (!resolvedTo) {
|
|
257
263
|
return json({ error: "action=add 时无法确定投递目标地址。请稍后重试。" });
|
|
258
264
|
}
|
|
265
|
+
// 从请求上下文获取当前账户 ID(多账户场景),fallback 到 "default"
|
|
266
|
+
const resolvedAccountId = getRequestAccountId() || "default";
|
|
259
267
|
if (!p.time) {
|
|
260
268
|
return json({ error: "action=add 时 time(时间)为必填参数。示例:\"5m\"、\"1h30m\"、\"0 8 * * *\"" });
|
|
261
269
|
}
|
|
@@ -263,9 +271,9 @@ export function registerRemindTool(api: OpenClawPluginApi): void {
|
|
|
263
271
|
// 判断是 cron 表达式还是相对时间
|
|
264
272
|
if (isCronExpression(p.time)) {
|
|
265
273
|
// 周期提醒
|
|
266
|
-
const cronJob = buildCronJob(p, resolvedTo);
|
|
274
|
+
const cronJob = buildCronJob(p, resolvedTo, resolvedAccountId);
|
|
267
275
|
return json({
|
|
268
|
-
_instruction: "请立即使用 cron
|
|
276
|
+
_instruction: "请立即使用 cron 工具,将 cronParams 的值原样作为参数传入(不要修改或省略任何字段,特别是 delivery.accountId),然后回复用户提醒已设置",
|
|
269
277
|
cronParams: cronJob,
|
|
270
278
|
summary: `⏰ 周期提醒: "${p.content}" (${p.time}, tz=${p.timezone || "Asia/Shanghai"})`,
|
|
271
279
|
});
|
|
@@ -285,9 +293,9 @@ export function registerRemindTool(api: OpenClawPluginApi): void {
|
|
|
285
293
|
return json({ error: "提醒时间不能少于 30 秒" });
|
|
286
294
|
}
|
|
287
295
|
|
|
288
|
-
const onceJob = buildOnceJob(p, delayMs, resolvedTo);
|
|
296
|
+
const onceJob = buildOnceJob(p, delayMs, resolvedTo, resolvedAccountId);
|
|
289
297
|
return json({
|
|
290
|
-
_instruction: "请立即使用 cron
|
|
298
|
+
_instruction: "请立即使用 cron 工具,将 cronParams 的值原样作为参数传入(不要修改或省略任何字段,特别是 delivery.accountId),然后回复用户提醒已设置",
|
|
291
299
|
cronParams: onceJob,
|
|
292
300
|
summary: `⏰ ${formatDelay(delayMs)}后提醒: "${p.content}"`,
|
|
293
301
|
});
|
package/src/types.ts
CHANGED
|
@@ -97,10 +97,17 @@ export interface QQBotAccountConfig {
|
|
|
97
97
|
upgradeUrl?: string;
|
|
98
98
|
/**
|
|
99
99
|
* /bot-upgrade 指令的行为模式
|
|
100
|
-
* - "doc"
|
|
101
|
-
* - "hot-reload":检测到新版本时直接执行 npm
|
|
100
|
+
* - "doc":展示升级文档链接(安全模式)
|
|
101
|
+
* - "hot-reload":检测到新版本时直接执行 npm 升级脚本进行热更新(默认)
|
|
102
102
|
*/
|
|
103
103
|
upgradeMode?: "doc" | "hot-reload";
|
|
104
|
+
/**
|
|
105
|
+
* /bot-upgrade 热更新时使用的 npm 包名
|
|
106
|
+
* 支持 "scope/name"(自动补 @)或 "@scope/name" 格式
|
|
107
|
+
* 默认: "@tencent-connect/openclaw-qqbot"
|
|
108
|
+
* 示例: "ryantest/openclaw-qqbot"
|
|
109
|
+
*/
|
|
110
|
+
upgradePkg?: string;
|
|
104
111
|
/**
|
|
105
112
|
* 出站消息合并回复(debounce)配置
|
|
106
113
|
* 当短时间内收到多次 deliver 时,将文本合并为一条消息发送,避免消息轰炸
|
package/src/update-checker.ts
CHANGED
|
@@ -122,9 +122,12 @@ export async function getUpdateInfo(): Promise<UpdateInfo> {
|
|
|
122
122
|
/**
|
|
123
123
|
* 检查指定版本是否存在于 npm registry
|
|
124
124
|
* 用于 /bot-upgrade --version 的前置校验
|
|
125
|
+
* @param version 要检查的版本号
|
|
126
|
+
* @param pkgName 可选的包名(如 "@ryantest/openclaw-qqbot"),默认使用内置包名
|
|
125
127
|
*/
|
|
126
|
-
export async function checkVersionExists(version: string): Promise<boolean> {
|
|
127
|
-
|
|
128
|
+
export async function checkVersionExists(version: string, pkgName?: string): Promise<boolean> {
|
|
129
|
+
const registries = pkgName ? buildRegistries(pkgName) : REGISTRIES;
|
|
130
|
+
for (const baseUrl of registries) {
|
|
128
131
|
try {
|
|
129
132
|
const url = `${baseUrl}/${version}`;
|
|
130
133
|
const json = await fetchJson(url, 10_000);
|
|
@@ -136,6 +139,15 @@ export async function checkVersionExists(version: string): Promise<boolean> {
|
|
|
136
139
|
return false;
|
|
137
140
|
}
|
|
138
141
|
|
|
142
|
+
/** 根据自定义包名构建 registry URL 列表 */
|
|
143
|
+
function buildRegistries(pkgName: string): string[] {
|
|
144
|
+
const encoded = encodeURIComponent(pkgName);
|
|
145
|
+
return [
|
|
146
|
+
`https://registry.npmjs.org/${encoded}`,
|
|
147
|
+
`https://registry.npmmirror.com/${encoded}`,
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
139
151
|
function compareVersions(a: string, b: string): number {
|
|
140
152
|
const parse = (v: string) => {
|
|
141
153
|
const clean = v.replace(/^v/, "");
|