Files
workdsh/docs/plugin-development.md
T
t4we fan 29d8cdcc44 docs: add plugin ecosystem manifesto and link it from the intro
Create docs/plugin-ecosystem(.en).md as the ecosystem manifesto: an open,
composable, sustainable vision with composition-first, declare-clearly,
and compatibility-first principles, the desktop shell as the first
example, living-document community governance, and the marketplace as
the trust signal.

The root README intro now ends with the co-progress vision and links the
manifesto; the docs index and root README tables list it; the
plugin-development ecosystem-vision section points to it as the full
statement.
2026-08-16 19:35:40 +08:00

121 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](../dsh-plugin-desktop/docs/plugin-services.md)。下面只给出选择方式和最小原则。
## Desktop 专用插件
如果插件只应该在 Desktop 中运行,可以把服务声明为 required injection:
```ts
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 中动态探测:
```ts
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>`,保留上游插件管理语义。安装、卸载、更新和依赖修复应使用它,例如:
```ts
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 组合。
开发者可以先阅读 [架构说明](architecture.md),再使用包级 [service contract](../dsh-plugin-desktop/docs/plugin-services.md)。
## 生态愿景:保持插件生态可组合
DSH 的插件生态正在快速增长。插件越多,它们能否协同工作就越重要——如果每个插件都假设或覆盖其他插件的内部实现,装几个插件就会开始冲突,生态会逐渐碎片化。
我们倡导像浏览器插件一样的开发方式:大家在同一个平台上、按同一套约定扩展,而不是各自维护一份改过的运行时。DSH Desktop 是这套方式的第一个实践者——桌面壳本身就是一个普通插件,与官方、第三方插件走同一条组合路径,没有任何特权。
为此我们发起一项开发规范倡议,希望它通过社区的采纳成为事实标准:
- **组合优先**:通过官方 slot、service 和 patch 组合能力,不要假设或覆盖其他插件的内部实现。
- **声明清晰**:明确声明依赖的 service 和 slot,不依赖运行时巧合。
- **兼容优先**:升级保持向后兼容,不破坏已有组合。
规范是活文档,随生态实践更新,接受社区讨论和修订。插件市场上线后,符合规范的插件将更容易被发现、安装和信任,让"按规范开发"成为对每个作者都有利的选择。完整的愿景、原则和参与方式见 [DSH 插件生态倡议书](plugin-ecosystem.md)。