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:
晴天
2026-05-16 13:02:08 +08:00
parent cb4c9bcdfc
commit 7d75486a53
9 changed files with 76 additions and 4 deletions

View File

@@ -190,7 +190,12 @@ export const KERNEL_FLOOR = {
export const KERNEL_TARGET = {
openclaw: {
// 内核协议在 5.12 升级到 v4MIN_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',
},

View File

@@ -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 => {

View File

@@ -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,
}