Compare commits

...

3 Commits

Author SHA1 Message Date
xream
b1618c3803 feat: 支持使用环境变量 SUB_STORE_PRODUCE_CRON 在后台定时处理订阅, 格式为 0 */2 * * *,sub,a;0 */3 * * *,col,b 2025-02-28 14:07:35 +08:00
xream
1b4c046b75 fix: mihomo 覆写可以多次使用
Some checks are pending
build / build (push) Waiting to run
2025-02-27 23:37:39 +08:00
xream
41034ceb46 feat: 规范化 subscription-userinfo 2025-02-27 23:23:32 +08:00
7 changed files with 107 additions and 17 deletions

View File

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

View File

@@ -20,6 +20,7 @@ import {
validCheck,
flowTransfer,
getRmainingDays,
normalizeFlowHeader,
} from '@/utils/flow';
function isObject(item) {
@@ -365,12 +366,20 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
if (output?.$file?.type === 'mihomoProfile') {
try {
let patch = YAML.safeLoad(script);
let config;
if (output?.$content) {
try {
config = YAML.safeLoad(output?.$content);
} catch (e) {
$.error(e.message ?? e);
}
}
// if (typeof patch !== 'object') patch = {};
if (typeof patch !== 'object')
throw new Error('patch is not an object');
output.$content = ProxyUtils.yaml.safeDump(
deepMerge(
{
config || {
proxies: await produceArtifact({
type:
output?.$file?.sourceType ||
@@ -413,7 +422,15 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
if($file.type === 'mihomoProfile') {
${script}
if(typeof main === 'function') {
const config = {
let config;
if ($content) {
try {
config = ProxyUtils.yaml.safeLoad($content);
} catch (e) {
console.log(e.message ?? e);
}
}
$content = ProxyUtils.yaml.safeDump(await main(config || {
proxies: await produceArtifact({
type: $file.sourceType || 'collection',
name: $file.sourceName,
@@ -423,8 +440,7 @@ function ScriptOperator(script, targetPlatform, $arguments, source, $options) {
'delete-underscore-fields': true
}
}),
}
$content = ProxyUtils.yaml.safeDump(await main(config))
}))
}
} else {
${script}
@@ -1083,6 +1099,7 @@ function createDynamicFunction(name, script, $arguments, $options) {
flowTransfer,
validCheck,
getRmainingDays,
normalizeFlowHeader,
};
if ($.env.isLoon) {
return new Function(

View File

@@ -5,7 +5,7 @@ import {
import { ProxyUtils } from '@/core/proxy-utils';
import { COLLECTIONS_KEY, SUBS_KEY } from '@/constants';
import { findByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow';
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
import $ from '@/core/app';
import { failed } from '@/restful/response';
import { InternalServerError, ResourceNotFoundError } from '@/restful/errors';
@@ -259,7 +259,10 @@ async function downloadSubscription(req, res) {
$arguments.flowUrl,
);
if (flowInfo) {
res.set('subscription-userinfo', flowInfo);
res.set(
'subscription-userinfo',
normalizeFlowHeader(flowInfo),
);
}
}
} catch (err) {
@@ -293,10 +296,9 @@ async function downloadSubscription(req, res) {
}
res.set(
'subscription-userinfo',
[subUserInfo, flowInfo]
.filter((i) => i)
.join('; ')
.replace(/\s*;\s*;\s*/g, ';'),
normalizeFlowHeader(
[subUserInfo, flowInfo].filter((i) => i).join(';'),
),
);
}
@@ -556,7 +558,7 @@ async function downloadCollection(req, res) {
if (subUserInfo) {
res.set(
'subscription-userinfo',
subUserInfo.replace(/\s*;\s*;\s*/g, ';'),
normalizeFlowHeader(subUserInfo),
);
}
if (platform === 'JSON') {

View File

@@ -1,5 +1,5 @@
import { deleteByName, findByName, updateByName } from '@/utils/database';
import { getFlowHeaders } from '@/utils/flow';
import { getFlowHeaders, normalizeFlowHeader } from '@/utils/flow';
import { FILES_KEY } from '@/constants';
import { failed, success } from '@/restful/response';
import $ from '@/core/app';
@@ -148,7 +148,7 @@ async function getFile(req, res) {
if (flowInfo) {
res.set(
'subscription-userinfo',
flowInfo.replace(/\s*;\s*;\s*/g, ';'),
normalizeFlowHeader(flowInfo),
);
}
}

View File

@@ -2,7 +2,7 @@ import express from '@/vendor/express';
import $ from '@/core/app';
import migrate from '@/utils/migration';
import download from '@/utils/download';
import { syncArtifacts } from '@/restful/sync';
import { syncArtifacts, produceArtifact } from '@/restful/sync';
import { gistBackupAction } from '@/restful/miscs';
import { TOKENS_KEY } from '@/constants';
@@ -75,6 +75,39 @@ export default function serve() {
// 'Asia/Shanghai' // timeZone
);
}
// 格式: 0 */2 * * *,sub,a;0 */3 * * *,col,b
// 每 2 小时处理一次单条订阅 a, 每 3 小时处理一次组合订阅 b
const produce_cron = eval('process.env.SUB_STORE_PRODUCE_CRON');
if (produce_cron) {
$.info(`[PRODUCE CRON] ${produce_cron} enabled`);
const { CronJob } = eval(`require("cron")`);
produce_cron.split(/\s*;\s*/).map((item) => {
const [cron, type, name] = item.split(/\s*,\s*/);
new CronJob(
cron.trim(),
async function () {
try {
$.info(
`[PRODUCE CRON] ${type} ${name} ${cron} started`,
);
await produceArtifact({ type, name });
$.info(
`[PRODUCE CRON] ${type} ${name} ${cron} finished`,
);
} catch (e) {
$.error(
`[PRODUCE CRON] ${type} ${name} ${cron} error: ${
e.message ?? e
}`,
);
}
}, // onTick
null, // onComplete
true, // start
// 'Asia/Shanghai' // timeZone
);
});
}
const backend_download_cron = eval(
'process.env.SUB_STORE_BACKEND_DOWNLOAD_CRON',
);

View File

@@ -43,7 +43,7 @@ async function produceArtifact({
}) {
platform = platform || 'JSON';
if (type === 'subscription') {
if (['subscription', 'sub'].includes(type)) {
let sub;
if (name) {
const allSubs = $.read(SUBS_KEY);
@@ -190,7 +190,7 @@ async function produceArtifact({
}
// produce
return ProxyUtils.produce(proxies, platform, produceType, produceOpts);
} else if (type === 'collection') {
} else if (['collection', 'col'].includes(type)) {
const allSubs = $.read(SUBS_KEY);
const allCols = $.read(COLLECTIONS_KEY);
const collection = findByName(allCols, name);

View File

@@ -313,3 +313,41 @@ export function getRmainingDays(opt = {}) {
$.error(`getRmainingDays failed: ${e.message ?? e}`);
}
}
export function normalizeFlowHeader(flowHeaders) {
try {
// 使用 Map 保持顺序并处理重复键
const kvMap = new Map();
flowHeaders
.split(';')
.map((p) => p.trim())
.filter(Boolean)
.forEach((pair) => {
const eqIndex = pair.indexOf('=');
if (eqIndex === -1) return;
const key = pair.slice(0, eqIndex).trim();
const encodedValue = pair.slice(eqIndex + 1).trim();
// 只保留第一个出现的 key
if (!kvMap.has(key)) {
try {
// 解码 URI 组件并保留原始值作为 fallback
const decodedValue = decodeURIComponent(encodedValue);
kvMap.set(key, decodedValue);
} catch (e) {
kvMap.set(key, encodedValue);
}
}
});
// 拼接标准化字符串
return Array.from(kvMap.entries())
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`) // 重新编码保持兼容性
.join('; ');
} catch (e) {
$.error(`normalizeFlowHeader failed: ${e.message ?? e}`);
return flowHeaders;
}
}