feat: clash/ss suport

This commit is contained in:
2026-04-13 22:19:30 +08:00
parent 699b038a6c
commit 35e95ee777
11 changed files with 665 additions and 129 deletions

View File

@@ -0,0 +1,79 @@
/**
* Convert proxy URIs (ss://, vmess://, trojan://) to Clash YAML flow-style proxy lines.
* Returns a string like ` - {name: "NodeName", type: ss, server: ..., port: ..., ...}`
* or null if the URI is unsupported/invalid.
*/
export function uriToClashLine(uri: string): string | null {
try {
if (uri.startsWith('ss://')) return ssToClash(uri);
if (uri.startsWith('vmess://')) return vmessToClash(uri);
if (uri.startsWith('trojan://')) return trojanToClash(uri);
return null;
} catch {
return null;
}
}
/** Escape a string value for use inside double-quoted YAML flow style */
function esc(s: string): string {
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
function ssToClash(uri: string): string | null {
const url = new URL(uri);
const name = decodeURIComponent(url.hash.slice(1));
const server = url.hostname;
const port = url.port;
// userinfo is base64(method:password)
const decoded = Buffer.from(url.username, 'base64').toString();
const firstColon = decoded.indexOf(':');
if (firstColon === -1) return null;
const cipher = decoded.slice(0, firstColon);
const password = decoded.slice(firstColon + 1);
return ` - {name: "${esc(name)}", type: ss, server: ${server}, port: ${port}, cipher: ${cipher}, password: "${esc(password)}"}`;
}
function vmessToClash(uri: string): string | null {
const b64 = uri.replace('vmess://', '');
const json = JSON.parse(Buffer.from(b64, 'base64').toString());
const name = json.ps || 'VMess';
const server = json.add;
const port = json.port;
const uuid = json.id;
if (!server || !port || !uuid) return null;
let line = ` - {name: "${esc(name)}", type: vmess, server: ${server}, port: ${port}, uuid: ${uuid}, alterId: 0, cipher: auto`;
if (json.tls === 'tls') {
line += ', tls: true';
if (json.sni) line += `, sni: ${json.sni}`;
}
if (json.net === 'ws') {
line += ', network: ws';
const wsOpts: string[] = [];
if (json.path) wsOpts.push(`path: "${esc(json.path)}"`);
if (json.host) wsOpts.push(`headers: {Host: ${json.host}}`);
if (wsOpts.length > 0) {
line += `, ws-opts: {${wsOpts.join(', ')}}`;
}
}
line += '}';
return line;
}
function trojanToClash(uri: string): string | null {
const url = new URL(uri);
const name = decodeURIComponent(url.hash.slice(1));
const server = url.hostname;
const port = url.port;
const password = url.username;
const sni = url.searchParams.get('sni') || server;
return ` - {name: "${esc(name)}", type: trojan, server: ${server}, port: ${port}, password: "${esc(password)}", sni: ${sni}}`;
}