订阅和规则组支持从文件导入,配置页与表单一批打磨

文件导入走两步:内容先经新端点 POST /api/provider-files 写进内核工作
目录的 providers/ 下(路径白名单校验,只认两个路径助手生成的形状),
成了再写 file 类型的声明——顺序反了内核重载会去读不存在的文件。
file 类型没有地址和自动更新(内核靠文件监视跟进),详情页照此展示,
编辑按钮禁用。ini 导入的取文件按钮换成同一个拖放模块。

订阅的更新间隔改成开关语义:内核只在 interval > 0 时才起定时拉取
(component/resource/fetcher.go:226-228),关掉开关就是不写这个键。
配置页加「更新所有订阅」,详情的「立刻更新」改叫「更新订阅」。

规则表从零开始写入时自动垫上内网直连——保留地址不直连的话第一条
代理规则一生效,访问局域网的流量就全进了代理;按钮改成「补全内网
直连」,只在有缺口时出现。

所有表单对话框点遮罩关闭;失败横幅右侧加关闭按钮。
This commit is contained in:
2026-08-12 00:46:17 +08:00
parent 7d08f37248
commit 013f537eaa
19 changed files with 871 additions and 101 deletions
+22
View File
@@ -9,7 +9,9 @@
* 透传只开放 GET。写操作(切换节点、关连接)属于运行时面板的功能,等做到那一步再单独开,
* 现在不留一个「什么都能转发」的口子。
*/
import { resolve, sep } from 'node:path'
import listenerFields from '../shared/listener-fields.json' with { type: 'json' }
import { isProviderFilePath } from '../shared/provider-files.ts'
import { applyConfig, listBackups, readConfig, restoreBackup } from './config-store.ts'
import { hostInfo, probeExit } from './host.ts'
import { kernelVersion, proxyGet, proxyWrite, restartKernel, selectProxy } from './kernel.ts'
@@ -181,6 +183,26 @@ function makeRoute(env: Env, token: string) {
return result.ok ? json(result) : json(result, applyFailureStatus(result.stage))
}
// ── 清单文件 ────────────────────────────────────────
// 从界面上传的订阅/规则清单,写进内核工作目录的 providers/ 下。
// 路径白名单校验 + 解析后确认没出工作目录,两道都过才落盘
if (path === '/api/provider-files' && req.method === 'POST') {
const body = (await req.json()) as { path?: string; content?: string }
if (typeof body.path !== 'string' || typeof body.content !== 'string') {
return json({ message: '请求缺少 path 或 content 字段' }, 400)
}
if (!isProviderFilePath(body.path)) {
return json({ message: `不允许写入该路径:${body.path}` }, 400)
}
const home = resolve(env.mihomoHome)
const target = resolve(home, body.path)
if (!target.startsWith(home + sep)) {
return json({ message: `不允许写入该路径:${body.path}` }, 400)
}
await Bun.write(target, body.content)
return json({ ok: true })
}
// ── 主机 ────────────────────────────────────────────
// 服务端和内核在同一台机器上,所以主机指标由 Patchbay 自己代答
if (path === '/api/host' && req.method === 'GET') {