@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.
- package/CHANGELOG.md +11 -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/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',
|
|
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);
|
package/lib/cooldown.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cooldown.js","sourceRoot":"","sources":["../src/cooldown.ts"],"names":[],"mappings":"AAAA;;;;;;;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,
|
|
1
|
+
{"version":3,"file":"cooldown.js","sourceRoot":"","sources":["../src/cooldown.ts"],"names":[],"mappings":"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","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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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 =
|
|
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
|
|
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
|
|
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(
|
|
180
|
+
lines.push(messages(locale, 'moreRecords', { count: matching.length - shown.length }));
|
|
179
181
|
}
|
|
180
182
|
if (matching.length === 0) {
|
|
181
|
-
lines.push(
|
|
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":"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","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"]}
|
package/lib/domain-price.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 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;
|
package/lib/domain-price.js
CHANGED
|
@@ -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', '
|
|
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', '
|
|
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', '
|
|
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', '
|
|
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
|
|
82
|
-
throw new NettoolsError('http',
|
|
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 =
|
|
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
|
|
144
|
+
return messages(locale, 'noPrice', { suffix: comparison.suffix, kind: label });
|
|
148
145
|
}
|
|
149
|
-
const lines = [
|
|
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(
|
|
153
|
+
lines.push(messages(locale, 'registrars', { count: comparison.entries.length }));
|
|
157
154
|
}
|
|
158
|
-
const updated = comparison.updatedAt ?
|
|
159
|
-
lines.push(
|
|
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) {
|
package/lib/domain-price.js.map
CHANGED
|
@@ -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
|
|
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
|
});
|