fix: 完善移动端订阅生成并同步分流配置文档
This commit is contained in:
@@ -1,15 +1,21 @@
|
||||
import YAML from 'yaml';
|
||||
import { clientSupports, normalizeSsCipher, type ExcludedNode, type NodeDto } from '@proxy-station/shared';
|
||||
import type { ProfileIR, RuleIR } from '../ir.js';
|
||||
import { clientSupports, normalizeSsCipher, splitRuleExpression, type ExcludedNode, type NodeDto } from '@proxy-station/shared';
|
||||
import type { ProfileIR } from '../ir.js';
|
||||
|
||||
export function clashProxy(n: NodeDto): Record<string, unknown> {
|
||||
const base: Record<string, unknown> = { name: n.name, server: n.server, port: n.port };
|
||||
const base: Record<string, unknown> = {
|
||||
name: n.name, server: n.server, port: n.port,
|
||||
// SIP003 的 WS 插件只转发 TCP,不能把 UDP 发往 nginx 的同端口。
|
||||
udp: !(n.protocol === 'shadowsocks' && n.network === 'ws'),
|
||||
};
|
||||
switch (n.protocol) {
|
||||
case 'vmess': {
|
||||
Object.assign(base, { type: 'vmess', uuid: n.uuid, alterId: n.alterId, cipher: n.security || 'auto' });
|
||||
const cipher = n.security === 'chacha20-ietf-poly1305' ? 'chacha20-poly1305' : n.security || 'auto';
|
||||
Object.assign(base, { type: 'vmess', uuid: n.uuid, alterId: n.alterId, cipher });
|
||||
if (n.tls) {
|
||||
base.tls = true;
|
||||
if (n.sni) base.servername = n.sni;
|
||||
if (n.skipCertVerify) base['skip-cert-verify'] = true;
|
||||
}
|
||||
if (n.network === 'ws') {
|
||||
base.network = 'ws';
|
||||
@@ -42,6 +48,7 @@ export function clashProxy(n: NodeDto): Record<string, unknown> {
|
||||
tls: n.tls,
|
||||
host: n.wsHost || n.sni || n.server,
|
||||
path: n.wsPath,
|
||||
...(n.skipCertVerify ? { 'skip-cert-verify': true } : {}),
|
||||
};
|
||||
}
|
||||
break;
|
||||
@@ -68,9 +75,39 @@ function mapPolicy(policy: string): string {
|
||||
return policy;
|
||||
}
|
||||
|
||||
/** Surge 逻辑规则里的子类型 → mihomo 命名 */
|
||||
function mapLogicValue(value: string): string {
|
||||
return value.replace(/\bPROTOCOL\b/g, 'NETWORK');
|
||||
/** 逐个转换逻辑子规则;任何不可表达的子条件使整条规则跳过,避免扩大匹配范围。 */
|
||||
function mapLogicValue(value: string): string | null {
|
||||
value = value.trim();
|
||||
if (!value.startsWith('(') || !value.endsWith(')')) return null;
|
||||
const children = splitRuleExpression(value.slice(1, -1));
|
||||
if (!children?.length) return null;
|
||||
const mapped: string[] = [];
|
||||
for (const child of children) {
|
||||
if (!child.startsWith('(') || !child.endsWith(')')) return null;
|
||||
const parts = splitRuleExpression(child.slice(1, -1));
|
||||
if (!parts || parts.length < 2) return null;
|
||||
let [type, payload] = parts;
|
||||
if (['AND', 'OR', 'NOT'].includes(type)) {
|
||||
const nested = mapLogicValue(payload);
|
||||
if (!nested) return null;
|
||||
payload = nested;
|
||||
} else if (type === 'PROTOCOL') {
|
||||
if (payload !== 'TCP' && payload !== 'UDP') return null;
|
||||
type = 'NETWORK';
|
||||
} else {
|
||||
const types: Record<string, string> = {
|
||||
DOMAIN: 'DOMAIN', 'DOMAIN-SUFFIX': 'DOMAIN-SUFFIX', 'DOMAIN-KEYWORD': 'DOMAIN-KEYWORD',
|
||||
'DOMAIN-WILDCARD': 'DOMAIN-WILDCARD', 'IP-CIDR': 'IP-CIDR', 'IP-CIDR6': 'IP-CIDR6',
|
||||
'IP-ASN': 'IP-ASN', GEOIP: 'GEOIP', 'PROCESS-NAME': 'PROCESS-NAME',
|
||||
'SRC-IP': 'SRC-IP-CIDR', 'DEST-PORT': 'DST-PORT', 'SRC-PORT': 'SRC-PORT',
|
||||
};
|
||||
if (!types[type] || /["'\\]/.test(payload)) return null;
|
||||
type = types[type];
|
||||
}
|
||||
if (parts.slice(2).some((p) => p !== 'no-resolve')) return null;
|
||||
mapped.push(`(${[type, payload, ...parts.slice(2)].join(',')})`);
|
||||
}
|
||||
return `(${mapped.join(',')})`;
|
||||
}
|
||||
|
||||
const CLASH_UNSUPPORTED_RULE_TYPES = new Set(['URL-REGEX', 'USER-AGENT']);
|
||||
@@ -100,17 +137,17 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }):
|
||||
if (g.filterRegex) {
|
||||
group['include-all-proxies'] = true;
|
||||
group.filter = g.filterRegex;
|
||||
if (members.length) group.proxies = [...new Set(members)];
|
||||
} else if (g.includeAllNodes) {
|
||||
group['include-all-proxies'] = true;
|
||||
const extra = members.filter((m) => !usableNames.has(m));
|
||||
if (extra.length) group.proxies = extra;
|
||||
if (members.length) group.proxies = [...new Set(members)];
|
||||
} else {
|
||||
group.proxies = members.length ? members : ['DIRECT'];
|
||||
}
|
||||
if (g.type !== 'select') {
|
||||
group.url = g.testUrl || 'http://cp.cloudflare.com/generate_204';
|
||||
group.interval = g.interval ?? 300;
|
||||
if (g.tolerance) group.tolerance = g.tolerance;
|
||||
if (g.type === 'url-test' && g.tolerance !== null) group.tolerance = g.tolerance;
|
||||
}
|
||||
return group;
|
||||
});
|
||||
@@ -158,7 +195,7 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }):
|
||||
}
|
||||
const policy = mapPolicy(r.policy);
|
||||
if (r.ruleset) {
|
||||
ruleLines.push(`RULE-SET,${providerNameByRulesetId.get(r.ruleset.id)},${policy}`);
|
||||
ruleLines.push(['RULE-SET', providerNameByRulesetId.get(r.ruleset.id), policy, ...r.params.filter((p) => p === 'no-resolve')].join(','));
|
||||
continue;
|
||||
}
|
||||
// Surge 内置规则集(非 URL):LAN → 私网 GEOIP;SYSTEM 无对应物,跳过
|
||||
@@ -171,7 +208,9 @@ export function generateClash(ir: ProfileIR, opts: { convertBaseUrl: string }):
|
||||
continue;
|
||||
}
|
||||
if (r.type === 'AND' || r.type === 'OR' || r.type === 'NOT') {
|
||||
ruleLines.push(`${r.type},${mapLogicValue(r.value || '')},${policy}`);
|
||||
const value = mapLogicValue(r.value || '');
|
||||
if (value) ruleLines.push([r.type, value, policy, ...r.params.filter((p) => p === 'no-resolve')].join(','));
|
||||
else skippedRules.push(`${r.type},${r.value},${r.policy}`);
|
||||
continue;
|
||||
}
|
||||
const params = r.params.filter((p) => p === 'no-resolve');
|
||||
|
||||
Reference in New Issue
Block a user