Compare commits

...

16 Commits

Author SHA1 Message Date
xream
ef4d0a228b feat: 支持从 Gist 恢复时保留当前 GitHub Token 2025-07-18 14:54:13 +08:00
xream
e816e5b3c0 fix: 尝试修复上传 Gist 2025-07-18 13:37:35 +08:00
xream
005051c4ac feat: Node 运行环境支持 SOCKS5 代理 2025-07-18 13:13:43 +08:00
xream
61078b10f3 feat: 备份数据到 Gist 时, 清除 GitHub Token. 恢复后请重新设置 GitHub Token
Some checks failed
build / build (push) Has been cancelled
2025-07-18 10:14:18 +08:00
xream
bfa1b11a0e feat: 正式弃用 SUB_STORE_BACKEND_CRON 和 SUB_STORE_CRON, 请使用 SUB_STORE_BACKEND_SYNC_CRON
Some checks failed
build / build (push) Has been cancelled
2025-07-17 18:49:34 +08:00
xream
7d60aed50d feat: 优化 Surge ws 传输层引号解析
Some checks failed
build / build (push) Has been cancelled
2025-07-12 21:06:02 +08:00
xream
e20d0c1dc9 feat: 订阅流量信息中的数值取整以兼容部分客户端解析; 不包含有效节点的订阅不写入乐观缓存
Some checks failed
build / build (push) Has been cancelled
2025-07-12 11:11:40 +08:00
xream
c5660024fb doc: demo.js 2025-07-12 09:26:04 +08:00
xream
76e12bd6a0 feat: Surge username password 逻辑优化
Some checks failed
build / build (push) Has been cancelled
2025-07-11 10:36:43 +08:00
xream
3a33446422 doc: demo.js
Some checks failed
build / build (push) Has been cancelled
2025-07-10 12:37:19 +08:00
xream
17b12711b4 fix: 修复 sing-box 和 mihomo 的 ip-version 2025-07-10 12:32:11 +08:00
xream
c266635ba1 fix: 修复 sing-box shadowsocks+shadow-tls
Some checks failed
build / build (push) Has been cancelled
2025-07-10 08:00:39 +08:00
xream
f34eac9568 feat: 节点本地内容支持 JSON5 2025-07-10 01:37:16 +08:00
xream
aa5b51a3cc feat: 放宽 sing-box VLESS flow 逻辑 2025-07-10 01:28:15 +08:00
xream
b8897dd94a fix: 修复 Egern transport 兼容性
Some checks failed
build / build (push) Has been cancelled
2025-07-08 21:59:16 +08:00
xream
71958e6bb1 doc: demo.js 2025-07-08 09:43:31 +08:00
16 changed files with 273 additions and 50 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "sub-store",
"version": "2.19.67",
"version": "2.19.83",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
"main": "src/main.js",
"scripts": {
@@ -32,6 +32,7 @@
"dns-packet": "^5.6.1",
"dotenv": "^16.4.7",
"express": "^4.17.1",
"fetch-socks": "^1.3.2",
"http-proxy-middleware": "^3.0.3",
"ip-address": "^9.0.5",
"js-base64": "^3.7.2",

26
backend/pnpm-lock.yaml generated
View File

@@ -40,6 +40,9 @@ importers:
express:
specifier: ^4.17.1
version: 4.21.2
fetch-socks:
specifier: ^1.3.2
version: 1.3.2
http-proxy-middleware:
specifier: ^3.0.3
version: 3.0.3
@@ -1974,6 +1977,9 @@ packages:
fastq@1.18.0:
resolution: {integrity: sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==}
fetch-socks@1.3.2:
resolution: {integrity: sha512-vkH5+Zgj2yEbU57Cei0iyLgTZ4OkEKJj56Xu3ViB5dpsl599JgEooQ3x6NVagIFRHWnWJ+7K0MO0aIV1TMgvnw==}
file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}
@@ -3636,6 +3642,10 @@ packages:
resolution: {integrity: sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==}
engines: {node: '>=0.10.0'}
smart-buffer@4.2.0:
resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==}
engines: {node: '>= 6.0.0', npm: '>= 3.0.0'}
snapdragon-node@2.1.1:
resolution: {integrity: sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==}
engines: {node: '>=0.10.0'}
@@ -3648,6 +3658,10 @@ packages:
resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==}
engines: {node: '>=0.10.0'}
socks@2.8.6:
resolution: {integrity: sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==}
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
source-map-generator@0.8.0:
resolution: {integrity: sha512-psgxdGMwl5MZM9S3FWee4EgsEaIjahYV5AzGnwUvPhWeITz/j6rKpysQHlQ4USdxvINlb8lKfWGIXwfkrgtqkA==}
engines: {node: '>= 10'}
@@ -6553,6 +6567,11 @@ snapshots:
dependencies:
reusify: 1.0.4
fetch-socks@1.3.2:
dependencies:
socks: 2.8.6
undici: 7.4.0
file-entry-cache@6.0.1:
dependencies:
flat-cache: 3.2.0
@@ -8424,6 +8443,8 @@ snapshots:
slash@1.0.0: {}
smart-buffer@4.2.0: {}
snapdragon-node@2.1.1:
dependencies:
define-property: 1.0.0
@@ -8447,6 +8468,11 @@ snapshots:
transitivePeerDependencies:
- supports-color
socks@2.8.6:
dependencies:
ip-address: 9.0.5
smart-buffer: 4.2.0
source-map-generator@0.8.0: {}
source-map-resolve@0.5.3:

View File

@@ -12,6 +12,7 @@ import getLoonParser from './peggy/loon';
import getQXParser from './peggy/qx';
import getTrojanURIParser from './peggy/trojan-uri';
import $ from '@/core/app';
import JSON5 from 'json5';
import { Base64 } from 'js-base64';
@@ -1130,14 +1131,14 @@ function Clash_All() {
const name = 'Clash Parser';
const test = (line) => {
try {
JSON.parse(line);
JSON5.parse(line);
} catch (e) {
return false;
}
return true;
};
const parse = (line) => {
const proxy = JSON.parse(line);
const proxy = JSON5.parse(line);
if (
![
'anytls',

View File

@@ -105,11 +105,11 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
proxy.type = "wireguard-surge";
handleShadowTLS();
}
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/fast_open/tfo/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "hysteria2";
handleShadowTLS();
}
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "socks5";
handleShadowTLS();
}
@@ -121,7 +121,6 @@ socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek pas
direct = tag equals "direct" (udp_relay/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/block_quic/others)* {
proxy.type = "direct";
}
address = comma server:server comma port:port {
proxy.server = server;
proxy.port = port;
@@ -179,8 +178,8 @@ username = & {
peg$currPos = end;
return true;
}
} { proxy.username = $.username; }
password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1'); }
} { proxy.username = $.username.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
sni = comma "sni" equals sni:("off"/domain) {
@@ -196,7 +195,7 @@ tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
@@ -212,11 +211,11 @@ ws_headers = comma "ws-headers" equals headers:$[^,]+ {
const result = {};
pairs.forEach(pair => {
const [key, value] = pair.trim().split(":");
result[key.trim()] = value.trim();
result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1');
})
obfs["ws-headers"] = result;
}
ws_path = comma "ws-path" equals path:uri { obfs.path = path; }
ws_path = comma "ws-path" equals path:uri { obfs.path = path.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; }
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };

View File

@@ -176,8 +176,8 @@ username = & {
peg$currPos = end;
return true;
}
} { proxy.username = $.username; }
password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1'); }
} { proxy.username = $.username.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
password = comma match:[^,]+ { proxy.password = match.join("").replace(/^"(.*)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
tls = comma "tls" equals flag:bool { proxy.tls = flag; }
sni = comma "sni" equals sni:("off"/domain) {
@@ -193,7 +193,7 @@ tls_fingerprint = comma "server-cert-fingerprint-sha256" equals tls_fingerprint:
snell_psk = comma "psk" equals match:[^,]+ { proxy.psk = match.join(""); }
snell_version = comma "version" equals match:$[0-9]+ { proxy.version = parseInt(match.trim()); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join(""); }
usernamek = comma "username" equals match:[^,]+ { proxy.username = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
passwordk = comma "password" equals match:[^,]+ { proxy.password = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
vmess_uuid = comma "username" equals match:[^,]+ { proxy.uuid = match.join(""); }
vmess_aead = comma "vmess-aead" equals flag:bool { proxy.aead = flag; }
@@ -209,11 +209,11 @@ ws_headers = comma "ws-headers" equals headers:$[^,]+ {
const result = {};
pairs.forEach(pair => {
const [key, value] = pair.trim().split(":");
result[key.trim()] = value.trim();
result[key.trim()] = value.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1');
})
obfs["ws-headers"] = result;
}
ws_path = comma "ws-path" equals path:uri { obfs.path = path; }
ws_path = comma "ws-path" equals path:uri { obfs.path = path.trim().replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
obfs = comma "obfs" equals type:("http"/"tls") { obfs.type = type; }
obfs_host = comma "obfs-host" equals host:domain { obfs.host = host; };

View File

@@ -1,5 +1,13 @@
import { isPresent } from '@/core/proxy-utils/producers/utils';
const ipVersions = {
dual: 'dual',
'v4-only': 'ipv4',
'v6-only': 'ipv6',
'prefer-v4': 'ipv4-prefer',
'prefer-v6': 'ipv6-prefer',
};
export default function ClashMeta_Producer() {
const type = 'ALL';
const produce = (proxies, type, opts = {}) => {
@@ -242,6 +250,11 @@ export default function ClashMeta_Producer() {
delete proxy[`${proxy.network}-opts`]['_grpc-type'];
delete proxy[`${proxy.network}-opts`]['_grpc-authority'];
}
if (proxy['ip-version']) {
proxy['ip-version'] =
ipVersions[proxy['ip-version']] || proxy['ip-version'];
}
return proxy;
});

View File

@@ -377,6 +377,23 @@ export default function Egern_Producer() {
delete proxy.id;
delete proxy.resolved;
delete proxy['no-resolve'];
if (proxy.transport) {
for (const key in proxy.transport) {
if (
Object.keys(proxy.transport[key]).length === 0 ||
Object.values(proxy.transport[key]).every(
(v) => v == null,
)
) {
delete proxy.transport[key];
}
}
if (Object.keys(proxy.transport).length === 0) {
delete proxy.transport;
}
}
if (type !== 'internal') {
for (const key in proxy) {
if (proxy[key] == null || /^_/i.test(key)) {

View File

@@ -2,6 +2,26 @@ import ClashMeta_Producer from './clashmeta';
import $ from '@/core/app';
import { isIPv4, isIPv6 } from '@/utils';
const ipVersions = {
ipv4: 'ipv4_only',
ipv6: 'ipv6_only',
'v4-only': 'ipv4_only',
'v6-only': 'ipv6_only',
'ipv4-prefer': 'prefer_ipv4',
'ipv6-prefer': 'prefer_ipv6',
'prefer-v4': 'prefer_ipv4',
'prefer-v6': 'prefer_ipv6',
};
const ipVersionParser = (proxy, parsedProxy) => {
const strategy = ipVersions[proxy['ip-version']];
if (proxy._dns_server && strategy) {
parsedProxy.domain_resolver = {
server: proxy._dns_server,
strategy,
};
}
};
const detourParser = (proxy, parsedProxy) => {
parsedProxy.detour = proxy['dialer-proxy'] || proxy.detour;
};
@@ -278,6 +298,7 @@ const sshParser = (proxy = {}) => {
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -305,6 +326,7 @@ const httpParser = (proxy = {}) => {
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -327,6 +349,7 @@ const socks5Parser = (proxy = {}) => {
networkParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -338,6 +361,17 @@ const shadowTLSParser = (proxy = {}) => {
password: proxy.password,
detour: `${proxy.name}_shadowtls`,
};
if (proxy.uot) ssPart.udp_over_tcp = true;
if (proxy['udp-over-tcp']) {
ssPart.udp_over_tcp = {
enabled: true,
version:
!proxy['udp-over-tcp-version'] ||
proxy['udp-over-tcp-version'] === 1
? 1
: 2,
};
}
const stPart = {
tag: `${proxy.name}_shadowtls`,
type: 'shadowtls',
@@ -360,6 +394,7 @@ const shadowTLSParser = (proxy = {}) => {
tfoParser(proxy, stPart);
detourParser(proxy, stPart);
smuxParser(proxy.smux, ssPart);
ipVersionParser(proxy, stPart);
return { type: 'ss-with-st', ssPart, stPart };
};
const ssParser = (proxy = {}) => {
@@ -389,6 +424,7 @@ const ssParser = (proxy = {}) => {
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
if (proxy.plugin) {
const optArr = [];
if (proxy.plugin === 'obfs') {
@@ -467,6 +503,7 @@ const ssrParser = (proxy = {}) => {
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -505,6 +542,7 @@ const vmessParser = (proxy = {}) => {
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -521,7 +559,8 @@ const vlessParser = (proxy = {}) => {
throw 'invalid port';
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
// if (['xtls-rprx-vision', ''].includes(proxy.flow)) parsedProxy.flow = proxy.flow;
if (proxy.flow != null) parsedProxy.flow = proxy.flow;
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
if (proxy.network === 'grpc') grpcParser(proxy, parsedProxy);
networkParser(proxy, parsedProxy);
@@ -529,6 +568,7 @@ const vlessParser = (proxy = {}) => {
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
tlsParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const trojanParser = (proxy = {}) => {
@@ -550,6 +590,7 @@ const trojanParser = (proxy = {}) => {
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const hysteriaParser = (proxy = {}) => {
@@ -599,6 +640,7 @@ const hysteriaParser = (proxy = {}) => {
detourParser(proxy, parsedProxy);
tfoParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const hysteria2Parser = (proxy = {}) => {
@@ -632,6 +674,7 @@ const hysteria2Parser = (proxy = {}) => {
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const tuic5Parser = (proxy = {}) => {
@@ -663,6 +706,7 @@ const tuic5Parser = (proxy = {}) => {
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
const anytlsParser = (proxy = {}) => {
@@ -685,6 +729,7 @@ const anytlsParser = (proxy = {}) => {
);
detourParser(proxy, parsedProxy);
tlsParser(proxy, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};
@@ -742,6 +787,7 @@ const wireguardParser = (proxy = {}) => {
tfoParser(proxy, parsedProxy);
detourParser(proxy, parsedProxy);
smuxParser(proxy.smux, parsedProxy);
ipVersionParser(proxy, parsedProxy);
return parsedProxy;
};

View File

@@ -370,9 +370,9 @@ function vmess(proxy, includeUnsupportedProxy) {
function ssh(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,username="${proxy.username}"`, 'username');
// 所有的类似的字段都有双引号的问题 暂不处理
result.appendIfPresent(`,"${proxy.password}"`, 'password');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
// https://manual.nssurge.com/policy/ssh.html
// 需配合 Keystore
@@ -439,8 +439,8 @@ function http(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'https' : 'http';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,"${proxy.password}"`, 'password');
result.appendIfPresent(`,username="${proxy.username}"`, 'username');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -565,8 +565,8 @@ function socks5(proxy) {
const result = new Result(proxy);
const type = proxy.tls ? 'socks5-tls' : 'socks5';
result.append(`${proxy.name}=${type},${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username');
result.appendIfPresent(`,"${proxy.password}"`, 'password');
result.appendIfPresent(`,username="${proxy.username}"`, 'username');
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');

View File

@@ -119,10 +119,11 @@ export default function serve() {
$app.start();
if ($.env.isNode) {
// Deprecated: SUB_STORE_BACKEND_CRON
const backend_sync_cron =
eval('process.env.SUB_STORE_BACKEND_SYNC_CRON') ||
eval('process.env.SUB_STORE_BACKEND_CRON');
// Deprecated: SUB_STORE_BACKEND_CRON, SUB_STORE_CRON
const backend_sync_cron = eval(
'process.env.SUB_STORE_BACKEND_SYNC_CRON',
);
if (backend_sync_cron) {
$.info(`[SYNC CRON] ${backend_sync_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
@@ -145,6 +146,17 @@ export default function serve() {
true, // start
// 'Asia/Shanghai' // timeZone
);
} else {
if (eval('process.env.SUB_STORE_BACKEND_CRON')) {
$.error(
`[SYNC CRON] SUB_STORE_BACKEND_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,
);
}
if (eval('process.env.SUB_STORE_CRON')) {
$.error(
`[SYNC CRON] SUB_STORE_CRON 已弃用, 请使用 SUB_STORE_BACKEND_SYNC_CRON`,
);
}
}
// 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b
// 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import $ from '@/core/app';
import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response';
@@ -104,7 +105,7 @@ async function refresh(_, res) {
success(res);
}
async function gistBackupAction(action) {
async function gistBackupAction(action, keep) {
// read token
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) throw new Error('GitHub Token is required for backup!');
@@ -114,6 +115,9 @@ async function gistBackupAction(action) {
key: GIST_BACKUP_KEY,
syncPlatform,
});
let currentContent = $.read('#sub-store');
currentContent = currentContent ? JSON.parse(currentContent) : {};
if ($.env.isNode) currentContent = JSON.parse(JSON.stringify($.cache));
let content;
const settings = $.read(SETTINGS_KEY);
const updated = settings.syncTime;
@@ -121,7 +125,10 @@ async function gistBackupAction(action) {
case 'upload':
try {
content = $.read('#sub-store');
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
content = content ? JSON.parse(content) : {};
if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));
content.settings.gistToken = '恢复后请重新设置 GitHub Token';
content = JSON.stringify(content, null, ` `);
$.info(`下载备份, 与本地内容对比...`);
const onlineContent = await gist.download(
GIST_BACKUP_FILE_NAME,
@@ -138,7 +145,10 @@ async function gistBackupAction(action) {
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
content = content ? JSON.parse(content) : {};
if ($.env.isNode) content = JSON.parse(JSON.stringify($.cache));
content.settings.gistToken = '恢复后请重新设置 GitHub Token';
content = JSON.stringify(content, null, ` `);
$.info(`上传备份中...`);
try {
await gist.upload({
@@ -156,7 +166,8 @@ async function gistBackupAction(action) {
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
try {
if (Object.keys(JSON.parse(content).settings).length === 0) {
content = JSON.parse(content);
if (Object.keys(content.settings).length === 0) {
throw new Error('备份文件应该至少包含 settings 字段');
}
} catch (err) {
@@ -167,10 +178,15 @@ async function gistBackupAction(action) {
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
if (keep) {
$.info(`保留原有设置 ${keep}`);
keep.split(',').forEach((path) => {
_.set(content, path, _.get(currentContent, path));
});
}
// restore settings
$.write(content, '#sub-store');
$.write(JSON.stringify(content, null, ` `), '#sub-store');
if ($.env.isNode) {
content = JSON.parse(content);
$.cache = content;
$.persistCache();
}
@@ -182,7 +198,7 @@ async function gistBackupAction(action) {
}
}
async function gistBackup(req, res) {
const { action } = req.query;
const { action, keep } = req.query;
// read token
const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) {
@@ -195,7 +211,7 @@ async function gistBackup(req, res) {
);
} else {
try {
await gistBackupAction(action);
await gistBackupAction(action, keep);
success(res);
} catch (err) {
$.error(

View File

@@ -14,6 +14,8 @@ import $ from '@/core/app';
import { findByName } from '@/utils/database';
import { produceArtifact } from '@/restful/sync';
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
import { ProxyUtils } from '@/core/proxy-utils';
const clashPreprocessor = PROXY_PREPROCESSORS.find(
(processor) => processor.name === 'Clash Pre-processor',
);
@@ -261,10 +263,34 @@ export default async function download(
if (shouldCache) {
resourceCache.set(id, body);
if (customCacheKey) {
$.info(
`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`,
);
$.write(body, customCacheKey);
let shouldWriteCustomCacheKey = true;
if (preprocess) {
try {
const proxies = ProxyUtils.parse(body);
if (
!Array.isArray(proxies) ||
proxies.length === 0
) {
$.error(
`URL ${url} 不包含有效节点\n不写入自定义缓存 ${$arguments?.cacheKey}`,
);
shouldWriteCustomCacheKey = false;
}
} catch (e) {
$.error(
`URL ${url} 尝试解析节点失败 ${
e.message ?? e
}\n不写入自定义缓存 ${$arguments?.cacheKey}`,
);
shouldWriteCustomCacheKey = false;
}
}
if (shouldWriteCustomCacheKey) {
$.info(
`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`,
);
$.write(body, customCacheKey);
}
}
}

View File

@@ -334,7 +334,22 @@ export function normalizeFlowHeader(flowHeaders) {
if (!kvMap.has(key)) {
try {
// 解码 URI 组件并保留原始值作为 fallback
const decodedValue = decodeURIComponent(encodedValue);
let decodedValue = decodeURIComponent(encodedValue);
if (
['upload', 'download', 'total', 'expire'].includes(
key,
)
) {
try {
decodedValue = Number(decodedValue).toFixed(0);
} catch (e) {
$.error(
`Failed to convert value for key "${key}=${encodedValue}": ${
e.message ?? e
}`,
);
}
}
kvMap.set(key, decodedValue);
} catch (e) {
kvMap.set(key, encodedValue);

View File

@@ -18,6 +18,26 @@ function isPlainObject(obj) {
);
}
function parseSocks5Uri(uri) {
// eslint-disable-next-line no-unused-vars
let [__, username, password, server, port, query, name] = uri.match(
/^socks5:\/\/(?:(.*?):(.*?)@)?(.*?)(?::(\d+?))?(\?.*?)?(?:#(.*?))?$/,
);
if (port) {
port = parseInt(port, 10);
} else {
$.error(`port is not present in line: ${uri}`);
throw new Error(`port is not present in line: ${uri}`);
}
return {
type: 5,
host: server,
port,
userId: username != null ? decodeURIComponent(username) : undefined,
password: password != null ? decodeURIComponent(password) : undefined,
};
}
export class OpenAPI {
constructor(name = 'untitled', debug = false) {
this.name = name;
@@ -393,6 +413,7 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
}
if (isNode) {
const undici = eval("require('undici')");
const { socksDispatcher } = eval("require('fetch-socks')");
const {
ProxyAgent,
EnvHttpProxyAgent,
@@ -422,16 +443,34 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
).toString('base64')}`,
};
}
let dispatcher;
if (!opts.proxy) {
const allProxy =
eval('process.env.all_proxy') ||
eval('process.env.ALL_PROXY');
if (allProxy && /^socks5:\/\//.test(allProxy)) {
opts.proxy = allProxy;
}
}
if (opts.proxy) {
if (/^socks5:\/\//.test(opts.proxy)) {
dispatcher = socksDispatcher(
parseSocks5Uri(opts.proxy),
agentOpts,
);
} else {
dispatcher = new ProxyAgent({
...agentOpts,
uri: opts.proxy,
});
}
} else {
dispatcher = new EnvHttpProxyAgent(agentOpts);
}
const response = await request(opts.url, {
...opts,
method: method.toUpperCase(),
dispatcher: (opts.proxy
? new ProxyAgent({
...agentOpts,
uri: opts.proxy,
})
: new EnvHttpProxyAgent(agentOpts)
).compose(
dispatcher: dispatcher.compose(
interceptors.redirect({
maxRedirections: 3,
throwOnMaxRedirects: true,

View File

@@ -26,7 +26,7 @@ function operator(proxies = [], targetPlatform, context) {
// 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`
// 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
// 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
// 15. `ip-version` 设置节点使用 IP 版本,可选dualipv4ipv6ipv4-preferipv6-prefer. 会进行内部转换, 若无法匹配则使用原始值
// 15. `ip-version` 设置节点使用 IP 版本,兼容各家的值. 会进行内部转换. sing-box 以外: 若无法匹配则使用原始值. sing-box: 需有匹配且节点上设置 `_dns_server` 字段, 将自动设置 `domain_resolver.server`
// 16. `sing-box` 支持使用 `_network` 来设置 `network`, 例如 `tcp`, `udp`
// 17. `block-quic` 支持 `auto`, `on`, `off`. 不同的平台不一定都支持, 会自动转换
@@ -151,6 +151,18 @@ function operator(proxies = [], targetPlatform, context) {
// });
// $server.sni = sni
// 示例: 从 config 文件中读取配置项并进行节点操作
// config 的本地内容为
// {
// "reuse": false
// }
// 脚本操作为
// const config = (ProxyUtils.JSON5 || JSON).parse(await produceArtifact({
// type: 'file',
// name: 'config' // 文件名
// }))
// $server.reuse = config.reuse
// 1. Surge 输出 WireGuard 完整配置
// let proxies = await produceArtifact({
@@ -235,14 +247,14 @@ function operator(proxies = [], targetPlatform, context) {
// 这个历史遗留原因, 是有点复杂. 提供一个例子, 用来取当前脚本所在的组合订阅或单条订阅名称
// let name = ''
// for (const [key, value] of Object.entries(env.source)) {
// for (const [key, value] of Object.entries(context.source)) {
// if (!key.startsWith('_')) {
// name = value.displayName || value.name
// break
// }
// }
// if (!name) {
// const collection = env.source._collection
// const collection = context.source._collection
// name = collection.displayName || collection.name
// }