koishi-plugin-forward-hime 1.4.2-alpha.0 → 1.4.2-alpha.2

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.
@@ -0,0 +1,45 @@
1
+ // client/index.ts
2
+ import { send } from "@koishijs/client";
3
+ import { defineComponent, h, inject } from "vue";
4
+ var index_default = (ctx) => {
5
+ ctx.slot({
6
+ type: "plugin-details",
7
+ order: -800,
8
+ component: defineComponent({
9
+ setup() {
10
+ const current = inject(
11
+ "manager.settings.current"
12
+ );
13
+ return () => {
14
+ const plugin = current?.value;
15
+ if (!plugin || plugin.name !== "forward-hime" && !plugin.path?.includes("forward-hime")) {
16
+ return null;
17
+ }
18
+ return h(
19
+ "button",
20
+ {
21
+ class: "k-button",
22
+ onClick: async () => {
23
+ const contents = await send(
24
+ "forward-hime:get-diagnostics"
25
+ );
26
+ const url = URL.createObjectURL(
27
+ new Blob([contents], { type: "application/x-ndjson" })
28
+ );
29
+ const anchor = document.createElement("a");
30
+ anchor.href = url;
31
+ anchor.download = "forward-hime-diagnostics.jsonl";
32
+ anchor.click();
33
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
34
+ }
35
+ },
36
+ "\u4E0B\u8F7D\u8BCA\u65AD\u65E5\u5FD7"
37
+ );
38
+ };
39
+ }
40
+ })
41
+ });
42
+ };
43
+ export {
44
+ index_default as default
45
+ };
@@ -0,0 +1,2 @@
1
+ import { Context } from "koishi";
2
+ export declare function consoleInit(ctx: Context): void;
@@ -1,14 +1,14 @@
1
- import { Session, Element } from "koishi";
1
+ import { Context, Session, Element } from "koishi";
2
2
  import { ConfigSet, ForwardNode } from "./config";
3
3
  interface ForwardMsg {
4
4
  head: Element[];
5
5
  content: Element[];
6
6
  }
7
- export declare function decoratorInit(cfg: ConfigSet): void;
7
+ export declare function decoratorInit(ctx: Context, cfg: ConfigSet): void;
8
8
  export declare function defaultDecorator({ head, content }: ForwardMsg): Element[];
9
9
  export declare function defaultMiddleware(session: Session): ForwardMsg;
10
10
  export declare function MsgMiddlewareCache(session: Session): Promise<void>;
11
- export declare function MsgDecorator(session: Session, node: ForwardNode): Promise<any>;
11
+ export declare function MsgDecorator(session: Session, node: ForwardNode, traceId?: string): Promise<any>;
12
12
  export declare function MsgDecoratorNoRelay(session: Session, node: ForwardNode): Promise<any>;
13
13
  export declare function MsgDecoratorFallback(session: Session, node: ForwardNode): Promise<Element[]>;
14
14
  export declare function MsgDecoratorFallbackReason(session: Session, node: ForwardNode, reason: string): Promise<Element[]>;
@@ -0,0 +1,18 @@
1
+ import { Context } from "koishi";
2
+ export interface DiagnosticEvent {
3
+ traceId: string;
4
+ phase: string;
5
+ [key: string]: unknown;
6
+ }
7
+ export declare function createTraceId(): `${string}-${string}-${string}-${string}-${string}`;
8
+ export declare function diagnosticFilename(value: unknown): string;
9
+ export declare function diagnosticMime(value: unknown): string;
10
+ export declare function diagnosticsInit(ctx: Context): void;
11
+ export declare function writeDiagnostic(event: DiagnosticEvent): void;
12
+ export declare function readDiagnosticLogs(): Promise<string>;
13
+ export declare function sanitizeError(error: unknown): {
14
+ name: string;
15
+ code: string | number;
16
+ httpStatus: number;
17
+ summary: string;
18
+ };
package/lib/index.js CHANGED
@@ -250,6 +250,99 @@ var onebot_default = {
250
250
 
251
251
  // src/relay.ts
252
252
  var import_koishi5 = require("koishi");
253
+
254
+ // src/diagnostics.ts
255
+ var import_crypto = require("crypto");
256
+ var import_promises = require("fs/promises");
257
+ var import_path = require("path");
258
+ var RETENTION_DAYS = 7;
259
+ var FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
260
+ var root = "";
261
+ var writeQueue = Promise.resolve();
262
+ var lastCleanupDate = "";
263
+ function createTraceId() {
264
+ return (0, import_crypto.randomUUID)();
265
+ }
266
+ function diagnosticFilename(value) {
267
+ if (typeof value !== "string" || /^https?:\/\//i.test(value)) return void 0;
268
+ return value.replace(/[?#].*$/, "").split(/[\\/]/).pop()?.slice(0, 120);
269
+ }
270
+ function diagnosticMime(value) {
271
+ return typeof value === "string" && /^[\w.+-]+\/[\w.+-]+$/.test(value) ? value : void 0;
272
+ }
273
+ function utcDate(timestamp = Date.now()) {
274
+ return new Date(timestamp).toISOString().slice(0, 10);
275
+ }
276
+ async function cleanup(date) {
277
+ if (date === lastCleanupDate) return;
278
+ const cutoff = Date.parse(`${date}T00:00:00.000Z`) - (RETENTION_DAYS - 1) * 864e5;
279
+ for (const filename of await (0, import_promises.readdir)(root)) {
280
+ if (!FILE_PATTERN.test(filename)) continue;
281
+ if (Date.parse(`${filename.slice(0, 10)}T00:00:00.000Z`) < cutoff) {
282
+ await (0, import_promises.rm)((0, import_path.join)(root, filename), { force: true });
283
+ }
284
+ }
285
+ lastCleanupDate = date;
286
+ }
287
+ function diagnosticsInit(ctx2) {
288
+ root = (0, import_path.join)(ctx2.baseDir, "data", "forward-hime");
289
+ writeQueue = Promise.resolve();
290
+ lastCleanupDate = "";
291
+ writeQueue = writeQueue.then(async () => {
292
+ await (0, import_promises.mkdir)(root, { recursive: true });
293
+ await cleanup(utcDate());
294
+ }).catch((error) => logger.warn("failed to initialize diagnostic log", error));
295
+ }
296
+ function writeDiagnostic(event) {
297
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
298
+ const date = timestamp.slice(0, 10);
299
+ const line = JSON.stringify({ timestamp, ...event }) + "\n";
300
+ writeQueue = writeQueue.then(async () => {
301
+ await (0, import_promises.mkdir)(root, { recursive: true });
302
+ await cleanup(date);
303
+ await (0, import_promises.appendFile)((0, import_path.join)(root, `${date}.jsonl`), line, "utf8");
304
+ }).catch((error) => logger.warn("failed to write diagnostic log", error));
305
+ }
306
+ async function readDiagnosticLogs() {
307
+ await writeQueue;
308
+ const cutoff = new Date(
309
+ Date.parse(`${utcDate()}T00:00:00.000Z`) - (RETENTION_DAYS - 1) * 864e5
310
+ ).toISOString().slice(0, 10);
311
+ const files = (await (0, import_promises.readdir)(root).catch(() => [])).filter(
312
+ (filename) => FILE_PATTERN.test(filename) && filename.slice(0, 10) >= cutoff
313
+ ).sort();
314
+ const contents = await Promise.all(
315
+ files.map((filename) => (0, import_promises.readFile)((0, import_path.join)(root, filename), "utf8"))
316
+ );
317
+ return contents.join("");
318
+ }
319
+ function sanitizeError(error) {
320
+ const value = error;
321
+ const message = typeof value?.message === "string" ? value.message : String(error);
322
+ const lowerMessage = message.toLowerCase();
323
+ let summary = "request failed";
324
+ if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) {
325
+ summary = "request timed out";
326
+ } else if (lowerMessage.includes("denied") || lowerMessage.includes("401") || lowerMessage.includes("403")) {
327
+ summary = "request denied";
328
+ } else if (lowerMessage.includes("not found") || lowerMessage.includes("404")) {
329
+ summary = "resource not found";
330
+ } else if (lowerMessage.includes("too large")) {
331
+ summary = "media exceeds size limit";
332
+ } else if (lowerMessage.includes("network") || lowerMessage.includes("econn") || lowerMessage.includes("enotfound")) {
333
+ summary = "network error";
334
+ } else if (lowerMessage.includes("empty media")) {
335
+ summary = "empty media payload";
336
+ }
337
+ return {
338
+ name: typeof value?.name === "string" && /^[a-z][\w.-]{0,63}$/i.test(value.name) ? value.name : "Error",
339
+ code: typeof value?.code === "string" && /^[\w.-]{1,64}$/.test(value.code) || typeof value?.code === "number" ? value.code : void 0,
340
+ httpStatus: typeof value?.status === "number" ? value.status : Number(/\bstatus\s+(\d{3})\b/i.exec(message)?.[1]) || void 0,
341
+ summary
342
+ };
343
+ }
344
+
345
+ // src/relay.ts
253
346
  var DEFAULT_IMAGE_MIME = "image/jpeg";
254
347
  var DEFAULT_AUDIO_MIME = "audio/mpeg";
255
348
  var DEFAULT_VIDEO_MIME = "video/mp4";
@@ -269,8 +362,21 @@ var MediaRelayError = class extends Error {
269
362
  this.userMessage = userMessage;
270
363
  }
271
364
  };
272
- function relayInit(cfg) {
365
+ function relayInit(ctx2, cfg) {
273
366
  relayConfig = cfg.MediaRelay;
367
+ ctx2.on("http/file", (url, options) => {
368
+ const { filename, file } = options;
369
+ if (typeof filename !== "string" || file !== filename) return;
370
+ const match = /^data:([\w.+/-]+);base64,(.*)$/.exec(url);
371
+ if (match) {
372
+ return {
373
+ data: Buffer.from(match[2], "base64"),
374
+ type: match[1],
375
+ mime: match[1],
376
+ filename
377
+ };
378
+ }
379
+ });
274
380
  }
275
381
  function cleanupExpiredRelayCache() {
276
382
  const now = Date.now();
@@ -332,16 +438,26 @@ function getCacheKey(element) {
332
438
  const src = mediaUrlFromElement(element);
333
439
  return `${mediaType}:${src}`;
334
440
  }
335
- function relayElementFromCache(item) {
336
- return createRelayElement(item.kind, item.mime, item.buffer, item.filename);
441
+ function relayElementFromCache(item, targetPlatform) {
442
+ return createRelayElement(
443
+ item.kind,
444
+ item.mime,
445
+ item.buffer,
446
+ item.filename,
447
+ targetPlatform
448
+ );
337
449
  }
338
- function createRelayElement(kind, mime, buffer, filename) {
450
+ function createRelayElement(kind, mime, buffer, filename, targetPlatform) {
339
451
  const helperMap = import_koishi5.h;
340
452
  if (kind === "img") {
341
453
  return import_koishi5.h.image(buffer, mime);
342
454
  }
455
+ if (kind === "audio") {
456
+ return import_koishi5.h.audio(buffer, mime, { file: filename });
457
+ }
343
458
  if (kind === "file") {
344
- return helperMap.file(buffer, mime, { filename });
459
+ const attrs = { file: filename, filename, title: filename };
460
+ return targetPlatform === "discord" ? import_koishi5.h.image(buffer, mime, attrs) : helperMap.file(buffer, mime, attrs);
345
461
  }
346
462
  if (helperMap[kind]) {
347
463
  return helperMap[kind](buffer, mime);
@@ -364,9 +480,23 @@ function networkMessageFromError(error) {
364
480
  }
365
481
  return "\u7F51\u7EDC\u95EE\u9898\uFF1A\u5A92\u4F53\u6587\u4EF6\u6682\u65F6\u65E0\u6CD5\u83B7\u53D6\u3002";
366
482
  }
367
- async function downloadAndRelay(element) {
483
+ async function downloadAndRelay(element, traceId, session, node) {
484
+ const startedAt = Date.now();
368
485
  const kind = normalizeMediaType(element.type);
369
486
  const src = mediaUrlFromElement(element);
487
+ const media = {
488
+ type: kind,
489
+ filename: diagnosticFilename(element.attrs?.filename),
490
+ mime: diagnosticMime(element.attrs?.mime),
491
+ size: typeof element.attrs?.size === "number" ? element.attrs.size : void 0
492
+ };
493
+ const context = {
494
+ source: session && { platform: session.platform, node: session.channelId },
495
+ target: node && { platform: node.Platform, node: node.Guild }
496
+ };
497
+ if (traceId) {
498
+ writeDiagnostic({ traceId, phase: "media-relay-start", media, ...context });
499
+ }
370
500
  if (!src) {
371
501
  throw new MediaRelayError(
372
502
  `missing media src for ${element.type}`,
@@ -377,7 +507,18 @@ async function downloadAndRelay(element) {
377
507
  const cacheKey = getCacheKey(element);
378
508
  const cacheItem = relayCache.get(cacheKey);
379
509
  if (cacheItem) {
380
- return relayElementFromCache(cacheItem);
510
+ if (traceId) {
511
+ writeDiagnostic({
512
+ traceId,
513
+ phase: "media-relay-result",
514
+ status: "cache-hit",
515
+ bytes: cacheItem.buffer.length,
516
+ mime: diagnosticMime(cacheItem.mime),
517
+ durationMs: Date.now() - startedAt,
518
+ ...context
519
+ });
520
+ }
521
+ return relayElementFromCache(cacheItem, node?.Platform);
381
522
  }
382
523
  const timeoutSignal = typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(relayConfig.RequestTimeoutSec * 1e3) : void 0;
383
524
  const response = await fetch(src, { signal: timeoutSignal });
@@ -428,9 +569,9 @@ async function downloadAndRelay(element) {
428
569
  `\u5A92\u4F53\u6587\u4EF6\u8FC7\u5927\uFF08>${relayConfig.MaxFileSizeMB} MB\uFF09\uFF0C\u65E0\u6CD5\u4E2D\u8F6C\u53D1\u9001\u3002`
429
570
  );
430
571
  }
431
- const mime = response.headers.get("content-type") || pickDefaultMime(kind);
432
- const filename = kind === "file" ? pickFilename(element, src) : void 0;
433
- const relayed = createRelayElement(kind, mime, buffer, filename);
572
+ const mime = response.headers.get("content-type")?.split(";")[0].trim() || pickDefaultMime(kind);
573
+ const filename = kind === "file" || kind === "audio" ? pickFilename(element, src) : void 0;
574
+ const relayed = createRelayElement(kind, mime, buffer, filename, node?.Platform);
434
575
  relayCache.set(cacheKey, {
435
576
  expiresAt: Date.now() + relayConfig.CacheMinutes * 60 * 1e3,
436
577
  kind,
@@ -438,9 +579,21 @@ async function downloadAndRelay(element) {
438
579
  buffer,
439
580
  filename
440
581
  });
582
+ if (traceId) {
583
+ writeDiagnostic({
584
+ traceId,
585
+ phase: "media-relay-result",
586
+ status: "success",
587
+ media,
588
+ bytes: buffer.length,
589
+ mime: diagnosticMime(mime),
590
+ ...context,
591
+ durationMs: Date.now() - startedAt
592
+ });
593
+ }
441
594
  return relayed;
442
595
  }
443
- async function relayForwardContent(content) {
596
+ async function relayForwardContent(content, traceId, session, node) {
444
597
  if (!relayConfig.Enabled) {
445
598
  return content;
446
599
  }
@@ -450,9 +603,24 @@ async function relayForwardContent(content) {
450
603
  newContent.push(element);
451
604
  continue;
452
605
  }
606
+ const startedAt = Date.now();
453
607
  try {
454
- newContent.push(await downloadAndRelay(element));
608
+ newContent.push(await downloadAndRelay(element, traceId, session, node));
455
609
  } catch (error) {
610
+ if (traceId) {
611
+ writeDiagnostic({
612
+ traceId,
613
+ phase: "media-relay-result",
614
+ status: "error",
615
+ error: sanitizeError(error),
616
+ durationMs: Date.now() - startedAt,
617
+ source: session && {
618
+ platform: session.platform,
619
+ node: session.channelId
620
+ },
621
+ target: node && { platform: node.Platform, node: node.Guild }
622
+ });
623
+ }
456
624
  throw new MediaRelayError(
457
625
  error instanceof Error ? error.message : "relay failed",
458
626
  networkMessageFromError(error)
@@ -463,29 +631,63 @@ async function relayForwardContent(content) {
463
631
  }
464
632
 
465
633
  // src/message.ts
466
- async function MessageSendWithDecorator(ctx2, node, session, Deco) {
634
+ async function MessageSendWithDecorator(ctx2, node, session, Deco, traceId, attempt) {
635
+ const startedAt = Date.now();
467
636
  const uuid = session.channelId + ":" + session.messageId;
468
- const content = await Deco(session, node);
469
- await ctx2.bots[`${node.Platform}:${node.BotID}`].sendMessage(node.Guild, content).then((res) => {
470
- const mc = {
471
- platform: node.Platform,
472
- bot: node.BotID,
473
- guild: node.Guild,
474
- msgid: res[0],
475
- uuid
476
- };
477
- if (!mc.msgid) {
478
- throw new Error(`Empty Message ID`);
479
- } else {
480
- msgCache(mc);
481
- logger.debug(`[MessageForward] to ${mc.platform} ${mc.uuid}`);
637
+ writeDiagnostic({
638
+ traceId,
639
+ phase: "send-attempt",
640
+ attempt,
641
+ source: { platform: session.platform, node: session.channelId },
642
+ target: { platform: node.Platform, node: node.Guild },
643
+ elementTypes: session.elements.map((element) => element.type)
644
+ });
645
+ try {
646
+ const content = await Deco(session, node, traceId);
647
+ const res = await ctx2.bots[`${node.Platform}:${node.BotID}`].sendMessage(
648
+ node.Guild,
649
+ content
650
+ );
651
+ {
652
+ const mc = {
653
+ platform: node.Platform,
654
+ bot: node.BotID,
655
+ guild: node.Guild,
656
+ msgid: res[0],
657
+ uuid
658
+ };
659
+ if (!mc.msgid) {
660
+ throw new Error(`Empty Message ID`);
661
+ } else {
662
+ msgCache(mc);
663
+ logger.debug(`[MessageForward] to ${mc.platform} ${mc.uuid}`);
664
+ }
482
665
  }
483
- }).catch((error) => {
666
+ writeDiagnostic({
667
+ traceId,
668
+ phase: "send-result",
669
+ attempt,
670
+ status: "success",
671
+ durationMs: Date.now() - startedAt,
672
+ source: { platform: session.platform, node: session.channelId },
673
+ target: { platform: node.Platform, node: node.Guild }
674
+ });
675
+ } catch (error) {
676
+ writeDiagnostic({
677
+ traceId,
678
+ phase: "send-result",
679
+ attempt,
680
+ status: "error",
681
+ durationMs: Date.now() - startedAt,
682
+ source: { platform: session.platform, node: session.channelId },
683
+ target: { platform: node.Platform, node: node.Guild },
684
+ error: sanitizeError(error)
685
+ });
484
686
  logger.error(
485
687
  `ERROR:<MessageSendWithDecorator ${node.Platform}> ctx=${ctx2} ${error}`
486
688
  );
487
689
  throw error;
488
- });
690
+ }
489
691
  }
490
692
  function sessionTypeArray(session) {
491
693
  let contentTypes = "|";
@@ -495,9 +697,34 @@ function sessionTypeArray(session) {
495
697
  return contentTypes;
496
698
  }
497
699
  async function MessageForward(ctx2, node, session, timeoutSec, relayEnabled) {
700
+ const traceId = createTraceId();
701
+ const source = { platform: session.platform, node: session.channelId };
702
+ const target = { platform: node.Platform, node: node.Guild };
498
703
  if (!botExistsCheck(ctx2, node)) {
704
+ writeDiagnostic({
705
+ traceId,
706
+ phase: "forward-result",
707
+ status: "bot-missing",
708
+ source,
709
+ target
710
+ });
499
711
  return;
500
712
  }
713
+ writeDiagnostic({
714
+ traceId,
715
+ phase: "forward-start",
716
+ source,
717
+ target,
718
+ elementTypes: session.elements.map((element) => element.type),
719
+ media: session.elements.filter(
720
+ (element) => ["img", "image", "audio", "video", "file"].includes(element.type)
721
+ ).map((element) => ({
722
+ type: element.type,
723
+ filename: diagnosticFilename(element.attrs?.filename),
724
+ mime: diagnosticMime(element.attrs?.mime),
725
+ size: typeof element.attrs?.size === "number" ? element.attrs.size : void 0
726
+ }))
727
+ });
501
728
  const timeoutMs = (timeoutSec ?? 30) * 1e3;
502
729
  const deco = relayEnabled !== false ? MsgDecorator : MsgDecoratorNoRelay;
503
730
  function warnIfSlow(promise) {
@@ -509,8 +736,16 @@ async function MessageForward(ctx2, node, session, timeoutSec, relayEnabled) {
509
736
  return promise.finally(() => clearTimeout(timer));
510
737
  }
511
738
  function sendDegraded(reason) {
739
+ writeDiagnostic({ traceId, phase: "forward-degraded", source, target });
512
740
  const fallbackDeco = reason ? (fallbackSession, fallbackNode) => MsgDecoratorFallbackReason(fallbackSession, fallbackNode, reason) : MsgDecoratorFallback;
513
- MessageSendWithDecorator(ctx2, node, session, fallbackDeco).catch((error) => {
741
+ MessageSendWithDecorator(
742
+ ctx2,
743
+ node,
744
+ session,
745
+ fallbackDeco,
746
+ traceId,
747
+ "fallback"
748
+ ).catch((error) => {
514
749
  logger.error(
515
750
  `ERROR:<MessageSendFallback ${node.Platform}> ctx=${ctx2} ${sessionTypeArray(session)} ${error}`
516
751
  );
@@ -522,28 +757,43 @@ async function MessageForward(ctx2, node, session, timeoutSec, relayEnabled) {
522
757
  }
523
758
  return void 0;
524
759
  }
525
- warnIfSlow(MessageSendWithDecorator(ctx2, node, session, deco)).catch(
526
- (firstError) => {
527
- logger.error(
528
- `ERROR:<MessageSend ${node.Platform}> ctx=${ctx2} ${sessionTypeArray(session)} ${firstError}`
760
+ const traceableDeco = (sourceSession, targetNode, id) => deco(sourceSession, targetNode, id);
761
+ warnIfSlow(
762
+ MessageSendWithDecorator(
763
+ ctx2,
764
+ node,
765
+ session,
766
+ traceableDeco,
767
+ traceId,
768
+ relayEnabled === false ? "direct" : "relay"
769
+ )
770
+ ).catch((firstError) => {
771
+ logger.error(
772
+ `ERROR:<MessageSend ${node.Platform}> ctx=${ctx2} ${sessionTypeArray(session)} ${firstError}`
773
+ );
774
+ if (relayEnabled !== false && firstError instanceof MediaRelayError) {
775
+ logger.info(
776
+ `[MessageForward] relay failed, retrying without relay: ${node.Platform}`
529
777
  );
530
- if (relayEnabled !== false && firstError instanceof MediaRelayError) {
531
- logger.info(
532
- `[MessageForward] relay failed, retrying without relay: ${node.Platform}`
778
+ warnIfSlow(
779
+ MessageSendWithDecorator(
780
+ ctx2,
781
+ node,
782
+ session,
783
+ MsgDecoratorNoRelay,
784
+ traceId,
785
+ "direct-retry"
786
+ )
787
+ ).catch((secondError) => {
788
+ logger.error(
789
+ `ERROR:<MessageSendDirect ${node.Platform}> ctx=${ctx2} ${sessionTypeArray(session)} ${secondError}`
533
790
  );
534
- warnIfSlow(
535
- MessageSendWithDecorator(ctx2, node, session, MsgDecoratorNoRelay)
536
- ).catch((secondError) => {
537
- logger.error(
538
- `ERROR:<MessageSendDirect ${node.Platform}> ctx=${ctx2} ${sessionTypeArray(session)} ${secondError}`
539
- );
540
- sendDegraded(reasonFromError(secondError));
541
- });
542
- } else {
543
- sendDegraded(reasonFromError(firstError));
544
- }
791
+ sendDegraded(reasonFromError(secondError));
792
+ });
793
+ } else {
794
+ sendDegraded(reasonFromError(firstError));
545
795
  }
546
- );
796
+ });
547
797
  }
548
798
  async function MessageDelete(ctx2, msg) {
549
799
  if (!botExistsCheck(ctx2, { Platform: msg.platform, BotID: msg.bot, Guild: msg.guild })) {
@@ -568,11 +818,11 @@ function MsgUUIDFromSession(session) {
568
818
  var defaultPrefix = "";
569
819
  var defaultFallback = "";
570
820
  var defaultPrefixNewline = true;
571
- function decoratorInit(cfg) {
821
+ function decoratorInit(ctx2, cfg) {
572
822
  defaultPrefix = cfg.DefaultDecorator.Prefix;
573
823
  defaultPrefixNewline = cfg.DefaultDecorator.Newline;
574
824
  defaultFallback = cfg.DefaultFallbackMsgPrefix;
575
- relayInit(cfg);
825
+ relayInit(ctx2, cfg);
576
826
  }
577
827
  function renderTemplate(template, data) {
578
828
  return template.replace(/\$\{(\w+)\}/g, (_, key) => {
@@ -589,7 +839,10 @@ function defaultMiddleware(session) {
589
839
  if (defaultPrefixNewline) {
590
840
  head.push((0, import_koishi6.h)("br"));
591
841
  }
592
- return { head, content: session.elements };
842
+ const content = session.platform === "discord" ? session.elements.map(
843
+ (element) => element.type === "record" ? (0, import_koishi6.h)("audio", element.attrs, element.children) : element
844
+ ) : session.elements;
845
+ return { head, content };
593
846
  }
594
847
  var localDecorators = [atTranslator, quoteTranslator];
595
848
  var msgMiddleCache = [];
@@ -615,12 +868,25 @@ function MsgToMiddleware(session) {
615
868
  }
616
869
  return defaultMiddleware(session);
617
870
  }
871
+ function discordAudioAttachments(session, node, content) {
872
+ if (session.platform !== "onebot" || node.Platform !== "discord") {
873
+ return content;
874
+ }
875
+ return content.map((element) => {
876
+ if (element.type !== "audio") return element;
877
+ const src = element.attrs.src || element.attrs.url;
878
+ if (typeof src !== "string") return element;
879
+ const name2 = element.attrs.filename || element.attrs.file;
880
+ const filename = typeof name2 === "string" && /^[^/\\:]+\.[a-z\d]+$/i.test(name2) ? name2 : "voice.bin";
881
+ return import_koishi6.h.image(src, { file: filename, mode: "download" });
882
+ });
883
+ }
618
884
  async function MsgMiddlewareCache(session) {
619
885
  const elems = MsgToMiddleware(session);
620
886
  msgMiddleCacheAppend({ UUID: MsgUUIDFromSession(session), msg: elems });
621
887
  logger.debug(`[msgMiddleCache] CACHED`);
622
888
  }
623
- async function MsgDecorator(session, node) {
889
+ async function MsgDecorator(session, node, traceId) {
624
890
  let elems;
625
891
  const _platform_out = decorators_exports[node.Platform];
626
892
  const elemCache = msgMiddleCacheFind(MsgUUIDFromSession(session));
@@ -636,7 +902,11 @@ async function MsgDecorator(session, node) {
636
902
  }
637
903
  elems = {
638
904
  head: elems.head,
639
- content: await relayForwardContent(elems.content)
905
+ content: discordAudioAttachments(
906
+ session,
907
+ node,
908
+ await relayForwardContent(elems.content, traceId, session, node)
909
+ )
640
910
  };
641
911
  if (_platform_out && typeof _platform_out.Decorator === "function") {
642
912
  return _platform_out.Decorator(elems);
@@ -657,6 +927,10 @@ async function MsgDecoratorNoRelay(session, node) {
657
927
  elems = await fn(session, node, elems);
658
928
  }
659
929
  logger.debug(`[MsgDecoratorNoRelay] fallback to direct forward`);
930
+ elems = {
931
+ head: elems.head,
932
+ content: discordAudioAttachments(session, node, elems.content)
933
+ };
660
934
  if (_platform_out && typeof _platform_out.Decorator === "function") {
661
935
  return _platform_out.Decorator(elems);
662
936
  } else {
@@ -692,7 +966,7 @@ async function MsgDecoratorFallbackReason(session, node, reason) {
692
966
  return defaultDecoratorFallback(elems, reason);
693
967
  }
694
968
  async function atTranslator(session, _, { head, content }) {
695
- const newMsg = await new Promise((resolve) => {
969
+ const newMsg = await new Promise((resolve2) => {
696
970
  const newContent = [];
697
971
  for (const key in content) {
698
972
  const element = content[key];
@@ -706,7 +980,7 @@ async function atTranslator(session, _, { head, content }) {
706
980
  newContent.push(element);
707
981
  }
708
982
  }
709
- resolve({ head, content: newContent });
983
+ resolve2({ head, content: newContent });
710
984
  });
711
985
  return newMsg;
712
986
  }
@@ -728,6 +1002,21 @@ async function quoteTranslator(session, node, { head, content }) {
728
1002
  return { head, content };
729
1003
  }
730
1004
 
1005
+ // src/console.ts
1006
+ var import_path2 = require("path");
1007
+ function consoleInit(ctx2) {
1008
+ ctx2.inject(["console"], (ctx3) => {
1009
+ const console = ctx3.console;
1010
+ console.addListener("forward-hime:get-diagnostics", readDiagnosticLogs, {
1011
+ authority: 4
1012
+ });
1013
+ console.addEntry({
1014
+ dev: (0, import_path2.resolve)(__dirname, "../client/index.ts"),
1015
+ prod: (0, import_path2.resolve)(__dirname, "client")
1016
+ });
1017
+ });
1018
+ }
1019
+
731
1020
  // src/index.ts
732
1021
  var name = `forward hime - \u8F6C\u53D1\u59EC`;
733
1022
  var usage = `
@@ -743,13 +1032,15 @@ var usage = `
743
1032
  ---`;
744
1033
  var reusable = true;
745
1034
  var inject = {
746
- optional: ["cache"]
1035
+ optional: ["cache", "console"]
747
1036
  };
748
1037
  var Config = createConfig();
749
1038
  function apply(ctx2, cfg) {
750
1039
  loggerInit(ctx2);
1040
+ diagnosticsInit(ctx2);
1041
+ consoleInit(ctx2);
751
1042
  msgCacheInit(ctx2, cfg);
752
- decoratorInit(cfg);
1043
+ decoratorInit(ctx2, cfg);
753
1044
  ctx2.on("message-created", async (session) => {
754
1045
  const hitGroup = [];
755
1046
  for (const g in cfg.ForwardGroups) {
package/lib/relay.d.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { Element } from "koishi";
1
+ import { Context, Element } from "koishi";
2
2
  import { ConfigSet } from "./config";
3
+ import { Session } from "koishi";
4
+ import { ForwardNode } from "./config";
3
5
  export declare class MediaRelayError extends Error {
4
6
  userMessage: string;
5
7
  constructor(message: string, userMessage: string);
6
8
  }
7
- export declare function relayInit(cfg: ConfigSet): void;
8
- export declare function relayForwardContent(content: Element[]): Promise<Element[]>;
9
+ export declare function relayInit(ctx: Context, cfg: ConfigSet): void;
10
+ export declare function relayForwardContent(content: Element[], traceId?: string, session?: Session, node?: ForwardNode): Promise<Element[]>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-forward-hime",
3
3
  "description": "多群组消息互通",
4
- "version": "1.4.2-alpha.0",
4
+ "version": "1.4.2-alpha.2",
5
5
  "author": {
6
6
  "name": "xianii",
7
7
  "email": "jiyucheng007@gmail.com"
@@ -57,7 +57,8 @@
57
57
  },
58
58
  "scripts": {
59
59
  "prepare": "husky",
60
- "build": "rm -f tsconfig.tsbuildinfo && rm -rf lib && tsc -b && esbuild src/index.ts --bundle --platform=node --packages=external --outfile=lib/index.js",
60
+ "build": "rm -f tsconfig.tsbuildinfo && rm -rf lib && tsc -b && esbuild src/index.ts --bundle --platform=node --packages=external --outfile=lib/index.js && esbuild client/index.ts --bundle --platform=browser --format=esm --packages=external --outfile=lib/client/index.js",
61
+ "test": "node --test",
61
62
  "eslint": "eslint \"src/**/*.{ts,tsx}\"",
62
63
  "format": "prettier --write .",
63
64
  "pub": "npm publish --access public"