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.
- package/lib/client/index.js +45 -0
- package/lib/console.d.ts +2 -0
- package/lib/decorator.d.ts +3 -3
- package/lib/diagnostics.d.ts +18 -0
- package/lib/index.js +349 -58
- package/lib/relay.d.ts +5 -3
- package/package.json +3 -2
|
@@ -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
|
+
};
|
package/lib/console.d.ts
ADDED
package/lib/decorator.d.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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:
|
|
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((
|
|
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
|
-
|
|
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.
|
|
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"
|