mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 18:08:04 +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)。
85 lines
2.2 KiB
Go
85 lines
2.2 KiB
Go
package webui
|
||
|
||
import (
|
||
"os"
|
||
"strings"
|
||
|
||
"net/http"
|
||
"path/filepath"
|
||
)
|
||
|
||
// 附件面:/files/ 中转移目录与 /uploads/ 用户上传目录的下载。
|
||
|
||
// handleFiles 服务 /files/<name>:仅限 webui_files 中转目录内的文件,
|
||
// 防路径穿越(name 必须是纯文件名),Content-Type 按扩展名白名单映射。
|
||
func (h *Handler) handleFiles(w http.ResponseWriter, r *http.Request) {
|
||
if webFilesDir == "" {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
name := strings.TrimPrefix(r.URL.Path, "/files/")
|
||
if name == "" || strings.Contains(name, "/") || strings.Contains(name, "\\") || strings.Contains(name, "..") {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
fp := filepath.Join(webFilesDir, name)
|
||
f, err := os.Open(fp)
|
||
if err != nil {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
defer f.Close()
|
||
st, err := f.Stat()
|
||
if err != nil || st.IsDir() {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
ct := contentTypeByExt(strings.ToLower(filepath.Ext(name)))
|
||
w.Header().Set("Content-Type", ct)
|
||
// 图片内联展示;其他类型 attachment 下载。X-Content-Type-Options 防 MIME sniff。
|
||
if strings.HasPrefix(ct, "image/") || strings.HasPrefix(ct, "video/") || strings.HasPrefix(ct, "audio/") {
|
||
w.Header().Set("Content-Disposition", "inline; filename="+name)
|
||
} else {
|
||
w.Header().Set("Content-Disposition", "attachment; filename="+name)
|
||
}
|
||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||
w.Header().Set("Cache-Control", "private, max-age=3600")
|
||
http.ServeContent(w, r, name, st.ModTime(), f)
|
||
}
|
||
|
||
func contentTypeByExt(ext string) string {
|
||
switch ext {
|
||
case ".png":
|
||
return "image/png"
|
||
case ".jpg", ".jpeg":
|
||
return "image/jpeg"
|
||
case ".gif":
|
||
return "image/gif"
|
||
case ".webp":
|
||
return "image/webp"
|
||
case ".bmp":
|
||
return "image/bmp"
|
||
case ".mp4":
|
||
return "video/mp4"
|
||
case ".webm":
|
||
return "video/webm"
|
||
case ".mp3":
|
||
return "audio/mpeg"
|
||
case ".wav":
|
||
return "audio/wav"
|
||
case ".ogg":
|
||
return "audio/ogg"
|
||
case ".pdf":
|
||
return "application/pdf"
|
||
case ".zip":
|
||
return "application/zip"
|
||
case ".json":
|
||
return "application/json"
|
||
case ".txt", ".log", ".md":
|
||
return "text/plain; charset=utf-8"
|
||
default:
|
||
// 未知类型强制二进制流 + nosniff,绝不内联执行
|
||
return "application/octet-stream"
|
||
}
|
||
}
|