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:
2026-09-03 00:44:38 +08:00
co-authored by Claude
parent 25bc30611f
commit 9371a28e35
2389 changed files with 5100 additions and 10127 deletions
+227 -39
View File
@@ -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>
)
}