fix: cherry-pick PR#94 improvements + dashboard loading fix

- ws-client: connection dedup (_connecting state), connect() guard, global singleton
- chat: 8 null guards (sendMessage/doSend/createStreamBubble/renderAttachments/showPageGuide/loadHistory)
- chat: auto-scroll control (wheel/touch/scrollBtn, disable on scroll-up)
- chat: tool call rendering (appendToolsToEl, collectToolsFromMessage, upsertTool, mergeToolEventData)
- chat: tool event tracking (agent tool events -> _toolEventData/_toolRunIndex)
- chat: extractChatContent/extractContent/dedupeHistory full tools support
- chat.css: .msg-tool collapsible card styles
- dashboard: .catch() on loadDashboardData fire-and-forget, error state + retry button
This commit is contained in:
晴天
2026-03-17 17:03:51 +08:00
parent 22a1fccd8f
commit 604ea3da96
4 changed files with 439 additions and 37 deletions

View File

@@ -38,6 +38,7 @@ export class WsClient {
this._connected = false
this._gatewayReady = false
this._handshaking = false
this._connecting = false
this._intentionalClose = false
this._snapshot = null
this._hello = null
@@ -50,6 +51,7 @@ export class WsClient {
}
get connected() { return this._connected }
get connecting() { return this._connecting }
get gatewayReady() { return this._gatewayReady }
get snapshot() { return this._snapshot }
get hello() { return this._hello }
@@ -72,7 +74,12 @@ export class WsClient {
this._token = token || ''
// 自动检测协议:如果页面通过 HTTPS 加载(反代场景),使用 wss://
const proto = opts.secure ?? (typeof location !== 'undefined' && location.protocol === 'https:') ? 'wss' : 'ws'
this._url = `${proto}://${host}/ws?token=${encodeURIComponent(this._token)}`
const nextUrl = `${proto}://${host}/ws?token=${encodeURIComponent(this._token)}`
if (this._connecting || this._handshaking || this._gatewayReady) {
if (this._url === nextUrl) return
}
if (this._ws && (this._ws.readyState === WebSocket.OPEN || this._ws.readyState === WebSocket.CONNECTING)) return
this._url = nextUrl
this._doConnect()
}
@@ -102,6 +109,7 @@ export class WsClient {
}
_doConnect() {
this._connecting = true
this._closeWs()
this._gatewayReady = false
this._handshaking = false
@@ -113,6 +121,7 @@ export class WsClient {
ws.onopen = () => {
if (wsId !== this._wsId) return
this._connecting = false
this._reconnectAttempts = 0
this._setConnected(true)
this._startPing()
@@ -135,6 +144,7 @@ export class WsClient {
ws.onclose = (e) => {
if (wsId !== this._wsId) return
this._ws = null
this._connecting = false
this._clearChallengeTimer()
if (e.code === 4001 || e.code === 4003 || e.code === 4004) {
this._setConnected(false, 'auth_failed', e.reason || 'Token 认证失败')
@@ -411,4 +421,6 @@ export class WsClient {
}
}
export const wsClient = new WsClient()
const _g = typeof window !== 'undefined' ? window : globalThis
if (!_g.__clawpanelWsClient) _g.__clawpanelWsClient = new WsClient()
export const wsClient = _g.__clawpanelWsClient