@zhin.js/plugin-nettools 0.1.2 → 0.1.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/CHANGELOG.md +20 -0
- package/commands/dns/[name]/[[type]]/index.js +5 -5
- package/commands/dns/[name]/[[type]]/index.ts +5 -5
- package/commands/domain-price/[suffix]/[[order]]/index.js +5 -5
- package/commands/domain-price/[suffix]/[[order]]/index.ts +5 -5
- package/commands/ping/[host]/index.js +4 -4
- package/commands/ping/[host]/index.ts +4 -4
- package/commands/whois/[domain]/index.js +4 -4
- package/commands/whois/[domain]/index.ts +4 -4
- package/lib/address-policy.d.ts +2 -0
- package/lib/address-policy.js +55 -54
- package/lib/address-policy.js.map +1 -1
- package/lib/cooldown.js +2 -1
- package/lib/cooldown.js.map +1 -1
- package/lib/dns.d.ts +2 -1
- package/lib/dns.js +14 -12
- package/lib/dns.js.map +1 -1
- package/lib/domain-price.d.ts +2 -1
- package/lib/domain-price.js +16 -19
- package/lib/domain-price.js.map +1 -1
- package/lib/errors.d.ts +5 -2
- package/lib/errors.js +8 -4
- package/lib/errors.js.map +1 -1
- package/lib/http.js +12 -11
- package/lib/http.js.map +1 -1
- package/lib/messages.d.ts +329 -0
- package/lib/messages.js +335 -0
- package/lib/messages.js.map +1 -0
- package/lib/ping.d.ts +2 -1
- package/lib/ping.js +13 -11
- package/lib/ping.js.map +1 -1
- package/lib/presentation.d.ts +1 -0
- package/lib/presentation.js +36 -0
- package/lib/presentation.js.map +1 -0
- package/lib/service.d.ts +6 -5
- package/lib/service.js +16 -15
- package/lib/service.js.map +1 -1
- package/lib/target-guard.js +6 -4
- package/lib/target-guard.js.map +1 -1
- package/lib/whois.d.ts +2 -1
- package/lib/whois.js +18 -16
- package/lib/whois.js.map +1 -1
- package/package.json +7 -7
- package/skills/nettools/tools/dns_lookup/index.js +1 -1
- package/skills/nettools/tools/domain_price/index.js +1 -1
- package/skills/nettools/tools/net_ping/index.js +1 -1
- package/skills/nettools/tools/whois_lookup/index.js +1 -1
package/lib/errors.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import { message as errorMessage, formatMessage } from './messages.js';
|
|
1
2
|
export class NettoolsError extends Error {
|
|
2
3
|
kind;
|
|
4
|
+
detail;
|
|
3
5
|
/** HTTP status when the failure came from a provider response. */
|
|
4
6
|
status;
|
|
5
7
|
/** Milliseconds the caller should wait before retrying (cooldown / 429). */
|
|
6
8
|
retryAfterMs;
|
|
7
9
|
constructor(kind, message, options = {}) {
|
|
8
|
-
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
10
|
+
super(typeof message === 'string' ? message : formatMessage(message), options.cause === undefined ? undefined : { cause: options.cause });
|
|
9
11
|
this.name = 'NettoolsError';
|
|
10
12
|
this.kind = kind;
|
|
13
|
+
if (typeof message !== 'string')
|
|
14
|
+
this.detail = message;
|
|
11
15
|
if (options.status !== undefined) {
|
|
12
16
|
this.status = options.status;
|
|
13
17
|
}
|
|
@@ -20,11 +24,11 @@ export function isNettoolsError(error) {
|
|
|
20
24
|
return error instanceof NettoolsError;
|
|
21
25
|
}
|
|
22
26
|
/** Render any thrown value as a short user-facing sentence. */
|
|
23
|
-
export function describeFailure(error) {
|
|
27
|
+
export function describeFailure(error, locale = 'zh-CN') {
|
|
24
28
|
if (!isNettoolsError(error)) {
|
|
25
29
|
const detail = error instanceof Error ? error.message : String(error);
|
|
26
|
-
return
|
|
30
|
+
return formatMessage(errorMessage('unexpected', { detail }), locale);
|
|
27
31
|
}
|
|
28
|
-
return error.message;
|
|
32
|
+
return error.detail ? formatMessage(error.detail, locale) : error.message;
|
|
29
33
|
}
|
|
30
34
|
//# sourceMappingURL=errors.js.map
|
package/lib/errors.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,aAAa,EAAwB,MAAM,eAAe,CAAC;AAoB7F,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,IAAI,CAAoB;IACxB,MAAM,CAAmB;IAClC,kEAAkE;IACzD,MAAM,CAAU;IACzB,4EAA4E;IACnE,YAAY,CAAU;IAE/B,YACE,IAAuB,EACvB,OAAiC,EACjC,UAAuE,EAAE;QAEzE,KAAK,CAAC,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QAC1I,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC;QACvD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC/B,CAAC;QACD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;YACvC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;QAC3C,CAAC;IACH,CAAC;CACF;AAED,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,KAAK,YAAY,aAAa,CAAC;AACxC,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,eAAe,CAAC,KAAc,EAAE,SAAiB,OAAO;IACtE,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtE,OAAO,aAAa,CAAC,YAAY,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC;AAC5E,CAAC","sourcesContent":["import { type Locale } from 'zhin.js/i18n';\nimport { message as errorMessage, formatMessage, type NettoolsMessage } from './messages.js';\n/**\n * Error categories surfaced by @zhin.js/plugin-nettools.\n *\n * Every failure that reaches a user is a {@link NettoolsError} so commands and\n * Agent tools can render one consistent, human-readable message instead of a\n * raw stack trace or provider payload.\n */\nexport type NettoolsErrorKind =\n | 'invalid_input'\n | 'blocked_target'\n | 'cooldown'\n | 'timeout'\n | 'rate_limited'\n | 'http'\n | 'network'\n | 'malformed'\n | 'not_found'\n | 'aborted';\n\nexport class NettoolsError extends Error {\n readonly kind: NettoolsErrorKind;\n readonly detail?: NettoolsMessage;\n /** HTTP status when the failure came from a provider response. */\n readonly status?: number;\n /** Milliseconds the caller should wait before retrying (cooldown / 429). */\n readonly retryAfterMs?: number;\n\n constructor(\n kind: NettoolsErrorKind,\n message: string | NettoolsMessage,\n options: { status?: number; retryAfterMs?: number; cause?: unknown } = {},\n ) {\n super(typeof message === 'string' ? message : formatMessage(message), options.cause === undefined ? undefined : { cause: options.cause });\n this.name = 'NettoolsError';\n this.kind = kind;\n if (typeof message !== 'string') this.detail = message;\n if (options.status !== undefined) {\n this.status = options.status;\n }\n if (options.retryAfterMs !== undefined) {\n this.retryAfterMs = options.retryAfterMs;\n }\n }\n}\n\nexport function isNettoolsError(error: unknown): error is NettoolsError {\n return error instanceof NettoolsError;\n}\n\n/** Render any thrown value as a short user-facing sentence. */\nexport function describeFailure(error: unknown, locale: Locale = 'zh-CN'): string {\n if (!isNettoolsError(error)) {\n const detail = error instanceof Error ? error.message : String(error);\n return formatMessage(errorMessage('unexpected', { detail }), locale);\n }\n return error.detail ? formatMessage(error.detail, locale) : error.message;\n}\n"]}
|
package/lib/http.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { message } from './messages.js';
|
|
1
2
|
/**
|
|
2
3
|
* Resilient JSON-over-HTTP for third-party, keyless public APIs.
|
|
3
4
|
*
|
|
@@ -69,7 +70,7 @@ export class HttpClient {
|
|
|
69
70
|
}
|
|
70
71
|
await this.#deps.sleep(delay, options.signal);
|
|
71
72
|
}
|
|
72
|
-
throw lastError ?? new NettoolsError('network',
|
|
73
|
+
throw lastError ?? new NettoolsError('network', message('requestFailed', { provider: options.provider }));
|
|
73
74
|
}
|
|
74
75
|
async #attempt(url, options) {
|
|
75
76
|
const timeoutSignal = AbortSignal.timeout(this.#policy.timeoutMs);
|
|
@@ -104,7 +105,7 @@ export class HttpClient {
|
|
|
104
105
|
}
|
|
105
106
|
const body = parseJsonSafely(text);
|
|
106
107
|
if (body === undefined) {
|
|
107
|
-
const error = new NettoolsError('malformed',
|
|
108
|
+
const error = new NettoolsError('malformed', message('responseMalformed', { provider: options.provider }));
|
|
108
109
|
return { kind: 'error', retryable: false, error };
|
|
109
110
|
}
|
|
110
111
|
const finalUrl = response.url || url.toString();
|
|
@@ -116,24 +117,24 @@ export class HttpClient {
|
|
|
116
117
|
}
|
|
117
118
|
if (timeoutSignal.aborted) {
|
|
118
119
|
const seconds = Math.round(this.#policy.timeoutMs / 1000);
|
|
119
|
-
const timeout = new NettoolsError('timeout',
|
|
120
|
+
const timeout = new NettoolsError('timeout', message('timeout', { provider: options.provider, seconds }), { cause: error });
|
|
120
121
|
return { kind: 'error', retryable: true, error: timeout };
|
|
121
122
|
}
|
|
122
123
|
const detail = error instanceof Error ? error.message : String(error);
|
|
123
|
-
const network = new NettoolsError('network',
|
|
124
|
+
const network = new NettoolsError('network', message('network', { provider: options.provider, detail }), { cause: error });
|
|
124
125
|
return { kind: 'error', retryable: true, error: network };
|
|
125
126
|
}
|
|
126
127
|
#classifyStatus(response, provider) {
|
|
127
128
|
const status = response.status;
|
|
128
129
|
const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'), this.#deps.now());
|
|
129
130
|
if (status === 429) {
|
|
130
|
-
const error = new NettoolsError('rate_limited',
|
|
131
|
+
const error = new NettoolsError('rate_limited', message('rateLimited', { provider }), {
|
|
131
132
|
status,
|
|
132
133
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
133
134
|
});
|
|
134
135
|
return { kind: 'error', retryable: true, error, retryAfterMs };
|
|
135
136
|
}
|
|
136
|
-
const error = new NettoolsError('http',
|
|
137
|
+
const error = new NettoolsError('http', message('http', { provider, status }), { status });
|
|
137
138
|
const retryable = RETRYABLE_STATUSES.has(status);
|
|
138
139
|
return { kind: 'error', retryable, error, retryAfterMs };
|
|
139
140
|
}
|
|
@@ -182,7 +183,7 @@ async function readLimitedText(response, maxBytes) {
|
|
|
182
183
|
const declared = Number(response.headers.get('content-length') ?? '');
|
|
183
184
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
184
185
|
await discardBody(response);
|
|
185
|
-
throw new NettoolsError('malformed', '
|
|
186
|
+
throw new NettoolsError('malformed', message('tooLarge'));
|
|
186
187
|
}
|
|
187
188
|
if (!response.body) {
|
|
188
189
|
return await response.text();
|
|
@@ -199,7 +200,7 @@ async function readLimitedText(response, maxBytes) {
|
|
|
199
200
|
received += chunk.value.byteLength;
|
|
200
201
|
if (received > maxBytes) {
|
|
201
202
|
await reader.cancel();
|
|
202
|
-
throw new NettoolsError('malformed', '
|
|
203
|
+
throw new NettoolsError('malformed', message('tooLarge'));
|
|
203
204
|
}
|
|
204
205
|
text += decoder.decode(chunk.value, { stream: true });
|
|
205
206
|
}
|
|
@@ -220,12 +221,12 @@ function throwIfAborted(signal, provider) {
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
function abortedError(provider) {
|
|
223
|
-
return new NettoolsError('aborted',
|
|
224
|
+
return new NettoolsError('aborted', message('providerAborted', { provider }));
|
|
224
225
|
}
|
|
225
226
|
export function abortableSleep(ms, signal) {
|
|
226
227
|
return new Promise((resolve, reject) => {
|
|
227
228
|
if (signal?.aborted) {
|
|
228
|
-
reject(new NettoolsError('aborted', '
|
|
229
|
+
reject(new NettoolsError('aborted', message('aborted')));
|
|
229
230
|
return;
|
|
230
231
|
}
|
|
231
232
|
const timer = setTimeout(() => {
|
|
@@ -234,7 +235,7 @@ export function abortableSleep(ms, signal) {
|
|
|
234
235
|
}, ms);
|
|
235
236
|
const onAbort = () => {
|
|
236
237
|
clearTimeout(timer);
|
|
237
|
-
reject(new NettoolsError('aborted', '
|
|
238
|
+
reject(new NettoolsError('aborted', message('aborted')));
|
|
238
239
|
};
|
|
239
240
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
240
241
|
});
|
package/lib/http.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Resilient JSON-over-HTTP for third-party, keyless public APIs.\n *\n * Every external call in this plugin goes through {@link fetchJson}:\n * - per-attempt timeout (AbortSignal), combined with the caller's signal;\n * - retry of transient failures only: network errors, timeouts, 408, 425, 429\n * and 5xx gateway-ish statuses (500, 502, 503, 504);\n * - exponential backoff with \"equal jitter\" (half fixed, half random) so many\n * bots do not retry in lock-step;\n * - `Retry-After` is honoured (seconds or HTTP-date) up to a cap; a longer\n * server-requested wait fails fast with a clear rate-limit message instead of\n * blocking a chat reply for minutes;\n * - response bodies are size-capped and parsed defensively; malformed JSON is\n * reported as `malformed`, never thrown as a raw SyntaxError.\n *\n * All clocks, sleeps, randomness and fetch are injectable for tests.\n */\nimport { NettoolsError } from './errors.js';\n\nexport type FetchLike = (input: URL, init: RequestInit) => Promise<Response>;\n\nexport interface HttpDependencies {\n readonly fetch: FetchLike;\n readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;\n readonly random: () => number;\n readonly now: () => number;\n}\n\nexport interface RetryPolicy {\n /** Per-attempt timeout in ms. */\n readonly timeoutMs: number;\n /** Extra attempts after the first one. */\n readonly maxRetries: number;\n /** First backoff step in ms; doubles on every retry. */\n readonly baseDelayMs: number;\n /** Upper bound for one computed backoff step. */\n readonly maxDelayMs: number;\n /** Longest `Retry-After` we are willing to wait inside one chat request. */\n readonly maxRetryAfterMs: number;\n}\n\nexport interface FetchJsonOptions {\n /** Human name of the provider, used in error messages. */\n readonly provider: string;\n readonly accept?: string;\n readonly signal?: AbortSignal;\n /** Statuses that are a valid answer rather than an error (e.g. RDAP 404). */\n readonly passStatuses?: readonly number[];\n readonly maxBodyBytes?: number;\n /** Override {@link RetryPolicy.maxRetries} for this call (e.g. when a fallback provider exists). */\n readonly maxRetries?: number;\n}\n\nexport interface JsonResponse {\n readonly status: number;\n readonly body: unknown;\n readonly url: string;\n}\n\nexport const DEFAULT_RETRY_POLICY: RetryPolicy = Object.freeze({\n timeoutMs: 8_000,\n maxRetries: 2,\n baseDelayMs: 500,\n maxDelayMs: 4_000,\n maxRetryAfterMs: 10_000,\n});\n\n/** 1 MiB is far above any legitimate answer from the APIs we call. */\nexport const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;\n\nconst RETRYABLE_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);\n\nexport function defaultHttpDependencies(): HttpDependencies {\n return {\n fetch: (input, init) => fetch(input, init),\n sleep: abortableSleep,\n random: Math.random,\n now: Date.now,\n };\n}\n\nexport class HttpClient {\n readonly #deps: HttpDependencies;\n readonly #policy: RetryPolicy;\n\n constructor(policy: RetryPolicy = DEFAULT_RETRY_POLICY, deps: HttpDependencies = defaultHttpDependencies()) {\n this.#policy = policy;\n this.#deps = deps;\n }\n\n get policy(): RetryPolicy {\n return this.#policy;\n }\n\n get dependencies(): HttpDependencies {\n return this.#deps;\n }\n\n /** GET a JSON document with timeout, retry, backoff and Retry-After handling. */\n async fetchJson(url: URL, options: FetchJsonOptions): Promise<JsonResponse> {\n const maxRetries = options.maxRetries ?? this.#policy.maxRetries;\n const attempts = Math.max(0, maxRetries) + 1;\n let lastError: NettoolsError | undefined;\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n throwIfAborted(options.signal, options.provider);\n const outcome = await this.#attempt(url, options);\n if (outcome.kind === 'ok') {\n return outcome.response;\n }\n lastError = outcome.error;\n const isLastAttempt = attempt === attempts - 1;\n if (!outcome.retryable || isLastAttempt) {\n break;\n }\n const delay = this.#computeDelay(attempt, outcome.retryAfterMs);\n if (delay === undefined) {\n break;\n }\n await this.#deps.sleep(delay, options.signal);\n }\n throw lastError ?? new NettoolsError('network', `${options.provider} 请求失败`);\n }\n\n async #attempt(url: URL, options: FetchJsonOptions): Promise<AttemptOutcome> {\n const timeoutSignal = AbortSignal.timeout(this.#policy.timeoutMs);\n const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;\n let response: Response;\n try {\n response = await this.#deps.fetch(url, {\n method: 'GET',\n headers: { accept: options.accept ?? 'application/json' },\n redirect: 'follow',\n signal,\n });\n } catch (error) {\n return this.#classifyTransportError(error, options, timeoutSignal);\n }\n\n const passStatuses = options.passStatuses ?? [];\n const statusIsAnswer = response.ok || passStatuses.includes(response.status);\n if (!statusIsAnswer) {\n await discardBody(response);\n return this.#classifyStatus(response, options.provider);\n }\n\n let text: string;\n try {\n text = await readLimitedText(response, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);\n } catch (error) {\n if (error instanceof NettoolsError) {\n return { kind: 'error', retryable: false, error };\n }\n return this.#classifyTransportError(error, options, timeoutSignal);\n }\n\n const body = parseJsonSafely(text);\n if (body === undefined) {\n const error = new NettoolsError('malformed', `${options.provider} 返回了无法解析的数据`);\n return { kind: 'error', retryable: false, error };\n }\n const finalUrl = response.url || url.toString();\n return { kind: 'ok', response: { status: response.status, body, url: finalUrl } };\n }\n\n #classifyTransportError(error: unknown, options: FetchJsonOptions, timeoutSignal: AbortSignal): AttemptOutcome {\n if (options.signal?.aborted) {\n return { kind: 'error', retryable: false, error: abortedError(options.provider) };\n }\n if (timeoutSignal.aborted) {\n const seconds = Math.round(this.#policy.timeoutMs / 1000);\n const timeout = new NettoolsError('timeout', `${options.provider} 响应超时(${seconds}s)`, { cause: error });\n return { kind: 'error', retryable: true, error: timeout };\n }\n const detail = error instanceof Error ? error.message : String(error);\n const network = new NettoolsError('network', `无法连接 ${options.provider}:${detail}`, { cause: error });\n return { kind: 'error', retryable: true, error: network };\n }\n\n #classifyStatus(response: Response, provider: string): AttemptOutcome {\n const status = response.status;\n const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'), this.#deps.now());\n if (status === 429) {\n const error = new NettoolsError('rate_limited', `${provider} 请求过于频繁,请稍后再试`, {\n status,\n ...(retryAfterMs === undefined ? {} : { retryAfterMs }),\n });\n return { kind: 'error', retryable: true, error, retryAfterMs };\n }\n const error = new NettoolsError('http', `${provider} 返回错误 HTTP ${status}`, { status });\n const retryable = RETRYABLE_STATUSES.has(status);\n return { kind: 'error', retryable, error, retryAfterMs };\n }\n\n /**\n * Backoff for retry number `attempt + 1`. Returns `undefined` when the\n * server asked us to wait longer than {@link RetryPolicy.maxRetryAfterMs}.\n */\n #computeDelay(attempt: number, retryAfterMs: number | undefined): number | undefined {\n if (retryAfterMs !== undefined) {\n if (retryAfterMs > this.#policy.maxRetryAfterMs) {\n return undefined;\n }\n return retryAfterMs;\n }\n const exponential = this.#policy.baseDelayMs * 2 ** attempt;\n const capped = Math.min(this.#policy.maxDelayMs, exponential);\n const half = capped / 2;\n return Math.round(half + this.#deps.random() * half);\n }\n}\n\ntype AttemptOutcome =\n | { readonly kind: 'ok'; readonly response: JsonResponse }\n | {\n readonly kind: 'error';\n readonly retryable: boolean;\n readonly error: NettoolsError;\n readonly retryAfterMs?: number | undefined;\n };\n\n/** Parse `Retry-After` as delta-seconds or an HTTP-date. Invalid values are ignored. */\nexport function parseRetryAfter(value: string | null, nowMs: number): number | undefined {\n if (value === null) {\n return undefined;\n }\n const trimmed = value.trim();\n if (/^\\d+$/u.test(trimmed)) {\n return Number(trimmed) * 1000;\n }\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) {\n return undefined;\n }\n return Math.max(0, dateMs - nowMs);\n}\n\nfunction parseJsonSafely(text: string): unknown {\n try {\n const parsed: unknown = JSON.parse(text);\n return parsed === undefined ? undefined : parsed;\n } catch {\n return undefined;\n }\n}\n\nasync function readLimitedText(response: Response, maxBytes: number): Promise<string> {\n const declared = Number(response.headers.get('content-length') ?? '');\n if (Number.isFinite(declared) && declared > maxBytes) {\n await discardBody(response);\n throw new NettoolsError('malformed', '响应体过大,已拒绝读取');\n }\n if (!response.body) {\n return await response.text();\n }\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let received = 0;\n let text = '';\n for (;;) {\n const chunk = await reader.read();\n if (chunk.done) {\n break;\n }\n received += chunk.value.byteLength;\n if (received > maxBytes) {\n await reader.cancel();\n throw new NettoolsError('malformed', '响应体过大,已拒绝读取');\n }\n text += decoder.decode(chunk.value, { stream: true });\n }\n text += decoder.decode();\n return text;\n}\n\nasync function discardBody(response: Response): Promise<void> {\n try {\n await response.body?.cancel();\n } catch {\n // Ignore: the body is irrelevant once we decided the status is an error.\n }\n}\n\nfunction throwIfAborted(signal: AbortSignal | undefined, provider: string): void {\n if (signal?.aborted) {\n throw abortedError(provider);\n }\n}\n\nfunction abortedError(provider: string): NettoolsError {\n return new NettoolsError('aborted', `${provider} 请求已取消`);\n}\n\nexport function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(new NettoolsError('aborted', '请求已取消'));\n return;\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n const onAbort = (): void => {\n clearTimeout(timer);\n reject(new NettoolsError('aborted', '请求已取消'));\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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","sourcesContent":["import { message } from './messages.js';\n/**\n * Resilient JSON-over-HTTP for third-party, keyless public APIs.\n *\n * Every external call in this plugin goes through {@link fetchJson}:\n * - per-attempt timeout (AbortSignal), combined with the caller's signal;\n * - retry of transient failures only: network errors, timeouts, 408, 425, 429\n * and 5xx gateway-ish statuses (500, 502, 503, 504);\n * - exponential backoff with \"equal jitter\" (half fixed, half random) so many\n * bots do not retry in lock-step;\n * - `Retry-After` is honoured (seconds or HTTP-date) up to a cap; a longer\n * server-requested wait fails fast with a clear rate-limit message instead of\n * blocking a chat reply for minutes;\n * - response bodies are size-capped and parsed defensively; malformed JSON is\n * reported as `malformed`, never thrown as a raw SyntaxError.\n *\n * All clocks, sleeps, randomness and fetch are injectable for tests.\n */\nimport { NettoolsError } from './errors.js';\n\nexport type FetchLike = (input: URL, init: RequestInit) => Promise<Response>;\n\nexport interface HttpDependencies {\n readonly fetch: FetchLike;\n readonly sleep: (ms: number, signal?: AbortSignal) => Promise<void>;\n readonly random: () => number;\n readonly now: () => number;\n}\n\nexport interface RetryPolicy {\n /** Per-attempt timeout in ms. */\n readonly timeoutMs: number;\n /** Extra attempts after the first one. */\n readonly maxRetries: number;\n /** First backoff step in ms; doubles on every retry. */\n readonly baseDelayMs: number;\n /** Upper bound for one computed backoff step. */\n readonly maxDelayMs: number;\n /** Longest `Retry-After` we are willing to wait inside one chat request. */\n readonly maxRetryAfterMs: number;\n}\n\nexport interface FetchJsonOptions {\n /** Human name of the provider, used in error messages. */\n readonly provider: string;\n readonly accept?: string;\n readonly signal?: AbortSignal;\n /** Statuses that are a valid answer rather than an error (e.g. RDAP 404). */\n readonly passStatuses?: readonly number[];\n readonly maxBodyBytes?: number;\n /** Override {@link RetryPolicy.maxRetries} for this call (e.g. when a fallback provider exists). */\n readonly maxRetries?: number;\n}\n\nexport interface JsonResponse {\n readonly status: number;\n readonly body: unknown;\n readonly url: string;\n}\n\nexport const DEFAULT_RETRY_POLICY: RetryPolicy = Object.freeze({\n timeoutMs: 8_000,\n maxRetries: 2,\n baseDelayMs: 500,\n maxDelayMs: 4_000,\n maxRetryAfterMs: 10_000,\n});\n\n/** 1 MiB is far above any legitimate answer from the APIs we call. */\nexport const DEFAULT_MAX_BODY_BYTES = 1024 * 1024;\n\nconst RETRYABLE_STATUSES = new Set([408, 425, 429, 500, 502, 503, 504]);\n\nexport function defaultHttpDependencies(): HttpDependencies {\n return {\n fetch: (input, init) => fetch(input, init),\n sleep: abortableSleep,\n random: Math.random,\n now: Date.now,\n };\n}\n\nexport class HttpClient {\n readonly #deps: HttpDependencies;\n readonly #policy: RetryPolicy;\n\n constructor(policy: RetryPolicy = DEFAULT_RETRY_POLICY, deps: HttpDependencies = defaultHttpDependencies()) {\n this.#policy = policy;\n this.#deps = deps;\n }\n\n get policy(): RetryPolicy {\n return this.#policy;\n }\n\n get dependencies(): HttpDependencies {\n return this.#deps;\n }\n\n /** GET a JSON document with timeout, retry, backoff and Retry-After handling. */\n async fetchJson(url: URL, options: FetchJsonOptions): Promise<JsonResponse> {\n const maxRetries = options.maxRetries ?? this.#policy.maxRetries;\n const attempts = Math.max(0, maxRetries) + 1;\n let lastError: NettoolsError | undefined;\n for (let attempt = 0; attempt < attempts; attempt += 1) {\n throwIfAborted(options.signal, options.provider);\n const outcome = await this.#attempt(url, options);\n if (outcome.kind === 'ok') {\n return outcome.response;\n }\n lastError = outcome.error;\n const isLastAttempt = attempt === attempts - 1;\n if (!outcome.retryable || isLastAttempt) {\n break;\n }\n const delay = this.#computeDelay(attempt, outcome.retryAfterMs);\n if (delay === undefined) {\n break;\n }\n await this.#deps.sleep(delay, options.signal);\n }\n throw lastError ?? new NettoolsError('network', message('requestFailed', { provider: options.provider }));\n }\n\n async #attempt(url: URL, options: FetchJsonOptions): Promise<AttemptOutcome> {\n const timeoutSignal = AbortSignal.timeout(this.#policy.timeoutMs);\n const signal = options.signal ? AbortSignal.any([options.signal, timeoutSignal]) : timeoutSignal;\n let response: Response;\n try {\n response = await this.#deps.fetch(url, {\n method: 'GET',\n headers: { accept: options.accept ?? 'application/json' },\n redirect: 'follow',\n signal,\n });\n } catch (error) {\n return this.#classifyTransportError(error, options, timeoutSignal);\n }\n\n const passStatuses = options.passStatuses ?? [];\n const statusIsAnswer = response.ok || passStatuses.includes(response.status);\n if (!statusIsAnswer) {\n await discardBody(response);\n return this.#classifyStatus(response, options.provider);\n }\n\n let text: string;\n try {\n text = await readLimitedText(response, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);\n } catch (error) {\n if (error instanceof NettoolsError) {\n return { kind: 'error', retryable: false, error };\n }\n return this.#classifyTransportError(error, options, timeoutSignal);\n }\n\n const body = parseJsonSafely(text);\n if (body === undefined) {\n const error = new NettoolsError('malformed', message('responseMalformed', { provider: options.provider }));\n return { kind: 'error', retryable: false, error };\n }\n const finalUrl = response.url || url.toString();\n return { kind: 'ok', response: { status: response.status, body, url: finalUrl } };\n }\n\n #classifyTransportError(error: unknown, options: FetchJsonOptions, timeoutSignal: AbortSignal): AttemptOutcome {\n if (options.signal?.aborted) {\n return { kind: 'error', retryable: false, error: abortedError(options.provider) };\n }\n if (timeoutSignal.aborted) {\n const seconds = Math.round(this.#policy.timeoutMs / 1000);\n const timeout = new NettoolsError('timeout', message('timeout', { provider: options.provider, seconds }), { cause: error });\n return { kind: 'error', retryable: true, error: timeout };\n }\n const detail = error instanceof Error ? error.message : String(error);\n const network = new NettoolsError('network', message('network', { provider: options.provider, detail }), { cause: error });\n return { kind: 'error', retryable: true, error: network };\n }\n\n #classifyStatus(response: Response, provider: string): AttemptOutcome {\n const status = response.status;\n const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'), this.#deps.now());\n if (status === 429) {\n const error = new NettoolsError('rate_limited', message('rateLimited', { provider }), {\n status,\n ...(retryAfterMs === undefined ? {} : { retryAfterMs }),\n });\n return { kind: 'error', retryable: true, error, retryAfterMs };\n }\n const error = new NettoolsError('http', message('http', { provider, status }), { status });\n const retryable = RETRYABLE_STATUSES.has(status);\n return { kind: 'error', retryable, error, retryAfterMs };\n }\n\n /**\n * Backoff for retry number `attempt + 1`. Returns `undefined` when the\n * server asked us to wait longer than {@link RetryPolicy.maxRetryAfterMs}.\n */\n #computeDelay(attempt: number, retryAfterMs: number | undefined): number | undefined {\n if (retryAfterMs !== undefined) {\n if (retryAfterMs > this.#policy.maxRetryAfterMs) {\n return undefined;\n }\n return retryAfterMs;\n }\n const exponential = this.#policy.baseDelayMs * 2 ** attempt;\n const capped = Math.min(this.#policy.maxDelayMs, exponential);\n const half = capped / 2;\n return Math.round(half + this.#deps.random() * half);\n }\n}\n\ntype AttemptOutcome =\n | { readonly kind: 'ok'; readonly response: JsonResponse }\n | {\n readonly kind: 'error';\n readonly retryable: boolean;\n readonly error: NettoolsError;\n readonly retryAfterMs?: number | undefined;\n };\n\n/** Parse `Retry-After` as delta-seconds or an HTTP-date. Invalid values are ignored. */\nexport function parseRetryAfter(value: string | null, nowMs: number): number | undefined {\n if (value === null) {\n return undefined;\n }\n const trimmed = value.trim();\n if (/^\\d+$/u.test(trimmed)) {\n return Number(trimmed) * 1000;\n }\n const dateMs = Date.parse(trimmed);\n if (Number.isNaN(dateMs)) {\n return undefined;\n }\n return Math.max(0, dateMs - nowMs);\n}\n\nfunction parseJsonSafely(text: string): unknown {\n try {\n const parsed: unknown = JSON.parse(text);\n return parsed === undefined ? undefined : parsed;\n } catch {\n return undefined;\n }\n}\n\nasync function readLimitedText(response: Response, maxBytes: number): Promise<string> {\n const declared = Number(response.headers.get('content-length') ?? '');\n if (Number.isFinite(declared) && declared > maxBytes) {\n await discardBody(response);\n throw new NettoolsError('malformed', message('tooLarge'));\n }\n if (!response.body) {\n return await response.text();\n }\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let received = 0;\n let text = '';\n for (;;) {\n const chunk = await reader.read();\n if (chunk.done) {\n break;\n }\n received += chunk.value.byteLength;\n if (received > maxBytes) {\n await reader.cancel();\n throw new NettoolsError('malformed', message('tooLarge'));\n }\n text += decoder.decode(chunk.value, { stream: true });\n }\n text += decoder.decode();\n return text;\n}\n\nasync function discardBody(response: Response): Promise<void> {\n try {\n await response.body?.cancel();\n } catch {\n // Ignore: the body is irrelevant once we decided the status is an error.\n }\n}\n\nfunction throwIfAborted(signal: AbortSignal | undefined, provider: string): void {\n if (signal?.aborted) {\n throw abortedError(provider);\n }\n}\n\nfunction abortedError(provider: string): NettoolsError {\n return new NettoolsError('aborted', message('providerAborted', { provider }));\n}\n\nexport function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(new NettoolsError('aborted', message('aborted')));\n return;\n }\n const timer = setTimeout(() => {\n signal?.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n const onAbort = (): void => {\n clearTimeout(timer);\n reject(new NettoolsError('aborted', message('aborted')));\n };\n signal?.addEventListener('abort', onAbort, { once: true });\n });\n}\n"]}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { type Locale } from 'zhin.js/i18n';
|
|
2
|
+
declare const catalog: {
|
|
3
|
+
readonly cloudMetadata: {
|
|
4
|
+
readonly "zh-CN": "云服务元数据地址";
|
|
5
|
+
readonly en: "Cloud metadata address";
|
|
6
|
+
};
|
|
7
|
+
readonly unspecifiedV4: {
|
|
8
|
+
readonly "zh-CN": "未指定 / “本网络”地址";
|
|
9
|
+
readonly en: "Unspecified / current-network address";
|
|
10
|
+
};
|
|
11
|
+
readonly privateV4: {
|
|
12
|
+
readonly "zh-CN": "私有地址 (RFC 1918)";
|
|
13
|
+
readonly en: "Private address (RFC 1918)";
|
|
14
|
+
};
|
|
15
|
+
readonly cgnat: {
|
|
16
|
+
readonly "zh-CN": "运营商级 NAT 地址 (CGNAT, RFC 6598)";
|
|
17
|
+
readonly en: "Carrier-grade NAT address (CGNAT, RFC 6598)";
|
|
18
|
+
};
|
|
19
|
+
readonly loopback: {
|
|
20
|
+
readonly "zh-CN": "回环地址";
|
|
21
|
+
readonly en: "Loopback address";
|
|
22
|
+
};
|
|
23
|
+
readonly linkLocal: {
|
|
24
|
+
readonly "zh-CN": "链路本地地址";
|
|
25
|
+
readonly en: "Link-local address";
|
|
26
|
+
};
|
|
27
|
+
readonly ietfReserved: {
|
|
28
|
+
readonly "zh-CN": "IETF 协议保留地址";
|
|
29
|
+
readonly en: "IETF protocol reserved address";
|
|
30
|
+
};
|
|
31
|
+
readonly documentation: {
|
|
32
|
+
readonly "zh-CN": "文档示例地址";
|
|
33
|
+
readonly en: "Documentation address";
|
|
34
|
+
};
|
|
35
|
+
readonly relay6to4: {
|
|
36
|
+
readonly "zh-CN": "已废弃的 6to4 中继地址";
|
|
37
|
+
readonly en: "Deprecated 6to4 relay address";
|
|
38
|
+
};
|
|
39
|
+
readonly benchmark: {
|
|
40
|
+
readonly "zh-CN": "基准测试保留地址";
|
|
41
|
+
readonly en: "Benchmarking address";
|
|
42
|
+
};
|
|
43
|
+
readonly multicast: {
|
|
44
|
+
readonly "zh-CN": "组播地址";
|
|
45
|
+
readonly en: "Multicast address";
|
|
46
|
+
};
|
|
47
|
+
readonly reservedBroadcast: {
|
|
48
|
+
readonly "zh-CN": "保留地址(含广播地址)";
|
|
49
|
+
readonly en: "Reserved address (including broadcast)";
|
|
50
|
+
};
|
|
51
|
+
readonly teredo: {
|
|
52
|
+
readonly "zh-CN": "Teredo 隧道地址";
|
|
53
|
+
readonly en: "Teredo tunnel address";
|
|
54
|
+
};
|
|
55
|
+
readonly orchid: {
|
|
56
|
+
readonly "zh-CN": "ORCHID 保留地址";
|
|
57
|
+
readonly en: "ORCHID reserved address";
|
|
58
|
+
};
|
|
59
|
+
readonly hostRequired: {
|
|
60
|
+
readonly "zh-CN": "请提供主机名或 IP 地址";
|
|
61
|
+
readonly en: "Provide a hostname or IP address";
|
|
62
|
+
};
|
|
63
|
+
readonly hostTooLong: {
|
|
64
|
+
readonly "zh-CN": "主机名过长";
|
|
65
|
+
readonly en: "Hostname is too long";
|
|
66
|
+
};
|
|
67
|
+
readonly bareHost: {
|
|
68
|
+
readonly "zh-CN": "只接受裸主机名或 IP,不要带协议、端口、路径或特殊字符";
|
|
69
|
+
readonly en: "Use a bare hostname or IP, without a scheme, port, path or special characters";
|
|
70
|
+
};
|
|
71
|
+
readonly ipv6NoPort: {
|
|
72
|
+
readonly "zh-CN": "不是有效的 IPv6 地址(如需端口请去掉端口)";
|
|
73
|
+
readonly en: "Invalid IPv6 address; omit any port";
|
|
74
|
+
};
|
|
75
|
+
readonly invalidHost: {
|
|
76
|
+
readonly "zh-CN": "不是有效的主机名或 IP 地址";
|
|
77
|
+
readonly en: "Invalid hostname or IP address";
|
|
78
|
+
};
|
|
79
|
+
readonly hostLength: {
|
|
80
|
+
readonly "zh-CN": "主机名长度不合法";
|
|
81
|
+
readonly en: "Invalid hostname length";
|
|
82
|
+
};
|
|
83
|
+
readonly fullDomain: {
|
|
84
|
+
readonly "zh-CN": "请提供完整域名(例如 example.com)";
|
|
85
|
+
readonly en: "Provide a full domain name, such as example.com";
|
|
86
|
+
};
|
|
87
|
+
readonly invalidLabel: {
|
|
88
|
+
readonly "zh-CN": "域名标签不合法:{label}";
|
|
89
|
+
readonly en: "Invalid domain label: {label}";
|
|
90
|
+
};
|
|
91
|
+
readonly empty: {
|
|
92
|
+
readonly "zh-CN": "(空)";
|
|
93
|
+
readonly en: "(empty)";
|
|
94
|
+
};
|
|
95
|
+
readonly numericTld: {
|
|
96
|
+
readonly "zh-CN": "顶级域不能是纯数字";
|
|
97
|
+
readonly en: "The top-level domain cannot be entirely numeric";
|
|
98
|
+
};
|
|
99
|
+
readonly notIp: {
|
|
100
|
+
readonly "zh-CN": "不是 IP 地址:{address}";
|
|
101
|
+
readonly en: "Not an IP address: {address}";
|
|
102
|
+
};
|
|
103
|
+
readonly localhost: {
|
|
104
|
+
readonly "zh-CN": "本机主机名";
|
|
105
|
+
readonly en: "Local hostname";
|
|
106
|
+
};
|
|
107
|
+
readonly reservedSuffix: {
|
|
108
|
+
readonly "zh-CN": "内网或保留域名后缀 .{suffix}";
|
|
109
|
+
readonly en: "Private or reserved domain suffix .{suffix}";
|
|
110
|
+
};
|
|
111
|
+
readonly unspecifiedV6: {
|
|
112
|
+
readonly "zh-CN": "未指定地址 (::)";
|
|
113
|
+
readonly en: "Unspecified address (::)";
|
|
114
|
+
};
|
|
115
|
+
readonly loopbackV6: {
|
|
116
|
+
readonly "zh-CN": "回环地址 (::1)";
|
|
117
|
+
readonly en: "Loopback address (::1)";
|
|
118
|
+
};
|
|
119
|
+
readonly mappedV4: {
|
|
120
|
+
readonly "zh-CN": "IPv4 映射地址";
|
|
121
|
+
readonly en: "IPv4-mapped address";
|
|
122
|
+
};
|
|
123
|
+
readonly nat64: {
|
|
124
|
+
readonly "zh-CN": "NAT64 地址";
|
|
125
|
+
readonly en: "NAT64 address";
|
|
126
|
+
};
|
|
127
|
+
readonly localNat64: {
|
|
128
|
+
readonly "zh-CN": "本地 NAT64 地址";
|
|
129
|
+
readonly en: "Local NAT64 address";
|
|
130
|
+
};
|
|
131
|
+
readonly sixToFour: {
|
|
132
|
+
readonly "zh-CN": "6to4 地址";
|
|
133
|
+
readonly en: "6to4 address";
|
|
134
|
+
};
|
|
135
|
+
readonly ula: {
|
|
136
|
+
readonly "zh-CN": "唯一本地地址 (ULA, fc00::/7)";
|
|
137
|
+
readonly en: "Unique-local address (ULA, fc00::/7)";
|
|
138
|
+
};
|
|
139
|
+
readonly linkLocalV6: {
|
|
140
|
+
readonly "zh-CN": "链路本地地址 (fe80::/10)";
|
|
141
|
+
readonly en: "Link-local address (fe80::/10)";
|
|
142
|
+
};
|
|
143
|
+
readonly siteLocalV6: {
|
|
144
|
+
readonly "zh-CN": "已废弃的站点本地地址 (fec0::/10)";
|
|
145
|
+
readonly en: "Deprecated site-local address (fec0::/10)";
|
|
146
|
+
};
|
|
147
|
+
readonly multicastV6: {
|
|
148
|
+
readonly "zh-CN": "组播地址 (ff00::/8)";
|
|
149
|
+
readonly en: "Multicast address (ff00::/8)";
|
|
150
|
+
};
|
|
151
|
+
readonly notGlobalV6: {
|
|
152
|
+
readonly "zh-CN": "非全球单播 IPv6 地址";
|
|
153
|
+
readonly en: "Non-global-unicast IPv6 address";
|
|
154
|
+
};
|
|
155
|
+
readonly embeddedV4: {
|
|
156
|
+
readonly "zh-CN": "{wrapper}指向 {address}({reason})";
|
|
157
|
+
readonly en: "{wrapper} points to {address} ({reason})";
|
|
158
|
+
};
|
|
159
|
+
readonly invalidV4: {
|
|
160
|
+
readonly "zh-CN": "不是有效的 IPv4 地址:{address}";
|
|
161
|
+
readonly en: "Invalid IPv4 address: {address}";
|
|
162
|
+
};
|
|
163
|
+
readonly invalidV6: {
|
|
164
|
+
readonly "zh-CN": "不是有效的 IPv6 地址:{address}";
|
|
165
|
+
readonly en: "Invalid IPv6 address: {address}";
|
|
166
|
+
};
|
|
167
|
+
readonly invalidV6Group: {
|
|
168
|
+
readonly "zh-CN": "IPv6 分组不合法:{group}";
|
|
169
|
+
readonly en: "Invalid IPv6 group: {group}";
|
|
170
|
+
};
|
|
171
|
+
readonly reverseNeedsIp: {
|
|
172
|
+
readonly "zh-CN": "反向解析需要 IP 地址";
|
|
173
|
+
readonly en: "Reverse lookup requires an IP address";
|
|
174
|
+
};
|
|
175
|
+
readonly probeBlocked: {
|
|
176
|
+
readonly "zh-CN": "拒绝探测 {host}:{reason}";
|
|
177
|
+
readonly en: "Refused to probe {host}: {reason}";
|
|
178
|
+
};
|
|
179
|
+
readonly queryBlocked: {
|
|
180
|
+
readonly "zh-CN": "拒绝查询 {host}:{reason}";
|
|
181
|
+
readonly en: "Refused to query {host}: {reason}";
|
|
182
|
+
};
|
|
183
|
+
readonly whoisDomainOnly: {
|
|
184
|
+
readonly "zh-CN": "whois 目前只支持域名查询";
|
|
185
|
+
readonly en: "WHOIS supports domain names only";
|
|
186
|
+
};
|
|
187
|
+
readonly reverseOnly: {
|
|
188
|
+
readonly "zh-CN": "IP 地址只能做反向解析,请使用 PTR 记录类型";
|
|
189
|
+
readonly en: "IP addresses only support reverse lookup; select the PTR record type";
|
|
190
|
+
};
|
|
191
|
+
readonly noAddresses: {
|
|
192
|
+
readonly "zh-CN": "无法解析 {host}(没有 A/AAAA 记录)";
|
|
193
|
+
readonly en: "Cannot resolve {host} (no A/AAAA records)";
|
|
194
|
+
};
|
|
195
|
+
readonly resolvedBlocked: {
|
|
196
|
+
readonly "zh-CN": "拒绝探测 {host}:解析到 {address}({reason})";
|
|
197
|
+
readonly en: "Refused to probe {host}: resolves to {address} ({reason})";
|
|
198
|
+
};
|
|
199
|
+
readonly unknownAddress: {
|
|
200
|
+
readonly "zh-CN": "无法识别的地址记录";
|
|
201
|
+
readonly en: "Unrecognized address record";
|
|
202
|
+
};
|
|
203
|
+
readonly cooldown: {
|
|
204
|
+
readonly "zh-CN": "操作太频繁,请 {seconds} 秒后再试";
|
|
205
|
+
readonly en: "Too many requests. Try again in {seconds} seconds";
|
|
206
|
+
};
|
|
207
|
+
readonly dnsType: {
|
|
208
|
+
readonly "zh-CN": "不支持的记录类型 {type},可选:{types}";
|
|
209
|
+
readonly en: "Unsupported record type {type}; choose {types}";
|
|
210
|
+
};
|
|
211
|
+
readonly queryFailed: {
|
|
212
|
+
readonly "zh-CN": "{provider} 查询失败";
|
|
213
|
+
readonly en: "{provider} lookup failed";
|
|
214
|
+
};
|
|
215
|
+
readonly dnsUnavailable: {
|
|
216
|
+
readonly "zh-CN": "所有 DNS 服务均不可用:{failures}";
|
|
217
|
+
readonly en: "All DNS services are unavailable: {failures}";
|
|
218
|
+
};
|
|
219
|
+
readonly dnsMalformed: {
|
|
220
|
+
readonly "zh-CN": "{provider} 返回了无法识别的 DNS 响应";
|
|
221
|
+
readonly en: "{provider} returned an unrecognized DNS response";
|
|
222
|
+
};
|
|
223
|
+
readonly dnsFailed: {
|
|
224
|
+
readonly "zh-CN": "{provider} 解析失败({rcode})";
|
|
225
|
+
readonly en: "{provider} lookup failed ({rcode})";
|
|
226
|
+
};
|
|
227
|
+
readonly requestFailed: {
|
|
228
|
+
readonly "zh-CN": "{provider} 请求失败";
|
|
229
|
+
readonly en: "{provider} request failed";
|
|
230
|
+
};
|
|
231
|
+
readonly responseMalformed: {
|
|
232
|
+
readonly "zh-CN": "{provider} 返回了无法解析的数据";
|
|
233
|
+
readonly en: "{provider} returned data that could not be parsed";
|
|
234
|
+
};
|
|
235
|
+
readonly timeout: {
|
|
236
|
+
readonly "zh-CN": "{provider} 响应超时({seconds}s)";
|
|
237
|
+
readonly en: "{provider} timed out ({seconds}s)";
|
|
238
|
+
};
|
|
239
|
+
readonly network: {
|
|
240
|
+
readonly "zh-CN": "无法连接 {provider}:{detail}";
|
|
241
|
+
readonly en: "Cannot connect to {provider}: {detail}";
|
|
242
|
+
};
|
|
243
|
+
readonly rateLimited: {
|
|
244
|
+
readonly "zh-CN": "{provider} 请求过于频繁,请稍后再试";
|
|
245
|
+
readonly en: "{provider} rate limit reached; try again later";
|
|
246
|
+
};
|
|
247
|
+
readonly http: {
|
|
248
|
+
readonly "zh-CN": "{provider} 返回错误 HTTP {status}";
|
|
249
|
+
readonly en: "{provider} returned HTTP {status}";
|
|
250
|
+
};
|
|
251
|
+
readonly tooLarge: {
|
|
252
|
+
readonly "zh-CN": "响应体过大,已拒绝读取";
|
|
253
|
+
readonly en: "Response body is too large; reading was refused";
|
|
254
|
+
};
|
|
255
|
+
readonly providerAborted: {
|
|
256
|
+
readonly "zh-CN": "{provider} 请求已取消";
|
|
257
|
+
readonly en: "{provider} request was cancelled";
|
|
258
|
+
};
|
|
259
|
+
readonly aborted: {
|
|
260
|
+
readonly "zh-CN": "请求已取消";
|
|
261
|
+
readonly en: "Request was cancelled";
|
|
262
|
+
};
|
|
263
|
+
readonly pingRefused: {
|
|
264
|
+
readonly "zh-CN": "check-host.net 拒绝了请求:{detail}";
|
|
265
|
+
readonly en: "check-host.net refused the request: {detail}";
|
|
266
|
+
};
|
|
267
|
+
readonly pingMalformed: {
|
|
268
|
+
readonly "zh-CN": "check-host.net 返回了无法识别的数据";
|
|
269
|
+
readonly en: "check-host.net returned unrecognized data";
|
|
270
|
+
};
|
|
271
|
+
readonly priceKind: {
|
|
272
|
+
readonly "zh-CN": "价格类型可选:注册(register) / 续费(renew) / 转入(transfer)";
|
|
273
|
+
readonly en: "Price type must be register, renew, or transfer";
|
|
274
|
+
};
|
|
275
|
+
readonly suffixRequired: {
|
|
276
|
+
readonly "zh-CN": "请提供域名后缀,例如 com 或 .cn";
|
|
277
|
+
readonly en: "Provide a domain suffix, such as com or .cn";
|
|
278
|
+
};
|
|
279
|
+
readonly suffixNotIp: {
|
|
280
|
+
readonly "zh-CN": "请提供域名或后缀,而不是 IP 地址";
|
|
281
|
+
readonly en: "Provide a domain name or suffix, rather than an IP address";
|
|
282
|
+
};
|
|
283
|
+
readonly priceMalformed: {
|
|
284
|
+
readonly "zh-CN": "nazhumi.com 返回了无法识别的数据";
|
|
285
|
+
readonly en: "nazhumi.com returned unrecognized data";
|
|
286
|
+
};
|
|
287
|
+
readonly priceError: {
|
|
288
|
+
readonly "zh-CN": "nazhumi.com 返回错误 {code}{detail}";
|
|
289
|
+
readonly en: "nazhumi.com returned error {code}{detail}";
|
|
290
|
+
};
|
|
291
|
+
readonly rdapFailed: {
|
|
292
|
+
readonly "zh-CN": "RDAP / WHOIS 查询失败";
|
|
293
|
+
readonly en: "RDAP / WHOIS lookup failed";
|
|
294
|
+
};
|
|
295
|
+
readonly ianaMalformed: {
|
|
296
|
+
readonly "zh-CN": "IANA RDAP 引导数据格式异常";
|
|
297
|
+
readonly en: "Invalid IANA RDAP bootstrap data";
|
|
298
|
+
};
|
|
299
|
+
readonly rdapMalformed: {
|
|
300
|
+
readonly "zh-CN": "RDAP 返回了无法识别的数据";
|
|
301
|
+
readonly en: "RDAP returned unrecognized data";
|
|
302
|
+
};
|
|
303
|
+
readonly rdapNotDomain: {
|
|
304
|
+
readonly "zh-CN": "RDAP 返回的不是域名对象";
|
|
305
|
+
readonly en: "RDAP did not return a domain object";
|
|
306
|
+
};
|
|
307
|
+
readonly whoisMalformed: {
|
|
308
|
+
readonly "zh-CN": "WHOIS 服务返回了无法识别的数据";
|
|
309
|
+
readonly en: "WHOIS service returned unrecognized data";
|
|
310
|
+
};
|
|
311
|
+
readonly whoisError: {
|
|
312
|
+
readonly "zh-CN": "WHOIS 服务返回错误 {code}";
|
|
313
|
+
readonly en: "WHOIS service returned error {code}";
|
|
314
|
+
};
|
|
315
|
+
readonly unexpected: {
|
|
316
|
+
readonly "zh-CN": "出错了:{detail}";
|
|
317
|
+
readonly en: "An error occurred: {detail}";
|
|
318
|
+
};
|
|
319
|
+
};
|
|
320
|
+
export type NettoolsMessageKey = keyof typeof catalog;
|
|
321
|
+
export type NettoolsMessageValue = string | number | NettoolsMessage | readonly (string | NettoolsMessage)[];
|
|
322
|
+
export interface NettoolsMessage {
|
|
323
|
+
readonly key: NettoolsMessageKey;
|
|
324
|
+
readonly values: Readonly<Record<string, NettoolsMessageValue>>;
|
|
325
|
+
}
|
|
326
|
+
/** Stable source keys preserve nested provider/target reasons until the caller chooses a language. */
|
|
327
|
+
export declare function message(key: NettoolsMessageKey, values?: Readonly<Record<string, NettoolsMessageValue>>): NettoolsMessage;
|
|
328
|
+
export declare function formatMessage(detail: NettoolsMessage, locale?: Locale): string;
|
|
329
|
+
export {};
|