轻提示铺开到全部页面;内核状态悬停变重启;GEO 支持单文件下载
侧栏动作排撤掉重启图标,内核状态(圆点加版本号)本身变成按钮:悬停 换层显示「重启内核」,不在线时只当状态看。行高压成 1 修掉圆点和等宽 字体不居中的问题。 四个页面的一次性反馈(保存、删除、更新订阅、断开连接、测延迟结论) 全部改走 toast,commit() 改返回成败给「失败不关对话框」用;持续状态 (连接页轮询失败、请求页日志流失败)留在页内,会自动消失的提示装不下 需要一直可见的信息。toast 加 info 类别给中性结论用。 GEO 每行的更新改成单独下载那一个文件:内核只有全量更新的口子,单文件 只能服务端自己下(POST /api/geo/sync),代价是只有形状校验(MaxMind 末尾标记、geodata 首字节,shared/geo.ts 带测试)且内核重启后才换用新 文件;「更新全部」文字按钮保留内核的全量口子,有完整校验和热加载。 上传两行的按钮改「选择或拖放文件」,投放区拖入高亮,恢复策略挪到 恢复行下方。
This commit is contained in:
+56
-8
@@ -4,12 +4,20 @@
|
||||
* 内核不提供这个信息,但文件就躺在它的工作目录里,和 Patchbay 服务端
|
||||
* 在同一台机器上,直接 stat 就是。
|
||||
*/
|
||||
import { readdir, stat } from 'node:fs/promises'
|
||||
import { readdir, rename, stat, writeFile } from 'node:fs/promises'
|
||||
import { join } from 'node:path'
|
||||
import { DEFAULT_GEOX_URLS, GEO_FILES } from '../shared/geo.ts'
|
||||
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>,
|
||||
@@ -28,12 +36,52 @@ export async function geoStatus(
|
||||
updatedAt = info.mtime.toISOString()
|
||||
}
|
||||
}
|
||||
const configured = geoxUrl[configKey]
|
||||
const url =
|
||||
typeof configured === 'string' && configured !== ''
|
||||
? configured
|
||||
: (DEFAULT_GEOX_URLS[configKey] ?? '')
|
||||
return { key, configKey, url, size, updatedAt }
|
||||
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 }
|
||||
}
|
||||
|
||||
+11
-1
@@ -22,7 +22,7 @@ import {
|
||||
restoreBackup,
|
||||
undoConfig,
|
||||
} from './config-store.ts'
|
||||
import { geoStatus } from './geo.ts'
|
||||
import { geoStatus, syncGeoFile } from './geo.ts'
|
||||
import { hostInfo, probeExit } from './host.ts'
|
||||
import {
|
||||
kernelVersion,
|
||||
@@ -306,6 +306,16 @@ function makeRoute(env: Env, token: string) {
|
||||
return json({ files: await geoStatus(env, geox) })
|
||||
}
|
||||
|
||||
// 单个 GEO 文件的下载。服务端自己下(内核只有全量更新的口子),重启后生效
|
||||
if (path === '/api/geo/sync' && req.method === 'POST') {
|
||||
const body = (await req.json()) as { key?: string }
|
||||
if (typeof body.key !== 'string') return json({ message: '请求缺少 key 字段' }, 400)
|
||||
const snapshot = await readConfig(env)
|
||||
const geox = (snapshot.data['geox-url'] ?? {}) as Record<string, unknown>
|
||||
const result = await syncGeoFile(env, body.key, geox)
|
||||
return result.ok ? json({ ok: true }) : json({ message: result.message }, 502)
|
||||
}
|
||||
|
||||
// 更新 GEO 数据库。运行时动作;同步接口,透传层给了长超时
|
||||
if (path === '/api/kernel/geo' && req.method === 'POST') {
|
||||
const upstream = await updateGeoDatabases(env)
|
||||
|
||||
Reference in New Issue
Block a user