Files
im0o 275f536429 追加 Hosts、路径撞名拦截、连接页表头随行滚动、GEO 借内核通路
设置页内核参数加 hosts 行,铅笔进弹窗编辑(顶层 hosts: 键,
config.go:454,值单个或数组 parseHosts config.go:1131)。弹窗里随便改、
保存才一次性提交——写配置有几秒的校验重载,逐行失焦提交会把等待塞进
每次输入。没动过的行原样写回,数组不被重整形。

订阅和规则组新增时查缓存路径撞名:路径由名字净化而来,「订阅 A」和
「订阅/A」是两个名字、同一个文件,名字查重挡不住,会互相覆盖缓存。
纯函数在 shared 带测试,页面提交前拦。

连接页表头挪进滚动容器加 sticky:.scroller 声明 overflow-y 后按规范
连带拿到横向滚动上下文,表头留在外面时窄屏横向滚动两者脱节。表头
40px 的窗口偏移由 overscan 8 行吸收,不配 scrollMargin。

GEO 单文件下载先借内核的混合端口出去再退直连——内核自己更新 GEO 走
的是它的分流,Patchbay 直连 GitHub 不一定通。ECharts 拆独立 chunk,
业务代码改动不再让这 500KB 跟着换 hash。
2026-08-12 02:47:17 +08:00

108 lines
4.1 KiB
TypeScript
Raw Permalink 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.
/**
* GEO 数据库文件的本地状态:多大、多久没更新了。
*
* 内核不提供这个信息,但文件就躺在它的工作目录里,和 Patchbay 服务端
* 在同一台机器上,直接 stat 就是。
*/
import { readdir, rename, stat, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { DEFAULT_GEOX_URLS, GEO_FILES, looksLikeGeodata, looksLikeMmdb } from '../shared/geo.ts'
import type { GeoFileStatus } from '../shared/geo.ts'
import type { Env } from './env.ts'
/** 配置里写了就用配置的,否则退回内核默认。 */
function urlFor(geoxUrl: Record<string, unknown>, configKey: string): string {
const configured = geoxUrl[configKey]
return typeof configured === 'string' && configured !== ''
? configured
: (DEFAULT_GEOX_URLS[configKey] ?? '')
}
export async function geoStatus(
env: Env,
geoxUrl: Record<string, unknown>,
): Promise<GeoFileStatus[]> {
const entries = await readdir(env.mihomoHome).catch(() => [] as string[])
return Promise.all(
GEO_FILES.map(async ({ key, configKey, file }) => {
// 内核找 GeoIP.dat/GeoSite.dat 时大小写不敏感(constant/path.go:193-224),这里照做
const actual = entries.find((name) => name.toLowerCase() === file.toLowerCase())
let size: number | null = null
let updatedAt: string | null = null
if (actual) {
const info = await stat(join(env.mihomoHome, actual)).catch(() => null)
if (info) {
size = info.size
updatedAt = info.mtime.toISOString()
}
}
return { key, configKey, url: urlFor(geoxUrl, configKey), size, updatedAt }
}),
)
}
async function download(url: string, proxy: string | null): Promise<Uint8Array> {
const res = await fetch(url, {
signal: AbortSignal.timeout(180_000),
redirect: 'follow',
...(proxy ? { proxy } : {}),
})
if (!res.ok) throw new Error(`HTTP ${String(res.status)}`)
return new Uint8Array(await res.arrayBuffer())
}
/**
* 下载单个 GEO 文件写进内核工作目录。
*
* 内核的更新接口是全量的(POST /configs/geo,按 enable 标记更新它用到的类型),
* 单文件只能 Patchbay 自己下。代价是拿不到内核那套完整校验,只能做形状检查
* (shared/geo.ts),也不触发热加载——内核用 mmap 缓存着旧数据,重启后才换新。
*
* `proxy` 给的话先经它出去(内核自己更新 GEO 时走的是它自己的分流,直连
* GitHub 不一定通,Patchbay 借内核的混合端口能拿到同样的通路),失败再直连。
*/
export async function syncGeoFile(
env: Env,
configKey: string,
geoxUrl: Record<string, unknown>,
proxy: string | null,
): Promise<{ ok: true } | { ok: false; message: string }> {
const spec = GEO_FILES.find((g) => g.configKey === configKey)
if (!spec) return { ok: false, message: `未知的 GEO 类型:${configKey}` }
const url = urlFor(geoxUrl, configKey)
let bytes: Uint8Array
try {
bytes = await download(url, proxy)
} catch (viaProxy) {
if (!proxy) return { ok: false, message: `下载失败:${String(viaProxy)}` }
try {
bytes = await download(url, null)
} catch (direct) {
return {
ok: false,
message: `经内核代理(${String(viaProxy)})和直连(${String(direct)})都没能下载。`,
}
}
}
const shapeOk =
configKey === 'mmdb' || configKey === 'asn' ? looksLikeMmdb(bytes) : looksLikeGeodata(bytes)
if (!shapeOk) {
return { ok: false, message: `下载到的内容不像 ${spec.key} 文件,已丢弃,本地文件没有动。` }
}
// 沿用目录里现有的大小写写法,内核就是这样找文件的
const entries = await readdir(env.mihomoHome).catch(() => [] as string[])
const actual = entries.find((name) => name.toLowerCase() === spec.file.toLowerCase()) ?? spec.file
const finalPath = join(env.mihomoHome, actual)
const tempPath = `${finalPath}.patchbay-tmp`
try {
await writeFile(tempPath, bytes)
await rename(tempPath, finalPath)
} catch (error) {
return { ok: false, message: `写入失败:${String(error)}` }
}
return { ok: true }
}