mirror of
https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-10 00:52:40 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
913638a233 | ||
|
|
bf642ce0e6 | ||
|
|
1ecac9da92 | ||
|
|
c5a417da8f | ||
|
|
8cd0545023 | ||
|
|
b6f848a6e6 | ||
|
|
99d058bcf1 | ||
|
|
533103e765 | ||
|
|
cf82764171 | ||
|
|
7b783c1fe3 | ||
|
|
372eff9a44 | ||
|
|
d3b5a529d7 | ||
|
|
8049134bb5 | ||
|
|
3f620700a4 | ||
|
|
9e64a68481 | ||
|
|
9ce5916414 | ||
|
|
047c21fe70 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sub-store",
|
"name": "sub-store",
|
||||||
"version": "2.14.321",
|
"version": "2.14.335",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import $ from '@/core/app';
|
|||||||
import { FILES_KEY, MODULES_KEY } from '@/constants';
|
import { FILES_KEY, MODULES_KEY } from '@/constants';
|
||||||
import { findByName } from '@/utils/database';
|
import { findByName } from '@/utils/database';
|
||||||
import { produceArtifact } from '@/restful/sync';
|
import { produceArtifact } from '@/restful/sync';
|
||||||
import { getFlag, getISO, MMDB } from '@/utils/geo';
|
import { getFlag, removeFlag, getISO, MMDB } from '@/utils/geo';
|
||||||
import Gist from '@/utils/gist';
|
import Gist from '@/utils/gist';
|
||||||
|
|
||||||
function preprocess(raw) {
|
function preprocess(raw) {
|
||||||
@@ -83,7 +83,7 @@ async function processFn(proxies, operators = [], targetPlatform, source) {
|
|||||||
const { mode, content } = item.args;
|
const { mode, content } = item.args;
|
||||||
if (mode === 'link') {
|
if (mode === 'link') {
|
||||||
let noCache;
|
let noCache;
|
||||||
let url = content;
|
let url = content || '';
|
||||||
if (url.endsWith('#noCache')) {
|
if (url.endsWith('#noCache')) {
|
||||||
url = url.replace(/#noCache$/, '');
|
url = url.replace(/#noCache$/, '');
|
||||||
noCache = true;
|
noCache = true;
|
||||||
@@ -276,6 +276,7 @@ export const ProxyUtils = {
|
|||||||
isIP,
|
isIP,
|
||||||
yaml: YAML,
|
yaml: YAML,
|
||||||
getFlag,
|
getFlag,
|
||||||
|
removeFlag,
|
||||||
getISO,
|
getISO,
|
||||||
MMDB,
|
MMDB,
|
||||||
Gist,
|
Gist,
|
||||||
@@ -437,6 +438,24 @@ function lastParse(proxy) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (['ws', 'http', 'h2'].includes(proxy.network)) {
|
||||||
|
if (
|
||||||
|
['ws', 'h2'].includes(proxy.network) &&
|
||||||
|
!proxy[`${proxy.network}-opts`]?.path
|
||||||
|
) {
|
||||||
|
proxy[`${proxy.network}-opts`] =
|
||||||
|
proxy[`${proxy.network}-opts`] || {};
|
||||||
|
proxy[`${proxy.network}-opts`].path = '/';
|
||||||
|
} else if (
|
||||||
|
proxy.network === 'http' &&
|
||||||
|
(!Array.isArray(proxy[`${proxy.network}-opts`]?.path) ||
|
||||||
|
proxy[`${proxy.network}-opts`]?.path.every((i) => !i))
|
||||||
|
) {
|
||||||
|
proxy[`${proxy.network}-opts`] =
|
||||||
|
proxy[`${proxy.network}-opts`] || {};
|
||||||
|
proxy[`${proxy.network}-opts`].path = ['/'];
|
||||||
|
}
|
||||||
|
}
|
||||||
if (['', 'off'].includes(proxy.sni)) {
|
if (['', 'off'].includes(proxy.sni)) {
|
||||||
proxy['disable-sni'] = true;
|
proxy['disable-sni'] = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,8 +305,9 @@ function URI_VMess() {
|
|||||||
if (params.net === 'ws' || params.obfs === 'websocket') {
|
if (params.net === 'ws' || params.obfs === 'websocket') {
|
||||||
proxy.network = 'ws';
|
proxy.network = 'ws';
|
||||||
} else if (
|
} else if (
|
||||||
['tcp', 'http'].includes(params.net) ||
|
['http'].includes(params.net) ||
|
||||||
params.obfs === 'http'
|
['http'].includes(params.obfs) ||
|
||||||
|
['http'].includes(params.type)
|
||||||
) {
|
) {
|
||||||
proxy.network = 'http';
|
proxy.network = 'http';
|
||||||
} else if (['grpc'].includes(params.net)) {
|
} else if (['grpc'].includes(params.net)) {
|
||||||
@@ -317,6 +318,8 @@ function URI_VMess() {
|
|||||||
) {
|
) {
|
||||||
proxy.network = 'ws';
|
proxy.network = 'ws';
|
||||||
httpupgrade = true;
|
httpupgrade = true;
|
||||||
|
} else if (params.net === 'h2' || proxy.network === 'h2') {
|
||||||
|
proxy.network = 'h2';
|
||||||
}
|
}
|
||||||
if (proxy.network) {
|
if (proxy.network) {
|
||||||
let transportHost = params.host ?? params.obfsParam;
|
let transportHost = params.host ?? params.obfsParam;
|
||||||
@@ -332,6 +335,10 @@ function URI_VMess() {
|
|||||||
|
|
||||||
if (proxy.network === 'http') {
|
if (proxy.network === 'http') {
|
||||||
if (transportHost) {
|
if (transportHost) {
|
||||||
|
// 1)http(tcp)->host中间逗号(,)隔开
|
||||||
|
transportHost = transportHost
|
||||||
|
.split(',')
|
||||||
|
.map((i) => i.trim());
|
||||||
transportHost = Array.isArray(transportHost)
|
transportHost = Array.isArray(transportHost)
|
||||||
? transportHost[0]
|
? transportHost[0]
|
||||||
: transportHost;
|
: transportHost;
|
||||||
@@ -340,6 +347,8 @@ function URI_VMess() {
|
|||||||
transportPath = Array.isArray(transportPath)
|
transportPath = Array.isArray(transportPath)
|
||||||
? transportPath[0]
|
? transportPath[0]
|
||||||
: transportPath;
|
: transportPath;
|
||||||
|
} else {
|
||||||
|
transportPath = '/';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (transportPath || transportHost) {
|
if (transportPath || transportHost) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import resourceCache from '@/utils/resource-cache';
|
|||||||
import scriptResourceCache from '@/utils/script-resource-cache';
|
import scriptResourceCache from '@/utils/script-resource-cache';
|
||||||
import { isIPv4, isIPv6 } from '@/utils';
|
import { isIPv4, isIPv6 } from '@/utils';
|
||||||
import { FULL } from '@/utils/logical';
|
import { FULL } from '@/utils/logical';
|
||||||
import { getFlag } from '@/utils/geo';
|
import { getFlag, removeFlag } from '@/utils/geo';
|
||||||
import lodash from 'lodash';
|
import lodash from 'lodash';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
import { hex_md5 } from '@/vendor/md5';
|
import { hex_md5 } from '@/vendor/md5';
|
||||||
@@ -512,12 +512,17 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
|
|||||||
return {
|
return {
|
||||||
name: 'Resolve Domain Operator',
|
name: 'Resolve Domain Operator',
|
||||||
func: async (proxies) => {
|
func: async (proxies) => {
|
||||||
|
proxies.forEach((p, i) => {
|
||||||
|
if (!p['_no-resolve'] && p['no-resolve']) {
|
||||||
|
proxies[i]['_no-resolve'] = p['no-resolve'];
|
||||||
|
}
|
||||||
|
});
|
||||||
const results = {};
|
const results = {};
|
||||||
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
|
const limit = 15; // more than 20 concurrency may result in surge TCP connection shortage.
|
||||||
const totalDomain = [
|
const totalDomain = [
|
||||||
...new Set(
|
...new Set(
|
||||||
proxies
|
proxies
|
||||||
.filter((p) => !isIP(p.server) && !p['no-resolve'])
|
.filter((p) => !isIP(p.server) && !p['_no-resolve'])
|
||||||
.map((c) => c.server),
|
.map((c) => c.server),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -543,7 +548,7 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
|
|||||||
await Promise.all(currentBatch);
|
await Promise.all(currentBatch);
|
||||||
}
|
}
|
||||||
proxies.forEach((p) => {
|
proxies.forEach((p) => {
|
||||||
if (!p['no-resolve']) {
|
if (!p['_no-resolve']) {
|
||||||
if (results[p.server]) {
|
if (results[p.server]) {
|
||||||
if (_type === 'IP4P') {
|
if (_type === 'IP4P') {
|
||||||
const { server, port } = parseIP4P(
|
const { server, port } = parseIP4P(
|
||||||
@@ -578,7 +583,7 @@ function ResolveDomainOperator({ provider, type: _type, filter, cache }) {
|
|||||||
|
|
||||||
return proxies.filter((p) => {
|
return proxies.filter((p) => {
|
||||||
if (filter === 'removeFailed') {
|
if (filter === 'removeFailed') {
|
||||||
return isIP(p.server) || p['no-resolve'] || p.resolved;
|
return isIP(p.server) || p['_no-resolve'] || p.resolved;
|
||||||
} else if (filter === 'IPOnly') {
|
} else if (filter === 'IPOnly') {
|
||||||
return isIP(p.server);
|
return isIP(p.server);
|
||||||
} else if (filter === 'IPv4Only') {
|
} else if (filter === 'IPv4Only') {
|
||||||
@@ -864,13 +869,6 @@ function clone(object) {
|
|||||||
return JSON.parse(JSON.stringify(object));
|
return JSON.parse(JSON.stringify(object));
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove flag
|
|
||||||
function removeFlag(str) {
|
|
||||||
return str
|
|
||||||
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴☠️|🏳️🌈/g, '')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDynamicFunction(name, script, $arguments) {
|
function createDynamicFunction(name, script, $arguments) {
|
||||||
const flowUtils = {
|
const flowUtils = {
|
||||||
getFlowField,
|
getFlowField,
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ export default function Clash_Producer() {
|
|||||||
delete proxy.collectionName;
|
delete proxy.collectionName;
|
||||||
delete proxy.id;
|
delete proxy.id;
|
||||||
delete proxy.resolved;
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
for (const key in proxy) {
|
for (const key in proxy) {
|
||||||
if (proxy[key] == null || /^_/i.test(key)) {
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
delete proxy[key];
|
delete proxy[key];
|
||||||
|
|||||||
@@ -173,6 +173,7 @@ export default function ClashMeta_Producer() {
|
|||||||
delete proxy.collectionName;
|
delete proxy.collectionName;
|
||||||
delete proxy.id;
|
delete proxy.id;
|
||||||
delete proxy.resolved;
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
for (const key in proxy) {
|
for (const key in proxy) {
|
||||||
if (proxy[key] == null || /^_/i.test(key)) {
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
delete proxy[key];
|
delete proxy[key];
|
||||||
|
|||||||
@@ -176,6 +176,7 @@ export default function ShadowRocket_Producer() {
|
|||||||
delete proxy.collectionName;
|
delete proxy.collectionName;
|
||||||
delete proxy.id;
|
delete proxy.id;
|
||||||
delete proxy.resolved;
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
for (const key in proxy) {
|
for (const key in proxy) {
|
||||||
if (proxy[key] == null || /^_/i.test(key)) {
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
delete proxy[key];
|
delete proxy[key];
|
||||||
|
|||||||
@@ -265,6 +265,7 @@ export default function Stash_Producer() {
|
|||||||
delete proxy.collectionName;
|
delete proxy.collectionName;
|
||||||
delete proxy.id;
|
delete proxy.id;
|
||||||
delete proxy.resolved;
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
for (const key in proxy) {
|
for (const key in proxy) {
|
||||||
if (proxy[key] == null || /^_/i.test(key)) {
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
delete proxy[key];
|
delete proxy[key];
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function Surge_Producer() {
|
|||||||
case 'trojan':
|
case 'trojan':
|
||||||
return trojan(proxy);
|
return trojan(proxy);
|
||||||
case 'vmess':
|
case 'vmess':
|
||||||
return vmess(proxy);
|
return vmess(proxy, opts['include-unsupported-proxy']);
|
||||||
case 'http':
|
case 'http':
|
||||||
return http(proxy);
|
return http(proxy);
|
||||||
case 'socks5':
|
case 'socks5':
|
||||||
@@ -264,7 +264,7 @@ function trojan(proxy) {
|
|||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function vmess(proxy) {
|
function vmess(proxy, includeUnsupportedProxy) {
|
||||||
const result = new Result(proxy);
|
const result = new Result(proxy);
|
||||||
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
result.append(`${proxy.name}=${proxy.type},${proxy.server},${proxy.port}`);
|
||||||
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
|
result.appendIfPresent(`,username=${proxy.uuid}`, 'uuid');
|
||||||
@@ -278,7 +278,7 @@ function vmess(proxy) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// transport
|
// transport
|
||||||
handleTransport(result, proxy);
|
handleTransport(result, proxy, includeUnsupportedProxy);
|
||||||
|
|
||||||
// AEAD
|
// AEAD
|
||||||
if (isPresent(proxy, 'aead')) {
|
if (isPresent(proxy, 'aead')) {
|
||||||
@@ -1013,7 +1013,7 @@ function hysteria2(proxy) {
|
|||||||
return result.toString();
|
return result.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTransport(result, proxy) {
|
function handleTransport(result, proxy, includeUnsupportedProxy) {
|
||||||
if (isPresent(proxy, 'network')) {
|
if (isPresent(proxy, 'network')) {
|
||||||
if (proxy.network === 'ws') {
|
if (proxy.network === 'ws') {
|
||||||
result.append(`,ws=true`);
|
result.append(`,ws=true`);
|
||||||
@@ -1039,7 +1039,13 @@ function handleTransport(result, proxy) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`network ${proxy.network} is unsupported`);
|
if (includeUnsupportedProxy && ['http'].includes(proxy.network)) {
|
||||||
|
$.info(
|
||||||
|
`Include Unsupported Proxy: nework ${proxy.network} -> tcp`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error(`network ${proxy.network} is unsupported`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export default function URI_Producer() {
|
|||||||
delete proxy.collectionName;
|
delete proxy.collectionName;
|
||||||
delete proxy.id;
|
delete proxy.id;
|
||||||
delete proxy.resolved;
|
delete proxy.resolved;
|
||||||
|
delete proxy['no-resolve'];
|
||||||
for (const key in proxy) {
|
for (const key in proxy) {
|
||||||
if (proxy[key] == null || /^_/i.test(key)) {
|
if (proxy[key] == null || /^_/i.test(key)) {
|
||||||
delete proxy[key];
|
delete proxy[key];
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ async function doSync() {
|
|||||||
await produceArtifact({
|
await produceArtifact({
|
||||||
type: 'subscription',
|
type: 'subscription',
|
||||||
name: subName,
|
name: subName,
|
||||||
|
awaitCustomCache: true,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// $.error(`${e.message ?? e}`);
|
// $.error(`${e.message ?? e}`);
|
||||||
|
|||||||
@@ -123,10 +123,11 @@ async function downloadSubscription(req, res) {
|
|||||||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
url = `${url || sub.url}`
|
url =
|
||||||
.split(/[\r\n]+/)
|
`${url || sub.url}`
|
||||||
.map((i) => i.trim())
|
.split(/[\r\n]+/)
|
||||||
.filter((i) => i.length)?.[0];
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)?.[0] || '';
|
||||||
|
|
||||||
let $arguments = {};
|
let $arguments = {};
|
||||||
const rawArgs = url.split('#');
|
const rawArgs = url.split('#');
|
||||||
@@ -156,6 +157,7 @@ async function downloadSubscription(req, res) {
|
|||||||
$arguments.flowUserAgent,
|
$arguments.flowUserAgent,
|
||||||
undefined,
|
undefined,
|
||||||
sub.proxy,
|
sub.proxy,
|
||||||
|
$arguments.flowUrl,
|
||||||
);
|
);
|
||||||
if (flowInfo) {
|
if (flowInfo) {
|
||||||
res.set('subscription-userinfo', flowInfo);
|
res.set('subscription-userinfo', flowInfo);
|
||||||
@@ -282,10 +284,11 @@ async function downloadCollection(req, res) {
|
|||||||
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
['localFirst', 'remoteFirst'].includes(sub.mergeSources)
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
let url = `${sub.url}`
|
let url =
|
||||||
.split(/[\r\n]+/)
|
`${sub.url}`
|
||||||
.map((i) => i.trim())
|
.split(/[\r\n]+/)
|
||||||
.filter((i) => i.length)?.[0];
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)?.[0] || '';
|
||||||
|
|
||||||
let $arguments = {};
|
let $arguments = {};
|
||||||
const rawArgs = url.split('#');
|
const rawArgs = url.split('#');
|
||||||
@@ -314,6 +317,7 @@ async function downloadCollection(req, res) {
|
|||||||
$arguments.flowUserAgent,
|
$arguments.flowUserAgent,
|
||||||
undefined,
|
undefined,
|
||||||
sub.proxy,
|
sub.proxy,
|
||||||
|
$arguments.flowUrl,
|
||||||
);
|
);
|
||||||
if (flowInfo) {
|
if (flowInfo) {
|
||||||
res.set('subscription-userinfo', flowInfo);
|
res.set('subscription-userinfo', flowInfo);
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export default function register($app) {
|
|||||||
async function getFlowInfo(req, res) {
|
async function getFlowInfo(req, res) {
|
||||||
let { name } = req.params;
|
let { name } = req.params;
|
||||||
name = decodeURIComponent(name);
|
name = decodeURIComponent(name);
|
||||||
|
let { url } = req.query;
|
||||||
|
if (url) {
|
||||||
|
url = decodeURIComponent(url);
|
||||||
|
$.info(`指定远程订阅 URL: ${url}`);
|
||||||
|
}
|
||||||
const allSubs = $.read(SUBS_KEY);
|
const allSubs = $.read(SUBS_KEY);
|
||||||
const sub = findByName(allSubs, name);
|
const sub = findByName(allSubs, name);
|
||||||
if (!sub) {
|
if (!sub) {
|
||||||
@@ -68,10 +73,11 @@ async function getFlowInfo(req, res) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
let url = `${sub.url}`
|
url =
|
||||||
.split(/[\r\n]+/)
|
`${url || sub.url}`
|
||||||
.map((i) => i.trim())
|
.split(/[\r\n]+/)
|
||||||
.filter((i) => i.length)?.[0];
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)?.[0] || '';
|
||||||
|
|
||||||
let $arguments = {};
|
let $arguments = {};
|
||||||
const rawArgs = url.split('#');
|
const rawArgs = url.split('#');
|
||||||
@@ -118,6 +124,7 @@ async function getFlowInfo(req, res) {
|
|||||||
$arguments.flowUserAgent,
|
$arguments.flowUserAgent,
|
||||||
undefined,
|
undefined,
|
||||||
sub.proxy,
|
sub.proxy,
|
||||||
|
$arguments.flowUrl,
|
||||||
);
|
);
|
||||||
if (!flowHeaders) {
|
if (!flowHeaders) {
|
||||||
failed(
|
failed(
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ async function produceArtifact({
|
|||||||
produceType,
|
produceType,
|
||||||
produceOpts = {},
|
produceOpts = {},
|
||||||
subscription,
|
subscription,
|
||||||
|
awaitCustomCache,
|
||||||
}) {
|
}) {
|
||||||
platform = platform || 'JSON';
|
platform = platform || 'JSON';
|
||||||
|
|
||||||
@@ -67,6 +68,8 @@ async function produceArtifact({
|
|||||||
ua || sub.ua,
|
ua || sub.ua,
|
||||||
undefined,
|
undefined,
|
||||||
sub.proxy,
|
sub.proxy,
|
||||||
|
undefined,
|
||||||
|
awaitCustomCache,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors[url] = err;
|
errors[url] = err;
|
||||||
@@ -112,6 +115,8 @@ async function produceArtifact({
|
|||||||
ua || sub.ua,
|
ua || sub.ua,
|
||||||
undefined,
|
undefined,
|
||||||
sub.proxy,
|
sub.proxy,
|
||||||
|
undefined,
|
||||||
|
awaitCustomCache,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors[url] = err;
|
errors[url] = err;
|
||||||
@@ -503,6 +508,7 @@ async function syncArtifacts() {
|
|||||||
await produceArtifact({
|
await produceArtifact({
|
||||||
type: 'subscription',
|
type: 'subscription',
|
||||||
name: subName,
|
name: subName,
|
||||||
|
awaitCustomCache: true,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// $.error(`${e.message ?? e}`);
|
// $.error(`${e.message ?? e}`);
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import $ from '@/core/app';
|
|||||||
const tasks = new Map();
|
const tasks = new Map();
|
||||||
|
|
||||||
export default async function download(
|
export default async function download(
|
||||||
rawUrl,
|
rawUrl = '',
|
||||||
ua,
|
ua,
|
||||||
timeout,
|
timeout,
|
||||||
proxy,
|
proxy,
|
||||||
skipCustomCache,
|
skipCustomCache,
|
||||||
|
awaitCustomCache,
|
||||||
) {
|
) {
|
||||||
let $arguments = {};
|
let $arguments = {};
|
||||||
let url = rawUrl.replace(/#noFlow$/, '');
|
let url = rawUrl.replace(/#noFlow$/, '');
|
||||||
@@ -41,25 +42,66 @@ export default async function download(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
|
||||||
|
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
|
||||||
|
$.read(SETTINGS_KEY);
|
||||||
|
const userAgent = ua || defaultUserAgent || 'clash.meta';
|
||||||
|
const requestTimeout = timeout || defaultTimeout;
|
||||||
|
const id = hex_md5(userAgent + url);
|
||||||
|
|
||||||
const customCacheKey = $arguments?.cacheKey
|
const customCacheKey = $arguments?.cacheKey
|
||||||
? `#sub-store-cached-custom-${$arguments?.cacheKey}`
|
? `#sub-store-cached-custom-${$arguments?.cacheKey}`
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (customCacheKey && !skipCustomCache) {
|
if (customCacheKey && !skipCustomCache) {
|
||||||
const cached = $.read(customCacheKey);
|
const customCached = $.read(customCacheKey);
|
||||||
if (cached) {
|
const cached = resourceCache.get(id);
|
||||||
|
if (!$arguments?.noCache && cached) {
|
||||||
$.info(
|
$.info(
|
||||||
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试更新缓存`,
|
`乐观缓存: URL ${url}\n存在有效的常规缓存\n使用常规缓存以避免重复请求`,
|
||||||
);
|
|
||||||
download(
|
|
||||||
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
|
|
||||||
ua,
|
|
||||||
timeout,
|
|
||||||
proxy,
|
|
||||||
true,
|
|
||||||
);
|
);
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
if (customCached) {
|
||||||
|
if (awaitCustomCache) {
|
||||||
|
$.info(`乐观缓存: URL ${url}\n本次进行请求 尝试更新缓存`);
|
||||||
|
try {
|
||||||
|
await download(
|
||||||
|
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
|
||||||
|
ua,
|
||||||
|
timeout,
|
||||||
|
proxy,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`乐观缓存: URL ${url} 更新缓存发生错误 ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
$.info('使用乐观缓存的数据刷新缓存, 防止后续请求');
|
||||||
|
resourceCache.set(id, customCached);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$.info(
|
||||||
|
`乐观缓存: URL ${url}\n本次返回自定义缓存 ${$arguments?.cacheKey}\n并进行请求 尝试异步更新缓存`,
|
||||||
|
);
|
||||||
|
download(
|
||||||
|
rawUrl.replace(/(\?|&)cacheKey=.*?(&|$)/, ''),
|
||||||
|
ua,
|
||||||
|
timeout,
|
||||||
|
proxy,
|
||||||
|
true,
|
||||||
|
).catch((e) => {
|
||||||
|
$.error(
|
||||||
|
`乐观缓存: URL ${url} 异步更新缓存发生错误 ${
|
||||||
|
e.message ?? e
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return customCached;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
|
// const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
|
||||||
@@ -79,12 +121,6 @@ export default async function download(
|
|||||||
// return item.content;
|
// return item.content;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
const { isNode, isStash, isLoon, isShadowRocket, isQX } = ENV();
|
|
||||||
const { defaultUserAgent, defaultTimeout, cacheThreshold } =
|
|
||||||
$.read(SETTINGS_KEY);
|
|
||||||
const userAgent = ua || defaultUserAgent || 'clash.meta';
|
|
||||||
const requestTimeout = timeout || defaultTimeout;
|
|
||||||
const id = hex_md5(userAgent + url);
|
|
||||||
if (!isNode && tasks.has(id)) {
|
if (!isNode && tasks.has(id)) {
|
||||||
return tasks.get(id);
|
return tasks.get(id);
|
||||||
}
|
}
|
||||||
@@ -104,9 +140,13 @@ export default async function download(
|
|||||||
|
|
||||||
// try to find in app cache
|
// try to find in app cache
|
||||||
const cached = resourceCache.get(id);
|
const cached = resourceCache.get(id);
|
||||||
if (!$arguments?.noCache && cached && !skipCustomCache) {
|
if (!$arguments?.noCache && cached) {
|
||||||
$.info(`使用缓存: ${url}`);
|
$.info(`使用缓存: ${url}`);
|
||||||
result = cached;
|
result = cached;
|
||||||
|
if (customCacheKey) {
|
||||||
|
$.info(`URL ${url}\n写入自定义缓存 ${$arguments?.cacheKey}`);
|
||||||
|
$.write(cached, customCacheKey);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$.info(
|
$.info(
|
||||||
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
|
`Downloading...\nUser-Agent: ${userAgent}\nTimeout: ${requestTimeout}\nProxy: ${proxy}\nURL: ${url}`,
|
||||||
@@ -177,6 +217,7 @@ export default async function download(
|
|||||||
$arguments.flowUserAgent,
|
$arguments.flowUserAgent,
|
||||||
undefined,
|
undefined,
|
||||||
proxy,
|
proxy,
|
||||||
|
$arguments.flowUrl,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ export function getFlowField(headers) {
|
|||||||
)[0];
|
)[0];
|
||||||
return headers[subkey];
|
return headers[subkey];
|
||||||
}
|
}
|
||||||
export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
|
export async function getFlowHeaders(rawUrl, ua, timeout, proxy, flowUrl) {
|
||||||
let url = rawUrl;
|
let url = flowUrl || rawUrl || '';
|
||||||
let $arguments = {};
|
let $arguments = {};
|
||||||
const rawArgs = url.split('#');
|
const rawArgs = url.split('#');
|
||||||
url = url.split('#')[0];
|
url = url.split('#')[0];
|
||||||
@@ -48,60 +48,76 @@ export async function getFlowHeaders(rawUrl, ua, timeout, proxy) {
|
|||||||
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
|
'Quantumult%20X/1.0.30 (iPhone14,2; iOS 15.6)';
|
||||||
const requestTimeout = timeout || defaultTimeout;
|
const requestTimeout = timeout || defaultTimeout;
|
||||||
const http = HTTP();
|
const http = HTTP();
|
||||||
try {
|
if (flowUrl) {
|
||||||
$.info(
|
$.info(
|
||||||
`使用 HEAD 方法获取流量信息: ${url}, User-Agent: ${
|
`使用 GET 方法从响应体获取流量信息: ${flowUrl}, User-Agent: ${
|
||||||
userAgent || ''
|
userAgent || ''
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
const { headers } = await http.head({
|
const { body } = await http.get({
|
||||||
url: url
|
url: flowUrl,
|
||||||
.split(/[\r\n]+/)
|
|
||||||
.map((i) => i.trim())
|
|
||||||
.filter((i) => i.length)[0],
|
|
||||||
headers: {
|
|
||||||
'User-Agent': userAgent,
|
|
||||||
...(isStash && proxy
|
|
||||||
? {
|
|
||||||
'X-Stash-Selected-Proxy':
|
|
||||||
encodeURIComponent(proxy),
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
...(isShadowRocket && proxy
|
|
||||||
? { 'X-Surge-Policy': proxy }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
timeout: requestTimeout,
|
|
||||||
...(proxy ? { proxy } : {}),
|
|
||||||
...(isLoon && proxy ? { node: proxy } : {}),
|
|
||||||
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
|
|
||||||
...(proxy ? getPolicyDescriptor(proxy) : {}),
|
|
||||||
});
|
|
||||||
flowInfo = getFlowField(headers);
|
|
||||||
} catch (e) {
|
|
||||||
$.error(
|
|
||||||
`使用 HEAD 方法获取流量信息失败: ${url}, User-Agent: ${
|
|
||||||
userAgent || ''
|
|
||||||
}: ${e.message ?? e}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!flowInfo) {
|
|
||||||
$.info(
|
|
||||||
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
|
|
||||||
userAgent || ''
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
const { headers } = await http.get({
|
|
||||||
url: url
|
|
||||||
.split(/[\r\n]+/)
|
|
||||||
.map((i) => i.trim())
|
|
||||||
.filter((i) => i.length)[0],
|
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': userAgent,
|
'User-Agent': userAgent,
|
||||||
},
|
},
|
||||||
timeout: requestTimeout,
|
timeout: requestTimeout,
|
||||||
});
|
});
|
||||||
flowInfo = getFlowField(headers);
|
flowInfo = body;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
$.info(
|
||||||
|
`使用 HEAD 方法从响应头获取流量信息: ${url}, User-Agent: ${
|
||||||
|
userAgent || ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
const { headers } = await http.head({
|
||||||
|
url: url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)[0],
|
||||||
|
headers: {
|
||||||
|
'User-Agent': userAgent,
|
||||||
|
...(isStash && proxy
|
||||||
|
? {
|
||||||
|
'X-Stash-Selected-Proxy':
|
||||||
|
encodeURIComponent(proxy),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
...(isShadowRocket && proxy
|
||||||
|
? { 'X-Surge-Policy': proxy }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
timeout: requestTimeout,
|
||||||
|
...(proxy ? { proxy } : {}),
|
||||||
|
...(isLoon && proxy ? { node: proxy } : {}),
|
||||||
|
...(isQX && proxy ? { opts: { policy: proxy } } : {}),
|
||||||
|
...(proxy ? getPolicyDescriptor(proxy) : {}),
|
||||||
|
});
|
||||||
|
flowInfo = getFlowField(headers);
|
||||||
|
} catch (e) {
|
||||||
|
$.error(
|
||||||
|
`使用 HEAD 方法从响应头获取流量信息失败: ${url}, User-Agent: ${
|
||||||
|
userAgent || ''
|
||||||
|
}: ${e.message ?? e}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!flowInfo) {
|
||||||
|
$.info(
|
||||||
|
`使用 GET 方法获取流量信息: ${url}, User-Agent: ${
|
||||||
|
userAgent || ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
const { headers } = await http.get({
|
||||||
|
url: url
|
||||||
|
.split(/[\r\n]+/)
|
||||||
|
.map((i) => i.trim())
|
||||||
|
.filter((i) => i.length)[0],
|
||||||
|
headers: {
|
||||||
|
'User-Agent': userAgent,
|
||||||
|
},
|
||||||
|
timeout: requestTimeout,
|
||||||
|
});
|
||||||
|
flowInfo = getFlowField(headers);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (flowInfo) {
|
if (flowInfo) {
|
||||||
headersResourceCache.set(url, flowInfo);
|
headersResourceCache.set(url, flowInfo);
|
||||||
|
|||||||
@@ -430,6 +430,13 @@ export function getISO(name) {
|
|||||||
return ISOFlags[getFlag(name)]?.[0];
|
return ISOFlags[getFlag(name)]?.[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove flag
|
||||||
|
export function removeFlag(str) {
|
||||||
|
return str
|
||||||
|
.replace(/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]|🏴☠️|🏳️🌈/g, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
export class MMDB {
|
export class MMDB {
|
||||||
constructor({ country, asn } = {}) {
|
constructor({ country, asn } = {}) {
|
||||||
if ($.env.isNode) {
|
if ($.env.isNode) {
|
||||||
|
|||||||
6
backend/src/vendor/open-api.js
vendored
6
backend/src/vendor/open-api.js
vendored
@@ -382,6 +382,12 @@ export function HTTP(defaultOptions = { baseURL: '' }) {
|
|||||||
url: options.url,
|
url: options.url,
|
||||||
headers: options.headers,
|
headers: options.headers,
|
||||||
body: options.body,
|
body: options.body,
|
||||||
|
options: {
|
||||||
|
Proxy: options.proxy,
|
||||||
|
Timeout: options.timeout
|
||||||
|
? options.timeout / 1000
|
||||||
|
: 15,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
resolve({
|
resolve({
|
||||||
statusCode: response.status,
|
statusCode: response.status,
|
||||||
|
|||||||
@@ -6,13 +6,23 @@ Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](http
|
|||||||
|
|
||||||
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
||||||
|
|
||||||
## 脚本配置:
|
## 服务器/云平台/Docker/Android 版
|
||||||
|
|
||||||
|
https://xream.notion.site/Sub-Store-abe6a96944724dc6a36833d5c9ab7c87
|
||||||
|
|
||||||
|
## App 版
|
||||||
|
|
||||||
### 1. Loon
|
### 1. Loon
|
||||||
安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
|
安装使用 插件 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Loon.plugin) 即可。
|
||||||
|
|
||||||
### 2. Surge
|
### 2. Surge
|
||||||
|
|
||||||
|
#### 关于 Surge 的格外说明
|
||||||
|
|
||||||
|
Surge Mac 版如何支持 SSR, 如何去除 HTTP 传输层以支持 类似 VMess HTTP 节点等 请查看 [链接参数说明](https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E)
|
||||||
|
|
||||||
|
定时处理订阅 功能, 避免 App 内拉取超时, 请查看 [定时处理订阅](https://t.me/zhetengsha/1449)
|
||||||
|
|
||||||
0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的特性): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule)
|
0. 最新 Surge iOS TestFlight 版本 可使用 Beta 版(支持最新 Surge iOS TestFlight 版本的特性): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Beta.sgmodule)
|
||||||
|
|
||||||
1. 官方默认版模块(支持 App 内使用编辑参数): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
|
1. 官方默认版模块(支持 App 内使用编辑参数): [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge.sgmodule)
|
||||||
@@ -39,3 +49,11 @@ Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
|
|||||||
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。
|
||||||
2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
|
2. 可以把 Sub-Store 添加到主屏幕,即可获得类似于 APP 的使用体验。
|
||||||
3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。
|
3. 更详细的使用指南请参考[文档](https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46)。
|
||||||
|
|
||||||
|
## 链接参数说明
|
||||||
|
|
||||||
|
https://github.com/sub-store-org/Sub-Store/wiki/%E9%93%BE%E6%8E%A5%E5%8F%82%E6%95%B0%E8%AF%B4%E6%98%8E
|
||||||
|
|
||||||
|
## 脚本使用说明
|
||||||
|
|
||||||
|
https://github.com/sub-store-org/Sub-Store/wiki/%E8%84%9A%E6%9C%AC%E4%BD%BF%E7%94%A8%E8%AF%B4%E6%98%8E
|
||||||
@@ -7,7 +7,7 @@ function operator(proxies = [], targetPlatform, context) {
|
|||||||
// proxies 为传入的内部节点数组
|
// proxies 为传入的内部节点数组
|
||||||
// 结构大致参考了 Clash.Meta(mihomo) 有私货
|
// 结构大致参考了 Clash.Meta(mihomo) 有私货
|
||||||
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
|
// 可在预览界面点击节点查看 JSON 结构 或查看 `target=JSON` 的通用订阅
|
||||||
// 1. `no-resolve` 为不解析域名
|
// 1. `_no-resolve` 为不解析域名
|
||||||
// 2. 域名解析后 会多一个 `_resolved` 字段
|
// 2. 域名解析后 会多一个 `_resolved` 字段
|
||||||
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段
|
// 3. 域名解析后会有`_IPv4`, `_IPv6`, `_IP`(若有多个步骤, 只取第一次成功的 v4 或 v6 数据), `_domain` 字段
|
||||||
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
|
// 4. 节点字段 `exec` 为 `ssr-local` 路径, 默认 `/usr/local/bin/ssr-local`; 端口从 10000 开始递增(暂不支持配置)
|
||||||
@@ -40,12 +40,15 @@ function operator(proxies = [], targetPlatform, context) {
|
|||||||
// isIP,
|
// isIP,
|
||||||
// yaml, // yaml 解析和生成
|
// yaml, // yaml 解析和生成
|
||||||
// getFlag, // 获取 emoji 旗帜
|
// getFlag, // 获取 emoji 旗帜
|
||||||
|
// removeFlag, // 移除 emoji 旗帜
|
||||||
// getISO, // 获取 ISO 3166-1 alpha-2 代码
|
// getISO, // 获取 ISO 3166-1 alpha-2 代码
|
||||||
// Gist, // Gist 类
|
// Gist, // Gist 类
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 示例: 给节点名添加前缀
|
// 示例: 给节点名添加前缀
|
||||||
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
|
// $server.name = `[${ProxyUtils.getISO($server.name)}] ${$server.name}`
|
||||||
|
// 示例: 给节点名添加旗帜
|
||||||
|
// $server.name = `[${ProxyUtils.getFlag($server.name).replace(/🇹🇼/g, '🇼🇸')}] ${ProxyUtils.removeFlag($server.name)}`
|
||||||
|
|
||||||
// 示例: 从 sni 文件中读取内容并进行节点操作
|
// 示例: 从 sni 文件中读取内容并进行节点操作
|
||||||
// const sni = await produceArtifact({
|
// const sni = await produceArtifact({
|
||||||
@@ -100,7 +103,7 @@ function operator(proxies = [], targetPlatform, context) {
|
|||||||
// 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist
|
// 4. 一个比较折腾的方案: 在脚本操作中, 把内容同步到另一个 gist
|
||||||
// 见 https://t.me/zhetengsha/1428
|
// 见 https://t.me/zhetengsha/1428
|
||||||
//
|
//
|
||||||
// const content = ProxyUtils.produce(proxies, platform)
|
// const content = ProxyUtils.produce([...proxies], platform)
|
||||||
|
|
||||||
// // YAML
|
// // YAML
|
||||||
// ProxyUtils.yaml.load('YAML String')
|
// ProxyUtils.yaml.load('YAML String')
|
||||||
|
|||||||
Reference in New Issue
Block a user