mirror of
https://git.mirrors.martin98.com/https://github.com/sub-store-org/Sub-Store.git
synced 2025-08-22 03:19:10 +08:00
Compare commits
20 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cacc106c68 | ||
![]() |
542fcc44a1 | ||
![]() |
dca3d2f79c | ||
![]() |
3e14f91347 | ||
![]() |
4aafdaaddb | ||
![]() |
e4f646af0c | ||
![]() |
532be2ff8c | ||
![]() |
37fc7ac88e | ||
![]() |
9e0028219d | ||
![]() |
54750d552b | ||
![]() |
0e7561a069 | ||
![]() |
6804c6368a | ||
![]() |
9c5d6e9a10 | ||
![]() |
ef2d6be8eb | ||
![]() |
04e12a4836 | ||
![]() |
f94cf7185a | ||
![]() |
fa7df51f8c | ||
![]() |
18659d1cc8 | ||
![]() |
1d12dc55bd | ||
![]() |
af9a2c86c1 |
@ -42,7 +42,7 @@ Core functionalities:
|
|||||||
- [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, AnyTLS)
|
- [x] Clash.Meta (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, AnyTLS)
|
||||||
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
|
- [x] Stash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, TUIC, Juicity, SSH)
|
||||||
|
|
||||||
Deprecated:
|
Deprecated(The frontend doesn't show it, but the backend still supports it, with the query parameter `target=Clash`):
|
||||||
|
|
||||||
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
|
- [x] Clash (SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard)
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sub-store",
|
"name": "sub-store",
|
||||||
"version": "2.19.39",
|
"version": "2.19.60",
|
||||||
"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": {
|
||||||
|
@ -142,9 +142,9 @@ async function processFn(
|
|||||||
? `#${rawArgs[1]}`
|
? `#${rawArgs[1]}`
|
||||||
: ''
|
: ''
|
||||||
}`;
|
}`;
|
||||||
const downloadUrlMatch = url.match(
|
const downloadUrlMatch = url
|
||||||
/^\/api\/(file|module)\/(.+)/,
|
.split('#')[0]
|
||||||
);
|
.match(/^\/api\/(file|module)\/(.+)/);
|
||||||
if (downloadUrlMatch) {
|
if (downloadUrlMatch) {
|
||||||
let type = '';
|
let type = '';
|
||||||
try {
|
try {
|
||||||
@ -174,6 +174,17 @@ async function processFn(
|
|||||||
);
|
);
|
||||||
throw new Error(`无法加载 ${type}: ${url}`);
|
throw new Error(`无法加载 ${type}: ${url}`);
|
||||||
}
|
}
|
||||||
|
} else if (url?.startsWith('/')) {
|
||||||
|
try {
|
||||||
|
const fs = eval(`require("fs")`);
|
||||||
|
script = fs.readFileSync(url.split('#')[0], 'utf8');
|
||||||
|
// $.info(`Script loaded: >>>\n ${script}`);
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`Error when reading local script: ${item.args.content}.\n Reason: ${err}`,
|
||||||
|
);
|
||||||
|
throw new Error(`无法从该路径读取脚本文件: ${url}`);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// if this is a remote script, download it
|
// if this is a remote script, download it
|
||||||
try {
|
try {
|
||||||
|
@ -439,7 +439,16 @@ function URI_VMess() {
|
|||||||
type: 'vmess',
|
type: 'vmess',
|
||||||
server,
|
server,
|
||||||
port,
|
port,
|
||||||
cipher: getIfPresent(params.scy, 'auto'),
|
// https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link
|
||||||
|
// https://github.com/XTLS/Xray-core/issues/91
|
||||||
|
cipher: [
|
||||||
|
'auto',
|
||||||
|
'aes-128-gcm',
|
||||||
|
'chacha20-poly1305',
|
||||||
|
'none',
|
||||||
|
].includes(params.scy)
|
||||||
|
? params.scy
|
||||||
|
: 'auto',
|
||||||
uuid: params.id,
|
uuid: params.id,
|
||||||
alterId: parseInt(
|
alterId: parseInt(
|
||||||
getIfPresent(params.aid ?? params.alterId, 0),
|
getIfPresent(params.aid ?? params.alterId, 0),
|
||||||
@ -473,8 +482,8 @@ function URI_VMess() {
|
|||||||
['http'].includes(params.type)
|
['http'].includes(params.type)
|
||||||
) {
|
) {
|
||||||
proxy.network = 'http';
|
proxy.network = 'http';
|
||||||
} else if (['grpc'].includes(params.net)) {
|
} else if (['grpc', 'kcp', 'quic'].includes(params.net)) {
|
||||||
proxy.network = 'grpc';
|
proxy.network = params.net;
|
||||||
} else if (
|
} else if (
|
||||||
params.net === 'httpupgrade' ||
|
params.net === 'httpupgrade' ||
|
||||||
proxy.network === 'httpupgrade'
|
proxy.network === 'httpupgrade'
|
||||||
@ -524,13 +533,28 @@ function URI_VMess() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 传输层应该有配置, 暂时不考虑兼容不给配置的节点
|
// 传输层应该有配置, 暂时不考虑兼容不给配置的节点
|
||||||
if (transportPath || transportHost) {
|
if (
|
||||||
|
transportPath ||
|
||||||
|
transportHost ||
|
||||||
|
['kcp', 'quic'].includes(proxy.network)
|
||||||
|
) {
|
||||||
if (['grpc'].includes(proxy.network)) {
|
if (['grpc'].includes(proxy.network)) {
|
||||||
proxy[`${proxy.network}-opts`] = {
|
proxy[`${proxy.network}-opts`] = {
|
||||||
'grpc-service-name': getIfNotBlank(transportPath),
|
'grpc-service-name': getIfNotBlank(transportPath),
|
||||||
'_grpc-type': getIfNotBlank(params.type),
|
'_grpc-type': getIfNotBlank(params.type),
|
||||||
'_grpc-authority': getIfNotBlank(params.authority),
|
'_grpc-authority': getIfNotBlank(params.authority),
|
||||||
};
|
};
|
||||||
|
} else if (['kcp', 'quic'].includes(proxy.network)) {
|
||||||
|
proxy[`${proxy.network}-opts`] = {
|
||||||
|
[`_${proxy.network}-type`]: getIfNotBlank(
|
||||||
|
params.type,
|
||||||
|
),
|
||||||
|
[`_${proxy.network}-host`]: getIfNotBlank(
|
||||||
|
getIfNotBlank(transportHost),
|
||||||
|
),
|
||||||
|
[`_${proxy.network}-path`]:
|
||||||
|
getIfNotBlank(transportPath),
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
const opts = {
|
const opts = {
|
||||||
path: getIfNotBlank(transportPath),
|
path: getIfNotBlank(transportPath),
|
||||||
@ -546,6 +570,12 @@ function URI_VMess() {
|
|||||||
delete proxy.network;
|
delete proxy.network;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
proxy['client-fingerprint'] = params.fp;
|
||||||
|
proxy.alpn = params.alpn ? params.alpn.split(',') : undefined;
|
||||||
|
// 然而 wiki 和 app 实测中都没有字段表示这个
|
||||||
|
// proxy['skip-cert-verify'] = /(TRUE)|1/i.test(params.allowInsecure);
|
||||||
|
|
||||||
return proxy;
|
return proxy;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -55,10 +55,11 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
|
|||||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "vmess";
|
proxy.type = "vmess";
|
||||||
proxy.cipher = proxy.cipher || "none";
|
proxy.cipher = proxy.cipher || "none";
|
||||||
|
// Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess
|
||||||
if (proxy.aead) {
|
if (proxy.aead) {
|
||||||
proxy.alterId = 0;
|
proxy.alterId = 0;
|
||||||
} else {
|
} else {
|
||||||
proxy.alterId = proxy.alterId || 0;
|
proxy.alterId = 1;
|
||||||
}
|
}
|
||||||
handleWebsocket();
|
handleWebsocket();
|
||||||
handleShadowTLS();
|
handleShadowTLS();
|
||||||
|
@ -53,10 +53,11 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
|
|||||||
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
|
||||||
proxy.type = "vmess";
|
proxy.type = "vmess";
|
||||||
proxy.cipher = proxy.cipher || "none";
|
proxy.cipher = proxy.cipher || "none";
|
||||||
|
// Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess
|
||||||
if (proxy.aead) {
|
if (proxy.aead) {
|
||||||
proxy.alterId = 0;
|
proxy.alterId = 0;
|
||||||
} else {
|
} else {
|
||||||
proxy.alterId = proxy.alterId || 0;
|
proxy.alterId = 1;
|
||||||
}
|
}
|
||||||
handleWebsocket();
|
handleWebsocket();
|
||||||
handleShadowTLS();
|
handleShadowTLS();
|
||||||
|
@ -50,6 +50,26 @@ function Base64Encoded() {
|
|||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fallbackBase64Encoded() {
|
||||||
|
const name = 'Fallback Base64 Pre-processor';
|
||||||
|
|
||||||
|
const test = function (raw) {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const parse = function (raw) {
|
||||||
|
const decoded = Base64.decode(raw);
|
||||||
|
if (!/^\w+(:\/\/|\s*?=\s*?)\w+/m.test(decoded)) {
|
||||||
|
$.error(
|
||||||
|
`Fallback Base64 Pre-processor error: decoded line does not start with protocol`,
|
||||||
|
);
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded;
|
||||||
|
};
|
||||||
|
return { name, test, parse };
|
||||||
|
}
|
||||||
|
|
||||||
function Clash() {
|
function Clash() {
|
||||||
const name = 'Clash Pre-processor';
|
const name = 'Clash Pre-processor';
|
||||||
const test = function (raw) {
|
const test = function (raw) {
|
||||||
@ -163,4 +183,11 @@ function FullConfig() {
|
|||||||
return { name, test, parse };
|
return { name, test, parse };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default [HTML(), Clash(), Base64Encoded(), SSD(), FullConfig()];
|
export default [
|
||||||
|
HTML(),
|
||||||
|
Clash(),
|
||||||
|
Base64Encoded(),
|
||||||
|
SSD(),
|
||||||
|
FullConfig(),
|
||||||
|
fallbackBase64Encoded(),
|
||||||
|
];
|
||||||
|
@ -365,7 +365,7 @@ function vmess(proxy) {
|
|||||||
|
|
||||||
// AEAD
|
// AEAD
|
||||||
if (isPresent(proxy, 'aead')) {
|
if (isPresent(proxy, 'aead')) {
|
||||||
result.append(`,alterId=0`);
|
result.append(`,alterId=${proxy.aead ? 0 : 1}`);
|
||||||
} else {
|
} else {
|
||||||
result.append(`,alterId=${proxy.alterId}`);
|
result.append(`,alterId=${proxy.alterId}`);
|
||||||
}
|
}
|
||||||
|
@ -405,6 +405,8 @@ function vless(proxy) {
|
|||||||
else append(`,obfs=ws`);
|
else append(`,obfs=ws`);
|
||||||
} else if (proxy.network === 'http') {
|
} else if (proxy.network === 'http') {
|
||||||
append(`,obfs=http`);
|
append(`,obfs=http`);
|
||||||
|
} else if (['tcp'].includes(proxy.network)) {
|
||||||
|
if (proxy.tls) append(`,obfs=over-tls`);
|
||||||
} else if (!['tcp'].includes(proxy.network)) {
|
} else if (!['tcp'].includes(proxy.network)) {
|
||||||
throw new Error(`network ${proxy.network} is unsupported`);
|
throw new Error(`network ${proxy.network} is unsupported`);
|
||||||
}
|
}
|
||||||
|
@ -9,12 +9,7 @@ export default function Shadowrocket_Producer() {
|
|||||||
if (opts['include-unsupported-proxy']) return true;
|
if (opts['include-unsupported-proxy']) return true;
|
||||||
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
if (proxy.type === 'snell' && String(proxy.version) === '4') {
|
||||||
return false;
|
return false;
|
||||||
} else if (['mieru', 'anytls'].includes(proxy.type)) {
|
} else if (['mieru'].includes(proxy.type)) {
|
||||||
return false;
|
|
||||||
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
|
|
||||||
$.error(
|
|
||||||
`Shadowrocket 不支持前置代理字段. 已过滤节点 ${proxy.name}. 请使用 App 内的 "代理通过" 功能`,
|
|
||||||
);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -519,6 +519,7 @@ const vlessParser = (proxy = {}) => {
|
|||||||
};
|
};
|
||||||
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
if (parsedProxy.server_port < 0 || parsedProxy.server_port > 65535)
|
||||||
throw 'invalid port';
|
throw 'invalid port';
|
||||||
|
if (proxy.xudp) parsedProxy.packet_encoding = 'xudp';
|
||||||
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
if (proxy['fast-open']) parsedProxy.udp_fragment = true;
|
||||||
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
|
if (proxy.flow === 'xtls-rprx-vision') parsedProxy.flow = proxy.flow;
|
||||||
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
if (proxy.network === 'ws') wsParser(proxy, parsedProxy);
|
||||||
|
@ -44,11 +44,9 @@ export default function Stash_Producer() {
|
|||||||
'2022-blake3-aes-256-gcm',
|
'2022-blake3-aes-256-gcm',
|
||||||
].includes(proxy.cipher)) ||
|
].includes(proxy.cipher)) ||
|
||||||
(proxy.type === 'snell' && String(proxy.version) === '4') ||
|
(proxy.type === 'snell' && String(proxy.version) === '4') ||
|
||||||
(opts['include-unsupported-proxy']
|
(proxy.type === 'vless' &&
|
||||||
? proxy.type === 'vless' &&
|
proxy['reality-opts'] &&
|
||||||
proxy['reality-opts'] &&
|
!['xtls-rprx-vision'].includes(proxy.flow))
|
||||||
!['xtls-rprx-vision'].includes(proxy.flow)
|
|
||||||
: proxy.type === 'vless' && proxy['reality-opts'])
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
|
} else if (proxy['underlying-proxy'] || proxy['dialer-proxy']) {
|
||||||
|
@ -33,7 +33,9 @@ export default function URI_Producer() {
|
|||||||
switch (proxy.type) {
|
switch (proxy.type) {
|
||||||
case 'socks5':
|
case 'socks5':
|
||||||
result = `socks://${encodeURIComponent(
|
result = `socks://${encodeURIComponent(
|
||||||
Base64.encode(`${proxy.username}:${proxy.password}`),
|
Base64.encode(
|
||||||
|
`${proxy.username ?? ''}:${proxy.password ?? ''}`,
|
||||||
|
),
|
||||||
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
|
)}@${proxy.server}:${proxy.port}#${proxy.name}`;
|
||||||
break;
|
break;
|
||||||
case 'ss':
|
case 'ss':
|
||||||
@ -117,12 +119,17 @@ export default function URI_Producer() {
|
|||||||
v: '2',
|
v: '2',
|
||||||
ps: proxy.name,
|
ps: proxy.name,
|
||||||
add: proxy.server,
|
add: proxy.server,
|
||||||
port: proxy.port,
|
port: `${proxy.port}`,
|
||||||
id: proxy.uuid,
|
id: proxy.uuid,
|
||||||
type,
|
aid: `${proxy.alterId || 0}`,
|
||||||
aid: proxy.alterId || 0,
|
scy: proxy.cipher,
|
||||||
net,
|
net,
|
||||||
|
type,
|
||||||
tls: proxy.tls ? 'tls' : '',
|
tls: proxy.tls ? 'tls' : '',
|
||||||
|
alpn: Array.isArray(proxy.alpn)
|
||||||
|
? proxy.alpn.join(',')
|
||||||
|
: proxy.alpn,
|
||||||
|
fp: proxy['client-fingerprint'],
|
||||||
};
|
};
|
||||||
if (proxy.tls && proxy.sni) {
|
if (proxy.tls && proxy.sni) {
|
||||||
result.sni = proxy.sni;
|
result.sni = proxy.sni;
|
||||||
@ -133,16 +140,7 @@ export default function URI_Producer() {
|
|||||||
proxy[`${proxy.network}-opts`]?.path;
|
proxy[`${proxy.network}-opts`]?.path;
|
||||||
let vmessTransportHost =
|
let vmessTransportHost =
|
||||||
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
proxy[`${proxy.network}-opts`]?.headers?.Host;
|
||||||
if (vmessTransportPath) {
|
|
||||||
result.path = Array.isArray(vmessTransportPath)
|
|
||||||
? vmessTransportPath[0]
|
|
||||||
: vmessTransportPath;
|
|
||||||
}
|
|
||||||
if (vmessTransportHost) {
|
|
||||||
result.host = Array.isArray(vmessTransportHost)
|
|
||||||
? vmessTransportHost[0]
|
|
||||||
: vmessTransportHost;
|
|
||||||
}
|
|
||||||
if (['grpc'].includes(proxy.network)) {
|
if (['grpc'].includes(proxy.network)) {
|
||||||
result.path =
|
result.path =
|
||||||
proxy[`${proxy.network}-opts`]?.[
|
proxy[`${proxy.network}-opts`]?.[
|
||||||
@ -154,6 +152,31 @@ export default function URI_Producer() {
|
|||||||
'gun';
|
'gun';
|
||||||
result.host =
|
result.host =
|
||||||
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
|
proxy[`${proxy.network}-opts`]?.['_grpc-authority'];
|
||||||
|
} else if (['kcp', 'quic'].includes(proxy.network)) {
|
||||||
|
// https://github.com/XTLS/Xray-core/issues/91
|
||||||
|
result.type =
|
||||||
|
proxy[`${proxy.network}-opts`]?.[
|
||||||
|
`_${proxy.network}-type`
|
||||||
|
] || 'none';
|
||||||
|
result.host =
|
||||||
|
proxy[`${proxy.network}-opts`]?.[
|
||||||
|
`_${proxy.network}-host`
|
||||||
|
];
|
||||||
|
result.path =
|
||||||
|
proxy[`${proxy.network}-opts`]?.[
|
||||||
|
`_${proxy.network}-path`
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
if (vmessTransportPath) {
|
||||||
|
result.path = Array.isArray(vmessTransportPath)
|
||||||
|
? vmessTransportPath[0]
|
||||||
|
: vmessTransportPath;
|
||||||
|
}
|
||||||
|
if (vmessTransportHost) {
|
||||||
|
result.host = Array.isArray(vmessTransportHost)
|
||||||
|
? vmessTransportHost[0]
|
||||||
|
: vmessTransportHost;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result = 'vmess://' + Base64.encode(JSON.stringify(result));
|
result = 'vmess://' + Base64.encode(JSON.stringify(result));
|
||||||
|
@ -259,7 +259,7 @@ async function downloadSubscription(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!$arguments.noFlow) {
|
if (!$arguments.noFlow && /^https?/.test(url)) {
|
||||||
// forward flow headers
|
// forward flow headers
|
||||||
flowInfo = await getFlowHeaders(
|
flowInfo = await getFlowHeaders(
|
||||||
$arguments?.insecure ? `${url}#insecure` : url,
|
$arguments?.insecure ? `${url}#insecure` : url,
|
||||||
@ -506,7 +506,7 @@ async function downloadCollection(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!$arguments.noFlow) {
|
if (!$arguments.noFlow && /^https?:/.test(url)) {
|
||||||
subUserInfoOfSub = await getFlowHeaders(
|
subUserInfoOfSub = await getFlowHeaders(
|
||||||
$arguments?.insecure ? `${url}#insecure` : url,
|
$arguments?.insecure ? `${url}#insecure` : url,
|
||||||
$arguments.flowUserAgent,
|
$arguments.flowUserAgent,
|
||||||
|
@ -64,6 +64,7 @@ async function getFile(req, res) {
|
|||||||
ignoreFailedRemoteFile,
|
ignoreFailedRemoteFile,
|
||||||
proxy,
|
proxy,
|
||||||
noCache,
|
noCache,
|
||||||
|
produceType,
|
||||||
} = req.query;
|
} = req.query;
|
||||||
let $options = {
|
let $options = {
|
||||||
_req: {
|
_req: {
|
||||||
@ -128,6 +129,10 @@ async function getFile(req, res) {
|
|||||||
if (noCache) {
|
if (noCache) {
|
||||||
$.info(`指定不使用缓存: ${noCache}`);
|
$.info(`指定不使用缓存: ${noCache}`);
|
||||||
}
|
}
|
||||||
|
if (produceType) {
|
||||||
|
produceType = decodeURIComponent(produceType);
|
||||||
|
$.info(`指定生产类型: ${produceType}`);
|
||||||
|
}
|
||||||
|
|
||||||
const allFiles = $.read(FILES_KEY);
|
const allFiles = $.read(FILES_KEY);
|
||||||
const file = findByName(allFiles, name);
|
const file = findByName(allFiles, name);
|
||||||
@ -144,6 +149,8 @@ async function getFile(req, res) {
|
|||||||
$options,
|
$options,
|
||||||
proxy,
|
proxy,
|
||||||
noCache,
|
noCache,
|
||||||
|
produceType,
|
||||||
|
all: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -178,9 +185,15 @@ async function getFile(req, res) {
|
|||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
res.set('Content-Type', 'text/plain; charset=utf-8').send(
|
res.set('Content-Type', 'text/plain; charset=utf-8');
|
||||||
output ?? '',
|
if (output?.$options?._res?.headers) {
|
||||||
);
|
Object.entries(output.$options._res.headers).forEach(
|
||||||
|
([key, value]) => {
|
||||||
|
res.set(key, value);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
res.send(output?.$content ?? '');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$.notify(
|
$.notify(
|
||||||
`🌍 Sub-Store 下载文件失败`,
|
`🌍 Sub-Store 下载文件失败`,
|
||||||
|
@ -49,11 +49,17 @@ export default function register($app) {
|
|||||||
success(res);
|
success(res);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Redirect sub.store to vercel webpage
|
if (ENV().isNode) {
|
||||||
$app.get('/', async (req, res) => {
|
$app.get('/', getEnv);
|
||||||
// 302 redirect
|
} else {
|
||||||
res.set('location', 'https://sub-store.vercel.app/').status(302).end();
|
// Redirect sub.store to vercel webpage
|
||||||
});
|
$app.get('/', async (req, res) => {
|
||||||
|
// 302 redirect
|
||||||
|
res.set('location', 'https://sub-store.vercel.app/')
|
||||||
|
.status(302)
|
||||||
|
.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// handle preflight request for QX
|
// handle preflight request for QX
|
||||||
if (ENV().isQX) {
|
if (ENV().isQX) {
|
||||||
@ -71,7 +77,19 @@ function getEnv(req, res) {
|
|||||||
if (req.query.share) {
|
if (req.query.share) {
|
||||||
env.feature.share = true;
|
env.feature.share = true;
|
||||||
}
|
}
|
||||||
success(res, env);
|
res.set('Content-Type', 'application/json;charset=UTF-8').send(
|
||||||
|
JSON.stringify(
|
||||||
|
{
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
guide: '⚠️⚠️⚠️ 您当前看到的是后端的响应. 若想配合前端使用, 可访问官方前端 https://sub-store.vercel.app 后自行配置后端地址, 或一键配置后端 https://sub-store.vercel.app?api=https://a.com/xxx (假设 https://a.com 是你后端的域名, /xxx 是自定义路径). 需注意 HTTPS 前端无法请求非本地的 HTTP 后端(部分浏览器上也无法访问本地 HTTP 后端). 请配置反代或在局域网自建 HTTP 前端. 如果还有问题, 可查看此排查说明: https://t.me/zhetengsha/1068',
|
||||||
|
...env,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refresh(_, res) {
|
async function refresh(_, res) {
|
||||||
|
@ -140,7 +140,7 @@ async function getFlowInfo(req, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($arguments.noFlow) {
|
if ($arguments.noFlow || !/^https?/.test(url)) {
|
||||||
failed(
|
failed(
|
||||||
res,
|
res,
|
||||||
new RequestInvalidError(
|
new RequestInvalidError(
|
||||||
|
@ -40,6 +40,7 @@ async function produceArtifact({
|
|||||||
$options,
|
$options,
|
||||||
proxy,
|
proxy,
|
||||||
noCache,
|
noCache,
|
||||||
|
all,
|
||||||
}) {
|
}) {
|
||||||
platform = platform || 'JSON';
|
platform = platform || 'JSON';
|
||||||
|
|
||||||
@ -173,6 +174,9 @@ async function produceArtifact({
|
|||||||
raw.push(sub.content);
|
raw.push(sub.content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (produceType === 'raw') {
|
||||||
|
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
|
||||||
|
}
|
||||||
// parse proxies
|
// parse proxies
|
||||||
let proxies = (Array.isArray(raw) ? raw : [raw])
|
let proxies = (Array.isArray(raw) ? raw : [raw])
|
||||||
.map((i) => ProxyUtils.parse(i))
|
.map((i) => ProxyUtils.parse(i))
|
||||||
@ -570,6 +574,9 @@ async function produceArtifact({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (produceType === 'raw') {
|
||||||
|
return JSON.stringify((Array.isArray(raw) ? raw : [raw]).flat());
|
||||||
|
}
|
||||||
const files = (Array.isArray(raw) ? raw : [raw]).flat();
|
const files = (Array.isArray(raw) ? raw : [raw]).flat();
|
||||||
let filesContent = files
|
let filesContent = files
|
||||||
.filter((i) => i != null && i !== '')
|
.filter((i) => i != null && i !== '')
|
||||||
@ -589,7 +596,7 @@ async function produceArtifact({
|
|||||||
)
|
)
|
||||||
: { $content: filesContent, $files: files, $options };
|
: { $content: filesContent, $files: files, $options };
|
||||||
|
|
||||||
return processed?.$content ?? '';
|
return (all ? processed : processed?.$content) ?? '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SETTINGS_KEY } from '@/constants';
|
import { SETTINGS_KEY, FILES_KEY, MODULES_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 { getPolicyDescriptor } from '@/utils';
|
||||||
@ -11,6 +11,8 @@ import {
|
|||||||
validCheck,
|
validCheck,
|
||||||
} from '@/utils/flow';
|
} from '@/utils/flow';
|
||||||
import $ from '@/core/app';
|
import $ from '@/core/app';
|
||||||
|
import { findByName } from '@/utils/database';
|
||||||
|
import { produceArtifact } from '@/restful/sync';
|
||||||
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
|
import PROXY_PREPROCESSORS from '@/core/proxy-utils/preprocessors';
|
||||||
const clashPreprocessor = PROXY_PREPROCESSORS.find(
|
const clashPreprocessor = PROXY_PREPROCESSORS.find(
|
||||||
(processor) => processor.name === 'Clash Pre-processor',
|
(processor) => processor.name === 'Clash Pre-processor',
|
||||||
@ -130,22 +132,53 @@ export default async function download(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const downloadUrlMatch = url.match(/^\/api\/(file|module)\/(.+)/);
|
const downloadUrlMatch = url
|
||||||
// if (downloadUrlMatch) {
|
.split('#')[0]
|
||||||
// let type = downloadUrlMatch?.[1];
|
.match(/^\/api\/(file|module)\/(.+)/);
|
||||||
// let name = downloadUrlMatch?.[2];
|
if (downloadUrlMatch) {
|
||||||
// if (name == null) {
|
let type = '';
|
||||||
// throw new Error(`本地 ${type} URL 无效: ${url}`);
|
try {
|
||||||
// }
|
type = downloadUrlMatch?.[1];
|
||||||
// name = decodeURIComponent(name);
|
let name = downloadUrlMatch?.[2];
|
||||||
// const key = type === 'module' ? MODULES_KEY : FILES_KEY;
|
if (name == null) {
|
||||||
// const item = findByName($.read(key), name);
|
throw new Error(`本地 ${type} URL 无效: ${url}`);
|
||||||
// if (!item) {
|
}
|
||||||
// throw new Error(`找不到本地 ${type}: ${name}`);
|
name = decodeURIComponent(name);
|
||||||
// }
|
const key = type === 'module' ? MODULES_KEY : FILES_KEY;
|
||||||
|
const item = findByName($.read(key), name);
|
||||||
|
if (!item) {
|
||||||
|
throw new Error(`找不到 ${type}: ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
// return item.content;
|
if (type === 'module') {
|
||||||
// }
|
return item.content;
|
||||||
|
} else {
|
||||||
|
return await produceArtifact({
|
||||||
|
type: 'file',
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`Error when loading ${type}: ${
|
||||||
|
url.split('#')[0]
|
||||||
|
}.\n Reason: ${err}`,
|
||||||
|
);
|
||||||
|
throw new Error(`无法加载 ${type}: ${url}`);
|
||||||
|
}
|
||||||
|
} else if (url?.startsWith('/')) {
|
||||||
|
try {
|
||||||
|
const fs = eval(`require("fs")`);
|
||||||
|
return fs.readFileSync(url.split('#')[0], 'utf8');
|
||||||
|
} catch (err) {
|
||||||
|
$.error(
|
||||||
|
`Error when reading local file: ${
|
||||||
|
url.split('#')[0]
|
||||||
|
}.\n Reason: ${err}`,
|
||||||
|
);
|
||||||
|
throw new Error(`无法从该路径读取文本内容: ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!isNode && tasks.has(id)) {
|
if (!isNode && tasks.has(id)) {
|
||||||
return tasks.get(id);
|
return tasks.get(id);
|
||||||
|
@ -49,7 +49,7 @@ export async function getFlowHeaders(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($arguments?.noFlow) {
|
if ($arguments?.noFlow || !/^https?/.test(url)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
|
const { isStash, isLoon, isShadowRocket, isQX } = ENV();
|
||||||
|
@ -61,41 +61,41 @@ export function getPlatformFromHeaders(headers) {
|
|||||||
return getPlatformFromUserAgent({ ua, UA, accept });
|
return getPlatformFromUserAgent({ ua, UA, accept });
|
||||||
}
|
}
|
||||||
export function shouldIncludeUnsupportedProxy(platform, ua) {
|
export function shouldIncludeUnsupportedProxy(platform, ua) {
|
||||||
try {
|
// try {
|
||||||
const target = getPlatformFromUserAgent({
|
// const target = getPlatformFromUserAgent({
|
||||||
UA: ua,
|
// UA: ua,
|
||||||
ua: ua.toLowerCase(),
|
// ua: ua.toLowerCase(),
|
||||||
});
|
// });
|
||||||
if (!['Stash', 'Egern', 'Loon'].includes(target)) {
|
// if (!['Stash', 'Egern', 'Loon'].includes(target)) {
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
const coerceVersion = coerce(ua);
|
// const coerceVersion = coerce(ua);
|
||||||
$.log(JSON.stringify(coerceVersion, null, 2));
|
// $.log(JSON.stringify(coerceVersion, null, 2));
|
||||||
const { version } = coerceVersion;
|
// const { version } = coerceVersion;
|
||||||
if (
|
// if (
|
||||||
platform === 'Stash' &&
|
// platform === 'Stash' &&
|
||||||
target === 'Stash' &&
|
// target === 'Stash' &&
|
||||||
gte(version, '3.1.0')
|
// gte(version, '3.1.0')
|
||||||
) {
|
// ) {
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
if (
|
// if (
|
||||||
platform === 'Egern' &&
|
// platform === 'Egern' &&
|
||||||
target === 'Egern' &&
|
// target === 'Egern' &&
|
||||||
gte(version, '1.29.0')
|
// gte(version, '1.29.0')
|
||||||
) {
|
// ) {
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
// Loon 的 UA 不规范, version 取出来是 build
|
// // Loon 的 UA 不规范, version 取出来是 build
|
||||||
if (
|
// if (
|
||||||
platform === 'Loon' &&
|
// platform === 'Loon' &&
|
||||||
target === 'Loon' &&
|
// target === 'Loon' &&
|
||||||
gte(version, '842.0.0')
|
// gte(version, '842.0.0')
|
||||||
) {
|
// ) {
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
$.error(`获取版本号失败: ${e}`);
|
// $.error(`获取版本号失败: ${e}`);
|
||||||
}
|
// }
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
6
backend/src/vendor/express.js
vendored
6
backend/src/vendor/express.js
vendored
@ -17,10 +17,12 @@ export default function express({ substore: $, port, host }) {
|
|||||||
const express_ = eval(`require("express")`);
|
const express_ = eval(`require("express")`);
|
||||||
const bodyParser = eval(`require("body-parser")`);
|
const bodyParser = eval(`require("body-parser")`);
|
||||||
const app = express_();
|
const app = express_();
|
||||||
|
const limit = eval('process.env.SUB_STORE_BODY_JSON_LIMIT') || '1mb';
|
||||||
|
$.info(`[BACKEND] body JSON limit: ${limit}`);
|
||||||
app.use(
|
app.use(
|
||||||
bodyParser.json({
|
bodyParser.json({
|
||||||
verify: rawBodySaver,
|
verify: rawBodySaver,
|
||||||
limit: eval('process.env.SUB_STORE_BODY_JSON_LIMIT') || '1mb',
|
limit,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
app.use(
|
app.use(
|
||||||
@ -36,7 +38,7 @@ export default function express({ substore: $, port, host }) {
|
|||||||
app.start = () => {
|
app.start = () => {
|
||||||
const listener = app.listen(port, host, () => {
|
const listener = app.listen(port, host, () => {
|
||||||
const { address, port } = listener.address();
|
const { address, port } = listener.address();
|
||||||
$.info(`[BACKEND] ${address}:${port}`);
|
$.info(`[BACKEND] listening on ${address}:${port}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return app;
|
return app;
|
||||||
|
49
backend/src/vendor/open-api.js
vendored
49
backend/src/vendor/open-api.js
vendored
@ -10,6 +10,14 @@ const isEgern = 'object' == typeof egern;
|
|||||||
const isLanceX = 'undefined' != typeof $native;
|
const isLanceX = 'undefined' != typeof $native;
|
||||||
const isGUIforCores = typeof $Plugins !== 'undefined';
|
const isGUIforCores = typeof $Plugins !== 'undefined';
|
||||||
|
|
||||||
|
function isPlainObject(obj) {
|
||||||
|
return (
|
||||||
|
obj !== null &&
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
[null, Object.prototype].includes(Object.getPrototypeOf(obj))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export class OpenAPI {
|
export class OpenAPI {
|
||||||
constructor(name = 'untitled', debug = false) {
|
constructor(name = 'untitled', debug = false) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@ -62,29 +70,50 @@ export class OpenAPI {
|
|||||||
const basePath =
|
const basePath =
|
||||||
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
eval('process.env.SUB_STORE_DATA_BASE_PATH') || '.';
|
||||||
let rootPath = `${basePath}/root.json`;
|
let rootPath = `${basePath}/root.json`;
|
||||||
|
const backupRootPath = `${basePath}/root_${Date.now()}.json`;
|
||||||
|
|
||||||
this.log(`Root path: ${rootPath}`);
|
this.log(`Root path: ${rootPath}`);
|
||||||
if (!this.node.fs.existsSync(rootPath)) {
|
if (this.node.fs.existsSync(rootPath)) {
|
||||||
|
try {
|
||||||
|
this.root = JSON.parse(
|
||||||
|
this.node.fs.readFileSync(`${rootPath}`),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.node.fs.copyFileSync(rootPath, backupRootPath);
|
||||||
|
this.error(
|
||||||
|
`Failed to parse ${rootPath}: ${e.message}. Backup created at ${backupRootPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isPlainObject(this.root)) {
|
||||||
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
|
this.node.fs.writeFileSync(rootPath, JSON.stringify({}), {
|
||||||
flag: 'wx',
|
flag: 'w',
|
||||||
});
|
});
|
||||||
this.root = {};
|
this.root = {};
|
||||||
} else {
|
|
||||||
this.root = JSON.parse(
|
|
||||||
this.node.fs.readFileSync(`${rootPath}`),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a json file with the given name if not exists
|
// create a json file with the given name if not exists
|
||||||
let fpath = `${basePath}/${this.name}.json`;
|
let fpath = `${basePath}/${this.name}.json`;
|
||||||
|
const backupPath = `${basePath}/${this.name}_${Date.now()}.json`;
|
||||||
|
|
||||||
this.log(`Data path: ${fpath}`);
|
this.log(`Data path: ${fpath}`);
|
||||||
if (!this.node.fs.existsSync(fpath)) {
|
if (this.node.fs.existsSync(fpath)) {
|
||||||
|
try {
|
||||||
|
this.cache = JSON.parse(
|
||||||
|
this.node.fs.readFileSync(`${fpath}`),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.node.fs.copyFileSync(fpath, backupPath);
|
||||||
|
this.error(
|
||||||
|
`Failed to parse ${fpath}: ${e.message}. Backup created at ${backupPath}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isPlainObject(this.cache)) {
|
||||||
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
this.node.fs.writeFileSync(fpath, JSON.stringify({}), {
|
||||||
flag: 'wx',
|
flag: 'w',
|
||||||
});
|
});
|
||||||
this.cache = {};
|
this.cache = {};
|
||||||
} else {
|
|
||||||
this.cache = JSON.parse(this.node.fs.readFileSync(`${fpath}`));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
0
backend/sub-store_1748083027961.json
Normal file
0
backend/sub-store_1748083027961.json
Normal file
@ -14,10 +14,12 @@ function operator(proxies = [], targetPlatform, context) {
|
|||||||
// 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名
|
// 6. `_collectionName` 为组合订阅名, `_collectionDisplayName` 为组合订阅显示名
|
||||||
// 7. `tls-fingerprint` 为 tls 指纹
|
// 7. `tls-fingerprint` 为 tls 指纹
|
||||||
// 8. `underlying-proxy` 为前置代理, 不同平台会自动转换
|
// 8. `underlying-proxy` 为前置代理, 不同平台会自动转换
|
||||||
|
// 例如 $server['underlying-proxy'] = '名称'
|
||||||
// 只给 mihomo 输出的话, `dialer-proxy` 也行
|
// 只给 mihomo 输出的话, `dialer-proxy` 也行
|
||||||
// 只给 sing-box 输出的话, `detour` 也行
|
// 只给 sing-box 输出的话, `detour` 也行
|
||||||
// 只给 egern 输出的话, `prev_hop` 也行
|
// 只给 Egern 输出的话, `prev_hop` 也行
|
||||||
// 输出到 Clash/Stash/Shadowrocket 时, 会过滤掉配置了前置代理的节点, 并提示使用对应的功能.
|
// 只给 Shadowrocket 输出的话, `chain` 也行
|
||||||
|
// 输出到 Clash/Stash 时, 会过滤掉配置了前置代理的节点, 并提示使用对应的功能.
|
||||||
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
|
// 9. `trojan`, `tuic`, `hysteria`, `hysteria2`, `juicity` 会在解析时设置 `tls`: true (会使用 tls 类协议的通用逻辑), 输出时删除
|
||||||
// 10. `sni` 在某些协议里会自动与 `servername` 转换
|
// 10. `sni` 在某些协议里会自动与 `servername` 转换
|
||||||
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
|
// 11. 读取节点的 ca-str 和 _ca (后端文件路径) 字段, 自动计算 fingerprint (参考 https://t.me/zhetengsha/1512)
|
||||||
@ -57,6 +59,15 @@ function operator(proxies = [], targetPlatform, context) {
|
|||||||
// }
|
// }
|
||||||
// console.log($options)
|
// console.log($options)
|
||||||
|
|
||||||
|
// 若设置 $options._res.headers
|
||||||
|
// 则会在输出文件时设置响应头, 例如:
|
||||||
|
|
||||||
|
// $options._res = {
|
||||||
|
// headers: {
|
||||||
|
// 'X-Custom': '1'
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
// targetPlatform 为输出的目标平台
|
// targetPlatform 为输出的目标平台
|
||||||
|
|
||||||
// lodash
|
// lodash
|
||||||
|
Loading…
x
Reference in New Issue
Block a user