Compare commits

..

14 Commits

Author SHA1 Message Date
xream
127ce90bb3 feat: 模块接口 2023-09-14 15:16:12 +08:00
xream
33a17c2d66 feat: 实验性支持本地脚本复用 2023-09-14 08:56:33 +08:00
xream
2c89a0ddbd feat: 支持 Clash VLESS 输出(与 Clash.Meta 的区别为: 无 XTLS 2023-09-11 02:35:36 +08:00
xream
939022e5a3 fix: 修复了 Clash.Meta 输出 VLESS 时 内部字段 sni 未作用到 servername 的问题 2023-09-09 14:03:40 +08:00
xream
59bca5670d fix: 预览时脚本下载报错导致的崩溃 2023-09-07 23:17:36 +08:00
Peng-YM
07b38cf971 release: backend version 2.14.49 2023-09-04 23:16:52 +08:00
Peng-YM
28186f596f feat: added the ability to change the base path for the data files
before starting node, use the command `export SUB_STORE_DATA_BASE_PATH="<YOUR_PATH>"`
2023-09-04 23:16:13 +08:00
xream
ea31b1d0ec fix: 排序接口修正为使用 name 排序 2023-09-04 21:31:55 +08:00
xream
77191f9caa feat: 为 Gist 备份还原增加基础校验逻辑 2023-09-04 17:06:37 +08:00
xream
07a270963e feat: 支持 Surge WireGuard 的输入和输出(由于 Surge 配置的特殊性, 仅支持 同进同出) 支持的字段格式: HK WARP = wireguard, section-name=Cloudflare, no-error-alert=true, underlying-proxy=HK, test-url=http://1.0.0.1/generate_204, ip-version=v4-only 2023-09-01 02:44:43 +08:00
xream
f1e1d50a2c fix: 暂时将后端上传限制放宽到 1mb 2023-09-01 02:07:24 +08:00
Peng-YM
a65cd1f1c9 Update README.md 2023-08-31 16:41:50 +08:00
xream
5b0e2e1ef2 docs: config 2023-08-30 22:52:05 +08:00
xream
b29266ac57 chore: sync to GitLab 2023-08-30 16:19:17 +08:00
23 changed files with 479 additions and 118 deletions

View File

@@ -64,3 +64,8 @@ jobs:
./backend/dist/sub-store-parser.loon.min.js
./backend/dist/cron-sync-artifacts.min.js
./backend/dist/sub-store.bundle.js
- name: Sync to GitLab
env:
GITLAB_PIPELINE_TOKEN: ${{ secrets.GITLAB_PIPELINE_TOKEN }}
run: |
curl -X POST --fail -F token=$GITLAB_PIPELINE_TOKEN -F ref=master https://gitlab.com/api/v4/projects/48891296/trigger/pipeline

View File

@@ -10,7 +10,7 @@
Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.
</p>
[![Build](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/Peng-YM/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/Peng-YM/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/Peng-YM/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/Peng-YM/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/Peng-YM/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)
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM)
@@ -32,7 +32,7 @@ Core functionalities:
- [x] V2RayN URI
- [x] QX (SS, SSR, VMess, Trojan, HTTP)
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, WireGuard, VLESS)
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, SSR(external, only for macOS))
- [x] Surge (SS, VMess, Trojan, HTTP, TUIC, Snell, SSR(external, only for macOS), WireGuard(Surge to Surge))
- [x] ShadowRocket (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Clash.Meta (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, Snell, VLESS, WireGuard, Hysteria)
@@ -99,6 +99,11 @@ This project is under the GPL V3 LICENSE.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FPeng-YM%2FSub-Store?ref=badge_large)
## Star History
[![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
- 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

@@ -9,7 +9,7 @@
* @updated: <%= updated %>
* @version: <%= pkg.version %>
* @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store
* @github: https://github.com/sub-store-org/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/

View File

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

View File

@@ -2,6 +2,8 @@ export const SCHEMA_VERSION_KEY = 'schemaVersion';
export const SETTINGS_KEY = 'settings';
export const SUBS_KEY = 'subs';
export const COLLECTIONS_KEY = 'collections';
export const FILES_KEY = 'files';
export const MODULES_KEY = 'modules';
export const ARTIFACTS_KEY = 'artifacts';
export const RULES_KEY = 'rules';
export const GIST_BACKUP_KEY = 'Auto Generated Sub-Store Backup';

View File

@@ -739,7 +739,7 @@ function Surge_Socks5() {
function Surge_Snell() {
const name = 'Surge Snell Parser';
const test = (line) => {
return /^.*=\s*snell?/.test(line.split(',')[0]);
return /^.*=\s*snell/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
@@ -748,7 +748,15 @@ function Surge_Snell() {
function Surge_Tuic() {
const name = 'Surge Tuic Parser';
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);
return { name, test, parse };
}
function Surge_WireGuard() {
const name = 'Surge WireGuard Parser';
const test = (line) => {
return /^.*=\s*wireguard/.test(line.split(',')[0]);
};
const parse = (line) => getSurgeParser().parse(line);
return { name, test, parse };
@@ -767,6 +775,7 @@ export default [
Surge_Http(),
Surge_Snell(),
Surge_Tuic(),
Surge_WireGuard(),
Surge_Socks5(),
Loon_SS(),
Loon_SSR(),

View File

@@ -32,7 +32,7 @@ const grammars = String.raw`
}
}
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) {
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard) {
return proxy;
}
@@ -83,6 +83,9 @@ tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_veri
proxy.type = "tuic";
proxy.version = 5;
}
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/others)* {
proxy.type = "wireguard-surge";
}
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
proxy.type = "socks5";
}
@@ -187,6 +190,10 @@ udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }

View File

@@ -30,7 +30,7 @@
}
}
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5) {
start = (shadowsocks/vmess/trojan/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard) {
return proxy;
}
@@ -81,6 +81,9 @@ tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/tls_veri
proxy.type = "tuic";
proxy.version = 5;
}
wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/underlying_proxy/test_url/others)* {
proxy.type = "wireguard-surge";
}
socks5 = tag equals "socks5" address (username password)? (fast_open/others)* {
proxy.type = "socks5";
}
@@ -185,6 +188,10 @@ udp_relay = comma "udp" equals flag:bool { proxy.udp = flag; }
fast_open = comma "fast-open" equals flag:bool { proxy.tfo = flag; }
tfo = comma "tfo" equals flag:bool { proxy.tfo = flag; }
ip_version = comma "ip-version" equals match:[^,]+ { proxy["ip-version"] = match.join(""); }
section_name = comma "section-name" equals match:[^,]+ { proxy["section-name"] = match.join(""); }
no_error_alert = comma "no-error-alert" equals match:[^,]+ { proxy["no-error-alert"] = match.join(""); }
underlying_proxy = comma "underlying-proxy" equals match:[^,]+ { proxy["underlying-proxy"] = match.join(""); }
test_url = comma "test-url" equals match:[^,]+ { proxy["test-url"] = match.join(""); }
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }

View File

@@ -3,6 +3,9 @@ import { isPresent } from '@/core/proxy-utils/producers/utils';
export default function Clash_Producer() {
const type = 'ALL';
const produce = (proxies) => {
// VLESS XTLS is not supported by Clash
// https://github.com/MetaCubeX/Clash.Meta/blob/Alpha/docs/config.yaml#L532
// github.com/Dreamacro/clash/pull/2891/files
// filter unsupported proxies
proxies = proxies.filter((proxy) => {
if (
@@ -10,17 +13,17 @@ export default function Clash_Producer() {
'ss',
'ssr',
'vmess',
'vless',
'socks',
'http',
'snell',
'trojan',
'wireguard',
].includes(proxy.type)
) {
return false;
} else if (
proxy.type === 'snell' &&
String(proxy.version) === '4'
].includes(proxy.type) ||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
(proxy.type === 'vless' &&
(typeof proxy.flow !== 'undefined' ||
proxy['reality-opts']))
) {
return false;
}

View File

@@ -81,6 +81,11 @@ export default function ClashMeta_Producer() {
proxy['preshared-key'] =
proxy['preshared-key'] ?? proxy['pre-shared-key'];
proxy['pre-shared-key'] = proxy['preshared-key'];
} else if (proxy.type === 'vless') {
if (isPresent(proxy, 'sni')) {
proxy.servername = proxy.sni;
delete proxy.sni;
}
}
if (

View File

@@ -29,6 +29,8 @@ export default function Surge_Producer() {
return snell(proxy);
case 'tuic':
return tuic(proxy);
case 'wireguard-surge':
return wireguard(proxy);
}
throw new Error(
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -289,6 +291,34 @@ function tuic(proxy) {
return result.toString();
}
function wireguard(proxy) {
const result = new Result(proxy);
result.append(`${proxy.name}=wireguard`);
result.appendIfPresent(
`,section-name=${proxy['section-name']}`,
'section-name',
);
result.appendIfPresent(
`,no-error-alert=${proxy['no-error-alert']}`,
'no-error-alert',
);
result.appendIfPresent(
`,underlying-proxy=${proxy['underlying-proxy']}`,
'underlying-proxy',
);
result.appendIfPresent(
`,ip-version=${ipVersions[proxy['ip-version']] || proxy['ip-version']}`,
'ip-version',
);
// test-url
result.appendIfPresent(`,test-url=${proxy['test-url']}`, 'test-url');
return result.toString();
}
function handleTransport(result, proxy) {
if (isPresent(proxy, 'network')) {
if (proxy.network === 'ws') {

View File

@@ -7,7 +7,7 @@
* ╚══════╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝
* Advanced Subscription Manager for QX, Loon, Surge and Clash.
* @author: Peng-YM
* @github: https://github.com/Peng-YM/Sub-Store
* @github: https://github.com/sub-store-org/Sub-Store
* @documentation: https://www.notion.so/Sub-Store-6259586994d34c11a4ced5c406264b46
*/
import { version } from '../package.json';

103
backend/src/restful/file.js Normal file
View File

@@ -0,0 +1,103 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
export default function register($app) {
if (!$.read(FILES_KEY)) $.write([], FILES_KEY);
$app.route('/api/file/:name')
.get(getFile)
.patch(updateFile)
.delete(deleteFile);
$app.route('/api/files').get(getAllFiles).post(createFile).put(replaceFile);
}
// file API
function createFile(req, res) {
const file = req.body;
$.info(`正在创建文件:${file.name}`);
const allFiles = $.read(FILES_KEY);
if (findByName(allFiles, file.name)) {
failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
`File ${file.name} already exists.`,
),
);
}
allFiles.push(file);
$.write(allFiles, FILES_KEY);
success(res, file, 201);
}
function getFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, name);
if (file) {
success(res, file);
} else {
failed(
res,
new ResourceNotFoundError(
`FILE_NOT_FOUND`,
`File ${name} does not exist`,
404,
),
);
}
}
function updateFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let file = req.body;
const allFiles = $.read(FILES_KEY);
const oldFile = findByName(allFiles, name);
if (oldFile) {
const newFile = {
...oldFile,
...file,
};
$.info(`正在更新文件:${name}...`);
updateByName(allFiles, name, newFile);
$.write(allFiles, FILES_KEY);
success(res, newFile);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`File ${name} does not exist!`,
),
404,
);
}
}
function deleteFile(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除文件:${name}`);
let allFiles = $.read(FILES_KEY);
deleteByName(allFiles, name);
$.write(allFiles, FILES_KEY);
success(res);
}
function getAllFiles(req, res) {
const allFiles = $.read(FILES_KEY);
success(res, allFiles);
}
function replaceFile(req, res) {
const allFiles = req.body;
$.write(allFiles, FILES_KEY);
success(res);
}

View File

@@ -4,6 +4,8 @@ import $ from '@/core/app';
import registerSubscriptionRoutes from './subscriptions';
import registerCollectionRoutes from './collections';
import registerArtifactRoutes from './artifacts';
import registerFileRoutes from './file';
import registerModuleRoutes from './module';
import registerSyncRoutes from './sync';
import registerDownloadRoutes from './download';
import registerSettingRoutes from './settings';
@@ -23,6 +25,8 @@ export default function serve() {
registerSortingRoutes($app);
registerSettingRoutes($app);
registerArtifactRoutes($app);
registerFileRoutes($app);
registerModuleRoutes($app);
registerSyncRoutes($app);
registerNodeInfoRoutes($app);
registerMiscRoutes($app);

View File

@@ -118,6 +118,23 @@ async function gistBackup(req, res) {
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) {

View File

@@ -0,0 +1,106 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { MODULES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
import { RequestInvalidError, ResourceNotFoundError } from '@/restful/errors';
export default function register($app) {
if (!$.read(MODULES_KEY)) $.write([], MODULES_KEY);
$app.route('/api/module/:name')
.get(getModule)
.patch(updateModule)
.delete(deleteModule);
$app.route('/api/modules')
.get(getAllModules)
.post(createModule)
.put(replaceModule);
}
// module API
function createModule(req, res) {
const module = req.body;
$.info(`正在创建模块:${module.name}`);
const allModules = $.read(MODULES_KEY);
if (findByName(allModules, module.name)) {
failed(
res,
new RequestInvalidError(
'DUPLICATE_KEY',
`Module ${module.name} already exists.`,
),
);
}
allModules.push(module);
$.write(allModules, MODULES_KEY);
success(res, module, 201);
}
function getModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
const allModules = $.read(MODULES_KEY);
const module = findByName(allModules, name);
if (module) {
success(res, module);
} else {
failed(
res,
new ResourceNotFoundError(
`MODULE_NOT_FOUND`,
`Module ${name} does not exist`,
404,
),
);
}
}
function updateModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
let module = req.body;
const allModules = $.read(MODULES_KEY);
const oldModule = findByName(allModules, name);
if (oldModule) {
const newModule = {
...oldModule,
...module,
};
$.info(`正在更新模块:${name}...`);
updateByName(allModules, name, newModule);
$.write(allModules, MODULES_KEY);
success(res, newModule);
} else {
failed(
res,
new ResourceNotFoundError(
'RESOURCE_NOT_FOUND',
`Module ${name} does not exist!`,
),
404,
);
}
}
function deleteModule(req, res) {
let { name } = req.params;
name = decodeURIComponent(name);
$.info(`正在删除模块:${name}`);
let allModules = $.read(MODULES_KEY);
deleteByName(allModules, name);
$.write(allModules, MODULES_KEY);
success(res);
}
function getAllModules(req, res) {
const allModules = $.read(MODULES_KEY);
success(res, allModules);
}
function replaceModule(req, res) {
const allModules = req.body;
$.write(allModules, MODULES_KEY);
success(res);
}

View File

@@ -12,98 +12,122 @@ export default function register($app) {
}
async function compareSub(req, res) {
const sub = req.body;
const target = req.query.target || 'JSON';
let content;
if (sub.source === 'local') {
content = sub.content;
} else {
try {
content = await download(sub.url, sub.ua);
} catch (err) {
failed(
res,
new NetworkError(
'FAILED_TO_DOWNLOAD_RESOURCE',
'无法下载远程资源',
`Reason: ${err}`,
),
);
return;
}
}
// parse proxies
const original = ProxyUtils.parse(content);
// add id
original.forEach((proxy, i) => {
proxy.id = i;
});
// apply processors
const processed = await ProxyUtils.process(
original,
sub.process || [],
target,
);
// produce
success(res, { original, processed });
}
async function compareCollection(req, res) {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = collection.subscriptions;
const results = {};
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
try {
const sub = req.body;
const target = req.query.target || 'JSON';
let content;
if (sub.source === 'local') {
content = sub.content;
} else {
try {
let raw;
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
'JSON',
);
results[name] = currentProxies;
content = await download(sub.url, sub.ua);
} catch (err) {
failed(
res,
new InternalServerError(
'PROCESS_FAILED',
`处理子订阅 ${name} 失败`,
new NetworkError(
'FAILED_TO_DOWNLOAD_RESOURCE',
'无法下载远程资源',
`Reason: ${err}`,
),
);
return;
}
}),
);
}
// parse proxies
const original = ProxyUtils.parse(content);
// merge proxies with the original order
const original = Array.prototype.concat.apply(
[],
subnames.map((name) => results[name] || []),
);
// add id
original.forEach((proxy, i) => {
proxy.id = i;
});
original.forEach((proxy, i) => {
proxy.id = i;
});
// apply processors
const processed = await ProxyUtils.process(
original,
sub.process || [],
target,
);
const processed = await ProxyUtils.process(
original,
collection.process || [],
'JSON',
);
success(res, { original, processed });
// produce
success(res, { original, processed });
} catch (err) {
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview subscription`,
`Reason: ${err.message ?? err}`,
),
);
}
}
async function compareCollection(req, res) {
try {
const allSubs = $.read(SUBS_KEY);
const collection = req.body;
const subnames = collection.subscriptions;
const results = {};
await Promise.all(
subnames.map(async (name) => {
const sub = findByName(allSubs, name);
try {
let raw;
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let currentProxies = ProxyUtils.parse(raw);
// apply processors
currentProxies = await ProxyUtils.process(
currentProxies,
sub.process || [],
'JSON',
);
results[name] = currentProxies;
} catch (err) {
failed(
res,
new InternalServerError(
'PROCESS_FAILED',
`处理子订阅 ${name} 失败`,
`Reason: ${err}`,
),
);
}
}),
);
// merge proxies with the original order
const original = Array.prototype.concat.apply(
[],
subnames.map((name) => results[name] || []),
);
original.forEach((proxy, i) => {
proxy.id = i;
});
const processed = await ProxyUtils.process(
original,
collection.process || [],
'JSON',
);
success(res, { original, processed });
} catch (err) {
$.error(err.message ?? err);
failed(
res,
new InternalServerError(
`INTERNAL_SERVER_ERROR`,
`Failed to preview collection`,
`Reason: ${err.message ?? err}`,
),
);
}
}

View File

@@ -11,7 +11,7 @@ export default function register($app) {
function sortSubs(req, res) {
const orders = req.body;
const allSubs = $.read(SUBS_KEY);
allSubs.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
allSubs.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allSubs, SUBS_KEY);
success(res, allSubs);
}
@@ -19,7 +19,7 @@ function sortSubs(req, res) {
function sortCollections(req, res) {
const orders = req.body;
const allCols = $.read(COLLECTIONS_KEY);
allCols.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
allCols.sort((a, b) => orders.indexOf(a.name) - orders.indexOf(b.name));
$.write(allCols, COLLECTIONS_KEY);
success(res, allCols);
}
@@ -27,7 +27,9 @@ function sortCollections(req, res) {
function sortArtifacts(req, res) {
const orders = req.body;
const allArtifacts = $.read(ARTIFACTS_KEY);
allArtifacts.sort((a, b) => orders.indexOf(a) - orders.indexOf(b));
allArtifacts.sort(
(a, b) => orders.indexOf(a.name) - orders.indexOf(b.name),
);
$.write(allArtifacts, ARTIFACTS_KEY);
success(res, allArtifacts);
}

View File

@@ -1,10 +1,28 @@
import { FILES_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { HTTP, ENV } from '@/vendor/open-api';
import { hex_md5 } from '@/vendor/md5';
import resourceCache from '@/utils/resource-cache';
import $ from '@/core/app';
const tasks = new Map();
export default async function download(url, ua) {
const downloadUrlMatch = url.match(/^\/api\/file\/(.+)/);
if (downloadUrlMatch) {
let fileName = downloadUrlMatch?.[1];
if (fileName == null) {
throw new Error(`本地文件 URL 无效: ${url}`);
}
fileName = decodeURIComponent(fileName);
const allFiles = $.read(FILES_KEY);
const file = findByName(allFiles, fileName);
if (!file) {
throw new Error(`找不到本地文件: ${fileName}`);
}
return file.content;
}
const { isNode } = ENV();
ua = ua || 'Quantumult%20X/1.0.29 (iPhone14,5; iOS 15.4.1)';
const id = hex_md5(ua + url);

View File

@@ -17,7 +17,7 @@ export default function express({ substore: $, port }) {
const express_ = eval(`require("express")`);
const bodyParser = eval(`require("body-parser")`);
const app = express_();
app.use(bodyParser.json({ verify: rawBodySaver }));
app.use(bodyParser.json({ verify: rawBodySaver, limit: '1mb' }));
app.use(
bodyParser.urlencoded({ verify: rawBodySaver, extended: true }),
);

View File

@@ -49,18 +49,23 @@ export class OpenAPI {
if (isNode) {
// create a json for root cache
let fpath = 'root.json';
if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
let rootPath = `${basePath}/root.json`;
this.log(`Root path: ${rootPath}`);
if (!this.node.fs.existsSync(rootPath)) {
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
flag: 'wx',
});
this.root = {};
} else {
this.root = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
this.root = JSON.parse(this.node.fs.readFileSync(`${rootPath}`));
}
// create a json file with the given name if not exists
fpath = `${this.name}.json`;
let fpath = `${basePath}/${this.name}.json`;
this.log(`Data path: ${fpath}`);
if (!this.node.fs.existsSync(fpath)) {
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
flag: 'wx',
@@ -68,7 +73,7 @@ export class OpenAPI {
this.cache = {};
} else {
this.cache = JSON.parse(
this.node.fs.readFileSync(`${this.name}.json`),
this.node.fs.readFileSync(`${fpath}`),
);
}
}
@@ -80,14 +85,17 @@ export class OpenAPI {
if (isQX) $prefs.setValueForKey(data, this.name);
if (isLoon || isSurge) $persistentStore.write(data, this.name);
if (isNode) {
const basePath =
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
this.node.fs.writeFileSync(
`${this.name}.json`,
`${basePath}/${this.name}.json`,
data,
{ flag: 'w' },
(err) => console.log(err),
);
this.node.fs.writeFileSync(
'root.json',
`${basePath}/root.json`,
JSON.stringify(this.root, null, 2),
{ flag: 'w' },
(err) => console.log(err),

View File

@@ -2,7 +2,7 @@
#!desc=高级订阅管理工具
#!openUrl=https://sub.store
#!author=Peng-YM
#!homepage=https://github.com/Peng-YM/Sub-Store
#!homepage=https://github.com/sub-store-org/Sub-Store
#!icon=https://raw.githubusercontent.com/58xinian/icon/master/Sub-Store1.png
#!select = 节点缓存有效期,1分钟,5分钟,10分钟,30分钟,1小时,2小时,3小时,6小时,12小时,24小时,48小时,72小时,参数传入

View File

@@ -2,25 +2,31 @@
## 查看更新说明:
[Sub-Store Releases](https://github.com/sub-store-org/Sub-Store/releases)
Sub-Store Releases: [`https://github.com/sub-store-org/Sub-Store/releases`](https://github.com/sub-store-org/Sub-Store/releases)
[Telegram 频道](https://t.me/cool_scripts)
Telegram 频道: [`https://t.me/cool_scripts` ](https://t.me/cool_scripts)
## 脚本配置:
### 1. Loon
安装使用[插件](https://raw.githubusercontent.com/Peng-YM/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
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
1. 官方默认版模块(目前不带 ability 参数, 不保证以后不会改动): [`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)
2. 固定带 ability 参数版本,可能会爆内存, 如果需要使用指定节点功能 例如[加国旗脚本或者cname脚本] 请使用此带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-ability.sgmodule)
3. 固定不带 ability 参数版本: [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Surge-Noability.sgmodule)
### 3. QX
订阅[重写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/QX.snippet)即可
订阅 重写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/QX.snippet) 即可
### 4. Stash
安装使用[ Stash 覆写](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Stash.stoverride)即可。
安装使用 覆写 [`https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride`](https://raw.githubusercontent.com/sub-store-org/Sub-Store/master/config/Stash.stoverride) 即可。
### 5. Shadowrocket
安装使用[模块](https://raw.githubusercontent.com/Peng-YM/Sub-Store/master/config/Surge.sgmodule)即可。
安装使用 模块 [`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) 即可。
## 使用 Sub-Store
1. 使用 Safari 打开这个 https://sub.store 如网页正常打开并且未弹出任何错误提示,说明 Sub-Store 已经配置成功。