@ryantest/openclaw-qqbot 1.6.7-beta.2 → 1.6.7-beta.3

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/dist/src/api.js CHANGED
@@ -106,7 +106,7 @@ async function doFetchToken(appId, clientSecret) {
106
106
  const requestBody = { appId, clientSecret };
107
107
  const requestHeaders = { "Content-Type": "application/json", "User-Agent": PLUGIN_USER_AGENT };
108
108
  // 打印请求信息(隐藏敏感信息)
109
- console.log(`[qqbot-api:${appId}] >>> POST ${TOKEN_URL}`);
109
+ console.log(`[qqbot-api:${appId}] >>> POST ${TOKEN_URL} [secret: ${clientSecret.slice(0, 6)}...len=${clientSecret.length}]`);
110
110
  let response;
111
111
  try {
112
112
  response = await fetch(TOKEN_URL, {
@@ -152,7 +152,7 @@ function checkUpgradeCompatibility() {
152
152
  // 3. 检查 Node.js 版本
153
153
  const nodeVer = process.version.replace(/^v/, "");
154
154
  if (compareSemver(nodeVer, req.minNodeVersion) < 0) {
155
- errors.push(`❌ Node.js 版本过低:当前 **v${nodeVer}**,热更新要求最低 **v${req.minNodeVersion}**`);
155
+ errors.push(`❌ NoVBNde.js 版本过低:当前 **v${nodeVer}**,热更新要求最低 **v${req.minNodeVersion}**`);
156
156
  }
157
157
  // 4. 检查系统架构(arm 等特殊架构提示)
158
158
  const arch = process.arch;
@@ -642,15 +642,24 @@ function cleanupTempScript() {
642
642
  *
643
643
  * 安全机制:脚本会被复制到临时目录再执行,避免升级过程中插件目录被操作导致脚本自身丢失。
644
644
  */
645
- function fireHotUpgrade(targetVersion) {
646
- // 优先从远端下载升级脚本,避免使用本地可能过时的版本
647
- const scriptPath = downloadRemoteUpgradeScript() || (() => {
648
- const local = getUpgradeScriptPath();
649
- if (!local)
650
- return null;
651
- console.log(`[qqbot] fireHotUpgrade: remote download failed, falling back to local script: ${local}`);
652
- return copyScriptToTemp(local) || local;
653
- })();
645
+ function fireHotUpgrade(targetVersion, pkg, useLocal) {
646
+ // --local: 直接使用本地脚本,跳过远端下载
647
+ // 默认: 优先从远端下载升级脚本,避免使用本地可能过时的版本
648
+ const scriptPath = useLocal
649
+ ? (() => {
650
+ const local = getUpgradeScriptPath();
651
+ if (!local)
652
+ return null;
653
+ console.log(`[qqbot] fireHotUpgrade: --local specified, using local script: ${local}`);
654
+ return copyScriptToTemp(local) || local;
655
+ })()
656
+ : downloadRemoteUpgradeScript() || (() => {
657
+ const local = getUpgradeScriptPath();
658
+ if (!local)
659
+ return null;
660
+ console.log(`[qqbot] fireHotUpgrade: remote download failed, falling back to local script: ${local}`);
661
+ return copyScriptToTemp(local) || local;
662
+ })();
654
663
  if (!scriptPath)
655
664
  return { ok: false, reason: "no-script" };
656
665
  const cli = findCli();
@@ -669,6 +678,7 @@ function fireHotUpgrade(targetVersion) {
669
678
  "-File", scriptPath,
670
679
  "-NoRestart",
671
680
  ...(targetVersion ? ["-Version", targetVersion] : []),
681
+ ...(pkg ? ["-Pkg", pkg] : []),
672
682
  ];
673
683
  }
674
684
  else {
@@ -677,9 +687,9 @@ function fireHotUpgrade(targetVersion) {
677
687
  if (!bash)
678
688
  return { ok: false, reason: "no-bash" };
679
689
  shell = bash;
680
- shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : [])];
690
+ shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : []), ...(pkg ? ["--pkg", pkg] : [])];
681
691
  }
682
- console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}`);
692
+ console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}, pkg=${pkg || "default"}`);
683
693
  // 异步执行升级脚本
684
694
  execFile(shell, shellArgs, {
685
695
  timeout: 120_000,
@@ -786,7 +796,9 @@ registerCommand({
786
796
  `/bot-upgrade 检查是否有新版本`,
787
797
  `/bot-upgrade --latest 确认升级到最新版本(需 upgradeMode=hot-reload)`,
788
798
  `/bot-upgrade --version X 升级到指定版本(需 upgradeMode=hot-reload)`,
799
+ `/bot-upgrade --pkg scope/name 指定 npm 包(如 ryantest/openclaw-qqbot)`,
789
800
  `/bot-upgrade --force 强制重新安装当前版本(需 upgradeMode=hot-reload)`,
801
+ `/bot-upgrade --local 使用本地升级脚本(跳过远端下载)`,
790
802
  ].join("\n"),
791
803
  handler: async (ctx) => {
792
804
  const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
@@ -834,7 +846,9 @@ registerCommand({
834
846
  }
835
847
  let isForce = false;
836
848
  let isLatest = false;
849
+ let isLocal = false;
837
850
  let versionArg;
851
+ let pkgArg;
838
852
  const tokens = args ? args.split(/\s+/).filter(Boolean) : [];
839
853
  for (let i = 0; i < tokens.length; i += 1) {
840
854
  const t = tokens[i];
@@ -846,6 +860,27 @@ registerCommand({
846
860
  isLatest = true;
847
861
  continue;
848
862
  }
863
+ if (t === "--local") {
864
+ isLocal = true;
865
+ continue;
866
+ }
867
+ if (t === "--pkg") {
868
+ const next = tokens[i + 1];
869
+ if (!next || next.startsWith("--")) {
870
+ return `❌ 参数错误:--pkg 需要包名\n\n示例:/bot-upgrade --pkg ryantest/openclaw-qqbot`;
871
+ }
872
+ pkgArg = next;
873
+ i += 1;
874
+ continue;
875
+ }
876
+ if (t.startsWith("--pkg=")) {
877
+ const v = t.slice("--pkg=".length).trim();
878
+ if (!v) {
879
+ return `❌ 参数错误:--pkg 需要包名\n\n示例:/bot-upgrade --pkg ryantest/openclaw-qqbot`;
880
+ }
881
+ pkgArg = v;
882
+ continue;
883
+ }
849
884
  if (t === "--version") {
850
885
  const next = tokens[i + 1];
851
886
  if (!next || next.startsWith("--")) {
@@ -904,9 +939,17 @@ registerCommand({
904
939
  `🌟官方 GitHub 仓库:[点击前往](${GITHUB_URL})`,
905
940
  ].join("\n");
906
941
  }
942
+ // 解析 npm 包名:--pkg 参数 > 配置项 upgradePkg > 默认
943
+ // 支持 "scope/name"(自动补 @)和 "@scope/name" 两种格式
944
+ let upgradePkg = pkgArg || ctx.accountConfig?.upgradePkg;
945
+ if (upgradePkg) {
946
+ upgradePkg = upgradePkg.trim();
947
+ if (!upgradePkg.startsWith("@"))
948
+ upgradePkg = `@${upgradePkg}`;
949
+ }
907
950
  // ── --version 指定版本:先校验版本号是否存在 ──
908
951
  if (versionArg) {
909
- const exists = await checkVersionExists(versionArg);
952
+ const exists = await checkVersionExists(versionArg, upgradePkg);
910
953
  if (!exists) {
911
954
  return `❌ 版本 ${versionArg} 不存在,请检查版本号`;
912
955
  }
@@ -951,7 +994,7 @@ registerCommand({
951
994
  // 热更新前保存凭证快照,防止更新过程被打断导致 appId/secret 丢失
952
995
  preUpgradeCredentialBackup(ctx.accountId, ctx.appId);
953
996
  // 异步执行升级
954
- const startResult = fireHotUpgrade(targetVersion);
997
+ const startResult = fireHotUpgrade(targetVersion, upgradePkg, isLocal);
955
998
  if (!startResult.ok) {
956
999
  _upgrading = false;
957
1000
  if (startResult.reason === "no-script") {
@@ -96,6 +96,13 @@ export interface QQBotAccountConfig {
96
96
  * - "hot-reload":检测到新版本时直接执行 npm 升级脚本进行热更新(默认)
97
97
  */
98
98
  upgradeMode?: "doc" | "hot-reload";
99
+ /**
100
+ * /bot-upgrade 热更新时使用的 npm 包名
101
+ * 支持 "scope/name"(自动补 @)或 "@scope/name" 格式
102
+ * 默认: "@tencent-connect/openclaw-qqbot"
103
+ * 示例: "ryantest/openclaw-qqbot"
104
+ */
105
+ upgradePkg?: string;
99
106
  /**
100
107
  * 出站消息合并回复(debounce)配置
101
108
  * 当短时间内收到多次 deliver 时,将文本合并为一条消息发送,避免消息轰炸
@@ -30,5 +30,7 @@ export declare function getUpdateInfo(): Promise<UpdateInfo>;
30
30
  /**
31
31
  * 检查指定版本是否存在于 npm registry
32
32
  * 用于 /bot-upgrade --version 的前置校验
33
+ * @param version 要检查的版本号
34
+ * @param pkgName 可选的包名(如 "@ryantest/openclaw-qqbot"),默认使用内置包名
33
35
  */
34
- export declare function checkVersionExists(version: string): Promise<boolean>;
36
+ export declare function checkVersionExists(version: string, pkgName?: string): Promise<boolean>;
@@ -99,9 +99,12 @@ export async function getUpdateInfo() {
99
99
  /**
100
100
  * 检查指定版本是否存在于 npm registry
101
101
  * 用于 /bot-upgrade --version 的前置校验
102
+ * @param version 要检查的版本号
103
+ * @param pkgName 可选的包名(如 "@ryantest/openclaw-qqbot"),默认使用内置包名
102
104
  */
103
- export async function checkVersionExists(version) {
104
- for (const baseUrl of REGISTRIES) {
105
+ export async function checkVersionExists(version, pkgName) {
106
+ const registries = pkgName ? buildRegistries(pkgName) : REGISTRIES;
107
+ for (const baseUrl of registries) {
105
108
  try {
106
109
  const url = `${baseUrl}/${version}`;
107
110
  const json = await fetchJson(url, 10_000);
@@ -114,6 +117,14 @@ export async function checkVersionExists(version) {
114
117
  }
115
118
  return false;
116
119
  }
120
+ /** 根据自定义包名构建 registry URL 列表 */
121
+ function buildRegistries(pkgName) {
122
+ const encoded = encodeURIComponent(pkgName);
123
+ return [
124
+ `https://registry.npmjs.org/${encoded}`,
125
+ `https://registry.npmmirror.com/${encoded}`,
126
+ ];
127
+ }
117
128
  function compareVersions(a, b) {
118
129
  const parse = (v) => {
119
130
  const clean = v.replace(/^v/, "");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryantest/openclaw-qqbot",
3
- "version": "1.6.7-beta.2",
3
+ "version": "1.6.7-beta.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -17,6 +17,7 @@ param(
17
17
  [string]$Secret = "",
18
18
  [switch]$NoRestart,
19
19
  [string]$Tag = "",
20
+ [string]$Pkg = "",
20
21
  [switch]$Help
21
22
  )
22
23
 
@@ -25,6 +26,13 @@ $PKG_NAME = "@tencent-connect/openclaw-qqbot"
25
26
  $SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Definition
26
27
  $PROJECT_DIR = Split-Path -Parent $SCRIPT_DIR
27
28
 
29
+ # -Pkg 覆盖包名(支持 "scope/name" 自动补 @)
30
+ if ($Pkg) {
31
+ $Pkg = $Pkg.Trim()
32
+ if (-not $Pkg.StartsWith("@")) { $Pkg = "@$Pkg" }
33
+ $PKG_NAME = $Pkg
34
+ }
35
+
28
36
  # Read local version
29
37
  $LOCAL_VERSION = ""
30
38
  try {
@@ -41,6 +49,7 @@ if ($Help) {
41
49
  Write-Host " .\upgrade-via-npm.ps1 -Version [version] # upgrade to specific version"
42
50
  Write-Host " .\upgrade-via-npm.ps1 -SelfVersion # upgrade to repo version ($LOCAL_VERSION)"
43
51
  Write-Host ""
52
+ Write-Host " -Pkg [scope/name] Custom npm package (e.g. ryantest/openclaw-qqbot)"
44
53
  Write-Host " -AppId [appid] QQ bot appid (required on first install)"
45
54
  Write-Host " -Secret [secret] QQ bot secret (required on first install)"
46
55
  exit 0
@@ -63,6 +63,7 @@ print_usage() {
63
63
  echo " upgrade-via-npm.sh --self-version # 升级到当前仓库版本"
64
64
  fi
65
65
  echo ""
66
+ echo " --pkg <scope/name> 指定 npm 包名(如 ryantest/openclaw-qqbot)"
66
67
  echo " --appid <appid> QQ机器人 appid(首次安装时必填)"
67
68
  echo " --secret <secret> QQ机器人 secret(首次安装时必填)"
68
69
  echo ""
@@ -76,22 +77,17 @@ while [[ $# -gt 0 ]]; do
76
77
  case "$1" in
77
78
  --tag)
78
79
  [ -z "$2" ] && echo "❌ --tag 需要参数" && exit 1
79
- _ver="${2#v}" # 去掉 v 前缀(npm 版本号不带 v)
80
- TARGET_VERSION="$_ver"
81
- INSTALL_SRC="${PKG_NAME}@$_ver"
80
+ TARGET_VERSION="${2#v}" # 去掉 v 前缀(npm 版本号不带 v)
82
81
  shift 2
83
82
  ;;
84
83
  --version)
85
84
  [ -z "$2" ] && echo "❌ --version 需要参数" && exit 1
86
- _ver="${2#v}" # 去掉 v 前缀(npm 版本号不带 v)
87
- TARGET_VERSION="$_ver"
88
- INSTALL_SRC="${PKG_NAME}@$_ver"
85
+ TARGET_VERSION="${2#v}" # 去掉 v 前缀(npm 版本号不带 v)
89
86
  shift 2
90
87
  ;;
91
88
  --self-version)
92
89
  [ -z "$LOCAL_VERSION" ] && echo "❌ 无法从 package.json 读取版本" && exit 1
93
90
  TARGET_VERSION="$LOCAL_VERSION"
94
- INSTALL_SRC="${PKG_NAME}@${LOCAL_VERSION}"
95
91
  shift 1
96
92
  ;;
97
93
  --appid)
@@ -104,6 +100,14 @@ while [[ $# -gt 0 ]]; do
104
100
  SECRET="$2"
105
101
  shift 2
106
102
  ;;
103
+ --pkg)
104
+ [ -z "$2" ] && echo "❌ --pkg 需要参数" && exit 1
105
+ _pkg="$2"
106
+ # 支持 "scope/name" 自动补 @
107
+ if [[ "$_pkg" != @* ]]; then _pkg="@$_pkg"; fi
108
+ PKG_NAME="$_pkg"
109
+ shift 2
110
+ ;;
107
111
  --no-restart)
108
112
  NO_RESTART=true
109
113
  shift 1
@@ -115,7 +119,12 @@ while [[ $# -gt 0 ]]; do
115
119
  *) echo "未知选项: $1"; print_usage; exit 1 ;;
116
120
  esac
117
121
  done
118
- INSTALL_SRC="${INSTALL_SRC:-${PKG_NAME}@latest}"
122
+ # 参数解析完毕后统一拼接 INSTALL_SRC(确保 --pkg 无论在 --version 前后都能生效)
123
+ if [ -n "$TARGET_VERSION" ]; then
124
+ INSTALL_SRC="${PKG_NAME}@${TARGET_VERSION}"
125
+ else
126
+ INSTALL_SRC="${PKG_NAME}@latest"
127
+ fi
119
128
 
120
129
  # 环境变量 fallback
121
130
  APPID="${APPID:-$QQBOT_APPID}"
@@ -344,13 +344,13 @@ if [ ! -f "$_INSTALL_DIR/dist/index.js" ] || [ ! -f "$_INSTALL_DIR/preload.cjs"
344
344
  echo "请先解决安装问题后再运行此脚本。"
345
345
  # 恢复 channels.qqbot 后再退出
346
346
  if [ -n "$_QQBOT_CHANNEL_STASH" ] && [ -n "$_STASH_CFG" ] && [ -f "$_STASH_CFG" ]; then
347
- node -e "
348
- const fs = require('fs');
349
- const cfg = JSON.parse(fs.readFileSync('$_STASH_CFG', 'utf8'));
347
+ _STASH="$_QQBOT_CHANNEL_STASH" _CFG="$_STASH_CFG" node -e '
348
+ const fs = require("fs");
349
+ const cfg = JSON.parse(fs.readFileSync(process.env._CFG, "utf8"));
350
350
  if (!cfg.channels) cfg.channels = {};
351
- cfg.channels.qqbot = $_QQBOT_CHANNEL_STASH;
352
- fs.writeFileSync('$_STASH_CFG', JSON.stringify(cfg, null, 4) + '\n');
353
- " 2>/dev/null || true
351
+ cfg.channels.qqbot = JSON.parse(process.env._STASH);
352
+ fs.writeFileSync(process.env._CFG, JSON.stringify(cfg, null, 4) + "\n");
353
+ ' 2>/dev/null || true
354
354
  fi
355
355
  exit 1
356
356
  ;;
@@ -592,11 +592,11 @@ echo "[4/6] 准备机器人通道配置..."
592
592
  # 注意:channels.qqbot 已被暂存移除,所以从 _QQBOT_CHANNEL_STASH 读取
593
593
  CURRENT_QQBOT_TOKEN=""
594
594
  if [ -n "$_QQBOT_CHANNEL_STASH" ]; then
595
- CURRENT_QQBOT_TOKEN=$(node -e "
596
- const ch = $_QQBOT_CHANNEL_STASH;
595
+ CURRENT_QQBOT_TOKEN=$(_STASH="$_QQBOT_CHANNEL_STASH" node -e '
596
+ const ch = JSON.parse(process.env._STASH);
597
597
  if (ch.token) { process.stdout.write(ch.token); }
598
- else if (ch.appId && ch.clientSecret) { process.stdout.write(ch.appId + ':' + ch.clientSecret); }
599
- " 2>/dev/null || true)
598
+ else if (ch.appId && ch.clientSecret) { process.stdout.write(ch.appId + ":" + ch.clientSecret); }
599
+ ' 2>/dev/null || true)
600
600
  fi
601
601
 
602
602
  DESIRED_QQBOT_TOKEN=""
@@ -787,34 +787,39 @@ case "$start_choice" in
787
787
 
788
788
  if [ -n "$_target_cfg" ]; then
789
789
  # 构建完整的 channels.qqbot 对象(合并暂存配置 + 新 token + markdown)
790
- node -e "
791
- const fs = require('fs');
792
- const cfg = JSON.parse(fs.readFileSync('$_target_cfg', 'utf8'));
790
+ # 通过环境变量传递,避免 JSON 双引号在 node -e "..." 中被 shell 错误解析
791
+ _STASH="$_QQBOT_CHANNEL_STASH" \
792
+ _DESIRED="$DESIRED_QQBOT_TOKEN" \
793
+ _MD="$MARKDOWN_VALUE" \
794
+ _CFG="$_target_cfg" \
795
+ node -e '
796
+ const fs = require("fs");
797
+ const cfg = JSON.parse(fs.readFileSync(process.env._CFG, "utf8"));
793
798
  if (!cfg.channels) cfg.channels = {};
794
799
 
795
800
  // 从暂存恢复基础配置
796
- const stash = '$_QQBOT_CHANNEL_STASH';
801
+ const stash = process.env._STASH;
797
802
  if (stash) {
798
803
  try { cfg.channels.qqbot = JSON.parse(stash); } catch {}
799
804
  }
800
805
  if (!cfg.channels.qqbot) cfg.channels.qqbot = {};
801
806
 
802
807
  // 覆盖 token(如果有新值)
803
- const desired = '$DESIRED_QQBOT_TOKEN';
804
- if (desired && desired.includes(':')) {
805
- const [appId, ...rest] = desired.split(':');
808
+ const desired = process.env._DESIRED;
809
+ if (desired && desired.includes(":")) {
810
+ const [appId, ...rest] = desired.split(":");
806
811
  cfg.channels.qqbot.appId = appId;
807
- cfg.channels.qqbot.clientSecret = rest.join(':');
812
+ cfg.channels.qqbot.clientSecret = rest.join(":");
808
813
  delete cfg.channels.qqbot.token;
809
814
  }
810
815
 
811
816
  // 覆盖 markdown(如果有指定)
812
- const md = '$MARKDOWN_VALUE';
813
- if (md === 'true') cfg.channels.qqbot.markdownSupport = true;
814
- else if (md === 'false') cfg.channels.qqbot.markdownSupport = false;
817
+ const md = process.env._MD;
818
+ if (md === "true") cfg.channels.qqbot.markdownSupport = true;
819
+ else if (md === "false") cfg.channels.qqbot.markdownSupport = false;
815
820
 
816
- fs.writeFileSync('$_target_cfg', JSON.stringify(cfg, null, 4) + '\n');
817
- " 2>/dev/null || true
821
+ fs.writeFileSync(process.env._CFG, JSON.stringify(cfg, null, 4) + "\n");
822
+ ' 2>&1 || echo " ⚠️ 配置写入失败"
818
823
  echo " ✅ 已恢复 channels.qqbot 配置(含 token/markdown)"
819
824
  _need_reload=1
820
825
  fi
@@ -884,13 +889,13 @@ case "$start_choice" in
884
889
  # 注意:下次 gateway 启动可能因 "unknown channel id" 失败,
885
890
  # 需要用户手动 stop → 移除 channels.qqbot → start → 恢复
886
891
  if [ -n "$_QQBOT_CHANNEL_STASH" ] && [ -n "$_STASH_CFG" ] && [ -f "$_STASH_CFG" ]; then
887
- node -e "
888
- const fs = require('fs');
889
- const cfg = JSON.parse(fs.readFileSync('$_STASH_CFG', 'utf8'));
892
+ _STASH="$_QQBOT_CHANNEL_STASH" _CFG="$_STASH_CFG" node -e '
893
+ const fs = require("fs");
894
+ const cfg = JSON.parse(fs.readFileSync(process.env._CFG, "utf8"));
890
895
  if (!cfg.channels) cfg.channels = {};
891
- cfg.channels.qqbot = $_QQBOT_CHANNEL_STASH;
892
- fs.writeFileSync('$_STASH_CFG', JSON.stringify(cfg, null, 4) + '\n');
893
- " 2>/dev/null || true
896
+ cfg.channels.qqbot = JSON.parse(process.env._STASH);
897
+ fs.writeFileSync(process.env._CFG, JSON.stringify(cfg, null, 4) + "\n");
898
+ ' 2>/dev/null || true
894
899
  echo " 已恢复 channels.qqbot 配置"
895
900
  fi
896
901
  echo ""
package/src/api.ts CHANGED
@@ -137,7 +137,7 @@ async function doFetchToken(appId: string, clientSecret: string): Promise<string
137
137
  const requestHeaders = { "Content-Type": "application/json", "User-Agent": PLUGIN_USER_AGENT };
138
138
 
139
139
  // 打印请求信息(隐藏敏感信息)
140
- console.log(`[qqbot-api:${appId}] >>> POST ${TOKEN_URL}`);
140
+ console.log(`[qqbot-api:${appId}] >>> POST ${TOKEN_URL} [secret: ${clientSecret.slice(0, 6)}...len=${clientSecret.length}]`);
141
141
 
142
142
  let response: Response;
143
143
  try {
@@ -162,7 +162,7 @@ function checkUpgradeCompatibility(): UpgradeCompatResult {
162
162
  // 3. 检查 Node.js 版本
163
163
  const nodeVer = process.version.replace(/^v/, "");
164
164
  if (compareSemver(nodeVer, req.minNodeVersion) < 0) {
165
- errors.push(`❌ Node.js 版本过低:当前 **v${nodeVer}**,热更新要求最低 **v${req.minNodeVersion}**`);
165
+ errors.push(`❌ NoVBNde.js 版本过低:当前 **v${nodeVer}**,热更新要求最低 **v${req.minNodeVersion}**`);
166
166
  }
167
167
 
168
168
  // 4. 检查系统架构(arm 等特殊架构提示)
@@ -746,14 +746,22 @@ function cleanupTempScript(): void {
746
746
  *
747
747
  * 安全机制:脚本会被复制到临时目录再执行,避免升级过程中插件目录被操作导致脚本自身丢失。
748
748
  */
749
- function fireHotUpgrade(targetVersion?: string): HotUpgradeStartResult {
750
- // 优先从远端下载升级脚本,避免使用本地可能过时的版本
751
- const scriptPath = downloadRemoteUpgradeScript() || (() => {
752
- const local = getUpgradeScriptPath();
753
- if (!local) return null;
754
- console.log(`[qqbot] fireHotUpgrade: remote download failed, falling back to local script: ${local}`);
755
- return copyScriptToTemp(local) || local;
756
- })();
749
+ function fireHotUpgrade(targetVersion?: string, pkg?: string, useLocal?: boolean): HotUpgradeStartResult {
750
+ // --local: 直接使用本地脚本,跳过远端下载
751
+ // 默认: 优先从远端下载升级脚本,避免使用本地可能过时的版本
752
+ const scriptPath = useLocal
753
+ ? (() => {
754
+ const local = getUpgradeScriptPath();
755
+ if (!local) return null;
756
+ console.log(`[qqbot] fireHotUpgrade: --local specified, using local script: ${local}`);
757
+ return copyScriptToTemp(local) || local;
758
+ })()
759
+ : downloadRemoteUpgradeScript() || (() => {
760
+ const local = getUpgradeScriptPath();
761
+ if (!local) return null;
762
+ console.log(`[qqbot] fireHotUpgrade: remote download failed, falling back to local script: ${local}`);
763
+ return copyScriptToTemp(local) || local;
764
+ })();
757
765
  if (!scriptPath) return { ok: false, reason: "no-script" };
758
766
 
759
767
  const cli = findCli();
@@ -772,16 +780,17 @@ function fireHotUpgrade(targetVersion?: string): HotUpgradeStartResult {
772
780
  "-File", scriptPath,
773
781
  "-NoRestart",
774
782
  ...(targetVersion ? ["-Version", targetVersion] : []),
783
+ ...(pkg ? ["-Pkg", pkg] : []),
775
784
  ];
776
785
  } else {
777
786
  // Mac / Linux: bash 执行 .sh
778
787
  const bash = findBash();
779
788
  if (!bash) return { ok: false, reason: "no-bash" };
780
789
  shell = bash;
781
- shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : [])];
790
+ shellArgs = [scriptPath, "--no-restart", ...(targetVersion ? ["--version", targetVersion] : []), ...(pkg ? ["--pkg", pkg] : [])];
782
791
  }
783
792
 
784
- console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}`);
793
+ console.log(`[qqbot] fireHotUpgrade: shell=${shell}, script=${scriptPath}, cli=${cli}, target=${targetVersion || "latest"}, pkg=${pkg || "default"}`);
785
794
 
786
795
  // 异步执行升级脚本
787
796
  execFile(shell, shellArgs, {
@@ -894,7 +903,9 @@ registerCommand({
894
903
  `/bot-upgrade 检查是否有新版本`,
895
904
  `/bot-upgrade --latest 确认升级到最新版本(需 upgradeMode=hot-reload)`,
896
905
  `/bot-upgrade --version X 升级到指定版本(需 upgradeMode=hot-reload)`,
906
+ `/bot-upgrade --pkg scope/name 指定 npm 包(如 ryantest/openclaw-qqbot)`,
897
907
  `/bot-upgrade --force 强制重新安装当前版本(需 upgradeMode=hot-reload)`,
908
+ `/bot-upgrade --local 使用本地升级脚本(跳过远端下载)`,
898
909
  ].join("\n"),
899
910
  handler: async (ctx) => {
900
911
  const url = ctx.accountConfig?.upgradeUrl || DEFAULT_UPGRADE_URL;
@@ -949,7 +960,9 @@ registerCommand({
949
960
 
950
961
  let isForce = false;
951
962
  let isLatest = false;
963
+ let isLocal = false;
952
964
  let versionArg: string | undefined;
965
+ let pkgArg: string | undefined;
953
966
  const tokens = args ? args.split(/\s+/).filter(Boolean) : [];
954
967
  for (let i = 0; i < tokens.length; i += 1) {
955
968
  const t = tokens[i]!;
@@ -961,6 +974,27 @@ registerCommand({
961
974
  isLatest = true;
962
975
  continue;
963
976
  }
977
+ if (t === "--local") {
978
+ isLocal = true;
979
+ continue;
980
+ }
981
+ if (t === "--pkg") {
982
+ const next = tokens[i + 1];
983
+ if (!next || next.startsWith("--")) {
984
+ return `❌ 参数错误:--pkg 需要包名\n\n示例:/bot-upgrade --pkg ryantest/openclaw-qqbot`;
985
+ }
986
+ pkgArg = next;
987
+ i += 1;
988
+ continue;
989
+ }
990
+ if (t.startsWith("--pkg=")) {
991
+ const v = t.slice("--pkg=".length).trim();
992
+ if (!v) {
993
+ return `❌ 参数错误:--pkg 需要包名\n\n示例:/bot-upgrade --pkg ryantest/openclaw-qqbot`;
994
+ }
995
+ pkgArg = v;
996
+ continue;
997
+ }
964
998
  if (t === "--version") {
965
999
  const next = tokens[i + 1];
966
1000
  if (!next || next.startsWith("--")) {
@@ -1022,9 +1056,17 @@ registerCommand({
1022
1056
  ].join("\n");
1023
1057
  }
1024
1058
 
1059
+ // 解析 npm 包名:--pkg 参数 > 配置项 upgradePkg > 默认
1060
+ // 支持 "scope/name"(自动补 @)和 "@scope/name" 两种格式
1061
+ let upgradePkg = pkgArg || ctx.accountConfig?.upgradePkg;
1062
+ if (upgradePkg) {
1063
+ upgradePkg = upgradePkg.trim();
1064
+ if (!upgradePkg.startsWith("@")) upgradePkg = `@${upgradePkg}`;
1065
+ }
1066
+
1025
1067
  // ── --version 指定版本:先校验版本号是否存在 ──
1026
1068
  if (versionArg) {
1027
- const exists = await checkVersionExists(versionArg);
1069
+ const exists = await checkVersionExists(versionArg, upgradePkg);
1028
1070
  if (!exists) {
1029
1071
  return `❌ 版本 ${versionArg} 不存在,请检查版本号`;
1030
1072
  }
@@ -1077,7 +1119,7 @@ registerCommand({
1077
1119
  preUpgradeCredentialBackup(ctx.accountId, ctx.appId);
1078
1120
 
1079
1121
  // 异步执行升级
1080
- const startResult = fireHotUpgrade(targetVersion);
1122
+ const startResult = fireHotUpgrade(targetVersion, upgradePkg, isLocal);
1081
1123
  if (!startResult.ok) {
1082
1124
  _upgrading = false;
1083
1125
  if (startResult.reason === "no-script") {
package/src/types.ts CHANGED
@@ -101,6 +101,13 @@ export interface QQBotAccountConfig {
101
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 时,将文本合并为一条消息发送,避免消息轰炸
@@ -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
- for (const baseUrl of REGISTRIES) {
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/, "");