@zhin.js/plugin-nettools 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +70 -2
  2. package/commands/dns/[name]/[[type]]/index.js +18 -0
  3. package/commands/dns/[name]/[[type]]/index.ts +18 -0
  4. package/commands/domain-price/[suffix]/[[order]]/index.js +19 -0
  5. package/commands/domain-price/[suffix]/[[order]]/index.ts +19 -0
  6. package/commands/ping/[host]/index.js +14 -0
  7. package/commands/ping/[host]/index.ts +14 -0
  8. package/commands/whois/[domain]/index.js +14 -0
  9. package/commands/whois/[domain]/index.ts +14 -0
  10. package/lib/address-policy.d.ts +44 -0
  11. package/lib/address-policy.js +388 -0
  12. package/lib/address-policy.js.map +1 -0
  13. package/lib/config.d.ts +44 -0
  14. package/lib/config.js +38 -0
  15. package/lib/config.js.map +1 -0
  16. package/lib/cooldown.d.ts +29 -0
  17. package/lib/cooldown.js +80 -0
  18. package/lib/cooldown.js.map +1 -0
  19. package/lib/dns.d.ts +52 -0
  20. package/lib/dns.js +191 -0
  21. package/lib/dns.js.map +1 -0
  22. package/lib/domain-price.d.ts +28 -0
  23. package/lib/domain-price.js +165 -0
  24. package/lib/domain-price.js.map +1 -0
  25. package/lib/errors.d.ts +23 -0
  26. package/lib/errors.js +30 -0
  27. package/lib/errors.js.map +1 -0
  28. package/lib/http.d.ts +50 -0
  29. package/lib/http.js +242 -0
  30. package/lib/http.js.map +1 -0
  31. package/lib/index.d.ts +12 -0
  32. package/lib/index.js +13 -0
  33. package/lib/index.js.map +1 -0
  34. package/lib/ping.d.ts +45 -0
  35. package/lib/ping.js +195 -0
  36. package/lib/ping.js.map +1 -0
  37. package/lib/runtime.d.ts +3 -0
  38. package/lib/runtime.js +4 -0
  39. package/lib/runtime.js.map +1 -0
  40. package/lib/service.d.ts +18 -0
  41. package/lib/service.js +96 -0
  42. package/lib/service.js.map +1 -0
  43. package/lib/target-guard.d.ts +24 -0
  44. package/lib/target-guard.js +59 -0
  45. package/lib/target-guard.js.map +1 -0
  46. package/lib/whois.d.ts +31 -0
  47. package/lib/whois.js +345 -0
  48. package/lib/whois.js.map +1 -0
  49. package/package.json +105 -3
  50. package/plugin.js +14 -0
  51. package/schema.json +62 -0
  52. package/skills/nettools/SKILL.md +44 -0
  53. package/skills/nettools/tools/dns_lookup/index.js +20 -0
  54. package/skills/nettools/tools/domain_price/index.js +19 -0
  55. package/skills/nettools/tools/net_ping/index.js +16 -0
  56. package/skills/nettools/tools/whois_lookup/index.js +16 -0
package/lib/whois.js ADDED
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Domain registration lookup over RDAP (RFC 9082/9083), the JSON successor
3
+ * of WHOIS.
4
+ *
5
+ * 1. The IANA bootstrap registry (`data.iana.org/rdap/dns.json`) maps each TLD
6
+ * to its registry's RDAP base URL. It is cached in memory (default 24 h).
7
+ * Only `https://` bases are used.
8
+ * 2. If the bootstrap is unavailable, lacks the TLD or the registry server
9
+ * fails, we fall back to the public redirector `rdap.org`.
10
+ * 3. Last resort: the 60s API WHOIS endpoint (`60s.viki.moe/v2/whois`), which
11
+ * also covers TLDs without RDAP such as `.cn`. Registrant personal data in
12
+ * that answer is ignored on purpose.
13
+ * 4. HTTP 404 from the TLD's own registry is authoritative ("not
14
+ * registered"). A 404 from rdap.org is not (the TLD may simply have no
15
+ * RDAP), so the WHOIS fallback still runs.
16
+ * 5. For names with more than two labels (e.g. `www.example.com`) that are
17
+ * not registered we retry once with the leftmost label removed.
18
+ */
19
+ import { NettoolsError } from './errors.js';
20
+ export const IANA_RDAP_BOOTSTRAP_URL = 'https://data.iana.org/rdap/dns.json';
21
+ export const RDAP_FALLBACK_BASE_URL = 'https://rdap.org/';
22
+ export const WHOIS_FALLBACK_URL = 'https://60s.viki.moe/v2/whois';
23
+ export const DEFAULT_BOOTSTRAP_TTL_MS = 24 * 60 * 60 * 1000;
24
+ const RDAP_ACCEPT = 'application/rdap+json, application/json;q=0.9';
25
+ export class RdapClient {
26
+ #http;
27
+ #bootstrapTtlMs;
28
+ #cache;
29
+ constructor(http, bootstrapTtlMs = DEFAULT_BOOTSTRAP_TTL_MS) {
30
+ this.#http = http;
31
+ this.#bootstrapTtlMs = bootstrapTtlMs;
32
+ }
33
+ async lookup(domain, signal) {
34
+ const first = await this.#lookupExact(domain, signal);
35
+ if (first.registered) {
36
+ return first;
37
+ }
38
+ const labels = domain.split('.');
39
+ if (labels.length <= 2) {
40
+ return first;
41
+ }
42
+ const parent = labels.slice(1).join('.');
43
+ const second = await this.#lookupExact(parent, signal);
44
+ return second.registered ? second : first;
45
+ }
46
+ async #lookupExact(domain, signal) {
47
+ const failures = [];
48
+ const registryBases = await this.#registryBases(domain, signal);
49
+ for (const base of registryBases) {
50
+ try {
51
+ return await this.#query(base, domain, signal);
52
+ }
53
+ catch (error) {
54
+ rethrowIfAborted(error);
55
+ failures.push(error);
56
+ }
57
+ }
58
+ let rdapOrgAnswer;
59
+ try {
60
+ rdapOrgAnswer = await this.#query(RDAP_FALLBACK_BASE_URL, domain, signal);
61
+ if (rdapOrgAnswer.registered) {
62
+ return rdapOrgAnswer;
63
+ }
64
+ }
65
+ catch (error) {
66
+ rethrowIfAborted(error);
67
+ failures.push(error);
68
+ }
69
+ try {
70
+ return await this.#queryWhoisFallback(domain, signal);
71
+ }
72
+ catch (error) {
73
+ rethrowIfAborted(error);
74
+ failures.push(error);
75
+ }
76
+ if (rdapOrgAnswer) {
77
+ return rdapOrgAnswer;
78
+ }
79
+ const lastError = failures[failures.length - 1];
80
+ if (lastError instanceof NettoolsError) {
81
+ throw lastError;
82
+ }
83
+ throw new NettoolsError('network', 'RDAP / WHOIS 查询失败', { cause: lastError });
84
+ }
85
+ async #queryWhoisFallback(domain, signal) {
86
+ const url = new URL(WHOIS_FALLBACK_URL);
87
+ url.searchParams.set('domain', domain);
88
+ const response = await this.#http.fetchJson(url, {
89
+ provider: 'WHOIS (60s.viki.moe)',
90
+ ...(signal ? { signal } : {}),
91
+ });
92
+ return parseWhoisFallback(response.body, domain, url.hostname);
93
+ }
94
+ async #query(base, domain, signal) {
95
+ const baseUrl = new URL(base.endsWith('/') ? base : `${base}/`);
96
+ const url = new URL(`domain/${encodeURIComponent(domain)}`, baseUrl);
97
+ const response = await this.#http.fetchJson(url, {
98
+ provider: `RDAP (${baseUrl.hostname})`,
99
+ accept: RDAP_ACCEPT,
100
+ passStatuses: [404],
101
+ ...(signal ? { signal } : {}),
102
+ });
103
+ if (response.status === 404) {
104
+ return unregistered(domain, baseUrl.hostname);
105
+ }
106
+ return parseRdapDomain(response.body, domain, baseUrl.hostname);
107
+ }
108
+ /** RDAP base URLs for the domain's TLD; empty when the bootstrap is unavailable. */
109
+ async #registryBases(domain, signal) {
110
+ const servers = await this.#bootstrap(signal);
111
+ if (!servers) {
112
+ return [];
113
+ }
114
+ const labels = domain.split('.');
115
+ for (let index = 0; index < labels.length; index += 1) {
116
+ const suffix = labels.slice(index).join('.');
117
+ const match = servers.get(suffix);
118
+ if (match) {
119
+ return match;
120
+ }
121
+ }
122
+ return [];
123
+ }
124
+ async #bootstrap(signal) {
125
+ const now = this.#http.dependencies.now();
126
+ if (this.#cache && now - this.#cache.fetchedAt < this.#bootstrapTtlMs) {
127
+ return this.#cache.servers;
128
+ }
129
+ try {
130
+ const response = await this.#http.fetchJson(new URL(IANA_RDAP_BOOTSTRAP_URL), {
131
+ provider: 'IANA RDAP bootstrap',
132
+ ...(signal ? { signal } : {}),
133
+ });
134
+ const servers = parseBootstrap(response.body);
135
+ this.#cache = { fetchedAt: now, servers };
136
+ return servers;
137
+ }
138
+ catch (error) {
139
+ if (error instanceof NettoolsError && error.kind === 'aborted') {
140
+ throw error;
141
+ }
142
+ // Stale data beats no data; otherwise fall back to rdap.org.
143
+ return this.#cache?.servers;
144
+ }
145
+ }
146
+ }
147
+ /** Parse the IANA bootstrap file into suffix → https base URLs. */
148
+ export function parseBootstrap(body) {
149
+ const servers = new Map();
150
+ if (!isRecord(body) || !Array.isArray(body.services)) {
151
+ throw new NettoolsError('malformed', 'IANA RDAP 引导数据格式异常');
152
+ }
153
+ for (const service of body.services) {
154
+ if (!Array.isArray(service) || !Array.isArray(service[0]) || !Array.isArray(service[1])) {
155
+ continue;
156
+ }
157
+ const urls = service[1].filter((value) => typeof value === 'string' && value.startsWith('https://'));
158
+ if (urls.length === 0) {
159
+ continue;
160
+ }
161
+ for (const suffix of service[0]) {
162
+ if (typeof suffix === 'string' && suffix.length > 0) {
163
+ servers.set(suffix.toLowerCase(), Object.freeze(urls));
164
+ }
165
+ }
166
+ }
167
+ return servers;
168
+ }
169
+ export function parseRdapDomain(body, domain, source) {
170
+ if (!isRecord(body)) {
171
+ throw new NettoolsError('malformed', 'RDAP 返回了无法识别的数据');
172
+ }
173
+ if (body.objectClassName !== undefined && body.objectClassName !== 'domain') {
174
+ throw new NettoolsError('malformed', 'RDAP 返回的不是域名对象');
175
+ }
176
+ const name = typeof body.ldhName === 'string' ? body.ldhName.toLowerCase() : domain;
177
+ const events = Array.isArray(body.events) ? body.events : [];
178
+ const eventDate = (action) => {
179
+ for (const event of events) {
180
+ if (isRecord(event) && event.eventAction === action && typeof event.eventDate === 'string') {
181
+ return event.eventDate;
182
+ }
183
+ }
184
+ return undefined;
185
+ };
186
+ const statuses = Array.isArray(body.status)
187
+ ? body.status.filter((value) => typeof value === 'string')
188
+ : [];
189
+ const nameservers = [];
190
+ if (Array.isArray(body.nameservers)) {
191
+ for (const nameserver of body.nameservers) {
192
+ if (isRecord(nameserver) && typeof nameserver.ldhName === 'string') {
193
+ nameservers.push(nameserver.ldhName.toLowerCase());
194
+ }
195
+ }
196
+ }
197
+ const secureDns = isRecord(body.secureDNS) ? body.secureDNS : undefined;
198
+ const dnssec = typeof secureDns?.delegationSigned === 'boolean' ? secureDns.delegationSigned : undefined;
199
+ return Object.freeze({
200
+ domain: name,
201
+ registered: true,
202
+ registrar: findRegistrarName(body.entities),
203
+ statuses: Object.freeze(statuses),
204
+ createdAt: eventDate('registration'),
205
+ expiresAt: eventDate('expiration'),
206
+ updatedAt: eventDate('last changed'),
207
+ nameservers: Object.freeze(nameservers),
208
+ dnssec,
209
+ source,
210
+ });
211
+ }
212
+ /**
213
+ * Parse the 60s API WHOIS answer. Only registry-level facts are kept;
214
+ * registrant name / organisation / e-mail are deliberately dropped.
215
+ */
216
+ export function parseWhoisFallback(body, domain, source) {
217
+ if (!isRecord(body) || !isRecord(body.data)) {
218
+ throw new NettoolsError('malformed', 'WHOIS 服务返回了无法识别的数据');
219
+ }
220
+ if (typeof body.code === 'number' && body.code !== 200 && body.code !== 0) {
221
+ throw new NettoolsError('http', `WHOIS 服务返回错误 ${body.code}`);
222
+ }
223
+ const data = body.data;
224
+ const text = (value) => typeof value === 'string' && value.trim() ? value.trim() : undefined;
225
+ const statuses = Array.isArray(data.status)
226
+ ? data.status.filter((value) => typeof value === 'string')
227
+ : [];
228
+ const nameservers = Array.isArray(data.nameservers)
229
+ ? data.nameservers.filter((value) => typeof value === 'string').map((value) => value.toLowerCase())
230
+ : [];
231
+ const registrar = text(data.registrar);
232
+ const createdAt = epochToIso(data.created_at) ?? text(data.created);
233
+ const expiresAt = epochToIso(data.expires_at) ?? text(data.expires);
234
+ const updatedAt = epochToIso(data.updated_at) ?? text(data.updated);
235
+ const registered = Boolean(registrar || createdAt || expiresAt || statuses.length > 0 || nameservers.length > 0);
236
+ if (!registered) {
237
+ return unregistered(domain, source);
238
+ }
239
+ const dnssecText = text(data.dnssec)?.toLowerCase();
240
+ const dnssec = dnssecText === undefined ? undefined : dnssecText !== 'unsigned' && dnssecText !== 'no';
241
+ return Object.freeze({
242
+ domain: (text(data.domain) ?? domain).toLowerCase(),
243
+ registered: true,
244
+ registrar,
245
+ statuses: Object.freeze(statuses),
246
+ createdAt,
247
+ expiresAt,
248
+ updatedAt,
249
+ nameservers: Object.freeze(nameservers),
250
+ dnssec,
251
+ source,
252
+ });
253
+ }
254
+ function epochToIso(value) {
255
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
256
+ return undefined;
257
+ }
258
+ return new Date(value).toISOString();
259
+ }
260
+ function rethrowIfAborted(error) {
261
+ if (error instanceof NettoolsError && error.kind === 'aborted') {
262
+ throw error;
263
+ }
264
+ }
265
+ function findRegistrarName(entities) {
266
+ if (!Array.isArray(entities)) {
267
+ return undefined;
268
+ }
269
+ for (const entity of entities) {
270
+ if (!isRecord(entity) || !Array.isArray(entity.roles) || !entity.roles.includes('registrar')) {
271
+ continue;
272
+ }
273
+ const fullName = readVcardFullName(entity.vcardArray);
274
+ if (fullName) {
275
+ return fullName;
276
+ }
277
+ if (typeof entity.handle === 'string') {
278
+ return entity.handle;
279
+ }
280
+ }
281
+ return undefined;
282
+ }
283
+ /** jCard: ["vcard", [["fn", {}, "text", "Name"], …]] */
284
+ function readVcardFullName(vcard) {
285
+ if (!Array.isArray(vcard) || !Array.isArray(vcard[1])) {
286
+ return undefined;
287
+ }
288
+ for (const property of vcard[1]) {
289
+ if (Array.isArray(property) && property[0] === 'fn' && typeof property[3] === 'string' && property[3].trim()) {
290
+ return property[3].trim();
291
+ }
292
+ }
293
+ return undefined;
294
+ }
295
+ function unregistered(domain, source) {
296
+ return Object.freeze({
297
+ domain,
298
+ registered: false,
299
+ registrar: undefined,
300
+ statuses: Object.freeze([]),
301
+ createdAt: undefined,
302
+ expiresAt: undefined,
303
+ updatedAt: undefined,
304
+ nameservers: Object.freeze([]),
305
+ dnssec: undefined,
306
+ source,
307
+ });
308
+ }
309
+ export function formatWhoisRecord(record) {
310
+ if (!record.registered) {
311
+ return `📇 ${record.domain}\n未查询到注册信息(可能未注册,或该后缀未提供 RDAP)`;
312
+ }
313
+ const lines = [`📇 ${record.domain} 注册信息(来源:${record.source})`];
314
+ if (record.registrar) {
315
+ lines.push(`注册商:${record.registrar}`);
316
+ }
317
+ if (record.createdAt) {
318
+ lines.push(`注册时间:${formatDate(record.createdAt)}`);
319
+ }
320
+ if (record.expiresAt) {
321
+ lines.push(`到期时间:${formatDate(record.expiresAt)}`);
322
+ }
323
+ if (record.updatedAt) {
324
+ lines.push(`最近更新:${formatDate(record.updatedAt)}`);
325
+ }
326
+ if (record.statuses.length > 0) {
327
+ lines.push(`状态:${record.statuses.join(', ')}`);
328
+ }
329
+ if (record.nameservers.length > 0) {
330
+ lines.push(`DNS 服务器:${record.nameservers.join(', ')}`);
331
+ }
332
+ if (record.dnssec !== undefined) {
333
+ lines.push(`DNSSEC:${record.dnssec ? '已启用' : '未启用'}`);
334
+ }
335
+ return lines.join('\n');
336
+ }
337
+ /** Keep the date part of an ISO timestamp (UTC); leave anything else untouched. */
338
+ function formatDate(value) {
339
+ const match = /^(\d{4}-\d{2}-\d{2})T/u.exec(value);
340
+ return match ? `${match[1]} (UTC)` : value;
341
+ }
342
+ function isRecord(value) {
343
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
344
+ }
345
+ //# sourceMappingURL=whois.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whois.js","sourceRoot":"","sources":["../src/whois.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Domain registration lookup over RDAP (RFC 9082/9083), the JSON successor\n * of WHOIS.\n *\n * 1. The IANA bootstrap registry (`data.iana.org/rdap/dns.json`) maps each TLD\n * to its registry's RDAP base URL. It is cached in memory (default 24 h).\n * Only `https://` bases are used.\n * 2. If the bootstrap is unavailable, lacks the TLD or the registry server\n * fails, we fall back to the public redirector `rdap.org`.\n * 3. Last resort: the 60s API WHOIS endpoint (`60s.viki.moe/v2/whois`), which\n * also covers TLDs without RDAP such as `.cn`. Registrant personal data in\n * that answer is ignored on purpose.\n * 4. HTTP 404 from the TLD's own registry is authoritative (\"not\n * registered\"). A 404 from rdap.org is not (the TLD may simply have no\n * RDAP), so the WHOIS fallback still runs.\n * 5. For names with more than two labels (e.g. `www.example.com`) that are\n * not registered we retry once with the leftmost label removed.\n */\nimport { NettoolsError } from './errors.js';\nimport type { HttpClient } from './http.js';\n\nexport const IANA_RDAP_BOOTSTRAP_URL = 'https://data.iana.org/rdap/dns.json';\nexport const RDAP_FALLBACK_BASE_URL = 'https://rdap.org/';\nexport const WHOIS_FALLBACK_URL = 'https://60s.viki.moe/v2/whois';\nexport const DEFAULT_BOOTSTRAP_TTL_MS = 24 * 60 * 60 * 1000;\nconst RDAP_ACCEPT = 'application/rdap+json, application/json;q=0.9';\n\nexport interface WhoisRecord {\n readonly domain: string;\n readonly registered: boolean;\n readonly registrar: string | undefined;\n readonly statuses: readonly string[];\n readonly createdAt: string | undefined;\n readonly expiresAt: string | undefined;\n readonly updatedAt: string | undefined;\n readonly nameservers: readonly string[];\n readonly dnssec: boolean | undefined;\n readonly source: string;\n}\n\ninterface BootstrapCache {\n readonly fetchedAt: number;\n readonly servers: ReadonlyMap<string, readonly string[]>;\n}\n\nexport class RdapClient {\n readonly #http: HttpClient;\n readonly #bootstrapTtlMs: number;\n #cache: BootstrapCache | undefined;\n\n constructor(http: HttpClient, bootstrapTtlMs = DEFAULT_BOOTSTRAP_TTL_MS) {\n this.#http = http;\n this.#bootstrapTtlMs = bootstrapTtlMs;\n }\n\n async lookup(domain: string, signal?: AbortSignal): Promise<WhoisRecord> {\n const first = await this.#lookupExact(domain, signal);\n if (first.registered) {\n return first;\n }\n const labels = domain.split('.');\n if (labels.length <= 2) {\n return first;\n }\n const parent = labels.slice(1).join('.');\n const second = await this.#lookupExact(parent, signal);\n return second.registered ? second : first;\n }\n\n async #lookupExact(domain: string, signal?: AbortSignal): Promise<WhoisRecord> {\n const failures: unknown[] = [];\n const registryBases = await this.#registryBases(domain, signal);\n for (const base of registryBases) {\n try {\n return await this.#query(base, domain, signal);\n } catch (error) {\n rethrowIfAborted(error);\n failures.push(error);\n }\n }\n\n let rdapOrgAnswer: WhoisRecord | undefined;\n try {\n rdapOrgAnswer = await this.#query(RDAP_FALLBACK_BASE_URL, domain, signal);\n if (rdapOrgAnswer.registered) {\n return rdapOrgAnswer;\n }\n } catch (error) {\n rethrowIfAborted(error);\n failures.push(error);\n }\n\n try {\n return await this.#queryWhoisFallback(domain, signal);\n } catch (error) {\n rethrowIfAborted(error);\n failures.push(error);\n }\n\n if (rdapOrgAnswer) {\n return rdapOrgAnswer;\n }\n const lastError = failures[failures.length - 1];\n if (lastError instanceof NettoolsError) {\n throw lastError;\n }\n throw new NettoolsError('network', 'RDAP / WHOIS 查询失败', { cause: lastError });\n }\n\n async #queryWhoisFallback(domain: string, signal?: AbortSignal): Promise<WhoisRecord> {\n const url = new URL(WHOIS_FALLBACK_URL);\n url.searchParams.set('domain', domain);\n const response = await this.#http.fetchJson(url, {\n provider: 'WHOIS (60s.viki.moe)',\n ...(signal ? { signal } : {}),\n });\n return parseWhoisFallback(response.body, domain, url.hostname);\n }\n\n async #query(base: string, domain: string, signal?: AbortSignal): Promise<WhoisRecord> {\n const baseUrl = new URL(base.endsWith('/') ? base : `${base}/`);\n const url = new URL(`domain/${encodeURIComponent(domain)}`, baseUrl);\n const response = await this.#http.fetchJson(url, {\n provider: `RDAP (${baseUrl.hostname})`,\n accept: RDAP_ACCEPT,\n passStatuses: [404],\n ...(signal ? { signal } : {}),\n });\n if (response.status === 404) {\n return unregistered(domain, baseUrl.hostname);\n }\n return parseRdapDomain(response.body, domain, baseUrl.hostname);\n }\n\n /** RDAP base URLs for the domain's TLD; empty when the bootstrap is unavailable. */\n async #registryBases(domain: string, signal?: AbortSignal): Promise<readonly string[]> {\n const servers = await this.#bootstrap(signal);\n if (!servers) {\n return [];\n }\n const labels = domain.split('.');\n for (let index = 0; index < labels.length; index += 1) {\n const suffix = labels.slice(index).join('.');\n const match = servers.get(suffix);\n if (match) {\n return match;\n }\n }\n return [];\n }\n\n async #bootstrap(signal?: AbortSignal): Promise<ReadonlyMap<string, readonly string[]> | undefined> {\n const now = this.#http.dependencies.now();\n if (this.#cache && now - this.#cache.fetchedAt < this.#bootstrapTtlMs) {\n return this.#cache.servers;\n }\n try {\n const response = await this.#http.fetchJson(new URL(IANA_RDAP_BOOTSTRAP_URL), {\n provider: 'IANA RDAP bootstrap',\n ...(signal ? { signal } : {}),\n });\n const servers = parseBootstrap(response.body);\n this.#cache = { fetchedAt: now, servers };\n return servers;\n } catch (error) {\n if (error instanceof NettoolsError && error.kind === 'aborted') {\n throw error;\n }\n // Stale data beats no data; otherwise fall back to rdap.org.\n return this.#cache?.servers;\n }\n }\n}\n\n/** Parse the IANA bootstrap file into suffix → https base URLs. */\nexport function parseBootstrap(body: unknown): ReadonlyMap<string, readonly string[]> {\n const servers = new Map<string, readonly string[]>();\n if (!isRecord(body) || !Array.isArray(body.services)) {\n throw new NettoolsError('malformed', 'IANA RDAP 引导数据格式异常');\n }\n for (const service of body.services) {\n if (!Array.isArray(service) || !Array.isArray(service[0]) || !Array.isArray(service[1])) {\n continue;\n }\n const urls = service[1].filter(\n (value: unknown): value is string => typeof value === 'string' && value.startsWith('https://'),\n );\n if (urls.length === 0) {\n continue;\n }\n for (const suffix of service[0]) {\n if (typeof suffix === 'string' && suffix.length > 0) {\n servers.set(suffix.toLowerCase(), Object.freeze(urls));\n }\n }\n }\n return servers;\n}\n\nexport function parseRdapDomain(body: unknown, domain: string, source: string): WhoisRecord {\n if (!isRecord(body)) {\n throw new NettoolsError('malformed', 'RDAP 返回了无法识别的数据');\n }\n if (body.objectClassName !== undefined && body.objectClassName !== 'domain') {\n throw new NettoolsError('malformed', 'RDAP 返回的不是域名对象');\n }\n const name = typeof body.ldhName === 'string' ? body.ldhName.toLowerCase() : domain;\n const events = Array.isArray(body.events) ? body.events : [];\n const eventDate = (action: string): string | undefined => {\n for (const event of events) {\n if (isRecord(event) && event.eventAction === action && typeof event.eventDate === 'string') {\n return event.eventDate;\n }\n }\n return undefined;\n };\n const statuses = Array.isArray(body.status)\n ? body.status.filter((value: unknown): value is string => typeof value === 'string')\n : [];\n const nameservers: string[] = [];\n if (Array.isArray(body.nameservers)) {\n for (const nameserver of body.nameservers) {\n if (isRecord(nameserver) && typeof nameserver.ldhName === 'string') {\n nameservers.push(nameserver.ldhName.toLowerCase());\n }\n }\n }\n const secureDns = isRecord(body.secureDNS) ? body.secureDNS : undefined;\n const dnssec = typeof secureDns?.delegationSigned === 'boolean' ? secureDns.delegationSigned : undefined;\n return Object.freeze({\n domain: name,\n registered: true,\n registrar: findRegistrarName(body.entities),\n statuses: Object.freeze(statuses),\n createdAt: eventDate('registration'),\n expiresAt: eventDate('expiration'),\n updatedAt: eventDate('last changed'),\n nameservers: Object.freeze(nameservers),\n dnssec,\n source,\n });\n}\n\n/**\n * Parse the 60s API WHOIS answer. Only registry-level facts are kept;\n * registrant name / organisation / e-mail are deliberately dropped.\n */\nexport function parseWhoisFallback(body: unknown, domain: string, source: string): WhoisRecord {\n if (!isRecord(body) || !isRecord(body.data)) {\n throw new NettoolsError('malformed', 'WHOIS 服务返回了无法识别的数据');\n }\n if (typeof body.code === 'number' && body.code !== 200 && body.code !== 0) {\n throw new NettoolsError('http', `WHOIS 服务返回错误 ${body.code}`);\n }\n const data = body.data;\n const text = (value: unknown): string | undefined =>\n typeof value === 'string' && value.trim() ? value.trim() : undefined;\n const statuses = Array.isArray(data.status)\n ? data.status.filter((value: unknown): value is string => typeof value === 'string')\n : [];\n const nameservers = Array.isArray(data.nameservers)\n ? data.nameservers.filter((value: unknown): value is string => typeof value === 'string').map((value) => value.toLowerCase())\n : [];\n const registrar = text(data.registrar);\n const createdAt = epochToIso(data.created_at) ?? text(data.created);\n const expiresAt = epochToIso(data.expires_at) ?? text(data.expires);\n const updatedAt = epochToIso(data.updated_at) ?? text(data.updated);\n const registered = Boolean(registrar || createdAt || expiresAt || statuses.length > 0 || nameservers.length > 0);\n if (!registered) {\n return unregistered(domain, source);\n }\n const dnssecText = text(data.dnssec)?.toLowerCase();\n const dnssec = dnssecText === undefined ? undefined : dnssecText !== 'unsigned' && dnssecText !== 'no';\n return Object.freeze({\n domain: (text(data.domain) ?? domain).toLowerCase(),\n registered: true,\n registrar,\n statuses: Object.freeze(statuses),\n createdAt,\n expiresAt,\n updatedAt,\n nameservers: Object.freeze(nameservers),\n dnssec,\n source,\n });\n}\n\nfunction epochToIso(value: unknown): string | undefined {\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {\n return undefined;\n }\n return new Date(value).toISOString();\n}\n\nfunction rethrowIfAborted(error: unknown): void {\n if (error instanceof NettoolsError && error.kind === 'aborted') {\n throw error;\n }\n}\n\nfunction findRegistrarName(entities: unknown): string | undefined {\n if (!Array.isArray(entities)) {\n return undefined;\n }\n for (const entity of entities) {\n if (!isRecord(entity) || !Array.isArray(entity.roles) || !entity.roles.includes('registrar')) {\n continue;\n }\n const fullName = readVcardFullName(entity.vcardArray);\n if (fullName) {\n return fullName;\n }\n if (typeof entity.handle === 'string') {\n return entity.handle;\n }\n }\n return undefined;\n}\n\n/** jCard: [\"vcard\", [[\"fn\", {}, \"text\", \"Name\"], …]] */\nfunction readVcardFullName(vcard: unknown): string | undefined {\n if (!Array.isArray(vcard) || !Array.isArray(vcard[1])) {\n return undefined;\n }\n for (const property of vcard[1]) {\n if (Array.isArray(property) && property[0] === 'fn' && typeof property[3] === 'string' && property[3].trim()) {\n return property[3].trim();\n }\n }\n return undefined;\n}\n\nfunction unregistered(domain: string, source: string): WhoisRecord {\n return Object.freeze({\n domain,\n registered: false,\n registrar: undefined,\n statuses: Object.freeze([]),\n createdAt: undefined,\n expiresAt: undefined,\n updatedAt: undefined,\n nameservers: Object.freeze([]),\n dnssec: undefined,\n source,\n });\n}\n\nexport function formatWhoisRecord(record: WhoisRecord): string {\n if (!record.registered) {\n return `📇 ${record.domain}\\n未查询到注册信息(可能未注册,或该后缀未提供 RDAP)`;\n }\n const lines = [`📇 ${record.domain} 注册信息(来源:${record.source})`];\n if (record.registrar) {\n lines.push(`注册商:${record.registrar}`);\n }\n if (record.createdAt) {\n lines.push(`注册时间:${formatDate(record.createdAt)}`);\n }\n if (record.expiresAt) {\n lines.push(`到期时间:${formatDate(record.expiresAt)}`);\n }\n if (record.updatedAt) {\n lines.push(`最近更新:${formatDate(record.updatedAt)}`);\n }\n if (record.statuses.length > 0) {\n lines.push(`状态:${record.statuses.join(', ')}`);\n }\n if (record.nameservers.length > 0) {\n lines.push(`DNS 服务器:${record.nameservers.join(', ')}`);\n }\n if (record.dnssec !== undefined) {\n lines.push(`DNSSEC:${record.dnssec ? '已启用' : '未启用'}`);\n }\n return lines.join('\\n');\n}\n\n/** Keep the date part of an ISO timestamp (UTC); leave anything else untouched. */\nfunction formatDate(value: string): string {\n const match = /^(\\d{4}-\\d{2}-\\d{2})T/u.exec(value);\n return match ? `${match[1]} (UTC)` : value;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,108 @@
1
1
  {
2
2
  "name": "@zhin.js/plugin-nettools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Network tools plugin for Zhin.js — multi-region ping, RDAP whois, DNS-over-HTTPS lookup and domain price comparison (Plugin Runtime)",
5
+ "type": "module",
6
+ "main": "./lib/index.js",
7
+ "types": "./lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "import": "./lib/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "lib",
17
+ "plugin.js",
18
+ "schema.json",
19
+ "commands",
20
+ "!commands/**/*.ts",
21
+ "skills",
22
+ "!skills/**/*.ts",
23
+ "README.md",
24
+ "CHANGELOG.md"
25
+ ],
26
+ "keywords": [
27
+ "zhin",
28
+ "zhin.js",
29
+ "bot",
30
+ "plugin",
31
+ "ping",
32
+ "whois",
33
+ "rdap",
34
+ "dns",
35
+ "doh",
36
+ "domain"
37
+ ],
38
+ "author": {
39
+ "name": "lc-cn",
40
+ "email": "admin@liucl.cn",
41
+ "url": "https://github.com/lc-cn"
42
+ },
43
+ "license": "MIT",
44
+ "dependencies": {
45
+ "@zhin.js/skill": "1.1.7",
46
+ "@zhin.js/tool": "1.1.7"
47
+ },
48
+ "devDependencies": {
49
+ "typescript": "^6.0.3",
50
+ "vitest": "^4.1.10",
51
+ "zod": "^4.4.3",
52
+ "zhin.js": "1.1.11",
53
+ "@zhin.js/command": "1.1.8"
54
+ },
55
+ "peerDependencies": {
56
+ "zod": "^4.0.0",
57
+ "@zhin.js/command": "^1.1.8",
58
+ "zhin.js": "^1.1.11"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "@zhin.js/command": {
62
+ "optional": true
63
+ },
64
+ "zod": {
65
+ "optional": true
66
+ }
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/zhinjs/zhin.git",
71
+ "directory": "plugins/utils/nettools"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public",
75
+ "registry": "https://registry.npmjs.org"
76
+ },
77
+ "engines": {
78
+ "node": ">=22.19.0"
79
+ },
80
+ "zhin": {
81
+ "protocol": 1,
82
+ "type": "plugin",
83
+ "entry": "./plugin.js",
84
+ "engine": "^1.0.0",
85
+ "runtime": "trusted",
86
+ "features": [
87
+ {
88
+ "package": "@zhin.js/command",
89
+ "api": "^1.0.0"
90
+ },
91
+ {
92
+ "package": "@zhin.js/tool",
93
+ "api": "^1.0.0"
94
+ },
95
+ {
96
+ "package": "@zhin.js/skill",
97
+ "api": "^1.0.0"
98
+ }
99
+ ],
100
+ "plugins": []
101
+ },
102
+ "scripts": {
103
+ "build": "tsc",
104
+ "check": "tsc -p tsconfig.check.json",
105
+ "clean": "rimraf lib",
106
+ "test": "NODE_OPTIONS=--experimental-strip-types vitest run --root ../../.. plugins/utils/nettools/tests"
107
+ }
6
108
  }
package/plugin.js ADDED
@@ -0,0 +1,14 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { definePlugin } from 'zhin.js';
3
+ import { nettoolsServiceToken } from "./lib/runtime.js";
4
+ import { NettoolsService } from "./lib/service.js";
5
+ export default definePlugin({
6
+ name: 'nettools',
7
+ metadata: {
8
+ displayName: '网络工具',
9
+ },
10
+ setup(context) {
11
+ const service = new NettoolsService({ config: context.config.get() });
12
+ context.resources.provide(nettoolsServiceToken, service);
13
+ },
14
+ });
package/schema.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "cooldownSeconds": {
7
+ "type": "integer",
8
+ "minimum": 0,
9
+ "maximum": 3600,
10
+ "default": 10,
11
+ "description": "Per-user cooldown in seconds, shared by all nettools commands and tools (0 disables)"
12
+ },
13
+ "requestTimeoutMs": {
14
+ "type": "integer",
15
+ "minimum": 1000,
16
+ "maximum": 30000,
17
+ "default": 8000,
18
+ "description": "Per-attempt timeout for each external HTTP request, in milliseconds"
19
+ },
20
+ "maxRetries": {
21
+ "type": "integer",
22
+ "minimum": 0,
23
+ "maximum": 5,
24
+ "default": 2,
25
+ "description": "Retries for transient failures (network errors, timeouts, 429, 5xx) with exponential backoff and jitter"
26
+ },
27
+ "pingMaxNodes": {
28
+ "type": "integer",
29
+ "minimum": 1,
30
+ "maximum": 10,
31
+ "default": 4,
32
+ "description": "Number of check-host.net probe nodes used by ping"
33
+ },
34
+ "pingPollIntervalMs": {
35
+ "type": "integer",
36
+ "minimum": 500,
37
+ "maximum": 10000,
38
+ "default": 2000,
39
+ "description": "Wait between ping result polls, in milliseconds"
40
+ },
41
+ "pingMaxPolls": {
42
+ "type": "integer",
43
+ "minimum": 1,
44
+ "maximum": 10,
45
+ "default": 5,
46
+ "description": "Maximum number of ping result polls before reporting partial results"
47
+ },
48
+ "dohProviders": {
49
+ "type": "array",
50
+ "items": { "type": "string", "enum": ["cloudflare", "alidns", "google"] },
51
+ "default": ["cloudflare", "alidns", "google"],
52
+ "description": "DNS-over-HTTPS providers in fallback order"
53
+ },
54
+ "priceListLimit": {
55
+ "type": "integer",
56
+ "minimum": 1,
57
+ "maximum": 20,
58
+ "default": 8,
59
+ "description": "Number of registrars shown in a domain price comparison"
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,44 @@
1
+ ---
2
+ name: nettools
3
+ description: >-
4
+ 网络诊断与域名信息能力:多地 Ping、RDAP whois、DNS 查询、域名价格比价。
5
+ 用户问「某网站能不能访问 / 延迟多少」「这个域名什么时候到期」「查一下 MX/TXT 记录」
6
+ 「.com 哪家注册便宜」时使用。
7
+ keywords:
8
+ - ping
9
+ - whois
10
+ - dns
11
+ - 域名
12
+ - 解析
13
+ - 延迟
14
+ - 域名价格
15
+ tags:
16
+ - network
17
+ - utility
18
+ tools:
19
+ - net_ping
20
+ - whois_lookup
21
+ - dns_lookup
22
+ - domain_price
23
+ ---
24
+
25
+ # 网络工具技能
26
+
27
+ | 工具 | 用途 | 参数 |
28
+ |------|------|------|
29
+ | `net_ping` | 多地区 Ping(check-host.net) | `host`:公网域名或 IP |
30
+ | `whois_lookup` | 域名注册信息(RDAP) | `domain` |
31
+ | `dns_lookup` | DNS 查询(DoH) | `name`,`type`(默认 A) |
32
+ | `domain_price` | 域名价格比价(nazhumi.com) | `suffix`,`order`(register/renew/transfer) |
33
+
34
+ **Example:**
35
+ ```
36
+ 用户: github.com 的邮件服务器是哪些?
37
+ → dns_lookup(name="github.com", type="MX")
38
+ ```
39
+
40
+ ## 注意
41
+
42
+ - 内网、回环、链路本地、组播、云元数据地址以及解析到这些地址的域名会被拒绝,直接把拒绝原因告诉用户即可,不要换写法重试。
43
+ - 每个用户有冷却时间;遇到「操作太频繁」时请让用户稍后再试,不要连续重复调用。
44
+ - 价格数据来自第三方聚合,提醒用户以注册商官网为准。
@@ -0,0 +1,20 @@
1
+ // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
+ import { defineAgentTool } from '@zhin.js/tool';
3
+ import { z } from 'zod';
4
+ import { toolCallerKey } from "../../../../lib/cooldown.js";
5
+ import { SUPPORTED_DNS_TYPES } from "../../../../lib/dns.js";
6
+ import { nettoolsServiceToken } from "../../../../lib/runtime.js";
7
+ export default defineAgentTool({
8
+ description: `通过 DNS-over-HTTPS 查询 DNS 记录。type 可选 ${SUPPORTED_DNS_TYPES.join('/')},默认 A;PTR 时 name 可为 IP。`,
9
+ inputSchema: z.object({
10
+ name: z.string().min(1).max(255),
11
+ type: z.string().max(10).optional(),
12
+ }),
13
+ keywords: ['dns', '解析', 'nslookup', 'dig', 'mx', 'txt', 'cname'],
14
+ tags: ['网络', '域名'],
15
+ async execute({ name, type }, context) {
16
+ const service = context.use(nettoolsServiceToken);
17
+ const callerKey = toolCallerKey(context);
18
+ return service.run(() => service.dns(name, type, callerKey, context.signal));
19
+ },
20
+ });