mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
feat: webui 设备网关 WS 反代(hijack 双向透传) + GUI 设备页本机卡片始终显示
- handleDeviceGatewayProxy: 识别 Upgrade:websocket 请求, 用 http.Transport(保留升级连接) + hijack 双向字节透传, 支持远程 homed 的 WS 设备通道(REST 已可用, WS 之前被 DefaultClient 卡死) - renderDevices: 本机 GUI 设备卡片不再被 conn.type!=='device' 挡住, 由 gui-prefs 的 deviceBridge 驱动(独立于连接), renderInit 从 device-bridge:get 拉取本机身份 - 对应 pi-desktop 排查记录(WS 反代不可用/本机卡片被挡)修复
This commit is contained in:
@ -581,16 +581,29 @@ async function refreshAll() {
|
|||||||
state.devices = [];
|
state.devices = [];
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (
|
// 本机设备桥身份:设备桥由 gui-prefs 驱动,独立于当前连接类型
|
||||||
state.currentConn &&
|
if (window.homeagent && window.homeagent.deviceBridge) {
|
||||||
state.currentConn.type === "device" &&
|
var dbinfo = await window.homeagent.deviceBridge.get();
|
||||||
window.homeagent &&
|
if (dbinfo && dbinfo.enabled) {
|
||||||
window.homeagent.device
|
if (dbinfo.deviceId) {
|
||||||
) {
|
state.selfDeviceId = dbinfo.deviceId;
|
||||||
var idn = await window.homeagent.device.identity();
|
state.selfGateway = dbinfo.address || state.selfGateway;
|
||||||
if (idn) {
|
}
|
||||||
state.selfDeviceId = idn.device_id || "";
|
// 若尚未有设备列表且已启用设备桥但非 device 连接,尝试经设备桥网关拉取
|
||||||
state.selfGateway = idn.address || "";
|
if (
|
||||||
|
state.devices.length === 0 &&
|
||||||
|
dbinfo.gateway &&
|
||||||
|
window.homeagent &&
|
||||||
|
window.homeagent.device
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
var eb = await window.homeagent.device.identity();
|
||||||
|
if (eb && eb.device_id) {
|
||||||
|
state.selfDeviceId = eb.device_id;
|
||||||
|
state.selfGateway = eb.address || dbinfo.gateway;
|
||||||
|
}
|
||||||
|
} catch (e2) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
@ -4839,21 +4852,6 @@ function renderDevices() {
|
|||||||
var el = document.getElementById("view-devices");
|
var el = document.getElementById("view-devices");
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
var conn = state.currentConn;
|
var conn = state.currentConn;
|
||||||
if (!conn || conn.type !== "device") {
|
|
||||||
el.innerHTML =
|
|
||||||
'<div class="card"><h2>' +
|
|
||||||
__("设备", "Devices") +
|
|
||||||
'</h2><p style="color:var(--text-muted)">' +
|
|
||||||
__(
|
|
||||||
"请先在设置中添加并切换到「设备网关 (remotedevice)」连接,以查看已接入设备。",
|
|
||||||
"Add and switch to a Remote Device Gateway connection in Settings to view devices.",
|
|
||||||
) +
|
|
||||||
"</p>" +
|
|
||||||
'<button class="btn btn-primary" onclick="goSettingsConn()">' +
|
|
||||||
__("管理连接", "Manage Connections") +
|
|
||||||
"</button></div>";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var devs = state.devices || [];
|
var devs = state.devices || [];
|
||||||
var selfDev = null;
|
var selfDev = null;
|
||||||
if (state.selfDeviceId) {
|
if (state.selfDeviceId) {
|
||||||
@ -4864,6 +4862,8 @@ function renderDevices() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 本机 GUI 设备卡片:始终显示(不依赖当前连接类型),
|
||||||
|
// 设备桥由 gui-prefs 的 deviceBridge 驱动,独立于连接。
|
||||||
var selfHtml =
|
var selfHtml =
|
||||||
'<div class="card" style="border-left:3px solid #4f8cff"><h2>' +
|
'<div class="card" style="border-left:3px solid #4f8cff"><h2>' +
|
||||||
__("本机 GUI 设备", "Local GUI Device") +
|
__("本机 GUI 设备", "Local GUI Device") +
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package webui
|
package webui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"embed"
|
"embed"
|
||||||
@ -1678,6 +1679,7 @@ var (
|
|||||||
|
|
||||||
// handleDeviceGatewayProxy 将 /api/v1/device/* 反代到 remotedevice 内部 HTTP 服务。
|
// handleDeviceGatewayProxy 将 /api/v1/device/* 反代到 remotedevice 内部 HTTP 服务。
|
||||||
// 鉴权:本端走 requireAPI(webui API key),转发时带 remotedevice 的 token(X-API-Key)。
|
// 鉴权:本端走 requireAPI(webui API key),转发时带 remotedevice 的 token(X-API-Key)。
|
||||||
|
// WS 升级请求(Upgrade: websocket)走 hijack 双向字节透传(标准库 http.Client 不支持 101 升级)。
|
||||||
func (h *Handler) handleDeviceGatewayProxy(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handleDeviceGatewayProxy(w http.ResponseWriter, r *http.Request) {
|
||||||
if !deviceGatewayEnabled {
|
if !deviceGatewayEnabled {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
@ -1692,6 +1694,13 @@ func (h *Handler) handleDeviceGatewayProxy(w http.ResponseWriter, r *http.Reques
|
|||||||
if r.URL.RawQuery != "" {
|
if r.URL.RawQuery != "" {
|
||||||
url += "?" + r.URL.RawQuery
|
url += "?" + r.URL.RawQuery
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebSocket 升级:hijack 双向透传(支持 WS over 远程 homed)
|
||||||
|
if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") {
|
||||||
|
h.proxyWebSocket(w, r, addr, path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(r.Context(), r.Method, url, r.Body)
|
req, err := http.NewRequestWithContext(r.Context(), r.Method, url, r.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
@ -1714,6 +1723,88 @@ func (h *Handler) handleDeviceGatewayProxy(w http.ResponseWriter, r *http.Reques
|
|||||||
io.Copy(w, resp.Body)
|
io.Copy(w, resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// proxyWebSocket 用 TCP 直连 + hijack 将客户端 WS 连接双向透传到设备网关。
|
||||||
|
func (h *Handler) proxyWebSocket(w http.ResponseWriter, r *http.Request, addr, path string) {
|
||||||
|
upstream := "ws://" + addr + path
|
||||||
|
if r.URL.RawQuery != "" {
|
||||||
|
upstream += "?" + r.URL.RawQuery
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
// 构造带 Upgrade 头的请求:http.Transport 对 Upgrade 请求保留连接字节流
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, upstream, nil)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "ws upstream: "+err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Header = r.Header.Clone()
|
||||||
|
if deviceGatewayToken != "" {
|
||||||
|
req.Header.Set("X-API-Key", deviceGatewayToken)
|
||||||
|
}
|
||||||
|
tr := &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
}
|
||||||
|
resp, err := tr.RoundTrip(req)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "ws upstream dial: "+err.Error(), http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
http.Error(w, "ws upstream status: "+resp.Status, http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户端 hijack:把 101 响应头写给客户端并接管双向连接
|
||||||
|
hj, ok := w.(http.Hijacker)
|
||||||
|
if !ok {
|
||||||
|
resp.Body.Close()
|
||||||
|
http.Error(w, "hijack not supported", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clientConn, brw, err := hj.Hijack()
|
||||||
|
if err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer clientConn.Close()
|
||||||
|
|
||||||
|
// 向上游写回 101 响应头
|
||||||
|
if err := resp.Write(brw); err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := brw.Flush(); err != nil {
|
||||||
|
resp.Body.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上游连接
|
||||||
|
upConn, ok := resp.Body.(io.ReadWriteCloser)
|
||||||
|
if !ok {
|
||||||
|
clientConn.Close()
|
||||||
|
http.Error(w, "upstream conn not rw", http.StatusBadGateway)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer upConn.Close()
|
||||||
|
|
||||||
|
// 双向透传(WS 帧字节不动)
|
||||||
|
errCh := make(chan struct{}, 2)
|
||||||
|
go func() {
|
||||||
|
io.Copy(upConn, brw)
|
||||||
|
if tc, ok := upConn.(interface{ CloseWrite() error }); ok {
|
||||||
|
tc.CloseWrite()
|
||||||
|
}
|
||||||
|
errCh <- struct{}{}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
io.Copy(bufio.NewWriter(clientConn), upConn)
|
||||||
|
errCh <- struct{}{}
|
||||||
|
}()
|
||||||
|
<-errCh
|
||||||
|
tr.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
|
||||||
// ======== Plugin Management (proxied to pluginmgr HTTP API) ========
|
// ======== Plugin Management (proxied to pluginmgr HTTP API) ========
|
||||||
|
|
||||||
func (h *Handler) pluginmgrAddr() string {
|
func (h *Handler) pluginmgrAddr() string {
|
||||||
|
|||||||
Reference in New Issue
Block a user