|
|
9f7d91c219
|
refactor(memory)!: 删除 CAS 包与第二套召回(块 union 化第四阶段:收尾)
块 union 化前三阶段已让块自带负载、回填存量字节。本提交拆掉 CAS 本身:
`internal/memory/media/`(2100 行)+ `QueryMediaScored`(第二套召回)
+ `media_refs`/`ref_count` 引用账本 + `internal/sdk/media.go`(MediaAPI)
+ `SDKConfig.Media` / `SetMediaStore` 接线。删完 CAS 才真正不存在。
## 为何可删(不是"先删了再说")
- **MediaAPI 在生产早已是死的**:`SDKConfig.Media` 自 86572d13 起从未被设置,
`sdk.Media()` 恒为 nil,插件 digest 路径一直走"内容寻址库不可用"分支。
删的是死代码,不是活路径。
- **第二套召回确无调用方**:`QueryMediaScored` 仅被 media 包自身的测试引用,
跨包引用为 0(块召回走 `RecallBlocks`)。
- **heldMediaDigests 补丁**(crossmodal 靠它防召回"无处可归的媒体")
已在 86572d13 删除 —— 两套存储才需要的那种补丁,随存储一起没了。
## 替代路径:块引用取代 digest
插件取媒体从 `sdk.Media().Get(digest)` 改为 `sdk.Memory().MediaBytes(ref)`:
- **新增 `MemoryAPI.MediaBytes(ref) (data, mime, ok, err)`**(内置接口,非 pubsdk):
按块引用(完整块 ID 或唯一前缀)取字节;`ok=false` 而非 error ——
"这个引用没有媒体"是调用方能处理的常态。
- **seemedia / multimodal**:`digest` 参数语义改为**媒体块引用**,工具描述同步。
path/url 两条来源不变(生产实际只用这两条)。
- **webui 知识上传**:媒体字节不再 Put 进 CAS 再拿 digest,而是作为
`KnowledgeMediaRef.Data` 随引用携带,由 knowledge.Write 落进条目目录
(与 content.md 同生共灭,Remove 的 RemoveAll 天然清掉)。
- **回填命令去依赖**:cmd/homed-media-backfill 不再 import 待删的 media 包,
改为直接读**冻结的历史布局** blobs/<d[:2]>/<d[2:]> 并自校验 sha256。
依赖待删代码的迁移工具会在删包那刻失效 —— 迁移必须比被迁移的东西活得久。
## 判据(全部做过变异测试)
- `TestMemoryBytes_ByBlockRef`:完整 ID / 唯一前缀可取回且逐字节一致;
不存在→ok=false;过短前缀(歧义)→ok=false;无负载块→ok=false;空引用不 panic。
变异(不校验 ok,把不存在当有)当场被 3 条断言抓住。
- `TestE2EMediaBytesAgainstBackfilledDB`(-tags e2e,读真回填库):
10/10 块经 MediaBytes 取回的字节与块 data 的 sha256 完全一致。
- `TestKnowledgeUploadedMediaRetrievable` 重写为**条目目录自带字节**判据
(不再是"向存储按 digest 读回");`TestKnowledgeUploadMediaNeedsNoStore`
改为"上传无外部存储依赖也必须成功"(旧的 503 语义已随 CAS 消失)。
## 静态确认
`QueryMediaScored` / `internal/memory/media` import / `MediaAPI` /
`sdk.Media()` / `SetMediaStore` / `heldMediaDigests` 全仓均为 0 处
(仅剩 medialoop.go 一句历史注释)。
回归:memory / sdk / agent-core / knowledge / plugin / webui / seemedia /
multimodal 八包全绿;全仓生产编译 GREEN。
|
2026-10-12 16:52:43 +08:00 |
|
|
|
16309c3307
|
feat(seemedia): 多模态直视插件——让模型一手看图/看视频/听音频 (#6)
* feat(seemedia): 新增多模态直视插件,让模型一手看图/看视频/听音频
## 动机
现有 multimodal(describe_image / transcribe_audio / ocr_image)走的是**二手**路:
插件自己起一次 VLM 子调用,把结果**文字**喂回主模型。省 token、能入记忆正文、
主模型非视觉也能用,但模型看到的是「别人转述的描述」,细节丢失不可逆。
缺的是**一手**路:让模型主动把原始像素/帧/音频投给**自己**去判读。本插件补上。
## 工具(三模态,用户决策 image+video+audio)
- `see_image`:path/digest/url → image_url 块,经 SetToolBlocks 直投主模型
- `see_video`:ffmpeg 抽帧(默认 6 帧,上限 10,对齐内核 modalfallback 约束)
→ 多张 image_url 块一起投;探时长成功走均匀定位抽帧,失败退单遍 thumbnail
- `hear_audio`:path/digest/url → audio_url 块直投
三者都 `ParallelSafe:false`:SetToolBlocks 写的是「当前 call」的块,并发会串台。
## 与 multimodal 并存(用户决策 coexist)
职责分工,不互相取代:
- 要省 token / 要把结论写进记忆 / 主模型不支持该模态 → describe_image(二手文字)
- 要模型亲自判读细节(看清截图、逐帧看操作) → see_*(一手原始媒体)
## 媒体来源(用户决策 path+digest+url 全支持)
- path:设备/工具回传的落盘文件(local_screensee、browser_screenshot、mc 截图…)
- digest:用户上传已入 CAS 的媒体,sdk.Media().Get 取回,不在内核↔插件间传 base64
- url:http(s) 直链;图/音频原样透传(不下载、不入 CAS),视频交 ffmpeg 原生读 http
## 直接进记忆(记忆载体已块化,天然打通)
seemedia 经 SetToolBlocks 注入的块,内核 task.go captureBlockMedia 处**无条件**
落进 CAS 并转成一等记忆块挂到本轮 ContextEvent(「无论直视还是回退,媒体本体都该
进记忆」)。所以一手看到的图/帧**同时**进了记忆,之后能按多模态向量被
doc_query / memory_recall 召回。本插件不写任何入库代码,复用既有块化链路。
★ 边界(刻意取舍):captureBlockMedia 只对 data URL 入库,http(s) URL 被跳过
(不下载、不入记忆、不算向量,规避 SSRF/超时)。即 path/digest 来源可召回,
url 来源只是模型当场看到但不进记忆。要让 http 媒体长期可召回,先落盘再传 path。
## 形态:内置插件(用户决策 builtin)
与 multimodal 并排 internal/plugins/seemedia,经 all.go init() 注册。代价是延续
「内核进程内读任意本地文件」这个 a02d8b00 本想消除的张力——但 multimodal 已是
内置且同样读 path,保持一致比单独走子进程沙箱更不易让人困惑;日后收紧应两者同搬。
## 判据(可杀变异,实测)
plugin_test.go:
- url 透传且拒 file://(去掉 isHTTPURL 校验 → FAIL)
- path 优先于 digest、url 优先于 path,且 path 命中时不碰 CAS
- 无来源报错且信息可操作(指明 path/digest/url)
- 文件错误带路径、空文件明说「空」
- clampFrames 把帧数夹到 [1,10]、非正回默认(超限不夹 → FAIL)
- 三工具不得声明 ParallelSafe:true(防 SetToolBlocks 串台)
9/9 单测通过;本机 ffmpeg/ffprobe 已就位;全插件 + homed(-tags onnxruntime)
构建通过;internal/plugins 装配冒烟通过。
* test(seemedia): 补集成/压力/变异测试(16 判据,5 变异全杀)
上一个提交只带 9 个纯逻辑单测(来源解析、clamp、MIME)。抽帧是本插件唯一
真正驱动外部进程、且会并发的部分,必须真跑 ffmpeg 压一压:
- 真视频 → 请求 N 帧就拿到 N 帧,每帧校验 PNG 魔数(1/3/6/10 四档)
- ffprobe 置空 → 走单遍兜底仍能拿到帧
- 损坏/非视频文件 → 如实报错,不 panic、不返半成品
- 已取消的 ctx → ffmpeg 必须立刻失败(取消传播真生效)
- 并发 16 路抽帧 → 不串台、不泄漏临时目录(前后计数比对)
- digest 视频 → 真 CAS 取字节落临时文件,cleanup 后必须删掉
- 静态判据:所有子进程必须 exec.CommandContext,不得裸 exec.Command
为什么超时判据从「计时」改成「已取消 ctx」:合成视频抽帧本来就快(~0.1s),
「1ns 超时」和「忽略超时」在计时上分不出——实测这种变异杀不掉,是判据设计
缺陷。改用已取消 ctx 的行为判据(CommandContext 在 ctx 已取消时 Start 即失败)
后,忽略 ctx 的变异被杀。
变异实测(5/5 全 FAIL,还原后全绿):
A clampFrames 超限不夹 → TestClampFrames FAIL
B url 分支去掉 isHTTPURL → TestResolveMediaURL_URLPassthrough FAIL
C cleanup 不删临时文件 → TestResolveVideoInput_DigestCleanup FAIL
D grabFrameAt 忽略 ctx → TestFrameGrabHonorsCanceledContext FAIL
E 抽帧数写死 1 → TestExtractFrames_RealVideoExactCount FAIL
16/16 通过;`-tags onnxruntime` 全量 homed 构建通过,三工具在册。
---------
Co-authored-by: JianFeeeee <JianFeeeee@users.noreply.gitcode.com>
|
2026-10-11 13:06:23 +08:00 |
|