mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
feat(remotedevice): screensee 落盘化,回路径而非自动送模型
## 为什么
原行为把截图的 base64 直接送去视觉模型自动描述。三个问题:
1. **上下文成本**:96KB 截图 ≈ 13 万 token 的内联 base64
2. **源不可控**:生产实证 AUTO 把大图路由到不支持视觉的源,模型回
「没有收到截图」——而我们无法区分「图没到」与「模型看不了」
3. **agent 无法决策**:不知道图存不存在,也就不知道该 describe 还是该 ocr
## 现在
截图落盘 → 回 {file,mime,size,sha256,width,height} →
agent 显式调 describe_image(path=…)/ocr_image(path=…)。
需要哪个能力由它自己判断,provider 参数也可控。
工具描述同步改写,明说「本工具不会自动描述任何内容,两步缺一不可」——
否则模型会以为拿到 file 就等于看到了画面。
## 实现
- `Registry.SaveInlineMedia(name, payload, mimeHint)`:
解 data URL / 裸 base64 → 落盘 → 回可核对的元信息
- 文件名用**内容摘要**后缀(`<name>_<sha16>.<ext>`):
截屏/录像会被反复抓取,用 reqID 命名会把同一张图存成多份,
用固定名则不同内容互相覆盖 —— 摘要两个问题都避免
- `decodeInlineMedia` / `imageDimensions`:JPEG SOFn 与 PNG IHDR
解尺寸,不解码全图;认不出的格式**省略字段而非填 0**
- 落盘失败当**工具失败**上抛,不静默退回内联:
图已在手上却拿不到路径,agent 既没有描述也没有可查证产物
- 旧路径保留在 `auto_describe=true`(调试用)
顺带把 cmd_data_end 的媒体落盘逻辑也换成同一个方法(原来内联了
一份 MkdirAll+WriteFile 的副本)。
## 判据
新增 inline_media_test.go,六个判据经四个变异自证会红:
- 变异 A:写入截半的字节但元信息照填 → 红(只查「文件存在」会放过)
- 变异 B:尺寸写死 0 占位 → 红(元信息「看起来有」但不可信)
- 变异 C:sha256 换成假的 → 红
- 变异 D:未配置 mediaDir 时静默降级 → 红
★ 变异 C 第一次跑出「无输出」,查下来是**编译失败不是被杀**
(`sum`/`hex` 变成未使用)。这正是本项目记过的坑:
编译失败 ≠ 被杀。改写成能编译的注入后确认被杀。
## 验证
- go build ./... / go vet / go test ./internal/... 全绿
- go test -race remotedevice + multimodal 无数据竞争
- 旧判据按新契约更新时保留原意:
TestScreenseE2E_SuccessReturnsDescription 现在同时验两条路径
(默认不出 description 且不调 handler;auto_describe 才出)
- 部署后:39 插件加载、0 panic/fatal、plugin=multimodal
- graph.db integrity ok,blocks 2787 / relations 980 未动
- 快照 /home/newqqagent/memory/pre-screensee-persist-20261006-091536/
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -11,6 +11,7 @@ import (
|
||||
"image/png"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@ -187,6 +188,8 @@ func TestScreenseE2E_VisualFailureIsErrorNotDescription(t *testing.T) {
|
||||
reg := NewRegistry()
|
||||
token := "tk-visual-fail"
|
||||
reg.SetAcceptToken(func(p string) bool { return p == token })
|
||||
// auto_describe 需要落盘目录先存在(默认路径也会落盘)
|
||||
reg.SetMediaDir(t.TempDir())
|
||||
|
||||
dev := &devicectlDevice{reg: reg}
|
||||
// 视觉源返回 403(MCP/screensee 生产上真实发生过)
|
||||
@ -224,7 +227,7 @@ func TestScreenseE2E_VisualFailureIsErrorNotDescription(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
res, err := dev.Execute("screensee", map[string]interface{}{"device_id": "vf-dev"})
|
||||
res, err := dev.Execute("screensee", map[string]interface{}{"device_id": "vf-dev", "auto_describe": true})
|
||||
|
||||
// ★ 必须报错
|
||||
if err == nil {
|
||||
@ -285,6 +288,9 @@ func TestScreenseE2E_SuccessReturnsDescription(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
// ★★ 2026-10-06:默认路径**不再自动描述**,改为落盘 + 回路径。
|
||||
// 旧行为保留在 auto_describe=true 下,另有一处判据覆盖。
|
||||
reg.SetMediaDir(t.TempDir())
|
||||
res, err := dev.Execute("screensee", map[string]interface{}{"device_id": "vo-dev"})
|
||||
if err != nil {
|
||||
t.Fatalf("成功路径被破坏: %v", err)
|
||||
@ -293,11 +299,33 @@ func TestScreenseE2E_SuccessReturnsDescription(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("返回类型变了: %T", res)
|
||||
}
|
||||
if m["description"] != "屏幕上是一个终端窗口" {
|
||||
t.Errorf("description 不对: %v", m["description"])
|
||||
fp, _ := m["file"].(string)
|
||||
if fp == "" {
|
||||
t.Fatalf("默认路径必须回 file,实际返回: %v", m)
|
||||
}
|
||||
if got, err := os.ReadFile(fp); err != nil || len(got) == 0 {
|
||||
t.Errorf("file 应指向落盘的截图: err=%v", err)
|
||||
}
|
||||
if _, hasDesc := m["description"]; hasDesc {
|
||||
t.Error("默认路径不应再返回 description(内容由 describe_image 显式取)")
|
||||
}
|
||||
if gotLen != 0 {
|
||||
t.Error("默认路径不应调用视觉描述 handler(模型必须显式调 describe_image)")
|
||||
}
|
||||
|
||||
// ★ auto_describe=true 走旧路径(调试用):handler 被调,description 回来。
|
||||
res2, err := dev.Execute("screensee", map[string]interface{}{
|
||||
"device_id": "vo-dev", "auto_describe": true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("auto_describe 路径被破坏: %v", err)
|
||||
}
|
||||
m2, _ := res2.(map[string]interface{})
|
||||
if m2["description"] != "屏幕上是一个终端窗口" {
|
||||
t.Errorf("auto_describe 应返回 description,实际: %v", m2["description"])
|
||||
}
|
||||
if gotLen == 0 {
|
||||
t.Error("handler 没收到 dataURL")
|
||||
t.Error("auto_describe 下 handler 应收到 dataURL")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user