refactor: restructure plugins per MTSS standard, consolidate assets to res/
Per docs/plugin-audit-report.md (plugins normalized to Trigger(handlers) → Service(services) → Model(repository/models) + utils): - Split monolithic __init__.py into handlers/services/utils across dailywife, deer_pipe, dice, galgame_card, helldivers_tools, huoziyinshua, learning_chat, makeaquote, mc_server_status, ncm_saying, picfinder_take, picstatus, random_jm_code, regif, steam_info, video_analysis, group_tools - Move static assets under res/: deer_pipe font/img, makeaquote font, helldivers img/templates, huoziyinshua HuoZiYinShua - Add config.py + register_config_items to ncm_saying, random_jm_code, group_tools; learning_chat unified config bridge - Remove deprecated: voice_trans plugin, bf_bot/test.py, dead code in dailywife/deer_pipe, empty dirs, debug scripts under helldivers temp - Disable brash_general_supercredits_tools (stub comment only) - bot.py: optional stdout/stderr redirect to log file for Web log viewer, force ANSI colorize on non-TTY sinks - Move runtime data (jm_code.json) out of plugin dir into hexi/data - Docs: plugin-audit-report.md; README reflects removed plugins Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Button, Card, Form, Input, Label, TextField, toast } from '@heroui/react'
|
||||
import { hubSettings, hubChangePassword } from '../../api/client'
|
||||
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, hubDashboardStream,
|
||||
} from '../../api/client'
|
||||
|
||||
interface SettingsData {
|
||||
ok: boolean
|
||||
@@ -8,6 +12,21 @@ interface SettingsData {
|
||||
plugins: { id: string; name: string; icon: string }[]
|
||||
version: string
|
||||
}
|
||||
interface BotConfigData {
|
||||
ok: boolean
|
||||
superusers: string[]
|
||||
nickname: string[]
|
||||
debug: boolean
|
||||
log_level: string
|
||||
log_level_options: string[]
|
||||
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<SettingsData | null>(null)
|
||||
@@ -16,11 +35,47 @@ export default function SettingsPage() {
|
||||
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 | 'stop' | 'restart'>(null)
|
||||
const botState = useOverlayState()
|
||||
const pollRef = useRef(0)
|
||||
const restartDisposeRef = useRef<(() => void) | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
hubSettings()
|
||||
.then(setData)
|
||||
.catch((e: any) => setError(e.message || '加载失败'))
|
||||
const [botCfg, setBotCfg] = useState<BotConfigData | null>(null)
|
||||
const [cfgSuperusers, setCfgSuperusers] = useState('')
|
||||
const [cfgNickname, setCfgNickname] = useState('')
|
||||
const [cfgDebug, setCfgDebug] = useState(false)
|
||||
const [cfgLogLevel, setCfgLogLevel] = useState('INFO')
|
||||
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')
|
||||
setCfgLoaded(true)
|
||||
} catch (e: any) {
|
||||
setCfgLoaded(false)
|
||||
console.warn('读取 bot 配置失败', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
useEffect(() => () => {
|
||||
if (restartDisposeRef.current) restartDisposeRef.current()
|
||||
}, [])
|
||||
|
||||
const changePassword = async () => {
|
||||
@@ -37,53 +92,154 @@ export default function SettingsPage() {
|
||||
} finally { setBusy(false) }
|
||||
}
|
||||
|
||||
const saveBotConfig = async () => {
|
||||
setCfgBusy(true)
|
||||
try {
|
||||
const values = {
|
||||
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 r = await hubBotConfigSave(values)
|
||||
setBotCfg(r)
|
||||
setCfgSuperusers((r.superusers || []).join(', '))
|
||||
setCfgNickname((r.nickname || []).join(', '))
|
||||
setCfgDebug(!!r.debug)
|
||||
setCfgLogLevel(r.log_level || 'INFO')
|
||||
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 = async () => {
|
||||
const id = ++pollRef.current
|
||||
// 先等旧进程完全退出(旧的 dashboard SSE 可能仍被旧进程服务,提前收到数据会误判就绪)
|
||||
await new Promise(res => setTimeout(res, 4000))
|
||||
if (pollRef.current !== id) return
|
||||
if (restartDisposeRef.current) restartDisposeRef.current()
|
||||
// 以 dashboard SSE 真正收到数据作为「后端已就绪」信号,避免仅 HTTP 起来但插件未挂接就放行
|
||||
const dispose = hubDashboardStream({
|
||||
onData: (d) => {
|
||||
if (pollRef.current !== id) return
|
||||
if (d && d.ok !== false) {
|
||||
restartDisposeRef.current = null
|
||||
dispose()
|
||||
setRestarting(null)
|
||||
toast.success('bot 已重启并回连')
|
||||
load()
|
||||
}
|
||||
},
|
||||
})
|
||||
restartDisposeRef.current = dispose
|
||||
window.setTimeout(() => {
|
||||
if (pollRef.current === id) {
|
||||
restartDisposeRef.current = null
|
||||
dispose()
|
||||
setRestarting(null)
|
||||
toast.danger('等待回连超时,请检查 bot 是否已启动')
|
||||
}
|
||||
}, 60000)
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-xl font-semibold text-slate-800">设置</h2>
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
<Card className="border border-slate-200 bg-white">
|
||||
<Card.Header><Card.Title>账号信息</Card.Title></Card.Header>
|
||||
<Card.Content>
|
||||
{error ? (
|
||||
<p className="text-red-500 text-sm">{error}</p>
|
||||
) : !data ? (
|
||||
<p className="text-slate-400 text-sm">加载中…</p>
|
||||
) : (
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex justify-between border-b border-slate-100 pb-2">
|
||||
<span className="text-slate-500">用户名</span>
|
||||
<span className="font-medium text-slate-800">{data.user.username}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-slate-100 pb-2">
|
||||
<span className="text-slate-500">用户 ID</span>
|
||||
<span className="font-medium text-slate-800">{data.user.id}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-slate-100 pb-2">
|
||||
<span className="text-slate-500">创建时间</span>
|
||||
<span className="text-slate-800">{data.user.created_at || '—'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between border-b border-slate-100 pb-2">
|
||||
<span className="text-slate-500">已接入插件</span>
|
||||
<span className="text-slate-800">{data.plugins.length} 个</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-slate-200 bg-white">
|
||||
<Card className="border border-slate-200 bg-white lg:col-span-2">
|
||||
<Card.Header><Card.Title>修改密码</Card.Title></Card.Header>
|
||||
<Card.Content>
|
||||
<Form className="space-y-4">
|
||||
<TextField><Label>原密码</Label><Input type="password" value={oldPassword} onChange={e => setOldPassword(e.target.value)} /></TextField>
|
||||
<TextField><Label>新密码</Label><Input type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} placeholder="至少 6 位" /></TextField>
|
||||
<TextField><Label>确认新密码</Label><Input type="password" value={confirm} onChange={e => setConfirm(e.target.value)} /></TextField>
|
||||
<Button isDisabled={busy} onPress={changePassword}>{busy ? '修改中…' : '保存修改'}</Button>
|
||||
<Button isDisabled={busy || !!restarting} onPress={changePassword}>{busy ? '修改中…' : '保存修改'}</Button>
|
||||
</Form>
|
||||
<p className="text-xs text-slate-400 mt-3">修改后旧登录态失效,当前会话保持在线。</p>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
{/* BOT 全局配置 */}
|
||||
<Card className="border border-slate-200 bg-white lg:col-span-2">
|
||||
<Card.Header><Card.Title>BOT 配置</Card.Title></Card.Header>
|
||||
<Card.Content>
|
||||
<p className="text-xs text-slate-400 mb-4">
|
||||
SUPERUSERS / NICKNAME 支持逗号或空格分隔;保存后会写入 .env 并尽量热应用。
|
||||
部分插件在启动时缓存了这些值,完全生效建议保存后重启 bot。
|
||||
</p>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<TextField>
|
||||
<Label>SUPERUSERS</Label>
|
||||
<Input value={cfgSuperusers} onChange={e => setCfgSuperusers(e.target.value)} placeholder="如: 123456789, 987654321" />
|
||||
<span className="text-xs text-slate-400">超级用户 QQ 号(逗号/空格分隔)</span>
|
||||
</TextField>
|
||||
<TextField>
|
||||
<Label>NICKNAME</Label>
|
||||
<Input value={cfgNickname} onChange={e => setCfgNickname(e.target.value)} placeholder="如: 爱丽丝" />
|
||||
<span className="text-xs text-slate-400">机器人昵称(逗号/空格分隔)</span>
|
||||
</TextField>
|
||||
<div className="flex items-center justify-between rounded-xl border border-slate-200 px-4 py-3">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-slate-700">DEBUG</div>
|
||||
<div className="text-xs text-slate-400">开启调试模式</div>
|
||||
</div>
|
||||
<Switch isSelected={cfgDebug} onChange={(v) => setCfgDebug(v)}>
|
||||
<Switch.Content><Switch.Control><Switch.Thumb /></Switch.Control>{cfgDebug ? '开' : '关'}</Switch.Content>
|
||||
</Switch>
|
||||
</div>
|
||||
<div>
|
||||
<Label>LOG_LEVEL</Label>
|
||||
<Select selectedKey={cfgLogLevel} onSelectionChange={(k) => setCfgLogLevel(String(k))}>
|
||||
<Select.Trigger className={selectCls}><Select.Value /></Select.Trigger>
|
||||
<Select.Popover><ListBox>
|
||||
{(botCfg?.log_level_options || LOG_LEVELS).map(o => <ListBox.Item key={o} id={o}>{o}</ListBox.Item>)}
|
||||
</ListBox></Select.Popover>
|
||||
</Select>
|
||||
<span className="text-xs text-slate-400">日志等级(INFO 为默认)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<Button isDisabled={cfgBusy || !cfgLoaded || !!restarting} onPress={saveBotConfig}>{cfgBusy ? '保存中…' : '保存 BOT 配置'}</Button>
|
||||
{!cfgLoaded && <span className="text-xs text-slate-400">配置未加载</span>}
|
||||
{botCfg?.source === 'env' && <span className="text-xs text-slate-400">当前来源:.env</span>}
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-slate-200 bg-white lg:col-span-2">
|
||||
<Card.Header><Card.Title>进程控制</Card.Title></Card.Header>
|
||||
<Card.Content className="space-y-3">
|
||||
<p className="text-sm text-slate-500">停止 / 重启当前 bot 进程。停止会断开 Web 与 QQ 连接;重启会先拉起新进程再退出旧进程,日志继续写入 _bot_run.log。重启过程中界面会禁用操作并等待回连。</p>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<Button variant="tertiary" className="text-red-500" isDisabled={botBusy || !!restarting} onPress={() => askBotAction('stop')}>停止 bot</Button>
|
||||
<Button isDisabled={botBusy || !!restarting} onPress={() => askBotAction('restart')}>重启 bot</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-slate-200 bg-white lg:col-span-2">
|
||||
<Card.Header><Card.Title>关于</Card.Title></Card.Header>
|
||||
<Card.Content>
|
||||
@@ -91,6 +247,38 @@ export default function SettingsPage() {
|
||||
</Card.Content>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Modal isOpen={botState.isOpen} onOpenChange={botState.setOpen}>
|
||||
<Modal.Backdrop><Modal.Container><Modal.Dialog>
|
||||
<Modal.Header><Modal.Heading>{pendingAction === 'stop' ? '停止 bot' : '重启 bot'}</Modal.Heading></Modal.Header>
|
||||
<Modal.Body><p className="text-sm text-slate-600">
|
||||
{pendingAction === 'stop' ? '确定停止当前 bot 进程吗?Web 管理台与 QQ 连接将断开。' : '确定重启 bot 进程吗?将先拉起新进程再退出旧进程。'}
|
||||
</p></Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="tertiary" onPress={botState.close}>取消</Button>
|
||||
<Button isDisabled={botBusy} onPress={confirmBotAction}>{botBusy ? '执行中…' : '确定'}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal.Dialog></Modal.Container></Modal.Backdrop>
|
||||
</Modal>
|
||||
|
||||
{restarting === 'stop' && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/60 p-4">
|
||||
<Card className="border border-slate-200 bg-white"><Card.Content className="space-y-3 text-center">
|
||||
<div className="text-sm font-medium text-slate-800">正在停止 bot…</div>
|
||||
<p className="text-xs text-slate-400">Web 与 QQ 连接已断开,请在服务器重新启动 bot。</p>
|
||||
<Button size="sm" variant="tertiary" onPress={() => setRestarting(null)}>知道了</Button>
|
||||
</Card.Content></Card>
|
||||
</div>
|
||||
)}
|
||||
{restarting === 'restart' && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/60 p-4">
|
||||
<Card className="border border-slate-200 bg-white"><Card.Content className="space-y-3 text-center">
|
||||
<div className="text-sm font-medium text-slate-800">正在重启 bot,等待回连…</div>
|
||||
<p className="text-xs text-slate-400">期间已禁用所有操作,恢复后自动继续。</p>
|
||||
<div className="flex justify-center"><Spinner /></div>
|
||||
</Card.Content></Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user