koishi-plugin-dynamic-bot 0.0.5 → 0.0.7
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 +308 -37
- package/package.json +1 -1
- package/src/dbk/gateway.ts +26 -5
- package/src/dbk/get-target.ts +16 -2
- package/src/dbk/handlers.ts +111 -10
- package/src/dbk/list-targets.ts +23 -2
- package/src/dbk/recall.ts +10 -1
- package/src/dbk/send.ts +101 -15
- package/src/dbk/session.ts +6 -1
package/lib/index.js
CHANGED
|
@@ -2817,8 +2817,8 @@ function findFileDependencies(proto, reg) {
|
|
|
2817
2817
|
return dep;
|
|
2818
2818
|
});
|
|
2819
2819
|
}
|
|
2820
|
-
function findEnumSharedPrefix(
|
|
2821
|
-
const prefix = camelToSnakeCase(
|
|
2820
|
+
function findEnumSharedPrefix(enumName2, values) {
|
|
2821
|
+
const prefix = camelToSnakeCase(enumName2) + "_";
|
|
2822
2822
|
for (const value of values) {
|
|
2823
2823
|
if (!value.name.toLowerCase().startsWith(prefix)) {
|
|
2824
2824
|
return void 0;
|
|
@@ -4577,6 +4577,29 @@ function listValueFromJson(listValue, json, ctx) {
|
|
|
4577
4577
|
var file_dbk_v1_common = /* @__PURE__ */ fileDesc("ChNkYmsvdjEvY29tbW9uLnByb3RvEgZkYmsudjEiSAoGVGFyZ2V0EiAKBGtpbmQYASABKA4yEi5kYmsudjEuVGFyZ2V0S2luZBIKCgJpZBgCIAEoCRIQCghndWlsZF9pZBgDIAEoCSI7CghScGNFcnJvchIfCgRjb2RlGAEgASgOMhEuZGJrLnYxLkVycm9yQ29kZRIOCgZkZXRhaWwYAiABKAkqhwEKClRhcmdldEtpbmQSGwoXVEFSR0VUX0tJTkRfVU5TUEVDSUZJRUQQABIUChBUQVJHRVRfS0lORF9VU0VSEAESFQoRVEFSR0VUX0tJTkRfR1JPVVAQAhIXChNUQVJHRVRfS0lORF9DSEFOTkVMEAMSFgoSVEFSR0VUX0tJTkRfVEhSRUFEEAQqdAoJQm90U3RhdHVzEhoKFkJPVF9TVEFUVVNfVU5TUEVDSUZJRUQQABIUChBCT1RfU1RBVFVTX1JFQURZEAESGgoWQk9UX1NUQVRVU19VTkFWQUlMQUJMRRACEhkKFUJPVF9TVEFUVVNfQ09OTkVDVElORxADKocBCgpTZW5kU3RhdHVzEhsKF1NFTkRfU1RBVFVTX1VOU1BFQ0lGSUVEEAASEgoOU0VORF9TVEFUVVNfT0sQARIXChNTRU5EX1NUQVRVU19QQVJUSUFMEAISFwoTU0VORF9TVEFUVVNfVU5LTk9XThADEhYKElNFTkRfU1RBVFVTX0ZBSUxFRBAEKqwBCglFcnJvckNvZGUSGgoWRVJST1JfQ09ERV9VTlNQRUNJRklFRBAAEhsKF0VSUk9SX0NPREVfVU5BVVRIT1JJWkVEEAESFwoTRVJST1JfQ09ERV9QUk9UT0NPTBACEhgKFEVSUk9SX0NPREVfTk9UX0ZPVU5EEAMSGgoWRVJST1JfQ09ERV9VTlNVUFBPUlRFRBAEEhcKE0VSUk9SX0NPREVfSU5URVJOQUwQBSqFAQoNQm90Q2hhbmdlVHlwZRIfChtCT1RfQ0hBTkdFX1RZUEVfVU5TUEVDSUZJRUQQABIZChVCT1RfQ0hBTkdFX1RZUEVfQURERUQQARIbChdCT1RfQ0hBTkdFX1RZUEVfVVBEQVRFRBACEhsKF0JPVF9DSEFOR0VfVFlQRV9SRU1PVkVEEANiBnByb3RvMw");
|
|
4578
4578
|
var TargetSchema = /* @__PURE__ */ messageDesc(file_dbk_v1_common, 0);
|
|
4579
4579
|
var RpcErrorSchema = /* @__PURE__ */ messageDesc(file_dbk_v1_common, 1);
|
|
4580
|
+
var TargetKind = /* @__PURE__ */ ((TargetKind2) => {
|
|
4581
|
+
TargetKind2[TargetKind2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
4582
|
+
TargetKind2[TargetKind2["USER"] = 1] = "USER";
|
|
4583
|
+
TargetKind2[TargetKind2["GROUP"] = 2] = "GROUP";
|
|
4584
|
+
TargetKind2[TargetKind2["CHANNEL"] = 3] = "CHANNEL";
|
|
4585
|
+
TargetKind2[TargetKind2["THREAD"] = 4] = "THREAD";
|
|
4586
|
+
return TargetKind2;
|
|
4587
|
+
})(TargetKind || {});
|
|
4588
|
+
var BotStatus = /* @__PURE__ */ ((BotStatus2) => {
|
|
4589
|
+
BotStatus2[BotStatus2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
4590
|
+
BotStatus2[BotStatus2["READY"] = 1] = "READY";
|
|
4591
|
+
BotStatus2[BotStatus2["UNAVAILABLE"] = 2] = "UNAVAILABLE";
|
|
4592
|
+
BotStatus2[BotStatus2["CONNECTING"] = 3] = "CONNECTING";
|
|
4593
|
+
return BotStatus2;
|
|
4594
|
+
})(BotStatus || {});
|
|
4595
|
+
var SendStatus = /* @__PURE__ */ ((SendStatus2) => {
|
|
4596
|
+
SendStatus2[SendStatus2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
4597
|
+
SendStatus2[SendStatus2["OK"] = 1] = "OK";
|
|
4598
|
+
SendStatus2[SendStatus2["PARTIAL"] = 2] = "PARTIAL";
|
|
4599
|
+
SendStatus2[SendStatus2["UNKNOWN"] = 3] = "UNKNOWN";
|
|
4600
|
+
SendStatus2[SendStatus2["FAILED"] = 4] = "FAILED";
|
|
4601
|
+
return SendStatus2;
|
|
4602
|
+
})(SendStatus || {});
|
|
4580
4603
|
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
4581
4604
|
ErrorCode2[ErrorCode2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
4582
4605
|
ErrorCode2[ErrorCode2["UNAUTHORIZED"] = 1] = "UNAUTHORIZED";
|
|
@@ -4586,6 +4609,13 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
|
4586
4609
|
ErrorCode2[ErrorCode2["INTERNAL"] = 5] = "INTERNAL";
|
|
4587
4610
|
return ErrorCode2;
|
|
4588
4611
|
})(ErrorCode || {});
|
|
4612
|
+
var BotChangeType = /* @__PURE__ */ ((BotChangeType2) => {
|
|
4613
|
+
BotChangeType2[BotChangeType2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
4614
|
+
BotChangeType2[BotChangeType2["ADDED"] = 1] = "ADDED";
|
|
4615
|
+
BotChangeType2[BotChangeType2["UPDATED"] = 2] = "UPDATED";
|
|
4616
|
+
BotChangeType2[BotChangeType2["REMOVED"] = 3] = "REMOVED";
|
|
4617
|
+
return BotChangeType2;
|
|
4618
|
+
})(BotChangeType || {});
|
|
4589
4619
|
|
|
4590
4620
|
// src/gen/dbk/v1/rpc_pb.ts
|
|
4591
4621
|
var file_dbk_v1_rpc = /* @__PURE__ */ fileDesc("ChBkYmsvdjEvcnBjLnByb3RvEgZkYmsudjEiTAoMSGVsbG9SZXF1ZXN0Eg0KBXRva2VuGAEgASgJEhMKC2FwcF92ZXJzaW9uGAIgASgJEhgKEHByb3RvY29sX3ZlcnNpb24YAyABKAkiXQoNSGVsbG9SZXNwb25zZRIYChBwcm90b2NvbF92ZXJzaW9uGAEgASgJEhcKD2dhdGV3YXlfdmVyc2lvbhgCIAEoCRIZCgRib3RzGAMgAygLMgsuZGJrLnYxLkJvdCIRCg9MaXN0Qm90c1JlcXVlc3QiLQoQTGlzdEJvdHNSZXNwb25zZRIZCgRib3RzGAEgAygLMgsuZGJrLnYxLkJvdCKMAQoDQm90Eg8KB2JvdF9rZXkYASABKAkSEAoIcGxhdGZvcm0YAiABKAkSDwoHc2VsZl9pZBgDIAEoCRIMCgRuYW1lGAQgASgJEg4KBmF2YXRhchgFIAEoCRIhCgZzdGF0dXMYBiABKA4yES5kYmsudjEuQm90U3RhdHVzEhAKCGZlYXR1cmVzGAcgAygJIlAKD0JvdENoYW5nZWRFdmVudBIjCgR0eXBlGAEgASgOMhUuZGJrLnYxLkJvdENoYW5nZVR5cGUSGAoDYm90GAIgASgLMgsuZGJrLnYxLkJvdCJHChJMaXN0VGFyZ2V0c1JlcXVlc3QSDwoHYm90X2tleRgBIAEoCRIgCgRraW5kGAIgASgOMhIuZGJrLnYxLlRhcmdldEtpbmQiTgoTTGlzdFRhcmdldHNSZXNwb25zZRIjCgd0YXJnZXRzGAEgAygLMhIuZGJrLnYxLlRhcmdldEluZm8SEgoKaW5jb21wbGV0ZRgCIAEoCCJDChBHZXRUYXJnZXRSZXF1ZXN0Eg8KB2JvdF9rZXkYASABKAkSHgoGdGFyZ2V0GAIgASgLMg4uZGJrLnYxLlRhcmdldCJLChFHZXRUYXJnZXRSZXNwb25zZRIiCgZ0YXJnZXQYASABKAsyEi5kYmsudjEuVGFyZ2V0SW5mbxISCgp1bnJlc29sdmVkGAIgASgIIoIBCgpUYXJnZXRJbmZvEh4KBnRhcmdldBgBIAEoCzIOLmRiay52MS5UYXJnZXQSDAoEbmFtZRgCIAEoCRIQCghndWlsZF9pZBgDIAEoCRISCgpndWlsZF9uYW1lGAQgASgJEhAKCGJvdF9rZXlzGAUgAygJEg4KBmF2YXRhchgGIAEoCSJ7CgpTZW5kUGFyYW1zEg8KB2JvdF9rZXkYASABKAkSHgoGdGFyZ2V0GAIgASgLMg4uZGJrLnYxLlRhcmdldBIbChNyZXBseV90b19tZXNzYWdlX2lkGAMgASgJEh8KBXVuaXRzGAQgAygLMhAuZGJrLnYxLlNlbmRVbml0ImAKCFNlbmRVbml0EiQKBm5vcm1hbBgBIAEoCzISLmRiay52MS5Ob3JtYWxVbml0SAASJgoHZm9yd2FyZBgCIAEoCzITLmRiay52MS5Gb3J3YXJkVW5pdEgAQgYKBGJvZHkiLwoKTm9ybWFsVW5pdBIhCghzZWdtZW50cxgBIAMoCzIPLmRiay52MS5TZWdtZW50IjEKC0ZvcndhcmRVbml0EiIKBW5vZGVzGAEgAygLMhMuZGJrLnYxLkZvcndhcmROb2RlIn0KC0ZvcndhcmROb2RlEhEKCXNlbmRlcl9pZBgBIAEoCRITCgtzZW5kZXJfbmFtZRgCIAEoCRIVCg1zZW5kZXJfYXZhdGFyGAMgASgJEgwKBHRpbWUYBCABKAMSIQoIc2VnbWVudHMYBSADKAsyDy5kYmsudjEuU2VnbWVudCL/AgoHU2VnbWVudBIjCgR0ZXh0GAEgASgLMhMuZGJrLnYxLlRleHRTZWdtZW50SAASJQoFaW1hZ2UYAiABKAsyFC5kYmsudjEuSW1hZ2VTZWdtZW50SAASJQoFdmlkZW8YAyABKAsyFC5kYmsudjEuVmlkZW9TZWdtZW50SAASJQoFYXVkaW8YBCABKAsyFC5kYmsudjEuQXVkaW9TZWdtZW50SAASKQoHbWVudGlvbhgFIAEoCzIWLmRiay52MS5NZW50aW9uU2VnbWVudEgAEjAKC21lbnRpb25fYWxsGAYgASgLMhkuZGJrLnYxLk1lbnRpb25BbGxTZWdtZW50SAASJQoFcmVwbHkYByABKAsyFC5kYmsudjEuUmVwbHlTZWdtZW50SAASIwoEbGluaxgIIAEoCzITLmRiay52MS5MaW5rU2VnbWVudEgAEikKB3Vua25vd24YCSABKAsyFi5kYmsudjEuVW5rbm93blNlZ21lbnRIAEIGCgRib2R5IhsKC1RleHRTZWdtZW50EgwKBHRleHQYASABKAkiNgoMSW1hZ2VTZWdtZW50EgsKA3VyaRgBIAEoCRIMCgRmaWxlGAIgASgJEgsKA2FsdBgDIAEoCSI2CgxWaWRlb1NlZ21lbnQSCwoDdXJpGAEgASgJEgwKBGZpbGUYAiABKAkSCwoDYWx0GAMgASgJIjYKDEF1ZGlvU2VnbWVudBILCgN1cmkYASABKAkSDAoEZmlsZRgCIAEoCRILCgNhbHQYAyABKAkiHAoOTWVudGlvblNlZ21lbnQSCgoCaWQYASABKAkiEwoRTWVudGlvbkFsbFNlZ21lbnQiIgoMUmVwbHlTZWdtZW50EhIKCm1lc3NhZ2VfaWQYASABKAkiKQoLTGlua1NlZ21lbnQSCwoDdXJsGAEgASgJEg0KBXRpdGxlGAIgASgJIisKDlVua25vd25TZWdtZW50EgwKBHR5cGUYASABKAkSCwoDcmF3GAIgASgJInoKClNlbmRSZXN1bHQSIgoGc3RhdHVzGAEgASgOMhIuZGJrLnYxLlNlbmRTdGF0dXMSDgoGcmVhc29uGAIgASgJEhEKCXJldHJ5YWJsZRgDIAEoCBIlCghyZWNlaXB0cxgEIAMoCzITLmRiay52MS5TZW5kUmVjZWlwdCI1CgtTZW5kUmVjZWlwdBISCgptZXNzYWdlX2lkGAEgASgJEhIKCnJlY2FsbGFibGUYAiABKAgiUwoMUmVjYWxsUGFyYW1zEg8KB2JvdF9rZXkYASABKAkSHgoGdGFyZ2V0GAIgASgLMg4uZGJrLnYxLlRhcmdldBISCgptZXNzYWdlX2lkGAMgASgJIlUKDFJlY2FsbFJlc3VsdBIiCgZzdGF0dXMYASABKA4yEi5kYmsudjEuU2VuZFN0YXR1cxIOCgZyZWFzb24YAiABKAkSEQoJcmV0cnlhYmxlGAMgASgIIvsBCg9JbmNvbWluZ01lc3NhZ2USDwoHYm90X2tleRgBIAEoCRIQCghwbGF0Zm9ybRgCIAEoCRIeCgZ0YXJnZXQYAyABKAsyDi5kYmsudjEuVGFyZ2V0EhEKCXNlbmRlcl9pZBgEIAEoCRISCgptZXNzYWdlX2lkGAUgASgJEhEKCXRpbWVzdGFtcBgGIAEoAxIMCgR0ZXh0GAcgASgJEhsKE3JlcGx5X3RvX21lc3NhZ2VfaWQYCCABKAkSEAoIbWVudGlvbnMYCSADKAkSIQoIc2VnbWVudHMYCiADKAsyDy5kYmsudjEuU2VnbWVudBILCgNyYXcYCyABKAliBnByb3RvMw", [file_dbk_v1_common]);
|
|
@@ -5001,8 +5031,12 @@ var DbkGatewaySession = class {
|
|
|
5001
5031
|
this.fail("DBK frame could not be decoded");
|
|
5002
5032
|
}
|
|
5003
5033
|
emit(method, event) {
|
|
5004
|
-
if (!this.isHandshook)
|
|
5034
|
+
if (!this.isHandshook) {
|
|
5035
|
+
this.options.logger.debug("drop EVENT before handshake: method=%s", method);
|
|
5036
|
+
return;
|
|
5037
|
+
}
|
|
5005
5038
|
this.seq += 1n;
|
|
5039
|
+
this.options.logger.debug("DBK EVENT %s seq=%s", method, this.seq.toString());
|
|
5006
5040
|
this.enqueueFrame(
|
|
5007
5041
|
createFrame({
|
|
5008
5042
|
op: 4 /* EVENT */,
|
|
@@ -5056,6 +5090,7 @@ var DbkGatewaySession = class {
|
|
|
5056
5090
|
this.replyError(frame.id, 4 /* UNSUPPORTED */, `unknown method: ${frame.method}`);
|
|
5057
5091
|
return;
|
|
5058
5092
|
}
|
|
5093
|
+
this.options.logger.debug("DBK CALL %s id=%s", frame.method, frame.id);
|
|
5059
5094
|
try {
|
|
5060
5095
|
const payload = await this.dispatchCall(frame.method, frame.payload);
|
|
5061
5096
|
this.enqueueFrame(
|
|
@@ -5217,11 +5252,19 @@ var DbkGateway = class {
|
|
|
5217
5252
|
return this.session?.isHandshook ?? false;
|
|
5218
5253
|
}
|
|
5219
5254
|
emit(method, event) {
|
|
5220
|
-
this.session
|
|
5255
|
+
if (!this.session) {
|
|
5256
|
+
this.ctx.logger.debug("drop EVENT without session: method=%s", method);
|
|
5257
|
+
return;
|
|
5258
|
+
}
|
|
5259
|
+
this.session.emit(method, event);
|
|
5221
5260
|
}
|
|
5222
5261
|
startForward() {
|
|
5223
5262
|
const path = normalizePath(this.config.path);
|
|
5224
|
-
this.ctx.logger.info(
|
|
5263
|
+
this.ctx.logger.info(
|
|
5264
|
+
"DBK forward WebSocket mounted on Koishi HTTP at %s encoding=%s",
|
|
5265
|
+
path,
|
|
5266
|
+
this.config.encoding
|
|
5267
|
+
);
|
|
5225
5268
|
this.ctx.server.ws(path, (socket) => {
|
|
5226
5269
|
this.attach("replaced", socket);
|
|
5227
5270
|
});
|
|
@@ -5237,11 +5280,16 @@ var DbkGateway = class {
|
|
|
5237
5280
|
this.session = void 0;
|
|
5238
5281
|
this.socket?.close(1e3, "plugin stop");
|
|
5239
5282
|
this.socket = void 0;
|
|
5283
|
+
this.ctx.logger.debug("DBK gateway stopped");
|
|
5240
5284
|
}
|
|
5241
5285
|
connectReverse() {
|
|
5242
5286
|
if (this.closing) return;
|
|
5243
5287
|
const url = `ws://${this.config.host}:${this.config.port}`;
|
|
5244
|
-
this.ctx.logger.info(
|
|
5288
|
+
this.ctx.logger.info(
|
|
5289
|
+
"DBK reverse WebSocket connecting to %s encoding=%s",
|
|
5290
|
+
url,
|
|
5291
|
+
this.config.encoding
|
|
5292
|
+
);
|
|
5245
5293
|
const socket = this.ctx.http.ws(url);
|
|
5246
5294
|
const onOpen = () => {
|
|
5247
5295
|
this.reconnectAttempts = 0;
|
|
@@ -5279,6 +5327,7 @@ var DbkGateway = class {
|
|
|
5279
5327
|
});
|
|
5280
5328
|
this.session = session;
|
|
5281
5329
|
attachSocket(socket, session, this.config.encoding);
|
|
5330
|
+
this.ctx.logger.debug("DBK connection attached encoding=%s", this.config.encoding);
|
|
5282
5331
|
}
|
|
5283
5332
|
onReverseClosed() {
|
|
5284
5333
|
this.session?.close("ws close");
|
|
@@ -5302,9 +5351,16 @@ var DbkGateway = class {
|
|
|
5302
5351
|
function watchBots(ctx, gateway) {
|
|
5303
5352
|
const emit = (type, bot) => {
|
|
5304
5353
|
if (bot.hidden && type !== 3 /* REMOVED */) return;
|
|
5354
|
+
const mapped = toDbkBot(bot);
|
|
5355
|
+
ctx.logger.debug(
|
|
5356
|
+
"bot.changed: %s %s status=%s",
|
|
5357
|
+
BotChangeType[type] ?? type,
|
|
5358
|
+
mapped.botKey || "-",
|
|
5359
|
+
BotStatus[mapped.status] ?? mapped.status
|
|
5360
|
+
);
|
|
5305
5361
|
gateway.emit(DbkEvent.BOT_CHANGED, create(BotChangedEventSchema, {
|
|
5306
5362
|
type,
|
|
5307
|
-
bot:
|
|
5363
|
+
bot: mapped
|
|
5308
5364
|
}));
|
|
5309
5365
|
};
|
|
5310
5366
|
ctx.on("bot-added", (bot) => emit(1 /* ADDED */, bot));
|
|
@@ -5383,12 +5439,26 @@ async function getTarget(ctx, request) {
|
|
|
5383
5439
|
throw new DbkRpcError(3 /* NOT_FOUND */, `bot not found: ${botKey}`);
|
|
5384
5440
|
}
|
|
5385
5441
|
const info = await resolveOnBot(bot, target);
|
|
5386
|
-
|
|
5442
|
+
if (info) {
|
|
5443
|
+
ctx.logger.debug("targets.get: resolved on %s kind=%s id=%s", botKey, TargetKind[info.target?.kind ?? 0], info.target?.id);
|
|
5444
|
+
return resolved(info);
|
|
5445
|
+
}
|
|
5446
|
+
ctx.logger.debug("targets.get: unresolved on %s id=%s", botKey, target?.id.trim() || "-");
|
|
5447
|
+
return unresolved(target);
|
|
5387
5448
|
}
|
|
5388
5449
|
for (const bot of listSearchableBots(ctx)) {
|
|
5389
5450
|
const info = await resolveOnBot(bot, target);
|
|
5390
|
-
if (info)
|
|
5451
|
+
if (info) {
|
|
5452
|
+
ctx.logger.debug(
|
|
5453
|
+
"targets.get: resolved on %s kind=%s id=%s",
|
|
5454
|
+
botKeyOf(bot),
|
|
5455
|
+
TargetKind[info.target?.kind ?? 0],
|
|
5456
|
+
info.target?.id
|
|
5457
|
+
);
|
|
5458
|
+
return resolved(info);
|
|
5459
|
+
}
|
|
5391
5460
|
}
|
|
5461
|
+
ctx.logger.debug("targets.get: unresolved id=%s", target?.id.trim() || "-");
|
|
5392
5462
|
return unresolved(target);
|
|
5393
5463
|
}
|
|
5394
5464
|
function findBot(ctx, botKey) {
|
|
@@ -5567,7 +5637,10 @@ async function listTargets(ctx, request) {
|
|
|
5567
5637
|
const merged = /* @__PURE__ */ new Map();
|
|
5568
5638
|
let incomplete = false;
|
|
5569
5639
|
for (const bot of bots) {
|
|
5570
|
-
if (isQq(bot))
|
|
5640
|
+
if (isQq(bot)) {
|
|
5641
|
+
ctx.logger?.debug("targets.list: skip qq bot %s", botKeyOf2(bot));
|
|
5642
|
+
continue;
|
|
5643
|
+
}
|
|
5571
5644
|
const listed = await listTargetsForBot(ctx, bot, kindFilter);
|
|
5572
5645
|
incomplete = incomplete || listed.incomplete;
|
|
5573
5646
|
for (const target of listed.targets) {
|
|
@@ -5616,6 +5689,12 @@ async function listTargetsForBot(ctx, bot, kindFilter) {
|
|
|
5616
5689
|
const nested = hasNestedChannels(bot);
|
|
5617
5690
|
const supported = botTargetKinds(bot);
|
|
5618
5691
|
if (kindFilter !== 0 /* UNSPECIFIED */ && supported.length > 0 && !supported.includes(kindFilter)) {
|
|
5692
|
+
ctx.logger?.debug(
|
|
5693
|
+
"targets.list: %s skip kind=%s supported=%s",
|
|
5694
|
+
botKey,
|
|
5695
|
+
TargetKind[kindFilter] ?? kindFilter,
|
|
5696
|
+
supported.map((kind) => TargetKind[kind] ?? kind).join(",")
|
|
5697
|
+
);
|
|
5619
5698
|
return { targets, incomplete: false };
|
|
5620
5699
|
}
|
|
5621
5700
|
let incomplete = hasGuildSurface(bot) && !nested;
|
|
@@ -5625,7 +5704,12 @@ async function listTargetsForBot(ctx, bot, kindFilter) {
|
|
|
5625
5704
|
const guilds = await paginate((next) => callBotList(bot, "getGuildList", next));
|
|
5626
5705
|
if (guilds.unsupported || guilds.failed || guilds.truncated) incomplete = true;
|
|
5627
5706
|
if (guilds.failed && !guilds.unsupported) {
|
|
5628
|
-
ctx.logger?.debug(
|
|
5707
|
+
ctx.logger?.debug(
|
|
5708
|
+
"targets.list: %s getGuildList failed items=%d truncated=%s",
|
|
5709
|
+
botKey,
|
|
5710
|
+
guilds.items.length,
|
|
5711
|
+
guilds.truncated
|
|
5712
|
+
);
|
|
5629
5713
|
}
|
|
5630
5714
|
let channelMode = nested ? "unknown" : "flat";
|
|
5631
5715
|
for (const guild of guilds.items) {
|
|
@@ -5694,6 +5778,13 @@ async function listTargetsForBot(ctx, bot, kindFilter) {
|
|
|
5694
5778
|
incomplete = true;
|
|
5695
5779
|
}
|
|
5696
5780
|
}
|
|
5781
|
+
ctx.logger?.debug(
|
|
5782
|
+
"targets.list: %s listed=%d incomplete=%s nested=%s",
|
|
5783
|
+
botKey,
|
|
5784
|
+
targets.length,
|
|
5785
|
+
incomplete,
|
|
5786
|
+
nested
|
|
5787
|
+
);
|
|
5697
5788
|
return { targets, incomplete };
|
|
5698
5789
|
}
|
|
5699
5790
|
function classifyGuildChannels(channels, guildId, guildName, guildAvatar, botKey) {
|
|
@@ -5938,15 +6029,24 @@ async function recallMessage(ctx, params) {
|
|
|
5938
6029
|
if (!channelId) {
|
|
5939
6030
|
return failed("target id is empty", false);
|
|
5940
6031
|
}
|
|
6032
|
+
ctx.logger.debug("message.recall: deleteMessage channel=%s message=%s", channelId, messageId);
|
|
5941
6033
|
try {
|
|
5942
6034
|
await withTimeout(Promise.resolve(bot.deleteMessage(channelId, messageId)), RECALL_TIMEOUT_MS);
|
|
6035
|
+
ctx.logger.debug("message.recall: adapter ok");
|
|
5943
6036
|
return create(RecallResultSchema, {
|
|
5944
6037
|
status: 1 /* OK */,
|
|
5945
6038
|
reason: "",
|
|
5946
6039
|
retryable: false
|
|
5947
6040
|
});
|
|
5948
6041
|
} catch (error) {
|
|
5949
|
-
|
|
6042
|
+
const result = classifyRecallError(error);
|
|
6043
|
+
ctx.logger.debug(
|
|
6044
|
+
"message.recall: adapter error status=%s retryable=%s: %s",
|
|
6045
|
+
SendStatus[result.status] ?? result.status,
|
|
6046
|
+
result.retryable,
|
|
6047
|
+
result.reason || errorMessage(error) || "message.recall failed"
|
|
6048
|
+
);
|
|
6049
|
+
return result;
|
|
5950
6050
|
}
|
|
5951
6051
|
}
|
|
5952
6052
|
function findBot3(ctx, botKey) {
|
|
@@ -6128,20 +6228,29 @@ async function sendMessage(ctx, request) {
|
|
|
6128
6228
|
const sendElements = async (elements) => {
|
|
6129
6229
|
if (aborted) return;
|
|
6130
6230
|
if (elements.length === 0) {
|
|
6231
|
+
ctx.logger.debug("message.send: no sendable segments");
|
|
6131
6232
|
failuresRetryable = false;
|
|
6132
6233
|
failures.push("no sendable segments");
|
|
6133
6234
|
return;
|
|
6134
6235
|
}
|
|
6135
6236
|
const outgoing = withQuote(elements, nextQuoteId());
|
|
6237
|
+
ctx.logger.debug(
|
|
6238
|
+
"message.send: bot.sendMessage channel=%s guild=%s elements=%d",
|
|
6239
|
+
channelId,
|
|
6240
|
+
guildId || "-",
|
|
6241
|
+
outgoing.length
|
|
6242
|
+
);
|
|
6136
6243
|
try {
|
|
6137
6244
|
const ids = await bot.sendMessage(channelId, outgoing, guildId);
|
|
6138
6245
|
if (aborted) return;
|
|
6139
6246
|
quoteUsed = true;
|
|
6140
6247
|
const kept = normalizeMessageIds(ids);
|
|
6141
6248
|
if (kept.length === 0) {
|
|
6249
|
+
ctx.logger.debug("message.send: adapter returned no message id");
|
|
6142
6250
|
unknownReasons.push("send returned no message id");
|
|
6143
6251
|
return;
|
|
6144
6252
|
}
|
|
6253
|
+
ctx.logger.debug("message.send: adapter ids=%s", kept.join(","));
|
|
6145
6254
|
for (const messageId of kept) {
|
|
6146
6255
|
receipts.push(create(SendReceiptSchema, { messageId, recallable }));
|
|
6147
6256
|
}
|
|
@@ -6149,10 +6258,17 @@ async function sendMessage(ctx, request) {
|
|
|
6149
6258
|
if (aborted) return;
|
|
6150
6259
|
if (isTimeoutError(error)) {
|
|
6151
6260
|
quoteUsed = true;
|
|
6152
|
-
|
|
6261
|
+
const reason = errorMessage2(error) || "send timed out";
|
|
6262
|
+
ctx.logger.debug("message.send: adapter timeout: %s", reason);
|
|
6263
|
+
unknownReasons.push(reason);
|
|
6153
6264
|
return;
|
|
6154
6265
|
}
|
|
6155
6266
|
const retryable = isRetryableSendError(error);
|
|
6267
|
+
ctx.logger.debug(
|
|
6268
|
+
"message.send: adapter error retryable=%s: %s",
|
|
6269
|
+
retryable,
|
|
6270
|
+
errorMessage2(error) || "send failed"
|
|
6271
|
+
);
|
|
6156
6272
|
failuresRetryable = failuresRetryable && retryable;
|
|
6157
6273
|
failures.push(errorMessage2(error) || "send failed");
|
|
6158
6274
|
}
|
|
@@ -6165,9 +6281,12 @@ async function sendMessage(ctx, request) {
|
|
|
6165
6281
|
} catch (error) {
|
|
6166
6282
|
if (aborted) return;
|
|
6167
6283
|
if (isTimeoutError(error)) {
|
|
6168
|
-
|
|
6284
|
+
const reason = errorMessage2(error) || "media fetch timed out";
|
|
6285
|
+
ctx.logger.debug("message.send: media timeout: %s", reason);
|
|
6286
|
+
unknownReasons.push(reason);
|
|
6169
6287
|
return;
|
|
6170
6288
|
}
|
|
6289
|
+
ctx.logger.debug("message.send: media error: %s", errorMessage2(error) || "media fetch failed");
|
|
6171
6290
|
failuresRetryable = failuresRetryable && isRetryableSendError(error);
|
|
6172
6291
|
failures.push(errorMessage2(error) || "media fetch failed");
|
|
6173
6292
|
return;
|
|
@@ -6288,30 +6407,85 @@ async function forwardNodeElements(ctx, node, options) {
|
|
|
6288
6407
|
`), ...body];
|
|
6289
6408
|
}
|
|
6290
6409
|
async function fetchMediaElement(ctx, kind, uri) {
|
|
6291
|
-
|
|
6292
|
-
if (typeof http?.file !== "function") {
|
|
6293
|
-
throw mediaFetchError(kind, uri, new Error("Koishi http service is required to send media"));
|
|
6294
|
-
}
|
|
6295
|
-
let file;
|
|
6296
|
-
try {
|
|
6297
|
-
file = await http.file(uri);
|
|
6298
|
-
} catch (error) {
|
|
6299
|
-
throw mediaFetchError(kind, uri, error);
|
|
6300
|
-
}
|
|
6410
|
+
ctx.logger.debug("message.send: fetch %s %s", kind, displayUri(uri));
|
|
6301
6411
|
let buffer;
|
|
6412
|
+
let mime;
|
|
6302
6413
|
try {
|
|
6303
|
-
|
|
6414
|
+
const inline = decodeInlineMedia(uri, kind);
|
|
6415
|
+
if (inline) {
|
|
6416
|
+
buffer = inline.buffer;
|
|
6417
|
+
mime = inline.mime;
|
|
6418
|
+
} else {
|
|
6419
|
+
const fetched = await fetchRemoteMedia(ctx, kind, uri);
|
|
6420
|
+
buffer = fetched.buffer;
|
|
6421
|
+
mime = fetched.mime;
|
|
6422
|
+
}
|
|
6304
6423
|
} catch (error) {
|
|
6305
6424
|
throw mediaFetchError(kind, uri, error);
|
|
6306
6425
|
}
|
|
6307
6426
|
if (buffer.length === 0) {
|
|
6308
6427
|
throw mediaFetchError(kind, uri, new Error("empty body"));
|
|
6309
6428
|
}
|
|
6310
|
-
|
|
6429
|
+
ctx.logger.debug("message.send: fetched %s bytes=%d mime=%s", kind, buffer.length, mime);
|
|
6311
6430
|
if (kind === "image") return import_koishi.h.image(buffer, mime);
|
|
6312
6431
|
if (kind === "video") return import_koishi.h.video(buffer, mime);
|
|
6313
6432
|
return import_koishi.h.audio(buffer, mime);
|
|
6314
6433
|
}
|
|
6434
|
+
async function fetchRemoteMedia(ctx, kind, uri) {
|
|
6435
|
+
const http = ctx.http;
|
|
6436
|
+
if (typeof http?.file !== "function") {
|
|
6437
|
+
throw new Error("Koishi http service is required to send media");
|
|
6438
|
+
}
|
|
6439
|
+
const file = await http.file(uri);
|
|
6440
|
+
const buffer = mediaBytes(file.data);
|
|
6441
|
+
const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
|
|
6442
|
+
return { buffer, mime };
|
|
6443
|
+
}
|
|
6444
|
+
function decodeInlineMedia(uri, kind) {
|
|
6445
|
+
if (/^data:/i.test(uri)) {
|
|
6446
|
+
const parsed = decodeDataUri(uri);
|
|
6447
|
+
if (!parsed) throw new Error("invalid data URI");
|
|
6448
|
+
return { buffer: parsed.buffer, mime: parsed.mime || sniffMime(parsed.buffer, kind) };
|
|
6449
|
+
}
|
|
6450
|
+
const base64 = /^base64:\/\//i.exec(uri);
|
|
6451
|
+
if (base64) {
|
|
6452
|
+
const buffer = decodeBase64Payload(uri.slice(base64[0].length));
|
|
6453
|
+
return { buffer, mime: sniffMime(buffer, kind) };
|
|
6454
|
+
}
|
|
6455
|
+
return void 0;
|
|
6456
|
+
}
|
|
6457
|
+
function decodeDataUri(uri) {
|
|
6458
|
+
const comma = uri.indexOf(",");
|
|
6459
|
+
if (comma < 5) return void 0;
|
|
6460
|
+
const parts = uri.slice(5, comma).split(";").map((part) => part.trim()).filter(Boolean);
|
|
6461
|
+
const isBase64 = parts.some((part) => part.toLowerCase() === "base64");
|
|
6462
|
+
const mime = parts.find((part) => part.includes("/")) ?? "";
|
|
6463
|
+
const payload = uri.slice(comma + 1);
|
|
6464
|
+
const buffer = isBase64 ? decodeBase64Payload(payload) : Buffer.from(decodeURIComponent(payload.replace(/\+/g, " ")));
|
|
6465
|
+
return { buffer, mime };
|
|
6466
|
+
}
|
|
6467
|
+
function decodeBase64Payload(payload) {
|
|
6468
|
+
const cleaned = payload.replace(/\s/g, "").replace(/-/g, "+").replace(/_/g, "/");
|
|
6469
|
+
if (!cleaned) throw new Error("empty base64 payload");
|
|
6470
|
+
const buffer = Buffer.from(cleaned, "base64");
|
|
6471
|
+
if (buffer.length === 0) throw new Error("invalid base64 payload");
|
|
6472
|
+
return buffer;
|
|
6473
|
+
}
|
|
6474
|
+
function sniffMime(buffer, kind) {
|
|
6475
|
+
if (buffer.length >= 8 && buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71) {
|
|
6476
|
+
return "image/png";
|
|
6477
|
+
}
|
|
6478
|
+
if (buffer.length >= 3 && buffer[0] === 255 && buffer[1] === 216 && buffer[2] === 255) {
|
|
6479
|
+
return "image/jpeg";
|
|
6480
|
+
}
|
|
6481
|
+
if (buffer.length >= 6 && buffer.toString("ascii", 0, 6).startsWith("GIF8")) {
|
|
6482
|
+
return "image/gif";
|
|
6483
|
+
}
|
|
6484
|
+
if (buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP") {
|
|
6485
|
+
return "image/webp";
|
|
6486
|
+
}
|
|
6487
|
+
return MEDIA_FALLBACK_TYPE[kind];
|
|
6488
|
+
}
|
|
6315
6489
|
function mediaBytes(data) {
|
|
6316
6490
|
if (Buffer.isBuffer(data)) return data;
|
|
6317
6491
|
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
@@ -6326,7 +6500,8 @@ function mediaFetchError(kind, uri, cause) {
|
|
|
6326
6500
|
return error;
|
|
6327
6501
|
}
|
|
6328
6502
|
function displayUri(uri) {
|
|
6329
|
-
if (
|
|
6503
|
+
if (/^data:/i.test(uri)) return "data:...";
|
|
6504
|
+
if (/^base64:\/\//i.test(uri)) return "base64://...";
|
|
6330
6505
|
return uri.length > 200 ? `${uri.slice(0, 200)}...` : uri;
|
|
6331
6506
|
}
|
|
6332
6507
|
function withQuote(elements, quoteId) {
|
|
@@ -6487,18 +6662,114 @@ function withTimeout2(promise, ms) {
|
|
|
6487
6662
|
function createGatewayHandlers(ctx) {
|
|
6488
6663
|
const snapshot = () => create(ListBotsResponseSchema, { bots: listBots(ctx) });
|
|
6489
6664
|
return {
|
|
6490
|
-
[DbkMethod.SESSION_HELLO]: () =>
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6665
|
+
[DbkMethod.SESSION_HELLO]: (request) => {
|
|
6666
|
+
const bots = snapshot().bots;
|
|
6667
|
+
ctx.logger.debug(
|
|
6668
|
+
"session.hello: app=%s protocol=%s gateway=%s bots=%d keys=%s",
|
|
6669
|
+
request.appVersion || "-",
|
|
6670
|
+
request.protocolVersion || "-",
|
|
6671
|
+
GATEWAY_VERSION,
|
|
6672
|
+
bots.length,
|
|
6673
|
+
bots.map((bot) => bot.botKey).join(",") || "-"
|
|
6674
|
+
);
|
|
6675
|
+
return create(HelloResponseSchema, {
|
|
6676
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
6677
|
+
gatewayVersion: GATEWAY_VERSION,
|
|
6678
|
+
bots
|
|
6679
|
+
});
|
|
6680
|
+
},
|
|
6681
|
+
[DbkMethod.BOTS_LIST]: () => {
|
|
6682
|
+
const result = snapshot();
|
|
6683
|
+
ctx.logger.debug(
|
|
6684
|
+
"bots.list: count=%d keys=%s",
|
|
6685
|
+
result.bots.length,
|
|
6686
|
+
result.bots.map((bot) => bot.botKey).join(",") || "-"
|
|
6687
|
+
);
|
|
6688
|
+
return result;
|
|
6689
|
+
},
|
|
6690
|
+
[DbkMethod.TARGETS_LIST]: async (request) => {
|
|
6691
|
+
ctx.logger.debug(
|
|
6692
|
+
"targets.list: bot=%s kind=%s",
|
|
6693
|
+
request.botKey.trim() || "*",
|
|
6694
|
+
enumName(TargetKind, request.kind)
|
|
6695
|
+
);
|
|
6696
|
+
const result = await listTargets(ctx, request);
|
|
6697
|
+
ctx.logger.debug(
|
|
6698
|
+
"targets.list: count=%d incomplete=%s",
|
|
6699
|
+
result.targets.length,
|
|
6700
|
+
result.incomplete
|
|
6701
|
+
);
|
|
6702
|
+
return result;
|
|
6703
|
+
},
|
|
6704
|
+
[DbkMethod.TARGETS_GET]: async (request) => {
|
|
6705
|
+
ctx.logger.debug(
|
|
6706
|
+
"targets.get: bot=%s target=%s",
|
|
6707
|
+
request.botKey.trim() || "*",
|
|
6708
|
+
formatTarget(request.target)
|
|
6709
|
+
);
|
|
6710
|
+
const result = await getTarget(ctx, request);
|
|
6711
|
+
ctx.logger.debug(
|
|
6712
|
+
"targets.get: unresolved=%s name=%s bots=%s",
|
|
6713
|
+
result.unresolved,
|
|
6714
|
+
result.target?.name || "-",
|
|
6715
|
+
result.target?.botKeys.join(",") || "-"
|
|
6716
|
+
);
|
|
6717
|
+
return result;
|
|
6718
|
+
},
|
|
6719
|
+
[DbkMethod.MESSAGE_SEND]: async (request) => {
|
|
6720
|
+
ctx.logger.debug(
|
|
6721
|
+
"message.send: bot=%s target=%s reply=%s units=%s",
|
|
6722
|
+
request.botKey.trim() || "-",
|
|
6723
|
+
formatTarget(request.target),
|
|
6724
|
+
request.replyToMessageId.trim() || "-",
|
|
6725
|
+
formatUnits(request)
|
|
6726
|
+
);
|
|
6727
|
+
const result = await sendMessage(ctx, request);
|
|
6728
|
+
ctx.logger.debug(
|
|
6729
|
+
"message.send: status=%s receipts=%d retryable=%s reason=%s",
|
|
6730
|
+
enumName(SendStatus, result.status),
|
|
6731
|
+
result.receipts.length,
|
|
6732
|
+
result.retryable,
|
|
6733
|
+
result.reason || "-"
|
|
6734
|
+
);
|
|
6735
|
+
return result;
|
|
6736
|
+
},
|
|
6737
|
+
[DbkMethod.MESSAGE_RECALL]: async (request) => {
|
|
6738
|
+
ctx.logger.debug(
|
|
6739
|
+
"message.recall: bot=%s target=%s message=%s",
|
|
6740
|
+
request.botKey.trim() || "-",
|
|
6741
|
+
formatTarget(request.target),
|
|
6742
|
+
request.messageId.trim() || "-"
|
|
6743
|
+
);
|
|
6744
|
+
const result = await recallMessage(ctx, request);
|
|
6745
|
+
ctx.logger.debug(
|
|
6746
|
+
"message.recall: status=%s retryable=%s reason=%s",
|
|
6747
|
+
enumName(SendStatus, result.status),
|
|
6748
|
+
result.retryable,
|
|
6749
|
+
result.reason || "-"
|
|
6750
|
+
);
|
|
6751
|
+
return result;
|
|
6752
|
+
}
|
|
6500
6753
|
};
|
|
6501
6754
|
}
|
|
6755
|
+
function formatTarget(target) {
|
|
6756
|
+
if (!target) return "-";
|
|
6757
|
+
const kind = enumName(TargetKind, target.kind);
|
|
6758
|
+
const id = target.id.trim() || "-";
|
|
6759
|
+
const guild = target.guildId.trim();
|
|
6760
|
+
return guild ? `${kind}:${id} guild=${guild}` : `${kind}:${id}`;
|
|
6761
|
+
}
|
|
6762
|
+
function formatUnits(request) {
|
|
6763
|
+
if (request.units.length === 0) return "-";
|
|
6764
|
+
return request.units.map((unit) => {
|
|
6765
|
+
if (unit.body.case === "normal") return `normal:${unit.body.value.segments.length}`;
|
|
6766
|
+
if (unit.body.case === "forward") return `forward:${unit.body.value.nodes.length}`;
|
|
6767
|
+
return "empty";
|
|
6768
|
+
}).join(",");
|
|
6769
|
+
}
|
|
6770
|
+
function enumName(map, value) {
|
|
6771
|
+
return map[value] ?? String(value);
|
|
6772
|
+
}
|
|
6502
6773
|
|
|
6503
6774
|
// src/index.ts
|
|
6504
6775
|
var name = "dynamic-bot";
|
package/package.json
CHANGED
package/src/dbk/gateway.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { create } from "@bufbuild/protobuf";
|
|
2
2
|
import type { Context } from "koishi";
|
|
3
|
-
import { BotChangeType } from "../gen/dbk/v1/common_pb";
|
|
3
|
+
import { BotChangeType, BotStatus } from "../gen/dbk/v1/common_pb";
|
|
4
4
|
import { BotChangedEventSchema } from "../gen/dbk/v1/rpc_pb";
|
|
5
5
|
import { toDbkBot } from "./bots";
|
|
6
6
|
import { DbkEvent, type DbkEncoding, type DbkEventMap, type DbkGatewayHandlers } from "./protocol";
|
|
@@ -46,12 +46,20 @@ export class DbkGateway {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
emit<K extends keyof DbkEventMap>(method: K, event: DbkEventMap[K]): void {
|
|
49
|
-
this.session
|
|
49
|
+
if (!this.session) {
|
|
50
|
+
this.ctx.logger.debug("drop EVENT without session: method=%s", method);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.session.emit(method, event);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
startForward(): void {
|
|
53
57
|
const path = normalizePath(this.config.path);
|
|
54
|
-
this.ctx.logger.info(
|
|
58
|
+
this.ctx.logger.info(
|
|
59
|
+
"DBK forward WebSocket mounted on Koishi HTTP at %s encoding=%s",
|
|
60
|
+
path,
|
|
61
|
+
this.config.encoding,
|
|
62
|
+
);
|
|
55
63
|
this.ctx.server.ws(path, (socket) => {
|
|
56
64
|
this.attach("replaced", socket as DbkSocket);
|
|
57
65
|
});
|
|
@@ -69,12 +77,17 @@ export class DbkGateway {
|
|
|
69
77
|
this.session = undefined;
|
|
70
78
|
this.socket?.close(1000, "plugin stop");
|
|
71
79
|
this.socket = undefined;
|
|
80
|
+
this.ctx.logger.debug("DBK gateway stopped");
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
private connectReverse(): void {
|
|
75
84
|
if (this.closing) return;
|
|
76
85
|
const url = `ws://${this.config.host}:${this.config.port}`;
|
|
77
|
-
this.ctx.logger.info(
|
|
86
|
+
this.ctx.logger.info(
|
|
87
|
+
"DBK reverse WebSocket connecting to %s encoding=%s",
|
|
88
|
+
url,
|
|
89
|
+
this.config.encoding,
|
|
90
|
+
);
|
|
78
91
|
const socket = this.ctx.http.ws(url) as DbkSocket;
|
|
79
92
|
const onOpen = () => {
|
|
80
93
|
this.reconnectAttempts = 0;
|
|
@@ -113,6 +126,7 @@ export class DbkGateway {
|
|
|
113
126
|
});
|
|
114
127
|
this.session = session;
|
|
115
128
|
attachSocket(socket, session, this.config.encoding);
|
|
129
|
+
this.ctx.logger.debug("DBK connection attached encoding=%s", this.config.encoding);
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
private onReverseClosed(): void {
|
|
@@ -139,9 +153,16 @@ export class DbkGateway {
|
|
|
139
153
|
export function watchBots(ctx: Context, gateway: DbkGateway): void {
|
|
140
154
|
const emit = (type: BotChangeType, bot: Parameters<typeof toDbkBot>[0]) => {
|
|
141
155
|
if (bot.hidden && type !== BotChangeType.REMOVED) return;
|
|
156
|
+
const mapped = toDbkBot(bot);
|
|
157
|
+
ctx.logger.debug(
|
|
158
|
+
"bot.changed: %s %s status=%s",
|
|
159
|
+
BotChangeType[type] ?? type,
|
|
160
|
+
mapped.botKey || "-",
|
|
161
|
+
BotStatus[mapped.status] ?? mapped.status,
|
|
162
|
+
);
|
|
142
163
|
gateway.emit(DbkEvent.BOT_CHANGED, create(BotChangedEventSchema, {
|
|
143
164
|
type,
|
|
144
|
-
bot:
|
|
165
|
+
bot: mapped,
|
|
145
166
|
}));
|
|
146
167
|
};
|
|
147
168
|
ctx.on("bot-added", (bot) => emit(BotChangeType.ADDED, bot));
|
package/src/dbk/get-target.ts
CHANGED
|
@@ -59,14 +59,28 @@ export async function getTarget(ctx: Context, request: GetTargetRequest): Promis
|
|
|
59
59
|
throw new DbkRpcError(ErrorCode.NOT_FOUND, `bot not found: ${botKey}`);
|
|
60
60
|
}
|
|
61
61
|
const info = await resolveOnBot(bot, target);
|
|
62
|
-
|
|
62
|
+
if (info) {
|
|
63
|
+
ctx.logger.debug("targets.get: resolved on %s kind=%s id=%s", botKey, TargetKind[info.target?.kind ?? 0], info.target?.id);
|
|
64
|
+
return resolved(info);
|
|
65
|
+
}
|
|
66
|
+
ctx.logger.debug("targets.get: unresolved on %s id=%s", botKey, target?.id.trim() || "-");
|
|
67
|
+
return unresolved(target);
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
// Empty bot_key: search non-hidden, non-qq bots in ctx.bots order; first successful resolve wins.
|
|
66
71
|
for (const bot of listSearchableBots(ctx)) {
|
|
67
72
|
const info = await resolveOnBot(bot, target);
|
|
68
|
-
if (info)
|
|
73
|
+
if (info) {
|
|
74
|
+
ctx.logger.debug(
|
|
75
|
+
"targets.get: resolved on %s kind=%s id=%s",
|
|
76
|
+
botKeyOf(bot),
|
|
77
|
+
TargetKind[info.target?.kind ?? 0],
|
|
78
|
+
info.target?.id,
|
|
79
|
+
);
|
|
80
|
+
return resolved(info);
|
|
81
|
+
}
|
|
69
82
|
}
|
|
83
|
+
ctx.logger.debug("targets.get: unresolved id=%s", target?.id.trim() || "-");
|
|
70
84
|
return unresolved(target);
|
|
71
85
|
}
|
|
72
86
|
|
package/src/dbk/handlers.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { create } from "@bufbuild/protobuf";
|
|
2
2
|
import type { Context } from "koishi";
|
|
3
|
+
import { SendStatus, TargetKind, type Target } from "../gen/dbk/v1/common_pb";
|
|
3
4
|
import {
|
|
4
5
|
HelloResponseSchema,
|
|
5
6
|
ListBotsResponseSchema,
|
|
7
|
+
type SendParams,
|
|
6
8
|
} from "../gen/dbk/v1/rpc_pb";
|
|
7
9
|
import { listBots } from "./bots";
|
|
8
10
|
import { getTarget } from "./get-target";
|
|
@@ -15,15 +17,114 @@ export function createGatewayHandlers(ctx: Context): DbkGatewayHandlers {
|
|
|
15
17
|
const snapshot = () => create(ListBotsResponseSchema, { bots: listBots(ctx) });
|
|
16
18
|
|
|
17
19
|
return {
|
|
18
|
-
[DbkMethod.SESSION_HELLO]: () =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
[DbkMethod.SESSION_HELLO]: (request) => {
|
|
21
|
+
const bots = snapshot().bots;
|
|
22
|
+
ctx.logger.debug(
|
|
23
|
+
"session.hello: app=%s protocol=%s gateway=%s bots=%d keys=%s",
|
|
24
|
+
request.appVersion || "-",
|
|
25
|
+
request.protocolVersion || "-",
|
|
26
|
+
GATEWAY_VERSION,
|
|
27
|
+
bots.length,
|
|
28
|
+
bots.map((bot) => bot.botKey).join(",") || "-",
|
|
29
|
+
);
|
|
30
|
+
return create(HelloResponseSchema, {
|
|
31
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
32
|
+
gatewayVersion: GATEWAY_VERSION,
|
|
33
|
+
bots,
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
[DbkMethod.BOTS_LIST]: () => {
|
|
37
|
+
const result = snapshot();
|
|
38
|
+
ctx.logger.debug(
|
|
39
|
+
"bots.list: count=%d keys=%s",
|
|
40
|
+
result.bots.length,
|
|
41
|
+
result.bots.map((bot) => bot.botKey).join(",") || "-",
|
|
42
|
+
);
|
|
43
|
+
return result;
|
|
44
|
+
},
|
|
45
|
+
[DbkMethod.TARGETS_LIST]: async (request) => {
|
|
46
|
+
ctx.logger.debug(
|
|
47
|
+
"targets.list: bot=%s kind=%s",
|
|
48
|
+
request.botKey.trim() || "*",
|
|
49
|
+
enumName(TargetKind, request.kind),
|
|
50
|
+
);
|
|
51
|
+
const result = await listTargets(ctx, request);
|
|
52
|
+
ctx.logger.debug(
|
|
53
|
+
"targets.list: count=%d incomplete=%s",
|
|
54
|
+
result.targets.length,
|
|
55
|
+
result.incomplete,
|
|
56
|
+
);
|
|
57
|
+
return result;
|
|
58
|
+
},
|
|
59
|
+
[DbkMethod.TARGETS_GET]: async (request) => {
|
|
60
|
+
ctx.logger.debug(
|
|
61
|
+
"targets.get: bot=%s target=%s",
|
|
62
|
+
request.botKey.trim() || "*",
|
|
63
|
+
formatTarget(request.target),
|
|
64
|
+
);
|
|
65
|
+
const result = await getTarget(ctx, request);
|
|
66
|
+
ctx.logger.debug(
|
|
67
|
+
"targets.get: unresolved=%s name=%s bots=%s",
|
|
68
|
+
result.unresolved,
|
|
69
|
+
result.target?.name || "-",
|
|
70
|
+
result.target?.botKeys.join(",") || "-",
|
|
71
|
+
);
|
|
72
|
+
return result;
|
|
73
|
+
},
|
|
74
|
+
[DbkMethod.MESSAGE_SEND]: async (request) => {
|
|
75
|
+
ctx.logger.debug(
|
|
76
|
+
"message.send: bot=%s target=%s reply=%s units=%s",
|
|
77
|
+
request.botKey.trim() || "-",
|
|
78
|
+
formatTarget(request.target),
|
|
79
|
+
request.replyToMessageId.trim() || "-",
|
|
80
|
+
formatUnits(request),
|
|
81
|
+
);
|
|
82
|
+
const result = await sendMessage(ctx, request);
|
|
83
|
+
ctx.logger.debug(
|
|
84
|
+
"message.send: status=%s receipts=%d retryable=%s reason=%s",
|
|
85
|
+
enumName(SendStatus, result.status),
|
|
86
|
+
result.receipts.length,
|
|
87
|
+
result.retryable,
|
|
88
|
+
result.reason || "-",
|
|
89
|
+
);
|
|
90
|
+
return result;
|
|
91
|
+
},
|
|
92
|
+
[DbkMethod.MESSAGE_RECALL]: async (request) => {
|
|
93
|
+
ctx.logger.debug(
|
|
94
|
+
"message.recall: bot=%s target=%s message=%s",
|
|
95
|
+
request.botKey.trim() || "-",
|
|
96
|
+
formatTarget(request.target),
|
|
97
|
+
request.messageId.trim() || "-",
|
|
98
|
+
);
|
|
99
|
+
const result = await recallMessage(ctx, request);
|
|
100
|
+
ctx.logger.debug(
|
|
101
|
+
"message.recall: status=%s retryable=%s reason=%s",
|
|
102
|
+
enumName(SendStatus, result.status),
|
|
103
|
+
result.retryable,
|
|
104
|
+
result.reason || "-",
|
|
105
|
+
);
|
|
106
|
+
return result;
|
|
107
|
+
},
|
|
28
108
|
};
|
|
29
109
|
}
|
|
110
|
+
|
|
111
|
+
function formatTarget(target: Target | undefined): string {
|
|
112
|
+
if (!target) return "-";
|
|
113
|
+
const kind = enumName(TargetKind, target.kind);
|
|
114
|
+
const id = target.id.trim() || "-";
|
|
115
|
+
const guild = target.guildId.trim();
|
|
116
|
+
return guild ? `${kind}:${id} guild=${guild}` : `${kind}:${id}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatUnits(request: SendParams): string {
|
|
120
|
+
if (request.units.length === 0) return "-";
|
|
121
|
+
return request.units.map((unit) => {
|
|
122
|
+
if (unit.body.case === "normal") return `normal:${unit.body.value.segments.length}`;
|
|
123
|
+
if (unit.body.case === "forward") return `forward:${unit.body.value.nodes.length}`;
|
|
124
|
+
return "empty";
|
|
125
|
+
}).join(",");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function enumName(map: Record<number, string>, value: number): string {
|
|
129
|
+
return map[value] ?? String(value);
|
|
130
|
+
}
|
package/src/dbk/list-targets.ts
CHANGED
|
@@ -60,7 +60,10 @@ export async function listTargets(ctx: Context, request: ListTargetsRequest): Pr
|
|
|
60
60
|
let incomplete = false;
|
|
61
61
|
|
|
62
62
|
for (const bot of bots) {
|
|
63
|
-
if (isQq(bot))
|
|
63
|
+
if (isQq(bot)) {
|
|
64
|
+
ctx.logger?.debug("targets.list: skip qq bot %s", botKeyOf(bot));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
64
67
|
const listed = await listTargetsForBot(ctx, bot, kindFilter);
|
|
65
68
|
incomplete = incomplete || listed.incomplete;
|
|
66
69
|
for (const target of listed.targets) {
|
|
@@ -120,6 +123,12 @@ async function listTargetsForBot(
|
|
|
120
123
|
const nested = hasNestedChannels(bot);
|
|
121
124
|
const supported = botTargetKinds(bot);
|
|
122
125
|
if (kindFilter !== TargetKind.UNSPECIFIED && supported.length > 0 && !supported.includes(kindFilter)) {
|
|
126
|
+
ctx.logger?.debug(
|
|
127
|
+
"targets.list: %s skip kind=%s supported=%s",
|
|
128
|
+
botKey,
|
|
129
|
+
TargetKind[kindFilter] ?? kindFilter,
|
|
130
|
+
supported.map((kind) => TargetKind[kind] ?? kind).join(","),
|
|
131
|
+
);
|
|
123
132
|
return { targets, incomplete: false };
|
|
124
133
|
}
|
|
125
134
|
|
|
@@ -134,7 +143,12 @@ async function listTargetsForBot(
|
|
|
134
143
|
const guilds = await paginate<GuildLike>((next) => callBotList(bot, "getGuildList", next));
|
|
135
144
|
if (guilds.unsupported || guilds.failed || guilds.truncated) incomplete = true;
|
|
136
145
|
if (guilds.failed && !guilds.unsupported) {
|
|
137
|
-
ctx.logger?.debug(
|
|
146
|
+
ctx.logger?.debug(
|
|
147
|
+
"targets.list: %s getGuildList failed items=%d truncated=%s",
|
|
148
|
+
botKey,
|
|
149
|
+
guilds.items.length,
|
|
150
|
+
guilds.truncated,
|
|
151
|
+
);
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
let channelMode: "unknown" | "nested" | "flat" = nested ? "unknown" : "flat";
|
|
@@ -209,6 +223,13 @@ async function listTargetsForBot(
|
|
|
209
223
|
}
|
|
210
224
|
}
|
|
211
225
|
|
|
226
|
+
ctx.logger?.debug(
|
|
227
|
+
"targets.list: %s listed=%d incomplete=%s nested=%s",
|
|
228
|
+
botKey,
|
|
229
|
+
targets.length,
|
|
230
|
+
incomplete,
|
|
231
|
+
nested,
|
|
232
|
+
);
|
|
212
233
|
return { targets, incomplete };
|
|
213
234
|
}
|
|
214
235
|
|
package/src/dbk/recall.ts
CHANGED
|
@@ -34,15 +34,24 @@ export async function recallMessage(ctx: Context, params: RecallParams): Promise
|
|
|
34
34
|
return failed("target id is empty", false);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
ctx.logger.debug("message.recall: deleteMessage channel=%s message=%s", channelId, messageId);
|
|
37
38
|
try {
|
|
38
39
|
await withTimeout(Promise.resolve(bot.deleteMessage(channelId, messageId)), RECALL_TIMEOUT_MS);
|
|
40
|
+
ctx.logger.debug("message.recall: adapter ok");
|
|
39
41
|
return create(RecallResultSchema, {
|
|
40
42
|
status: SendStatus.OK,
|
|
41
43
|
reason: "",
|
|
42
44
|
retryable: false,
|
|
43
45
|
});
|
|
44
46
|
} catch (error) {
|
|
45
|
-
|
|
47
|
+
const result = classifyRecallError(error);
|
|
48
|
+
ctx.logger.debug(
|
|
49
|
+
"message.recall: adapter error status=%s retryable=%s: %s",
|
|
50
|
+
SendStatus[result.status] ?? result.status,
|
|
51
|
+
result.retryable,
|
|
52
|
+
result.reason || errorMessage(error) || "message.recall failed",
|
|
53
|
+
);
|
|
54
|
+
return result;
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
package/src/dbk/send.ts
CHANGED
|
@@ -77,20 +77,29 @@ export async function sendMessage(ctx: Context, request: SendParams): Promise<Se
|
|
|
77
77
|
const sendElements = async (elements: El[]): Promise<void> => {
|
|
78
78
|
if (aborted) return;
|
|
79
79
|
if (elements.length === 0) {
|
|
80
|
+
ctx.logger.debug("message.send: no sendable segments");
|
|
80
81
|
failuresRetryable = false;
|
|
81
82
|
failures.push("no sendable segments");
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
const outgoing = withQuote(elements, nextQuoteId());
|
|
86
|
+
ctx.logger.debug(
|
|
87
|
+
"message.send: bot.sendMessage channel=%s guild=%s elements=%d",
|
|
88
|
+
channelId,
|
|
89
|
+
guildId || "-",
|
|
90
|
+
outgoing.length,
|
|
91
|
+
);
|
|
85
92
|
try {
|
|
86
93
|
const ids = await bot.sendMessage(channelId, outgoing, guildId);
|
|
87
94
|
if (aborted) return;
|
|
88
95
|
quoteUsed = true;
|
|
89
96
|
const kept = normalizeMessageIds(ids);
|
|
90
97
|
if (kept.length === 0) {
|
|
98
|
+
ctx.logger.debug("message.send: adapter returned no message id");
|
|
91
99
|
unknownReasons.push("send returned no message id");
|
|
92
100
|
return;
|
|
93
101
|
}
|
|
102
|
+
ctx.logger.debug("message.send: adapter ids=%s", kept.join(","));
|
|
94
103
|
for (const messageId of kept) {
|
|
95
104
|
receipts.push(create(SendReceiptSchema, { messageId, recallable }));
|
|
96
105
|
}
|
|
@@ -98,10 +107,17 @@ export async function sendMessage(ctx: Context, request: SendParams): Promise<Se
|
|
|
98
107
|
if (aborted) return;
|
|
99
108
|
if (isTimeoutError(error)) {
|
|
100
109
|
quoteUsed = true;
|
|
101
|
-
|
|
110
|
+
const reason = errorMessage(error) || "send timed out";
|
|
111
|
+
ctx.logger.debug("message.send: adapter timeout: %s", reason);
|
|
112
|
+
unknownReasons.push(reason);
|
|
102
113
|
return;
|
|
103
114
|
}
|
|
104
115
|
const retryable = isRetryableSendError(error);
|
|
116
|
+
ctx.logger.debug(
|
|
117
|
+
"message.send: adapter error retryable=%s: %s",
|
|
118
|
+
retryable,
|
|
119
|
+
errorMessage(error) || "send failed",
|
|
120
|
+
);
|
|
105
121
|
failuresRetryable = failuresRetryable && retryable;
|
|
106
122
|
failures.push(errorMessage(error) || "send failed");
|
|
107
123
|
}
|
|
@@ -115,9 +131,12 @@ export async function sendMessage(ctx: Context, request: SendParams): Promise<Se
|
|
|
115
131
|
} catch (error) {
|
|
116
132
|
if (aborted) return;
|
|
117
133
|
if (isTimeoutError(error)) {
|
|
118
|
-
|
|
134
|
+
const reason = errorMessage(error) || "media fetch timed out";
|
|
135
|
+
ctx.logger.debug("message.send: media timeout: %s", reason);
|
|
136
|
+
unknownReasons.push(reason);
|
|
119
137
|
return;
|
|
120
138
|
}
|
|
139
|
+
ctx.logger.debug("message.send: media error: %s", errorMessage(error) || "media fetch failed");
|
|
121
140
|
failuresRetryable = failuresRetryable && isRetryableSendError(error);
|
|
122
141
|
failures.push(errorMessage(error) || "media fetch failed");
|
|
123
142
|
return;
|
|
@@ -257,31 +276,97 @@ async function forwardNodeElements(
|
|
|
257
276
|
type MediaKind = keyof typeof MEDIA_FALLBACK_TYPE;
|
|
258
277
|
|
|
259
278
|
async function fetchMediaElement(ctx: Context, kind: MediaKind, uri: string): Promise<El> {
|
|
260
|
-
|
|
261
|
-
if (typeof http?.file !== "function") {
|
|
262
|
-
throw mediaFetchError(kind, uri, new Error("Koishi http service is required to send media"));
|
|
263
|
-
}
|
|
264
|
-
let file: { data?: unknown; mime?: string; type?: string };
|
|
265
|
-
try {
|
|
266
|
-
file = await http.file(uri);
|
|
267
|
-
} catch (error) {
|
|
268
|
-
throw mediaFetchError(kind, uri, error);
|
|
269
|
-
}
|
|
279
|
+
ctx.logger.debug("message.send: fetch %s %s", kind, displayUri(uri));
|
|
270
280
|
let buffer: Buffer;
|
|
281
|
+
let mime: string;
|
|
271
282
|
try {
|
|
272
|
-
|
|
283
|
+
const inline = decodeInlineMedia(uri, kind);
|
|
284
|
+
if (inline) {
|
|
285
|
+
buffer = inline.buffer;
|
|
286
|
+
mime = inline.mime;
|
|
287
|
+
} else {
|
|
288
|
+
const fetched = await fetchRemoteMedia(ctx, kind, uri);
|
|
289
|
+
buffer = fetched.buffer;
|
|
290
|
+
mime = fetched.mime;
|
|
291
|
+
}
|
|
273
292
|
} catch (error) {
|
|
274
293
|
throw mediaFetchError(kind, uri, error);
|
|
275
294
|
}
|
|
276
295
|
if (buffer.length === 0) {
|
|
277
296
|
throw mediaFetchError(kind, uri, new Error("empty body"));
|
|
278
297
|
}
|
|
279
|
-
|
|
298
|
+
ctx.logger.debug("message.send: fetched %s bytes=%d mime=%s", kind, buffer.length, mime);
|
|
280
299
|
if (kind === "image") return h.image(buffer, mime);
|
|
281
300
|
if (kind === "video") return h.video(buffer, mime);
|
|
282
301
|
return h.audio(buffer, mime);
|
|
283
302
|
}
|
|
284
303
|
|
|
304
|
+
async function fetchRemoteMedia(
|
|
305
|
+
ctx: Context,
|
|
306
|
+
kind: MediaKind,
|
|
307
|
+
uri: string,
|
|
308
|
+
): Promise<{ buffer: Buffer; mime: string }> {
|
|
309
|
+
const http = ctx.http;
|
|
310
|
+
if (typeof http?.file !== "function") {
|
|
311
|
+
throw new Error("Koishi http service is required to send media");
|
|
312
|
+
}
|
|
313
|
+
const file = await http.file(uri);
|
|
314
|
+
const buffer = mediaBytes(file.data);
|
|
315
|
+
const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
|
|
316
|
+
return { buffer, mime };
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function decodeInlineMedia(uri: string, kind: MediaKind): { buffer: Buffer; mime: string } | undefined {
|
|
320
|
+
if (/^data:/i.test(uri)) {
|
|
321
|
+
const parsed = decodeDataUri(uri);
|
|
322
|
+
if (!parsed) throw new Error("invalid data URI");
|
|
323
|
+
return { buffer: parsed.buffer, mime: parsed.mime || sniffMime(parsed.buffer, kind) };
|
|
324
|
+
}
|
|
325
|
+
const base64 = /^base64:\/\//i.exec(uri);
|
|
326
|
+
if (base64) {
|
|
327
|
+
const buffer = decodeBase64Payload(uri.slice(base64[0].length));
|
|
328
|
+
return { buffer, mime: sniffMime(buffer, kind) };
|
|
329
|
+
}
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function decodeDataUri(uri: string): { buffer: Buffer; mime: string } | undefined {
|
|
334
|
+
const comma = uri.indexOf(",");
|
|
335
|
+
if (comma < 5) return undefined;
|
|
336
|
+
const parts = uri.slice(5, comma).split(";").map((part) => part.trim()).filter(Boolean);
|
|
337
|
+
const isBase64 = parts.some((part) => part.toLowerCase() === "base64");
|
|
338
|
+
const mime = parts.find((part) => part.includes("/")) ?? "";
|
|
339
|
+
const payload = uri.slice(comma + 1);
|
|
340
|
+
const buffer = isBase64
|
|
341
|
+
? decodeBase64Payload(payload)
|
|
342
|
+
: Buffer.from(decodeURIComponent(payload.replace(/\+/g, " ")));
|
|
343
|
+
return { buffer, mime };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function decodeBase64Payload(payload: string): Buffer {
|
|
347
|
+
const cleaned = payload.replace(/\s/g, "").replace(/-/g, "+").replace(/_/g, "/");
|
|
348
|
+
if (!cleaned) throw new Error("empty base64 payload");
|
|
349
|
+
const buffer = Buffer.from(cleaned, "base64");
|
|
350
|
+
if (buffer.length === 0) throw new Error("invalid base64 payload");
|
|
351
|
+
return buffer;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function sniffMime(buffer: Buffer, kind: MediaKind): string {
|
|
355
|
+
if (buffer.length >= 8 && buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
|
|
356
|
+
return "image/png";
|
|
357
|
+
}
|
|
358
|
+
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
359
|
+
return "image/jpeg";
|
|
360
|
+
}
|
|
361
|
+
if (buffer.length >= 6 && buffer.toString("ascii", 0, 6).startsWith("GIF8")) {
|
|
362
|
+
return "image/gif";
|
|
363
|
+
}
|
|
364
|
+
if (buffer.length >= 12 && buffer.toString("ascii", 0, 4) === "RIFF" && buffer.toString("ascii", 8, 12) === "WEBP") {
|
|
365
|
+
return "image/webp";
|
|
366
|
+
}
|
|
367
|
+
return MEDIA_FALLBACK_TYPE[kind];
|
|
368
|
+
}
|
|
369
|
+
|
|
285
370
|
function mediaBytes(data: unknown): Buffer {
|
|
286
371
|
if (Buffer.isBuffer(data)) return data;
|
|
287
372
|
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
@@ -298,7 +383,8 @@ function mediaFetchError(kind: MediaKind, uri: string, cause: unknown): Error {
|
|
|
298
383
|
}
|
|
299
384
|
|
|
300
385
|
function displayUri(uri: string): string {
|
|
301
|
-
if (
|
|
386
|
+
if (/^data:/i.test(uri)) return "data:...";
|
|
387
|
+
if (/^base64:\/\//i.test(uri)) return "base64://...";
|
|
302
388
|
return uri.length > 200 ? `${uri.slice(0, 200)}...` : uri;
|
|
303
389
|
}
|
|
304
390
|
|
package/src/dbk/session.ts
CHANGED
|
@@ -68,8 +68,12 @@ export class DbkGatewaySession {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
emit<K extends keyof DbkEventMap>(method: K, event: DbkEventMap[K]): void {
|
|
71
|
-
if (!this.isHandshook)
|
|
71
|
+
if (!this.isHandshook) {
|
|
72
|
+
this.options.logger.debug("drop EVENT before handshake: method=%s", method);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
72
75
|
this.seq += 1n;
|
|
76
|
+
this.options.logger.debug("DBK EVENT %s seq=%s", method, this.seq.toString());
|
|
73
77
|
this.enqueueFrame(
|
|
74
78
|
createFrame({
|
|
75
79
|
op: FrameOp.EVENT,
|
|
@@ -126,6 +130,7 @@ export class DbkGatewaySession {
|
|
|
126
130
|
this.replyError(frame.id, ErrorCode.UNSUPPORTED, `unknown method: ${frame.method}`);
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
133
|
+
this.options.logger.debug("DBK CALL %s id=%s", frame.method, frame.id);
|
|
129
134
|
try {
|
|
130
135
|
const payload = await this.dispatchCall(frame.method, frame.payload);
|
|
131
136
|
this.enqueueFrame(
|