Files
HomeAgent/internal/plugins/webui/handler_files_test.go
JianFeeeee 0afa84a13f 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 覆盖。
2026-08-26 09:17:07 +08:00

84 lines
2.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}
}
}