配置页:订阅的增删改和立刻更新
写配置的纯逻辑在 shared/providers.ts。删除前 subscriptionReferences 同时查 直接 use 和经订阅副本(共用缓存文件 path)的间接引用,有被引用的就拦下并列出 策略组。订阅不允许改名:名字被 use: 和副本引用,同步所有引用不如删了重建。 页面还没挂进 App.vue 的导航,由集成者统一加。
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
/**
|
||||
* 订阅的读写。
|
||||
*
|
||||
* 配置文件里的 `proxy-providers` 段:一个名字对一份声明,内核定时从地址拉回一批线路。
|
||||
* 这段里除了用户自己加的订阅,还有 Patchbay 为链式代理生成的副本(带 `patchbay:` 前缀)。
|
||||
* 这个模块只操作用户订阅;副本的增删由 `inbound-groups.ts` 的 applySetup/pruneGenerated 负责,
|
||||
* 写配置的链路(页面里的 commit)每次都会跑它们,所以这里改完不用补同步。
|
||||
*
|
||||
* 订阅不提供改名:名字被策略组的 `use:` 和生成的副本引用,改名等于改一个被引用的
|
||||
* 标识符,要同步更新所有引用,漏一处就是断引用。删了重建更简单。
|
||||
*/
|
||||
import type { ConfigObject } from '../vendor/config-editor/index.ts'
|
||||
import { isGenerated, providerCachePath } from './inbound-groups.ts'
|
||||
|
||||
/** 新增订阅时的默认更新间隔,秒。一天一次。 */
|
||||
export const DEFAULT_INTERVAL = 86400
|
||||
|
||||
/** 一个用户订阅在界面上的样子。只读配置文件的 `proxy-providers` 段(决策记录 0004)。 */
|
||||
export interface Subscription {
|
||||
name: string
|
||||
url: string
|
||||
/** 更新间隔,秒。null 表示配置里没写 */
|
||||
interval: number | null
|
||||
/** 健康检查。没写或 enable 不是 true 都算未开启 */
|
||||
healthCheck: { url: string | null; interval: number | null } | null
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is ConfigObject {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function providersOf(data: ConfigObject): ConfigObject {
|
||||
return isPlainObject(data['proxy-providers']) ? data['proxy-providers'] : {}
|
||||
}
|
||||
|
||||
function groupsOf(data: ConfigObject): ConfigObject[] {
|
||||
return (Array.isArray(data['proxy-groups']) ? data['proxy-groups'] : []).filter(isPlainObject)
|
||||
}
|
||||
|
||||
function stringList(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
|
||||
}
|
||||
|
||||
/** 所有用户订阅,按配置里的顺序。不含生成的副本。 */
|
||||
export function listSubscriptions(data: ConfigObject): Subscription[] {
|
||||
const result: Subscription[] = []
|
||||
for (const [name, entry] of Object.entries(providersOf(data))) {
|
||||
if (isGenerated(name) || !isPlainObject(entry)) continue
|
||||
const hc = isPlainObject(entry['health-check']) ? entry['health-check'] : null
|
||||
result.push({
|
||||
name,
|
||||
url: typeof entry.url === 'string' ? entry.url : '',
|
||||
interval: typeof entry.interval === 'number' ? entry.interval : null,
|
||||
healthCheck:
|
||||
hc !== null && hc.enable === true
|
||||
? {
|
||||
url: typeof hc.url === 'string' ? hc.url : null,
|
||||
interval: typeof hc.interval === 'number' ? hc.interval : null,
|
||||
}
|
||||
: null,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* `proxy-providers` 里的所有名字,含生成的副本。
|
||||
* 新增订阅挡重名要用全部键:它是映射,和用户看不见的副本撞名也会被静默丢掉。
|
||||
*/
|
||||
export function providerNames(data: ConfigObject): string[] {
|
||||
return Object.keys(providersOf(data))
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增一个订阅。
|
||||
*
|
||||
* `path` 自动给出确定的缓存文件路径,不让用户填:不写的话内核按 url 哈希自己生成一个,
|
||||
* 那个路径 Patchbay 算不出来,链式代理的副本就没法靠「读同一个缓存文件」认来源
|
||||
* (见 inbound-groups.ts 的 applySetup)。
|
||||
*/
|
||||
export function addSubscription(data: ConfigObject, name: string, url: string): ConfigObject {
|
||||
const next = { ...data }
|
||||
next['proxy-providers'] = {
|
||||
...providersOf(data),
|
||||
[name]: { type: 'http', url, path: providerCachePath(name), interval: DEFAULT_INTERVAL },
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
/** 改订阅的地址和更新间隔。其余字段(缓存路径、健康检查、筛选)原样保留。 */
|
||||
export function updateSubscription(
|
||||
data: ConfigObject,
|
||||
name: string,
|
||||
url: string,
|
||||
interval: number,
|
||||
): ConfigObject {
|
||||
const entry = providersOf(data)[name]
|
||||
if (!isPlainObject(entry)) return data
|
||||
const next = { ...data }
|
||||
next['proxy-providers'] = {
|
||||
...providersOf(data),
|
||||
[name]: { ...entry, url, interval },
|
||||
}
|
||||
return next
|
||||
}
|
||||
|
||||
/** 删除一个订阅。删到最后一个时把整个键去掉,不在文件里留空映射。 */
|
||||
export function removeSubscription(data: ConfigObject, name: string): ConfigObject {
|
||||
const providers = { ...providersOf(data) }
|
||||
if (!(name in providers)) return data
|
||||
delete providers[name]
|
||||
const next = { ...data }
|
||||
if (Object.keys(providers).length === 0) delete next['proxy-providers']
|
||||
else next['proxy-providers'] = providers
|
||||
return next
|
||||
}
|
||||
|
||||
/**
|
||||
* 哪些策略组在引用这个订阅。删除前必须先查:被引用的订阅删了,那些组的 `use:` 就空了,
|
||||
* 而空组会让指向它的入站无路可走。
|
||||
*
|
||||
* 引用有两种,第二种容易漏:
|
||||
* 直接 组的 `use:` 里有这个订阅。链式代理的入口组就是这么引原订阅的
|
||||
* 间接 开了链式代理的入站,它的专属组 `use:` 的是订阅副本;副本靠和原订阅
|
||||
* 共用缓存文件(`path` 相同)认来源,名字不可靠解析(见 inbound-groups.ts)
|
||||
*/
|
||||
export function subscriptionReferences(data: ConfigObject, name: string): string[] {
|
||||
const target = providersOf(data)[name]
|
||||
if (!isPlainObject(target)) return []
|
||||
|
||||
const copies = new Set<string>()
|
||||
if (typeof target.path === 'string') {
|
||||
for (const [copyName, entry] of Object.entries(providersOf(data))) {
|
||||
if (isGenerated(copyName) && isPlainObject(entry) && entry.path === target.path) {
|
||||
copies.add(copyName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupsOf(data)
|
||||
.filter((group) => {
|
||||
const use = stringList(group.use)
|
||||
return use.includes(name) || use.some((u) => copies.has(u))
|
||||
})
|
||||
.map((group) => String(group.name))
|
||||
}
|
||||
Reference in New Issue
Block a user