Files
-Fonu/web/src/api/client.ts
T
mrlee 278f8267b7 chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听)
- 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP)
- 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点)
- 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
2026-09-23 09:40:59 +08:00

373 lines
17 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.
import type {
AccessLogEntry,
ApiError,
AppVersion,
AuthStatus,
CertificateCAOption,
CertificateRecord,
DashboardStatus,
DDNSConfig,
DDNSSavePayload,
DDNSTestPayload,
FRPResponse,
FRPSavePayload,
FRPSaveResponse,
FRPStatus,
FrpBinary,
FrpDownloadTask,
FrpProxy,
FrpProxyPayload,
FrpProxiesTraffic,
FrpRelease,
FrpRuntimeInfo,
FrpServer,
FrpServerDiagnostics,
FrpServerDetail,
FrpServerPayload,
FrpStatus,
DiscoveredService,
ProxyClientConn,
ChinaCIDRStatus,
ProxyRule,
ProxySavePayload,
ProxyTraffic,
GlobalNginxView,
RuleNginxView,
SettingsMap,
SystemLogEntry,
NotifyTestPayload,
} from './types'
export function asList<T>(value: T[] | null | undefined): T[] {
return Array.isArray(value) ? value : []
}
const REQUEST_TIMEOUT_MS = 30_000
const CERT_REQUEST_TIMEOUT_MS = 10 * 60_000
async function request<T>(path: string, init?: RequestInit, timeoutMs = REQUEST_TIMEOUT_MS): Promise<T> {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), timeoutMs)
try {
return await requestWithSignal<T>(path, init, controller.signal)
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
throw new Error(
timeoutMs > REQUEST_TIMEOUT_MS
? '证书操作超时,请稍后在证书列表查看状态,或在「日志 → 实时日志」查看 Nginx 输出'
: '请求超时,请检查 Fonu 服务或 Nginx 状态',
)
}
throw error
} finally {
clearTimeout(timeout)
}
}
async function requestWithSignal<T>(path: string, init?: RequestInit, signal?: AbortSignal): Promise<T> {
const response = await fetch(path, {
credentials: 'include',
signal,
headers: {
'Content-Type': 'application/json',
...(init?.headers ?? {}),
},
...init,
})
if (!response.ok) {
let message = '请求失败'
try {
const body = (await response.json()) as ApiError
if (body.error) {
message = body.error
}
} catch {
// ignore
}
throw new Error(message)
}
if (response.status === 204) {
return undefined as T
}
return (await response.json()) as T
}
export const api = {
getVersion: () => request<AppVersion>('/api/version'),
authStatus: () => request<AuthStatus>('/api/auth/status'),
login: (username: string, password: string) =>
request<{ message: string }>('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
}),
logout: () => request<{ message: string }>('/api/auth/logout', { method: 'POST' }),
changePassword: (old_password: string, new_password: string) =>
request<{ message: string }>('/api/auth/password', {
method: 'POST',
body: JSON.stringify({ old_password, new_password }),
}),
getStatus: () => request<DashboardStatus>('/api/status'),
listProxies: () => request<ProxyRule[]>('/api/proxies'),
createProxy: (payload: ProxySavePayload) =>
request<ProxyRule>('/api/proxies', { method: 'POST', body: JSON.stringify(payload) }),
updateProxy: (id: number, payload: Partial<ProxySavePayload>) =>
request<ProxyRule>(`/api/proxies/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
deleteProxy: (id: number) => request<void>(`/api/proxies/${id}`, { method: 'DELETE' }),
reorderProxies: (ids: number[]) =>
request<void>('/api/proxies/reorder', { method: 'PUT', body: JSON.stringify({ ids }) }),
getProxyTraffic: () => request<ProxyTraffic[]>('/api/proxies/traffic'),
getProxyClients: (id: number) => request<ProxyClientConn[]>(`/api/proxies/${id}/clients`),
getProxyNginx: (id: number) => request<RuleNginxView>(`/api/proxies/${id}/nginx`),
saveProxyNginx: (id: number, payload: { mode: 'auto' | 'custom'; content?: string }) =>
request<RuleNginxView>(`/api/proxies/${id}/nginx`, { method: 'PUT', body: JSON.stringify(payload) }),
rollbackProxyNginx: (id: number, backup?: string) =>
request<RuleNginxView>(`/api/proxies/${id}/nginx/rollback`, {
method: 'POST',
body: JSON.stringify(backup ? { backup } : {}),
}),
listDDNS: () => request<DDNSConfig[]>('/api/ddns'),
listDDNSLite: () => request<DDNSConfig[]>('/api/ddns?lite=1'),
createDDNS: (payload: DDNSSavePayload) =>
request<DDNSConfig>('/api/ddns', { method: 'POST', body: JSON.stringify(payload) }),
updateDDNS: (id: number, payload: DDNSSavePayload) =>
request<DDNSConfig>(`/api/ddns/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
deleteDDNS: (id: number) => request<void>(`/api/ddns/${id}`, { method: 'DELETE' }),
testDDNS: (payload: DDNSTestPayload) =>
request<{ message: string }>('/api/ddns/test', {
method: 'POST',
body: JSON.stringify(payload),
}),
updateAllDDNS: () => request<DDNSConfig[]>('/api/ddns/update', { method: 'POST' }),
updateDDNSOne: (id: number) => request<DDNSConfig>(`/api/ddns/${id}/update`, { method: 'POST' }),
listCertificates: () => request<CertificateRecord[]>('/api/certificates'),
listCertificateCAOptions: () => request<CertificateCAOption[]>('/api/certificates/ca-options'),
applyCertificate: (payload: {
dns_zone?: string
ddns_config_id?: number
domains: string[]
ca?: string
email?: string
}) =>
request<{ job_id: string }>('/api/certificates/apply', {
method: 'POST',
body: JSON.stringify({
dns_zone: payload.dns_zone ?? '',
ddns_config_id: payload.ddns_config_id ?? 0,
domains: payload.domains,
ca: payload.ca ?? '',
email: payload.email ?? '',
}),
}),
certificateApplyStreamURL: (jobId: string) => `/api/certificates/jobs/${encodeURIComponent(jobId)}/stream`,
importCertificate: (payload: {
certificate?: string
private_key?: string
cert_path?: string
key_path?: string
}) =>
request<CertificateRecord>('/api/certificates/import', {
method: 'POST',
body: JSON.stringify(payload),
}),
async downloadCertificate(domain: string, part: 'zip' | 'cert' | 'key' = 'zip') {
const response = await fetch(
`/api/certificates/${encodeURIComponent(domain)}/download?part=${part}`,
{ credentials: 'include' },
)
if (!response.ok) {
let message = '下载证书失败'
try {
const body = (await response.json()) as ApiError
if (body.error) message = body.error
} catch {
// ignore
}
throw new Error(message)
}
const blob = await response.blob()
const disposition = response.headers.get('Content-Disposition') ?? ''
const match = disposition.match(/filename="?([^";\n]+)"?/)
const filename = match?.[1] ?? `${domain}.zip`
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
},
deleteCertificate: (domain: string) =>
request<{ message: string }>(`/api/certificates/${encodeURIComponent(domain)}`, { method: 'DELETE' }),
renewCertificate: (domain?: string, ca?: string) =>
request<CertificateRecord>(
'/api/certificates/renew',
{
method: 'POST',
body: JSON.stringify({ domain: domain ?? '', ca: ca ?? '' }),
},
CERT_REQUEST_TIMEOUT_MS,
),
getSettings: () => request<SettingsMap>('/api/settings'),
saveSettings: (payload: SettingsMap) =>
request<SettingsMap>('/api/settings', { method: 'PUT', body: JSON.stringify(payload) }),
getGlobalNginx: () => request<GlobalNginxView>('/api/settings/nginx/global'),
saveGlobalNginx: (payload: { mode: 'auto' | 'custom'; content?: string }) =>
request<GlobalNginxView>('/api/settings/nginx/global', { method: 'PUT', body: JSON.stringify(payload) }),
rollbackGlobalNginx: (backup?: string) =>
request<GlobalNginxView>('/api/settings/nginx/global/rollback', {
method: 'POST',
body: JSON.stringify(backup ? { backup } : {}),
}),
getChinaCIDRStatus: () => request<ChinaCIDRStatus>('/api/settings/china-cidr'),
refreshChinaCIDR: () =>
request<ChinaCIDRStatus>('/api/settings/china-cidr/refresh', { method: 'POST' }),
testNotify: (payload: NotifyTestPayload) =>
request<{ message: string }>('/api/settings/notify/test', {
method: 'POST',
body: JSON.stringify(payload),
}),
getFRP: () => request<FRPResponse>('/api/frp'),
saveFRP: (payload: FRPSavePayload) =>
request<FRPSaveResponse>('/api/frp', {
method: 'PUT',
body: JSON.stringify(payload),
}),
getFRPStatus: () => request<FRPStatus>('/api/frp/status'),
syncFRPDomains: () =>
request<FRPSaveResponse & { domains: string[] }>('/api/frp/sync-domains', {
method: 'POST',
}),
getFRPLogs: (limit = 200) => request<string[]>(`/api/frp/logs?limit=${limit}`),
// ===== FRP 多服务端模块(/api/frpmulti)=====
getFrpStatus: () => request<FrpStatus>('/api/frpmulti/status'),
getFrpRuntime: () => request<FrpRuntimeInfo>('/api/frpmulti/runtime'),
setFrpDownloadProxy: (proxy: string) => request<FrpRuntimeInfo>('/api/frpmulti/runtime/download-proxy', { method: 'PUT', body: JSON.stringify({ proxy }) }),
listFrpReleases: () => request<FrpRelease[]>('/api/frpmulti/releases'),
listFrpBinaries: () => request<FrpBinary[]>('/api/frpmulti/binaries'),
getFrpDownloadStatus: () => request<FrpDownloadTask>('/api/frpmulti/download'),
downloadFrpRelease: (version: string) => request<FrpDownloadTask>(`/api/frpmulti/releases/${encodeURIComponent(version)}/download`, { method: 'POST' }),
activateFrpBinary: (version: string) => request<FrpStatus>(`/api/frpmulti/binaries/${encodeURIComponent(version)}/activate`, { method: 'POST' }),
deleteFrpBinary: (version: string) => request<void>(`/api/frpmulti/binaries/${encodeURIComponent(version)}`, { method: 'DELETE' }),
listFrpServers: () => request<FrpServer[]>('/api/frpmulti/servers'),
getFrpServerDiagnostics: (id: number) => request<FrpServerDiagnostics>(`/api/frpmulti/servers/${id}/diagnostics`),
getFrpServerDetail: (id: number) => request<FrpServerDetail>(`/api/frpmulti/servers/${id}/detail`),
startFrpServer: (id: number) => request<FrpServer>(`/api/frpmulti/servers/${id}/start`, { method: 'POST' }),
stopFrpServer: (id: number) => request<FrpServer>(`/api/frpmulti/servers/${id}/stop`, { method: 'POST' }),
createFrpServer: (payload: FrpServerPayload) =>
request<FrpServer>('/api/frpmulti/servers', { method: 'POST', body: JSON.stringify(payload) }),
updateFrpServer: (id: number, payload: FrpServerPayload) =>
request<FrpServer>(`/api/frpmulti/servers/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
deleteFrpServer: (id: number) => request<void>(`/api/frpmulti/servers/${id}`, { method: 'DELETE' }),
testFrpServer: (id: number) => request<{ message: string }>(`/api/frpmulti/servers/${id}/test`, { method: 'POST' }),
listFrpProxies: () => request<FrpProxy[]>('/api/frpmulti/proxies'),
getFrpProxyLogs: (id: number, limit = 200) => request<string[]>(`/api/frpmulti/proxies/${id}/logs?limit=${limit}`),
listFrpProxiesTraffic: () => request<FrpProxiesTraffic>('/api/frpmulti/proxies/traffic'),
createFrpProxy: (payload: FrpProxyPayload) =>
request<FrpProxy>('/api/frpmulti/proxies', { method: 'POST', body: JSON.stringify(payload) }),
updateFrpProxy: (id: number, payload: FrpProxyPayload) =>
request<FrpProxy>(`/api/frpmulti/proxies/${id}`, { method: 'PUT', body: JSON.stringify(payload) }),
deleteFrpProxy: (id: number) => request<void>(`/api/frpmulti/proxies/${id}`, { method: 'DELETE' }),
startFrp: () => request<FrpStatus>('/api/frpmulti/start', { method: 'POST' }),
stopFrp: () => request<FrpStatus>('/api/frpmulti/stop', { method: 'POST' }),
reloadFrp: () => request<FrpStatus>('/api/frpmulti/reload', { method: 'POST' }),
getFrpLogs: (limit = 200, serverID?: number) => {
const query = new URLSearchParams({ limit: String(limit) })
if (serverID) query.set('server_id', String(serverID))
return request<string[]>(`/api/frpmulti/logs?${query.toString()}`)
},
exportFrpConfig: () => request<{ content: string }>('/api/frpmulti/config/export'),
getFrpFRPSConfig: (id: number) => request<{ content: string }>(`/api/frpmulti/servers/${id}/frps-config`),
testFrpAgent: (id: number) => request<{ ok: boolean }>(`/api/frpmulti/servers/${id}/agent/test`, { method: 'POST' }),
probeFrpAgent: (id: number) => request<Record<string, any>>(`/api/frpmulti/servers/${id}/agent/probe`, { method: 'POST' }),
installFrpAgent: (id: number) => request<FrpServer>(`/api/frpmulti/servers/${id}/agent/install`, { method: 'POST' }),
uninstallFrpAgent: (id: number, keepData: boolean) => request<{ ok: boolean; output: string }>(`/api/frpmulti/servers/${id}/agent/uninstall?keep_data=${keepData}`, { method: 'POST' }),
sshDiagnoseFrpAgent: (id: number) => request<{ output: string }>(`/api/frpmulti/servers/${id}/agent/ssh-diagnose`, { method: 'POST' }),
getFrpAgentStatus: (id: number) => request<import('./types').FrpAgentStatus>(`/api/frpmulti/servers/${id}/agent/status`),
getFrpAgentRoutes: (id: number) => request<import('./types').FrpAgentRouteDetail[]>(`/api/frpmulti/servers/${id}/agent/routes`),
getFrpAgentRouteCandidates: (id: number) => request<import('./types').FrpProxy[]>(`/api/frpmulti/servers/${id}/agent/route-candidates`),
deployFrpAgentRoute: (serverId: number, proxyId: number) => request<{ ok: boolean }>(`/api/frpmulti/servers/${serverId}/agent/routes/${proxyId}`, { method: 'POST' }),
getFrpAgentRouteConf: (serverId: number, proxyId: number) =>
request<{ proxy_id: number; domain: string; content: string }>(`/api/frpmulti/servers/${serverId}/agent/routes/${proxyId}/conf`),
installFrpAgentNginx: (serverId: number) =>
request<{ ok: boolean; manager?: string; output?: string; path?: string; error?: string }>(`/api/frpmulti/servers/${serverId}/agent/nginx/install`, { method: 'POST' }),
getFrpAgentRouteHealth: (serverId: number) =>
request<import('./types').FrpAgentRouteHealth[]>(`/api/frpmulti/servers/${serverId}/agent/routes/health`),
removeFrpAgentRoute: (serverId: number, proxyId: number) => request<{ ok: boolean }>(`/api/frpmulti/servers/${serverId}/agent/routes/${proxyId}`, { method: 'DELETE' }),
pushFrpConfigToAgent: (id: number) => request<{ ok: boolean }>(`/api/frpmulti/servers/${id}/agent/frps-config`, { method: 'POST' }),
frpsServerAction: (id: number, action: 'start' | 'stop' | 'restart') =>
request<{ ok: boolean }>(`/api/frpmulti/servers/${id}/agent/frps-action`, { method: 'POST', body: JSON.stringify({ action }) }),
frpsServerActionRaw: (id: number, action: 'start' | 'stop' | 'restart') =>
request<{ ok: boolean; error?: string }>(`/api/frpmulti/servers/${id}/agent/frps-action`, { method: 'POST', body: JSON.stringify({ action }) }),
installFrpServer: (id: number, version: string, proxy: string) =>
request<Record<string, any>>(`/api/frpmulti/servers/${id}/agent/frps-install`, { method: 'POST', body: JSON.stringify({ version, proxy }) }),
getAccessLogs: (params?: { limit?: number; keyword?: string; status?: number }) => {
const q = new URLSearchParams()
if (params?.limit) q.set('limit', String(params.limit))
if (params?.keyword) q.set('keyword', params.keyword)
if (params?.status) q.set('status', String(params.status))
return request<AccessLogEntry[]>(`/api/logs/access?${q}`)
},
getErrorLogs: (params?: { limit?: number; keyword?: string }) => {
const q = new URLSearchParams()
if (params?.limit) q.set('limit', String(params.limit))
if (params?.keyword) q.set('keyword', params.keyword)
return request<string[]>(`/api/logs/error?${q}`)
},
getSystemLogs: (params?: { limit?: number; level?: string; keyword?: string }) => {
const q = new URLSearchParams()
if (params?.limit) q.set('limit', String(params.limit))
if (params?.level) q.set('level', params.level)
if (params?.keyword) q.set('keyword', params.keyword)
return request<SystemLogEntry[]>(`/api/logs/system?${q}`)
},
scanDiscovery: (host?: string) => {
const q = new URLSearchParams()
if (host) q.set('host', host)
return request<DiscoveredService[]>(`/api/discovery/scan?${q}`)
},
async exportBackup() {
const response = await fetch('/api/backup/export', { credentials: 'include' })
if (!response.ok) {
throw new Error('导出备份失败')
}
const blob = await response.blob()
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `fonu-backup-${new Date().toISOString().slice(0, 10)}.tar.gz`
link.click()
URL.revokeObjectURL(url)
},
async restoreBackup(file: File) {
const form = new FormData()
form.append('file', file)
const response = await fetch('/api/backup/restore', {
method: 'POST',
credentials: 'include',
body: form,
})
if (!response.ok) {
let message = '恢复备份失败'
try {
const body = (await response.json()) as ApiError
if (body.error) message = body.error
} catch {
// ignore
}
throw new Error(message)
}
return (await response.json()) as { message: string }
},
}