@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 +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 +6 -2
- package/scripts/upgrade-via-npm.ps1 +2 -1
- package/scripts/upgrade-via-npm.sh +143 -189
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tencent-connect/openclaw-qqbot",
|
|
3
|
-
"version": "1.6.5-alpha.
|
|
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", "
|
|
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 通过
|
|
3
|
+
# qqbot 通过 openclaw 原生插件指令升级
|
|
4
4
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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
|
|
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
|
|
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
|
|
121
|
+
echo " qqbot 升级: $INSTALL_SRC"
|
|
114
122
|
echo "==========================================="
|
|
115
123
|
echo ""
|
|
116
124
|
|
|
117
|
-
#
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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 "
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
#
|
|
210
|
+
# [2/4] 验证安装
|
|
170
211
|
echo ""
|
|
171
|
-
echo "[2/
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
237
|
+
# 入口文件
|
|
195
238
|
ENTRY_FILE=""
|
|
196
239
|
for candidate in "dist/index.js" "index.js"; do
|
|
197
|
-
if [ -f "$
|
|
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
|
-
#
|
|
210
|
-
if [
|
|
211
|
-
|
|
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
|
-
#
|
|
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 "$
|
|
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
|
-
#
|
|
237
|
-
if [ -d "$
|
|
279
|
+
# bundled 依赖
|
|
280
|
+
if [ -d "$TARGET_DIR/node_modules" ]; then
|
|
238
281
|
BUNDLED_OK=true
|
|
239
|
-
for dep in "ws" "
|
|
240
|
-
if [ ! -d "$
|
|
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 "❌
|
|
262
|
-
|
|
295
|
+
echo "❌ 验证未通过"
|
|
296
|
+
echo "QQBOT_NEW_VERSION=unknown"
|
|
297
|
+
echo "QQBOT_REPORT=⚠️ QQBot 升级异常,验证未通过"
|
|
263
298
|
exit 1
|
|
264
299
|
fi
|
|
265
|
-
echo " ✅
|
|
300
|
+
echo " ✅ 验证全部通过"
|
|
266
301
|
|
|
267
|
-
# [3/
|
|
268
|
-
# 策略:先把 staging 放到 extensions/ 同级的临时名,再做单次 mv 替换
|
|
302
|
+
# [3/4] 输出结构化信息(供 TS handler 解析)
|
|
269
303
|
echo ""
|
|
270
|
-
echo "[3/
|
|
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
|
-
|
|
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 "
|
|
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
|
-
# [
|
|
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
|
-
|
|
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
|
+
}
|