koishi-plugin-dynamic-bot 0.0.4 → 0.0.6

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/dbk/send.d.ts CHANGED
@@ -12,5 +12,5 @@ export interface SegmentRenderOptions {
12
12
  mentionAll?: boolean;
13
13
  }
14
14
  export declare function sendMessage(ctx: Context, request: SendParams): Promise<SendResult>;
15
- export declare function segmentsToElements(segments: Segment[], options?: SegmentRenderOptions): El[];
15
+ export declare function segmentsToElements(ctx: Context, segments: Segment[], options?: SegmentRenderOptions): Promise<El[]>;
16
16
  export {};
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) {
@@ -6086,6 +6186,11 @@ function withTimeout(promise, ms) {
6086
6186
  var import_koishi = require("koishi");
6087
6187
  var SEND_TIMEOUT_MS = 25e3;
6088
6188
  var MENTION_ALL_FALLBACK = "@\u5168\u4F53\u6210\u5458";
6189
+ var MEDIA_FALLBACK_TYPE = {
6190
+ image: "image/png",
6191
+ video: "video/mp4",
6192
+ audio: "audio/mpeg"
6193
+ };
6089
6194
  async function sendMessage(ctx, request) {
6090
6195
  const bot = findBot4(ctx, request.botKey);
6091
6196
  if (!bot) {
@@ -6123,20 +6228,29 @@ async function sendMessage(ctx, request) {
6123
6228
  const sendElements = async (elements) => {
6124
6229
  if (aborted) return;
6125
6230
  if (elements.length === 0) {
6231
+ ctx.logger.debug("message.send: no sendable segments");
6126
6232
  failuresRetryable = false;
6127
6233
  failures.push("no sendable segments");
6128
6234
  return;
6129
6235
  }
6130
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
+ );
6131
6243
  try {
6132
6244
  const ids = await bot.sendMessage(channelId, outgoing, guildId);
6133
6245
  if (aborted) return;
6134
6246
  quoteUsed = true;
6135
6247
  const kept = normalizeMessageIds(ids);
6136
6248
  if (kept.length === 0) {
6249
+ ctx.logger.debug("message.send: adapter returned no message id");
6137
6250
  unknownReasons.push("send returned no message id");
6138
6251
  return;
6139
6252
  }
6253
+ ctx.logger.debug("message.send: adapter ids=%s", kept.join(","));
6140
6254
  for (const messageId of kept) {
6141
6255
  receipts.push(create(SendReceiptSchema, { messageId, recallable }));
6142
6256
  }
@@ -6144,19 +6258,46 @@ async function sendMessage(ctx, request) {
6144
6258
  if (aborted) return;
6145
6259
  if (isTimeoutError(error)) {
6146
6260
  quoteUsed = true;
6147
- 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);
6148
6264
  return;
6149
6265
  }
6150
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
+ );
6151
6272
  failuresRetryable = failuresRetryable && retryable;
6152
6273
  failures.push(errorMessage2(error) || "send failed");
6153
6274
  }
6154
6275
  };
6276
+ const sendRendered = async (render) => {
6277
+ if (aborted) return;
6278
+ let elements;
6279
+ try {
6280
+ elements = await render;
6281
+ } catch (error) {
6282
+ if (aborted) return;
6283
+ if (isTimeoutError(error)) {
6284
+ const reason = errorMessage2(error) || "media fetch timed out";
6285
+ ctx.logger.debug("message.send: media timeout: %s", reason);
6286
+ unknownReasons.push(reason);
6287
+ return;
6288
+ }
6289
+ ctx.logger.debug("message.send: media error: %s", errorMessage2(error) || "media fetch failed");
6290
+ failuresRetryable = failuresRetryable && isRetryableSendError(error);
6291
+ failures.push(errorMessage2(error) || "media fetch failed");
6292
+ return;
6293
+ }
6294
+ await sendElements(elements);
6295
+ };
6155
6296
  const work = (async () => {
6156
6297
  for (const unit of request.units) {
6157
6298
  if (aborted) break;
6158
6299
  if (unit.body.case === "normal") {
6159
- await sendElements(segmentsToElements(unit.body.value.segments, { mentionAll }));
6300
+ await sendRendered(segmentsToElements(ctx, unit.body.value.segments, { mentionAll }));
6160
6301
  } else if (unit.body.case === "forward") {
6161
6302
  const nodes = unit.body.value.nodes;
6162
6303
  if (nodes.length === 0) {
@@ -6166,7 +6307,7 @@ async function sendMessage(ctx, request) {
6166
6307
  }
6167
6308
  for (const node of nodes) {
6168
6309
  if (aborted) break;
6169
- await sendElements(forwardNodeElements(node, { mentionAll }));
6310
+ await sendRendered(forwardNodeElements(ctx, node, { mentionAll }));
6170
6311
  }
6171
6312
  } else {
6172
6313
  failuresRetryable = false;
@@ -6196,7 +6337,7 @@ async function sendMessage(ctx, request) {
6196
6337
  });
6197
6338
  }
6198
6339
  }
6199
- function segmentsToElements(segments, options) {
6340
+ async function segmentsToElements(ctx, segments, options) {
6200
6341
  const elements = [];
6201
6342
  const quoted = /* @__PURE__ */ new Set();
6202
6343
  const addQuote = (id) => {
@@ -6214,17 +6355,17 @@ function segmentsToElements(segments, options) {
6214
6355
  }
6215
6356
  case "image": {
6216
6357
  const uri = segment.body.value.uri.trim();
6217
- if (uri) elements.push(import_koishi.h.image(uri));
6358
+ if (uri) elements.push(await fetchMediaElement(ctx, "image", uri));
6218
6359
  break;
6219
6360
  }
6220
6361
  case "video": {
6221
6362
  const uri = segment.body.value.uri.trim();
6222
- if (uri) elements.push(import_koishi.h.video(uri));
6363
+ if (uri) elements.push(await fetchMediaElement(ctx, "video", uri));
6223
6364
  break;
6224
6365
  }
6225
6366
  case "audio": {
6226
6367
  const uri = segment.body.value.uri.trim();
6227
- if (uri) elements.push(import_koishi.h.audio(uri));
6368
+ if (uri) elements.push(await fetchMediaElement(ctx, "audio", uri));
6228
6369
  break;
6229
6370
  }
6230
6371
  case "mention": {
@@ -6258,13 +6399,57 @@ function segmentsToElements(segments, options) {
6258
6399
  }
6259
6400
  return elements;
6260
6401
  }
6261
- function forwardNodeElements(node, options) {
6262
- const body = segmentsToElements(node.segments, options);
6402
+ async function forwardNodeElements(ctx, node, options) {
6403
+ const body = await segmentsToElements(ctx, node.segments, options);
6263
6404
  const name2 = node.senderName.trim();
6264
6405
  if (!name2) return body;
6265
6406
  return [import_koishi.h.text(`${name2}
6266
6407
  `), ...body];
6267
6408
  }
6409
+ async function fetchMediaElement(ctx, kind, uri) {
6410
+ const http = ctx.http;
6411
+ if (typeof http?.file !== "function") {
6412
+ throw mediaFetchError(kind, uri, new Error("Koishi http service is required to send media"));
6413
+ }
6414
+ ctx.logger.debug("message.send: fetch %s %s", kind, displayUri(uri));
6415
+ let file;
6416
+ try {
6417
+ file = await http.file(uri);
6418
+ } catch (error) {
6419
+ throw mediaFetchError(kind, uri, error);
6420
+ }
6421
+ let buffer;
6422
+ try {
6423
+ buffer = mediaBytes(file.data);
6424
+ } catch (error) {
6425
+ throw mediaFetchError(kind, uri, error);
6426
+ }
6427
+ if (buffer.length === 0) {
6428
+ throw mediaFetchError(kind, uri, new Error("empty body"));
6429
+ }
6430
+ const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
6431
+ ctx.logger.debug("message.send: fetched %s bytes=%d mime=%s", kind, buffer.length, mime);
6432
+ if (kind === "image") return import_koishi.h.image(buffer, mime);
6433
+ if (kind === "video") return import_koishi.h.video(buffer, mime);
6434
+ return import_koishi.h.audio(buffer, mime);
6435
+ }
6436
+ function mediaBytes(data) {
6437
+ if (Buffer.isBuffer(data)) return data;
6438
+ if (data instanceof ArrayBuffer) return Buffer.from(data);
6439
+ if (ArrayBuffer.isView(data)) {
6440
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
6441
+ }
6442
+ throw new Error("media body is not binary");
6443
+ }
6444
+ function mediaFetchError(kind, uri, cause) {
6445
+ const error = new Error(`failed to fetch ${kind} ${displayUri(uri)}: ${errorMessage2(cause) || "unknown error"}`);
6446
+ error.cause = cause;
6447
+ return error;
6448
+ }
6449
+ function displayUri(uri) {
6450
+ if (uri.startsWith("data:")) return "data:...";
6451
+ return uri.length > 200 ? `${uri.slice(0, 200)}...` : uri;
6452
+ }
6268
6453
  function withQuote(elements, quoteId) {
6269
6454
  if (!quoteId) return elements;
6270
6455
  if (hasQuote(elements, quoteId)) return elements;
@@ -6328,28 +6513,33 @@ function failed2(reason, retryable) {
6328
6513
  });
6329
6514
  }
6330
6515
  function isRetryableSendError(error) {
6331
- const text = errorMessage2(error);
6332
- const lower = text.toLowerCase();
6333
- const status = httpStatusOf2(error);
6334
- if (status === 401 || status === 403 || isForbidden2(lower)) return false;
6335
- if (status === 400 || status === 404) return false;
6336
- if (status === 429 || status !== void 0 && status >= 500) return true;
6337
- if (isRetryableNetwork2(error, lower)) return true;
6338
- if (/\brate.?limit\b|too many requests/.test(lower)) return true;
6516
+ for (const item of errorChain(error)) {
6517
+ const text = errorMessage2(item);
6518
+ const lower = text.toLowerCase();
6519
+ const status = httpStatusOf2(item);
6520
+ if (status === 401 || status === 403 || isForbidden2(lower)) return false;
6521
+ if (status === 400 || status === 404) return false;
6522
+ if (status === 429 || status !== void 0 && status >= 500) return true;
6523
+ if (isRetryableNetwork2(item, lower)) return true;
6524
+ if (/\brate.?limit\b|too many requests/.test(lower)) return true;
6525
+ }
6339
6526
  return false;
6340
6527
  }
6341
6528
  function isTimeoutError(error) {
6342
- const text = errorMessage2(error).toLowerCase();
6343
- const status = httpStatusOf2(error);
6344
- if (error instanceof SendTimeoutError) return true;
6345
- if (status === 408 || status === 504) return true;
6346
- if (error && typeof error === "object") {
6347
- const name2 = error.name;
6348
- if (name2 === "TimeoutError" || name2 === "AbortError") return true;
6349
- const code = error.code;
6350
- if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ABORT_ERR") return true;
6529
+ for (const item of errorChain(error)) {
6530
+ const text = errorMessage2(item).toLowerCase();
6531
+ const status = httpStatusOf2(item);
6532
+ if (item instanceof SendTimeoutError) return true;
6533
+ if (status === 408 || status === 504) return true;
6534
+ if (item && typeof item === "object") {
6535
+ const name2 = item.name;
6536
+ if (name2 === "TimeoutError" || name2 === "AbortError") return true;
6537
+ const code = item.code;
6538
+ if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ABORT_ERR") return true;
6539
+ }
6540
+ if (text.includes("timed out") || text.includes("timeout")) return true;
6351
6541
  }
6352
- return text.includes("timed out") || text.includes("timeout");
6542
+ return false;
6353
6543
  }
6354
6544
  function httpStatusOf2(error) {
6355
6545
  if (!error || typeof error !== "object") return void 0;
@@ -6364,6 +6554,17 @@ function httpStatusOf2(error) {
6364
6554
  if (typeof record.code === "number" && record.code >= 400 && record.code < 600) return record.code;
6365
6555
  return void 0;
6366
6556
  }
6557
+ function errorChain(error) {
6558
+ const seen = /* @__PURE__ */ new Set();
6559
+ const chain = [];
6560
+ let current = error;
6561
+ while (current != null && !seen.has(current) && chain.length < 4) {
6562
+ seen.add(current);
6563
+ chain.push(current);
6564
+ current = typeof current === "object" && "cause" in current ? current.cause : void 0;
6565
+ }
6566
+ return chain;
6567
+ }
6367
6568
  function errorMessage2(error) {
6368
6569
  if (error instanceof Error) return error.message.trim();
6369
6570
  if (typeof error === "string") return error.trim();
@@ -6407,18 +6608,114 @@ function withTimeout2(promise, ms) {
6407
6608
  function createGatewayHandlers(ctx) {
6408
6609
  const snapshot = () => create(ListBotsResponseSchema, { bots: listBots(ctx) });
6409
6610
  return {
6410
- [DbkMethod.SESSION_HELLO]: () => create(HelloResponseSchema, {
6411
- protocolVersion: PROTOCOL_VERSION,
6412
- gatewayVersion: GATEWAY_VERSION,
6413
- bots: snapshot().bots
6414
- }),
6415
- [DbkMethod.BOTS_LIST]: snapshot,
6416
- [DbkMethod.TARGETS_LIST]: (request) => listTargets(ctx, request),
6417
- [DbkMethod.TARGETS_GET]: (request) => getTarget(ctx, request),
6418
- [DbkMethod.MESSAGE_SEND]: (request) => sendMessage(ctx, request),
6419
- [DbkMethod.MESSAGE_RECALL]: (request) => recallMessage(ctx, request)
6611
+ [DbkMethod.SESSION_HELLO]: (request) => {
6612
+ const bots = snapshot().bots;
6613
+ ctx.logger.debug(
6614
+ "session.hello: app=%s protocol=%s gateway=%s bots=%d keys=%s",
6615
+ request.appVersion || "-",
6616
+ request.protocolVersion || "-",
6617
+ GATEWAY_VERSION,
6618
+ bots.length,
6619
+ bots.map((bot) => bot.botKey).join(",") || "-"
6620
+ );
6621
+ return create(HelloResponseSchema, {
6622
+ protocolVersion: PROTOCOL_VERSION,
6623
+ gatewayVersion: GATEWAY_VERSION,
6624
+ bots
6625
+ });
6626
+ },
6627
+ [DbkMethod.BOTS_LIST]: () => {
6628
+ const result = snapshot();
6629
+ ctx.logger.debug(
6630
+ "bots.list: count=%d keys=%s",
6631
+ result.bots.length,
6632
+ result.bots.map((bot) => bot.botKey).join(",") || "-"
6633
+ );
6634
+ return result;
6635
+ },
6636
+ [DbkMethod.TARGETS_LIST]: async (request) => {
6637
+ ctx.logger.debug(
6638
+ "targets.list: bot=%s kind=%s",
6639
+ request.botKey.trim() || "*",
6640
+ enumName(TargetKind, request.kind)
6641
+ );
6642
+ const result = await listTargets(ctx, request);
6643
+ ctx.logger.debug(
6644
+ "targets.list: count=%d incomplete=%s",
6645
+ result.targets.length,
6646
+ result.incomplete
6647
+ );
6648
+ return result;
6649
+ },
6650
+ [DbkMethod.TARGETS_GET]: async (request) => {
6651
+ ctx.logger.debug(
6652
+ "targets.get: bot=%s target=%s",
6653
+ request.botKey.trim() || "*",
6654
+ formatTarget(request.target)
6655
+ );
6656
+ const result = await getTarget(ctx, request);
6657
+ ctx.logger.debug(
6658
+ "targets.get: unresolved=%s name=%s bots=%s",
6659
+ result.unresolved,
6660
+ result.target?.name || "-",
6661
+ result.target?.botKeys.join(",") || "-"
6662
+ );
6663
+ return result;
6664
+ },
6665
+ [DbkMethod.MESSAGE_SEND]: async (request) => {
6666
+ ctx.logger.debug(
6667
+ "message.send: bot=%s target=%s reply=%s units=%s",
6668
+ request.botKey.trim() || "-",
6669
+ formatTarget(request.target),
6670
+ request.replyToMessageId.trim() || "-",
6671
+ formatUnits(request)
6672
+ );
6673
+ const result = await sendMessage(ctx, request);
6674
+ ctx.logger.debug(
6675
+ "message.send: status=%s receipts=%d retryable=%s reason=%s",
6676
+ enumName(SendStatus, result.status),
6677
+ result.receipts.length,
6678
+ result.retryable,
6679
+ result.reason || "-"
6680
+ );
6681
+ return result;
6682
+ },
6683
+ [DbkMethod.MESSAGE_RECALL]: async (request) => {
6684
+ ctx.logger.debug(
6685
+ "message.recall: bot=%s target=%s message=%s",
6686
+ request.botKey.trim() || "-",
6687
+ formatTarget(request.target),
6688
+ request.messageId.trim() || "-"
6689
+ );
6690
+ const result = await recallMessage(ctx, request);
6691
+ ctx.logger.debug(
6692
+ "message.recall: status=%s retryable=%s reason=%s",
6693
+ enumName(SendStatus, result.status),
6694
+ result.retryable,
6695
+ result.reason || "-"
6696
+ );
6697
+ return result;
6698
+ }
6420
6699
  };
6421
6700
  }
6701
+ function formatTarget(target) {
6702
+ if (!target) return "-";
6703
+ const kind = enumName(TargetKind, target.kind);
6704
+ const id = target.id.trim() || "-";
6705
+ const guild = target.guildId.trim();
6706
+ return guild ? `${kind}:${id} guild=${guild}` : `${kind}:${id}`;
6707
+ }
6708
+ function formatUnits(request) {
6709
+ if (request.units.length === 0) return "-";
6710
+ return request.units.map((unit) => {
6711
+ if (unit.body.case === "normal") return `normal:${unit.body.value.segments.length}`;
6712
+ if (unit.body.case === "forward") return `forward:${unit.body.value.nodes.length}`;
6713
+ return "empty";
6714
+ }).join(",");
6715
+ }
6716
+ function enumName(map, value) {
6717
+ return map[value] ?? String(value);
6718
+ }
6422
6719
 
6423
6720
  // src/index.ts
6424
6721
  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.4",
7
+ "version": "0.0.6",
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
@@ -1,5 +1,6 @@
1
1
  import { create } from "@bufbuild/protobuf";
2
2
  import { h, type Bot, type Context } from "koishi";
3
+ import type {} from "@koishijs/plugin-http";
3
4
  import { BotStatus, ErrorCode, SendStatus } from "../gen/dbk/v1/common_pb";
4
5
  import {
5
6
  SendReceiptSchema,
@@ -15,6 +16,11 @@ import { DbkRpcError } from "./error";
15
16
 
16
17
  const SEND_TIMEOUT_MS = 25_000;
17
18
  const MENTION_ALL_FALLBACK = "@全体成员";
19
+ const MEDIA_FALLBACK_TYPE = {
20
+ image: "image/png",
21
+ video: "video/mp4",
22
+ audio: "audio/mpeg",
23
+ } as const;
18
24
 
19
25
  type El = ReturnType<typeof h>;
20
26
 
@@ -71,20 +77,29 @@ export async function sendMessage(ctx: Context, request: SendParams): Promise<Se
71
77
  const sendElements = async (elements: El[]): Promise<void> => {
72
78
  if (aborted) return;
73
79
  if (elements.length === 0) {
80
+ ctx.logger.debug("message.send: no sendable segments");
74
81
  failuresRetryable = false;
75
82
  failures.push("no sendable segments");
76
83
  return;
77
84
  }
78
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
+ );
79
92
  try {
80
93
  const ids = await bot.sendMessage(channelId, outgoing, guildId);
81
94
  if (aborted) return;
82
95
  quoteUsed = true;
83
96
  const kept = normalizeMessageIds(ids);
84
97
  if (kept.length === 0) {
98
+ ctx.logger.debug("message.send: adapter returned no message id");
85
99
  unknownReasons.push("send returned no message id");
86
100
  return;
87
101
  }
102
+ ctx.logger.debug("message.send: adapter ids=%s", kept.join(","));
88
103
  for (const messageId of kept) {
89
104
  receipts.push(create(SendReceiptSchema, { messageId, recallable }));
90
105
  }
@@ -92,20 +107,48 @@ export async function sendMessage(ctx: Context, request: SendParams): Promise<Se
92
107
  if (aborted) return;
93
108
  if (isTimeoutError(error)) {
94
109
  quoteUsed = true;
95
- 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);
96
113
  return;
97
114
  }
98
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
+ );
99
121
  failuresRetryable = failuresRetryable && retryable;
100
122
  failures.push(errorMessage(error) || "send failed");
101
123
  }
102
124
  };
103
125
 
126
+ const sendRendered = async (render: Promise<El[]>): Promise<void> => {
127
+ if (aborted) return;
128
+ let elements: El[];
129
+ try {
130
+ elements = await render;
131
+ } catch (error) {
132
+ if (aborted) return;
133
+ if (isTimeoutError(error)) {
134
+ const reason = errorMessage(error) || "media fetch timed out";
135
+ ctx.logger.debug("message.send: media timeout: %s", reason);
136
+ unknownReasons.push(reason);
137
+ return;
138
+ }
139
+ ctx.logger.debug("message.send: media error: %s", errorMessage(error) || "media fetch failed");
140
+ failuresRetryable = failuresRetryable && isRetryableSendError(error);
141
+ failures.push(errorMessage(error) || "media fetch failed");
142
+ return;
143
+ }
144
+ await sendElements(elements);
145
+ };
146
+
104
147
  const work = (async (): Promise<SendResult> => {
105
148
  for (const unit of request.units) {
106
149
  if (aborted) break;
107
150
  if (unit.body.case === "normal") {
108
- await sendElements(segmentsToElements(unit.body.value.segments, { mentionAll }));
151
+ await sendRendered(segmentsToElements(ctx, unit.body.value.segments, { mentionAll }));
109
152
  } else if (unit.body.case === "forward") {
110
153
  // v1: never emit a single merged-forward receipt. Always one send per node.
111
154
  const nodes = unit.body.value.nodes;
@@ -116,7 +159,7 @@ export async function sendMessage(ctx: Context, request: SendParams): Promise<Se
116
159
  }
117
160
  for (const node of nodes) {
118
161
  if (aborted) break;
119
- await sendElements(forwardNodeElements(node, { mentionAll }));
162
+ await sendRendered(forwardNodeElements(ctx, node, { mentionAll }));
120
163
  }
121
164
  } else {
122
165
  failuresRetryable = false;
@@ -148,7 +191,11 @@ export async function sendMessage(ctx: Context, request: SendParams): Promise<Se
148
191
  }
149
192
  }
150
193
 
151
- export function segmentsToElements(segments: Segment[], options?: SegmentRenderOptions): El[] {
194
+ export async function segmentsToElements(
195
+ ctx: Context,
196
+ segments: Segment[],
197
+ options?: SegmentRenderOptions,
198
+ ): Promise<El[]> {
152
199
  const elements: El[] = [];
153
200
  const quoted = new Set<string>();
154
201
 
@@ -169,17 +216,17 @@ export function segmentsToElements(segments: Segment[], options?: SegmentRenderO
169
216
  }
170
217
  case "image": {
171
218
  const uri = segment.body.value.uri.trim();
172
- if (uri) elements.push(h.image(uri));
219
+ if (uri) elements.push(await fetchMediaElement(ctx, "image", uri));
173
220
  break;
174
221
  }
175
222
  case "video": {
176
223
  const uri = segment.body.value.uri.trim();
177
- if (uri) elements.push(h.video(uri));
224
+ if (uri) elements.push(await fetchMediaElement(ctx, "video", uri));
178
225
  break;
179
226
  }
180
227
  case "audio": {
181
228
  const uri = segment.body.value.uri.trim();
182
- if (uri) elements.push(h.audio(uri));
229
+ if (uri) elements.push(await fetchMediaElement(ctx, "audio", uri));
183
230
  break;
184
231
  }
185
232
  case "mention": {
@@ -215,13 +262,67 @@ export function segmentsToElements(segments: Segment[], options?: SegmentRenderO
215
262
  return elements;
216
263
  }
217
264
 
218
- function forwardNodeElements(node: ForwardNode, options?: SegmentRenderOptions): El[] {
219
- const body = segmentsToElements(node.segments, options);
265
+ async function forwardNodeElements(
266
+ ctx: Context,
267
+ node: ForwardNode,
268
+ options?: SegmentRenderOptions,
269
+ ): Promise<El[]> {
270
+ const body = await segmentsToElements(ctx, node.segments, options);
220
271
  const name = node.senderName.trim();
221
272
  if (!name) return body;
222
273
  return [h.text(`${name}\n`), ...body];
223
274
  }
224
275
 
276
+ type MediaKind = keyof typeof MEDIA_FALLBACK_TYPE;
277
+
278
+ async function fetchMediaElement(ctx: Context, kind: MediaKind, uri: string): Promise<El> {
279
+ const http = ctx.http;
280
+ if (typeof http?.file !== "function") {
281
+ throw mediaFetchError(kind, uri, new Error("Koishi http service is required to send media"));
282
+ }
283
+ ctx.logger.debug("message.send: fetch %s %s", kind, displayUri(uri));
284
+ let file: { data?: unknown; mime?: string; type?: string };
285
+ try {
286
+ file = await http.file(uri);
287
+ } catch (error) {
288
+ throw mediaFetchError(kind, uri, error);
289
+ }
290
+ let buffer: Buffer;
291
+ try {
292
+ buffer = mediaBytes(file.data);
293
+ } catch (error) {
294
+ throw mediaFetchError(kind, uri, error);
295
+ }
296
+ if (buffer.length === 0) {
297
+ throw mediaFetchError(kind, uri, new Error("empty body"));
298
+ }
299
+ const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
300
+ ctx.logger.debug("message.send: fetched %s bytes=%d mime=%s", kind, buffer.length, mime);
301
+ if (kind === "image") return h.image(buffer, mime);
302
+ if (kind === "video") return h.video(buffer, mime);
303
+ return h.audio(buffer, mime);
304
+ }
305
+
306
+ function mediaBytes(data: unknown): Buffer {
307
+ if (Buffer.isBuffer(data)) return data;
308
+ if (data instanceof ArrayBuffer) return Buffer.from(data);
309
+ if (ArrayBuffer.isView(data)) {
310
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
311
+ }
312
+ throw new Error("media body is not binary");
313
+ }
314
+
315
+ function mediaFetchError(kind: MediaKind, uri: string, cause: unknown): Error {
316
+ const error = new Error(`failed to fetch ${kind} ${displayUri(uri)}: ${errorMessage(cause) || "unknown error"}`);
317
+ error.cause = cause;
318
+ return error;
319
+ }
320
+
321
+ function displayUri(uri: string): string {
322
+ if (uri.startsWith("data:")) return "data:...";
323
+ return uri.length > 200 ? `${uri.slice(0, 200)}...` : uri;
324
+ }
325
+
225
326
  function withQuote(elements: El[], quoteId: string | undefined): El[] {
226
327
  if (!quoteId) return elements;
227
328
  if (hasQuote(elements, quoteId)) return elements;
@@ -297,30 +398,35 @@ function failed(reason: string, retryable: boolean): SendResult {
297
398
  }
298
399
 
299
400
  function isRetryableSendError(error: unknown): boolean {
300
- const text = errorMessage(error);
301
- const lower = text.toLowerCase();
302
- const status = httpStatusOf(error);
303
-
304
- if (status === 401 || status === 403 || isForbidden(lower)) return false;
305
- if (status === 400 || status === 404) return false;
306
- if (status === 429 || (status !== undefined && status >= 500)) return true;
307
- if (isRetryableNetwork(error, lower)) return true;
308
- if (/\brate.?limit\b|too many requests/.test(lower)) return true;
401
+ for (const item of errorChain(error)) {
402
+ const text = errorMessage(item);
403
+ const lower = text.toLowerCase();
404
+ const status = httpStatusOf(item);
405
+
406
+ if (status === 401 || status === 403 || isForbidden(lower)) return false;
407
+ if (status === 400 || status === 404) return false;
408
+ if (status === 429 || (status !== undefined && status >= 500)) return true;
409
+ if (isRetryableNetwork(item, lower)) return true;
410
+ if (/\brate.?limit\b|too many requests/.test(lower)) return true;
411
+ }
309
412
  return false;
310
413
  }
311
414
 
312
415
  function isTimeoutError(error: unknown): boolean {
313
- const text = errorMessage(error).toLowerCase();
314
- const status = httpStatusOf(error);
315
- if (error instanceof SendTimeoutError) return true;
316
- if (status === 408 || status === 504) return true;
317
- if (error && typeof error === "object") {
318
- const name = (error as { name?: unknown }).name;
319
- if (name === "TimeoutError" || name === "AbortError") return true;
320
- const code = (error as { code?: unknown }).code;
321
- if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ABORT_ERR") return true;
416
+ for (const item of errorChain(error)) {
417
+ const text = errorMessage(item).toLowerCase();
418
+ const status = httpStatusOf(item);
419
+ if (item instanceof SendTimeoutError) return true;
420
+ if (status === 408 || status === 504) return true;
421
+ if (item && typeof item === "object") {
422
+ const name = (item as { name?: unknown }).name;
423
+ if (name === "TimeoutError" || name === "AbortError") return true;
424
+ const code = (item as { code?: unknown }).code;
425
+ if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ABORT_ERR") return true;
426
+ }
427
+ if (text.includes("timed out") || text.includes("timeout")) return true;
322
428
  }
323
- return text.includes("timed out") || text.includes("timeout");
429
+ return false;
324
430
  }
325
431
 
326
432
  function httpStatusOf(error: unknown): number | undefined {
@@ -337,6 +443,20 @@ function httpStatusOf(error: unknown): number | undefined {
337
443
  return undefined;
338
444
  }
339
445
 
446
+ function errorChain(error: unknown): unknown[] {
447
+ const seen = new Set<unknown>();
448
+ const chain: unknown[] = [];
449
+ let current: unknown = error;
450
+ while (current != null && !seen.has(current) && chain.length < 4) {
451
+ seen.add(current);
452
+ chain.push(current);
453
+ current = typeof current === "object" && "cause" in current
454
+ ? (current as { cause?: unknown }).cause
455
+ : undefined;
456
+ }
457
+ return chain;
458
+ }
459
+
340
460
  function errorMessage(error: unknown): string {
341
461
  if (error instanceof Error) return error.message.trim();
342
462
  if (typeof error === "string") return error.trim();
@@ -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(