dsh-sub2api-sync 0.2.2 → 0.2.4

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/icon.d.ts CHANGED
@@ -41,9 +41,19 @@ export declare function parseSiteTitle(html: string): string | undefined;
41
41
  * the closure, so the icon and site-name consumers share one HTML read per
42
42
  * TTL window. Errors never throw — an unavailable answer resolves to
43
43
  * `undefined` and the client keeps its fallbacks.
44
+ *
45
+ * Title caching is two-tier: a RESOLVED title rides the shared 10-minute
46
+ * window, while a title-less result is only trusted for
47
+ * {@link NEGATIVE_TTL_MS} and one in-call retry backs the first read — the
48
+ * login-to-first-sync window is exactly when a cold connection would
49
+ * otherwise brand every provider with the `sub2api` fallback name.
44
50
  */
45
51
  export declare function createInstanceProbe(options?: {
46
52
  fetchImpl?: typeof fetch;
53
+ /** Test knob: the title-less retry wait (default {@link RETRY_DELAY_MS}). */
54
+ retryDelayMs?: number;
55
+ /** Test knob: the title-less negative-cache window (default {@link NEGATIVE_TTL_MS}). */
56
+ negativeTtlMs?: number;
47
57
  }): {
48
58
  icon: (instance: string) => Promise<IconPayload | undefined>;
49
59
  siteName: (instance: string) => Promise<string | undefined>;
package/lib/index.js CHANGED
@@ -305,12 +305,13 @@ function buildProviderProfile(input) {
305
305
  const reasoning = input.template?.reasoningEfforts;
306
306
  const override = input.retryPolicy ?? {};
307
307
  const retryPolicy = {
308
- ...SUB2API_RETRY_POLICY,
309
- ...override,
310
- retryableCodes: [...override.retryableCodes ?? SUB2API_RETRY_POLICY.retryableCodes],
308
+ mode: override.mode ?? SUB2API_RETRY_POLICY.mode,
309
+ maxRetries: override.maxRetries ?? SUB2API_RETRY_POLICY.maxRetries,
310
+ retryableCodes: override.retryableCodes !== undefined && override.retryableCodes.length > 0 ? [...override.retryableCodes] : [...SUB2API_RETRY_POLICY.retryableCodes],
311
311
  backoff: {
312
- ...SUB2API_RETRY_POLICY.backoff,
313
- ...override.backoff
312
+ initialDelayMs: override.backoff?.initialDelayMs ?? SUB2API_RETRY_POLICY.backoff.initialDelayMs,
313
+ maxDelayMs: override.backoff?.maxDelayMs ?? SUB2API_RETRY_POLICY.backoff.maxDelayMs,
314
+ jitterRatio: override.backoff?.jitterRatio ?? SUB2API_RETRY_POLICY.backoff.jitterRatio
314
315
  }
315
316
  };
316
317
  return {
@@ -894,6 +895,7 @@ var Sub2apiSyncEngine = class {
894
895
  const site = await this.siteName?.().catch(() => undefined);
895
896
  const syncWarnings = [];
896
897
  if (keyWalkTruncated) syncWarnings.push("面板 key 数量超过单次同步的读取上限(10 页 × 100 = 1000 把)——超出部分未参与同步,也不会被移除");
898
+ if (site === undefined && this.siteName !== undefined && (config.providerNameTemplate ?? "").includes("{siteName}")) syncWarnings.push("实例站点名探测失败(实例不可达或页面 <title> 缺失)——displayName 暂用回退名 sub2api,站点名可解析后的同步会自动更正");
897
899
  const effective = {
898
900
  ...config,
899
901
  selectedKeys: selection ?? config.selectedKeys
@@ -1167,6 +1169,14 @@ else if (isOwnedRoute(route)) {
1167
1169
  //#region src/icon.ts
1168
1170
  /** How long a resolved icon URL is trusted before re-reading the HTML. */
1169
1171
  const URL_TTL_MS = 6e5;
1172
+ /** How long a TITLE-LESS probe result is trusted before re-reading: the sync
1173
+ * that renders `{siteName}` must not pin a 10-minute negative on a cold
1174
+ * connection, or the first sync after login names every provider with the
1175
+ * `sub2api` fallback until the cache expires. */
1176
+ const NEGATIVE_TTL_MS = 15e3;
1177
+ /** One in-call retry wait when a fresh probe still answers without a title
1178
+ * (cold DNS/TLS or a momentary gateway hiccup — not proof the title is absent). */
1179
+ const RETRY_DELAY_MS = 1200;
1170
1180
  /** Discovery + byte fetches cap, so an offline instance cannot stall callers. */
1171
1181
  const TIMEOUT_MS = 2500;
1172
1182
  /** Refuse to proxy absurd payloads. */
@@ -1251,6 +1261,8 @@ function guessContentType(url) {
1251
1261
  }
1252
1262
  function createInstanceProbe(options = {}) {
1253
1263
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
1264
+ const retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
1265
+ const negativeTtlMs = options.negativeTtlMs ?? NEGATIVE_TTL_MS;
1254
1266
  const cache = new Map();
1255
1267
  const metaOf = async (instance) => {
1256
1268
  const base = instance.trim().replace(/\/+$/, "");
@@ -1265,6 +1277,9 @@ function createInstanceProbe(options = {}) {
1265
1277
  });
1266
1278
  return meta;
1267
1279
  };
1280
+ const wait = (ms) => new Promise((resolve) => {
1281
+ setTimeout(resolve, ms);
1282
+ });
1268
1283
  return {
1269
1284
  icon: async (instance) => {
1270
1285
  const meta = await metaOf(instance);
@@ -1283,7 +1298,27 @@ function createInstanceProbe(options = {}) {
1283
1298
  return undefined;
1284
1299
  }
1285
1300
  },
1286
- siteName: async (instance) => (await metaOf(instance))?.title
1301
+ siteName: async (instance) => {
1302
+ const base = instance.trim().replace(/\/+$/, "");
1303
+ if (!/^https?:\/\//i.test(base)) return undefined;
1304
+ const now = Date.now();
1305
+ const hit = cache.get(base);
1306
+ if (hit !== undefined && hit.meta.title !== undefined && now - hit.at < URL_TTL_MS) return hit.meta.title;
1307
+ if (hit !== undefined && hit.meta.title === undefined && now - hit.at < negativeTtlMs) return undefined;
1308
+ const first = await probeInstance(base, fetchImpl);
1309
+ cache.set(base, {
1310
+ meta: first,
1311
+ at: Date.now()
1312
+ });
1313
+ if (first.title !== undefined) return first.title;
1314
+ await wait(retryDelayMs);
1315
+ const second = await probeInstance(base, fetchImpl);
1316
+ cache.set(base, {
1317
+ meta: second,
1318
+ at: Date.now()
1319
+ });
1320
+ return second.title;
1321
+ }
1287
1322
  };
1288
1323
  }
1289
1324
 
@@ -2635,6 +2670,14 @@ function apply(ctx, config) {
2635
2670
  const mutateOwn = (ops) => mutateWithRetry(ctx.settings, OWN_NS, () => ops, { missingError: "sub2api-sync: 自身 settings 命名空间未注册" });
2636
2671
  const instance = () => source().baseURL?.trim() ?? "";
2637
2672
  const instanceProbe = createInstanceProbe();
2673
+ /** Warm the site-name cache at auth events. The first sync right after a
2674
+ * login/authorization renders `{siteName}` — a cold probe there would
2675
+ * brand the providers with the `sub2api` fallback name. Fire-and-forget:
2676
+ * the probe's own negative-TTL window bounds any retry fetching. */
2677
+ const warmProbe = () => {
2678
+ const base = instance();
2679
+ if (/^https?:\/\//i.test(base)) void instanceProbe.siteName(base).catch(() => undefined);
2680
+ };
2638
2681
  const oauthAttempts = new OAuthAttemptStore();
2639
2682
  const deps = {
2640
2683
  instance,
@@ -2649,6 +2692,7 @@ function apply(ctx, config) {
2649
2692
  value: email
2650
2693
  }]).catch(() => undefined);
2651
2694
  const outcome = await gateway.login(email, pwd);
2695
+ warmProbe();
2652
2696
  if (outcome.kind === "2fa") {
2653
2697
  pendingTypedPassword = typed;
2654
2698
  return {
@@ -2666,6 +2710,7 @@ function apply(ctx, config) {
2666
2710
  },
2667
2711
  login2FA: async (code) => {
2668
2712
  await gateway.login2FA(code);
2713
+ warmProbe();
2669
2714
  if (pendingTypedPassword !== undefined) {
2670
2715
  await credentials.set(panelPasswordRef(instance()), pendingTypedPassword).catch(() => undefined);
2671
2716
  pendingTypedPassword = undefined;
@@ -2824,6 +2869,7 @@ function apply(ctx, config) {
2824
2869
  });
2825
2870
  await gateway.storeOAuthTokens(pair);
2826
2871
  oauthAttempts.settle(attempt.id, "completed");
2872
+ warmProbe();
2827
2873
  const successMessage = lang === "en" ? "You can close this page and return to the desktop app to continue." : "你可以关闭此页面,返回桌面端继续工作。";
2828
2874
  return { html: callbackPageHtml("success", successMessage, lang) };
2829
2875
  } catch (cause) {
@@ -2833,6 +2879,7 @@ function apply(ctx, config) {
2833
2879
  }
2834
2880
  },
2835
2881
  oauthStatus: async (attemptId) => {
2882
+ warmProbe();
2836
2883
  const attempt = attemptId !== undefined ? oauthAttempts.peek(attemptId) : undefined;
2837
2884
  const email = source().email;
2838
2885
  return {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-sub2api-sync",
3
3
  "description": "Sync one sub2api instance's groups, keys, and models into dsh-llm-pi-ai provider routes: panel-driven login (password / OAuth), one relay key one provider.",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "license": "MIT",
6
6
  "author": "Shuai-S",
7
7
  "homepage": "https://github.com/Shuai-S/dsh-sub2api-sync#readme",