@ryantest/openclaw-qqbot 0.0.3 → 1.6.6-alpha.0
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/README.md +2 -15
- package/README.zh.md +3 -16
- package/dist/src/admin-resolver.d.ts +12 -6
- package/dist/src/admin-resolver.js +69 -34
- package/dist/src/api.d.ts +105 -1
- package/dist/src/api.js +164 -15
- package/dist/src/channel.js +13 -0
- package/dist/src/config.js +3 -10
- package/dist/src/deliver-debounce.d.ts +74 -0
- package/dist/src/deliver-debounce.js +174 -0
- package/dist/src/gateway.js +450 -248
- package/dist/src/image-server.d.ts +27 -8
- package/dist/src/image-server.js +179 -71
- package/dist/src/inbound-attachments.d.ts +3 -1
- package/dist/src/inbound-attachments.js +28 -14
- package/dist/src/outbound-deliver.js +77 -148
- package/dist/src/outbound.d.ts +6 -4
- package/dist/src/outbound.js +266 -442
- package/dist/src/reply-dispatcher.js +4 -4
- package/dist/src/request-context.d.ts +18 -0
- package/dist/src/request-context.js +30 -0
- package/dist/src/slash-commands.js +277 -32
- package/dist/src/startup-greeting.d.ts +5 -5
- package/dist/src/startup-greeting.js +32 -13
- package/dist/src/streaming.d.ts +244 -0
- package/dist/src/streaming.js +907 -0
- package/dist/src/tools/remind.js +11 -10
- package/dist/src/types.d.ts +101 -0
- package/dist/src/types.js +17 -1
- package/dist/src/update-checker.js +2 -8
- package/dist/src/utils/audio-convert.d.ts +9 -0
- package/dist/src/utils/audio-convert.js +51 -0
- package/dist/src/utils/chunked-upload.d.ts +59 -0
- package/dist/src/utils/chunked-upload.js +289 -0
- package/dist/src/utils/file-utils.d.ts +7 -1
- package/dist/src/utils/file-utils.js +24 -2
- package/dist/src/utils/media-send.d.ts +147 -0
- package/dist/src/utils/media-send.js +434 -0
- package/dist/src/utils/pkg-version.d.ts +5 -0
- package/dist/src/utils/pkg-version.js +51 -0
- package/dist/src/utils/ssrf-guard.d.ts +25 -0
- package/dist/src/utils/ssrf-guard.js +91 -0
- package/node_modules/ws/index.js +15 -6
- package/node_modules/ws/lib/permessage-deflate.js +6 -6
- package/node_modules/ws/lib/websocket-server.js +5 -5
- package/node_modules/ws/lib/websocket.js +6 -6
- package/node_modules/ws/package.json +4 -3
- package/node_modules/ws/wrapper.mjs +14 -1
- package/openclaw.plugin.json +1 -0
- package/package.json +11 -22
- package/scripts/postinstall-link-sdk.js +113 -0
- package/scripts/upgrade-via-npm.ps1 +161 -6
- package/scripts/upgrade-via-npm.sh +311 -104
- package/scripts/upgrade-via-source.sh +117 -0
- package/skills/qqbot-media/SKILL.md +9 -5
- package/skills/qqbot-remind/SKILL.md +3 -3
- package/src/admin-resolver.ts +76 -35
- package/src/api.ts +284 -12
- package/src/channel.ts +12 -0
- package/src/config.ts +3 -10
- package/src/deliver-debounce.ts +229 -0
- package/src/gateway.ts +277 -67
- package/src/image-server.ts +213 -77
- package/src/inbound-attachments.ts +32 -15
- package/src/outbound-deliver.ts +77 -157
- package/src/outbound.ts +304 -451
- package/src/reply-dispatcher.ts +4 -4
- package/src/request-context.ts +39 -0
- package/src/slash-commands.ts +303 -33
- package/src/startup-greeting.ts +35 -13
- package/src/streaming.ts +1096 -0
- package/src/tools/remind.ts +15 -11
- package/src/types.ts +111 -0
- package/src/update-checker.ts +2 -7
- package/src/utils/audio-convert.ts +56 -0
- package/src/utils/chunked-upload.ts +419 -0
- package/src/utils/file-utils.ts +28 -2
- package/src/utils/media-send.ts +563 -0
- package/src/utils/pkg-version.ts +54 -0
- package/src/utils/ssrf-guard.ts +102 -0
- package/clawdbot.plugin.json +0 -16
- package/dist/src/user-messages.d.ts +0 -8
- package/dist/src/user-messages.js +0 -8
- package/moltbot.plugin.json +0 -16
- package/scripts/upgrade-via-alt-pkg.sh +0 -307
- package/src/bot-logs-2026-03-21T11-21-47(2).txt +0 -46
- package/src/gateway.log +0 -43
- package/src/openclaw-2026-03-21.log +0 -3729
- package/src/user-messages.ts +0 -7
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 远程 URL 安全校验
|
|
3
|
+
*
|
|
4
|
+
* 下载外部资源前,确保目标地址不会命中内部网络或云元数据端点,
|
|
5
|
+
* 避免模型输出的恶意链接触达内网服务。
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* 检查给定 IP 是否落在不可路由 / 私有网段内。
|
|
9
|
+
*
|
|
10
|
+
* 覆盖:
|
|
11
|
+
* - IPv4: 127/8, 10/8, 172.16/12, 192.168/16, 169.254/16, 0.0.0.0
|
|
12
|
+
* - IPv6: ::1, ::, fe80 (link-local), fc/fd (ULA)
|
|
13
|
+
*/
|
|
14
|
+
export declare function isReservedAddr(ip: string): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* 校验远程 URL 是否可安全请求。
|
|
17
|
+
*
|
|
18
|
+
* 规则:
|
|
19
|
+
* 1. 仅放行 http / https 协议
|
|
20
|
+
* 2. 若 URL 直接携带 IP 则即时判定
|
|
21
|
+
* 3. 若为域名则先做 DNS 解析,逐条检查解析结果
|
|
22
|
+
*
|
|
23
|
+
* @throws {Error} 当 URL 指向受限地址时
|
|
24
|
+
*/
|
|
25
|
+
export declare function validateRemoteUrl(raw: string): Promise<void>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 远程 URL 安全校验
|
|
3
|
+
*
|
|
4
|
+
* 下载外部资源前,确保目标地址不会命中内部网络或云元数据端点,
|
|
5
|
+
* 避免模型输出的恶意链接触达内网服务。
|
|
6
|
+
*/
|
|
7
|
+
import net from "node:net";
|
|
8
|
+
import dns from "node:dns/promises";
|
|
9
|
+
/* ---------- 内网 / 保留地址判定 ---------- */
|
|
10
|
+
/** IPv4 保留网段前缀(覆盖 RFC 1918、链路本地、回环等) */
|
|
11
|
+
const RESERVED_V4_PREFIXES = [
|
|
12
|
+
"127.", // loopback
|
|
13
|
+
"10.", // class-A private
|
|
14
|
+
"192.168.", // class-C private
|
|
15
|
+
"169.254.", // link-local / cloud metadata
|
|
16
|
+
];
|
|
17
|
+
/** 172.16.0.0 – 172.31.255.255 需要单独用正则匹配 */
|
|
18
|
+
const PRIVATE_172_RE = /^172\.(1[6-9]|2\d|3[01])\./;
|
|
19
|
+
/**
|
|
20
|
+
* 检查给定 IP 是否落在不可路由 / 私有网段内。
|
|
21
|
+
*
|
|
22
|
+
* 覆盖:
|
|
23
|
+
* - IPv4: 127/8, 10/8, 172.16/12, 192.168/16, 169.254/16, 0.0.0.0
|
|
24
|
+
* - IPv6: ::1, ::, fe80 (link-local), fc/fd (ULA)
|
|
25
|
+
*/
|
|
26
|
+
export function isReservedAddr(ip) {
|
|
27
|
+
// --- IPv4 ---
|
|
28
|
+
if (ip === "0.0.0.0")
|
|
29
|
+
return true;
|
|
30
|
+
for (const pfx of RESERVED_V4_PREFIXES) {
|
|
31
|
+
if (ip.startsWith(pfx))
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (PRIVATE_172_RE.test(ip))
|
|
35
|
+
return true;
|
|
36
|
+
// --- IPv6 ---
|
|
37
|
+
const lower = ip.toLowerCase();
|
|
38
|
+
if (lower === "::1" || lower === "::")
|
|
39
|
+
return true;
|
|
40
|
+
if (lower.startsWith("fe80:"))
|
|
41
|
+
return true; // link-local
|
|
42
|
+
if (lower.startsWith("fc") || lower.startsWith("fd"))
|
|
43
|
+
return true; // unique local
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
/* ---------- URL 合法性校验 ---------- */
|
|
47
|
+
const ALLOWED_SCHEMES = new Set(["http:", "https:"]);
|
|
48
|
+
/**
|
|
49
|
+
* 校验远程 URL 是否可安全请求。
|
|
50
|
+
*
|
|
51
|
+
* 规则:
|
|
52
|
+
* 1. 仅放行 http / https 协议
|
|
53
|
+
* 2. 若 URL 直接携带 IP 则即时判定
|
|
54
|
+
* 3. 若为域名则先做 DNS 解析,逐条检查解析结果
|
|
55
|
+
*
|
|
56
|
+
* @throws {Error} 当 URL 指向受限地址时
|
|
57
|
+
*/
|
|
58
|
+
export async function validateRemoteUrl(raw) {
|
|
59
|
+
const url = new URL(raw);
|
|
60
|
+
if (!ALLOWED_SCHEMES.has(url.protocol)) {
|
|
61
|
+
throw new Error(`不支持的协议 "${url.protocol}",仅允许 http/https(URL: ${raw})`);
|
|
62
|
+
}
|
|
63
|
+
// 去掉 IPv6 方括号
|
|
64
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
65
|
+
if (net.isIP(host)) {
|
|
66
|
+
assertPublicAddr(host, raw);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// 域名 → 解析后逐条检查
|
|
70
|
+
try {
|
|
71
|
+
const ips = await dns.resolve(host);
|
|
72
|
+
for (const ip of ips) {
|
|
73
|
+
assertPublicAddr(ip, raw, host);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
// 已经是我们自己抛的安全错误,继续向上传播
|
|
78
|
+
if (err instanceof Error && err.message.includes("内网"))
|
|
79
|
+
throw err;
|
|
80
|
+
// DNS 查询失败不阻塞,后续 fetch 会产生网络错误
|
|
81
|
+
console.warn(`[url-check] DNS 解析 "${host}" 失败: ${err}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/* ---------- 内部辅助 ---------- */
|
|
85
|
+
/** 断言 IP 为公网地址,否则抛出错误 */
|
|
86
|
+
function assertPublicAddr(ip, originalUrl, domain) {
|
|
87
|
+
if (!isReservedAddr(ip))
|
|
88
|
+
return;
|
|
89
|
+
const target = domain ? `域名 "${domain}" 解析到内网地址 "${ip}"` : `内网地址 "${ip}"`;
|
|
90
|
+
throw new Error(`禁止访问${target},已拦截潜在的 SSRF 请求(URL: ${originalUrl})`);
|
|
91
|
+
}
|
package/node_modules/ws/index.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const createWebSocketStream = require('./lib/stream');
|
|
4
|
+
const extension = require('./lib/extension');
|
|
5
|
+
const PerMessageDeflate = require('./lib/permessage-deflate');
|
|
6
|
+
const Receiver = require('./lib/receiver');
|
|
7
|
+
const Sender = require('./lib/sender');
|
|
8
|
+
const subprotocol = require('./lib/subprotocol');
|
|
3
9
|
const WebSocket = require('./lib/websocket');
|
|
10
|
+
const WebSocketServer = require('./lib/websocket-server');
|
|
4
11
|
|
|
5
|
-
WebSocket.createWebSocketStream =
|
|
6
|
-
WebSocket.
|
|
7
|
-
WebSocket.
|
|
8
|
-
WebSocket.
|
|
9
|
-
|
|
12
|
+
WebSocket.createWebSocketStream = createWebSocketStream;
|
|
13
|
+
WebSocket.extension = extension;
|
|
14
|
+
WebSocket.PerMessageDeflate = PerMessageDeflate;
|
|
15
|
+
WebSocket.Receiver = Receiver;
|
|
16
|
+
WebSocket.Sender = Sender;
|
|
17
|
+
WebSocket.Server = WebSocketServer;
|
|
18
|
+
WebSocket.subprotocol = subprotocol;
|
|
10
19
|
WebSocket.WebSocket = WebSocket;
|
|
11
|
-
WebSocket.WebSocketServer =
|
|
20
|
+
WebSocket.WebSocketServer = WebSocketServer;
|
|
12
21
|
|
|
13
22
|
module.exports = WebSocket;
|
|
@@ -37,6 +37,9 @@ class PerMessageDeflate {
|
|
|
37
37
|
* acknowledge disabling of client context takeover
|
|
38
38
|
* @param {Number} [options.concurrencyLimit=10] The number of concurrent
|
|
39
39
|
* calls to zlib
|
|
40
|
+
* @param {Boolean} [options.isServer=false] Create the instance in either
|
|
41
|
+
* server or client mode
|
|
42
|
+
* @param {Number} [options.maxPayload=0] The maximum allowed message length
|
|
40
43
|
* @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
|
|
41
44
|
* use of a custom server window size
|
|
42
45
|
* @param {Boolean} [options.serverNoContextTakeover=false] Request/accept
|
|
@@ -47,16 +50,13 @@ class PerMessageDeflate {
|
|
|
47
50
|
* deflate
|
|
48
51
|
* @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
|
|
49
52
|
* inflate
|
|
50
|
-
* @param {Boolean} [isServer=false] Create the instance in either server or
|
|
51
|
-
* client mode
|
|
52
|
-
* @param {Number} [maxPayload=0] The maximum allowed message length
|
|
53
53
|
*/
|
|
54
|
-
constructor(options
|
|
55
|
-
this._maxPayload = maxPayload | 0;
|
|
54
|
+
constructor(options) {
|
|
56
55
|
this._options = options || {};
|
|
57
56
|
this._threshold =
|
|
58
57
|
this._options.threshold !== undefined ? this._options.threshold : 1024;
|
|
59
|
-
this.
|
|
58
|
+
this._maxPayload = this._options.maxPayload | 0;
|
|
59
|
+
this._isServer = !!this._options.isServer;
|
|
60
60
|
this._deflate = null;
|
|
61
61
|
this._inflate = null;
|
|
62
62
|
|
|
@@ -293,11 +293,11 @@ class WebSocketServer extends EventEmitter {
|
|
|
293
293
|
this.options.perMessageDeflate &&
|
|
294
294
|
secWebSocketExtensions !== undefined
|
|
295
295
|
) {
|
|
296
|
-
const perMessageDeflate = new PerMessageDeflate(
|
|
297
|
-
this.options.perMessageDeflate,
|
|
298
|
-
true,
|
|
299
|
-
this.options.maxPayload
|
|
300
|
-
);
|
|
296
|
+
const perMessageDeflate = new PerMessageDeflate({
|
|
297
|
+
...this.options.perMessageDeflate,
|
|
298
|
+
isServer: true,
|
|
299
|
+
maxPayload: this.options.maxPayload
|
|
300
|
+
});
|
|
301
301
|
|
|
302
302
|
try {
|
|
303
303
|
const offers = extension.parse(secWebSocketExtensions);
|
|
@@ -693,7 +693,7 @@ function initAsClient(websocket, address, protocols, options) {
|
|
|
693
693
|
} else {
|
|
694
694
|
try {
|
|
695
695
|
parsedUrl = new URL(address);
|
|
696
|
-
} catch
|
|
696
|
+
} catch {
|
|
697
697
|
throw new SyntaxError(`Invalid URL: ${address}`);
|
|
698
698
|
}
|
|
699
699
|
}
|
|
@@ -755,11 +755,11 @@ function initAsClient(websocket, address, protocols, options) {
|
|
|
755
755
|
opts.timeout = opts.handshakeTimeout;
|
|
756
756
|
|
|
757
757
|
if (opts.perMessageDeflate) {
|
|
758
|
-
perMessageDeflate = new PerMessageDeflate(
|
|
759
|
-
opts.perMessageDeflate
|
|
760
|
-
false,
|
|
761
|
-
opts.maxPayload
|
|
762
|
-
);
|
|
758
|
+
perMessageDeflate = new PerMessageDeflate({
|
|
759
|
+
...opts.perMessageDeflate,
|
|
760
|
+
isServer: false,
|
|
761
|
+
maxPayload: opts.maxPayload
|
|
762
|
+
});
|
|
763
763
|
opts.headers['Sec-WebSocket-Extensions'] = format({
|
|
764
764
|
[PerMessageDeflate.extensionName]: perMessageDeflate.offer()
|
|
765
765
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ws",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.20.0",
|
|
4
4
|
"description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"HyBi",
|
|
@@ -55,12 +55,13 @@
|
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
+
"@eslint/js": "^10.0.1",
|
|
58
59
|
"benchmark": "^2.1.4",
|
|
59
60
|
"bufferutil": "^4.0.1",
|
|
60
|
-
"eslint": "^
|
|
61
|
+
"eslint": "^10.0.1",
|
|
61
62
|
"eslint-config-prettier": "^10.0.1",
|
|
62
63
|
"eslint-plugin-prettier": "^5.0.0",
|
|
63
|
-
"globals": "^
|
|
64
|
+
"globals": "^17.0.0",
|
|
64
65
|
"mocha": "^8.4.0",
|
|
65
66
|
"nyc": "^15.0.0",
|
|
66
67
|
"prettier": "^3.0.0",
|
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import createWebSocketStream from './lib/stream.js';
|
|
2
|
+
import extension from './lib/extension.js';
|
|
3
|
+
import PerMessageDeflate from './lib/permessage-deflate.js';
|
|
2
4
|
import Receiver from './lib/receiver.js';
|
|
3
5
|
import Sender from './lib/sender.js';
|
|
6
|
+
import subprotocol from './lib/subprotocol.js';
|
|
4
7
|
import WebSocket from './lib/websocket.js';
|
|
5
8
|
import WebSocketServer from './lib/websocket-server.js';
|
|
6
9
|
|
|
7
|
-
export {
|
|
10
|
+
export {
|
|
11
|
+
createWebSocketStream,
|
|
12
|
+
extension,
|
|
13
|
+
PerMessageDeflate,
|
|
14
|
+
Receiver,
|
|
15
|
+
Sender,
|
|
16
|
+
subprotocol,
|
|
17
|
+
WebSocket,
|
|
18
|
+
WebSocketServer
|
|
19
|
+
};
|
|
20
|
+
|
|
8
21
|
export default WebSocket;
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"name": "OpenClaw QQ Bot",
|
|
4
4
|
"description": "QQ Bot channel plugin with message support, cron jobs, and proactive messaging",
|
|
5
5
|
"channels": ["qqbot"],
|
|
6
|
+
"extensions": ["./dist/index.js"],
|
|
6
7
|
"skills": ["skills/qqbot-channel", "skills/qqbot-remind", "skills/qqbot-media"],
|
|
7
8
|
"capabilities": {
|
|
8
9
|
"proactiveMessaging": true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ryantest/openclaw-qqbot",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.6.6-alpha.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,32 +16,23 @@
|
|
|
16
16
|
"scripts",
|
|
17
17
|
"index.ts",
|
|
18
18
|
"tsconfig.json",
|
|
19
|
-
"openclaw.plugin.json"
|
|
20
|
-
"clawdbot.plugin.json",
|
|
21
|
-
"moltbot.plugin.json"
|
|
19
|
+
"openclaw.plugin.json"
|
|
22
20
|
],
|
|
23
|
-
"clawdbot": {
|
|
24
|
-
"id": "openclaw-qqbot",
|
|
25
|
-
"extensions": [
|
|
26
|
-
"./index.ts"
|
|
27
|
-
]
|
|
28
|
-
},
|
|
29
|
-
"moltbot": {
|
|
30
|
-
"id": "openclaw-qqbot",
|
|
31
|
-
"extensions": [
|
|
32
|
-
"./index.ts"
|
|
33
|
-
]
|
|
34
|
-
},
|
|
35
21
|
"openclaw": {
|
|
36
22
|
"id": "openclaw-qqbot",
|
|
37
23
|
"extensions": [
|
|
38
|
-
"./index.
|
|
39
|
-
]
|
|
24
|
+
"./dist/index.js"
|
|
25
|
+
],
|
|
26
|
+
"channel": {
|
|
27
|
+
"id": "qqbot",
|
|
28
|
+
"label": "QQ Bot"
|
|
29
|
+
}
|
|
40
30
|
},
|
|
41
31
|
"scripts": {
|
|
42
32
|
"build": "tsc || true",
|
|
43
33
|
"dev": "tsc --watch",
|
|
44
|
-
"prepack": "npm install --omit=dev"
|
|
34
|
+
"prepack": "npm install --omit=dev",
|
|
35
|
+
"postinstall": "node scripts/postinstall-link-sdk.js 2>/dev/null || true"
|
|
45
36
|
},
|
|
46
37
|
"dependencies": {
|
|
47
38
|
"mpg123-decoder": "^1.0.3",
|
|
@@ -59,8 +50,6 @@
|
|
|
59
50
|
"typescript": "^5.9.3"
|
|
60
51
|
},
|
|
61
52
|
"peerDependencies": {
|
|
62
|
-
"clawdbot": "*",
|
|
63
|
-
"moltbot": "*",
|
|
64
53
|
"openclaw": "*"
|
|
65
54
|
},
|
|
66
55
|
"homepage": "https://github.com/tencent-connect/openclaw-qqbot",
|
|
@@ -73,4 +62,4 @@
|
|
|
73
62
|
"silk-wasm",
|
|
74
63
|
"ws"
|
|
75
64
|
]
|
|
76
|
-
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// When installed as an openclaw extension under ~/.openclaw/extensions/,
|
|
4
|
+
// the plugin needs access to `openclaw/plugin-sdk` at runtime.
|
|
5
|
+
// openclaw's jiti loader resolves this via alias by walking up from the plugin
|
|
6
|
+
// path to find the openclaw package root — but ~/.openclaw/extensions/ is not
|
|
7
|
+
// under the openclaw package tree, so the alias lookup fails.
|
|
8
|
+
//
|
|
9
|
+
// This script creates a symlink from the plugin's node_modules/openclaw to the
|
|
10
|
+
// globally installed openclaw package, allowing Node's native ESM resolver
|
|
11
|
+
// (used by jiti with tryNative:true for .js files) to find `openclaw/plugin-sdk`.
|
|
12
|
+
|
|
13
|
+
import { existsSync, symlinkSync, mkdirSync, realpathSync } from "node:fs";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { execSync } from "node:child_process";
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const pluginRoot = resolve(__dirname, "..");
|
|
20
|
+
|
|
21
|
+
// Only run when installed under an openclaw-like extensions directory
|
|
22
|
+
// (supports openclaw, clawdbot, moltbot, etc.)
|
|
23
|
+
if (!pluginRoot.includes("extensions")) {
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const linkTarget = join(pluginRoot, "node_modules", "openclaw");
|
|
28
|
+
|
|
29
|
+
// Already linked or exists
|
|
30
|
+
if (existsSync(linkTarget)) {
|
|
31
|
+
process.exit(0);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// CLI names to try (openclaw and its aliases)
|
|
35
|
+
const CLI_NAMES = ["openclaw", "clawdbot", "moltbot"];
|
|
36
|
+
|
|
37
|
+
// Find the global openclaw installation
|
|
38
|
+
let openclawRoot = null;
|
|
39
|
+
|
|
40
|
+
// Strategy 1: npm root -g → look for any known CLI package name
|
|
41
|
+
if (!openclawRoot) {
|
|
42
|
+
try {
|
|
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
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
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
|
+
|
|
100
|
+
if (!openclawRoot) {
|
|
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");
|
|
104
|
+
process.exit(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
mkdirSync(join(pluginRoot, "node_modules"), { recursive: true });
|
|
109
|
+
symlinkSync(openclawRoot, linkTarget, "junction");
|
|
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}`);
|
|
113
|
+
}
|
|
@@ -95,7 +95,7 @@ Write-Host "==========================================="
|
|
|
95
95
|
Write-Host ""
|
|
96
96
|
|
|
97
97
|
# [1/3] Download and extract new version
|
|
98
|
-
Write-Host "[1/
|
|
98
|
+
Write-Host "[1/5] Downloading new version..."
|
|
99
99
|
$TMPDIR_PACK = Join-Path ([System.IO.Path]::GetTempPath()) "qqbot-pack-$([guid]::NewGuid().ToString('N').Substring(0,8))"
|
|
100
100
|
$EXTRACT_DIR = Join-Path ([System.IO.Path]::GetTempPath()) "qqbot-extract-$([guid]::NewGuid().ToString('N').Substring(0,8))"
|
|
101
101
|
New-Item -ItemType Directory -Path $TMPDIR_PACK -Force | Out-Null
|
|
@@ -172,9 +172,108 @@ try {
|
|
|
172
172
|
if (Test-Path $EXTRACT_DIR) { Remove-Item -Recurse -Force $EXTRACT_DIR -ErrorAction SilentlyContinue }
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
#
|
|
175
|
+
# ── Preflight: validate new package before writing to extensions ──
|
|
176
176
|
Write-Host ""
|
|
177
|
-
Write-Host "[2/
|
|
177
|
+
Write-Host "[2/5] Preflight checks..."
|
|
178
|
+
$PreflightOK = $true
|
|
179
|
+
|
|
180
|
+
# (a) package.json exists and has version
|
|
181
|
+
$StagingPkg = Join-Path $STAGING_DIR "package.json"
|
|
182
|
+
$StagingVersion = ""
|
|
183
|
+
if (-not (Test-Path $StagingPkg)) {
|
|
184
|
+
Write-Host " [FAIL] New package missing package.json" -ForegroundColor Red
|
|
185
|
+
$PreflightOK = $false
|
|
186
|
+
} else {
|
|
187
|
+
try {
|
|
188
|
+
$spkg = Get-Content $StagingPkg -Raw | ConvertFrom-Json
|
|
189
|
+
$StagingVersion = $spkg.version
|
|
190
|
+
if (-not $StagingVersion) { throw "no version" }
|
|
191
|
+
Write-Host " [OK] Version: $StagingVersion"
|
|
192
|
+
} catch {
|
|
193
|
+
Write-Host " [FAIL] package.json unreadable or missing version" -ForegroundColor Red
|
|
194
|
+
$PreflightOK = $false
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# (b) Entry file exists
|
|
199
|
+
$EntryFile = ""
|
|
200
|
+
foreach ($candidate in @("dist\index.js", "index.js")) {
|
|
201
|
+
if (Test-Path (Join-Path $STAGING_DIR $candidate)) {
|
|
202
|
+
$EntryFile = $candidate
|
|
203
|
+
break
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (-not $EntryFile) {
|
|
207
|
+
Write-Host " [FAIL] Missing entry file (dist\index.js or index.js)" -ForegroundColor Red
|
|
208
|
+
$PreflightOK = $false
|
|
209
|
+
} else {
|
|
210
|
+
Write-Host " [OK] Entry file: $EntryFile"
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
# (c) Core directory dist/src
|
|
214
|
+
$CoreSrcDir = Join-Path $STAGING_DIR "dist" "src"
|
|
215
|
+
if (-not (Test-Path $CoreSrcDir)) {
|
|
216
|
+
Write-Host " [FAIL] Missing core directory dist\src\" -ForegroundColor Red
|
|
217
|
+
$PreflightOK = $false
|
|
218
|
+
} else {
|
|
219
|
+
$CoreJsCount = (Get-ChildItem -Path $CoreSrcDir -Filter "*.js" -File -Recurse -ErrorAction SilentlyContinue | Measure-Object).Count
|
|
220
|
+
Write-Host " [OK] dist\src\ contains $CoreJsCount JS files"
|
|
221
|
+
if ($CoreJsCount -lt 5) {
|
|
222
|
+
Write-Host " [FAIL] JS file count too low (expected >= 5, got $CoreJsCount)" -ForegroundColor Red
|
|
223
|
+
$PreflightOK = $false
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
# (d) Critical module files
|
|
228
|
+
$MissingModules = @()
|
|
229
|
+
foreach ($mod in @("dist\src\gateway.js", "dist\src\api.js", "dist\src\admin-resolver.js")) {
|
|
230
|
+
if (-not (Test-Path (Join-Path $STAGING_DIR $mod))) {
|
|
231
|
+
$MissingModules += $mod
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if ($MissingModules.Count -gt 0) {
|
|
235
|
+
Write-Host " [FAIL] Missing critical modules: $($MissingModules -join ', ')" -ForegroundColor Red
|
|
236
|
+
$PreflightOK = $false
|
|
237
|
+
} else {
|
|
238
|
+
Write-Host " [OK] Critical modules intact"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# (e) Bundled node_modules health check
|
|
242
|
+
$nmDir = Join-Path $STAGING_DIR "node_modules"
|
|
243
|
+
if (Test-Path $nmDir) {
|
|
244
|
+
$BundledOK = $true
|
|
245
|
+
foreach ($dep in @("ws", "silk-wasm")) {
|
|
246
|
+
if (-not (Test-Path (Join-Path $nmDir $dep))) {
|
|
247
|
+
Write-Host " [WARN] Bundled dependency missing: $dep" -ForegroundColor Yellow
|
|
248
|
+
$BundledOK = $false
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if ($BundledOK) {
|
|
252
|
+
Write-Host " [OK] Core bundled dependencies intact"
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
# (f) Version sanity check
|
|
257
|
+
if ($StagingVersion) {
|
|
258
|
+
$StagingMajor = ($StagingVersion -split "\.")[0]
|
|
259
|
+
if ($StagingMajor -eq "0") {
|
|
260
|
+
Write-Host " [WARN] Major version is 0 ($StagingVersion), may not be a production release" -ForegroundColor Yellow
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
# Preflight result
|
|
265
|
+
if (-not $PreflightOK) {
|
|
266
|
+
Write-Host ""
|
|
267
|
+
Write-Host "[ABORT] Preflight checks failed, upgrade cancelled (old version unaffected)" -ForegroundColor Red
|
|
268
|
+
Remove-Item -Recurse -Force $STAGING_DIR -ErrorAction SilentlyContinue
|
|
269
|
+
exit 1
|
|
270
|
+
}
|
|
271
|
+
Write-Host " [OK] All preflight checks passed"
|
|
272
|
+
|
|
273
|
+
# [3/5] Replace plugin directory (in-place overwrite to avoid file-lock issues)
|
|
274
|
+
Write-Host ""
|
|
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 }
|
|
178
277
|
$TARGET_DIR = Join-Path $EXTENSIONS_DIR "openclaw-qqbot"
|
|
179
278
|
|
|
180
279
|
if (-not (Test-Path $TARGET_DIR)) {
|
|
@@ -217,9 +316,50 @@ foreach ($legacyName in @("qqbot", "openclaw-qq")) {
|
|
|
217
316
|
}
|
|
218
317
|
Write-Host " Installed to: $TARGET_DIR"
|
|
219
318
|
|
|
220
|
-
#
|
|
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
|
+
|
|
360
|
+
# [4/5] Verify installation
|
|
221
361
|
Write-Host ""
|
|
222
|
-
Write-Host "[
|
|
362
|
+
Write-Host "[4/5] Verifying installation..."
|
|
223
363
|
$NEW_VERSION = "unknown"
|
|
224
364
|
try {
|
|
225
365
|
$newPkgPath = Join-Path $TARGET_DIR "package.json"
|
|
@@ -249,7 +389,7 @@ if ($NoRestart) {
|
|
|
249
389
|
exit 0
|
|
250
390
|
}
|
|
251
391
|
|
|
252
|
-
# [
|
|
392
|
+
# [配置] Configure appid/secret
|
|
253
393
|
if ($AppId -and $Secret) {
|
|
254
394
|
Write-Host ""
|
|
255
395
|
Write-Host "[Config] Writing qqbot channel config..."
|
|
@@ -285,11 +425,26 @@ if ($AppId -and $Secret) {
|
|
|
285
425
|
|
|
286
426
|
# [5/5] Restart gateway
|
|
287
427
|
Write-Host ""
|
|
428
|
+
|
|
429
|
+
# Manual upgrade: write startup-marker before restart to prevent bot from sending duplicate notification
|
|
430
|
+
if ($NEW_VERSION -and $NEW_VERSION -ne "unknown") {
|
|
431
|
+
$MarkerDir = Join-Path $HOME_DIR ".openclaw" "qqbot" "data"
|
|
432
|
+
if (-not (Test-Path $MarkerDir)) { New-Item -ItemType Directory -Path $MarkerDir -Force | Out-Null }
|
|
433
|
+
$MarkerFile = Join-Path $MarkerDir "startup-marker.json"
|
|
434
|
+
$Now = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
|
|
435
|
+
@{ version = $NEW_VERSION; startedAt = $Now; greetedAt = $Now } | ConvertTo-Json -Compress | Set-Content $MarkerFile -Encoding UTF8
|
|
436
|
+
}
|
|
437
|
+
|
|
288
438
|
Write-Host "[Restart] Restarting gateway..."
|
|
289
439
|
try {
|
|
290
440
|
& $CMD gateway restart 2>&1 | Out-Null
|
|
291
441
|
if ($LASTEXITCODE -eq 0) {
|
|
292
442
|
Write-Host " Gateway restarted"
|
|
443
|
+
# Print the same upgrade greeting as bot notification (no need to push via bot in manual upgrade)
|
|
444
|
+
if ($NEW_VERSION -and $NEW_VERSION -ne "unknown") {
|
|
445
|
+
Write-Host ""
|
|
446
|
+
Write-Host "🎉 QQBot 插件已更新至 v${NEW_VERSION},在线等候你的吩咐。"
|
|
447
|
+
}
|
|
293
448
|
} else { throw "restart failed" }
|
|
294
449
|
} catch {
|
|
295
450
|
Write-Host " [WARN] Gateway restart failed, please run manually: $CMD gateway restart" -ForegroundColor Yellow
|