@koishi-ce/plugin-market 1.0.12 → 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/lib/index.d.ts CHANGED
@@ -113,6 +113,10 @@ declare class MarketProvider$1 extends MarketProvider {
113
113
  * 服务端瞬态错误(408 / 5xx)可安全重试;allowNotFound 时把 404
114
114
  * 也视为瞬态——镜像索引以「302 指向版本化文件」方式发布,切换文件
115
115
  * 的部署窗口会出现短时 404。
116
+ *
117
+ * 无 code 且无 response 的 HTTP.Error 是 plugin-http 对 fetch 网络层
118
+ * 异常(证书验证失败、连接重置、DNS 等)的统一包装——代理节点切换
119
+ * 等瞬态网络抖动即此形态,重试可自愈。
116
120
  */
117
121
  private isRetryable;
118
122
  /**
package/lib/index.mjs CHANGED
@@ -359,10 +359,14 @@ var MarketProvider$1 = class extends MarketProvider {
359
359
  * 服务端瞬态错误(408 / 5xx)可安全重试;allowNotFound 时把 404
360
360
  * 也视为瞬态——镜像索引以「302 指向版本化文件」方式发布,切换文件
361
361
  * 的部署窗口会出现短时 404。
362
+ *
363
+ * 无 code 且无 response 的 HTTP.Error 是 plugin-http 对 fetch 网络层
364
+ * 异常(证书验证失败、连接重置、DNS 等)的统一包装——代理节点切换
365
+ * 等瞬态网络抖动即此形态,重试可自愈。
362
366
  */
363
367
  isRetryable(error, allowNotFound = false) {
364
368
  if (!this.ctx.http.isError(error)) return false;
365
- if (!error.response) return error.code === "ETIMEDOUT";
369
+ if (!error.response) return error.code === void 0 || error.code === "ETIMEDOUT";
366
370
  const { status } = error.response;
367
371
  return status === 429 || status === 408 || allowNotFound && status === 404 || status !== void 0 && status >= 500;
368
372
  }
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.12",
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/client": "^1.1.0",
64
- "@koishi-ce/loader": "^1.0.6",
65
- "@koishi-ce/plugin-config": "^1.1.0",
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 "@koishijs/plugin-database-memory";
22
+ import memory from "@koishi-ce/plugin-database-memory";
22
23
 
23
24
  /**
24
25
  * market 插件测试:
@@ -297,8 +298,7 @@ type TestApp = InstanceType<typeof App>;
297
298
 
298
299
  const app = new App();
299
300
 
300
- // 同 admin:CJS 实现配 ESM 声明,nodenext 互操作视图多包一层 default,类型层穿透取真实类
301
- app.plugin(memory as unknown as typeof memory.default);
301
+ app.plugin(memory);
302
302
  app.plugin(http);
303
303
  // Console 基类的 static inject 是 cordis 3 旧形态,与 Plugin.Constructor 期待类型不兼容,仅做类型层转型
304
304
  app.plugin(
@@ -1014,9 +1014,7 @@ describe("镜像索引瞬态重试", () => {
1014
1014
  timeout?: number;
1015
1015
  }) {
1016
1016
  const mirror = new App();
1017
- mirror.plugin(
1018
- memory as unknown as typeof memory.default,
1019
- );
1017
+ mirror.plugin(memory);
1020
1018
  mirror.plugin(http);
1021
1019
  mirror.plugin(
1022
1020
  FakeConsole as unknown as Plugin.Constructor<TestApp>,
@@ -1072,4 +1070,71 @@ describe("镜像索引瞬态重试", () => {
1072
1070
  },
1073
1071
  15000,
1074
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);
1075
1140
  });
@@ -174,7 +174,7 @@ class Installer extends Service {
174
174
  Promise<Dict<Pick<RemotePackage, DependencyMetaKey>>>
175
175
  > = {};
176
176
  private manifest: LocalPackage;
177
- private declare depTask: Promise<Dict<Dependency>>;
177
+ declare private depTask: Promise<Dict<Dependency>>;
178
178
  private flushData: () => void;
179
179
 
180
180
  override config: Installer.Config;
@@ -22,8 +22,8 @@ const logger = new Logger("market");
22
22
 
23
23
  // registry 接口对无认证请求有速率限制:搜索接口(/-/v1/search)超频时
24
24
  // 返回 429,collect 一旦失败会中断整个市场数据刷新(prepare 置 _error,
25
- // 页面清空)。对限流、请求超时与 5xx 瞬态错误做指数退避重试;镜像索引
26
- // 的 404(部署窗口内版本化文件尚未就绪)同样按瞬态处理。
25
+ // 页面清空)。对限流、请求超时、fetch 网络层瞬态错误与 5xx 做指数退避
26
+ // 重试;镜像索引的 404(部署窗口内版本化文件尚未就绪)同样按瞬态处理。
27
27
  /** 首次请求失败后的最大重试次数 */
28
28
  const maxRetries = 2;
29
29
  /** 重试的基础退避时长,按尝试次数指数放大 */
@@ -69,13 +69,21 @@ class MarketProvider extends BaseMarketProvider {
69
69
  * 服务端瞬态错误(408 / 5xx)可安全重试;allowNotFound 时把 404
70
70
  * 也视为瞬态——镜像索引以「302 指向版本化文件」方式发布,切换文件
71
71
  * 的部署窗口会出现短时 404。
72
+ *
73
+ * 无 code 且无 response 的 HTTP.Error 是 plugin-http 对 fetch 网络层
74
+ * 异常(证书验证失败、连接重置、DNS 等)的统一包装——代理节点切换
75
+ * 等瞬态网络抖动即此形态,重试可自愈。
72
76
  */
73
77
  private isRetryable(
74
78
  error: unknown,
75
79
  allowNotFound = false,
76
80
  ): error is HTTP.Error {
77
81
  if (!this.ctx.http.isError(error)) return false;
78
- if (!error.response) return error.code === "ETIMEDOUT";
82
+ if (!error.response)
83
+ return (
84
+ error.code === undefined ||
85
+ error.code === "ETIMEDOUT"
86
+ );
79
87
  const { status } = error.response;
80
88
  return (
81
89
  status === 429 ||