@zhin.js/plugin-nettools 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/commands/dns/[name]/[[type]]/index.js +5 -5
  3. package/commands/dns/[name]/[[type]]/index.ts +5 -5
  4. package/commands/domain-price/[suffix]/[[order]]/index.js +5 -5
  5. package/commands/domain-price/[suffix]/[[order]]/index.ts +5 -5
  6. package/commands/ping/[host]/index.js +4 -4
  7. package/commands/ping/[host]/index.ts +4 -4
  8. package/commands/whois/[domain]/index.js +4 -4
  9. package/commands/whois/[domain]/index.ts +4 -4
  10. package/lib/address-policy.d.ts +2 -0
  11. package/lib/address-policy.js +55 -54
  12. package/lib/address-policy.js.map +1 -1
  13. package/lib/cooldown.js +2 -1
  14. package/lib/cooldown.js.map +1 -1
  15. package/lib/dns.d.ts +2 -1
  16. package/lib/dns.js +14 -12
  17. package/lib/dns.js.map +1 -1
  18. package/lib/domain-price.d.ts +2 -1
  19. package/lib/domain-price.js +16 -19
  20. package/lib/domain-price.js.map +1 -1
  21. package/lib/errors.d.ts +5 -2
  22. package/lib/errors.js +8 -4
  23. package/lib/errors.js.map +1 -1
  24. package/lib/http.js +12 -11
  25. package/lib/http.js.map +1 -1
  26. package/lib/messages.d.ts +329 -0
  27. package/lib/messages.js +335 -0
  28. package/lib/messages.js.map +1 -0
  29. package/lib/ping.d.ts +2 -1
  30. package/lib/ping.js +13 -11
  31. package/lib/ping.js.map +1 -1
  32. package/lib/presentation.d.ts +1 -0
  33. package/lib/presentation.js +36 -0
  34. package/lib/presentation.js.map +1 -0
  35. package/lib/service.d.ts +6 -5
  36. package/lib/service.js +16 -15
  37. package/lib/service.js.map +1 -1
  38. package/lib/target-guard.js +6 -4
  39. package/lib/target-guard.js.map +1 -1
  40. package/lib/whois.d.ts +2 -1
  41. package/lib/whois.js +18 -16
  42. package/lib/whois.js.map +1 -1
  43. package/package.json +7 -7
  44. package/skills/nettools/tools/dns_lookup/index.js +1 -1
  45. package/skills/nettools/tools/domain_price/index.js +1 -1
  46. package/skills/nettools/tools/net_ping/index.js +1 -1
  47. package/skills/nettools/tools/whois_lookup/index.js +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @zhin.js/plugin-nettools
2
2
 
3
+ ## 0.1.3
4
+
5
+ ### Patch Changes
6
+
7
+ - 53bc6a0: 官方工具插件与游戏支持中英文反馈、帮助和配置标签;提醒增加与中文对应的有限英文时间语法。聊天后台任务记录创建者当时语言,管理员任务记录项目语言,旧任务保持中文;多轮选择与游戏使用开始时语言,保留第三方内容和已生成报告。
8
+ - Updated dependencies [53bc6a0]
9
+ - Updated dependencies [7595136]
10
+ - Updated dependencies [17d1301]
11
+ - @zhin.js/tool@1.1.10
12
+ - @zhin.js/skill@1.1.10
13
+
14
+ ## 0.1.2
15
+
16
+ ### Patch Changes
17
+
18
+ - Updated dependencies [6da2bad]
19
+ - Updated dependencies [3781f1c]
20
+ - @zhin.js/tool@1.1.9
21
+ - @zhin.js/skill@1.1.9
22
+
3
23
  ## 0.1.1
4
24
 
5
25
  ### Patch Changes
@@ -3,16 +3,16 @@ import { defineCommand } from 'zhin.js/command';
3
3
  import { commandCallerKey } from "../../../../lib/cooldown.js";
4
4
  import { nettoolsServiceToken } from "../../../../lib/runtime.js";
5
5
  export default defineCommand({
6
- description: 'DNS 查询(DoH),类型可选 A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY',
6
+ description: 'DNS 查询(DoH),类型可选 A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY', descriptionI18n: { en: "DNS lookup over HTTPS; supports A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY" },
7
7
  params: {
8
- name: { type: 'string', description: '域名;PTR 查询时可填 IP' },
9
- type: { type: 'string', description: '记录类型,默认 A' },
8
+ name: { type: 'string', description: '域名;PTR 查询时可填 IP', descriptionI18n: { en: "Domain name, or an IP address for PTR queries" } },
9
+ type: { type: 'string', description: '记录类型,默认 A', descriptionI18n: { en: "Record type (default: A)" } },
10
10
  },
11
- async execute({ params, input, use, signal }) {
11
+ async execute({ params, input, use, signal, locale }) {
12
12
  const service = use(nettoolsServiceToken);
13
13
  const name = String(params.name ?? '');
14
14
  const type = params.type == null ? undefined : String(params.type);
15
15
  const callerKey = commandCallerKey(input);
16
- return service.run(() => service.dns(name, type, callerKey, signal));
16
+ return service.run(() => service.dns(name, type, callerKey, signal, locale), locale);
17
17
  },
18
18
  });
@@ -3,16 +3,16 @@ import { commandCallerKey } from '../../../../src/cooldown.js';
3
3
  import { nettoolsServiceToken } from '../../../../src/runtime.js';
4
4
 
5
5
  export default defineCommand({
6
- description: 'DNS 查询(DoH),类型可选 A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY',
6
+ description: 'DNS 查询(DoH),类型可选 A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY', descriptionI18n: { en: "DNS lookup over HTTPS; supports A/AAAA/CNAME/MX/TXT/NS/SOA/CAA/SRV/PTR/DS/DNSKEY" },
7
7
  params: {
8
- name: { type: 'string', description: '域名;PTR 查询时可填 IP' },
9
- type: { type: 'string', description: '记录类型,默认 A' },
8
+ name: { type: 'string', description: '域名;PTR 查询时可填 IP', descriptionI18n: { en: "Domain name, or an IP address for PTR queries" } },
9
+ type: { type: 'string', description: '记录类型,默认 A', descriptionI18n: { en: "Record type (default: A)" } },
10
10
  },
11
- async execute({ params, input, use, signal }) {
11
+ async execute({ params, input, use, signal, locale }) {
12
12
  const service = use(nettoolsServiceToken);
13
13
  const name = String(params.name ?? '');
14
14
  const type = params.type == null ? undefined : String(params.type);
15
15
  const callerKey = commandCallerKey(input);
16
- return service.run(() => service.dns(name, type, callerKey, signal));
16
+ return service.run(() => service.dns(name, type, callerKey, signal, locale), locale);
17
17
  },
18
18
  });
@@ -3,17 +3,17 @@ import { defineCommand } from 'zhin.js/command';
3
3
  import { commandCallerKey } from "../../../../lib/cooldown.js";
4
4
  import { nettoolsServiceToken } from "../../../../lib/runtime.js";
5
5
  export default defineCommand({
6
- description: '域名注册/续费/转入价格比价(nazhumi.com)',
6
+ description: '域名注册/续费/转入价格比价(nazhumi.com)', descriptionI18n: { en: "Compare domain registration, renewal and transfer prices (nazhumi.com)" },
7
7
  alias: ['域名比价'],
8
8
  params: {
9
- suffix: { type: 'string', description: '域名后缀(com、.cn、.co.uk)或完整域名' },
10
- order: { type: 'string', description: '注册(register) / 续费(renew) / 转入(transfer),默认注册' },
9
+ suffix: { type: 'string', description: '域名后缀(com、.cn、.co.uk)或完整域名', descriptionI18n: { en: "Domain suffix (com, .cn, .co.uk) or a full domain" } },
10
+ order: { type: 'string', description: '注册(register) / 续费(renew) / 转入(transfer),默认注册', descriptionI18n: { en: "register (default), renew, or transfer" } },
11
11
  },
12
- async execute({ params, input, use, signal }) {
12
+ async execute({ params, input, use, signal, locale }) {
13
13
  const service = use(nettoolsServiceToken);
14
14
  const suffix = String(params.suffix ?? '');
15
15
  const order = params.order == null ? undefined : String(params.order);
16
16
  const callerKey = commandCallerKey(input);
17
- return service.run(() => service.domainPrice(suffix, order, callerKey, signal));
17
+ return service.run(() => service.domainPrice(suffix, order, callerKey, signal, locale), locale);
18
18
  },
19
19
  });
@@ -3,17 +3,17 @@ import { commandCallerKey } from '../../../../src/cooldown.js';
3
3
  import { nettoolsServiceToken } from '../../../../src/runtime.js';
4
4
 
5
5
  export default defineCommand({
6
- description: '域名注册/续费/转入价格比价(nazhumi.com)',
6
+ description: '域名注册/续费/转入价格比价(nazhumi.com)', descriptionI18n: { en: "Compare domain registration, renewal and transfer prices (nazhumi.com)" },
7
7
  alias: ['域名比价'],
8
8
  params: {
9
- suffix: { type: 'string', description: '域名后缀(com、.cn、.co.uk)或完整域名' },
10
- order: { type: 'string', description: '注册(register) / 续费(renew) / 转入(transfer),默认注册' },
9
+ suffix: { type: 'string', description: '域名后缀(com、.cn、.co.uk)或完整域名', descriptionI18n: { en: "Domain suffix (com, .cn, .co.uk) or a full domain" } },
10
+ order: { type: 'string', description: '注册(register) / 续费(renew) / 转入(transfer),默认注册', descriptionI18n: { en: "register (default), renew, or transfer" } },
11
11
  },
12
- async execute({ params, input, use, signal }) {
12
+ async execute({ params, input, use, signal, locale }) {
13
13
  const service = use(nettoolsServiceToken);
14
14
  const suffix = String(params.suffix ?? '');
15
15
  const order = params.order == null ? undefined : String(params.order);
16
16
  const callerKey = commandCallerKey(input);
17
- return service.run(() => service.domainPrice(suffix, order, callerKey, signal));
17
+ return service.run(() => service.domainPrice(suffix, order, callerKey, signal, locale), locale);
18
18
  },
19
19
  });
@@ -3,12 +3,12 @@ import { defineCommand } from 'zhin.js/command';
3
3
  import { commandCallerKey } from "../../../lib/cooldown.js";
4
4
  import { nettoolsServiceToken } from "../../../lib/runtime.js";
5
5
  export default defineCommand({
6
- description: '从多个地区 Ping 一个公网主机(check-host.net)',
7
- params: { host: { type: 'string', description: '域名或公网 IP' } },
8
- async execute({ params, input, use, signal }) {
6
+ description: '从多个地区 Ping 一个公网主机(check-host.net)', descriptionI18n: { en: "Ping a public host from multiple regions (check-host.net)" },
7
+ params: { host: { type: 'string', description: '域名或公网 IP', descriptionI18n: { en: "Domain name or public IP address" } } },
8
+ async execute({ params, input, use, signal, locale }) {
9
9
  const service = use(nettoolsServiceToken);
10
10
  const host = String(params.host ?? '');
11
11
  const callerKey = commandCallerKey(input);
12
- return service.run(() => service.ping(host, callerKey, signal));
12
+ return service.run(() => service.ping(host, callerKey, signal, locale), locale);
13
13
  },
14
14
  });
@@ -3,12 +3,12 @@ import { commandCallerKey } from '../../../src/cooldown.js';
3
3
  import { nettoolsServiceToken } from '../../../src/runtime.js';
4
4
 
5
5
  export default defineCommand({
6
- description: '从多个地区 Ping 一个公网主机(check-host.net)',
7
- params: { host: { type: 'string', description: '域名或公网 IP' } },
8
- async execute({ params, input, use, signal }) {
6
+ description: '从多个地区 Ping 一个公网主机(check-host.net)', descriptionI18n: { en: "Ping a public host from multiple regions (check-host.net)" },
7
+ params: { host: { type: 'string', description: '域名或公网 IP', descriptionI18n: { en: "Domain name or public IP address" } } },
8
+ async execute({ params, input, use, signal, locale }) {
9
9
  const service = use(nettoolsServiceToken);
10
10
  const host = String(params.host ?? '');
11
11
  const callerKey = commandCallerKey(input);
12
- return service.run(() => service.ping(host, callerKey, signal));
12
+ return service.run(() => service.ping(host, callerKey, signal, locale), locale);
13
13
  },
14
14
  });
@@ -3,12 +3,12 @@ import { defineCommand } from 'zhin.js/command';
3
3
  import { commandCallerKey } from "../../../lib/cooldown.js";
4
4
  import { nettoolsServiceToken } from "../../../lib/runtime.js";
5
5
  export default defineCommand({
6
- description: '查询域名注册信息(RDAP / WHOIS)',
7
- params: { domain: { type: 'string', description: '要查询的域名' } },
8
- async execute({ params, input, use, signal }) {
6
+ description: '查询域名注册信息(RDAP / WHOIS)', descriptionI18n: { en: "Look up domain registration information using RDAP / WHOIS" },
7
+ params: { domain: { type: 'string', description: '要查询的域名', descriptionI18n: { en: "Domain to look up" } } },
8
+ async execute({ params, input, use, signal, locale }) {
9
9
  const service = use(nettoolsServiceToken);
10
10
  const domain = String(params.domain ?? '');
11
11
  const callerKey = commandCallerKey(input);
12
- return service.run(() => service.whois(domain, callerKey, signal));
12
+ return service.run(() => service.whois(domain, callerKey, signal, locale), locale);
13
13
  },
14
14
  });
@@ -3,12 +3,12 @@ import { commandCallerKey } from '../../../src/cooldown.js';
3
3
  import { nettoolsServiceToken } from '../../../src/runtime.js';
4
4
 
5
5
  export default defineCommand({
6
- description: '查询域名注册信息(RDAP / WHOIS)',
7
- params: { domain: { type: 'string', description: '要查询的域名' } },
8
- async execute({ params, input, use, signal }) {
6
+ description: '查询域名注册信息(RDAP / WHOIS)', descriptionI18n: { en: "Look up domain registration information using RDAP / WHOIS" },
7
+ params: { domain: { type: 'string', description: '要查询的域名', descriptionI18n: { en: "Domain to look up" } } },
8
+ async execute({ params, input, use, signal, locale }) {
9
9
  const service = use(nettoolsServiceToken);
10
10
  const domain = String(params.domain ?? '');
11
11
  const callerKey = commandCallerKey(input);
12
- return service.run(() => service.whois(domain, callerKey, signal));
12
+ return service.run(() => service.whois(domain, callerKey, signal, locale), locale);
13
13
  },
14
14
  });
@@ -1,3 +1,4 @@
1
+ import { type NettoolsMessage } from './messages.js';
1
2
  export type ParsedHost = {
2
3
  readonly kind: 'ipv4';
3
4
  readonly address: string;
@@ -15,6 +16,7 @@ export type AddressVerdict = {
15
16
  } | {
16
17
  readonly blocked: true;
17
18
  readonly reason: string;
19
+ readonly detail: NettoolsMessage;
18
20
  };
19
21
  export interface ParseHostOptions {
20
22
  /** Allow `_` in labels (needed for DNS names such as `_dmarc.example.com`). */
@@ -1,3 +1,4 @@
1
+ import { message, formatMessage } from './messages.js';
1
2
  /**
2
3
  * Target address policy (SSRF guard).
3
4
  *
@@ -26,10 +27,10 @@
26
27
  import { isIPv4, isIPv6 } from 'node:net';
27
28
  import { NettoolsError } from './errors.js';
28
29
  const ALLOWED = Object.freeze({ blocked: false });
29
- function blocked(reason) {
30
- return Object.freeze({ blocked: true, reason });
30
+ function blocked(detail) {
31
+ return Object.freeze({ blocked: true, reason: formatMessage(detail), detail });
31
32
  }
32
- const CLOUD_METADATA_REASON = '云服务元数据地址';
33
+ const CLOUD_METADATA_REASON = message('cloudMetadata');
33
34
  /** Exact IPv4 metadata endpoints, checked before the range table for a clearer message. */
34
35
  const CLOUD_METADATA_IPV4 = new Set([
35
36
  '169.254.169.254', // AWS / GCP / Azure / Oracle / OpenStack instance metadata
@@ -41,29 +42,29 @@ const CLOUD_METADATA_IPV4 = new Set([
41
42
  /** Exact IPv6 metadata endpoints. */
42
43
  const CLOUD_METADATA_IPV6 = ['fd00:ec2::254', 'fd00:ec2::23'];
43
44
  const BLOCKED_IPV4_RANGES = [
44
- ipv4Rule('0.0.0.0', 8, '未指定 / “本网络”地址'),
45
- ipv4Rule('10.0.0.0', 8, '私有地址 (RFC 1918)'),
46
- ipv4Rule('100.64.0.0', 10, '运营商级 NAT 地址 (CGNAT, RFC 6598)'),
47
- ipv4Rule('127.0.0.0', 8, '回环地址'),
48
- ipv4Rule('169.254.0.0', 16, '链路本地地址'),
49
- ipv4Rule('172.16.0.0', 12, '私有地址 (RFC 1918)'),
50
- ipv4Rule('192.0.0.0', 24, 'IETF 协议保留地址'),
51
- ipv4Rule('192.0.2.0', 24, '文档示例地址'),
52
- ipv4Rule('192.88.99.0', 24, '已废弃的 6to4 中继地址'),
53
- ipv4Rule('192.168.0.0', 16, '私有地址 (RFC 1918)'),
54
- ipv4Rule('198.18.0.0', 15, '基准测试保留地址'),
55
- ipv4Rule('198.51.100.0', 24, '文档示例地址'),
56
- ipv4Rule('203.0.113.0', 24, '文档示例地址'),
57
- ipv4Rule('224.0.0.0', 4, '组播地址'),
58
- ipv4Rule('240.0.0.0', 4, '保留地址(含广播地址)'),
45
+ ipv4Rule('0.0.0.0', 8, message('unspecifiedV4')),
46
+ ipv4Rule('10.0.0.0', 8, message('privateV4')),
47
+ ipv4Rule('100.64.0.0', 10, message('cgnat')),
48
+ ipv4Rule('127.0.0.0', 8, message('loopback')),
49
+ ipv4Rule('169.254.0.0', 16, message('linkLocal')),
50
+ ipv4Rule('172.16.0.0', 12, message('privateV4')),
51
+ ipv4Rule('192.0.0.0', 24, message('ietfReserved')),
52
+ ipv4Rule('192.0.2.0', 24, message('documentation')),
53
+ ipv4Rule('192.88.99.0', 24, message('relay6to4')),
54
+ ipv4Rule('192.168.0.0', 16, message('privateV4')),
55
+ ipv4Rule('198.18.0.0', 15, message('benchmark')),
56
+ ipv4Rule('198.51.100.0', 24, message('documentation')),
57
+ ipv4Rule('203.0.113.0', 24, message('documentation')),
58
+ ipv4Rule('224.0.0.0', 4, message('multicast')),
59
+ ipv4Rule('240.0.0.0', 4, message('reservedBroadcast')),
59
60
  ];
60
61
  /** Special IPv6 ranges inside 2000::/3 that are still refused. */
61
62
  const BLOCKED_GLOBAL_IPV6_RANGES = [
62
- ipv6Rule('2001::', 32, 'Teredo 隧道地址'),
63
- ipv6Rule('2001:2::', 48, '基准测试保留地址'),
64
- ipv6Rule('2001:10::', 28, 'ORCHID 保留地址'),
65
- ipv6Rule('2001:db8::', 32, '文档示例地址'),
66
- ipv6Rule('3fff::', 20, '文档示例地址'),
63
+ ipv6Rule('2001::', 32, message('teredo')),
64
+ ipv6Rule('2001:2::', 48, message('benchmark')),
65
+ ipv6Rule('2001:10::', 28, message('orchid')),
66
+ ipv6Rule('2001:db8::', 32, message('documentation')),
67
+ ipv6Rule('3fff::', 20, message('documentation')),
67
68
  ];
68
69
  const BLOCKED_HOSTNAMES = new Set([
69
70
  'localhost',
@@ -105,19 +106,19 @@ const MAX_LABEL_LENGTH = 63;
105
106
  export function parseHostInput(input, options = {}) {
106
107
  const trimmed = input.trim();
107
108
  if (trimmed.length === 0) {
108
- throw invalid('请提供主机名或 IP 地址');
109
+ throw invalid(message('hostRequired'));
109
110
  }
110
111
  if (trimmed.length > MAX_HOSTNAME_LENGTH + 2) {
111
- throw invalid('主机名过长');
112
+ throw invalid(message('hostTooLong'));
112
113
  }
113
114
  if (/[\s/\\@?#%]/u.test(trimmed)) {
114
- throw invalid('只接受裸主机名或 IP,不要带协议、端口、路径或特殊字符');
115
+ throw invalid(message('bareHost'));
115
116
  }
116
117
  const bracketed = trimmed.startsWith('[') && trimmed.endsWith(']');
117
118
  const ipv6Candidate = bracketed ? trimmed.slice(1, -1) : trimmed;
118
119
  if (bracketed || trimmed.includes(':')) {
119
120
  if (!isIPv6(ipv6Candidate)) {
120
- throw invalid('不是有效的 IPv6 地址(如需端口请去掉端口)');
121
+ throw invalid(message('ipv6NoPort'));
121
122
  }
122
123
  const canonical = canonicalizeWithUrlParser(`[${ipv6Candidate}]`);
123
124
  const address = canonical.slice(1, -1);
@@ -142,33 +143,33 @@ function canonicalizeWithUrlParser(host) {
142
143
  const result = probe.hostname;
143
144
  const unchanged = result === placeholder && host.toLowerCase() !== placeholder;
144
145
  if (unchanged || result.length === 0) {
145
- throw invalid('不是有效的主机名或 IP 地址');
146
+ throw invalid(message('invalidHost'));
146
147
  }
147
148
  return result;
148
149
  }
149
150
  function validateDomainSyntax(name, options) {
150
151
  if (name.length === 0 || name.length > MAX_HOSTNAME_LENGTH) {
151
- throw invalid('主机名长度不合法');
152
+ throw invalid(message('hostLength'));
152
153
  }
153
154
  const labels = name.split('.');
154
155
  if (labels.length < (options.minLabels ?? 2)) {
155
- throw invalid('请提供完整域名(例如 example.com)');
156
+ throw invalid(message('fullDomain'));
156
157
  }
157
158
  const labelPattern = options.allowUnderscore
158
159
  ? /^[a-z0-9_](?:[a-z0-9_-]*[a-z0-9_])?$/u
159
160
  : /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u;
160
161
  for (const label of labels) {
161
162
  if (label.length === 0 || label.length > MAX_LABEL_LENGTH || !labelPattern.test(label)) {
162
- throw invalid(`域名标签不合法:${label || '(空)'}`);
163
+ throw invalid(message('invalidLabel', { label: label || message('empty') }));
163
164
  }
164
165
  }
165
166
  const topLevel = labels[labels.length - 1] ?? '';
166
167
  if (/^\d+$/u.test(topLevel)) {
167
- throw invalid('顶级域不能是纯数字');
168
+ throw invalid(message('numericTld'));
168
169
  }
169
170
  }
170
- function invalid(message) {
171
- return new NettoolsError('invalid_input', message);
171
+ function invalid(detail) {
172
+ return new NettoolsError('invalid_input', detail);
172
173
  }
173
174
  /* ------------------------------------------------------------------ */
174
175
  /* Classification */
@@ -187,7 +188,7 @@ export function classifyParsedHost(host) {
187
188
  export function classifyAddress(address) {
188
189
  const parsed = parseHostInput(address);
189
190
  if (parsed.kind === 'domain') {
190
- throw invalid(`不是 IP 地址:${address}`);
191
+ throw invalid(message('notIp', { address }));
191
192
  }
192
193
  return classifyParsedHost(parsed);
193
194
  }
@@ -195,12 +196,12 @@ export function classifyHostname(name) {
195
196
  const normalized = name.toLowerCase().replace(/\.$/u, '');
196
197
  if (BLOCKED_HOSTNAMES.has(normalized)) {
197
198
  return normalized.startsWith('localhost')
198
- ? blocked('本机主机名')
199
+ ? blocked(message('localhost'))
199
200
  : blocked(CLOUD_METADATA_REASON);
200
201
  }
201
202
  for (const suffix of BLOCKED_SUFFIXES) {
202
203
  if (normalized === suffix || normalized.endsWith(`.${suffix}`)) {
203
- return blocked(`内网或保留域名后缀 .${suffix}`);
204
+ return blocked(message('reservedSuffix', { suffix }));
204
205
  }
205
206
  }
206
207
  return ALLOWED;
@@ -229,11 +230,11 @@ const MULTICAST_PREFIX = ipv6ToBytes('ff00::');
229
230
  const CLOUD_METADATA_IPV6_BYTES = CLOUD_METADATA_IPV6.map((address) => ipv6ToBytes(address));
230
231
  function classifyIpv6Bytes(bytes) {
231
232
  if (bytes.every((byte) => byte === 0)) {
232
- return blocked('未指定地址 (::)');
233
+ return blocked(message('unspecifiedV6'));
233
234
  }
234
235
  const isLoopback = bytes.slice(0, 15).every((byte) => byte === 0) && bytes[15] === 1;
235
236
  if (isLoopback) {
236
- return blocked('回环地址 (::1)');
237
+ return blocked(message('loopbackV6'));
237
238
  }
238
239
  for (const metadata of CLOUD_METADATA_IPV6_BYTES) {
239
240
  if (matchesPrefix(bytes, metadata, 128)) {
@@ -241,31 +242,31 @@ function classifyIpv6Bytes(bytes) {
241
242
  }
242
243
  }
243
244
  if (matchesPrefix(bytes, IPV4_MAPPED_PREFIX, 96)) {
244
- return classifyEmbeddedIpv4(bytes.slice(12, 16), 'IPv4 映射地址');
245
+ return classifyEmbeddedIpv4(bytes.slice(12, 16), message('mappedV4'));
245
246
  }
246
247
  if (matchesPrefix(bytes, NAT64_WELL_KNOWN_PREFIX, 96)) {
247
- return classifyEmbeddedIpv4(bytes.slice(12, 16), 'NAT64 地址');
248
+ return classifyEmbeddedIpv4(bytes.slice(12, 16), message('nat64'));
248
249
  }
249
250
  if (matchesPrefix(bytes, NAT64_LOCAL_PREFIX, 48)) {
250
- return blocked('本地 NAT64 地址');
251
+ return blocked(message('localNat64'));
251
252
  }
252
253
  if (matchesPrefix(bytes, SIX_TO_FOUR_PREFIX, 16)) {
253
- return classifyEmbeddedIpv4(bytes.slice(2, 6), '6to4 地址');
254
+ return classifyEmbeddedIpv4(bytes.slice(2, 6), message('sixToFour'));
254
255
  }
255
256
  if (matchesPrefix(bytes, UNIQUE_LOCAL_PREFIX, 7)) {
256
- return blocked('唯一本地地址 (ULA, fc00::/7)');
257
+ return blocked(message('ula'));
257
258
  }
258
259
  if (matchesPrefix(bytes, LINK_LOCAL_PREFIX, 10)) {
259
- return blocked('链路本地地址 (fe80::/10)');
260
+ return blocked(message('linkLocalV6'));
260
261
  }
261
262
  if (matchesPrefix(bytes, SITE_LOCAL_PREFIX, 10)) {
262
- return blocked('已废弃的站点本地地址 (fec0::/10)');
263
+ return blocked(message('siteLocalV6'));
263
264
  }
264
265
  if (matchesPrefix(bytes, MULTICAST_PREFIX, 8)) {
265
- return blocked('组播地址 (ff00::/8)');
266
+ return blocked(message('multicastV6'));
266
267
  }
267
268
  if (!matchesPrefix(bytes, GLOBAL_UNICAST_PREFIX, 3)) {
268
- return blocked('非全球单播 IPv6 地址');
269
+ return blocked(message('notGlobalV6'));
269
270
  }
270
271
  for (const rule of BLOCKED_GLOBAL_IPV6_RANGES) {
271
272
  if (matchesPrefix(bytes, rule.prefix, rule.bits)) {
@@ -280,7 +281,7 @@ function classifyEmbeddedIpv4(embedded, wrapper) {
280
281
  return verdict;
281
282
  }
282
283
  const dotted = Array.from(embedded).join('.');
283
- return blocked(`${wrapper}指向 ${dotted}(${verdict.reason})`);
284
+ return blocked(message('embeddedV4', { wrapper, address: dotted, reason: verdict.detail }));
284
285
  }
285
286
  /* ------------------------------------------------------------------ */
286
287
  /* Byte helpers */
@@ -307,7 +308,7 @@ export function ipv4ToBytes(address) {
307
308
  const parts = address.split('.').map((part) => Number(part));
308
309
  const valid = parts.length === 4 && parts.every((part) => Number.isInteger(part) && part >= 0 && part <= 255);
309
310
  if (!valid) {
310
- throw invalid(`不是有效的 IPv4 地址:${address}`);
311
+ throw invalid(message('invalidV4', { address }));
311
312
  }
312
313
  return Uint8Array.from(parts);
313
314
  }
@@ -326,13 +327,13 @@ export function ipv6ToBytes(address) {
326
327
  }
327
328
  const halves = text.split('::');
328
329
  if (halves.length > 2) {
329
- throw invalid(`不是有效的 IPv6 地址:${address}`);
330
+ throw invalid(message('invalidV6', { address }));
330
331
  }
331
332
  const head = parseGroups(halves[0] ?? '');
332
333
  const rest = halves.length === 2 ? parseGroups(halves[1] ?? '') : [];
333
334
  const missing = 8 - head.length - rest.length;
334
335
  if (halves.length === 2 ? missing < 1 : missing !== 0) {
335
- throw invalid(`不是有效的 IPv6 地址:${address}`);
336
+ throw invalid(message('invalidV6', { address }));
336
337
  }
337
338
  groups.push(...head);
338
339
  for (let index = 0; index < missing && halves.length === 2; index += 1) {
@@ -355,7 +356,7 @@ function parseGroups(text) {
355
356
  }
356
357
  return text.split(':').map((group) => {
357
358
  if (!/^[0-9a-f]{1,4}$/u.test(group)) {
358
- throw invalid(`IPv6 分组不合法:${group}`);
359
+ throw invalid(message('invalidV6Group', { group }));
359
360
  }
360
361
  return Number.parseInt(group, 16);
361
362
  });
@@ -379,7 +380,7 @@ export function reverseLookupName(host) {
379
380
  }
380
381
  return `${nibbles.reverse().join('.')}.ip6.arpa`;
381
382
  }
382
- throw invalid('反向解析需要 IP 地址');
383
+ throw invalid(message('reverseNeedsIp'));
383
384
  }
384
385
  /** Human label for a parsed host. */
385
386
  export function displayHost(host) {