- 人工智能
- 大模型
- AI 应用
- 交互助手
- 本地部署
【免费下载链接】cherry-studio
🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端
本文围绕 Cherry Studio 开源仓库中的一份 breaking-change 记录(v2-refactor-temp/docs/breaking-changes/2026-08-27-embedding-model-download-moved.md)展开,解读知识库本地 Embedding 模型(Qwen3-Embedding-0.6B)下载方式的一次关键变更:模型文件不再由加载它的机器学习库(transformers.js)负责下载,而是改由应用自身完成。文章将结合仓库源码,说明这次变更背后的下载引擎实现、镜像与代理策略、旧目录自动迁移机制,以及它对普通用户和发布管理者的实际影响。
变更概览:一次 notice 级别的行为调整
该文档的 frontmatter 标记为category: changed、severity: notice,即这是一次行为变更类、低影响提示的改动,发布于 2026-08-27。核心变化只有一句话:知识库 Embedding 模型现在由应用自己下载,而不是由加载它的机器学习库下载。
具体到当前仓库,这条链路对应的是src/main/ai/localModel/目录下的"本地模型子系统":Embedding 模型(qwen3-embedding-0.6b)和 OCR 模型(pp-ocrv6-medium)的权重、分词器等文件,统一由应用侧的下载引擎(acquisition/目录)拉取,再由installation/LocalModelStorageService.ts管理磁盘状态。文档提到的新行为包括三点:
- 逐文件校验:每个文件到达时都做 SHA-256 校验;
- 断点续传:被中断的下载会保留已完成的部分,下次只补缺失文件,而不是全部重下;
- 代理一致性:下载走应用自身配置的代理,与 App 内其他下载行为完全一致。
为什么这样做:三个问题的闭环
文档的 "Why this matters to the user" 小节阐述了这次变更要解决的三个实际问题,每一处都能在源码中找到对应设计。
1. 代理设置不再失效
此前模型由机器学习库(transformers.js)自行下载,走的是库内部自己的网络栈,不经过应用配置的代理。结果是:用户在应用里配置的代理对其他地方都有效,唯独 Embedding 模型下载失效。
现在所有下载都经由src/main/ai/localModel/acquisition/downloadEngine.ts中的net.fetch——这是 Electron 主进程的网络模块,天然继承应用级代理配置。源码注释明确写道:
"The one way bytes enter the local-model directories"(这是字节进入本地模型目录的唯一途径)。
即模型文件与共享运行时 tarball 全部经过withMirrorFallback和streamToFileVerified两个函数,网络层统一、行为一致。
2. 损坏或被篡改的下载当场拒绝
文档指出,过去"被损坏或被拦截的下载"可能在下载时不被察觉,直到加载模型失败时才暴露。现在,校验发生在字节流经的途中,而不是事后。
streamToFileVerified(downloadEngine.ts)的实现要点:
const response = await net.fetch(url, { signal }) if (!response.ok || !response.body) throw new Error(`HTTP ${response.status} for ${url}`) const total = Number(response.headers.get('content-length')) || 0 const hash = crypto.createHash('sha256')数据通过一个Transform流边下载边更新 sha256 摘要,写盘目标是一个${dest}.tmp临时文件;当整个流结束后:
const digest = hash.digest('hex') if (digest !== sha256) { await prepared.abort() throw new Error(`sha256 mismatch for ${url}: expected ${sha256}, got ${digest}`) } await prepared.commit()只有摘要匹配才把临时文件原子性地重命名到最终位置。任何截断响应、LFS 指针、强制门户(captive portal)页面都会在校验环节被拒绝,且不会留下一个让就绪探测误判为"已安装"的假文件。
3. 失败后只补缺的部分
文档举例说明恢复下载"只获取仍然缺失的内容,而不是重新下载完整的约 614MB"。
这在两个层面实现。其一,LocalModelStorageService.pendingBundleFiles会逐文件比对磁盘现状,只返回缺失清单;其二,下载引擎对单个文件同样使用临时文件 + 原子提交,已完整落盘的文件不会再次抓取。614MB 这个数字也对应 catalog 中的权重文件描述:qwen3-embedding-0.6b的onnx/model_quantized.onnx的minBytes为 100,000,000(100MB)以上,配合weight: 585,可推知权重文件约占整个 bundle 绝大部分体积。
下载引擎的实现细节
下载引擎位于src/main/ai/localModel/acquisition/,由四个文件组成,职责清晰:
| 文件 | 职责 |
|---|---|
| modelSource.ts | 定义下载镜像源(HuggingFace / ModelScope)与区域偏好 |
| downloadEngine.ts | 底层网络原语:镜像回退、流式校验写盘、文本抓取 |
| bundleDownload.ts | 按 bundle 编排多文件下载与进度条 |
| tarballArtifact.ts | 共享原生运行时(onnxruntime-node)npm tarball 的获取与安装 |
镜像回退:一个坏镜像不能拖垮整个下载
withMirrorFallback(downloadEngine.ts)会依次尝试每个 URL,直到其中一个成功。关键设计是:不可达的镜像和返回损坏字节的镜像在此处失败方式完全相同——因为校验在attempt内部(streamToFileVerified抛出的 sha256 不匹配异常同样会被withMirrorFallback捕获并尝试下一个镜像),所以"活着但数据是坏的"镜像永远不可能让下载变成终态错误,只要另一个镜像还有好字节就能继续。
同时,取消(abort)不算镜像失败:被取消的下载必须停下来,而不是遍历剩余镜像列表重新发出也注定被取消的请求。
派生文件:先校验文本再改写落盘
并非所有文件都是原样落盘。bundleDownload.ts中的writeBundleFile区分两种情形:
if (!derivation) { await streamToFileVerified(url, dest, { sha256, signal, onProgress }) return } const fetched = await fetchTextVerified(url, { sha256, signal }) signal.throwIfAborted() await writeFileAtomic(dest, applyDerivation(derivation, fetched))带derivation的文件(目前只有 OCR 的paddle_dict_from_inference_yml)体积很小,是下载后需要改写再写入的配置类文件,因此走fetchTextVerified(抓取文本后校验 sha256)加writeFileAtomic(临时文件 + 原子写入,崩溃不会留下"看起来已安装"的半成品)。Embedding 模型本身不使用派生,四个文件全部直接流式校验落盘。
进度条:按文件权重加权
bundleDownload.ts的进度回调按BundleFile.weight加权(约等于文件 MB 数),因此进度条反映的是真实字节进度而非文件个数。对于跨多个文件的 bundle,权重还用于"已完成部分 + 当前文件内进度"的累计:
const totalWeight = files.reduce((sum, file) => sum + file.weight, 0) // ... onProgress?.((doneWeight + file.weight * fraction) / totalWeight)镜像源与区域策略
modelSource.ts把下载镜像建模为一张地址表,而非单一 base URL,原因在于 HuggingFace 与 ModelScope 的寻址差异:
const SOURCES: Record<ModelSourceId, ModelSource> = { huggingface: { remoteHost: 'https://huggingface.co', remotePathTemplate: '{model}/resolve/{revision}', revision: 'main' }, modelscope: { remoteHost: 'https://www.modelscope.cn', remotePathTemplate: 'models/{model}/resolve/{revision}', revision: 'master' } }两个镜像都暴露 HF 兼容的/<repo>/resolve/<revision>/<file>路由;区别是 ModelScope 把仓库嵌套在models/下、默认分支叫master(HuggingFace 叫main)。
区域偏好DownloadSourcePreference取'china-first' | 'global-first'两个值:china-first默认 ModelScope(国内访问 HuggingFace 困难),global-first默认 HuggingFace。偏好只在管理边界处依据出口区域(egress region)解析一次,而不是依据显示语言。镜像顺序则把区域默认源放在第一位、另一个作为回退:
export function modelSourceOrder(preference: DownloadSourcePreference): [ModelSourceId, ...ModelSourceId[]] { return defaultModelSourceId(preference) === 'modelscope' ? ['modelscope', 'huggingface'] : ['huggingface', 'modelscope'] }源码注释特别强调:推理从不查询这张表——模型按绝对路径加载,这正是"推理完全离线"的保证。这一点与"应用自管下载"的变更互为表里:下载是唯一联网环节,下载完成后的一切都发生在本地。
目录布局与旧布局自动迁移
当前布局与遗留布局
catalog 中 Embedding bundle 的目录定义(catalog.ts):
'qwen3-embedding-0.6b': { id: 'qwen3-embedding-0.6b', capability: 'embedding', installDirKey: 'feature.embedding.models', installSubdir: 'onnx-community/Qwen3-Embedding-0.6B-ONNX', legacyInstallSubdir: 'onnx-community/Qwen3-Embedding-0.6B-ONNX/master', requires: ['onnxruntime-node'], runtime: { dtype: 'q8' }, // 4 files... }两个要点值得展开:
installSubdir之所以以仓库名命名,是因为 transformers.js 从存放 config.json 的目录加载模型,而早期版本允许它把目录命名为仓库名。保持同样的布局,是"已安装的模型无需重新下载 614MB"的关键。legacyInstallSubdir末尾多了一层master/,这正是文档中"从 ModelScope 镜像下载的用户,模型多了一层master/目录"的由来:ModelScope 的默认分支是master,旧版 transformers.js 会把非main的 revision 追加进目录层级。
自动迁移:尽力而为,绝不半途
LocalModelStorageService.resolveInstalledDir(LocalModelStorageService.ts)的判定逻辑:当前布局完整 → 返回当前目录;当前布局不完整但遗留布局完整 → 触发liftLegacyInstall把遗留目录提升到当前布局;提升后再读一次两种布局,避免"提升中回滚也失败导致丢文件"的目录被交给调用方。
liftLegacyInstall(同文件 L127-L167)是严格"尽力而为"的:如果文件正被存活的推理 worker 占用导致移动失败,会记录警告并留在原地继续使用(fallback 零成本);但绝不会处于半完成状态——凡是已经移动的文件都会移回去,因为"一个横跨两种布局的安装会让两边都不完整,从而重新下载一个其实完全在磁盘上的模型"。
这与文档中发布管理说明完全吻合:
"如果移动无法完成(例如文件暂时被占用),模型会继续从原位置正常工作,应用稍后重试。任何情况下都不会删除或重新下载任何内容。"
断点续传与未完成下载的清理
只下载缺失文件
missingFilesIn用statSync检查每个文件的存在性与最小字节数(minBytes),pendingBundleFiles据此返回还需抓取的清单:
private missingFilesIn(bundle: ModelBundle, dir: string): BundleFile[] { return bundle.files.filter((file) => { const stat = fs.statSync(this.bundleFilePath(bundle, file, dir), { throwIfNoEntry: false }) return !stat?.isFile() || stat.size < file.minBytes }) }minBytes的语义值得注意:它是磁盘扫描的下限,足够捕获旧版(无校验时代)下载留下的截断文件,又不会因为上游修订号变动而强制重下。源码注释特别说明磁盘扫描永远不做 sha256 校验——每次状态查询都对约 700MB 权重做哈希会让查询慢到不可用;校验只发生在字节到达的下载路径上。
启动时的陈旧临时文件清扫
pendingBundleFiles之外,sweepStaleDownloads处理另一种残留:崩溃/强杀会让写入方来不及删除自己的临时文件。它清理两类东西:
- 每个 bundle 文件旁形如
<file>.tmp-<uuid>的残留(每次重试都写新的 uuid,不清理会不断累积); - 每个所需共享运行时的 staging 目录。
清扫只在启动时执行——下载进行中时,临时文件属于写入方,不能动。
共享运行时:onnxruntime-node 的按需获取
Embedding 与 OCR 两个 bundle 都声明requires: ['onnxruntime-node']。onnxruntime-node 是共享原生运行时,被设计为按需下载而非随安装包分发:npm 包携带所有平台的二进制,打包进安装器会给从不使用本地模型的用户凭空增加数百 MB(见 catalog.ts 的注释)。
它的获取走tarballArtifact.ts:
- 从 npm 注册表(
registry.npmjs.org/registry.npmmirror.com)下载整个 tarball,用tarballSha256对整个 tarball校验——因此解压出的平台文件无需各自的校验和; - 只解压当前平台
tarballPrefix下的文件(如package/bin/napi-v6/linux/x64/),拍平到安装目录; - 安装顺序是支持文件先于入口文件,保证
isArtifactInstalled永远不会看到"绑定文件在而它依赖的库还没到"的状态; - 删除时对 Windows 的
EPERM/EBUSY做重试退避(50/100/200/400ms)。
LocalModelStorageService.ensureArtifact还做了并发合并:同一 artifact 的多个并发安装共享同一次下载(artifactInstallsMap),"Embedding 和 OCR 下载竞速同一个运行时"时,二者等待同一个请求而非同时写同一批文件。删除(removeArtifactIfUnused)则通过预留计数(artifactReservations)保证"只用引用计数降到 0 才删"。
推理侧:绝对路径、完全离线
变更的另一半是推理。EmbeddingInferenceService.embed通过resolveModel()拿到安装目录:
private resolveModel(): { modelDir: string; dtype: string } { const bundle = bundleForCapability('embedding') const modelDir = localModelStorageService.resolveInstalledDir(bundle) const artifactsReady = bundle.requires.every((id) => localModelStorageService.isArtifactReady(id)) if (!modelDir || !artifactsReady) throw new Error('the local embedding model is not fully downloaded') return { modelDir, dtype: bundleDtype(bundle) } }注意这里的防御逻辑:目录不完整或共享运行时未就绪时直接抛错,而不是"试图从网络补"。下载与加载被严格分层。
推理侧的处理函数inferenceEmbeddingHandlers.ts进一步保证离线:传给 transformers.js 的modelDir是绝对目录路径,而 transformers.js 会把绝对路径当非法 repo id 拒绝,其文件发现机制因此无法触网:
"The model id is an absolute directory, which transformers.js rejects as a repo id (isValidHfModelId) — and every remote branch in its resolver is gated on that check, so file discovery cannot reach the network no matter what
revision/local_files_onlyits internal stages default to."
这段话还解释了旧问题的根因:transformers.js 4.2.0 在发现阶段(get_pipeline_files → get_files → get_config / get_tokenizer_files)会丢弃revision/local_files_only两个选项,这正是旧版"仅 ModelScope 缓存无法离线使用"的原因——ModelScope 的masterrevision 无法被正确寻址。新的绝对路径加载方式从根本上绕开了这一缺陷。
用户与发布管理要点
文档的收尾部分对两类读者给出了明确的行动指引,本文照录并补充说明:
对普通用户——什么都不用做,全部自动:
- 已下载的 Embedding 模型保持已安装状态,不会被重新下载(
resolveInstalledDir对当前布局直接返回,pendingBundleFiles返回空); - 若文件位于被取代的旧布局(ModelScope
master/目录),应用会在首次使用时自动移动它们(liftLegacyInstall惰性触发); - 移动失败不影响使用——模型继续从原位置加载,应用稍后重试;
- 任何情况下都不会删除或重新下载任何内容。
对发布管理者——需要知晓的边界情况:
- 从 ModelScope 镜像下载的用户,文件多一层
master/目录,该副本会被自动迁移; - 迁移是尽力而为的:文件被占用时留在原地、稍后重试(对应
liftLegacyInstall的警告日志could not lift a legacy local model install; using it in place); - 本次变更不引入任何删除或重下行为。
测试与验证路径
仓库为这套机制提供了完整测试,可作为阅读与验证的入口:
- modelSource.test.ts:镜像寻址表、区域默认与回退顺序;
- downloadEngine.test.ts:镜像回退、sha256 校验、原子写盘;
- bundleDownload.test.ts:多文件编排、加权进度、派生文件改写;
- LocalModelStorageService.test.ts:安装状态扫描、遗留布局提升与回滚、陈旧临时文件清扫、并发安装合并;
- inferenceEntryOffline.test.ts:验证离线推理路径(ModelScope 缓存离线可用性问题)。
总结
这次变更把本地 Embedding 模型的下载从"机器学习库内部行为"提升为"应用自治的基础设施":网络层统一走应用代理(net.fetch),完整性由流式 SHA-256 校验兜底,失败恢复由"临时文件 + 原子提交 + 只补缺失文件"三件套支撑,旧布局迁移则被设计为"尽力而为、可回滚、不重下不删除"的低风险操作。对用户而言它是一次无感的自动化改进;对开发者而言,它是一份值得借鉴的"下载可靠性"实现样本——镜像回退、边下载边校验、断点续传与旧目录迁移,四件事在 acquisition/ 与 installation/ 两个目录中做到了职责分明、可测可验证。
- 人工智能
- 大模型
- AI 应用
- 交互助手
- 本地部署
【免费下载链接】cherry-studio
🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端
相关推荐
Nativefier 构建资源下载工具:断点续传与校验
Nativefier 构建资源下载工具:断点续传与校验 你是否曾因网络中断导致大文件下载前功尽弃?是否担心下载的安装包损坏而无法使用?Nativefier 的资
桌面应用CLICherry Studio 本地嵌入模型与 PaddleOCR 模型怎么下载?镜像回退与 SHA256 校验
Cherry Studio 本地嵌入模型与 PaddleOCR 模型怎么下载?镜像回退与 SHA256 校验 Cherry Studio 可以在本机运行两种模型
AI 应用大模型桌面应用本地部署RAGWSABuilds:在 Windows 上校验已下载 WSA 构建包的 SHA-256 完整性
WSABuilds:在 Windows 上校验已下载 WSA 构建包的 SHA 256 完整性 本篇指南围绕 WSABuilds 官方文档 Checksum 指
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考