From 43cea50df846be0e5b45a9678af82c37bfe3afbf Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Fri, 12 Jun 2026 19:17:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=BC=B9=E7=AA=97=EF=BC=8C=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E9=85=8D=E7=BD=AE=20WebSocket=20=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/App.tsx | 34 +++- web/src/components/Settings/SettingsModal.tsx | 171 ++++++++++++++++++ web/src/hooks/useWebSocket.ts | 23 ++- web/vite.config.ts | 6 +- 4 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 web/src/components/Settings/SettingsModal.tsx diff --git a/web/src/App.tsx b/web/src/App.tsx index f0a3e33..3eb0cea 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,11 +1,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, X, Brain } from 'lucide-react' +import { Zap, ArrowLeft, Bot, Clock, Sun, Moon, PanelRightOpen, X, Brain, Wifi } from 'lucide-react' import { ChatContainer } from './components/Chat/ChatContainer' import { TopicList } from './components/Sidebar/TopicList' import { SchedulerJobList } from './components/Sidebar/SchedulerJobList' import { MemoryPanel } from './components/Panel/MemoryPanel' import { SkillList } from './components/Panel/SkillList' import { TodoPanel } from './components/Panel/TodoPanel' +import { SettingsModal, getGatewaySettings, buildWsUrl, type GatewaySettings } from './components/Settings/SettingsModal' import { ConnectionStatus } from './components/ConnectionStatus' import { ChannelSelector } from './components/Header/ChannelSelector' import { SessionSelector } from './components/Header/SessionSelector' @@ -13,9 +14,13 @@ import { useWebSocket } from './hooks/useWebSocket' import { useChat } from './hooks/useChat' import type { ChatMessage, Command, Attachment, SchedulerJobSessionLookup } from './types/protocol' -const WS_URL = 'ws://127.0.0.1:19876/ws' +function getInitialSettings(): GatewaySettings { + return getGatewaySettings() +} function App() { + const [gatewaySettings, setGatewaySettings] = useState(getInitialSettings) + const wsUrl = buildWsUrl(gatewaySettings) const lastAutoSwitchedTopicRef = useRef(null) const { @@ -81,7 +86,7 @@ function App() { } = useChat() const { status, sendMessage } = useWebSocket({ - url: WS_URL, + url: wsUrl, onMessage: handleServerMessage, }) @@ -128,6 +133,13 @@ function App() { return localStorage.getItem('picobot-show-thinking') !== 'false' }) + const [settingsOpen, setSettingsOpen] = useState(false) + + const handleSaveGatewaySettings = useCallback((newSettings: GatewaySettings) => { + setGatewaySettings(newSettings) + setSettingsOpen(false) + }, []) + useEffect(() => { localStorage.setItem('picobot-show-thinking', String(showThinking)) }, [showThinking]) @@ -481,6 +493,14 @@ function App() { > +
+ + {/* 连接设置弹窗 */} + {settingsOpen && ( + setSettingsOpen(false)} + onSave={handleSaveGatewaySettings} + /> + )}
) } diff --git a/web/src/components/Settings/SettingsModal.tsx b/web/src/components/Settings/SettingsModal.tsx new file mode 100644 index 0000000..e226fe6 --- /dev/null +++ b/web/src/components/Settings/SettingsModal.tsx @@ -0,0 +1,171 @@ +import { useState, useEffect } from 'react' +import { X, Wifi, RotateCcw } from 'lucide-react' + +export interface GatewaySettings { + host: string + port: number +} + +const DEFAULT_HOST = '127.0.0.1' +const DEFAULT_PORT = 19876 + +export function getGatewaySettings(): GatewaySettings { + try { + const host = localStorage.getItem('picobot-gateway-host') || DEFAULT_HOST + const portStr = localStorage.getItem('picobot-gateway-port') + const port = portStr ? parseInt(portStr, 10) : DEFAULT_PORT + return { host, port: isNaN(port) ? DEFAULT_PORT : port } + } catch { + return { host: DEFAULT_HOST, port: DEFAULT_PORT } + } +} + +export function buildWsUrl(settings: GatewaySettings): string { + return `ws://${settings.host}:${settings.port}/ws` +} + +interface SettingsModalProps { + onClose: () => void + onSave: (settings: GatewaySettings) => void +} + +export function SettingsModal({ onClose, onSave }: SettingsModalProps) { + const [host, setHost] = useState(DEFAULT_HOST) + const [port, setPort] = useState(String(DEFAULT_PORT)) + const [error, setError] = useState('') + + useEffect(() => { + const settings = getGatewaySettings() + setHost(settings.host) + setPort(String(settings.port)) + }, []) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose() + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, [onClose]) + + const handleSave = () => { + const trimmedHost = host.trim() + const portNum = parseInt(port, 10) + + if (!trimmedHost) { + setError('主机地址不能为空') + return + } + if (isNaN(portNum) || portNum < 1 || portNum > 65535) { + setError('端口号必须在 1-65535 之间') + return + } + + setError('') + localStorage.setItem('picobot-gateway-host', trimmedHost) + localStorage.setItem('picobot-gateway-port', String(portNum)) + onSave({ host: trimmedHost, port: portNum }) + } + + const handleReset = () => { + setHost(DEFAULT_HOST) + setPort(String(DEFAULT_PORT)) + setError('') + } + + return ( +
+ {/* Modal container */} +
e.stopPropagation()} + > + {/* Header */} +
+ + 连接设置 + +
+ + {/* Body */} +
+ {/* Host */} +
+ + { setHost(e.target.value); setError('') }} + placeholder="127.0.0.1" + className="w-full px-3 py-2.5 rounded-xl bg-[var(--bg-tertiary)] border border-[var(--border-color)] text-[var(--text-primary)] text-sm placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--accent-cyan)] focus:ring-2 focus:ring-[var(--focus-ring)] transition-colors" + /> +
+ + {/* Port */} +
+ + { setPort(e.target.value); setError('') }} + placeholder="19876" + min={1} + max={65535} + className="w-full px-3 py-2.5 rounded-xl bg-[var(--bg-tertiary)] border border-[var(--border-color)] text-[var(--text-primary)] text-sm placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--accent-cyan)] focus:ring-2 focus:ring-[var(--focus-ring)] transition-colors" + /> +
+ + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Current URL preview */} +
+ ws://{host.trim() || '...'}:{port || '...'}/ws +
+
+ + {/* Footer */} +
+ +
+ + +
+
+
+ ) +} diff --git a/web/src/hooks/useWebSocket.ts b/web/src/hooks/useWebSocket.ts index 2433d99..88ce2c4 100644 --- a/web/src/hooks/useWebSocket.ts +++ b/web/src/hooks/useWebSocket.ts @@ -108,15 +108,28 @@ export function useWebSocket({ return false }, []) - // Auto connect on mount - useEffect(() => { - connect() + // Auto connect on mount, reconnect when url changes + const prevUrlRef = useRef(url) + + useEffect(() => { + // 首次挂载,或者 url 发生了变化,都要重连 + if (prevUrlRef.current !== url) { + console.log(`WebSocket URL changed: ${prevUrlRef.current} → ${url}`) + // 先断开旧连接 + disconnect() + prevUrlRef.current = url + } + + // 短暂延迟确保旧 socket 已关闭 + const timer = setTimeout(() => { + connect() + }, 50) - // Cleanup on unmount return () => { + clearTimeout(timer) disconnect() } - }, [connect, disconnect]) + }, [url, connect, disconnect]) return { status, diff --git a/web/vite.config.ts b/web/vite.config.ts index 64ad4eb..4a1b2a5 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -2,6 +2,8 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { resolve } from 'path' +const gatewayPort = process.env.VITE_GATEWAY_PORT || '19876' + // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], @@ -14,10 +16,10 @@ export default defineConfig({ port: 3000, proxy: { '/ws': { - target: 'ws://127.0.0.1:19876', + target: `ws://127.0.0.1:${gatewayPort}`, ws: true, }, - '/health': 'http://127.0.0.1:19876', + '/health': `http://127.0.0.1:${gatewayPort}`, }, }, build: {