User side: the root README and user guide now say in plain language that every plugin is built against the same rules so plugins can be installed and used together, like a phone app store. Developer content in the plugin-ecosystem section moves under a 'For developers' subsection. Developer side: plugin-development ends with the ecosystem vision — composition-first, clear declarations, compatibility-first conventions, adopted through community use, with the marketplace as the discovery and trust signal. No internal criticism or product comparison is voiced.
6.0 KiB
DSH Desktop 插件开发
先理解两层插件
一个普通 DSH 插件可以提供 Host service、命令、路由、bundle 或 Web Client。它应该尽量只依赖官方 DSH contract,因此可以在命令行、普通 Web profile 和 DSH Desktop 中复用。
Desktop 另外提供两个公开的 Host service:
desktopProfiles:读取当前 profile、发现可选 profile,并请求安全的 profile 切换。desktopPnpm:在当前 profile 中执行 pnpm,或通过官方dsh plugin语义管理插件。
它们属于 Electron main 进程中的 Host Cordis generation。Renderer 不能直接读取它们;有浏览器界面的插件仍应使用普通 DSH Web routes、RPC、client metadata、service 和 slot。
完整类型、生命周期和失败语义见 dsh-plugin-desktop/docs/plugin-services.md。下面只给出选择方式和最小原则。
Desktop 专用插件
如果插件只应该在 Desktop 中运行,可以把服务声明为 required injection:
import type { Context } from '@deepseek-ai/cordis'
import type {} from 'dsh-plugin-desktop/profile-service'
import type { DesktopPnpmHandle } from 'dsh-plugin-desktop/pnpm'
export const name = 'example-desktop-plugin'
export const inject = ['desktopProfiles', 'desktopPnpm']
export function apply(ctx: Context): void {
ctx.logger.info(`profile: ${ctx.desktopProfiles.current.name}`)
let active: DesktopPnpmHandle | undefined
// 将这个函数连接到插件界面的明确用户操作。
async function installExample(): Promise<void> {
active = ctx.desktopPnpm.runPlugin(
['add', 'example-plugin'],
process.cwd(),
)
await active.done
}
ctx.effect(() => {
return async () => {
active?.cancel()
await active?.done.catch(() => {})
}
}, 'example plugin operation')
}
实际项目应该把 package operation 放在明确的用户动作中,校验目标来源,读取 stdout/stderr,设置自己的 timeout,并同时检查 exitCode 和 signal。一个 generation 同时只允许一个 desktopPnpm package operation;插件卸载时必须取消并等待它结束。
兼容 Desktop 和普通 DSH
如果同一个插件也要在普通 dsh web 中运行,不要把 Desktop service 放进顶层 required inject。先注入普通依赖,再在 callback 中动态探测:
export const inject = ['webServer', 'loader']
export function apply(ctx: Context, config: { profile?: string }): void {
const profiles = ctx.get('desktopProfiles')
if (profiles === undefined) {
mountOrdinaryDshManager(ctx, config.profile ?? 'web')
return
}
ctx.inject(['desktopPnpm'], (desktopPnpm) => {
mountManager(ctx, {
profile: profiles.current.name,
profileDir: profiles.current.dir,
runPlugin: (args, cwd, signal) => desktopPnpm.runPlugin(args, cwd, signal),
})
})
}
普通 DSH 的 fallback 仍然是插件自己的权威实现。不要从 process.argv、ctx.baseUrl、settings 或 $DSH_HOME 推断 Desktop profile;在 Desktop 中以 desktopProfiles.current 为准。
run() 和 runPlugin() 的区别
desktopPnpm.run(args) 是低层 pnpm operation,cwd 是当前 profile。它不保证 DSH 的 profile 初始化、调用方相对 file:/link: source 锚定或 dsh.profile.bundles reconcile。
desktopPnpm.runPlugin(args, invokingDir) 执行打包的 dsh plugin --profile <active>,保留上游插件管理语义。安装、卸载、更新和依赖修复应使用它,例如:
desktopPnpm.runPlugin(['add', target], invokingDir, signal)
desktopPnpm.runPlugin(['remove', packageName], invokingDir, signal)
desktopPnpm.runPlugin(['update'], invokingDir, signal)
desktopPnpm.runPlugin(['install', '--no-frozen-lockfile'], invokingDir, signal)
参数始终作为 argv 传递;不要拼接 shell 字符串,也不要依赖 Windows .cmd shim。服务会在完整子进程树退出后 settle,并在 generation dispose 时终止仍在运行的 operation。
不要依赖的接口
desktopRuntime、desktopPnpmBootstrap、Electron BrowserWindow、托盘注册表、private Node helper、ELECTRON_RUN_AS_NODE 和生成的 shim 都是 Desktop 内部实现。即使它们出现在 declaration 或运行时上下文中,也不属于第三方兼容 contract。
测试与发布检查
插件至少应覆盖:
- 在普通 DSH 中没有 Desktop service 时仍能加载,或按产品定义保持 pending。
- Desktop 中读取的 profile name/dir 与用户实际选择一致。
- package operation 的取消、非零退出、spawn failure 和 generation teardown。
- 插件变更后重新启动,bundle 能进入下一次 Loader 组合。
开发者可以先阅读 架构说明,再使用包级 service contract。
生态愿景:保持插件生态可组合
DSH 的插件生态正在快速增长。插件越多,它们能否协同工作就越重要——如果每个插件都假设或覆盖其他插件的内部实现,装几个插件就会开始冲突,生态会逐渐碎片化。
我们倡导像浏览器插件一样的开发方式:大家在同一个平台上、按同一套约定扩展,而不是各自维护一份改过的运行时。DSH Desktop 是这套方式的第一个实践者——桌面壳本身就是一个普通插件,与官方、第三方插件走同一条组合路径,没有任何特权。
为此我们发起一项开发规范倡议,希望它通过社区的采纳成为事实标准:
- 组合优先:通过官方 slot、service 和 patch 组合能力,不要假设或覆盖其他插件的内部实现。
- 声明清晰:明确声明依赖的 service 和 slot,不依赖运行时巧合。
- 兼容优先:升级保持向后兼容,不破坏已有组合。
规范是活文档,随生态实践更新,接受社区讨论和修订。插件市场上线后,符合规范的插件将更容易被发现、安装和信任,让"按规范开发"成为对每个作者都有利的选择。