koishi-plugin-adapter-onebot-multi 0.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/lib/index.js ADDED
@@ -0,0 +1,3165 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
+ var __export = (target, all) => {
9
+ for (var name2 in all)
10
+ __defProp(target, name2, { get: all[name2], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ BaseBot: () => BaseBot2,
34
+ CQCode: () => CQCode,
35
+ Config: () => Config,
36
+ ConfigManager: () => ConfigManager,
37
+ HeartbeatMonitor: () => HeartbeatMonitor,
38
+ OneBot: () => utils_exports,
39
+ OneBotBot: () => OneBotBot,
40
+ OneBotMessageEncoder: () => OneBotMessageEncoder,
41
+ PRIVATE_PFX: () => PRIVATE_PFX,
42
+ PanelConfig: () => PanelConfig,
43
+ QQGuildBot: () => QQGuildBot,
44
+ StatusManager: () => StatusManager,
45
+ StatusPanel: () => StatusPanel,
46
+ WsClient: () => WsClient,
47
+ WsServer: () => WsServer,
48
+ accept: () => accept,
49
+ apply: () => apply,
50
+ inject: () => inject,
51
+ name: () => name
52
+ });
53
+ module.exports = __toCommonJS(src_exports);
54
+ var import_koishi11 = require("koishi");
55
+
56
+ // src/bot/index.ts
57
+ var import_koishi8 = require("koishi");
58
+
59
+ // src/bot/base.ts
60
+ var import_koishi4 = require("koishi");
61
+
62
+ // src/utils.ts
63
+ var utils_exports = {};
64
+ __export(utils_exports, {
65
+ Internal: () => Internal,
66
+ SafetyLevel: () => SafetyLevel,
67
+ TimeoutError: () => TimeoutError,
68
+ adaptChannel: () => adaptChannel,
69
+ adaptGuild: () => adaptGuild,
70
+ adaptMessage: () => adaptMessage,
71
+ adaptQQGuildMemberInfo: () => adaptQQGuildMemberInfo,
72
+ adaptQQGuildMemberProfile: () => adaptQQGuildMemberProfile,
73
+ adaptSession: () => adaptSession,
74
+ decodeGuildMember: () => decodeGuildMember,
75
+ decodeUser: () => decodeUser,
76
+ dispatchSession: () => dispatchSession
77
+ });
78
+ var import_koishi2 = require("koishi");
79
+ var qface = __toESM(require("qface"));
80
+
81
+ // src/types.ts
82
+ var import_koishi = require("koishi");
83
+ var SafetyLevel = /* @__PURE__ */ ((SafetyLevel2) => {
84
+ SafetyLevel2[SafetyLevel2["safe"] = 0] = "safe";
85
+ SafetyLevel2[SafetyLevel2["unknown"] = 1] = "unknown";
86
+ SafetyLevel2[SafetyLevel2["danger"] = 2] = "danger";
87
+ return SafetyLevel2;
88
+ })(SafetyLevel || {});
89
+ var TimeoutError = class extends Error {
90
+ static {
91
+ __name(this, "TimeoutError");
92
+ }
93
+ constructor(args, url) {
94
+ super(`Timeout with request ${url}, args: ${JSON.stringify(args)}`);
95
+ Object.defineProperties(this, {
96
+ args: { value: args },
97
+ url: { value: url }
98
+ });
99
+ }
100
+ };
101
+ var SenderError = class extends Error {
102
+ static {
103
+ __name(this, "SenderError");
104
+ }
105
+ constructor(args, url, retcode) {
106
+ super(`Error with request ${url}, args: ${JSON.stringify(args)}, retcode: ${retcode}`);
107
+ Object.defineProperties(this, {
108
+ code: { value: retcode },
109
+ args: { value: args },
110
+ url: { value: url }
111
+ });
112
+ }
113
+ };
114
+ var Internal = class _Internal {
115
+ constructor(bot) {
116
+ this.bot = bot;
117
+ }
118
+ static {
119
+ __name(this, "Internal");
120
+ }
121
+ async _get(action, params = {}) {
122
+ this.bot.logger.debug("[request] %s %o", action, params);
123
+ const response = await this._request(action, params);
124
+ this.bot.logger.debug("[response] %o", response);
125
+ const { data, retcode } = response;
126
+ if (retcode === 0) return data;
127
+ throw new SenderError(params, action, retcode);
128
+ }
129
+ async setGroupAnonymousBan(group_id, meta, duration) {
130
+ const args = { group_id, duration };
131
+ args[typeof meta === "string" ? "flag" : "anonymous"] = meta;
132
+ await this._get("set_group_anonymous_ban", args);
133
+ }
134
+ async setGroupAnonymousBanAsync(group_id, meta, duration) {
135
+ const args = { group_id, duration };
136
+ args[typeof meta === "string" ? "flag" : "anonymous"] = meta;
137
+ await this._get("set_group_anonymous_ban_async", args);
138
+ }
139
+ static asyncPrefixes = ["set", "send", "delete", "create", "upload", "move", "rename"];
140
+ static prepareMethod(name2) {
141
+ const prop = (0, import_koishi.camelize)(name2.replace(/^[_.]/, ""));
142
+ const isAsync = _Internal.asyncPrefixes.some((prefix) => prop.startsWith(prefix));
143
+ return [prop, isAsync];
144
+ }
145
+ static prepareArg(name2, params, args) {
146
+ const fixedArg = Object.fromEntries(params.map((name3, index) => [name3, args[index]]));
147
+ for (const key in fixedArg) {
148
+ if (!name2.includes("guild") && key.endsWith("_id")) {
149
+ const value = +fixedArg[key];
150
+ if (Math.abs(value) < 4294967296) {
151
+ fixedArg[key] = value;
152
+ }
153
+ }
154
+ }
155
+ return fixedArg;
156
+ }
157
+ static define(name2, ...params) {
158
+ const [prop, isAsync] = _Internal.prepareMethod(name2);
159
+ _Internal.prototype[prop] = async function(...args) {
160
+ const data = await this._get(name2, _Internal.prepareArg(name2, params, args));
161
+ if (!isAsync) return data;
162
+ };
163
+ isAsync && (_Internal.prototype[prop + "Async"] = async function(...args) {
164
+ await this._get(name2 + "_async", _Internal.prepareArg(name2, params, args));
165
+ });
166
+ }
167
+ static defineExtract(name2, key, ...params) {
168
+ const [prop, isAsync] = _Internal.prepareMethod(name2);
169
+ _Internal.prototype[prop] = async function(...args) {
170
+ const data = await this._get(name2, _Internal.prepareArg(name2, params, args));
171
+ return data[key];
172
+ };
173
+ isAsync && (_Internal.prototype[prop + "Async"] = async function(...args) {
174
+ await this._get(name2 + "_async", _Internal.prepareArg(name2, params, args));
175
+ });
176
+ }
177
+ };
178
+ Internal.defineExtract("send_private_msg", "message_id", "user_id", "message", "auto_escape");
179
+ Internal.defineExtract("send_group_msg", "message_id", "group_id", "message", "auto_escape");
180
+ Internal.defineExtract("send_group_forward_msg", "message_id", "group_id", "messages");
181
+ Internal.defineExtract("send_private_forward_msg", "message_id", "user_id", "messages");
182
+ Internal.define("delete_msg", "message_id");
183
+ Internal.define("mark_msg_as_read", "message_id");
184
+ Internal.define("set_essence_msg", "message_id");
185
+ Internal.define("delete_essence_msg", "message_id");
186
+ Internal.define("send_group_sign", "group_id");
187
+ Internal.define("send_like", "user_id", "times");
188
+ Internal.define("get_msg", "message_id");
189
+ Internal.define("get_essence_msg_list", "group_id");
190
+ Internal.define("ocr_image", "image");
191
+ Internal.defineExtract("get_forward_msg", "messages", "message_id");
192
+ Internal.defineExtract(".get_word_slices", "slices", "content");
193
+ Internal.define("get_group_msg_history", "group_id", "message_seq");
194
+ Internal.define("set_friend_add_request", "flag", "approve", "remark");
195
+ Internal.define("set_group_add_request", "flag", "sub_type", "approve", "reason");
196
+ Internal.defineExtract("_get_model_show", "variants", "model");
197
+ Internal.define("_set_model_show", "model", "model_show");
198
+ Internal.define("set_group_kick", "group_id", "user_id", "reject_add_request");
199
+ Internal.define("set_group_ban", "group_id", "user_id", "duration");
200
+ Internal.define("set_group_whole_ban", "group_id", "enable");
201
+ Internal.define("set_group_admin", "group_id", "user_id", "enable");
202
+ Internal.define("set_group_anonymous", "group_id", "enable");
203
+ Internal.define("set_group_card", "group_id", "user_id", "card");
204
+ Internal.define("set_group_leave", "group_id", "is_dismiss");
205
+ Internal.define("set_group_special_title", "group_id", "user_id", "special_title", "duration");
206
+ Internal.define("set_group_name", "group_id", "group_name");
207
+ Internal.define("set_group_portrait", "group_id", "file", "cache");
208
+ Internal.define("_send_group_notice", "group_id", "content", "image", "pinned", "confirm_required");
209
+ Internal.define("_get_group_notice", "group_id");
210
+ Internal.define("_del_group_notice", "group_id", "notice_id");
211
+ Internal.define("get_group_at_all_remain", "group_id");
212
+ Internal.define("get_login_info");
213
+ Internal.define("qidian_get_login_info");
214
+ Internal.define("set_qq_profile", "nickname", "company", "email", "college", "personal_note");
215
+ Internal.define("set_qq_avatar", "file");
216
+ Internal.define("set_online_status", "status", "ext_status", "battery_status");
217
+ Internal.define("get_stranger_info", "user_id", "no_cache");
218
+ Internal.define("_get_vip_info", "user_id");
219
+ Internal.define("get_friend_list");
220
+ Internal.define("get_unidirectional_friend_list");
221
+ Internal.define("delete_friend", "user_id");
222
+ Internal.define("delete_unidirectional_friend", "user_id");
223
+ Internal.define("get_group_info", "group_id", "no_cache");
224
+ Internal.define("get_group_list", "no_cache");
225
+ Internal.define("get_group_member_info", "group_id", "user_id", "no_cache");
226
+ Internal.define("get_group_member_list", "group_id");
227
+ Internal.define("get_group_honor_info", "group_id", "type");
228
+ Internal.define("get_group_system_msg");
229
+ Internal.define("get_group_file_system_info", "group_id");
230
+ Internal.define("get_group_root_files", "group_id");
231
+ Internal.define("get_group_files_by_folder", "group_id", "folder_id");
232
+ Internal.define("upload_private_file", "user_id", "file", "name");
233
+ Internal.define("upload_group_file", "group_id", "file", "name", "folder");
234
+ Internal.define("create_group_file_folder", "group_id", "folder_id", "name");
235
+ Internal.define("delete_group_folder", "group_id", "folder_id");
236
+ Internal.define("delete_group_file", "group_id", "folder_id", "file_id", "busid");
237
+ Internal.defineExtract("get_group_file_url", "url", "group_id", "file_id", "busid");
238
+ Internal.defineExtract("download_file", "file", "url", "headers", "thread_count");
239
+ Internal.defineExtract("get_online_clients", "clients", "no_cache");
240
+ Internal.defineExtract("check_url_safely", "level", "url");
241
+ Internal.defineExtract("get_cookies", "cookies", "domain");
242
+ Internal.defineExtract("get_csrf_token", "token");
243
+ Internal.define("get_credentials", "domain");
244
+ Internal.define("get_record", "file", "out_format", "full_path");
245
+ Internal.define("get_image", "file");
246
+ Internal.defineExtract("can_send_image", "yes");
247
+ Internal.defineExtract("can_send_record", "yes");
248
+ Internal.define("get_status");
249
+ Internal.define("get_version_info");
250
+ Internal.define("set_restart", "delay");
251
+ Internal.define("reload_event_filter");
252
+ Internal.define("clean_cache");
253
+ Internal.define("get_guild_service_profile");
254
+ Internal.define("get_guild_list");
255
+ Internal.define("get_guild_meta_by_guest", "guild_id");
256
+ Internal.define("get_guild_channel_list", "guild_id", "no_cache");
257
+ Internal.define("get_guild_member_list", "guild_id", "next_token");
258
+ Internal.define("get_guild_member_profile", "guild_id", "user_id");
259
+ Internal.defineExtract("send_guild_channel_msg", "message_id", "guild_id", "channel_id", "message");
260
+ Internal.define("upload_image", "file");
261
+ Internal.defineExtract("get_private_file_url", "url", "user_id", "file_id", "file_hash");
262
+ Internal.define("move_group_file", "group_id", "file_id", "parent_directory", "target_directory");
263
+ Internal.define("delete_group_file_folder", "group_id", "folder_id");
264
+ Internal.define("rename_group_file_folder", "group_id", "folder_id", "new_folder_name");
265
+
266
+ // src/utils.ts
267
+ var decodeUser = /* @__PURE__ */ __name((user) => ({
268
+ id: user.tiny_id || user.user_id.toString(),
269
+ name: user.nickname,
270
+ userId: user.tiny_id || user.user_id.toString(),
271
+ avatar: user.user_id ? `http://q.qlogo.cn/headimg_dl?dst_uin=${user.user_id}&spec=640` : void 0,
272
+ username: user.nickname
273
+ }), "decodeUser");
274
+ var decodeGuildMember = /* @__PURE__ */ __name((user) => ({
275
+ user: decodeUser(user),
276
+ nick: user.card,
277
+ roles: [user.role]
278
+ }), "decodeGuildMember");
279
+ var adaptQQGuildMemberInfo = /* @__PURE__ */ __name((user) => ({
280
+ user: {
281
+ id: user.tiny_id,
282
+ name: user.nickname,
283
+ isBot: user.role_name === "机器人"
284
+ },
285
+ name: user.nickname,
286
+ roles: user.role_name ? [user.role_name] : []
287
+ }), "adaptQQGuildMemberInfo");
288
+ var adaptQQGuildMemberProfile = /* @__PURE__ */ __name((user) => ({
289
+ user: {
290
+ id: user.tiny_id,
291
+ name: user.nickname,
292
+ isBot: user.roles?.some((r) => r.role_name === "机器人")
293
+ },
294
+ name: user.nickname,
295
+ roles: user.roles?.map((r) => r.role_name) || []
296
+ }), "adaptQQGuildMemberProfile");
297
+ async function adaptMessage(bot, data, message = {}, payload = message) {
298
+ message.id = message.messageId = data.message_id.toString();
299
+ const chain = CQCode.parse(data.message);
300
+ if (bot.config.advanced.splitMixedContent) {
301
+ chain.forEach((item, index) => {
302
+ if (item.type !== "image") return;
303
+ const left = chain[index - 1];
304
+ if (left && left.type === "text" && left.attrs.content.trimEnd() === left.attrs.content) {
305
+ left.attrs.content += " ";
306
+ }
307
+ const right = chain[index + 1];
308
+ if (right && right.type === "text" && right.attrs.content.trimStart() === right.attrs.content) {
309
+ right.attrs.content = " " + right.attrs.content;
310
+ }
311
+ });
312
+ }
313
+ message.elements = import_koishi2.h.transform(chain, {
314
+ at(attrs) {
315
+ if (attrs.qq !== "all") return import_koishi2.h.at(attrs.qq, { name: attrs.name });
316
+ return (0, import_koishi2.h)("at", { type: "all" });
317
+ },
318
+ face({ id }) {
319
+ const name2 = qface.get(id)?.QDes.slice(1);
320
+ return (0, import_koishi2.h)("face", { id, name: name2, platform: bot.platform }, [
321
+ import_koishi2.h.image(qface.getUrl(id))
322
+ ]);
323
+ },
324
+ image(attrs) {
325
+ return (0, import_koishi2.h)("img", {
326
+ src: attrs.url || attrs.file,
327
+ ...(0, import_koishi2.omit)(attrs, ["url"])
328
+ });
329
+ },
330
+ record(attrs) {
331
+ return (0, import_koishi2.h)("audio", {
332
+ src: attrs.url || attrs.file,
333
+ ...(0, import_koishi2.omit)(attrs, ["url"])
334
+ });
335
+ },
336
+ video(attrs) {
337
+ return (0, import_koishi2.h)("video", {
338
+ src: attrs.url || attrs.file,
339
+ ...(0, import_koishi2.omit)(attrs, ["url"])
340
+ });
341
+ },
342
+ file(attrs) {
343
+ return (0, import_koishi2.h)("file", {
344
+ src: attrs.url || attrs.file,
345
+ ...(0, import_koishi2.omit)(attrs, ["url"])
346
+ });
347
+ }
348
+ });
349
+ const [guildId, channelId] = decodeGuildChannelId(data);
350
+ if (message.elements[0]?.type === "reply") {
351
+ const reply = message.elements.shift();
352
+ message.quote = await bot.getMessage(channelId, reply.attrs.id).catch((error) => {
353
+ bot.logger.warn(error);
354
+ return void 0;
355
+ });
356
+ }
357
+ message.content = message.elements.join("");
358
+ if (!payload) return message;
359
+ payload.user = decodeUser(data.sender);
360
+ payload.member = decodeGuildMember(data.sender);
361
+ payload.timestamp = data.time * 1e3;
362
+ payload.guild = guildId && { id: guildId };
363
+ payload.channel = channelId && { id: channelId, type: guildId ? import_koishi2.Universal.Channel.Type.TEXT : import_koishi2.Universal.Channel.Type.DIRECT };
364
+ return message;
365
+ }
366
+ __name(adaptMessage, "adaptMessage");
367
+ var decodeGuildChannelId = /* @__PURE__ */ __name((data) => {
368
+ if (data.guild_id) {
369
+ return [data.guild_id, data.channel_id];
370
+ } else if (data.group_id) {
371
+ return [data.group_id.toString(), data.group_id.toString()];
372
+ } else {
373
+ return [void 0, "private:" + data.sender.user_id];
374
+ }
375
+ }, "decodeGuildChannelId");
376
+ var adaptGuild = /* @__PURE__ */ __name((info) => {
377
+ if (info.guild_id) {
378
+ const guild = info;
379
+ return {
380
+ id: guild.guild_id,
381
+ name: guild.guild_name
382
+ };
383
+ } else {
384
+ const group = info;
385
+ return {
386
+ id: group.group_id.toString(),
387
+ name: group.group_name
388
+ };
389
+ }
390
+ }, "adaptGuild");
391
+ var adaptChannel = /* @__PURE__ */ __name((info) => {
392
+ if (info.channel_id) {
393
+ const channel = info;
394
+ return {
395
+ id: channel.channel_id,
396
+ name: channel.channel_name,
397
+ type: import_koishi2.Universal.Channel.Type.TEXT
398
+ };
399
+ } else {
400
+ const group = info;
401
+ return {
402
+ id: group.group_id.toString(),
403
+ name: group.group_name,
404
+ type: import_koishi2.Universal.Channel.Type.TEXT
405
+ };
406
+ }
407
+ }, "adaptChannel");
408
+ async function dispatchSession(bot, data) {
409
+ if (data.self_tiny_id) {
410
+ bot = bot["guildBot"];
411
+ if (!bot) return;
412
+ }
413
+ const session = await adaptSession(bot, data);
414
+ if (!session) return;
415
+ session.setInternal("onebot", data);
416
+ bot.dispatch(session);
417
+ }
418
+ __name(dispatchSession, "dispatchSession");
419
+ async function adaptSession(bot, data) {
420
+ const session = bot.session();
421
+ session.selfId = data.self_tiny_id ? data.self_tiny_id : "" + data.self_id;
422
+ session.type = data.post_type;
423
+ if (data.post_type === "message" || data.post_type === "message_sent") {
424
+ await adaptMessage(bot, data, session.event.message = {}, session.event);
425
+ if (data.post_type === "message_sent" && !session.guildId) {
426
+ session.channelId = "private:" + data.target_id;
427
+ }
428
+ session.type = "message";
429
+ session.subtype = data.message_type === "guild" ? "group" : data.message_type;
430
+ session.isDirect = data.message_type === "private";
431
+ session.subsubtype = data.message_type;
432
+ return session;
433
+ }
434
+ session.subtype = data.sub_type;
435
+ if (data.user_id) session.userId = "" + data.user_id;
436
+ if (data.group_id) session.guildId = session.channelId = "" + data.group_id;
437
+ if (data.guild_id) session.guildId = "" + data.guild_id;
438
+ if (data.channel_id) session.channelId = "" + data.channel_id;
439
+ if (data.target_id) session["targetId"] = "" + data.target_id;
440
+ if (data.operator_id) session.operatorId = "" + data.operator_id;
441
+ if (data.message_id) session.messageId = "" + data.message_id;
442
+ if (data.post_type === "request") {
443
+ session.content = data.comment;
444
+ session.messageId = data.flag;
445
+ if (data.request_type === "friend") {
446
+ session.type = "friend-request";
447
+ session.channelId = `private:${session.userId}`;
448
+ } else if (data.sub_type === "add") {
449
+ session.type = "guild-member-request";
450
+ } else {
451
+ session.type = "guild-request";
452
+ }
453
+ } else if (data.post_type === "notice") {
454
+ switch (data.notice_type) {
455
+ case "group_recall":
456
+ session.type = "message-deleted";
457
+ session.subtype = "group";
458
+ break;
459
+ case "friend_recall":
460
+ session.type = "message-deleted";
461
+ session.subtype = "private";
462
+ session.channelId = `private:${session.userId}`;
463
+ break;
464
+ // from go-cqhttp source code, but not mentioned in official docs
465
+ case "guild_channel_recall":
466
+ session.type = "message-deleted";
467
+ session.subtype = "guild";
468
+ break;
469
+ case "friend_add":
470
+ session.type = "friend-added";
471
+ break;
472
+ case "group_admin":
473
+ session.type = "guild-member";
474
+ session.subtype = "role";
475
+ break;
476
+ case "group_ban":
477
+ session.type = "guild-member";
478
+ session.subtype = "ban";
479
+ break;
480
+ case "group_decrease":
481
+ session.type = session.userId === session.selfId ? "guild-deleted" : "guild-member-deleted";
482
+ session.subtype = session.userId === session.operatorId ? "active" : "passive";
483
+ break;
484
+ case "group_increase":
485
+ session.type = session.userId === session.selfId ? "guild-added" : "guild-member-added";
486
+ session.subtype = session.userId === session.operatorId ? "active" : "passive";
487
+ break;
488
+ case "group_card":
489
+ session.type = "guild-member";
490
+ session.subtype = "nickname";
491
+ break;
492
+ case "notify":
493
+ session.type = "notice";
494
+ session.subtype = (0, import_koishi2.hyphenate)(data.sub_type);
495
+ if (session.subtype === "poke") {
496
+ session.channelId ||= `private:${session.userId}`;
497
+ } else if (session.subtype === "honor") {
498
+ session.subsubtype = (0, import_koishi2.hyphenate)(data.honor_type);
499
+ }
500
+ break;
501
+ case "message_reactions_updated":
502
+ session.type = "onebot";
503
+ session.subtype = "message-reactions-updated";
504
+ break;
505
+ case "channel_created":
506
+ session.type = "onebot";
507
+ session.subtype = "channel-created";
508
+ break;
509
+ case "channel_updated":
510
+ session.type = "onebot";
511
+ session.subtype = "channel-updated";
512
+ break;
513
+ case "channel_destroyed":
514
+ session.type = "onebot";
515
+ session.subtype = "channel-destroyed";
516
+ break;
517
+ default:
518
+ return;
519
+ }
520
+ } else return;
521
+ return session;
522
+ }
523
+ __name(adaptSession, "adaptSession");
524
+
525
+ // src/bot/message.ts
526
+ var import_koishi3 = require("koishi");
527
+ var import_node_url = require("node:url");
528
+ var State = class {
529
+ constructor(type) {
530
+ this.type = type;
531
+ }
532
+ static {
533
+ __name(this, "State");
534
+ }
535
+ author = {};
536
+ children = [];
537
+ };
538
+ var PRIVATE_PFX = "private:";
539
+ var OneBotMessageEncoder = class extends import_koishi3.MessageEncoder {
540
+ static {
541
+ __name(this, "OneBotMessageEncoder");
542
+ }
543
+ stack = [new State("message")];
544
+ children = [];
545
+ async prepare() {
546
+ super.prepare();
547
+ const { event: { channel } } = this.session;
548
+ if (!channel.type) {
549
+ channel.type = channel.id.startsWith(PRIVATE_PFX) ? import_koishi3.Universal.Channel.Type.DIRECT : import_koishi3.Universal.Channel.Type.TEXT;
550
+ }
551
+ if (!this.session.isDirect) {
552
+ this.session.guildId ??= this.channelId;
553
+ }
554
+ }
555
+ async forward() {
556
+ if (!this.stack[0].children.length) return;
557
+ const session = this.bot.session();
558
+ session.content = "";
559
+ session.messageId = this.session.event.channel.type === import_koishi3.Universal.Channel.Type.DIRECT ? "" + await this.bot.internal.sendPrivateForwardMsg(this.channelId.slice(PRIVATE_PFX.length), this.stack[0].children) : "" + await this.bot.internal.sendGroupForwardMsg(this.channelId, this.stack[0].children);
560
+ session.userId = this.bot.selfId;
561
+ session.channelId = this.session.channelId;
562
+ session.guildId = this.session.guildId;
563
+ session.isDirect = this.session.isDirect;
564
+ session.app.emit(session, "send", session);
565
+ this.results.push(session.event.message);
566
+ }
567
+ async flush() {
568
+ while (true) {
569
+ const first = this.children[0];
570
+ if (first?.type !== "text") break;
571
+ first.data.text = first.data.text.trimStart();
572
+ if (first.data.text) break;
573
+ this.children.shift();
574
+ }
575
+ while (true) {
576
+ const last = this.children[this.children.length - 1];
577
+ if (last?.type !== "text") break;
578
+ last.data.text = last.data.text.trimEnd();
579
+ if (last.data.text) break;
580
+ this.children.pop();
581
+ }
582
+ const { type, author } = this.stack[0];
583
+ if (!this.children.length && !author.messageId) return;
584
+ if (type === "forward") {
585
+ if (author.messageId) {
586
+ this.stack[1].children.push({
587
+ type: "node",
588
+ data: {
589
+ id: author.messageId
590
+ }
591
+ });
592
+ } else {
593
+ this.stack[1].children.push({
594
+ type: "node",
595
+ data: {
596
+ name: author.name || this.bot.user.name,
597
+ uin: author.id || this.bot.userId,
598
+ content: this.children,
599
+ time: `${Math.floor((+author.time || Date.now()) / 1e3)}`
600
+ }
601
+ });
602
+ }
603
+ this.children = [];
604
+ return;
605
+ }
606
+ const session = this.bot.session();
607
+ session.content = "";
608
+ session.messageId = this.bot.parent ? "" + await this.bot.internal.sendGuildChannelMsg(this.session.guildId, this.channelId, this.children) : this.session.event.channel.type === import_koishi3.Universal.Channel.Type.DIRECT ? "" + await this.bot.internal.sendPrivateMsg(this.channelId.slice(PRIVATE_PFX.length), this.children) : "" + await this.bot.internal.sendGroupMsg(this.channelId, this.children);
609
+ session.userId = this.bot.selfId;
610
+ session.channelId = this.session.channelId;
611
+ session.guildId = this.session.guildId;
612
+ session.isDirect = this.session.isDirect;
613
+ session.app.emit(session, "send", session);
614
+ this.results.push(session.event.message);
615
+ this.children = [];
616
+ }
617
+ async sendFile(attrs) {
618
+ const src = attrs.src || attrs.url;
619
+ const name2 = attrs.title || (await this.bot.ctx.http.file(src)).filename;
620
+ const file = src.startsWith("file:") ? (0, import_node_url.fileURLToPath)(src) : await this.bot.internal.downloadFile(src);
621
+ if (this.session.event.channel.type === import_koishi3.Universal.Channel.Type.DIRECT) {
622
+ await this.bot.internal.uploadPrivateFile(
623
+ this.channelId.slice(PRIVATE_PFX.length),
624
+ file,
625
+ name2
626
+ );
627
+ } else {
628
+ await this.bot.internal.uploadGroupFile(
629
+ this.channelId,
630
+ file,
631
+ name2
632
+ );
633
+ }
634
+ const session = this.bot.session();
635
+ session.messageId = "";
636
+ session.content = "";
637
+ session.userId = this.bot.selfId;
638
+ session.channelId = this.session.channelId;
639
+ session.guildId = this.session.guildId;
640
+ session.isDirect = this.session.isDirect;
641
+ session.app.emit(session, "send", session);
642
+ this.results.push(session.event.message);
643
+ }
644
+ text(text) {
645
+ this.children.push({ type: "text", data: { text } });
646
+ }
647
+ async visit(element) {
648
+ let { type, attrs, children } = element;
649
+ if (type === "text") {
650
+ this.text(attrs.content);
651
+ } else if (type === "br") {
652
+ this.text("\n");
653
+ } else if (type === "p") {
654
+ const prev = this.children[this.children.length - 1];
655
+ if (prev?.type === "text") {
656
+ if (!prev.data.text.endsWith("\n")) {
657
+ prev.data.text += "\n";
658
+ }
659
+ } else {
660
+ this.text("\n");
661
+ }
662
+ await this.render(children);
663
+ this.text("\n");
664
+ } else if (type === "at") {
665
+ if (attrs.type === "all") {
666
+ this.children.push({ type: "at", data: { qq: "all" } });
667
+ } else {
668
+ this.children.push({ type: "at", data: { qq: attrs.id, name: attrs.name } });
669
+ }
670
+ } else if (type === "sharp") {
671
+ if (attrs.id) this.text(attrs.id);
672
+ } else if (type === "face") {
673
+ if (attrs.platform && attrs.platform !== this.bot.platform) {
674
+ await this.render(children);
675
+ } else {
676
+ this.children.push({ type: "face", data: { id: attrs.id } });
677
+ }
678
+ } else if (type === "a") {
679
+ await this.render(children);
680
+ if (attrs.href) this.text(`(${attrs.href})`);
681
+ } else if (["video", "audio", "image", "img"].includes(type)) {
682
+ if (type === "video" || type === "audio") await this.flush();
683
+ if (type === "audio") type = "record";
684
+ if (type === "img") type = "image";
685
+ attrs = { ...attrs };
686
+ attrs.file = attrs.src || attrs.url;
687
+ delete attrs.src;
688
+ delete attrs.url;
689
+ if (attrs.cache) {
690
+ attrs.cache = 1;
691
+ } else {
692
+ attrs.cache = 0;
693
+ }
694
+ const cap = /^data:([\w/.+-]+);base64,/.exec(attrs.file);
695
+ if (cap) attrs.file = "base64://" + attrs.file.slice(cap[0].length);
696
+ this.children.push({ type, data: attrs });
697
+ } else if (type === "file") {
698
+ await this.flush();
699
+ await this.sendFile(attrs);
700
+ } else if (type === "onebot:music") {
701
+ await this.flush();
702
+ this.children.push({ type: "music", data: attrs });
703
+ } else if (type === "onebot:tts") {
704
+ await this.flush();
705
+ this.children.push({ type: "tts", data: attrs });
706
+ } else if (type === "onebot:poke") {
707
+ await this.flush();
708
+ this.children.push({ type: "poke", data: attrs });
709
+ } else if (type === "onebot:gift") {
710
+ await this.flush();
711
+ this.children.push({ type: "gift", data: attrs });
712
+ } else if (type === "onebot:share") {
713
+ await this.flush();
714
+ this.children.push({ type: "share", data: attrs });
715
+ } else if (type === "onebot:json") {
716
+ await this.flush();
717
+ this.children.push({ type: "json", data: attrs });
718
+ } else if (type === "onebot:xml") {
719
+ await this.flush();
720
+ this.children.push({ type: "xml", data: attrs });
721
+ } else if (type === "onebot:cardimage") {
722
+ await this.flush();
723
+ this.children.push({ type: "cardimage", data: attrs });
724
+ } else if (type === "author") {
725
+ Object.assign(this.stack[0].author, attrs);
726
+ } else if (type === "figure" && !this.bot.parent) {
727
+ await this.flush();
728
+ this.stack.unshift(new State("forward"));
729
+ await this.render(children);
730
+ await this.flush();
731
+ this.stack.shift();
732
+ await this.forward();
733
+ } else if (type === "figure") {
734
+ await this.render(children);
735
+ await this.flush();
736
+ } else if (type === "quote") {
737
+ await this.flush();
738
+ this.children.push({ type: "reply", data: attrs });
739
+ } else if (type === "message") {
740
+ await this.flush();
741
+ if ("forward" in attrs && !this.bot.parent) {
742
+ this.stack.unshift(new State("forward"));
743
+ await this.render(children);
744
+ await this.flush();
745
+ this.stack.shift();
746
+ await this.forward();
747
+ } else if ("id" in attrs) {
748
+ this.stack[0].author.messageId = attrs.id.toString();
749
+ } else {
750
+ Object.assign(this.stack[0].author, (0, import_koishi3.pick)(attrs, ["userId", "username", "nickname", "time"]));
751
+ await this.render(children);
752
+ await this.flush();
753
+ }
754
+ } else {
755
+ await this.render(children);
756
+ }
757
+ }
758
+ };
759
+
760
+ // src/bot/base.ts
761
+ var BaseBot2 = class extends import_koishi4.Bot {
762
+ static {
763
+ __name(this, "BaseBot");
764
+ }
765
+ static MessageEncoder = OneBotMessageEncoder;
766
+ static inject = ["http"];
767
+ parent;
768
+ internal;
769
+ async createDirectChannel(userId) {
770
+ return { id: `${PRIVATE_PFX}${userId}`, type: import_koishi4.Universal.Channel.Type.DIRECT };
771
+ }
772
+ async getMessage(channelId, messageId) {
773
+ const data = await this.internal.getMsg(messageId);
774
+ return await adaptMessage(this, data);
775
+ }
776
+ async deleteMessage(channelId, messageId) {
777
+ await this.internal.deleteMsg(messageId);
778
+ }
779
+ async getLogin() {
780
+ const data = await this.internal.getLoginInfo();
781
+ this.user = decodeUser(data);
782
+ return this.toJSON();
783
+ }
784
+ async getUser(userId) {
785
+ const data = await this.internal.getStrangerInfo(userId);
786
+ return decodeUser(data);
787
+ }
788
+ async getFriendList() {
789
+ const data = await this.internal.getFriendList();
790
+ return { data: data.map(decodeUser) };
791
+ }
792
+ async handleFriendRequest(messageId, approve, comment) {
793
+ await this.internal.setFriendAddRequest(messageId, approve, comment);
794
+ }
795
+ async handleGuildRequest(messageId, approve, comment) {
796
+ await this.internal.setGroupAddRequest(messageId, "invite", approve, comment);
797
+ }
798
+ async handleGuildMemberRequest(messageId, approve, comment) {
799
+ await this.internal.setGroupAddRequest(messageId, "add", approve, comment);
800
+ }
801
+ async deleteFriend(userId) {
802
+ await this.internal.deleteFriend(userId);
803
+ }
804
+ async getMessageList(channelId, before, direction = "before") {
805
+ if (direction !== "before") throw new Error("Unsupported direction.");
806
+ let list;
807
+ if (before) {
808
+ const msg = await this.internal.getMsg(before);
809
+ if (msg?.message_seq) {
810
+ list = (await this.internal.getGroupMsgHistory(Number(channelId), msg.message_seq)).messages;
811
+ }
812
+ } else {
813
+ list = (await this.internal.getGroupMsgHistory(Number(channelId))).messages;
814
+ }
815
+ return { data: await Promise.all(list.map((item) => adaptMessage(this, item))) };
816
+ }
817
+ };
818
+ ((BaseBot3) => {
819
+ BaseBot3.AdvancedConfig = import_koishi4.Schema.object({
820
+ splitMixedContent: import_koishi4.Schema.boolean().description("是否自动在混合内容间插入空格。").default(true)
821
+ }).description("高级设置");
822
+ })(BaseBot2 || (BaseBot2 = {}));
823
+
824
+ // src/bot/qqguild.ts
825
+ var QQGuildBot = class extends BaseBot2 {
826
+ static {
827
+ __name(this, "QQGuildBot");
828
+ }
829
+ hidden = true;
830
+ constructor(ctx, config) {
831
+ super(ctx, config, "qqguild");
832
+ this.platform = "qqguild";
833
+ this.selfId = config.profile.tiny_id;
834
+ this.parent = config.parent;
835
+ this.internal = config.parent.internal;
836
+ this.user.name = config.profile.nickname;
837
+ this.user.avatar = config.profile.avatar_url;
838
+ this.parent.guildBot = this;
839
+ }
840
+ get status() {
841
+ return this.parent.status;
842
+ }
843
+ set status(status) {
844
+ this.parent.status = status;
845
+ }
846
+ async start() {
847
+ await this.context.parallel("bot-connect", this);
848
+ }
849
+ async stop() {
850
+ if (!this.parent) return;
851
+ this.parent = void 0;
852
+ await this.context.parallel("bot-disconnect", this);
853
+ }
854
+ async getChannel(channelId, guildId) {
855
+ const { data } = await this.getChannelList(guildId);
856
+ return data.find((channel) => channel.id === channelId);
857
+ }
858
+ async getChannelList(guildId) {
859
+ const data = await this.internal.getGuildChannelList(guildId, false);
860
+ return { data: (data || []).map(adaptChannel) };
861
+ }
862
+ async getGuild(guildId) {
863
+ const data = await this.internal.getGuildMetaByGuest(guildId);
864
+ return adaptGuild(data);
865
+ }
866
+ async getGuildList() {
867
+ const data = await this.internal.getGuildList();
868
+ return { data: data.map(adaptGuild) };
869
+ }
870
+ async getGuildMember(guildId, userId) {
871
+ const profile = await this.internal.getGuildMemberProfile(guildId, userId);
872
+ return adaptQQGuildMemberProfile(profile);
873
+ }
874
+ async getGuildMemberList(guildId) {
875
+ let nextToken;
876
+ let list = [];
877
+ while (true) {
878
+ const data = await this.internal.getGuildMemberList(guildId, nextToken);
879
+ if (!data.members?.length) break;
880
+ list = list.concat(data.members.map(adaptQQGuildMemberInfo));
881
+ if (data.finished) break;
882
+ nextToken = data.next_token;
883
+ }
884
+ return { data: list };
885
+ }
886
+ };
887
+
888
+ // src/ws.ts
889
+ var import_koishi5 = require("koishi");
890
+ var WsClient = class extends import_koishi5.Adapter.WsClient {
891
+ static {
892
+ __name(this, "WsClient");
893
+ }
894
+ accept(socket) {
895
+ accept(socket, this.bot);
896
+ }
897
+ prepare() {
898
+ const { token, endpoint } = this.bot.config;
899
+ const http = this.ctx.http.extend(this.bot.config);
900
+ if (token) http.config.headers.Authorization = `Bearer ${token}`;
901
+ return http.ws(endpoint);
902
+ }
903
+ };
904
+ ((WsClient2) => {
905
+ WsClient2.Options = import_koishi5.Schema.intersect([
906
+ import_koishi5.Schema.object({
907
+ protocol: import_koishi5.Schema.const("ws").required(process.env.KOISHI_ENV !== "browser"),
908
+ endpoint: import_koishi5.Schema.string().description("OneBot 实现的 WebSocket 服务器地址。").required(),
909
+ responseTimeout: import_koishi5.Schema.natural().role("time").default(import_koishi5.Time.minute).description("等待响应的时间 (单位为毫秒)。")
910
+ }).description("连接设置"),
911
+ import_koishi5.HTTP.createConfig(true),
912
+ import_koishi5.Adapter.WsClientConfig
913
+ ]);
914
+ })(WsClient || (WsClient = {}));
915
+ var kSocket = Symbol("socket");
916
+ var WsServer = class extends import_koishi5.Adapter {
917
+ static {
918
+ __name(this, "WsServer");
919
+ }
920
+ static inject = ["server"];
921
+ logger;
922
+ wsServer;
923
+ constructor(ctx, bot) {
924
+ super(ctx);
925
+ this.logger = ctx.logger("onebot");
926
+ const { path = "/onebot" } = bot.config;
927
+ this.wsServer = ctx.server.ws(path, (socket, { headers }) => {
928
+ this.logger.debug("connected with", headers);
929
+ if (headers["x-client-role"] !== "Universal") {
930
+ return socket.close(1008, "invalid x-client-role");
931
+ }
932
+ const selfId = headers["x-self-id"]?.toString();
933
+ const bot2 = this.bots.find((bot3) => bot3.selfId === selfId);
934
+ if (!bot2) return socket.close(1008, "invalid x-self-id");
935
+ this.logger.info(`Bot ${selfId} 已连接 (ws-reverse)`);
936
+ bot2[kSocket] = socket;
937
+ accept(socket, bot2);
938
+ });
939
+ ctx.on("dispose", () => {
940
+ this.logger.debug("ws server closing");
941
+ this.wsServer.close();
942
+ });
943
+ }
944
+ async disconnect(bot) {
945
+ bot[kSocket]?.close();
946
+ bot[kSocket] = null;
947
+ }
948
+ };
949
+ ((WsServer2) => {
950
+ WsServer2.Options = import_koishi5.Schema.object({
951
+ protocol: import_koishi5.Schema.const("ws-reverse").required(process.env.KOISHI_ENV === "browser"),
952
+ path: import_koishi5.Schema.string().description("服务器监听的路径。").default("/onebot"),
953
+ responseTimeout: import_koishi5.Schema.natural().role("time").default(import_koishi5.Time.minute).description("等待响应的时间 (单位为毫秒)。")
954
+ }).description("连接设置");
955
+ })(WsServer || (WsServer = {}));
956
+ var counter = 0;
957
+ var listeners = {};
958
+ function accept(socket, bot) {
959
+ socket.addEventListener("message", ({ data }) => {
960
+ let parsed;
961
+ const strData = data.toString();
962
+ try {
963
+ parsed = JSON.parse(strData);
964
+ } catch (error) {
965
+ return bot.logger.warn("cannot parse message", strData);
966
+ }
967
+ if ("post_type" in parsed) {
968
+ bot.logger.debug("[receive] %o", parsed);
969
+ dispatchSession(bot, parsed);
970
+ } else if (parsed.echo in listeners) {
971
+ listeners[parsed.echo](parsed);
972
+ delete listeners[parsed.echo];
973
+ }
974
+ });
975
+ socket.addEventListener("close", () => {
976
+ delete bot.internal._request;
977
+ bot.offline();
978
+ });
979
+ bot.internal._request = (action, params) => {
980
+ const data = { action, params, echo: ++counter };
981
+ data.echo = ++counter;
982
+ return new Promise((resolve, reject) => {
983
+ listeners[data.echo] = resolve;
984
+ setTimeout(() => {
985
+ delete listeners[data.echo];
986
+ reject(new TimeoutError(params, action));
987
+ }, bot.config.responseTimeout);
988
+ socket.send(JSON.stringify(data));
989
+ });
990
+ };
991
+ bot.initialize();
992
+ }
993
+ __name(accept, "accept");
994
+
995
+ // src/heartbeat.ts
996
+ var import_koishi6 = require("koishi");
997
+ var HeartbeatMonitor = class {
998
+ constructor(ctx, bot, interval) {
999
+ this.ctx = ctx;
1000
+ this.bot = bot;
1001
+ this.interval = interval;
1002
+ this.logger = ctx.logger("onebot-heartbeat");
1003
+ }
1004
+ static {
1005
+ __name(this, "HeartbeatMonitor");
1006
+ }
1007
+ timer = null;
1008
+ logger;
1009
+ lastOnlineState = null;
1010
+ start() {
1011
+ if (this.timer) return;
1012
+ this.logger.debug(`Bot ${this.bot.selfId} 心跳检测已启动,间隔 ${this.interval}ms`);
1013
+ this.check();
1014
+ this.timer = setInterval(() => this.check(), this.interval);
1015
+ this.ctx.on("dispose", () => this.stop());
1016
+ }
1017
+ stop() {
1018
+ if (this.timer) {
1019
+ clearInterval(this.timer);
1020
+ this.timer = null;
1021
+ this.logger.debug(`Bot ${this.bot.selfId} 心跳检测已停止`);
1022
+ }
1023
+ }
1024
+ async check() {
1025
+ try {
1026
+ const status = await this.bot.internal.getStatus();
1027
+ const isOnline = status?.online ?? false;
1028
+ if (this.lastOnlineState === isOnline) return;
1029
+ this.logger.info(`Bot ${this.bot.selfId} 状态变化: ${this.lastOnlineState} -> ${isOnline}`);
1030
+ this.lastOnlineState = isOnline;
1031
+ if (isOnline) {
1032
+ if (this.bot.status !== import_koishi6.Universal.Status.ONLINE) {
1033
+ this.bot.online();
1034
+ this.ctx.emit("onebot-multi/bot-online", this.bot);
1035
+ }
1036
+ } else {
1037
+ if (this.bot.status === import_koishi6.Universal.Status.ONLINE) {
1038
+ this.bot.offline();
1039
+ this.ctx.emit("onebot-multi/bot-offline", this.bot);
1040
+ }
1041
+ }
1042
+ } catch (error) {
1043
+ this.logger.warn(`Bot ${this.bot.selfId} 心跳检测失败:`, error);
1044
+ if (this.lastOnlineState !== false) {
1045
+ this.lastOnlineState = false;
1046
+ if (this.bot.status === import_koishi6.Universal.Status.ONLINE) {
1047
+ this.bot.offline(error);
1048
+ this.ctx.emit("onebot-multi/bot-offline", this.bot);
1049
+ }
1050
+ }
1051
+ }
1052
+ }
1053
+ };
1054
+
1055
+ // src/bot/cqcode.ts
1056
+ var import_koishi7 = require("koishi");
1057
+ function CQCode(type, attrs) {
1058
+ if (type === "text") return attrs.content;
1059
+ let output = "[CQ:" + type;
1060
+ for (const key in attrs) {
1061
+ if (attrs[key]) output += `,${key}=${import_koishi7.h.escape(attrs[key], true)}`;
1062
+ }
1063
+ return output + "]";
1064
+ }
1065
+ __name(CQCode, "CQCode");
1066
+ ((CQCode2) => {
1067
+ function escape(source, inline = false) {
1068
+ const result = String(source).replace(/&/g, "&amp;").replace(/\[/g, "&#91;").replace(/\]/g, "&#93;");
1069
+ return inline ? result.replace(/,/g, "&#44;").replace(/(\ud83c[\udf00-\udfff])|(\ud83d[\udc00-\ude4f\ude80-\udeff])|[\u2600-\u2B55]/g, " ") : result;
1070
+ }
1071
+ CQCode2.escape = escape;
1072
+ __name(escape, "escape");
1073
+ function unescape(source) {
1074
+ return String(source).replace(/&#91;/g, "[").replace(/&#93;/g, "]").replace(/&#44;/g, ",").replace(/&amp;/g, "&");
1075
+ }
1076
+ CQCode2.unescape = unescape;
1077
+ __name(unescape, "unescape");
1078
+ const pattern = /\[CQ:(\w+)((,\w+=[^,\]]*)*)\]/;
1079
+ function from(source) {
1080
+ const capture = pattern.exec(source);
1081
+ if (!capture) return null;
1082
+ const [, type, attrs] = capture;
1083
+ const data = {};
1084
+ attrs && attrs.slice(1).split(",").forEach((str) => {
1085
+ const index = str.indexOf("=");
1086
+ data[str.slice(0, index)] = unescape(str.slice(index + 1));
1087
+ });
1088
+ return { type, data, capture };
1089
+ }
1090
+ CQCode2.from = from;
1091
+ __name(from, "from");
1092
+ function parse(source) {
1093
+ if (typeof source !== "string") {
1094
+ return source.map(({ type, data }) => {
1095
+ if (type === "text") {
1096
+ return (0, import_koishi7.h)("text", { content: data.text });
1097
+ } else {
1098
+ return (0, import_koishi7.h)(type, data);
1099
+ }
1100
+ });
1101
+ }
1102
+ const elements = [];
1103
+ let result;
1104
+ while (result = from(source)) {
1105
+ const { type, data, capture } = result;
1106
+ if (capture.index) {
1107
+ elements.push((0, import_koishi7.h)("text", { content: unescape(source.slice(0, capture.index)) }));
1108
+ }
1109
+ elements.push((0, import_koishi7.h)(type, data));
1110
+ source = source.slice(capture.index + capture[0].length);
1111
+ }
1112
+ if (source) elements.push((0, import_koishi7.h)("text", { content: unescape(source) }));
1113
+ return elements;
1114
+ }
1115
+ CQCode2.parse = parse;
1116
+ __name(parse, "parse");
1117
+ })(CQCode || (CQCode = {}));
1118
+
1119
+ // src/bot/index.ts
1120
+ var OneBotBot = class extends BaseBot2 {
1121
+ static {
1122
+ __name(this, "OneBotBot");
1123
+ }
1124
+ guildBot;
1125
+ heartbeat;
1126
+ constructor(ctx, config) {
1127
+ super(ctx, config, "onebot");
1128
+ this.selfId = config.selfId;
1129
+ this.internal = new Internal(this);
1130
+ this.user.avatar = `http://q.qlogo.cn/headimg_dl?dst_uin=${config.selfId}&spec=640`;
1131
+ if (config.protocol === "ws") {
1132
+ ctx.plugin(WsClient, this);
1133
+ } else {
1134
+ ctx.plugin(WsServer, this);
1135
+ }
1136
+ if (config.heartbeatInterval > 0) {
1137
+ this.heartbeat = new HeartbeatMonitor(ctx, this, config.heartbeatInterval);
1138
+ }
1139
+ }
1140
+ async stop() {
1141
+ if (this.guildBot) {
1142
+ delete this.ctx.bots[this.guildBot.sid];
1143
+ }
1144
+ await super.stop();
1145
+ }
1146
+ async initialize() {
1147
+ await Promise.all([
1148
+ this.getLogin(),
1149
+ this.setupGuildService().catch(import_koishi8.noop)
1150
+ ]).then(() => {
1151
+ this.online();
1152
+ this.ctx.emit("onebot-multi/bot-online", this);
1153
+ this.heartbeat?.start();
1154
+ }, (error) => this.offline(error));
1155
+ }
1156
+ async setupGuildService() {
1157
+ const profile = await this.internal.getGuildServiceProfile();
1158
+ if (!profile?.tiny_id || profile.tiny_id === "0") return;
1159
+ this.ctx.plugin(QQGuildBot, {
1160
+ profile,
1161
+ parent: this,
1162
+ advanced: this.config.advanced
1163
+ });
1164
+ }
1165
+ async getChannel(channelId) {
1166
+ const data = await this.internal.getGroupInfo(channelId);
1167
+ return adaptChannel(data);
1168
+ }
1169
+ async getGuild(guildId) {
1170
+ const data = await this.internal.getGroupInfo(guildId);
1171
+ return adaptGuild(data);
1172
+ }
1173
+ async getGuildList() {
1174
+ const data = await this.internal.getGroupList();
1175
+ return { data: data.map(adaptGuild) };
1176
+ }
1177
+ async getChannelList(guildId) {
1178
+ return { data: [await this.getChannel(guildId)] };
1179
+ }
1180
+ async getGuildMember(guildId, userId) {
1181
+ const data = await this.internal.getGroupMemberInfo(guildId, userId);
1182
+ return decodeGuildMember(data);
1183
+ }
1184
+ async getGuildMemberList(guildId) {
1185
+ const data = await this.internal.getGroupMemberList(guildId);
1186
+ return { data: data.map(decodeGuildMember) };
1187
+ }
1188
+ async kickGuildMember(guildId, userId, permanent) {
1189
+ return this.internal.setGroupKick(guildId, userId, permanent);
1190
+ }
1191
+ async muteGuildMember(guildId, userId, duration) {
1192
+ return this.internal.setGroupBan(guildId, userId, Math.round(duration / 1e3));
1193
+ }
1194
+ async muteChannel(channelId, guildId, enable) {
1195
+ return this.internal.setGroupWholeBan(channelId, enable);
1196
+ }
1197
+ async checkPermission(name2, session) {
1198
+ if (name2 === "onebot.group.admin") {
1199
+ return session.author?.roles?.[0] === "admin";
1200
+ } else if (name2 === "onebot.group.owner") {
1201
+ return session.author?.roles?.[0] === "owner";
1202
+ }
1203
+ return super.checkPermission(name2, session);
1204
+ }
1205
+ };
1206
+ ((OneBotBot2) => {
1207
+ OneBotBot2.BaseConfig = import_koishi8.Schema.object({
1208
+ selfId: import_koishi8.Schema.string().description("机器人的账号。").required(),
1209
+ token: import_koishi8.Schema.string().role("secret").description("发送信息时用于验证的字段,应与 OneBot 配置文件中的 `access_token` 保持一致。"),
1210
+ protocol: process.env.KOISHI_ENV === "browser" ? import_koishi8.Schema.const("ws").default("ws") : import_koishi8.Schema.union(["ws", "ws-reverse"]).description("选择要使用的协议。").default("ws-reverse"),
1211
+ heartbeatInterval: import_koishi8.Schema.natural().role("ms").description("心跳检测间隔(毫秒),设为 0 禁用。").default(3e4)
1212
+ });
1213
+ OneBotBot2.Config = import_koishi8.Schema.intersect([
1214
+ OneBotBot2.BaseConfig,
1215
+ import_koishi8.Schema.union([
1216
+ WsClient.Options,
1217
+ WsServer.Options
1218
+ ]),
1219
+ import_koishi8.Schema.object({
1220
+ advanced: BaseBot2.AdvancedConfig
1221
+ })
1222
+ ]);
1223
+ })(OneBotBot || (OneBotBot = {}));
1224
+
1225
+ // src/status.ts
1226
+ var import_koishi9 = require("koishi");
1227
+ var StatusManager = class {
1228
+ constructor(ctx) {
1229
+ this.ctx = ctx;
1230
+ ctx.on("onebot-multi/bot-online", async (bot) => {
1231
+ await this.fetchBotInfo(bot);
1232
+ });
1233
+ ctx.on("onebot-multi/bot-offline", (bot) => {
1234
+ const cached = this.cache.get(bot.selfId);
1235
+ if (cached) {
1236
+ cached.lastHeartbeat = Date.now();
1237
+ }
1238
+ });
1239
+ }
1240
+ static {
1241
+ __name(this, "StatusManager");
1242
+ }
1243
+ // 缓存额外信息(统计数据等)
1244
+ cache = /* @__PURE__ */ new Map();
1245
+ /**
1246
+ * 获取 Bot 的额外信息(统计、群数等)
1247
+ */
1248
+ async fetchBotInfo(bot) {
1249
+ const cached = this.cache.get(bot.selfId) || {};
1250
+ try {
1251
+ const status = await bot.internal.getStatus();
1252
+ if (status?.stat) {
1253
+ cached.messageReceived = status.stat.message_received;
1254
+ cached.messageSent = status.stat.message_sent;
1255
+ cached.lastMessageTime = status.stat.last_message_time;
1256
+ cached.startupTime = status.stat.startup_time;
1257
+ }
1258
+ try {
1259
+ const groups = await bot.internal.getGroupList();
1260
+ cached.groupCount = groups?.length || 0;
1261
+ } catch {
1262
+ }
1263
+ try {
1264
+ const friends = await bot.internal.getFriendList();
1265
+ cached.friendCount = friends?.length || 0;
1266
+ } catch {
1267
+ }
1268
+ cached.lastHeartbeat = Date.now();
1269
+ this.cache.set(bot.selfId, cached);
1270
+ } catch (error) {
1271
+ this.ctx.logger.warn(`获取 Bot ${bot.selfId} 信息失败:`, error);
1272
+ }
1273
+ }
1274
+ /**
1275
+ * 从 ctx.bots 获取实时状态
1276
+ */
1277
+ getStatus() {
1278
+ const bots = [];
1279
+ for (const bot of this.ctx.bots) {
1280
+ if (bot.platform !== "onebot") continue;
1281
+ const onebotBot = bot;
1282
+ const config = onebotBot.config;
1283
+ const cached = this.cache.get(bot.selfId) || {};
1284
+ let status = "offline";
1285
+ switch (bot.status) {
1286
+ case import_koishi9.Universal.Status.ONLINE:
1287
+ status = "online";
1288
+ break;
1289
+ case import_koishi9.Universal.Status.CONNECT:
1290
+ status = "connecting";
1291
+ break;
1292
+ default:
1293
+ status = "offline";
1294
+ }
1295
+ bots.push({
1296
+ selfId: bot.selfId,
1297
+ nickname: bot.user?.name,
1298
+ protocol: config.protocol || "ws-reverse",
1299
+ status,
1300
+ endpoint: config.endpoint,
1301
+ path: config.path,
1302
+ messageReceived: cached.messageReceived,
1303
+ messageSent: cached.messageSent,
1304
+ lastMessageTime: cached.lastMessageTime,
1305
+ startupTime: cached.startupTime,
1306
+ groupCount: cached.groupCount,
1307
+ friendCount: cached.friendCount,
1308
+ lastHeartbeat: cached.lastHeartbeat
1309
+ });
1310
+ }
1311
+ return {
1312
+ bots,
1313
+ updatedAt: Date.now()
1314
+ };
1315
+ }
1316
+ /**
1317
+ * 刷新所有 Bot 的信息
1318
+ */
1319
+ async refreshAll() {
1320
+ for (const bot of this.ctx.bots) {
1321
+ if (bot.platform !== "onebot") continue;
1322
+ if (bot.status === import_koishi9.Universal.Status.ONLINE) {
1323
+ await this.fetchBotInfo(bot);
1324
+ }
1325
+ }
1326
+ return this.getStatus();
1327
+ }
1328
+ getBotStatus(selfId) {
1329
+ const status = this.getStatus();
1330
+ return status.bots.find((b) => b.selfId === selfId);
1331
+ }
1332
+ };
1333
+
1334
+ // src/panel.ts
1335
+ var import_koa = __toESM(require("koa"));
1336
+ var import_router = __toESM(require("@koa/router"));
1337
+ var import_koishi10 = require("koishi");
1338
+ var import_crypto = require("crypto");
1339
+ var SERVER_SECRET = (0, import_crypto.randomBytes)(32).toString("hex");
1340
+ var TOKEN_EXPIRY = 24 * 60 * 60 * 1e3;
1341
+ function generateSignedToken() {
1342
+ const payload = {
1343
+ iat: Date.now(),
1344
+ exp: Date.now() + TOKEN_EXPIRY
1345
+ };
1346
+ const payloadStr = Buffer.from(JSON.stringify(payload)).toString("base64url");
1347
+ const signature = (0, import_crypto.createHash)("sha256").update(payloadStr + SERVER_SECRET).digest("base64url");
1348
+ return `${payloadStr}.${signature}`;
1349
+ }
1350
+ __name(generateSignedToken, "generateSignedToken");
1351
+ function verifySignedToken(token) {
1352
+ try {
1353
+ const [payloadStr, signature] = token.split(".");
1354
+ if (!payloadStr || !signature) return false;
1355
+ const expectedSig = (0, import_crypto.createHash)("sha256").update(payloadStr + SERVER_SECRET).digest("base64url");
1356
+ if (signature !== expectedSig) return false;
1357
+ const payload = JSON.parse(Buffer.from(payloadStr, "base64url").toString());
1358
+ if (Date.now() > payload.exp) return false;
1359
+ return true;
1360
+ } catch {
1361
+ return false;
1362
+ }
1363
+ }
1364
+ __name(verifySignedToken, "verifySignedToken");
1365
+ var PanelConfig = import_koishi10.Schema.object({
1366
+ enabled: import_koishi10.Schema.boolean().default(false).description("是否启用状态展示面板。"),
1367
+ port: import_koishi10.Schema.natural().default(8212).description("面板端口。"),
1368
+ basePath: import_koishi10.Schema.string().default("/status").description("面板路径。")
1369
+ }).description("展示面板");
1370
+ var StatusPanel = class {
1371
+ constructor(ctx, config, statusManager, configManager) {
1372
+ this.ctx = ctx;
1373
+ this.config = config;
1374
+ this.statusManager = statusManager;
1375
+ this.configManager = configManager;
1376
+ if (!config.enabled) return;
1377
+ this.app = new import_koa.default();
1378
+ this.setupRoutes();
1379
+ this.start();
1380
+ ctx.on("dispose", () => this.stop());
1381
+ }
1382
+ static {
1383
+ __name(this, "StatusPanel");
1384
+ }
1385
+ app;
1386
+ server = null;
1387
+ async parseJsonBody(ctx) {
1388
+ return new Promise((resolve, reject) => {
1389
+ let body = "";
1390
+ ctx.req.on("data", (chunk) => {
1391
+ body += chunk.toString();
1392
+ });
1393
+ ctx.req.on("end", () => {
1394
+ try {
1395
+ resolve(body ? JSON.parse(body) : {});
1396
+ } catch (e) {
1397
+ reject(new Error("Invalid JSON"));
1398
+ }
1399
+ });
1400
+ ctx.req.on("error", reject);
1401
+ });
1402
+ }
1403
+ setupRoutes() {
1404
+ const router = new import_router.default();
1405
+ const basePath = this.config.basePath || "/status";
1406
+ router.get(`${basePath}/api/status`, (ctx) => {
1407
+ const status = this.statusManager.getStatus();
1408
+ ctx.body = {
1409
+ bots: status.bots.map((bot) => ({
1410
+ selfId: bot.selfId,
1411
+ nickname: bot.nickname,
1412
+ status: bot.status,
1413
+ groupCount: bot.groupCount,
1414
+ friendCount: bot.friendCount,
1415
+ messageReceived: bot.messageReceived,
1416
+ messageSent: bot.messageSent
1417
+ // 不返回: protocol, endpoint, path, token 等敏感信息
1418
+ })),
1419
+ updatedAt: status.updatedAt
1420
+ };
1421
+ });
1422
+ router.get(basePath, (ctx) => {
1423
+ ctx.type = "html";
1424
+ ctx.body = this.renderStatusPage();
1425
+ });
1426
+ const adminPath = `${basePath}/admin`;
1427
+ const authMiddleware = /* @__PURE__ */ __name(async (ctx, next) => {
1428
+ const cookies = ctx.headers.cookie || "";
1429
+ const tokenMatch = cookies.match(/ob_admin_token=([^;]+)/);
1430
+ const token = tokenMatch ? tokenMatch[1] : null;
1431
+ if (!token || !verifySignedToken(token)) {
1432
+ ctx.status = 401;
1433
+ ctx.body = { error: "未授权访问" };
1434
+ return;
1435
+ }
1436
+ await next();
1437
+ }, "authMiddleware");
1438
+ router.get(`${adminPath}/api/check-init`, async (ctx) => {
1439
+ const hasPassword = await this.configManager.hasAdminPassword();
1440
+ ctx.body = { needsInit: !hasPassword };
1441
+ });
1442
+ router.get(`${adminPath}/api/check-auth`, async (ctx) => {
1443
+ const cookies = ctx.headers.cookie || "";
1444
+ const tokenMatch = cookies.match(/ob_admin_token=([^;]+)/);
1445
+ const token = tokenMatch ? tokenMatch[1] : null;
1446
+ ctx.body = { authenticated: token && verifySignedToken(token) };
1447
+ });
1448
+ router.post(`${adminPath}/api/init-password`, async (ctx) => {
1449
+ const hasPassword = await this.configManager.hasAdminPassword();
1450
+ if (hasPassword) {
1451
+ ctx.status = 400;
1452
+ ctx.body = { error: "密码已设置" };
1453
+ return;
1454
+ }
1455
+ const data = await this.parseJsonBody(ctx);
1456
+ if (!data.password || data.password.length < 4) {
1457
+ ctx.status = 400;
1458
+ ctx.body = { error: "密码至少需要4位" };
1459
+ return;
1460
+ }
1461
+ await this.configManager.setAdminPassword(data.password);
1462
+ this.ctx.logger.info("管理密码已设置");
1463
+ const token = generateSignedToken();
1464
+ ctx.set("Set-Cookie", `ob_admin_token=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${TOKEN_EXPIRY / 1e3}`);
1465
+ ctx.body = { success: true };
1466
+ });
1467
+ router.post(`${adminPath}/api/login`, async (ctx) => {
1468
+ const data = await this.parseJsonBody(ctx);
1469
+ const valid = await this.configManager.verifyAdminPassword(data.password || "");
1470
+ this.ctx.logger.debug(`登录尝试: ${valid ? "成功" : "失败"}`);
1471
+ if (valid) {
1472
+ const token = generateSignedToken();
1473
+ this.ctx.logger.debug(`生成签名 token`);
1474
+ ctx.set("Set-Cookie", `ob_admin_token=${token}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${TOKEN_EXPIRY / 1e3}`);
1475
+ ctx.body = { success: true };
1476
+ } else {
1477
+ ctx.status = 401;
1478
+ ctx.body = { error: "密码错误" };
1479
+ }
1480
+ });
1481
+ router.post(`${adminPath}/api/logout`, async (ctx) => {
1482
+ ctx.set("Set-Cookie", `ob_admin_token=; Path=/; HttpOnly; Max-Age=0`);
1483
+ ctx.body = { success: true };
1484
+ });
1485
+ router.get(adminPath, async (ctx) => {
1486
+ const hasPassword = await this.configManager.hasAdminPassword();
1487
+ ctx.type = "html";
1488
+ ctx.set("Cache-Control", "no-store, no-cache, must-revalidate");
1489
+ ctx.set("Pragma", "no-cache");
1490
+ if (!hasPassword) {
1491
+ ctx.body = this.renderSetupPage();
1492
+ } else {
1493
+ const cookies = ctx.headers.cookie || "";
1494
+ const tokenMatch = cookies.match(/ob_admin_token=([^;]+)/);
1495
+ const token = tokenMatch ? tokenMatch[1] : null;
1496
+ const isValid = token && verifySignedToken(token);
1497
+ this.ctx.logger.debug(`访问管理面板: token=${token ? "存在" : "无"}, 有效=${isValid}`);
1498
+ if (isValid) {
1499
+ ctx.body = this.renderAdminPage();
1500
+ } else {
1501
+ ctx.body = this.renderLoginPage();
1502
+ }
1503
+ }
1504
+ });
1505
+ router.get(`${adminPath}/api/list`, authMiddleware, async (ctx) => {
1506
+ const configs = await this.configManager.getAllConfigs();
1507
+ const status = this.statusManager.getStatus();
1508
+ const bots = configs.map((config) => {
1509
+ const botStatus = status.bots.find((b) => b.selfId === config.selfId);
1510
+ return {
1511
+ selfId: config.selfId,
1512
+ nickname: botStatus?.nickname,
1513
+ protocol: config.protocol,
1514
+ status: botStatus?.status || "offline",
1515
+ endpoint: config.endpoint,
1516
+ path: config.path,
1517
+ enabled: config.enabled,
1518
+ messageReceived: botStatus?.messageReceived,
1519
+ messageSent: botStatus?.messageSent,
1520
+ lastMessageTime: botStatus?.lastMessageTime,
1521
+ startupTime: botStatus?.startupTime,
1522
+ groupCount: botStatus?.groupCount,
1523
+ friendCount: botStatus?.friendCount
1524
+ };
1525
+ });
1526
+ ctx.body = { bots, updatedAt: Date.now() };
1527
+ });
1528
+ router.post(`${adminPath}/api/create`, authMiddleware, async (ctx) => {
1529
+ const data = await this.parseJsonBody(ctx);
1530
+ if (!data.selfId) {
1531
+ ctx.status = 400;
1532
+ ctx.body = { error: "selfId 不能为空" };
1533
+ return;
1534
+ }
1535
+ const existing = await this.configManager.getConfig(data.selfId);
1536
+ if (existing) {
1537
+ ctx.status = 400;
1538
+ ctx.body = { error: `Bot ${data.selfId} 已存在` };
1539
+ return;
1540
+ }
1541
+ if (data.protocol === "ws" && !data.endpoint) {
1542
+ ctx.status = 400;
1543
+ ctx.body = { error: "WS 协议需要配置 endpoint" };
1544
+ return;
1545
+ }
1546
+ const record = await this.configManager.createConfig({
1547
+ selfId: data.selfId,
1548
+ token: data.token,
1549
+ protocol: data.protocol || "ws-reverse",
1550
+ endpoint: data.endpoint,
1551
+ path: data.path || "/onebot",
1552
+ enabled: true
1553
+ });
1554
+ this.ctx.logger.info(`创建 Bot 配置: ${data.selfId}`);
1555
+ await this.startBot(this.configManager.toBotConfig(record));
1556
+ ctx.body = { success: true };
1557
+ });
1558
+ router.post(`${adminPath}/api/update`, authMiddleware, async (ctx) => {
1559
+ const data = await this.parseJsonBody(ctx);
1560
+ const existing = await this.configManager.getConfig(data.selfId);
1561
+ if (!existing) {
1562
+ ctx.status = 404;
1563
+ ctx.body = { error: `Bot ${data.selfId} 不存在` };
1564
+ return;
1565
+ }
1566
+ if (data.updates?.protocol === "ws" && !data.updates?.endpoint && !existing.endpoint) {
1567
+ ctx.status = 400;
1568
+ ctx.body = { error: "WS 协议需要配置 endpoint" };
1569
+ return;
1570
+ }
1571
+ await this.configManager.updateConfig(data.selfId, data.updates);
1572
+ this.ctx.logger.info(`更新 Bot 配置: ${data.selfId}`);
1573
+ ctx.body = { success: true, needRestart: true };
1574
+ });
1575
+ router.post(`${adminPath}/api/delete`, authMiddleware, async (ctx) => {
1576
+ const data = await this.parseJsonBody(ctx);
1577
+ const selfId = data.selfId;
1578
+ const existing = await this.configManager.getConfig(selfId);
1579
+ if (!existing) {
1580
+ ctx.status = 404;
1581
+ ctx.body = { error: `Bot ${selfId} 不存在` };
1582
+ return;
1583
+ }
1584
+ await this.stopBot(selfId);
1585
+ await this.configManager.deleteConfig(selfId);
1586
+ this.ctx.logger.info(`删除 Bot 配置: ${selfId}`);
1587
+ ctx.body = { success: true };
1588
+ });
1589
+ router.post(`${adminPath}/api/toggle`, authMiddleware, async (ctx) => {
1590
+ const data = await this.parseJsonBody(ctx);
1591
+ const selfId = data.selfId;
1592
+ const existing = await this.configManager.getConfig(selfId);
1593
+ if (!existing) {
1594
+ ctx.status = 404;
1595
+ ctx.body = { error: `Bot ${selfId} 不存在` };
1596
+ return;
1597
+ }
1598
+ const newEnabled = !existing.enabled;
1599
+ await this.configManager.updateConfig(selfId, { enabled: newEnabled });
1600
+ if (newEnabled) {
1601
+ const config = await this.configManager.getConfig(selfId);
1602
+ if (config) {
1603
+ await this.startBot(this.configManager.toBotConfig(config));
1604
+ }
1605
+ } else {
1606
+ await this.stopBot(selfId);
1607
+ }
1608
+ this.ctx.logger.info(`${newEnabled ? "启用" : "禁用"} Bot: ${selfId}`);
1609
+ ctx.body = { success: true, enabled: newEnabled };
1610
+ });
1611
+ router.post(`${adminPath}/api/restart`, authMiddleware, async (ctx) => {
1612
+ const data = await this.parseJsonBody(ctx);
1613
+ const selfId = data.selfId;
1614
+ const config = await this.configManager.getConfig(selfId);
1615
+ if (!config) {
1616
+ ctx.status = 404;
1617
+ ctx.body = { error: `Bot ${selfId} 不存在` };
1618
+ return;
1619
+ }
1620
+ if (!config.enabled) {
1621
+ ctx.status = 400;
1622
+ ctx.body = { error: `Bot ${selfId} 未启用` };
1623
+ return;
1624
+ }
1625
+ await this.stopBot(selfId);
1626
+ await this.startBot(this.configManager.toBotConfig(config));
1627
+ this.ctx.logger.info(`重启 Bot: ${selfId}`);
1628
+ ctx.body = { success: true };
1629
+ });
1630
+ this.app.use(router.routes());
1631
+ this.app.use(router.allowedMethods());
1632
+ }
1633
+ async startBot(config) {
1634
+ const existing = this.ctx.bots.find((b) => b.selfId === config.selfId && b.platform === "onebot");
1635
+ if (existing) {
1636
+ this.ctx.logger.warn(`Bot ${config.selfId} 已在运行`);
1637
+ return;
1638
+ }
1639
+ const globalConfig = this.ctx._onebotMultiGlobalConfig || {};
1640
+ const fullConfig = {
1641
+ ...globalConfig,
1642
+ ...config,
1643
+ protocol: config.protocol || "ws-reverse"
1644
+ };
1645
+ this.ctx.plugin(OneBotBot, fullConfig);
1646
+ }
1647
+ async stopBot(selfId) {
1648
+ const bot = this.ctx.bots.find((b) => b.selfId === selfId && b.platform === "onebot");
1649
+ if (bot) {
1650
+ await bot.stop();
1651
+ delete this.ctx.bots[bot.sid];
1652
+ }
1653
+ }
1654
+ start() {
1655
+ const port = this.config.port || 8212;
1656
+ this.server = this.app.listen(port, () => {
1657
+ this.ctx.logger.info(`状态面板已启动: http://localhost:${port}${this.config.basePath}`);
1658
+ this.ctx.logger.info(`管理面板已启动: http://localhost:${port}${this.config.basePath}/admin`);
1659
+ });
1660
+ }
1661
+ stop() {
1662
+ if (this.server) {
1663
+ this.server.close();
1664
+ this.ctx.logger.info("状态面板已关闭");
1665
+ }
1666
+ }
1667
+ renderLoginPage() {
1668
+ const basePath = this.config.basePath || "/status";
1669
+ return `<!DOCTYPE html>
1670
+ <html lang="zh-CN">
1671
+ <head>
1672
+ <meta charset="UTF-8">
1673
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1674
+ <title>OneBot Multi - 管理登录</title>
1675
+ <link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
1676
+ <style>
1677
+ :root {
1678
+ --nl-primary: #fbbf24;
1679
+ --nl-bg: #fffbeb;
1680
+ --nl-surface: #ffffff;
1681
+ --nl-text: #451a03;
1682
+ --nl-border: 3px solid #451a03;
1683
+ --nl-shadow: 4px 4px 0 #451a03;
1684
+ }
1685
+
1686
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1687
+
1688
+ body {
1689
+ font-family: 'Noto Sans SC', 'Segoe UI', sans-serif;
1690
+ background-color: var(--nl-bg);
1691
+ background-image:
1692
+ radial-gradient(#fde68a 1px, transparent 1px),
1693
+ radial-gradient(#fde68a 1px, transparent 1px);
1694
+ background-size: 20px 20px;
1695
+ background-position: 0 0, 10px 10px;
1696
+ color: var(--nl-text);
1697
+ min-height: 100vh;
1698
+ display: flex;
1699
+ align-items: center;
1700
+ justify-content: center;
1701
+ padding: 2rem;
1702
+ }
1703
+
1704
+ .login-box {
1705
+ background: var(--nl-surface);
1706
+ padding: 3rem;
1707
+ border: var(--nl-border);
1708
+ box-shadow: var(--nl-shadow);
1709
+ border-radius: 16px;
1710
+ max-width: 400px;
1711
+ width: 100%;
1712
+ text-align: center;
1713
+ }
1714
+
1715
+ h1 {
1716
+ font-family: 'Fredoka One', 'Noto Sans SC', cursive;
1717
+ font-size: 1.8rem;
1718
+ margin-bottom: 2rem;
1719
+ text-shadow: 2px 2px 0 #fff;
1720
+ }
1721
+
1722
+ .form-group {
1723
+ margin-bottom: 1.5rem;
1724
+ }
1725
+
1726
+ label {
1727
+ display: block;
1728
+ font-weight: 700;
1729
+ margin-bottom: 0.5rem;
1730
+ text-align: left;
1731
+ }
1732
+
1733
+ input[type="password"] {
1734
+ width: 100%;
1735
+ padding: 1rem;
1736
+ border: var(--nl-border);
1737
+ border-radius: 8px;
1738
+ font-size: 1rem;
1739
+ font-family: inherit;
1740
+ }
1741
+
1742
+ input[type="password"]:focus {
1743
+ outline: none;
1744
+ box-shadow: 0 0 0 3px #fbbf24;
1745
+ }
1746
+
1747
+ .btn {
1748
+ background: var(--nl-primary);
1749
+ border: var(--nl-border);
1750
+ color: var(--nl-text);
1751
+ padding: 1rem 2rem;
1752
+ border-radius: 12px;
1753
+ cursor: pointer;
1754
+ font-size: 1.1rem;
1755
+ font-weight: 800;
1756
+ box-shadow: var(--nl-shadow);
1757
+ transition: all 0.1s;
1758
+ width: 100%;
1759
+ }
1760
+
1761
+ .btn:hover {
1762
+ transform: translate(-1px, -1px);
1763
+ box-shadow: 5px 5px 0 #451a03;
1764
+ }
1765
+
1766
+ .btn:active {
1767
+ transform: translate(1px, 1px);
1768
+ box-shadow: 2px 2px 0 #451a03;
1769
+ }
1770
+
1771
+ .error {
1772
+ color: #b91c1c;
1773
+ margin-top: 1rem;
1774
+ display: none;
1775
+ }
1776
+ </style>
1777
+ </head>
1778
+ <body>
1779
+ <div class="login-box">
1780
+ <h1>🔐 管理登录</h1>
1781
+ <form id="loginForm">
1782
+ <div class="form-group">
1783
+ <label for="adminKey">管理密钥</label>
1784
+ <input type="password" id="adminKey" placeholder="请输入管理密钥" required>
1785
+ </div>
1786
+ <button type="submit" class="btn">登录</button>
1787
+ <p class="error" id="error">密钥错误</p>
1788
+ </form>
1789
+ </div>
1790
+
1791
+ <script>
1792
+ document.getElementById('loginForm').addEventListener('submit', async function(e) {
1793
+ e.preventDefault()
1794
+ const password = document.getElementById('adminKey').value
1795
+ const errorEl = document.getElementById('error')
1796
+ const btn = this.querySelector('button[type="submit"]')
1797
+ btn.disabled = true
1798
+ btn.textContent = '登录中...'
1799
+
1800
+ try {
1801
+ const res = await fetch('${basePath}/admin/api/login', {
1802
+ method: 'POST',
1803
+ headers: { 'Content-Type': 'application/json' },
1804
+ body: JSON.stringify({ password }),
1805
+ credentials: 'include'
1806
+ })
1807
+ const data = await res.json()
1808
+ if (data.success) {
1809
+ // 短暂延迟确保 Cookie 已设置
1810
+ setTimeout(() => {
1811
+ window.location.href = '${basePath}/admin'
1812
+ }, 100)
1813
+ } else {
1814
+ errorEl.textContent = data.error || '登录失败'
1815
+ errorEl.style.display = 'block'
1816
+ btn.disabled = false
1817
+ btn.textContent = '登录'
1818
+ }
1819
+ } catch (e) {
1820
+ errorEl.textContent = '网络错误'
1821
+ errorEl.style.display = 'block'
1822
+ btn.disabled = false
1823
+ btn.textContent = '登录'
1824
+ }
1825
+ })
1826
+ </script>
1827
+ </body>
1828
+ </html>`;
1829
+ }
1830
+ renderSetupPage() {
1831
+ const basePath = this.config.basePath || "/status";
1832
+ return `<!DOCTYPE html>
1833
+ <html lang="zh-CN">
1834
+ <head>
1835
+ <meta charset="UTF-8">
1836
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1837
+ <title>OneBot Multi - 初始设置</title>
1838
+ <link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
1839
+ <style>
1840
+ :root {
1841
+ --nl-primary: #fbbf24;
1842
+ --nl-bg: #fffbeb;
1843
+ --nl-surface: #ffffff;
1844
+ --nl-text: #451a03;
1845
+ --nl-border: 3px solid #451a03;
1846
+ --nl-shadow: 4px 4px 0 #451a03;
1847
+ }
1848
+
1849
+ * { margin: 0; padding: 0; box-sizing: border-box; }
1850
+
1851
+ body {
1852
+ font-family: 'Noto Sans SC', 'Segoe UI', sans-serif;
1853
+ background-color: var(--nl-bg);
1854
+ background-image:
1855
+ radial-gradient(#fde68a 1px, transparent 1px),
1856
+ radial-gradient(#fde68a 1px, transparent 1px);
1857
+ background-size: 20px 20px;
1858
+ background-position: 0 0, 10px 10px;
1859
+ color: var(--nl-text);
1860
+ min-height: 100vh;
1861
+ display: flex;
1862
+ align-items: center;
1863
+ justify-content: center;
1864
+ padding: 2rem;
1865
+ }
1866
+
1867
+ .setup-box {
1868
+ background: var(--nl-surface);
1869
+ padding: 3rem;
1870
+ border: var(--nl-border);
1871
+ box-shadow: var(--nl-shadow);
1872
+ border-radius: 16px;
1873
+ max-width: 450px;
1874
+ width: 100%;
1875
+ text-align: center;
1876
+ }
1877
+
1878
+ h1 {
1879
+ font-family: 'Fredoka One', 'Noto Sans SC', cursive;
1880
+ font-size: 1.8rem;
1881
+ margin-bottom: 1rem;
1882
+ text-shadow: 2px 2px 0 #fff;
1883
+ }
1884
+
1885
+ .desc {
1886
+ color: #92400e;
1887
+ margin-bottom: 2rem;
1888
+ font-weight: 600;
1889
+ }
1890
+
1891
+ .form-group {
1892
+ margin-bottom: 1.5rem;
1893
+ text-align: left;
1894
+ }
1895
+
1896
+ label {
1897
+ display: block;
1898
+ font-weight: 700;
1899
+ margin-bottom: 0.5rem;
1900
+ }
1901
+
1902
+ input[type="password"] {
1903
+ width: 100%;
1904
+ padding: 1rem;
1905
+ border: var(--nl-border);
1906
+ border-radius: 8px;
1907
+ font-size: 1rem;
1908
+ font-family: inherit;
1909
+ }
1910
+
1911
+ input[type="password"]:focus {
1912
+ outline: none;
1913
+ box-shadow: 0 0 0 3px #fbbf24;
1914
+ }
1915
+
1916
+ .btn {
1917
+ background: var(--nl-primary);
1918
+ border: var(--nl-border);
1919
+ color: var(--nl-text);
1920
+ padding: 1rem 2rem;
1921
+ border-radius: 12px;
1922
+ cursor: pointer;
1923
+ font-size: 1.1rem;
1924
+ font-weight: 800;
1925
+ box-shadow: var(--nl-shadow);
1926
+ transition: all 0.1s;
1927
+ width: 100%;
1928
+ }
1929
+
1930
+ .btn:hover {
1931
+ transform: translate(-1px, -1px);
1932
+ box-shadow: 5px 5px 0 #451a03;
1933
+ }
1934
+
1935
+ .btn:active {
1936
+ transform: translate(1px, 1px);
1937
+ box-shadow: 2px 2px 0 #451a03;
1938
+ }
1939
+
1940
+ .error {
1941
+ color: #b91c1c;
1942
+ margin-top: 1rem;
1943
+ display: none;
1944
+ }
1945
+ </style>
1946
+ </head>
1947
+ <body>
1948
+ <div class="setup-box">
1949
+ <h1>🔧 初始设置</h1>
1950
+ <p class="desc">首次使用,请设置管理密码</p>
1951
+ <form id="setupForm">
1952
+ <div class="form-group">
1953
+ <label for="password">设置密码</label>
1954
+ <input type="password" id="password" placeholder="至少4位" required minlength="4">
1955
+ </div>
1956
+ <div class="form-group">
1957
+ <label for="confirmPassword">确认密码</label>
1958
+ <input type="password" id="confirmPassword" placeholder="再次输入密码" required>
1959
+ </div>
1960
+ <button type="submit" class="btn">确认设置</button>
1961
+ <p class="error" id="error"></p>
1962
+ </form>
1963
+ </div>
1964
+
1965
+ <script>
1966
+ document.getElementById('setupForm').addEventListener('submit', async function(e) {
1967
+ e.preventDefault()
1968
+ const password = document.getElementById('password').value
1969
+ const confirmPassword = document.getElementById('confirmPassword').value
1970
+ const errorEl = document.getElementById('error')
1971
+
1972
+ if (password !== confirmPassword) {
1973
+ errorEl.textContent = '两次密码不一致'
1974
+ errorEl.style.display = 'block'
1975
+ return
1976
+ }
1977
+
1978
+ if (password.length < 4) {
1979
+ errorEl.textContent = '密码至少需要4位'
1980
+ errorEl.style.display = 'block'
1981
+ return
1982
+ }
1983
+
1984
+ try {
1985
+ const res = await fetch('${basePath}/admin/api/init-password', {
1986
+ method: 'POST',
1987
+ headers: { 'Content-Type': 'application/json' },
1988
+ body: JSON.stringify({ password }),
1989
+ credentials: 'include'
1990
+ })
1991
+ const data = await res.json()
1992
+ if (data.success) {
1993
+ window.location.href = '${basePath}/admin'
1994
+ } else {
1995
+ errorEl.textContent = data.error || '设置失败'
1996
+ errorEl.style.display = 'block'
1997
+ }
1998
+ } catch (e) {
1999
+ errorEl.textContent = '网络错误'
2000
+ errorEl.style.display = 'block'
2001
+ }
2002
+ })
2003
+ </script>
2004
+ </body>
2005
+ </html>`;
2006
+ }
2007
+ renderStatusPage() {
2008
+ const basePath = this.config.basePath || "/status";
2009
+ const status = this.statusManager.getStatus();
2010
+ const onlineCount = status.bots.filter((b) => b.status === "online").length;
2011
+ const offlineCount = status.bots.filter((b) => b.status === "offline").length;
2012
+ const totalCount = status.bots.length;
2013
+ const botCardsHtml = status.bots.map((bot) => {
2014
+ const statusText = bot.status === "online" ? "在线" : bot.status === "offline" ? "离线" : "连接中";
2015
+ const displayName = bot.nickname || bot.selfId;
2016
+ const groupCount = bot.groupCount ?? "-";
2017
+ const friendCount = bot.friendCount ?? "-";
2018
+ const messageReceived = bot.messageReceived ?? "-";
2019
+ const messageSent = bot.messageSent ?? "-";
2020
+ const lastMessageTime = bot.lastMessageTime ? new Date(bot.lastMessageTime * 1e3).toLocaleTimeString() : "-";
2021
+ const uptime = bot.startupTime ? this.formatUptime(bot.startupTime) : "-";
2022
+ return `
2023
+ <div class="bot-card">
2024
+ <div class="bot-header">
2025
+ <img class="bot-avatar" src="http://q.qlogo.cn/headimg_dl?dst_uin=${bot.selfId}&spec=640" alt="avatar">
2026
+ <div class="bot-info">
2027
+ <div class="bot-id">${displayName}</div>
2028
+ <div class="bot-protocol">QQ: ${bot.selfId}</div>
2029
+ </div>
2030
+ <span class="status-badge status-${bot.status}">${statusText}</span>
2031
+ </div>
2032
+ <div class="bot-stats">
2033
+ <div class="bot-stat">
2034
+ <span class="bot-stat-label">群聊</span>
2035
+ <span class="bot-stat-value">${groupCount}</span>
2036
+ </div>
2037
+ <div class="bot-stat">
2038
+ <span class="bot-stat-label">好友</span>
2039
+ <span class="bot-stat-value">${friendCount}</span>
2040
+ </div>
2041
+ <div class="bot-stat">
2042
+ <span class="bot-stat-label">收到</span>
2043
+ <span class="bot-stat-value">${messageReceived}</span>
2044
+ </div>
2045
+ <div class="bot-stat">
2046
+ <span class="bot-stat-label">发送</span>
2047
+ <span class="bot-stat-value">${messageSent}</span>
2048
+ </div>
2049
+ <div class="bot-stat">
2050
+ <span class="bot-stat-label">最后消息</span>
2051
+ <span class="bot-stat-value">${lastMessageTime}</span>
2052
+ </div>
2053
+ <div class="bot-stat">
2054
+ <span class="bot-stat-label">运行时间</span>
2055
+ <span class="bot-stat-value">${uptime}</span>
2056
+ </div>
2057
+ </div>
2058
+ </div>`;
2059
+ }).join("");
2060
+ const updatedAtStr = new Date(status.updatedAt).toLocaleString();
2061
+ return `<!DOCTYPE html>
2062
+ <html lang="zh-CN">
2063
+ <head>
2064
+ <meta charset="UTF-8">
2065
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2066
+ <title>OneBot Multi - 状态面板</title>
2067
+ <link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
2068
+ <style>
2069
+ :root {
2070
+ --nl-primary: #fbbf24;
2071
+ --nl-bg: #fffbeb;
2072
+ --nl-surface: #ffffff;
2073
+ --nl-text: #451a03;
2074
+ --nl-border: 3px solid #451a03;
2075
+ --nl-shadow: 4px 4px 0 #451a03;
2076
+
2077
+ --status-online: #32CD32;
2078
+ --status-offline: #FF4500;
2079
+ --status-connecting: #FFA500;
2080
+ }
2081
+
2082
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2083
+
2084
+ body {
2085
+ font-family: 'Noto Sans SC', 'Segoe UI', sans-serif;
2086
+ background-color: var(--nl-bg);
2087
+ background-image:
2088
+ radial-gradient(#fde68a 1px, transparent 1px),
2089
+ radial-gradient(#fde68a 1px, transparent 1px);
2090
+ background-size: 20px 20px;
2091
+ background-position: 0 0, 10px 10px;
2092
+ color: var(--nl-text);
2093
+ min-height: 100vh;
2094
+ padding: 2rem;
2095
+ }
2096
+
2097
+ .container {
2098
+ max-width: 1200px;
2099
+ margin: 0 auto;
2100
+ background: var(--nl-surface);
2101
+ padding: 2rem;
2102
+ border: var(--nl-border);
2103
+ box-shadow: var(--nl-shadow);
2104
+ border-radius: 16px;
2105
+ }
2106
+
2107
+ h1 {
2108
+ font-family: 'Fredoka One', 'Noto Sans SC', cursive;
2109
+ font-size: 2.5rem;
2110
+ margin-bottom: 2rem;
2111
+ text-align: center;
2112
+ text-shadow: 2px 2px 0 #fff;
2113
+ color: var(--nl-text);
2114
+ }
2115
+
2116
+ .stats {
2117
+ display: grid;
2118
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
2119
+ gap: 1.5rem;
2120
+ margin-bottom: 3rem;
2121
+ }
2122
+
2123
+ .stat-card {
2124
+ background: var(--nl-surface);
2125
+ border: var(--nl-border);
2126
+ padding: 1rem;
2127
+ text-align: center;
2128
+ box-shadow: 2px 2px 0 #451a03;
2129
+ transition: transform 0.2s, box-shadow 0.2s;
2130
+ border-radius: 16px;
2131
+ }
2132
+
2133
+ .stat-card:hover {
2134
+ transform: translate(-2px, -2px);
2135
+ box-shadow: 4px 4px 0 #451a03;
2136
+ }
2137
+
2138
+ .stat-value {
2139
+ font-family: 'Fredoka One', sans-serif;
2140
+ font-size: 2.5rem;
2141
+ line-height: 1.2;
2142
+ }
2143
+
2144
+ .stat-label {
2145
+ font-weight: 800;
2146
+ font-size: 14px;
2147
+ color: #92400e;
2148
+ }
2149
+
2150
+ .stat-online .stat-value { color: var(--status-online); }
2151
+ .stat-offline .stat-value { color: var(--status-offline); }
2152
+ .stat-total .stat-value { color: #00BFFF; }
2153
+
2154
+ .bots {
2155
+ display: grid;
2156
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
2157
+ gap: 2rem;
2158
+ }
2159
+
2160
+ .bot-card {
2161
+ background: var(--nl-surface);
2162
+ border: var(--nl-border);
2163
+ border-radius: 16px;
2164
+ padding: 1.5rem;
2165
+ position: relative;
2166
+ box-shadow: var(--nl-shadow);
2167
+ transition: all 0.2s ease;
2168
+ overflow: hidden;
2169
+ }
2170
+
2171
+ .bot-card:hover {
2172
+ transform: translate(-2px, -2px);
2173
+ box-shadow: 6px 6px 0 #451a03;
2174
+ }
2175
+
2176
+ .bot-header {
2177
+ display: flex;
2178
+ align-items: center;
2179
+ gap: 1rem;
2180
+ margin-bottom: 1.5rem;
2181
+ }
2182
+
2183
+ .bot-avatar {
2184
+ width: 64px;
2185
+ height: 64px;
2186
+ border: var(--nl-border);
2187
+ border-radius: 50%;
2188
+ background: #fff;
2189
+ }
2190
+
2191
+ .bot-info { flex: 1; }
2192
+
2193
+ .bot-id {
2194
+ font-weight: 900;
2195
+ font-size: 1.2rem;
2196
+ color: var(--nl-text);
2197
+ }
2198
+
2199
+ .bot-protocol {
2200
+ color: #92400e;
2201
+ font-size: 0.9rem;
2202
+ font-weight: 700;
2203
+ background: #fffbeb;
2204
+ display: inline-block;
2205
+ padding: 2px 6px;
2206
+ border: 2px solid #451a03;
2207
+ border-radius: 6px;
2208
+ margin-top: 4px;
2209
+ }
2210
+
2211
+ .status-badge {
2212
+ padding: 0.4rem 0.8rem;
2213
+ border: 2px solid #451a03;
2214
+ border-radius: 20px;
2215
+ font-size: 0.8rem;
2216
+ font-weight: 900;
2217
+ }
2218
+
2219
+ .status-online { background: #dcfce7; border-color: #10b981; color: #15803d; }
2220
+ .status-offline { background: #fee2e2; border-color: #ef4444; color: #b91c1c; }
2221
+ .status-connecting { background: #ffedd5; border-color: #f97316; color: #c2410c; }
2222
+
2223
+ .bot-stats {
2224
+ display: grid;
2225
+ grid-template-columns: 1fr 1fr;
2226
+ gap: 10px;
2227
+ background: #fff7ed;
2228
+ padding: 1rem;
2229
+ border: 2px solid #451a03;
2230
+ border-radius: 12px;
2231
+ }
2232
+
2233
+ .bot-stat {
2234
+ display: flex;
2235
+ flex-direction: column;
2236
+ align-items: flex-start;
2237
+ }
2238
+
2239
+ .bot-stat-label {
2240
+ font-size: 0.75rem;
2241
+ font-weight: 800;
2242
+ color: #92400e;
2243
+ }
2244
+
2245
+ .bot-stat-value {
2246
+ font-weight: 700;
2247
+ font-size: 1rem;
2248
+ color: var(--nl-text);
2249
+ }
2250
+
2251
+ .footer {
2252
+ margin-top: 3rem;
2253
+ text-align: center;
2254
+ font-weight: 700;
2255
+ color: #92400e;
2256
+ padding: 0.5rem;
2257
+ width: 100%;
2258
+ }
2259
+
2260
+ .refresh-btn {
2261
+ position: fixed;
2262
+ bottom: 2rem;
2263
+ right: 2rem;
2264
+ background: var(--nl-primary);
2265
+ border: var(--nl-border);
2266
+ color: var(--nl-text);
2267
+ padding: 1rem 2rem;
2268
+ border-radius: 12px;
2269
+ cursor: pointer;
2270
+ font-size: 1.1rem;
2271
+ font-weight: 800;
2272
+ box-shadow: var(--nl-shadow);
2273
+ transition: all 0.1s;
2274
+ z-index: 100;
2275
+ }
2276
+
2277
+ .refresh-btn:hover {
2278
+ transform: translate(-1px, -1px);
2279
+ box-shadow: 5px 5px 0 #451a03;
2280
+ }
2281
+
2282
+ .refresh-btn:active {
2283
+ transform: translate(1px, 1px);
2284
+ box-shadow: 2px 2px 0 #451a03;
2285
+ }
2286
+
2287
+ @media (max-width: 768px) {
2288
+ body { padding: 1rem; }
2289
+ .container { padding: 1rem; }
2290
+ .stats { grid-template-columns: 1fr; }
2291
+ .bot-card { margin-bottom: 1rem; }
2292
+ }
2293
+ </style>
2294
+ </head>
2295
+ <body>
2296
+ <div class="container">
2297
+ <h1 ondblclick="window.location='${basePath}/admin'" style="cursor: default;" title="双击进入管理">OneBot Multi 状态面板</h1>
2298
+
2299
+ <div class="stats">
2300
+ <div class="stat-card stat-online">
2301
+ <div class="stat-value">${onlineCount}</div>
2302
+ <div class="stat-label">在线</div>
2303
+ </div>
2304
+ <div class="stat-card stat-offline">
2305
+ <div class="stat-value">${offlineCount}</div>
2306
+ <div class="stat-label">离线</div>
2307
+ </div>
2308
+ <div class="stat-card stat-total">
2309
+ <div class="stat-value">${totalCount}</div>
2310
+ <div class="stat-label">总计</div>
2311
+ </div>
2312
+ </div>
2313
+
2314
+ <div class="bots">
2315
+ ${botCardsHtml}
2316
+ </div>
2317
+
2318
+ <div class="footer">
2319
+ 更新时间: ${updatedAtStr}
2320
+ </div>
2321
+ </div>
2322
+
2323
+ <button class="refresh-btn" onclick="location.reload()">刷新 ⚡</button>
2324
+
2325
+ <script>
2326
+ // 自动刷新
2327
+ setTimeout(() => location.reload(), 30000)
2328
+ </script>
2329
+ </body>
2330
+ </html>`;
2331
+ }
2332
+ renderAdminPage() {
2333
+ const basePath = this.config.basePath || "/status";
2334
+ return `<!DOCTYPE html>
2335
+ <html lang="zh-CN">
2336
+ <head>
2337
+ <meta charset="UTF-8">
2338
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
2339
+ <title>OneBot Multi - 管理面板</title>
2340
+ <link href="https://fonts.googleapis.com/css2?family=Fredoka+One&family=Noto+Sans+SC:wght@400;700;900&display=swap" rel="stylesheet">
2341
+ <style>
2342
+ :root {
2343
+ --nl-primary: #fbbf24;
2344
+ --nl-bg: #fffbeb;
2345
+ --nl-surface: #ffffff;
2346
+ --nl-text: #451a03;
2347
+ --nl-border: 3px solid #451a03;
2348
+ --nl-shadow: 4px 4px 0 #451a03;
2349
+
2350
+ --status-online: #32CD32;
2351
+ --status-offline: #FF4500;
2352
+ --status-connecting: #FFA500;
2353
+ }
2354
+
2355
+ * { margin: 0; padding: 0; box-sizing: border-box; }
2356
+
2357
+ body {
2358
+ font-family: 'Noto Sans SC', 'Segoe UI', sans-serif;
2359
+ background-color: var(--nl-bg);
2360
+ background-image:
2361
+ radial-gradient(#fde68a 1px, transparent 1px),
2362
+ radial-gradient(#fde68a 1px, transparent 1px);
2363
+ background-size: 20px 20px;
2364
+ background-position: 0 0, 10px 10px;
2365
+ color: var(--nl-text);
2366
+ min-height: 100vh;
2367
+ padding: 2rem;
2368
+ }
2369
+
2370
+ .container {
2371
+ max-width: 1200px;
2372
+ margin: 0 auto;
2373
+ background: var(--nl-surface);
2374
+ padding: 2rem;
2375
+ border: var(--nl-border);
2376
+ box-shadow: var(--nl-shadow);
2377
+ border-radius: 16px;
2378
+ }
2379
+
2380
+ h1 {
2381
+ font-family: 'Fredoka One', 'Noto Sans SC', cursive;
2382
+ font-size: 2.5rem;
2383
+ margin-bottom: 2rem;
2384
+ text-align: center;
2385
+ text-shadow: 2px 2px 0 #fff;
2386
+ color: var(--nl-text);
2387
+ }
2388
+
2389
+ .toolbar {
2390
+ display: flex;
2391
+ gap: 1rem;
2392
+ margin-bottom: 2rem;
2393
+ flex-wrap: wrap;
2394
+ }
2395
+
2396
+ .btn {
2397
+ background: var(--nl-primary);
2398
+ border: var(--nl-border);
2399
+ color: var(--nl-text);
2400
+ padding: 0.8rem 1.5rem;
2401
+ border-radius: 12px;
2402
+ cursor: pointer;
2403
+ font-size: 1rem;
2404
+ font-weight: 800;
2405
+ box-shadow: 3px 3px 0 #451a03;
2406
+ transition: all 0.1s;
2407
+ text-decoration: none;
2408
+ }
2409
+
2410
+ .btn:hover {
2411
+ transform: translate(-1px, -1px);
2412
+ box-shadow: 4px 4px 0 #451a03;
2413
+ }
2414
+
2415
+ .btn:active {
2416
+ transform: translate(1px, 1px);
2417
+ box-shadow: 2px 2px 0 #451a03;
2418
+ }
2419
+
2420
+ .btn-danger {
2421
+ background: #fee2e2;
2422
+ }
2423
+
2424
+ .btn-success {
2425
+ background: #dcfce7;
2426
+ }
2427
+
2428
+ .btn-secondary {
2429
+ background: #f3f4f6;
2430
+ }
2431
+
2432
+ .bots {
2433
+ display: grid;
2434
+ grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
2435
+ gap: 2rem;
2436
+ }
2437
+
2438
+ .bot-card {
2439
+ background: var(--nl-surface);
2440
+ border: var(--nl-border);
2441
+ border-radius: 16px;
2442
+ padding: 1.5rem;
2443
+ position: relative;
2444
+ box-shadow: var(--nl-shadow);
2445
+ transition: all 0.2s ease;
2446
+ overflow: hidden;
2447
+ }
2448
+
2449
+ .bot-card.disabled {
2450
+ opacity: 0.6;
2451
+ }
2452
+
2453
+ .bot-card:hover {
2454
+ transform: translate(-2px, -2px);
2455
+ box-shadow: 6px 6px 0 #451a03;
2456
+ }
2457
+
2458
+ .bot-header {
2459
+ display: flex;
2460
+ align-items: center;
2461
+ gap: 1rem;
2462
+ margin-bottom: 1rem;
2463
+ }
2464
+
2465
+ .bot-avatar {
2466
+ width: 64px;
2467
+ height: 64px;
2468
+ border: var(--nl-border);
2469
+ border-radius: 50%;
2470
+ background: #fff;
2471
+ }
2472
+
2473
+ .bot-info { flex: 1; }
2474
+
2475
+ .bot-id {
2476
+ font-weight: 900;
2477
+ font-size: 1.2rem;
2478
+ color: var(--nl-text);
2479
+ }
2480
+
2481
+ .bot-protocol {
2482
+ color: #92400e;
2483
+ font-size: 0.9rem;
2484
+ font-weight: 700;
2485
+ background: #fffbeb;
2486
+ display: inline-block;
2487
+ padding: 2px 6px;
2488
+ border: 2px solid #451a03;
2489
+ border-radius: 6px;
2490
+ margin-top: 4px;
2491
+ }
2492
+
2493
+ .status-badge {
2494
+ padding: 0.4rem 0.8rem;
2495
+ border: 2px solid #451a03;
2496
+ border-radius: 20px;
2497
+ font-size: 0.8rem;
2498
+ font-weight: 900;
2499
+ }
2500
+
2501
+ .status-online { background: #dcfce7; border-color: #10b981; color: #15803d; }
2502
+ .status-offline { background: #fee2e2; border-color: #ef4444; color: #b91c1c; }
2503
+ .status-connecting { background: #ffedd5; border-color: #f97316; color: #c2410c; }
2504
+
2505
+ .bot-stats {
2506
+ display: grid;
2507
+ grid-template-columns: 1fr 1fr;
2508
+ gap: 10px;
2509
+ background: #fff7ed;
2510
+ padding: 1rem;
2511
+ border: 2px solid #451a03;
2512
+ border-radius: 12px;
2513
+ margin-bottom: 1rem;
2514
+ }
2515
+
2516
+ .bot-stat {
2517
+ display: flex;
2518
+ flex-direction: column;
2519
+ align-items: flex-start;
2520
+ }
2521
+
2522
+ .bot-stat-label {
2523
+ font-size: 0.75rem;
2524
+ font-weight: 800;
2525
+ color: #92400e;
2526
+ }
2527
+
2528
+ .bot-stat-value {
2529
+ font-weight: 700;
2530
+ font-size: 1rem;
2531
+ color: var(--nl-text);
2532
+ }
2533
+
2534
+ .bot-actions {
2535
+ display: flex;
2536
+ gap: 0.5rem;
2537
+ flex-wrap: wrap;
2538
+ }
2539
+
2540
+ .bot-actions .btn {
2541
+ padding: 0.5rem 1rem;
2542
+ font-size: 0.85rem;
2543
+ }
2544
+
2545
+ /* Modal */
2546
+ .modal {
2547
+ display: none;
2548
+ position: fixed;
2549
+ top: 0;
2550
+ left: 0;
2551
+ width: 100%;
2552
+ height: 100%;
2553
+ background: rgba(0,0,0,0.5);
2554
+ z-index: 1000;
2555
+ align-items: center;
2556
+ justify-content: center;
2557
+ }
2558
+
2559
+ .modal.active {
2560
+ display: flex;
2561
+ }
2562
+
2563
+ .modal-content {
2564
+ background: var(--nl-surface);
2565
+ border: var(--nl-border);
2566
+ box-shadow: var(--nl-shadow);
2567
+ border-radius: 16px;
2568
+ padding: 2rem;
2569
+ max-width: 500px;
2570
+ width: 90%;
2571
+ }
2572
+
2573
+ .modal h2 {
2574
+ font-family: 'Fredoka One', cursive;
2575
+ margin-bottom: 1.5rem;
2576
+ }
2577
+
2578
+ .form-group {
2579
+ margin-bottom: 1rem;
2580
+ }
2581
+
2582
+ .form-group label {
2583
+ display: block;
2584
+ font-weight: 700;
2585
+ margin-bottom: 0.5rem;
2586
+ }
2587
+
2588
+ .form-group input, .form-group select {
2589
+ width: 100%;
2590
+ padding: 0.8rem;
2591
+ border: var(--nl-border);
2592
+ border-radius: 8px;
2593
+ font-size: 1rem;
2594
+ font-family: inherit;
2595
+ }
2596
+
2597
+ .form-group input:focus, .form-group select:focus {
2598
+ outline: none;
2599
+ box-shadow: 0 0 0 3px #fbbf24;
2600
+ }
2601
+
2602
+ .modal-actions {
2603
+ display: flex;
2604
+ gap: 1rem;
2605
+ margin-top: 1.5rem;
2606
+ }
2607
+
2608
+ .footer {
2609
+ margin-top: 3rem;
2610
+ text-align: center;
2611
+ font-weight: 700;
2612
+ color: #92400e;
2613
+ }
2614
+
2615
+ .toast {
2616
+ position: fixed;
2617
+ bottom: 2rem;
2618
+ left: 50%;
2619
+ transform: translateX(-50%);
2620
+ background: var(--nl-surface);
2621
+ border: var(--nl-border);
2622
+ box-shadow: var(--nl-shadow);
2623
+ padding: 1rem 2rem;
2624
+ border-radius: 12px;
2625
+ font-weight: 700;
2626
+ z-index: 2000;
2627
+ display: none;
2628
+ }
2629
+
2630
+ .toast.show {
2631
+ display: block;
2632
+ }
2633
+
2634
+ .toast.error {
2635
+ background: #fee2e2;
2636
+ }
2637
+
2638
+ .toast.success {
2639
+ background: #dcfce7;
2640
+ }
2641
+
2642
+ .empty-state {
2643
+ text-align: center;
2644
+ padding: 3rem;
2645
+ color: #92400e;
2646
+ }
2647
+
2648
+ .empty-state h3 {
2649
+ font-size: 1.5rem;
2650
+ margin-bottom: 1rem;
2651
+ }
2652
+
2653
+ @media (max-width: 768px) {
2654
+ body { padding: 1rem; }
2655
+ .container { padding: 1rem; }
2656
+ .bots { grid-template-columns: 1fr; }
2657
+ }
2658
+ </style>
2659
+ </head>
2660
+ <body>
2661
+ <div class="container">
2662
+ <h1>🛠️ OneBot Multi 管理面板</h1>
2663
+
2664
+ <div class="toolbar">
2665
+ <button class="btn" onclick="showAddModal()">➕ 添加 Bot</button>
2666
+ <button class="btn btn-secondary" onclick="refreshList()">🔄 刷新</button>
2667
+ <a href="${basePath}" class="btn btn-secondary">📊 状态面板</a>
2668
+ </div>
2669
+
2670
+ <div class="bots" id="botList">
2671
+ <div class="empty-state">
2672
+ <h3>加载中...</h3>
2673
+ </div>
2674
+ </div>
2675
+
2676
+ <div class="footer">
2677
+ OneBot Multi 管理面板
2678
+ </div>
2679
+ </div>
2680
+
2681
+ <!-- Add Modal -->
2682
+ <div class="modal" id="addModal">
2683
+ <div class="modal-content">
2684
+ <h2>添加 Bot</h2>
2685
+ <form id="addForm">
2686
+ <div class="form-group">
2687
+ <label for="selfId">QQ 号 *</label>
2688
+ <input type="text" id="selfId" required placeholder="例如: 123456789">
2689
+ </div>
2690
+ <div class="form-group">
2691
+ <label for="protocol">协议</label>
2692
+ <select id="protocol" onchange="toggleEndpoint()">
2693
+ <option value="ws-reverse">反向 WebSocket (ws-reverse)</option>
2694
+ <option value="ws">正向 WebSocket (ws)</option>
2695
+ </select>
2696
+ </div>
2697
+ <div class="form-group" id="endpointGroup" style="display: none;">
2698
+ <label for="endpoint">Endpoint *</label>
2699
+ <input type="text" id="endpoint" placeholder="例如: ws://127.0.0.1:6700">
2700
+ </div>
2701
+ <div class="form-group" id="pathGroup">
2702
+ <label for="path">路径</label>
2703
+ <input type="text" id="path" value="/onebot" placeholder="例如: /onebot">
2704
+ </div>
2705
+ <div class="form-group">
2706
+ <label for="token">Token(可选)</label>
2707
+ <input type="password" id="token" placeholder="访问令牌">
2708
+ </div>
2709
+ <div class="modal-actions">
2710
+ <button type="submit" class="btn">确定</button>
2711
+ <button type="button" class="btn btn-secondary" onclick="hideAddModal()">取消</button>
2712
+ </div>
2713
+ </form>
2714
+ </div>
2715
+ </div>
2716
+
2717
+ <!-- Toast -->
2718
+ <div class="toast" id="toast"></div>
2719
+
2720
+ <script>
2721
+ const BASE_PATH = '${basePath}/admin'
2722
+
2723
+ async function apiCall(endpoint, data = null) {
2724
+ const options = {
2725
+ method: data ? 'POST' : 'GET',
2726
+ headers: {
2727
+ 'Content-Type': 'application/json'
2728
+ },
2729
+ credentials: 'include'
2730
+ }
2731
+ if (data) {
2732
+ options.body = JSON.stringify(data)
2733
+ }
2734
+ const res = await fetch(BASE_PATH + endpoint, options)
2735
+ if (res.status === 401) {
2736
+ // 认证失败,重定向到登录页
2737
+ window.location.href = '${basePath}/admin'
2738
+ throw new Error('认证失败')
2739
+ }
2740
+ return res.json()
2741
+ }
2742
+
2743
+ function showToast(message, type = 'success') {
2744
+ const toast = document.getElementById('toast')
2745
+ toast.textContent = message
2746
+ toast.className = 'toast show ' + type
2747
+ setTimeout(() => toast.className = 'toast', 3000)
2748
+ }
2749
+
2750
+ function formatUptime(startTime) {
2751
+ const diff = Math.floor(Date.now() / 1000 - startTime)
2752
+ const hours = Math.floor(diff / 3600)
2753
+ const minutes = Math.floor((diff % 3600) / 60)
2754
+ if (hours > 0) return hours + 'h ' + minutes + 'm'
2755
+ return minutes + 'm'
2756
+ }
2757
+
2758
+ async function refreshList() {
2759
+ try {
2760
+ const data = await apiCall('/api/list')
2761
+ if (data.error) {
2762
+ showToast('加载失败: ' + data.error, 'error')
2763
+ return
2764
+ }
2765
+ renderBots(data.bots || [])
2766
+ } catch (e) {
2767
+ console.error('refreshList error:', e)
2768
+ showToast('加载失败: ' + e.message, 'error')
2769
+ }
2770
+ }
2771
+
2772
+ function renderBots(bots) {
2773
+ const container = document.getElementById('botList')
2774
+ if (bots.length === 0) {
2775
+ container.innerHTML = '<div class="empty-state"><h3>暂无 Bot</h3><p>点击"添加 Bot"按钮创建</p></div>'
2776
+ return
2777
+ }
2778
+
2779
+ container.innerHTML = bots.map(bot => {
2780
+ const statusText = bot.status === 'online' ? '在线' : bot.status === 'offline' ? '离线' : '连接中'
2781
+ const displayName = bot.nickname || bot.selfId
2782
+ const uptime = bot.startupTime ? formatUptime(bot.startupTime) : '-'
2783
+ const disabledClass = !bot.enabled ? 'disabled' : ''
2784
+
2785
+ return '<div class="bot-card ' + disabledClass + '">' +
2786
+ '<div class="bot-header">' +
2787
+ '<img class="bot-avatar" src="http://q.qlogo.cn/headimg_dl?dst_uin=' + bot.selfId + '&spec=640" alt="avatar">' +
2788
+ '<div class="bot-info">' +
2789
+ '<div class="bot-id">' + displayName + '</div>' +
2790
+ '<div class="bot-protocol">' + bot.protocol.toUpperCase() + ' | QQ: ' + bot.selfId + '</div>' +
2791
+ '</div>' +
2792
+ '<span class="status-badge status-' + bot.status + '">' + statusText + '</span>' +
2793
+ '</div>' +
2794
+ '<div class="bot-stats">' +
2795
+ '<div class="bot-stat"><span class="bot-stat-label">群聊</span><span class="bot-stat-value">' + (bot.groupCount ?? '-') + '</span></div>' +
2796
+ '<div class="bot-stat"><span class="bot-stat-label">好友</span><span class="bot-stat-value">' + (bot.friendCount ?? '-') + '</span></div>' +
2797
+ '<div class="bot-stat"><span class="bot-stat-label">收到</span><span class="bot-stat-value">' + (bot.messageReceived ?? '-') + '</span></div>' +
2798
+ '<div class="bot-stat"><span class="bot-stat-label">发送</span><span class="bot-stat-value">' + (bot.messageSent ?? '-') + '</span></div>' +
2799
+ '<div class="bot-stat"><span class="bot-stat-label">连接</span><span class="bot-stat-value">' + (bot.endpoint || bot.path || '-') + '</span></div>' +
2800
+ '<div class="bot-stat"><span class="bot-stat-label">运行时间</span><span class="bot-stat-value">' + uptime + '</span></div>' +
2801
+ '</div>' +
2802
+ '<div class="bot-actions">' +
2803
+ '<button class="btn btn-secondary" onclick="toggleBot('' + bot.selfId + '')">' + (bot.enabled ? '禁用' : '启用') + '</button>' +
2804
+ '<button class="btn btn-secondary" onclick="restartBot('' + bot.selfId + '')">重启</button>' +
2805
+ '<button class="btn btn-danger" onclick="deleteBot('' + bot.selfId + '')">删除</button>' +
2806
+ '</div>' +
2807
+ '</div>'
2808
+ }).join('')
2809
+ }
2810
+
2811
+ function showAddModal() {
2812
+ document.getElementById('addModal').classList.add('active')
2813
+ document.getElementById('addForm').reset()
2814
+ toggleEndpoint()
2815
+ }
2816
+
2817
+ function hideAddModal() {
2818
+ document.getElementById('addModal').classList.remove('active')
2819
+ }
2820
+
2821
+ function toggleEndpoint() {
2822
+ const protocol = document.getElementById('protocol').value
2823
+ document.getElementById('endpointGroup').style.display = protocol === 'ws' ? 'block' : 'none'
2824
+ document.getElementById('pathGroup').style.display = protocol === 'ws-reverse' ? 'block' : 'none'
2825
+ }
2826
+
2827
+ document.getElementById('addForm').addEventListener('submit', async (e) => {
2828
+ e.preventDefault()
2829
+ const data = {
2830
+ selfId: document.getElementById('selfId').value,
2831
+ protocol: document.getElementById('protocol').value,
2832
+ endpoint: document.getElementById('endpoint').value || undefined,
2833
+ path: document.getElementById('path').value || undefined,
2834
+ token: document.getElementById('token').value || undefined
2835
+ }
2836
+
2837
+ try {
2838
+ const result = await apiCall('/api/create', data)
2839
+ if (result.error) throw new Error(result.error)
2840
+ showToast('Bot 添加成功')
2841
+ hideAddModal()
2842
+ refreshList()
2843
+ } catch (e) {
2844
+ showToast('添加失败: ' + e.message, 'error')
2845
+ }
2846
+ })
2847
+
2848
+ async function toggleBot(selfId) {
2849
+ try {
2850
+ const result = await apiCall('/api/toggle', { selfId })
2851
+ if (result.error) throw new Error(result.error)
2852
+ showToast(result.enabled ? 'Bot 已启用' : 'Bot 已禁用')
2853
+ refreshList()
2854
+ } catch (e) {
2855
+ showToast('操作失败: ' + e.message, 'error')
2856
+ }
2857
+ }
2858
+
2859
+ async function restartBot(selfId) {
2860
+ try {
2861
+ const result = await apiCall('/api/restart', { selfId })
2862
+ if (result.error) throw new Error(result.error)
2863
+ showToast('Bot 正在重启')
2864
+ setTimeout(refreshList, 1000)
2865
+ } catch (e) {
2866
+ showToast('重启失败: ' + e.message, 'error')
2867
+ }
2868
+ }
2869
+
2870
+ async function deleteBot(selfId) {
2871
+ if (!confirm('确定要删除 Bot ' + selfId + ' 吗?')) return
2872
+ try {
2873
+ const result = await apiCall('/api/delete', { selfId })
2874
+ if (result.error) throw new Error(result.error)
2875
+ showToast('Bot 已删除')
2876
+ refreshList()
2877
+ } catch (e) {
2878
+ showToast('删除失败: ' + e.message, 'error')
2879
+ }
2880
+ }
2881
+
2882
+ // 初始加载
2883
+ refreshList()
2884
+ // 自动刷新
2885
+ setInterval(refreshList, 30000)
2886
+ </script>
2887
+ </body>
2888
+ </html>`;
2889
+ }
2890
+ formatUptime(startTime) {
2891
+ const diff = Math.floor(Date.now() / 1e3 - startTime);
2892
+ const hours = Math.floor(diff / 3600);
2893
+ const minutes = Math.floor(diff % 3600 / 60);
2894
+ if (hours > 0) return `${hours}h ${minutes}m`;
2895
+ return `${minutes}m`;
2896
+ }
2897
+ };
2898
+
2899
+ // src/config-manager.ts
2900
+ var import_crypto2 = require("crypto");
2901
+ var SYSTEM_CONFIG_ID = "_system_";
2902
+ function generateSalt() {
2903
+ return (0, import_crypto2.randomBytes)(16).toString("hex");
2904
+ }
2905
+ __name(generateSalt, "generateSalt");
2906
+ function hashPassword(password, salt) {
2907
+ return (0, import_crypto2.createHash)("sha256").update(password + salt).digest("hex");
2908
+ }
2909
+ __name(hashPassword, "hashPassword");
2910
+ function createPasswordHash(password) {
2911
+ const salt = generateSalt();
2912
+ const hash = hashPassword(password, salt);
2913
+ return `${salt}:${hash}`;
2914
+ }
2915
+ __name(createPasswordHash, "createPasswordHash");
2916
+ function verifyPasswordHash(password, storedHash) {
2917
+ const [salt, hash] = storedHash.split(":");
2918
+ if (!salt || !hash) return false;
2919
+ return hashPassword(password, salt) === hash;
2920
+ }
2921
+ __name(verifyPasswordHash, "verifyPasswordHash");
2922
+ var ConfigManager = class {
2923
+ constructor(ctx) {
2924
+ this.ctx = ctx;
2925
+ ctx.model.extend("onebot_multi_bots", {
2926
+ id: "unsigned",
2927
+ selfId: "string",
2928
+ token: "string",
2929
+ protocol: "string",
2930
+ endpoint: "string",
2931
+ path: "string",
2932
+ enabled: "boolean",
2933
+ createdAt: "timestamp",
2934
+ updatedAt: "timestamp"
2935
+ }, {
2936
+ autoInc: true,
2937
+ primary: "id",
2938
+ unique: [["selfId"]]
2939
+ });
2940
+ }
2941
+ static {
2942
+ __name(this, "ConfigManager");
2943
+ }
2944
+ // ==================== 密码相关方法(使用哈希存储) ====================
2945
+ /**
2946
+ * 获取存储的密码哈希
2947
+ */
2948
+ async getStoredPasswordHash() {
2949
+ const results = await this.ctx.database.get("onebot_multi_bots", { selfId: SYSTEM_CONFIG_ID });
2950
+ return results[0]?.token || null;
2951
+ }
2952
+ /**
2953
+ * 设置管理密码(哈希存储)
2954
+ */
2955
+ async setAdminPassword(password) {
2956
+ const hashedPassword = createPasswordHash(password);
2957
+ const existing = await this.ctx.database.get("onebot_multi_bots", { selfId: SYSTEM_CONFIG_ID });
2958
+ if (existing.length > 0) {
2959
+ await this.ctx.database.set("onebot_multi_bots", { selfId: SYSTEM_CONFIG_ID }, { token: hashedPassword, updatedAt: /* @__PURE__ */ new Date() });
2960
+ } else {
2961
+ await this.ctx.database.create("onebot_multi_bots", {
2962
+ selfId: SYSTEM_CONFIG_ID,
2963
+ token: hashedPassword,
2964
+ protocol: "ws-reverse",
2965
+ enabled: false,
2966
+ createdAt: /* @__PURE__ */ new Date(),
2967
+ updatedAt: /* @__PURE__ */ new Date()
2968
+ });
2969
+ }
2970
+ }
2971
+ /**
2972
+ * 检查是否已设置密码
2973
+ */
2974
+ async hasAdminPassword() {
2975
+ const hash = await this.getStoredPasswordHash();
2976
+ return !!hash;
2977
+ }
2978
+ /**
2979
+ * 验证管理密码
2980
+ */
2981
+ async verifyAdminPassword(password) {
2982
+ const storedHash = await this.getStoredPasswordHash();
2983
+ if (!storedHash) return false;
2984
+ if (!storedHash.includes(":")) {
2985
+ if (storedHash === password) {
2986
+ await this.setAdminPassword(password);
2987
+ return true;
2988
+ }
2989
+ return false;
2990
+ }
2991
+ return verifyPasswordHash(password, storedHash);
2992
+ }
2993
+ // ==================== Bot 配置相关方法 ====================
2994
+ /**
2995
+ * 获取所有 Bot 配置(排除系统配置)
2996
+ */
2997
+ async getAllConfigs() {
2998
+ const all = await this.ctx.database.get("onebot_multi_bots", {});
2999
+ return all.filter((r) => r.selfId !== SYSTEM_CONFIG_ID);
3000
+ }
3001
+ /**
3002
+ * 获取启用的 Bot 配置(排除系统配置)
3003
+ */
3004
+ async getEnabledConfigs() {
3005
+ const all = await this.ctx.database.get("onebot_multi_bots", { enabled: true });
3006
+ return all.filter((r) => r.selfId !== SYSTEM_CONFIG_ID);
3007
+ }
3008
+ /**
3009
+ * 获取单个 Bot 配置
3010
+ */
3011
+ async getConfig(selfId) {
3012
+ const results = await this.ctx.database.get("onebot_multi_bots", { selfId });
3013
+ return results[0];
3014
+ }
3015
+ /**
3016
+ * 创建 Bot 配置
3017
+ */
3018
+ async createConfig(config) {
3019
+ const now = /* @__PURE__ */ new Date();
3020
+ const record = await this.ctx.database.create("onebot_multi_bots", {
3021
+ ...config,
3022
+ createdAt: now,
3023
+ updatedAt: now
3024
+ });
3025
+ return record;
3026
+ }
3027
+ /**
3028
+ * 更新 Bot 配置
3029
+ */
3030
+ async updateConfig(selfId, updates) {
3031
+ const result = await this.ctx.database.set("onebot_multi_bots", { selfId }, {
3032
+ ...updates,
3033
+ updatedAt: /* @__PURE__ */ new Date()
3034
+ });
3035
+ return result.modified > 0;
3036
+ }
3037
+ /**
3038
+ * 删除 Bot 配置
3039
+ */
3040
+ async deleteConfig(selfId) {
3041
+ const result = await this.ctx.database.remove("onebot_multi_bots", { selfId });
3042
+ return result.removed > 0;
3043
+ }
3044
+ /**
3045
+ * 切换 Bot 启用状态
3046
+ */
3047
+ async toggleEnabled(selfId) {
3048
+ const config = await this.getConfig(selfId);
3049
+ if (!config) return false;
3050
+ return this.updateConfig(selfId, { enabled: !config.enabled });
3051
+ }
3052
+ /**
3053
+ * 将数据库配置转换为 BotConfig 格式
3054
+ */
3055
+ toBotConfig(record) {
3056
+ return {
3057
+ selfId: record.selfId,
3058
+ token: record.token,
3059
+ protocol: record.protocol,
3060
+ endpoint: record.endpoint,
3061
+ path: record.path
3062
+ };
3063
+ }
3064
+ };
3065
+
3066
+ // src/index.ts
3067
+ var Config = import_koishi11.Schema.intersect([
3068
+ // 全局连接设置
3069
+ import_koishi11.Schema.object({
3070
+ responseTimeout: import_koishi11.Schema.natural().role("time").default(import_koishi11.Time.minute).description("等待响应的时间(毫秒)。"),
3071
+ heartbeatInterval: import_koishi11.Schema.natural().role("ms").default(30 * import_koishi11.Time.second).description("心跳检测间隔(毫秒),设为 0 禁用。")
3072
+ }).description("连接设置"),
3073
+ // 全局重连设置(折叠)
3074
+ import_koishi11.Schema.object({
3075
+ retryTimes: import_koishi11.Schema.natural().description("初次连接时的最大重试次数。").default(6),
3076
+ retryInterval: import_koishi11.Schema.natural().role("ms").description("初次连接时的重试时间间隔。").default(5 * import_koishi11.Time.second),
3077
+ retryLazy: import_koishi11.Schema.natural().role("ms").description("连接关闭后的重试时间间隔。").default(import_koishi11.Time.minute)
3078
+ }).description("重连设置").collapse(),
3079
+ // 全局高级设置(折叠)
3080
+ import_koishi11.Schema.object({
3081
+ advanced: BaseBot2.AdvancedConfig
3082
+ }).description("高级设置").collapse(),
3083
+ // 展示面板设置(折叠)
3084
+ import_koishi11.Schema.object({
3085
+ panel: PanelConfig
3086
+ }).collapse()
3087
+ ]);
3088
+ var name = "adapter-onebot-multi";
3089
+ var inject = {
3090
+ required: ["server", "database"]
3091
+ };
3092
+ function apply(ctx, config) {
3093
+ const logger = ctx.logger("adapter-onebot-multi");
3094
+ const statusManager = new StatusManager(ctx);
3095
+ const configManager = new ConfigManager(ctx);
3096
+ const globalConfig = {
3097
+ responseTimeout: config.responseTimeout,
3098
+ heartbeatInterval: config.heartbeatInterval,
3099
+ retryTimes: config.retryTimes,
3100
+ retryInterval: config.retryInterval,
3101
+ retryLazy: config.retryLazy,
3102
+ advanced: config.advanced
3103
+ };
3104
+ ctx._onebotMultiGlobalConfig = globalConfig;
3105
+ if (config.panel?.enabled) {
3106
+ new StatusPanel(ctx, config.panel, statusManager, configManager);
3107
+ }
3108
+ const startBots = /* @__PURE__ */ __name(async () => {
3109
+ const dbConfigs = await configManager.getEnabledConfigs();
3110
+ if (dbConfigs.length > 0) {
3111
+ logger.info(`从数据库加载 ${dbConfigs.length} 个 Bot 配置`);
3112
+ for (const record of dbConfigs) {
3113
+ const botConfig = configManager.toBotConfig(record);
3114
+ startBot(ctx, botConfig, globalConfig, logger);
3115
+ }
3116
+ } else {
3117
+ logger.info("没有配置任何 Bot,请在管理面板中添加");
3118
+ }
3119
+ }, "startBots");
3120
+ ctx.on("ready", () => {
3121
+ startBots();
3122
+ });
3123
+ }
3124
+ __name(apply, "apply");
3125
+ function startBot(ctx, botConfig, globalConfig, logger) {
3126
+ const protocol = botConfig.protocol || "ws-reverse";
3127
+ const fullConfig = {
3128
+ ...globalConfig,
3129
+ ...botConfig,
3130
+ protocol
3131
+ };
3132
+ if (protocol === "ws") {
3133
+ if (!botConfig.endpoint) {
3134
+ logger.warn(`Bot ${botConfig.selfId} 使用 ws 协议但未配置 endpoint,跳过`);
3135
+ return;
3136
+ }
3137
+ logger.info(`创建 Bot: ${botConfig.selfId} (ws → ${botConfig.endpoint})`);
3138
+ } else {
3139
+ logger.info(`创建 Bot: ${botConfig.selfId} (ws-reverse ← ${botConfig.path || "/onebot"})`);
3140
+ }
3141
+ ctx.plugin(OneBotBot, fullConfig);
3142
+ }
3143
+ __name(startBot, "startBot");
3144
+ // Annotate the CommonJS export names for ESM import in node:
3145
+ 0 && (module.exports = {
3146
+ BaseBot,
3147
+ CQCode,
3148
+ Config,
3149
+ ConfigManager,
3150
+ HeartbeatMonitor,
3151
+ OneBot,
3152
+ OneBotBot,
3153
+ OneBotMessageEncoder,
3154
+ PRIVATE_PFX,
3155
+ PanelConfig,
3156
+ QQGuildBot,
3157
+ StatusManager,
3158
+ StatusPanel,
3159
+ WsClient,
3160
+ WsServer,
3161
+ accept,
3162
+ apply,
3163
+ inject,
3164
+ name
3165
+ });