Compare commits

...

27 Commits

Author SHA1 Message Date
xream
c27c589024 chore: 调整部分日志 2024-03-25 02:47:06 +08:00
xream
0efed4f1a0 feat: 处理传入 httpClient 的 timeout 参数 2024-03-24 07:28:16 +08:00
xream
e3a514d1fb feat: hysteria2 支持 mport, clash.meta(mihomo) 支持 ports 2024-03-21 20:07:24 +08:00
xream
64478c7a27 feat: 刷新时 清除所有缓存 2024-03-21 02:37:21 +08:00
xream
dc8f19f350 doc: demo.js 2024-03-21 01:52:42 +08:00
xream
b4ccfc7e07 chore: Sub-Store Simple 脚本增加脚本超时(默认 120) 可能会影响某些逻辑 待观察 2024-03-19 23:48:26 +08:00
xream
3f1940630a chore: 增加 gist 错误日志 2024-03-19 21:30:52 +08:00
xream
5a0bdb1276 doc: demo.js 2024-03-18 20:24:18 +08:00
xream
a1b86e26a2 chore: 增加上传同步配置的详细日志 2024-03-16 02:47:01 +08:00
xream
6ec8c29f6a feat: 规则中处理 GEOIP/GEOSITE, Loon 已支持 SRC-PORT/DEST-PORT/PROTOCOL 2024-03-15 08:49:36 +08:00
xream
bbb9602f9f release: backend version 2.14.256 2024-03-15 08:12:06 +08:00
xream
6db6153672 Merge pull request #295 from makaspacex/master
[Feat]规则转换增加对GEOIP与GEOSITE的支持
2024-03-15 08:10:30 +08:00
makabaka
b66189948a 规则转换增加对GEOIP与GEOSITE的支持 2024-03-14 22:07:45 +08:00
xream
2611dccc73 feat: 支持设置查询远程订阅流量信息时的 User-Agent 2024-03-14 19:45:39 +08:00
xream
25d3cf6ca4 feat: 通过代理/节点/策略获取订阅 现已支持 Surge, Loon, Stash, Shadowrocket, QX, Node.js 2024-03-14 01:54:07 +08:00
xream
3637c5eb74 feat: SSH 协议跟进 clash.meta(mihomo) 的修改 2024-03-13 16:24:30 +08:00
xream
80d46597b4 feat: 支持使用代理/节点/策略获取订阅 2024-03-13 05:33:52 +08:00
xream
ca65e4209e feat: 支持自定义订阅流量信息 2024-03-12 01:17:56 +08:00
xream
53bb4866e7 fix: 修复订阅流量传递 2024-03-12 00:55:30 +08:00
xream
09495fa607 fix: 修复重置天数微妙的偏差 2024-03-11 19:33:51 +08:00
xream
4b27d40602 feat: 订阅支持开始日期和重置周期 2024-03-11 13:39:52 +08:00
xream
518de2e919 feat: 订阅支持每月重置天数 2024-03-10 23:08:56 +08:00
xream
078bf228de feat: produceArtifact 方法支持传入自定义 subscription; VLESS 非 reality 删除空 flow 2024-03-10 17:22:25 +08:00
xream
aaef97cf5d feat: SSH 新增 clash.meta(mihomo), 调整 Surge 和 sing-box 2024-03-08 19:01:01 +08:00
xream
7beff4013f feat: 订阅列表的流量信息兼容远程和本地合并的情况, 排除设置了不查询订阅信息的链接 2024-03-08 18:40:44 +08:00
xream
23cf81d0a5 feat: Node.js 版 /api/utils/env 增加 meta 信息 2024-03-08 14:20:55 +08:00
xream
572f2f5533 feat: OpenAPI 增加 isEgern, isLanceX; /api/utils/env 增加 meta 信息 2024-03-08 13:56:59 +08:00
28 changed files with 563 additions and 101 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "sub-store", "name": "sub-store",
"version": "2.14.240", "version": "2.14.264",
"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

@@ -159,7 +159,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
continue; continue;
} }
$.info( $.log(
`Applying "${item.type}" with arguments:\n >>> ${ `Applying "${item.type}" with arguments:\n >>> ${
JSON.stringify(item.args, null, 2) || 'None' JSON.stringify(item.args, null, 2) || 'None'
}`, }`,
@@ -199,7 +199,7 @@ function produce(proxies, targetPlatform, type, opts = {}) {
return proxy; return proxy;
}); });
$.info(`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 localPort = 10000;
const list = proxies const list = proxies
@@ -370,6 +370,10 @@ function lastParse(proxy) {
delete proxy.ports; delete proxy.ports;
} }
if (['vless'].includes(proxy.type)) { if (['vless'].includes(proxy.type)) {
// 非 reality, 空 flow 没有意义
if (!proxy['reality-opts'] && !proxy.flow) {
delete proxy.flow;
}
if (['http'].includes(proxy.network)) { if (['http'].includes(proxy.network)) {
let transportPath = proxy[`${proxy.network}-opts`]?.path; let transportPath = proxy[`${proxy.network}-opts`]?.path;
if (!transportPath) { if (!transportPath) {

View File

@@ -444,6 +444,7 @@ function URI_VLESS() {
proxy[`${params.security}-opts`] = opts; proxy[`${params.security}-opts`] = opts;
} }
} }
proxy.network = params.type; proxy.network = params.type;
if (proxy.network === 'tcp' && params.headerType === 'http') { if (proxy.network === 'tcp' && params.headerType === 'http') {
proxy.network = 'http'; proxy.network = 'http';
@@ -546,6 +547,7 @@ function URI_Hysteria2() {
proxy.obfs = params.obfs; proxy.obfs = params.obfs;
} }
proxy.ports = params.mport;
proxy['obfs-password'] = params['obfs-password']; proxy['obfs-password'] = params['obfs-password'];
proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure); proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.insecure);
proxy.tfo = /(TRUE)|1/i.test(params.fastopen); proxy.tfo = /(TRUE)|1/i.test(params.fastopen);
@@ -722,6 +724,7 @@ function Clash_All() {
'hysteria', 'hysteria',
'hysteria2', 'hysteria2',
'wireguard', 'wireguard',
'ssh',
].includes(proxy.type) ].includes(proxy.type)
) { ) {
throw new Error( throw new Error(

View File

@@ -77,7 +77,7 @@ http = tag equals "http" address (username password)? (usernamek passwordk)? (ip
proxy.type = "http"; proxy.type = "http";
handleShadowTLS(); handleShadowTLS();
} }
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ssh"; proxy.type = "ssh";
handleShadowTLS(); handleShadowTLS();
} }
@@ -229,6 +229,7 @@ interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; } allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; } hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); } idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); } server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); } block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); } shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }

View File

@@ -75,7 +75,7 @@ http = tag equals "http" address (username password)? (usernamek passwordk)? (ip
proxy.type = "http"; proxy.type = "http";
handleShadowTLS(); handleShadowTLS();
} }
ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* { ssh = tag equals "ssh" address (username password)? (usernamek passwordk)? (server_fingerprint/idle_timeout/private_key/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
proxy.type = "ssh"; proxy.type = "ssh";
handleShadowTLS(); handleShadowTLS();
} }
@@ -227,6 +227,7 @@ interface = comma "interface" equals match:[^,]+ { proxy.interface = match.join(
allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; } allow_other_interface = comma "allow-other-interface" equals flag:bool { proxy["allow-other-interface"] = flag; }
hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; } hybrid = comma "hybrid" equals flag:bool { proxy.hybrid = flag; }
idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); } idle_timeout = comma "idle-timeout" equals match:$[0-9]+ { proxy["idle-timeout"] = parseInt(match.trim()); }
private_key = comma "private-key" equals match:[^,]+ { proxy["keystore-private-key"] = match.join("").replace(/^"(.*)"$/, '$1'); }
server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); } server_fingerprint = comma "server-fingerprint" equals match:[^,]+ { proxy["server-fingerprint"] = match.join("").replace(/^"(.*)"$/, '$1'); }
block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); } block_quic = comma "block-quic" equals match:[^,]+ { proxy["block-quic"] = match.join(""); }
shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); } shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["shadow-tls-version"] = parseInt(match.trim()); }

View File

@@ -16,6 +16,7 @@ import {
parseFlowHeaders, parseFlowHeaders,
validCheck, validCheck,
flowTransfer, flowTransfer,
getRmainingDays,
} from '@/utils/flow'; } from '@/utils/flow';
/** /**
@@ -863,6 +864,7 @@ function createDynamicFunction(name, script, $arguments) {
parseFlowHeaders, parseFlowHeaders,
flowTransfer, flowTransfer,
validCheck, validCheck,
getRmainingDays,
}; };
if ($.env.isLoon) { if ($.env.isLoon) {
return new Function( return new Function(

View File

@@ -228,6 +228,13 @@ const sshParser = (proxy = {}) => {
throw 'invalid port'; throw 'invalid port';
if (proxy.username) parsedProxy.user = proxy.username; if (proxy.username) parsedProxy.user = proxy.username;
if (proxy.password) parsedProxy.password = proxy.password; if (proxy.password) parsedProxy.password = proxy.password;
// https://wiki.metacubex.one/config/proxies/ssh
// https://sing-box.sagernet.org/zh/configuration/outbound/ssh
if (proxy['privateKey']) parsedProxy.private_key_path = proxy['privateKey'];
if (proxy['private-key'])
parsedProxy.private_key_path = proxy['private-key'];
if (proxy['private-key-passphrase'])
parsedProxy.private_key_passphrase = proxy['private-key-passphrase'];
if (proxy['server-fingerprint']) { if (proxy['server-fingerprint']) {
parsedProxy.host_key = [proxy['server-fingerprint']]; parsedProxy.host_key = [proxy['server-fingerprint']];
// https://manual.nssurge.com/policy/ssh.html // https://manual.nssurge.com/policy/ssh.html
@@ -237,6 +244,9 @@ const sshParser = (proxy = {}) => {
proxy['server-fingerprint'].split(' ')[0], proxy['server-fingerprint'].split(' ')[0],
]; ];
} }
if (proxy['host-key']) parsedProxy.host_key = proxy['host-key'];
if (proxy['host-key-algorithms'])
parsedProxy.host_key_algorithms = proxy['host-key-algorithms'];
if (proxy['fast-open']) parsedProxy.udp_fragment = true; if (proxy['fast-open']) parsedProxy.udp_fragment = true;
tfoParser(proxy, parsedProxy); tfoParser(proxy, parsedProxy);
return parsedProxy; return parsedProxy;

View File

@@ -356,8 +356,15 @@ function ssh(proxy) {
const result = new Result(proxy); const result = new Result(proxy);
result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`); result.append(`${proxy.name}=ssh,${proxy.server},${proxy.port}`);
result.appendIfPresent(`,${proxy.username}`, 'username'); result.appendIfPresent(`,${proxy.username}`, 'username');
// 所有的类似的字段都有双引号的问题 暂不处理
result.appendIfPresent(`,${proxy.password}`, 'password'); result.appendIfPresent(`,${proxy.password}`, 'password');
// https://manual.nssurge.com/policy/ssh.html
// 需配合 Keystore
result.appendIfPresent(
`,private-key=${proxy['keystore-private-key']}`,
'keystore-private-key',
);
result.appendIfPresent( result.appendIfPresent(
`,idle-timeout=${proxy['idle-timeout']}`, `,idle-timeout=${proxy['idle-timeout']}`,
'idle-timeout', 'idle-timeout',

View File

@@ -274,6 +274,9 @@ export default function URI_Producer() {
`sni=${encodeURIComponent(proxy.sni)}`, `sni=${encodeURIComponent(proxy.sni)}`,
); );
} }
if (proxy.ports) {
hysteria2params.push(`mport=${proxy.ports}`);
}
if (proxy['tls-fingerprint']) { if (proxy['tls-fingerprint']) {
hysteria2params.push( hysteria2params.push(
`pinSHA256=${encodeURIComponent( `pinSHA256=${encodeURIComponent(

View File

@@ -10,6 +10,8 @@ const RULE_TYPES_MAPPING = [
[/^PROTOCOL$/, 'PROTOCOL'], [/^PROTOCOL$/, 'PROTOCOL'],
[/^IP-CIDR$/i, 'IP-CIDR'], [/^IP-CIDR$/i, 'IP-CIDR'],
[/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'], [/^(IP-CIDR6|ip6-cidr|IP6-CIDR)$/, 'IP-CIDR6'],
[/^GEOIP$/i, 'GEOIP'],
[/^GEOSITE$/i, 'GEOSITE'],
]; ];
function AllRuleParser() { function AllRuleParser() {
@@ -37,8 +39,7 @@ function AllRuleParser() {
content: params[1], content: params[1],
}; };
if ( if (
rule.type === 'IP-CIDR' || ['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)
rule.type === 'IP-CIDR6'
) { ) {
rule.options = params.slice(2); rule.options = params.slice(2);
} }

View File

@@ -10,6 +10,8 @@ function QXFilter() {
'SRC-IP', 'SRC-IP',
'IN-PORT', 'IN-PORT',
'PROTOCOL', 'PROTOCOL',
'GEOSITE',
'GEOIP',
]; ];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
@@ -29,6 +31,8 @@ function QXFilter() {
function SurgeRuleSet() { function SurgeRuleSet() {
const type = 'SINGLE'; const type = 'SINGLE';
const func = (rule) => { const func = (rule) => {
const UNSUPPORTED = ['GEOSITE', 'GEOIP'];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
let output = `${rule.type},${rule.content}`; let output = `${rule.type},${rule.content}`;
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) { if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) {
output += output +=
@@ -43,7 +47,7 @@ function LoonRules() {
const type = 'SINGLE'; const type = 'SINGLE';
const func = (rule) => { const func = (rule) => {
// skip unsupported rules // skip unsupported rules
const UNSUPPORTED = ['DEST-PORT', 'SRC-IP', 'IN-PORT', 'PROTOCOL']; const UNSUPPORTED = ['SRC-IP', 'GEOSITE', 'GEOIP'];
if (UNSUPPORTED.indexOf(rule.type) !== -1) return null; if (UNSUPPORTED.indexOf(rule.type) !== -1) return null;
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type) && rule.options) { if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type) && rule.options) {
// Loon only supports the no-resolve option // Loon only supports the no-resolve option
@@ -69,7 +73,7 @@ function ClashRuleProvider() {
let output = `${TRANSFORM[rule.type] || rule.type},${ let output = `${TRANSFORM[rule.type] || rule.type},${
rule.content rule.content
}`; }`;
if (['IP-CIDR', 'IP-CIDR6'].includes(rule.type)) { if (['IP-CIDR', 'IP-CIDR6', 'GEOIP'].includes(rule.type)) {
if (rule.options) { if (rule.options) {
// Clash only supports the no-resolve option // Clash only supports the no-resolve option
rule.options = rule.options.filter((option) => rule.options = rule.options.filter((option) =>

View File

@@ -128,10 +128,19 @@ async function doSync() {
files.map((item) => [item.path, item]), files.map((item) => [item.path, item]),
); );
} }
const url = files[encodeURIComponent(artifact.name)]?.raw_url; const raw_url =
artifact.url = isGitLab files[encodeURIComponent(artifact.name)]?.raw_url;
? url const new_url = isGitLab
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); ? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
} }
} }

View File

@@ -77,12 +77,48 @@ async function downloadSubscription(req, res) {
}, },
}); });
if (sub.source !== 'local' || url) { if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try { try {
// forward flow headers url = `${url || sub.url}`
const flowInfo = await getFlowHeaders(url || sub.url); .split(/[\r\n]+/)
if (flowInfo) { .map((i) => i.trim())
res.set('subscription-userinfo', flowInfo); .filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(
decodeURIComponent(rawArgs[1]),
);
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if (!$arguments.noFlow) {
// forward flow headers
const flowInfo = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
} }
} catch (err) { } catch (err) {
$.error( $.error(
@@ -92,6 +128,9 @@ async function downloadSubscription(req, res) {
); );
} }
} }
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.subUserinfo);
}
if (platform === 'JSON') { if (platform === 'JSON') {
res.set('Content-Type', 'application/json;charset=utf-8').send( res.set('Content-Type', 'application/json;charset=utf-8').send(
@@ -176,11 +215,47 @@ async function downloadCollection(req, res) {
const subnames = collection.subscriptions; const subnames = collection.subscriptions;
if (subnames.length > 0) { if (subnames.length > 0) {
const sub = findByName(allSubs, subnames[0]); const sub = findByName(allSubs, subnames[0]);
if (sub.source !== 'local') { if (
sub.source !== 'local' ||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
) {
try { try {
const flowInfo = await getFlowHeaders(sub.url); let url = `${sub.url}`
if (flowInfo) { .split(/[\r\n]+/)
res.set('subscription-userinfo', flowInfo); .map((i) => i.trim())
.filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(
decodeURIComponent(rawArgs[1]),
);
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if (!$arguments.noFlow) {
const flowInfo = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
} }
} catch (err) { } catch (err) {
$.error( $.error(
@@ -190,6 +265,9 @@ async function downloadCollection(req, res) {
); );
} }
} }
if (sub.subUserinfo) {
res.set('subscription-userinfo', sub.subUserinfo);
}
} }
if (platform === 'JSON') { if (platform === 'JSON') {

View File

@@ -3,6 +3,8 @@ import { ENV } from '@/vendor/open-api';
import { failed, success } from '@/restful/response'; import { failed, success } from '@/restful/response';
import { updateArtifactStore, updateAvatar } from '@/restful/settings'; import { updateArtifactStore, updateAvatar } from '@/restful/settings';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import scriptResourceCache from '@/utils/script-resource-cache';
import headersResourceCache from '@/utils/headers-resource-cache';
import { import {
GIST_BACKUP_FILE_NAME, GIST_BACKUP_FILE_NAME,
GIST_BACKUP_KEY, GIST_BACKUP_KEY,
@@ -73,6 +75,8 @@ async function refresh(_, res) {
// 2. clear resource cache // 2. clear resource cache
resourceCache.revokeAll(); resourceCache.revokeAll();
scriptResourceCache.revokeAll();
headersResourceCache.revokeAll();
success(res); success(res);
} }
@@ -153,11 +157,14 @@ async function gistBackup(req, res) {
} }
success(res); success(res);
} catch (err) { } catch (err) {
$.error(
`Failed to ${action} gist data.\nReason: ${err.message ?? err}`,
);
failed( failed(
res, res,
new InternalServerError( new InternalServerError(
'BACKUP_FAILED', 'BACKUP_FAILED',
`Failed to ${action} data to gist!`, `Failed to ${action} gist data!`,
`Reason: ${err.message ?? err}`, `Reason: ${err.message ?? err}`,
), ),
); );

View File

@@ -109,7 +109,12 @@ async function compareSub(req, res) {
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, sub.ua); return await download(
url,
sub.ua,
undefined,
sub.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@@ -195,7 +200,12 @@ async function compareCollection(req, res) {
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, sub.ua); return await download(
url,
sub.ua,
undefined,
sub.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(

View File

@@ -6,7 +6,11 @@ import {
} from './errors'; } from './errors';
import { deleteByName, findByName, updateByName } from '@/utils/database'; import { deleteByName, findByName, updateByName } from '@/utils/database';
import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants'; import { SUBS_KEY, COLLECTIONS_KEY, ARTIFACTS_KEY } from '@/constants';
import { getFlowHeaders, parseFlowHeaders } from '@/utils/flow'; import {
getFlowHeaders,
parseFlowHeaders,
getRmainingDays,
} from '@/utils/flow';
import { success, failed } from './response'; import { success, failed } from './response';
import $ from '@/core/app'; import $ from '@/core/app';
@@ -43,32 +47,98 @@ async function getFlowInfo(req, res) {
); );
return; return;
} }
if (sub.source === 'local') { if (
failed( sub.source === 'local' &&
res, !['localFirst', 'remoteFirst'].includes(sub.mergeSources)
new RequestInvalidError( ) {
'NO_FLOW_INFO', if (sub.subUserinfo) {
'N/A', success(res, {
`Local subscription ${name} has no flow information!`, ...parseFlowHeaders(sub.subUserinfo),
), });
); } else {
failed(
res,
new RequestInvalidError(
'NO_FLOW_INFO',
'N/A',
`Local subscription ${name} has no flow information!`,
),
);
}
return; return;
} }
try { try {
const flowHeaders = await getFlowHeaders(sub.url); let url = `${sub.url}`
if (!flowHeaders) { .split(/[\r\n]+/)
.map((i) => i.trim())
.filter((i) => i.length)?.[0];
let $arguments = {};
const rawArgs = url.split('#');
url = url.split('#')[0];
if (rawArgs.length > 1) {
try {
// 支持 `#${encodeURIComponent(JSON.stringify({arg1: "1"}))}`
$arguments = JSON.parse(decodeURIComponent(rawArgs[1]));
} catch (e) {
for (const pair of rawArgs[1].split('&')) {
const key = pair.split('=')[0];
const value = pair.split('=')[1];
// 部分兼容之前的逻辑 const value = pair.split('=')[1] || true;
$arguments[key] =
value == null || value === ''
? true
: decodeURIComponent(value);
}
}
}
if ($arguments.noFlow) {
failed( failed(
res, res,
new InternalServerError( new RequestInvalidError(
'NO_FLOW_INFO', 'NO_FLOW_INFO',
'No flow info', 'N/A',
`Failed to fetch flow headers`, `Subscription ${name}: noFlow`,
), ),
); );
return; return;
} }
if (sub.subUserinfo) {
success(res, parseFlowHeaders(flowHeaders)); success(res, {
...parseFlowHeaders(sub.subUserinfo),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
} else {
const flowHeaders = await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
sub.proxy,
);
if (!flowHeaders) {
failed(
res,
new InternalServerError(
'NO_FLOW_INFO',
'No flow info',
`Failed to fetch flow headers`,
),
);
return;
}
success(res, {
...parseFlowHeaders(flowHeaders),
remainingDays: getRmainingDays({
resetDay: $arguments.resetDay,
startDate: $arguments.startDate,
cycleDays: $arguments.cycleDays,
}),
});
}
} catch (err) { } catch (err) {
failed( failed(
res, res,

View File

@@ -35,13 +35,21 @@ async function produceArtifact({
ignoreFailedRemoteFile, ignoreFailedRemoteFile,
produceType, produceType,
produceOpts = {}, produceOpts = {},
subscription,
}) { }) {
platform = platform || 'JSON'; platform = platform || 'JSON';
if (type === 'subscription') { if (type === 'subscription') {
const allSubs = $.read(SUBS_KEY); let sub;
const sub = findByName(allSubs, name); if (name) {
if (!sub) throw new Error(`找不到订阅 ${name}`); const allSubs = $.read(SUBS_KEY);
sub = findByName(allSubs, name);
if (!sub) throw new Error(`找不到订阅 ${name}`);
} else if (subscription) {
sub = subscription;
} else {
throw new Error('未提供订阅名称或订阅数据');
}
let raw; let raw;
if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) { if (content && !['localFirst', 'remoteFirst'].includes(mergeSources)) {
raw = content; raw = content;
@@ -54,7 +62,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 || sub.ua); return await download(
url,
ua || sub.ua,
undefined,
sub.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@@ -94,7 +107,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 || sub.ua); return await download(
url,
ua || sub.ua,
undefined,
sub.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@@ -190,7 +208,12 @@ async function produceArtifact({
.filter((i) => i.length) .filter((i) => i.length)
.map(async (url) => { .map(async (url) => {
try { try {
return await download(url, sub.ua); return await download(
url,
sub.ua,
undefined,
sub.proxy,
);
} catch (err) { } catch (err) {
errors[url] = err; errors[url] = err;
$.error( $.error(
@@ -540,10 +563,19 @@ async function syncArtifacts() {
files.map((item) => [item.path, item]), files.map((item) => [item.path, item]),
); );
} }
const url = files[encodeURIComponent(artifact.name)]?.raw_url; const raw_url =
artifact.url = isGitLab files[encodeURIComponent(artifact.name)]?.raw_url;
? url const new_url = isGitLab
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); ? raw_url
: raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
} }
} }
@@ -637,10 +669,18 @@ async function syncArtifact(req, res) {
isGitLab = true; isGitLab = true;
files = Object.fromEntries(files.map((item) => [item.path, item])); files = Object.fromEntries(files.map((item) => [item.path, item]));
} }
const url = files[encodeURIComponent(artifact.name)]?.raw_url; const raw_url = files[encodeURIComponent(artifact.name)]?.raw_url;
artifact.url = isGitLab const new_url = isGitLab
? url ? raw_url
: url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1'); : raw_url?.replace(/\/raw\/[^/]*\/(.*)/, '/raw/$1');
$.info(
`上传配置完成\n文件列表: ${Object.keys(files).join(
', ',
)}\n当前文件: ${encodeURIComponent(
artifact.name,
)}\n响应返回的原始链接: ${raw_url}\n处理完的新链接: ${new_url}`,
);
artifact.url = new_url;
$.write(allArtifacts, ARTIFACTS_KEY); $.write(allArtifacts, ARTIFACTS_KEY);
success(res, artifact); success(res, artifact);
} catch (err) { } catch (err) {

View File

@@ -1,6 +1,7 @@
import { SETTINGS_KEY } from '@/constants'; import { SETTINGS_KEY } from '@/constants';
import { HTTP, ENV } from '@/vendor/open-api'; import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5'; import { hex_md5 } from '@/vendor/md5';
import { getPolicyDescriptor } from '@/utils';
import resourceCache from '@/utils/resource-cache'; import resourceCache from '@/utils/resource-cache';
import headersResourceCache from '@/utils/headers-resource-cache'; import headersResourceCache from '@/utils/headers-resource-cache';
import { import {
@@ -13,7 +14,7 @@ import $ from '@/core/app';
const tasks = new Map(); const tasks = new Map();
export default async function download(rawUrl, ua, timeout) { export default async function download(rawUrl, ua, timeout, proxy) {
let $arguments = {}; let $arguments = {};
let url = rawUrl.replace(/#noFlow$/, ''); let url = rawUrl.replace(/#noFlow$/, '');
const rawArgs = url.split('#'); const rawArgs = url.split('#');
@@ -52,7 +53,7 @@ export default async function download(rawUrl, ua, timeout) {
// return item.content; // return item.content;
// } // }
const { isNode } = ENV(); const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
const { defaultUserAgent, defaultTimeout, cacheThreshold } = const { defaultUserAgent, defaultTimeout, cacheThreshold } =
$.read(SETTINGS_KEY); $.read(SETTINGS_KEY);
const userAgent = ua || defaultUserAgent || 'clash.meta'; const userAgent = ua || defaultUserAgent || 'clash.meta';
@@ -65,6 +66,10 @@ export default async function download(rawUrl, ua, timeout) {
const http = HTTP({ const http = HTTP({
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,
}); });
@@ -78,10 +83,16 @@ export default async function download(rawUrl, ua, timeout) {
result = cached; result = cached;
} else { } else {
$.info( $.info(
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nURL: ${url}`, `Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
); );
try { try {
const { body, headers } = await http.get(url); const { body, headers } = await http.get({
url,
...(proxy ? { proxy } : {}),
...(isLoon && proxy ? { node: proxy } : {}),
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
...(proxy ? getPolicyDescriptor(proxy) : {}),
});
if (headers) { if (headers) {
const flowInfo = getFlowField(headers); const flowInfo = getFlowField(headers);
@@ -116,7 +127,16 @@ export default async function download(rawUrl, ua, timeout) {
// 检查订阅有效性 // 检查订阅有效性
if ($arguments?.validCheck) { if ($arguments?.validCheck) {
await validCheck(parseFlowHeaders(await getFlowHeaders(url))); await validCheck(
parseFlowHeaders(
await getFlowHeaders(
url,
$arguments.flowUserAgent,
undefined,
proxy,
),
),
);
} }
if (!isNode) { if (!isNode) {

View File

@@ -1,7 +1,16 @@
import { version as substoreVersion } from '../../package.json'; import { version as substoreVersion } from '../../package.json';
import { ENV } from '@/vendor/open-api'; import { ENV } from '@/vendor/open-api';
const { isNode, isQX, isLoon, isSurge, isStash, isShadowRocket } = ENV(); const {
isNode,
isQX,
isLoon,
isSurge,
isStash,
isShadowRocket,
isLanceX,
isEgern,
} = ENV();
let backend = 'Node'; let backend = 'Node';
if (isNode) backend = 'Node'; if (isNode) backend = 'Node';
if (isQX) backend = 'QX'; if (isQX) backend = 'QX';
@@ -9,8 +18,44 @@ if (isLoon) backend = 'Loon';
if (isSurge) backend = 'Surge'; if (isSurge) backend = 'Surge';
if (isStash) backend = 'Stash'; if (isStash) backend = 'Stash';
if (isShadowRocket) backend = 'ShadowRocket'; if (isShadowRocket) backend = 'ShadowRocket';
if (isEgern) backend = 'Egern';
if (isLanceX) backend = 'LanceX';
let meta = {};
try {
if (typeof $environment !== 'undefined') {
// eslint-disable-next-line no-undef
meta.env = $environment;
}
if (typeof $loon !== 'undefined') {
// eslint-disable-next-line no-undef
meta.loon = $loon;
}
if (typeof $script !== 'undefined') {
// eslint-disable-next-line no-undef
meta.script = $script;
}
if (isNode) {
meta.node = {
version: eval('process.version'),
argv: eval('process.argv'),
filename: eval('__filename'),
dirname: eval('__dirname'),
env: {},
};
const env = eval('process.env');
for (const key in env) {
if (/^SUB_STORE_/.test(key)) {
meta.node.env[key] = env[key];
}
}
}
// eslint-disable-next-line no-empty
} catch (e) {}
export default { export default {
backend, backend,
version: substoreVersion, version: substoreVersion,
meta,
}; };

View File

@@ -1,5 +1,6 @@
import { SETTINGS_KEY } from '@/constants'; import { SETTINGS_KEY } from '@/constants';
import { HTTP } from '@/vendor/open-api'; import { HTTP, ENV } from '@/vendor/open-api';
import { getPolicyDescriptor } from '@/utils';
import $ from '@/core/app'; import $ from '@/core/app';
import headersResourceCache from '@/utils/headers-resource-cache'; import headersResourceCache from '@/utils/headers-resource-cache';
@@ -9,7 +10,7 @@ export function getFlowField(headers) {
)[0]; )[0];
return headers[subkey]; return headers[subkey];
} }
export async function getFlowHeaders(rawUrl, ua, timeout) { export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
let url = rawUrl; let url = rawUrl;
let $arguments = {}; let $arguments = {};
const rawArgs = url.split('#'); const rawArgs = url.split('#');
@@ -33,6 +34,7 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
if ($arguments?.noFlow) { if ($arguments?.noFlow) {
return; return;
} }
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
const cached = headersResourceCache.get(url); const cached = headersResourceCache.get(url);
let flowInfo; let flowInfo;
if (!$arguments?.noCache && cached) { if (!$arguments?.noCache && cached) {
@@ -47,7 +49,11 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
const requestTimeout = timeout || defaultTimeout; const requestTimeout = timeout || defaultTimeout;
const http = HTTP(); const http = HTTP();
try { try {
// $.info(`使用 HEAD 方法获取流量信息: ${url}`); $.info(
`使用 HEAD 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
);
const { headers } = await http.head({ const { headers } = await http.head({
url: url url: url
.split(/[\r\n]+/) .split(/[\r\n]+/)
@@ -55,17 +61,36 @@ export async function getFlowHeaders(rawUrl, ua, timeout) {
.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);
} catch (e) { } catch (e) {
$.error( $.error(
`使用 HEAD 方法获取流量信息失败: ${url}: ${e.message ?? e}`, `使用 HEAD 方法获取流量信息失败: ${url}, User-Agent: ${
userAgent || ''
}: ${e.message ?? e}`,
); );
} }
if (!flowInfo) { if (!flowInfo) {
$.info(`使用 GET 方法获取流量信息: ${url}`); $.info(
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
userAgent || ''
}`,
);
const { headers } = await http.get({ const { headers } = await http.get({
url: url url: url
.split(/[\r\n]+/) .split(/[\r\n]+/)
@@ -143,3 +168,60 @@ export function validCheck(flow) {
} }
} }
} }
export function getRmainingDays(opt = {}) {
try {
let { resetDay, startDate, cycleDays } = opt;
if (['string', 'number'].includes(typeof opt)) {
resetDay = opt;
}
if (startDate && cycleDays) {
cycleDays = parseInt(cycleDays);
if (isNaN(cycleDays) || cycleDays <= 0)
throw new Error('重置周期应为正整数');
if (!startDate || !Date.parse(startDate))
throw new Error('开始日期不合法');
const start = new Date(startDate);
const today = new Date();
start.setHours(0, 0, 0, 0);
today.setHours(0, 0, 0, 0);
if (start.getTime() > today.getTime())
throw new Error('开始日期应早于现在');
let resetDate = new Date(startDate);
resetDate.setDate(resetDate.getDate() + cycleDays);
while (resetDate < today) {
resetDate.setDate(resetDate.getDate() + cycleDays);
}
resetDate.setHours(0, 0, 0, 0);
const timeDiff = resetDate.getTime() - today.getTime();
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
return daysDiff;
} else {
if (!resetDay) return;
resetDay = parseInt(resetDay);
if (isNaN(resetDay) || resetDay <= 0 || resetDay > 31)
throw new Error('月重置日应为 1-31 之间的整数');
let now = new Date();
let today = now.getDate();
let month = now.getMonth();
let year = now.getFullYear();
let daysInMonth;
if (resetDay > today) {
daysInMonth = 0;
} else {
daysInMonth = new Date(year, month + 1, 0).getDate();
}
return daysInMonth - today + resetDay;
}
} catch (e) {
$.error(`getRmainingDays failed: ${e.message ?? e}`);
}
}

View File

@@ -35,6 +35,17 @@ function getIfPresent(obj, defaultValue) {
return isPresent(obj) ? obj : defaultValue; return isPresent(obj) ? obj : defaultValue;
} }
function getPolicyDescriptor(str) {
if (!str) return {};
return /^.+?\s*?=\s*?.+?\s*?,.+?/.test(str)
? {
'policy-descriptor': str,
}
: {
policy: str,
};
}
const utf8ArrayToStr = const utf8ArrayToStr =
typeof TextDecoder !== 'undefined' typeof TextDecoder !== 'undefined'
? (v) => new TextDecoder().decode(new Uint8Array(v)) ? (v) => new TextDecoder().decode(new Uint8Array(v))
@@ -91,4 +102,5 @@ export {
isPresent, isPresent,
getIfPresent, getIfPresent,
utf8ArrayToStr, utf8ArrayToStr,
getPolicyDescriptor,
}; };

View File

@@ -6,6 +6,8 @@ const isNode = eval(`typeof process !== "undefined"`); // eval is needed in orde
const isStash = const isStash =
'undefined' !== typeof $environment && $environment['stash-version']; 'undefined' !== typeof $environment && $environment['stash-version'];
const isShadowRocket = 'undefined' !== typeof $rocket; const isShadowRocket = 'undefined' !== typeof $rocket;
const isEgern = 'object' == typeof egern;
const isLanceX = 'undefined' != typeof $native;
export class OpenAPI { export class OpenAPI {
constructor(name = 'untitled', debug = false) { constructor(name = 'untitled', debug = false) {
@@ -251,7 +253,16 @@ export class OpenAPI {
} }
export function ENV() { export function ENV() {
return { isQX, isLoon, isSurge, isNode, isStash, isShadowRocket }; return {
isQX,
isLoon,
isSurge,
isNode,
isStash,
isShadowRocket,
isEgern,
isLanceX,
};
} }
export function HTTP(defaultOptions = { baseURL: '' }) { export function HTTP(defaultOptions = { baseURL: '' }) {
@@ -305,42 +316,53 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
url: options.url, url: options.url,
headers: options.headers, headers: options.headers,
body: options.body, body: options.body,
opts: options.opts,
}); });
} else if (isLoon || isSurge || isNode) { } else if (isLoon || isSurge || isNode) {
worker = new Promise((resolve, reject) => { worker = new Promise((resolve, reject) => {
const request = isNode const request = isNode
? eval("require('request')") ? eval("require('request')")
: $httpClient; : $httpClient;
request[method.toLowerCase()]( const opts = JSON.parse(JSON.stringify(options));
JSON.parse(JSON.stringify(options)), if (!isNode && opts.timeout) {
(err, response, body) => { opts.timeout++;
// if (err) { let unit = 'ms';
// console.log(err); // 这些客户端单位为 s
// } else { if (isSurge || isStash || isShadowRocket) {
// console.log({ opts.timeout = Math.ceil(opts.timeout / 1000);
// statusCode: unit = 's';
// response.status || response.statusCode, }
// headers: response.headers, // Loon 为 ms
// body, // console.log(`[httpClient timeout] ${opts.timeout}${unit}`);
// }); }
// } request[method.toLowerCase()](opts, (err, response, body) => {
// if (err) {
// console.log(err);
// } else {
// console.log({
// statusCode:
// response.status || response.statusCode,
// headers: response.headers,
// body,
// });
// }
if (err) reject(err); if (err) reject(err);
else else
resolve({ resolve({
statusCode: statusCode: response.status || response.statusCode,
response.status || response.statusCode, headers: response.headers,
headers: response.headers, body,
body, });
}); });
},
);
}); });
} }
let timeoutid; let timeoutid;
const timer = timeout const timer = timeout
? new Promise((_, reject) => { ? new Promise((_, reject) => {
// console.log(`[request timeout] ${timeout}ms`);
timeoutid = setTimeout(() => { timeoutid = setTimeout(() => {
events.onTimeout(); events.onTimeout();
return reject( return reject(

View File

@@ -17,4 +17,4 @@ hostname=sub.store
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://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 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://github.com/sub-store-org/Sub-Store/releases/latest/download/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, tag=Sub-Store Sync 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

View File

@@ -10,6 +10,6 @@ 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="{{{ability}}}" 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="{{{ability}}}"
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 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
{{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js {{{sync}}}=type=cron,cronexp="{{{cronexp}}}",wake-system=1,timeout=120,script-path=https://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -8,6 +8,6 @@ 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://github.com/sub-store-org/Sub-Store/releases/latest/download/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 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 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://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -7,6 +7,6 @@ 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://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 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 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 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://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -7,6 +7,6 @@ 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 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 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 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 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://github.com/sub-store-org/Sub-Store/releases/latest/download/cron-sync-artifacts.min.js

View File

@@ -22,6 +22,9 @@ function operator(proxies = [], targetPlatform, context) {
// scriptResourceCache 缓存 // scriptResourceCache 缓存
// 可参考 https://t.me/zhetengsha/1003 // 可参考 https://t.me/zhetengsha/1003
// const cache = scriptResourceCache
// cache.set(id, data)
// cache.get(id)
// ProxyUtils 为节点处理工具 // ProxyUtils 为节点处理工具
// 可参考 https://t.me/zhetengsha/1066 // 可参考 https://t.me/zhetengsha/1066
@@ -35,6 +38,13 @@ function operator(proxies = [], targetPlatform, context) {
// yaml, // yaml 解析和生成 // yaml, // yaml 解析和生成
// } // }
// 示例: 从 sni 文件中读取内容并进行节点操作
// const sni = await produceArtifact({
// type: 'file',
// name: 'sni' // 文件名
// });
// $server.sni = sni
// 1. Surge 输出 WireGuard 完整配置 // 1. Surge 输出 WireGuard 完整配置
// let proxies = await produceArtifact({ // let proxies = await produceArtifact({
@@ -49,7 +59,10 @@ function operator(proxies = [], targetPlatform, context) {
// 2. sing-box // 2. sing-box
// 但是一般不需要这样用, 可参考 1. https://t.me/zhetengsha/1111 和 2. https://t.me/zhetengsha/1070 // 但是一般不需要这样用, 可参考
// 1. https://t.me/zhetengsha/1111
// 2. https://t.me/zhetengsha/1070
// 3. https://t.me/zhetengsha/1241
// let singboxProxies = await produceArtifact({ // let singboxProxies = await produceArtifact({
// type: 'subscription', // type: 'subscription' 或 'collection' // type: 'subscription', // type: 'subscription' 或 'collection'
@@ -63,24 +76,42 @@ function operator(proxies = [], targetPlatform, context) {
// 3. clash.meta // 3. clash.meta
// 但是一般不需要这样用, 可参考 1. https://t.me/zhetengsha/1111 和 2. https://t.me/zhetengsha/1070 // 但是一般不需要这样用, 可参考
// 1. https://t.me/zhetengsha/1111
// 2. https://t.me/zhetengsha/1070
// 3. https://t.me/zhetengsha/1234
// let clashMetaProxies = await produceArtifact({ // let clashMetaProxies = await produceArtifact({
// type: 'subscription', // type: 'subscription',
// name: 'sub', // name: 'sub',
// platform: 'ClashMeta', // platform: 'ClashMeta',
// produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies ) // produceType: 'internal' // 'internal' produces an Array, otherwise produces a String( ProxyUtils.yaml.safeLoad('YAML String').proxies )
// })) // })
// // YAML // // YAML
// ProxyUtils.yaml.load('YAML String')
// ProxyUtils.yaml.safeLoad('YAML String')
// $content = ProxyUtils.yaml.safeDump({}) // $content = ProxyUtils.yaml.safeDump({})
// $content = ProxyUtils.yaml.dump({})
// 一个往文件里插入本地节点的例子:
// const yaml = ProxyUtils.yaml.safeLoad($content ?? $files[0])
// let clashMetaProxies = await produceArtifact({
// type: 'collection',
// name: '机场',
// platform: 'ClashMeta',
// produceType: 'internal'
// })
// yaml.proxies.unshift(...clashMetaProxies)
// $content = ProxyUtils.yaml.dump(yaml)
// { $content, $files } will be passed to the next operator // { $content, $files } 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 为机场订阅流量信息处理工具
// 可参考 https://t.me/zhetengsha/948 // 可参考:
// https://github.com/sub-store-org/Sub-Store/blob/31b6dd0507a9286d6ab834ec94ad3050f6bdc86b/backend/src/utils/download.js#L104 // 1. https://t.me/zhetengsha/948
// context 为传入的上下文 // context 为传入的上下文
// 有三种情况, 按需判断 // 有三种情况, 按需判断