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 CHANGED
@@ -2817,8 +2817,8 @@ function findFileDependencies(proto, reg) {
2817
2817
  return dep;
2818
2818
  });
2819
2819
  }
2820
- function findEnumSharedPrefix(enumName, values) {
2821
- const prefix = camelToSnakeCase(enumName) + "_";
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) return;
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?.emit(method, event);
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("DBK forward WebSocket mounted on Koishi HTTP at %s", path);
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("DBK reverse WebSocket connecting to %s", url);
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: toDbkBot(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
- return info ? resolved(info) : unresolved(target);
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) return resolved(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)) continue;
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("targets.list: %s getGuildList failed", botKey);
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
- return classifyRecallError(error);
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
- unknownReasons.push(errorMessage2(error) || "send timed out");
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
- unknownReasons.push(errorMessage2(error) || "media fetch timed out");
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
- const http = ctx.http;
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
- buffer = mediaBytes(file.data);
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
- const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
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 (uri.startsWith("data:")) return "data:...";
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]: () => create(HelloResponseSchema, {
6491
- protocolVersion: PROTOCOL_VERSION,
6492
- gatewayVersion: GATEWAY_VERSION,
6493
- bots: snapshot().bots
6494
- }),
6495
- [DbkMethod.BOTS_LIST]: snapshot,
6496
- [DbkMethod.TARGETS_LIST]: (request) => listTargets(ctx, request),
6497
- [DbkMethod.TARGETS_GET]: (request) => getTarget(ctx, request),
6498
- [DbkMethod.MESSAGE_SEND]: (request) => sendMessage(ctx, request),
6499
- [DbkMethod.MESSAGE_RECALL]: (request) => recallMessage(ctx, request)
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
@@ -4,7 +4,7 @@
4
4
  "type": "git",
5
5
  "url": "https://github.com/incubator4/dynamic-bot-koishi"
6
6
  },
7
- "version": "0.0.5",
7
+ "version": "0.0.7",
8
8
  "license": "Apache-2.0",
9
9
  "contributors": [
10
10
  "AresWang <incubator4@outlook.com>"
@@ -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?.emit(method, event);
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("DBK forward WebSocket mounted on Koishi HTTP at %s", path);
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("DBK reverse WebSocket connecting to %s", url);
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: toDbkBot(bot),
165
+ bot: mapped,
145
166
  }));
146
167
  };
147
168
  ctx.on("bot-added", (bot) => emit(BotChangeType.ADDED, bot));
@@ -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
- return info ? resolved(info) : unresolved(target);
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) return resolved(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
 
@@ -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]: () => create(HelloResponseSchema, {
19
- protocolVersion: PROTOCOL_VERSION,
20
- gatewayVersion: GATEWAY_VERSION,
21
- bots: snapshot().bots,
22
- }),
23
- [DbkMethod.BOTS_LIST]: snapshot,
24
- [DbkMethod.TARGETS_LIST]: (request) => listTargets(ctx, request),
25
- [DbkMethod.TARGETS_GET]: (request) => getTarget(ctx, request),
26
- [DbkMethod.MESSAGE_SEND]: (request) => sendMessage(ctx, request),
27
- [DbkMethod.MESSAGE_RECALL]: (request) => recallMessage(ctx, request),
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
+ }
@@ -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)) continue;
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("targets.list: %s getGuildList failed", botKey);
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
- return classifyRecallError(error);
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
- unknownReasons.push(errorMessage(error) || "send timed out");
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
- unknownReasons.push(errorMessage(error) || "media fetch timed out");
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
- const http = ctx.http;
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
- buffer = mediaBytes(file.data);
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
- const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
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 (uri.startsWith("data:")) return "data:...";
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
 
@@ -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) return;
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(