DSHX 是一套面向 DeepSeek Harness 的插件开发工具链。本文复盘它如何从 Vite 插件演进为覆盖 Host/Client、Typed API、真实 Profile 调试、兼容性诊断和 Framework Hub 的完整工作流。
最近一直在折腾 DeepSeek Harness。
最开始只是想给 DSH 写几个插件。真正动手以后,我发现插件逻辑本身并没有想象中难,麻烦的是周围那一圈工程问题:
- Host 和 Client 分别运行在哪里;
- Cordis 的依赖注入与
inject应该怎样声明; - UI 应该挂到哪个 Slot;
package.json需要哪些 DSH 元数据;- Host 与 Client 怎样分别构建和调试;
- 插件怎样面对仍在快速变化的 DSH 版本。
只写一个插件时,这些问题查一遍源码也能解决。但如果以后想持续写插件,甚至让更多开发者参与进来,每个人都从源码里重新拼一遍开发流程就有些浪费了。
所以我做了 DSHX:
- 项目地址:github.com/liyown/dshx
- Framework Hub:dshx.io
截至 2026 年 8 月,DSHX 仍处于 0.1.x Preview。当前 Authoring API 是 API Candidate,不是 1.0 稳定承诺。
写插件不难,难的是把插件真正跑起来
一开始,我只是想给 DSH 做一个 Vite 插件。
想法很直接:
TypeScript / React
↓
Vite
↓
Host + Client
↓
DSH Plugin
如果把 Host、Client 的构建入口封装好,再补一点类型,开发体验应该就会顺很多。
但继续往下做,很快就遇到了更实际的问题。比如一个 Client 组件需要调用 Host API,开发者同时需要知道 Host 暴露了什么、Client 如何获得 Connection、插件要声明哪个 Provider、输入输出是什么类型,以及当前 DSH Runtime 是否真的支持这条链路。
这些知识分散在构建、运行时、Manifest 和具体服务之间。Vite 只能解决其中一部分。
DSHX 也就慢慢从一个 Vite 插件,变成了包含 Authoring API、构建、检查、脚手架、真实 Profile 调试和兼容性诊断的工具链。
我现在对它的理解是:DSHX 负责把“DSH 能做什么”整理成“插件开发者应该怎么写”,但不重新实现 DeepSeek Harness。
先划清边界:DSHX 不重新实现 DSH Runtime
做 Framework 很容易越做越大。
API 不方便,包一层;通信不方便,做一套 RPC;状态不好处理,再加一个 Store。最后 Framework 自己拥有 DI、Runtime、RPC、Cache 和 Event Bus,原来的 DSH 反而只剩下一个底层驱动。
这条路我后来刻意避开了。
DSHX 的原则是:开发体验可以重新设计,运行时语义尽量交给 DSH。下面这张表不是逐项的一一映射,而是两边的职责边界:
| DSHX 负责 | DSH / Cordis 继续负责 |
|---|---|
| 类型与声明 | Fiber 与 Scope |
| 代码生成与静态检查 | Registry 与依赖注入 |
| Host / Client 构建 | Connection 与运行时通信 |
| 脚手架与开发流程 | Persistence 与 Prompt Assembly |
| 兼容性诊断 | HMR、卸载与 disposer 生命周期 |
DSHX 可以提供 defineHost、defineApi、defineSlot 这类 Authoring API,但它们最终仍会映射回 DSH 官方能力。构建产物也不需要携带一个私有的 DSHX Runtime 才能运行。
这听起来有些保守,但 DSH 还在快速迭代。如果 DSHX 在上面再创造一套 Runtime,短期可能很顺手,半年后同时维护两套语义大概率会把项目拖垮。
Typed API:先让 TypeScript 报错
Host 与 Client 通信是一个很典型的例子。
先定义共享 Contract:
import { defineApi, method } from "@becomeopc/dshx/api";
export const statusApi = defineApi({
id: "status",
version: 1,
methods: {
get: method<void, { readonly ready: boolean }>(),
},
});
Host 实现它:
import { defineHost } from "@becomeopc/dshx/host";
import { statusApi } from "./api/status.js";
export default defineHost({
apis: [
statusApi.host({
get: () => ({ ready: true }),
}),
],
});
Client 直接消费同一个 Contract:
import { useApiQuery } from "@becomeopc/dshx/client";
import { statusApi } from "./api/status.js";
function Status() {
const query = useApiQuery(statusApi, "get", {
enabled: true,
});
if (query.status === "pending") {
return <span>Loading...</span>;
}
if (query.status === "error") {
return <button onClick={query.refetch}>Retry</button>;
}
return <span>{query.data.ready ? "Ready" : "Unavailable"}</span>;
}
这里我在意的并不是少写几行代码,而是错误能不能更早出现:
- 方法名写错,由 TypeScript 报错;
- Host 少实现一个 Handler,由精确的 Handler 类型拦住;
- 输入输出不符合 Schema,在 Host 边界拒绝;
- Client 使用了某项能力,但插件没有声明对应 Provider,由
dshx check提示; - 本地安装的 DSH 不在当前 Adapter 支持范围,构建或开发阶段直接给出兼容性诊断。
我不想一直等到插件装进 DSH、页面打开以后,才看到一个缺少上下文的 Runtime Error。
dshx dev 为什么必须运行真实 DSH
开发服务器最省事的做法,是自己 Mock 一个 DSH 环境。
这样启动快,也容易控制。但 Mock 出来的 Slot、Connection、Provider 和生命周期都是假的。开发服务器里一切正常,不代表插件安装到真实 DSH 后还能正常工作。
所以 dshx dev 运行的是真实 DSH Profile,而不是一套平行的模拟 Runtime:
- Client 修改走 DSH 官方 HMR;
- Host 修改成功后重新构建,并默认重启 Host;
- 初始构建通过后才启动 DSH;
- 配置或依赖重新加载失败时保留上一次可用会话。
Runtime Inspect 也遵循同样的原则:
dshx inspect slots
dshx inspect tools
dshx inspect services
dshx inspect events
inspect 只读取当前 Composition 中 Adapter 支持的官方 Provider。Runtime 不可用时,它会返回诊断,不会退回一份看起来完整、实际上与现场无关的离线目录。
这对 Coding Agent 也很有用。Agent 不必猜“这里大概有一个 sidebar.xxx Slot”,可以先 Inspect Runtime,再决定代码挂在哪里。
我希望 DSHX CLI 的命令尽量原子、可检查、可组合。CLI 给事实和诊断,下一步由开发者或 Agent 自己规划。
兼容性不能按每个 DSH 版本穷举
DSH 仍处于 Developer Preview。假设以后连续出现:
0.1.0
0.1.1
0.1.2
0.2.0
...
如果 DSHX 为每个版本写一个 Adapter,再给“每个插件 × 每个 DSH 版本”跑完整测试,这套维护模型很快就会失控。
所以 DSHX 使用“协议代际”管理兼容性:只有当官方 Contract、API seam、Loader 行为或 Runtime invariant 发生了需要不同适配的变化,才进入新的 Protocol Generation。单纯发布一个 patch 或 minor,并不会自动产生新 Adapter。
我也开始刻意区分三种经常被混在一起的事实:
Declared
作者通过 peerDependencies 声明支持范围
Compatible
版本落在一个已知协议代际中,但没有在该版本上完成真实验证
Verified
这个具体 DSH 版本通过了真实 Runtime smoke
对于未验证的 prerelease,DSHX 会进一步标记为 experimental;没有 Adapter 接管的版本则是 unsupported。
一个版本落在 SemVer 范围里,只能说明它与某个协议代际相交,不等于已经在真实 Runtime 上跑过。这个区别对插件市场尤其重要。
Framework Hub 不替插件作者做保证
我之前一度想把插件市场做得很严格:自动判断一个包是不是 DSH 插件、兼容哪些版本、能不能安装、元数据是否完整。
很快就发现,这会把维护成本推到不可接受的程度。第三方插件不会都按照 DSHX 的约定提供完整元数据,我也不可能替所有作者测试所有版本组合。
现在 dshx.io 的定位收敛了很多。Framework Hub 更像插件信息层:从 GitHub、npm 等公开来源整理项目、版本、源码、作者、README、安装目标、兼容声明和风险信号,并明确区分来源事实、社区整理和真实验证证据。
Hub 不会因为一个插件没有经过 DSHX 验证,就直接拒绝收录;也不会承诺它在某个用户的 DSH 环境里一定能安装成功。
对于社区插件,我更愿意把事实、证据和风险提示摆出来,把最后的决定留给用户。DSHX 只对自己确实知道的事情负责。
用 DSHX 写一个 DSH 内的插件市场
仓库里还有一个我很喜欢的 Dogfooding 项目:
@becomeopc/dshx-plugin-marketplace
它本身就是一个普通 DSH Bundle。安装以后,可以在:
Settings → Plugins → Marketplace
里浏览 Framework Hub 中可安装的插件。
这个 Marketplace 完整使用了 DSHX 的开发路径,包括:
defineHostdefineSettingsdefineApidefineClientdefineLocaledefineSlot- Standard Schema
useApiQuery- CSS Modules
- Profile 开发流程
- Client HMR
Preview 版本可以这样安装:
dsh plugin --profile web add @becomeopc/dshx-plugin-marketplace@preview
dsh --profile web
如果我自己的 Framework 连自己的插件市场都写得很痛苦,那 API 大概率还没有设计好。相比堆几十个独立 Demo,我更喜欢用一个真实插件持续暴露问题。
DSHX 终于有了第一个可用 Preview
折腾了几轮 API 和架构以后,DSHX 进入了第一个可以实际使用的 Preview 阶段。
创建一个插件:
pnpm create dshx@preview my-plugin
cd my-plugin
pnpm check
pnpm dev
需要更完整的 API 示例时:
pnpm create dshx@preview my-plugin --template showcase --style tailwind
目前已经覆盖:
- Host / Client Authoring;
- Typed API、Settings 与 Prompt;
- Slot 与 Locale;
- Vite 构建、CSS Modules 与 Tailwind;
- Profile 开发流程和 Client HMR;
- Runtime Inspect;
- CLI 检查、诊断与有限的确定性修复;
- DSH 协议代际与兼容 Adapter;
- 插件脚手架;
- Framework Hub;
- DSH 内的 Marketplace 插件。
Conversation Components 仍然放在 @becomeopc/dshx/experimental/conversation。Streaming 也没有急着做成公共抽象。
这些能力依赖上游更稳定的事件词汇、持久化、Connection Ownership、取消、重连和背压语义。现在先不提供,比做一套半年后必须废弃的私有协议更稳妥。
接下来,先用更多真实插件继续打磨
DSHX 目前仍然是 0.1.x。我没有急着继续增加更多 API,接下来更想找一些真实插件来写,看看这条链路还会在哪里卡住:
创建项目
↓
发现 DSH 能力
↓
编写 Host / Client
↓
本地检查
↓
真实 Profile 调试
↓
构建与发布 npm
↓
进入 Framework Hub
↓
用户安装
如果这条路径能够稳定下来,DSHX 才算真正解决了 DeepSeek Harness 插件开发体验的问题。
我最开始只是想写一个 Vite 插件。现在回头看,想做的其实是给 DeepSeek Harness 补一套完整、可检查、能持续演进的插件开发工作流。
项目仍是 Preview,API 还会调整。也正因为如此,现在很适合拿真实插件来折腾: