@alemonjs/qq-bot 2.1.33 → 2.1.34
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 +46 -8
- package/dist/assets/index.css +1 -1
- package/dist/assets/index.js +12 -5
- package/dist/index.html +1 -1
- package/lib/config.d.ts +44 -19
- package/lib/config.js +39 -35
- package/lib/desktop.d.ts +4 -1
- package/lib/desktop.js +53 -57
- package/lib/format.d.ts +26 -5
- package/lib/format.js +84 -101
- package/lib/hook.d.ts +82 -65
- package/lib/hook.js +35 -32
- package/lib/index.d.ts +6 -4
- package/lib/index.js +13 -16
- package/lib/index.webhook.d.ts +4 -1
- package/lib/index.webhook.js +12 -14
- package/lib/index.websoket.d.ts +6 -3
- package/lib/index.websoket.js +131 -41
- package/lib/message/AT_MESSAGE_CREATE.d.ts +36 -33
- package/lib/message/AT_MESSAGE_CREATE.js +1 -1
- package/lib/message/CHANNEL_CREATE.d.ts +17 -14
- package/lib/message/CHANNEL_CREATE.js +1 -1
- package/lib/message/CHANNEL_DELETE.d.ts +22 -14
- package/lib/message/CHANNEL_DELETE.js +1 -1
- package/lib/message/CHANNEL_UPDATE.d.ts +22 -14
- package/lib/message/CHANNEL_UPDATE.js +1 -1
- package/lib/message/DIRECT_MESSAGE_CREATE.d.ts +35 -27
- package/lib/message/DIRECT_MESSAGE_CREATE.js +1 -1
- package/lib/message/DIRECT_MESSAGE_DELETE.d.ts +23 -15
- package/lib/message/DIRECT_MESSAGE_DELETE.js +1 -1
- package/lib/message/ERROR.d.ts +4 -1
- package/lib/message/ERROR.js +1 -1
- package/lib/message/GUILD_CREATE.d.ts +22 -14
- package/lib/message/GUILD_CREATE.js +1 -1
- package/lib/message/GUILD_DELETE.d.ts +22 -14
- package/lib/message/GUILD_DELETE.js +1 -1
- package/lib/message/GUILD_MEMBER_ADD.d.ts +21 -13
- package/lib/message/GUILD_MEMBER_ADD.js +1 -1
- package/lib/message/GUILD_MEMBER_REMOVE.d.ts +21 -13
- package/lib/message/GUILD_MEMBER_REMOVE.js +1 -1
- package/lib/message/GUILD_MEMBER_UPDATE.d.ts +21 -13
- package/lib/message/GUILD_MEMBER_UPDATE.js +1 -1
- package/lib/message/GUILD_UPDATE.d.ts +22 -14
- package/lib/message/GUILD_UPDATE.js +1 -1
- package/lib/message/INTERACTION_CREATE.d.ts +52 -44
- package/lib/message/INTERACTION_CREATE.js +1 -1
- package/lib/message/MESSAGE_CREATE.d.ts +12 -1
- package/lib/message/MESSAGE_CREATE.js +1 -1
- package/lib/message/MESSAGE_DELETE.d.ts +12 -1
- package/lib/message/MESSAGE_DELETE.js +1 -1
- package/lib/message/MESSAGE_REACTION_ADD.d.ts +15 -12
- package/lib/message/MESSAGE_REACTION_ADD.js +1 -1
- package/lib/message/MESSAGE_REACTION_REMOVE.d.ts +15 -12
- package/lib/message/MESSAGE_REACTION_REMOVE.js +1 -1
- package/lib/message/PUBLIC_MESSAGE_DELETE.d.ts +20 -13
- package/lib/message/PUBLIC_MESSAGE_DELETE.js +1 -1
- package/lib/message/READY.d.ts +11 -5
- package/lib/message/READY.js +1 -1
- package/lib/message/group/C2C_MESSAGE_CREATE.d.ts +18 -15
- package/lib/message/group/C2C_MESSAGE_CREATE.js +1 -1
- package/lib/message/group/C2C_MSG_RECEIVE.d.ts +13 -3
- package/lib/message/group/C2C_MSG_RECEIVE.js +1 -1
- package/lib/message/group/C2C_MSG_REJECT.d.ts +9 -3
- package/lib/message/group/C2C_MSG_REJECT.js +1 -1
- package/lib/message/group/FRIEND_ADD.d.ts +13 -3
- package/lib/message/group/FRIEND_ADD.js +1 -1
- package/lib/message/group/FRIEND_DEL.d.ts +9 -3
- package/lib/message/group/FRIEND_DEL.js +1 -1
- package/lib/message/group/GROUP_ADD_ROBOT.d.ts +23 -4
- package/lib/message/group/GROUP_ADD_ROBOT.js +1 -1
- package/lib/message/group/GROUP_AT_MESSAGE_CREATE.d.ts +38 -32
- package/lib/message/group/GROUP_AT_MESSAGE_CREATE.js +1 -1
- package/lib/message/group/GROUP_DEL_ROBOT.d.ts +19 -4
- package/lib/message/group/GROUP_DEL_ROBOT.js +1 -1
- package/lib/message/group/GROUP_JOIN_REQUEST.d.ts +83 -22
- package/lib/message/group/GROUP_JOIN_REQUEST.js +1 -1
- package/lib/message/group/GROUP_MEMBER_ADD.d.ts +8 -5
- package/lib/message/group/GROUP_MEMBER_ADD.js +1 -1
- package/lib/message/group/GROUP_MEMBER_REMOVE.d.ts +8 -5
- package/lib/message/group/GROUP_MEMBER_REMOVE.js +1 -1
- package/lib/message/group/GROUP_MESSAGE_CREATE.d.ts +38 -32
- package/lib/message/group/GROUP_MESSAGE_CREATE.js +1 -1
- package/lib/message/group/GROUP_MSG_RECEIVE.d.ts +14 -4
- package/lib/message/group/GROUP_MSG_RECEIVE.js +1 -1
- package/lib/message/group/GROUP_MSG_REJECT.d.ts +10 -4
- package/lib/message/group/GROUP_MSG_REJECT.js +1 -1
- package/lib/message/group/MESSAGE_AUDIT_PASS.d.ts +12 -6
- package/lib/message/group/MESSAGE_AUDIT_PASS.js +1 -1
- package/lib/message/group/MESSAGE_AUDIT_REJECT.d.ts +12 -6
- package/lib/message/group/MESSAGE_AUDIT_REJECT.js +1 -1
- package/lib/register.d.ts +12 -9
- package/lib/register.js +1246 -1404
- package/lib/sdk/api.d.ts +937 -306
- package/lib/sdk/api.js +1481 -893
- package/lib/sdk/client.webhook.d.ts +25 -8
- package/lib/sdk/client.webhook.js +185 -189
- package/lib/sdk/client.websoket.d.ts +31 -9
- package/lib/sdk/client.websoket.js +369 -392
- package/lib/sdk/config.d.ts +6 -2
- package/lib/sdk/config.js +9 -6
- package/lib/sdk/instance.d.ts +11 -3
- package/lib/sdk/instance.js +72 -84
- package/lib/sdk/intents.d.ts +89 -3
- package/lib/sdk/intents.js +116 -33
- package/lib/sdk/message.d.ts +7 -4
- package/lib/sdk/message.group.d.ts +38 -35
- package/lib/sdk/message.group.js +1 -1
- package/lib/sdk/message.guild.d.ts +40 -37
- package/lib/sdk/message.guild.js +1 -1
- package/lib/sdk/message.js +1 -1
- package/lib/sdk/message.public.d.ts +8 -5
- package/lib/sdk/message.public.js +1 -1
- package/lib/sdk/qr-auth.d.ts +55 -0
- package/lib/sdk/qr-auth.js +169 -0
- package/lib/sdk/registry.d.ts +34 -24
- package/lib/sdk/registry.js +134 -114
- package/lib/sdk/session.d.ts +19 -15
- package/lib/sdk/session.js +51 -54
- package/lib/sdk/typing.d.ts +410 -184
- package/lib/sdk/typing.js +1 -1
- package/lib/sdk/webhook.secret.d.ts +33 -13
- package/lib/sdk/webhook.secret.js +50 -36
- package/lib/sends.d.ts +42 -25
- package/lib/sends.js +519 -541
- package/lib/types.d.ts +52 -41
- package/lib/types.js +1 -1
- package/lib/upload.d.ts +47 -18
- package/lib/upload.js +80 -51
- package/lib/utils.d.ts +9 -1
- package/lib/utils.js +10 -5
- package/package.json +9 -6
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { INTERACTION_CREATE_TYPE } from "../message/INTERACTION_CREATE.js";
|
|
2
|
+
import { ERROR_TYPE } from "../message/ERROR.js";
|
|
3
|
+
//#region src/sdk/message.public.d.ts
|
|
4
|
+
type QQBotPublicEventMap = {
|
|
5
|
+
INTERACTION_CREATE: INTERACTION_CREATE_TYPE;
|
|
6
|
+
ERROR: ERROR_TYPE;
|
|
6
7
|
};
|
|
8
|
+
//#endregion
|
|
9
|
+
export { QQBotPublicEventMap };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region src/sdk/qr-auth.d.ts
|
|
2
|
+
/** 绑定任务状态码(与平台返回一致) */
|
|
3
|
+
declare const BindStatus: {
|
|
4
|
+
readonly NONE: 0;
|
|
5
|
+
readonly PENDING: 1;
|
|
6
|
+
readonly COMPLETED: 2;
|
|
7
|
+
readonly EXPIRED: 3;
|
|
8
|
+
};
|
|
9
|
+
type PortalResponse = {
|
|
10
|
+
retcode: number;
|
|
11
|
+
msg?: string;
|
|
12
|
+
data?: Record<string, any>;
|
|
13
|
+
};
|
|
14
|
+
/** 门户请求函数;可注入替换用于测试 */
|
|
15
|
+
type PortalRequest = (path: string, body: Record<string, any>) => Promise<PortalResponse>;
|
|
16
|
+
/**
|
|
17
|
+
* 解密平台下发的机器人密钥:AES-256-GCM,密文结构为 Base64(IV(12字节) + 密文 + AuthTag(16字节)),
|
|
18
|
+
* 密钥为创建绑定任务时本地生成并提交的 key
|
|
19
|
+
*/
|
|
20
|
+
declare const decryptSecret: (encryptedBase64: string, keyBase64: string) => string;
|
|
21
|
+
/** 构造扫码授权页链接(手机 QQ 扫码后打开) */
|
|
22
|
+
declare const buildConnectUrl: (taskId: string) => string;
|
|
23
|
+
type QRLoginResult = {
|
|
24
|
+
loginId: string;
|
|
25
|
+
appId: string;
|
|
26
|
+
clientSecret: string;
|
|
27
|
+
userOpenid: string;
|
|
28
|
+
};
|
|
29
|
+
type QRLoginOptions = {
|
|
30
|
+
/** 整个扫码流程的总超时(秒) */
|
|
31
|
+
timeoutSeconds?: number;
|
|
32
|
+
/** 二维码过期后的最大刷新次数 */
|
|
33
|
+
maxRefreshes?: number;
|
|
34
|
+
/** 轮询间隔(毫秒) */
|
|
35
|
+
pollInterval?: number;
|
|
36
|
+
/** 注入门户请求(测试用) */
|
|
37
|
+
request?: PortalRequest;
|
|
38
|
+
/** 二维码生成回调(默认同时终端出码并保存图片到运行目录) */
|
|
39
|
+
onQRCode?: (qrBuffer: Buffer, url: string, qrImagePath: string | undefined, loginId: string, refresh: number) => void | Promise<void>;
|
|
40
|
+
/** 状态变化回调 */
|
|
41
|
+
onStatus?: (message: string) => void | Promise<void>;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* 扫码登录流程:
|
|
45
|
+
* 创建绑定任务 → 终端出码并保存二维码图片到运行目录 → 轮询授权结果 → 解密 AppSecret。
|
|
46
|
+
* 成功返回 { appId, clientSecret, userOpenid };超时/失败/刷新次数用尽返回 null
|
|
47
|
+
*/
|
|
48
|
+
declare const qrLogin: (options?: QRLoginOptions) => Promise<QRLoginResult | null>;
|
|
49
|
+
/**
|
|
50
|
+
* 将扫码得到的 app_id/secret 写入 alemon 配置文件。
|
|
51
|
+
* 使用 YAML.parseDocument 在文档节点上仅更新目标键值,文件中已有的注释与原有格式保持不变
|
|
52
|
+
*/
|
|
53
|
+
declare const saveBotCredentials: (appId: string, secret: string) => boolean;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { BindStatus, PortalRequest, PortalResponse, QRLoginOptions, QRLoginResult, buildConnectUrl, decryptSecret, qrLogin, saveBotCredentials };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { getConfig, logger } from "alemonjs";
|
|
2
|
+
import axios from "axios";
|
|
3
|
+
import { createDecipheriv, randomBytes } from "node:crypto";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import QRCode from "qrcode";
|
|
7
|
+
import YAML from "yaml";
|
|
8
|
+
|
|
9
|
+
//#region src/sdk/qr-auth.ts
|
|
10
|
+
/** QQ 门户扫码绑定接口(社区通用入口,官方未公开文档,改版可能失效) */
|
|
11
|
+
const PORTAL_HOST = process.env.QQ_PORTAL_HOST || "q.qq.com";
|
|
12
|
+
const CONNECT_URL_TEMPLATE = "https://q.qq.com/qqbot/openclaw/connect.html?task_id={task_id}&_wv=2&source=alemonjs";
|
|
13
|
+
const REQUEST_TIMEOUT = 1e4;
|
|
14
|
+
const PORTAL_USER_AGENT = "QQBotAdapter/alemonjs (Node.js)";
|
|
15
|
+
/** 绑定任务状态码(与平台返回一致) */
|
|
16
|
+
const BindStatus = {
|
|
17
|
+
NONE: 0,
|
|
18
|
+
PENDING: 1,
|
|
19
|
+
COMPLETED: 2,
|
|
20
|
+
EXPIRED: 3
|
|
21
|
+
};
|
|
22
|
+
const defaultPortalRequest = async (path, body) => {
|
|
23
|
+
return (await axios.post(`https://${PORTAL_HOST}${path}`, body, {
|
|
24
|
+
timeout: REQUEST_TIMEOUT,
|
|
25
|
+
headers: {
|
|
26
|
+
"Content-Type": "application/json",
|
|
27
|
+
Accept: "application/json",
|
|
28
|
+
"User-Agent": PORTAL_USER_AGENT
|
|
29
|
+
}
|
|
30
|
+
})).data;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* 解密平台下发的机器人密钥:AES-256-GCM,密文结构为 Base64(IV(12字节) + 密文 + AuthTag(16字节)),
|
|
34
|
+
* 密钥为创建绑定任务时本地生成并提交的 key
|
|
35
|
+
*/
|
|
36
|
+
const decryptSecret = (encryptedBase64, keyBase64) => {
|
|
37
|
+
const key = Buffer.from(keyBase64, "base64");
|
|
38
|
+
const raw = Buffer.from(encryptedBase64, "base64");
|
|
39
|
+
const iv = raw.subarray(0, 12);
|
|
40
|
+
const ciphertext = raw.subarray(12);
|
|
41
|
+
const authTag = ciphertext.subarray(ciphertext.length - 16);
|
|
42
|
+
const encryptedData = ciphertext.subarray(0, ciphertext.length - 16);
|
|
43
|
+
const decipher = createDecipheriv("aes-256-gcm", key, iv);
|
|
44
|
+
decipher.setAuthTag(authTag);
|
|
45
|
+
return Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString("utf-8");
|
|
46
|
+
};
|
|
47
|
+
/** 构造扫码授权页链接(手机 QQ 扫码后打开) */
|
|
48
|
+
const buildConnectUrl = (taskId) => {
|
|
49
|
+
return CONNECT_URL_TEMPLATE.replace("{task_id}", encodeURIComponent(taskId));
|
|
50
|
+
};
|
|
51
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
52
|
+
/**
|
|
53
|
+
* 扫码登录流程:
|
|
54
|
+
* 创建绑定任务 → 终端出码并保存二维码图片到运行目录 → 轮询授权结果 → 解密 AppSecret。
|
|
55
|
+
* 成功返回 { appId, clientSecret, userOpenid };超时/失败/刷新次数用尽返回 null
|
|
56
|
+
*/
|
|
57
|
+
const qrLogin = async (options = {}) => {
|
|
58
|
+
const { timeoutSeconds = 600, maxRefreshes = 3, pollInterval = 2e3, request = defaultPortalRequest, onQRCode, onStatus } = options;
|
|
59
|
+
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
60
|
+
for (let refresh = 0; refresh <= maxRefreshes; refresh++) try {
|
|
61
|
+
const key = randomBytes(32).toString("base64");
|
|
62
|
+
const created = await request("/lite/create_bind_task", { key });
|
|
63
|
+
if (created.retcode !== 0) {
|
|
64
|
+
await onStatus?.(`创建绑定任务失败:${created.msg || `retcode=${created.retcode}`}`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const taskId = created.data?.task_id;
|
|
68
|
+
if (!taskId) {
|
|
69
|
+
await onStatus?.("创建绑定任务失败:响应缺少 task_id");
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const url = buildConnectUrl(taskId);
|
|
73
|
+
let qrImagePath = null;
|
|
74
|
+
let qrBuffer = null;
|
|
75
|
+
try {
|
|
76
|
+
qrBuffer = await QRCode.toBuffer(url, {
|
|
77
|
+
type: "png",
|
|
78
|
+
width: 320,
|
|
79
|
+
margin: 2
|
|
80
|
+
});
|
|
81
|
+
qrImagePath = join(process.cwd(), "qqbot-login-qr.png");
|
|
82
|
+
writeFileSync(qrImagePath, qrBuffer);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
logger.warn(`[qq-bot] 二维码图片保存失败:${err?.message ?? err}`);
|
|
85
|
+
}
|
|
86
|
+
if (qrBuffer && onQRCode) await onQRCode(qrBuffer, url, qrImagePath ?? void 0, taskId, refresh);
|
|
87
|
+
await onStatus?.("请使用手机 QQ 扫描二维码并确认授权");
|
|
88
|
+
logger.warn("[qq-bot] 注意:扫码授权完成后平台将重置机器人密钥,旧密钥会立即失效;若该机器人已在其他实例中运行,请更新其配置");
|
|
89
|
+
while (Date.now() < deadline) {
|
|
90
|
+
let polled;
|
|
91
|
+
try {
|
|
92
|
+
polled = await request("/lite/poll_bind_result", { task_id: taskId });
|
|
93
|
+
} catch {
|
|
94
|
+
await sleep(pollInterval);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (polled.retcode !== 0) {
|
|
98
|
+
await onStatus?.(`轮询返回异常:${polled.msg || `retcode=${polled.retcode}`},继续等待…`);
|
|
99
|
+
await sleep(pollInterval);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const data = polled.data ?? {};
|
|
103
|
+
const status = data.status ?? BindStatus.NONE;
|
|
104
|
+
if (status === BindStatus.COMPLETED) {
|
|
105
|
+
const clientSecret = decryptSecret(String(data.bot_encrypt_secret ?? ""), key);
|
|
106
|
+
await onStatus?.("扫码授权成功");
|
|
107
|
+
logger.info(`[qq-bot] 扫码登录成功:AppID=${data.bot_appid}`);
|
|
108
|
+
return {
|
|
109
|
+
loginId: taskId,
|
|
110
|
+
appId: String(data.bot_appid ?? ""),
|
|
111
|
+
clientSecret,
|
|
112
|
+
userOpenid: String(data.user_openid ?? "")
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (status === BindStatus.EXPIRED) {
|
|
116
|
+
await onStatus?.(`二维码已过期,正在刷新(${refresh + 1}/${maxRefreshes + 1})`);
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
await sleep(pollInterval);
|
|
120
|
+
}
|
|
121
|
+
} catch (err) {
|
|
122
|
+
logger.error(`[qq-bot] 扫码登录异常:${err?.message ?? err}`);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
await onStatus?.("扫码授权超时");
|
|
126
|
+
logger.warn("[qq-bot] 扫码登录超时");
|
|
127
|
+
return null;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* 将扫码得到的 app_id/secret 写入 alemon 配置文件。
|
|
131
|
+
* 使用 YAML.parseDocument 在文档节点上仅更新目标键值,文件中已有的注释与原有格式保持不变
|
|
132
|
+
*/
|
|
133
|
+
const saveBotCredentials = (appId, secret) => {
|
|
134
|
+
try {
|
|
135
|
+
const config = getConfig();
|
|
136
|
+
const filePath = config.path;
|
|
137
|
+
if (!filePath) {
|
|
138
|
+
logger.warn("[qq-bot] 配置文件路径不可用,无法保存扫码登录凭证");
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
const raw = existsSync(filePath) ? readFileSync(filePath, "utf-8") : "";
|
|
142
|
+
const doc = YAML.parseDocument(raw);
|
|
143
|
+
if (doc.errors.length > 0) {
|
|
144
|
+
logger.error(`[qq-bot] 配置文件存在 YAML 语法错误,无法安全写入凭证(${filePath}),请先修复语法错误后重试`);
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
const section = doc.has("@alemonjs/qq-bot") ? "@alemonjs/qq-bot" : "qq-bot";
|
|
148
|
+
const existing = doc.get(section, true);
|
|
149
|
+
if (existing === void 0 || !YAML.isMap(existing)) doc.set(section, {
|
|
150
|
+
app_id: appId,
|
|
151
|
+
secret
|
|
152
|
+
});
|
|
153
|
+
else {
|
|
154
|
+
doc.setIn([section, "app_id"], appId);
|
|
155
|
+
doc.setIn([section, "secret"], secret);
|
|
156
|
+
}
|
|
157
|
+
const dirPath = dirname(filePath);
|
|
158
|
+
if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
|
|
159
|
+
writeFileSync(filePath, doc.toString(), "utf-8");
|
|
160
|
+
config.reload();
|
|
161
|
+
return true;
|
|
162
|
+
} catch (err) {
|
|
163
|
+
logger.error(`[qq-bot] 保存扫码登录凭证失败:${err?.message ?? err}`);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
//#endregion
|
|
169
|
+
export { BindStatus, buildConnectUrl, decryptSecret, qrLogin, saveBotCredentials };
|
package/lib/sdk/registry.d.ts
CHANGED
|
@@ -1,25 +1,35 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
import { Options } from "./typing.js";
|
|
2
|
+
import { QQBotConnectionState } from "./api.js";
|
|
3
|
+
import { QQBotClients } from "./client.websoket.js";
|
|
4
|
+
import { ConnectionLoginStatus, cbpPlatform } from "alemonjs";
|
|
5
|
+
//#region src/sdk/registry.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The only CBP dispatcher for QQ WebSocket mode. It keeps bot credentials,
|
|
8
|
+
* message sequence state and action routing isolated while exposing one
|
|
9
|
+
* platform connection to AlemonJS.
|
|
10
|
+
*/
|
|
11
|
+
declare class QQBotRegistry {
|
|
12
|
+
#private;
|
|
13
|
+
constructor(defaultBot?: string, cbp?: ReturnType<typeof cbpPlatform>, getLoginStatus?: () => ConnectionLoginStatus);
|
|
14
|
+
add(botId: string, options: Options): QQBotClients;
|
|
15
|
+
get(botId?: string): QQBotClients;
|
|
16
|
+
getConnectionStatus(botId?: string): {
|
|
17
|
+
Platform: string;
|
|
18
|
+
state: QQBotConnectionState;
|
|
19
|
+
bots: {
|
|
20
|
+
state: QQBotConnectionState;
|
|
21
|
+
transport: 'websocket' | 'webhook' | 'proxy' | null;
|
|
22
|
+
reconnectAttempts: number;
|
|
23
|
+
heartbeatAcknowledged: boolean;
|
|
24
|
+
sessionId?: string;
|
|
25
|
+
sequence?: number;
|
|
26
|
+
resumed: boolean;
|
|
27
|
+
lastError?: string;
|
|
28
|
+
BotId: string;
|
|
29
|
+
}[];
|
|
30
|
+
login?: ConnectionLoginStatus;
|
|
31
|
+
};
|
|
32
|
+
disconnect(): void;
|
|
25
33
|
}
|
|
34
|
+
//#endregion
|
|
35
|
+
export { QQBotRegistry };
|
package/lib/sdk/registry.js
CHANGED
|
@@ -1,116 +1,136 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { getQQBotConfig } from "../config.js";
|
|
2
|
+
import { register } from "../register.js";
|
|
3
|
+
import { QQBotClients } from "./client.websoket.js";
|
|
4
|
+
import { ResultCode, cbpPlatform, createResult } from "alemonjs";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
6
|
+
//#region src/sdk/registry.ts
|
|
7
|
+
/**
|
|
8
|
+
* The only CBP dispatcher for QQ WebSocket mode. It keeps bot credentials,
|
|
9
|
+
* message sequence state and action routing isolated while exposing one
|
|
10
|
+
* platform connection to AlemonJS.
|
|
11
|
+
*/
|
|
12
|
+
var QQBotRegistry = class {
|
|
13
|
+
#bots = /* @__PURE__ */ new Map();
|
|
14
|
+
#streams = /* @__PURE__ */ new Map();
|
|
15
|
+
#defaultBot;
|
|
16
|
+
#cbp;
|
|
17
|
+
#getLoginStatus;
|
|
18
|
+
constructor(defaultBot, cbp, getLoginStatus) {
|
|
19
|
+
this.#defaultBot = defaultBot;
|
|
20
|
+
this.#getLoginStatus = getLoginStatus;
|
|
21
|
+
if (cbp) this.#cbp = cbp;
|
|
22
|
+
else {
|
|
23
|
+
const config = getQQBotConfig();
|
|
24
|
+
const port = process.env.port || config.port || 17117;
|
|
25
|
+
this.#cbp = cbpPlatform(`ws://127.0.0.1:${port}`);
|
|
26
|
+
}
|
|
27
|
+
this.#cbp.onactions((data, consume) => void this.#onAction(data, consume));
|
|
28
|
+
this.#cbp.onapis((data, consume) => void this.#onApi(data, consume));
|
|
29
|
+
}
|
|
30
|
+
add(botId, options) {
|
|
31
|
+
if (!botId) throw new Error("QQ BotId is required");
|
|
32
|
+
if (this.#bots.has(botId)) throw new Error(`Duplicate QQ BotId: ${botId}`);
|
|
33
|
+
const client = new QQBotClients({
|
|
34
|
+
...options,
|
|
35
|
+
app_id: botId
|
|
36
|
+
});
|
|
37
|
+
client.setStreamLifecycleListener((streamId) => this.#streams.delete(streamId));
|
|
38
|
+
client.setConnectionStatusListener((status, previous) => {
|
|
39
|
+
if (status.state === "ready" && previous.state !== "ready") this.#cbp.send({
|
|
40
|
+
name: "connection.ready",
|
|
41
|
+
value: "",
|
|
42
|
+
Platform: "qq-bot",
|
|
43
|
+
BotId: botId,
|
|
44
|
+
transport: status.transport,
|
|
45
|
+
resumed: status.resumed
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
const adapter = register(client, {
|
|
49
|
+
botId,
|
|
50
|
+
cbp: this.#cbp,
|
|
51
|
+
bindActions: false
|
|
52
|
+
});
|
|
53
|
+
this.#bots.set(botId, {
|
|
54
|
+
client,
|
|
55
|
+
adapter
|
|
56
|
+
});
|
|
57
|
+
return client;
|
|
58
|
+
}
|
|
59
|
+
get(botId) {
|
|
60
|
+
return botId ? this.#bots.get(botId)?.client : void 0;
|
|
61
|
+
}
|
|
62
|
+
getConnectionStatus(botId) {
|
|
63
|
+
const bots = [...this.#bots].flatMap(([BotId, entry]) => {
|
|
64
|
+
if (botId && botId !== BotId) return [];
|
|
65
|
+
return [{
|
|
66
|
+
BotId,
|
|
67
|
+
...entry.client.getConnectionStatus()
|
|
68
|
+
}];
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
Platform: "qq-bot",
|
|
72
|
+
state: bots.some((item) => item.state === "ready") ? "ready" : bots.some((item) => item.state === "connecting" || item.state === "reconnecting") ? "connecting" : bots[0]?.state || "idle",
|
|
73
|
+
bots,
|
|
74
|
+
...this.#getLoginStatus ? { login: this.#getLoginStatus() } : {}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
disconnect() {
|
|
78
|
+
for (const { client } of this.#bots.values()) client.disconnect();
|
|
79
|
+
this.#streams.clear();
|
|
80
|
+
}
|
|
81
|
+
#requestedBotId(data) {
|
|
82
|
+
const payload = data?.payload || {};
|
|
83
|
+
return payload.target?.BotId || payload.BotId || payload.event?.BotId || payload.event?.Platform?.BotId;
|
|
84
|
+
}
|
|
85
|
+
#select(data) {
|
|
86
|
+
const requested = this.#requestedBotId(data);
|
|
87
|
+
if (requested) {
|
|
88
|
+
const entry = this.#bots.get(String(requested));
|
|
89
|
+
return entry ? { entry } : { error: `BotId ${requested} is not configured (available: ${[...this.#bots.keys()].join(", ") || "none"})` };
|
|
90
|
+
}
|
|
91
|
+
if (this.#bots.size === 1) return { entry: this.#bots.values().next().value };
|
|
92
|
+
if (this.#defaultBot) return { entry: this.#bots.get(this.#defaultBot) };
|
|
93
|
+
return { error: "BotId is required because multiple QQ bots are active and no default_bot is configured" };
|
|
94
|
+
}
|
|
95
|
+
async #onAction(data, consume) {
|
|
96
|
+
if (data?.action === "connection.status") {
|
|
97
|
+
const botId = data?.payload?.BotId;
|
|
98
|
+
const status = this.getConnectionStatus(botId);
|
|
99
|
+
if (botId && !status.bots.length) consume([createResult(ResultCode.FailParams, `BotId ${botId} is not configured`, null)]);
|
|
100
|
+
else consume([createResult(ResultCode.Ok, data.action, status)]);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { entry, error } = this.#select(data);
|
|
104
|
+
if (!entry) {
|
|
105
|
+
consume([createResult(ResultCode.FailParams, error || "QQ Bot is unavailable", null)]);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await entry.adapter.onAction(data, consume);
|
|
109
|
+
}
|
|
110
|
+
async #onApi(data, consume) {
|
|
111
|
+
const params = data?.payload?.params;
|
|
112
|
+
const method = String(data?.payload?.key || "").split(".").at(-1);
|
|
113
|
+
const streamId = method === "streamUpdate" || method === "streamComplete" || method === "streamCancel" ? params?.[0] : void 0;
|
|
114
|
+
const requested = (Array.isArray(params) ? params[0]?.BotId : void 0) || (streamId ? this.#streams.get(String(streamId)) : void 0);
|
|
115
|
+
const { entry, error } = this.#select({ payload: {
|
|
116
|
+
BotId: requested,
|
|
117
|
+
event: data?.payload?.event
|
|
118
|
+
} });
|
|
119
|
+
if (!entry) {
|
|
120
|
+
consume([createResult(ResultCode.FailParams, error || "QQ Bot is unavailable", null)]);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await entry.adapter.onApi(data, (results) => {
|
|
124
|
+
const result = results.find((item) => item.code === ResultCode.Ok)?.data;
|
|
125
|
+
if (method === "streamOpen" && result?.streamId) this.#streams.set(String(result.streamId), requested || this.#botIdFor(entry));
|
|
126
|
+
if (streamId && (method === "streamComplete" || method === "streamCancel")) this.#streams.delete(String(streamId));
|
|
127
|
+
consume(results);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
#botIdFor(entry) {
|
|
131
|
+
return [...this.#bots.entries()].find(([, value]) => value === entry)?.[0] || "";
|
|
132
|
+
}
|
|
133
|
+
};
|
|
115
134
|
|
|
116
|
-
|
|
135
|
+
//#endregion
|
|
136
|
+
export { QQBotRegistry };
|
package/lib/sdk/session.d.ts
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
//#region src/sdk/session.d.ts
|
|
2
|
+
type PersistedSession = {
|
|
3
|
+
botId: string;
|
|
4
|
+
sessionId: string;
|
|
5
|
+
sequence: number;
|
|
5
6
|
};
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
interface SessionStore {
|
|
8
|
+
load(botId: string): Promise<PersistedSession | null>;
|
|
9
|
+
save(session: PersistedSession): Promise<void>;
|
|
10
|
+
clear(botId: string): Promise<void>;
|
|
10
11
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
/** Default store. Sessions contain no access token or application secret. */
|
|
13
|
+
declare class FileSessionStore implements SessionStore {
|
|
14
|
+
#private;
|
|
15
|
+
private readonly root;
|
|
16
|
+
constructor(root?: string);
|
|
17
|
+
load(botId: string): Promise<PersistedSession | null>;
|
|
18
|
+
save(session: PersistedSession): Promise<void>;
|
|
19
|
+
clear(botId: string): Promise<void>;
|
|
18
20
|
}
|
|
21
|
+
//#endregion
|
|
22
|
+
export { FileSessionStore, PersistedSession, SessionStore };
|