mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-06-01 05:40:07 +08:00
feat(gateway): surface negotiated handshake protocol version in UI
Users have reported confusion about "when will ClawPanel update its gateway protocol to v4". This is actually a misreading: ClawPanel v0.15+ already advertises `minProtocol=3, maxProtocol=4` in its connect frame, and negotiates v4 transparently when the kernel is >= 2026.5.12. The `v3|` prefix users were seeing in dev-api.js is the device signature payload string schema version, which is a completely separate concept from the handshake protocol version. Make this visible and unambiguous: UI - Add a "Proto v4" badge next to the Gateway service name in /services once the WS handshake succeeds, with a tooltip explaining that this is the WS handshake protocol version (not the device signature payload v3 format). - Add the same protocol info to the WebSocket row in /chat-debug. API - WsClient now exposes `negotiatedProtocol` which prefers the explicit field from the hello payload (`protocol` / `protocolVersion` / `negotiatedProtocol`) and falls back to inferring from serverVersion: kernels >= 2026.5.12 are reported as v4, older as v3. This matches the panel's advertised range of [3, 4]. - KernelSnapshot grows a `protocol` field so feature gates and UIs that already consume the snapshot can read it without touching wsClient. Comments - Expand the KERNEL_TARGET comment in feature-catalog.js to spell out the two-distinct-version-numbers rule explicitly. - Add matching clarifying comments next to the `v3|...` payload string in both scripts/dev-api.js and src-tauri/src/commands/device.rs, so the next reader does not confuse payload schema with handshake. ## Verification - node --check on all touched JS files - npm run build - cargo fmt --check && cargo check (clippy errors that surface are pre-existing debt in config.rs, untouched here) - Playwright /services: mock wsClient state, observe `协议 v4` badge rendered with `rgba(99, 102, 241, 0.1)` background and accent color, for both the explicit-protocol path and the version-inferred path.
This commit is contained in:
@@ -190,7 +190,12 @@ export const KERNEL_FLOOR = {
|
||||
export const KERNEL_TARGET = {
|
||||
openclaw: {
|
||||
// 内核协议在 5.12 升级到 v4(MIN_CLIENT_PROTOCOL_VERSION=4,新增增量 chat delta payloads),
|
||||
// 面板通过 connect frame `[minProtocol=3, maxProtocol=4]` 同时兼容新旧内核
|
||||
// 面板通过 connect frame `[minProtocol=3, maxProtocol=4]` 同时兼容新旧内核。
|
||||
//
|
||||
// ⚠️ 警告:这里的"协议 v3/v4"指 **Gateway WebSocket 握手帧协议版本**。
|
||||
// 不要与 dev-api.js 中设备签名 payload 字符串前缀 `v3|deviceId|...` 混淆——
|
||||
// 后者是 **device signature payload 字符串格式版本**,两者完全独立、互不相关。
|
||||
// 即使在 v4 握手协议下,签名 payload 字符串仍以 `v3|` 开头(这是 payload schema 版本)。
|
||||
official: '2026.5.12',
|
||||
chinese: '2026.5.12-zh.2',
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ const _listeners = []
|
||||
* @property {boolean} isLatest 是否 >= target
|
||||
* @property {Set<string>} features 当前启用的特性 id 集合
|
||||
* @property {string} versionLabel 人类可读的版本显示,例如 "2026.5.6 汉化"
|
||||
* @property {number|null} protocol 握手协商出的 Gateway WS 协议版本 (3 或 4),未握手为 null
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -104,6 +105,7 @@ export function buildSnapshot(engineId, version) {
|
||||
versionLabel: version
|
||||
? `${versionBase}${variant === 'chinese' ? ' 汉化' : ''}`
|
||||
: '',
|
||||
protocol: wsClient.negotiatedProtocol,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +188,7 @@ function refresh() {
|
||||
|| _snapshot.engine !== next.engine
|
||||
|| _snapshot.version !== next.version
|
||||
|| _snapshot.features.size !== next.features.size
|
||||
|| _snapshot.protocol !== next.protocol
|
||||
_snapshot = next
|
||||
if (changed) {
|
||||
_listeners.forEach(fn => {
|
||||
|
||||
@@ -128,6 +128,34 @@ export class WsClient {
|
||||
get hello() { return this._hello }
|
||||
get sessionKey() { return this._sessionKey }
|
||||
get serverVersion() { return this._serverVersion }
|
||||
/**
|
||||
* 当前 Gateway 与 ClawPanel 协商出的握手协议版本 (3 或 4)。
|
||||
*
|
||||
* 注意:这里的 "协议版本" 指 Gateway WebSocket 握手帧协议 (kernel ws frame protocol),
|
||||
* 不要与设备签名 payload 的前缀 `v3|deviceId|...` 混淆,后者是 device-signature
|
||||
* payload 字符串格式版本,两者完全独立。
|
||||
*
|
||||
* 取值优先级:
|
||||
* 1. hello payload 中显式回传的 protocol / protocolVersion / negotiatedProtocol 字段
|
||||
* 2. 按 serverVersion 推断:OpenClaw 内核 >= 2026.5.12 → v4,否则 v3
|
||||
* (ClawPanel 客户端永远声明 minProtocol=3, maxProtocol=4,所以协商只会是 3 或 4)
|
||||
*
|
||||
* 未握手时返回 null。
|
||||
*/
|
||||
get negotiatedProtocol() {
|
||||
if (!this._hello) return null
|
||||
const explicit = this._hello.protocol ?? this._hello.protocolVersion ?? this._hello.negotiatedProtocol
|
||||
if (Number.isFinite(explicit)) return explicit
|
||||
const ver = this._serverVersion
|
||||
if (!ver) return null
|
||||
const base = String(ver).replace(/-.*$/, '')
|
||||
const parts = base.split('.').map(n => Number(n))
|
||||
if (parts.length >= 3 && parts.every(Number.isFinite)) {
|
||||
const [y, mo, d] = parts
|
||||
if (y > 2026 || (y === 2026 && (mo > 5 || (mo === 5 && d >= 12)))) return 4
|
||||
}
|
||||
return 3
|
||||
}
|
||||
get reconnectState() { return this._reconnectState }
|
||||
get reconnectAttempts() { return this._reconnectAttempts }
|
||||
get lastConnectedAt() { return this._lastConnectedAt }
|
||||
@@ -145,6 +173,7 @@ export class WsClient {
|
||||
reconnectAttempts: this._reconnectAttempts,
|
||||
reconnectState: this._reconnectState,
|
||||
serverVersion: this._serverVersion,
|
||||
negotiatedProtocol: this.negotiatedProtocol,
|
||||
missedHeartbeats: this._missedHeartbeats,
|
||||
pendingReconnect: this._pendingReconnect,
|
||||
}
|
||||
|
||||
@@ -202,4 +202,7 @@ export default {
|
||||
cleanupConfirmUninstallConfig: _('确定要卸载所有 OpenClaw 并删除配置目录吗?\n⚠️ 这将删除所有配置、Agent 数据和会话记录,无法恢复!', 'Uninstall all OpenClaw AND delete config directory?\n⚠️ This will delete all config, agent data, and session history permanently!', '確定要卸載所有 OpenClaw 並刪除設定目錄嗎?\n⚠️ 這將刪除所有設定、Agent 資料和會話記錄,無法恢復!'),
|
||||
cleanupUninstalling: _('正在卸载,请稍候...', 'Uninstalling, please wait...', '正在卸載,請稍候...'),
|
||||
cleanupUninstallFailed: _('卸载失败', 'Uninstall failed', '卸載失敗'),
|
||||
// 协议版本徽章 (Gateway 卡片 + 诊断页)
|
||||
protocolBadge: _('协议 v{proto}', 'Proto v{proto}', '協議 v{proto}'),
|
||||
protocolBadgeTitle: _('当前 Gateway 与 ClawPanel 协商出的 WebSocket 握手协议版本 (与设备签名 payload 的 v3 格式无关)', 'WebSocket handshake protocol version negotiated with Gateway (independent of the device signature payload v3 format)', '當前 Gateway 與 ClawPanel 協商出的 WebSocket 握手協議版本 (與設備簽名 payload 的 v3 格式無關)'),
|
||||
}
|
||||
|
||||
@@ -212,7 +212,11 @@ async function runScan(page) {
|
||||
|
||||
// 5. WebSocket 连接
|
||||
const wsOk = wsClient.connected && wsClient.gatewayReady
|
||||
items.push({ label: 'WebSocket', ok: wsOk, detail: wsOk ? (wsClient.serverVersion ? `Gateway ${wsClient.serverVersion}` : t('chatDebug.connected')) : t('chatDebug.scanWsDown') })
|
||||
const proto = wsOk ? wsClient.negotiatedProtocol : null
|
||||
const wsDetail = wsOk
|
||||
? `${wsClient.serverVersion ? `Gateway ${wsClient.serverVersion}` : t('chatDebug.connected')}${proto ? ` · ${t('services.protocolBadge', { proto })}` : ''}`
|
||||
: t('chatDebug.scanWsDown')
|
||||
items.push({ label: 'WebSocket', ok: wsOk, detail: wsDetail })
|
||||
if (!wsOk && gwRunning) fixable = true
|
||||
|
||||
// 6. Token 配置
|
||||
|
||||
@@ -11,6 +11,7 @@ import { isForeignGatewayError, isForeignGatewayService, maybeShowForeignGateway
|
||||
import { diagnoseInstallError } from '../lib/error-diagnosis.js'
|
||||
import { icon, statusIcon } from '../lib/icons.js'
|
||||
import { t } from '../lib/i18n.js'
|
||||
import { wsClient } from '../lib/ws-client.js'
|
||||
|
||||
// HTML 转义,防止 XSS
|
||||
function escapeHtml(str) {
|
||||
@@ -476,13 +477,16 @@ function renderServices(container, services) {
|
||||
const cliMissing = gw.cli_installed === false
|
||||
const foreignGateway = !cliMissing && isForeignGatewayService(gw)
|
||||
const foreignPidText = gw.pid ? ` (PID: ${gw.pid})` : ''
|
||||
// 协议版本徽章(仅在 Gateway 跑起来并完成 WS 握手时显示)
|
||||
const proto = wsClient.gatewayReady ? wsClient.negotiatedProtocol : null
|
||||
const protoBadge = proto ? `<span class="proto-badge" title="${t('services.protocolBadgeTitle', { proto })}">${t('services.protocolBadge', { proto })}</span>` : ''
|
||||
|
||||
html += `
|
||||
<div class="service-card" data-label="${gw.label}">
|
||||
<div class="service-info">
|
||||
<span class="status-dot ${cliMissing ? 'stopped' : foreignGateway ? 'warning' : gw.running ? 'running' : 'stopped'}"></span>
|
||||
<div>
|
||||
<div class="service-name">${gw.label}</div>
|
||||
<div class="service-name">${gw.label}${protoBadge}</div>
|
||||
<div class="service-desc">${cliMissing
|
||||
? t('services.cliNotInstalled')
|
||||
: foreignGateway
|
||||
|
||||
@@ -218,6 +218,21 @@
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.service-name .proto-badge {
|
||||
display: inline-block;
|
||||
margin-left: 8px;
|
||||
padding: 1px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
border-radius: 4px;
|
||||
background: var(--accent-muted);
|
||||
color: var(--accent);
|
||||
border: 1px solid var(--accent-muted);
|
||||
vertical-align: middle;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.service-desc {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--text-tertiary);
|
||||
|
||||
Reference in New Issue
Block a user