feat(web): 停止/重启 bot 的等待相位全局化 / 覆盖层收进 BotActionProvider
重启入口有两个 —— 顶栏账号菜单(任何页面可点)和设置页「进程控制」。以前两边 各写一套:设置页只有本页的 fixed 覆盖层,顶栏只弹个 toast。于是从插件页/日志页 点「重启 bot」时界面照常可点,请求全打在已经退出的旧进程上。 - 新增 src/botAction.tsx:Provider 持 phase(null / stop / restart)与 readyCount, 覆盖层由 AppLayout 在 <Outlet/> 之外统一渲染,任何页面触发都盖得住; 纯等待逻辑仍在 lib/botAction.ts,两边共用一份 - 设置页删掉本地的 restarting 态与两份覆盖层,只负责发起 + 用 phase 禁用按钮; 改为消费 readyCount 在回连后重新拉数据(停机期间配置是旧的) - 未挂载 Provider 时 run() 直接抛错,避免静默无覆盖层 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
1 parent
574216645c
commit
023dc1cb5d
5 files changed
+137
-73
No files matched your search
@@ -1,11 +1,11 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import {
|
||||
Button, Card, Form, Input, Label, ListBox, Modal, Select, Spinner, Switch, TextField, toast, useOverlayState,
|
||||
Button, Card, Form, Input, Label, ListBox, Modal, Select, Switch, TextField, toast, useOverlayState,
|
||||
} from '@heroui/react'
|
||||
import {
|
||||
hubSettings, hubChangePassword, hubBotAction, hubBotConfig, hubBotConfigSave,
|
||||
hubSettings, hubChangePassword, hubBotConfig, hubBotConfigSave,
|
||||
} from '../../api/client'
|
||||
import { watchBotReconnect } from '../../lib/botAction'
|
||||
import { useBotAction } from '../../botAction'
|
||||
|
||||
interface SettingsData {
|
||||
ok: boolean
|
||||
@@ -38,9 +38,10 @@ export default function SettingsPage() {
|
||||
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 restartDisposeRef = useRef<(() => void) | null>(null)
|
||||
// 停止/重启的等待相位与覆盖层是全局的(见 src/botAction.tsx):本页只负责
|
||||
// 发起 + 用 phase 禁用自家按钮,覆盖层由 AppLayout 统一渲染。
|
||||
const { phase: botPhase, readyCount, run: runBotAction } = useBotAction()
|
||||
|
||||
const [botCfg, setBotCfg] = useState<BotConfigData | null>(null)
|
||||
const [cfgSuperusers, setCfgSuperusers] = useState('')
|
||||
@@ -76,8 +77,9 @@ export default function SettingsPage() {
|
||||
|
||||
useEffect(() => { load() }, [load])
|
||||
|
||||
// dispose 内部会一并撤掉 SSE 与超时定时器
|
||||
useEffect(() => () => { restartDisposeRef.current?.() }, [])
|
||||
// 重启回连后重新拉一遍:停机期间的数据是旧的
|
||||
// (readyCount 由全局 provider 在回连成功时自增;Suspense 重挂不会误触发)
|
||||
useEffect(() => { if (readyCount > 0) load() }, [readyCount, load])
|
||||
|
||||
const changePassword = async () => {
|
||||
if (!oldPassword) { toast.warning('请输入原密码'); return }
|
||||
@@ -127,35 +129,14 @@ export default function SettingsPage() {
|
||||
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`)
|
||||
await runBotAction(action)
|
||||
botState.close()
|
||||
setPendingAction(null)
|
||||
setRestarting(action)
|
||||
if (action === 'restart') pollForReconnect()
|
||||
} catch (e: any) {
|
||||
toast.danger(e.message || '操作失败')
|
||||
} finally { setBotBusy(false) }
|
||||
@@ -172,7 +153,7 @@ export default function SettingsPage() {
|
||||
<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 || !!restarting} onPress={changePassword}>{busy ? '修改中…' : '保存修改'}</Button>
|
||||
<Button isDisabled={busy || !!botPhase} onPress={changePassword}>{busy ? '修改中…' : '保存修改'}</Button>
|
||||
</Form>
|
||||
<p className="text-xs text-slate-400 mt-3">修改后旧登录态失效,当前会话保持在线。</p>
|
||||
</Card.Content>
|
||||
@@ -224,7 +205,7 @@ export default function SettingsPage() {
|
||||
</TextField>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<Button isDisabled={cfgBusy || !cfgLoaded || !!restarting} onPress={saveBotConfig}>{cfgBusy ? '保存中…' : '保存 BOT 配置'}</Button>
|
||||
<Button isDisabled={cfgBusy || !cfgLoaded || !!botPhase} 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>
|
||||
@@ -236,8 +217,8 @@ export default function SettingsPage() {
|
||||
<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>
|
||||
<Button variant="tertiary" className="text-red-500" isDisabled={botBusy || !!botPhase} onPress={() => askBotAction('stop')}>停止 bot</Button>
|
||||
<Button isDisabled={botBusy || !!botPhase} onPress={() => askBotAction('restart')}>重启 bot</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
@@ -262,25 +243,6 @@ export default function SettingsPage() {
|
||||
</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