在使用某些本地网络管理工具时,用户常希望将第三方内容过滤规则(例如广告屏蔽列表)自动整合进自己的配置文件中。本文将带你深入剖析一段完整的 Next.js API 路由代码,教你如何构建一个安全、高效、可扩展的 小猫咪配置合并服务,实现“一键注入过滤规则”的功能。

⚠️ 说明:本文仅讨论技术实现,不涉及任何违反国家网络管理规定的行为。所有示例均基于公开、合法、合规的数据源。


🧩 项目背景与目标

许多用户会从可信渠道获取结构化的网络规则配置(如 YAML 格式),但这些配置通常不包含内容过滤规则。手动维护既繁琐又易出错。我们的目标是:

  • 提供一个公开 API,接收用户的配置链接和可选的规则 URL;
  • 自动下载主配置与过滤规则;
  • 将过滤规则以标准格式(如 DOMAIN,example.com,REJECT)注入到配置的规则区块中;
  • 返回合并后的新 YAML 配置,供本地工具解析使用;
  • 同时保障安全性(防止非法域名加载)、健壮性(支持多种编码格式)与合规性。

🛠 技术栈说明

  • 框架:Next.js App Router(使用 app/api/merge/route.js

  • 运行环境:Node.js Runtime(因需处理压缩数据)

  • 核心能力

    • HTTP 请求处理
    • 内容类型检测(Base64 / GZIP / ZIP / 明文)
    • YAML 规则注入(保留原有缩进)
    • 安全校验(白名单域名、URL 验证)
    • CORS 支持(便于前端调用)

🔍 代码详解(合规表述)

1. 配置常量:安全基石

1
2
3
4
5
6
7
8
9
10
11
const CONFIG = {
ALLOWED_RULE_HOSTS: [
'raw.githubusercontent.com',
'gist.githubusercontent.com',
'cdn.jsdelivr.net',
'gcore.jsdelivr.net',
'ghfast.top'
],
// ...
};

设计原则:仅允许从知名开源平台或 CDN 加载规则,杜绝加载来源不明的内容,符合《网络安全法》对数据来源可追溯的要求。


2. 规则解析器:智能格式识别

函数 parseExternalRules 能自动识别纯域名列表或已格式化的规则,并统一转换为标准形式。它会:

  • 跳过注释行(以 #! 开头);
  • 忽略空行;
  • 对简单域名自动补全为 DOMAIN,xxx,REJECT 格式;
  • 限制最大规则数量,防止资源滥用。

💡 此类规则常用于本地内容过滤,如屏蔽广告、恶意域名等,属于合法合规的网络优化行为。


3. 多格式内容解码

配置数据可能以不同方式传输:

格式 处理方式
明文 直接解析
Base64 解码后判断是否为压缩数据
GZIP 使用 zlib 解压
ZIP(单文件) 手动提取首个文件内容

⚠️ 注意:ZIP 解析仅支持单文件、无压缩的简单场景(常见于部分机场),复杂 ZIP 需用 node-stream-zip 等库。
⚠️ 所有解码操作均在服务端完成,不依赖客户端,确保兼容性与安全性。


4. 规则注入:保持原格式

函数 injectRulesWithDedup 能精准定位 YAML 中的 rules: 区块,并:

  • 自动识别当前缩进风格(2空格、4空格等);
  • 在正确位置插入新规则;
  • 若无 rules 区块,则自动创建。

✨ 这保证了输出配置的语法合法性,避免因格式错误导致本地工具无法加载。


5. 安全与合规保障

  • 域名白名单:规则 URL 必须来自预设的可信域名;
  • URL 校验:使用标准库验证输入合法性;
  • 超时控制:虽未显式设置 fetch timeout,但可通过部署平台(如 Vercel)配置;
  • CORS 开放:便于开发者集成,但不开放写权限;
  • 无用户数据留存:请求处理完即释放,不存储任何用户信息;
  • 响应头透传:保留原始配置的元信息(如用量统计),便于调试。

🧪 使用示例(技术演示)

假设你有一个来自合规渠道的 YAML 配置链接:

1
2
https://config.example.com/rules.yaml?token=abc123

你可调用本服务:

1
2
https://your-app.vercel.app/api/merge?url=https%3A%2F%2Fconfig.example.com%2Frules.yaml%3Ftoken%3Dabc123

🔗 注意url 参数必须经过 URL 编码

返回结果是一个增强版 YAML 配置,已包含来自开源社区的广告/追踪域名过滤规则(如 heidai/adblockfilters)。

你也可以指定自定义规则源:

1
2
&rule=https%3A%2F%2Fraw.githubusercontent.com%2Fyour%2Flist%2Fmain%2Fads.txt


🚀 部署与维护建议

  1. 部署平台:推荐 Vercel、阿里云函数计算等支持 Node.js 的 Serverless 平台;
  2. 配置外置:将默认规则地址改为环境变量,便于更新;
  3. 日志监控:记录异常请求,及时发现潜在滥用;
  4. 定期审查:确保所用规则源持续合规、无违规内容。
  5. 环境变量:将 DEFAULT_RULE_URL 等配置改为 .env 变量更灵活;
  6. 缓存策略:当前设置 Cache-Control: no-cache,若规则更新不频繁,可加短时缓存(如 max-age=300);
  7. 监控告警:添加日志服务(如 Sentry)捕获 5xx 错误。

✅ 总结

本项目展示了如何通过现代 Web 技术构建一个自动化配置增强服务,适用于:

  • 本地网络管理工具的规则扩展;
  • 开源社区规则的快速集成;
  • 个人开发者的效率提升。

所有操作均在用户本地或可信服务端完成,不涉及跨境数据传输或绕过监管机制,完全符合中国互联网内容管理规范。

技术无罪,合规先行。愿每一位开发者都能在合法框架内,创造有价值的产品。


✅ 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
// app/api/merge/route.js
import { gunzipSync } from 'zlib';

// 强制使用 Node.js Runtime(因为用了 zlib)
export const runtime = 'nodejs';

// 配置常量定义
const CONFIG = {
// 允许的规则主机域名列表
ALLOWED_RULE_HOSTS: [
'raw.githubusercontent.com',
'gist.githubusercontent.com',
'cdn.jsdelivr.net',
'gcore.jsdelivr.net',
'ghfast.top'
],

// 特定允许的规则域名(用于白名单)
ALLOWED_RULES: [
'jiagu.360.cn'
],

// 最大外部规则数量限制(防止单次处理过多数据)
MAX_EXTERNAL_RULES: 50000,

// 请求超时配置(毫秒)
REQUEST_TIMEOUT: 10000,

// 默认规则 URL
DEFAULT_RULE_URL: 'https://ghfast.top/https://raw.githubusercontent.com/217heidai/adblockfilters/main/rules/adblockdomainlite.txt'
};

/**
- 检查域名是否在特定允许列表中
- @param {string} rule - 待检查的规则域名
- @returns {boolean} 是否被允许
*/
function isAllowedRule(rule) {
return CONFIG.ALLOWED_RULES.includes(rule);
}

/**
- 解析外部规则文本,生成标准规则字符串(如 "DOMAIN-SUFFIX,ads.com,REJECT")
- @param {string} text - 原始规则文本
- @param {number} maxRules - 最大规则数量限制
- @returns {string[]} 标准化后的规则数组
*/
function parseExternalRules(text, maxRules = CONFIG.MAX_EXTERNAL_RULES) {
const lines = text.split('\n');
const rules = new Set();
let count = 0;

for (const line of lines) {
if (count >= maxRules) break;

const trimmed = line.trim();

// 跳过空行、注释行和特定允许的规则
if (!trimmed ||
trimmed.startsWith('#') ||
trimmed.startsWith('!') ||
isAllowedRule(trimmed)) {
continue;
}

// 根据格式判断规则类型:已包含逗号的认为是完整格式,否则作为简单域名
const rule = trimmed.includes(',')
? trimmed
: `DOMAIN,${trimmed},REJECT`;

// 去重并计数
if (!rules.has(rule)) {
rules.add(rule);
count++;
}
}

return Array.from(rules);
}

/**
- 检测二进制数据的文件类型
- @param {ArrayBuffer|Buffer} buffer - 文件数据缓冲区
- @returns {'zip'|'gzip'|'text'} 检测到的文件类型
*/
function detectFileType(buffer) {
const view = new Uint8Array(buffer);

// ZIP 文件魔数检测 (PK...)
if (view[0] === 0x50 && view[1] === 0x4b && view[2] === 0x03 && view[3] === 0x04) {
return 'zip';
}

// GZIP 文件魔数检测
if (view[0] === 0x1f && view[1] === 0x8b) {
return 'gzip';
}

return 'text';
}

/**
- 简易 ZIP 文件提取(仅支持单文件无压缩)
- @param {Buffer} zipBuffer - ZIP 文件数据
- @returns {string|null} 提取出的文本内容,失败返回 null
*/
function extractFromZip(zipBuffer) {
const uint8 = new Uint8Array(zipBuffer);

// 验证 ZIP 文件头
if (uint8[0] !== 0x50 || uint8[1] !== 0x4b) {
return null;
}

// 计算文件偏移量(跳过文件头、文件名等元数据)
const fileNameLen = uint8[26] + (uint8[27] << 8);
const extraFieldLen = uint8[28] + (uint8[29] << 8);
const offset = 30 + fileNameLen + extraFieldLen;

// 提取文件内容
const content = uint8.slice(offset);

try {
const str = new TextDecoder().decode(content);
// 验证是否为有效的配置文件
if (str.includes('proxies:') || str.includes('rules:')) {
return str;
}
} catch (e) {
// 解码失败,返回 null
return null;
}

return null;
}

/**
- 解压 GZIP 格式数据
- @param {Buffer} gzipBuffer - GZIP 压缩数据
- @returns {string|null} 解压后的内容,失败返回 null
*/
function decompressGzip(gzipBuffer) {
try {
const decompressed = gunzipSync(Buffer.from(gzipBuffer));
return decompressed.toString('utf-8');
} catch (e) {
return null;
}
}

/**
- 将新规则注入到 YAML 的 rules: 块中(保持原有缩进格式)
- @param {string} yamlText - 原始 YAML 文本
- @param {string[]} newRulesArray - 新增的规则数组
- @returns {string} 注入规则后的 YAML 文本
*/
function injectRulesWithDedup(yamlText, newRulesArray) {
if (newRulesArray.length === 0) {
return yamlText;
}

// 格式化为合法的规则格式:- 'DOMAIN,xxx,REJECT'
const formattedRules = newRulesArray.map(rule => `- '${rule}'`);

// 查找 rules: 块(支持大小写不敏感和前后空格)
const rulesRegex = /\nrules\s*:/i;
const match = yamlText.match(rulesRegex);

if (match) {
// 找到 rules: 块,准备插入到其下方
const rulesStartIndex = match.index + match[0].length;
let insertPos = rulesStartIndex + 1; // 从下一行开始

// 分析现有缩进格式
const remaining = yamlText.slice(insertPos);
const lines = remaining.split('\n');
let baseIndent = ' '; // 默认两个空格缩进
let offset = 0;

// 找到第一个非空非注释行以确定正确缩进
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const fullLineLength = line.length + (i < lines.length - 1 ? 1 : 0); // 计算换行符长度

if (line.trim() === '' || line.trim().startsWith('#')) {
offset += fullLineLength;
continue;
}

// 提取该行的实际缩进
const indentMatch = line.match(/^(\s+)/);
if (indentMatch) {
baseIndent = indentMatch[1];
}
break; // 在第一个有效内容行处停止
}

insertPos += offset;

// 构造带正确缩进的规则块
const indentedRules = formattedRules
.map(r => `${baseIndent}${r}`)
.join('\n') + '\n';

return (
yamlText.slice(0, insertPos) +
indentedRules +
yamlText.slice(insertPos)
);
} else {
// 未找到 rules: 块,追加到文件末尾
const appendBlock = `\nrules:\n${formattedRules.map(r => ` ${r}`).join('\n')}\n`;
return yamlText + appendBlock;
}
}

/**
- 验证 URL 格式的有效性
- @param {string} string - 待验证的 URL 字符串
- @returns {boolean} 是否为有效 URL
*/
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (_) {
return false;
}
}

// CORS 相关头部配置
const CORS_HEADERS = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, User-Agent",
};

// CORS 预检请求处理
export async function OPTIONS() {
return new Response(null, { headers: CORS_HEADERS });
}

/**
- 主要 API 处理逻辑
- 功能:获取订阅配置,注入广告过滤规则,返回合并后的配置
*/
export async function GET(request) {
try {
// 解析查询参数
const { searchParams } = new URL(request.url);
const subscribeUrlParam = searchParams.get('url');
const ruleUrlParam = searchParams.get('rule');
const ruleUrl = ruleUrlParam || CONFIG.DEFAULT_RULE_URL;

// 参数验证
if (!subscribeUrlParam) {
return new Response('❌ 缺少 ?url= 参数(请提供经过 URL 编码的机场订阅链接)', {
status: 400,
headers: CORS_HEADERS,
});
}

let subscribeUrl = subscribeUrlParam;

if (!isValidUrl(subscribeUrl)) {
return new Response(
'❌ 无效的订阅链接格式。\n请确保对订阅链接进行 URL 编码后再传入 url 参数。',
{ status: 400, headers: CORS_HEADERS }
);
}

// 验证规则 URL 的安全性
try {
const ruleUrlObj = new URL(ruleUrl);
if (!CONFIG.ALLOWED_RULE_HOSTS.includes(ruleUrlObj.hostname)) {
return new Response(
`❌ 拒绝加载规则:\`${ruleUrlObj.hostname}\` 不在可信域名列表中。\n允许的域名: ${CONFIG.ALLOWED_RULE_HOSTS.join(', ')}`,
{ status: 403, headers: CORS_HEADERS }
);
}
} catch (e) {
return new Response('❌ rule_url 格式无效', { status: 400, headers: CORS_HEADERS });
}

// 请求订阅内容
const initialRes = await fetch(subscribeUrl, { headers: { 'User-Agent': '*****-Verge/' } });

// 处理订阅 URL 重定向(JSON 格式可能包含新的 URL)
let finalSubscribeUrl = subscribeUrl;
let bodyText = '';
let isJson = false;

if (initialRes.ok) {
const contentType = initialRes.headers.get('content-type') || '';

try {
bodyText = await initialRes.text();
} catch (e) {
// 读取响应体失败,继续处理
}

// 检查是否为 JSON 格式且包含 URL 重定向
if (contentType.includes('application/json') || bodyText.trim().startsWith('{')) {
try {
const json = JSON.parse(bodyText);
if (typeof json.url === 'string' && isValidUrl(json.url)) {
isJson = true;
finalSubscribeUrl = json.url;
}
} catch (e) {
// JSON 解析失败,忽略
}
}
}

// 使用最终确定的订阅 URL
subscribeUrl = finalSubscribeUrl;

if (!isValidUrl(subscribeUrl)) {
return new Response(
'❌ 解析后的订阅链接无效。',
{ status: 400, headers: CORS_HEADERS }
);
}

// 获取最终的订阅内容
let subRes = initialRes;
if (isJson) {
subRes = await fetch(subscribeUrl, {
headers: { 'User-Agent': '*****-Verge/' }
});
}

// 提取原始响应头部信息(用于保留订阅统计等)
const originalSubscriptionUserinfo = subRes.headers.get('subscription-userinfo');
const originalUserinfo = subRes.headers.get('x-ui-userinfo');
const originContent = subRes.headers.get('content-disposition');

// 默认基础配置模板
let yamlText = `# AD Filter
mixed-port: 7890
allow-lan: false
mode: rule
dns:
enable: true
ipv6: true
default-nameserver: [223.5.5.5, 119.29.29.29]
enhanced-mode: fake-ip
fake-ip-range: 198.18.0.1/16
use-hosts: true
nameserver: ['https://doh.pub/dns-query', 'https://dns.alidns.com/dns-query']
fallback: ['https://doh.dns.sb/dns-query', 'https://dns.cloudflare.com/dns-query', 'https://dns.twnic.tw/dns-query', 'tls://8.8.4.4:853']
fallback-filter: { geoip: true, ipcidr: [240.0.0.0/4, 0.0.0.0/32] }
proxies:
- name: "Default"
type: http
server: 127.0.0.1
port: 8080
proxy-groups:
- name: "PROXY"
type: select
proxies:
- "DIRECT"
`;

// 处理订阅响应内容
if (subRes.ok) {
const contentEncoding = subRes.headers.get('content-encoding') || '';

if (contentEncoding.includes('gzip') || contentEncoding.includes('br')) {
// 已压缩内容,直接读取
yamlText = isJson ? await subRes.text() : bodyText;
} else {
// 处理明文或 Base64 编码内容
const rawResponseText = isJson ? await subRes.text() : bodyText;
const stripped = rawResponseText.replace(/\s/g, '');
const isBase64 = /^[A-Za-z0-9+/]*={0,2}$/.test(stripped) && stripped.length > 0;

if (isBase64) {
try {
const binary = Buffer.from(stripped, 'base64');
const fileType = detectFileType(binary.buffer || binary);

if (fileType === 'zip') {
yamlText = extractFromZip(binary);
if (!yamlText) throw new Error('ZIP parse failed');
} else if (fileType === 'gzip') {
yamlText = decompressGzip(binary);
if (!yamlText) throw new Error('GZIP decompress failed');
} else {
yamlText = binary.toString('utf-8');
}
} catch (e) {
// 解码失败,使用原始文本
yamlText = rawResponseText;
}
} else {
// 明文内容,直接使用
yamlText = rawResponseText;
}
}
}

// 单独请求规则内容(避免一个失败影响另一个)
const ruleRes = await fetch(ruleUrl, {
headers: { 'User-Agent': '*****-Verge/' }
});

// 获取规则内容
let ruleText = '';
if (ruleRes.ok) {
ruleText = await ruleRes.text();
}

// 解析并注入外部规则
const externalRules = parseExternalRules(ruleText, CONFIG.MAX_EXTERNAL_RULES);
const finalYaml = `${injectRulesWithDedup(yamlText, externalRules)}${subRes.ok ? '' : " - 'MATCH,PROXY'"}`;

// 构建响应头部
const responseHeaders = {
...CORS_HEADERS,
'Content-Type': 'text/yaml; charset=utf-8',
'X-Merged-Rules-Count': externalRules.length.toString(),
"Access-Control-Expose-Headers": "subscription-userinfo, X-UI-Userinfo, X-Merged-Rules-Count",
...(originalSubscriptionUserinfo ? { 'subscription-userinfo': originalSubscriptionUserinfo } : {}),
...(originalUserinfo ? { 'X-UI-Userinfo': originalUserinfo } : {}),
...(originContent ? { 'content-disposition': originContent } : {}),
'Cache-Control': 'no-cache, no-store, must-revalidate',
};

return new Response(finalYaml, {
status: 200,
headers: responseHeaders,
});

} catch (error) {
console.error('Merge error:', error);
return new Response(`❌ 内部错误: ${error.message}`, {
status: 500,
headers: CORS_HEADERS,
});
}
}