import { useCallback, useEffect, useRef, useState } from 'react' import { Button, Card, Form, Input, Label, ListBox, Modal, Select, Spinner, Switch, TextField, toast, useOverlayState, } from '@heroui/react' import { hubSettings, hubChangePassword, hubBotAction, hubBotConfig, hubBotConfigSave, } from '../../api/client' import { watchBotReconnect } from '../../lib/botAction' interface SettingsData { ok: boolean version: string } interface BotConfigData { ok: boolean superusers: string[] nickname: string[] debug: boolean log_level: string log_level_options: string[] log_max_mb: number log_max_mb_default: number source: string notice?: string applied?: string[] restart_required?: boolean } const LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] const selectCls = 'min-w-[150px] bg-white border border-slate-200 rounded-lg px-3 py-2 text-sm text-slate-800 focus:border-indigo-400 focus:outline-none' export default function SettingsPage() { const [data, setData] = useState(null) const [error, setError] = useState('') const [oldPassword, setOldPassword] = useState('') const [newPassword, setNewPassword] = useState('') const [confirm, setConfirm] = useState('') const [busy, setBusy] = useState(false) const [botBusy, setBotBusy] = useState(false) const [pendingAction, setPendingAction] = useState<'stop' | 'restart' | null>(null) const [restarting, setRestarting] = useState(null) const botState = useOverlayState() const restartDisposeRef = useRef<(() => void) | null>(null) const [botCfg, setBotCfg] = useState(null) const [cfgSuperusers, setCfgSuperusers] = useState('') const [cfgNickname, setCfgNickname] = useState('') const [cfgDebug, setCfgDebug] = useState(false) const [cfgLogLevel, setCfgLogLevel] = useState('INFO') const [cfgLogMaxMb, setCfgLogMaxMb] = useState('20') const [cfgBusy, setCfgBusy] = useState(false) const [cfgLoaded, setCfgLoaded] = useState(false) const load = useCallback(async () => { try { const s = await hubSettings() setData(s) setError('') } catch (e: any) { setError(e.message || '加载失败') } try { const c = await hubBotConfig() setBotCfg(c) setCfgSuperusers((c.superusers || []).join(', ')) setCfgNickname((c.nickname || []).join(', ')) setCfgDebug(!!c.debug) setCfgLogLevel(c.log_level || 'INFO') setCfgLogMaxMb(String(c.log_max_mb ?? c.log_max_mb_default ?? 20)) setCfgLoaded(true) } catch (e: any) { setCfgLoaded(false) console.warn('读取 bot 配置失败', e) } }, []) useEffect(() => { load() }, [load]) // dispose 内部会一并撤掉 SSE 与超时定时器 useEffect(() => () => { restartDisposeRef.current?.() }, []) const changePassword = async () => { if (!oldPassword) { toast.warning('请输入原密码'); return } if (newPassword.length < 6) { toast.warning('新密码长度不能少于 6 位'); return } if (newPassword !== confirm) { toast.warning('两次输入的新密码不一致'); return } setBusy(true) try { await hubChangePassword(oldPassword, newPassword) toast.success('密码已修改') setOldPassword(''); setNewPassword(''); setConfirm('') } catch (e: any) { toast.danger(e.message || '修改失败') } finally { setBusy(false) } } const saveBotConfig = async () => { setCfgBusy(true) try { const values: Record = { superusers: (cfgSuperusers || '').split(/[,\s]+/).map(s => s.trim()).filter(Boolean), nickname: (cfgNickname || '').split(/[,\s]+/).map(s => s.trim()).filter(Boolean), debug: cfgDebug, log_level: cfgLogLevel, } const logMaxMbRaw = cfgLogMaxMb.trim() if (logMaxMbRaw !== '') { const logMaxMb = parseInt(logMaxMbRaw, 10) if (!(logMaxMb > 0)) { toast.warning('日志清理大小需为正整数(MB)'); setCfgBusy(false); return } values.log_max_mb = logMaxMb } const r = await hubBotConfigSave(values) setBotCfg(r) setCfgSuperusers((r.superusers || []).join(', ')) setCfgNickname((r.nickname || []).join(', ')) setCfgDebug(!!r.debug) setCfgLogLevel(r.log_level || 'INFO') setCfgLogMaxMb(String(r.log_max_mb ?? r.log_max_mb_default ?? 20)) toast.success('BOT 配置已保存') if (r.notice) toast.info(r.notice) } catch (e: any) { toast.danger(e.message || '保存失败') } finally { setCfgBusy(false) } } const askBotAction = (action: 'stop' | 'restart') => { setPendingAction(action) botState.open() } const pollForReconnect = () => { // 等待逻辑与顶栏账号菜单共用一份(lib/botAction.ts),别在两边各写一套 restartDisposeRef.current?.() restartDisposeRef.current = watchBotReconnect({ onReady: () => { restartDisposeRef.current = null setRestarting(null) toast.success('bot 已重启并回连') load() }, onTimeout: () => { restartDisposeRef.current = null setRestarting(null) toast.danger('等待回连超时,请检查 bot 是否已启动') }, }) } const confirmBotAction = async () => { if (!pendingAction) return const action = pendingAction setBotBusy(true) try { const r = await hubBotAction(action) toast.success((r as any)?.msg || `已请求${action === 'stop' ? '停止' : '重启'} bot`) botState.close() setPendingAction(null) setRestarting(action) if (action === 'restart') pollForReconnect() } catch (e: any) { toast.danger(e.message || '操作失败') } finally { setBotBusy(false) } } return (

设置

修改密码
setOldPassword(e.target.value)} /> setNewPassword(e.target.value)} placeholder="至少 6 位" /> setConfirm(e.target.value)} />

修改后旧登录态失效,当前会话保持在线。

{/* BOT 全局配置 */} BOT 配置

SUPERUSERS / NICKNAME 支持逗号或空格分隔;保存后会写入 .env 并尽量热应用。 部分插件在启动时缓存了这些值,完全生效建议保存后重启 bot。 日志清理大小保存后仅在下一个检查周期(约 1 分钟)内生效,无需重启。

setCfgSuperusers(e.target.value)} placeholder="如: 123456789, 987654321" /> 超级用户 QQ 号(逗号/空格分隔) setCfgNickname(e.target.value)} placeholder="如: 爱丽丝" /> 机器人昵称(逗号/空格分隔)
DEBUG
开启调试模式
setCfgDebug(v)}> {cfgDebug ? '开' : '关'}
日志等级(INFO 为默认)
setCfgLogMaxMb(e.target.value)} placeholder="如: 20" /> _bot_run.log 超过该大小即清空(不归档)
{!cfgLoaded && 配置未加载} {botCfg?.source === 'env' && 当前来源:.env}
进程控制

停止 / 重启当前 bot 进程。停止会断开 Web 与 QQ 连接;重启会先拉起新进程再退出旧进程,日志继续写入 _bot_run.log。重启过程中界面会禁用操作并等待回连。

关于

HeXi 管理系统 v{data?.version || '—'} · 统一 Web 管理台

{pendingAction === 'stop' ? '停止 bot' : '重启 bot'}

{pendingAction === 'stop' ? '确定停止当前 bot 进程吗?Web 管理台与 QQ 连接将断开。' : '确定重启 bot 进程吗?将先拉起新进程再退出旧进程。'}

{restarting === 'stop' && (
正在停止 bot…

Web 与 QQ 连接已断开,请在服务器重新启动 bot。

)} {restarting === 'restart' && (
正在重启 bot,等待回连…

期间已禁用所有操作,恢复后自动继续。

)}
) }