Files
mihomo-patchbay/server/kernel.ts
T
im0o 0062a261f7 补上真正日常要点的那个动作:切换策略组当前选中谁
之前的面板只做了「这个入站指向哪个出站」,那是改配置文件。但 metacubexd 里点一条线路
改的根本不是入站,是策略组的当前选中——调 PUT /proxies/{组},不写文件、立刻生效。
mixed-port 走规则表,末尾 MATCH,PROXY 把流量交给 PROXY 组,所以换那个组的选中就等于
换了这个端口的出口。这是每天都要点的动作,之前整个漏了。

面板顶部现在显示完整链条(PROXY → AUTO → 某条线路),并让人在最后一个策略组上换选中。
只显示第一层等于没回答「这个端口现在走哪条线」。

透传的白名单里为此开了唯一一个写操作,单独一条路由而不是放开整个转发。
2026-08-10 12:44:31 +08:00

126 lines
4.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.
/**
* 跟 mihomo 内核打交道的两条通道。
*
* 一条是命令行:`mihomo -t` 校验一份配置能不能被内核接受。
* 一条是 HTTP 管理接口:读运行时状态、让内核重新加载配置。
*
* 两条通道的能力边界不一样,而且有一处不对称必须记住:
* `mihomo -t` **不检查入站的 `proxy:` 指向的出站是否存在**(实测 v1.19.29,
* 这类配置校验通过、重载成功,直到有流量走到那个入站才在日志里报错)。
* 所以这一层不是安全网的全部,落盘前还要跑 Patchbay 自己的检查。
*/
import type { Env } from './env.ts'
export interface ValidateResult {
ok: boolean
/** 内核输出的原文,校验失败时用来告诉用户到底哪里不对 */
message: string
}
/**
* 用 `mihomo -t` 校验一份配置文件。
*
* 超时设得比较宽,因为内核首次校验时可能同步下载 GEO 数据库。
*/
export async function validateConfigFile(
env: Env,
configPath: string,
timeoutMs = 300_000,
): Promise<ValidateResult> {
const proc = Bun.spawn([env.mihomoBin, '-t', '-d', env.mihomoHome, '-f', configPath], {
stdout: 'pipe',
stderr: 'pipe',
})
const timer = setTimeout(() => proc.kill(), timeoutMs)
try {
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
])
const message = [stdout, stderr].filter(Boolean).join('\n').trim()
if (exitCode === 0) return { ok: true, message }
return {
ok: false,
message: message || `mihomo -t 以退出码 ${String(exitCode)} 结束,但没有任何输出`,
}
} finally {
clearTimeout(timer)
}
}
/** 带上认证头访问管理接口。 */
async function callApi(env: Env, path: string, init?: RequestInit): Promise<Response> {
return fetch(`${env.mihomoApi}${path}`, {
...init,
headers: {
Authorization: `Bearer ${env.mihomoSecret}`,
'Content-Type': 'application/json',
...init?.headers,
},
})
}
/** 内核是否在线,在线时返回版本号。 */
export async function kernelVersion(env: Env): Promise<{ online: boolean; version?: string }> {
try {
const res = await callApi(env, '/version', { signal: AbortSignal.timeout(5_000) })
if (!res.ok) return { online: false }
const body = (await res.json()) as { version?: string }
return { online: true, version: body.version }
} catch {
return { online: false }
}
}
/**
* 让内核重新加载配置文件,不重启进程。
*
* `force=true` 意味着连端口这类需要重开监听的改动也一并生效——正是本项目改入站时需要的。
* 注意 `path` 是**内核所在机器上的路径**:部署时 Patchbay 和内核在同一台机器上所以没有问题,
* 但开发时如果 Patchbay 在本机、内核在远端,这个调用不会加载到本地的文件。
*/
export async function reloadConfig(env: Env, configPath: string): Promise<ValidateResult> {
try {
const res = await callApi(env, '/configs?force=true', {
method: 'PUT',
body: JSON.stringify({ path: configPath }),
signal: AbortSignal.timeout(30_000),
})
if (res.ok) return { ok: true, message: '' }
return { ok: false, message: (await res.text()) || `管理接口返回 ${String(res.status)}` }
} catch (error) {
return { ok: false, message: `连不上管理接口:${String(error)}` }
}
}
/**
* 切换某个策略组当前选中哪个成员。
*
* 这是运行时状态,不写配置文件——所以不走 config-store 那条保护链路,也不需要备份和还原。
* 内核重启后会不会记住,取决于配置里的 profile.store-selected。
*/
export async function selectProxy(
env: Env,
group: string,
member: string,
): Promise<ValidateResult> {
try {
const res = await callApi(env, `/proxies/${encodeURIComponent(group)}`, {
method: 'PUT',
body: JSON.stringify({ name: member }),
signal: AbortSignal.timeout(10_000),
})
if (res.ok) return { ok: true, message: '' }
return { ok: false, message: (await res.text()) || `管理接口返回 ${String(res.status)}` }
} catch (error) {
return { ok: false, message: `连不上管理接口:${String(error)}` }
}
}
/** 透传一个管理接口的 GET 请求,用于运行时数据(线路、连接、日志等)。 */
export async function proxyGet(env: Env, path: string): Promise<Response> {
return callApi(env, path, { signal: AbortSignal.timeout(15_000) })
}