优化: 跨端链路协议对齐
- miniapp 配置域旧端点强制重置 - df-tunnel 隧道协议与 relay 现网对齐 - relay e2e 例程入 examples
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* df-relay WS 客户端(uni-app 跨端适配)
|
||||
* u-relay WS 客户端(uni-app 跨端适配)
|
||||
*
|
||||
* 协议(对齐 crates/df-relay/src/relay.rs handle_connection):
|
||||
* 1. 建立 WS 连接到 `/ws/miniapp`
|
||||
* 2. 首帧发 Hello{kind:'miniapp', device_id, token} 身份宣告(relay.rs:197-208,10s 超时)
|
||||
* 3. 校验通过后进入收发循环;失败 relay 发 Error 帧 + Close
|
||||
* 协议(对齐 E:/wk-lab/u-relay/src/relay.rs handle_connection):
|
||||
* 1. 建立 WS 连接到 `/ws/commander`
|
||||
* 2. 首帧发 Hello{kind:'commander', channel_id, token} 身份宣告(relay 10s 超时)
|
||||
* 3. 校验通过 → relay 回 hello_ack;失败 relay 发 error 帧 + Close
|
||||
* (错误码:handshake_failed/kind_mismatch/invalid_channel_id/server_misconfigured/auth_failed)
|
||||
*
|
||||
* 跨端适配:
|
||||
* - 微信小程序:uni.connectSocket(微信原生 API,非浏览器 WebSocket)
|
||||
@@ -176,10 +177,10 @@ class WsClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 Command(miniapp → device)
|
||||
* 发送 Command(commander → agent)
|
||||
*
|
||||
* 不包装 BroadcastMessage —— relay.rs:332-365 入站文本帧直接当 payload 包成
|
||||
* BroadcastMessage(device_id/kind/source/from 由 relay 填),故客户端仅发业务 JSON。
|
||||
* BroadcastMessage(channel_id/kind/source/from 由 relay 填),故客户端仅发业务 JSON。
|
||||
*/
|
||||
send(cmd: MiniCommand): boolean {
|
||||
if (DEBUG) console.log('[dbg:ws] send', cmd.cmd, 'socket=', !!this.socket, 'handshaked=', this.handshaked)
|
||||
@@ -314,8 +315,8 @@ class WsClient {
|
||||
this.setStatus('handshaking')
|
||||
const cfg = getConfig()
|
||||
const hello: Hello = {
|
||||
kind: 'miniapp',
|
||||
device_id: cfg.deviceId,
|
||||
kind: 'commander',
|
||||
channel_id: cfg.channelId,
|
||||
token: cfg.token,
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/**
|
||||
* miniapp 运行时配置(host/device_id/token 占位,由用户在设置页或 build env 填)
|
||||
* miniapp 运行时配置(channel_id/token 占位,由用户在设置页或 build env 填)
|
||||
*
|
||||
* 这些值在真实部署前是占位:
|
||||
* - RELAY_HOST:ws/wss 地址,需替换为实际部署的 df-relay 地址
|
||||
* - DEVICE_ID:用户首次配置桌面端时生成的设备 ID
|
||||
* - TOKEN:对齐 df-relay 的 DF_RELAY_TOKEN(生产建议每 device 独立 token)
|
||||
* - RELAY_HOST:ws/wss 地址,需替换为实际部署的 u-relay 地址
|
||||
* - CHANNEL_ID:u-relay 配对通道 ID(与桌面端 df-tunnel 共享同键)
|
||||
* - TOKEN:对齐 u-relay 的 U_RELAY_TOKEN(生产建议 per-channel 独立 token,Phase3)
|
||||
*/
|
||||
|
||||
export interface MiniappConfig {
|
||||
/** relay WS 地址(含协议),如 'ws://192.168.1.100:8080/ws/miniapp' 或 'wss://relay.example.com/ws/miniapp' */
|
||||
/** relay WS 地址(含协议),如 'ws://192.168.1.100:9180/ws/commander' 或 'wss://relay.example.com/ws/commander' */
|
||||
relayHost: string
|
||||
/** 配对绑定的设备 ID */
|
||||
deviceId: string
|
||||
/** 配对 token(对齐 df-relay DF_RELAY_TOKEN) */
|
||||
/** 配对绑定的通道 ID(u-relay channel_id,与桌面端共享同键配对) */
|
||||
channelId: string
|
||||
/** 配对 token(对齐 u-relay U_RELAY_TOKEN) */
|
||||
token: string
|
||||
/** 心跳间隔(ms) */
|
||||
heartbeatInterval: number
|
||||
@@ -29,17 +29,17 @@ export interface MiniappConfig {
|
||||
/**
|
||||
* 默认配置(占位值,需替换)
|
||||
*
|
||||
* 真实部署:用户在设置页填 relayHost/deviceId/token,或通过 build-time env 注入。
|
||||
* 真实部署:用户在设置页填 relayHost/channelId/token,或通过 build-time env 注入。
|
||||
* MVP 阶段留占位让类型自洽,实际连接会失败(token 校验不通过 → relay 关连接,
|
||||
* 这符合预期,提示用户配置)。
|
||||
*/
|
||||
export const defaultConfig: MiniappConfig = {
|
||||
// 测试服 wss(u-work.1216.top → nginx 终止 TLS → df-relay :9180 明文 ws)
|
||||
// u-relay 现行协议 commander 端点:wss://u-work.1216.top → nginx 终止 TLS → u-relay :9180 明文 ws
|
||||
// 微信小程序真机要求 wss + socket 合法域名白名单(微信公众平台→开发设置→服务器域名)
|
||||
relayHost: 'wss://u-work.1216.top/ws/miniapp',
|
||||
// 配对桌面端 device_id(查桌面端 KV app_settings:device_id;填错则握手过但路由无对端)
|
||||
// 当前填本机桌面端 device_id(联调期硬编码,后续做配对绑定流程)
|
||||
deviceId: '1b92cbbd-d03e-4a0c-b226-9fadf8dcf0f0',
|
||||
relayHost: 'wss://u-work.1216.top/ws/commander',
|
||||
// 配对桌面端 channel_id(查桌面端 KV app_settings:channel_id;填错则握手过但路由无对端)
|
||||
// 当前填本机桌面端 channel_id(联调期硬编码,后续做配对绑定流程)
|
||||
channelId: '1b92cbbd-d03e-4a0c-b226-9fadf8dcf0f0',
|
||||
token: 'devflow-relay-default-token',
|
||||
heartbeatInterval: 30000,
|
||||
reconnectBaseDelay: 1000,
|
||||
@@ -54,6 +54,23 @@ const STORAGE_KEY = 'df-miniapp-config'
|
||||
/** 单例配置(null 表示尚未从 storage 加载) */
|
||||
let _config: MiniappConfig | null = null
|
||||
|
||||
/**
|
||||
* 旧版字段迁移(df-relay 一代 → u-relay):
|
||||
* storage 里的旧 JSON 含 deviceId 字段,映射为 channelId,避免老用户升级后配置丢失。
|
||||
*/
|
||||
function migrateLegacyFields(parsed: Record<string, unknown>): Partial<MiniappConfig> {
|
||||
const out = { ...parsed } as Record<string, unknown>
|
||||
if (out.deviceId !== undefined && out.channelId === undefined) {
|
||||
out.channelId = out.deviceId
|
||||
}
|
||||
delete out.deviceId
|
||||
// 旧 relayHost 指向已下线的 /ws/miniapp 端点,升级后强制重置为新端点
|
||||
if (typeof out.relayHost === 'string' && out.relayHost.includes('/ws/miniapp')) {
|
||||
out.relayHost = defaultConfig.relayHost
|
||||
}
|
||||
return out as Partial<MiniappConfig>
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 storage 读取并合并默认配置
|
||||
*
|
||||
@@ -64,9 +81,10 @@ function loadFromStorage(): MiniappConfig {
|
||||
try {
|
||||
const raw = uni.getStorageSync(STORAGE_KEY)
|
||||
if (!raw) return { ...defaultConfig }
|
||||
const parsed = JSON.parse(raw) as Partial<MiniappConfig>
|
||||
// 合并默认值,避免旧版本字段缺失导致 undefined
|
||||
return { ...defaultConfig, ...parsed }
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>
|
||||
// 合并默认值 + 旧字段迁移,避免旧版本字段缺失/更名导致 undefined
|
||||
const migrated = migrateLegacyFields(parsed)
|
||||
return { ...defaultConfig, ...migrated }
|
||||
} catch (e) {
|
||||
console.warn('[df-miniapp] load config from storage failed:', e)
|
||||
return { ...defaultConfig }
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useAiChat } from '@/composables/useAiChat'
|
||||
/**
|
||||
* 设置页(连接配置查看/编辑 + 连接状态 + 连接操作)。
|
||||
*
|
||||
* 默认查看模式(只读展示 relayHost/deviceId/token,token 脱敏);点「编辑」进入编辑模式,
|
||||
* 默认查看模式(只读展示 relayHost/channelId/token,token 脱敏);点「编辑」进入编辑模式,
|
||||
* 保存时 setConfig 持久化并 resumeIfDisconnected 用新配置重连。连接态可「断开」,
|
||||
* 断开态可「重连」,另展示最近状态详情(wsStatusDetail)辅助排查。
|
||||
*
|
||||
@@ -82,13 +82,13 @@ function onConnOp(): void {
|
||||
/** 编辑态(false=查看模式,true=编辑模式;草稿保存时才 setConfig,取消丢弃) */
|
||||
const editing = ref(false)
|
||||
const draftRelayHost = ref('')
|
||||
const draftDeviceId = ref('')
|
||||
const draftChannelId = ref('')
|
||||
const draftToken = ref('')
|
||||
|
||||
/** 进入编辑模式:以当前配置填充草稿 */
|
||||
function enterEdit(): void {
|
||||
draftRelayHost.value = cfg.value.relayHost
|
||||
draftDeviceId.value = cfg.value.deviceId
|
||||
draftChannelId.value = cfg.value.channelId
|
||||
draftToken.value = cfg.value.token
|
||||
editing.value = true
|
||||
}
|
||||
@@ -101,12 +101,12 @@ function cancelEdit(): void {
|
||||
/** 保存编辑:setConfig 持久化 + 提示 + resumeIfDisconnected 用新配置重连 */
|
||||
function saveEdit(): void {
|
||||
const relayHost = draftRelayHost.value.trim()
|
||||
const deviceId = draftDeviceId.value.trim()
|
||||
if (!relayHost || !deviceId) {
|
||||
uni.showToast({ title: '中继地址和设备 ID 不能为空', icon: 'none' })
|
||||
const channelId = draftChannelId.value.trim()
|
||||
if (!relayHost || !channelId) {
|
||||
uni.showToast({ title: '中继地址和通道 ID 不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
setConfig({ relayHost, deviceId, token: draftToken.value.trim() })
|
||||
setConfig({ relayHost, channelId, token: draftToken.value.trim() })
|
||||
editing.value = false
|
||||
uni.showToast({ title: '已保存,正在重连…', icon: 'none' })
|
||||
ai.resumeIfDisconnected()
|
||||
@@ -139,7 +139,7 @@ function saveEdit(): void {
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 连接配置(查看/编辑双模式:编辑可改 relayHost/deviceId/token) -->
|
||||
<!-- 连接配置(查看/编辑双模式:编辑可改 relayHost/channelId/token) -->
|
||||
<view class="card">
|
||||
<view class="card-head">
|
||||
<text class="card-title">连接配置</text>
|
||||
@@ -151,13 +151,13 @@ function saveEdit(): void {
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="row-label">中继地址</text>
|
||||
<input v-if="editing" v-model="draftRelayHost" class="row-input" placeholder="ws://…/ws/miniapp" placeholder-class="row-ph" />
|
||||
<input v-if="editing" v-model="draftRelayHost" class="row-input" placeholder="ws://…/ws/commander" placeholder-class="row-ph" />
|
||||
<text v-else class="row-value mono">{{ cfg.relayHost }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="row-label">设备 ID</text>
|
||||
<input v-if="editing" v-model="draftDeviceId" class="row-input" placeholder="桌面端设备 ID" placeholder-class="row-ph" />
|
||||
<text v-else class="row-value mono">{{ cfg.deviceId }}</text>
|
||||
<text class="row-label">通道 ID</text>
|
||||
<input v-if="editing" v-model="draftChannelId" class="row-input" placeholder="u-relay 通道 ID" placeholder-class="row-ph" />
|
||||
<text v-else class="row-value mono">{{ cfg.channelId }}</text>
|
||||
</view>
|
||||
<view class="row">
|
||||
<text class="row-label">配对 Token</text>
|
||||
|
||||
@@ -1,44 +1,47 @@
|
||||
/**
|
||||
* df-relay 协议类型镜像(对齐 Rust crates/df-relay/src/)
|
||||
* u-relay 协议类型镜像(对齐 E:/wk-lab/u-relay/src/)
|
||||
*
|
||||
* 类型证据:
|
||||
* - ClientKind / MessageKind:crates/df-relay/src/broadcast.rs:21-41
|
||||
* (#[serde(rename_all = "snake_case")] → 'device'/'miniapp', 'event'/'command'/'control')
|
||||
* - BroadcastMessage:broadcast.rs:48-62(device_id/kind/source/from/payload/ts)
|
||||
* - Hello 握手帧:crates/df-relay/src/relay.rs:44-53(kind/device_id/token)
|
||||
* kind 用 ClientKindWire(relay.rs:56-61),序列化为 'device'/'miniapp'
|
||||
* - 路由端点:relay.rs:177-183 `/ws/miniapp`(小程序专用端点)
|
||||
* - ClientKind / MessageKind:u-relay/src/broadcast.rs
|
||||
* (#[serde(rename_all = "snake_case")] → 'agent'/'commander', 'event'/'command'/'control')
|
||||
* - BroadcastMessage:broadcast.rs(channel_id/kind/source/from/payload/ts)
|
||||
* - Hello 握手帧:u-relay/src/relay.rs(kind/channel_id/token)
|
||||
* kind 用 ClientKindWire,序列化为 'agent'/'commander'
|
||||
* - 路由端点:relay.rs `/ws/agent`(执行端)/ `/ws/commander`(控制端)
|
||||
*
|
||||
* 关键协议点(relay.rs:332-365 handle_inbound_text):
|
||||
* relay 不解析 payload —— miniapp 入站文本帧直接当 payload 包成 BroadcastMessage 路由给 device。
|
||||
* 关键协议点(relay.rs handle_inbound_text):
|
||||
* relay 不解析 payload —— commander 入站文本帧直接当 payload 包成 BroadcastMessage 路由给 agent。
|
||||
* 故 miniapp 发送时,直接序列化 Command JSON(relay 自动包 payload),不需客户端构造 BroadcastMessage。
|
||||
* 接收时,device 事件经 relay 包成完整 BroadcastMessage 序列化下发,miniapp 需先解 BroadcastMessage
|
||||
* 接收时,agent 事件经 relay 包成完整 BroadcastMessage 序列化下发,miniapp 需先解 BroadcastMessage
|
||||
* 再从 payload 取 AiChatEvent。
|
||||
*
|
||||
* 历史注:df-relay 一代协议(kind:'device'/'miniapp' + device_id)已在 u-relay 全部下线,
|
||||
* 旧名握手期直接拒绝(端点 /ws/miniapp、/ws/device 均已失效)。
|
||||
*/
|
||||
|
||||
/** 客户端类型(对齐 Rust ClientKind,snake_case 序列化,broadcast.rs:21-28) */
|
||||
export type ClientKind = 'device' | 'miniapp'
|
||||
/** 客户端类型(对齐 Rust ClientKind,snake_case 序列化;u-relay 现行仅此两值) */
|
||||
export type ClientKind = 'agent' | 'commander'
|
||||
|
||||
/** 消息语义分类(对齐 Rust MessageKind,snake_case 序列化,broadcast.rs:35-41) */
|
||||
/** 消息语义分类(对齐 Rust MessageKind,snake_case 序列化) */
|
||||
export type MessageKind = 'event' | 'command' | 'control'
|
||||
|
||||
/** 在线状态(对齐 Rust PresenceState,snake_case 序列化,broadcast.rs:119-126) */
|
||||
/** 在线状态(对齐 Rust PresenceState,snake_case 序列化) */
|
||||
export type PresenceState = 'online' | 'offline'
|
||||
|
||||
/** ConnId 是 Rust u64(对齐 conn.rs ConnId struct),TS 用 number */
|
||||
/** ConnId 是 Rust u64(对齐 conn.rs ConnId struct),TS 用 number;0 为 NIL 伪值 */
|
||||
export type ConnId = number
|
||||
|
||||
/**
|
||||
* 中继路由消息骨架(对齐 Rust BroadcastMessage,broadcast.rs:48-62)
|
||||
* 中继路由消息骨架(对齐 Rust BroadcastMessage)
|
||||
*
|
||||
* miniapp 接收时 payload 为 AiChatEvent JSON(relay 透传不解析)。
|
||||
*/
|
||||
export interface BroadcastMessage {
|
||||
/** 配对绑定的设备 ID */
|
||||
device_id: string
|
||||
/** 配对绑定的通道 ID */
|
||||
channel_id: string
|
||||
/** 消息语义 */
|
||||
kind: MessageKind
|
||||
/** 消息来源连接 ID(relay 分配,u64) */
|
||||
/** 消息来源连接 ID(relay 分配,u64;0=relay 自构造 NIL 伪值) */
|
||||
source: ConnId
|
||||
/** 来源客户端类型 */
|
||||
from: ClientKind
|
||||
@@ -49,36 +52,36 @@ export interface BroadcastMessage {
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端首消息:身份宣告(对齐 Rust Hello,relay.rs:44-53)
|
||||
* 客户端首消息:身份宣告(对齐 Rust Hello)
|
||||
*
|
||||
* 小程序连 `/ws/miniapp` 后首帧必须发此结构,否则 relay 关连接(relay.rs:197-208)。
|
||||
* kind 恒为 'miniapp'(与端点匹配,relay.rs:212-225 校验,不匹配则关连接)。
|
||||
* 小程序连 `/ws/commander` 后首帧必须发此结构(10s 内),否则 relay 关连接。
|
||||
* kind 恒为 'commander'(与端点匹配,不匹配 relay 回 kind_mismatch 错误帧 + Close)。
|
||||
*/
|
||||
export interface Hello {
|
||||
/** 客户端类型,小程序恒 'miniapp'(对齐 ClientKindWire) */
|
||||
kind: 'miniapp' | 'device'
|
||||
/** 配对绑定的设备 ID(用户首次配置时生成) */
|
||||
device_id: string
|
||||
/** 配对 token(env DF_RELAY_TOKEN,fallback "devflow-relay-default-token") */
|
||||
/** 客户端类型,小程序(控制端)恒 'commander'(对齐 ClientKindWire) */
|
||||
kind: 'commander'
|
||||
/** 配对绑定的通道 ID(非空,≤128B,无控制字符) */
|
||||
channel_id: string
|
||||
/** 配对 token(env U_RELAY_TOKEN,与桌面端共享) */
|
||||
token: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制面消息(对齐 Rust ControlMessage,broadcast.rs:132-145,tag = "control_kind")
|
||||
* 控制面消息(对齐 Rust ControlMessage,tag = "control_kind")
|
||||
*
|
||||
* miniapp 主要发 Heartbeat 心跳 + Ping;Pair 在首次绑定时用(本批未实现绑定流程)。
|
||||
* miniapp 主要发 Ping 心跳(relay 本地直回 pong,不经对端);Pair/Heartbeat/Presence 骨架保留。
|
||||
*/
|
||||
export type ControlMessage =
|
||||
| { control_kind: 'pair'; device_id: string; token: string }
|
||||
| { control_kind: 'heartbeat'; device_id: string }
|
||||
| { control_kind: 'presence'; device_id: string; state: PresenceState }
|
||||
| { control_kind: 'pair'; channel_id: string; token: string }
|
||||
| { control_kind: 'heartbeat'; channel_id: string }
|
||||
| { control_kind: 'presence'; channel_id: string; state: PresenceState }
|
||||
| { control_kind: 'ping' }
|
||||
| { control_kind: 'pong' }
|
||||
|
||||
/**
|
||||
* 小程序 → 桌面端 Command 协议(miniapp 发起操作)
|
||||
* 小程序(控制端)→ 桌面端(执行端)Command 协议
|
||||
*
|
||||
* 类型为对齐 Tauri command 名,relay 透传 device_id 路由到桌面端,桌面端 invoke 对应 command。
|
||||
* 类型为对齐 Tauri command 名,relay 透传 channel_id 路由到桌面端,桌面端 invoke 对应 command。
|
||||
* 字段:
|
||||
* - cmd:命令名(对齐 src-tauri command 函数名)
|
||||
* - args:命令参数(对齐各 command 的参数)
|
||||
@@ -227,6 +230,6 @@ export interface ContinueLoopArgs {
|
||||
|
||||
/** stop_loop 命令参数(对齐 src-tauri ai_stop_loop,Phase3 §2.2 路由表) */
|
||||
export interface StopLoopArgs {
|
||||
/** 霷停止循环的会话 id */
|
||||
/** 需停止循环的会话 id */
|
||||
conversation_id: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user