@tencent-connect/openclaw-qqbot 1.6.5-alpha.2 → 1.6.5-alpha.4

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
@@ -2,7 +2,6 @@
2
2
  * QQ Bot API 鉴权和请求封装
3
3
  * [修复版] 已重构为支持多实例并发,消除全局变量冲突
4
4
  */
5
- import { createRequire } from "node:module";
6
5
  import os from "node:os";
7
6
  import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./utils/upload-cache.js";
8
7
  import { sanitizeFileName } from "./utils/platform.js";
@@ -11,12 +10,8 @@ const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
11
10
  // ============ Plugin User-Agent ============
12
11
  // 格式: QQBotPlugin/{version} (Node/{nodeVersion}; {os})
13
12
  // 示例: QQBotPlugin/1.6.0 (Node/22.14.0; darwin)
14
- const _require = createRequire(import.meta.url);
15
- let _pluginVersion = "unknown";
16
- try {
17
- _pluginVersion = _require("../package.json").version ?? "unknown";
18
- }
19
- catch { /* fallback */ }
13
+ import { getPackageVersion } from "./utils/pkg-version.js";
14
+ const _pluginVersion = getPackageVersion(import.meta.url);
20
15
  export const PLUGIN_USER_AGENT = `QQBotPlugin/${_pluginVersion} (Node/${process.versions.node}; ${os.platform()})`;
21
16
  // 运行时配置
22
17
  let currentMarkdownSupport = false;
@@ -234,21 +229,48 @@ export async function apiRequest(accessToken, method, path, body, timeoutMs) {
234
229
  });
235
230
  const traceId = res.headers.get("x-tps-trace-id") ?? "";
236
231
  console.log(`[qqbot-api] <<< Status: ${res.status} ${res.statusText}${traceId ? ` | TraceId: ${traceId}` : ""}`);
237
- let data;
238
232
  let rawBody;
239
233
  try {
240
234
  rawBody = await res.text();
241
- console.log(`[qqbot-api] <<< Body:`, rawBody);
242
- data = JSON.parse(rawBody);
243
235
  }
244
236
  catch (err) {
245
- throw new Error(`Failed to parse response[${path}]: ${err instanceof Error ? err.message : String(err)}`);
237
+ throw new Error(`读取响应失败[${path}]: ${err instanceof Error ? err.message : String(err)}`);
246
238
  }
239
+ console.log(`[qqbot-api] <<< Body:`, rawBody);
240
+ // 检测非 JSON 响应(HTML 网关错误页 / CDN 限流页等)
241
+ const contentType = res.headers.get("content-type") ?? "";
242
+ const isHtmlResponse = contentType.includes("text/html") || rawBody.trimStart().startsWith("<");
247
243
  if (!res.ok) {
248
- const error = data;
249
- throw new Error(`API Error [${path}]: ${error.message ?? JSON.stringify(data)}`);
244
+ if (isHtmlResponse) {
245
+ // HTML 响应 = 网关/限流层返回的错误页,给出友好提示
246
+ const statusHint = res.status === 502 || res.status === 503 || res.status === 504
247
+ ? "调用发生异常,请稍候重试"
248
+ : res.status === 429
249
+ ? "请求过于频繁,已被限流"
250
+ : `开放平台返回 HTTP ${res.status}`;
251
+ throw new Error(`${statusHint}(${path}),请稍后重试`);
252
+ }
253
+ // JSON 错误响应
254
+ try {
255
+ const error = JSON.parse(rawBody);
256
+ throw new Error(`API Error [${path}]: ${error.message ?? rawBody}`);
257
+ }
258
+ catch (parseErr) {
259
+ if (parseErr instanceof Error && parseErr.message.startsWith("API Error"))
260
+ throw parseErr;
261
+ throw new Error(`API Error [${path}] HTTP ${res.status}: ${rawBody.slice(0, 200)}`);
262
+ }
263
+ }
264
+ // 成功响应但不是 JSON(极端异常情况)
265
+ if (isHtmlResponse) {
266
+ throw new Error(`QQ 服务端返回了非 JSON 响应(${path}),可能是临时故障,请稍后重试`);
267
+ }
268
+ try {
269
+ return JSON.parse(rawBody);
270
+ }
271
+ catch {
272
+ throw new Error(`开放平台响应格式异常(${path}),请稍后重试`);
250
273
  }
251
- return data;
252
274
  }
253
275
  // ============ 上传重试(指数退避) ============
254
276
  const UPLOAD_MAX_RETRIES = 2;
@@ -18,16 +18,9 @@ import { getUpdateInfo, checkVersionExists } from "./update-checker.js";
18
18
  import { getHomeDir, getQQBotDataDir, isWindows } from "./utils/platform.js";
19
19
  import { saveCredentialBackup } from "./credential-backup.js";
20
20
  import { fileURLToPath } from "node:url";
21
+ import { getPackageVersion } from "./utils/pkg-version.js";
21
22
  const require = createRequire(import.meta.url);
22
- // 读取 package.json 中的版本号
23
- let PLUGIN_VERSION = "unknown";
24
- try {
25
- const pkg = require("../package.json");
26
- PLUGIN_VERSION = pkg.version ?? "unknown";
27
- }
28
- catch {
29
- // fallback
30
- }
23
+ let PLUGIN_VERSION = getPackageVersion(import.meta.url);
31
24
  // 获取 openclaw 框架版本(缓存结果,只执行一次)
32
25
  let _frameworkVersion = null;
33
26
  function getFrameworkVersion() {
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { createRequire } from "node:module";
11
11
  import https from "node:https";
12
+ import { getPackageVersion } from "./utils/pkg-version.js";
12
13
  const require = createRequire(import.meta.url);
13
14
  const PKG_NAME = "@tencent-connect/openclaw-qqbot";
14
15
  const ENCODED_PKG = encodeURIComponent(PKG_NAME);
@@ -16,14 +17,7 @@ const REGISTRIES = [
16
17
  `https://registry.npmjs.org/${ENCODED_PKG}`,
17
18
  `https://registry.npmmirror.com/${ENCODED_PKG}`,
18
19
  ];
19
- let CURRENT_VERSION = "unknown";
20
- try {
21
- const pkg = require("../package.json");
22
- CURRENT_VERSION = pkg.version ?? "unknown";
23
- }
24
- catch {
25
- // fallback
26
- }
20
+ let CURRENT_VERSION = getPackageVersion(import.meta.url);
27
21
  let _log;
28
22
  function fetchJson(url, timeoutMs) {
29
23
  return new Promise((resolve, reject) => {
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 从 import.meta.url 向上遍历目录树查找 package.json 并读取 version。
3
+ * 不依赖硬编码的 "../" 层级,无论编译输出结构如何变化都能可靠找到。
4
+ */
5
+ export declare function getPackageVersion(metaUrl?: string): string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 从 import.meta.url 向上遍历目录树查找 package.json 并读取 version。
3
+ * 不依赖硬编码的 "../" 层级,无论编译输出结构如何变化都能可靠找到。
4
+ */
5
+ import { fileURLToPath } from "node:url";
6
+ import { createRequire } from "node:module";
7
+ import path from "node:path";
8
+ import fs from "node:fs";
9
+ let _cached = null;
10
+ export function getPackageVersion(metaUrl) {
11
+ if (_cached !== null)
12
+ return _cached;
13
+ // Strategy 1: 从调用者的 import.meta.url(或本模块)向上遍历找 package.json
14
+ const startFile = metaUrl ? fileURLToPath(metaUrl) : fileURLToPath(import.meta.url);
15
+ let dir = path.dirname(startFile);
16
+ const root = path.parse(dir).root;
17
+ while (dir !== root) {
18
+ const candidate = path.join(dir, "package.json");
19
+ try {
20
+ if (fs.existsSync(candidate)) {
21
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
22
+ // 确认是我们自己的包(避免找到其他 package.json)
23
+ if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
24
+ _cached = pkg.version;
25
+ return _cached;
26
+ }
27
+ }
28
+ }
29
+ catch {
30
+ // ignore and try parent
31
+ }
32
+ dir = path.dirname(dir);
33
+ }
34
+ // Strategy 2: fallback 用 createRequire 尝试常见相对路径
35
+ try {
36
+ const require = createRequire(metaUrl ?? import.meta.url);
37
+ for (const rel of ["../../package.json", "../package.json", "./package.json"]) {
38
+ try {
39
+ const pkg = require(rel);
40
+ if (pkg?.version) {
41
+ _cached = pkg.version;
42
+ return _cached;
43
+ }
44
+ }
45
+ catch { /* next */ }
46
+ }
47
+ }
48
+ catch { /* fallback */ }
49
+ _cached = "unknown";
50
+ return _cached;
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tencent-connect/openclaw-qqbot",
3
- "version": "1.6.5-alpha.2",
3
+ "version": "1.6.5-alpha.4",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,7 +22,11 @@
22
22
  "id": "openclaw-qqbot",
23
23
  "extensions": [
24
24
  "./dist/index.js"
25
- ]
25
+ ],
26
+ "channel": {
27
+ "id": "qqbot",
28
+ "label": "QQ Bot"
29
+ }
26
30
  },
27
31
  "scripts": {
28
32
  "build": "tsc || true",
@@ -242,7 +242,7 @@ if ($MissingModules.Count -gt 0) {
242
242
  $nmDir = Join-Path $STAGING_DIR "node_modules"
243
243
  if (Test-Path $nmDir) {
244
244
  $BundledOK = $true
245
- foreach ($dep in @("ws", "undici")) {
245
+ foreach ($dep in @("ws", "silk-wasm")) {
246
246
  if (-not (Test-Path (Join-Path $nmDir $dep))) {
247
247
  Write-Host " [WARN] Bundled dependency missing: $dep" -ForegroundColor Yellow
248
248
  $BundledOK = $false
@@ -273,6 +273,7 @@ Write-Host " [OK] All preflight checks passed"
273
273
  # [3/5] Replace plugin directory (in-place overwrite to avoid file-lock issues)
274
274
  Write-Host ""
275
275
  Write-Host "[3/5] Replacing plugin directory..."
276
+ if (-not (Test-Path $EXTENSIONS_DIR)) { New-Item -ItemType Directory -Path $EXTENSIONS_DIR -Force | Out-Null }
276
277
  $TARGET_DIR = Join-Path $EXTENSIONS_DIR "openclaw-qqbot"
277
278
 
278
279
  if (-not (Test-Path $TARGET_DIR)) {
@@ -1,22 +1,27 @@
1
1
  #!/bin/bash
2
2
 
3
- # qqbot 通过 npm 包升级(纯文件操作版本)
3
+ # qqbot 通过 openclaw 原生插件指令升级
4
4
  #
5
- # 默认只做文件替换,不修改 openclaw.json 配置文件。
6
- # 但如果提供了 --appid/--secret 参数(首次安装场景),
7
- # 则在文件安装完成后自动写入通道配置。
5
+ # 使用 openclaw plugins install/update 原生命令进行安装和升级,
6
+ # 保留 appid/secret 配置写入、热更新 (--no-restart)、结构化输出等功能。
7
+ #
8
+ # 升级策略:
9
+ # 1. 已安装(plugins.installs 有记录)→ openclaw plugins update
10
+ # 2. 未安装 / update 失败 → 删除旧目录 + openclaw plugins install
8
11
  #
9
12
  # 用法:
10
13
  # upgrade-via-npm.sh # 升级到 latest(默认)
11
14
  # upgrade-via-npm.sh --version <version> # 升级到指定版本
12
15
  # upgrade-via-npm.sh --self-version # 升级到当前仓库 package.json 版本
13
16
  # upgrade-via-npm.sh --appid <appid> --secret <secret> # 首次安装时配置 appid/secret
14
- # upgrade-via-npm.sh --no-restart # 只做文件替换,不重启 gateway(供热更指令使用)
17
+ # upgrade-via-npm.sh --no-restart # 只做文件替换,不重启 gateway(供热更指令使用)
15
18
 
16
19
  set -eo pipefail
17
20
 
18
21
  PKG_NAME="@tencent-connect/openclaw-qqbot"
22
+ PLUGIN_ID="openclaw-qqbot"
19
23
  INSTALL_SRC=""
24
+ TARGET_VERSION=""
20
25
  APPID=""
21
26
  SECRET=""
22
27
  NO_RESTART=false
@@ -56,16 +61,19 @@ while [[ $# -gt 0 ]]; do
56
61
  case "$1" in
57
62
  --tag)
58
63
  [ -z "$2" ] && echo "❌ --tag 需要参数" && exit 1
64
+ TARGET_VERSION="$2"
59
65
  INSTALL_SRC="${PKG_NAME}@$2"
60
66
  shift 2
61
67
  ;;
62
68
  --version)
63
69
  [ -z "$2" ] && echo "❌ --version 需要参数" && exit 1
70
+ TARGET_VERSION="$2"
64
71
  INSTALL_SRC="${PKG_NAME}@$2"
65
72
  shift 2
66
73
  ;;
67
74
  --self-version)
68
75
  [ -z "$LOCAL_VERSION" ] && echo "❌ 无法从 package.json 读取版本" && exit 1
76
+ TARGET_VERSION="$LOCAL_VERSION"
69
77
  INSTALL_SRC="${PKG_NAME}@${LOCAL_VERSION}"
70
78
  shift 1
71
79
  ;;
@@ -100,7 +108,7 @@ if [ -z "$APPID" ] && [ -z "$SECRET" ] && [ -n "$QQBOT_TOKEN" ]; then
100
108
  SECRET="${QQBOT_TOKEN#*:}"
101
109
  fi
102
110
 
103
- # 检测 CLI(仅用于确定 extensions 目录路径)
111
+ # 检测 CLI
104
112
  CMD=""
105
113
  for name in openclaw clawdbot moltbot; do
106
114
  command -v "$name" &>/dev/null && CMD="$name" && break
@@ -110,91 +118,126 @@ done
110
118
  EXTENSIONS_DIR="$HOME/.$CMD/extensions"
111
119
 
112
120
  echo "==========================================="
113
- echo " qqbot npm 升级: $INSTALL_SRC"
121
+ echo " qqbot 升级: $INSTALL_SRC"
114
122
  echo "==========================================="
115
123
  echo ""
116
124
 
117
- # [1/5] 下载并安装新版本到临时目录
118
- echo "[1/5] 下载新版本..."
119
- TMPDIR_PACK=$(mktemp -d)
120
- EXTRACT_DIR=$(mktemp -d)
121
- trap "rm -rf '$TMPDIR_PACK' '$EXTRACT_DIR'" EXIT
122
-
123
- cd "$TMPDIR_PACK"
124
- # registry fallback:npmjs.org → npmmirror(国内镜像)→ 默认 registry
125
- PACK_OK=false
126
- for _registry in "https://registry.npmjs.org/" "https://registry.npmmirror.com/" ""; do
127
- if [ -n "$_registry" ]; then
128
- echo " 尝试 registry: $_registry"
129
- npm pack "$INSTALL_SRC" --registry "$_registry" --quiet 2>&1 && PACK_OK=true && break
125
+ # 记录升级前的版本
126
+ OLD_VERSION=""
127
+ OLD_PKG="$EXTENSIONS_DIR/$PLUGIN_ID/package.json"
128
+ if [ -f "$OLD_PKG" ]; then
129
+ OLD_VERSION="$(node -e "
130
+ try {
131
+ const v = JSON.parse(require('fs').readFileSync('$OLD_PKG', 'utf8')).version;
132
+ if (v) process.stdout.write(String(v));
133
+ } catch {}
134
+ " 2>/dev/null || true)"
135
+ echo " 当前版本: ${OLD_VERSION:-unknown}"
136
+ fi
137
+
138
+ # [1/4] 通过 openclaw 原生指令安装/升级
139
+ echo ""
140
+ echo "[1/4] 安装/升级插件..."
141
+
142
+ UPGRADE_OK=false
143
+
144
+ # 检测安装状态:同时检查配置记录和磁盘目录
145
+ HAS_INSTALL_RECORD="$(node -e "
146
+ try {
147
+ const fs = require('fs');
148
+ const p = '$HOME/.$CMD/$CMD.json';
149
+ const cfg = JSON.parse(fs.readFileSync(p, 'utf8'));
150
+ const inst = cfg.plugins && cfg.plugins.installs && cfg.plugins.installs['$PLUGIN_ID'];
151
+ if (inst) process.stdout.write('yes');
152
+ } catch {}
153
+ " 2>/dev/null || true)"
154
+ HAS_PLUGIN_DIR=false
155
+ [ -d "$EXTENSIONS_DIR/$PLUGIN_ID" ] && [ -f "$EXTENSIONS_DIR/$PLUGIN_ID/package.json" ] && HAS_PLUGIN_DIR=true
156
+
157
+ # 决策矩阵:
158
+ # 配置有记录 + 目录存在 → update(最佳路径)
159
+ # 配置有记录 + 目录不存在 → 清理残留记录,走 install
160
+ # 配置无记录 + 目录存在 → 删目录,走 install(配置与文件不一致)
161
+ # 配置无记录 + 目录不存在 → 走 install(全新安装)
162
+ #
163
+ # 指定了具体版本(--version/--tag/--self-version)时:
164
+ # update 不支持指定版本,直接走 删除 + install
165
+
166
+ USE_UPDATE=false
167
+
168
+ if [ "$HAS_INSTALL_RECORD" = "yes" ] && [ "$HAS_PLUGIN_DIR" = "true" ] && [ -z "$TARGET_VERSION" ]; then
169
+ # 配置和目录都齐全,且未指定版本 → 走 update
170
+ USE_UPDATE=true
171
+ echo " [检测] 配置记录 ✓ | 插件目录 ✓ | 未指定版本 → 使用 update"
172
+ elif [ "$HAS_INSTALL_RECORD" = "yes" ] && [ "$HAS_PLUGIN_DIR" = "true" ]; then
173
+ echo " [检测] 配置记录 ✓ | 插件目录 ✓ | 指定版本 $TARGET_VERSION → 使用 reinstall"
174
+ elif [ "$HAS_INSTALL_RECORD" = "yes" ]; then
175
+ echo " [检测] 配置记录 ✓ | 插件目录 ✗ → 配置与文件不一致,使用 install"
176
+ elif [ "$HAS_PLUGIN_DIR" = "true" ]; then
177
+ echo " [检测] 配置记录 ✗ | 插件目录 ✓ → 目录残留,清理后 install"
178
+ else
179
+ echo " [检测] 配置记录 ✗ | 插件目录 ✗ → 全新安装"
180
+ fi
181
+
182
+ if [ "$USE_UPDATE" = "true" ]; then
183
+ echo " 尝试 update..."
184
+ if $CMD plugins update "$PLUGIN_ID" 2>&1; then
185
+ UPGRADE_OK=true
186
+ echo " ✅ update 成功"
130
187
  else
131
- echo " 尝试默认 registry..."
132
- npm pack "$INSTALL_SRC" --quiet 2>&1 && PACK_OK=true && break
188
+ echo " ⚠️ update 失败,回退到 reinstall..."
133
189
  fi
134
- done
135
- $PACK_OK || { echo "❌ npm pack 失败(所有 registry 均不可用)"; exit 1; }
136
- TGZ_FILE=$(ls -1 *.tgz 2>/dev/null | head -1)
137
- [ -z "$TGZ_FILE" ] && echo "❌ 未找到下载的 tgz 文件" && exit 1
138
- echo " 已下载: $TGZ_FILE"
139
-
140
- tar xzf "$TGZ_FILE" -C "$EXTRACT_DIR"
141
- PACKAGE_DIR="$EXTRACT_DIR/package"
142
- [ ! -d "$PACKAGE_DIR" ] && echo "❌ 解压失败,未找到 package 目录" && exit 1
143
-
144
- # 准备 staging 目录:放在 ~/.openclaw/ 下(extensions 的父目录),
145
- # 同一文件系统保证 mv 原子操作,同时避免 OpenClaw 扫描 extensions/ 时发现它。
146
- STAGING_DIR="$(dirname "$EXTENSIONS_DIR")/.qqbot-upgrade-staging"
147
- rm -rf "$STAGING_DIR"
148
- mkdir -p "$STAGING_DIR"
149
- cp -R "$PACKAGE_DIR/"* "$STAGING_DIR/"
150
-
151
- # 依赖处理:所有 production dependencies 都声明为 bundledDependencies,
152
- # npm pack 时已打包进 tgz,解压后 node_modules/ 已包含全部依赖,无需 npm install。
153
- # 注意:不能执行 npm install,否则会安装 peerDependencies(openclaw 平台及其 400+ 传递依赖),
154
- # 导致插件目录膨胀到 900MB+,而这些依赖在运行时由宿主 openclaw 提供。
155
- if [ -d "$STAGING_DIR/node_modules" ]; then
156
- BUNDLED_COUNT=$(ls -d "$STAGING_DIR/node_modules"/*/ "$STAGING_DIR/node_modules"/@*/*/ 2>/dev/null | wc -l | tr -d ' ')
157
- echo " bundled 依赖已就绪(${BUNDLED_COUNT} 个包)"
158
- else
159
- echo " ⚠️ 未找到 bundled node_modules,尝试安装依赖..."
160
- NPM_TMP_CACHE=$(mktemp -d)
161
- (cd "$STAGING_DIR" && npm install --omit=dev --omit=peer --ignore-scripts --cache="$NPM_TMP_CACHE" --quiet 2>&1) || echo " ⚠️ 依赖安装失败"
162
- rm -rf "$NPM_TMP_CACHE"
163
190
  fi
164
191
 
165
- # 清理下载临时文件
166
- rm -rf "$TMPDIR_PACK" "$EXTRACT_DIR"
167
- cd "$HOME"
192
+ if [ "$UPGRADE_OK" != "true" ]; then
193
+ # 清理旧目录(包含当前插件和历史遗留名称)
194
+ for dir_name in "$PLUGIN_ID" qqbot openclaw-qq; do
195
+ [ -d "$EXTENSIONS_DIR/$dir_name" ] && rm -rf "$EXTENSIONS_DIR/$dir_name" && echo " 已清理: $EXTENSIONS_DIR/$dir_name"
196
+ done
197
+
198
+ echo " 执行 install: $INSTALL_SRC"
199
+ if $CMD plugins install "$INSTALL_SRC" --pin 2>&1; then
200
+ UPGRADE_OK=true
201
+ echo " ✅ install 成功"
202
+ else
203
+ echo "❌ install 失败"
204
+ echo "QQBOT_NEW_VERSION=unknown"
205
+ echo "QQBOT_REPORT=❌ QQBot 安装失败,请检查网络和 npm registry"
206
+ exit 1
207
+ fi
208
+ fi
168
209
 
169
- # ── Preflight 检查:在写入 extensions 之前确保新包完整有效 ──
210
+ # [2/4] 验证安装
170
211
  echo ""
171
- echo "[2/5] Preflight 检查..."
212
+ echo "[2/4] 验证安装..."
213
+
214
+ NEW_VERSION="$(node -e "
215
+ try {
216
+ const fs = require('fs');
217
+ const path = require('path');
218
+ const p = path.join('$EXTENSIONS_DIR', '$PLUGIN_ID', 'package.json');
219
+ if (fs.existsSync(p)) {
220
+ const v = JSON.parse(fs.readFileSync(p, 'utf8')).version;
221
+ if (v) { process.stdout.write(v); process.exit(0); }
222
+ }
223
+ } catch {}
224
+ " 2>/dev/null || true)"
225
+
226
+ # Preflight 检查
172
227
  PREFLIGHT_OK=true
228
+ TARGET_DIR="$EXTENSIONS_DIR/$PLUGIN_ID"
173
229
 
174
- # (a) package.json 存在且可解析,且包含 version 字段
175
- STAGING_PKG="$STAGING_DIR/package.json"
176
- if [ ! -f "$STAGING_PKG" ]; then
177
- echo " ❌ 新包缺少 package.json"
230
+ if [ -z "$NEW_VERSION" ]; then
231
+ echo " ❌ 无法读取新版本号"
178
232
  PREFLIGHT_OK=false
179
233
  else
180
- STAGING_VERSION="$(node -e "
181
- try {
182
- const v = JSON.parse(require('fs').readFileSync('$STAGING_PKG', 'utf8')).version;
183
- if (v) process.stdout.write(String(v));
184
- } catch {}
185
- " 2>/dev/null || true)"
186
- if [ -z "$STAGING_VERSION" ]; then
187
- echo " ❌ package.json 无法解析或缺少 version 字段"
188
- PREFLIGHT_OK=false
189
- else
190
- echo " ✅ 版本号: $STAGING_VERSION"
191
- fi
234
+ echo " 版本号: $NEW_VERSION"
192
235
  fi
193
236
 
194
- # (b) 入口文件存在(dist/index.js 或 index.js)
237
+ # 入口文件
195
238
  ENTRY_FILE=""
196
239
  for candidate in "dist/index.js" "index.js"; do
197
- if [ -f "$STAGING_DIR/$candidate" ]; then
240
+ if [ -f "$TARGET_DIR/$candidate" ]; then
198
241
  ENTRY_FILE="$candidate"
199
242
  break
200
243
  fi
@@ -206,23 +249,23 @@ else
206
249
  echo " ✅ 入口文件: $ENTRY_FILE"
207
250
  fi
208
251
 
209
- # (c) 核心目录 dist/src 存在
210
- if [ ! -d "$STAGING_DIR/dist/src" ]; then
211
- echo " ❌ 缺少核心目录 dist/src/"
212
- PREFLIGHT_OK=false
213
- else
214
- CORE_JS_COUNT=$(find "$STAGING_DIR/dist/src" -name "*.js" -type f 2>/dev/null | wc -l | tr -d ' ')
252
+ # 核心目录
253
+ if [ -d "$TARGET_DIR/dist/src" ]; then
254
+ CORE_JS_COUNT=$(find "$TARGET_DIR/dist/src" -name "*.js" -type f 2>/dev/null | wc -l | tr -d ' ')
215
255
  echo " ✅ dist/src/ 包含 ${CORE_JS_COUNT} 个 JS 文件"
216
256
  if [ "$CORE_JS_COUNT" -lt 5 ]; then
217
257
  echo " ❌ JS 文件数量异常偏少(预期 ≥ 5,实际 ${CORE_JS_COUNT})"
218
258
  PREFLIGHT_OK=false
219
259
  fi
260
+ else
261
+ echo " ❌ 缺少核心目录 dist/src/"
262
+ PREFLIGHT_OK=false
220
263
  fi
221
264
 
222
- # (d) 关键模块文件存在
265
+ # 关键模块
223
266
  MISSING_MODULES=""
224
267
  for module in "dist/src/gateway.js" "dist/src/api.js" "dist/src/admin-resolver.js"; do
225
- if [ ! -f "$STAGING_DIR/$module" ]; then
268
+ if [ ! -f "$TARGET_DIR/$module" ]; then
226
269
  MISSING_MODULES="$MISSING_MODULES $module"
227
270
  fi
228
271
  done
@@ -233,11 +276,11 @@ else
233
276
  echo " ✅ 关键模块完整"
234
277
  fi
235
278
 
236
- # (e) bundled node_modules 健康检查
237
- if [ -d "$STAGING_DIR/node_modules" ]; then
279
+ # bundled 依赖
280
+ if [ -d "$TARGET_DIR/node_modules" ]; then
238
281
  BUNDLED_OK=true
239
- for dep in "ws" "undici"; do
240
- if [ ! -d "$STAGING_DIR/node_modules/$dep" ]; then
282
+ for dep in "ws" "silk-wasm"; do
283
+ if [ ! -d "$TARGET_DIR/node_modules/$dep" ]; then
241
284
  echo " ⚠️ bundled 依赖缺失: $dep"
242
285
  BUNDLED_OK=false
243
286
  fi
@@ -247,103 +290,20 @@ if [ -d "$STAGING_DIR/node_modules" ]; then
247
290
  fi
248
291
  fi
249
292
 
250
- # (f) 如果有旧版本,检查新版本是否合理(不允许降级到 0.x 等异常版本)
251
- if [ -n "$STAGING_VERSION" ]; then
252
- STAGING_MAJOR="$(echo "$STAGING_VERSION" | cut -d. -f1)"
253
- if [ "$STAGING_MAJOR" = "0" ]; then
254
- echo " ⚠️ 新版本主版本号为 0($STAGING_VERSION),可能不是正式发布版"
255
- fi
256
- fi
257
-
258
- # 检查结果
259
293
  if [ "$PREFLIGHT_OK" != "true" ]; then
260
294
  echo ""
261
- echo "❌ Preflight 检查未通过,中止升级(旧版本未受影响)"
262
- rm -rf "$STAGING_DIR"
295
+ echo "❌ 验证未通过"
296
+ echo "QQBOT_NEW_VERSION=unknown"
297
+ echo "QQBOT_REPORT=⚠️ QQBot 升级异常,验证未通过"
263
298
  exit 1
264
299
  fi
265
- echo " ✅ Preflight 检查全部通过"
300
+ echo " ✅ 验证全部通过"
266
301
 
267
- # [3/5] 原子替换:使用 mv -T/rename 确保目录切换尽可能原子
268
- # 策略:先把 staging 放到 extensions/ 同级的临时名,再做单次 mv 替换
302
+ # [3/4] 输出结构化信息(供 TS handler 解析)
269
303
  echo ""
270
- echo "[3/5] 原子替换插件目录..."
271
- TARGET_DIR="$EXTENSIONS_DIR/openclaw-qqbot"
272
- OLD_DIR="$(dirname "$EXTENSIONS_DIR")/.qqbot-upgrade-old"
273
-
274
- rm -rf "$OLD_DIR"
275
-
276
- # 先把 staging 目录移到 extensions/ 下的临时位置(同文件系统,确保 mv 是 rename 操作)
277
- STAGING_IN_EXT="$EXTENSIONS_DIR/.openclaw-qqbot-new"
278
- rm -rf "$STAGING_IN_EXT"
279
- mv "$STAGING_DIR" "$STAGING_IN_EXT"
280
-
281
- if [ -d "$TARGET_DIR" ]; then
282
- # 使用连续两个 mv 但中间零操作,最小化目录不存在的时间窗口
283
- mv "$TARGET_DIR" "$OLD_DIR" && mv "$STAGING_IN_EXT" "$TARGET_DIR"
284
- else
285
- mv "$STAGING_IN_EXT" "$TARGET_DIR"
286
- fi
287
- rm -rf "$OLD_DIR"
288
-
289
- # 清理可能残留的旧版 staging 目录(extensions 内外都清理)
290
- rm -rf "$EXTENSIONS_DIR/openclaw-qqbot.staging"
291
- rm -rf "$EXTENSIONS_DIR/.qqbot-upgrade-staging"
292
- rm -rf "$EXTENSIONS_DIR/.qqbot-upgrade-old"
293
-
294
- # 同时清理历史遗留的其他目录名
295
- for dir_name in qqbot openclaw-qq; do
296
- [ -d "$EXTENSIONS_DIR/$dir_name" ] && rm -rf "$EXTENSIONS_DIR/$dir_name"
297
- done
298
- echo " 已安装到: $TARGET_DIR"
299
-
300
- # 执行 postinstall 脚本创建 openclaw SDK symlink
301
- # (upgrade-via-npm 是纯文件操作,不走 npm install,所以 postinstall 不会自动触发)
302
- POSTINSTALL_SCRIPT="$TARGET_DIR/scripts/postinstall-link-sdk.js"
303
- if [ -f "$POSTINSTALL_SCRIPT" ]; then
304
- echo " 执行 postinstall: 创建 openclaw SDK symlink..."
305
- POSTINSTALL_OUTPUT="$(cd "$TARGET_DIR" && node "$POSTINSTALL_SCRIPT" 2>&1)" || true
306
- [ -n "$POSTINSTALL_OUTPUT" ] && echo " $POSTINSTALL_OUTPUT"
307
- # 验证 symlink 是否创建成功
308
- if [ -d "$TARGET_DIR/node_modules/openclaw" ]; then
309
- echo " ✅ openclaw SDK symlink 已就绪"
310
- else
311
- echo " ⚠️ openclaw SDK symlink 未创建,插件可能无法加载"
312
- echo " 尝试手动创建 symlink..."
313
- # 手动 fallback:尝试从 CLI 数据目录名推断全局包名
314
- _CLI_DATA_DIR="$(dirname "$EXTENSIONS_DIR")"
315
- _CLI_NAME="$(basename "$_CLI_DATA_DIR" | sed 's/^\.//')"
316
- _GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"
317
- if [ -n "$_GLOBAL_ROOT" ] && [ -n "$_CLI_NAME" ] && [ -d "$_GLOBAL_ROOT/$_CLI_NAME" ]; then
318
- mkdir -p "$TARGET_DIR/node_modules"
319
- ln -sf "$_GLOBAL_ROOT/$_CLI_NAME" "$TARGET_DIR/node_modules/openclaw" 2>/dev/null && \
320
- echo " ✅ 手动 symlink 创建成功: -> $_GLOBAL_ROOT/$_CLI_NAME" || \
321
- echo " ❌ 手动 symlink 创建也失败了"
322
- else
323
- echo " ❌ 无法定位全局 $_CLI_NAME 安装路径(npm root -g: $_GLOBAL_ROOT)"
324
- fi
325
- fi
326
- else
327
- echo " ⚠️ 未找到 postinstall 脚本,跳过 symlink 创建"
328
- fi
329
-
330
- # [4/5] 输出新版本号和升级报告(供调用方解析)
331
- echo ""
332
- echo "[4/5] 验证安装..."
333
- NEW_VERSION="$(node -e "
334
- try {
335
- const fs = require('fs');
336
- const path = require('path');
337
- const p = path.join('$EXTENSIONS_DIR', 'openclaw-qqbot', 'package.json');
338
- if (fs.existsSync(p)) {
339
- const v = JSON.parse(fs.readFileSync(p, 'utf8')).version;
340
- if (v) { process.stdout.write(v); process.exit(0); }
341
- }
342
- } catch {}
343
- " 2>/dev/null || true)"
304
+ echo "[3/4] 升级结果..."
344
305
  echo "QQBOT_NEW_VERSION=${NEW_VERSION:-unknown}"
345
306
 
346
- # 输出结构化升级报告(QQBOT_REPORT=...),供 TS handler 解析后直接回复用户
347
307
  if [ -n "$NEW_VERSION" ] && [ "$NEW_VERSION" != "unknown" ]; then
348
308
  echo "QQBOT_REPORT=✅ QQBot 升级完成: v${NEW_VERSION}"
349
309
  else
@@ -352,13 +312,10 @@ fi
352
312
 
353
313
  echo ""
354
314
  echo "==========================================="
355
- echo " ✅ 文件安装完成"
315
+ echo " ✅ 安装完成"
356
316
  echo "==========================================="
357
317
 
358
- # --no-restart 模式(热更新场景):文件替换完成后立即退出,
359
- # 让调用方尽快触发 gateway restart,避免 openclaw 配置轮询
360
- # 在旧进程中检测到插件变更产生 "plugin not found" warning 刷屏。
361
- # appid/secret 配置在热更新场景下已经存在,无需重新写入。
318
+ # --no-restart 模式(热更新场景):立即退出,让调用方触发 gateway restart
362
319
  if [ "$NO_RESTART" = "true" ]; then
363
320
  echo ""
364
321
  echo "[跳过重启] --no-restart 已指定,脚本立即退出以便调用方触发 gateway restart"
@@ -394,10 +351,9 @@ if [ -n "$APPID" ] && [ -n "$SECRET" ]; then
394
351
 
395
352
  if [ "$CURRENT_TOKEN" = "$DESIRED_TOKEN" ]; then
396
353
  echo " ✅ 当前配置已是目标值,跳过写入"
397
- elif $CMD channels add --channel qqbot --token "$DESIRED_TOKEN" 2>&1; then
398
- echo " ✅ 通道配置写入成功"
399
354
  else
400
- echo " ⚠️ $CMD channels add 失败,尝试直接编辑配置文件..."
355
+ # qqbot 是插件自定义通道,openclaw channels add --channel 不支持,
356
+ # 直接编辑配置文件写入 channels.qqbot
401
357
  CONFIG_FILE="$HOME/.$CMD/$CMD.json"
402
358
  if [ -f "$CONFIG_FILE" ] && node -e "
403
359
  const fs = require('fs');
@@ -408,10 +364,10 @@ if [ -n "$APPID" ] && [ -n "$SECRET" ]; then
408
364
  cfg.channels.qqbot.clientSecret = '$SECRET';
409
365
  fs.writeFileSync('$CONFIG_FILE', JSON.stringify(cfg, null, 4) + '\n');
410
366
  " 2>&1; then
411
- echo " ✅ 通道配置写入成功(直接编辑配置文件)"
367
+ echo " ✅ 通道配置写入成功"
412
368
  else
413
- echo " ❌ 配置写入失败,请手动配置:"
414
- echo " $CMD channels add --channel qqbot --token \"${APPID}:${SECRET}\""
369
+ echo " ❌ 配置写入失败,请手动编辑 $CONFIG_FILE 添加 channels.qqbot:"
370
+ echo " { \"channels\": { \"qqbot\": { \"appId\": \"$APPID\", \"clientSecret\": \"...\" } } }"
415
371
  fi
416
372
  fi
417
373
  elif [ -n "$APPID" ] || [ -n "$SECRET" ]; then
@@ -419,11 +375,10 @@ elif [ -n "$APPID" ] || [ -n "$SECRET" ]; then
419
375
  echo "⚠️ --appid 和 --secret 必须同时提供"
420
376
  fi
421
377
 
422
- # [5/5] 重启 gateway 使新版本生效
378
+ # [4/4] 重启 gateway 使新版本生效
423
379
  echo ""
424
380
 
425
381
  # 手动升级场景:提前写入 startup-marker,阻止重启后 bot 重复推送升级通知
426
- # (控制台已打印同款提示语,无需 bot 再发一次)
427
382
  if [ -n "$NEW_VERSION" ] && [ "$NEW_VERSION" != "unknown" ]; then
428
383
  MARKER_DIR="$HOME/.openclaw/qqbot/data"
429
384
  mkdir -p "$MARKER_DIR"
@@ -435,7 +390,6 @@ fi
435
390
  echo "[重启] 重启 gateway 使新版本生效..."
436
391
  if $CMD gateway restart 2>&1; then
437
392
  echo " ✅ gateway 已重启"
438
- # 打印与 bot 通知同款的更新提示语(手动升级场景无需通过 bot 推送)
439
393
  echo ""
440
394
  if [ -n "$NEW_VERSION" ] && [ "$NEW_VERSION" != "unknown" ]; then
441
395
  echo "🎉 QQBot 插件已更新至 v${NEW_VERSION},在线等候你的吩咐。"
package/src/api.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  * [修复版] 已重构为支持多实例并发,消除全局变量冲突
4
4
  */
5
5
 
6
- import { createRequire } from "node:module";
7
6
  import os from "node:os";
8
7
  import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./utils/upload-cache.js";
9
8
  import { sanitizeFileName } from "./utils/platform.js";
@@ -14,9 +13,8 @@ const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
14
13
  // ============ Plugin User-Agent ============
15
14
  // 格式: QQBotPlugin/{version} (Node/{nodeVersion}; {os})
16
15
  // 示例: QQBotPlugin/1.6.0 (Node/22.14.0; darwin)
17
- const _require = createRequire(import.meta.url);
18
- let _pluginVersion = "unknown";
19
- try { _pluginVersion = _require("../package.json").version ?? "unknown"; } catch { /* fallback */ }
16
+ import { getPackageVersion } from "./utils/pkg-version.js";
17
+ const _pluginVersion = getPackageVersion(import.meta.url);
20
18
  export const PLUGIN_USER_AGENT = `QQBotPlugin/${_pluginVersion} (Node/${process.versions.node}; ${os.platform()})`;
21
19
 
22
20
  // 运行时配置
@@ -285,22 +283,48 @@ export async function apiRequest<T = unknown>(
285
283
  const traceId = res.headers.get("x-tps-trace-id") ?? "";
286
284
  console.log(`[qqbot-api] <<< Status: ${res.status} ${res.statusText}${traceId ? ` | TraceId: ${traceId}` : ""}`);
287
285
 
288
- let data: T;
289
286
  let rawBody: string;
290
287
  try {
291
288
  rawBody = await res.text();
292
- console.log(`[qqbot-api] <<< Body:`, rawBody);
293
- data = JSON.parse(rawBody) as T;
294
289
  } catch (err) {
295
- throw new Error(`Failed to parse response[${path}]: ${err instanceof Error ? err.message : String(err)}`);
290
+ throw new Error(`读取响应失败[${path}]: ${err instanceof Error ? err.message : String(err)}`);
296
291
  }
292
+ console.log(`[qqbot-api] <<< Body:`, rawBody);
293
+
294
+ // 检测非 JSON 响应(HTML 网关错误页 / CDN 限流页等)
295
+ const contentType = res.headers.get("content-type") ?? "";
296
+ const isHtmlResponse = contentType.includes("text/html") || rawBody.trimStart().startsWith("<");
297
297
 
298
298
  if (!res.ok) {
299
- const error = data as { message?: string; code?: number };
300
- throw new Error(`API Error [${path}]: ${error.message ?? JSON.stringify(data)}`);
299
+ if (isHtmlResponse) {
300
+ // HTML 响应 = 网关/限流层返回的错误页,给出友好提示
301
+ const statusHint = res.status === 502 || res.status === 503 || res.status === 504
302
+ ? "调用发生异常,请稍候重试"
303
+ : res.status === 429
304
+ ? "请求过于频繁,已被限流"
305
+ : `开放平台返回 HTTP ${res.status}`;
306
+ throw new Error(`${statusHint}(${path}),请稍后重试`);
307
+ }
308
+ // JSON 错误响应
309
+ try {
310
+ const error = JSON.parse(rawBody) as { message?: string; code?: number };
311
+ throw new Error(`API Error [${path}]: ${error.message ?? rawBody}`);
312
+ } catch (parseErr) {
313
+ if (parseErr instanceof Error && parseErr.message.startsWith("API Error")) throw parseErr;
314
+ throw new Error(`API Error [${path}] HTTP ${res.status}: ${rawBody.slice(0, 200)}`);
315
+ }
301
316
  }
302
317
 
303
- return data;
318
+ // 成功响应但不是 JSON(极端异常情况)
319
+ if (isHtmlResponse) {
320
+ throw new Error(`QQ 服务端返回了非 JSON 响应(${path}),可能是临时故障,请稍后重试`);
321
+ }
322
+
323
+ try {
324
+ return JSON.parse(rawBody) as T;
325
+ } catch {
326
+ throw new Error(`开放平台响应格式异常(${path}),请稍后重试`);
327
+ }
304
328
  }
305
329
 
306
330
  // ============ 上传重试(指数退避) ============
@@ -20,16 +20,10 @@ import { getUpdateInfo, checkVersionExists } from "./update-checker.js";
20
20
  import { getHomeDir, getQQBotDataDir, isWindows } from "./utils/platform.js";
21
21
  import { saveCredentialBackup } from "./credential-backup.js";
22
22
  import { fileURLToPath } from "node:url";
23
+ import { getPackageVersion } from "./utils/pkg-version.js";
23
24
  const require = createRequire(import.meta.url);
24
25
 
25
- // 读取 package.json 中的版本号
26
- let PLUGIN_VERSION = "unknown";
27
- try {
28
- const pkg = require("../package.json");
29
- PLUGIN_VERSION = pkg.version ?? "unknown";
30
- } catch {
31
- // fallback
32
- }
26
+ let PLUGIN_VERSION = getPackageVersion(import.meta.url);
33
27
 
34
28
  // 获取 openclaw 框架版本(缓存结果,只执行一次)
35
29
  let _frameworkVersion: string | null = null;
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { createRequire } from "node:module";
12
12
  import https from "node:https";
13
+ import { getPackageVersion } from "./utils/pkg-version.js";
13
14
 
14
15
  const require = createRequire(import.meta.url);
15
16
 
@@ -21,13 +22,7 @@ const REGISTRIES = [
21
22
  `https://registry.npmmirror.com/${ENCODED_PKG}`,
22
23
  ];
23
24
 
24
- let CURRENT_VERSION = "unknown";
25
- try {
26
- const pkg = require("../package.json");
27
- CURRENT_VERSION = pkg.version ?? "unknown";
28
- } catch {
29
- // fallback
30
- }
25
+ let CURRENT_VERSION = getPackageVersion(import.meta.url);
31
26
 
32
27
  export interface UpdateInfo {
33
28
  current: string;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * 从 import.meta.url 向上遍历目录树查找 package.json 并读取 version。
3
+ * 不依赖硬编码的 "../" 层级,无论编译输出结构如何变化都能可靠找到。
4
+ */
5
+
6
+ import { fileURLToPath } from "node:url";
7
+ import { createRequire } from "node:module";
8
+ import path from "node:path";
9
+ import fs from "node:fs";
10
+
11
+ let _cached: string | null = null;
12
+
13
+ export function getPackageVersion(metaUrl?: string): string {
14
+ if (_cached !== null) return _cached;
15
+
16
+ // Strategy 1: 从调用者的 import.meta.url(或本模块)向上遍历找 package.json
17
+ const startFile = metaUrl ? fileURLToPath(metaUrl) : fileURLToPath(import.meta.url);
18
+ let dir = path.dirname(startFile);
19
+ const root = path.parse(dir).root;
20
+
21
+ while (dir !== root) {
22
+ const candidate = path.join(dir, "package.json");
23
+ try {
24
+ if (fs.existsSync(candidate)) {
25
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
26
+ // 确认是我们自己的包(避免找到其他 package.json)
27
+ if (pkg.name === "@tencent-connect/openclaw-qqbot" && pkg.version) {
28
+ _cached = pkg.version as string;
29
+ return _cached;
30
+ }
31
+ }
32
+ } catch {
33
+ // ignore and try parent
34
+ }
35
+ dir = path.dirname(dir);
36
+ }
37
+
38
+ // Strategy 2: fallback 用 createRequire 尝试常见相对路径
39
+ try {
40
+ const require = createRequire(metaUrl ?? import.meta.url);
41
+ for (const rel of ["../../package.json", "../package.json", "./package.json"]) {
42
+ try {
43
+ const pkg = require(rel);
44
+ if (pkg?.version) {
45
+ _cached = pkg.version as string;
46
+ return _cached;
47
+ }
48
+ } catch { /* next */ }
49
+ }
50
+ } catch { /* fallback */ }
51
+
52
+ _cached = "unknown";
53
+ return _cached;
54
+ }