Files
mihomo-patchbay/server/geo.ts
T
im0o b276c4a8fe 轻提示铺开到全部页面;内核状态悬停变重启;GEO 支持单文件下载
侧栏动作排撤掉重启图标,内核状态(圆点加版本号)本身变成按钮:悬停
换层显示「重启内核」,不在线时只当状态看。行高压成 1 修掉圆点和等宽
字体不居中的问题。

四个页面的一次性反馈(保存、删除、更新订阅、断开连接、测延迟结论)
全部改走 toast,commit() 改返回成败给「失败不关对话框」用;持续状态
(连接页轮询失败、请求页日志流失败)留在页内,会自动消失的提示装不下
需要一直可见的信息。toast 加 info 类别给中性结论用。

GEO 每行的更新改成单独下载那一个文件:内核只有全量更新的口子,单文件
只能服务端自己下(POST /api/geo/sync),代价是只有形状校验(MaxMind
末尾标记、geodata 首字节,shared/geo.ts 带测试)且内核重启后才换用新
文件;「更新全部」文字按钮保留内核的全量口子,有完整校验和热加载。
上传两行的按钮改「选择或拖放文件」,投放区拖入高亮,恢复策略挪到
恢复行下方。
2026-08-12 02:20:46 +08:00

88 lines
3.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.
/**
* 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 }
}),
)
}
/**
* 下载单个 GEO 文件写进内核工作目录。
*
* 内核的更新接口是全量的(POST /configs/geo,按 enable 标记更新它用到的类型),
* 单文件只能 Patchbay 自己下。代价是拿不到内核那套完整校验,只能做形状检查
* (shared/geo.ts),也不触发热加载——内核用 mmap 缓存着旧数据,重启后才换新。
*/
export async function syncGeoFile(
env: Env,
configKey: string,
geoxUrl: Record<string, unknown>,
): 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 {
const res = await fetch(url, { signal: AbortSignal.timeout(300_000), redirect: 'follow' })
if (!res.ok) return { ok: false, message: `下载失败:HTTP ${String(res.status)}` }
bytes = new Uint8Array(await res.arrayBuffer())
} catch (error) {
return { ok: false, message: `下载失败:${String(error)}` }
}
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 }
}