mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-27 21:03:16 +00:00
feat(webui): agent 可向 webui 发送图片/文件,前端内联展示与下载
输出通道能力升级:webui 通道从 CapText(1) 扩展为 CapText|CapFile|CapImage(7),agent 经 output_send__webui 即可发送 image/file(此前仅文本)。 服务端: - stageWebFile 把本地路径文件拷贝到 <data>/webui_files/<hex>.<ext> (随机名防猜测、危险扩展名强制 .bin),http(s) URL 直接透传不落盘 - 新增 GET /files/<name>(requireWeb 与 dashboard 同鉴权):扩展名 白名单映射 Content-Type,图片/音视频 inline、其余 attachment 下载, nosniff + 路径穿越拒绝 - SSE agent_output 事件携带 output_type/url/size 字段 前端(dashboard.html): - channel_output 识别附件消息:image 渲染内联预览(点击原图)、 file 渲染下载卡片(含大小);formatBytes 人性化显示 典型场景:agent 把 remotedevice 回传的录像/截图(device_media/*.mp4) 直接发给 webui,用户在聊天里看到视频预览或一键下载。 新增 TestStageWebFileAndDownload / TestHandleFilesAuth 覆盖。
This commit is contained in:
@ -13,6 +13,8 @@ import (
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@ -703,6 +705,8 @@ func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/plugins/", h.requireAPI(h.handlePluginByID))
|
||||
// 设备网关(可配置反代到 remotedevice;默认禁用,未启用时返回 404)
|
||||
mux.HandleFunc("/api/v1/device/", h.requireAPI(h.handleDeviceGatewayProxy))
|
||||
// agent 发送的文件下载(webui_files 中转目录;requireWeb 与 dashboard 同源同鉴权)
|
||||
mux.HandleFunc("/files/", h.requireWeb(h.handleFiles))
|
||||
mux.HandleFunc("/v1/chat/completions", h.requireAPI(h.handleOpenAICompletions))
|
||||
mux.HandleFunc("/", h.requireWeb(h.handleStatic))
|
||||
}
|
||||
@ -2228,6 +2232,79 @@ func (h *Handler) handlePluginByID(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleStatic(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
Reference in New Issue
Block a user