追加 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。
This commit is contained in:
2026-08-12 02:47:17 +08:00
parent b276c4a8fe
commit 275f536429
12 changed files with 513 additions and 36 deletions
+25 -5
View File
@@ -41,17 +41,31 @@ export async function geoStatus(
)
}
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}` }
@@ -59,11 +73,17 @@ export async function syncGeoFile(
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)}` }
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 =
+7 -1
View File
@@ -312,7 +312,13 @@ function makeRoute(env: Env, token: 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)
// 内核在线且开着老式 http 入口时借它的通路下载,直连 GitHub 不一定通
const port = [snapshot.data['mixed-port'], snapshot.data.port].find(
(p): p is number => typeof p === 'number' && p > 0,
)
const kernelUp = port !== undefined && (await kernelVersion(env)).online
const proxy = kernelUp ? `http://127.0.0.1:${String(port)}` : null
const result = await syncGeoFile(env, body.key, geox, proxy)
return result.ok ? json({ ok: true }) : json({ message: result.message }, 502)
}