feat(web): 顶栏账号菜单接入 bot 启停 / 插件页分区 / 首页协议端配对信息

- 重启/关闭 bot 从设置页提到顶栏账号下拉(破坏性操作弹确认框),等待回连逻辑抽成
  lib/botAction.ts 供两处共用:先等旧进程让出端口,再以 dashboard SSE 真收到一帧为准,
  只看 HTTP 200 会在"端口起来但插件未挂接"时误判就绪
- 插件管理页按 带 Web 页面 / 带配置项 / 都没有 分区,卡片加类型与 Web/配置 徽章
- 首页「BOT 协议端信息」分两半:左半登录账号,右半 NoneBot 接入信息
  (IP / 运行端口 / WS 路径 / 访问令牌,均可一键复制,另给整条反向 WS 地址),
  监听地址非 0.0.0.0 时提示仅本机可连;「BOT 后端信息」移到自己一行
- dashboard.py 新增 endpoint 段:读 driver.config 的 host/port、驱动实际注册的
  反向 WS 路由(自动跳过根路径与尾斜杠别名)、适配器令牌与本机网卡 IP

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-22 14:23:52 +08:00
co-authored by Claude Code
parent 814c1994b6
commit 4b5e8b8b2e
7 changed files with 546 additions and 194 deletions
+17 -42
View File
@@ -3,8 +3,9 @@ import {
Button, Card, Form, Input, Label, ListBox, Modal, Select, Spinner, Switch, TextField, toast, useOverlayState,
} from '@heroui/react'
import {
hubSettings, hubChangePassword, hubBotAction, hubBotConfig, hubBotConfigSave, hubDashboardStream,
hubSettings, hubChangePassword, hubBotAction, hubBotConfig, hubBotConfigSave,
} from '../../api/client'
import { watchBotReconnect } from '../../lib/botAction'
interface SettingsData {
ok: boolean
@@ -39,8 +40,6 @@ export default function SettingsPage() {
const [pendingAction, setPendingAction] = useState<'stop' | 'restart' | null>(null)
const [restarting, setRestarting] = useState<null | 'stop' | 'restart'>(null)
const botState = useOverlayState()
const pollRef = useRef(0)
const pollTimerRef = useRef<number | null>(null)
const restartDisposeRef = useRef<(() => void) | null>(null)
const [botCfg, setBotCfg] = useState<BotConfigData | null>(null)
@@ -77,10 +76,8 @@ export default function SettingsPage() {
useEffect(() => { load() }, [load])
useEffect(() => () => {
if (restartDisposeRef.current) restartDisposeRef.current()
if (pollTimerRef.current != null) window.clearTimeout(pollTimerRef.current)
}, [])
// dispose 内部会一并撤掉 SSE 与超时定时器
useEffect(() => () => { restartDisposeRef.current?.() }, [])
const changePassword = async () => {
if (!oldPassword) { toast.warning('请输入原密码'); return }
@@ -130,44 +127,22 @@ export default function SettingsPage() {
botState.open()
}
const clearPollTimer = () => {
if (pollTimerRef.current != null) {
window.clearTimeout(pollTimerRef.current)
pollTimerRef.current = null
}
}
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()
clearPollTimer()
pollRef.current += 1 // 让旧定时器即使未被清除也不会误判超时
setRestarting(null)
toast.success('bot 已重启并回连')
load()
}
},
})
restartDisposeRef.current = dispose
pollTimerRef.current = window.setTimeout(() => {
pollTimerRef.current = null
if (pollRef.current === id) {
const pollForReconnect = () => {
// 等待逻辑与顶栏账号菜单共用一份(lib/botAction.ts),别在两边各写一套
restartDisposeRef.current?.()
restartDisposeRef.current = watchBotReconnect({
onReady: () => {
restartDisposeRef.current = null
setRestarting(null)
toast.success('bot 已重启并回连')
load()
},
onTimeout: () => {
restartDisposeRef.current = null
dispose()
setRestarting(null)
toast.danger('等待回连超时,请检查 bot 是否已启动')
}
}, 60000)
},
})
}
const confirmBotAction = async () => {