多项改进

1. 现在可以编辑远程配置了。
2. 远程配置增加自动同步的选项,配合Cron脚本可以定期同步并上传配置到gist。
This commit is contained in:
Peng-YM 2021-02-27 13:00:01 +08:00
parent 85ee743988
commit a0691bedaf
4 changed files with 157 additions and 59 deletions

View File

@ -116,6 +116,9 @@ function service() {
$app.get("/api/utils/env", getEnv); // get runtime environment
$app.get("/api/utils/backup", gistBackup); // gist backup actions
// cron triggered functions
$app.get("/api/cron/sync-artifacts", cronSyncArtifacts); // sync all artifacts
// Redirect sub.store to vercel webpage
$app.get("/", async (req, res) => {
// 302 redirect
@ -600,7 +603,7 @@ function service() {
}
function updateArtifact(req, res) {
const allArtifacts = $.read(SETTINGS_KEY);
const allArtifacts = $.read(ARTIFACTS_KEY);
const oldName = req.params.name;
const artifact = allArtifacts[oldName];
if (artifact) {
@ -632,9 +635,61 @@ function service() {
}
}
async function cronSyncArtifacts(req, res) {
$.info("开始同步所有远程配置...");
const allArtifacts = $.read(ARTIFACTS_KEY);
let success = [], failed = [];
for (const artifact of Object.values(allArtifacts)) {
if (artifact.sync) {
$.info(`正在同步云配置:${artifact.name}...`);
try {
let item;
switch (artifact.type) {
case 'subscription':
item = $.read(SUBS_KEY)[artifact.source];
break;
case 'collection':
item = $.read(COLLECTIONS_KEY)[artifact.source];
break;
case 'rule':
item = $.read(RULES_KEY)[artifact.source];
break;
}
const output = await produceArtifact({
type: artifact.type,
item,
platform: artifact.platform
});
const resp = await syncArtifact({
filename: artifact.name,
content: output
});
artifact.updated = new Date().getTime();
const body = JSON.parse(resp.body);
artifact.url = body.files[artifact.name].raw_url.replace(/\/raw\/[^\/]*\/(.*)/, "/raw/$1");
$.write(allArtifacts, ARTIFACTS_KEY);
$.info(`✅ 成功同步云配置:${artifact.name}`);
success.push(artifact);
} catch (err) {
$.error(`云配置: ${artifact.name} 同步失败!原因:${err}`);
$.notify(
`🌍 『 𝑺𝒖𝒃-𝑺𝒕𝒐𝒓𝒆 』 同步订阅失败`,
`❌ 无法同步订阅:${artifact.name}`,
`🤔 原因:${err}`
);
failed.push(artifact);
}
}
}
res.json({
success,
failed
});
}
async function deleteArtifact(req, res) {
const name = req.params.name;
$.info(`正在删除Artifact${name}`);
$.info(`正在删除远程配置${name}`);
const allArtifacts = $.read(ARTIFACTS_KEY);
try {
const artifact = allArtifacts[name];

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,6 @@
const $ = API();
$.http.get('https://sub.store/api/cron/sync-artifacts');
$.done();
// prettier ignore
function ENV(){const e="undefined"!=typeof $task,t="undefined"!=typeof $loon,s="undefined"!=typeof $httpClient&&!t,i="function"==typeof require&&"undefined"!=typeof $jsbox;return{isQX:e,isLoon:t,isSurge:s,isNode:"function"==typeof require&&!i,isJSBox:i,isRequest:"undefined"!=typeof $request,isScriptable:"undefined"!=typeof importModule}}function HTTP(e={baseURL:""}){const{isQX:t,isLoon:s,isSurge:i,isScriptable:n,isNode:o}=ENV(),r=/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&\/\/=]*)/;const u={};return["GET","POST","PUT","DELETE","HEAD","OPTIONS","PATCH"].forEach(l=>u[l.toLowerCase()]=(u=>(function(u,l){l="string"==typeof l?{url:l}:l;const h=e.baseURL;h&&!r.test(l.url||"")&&(l.url=h?h+l.url:l.url);const a=(l={...e,...l}).timeout,c={onRequest:()=>{},onResponse:e=>e,onTimeout:()=>{},...l.events};let f,d;if(c.onRequest(u,l),t)f=$task.fetch({method:u,...l});else if(s||i||o)f=new Promise((e,t)=>{(o?require("request"):$httpClient)[u.toLowerCase()](l,(s,i,n)=>{s?t(s):e({statusCode:i.status||i.statusCode,headers:i.headers,body:n})})});else if(n){const e=new Request(l.url);e.method=u,e.headers=l.headers,e.body=l.body,f=new Promise((t,s)=>{e.loadString().then(s=>{t({statusCode:e.response.statusCode,headers:e.response.headers,body:s})}).catch(e=>s(e))})}const p=a?new Promise((e,t)=>{d=setTimeout(()=>(c.onTimeout(),t(`${u} URL: ${l.url} exceeds the timeout ${a} ms`)),a)}):null;return(p?Promise.race([p,f]).then(e=>(clearTimeout(d),e)):f).then(e=>c.onResponse(e))})(l,u))),u}function API(e="untitled",t=!1){const{isQX:s,isLoon:i,isSurge:n,isNode:o,isJSBox:r,isScriptable:u}=ENV();return new class{constructor(e,t){this.name=e,this.debug=t,this.http=HTTP(),this.env=ENV(),this.node=(()=>{if(o){return{fs:require("fs")}}return null})(),this.initCache();Promise.prototype.delay=function(e){return this.then(function(t){return((e,t)=>new Promise(function(s){setTimeout(s.bind(null,t),e)}))(e,t)})}}initCache(){if(s&&(this.cache=JSON.parse($prefs.valueForKey(this.name)||"{}")),(i||n)&&(this.cache=JSON.parse($persistentStore.read(this.name)||"{}")),o){let e="root.json";this.node.fs.existsSync(e)||this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.root={},e=`${this.name}.json`,this.node.fs.existsSync(e)?this.cache=JSON.parse(this.node.fs.readFileSync(`${this.name}.json`)):(this.node.fs.writeFileSync(e,JSON.stringify({}),{flag:"wx"},e=>console.log(e)),this.cache={})}}persistCache(){const e=JSON.stringify(this.cache,null,2);s&&$prefs.setValueForKey(e,this.name),(i||n)&&$persistentStore.write(e,this.name),o&&(this.node.fs.writeFileSync(`${this.name}.json`,e,{flag:"w"},e=>console.log(e)),this.node.fs.writeFileSync("root.json",JSON.stringify(this.root,null,2),{flag:"w"},e=>console.log(e)))}write(e,t){if(this.log(`SET ${t}`),-1!==t.indexOf("#")){if(t=t.substr(1),n||i)return $persistentStore.write(e,t);if(s)return $prefs.setValueForKey(e,t);o&&(this.root[t]=e)}else this.cache[t]=e;this.persistCache()}read(e){return this.log(`READ ${e}`),-1===e.indexOf("#")?this.cache[e]:(e=e.substr(1),n||i?$persistentStore.read(e):s?$prefs.valueForKey(e):o?this.root[e]:void 0)}delete(e){if(this.log(`DELETE ${e}`),-1!==e.indexOf("#")){if(e=e.substr(1),n||i)return $persistentStore.write(null,e);if(s)return $prefs.removeValueForKey(e);o&&delete this.root[e]}else delete this.cache[e];this.persistCache()}notify(e,t="",l="",h={}){const a=h["open-url"],c=h["media-url"];if(s&&$notify(e,t,l,h),n&&$notification.post(e,t,l+`${c?"\n多媒体:"+c:""}`,{url:a}),i){let s={};a&&(s.openUrl=a),c&&(s.mediaUrl=c),"{}"===JSON.stringify(s)?$notification.post(e,t,l):$notification.post(e,t,l,s)}if(o||u){const s=l+(a?`\n点击跳转: ${a}`:"")+(c?`\n多媒体: ${c}`:"");if(r){require("push").schedule({title:e,body:(t?t+"\n":"")+s})}else console.log(`${e}\n${t}\n${s}\n\n`)}}log(e){this.debug&&console.log(`[${this.name}] LOG: ${this.stringify(e)}`)}info(e){console.log(`[${this.name}] INFO: ${this.stringify(e)}`)}error(e){console.log(`[${this.name}] ERROR: ${this.stringify(e)}`)}wait(e){return new Promise(t=>setTimeout(t,e))}done(e={}){s||i||n?$done(e):o&&!r&&"undefined"!=typeof $context&&($context.headers=e.headers,$context.statusCode=e.statusCode,$context.body=e.body)}stringify(e){if("string"==typeof e||e instanceof String)return e;try{return JSON.stringify(e,null,2)}catch(e){return"[object Object]"}}}(e,t)}

View File

@ -8,34 +8,35 @@
<v-btn icon @click="openGist()">
<v-icon>visibility</v-icon>
</v-btn>
<v-dialog max-width="400px" v-model="addArtifactDialog">
<v-dialog v-model="showArtifactDialog" max-width="400px">
<template #activator="{on}">
<v-btn icon v-on="on">
<v-btn v-on="on" icon>
<v-icon color="primary">mdi-plus-circle</v-icon>
</v-btn>
</template>
<v-card class="pl-4 pr-4 pb-4 pt-4">
<v-subheader>
<v-icon left>mdi-plus-circle</v-icon>
<h3>添加同步配置</h3>
<v-icon left>{{ editing ? 'edit_off' : 'mdi-plus-circle' }}</v-icon>
<h3>{{ editing ? '修改' : '添加' }}同步配置</h3>
</v-subheader>
<v-divider></v-divider>
<v-form class="pt-4 pl-4 pr-4 pb-0" v-model="formValid">
<v-form v-model="formValid" class="pt-4 pl-4 pr-4 pb-0">
<v-text-field
v-model="newArtifact.name"
v-model="currentArtifact.name"
:disabled="editing"
:rules="validations.nameRules"
clear-icon="clear"
clearable
label="配置名称"
placeholder="填入生成配置名称名称需唯一如Clash.yaml。"
:rules="validations.nameRules"
clearable
clear-icon="clear"
/>
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-text-field
label="类型"
v-on="on"
:rules="validations.required"
:value="getType(newArtifact.type)"
:value="getType(currentArtifact.type)"
label="类型"
/>
</template>
<v-list dense>
@ -56,22 +57,22 @@
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-text-field
v-model="newArtifact.source"
label="来源"
:rules="validations.required"
:placeholder="`填入${getType(newArtifact.type) || '来源'}的名称。`"
v-model="currentArtifact.source"
v-on="on"
:placeholder="`填入${getType(currentArtifact.type) || '来源'}的名称。`"
:rules="validations.required"
label="来源"
/>
</template>
<v-list dense>
<v-list-item
v-for="(sub, idx) in getSources(newArtifact.type)"
@click="newArtifact.source = sub.name"
v-for="(sub, idx) in getSources(currentArtifact.type)"
:key="idx"
@click="currentArtifact.source = sub.name"
>
<v-list-item-avatar>
<v-icon v-if="!sub.icon" color="teal darken-1">mdi-cloud</v-icon>
<v-img :src="sub.icon" v-else :class="getIconClass(sub.icon)"/>
<v-img v-else :class="getIconClass(sub.icon)" :src="sub.icon"/>
</v-list-item-avatar>
<v-list-item-title>{{ sub.name }}</v-list-item-title>
</v-list-item>
@ -81,20 +82,20 @@
<v-menu offset-y>
<template v-slot:activator="{on}">
<v-text-field
label="目标"
v-on="on"
:rules="validations.required"
:value="newArtifact.platform"
:value="currentArtifact.platform"
label="目标"
/>
</template>
<v-list dense>
<v-list-item
v-for="platform in ['Surge', 'Loon', 'QX', 'Clash']"
:key="platform"
@click="newArtifact.platform = platform"
@click="currentArtifact.platform = platform"
>
<v-list-item-avatar>
<v-img :src="getIcon(platform)" :class="getIconClass('#invert')"></v-img>
<v-img :class="getIconClass('#invert')" :src="getIcon(platform)"></v-img>
</v-list-item-avatar>
<v-list-item-title>{{ platform }}</v-list-item-title>
</v-list-item>
@ -104,10 +105,10 @@
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text small :disabled="!formValid" @click="createArtifact()">
<v-btn :disabled="!formValid" color="primary" small text @click="doneEditArtifact()">
确认
</v-btn>
<v-btn text small @click="clear()">
<v-btn small text @click="clear()">
取消
</v-btn>
</v-card-actions>
@ -116,9 +117,9 @@
</v-card-title>
<template v-for="(artifact, idx) in artifacts">
<v-list-item three-line dense :key="artifact.name">
<v-list-item :key="artifact.name" dense three-line>
<v-list-item-avatar>
<v-img :src="getIcon(artifact.platform)" :class="getIconClass('#invert')"/>
<v-img :class="getIconClass('#invert')" :src="getIcon(artifact.platform)"/>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
@ -137,27 +138,39 @@
<v-list-item-subtitle>更新于{{ getUpdatedTime(artifact.updated) }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-menu bottom left>
<template #activator="{ on }">
<v-btn icon v-on="on">
<v-icon>mdi-dots-vertical</v-icon>
<v-row>
<v-col>
<v-btn icon @click="toggleSync(artifact)">
<v-icon :color="artifact.sync ? undefined: 'red'">{{ artifact.sync ? "alarm" : "alarm_off" }}</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item @click="copy(artifact.url)" v-if="artifact.url">
<v-list-item-title>复制</v-list-item-title>
</v-list-item>
<v-list-item @click="preview(artifact.name)">
<v-list-item-title>预览</v-list-item-title>
</v-list-item>
<v-list-item @click="sync(artifact.name)">
<v-list-item-title>同步</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteArtifact(idx, artifact.name)">
<v-list-item-title>删除</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-col>
<v-col>
<v-menu bottom left>
<template #activator="{ on }">
<v-btn v-on="on" icon>
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item v-if="artifact.url" @click="copy(artifact.url)">
<v-list-item-title>复制</v-list-item-title>
</v-list-item>
<v-list-item @click="editArtifact(artifact)">
<v-list-item-title>编辑</v-list-item-title>
</v-list-item>
<v-list-item @click="preview(artifact.name)">
<v-list-item-title>预览</v-list-item-title>
</v-list-item>
<v-list-item @click="sync(artifact.name)">
<v-list-item-title>同步</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteArtifact(idx, artifact.name)">
<v-list-item-title>删除</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-col>
</v-row>
</v-list-item-action>
</v-list-item>
</template>
@ -174,13 +187,14 @@ export default {
name: "Cloud",
data() {
return {
addArtifactDialog: false,
newArtifact: {
showArtifactDialog: false,
currentArtifact: {
name: "",
type: "subscription",
source: "",
platform: "",
},
editing: null,
formValid: false,
validations: {
nameRules: [
@ -230,13 +244,35 @@ export default {
}
},
async createArtifact() {
async doneEditArtifact() {
console.log(JSON.stringify(this.currentArtifact, null, 2));
try {
await axios.post("/artifacts", this.newArtifact);
if (this.editing) {
await axios.patch(`/artifact/${this.currentArtifact.name}`, this.currentArtifact);
} else {
await axios.post("/artifacts", this.currentArtifact);
}
await this.$store.dispatch("FETCH_ARTIFACTS");
this.clear();
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", `创建配置失败!${err}`);
this.$store.commit("SET_ERROR_MESSAGE", `${this.editing ? "更新" : "创建"}配置失败!${err}`);
}
},
async editArtifact(artifact) {
this.editing = true;
Object.assign(this.currentArtifact, artifact);
this.showArtifactDialog = true;
},
async toggleSync(artifact) {
artifact.sync = !artifact.sync;
try {
await axios.patch(`/artifact/${artifact.name}`, artifact);
await this.$store.dispatch("FETCH_ARTIFACTS");
this.$store.commit("SET_SUCCESS_MESSAGE", `${artifact.sync ? '启用' : '禁用'}自动同步配置${artifact.name}`);
} catch (err) {
this.$store.commit("SET_ERROR_MESSAGE", `更改同步配置失败!${err}`);
}
},
@ -250,13 +286,14 @@ export default {
},
clear() {
this.newArtifact = {
this.currentArtifact = {
name: "",
type: "subscription",
source: "",
platform: ""
}
this.addArtifactDialog = false;
this.showArtifactDialog = false;
this.editing = false;
},
copy(url) {
@ -282,8 +319,8 @@ export default {
},
setArtifactType(type) {
this.newArtifact.type = type;
this.newArtifact.source = "";
this.currentArtifact.type = type;
this.currentArtifact.source = "";
},
getSources(type) {