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
This commit is contained in:
2026-09-23 09:40:59 +08:00
commit 278f8267b7
333 changed files with 53562 additions and 0 deletions
+372
View File
@@ -0,0 +1,372 @@
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 }
},
}
+794
View File
@@ -0,0 +1,794 @@
export interface ProxyHost {
id: number
hostname: string
listen_port?: number | null
}
export interface BasicAuthConfig {
enabled?: boolean
username?: string
has_password?: boolean
}
export interface RateLimitConfig {
enabled?: boolean
rate?: number
burst?: number
}
export interface ConnLimitConfig {
enabled?: boolean
max?: number
}
export interface ProxySecurityConfig {
ip_blacklist?: string[]
ip_whitelist?: string[]
ip_whitelist_mode?: boolean
china_only?: boolean
basic_auth?: BasicAuthConfig
rate_limit?: RateLimitConfig
conn_limit?: ConnLimitConfig
proxy_ssl_verify_off?: boolean
proxy_host_upstream?: boolean
tls_min_13_only?: boolean
security_headers?: boolean
}
export interface ProxyRule {
id: number
domain: string
upstream: string
listen_port: number
listen_ipv4: boolean
listen_ipv6: boolean
hosts: ProxyHost[]
https_enabled: boolean
http_redirect: boolean
enabled: boolean
nginx_mode?: string
name: string
sort_order: number
security: ProxySecurityConfig
created_at: string
updated_at: string
}
export interface ChinaCIDRStatus {
updated_at?: string
entry_count_v4: number
entry_count_v6: number
last_error?: string
updating?: boolean
ready?: boolean
}
export interface ProxyTraffic {
rule_id: number
upload_total: number
download_total: number
upload_rate: number
download_rate: number
connections: number
}
export interface ProxyClientConn {
ip: string
last_seen: string
}
export interface ProxySecurityPayload {
ip_blacklist?: string[]
ip_whitelist?: string[]
ip_whitelist_mode?: boolean
ip_blacklist_text?: string
ip_whitelist_text?: string
china_only?: boolean
basic_auth?: {
enabled?: boolean
username?: string
password?: string
}
rate_limit?: RateLimitConfig
conn_limit?: ConnLimitConfig
proxy_ssl_verify_off?: boolean
proxy_host_upstream?: boolean
tls_min_13_only?: boolean
security_headers?: boolean
}
export interface ProxySavePayload {
upstream: string
listen_port?: number
listen_ipv4?: boolean
listen_ipv6?: boolean
hosts: string[]
https_enabled?: boolean
http_redirect?: boolean
enabled?: boolean
name?: string
security?: ProxySecurityPayload
}
export interface AuthStatus {
initialized: boolean
authenticated: boolean
}
export interface AppVersion {
version: string
nginx_http_port: number
nginx_https_port: number
}
export interface ApiError {
error: string
}
export interface DashboardStatus {
public_ipv4: string
public_ipv6: string
public_ip_source?: 'ddns' | 'detect' | 'none'
ddns_status: string
ddns_count: number
ddns_last_updated?: string
certificate_status: string
certificate_days: number
certificate_count: number
certificate_summary?: string
proxy_count: number
nginx_status: string
request_today: number
request_yesterday?: number
request_trend?: number | null
requests_hourly: number[]
requests_hourly_labels?: string[]
error_today: number
avg_response_ms: number
started_at: string
uptime_seconds: number
}
export interface DDNSDomainRecord {
domain: string
ipv4?: string
ipv6?: string
status: string
message?: string
}
export interface DDNSConfig {
id: number
provider: string
root_domain: string
record_name: string
record_names?: string[]
ipv4_enabled: boolean
ipv6_enabled: boolean
enabled: boolean
has_token: boolean
last_ipv4?: string
last_ipv6?: string
last_status?: string
last_error?: string
custom_ipv4?: string
custom_ipv6?: string
domain_records?: DDNSDomainRecord[]
last_updated_at?: string
}
export interface CertificateCAOption {
value: string
label: string
}
export interface CertificateJobDone {
ok: boolean
error?: string
domain?: string
cert_path?: string
key_path?: string
cert_dir?: string
expires_at?: string
}
export interface CertificateJobEvent {
type: 'log' | 'done'
level?: string
message?: string
result?: CertificateJobDone
}
export interface CertificateRecord {
id: number
domain: string
domains?: string[]
wildcard: boolean
acme_ca?: string
status: string
days_left: number
last_error?: string
expires_at?: string
last_renew_at?: string
}
export interface AccessLogEntry {
time: string
domain: string
server_port?: number
method: string
path: string
status: number
response_time: number
client_ip: string
upstream: string
}
export interface SystemLogEntry {
time: string
level: string
module: string
message: string
}
export type NotifyType = '' | 'email' | 'webhook' | 'telegram'
export type WebhookProvider = 'bark' | 'ntfy' | 'gotify' | 'custom'
export interface NotifyEmailConfig {
host: string
port: number
username: string
from: string
to: string[]
tls: boolean
has_password?: boolean
}
export interface NotifyWebhookConfig {
provider: WebhookProvider
server: string
key: string
topic: string
url: string
has_secret?: boolean
}
export interface NotifyTelegramConfig {
chat_id: string
proxy_url: string
has_bot_token?: boolean
}
export interface NotifyTestPayload {
type: NotifyType
email: NotifyEmailConfig
webhook: NotifyWebhookConfig
telegram: NotifyTelegramConfig
on_ddns_ip_change: boolean
on_ddns_failure: boolean
on_cert_expiry: boolean
on_cert_renew_success: boolean
on_cert_renew_failure: boolean
on_ip_frequent_access: boolean
on_login_failure: boolean
on_nginx_reload_failure: boolean
ip_frequent_threshold: number
ip_frequent_window_sec: number
login_failure_threshold: number
login_failure_window_sec: number
smtp_password?: string
webhook_secret?: string
telegram_token?: string
}
export type SettingsMap = Record<string, string>
export interface DiscoveredService {
name: string
port: number
host: string
upstream: string
detected: boolean
platform: string
title?: string
suggestion?: string
}
export interface DDNSSavePayload {
provider?: string
root_domain?: string
record_name?: string
record_names?: string[]
domains?: string[]
ipv4_enabled?: boolean
ipv6_enabled?: boolean
enabled?: boolean
custom_ipv4?: string
custom_ipv6?: string
api_token?: string
api_token_id?: string
api_secret?: string
}
export interface DDNSTestPayload {
config_id?: number
provider?: string
api_token?: string
api_token_id?: string
api_secret?: string
}
export interface FRPStatus {
enabled: boolean
connected: boolean
message: string
last_error?: string
server_endpoint?: string
connected_at?: string
uptime_seconds?: number
client_version?: string
http_gateway_enabled?: boolean
https_gateway_enabled?: boolean
synced_domain_count?: number
}
export interface FRPConfig {
enabled: boolean
server_addr: string
server_port: number
auth_token: string
has_auth_token: boolean
tls_enabled: boolean
custom_domains: string[]
}
export interface FRPResponse {
enabled: boolean
server_addr: string
server_port: number
auth_token: string
has_auth_token: boolean
tls_enabled: boolean
custom_domains: string[]
status: FRPStatus
frps_config: string
nginx_http_port: number
nginx_https_port: number
}
export interface FRPSaveResponse {
message: string
config: FRPConfig
status: FRPStatus
frps_config: string
nginx_http_port: number
nginx_https_port: number
}
export interface FRPSavePayload {
enabled: boolean
server_addr: string
server_port: number
auth_token?: string
tls_enabled: boolean
custom_domains: string[]
}
// ===== FRP 多服务端模块(/api/frpmulti,0.1.25 核心迁移)=====
export interface FrpServer {
agent_url?: string
agent_configured?: boolean
ssh_configured?: boolean
mgmt_enabled?: boolean
ssh_host?: string
ssh_port?: number
ssh_user?: string
ssh_auth?: string
id: number
name: string
server_addr: string
server_port: number
tls_enabled: boolean
tls_server_name?: string
enabled: boolean
has_token: boolean
has_oidc_client_secret: boolean
dashboard_has_pwd?: boolean
options: FrpServerOptions
boot_status: 'starting' | 'running' | 'stopped' | 'error'
desired_state: 'starting' | 'running' | 'stopped' | 'error' | string
process_state: 'starting' | 'running' | 'stopped' | 'error' | 'unknown' | string
connection_state: 'connected' | 'connecting' | 'disconnected' | 'error' | 'unknown' | string
state_source: string
process_pid?: number
exit_code?: number
last_connected_at?: string
last_disconnected_at?: string
last_error?: string
last_started_at?: string
created_at: string
updated_at: string
}
export interface FrpServerOptions {
user?: string
location?: string
/** frps 的 vhost 监听端口(http/https 反代回源端口),与 frps.toml 和 Nginx upstream 同源;留空用默认 8080/8443 */
vhost_http_port?: number
vhost_https_port?: number
/** frps 进阶:子域名根(规则 subdomain 生效前提)*/
sub_domain_host?: string
/** HTTP vhost 404 页面路径(VPS 本地路径)*/
custom_404_page?: string
/** 强制 TLS:只接受 TLS 连接(frpc 必须开 transport.tls.enable)*/
tls_force?: boolean
/** 端口白名单:逗号分隔的单端口或范围,如 6000,6005-6010;留空不限制 */
allow_ports?: string
/** 每客户端可绑定端口上限;0/留空不限 */
max_ports_per_client?: number
/** tcpmux 规则的 HTTP CONNECT 监听端口 */
tcpmux_http_connect_port?: number
/** frps 写文件日志 /var/log/frps.log(journalctl 之外)*/
frps_log_to_file?: boolean
/** 文件日志保留天数,默认 3 */
frps_log_max_days?: number
/** frps 管理接口(webServer),仅用于 Fonu 拉取穿透流量等只读信息 */
dashboard_addr?: string
dashboard_port?: number
dashboard_user?: string
dashboard_has_pwd?: boolean
auth_method: 'none' | 'token' | 'oidc'
oidc_client_id?: string
oidc_audience?: string
oidc_scope?: string
oidc_token_endpoint_url?: string
oidc_trusted_ca_path?: string
oidc_insecure_skip_verify?: boolean
oidc_proxy_url?: string
log_level: 'trace' | 'debug' | 'info' | 'warn' | 'error'
log_max_days: number
protocol: 'tcp' | 'kcp' | 'quic' | 'websocket' | 'wss'
proxy_url?: string
dial_server_timeout: number
dial_server_keepalive: number
connect_server_local_ip?: string
dns_server?: string
pool_count: number
tcp_mux: boolean
tcp_mux_keepalive_interval: number
heartbeat_interval: number
heartbeat_timeout: number
login_fail_exit: boolean
tls_disable_custom_first_byte: boolean
auto_start: boolean
remark?: string
tls_certificate_configured?: boolean
tls_key_configured?: boolean
tls_trusted_ca_configured?: boolean
}
export interface FrpProxy {
id: number
server_id: number
name: string
type: 'tcp' | 'udp' | 'http' | 'https' | 'tcpmux' | 'stcp' | 'sudp' | 'xtcp'
local_ip: string
local_port: number
remote_port?: number | null
custom_domains: string[]
host_header_rewrite?: string
options: FrpProxyOptions
enabled: boolean
remark?: string
created_at: string
updated_at: string
}
export interface FrpProxyOptions {
subdomain?: string
/** WebSocket 开关:仅作用于公网 Nginx 反代模板(加 Upgrade/Connection 头) */
websocket?: boolean
/** 公网反代:HTTP 80 强制 301 跳转 HTTPS(需公网证书已部署)*/
redirect_https?: boolean
/** 公网反代:IP 白名单(allow/deny),空则不限制 */
allow_ips?: string[]
/** 公网反代:Basic Auth 用户名;密码由 agent 首次部署自动生成(见配置注释)*/
basic_auth_user?: string
/** 公网反代:IP 黑名单(deny),与白名单可同时使用 */
deny_ips?: string[]
/** 公网反代:Basic Auth 自定义密码;留空则首次部署自动生成 */
basic_auth_password?: string
/** 公网反代:显式关闭 HTTPS(证书已推送也不监听 443);缺省 = 启用 */
https_disabled?: boolean
/** 公网反代:客户端上传大小限制,如 50m */
client_max_body_size?: string
/** 公网反代:代理读超时,如 3600s(长轮询 / 流媒体 / 大文件必需)*/
proxy_read_timeout?: string
/** 公网反代:安全响应头(HSTS / X-Frame-Options / X-Content-Type-Options / Referrer-Policy)*/
security_headers?: boolean
/** 公网反代:仅允许 TLS 1.3 */
tls13_only?: boolean
/** 公网反代:请求限流(次/秒);0 = 不限制 */
rate_limit_rate?: number
/** 公网反代:限流突发允许量 */
rate_limit_burst?: number
/** 公网反代:每 IP 并发连接数上限;0 = 不限制 */
conn_limit_max?: number
/** 公网反代:仅允许中国大陆 IP(部署时自动下发本地维护的 IP 段)*/
china_only?: boolean
locations?: string[]
route_by_http_user?: string
http_user?: string
http_password?: string
multiplexer?: string
secret_key?: string
allow_users?: string[]
transport: {
bandwidth_limit?: string
bandwidth_limit_mode?: 'client' | 'server' | string
use_encryption?: boolean
use_compression?: boolean
proxy_protocol_version?: 'v1' | 'v2' | string
}
health_check: {
type?: 'tcp' | 'http' | string
path?: string
interval_seconds?: number
max_failed?: number
timeout_seconds?: number
http_headers?: Array<{ name: string; value: string }>
}
load_balancer: { group?: string; group_key?: string }
metadatas?: Record<string, string>
request_headers?: Record<string, string>
response_headers?: Record<string, string>
plugin?: {
type: string
unix_path?: string
local_path?: string
strip_prefix?: string
local_addr?: string
http_user?: string
http_password?: string
username?: string
password?: string
crt_path?: string
key_path?: string
host_header_rewrite?: string
destination_ip?: string
}
nat_traversal: { disable_assisted_addrs?: boolean }
}
/** frps 侧单条穿透规则的流量快照;速率为两次采样间隔内的均值 */
export interface FrpProxyTraffic {
traffic_in: number
traffic_out: number
cur_conns: number
traffic_in_rate: number
traffic_out_rate: number
}
/** 按 frp 规则 ID 聚合的流量数据;errors 记录采集失败的服务端/规则错误 */
export interface FrpProxiesTraffic {
items: Record<string, FrpProxyTraffic>
errors?: Record<string, string>
sampled_at: string
}
export interface FrpStatus {
status: 'running' | 'stopped' | 'unknown'
pid?: number
frpc_version?: string
last_started_at?: string
last_reloaded_at?: string
last_error?: string
configured: boolean
enabled_proxies: number
}
export interface FrpRuntimeInfo {
platform: string
configured_bin: string
active_bin?: string
active_version?: string
detected_version?: string
download_proxy: string
config_path: string
config_paths?: string[]
config_exists: boolean
}
export interface FrpRelease {
version: string
published_at?: string
asset_name: string
size: number
}
export interface FrpBinary {
version: string
path: string
size: number
installed_at: string
active: boolean
}
/** 单项诊断结果;state 为 ok/failed/unknown,unknown 表示无法确认而非失败 */
export interface FrpDiagnosticCheck {
name: 'tcp_reachable' | 'frpc_login' | 'proxy_reachable' | 'dns_resolution' | 'location' | string
state: 'ok' | 'failed' | 'unknown' | string
detail?: string
latency_ms?: number
}
export interface FrpServerDiagnostics {
server_id: number
available: boolean
latency_ms?: number
location?: string
checks?: FrpDiagnosticCheck[]
error?: string
}
export interface FrpServerSample {
checked_at: string
available: boolean
latency_ms?: number
error?: string
}
export interface FrpServerStatusDetail {
available: boolean
availability: number
average_latency_ms: number
peak_latency_ms: number
samples: FrpServerSample[]
last_checked_at?: string
last_error?: string
}
export interface FrpServerDetail {
server: FrpServer
status: FrpServerStatusDetail
proxies: FrpProxy[]
}
export interface FrpDownloadTask {
version?: string
status: 'idle' | 'queued' | 'downloading' | 'verifying' | 'extracting' | 'completed' | 'failed'
message: string
downloaded: number
total: number
error?: string
updated_at?: string
}
export interface FrpServerPayload {
agent_url?: string
agent_token?: string
clear_agent_token?: boolean
ssh_host?: string
ssh_port?: number
ssh_user?: string
ssh_auth?: string
ssh_secret?: string
clear_ssh_secret?: boolean
mgmt_enabled?: boolean
name: string
server_addr: string
server_port: number
tls_enabled?: boolean
tls_server_name?: string
enabled?: boolean
token?: string
clear_token?: boolean
options: FrpServerOptions
oidc_client_secret?: string
clear_oidc_client_secret?: boolean
dashboard_password?: string
clear_dashboard_password?: boolean
tls_certificate?: string
tls_key?: string
tls_trusted_ca?: string
clear_tls_certificate?: boolean
clear_tls_key?: boolean
clear_tls_trusted_ca?: boolean
}
export interface FrpAgentRouteCandidate {
id: number
name: string
type: 'http' | 'https'
custom_domains: string[]
local_ip: string
local_port: number
enabled: boolean
}
export interface FrpAgentRouteHealth {
proxy_id: number
domain: string
dns_ok: boolean
dns_detail?: string
cert_ok: boolean
cert_detail?: string
tunnel_ok: boolean
tunnel_name?: string
tunnel_detail?: string
service_ok: boolean
}
export interface FrpAgentRouteDetail {
domain: string
file_exists: boolean
syntax_ok: boolean
loaded: boolean
}
export interface FrpAgentStatus {
agent_version?: string
ports?: Record<string, boolean>
frps_active?: boolean
frps_enabled?: boolean
frps_version?: string
frps_log_tail?: string
frps_unit_exec?: string
dashboard_addr?: string
service: string
nginx_binary: string
nginx_conf_dir: string
/** nginx -t 是否通过 */
nginx_ok?: boolean
/** 未找到 nginx 可执行文件 */
nginx_missing?: boolean
/** 解析到的 nginx 路径 */
nginx_path?: string
/** 有 nginx 进程在运行 */
nginx_running?: boolean
/** nginx 版本,如 nginx/1.24.0 */
nginx_version?: string
/** 探测失败原因(缺失或配置有误) */
nginx_error?: string
frps_binary: string
frps_config_path: string
routes: string[]
checked_at: string
}
export interface FrpProxyPayload {
server_id: number
name: string
type: FrpProxy['type']
local_ip: string
local_port: number
remote_port?: number | null
custom_domains?: string[]
host_header_rewrite?: string
options?: FrpProxyOptions
enabled?: boolean
remark?: string
}
export interface NginxBackupEntry {
name: string
created_at: string
}
export interface RuleNginxView {
mode: 'auto' | 'custom'
enabled: boolean
active: boolean
generated: string
content: string
backups: NginxBackupEntry[]
}
export interface GlobalNginxView {
mode: 'auto' | 'custom'
generated_framework: string
generated_snippet: string
content: string
backups: NginxBackupEntry[]
}