Compare commits

...

24 Commits

Author SHA1 Message Date
xream
ef85b6d0e9 feat: 文件支持设置代理/策略, 链接支持传入 proxy 参数指定代理/策略; 修复代理/策略优先级 2024-10-07 22:05:07 +08:00
xream
8ffb060cb4 feat: 组合订阅支持设置代理/策略, 链接支持传入 proxy 参数指定代理/策略 2024-10-07 20:56:33 +08:00
xream
6d43961e96 feat: Node.js 支持使用环境变量 SUB_STORE_BACKEND_DEFAULT_PROXY 设置默认代理; ProxyUtils 增加 download 方法 2024-10-07 18:43:29 +08:00
xream
f3200aea8c feat: 流量和同步配置也使用默认代理/策略 2024-10-07 18:34:39 +08:00
xream
e2346d16a2 feat: 新增全局代理/策略设置, 前端 > 2.14.265 2024-10-07 18:05:06 +08:00
xream
dc320eaa6c feat(file): 新增启用下载(文件名为显示名称), 前端 > 2.14.264 2024-10-07 17:26:15 +08:00
xream
02031019f7 doc: demo.js 2024-09-22 06:02:58 +08:00
xream
5d09fe782f feat: 增加 _subDisplayName _collectionDisplayName 2024-09-18 19:42:53 +08:00
xream
6e425e5908 doc: demo.js 2024-09-18 19:22:12 +08:00
xream
d10c9233c0 feat: 正式弃用旧的 subName 和 collectionName 2024-09-18 19:18:50 +08:00
xream
cc556b641d fix: 修复 password 为数字时的 bug 2024-09-16 01:43:16 +08:00
xream
de2813b035 feat: 使用自定义缓存时 cacheKey 的值不能为空 2024-09-13 23:42:55 +08:00
xream
c9158ceb1d feat: 内置的 Google/Cloudflare DNS 更换为 DoH 2024-09-09 14:56:47 +08:00
xream
5cf0c98f5f chroe: 修改脚本链接为 release 分支 2024-09-07 23:21:42 +08:00
xream
7d0414f8ca fix: 传输层 path 应为以 / 开头的字符串 2024-09-05 17:39:42 +08:00
xream
bee1d62a1a fix: 传输层 path 应以 / 开头 2024-09-05 17:15:30 +08:00
xream
72bc9b9456 feat: 处理非字符串的 ports 字段 2024-09-04 13:40:17 +08:00
xream
3b4c14e7d0 doc: README 2024-09-04 10:49:52 +08:00
xream
59d93483bb feat: Node.js 版支持环境变量 SUB_STORE_BACKEND_DOWNLOAD_CRON 设置定时恢复配置, SUB_STORE_BACKEND_UPLOAD_CRON 设置定时备份配置, SUB_STORE_BACKEND_SYNC_CRON 设置定时同步订阅/文件 2024-09-04 02:20:28 +08:00
xream
75d88c02c7 feat: SurgeMac 支持使用 mihomo 来支援 Surge 本身不支持的协议; 弃用旧的 ssr-local 方案 2024-09-03 20:31:42 +08:00
xream
99d5868cff feat: 订阅和文件的请求链接支持传入 $options , 可在脚本中使用 2024-09-03 13:58:10 +08:00
xream
e1489a3cf7 feat: sing-box VLESS Reality uTLS 默认启用 2024-09-02 21:20:22 +08:00
xream
59fe16a7b0 feat: Surge Hysteria2 与 TUIC 协议支持端口跳跃; Hysteria2 URI 的端口部分支持 端口跳跃 的「多端口地址格式」 2024-09-02 16:38:21 +08:00
xream
562d349629 feat: 脚本操作传入上下文 require (仅对应的环境支持)" 2024-08-31 22:39:54 +08:00
35 changed files with 722 additions and 245 deletions

View File

@@ -10,10 +10,10 @@
Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket. Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
</p> </p>
[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store) [![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
<a href="https://trendshift.io/repositories/4572" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4572" alt="sub-store-org%2FSub-Store | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a> <a href="https://trendshift.io/repositories/4572" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4572" alt="sub-store-org%2FSub-Store | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM) [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM)
Core functionalities: Core functionalities:
1. Conversion among various formats. 1. Conversion among various formats.
@@ -21,7 +21,7 @@ Core functionalities:
3. Collect multiple subscriptions in one URL. 3. Collect multiple subscriptions in one URL.
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information. > The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
## 1. Subscription Conversion ## 1. Subscription Conversion
### Supported Input Formats ### Supported Input Formats
@@ -31,7 +31,7 @@ Core functionalities:
- [x] Clash Proxy JSON(single line) - [x] Clash Proxy JSON(single line)
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS) - [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2) - [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2)
- [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), SSR(external, only for macOS), External Proxy Program(only for macOS), WireGuard(Surge to Surge)) - [x] Surge (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
- [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard)) - [x] Surfboard (SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard(Surfboard to Surfboard))
- [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC) - [x] Shadowrocket (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC) - [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC)
@@ -46,6 +46,7 @@ Core functionalities:
- [x] Clash - [x] Clash
- [x] Surfboard - [x] Surfboard
- [x] Surge - [x] Surge
- [x] SurgeMac(Use mihomo to support protocols that are not supported by Surge itself)
- [x] Loon - [x] Loon
- [x] Shadowrocket - [x] Shadowrocket
- [x] QX - [x] QX
@@ -98,7 +99,7 @@ or
esbuild(experimental) esbuild(experimental)
``` ```
pnpm run --parallel "/^dev:.*/" SUB_STORE_BACKEND_API_PORT=3000 pnpm run --parallel "/^dev:.*/"
``` ```
## LICENSE ## LICENSE
@@ -111,7 +112,6 @@ This project is under the GPL V3 LICENSE.
[![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date) [![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date)
## Acknowledgements ## Acknowledgements
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work! - Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.365", "version": "2.14.391",
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.", "description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
"main": "src/main.js", "main": "src/main.js",
"scripts": { "scripts": {

View File

@@ -9,6 +9,7 @@ import {
isNotBlank, isNotBlank,
ipAddress, ipAddress,
getRandomPort, getRandomPort,
numberToString,
} from '@/utils'; } from '@/utils';
import PROXY_PROCESSORS, { ApplyProcessor } from './processors'; import PROXY_PROCESSORS, { ApplyProcessor } from './processors';
import PROXY_PREPROCESSORS from './preprocessors'; import PROXY_PREPROCESSORS from './preprocessors';
@@ -77,7 +78,13 @@ function parse(raw) {
return proxies; return proxies;
} }
async function processFn(proxies, operators = [], targetPlatform, source) { async function processFn(
proxies,
operators = [],
targetPlatform,
source,
$options,
) {
for (const item of operators) { for (const item of operators) {
// process script // process script
let script; let script;
@@ -176,6 +183,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
targetPlatform, targetPlatform,
$arguments, $arguments,
source, source,
$options,
); );
} else { } else {
processor = PROXY_PROCESSORS[item.type](item.args || {}); processor = PROXY_PROCESSORS[item.type](item.args || {});
@@ -202,8 +210,6 @@ function produce(proxies, targetPlatform, type, opts = {}) {
); );
proxies = proxies.map((proxy) => { proxies = proxies.map((proxy) => {
proxy._subName = proxy.subName;
proxy._collectionName = proxy.collectionName;
proxy._resolved = proxy.resolved; proxy._resolved = proxy.resolved;
if (!isNotBlank(proxy.name)) { if (!isNotBlank(proxy.name)) {
@@ -224,6 +230,7 @@ function produce(proxies, targetPlatform, type, opts = {}) {
// 处理 端口跳跃 // 处理 端口跳跃
if (proxy.ports) { if (proxy.ports) {
proxy.ports = String(proxy.ports);
if (!['ClashMeta'].includes(targetPlatform)) { if (!['ClashMeta'].includes(targetPlatform)) {
proxy.ports = proxy.ports.replace(/\//g, ','); proxy.ports = proxy.ports.replace(/\//g, ',');
} }
@@ -237,21 +244,10 @@ function produce(proxies, targetPlatform, type, opts = {}) {
$.log(`Producing proxies for target: ${targetPlatform}`); $.log(`Producing proxies for target: ${targetPlatform}`);
if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') { if (typeof producer.type === 'undefined' || producer.type === 'SINGLE') {
let localPort = 10000;
let list = proxies let list = proxies
.map((proxy) => { .map((proxy) => {
try { try {
let line = producer.produce(proxy, type, opts); return producer.produce(proxy, type, opts);
if (
line.length > 0 &&
line.includes('__SubStoreLocalPort__')
) {
line = line.replace(
/__SubStoreLocalPort__/g,
localPort++,
);
}
return line;
} catch (err) { } catch (err) {
$.error( $.error(
`Cannot produce proxy: ${JSON.stringify( `Cannot produce proxy: ${JSON.stringify(
@@ -270,7 +266,7 @@ function produce(proxies, targetPlatform, type, opts = {}) {
proxies.length > 0 && proxies.length > 0 &&
proxies.every((p) => p.type === 'wireguard') proxies.every((p) => p.type === 'wireguard')
) { ) {
list = `#!name=${proxies[0]?.subName} list = `#!name=${proxies[0]?._subName}
#!desc=${proxies[0]?._desc ?? ''} #!desc=${proxies[0]?._desc ?? ''}
#!category=${proxies[0]?._category ?? ''} #!category=${proxies[0]?._category ?? ''}
${list}`; ${list}`;
@@ -296,6 +292,7 @@ export const ProxyUtils = {
getISO, getISO,
MMDB, MMDB,
Gist, Gist,
download,
}; };
function tryParse(parser, line) { function tryParse(parser, line) {
@@ -316,7 +313,23 @@ function safeMatch(parser, line) {
} }
} }
function formatTransportPath(path) {
if (typeof path === 'string' || typeof path === 'number') {
path = String(path).trim();
if (path === '') {
return '/';
} else if (!path.startsWith('/')) {
return '/' + path;
}
}
return path;
}
function lastParse(proxy) { function lastParse(proxy) {
if (typeof proxy.password === 'number') {
proxy.password = numberToString(proxy.password);
}
if (proxy.interface) { if (proxy.interface) {
proxy['interface-name'] = proxy.interface; proxy['interface-name'] = proxy.interface;
delete proxy.interface; delete proxy.interface;
@@ -344,6 +357,17 @@ function lastParse(proxy) {
delete proxy['ws-headers']; delete proxy['ws-headers'];
} }
const transportPath = proxy[`${proxy.network}-opts`]?.path;
if (Array.isArray(transportPath)) {
proxy[`${proxy.network}-opts`].path = transportPath.map((item) =>
formatTransportPath(item),
);
} else if (transportPath != null) {
proxy[`${proxy.network}-opts`].path =
formatTransportPath(transportPath);
}
if (proxy.type === 'trojan') { if (proxy.type === 'trojan') {
if (proxy.network === 'tcp') { if (proxy.network === 'tcp') {
delete proxy.network; delete proxy.network;
@@ -424,9 +448,13 @@ function lastParse(proxy) {
proxy[`${proxy.network}-opts`].path = [transportPath]; proxy[`${proxy.network}-opts`].path = [transportPath];
} }
} }
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) { // if (['hysteria', 'hysteria2', 'tuic'].includes(proxy.type)) {
if (proxy.ports) {
proxy.ports = String(proxy.ports).replace(/\//g, ',');
} else {
delete proxy.ports; delete proxy.ports;
} }
// }
if ( if (
['hysteria2'].includes(proxy.type) && ['hysteria2'].includes(proxy.type) &&
proxy.obfs && proxy.obfs &&

View File

@@ -5,6 +5,7 @@ import {
isPresent, isPresent,
isNotBlank, isNotBlank,
getIfPresent, getIfPresent,
getRandomPort,
} from '@/utils'; } from '@/utils';
import getSurgeParser from './peggy/surge'; import getSurgeParser from './peggy/surge';
import getLoonParser from './peggy/loon'; import getLoonParser from './peggy/loon';
@@ -13,6 +14,19 @@ import getTrojanURIParser from './peggy/trojan-uri';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
function surge_port_hopping(raw) {
const [parts, port_hopping] =
raw.match(
/,\s*?port-hopping\s*?=\s*?["']?\s*?((\d+(-\d+)?)([,;]\d+(-\d+)?)*)\s*?["']?\s*?/,
) || [];
return {
port_hopping: port_hopping
? port_hopping.replace(/;/g, ',')
: undefined,
line: parts ? raw.replace(parts, '') : raw,
};
}
// Parse SS URI format (only supports new SIP002, legacy format is depreciated). // Parse SS URI format (only supports new SIP002, legacy format is depreciated).
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme // reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
function URI_SS() { function URI_SS() {
@@ -545,13 +559,42 @@ function URI_Hysteria2() {
}; };
const parse = (line) => { const parse = (line) => {
line = line.split(/(hysteria2|hy2):\/\//)[2]; line = line.split(/(hysteria2|hy2):\/\//)[2];
// eslint-disable-next-line no-unused-vars // 端口跳跃有两种写法:
let [__, password, server, ___, port, ____, addons = '', name] = // 1. 服务器的地址和可选端口。如果省略端口,则默认为 443。
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line); // 端口部分支持 端口跳跃 的「多端口地址格式」。
port = parseInt(`${port}`, 10); // https://hysteria.network/zh/docs/advanced/Port-Hopping
if (isNaN(port)) { // 2. 参数 mport
let ports;
/* eslint-disable no-unused-vars */
let [
__,
password,
server,
___,
port,
____,
_____,
______,
_______,
________,
addons = '',
name,
] = /^(.*?)@(.*?)(:((\d+(-\d+)?)([,;]\d+(-\d+)?)*))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(
line,
);
/* eslint-enable no-unused-vars */
if (/^\d+$/.test(port)) {
port = parseInt(`${port}`, 10);
if (isNaN(port)) {
port = 443;
}
} else if (port) {
ports = port;
port = getRandomPort(ports);
} else {
port = 443; port = 443;
} }
password = decodeURIComponent(password); password = decodeURIComponent(password);
if (name != null) { if (name != null) {
name = decodeURIComponent(name); name = decodeURIComponent(name);
@@ -563,6 +606,7 @@ function URI_Hysteria2() {
name, name,
server, server,
port, port,
ports,
password, password,
}; };
@@ -1295,7 +1339,12 @@ function Surge_Tuic() {
const test = (line) => { const test = (line) => {
return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]); return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
}; };
const parse = (line) => getSurgeParser().parse(line); const parse = (raw) => {
const { port_hopping, line } = surge_port_hopping(raw);
const proxy = getSurgeParser().parse(line);
proxy['ports'] = port_hopping;
return proxy;
};
return { name, test, parse }; return { name, test, parse };
} }
function Surge_WireGuard() { function Surge_WireGuard() {
@@ -1312,7 +1361,12 @@ function Surge_Hysteria2() {
const test = (line) => { const test = (line) => {
return /^.*=\s*hysteria2/.test(line.split(',')[0]); return /^.*=\s*hysteria2/.test(line.split(',')[0]);
}; };
const parse = (line) => getSurgeParser().parse(line); const parse = (raw) => {
const { port_hopping, line } = surge_port_hopping(raw);
const proxy = getSurgeParser().parse(line);
proxy['ports'] = port_hopping;
return proxy;
};
return { name, test, parse }; return { name, test, parse };
} }

View File

@@ -182,7 +182,6 @@ ecn = comma "ecn" equals flag:bool { proxy.ecn = flag; }
download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); } download_bandwidth = comma "download-bandwidth" equals match:[^,]+ { proxy.down = match.join(""); }
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; } salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join(""); proxy.obfs = 'salamander'; }
tag = match:[^=,]* { proxy.name = match.join("").trim(); } tag = match:[^=,]* { proxy.name = match.join("").trim(); }
comma = _ "," _ comma = _ "," _
equals = _ "=" _ equals = _ "=" _

View File

@@ -91,11 +91,11 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
} }
handleShadowTLS(); handleShadowTLS();
} }
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
handleShadowTLS(); handleShadowTLS();
} }
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
handleShadowTLS(); handleShadowTLS();
@@ -104,7 +104,7 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
proxy.type = "wireguard-surge"; proxy.type = "wireguard-surge";
handleShadowTLS(); 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/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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"; proxy.type = "hysteria2";
handleShadowTLS(); handleShadowTLS();
} }
@@ -151,6 +151,8 @@ port = digits:[0-9]+ {
} }
} }
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
username = & { username = & {
let j = peg$currPos; let j = peg$currPos;
let start, end; let start, end;

View File

@@ -89,11 +89,11 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
} }
handleShadowTLS(); handleShadowTLS();
} }
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
handleShadowTLS(); handleShadowTLS();
} }
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
proxy.type = "tuic"; proxy.type = "tuic";
proxy.version = 5; proxy.version = 5;
handleShadowTLS(); handleShadowTLS();
@@ -102,7 +102,7 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
proxy.type = "wireguard-surge"; proxy.type = "wireguard-surge";
handleShadowTLS(); 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/tls_verification/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/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"; proxy.type = "hysteria2";
handleShadowTLS(); handleShadowTLS();
} }
@@ -149,6 +149,8 @@ port = digits:[0-9]+ {
} }
} }
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
username = & { username = & {
let j = peg$currPos; let j = peg$currPos;
let start, end; let start, end;

View File

@@ -316,7 +316,7 @@ function RegexDeleteOperator(regex) {
1. This function name should be `operator`! 1. This function name should be `operator`!
2. Always declare variables before using them! 2. Always declare variables before using them!
*/ */
function ScriptOperator(script, targetPlatform, $arguments, source) { function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
return { return {
name: 'Script Operator', name: 'Script Operator',
func: async (proxies) => { func: async (proxies) => {
@@ -326,6 +326,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
'operator', 'operator',
script, script,
$arguments, $arguments,
$options,
); );
output = operator(proxies, targetPlatform, { source, ...env }); output = operator(proxies, targetPlatform, { source, ...env });
})(); })();
@@ -338,9 +339,9 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
'operator', 'operator',
`async function operator(input = []) { `async function operator(input = []) {
if (input && (input.$files || input.$content)) { if (input && (input.$files || input.$content)) {
let { $content, $files } = input let { $content, $files, $options } = input
${script} ${script}
return { $content, $files } return { $content, $files, $options }
} else { } else {
let proxies = input let proxies = input
let list = [] let list = []
@@ -352,6 +353,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source) {
} }
}`, }`,
$arguments, $arguments,
$options,
); );
output = operator(proxies, targetPlatform, { source, ...env }); output = operator(proxies, targetPlatform, { source, ...env });
})(); })();
@@ -419,26 +421,23 @@ const DOMAIN_RESOLVERS = {
const id = hex_md5(`GOOGLE:${domain}:${type}`); const id = hex_md5(`GOOGLE:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!noCache && cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const answerType = type === 'IPv6' ? 'AAAA' : 'A';
url: `https://8.8.4.4/resolve?name=${encodeURIComponent( const res = await doh({
domain, url: 'https://8.8.4.4/dns-query',
)}&type=${ domain,
type === 'IPv6' ? 'AAAA' : 'A' type: answerType,
}&edns_client_subnet=${edns}`,
headers: {
accept: 'application/dns-json',
},
timeout, timeout,
edns,
}); });
const body = JSON.parse(resp.body);
if (body['Status'] !== 0) { const { answers } = res;
throw new Error(`Status is ${body['Status']}`);
}
const answers = body['Answer'];
if (!Array.isArray(answers) || answers.length === 0) { if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers.map((i) => i?.data).filter((i) => i); const result = answers
.filter((i) => i?.type === answerType)
.map((i) => i?.data)
.filter((i) => i);
if (result.length === 0) { if (result.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
@@ -472,28 +471,27 @@ const DOMAIN_RESOLVERS = {
resourceCache.set(id, result); resourceCache.set(id, result);
return result; return result;
}, },
Cloudflare: async function (domain, type, noCache, timeout) { Cloudflare: async function (domain, type, noCache, timeout, edns) {
const id = hex_md5(`CLOUDFLARE:${domain}:${type}`); const id = hex_md5(`CLOUDFLARE:${domain}:${type}`);
const cached = resourceCache.get(id); const cached = resourceCache.get(id);
if (!noCache && cached) return cached; if (!noCache && cached) return cached;
const resp = await $.http.get({ const answerType = type === 'IPv6' ? 'AAAA' : 'A';
url: `https://1.0.0.1/dns-query?name=${encodeURIComponent( const res = await doh({
domain, url: 'https://1.0.0.1/dns-query',
)}&type=${type === 'IPv6' ? 'AAAA' : 'A'}`, domain,
headers: { type: answerType,
accept: 'application/dns-json',
},
timeout, timeout,
edns,
}); });
const body = JSON.parse(resp.body);
if (body['Status'] !== 0) { const { answers } = res;
throw new Error(`Status is ${body['Status']}`);
}
const answers = body['Answer'];
if (!Array.isArray(answers) || answers.length === 0) { if (!Array.isArray(answers) || answers.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
const result = answers.map((i) => i?.data).filter((i) => i); const result = answers
.filter((i) => i?.type === answerType)
.map((i) => i?.data)
.filter((i) => i);
if (result.length === 0) { if (result.length === 0) {
throw new Error('No answers'); throw new Error('No answers');
} }
@@ -794,7 +792,7 @@ function TypeFilter(types) {
1. This function name should be `filter`! 1. This function name should be `filter`!
2. Always declare variables before using them! 2. Always declare variables before using them!
*/ */
function ScriptFilter(script, targetPlatform, $arguments, source) { function ScriptFilter(script, targetPlatform, $arguments, source, $options) {
return { return {
name: 'Script Filter', name: 'Script Filter',
func: async (proxies) => { func: async (proxies) => {
@@ -804,6 +802,7 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
'filter', 'filter',
script, script,
$arguments, $arguments,
$options,
); );
output = filter(proxies, targetPlatform, { source, ...env }); output = filter(proxies, targetPlatform, { source, ...env });
})(); })();
@@ -826,6 +825,7 @@ function ScriptFilter(script, targetPlatform, $arguments, source) {
return list return list
}`, }`,
$arguments, $arguments,
$options,
); );
output = filter(proxies, targetPlatform, { source, ...env }); output = filter(proxies, targetPlatform, { source, ...env });
})(); })();
@@ -966,7 +966,7 @@ function clone(object) {
return JSON.parse(JSON.stringify(object)); return JSON.parse(JSON.stringify(object));
} }
function createDynamicFunction(name, script, $arguments) { function createDynamicFunction(name, script, $arguments, $options) {
const flowUtils = { const flowUtils = {
getFlowField, getFlowField,
getFlowHeaders, getFlowHeaders,
@@ -978,6 +978,7 @@ function createDynamicFunction(name, script, $arguments) {
if ($.env.isLoon) { if ($.env.isLoon) {
return new Function( return new Function(
'$arguments', '$arguments',
'$options',
'$substore', '$substore',
'lodash', 'lodash',
'$persistentStore', '$persistentStore',
@@ -987,9 +988,11 @@ function createDynamicFunction(name, script, $arguments) {
'scriptResourceCache', 'scriptResourceCache',
'flowUtils', 'flowUtils',
'produceArtifact', 'produceArtifact',
'require',
`${script}\n return ${name}`, `${script}\n return ${name}`,
)( )(
$arguments, $arguments,
$options,
$, $,
lodash, lodash,
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
@@ -1002,26 +1005,30 @@ function createDynamicFunction(name, script, $arguments) {
scriptResourceCache, scriptResourceCache,
flowUtils, flowUtils,
produceArtifact, produceArtifact,
eval(`typeof require !== "undefined"`) ? require : undefined,
); );
} else { } else {
return new Function( return new Function(
'$arguments', '$arguments',
'$options',
'$substore', '$substore',
'lodash', 'lodash',
'ProxyUtils', 'ProxyUtils',
'scriptResourceCache', 'scriptResourceCache',
'flowUtils', 'flowUtils',
'produceArtifact', 'produceArtifact',
'require',
`${script}\n return ${name}`, `${script}\n return ${name}`,
)( )(
$arguments, $arguments,
$options,
$, $,
lodash, lodash,
ProxyUtils, ProxyUtils,
scriptResourceCache, scriptResourceCache,
flowUtils, flowUtils,
produceArtifact, produceArtifact,
eval(`typeof require !== "undefined"`) ? require : undefined,
); );
} }
} }

View File

@@ -163,9 +163,11 @@ export default function Clash_Producer() {
delete proxy.id; delete proxy.id;
delete proxy.resolved; delete proxy.resolved;
delete proxy['no-resolve']; delete proxy['no-resolve'];
for (const key in proxy) { if (type !== 'internal') {
if (proxy[key] == null || /^_/i.test(key)) { for (const key in proxy) {
delete proxy[key]; if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
} }
} }
if ( if (

View File

@@ -178,9 +178,11 @@ export default function ClashMeta_Producer() {
delete proxy.id; delete proxy.id;
delete proxy.resolved; delete proxy.resolved;
delete proxy['no-resolve']; delete proxy['no-resolve'];
for (const key in proxy) { if (type !== 'internal') {
if (proxy[key] == null || /^_/i.test(key)) { for (const key in proxy) {
delete proxy[key]; if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
} }
} }
if ( if (

View File

@@ -181,9 +181,11 @@ export default function ShadowRocket_Producer() {
delete proxy.id; delete proxy.id;
delete proxy.resolved; delete proxy.resolved;
delete proxy['no-resolve']; delete proxy['no-resolve'];
for (const key in proxy) { if (type !== 'internal') {
if (proxy[key] == null || /^_/i.test(key)) { for (const key in proxy) {
delete proxy[key]; if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
} }
} }
if ( if (

View File

@@ -204,11 +204,6 @@ const tlsParser = (proxy, parsedProxy) => {
if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`; if (proxy.ca) parsedProxy.tls.certificate_path = `${proxy.ca}`;
if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str]; if (proxy.ca_str) parsedProxy.tls.certificate = [proxy.ca_str];
if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']]; if (proxy['ca-str']) parsedProxy.tls.certificate = [proxy['ca-str']];
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
parsedProxy.tls.utls = {
enabled: true,
fingerprint: proxy['client-fingerprint'],
};
if (proxy['reality-opts']) { if (proxy['reality-opts']) {
parsedProxy.tls.reality = { enabled: true }; parsedProxy.tls.reality = { enabled: true };
if (proxy['reality-opts']['public-key']) if (proxy['reality-opts']['public-key'])
@@ -217,7 +212,13 @@ const tlsParser = (proxy, parsedProxy) => {
if (proxy['reality-opts']['short-id']) if (proxy['reality-opts']['short-id'])
parsedProxy.tls.reality.short_id = parsedProxy.tls.reality.short_id =
proxy['reality-opts']['short-id']; proxy['reality-opts']['short-id'];
parsedProxy.tls.utls = { enabled: true };
} }
if (proxy['client-fingerprint'] && proxy['client-fingerprint'] !== '')
parsedProxy.tls.utls = {
enabled: true,
fingerprint: proxy['client-fingerprint'],
};
if (!parsedProxy.tls.enabled) delete parsedProxy.tls; if (!parsedProxy.tls.enabled) delete parsedProxy.tls;
}; };

View File

@@ -272,9 +272,11 @@ export default function Stash_Producer() {
delete proxy.id; delete proxy.id;
delete proxy.resolved; delete proxy.resolved;
delete proxy['no-resolve']; delete proxy['no-resolve'];
for (const key in proxy) { if (type !== 'internal') {
if (proxy[key] == null || /^_/i.test(key)) { for (const key in proxy) {
delete proxy[key]; if (proxy[key] == null || /^_/i.test(key)) {
delete proxy[key];
}
} }
} }
if ( if (

View File

@@ -15,6 +15,9 @@ const ipVersions = {
export default function Surge_Producer() { export default function Surge_Producer() {
const produce = (proxy, type, opts = {}) => { const produce = (proxy, type, opts = {}) => {
proxy.name = proxy.name.replace(/=|,/g, ''); proxy.name = proxy.name.replace(/=|,/g, '');
if (proxy.ports) {
proxy.ports = String(proxy.ports);
}
switch (proxy.type) { switch (proxy.type) {
case 'ss': case 'ss':
return shadowsocks(proxy); return shadowsocks(proxy);
@@ -675,6 +678,15 @@ function tuic(proxy) {
'alpn', 'alpn',
); );
if (isPresent(proxy, 'ports')) {
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
}
result.appendIfPresent(
`,port-hopping-interval=${proxy['hop-interval']}`,
'hop-interval',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
@@ -935,6 +947,15 @@ function hysteria2(proxy) {
result.appendIfPresent(`,password=${proxy.password}`, 'password'); result.appendIfPresent(`,password=${proxy.password}`, 'password');
if (isPresent(proxy, 'ports')) {
result.append(`,port-hopping="${proxy.ports.replace(/,/g, ';')}"`);
}
result.appendIfPresent(
`,port-hopping-interval=${proxy['hop-interval']}`,
'hop-interval',
);
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version']; const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version'); result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');

View File

@@ -1,5 +1,7 @@
import { Base64 } from 'js-base64';
import { Result, isPresent } from './utils'; import { Result, isPresent } from './utils';
import Surge_Producer from './surge'; import Surge_Producer from './surge';
import ClashMeta_Producer from './clashmeta';
import { isIPv4, isIPv6 } from '@/utils'; import { isIPv4, isIPv6 } from '@/utils';
import $ from '@/core/app'; import $ from '@/core/app';
@@ -8,14 +10,22 @@ const targetPlatform = 'SurgeMac';
const surge_Producer = Surge_Producer(); const surge_Producer = Surge_Producer();
export default function SurgeMac_Producer() { export default function SurgeMac_Producer() {
const produce = (proxy) => { const produce = (proxy, type, opts = {}) => {
switch (proxy.type) { switch (proxy.type) {
case 'external': case 'external':
return external(proxy); return external(proxy);
case 'ssr': // case 'ssr':
return shadowsocksr(proxy); // return shadowsocksr(proxy);
default: default: {
return surge_Producer.produce(proxy); try {
return surge_Producer.produce(proxy, type, opts);
} catch (e) {
$.log(
`${proxy.name} is not supported on ${targetPlatform}, try to use Mihomo(SurgeMac - External Proxy Program) instead`,
);
return mihomo(proxy, type, opts);
}
}
} }
}; };
return { produce }; return { produce };
@@ -60,6 +70,7 @@ function external(proxy) {
return result.toString(); return result.toString();
} }
// eslint-disable-next-line no-unused-vars
function shadowsocksr(proxy) { function shadowsocksr(proxy) {
const external_proxy = { const external_proxy = {
...proxy, ...proxy,
@@ -101,6 +112,65 @@ function shadowsocksr(proxy) {
return external(external_proxy); return external(external_proxy);
} }
// eslint-disable-next-line no-unused-vars
function mihomo(proxy, type, opts) {
const clashProxy = ClashMeta_Producer().produce([proxy], 'internal')?.[0];
if (clashProxy) {
const localPort = opts?.localPort || proxy._localPort || 65535;
const ipv6 = ['ipv4', 'v4-only'].includes(proxy['ip-version'])
? false
: true;
const external_proxy = {
name: proxy.name,
type: 'external',
exec: proxy._exec || '/usr/local/bin/mihomo',
'local-port': localPort,
args: [
'-config',
Base64.encode(
JSON.stringify({
'mixed-port': localPort,
ipv6,
mode: 'global',
dns: {
enable: true,
ipv6,
nameserver: [
'https://223.6.6.6/dns-query',
'https://120.53.53.53/dns-query',
],
},
proxies: [
{
...clashProxy,
name: 'proxy',
},
],
'proxy-groups': [
{
name: 'GLOBAL',
type: 'select',
proxies: ['proxy'],
},
],
}),
),
],
addresses: [],
};
// https://manual.nssurge.com/policy/external-proxy.html
if (isIP(proxy.server)) {
external_proxy.addresses.push(proxy.server);
} else {
$.log(
`Platform ${targetPlatform}, proxy type ${proxy.type}: addresses should be an IP address, but got ${proxy.server}`,
);
}
opts.localPort = localPort - 1;
return external(external_proxy);
}
}
function isIP(ip) { function isIP(ip) {
return isIPv4(ip) || isIPv6(ip); return isIPv4(ip) || isIPv6(ip);

View File

@@ -69,11 +69,34 @@ async function downloadSubscription(req, res) {
produceType, produceType,
includeUnsupportedProxy, includeUnsupportedProxy,
resultFormat, resultFormat,
proxy,
} = req.query; } = req.query;
let $options = {};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (url) { if (url) {
url = decodeURIComponent(url); url = decodeURIComponent(url);
$.info(`指定远程订阅 URL: ${url}`); $.info(`指定远程订阅 URL: ${url}`);
} }
if (proxy) {
proxy = decodeURIComponent(proxy);
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
if (ua) { if (ua) {
ua = decodeURIComponent(ua); ua = decodeURIComponent(ua);
$.info(`指定远程订阅 User-Agent: ${ua}`); $.info(`指定远程订阅 User-Agent: ${ua}`);
@@ -116,6 +139,8 @@ async function downloadSubscription(req, res) {
produceOpts: { produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy, 'include-unsupported-proxy': includeUnsupportedProxy,
}, },
$options,
proxy,
}); });
if ( if (
@@ -156,7 +181,7 @@ async function downloadSubscription(req, res) {
url, url,
$arguments.flowUserAgent, $arguments.flowUserAgent,
undefined, undefined,
sub.proxy, proxy || sub.proxy,
$arguments.flowUrl, $arguments.flowUrl,
); );
if (flowInfo) { if (flowInfo) {
@@ -245,8 +270,33 @@ async function downloadCollection(req, res) {
produceType, produceType,
includeUnsupportedProxy, includeUnsupportedProxy,
resultFormat, resultFormat,
proxy,
} = req.query; } = req.query;
let $options = {};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (proxy) {
proxy = decodeURIComponent(proxy);
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') { if (ignoreFailedRemoteSub != null && ignoreFailedRemoteSub !== '') {
ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub); ignoreFailedRemoteSub = decodeURIComponent(ignoreFailedRemoteSub);
$.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`); $.info(`指定忽略失败的远程订阅: ${ignoreFailedRemoteSub}`);
@@ -272,6 +322,8 @@ async function downloadCollection(req, res) {
produceOpts: { produceOpts: {
'include-unsupported-proxy': includeUnsupportedProxy, 'include-unsupported-proxy': includeUnsupportedProxy,
}, },
$options,
proxy,
}); });
// forward flow header from the first subscription in this collection // forward flow header from the first subscription in this collection
@@ -316,7 +368,7 @@ async function downloadCollection(req, res) {
url, url,
$arguments.flowUserAgent, $arguments.flowUserAgent,
undefined, undefined,
sub.proxy, proxy || sub.proxy || collection.proxy,
$arguments.flowUrl, $arguments.flowUrl,
); );
if (flowInfo) { if (flowInfo) {

View File

@@ -59,11 +59,34 @@ async function getFile(req, res) {
content, content,
mergeSources, mergeSources,
ignoreFailedRemoteFile, ignoreFailedRemoteFile,
proxy,
} = req.query; } = req.query;
let $options = {};
if (req.query.$options) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$options = JSON.parse(decodeURIComponent(req.query.$options));
} catch (e) {
for (const pair of req.query.$options.split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$options[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
$.info(`传入 $options: ${JSON.stringify($options)}`);
}
if (url) { if (url) {
url = decodeURIComponent(url); url = decodeURIComponent(url);
$.info(`指定远程文件 URL: ${url}`); $.info(`指定远程文件 URL: ${url}`);
} }
if (proxy) {
proxy = decodeURIComponent(proxy);
$.info(`指定远程订阅使用代理/策略 proxy: ${proxy}`);
}
if (ua) { if (ua) {
ua = decodeURIComponent(ua); ua = decodeURIComponent(ua);
$.info(`指定远程文件 User-Agent: ${ua}`); $.info(`指定远程文件 User-Agent: ${ua}`);
@@ -101,6 +124,8 @@ async function getFile(req, res) {
content, content,
mergeSources, mergeSources,
ignoreFailedRemoteFile, ignoreFailedRemoteFile,
$options,
proxy,
}); });
try { try {
@@ -110,6 +135,8 @@ async function getFile(req, res) {
const flowInfo = await getFlowHeaders( const flowInfo = await getFlowHeaders(
subInfoUrl, subInfoUrl,
subInfoUserAgent || file.subInfoUserAgent, subInfoUserAgent || file.subInfoUserAgent,
undefined,
proxy || file.proxy,
); );
if (flowInfo) { if (flowInfo) {
res.set('subscription-userinfo', flowInfo); res.set('subscription-userinfo', flowInfo);
@@ -122,7 +149,14 @@ async function getFile(req, res) {
)}`, )}`,
); );
} }
if (file.download) {
res.set(
'Content-Disposition',
`attachment; filename*=UTF-8''${encodeURIComponent(
file.displayName || file.name,
)}`,
);
}
res.set('Content-Type', 'text/plain; charset=utf-8').send( res.set('Content-Type', 'text/plain; charset=utf-8').send(
output ?? '', output ?? '',
); );

View File

@@ -3,6 +3,7 @@ import $ from '@/core/app';
import migrate from '@/utils/migration'; import migrate from '@/utils/migration';
import download from '@/utils/download'; import download from '@/utils/download';
import { syncArtifacts } from '@/restful/sync'; import { syncArtifacts } from '@/restful/sync';
import { gistBackupAction } from '@/restful/miscs';
import registerSubscriptionRoutes from './subscriptions'; import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections'; import registerCollectionRoutes from './collections';
@@ -44,20 +45,81 @@ export default function serve() {
$app.start(); $app.start();
if ($.env.isNode) { if ($.env.isNode) {
const backend_cron = eval('process.env.SUB_STORE_BACKEND_CRON'); // Deprecated: SUB_STORE_BACKEND_CRON
if (backend_cron) { const backend_sync_cron =
$.info(`[CRON] ${backend_cron} enabled`); eval('process.env.SUB_STORE_BACKEND_SYNC_CRON') ||
eval('process.env.SUB_STORE_BACKEND_CRON');
if (backend_sync_cron) {
$.info(`[SYNC CRON] ${backend_sync_cron} enabled`);
const { CronJob } = eval(`require("cron")`); const { CronJob } = eval(`require("cron")`);
new CronJob( new CronJob(
backend_cron, backend_sync_cron,
async function () { async function () {
try { try {
$.info(`[CRON] ${backend_cron} started`); $.info(`[SYNC CRON] ${backend_sync_cron} started`);
await syncArtifacts(); await syncArtifacts();
$.info(`[CRON] ${backend_cron} finished`); $.info(`[SYNC CRON] ${backend_sync_cron} finished`);
} catch (e) { } catch (e) {
$.error( $.error(
`[CRON] ${backend_cron} error: ${e.message ?? e}`, `[SYNC CRON] ${backend_sync_cron} error: ${
e.message ?? e
}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
}
const backend_download_cron = eval(
'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON',
);
if (backend_download_cron) {
$.info(`[DOWNLOAD CRON] ${backend_download_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
new CronJob(
backend_download_cron,
async function () {
try {
$.info(
`[DOWNLOAD CRON] ${backend_download_cron} started`,
);
await gistBackupAction('download');
$.info(
`[DOWNLOAD CRON] ${backend_download_cron} finished`,
);
} catch (e) {
$.error(
`[DOWNLOAD CRON] ${backend_download_cron} error: ${
e.message ?? e
}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
}
const backend_upload_cron = eval(
'process.env.SUB_STORE_BACKEND_UPLOAD_CRON',
);
if (backend_upload_cron) {
$.info(`[UPLOAD CRON] ${backend_upload_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
new CronJob(
backend_upload_cron,
async function () {
try {
$.info(`[UPLOAD CRON] ${backend_upload_cron} started`);
await gistBackupAction('upload');
$.info(`[UPLOAD CRON] ${backend_upload_cron} finished`);
} catch (e) {
$.error(
`[UPLOAD CRON] ${backend_upload_cron} error: ${
e.message ?? e
}`,
); );
} }
}, // onTick }, // onTick

View File

@@ -80,10 +80,72 @@ async function refresh(_, res) {
success(res); success(res);
} }
async function gistBackupAction(action) {
// read token
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY);
if (!gistToken) throw new Error('GitHub Token is required for backup!');
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY,
syncPlatform,
});
let content;
const settings = $.read(SETTINGS_KEY);
const updated = settings.syncTime;
switch (action) {
case 'upload':
// update syncTime
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode) content = JSON.stringify($.cache, null, ` `);
$.info(`上传备份中...`);
try {
await gist.upload({
[GIST_BACKUP_FILE_NAME]: { content },
});
$.info(`上传备份完成`);
} catch (err) {
// restore syncTime if upload failed
settings.syncTime = updated;
$.write(settings, SETTINGS_KEY);
throw err;
}
break;
case 'download':
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
try {
if (Object.keys(JSON.parse(content).settings).length === 0) {
throw new Error('备份文件应该至少包含 settings 字段');
}
} catch (err) {
$.error(
`Gist 备份文件校验失败, 无法还原\nReason: ${
err.message ?? err
}`,
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
// restore settings
$.write(content, '#sub-store');
if ($.env.isNode) {
content = JSON.parse(content);
$.cache = content;
$.persistCache();
}
$.info(`perform migration after restoring from gist...`);
migrate();
$.info(`migration completed`);
$.info(`还原备份完成`);
break;
}
}
async function gistBackup(req, res) { async function gistBackup(req, res) {
const { action } = req.query; const { action } = req.query;
// read token // read token
const { gistToken, syncPlatform } = $.read(SETTINGS_KEY); const { gistToken } = $.read(SETTINGS_KEY);
if (!gistToken) { if (!gistToken) {
failed( failed(
res, res,
@@ -93,68 +155,8 @@ async function gistBackup(req, res) {
), ),
); );
} else { } else {
const gist = new Gist({
token: gistToken,
key: GIST_BACKUP_KEY,
syncPlatform,
});
try { try {
let content; await gistBackupAction(action);
const settings = $.read(SETTINGS_KEY);
const updated = settings.syncTime;
switch (action) {
case 'upload':
// update syncTime
settings.syncTime = new Date().getTime();
$.write(settings, SETTINGS_KEY);
content = $.read('#sub-store');
if ($.env.isNode)
content = JSON.stringify($.cache, null, ` `);
$.info(`上传备份中...`);
try {
await gist.upload({
[GIST_BACKUP_FILE_NAME]: { content },
});
} catch (err) {
// restore syncTime if upload failed
settings.syncTime = updated;
$.write(settings, SETTINGS_KEY);
throw err;
}
break;
case 'download':
$.info(`还原备份中...`);
content = await gist.download(GIST_BACKUP_FILE_NAME);
try {
if (
Object.keys(JSON.parse(content).settings).length ===
0
) {
throw new Error(
'备份文件应该至少包含 settings 字段',
);
}
} catch (err) {
$.error(
`Gist 备份文件校验失败, 无法还原\nReason: ${
err.message ?? err
}`,
);
throw new Error('Gist 备份文件校验失败, 无法还原');
}
// restore settings
$.write(content, '#sub-store');
if ($.env.isNode) {
content = JSON.parse(content);
$.cache = content;
$.persistCache();
}
$.info(`perform migration after restoring from gist...`);
migrate();
$.info(`migration completed`);
$.info(`还原备份完成`);
break;
}
success(res); success(res);
} catch (err) { } catch (err) {
$.error( $.error(
@@ -171,3 +173,5 @@ async function gistBackup(req, res) {
} }
} }
} }
export { gistBackupAction };

View File

@@ -146,7 +146,8 @@ async function compareSub(req, res) {
// add id // add id
original.forEach((proxy, i) => { original.forEach((proxy, i) => {
proxy.id = i; proxy.id = i;
proxy.subName = sub.name; proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
}); });
// apply processors // apply processors
@@ -237,8 +238,10 @@ async function compareCollection(req, res) {
.flat(); .flat();
currentProxies.forEach((proxy) => { currentProxies.forEach((proxy) => {
proxy.subName = sub.name; proxy._subName = sub.name;
proxy.collectionName = collection.name; proxy._subDisplayName = sub.displayName;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
}); });
// apply processors // apply processors
@@ -276,7 +279,8 @@ async function compareCollection(req, res) {
original.forEach((proxy, i) => { original.forEach((proxy, i) => {
proxy.id = i; proxy.id = i;
proxy.collectionName = collection.name; proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
}); });
const processed = await ProxyUtils.process( const processed = await ProxyUtils.process(

View File

@@ -37,6 +37,8 @@ async function produceArtifact({
produceOpts = {}, produceOpts = {},
subscription, subscription,
awaitCustomCache, awaitCustomCache,
$options,
proxy,
}) { }) {
platform = platform || 'JSON'; platform = platform || 'JSON';
@@ -67,7 +69,7 @@ async function produceArtifact({
url, url,
ua || sub.ua, ua || sub.ua,
undefined, undefined,
sub.proxy, proxy || sub.proxy,
undefined, undefined,
awaitCustomCache, awaitCustomCache,
); );
@@ -114,7 +116,7 @@ async function produceArtifact({
url, url,
ua || sub.ua, ua || sub.ua,
undefined, undefined,
sub.proxy, proxy || sub.proxy,
undefined, undefined,
awaitCustomCache, awaitCustomCache,
); );
@@ -150,7 +152,8 @@ async function produceArtifact({
.flat(); .flat();
proxies.forEach((proxy) => { proxies.forEach((proxy) => {
proxy.subName = sub.name; proxy._subName = sub.name;
proxy._subDisplayName = sub.displayName;
}); });
// apply processors // apply processors
proxies = await ProxyUtils.process( proxies = await ProxyUtils.process(
@@ -158,6 +161,7 @@ async function produceArtifact({
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub }, { [sub.name]: sub },
$options,
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`订阅 ${name} 中不含有效节点`); throw new Error(`订阅 ${name} 中不含有效节点`);
@@ -217,7 +221,9 @@ async function produceArtifact({
url, url,
sub.ua, sub.ua,
undefined, undefined,
sub.proxy, proxy ||
sub.proxy ||
collection.proxy,
); );
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
@@ -250,8 +256,10 @@ async function produceArtifact({
.flat(); .flat();
currentProxies.forEach((proxy) => { currentProxies.forEach((proxy) => {
proxy.subName = sub.name; proxy._subName = sub.name;
proxy.collectionName = collection.name; proxy._subDisplayName = sub.displayName;
proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
}); });
// apply processors // apply processors
@@ -259,7 +267,11 @@ async function produceArtifact({
currentProxies, currentProxies,
sub.process || [], sub.process || [],
platform, platform,
{ [sub.name]: sub, _collection: collection }, {
[sub.name]: sub,
_collection: collection,
$options,
},
); );
results[name] = currentProxies; results[name] = currentProxies;
processed++; processed++;
@@ -303,7 +315,8 @@ async function produceArtifact({
); );
proxies.forEach((proxy) => { proxies.forEach((proxy) => {
proxy.collectionName = collection.name; proxy._collectionName = collection.name;
proxy._collectionDisplayName = collection.displayName;
}); });
// apply own processors // apply own processors
@@ -312,6 +325,7 @@ async function produceArtifact({
collection.process || [], collection.process || [],
platform, platform,
{ _collection: collection }, { _collection: collection },
$options,
); );
if (proxies.length === 0) { if (proxies.length === 0) {
throw new Error(`组合订阅 ${name} 中不含有效节点`); throw new Error(`组合订阅 ${name} 中不含有效节点`);
@@ -378,7 +392,12 @@ async function produceArtifact({
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, ua || file.ua); return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@@ -421,7 +440,12 @@ async function produceArtifact({
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, ua || file.ua); return await download(
url,
ua || file.ua,
undefined,
file.proxy || proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@@ -460,10 +484,10 @@ async function produceArtifact({
const processed = const processed =
Array.isArray(file.process) && file.process.length > 0 Array.isArray(file.process) && file.process.length > 0
? await ProxyUtils.process( ? await ProxyUtils.process(
{ $files: files, $content: filesContent }, { $files: files, $content: filesContent, $options },
file.process, file.process,
) )
: { $content: filesContent, $files: files }; : { $content: filesContent, $files: files, $options };
return processed?.$content ?? ''; return processed?.$content ?? '';
} }

View File

@@ -18,7 +18,7 @@ export default async function download(
rawUrl = '', rawUrl = '',
ua, ua,
timeout, timeout,
proxy, customProxy,
skipCustomCache, skipCustomCache,
awaitCustomCache, awaitCustomCache,
) { ) {
@@ -43,12 +43,21 @@ export default async function download(
} }
} }
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV(); const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultUserAgent, defaultTimeout, cacheThreshold } = const { defaultProxy, defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY); $.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy = proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = ua || defaultUserAgent || 'clash.meta'; const userAgent = ua || defaultUserAgent || 'clash.meta';
const requestTimeout = timeout || defaultTimeout; const requestTimeout = timeout || defaultTimeout;
const id = hex_md5(userAgent + url); const id = hex_md5(userAgent + url);
if ($arguments?.cacheKey === true) {
$.error(`使用自定义缓存时 cacheKey 的值不能为空`);
$arguments.cacheKey = undefined;
}
const customCacheKey = $arguments?.cacheKey const customCacheKey = $arguments?.cacheKey
? `#sub-store-cached-custom-${$arguments?.cacheKey}` ? `#sub-store-cached-custom-${$arguments?.cacheKey}`
: undefined; : undefined;

View File

@@ -10,7 +10,13 @@ export function getFlowField(headers) {
)[0]; )[0];
return headers[subkey]; return headers[subkey];
} }
export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) { export async function getFlowHeaders(
rawUrl,
ua,
timeout,
customProxy,
flowUrl,
) {
let url = flowUrl || rawUrl || ''; let url = flowUrl || rawUrl || '';
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');
@@ -41,7 +47,13 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
// $.info(`使用缓存的流量信息: ${url}`); // $.info(`使用缓存的流量信息: ${url}`);
flowInfo = cached; flowInfo = cached;
} else { } else {
const { defaultFlowUserAgent, defaultTimeout } = $.read(SETTINGS_KEY); const { defaultProxy, defaultFlowUserAgent, defaultTimeout } =
$.read(SETTINGS_KEY);
let proxy = customProxy || defaultProxy;
if ($.env.isNode) {
proxy =
proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
const userAgent = const userAgent =
ua || ua ||
defaultFlowUserAgent || defaultFlowUserAgent ||
@@ -67,7 +79,7 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
$.info( $.info(
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${ `使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
userAgent || '' userAgent || ''
}`, }, Proxy: ${proxy}`,
); );
const { headers } = await http.head({ const { headers } = await http.head({
url: url url: url
@@ -97,14 +109,14 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
$.error( $.error(
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${ `使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
userAgent || '' userAgent || ''
}: ${e.message ?? e}`, }, Proxy: ${proxy}: ${e.message ?? e}`,
); );
} }
if (!flowInfo) { if (!flowInfo) {
$.info( $.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${ `使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || '' userAgent || ''
}`, }, Proxy: ${proxy}`,
); );
const { headers } = await http.get({ const { headers } = await http.get({
url: url url: url
@@ -113,8 +125,21 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
.filter((i) => i.length)[0], .filter((i) => i.length)[0],
headers: { headers: {
'User-Agent': userAgent, 'User-Agent': userAgent,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
}, },
timeout: requestTimeout, timeout: requestTimeout,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
}); });
flowInfo = getFlowField(headers); flowInfo = getFlowField(headers);
} }

View File

@@ -1,10 +1,21 @@
import { HTTP } from '@/vendor/open-api'; import { HTTP, ENV } from '@/vendor/open-api';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app';
import { SETTINGS_KEY } from '@/constants';
/** /**
* Gist backup * Gist backup
*/ */
export default class Gist { export default class Gist {
constructor({ token, key, syncPlatform }) { constructor({ token, key, syncPlatform }) {
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultProxy, defaultTimeout: timeout } = $.read(SETTINGS_KEY);
let proxy = defaultProxy;
if ($.env.isNode) {
proxy =
proxy || eval('process.env.SUB_STORE_BACKEND_DEFAULT_PROXY');
}
if (syncPlatform === 'gitlab') { if (syncPlatform === 'gitlab') {
this.headers = { this.headers = {
'PRIVATE-TOKEN': `${token}`, 'PRIVATE-TOKEN': `${token}`,
@@ -13,7 +24,25 @@ export default class Gist {
}; };
this.http = HTTP({ this.http = HTTP({
baseURL: 'https://gitlab.com/api/v4', baseURL: 'https://gitlab.com/api/v4',
headers: { ...this.headers }, headers: {
...this.headers,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
timeout,
events: { events: {
onResponse: (resp) => { onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) { if (/^[45]/.test(String(resp.statusCode))) {
@@ -35,7 +64,25 @@ export default class Gist {
}; };
this.http = HTTP({ this.http = HTTP({
baseURL: 'https://api.github.com', baseURL: 'https://api.github.com',
headers: { ...this.headers }, headers: {
...this.headers,
...(isStash && proxy
? {
'X-Stash-Selected-Proxy':
encodeURIComponent(proxy),
}
: {}),
...(isShadowRocket && proxy
? { 'X-Surge-Policy': proxy }
: {}),
},
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
timeout,
events: { events: {
onResponse: (resp) => { onResponse: (resp) => {
if (/^[45]/.test(String(resp.statusCode))) { if (/^[45]/.test(String(resp.statusCode))) {

View File

@@ -111,6 +111,12 @@ function getRandomPort(portString) {
} }
} }
function numberToString(value) {
return Number.isSafeInteger(value)
? String(value)
: BigInt(value).toString();
}
export { export {
ipAddress, ipAddress,
isIPv4, isIPv4,
@@ -123,4 +129,5 @@ export {
// utf8ArrayToStr, // utf8ArrayToStr,
getPolicyDescriptor, getPolicyDescriptor,
getRandomPort, getRandomPort,
numberToString,
}; };

View File

@@ -1,10 +1,10 @@
name: Sub-Store name: Sub-Store
description: '支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *' description: "支持 Surge 正式版的参数设置功能. 测落地功能 ability: http-client-policy, 同步配置的定时 cronexp: 55 23 * * *"
compat_arguments: compat_arguments:
ability: http-client-policy ability: http-client-policy
cronexp: 55 23 * * * cronexp: 55 23 * * *
sync: '"Sub-Store Sync"' sync: '"Sub-Store Sync"'
timeout: '120' timeout: "120"
engine: auto engine: auto
produce: '"# Sub-Store Produce"' produce: '"# Sub-Store Produce"'
produce_cronexp: 50 */6 * * * produce_cronexp: 50 */6 * * *
@@ -12,26 +12,26 @@ compat_arguments:
produce_col: '"col1,col2"' produce_col: '"col1,col2"'
compat_arguments_desc: '\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 ''同步'' 或 ''同步配置''\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n脚本超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅' compat_arguments_desc: '\n1⃣ ability\n\n默认已开启测落地能力\n需要配合脚本操作\n如 https://raw.githubusercontent.com/Keywos/rule/main/cname.js\n填写任意其他值关闭\n\n2⃣ cronexp\n\n同步配置定时任务\n默认为每天 23 点 55 分\n\n定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 ''同步'' 或 ''同步配置''\n\n3⃣ sync\n\n自定义定时任务名\n便于在脚本编辑器中选择\n若设为 # 可取消定时任务\n\n4⃣ timeout\n\n脚本超时, 单位为秒\n\n5⃣ engine\n\n默认为自动使用 webview 引擎, 可设为指定 jsc, 但 jsc 容易爆内存\n\n6⃣ produce\n\n自定义处理订阅的定时任务名\n一般用于定时处理耗时较长的订阅, 以更新缓存\n这样 Surge 中拉取的时候就能用到缓存, 不至于总是超时\n若设为 # 可取消此定时任务\n默认不开启\n\n7⃣ produce_cronexp\n\n配置处理订阅的定时任务\n\n默认为每 6 小时\n\n9⃣ produce_sub\n\n自定义需定时处理的单条订阅名\n多个用 , 连接\n\n🔟 produce_col\n\n自定义需定时处理的组合订阅名\n多个用 , 连接\n\n⚠ 注意: 是 名称(name) 不是 显示名称(displayName)\n如果名称需要编码, 请编码后再用 , 连接\n顺序: 并发执行单条订阅, 然后并发执行组合订阅'
scriptings: scriptings:
- http_request: - http_request:
name: Sub-Store Core name: Sub-Store Core
match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) match: ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info)))
script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
body_required: true body_required: true
- http_request: - http_request:
name: Sub-Store Simple name: Sub-Store Simple
match: ^https?:\/\/sub\.store match: ^https?:\/\/sub\.store
script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
body_required: true body_required: true
- schedule: - schedule:
name: '{{{sync}}}' name: "{{{sync}}}"
cron: '{{{cronexp}}}' cron: "{{{cronexp}}}"
script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
- schedule: - schedule:
name: '{{{produce}}}' name: "{{{produce}}}"
cron: '{{{produce_cronexp}}}' cron: "{{{produce_cronexp}}}"
script_url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js script_url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
arguments: arguments:
_compat.$argument: '"sub={{{produce_sub}}}&col={{{produce_col}}}"' _compat.$argument: '"sub={{{produce_sub}}}&col={{{produce_col}}}"'
mitm: mitm:
hostnames: hostnames:
- sub.store - sub.store

View File

@@ -14,7 +14,7 @@ DOMAIN,sub-store.vercel.app,PROXY
hostname=sub.store hostname=sub.store
[Script] [Script]
http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core http-request ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js, requires-body=true, timeout=120, tag=Sub-Store Core
http-request ^https?:\/\/sub\.store script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple http-request ^https?:\/\/sub\.store script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js, requires-body=true, timeout=120, tag=Sub-Store Simple
cron "55 23 * * *" script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync cron "55 23 * * *" script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js, timeout=120, tag=Sub-Store Sync

View File

@@ -2,6 +2,6 @@
"name": "Sub-Store", "name": "Sub-Store",
"description": "定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'", "description": "定时任务默认为每天 23 点 55 分. 定时任务指定时将订阅/文件上传到私有 Gist. 在前端, 叫做 '同步' 或 '同步配置'",
"task": [ "task": [
"55 23 * * * https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png" "55 23 * * * https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js, tag=Sub-Store Sync, img-url=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png"
] ]
} }

View File

@@ -1,4 +1,4 @@
hostname=sub.store hostname=sub.store
^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js ^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))) url script-analyze-echo-response https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
^https?:\/\/sub\.store url script-analyze-echo-response https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js ^https?:\/\/sub\.store url script-analyze-echo-response https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js

View File

@@ -25,13 +25,13 @@ cron:
script-providers: script-providers:
sub-store-0: sub-store-0:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js
interval: 86400 interval: 86400
sub-store-1: sub-store-1:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js
interval: 86400 interval: 86400
cron-sync-artifacts: cron-sync-artifacts:
url: https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js url: https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js
interval: 86400 interval: 86400

View File

@@ -8,10 +8,10 @@
hostname = %APPEND% sub.store hostname = %APPEND% sub.store
[Script] [Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}} Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}}
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}} Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}} {{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}}
{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}" {{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"

View File

@@ -7,7 +7,7 @@ hostname = %APPEND% sub.store
[Script] [Script]
# 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 则可以使用此脚本 # 主程序 已经去掉 Sub-Store Core 的参数 [,ability=http-client-policy] 不会爆内存,这个参数在 Surge 非常占用内存; 如果不需要使用指定节点功能 例如[加旗帜脚本或者cname脚本] 则可以使用此脚本
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120 Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout=120
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120 Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js

View File

@@ -6,7 +6,7 @@
hostname = %APPEND% sub.store hostname = %APPEND% sub.store
[Script] [Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout=120,ability=http-client-policy
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout=120 Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout=120
Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js Sub-Store Sync=type=cron,cronexp=55 23 * * *,wake-system=1,timeout=120,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js

View File

@@ -8,10 +8,10 @@
hostname = %APPEND% sub.store hostname = %APPEND% sub.store
[Script] [Script]
Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}} Sub-Store Core=type=http-request,pattern=^https?:\/\/sub\.store\/((download)|api\/(preview|sync|(utils\/node-info))),script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-1.min.js,requires-body=true,timeout={{{timeout}}},ability="{{{ability}}}",engine={{{engine}}}
Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}} Sub-Store Simple=type=http-request,pattern=^https?:\/\/sub\.store,script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/sub-store-0.min.js,requires-body=true,timeout={{{timeout}}},engine={{{engine}}}
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}} {{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}}
{{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}" {{{produce}}}=type=cron,cronexp="{{{produce_cronexp}}}",wake-system=1,timeout={{{timeout}}},script-path=https://raw.githubusercontent.com/sub-store-org/Sub-Store/release/cron-sync-artifacts.min.js,engine={{{engine}}},argument="sub={{{produce_sub}}}&col={{{produce_col}}}"

View File

@@ -10,19 +10,33 @@ function operator(proxies = [], targetPlatform, context) {
// 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功 // 2. 域名解析后 会多一个 `_resolved` 字段, 表示是否解析成功
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_IP4P`(若解析类型为 IPv6 且符合 IP4P 类型, 将自动转换), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP // 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_IP4P`(若解析类型为 IPv6 且符合 IP4P 类型, 将自动转换), `_domain` 字段, `_resolved_ips` 为解析出的所有 IP
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置) // 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
// 5. `_subName` 为单条订阅名 // 5. `_subName` 为单条订阅名, `_subDisplayName` 为单条订阅显示名
// 6. `_collectionName` 为组合订阅名 // 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名
// 7. `tls-fingerprint` 为 tls 指纹 // 7. `tls-fingerprint` 为 tls 指纹
// 8. `underlying-proxy` 为前置代理 // 8. `underlying-proxy` 为前置代理
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除 // 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
// 10. `sni` 在某些协议里会自动与 `servername` 转换 // 10. `sni` 在某些协议里会自动与 `servername` 转换
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512) // 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
// 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down` // 12. 以 Surge 为例, 最新的参数一般我都会跟进, 以 Surge 文档为例, 一些常用的: TUIC/Hysteria 2 的 `ecn`, Snell 的 `reuse` 连接复用, QUIC 策略 block-quic`, Hysteria 2 下载带宽 `down`
// // 13. `test-url` 为测延迟链接, `test-timeout` 为测延迟超时
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009 // 14. `ports` 为端口跳跃, `hop-interval` 变换端口号的时间间隔
// 15. `ip-version` 设置节点使用 IP 版本可选dualipv4ipv6ipv4-preferipv6-prefer. 会进行内部转换, 若无法匹配则使用原始值
// require 为 Node.js 的 require, 在 Node.js 运行环境下 可以用来引入模块
// $arguments 为传入的脚本参数 // $arguments 为传入的脚本参数
// $options 为通过链接传入的参数
// 例如: { arg1: 'a', arg2: 'b' }
// 可这样传:
// 先这样处理 encodeURIComponent(JSON.stringify({ arg1: 'a', arg2: 'b' }))
// /api/file/foo?$options=%7B%22arg1%22%3A%22a%22%2C%22arg2%22%3A%22b%22%7D
// 或这样传:
// 先这样处理 encodeURIComponent('arg1=a&arg2=b')
// /api/file/foo?$options=arg1%3Da%26arg2%3Db
// console.log($options)
// targetPlatform 为输出的目标平台 // targetPlatform 为输出的目标平台
// lodash // lodash
@@ -52,8 +66,11 @@ function operator(proxies = [], targetPlatform, context) {
// removeFlag, // 移除 emoji 旗帜 // removeFlag, // 移除 emoji 旗帜
// getISO, // 获取 ISO 3166-1 alpha-2 代码 // getISO, // 获取 ISO 3166-1 alpha-2 代码
// Gist, // Gist 类 // Gist, // Gist 类
// download, // 内部的下载方法, 见 backend/src/utils/download.js
// } // }
// 如果只是为了快速修改或者筛选 可以参考 脚本操作支持节点快捷脚本 https://t.me/zhetengsha/970 和 脚本筛选支持节点快捷脚本 https://t.me/zhetengsha/1009
// ⚠️ 注意: 函数式(即本文件这样的 function operator() {}) 和快捷操作(下面使用 $server) 只能二选一
// 示例: 给节点名添加前缀 // 示例: 给节点名添加前缀
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}` // $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
// 示例: 给节点名添加旗帜 // 示例: 给节点名添加旗帜
@@ -131,7 +148,7 @@ function operator(proxies = [], targetPlatform, context) {
// yaml.proxies.unshift(...clashMetaProxies) // yaml.proxies.unshift(...clashMetaProxies)
// $content = ProxyUtils.yaml.dump(yaml) // $content = ProxyUtils.yaml.dump(yaml)
// { $content, $files } will be passed to the next operator // { $content, $files, $options } will be passed to the next operator
// $content is the final content of the file // $content is the final content of the file
// flowUtils 为机场订阅流量信息处理工具 // flowUtils 为机场订阅流量信息处理工具
@@ -139,7 +156,7 @@ function operator(proxies = [], targetPlatform, context) {
// 1. https://t.me/zhetengsha/948 // 1. https://t.me/zhetengsha/948
// context 为传入的上下文 // context 为传入的上下文
// 有三种情况, 按需判断 // 其中 source 为 订阅和组合订阅的数据, 有三种情况, 按需判断 (若只需要取订阅/组合订阅名称 直接用 `_subName` `_subDisplayName` `_collectionName` `_collectionDisplayName` 即可)
// 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上 // 若存在 `source._collection` 且 `source._collection.subscriptions` 中的 key 在 `source` 上也存在, 说明输出结果为组合订阅, 但是脚本设置在单条订阅上