mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-15 12:15:16 +00:00
refactor(memory)!: 块 union 化,彻底淘汰 CAS 第一阶段(生产代码)
块自带负载(MemoryBlock.Data),删 PayloadDigest;检索纯向量,媒体随块 同生灭。生产代码全绿;CAS 专属测试将在下一提交随判据重写。 - memory: Data BLOB + migration9;blockColumns 不含 data(召回只比向量); MediaBlockID 内容派生 ID;BlockByIDPrefix/BlockData 按需加载 - sdk: memory_impl 去 media.Store;MemoryBlockView PayloadDigest→HasData - core: mediaref/graphmedia/crossmodal/medialoop 全部改块语义; captureBlockMedia 直接产块;resolveMediaRefs 按块 ID 解析 - knowledge: 媒体字节随条目目录自带(File+Data),删 mediaGet/mediaPut - webui: graphBlockView/pulse payload_digest→has_data - bootstrap/registry: 去 MediaStore 接线(SetMediaStore 空实现,待⑥删包)
This commit is contained in:
@ -85,10 +85,9 @@ func TestDenseCrossModalRecall(t *testing.T) {
|
||||
img: map[string][]float64{"__default": eby, "d-cat": eax},
|
||||
}
|
||||
s.SetDenseSpace(mm)
|
||||
s.SetMediaGetter(fakeMedia{byDigest: map[string][]byte{"d-cat": []byte("d-cat")}})
|
||||
|
||||
if err := s.AddWithMedia("cat-photo", "猫 图片 说明", []KnowledgeMediaRef{
|
||||
{Digest: "d-cat", MIME: "image/png", Kind: "image"},
|
||||
{File: "cat.png", MIME: "image/png", Kind: "image"},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -224,9 +223,8 @@ func TestDenseUnsupportedModalityStillTextEmbedded(t *testing.T) {
|
||||
imgErr: map[string]error{"d-audio": vector.ErrModalityUnsupported},
|
||||
}
|
||||
s.SetDenseSpace(mm)
|
||||
s.SetMediaGetter(fakeMedia{byDigest: map[string][]byte{"d-audio": []byte("d-audio")}})
|
||||
|
||||
if err := s.AddWithMedia("song", "一首歌", []KnowledgeMediaRef{{Digest: "d-audio", MIME: "audio/mpeg"}}); err != nil {
|
||||
if err := s.AddWithMedia("song", "一首歌", []KnowledgeMediaRef{{File: "a.mp3", MIME: "audio/mpeg"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
k := s.items["song"]
|
||||
@ -289,13 +287,12 @@ func TestAttachMediaRecomputesDense(t *testing.T) {
|
||||
img: map[string][]float64{"__default": {0, 1}, "d-x": eax},
|
||||
}
|
||||
s.SetDenseSpace(mm)
|
||||
s.SetMediaGetter(fakeMedia{byDigest: map[string][]byte{"d-x": []byte("d-x")}})
|
||||
|
||||
if err := s.Add("k", "正文"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
before := append([]float64{}, s.items["k"].Dense...)
|
||||
if err := s.AttachMedia("k", KnowledgeMediaRef{Digest: "d-x", MIME: "image/png"}); err != nil {
|
||||
if err := s.AttachMedia("k", KnowledgeMediaRef{File: "x.png", MIME: "image/png"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
after := s.items["k"].Dense
|
||||
|
||||
@ -8,7 +8,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/media"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
)
|
||||
|
||||
// ===== 从目录导入知识 =====
|
||||
@ -370,37 +370,74 @@ func (s *Store) has(name string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// MediaPutter 是把字节写进媒体库的能力(由 media.Store 实现)。
|
||||
// putMediaFile 读一个媒体文件进条目引用(字节随 Data 瞬态携带,
|
||||
// 由 Write 落盘到条目目录)。
|
||||
//
|
||||
// 为何用接口而不是直接依赖 *media.Store:Store 已经有 SetMediaGetter
|
||||
// 这类注入点,导入沿用同一形状,测试才能用假实现,不必拉起真媒体库。
|
||||
type MediaPutter interface {
|
||||
Put(data []byte, meta media.Item) (string, error)
|
||||
}
|
||||
|
||||
// putMediaFile 把一个媒体文件复制进媒体库,返回条目用的引用。
|
||||
//
|
||||
// 媒体库未接入时返回错误而不是静默跳过 —— 但调用方对错误只记警告:
|
||||
// 媒体是可选增强,正文不该因为图片入不了库而整个条目失败。
|
||||
// ★ 2026-10-11 CAS 淘汰:不再写入全局媒体库 —— 字节是条目自带数据,
|
||||
// 与 content.md 同目录同生灭。读取失败返回错误,调用方只记警告跳过该媒体
|
||||
// (正文不该因为图片入不了库而整个条目失败)。
|
||||
func (s *Store) putMediaFile(path, kind string) (KnowledgeMediaRef, error) {
|
||||
if s.mediaPut == nil {
|
||||
return KnowledgeMediaRef{}, fmt.Errorf("媒体库不可用")
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return KnowledgeMediaRef{}, err
|
||||
}
|
||||
mime := mimeByExt(filepath.Ext(path))
|
||||
digest, err := s.mediaPut.Put(data, media.Item{
|
||||
Kind: media.Kind(kind),
|
||||
MIME: mime,
|
||||
Size: int64(len(data)),
|
||||
OriginPath: path,
|
||||
})
|
||||
if err != nil {
|
||||
return KnowledgeMediaRef{}, err
|
||||
// File 名取源文件名:同目录内天然唯一(同名正文+同名媒体本就是一对)。
|
||||
return KnowledgeMediaRef{
|
||||
Digest: memory.MediaBlockID(mime, data), // 内容身份:与媒体块 ID 同构
|
||||
MIME: mime,
|
||||
Kind: kind,
|
||||
File: filepath.Base(path),
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// writeMediaFiles 把引用携带的瞬态字节落进条目目录。
|
||||
//
|
||||
// 只写引用里带 Data 的条目(目录导入路径);字节已在盘上的(如模型引用
|
||||
// 已有媒体)直接跳过。File 为空时补足。
|
||||
func (s *Store) writeMediaFiles(dir string, refs []KnowledgeMediaRef) error {
|
||||
for i := range refs {
|
||||
r := &refs[i]
|
||||
if len(r.Data) == 0 && r.File != "" {
|
||||
if _, err := os.Stat(filepath.Join(dir, r.File)); err == nil {
|
||||
continue // 字节已在盘上
|
||||
}
|
||||
}
|
||||
if len(r.Data) == 0 {
|
||||
continue // 没字节也没文件:引用悬空,留给 denseFor 静默降级
|
||||
}
|
||||
if r.File == "" {
|
||||
ext := extByMIME(r.MIME)
|
||||
r.File = fmt.Sprintf("media_%d%s", i, ext)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, r.File), r.Data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extByMIME 按常见 MIME 给扩展名(writeMediaFiles 补 File 名用)。
|
||||
func extByMIME(mime string) string {
|
||||
switch mime {
|
||||
case "image/png":
|
||||
return ".png"
|
||||
case "image/jpeg":
|
||||
return ".jpg"
|
||||
case "image/gif":
|
||||
return ".gif"
|
||||
case "image/webp":
|
||||
return ".webp"
|
||||
case "audio/mpeg":
|
||||
return ".mp3"
|
||||
case "audio/wav":
|
||||
return ".wav"
|
||||
case "video/mp4":
|
||||
return ".mp4"
|
||||
default:
|
||||
return ".bin"
|
||||
}
|
||||
return KnowledgeMediaRef{Digest: digest, MIME: mime, Kind: kind}, nil
|
||||
}
|
||||
|
||||
// mimeByExt 按扩展名给 MIME。够用即可:媒体库靠 MIME 决定能否解码
|
||||
|
||||
@ -2,14 +2,11 @@ package knowledge
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/media"
|
||||
)
|
||||
|
||||
// ===== 从目录导入知识 =====
|
||||
@ -400,27 +397,9 @@ func TestImportDirDoesNotFlushPerItem(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 媒体导入:图片按字节复制进媒体库,条目挂 digest 引用。
|
||||
//
|
||||
// 用假 MediaPutter 而不是真媒体库:这里要验的是「调用了 Put、
|
||||
// 且把返回的 digest 挂到条目上」,不是媒体库自己的落盘与去重
|
||||
// (那是 media 包的判据,不该在这里重测)。
|
||||
type fakePutter struct {
|
||||
puts [][]byte
|
||||
seq int
|
||||
}
|
||||
|
||||
func (f *fakePutter) Put(data []byte, _ media.Item) (string, error) {
|
||||
f.puts = append(f.puts, data)
|
||||
f.seq++
|
||||
return fmt.Sprintf("digest%02d", f.seq), nil
|
||||
}
|
||||
|
||||
func TestImportDirMediaAttachedToEntry(t *testing.T) {
|
||||
src := mkSrc(t, map[string]string{"note.md": "看图", "note.png": "PNGDATA"})
|
||||
s, root := newImportStore(t)
|
||||
fp := &fakePutter{}
|
||||
s.SetMediaPutter(fp)
|
||||
|
||||
st, err := s.ImportDir(ImportOptions{Dir: src, IncludeMedia: true})
|
||||
if err != nil {
|
||||
@ -429,16 +408,21 @@ func TestImportDirMediaAttachedToEntry(t *testing.T) {
|
||||
if st.Media != 1 {
|
||||
t.Errorf("应导入 1 个媒体,实际 %d(%+v)", st.Media, st)
|
||||
}
|
||||
if len(fp.puts) != 1 || string(fp.puts[0]) != "PNGDATA" {
|
||||
t.Errorf("媒体字节未原样复制进媒体库:%v", fp.puts)
|
||||
// 媒体字节随条目落盘(CAS 淘汰后:字节是条目自带数据,与 content.md 同目录)
|
||||
bytesOnDisk, err := os.ReadFile(filepath.Join(root, "note", "note.png"))
|
||||
if err != nil {
|
||||
t.Fatalf("媒体字节未落盘到条目目录:%v", err)
|
||||
}
|
||||
// digest 引用要落到条目的侧车文件里
|
||||
if string(bytesOnDisk) != "PNGDATA" {
|
||||
t.Errorf("媒体字节失真:%q", bytesOnDisk)
|
||||
}
|
||||
// 引用要落到条目的侧车文件里(File 指向同目录文件)
|
||||
data, err := os.ReadFile(filepath.Join(root, "note", mediaSidecarName))
|
||||
if err != nil {
|
||||
t.Fatalf("媒体侧车未写入:%v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "digest01") {
|
||||
t.Errorf("侧车里没有 Put 返回的 digest:%s", data)
|
||||
if !strings.Contains(string(data), "note.png") {
|
||||
t.Errorf("侧车里没有媒体文件名:%s", data)
|
||||
}
|
||||
// 内存条目也要挂上(否则稠密路算不出这条的图向量)
|
||||
k := s.items["note"]
|
||||
@ -447,26 +431,33 @@ func TestImportDirMediaAttachedToEntry(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// include_media 未开启时不该碰媒体库。
|
||||
// include_media 未开启时不该把媒体落进条目目录。
|
||||
func TestImportDirMediaSkippedWhenNotRequested(t *testing.T) {
|
||||
src := mkSrc(t, map[string]string{"note.md": "看图", "note.png": "PNGDATA"})
|
||||
s, _ := newImportStore(t)
|
||||
fp := &fakePutter{}
|
||||
s.SetMediaPutter(fp)
|
||||
s, root := newImportStore(t)
|
||||
|
||||
if _, err := s.ImportDir(ImportOptions{Dir: src}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(fp.puts) != 0 {
|
||||
t.Errorf("未开 include_media 却写了媒体库:%v", fp.puts)
|
||||
if _, err := os.Stat(filepath.Join(root, "note", "note.png")); !os.IsNotExist(err) {
|
||||
t.Errorf("未开 include_media 却落了媒体文件")
|
||||
}
|
||||
}
|
||||
|
||||
// 媒体入不了库时,正文条目仍应成功(媒体是增强,不是前提)。
|
||||
// 媒体读不了时,正文条目仍应成功(媒体是增强,不是前提)。
|
||||
func TestImportDirMediaFailureDoesNotFailEntry(t *testing.T) {
|
||||
src := mkSrc(t, map[string]string{"note.md": "正文", "note.png": "PNG"})
|
||||
src := mkSrc(t, map[string]string{"note.md": "正文"})
|
||||
s, _ := newImportStore(t)
|
||||
s.SetMediaPutter(failPutter{})
|
||||
// 造一个指向不存在文件的媒体候选:ImportDir 收集目录时按扩展名识别,
|
||||
// 读不出的文件走 putMediaFile 错误路径,只跳过该媒体。
|
||||
bad := filepath.Join(src, "broken.png")
|
||||
if err := os.WriteFile(bad, []byte("x"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Chmod(bad, 0000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = os.Chmod(bad, 0644) })
|
||||
|
||||
st, err := s.ImportDir(ImportOptions{Dir: src, IncludeMedia: true})
|
||||
if err != nil {
|
||||
@ -475,13 +466,4 @@ func TestImportDirMediaFailureDoesNotFailEntry(t *testing.T) {
|
||||
if st.Imported != 1 {
|
||||
t.Errorf("正文条目应仍导入成功,实际 %d(%+v)", st.Imported, st)
|
||||
}
|
||||
if st.Media != 0 {
|
||||
t.Errorf("媒体应计入 0,实际 %d", st.Media)
|
||||
}
|
||||
}
|
||||
|
||||
type failPutter struct{}
|
||||
|
||||
func (failPutter) Put([]byte, media.Item) (string, error) {
|
||||
return "", fmt.Errorf("磁盘满")
|
||||
}
|
||||
|
||||
@ -49,6 +49,15 @@ type KnowledgeMediaRef struct {
|
||||
Digest string `json:"digest"`
|
||||
MIME string `json:"mime"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
// File 是媒体字节在**条目目录内**的相对文件名(随条目生灭)。
|
||||
//
|
||||
// ★ 2026-10-11 CAS 淘汰:媒体字节不再寄存于全局内容寻址库,而是作为
|
||||
// 条目自带数据落在 entry 目录里(与 content.md、.media.json 并列)。
|
||||
// Remove 的 os.RemoveAll 天然把它清掉,不留孤儿。
|
||||
File string `json:"file,omitempty"`
|
||||
// Data 是写入期的瞬态字节:putMediaFile → Write 之间搬运,Write 落盘后清空。
|
||||
// json:"-" 保证它不进 .media.json 侧车(侧车只记引用,不记字节)。
|
||||
Data []byte `json:"-"`
|
||||
}
|
||||
|
||||
// KnowledgeMediaRef 的别名,照顾内部可读性。
|
||||
@ -126,8 +135,6 @@ type Store struct {
|
||||
mu sync.RWMutex
|
||||
items map[string]*Knowledge
|
||||
|
||||
// mediaPut 是媒体写入器(ImportDir 复制媒体时用),见 SetMediaPutter。
|
||||
mediaPut MediaPutter
|
||||
|
||||
indexPath string
|
||||
denseCachePath string
|
||||
@ -157,8 +164,7 @@ type Store struct {
|
||||
// (feature → {docID: weight}),稠密向量会把倒排表退化成全量特征桶,
|
||||
// 同时破坏 TF-IDF 语义(vector.MultimodalEmbedder 的注释已明言)。
|
||||
// 因此稠密路自成一等路,与另两路并列,不混进任何一个 Store。
|
||||
dense vector.MultimodalEmbedder
|
||||
mediaGet MediaGetter
|
||||
dense vector.MultimodalEmbedder
|
||||
}
|
||||
|
||||
// mediaSidecarName 是每条知识目录下存放媒体引用的文件名。
|
||||
@ -187,12 +193,6 @@ type denseCache struct {
|
||||
Entries map[string]denseCacheEntry `json:"entries"`
|
||||
}
|
||||
|
||||
// MediaGetter 让知识库能取回媒体字节以计算嵌入,而不依赖具体的媒体存储包。
|
||||
// 取不到(或未接线)时,该条目退化为纯文本嵌入——而不是整条不可用。
|
||||
type MediaGetter interface {
|
||||
Get(digest string) ([]byte, error)
|
||||
}
|
||||
|
||||
func NewStore(root string) *Store {
|
||||
return &Store{
|
||||
root: root,
|
||||
@ -224,25 +224,6 @@ func (s *Store) SetDenseSpace(ds vector.MultimodalEmbedder) {
|
||||
s.maybeLoadDenseCacheLocked()
|
||||
}
|
||||
|
||||
// SetMediaGetter 注入媒体取回器(用于为 Media 块算嵌入)。
|
||||
// 不注入时多媒体条目仍可入库,只是退化为纯文本嵌入。
|
||||
func (s *Store) SetMediaGetter(g MediaGetter) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.mediaGet = g
|
||||
}
|
||||
|
||||
// SetMediaPutter 注入媒体写入器(ImportDir 把图片/音视频复制进媒体库时用)。
|
||||
//
|
||||
// 与 SetMediaGetter 分开:一个是取(算嵌入时读回媒体块),一个是存
|
||||
// (目录导入时收字节)。合成一个接口会强迫测试同时实现两侧。
|
||||
// 不注入时 ImportDir 仍能导入正文,只是媒体被跳过并记原因。
|
||||
func (s *Store) SetMediaPutter(p MediaPutter) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.mediaPut = p
|
||||
}
|
||||
|
||||
// denseEnabled 报告稠密路是否可用(供 Stats/自证与分支判断)。
|
||||
// 调用方必须已持锁。
|
||||
func (s *Store) denseEnabled() bool {
|
||||
@ -265,21 +246,24 @@ func (s *Store) denseFor(k *Knowledge) []float64 {
|
||||
if tv, err := s.dense.VectorizeDense(k.Name + " " + k.Content); err == nil && len(tv) == dim {
|
||||
parts = append(parts, tv)
|
||||
}
|
||||
dir := filepath.Dir(k.Path)
|
||||
for _, m := range k.Media {
|
||||
if v := s.mediaDense(m); v != nil {
|
||||
if v := s.mediaDense(m, dir); v != nil {
|
||||
parts = append(parts, v)
|
||||
}
|
||||
}
|
||||
return vector.FuseVectors(parts...)
|
||||
}
|
||||
|
||||
// mediaDense 取回媒体字节并嵌入。任何一步拿不到就返回 nil——
|
||||
// mediaDense 读条目目录里的媒体字节并嵌入。任何一步拿不到就返回 nil——
|
||||
// 媒体缺失不应让整条知识失去文本向量。
|
||||
func (s *Store) mediaDense(m KnowledgeMediaRef) []float64 {
|
||||
if !s.denseEnabled() || s.mediaGet == nil || m.Digest == "" {
|
||||
//
|
||||
// ★ 2026-10-11 CAS 淘汰:字节直接从条目目录的 File 读,不再经 mediaGet。
|
||||
func (s *Store) mediaDense(m KnowledgeMediaRef, dir string) []float64 {
|
||||
if !s.denseEnabled() || m.File == "" {
|
||||
return nil
|
||||
}
|
||||
data, err := s.mediaGet.Get(m.Digest)
|
||||
data, err := os.ReadFile(filepath.Join(dir, m.File))
|
||||
if err != nil || len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
@ -365,8 +349,7 @@ func (s *Store) DenseStats() map[string]interface{} {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
out := map[string]interface{}{
|
||||
"enabled": s.denseEnabled(),
|
||||
"media_getter": s.mediaGet != nil,
|
||||
"enabled": s.denseEnabled(),
|
||||
}
|
||||
if s.denseEnabled() {
|
||||
fp := s.dense.Fingerprint()
|
||||
@ -746,6 +729,12 @@ func (s *Store) Write(in KnowledgeEntryInput) error {
|
||||
return fmt.Errorf("write knowledge: %w", err)
|
||||
}
|
||||
|
||||
// 媒体字节落进条目目录(CAS 淘汰后,字节是条目自带数据)。
|
||||
// 失败只跳过该媒体并记录——正文已落盘,图片写不进顶多搜不到图。
|
||||
if err := s.writeMediaFiles(dir, in.Media); err != nil {
|
||||
log.Printf("[knowledge] media bytes write error for %s: %v", in.Name, err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
k := &Knowledge{
|
||||
Name: id,
|
||||
|
||||
@ -8,9 +8,10 @@ import (
|
||||
"image/png"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/media"
|
||||
"github.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||||
"github.com/JianFeeeee/HomeAgent/pkg/embedding"
|
||||
)
|
||||
@ -67,10 +68,6 @@ func TestMultimodalEndToEndWithRealProvider(t *testing.T) {
|
||||
}
|
||||
t.Logf("真实 provider: dim=%d fp=%s loaded=%v", ds.Dim(), ds.Fingerprint(), ds.Loaded())
|
||||
|
||||
ms, err := media.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
s := NewStore(dir)
|
||||
if err := s.Start(); err != nil {
|
||||
@ -78,16 +75,19 @@ func TestMultimodalEndToEndWithRealProvider(t *testing.T) {
|
||||
}
|
||||
defer s.Stop()
|
||||
s.SetDenseSpace(ds)
|
||||
s.SetMediaGetter(ms)
|
||||
|
||||
img := mmTinyPNG(t)
|
||||
digest, err := ms.Put(img, media.Item{MIME: "image/png", Tool: "test"})
|
||||
if err != nil {
|
||||
if err := s.AddWithMedia("photo", "一只猫的照片", []KnowledgeMediaRef{
|
||||
{File: "photo.png", MIME: "image/png", Kind: "image"},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.AddWithMedia("photo", "一只猫的照片", []KnowledgeMediaRef{
|
||||
{Digest: digest, MIME: "image/png", Kind: "image"},
|
||||
}); err != nil {
|
||||
// 字节落进条目目录(CAS 淘汰后:媒体随条目生灭)
|
||||
if err := os.WriteFile(filepath.Join(dir, "photo", "photo.png"), img, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 重算该条稠密向量以纳入图片
|
||||
if err := s.AttachMedia("photo", KnowledgeMediaRef{File: "photo.png", MIME: "image/png"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.Add("plain", "无关的纯文本知识"); err != nil {
|
||||
@ -125,7 +125,6 @@ func TestMultimodalEndToEndWithRealProvider(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s2.SetDenseSpace(ds)
|
||||
s2.SetMediaGetter(ms)
|
||||
built, _ := s2.ReindexDense()
|
||||
t.Logf("重启后 ReindexDense built=%d(0=命中缓存)", built)
|
||||
if built != 0 {
|
||||
|
||||
@ -34,7 +34,6 @@ func TestDenseCacheAvoidsRecompute(t *testing.T) {
|
||||
s2 := NewStore(dir)
|
||||
_ = s2.Start()
|
||||
s2.SetDenseSpace(mm)
|
||||
s2.SetMediaGetter(fakeMedia{})
|
||||
for _, id := range s2.List() {
|
||||
if len(s2.items[id].Dense) == 0 {
|
||||
t.Fatalf("重启后 %s 未从缓存恢复稠密向量", id)
|
||||
|
||||
Reference in New Issue
Block a user