koishi-plugin-dynamic-bot 0.0.4 → 0.0.5
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 +106 -26
- package/package.json +1 -1
- package/src/dbk/send.ts +126 -27
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
|
@@ -6086,6 +6086,11 @@ function withTimeout(promise, ms) {
|
|
|
6086
6086
|
var import_koishi = require("koishi");
|
|
6087
6087
|
var SEND_TIMEOUT_MS = 25e3;
|
|
6088
6088
|
var MENTION_ALL_FALLBACK = "@\u5168\u4F53\u6210\u5458";
|
|
6089
|
+
var MEDIA_FALLBACK_TYPE = {
|
|
6090
|
+
image: "image/png",
|
|
6091
|
+
video: "video/mp4",
|
|
6092
|
+
audio: "audio/mpeg"
|
|
6093
|
+
};
|
|
6089
6094
|
async function sendMessage(ctx, request) {
|
|
6090
6095
|
const bot = findBot4(ctx, request.botKey);
|
|
6091
6096
|
if (!bot) {
|
|
@@ -6152,11 +6157,28 @@ async function sendMessage(ctx, request) {
|
|
|
6152
6157
|
failures.push(errorMessage2(error) || "send failed");
|
|
6153
6158
|
}
|
|
6154
6159
|
};
|
|
6160
|
+
const sendRendered = async (render) => {
|
|
6161
|
+
if (aborted) return;
|
|
6162
|
+
let elements;
|
|
6163
|
+
try {
|
|
6164
|
+
elements = await render;
|
|
6165
|
+
} catch (error) {
|
|
6166
|
+
if (aborted) return;
|
|
6167
|
+
if (isTimeoutError(error)) {
|
|
6168
|
+
unknownReasons.push(errorMessage2(error) || "media fetch timed out");
|
|
6169
|
+
return;
|
|
6170
|
+
}
|
|
6171
|
+
failuresRetryable = failuresRetryable && isRetryableSendError(error);
|
|
6172
|
+
failures.push(errorMessage2(error) || "media fetch failed");
|
|
6173
|
+
return;
|
|
6174
|
+
}
|
|
6175
|
+
await sendElements(elements);
|
|
6176
|
+
};
|
|
6155
6177
|
const work = (async () => {
|
|
6156
6178
|
for (const unit of request.units) {
|
|
6157
6179
|
if (aborted) break;
|
|
6158
6180
|
if (unit.body.case === "normal") {
|
|
6159
|
-
await
|
|
6181
|
+
await sendRendered(segmentsToElements(ctx, unit.body.value.segments, { mentionAll }));
|
|
6160
6182
|
} else if (unit.body.case === "forward") {
|
|
6161
6183
|
const nodes = unit.body.value.nodes;
|
|
6162
6184
|
if (nodes.length === 0) {
|
|
@@ -6166,7 +6188,7 @@ async function sendMessage(ctx, request) {
|
|
|
6166
6188
|
}
|
|
6167
6189
|
for (const node of nodes) {
|
|
6168
6190
|
if (aborted) break;
|
|
6169
|
-
await
|
|
6191
|
+
await sendRendered(forwardNodeElements(ctx, node, { mentionAll }));
|
|
6170
6192
|
}
|
|
6171
6193
|
} else {
|
|
6172
6194
|
failuresRetryable = false;
|
|
@@ -6196,7 +6218,7 @@ async function sendMessage(ctx, request) {
|
|
|
6196
6218
|
});
|
|
6197
6219
|
}
|
|
6198
6220
|
}
|
|
6199
|
-
function segmentsToElements(segments, options) {
|
|
6221
|
+
async function segmentsToElements(ctx, segments, options) {
|
|
6200
6222
|
const elements = [];
|
|
6201
6223
|
const quoted = /* @__PURE__ */ new Set();
|
|
6202
6224
|
const addQuote = (id) => {
|
|
@@ -6214,17 +6236,17 @@ function segmentsToElements(segments, options) {
|
|
|
6214
6236
|
}
|
|
6215
6237
|
case "image": {
|
|
6216
6238
|
const uri = segment.body.value.uri.trim();
|
|
6217
|
-
if (uri) elements.push(
|
|
6239
|
+
if (uri) elements.push(await fetchMediaElement(ctx, "image", uri));
|
|
6218
6240
|
break;
|
|
6219
6241
|
}
|
|
6220
6242
|
case "video": {
|
|
6221
6243
|
const uri = segment.body.value.uri.trim();
|
|
6222
|
-
if (uri) elements.push(
|
|
6244
|
+
if (uri) elements.push(await fetchMediaElement(ctx, "video", uri));
|
|
6223
6245
|
break;
|
|
6224
6246
|
}
|
|
6225
6247
|
case "audio": {
|
|
6226
6248
|
const uri = segment.body.value.uri.trim();
|
|
6227
|
-
if (uri) elements.push(
|
|
6249
|
+
if (uri) elements.push(await fetchMediaElement(ctx, "audio", uri));
|
|
6228
6250
|
break;
|
|
6229
6251
|
}
|
|
6230
6252
|
case "mention": {
|
|
@@ -6258,13 +6280,55 @@ function segmentsToElements(segments, options) {
|
|
|
6258
6280
|
}
|
|
6259
6281
|
return elements;
|
|
6260
6282
|
}
|
|
6261
|
-
function forwardNodeElements(node, options) {
|
|
6262
|
-
const body = segmentsToElements(node.segments, options);
|
|
6283
|
+
async function forwardNodeElements(ctx, node, options) {
|
|
6284
|
+
const body = await segmentsToElements(ctx, node.segments, options);
|
|
6263
6285
|
const name2 = node.senderName.trim();
|
|
6264
6286
|
if (!name2) return body;
|
|
6265
6287
|
return [import_koishi.h.text(`${name2}
|
|
6266
6288
|
`), ...body];
|
|
6267
6289
|
}
|
|
6290
|
+
async function fetchMediaElement(ctx, kind, uri) {
|
|
6291
|
+
const http = ctx.http;
|
|
6292
|
+
if (typeof http?.file !== "function") {
|
|
6293
|
+
throw mediaFetchError(kind, uri, new Error("Koishi http service is required to send media"));
|
|
6294
|
+
}
|
|
6295
|
+
let file;
|
|
6296
|
+
try {
|
|
6297
|
+
file = await http.file(uri);
|
|
6298
|
+
} catch (error) {
|
|
6299
|
+
throw mediaFetchError(kind, uri, error);
|
|
6300
|
+
}
|
|
6301
|
+
let buffer;
|
|
6302
|
+
try {
|
|
6303
|
+
buffer = mediaBytes(file.data);
|
|
6304
|
+
} catch (error) {
|
|
6305
|
+
throw mediaFetchError(kind, uri, error);
|
|
6306
|
+
}
|
|
6307
|
+
if (buffer.length === 0) {
|
|
6308
|
+
throw mediaFetchError(kind, uri, new Error("empty body"));
|
|
6309
|
+
}
|
|
6310
|
+
const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
|
|
6311
|
+
if (kind === "image") return import_koishi.h.image(buffer, mime);
|
|
6312
|
+
if (kind === "video") return import_koishi.h.video(buffer, mime);
|
|
6313
|
+
return import_koishi.h.audio(buffer, mime);
|
|
6314
|
+
}
|
|
6315
|
+
function mediaBytes(data) {
|
|
6316
|
+
if (Buffer.isBuffer(data)) return data;
|
|
6317
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
6318
|
+
if (ArrayBuffer.isView(data)) {
|
|
6319
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
6320
|
+
}
|
|
6321
|
+
throw new Error("media body is not binary");
|
|
6322
|
+
}
|
|
6323
|
+
function mediaFetchError(kind, uri, cause) {
|
|
6324
|
+
const error = new Error(`failed to fetch ${kind} ${displayUri(uri)}: ${errorMessage2(cause) || "unknown error"}`);
|
|
6325
|
+
error.cause = cause;
|
|
6326
|
+
return error;
|
|
6327
|
+
}
|
|
6328
|
+
function displayUri(uri) {
|
|
6329
|
+
if (uri.startsWith("data:")) return "data:...";
|
|
6330
|
+
return uri.length > 200 ? `${uri.slice(0, 200)}...` : uri;
|
|
6331
|
+
}
|
|
6268
6332
|
function withQuote(elements, quoteId) {
|
|
6269
6333
|
if (!quoteId) return elements;
|
|
6270
6334
|
if (hasQuote(elements, quoteId)) return elements;
|
|
@@ -6328,28 +6392,33 @@ function failed2(reason, retryable) {
|
|
|
6328
6392
|
});
|
|
6329
6393
|
}
|
|
6330
6394
|
function isRetryableSendError(error) {
|
|
6331
|
-
const
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6395
|
+
for (const item of errorChain(error)) {
|
|
6396
|
+
const text = errorMessage2(item);
|
|
6397
|
+
const lower = text.toLowerCase();
|
|
6398
|
+
const status = httpStatusOf2(item);
|
|
6399
|
+
if (status === 401 || status === 403 || isForbidden2(lower)) return false;
|
|
6400
|
+
if (status === 400 || status === 404) return false;
|
|
6401
|
+
if (status === 429 || status !== void 0 && status >= 500) return true;
|
|
6402
|
+
if (isRetryableNetwork2(item, lower)) return true;
|
|
6403
|
+
if (/\brate.?limit\b|too many requests/.test(lower)) return true;
|
|
6404
|
+
}
|
|
6339
6405
|
return false;
|
|
6340
6406
|
}
|
|
6341
6407
|
function isTimeoutError(error) {
|
|
6342
|
-
const
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6408
|
+
for (const item of errorChain(error)) {
|
|
6409
|
+
const text = errorMessage2(item).toLowerCase();
|
|
6410
|
+
const status = httpStatusOf2(item);
|
|
6411
|
+
if (item instanceof SendTimeoutError) return true;
|
|
6412
|
+
if (status === 408 || status === 504) return true;
|
|
6413
|
+
if (item && typeof item === "object") {
|
|
6414
|
+
const name2 = item.name;
|
|
6415
|
+
if (name2 === "TimeoutError" || name2 === "AbortError") return true;
|
|
6416
|
+
const code = item.code;
|
|
6417
|
+
if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ABORT_ERR") return true;
|
|
6418
|
+
}
|
|
6419
|
+
if (text.includes("timed out") || text.includes("timeout")) return true;
|
|
6351
6420
|
}
|
|
6352
|
-
return
|
|
6421
|
+
return false;
|
|
6353
6422
|
}
|
|
6354
6423
|
function httpStatusOf2(error) {
|
|
6355
6424
|
if (!error || typeof error !== "object") return void 0;
|
|
@@ -6364,6 +6433,17 @@ function httpStatusOf2(error) {
|
|
|
6364
6433
|
if (typeof record.code === "number" && record.code >= 400 && record.code < 600) return record.code;
|
|
6365
6434
|
return void 0;
|
|
6366
6435
|
}
|
|
6436
|
+
function errorChain(error) {
|
|
6437
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6438
|
+
const chain = [];
|
|
6439
|
+
let current = error;
|
|
6440
|
+
while (current != null && !seen.has(current) && chain.length < 4) {
|
|
6441
|
+
seen.add(current);
|
|
6442
|
+
chain.push(current);
|
|
6443
|
+
current = typeof current === "object" && "cause" in current ? current.cause : void 0;
|
|
6444
|
+
}
|
|
6445
|
+
return chain;
|
|
6446
|
+
}
|
|
6367
6447
|
function errorMessage2(error) {
|
|
6368
6448
|
if (error instanceof Error) return error.message.trim();
|
|
6369
6449
|
if (typeof error === "string") return error.trim();
|
package/package.json
CHANGED
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
|
|
|
@@ -101,11 +107,29 @@ export async function sendMessage(ctx: Context, request: SendParams): Promise<Se
|
|
|
101
107
|
}
|
|
102
108
|
};
|
|
103
109
|
|
|
110
|
+
const sendRendered = async (render: Promise<El[]>): Promise<void> => {
|
|
111
|
+
if (aborted) return;
|
|
112
|
+
let elements: El[];
|
|
113
|
+
try {
|
|
114
|
+
elements = await render;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (aborted) return;
|
|
117
|
+
if (isTimeoutError(error)) {
|
|
118
|
+
unknownReasons.push(errorMessage(error) || "media fetch timed out");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
failuresRetryable = failuresRetryable && isRetryableSendError(error);
|
|
122
|
+
failures.push(errorMessage(error) || "media fetch failed");
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
await sendElements(elements);
|
|
126
|
+
};
|
|
127
|
+
|
|
104
128
|
const work = (async (): Promise<SendResult> => {
|
|
105
129
|
for (const unit of request.units) {
|
|
106
130
|
if (aborted) break;
|
|
107
131
|
if (unit.body.case === "normal") {
|
|
108
|
-
await
|
|
132
|
+
await sendRendered(segmentsToElements(ctx, unit.body.value.segments, { mentionAll }));
|
|
109
133
|
} else if (unit.body.case === "forward") {
|
|
110
134
|
// v1: never emit a single merged-forward receipt. Always one send per node.
|
|
111
135
|
const nodes = unit.body.value.nodes;
|
|
@@ -116,7 +140,7 @@ export async function sendMessage(ctx: Context, request: SendParams): Promise<Se
|
|
|
116
140
|
}
|
|
117
141
|
for (const node of nodes) {
|
|
118
142
|
if (aborted) break;
|
|
119
|
-
await
|
|
143
|
+
await sendRendered(forwardNodeElements(ctx, node, { mentionAll }));
|
|
120
144
|
}
|
|
121
145
|
} else {
|
|
122
146
|
failuresRetryable = false;
|
|
@@ -148,7 +172,11 @@ export async function sendMessage(ctx: Context, request: SendParams): Promise<Se
|
|
|
148
172
|
}
|
|
149
173
|
}
|
|
150
174
|
|
|
151
|
-
export function segmentsToElements(
|
|
175
|
+
export async function segmentsToElements(
|
|
176
|
+
ctx: Context,
|
|
177
|
+
segments: Segment[],
|
|
178
|
+
options?: SegmentRenderOptions,
|
|
179
|
+
): Promise<El[]> {
|
|
152
180
|
const elements: El[] = [];
|
|
153
181
|
const quoted = new Set<string>();
|
|
154
182
|
|
|
@@ -169,17 +197,17 @@ export function segmentsToElements(segments: Segment[], options?: SegmentRenderO
|
|
|
169
197
|
}
|
|
170
198
|
case "image": {
|
|
171
199
|
const uri = segment.body.value.uri.trim();
|
|
172
|
-
if (uri) elements.push(
|
|
200
|
+
if (uri) elements.push(await fetchMediaElement(ctx, "image", uri));
|
|
173
201
|
break;
|
|
174
202
|
}
|
|
175
203
|
case "video": {
|
|
176
204
|
const uri = segment.body.value.uri.trim();
|
|
177
|
-
if (uri) elements.push(
|
|
205
|
+
if (uri) elements.push(await fetchMediaElement(ctx, "video", uri));
|
|
178
206
|
break;
|
|
179
207
|
}
|
|
180
208
|
case "audio": {
|
|
181
209
|
const uri = segment.body.value.uri.trim();
|
|
182
|
-
if (uri) elements.push(
|
|
210
|
+
if (uri) elements.push(await fetchMediaElement(ctx, "audio", uri));
|
|
183
211
|
break;
|
|
184
212
|
}
|
|
185
213
|
case "mention": {
|
|
@@ -215,13 +243,65 @@ export function segmentsToElements(segments: Segment[], options?: SegmentRenderO
|
|
|
215
243
|
return elements;
|
|
216
244
|
}
|
|
217
245
|
|
|
218
|
-
function forwardNodeElements(
|
|
219
|
-
|
|
246
|
+
async function forwardNodeElements(
|
|
247
|
+
ctx: Context,
|
|
248
|
+
node: ForwardNode,
|
|
249
|
+
options?: SegmentRenderOptions,
|
|
250
|
+
): Promise<El[]> {
|
|
251
|
+
const body = await segmentsToElements(ctx, node.segments, options);
|
|
220
252
|
const name = node.senderName.trim();
|
|
221
253
|
if (!name) return body;
|
|
222
254
|
return [h.text(`${name}\n`), ...body];
|
|
223
255
|
}
|
|
224
256
|
|
|
257
|
+
type MediaKind = keyof typeof MEDIA_FALLBACK_TYPE;
|
|
258
|
+
|
|
259
|
+
async function fetchMediaElement(ctx: Context, kind: MediaKind, uri: string): Promise<El> {
|
|
260
|
+
const http = ctx.http;
|
|
261
|
+
if (typeof http?.file !== "function") {
|
|
262
|
+
throw mediaFetchError(kind, uri, new Error("Koishi http service is required to send media"));
|
|
263
|
+
}
|
|
264
|
+
let file: { data?: unknown; mime?: string; type?: string };
|
|
265
|
+
try {
|
|
266
|
+
file = await http.file(uri);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
throw mediaFetchError(kind, uri, error);
|
|
269
|
+
}
|
|
270
|
+
let buffer: Buffer;
|
|
271
|
+
try {
|
|
272
|
+
buffer = mediaBytes(file.data);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw mediaFetchError(kind, uri, error);
|
|
275
|
+
}
|
|
276
|
+
if (buffer.length === 0) {
|
|
277
|
+
throw mediaFetchError(kind, uri, new Error("empty body"));
|
|
278
|
+
}
|
|
279
|
+
const mime = (file.mime ?? file.type ?? "").trim() || MEDIA_FALLBACK_TYPE[kind];
|
|
280
|
+
if (kind === "image") return h.image(buffer, mime);
|
|
281
|
+
if (kind === "video") return h.video(buffer, mime);
|
|
282
|
+
return h.audio(buffer, mime);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function mediaBytes(data: unknown): Buffer {
|
|
286
|
+
if (Buffer.isBuffer(data)) return data;
|
|
287
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
288
|
+
if (ArrayBuffer.isView(data)) {
|
|
289
|
+
return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
290
|
+
}
|
|
291
|
+
throw new Error("media body is not binary");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function mediaFetchError(kind: MediaKind, uri: string, cause: unknown): Error {
|
|
295
|
+
const error = new Error(`failed to fetch ${kind} ${displayUri(uri)}: ${errorMessage(cause) || "unknown error"}`);
|
|
296
|
+
error.cause = cause;
|
|
297
|
+
return error;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function displayUri(uri: string): string {
|
|
301
|
+
if (uri.startsWith("data:")) return "data:...";
|
|
302
|
+
return uri.length > 200 ? `${uri.slice(0, 200)}...` : uri;
|
|
303
|
+
}
|
|
304
|
+
|
|
225
305
|
function withQuote(elements: El[], quoteId: string | undefined): El[] {
|
|
226
306
|
if (!quoteId) return elements;
|
|
227
307
|
if (hasQuote(elements, quoteId)) return elements;
|
|
@@ -297,30 +377,35 @@ function failed(reason: string, retryable: boolean): SendResult {
|
|
|
297
377
|
}
|
|
298
378
|
|
|
299
379
|
function isRetryableSendError(error: unknown): boolean {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
380
|
+
for (const item of errorChain(error)) {
|
|
381
|
+
const text = errorMessage(item);
|
|
382
|
+
const lower = text.toLowerCase();
|
|
383
|
+
const status = httpStatusOf(item);
|
|
384
|
+
|
|
385
|
+
if (status === 401 || status === 403 || isForbidden(lower)) return false;
|
|
386
|
+
if (status === 400 || status === 404) return false;
|
|
387
|
+
if (status === 429 || (status !== undefined && status >= 500)) return true;
|
|
388
|
+
if (isRetryableNetwork(item, lower)) return true;
|
|
389
|
+
if (/\brate.?limit\b|too many requests/.test(lower)) return true;
|
|
390
|
+
}
|
|
309
391
|
return false;
|
|
310
392
|
}
|
|
311
393
|
|
|
312
394
|
function isTimeoutError(error: unknown): boolean {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
395
|
+
for (const item of errorChain(error)) {
|
|
396
|
+
const text = errorMessage(item).toLowerCase();
|
|
397
|
+
const status = httpStatusOf(item);
|
|
398
|
+
if (item instanceof SendTimeoutError) return true;
|
|
399
|
+
if (status === 408 || status === 504) return true;
|
|
400
|
+
if (item && typeof item === "object") {
|
|
401
|
+
const name = (item as { name?: unknown }).name;
|
|
402
|
+
if (name === "TimeoutError" || name === "AbortError") return true;
|
|
403
|
+
const code = (item as { code?: unknown }).code;
|
|
404
|
+
if (code === "ETIMEDOUT" || code === "UND_ERR_CONNECT_TIMEOUT" || code === "ABORT_ERR") return true;
|
|
405
|
+
}
|
|
406
|
+
if (text.includes("timed out") || text.includes("timeout")) return true;
|
|
322
407
|
}
|
|
323
|
-
return
|
|
408
|
+
return false;
|
|
324
409
|
}
|
|
325
410
|
|
|
326
411
|
function httpStatusOf(error: unknown): number | undefined {
|
|
@@ -337,6 +422,20 @@ function httpStatusOf(error: unknown): number | undefined {
|
|
|
337
422
|
return undefined;
|
|
338
423
|
}
|
|
339
424
|
|
|
425
|
+
function errorChain(error: unknown): unknown[] {
|
|
426
|
+
const seen = new Set<unknown>();
|
|
427
|
+
const chain: unknown[] = [];
|
|
428
|
+
let current: unknown = error;
|
|
429
|
+
while (current != null && !seen.has(current) && chain.length < 4) {
|
|
430
|
+
seen.add(current);
|
|
431
|
+
chain.push(current);
|
|
432
|
+
current = typeof current === "object" && "cause" in current
|
|
433
|
+
? (current as { cause?: unknown }).cause
|
|
434
|
+
: undefined;
|
|
435
|
+
}
|
|
436
|
+
return chain;
|
|
437
|
+
}
|
|
438
|
+
|
|
340
439
|
function errorMessage(error: unknown): string {
|
|
341
440
|
if (error instanceof Error) return error.message.trim();
|
|
342
441
|
if (typeof error === "string") return error.trim();
|