@koishi-ce/plugin-market 1.0.11 → 1.0.13
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/README.md +3 -1
- package/dist/index.js +4 -4
- package/lib/index.d.ts +19 -10
- package/lib/index.mjs +17 -7
- package/package.json +5 -4
- package/src/__tests__/index.test.ts +168 -3
- package/src/node/installer.ts +1 -1
- package/src/node/market.ts +28 -6
package/lib/index.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ declare module "@koishi-ce/console" {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
|
-
declare abstract class MarketProvider extends DataService<MarketProvider.Payload> {
|
|
80
|
+
export declare abstract class MarketProvider extends DataService<MarketProvider.Payload> {
|
|
81
81
|
private _task;
|
|
82
82
|
private _timestamp;
|
|
83
83
|
protected _error: unknown;
|
|
@@ -86,7 +86,7 @@ declare abstract class MarketProvider extends DataService<MarketProvider.Payload
|
|
|
86
86
|
abstract collect(): Promise<SearchResult | undefined>;
|
|
87
87
|
prepare(): Promise<SearchResult | undefined>;
|
|
88
88
|
}
|
|
89
|
-
declare namespace MarketProvider {
|
|
89
|
+
export declare namespace MarketProvider {
|
|
90
90
|
interface Payload {
|
|
91
91
|
registry?: string | undefined;
|
|
92
92
|
data: Dict<SearchObject>;
|
|
@@ -108,7 +108,16 @@ declare class MarketProvider$1 extends MarketProvider {
|
|
|
108
108
|
config: MarketProvider$1.Config;
|
|
109
109
|
constructor(ctx: Context, config: MarketProvider$1.Config);
|
|
110
110
|
start(refresh?: boolean): Promise<void>;
|
|
111
|
-
/**
|
|
111
|
+
/**
|
|
112
|
+
* 限流(429)、请求超时(abort 后无 response,code 为 ETIMEDOUT)、
|
|
113
|
+
* 服务端瞬态错误(408 / 5xx)可安全重试;allowNotFound 时把 404
|
|
114
|
+
* 也视为瞬态——镜像索引以「302 指向版本化文件」方式发布,切换文件
|
|
115
|
+
* 的部署窗口会出现短时 404。
|
|
116
|
+
*
|
|
117
|
+
* 无 code 且无 response 的 HTTP.Error 是 plugin-http 对 fetch 网络层
|
|
118
|
+
* 异常(证书验证失败、连接重置、DNS 等)的统一包装——代理节点切换
|
|
119
|
+
* 等瞬态网络抖动即此形态,重试可自愈。
|
|
120
|
+
*/
|
|
112
121
|
private isRetryable;
|
|
113
122
|
/**
|
|
114
123
|
* 带退避重试的 registry 请求。优先遵循服务端的 Retry-After 响应头
|
|
@@ -160,8 +169,8 @@ declare module "@koishi-ce/console" {
|
|
|
160
169
|
"market/registry"(names: string[]): Promise<Dict<Dict<Pick<RemotePackage, DependencyMetaKey>>>>;
|
|
161
170
|
}
|
|
162
171
|
}
|
|
163
|
-
declare const name = "market";
|
|
164
|
-
declare const inject: {
|
|
172
|
+
export declare const name = "market";
|
|
173
|
+
export declare const inject: {
|
|
165
174
|
http: {
|
|
166
175
|
required: boolean;
|
|
167
176
|
};
|
|
@@ -169,12 +178,12 @@ declare const inject: {
|
|
|
169
178
|
required: boolean;
|
|
170
179
|
};
|
|
171
180
|
};
|
|
172
|
-
declare const usage = "\n如果插件市场页面提示「无法连接到插件市场」,则可以选择一个 Koishi 社区提供的镜像地址,填入下方对应的配置项中。\n\n## 插件市场(填入 search.endpoint)\n\n- Koishi(全球):https://registry.koishi.chat/index.json\n- [t4wefan](https://k.ilharp.cc/2611)(大陆):https://registry.koishi.t4wefan.pub/index.json\n- [Lipraty](https://k.ilharp.cc/3530)(大陆):https://koi.nyan.zone/registry/index.json\n- [itzdrli](https://k.ilharp.cc/9975)(全球):https://kp.itzdrli.cc\n- [Q78KG](https://k.ilharp.cc/10042)(全球):https://koishi-registry.yumetsuki.moe/index.json\n\n要浏览更多社区镜像,请访问 [Koishi 论坛上的镜像一览](https://k.ilharp.cc/4000)。";
|
|
173
|
-
interface Config {
|
|
181
|
+
export declare const usage = "\n如果插件市场页面提示「无法连接到插件市场」,则可以选择一个 Koishi 社区提供的镜像地址,填入下方对应的配置项中。\n\n## 插件市场(填入 search.endpoint)\n\n- Koishi(全球):https://registry.koishi.chat/index.json\n- [t4wefan](https://k.ilharp.cc/2611)(大陆):https://registry.koishi.t4wefan.pub/index.json\n- [Lipraty](https://k.ilharp.cc/3530)(大陆):https://koi.nyan.zone/registry/index.json\n- [itzdrli](https://k.ilharp.cc/9975)(全球):https://kp.itzdrli.cc\n- [Q78KG](https://k.ilharp.cc/10042)(全球):https://koishi-registry.yumetsuki.moe/index.json\n\n要浏览更多社区镜像,请访问 [Koishi 论坛上的镜像一览](https://k.ilharp.cc/4000)。";
|
|
182
|
+
export interface Config {
|
|
174
183
|
registry?: Installer.Config;
|
|
175
184
|
search?: MarketProvider$1.Config;
|
|
176
185
|
}
|
|
177
|
-
declare const Config: Schema<Config>;
|
|
178
|
-
declare function apply(ctx: Context, config: Config): void;
|
|
186
|
+
export declare const Config: Schema<Config>;
|
|
187
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
179
188
|
//#endregion
|
|
180
|
-
export {
|
|
189
|
+
export { DependencyProvider, Installer, RegistryProvider };
|
package/lib/index.mjs
CHANGED
|
@@ -354,21 +354,31 @@ var MarketProvider$1 = class extends MarketProvider {
|
|
|
354
354
|
await this.prepare();
|
|
355
355
|
super.start();
|
|
356
356
|
}
|
|
357
|
-
/**
|
|
358
|
-
|
|
357
|
+
/**
|
|
358
|
+
* 限流(429)、请求超时(abort 后无 response,code 为 ETIMEDOUT)、
|
|
359
|
+
* 服务端瞬态错误(408 / 5xx)可安全重试;allowNotFound 时把 404
|
|
360
|
+
* 也视为瞬态——镜像索引以「302 指向版本化文件」方式发布,切换文件
|
|
361
|
+
* 的部署窗口会出现短时 404。
|
|
362
|
+
*
|
|
363
|
+
* 无 code 且无 response 的 HTTP.Error 是 plugin-http 对 fetch 网络层
|
|
364
|
+
* 异常(证书验证失败、连接重置、DNS 等)的统一包装——代理节点切换
|
|
365
|
+
* 等瞬态网络抖动即此形态,重试可自愈。
|
|
366
|
+
*/
|
|
367
|
+
isRetryable(error, allowNotFound = false) {
|
|
359
368
|
if (!this.ctx.http.isError(error)) return false;
|
|
360
|
-
|
|
361
|
-
|
|
369
|
+
if (!error.response) return error.code === void 0 || error.code === "ETIMEDOUT";
|
|
370
|
+
const { status } = error.response;
|
|
371
|
+
return status === 429 || status === 408 || allowNotFound && status === 404 || status !== void 0 && status >= 500;
|
|
362
372
|
}
|
|
363
373
|
/**
|
|
364
374
|
* 带退避重试的 registry 请求。优先遵循服务端的 Retry-After 响应头
|
|
365
375
|
* (秒数),缺省时按基础时长指数退避。
|
|
366
376
|
*/
|
|
367
|
-
async request(http, url, config) {
|
|
377
|
+
async request(http, url, config, allowNotFound = false) {
|
|
368
378
|
for (let attempt = 0;; attempt++) try {
|
|
369
379
|
return await http.get(url, config);
|
|
370
380
|
} catch (error) {
|
|
371
|
-
if (attempt >= maxRetries || !this.isRetryable(error)) throw error;
|
|
381
|
+
if (attempt >= maxRetries || !this.isRetryable(error, allowNotFound)) throw error;
|
|
372
382
|
const retryAfter = Number(error.response?.headers.get("retry-after") ?? "");
|
|
373
383
|
const delay = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * Time.second : retryBaseDelay * 2 ** attempt;
|
|
374
384
|
logger.debug("请求 %s 失败(%s),%d 秒后重试(第 %d 次)", url, error.message, delay / Time.second, attempt + 1);
|
|
@@ -381,7 +391,7 @@ var MarketProvider$1 = class extends MarketProvider {
|
|
|
381
391
|
this.failed = [];
|
|
382
392
|
this.scanner = new Scanner((url, config) => this.request(registry, url, config));
|
|
383
393
|
if (this.http) {
|
|
384
|
-
const result = await this.request(this.http, "");
|
|
394
|
+
const result = await this.request(this.http, "", void 0, true);
|
|
385
395
|
this.scanner.objects = result.objects.filter((object) => !object.ignored);
|
|
386
396
|
this.scanner.total = this.scanner.objects.length;
|
|
387
397
|
if (result.version !== void 0) this.scanner.version = result.version;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@koishi-ce/plugin-market",
|
|
3
3
|
"description": "Manage your bots and plugins with console",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.13",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.mjs",
|
|
7
7
|
"typings": "lib/index.d.ts",
|
|
@@ -60,9 +60,10 @@
|
|
|
60
60
|
}
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@koishi-ce/
|
|
64
|
-
"@koishi-ce/
|
|
65
|
-
"@koishi-ce/
|
|
63
|
+
"@koishi-ce/plugin-database-memory": "^1.1.0",
|
|
64
|
+
"@koishi-ce/client": "^1.2.3",
|
|
65
|
+
"@koishi-ce/loader": "^1.0.8",
|
|
66
|
+
"@koishi-ce/plugin-config": "^1.1.2",
|
|
66
67
|
"@koishijs/market": "^4.2.10",
|
|
67
68
|
"@types/semver": "^7.5.8",
|
|
68
69
|
"@vueuse/core": "^14.4.0",
|
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
rmSync,
|
|
17
17
|
writeFileSync,
|
|
18
18
|
} from "node:fs";
|
|
19
|
+
import net from "node:net";
|
|
19
20
|
import { tmpdir } from "node:os";
|
|
20
21
|
import { join } from "node:path";
|
|
21
|
-
import memory from "@
|
|
22
|
+
import memory from "@koishi-ce/plugin-database-memory";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* market 插件测试:
|
|
@@ -139,6 +140,12 @@ let searchResponse: {
|
|
|
139
140
|
total: 0,
|
|
140
141
|
};
|
|
141
142
|
|
|
143
|
+
/** flaky-index 镜像索引桩:已发生的请求计数与计划内的 404 次数。 */
|
|
144
|
+
let flakyIndexHits = 0;
|
|
145
|
+
let flakyIndexMisses = 0;
|
|
146
|
+
/** slow-index 慢响应桩:已发生的请求计数。 */
|
|
147
|
+
let slowIndexHits = 0;
|
|
148
|
+
|
|
142
149
|
/** 声明预期触发 market 域告警(404 / 限流等桩失败路径)的用例:执行期间静默该域。 */
|
|
143
150
|
const itQuiet = (
|
|
144
151
|
name: string,
|
|
@@ -173,6 +180,33 @@ const registryServer = Bun.serve({
|
|
|
173
180
|
}
|
|
174
181
|
return Response.json(searchResponse);
|
|
175
182
|
}
|
|
183
|
+
// 模拟镜像索引的部署窗口:前 flakyIndexMisses 次 404,随后返回完整索引
|
|
184
|
+
if (url.pathname === "/flaky-index") {
|
|
185
|
+
if (flakyIndexHits++ < flakyIndexMisses) {
|
|
186
|
+
return new Response("not found", { status: 404 });
|
|
187
|
+
}
|
|
188
|
+
return Response.json({
|
|
189
|
+
version: "4",
|
|
190
|
+
objects: [
|
|
191
|
+
{
|
|
192
|
+
package: {
|
|
193
|
+
name: "koishi-plugin-demo",
|
|
194
|
+
version: "2.0.0",
|
|
195
|
+
date: "2024-06-01T00:00:00Z",
|
|
196
|
+
keywords: ["koishi", "plugin"],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
// 模拟镜像索引挂起:延迟超过用例配置的 timeout,驱动超时重试路径
|
|
203
|
+
if (url.pathname === "/slow-index") {
|
|
204
|
+
slowIndexHits++;
|
|
205
|
+
await new Promise((resolve) =>
|
|
206
|
+
setTimeout(resolve, 300),
|
|
207
|
+
);
|
|
208
|
+
return Response.json({ version: "4", objects: [] });
|
|
209
|
+
}
|
|
176
210
|
// 模拟 registry 限流:恒定 429 + 极短的 Retry-After,驱动重试后失败
|
|
177
211
|
if (
|
|
178
212
|
url.pathname.includes("koishi-plugin-ratelimited")
|
|
@@ -264,8 +298,7 @@ type TestApp = InstanceType<typeof App>;
|
|
|
264
298
|
|
|
265
299
|
const app = new App();
|
|
266
300
|
|
|
267
|
-
|
|
268
|
-
app.plugin(memory as unknown as typeof memory.default);
|
|
301
|
+
app.plugin(memory);
|
|
269
302
|
app.plugin(http);
|
|
270
303
|
// Console 基类的 static inject 是 cordis 3 旧形态,与 Plugin.Constructor 期待类型不兼容,仅做类型层转型
|
|
271
304
|
app.plugin(
|
|
@@ -973,3 +1006,135 @@ describe("market 进阶链路", () => {
|
|
|
973
1006
|
20000,
|
|
974
1007
|
);
|
|
975
1008
|
});
|
|
1009
|
+
|
|
1010
|
+
describe("镜像索引瞬态重试", () => {
|
|
1011
|
+
/** 搭建配了 search.endpoint 的独立宿主(镜像索引经本测试桩提供)。 */
|
|
1012
|
+
async function createMirrorApp(search: {
|
|
1013
|
+
endpoint: string;
|
|
1014
|
+
timeout?: number;
|
|
1015
|
+
}) {
|
|
1016
|
+
const mirror = new App();
|
|
1017
|
+
mirror.plugin(memory);
|
|
1018
|
+
mirror.plugin(http);
|
|
1019
|
+
mirror.plugin(
|
|
1020
|
+
FakeConsole as unknown as Plugin.Constructor<TestApp>,
|
|
1021
|
+
);
|
|
1022
|
+
mirror.plugin(FakeLoader);
|
|
1023
|
+
mirror.plugin(market, {
|
|
1024
|
+
registry: {
|
|
1025
|
+
endpoint: `http://127.0.0.1:${registryServer.port}/`,
|
|
1026
|
+
},
|
|
1027
|
+
search,
|
|
1028
|
+
});
|
|
1029
|
+
await mirror.start();
|
|
1030
|
+
return mirror;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
it("镜像索引部署窗口 404 经退避重试后恢复", async () => {
|
|
1034
|
+
flakyIndexHits = 0;
|
|
1035
|
+
flakyIndexMisses = 2;
|
|
1036
|
+
const mirror = await createMirrorApp({
|
|
1037
|
+
endpoint: `http://127.0.0.1:${registryServer.port}/flaky-index`,
|
|
1038
|
+
});
|
|
1039
|
+
const svc = mirror.get("console.services.market");
|
|
1040
|
+
// start(true) 内部 await prepare:前两次 404,退避重试后第三次成功
|
|
1041
|
+
await svc?.start(true);
|
|
1042
|
+
expect(flakyIndexHits).toBeGreaterThanOrEqual(3);
|
|
1043
|
+
const payload = await svc?.get();
|
|
1044
|
+
expect(Object.keys(payload?.data ?? {})).toEqual([
|
|
1045
|
+
"koishi-plugin-demo",
|
|
1046
|
+
]);
|
|
1047
|
+
expect(payload?.total).toBe(1);
|
|
1048
|
+
await mirror.stop();
|
|
1049
|
+
}, 10000);
|
|
1050
|
+
|
|
1051
|
+
itQuiet(
|
|
1052
|
+
"镜像索引持续超时经重试后优雅失败",
|
|
1053
|
+
async () => {
|
|
1054
|
+
slowIndexHits = 0;
|
|
1055
|
+
const mirror = await createMirrorApp({
|
|
1056
|
+
endpoint: `http://127.0.0.1:${registryServer.port}/slow-index`,
|
|
1057
|
+
timeout: 50,
|
|
1058
|
+
});
|
|
1059
|
+
const svc = mirror.get("console.services.market");
|
|
1060
|
+
// 每次尝试 50ms 即超时,退避 1s + 2s 后仍失败;重试确实发生
|
|
1061
|
+
await svc?.start(true);
|
|
1062
|
+
expect(slowIndexHits).toBeGreaterThanOrEqual(3);
|
|
1063
|
+
// start 末尾的 refresh 会再触发一轮 collect,等待其失败落地
|
|
1064
|
+
await new Promise((resolve) =>
|
|
1065
|
+
setTimeout(resolve, 3600),
|
|
1066
|
+
);
|
|
1067
|
+
const error = svc?.["_error" as keyof typeof svc];
|
|
1068
|
+
expect(error).toBeDefined();
|
|
1069
|
+
await mirror.stop();
|
|
1070
|
+
},
|
|
1071
|
+
15000,
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
// 裸 TCP 桩:前 planned 次请求收到请求头后直接销毁连接(复现
|
|
1075
|
+
// 证书验证失败 / 连接重置等 fetch 网络层瞬态错误),随后回正常索引
|
|
1076
|
+
function startFlakyTcpServer(planned: number) {
|
|
1077
|
+
let hits = 0;
|
|
1078
|
+
const server = net.createServer((socket) => {
|
|
1079
|
+
socket.on("data", () => {
|
|
1080
|
+
hits++;
|
|
1081
|
+
if (hits <= planned) {
|
|
1082
|
+
socket.destroy();
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
const body = JSON.stringify({
|
|
1086
|
+
version: "4",
|
|
1087
|
+
objects: [
|
|
1088
|
+
{
|
|
1089
|
+
package: {
|
|
1090
|
+
name: "koishi-plugin-demo",
|
|
1091
|
+
version: "2.0.0",
|
|
1092
|
+
date: "2024-06-01T00:00:00Z",
|
|
1093
|
+
keywords: ["koishi", "plugin"],
|
|
1094
|
+
},
|
|
1095
|
+
},
|
|
1096
|
+
],
|
|
1097
|
+
});
|
|
1098
|
+
socket.end(
|
|
1099
|
+
`HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ${body.length}\r\nConnection: close\r\n\r\n${body}`,
|
|
1100
|
+
);
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
return new Promise<{
|
|
1104
|
+
port: number;
|
|
1105
|
+
hits: () => number;
|
|
1106
|
+
close: () => void;
|
|
1107
|
+
}>((resolve) => {
|
|
1108
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1109
|
+
const { port } =
|
|
1110
|
+
server.address() as net.AddressInfo;
|
|
1111
|
+
resolve({
|
|
1112
|
+
port,
|
|
1113
|
+
hits: () => hits,
|
|
1114
|
+
close: () => {
|
|
1115
|
+
server.close();
|
|
1116
|
+
},
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
it("网络层瞬态错误(连接重置)经退避重试后恢复", async () => {
|
|
1123
|
+
const stub = await startFlakyTcpServer(2);
|
|
1124
|
+
const mirror = await createMirrorApp({
|
|
1125
|
+
endpoint: `http://127.0.0.1:${stub.port}/`,
|
|
1126
|
+
});
|
|
1127
|
+
const svc = mirror.get("console.services.market");
|
|
1128
|
+
// 前两次连接被重置(fetch 抛 TypeError,plugin-http 包装为无 code
|
|
1129
|
+
// 无 response 的 HTTP.Error),退避重试后第三次成功
|
|
1130
|
+
await svc?.start(true);
|
|
1131
|
+
expect(stub.hits()).toBeGreaterThanOrEqual(3);
|
|
1132
|
+
const payload = await svc?.get();
|
|
1133
|
+
expect(Object.keys(payload?.data ?? {})).toEqual([
|
|
1134
|
+
"koishi-plugin-demo",
|
|
1135
|
+
]);
|
|
1136
|
+
expect(payload?.total).toBe(1);
|
|
1137
|
+
await mirror.stop();
|
|
1138
|
+
stub.close();
|
|
1139
|
+
}, 10000);
|
|
1140
|
+
});
|
package/src/node/installer.ts
CHANGED
|
@@ -174,7 +174,7 @@ class Installer extends Service {
|
|
|
174
174
|
Promise<Dict<Pick<RemotePackage, DependencyMetaKey>>>
|
|
175
175
|
> = {};
|
|
176
176
|
private manifest: LocalPackage;
|
|
177
|
-
private
|
|
177
|
+
declare private depTask: Promise<Dict<Dependency>>;
|
|
178
178
|
private flushData: () => void;
|
|
179
179
|
|
|
180
180
|
override config: Installer.Config;
|
package/src/node/market.ts
CHANGED
|
@@ -21,8 +21,9 @@ import { MarketProvider as BaseMarketProvider } from "../shared/index.ts";
|
|
|
21
21
|
const logger = new Logger("market");
|
|
22
22
|
|
|
23
23
|
// registry 接口对无认证请求有速率限制:搜索接口(/-/v1/search)超频时
|
|
24
|
-
// 返回 429,collect
|
|
25
|
-
//
|
|
24
|
+
// 返回 429,collect 一旦失败会中断整个市场数据刷新(prepare 置 _error,
|
|
25
|
+
// 页面清空)。对限流、请求超时、fetch 网络层瞬态错误与 5xx 做指数退避
|
|
26
|
+
// 重试;镜像索引的 404(部署窗口内版本化文件尚未就绪)同样按瞬态处理。
|
|
26
27
|
/** 首次请求失败后的最大重试次数 */
|
|
27
28
|
const maxRetries = 2;
|
|
28
29
|
/** 重试的基础退避时长,按尝试次数指数放大 */
|
|
@@ -63,13 +64,31 @@ class MarketProvider extends BaseMarketProvider {
|
|
|
63
64
|
super.start();
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
/**
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* 限流(429)、请求超时(abort 后无 response,code 为 ETIMEDOUT)、
|
|
69
|
+
* 服务端瞬态错误(408 / 5xx)可安全重试;allowNotFound 时把 404
|
|
70
|
+
* 也视为瞬态——镜像索引以「302 指向版本化文件」方式发布,切换文件
|
|
71
|
+
* 的部署窗口会出现短时 404。
|
|
72
|
+
*
|
|
73
|
+
* 无 code 且无 response 的 HTTP.Error 是 plugin-http 对 fetch 网络层
|
|
74
|
+
* 异常(证书验证失败、连接重置、DNS 等)的统一包装——代理节点切换
|
|
75
|
+
* 等瞬态网络抖动即此形态,重试可自愈。
|
|
76
|
+
*/
|
|
77
|
+
private isRetryable(
|
|
78
|
+
error: unknown,
|
|
79
|
+
allowNotFound = false,
|
|
80
|
+
): error is HTTP.Error {
|
|
68
81
|
if (!this.ctx.http.isError(error)) return false;
|
|
69
|
-
|
|
82
|
+
if (!error.response)
|
|
83
|
+
return (
|
|
84
|
+
error.code === undefined ||
|
|
85
|
+
error.code === "ETIMEDOUT"
|
|
86
|
+
);
|
|
87
|
+
const { status } = error.response;
|
|
70
88
|
return (
|
|
71
89
|
status === 429 ||
|
|
72
90
|
status === 408 ||
|
|
91
|
+
(allowNotFound && status === 404) ||
|
|
73
92
|
(status !== undefined && status >= 500)
|
|
74
93
|
);
|
|
75
94
|
}
|
|
@@ -82,6 +101,7 @@ class MarketProvider extends BaseMarketProvider {
|
|
|
82
101
|
http: HTTP,
|
|
83
102
|
url: string,
|
|
84
103
|
config?: RequestConfig,
|
|
104
|
+
allowNotFound = false,
|
|
85
105
|
): Promise<T> {
|
|
86
106
|
for (let attempt = 0; ; attempt++) {
|
|
87
107
|
try {
|
|
@@ -91,7 +111,7 @@ class MarketProvider extends BaseMarketProvider {
|
|
|
91
111
|
} catch (error) {
|
|
92
112
|
if (
|
|
93
113
|
attempt >= maxRetries ||
|
|
94
|
-
!this.isRetryable(error)
|
|
114
|
+
!this.isRetryable(error, allowNotFound)
|
|
95
115
|
)
|
|
96
116
|
throw error;
|
|
97
117
|
const retryAfter = Number(
|
|
@@ -126,6 +146,8 @@ class MarketProvider extends BaseMarketProvider {
|
|
|
126
146
|
const result = await this.request<SearchResult>(
|
|
127
147
|
this.http,
|
|
128
148
|
"",
|
|
149
|
+
undefined,
|
|
150
|
+
true,
|
|
129
151
|
);
|
|
130
152
|
this.scanner.objects = result.objects.filter(
|
|
131
153
|
(object) => !object.ignored,
|