【免费下载链接】deepseek-harness-desktop
为 DeepSeek Harness (DSH) 插件生态打造的现代化桌面端解决方案。万物皆「插件」,桌面本身也是「插件」。
导读
本文深入解析 DeepSeek Harness Desktop 为解决上游 dsh-agent-presets 把 PTC preset 从 code 改名为 ptc 后旧会话无法恢复的问题而实现的「旧版 agent preset 别名」机制(对应仓库内架构决策记录 2026-08-30-legacy-agent-preset-alias.zh.md,Status: implemented)。文章将说明该问题产生的根源、桌面启动器「物化兼容 preset」而非「改写会话存储」的设计取舍、agent-presets 行配置接管 preset 名册排序的细节,以及改名前创建的会话如何在升级后无损恢复。读完本文,你将理解会话持久化 preset id 与随包 preset 名册更新之间的冲突模型,以及桌面端如何用最小侵入的方式为运行时重命名提供兼容层。
问题背景:上游重命名 preset 导致旧会话集体失联
上游在 0.1.2-alpha.1 中把 PTC(programmable tool calling)agent preset 的 id 从 code 改名为 ptc,并按照其重命名 Agent Note 记录的 pre-release 策略,不提供任何兼容别名。但会话系统存在一个关键事实:
- 会话会在自己的 header 与
agentPresetprojection 中持久化创建时的 preset id; - 桌面更新只替换随包(shipped)的 preset 名册,不会触碰用户的会话存储。
两个事实叠加,产生了如下后果:在 runtime 0.1.1-rc.2(对应 Desktop ≤ 2.0.3)上以 code 预设创建的会话,升级到 Desktop 2.0.4 后全部无法恢复。具体表现是:用户提交提示词会触发会话恢复流程,恢复流程拿持久化的 id 去查询改名后的名册,随即客户端报出:
resume failed for session "…": agent-presets: preset "code" not found (available: standard, ptc, minimal, cordis)
注意错误信息末尾的 available 列表已经只有 standard, ptc, minimal, cordis——code 从名册中彻底消失。此时会话历史本身完好无损,只是挂载(mount)被拒绝,即历史数据并未损坏,仅仅是「找不到对应的 preset 组合」而无法继续运行。该问题由 GitHub issue #727 报告。
决策:物化兼容 preset,而不是改写会话存储
面对这一兼容性问题,桌面启动器选择的方案是物化兼容 preset(materialize compatible presets),而不是改写会话存储。核心设计如下:
- 新增
agent-preset-compat概念,维护旧 id → 替代它的随包 preset 的映射(当前为code → ptc); - 在每次 profile 准备(prepare)时,把随包 preset 原样复制到
<profileDir>/agent-preset-compat/<legacyId>/目录下; - 复制时只改写展示元数据:preset 名称追加
(兼容)后缀,描述中注明其对应旧 id; - 由于每次准备都会从随包 preset 重新复制,别名会随固定版本的运行时组合保持同步——上游名册更新时,别名指向的内容也随之刷新。
这里「物化(materialize)」与「别名(alias)」的区别值得强调:兼容条目不是指向随包 preset 的软链接或逻辑引用,而是一份实际落盘到 profile 目录中的副本,因此它进入 preset 发现机制的方式与自建 preset、随包 preset 完全一致,不需要上游运行时提供任何别名支持。
物化机制的源码佐证
在 dsh-plugin-desktop/src/profile.ts 中可以看到 preset 相关实现:AGENT_PRESETS_ROW_ID = 'agent-presets' 定义了预设名册对应的配置行 id,USER_PRESET_DIRNAME = '.agent-presets' 是 harness home 中存放用户自建 preset 的目录名,而 shippedPresetRoot() 则解析匹配版本依赖 @deepseek-ai/dsh-agent-presets 的 presets 目录作为随包名册根。
在测试端,dsh-plugin-desktop/tests/profile.spec.ts(assembles the Host shell without replacing the upstream client shell 用例)验证了 profile 准备流程中会生成 agent-preset-compat/code/agent.cordis.yml 路径结构,说明兼容条目以标准的 agent.cordis.yml 形式物化在 profile 目录中。
agent-presets 行配置接管:名册排序 随包 → 用户 → 别名
兼容别名能正常工作,还依赖桌面启动器对 agent-presets 配置行的接管。桌面不再依赖 harness home 默认的用户 root 发现机制(includeUserRoot: false),而是改为显式配置 preset 名册的 roots:
roots:
- path: <shippedPresetRoot> # 随包 preset 名册,trust: system
- path: <home>/.agent-presets # harness home 用户自建 preset,trust: user
includeUserRoot: false
上述配置在 dsh-plugin-desktop/src/profile.ts(prepareDesktopProfile 中 const presets = rows.get(AGENT_PRESETS_ROW_ID) 分支)可以找到完整实现:当 agent-presets 行存在时,启动器组装 roots 数组,依次为 { path: shippedPresetRoot(), trust: 'system' } 与 { path: join(home, USER_PRESET_DIRNAME), trust: 'user' },并以 includeUserRoot: false 显式声明不再自动扫描默认用户 root。测试 dsh-plugin-desktop/tests/profile.spec.ts 中对 agent-presets 行的断言也明确要求:
roots: [
{ path: shippedPresetRoot(), trust: 'system' },
{ path: join(home, '.agent-presets'), trust: 'user' },
],
includeUserRoot: false,
配合上述 roots 顺序,最终的名册排序为 随包 → 用户 → 别名。这一排序是刻意的:
- 兼容别名(物化在 profile 的
agent-preset-compat下,trust: system)只提供随包名册和本地自建 preset 都无法提供的 id(即code); - 对于在旧 id
code下自建过 preset 的用户,其自建组合在用户 root 中优先于别名被挂载,别名绝不会遮蔽用户的组合; - 当且仅当
code既不存在于随包名册、也不存在于用户自建目录时,兼容别名才成为该 id 的唯一来源,从而精确恢复旧会话挂载的组合。
相关配套:设置向导中的默认值迁移
与别名机制配套的还有设置向导(setup wizard)对「默认 preset」的原子迁移。在 dsh-plugin-desktop/src/setup-wizard-settings.ts 中可以找到注释 Replace the released code preset default with its current ptc id,即把已发布版本中默认选中的 code 替换为当前 id ptc;对应测试 dsh-plugin-desktop/tests/setup-wizard-settings.spec.ts 中的 atomically migrates the released code preset default to ptc 用例验证了迁移的原子性(涉及 legacy-preset.yaml 与 legacy-preset.json 两类持久化格式)。
需要说明的是:默认值迁移与会话恢复别名是两个独立问题。前者负责「新会话默认选中哪个 preset」——升级后新会话应直接选择 ptc;后者负责「旧会话按持久化的 code id 恢复」。两者配合,才能做到升级后新老会话都表现正常。
备选方案分析:为什么不改会话存储、不回退默认、不等上游
决策记录同时给出了三个被否决的替代方案及其理由,理解它们有助于把握该机制的边界:
-
启动时改写持久化的会话 header 与 projection —— 拒绝:桌面将不得不在上游
SESSION_FORMAT_VERSIONv0→v1 迁移落地之前,自行对会话日志做「手术」。一次部分失败或被中断的改写,就可能损坏本完好的历史数据。这是典型的「为了兼容而引入更大风险」的坏交易。 -
持久化 id 未知时回退到默认 preset —— 拒绝:会话一旦产生历史,其使用的 preset 组合就被固定了。悄悄挂载一个不同的组合,会改变模型看到的工具 schema 与 prompt 分节,等于在用户不知情的情况下篡改了会话语义,不符合会话可复现性的要求。
-
等待上游提供别名或迁移 —— 拒绝:上游的重命名把全部会话持久化词汇(persistence vocabulary)的兼容工作推迟到被格式版本迁移(format version migration)阻塞的 stacked persistence PR 中,周期不可控;而用户的会话当下就无法恢复,等待的成本直接转嫁给用户。
相对地,「物化兼容 preset」方案既不触碰会话日志(零数据风险),又不改变任何会话的组合语义(恢复即原组合),且完全不依赖上游的迁移节奏(桌面侧独立完成),因此被采纳为最终方案。
后果与行为影响
从决策记录与实现来看,该机制落地后用户可观察到的行为如下:
- 改名前创建的会话可以恢复,并继续运行它一直使用的 preset 组合,界面上也继续显示其持久化的
codeid——用户视角下会话与升级前完全一致; - 新会话继续选择
ptc,不受兼容层影响; - preset 选择器会多出一个条目:以随包 preset 命名、带
(兼容)后缀(对应物化副本中改写的展示元数据); - 回退是自动的:若运行时未来重新提供
code(或不再提供ptc),则不会物化任何内容,agent-preset-compat兼容 root 也完全不会配置——别名层随运行时实际能力自动消失,不留残留; - trust 语义明确:别名的 trust 为
system,因为它由启动器依据随包名册生成,而非用户手写;用户在 harness home 中以旧 id 自建的 preset 仍然优先于别名。
这一机制的生命周期终点也值得留意:仓库中 dsh-plugin-desktop/tests/upstream-session-migration.spec.ts 的注释(Exercise the shipped JSONL worker and native lock after retiring Desktop's preset alias)表明,随着上游会话格式迁移(SESSION_FORMAT_VERSION v0→v1)落地,桌面端最终会退役(retire)这个 preset 别名,届时会话恢复改为依赖上游自身的迁移能力。也就是说,「物化兼容 preset」是一个明确的、有退出计划的临时兼容层,而不是永久性的架构债务。
小结
DeepSeek Harness Desktop 通过「物化兼容 preset + 显式接管 preset 名册排序」的组合,在不动会话存储、不依赖上游迁移的前提下,解决了 preset id 重命名导致的旧会话批量恢复失败问题:每次 profile 准备时把 code 指向的随包 preset 原样物化到 <profileDir>/agent-preset-compat/code/,仅改写展示元数据;agent-presets 行以 includeUserRoot: false 显式配置 roots(随包 → 用户 → 别名),保证别名只兜底随包与自建名册都缺失的 id,绝不遮蔽用户组合。相关实现位于 dsh-plugin-desktop/src/profile.ts 与 dsh-plugin-desktop/src/setup-wizard-settings.ts,行为由 dsh-plugin-desktop/tests/profile.spec.ts 与 dsh-plugin-desktop/tests/setup-wizard-settings.spec.ts 覆盖验证,完整设计论证见 架构决策记录(英文版)。
【免费下载链接】deepseek-harness-desktop
为 DeepSeek Harness (DSH) 插件生态打造的现代化桌面端解决方案。万物皆「插件」,桌面本身也是「插件」。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



