Cherry Studio 本地 Embedding 模型下载链路重构:应用自管下载、SHA-256 校验与断点续传
发布时间:2026/9/20 12:25:04 作者:尧图编辑部 阅读量:1,286

人工智能大模型AI 应用交互助手本地部署【免费下载链接】cherry-studio Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端项目地址https://gitcode.com/CherryHQ/cherry-studio点击查看免费下载本文围绕 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. 损坏或被篡改的下载当场拒绝文档指出过去被损坏或被拦截的下载可能在下载时不被察觉直到加载模型失败时才暴露。现在校验发生在字节流经的途中而不是事后。streamToFileVerifieddownloadEngine.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,000100MB以上配合weight: 585可推知权重文件约占整个 bundle 绝大部分体积。下载引擎的实现细节下载引擎位于src/main/ai/localModel/acquisition/由四个文件组成职责清晰文件职责modelSource.ts定义下载镜像源HuggingFace / ModelScope与区域偏好downloadEngine.ts底层网络原语镜像回退、流式校验写盘、文本抓取bundleDownload.ts按 bundle 编排多文件下载与进度条tarballArtifact.ts共享原生运行时onnxruntime-nodenpm tarball 的获取与安装镜像回退一个坏镜像不能拖垮整个下载withMirrorFallbackdownloadEngine.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: RecordModelSourceId, 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/下、默认分支叫masterHuggingFace 叫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.tsqwen3-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.resolveInstalledDirLocalModelStorageService.ts的判定逻辑当前布局完整 → 返回当前目录当前布局不完整但遗留布局完整 → 触发liftLegacyInstall把遗留目录提升到当前布局提升后再读一次两种布局避免提升中回滚也失败导致丢文件的目录被交给调用方。liftLegacyInstall同文件 L127-L167是严格尽力而为的如果文件正被存活的推理 worker 占用导致移动失败会记录警告并留在原地继续使用fallback 零成本但绝不会处于半完成状态——凡是已经移动的文件都会移回去因为一个横跨两种布局的安装会让两边都不完整从而重新下载一个其实完全在磁盘上的模型。这与文档中发布管理说明完全吻合如果移动无法完成例如文件暂时被占用模型会继续从原位置正常工作应用稍后重试。任何情况下都不会删除或重新下载任何内容。断点续传与未完成下载的清理只下载缺失文件missingFilesIn用statSync检查每个文件的存在性与最小字节数minBytespendingBundleFiles据此返回还需抓取的清单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 的多个并发安装共享同一次下载artifactInstallsMapEmbedding 和 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 whatrevision/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返回空若文件位于被取代的旧布局ModelScopemaster/目录应用会在首次使用时自动移动它们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 提供商的桌面客户端项目地址https://gitcode.com/CherryHQ/cherry-studio点击查看免费下载相关推荐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),仅供参考