Files
mihomo-patchbay/shared/inbounds.ts
T
im0o 206e14c7e7 路由页改成主从布局,出站可以真的改了
右侧详情面板取代原来的手风琴展开:空间用起来了,选中状态更清楚,行也不会被推来推去。
出站选择做成分组的按钮网格而不是下拉框——一眼看得到全部可选项和当前状态。

点一下就走完整的落盘链路(校验、备份、写入、重载),失败时按停在哪一步显示不同的话。
订阅线路那一档暂时禁用:选中它需要先自动建一个筛选组,那部分还没做。

顺带两处从截图里看出来的问题:连接线原本是固定 40px 的短横,读起来像装饰不像连接,
改成占据弹性列真的把两头连起来;默认入站的名字原本是「HTTP 和 SOCKS5 共用」,
和类型列重复,改成它来自的配置键 mixed-port。
2026-08-10 11:55:53 +08:00

146 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.
/**
* 把配置文件里两套写法的入站合并成界面上的一张表。
*
* mihomo 有新旧两套声明入站的方式:
* 旧:顶层的 mixed-port / port / socks-port / redir-port / tproxy-port,
* 每种只能有一个,而且没法给它单独指定出站
* 新:listeners 数组,数量不限,每个都能有自己的出站和附加规则
*
* 界面上必须两边都画。只画 listeners 的话,一台只用 mixed-port 的机器打开是空板——
* 而实际上那个端口正在跑,这等于对用户撒谎。
*/
import type { ConfigObject } from '../vendor/config-editor/index.ts'
/** 内核自带、不需要配置就存在的出站。 */
export const BUILTIN_OUTBOUNDS = [
'DIRECT',
'REJECT',
'REJECT-DROP',
'PASS',
'PASS-RULE',
'COMPATIBLE',
'GLOBAL',
] as const
/** 旧写法的顶层端口字段,以及它们各自等价于哪种入站类型。 */
const LEGACY_PORTS: Array<{ key: string; type: string }> = [
{ key: 'mixed-port', type: 'mixed' },
{ key: 'port', type: 'http' },
{ key: 'socks-port', type: 'socks' },
{ key: 'redir-port', type: 'redir' },
{ key: 'tproxy-port', type: 'tproxy' },
]
/** 一路入站的出站去向。四选一,对应界面上那个下拉框。 */
export type OutboundMode =
| { kind: 'rules' } // 没写 proxy,流量进分流规则
| { kind: 'direct' } // proxy: DIRECT
| { kind: 'global' } // proxy: GLOBAL
| { kind: 'named'; name: string } // proxy: 某个策略组或自有线路
export interface Inbound {
/** 界面上用来定位这一行 */
id: string
name: string
type: string
/** 端口。旧写法一定是单个数字,新写法可以是 "200,302" 或 "401-429" 这类写法 */
port: string
listen: string | null
outbound: OutboundMode
/** 挂在这路入站上的附加规则组名,没有就是 null */
ruleSet: string | null
/** 旧写法声明的入站。界面上不可删除,也不能单独指定出站 */
isDefault: boolean
/** 旧写法对应的顶层字段名,用于写回。新写法为 null */
legacyKey: string | null
/** 在 listeners 数组里的下标,用于写回。旧写法为 null */
listenerIndex: number | null
/** 给人看的一句话说明,主要是给默认入站解释它为什么不能改出站 */
note: string | null
}
function outboundOf(proxy: unknown): OutboundMode {
if (typeof proxy !== 'string' || proxy === '') return { kind: 'rules' }
if (proxy === 'DIRECT') return { kind: 'direct' }
if (proxy === 'GLOBAL') return { kind: 'global' }
return { kind: 'named', name: proxy }
}
/** 把出站去向写回成配置里 proxy 字段该有的值。返回 undefined 表示不写这个字段。 */
export function outboundToProxyField(mode: OutboundMode): string | undefined {
if (mode.kind === 'rules') return undefined
if (mode.kind === 'direct') return 'DIRECT'
if (mode.kind === 'global') return 'GLOBAL'
return mode.name
}
export function collectInbounds(data: ConfigObject): Inbound[] {
const result: Inbound[] = []
for (const { key, type } of LEGACY_PORTS) {
const raw = data[key]
const port = typeof raw === 'number' ? raw : Number(raw)
// 0 和缺省都表示没启用这个端口
if (!Number.isFinite(port) || port <= 0) continue
result.push({
id: `legacy:${key}`,
// 名字直接用它来自的那个配置键。比「HTTP 和 SOCKS5 共用」这种描述好:
// 类型那一列已经写着协议了,名字再说一遍是重复,而配置键能直接对上文件里的行。
name: key,
type,
port: String(port),
listen: null,
outbound: { kind: 'rules' },
ruleSet: null,
isDefault: true,
legacyKey: key,
listenerIndex: null,
note: `由配置文件顶层的 ${key} 声明。这种写法没法单独指定出站,流量一律走分流规则。想让这个端口固定走某个出站,要把它改成命名入站。`,
})
}
const listeners = Array.isArray(data.listeners) ? data.listeners : []
listeners.forEach((entry, index) => {
if (typeof entry !== 'object' || entry === null) return
const item = entry as Record<string, unknown>
result.push({
id: `listener:${String(index)}`,
name: typeof item.name === 'string' ? item.name : `第 ${String(index + 1)} 个入站`,
type: typeof item.type === 'string' ? item.type : '',
port: item.port === undefined || item.port === null ? '' : String(item.port),
listen: typeof item.listen === 'string' ? item.listen : null,
outbound: outboundOf(item.proxy),
ruleSet: typeof item.rule === 'string' && item.rule ? item.rule : null,
isDefault: false,
legacyKey: null,
listenerIndex: index,
note: null,
})
})
return result
}
/**
* 所有可以被入站的出站字段引用的名字。
*
* 订阅带来的线路**不在这里**——内核按名字找出站时查的是一张由内置出站、顶层 proxies
* 和 proxy-groups 构成的表,订阅线路住在订阅对象里,不在表上。想让入站固定走订阅里的
* 某条线路,得先建一个只装它的策略组。
*/
export function collectOutboundNames(data: ConfigObject): {
builtins: string[]
groups: string[]
ownProxies: string[]
} {
const groups = (Array.isArray(data['proxy-groups']) ? data['proxy-groups'] : [])
.map((g) => (typeof g === 'object' && g !== null ? (g as Record<string, unknown>).name : null))
.filter((n): n is string => typeof n === 'string')
const ownProxies = (Array.isArray(data.proxies) ? data.proxies : [])
.map((p) => (typeof p === 'object' && p !== null ? (p as Record<string, unknown>).name : null))
.filter((n): n is string => typeof n === 'string')
return { builtins: [...BUILTIN_OUTBOUNDS], groups, ownProxies }
}