@karinjs/adapter-qqbot 2.2.1 → 3.0.1
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/apps/cleanup.d.ts +6 -2
- package/dist/apps/cleanup.js +5 -10
- package/dist/apps/login.d.ts +2 -16
- package/dist/apps/login.js +138 -85
- package/dist/config-D1_LqrcJ.js +196 -0
- package/dist/index.d.ts +409 -234
- package/dist/index.js +347 -365
- package/dist/logger-sza6kuy3.js +27 -0
- package/dist/message-CVJVFAGR.js +1844 -0
- package/dist/utils-lb9ZdWay.js +32 -0
- package/dist/web.config.d.ts +3 -5
- package/dist/web.config.js +44 -84
- package/package.json +8 -5
- package/dist/common-by1GpV0b.js +0 -49
- package/dist/config-B_Gc9JKZ.js +0 -253
- package/dist/message-Dt4QkDYn.js +0 -1739
- package/dist/onboard-BXQ1cG8h.js +0 -252
- package/dist/utils-DIq0lGMt.js +0 -4
package/dist/onboard-BXQ1cG8h.js
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
import { i as getDefaultConfig, m as log, n as config, o as writeConfig } from "./config-B_Gc9JKZ.js";
|
|
2
|
-
import { a as sleep } from "./common-by1GpV0b.js";
|
|
3
|
-
import qrcode from "qrcode";
|
|
4
|
-
import axios from "node-karin/axios";
|
|
5
|
-
import crypto from "node:crypto";
|
|
6
|
-
|
|
7
|
-
//#region src/core/onboard/crypto.ts
|
|
8
|
-
/**
|
|
9
|
-
* 生成 256-bit 随机 AES 密钥(base64)
|
|
10
|
-
*/
|
|
11
|
-
const generateBindKey = () => crypto.randomBytes(32).toString("base64");
|
|
12
|
-
/**
|
|
13
|
-
* 解密 bot_encrypt_secret(AES-256-GCM)
|
|
14
|
-
*
|
|
15
|
-
* 密文布局:IV (12 bytes) || ciphertext (N bytes) || AuthTag (16 bytes)
|
|
16
|
-
*/
|
|
17
|
-
const decryptSecret = (encryptedBase64, keyBase64) => {
|
|
18
|
-
const key = Buffer.from(keyBase64, "base64");
|
|
19
|
-
if (key.length !== 32) throw new Error(`AES key length must be 32 bytes, got ${key.length}`);
|
|
20
|
-
const raw = Buffer.from(encryptedBase64, "base64");
|
|
21
|
-
const iv = raw.subarray(0, 12);
|
|
22
|
-
const ciphertext = raw.subarray(12, raw.length - 16);
|
|
23
|
-
const authTag = raw.subarray(raw.length - 16);
|
|
24
|
-
const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
|
|
25
|
-
decipher.setAuthTag(authTag);
|
|
26
|
-
let plaintext = decipher.update(ciphertext, void 0, "utf8");
|
|
27
|
-
plaintext += decipher.final("utf8");
|
|
28
|
-
return plaintext;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
//#endregion
|
|
32
|
-
//#region src/core/onboard/portal.ts
|
|
33
|
-
/** QQ 开放平台扫码绑定服务域名 */
|
|
34
|
-
const PORTAL_HOST = "q.qq.com";
|
|
35
|
-
const CREATE_PATH = "/lite/create_bind_task";
|
|
36
|
-
const POLL_PATH = "/lite/poll_bind_result";
|
|
37
|
-
/** 扫码页面模板 */
|
|
38
|
-
const CONNECT_URL = "https://q.qq.com/qqbot/openclaw/connect.html?task_id={task_id}&_wv=2&source=karin";
|
|
39
|
-
const headers = {
|
|
40
|
-
"Content-Type": "application/json",
|
|
41
|
-
Accept: "application/json"
|
|
42
|
-
};
|
|
43
|
-
/** 绑定状态 */
|
|
44
|
-
const BindStatus = {
|
|
45
|
-
NONE: 0,
|
|
46
|
-
PENDING: 1,
|
|
47
|
-
COMPLETED: 2,
|
|
48
|
-
EXPIRED: 3
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* 创建绑定任务
|
|
52
|
-
* @returns task_id 与本地生成的 AES key
|
|
53
|
-
*/
|
|
54
|
-
const createBindTask = async () => {
|
|
55
|
-
const key = generateBindKey();
|
|
56
|
-
const { data } = await axios.post(`https://${PORTAL_HOST}${CREATE_PATH}`, { key }, { headers });
|
|
57
|
-
if (data.retcode !== 0) throw new Error(`create_bind_task: ${data.msg || "unknown error"}`);
|
|
58
|
-
const taskId = data.data?.task_id;
|
|
59
|
-
if (!taskId) throw new Error("create_bind_task: missing task_id");
|
|
60
|
-
return {
|
|
61
|
-
taskId,
|
|
62
|
-
aesKey: key
|
|
63
|
-
};
|
|
64
|
-
};
|
|
65
|
-
/**
|
|
66
|
-
* 轮询绑定结果
|
|
67
|
-
*/
|
|
68
|
-
const pollBindResult = async (taskId) => {
|
|
69
|
-
const { data } = await axios.post(`https://${PORTAL_HOST}${POLL_PATH}`, { task_id: taskId }, { headers });
|
|
70
|
-
if (data.retcode !== 0) throw new Error(`poll_bind_result: ${data.msg || "unknown error"}`);
|
|
71
|
-
const d = data.data ?? {};
|
|
72
|
-
return {
|
|
73
|
-
status: d.status ?? BindStatus.NONE,
|
|
74
|
-
appId: String(d.bot_appid ?? ""),
|
|
75
|
-
encryptedSecret: d.bot_encrypt_secret ?? "",
|
|
76
|
-
userOpenid: d.user_openid ?? ""
|
|
77
|
-
};
|
|
78
|
-
};
|
|
79
|
-
/**
|
|
80
|
-
* 构造扫码 URL
|
|
81
|
-
*/
|
|
82
|
-
const buildConnectUrl = (taskId) => CONNECT_URL.replace("{task_id}", encodeURIComponent(taskId));
|
|
83
|
-
/**
|
|
84
|
-
* 通过 appId + secret 直接调一次 @me,拿到 bot 昵称
|
|
85
|
-
* 用于扫码完成后的展示与写入配置
|
|
86
|
-
*/
|
|
87
|
-
const fetchBotName = async (appId, secret) => {
|
|
88
|
-
try {
|
|
89
|
-
const accessToken = (await axios.post("https://bots.qq.com/app/getAppAccessToken", {
|
|
90
|
-
appId,
|
|
91
|
-
clientSecret: secret
|
|
92
|
-
})).data?.access_token;
|
|
93
|
-
if (!accessToken) return "";
|
|
94
|
-
return (await axios.get("https://api.sgroup.qq.com/users/@me", { headers: { Authorization: `QQBot ${accessToken}` } })).data?.username || "";
|
|
95
|
-
} catch {
|
|
96
|
-
return "";
|
|
97
|
-
}
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
//#endregion
|
|
101
|
-
//#region src/core/onboard/qr.ts
|
|
102
|
-
/** 默认轮询间隔(毫秒) */
|
|
103
|
-
const POLL_INTERVAL = 2e3;
|
|
104
|
-
/** 默认总超时(秒) */
|
|
105
|
-
const DEFAULT_TIMEOUT_S = 600;
|
|
106
|
-
/** 单张二维码硬编码有效期:60s,到时主动判定过期触发刷新 */
|
|
107
|
-
const QR_TTL_MS = 6e4;
|
|
108
|
-
/** 二维码过期最大刷新次数 */
|
|
109
|
-
const MAX_REFRESHES = 3;
|
|
110
|
-
/**
|
|
111
|
-
* 生成二维码(同时拿到 ascii / base64)
|
|
112
|
-
*/
|
|
113
|
-
const renderQr = async (url) => {
|
|
114
|
-
return {
|
|
115
|
-
ascii: await qrcode.toString(url, {
|
|
116
|
-
type: "terminal",
|
|
117
|
-
small: true
|
|
118
|
-
}).catch(() => ""),
|
|
119
|
-
base64: `base64://${(await qrcode.toDataURL(url, {
|
|
120
|
-
width: 320,
|
|
121
|
-
margin: 2
|
|
122
|
-
})).split(",")[1]}`
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
/**
|
|
126
|
-
* 扫码登录主流程:
|
|
127
|
-
* 1. 创建绑定任务,调用 onQr 暴露二维码
|
|
128
|
-
* 2. 轮询直到 COMPLETED / 二维码过期刷新 / 超时
|
|
129
|
-
* 3. 解密返回 secret
|
|
130
|
-
*/
|
|
131
|
-
const qrRegister = async (opts = {}) => {
|
|
132
|
-
const { timeoutSeconds = DEFAULT_TIMEOUT_S, printTerminal = false, onQr } = opts;
|
|
133
|
-
const deadline = Date.now() + timeoutSeconds * 1e3;
|
|
134
|
-
for (let refresh = 0; refresh <= MAX_REFRESHES; refresh++) {
|
|
135
|
-
let task;
|
|
136
|
-
try {
|
|
137
|
-
task = await createBindTask();
|
|
138
|
-
} catch (err) {
|
|
139
|
-
log("error", "[扫码登录] 创建绑定任务失败:", err);
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
const url = buildConnectUrl(task.taskId);
|
|
143
|
-
const { ascii, base64 } = await renderQr(url);
|
|
144
|
-
const qrDeadline = Date.now() + QR_TTL_MS;
|
|
145
|
-
if (printTerminal) {
|
|
146
|
-
if (ascii) {
|
|
147
|
-
console.log();
|
|
148
|
-
console.log(ascii);
|
|
149
|
-
}
|
|
150
|
-
console.log(` 请使用 QQ 扫码(60s 内有效),或在手机 QQ 打开:\n ${url}\n`);
|
|
151
|
-
}
|
|
152
|
-
if (onQr) try {
|
|
153
|
-
await onQr({
|
|
154
|
-
url,
|
|
155
|
-
ascii,
|
|
156
|
-
base64,
|
|
157
|
-
refresh
|
|
158
|
-
});
|
|
159
|
-
} catch (err) {
|
|
160
|
-
log("warn", "[扫码登录] onQr 回调异常:", err);
|
|
161
|
-
}
|
|
162
|
-
let expired = false;
|
|
163
|
-
while (Date.now() < deadline) {
|
|
164
|
-
if (Date.now() >= qrDeadline) {
|
|
165
|
-
expired = true;
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
let result;
|
|
169
|
-
try {
|
|
170
|
-
result = await pollBindResult(task.taskId);
|
|
171
|
-
} catch {
|
|
172
|
-
await sleep(POLL_INTERVAL);
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
if (result.status === BindStatus.COMPLETED) {
|
|
176
|
-
if (!result.encryptedSecret) {
|
|
177
|
-
log("error", "[扫码登录] 绑定结果缺少加密密钥");
|
|
178
|
-
return null;
|
|
179
|
-
}
|
|
180
|
-
let secret;
|
|
181
|
-
try {
|
|
182
|
-
secret = decryptSecret(result.encryptedSecret, task.aesKey);
|
|
183
|
-
} catch (err) {
|
|
184
|
-
log("error", "[扫码登录] 解密 secret 失败:", err);
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
log("info", `[扫码登录] 授权成功,App ID: ${result.appId}`);
|
|
188
|
-
return {
|
|
189
|
-
appId: result.appId,
|
|
190
|
-
secret,
|
|
191
|
-
userOpenid: result.userOpenid
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
if (result.status === BindStatus.EXPIRED) {
|
|
195
|
-
expired = true;
|
|
196
|
-
break;
|
|
197
|
-
}
|
|
198
|
-
await sleep(POLL_INTERVAL);
|
|
199
|
-
}
|
|
200
|
-
if (expired) {
|
|
201
|
-
if (refresh >= MAX_REFRESHES) {
|
|
202
|
-
log("error", `[扫码登录] 二维码已过期 4 次,终止流程`);
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
log("info", `[扫码登录] 二维码 60s 内未完成扫码,正在刷新 (${refresh + 1}/${MAX_REFRESHES})`);
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
log("error", `[扫码登录] 等待扫码总超时(${timeoutSeconds}s)`);
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
return null;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
//#endregion
|
|
215
|
-
//#region src/core/onboard/index.ts
|
|
216
|
-
/** 是否需要触发扫码引导:完全没有 bot 配置时为 true */
|
|
217
|
-
const needQrOnboard = () => config().length === 0;
|
|
218
|
-
/**
|
|
219
|
-
* 执行扫码登录并把结果写入 config.json
|
|
220
|
-
*
|
|
221
|
-
* - 同 appId 已存在 → 覆盖 secret / name
|
|
222
|
-
* - 不存在 → 追加新配置
|
|
223
|
-
*
|
|
224
|
-
* 支持 `opts.onQr` 把新二维码透出给调用方(如 #QQ登录 指令把图片回给用户)
|
|
225
|
-
*/
|
|
226
|
-
const runQrOnboard = async (opts = {}) => {
|
|
227
|
-
const result = await qrRegister(opts);
|
|
228
|
-
if (!result) return false;
|
|
229
|
-
const name = await fetchBotName(result.appId, result.secret);
|
|
230
|
-
if (name) log("info", `[扫码登录] Bot 名称: ${name}`);
|
|
231
|
-
const list = config();
|
|
232
|
-
const idx = list.findIndex((item) => item.appId === result.appId);
|
|
233
|
-
if (idx >= 0) {
|
|
234
|
-
list[idx].secret = result.secret;
|
|
235
|
-
if (name) list[idx].name = name;
|
|
236
|
-
log("info", `[扫码登录] 已更新已有配置: ${result.appId}`);
|
|
237
|
-
} else {
|
|
238
|
-
const defaults = getDefaultConfig()[0];
|
|
239
|
-
list.push({
|
|
240
|
-
...defaults,
|
|
241
|
-
name: name || "",
|
|
242
|
-
appId: result.appId,
|
|
243
|
-
secret: result.secret
|
|
244
|
-
});
|
|
245
|
-
log("info", `[扫码登录] 已添加新配置: ${result.appId}`);
|
|
246
|
-
}
|
|
247
|
-
writeConfig(list);
|
|
248
|
-
return true;
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
//#endregion
|
|
252
|
-
export { runQrOnboard as n, needQrOnboard as t };
|
package/dist/utils-DIq0lGMt.js
DELETED