@mangoc/dsh-sub2api-sync 0.2.3 → 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 +10 -0
- package/lib/index.js +47 -1
- package/package.json +1 -1
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
|
@@ -895,6 +895,7 @@ var Sub2apiSyncEngine = class {
|
|
|
895
895
|
const site = await this.siteName?.().catch(() => undefined);
|
|
896
896
|
const syncWarnings = [];
|
|
897
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,站点名可解析后的同步会自动更正");
|
|
898
899
|
const effective = {
|
|
899
900
|
...config,
|
|
900
901
|
selectedKeys: selection ?? config.selectedKeys
|
|
@@ -1168,6 +1169,14 @@ else if (isOwnedRoute(route)) {
|
|
|
1168
1169
|
//#region src/icon.ts
|
|
1169
1170
|
/** How long a resolved icon URL is trusted before re-reading the HTML. */
|
|
1170
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;
|
|
1171
1180
|
/** Discovery + byte fetches cap, so an offline instance cannot stall callers. */
|
|
1172
1181
|
const TIMEOUT_MS = 2500;
|
|
1173
1182
|
/** Refuse to proxy absurd payloads. */
|
|
@@ -1252,6 +1261,8 @@ function guessContentType(url) {
|
|
|
1252
1261
|
}
|
|
1253
1262
|
function createInstanceProbe(options = {}) {
|
|
1254
1263
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
1264
|
+
const retryDelayMs = options.retryDelayMs ?? RETRY_DELAY_MS;
|
|
1265
|
+
const negativeTtlMs = options.negativeTtlMs ?? NEGATIVE_TTL_MS;
|
|
1255
1266
|
const cache = new Map();
|
|
1256
1267
|
const metaOf = async (instance) => {
|
|
1257
1268
|
const base = instance.trim().replace(/\/+$/, "");
|
|
@@ -1266,6 +1277,9 @@ function createInstanceProbe(options = {}) {
|
|
|
1266
1277
|
});
|
|
1267
1278
|
return meta;
|
|
1268
1279
|
};
|
|
1280
|
+
const wait = (ms) => new Promise((resolve) => {
|
|
1281
|
+
setTimeout(resolve, ms);
|
|
1282
|
+
});
|
|
1269
1283
|
return {
|
|
1270
1284
|
icon: async (instance) => {
|
|
1271
1285
|
const meta = await metaOf(instance);
|
|
@@ -1284,7 +1298,27 @@ function createInstanceProbe(options = {}) {
|
|
|
1284
1298
|
return undefined;
|
|
1285
1299
|
}
|
|
1286
1300
|
},
|
|
1287
|
-
siteName: async (instance) =>
|
|
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
|
+
}
|
|
1288
1322
|
};
|
|
1289
1323
|
}
|
|
1290
1324
|
|
|
@@ -2636,6 +2670,14 @@ function apply(ctx, config) {
|
|
|
2636
2670
|
const mutateOwn = (ops) => mutateWithRetry(ctx.settings, OWN_NS, () => ops, { missingError: "sub2api-sync: 自身 settings 命名空间未注册" });
|
|
2637
2671
|
const instance = () => source().baseURL?.trim() ?? "";
|
|
2638
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
|
+
};
|
|
2639
2681
|
const oauthAttempts = new OAuthAttemptStore();
|
|
2640
2682
|
const deps = {
|
|
2641
2683
|
instance,
|
|
@@ -2650,6 +2692,7 @@ function apply(ctx, config) {
|
|
|
2650
2692
|
value: email
|
|
2651
2693
|
}]).catch(() => undefined);
|
|
2652
2694
|
const outcome = await gateway.login(email, pwd);
|
|
2695
|
+
warmProbe();
|
|
2653
2696
|
if (outcome.kind === "2fa") {
|
|
2654
2697
|
pendingTypedPassword = typed;
|
|
2655
2698
|
return {
|
|
@@ -2667,6 +2710,7 @@ function apply(ctx, config) {
|
|
|
2667
2710
|
},
|
|
2668
2711
|
login2FA: async (code) => {
|
|
2669
2712
|
await gateway.login2FA(code);
|
|
2713
|
+
warmProbe();
|
|
2670
2714
|
if (pendingTypedPassword !== undefined) {
|
|
2671
2715
|
await credentials.set(panelPasswordRef(instance()), pendingTypedPassword).catch(() => undefined);
|
|
2672
2716
|
pendingTypedPassword = undefined;
|
|
@@ -2825,6 +2869,7 @@ function apply(ctx, config) {
|
|
|
2825
2869
|
});
|
|
2826
2870
|
await gateway.storeOAuthTokens(pair);
|
|
2827
2871
|
oauthAttempts.settle(attempt.id, "completed");
|
|
2872
|
+
warmProbe();
|
|
2828
2873
|
const successMessage = lang === "en" ? "You can close this page and return to the desktop app to continue." : "你可以关闭此页面,返回桌面端继续工作。";
|
|
2829
2874
|
return { html: callbackPageHtml("success", successMessage, lang) };
|
|
2830
2875
|
} catch (cause) {
|
|
@@ -2834,6 +2879,7 @@ function apply(ctx, config) {
|
|
|
2834
2879
|
}
|
|
2835
2880
|
},
|
|
2836
2881
|
oauthStatus: async (attemptId) => {
|
|
2882
|
+
warmProbe();
|
|
2837
2883
|
const attempt = attemptId !== undefined ? oauthAttempts.peek(attemptId) : undefined;
|
|
2838
2884
|
const email = source().email;
|
|
2839
2885
|
return {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mangoc/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.
|
|
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",
|