@tencent-connect/openclaw-qqbot 1.6.5-alpha.1 → 1.6.5-alpha.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
@@ -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.1",
3
+ "version": "1.6.5-alpha.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  // globally installed openclaw package, allowing Node's native ESM resolver
11
11
  // (used by jiti with tryNative:true for .js files) to find `openclaw/plugin-sdk`.
12
12
 
13
- import { existsSync, symlinkSync, mkdirSync, readlinkSync } from "node:fs";
13
+ import { existsSync, symlinkSync, mkdirSync, realpathSync } from "node:fs";
14
14
  import { dirname, join, resolve } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { execSync } from "node:child_process";
@@ -18,8 +18,9 @@ import { execSync } from "node:child_process";
18
18
  const __dirname = dirname(fileURLToPath(import.meta.url));
19
19
  const pluginRoot = resolve(__dirname, "..");
20
20
 
21
- // Only run when installed under .openclaw/extensions/
22
- if (!pluginRoot.includes(".openclaw") && !pluginRoot.includes("extensions")) {
21
+ // Only run when installed under an openclaw-like extensions directory
22
+ // (supports openclaw, clawdbot, moltbot, etc.)
23
+ if (!pluginRoot.includes("extensions")) {
23
24
  process.exit(0);
24
25
  }
25
26
 
@@ -30,37 +31,83 @@ if (existsSync(linkTarget)) {
30
31
  process.exit(0);
31
32
  }
32
33
 
34
+ // CLI names to try (openclaw and its aliases)
35
+ const CLI_NAMES = ["openclaw", "clawdbot", "moltbot"];
36
+
33
37
  // Find the global openclaw installation
34
38
  let openclawRoot = null;
35
- try {
36
- // Try require.resolve from global context
37
- const globalRoot = execSync("npm root -g", { encoding: "utf-8" }).trim();
38
- const candidate = join(globalRoot, "openclaw");
39
- if (existsSync(join(candidate, "package.json"))) {
40
- openclawRoot = candidate;
41
- }
42
- } catch {}
43
39
 
40
+ // Strategy 1: npm root -g → look for any known CLI package name
44
41
  if (!openclawRoot) {
45
42
  try {
46
- // Try resolving from the openclaw CLI binary
47
- const bin = execSync("which openclaw", { encoding: "utf-8" }).trim();
48
- // bin is typically <prefix>/bin/openclaw -> ../lib/node_modules/openclaw/...
49
- const candidate = resolve(dirname(bin), "..", "lib", "node_modules", "openclaw");
50
- if (existsSync(join(candidate, "package.json"))) {
51
- openclawRoot = candidate;
43
+ const globalRoot = execSync("npm root -g", { encoding: "utf-8" }).trim();
44
+ for (const name of CLI_NAMES) {
45
+ const candidate = join(globalRoot, name);
46
+ if (existsSync(join(candidate, "package.json"))) {
47
+ openclawRoot = candidate;
48
+ break;
49
+ }
52
50
  }
53
51
  } catch {}
54
52
  }
55
53
 
54
+ // Strategy 2: resolve from the CLI binary (which openclaw / clawdbot / moltbot)
55
+ if (!openclawRoot) {
56
+ const whichCmd = process.platform === "win32" ? "where" : "which";
57
+ for (const name of CLI_NAMES) {
58
+ try {
59
+ const bin = execSync(`${whichCmd} ${name}`, { encoding: "utf-8" }).trim().split("\n")[0];
60
+ if (!bin) continue;
61
+ // Resolve symlinks to get actual binary location
62
+ const realBin = realpathSync(bin);
63
+ // bin is typically <prefix>/bin/<name> -> ../lib/node_modules/<name>/...
64
+ const candidate = resolve(dirname(realBin), "..", "lib", "node_modules", name);
65
+ if (existsSync(join(candidate, "package.json"))) {
66
+ openclawRoot = candidate;
67
+ break;
68
+ }
69
+ // Also try: binary might be inside the package itself (e.g. .../node_modules/<name>/bin/<name>)
70
+ const candidate2 = resolve(dirname(realBin), "..");
71
+ if (existsSync(join(candidate2, "package.json")) && existsSync(join(candidate2, "plugin-sdk"))) {
72
+ openclawRoot = candidate2;
73
+ break;
74
+ }
75
+ } catch {}
76
+ }
77
+ }
78
+
79
+ // Strategy 3: walk up from the extensions directory to find the CLI's data root,
80
+ // then look for a global node_modules sibling
81
+ if (!openclawRoot) {
82
+ // pluginRoot is like /home/user/.openclaw/extensions/openclaw-qqbot
83
+ // The CLI data dir is /home/user/.openclaw (or .clawdbot, .moltbot)
84
+ const extensionsDir = dirname(pluginRoot);
85
+ const dataDir = dirname(extensionsDir);
86
+ const dataDirName = dataDir.split("/").pop() || dataDir.split("\\").pop() || "";
87
+ // dataDirName is like ".openclaw" → strip the dot to get "openclaw"
88
+ const cliName = dataDirName.replace(/^\./, "");
89
+ if (cliName) {
90
+ try {
91
+ const globalRoot = execSync("npm root -g", { encoding: "utf-8" }).trim();
92
+ const candidate = join(globalRoot, cliName);
93
+ if (existsSync(join(candidate, "package.json"))) {
94
+ openclawRoot = candidate;
95
+ }
96
+ } catch {}
97
+ }
98
+ }
99
+
56
100
  if (!openclawRoot) {
57
101
  // Not fatal — plugin may work if openclaw loads it with proper alias resolution
102
+ // But log a warning so upgrade scripts can detect the failure
103
+ console.error("[postinstall-link-sdk] WARNING: could not find openclaw/clawdbot/moltbot global installation, symlink not created");
58
104
  process.exit(0);
59
105
  }
60
106
 
61
107
  try {
62
108
  mkdirSync(join(pluginRoot, "node_modules"), { recursive: true });
63
109
  symlinkSync(openclawRoot, linkTarget, "junction");
64
- } catch {
65
- // Silently ignore — symlink creation may fail on some systems
110
+ console.log(`[postinstall-link-sdk] symlink created: node_modules/openclaw -> ${openclawRoot}`);
111
+ } catch (e) {
112
+ console.error(`[postinstall-link-sdk] WARNING: symlink creation failed: ${e.message}`);
66
113
  }
@@ -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)) {
@@ -315,6 +316,47 @@ foreach ($legacyName in @("qqbot", "openclaw-qq")) {
315
316
  }
316
317
  Write-Host " Installed to: $TARGET_DIR"
317
318
 
319
+ # Execute postinstall script to create openclaw SDK symlink
320
+ # (upgrade-via-npm is pure file operation, npm install is not run, so postinstall won't trigger automatically)
321
+ $PostinstallScript = Join-Path $TARGET_DIR "scripts" "postinstall-link-sdk.js"
322
+ if (Test-Path $PostinstallScript) {
323
+ Write-Host " Running postinstall: creating openclaw SDK symlink..."
324
+ try {
325
+ Push-Location $TARGET_DIR
326
+ $postOutput = & node $PostinstallScript 2>&1
327
+ Pop-Location
328
+ if ($postOutput) { Write-Host " $postOutput" }
329
+ } catch {
330
+ Write-Host " [WARN] postinstall script failed (non-fatal)" -ForegroundColor Yellow
331
+ try { Pop-Location } catch {}
332
+ }
333
+ # Verify symlink creation
334
+ $symlinkPath = Join-Path $TARGET_DIR "node_modules" "openclaw"
335
+ if (Test-Path $symlinkPath) {
336
+ Write-Host " [OK] openclaw SDK symlink ready"
337
+ } else {
338
+ Write-Host " [WARN] openclaw SDK symlink not created, attempting manual fallback..." -ForegroundColor Yellow
339
+ $cliDataDir = Split-Path $EXTENSIONS_DIR -Parent
340
+ $cliName = (Split-Path $cliDataDir -Leaf) -replace '^\.',''
341
+ try {
342
+ $globalRoot = (& npm root -g 2>$null).Trim()
343
+ $globalPkg = Join-Path $globalRoot $cliName
344
+ if ($globalRoot -and (Test-Path $globalPkg)) {
345
+ $nmDir = Join-Path $TARGET_DIR "node_modules"
346
+ if (-not (Test-Path $nmDir)) { New-Item -ItemType Directory -Path $nmDir -Force | Out-Null }
347
+ New-Item -ItemType Junction -Path $symlinkPath -Target $globalPkg -Force | Out-Null
348
+ Write-Host " [OK] Manual symlink created: -> $globalPkg"
349
+ } else {
350
+ Write-Host " [ERROR] Cannot locate global $cliName installation (npm root -g: $globalRoot)" -ForegroundColor Red
351
+ }
352
+ } catch {
353
+ Write-Host " [ERROR] Manual symlink creation also failed: $($_.Exception.Message)" -ForegroundColor Red
354
+ }
355
+ }
356
+ } else {
357
+ Write-Host " [WARN] postinstall script not found, skipping symlink creation" -ForegroundColor Yellow
358
+ }
359
+
318
360
  # [4/5] Verify installation
319
361
  Write-Host ""
320
362
  Write-Host "[4/5] Verifying installation..."
@@ -236,7 +236,7 @@ fi
236
236
  # (e) bundled node_modules 健康检查
237
237
  if [ -d "$STAGING_DIR/node_modules" ]; then
238
238
  BUNDLED_OK=true
239
- for dep in "ws" "undici"; do
239
+ for dep in "ws" "silk-wasm"; do
240
240
  if [ ! -d "$STAGING_DIR/node_modules/$dep" ]; then
241
241
  echo " ⚠️ bundled 依赖缺失: $dep"
242
242
  BUNDLED_OK=false
@@ -268,6 +268,7 @@ echo " ✅ Preflight 检查全部通过"
268
268
  # 策略:先把 staging 放到 extensions/ 同级的临时名,再做单次 mv 替换
269
269
  echo ""
270
270
  echo "[3/5] 原子替换插件目录..."
271
+ mkdir -p "$EXTENSIONS_DIR"
271
272
  TARGET_DIR="$EXTENSIONS_DIR/openclaw-qqbot"
272
273
  OLD_DIR="$(dirname "$EXTENSIONS_DIR")/.qqbot-upgrade-old"
273
274
 
@@ -297,6 +298,36 @@ for dir_name in qqbot openclaw-qq; do
297
298
  done
298
299
  echo " 已安装到: $TARGET_DIR"
299
300
 
301
+ # 执行 postinstall 脚本创建 openclaw SDK symlink
302
+ # (upgrade-via-npm 是纯文件操作,不走 npm install,所以 postinstall 不会自动触发)
303
+ POSTINSTALL_SCRIPT="$TARGET_DIR/scripts/postinstall-link-sdk.js"
304
+ if [ -f "$POSTINSTALL_SCRIPT" ]; then
305
+ echo " 执行 postinstall: 创建 openclaw SDK symlink..."
306
+ POSTINSTALL_OUTPUT="$(cd "$TARGET_DIR" && node "$POSTINSTALL_SCRIPT" 2>&1)" || true
307
+ [ -n "$POSTINSTALL_OUTPUT" ] && echo " $POSTINSTALL_OUTPUT"
308
+ # 验证 symlink 是否创建成功
309
+ if [ -d "$TARGET_DIR/node_modules/openclaw" ]; then
310
+ echo " ✅ openclaw SDK symlink 已就绪"
311
+ else
312
+ echo " ⚠️ openclaw SDK symlink 未创建,插件可能无法加载"
313
+ echo " 尝试手动创建 symlink..."
314
+ # 手动 fallback:尝试从 CLI 数据目录名推断全局包名
315
+ _CLI_DATA_DIR="$(dirname "$EXTENSIONS_DIR")"
316
+ _CLI_NAME="$(basename "$_CLI_DATA_DIR" | sed 's/^\.//')"
317
+ _GLOBAL_ROOT="$(npm root -g 2>/dev/null || true)"
318
+ if [ -n "$_GLOBAL_ROOT" ] && [ -n "$_CLI_NAME" ] && [ -d "$_GLOBAL_ROOT/$_CLI_NAME" ]; then
319
+ mkdir -p "$TARGET_DIR/node_modules"
320
+ ln -sf "$_GLOBAL_ROOT/$_CLI_NAME" "$TARGET_DIR/node_modules/openclaw" 2>/dev/null && \
321
+ echo " ✅ 手动 symlink 创建成功: -> $_GLOBAL_ROOT/$_CLI_NAME" || \
322
+ echo " ❌ 手动 symlink 创建也失败了"
323
+ else
324
+ echo " ❌ 无法定位全局 $_CLI_NAME 安装路径(npm root -g: $_GLOBAL_ROOT)"
325
+ fi
326
+ fi
327
+ else
328
+ echo " ⚠️ 未找到 postinstall 脚本,跳过 symlink 创建"
329
+ fi
330
+
300
331
  # [4/5] 输出新版本号和升级报告(供调用方解析)
301
332
  echo ""
302
333
  echo "[4/5] 验证安装..."
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
+ }