Added support for local subscription

This commit is contained in:
Peng-YM 2022-05-25 17:32:05 +08:00
parent 5eceb8d72b
commit 7ed08ef451
4 changed files with 125 additions and 168 deletions

View File

@ -265,15 +265,18 @@ async function syncArtifact(files) {
return manager.upload(files);
}
async function produceArtifact(
{ type, item, platform, noProcessor } = {
platform: 'JSON',
noProcessor: false,
},
) {
async function produceArtifact({ type, item, platform, noProcessor }) {
platform = platform || 'JSON';
noProcessor = noProcessor || false;
if (type === 'subscription') {
const sub = item;
const raw = await download(sub.url, sub.ua);
let raw;
if (sub.source === 'local') {
raw = sub.content;
} else {
raw = await download(sub.url, sub.ua);
}
// parse proxies
let proxies = ProxyUtils.parse(raw);
if (!noProcessor) {
@ -306,12 +309,12 @@ async function produceArtifact(
} else if (type === 'collection') {
const allSubs = $.read(SUBS_KEY);
const collection = item;
const subs = collection['subscriptions'];
const subnames = collection['subscriptions'];
const results = {};
let processed = 0;
await Promise.all(
subs.map(async (name) => {
subnames.map(async (name) => {
const sub = allSubs[name];
try {
$.info(`正在处理子订阅:${sub.name}...`);
@ -330,7 +333,7 @@ async function produceArtifact(
processed++;
$.info(
`✅ 子订阅:${sub.name}加载成功,进度--${
100 * (processed / subs.length).toFixed(1)
100 * (processed / subnames.length).toFixed(1)
}% `,
);
} catch (err) {
@ -339,7 +342,7 @@ async function produceArtifact(
`❌ 处理组合订阅中的子订阅: ${
sub.name
}时出现错误${err}该订阅已被跳过进度--${
100 * (processed / subs.length).toFixed(1)
100 * (processed / subnames.length).toFixed(1)
}%`,
);
}
@ -349,7 +352,7 @@ async function produceArtifact(
// merge proxies with the original order
let proxies = Array.prototype.concat.apply(
[],
subs.map((name) => results[name]),
subnames.map((name) => results[name]),
);
if (!noProcessor) {

View File

@ -35,10 +35,12 @@ async function downloadSubscription(req, res) {
noProcessor: raw,
});
// forward flow headers
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
if (sub.source !== 'local') {
// forward flow headers
const flowInfo = await getFlowHeaders(sub.url);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
}
}
if (platform === 'JSON') {

File diff suppressed because one or more lines are too long

View File

@ -3,72 +3,47 @@
<v-card class="mb-4">
<v-subheader>订阅配置</v-subheader>
<v-form v-model="formState.basicValid" class="pl-4 pr-4 pb-0">
<v-text-field
v-model="options.name"
:rules="validations.nameRules"
class="mt-2"
clear-icon="clear"
clearable
label="订阅名称"
placeholder="填入订阅名称,名称需唯一"
required
/>
<v-text-field v-model="options.name" :rules="validations.nameRules" class="mt-2" clear-icon="clear" clearable
label="订阅名称" placeholder="填入订阅名称,名称需唯一" required />
<!--For Subscription-->
<v-textarea
v-if="!isCollection"
v-model="options.url"
:rules="validations.urlRules"
auto-grow
class="mt-2"
clear-icon="clear"
clearable
label="订阅链接"
placeholder="填入机场原始订阅链接"
required
rows="2"
/>
<v-radio-group v-if="!isCollection" v-model="options.source" class="mt-0 mb-0">
<template v-slot:label>
<div style="font-size: 2px">订阅来源</div>
</template>
<v-row dense>
<v-col>
<v-radio label="远程" value="remote" />
</v-col>
<v-col>
<v-radio label="本地" value="local" />
</v-col>
<v-col></v-col>
</v-row>
</v-radio-group>
<v-textarea v-if="options.source !== 'local'" v-model="options.url" :rules="validations.urlRules" auto-grow
class="mt-0" clear-icon="clear" clearable label="订阅链接" placeholder="填入机场原始订阅链接" required rows="2" />
<v-textarea v-if="options.source === 'local'" v-model="options.content" clear-icon="clear" clearable
label="订阅内容" placeholder="填入原始订阅内容" autogrow rows="5" row-height="15" class="mt-0">
</v-textarea>
<v-textarea v-if="!isCollection && options.source !== 'local'" v-model="options.ua" auto-grow class="mt-2"
clear-icon="clear" clearable label="User-Agent" placeholder="自定义下载订阅使用的User-Agent可选。" rows="2" />
<!--For Collection-->
<v-list
v-if="isCollection"
dense
>
<v-list v-if="isCollection" dense>
<v-subheader class="pl-0">包含的订阅</v-subheader>
<v-list-item v-for="sub in availableSubs" :key="sub.name">
<v-list-item-avatar>
<v-icon v-if="!sub.icon" color="teal darken-1">mdi-cloud</v-icon>
<v-img v-else :class="getIconClass(sub.icon)" :src="sub.icon"/>
<v-img v-else :class="getIconClass(sub.icon)" :src="sub.icon" />
</v-list-item-avatar>
<v-list-item-content>
{{ sub.name }}
</v-list-item-content>
<v-spacer></v-spacer>
<v-checkbox
v-model="selected"
:value="sub.name"
class="pr-1"
/>
<v-checkbox v-model="selected" :value="sub.name" class="pr-1" />
</v-list-item>
</v-list>
<v-textarea
v-model="options.ua"
auto-grow
class="mt-2"
clear-icon="clear"
clearable
label="User-Agent"
placeholder="自定义下载订阅使用的User-Agent可选。"
rows="2"
/>
<v-textarea
v-model="options.icon"
auto-grow
class="mt-2"
clear-icon="clear"
clearable
label="图标链接"
placeholder="填入想要展示的图标链接,可选。"
rows="2"
/>
<v-textarea v-model="options.icon" auto-grow class="mt-2" clear-icon="clear" clearable label="图标链接"
placeholder="填入想要展示的图标链接,可选。" rows="2" />
</v-form>
<v-card-actions>
<v-spacer></v-spacer>
@ -76,7 +51,7 @@
<v-icon>save_alt</v-icon>
</v-btn>
<v-dialog v-model="showShareDialog" max-width="400px">
<template #activator="{on}">
<template #activator="{ on }">
<v-btn v-on="on" icon>
<v-icon>cloud_circle</v-icon>
</v-btn>
@ -85,20 +60,13 @@
<v-card-title>
<v-icon left>cloud_circle</v-icon>
配置导入
<v-spacer/>
<v-spacer />
<v-btn icon @click="share">
<v-icon small>share</v-icon>
</v-btn>
</v-card-title>
<v-textarea
v-model="imported"
:rules="validations.importRules"
clear-icon="clear"
clearable
label="粘贴配置以导入"
rows="5"
solo
/>
<v-textarea v-model="imported" :rules="validations.importRules" clear-icon="clear" clearable label="粘贴配置以导入"
rows="5" solo />
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text @click="importConf">确认</v-btn>
@ -113,93 +81,73 @@
<v-subheader>常用选项</v-subheader>
<v-form class="pl-4 pr-4">
<v-item-group>
<v-radio-group
v-model="options.useless"
class="mt-0 mb-0"
dense
>
<v-radio-group v-model="options.useless" class="mt-0 mb-0" dense>
过滤非法节点
<v-row>
<v-col>
<v-radio label="保留" value="KEEP"/>
<v-radio label="保留" value="KEEP" />
</v-col>
<v-col>
<v-radio label="删除" value="REMOVE"/>
<v-radio label="删除" value="REMOVE" />
</v-col>
<v-col></v-col>
</v-row>
</v-radio-group>
<v-radio-group
v-model="options.udp"
class="mt-0 mb-0"
dense
>
<v-radio-group v-model="options.udp" class="mt-0 mb-0" dense>
UDP转发
<v-row>
<v-col>
<v-radio label="默认" value="DEFAULT"/>
<v-radio label="默认" value="DEFAULT" />
</v-col>
<v-col>
<v-radio label="强制开启" value="FORCE_OPEN"/>
<v-radio label="强制开启" value="FORCE_OPEN" />
</v-col>
<v-col>
<v-radio label="强制关闭" value="FORCE_CLOSE"/>
<v-radio label="强制关闭" value="FORCE_CLOSE" />
</v-col>
</v-row>
</v-radio-group>
<v-radio-group
v-model="options['skip-cert-verify']"
class="mt-0 mb-0"
dense
>
<v-radio-group v-model="options['skip-cert-verify']" class="mt-0 mb-0" dense>
跳过证书验证
<v-row>
<v-col>
<v-radio label="默认" value="DEFAULT"/>
<v-radio label="默认" value="DEFAULT" />
</v-col>
<v-col>
<v-radio label="强制跳过" value="FORCE_OPEN"/>
<v-radio label="强制跳过" value="FORCE_OPEN" />
</v-col>
<v-col>
<v-radio label="强制验证" value="FORCE_CLOSE"/>
<v-radio label="强制验证" value="FORCE_CLOSE" />
</v-col>
</v-row>
</v-radio-group>
<v-radio-group
v-model="options.tfo"
class="mt-0 mb-0"
dense
>
<v-radio-group v-model="options.tfo" class="mt-0 mb-0" dense>
TCP Fast Open
<v-row>
<v-col>
<v-radio label="默认" value="DEFAULT"/>
<v-radio label="默认" value="DEFAULT" />
</v-col>
<v-col>
<v-radio label="强制开启" value="FORCE_OPEN"/>
<v-radio label="强制开启" value="FORCE_OPEN" />
</v-col>
<v-col>
<v-radio label="强制关闭" value="FORCE_CLOSE"/>
<v-radio label="强制关闭" value="FORCE_CLOSE" />
</v-col>
</v-row>
</v-radio-group>
<v-radio-group
v-model="options['vmess-aead']"
class="mt-0 mb-0"
dense
>
<v-radio-group v-model="options['vmess-aead']" class="mt-0 mb-0" dense>
Vmess AEAD
<v-row>
<v-col>
<v-radio label="默认" value="DEFAULT"/>
<v-radio label="默认" value="DEFAULT" />
</v-col>
<v-col>
<v-radio label="强制开启" value="FORCE_OPEN"/>
<v-radio label="强制开启" value="FORCE_OPEN" />
</v-col>
<v-col>
<v-radio label="强制关闭" value="FORCE_CLOSE"/>
<v-radio label="强制关闭" value="FORCE_CLOSE" />
</v-col>
</v-row>
</v-radio-group>
@ -210,21 +158,17 @@
<v-card class="mb-4">
<v-subheader>Surge 选项</v-subheader>
<v-form class="pl-4 pr-4">
<v-radio-group
v-model="options['surge-hybrid']"
class="mt-0 mb-0"
dense
>
<v-radio-group v-model="options['surge-hybrid']" class="mt-0 mb-0" dense>
Hybrid 策略
<v-row>
<v-col>
<v-radio label="默认" value="DEFAULT"/>
<v-radio label="默认" value="DEFAULT" />
</v-col>
<v-col>
<v-radio label="强制开启" value="FORCE_OPEN"/>
<v-radio label="强制开启" value="FORCE_OPEN" />
</v-col>
<v-col>
<v-radio label="强制关闭" value="FORCE_CLOSE"/>
<v-radio label="强制关闭" value="FORCE_CLOSE" />
</v-col>
</v-row>
</v-radio-group>
@ -235,7 +179,7 @@
节点操作
<v-spacer></v-spacer>
<v-dialog v-model="dialog" scrollable>
<template #activator="{on}">
<template #activator="{ on }">
<v-btn v-on="on" icon>
<v-icon color="primary">add_circle</v-icon>
</v-btn>
@ -246,7 +190,7 @@
<v-card-text>
<v-radio-group v-model="selectedProcess" dense>
<v-radio v-for="(k, idx) in Object.keys(availableProcessors)" :key="idx"
:label="availableProcessors[k].name" :value="k"></v-radio>
:label="availableProcessors[k].name" :value="k"></v-radio>
</v-radio-group>
</v-card-text>
<v-card-actions>
@ -259,22 +203,15 @@
</v-subheader>
<v-divider></v-divider>
<component :is="p.component"
v-for="p in processors"
:key="p.id"
:args="p.args"
@dataChanged="dataChanged"
@deleteProcess="deleteProcess"
@down="moveDown"
@up="moveUp"
>
<component :is="p.component" v-for="p in processors" :key="p.id" :args="p.args" @dataChanged="dataChanged"
@deleteProcess="deleteProcess" @down="moveDown" @up="moveUp">
</component>
</v-card>
</v-container>
</template>
<script>
import {showError, showInfo} from "@/utils";
import { showError, showInfo } from "@/utils";
import TypeFilter from "@/components/TypeFilter";
import RegionFilter from "@/components/RegionFilter";
import RegexFilter from "@/components/RegexFilter";
@ -368,8 +305,8 @@ export default {
v => /^[\w-_]*$/.test(v) || "订阅名称只能包含英文字符、横杠和下划线!"
],
urlRules: [
v => !!v || "订阅链接不能为空!",
v => /^https?:\/\//.test(v) || ""
v => this.options.source === 'remote' && (!!v || "订阅链接不能为空!"),
v => this.options.source === 'remote' && (/^https?:\/\//.test(v) || "订阅链接不合法!")
],
importRules: [
v => !!v || "不能导入空配置!"
@ -380,7 +317,9 @@ export default {
},
options: {
name: "",
source: "",
url: "",
content: "",
icon: "",
ua: "",
useless: "KEEP",
@ -408,7 +347,7 @@ export default {
this.$store.commit("SET_NAV_TITLE", source.name ? `订阅编辑 ➤ ${source.name}` : "新建订阅");
}
this.name = source.name;
const {options, process} = loadProcess(this.options, source);
const { options, process } = loadProcess(this.options, source);
this.options = options;
this.process = process;
},
@ -442,13 +381,15 @@ export default {
output.subscriptions = this.selected;
} else {
output.url = this.options.url;
output.source = this.options.source;
output.content = this.options.content;
}
// assign user-agent, if ua is set
let ua = this.options.ua;
if (typeof ua != "undefined" && ua != null && ua.trim().length > 0) {
output.ua = ua;
}else{
output.ua=""
} else {
output.ua = ""
}
// useless filter
if (this.options.useless === 'REMOVE') {
@ -461,7 +402,7 @@ export default {
if (this.options[opt] !== 'DEFAULT') {
output.process.push({
type: "Set Property Operator",
args: {key: opt, value: this.options[opt] === 'FORCE_OPEN'}
args: { key: opt, value: this.options[opt] === 'FORCE_OPEN' }
});
}
}
@ -498,25 +439,33 @@ export default {
}
} else {
console.log("Saving subscription...");
if (this.options.name && this.options.url) {
if (this.$route.params.name !== "UNTITLED") {
this.$store.dispatch("UPDATE_SUBSCRIPTION", {
name: this.$route.params.name,
sub: this.config
}).then(() => {
showInfo(`成功保存订阅:${this.options.name}`);
}).catch(() => {
showError(`发生错误,无法保存订阅!`);
});
} else {
this.$store.dispatch("NEW_SUBSCRIPTION", this.config).then(() => {
showInfo(`成功创建订阅:${this.options.name}`);
this.$router.back();
}).catch(() => {
showError(`发生错误,无法创建订阅!`);
});
}
if (!this.options.name) {
showError(`订阅名字不能为空!`);
return;
}
if (this.options.source === 'remote' && !this.options.url) {
showError(`订阅链接不能为空!`);
return;
}
if (this.$route.params.name !== "UNTITLED") {
this.$store.dispatch("UPDATE_SUBSCRIPTION", {
name: this.$route.params.name,
sub: this.config
}).then(() => {
showInfo(`成功保存订阅:${this.options.name}`);
}).catch(() => {
showError(`发生错误,无法保存订阅!`);
});
} else {
this.$store.dispatch("NEW_SUBSCRIPTION", this.config).then(() => {
showInfo(`成功创建订阅:${this.options.name}`);
this.$router.back();
}).catch(() => {
showError(`发生错误,无法创建订阅!`);
});
}
}
},
@ -537,9 +486,10 @@ export default {
importConf() {
if (this.imported) {
const sub = JSON.parse(this.imported);
const {options, process} = loadProcess(this.options, sub);
const { options, process } = loadProcess(this.options, sub);
delete options.name;
delete options.url;
delete options.content;
Object.assign(this.options, options);
this.process = process;
@ -562,7 +512,7 @@ export default {
},
addProcess(type) {
this.process.push({type, id: uuidv4()});
this.process.push({ type, id: uuidv4() });
this.dialog = false;
},
@ -616,6 +566,8 @@ function loadProcess(options, source, isCollection = false) {
options.subscriptions = source.subscriptions;
} else {
options.url = source.url;
options.source = source.source || "remote";
options.content = source.content;
}
let process = []
@ -633,7 +585,7 @@ function loadProcess(options, source, isCollection = false) {
process.push(p);
}
}
return {options, process};
return { options, process };
}
function uuidv4() {