
DSH Desktop 2.x 插件集成指南desktopProfiles、desktopPnpm 与 desktopWindow 三大公开 Service Contract 深度解析【免费下载链接】deepseek-harness-desktop为 DeepSeek Harness (DSH) 插件生态打造的现代化桌面端解决方案。万物皆「插件」桌面本身也是「插件」。项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness-desktop输出文章DSH Desktop 2.x 插件集成指南desktopProfiles、desktopPnpm 与 desktopWindow 三大公开 Service Contract 深度解析DSH Desktopdeepseek-harness-desktop为 DeepSeek Harness 插件生态提供了三种呈现模式兼容、扩展窗口、增强并在 Host 与 Client 两侧导出一组受支持的 Cordis servicedesktopProfiles、desktopPnpm与desktopWindow。本文以 dsh-plugin-desktop-beta/docs/plugin-services.zh.md 为主体结合dsh-plugin-desktop-beta源码逐层拆解这些 service 的 contract、数据流、注入模式、失败处理与稳定性边界。读完本文你将能判断自己的插件该注入哪个 service、如何在普通 DSH 与 Desktop 之间做环境探测与 fallback以及如何安全地驱动 pnpm 包操作与 profile 切换。集成边界哪些能力是受支持的 contract本文档描述的是面向插件作者的受支持集成 contract覆盖 DSH Desktop 2.x 在三种呈现模式下导出的Host 公开 servicedesktopProfiles、desktopPnpmClient 公开 servicedesktopWindow需要特别强调的是该边界不授予第三方访问原始 Electron API 或 launcher bootstrap 状态的能力。Desktop 的 Launcher 私有 service如desktopPnpmBootstrap、native adapterdesktopRuntime以及构造输入DesktopProfileServiceBootstrap都不属于第三方能力即使其类型出现在生成的 declaration 中也不代表 runtime service 成为受支持的集成点。兼容边界的唯一权威是两个公开 Host service 名称 各自的 contract moduledsh-plugin-desktop-beta/profile-service、dsh-plugin-desktop-beta/pnpm、dsh-plugin-desktop-beta/client。分层与数据流从 Electron main 到沙箱 Renderer原文档给出了完整的数据流图这里保留并逐层解读关键语义Launcher 在 Loader tree 挂载前解析一个 profile。desktopProfiles.current在整个 Cordis generation dispose 前保持不变desktop-pnpmHost row 依据 launcher 私有 fact 与上游subprocessservice 构造desktopPnpm见 pnpm.ts 中export const inject [desktopPnpmBootstrap, subprocess]。切换 profile 或呈现模式会 dispose 当前 generation 并启动新 generation因此 service reference 不能跨越该边界。Renderer 侧没有 preload 与 Electron IPC bridgerenderer 通过现有 loopback carrier 接收普通 Web Client module无法直接读取 Host service。Desktop Client 改在自己的 Cordis fiber 生命周期内通过desktopWindow提供不可变的原生布局信息。包含浏览器 UI 的插件继续使用普通 DSH Host route、RPC、client metadata、service 与 slot。公开 Client servicedesktopWindow声明方式从受支持的 client export 导入 contract并只在浏览器侧代码中 injectdesktopWindowimport type { Context as ClientContext } from deepseek-ai/cordis import type { DesktopWindowService } from dsh-plugin-desktop-beta/client export const inject [desktopWindow] export function apply(ctx: ClientContext): void { const geometry: DesktopWindowService ctx.desktopWindow document.documentElement.style.setProperty( --example-desktop-safe-top, ${geometry.safeAreaInsets.top}px, ) }接口定义interface DesktopWindowService { readonly mode: compatibility | extended | advanced readonly platform: darwin | win32 | linux readonly material: off | transparent | mica readonly micaSupported: boolean readonly availableMaterials: readonly (off | transparent | mica)[] readonly safeAreaInsets: { readonly top: number readonly right: number readonly bottom: number readonly left: number } readonly dragRegion: { readonly height: number readonly leftInset: number readonly rightInset: number } }该 interface 的完整定义可参见 client/contracts.ts并包含version当前 renderer generation 安装的 Desktop 产品版本字段。语义要点所有值在一个 renderer generation 内保持不变几何值使用 CSS 像素整个对象被Object.freeze冻结见 window-service.ts体现不可变几何信息的承诺。material是经过系统能力门槛解析后的实际材质而非持久化的偏好。macOS 的availableMaterials为off/transparentWindows 10 为offWindows 11 build 22621 及以上为off/mica。已移除的旧acrylic偏好会按off读取并在设置文件可写时自动迁移。该逻辑与 window-service.ts 的平台分支一一对应。兼容模式与扩展窗口在 macOS 与 Windows 上都报告顶部36 像素的预留区与拖动带并在 macOS 左侧为红绿灯排除80 像素、在 Windows 右侧为原生标题栏按钮排除138 像素。兼容模式会把完整官方 frame 下移到该区域下方扩展窗口则由 Desktop 持有 root layout/sidebar surface并在同一预留区下方承载官方 sidebar、conversation 与 details occupant因此普通 occupant 不能再次叠加这一 inset。Linux 兼容模式保留普通原生 frame因此报告零 inset 和零高度拖动区域。增强模式使用独立的紧凑几何macOS 报告 20 像素内容 inset、32 像素拖动带与 80 像素左侧排除Windows 报告 32 像素内容 inset、32 像素拖动带与 138 像素右侧排除。源码常量可见 window-chrome.tsADVANCED_MACOS_DRAG_REGION_HEIGHT 32、MACOS_TRAFFIC_LIGHT_SAFE_WIDTH 80、ADVANCED_WINDOWS_TITLEBAR_HEIGHT 32、WINDOWS_CAPTION_CONTROLS_WIDTH 138。safeAreaInsets与dragRegion是两个独立概念前者描述 Desktop 从哪里开始放置完整的上游内容 surface后者单独描述原生标题栏命中区域consumer 不能假设两者高度相同。交互与布局约束拖动带内的交互元素必须设置-webkit-app-region: no-dragDesktop 已经为标准按钮、链接、输入框、可编辑字段、菜单、标签页、开关与对话框设置该排除规则。该 service只报告几何信息不提供窗口 mutation、焦点、Electron 或 IPC capability普通浏览器启动中不存在该 service。兼容模式与扩展窗口都会让操作栏保持 Desktop 私有不会声明标题栏 action slot第一方图标组由 Desktop frame 直接渲染macOS 右侧、Windows 左侧。Web Client 插件必须使用各自已有文档的内容 slot不能把控件放到原生操作旁。Renderer 重载与开发者工具切换仍是第一方私有 launcher 操作。Desktop 会用data-dsh-desktop-frametitlebar标记操作栏、用data-dsh-desktop-content-viewport标记上游 root。Root 成为操作栏下方独立的 fixed viewport因此 fixed descendant 不能逃逸到 Desktop chrome直接 portal 到document.body的全视口对话框会获得相同的内容偏移。Body 级插件 portal 可以读取dsh-desktop-titlebar-insetURL contract带 frame 的模式发布精确的 36px 预留插件不能重复补偿已经消费的边界。公开 Host servicedesktopProfiles声明方式从受支持的 contract 路径执行type-only importimport type { DesktopCurrentProfile, DesktopProfiles, } from dsh-plugin-desktop-beta/profile-service import type { DesktopPnpm, DesktopPnpmHandle, DesktopPnpmOutcome, } from dsh-plugin-desktop-beta/pnpm注意dsh-plugin-desktop-beta/profiles是 Desktop 自有托盘 consumer不是 profile service contract不要从该路径导入 service 类型。接口定义interface DesktopProfiles { readonly current: { readonly name: string readonly dir: string } list(): readonly DesktopProfileSummary[] select(name: string): Promisevoid }源码 profile-service.ts 中的完整接口还包含create(name)、prepareSelection(name)返回带restartRequired与restart()的DesktopProfileSelection把重启时机交给调用方、canDelete(name)与delete(name)。DesktopCurrentProfile.name是传给 profile launcher 的名称dir是激活 profile 的绝对目录profile-service.ts。语义要点current在一个 generation 内不可变构造时Object.freeze({ ...bootstrap.current })。不要从 argv、ctx.baseUrl、settings、Loader row 或$DSH_HOME推断 name 与 dir。list()会重新读取 profile manifest但不会改变 patch、dependency 或 bundle 顺序返回项可以描述可见但不可选择的 profile。select(name)是重启 operation不是就地 mutation先持久化被接受的目标再请求有序 Cordis teardown 与 Electron relaunch。源码层面表现为先persistSelection成功后记录committedName再requestRestartprofile-service.ts。并发与失败语义同一目标的并发调用共享一个 operation目标被提交为 pending 后其它目标会在重启前被拒绝committedSelectionError。持久化失败会释放选择 slot重启失败则保留已提交目标使同一个 restart 可以重试而不会覆盖状态见类注释 profile-service.ts。Service dispose 后通过保留 reference 发起的调用会失败assertActive抛desktopProfiles service disposed。应从下一 generation 重新读取current不能全局缓存旧 service。公开 Host servicedesktopPnpm接口定义interface DesktopPnpm { run(argv: readonly string[], signal?: AbortSignal): DesktopPnpmHandle runPlugin(argv: readonly string[], invokingDir: string, signal?: AbortSignal): DesktopPnpmHandle runExternalMarketPluginInstall( argv: readonly string[], invokingDir: string, signal?: AbortSignal, ): DesktopPnpmHandle } interface DesktopPnpmHandle { readonly stdout: NodeJS.ReadableStream readonly stderr: NodeJS.ReadableStream readonly done: Promise{ readonly exitCode: number | null readonly signal: NodeJS.Signals | null } cancel(): void }实际 stream 类型是 Node 的Readable。所有方法都会校验 argv 非空且不含 NULvalidatedArgvpnpm.ts。run()始终以激活 Profile 目录作为cwd插件适配器则要求调用方提供绝对 working directory。三个方法的分工方法进程与 working directory受支持用途run(argv, signal?)直接执行已打包 pnpm JavaScript entry以激活 Profile 目录为cwd。任意由调用方负责的 pnpm operation。runPlugin(argv, invokingDir, signal?)从绝对调用方目录执行已打包的dsh plugin --profile active并传入插件 argv。依赖 DSH bundle reconcile 的插件管理器兼容适配。runExternalMarketPluginInstall(argv, invokingDir, signal?)使用同一套已打包 DSH plugin CLI但只接受add、flag 选项与一个精确版本 npm target。内置dshmarketruntime 的窄兼容适配。源码层面run直接以appExecutable --import clearEnvironmentPath pnpmBinPath ...启动cwd为activeProfileDir两个 plugin 适配器则走appExecutable --expose-internals dshBootstrapPath plugin --profile activeProfileName ...cwd为调用方目录pnpm.ts。推荐用法优先构造明确 argv新集成应优先直接传入 pnpm argv例如[add, --save-exact, example-plugin1.0.0] [remove, example-plugin] [install, --no-frozen-lockfile]使用run()时package 身份策略、命令构造、dsh.profile.bundlesreconcile、receipt 和操作后验证均由调用方负责兼容适配器会把 bundle reconcile 委托给已打包的 DSH CLI。三个方法都不会为 package operation 做快照、回滚、重试、保护或记录。Desktop 恢复与此独立每次健康启动写入三个轮转配置 checkpoint 之一同时覆盖激活 Profile 与共享 DSH home 设置和补丁用户可在恢复页面明确选择精确槽位恢复。并发、取消与错误语义Service 在每个 generation 同时最多启动一个 package operation已有 operation 活跃时再次调用会同步抛错another desktop pnpm operation is already runningpnpm.ts。只暴露输出不选择 progress UI也没有内置 timeout。Consumer 拥有 deadline读取两个 stream、报告 progress、在需要时调用cancel()或 abort signal、等待done并同时检查exitCode与signal。无效 argv、已经关闭或忙碌的 generation以及调用前就已 abort 的 signal都会在返回 handle 前同步抛错command.signal?.throwIfAborted()。Handle 存在后cancellation 与 generation teardown 会作用于完整 subprocess tree。done不会仅因直接 wrapper 退出而 settle在后代进程消失前operation gate 始终保持占用settle中waitForExit后才释放active。异步 spawn-level failure 会 rejectdone普通命令失败则 resolve 为非零 exit code。Desktop 会仅在当前进程中、最终执行 pnpm 时为所有操作加入一次--config.minimumReleaseAge0包括打包的dsh plugin转发和终端 shim且不会持久化修改用户配置见 pnpm-policy.tswithDesktopPnpmPolicy在 argv 已含该参数时不重复添加。源码注释也说明dsh plugin最终解析到 Desktop pnpm shimshim 拥有这一个策略参数因此转发前会通过withoutForwardedDesktopPnpmPolicy去掉被提前转发的副本。Windows 上provider 会使用 argv 启动准确的已打包 pnpm entry并把进程树 ownership 委托给 subprocess service因此插件作者无需发现.cmdshim也不应拼接 shell 文本。内部与 launcher 私有 capability禁止越界名称边界面向插件作者的状态desktopProfiles作用于 generation 的 Host service。公开通过dsh-plugin-desktop-beta/profile-service获得受支持 contract。desktopPnpm作用于 generation 的 Host service。公开通过dsh-plugin-desktop-beta/pnpm获得受支持 contract。desktopWindow作用于 generation 的 Client service。公开通过dsh-plugin-desktop-beta/client获得受支持 contract只包含不可变几何信息。desktopRuntimeLauncher 提供的 native adapter供 Desktop 自有 shell、tray、terminal、profile 与 update row 使用。Desktop 内部。第三方插件不得 inject也不得依赖其 window/tray 方法。desktopPnpmBootstrap提供给desktop-pnpmprovider 的已打包绝对路径、被选 profile fact、Electron ABI 值与私有 Node helper。Launcher 私有。不得读取、provide、intercept 或声明为 dependency。DesktopProfileServiceBootstrapLauncher 注册desktopProfiles时使用的 constructor input它不是 Cordis service。Launcher 私有实现细节。私有类型出现在生成的 declaration 中并不代表其 runtime service 成为了受支持第三方 capability。两个公开 service 名称及其 contract module 才是兼容边界。Injection 模式两种官方姿势仅支持 Desktop 的插件required injection只在 DSH Desktop 中有意义的插件可以把两个 service 都声明为 required dependency。Cordis 会让插件保持 pending直到两个 provider 都可用任一 required service 消失时插件 effect 会被 unload。import type { Context } from deepseek-ai/cordis import type {} from dsh-plugin-desktop-beta/profile-service import type { DesktopPnpmHandle } from dsh-plugin-desktop-beta/pnpm export const name example-desktop-plugin-manager export const inject [desktopProfiles, desktopPnpm] declare function registerInstallAction( callback: (target: string) Promisevoid, ): () void export function apply(ctx: Context): void { ctx.logger.info(active Desktop profile: ${ctx.desktopProfiles.current.name}) ctx.effect(() { let active: DesktopPnpmHandle | undefined const disposeAction registerInstallAction(async (target) { // 先校验 target该 callback 表示显式用户操作。 const signal AbortSignal.timeout(5 * 60_000) const operation ctx.desktopPnpm.run([add, --save-exact, ${target}1.0.0], signal) active operation operation.stdout.setEncoding(utf8) operation.stderr.setEncoding(utf8) operation.stdout.on(data, chunk ctx.logger.info(String(chunk).trimEnd())) operation.stderr.on(data, chunk ctx.logger.warn(String(chunk).trimEnd())) try { const outcome await operation.done if (outcome.exitCode ! 0) { throw new Error(plugin install failed: exit${String(outcome.exitCode)} signal${String(outcome.signal)}) } } finally { if (active operation) active undefined } }) return async () { disposeAction() const operation active operation?.cancel() await operation?.done.catch(() {}) } }, example: package-manager user action) }生产代码必须在调用 package manager 前根据插件自身 trust policy 校验target。进程 exit code 为零也不能替代领域相关的 post-install validation。跨环境插件可选 Desktop adapter 与普通 DSH fallback当同一个 package 必须在普通 DSH 中激活时不要把 Desktop service 放入顶层 requiredinject列表。Launcher 会在 Loader entry 挂载前注册desktopProfiles因此它是否存在可以区分 Desktop 环境。若存在创建嵌套ctx.inject()callback 等待desktopPnpm若不存在挂载已有普通 DSH 实现。import type { Context } from deepseek-ai/cordis import type {} from dsh-plugin-desktop-beta/profile-service import type {} from dsh-plugin-desktop-beta/pnpm export const name cross-environment-plugin-manager export const inject [webServer, loader] interface ManagerAdapter { readonly profile: string readonly profileDir?: string runPnpm(argv: readonly string[], signal?: AbortSignal): unknown } declare function mountManager(ctx: Context, adapter: ManagerAdapter): () void declare function ordinaryDshAdapter(profile: string): ManagerAdapter export function apply(ctx: Context, config: { profile?: string }): void { const profiles ctx.get(desktopProfiles) if (profiles undefined) { // 现有非 Desktop 行为在此保持权威。 const profile config.profile ?? web ctx.effect( () mountManager(ctx, ordinaryDshAdapter(profile)), example: ordinary DSH plugin manager, ) return } // 对该嵌套 callback 而言ctx.inject() 仍把 desktopPnpm 视为 required。 // Desktop-only dependency 没有进入顶层 inject所以 parent 插件仍能在普通 DSH 中加载。 ctx.inject([desktopPnpm], (desktopCtx) { desktopCtx.effect(() mountManager(desktopCtx, { profile: profiles.current.name, profileDir: profiles.current.dir, runPnpm: (argv, signal) desktopCtx.desktopPnpm.run(argv, signal), }), example: Desktop plugin manager) }) }两个关键澄清ctx.inject()不是 optional-dependency declaration传给 callback 的每个名称在该 callback 内都是 required。这里使用它只是为了让嵌套 Desktop adapter 等待desktopPnpm而外层插件仍拥有普通 fallback。对于纯新增 Desktop feature也可以用同样的嵌套模式在 service 存在时贡献 effect其它环境不做任何操作。desktopProfiles已存在后绝不能回退到猜测的webprofile。部分缺失或启动失败的 Desktop provider set 属于 Desktop generation failure不是通过 ambient CLI 修改另一个 profile 的许可。也不要用ctx.baseUrl、settings、Loader inventory 或 launcher 的内部cmdlineArgs替代desktopProfiles.current。Type-only import 会从 JavaScript 中消除。跨环境 package 可以把dsh-plugin-desktop-beta作为编译所需 dev dependency若发布的 declaration 会暴露这些类型也可以将其声明为 optional peer。仅为了探测 service不需要 runtime import。最小可运行测试插件与验证命令仓库在 dsh-plugin-desktop-beta/tests/fixtures/desktop-host-services-smoke-plugin 提供了一个只有两个文件index.js与package.json的 profile-local fixture。其 entry 声明inject [desktopProfiles, desktopPnpm]读取desktopProfiles.current并确认run()可用它只把结果发布为测试 probedesktopHostServiceProbe绝不会执行 pnpm 或修改 profile见 index.js。完整 Profile Loader smoke 会把该 package 复制到临时 profile 的node_modules以普通 bare-package Loader entry 加载并在 probe 没有返回激活 profile 或run()时失败。运行命令yarn workspace dsh-plugin-desktop-beta build yarn workspace dsh-plugin-desktop-beta verify:profile该 fixture 位于tests/不在 npmfiles列表或 Electron build files 中因此不会进入生产 archive。Failure 与 teardown checklist原文档给出十条可直接落地的检查清单这里完整保留只有显式用户或管理员操作才能启动 package mutation。把desktopProfiles.current当作单 generation snapshot不能跨重启保留 service。优先构造明确的 pnpm argv 并使用run(argv, signal?)只有兼容性确实依赖 DSH bundle reconcile 时才使用插件适配器。pnpm 完成后由调用方 reconcile Profile bundle 并验证领域状态。为面向用户的 deadline 提供AbortSignal并保留 handle 以便显式 cancellation。持续读取 stdout 与 stderr状态 endpoint 保存的内存历史必须有界。等待done并分别处理 rejection、非零exitCode与 terminatingsignal。向用户报告 generation-wide busy error不能并发启动 profile mutation。在所属 Cordis effect disposer 中 cancel 活跃工作协调 teardown 时还要等待其结束。把desktopProfiles.select()视为重启边界不能继续假设目标已在旧 generation 中生效。内置 dshmarket 适配内置dshmarketruntime 使用runPlugin()执行普通插件命令并使用runExternalMarketPluginInstall()执行精确 npm add。后者会在跨越 service 前解析版本并拒绝非精确或多 target 请求——源码中validatedExternalMarketArgv要求第一个参数为add且恰好包含一个匹配NPM_PACKAGE_NAME_PATTERN与NPM_EXACT_VERSION_PATTERN的nameversiontargetpnpm.ts。两种操作都会使用当前 Desktop Profile 与已打包 DSH CLI都不会创建安装 transaction、快照、receipt、自动回滚或恢复提示。稳定性边界受支持的插件作者 surface是本文描述且由dsh-plugin-desktop-beta/profile-service、dsh-plugin-desktop-beta/pnpm与dsh-plugin-desktop-beta/client导出的desktopProfiles、desktopPnpm和desktopWindowservice contract。Launcher bootstrap 值、native adapter、生成 shim、状态文件格式、Loader row 顺序与 Electron 实现细节都可能变化但不会因此成为第三方 API。Fallback 必须保持显式、限定在生命周期内并且 headless-safe。相关文件索引官方 contract 文档dsh-plugin-desktop-beta/docs/plugin-services.zh.md英文版见 plugin-services.mddesktopProfiles实现src/profile-service.tsdesktopPnpm实现src/pnpm.ts、src/pnpm-policy.tsdesktopWindowcontract 与实现src/client/contracts.ts、src/client/window-service.ts几何常量src/window-chrome.ts冒烟测试 fixturetests/fixtures/desktop-host-services-smoke-plugin/index.js【免费下载链接】deepseek-harness-desktop为 DeepSeek Harness (DSH) 插件生态打造的现代化桌面端解决方案。万物皆「插件」桌面本身也是「插件」。项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness-desktop创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考