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 +1 -1
- package/lib/index.js +346 -49
- package/package.json +1 -1
- package/src/dbk/gateway.ts +26 -5
- package/src/dbk/get-target.ts +16 -2
- package/src/dbk/handlers.ts +111 -10
- package/src/dbk/list-targets.ts +23 -2
- package/src/dbk/recall.ts +10 -1
- package/src/dbk/send.ts +148 -28
- package/src/dbk/session.ts +6 -1
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(
|
|
2821
|
-
const prefix = camelToSnakeCase(
|
|
2820
|
+
function findEnumSharedPrefix(enumName2, values) {
|
|
2821
|
+
const prefix = camelToSnakeCase(enumName2) + "_";
|
|
2822
2822
|
for (const value of values) {
|
|
2823
2823
|
if (!value.name.toLowerCase().startsWith(prefix)) {
|
|
2824
2824
|
return void 0;
|
|
@@ -4577,6 +4577,29 @@ function listValueFromJson(listValue, json, ctx) {
|
|
|
4577
4577
|
var file_dbk_v1_common = /* @__PURE__ */ fileDesc("ChNkYmsvdjEvY29tbW9uLnByb3RvEgZkYmsudjEiSAoGVGFyZ2V0EiAKBGtpbmQYASABKA4yEi5kYmsudjEuVGFyZ2V0S2luZBIKCgJpZBgCIAEoCRIQCghndWlsZF9pZBgDIAEoCSI7CghScGNFcnJvchIfCgRjb2RlGAEgASgOMhEuZGJrLnYxLkVycm9yQ29kZRIOCgZkZXRhaWwYAiABKAkqhwEKClRhcmdldEtpbmQSGwoXVEFSR0VUX0tJTkRfVU5TUEVDSUZJRUQQABIUChBUQVJHRVRfS0lORF9VU0VSEAESFQoRVEFSR0VUX0tJTkRfR1JPVVAQAhIXChNUQVJHRVRfS0lORF9DSEFOTkVMEAMSFgoSVEFSR0VUX0tJTkRfVEhSRUFEEAQqdAoJQm90U3RhdHVzEhoKFkJPVF9TVEFUVVNfVU5TUEVDSUZJRUQQABIUChBCT1RfU1RBVFVTX1JFQURZEAESGgoWQk9UX1NUQVRVU19VTkFWQUlMQUJMRRACEhkKFUJPVF9TVEFUVVNfQ09OTkVDVElORxADKocBCgpTZW5kU3RhdHVzEhsKF1NFTkRfU1RBVFVTX1VOU1BFQ0lGSUVEEAASEgoOU0VORF9TVEFUVVNfT0sQARIXChNTRU5EX1NUQVRVU19QQVJUSUFMEAISFwoTU0VORF9TVEFUVVNfVU5LTk9XThADEhYKElNFTkRfU1RBVFVTX0ZBSUxFRBAEKqwBCglFcnJvckNvZGUSGgoWRVJST1JfQ09ERV9VTlNQRUNJRklFRBAAEhsKF0VSUk9SX0NPREVfVU5BVVRIT1JJWkVEEAESFwoTRVJST1JfQ09ERV9QUk9UT0NPTBACEhgKFEVSUk9SX0NPREVfTk9UX0ZPVU5EEAMSGgoWRVJST1JfQ09ERV9VTlNVUFBPUlRFRBAEEhcKE0VSUk9SX0NPREVfSU5URVJOQUwQBSqFAQoNQm90Q2hhbmdlVHlwZRIfChtCT1RfQ0hBTkdFX1RZUEVfVU5TUEVDSUZJRUQQABIZChVCT1RfQ0hBTkdFX1RZUEVfQURERUQQARIbChdCT1RfQ0hBTkdFX1RZUEVfVVBEQVRFRBACEhsKF0JPVF9DSEFOR0VfVFlQRV9SRU1PVkVEEANiBnByb3RvMw");
|
|
4578
4578
|
var TargetSchema = /* @__PURE__ */ messageDesc(file_dbk_v1_common, 0);
|
|
4579
4579
|
var RpcErrorSchema = /* @__PURE__ */ messageDesc(file_dbk_v1_common, 1);
|
|
4580
|
+
var TargetKind = /* @__PURE__ */ ((TargetKind2) => {
|
|
4581
|
+
TargetKind2[TargetKind2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
4582
|
+
TargetKind2[TargetKind2["USER"] = 1] = "USER";
|
|
4583
|
+
TargetKind2[TargetKind2["GROUP"] = 2] = "GROUP";
|
|
4584
|
+
TargetKind2[TargetKind2["CHANNEL"] = 3] = "CHANNEL";
|
|
4585
|
+
TargetKind2[TargetKind2["THREAD"] = 4] = "THREAD";
|
|
4586
|
+
return TargetKind2;
|
|
4587
|
+
})(TargetKind || {});
|
|
4588
|
+
var BotStatus = /* @__PURE__ */ ((BotStatus2) => {
|
|
4589
|
+
BotStatus2[BotStatus2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
4590
|
+
BotStatus2[BotStatus2["READY"] = 1] = "READY";
|
|
4591
|
+
BotStatus2[BotStatus2["UNAVAILABLE"] = 2] = "UNAVAILABLE";
|
|
4592
|
+
BotStatus2[BotStatus2["CONNECTING"] = 3] = "CONNECTING";
|
|
4593
|
+
return BotStatus2;
|
|
4594
|
+
})(BotStatus || {});
|
|
4595
|
+
var SendStatus = /* @__PURE__ */ ((SendStatus2) => {
|
|
4596
|
+
SendStatus2[SendStatus2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
4597
|
+
SendStatus2[SendStatus2["OK"] = 1] = "OK";
|
|
4598
|
+
SendStatus2[SendStatus2["PARTIAL"] = 2] = "PARTIAL";
|
|
4599
|
+
SendStatus2[SendStatus2["UNKNOWN"] = 3] = "UNKNOWN";
|
|
4600
|
+
SendStatus2[SendStatus2["FAILED"] = 4] = "FAILED";
|
|
4601
|
+
return SendStatus2;
|
|
4602
|
+
})(SendStatus || {});
|
|
4580
4603
|
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
4581
4604
|
ErrorCode2[ErrorCode2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
4582
4605
|
ErrorCode2[ErrorCode2["UNAUTHORIZED"] = 1] = "UNAUTHORIZED";
|
|
@@ -4586,6 +4609,13 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
|
4586
4609
|
ErrorCode2[ErrorCode2["INTERNAL"] = 5] = "INTERNAL";
|
|
4587
4610
|
return ErrorCode2;
|
|
4588
4611
|
})(ErrorCode || {});
|
|
4612
|
+
var BotChangeType = /* @__PURE__ */ ((BotChangeType2) => {
|
|
4613
|
+
BotChangeType2[BotChangeType2["UNSPECIFIED"] = 0] = "UNSPECIFIED";
|
|
4614
|
+
BotChangeType2[BotChangeType2["ADDED"] = 1] = "ADDED";
|
|
4615
|
+
BotChangeType2[BotChangeType2["UPDATED"] = 2] = "UPDATED";
|
|
4616
|
+
BotChangeType2[BotChangeType2["REMOVED"] = 3] = "REMOVED";
|
|
4617
|
+
return BotChangeType2;
|
|
4618
|
+
})(BotChangeType || {});
|
|
4589
4619
|
|
|
4590
4620
|
// src/gen/dbk/v1/rpc_pb.ts
|
|
4591
4621
|
var file_dbk_v1_rpc = /* @__PURE__ */ fileDesc("ChBkYmsvdjEvcnBjLnByb3RvEgZkYmsudjEiTAoMSGVsbG9SZXF1ZXN0Eg0KBXRva2VuGAEgASgJEhMKC2FwcF92ZXJzaW9uGAIgASgJEhgKEHByb3RvY29sX3ZlcnNpb24YAyABKAkiXQoNSGVsbG9SZXNwb25zZRIYChBwcm90b2NvbF92ZXJzaW9uGAEgASgJEhcKD2dhdGV3YXlfdmVyc2lvbhgCIAEoCRIZCgRib3RzGAMgAygLMgsuZGJrLnYxLkJvdCIRCg9MaXN0Qm90c1JlcXVlc3QiLQoQTGlzdEJvdHNSZXNwb25zZRIZCgRib3RzGAEgAygLMgsuZGJrLnYxLkJvdCKMAQoDQm90Eg8KB2JvdF9rZXkYASABKAkSEAoIcGxhdGZvcm0YAiABKAkSDwoHc2VsZl9pZBgDIAEoCRIMCgRuYW1lGAQgASgJEg4KBmF2YXRhchgFIAEoCRIhCgZzdGF0dXMYBiABKA4yES5kYmsudjEuQm90U3RhdHVzEhAKCGZlYXR1cmVzGAcgAygJIlAKD0JvdENoYW5nZWRFdmVudBIjCgR0eXBlGAEgASgOMhUuZGJrLnYxLkJvdENoYW5nZVR5cGUSGAoDYm90GAIgASgLMgsuZGJrLnYxLkJvdCJHChJMaXN0VGFyZ2V0c1JlcXVlc3QSDwoHYm90X2tleRgBIAEoCRIgCgRraW5kGAIgASgOMhIuZGJrLnYxLlRhcmdldEtpbmQiTgoTTGlzdFRhcmdldHNSZXNwb25zZRIjCgd0YXJnZXRzGAEgAygLMhIuZGJrLnYxLlRhcmdldEluZm8SEgoKaW5jb21wbGV0ZRgCIAEoCCJDChBHZXRUYXJnZXRSZXF1ZXN0Eg8KB2JvdF9rZXkYASABKAkSHgoGdGFyZ2V0GAIgASgLMg4uZGJrLnYxLlRhcmdldCJLChFHZXRUYXJnZXRSZXNwb25zZRIiCgZ0YXJnZXQYASABKAsyEi5kYmsudjEuVGFyZ2V0SW5mbxISCgp1bnJlc29sdmVkGAIgASgIIoIBCgpUYXJnZXRJbmZvEh4KBnRhcmdldBgBIAEoCzIOLmRiay52MS5UYXJnZXQSDAoEbmFtZRgCIAEoCRIQCghndWlsZF9pZBgDIAEoCRISCgpndWlsZF9uYW1lGAQgASgJEhAKCGJvdF9rZXlzGAUgAygJEg4KBmF2YXRhchgGIAEoCSJ7CgpTZW5kUGFyYW1zEg8KB2JvdF9rZXkYASABKAkSHgoGdGFyZ2V0GAIgASgLMg4uZGJrLnYxLlRhcmdldBIbChNyZXBseV90b19tZXNzYWdlX2lkGAMgASgJEh8KBXVuaXRzGAQgAygLMhAuZGJrLnYxLlNlbmRVbml0ImAKCFNlbmRVbml0EiQKBm5vcm1hbBgBIAEoCzISLmRiay52MS5Ob3JtYWxVbml0SAASJgoHZm9yd2FyZBgCIAEoCzITLmRiay52MS5Gb3J3YXJkVW5pdEgAQgYKBGJvZHkiLwoKTm9ybWFsVW5pdBIhCghzZWdtZW50cxgBIAMoCzIPLmRiay52MS5TZWdtZW50IjEKC0ZvcndhcmRVbml0EiIKBW5vZGVzGAEgAygLMhMuZGJrLnYxLkZvcndhcmROb2RlIn0KC0ZvcndhcmROb2RlEhEKCXNlbmRlcl9pZBgBIAEoCRITCgtzZW5kZXJfbmFtZRgCIAEoCRIVCg1zZW5kZXJfYXZhdGFyGAMgASgJEgwKBHRpbWUYBCABKAMSIQoIc2VnbWVudHMYBSADKAsyDy5kYmsudjEuU2VnbWVudCL/AgoHU2VnbWVudBIjCgR0ZXh0GAEgASgLMhMuZGJrLnYxLlRleHRTZWdtZW50SAASJQoFaW1hZ2UYAiABKAsyFC5kYmsudjEuSW1hZ2VTZWdtZW50SAASJQoFdmlkZW8YAyABKAsyFC5kYmsudjEuVmlkZW9TZWdtZW50SAASJQoFYXVkaW8YBCABKAsyFC5kYmsudjEuQXVkaW9TZWdtZW50SAASKQoHbWVudGlvbhgFIAEoCzIWLmRiay52MS5NZW50aW9uU2VnbWVudEgAEjAKC21lbnRpb25fYWxsGAYgASgLMhkuZGJrLnYxLk1lbnRpb25BbGxTZWdtZW50SAASJQoFcmVwbHkYByABKAsyFC5kYmsudjEuUmVwbHlTZWdtZW50SAASIwoEbGluaxgIIAEoCzITLmRiay52MS5MaW5rU2VnbWVudEgAEikKB3Vua25vd24YCSABKAsyFi5kYmsudjEuVW5rbm93blNlZ21lbnRIAEIGCgRib2R5IhsKC1RleHRTZWdtZW50EgwKBHRleHQYASABKAkiNgoMSW1hZ2VTZWdtZW50EgsKA3VyaRgBIAEoCRIMCgRmaWxlGAIgASgJEgsKA2FsdBgDIAEoCSI2CgxWaWRlb1NlZ21lbnQSCwoDdXJpGAEgASgJEgwKBGZpbGUYAiABKAkSCwoDYWx0GAMgASgJIjYKDEF1ZGlvU2VnbWVudBILCgN1cmkYASABKAkSDAoEZmlsZRgCIAEoCRILCgNhbHQYAyABKAkiHAoOTWVudGlvblNlZ21lbnQSCgoCaWQYASABKAkiEwoRTWVudGlvbkFsbFNlZ21lbnQiIgoMUmVwbHlTZWdtZW50EhIKCm1lc3NhZ2VfaWQYASABKAkiKQoLTGlua1NlZ21lbnQSCwoDdXJsGAEgASgJEg0KBXRpdGxlGAIgASgJIisKDlVua25vd25TZWdtZW50EgwKBHR5cGUYASABKAkSCwoDcmF3GAIgASgJInoKClNlbmRSZXN1bHQSIgoGc3RhdHVzGAEgASgOMhIuZGJrLnYxLlNlbmRTdGF0dXMSDgoGcmVhc29uGAIgASgJEhEKCXJldHJ5YWJsZRgDIAEoCBIlCghyZWNlaXB0cxgEIAMoCzITLmRiay52MS5TZW5kUmVjZWlwdCI1CgtTZW5kUmVjZWlwdBISCgptZXNzYWdlX2lkGAEgASgJEhIKCnJlY2FsbGFibGUYAiABKAgiUwoMUmVjYWxsUGFyYW1zEg8KB2JvdF9rZXkYASABKAkSHgoGdGFyZ2V0GAIgASgLMg4uZGJrLnYxLlRhcmdldBISCgptZXNzYWdlX2lkGAMgASgJIlUKDFJlY2FsbFJlc3VsdBIiCgZzdGF0dXMYASABKA4yEi5kYmsudjEuU2VuZFN0YXR1cxIOCgZyZWFzb24YAiABKAkSEQoJcmV0cnlhYmxlGAMgASgIIvsBCg9JbmNvbWluZ01lc3NhZ2USDwoHYm90X2tleRgBIAEoCRIQCghwbGF0Zm9ybRgCIAEoCRIeCgZ0YXJnZXQYAyABKAsyDi5kYmsudjEuVGFyZ2V0EhEKCXNlbmRlcl9pZBgEIAEoCRISCgptZXNzYWdlX2lkGAUgASgJEhEKCXRpbWVzdGFtcBgGIAEoAxIMCgR0ZXh0GAcgASgJEhsKE3JlcGx5X3RvX21lc3NhZ2VfaWQYCCABKAkSEAoIbWVudGlvbnMYCSADKAkSIQoIc2VnbWVudHMYCiADKAsyDy5kYmsudjEuU2VnbWVudBILCgNyYXcYCyABKAliBnByb3RvMw", [file_dbk_v1_common]);
|
|
@@ -5001,8 +5031,12 @@ var DbkGatewaySession = class {
|
|
|
5001
5031
|
this.fail("DBK frame could not be decoded");
|
|
5002
5032
|
}
|
|
5003
5033
|
emit(method, event) {
|
|
5004
|
-
if (!this.isHandshook)
|
|
5034
|
+
if (!this.isHandshook) {
|
|
5035
|
+
this.options.logger.debug("drop EVENT before handshake: method=%s", method);
|
|
5036
|
+
return;
|
|
5037
|
+
}
|
|
5005
5038
|
this.seq += 1n;
|
|
5039
|
+
this.options.logger.debug("DBK EVENT %s seq=%s", method, this.seq.toString());
|
|
5006
5040
|
this.enqueueFrame(
|
|
5007
5041
|
createFrame({
|
|
5008
5042
|
op: 4 /* EVENT */,
|
|
@@ -5056,6 +5090,7 @@ var DbkGatewaySession = class {
|
|
|
5056
5090
|
this.replyError(frame.id, 4 /* UNSUPPORTED */, `unknown method: ${frame.method}`);
|
|
5057
5091
|
return;
|
|
5058
5092
|
}
|
|
5093
|
+
this.options.logger.debug("DBK CALL %s id=%s", frame.method, frame.id);
|
|
5059
5094
|
try {
|
|
5060
5095
|
const payload = await this.dispatchCall(frame.method, frame.payload);
|
|
5061
5096
|
this.enqueueFrame(
|
|
@@ -5217,11 +5252,19 @@ var DbkGateway = class {
|
|
|
5217
5252
|
return this.session?.isHandshook ?? false;
|
|
5218
5253
|
}
|
|
5219
5254
|
emit(method, event) {
|
|
5220
|
-
this.session
|
|
5255
|
+
if (!this.session) {
|
|
5256
|
+
this.ctx.logger.debug("drop EVENT without session: method=%s", method);
|
|
5257
|
+
return;
|
|
5258
|
+
}
|
|
5259
|
+
this.session.emit(method, event);
|
|
5221
5260
|
}
|
|
5222
5261
|
startForward() {
|
|
5223
5262
|
const path = normalizePath(this.config.path);
|
|
5224
|
-
this.ctx.logger.info(
|
|
5263
|
+
this.ctx.logger.info(
|
|
5264
|
+
"DBK forward WebSocket mounted on Koishi HTTP at %s encoding=%s",
|
|
5265
|
+
path,
|
|
5266
|
+
this.config.encoding
|
|
5267
|
+
);
|
|
5225
5268
|
this.ctx.server.ws(path, (socket) => {
|
|
5226
5269
|
this.attach("replaced", socket);
|
|
5227
5270
|
});
|
|
@@ -5237,11 +5280,16 @@ var DbkGateway = class {
|
|
|
5237
5280
|
this.session = void 0;
|
|
5238
5281
|
this.socket?.close(1e3, "plugin stop");
|
|
5239
5282
|
this.socket = void 0;
|
|
5283
|
+
this.ctx.logger.debug("DBK gateway stopped");
|
|
5240
5284
|
}
|
|
5241
5285
|
connectReverse() {
|
|
5242
5286
|
if (this.closing) return;
|
|
5243
5287
|
const url = `ws://${this.config.host}:${this.config.port}`;
|
|
5244
|
-
this.ctx.logger.info(
|
|
5288
|
+
this.ctx.logger.info(
|
|
5289
|
+
"DBK reverse WebSocket connecting to %s encoding=%s",
|
|
5290
|
+
url,
|
|
5291
|
+
this.config.encoding
|
|
5292
|
+
);
|
|
5245
5293
|
const socket = this.ctx.http.ws(url);
|
|
5246
5294
|
const onOpen = () => {
|
|
5247
5295
|
this.reconnectAttempts = 0;
|
|
@@ -5279,6 +5327,7 @@ var DbkGateway = class {
|
|
|
5279
5327
|
});
|
|
5280
5328
|
this.session = session;
|
|
5281
5329
|
attachSocket(socket, session, this.config.encoding);
|
|
5330
|
+
this.ctx.logger.debug("DBK connection attached encoding=%s", this.config.encoding);
|
|
5282
5331
|
}
|
|
5283
5332
|
onReverseClosed() {
|
|
5284
5333
|
this.session?.close("ws close");
|
|
@@ -5302,9 +5351,16 @@ var DbkGateway = class {
|
|
|
5302
5351
|
function watchBots(ctx, gateway) {
|
|
5303
5352
|
const emit = (type, bot) => {
|
|
5304
5353
|
if (bot.hidden && type !== 3 /* REMOVED */) return;
|
|
5354
|
+
const mapped = toDbkBot(bot);
|
|
5355
|
+
ctx.logger.debug(
|
|
5356
|
+
"bot.changed: %s %s status=%s",
|
|
5357
|
+
BotChangeType[type] ?? type,
|
|
5358
|
+
mapped.botKey || "-",
|
|
5359
|
+
BotStatus[mapped.status] ?? mapped.status
|
|
5360
|
+
);
|
|
5305
5361
|
gateway.emit(DbkEvent.BOT_CHANGED, create(BotChangedEventSchema, {
|
|
5306
5362
|
type,
|
|
5307
|
-
bot:
|
|
5363
|
+
bot: mapped
|
|
5308
5364
|
}));
|
|
5309
5365
|
};
|
|
5310
5366
|
ctx.on("bot-added", (bot) => emit(1 /* ADDED */, bot));
|
|
@@ -5383,12 +5439,26 @@ async function getTarget(ctx, request) {
|
|
|
5383
5439
|
throw new DbkRpcError(3 /* NOT_FOUND */, `bot not found: ${botKey}`);
|
|
5384
5440
|
}
|
|
5385
5441
|
const info = await resolveOnBot(bot, target);
|
|
5386
|
-
|
|
5442
|
+
if (info) {
|
|
5443
|
+
ctx.logger.debug("targets.get: resolved on %s kind=%s id=%s", botKey, TargetKind[info.target?.kind ?? 0], info.target?.id);
|
|
5444
|
+
return resolved(info);
|
|
5445
|
+
}
|
|
5446
|
+
ctx.logger.debug("targets.get: unresolved on %s id=%s", botKey, target?.id.trim() || "-");
|
|
5447
|
+
return unresolved(target);
|
|
5387
5448
|
}
|
|
5388
5449
|
for (const bot of listSearchableBots(ctx)) {
|
|
5389
5450
|
const info = await resolveOnBot(bot, target);
|
|
5390
|
-
if (info)
|
|
5451
|
+
if (info) {
|
|
5452
|
+
ctx.logger.debug(
|
|
5453
|
+
"targets.get: resolved on %s kind=%s id=%s",
|
|
5454
|
+
botKeyOf(bot),
|
|
5455
|
+
TargetKind[info.target?.kind ?? 0],
|
|
5456
|
+
info.target?.id
|
|
5457
|
+
);
|
|
5458
|
+
return resolved(info);
|
|
5459
|
+
}
|
|
5391
5460
|
}
|
|
5461
|
+
ctx.logger.debug("targets.get: unresolved id=%s", target?.id.trim() || "-");
|
|
5392
5462
|
return unresolved(target);
|
|
5393
5463
|
}
|
|
5394
5464
|
function findBot(ctx, botKey) {
|
|
@@ -5567,7 +5637,10 @@ async function listTargets(ctx, request) {
|
|
|
5567
5637
|
const merged = /* @__PURE__ */ new Map();
|
|
5568
5638
|
let incomplete = false;
|
|
5569
5639
|
for (const bot of bots) {
|
|
5570
|
-
if (isQq(bot))
|
|
5640
|
+
if (isQq(bot)) {
|
|
5641
|
+
ctx.logger?.debug("targets.list: skip qq bot %s", botKeyOf2(bot));
|
|
5642
|
+
continue;
|
|
5643
|
+
}
|
|
5571
5644
|
const listed = await listTargetsForBot(ctx, bot, kindFilter);
|
|
5572
5645
|
incomplete = incomplete || listed.incomplete;
|
|
5573
5646
|
for (const target of listed.targets) {
|
|
@@ -5616,6 +5689,12 @@ async function listTargetsForBot(ctx, bot, kindFilter) {
|
|
|
5616
5689
|
const nested = hasNestedChannels(bot);
|
|
5617
5690
|
const supported = botTargetKinds(bot);
|
|
5618
5691
|
if (kindFilter !== 0 /* UNSPECIFIED */ && supported.length > 0 && !supported.includes(kindFilter)) {
|
|
5692
|
+
ctx.logger?.debug(
|
|
5693
|
+
"targets.list: %s skip kind=%s supported=%s",
|
|
5694
|
+
botKey,
|
|
5695
|
+
TargetKind[kindFilter] ?? kindFilter,
|
|
5696
|
+
supported.map((kind) => TargetKind[kind] ?? kind).join(",")
|
|
5697
|
+
);
|
|
5619
5698
|
return { targets, incomplete: false };
|
|
5620
5699
|
}
|
|
5621
5700
|
let incomplete = hasGuildSurface(bot) && !nested;
|
|
@@ -5625,7 +5704,12 @@ async function listTargetsForBot(ctx, bot, kindFilter) {
|
|
|
5625
5704
|
const guilds = await paginate((next) => callBotList(bot, "getGuildList", next));
|
|
5626
5705
|
if (guilds.unsupported || guilds.failed || guilds.truncated) incomplete = true;
|
|
5627
5706
|
if (guilds.failed && !guilds.unsupported) {
|
|
5628
|
-
ctx.logger?.debug(
|
|
5707
|
+
ctx.logger?.debug(
|
|
5708
|
+
"targets.list: %s getGuildList failed items=%d truncated=%s",
|
|
5709
|
+
botKey,
|
|
5710
|
+
guilds.items.length,
|
|
5711
|
+
guilds.truncated
|
|
5712
|
+
);
|
|
5629
5713
|
}
|
|
5630
5714
|
let channelMode = nested ? "unknown" : "flat";
|
|
5631
5715
|
for (const guild of guilds.items) {
|
|
@@ -5694,6 +5778,13 @@ async function listTargetsForBot(ctx, bot, kindFilter) {
|
|
|
5694
5778
|
incomplete = true;
|
|
5695
5779
|
}
|
|
5696
5780
|
}
|
|
5781
|
+
ctx.logger?.debug(
|
|
5782
|
+
"targets.list: %s listed=%d incomplete=%s nested=%s",
|
|
5783
|
+
botKey,
|
|
5784
|
+
targets.length,
|
|
5785
|
+
incomplete,
|
|
5786
|
+
nested
|
|
5787
|
+
);
|
|
5697
5788
|
return { targets, incomplete };
|
|
5698
5789
|
}
|
|
5699
5790
|
function classifyGuildChannels(channels, guildId, guildName, guildAvatar, botKey) {
|
|
@@ -5938,15 +6029,24 @@ async function recallMessage(ctx, params) {
|
|
|
5938
6029
|
if (!channelId) {
|
|
5939
6030
|
return failed("target id is empty", false);
|
|
5940
6031
|
}
|
|
6032
|
+
ctx.logger.debug("message.recall: deleteMessage channel=%s message=%s", channelId, messageId);
|
|
5941
6033
|
try {
|
|
5942
6034
|
await withTimeout(Promise.resolve(bot.deleteMessage(channelId, messageId)), RECALL_TIMEOUT_MS);
|
|
6035
|
+
ctx.logger.debug("message.recall: adapter ok");
|
|
5943
6036
|
return create(RecallResultSchema, {
|
|
5944
6037
|
status: 1 /* OK */,
|
|
5945
6038
|
reason: "",
|
|
5946
6039
|
retryable: false
|
|
5947
6040
|
});
|
|
5948
6041
|
} catch (error) {
|
|
5949
|
-
|
|
6042
|
+
const result = classifyRecallError(error);
|
|
6043
|
+
ctx.logger.debug(
|
|
6044
|
+
"message.recall: adapter error status=%s retryable=%s: %s",
|
|
6045
|
+
SendStatus[result.status] ?? result.status,
|
|
6046
|
+
result.retryable,
|
|
6047
|
+
result.reason || errorMessage(error) || "message.recall failed"
|
|
6048
|
+
);
|
|
6049
|
+
return result;
|
|
5950
6050
|
}
|
|
5951
6051
|
}
|
|
5952
6052
|
function findBot3(ctx, botKey) {
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
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
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
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
|
|
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]: () =>
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
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
package/src/dbk/gateway.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { create } from "@bufbuild/protobuf";
|
|
2
2
|
import type { Context } from "koishi";
|
|
3
|
-
import { BotChangeType } from "../gen/dbk/v1/common_pb";
|
|
3
|
+
import { BotChangeType, BotStatus } from "../gen/dbk/v1/common_pb";
|
|
4
4
|
import { BotChangedEventSchema } from "../gen/dbk/v1/rpc_pb";
|
|
5
5
|
import { toDbkBot } from "./bots";
|
|
6
6
|
import { DbkEvent, type DbkEncoding, type DbkEventMap, type DbkGatewayHandlers } from "./protocol";
|
|
@@ -46,12 +46,20 @@ export class DbkGateway {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
emit<K extends keyof DbkEventMap>(method: K, event: DbkEventMap[K]): void {
|
|
49
|
-
this.session
|
|
49
|
+
if (!this.session) {
|
|
50
|
+
this.ctx.logger.debug("drop EVENT without session: method=%s", method);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.session.emit(method, event);
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
startForward(): void {
|
|
53
57
|
const path = normalizePath(this.config.path);
|
|
54
|
-
this.ctx.logger.info(
|
|
58
|
+
this.ctx.logger.info(
|
|
59
|
+
"DBK forward WebSocket mounted on Koishi HTTP at %s encoding=%s",
|
|
60
|
+
path,
|
|
61
|
+
this.config.encoding,
|
|
62
|
+
);
|
|
55
63
|
this.ctx.server.ws(path, (socket) => {
|
|
56
64
|
this.attach("replaced", socket as DbkSocket);
|
|
57
65
|
});
|
|
@@ -69,12 +77,17 @@ export class DbkGateway {
|
|
|
69
77
|
this.session = undefined;
|
|
70
78
|
this.socket?.close(1000, "plugin stop");
|
|
71
79
|
this.socket = undefined;
|
|
80
|
+
this.ctx.logger.debug("DBK gateway stopped");
|
|
72
81
|
}
|
|
73
82
|
|
|
74
83
|
private connectReverse(): void {
|
|
75
84
|
if (this.closing) return;
|
|
76
85
|
const url = `ws://${this.config.host}:${this.config.port}`;
|
|
77
|
-
this.ctx.logger.info(
|
|
86
|
+
this.ctx.logger.info(
|
|
87
|
+
"DBK reverse WebSocket connecting to %s encoding=%s",
|
|
88
|
+
url,
|
|
89
|
+
this.config.encoding,
|
|
90
|
+
);
|
|
78
91
|
const socket = this.ctx.http.ws(url) as DbkSocket;
|
|
79
92
|
const onOpen = () => {
|
|
80
93
|
this.reconnectAttempts = 0;
|
|
@@ -113,6 +126,7 @@ export class DbkGateway {
|
|
|
113
126
|
});
|
|
114
127
|
this.session = session;
|
|
115
128
|
attachSocket(socket, session, this.config.encoding);
|
|
129
|
+
this.ctx.logger.debug("DBK connection attached encoding=%s", this.config.encoding);
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
private onReverseClosed(): void {
|
|
@@ -139,9 +153,16 @@ export class DbkGateway {
|
|
|
139
153
|
export function watchBots(ctx: Context, gateway: DbkGateway): void {
|
|
140
154
|
const emit = (type: BotChangeType, bot: Parameters<typeof toDbkBot>[0]) => {
|
|
141
155
|
if (bot.hidden && type !== BotChangeType.REMOVED) return;
|
|
156
|
+
const mapped = toDbkBot(bot);
|
|
157
|
+
ctx.logger.debug(
|
|
158
|
+
"bot.changed: %s %s status=%s",
|
|
159
|
+
BotChangeType[type] ?? type,
|
|
160
|
+
mapped.botKey || "-",
|
|
161
|
+
BotStatus[mapped.status] ?? mapped.status,
|
|
162
|
+
);
|
|
142
163
|
gateway.emit(DbkEvent.BOT_CHANGED, create(BotChangedEventSchema, {
|
|
143
164
|
type,
|
|
144
|
-
bot:
|
|
165
|
+
bot: mapped,
|
|
145
166
|
}));
|
|
146
167
|
};
|
|
147
168
|
ctx.on("bot-added", (bot) => emit(BotChangeType.ADDED, bot));
|
package/src/dbk/get-target.ts
CHANGED
|
@@ -59,14 +59,28 @@ export async function getTarget(ctx: Context, request: GetTargetRequest): Promis
|
|
|
59
59
|
throw new DbkRpcError(ErrorCode.NOT_FOUND, `bot not found: ${botKey}`);
|
|
60
60
|
}
|
|
61
61
|
const info = await resolveOnBot(bot, target);
|
|
62
|
-
|
|
62
|
+
if (info) {
|
|
63
|
+
ctx.logger.debug("targets.get: resolved on %s kind=%s id=%s", botKey, TargetKind[info.target?.kind ?? 0], info.target?.id);
|
|
64
|
+
return resolved(info);
|
|
65
|
+
}
|
|
66
|
+
ctx.logger.debug("targets.get: unresolved on %s id=%s", botKey, target?.id.trim() || "-");
|
|
67
|
+
return unresolved(target);
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
// Empty bot_key: search non-hidden, non-qq bots in ctx.bots order; first successful resolve wins.
|
|
66
71
|
for (const bot of listSearchableBots(ctx)) {
|
|
67
72
|
const info = await resolveOnBot(bot, target);
|
|
68
|
-
if (info)
|
|
73
|
+
if (info) {
|
|
74
|
+
ctx.logger.debug(
|
|
75
|
+
"targets.get: resolved on %s kind=%s id=%s",
|
|
76
|
+
botKeyOf(bot),
|
|
77
|
+
TargetKind[info.target?.kind ?? 0],
|
|
78
|
+
info.target?.id,
|
|
79
|
+
);
|
|
80
|
+
return resolved(info);
|
|
81
|
+
}
|
|
69
82
|
}
|
|
83
|
+
ctx.logger.debug("targets.get: unresolved id=%s", target?.id.trim() || "-");
|
|
70
84
|
return unresolved(target);
|
|
71
85
|
}
|
|
72
86
|
|
package/src/dbk/handlers.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { create } from "@bufbuild/protobuf";
|
|
2
2
|
import type { Context } from "koishi";
|
|
3
|
+
import { SendStatus, TargetKind, type Target } from "../gen/dbk/v1/common_pb";
|
|
3
4
|
import {
|
|
4
5
|
HelloResponseSchema,
|
|
5
6
|
ListBotsResponseSchema,
|
|
7
|
+
type SendParams,
|
|
6
8
|
} from "../gen/dbk/v1/rpc_pb";
|
|
7
9
|
import { listBots } from "./bots";
|
|
8
10
|
import { getTarget } from "./get-target";
|
|
@@ -15,15 +17,114 @@ export function createGatewayHandlers(ctx: Context): DbkGatewayHandlers {
|
|
|
15
17
|
const snapshot = () => create(ListBotsResponseSchema, { bots: listBots(ctx) });
|
|
16
18
|
|
|
17
19
|
return {
|
|
18
|
-
[DbkMethod.SESSION_HELLO]: () =>
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
[DbkMethod.SESSION_HELLO]: (request) => {
|
|
21
|
+
const bots = snapshot().bots;
|
|
22
|
+
ctx.logger.debug(
|
|
23
|
+
"session.hello: app=%s protocol=%s gateway=%s bots=%d keys=%s",
|
|
24
|
+
request.appVersion || "-",
|
|
25
|
+
request.protocolVersion || "-",
|
|
26
|
+
GATEWAY_VERSION,
|
|
27
|
+
bots.length,
|
|
28
|
+
bots.map((bot) => bot.botKey).join(",") || "-",
|
|
29
|
+
);
|
|
30
|
+
return create(HelloResponseSchema, {
|
|
31
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
32
|
+
gatewayVersion: GATEWAY_VERSION,
|
|
33
|
+
bots,
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
[DbkMethod.BOTS_LIST]: () => {
|
|
37
|
+
const result = snapshot();
|
|
38
|
+
ctx.logger.debug(
|
|
39
|
+
"bots.list: count=%d keys=%s",
|
|
40
|
+
result.bots.length,
|
|
41
|
+
result.bots.map((bot) => bot.botKey).join(",") || "-",
|
|
42
|
+
);
|
|
43
|
+
return result;
|
|
44
|
+
},
|
|
45
|
+
[DbkMethod.TARGETS_LIST]: async (request) => {
|
|
46
|
+
ctx.logger.debug(
|
|
47
|
+
"targets.list: bot=%s kind=%s",
|
|
48
|
+
request.botKey.trim() || "*",
|
|
49
|
+
enumName(TargetKind, request.kind),
|
|
50
|
+
);
|
|
51
|
+
const result = await listTargets(ctx, request);
|
|
52
|
+
ctx.logger.debug(
|
|
53
|
+
"targets.list: count=%d incomplete=%s",
|
|
54
|
+
result.targets.length,
|
|
55
|
+
result.incomplete,
|
|
56
|
+
);
|
|
57
|
+
return result;
|
|
58
|
+
},
|
|
59
|
+
[DbkMethod.TARGETS_GET]: async (request) => {
|
|
60
|
+
ctx.logger.debug(
|
|
61
|
+
"targets.get: bot=%s target=%s",
|
|
62
|
+
request.botKey.trim() || "*",
|
|
63
|
+
formatTarget(request.target),
|
|
64
|
+
);
|
|
65
|
+
const result = await getTarget(ctx, request);
|
|
66
|
+
ctx.logger.debug(
|
|
67
|
+
"targets.get: unresolved=%s name=%s bots=%s",
|
|
68
|
+
result.unresolved,
|
|
69
|
+
result.target?.name || "-",
|
|
70
|
+
result.target?.botKeys.join(",") || "-",
|
|
71
|
+
);
|
|
72
|
+
return result;
|
|
73
|
+
},
|
|
74
|
+
[DbkMethod.MESSAGE_SEND]: async (request) => {
|
|
75
|
+
ctx.logger.debug(
|
|
76
|
+
"message.send: bot=%s target=%s reply=%s units=%s",
|
|
77
|
+
request.botKey.trim() || "-",
|
|
78
|
+
formatTarget(request.target),
|
|
79
|
+
request.replyToMessageId.trim() || "-",
|
|
80
|
+
formatUnits(request),
|
|
81
|
+
);
|
|
82
|
+
const result = await sendMessage(ctx, request);
|
|
83
|
+
ctx.logger.debug(
|
|
84
|
+
"message.send: status=%s receipts=%d retryable=%s reason=%s",
|
|
85
|
+
enumName(SendStatus, result.status),
|
|
86
|
+
result.receipts.length,
|
|
87
|
+
result.retryable,
|
|
88
|
+
result.reason || "-",
|
|
89
|
+
);
|
|
90
|
+
return result;
|
|
91
|
+
},
|
|
92
|
+
[DbkMethod.MESSAGE_RECALL]: async (request) => {
|
|
93
|
+
ctx.logger.debug(
|
|
94
|
+
"message.recall: bot=%s target=%s message=%s",
|
|
95
|
+
request.botKey.trim() || "-",
|
|
96
|
+
formatTarget(request.target),
|
|
97
|
+
request.messageId.trim() || "-",
|
|
98
|
+
);
|
|
99
|
+
const result = await recallMessage(ctx, request);
|
|
100
|
+
ctx.logger.debug(
|
|
101
|
+
"message.recall: status=%s retryable=%s reason=%s",
|
|
102
|
+
enumName(SendStatus, result.status),
|
|
103
|
+
result.retryable,
|
|
104
|
+
result.reason || "-",
|
|
105
|
+
);
|
|
106
|
+
return result;
|
|
107
|
+
},
|
|
28
108
|
};
|
|
29
109
|
}
|
|
110
|
+
|
|
111
|
+
function formatTarget(target: Target | undefined): string {
|
|
112
|
+
if (!target) return "-";
|
|
113
|
+
const kind = enumName(TargetKind, target.kind);
|
|
114
|
+
const id = target.id.trim() || "-";
|
|
115
|
+
const guild = target.guildId.trim();
|
|
116
|
+
return guild ? `${kind}:${id} guild=${guild}` : `${kind}:${id}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatUnits(request: SendParams): string {
|
|
120
|
+
if (request.units.length === 0) return "-";
|
|
121
|
+
return request.units.map((unit) => {
|
|
122
|
+
if (unit.body.case === "normal") return `normal:${unit.body.value.segments.length}`;
|
|
123
|
+
if (unit.body.case === "forward") return `forward:${unit.body.value.nodes.length}`;
|
|
124
|
+
return "empty";
|
|
125
|
+
}).join(",");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function enumName(map: Record<number, string>, value: number): string {
|
|
129
|
+
return map[value] ?? String(value);
|
|
130
|
+
}
|
package/src/dbk/list-targets.ts
CHANGED
|
@@ -60,7 +60,10 @@ export async function listTargets(ctx: Context, request: ListTargetsRequest): Pr
|
|
|
60
60
|
let incomplete = false;
|
|
61
61
|
|
|
62
62
|
for (const bot of bots) {
|
|
63
|
-
if (isQq(bot))
|
|
63
|
+
if (isQq(bot)) {
|
|
64
|
+
ctx.logger?.debug("targets.list: skip qq bot %s", botKeyOf(bot));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
64
67
|
const listed = await listTargetsForBot(ctx, bot, kindFilter);
|
|
65
68
|
incomplete = incomplete || listed.incomplete;
|
|
66
69
|
for (const target of listed.targets) {
|
|
@@ -120,6 +123,12 @@ async function listTargetsForBot(
|
|
|
120
123
|
const nested = hasNestedChannels(bot);
|
|
121
124
|
const supported = botTargetKinds(bot);
|
|
122
125
|
if (kindFilter !== TargetKind.UNSPECIFIED && supported.length > 0 && !supported.includes(kindFilter)) {
|
|
126
|
+
ctx.logger?.debug(
|
|
127
|
+
"targets.list: %s skip kind=%s supported=%s",
|
|
128
|
+
botKey,
|
|
129
|
+
TargetKind[kindFilter] ?? kindFilter,
|
|
130
|
+
supported.map((kind) => TargetKind[kind] ?? kind).join(","),
|
|
131
|
+
);
|
|
123
132
|
return { targets, incomplete: false };
|
|
124
133
|
}
|
|
125
134
|
|
|
@@ -134,7 +143,12 @@ async function listTargetsForBot(
|
|
|
134
143
|
const guilds = await paginate<GuildLike>((next) => callBotList(bot, "getGuildList", next));
|
|
135
144
|
if (guilds.unsupported || guilds.failed || guilds.truncated) incomplete = true;
|
|
136
145
|
if (guilds.failed && !guilds.unsupported) {
|
|
137
|
-
ctx.logger?.debug(
|
|
146
|
+
ctx.logger?.debug(
|
|
147
|
+
"targets.list: %s getGuildList failed items=%d truncated=%s",
|
|
148
|
+
botKey,
|
|
149
|
+
guilds.items.length,
|
|
150
|
+
guilds.truncated,
|
|
151
|
+
);
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
let channelMode: "unknown" | "nested" | "flat" = nested ? "unknown" : "flat";
|
|
@@ -209,6 +223,13 @@ async function listTargetsForBot(
|
|
|
209
223
|
}
|
|
210
224
|
}
|
|
211
225
|
|
|
226
|
+
ctx.logger?.debug(
|
|
227
|
+
"targets.list: %s listed=%d incomplete=%s nested=%s",
|
|
228
|
+
botKey,
|
|
229
|
+
targets.length,
|
|
230
|
+
incomplete,
|
|
231
|
+
nested,
|
|
232
|
+
);
|
|
212
233
|
return { targets, incomplete };
|
|
213
234
|
}
|
|
214
235
|
|
package/src/dbk/recall.ts
CHANGED
|
@@ -34,15 +34,24 @@ export async function recallMessage(ctx: Context, params: RecallParams): Promise
|
|
|
34
34
|
return failed("target id is empty", false);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
ctx.logger.debug("message.recall: deleteMessage channel=%s message=%s", channelId, messageId);
|
|
37
38
|
try {
|
|
38
39
|
await withTimeout(Promise.resolve(bot.deleteMessage(channelId, messageId)), RECALL_TIMEOUT_MS);
|
|
40
|
+
ctx.logger.debug("message.recall: adapter ok");
|
|
39
41
|
return create(RecallResultSchema, {
|
|
40
42
|
status: SendStatus.OK,
|
|
41
43
|
reason: "",
|
|
42
44
|
retryable: false,
|
|
43
45
|
});
|
|
44
46
|
} catch (error) {
|
|
45
|
-
|
|
47
|
+
const result = classifyRecallError(error);
|
|
48
|
+
ctx.logger.debug(
|
|
49
|
+
"message.recall: adapter error status=%s retryable=%s: %s",
|
|
50
|
+
SendStatus[result.status] ?? result.status,
|
|
51
|
+
result.retryable,
|
|
52
|
+
result.reason || errorMessage(error) || "message.recall failed",
|
|
53
|
+
);
|
|
54
|
+
return result;
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
|
package/src/dbk/send.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
219
|
-
|
|
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
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
|
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();
|
package/src/dbk/session.ts
CHANGED
|
@@ -68,8 +68,12 @@ export class DbkGatewaySession {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
emit<K extends keyof DbkEventMap>(method: K, event: DbkEventMap[K]): void {
|
|
71
|
-
if (!this.isHandshook)
|
|
71
|
+
if (!this.isHandshook) {
|
|
72
|
+
this.options.logger.debug("drop EVENT before handshake: method=%s", method);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
72
75
|
this.seq += 1n;
|
|
76
|
+
this.options.logger.debug("DBK EVENT %s seq=%s", method, this.seq.toString());
|
|
73
77
|
this.enqueueFrame(
|
|
74
78
|
createFrame({
|
|
75
79
|
op: FrameOp.EVENT,
|
|
@@ -126,6 +130,7 @@ export class DbkGatewaySession {
|
|
|
126
130
|
this.replyError(frame.id, ErrorCode.UNSUPPORTED, `unknown method: ${frame.method}`);
|
|
127
131
|
return;
|
|
128
132
|
}
|
|
133
|
+
this.options.logger.debug("DBK CALL %s id=%s", frame.method, frame.id);
|
|
129
134
|
try {
|
|
130
135
|
const payload = await this.dispatchCall(frame.method, frame.payload);
|
|
131
136
|
this.enqueueFrame(
|