@zhin.js/plugin-nettools 0.1.2 → 0.1.3

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/commands/dns/[name]/[[type]]/index.js +5 -5
  3. package/commands/dns/[name]/[[type]]/index.ts +5 -5
  4. package/commands/domain-price/[suffix]/[[order]]/index.js +5 -5
  5. package/commands/domain-price/[suffix]/[[order]]/index.ts +5 -5
  6. package/commands/ping/[host]/index.js +4 -4
  7. package/commands/ping/[host]/index.ts +4 -4
  8. package/commands/whois/[domain]/index.js +4 -4
  9. package/commands/whois/[domain]/index.ts +4 -4
  10. package/lib/address-policy.d.ts +2 -0
  11. package/lib/address-policy.js +55 -54
  12. package/lib/address-policy.js.map +1 -1
  13. package/lib/cooldown.js +2 -1
  14. package/lib/cooldown.js.map +1 -1
  15. package/lib/dns.d.ts +2 -1
  16. package/lib/dns.js +14 -12
  17. package/lib/dns.js.map +1 -1
  18. package/lib/domain-price.d.ts +2 -1
  19. package/lib/domain-price.js +16 -19
  20. package/lib/domain-price.js.map +1 -1
  21. package/lib/errors.d.ts +5 -2
  22. package/lib/errors.js +8 -4
  23. package/lib/errors.js.map +1 -1
  24. package/lib/http.js +12 -11
  25. package/lib/http.js.map +1 -1
  26. package/lib/messages.d.ts +329 -0
  27. package/lib/messages.js +335 -0
  28. package/lib/messages.js.map +1 -0
  29. package/lib/ping.d.ts +2 -1
  30. package/lib/ping.js +13 -11
  31. package/lib/ping.js.map +1 -1
  32. package/lib/presentation.d.ts +1 -0
  33. package/lib/presentation.js +36 -0
  34. package/lib/presentation.js.map +1 -0
  35. package/lib/service.d.ts +6 -5
  36. package/lib/service.js +16 -15
  37. package/lib/service.js.map +1 -1
  38. package/lib/target-guard.js +6 -4
  39. package/lib/target-guard.js.map +1 -1
  40. package/lib/whois.d.ts +2 -1
  41. package/lib/whois.js +18 -16
  42. package/lib/whois.js.map +1 -1
  43. package/package.json +7 -7
  44. package/skills/nettools/tools/dns_lookup/index.js +1 -1
  45. package/skills/nettools/tools/domain_price/index.js +1 -1
  46. package/skills/nettools/tools/net_ping/index.js +1 -1
  47. package/skills/nettools/tools/whois_lookup/index.js +1 -1
package/lib/cooldown.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { message } from './messages.js';
1
2
  /**
2
3
  * Per-user cooldown shared by every nettools command and Agent tool of one
3
4
  * plugin instance. Each call to an external API spends the caller's budget
@@ -38,7 +39,7 @@ export class UserCooldown {
38
39
  const remaining = this.remainingMs(callerKey);
39
40
  if (remaining > 0) {
40
41
  const seconds = Math.ceil(remaining / 1000);
41
- throw new NettoolsError('cooldown', `操作太频繁,请 ${seconds} 秒后再试`, { retryAfterMs: remaining });
42
+ throw new NettoolsError('cooldown', message('cooldown', { seconds }), { retryAfterMs: remaining });
42
43
  }
43
44
  this.#readyAt.delete(callerKey);
44
45
  this.#readyAt.set(callerKey, this.#now() + this.#cooldownMs);
@@ -1 +1 @@
1
- {"version":3,"file":"cooldown.js","sourceRoot":"","sources":["../src/cooldown.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-user cooldown shared by every nettools command and Agent tool of one\n * plugin instance. Each call to an external API spends the caller's budget\n * at admission time (after input validation), so typos are free but a failed\n * lookup still counts — that is what protects the upstream free APIs.\n *\n * State is in-memory and bounded; it resets on restart.\n */\nimport { NettoolsError } from './errors.js';\n\nexport const DEFAULT_COOLDOWN_CAPACITY = 10_000;\n\nexport class UserCooldown {\n readonly #cooldownMs: number;\n readonly #capacity: number;\n readonly #now: () => number;\n /** caller key → epoch ms when the caller may call again. */\n readonly #readyAt = new Map<string, number>();\n\n constructor(cooldownMs: number, now: () => number = Date.now, capacity = DEFAULT_COOLDOWN_CAPACITY) {\n this.#cooldownMs = Math.max(0, cooldownMs);\n this.#now = now;\n this.#capacity = Math.max(1, capacity);\n }\n\n get cooldownMs(): number {\n return this.#cooldownMs;\n }\n\n /** Milliseconds until `callerKey` may call again (0 when allowed). */\n remainingMs(callerKey: string): number {\n const readyAt = this.#readyAt.get(callerKey);\n if (readyAt === undefined) {\n return 0;\n }\n return Math.max(0, readyAt - this.#now());\n }\n\n /** Spend one call for `callerKey`, or throw a `cooldown` error. */\n take(callerKey: string): void {\n if (this.#cooldownMs === 0) {\n return;\n }\n const remaining = this.remainingMs(callerKey);\n if (remaining > 0) {\n const seconds = Math.ceil(remaining / 1000);\n throw new NettoolsError('cooldown', `操作太频繁,请 ${seconds} 秒后再试`, { retryAfterMs: remaining });\n }\n this.#readyAt.delete(callerKey);\n this.#readyAt.set(callerKey, this.#now() + this.#cooldownMs);\n this.#enforceCapacity();\n }\n\n #enforceCapacity(): void {\n if (this.#readyAt.size <= this.#capacity) {\n return;\n }\n const now = this.#now();\n for (const [key, readyAt] of this.#readyAt) {\n if (readyAt <= now) {\n this.#readyAt.delete(key);\n }\n }\n while (this.#readyAt.size > this.#capacity) {\n const oldest = this.#readyAt.keys().next().value;\n if (oldest === undefined) {\n break;\n }\n this.#readyAt.delete(oldest);\n }\n }\n}\n\n/** Cooldown key for a chat command: endpoint + sender, shared across conversations. */\nexport function commandCallerKey(input: {\n readonly metadata?: Readonly<Record<string, unknown>>;\n readonly conversation?: { readonly id?: unknown; readonly endpoint?: { readonly id?: unknown } };\n readonly sender?: { readonly id?: unknown };\n} | undefined): string {\n const senderId = input?.sender?.id;\n if (senderId === undefined || senderId === null || String(senderId) === '') {\n return 'anonymous';\n }\n const endpointId = input?.metadata?.endpointId ?? input?.conversation?.endpoint?.id ?? '';\n return `im:${String(endpointId)}:${String(senderId)}`;\n}\n\n/** Cooldown key for an Agent tool call: the invoking principal. */\nexport function toolCallerKey(context: { readonly principal?: { readonly subjectId?: string } } | undefined): string {\n const subjectId = context?.principal?.subjectId;\n return subjectId ? `principal:${subjectId}` : 'anonymous';\n}\n"]}
1
+ {"version":3,"file":"cooldown.js","sourceRoot":"","sources":["../src/cooldown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC;;;;;;;GAOG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,CAAC,MAAM,yBAAyB,GAAG,MAAM,CAAC;AAEhD,MAAM,OAAO,YAAY;IACd,WAAW,CAAS;IACpB,SAAS,CAAS;IAClB,IAAI,CAAe;IAC5B,4DAA4D;IACnD,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE9C,YAAY,UAAkB,EAAE,MAAoB,IAAI,CAAC,GAAG,EAAE,QAAQ,GAAG,yBAAyB;QAChG,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QAC3C,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;IACzC,CAAC;IAED,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,sEAAsE;IACtE,WAAW,CAAC,SAAiB;QAC3B,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC;QACX,CAAC;QACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,mEAAmE;IACnE,IAAI,CAAC,SAAiB;QACpB,IAAI,IAAI,CAAC,WAAW,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YAClB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;YAC5C,MAAM,IAAI,aAAa,CAAC,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC;QACrG,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAChC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;QAC7D,IAAI,CAAC,gBAAgB,EAAE,CAAC;IAC1B,CAAC;IAED,gBAAgB;QACd,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACzC,OAAO;QACT,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3C,IAAI,OAAO,IAAI,GAAG,EAAE,CAAC;gBACnB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;YAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YACjD,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,MAAM;YACR,CAAC;YACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;CACF;AAED,uFAAuF;AACvF,MAAM,UAAU,gBAAgB,CAAC,KAIpB;IACX,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC;IACnC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC;QAC3E,OAAO,WAAW,CAAC;IACrB,CAAC;IACD,MAAM,UAAU,GAAG,KAAK,EAAE,QAAQ,EAAE,UAAU,IAAI,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC;IAC1F,OAAO,MAAM,MAAM,CAAC,UAAU,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;AACxD,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,aAAa,CAAC,OAA6E;IACzG,MAAM,SAAS,GAAG,OAAO,EAAE,SAAS,EAAE,SAAS,CAAC;IAChD,OAAO,SAAS,CAAC,CAAC,CAAC,aAAa,SAAS,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;AAC5D,CAAC","sourcesContent":["import { message } from './messages.js';\n/**\n * Per-user cooldown shared by every nettools command and Agent tool of one\n * plugin instance. Each call to an external API spends the caller's budget\n * at admission time (after input validation), so typos are free but a failed\n * lookup still counts — that is what protects the upstream free APIs.\n *\n * State is in-memory and bounded; it resets on restart.\n */\nimport { NettoolsError } from './errors.js';\n\nexport const DEFAULT_COOLDOWN_CAPACITY = 10_000;\n\nexport class UserCooldown {\n readonly #cooldownMs: number;\n readonly #capacity: number;\n readonly #now: () => number;\n /** caller key → epoch ms when the caller may call again. */\n readonly #readyAt = new Map<string, number>();\n\n constructor(cooldownMs: number, now: () => number = Date.now, capacity = DEFAULT_COOLDOWN_CAPACITY) {\n this.#cooldownMs = Math.max(0, cooldownMs);\n this.#now = now;\n this.#capacity = Math.max(1, capacity);\n }\n\n get cooldownMs(): number {\n return this.#cooldownMs;\n }\n\n /** Milliseconds until `callerKey` may call again (0 when allowed). */\n remainingMs(callerKey: string): number {\n const readyAt = this.#readyAt.get(callerKey);\n if (readyAt === undefined) {\n return 0;\n }\n return Math.max(0, readyAt - this.#now());\n }\n\n /** Spend one call for `callerKey`, or throw a `cooldown` error. */\n take(callerKey: string): void {\n if (this.#cooldownMs === 0) {\n return;\n }\n const remaining = this.remainingMs(callerKey);\n if (remaining > 0) {\n const seconds = Math.ceil(remaining / 1000);\n throw new NettoolsError('cooldown', message('cooldown', { seconds }), { retryAfterMs: remaining });\n }\n this.#readyAt.delete(callerKey);\n this.#readyAt.set(callerKey, this.#now() + this.#cooldownMs);\n this.#enforceCapacity();\n }\n\n #enforceCapacity(): void {\n if (this.#readyAt.size <= this.#capacity) {\n return;\n }\n const now = this.#now();\n for (const [key, readyAt] of this.#readyAt) {\n if (readyAt <= now) {\n this.#readyAt.delete(key);\n }\n }\n while (this.#readyAt.size > this.#capacity) {\n const oldest = this.#readyAt.keys().next().value;\n if (oldest === undefined) {\n break;\n }\n this.#readyAt.delete(oldest);\n }\n }\n}\n\n/** Cooldown key for a chat command: endpoint + sender, shared across conversations. */\nexport function commandCallerKey(input: {\n readonly metadata?: Readonly<Record<string, unknown>>;\n readonly conversation?: { readonly id?: unknown; readonly endpoint?: { readonly id?: unknown } };\n readonly sender?: { readonly id?: unknown };\n} | undefined): string {\n const senderId = input?.sender?.id;\n if (senderId === undefined || senderId === null || String(senderId) === '') {\n return 'anonymous';\n }\n const endpointId = input?.metadata?.endpointId ?? input?.conversation?.endpoint?.id ?? '';\n return `im:${String(endpointId)}:${String(senderId)}`;\n}\n\n/** Cooldown key for an Agent tool call: the invoking principal. */\nexport function toolCallerKey(context: { readonly principal?: { readonly subjectId?: string } } | undefined): string {\n const subjectId = context?.principal?.subjectId;\n return subjectId ? `principal:${subjectId}` : 'anonymous';\n}\n"]}
package/lib/dns.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type Locale } from 'zhin.js/i18n';
1
2
  import type { HttpClient } from './http.js';
2
3
  export declare const DNS_RECORD_TYPES: {
3
4
  readonly A: 1;
@@ -49,4 +50,4 @@ export declare class DohResolver {
49
50
  }
50
51
  /** Validate and normalise one DoH JSON body. Throws `malformed` or `http` (SERVFAIL etc.). */
51
52
  export declare function parseDohResponse(body: unknown, name: string, type: DnsRecordType, provider: string): DnsAnswer;
52
- export declare function formatDnsAnswer(answer: DnsAnswer, maxRecords?: number): string;
53
+ export declare function formatDnsAnswer(answer: DnsAnswer, maxRecords?: number, locale?: Locale): string;
package/lib/dns.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { messages } from './presentation.js';
2
+ import { message } from './messages.js';
1
3
  /**
2
4
  * DNS lookups over HTTPS (DoH JSON API) with provider fallback.
3
5
  *
@@ -59,7 +61,7 @@ export function parseDnsType(input) {
59
61
  if (Object.hasOwn(DNS_RECORD_TYPES, upper)) {
60
62
  return upper;
61
63
  }
62
- throw new NettoolsError('invalid_input', `不支持的记录类型 ${input},可选:${SUPPORTED_DNS_TYPES.join(' / ')}`);
64
+ throw new NettoolsError('invalid_input', message('dnsType', { type: input, types: SUPPORTED_DNS_TYPES.join(' / ') }));
63
65
  }
64
66
  export class DohResolver {
65
67
  #http;
@@ -88,15 +90,15 @@ export class DohResolver {
88
90
  }
89
91
  const wrapped = error instanceof NettoolsError
90
92
  ? error
91
- : new NettoolsError('network', `${provider.label} 查询失败`, { cause: error });
93
+ : new NettoolsError('network', message('queryFailed', { provider: provider.label }), { cause: error });
92
94
  lastError = wrapped;
93
- failures.push(wrapped.message);
95
+ failures.push(wrapped.detail ?? wrapped.message);
94
96
  }
95
97
  }
96
98
  if (failures.length <= 1 && lastError) {
97
99
  throw lastError;
98
100
  }
99
- throw new NettoolsError('network', `所有 DNS 服务均不可用:${failures.join(';')}`, { cause: lastError });
101
+ throw new NettoolsError('network', message('dnsUnavailable', { failures }), { cause: lastError });
100
102
  }
101
103
  /**
102
104
  * When another provider is still available we retry at most once here and
@@ -120,12 +122,12 @@ export class DohResolver {
120
122
  /** Validate and normalise one DoH JSON body. Throws `malformed` or `http` (SERVFAIL etc.). */
121
123
  export function parseDohResponse(body, name, type, provider) {
122
124
  if (!isRecord(body) || typeof body.Status !== 'number') {
123
- throw new NettoolsError('malformed', `${provider} 返回了无法识别的 DNS 响应`);
125
+ throw new NettoolsError('malformed', message('dnsMalformed', { provider }));
124
126
  }
125
127
  const status = body.Status;
126
128
  if (status !== 0 && status !== 3) {
127
129
  const rcode = RCODE_NAMES[status] ?? `RCODE ${status}`;
128
- throw new NettoolsError('http', `${provider} 解析失败(${rcode})`);
130
+ throw new NettoolsError('http', message('dnsFailed', { provider, rcode }));
129
131
  }
130
132
  const rawAnswers = Array.isArray(body.Answer) ? body.Answer : [];
131
133
  const records = [];
@@ -156,15 +158,15 @@ function parseDnsRecord(raw) {
156
158
  const name = typeof raw.name === 'string' ? raw.name.replace(/\.$/u, '') : '';
157
159
  return Object.freeze({ name, type: typeName, ttl, data: raw.data });
158
160
  }
159
- export function formatDnsAnswer(answer, maxRecords = 20) {
160
- const header = `🔎 ${answer.name} ${answer.type} 记录(${answer.provider}${answer.authenticated ? ',DNSSEC 已验证' : ''})`;
161
+ export function formatDnsAnswer(answer, maxRecords = 20, locale = 'zh-CN') {
162
+ const header = messages(locale, 'dnsHeader', { name: answer.name, type: answer.type, provider: answer.provider, verified: answer.authenticated ? messages(locale, 'verified') : '' });
161
163
  if (answer.status === 'NXDOMAIN') {
162
- return `${header}\n域名不存在 (NXDOMAIN)`;
164
+ return `${header}\n${messages(locale, 'nxdomain')}`;
163
165
  }
164
166
  const matching = answer.records.filter((record) => record.type === answer.type);
165
167
  const chain = answer.records.filter((record) => record.type !== answer.type);
166
168
  if (matching.length === 0 && chain.length === 0) {
167
- return `${header}\n没有 ${answer.type} 记录`;
169
+ return `${header}\n${messages(locale, 'noRecord', { type: answer.type })}`;
168
170
  }
169
171
  const lines = [header];
170
172
  for (const record of chain.slice(0, maxRecords)) {
@@ -175,10 +177,10 @@ export function formatDnsAnswer(answer, maxRecords = 20) {
175
177
  lines.push(`• ${truncate(record.data)} (TTL ${record.ttl})`);
176
178
  }
177
179
  if (matching.length > shown.length) {
178
- lines.push(`…另有 ${matching.length - shown.length} 条`);
180
+ lines.push(messages(locale, 'moreRecords', { count: matching.length - shown.length }));
179
181
  }
180
182
  if (matching.length === 0) {
181
- lines.push(`没有直接的 ${answer.type} 记录`);
183
+ lines.push(messages(locale, 'noDirectRecord', { type: answer.type }));
182
184
  }
183
185
  return lines.join('\n');
184
186
  }
package/lib/dns.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"dns.js","sourceRoot":"","sources":["../src/dns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,CAAC,EAAE,CAAC;IACJ,EAAE,EAAE,CAAC;IACL,KAAK,EAAE,CAAC;IACR,GAAG,EAAE,CAAC;IACN,GAAG,EAAE,EAAE;IACP,EAAE,EAAE,EAAE;IACN,GAAG,EAAE,EAAE;IACP,IAAI,EAAE,EAAE;IACR,GAAG,EAAE,EAAE;IACP,EAAE,EAAE,EAAE;IACN,MAAM,EAAE,EAAE;IACV,GAAG,EAAE,GAAG;CACA,CAAC;AAIX,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAoB,CAAC;AAUpF,MAAM,CAAC,MAAM,aAAa,GAAiD,MAAM,CAAC,MAAM,CAAC;IACvF,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC;QACxB,EAAE,EAAE,YAAY;QAChB,KAAK,EAAE,gBAAgB;QACvB,QAAQ,EAAE,sCAAsC;KACjD,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,YAAY;QACnB,QAAQ,EAAE,gCAAgC;KAC3C,CAAC;IACF,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;QACpB,EAAE,EAAE,QAAQ;QACZ,KAAK,EAAE,YAAY;QACnB,QAAQ,EAAE,4BAA4B;KACvC,CAAC;CACH,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,qBAAqB,GAA6B,CAAC,YAAY,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAqBlG,MAAM,WAAW,GAAqC;IACpD,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,UAAU;IACb,CAAC,EAAE,UAAU;IACb,CAAC,EAAE,QAAQ;IACX,CAAC,EAAE,SAAS;CACb,CAAC;AAEF,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAChC,MAAM,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CACrE,CAAC;AAEF,MAAM,UAAU,YAAY,CAAC,KAAyB;IACpD,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QAC/C,OAAO,GAAG,CAAC;IACb,CAAC;IACD,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACzC,IAAI,MAAM,CAAC,MAAM,CAAC,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC;QAC3C,OAAO,KAAsB,CAAC;IAChC,CAAC;IACD,MAAM,IAAI,aAAa,CACrB,eAAe,EACf,YAAY,KAAK,OAAO,mBAAmB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAC1D,CAAC;AACJ,CAAC;AAED,MAAM,OAAO,WAAW;IACb,KAAK,CAAa;IAClB,UAAU,CAAyB;IAE5C,YAAY,IAAgB,EAAE,cAAwC,qBAAqB;QACzF,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC;QACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,qBAAqB,CAAC,CAAC;QACzE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5D,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAED,6FAA6F;IAC7F,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,IAAmB,EAAE,MAAoB;QACnE,MAAM,QAAQ,GAAa,EAAE,CAAC;QAC9B,IAAI,SAAoC,CAAC;QACzC,KAAK,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC;YAC1D,MAAM,WAAW,GAAG,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC;YACvD,IAAI,CAAC;gBACH,OAAO,MAAM,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC;YAC5E,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,KAAK,YAAY,aAAa,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;oBAC/D,MAAM,KAAK,CAAC;gBACd,CAAC;gBACD,MAAM,OAAO,GAAG,KAAK,YAAY,aAAa;oBAC5C,CAAC,CAAC,KAAK;oBACP,CAAC,CAAC,IAAI,aAAa,CAAC,SAAS,EAAE,GAAG,QAAQ,CAAC,KAAK,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;gBAC7E,SAAS,GAAG,OAAO,CAAC;gBACpB,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,IAAI,SAAS,EAAE,CAAC;YACtC,MAAM,SAAS,CAAC;QAClB,CAAC;QACD,MAAM,IAAI,aAAa,CAAC,SAAS,EAAE,iBAAiB,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAClG,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,YAAY,CAChB,QAAqB,EACrB,IAAY,EACZ,IAAmB,EACnB,WAAoB,EACpB,MAAoB;QAEpB,MAAM,iBAAiB,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,UAAU,CAAC;QACvD,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC;QACpF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACvC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,EAAE;YAC/C,QAAQ,EAAE,QAAQ,CAAC,KAAK;YACxB,MAAM,EAAE,sBAAsB;YAC9B,UAAU;YACV,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC9B,CAAC,CAAC;QACH,OAAO,gBAAgB,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACrE,CAAC;CACF;AAED,8FAA8F;AAC9F,MAAM,UAAU,gBAAgB,CAAC,IAAa,EAAE,IAAY,EAAE,IAAmB,EAAE,QAAgB;IACjG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;QACvD,MAAM,IAAI,aAAa,CAAC,WAAW,EAAE,GAAG,QAAQ,kBAAkB,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IAC3B,IAAI,MAAM,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,WAAW,CAAC,MAAM,CAAC,IAAI,SAAS,MAAM,EAAE,CAAC;QACvD,MAAM,IAAI,aAAa,CAAC,MAAM,EAAE,GAAG,QAAQ,SAAS,KAAK,GAAG,CAAC,CAAC;IAChE,CAAC;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACjE,MAAM,OAAO,GAAgB,EAAE,CAAC;IAChC,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,IAAI;QACJ,IAAI;QACJ,MAAM,EAAE,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;QAC7C,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC;QAC/B,aAAa,EAAE,IAAI,CAAC,EAAE,KAAK,IAAI;QAC/B,QAAQ;KACT,CAAC,CAAC;AACL,CAAC;AAED,SAAS,cAAc,CAAC,GAAY;IAClC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QACjE,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;IACvE,MAAM,GAAG,GAAG,OAAO,GAAG,CAAC,GAAG,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAClF,MAAM,IAAI,GAAG,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9E,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;AACtE,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAAiB,EAAE,UAAU,GAAG,EAAE;IAChE,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,OAAO,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;IACrH,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,OAAO,GAAG,MAAM,oBAAoB,CAAC;IACvC,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC;IAChF,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,GAAG,MAAM,QAAQ,MAAM,CAAC,IAAI,KAAK,CAAC;IAC3C,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,MAAM,CAAC,CAAC;IACvB,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC;QAChD,KAAK,CAAC,IAAI,CAAC,KAAK,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IACD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;IAC5C,KAAK,MAAM,MAAM,IAAI,KAAK,EAAE,CAAC;QAC3B,KAAK,CAAC,IAAI,CAAC,KAAK,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC;IAChE,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACnC,KAAK,CAAC,IAAI,CAAC,OAAO,QAAQ,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,KAAK,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC;IACxC,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,QAAQ,CAAC,IAAY,EAAE,GAAG,GAAG,GAAG;IACvC,OAAO,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7D,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC","sourcesContent":["/**\n * DNS lookups over HTTPS (DoH JSON API) with provider fallback.\n *\n * Cloudflare (`cloudflare-dns.com/dns-query`), AliDNS (`dns.alidns.com/resolve`,\n * reachable from mainland China) and Google (`dns.google/resolve`) speak the\n * same JSON dialect. Providers are tried in\n * order; we move on to the next one when a provider is unreachable, times out,\n * returns an HTTP error, returns malformed JSON or answers SERVFAIL/REFUSED.\n * NXDOMAIN and \"no records\" are authoritative answers and are returned as-is.\n */\nimport { NettoolsError } from './errors.js';\nimport type { HttpClient } from './http.js';\n\nexport const DNS_RECORD_TYPES = {\n A: 1,\n NS: 2,\n CNAME: 5,\n SOA: 6,\n PTR: 12,\n MX: 15,\n TXT: 16,\n AAAA: 28,\n SRV: 33,\n DS: 43,\n DNSKEY: 48,\n CAA: 257,\n} as const;\n\nexport type DnsRecordType = keyof typeof DNS_RECORD_TYPES;\n\nexport const SUPPORTED_DNS_TYPES = Object.keys(DNS_RECORD_TYPES) as DnsRecordType[];\n\nexport type DohProviderId = 'cloudflare' | 'alidns' | 'google';\n\nexport interface DohProvider {\n readonly id: DohProviderId;\n readonly label: string;\n readonly endpoint: string;\n}\n\nexport const DOH_PROVIDERS: Readonly<Record<DohProviderId, DohProvider>> = Object.freeze({\n cloudflare: Object.freeze({\n id: 'cloudflare',\n label: 'Cloudflare DoH',\n endpoint: 'https://cloudflare-dns.com/dns-query',\n }),\n alidns: Object.freeze({\n id: 'alidns',\n label: 'AliDNS DoH',\n endpoint: 'https://dns.alidns.com/resolve',\n }),\n google: Object.freeze({\n id: 'google',\n label: 'Google DoH',\n endpoint: 'https://dns.google/resolve',\n }),\n});\n\nexport const DEFAULT_DOH_PROVIDERS: readonly DohProviderId[] = ['cloudflare', 'alidns', 'google'];\n\nexport interface DnsRecord {\n readonly name: string;\n readonly type: string;\n readonly ttl: number;\n readonly data: string;\n}\n\nexport type DnsStatus = 'NOERROR' | 'NXDOMAIN';\n\nexport interface DnsAnswer {\n readonly name: string;\n readonly type: DnsRecordType;\n readonly status: DnsStatus;\n readonly records: readonly DnsRecord[];\n /** Whether the resolver validated the answer with DNSSEC (AD bit). */\n readonly authenticated: boolean;\n readonly provider: string;\n}\n\nconst RCODE_NAMES: Readonly<Record<number, string>> = {\n 0: 'NOERROR',\n 1: 'FORMERR',\n 2: 'SERVFAIL',\n 3: 'NXDOMAIN',\n 4: 'NOTIMP',\n 5: 'REFUSED',\n};\n\nconst TYPE_NAMES_BY_CODE = new Map<number, string>(\n Object.entries(DNS_RECORD_TYPES).map(([name, code]) => [code, name]),\n);\n\nexport function parseDnsType(input: string | undefined): DnsRecordType {\n if (input === undefined || input.trim() === '') {\n return 'A';\n }\n const upper = input.trim().toUpperCase();\n if (Object.hasOwn(DNS_RECORD_TYPES, upper)) {\n return upper as DnsRecordType;\n }\n throw new NettoolsError(\n 'invalid_input',\n `不支持的记录类型 ${input},可选:${SUPPORTED_DNS_TYPES.join(' / ')}`,\n );\n}\n\nexport class DohResolver {\n readonly #http: HttpClient;\n readonly #providers: readonly DohProvider[];\n\n constructor(http: HttpClient, providerIds: readonly DohProviderId[] = DEFAULT_DOH_PROVIDERS) {\n const unique = [...new Set(providerIds)];\n const selected = unique.length > 0 ? unique : [...DEFAULT_DOH_PROVIDERS];\n this.#http = http;\n this.#providers = selected.map((id) => DOH_PROVIDERS[id]);\n }\n\n get providers(): readonly DohProvider[] {\n return this.#providers;\n }\n\n /** Resolve `name` (already validated) for one record type, falling back across providers. */\n async resolve(name: string, type: DnsRecordType, signal?: AbortSignal): Promise<DnsAnswer> {\n const failures: string[] = [];\n let lastError: NettoolsError | undefined;\n for (const [index, provider] of this.#providers.entries()) {\n const hasFallback = index < this.#providers.length - 1;\n try {\n return await this.#resolveWith(provider, name, type, hasFallback, signal);\n } catch (error) {\n if (error instanceof NettoolsError && error.kind === 'aborted') {\n throw error;\n }\n const wrapped = error instanceof NettoolsError\n ? error\n : new NettoolsError('network', `${provider.label} 查询失败`, { cause: error });\n lastError = wrapped;\n failures.push(wrapped.message);\n }\n }\n if (failures.length <= 1 && lastError) {\n throw lastError;\n }\n throw new NettoolsError('network', `所有 DNS 服务均不可用:${failures.join(';')}`, { cause: lastError });\n }\n\n /**\n * When another provider is still available we retry at most once here and\n * let the fallback absorb the rest, keeping the worst-case latency bounded.\n */\n async #resolveWith(\n provider: DohProvider,\n name: string,\n type: DnsRecordType,\n hasFallback: boolean,\n signal?: AbortSignal,\n ): Promise<DnsAnswer> {\n const configuredRetries = this.#http.policy.maxRetries;\n const maxRetries = hasFallback ? Math.min(1, configuredRetries) : configuredRetries;\n const url = new URL(provider.endpoint);\n url.searchParams.set('name', name);\n url.searchParams.set('type', type);\n const response = await this.#http.fetchJson(url, {\n provider: provider.label,\n accept: 'application/dns-json',\n maxRetries,\n ...(signal ? { signal } : {}),\n });\n return parseDohResponse(response.body, name, type, provider.label);\n }\n}\n\n/** Validate and normalise one DoH JSON body. Throws `malformed` or `http` (SERVFAIL etc.). */\nexport function parseDohResponse(body: unknown, name: string, type: DnsRecordType, provider: string): DnsAnswer {\n if (!isRecord(body) || typeof body.Status !== 'number') {\n throw new NettoolsError('malformed', `${provider} 返回了无法识别的 DNS 响应`);\n }\n const status = body.Status;\n if (status !== 0 && status !== 3) {\n const rcode = RCODE_NAMES[status] ?? `RCODE ${status}`;\n throw new NettoolsError('http', `${provider} 解析失败(${rcode})`);\n }\n const rawAnswers = Array.isArray(body.Answer) ? body.Answer : [];\n const records: DnsRecord[] = [];\n for (const raw of rawAnswers) {\n const record = parseDnsRecord(raw);\n if (record) {\n records.push(record);\n }\n }\n return Object.freeze({\n name,\n type,\n status: status === 3 ? 'NXDOMAIN' : 'NOERROR',\n records: Object.freeze(records),\n authenticated: body.AD === true,\n provider,\n });\n}\n\nfunction parseDnsRecord(raw: unknown): DnsRecord | undefined {\n if (!isRecord(raw)) {\n return undefined;\n }\n if (typeof raw.data !== 'string' || typeof raw.type !== 'number') {\n return undefined;\n }\n const typeName = TYPE_NAMES_BY_CODE.get(raw.type) ?? `TYPE${raw.type}`;\n const ttl = typeof raw.TTL === 'number' && Number.isFinite(raw.TTL) ? raw.TTL : 0;\n const name = typeof raw.name === 'string' ? raw.name.replace(/\\.$/u, '') : '';\n return Object.freeze({ name, type: typeName, ttl, data: raw.data });\n}\n\nexport function formatDnsAnswer(answer: DnsAnswer, maxRecords = 20): string {\n const header = `🔎 ${answer.name} ${answer.type} 记录(${answer.provider}${answer.authenticated ? ',DNSSEC 已验证' : ''})`;\n if (answer.status === 'NXDOMAIN') {\n return `${header}\\n域名不存在 (NXDOMAIN)`;\n }\n const matching = answer.records.filter((record) => record.type === answer.type);\n const chain = answer.records.filter((record) => record.type !== answer.type);\n if (matching.length === 0 && chain.length === 0) {\n return `${header}\\n没有 ${answer.type} 记录`;\n }\n const lines = [header];\n for (const record of chain.slice(0, maxRecords)) {\n lines.push(`↪ ${record.name} ${record.type} ${truncate(record.data)}`);\n }\n const shown = matching.slice(0, maxRecords);\n for (const record of shown) {\n lines.push(`• ${truncate(record.data)} (TTL ${record.ttl})`);\n }\n if (matching.length > shown.length) {\n lines.push(`…另有 ${matching.length - shown.length} 条`);\n }\n if (matching.length === 0) {\n lines.push(`没有直接的 ${answer.type} 记录`);\n }\n return lines.join('\\n');\n}\n\nfunction truncate(text: string, max = 300): string {\n return text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
1
+ {"version":3,"file":"dns.js","sourceRoot":"","sources":["../src/dns.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Locale } from 'zhin.js/i18n';\nimport { messages } from './presentation.js';\nimport { message } from './messages.js';\n/**\n * DNS lookups over HTTPS (DoH JSON API) with provider fallback.\n *\n * Cloudflare (`cloudflare-dns.com/dns-query`), AliDNS (`dns.alidns.com/resolve`,\n * reachable from mainland China) and Google (`dns.google/resolve`) speak the\n * same JSON dialect. Providers are tried in\n * order; we move on to the next one when a provider is unreachable, times out,\n * returns an HTTP error, returns malformed JSON or answers SERVFAIL/REFUSED.\n * NXDOMAIN and \"no records\" are authoritative answers and are returned as-is.\n */\nimport { NettoolsError } from './errors.js';\nimport type { HttpClient } from './http.js';\n\nexport const DNS_RECORD_TYPES = {\n A: 1,\n NS: 2,\n CNAME: 5,\n SOA: 6,\n PTR: 12,\n MX: 15,\n TXT: 16,\n AAAA: 28,\n SRV: 33,\n DS: 43,\n DNSKEY: 48,\n CAA: 257,\n} as const;\n\nexport type DnsRecordType = keyof typeof DNS_RECORD_TYPES;\n\nexport const SUPPORTED_DNS_TYPES = Object.keys(DNS_RECORD_TYPES) as DnsRecordType[];\n\nexport type DohProviderId = 'cloudflare' | 'alidns' | 'google';\n\nexport interface DohProvider {\n readonly id: DohProviderId;\n readonly label: string;\n readonly endpoint: string;\n}\n\nexport const DOH_PROVIDERS: Readonly<Record<DohProviderId, DohProvider>> = Object.freeze({\n cloudflare: Object.freeze({\n id: 'cloudflare',\n label: 'Cloudflare DoH',\n endpoint: 'https://cloudflare-dns.com/dns-query',\n }),\n alidns: Object.freeze({\n id: 'alidns',\n label: 'AliDNS DoH',\n endpoint: 'https://dns.alidns.com/resolve',\n }),\n google: Object.freeze({\n id: 'google',\n label: 'Google DoH',\n endpoint: 'https://dns.google/resolve',\n }),\n});\n\nexport const DEFAULT_DOH_PROVIDERS: readonly DohProviderId[] = ['cloudflare', 'alidns', 'google'];\n\nexport interface DnsRecord {\n readonly name: string;\n readonly type: string;\n readonly ttl: number;\n readonly data: string;\n}\n\nexport type DnsStatus = 'NOERROR' | 'NXDOMAIN';\n\nexport interface DnsAnswer {\n readonly name: string;\n readonly type: DnsRecordType;\n readonly status: DnsStatus;\n readonly records: readonly DnsRecord[];\n /** Whether the resolver validated the answer with DNSSEC (AD bit). */\n readonly authenticated: boolean;\n readonly provider: string;\n}\n\nconst RCODE_NAMES: Readonly<Record<number, string>> = {\n 0: 'NOERROR',\n 1: 'FORMERR',\n 2: 'SERVFAIL',\n 3: 'NXDOMAIN',\n 4: 'NOTIMP',\n 5: 'REFUSED',\n};\n\nconst TYPE_NAMES_BY_CODE = new Map<number, string>(\n Object.entries(DNS_RECORD_TYPES).map(([name, code]) => [code, name]),\n);\n\nexport function parseDnsType(input: string | undefined): DnsRecordType {\n if (input === undefined || input.trim() === '') {\n return 'A';\n }\n const upper = input.trim().toUpperCase();\n if (Object.hasOwn(DNS_RECORD_TYPES, upper)) {\n return upper as DnsRecordType;\n }\n throw new NettoolsError(\n 'invalid_input',\n message('dnsType', { type: input, types: SUPPORTED_DNS_TYPES.join(' / ') }),\n );\n}\n\nexport class DohResolver {\n readonly #http: HttpClient;\n readonly #providers: readonly DohProvider[];\n\n constructor(http: HttpClient, providerIds: readonly DohProviderId[] = DEFAULT_DOH_PROVIDERS) {\n const unique = [...new Set(providerIds)];\n const selected = unique.length > 0 ? unique : [...DEFAULT_DOH_PROVIDERS];\n this.#http = http;\n this.#providers = selected.map((id) => DOH_PROVIDERS[id]);\n }\n\n get providers(): readonly DohProvider[] {\n return this.#providers;\n }\n\n /** Resolve `name` (already validated) for one record type, falling back across providers. */\n async resolve(name: string, type: DnsRecordType, signal?: AbortSignal): Promise<DnsAnswer> {\n const failures: (string | import('./messages.js').NettoolsMessage)[] = [];\n let lastError: NettoolsError | undefined;\n for (const [index, provider] of this.#providers.entries()) {\n const hasFallback = index < this.#providers.length - 1;\n try {\n return await this.#resolveWith(provider, name, type, hasFallback, signal);\n } catch (error) {\n if (error instanceof NettoolsError && error.kind === 'aborted') {\n throw error;\n }\n const wrapped = error instanceof NettoolsError\n ? error\n : new NettoolsError('network', message('queryFailed', { provider: provider.label }), { cause: error });\n lastError = wrapped;\n failures.push(wrapped.detail ?? wrapped.message);\n }\n }\n if (failures.length <= 1 && lastError) {\n throw lastError;\n }\n throw new NettoolsError('network', message('dnsUnavailable', { failures }), { cause: lastError });\n }\n\n /**\n * When another provider is still available we retry at most once here and\n * let the fallback absorb the rest, keeping the worst-case latency bounded.\n */\n async #resolveWith(\n provider: DohProvider,\n name: string,\n type: DnsRecordType,\n hasFallback: boolean,\n signal?: AbortSignal,\n ): Promise<DnsAnswer> {\n const configuredRetries = this.#http.policy.maxRetries;\n const maxRetries = hasFallback ? Math.min(1, configuredRetries) : configuredRetries;\n const url = new URL(provider.endpoint);\n url.searchParams.set('name', name);\n url.searchParams.set('type', type);\n const response = await this.#http.fetchJson(url, {\n provider: provider.label,\n accept: 'application/dns-json',\n maxRetries,\n ...(signal ? { signal } : {}),\n });\n return parseDohResponse(response.body, name, type, provider.label);\n }\n}\n\n/** Validate and normalise one DoH JSON body. Throws `malformed` or `http` (SERVFAIL etc.). */\nexport function parseDohResponse(body: unknown, name: string, type: DnsRecordType, provider: string): DnsAnswer {\n if (!isRecord(body) || typeof body.Status !== 'number') {\n throw new NettoolsError('malformed', message('dnsMalformed', { provider }));\n }\n const status = body.Status;\n if (status !== 0 && status !== 3) {\n const rcode = RCODE_NAMES[status] ?? `RCODE ${status}`;\n throw new NettoolsError('http', message('dnsFailed', { provider, rcode }));\n }\n const rawAnswers = Array.isArray(body.Answer) ? body.Answer : [];\n const records: DnsRecord[] = [];\n for (const raw of rawAnswers) {\n const record = parseDnsRecord(raw);\n if (record) {\n records.push(record);\n }\n }\n return Object.freeze({\n name,\n type,\n status: status === 3 ? 'NXDOMAIN' : 'NOERROR',\n records: Object.freeze(records),\n authenticated: body.AD === true,\n provider,\n });\n}\n\nfunction parseDnsRecord(raw: unknown): DnsRecord | undefined {\n if (!isRecord(raw)) {\n return undefined;\n }\n if (typeof raw.data !== 'string' || typeof raw.type !== 'number') {\n return undefined;\n }\n const typeName = TYPE_NAMES_BY_CODE.get(raw.type) ?? `TYPE${raw.type}`;\n const ttl = typeof raw.TTL === 'number' && Number.isFinite(raw.TTL) ? raw.TTL : 0;\n const name = typeof raw.name === 'string' ? raw.name.replace(/\\.$/u, '') : '';\n return Object.freeze({ name, type: typeName, ttl, data: raw.data });\n}\n\nexport function formatDnsAnswer(answer: DnsAnswer, maxRecords = 20, locale: Locale = 'zh-CN'): string {\n const header = messages(locale, 'dnsHeader', { name: answer.name, type: answer.type, provider: answer.provider, verified: answer.authenticated ? messages(locale, 'verified') : '' });\n if (answer.status === 'NXDOMAIN') {\n return `${header}\\n${messages(locale, 'nxdomain')}`;\n }\n const matching = answer.records.filter((record) => record.type === answer.type);\n const chain = answer.records.filter((record) => record.type !== answer.type);\n if (matching.length === 0 && chain.length === 0) {\n return `${header}\\n${messages(locale, 'noRecord', { type: answer.type })}`;\n }\n const lines = [header];\n for (const record of chain.slice(0, maxRecords)) {\n lines.push(`↪ ${record.name} ${record.type} ${truncate(record.data)}`);\n }\n const shown = matching.slice(0, maxRecords);\n for (const record of shown) {\n lines.push(`• ${truncate(record.data)} (TTL ${record.ttl})`);\n }\n if (matching.length > shown.length) {\n lines.push(messages(locale, 'moreRecords', { count: matching.length - shown.length }));\n }\n if (matching.length === 0) {\n lines.push(messages(locale, 'noDirectRecord', { type: answer.type }));\n }\n return lines.join('\\n');\n}\n\nfunction truncate(text: string, max = 300): string {\n return text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import { type Locale } from 'zhin.js/i18n';
1
2
  import type { HttpClient } from './http.js';
2
3
  export declare const NAZHUMI_API_URL = "https://www.nazhumi.com/api/v1";
3
4
  export type PriceKind = 'register' | 'renew' | 'transfer';
@@ -25,4 +26,4 @@ export declare function parsePriceKind(input: string | undefined): PriceKind;
25
26
  export declare function parseDomainSuffix(input: string): string;
26
27
  export declare function comparePrices(http: HttpClient, suffix: string, kind: PriceKind, signal?: AbortSignal): Promise<PriceComparison>;
27
28
  export declare function parsePriceResponse(body: unknown, suffix: string, kind: PriceKind): PriceComparison;
28
- export declare function formatPriceComparison(comparison: PriceComparison, limit: number): string;
29
+ export declare function formatPriceComparison(comparison: PriceComparison, limit: number, locale?: Locale): string;
@@ -1,3 +1,5 @@
1
+ import { messages } from './presentation.js';
2
+ import { message } from './messages.js';
1
3
  /**
2
4
  * Domain registration / renewal / transfer price comparison through the
3
5
  * public nazhumi.com API (`GET /api/v1?domain=<suffix>&order=register`).
@@ -25,18 +27,13 @@ const PRICE_KIND_ALIASES = {
25
27
  transfer: 'transfer',
26
28
  转入: 'transfer',
27
29
  };
28
- const PRICE_KIND_LABELS = {
29
- register: '注册',
30
- renew: '续费',
31
- transfer: '转入',
32
- };
33
30
  export function parsePriceKind(input) {
34
31
  if (input === undefined || input.trim() === '') {
35
32
  return 'register';
36
33
  }
37
34
  const kind = PRICE_KIND_ALIASES[input.trim().toLowerCase()];
38
35
  if (!kind) {
39
- throw new NettoolsError('invalid_input', '价格类型可选:注册(register) / 续费(renew) / 转入(transfer)');
36
+ throw new NettoolsError('invalid_input', message('priceKind'));
40
37
  }
41
38
  return kind;
42
39
  }
@@ -51,11 +48,11 @@ export function parseDomainSuffix(input) {
51
48
  const explicitSuffix = trimmed.startsWith('.');
52
49
  const withoutDot = explicitSuffix ? trimmed.slice(1) : trimmed;
53
50
  if (withoutDot.length === 0) {
54
- throw new NettoolsError('invalid_input', '请提供域名后缀,例如 com 或 .cn');
51
+ throw new NettoolsError('invalid_input', message('suffixRequired'));
55
52
  }
56
53
  const parsed = parseHostInput(withoutDot, { minLabels: 1 });
57
54
  if (parsed.kind !== 'domain') {
58
- throw new NettoolsError('invalid_input', '请提供域名或后缀,而不是 IP 地址');
55
+ throw new NettoolsError('invalid_input', message('suffixNotIp'));
59
56
  }
60
57
  const labels = parsed.name.split('.');
61
58
  if (explicitSuffix || labels.length === 1) {
@@ -75,11 +72,11 @@ export async function comparePrices(http, suffix, kind, signal) {
75
72
  }
76
73
  export function parsePriceResponse(body, suffix, kind) {
77
74
  if (!isRecord(body)) {
78
- throw new NettoolsError('malformed', 'nazhumi.com 返回了无法识别的数据');
75
+ throw new NettoolsError('malformed', message('priceMalformed'));
79
76
  }
80
77
  if (typeof body.code === 'number' && body.code !== 100 && body.code !== 200 && body.code !== 0) {
81
- const message = typeof body.msg === 'string' ? body.msg : typeof body.message === 'string' ? body.message : '';
82
- throw new NettoolsError('http', `nazhumi.com 返回错误 ${body.code}${message ? `:${message}` : ''}`);
78
+ const providerMessage = typeof body.msg === 'string' ? body.msg : typeof body.message === 'string' ? body.message : '';
79
+ throw new NettoolsError('http', message('priceError', { code: body.code, detail: providerMessage ? `: ${providerMessage}` : '' }));
83
80
  }
84
81
  const data = isRecord(body.data) ? body.data : {};
85
82
  const rawPrices = Array.isArray(data.price) ? data.price : [];
@@ -141,22 +138,22 @@ function compareByCnyPrice(left, right) {
141
138
  }
142
139
  return left.priceCny - right.priceCny;
143
140
  }
144
- export function formatPriceComparison(comparison, limit) {
145
- const label = PRICE_KIND_LABELS[comparison.kind];
141
+ export function formatPriceComparison(comparison, limit, locale = 'zh-CN') {
142
+ const label = messages(locale, comparison.kind);
146
143
  if (comparison.entries.length === 0) {
147
- return `💰 .${comparison.suffix} 暂无${label}价格数据`;
144
+ return messages(locale, 'noPrice', { suffix: comparison.suffix, kind: label });
148
145
  }
149
- const lines = [`💰 .${comparison.suffix} ${label}价格(低→高,nazhumi.com)`];
146
+ const lines = [messages(locale, 'priceHeader', { suffix: comparison.suffix, kind: label })];
150
147
  comparison.entries.slice(0, limit).forEach((entry, index) => {
151
148
  const cny = entry.priceCny === undefined ? '' : `(≈¥${entry.priceCny.toFixed(2)})`;
152
- const promo = entry.hasPromoCode ? ' 🎟️有优惠码' : '';
149
+ const promo = entry.hasPromoCode ? messages(locale, 'promo') : '';
153
150
  lines.push(`${index + 1}. ${entry.registrar}:${entry.price} ${entry.currency}${cny}${promo}`);
154
151
  });
155
152
  if (comparison.entries.length > limit) {
156
- lines.push(`…共 ${comparison.entries.length} 家`);
153
+ lines.push(messages(locale, 'registrars', { count: comparison.entries.length }));
157
154
  }
158
- const updated = comparison.updatedAt ? `,数据更新于 ${comparison.updatedAt}` : '';
159
- lines.push(`价格仅供参考${updated},以注册商官网为准`);
155
+ const updated = comparison.updatedAt ? messages(locale, 'priceUpdated', { date: comparison.updatedAt }) : '';
156
+ lines.push(messages(locale, 'priceNote', { updated }));
160
157
  return lines.join('\n');
161
158
  }
162
159
  function isRecord(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"domain-price.js","sourceRoot":"","sources":["../src/domain-price.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Domain registration / renewal / transfer price comparison through the\n * public nazhumi.com API (`GET /api/v1?domain=<suffix>&order=register`).\n * `register` is the only documented `order` value, so renew/transfer\n * comparisons read the matching `renew` / `transfer` (+ `_cny`) fields and are\n * sorted locally.\n *\n * There is no second keyless provider with a comparable dataset, so instead\n * of a provider fallback this module tolerates partial data: entries with a\n * missing or non-numeric price are skipped, and an empty list is reported as\n * \"no data\" rather than an error.\n */\nimport { parseHostInput } from './address-policy.js';\nimport { NettoolsError } from './errors.js';\nimport type { HttpClient } from './http.js';\n\nexport const NAZHUMI_API_URL = 'https://www.nazhumi.com/api/v1';\nconst PROVIDER = 'nazhumi.com';\n\nexport type PriceKind = 'register' | 'renew' | 'transfer';\n\nconst PRICE_KIND_ALIASES: Readonly<Record<string, PriceKind>> = {\n register: 'register',\n new: 'register',\n reg: 'register',\n 注册: 'register',\n 新购: 'register',\n renew: 'renew',\n 续费: 'renew',\n transfer: 'transfer',\n 转入: 'transfer',\n};\n\nconst PRICE_KIND_LABELS: Readonly<Record<PriceKind, string>> = {\n register: '注册',\n renew: '续费',\n transfer: '转入',\n};\n\nexport interface PriceEntry {\n readonly registrar: string;\n readonly website: string | undefined;\n readonly price: number;\n readonly currency: string;\n readonly priceCny: number | undefined;\n readonly hasPromoCode: boolean;\n}\n\nexport interface PriceComparison {\n readonly suffix: string;\n readonly kind: PriceKind;\n readonly entries: readonly PriceEntry[];\n readonly updatedAt: string | undefined;\n}\n\nexport function parsePriceKind(input: string | undefined): PriceKind {\n if (input === undefined || input.trim() === '') {\n return 'register';\n }\n const kind = PRICE_KIND_ALIASES[input.trim().toLowerCase()];\n if (!kind) {\n throw new NettoolsError('invalid_input', '价格类型可选:注册(register) / 续费(renew) / 转入(transfer)');\n }\n return kind;\n}\n\n/**\n * Normalise user input to a domain suffix.\n * - `com`, `.com`, `.co.uk`: a leading dot or a single label is taken verbatim;\n * - `example.com`, `www.example.co.uk`: the first label is dropped\n * (`com`, `example.co.uk` → use `.co.uk` explicitly for multi-level suffixes).\n */\nexport function parseDomainSuffix(input: string): string {\n const trimmed = input.trim();\n const explicitSuffix = trimmed.startsWith('.');\n const withoutDot = explicitSuffix ? trimmed.slice(1) : trimmed;\n if (withoutDot.length === 0) {\n throw new NettoolsError('invalid_input', '请提供域名后缀,例如 com 或 .cn');\n }\n const parsed = parseHostInput(withoutDot, { minLabels: 1 });\n if (parsed.kind !== 'domain') {\n throw new NettoolsError('invalid_input', '请提供域名或后缀,而不是 IP 地址');\n }\n const labels = parsed.name.split('.');\n if (explicitSuffix || labels.length === 1) {\n return parsed.name;\n }\n return labels.slice(1).join('.');\n}\n\nexport async function comparePrices(\n http: HttpClient,\n suffix: string,\n kind: PriceKind,\n signal?: AbortSignal,\n): Promise<PriceComparison> {\n const url = new URL(NAZHUMI_API_URL);\n url.searchParams.set('domain', suffix);\n url.searchParams.set('order', 'register');\n const response = await http.fetchJson(url, {\n provider: PROVIDER,\n ...(signal ? { signal } : {}),\n });\n return parsePriceResponse(response.body, suffix, kind);\n}\n\nexport function parsePriceResponse(body: unknown, suffix: string, kind: PriceKind): PriceComparison {\n if (!isRecord(body)) {\n throw new NettoolsError('malformed', 'nazhumi.com 返回了无法识别的数据');\n }\n if (typeof body.code === 'number' && body.code !== 100 && body.code !== 200 && body.code !== 0) {\n const message = typeof body.msg === 'string' ? body.msg : typeof body.message === 'string' ? body.message : '';\n throw new NettoolsError('http', `nazhumi.com 返回错误 ${body.code}${message ? `:${message}` : ''}`);\n }\n const data = isRecord(body.data) ? body.data : {};\n const rawPrices = Array.isArray(data.price) ? data.price : [];\n const entries: PriceEntry[] = [];\n let updatedAt: string | undefined;\n for (const raw of rawPrices) {\n const entry = parsePriceEntry(raw, kind);\n if (entry) {\n entries.push(entry);\n }\n if (isRecord(raw) && typeof raw.updatedtime === 'string' && (!updatedAt || raw.updatedtime > updatedAt)) {\n updatedAt = raw.updatedtime;\n }\n }\n entries.sort(compareByCnyPrice);\n return Object.freeze({ suffix, kind, entries: Object.freeze(entries), updatedAt });\n}\n\nfunction parsePriceEntry(raw: unknown, kind: PriceKind): PriceEntry | undefined {\n if (!isRecord(raw)) {\n return undefined;\n }\n const price = raw[kind];\n if (typeof price !== 'number' || !Number.isFinite(price) || price <= 0) {\n return undefined;\n }\n const registrar = typeof raw.registrarname === 'string' && raw.registrarname.trim()\n ? raw.registrarname.trim()\n : typeof raw.registrar === 'string' ? raw.registrar : '';\n if (!registrar) {\n return undefined;\n }\n const cny = raw[`${kind}_cny`];\n const promo = isRecord(raw.promocode) ? raw.promocode[kind] === true : false;\n const website = typeof raw.registrarweb === 'string' && raw.registrarweb.startsWith('https://')\n ? raw.registrarweb\n : undefined;\n const currency = typeof raw.currencyname === 'string' && raw.currencyname\n ? raw.currencyname\n : typeof raw.currency === 'string' ? raw.currency.toUpperCase() : '';\n return Object.freeze({\n registrar,\n website,\n price,\n currency,\n priceCny: typeof cny === 'number' && Number.isFinite(cny) ? cny : undefined,\n hasPromoCode: promo,\n });\n}\n\n/** Cheapest first by CNY equivalent; entries without CNY go last, keeping API order. */\nfunction compareByCnyPrice(left: PriceEntry, right: PriceEntry): number {\n if (left.priceCny === undefined && right.priceCny === undefined) {\n return 0;\n }\n if (left.priceCny === undefined) {\n return 1;\n }\n if (right.priceCny === undefined) {\n return -1;\n }\n return left.priceCny - right.priceCny;\n}\n\nexport function formatPriceComparison(comparison: PriceComparison, limit: number): string {\n const label = PRICE_KIND_LABELS[comparison.kind];\n if (comparison.entries.length === 0) {\n return `💰 .${comparison.suffix} 暂无${label}价格数据`;\n }\n const lines = [`💰 .${comparison.suffix} ${label}价格(低→高,nazhumi.com)`];\n comparison.entries.slice(0, limit).forEach((entry, index) => {\n const cny = entry.priceCny === undefined ? '' : `(≈¥${entry.priceCny.toFixed(2)})`;\n const promo = entry.hasPromoCode ? ' 🎟️有优惠码' : '';\n lines.push(`${index + 1}. ${entry.registrar}:${entry.price} ${entry.currency}${cny}${promo}`);\n });\n if (comparison.entries.length > limit) {\n lines.push(`…共 ${comparison.entries.length} 家`);\n }\n const updated = comparison.updatedAt ? `,数据更新于 ${comparison.updatedAt}` : '';\n lines.push(`价格仅供参考${updated},以注册商官网为准`);\n return lines.join('\\n');\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
1
+ {"version":3,"file":"domain-price.js","sourceRoot":"","sources":["../src/domain-price.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Locale } from 'zhin.js/i18n';\nimport { messages } from './presentation.js';\nimport { message } from './messages.js';\n/**\n * Domain registration / renewal / transfer price comparison through the\n * public nazhumi.com API (`GET /api/v1?domain=<suffix>&order=register`).\n * `register` is the only documented `order` value, so renew/transfer\n * comparisons read the matching `renew` / `transfer` (+ `_cny`) fields and are\n * sorted locally.\n *\n * There is no second keyless provider with a comparable dataset, so instead\n * of a provider fallback this module tolerates partial data: entries with a\n * missing or non-numeric price are skipped, and an empty list is reported as\n * \"no data\" rather than an error.\n */\nimport { parseHostInput } from './address-policy.js';\nimport { NettoolsError } from './errors.js';\nimport type { HttpClient } from './http.js';\n\nexport const NAZHUMI_API_URL = 'https://www.nazhumi.com/api/v1';\nconst PROVIDER = 'nazhumi.com';\n\nexport type PriceKind = 'register' | 'renew' | 'transfer';\n\nconst PRICE_KIND_ALIASES: Readonly<Record<string, PriceKind>> = {\n register: 'register',\n new: 'register',\n reg: 'register',\n 注册: 'register',\n 新购: 'register',\n renew: 'renew',\n 续费: 'renew',\n transfer: 'transfer',\n 转入: 'transfer',\n};\n\n\n\nexport interface PriceEntry {\n readonly registrar: string;\n readonly website: string | undefined;\n readonly price: number;\n readonly currency: string;\n readonly priceCny: number | undefined;\n readonly hasPromoCode: boolean;\n}\n\nexport interface PriceComparison {\n readonly suffix: string;\n readonly kind: PriceKind;\n readonly entries: readonly PriceEntry[];\n readonly updatedAt: string | undefined;\n}\n\nexport function parsePriceKind(input: string | undefined): PriceKind {\n if (input === undefined || input.trim() === '') {\n return 'register';\n }\n const kind = PRICE_KIND_ALIASES[input.trim().toLowerCase()];\n if (!kind) {\n throw new NettoolsError('invalid_input', message('priceKind'));\n }\n return kind;\n}\n\n/**\n * Normalise user input to a domain suffix.\n * - `com`, `.com`, `.co.uk`: a leading dot or a single label is taken verbatim;\n * - `example.com`, `www.example.co.uk`: the first label is dropped\n * (`com`, `example.co.uk` → use `.co.uk` explicitly for multi-level suffixes).\n */\nexport function parseDomainSuffix(input: string): string {\n const trimmed = input.trim();\n const explicitSuffix = trimmed.startsWith('.');\n const withoutDot = explicitSuffix ? trimmed.slice(1) : trimmed;\n if (withoutDot.length === 0) {\n throw new NettoolsError('invalid_input', message('suffixRequired'));\n }\n const parsed = parseHostInput(withoutDot, { minLabels: 1 });\n if (parsed.kind !== 'domain') {\n throw new NettoolsError('invalid_input', message('suffixNotIp'));\n }\n const labels = parsed.name.split('.');\n if (explicitSuffix || labels.length === 1) {\n return parsed.name;\n }\n return labels.slice(1).join('.');\n}\n\nexport async function comparePrices(\n http: HttpClient,\n suffix: string,\n kind: PriceKind,\n signal?: AbortSignal,\n): Promise<PriceComparison> {\n const url = new URL(NAZHUMI_API_URL);\n url.searchParams.set('domain', suffix);\n url.searchParams.set('order', 'register');\n const response = await http.fetchJson(url, {\n provider: PROVIDER,\n ...(signal ? { signal } : {}),\n });\n return parsePriceResponse(response.body, suffix, kind);\n}\n\nexport function parsePriceResponse(body: unknown, suffix: string, kind: PriceKind): PriceComparison {\n if (!isRecord(body)) {\n throw new NettoolsError('malformed', message('priceMalformed'));\n }\n if (typeof body.code === 'number' && body.code !== 100 && body.code !== 200 && body.code !== 0) {\n const providerMessage = typeof body.msg === 'string' ? body.msg : typeof body.message === 'string' ? body.message : '';\n throw new NettoolsError('http', message('priceError', { code: body.code, detail: providerMessage ? `: ${providerMessage}` : '' }));\n }\n const data = isRecord(body.data) ? body.data : {};\n const rawPrices = Array.isArray(data.price) ? data.price : [];\n const entries: PriceEntry[] = [];\n let updatedAt: string | undefined;\n for (const raw of rawPrices) {\n const entry = parsePriceEntry(raw, kind);\n if (entry) {\n entries.push(entry);\n }\n if (isRecord(raw) && typeof raw.updatedtime === 'string' && (!updatedAt || raw.updatedtime > updatedAt)) {\n updatedAt = raw.updatedtime;\n }\n }\n entries.sort(compareByCnyPrice);\n return Object.freeze({ suffix, kind, entries: Object.freeze(entries), updatedAt });\n}\n\nfunction parsePriceEntry(raw: unknown, kind: PriceKind): PriceEntry | undefined {\n if (!isRecord(raw)) {\n return undefined;\n }\n const price = raw[kind];\n if (typeof price !== 'number' || !Number.isFinite(price) || price <= 0) {\n return undefined;\n }\n const registrar = typeof raw.registrarname === 'string' && raw.registrarname.trim()\n ? raw.registrarname.trim()\n : typeof raw.registrar === 'string' ? raw.registrar : '';\n if (!registrar) {\n return undefined;\n }\n const cny = raw[`${kind}_cny`];\n const promo = isRecord(raw.promocode) ? raw.promocode[kind] === true : false;\n const website = typeof raw.registrarweb === 'string' && raw.registrarweb.startsWith('https://')\n ? raw.registrarweb\n : undefined;\n const currency = typeof raw.currencyname === 'string' && raw.currencyname\n ? raw.currencyname\n : typeof raw.currency === 'string' ? raw.currency.toUpperCase() : '';\n return Object.freeze({\n registrar,\n website,\n price,\n currency,\n priceCny: typeof cny === 'number' && Number.isFinite(cny) ? cny : undefined,\n hasPromoCode: promo,\n });\n}\n\n/** Cheapest first by CNY equivalent; entries without CNY go last, keeping API order. */\nfunction compareByCnyPrice(left: PriceEntry, right: PriceEntry): number {\n if (left.priceCny === undefined && right.priceCny === undefined) {\n return 0;\n }\n if (left.priceCny === undefined) {\n return 1;\n }\n if (right.priceCny === undefined) {\n return -1;\n }\n return left.priceCny - right.priceCny;\n}\n\nexport function formatPriceComparison(comparison: PriceComparison, limit: number, locale: Locale = 'zh-CN'): string {\n const label = messages(locale, comparison.kind);\n if (comparison.entries.length === 0) {\n return messages(locale, 'noPrice', { suffix: comparison.suffix, kind: label });\n }\n const lines = [messages(locale, 'priceHeader', { suffix: comparison.suffix, kind: label })];\n comparison.entries.slice(0, limit).forEach((entry, index) => {\n const cny = entry.priceCny === undefined ? '' : `(≈¥${entry.priceCny.toFixed(2)})`;\n const promo = entry.hasPromoCode ? messages(locale, 'promo') : '';\n lines.push(`${index + 1}. ${entry.registrar}:${entry.price} ${entry.currency}${cny}${promo}`);\n });\n if (comparison.entries.length > limit) {\n lines.push(messages(locale, 'registrars', { count: comparison.entries.length }));\n }\n const updated = comparison.updatedAt ? messages(locale, 'priceUpdated', { date: comparison.updatedAt }) : '';\n lines.push(messages(locale, 'priceNote', { updated }));\n return lines.join('\\n');\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
package/lib/errors.d.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { type Locale } from 'zhin.js/i18n';
2
+ import { type NettoolsMessage } from './messages.js';
1
3
  /**
2
4
  * Error categories surfaced by @zhin.js/plugin-nettools.
3
5
  *
@@ -8,11 +10,12 @@
8
10
  export type NettoolsErrorKind = 'invalid_input' | 'blocked_target' | 'cooldown' | 'timeout' | 'rate_limited' | 'http' | 'network' | 'malformed' | 'not_found' | 'aborted';
9
11
  export declare class NettoolsError extends Error {
10
12
  readonly kind: NettoolsErrorKind;
13
+ readonly detail?: NettoolsMessage;
11
14
  /** HTTP status when the failure came from a provider response. */
12
15
  readonly status?: number;
13
16
  /** Milliseconds the caller should wait before retrying (cooldown / 429). */
14
17
  readonly retryAfterMs?: number;
15
- constructor(kind: NettoolsErrorKind, message: string, options?: {
18
+ constructor(kind: NettoolsErrorKind, message: string | NettoolsMessage, options?: {
16
19
  status?: number;
17
20
  retryAfterMs?: number;
18
21
  cause?: unknown;
@@ -20,4 +23,4 @@ export declare class NettoolsError extends Error {
20
23
  }
21
24
  export declare function isNettoolsError(error: unknown): error is NettoolsError;
22
25
  /** Render any thrown value as a short user-facing sentence. */
23
- export declare function describeFailure(error: unknown): string;
26
+ export declare function describeFailure(error: unknown, locale?: Locale): string;
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 `出错了:${detail}`;
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":"AAmBA,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,IAAI,CAAoB;IACjC,kEAAkE;IACzD,MAAM,CAAU;IACzB,4EAA4E;IACnE,YAAY,CAAU;IAE/B,YACE,IAAuB,EACvB,OAAe,EACf,UAAuE,EAAE;QAEzE,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;QACnF,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,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;IAC5C,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,OAAO,MAAM,EAAE,CAAC;IACzB,CAAC;IACD,OAAO,KAAK,CAAC,OAAO,CAAC;AACvB,CAAC","sourcesContent":["/**\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 /** 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,\n options: { status?: number; retryAfterMs?: number; cause?: unknown } = {},\n ) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = 'NettoolsError';\n this.kind = kind;\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): string {\n if (!isNettoolsError(error)) {\n const detail = error instanceof Error ? error.message : String(error);\n return `出错了:${detail}`;\n }\n return error.message;\n}\n"]}
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', `${options.provider} 请求失败`);
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', `${options.provider} 返回了无法解析的数据`);
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', `${options.provider} 响应超时(${seconds}s)`, { cause: error });
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', `无法连接 ${options.provider}:${detail}`, { cause: error });
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', `${provider} 请求过于频繁,请稍后再试`, {
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', `${provider} 返回错误 HTTP ${status}`, { status });
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', `${provider} 请求已取消`);
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
  });