@nvae/llmswitch 0.8.0 → 0.9.0
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/dist/adapters/opencode.js +22 -2
- package/dist/commands/prompts.js +63 -18
- package/dist/commands/tool.js +1 -0
- package/dist/store/profiles.js +10 -0
- package/dist/utils/model-metadata.js +179 -0
- package/package.json +1 -1
|
@@ -31,13 +31,33 @@ export function readOpenCodeAuth(path = getOpenCodeAuthPath()) {
|
|
|
31
31
|
return {};
|
|
32
32
|
return JSON.parse(readFileSync(path, "utf8"));
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Build one model entry for the OpenCode provider block. When metadata from
|
|
36
|
+
* models.lonae.com is available, expose the supported input/output modalities
|
|
37
|
+
* and attachment support, e.g.
|
|
38
|
+
* { "name": "id", "modalities": { "input": ["text","image"], "output": ["text"] }, "attachment": true }
|
|
39
|
+
*/
|
|
40
|
+
function buildOpenCodeModelEntry(id, meta) {
|
|
41
|
+
const entry = { name: id };
|
|
42
|
+
if (meta?.modalities) {
|
|
43
|
+
entry.modalities = {
|
|
44
|
+
input: meta.modalities.input?.length ? meta.modalities.input : ["text"],
|
|
45
|
+
output: meta.modalities.output?.length ? meta.modalities.output : ["text"],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (typeof meta?.attachment === "boolean") {
|
|
49
|
+
entry.attachment = meta.attachment;
|
|
50
|
+
}
|
|
51
|
+
return entry;
|
|
52
|
+
}
|
|
34
53
|
export function buildOpenCodeProviderBlock(profile, overrides) {
|
|
54
|
+
const metaById = profile.models.meta || {};
|
|
35
55
|
const models = {};
|
|
36
56
|
for (const id of profile.models.list) {
|
|
37
|
-
models[id] =
|
|
57
|
+
models[id] = buildOpenCodeModelEntry(id, metaById[id]);
|
|
38
58
|
}
|
|
39
59
|
if (!models[profile.models.default]) {
|
|
40
|
-
models[profile.models.default] =
|
|
60
|
+
models[profile.models.default] = buildOpenCodeModelEntry(profile.models.default, metaById[profile.models.default]);
|
|
41
61
|
}
|
|
42
62
|
const options = {
|
|
43
63
|
baseURL: overrides?.baseURL ||
|
package/dist/commands/prompts.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getPreset, presetsForTool } from "../presets/index.js";
|
|
|
6
6
|
import { detectApiFormat } from "../utils/detect-format.js";
|
|
7
7
|
import { assertValidProfileName, listProfiles, profileExists, saveProfile, } from "../store/profiles.js";
|
|
8
8
|
import { fetchModelList, preferResolvedBaseUrl, } from "../utils/fetch-models.js";
|
|
9
|
+
import { collectModelMeta, fetchModelMetadata, lookupModelMeta, } from "../utils/model-metadata.js";
|
|
9
10
|
import { normalizeBaseUrlForFormat } from "../utils/base-url.js";
|
|
10
11
|
import { maskSecret } from "../utils/fs.js";
|
|
11
12
|
export function isCancel(value) {
|
|
@@ -391,6 +392,7 @@ export async function promptProfileDraft(tool, partial = {}) {
|
|
|
391
392
|
models: {
|
|
392
393
|
default: defaultModel,
|
|
393
394
|
list: Array.from(new Set(modelList)),
|
|
395
|
+
meta: resolved.modelMeta,
|
|
394
396
|
},
|
|
395
397
|
proxy,
|
|
396
398
|
bridgeMode: tool === "codex" && apiFormat === "openai-chat"
|
|
@@ -475,37 +477,76 @@ export async function promptEditProfile(tool, current) {
|
|
|
475
477
|
/**
|
|
476
478
|
* Fetch models from the provider API (when possible), then let the user
|
|
477
479
|
* pick a default + a saved list. Falls back to manual text entry.
|
|
480
|
+
* Model metadata (modalities/attachment) is fetched from models.lonae.com
|
|
481
|
+
* while the model list is loading and attached to the final selection.
|
|
478
482
|
*/
|
|
479
483
|
export async function resolveModelsInteractive(input) {
|
|
484
|
+
// Best-effort: fetch metadata in the background so it overlaps with /models.
|
|
485
|
+
const metaPromise = tryFetchModelMetadata(input);
|
|
480
486
|
if (input.fixedDefault && input.fixedList?.length) {
|
|
481
487
|
const list = [...input.fixedList];
|
|
482
488
|
if (!list.includes(input.fixedDefault))
|
|
483
489
|
list.unshift(input.fixedDefault);
|
|
484
|
-
return {
|
|
490
|
+
return {
|
|
491
|
+
defaultModel: input.fixedDefault,
|
|
492
|
+
modelList: list,
|
|
493
|
+
modelMeta: collectModelMeta(await metaPromise, list),
|
|
494
|
+
};
|
|
485
495
|
}
|
|
486
496
|
if (input.fixedDefault && !input.fixedList) {
|
|
487
497
|
const fetched = await tryFetchModels(input);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
defaultModel: input.fixedDefault,
|
|
492
|
-
modelList: list,
|
|
493
|
-
resolvedBaseUrl: fetched.resolvedBaseUrl,
|
|
494
|
-
};
|
|
495
|
-
}
|
|
498
|
+
const list = fetched?.models.length
|
|
499
|
+
? Array.from(new Set([input.fixedDefault, ...fetched.models]))
|
|
500
|
+
: [input.fixedDefault];
|
|
496
501
|
return {
|
|
497
502
|
defaultModel: input.fixedDefault,
|
|
498
|
-
modelList:
|
|
503
|
+
modelList: list,
|
|
504
|
+
resolvedBaseUrl: fetched?.resolvedBaseUrl,
|
|
505
|
+
modelMeta: collectModelMeta(await metaPromise, list),
|
|
499
506
|
};
|
|
500
507
|
}
|
|
508
|
+
const catalog = await metaPromise;
|
|
501
509
|
const fetched = await tryFetchModels(input);
|
|
502
510
|
if (fetched && fetched.models.length > 0) {
|
|
503
|
-
const selected = await selectModelsFromFetched(fetched.models, input);
|
|
511
|
+
const selected = await selectModelsFromFetched(fetched.models, input, catalog);
|
|
504
512
|
return { ...selected, resolvedBaseUrl: fetched.resolvedBaseUrl };
|
|
505
513
|
}
|
|
506
|
-
return manualModelsEntry(input);
|
|
514
|
+
return manualModelsEntry(input, catalog);
|
|
507
515
|
}
|
|
508
|
-
|
|
516
|
+
/**
|
|
517
|
+
* Best-effort metadata fetch from models.lonae.com.
|
|
518
|
+
* Returns undefined (with a warning) when unreachable.
|
|
519
|
+
*/
|
|
520
|
+
async function tryFetchModelMetadata(input) {
|
|
521
|
+
try {
|
|
522
|
+
const catalog = await fetchModelMetadata({ proxy: input.proxy });
|
|
523
|
+
p.log.info(`已从 models.lonae.com 获取 ${Object.keys(catalog.full).length} 个模型的元数据`);
|
|
524
|
+
return catalog;
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
528
|
+
p.log.warn(`获取模型元数据失败(models.lonae.com):${msg}`);
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/** Human-readable modality hint for a model, e.g. "text/image → text · 支持附件". */
|
|
533
|
+
function metadataHint(modelId, catalog) {
|
|
534
|
+
const meta = catalog ? lookupModelMeta(catalog, modelId) : undefined;
|
|
535
|
+
if (!meta)
|
|
536
|
+
return undefined;
|
|
537
|
+
const parts = [];
|
|
538
|
+
if (meta.modalities) {
|
|
539
|
+
parts.push(`${meta.modalities.input.join("/")} → ${meta.modalities.output.join("/")}`);
|
|
540
|
+
}
|
|
541
|
+
if (meta.attachment === true)
|
|
542
|
+
parts.push("支持附件");
|
|
543
|
+
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
544
|
+
}
|
|
545
|
+
function joinHints(...hints) {
|
|
546
|
+
const joined = hints.filter(Boolean).join(" · ");
|
|
547
|
+
return joined || undefined;
|
|
548
|
+
}
|
|
549
|
+
async function selectModelsFromFetched(fetched, input, catalog) {
|
|
509
550
|
const preferredDefault = (input.preferredDefault && fetched.includes(input.preferredDefault)
|
|
510
551
|
? input.preferredDefault
|
|
511
552
|
: undefined) ||
|
|
@@ -518,7 +559,7 @@ async function selectModelsFromFetched(fetched, input) {
|
|
|
518
559
|
options: fetched.map((id) => ({
|
|
519
560
|
value: id,
|
|
520
561
|
label: id,
|
|
521
|
-
hint: id === input.preferredDefault ? "当前" : undefined,
|
|
562
|
+
hint: joinHints(id === input.preferredDefault ? "当前" : undefined, metadataHint(id, catalog)),
|
|
522
563
|
})),
|
|
523
564
|
initialValue: preferredDefault,
|
|
524
565
|
});
|
|
@@ -530,7 +571,7 @@ async function selectModelsFromFetched(fetched, input) {
|
|
|
530
571
|
options: fetched.map((id) => ({
|
|
531
572
|
value: id,
|
|
532
573
|
label: id,
|
|
533
|
-
hint: (input.preferredList || []).includes(id) ? "当前" : undefined,
|
|
574
|
+
hint: joinHints((input.preferredList || []).includes(id) ? "当前" : undefined, metadataHint(id, catalog)),
|
|
534
575
|
})),
|
|
535
576
|
initialValues: Array.from(new Set([defaultModel, ...preferredList, ...presetList])),
|
|
536
577
|
required: true,
|
|
@@ -540,9 +581,9 @@ async function selectModelsFromFetched(fetched, input) {
|
|
|
540
581
|
process.exit(0);
|
|
541
582
|
}
|
|
542
583
|
const modelList = Array.from(new Set([defaultModel, ...picked]));
|
|
543
|
-
return { defaultModel, modelList };
|
|
584
|
+
return { defaultModel, modelList, modelMeta: collectModelMeta(catalog, modelList) };
|
|
544
585
|
}
|
|
545
|
-
async function manualModelsEntry(input) {
|
|
586
|
+
async function manualModelsEntry(input, catalog) {
|
|
546
587
|
p.log.warn("未能自动获取模型列表,改为手动输入。");
|
|
547
588
|
let defaultModel = input.fixedDefault || input.preferredDefault;
|
|
548
589
|
if (!defaultModel) {
|
|
@@ -571,7 +612,11 @@ async function manualModelsEntry(input) {
|
|
|
571
612
|
if (!modelList.includes(defaultModel)) {
|
|
572
613
|
modelList = [defaultModel, ...modelList];
|
|
573
614
|
}
|
|
574
|
-
return {
|
|
615
|
+
return {
|
|
616
|
+
defaultModel,
|
|
617
|
+
modelList,
|
|
618
|
+
modelMeta: collectModelMeta(catalog, modelList),
|
|
619
|
+
};
|
|
575
620
|
}
|
|
576
621
|
export async function tryFetchModels(input) {
|
|
577
622
|
const spin = p.spinner();
|
package/dist/commands/tool.js
CHANGED
|
@@ -376,6 +376,7 @@ async function configureProfileModels(tool, profile) {
|
|
|
376
376
|
});
|
|
377
377
|
profile.models.default = resolved.defaultModel;
|
|
378
378
|
profile.models.list = resolved.modelList;
|
|
379
|
+
profile.models.meta = resolved.modelMeta;
|
|
379
380
|
if (resolved.resolvedBaseUrl &&
|
|
380
381
|
resolved.resolvedBaseUrl !== profile.baseUrl) {
|
|
381
382
|
p.log.info(`已根据可用接口将 Base URL 规范为 ${resolved.resolvedBaseUrl}(原:${profile.baseUrl})`);
|
package/dist/store/profiles.js
CHANGED
|
@@ -5,6 +5,13 @@ import { normalizeBaseUrlForFormat } from "../utils/base-url.js";
|
|
|
5
5
|
import { atomicWriteFile, ensureDir, maskSecret } from "../utils/fs.js";
|
|
6
6
|
import { getProfilePath, getProfilesDir, getStatePath, getToolStoreDir, } from "../utils/paths.js";
|
|
7
7
|
const NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
|
|
8
|
+
/** Keep only metadata entries whose model id is still in the list. */
|
|
9
|
+
function filterModelMeta(meta, list) {
|
|
10
|
+
if (!meta)
|
|
11
|
+
return undefined;
|
|
12
|
+
const filtered = Object.fromEntries(Object.entries(meta).filter(([id]) => list.includes(id)));
|
|
13
|
+
return Object.keys(filtered).length > 0 ? filtered : undefined;
|
|
14
|
+
}
|
|
8
15
|
export function assertValidProfileName(name) {
|
|
9
16
|
if (!NAME_RE.test(name)) {
|
|
10
17
|
throw new Error(`无效的 profile 名称「${name}」。仅允许字母、数字、下划线、连字符,且以字母或数字开头。`);
|
|
@@ -115,6 +122,7 @@ export function saveProfile(tool, profile) {
|
|
|
115
122
|
const list = Array.from(new Set([profile.models.default, profile.models.fast, ...(profile.models.list || [])]
|
|
116
123
|
.filter(Boolean)
|
|
117
124
|
.map((m) => m.trim())));
|
|
125
|
+
const meta = filterModelMeta(profile.models.meta, list);
|
|
118
126
|
const next = {
|
|
119
127
|
...profile,
|
|
120
128
|
displayName: profile.displayName || profile.name,
|
|
@@ -124,6 +132,7 @@ export function saveProfile(tool, profile) {
|
|
|
124
132
|
default: profile.models.default.trim(),
|
|
125
133
|
fast: profile.models.fast?.trim() || undefined,
|
|
126
134
|
list,
|
|
135
|
+
meta,
|
|
127
136
|
},
|
|
128
137
|
headers: profile.headers || {},
|
|
129
138
|
updatedAt: new Date().toISOString(),
|
|
@@ -228,6 +237,7 @@ function normalizeProfile(raw, fallbackName) {
|
|
|
228
237
|
default: raw.models?.default || list[0] || "",
|
|
229
238
|
fast: raw.models?.fast || undefined,
|
|
230
239
|
list: list.length ? list : raw.models?.default ? [raw.models.default] : [],
|
|
240
|
+
meta: filterModelMeta(raw.models?.meta, list),
|
|
231
241
|
},
|
|
232
242
|
proxy: normalizeProxyValue(raw.proxy),
|
|
233
243
|
bridgeMode: raw.bridgeMode,
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { requestWithNodeTransport } from "../bridge/transport.js";
|
|
2
|
+
export const MODEL_METADATA_SOURCE = "https://models.lonae.com";
|
|
3
|
+
const DEFAULT_ENDPOINT = `${MODEL_METADATA_SOURCE}/api/v1/models`;
|
|
4
|
+
const PAGE_SIZE = 1000;
|
|
5
|
+
/** Trailing date stamp, e.g. "-20250219" in "claude-3-7-sonnet-20250219". */
|
|
6
|
+
const DATE_SUFFIX_RE = /-?\d{8}$/;
|
|
7
|
+
/**
|
|
8
|
+
* Normalize a model id for fuzzy matching: lowercase, drop separators, and
|
|
9
|
+
* align dotted version segments ("claude-3.7-sonnet" ↔ "claude-3-7-sonnet").
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeModelKey(value) {
|
|
12
|
+
return value
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/\./g, "-")
|
|
15
|
+
.replace(/[^a-z0-9]/g, "");
|
|
16
|
+
}
|
|
17
|
+
/** Normalize a model id ignoring a trailing date stamp. */
|
|
18
|
+
export function normalizeModelKeyLoose(value) {
|
|
19
|
+
const bare = value.replace(DATE_SUFFIX_RE, "");
|
|
20
|
+
return normalizeModelKey(bare) || normalizeModelKey(value);
|
|
21
|
+
}
|
|
22
|
+
function asModalityList(value) {
|
|
23
|
+
if (!Array.isArray(value))
|
|
24
|
+
return undefined;
|
|
25
|
+
const list = value.filter((item) => typeof item === "string" && !!item.trim());
|
|
26
|
+
return list.length > 0 ? list : undefined;
|
|
27
|
+
}
|
|
28
|
+
function parseModalities(row) {
|
|
29
|
+
const nested = row.modalities;
|
|
30
|
+
const input = (nested && typeof nested === "object"
|
|
31
|
+
? asModalityList(nested.input)
|
|
32
|
+
: undefined) ?? asModalityList(row.inputModalities);
|
|
33
|
+
const output = (nested && typeof nested === "object"
|
|
34
|
+
? asModalityList(nested.output)
|
|
35
|
+
: undefined) ?? asModalityList(row.outputModalities);
|
|
36
|
+
if (!input && !output)
|
|
37
|
+
return undefined;
|
|
38
|
+
return {
|
|
39
|
+
input: input ?? ["text"],
|
|
40
|
+
output: output ?? ["text"],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Insert into a normalized bucket, preferring undated ids as canonical. */
|
|
44
|
+
function indexNorm(bucket, datedMetas, key, meta, dated) {
|
|
45
|
+
if (!key)
|
|
46
|
+
return;
|
|
47
|
+
const existing = bucket[key];
|
|
48
|
+
if (!existing) {
|
|
49
|
+
bucket[key] = meta;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (datedMetas.has(existing) && !dated)
|
|
53
|
+
bucket[key] = meta;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build the lookup catalog from a /api/v1/models payload.
|
|
57
|
+
* Indexes: full id ("lab/model"), bare id ("model"), plus normalized variants
|
|
58
|
+
* (date-stamp tolerant) so "claude-3.7-sonnet" can find
|
|
59
|
+
* "claude-3-7-sonnet-20250219".
|
|
60
|
+
*/
|
|
61
|
+
export function parseModelMetadata(payload) {
|
|
62
|
+
const catalog = { full: {}, bare: {}, norm: {} };
|
|
63
|
+
const rows = payload && typeof payload === "object"
|
|
64
|
+
? payload.data
|
|
65
|
+
: null;
|
|
66
|
+
if (!Array.isArray(rows))
|
|
67
|
+
return catalog;
|
|
68
|
+
const datedMetas = new Set();
|
|
69
|
+
for (const item of rows) {
|
|
70
|
+
if (!item || typeof item !== "object")
|
|
71
|
+
continue;
|
|
72
|
+
const row = item;
|
|
73
|
+
if (typeof row.id !== "string" || !row.id.trim())
|
|
74
|
+
continue;
|
|
75
|
+
const id = row.id.trim();
|
|
76
|
+
const meta = {
|
|
77
|
+
id,
|
|
78
|
+
name: typeof row.name === "string" && row.name.trim() ? row.name.trim() : undefined,
|
|
79
|
+
modalities: parseModalities(row),
|
|
80
|
+
attachment: typeof row.attachment === "boolean" ? row.attachment : undefined,
|
|
81
|
+
};
|
|
82
|
+
const bareIndex = id.indexOf("/");
|
|
83
|
+
const bare = bareIndex >= 0 ? id.slice(bareIndex + 1).trim() : id;
|
|
84
|
+
const dated = DATE_SUFFIX_RE.test(bare);
|
|
85
|
+
if (dated)
|
|
86
|
+
datedMetas.add(meta);
|
|
87
|
+
const fullKey = id.toLowerCase();
|
|
88
|
+
const bareKey = bare.toLowerCase();
|
|
89
|
+
const normKey = normalizeModelKey(bare);
|
|
90
|
+
const looseKey = normalizeModelKeyLoose(bare);
|
|
91
|
+
if (!catalog.full[fullKey])
|
|
92
|
+
catalog.full[fullKey] = meta;
|
|
93
|
+
if (bare && !catalog.bare[bareKey])
|
|
94
|
+
catalog.bare[bareKey] = meta;
|
|
95
|
+
indexNorm(catalog.norm, datedMetas, normKey, meta, dated);
|
|
96
|
+
if (looseKey !== normKey)
|
|
97
|
+
indexNorm(catalog.norm, datedMetas, looseKey, meta, dated);
|
|
98
|
+
}
|
|
99
|
+
return catalog;
|
|
100
|
+
}
|
|
101
|
+
/** Look up metadata for a profile model id (exact → bare → normalized). */
|
|
102
|
+
export function lookupModelMeta(catalog, modelId) {
|
|
103
|
+
const trimmed = modelId.trim();
|
|
104
|
+
if (!trimmed)
|
|
105
|
+
return undefined;
|
|
106
|
+
const bareIndex = trimmed.indexOf("/");
|
|
107
|
+
const bare = bareIndex >= 0 ? trimmed.slice(bareIndex + 1).trim() : trimmed;
|
|
108
|
+
return (catalog.full[trimmed.toLowerCase()] ??
|
|
109
|
+
catalog.bare[bare.toLowerCase()] ??
|
|
110
|
+
catalog.norm[normalizeModelKey(bare)] ??
|
|
111
|
+
catalog.norm[normalizeModelKeyLoose(bare)]);
|
|
112
|
+
}
|
|
113
|
+
/** Collect metadata for a set of profile model ids. */
|
|
114
|
+
export function collectModelMeta(catalog, models) {
|
|
115
|
+
if (!catalog)
|
|
116
|
+
return undefined;
|
|
117
|
+
const meta = {};
|
|
118
|
+
for (const id of models) {
|
|
119
|
+
const found = lookupModelMeta(catalog, id);
|
|
120
|
+
if (found)
|
|
121
|
+
meta[id] = found;
|
|
122
|
+
}
|
|
123
|
+
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Fetch the model metadata catalog from models.lonae.com.
|
|
127
|
+
* Paginates automatically (page_size capped at 1000 per request).
|
|
128
|
+
*/
|
|
129
|
+
export async function fetchModelMetadata(options = {}) {
|
|
130
|
+
const endpoint = options.endpoint || DEFAULT_ENDPOINT;
|
|
131
|
+
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
132
|
+
const rows = [];
|
|
133
|
+
let total = Infinity;
|
|
134
|
+
let page = 1;
|
|
135
|
+
while (rows.length < total) {
|
|
136
|
+
const url = `${endpoint}${endpoint.includes("?") ? "&" : "?"}page_size=${PAGE_SIZE}&page=${page}`;
|
|
137
|
+
const payload = await requestJson(url, options.proxy, timeoutMs);
|
|
138
|
+
const batch = payload && typeof payload === "object"
|
|
139
|
+
? payload.data
|
|
140
|
+
: null;
|
|
141
|
+
if (!Array.isArray(batch) || batch.length === 0)
|
|
142
|
+
break;
|
|
143
|
+
rows.push(...batch);
|
|
144
|
+
const totalRaw = payload && typeof payload === "object"
|
|
145
|
+
? payload.meta?.total
|
|
146
|
+
: undefined;
|
|
147
|
+
total = typeof totalRaw === "number" && totalRaw > 0 ? totalRaw : rows.length;
|
|
148
|
+
page += 1;
|
|
149
|
+
}
|
|
150
|
+
return parseModelMetadata({ data: rows });
|
|
151
|
+
}
|
|
152
|
+
async function requestJson(url, proxy, timeoutMs) {
|
|
153
|
+
const controller = new AbortController();
|
|
154
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
155
|
+
try {
|
|
156
|
+
const res = await requestWithNodeTransport({
|
|
157
|
+
url,
|
|
158
|
+
method: "GET",
|
|
159
|
+
headers: { Accept: "application/json" },
|
|
160
|
+
proxy,
|
|
161
|
+
signal: controller.signal,
|
|
162
|
+
totalTimeoutMs: timeoutMs,
|
|
163
|
+
});
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
const body = (await res.text().catch(() => "")).slice(0, 200);
|
|
166
|
+
throw new Error(`HTTP ${res.status}${body ? `: ${body}` : ""}`);
|
|
167
|
+
}
|
|
168
|
+
return await res.json();
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
172
|
+
throw new Error(`请求超时(${timeoutMs}ms)`);
|
|
173
|
+
}
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
}
|
|
179
|
+
}
|