@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 +36 -14
- package/dist/src/slash-commands.js +2 -9
- package/dist/src/update-checker.js +2 -8
- package/dist/src/utils/pkg-version.d.ts +5 -0
- package/dist/src/utils/pkg-version.js +51 -0
- package/package.json +1 -1
- package/scripts/postinstall-link-sdk.js +66 -19
- package/scripts/upgrade-via-npm.ps1 +43 -1
- package/scripts/upgrade-via-npm.sh +32 -1
- package/src/api.ts +35 -11
- package/src/slash-commands.ts +2 -8
- package/src/update-checker.ts +2 -7
- package/src/utils/pkg-version.ts +54 -0
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
|
-
|
|
15
|
-
|
|
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(
|
|
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
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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 =
|
|
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,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
|
@@ -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,
|
|
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
|
|
22
|
-
|
|
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
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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", "
|
|
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" "
|
|
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
|
-
|
|
18
|
-
|
|
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(
|
|
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
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
// ============ 上传重试(指数退避) ============
|
package/src/slash-commands.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/update-checker.ts
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|