455f46caa0
Build and Deploy / Build and Push Docker Image (push) Successful in 1m53s
- Initialize activeInterconnectNodeId with empty string - Read from localStorage only in client-side - Defer selected state initialization in NodeSwitcher to useEffect
881 lines
30 KiB
TypeScript
881 lines
30 KiB
TypeScript
// 获取 base URL(从后端注入的全局变量)
|
||
const BASE_URL = (window as any).__BASE_URL__ || ''
|
||
const API_VERSION = (window as any).__API_VERSION__ || '/api/v1'
|
||
const API_BASE_URL = BASE_URL + API_VERSION
|
||
|
||
interface ApiResponse<T> {
|
||
code: number
|
||
msg: string
|
||
data: T
|
||
}
|
||
|
||
export interface MonitorStats {
|
||
env: {
|
||
os: string
|
||
arch: string
|
||
go_version: string
|
||
num_cpu: number
|
||
goroutines: number
|
||
}
|
||
host: {
|
||
cpu_percent: number
|
||
mem_total: number
|
||
mem_used: number
|
||
mem_percent: number
|
||
disk_total: number
|
||
disk_used: number
|
||
disk_percent: number
|
||
uptime: number
|
||
platform: string
|
||
}
|
||
mem: {
|
||
alloc: number
|
||
total_alloc: number
|
||
sys: number
|
||
lookups: number
|
||
mallocs: number
|
||
frees: number
|
||
}
|
||
heap: {
|
||
heap_alloc: number
|
||
heap_sys: number
|
||
heap_idle: number
|
||
heap_inuse: number
|
||
heap_released: number
|
||
heap_objects: number
|
||
}
|
||
gc: {
|
||
next_gc: number
|
||
last_gc: number
|
||
pause_total_ns: number
|
||
num_gc: number
|
||
}
|
||
scheduler: {
|
||
scheduled: number
|
||
running: number
|
||
queue_size: number
|
||
worker_count: number
|
||
workers: {
|
||
id: number
|
||
status: string
|
||
task_id?: string
|
||
task_name?: string
|
||
start_time?: number
|
||
duration?: number
|
||
}[]
|
||
}
|
||
}
|
||
|
||
// 初始化时使用空字符串,避免 SSR 时访问 localStorage
|
||
export let activeInterconnectNodeId = ''
|
||
export let activeInterconnectNodeName = ''
|
||
|
||
// 在客户端初始化时从 localStorage 读取
|
||
if (typeof window !== 'undefined') {
|
||
activeInterconnectNodeId = localStorage.getItem('activeInterconnectNodeId') || ''
|
||
activeInterconnectNodeName = localStorage.getItem('activeInterconnectNodeName') || ''
|
||
}
|
||
|
||
export function setActiveInterconnectNodeId(id: string, name?: string) {
|
||
activeInterconnectNodeId = id
|
||
if (name !== undefined) {
|
||
activeInterconnectNodeName = name
|
||
}
|
||
localStorage.removeItem('site_settings_cache')
|
||
if (id) {
|
||
localStorage.setItem('activeInterconnectNodeId', id)
|
||
if (name) localStorage.setItem('activeInterconnectNodeName', name)
|
||
document.cookie = `active_interconnect_node_id=${id}; path=/; max-age=${7 * 24 * 3600}`
|
||
} else {
|
||
localStorage.removeItem('activeInterconnectNodeId')
|
||
localStorage.removeItem('activeInterconnectNodeName')
|
||
activeInterconnectNodeName = ''
|
||
document.cookie = `active_interconnect_node_id=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC`
|
||
}
|
||
}
|
||
|
||
let redirectingToLogin = false
|
||
|
||
function redirectToLogin() {
|
||
const loginPath = `${BASE_URL}/login`
|
||
if (redirectingToLogin) return
|
||
if (window.location.pathname === `${BASE_URL}/login` || window.location.pathname.endsWith('/login')) return
|
||
redirectingToLogin = true
|
||
window.location.href = loginPath
|
||
}
|
||
|
||
async function parseApiResponse<T>(res: Response, url: string): Promise<ApiResponse<T>> {
|
||
const text = await res.text()
|
||
const trimmed = text.trim()
|
||
|
||
if (!trimmed) {
|
||
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
||
throw new Error('后端服务不可用,请确认已启动 TaskPool server(默认 :8052)')
|
||
}
|
||
if (!res.ok) {
|
||
throw new Error(`请求失败(HTTP ${res.status})`)
|
||
}
|
||
throw new Error('服务器返回了空响应')
|
||
}
|
||
|
||
let json: ApiResponse<T>
|
||
try {
|
||
json = JSON.parse(trimmed) as ApiResponse<T>
|
||
} catch {
|
||
if (res.status === 502 || res.status === 503 || res.status === 504) {
|
||
throw new Error('后端服务不可用,请确认已启动 TaskPool server(默认 :8052)')
|
||
}
|
||
throw new Error(`服务器返回了非 JSON 响应(HTTP ${res.status})`)
|
||
}
|
||
|
||
if (json.code === 401) {
|
||
// 公开接口不应触发硬跳转;其它接口未登录时跳转登录页
|
||
const isPublic =
|
||
url.startsWith('/settings/public') || url.startsWith('/auth/login') || url.startsWith('/install')
|
||
if (!isPublic) redirectToLogin()
|
||
throw new Error(json.msg || '请先登录')
|
||
}
|
||
|
||
if (typeof json.code === 'number' && json.code !== 200) {
|
||
throw new Error(json.msg || '请求失败')
|
||
}
|
||
|
||
// 兼容非标准成功体:HTTP 非 2xx 时优先报状态码
|
||
if (!res.ok && (json as any)?.code === undefined) {
|
||
throw new Error(json.msg || `请求失败(HTTP ${res.status})`)
|
||
}
|
||
|
||
return json
|
||
}
|
||
|
||
export async function request<T>(url: string, options?: RequestInit): Promise<T> {
|
||
let res: Response
|
||
try {
|
||
res = await fetch(`${API_BASE_URL}${url}`, {
|
||
...options,
|
||
credentials: 'include', // 携带 Cookie
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...options?.headers,
|
||
},
|
||
})
|
||
} catch (err: any) {
|
||
// 路由切换 / React Query 取消请求时,浏览器会报 net::ERR_ABORTED
|
||
if (err?.name === 'AbortError' || options?.signal?.aborted) {
|
||
throw err
|
||
}
|
||
throw new Error(err?.message || '网络请求失败,请检查后端是否已启动')
|
||
}
|
||
|
||
const json = await parseApiResponse<T>(res, url)
|
||
return json.data
|
||
}
|
||
|
||
// 检查登录状态(不触发自动跳转)
|
||
export async function checkAuth(): Promise<boolean> {
|
||
try {
|
||
const res = await fetch(`${API_BASE_URL}/auth/me`, {
|
||
credentials: 'include',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
})
|
||
const text = await res.text()
|
||
if (!text.trim()) return false
|
||
const json = JSON.parse(text) as ApiResponse<{ username: string }>
|
||
return json.code === 200
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
export const api = {
|
||
auth: {
|
||
login: (data: { username: string; password: string }) =>
|
||
request<{ user: string }>('/auth/login', { method: 'POST', body: JSON.stringify(data) }),
|
||
logout: () => request('/auth/logout', { method: 'POST' }),
|
||
me: () => request<{ username: string; role: string }>('/auth/me'),
|
||
register: (data: { username: string; password: string; email: string }) =>
|
||
request('/auth/register', { method: 'POST', body: JSON.stringify(data) })
|
||
},
|
||
tasks: {
|
||
list: (params?: { page?: number; page_size?: number; name?: string; agent_id?: string; tags?: string; type?: string; sort_by?: string; order?: string }) => {
|
||
const query = new URLSearchParams()
|
||
if (params?.page) query.set('page', String(params.page))
|
||
if (params?.page_size) query.set('page_size', String(params.page_size))
|
||
if (params?.name) query.set('name', params.name)
|
||
if (params?.tags) query.set('tags', params.tags)
|
||
if (params?.agent_id) query.set('agent_id', params.agent_id)
|
||
if (params?.type) query.set('type', params.type)
|
||
if (params?.sort_by) query.set('sort_by', params.sort_by)
|
||
if (params?.order) query.set('order', params.order)
|
||
return request<TaskListResponse>(`/tasks?${query}`)
|
||
},
|
||
create: (data: Partial<Task>) => request<Task>('/tasks', { method: 'POST', body: JSON.stringify(data) }),
|
||
update: (id: string, data: Partial<Task>) => request<Task>(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||
delete: (id: string, params?: { delete_files?: boolean }) => {
|
||
const query = new URLSearchParams()
|
||
if (params?.delete_files !== undefined) query.set('delete_files', String(params.delete_files))
|
||
const queryString = query.toString()
|
||
return request(`/tasks/${id}${queryString ? '?' + queryString : ''}`, { method: 'DELETE' })
|
||
},
|
||
batchDelete: (ids: string[]) => request<{ count: number }>('/tasks/batch-delete', { method: 'POST', body: JSON.stringify({ ids }) }),
|
||
batchDeleteByQuery: (params?: { name?: string, agent_id?: string, tags?: string, type?: string }) => {
|
||
const query = new URLSearchParams()
|
||
if (params?.name) query.append('name', params.name)
|
||
if (params?.agent_id) query.append('agent_id', params.agent_id)
|
||
if (params?.tags) query.append('tags', params.tags)
|
||
if (params?.type && params.type !== 'all') query.append('type', params.type)
|
||
return request<{ count: number }>(`/tasks/batch-by-query?${query.toString()}`, { method: 'DELETE' })
|
||
},
|
||
execute: (id: string) => request<ExecutionResult>(`/execute/task/${id}`, { method: 'POST' }),
|
||
stop: (logID: string) => request(`/tasks/stop/${logID}`, { method: 'POST' }),
|
||
tags: () => request<string[]>('/tasks/tags')
|
||
},
|
||
scripts: {
|
||
list: () => request<Script[]>('/scripts'),
|
||
create: (data: Partial<Script>) => request<Script>('/scripts', { method: 'POST', body: JSON.stringify(data) }),
|
||
update: (id: string, data: Partial<Script>) => request<Script>(`/scripts/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||
delete: (id: string) => request(`/scripts/${id}`, { method: 'DELETE' })
|
||
},
|
||
env: {
|
||
list: (params?: { page?: number; page_size?: number; name?: string; type?: string; tags?: string }) => {
|
||
const query = new URLSearchParams()
|
||
if (params?.page) query.set('page', String(params.page))
|
||
if (params?.page_size) query.set('page_size', String(params.page_size))
|
||
if (params?.name) query.set('name', params.name)
|
||
if (params?.type && params.type !== 'all') query.set('type', params.type)
|
||
if (params?.tags) query.set('tags', params.tags)
|
||
return request<EnvListResponse>(`/env?${query}`)
|
||
},
|
||
tags: () => request<string[]>('/env/tags'),
|
||
secretStatus: () => request<boolean>('/env/secret-status'),
|
||
all: () => request<EnvVar[]>('/env/all'),
|
||
tasks: (id: string) => request<Task[]>(`/env/${id}/tasks`),
|
||
create: (data: Partial<EnvVar>) => request<EnvVar>('/env', { method: 'POST', body: JSON.stringify(data) }),
|
||
update: (id: string, data: Partial<EnvVar>) => request<EnvVar>(`/env/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
|
||
delete: (id: string, force?: boolean) => {
|
||
const query = force ? '?force=true' : ''
|
||
return fetch(`${API_BASE_URL}/env/${id}${query}`, {
|
||
method: 'DELETE',
|
||
credentials: 'include'
|
||
}).then(res => res.json() as Promise<ApiResponse<any>>)
|
||
}
|
||
},
|
||
execute: {
|
||
command: (command: string) => request('/execute/command', { method: 'POST', body: JSON.stringify({ command }) }),
|
||
results: () => request('/execute/results')
|
||
},
|
||
logs: {
|
||
list: (params?: { page?: number; page_size?: number; task_id?: string; task_name?: string; status?: string }) => {
|
||
const query = new URLSearchParams()
|
||
if (params?.page) query.set('page', String(params.page))
|
||
if (params?.page_size) query.set('page_size', String(params.page_size))
|
||
if (params?.task_id) query.set('task_id', params.task_id)
|
||
if (params?.task_name) query.set('task_name', params.task_name)
|
||
if (params?.status) query.set('status', params.status)
|
||
return request<LogListResponse>(`/logs?${query}`)
|
||
},
|
||
get: (id: string) => request<LogDetail>(`/logs/${id}`),
|
||
detail: (id: string) => request<LogDetail>(`/logs/${id}`),
|
||
delete: (id: string) => request(`/logs/${id}`, { method: 'DELETE' }),
|
||
clear: (taskId?: string) => request('/logs/clear', { method: 'POST', body: JSON.stringify({ task_id: taskId }) })
|
||
},
|
||
dashboard: {
|
||
stats: () => request<Stats>('/stats'),
|
||
sentence: () => request<{ sentence: string }>('/sentence'),
|
||
sendStats: (days?: number) => request<DailyStats[]>(`/sendstats${days ? `?days=${days}` : ''}`),
|
||
taskStats: (days?: number) => request<TaskStatsItem[]>(`/taskstats${days ? `?days=${days}` : ''}`)
|
||
},
|
||
settings: {
|
||
getMonitor: () => request<MonitorStats>('/monitor'),
|
||
changePassword: (data: { old_username?: string; username?: string; old_password: string; new_password?: string }) =>
|
||
request('/settings/password', { method: 'POST', body: JSON.stringify(data) }),
|
||
getSite: () => request<SiteSettings>('/settings/site'),
|
||
getPublicSite: () => request<{ title: string; subtitle: string; icon: string; demo_mode: boolean }>('/settings/public'),
|
||
updateSite: (data: SiteSettings) =>
|
||
request('/settings/site', { method: 'PUT', body: JSON.stringify(data) }),
|
||
generateOpenapiToken: () => request<{ token: string }>('/settings/site/openapi-token/generate', { method: 'POST' }),
|
||
getScheduler: () => request<SchedulerSettings>('/settings/scheduler'),
|
||
updateScheduler: (data: SchedulerSettings) =>
|
||
request('/settings/scheduler', { method: 'PUT', body: JSON.stringify(data) }),
|
||
getPaths: () => request<{ scripts_dir: string }>('/settings/paths'),
|
||
getAbout: () => request<AboutInfo>('/settings/about'),
|
||
getChangelog: () => request<string>('/settings/changelog'),
|
||
get: (section: string, key: string) => request<string>(`/settings/${section}/${key}`),
|
||
getSection: (section: string) => request<Record<string, string>>(`/settings/${section}`),
|
||
setSection: (section: string, values: Record<string, string>) =>
|
||
request(`/settings/${section}`, { method: 'PUT', body: JSON.stringify(values) }),
|
||
generateToken: (section: string, key: string) =>
|
||
request<string>(`/settings/${section}/${key}/generate`, { method: 'POST' }),
|
||
getLoginLogs: (params?: { page?: number; page_size?: number; username?: string }) => {
|
||
const query = new URLSearchParams()
|
||
if (params?.page) query.set('page', String(params.page))
|
||
if (params?.page_size) query.set('page_size', String(params.page_size))
|
||
if (params?.username) query.set('username', params.username)
|
||
return request<LoginLogListResponse>(`/settings/loginlogs?${query}`)
|
||
},
|
||
createBackup: () => request('/settings/backup', { method: 'POST' }),
|
||
getBackupStatus: () => request<{ has_backup: boolean; backup_time: string }>('/settings/backup/status'),
|
||
downloadBackup: () => `${API_BASE_URL}/settings/backup/download`,
|
||
restoreBackup: async (file: File) => {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
const res = await fetch(`${API_BASE_URL}/settings/restore`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: formData
|
||
})
|
||
const json: ApiResponse<null> = await res.json()
|
||
if (json.code === 401) {
|
||
window.location.href = BASE_URL + '/login'
|
||
throw new Error('请先登录')
|
||
}
|
||
if (json.code !== 200) throw new Error(json.msg || '恢复失败')
|
||
}
|
||
},
|
||
files: {
|
||
tree: () => request<FileNode[]>('/files/tree'),
|
||
getContent: (path: string) => request<{ path: string; content: string }>(`/files/content?path=${encodeURIComponent(path)}`),
|
||
download: (path: string) => `${API_BASE_URL}/files/download?path=${encodeURIComponent(path)}`,
|
||
downloadZip: (path: string) => `${API_BASE_URL}/files/download-zip?path=${encodeURIComponent(path)}`,
|
||
saveContent: (path: string, content: string) => request('/files/content', { method: 'POST', body: JSON.stringify({ path, content }) }),
|
||
create: (path: string, isDir: boolean) => request('/files/create', { method: 'POST', body: JSON.stringify({ path, isDir }) }),
|
||
delete: (path: string) => request('/files/delete', { method: 'POST', body: JSON.stringify({ path }) }),
|
||
rename: (oldPath: string, newPath: string) => request('/files/rename', { method: 'POST', body: JSON.stringify({ oldPath, newPath }) }),
|
||
move: (oldPath: string, newPath: string) => request('/files/move', { method: 'POST', body: JSON.stringify({ oldPath, newPath }) }),
|
||
copy: (sourcePath: string, targetPath: string) => request('/files/copy', { method: 'POST', body: JSON.stringify({ sourcePath, targetPath }) }),
|
||
uploadArchive: async (file: File, targetPath?: string) => {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
if (targetPath) formData.append('path', targetPath)
|
||
|
||
const res = await fetch(`${API_BASE_URL}/files/upload`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: formData
|
||
})
|
||
const json: ApiResponse<null> = await res.json()
|
||
if (json.code === 401) {
|
||
window.location.href = BASE_URL + '/login'
|
||
throw new Error('请先登录')
|
||
}
|
||
if (json.code !== 200) throw new Error(json.msg || '上传失败')
|
||
},
|
||
uploadFiles: async (files: FileList, paths: string[], targetPath?: string) => {
|
||
const formData = new FormData()
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i]
|
||
if (file) {
|
||
formData.append('files', file)
|
||
formData.append('paths', paths[i] || file.name)
|
||
}
|
||
}
|
||
if (targetPath) formData.append('path', targetPath)
|
||
|
||
const res = await fetch(`${API_BASE_URL}/files/uploadfiles`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: formData
|
||
})
|
||
const json: ApiResponse<null> = await res.json()
|
||
if (json.code === 401) {
|
||
window.location.href = BASE_URL + '/login'
|
||
throw new Error('请先登录')
|
||
}
|
||
if (json.code !== 200) throw new Error(json.msg || '上传失败')
|
||
}
|
||
},
|
||
deps: {
|
||
list: (params?: { language?: string; lang_version?: string }) => {
|
||
const query = new URLSearchParams()
|
||
if (params?.language) query.set('language', params.language)
|
||
if (params?.lang_version) query.set('lang_version', params.lang_version)
|
||
return request<Dependency[]>(`/deps?${query}`)
|
||
},
|
||
create: (data: { name: string; version?: string; language: string; lang_version?: string; remark?: string }) =>
|
||
request<Dependency>('/deps', { method: 'POST', body: JSON.stringify(data) }),
|
||
delete: (id: string) => request(`/deps/${id}`, { method: 'DELETE' }),
|
||
install: (data: any) => request<any>('/deps/install', { method: 'POST', body: JSON.stringify(data) }),
|
||
getInstallCmd: (data: any) => request<{ command: string }>('/deps/install-cmd', { method: 'POST', body: JSON.stringify(data) }),
|
||
uninstall: (id: string, force?: boolean) => {
|
||
const query = force ? '?force=true' : ''
|
||
return request<any>(`/deps/uninstall/${id}${query}`, { method: 'POST' })
|
||
},
|
||
reinstall: (id: string) => request(`/deps/reinstall/${id}`, { method: 'POST' }),
|
||
reinstallAll: (language: string, lang_version?: string) => {
|
||
const query = new URLSearchParams({ language })
|
||
if (lang_version) query.set('lang_version', lang_version)
|
||
return request(`/deps/reinstall-all?${query}`, { method: 'POST' })
|
||
},
|
||
getReinstallAllCmd: (language: string, lang_version?: string) => {
|
||
const query = new URLSearchParams({ language })
|
||
if (lang_version) query.set('lang_version', lang_version)
|
||
return request<{ command: string }>(`/deps/reinstall-all-cmd?${query}`, { method: 'POST' })
|
||
},
|
||
getBatchInstallCmd: (data: { items: { name: string; version?: string; language: string; lang_version?: string }[] }) =>
|
||
request<{ command: string }>('/deps/batch-install-cmd', { method: 'POST', body: JSON.stringify(data) }),
|
||
import: (data: { language: string; lang_version?: string; content: string; import_db?: boolean }) =>
|
||
request<{ dependencies: Dependency[]; command: string }>('/deps/import', { method: 'POST', body: JSON.stringify(data) }),
|
||
getInstalled: (language: string, lang_version?: string) => {
|
||
const query = new URLSearchParams({ language })
|
||
if (lang_version) query.set('lang_version', lang_version)
|
||
return request<Dependency[]>(`/deps/installed?${query}`)
|
||
},
|
||
getInstallSuggestCmd: (logID: string) => request<{ command: string }>(`/deps/install-suggest-cmd?log_id=${logID}`)
|
||
},
|
||
agents: {
|
||
list: () => request<Agent[]>('/agents'),
|
||
getVersion: () => request<{ version: string; platforms: { os: string; arch: string; filename: string }[] }>('/agents/version'),
|
||
update: (id: string, data: { name: string; description?: string; enabled: boolean; scheduler_config: SchedulerConfig | null }) =>
|
||
request('/agents/' + id, { method: 'PUT', body: JSON.stringify(data) }),
|
||
delete: (id: string) => request('/agents/' + id, { method: 'DELETE' }),
|
||
forceUpdate: (id: string) => request('/agents/' + id + '/update', { method: 'POST' }),
|
||
downloadUrl: (os: string, arch: string) => `${API_BASE_URL}/agent/download?os=${os}&arch=${arch}`,
|
||
// 令牌管理
|
||
listTokens: () => request<AgentToken[]>('/agents/tokens'),
|
||
createToken: (data: { remark?: string; max_uses?: number; expires_at?: string }) =>
|
||
request<AgentToken>('/agents/tokens', { method: 'POST', body: JSON.stringify(data) }),
|
||
deleteToken: (id: string) => request('/agents/tokens/' + id, { method: 'DELETE' }),
|
||
updateToken: (id: string, data: { remark?: string; max_uses?: number; expires_at?: string }) =>
|
||
request<AgentToken>('/agents/tokens/' + id, { method: 'PUT', body: JSON.stringify(data) })
|
||
},
|
||
mise: {
|
||
list: () => request<MiseLanguage[]>('/mise/ls'),
|
||
sync: () => request<void>('/mise/sync', { method: 'POST' }),
|
||
plugins: () => request<string[]>('/mise/plugins'),
|
||
versions: (plugin: string) => request<string[]>(`/mise/versions?plugin=${plugin}`),
|
||
verifyCommand: (plugin: string, version: string) => request<{ command: string }>(`/mise/verify-cmd?plugin=${plugin}&version=${version}`),
|
||
useGlobal: (plugin: string, version: string) => request<void>('/mise/use-global', { method: 'POST', body: JSON.stringify({ plugin, version }) }),
|
||
unsetGlobal: (plugin: string, version: string) => request<void>('/mise/unset-global', { method: 'POST', body: JSON.stringify({ plugin, version }) }),
|
||
getEnvs: () => request<Record<string, string>>('/mise/envs'),
|
||
setEnv: (key: string, value: string) => request<void>('/mise/envs', { method: 'POST', body: JSON.stringify({ key, value }) }),
|
||
unsetEnv: (key: string) => request<void>(`/mise/envs?key=${key}`, { method: 'DELETE' })
|
||
},
|
||
terminal: {
|
||
cmds: () => request<{ name: string, description: string }[]>('/terminal/cmds')
|
||
},
|
||
notify: {
|
||
getTypes: () => request<{ channel_types: ChannelType[]; event_types: EventType[] }>('/notify/types'),
|
||
getChannels: () => request<NotifyChannel[]>('/notify/channels'),
|
||
saveChannel: (data: Partial<NotifyChannel>) =>
|
||
request('/notify/channels', { method: 'POST', body: JSON.stringify(data) }),
|
||
deleteChannel: (id: string) => request('/notify/channels/' + id, { method: 'DELETE' }),
|
||
testChannel: (data: Partial<NotifyChannel>) =>
|
||
request<NotifyResult>('/notify/channels/test', { method: 'POST', body: JSON.stringify(data) }),
|
||
|
||
getBindings: () => request<NotifyBinding[]>('/notify/bindings'),
|
||
saveBinding: (data: Partial<NotifyBinding>) =>
|
||
request<NotifyBinding>('/notify/bindings', { method: 'POST', body: JSON.stringify(data) }),
|
||
saveBindingsBatch: (data: { type: string; data_id: string; bindings: Partial<NotifyBinding>[] }) =>
|
||
request('/notify/bindings/batch', { method: 'POST', body: JSON.stringify(data) }),
|
||
deleteBinding: (id: string) => request('/notify/bindings/' + id, { method: 'DELETE' }),
|
||
send: (data: { channel_id: string; title: string; text: string }) =>
|
||
request<NotifyResult>('/notify/send', { method: 'POST', body: JSON.stringify(data) })
|
||
},
|
||
appLogs: {
|
||
list: (params?: { page?: number; page_size?: number; category?: string; status?: string; level?: string; keyword?: string }) => {
|
||
const query = new URLSearchParams()
|
||
if (params?.page) query.set('page', String(params.page))
|
||
if (params?.page_size) query.set('page_size', String(params.page_size))
|
||
if (params?.category) query.set('category', params.category)
|
||
if (params?.status) query.set('status', params.status)
|
||
if (params?.level) query.set('level', params.level)
|
||
if (params?.keyword) query.set('keyword', params.keyword)
|
||
return request<AppLogListResponse>(`/app-logs?${query}`)
|
||
},
|
||
markAsRead: (data: { id?: string; category?: string }) => request('/app-logs/read', { method: 'POST', body: JSON.stringify(data) }),
|
||
clear: (category: string) => request('/app-logs/clear', { method: 'POST', body: JSON.stringify({ category }) })
|
||
},
|
||
webui: {
|
||
list: () => request<WebUI[]>('/webui'),
|
||
upload: async (file: File) => {
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
const res = await fetch(`${API_BASE_URL}/webui/upload`, {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: formData
|
||
})
|
||
const json: ApiResponse<{ message: string, theme: string }> = await res.json()
|
||
if (json.code === 401) {
|
||
window.location.href = BASE_URL + '/login'
|
||
throw new Error('请先登录')
|
||
}
|
||
if (json.code !== 200) throw new Error(json.msg || '上传失败')
|
||
return json.data
|
||
},
|
||
setActive: (name: string) => request<{ message: string }>('/webui/active', { method: 'PUT', body: JSON.stringify({ name }) }),
|
||
delete: (name: string) => request<{ message: string }>(`/webui/${name}`, { method: 'DELETE' })
|
||
},
|
||
system: {
|
||
export: (data: { task_ids?: string[], env_ids?: string[] }) => request<any>('/system/export', { method: 'POST', body: JSON.stringify(data) })
|
||
}
|
||
}
|
||
|
||
export interface WebUI {
|
||
name: string
|
||
version: string
|
||
author: string
|
||
description: string
|
||
min_panel_version: string
|
||
}
|
||
|
||
export interface FileNode {
|
||
name: string
|
||
path: string
|
||
isDir: boolean
|
||
modTime: number
|
||
children?: FileNode[]
|
||
}
|
||
|
||
export interface Task {
|
||
id: string
|
||
name: string
|
||
remark: string
|
||
command: string
|
||
pre_command: string
|
||
post_command: string
|
||
tags: string
|
||
type: string
|
||
trigger_type: string
|
||
config: string
|
||
schedule: string
|
||
timeout: number
|
||
work_dir: string
|
||
clean_config: string
|
||
envs: string
|
||
retry_count: number
|
||
retry_interval: number
|
||
random_range: number
|
||
pin_type: 'none' | 'top'
|
||
languages: { name: string; version: string }[]
|
||
agent_id: string | null
|
||
enabled: boolean
|
||
last_run: string
|
||
next_run: string
|
||
running_status?: string
|
||
repo_task_id?: string
|
||
created_at?: string
|
||
updated_at?: string
|
||
}
|
||
|
||
export interface RepoConfig {
|
||
source_type: string
|
||
source_url: string
|
||
target_path: string
|
||
branch: string
|
||
sparse_path: string
|
||
single_file: boolean
|
||
proxy: string
|
||
proxy_url: string
|
||
auth_token: string
|
||
whitelist_paths?: string
|
||
blacklist?: string
|
||
dependence?: string
|
||
extensions?: string
|
||
auto_add_cron?: boolean
|
||
commenttotask?: string
|
||
concurrency?: number
|
||
repo_source?: string
|
||
repo_dir_name?: string
|
||
}
|
||
|
||
export interface ExecutionResult {
|
||
task_id: string
|
||
log_id?: string
|
||
success: boolean
|
||
status?: string
|
||
output?: string
|
||
error?: string
|
||
duration?: number
|
||
exit_code?: number
|
||
start_time?: string
|
||
end_time?: string
|
||
}
|
||
|
||
export interface TaskListResponse {
|
||
data: Task[]
|
||
total: number
|
||
page: number
|
||
page_size: number
|
||
}
|
||
|
||
export interface Script {
|
||
id: string
|
||
name: string
|
||
content: string
|
||
}
|
||
|
||
export interface EnvVar {
|
||
id: string
|
||
name: string
|
||
value: string
|
||
remark: string
|
||
type: string
|
||
hidden: boolean
|
||
enabled: boolean
|
||
tags: string
|
||
created_at?: string
|
||
updated_at?: string
|
||
}
|
||
|
||
export interface EnvListResponse {
|
||
data: EnvVar[]
|
||
total: number
|
||
page: number
|
||
page_size: number
|
||
}
|
||
|
||
export interface Stats {
|
||
tasks: number
|
||
today_execs: number
|
||
envs: number
|
||
logs: number
|
||
scheduled: number
|
||
running: number
|
||
}
|
||
|
||
|
||
export interface TaskLog {
|
||
id: string
|
||
task_id: string
|
||
task_name: string
|
||
task_type: string
|
||
command: string
|
||
status: string
|
||
duration: number
|
||
error: string | null
|
||
start_time: string | null
|
||
end_time: string | null
|
||
created_at: string
|
||
}
|
||
|
||
export interface LogListResponse {
|
||
data: TaskLog[]
|
||
total: number
|
||
page: number
|
||
page_size: number
|
||
}
|
||
|
||
export interface LogDetail {
|
||
id: string
|
||
task_id: string
|
||
command: string
|
||
output: string
|
||
error: string | null
|
||
status: string
|
||
duration: number
|
||
start_time: string | null
|
||
end_time: string | null
|
||
created_at: string
|
||
}
|
||
|
||
export interface AboutInfo {
|
||
version: string
|
||
remote_version?: string
|
||
build_time: string
|
||
mem_usage: string
|
||
goroutines: number
|
||
uptime: string
|
||
task_count: number
|
||
log_count: number
|
||
env_count: number
|
||
}
|
||
|
||
export interface SiteSettings {
|
||
title: string
|
||
subtitle: string
|
||
icon: string
|
||
page_size: string
|
||
cookie_days: string
|
||
openapi_enabled?: boolean
|
||
openapi_token?: string
|
||
openapi_token_expire?: string
|
||
system_notice_days?: string
|
||
system_notice_max_count?: string
|
||
push_log_days?: string
|
||
push_log_max_count?: string
|
||
login_log_days?: string
|
||
login_log_max_count?: string
|
||
scheduler_log_days?: string
|
||
scheduler_log_max_count?: string
|
||
active_webui?: string
|
||
}
|
||
|
||
export interface SchedulerSettings {
|
||
worker_count: string
|
||
queue_size: string
|
||
rate_interval: string
|
||
}
|
||
|
||
|
||
export interface LoginLog {
|
||
id: string
|
||
username: string
|
||
ip: string
|
||
user_agent: string
|
||
status: string
|
||
message: string
|
||
created_at: string
|
||
}
|
||
|
||
export interface LoginLogListResponse {
|
||
data: LoginLog[]
|
||
total: number
|
||
page: number
|
||
page_size: number
|
||
}
|
||
|
||
export interface DailyStats {
|
||
day: string
|
||
total: number
|
||
success: number
|
||
failed: number
|
||
}
|
||
|
||
export interface TaskStatsItem {
|
||
task_id: string
|
||
task_name: string
|
||
count: number
|
||
}
|
||
|
||
export interface Dependency {
|
||
id: string
|
||
name: string
|
||
version: string
|
||
language: string
|
||
lang_version: string
|
||
remark: string
|
||
log: string
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export interface Agent {
|
||
id: string
|
||
name: string
|
||
token: string
|
||
machine_id: string
|
||
description: string
|
||
status: string
|
||
last_seen: string
|
||
ip: string
|
||
version: string
|
||
build_time: string
|
||
hostname: string
|
||
os: string
|
||
arch: string
|
||
enabled: boolean
|
||
scheduler_config: SchedulerConfig | null
|
||
created_at: string
|
||
updated_at: string
|
||
}
|
||
|
||
export interface SchedulerConfig {
|
||
worker_count: number
|
||
queue_size: number
|
||
rate_interval: number
|
||
verbose: boolean
|
||
strict_queue: boolean
|
||
}
|
||
|
||
export interface AgentToken {
|
||
id: string
|
||
token: string
|
||
remark: string
|
||
max_uses: number
|
||
used_count: number
|
||
expires_at: string | null
|
||
enabled: boolean
|
||
created_at: string
|
||
}
|
||
|
||
export interface MiseLanguage {
|
||
plugin: string
|
||
version: string
|
||
source: { type?: string; path?: string } | string
|
||
is_global: boolean
|
||
install_path?: string
|
||
installed_at?: string // 安装日期
|
||
}
|
||
|
||
export interface ChannelType {
|
||
type: string
|
||
label: string
|
||
}
|
||
|
||
export interface EventType {
|
||
type: string
|
||
label: string
|
||
binding_type?: string
|
||
}
|
||
|
||
export interface NotifyChannel {
|
||
id: string
|
||
name: string
|
||
type: string
|
||
enabled: boolean
|
||
created_at?: string
|
||
config: Record<string, string>
|
||
}
|
||
|
||
export interface NotifyBinding {
|
||
id: string
|
||
type: string
|
||
event: string
|
||
way_id: string
|
||
data_id: string
|
||
extra?: string
|
||
created_at?: string
|
||
updated_at?: string
|
||
}
|
||
|
||
export interface BindingExtra {
|
||
enable_log: boolean
|
||
log_limit: number
|
||
}
|
||
|
||
export interface NotifyResult {
|
||
success: boolean
|
||
error?: string
|
||
}
|
||
|
||
export interface AppLog {
|
||
id: string
|
||
category: string
|
||
title: string
|
||
content: string
|
||
level: string
|
||
status: string
|
||
ref_id: string
|
||
channel_name?: string
|
||
error_msg: string
|
||
created_at: string
|
||
read_at: string | null
|
||
}
|
||
|
||
export interface AppLogListResponse {
|
||
data: AppLog[]
|
||
total: number
|
||
}
|
||
|
||
export const LOG_CATEGORY = {
|
||
SYSTEM_NOTICE: 'system_notice',
|
||
PUSH_LOG: 'push_log',
|
||
LOGIN_LOG: 'login_log',
|
||
SCHEDULER_LOG: 'scheduler_log'
|
||
} as const
|
||
|
||
export const LOG_LEVEL = {
|
||
INFO: 'info',
|
||
WARNING: 'warning',
|
||
ERROR: 'error'
|
||
} as const
|
||
|
||
export const LOG_STATUS = {
|
||
UNREAD: 'unread',
|
||
READ: 'read',
|
||
SUCCESS: 'success',
|
||
FAILED: 'failed'
|
||
} as const
|
||
|