import { useEffect, useRef, useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Download, ExternalLink, RefreshCw, Trash2, Upload, UploadCloud } from 'lucide-react' import { api, type WebUI } from '@/api' import { useAbout, useSchedulerSettings, useSiteSettings } from '@/api/hooks' import { useTheme } from '@/context/ThemeContext' import { Button, Card, Input, PageHeader, Switch, Tabs, Textarea } from '@/components/ui' import { toast } from '@/lib/toast' export default function Settings() { const { theme, setTheme } = useTheme() const qc = useQueryClient() const { data: site } = useSiteSettings() const { data: scheduler } = useSchedulerSettings() const { data: about } = useAbout() const [tab, setTab] = useState('site') const [siteForm, setSiteForm] = useState({ title: '', subtitle: '', page_size: '20', cookie_days: '7', openapi_enabled: false, openapi_token: '', system_notice_days: '7', system_notice_max_count: '1000', push_log_days: '7', push_log_max_count: '1000', login_log_days: '30', login_log_max_count: '1000', scheduler_log_days: '7', scheduler_log_max_count: '1000', }) const [schedulerForm, setSchedulerForm] = useState({ worker_count: '4', queue_size: '100', rate_interval: '0' }) const [passwordForm, setPasswordForm] = useState({ old_password: '', new_password: '', username: '' }) const [changelog, setChangelog] = useState('') const [backupStatus, setBackupStatus] = useState<{ has_backup: boolean; backup_time: string } | null>(null) const [message, setMessage] = useState('') const webuiInputRef = useRef(null) const { data: loginLogs } = useQuery({ queryKey: ['loginLogs'], queryFn: () => api.settings.getLoginLogs({ page: 1, page_size: 20 }), enabled: tab === 'security', }) const { data: webuis, refetch: refetchWebui, isFetching: webuiFetching } = useQuery({ queryKey: ['webuis'], queryFn: async () => { const list = await api.webui.list() return Array.isArray(list) ? list : [] }, enabled: tab === 'webui', }) useEffect(() => { if (site) { setSiteForm({ title: site.title || '', subtitle: site.subtitle || '', page_size: site.page_size || '20', cookie_days: site.cookie_days || '7', openapi_enabled: !!site.openapi_enabled, openapi_token: site.openapi_token || '', system_notice_days: site.system_notice_days || '7', system_notice_max_count: site.system_notice_max_count || '1000', push_log_days: site.push_log_days || '7', push_log_max_count: site.push_log_max_count || '1000', login_log_days: site.login_log_days || '30', login_log_max_count: site.login_log_max_count || '1000', scheduler_log_days: site.scheduler_log_days || '7', scheduler_log_max_count: site.scheduler_log_max_count || '1000', }) } }, [site]) useEffect(() => { if (scheduler) { setSchedulerForm({ worker_count: scheduler.worker_count || '4', queue_size: scheduler.queue_size || '100', rate_interval: scheduler.rate_interval || '0', }) } }, [scheduler]) useEffect(() => { if (tab !== 'about' && tab !== 'backup') return if (tab === 'about') { api.settings .getChangelog() .then((t) => setChangelog(typeof t === 'string' ? t : '')) .catch(() => setChangelog('')) } api.settings .getBackupStatus() .then(setBackupStatus) .catch(() => setBackupStatus(null)) }, [tab]) const saveSite = useMutation({ mutationFn: () => api.settings.updateSite({ ...(site || {}), ...siteForm, openapi_enabled: siteForm.openapi_enabled, } as any), onSuccess: () => { qc.invalidateQueries({ queryKey: ['siteSettings'] }) qc.invalidateQueries({ queryKey: ['publicSite'] }) setMessage('站点设置已保存') toast.success('站点设置已保存') }, onError: (e: any) => { setMessage(e?.message || '保存失败') toast.error(e?.message || '保存失败') }, }) const saveScheduler = useMutation({ mutationFn: () => api.settings.updateScheduler(schedulerForm), onSuccess: () => { qc.invalidateQueries({ queryKey: ['schedulerSettings'] }) setMessage('调度设置已保存') toast.success('调度设置已保存') }, onError: (e: any) => { setMessage(e?.message || '保存失败') toast.error(e?.message || '保存失败') }, }) const changePassword = useMutation({ mutationFn: () => api.settings.changePassword({ old_password: passwordForm.old_password, new_password: passwordForm.new_password || undefined, username: passwordForm.username || undefined, }), onSuccess: () => { setPasswordForm({ old_password: '', new_password: '', username: '' }) setMessage('账号信息已更新') toast.success('账号信息已更新') }, onError: (e: any) => { setMessage(e?.message || '更新失败') toast.error(e?.message || '更新失败') }, }) const genToken = useMutation({ mutationFn: () => api.settings.generateOpenapiToken(), onSuccess: (res) => { setSiteForm((f) => ({ ...f, openapi_token: res.token || '' })) setMessage('OpenAPI Token 已生成') toast.success('OpenAPI Token 已生成') qc.invalidateQueries({ queryKey: ['siteSettings'] }) }, onError: (e: any) => { setMessage(e?.message || '生成失败') toast.error(e?.message || '生成失败') }, }) const createBackup = useMutation({ mutationFn: () => api.settings.createBackup(), onSuccess: async () => { setMessage('备份已创建') toast.success('备份已创建') setBackupStatus(await api.settings.getBackupStatus()) }, onError: (e: any) => { setMessage(e?.message || '备份失败') toast.error(e?.message || '备份失败') }, }) const uploadWebui = useMutation({ mutationFn: (file: File) => api.webui.upload(file), onSuccess: (res) => { const msg = res?.message || 'WebUI 上传成功' setMessage(msg) toast.success(msg) refetchWebui() qc.invalidateQueries({ queryKey: ['siteSettings'] }) }, onError: (e: any) => { setMessage(e?.message || '上传失败') toast.error(e?.message || '上传失败') }, }) const activateWebui = useMutation({ mutationFn: (name: string) => api.webui.setActive(name), onSuccess: (res) => { const msg = res?.message || '已切换前端主题' setMessage(msg) toast.success(msg) refetchWebui() qc.invalidateQueries({ queryKey: ['siteSettings'] }) }, onError: (e: any) => { setMessage(e?.message || '切换失败') toast.error(e?.message || '切换失败') }, }) const deleteWebui = useMutation({ mutationFn: (name: string) => api.webui.delete(name), onSuccess: (res) => { const msg = res?.message || '已删除 WebUI' setMessage(msg) toast.success(msg) refetchWebui() }, onError: (e: any) => { setMessage(e?.message || '删除失败') toast.error(e?.message || '删除失败') }, }) async function restoreBackup(file?: File | null) { if (!file) return try { await api.settings.restoreBackup(file) setMessage('恢复成功,建议刷新页面') toast.success('恢复成功,建议刷新页面') } catch (e: any) { setMessage(e?.message || '恢复失败') toast.error(e?.message || '恢复失败') } } const logs = Array.isArray(loginLogs?.data) ? loginLogs.data : [] const webuiList: WebUI[] = Array.isArray(webuis) ? webuis : [] const activeWebui = site?.active_webui || '' return (
{message ?
{message}
: null} {tab === 'site' ? (

外观

站点设置

setSiteForm((f) => ({ ...f, title: e.target.value }))} /> setSiteForm((f) => ({ ...f, subtitle: e.target.value }))} />
setSiteForm((f) => ({ ...f, page_size: e.target.value }))} />
setSiteForm((f) => ({ ...f, cookie_days: e.target.value }))} />
MCP Server(Hermes / OpenClaw)

OpenAPI 是 REST 接口;AI Agent 请使用 taskpool mcp 作为 MCP Server 完整接管任务/脚本/日志。

{`{
  "mcpServers": {
    "taskpool": {
      "command": "taskpool",
      "args": ["mcp"],
      "env": {
        "TASKPOOL_URL": "${window.location.origin}",
        "TASKPOOL_TOKEN": "${siteForm.openapi_token || '在设置页生成 OpenAPI Token'}"
      }
    }
  }
}`}

详细说明见仓库 docs/guide/mcp.md 与 skills/taskpool/SKILL.md

日志保留

设置各类日志的保留天数和最大条数限制,超出后自动清理最旧记录

系统通知

系统级事件通知,如任务失败报警、服务状态变更等

setSiteForm((f) => ({ ...f, system_notice_days: e.target.value }))} /> setSiteForm((f) => ({ ...f, system_notice_max_count: e.target.value }))} />
推送日志

消息推送记录,如钉钉、企业微信、Telegram 等渠道的发送历史

setSiteForm((f) => ({ ...f, push_log_days: e.target.value }))} /> setSiteForm((f) => ({ ...f, push_log_max_count: e.target.value }))} />
登录日志

用户登录记录,包含 IP、时间、状态等安全审计信息

setSiteForm((f) => ({ ...f, login_log_days: e.target.value }))} /> setSiteForm((f) => ({ ...f, login_log_max_count: e.target.value }))} />
调度日志

任务执行日志,包含命令输出、执行状态、耗时等信息

setSiteForm((f) => ({ ...f, scheduler_log_days: e.target.value }))} /> setSiteForm((f) => ({ ...f, scheduler_log_max_count: e.target.value }))} />
) : null} {tab === 'webui' ? (

自定义前端包 (WebUI)

上传并切换自定义前端资源包,实现深度定制

开发文档
{ const file = e.target.files?.[0] if (file) uploadWebui.mutate(file) e.target.value = '' }} />
{webuiList.length === 0 ? (
暂无自定义前端包,可上传 zip 资源包
) : (
{webuiList.map((item) => { const active = activeWebui === item.name return (
{item.name} {active ? ( 当前 ) : null}
v{item.version || '-'} · {item.author || '未知作者'} {item.min_panel_version ? ` · 最低面板 ${item.min_panel_version}` : ''}
{item.description ?
{item.description}
: null}
) })}
)}
) : null} {tab === 'scheduler' ? (

调度设置

控制任务调度器的并发能力和队列行为

setSchedulerForm((f) => ({ ...f, worker_count: e.target.value }))} />

并发执行任务的最大数量

setSchedulerForm((f) => ({ ...f, queue_size: e.target.value }))} />

等待执行的任务队列容量

setSchedulerForm((f) => ({ ...f, rate_interval: e.target.value }))} />

任务启动间隔,0 表示不限速

) : null} {tab === 'security' ? (

账号安全

setPasswordForm((f) => ({ ...f, username: e.target.value }))} /> setPasswordForm((f) => ({ ...f, old_password: e.target.value }))} /> setPasswordForm((f) => ({ ...f, new_password: e.target.value }))} />
最近登录
{logs.length === 0 ? (
暂无登录日志
) : (
{logs.map((log) => (
{log.username} {log.ip} {log.status}
{log.message} · {log.created_at}
))}
)}
) : null} {tab === 'backup' ? (

备份与恢复

{backupStatus?.has_backup ? `最近备份: ${backupStatus.backup_time}` : '暂无备份'}
) : null} {tab === 'about' ? (

关于

版本: {about?.version || '-'}
远程版本: {about?.remote_version || '-'}
构建时间: {about?.build_time || '-'}
运行时间: {about?.uptime || '-'}
内存: {about?.mem_usage || '-'}
任务/日志/变量: {about?.task_count ?? '-'} / {about?.log_count ?? '-'} / {about?.env_count ?? '-'}

更新日志