DeepSeek DSH 桌面客户端运行时与打包设计
August 24, 2026 · View on GitHub
状态:当前实现约定 更新时间:2026-08-24 适用范围:Tauri 2 桌面客户端,内置 DeepSeek Harness(
@deepseek-ai/dsh)、Node.js、pnpm 和 llama.cpp
1. 文档目的
本文定义 DeepSeek DSH 桌面客户端的运行时准备、跨平台打包、首次启动解压、模型下载和本机 llama.cpp 服务方案,并明确 Makefile、构建脚本、Tauri Host 和 DSH 各自的职责。
核心目标是让最终用户在没有安装 Node.js、npm 或 pnpm 的机器上直接运行客户端。Windows 第一阶段应能生成不依赖 NSIS/MSI 的单一可执行文件;macOS 和 Linux 使用各自的标准应用产物, 但共用同一套内置运行时、校验、解压和进程管理逻辑。
当前实现还包含本地模型 DSH 插件和设置界面;自动更新、旧运行时清理和发布签名仍不在第一版范围内。
2. 核心结论
- 每个发行产物都内置一份平台专属的完整运行时,其中包含 Node.js、pnpm、DSH、
llama-server、 llama.cpp 动态库及其生产依赖;模型 GGUF 不进入安装包,由用户按需下载。 - 构建时先在仓库的生成目录中准备一个可独立运行的 runtime payload;它是打包阶段唯一允许使用的运行时来源。
- runtime payload 压缩成单个归档文件,通过 Rust
include_bytes!编译进 Tauri 主程序。 - 开发构建版本使用
<branch>-<YYYYMMDD>-<short-sha>;发行构建必须位于干净工作区中,并由 HEAD 上唯一的v<major>.<minor>.<patch>Tag 定义版本。用户首次启动或构建版本变化时,将归档 原子解压到应用数据目录;版本一致的后续启动直接复用。原生“关于”面板和 DSH 设置的Local DSH分区显示同一个编译期版本,后者同时列出内置 DSH、llama.cpp、Node.js 和 pnpm 版本。 - DSH 程序文件和用户数据分离。程序文件位于版本化 runtime 目录;配置、凭据、会话、插件和
桌面端下载的模型使用 DSH 标准 Home:优先继承
DSH_HOME,未设置时使用~/.dsh。 - 不把构建机的系统 Node、全局 pnpm、全局 pnpm store 或现有
node_modules直接带入安装包。 - Node 和 pnpm 版本由
apps/desktop/packaging/runtime/package.json锁定,不再维护重复的versions.json;Node 和 llama.cpp 的平台下载清单只补充 npmpackage.json无法表达的归档映射与 SHA-256。 - 多个平台使用相同的构建和运行时状态机,但每个 OS/CPU 必须生成自己的 runtime archive。
3. 为什么采用启动时解压
DSH 不是单个 JavaScript 文件。它通过 Node 的标准模块加载机制解析 profile、Bundle、插件和
node_modules,并包含 sharp、node-pty、koffi 等平台原生依赖。因此它需要一个真实、
完整的磁盘目录,不能依赖只支持少量静态 import 的单文件虚拟文件系统。
直接把数万个文件作为 Tauri loose resources 分发在技术上可行,但会增加打包、签名、复制、 安装和病毒扫描阶段的文件数量。Windows 若只发布一个便携 EXE,Node 子进程和 DSH 模块也 必须先成为磁盘文件才能执行或加载。因此统一采用“单一归档资源 + 首次解压”可以兼顾:
- Windows 单 EXE 分发;
- macOS、Windows、Linux 共用运行时安装逻辑;
- 对 runtime archive 做单一 SHA-256 校验;
- 按版本并存和原子切换运行时;
- 避免让 Tauri bundler 直接处理约数万个 Node 依赖文件。
归档不是用户数据容器。用户配置、会话和模型始终保存在标准 DSH Home,不会随着 runtime 升级被覆盖。
4. 两个“本地目录”
设计中存在两个用途完全不同的本地目录。
4.1 构建时本地目录
位于仓库的 .build/ 下,只用于准备和验证发行资源:
.build/
├─ cache/
│ ├─ downloads/ # Node、npm 包等下载缓存
│ └─ pnpm-store/ # 构建期 pnpm store,不进入发行包
├─ tools/
│ └─ <target-triple>/
│ ├─ node/ # 本地构建 Node
│ └─ pnpm/ # 本地构建 pnpm
├─ runtime/
│ └─ <target-triple>/
│ └─ payload/ # 经过整理的唯一打包源
└─ artifacts/
└─ dsh-runtime-<target-triple>.tar.gz
.build/ 是可删除、可重新生成的目录,必须加入 .gitignore。
4.2 用户运行时目录
位于 Tauri 提供的应用数据目录。Windows 示例:
%LOCALAPPDATA%\<bundle-identifier>\
├─ runtime\
│ └─ <runtime-id>\
│ ├─ bin\
│ ├─ lib\
│ ├─ tools\llama\
│ └─ .installed.json
├─ launch-workspace\
└─ logs\
├─ dsh.log
└─ llama-server.log
macOS 和 Linux 使用相同的相对结构,根目录分别由 Tauri 的 app_data_dir/app_local_data_dir
解析,不在业务代码中拼接固定的用户主目录。
.installed.json 记录应用的 Git 构建版本、runtime ID 和归档 SHA-256。启动时先在安装锁内比较
这三个值;全部一致便直接使用现有目录,不读取、计算或解压内置归档。任一值不一致时才校验归档
SHA-256、解压到临时目录、验证入口,并原子替换目标 runtime。启动页也只在这个分支显示
“正在解压内置运行环境”,缓存命中时显示“正在准备本地运行环境”。
Git 构建版本通过 LOCAL_DSH_BUILD_VERSION 注入 Rust 编译,是原生“关于”面板、插件“关于”Tab
和 runtime 安装标记的共同版本来源:
make dev和make check使用分支名、HEAD 提交日期和 8 位提交哈希组成<branch>-<YYYYMMDD>-<short-sha>;工作区是否 dirty 不改变开发版本。make build只接受干净工作区,并要求 HEAD 恰好带一个符合v<major>.<minor>.<patch>的发布 Tag。应用内保留v前缀,例如v0.1.0;Tauri 构建配置在命令行覆盖为平台包元数据需要的0.1.0,DMG 文件名和Info.plist使用同一个 SemVer。- HEAD 没有发布 Tag、Tag 格式不正确、同一提交存在多个发布 Tag或 Tag 后存在未提交改动时, release check 在 runtime 签名和 Tauri 打包前失败。Cargo 和内部 npm 包的版本仍表达各组件元数据, 不作为用户看到的 Local DSH 发行版本。
DSH 数据不放在上述产品私有目录。应用遵循 DSH 标准 Home 解析规则:
${DSH_HOME:-~/.dsh}/
├─ profiles/
├─ sessions/
├─ .credentials.yaml
├─ settings.yaml # 具体文件以 DSH 当前标准结构为准
└─ models/ # 桌面端管理的本机模型
├─ .downloads/
└─ <model-id>/
若用户已经设置 DSH_HOME,桌面端和它启动的 DSH/pnpm 子进程都继承该值;未设置时不注入另一个
产品私有 Home,而是让 DSH 使用标准 ~/.dsh。Rust Host 需要访问模型目录时必须复用 DSH 的
同一解析规则。
5. 仓库结构
local-dsh/
├─ apps/
│ ├─ desktop/
│ │ ├─ src/ # Tauri 启动、错误和重试页面
│ │ ├─ src-tauri/ # Rust Host 与平台打包配置
│ │ ├─ resources/
│ │ │ └─ models.json # 可下载模型、文件、大小和 SHA-256
│ │ └─ packaging/
│ │ └─ runtime/
│ │ ├─ package.json # 精确固定 DSH/pnpm 发行依赖
│ │ ├─ pnpm-lock.yaml # 提交到 Git
│ │ ├─ node-checksums.json # 各平台 Node 官方归档映射及 SHA-256
│ │ └─ llama-checksums.json # llama.cpp 版本、平台归档及 SHA-256
│ └─ website/ # 官网及独立部署配置
├─ docs/
│ ├─ runtime-packaging.md
│ └─ local-model-plugin.md
├─ packages/
│ └─ local-dsh/ # DSH Host + React Client 双面插件
├─ scripts/
│ ├─ bootstrap-tools.sh # 下载仓库私有 Node/pnpm
│ ├─ detect-target.sh
│ ├─ clean.sh
│ └─ run.mjs # runtime、插件和 Tauri 构建编排
├─ Makefile
├─ Makefile.local.example
├─ package.json
└─ pnpm-lock.yaml
根目录 package.json/pnpm-lock.yaml 管理 Tauri CLI、插件和网站 workspace;
apps/desktop/packaging/runtime/ 只管理最终内置运行时。两份 lockfile 不得混用。所有可重新生成的
工具链、runtime payload 和归档统一写入根目录 .build/,不与上述构建输入混放。
6. 版本和输入的事实来源
Node、pnpm 和 DSH 的版本统一由 apps/desktop/packaging/runtime/package.json 表达,不额外维护 versions.json:
{
"name": "local-dsh-runtime",
"private": true,
"engines": {
"node": "24.19.0"
},
"packageManager": "pnpm@11.22.0",
"dependencies": {
"@deepseek-ai/dsh": "0.1.1-rc.2",
"@dsh-fish/hub": "0.3.1"
}
}
engines.node在本项目中使用精确版本,既表示兼容要求,也作为要下载和内置的 Node 版本;packageManager固定构建和随 runtime 提供的 pnpm;dependencies与apps/desktop/packaging/runtime/pnpm-lock.yaml固定 DSH、随应用发布的受管插件及其完整生产依赖;- 不使用
latest、^或~作为发行输入。
以上版本是初始候选值。实现时应在对应平台完成 runtime smoke test 后再固定。版本升级必须通过
代码评审修改 apps/desktop/packaging/runtime/package.json 和 lockfile,重新生成运行时并发布新的桌面客户端版本;
客户端启动时不自动从 npm 获取最新 DSH。
package.json 不能表达 Node 官方归档的平台文件名和哈希,因此 apps/desktop/packaging/runtime/node-checksums.json
只记录 target 到下载变体及 SHA-256 的映射,Node 版本仍从 engines.node 读取:
{
"x86_64-pc-windows-msvc": {
"platform": "win-x64",
"format": "zip",
"sha256": "<pinned-sha256>"
},
"aarch64-apple-darwin": {
"platform": "darwin-arm64",
"format": "tar.xz",
"sha256": "<pinned-sha256>"
}
}
llama.cpp 不是 npm 依赖,因此其发行版本、平台归档、动态库清单和 SHA-256 单独记录在
apps/desktop/packaging/runtime/llama-checksums.json。模型文件也不是构建依赖,其 URL、文件名、大小和 SHA-256
记录在 apps/desktop/resources/models.json。
所有下载必须在使用前校验固定哈希。构建缓存可以复用下载结果,但不能绕过校验。
7. Runtime payload 契约
归档顶层固定为 runtime/:
runtime/
├─ manifest.json
├─ bin/
│ ├─ node.exe | node
│ ├─ dsh.cmd | dsh
│ └─ pnpm.cmd | pnpm
├─ lib/
│ └─ node_modules/
│ ├─ @deepseek-ai/dsh/
│ ├─ @local-dsh/local-dsh/
│ ├─ @dsh-fish/hub/
│ ├─ pnpm/
│ └─ ...production dependency closure...
├─ tools/
│ └─ llama/
│ ├─ llama-server.exe | llama-server
│ └─ ...platform dynamic libraries...
└─ licenses/
├─ THIRD_PARTY_NOTICES.md
└─ dependency-licenses.json
manifest.json 至少包含:
{
"schemaVersion": 1,
"runtimeId": "dsh-0.1.1-rc.2_node-24.19.0_x86_64-pc-windows-msvc_<content-hash>",
"targetTriple": "x86_64-pc-windows-msvc",
"nodeVersion": "24.19.0",
"pnpmVersion": "11.22.0",
"dshVersion": "0.1.1-rc.2",
"llamaVersion": "<pinned-llama.cpp-version>",
"entrypoints": {
"node": "bin/node.exe",
"dsh": "lib/node_modules/@deepseek-ai/dsh/lib/bin.js",
"pnpm": "lib/node_modules/pnpm/bin/pnpm.cjs",
"llamaServer": "tools/llama/llama-server.exe",
"localDshPlugin": "lib/node_modules/@local-dsh/local-dsh",
"dshFishHub": "lib/node_modules/@dsh-fish/hub"
}
}
Unix 目标使用不带 .exe 的入口。外层 runtime manifest 生成时应写入当前目标的实际相对路径,
上例只展示 Windows 形态。
Rust Host 从 manifest 取得相对入口,不在多个模块中重复硬编码目录结构。
7.1 pnpm 依赖树约束
构建期 pnpm store 不属于 runtime。staging 后必须满足:
- 所有运行文件都位于 payload 内;
- 不存在指向仓库、用户主目录或全局 pnpm store 的绝对链接;
- 所有符号链接目标都留在 payload 根目录内;
- 不包含 devDependencies、测试 fixture 或构建缓存;
- DSH Web UI 静态资源和运行时动态加载的 Bundle 不得被错误裁剪。
第一版优先使用 pnpm deploy 或经过验证的 hoisted 安装生成可搬运依赖闭包,不直接复制根目录
开发用 node_modules。
pnpm 本身进入 payload 是为了保留 DSH 标准插件管理能力。随桌面端发布的受管插件(本地模型插件与
@dsh-fish/hub)先进入 runtime,启动时再复制到 profile 的 .managed 和 node_modules。
package.json 用 file: 指向 .managed 副本,这样 dsh plugin add 解析已有依赖时不会去 npm
拉这些包。复制内容很小,不会重复整棵 DSH 依赖。用户自行安装的插件仍由 DSH/pnpm 按标准 profile
和 store 规则管理。
8. Makefile 目标契约
Makefile 是构建工作流的统一入口,复杂的下载、目录遍历、JSON 修改和归档逻辑放在 scripts/
中,不在 Make recipe 中堆积大量平台判断。
8.1 基础变量
建议保留以下可覆盖变量:
RUSTC ?= rustc
CARGO ?= cargo
CURL ?= curl
TARGET_TRIPLE ?= $(shell "$(RUSTC)" --print host-tuple)
BUILD_DIR ?= .build
NODE_DIST_BASE_URL ?= https://nodejs.org/dist
NPM_REGISTRY ?= https://registry.npmjs.org
PROXY ?=
代理、镜像和本机路径通过未提交的 Makefile.local 覆盖。版本不通过 Makefile.local 漂移,
Node/pnpm/DSH 必须来自 apps/desktop/packaging/runtime/package.json,llama.cpp 必须来自已提交的下载清单。
8.2 make tools
准备仓库私有的构建工具链:
- 从
apps/desktop/packaging/runtime/package.json读取精确 Node 和 pnpm 版本; - 根据 target triple 选择 Node 官方归档;
- 下载到
.build/cache/downloads/; - 校验固定 SHA-256;
- 解压到
.build/tools/<target>/node/; - 使用该 Node 准备
packageManager指定的 pnpm 到.build/tools/<target>/pnpm/; - 输出 Node 和 pnpm 版本并写入 stamp。
除 Rust、Make、下载和解压所需的基本系统工具外,该目标不得依赖系统 Node/npm/pnpm。
8.3 make dep
依赖 tools,使用仓库私有 Node/pnpm 安装 workspace 内的 Tauri CLI 和插件构建依赖。
8.4 make plugin
对 packages/local-dsh 执行 TypeScript 检查,并生成 DSH Host ESM 与由 DSH 模块加载器接管的
React Client bundle。生成物是 runtime staging 的显式输入。
8.5 make runtime-stage
依赖 tools,执行以下操作:
- 清理或创建当前 target 的临时 staging 目录;
- 使用
apps/desktop/packaging/runtime/pnpm-lock.yaml和本地 pnpm 安装生产依赖(含钉死的@dsh-fish/hub); - 将目标平台 Node 可执行文件复制到
payload/runtime/bin/; - 将
packageManager指定的 pnpm 分发复制到payload/runtime/lib/node_modules/pnpm/; - 生成相对路径的
dsh/pnpmshim; - 合入
llama-stage生成的llama-server和平台动态库; - 生成第三方许可证清单;
- 写入
manifest.json; - 检查 payload 不引用构建期 store 或仓库外路径;
- 原子替换最终
payload/。
8.6 make llama-stage
根据 apps/desktop/packaging/runtime/llama-checksums.json 为当前 target 准备 llama.cpp:
- 下载或复用固定版本的官方/项目认可归档;
- 校验 SHA-256;
- 只提取
llama-server及其运行所需动态库; - 检查架构、可执行权限和动态库依赖;
- 写入可供
runtime-stage合并的 staging 目录。
llama-server 二进制和动态库不提交到 Git,也不从系统 PATH 复制。开发时可通过显式变量指向
本机测试构建,但 release 必须使用经过固定版本和哈希校验的 staging 产物。
8.7 make runtime-check
只使用 payload 中的文件验证:
- Node 版本等于 manifest;
- pnpm 版本等于 manifest;
- DSH 版本等于 manifest;
llama-server版本等于 manifest,所需动态库可以解析;- 不存在逃逸 payload 的符号链接;
- 在隔离工作目录和临时
DSH_HOME中部署桌面 profile 插件; - 执行
dsh --profile local-dsh --dump-config; - 确认本地模型插件与
@dsh-fish/hub已经进入最终 Cordis 配置树。
构建期 smoke test 必须设置临时 DSH_HOME,不得读取或修改开发者现有的 ~/.dsh。这是测试
隔离措施;正式应用启动时仍采用标准 DSH Home 解析规则。
8.8 make runtime-pack
依赖 runtime-check,将 payload 生成确定性的 tar.gz:
- 条目按路径排序;
- 固定 owner/group;
- 固定或归一化 mtime;
- 保留 Unix 可执行权限;
- 不包含 macOS AppleDouble、扩展属性和临时文件;
- 计算 archive SHA-256,并生成供 Rust 编译期读取的外层 manifest。
8.9 make runtime
聚合目标:
tools + plugin + llama-stage -> runtime-stage -> runtime-check -> runtime-pack
这是“把 Node、pnpm、DSH 和 llama.cpp 安装到仓库本地,并形成唯一打包源”的标准入口。
8.10 make check
执行前端类型检查、Rust cargo check、运行时版本一致性检查和构建脚本单元测试。普通代码检查
不应在网络可用时自动升级或重新安装 runtime。
8.11 make dev
开发模式默认仍使用经过 staging 的 runtime,以尽早发现生产目录结构问题。启动完成后,根构建
脚本持续监听本地模型插件的 src/ 和 scripts/;标准 DSH profile 中该受管插件的 lib/ 在 debug
模式下临时链接到仓库生成目录。Host HMR 只观察这个位于 node_modules 之外的真实目录,浏览器
bundle 则由 DSH 自带的 Client HMR 重载。因此日常修改插件不重新 staging/归档完整 runtime,也
不重启 Tauri、DSH 或 llama.cpp。构建失败保留上一次成功的 bundle。
开发链接写入 .local-dsh-dev 标记;未设置开发环境的应用下次启动会从内置 runtime 恢复普通目录。
Windows 开发机需要具备创建目录符号链接的权限。这里不能退化为只复制 bundle:DSH Host HMR 会
主动排除 node_modules 模块,复制只能触发 Client HMR,不能可靠重载 Host。
该 watcher 和 HMR overlay 是项目级开发约定,实现在根 Makefile 与构建脚本中;Makefile.local
只承担代理、镜像和目标平台等机器本地覆盖。release 不接受这条 debug overlay,仍必须包含内置
runtime。
8.12 make build
推荐依赖关系:
dep -> check -> runtime -> build-tauri
build-tauri 将 runtime archive 的绝对路径作为环境变量传入 Cargo/Tauri。release 构建没有
archive、manifest 不匹配或 runtime smoke 未通过时必须失败。make build 还要求干净工作区的
HEAD 带唯一 v<major>.<minor>.<patch> Tag,并用该 Tag 覆盖 Tauri 平台包版本。
平台产物:
- Windows:
tauri build --no-bundle,发布签名后的单 EXE; - macOS:
make build通过--bundles app,dmg同时生成、签名并公证.app和.dmg。 GitHub Actions 设置CI=true时跳过website-dmg,避免把公证 DMG 写进仓库工作区;网站更新仍是本地或后续独立步骤。 - Linux:第一阶段生成 AppImage,后续按需要增加 deb/rpm。
8.12 清理目标
建议区分:
make clean-runtime:删除 staging 和 runtime archive,保留下载与 pnpm store 缓存;make clean-cache:显式删除可重新下载的构建缓存;make clean:只删除本项目明确生成的路径,不删除用户数据或仓库外目录。
用户机器上的旧 runtime 清理功能不属于 Makefile,留给桌面应用后续实现。
9. 编译期嵌入
为了让 Windows --no-bundle 产物仍是包含完整运行时的单 EXE,runtime archive 不仅作为
Tauri loose resource,而是统一编译进 Rust 主程序。
Makefile 传入:
DSH_RUNTIME_ARCHIVE_PATH=<absolute archive path>
DSH_RUNTIME_MANIFEST_PATH=<absolute outer manifest path>
apps/desktop/src-tauri/build.rs 负责:
- 声明
rerun-if-env-changed和rerun-if-changed; - canonicalize 并验证文件存在;
- 校验 target triple、archive hash 和必需版本字段;
- 通过
rustc-env将规范化路径传给 Rust; - release 构建缺少 runtime 时立即失败。
Rust 侧使用:
const RUNTIME_ARCHIVE: &[u8] =
include_bytes!(env!("DSH_RUNTIME_ARCHIVE_PATH"));
const RUNTIME_MANIFEST: &str =
include_str!(env!("DSH_RUNTIME_MANIFEST_PATH"));
这样 Windows 只需分发主 EXE,macOS/Linux 也复用完全相同的 runtime bytes 和安装逻辑。
10. 首次启动安装状态机
10.1 目标目录
最终目录为:
<app-data>/runtime/<runtime-id>/
runtime-id 同时包含 DSH、Node、target 和内容 hash。相同版本但 payload 内容变化时必须得到
不同 ID,避免错误复用旧文件。
10.2 安装流程
- 解析编译期 manifest,验证目标平台和当前进程一致;
- 获取跨进程安装锁;
- 若最终目录的
.installed.json与 manifest 完全匹配,直接复用; - 将内嵌 archive 的 SHA-256 与 manifest 比较;
- 创建同一父目录下的
.partial-<runtime-id>-<pid>; - 安全解压,拒绝绝对路径、
..路径逃逸和越界符号链接; - 恢复 Unix 可执行权限;
- 执行
node --version和dsh --version; - 写入
.installed.json并 flush; - 将 partial 目录原子重命名为最终目录;
- 释放安装锁并启动 DSH。
安装失败时删除本次 partial 目录,保留之前已经验证的 runtime。不得在原目录上增量覆盖。
10.3 并发与崩溃恢复
- 使用命名 Mutex(Windows)或锁文件(Unix)防止两个应用实例同时解压;
- 锁内再次检查最终目录,避免等待者重复安装;
- 启动时可以清理超过安全时间阈值且不属于活跃进程的 partial 目录;
- 单实例插件负责将第二次应用启动转为聚焦现有窗口,但 runtime installer 仍需独立保证并发安全。
11. DSH 进程启动
Rust Host 从已验证 runtime manifest 解析入口,并以固定参数启动:
<runtime>/bin/node[.exe]
<runtime>/lib/node_modules/@deepseek-ai/dsh/lib/bin.js
--profile local-dsh
--host 127.0.0.1
--port <reserved-loopback-port>
--trusted-host 127.0.0.1
--no-open
环境变量至少包括:
PATH=<runtime>/bin + inherited safe PATH
DSH_PROFILE=local-dsh
Host 不主动覆盖 DSH_HOME。若父进程环境已经设置 DSH_HOME,原样传递;否则让 DSH 按标准
行为使用 ~/.dsh。pnpm 也使用用户已有的标准配置和 store 规则,桌面端不创建另一套私有
pnpm Home。runtime 中的 shim 只保证 node、dsh 和 pnpm 可以从子进程 PATH 找到。
工作目录使用应用自有的 launch-workspace/,不使用安装目录、runtime 目录或不确定的系统当前目录。
用户随后通过 DSH 自己的工作区界面添加项目目录。
Rust Host:
- 同时捕获 stdout/stderr 并写入
logs/dsh.log; - 在启动前预留随机 loopback 端口,并将其作为明确参数传入;
- 在超时内完成 HTTP 健康检查后才让 WebView 导航;
- 启动失败时保留日志并向本地启动页提供重试和打开日志操作;
- 应用退出时先请求优雅关闭,超时后终止整个进程树;
- Windows 使用 Job Object 或等价机制,避免遗留 Node 和工具子进程。
12. 模型下载与 llama.cpp 服务
本机模型能力复用参考项目已经验证的职责划分:下载器负责受控文件传输,模型 Feature 负责目录和
安装状态,Rust Host 持有 llama-server 生命周期,DSH 标准设置体系仍是模型路由配置的事实来源。
12.1 模型目录
模型安装在标准 DSH Home 下:
${DSH_HOME:-~/.dsh}/models/
├─ .downloads/
│ └─ <download-id>.part
└─ <model-id>/
├─ model.gguf
└─ installation.json
installation.json 至少记录 schema 版本、model ID、文件相对路径、文件大小、SHA-256、来源 URL
和安装时间。只有所有必需文件完成下载并通过校验后才写该文件;存在目录或零字节文件不能视为安装成功。
12.2 模型目录清单
apps/desktop/resources/models.json 是桌面端可下载模型的只读目录,至少包含:
- 稳定 model ID 和显示信息;
- 主 GGUF 的 URL、文件名、期望大小和 SHA-256;
- 建议上下文大小和最低内存提示;
- 与内置 llama.cpp 版本的兼容性说明。
当前 DSH 客户端不提供图片输入,因此第一版目录只分发文本推理所需的主 GGUF,不下载或加载 multimodal projector。未来只有在 DSH 完成视觉输入链路后,才随对应版本恢复相关资源和能力声明。
第一版可以参考现有项目提供少量经过验证的 GGUF,而不是开放 renderer 传入任意下载 URL 或任意 目标路径。模型目录升级随桌面客户端发布,并经过下载和推理 smoke test。
12.3 下载器
Rust Host 提供受控、可取消、可续传的下载能力:
- 由 model ID 在内置 catalog 中解析 URL 和目标文件;
- 临时文件写入
models/.downloads/; - 已有 partial 文件时使用 HTTP
Range续传;服务端不接受 Range 时安全地从零开始; - 分别按真实读取字节持续发送 downloaded/verified/total 进度,下载与 SHA-256 校验在界面中独立展示;
- 完成后校验长度和 SHA-256;
- 将文件原子移动到
models/<model-id>/; - 主 GGUF 完成后最后写
installation.json; - 失败或取消时保留可续传 partial,但不产生有效安装标记。
下载队列和按钮状态可以由桌面 UI 管理,但 renderer 只能提交 catalog model ID,不获得任意网络下载 和任意文件写入能力。
12.4 llama-server 生命周期
llama-server 及其动态库来自已解压、已验证的 runtime:
<runtime>/tools/llama/llama-server[.exe]
Rust Host 不从 renderer 接收可执行文件路径,只允许启动 manifest 中的固定入口。启动参数由 Host 根据已验证的模型安装生成,例如:
llama-server
-m <DSH_HOME>/models/<model-id>/model.gguf
--alias <model-alias>
--host 127.0.0.1
--port <managed-port>
--parallel 1
--ctx-size <hardware-profile-context>
--fit on
Rust Host 根据 catalog 内的平台、后端和容量矩阵选择模型专属上下文。当前 macOS/Metal 读取统一
内存容量;Windows/CUDA 和 Linux/CUDA 使用独立、尚待实测的显存矩阵。找不到 profile 时省略
--ctx-size 并回退到 --fit on。服务就绪后,Host 从 /props 的
default_generation_settings.n_ctx 读取最终生效值并交给 DSH,不能在 provider 中另写固定值。
生命周期要求:
- 启动前校验
installation.json和模型文件; - 默认只监听 loopback,不提供
0.0.0.0开关; - stdout/stderr 写入桌面应用日志目录的
llama-server.log; - 在限定时间内轮询
/props并读取实际上下文长度; - 唯一运行操作为幂等的
ensure(model_id):相同模型直接复用,不同模型才停止旧服务并切换; - Host 启动时使用有界等待的 OS 文件锁和 PID 租约清理旧实例;PID 已消失或身份不匹配时立即删除陈旧租约;
- 进程异常退出后更新状态并保留日志;
- 桌面应用退出时无条件清理它创建的
llama-server进程树,用户界面不提供手动停止。
12.5 与 DSH 的配置边界
桌面端不另建一份“当前模型服务配置”。启用本机模型时,local-dsh Host 插件通过 DSH
Settings 服务写入 llm-pi-ai.providers.local-llama,并通过 agentDefaultModel 服务把它设为新会话
默认模型。外部模型和本机模型继续由同一套 DSH 标准配置读取和展示。
模型安装记录只回答“本机有哪些 GGUF 文件可用”,不替代 DSH 的 provider、model、preset 或凭据 配置。退出桌面端后 llama.cpp 不继续常驻,但标准 DSH Home 中的配置、会话和模型文件保留。
13. WebView 与安全边界
DSH Web UI 是本地代理控制面,能够驱动文件访问和命令执行,因此必须遵守:
- 只绑定
127.0.0.1,禁止默认使用0.0.0.0; - 使用端口
0让系统分配随机端口; - WebView 只导航到本次启动解析出的精确 loopback URL;
- 阻止应用内导航到非 loopback HTTP(S) 地址,外部链接交给系统浏览器;
- 不为 DSH 远程页面开放 Tauri Shell、文件系统或任意 IPC 权限;
- 前端不能传入任意可执行文件、任意启动参数或任意环境变量;
- API Key 和凭据由 DSH 的凭据机制保存,不进入日志、manifest 或 runtime archive。
14. 平台策略
14.1 Windows x64
目标 triple:x86_64-pc-windows-msvc。
- 通过
tauri build --no-bundle生成单 EXE; - Node、pnpm、DSH、
llama-server和动态库 archive 全部编译进主 EXE; - 首次运行解压到
%LOCALAPPDATA%,不请求管理员权限、不写卸载注册表; - 路径应保持短,例如
runtime/<short-hash>,降低深层 Node 依赖触发长路径问题的风险; - 检测 WebView2 Evergreen Runtime。第一阶段将其作为 Windows 10/11 系统前置条件,不内置约 数百 MB 的 Fixed WebView2;缺失时展示明确安装提示;
- 正式发布必须对主 EXE 签名,并测试 SmartScreen 和常见杀毒软件行为。
14.2 macOS arm64/x64
两个架构分别构建,不在第一阶段合并 universal runtime。
- 生成
.app和.dmg; - runtime archive 仍编译在主程序中,首次启动解压到 Application Support;
- payload 中的
.node、llama.cpp.dylib和其他 Mach-O 文件必须在生成 archive 前完成正确签名; - 对签名、Hardened Runtime、Library Validation 和 notarization 后的首次解压启动执行真实机器测试;
- 若未来支持安装任意含原生模块的第三方 DSH 插件,需要单独评估 Library Validation 策略, 不能默认放宽整个应用的安全权限。
14.3 Linux x64/arm64
- 每个架构分别 staging;
- 第一阶段优先 AppImage;
- 验证 Node 官方二进制的 glibc 基线和 WebKitGTK/GStreamer 运行依赖;
- 不假设一个 Linux runtime archive 可以跨 CPU 或 libc 家族使用。
15. 跨平台构建矩阵
相同 Makefile 目标在对应原生 runner 上执行:
| Runner | Target triple | 主要产物 |
|---|---|---|
| macOS arm64 | aarch64-apple-darwin | .app、.dmg |
| macOS x64 | x86_64-apple-darwin | .app、.dmg |
| Windows x64 | x86_64-pc-windows-msvc | 便携单 EXE |
| Linux x64 | x86_64-unknown-linux-gnu | AppImage |
因为 DSH 依赖包含平台原生模块,默认不从 macOS 交叉生成 Windows/Linux runtime。若未来启用 交叉编译,也必须先证明依赖解析、optionalDependencies、原生模块和签名结果与原生 runner 一致。
15.1 GitHub Actions
CI 只调用仓库 Makefile,不另走 tauri-action 默认打包路径。
.github/workflows/check.yml:Pull Request 与main在macos-14和windows-latest上执行make check。不注入签名密钥。GITHUB_TOKEN仅contents: read。.github/workflows/release.yml:推送唯一的v<major>.<minor>.<patch>Tag 后,在macos-14上导入 Developer ID、执行make build、公证 DMG,再把 DMG、runtime 外层 manifest 和SHA256SUMS.txt挂到该 Tag 的 GitHub Release。- 缓存
.build/downloads、.build/pnpm-store和 Cargo registry;不缓存已签名 runtime payload 或最终.app/DMG。 - Windows 已签名便携 EXE 与 Linux AppImage 仍按上表在原生 runner 补齐,不在第一版 Release 工作流中发布。
macOS 发版 Secrets 与本机 Makefile.local 对应,外加 CI 专用的证书材料,见 Makefile.local.example。证书在 job 内导入临时 keychain,公证 API Key 写入 $RUNNER_TEMP,job 结束删除。工作流不得打印这些值。
16. 验证要求
每个平台至少执行以下验证:
16.1 构建期验证
- 在没有系统 Node/npm/pnpm 的环境运行
make tools; - 连续执行两次
make runtime,第二次正确复用缓存且产物一致; - 修改
apps/desktop/packaging/runtime/package.json、lockfile 或下载清单后正确失效 stamp; - archive 中不存在绝对路径、构建缓存和仓库外链接;
- archive hash、内层 manifest 和外层 manifest 一致。
16.2 Runtime smoke
- 内置 Node/pnpm/DSH/llama.cpp 版本正确;
- 临时
DSH_HOME下首次启动成功; - 随机 loopback 端口可访问;
- 停止后端口释放且无遗留子进程;
- 全程不访问系统 Node/pnpm。
- 使用测试 GGUF 启动
llama-server,/props返回成功;
16.3 已构建产物验证
- 在未安装 Node/npm/pnpm 的干净虚拟机离线启动;
- 首次解压成功,第二次启动不重复解压;
- 模拟解压中断后能够自动恢复;
- 两个进程同时首次启动时不会损坏 runtime;
- 应用升级后新旧 runtime 不互相覆盖;
- DSH 会话、设置和凭据在升级后保留;
- 继承自定义
DSH_HOME,未设置时与命令行 DSH 共用标准~/.dsh; - 模型断点续传、取消、哈希失败、原子安装和安装后 llama.cpp 启动均通过;
- Windows 单 EXE、macOS 签名/公证产物和 Linux AppImage 分别进行安装后 smoke test。
17. 许可证与供应链
- DSH、Node、pnpm、llama.cpp 和所有生产依赖必须固定版本并记录来源;
- Node 下载使用官方发布归档和固定 SHA-256;
- llama.cpp 二进制、动态库和模型 catalog 使用固定来源和 SHA-256;
- npm 依赖使用提交的 lockfile 和完整 integrity;
- runtime archive 内置第三方许可证清单;
- CI 保存 runtime manifest、archive SHA-256 和最终应用哈希;
- 构建日志不得输出 API Key、代理凭据或签名密钥;
- DSH 目前仍处于快速迭代阶段,每次升级都需要完整 runtime smoke,而不是仅检查
--version。
18. 暂缓事项
以下内容后续单独设计,不阻塞第一版运行时打包:
- 用户界面中的旧 runtime 清理功能;
- runtime 最大保留数量和磁盘空间策略;
- DSH 与桌面壳的独立自动更新;
- 已下载模型和失败 partial 的用户清理界面;
- Windows Fixed WebView2 离线运行时;
- macOS universal binary;
- 任意含原生模块的第三方插件签名策略;
- 完全便携、将用户数据保存在 EXE 同目录的模式。
19. 后续工作
当前开发平台已经完成 runtime、Tauri 启动、标准 DSH profile、本地模型插件、下载器和 llama.cpp
控制链路,以及 make check / 公证 macOS DMG 的 GitHub Actions。发布前仍需在 Windows x64 原生
runner 验证并签名便携单 EXE,补齐 Linux AppImage、干净虚拟机 smoke、真实测试 GGUF 推理以及
旧 runtime/模型清理界面。