Files
mihomo-patchbay/shared/providers.ts
T
im0o 0eef680fba 配置页:订阅的增删改和立刻更新
写配置的纯逻辑在 shared/providers.ts。删除前 subscriptionReferences 同时查
直接 use 和经订阅副本(共用缓存文件 path)的间接引用,有被引用的就拦下并列出
策略组。订阅不允许改名:名字被 use: 和副本引用,同步所有引用不如删了重建。

页面还没挂进 App.vue 的导航,由集成者统一加。
2026-08-11 18:50:13 +08:00

147 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 订阅的读写。
*
* 配置文件里的 `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))
}