init: init proj

This commit is contained in:
2026-08-25 11:35:05 +08:00
commit bf7f2df67e
80 changed files with 9936 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
import type { ProfileIR } from '../ir.js';
/** 成员名含空格/逗号时加引号 */
function quoteName(name: string): string {
return /[\s,]/.test(name) ? `"${name}"` : name;
}
/**
* ShadowRocket 的 [General] 只保留它认识的通用键——模板里的 Surge 专有键
* all-hybrid / udp-priority / http-api / compatibility-mode 等)会被过滤掉。
*/
const SR_GENERAL_KEYS = new Set([
'bypass-system',
'skip-proxy',
'bypass-tun',
'dns-server',
'fallback-dns-server',
'ipv6',
'prefer-ipv6',
'dns-direct-system',
'icmp-auto-reply',
'always-real-ip',
'hijack-dns',
'udp-policy-not-supported-behaviour',
'interface-mode',
]);
function shadowrocketGeneral(surgeGeneral: string): string {
const lines: string[] = [];
for (const raw of surgeGeneral.split(/\r?\n/)) {
const line = raw.trim();
if (!line || line.startsWith('#') || line.startsWith('//')) continue;
const key = line.split('=')[0].trim();
if (SR_GENERAL_KEYS.has(key)) lines.push(line);
}
// ShadowRocket 的常用默认值,模板里没有就补上
if (!lines.some((l) => l.startsWith('bypass-system'))) lines.push('bypass-system = true');
if (!lines.some((l) => l.startsWith('ipv6'))) lines.push('ipv6 = false');
if (!lines.some((l) => l.startsWith('dns-server'))) lines.push('dns-server = 223.5.5.5, 119.29.29.29');
return lines.join('\n');
}
/** ShadowRocket 不认识的规则类型 */
const SR_UNSUPPORTED_RULE_TYPES = new Set(['DOMAIN-SET', 'AND', 'OR', 'NOT', 'URL-REGEX']);
export interface ShadowrocketConfOutput {
content: string;
skippedRules: string[];
}
/**
* 生成 ShadowRocket 配置文件:策略组 + 规则,不含节点。
* 节点由「节点订阅」单独导入,策略组通过 `use=true` 引用该订阅的名称,
* 因此用户在 App 里给订阅起的名字必须与 subscriptionName 一致。
*/
export function generateShadowrocketConf(
ir: ProfileIR,
opts: { subscriptionName: string }
): ShadowrocketConfOutput {
const sub = quoteName(opts.subscriptionName);
const lines: string[] = [];
lines.push('# 由 Proxy Station 生成的 ShadowRocket 配置(策略组 + 规则)');
lines.push(`# 节点请另行导入「节点订阅」,并在 App 中将其命名为:${opts.subscriptionName}`);
lines.push('');
lines.push('[General]');
lines.push(shadowrocketGeneral(ir.sections['General'] ?? ''));
lines.push('', '[Proxy Group]');
for (const g of ir.groups) {
const parts: string[] = [g.type];
if (g.filterRegex) {
// 地区组:引用订阅 + 正则筛选ShadowRocket 原生支持 policy-regex-filter
parts.push(sub, 'use=true', `policy-regex-filter=${g.filterRegex}`);
} else if (g.includeAllNodes) {
// 收纳全部节点:引用订阅
parts.push(sub, 'use=true');
} else {
const members = g.members.map((m) => quoteName(m.name));
parts.push(...(members.length ? members : ['DIRECT']));
}
if (g.type !== 'select') {
parts.push(`url=${g.testUrl || 'http://cp.cloudflare.com/generate_204'}`);
if (g.interval) parts.push(`interval=${g.interval}`);
if (g.tolerance) parts.push(`tolerance=${g.tolerance}`);
}
lines.push(`${g.name} = ${parts.join(', ')}`);
}
const skippedRules: string[] = [];
lines.push('', '[Rule]');
for (const r of ir.rules) {
if (r.type === 'FINAL') {
lines.push(['FINAL', quoteName(r.policy)].join(','));
continue;
}
if (SR_UNSUPPORTED_RULE_TYPES.has(r.type)) {
skippedRules.push(`${r.type},${r.ruleset ? r.ruleset.name : r.value},${r.policy}`);
continue;
}
const value = r.ruleset ? r.ruleset.surgeUrl : r.value;
lines.push([r.type, value, quoteName(r.policy), ...r.params].join(','));
}
for (const section of ['Host', 'URL Rewrite', 'MITM']) {
if (ir.sections[section]) lines.push('', `[${section}]`, ir.sections[section]);
}
return { content: lines.join('\n') + '\n', skippedRules };
}