Files
mihomo-patchbay/web/api.ts
T
im0o 7a764f5fe1 搭出前端骨架和入站列表
设计基调定为:层次靠底色和一像素描边而不是投影,一屏最多两层容器,间距给足。
tokens.css 顶部写了这三条规则,后面的组件都是它们的推论。

入站列表把两套写法合并成一张表:老式顶层端口字段(mixed-port 这些)和 listeners 数组。
只画 listeners 的话,一台只用 mixed-port 的机器打开会是空板,而那个端口正在跑——
这等于对用户撒谎。合并逻辑在 shared/inbounds.ts,带 10 个测试。

后端顺带能提供打包后的静态文件了,一个进程就能跑起来。
2026-08-10 04:10:17 +08:00

95 lines
2.8 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.
/**
* 后端接口的客户端。
*
* 令牌存在 localStorage 里。它等同于密码,但这个页面本来就只有能访问 127.0.0.1
* (或者持有 SSH 隧道)的人才打得开,存在浏览器里不额外扩大暴露面。
*/
import type { ConfigDiagnostic, ConfigObject } from '../vendor/config-editor/index.ts'
const TOKEN_KEY = 'patchbay.token'
export function getToken(): string {
return localStorage.getItem(TOKEN_KEY) ?? ''
}
export function setToken(value: string): void {
localStorage.setItem(TOKEN_KEY, value.trim())
}
export function clearToken(): void {
localStorage.removeItem(TOKEN_KEY)
}
/** 接口报错时抛这个,界面据此决定是提示重填令牌还是显示错误内容。 */
export class ApiError extends Error {
constructor(
readonly status: number,
readonly payload: Record<string, unknown>,
) {
super(typeof payload.message === 'string' ? payload.message : `请求失败(${String(status)})`)
this.name = 'ApiError'
}
get unauthorized(): boolean {
return this.status === 401
}
}
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(path, {
method,
headers: {
Authorization: `Bearer ${getToken()}`,
...(body === undefined ? {} : { 'Content-Type': 'application/json' }),
},
body: body === undefined ? undefined : JSON.stringify(body),
})
const text = await res.text()
const payload = text ? (JSON.parse(text) as Record<string, unknown>) : {}
if (!res.ok) throw new ApiError(res.status, payload)
return payload as T
}
export interface ConfigSnapshot {
data: ConfigObject
yaml: string
revision: string
diagnostics: ConfigDiagnostic[]
}
export interface ApplySuccess {
ok: true
snapshot: ConfigSnapshot
reloaded: boolean
reloadMessage: string
}
export interface ApplyFailure {
ok: false
stage: 'stale' | 'self-check' | 'kernel-check' | 'write' | 'reload'
message: string
hint: string
restored: boolean
}
export const api = {
readConfig: () => request<ConfigSnapshot>('GET', '/api/config'),
applyConfig: (data: ConfigObject, baseRevision: string) =>
request<ApplySuccess>('PUT', '/api/config', { data, baseRevision }),
listBackups: () => request<{ backups: string[] }>('GET', '/api/config/backups'),
restoreBackup: (name: string) =>
request<ApplySuccess>('POST', `/api/config/backups/${encodeURIComponent(name)}/restore`),
kernel: () => request<{ online: boolean; version?: string }>('GET', '/api/kernel'),
/** 订阅里当前有哪些线路。配置文件给不出这个答案,只能问运行中的内核。 */
providers: () =>
request<{ providers: Record<string, { vehicleType: string; proxies: { name: string }[] }> }>(
'GET',
'/api/kernel/providers/proxies',
),
}