Files
mihomo-patchbay/shared/connections.ts
T
im0o c9a0b87af4 连接页:每秒轮询的连接表格,能按入站看、能断开
/connections 没有流式版本,只能全量轮询;断开是运行时动作,不走
落盘那条保护链路。两个容易做反的地方都收在 shared/connections.ts:
chains 数组第一个是最终出口,显示要倒过来;没有连接时 connections
字段是 null 而不是空数组。虚拟滚动用 @tanstack/vue-virtual,ADR 0006
留的两个例外之一,它只有 virtual-core 一个运行时依赖。
2026-08-11 18:48:55 +08:00

132 lines
4.5 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.
/**
* 连接页的领域逻辑:内核当前连接的显示、筛选、排序和按入站聚合。
*
* 类型按 v1.19.29 管理接口 GET /connections 的实际返回写(开发内核实测),
* 只保留界面用到的字段。`connections` 在没有连接时是 null 而不是空数组。
*/
/** 一条连接里界面关心的元数据。内核返回的字段远不止这些,用不到的不收。 */
export interface ConnectionMeta {
host: string
destinationIP: string
destinationPort: string
/** 从哪一路入站进来:命名入站是它的名字,默认入站是内核造的 DEFAULT-* */
inboundName: string
inboundPort: string
}
export interface ConnectionItem {
id: string
upload: number
download: number
/** ISO 时间,带时区偏移(内核按本机时区输出) */
start: string
/** 经过的出站链。第一个是最终出口,最后一个是最先接触的——和直觉相反 */
chains: string[]
/** 命中的分流规则类型,如 DomainSuffix;兜底是 Match */
rule: string
rulePayload: string
metadata: ConnectionMeta
}
export interface ConnectionSnapshot {
downloadTotal: number
uploadTotal: number
connections: ConnectionItem[] | null
}
/** 这条连接连往哪。有域名显示域名,没有退回 IP,都带端口。 */
export function hostLabel(meta: ConnectionMeta): string {
const target = meta.host || meta.destinationIP
return meta.destinationPort ? `${target}:${meta.destinationPort}` : target
}
/**
* 出站链按流量经过的顺序显示:最先接触的在左,最终出口在右。
* 内核给的数组恰好相反,所以要倒过来。
*/
export function chainLabel(chains: string[]): string {
return [...chains].reverse().join(' → ')
}
/**
* 规则显示成「类型 载荷」,兜底规则没有载荷。
* 类型和载荷都是配置文件 rules: 里的原词,不翻译。
*/
export function ruleLabel(rule: string, payload: string): string {
return payload ? `${rule} ${payload}` : rule
}
/** 持续时间。start 解析不出来(坏数据)时返回空串,由界面决定摆什么。 */
export function formatDuration(startIso: string, now: number): string {
const started = Date.parse(startIso)
if (Number.isNaN(started)) return ''
const seconds = Math.max(0, Math.floor((now - started) / 1000))
if (seconds < 60) return `${String(seconds)} 秒`
const minutes = Math.floor(seconds / 60)
if (minutes < 60) return `${String(minutes)} 分 ${String(seconds % 60)} 秒`
return `${String(Math.floor(minutes / 60))} 小时 ${String(minutes % 60)} 分`
}
export type SortKey = 'download' | 'duration'
/**
* 排序。都是降序:下载多的、活得久的排前面。
* 持续时间没有现成字段,从 start 现算;排坏的 start 沉底。
*/
export function sortConnections(
list: ConnectionItem[],
key: SortKey,
now: number,
): ConnectionItem[] {
const age = (c: ConnectionItem) => {
const started = Date.parse(c.start)
return Number.isNaN(started) ? -1 : now - started
}
return [...list].sort((a, b) => (key === 'download' ? b.download - a.download : age(b) - age(a)))
}
/** 按主机名筛选。大小写不敏感,空串不筛。 */
export function filterConnections(list: ConnectionItem[], query: string): ConnectionItem[] {
const q = query.trim().toLowerCase()
if (!q) return list
return list.filter((c) => hostLabel(c.metadata).toLowerCase().includes(q))
}
/** 一路入站当前有多少条连接。 */
export interface InboundUse {
/** 命名入站显示名字;默认入站内核只给 DEFAULT-* 这类内部名,附上端口好认 */
label: string
count: number
upload: number
download: number
}
/**
* 按入站聚合。这是连接页对本项目的独特价值:每一路入站(每个容器)现在有多少条
* 连接、走了多少流量。
*/
export function groupByInbound(list: ConnectionItem[]): InboundUse[] {
const byName = new Map<string, InboundUse>()
for (const c of list) {
const name = c.metadata.inboundName || c.metadata.inboundPort
let entry = byName.get(name)
if (!entry) {
entry = {
label: c.metadata.inboundName
? `${c.metadata.inboundName}(${c.metadata.inboundPort})`
: c.metadata.inboundPort,
count: 0,
upload: 0,
download: 0,
}
byName.set(name, entry)
}
entry.count += 1
entry.upload += c.upload
entry.download += c.download
}
// 连接多的排前面,和表格的默认排序一个取向
return [...byName.values()].sort((a, b) => b.count - a.count)
}