使用 Rust + Tauri 2.0 重新构建的 DeepSeek Harness 桌面套壳。
39.1/100
综合分
12
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/31 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
自动兼容检查未通过:缺少 main/exports/bin 入口声明;engines.node 要求 >=24.19.0 <25,不满足 Node 22.19.0;仓库 package.json 标记 private,未发布到 npm,需从源码安装。以下命令仍可尝试,但可能失败。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add wang-yi-bit64/dsh-desktop该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:已验证
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装缺少 main/exports/bin 入口声明;engines.node 要求 >=24.19.0 <25,不满足 Node 22.19.0;仓库 package.json 标记 private,未发布到 npm,需从源码安装
展开查看检查明细 ↓
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包dsh-desktop-tauri(未发布到 npm,仅可源码安装)
✗Node 引擎要求 >=24.19.0 <25 · 基线 Node 22.19 不满足
✓dsh CLI 依赖未声明 dsh 版本约束
✗入口文件缺少入口声明
源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。
验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/25 10:35:52
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
可执行外部命令读写/删除本地文件
查看扫描证据(共 32 处,展示前 16 条)
- 读取本地文件build/sidecar-entry.mjs:32
let dshEntry = candidatePaths.find((p) => existsSync(p)) - 写入本地文件docs/archive/dry-run-cli-publish.mjs:172
mkdirSync(binDir, { recursive: true }) - 写入本地文件docs/archive/dry-run-cli-publish.mjs:174
writeFileSync( - 写入本地文件docs/archive/dry-run-cli-publish.mjs:213
chmodSync(stub, 0o755) - 读取本地文件docs/archive/dry-run-cli-publish.mjs:507
if (!existsSync(full)) { - 读取本地文件docs/archive/dry-run-cli-publish.mjs:511
for (const specifier of relativeImportSpecifiers(readFileSync(full, 'utf8'))) { - 读取本地文件docs/archive/dry-run-cli-publish.mjs:535
const source = readFileSync(join(dir, file), 'utf8') - 写入本地文件docs/archive/dry-run-cli-publish.mjs:581
mkdirSync(join(tmp, 'scripts'), { recursive: true }) - 写入本地文件docs/archive/dry-run-cli-publish.mjs:582
writeFileSync( - 写入本地文件docs/archive/dry-run-cli-publish.mjs:586
writeFileSync(join(tmp, 'scripts', 'b.mjs'), "export { c } from './c.mjs'\n") - 删除本地文件递归docs/archive/dry-run-cli-publish.mjs:602
rmSync(tmp, { recursive: true, force: true }) - 读取本地文件docs/archive/dry-run-cli-publish.mjs:617
if (!existsSync(binary) && existsSync(`${binary}.exe`)) binary = `${binary}.exe` - 读取本地文件docs/archive/dry-run-cli-publish.mjs:619
const text = readFileSync(RELEASE_YML, 'utf8').replace(/ /g, '\n') - 读取本地文件docs/archive/dry-run-cli-publish.mjs:641
if (!existsSync(binary)) { - 执行外部命令固定命令docs/archive/dry-run-cli-publish.mjs:740
const result = spawnSync('bash', ['-euo', 'pipefail', '-c', step.script], { - 删除本地文件递归docs/archive/dry-run-cli-publish.mjs:816
if (failures.length === 0) rmSync(work, { recursive: true, force: true })
扫描于 2026/10/31(1 天前) · 范围 全包扫描 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
README
由 DeepSeek 最新模型翻译生成DSH Desktop (Tauri)
English Document
使用 Rust + Tauri 2.0 重新构建的 DeepSeek Harness 桌面套壳。
本仓库是从零开始的 Rust/Tauri 移植版本,目标是替代基于 Electron 的 dataelement/dsh-desktop 套壳。它在行为上与原始套壳保持一致,使桌面应用与 Harness Web UI 功能完全对齐,同时用更轻量、更高性能的 Tauri 外壳取代 Electron 运行时。
功能特性
状态标记约定:✅ 已接线(在运行时路径上)· ⚠️ 未接线(代码已实现并测试,但无运行时调用方)· 🟡 部分 · ❌ 未实现 · 🗄️ 已归档(曾实现,现已删除并正式裁定不做)· 🕓 计划中(无代码,且刻意不现在做)。
每项的代码证据与调用方见 AGENTS.md §7 宣称纪律。本文件与 AGENTS.md 的状态必须一致,改一处须同步另一处。
截至 2026-09-10 批次 A~G 收尾,表中已无「未接线」条目:能接的都接了,其余都正式归档并删除;剩下的「计划中」条目——安全模式界面指示器、插件卸载/禁用、runtime bundle 独立发布(Phase 2)——都是写明理由的决策,不是欠债。
展开完整文档(README 剩余 1 节)
DSH Desktop (Tauri)
English Document
使用 Rust + Tauri 2.0 重新构建的 DeepSeek Harness 桌面套壳。
本仓库是从零开始的 Rust/Tauri 移植版本,目标是替代基于 Electron 的 dataelement/dsh-desktop 套壳。它在行为上与原始套壳保持一致,使桌面应用与 Harness Web UI 功能完全对齐,同时用更轻量、更高性能的 Tauri 外壳取代 Electron 运行时。
功能特性
状态标记约定:✅ 已接线(在运行时路径上)· ⚠️ 未接线(代码已实现并测试,但无运行时调用方)· 🟡 部分 · ❌ 未实现 · 🗄️ 已归档(曾实现,现已删除并正式裁定不做)· 🕓 计划中(无代码,且刻意不现在做)。
每项的代码证据与调用方见 AGENTS.md §7 宣称纪律。本文件与 AGENTS.md 的状态必须一致,改一处须同步另一处。
截至 2026-09-10 批次 A~G 收尾,表中已无「未接线」条目:能接的都接了,其余都正式归档并删除;剩下的「计划中」条目——安全模式界面指示器、插件卸载/禁用、runtime bundle 独立发布(Phase 2)——都是写明理由的决策,不是欠债。
已接线能力
- 内置运行时 ✅ —— 自带 Node.js (v24) 与完整的 @deepseek-ai/dsh 依赖树,宿主机无需预先安装 Node.js。
- 独立契约库 (dsh-contracts) ✅ —— 彻底剥离 UI 依赖,提炼统一常量、标准错误码总表 (E1xxx~E7xxx)、前后端 IPC 封套 (IpcEnvelope<T>,其 error 载荷为类型化的 AppError) 以及 JSON-RPC 2.0 规范定义(唯一定义点,dsh-host 等下游 crate 仅 re-export,不重复定义)。
- 全命令面统一 IPC 封套 ✅ —— 21 个命令中 20 个返回 IpcEnvelope<T>(成败标记 + 类型化数据 + 机器可读的 error.code / error.category),且每个壳页面都会解包该封套;唯一刻意的例外是 install_mode(安装形态判定,直接返回裸字符串 installed / portable / unknown)。失败不用 reject 表达,因此页面可以按错误的类别分派(「端口占用」→ 换端口重试 vs「插件故障」→ 进入安全模式),而不是对英文文案做字符串匹配。
- Harness 核心生命周期 ✅ —— 在保留的 loopback 端口上拉起 Harness,提取进程级启动令牌,并轮询其 HTTP 就绪状态。
- 看门狗与崩溃自愈 (Supervisor) ✅ —— 核心宿主进程内嵌状态机与心跳监督器,提供自动恢复、进程级断路器与自愈能力。
- 孤儿进程防护(INV-3) ✅ —— Windows 走 Win32 JobObject (KILL_ON_JOB_CLOSE),Linux 走 PR_SET_PDEATHSIG + 进程组,macOS 走进程组 + 退出扫描;主程序崩溃或退出时不残留子进程。
- 插件运行异常守护(进程内) ✅ —— plugin-safety-guard.mjs 拦截未捕获异常与未处理 Promise 拒绝,产出 [dsh-plugin-fault] 归因信息,让插件故障可归因、可诊断而不是静默发生。它运行在 Harness 进程内,因此不隔离崩溃的插件:该进程内的硬崩溃仍可能带走 Harness(见下方「已归档」的插件隔离条目——本仓库不得作相反表述)。
- 安全模式与故障恢复 ✅ —— 自动检测启动失败原因并写入独立隔离 profile,随后真正以该 profile 启动 Harness(--profile desktop-safe-mode 搭配 dsh-desktop-safe.patch.yml,后者会摘掉常规补丁层挂载的产品插件),保障基础功能可用。注意:安全模式生效期间,壳层界面上尚无任何提示——该指示器属刻意后置的功能,见后续计划。
- 多 Profile 与会话管理 ✅ —— 内置 Session / Profile 状态管理与元数据持久化,支持多环境无缝切换。
- 壳层结构化日志 ✅ —— tauri-plugin-log 落盘到 app_data_dir/desktop.log(5 MB × 2 轮转,含本地时区),与 Harness 侧 harness.log / app.log 分离,便于归因「是壳的问题还是 Harness 的问题」。
- 移动桥接 (Mobile Bridge) ✅ —— 局域网 HTTP 服务,配对页内置二维码与配对令牌,转发 RPC 到 Harness。默认不监听,需从应用菜单「Phone Pairing (LAN)…」显式启动;受 Harness 的 dsh-auth- cookie 握手与会话令牌双重约束,进程退出即失效。菜单的 Phone 子菜单会实时显示桥状态(off / listening / paired)——托盘菜单里同样有一份——Harness 侧边栏另有一枚对应的页内状态指示器。
- 该指示器由壳层注入到 Harness 页面(见下方Harness 页面注入)。它只表示状态、刻意不可点击:配对与停止在原生 Phone 菜单里,壳层不会为了复制一个菜单项而向 Harness 这个远程 origin 开 IPC 入口。文案随连接状态切换(「手机未连接」/「手机已连接」),侧边栏收起且未配对时保持隐藏。
- 桌面深度定制 ✅ —— 通过 patch-package 补丁以及传给 web --patch 的 patch.yml 层应用桌面品牌资源与 UI 行为。补丁按 functional / ui-behavior / brand 三层分级(见 patches/LAYERS.md),失败时默认降级并在 MANIFEST.json 的 patches[] 逐条留证;--strict 可恢复全量 fail-fast。
- 单实例锁定 ✅ —— 第二次启动时把已有窗口带到前台(若它正停在托盘里,会先把它取回),避免重复拉起多实例。
- 打包版 DevTools ✅ —— Harness → Toggle Developer Tools(CmdOrCtrl+Alt+I)在 release 构建里同样能打开主窗口的开发者工具(依赖 Tauri 的 devtools feature)。壳起不来、日志查看器本身也打不开时,这是唯一的排障入口。
- 更新就绪的可见回执 ✅ —— 关窗即隐藏意味着更新页可能根本不在屏幕上;下载完成时托盘多出一行状态、系统弹一条只含版本号的通知。通知失败只记 debug——判定的回执仍是 updates://status 状态机。
- 更新失败分类 ✅ —— 更新失败带稳定错误码(E1005~E1008:check/download/install × network、签名拒绝、其他),页面据此分派,而不是对上游 updater 的英文文案做字符串匹配。
- 更新 journal ✅ —— app_data_dir/update-journal/updates.jsonl 记录每次更新的可观察事实(阶段迁移、版本、稳定错误码、一行摘要),512 KiB 轮转。只写版本号与错误码,不写 URL / Token / 路径;写盘失败降级为 warn,绝不让更新因它失败。
- Harness 页 origin 跨重启稳定 ✅ —— 首个启动尝试优先复用上次实际就绪的端口(<launch_root>/harness.port),被占用才回退新随机端口。修的是真缺陷:WebView 的 localStorage 按 origin(含端口)隔离,端口每次随机曾让页内偏好(可重映射快捷键 dsh.keybindings.v1、dshmarket 的 WebDAV/Gist 配置)每次重启全丢。
- 退出前「有没有会话在跑」检查 ✅ —— Quit 前经已鉴权的 HTTP/RPC 问 Harness(POST /api/session/list 的 items[].running,那是 Harness 自己的权威判断):有会话在跑就弹原生模态确认(「退出 / 取消」),取消则留在原处;全部空闲才直接退出。问不出来(超时 2s / 端点不可达 / 响应形状变了)同样会问——绝不在未知时静默打断用户。
- 随包 primary runtime 载荷(office skills) ✅ 已接线 —— npm run dev / npm run build 改走 npm run prepare:runtime:拉取输入 → 组装进 src-tauri/resources/runtime/ → 校验落盘,打包 glob resources/runtime// 随即把整个载荷带进安装包。三个输入全部有锁定来源(URL/版本/sha256 见 runtime-locks/primary-runtime.json):解释器取 python-build-standalone(Release 逐产物公布 sha256);office skills 取上游 @deepseek-ai/dsh-skill-office npm 包的 assets/(三份 SKILL.md 与 check_office.py 都在里面);pnpm 取 pnpm@11.7.0。退路:DSH_SKIP_PRIMARY_RUNTIME=1 完全跳过载荷。载荷 fail-closed:缺任一项就不注入环境变量,Harness 那两行保持 disabled,而不是在启动期崩掉。注意:模型可创建/编辑 docx/pptx/xlsx 并跑纯标准库的 check_office.py,但 8 个创作库(numpy/pandas/python-docx/…)不在载荷内** —— 它们唯一的读者是上游工具描述,本仓代码零引用。
- 系统托盘 ✅ —— 应用常驻托盘,关窗只是把窗口收进托盘而不是退出:Harness 与局域网手机桥继续运行——这正是「手机连着一直开着的桌面」这个用法的前提。托盘菜单项复用应用菜单的 id 与分发,因此同一个动作不会因为「从哪儿点的」而行为不同;菜单里另有两行实时状态(Harness 相位、手机桥状态)。Windows 上左键单击切换窗口显隐;Linux 完全没有托盘点击事件(libappindicator 后端不派发),因此那边靠菜单里的「Show Window」回到窗口;macOS 用单色模板图标,保证在深色菜单栏里依然清晰。退出(托盘 / 菜单 / 错误页三处)都会先停 Harness 再退出,而不是交给操作系统强杀。
- 若托盘本身创建失败(例如 Linux 上没有 AppIndicator 支持),壳层保持旧行为:关窗即退出——一个把自己藏起来、又没有托盘能把用户带回来的窗口,会让进程变得无从触达。
- 应用内反馈入口 ✅ —— DSH Desktop → Send Feedback…(托盘与应用错误页同样有入口)会打开一个本地反馈页,在用户离开应用去 GitHub 之前就回答「我该带上什么」:页面自己从 MANIFEST.json 读出应用版本、运行时通道(next / alpha)、内置 DSH 版本与补丁统计,提供一键脱敏诊断包导出,再把用户交给四个白名单出口(Bug 表单 / 功能建议表单 / Discussions / 上游 Harness),全部经系统浏览器打开。应用自身不上传任何内容——贴什么、附什么由用户决定。通道之所以要写清楚:两条上游线共用同一批桌面版本号,光看版本号认不出运行时。
- 自动更新 ✅ —— tauri-plugin-updater 已注册,检查 → 下载 → 择机重启安装的完整链路均已接线,并配有应用内更新页(frontend/updates.html,由应用菜单「检查更新」打开)。更新源为本仓库(wang-yi-bit64/dsh-desktop),验签使用本项目自有的 minisign 密钥。发布正式包前请先读自动更新与签名密钥。
- 插件恢复流程 ✅ —— 真正的恢复页(frontend/plugin-recovery.html),在疑似插件故障时从错误页进入。它调用 recovery_status(归因结论 + 嫌疑插件清单,数据直接来自 dsh_host::diagnostics)与 recovery_action(safe-mode / restart / show-log / quit),失败在页面上可见,并监听 harness://status 让重启过程真的可见。它只提供非破坏性动作——「卸载插件」为何刻意缺席见下方「已归档 / 计划中」。
- 一键脱敏诊断包导出 ✅ —— 一键产出 app_data_dir/exports/diagnostics-<时间戳>.zip,内含三个日志文件、归因结论、环境快照与 MANIFEST.json。每个文本条目都过五条脱敏规则(launch token / dsh-auth- cookie / 路径用户名段 / API key 形态 / 代理口令),且每条规则的命中次数都写进包里——因此「到底脱敏了没有」是可核对的事实,而不是一句承诺。错误页、日志页与应用菜单三处入口。
- 应用内日志查看器 ✅ —— frontend/logs.html 读取 harness.log / desktop.log / app.log 的尾部(经 logs_read),显示文件大小,在只显示尾部时明确标记,并提供诊断导出与打开日志目录的入口。
- 无头 CLI(dsh-host-cli)—— 仓库内使用,不作为发布产物 ✅ —— 与桌面壳驱动的是同一份生命周期逻辑,但作为独立命令行二进制存在:start / stop / status / tail / probe / doctor,无 GUI、不依赖 Tauri。它是 INV-6(「主链路必须能在命令行里独立复现」)的兑现载体,也是 scripts/smoke-launch.mjs(L1/L2 冒烟)与 scripts/fault-inject.mjs(三平台孤儿清理硬门禁)的执行对象,两者都从 target/debug/ 取二进制。它不作为 Release 资产发布。 发布通道自 v0.4.0 存在,已于 2026-09-24 退役:本仓之外零消费者,且归档从来就不自足——它不含 runtime,没有已组装的资源树时 start 必然退出码 3(用 --resource <RES> 指过去)。退役未触动 crate 本体、打包脚本及其可证伪的打包判据,只归档了上传步骤与它的演练。理由与恢复触发条件见 docs/dev-plan-cli-distribution.md §5。
已归档(曾实现,现已删除——勿宣称可用)
- 插件分级隔离 2.0 (Tier 0/1/2) 🗄️ 已归档(2026-09-10) —— Tier 0/1/2 分级沙箱宿主(plugin-worker-host.mjs)、其 JSON-RPC 2.0 双向通信与熔断断路器(plugin_worker.rs)曾经实现且有单测,但从来没有任何调用方:Node 侧 PluginWorkerClient 无消费者,Rust 侧返回的是可辨识的 ISOLATION_NOT_WIRED 错误而非伪造成功。更关键的是它根本不在插件挂载路径上(真实挂载发生在 Harness 进程内的官方 Cordis 体系)。现已整体删除而非保留:一个既不被编译、不被测试、又够不着它所声称守护的加载器的模块,只会静默腐烂。因此当前生效的插件防护仅为上文的进程内守护——同进程崩溃仍可能带走 Harness,本仓库不得作相反表述。设计文档归档于 docs/archive/plugin_isolation_architecture.md。
- 多模型工具网关 2.0 (dsh-model-gateway) 🗄️ 已归档(2026-09-10) —— Schema 校验、anyOf/oneOf 降级净化、多厂商(OpenAI/DeepSeek/Gemini/Claude)方言适配曾实现并测试通过,但无任何运行时消费者。该 crate 已从 workspace 整体移除(目录 + members + [workspace.dependencies]),设计文档归档于 docs/archive/model_gateway_design.md,其中仍保留恢复它的判据。
- 插件卸载 / 禁用 🕓 计划中,刻意不实现 —— 恢复页原本以「卸载插件」为主按钮。它不是疏漏:卸载是破坏性且不可逆的操作,而显而易见的「可逆」替代方案(把目录改名为 <name>.disabled)经核验根本不具备可逆性——市场安装的插件由 profiles/.generations/desired.json 单一权威描述,冷启动投影会按它重放链接,手工改名会被静默撤销;而改为从该文件摘除条目,则会让 sweepRegistry() 直接删掉 generation 目录。既然只剩「假装关掉」与「真的删除」两条路,壳层两条都不提供。完整证据链见 docs/dev-plan-disconnected-points.md 批次 C。
环境要求
- Rust 工具链(stable,>= 1.90 —— 唯一产地是 Cargo.toml 的 rust-version)
- Node.js(v24 —— 见 .nvmrc,用于构建工具链)
- Tauri v2 对应的平台构建依赖(WebView2 / WebKit / WebKitGTK)
快速开始
npm install
npm run dev
npm run dev 会先把 Harness 运行时组装进 src-tauri/resources/(参见 scripts/prepare-harness.mjs),再启动桌面应用。
构建安装包 / 生产二进制产物:
npm run build
或直接调用
npm run tauri build
npm 脚本
所有动作都经 npm run 驱动。下表是入口地图,不是完整清单;逐脚本的权威清单与每道门禁「到底在守什么」见 docs/commands.md。
| 命令 | 作用 | 何时使用 |
| npm run dev / npm run build | 先把 Harness 运行时组装进 src-tauri/resources/,再启动开发调试环境 / 产出安装包 | 日常开发 · 出正式产物 |
| npm run tauri | Tauri CLI 原样透传(npm run tauri -- build --bundles nsis) | 需要 build 未暴露的参数时 |
| npm run verify:fast / npm run verify:full | 本地门禁编排:跑总表里分档为 fast 的全部门禁 + 无头 cargo test;full 额外含 doc-test | 每次提交前;打 tag 前用 verify:full |
| npm run verify:sentinels | 两条需联网的真检查(npm run gate -- drift、npm run gate -- update-channel)成组。它们因外部状态而非你的代码变红,这正是它们被排除在 verify:fast 之外的原因 | 每周例行,或规划上游升级时 |
| npm run verify:all | verify:full + verify:sentinels:本仓本地能验的全部 | 发布前最后一次本地核验 |
| npm run gate -- version | 版本号在 package.json / tauri.conf.json / Cargo.toml 三处一致(tag 构建时另校验 tag ↔ 版本) | 打 tag 前——不一致会让所有已安装用户收到错误版本 |
| npm run smoke:headless / npm run smoke | 分层烟雾:L1 无头(派生 → 就绪 → 真的在服务页面 → 干净退出、无孤儿)与 L1 + L2(L2 需已构建的壳二进制,缺失时自行 SKIP) | 任意机器跑 smoke:headless;npm run build 之后跑 smoke |
| npm run fault-inject | 针对真实 dsh-host-cli 验证孤儿进程清理与退出码归因 | 改动进程 / 生命周期相关代码后 |
| npm run size:report | 壳二进制、安装包与资源树三口径体积 | 构建后需要对外给出体积数字时 |
| npm run prepare:harness | 只组装运行时(-- --dsh-target=alpha 选通道,-- --check 只校验树) | 需要真实资源树才能跑 cargo build / cargo test 时 |
| npm run harness:lockfile | 重新生成某目标的提交式 lockfile 与 inputs 对(-- --dsh-target=next) | 版本锚点 / 补丁集 / vendored 包变更后 |
| npm run check:patch-applicability / npm run report:patches | 上游升级预检(哪些补丁仍适用,约 4 MB 下载)与补丁健康度报告 | 升级上游期间;产出 MANIFEST.json 的构建之后 |
| npm run package:cli / npm run gate -- cli-package | CLI 打包链路:产物命名、.sha256 边车、回读校验、真执行一次 | 需要本地 dsh-host-cli 归档时(它不是发布产物) |
| npm run version:show / version:bump / changelog:notes / changelog:write | 版本查看与推进(依 Conventional Commits 判定)以及变更日志生成 | 发版流程中 |
2026-10-03 起,门禁清单只有一处产地:scripts/gate-manifest.mjs。 每条门禁在那里登记一次(脚本 / 参数 / 分档 / 为什么有它),ci.yml、release.yml 与本地 fast 档都从它派生,不再各抄一份。跑门禁用 npm run gate -- <name>;旧写法 npm run verify:<name> 仍能经门禁 CLI 解析(npm run gate -- verify:<name>),历史文档与 ADR 因此不必改写。
入口脚本名是对外契约——只新增,不改义。 .github/workflows/、src-tauri/tauri.conf.json(beforeDevCommand / beforeBuildCommand)、AGENTS.md 与 docs/ 都按名字引用它们。改名从不只是本地改动:它要么打断某个工作流,要么让某道门禁静默消失。新能力请用新名字——新门禁登记进总表,不要在 workflow 里另抄一行。
仓库与 Workspace 结构
crates/
dsh-contracts/ # 纯 Rust 通用契约库(常量、错误码总表 E1xxx~E7xxx、IPC 封套、JSON-RPC、生命周期定义) dsh-host/ # 无 GUI 核心宿主库(子进程管理、Supervisor 监督器、崩溃诊断、日志环形缓冲、脱敏诊断包导出、日志尾部读取) dsh-host-cli/ # dsh-host 命令行工具(支持 start / status / stop / tail / doctor 等) src-tauri/ frontend/ # 壳页面静态资源(Splash 启动页、Error 错误归因页、插件恢复页、更新页、日志查看器、反馈页) resources/ # 组装好的 Harness 运行时 + 品牌资源(已 gitignore,构建自动生成) src/ # Tauri 桌面应用层(窗口管理、应用菜单、IPC 命令、安全模式切换、LAN 手机桥、壳层日志、自动更新) commands.rs # 完整 IPC 命令面:21 个命令(20 个返回 IpcEnvelope<T>,install_mode 返回裸字符串) logging.rs # 壳层结构化日志(desktop.log,5MB × 2 轮转) mobile_bridge.rs # 局域网手机桥(配对页 + dsh-auth-* cookie 握手 + RPC 转发) build/ # 运行时组装与辅助注入脚本 harness-node-entry.mjs # Node 端入口与启动参数适配(含 cold-start 投影与故障归因接线) plugin-safety-guard.mjs # 插件运行异常全局守护网 —— formatFaultDetails 是当前唯一生效的插件防护(进程内) patches/ # 应用到 Harness 依赖树的 patch-package 补丁 + LAYERS.md 分级清单 vendor/ # 本地桌面定制包(dshmarket 等) packages/ # 被打补丁包的 vendored tgz 覆盖 .github/ # 工作流(ci / smoke / drift / release)+ Issue 表单与 Discussions 入口 ISSUE_TEMPLATE/ # Issue 表单:bug_report.yml(内嵌脱敏诊断包两步指引)、feature_request.yml、config.yml scripts/ # 构建与测试辅助(prepare-harness、stub-tauri-resources 等) prepare-harness.mjs # 组装运行时到 src-tauri/resources/(补丁按级降级,--strict 恢复 fail-fast) stub-tauri-resources.mjs # 全新 checkout / CI 用的仅编译桩资源 mock-harness.mjs # 可注入故障的假 Harness,供集成测试使用 fault-inject.mjs # 孤儿进程与失败归因验证 patch-layers.mjs # 补丁分级唯一产地(--self-test / --list 自检) smoke-launch.mjs # CI 分层烟雾(L1 无头门禁 / L2 GUI xvfb) report-bundle-size.mjs # 采集壳/安装包/资源树体积,写入 CI job summary docs/ # 架构设计、契约定义与技术方案 roadmap.md # 【顶层路线图】定位、边界与阶段序列(H0~H3) dev-plan-hardening-and-differentiation.md # 近端施工计划(路线图 H0 阶段):风险清单 R1~R9 与批次 H~N dev-plan-0.2-hardening.md # 【产品与分发侧增补】批次 0.2-A~D:发布通道、体验底线、反馈闭环(OS 代码签名:不做——ADR-046) dev-plan-disconnected-points.md # 上一阶段主计划(已闭环):断线点清单(D1~D11)与批次 A~G 施工记录 version-policy.md # 【版本号策略唯一口径】命名规则、按变更类型的递增条件、递增权限(谁有权决定版本号)、发布流程与守卫 dsh-desktop-redesign-architecture-and-plan.md # 架构重构与开发执行完整计划 system_design.md # 系统架构设计与不变量规范 archive/ # 已归档设计(刻意不做,代码已删,仅留作追溯) model_gateway_design.md # 多模型网关(2026-09-10 归档;仍保留恢复判据) plugin_isolation_architecture.md # 插件隔离沙箱(2026-09-10 归档) dsh-upgrade-checklist.md # DSH 官方版本升级清单(补丁重生成 → 门禁 → 三平台烟雾) harness-packaging-and-compatibility.md # 产物瘦身与补丁脆弱性治理的长期方案
架构演进与路线图 (Roadmap)
设计与当前状态对照
下表左列是设计目标,右列是当前是否在运行时路径上。二者不可混用(详见 AGENTS.md §7)。
| 阶段 | 设计范围 | 当前状态 |
| P0 | 契约库 dsh-contracts、错误码分类体系、跨平台孤儿进程防护(Win32 Job Objects / POSIX 进程组)、无头核心宿主库(dsh-host、dsh-host-cli) | ✅ 已接线 |
| P1 | Supervisor 状态机与自愈、日志环形缓冲区(LogRing)、崩溃归因分析(DiagnosticsAnalyzer)、安全模式(Safe Mode)隔离 Profile | ✅ 已接线 |
| P2 | 解耦 Worker 线程沙箱(plugin-worker-host.mjs)、JSON-RPC 2.0 双向通信、故障计数与断路器熔断自愈(plugin_worker.rs) | 🗄️ 已归档(2026-09-10):从未有调用方,且根本不在插件挂载路径上;整体删除而非留着腐烂。只剩进程内故障归因 |
| P3 | 多厂商工具调用 Schema 清洗、复杂嵌套/anyOf/oneOf 降级、Payload 组装适配(dsh-model-gateway)、微秒级性能基准 | 🗄️ 已归档(2026-09-10):已从 workspace 整体移除;归档文档保留恢复判据 |
| P4 | Tauri Commands 统一采用 IpcEnvelope<T> 封套返回;一键脱敏导出诊断包 (diagnostics.zip);应用内日志查看器 | ✅ 已接线:21 个命令中 20 个返回 IpcEnvelope<T>(install_mode 刻意返回裸字符串);脱敏导出(5 条规则,落 app_data_dir/exports/);frontend/logs.html |
后续计划(尚未开工)
- 安全模式界面指示器 🕓 刻意后置 —— 安全模式生效时,壳层目前不给用户任何可见提示。上游的做法是往 Harness 页注入一条横幅(带「卸载插件」/「退出安全模式」两个动作)。这项能力不是遗漏:它刻意推迟到壳层其余部分稳定之后再做,好让注入机制在已定型的地基上一次性建好,而不是事后回补。安全模式本身当前工作正常(见「已接线能力」),待补的只是它的「可见性」。
- 插件卸载 / 禁用 🕓 刻意后置 —— 见上方功能表。这一项在等上游的停用(disable)语义,不是等壳层开发:眼下只有「冷启动被静默还原」与「不可逆删除」两种行为可选。
- runtime bundle 独立发布(Phase 2) 🕓 带触发条件,刻意后置 —— 无代码。门槛与前置改造写在 docs/dev-plan-cli-distribution.md §4:出现第一个本仓之外的消费者,或开工 H3-a 运行时更新事务 / H2-a 兼容矩阵时才做。
测试
无头门禁无需显示器、也无需组装 300MB 运行时(INV-6):
cargo test -p dsh-contracts -p dsh-host -p dsh-host-cli
集成测试会真实派生 Node 进程运行 scripts/mock-harness.mjs,因此需要 PATH 上有 Node.js(可用 DSH_TEST_NODE 指定),找不到时会自行跳过而非失败。各类故障模式(启动失败、不打印 URL、端口占用重试、就绪后崩溃)通过 mock-harness.mjs 的 argv 与环境变量注入,而不是在 Rust 侧打桩。scripts/fault-inject.mjs 额外针对 dsh-host-cli 验证孤儿进程清理与退出码归因。
校验门禁一览
本文件里有若干宣称编译器查不出来——它们要么跨语言,要么描述的是「产出的文件长什么样」而不是某个函数的行为。每一条都有对应的门禁:
| 命令 | 它到底在守什么 |
| npm run gate -- ipc-surface | 命令定义 ↔ generate_handler! 注册 ↔ 页面 invoke/listen ↔ local_page 目标 ↔ #[allow(dead_code)] 登记。src-tauri 是 rlib,没人调用的 pub 命令不会触发 dead_code——这是唯一能抓到「写了但没接线」的东西。 |
| npm run gate -- shell-pages | 把每个壳页面的内联脚本放进 DOM 桩里真跑一遍,并逐个点一遍按钮。抓 getElementById 返回 null(脚本会就此中断,该页所有监听全部失效),以及 HTML 留了按钮却没挂监听。 |
| npm run gate -- harness-inject | Harness 页注入脚本的 DOM 行为,含可证伪性检查:把脚本回退成上游行为,断言必须变红。 |
| npm run gate -- patches | patches/<target>/ 与 scripts/patch-layers.mjs 的分级清单一致——逐目标各查一遍(next 与 alpha):只验默认目标会让另一条线的补丁整目录漏登记、或版本段停在旧值而无人发现。每个补丁文件名仍须能推导出包名;分级表按包名索引,因此新增一条通道不需要动它。 |
| npm run gate -- targets | 构建目标总表本身(scripts/dsh-targets.mjs,别与 target 门禁混淆):通道 ↔ 目标 ↔ 钉住的上游版本、版本号 → 目标的解析规则,以及「未知预发布通道不得回退默认目标」这条硬约束(静默回退会产出「版本号说一条线、运行时却是另一条线」的包)。纯逻辑,不联网。 |
| npm run gate -- prune / npm run gate -- variants | 决定出厂 node_modules 形状的两条剪枝规则:哪些开发产物目录可安全删除(看内容不看名字——yaml/dist/doc 是运行时路径),以及哪些外来平台原生变体必须在 linuxdeploy 扫描 AppDir 前清掉。两条都带针对旧判据的可伪证性检查。 |
| npm run gate -- version | 版本号在 package.json(唯一真源)/ tauri.conf.json(继承真源)/ Cargo.toml(脚本同步)三处一致;tag 构建时额外校验 tag 与版本号匹配。不一致会让安装包自称另一个版本,updater 据此决定推不推更新——错一次影响所有已安装用户。 |
| npm run gate -- commits / npm run gate -- changelog | 变更日志生成器与其解析器的自测。一个写坏了却静默产出空变更日志的脚本,比没有脚本更危险——Release 页会显示「没有任何改动」。 |
| npm run gate -- target | 构建主机与打包目标是同一平台/架构——在 300MB 运行时被组装进产物之前就拦住。 |
| npm run gate -- release-workflow | 发布工作流自身的静默失败模式:tauri-action 会自插 build 与 --(故 tauriScript 不能带这两者),以及 shell 变量后接非 ASCII 标点时须写 ${花括号}(否则 macOS bash 3.2 会把标点并进变量名)。这两条曾让首个 v0.1.0 发布三平台全红,而本地门禁全绿。自 2026-09-24 起,对已退役的 CLI 发布通道判据方向反转:cli / cli-publish 两个 job 必须缺席,而 gh release upload 按宾语判——上传 dist/portable/ 放行(3 个便携版是期望 13 项的一部分),上传 dist/cli/ 判红。同时 dsh-host-cli crate、打包守卫、以及走 target/debug/ 的消费者都必须仍在——「取消发布」与「删掉 crate」是两件事,两个方向都有夹具证明。 |
| npm run gate -- release-assets | 期望的发布资产清单(13 项)。按名字模式逐项枚举而不是数个数:10 项来自 tauri-action(安装包、.sig 边车、macOS updater 包、latest.json),另 3 项是便携版。之所以要它:13 这个数字此前只活在文字里,是一句承诺而非断言,而 F13 就是在全部门禁绿灯下产出 10 项。发布后跑 node scripts/verify-release-assets.mjs --check-release <tag> 可把真实 Release 与期望清单逐项比对。⚠️ 模式取自实测的 Release 输出,不是从 tauri.conf.json 反推:bundler 把 DSH Desktop 渲染成 DSH.Desktop_<版本>_x64-setup.exe(点号分隔产品名、下划线分隔版本),而 macOS 的 updater 包不含版本段。喂入连字符写法的夹具必须判红。 |
| npm run gate -- cli-package | CLI 打包链路(scripts/package-cli.mjs):产物名由 package.json 与目标三元组推导(不接受手写)、.sha256 边车是 sha256sum -c 真能读的格式、归档解包回读逐字节一致且可执行位完好。同一脚本幂等地渲染 Release 正文表格,并带可伪证夹具——被篡改的解包内容、丢掉的可执行位、不匹配的边车、缺失的归档,全都必须判红。 |
| npm run gate -- portable-package | 便携版打包链路(scripts/package-portable.mjs):产物命名、.sha256 边车形状、以及「解 zip 按平台选命令」——发布 job 跑在 ubuntu 上而要核验的是 Windows 产出的 .zip,无条件 spawn powershell 会让核验必然失败。真打包与输入缺 WebView2Loader.dll / 体积不达标必须拒绝打包等可伪证夹具依赖 PowerShell,因此这一步只在 Windows 上跑;解包命令那条是纯逻辑(平台是入参),跑一次等于两个分支都验到。 |
| npm run gate -- harness-entry | 壳入口 ↔ 上游 dsh CLI 的调用约定兼容性:上游 0.1.5-rc.1 把 CLI 改成 if (import.meta.main) runCli(),因此import 它的包装器必须显式调用导出的 runCli(),否则进程以退出码 0 静默结束。同时钉住 macOS 父死看门狗——由入口与 mock-harness.mjs 共同安装(故障注入的 mock 模式会把入口整体替换掉)。带可证伪性自检。 |
| npm run gate -- profile-names | 任何 profile 字面量都不得等于官方保留名 desktop(大小写不敏感)——官方桌面版独占该 profile。同时钉住两个契约锚点(SAFE_MODE_PROFILE 保持 desktop-safe-mode、HARNESS_CLI 保持裸子命令 web)。带可证伪性自检。 |
| npm run gate -- claims | README ↔ AGENTS.md 的宣称纪律:AGENTS.md §7 明令禁止的表述不得出现在两份 README;五种状态词须三处俱全;§7.2 表里每一条欠债行都必须登记。带以修复前原文为夹具的可证伪性自检。 |
| npm run gate -- drift / npm run gate -- drift --self-test | 两条通道是否各自落后于自己的 npm dist-tag——next 对照 next tag、alpha 对照 alpha tag。落后一个 minor 位或预发布阶段即失败;同阶段内只落后补丁位仅提示;registry 不可达时打印 SKIP(不等于「已核对」)。真检查跑在 nightly 定时任务;CI 只跑自检。 |
| npm run report:patches | 补丁健康度报告:把层 / 退役条件表与 MANIFEST.json 里逐条真实的 applied/skipped/failed 合并成一张表。manifest 缺失时如实标为不可用——不伪造 applied。 |
| npm run check:patch-applicability / --target=<v> | 上游升级预检:仓库内补丁在目标版本上哪些仍可用、在哪一段 hunk 断裂。约 4MB 下载,不必组装 300MB。零外部二进制(Node fetch + zlib + 自带 tar 读取器)。 |
| npm run fault-inject | 针对真实 dsh-host-cli 验证孤儿进程清理与退出码归因(10 项断言)。 |
| npm run smoke:headless / npm run smoke | 分层烟雾:L1 无头(派生 → 就绪 → 真的在服务页面 → 干净退出、无孤儿)与 L2 GUI 启动。 |
| npm run verify:fast / npm run verify:full | 编排器(scripts/gates.mjs)跑 scripts/gate-manifest.mjs 这一份清单。某个分档解析出 0 步即判红——本仓已两次出现「清单被静默清空却一路绿灯」;npm run gate -- gates 另守着「每个支持 --self-test 的脚本要么登记、要么带理由豁免」。fast = fast 档 + cargo test --tests;full 额外含 doc-test。 |
| npm run verify:sentinels / npm run verify:all | 两条联网哨兵(npm run gate -- drift、npm run gate -- update-channel)成组;它们会因外部状态红,故刻意不在 verify:fast 档内。verify:all = verify:full + verify:sentinels,即发布前本仓本地能查的全部。 |
npm run gate -- shell-pages 不替代肉眼看页面:它不渲染、不布局、不跑 CSS。它只回答一个很窄的问题——页面脚本加载完之后,该挂的监听是不是都挂上了。
说明
- node_modules/、harness-deps/、src-tauri/target/ 与 src-tauri/resources/ 均已被 gitignore;其中 resources/ 由构建脚本按需组装生成,避免增大仓库体积。
- 在全新拉取的无资源环境中运行 cargo check 或 cargo test 前,可先执行 node scripts/stub-tauri-resources.mjs 生成桩资源以通过 Tauri 编译期校验。
- 更新端点与签名密钥:自动更新的更新源为本仓库发布页,验签使用本项目自有的 minisign 公钥。密钥私钥的存放位置、CI 如何取得、以及一旦丢失会怎样,见自动更新与签名密钥。
自动更新与签名密钥
更新端点按运行时通道各有一条:每个通道拥有一个滚动 Release(updater-rc、updater-alpha),各自只承载一个 latest.json。tauri.conf.json → bundle.createUpdaterArtifacts 已置 true,因此 npm run build 会同时产出已签名的安装包与该清单。二进制里的 URL 由 scripts/updater-manifest.mjs 在构建期注入——它是端点 URL 的唯一产地,所以「更新源」与「内置的运行时线」不可能各说各话。tauri.conf.json 里的默认值指向 rc 通道。
latest.json 必须出现在 Release 资产里——它不在,自动更新就是断的。发布工作流(见下)由 tauri-action 的 uploadUpdaterJson 负责生成上传;手工 npm run build 后自行上传产物时,别忘了这个文件。
更新的可信度完全落在同一对 minisign 密钥上:
| 项目 | 位置 |
| 公钥 | src-tauri/tauri.conf.json → plugins.updater.pubkey(可入库,它只用于验签) |
| 私钥 | ~/.tauri/dsh-desktop.key —— 严禁入库,严禁复制进仓库 |
| 离线备份 | ~/.tauri/backup/dsh-desktop.key.<时间戳>,同目录存放配对的 .pub |
| CI 获取方式 | wang-yi-bit64/dsh-desktop 仓库的 GitHub Actions Secret TAURI_SIGNING_PRIVATE_KEY,无口令使用 |
没有签名凭据时构建仍能产出安装包,但这种包无法被已发布的客户端安装——验签会直接拒绝。因此 CI 会在开始昂贵的构建之前先校验该 Secret 是否存在,缺失时以显式 ::error:: 提前失败,而不是等到最后才发现问题。
- 不做 OS 级代码签名与公证(ADR-046):上面这把 minisign 密钥是本项目唯一的完整性签名层。Windows 安装包不使用 Authenticode 证书签名,macOS 包也不做 Developer ID 公证。这是已裁决接受的边界,不是待办——本项目不会去接线证书。
- 预期中的首次运行摩擦与解法:macOS 上右键 → 打开(或在「系统设置 → 隐私与安全性」中放行);Windows 上 SmartScreen 可能提示「未知发布者」,点「更多信息 → 仍要运行」。可用 Release 说明里的 SHA-256 校验你的下载。
私钥一旦丢失,对已安装用户而言不可挽回。 公钥被编译进每一个已发布的二进制;换新密钥对就意味着换新公钥,而已安装的客户端会持续拒绝由新公钥签名的更新。请把 ~/.tauri/dsh-desktop.key 当作发布关键基础设施,而不是本地开发文件。
版本与发布
版本号的唯一真源是 package.json 的 version。 src-tauri/tauri.conf.json 写 "../package.json" 原生继承(Tauri schema 明确支持该写法),因此它不存第二份值;Cargo.toml 的 workspace version 由脚本同步(Cargo 读不了 package.json)。
推进规则与 Conventional Commits 对齐,且可执行而非靠人记:
| 提交内容 | 推进 | 例子 |
| 破坏性变更(! 或 BREAKING CHANGE: 页脚) | major | 0.3.1 → 1.0.0 |
| 任一 feat | minor | 0.1.0 → 0.2.0 |
| 任一 fix / perf | patch | 0.2.0 → 0.2.1 |
| 只有 docs / chore / ci 等 | 不发版 | 产物没有任何行为变化 |
npm run version:show # 看各处版本、最近 tag、建议升级位
npm run version:bump -- auto --dry-run # 只预览,不写文件
★ 2026-09-11 起为必需:日常提交不跑任何 CI、冒烟也改为手动,这两次 dispatch 是提交在打 tag 前唯一会跑 cargo test / clippy(CI)与真实进程冒烟(Smoke)的机会。
gh workflow run ci.yml --ref main # 静态门禁 + clippy + 单测 gh workflow run smoke.yml --ref main -f scope=full # 组装真实资源 + 打包 + L2 冒烟 gh run watch # 等两个都全绿再继续
npm run version:bump -- auto --commit --tag # 落版本号 + 更新 CHANGELOG + 提交 + 打本地 tag
git push origin main:main # 第一步:推分支
git push origin "<tag>" # 第二步:显式推 tag(触发发布)
⛔ 不要用 --follow-tags:它会把「本机有、远端没有」的注释标签一并推上去,与年龄无关。远端 tag 被清理后,本机残留的同名 tag 会被它复活,而推 v* tag 会再次触发 release.yml(见 docs/release-runbook.md §8.6)。
省掉那几行 dispatch 不是无伤大雅:release.yml 的 preflight 只跑秒级静态门禁,一个从未通过三平台矩阵、也没跑过冒烟的提交会被直接打包并发布,没有任何环节会拦住它。
只想要快速一档时,smoke.yml 用默认的 scope=l1(几十秒、不起窗口);要复现发布形态就用 scope=full。
--commit 会顺带重新生成 CHANGELOG.md 的对应段落并纳入同一个提交——版本号与变更日志同属一次发布,分成两个提交就会出现「tag 指向有版本号、没变更日志的那个提交」,CHANGELOG 从此永久滞后一版。--tag 只创建本地 tag,是否推送由人决定。
变更日志由 git 提交历史自动生成(npm run changelog:write / changelog:notes),仓库内 CHANGELOG.md 与 GitHub Release 正文来自同一份数据、同一套渲染逻辑,因此不会互相矛盾。两条硬规则:认不出的提交类型(如历史遗留的 debug(ci):)归入「其他」而不静默丢弃(丢弃会让变更日志因遗漏而撒谎);破坏性变更同时出现在置顶章节与它的类型章节。
为什么不用 GitHub 内置的 --generate-notes:它按已合并 PR 归纳,而本仓库全程直推 main(gh pr list --state all 为空)。实测其产出只有一行 Full Changelog: …、零条目。
运行时通道(next / alpha)
壳层内置的上游运行时双通道并行维护:每条通道各自钉一个 @deepseek-ai/dsh 版本,并各持一套补丁(patches/<target>/)与 vendored 覆盖包(packages/<target>/):
| 目标 | 上游线(upstreamDistTag) | 固定的 DSH | 桌面后缀(publishChannel) | 桌面版本形态 |
| next(默认) | npm next dist-tag(rc 阶段) | 0.2.1-rc.4 | rc | <x.y.z>-rc.<n>[+<w>] |
| alpha | npm alpha dist-tag(下一 minor 的早期预览) | 0.2.1-alpha.5 | alpha | <x.y.z>-alpha.<n>[+<w>] |
桌面版本号是「合成号」,不是上游版本号的副本。 自 ADR-061 起它是 <上游精确版本>.<n>[+<w>]:前三段以及上游自己的整段预发布标识(rc.2、alpha.1)原样照抄,<n> 是本仓的构建序号(按上游版本分组计数:台账 builds[] 里同组的 max(n) + 1),+<w> 是人读批次标签、永不参与排序。所以 next 目标(其上游是 rc 阶段版本)的桌面版本长成 0.2.0-rc.2.<n> 这样,而不是 0.7.x。上表那一列因此给的是形态而不是值:写一个具体号,锚点一动它就漂,而没有任何守卫盯着那一格。ADR-057 的跨通道单调要求(alpha 线的版本必须排在最高 rc 线之上)继续有效——x.y.z 来自上游、<n> 归本仓,因此可满足;它原先那句数值示例(0.7.3-alpha.1 > 0.7.2-rc.1)是旧模型下的下游结论,已作废。
两个「通道名」不是一回事,别混用。 每个目标带两个字段:upstreamDistTag 是组装时拉哪条上游 npm dist-tag(上游客观事实,改不了名;2026-10-09 由 channel 改名——旧名会被读成「本仓发布通道」),publishChannel 是桌面 tag 的预发布后缀(本仓自己的命名)。上游 next dist-tag 当下指向一个 rc 阶段版本,所以桌面后缀是 rc 而目标键仍是 next。若把两者强行同名,一改桌面后缀就会让漂移哨兵去查一个上游不存在的 rc tag,静默退回 latest。本仓「通道」一词现在只指桌面通道(stable / rc / alpha,由版本后缀推导)。
⚠️ 目标键告诉不了你钉的是哪个上游版本。 目标的 upstreamDistTag 是它对着哪条上游 dist-tag 定义的;本仓实际钉住的是哪个上游版本由 harness-locks/dsh-releases.json(每个目标一条在役键)回答——目标表的 dshVersion 只是组装锚点(答「用哪套补丁目录」),两者历史上可以不一致。截至 2026-10-29 next 单线推进后,next 钉住 0.2.1-rc.4,alpha 钉住 0.2.1-alpha.5(与上游 dist-tag 是否一致可用 npm run gate -- drift 核对)。要权威值请读台账;不要按目标键推断版本。
桌面版本号的预发布后缀就是 publishChannel——在合成号模型下,它就是上游自己的那段预发布标识,原样保留:next 目标(上游是 rc 阶段版本)的构建形如 <x.y.z>-rc.<n>,alpha 的形如 <x.y.z>-alpha.<n>。发布流程从 tag 本身反推构建目标(scripts/dsh-targets.mjs --channel-of),tag 后缀因此自动决定组装哪条运行时——不需要在 tag 之外再声明一次通道。后缀不对应任何已知通道的 tag(例如 beta)会直接让发布失败,而不是回退到默认目标:静默回退会产出「版本号说一条线、运行时却是另一条线」的包,这类错配只有用户装上之后才会被发现。⚠️ 旧的 -next.1 后缀已不再是合法输入——随 2026-10-09 的改名退役,合成号模型也不会产出它。
本地组装某条通道用 --dsh-target(缺省 next):
npm run prepare:harness -- --dsh-target=alpha
每条运行时通道读自己的滚动清单,因此一个构建会到达本通道的用户。旧的单一端点 releases/latest/download/latest.json 之所以废弃:GitHub 的「latest」按定义不含 prerelease,而本仓版本全是预发布——它返回的版本号比任何 0.6/0.7 构建都低,2026-09-15 到本次改动之间自动更新一次都没送出去。⚠️ 该改动之前发布的构建把旧端点烧进了二进制,它们无法自动更新,需要手动装一次新构建;updater-rc / updater-alpha 是更新通道载体,不是可安装版本。
发布工作流
| 工作流 | 触发 | 做什么 |
| .github/workflows/ci.yml | 任意 PR / 手动(刻意不监听 push) | 三平台 test(静态门禁 + clippy + 单测)。不组装资源、不打包、不跑烟雾 |
| .github/workflows/smoke.yml | 仅手动(workflow_dispatch) | 按需跑的冒烟测试,用 scope 分档:l1(mock 资源树 L1 + 可选故障注入)/ assembled(组装真实资源树 + 真实树 L1)/ full(+ 打安装包 + L2 GUI 烟雾 + 体积采集)。不发布任何东西 |
| .github/workflows/release.yml | 推 v tag / 手动指定 tag | preflight(版本↔tag 一致性、按 tag 解析运行时通道 + 秒级静态门禁)→ 三平台并行出包,创建/更新 GitHub Release 并上传安装包、.sig 签名与 latest.json |
- 日常提交不触发任何 CI:ci.yml 不再监听 push(也从不监听 tag——出包只有一个产地 release.yml)。静态门禁 + 单测已够一次提交拿到快反馈;组装 300MB 运行时、打包、起窗口这些成本高的动作不该挂在每次提交上。
- 冒烟测试改为手动触发:L1/L2 冒烟从 ci.yml 迁到独立的 smoke.yml,只由 workflow_dispatch 触发。保留能力、去掉自动化正是目的——需要时能跑,平时不占额度。三种 scope 对应三档成本;gh workflow run smoke.yml -f scope=l1 是命令行等价物。它用的是同一批脚本与同一套断言,所以这是搬家,不是另立一套判据。
- 发布用 tag 触发而非 main 提交:发布是一次性、不可撤销的动作。tag 是显式的单一意图声明;让每次提交都发版会把「发布」退化成无需决策的背景动作。手动触发用于失败后指定同一 tag 重跑(删已发布的 tag 是破坏性操作)。
- 各平台产物类型显式声明:Windows nsis、macOS app,dmg、Linux deb,appimage。不依赖 tauri.conf.json 的 bundle.targets——那里只有 Windows 专属的 nsis。
- 与 CI 的边界(2026-09-11 重述):发布流程不重跑整套 Rust 测试矩阵,preflight 只跑秒级门禁以便在组装 300MB 资源前失败。⚠️ 由于 ci.yml 不再随 push main 触发、冒烟也改为手动,「tag 提交早已绿过」这个前提不再自动成立,而 preflight 看不见运行时行为——发布前请分别手动 dispatch CI(静态 + 单测)与 Smoke(冒烟)两个工作流并等都变绿,再打 tag。给从未跑过门禁的提交打 tag,Release 不会发现,这是有意接受的边界。
Harness 页面注入
壳层通过 Tauri 的 initialization_script 在每一次 Harness 页面加载时注入一小段脚本——它等价于 Electron 的 preload。脚本在文档解析之前执行、在每次顶层导航时生效,并按 origin 自我早退,因此壳层自己的本地页(index.html、error.html、updates.html 等)与子框架都不受影响。
有两点必须说清,因为本文档早期版本把它们搞错了:
1. Harness 页上是可以定义 window. 全局的。 本文档曾写道「Tauri webview 没有 preload / initialization script」——那是一个错误的机制判断。它当年用来支撑的「目录选择器规则」依然成立,但依据已改为真正成立的理由,见上文目录选择器一段。
2. 这条通道是单向的:壳层 → 页面。 状态经 webview.eval() 推送,因此不向 Harness 这个远程 origin 暴露任何 IPC 命令。任何需要反向通信的能力都被刻意不做。
当前消费方是手机状态指示器(src-tauri/frontend/harness-ui-inject.js,由 src-tauri/src/harness_ui.rs 推送)。它的 DOM 侧行为由一套基于最小 DOM 桩的无头自测覆盖:
npm run gate -- harness-inject
该套件还带一项可证伪性检查:它把脚本回退成上游的行为(会把指示器以未渲染状态漏进侧边栏),并要求断言必须变红;若不变红则门禁失败。一个不可能失败的测试是装饰,不是防线。
体积现状与期望管理
安装包体积主要由内置 Node.js 运行时 + 完整 Harness 依赖树决定,壳层(Rust/Tauri)自身的贡献很小。这不是「胖壳」问题,而是为了「宿主机零依赖」付出的必然代价——把这个前提说清楚,比给出一个笼统的 MB 数字更有用。
体积数据由 scripts/report-bundle-size.mjs 在构建后采集并写入 CI job summary,避免 README 里的数字随时间失真。三个可区分的体积口径:
| 口径 | 含义 | 受什么影响 |
| 壳二进制 | src-tauri/target/release/.exe(未打包) | Rust 依赖与 LTO 设置;与 Harness 无关 |
| 安装包 | NSIS -setup.exe | 壳二进制 + 压缩后的资源树 |
| 资源树 | src-tauri/resources/(未压缩) | Node 运行时 + harness/node_modules 依赖树;体积主项 |
实际数值以 npm run build 后 node scripts/report-bundle-size.mjs 的输出为准(或见 CI 的 build job summary)。本 README 不再维护会失真的手写数字。
若需显著瘦身,方向是减少打包进 resources/ 的依赖(见 docs/harness-packaging-and-compatibility.md),而非改动 Tauri 侧配置——后者对总量的影响在个位数 MB 量级。
Windows 打包注意事项
此前有三个缺陷会让 Windows 构建产出的安装包启动即失败,现已全部修复。本节记录根因,避免回归。
- \\?\ verbatim 路径绝不能传给 Node 子进程。 Tauri 的 resource_dir() 来自 current_exe().canonicalize(),Windows 上返回扩展长度路径(\\?\D:\…)。把它拼进 Node 入口脚本后,CJS loader 会将其还原成裸盘符 D:,lstat 抛 EISDIR: illegal operation on a directory, lstat 'D:'。修复位于 crates/dsh-host/src/paths.rs 的 Layout::resolve——所有派生路径的唯一产地,因此没有任何调用点能再泄漏该前缀。
- prepare:harness 幂等快速路径按完整打包清单校验。 此前只检查 3 个文件,导致缺 bin/ 或 plugin-safety-guard.mjs 的 resources/ 被判定为完整并跳过组装。plugin-safety-guard.mjs 由 harness-node-entry.mjs 直接 import,缺失时 Harness 根本无法启动;缺 bin/ 还会让 cargo build 因 resources/bin/ glob 不匹配而失败。完整性检查现已对齐 tauri.conf.json → bundle.resources。
- shell == None 时同样要走 harness_env。 Launcher::execute 此前把 capture_shell_environment() 的原始结果直接交给子进程,DSH_HOME 等契约变量因此从未注入,Harness 回退到 ~/.dsh,把可变状态写到了 app_data_dir 之外(违反 INV-1)。GUI 与 CLI 两条路径现已都注入契约环境。
另有工作区目录选择器必须走 Host seam、不得使用 renderer 全局桥。这条规则原先的理由是「Tauri webview 没有 preload / initialization script,window. 全局无处定义」——这个机制描述是错的(Tauri 的 initialization_script 正是 preload 的等价物,本仓现已用它向 Harness 页注入手机状态指示器)。规则依旧成立,但要用成立的理由:曾有一个 patch-package 补丁把原生目录选择器改成 window.dshDesktopDirectoryPicker.pick(),而该全局从未被任何东西定义,因此导入项目时必然弹出「无法打开文件夹 / DSH Desktop directory picker bridge is unavailable」;并且即便今天有能力定义这样的全局,让选择器经由页面回连宿主也要为 Harness 这个远程 origin 开一个 IPC 入口,而注入机制刻意只用于壳层 → 页面的单向下发。现已移除该补丁,恢复上游 stock 路径:客户端 ctx.uiWorkspace.pickDirectory() → Host ctx.directoryPicker seam → Harness 进程内拉起 Win32 IFileOpenDialog。scripts/prepare-harness.mjs 的 assertPickerSurfaceIsHostBacked() 会在打补丁后校验这一点,违反即构建失败。
要构建真实安装包,npm run tauri build 首次运行会下载 NSIS 工具链;在网络无法访问 GitHub releases 的环境下,打包步骤会以 timeout: global 失败,但此时 .exe 与资源其实已成功产出。
安全与依赖说明
- Dependabot / RUSTSEC-2024-0429 (GHSA-wrw7-89jp-8q8g) —— glib 0.18.5 被标记存在 glib::VariantStrIter 的内存不安全性。它是经 Tauri 的 GTK3 后端引入的仅 Linux、传递性依赖,且项目未调用受影响 API。修复版本(glib ≥ 0.20.0)需等待上游 Tauri 迁移至 gtk-rs 0.20+ 后自动解析更新。
- RUSTSEC-2026-0367 / 0368 / 0369(zip < 9.0.0) —— 三条都属解包侧问题:解包时保留 SUID/SGID 位、符号链接目标可逃出解包目录、经符号链接套用 Unix 权限。zip 4.6.1 由 tauri-plugin-updater 传递引入,其最新版 2.14.0 仍锁定 zip ^4,本仓无法通过版本约束消除(把 dsh-host 自己的 zip 升到 9 只会并存第二个版本,告警照样按 4.6.1 报)。实际不可达:本仓代码从不调用 extract(只用 ZipWriter 写入);updater 的 ZipArchive::extract 仅在下载物被识别为 zip 时才走,而本项目 bundle.targets 只有 nsis、更新物是 -setup.exe,且 download() 在 install() 之前已完成 minisign 校验。处置口径记在 deny.toml 的 ignore 理由里,等上游改用 zip 9 后自动消失。
- RUSTSEC-2026-0370 / 0371 / 0372(zvariant / zbus_names / zvariant_derive 已停止维护) —— 三个 crate 都已被并入 zbus 6.0.0(分别成为其 wire 模块、names 模块与内建 derive),所以公告的 patched = []:不存在「修好」的版本,只有「换掉依赖」这一条路。zbus 6.0.0 虽已发布,但把 5.x 引进来的两个 crate 仍声明旧线——notify-rust 4.18.2 要求 zbus ^5、tauri-plugin-single-instance 要求 zbus ^5.9(2026-10-18 用 crates.io sparse index 实测)——本仓无法通过版本约束消除。三条均为 informational = "unmaintained",不含任何已知缺陷或安全影响,且两条链各有自己的 cfg 门:notify-rust 链覆盖非 macOS 的 Unix 目标(cfg(all(unix, not(target_os = "macos")))——含 Linux 与 BSD 等,本仓只构建 Windows/macOS/Linux),tauri-plugin-single-instance 链仅 Linux(cfg(target_os = "linux"))。与 RUSTSEC-2024-0370 同性质:处置口径记在 deny.toml 的 ignore 理由里,等上游中间件改吃 zbus 6 后自动消失。这三条公告 2026-10-16 报告、2026-10-17 入库,无需本仓任何改动就自行让 main 的定时 CI 变红。
- 诊断包是「写入前脱敏」,不是「写完之后再处理」。 导出路径服务于「把你的日志发给我」这个场景,而用户手写这条消息时不会先去删凭据——harness.log 里就有明文的首航 token 与 dsh-auth- cookie。因此每个文本条目在写进压缩包之前都要过五条脱敏规则(launch token / dsh-auth-* cookie / 路径用户名段 / API key 形态 / 代理口令),且每条规则的命中次数都会写进包内 README.txt 与命令返回值——「到底脱敏了没有」是可核对的事实,不是一句承诺。规则同时有正例(原文不得残留)与反例(版本号、端口、E 码、包名不得被误伤;一份把版本号涂掉的诊断包没有诊断价值)。包不会上传到任何地方,产物留在磁盘上,直到用户自己决定发出。
来源与致谢
本项目是一次从零开始的独立重写,不是 dataelement/dsh-desktop 的 fork。应当说明的来源:
- DataElement 开发了基于 Electron 的原版 dsh-desktop 套壳,本壳在行为上刻意与其对齐;项目名沿用自该仓库,作为其 Rust/Tauri 后继形态。
- DeepSeek Harness(npm 包 @deepseek-ai/dsh)才是本壳承载的产品本体——壳只负责生命周期管理与窗口承载。
- 两者均为第三方项目、各自持有许可;本仓库的 MIT 许可 仅覆盖本仓库内的 Rust/Tauri 壳层代码。
项目作者:wang-yi-bit64。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
wang-yi-bit64/dsh-desktop
使用 Rust + Tauri 2.0 重新构建的 DeepSeek Harness 桌面套壳。
相关插件 · 开发 / 工程
隐私简历生成器reactive-resume/reactive-resume
Reactive Resume 已迁至 GitHub 上的…
★ 45,125已汉化MCP
71.7
AI 架构文档生成器sopaco/deepwiki-rs
🚀 Litho 已进化为 Terrain —— 给你的 AI 智能体一张会呼吸的代码库地图。
★ 3,157已汉化DSH
70.8
长时任务执行闭环AMAP-ML/LongHorizon-Harness
面向 Computer-Use Agent 的 Loop Engineering
★ 1,764已汉化DSH
70.5
AI 编程纪律方法包GanyuanRan/Aegis
让 AI 编程 agent 变得可信:少返工、更安全、说“完成”前先给证据。
★ 1,384已汉化DSH
69.6
多代理编排运行时Enderfga/claw-orchestrator
面向编程智能体的运行时。把 Claude Code、Codex、Antigravity、Grok…
★ 586已汉化MCP
69.4
记忆进化插件csyangwen/dsh-memory-evolve
dsh-memory-evolve 使用场景指南
★ 461已汉化DSH
67.9