DeepSeek Harness Desktop Profile 切换所有权统一:两阶段转换与并发保护设计解析

DeepSeek Harness Desktop Profile 切换所有权统一:两阶段转换与并发保护设计解析 DeepSeek Harness Desktop Profile 切换所有权统一两阶段转换与并发保护设计解析【免费下载链接】deepseek-harness-desktop为 DeepSeek Harness (DSH) 插件生态打造的现代化桌面端解决方案。万物皆「插件」桌面本身也是「插件」。项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness-desktop本文解析 deepseek-harness-desktop 仓库中一项已落地的架构决策让DesktopProfileService统一拥有单个 Host 生成期generation内的全部 Profile 切换transition行为并通过持久化 重启句柄两阶段转换消除设置页、托盘、原生创建器三路入口之间的并发竞态。读完本文你将掌握prepareSelection/select的职责划分、afterResponse延迟重启机制、删除保护的双层防线以及如何从源码与回归测试中验证这些不变量。背景Profile 切换的三路入口与并发缺口DeepSeek Harness Desktop 的 Profile配置档案切换并非只有一个入口。在同一个运行中的 Host 生成期内存在三条会触达profiles.json选择状态的调用链系统托盘Tray通过DesktopProfileService完成切换原生 Profile 创建器Native Profile creator同样经由DesktopProfileService设置页面Settings page经由DesktopSettingsController独立路径。其中前两条路径共享同一套并发规则DesktopProfileService保证一个 Host 生成期只接受第一个成功持久化的非当前 Profile因此后续请求无法覆盖已提交的目标。而设置页在当时的实现中绕开了这一规则——DesktopSettingsController自己执行发现discovery检查并直接把选择状态写入磁盘再把重启延迟到 HTTP 响应之后。这种做法虽然保住了先回包、再重启的响应顺序却把并发裁决交给了两个互不感知的模块埋下了两类问题写入竞态两个并发的设置请求可以同时被报告为已接受后一个写入会悄悄替换掉前一个目标而前一个调用方毫不知情删除漏洞删除校验只检查启动当前生成期的 Profile即currentProfileName虽然selectionStatePath被传入了删除逻辑却从未被读取。于是存在这样一个窗口选择已持久化、但重启尚未开始此时新目标在磁盘上仍被判定为非活跃可以被删除请求清除。最短失败路径时序原文档给出了这条最短失败路径的完整时序注意这并非原子写profiles.json的失败——每一次写入本身都是完整、成功的。问题出在选择与删除没有服从同一个所有者一个模块认为work 是已选中的目标另一个模块却认为work 只是普通非活跃档案。决策由 DesktopProfileService 统一拥有转换所有权本次架构决策的核心原则是DesktopProfileService拥有一个 Host 生成期内每一次 Profile 转换transition。设置页不再持有原始的persistProfileSelection写能力也不再自行判断某个 Profile 是否可选selectable。由此一次转换被拆分为两个显式阶段阶段一验证与持久化Profile 模块校验目标、执行持久化然后为本次转换返回一个重启句柄restart handle阶段二按需重启调用方在合适时机运行重启句柄——托盘与原生创建器立即执行设置页则在 HTTP 响应结束之后执行。重启句柄对外只暴露两件事本次选择是否需要重启、以及如何请求这次重启。至于目标所有权、重复请求合并duplicate coalescing、对不同目标的拒绝、持久化失败后的释放release、以及重启失败后的同目标重试全部保留在DesktopProfileService内部调用方无法绕过。源码实现prepareSelection 与重启句柄DesktopProfileService定义在 dsh-plugin-desktop/src/profile-service.ts作为 Cordis 服务以ctx.desktopProfiles形式暴露。其能力接口DesktopProfiles完整描述了新所有权模型current本生成期不可变的当前 Profile 身份DesktopCurrentProfile含name与dircreate(name)创建档案但不选择、不重启list()重新读取档案清单但不修改prepareSelection(name)验证并持久化目标把重启时机留给调用方返回DesktopProfileSelectionselect(name)持久化兼容档案并立即请求有序重启面向托盘/创建器这类即点即走的入口canDelete(name)/delete(name)带保护的非活跃档案删除。重启句柄的形态定义如下profile-service.ts#L14-L20/** A persisted Profile target whose restart timing belongs to the caller. */ export interface DesktopProfileSelection { /** Whether another generation is required to make the selection effective. */ readonly restartRequired: boolean /** Request the one orderly restart associated with this selection. */ restart(): Promisevoid }prepareSelection 的并发裁决逻辑prepareSelectionprofile-service.ts#L124-L165是本设计的核心。逐分支看目标是当前 Profile直接返回restartRequired: false的空句柄不做任何写入已有进行中的转换operation且目标相同返回同一个 promise实现重复请求合并——并发的同目标请求共享同一次持久化与同一次重启已有进行中的转换且目标不同等待前一个转换结束无论成败后再次尝试形成串行化队列已有提交目标committed若请求的就是已提交目标则复用其句柄若是不同目标则直接拒绝——committedSelectionError会抛出形如profile X is already selected for restart; cannot select Y before restart的错误无冲突通过runExclusive独占执行bootstrap.persistSelection(name)成功后把committedName固定下来并冻结返回句柄。持久化失败时committedName不会被写入即释放插槽后续目标可以再试而重启失败时committedName依然保留于是针对同一目标的重试无法覆盖已持久化的选择——这正是文档中重启失败保留提交目标的实现基础。restartSelectionprofile-service.ts#L221-L240还会合并多次重启请求同一提交目标只触发一次底层requestRestart()其余调用共享同一 promise。select立即重启的便捷路径托盘与原生创建器走select(name)profile-service.ts#L167-L185若目标已是提交目标直接调用其restart()否则先prepareSelection再立即restart()。immediateSelectionsMap 同样对同目标并发调用做去重。它本质上是两阶段在即时入口上的语法糖底层语义与设置页路径完全一致。设置页集成先回包、再重启的 afterResponse 模式设置页不再触碰任何底层写状态能力而是通过DesktopSettingsController的selectProfile与DesktopProfileService交互。控制器定义在 dsh-plugin-desktop/src/desktop-settings-controller.ts其 bootstrap 只获得PickDesktopProfiles, current | list | create | prepareSelection的子集——没有select更没有裸的persistProfileSelection从类型上就杜绝了绕过。selectProfiledesktop-settings-controller.ts#L137-L146的返回结构是DesktopSettingsPostResponseTexport interface DesktopSettingsPostResponseT extends object { readonly response: T readonly afterResponse?: () void | Promisevoid }选择请求返回{ accepted: true, restartRequired }若需要重启afterResponse就是() selection.restart()。这个响应后动作由路由层finishPostResponsedesktop-settings-route.ts#L175-L190保证执行时机先res.end()完成响应体再用setImmediate把afterResponse挂到事件循环下一轮执行若异步重启抛错会走reportError进入既有错误上报路径——这正是回归测试中响应后的异步重启失败进入错误上报所覆盖的行为。路由handleDesktopProfileSelectRequestdesktop-settings-route.ts#L236-L265据此把状态码区分开需要重启时返回202 Accepted否则200 OK。这样设置页在收到 202 后立刻得知选择已被接受、重启将在响应结束后发起同时profiles.json的写入早在响应返回之前就已完成二者顺序严格可预期。另外值得注意的是DesktopSettingsController.read()中deletable标志由profiles.canDelete?.(name)投影而来desktop-settings-controller.ts#L104-L108因此设置页展示的可删除本身就是服务层裁决后的结果而非页面自行推断。删除保护内存侧与文件系统侧的双层防线删除保护在本次决策中被加固为两道防线。第一道服务内存侧isSelectionTargetDesktopProfileService.canDelete/deleteprofile-service.ts#L187-L204先调用isSelectionTarget(name)profile-service.ts#L242-L245private isSelectionTarget(name: string): boolean { return this.operation?.name name || this.committedName name }即正在进行持久化的目标、以及已提交待重启的目标在内存中就被拦截。这填补了持久化已完成、磁盘检查尚未能看到的竞态窗口——delete会在bootstrap.delete之前直接抛错而非把判断留给磁盘。第二道文件系统侧selectionStatePath复核删除请求穿过服务层后还会经过 dsh-plugin-desktop/src/profile-manager.ts 的deletionTargetprofile-manager.ts#L293-L327做最终校验它现在真正读取了selectionStatePathif (name readDesktopProfileState(options.selectionStatePath).active) { throw new Error(${BIN_NAME}: selected profile ${JSON.stringify(name)} cannot be deleted) }加上原有的currentProfileName检查与目录/清单安全校验必须是真实目录、manifest 必须是普通文件且非符号链接共同构成删除的前置条件。canDeleteDesktopProfileprofile-manager.ts#L329-L337是它的同步判定版本供设置页投影使用。deleteDesktopProfileprofile-manager.ts#L339-L372在真正动手前会连续执行两次deletionTarget验证防止验证期间目标被替换然后通过同文件系统暂存改名删除先把目录改名为.${name}.deleting-${pid}-${randomUUID()}清理禁用状态与检查点后再递归移除若清理失败则把暂存目录改名回原位保证用户档案可恢复。这两道防线分别解决文档中点名的两个窗口内存侧堵住持久化在途文件系统侧堵住已持久化但尚未重启。此外bootstrap 装配处dsh-plugin-desktop/src/host-bootstrap.ts把persistSelection、requestRestart、canDelete、delete统一接到 launcher 的profile-manager与runtime.requestRestart()上确保所有入口共享同一份底层实现。不变量本次设计在 2026-09-04-desktop-profile-transition-ownership.md 中明确列出六条不变量逐一对应上述实现一个 Host 生成期只接受第一个成功持久化的非当前 Profile——由prepareSelection的 operation/committed 串行化保证同目标重复请求共享持久化与重启不同目标不能替换已持久化目标——running.promise复用与committedSelectionError拒绝持久化失败不请求重启且允许后续目标重试——committedName仅在成功写入后赋值成功的设置响应先于重启请求完成——finishPostResponse先res.end再setImmediate(afterResponse)当前 Profile、在途持久化目标、profiles.json.active命名的档案均不可删除——isSelectionTargetdeletionTarget双重校验其他非活跃 Profile 仍可删除——两条防线只拦截受保护目标不扩大禁止范围。未改变的行为本次决策刻意保持了以下行为的稳定性选择状态仍采用version 2 的单一active字段与既有的原子写入见profile-manager.ts的parseState拒绝未知版本号的STATE_VERSION校验启动失败仍由当前 checkpoint 与 Recovery 流程拥有Profile 发现、档案创建、偏好清理、分阶段目录删除均保持不变Market市场选择保留自己的持久化与重启路径——DesktopSettingsController.selectMarket仍走bootstrap.scheduleRestart()与 Profile 转换互不干扰。回归验证原文档列出的回归测试覆盖Stable 与 Beta 双变体验证见 dsh-plugin-desktop/tests/profile-manager.spec.ts 与 dsh-plugin-desktop/tests/package.spec.ts 等两个不同并发目标时只有第一个成功持久化的目标被接受设置页持久化后、响应完成前重启尚未被请求已持久化、等待重启的目标不可被删除持久化在途时同一目标的删除请求无法穿过 Profile 模块其他非活跃 Profile 仍然可删除设置页的 Profile 选择入口不再持有裸的写状态能力类型层面由PickDesktopProfiles, ...限定响应之后的异步重启失败进入既有错误上报路径finishPostResponse的catch分支。影响与后续演进所有权统一带来的直接收益是Profile 的各入口仍可自行决定何时发起重启托盘/创建器即时设置页响应后但目标选择、并发规则、删除保护只有一个所有者。今后若出现新的 Profile 入口点它将拿到 Profile 模块的转换接口prepareSelection 句柄而不再拥有直接写profiles.json的能力——从架构上杜绝了下一代入口重蹈设置页的覆辙。对二次开发者而言接入一个受控的 Profile 切换入口的标准姿势就是调用prepareSelection拿到句柄在自己的请求生命周期内决定restart()的时机并把删除一律交给canDelete/delete。【免费下载链接】deepseek-harness-desktop为 DeepSeek Harness (DSH) 插件生态打造的现代化桌面端解决方案。万物皆「插件」桌面本身也是「插件」。项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness-desktop创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考