Cherry Studio 知识库 V2 迁移深度解析:KnowledgeVectorMigrator 向量存储迁移全流程
【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio
导读
KnowledgeVectorMigrator是 Cherry Studio V2 数据迁移流水线中专用于知识库向量数据的迁移器:它将 v1 时代按知识库(base)独立的embedjs向量数据库,重建为 v2 时代每个知识库一套 7 表结构的index.sqlite索引存储(KnowledgeIndexStore)。本文以迁移器文档与 KnowledgeVectorMigrator.ts 源码为准,完整讲解其数据源、目标存储、六大核心转换、内存与文件安全契约、校验规则与跳过语义,帮助你理解一次"不重新向量化"的向量库迁移是如何在字节级、路径级和内存级三个维度上保证与运行时一致的。读完本文,你将掌握 Cherry Studio 知识库 V2 迁移的设计取舍、目录展开向量重归属机制、以及迁移器如何防御 Windows 文件锁与 V8 堆内存溢出。
迁移器在 V2 迁移流水线中的位置
KnowledgeVectorMigrator注册于 migratorRegistry.ts:
new KnowledgeMigrator(), new KnowledgeVectorMigrator(), new ChatMigrator(),其类定义声明:
readonly id = 'knowledge_vector' readonly name = 'KnowledgeVector' readonly description = 'Rebuild legacy knowledge vectors into the per-base index.sqlite store' readonly order = 3.5几个关键位置语义:
order = 3.5:它严格排在KnowledgeMigrator(负责把 Redux + Dexie 导出迁移进knowledge_base/knowledge_item表)之后执行,因为向量迁移必须依赖前者产出的已迁移库与条目行。- 两阶段模型:与流水线中所有迁移器一致,它实现
prepare()与execute()两个阶段——prepare()只读、只规划、只统计;execute()才真正重建存储并落库。 - 共享数据(
ctx.sharedData):它通过三个共享键从KnowledgeMigrator接手映射关系:KNOWLEDGE_BASE_ID_REMAP_SHARED_DATA_KEY(legacy base id → 迁移后 base id)、KNOWLEDGE_ITEM_ID_REMAP_SHARED_DATA_KEY(legacy item id → 迁移后 item id)、KNOWLEDGE_DIRECTORY_CHILD_LOADER_REMAP_SHARED_DATA_KEY(迁移后 base id → loader id → 目录展开子条目 id)。
注意:
KnowledgeMigrator在 order 1.8 只对 legacy 向量库做count(*)加一次length(vector)探测,而KnowledgeVectorMigrator在 order 3.5 要逐行读取pageContent/uniqueLoaderId/vector并重解 base 重映射——因此"探测通过、真正读取时失败"(如损坏页、瞬时文件锁、重映射断裂)只会发生在 3.5,源码markBaseUnmigrated的注释明确说明了这一可达路径。
数据源与目标存储
数据源
| 数据 | 来源 | 文件/路径 |
|---|---|---|
| 迁移后的知识库身份与维度 | SQLiteknowledge_base | knowledge_base表 |
| 迁移后的知识条目身份 | SQLiteknowledge_item | knowledge_item表 |
| Legacy loader 元数据 | Reduxknowledge.bases[].items[] | ReduxStateReader.getCategory('knowledge') |
| Legacy 分块向量 | 每库独立的 legacy 向量库 | ctx.sources.knowledgeVectorSource.openBase(base.id)(流式) |
源码中,Redux 状态读取为:
const knowledgeState = ctx.sources.reduxState.getCategory<LegacyKnowledgeStateWithLoaders>('knowledge') const migratedBases = await ctx.db.select().from(knowledgeBaseTable)路径安全要求:source reader 由MigrationContext以ctx.paths.knowledgeBaseDir初始化,必须读取迁移解析出的 v1 userData 路径,而非 v2 路径注册表或app.getPath();KnowledgeVectorMigrator自身也应持续使用 reader 抽象,而不是内联拼接向量库路径。这与KnowledgeMigrator文档中的 Path Safety 约束一致(README-KnowledgeMigrator.md)。
目标存储
- 目标位置为迁移后 base 的运行时路径:
{knowledgeBaseDir}/{migratedBaseId}/.cherry/index.sqlite,即每库一套 7 表索引存储。 - 构建路径是
createKnowledgeIndexStoreAtPath——与运行时KnowledgeVectorStoreService打开存储所用的同一个工厂(driver → schema →ensureIndexMeta→KnowledgeIndexStore),随后按条目调用KnowledgeIndexStore.rebuildMaterial。因此迁移产物与运行时自己构建的存储逐字节一致:每个迁移条目对应一个material,其 legacy 分块成为该 material 的search_unit。
源码中的布局常量与注释明确了运行时路径的唯一事实来源:
// Runtime vector store + material layout — source of truth: // src/main/features/knowledge/pathStorage.ts // (CHERRY_META_DIR / VECTOR_STORE_FILE / MATERIAL_ROOT_DIR). Runtime opens // {knowledgeBaseDir}/{baseId}/.cherry/index.sqlite by the migrated (new) base id... const KNOWLEDGE_META_DIR = '.cherry' const KNOWLEDGE_VECTOR_STORE_FILE = 'index.sqlite' const KNOWLEDGE_MATERIAL_ROOT_DIR = 'raw'测试文件 KnowledgeVectorMigrator.test.ts 专门用runtimeVectorStorePath(baseId)与runtimeMaterialPath(baseId, relativePath)两个镜像函数做读回断言——"如果迁移器写到了运行时不会打开的位置,测试就会失败",这正是该回归测试要防御的 bug。
六大核心转换
1. Loader 身份重映射
- 无嵌入模型的可恢复失败库在 base 级被跳过:它们保留 SQLite 中的 base/items 行,用户选择新模型后必须重建。
uniqueLoaderId不作为持久化字段保留,而是被解析回迁移后的knowledge_item(即material_id)。uniqueIds[]优先级高于 legacy 单值uniqueId(源码buildLoaderTargetMap中先遍历uniqueIds,非空即用之)。- 一条 legacy 向量行只有在能映射到已存在的 V2
knowledge_item.id时才有效;未映射的 legacy 行被视为无效索引残留而非必须保留的业务数据(源码classifyVectorRow中的unmapped_loader分支明确注释了这一点)。
2. 可索引条目过滤
- 只有映射到可索引 V2 条目类型的向量才会迁移,可索引类型为
file、url、note(源码常量INDEXABLE_KNOWLEDGE_ITEM_TYPES = new Set<KnowledgeItemType>(['file', 'url', 'note']))。 - 被 v1 索引的
directory的容器级向量,正常路径下会被重归属到按每个嵌入 loader id 合成的 per-file 子条目(一个file子条目对应一个嵌入 loader id,见KnowledgeMigrator.expandLegacyDirectoryItem),从而文件夹无需重新向量化即可保持可搜索。 - 容器级向量仅作为兜底被跳过并告警——即 legacy 向量源不可读,或某嵌入文件没有可迁移向量时——此时文件夹被保留为迁移失败墓碑(
directory_not_migrated)。 - 该过滤不会从
knowledge_item删除directory行,只是阻止容器级向量写入 V2 存储。 - 目录子条目的合成逻辑来自
collectDirectoryGroups:每个子条目携带groupId = 容器id,而迁移只在目录展开子条目上设置非空groupId,因此按行分组可捕获 base 的全部子条目。源码特意强调不能从 loader 重映射的值来推导分组,因为两个重叠/重复的 v1 文件夹共享同一文件的 loader id 时,KnowledgeMigrator的 last-write-wins 重映射只保留后一个子条目,按行扫描才能把未分到向量的子条目也正确降级。 - 独立条目与目录的 loader 冲突:
collectStandaloneLoaderOwners防止目录重归属"偷走"独立条目的向量——md5(path)会让独立添加的文件与文件夹内的文件碰撞同一个 loader id;解析时独立条目保持向量所有权,并记录directory_child_loader_conflict告警。
3. Material 组装(Route A——保留 v1 切分)
- 每个迁移条目一个
material;其relative_path通过共享的toMaterialRelativePath助手从迁移后的knowledge_item推导,与运行时索引任务完全一致——文件使用其存储的relativePath(有处理产物时用处理产物路径)。 - 迁移的 url 则改为固定指向为它在
raw/下物化的快照文件(由content.text加 OKF frontmatter 印章生成),替换助手原本会返回的 item-id 虚拟路径。 - 存储填充其余行(
current_content_hash、时间戳);没有origin/index_policy/file_ext列,content不携带text_format。 - 条目的 legacy 分块正文按 legacy 读取顺序拼接为单一规范
content.text,以文档分隔符(\n\n,源码常量DOCUMENT_SEPARATOR)连接;每个分块成为一个search_unit,其[char_start, char_end)切片精确切回自身正文,并附带 body 的search_text行;unit_index为条目的读取顺序。
function buildMigratedUnits(pageContents: string[]): RebuildMaterialInput['units'] { const units: RebuildMaterialInput['units'] = [] let cursor = 0 pageContents.forEach((pageContent, index) => { if (index > 0) cursor += DOCUMENT_SEPARATOR.length const charStart = cursor const charEnd = cursor + pageContent.length cursor = charEnd units.push({ unitType: 'chunk', unitIndex: index, charStart, charEnd }) }) return units }- 这是合成拼接而非重新切分:第一次真正的 reindex 会用运行时切分器重新切分并收敛。迁移按库记录日志。
4. 嵌入复用(零重新向量化)
- legacy
vector载荷从F32_BLOB解码为Float32Array(端到端保持,驻留大小仅为number[]的一半),通过encodeVectorBlob(raw little-endian float32)写入embedding表,以正文的embedding_text_hash为键。字节与运行时编码完全一致,因此无需重新向量化,存储可跨引擎移植。 - 相同分块正文(库内或跨库)折叠为一行
embedding(hashEmbeddingText去重 + INSERT OR IGNORE)。 - 不支持的向量编码在
unsupported_vector_encoding下跳过,与真正缺失载荷分开统计。 - 向量长度与库记录
dimensions不一致的在dimension_mismatch下跳过,而不是破坏整个库的暴力余弦扫描。
5. 身份再生成
- legacy 分块行 id 不重用;
unit_id/content_hash/search_text_id由存储根据 material id、内容与偏移确定性推导。
6. 身份戳
ensureIndexMeta写入单一meta身份行(schema 版本 + base id),使运行时打开存储时无需重新引导,并在base_id不匹配时拒绝被换入/外来的index.sqlite。- 构建契约快照(嵌入模型、维度、切分器配置哈希)有意不存储——模型/维度变更会创建新库,切分器变更会重建派生索引。
内存契约:最多驻留"一个条目的文本 + 一批向量"
prepare() 阶段
prepare()将每个库的 legacy 行流式扫描一次(openBase().reader.iterateRows()),只保留:
- 每个条目的 rowid 列表;
- 计数(
expectedUnitCount、expectedEmbeddingCount、sourceRowCount); - 按原因聚合的跳过统计(采样封顶,绝不每个被拒行一条消息);
- 每个 url/note 条目的预留快照路径(通过向量无关的文本投影 point-read 该条目自身行推导)。
PreparedBasePlan接口刻意不持有任何向量或分块文本。它保留的唯一按分块结构是 rowid 列表,且该列表确实随迁移总块数线性增长:每个分块一个 JS number(packed SMI 数组)——100 万分块时约 10.5 B/分块,1000 万时约 8.0 B/分块(node --expose-gc下围绕Map<string, number[]>构建的堆增量实测)。
文档给出的典型语料估算(注意这是估算而非代码强制上限):
- 迁移器只要求
dimensions为正整数,不约束pageContent长度或库的分块数; - 普通语料下,一个分块在 legacy 库中占用 ≥5 KB(1024 维 4 KB float32 向量 + ~1 KB 文本);
- 因此计划开销约为 legacy 文件磁盘大小的 1/500:100 MB 计划意味着 ~50 GB 的 v1 向量库;
- 耗尽 V8 的 4 GB old-space 需要约 4 亿分块(约 2 TB 源数据);
- 观测到的最重语料约 7.5 万分块 ≈ 0.7 MB 计划。
低维度、极短分块的语料会改变这一比例。保留该计划是可接受的权衡——它换来的是对 legacy 库按条目重读(而非第二次全量扫描)——其合理性来自 v1 "一次嵌入调用一个分块"的索引方式从未产生过接近该量级的数据,而非存在代码检查的上限。
execute() 阶段
execute()一次只重读一个条目:
- 其文本整体通过向量无关的列投影读取(
loadTextRowsByRowids——内容 schema 每个 material 一行文本,因此拼接文本在 schema 不变的前提下不可再压缩); - 其向量以固定≤500 rowid的 point-read 批次读取(
loadRowsByRowids),由rebuildMaterial的Iterable嵌入输入在其写事务内惰性拉取。
const VECTOR_STREAM_BATCH_SIZE = 500 // 与 reader 的 IN 子句批次 ROWID_BATCH_SIZE 对齐function* iterateLegacyEmbeddingBatches(...): Generator<RebuildMaterialEmbeddingInput> { for (let offset = 0; offset < rowids.length; offset += VECTOR_STREAM_BATCH_SIZE) { const batch = rowids.slice(offset, offset + VECTOR_STREAM_BATCH_SIZE) const rows = reader.loadRowsByRowids(batch) if (rows.length !== batch.length) { throw new Error(...) } // fail-closed ... } }- url/note 快照文件在该条目的回合内写入,绝不跨库缓冲;
- 解码后的向量端到端保持
Float32Array(驻留为number[]的一半)。
OOM 历史(源码注释完整记录):最初在 prepare→execute 之间保留每个库的所有 material,峰值达到所有库向量之和(28 库语料耗尽 V8 堆);后续的"按库重读"仍一次性加载整个库——单个大库(六位数分块 × 高维度)就能独自耗尽,且因迁移每次启动都从头重跑而循环崩溃。OOM 回归防护测试(KnowledgeVectorMigrator.test.ts)钉死了PreparedBasePlan的精确键集合与每个阶段的读取形态(prepare 流式;execute 经投影读文本、向量按 ≤500 rowid 分批——一个 501 分块的条目必须以 500 + 1 到达)。
此外,流式扫描每 1024 行(STREAM_ROW_YIELD_INTERVAL)主动让出事件循环一次,避免六位数行的库在整个扫描期间冻结迁移 UI。
文件安全契约
原地构建,不做 rename
迁移器将每个重建的存储直接构建在运行时路径{migratedBaseId}/.cherry/index.sqlite——无临时文件、无 rename。文档解释了 rename 曾是迁移在 Windows 上最脆弱的环节:
- WAL 模式下打开的 SQLite 存储可能使文件在
close()之后仍被锁住(wal_checkpoint(TRUNCATE)、PERSIST_WAL与数秒等待都不可靠地释放它); - 叠加 AV/搜索索引器扫描刚写入的文件(无
DELETEshare); MoveFileEx需要源文件的DELETE权限,于是 rename 抛出EBUSY/EPERM,库丢失存储。重试只能等掉瞬态 AV 扫描,等不掉永不释放的句柄。
原地构建完全移除了移动操作——close()后残留的任何锁都无害,因为没有东西移动或重开该文件;运行时只在 bootstrap 之后很久才打开它。
源码中对文件系统瞬态锁做了多层防御:
const REMOVE_RETRY_OPTIONS = { recursive: true, force: true, maxRetries: 5, retryDelay: 100 } as const const TRANSIENT_FS_LOCK_CODES = new Set(['EPERM', 'EACCES', 'EBUSY']) const FS_RETRY_MAX_ATTEMPTS = 8 const FS_RETRY_BASE_DELAY_MS = 100 const FS_RETRY_MAX_DELAY_MS = 1500retryOnTransientFsLock以指数退避(100ms → 最大 1500ms)保护raw/快照writeFile与mkdir(fs.rm的内置重试不覆盖写/建目录,且其 errno 集合显著遗漏了EACCES)。
崩溃原子性的取舍
原地构建以 rename 的崩溃原子性(中断的构建会在运行时路径留下部分索引)换取上述鲁棒性。这是安全的,因为:
- 迁移闸门对任何未完成的运行都从头重跑(
verifyAndClearNewTables()清行、KnowledgeMigrator重新铸新 uuid 目录、运行时绝不在迁移中途打开存储); - 每库 catch 会在捕获失败时擦除部分产物;
- 崩溃遗留的目录永远不会被
knowledge_base行引用,因此永远不会被挂载(与 rename 路径产出的死磁盘一致); - 目标处的
index.sqlite{-wal,-shm}家族在(重新)构建前被移除(带可存活 EBUSY 的重试),WAL 通过PRAGMA wal_checkpoint(TRUNCATE)折回主文件,运行时打开的是一个自包含的存储。
全有或全无发布 + 快照 pin
一个库全有或全无地发布,将其 url/note 行固定到快照是发布的最后一步——只有该事务提交后存储才算提升。此前任何抛出(构建、关闭或 pin 事务)都会擦除索引并将库标记为可恢复的failed。
- 零 pin 不得记为成功:一个
completed的 url/note 条目没有relativePath是deriveConceptId守卫的不变量违例,且没有任何机制修复它——index-documents跳过completed条目,所以 ensure-snapshot 永远不会重新捕获;已完成的迁移永远不会重跑。 - 标记为
failed的库转而走恢复流程:把条目重新加入一个新库(其行不是completed,因此确实会被索引)。 - 该恢复只有一种情况有损:url 条目会重新抓取实时页面,而不是重读本迁移器写入但从未 pin 的
raw/快照——因此已经失效的链接不可恢复。
legacy 源永不移动或删除
v1 的 legacy embedjs 库({knowledgeBaseDir}/{legacyBaseId})绝不被移动或删除。每个迁移库获得新 uuid,重建的 V2 存储位于不同路径({migratedBaseId}/.cherry/index.sqlite),永不与 legacy 扁平路径冲突——legacy 源无需搬迁。用户在失败、放弃甚至成功的迁移后回滚到 v1,知识库依然可用。
重试天然幂等
legacy 源仍在原处,重试直接通过KnowledgeVectorSourceReader重读原始 legacy 库。
当前限制
单库失败非致命
单个库的失败不会中止整个迁移。当一个库的向量存储无法重建——无论prepare()完全无法读取/映射其 legacy 源,还是execute()在重建或发布中途失败——该库被跳过、标记为可恢复的failed/missing_vector_store行(UI 据此显示 re-index 入口),失败以告警呈现,execute()仍返回success: true,其余库继续迁移。
- 这防止单个库的迁移错误把用户挡在应用之外:失败库在 UI 内恢复,而不是重跑整个迁移。
- 整个迁移只在结构性/完整性错误(迁移器抛出,或
validate()的对账失败)时整体失败,绝不会因某库数据无法迁移而失败。 - 唯一例外:如果库自身的
failed/missing_vector_store标记无法写入应用数据库,迁移器抛出且整个迁移失败。该标记是阻止库进入运行时打开路径的唯一手段,因此迁移记录为completed却没有该标记,会让库永久无法搜索且无路可退;失败反而让下次启动从头重跑。
磁盘空间不回收
成功迁移后,v1 legacy 向量库(以及复制的 legacy 上传文件)以孤儿形式留在磁盘上,磁盘空间不回收。回收被有意留给未来一个独立的清理步骤,并以用户放弃 v1 为前提。
校验
每个成功迁移的库,重建存储的行数必须与 prepare 时的规划一致:
material数 == 每个迁移条目一个;search_unit数 == 这些条目保留的分块总数;embedding数 == 库内不同的 embedding 文本哈希数;- 每个
search_text行必须解析到已存储的embedding(零未覆盖单元)——这是重建自愈不变量在迁移期的形态:没有支撑向量的单元会静默缺席向量搜索。
流式批次还有 fail-closed 的漂移防御(iterateLegacyEmbeddingBatches):行数/解码/维度不匹配会抛出(中止事务);文本扫描之后pageContent变化产生的哈希没有单元推导它,存储的 embedding 覆盖检查会将其转为回滚。
被跳过的数据清单
库级跳过
- 不在迁移后
knowledge_base中的库; - 标记为
failed或embeddingModelId = null的库; dimensions无效的库;- legacy 库文件缺失、解析为目录、不含
vectors表、或扫描中途不可读的库; - 迁移后 id 无法映射回 legacy 知识库 id 的库;
- 重映射后的 legacy id 不在 legacy Redux 状态中的库。
行级跳过
uniqueLoaderId无法映射到迁移后knowledge_item.id的向量行;- 映射到非索引容器类型(如
directory)的向量行——仅限兜底路径(legacy 源不可读,或嵌入文件没有可迁移向量);正常路径会重归属到 per-file 子条目; vector载荷缺失或为空的向量行;- 载荷存在但通过不支持的运行时编码暴露的向量行;
- 向量长度与库记录
dimensions不一致的向量行。
跳过的语义
被跳过的库根本不产生存储,这与产生空存储不同。因此上述每个库级跳过都会在execute()的 flush 中被标记为可恢复的failed/missing_vector_store,而非completed,仅两个例外:根本没有迁移后knowledge_base行的库(prepare()迭代的是迁移后的行,无行可标记),以及已为failed的库(它携带自己的错误——如missing_embedding_model——不得覆盖)。
标记的原因:没有任何机制把knowledge_base行与文件系统对账。一个没有存储的completed库,首次运行时打开会创建并缓存一个空白的index.sqlite,搜索结果永远为空,没有失败徽标也没有恢复入口。恢复是 UI 内的 restore 流程(把条目重新加入新库并重新向量化),而不是重跑迁移——迁移本身仍然完成。
如果某库下所有 legacy 向量行都被跳过,该库仍会被规划,其重建的 V2 存储预期为空(仅 schema +meta行)。这是有意为之:只有能被证明属于迁移后knowledge_item行的向量在 V2 中才保持有效。
目录孤儿降级
源码中还有一层精细的孤儿降级逻辑:目录展开产生的子条目若未获得任何向量(markEmptyDirectoryChildren),会被降级为failed/directory_not_migrated,其虚拟路径源无法 reindex;若一组内所有子条目都为空,容器本身也被降级。整个库被跳过时(markDirectoryGroupsFullyOrphaned),容器与所有子条目一起降级。降级写入(flushDirectoryDegradations)按 500 一批(DEGRADE_UPDATE_CHUNK)执行,规避 SQLite 绑定变量上限;且降级失败只记告警不抛出——丢失降级只留下某库内几个空文档,而丢失库标记会让整个库静默不可搜索,两者的爆炸半径完全不同(KnowledgeVectorMigrator.ts)。
测试与回归保障
KnowledgeVectorMigrator.test.ts(约 3300 行)围绕迁移器的核心不变量构建了多层回归防线:
- 运行时路径读回断言:
runtimeVectorStorePath镜像pathStorage.ts的{root}/{baseId}/.cherry/index.sqlite布局,runtimeMaterialPath镜像raw/布局——迁移器写到运行时不会打开的位置即失败; - OOM 回归守卫:钉死
PreparedBasePlan的精确键集合与每阶段读取形态(prepare 流式;execute 按 ≤500 rowid 分批,501 分块条目必须以 500 + 1 到达); - 字节级等价验证:读回时用
KnowledgeIndexStore、encodeVectorBlob、hashEmbeddingText等运行时同一套实现断言存储内容; - skip 语义验证:
unmapped_loader、dimension_mismatch、缺失/空载荷、非索引容器类型、legacy 库缺失/目录/非 embedjs 格式等各分支; - directory 展开与重归属验证:legacy sitemap 映射为 url、目录容器级向量重归属到合成子条目、冲突时的独立条目所有权保留。
与 KnowledgeMigrator 的协作与恢复流
KnowledgeVectorMigrator无法脱离其前置迁移器独立理解。两阶段的协作要点:
- 目录展开:
KnowledgeMigrator.expandLegacyDirectoryItem为每个嵌入 loader 源合成一个file子条目,并记录 loader id → 子条目 id 的映射,KnowledgeVectorMigrator据此把容器级向量重归属到子条目(README-KnowledgeMigrator.md)。 - 缺失嵌入模型的可恢复失败:当 legacy 库的嵌入模型 id 存在于 Redux 但不存在于 V2
user_model(例如ollama::dengcao/Qwen3-Embedding-0.6B:Q8_0),KnowledgeMigrator将其保存为embeddingModelId = null、status = failed、error = missing_embedding_model;KnowledgeVectorMigrator因嵌入模型契约无法验证而跳过该库的向量。恢复走knowledge:restore-base运行时流程——以源库配置与所选模型创建新库、仅复制根条目、运行正常索引流——原始 failed 库保留供用户确认后再删除。 - 维度解析:
KnowledgeMigrator从 legacy 向量库vectors表的首个非空 blob 长度(length(vector)/4)解析dimensions;KnowledgeVectorMigrator用同一值做dimension_mismatch校验。
总结
KnowledgeVectorMigrator是 Cherry Studio V2 迁移中最能体现"与运行时逐字节一致"设计哲学的实现:复用createKnowledgeIndexStoreAtPath工厂与rebuildMaterial,使迁移产物与运行时自建存储无差别;通过 F32_BLOB →Float32Array→encodeVectorBlob的端到端浮点通道实现零重新向量化;以"prepare 流式 + execute 分批 point-read"的内存契约消化了 OOM 崩溃历史;以原地构建 + 全有或全无发布 + 快照 pin 处理了 Windows 文件锁与崩溃原子性的矛盾;最终以 500 分批的写入、封顶采样的告警和逐库非致命的失败语义,保证任意规模的语料都能在不阻塞用户的前提下完成迁移。
【免费下载链接】cherry-studioAI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs项目地址: https://gitcode.com/GitHub_Trending/ch/cherry-studio
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考