mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-14 21:20:48 +08:00
* refactor(ipc): generate bindings with Specta * fix(profile): project IPC inputs to consumed fields * fix(events): remove unused refresh payloads * fix(webdav): return only consumed backup metadata * fix(network): trim unused interface metadata * fix(sysinfo): return only displayed system fields * chore(knip): check generated runtime exports * fix(proxy): confirm runtime chain updates before UI changes * fix(clipboard): propagate proxy environment copy failures * refactor(dns): remove ignored runtime patch * refactor(core): trim unused upgrade response fields * refactor(ipc): avoid redundant response copies * refactor(proxy): clarify automatic proxy query naming * chore(tests): remove added IPC regression tests * refactor(ipc): enforce typed command boundaries * chore(contract): check generated bindings before push * refactor(ipc): remove unused proxy liveness payloads
234 lines
6.4 KiB
TypeScript
234 lines
6.4 KiB
TypeScript
import type {
|
|
ProxyViewV1_Serialize as ProxyViewV1,
|
|
ProxyGroupView_Serialize as ProxyGroupView,
|
|
ProxyNodeView_Serialize as ProxyNodeView,
|
|
ProxyMemberRef,
|
|
} from '@/services/bindings'
|
|
export type {
|
|
ProxyViewV1_Serialize as ProxyViewV1,
|
|
ProxyGroupView_Serialize as ProxyGroupView,
|
|
ProxyNodeView_Serialize as ProxyNodeView,
|
|
ProxyMemberRef,
|
|
} from '@/services/bindings'
|
|
|
|
export type ResolvedProxyMember =
|
|
| {
|
|
kind: 'group'
|
|
ref: Extract<ProxyMemberRef, { kind: 'group' }>
|
|
group: ProxyGroupView
|
|
}
|
|
| {
|
|
kind: 'node'
|
|
ref: Extract<ProxyMemberRef, { kind: 'node' }>
|
|
node: ProxyNodeView
|
|
}
|
|
| {
|
|
kind: 'unresolved'
|
|
ref: Extract<ProxyMemberRef, { kind: 'unresolved' }>
|
|
}
|
|
|
|
export type InteractableProxyMember = Exclude<
|
|
ResolvedProxyMember,
|
|
{ kind: 'unresolved' }
|
|
>
|
|
|
|
export interface ProxyNodeBinding {
|
|
name: string
|
|
source?: ProxyNodeView['source']
|
|
}
|
|
|
|
export type ProxyMemberOccurrenceBinding =
|
|
| { kind: 'group'; name: string; occurrence: number }
|
|
| {
|
|
kind: 'node'
|
|
name: string
|
|
source: ProxyNodeView['source']
|
|
occurrence: number
|
|
}
|
|
|
|
export interface InteractableProxyMemberOccurrence {
|
|
memberIndex: number
|
|
member: InteractableProxyMember
|
|
}
|
|
|
|
export const getRecord = (view: ProxyViewV1, recordId: string) =>
|
|
view.records[recordId]
|
|
|
|
const findGroup = (view: ProxyViewV1, name: string) =>
|
|
view.global?.name === name
|
|
? view.global
|
|
: view.groups.find((group) => group.name === name)
|
|
|
|
export function resolveMember(
|
|
view: ProxyViewV1,
|
|
member: ProxyMemberRef,
|
|
): ResolvedProxyMember {
|
|
if (member.kind === 'unresolved') {
|
|
return { kind: 'unresolved', ref: member }
|
|
}
|
|
if (member.kind === 'group') {
|
|
const group = findGroup(view, member.name)
|
|
if (!group) throw new Error('Proxy view group not found: ' + member.name)
|
|
return { kind: 'group', ref: member, group }
|
|
}
|
|
|
|
const node = getRecord(view, member.recordId)
|
|
if (!node) {
|
|
throw new Error('Proxy view record not found: ' + member.recordId)
|
|
}
|
|
return { kind: 'node', ref: member, node }
|
|
}
|
|
|
|
export const isInteractableMember = (
|
|
member: ResolvedProxyMember,
|
|
): member is InteractableProxyMember => member.kind !== 'unresolved'
|
|
|
|
export const memberDetails = (member: ResolvedProxyMember) =>
|
|
member.kind === 'node'
|
|
? member.node
|
|
: member.kind === 'group'
|
|
? member.group
|
|
: undefined
|
|
|
|
export function findCurrentGroupMember(
|
|
view: ProxyViewV1,
|
|
group: ProxyGroupView,
|
|
): InteractableProxyMemberOccurrence | undefined {
|
|
if (!group.now) return undefined
|
|
|
|
for (const [memberIndex, memberRef] of group.members.entries()) {
|
|
const member = resolveMember(view, memberRef)
|
|
if (isInteractableMember(member) && member.ref.name === group.now) {
|
|
return { memberIndex, member }
|
|
}
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
export const providerNameOf = (node: ProxyNodeView) =>
|
|
node.source.kind === 'provider' ? node.source.providerName : undefined
|
|
|
|
const resolveStandaloneNodes = (view: ProxyViewV1) =>
|
|
view.standalone.map((recordId) => {
|
|
const node = getRecord(view, recordId)
|
|
if (!node) throw new Error('Proxy view record not found: ' + recordId)
|
|
return node
|
|
})
|
|
|
|
const selectRuntimeStandaloneNodes = (
|
|
view: ProxyViewV1,
|
|
runtimeProxies: unknown,
|
|
) => {
|
|
const runtimeProxyNames = new Set(
|
|
(Array.isArray(runtimeProxies) ? runtimeProxies : []).flatMap((proxy) => {
|
|
const name =
|
|
typeof proxy === 'object' && proxy !== null && 'name' in proxy
|
|
? proxy.name
|
|
: undefined
|
|
return typeof name === 'string' && name.length > 0 ? [name] : []
|
|
}),
|
|
)
|
|
return resolveStandaloneNodes(view).filter(
|
|
(node) =>
|
|
node.source.kind === 'core' &&
|
|
runtimeProxyNames.has(node.source.proxyName),
|
|
)
|
|
}
|
|
|
|
export const selectGlobalChainNodes = (
|
|
view: ProxyViewV1,
|
|
runtimeProxies: unknown,
|
|
) =>
|
|
view.global === null ? [] : selectRuntimeStandaloneNodes(view, runtimeProxies)
|
|
|
|
export const selectRuleChainMembers = (
|
|
view: ProxyViewV1,
|
|
groupName: string,
|
|
): Array<{ memberIndex: number; member: ResolvedProxyMember }> => {
|
|
const group = view.groups.find(({ name }) => name === groupName)
|
|
if (!group) return []
|
|
|
|
return group.members.flatMap((memberRef, memberIndex) => {
|
|
const member = resolveMember(view, memberRef)
|
|
return member.kind === 'group' ? [] : [{ memberIndex, member }]
|
|
})
|
|
}
|
|
|
|
const sameSource = (
|
|
left: ProxyNodeView['source'],
|
|
right: ProxyNodeView['source'],
|
|
) =>
|
|
left.kind === right.kind &&
|
|
left.proxyName === right.proxyName &&
|
|
(left.kind === 'core' ||
|
|
(right.kind === 'provider' && left.providerName === right.providerName))
|
|
|
|
const matchesMemberBinding = (
|
|
member: ResolvedProxyMember,
|
|
binding: ProxyMemberOccurrenceBinding,
|
|
) =>
|
|
member.kind === binding.kind &&
|
|
member.ref.name === binding.name &&
|
|
(member.kind !== 'node' ||
|
|
(binding.kind === 'node' && sameSource(member.node.source, binding.source)))
|
|
|
|
export function toMemberOccurrenceBinding(
|
|
members: readonly ResolvedProxyMember[],
|
|
memberIndex: number,
|
|
): ProxyMemberOccurrenceBinding | undefined {
|
|
const member = members[memberIndex]
|
|
if (!member || !isInteractableMember(member)) return undefined
|
|
|
|
const binding: ProxyMemberOccurrenceBinding =
|
|
member.kind === 'node'
|
|
? {
|
|
kind: 'node',
|
|
name: member.ref.name,
|
|
source: member.node.source,
|
|
occurrence: 0,
|
|
}
|
|
: { kind: 'group', name: member.ref.name, occurrence: 0 }
|
|
|
|
for (let index = 0; index < memberIndex; index += 1) {
|
|
if (matchesMemberBinding(members[index], binding)) {
|
|
binding.occurrence += 1
|
|
}
|
|
}
|
|
|
|
return binding
|
|
}
|
|
|
|
export function rebindMemberOccurrence(
|
|
members: readonly ResolvedProxyMember[],
|
|
binding: ProxyMemberOccurrenceBinding,
|
|
): InteractableProxyMemberOccurrence | undefined {
|
|
let occurrence = 0
|
|
|
|
for (const [memberIndex, member] of members.entries()) {
|
|
if (
|
|
!isInteractableMember(member) ||
|
|
!matchesMemberBinding(member, binding)
|
|
) {
|
|
continue
|
|
}
|
|
if (occurrence === binding.occurrence) return { memberIndex, member }
|
|
occurrence += 1
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
export function rebindNode(
|
|
candidates: readonly ProxyNodeView[],
|
|
binding: ProxyNodeBinding,
|
|
) {
|
|
const matches = candidates.filter(
|
|
(node) =>
|
|
node.name === binding.name &&
|
|
(binding.source === undefined || sameSource(node.source, binding.source)),
|
|
)
|
|
const unique = new Map(matches.map((node) => [node.recordId, node]))
|
|
return unique.size === 1 ? unique.values().next().value : undefined
|
|
}
|