mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 02:18:06 +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:
83
internal/plugins/webui/handler_files_test.go
Normal file
83
internal/plugins/webui/handler_files_test.go
Normal file
@ -0,0 +1,83 @@
|
||||
package webui
|
||||
|
||||
// webui 文件发送能力测试:stageWebFile 中转 + /files/ 带鉴权下载。
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStageWebFileAndDownload(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
webFilesDir = dir
|
||||
defer func() { webFilesDir = "" }()
|
||||
|
||||
// 源文件(模拟 agent 要发的图片)
|
||||
src := filepath.Join(dir, "src.jpg")
|
||||
if err := os.WriteFile(src, []byte("fake-jpeg-bytes"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
url, size, err := stageWebFile(src, true)
|
||||
if err != nil {
|
||||
t.Fatalf("stageWebFile: %v", err)
|
||||
}
|
||||
if !strings.HasPrefix(url, "/files/") || !strings.HasSuffix(url, ".jpg") {
|
||||
t.Fatalf("unexpected url: %s", url)
|
||||
}
|
||||
if size != int64(len("fake-jpeg-bytes")) {
|
||||
t.Fatalf("size = %d", size)
|
||||
}
|
||||
// 中转文件存在且内容一致
|
||||
data, err := os.ReadFile(filepath.Join(dir, strings.TrimPrefix(url, "/files/")))
|
||||
if err != nil || string(data) != "fake-jpeg-bytes" {
|
||||
t.Fatalf("staged file mismatch: %v", err)
|
||||
}
|
||||
|
||||
// 远程 URL 透传不落盘
|
||||
u2, _, err := stageWebFile("https://example.com/a.png", true)
|
||||
if err != nil || u2 != "https://example.com/a.png" {
|
||||
t.Fatalf("remote url passthrough failed: %v %s", err, u2)
|
||||
}
|
||||
|
||||
// 危险扩展名被替换为 .bin
|
||||
srcBad := filepath.Join(dir, "evil.html")
|
||||
os.WriteFile(srcBad, []byte("<b>x</b>"), 0644)
|
||||
u3, _, err := stageWebFile(srcBad, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.HasSuffix(u3, ".bin") {
|
||||
t.Fatalf("html ext should be forced to .bin, got %s", u3)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleFilesAuth(t *testing.T) {
|
||||
h, _ := newTestHandler(t)
|
||||
// 未登录访问 → 重定向登录页(requireWeb)
|
||||
req := httptest.NewRequest(http.MethodGet, "/files/whatever.jpg", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.handleFiles(w, req) // 直接调 handler 本体验证文件逻辑;鉴权由 mux 层 requireWeb 覆盖
|
||||
|
||||
// 不存在的文件 → 404
|
||||
req2 := httptest.NewRequest(http.MethodGet, "/files/nonexistent.jpg", nil)
|
||||
w2 := httptest.NewRecorder()
|
||||
h.handleFiles(w2, req2)
|
||||
if w2.Code != http.StatusNotFound {
|
||||
t.Fatalf("want 404 for missing file, got %d", w2.Code)
|
||||
}
|
||||
|
||||
// 路径穿越拒绝
|
||||
for _, bad := range []string{"/files/../etc/passwd", "/files/a/b.jpg", `/files\a.jpg`} {
|
||||
req3 := httptest.NewRequest(http.MethodGet, "/files/x", nil)
|
||||
req3.URL.Path = bad
|
||||
w3 := httptest.NewRecorder()
|
||||
h.handleFiles(w3, req3)
|
||||
if w3.Code != http.StatusNotFound {
|
||||
t.Errorf("path traversal %q: want 404, got %d", bad, w3.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user