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. 核心结论

  1. 每个发行产物都内置一份平台专属的完整运行时,其中包含 Node.js、pnpm、DSH、llama-server、 llama.cpp 动态库及其生产依赖;模型 GGUF 不进入安装包,由用户按需下载。
  2. 构建时先在仓库的生成目录中准备一个可独立运行的 runtime payload;它是打包阶段唯一允许使用的运行时来源。
  3. runtime payload 压缩成单个归档文件,通过 Rust include_bytes! 编译进 Tauri 主程序。
  4. 开发构建版本使用 <branch>-<YYYYMMDD>-<short-sha>;发行构建必须位于干净工作区中,并由 HEAD 上唯一的 v<major>.<minor>.<patch> Tag 定义版本。用户首次启动或构建版本变化时,将归档 原子解压到应用数据目录;版本一致的后续启动直接复用。原生“关于”面板和 DSH 设置的 Local DSH 分区显示同一个编译期版本,后者同时列出内置 DSH、llama.cpp、Node.js 和 pnpm 版本。
  5. DSH 程序文件和用户数据分离。程序文件位于版本化 runtime 目录;配置、凭据、会话、插件和 桌面端下载的模型使用 DSH 标准 Home:优先继承 DSH_HOME,未设置时使用 ~/.dsh
  6. 不把构建机的系统 Node、全局 pnpm、全局 pnpm store 或现有 node_modules 直接带入安装包。
  7. Node 和 pnpm 版本由 apps/desktop/packaging/runtime/package.json 锁定,不再维护重复的 versions.json;Node 和 llama.cpp 的平台下载清单只补充 npm package.json 无法表达的归档映射与 SHA-256。
  8. 多个平台使用相同的构建和运行时状态机,但每个 OS/CPU 必须生成自己的 runtime archive。

3. 为什么采用启动时解压

DSH 不是单个 JavaScript 文件。它通过 Node 的标准模块加载机制解析 profile、Bundle、插件和 node_modules,并包含 sharpnode-ptykoffi 等平台原生依赖。因此它需要一个真实、 完整的磁盘目录,不能依赖只支持少量静态 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 devmake 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;
  • dependenciesapps/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 的 .managednode_modulespackage.jsonfile: 指向 .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

准备仓库私有的构建工具链:

  1. apps/desktop/packaging/runtime/package.json 读取精确 Node 和 pnpm 版本;
  2. 根据 target triple 选择 Node 官方归档;
  3. 下载到 .build/cache/downloads/
  4. 校验固定 SHA-256;
  5. 解压到 .build/tools/<target>/node/
  6. 使用该 Node 准备 packageManager 指定的 pnpm 到 .build/tools/<target>/pnpm/
  7. 输出 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,执行以下操作:

  1. 清理或创建当前 target 的临时 staging 目录;
  2. 使用 apps/desktop/packaging/runtime/pnpm-lock.yaml 和本地 pnpm 安装生产依赖(含钉死的 @dsh-fish/hub);
  3. 将目标平台 Node 可执行文件复制到 payload/runtime/bin/
  4. packageManager 指定的 pnpm 分发复制到 payload/runtime/lib/node_modules/pnpm/
  5. 生成相对路径的 dsh/pnpm shim;
  6. 合入 llama-stage 生成的 llama-server 和平台动态库;
  7. 生成第三方许可证清单;
  8. 写入 manifest.json
  9. 检查 payload 不引用构建期 store 或仓库外路径;
  10. 原子替换最终 payload/

8.6 make llama-stage

根据 apps/desktop/packaging/runtime/llama-checksums.json 为当前 target 准备 llama.cpp:

  1. 下载或复用固定版本的官方/项目认可归档;
  2. 校验 SHA-256;
  3. 只提取 llama-server 及其运行所需动态库;
  4. 检查架构、可执行权限和动态库依赖;
  5. 写入可供 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-changedrerun-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 安装流程

  1. 解析编译期 manifest,验证目标平台和当前进程一致;
  2. 获取跨进程安装锁;
  3. 若最终目录的 .installed.json 与 manifest 完全匹配,直接复用;
  4. 将内嵌 archive 的 SHA-256 与 manifest 比较;
  5. 创建同一父目录下的 .partial-<runtime-id>-<pid>
  6. 安全解压,拒绝绝对路径、.. 路径逃逸和越界符号链接;
  7. 恢复 Unix 可执行权限;
  8. 执行 node --versiondsh --version
  9. 写入 .installed.json 并 flush;
  10. 将 partial 目录原子重命名为最终目录;
  11. 释放安装锁并启动 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 只保证 nodedshpnpm 可以从子进程 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 提供受控、可取消、可续传的下载能力:

  1. 由 model ID 在内置 catalog 中解析 URL 和目标文件;
  2. 临时文件写入 models/.downloads/
  3. 已有 partial 文件时使用 HTTP Range 续传;服务端不接受 Range 时安全地从零开始;
  4. 分别按真实读取字节持续发送 downloaded/verified/total 进度,下载与 SHA-256 校验在界面中独立展示;
  5. 完成后校验长度和 SHA-256;
  6. 将文件原子移动到 models/<model-id>/
  7. 主 GGUF 完成后最后写 installation.json
  8. 失败或取消时保留可续传 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 从 /propsdefault_generation_settings.n_ctx 读取最终生效值并交给 DSH,不能在 provider 中另写固定值。

生命周期要求:

  1. 启动前校验 installation.json 和模型文件;
  2. 默认只监听 loopback,不提供 0.0.0.0 开关;
  3. stdout/stderr 写入桌面应用日志目录的 llama-server.log
  4. 在限定时间内轮询 /props 并读取实际上下文长度;
  5. 唯一运行操作为幂等的 ensure(model_id):相同模型直接复用,不同模型才停止旧服务并切换;
  6. Host 启动时使用有界等待的 OS 文件锁和 PID 租约清理旧实例;PID 已消失或身份不匹配时立即删除陈旧租约;
  7. 进程异常退出后更新状态并保留日志;
  8. 桌面应用退出时无条件清理它创建的 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 上执行:

RunnerTarget triple主要产物
macOS arm64aarch64-apple-darwin.app.dmg
macOS x64x86_64-apple-darwin.app.dmg
Windows x64x86_64-pc-windows-msvc便携单 EXE
Linux x64x86_64-unknown-linux-gnuAppImage

因为 DSH 依赖包含平台原生模块,默认不从 macOS 交叉生成 Windows/Linux runtime。若未来启用 交叉编译,也必须先证明依赖解析、optionalDependencies、原生模块和签名结果与原生 runner 一致。

15.1 GitHub Actions

CI 只调用仓库 Makefile,不另走 tauri-action 默认打包路径。

  • .github/workflows/check.yml:Pull Request 与 mainmacos-14windows-latest 上执行 make check。不注入签名密钥。GITHUB_TOKENcontents: 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/模型清理界面。