mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
拆法:按「资源面」搬家,每个顶层声明(func/type/var/const)整体搬到目标文件, 声明体一字未改,各文件按实际用到的包重新生成 import。文件头加一行说明本文件负责哪一面。 handler.go 骨架:嵌入前端资源、Handler/构造、路由表、鉴权会话日志中间件、静态页 handler_chat.go 对话面:消息模型与内存历史、SSE 事件订阅、对话/历史接口 handler_upload.go 上传面:handleChatFile / handleUploads / 中断对话 handler_memory.go 记忆面:图/文档/文本记忆、知识库、LLM 源、变更追踪 handler_agents.go 内核与代理面:状态、kernel、人格、代理/快照/回滚 handler_settings.go 设置与插件面:配置读写、插件列表详情(含 pluginmgr 反代) handler_terminal.go 终端面:终端会话、终端接口、命令历史 handler_sse.go SSE 环形缓冲(断线重连补发) handler_openai.go OpenAI 兼容面:/v1/chat/completions handler_device.go 设备网关反代(HTTP + WS 升级透传) handler_files.go /files/ 与 /uploads/ 下载 零漂移校验:拿重构前的 handler.go 与新 11 个文件逐行比对(忽略空行、package/import 头), **丢失行 0**;新增行恰好是 11 个文件头注释(14 行)。 顺带修掉 import 里两处假使用:handler_openai 的 sdk 只作为 Handler 字段名出现(h.sdk.), handler_settings 的 fmt 只出现在注释里 —— 都从 import 里去掉。 验证:go build ./... / go vet / webui+config+sdk 测试全绿; 起真实实例(沿用已有 data 目录)后 /status /settings /chat/history /plugins /terminals /kernel /memory /config /login 全部 200,设置在注入 5MB 历史的情况下仍是 33,921 字节。 最大文件从 2993 → 706 行(handler_chat.go)。
192 lines
5.7 KiB
Go
192 lines
5.7 KiB
Go
package webui
|
||
|
||
import (
|
||
"bufio"
|
||
"io"
|
||
"net"
|
||
"strings"
|
||
"time"
|
||
|
||
"net/http"
|
||
)
|
||
|
||
// 设备网关反代:/api/v1/device/* → remotedevice(HTTP + WS 升级透传)。
|
||
|
||
// ======== Remote Device Gateway (proxied to remotedevice, opt-in) ========
|
||
|
||
// deviceGatewayEnabled / deviceGatewayAddr 由 webui 插件启动时从设置读取并注入。
|
||
// 默认禁用:用户显式配置 device_gateway_enabled=true 后,/api/v1/device/* 才会反代到
|
||
// remotedevice 插件(self-contained),避免与 remotedevice 耦合。
|
||
var (
|
||
deviceGatewayEnabled bool
|
||
deviceGatewayAddr string
|
||
deviceGatewayToken string
|
||
)
|
||
|
||
// handleDeviceGatewayProxy 将 /api/v1/device/* 反代到 remotedevice 内部 HTTP 服务。
|
||
// 鉴权:本端走 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) {
|
||
if !deviceGatewayEnabled {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
// 客户端鉴权模式:服务端不再提供授权接口(授权由设备端本地控制)。
|
||
// 拒绝旧的 /device/auth 调用,避免误导。
|
||
if strings.HasSuffix(r.URL.Path, "/device/auth") {
|
||
writeJSON(w, http.StatusGone, map[string]string{
|
||
"error": "device authorization moved to client-side; the server no longer stores authorization state",
|
||
})
|
||
return
|
||
}
|
||
addr := deviceGatewayAddr
|
||
if addr == "" {
|
||
addr = "127.0.0.1:9890"
|
||
}
|
||
path := r.URL.Path // 保留 /api/v1/device/... 全路径
|
||
url := "http://" + addr + path
|
||
if 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)
|
||
if err != nil {
|
||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||
return
|
||
}
|
||
req.Header = r.Header.Clone()
|
||
if deviceGatewayToken != "" {
|
||
req.Header.Set("X-API-Key", deviceGatewayToken)
|
||
}
|
||
resp, err := http.DefaultClient.Do(req)
|
||
if err != nil {
|
||
writeJSON(w, http.StatusBadGateway, map[string]string{"error": "device gateway unreachable: " + err.Error()})
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
for k, v := range resp.Header {
|
||
w.Header()[k] = v
|
||
}
|
||
w.WriteHeader(resp.StatusCode)
|
||
io.Copy(w, resp.Body)
|
||
}
|
||
|
||
// proxyWebSocket 用 TCP 直连 + hijack 将客户端 WS 连接双向透传到设备网关。
|
||
func (h *Handler) proxyWebSocket(w http.ResponseWriter, r *http.Request, addr, path string) {
|
||
// 设备网关默认仅监听 127.0.0.1(remotedevice),反代目标即内网 homed 本机或指定 addr。
|
||
// 用 net.Dial 直连网关并手动发起 WS 升级握手(net/http 客户端不支持 ws:// 升级)。
|
||
host, port := addr, "9890"
|
||
if h2, p2, ok := splitHostPort(addr); ok {
|
||
host, port = h2, p2
|
||
}
|
||
target := net.JoinHostPort(host, port)
|
||
upConn, err := net.DialTimeout("tcp", target, 15*time.Second)
|
||
if err != nil {
|
||
http.Error(w, "ws upstream dial: "+err.Error(), http.StatusBadGateway)
|
||
return
|
||
}
|
||
defer upConn.Close()
|
||
|
||
// 手动构造 WS 升级请求(保留客户端头 + 注入网关 token)
|
||
key := r.Header.Get("Sec-WebSocket-Key")
|
||
if key == "" {
|
||
key = "homeagent-proxy-random-key"
|
||
}
|
||
reqPath := path
|
||
if r.URL.RawQuery != "" {
|
||
reqPath += "?" + r.URL.RawQuery
|
||
}
|
||
var b strings.Builder
|
||
b.WriteString("GET " + reqPath + " HTTP/1.1\r\n")
|
||
b.WriteString("Host: " + addr + "\r\n")
|
||
b.WriteString("Upgrade: websocket\r\n")
|
||
b.WriteString("Connection: Upgrade\r\n")
|
||
b.WriteString("Sec-WebSocket-Key: " + key + "\r\n")
|
||
b.WriteString("Sec-WebSocket-Version: 13\r\n")
|
||
if deviceGatewayToken != "" {
|
||
b.WriteString("X-API-Key: " + deviceGatewayToken + "\r\n")
|
||
}
|
||
for k, vv := range r.Header {
|
||
kl := strings.ToLower(k)
|
||
if kl == "upgrade" || kl == "connection" || kl == "sec-websocket-key" || kl == "sec-websocket-version" || kl == "host" || kl == "x-api-key" || kl == "authorization" {
|
||
continue
|
||
}
|
||
for _, v := range vv {
|
||
b.WriteString(k + ": " + v + "\r\n")
|
||
}
|
||
}
|
||
b.WriteString("\r\n")
|
||
if _, err := upConn.Write([]byte(b.String())); err != nil {
|
||
http.Error(w, "ws upstream write: "+err.Error(), http.StatusBadGateway)
|
||
return
|
||
}
|
||
|
||
// 读上游 101 响应
|
||
br := bufio.NewReader(upConn)
|
||
resp, err := http.ReadResponse(br, nil)
|
||
if err != nil {
|
||
http.Error(w, "ws upstream response: "+err.Error(), http.StatusBadGateway)
|
||
return
|
||
}
|
||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||
http.Error(w, "ws upstream status: "+resp.Status, http.StatusBadGateway)
|
||
return
|
||
}
|
||
|
||
// 客户端 hijack:把 101 响应头写给客户端并接管双向连接
|
||
hj, ok := w.(http.Hijacker)
|
||
if !ok {
|
||
http.Error(w, "hijack not supported", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
clientConn, brw, err := hj.Hijack()
|
||
if err != nil {
|
||
return
|
||
}
|
||
defer clientConn.Close()
|
||
|
||
// 向上游 101 响应头转发给客户端
|
||
if err := resp.Write(brw); err != nil {
|
||
return
|
||
}
|
||
if err := brw.Flush(); err != nil {
|
||
return
|
||
}
|
||
|
||
// 双向透传(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() {
|
||
wb := bufio.NewWriter(clientConn)
|
||
io.Copy(wb, br)
|
||
wb.Flush()
|
||
errCh <- struct{}{}
|
||
}()
|
||
<-errCh
|
||
}
|
||
|
||
// splitHostPort 拆分 addr 为 host/port;无端口时返回 ok=false。
|
||
func splitHostPort(addr string) (string, string, bool) {
|
||
if strings.Contains(addr, ":") {
|
||
h, p, err := net.SplitHostPort(addr)
|
||
if err == nil {
|
||
return h, p, true
|
||
}
|
||
}
|
||
return addr, "", false
|
||
}
|