设置:显式主题补 color-scheme,滚动条和 select 弹层这些 UA 部件不看 变量。连接:断开失败的提示与轮询取数分成两条通道,不再被下一秒的轮询 成功抹掉;排序加 id 决胜,内核快照的遍历序无序,空闲连接下载量普遍 相同,没有决胜键行会在断开按钮下面跳。仪表盘与请求:一条流断了连带 收掉同组连接;断流不等于内核没了(配置重载掐断流而 online 从未翻转), 5 秒自动重连。规则:正则类规则(DOMAIN-REGEX 等三种)按内核的 payloadCanContainComma 语义拆分与禁入结构化编辑,顶层逗号不再把正则 截一半当目标;findRuleIssues 注释里对内核校验的错误断言改正。
137 lines
4.8 KiB
TypeScript
137 lines
4.8 KiB
TypeScript
/**
|
||
* 连接页的领域逻辑:内核当前连接的显示、筛选、排序和按入站聚合。
|
||
*
|
||
* 类型按 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
|
||
}
|
||
// 平局按 id 决胜。输入顺序来自内核快照的 map 遍历,本身无序且每秒在变;
|
||
// 空闲连接的下载量普遍相同,没有决胜键的话行会在指针(和断开按钮)下面跳
|
||
return [...list].sort((a, b) => {
|
||
const main = key === 'download' ? b.download - a.download : age(b) - age(a)
|
||
return main !== 0 ? main : a.id.localeCompare(b.id)
|
||
})
|
||
}
|
||
|
||
/** 按主机名筛选。大小写不敏感,空串不筛。 */
|
||
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)
|
||
}
|