引言
DeepSeek Harness 是deepseek刚发布的一个基于 Cordis 构建的插件化 Agent Harness平台。
按照官方说法:一切皆插件,运行有迹可循;
模型、工具、技能、会话、沙箱、存储、循环、调度、UI 等所有 Agent 能力均由插件组合而成,可以自由替换和灵活重组,同时,模型看到的一切都会写入仅追加设计的会话日志,包括系统提示词、思维链、工具调用与结果、子 Agent 调度,以及每一次上下文注入。在 Trajectory 视图中,你可以按来源查看这些信息。恢复、分叉、检索与回放也共享同一份事件流。
涨价的“梁子”,现在这波操作下来,我单方面宣布你又是“梁圣”了。

DeepSeek Harness 这套东西,牛X就牛X在一切皆插件,看着官方刚发布的热乎文档,手痒难耐,跟着codex老师搓了一个插件demo。

插件效果:
轨迹追溯:

DeepSeek Harness 插件开发的三个核心概念:
Plugin:通过 apply(ctx)向运行环境注册工具、服务、事件或界面组件。Bundle:可安装的 npm 包,描述插件向配置树中增加什么。 Profile:一个可运行的 Harness 组合,记录当前启用了哪些 Bundle。
烟花demo插件注册了一个名为 launch_fireworks 的模型工具。模型调用工具时会实现:
用户请求 1.模型生成 launch_fireworks 调用 2. Host 校验参数并执行工具 3.工具调用和结果写入 Session Log 4.Web 按工具名称选择 FireworksRow 5.React 与 CSS 渲染烟花卡片插件目录:
fireworks-plugin/├── src/index.ts # Host 工具├── src/client/index.ts # Web 插件注册├── src/client/FireworksRow.tsx # 工具卡片├── src/client/FireworksRow.module.css # 烟花动画├── package.json # Bundle 与 Client 声明└── cordis.patch.yml # Profile 配置层烟花插件 Demo 整体设计上,把「能力」和「表现」拆分开来:Host 负责产生规范结果,Web 负责解释并展示该结果。
1.从 apply 开始
DeepSeek Harness 基于 Cordis 组织插件。一个插件模块导出 apply 函数,并通过 ctx 注册能力:
import type { Context } from '@deepseek-ai/cordis'export const name = 'fireworks-demo'export const inject = ['tools']export function apply(ctx: Context): void { ctx.tools.register(fireworksTool)}inject = ['tools'] 声明该插件依赖工具注册表。Cordis 会等待 tools 服务可用,再执行 apply。
通过 ctx.tools.register() 建立的注册与插件 fiber 具有相同生命周期。插件释放后,launch_fireworks 也会从工具注册表移除。
对实际业务插件而言,这项机制可以减少手动清理逻辑。工具、事件监听器等通过 ctx 完成的注册,都由插件生命周期管理。
2. 定义工具
烟花工具使用原始 ToolDefinition:
const fireworksTool: ToolDefinition = {name: 'launch_fireworks',description:'在 Web 界面中播放一场庆祝烟花动画。当用户要求放烟花或进行庆祝时使用此工具。',parameters: {type: 'object',additionalProperties: false,properties: {message: {type: 'string',description: '要显示的庆祝短文本。', },bursts: {type: 'integer',description: '烟花爆炸次数,范围 1 ~ 12。', },duration_ms: {type: 'integer',description: '动画持续时间,范围 2000 ~ 8000 毫秒。', }, }, },async execute(args: unknown) {return resolveArgs(args) },isConcurrencySafe: () => true,}工具名称、描述和参数 schema 会进入模型可见的工具集合。模型根据这些信息决定是否调用工具,以及应该生成哪些参数。
烟花工具接受三个可选参数:
message | 为你绽放 | ||
bursts | |||
duration_ms |
参数来自模型生成的 JSON,需要在运行时重新校验。Demo 使用 resolveArgs() 拒绝未知字段、错误类型和越界数值:
function resolveArgs(value: unknown): FireworksResult {if (value === null || typeof value !== 'object' || Array.isArray(value)) {throw new Error('launch_fireworks arguments must be an object') }const record = value as Record<string, unknown>const unknown = Object.keys(record) .filter(key => !['message', 'bursts', 'duration_ms'].includes(key))if (unknown.length > 0) {throw new Error(`launch_fireworks received unknown argument ${JSON.stringify(unknown[0])}`, ) }const bursts = record.bursts ?? 8if ( !Number.isInteger(bursts) || (bursts as number) < 1 || (bursts as number) > 12 ) {throw new Error('launch_fireworks `bursts` must be an integer from 1 to 12', ) }return {message: resolveMessage(record.message),bursts: bursts as number,durationMs: resolveDuration(record.duration_ms), }}bursts 的上限也约束了浏览器资源使用。每簇烟花包含 18 个粒子,因此最大 12 簇对应 216 个粒子节点。
3. 规范结果与模型可见文本
工具执行返回一个规范 JSON 对象:
interface FireworksResult {readonly message: stringreadonly bursts: numberreadonly durationMs: number}output.schema 描述该结果,output.render 再把它转换为模型可读取的内容:
output: {schema: {type: 'object',additionalProperties: false,properties: {message: { type: 'string' },bursts: { type: 'integer' },durationMs: { type: 'integer' }, },required: ['message', 'bursts', 'durationMs'], },render(_args, value) {const result = value as unknown as FireworksResultreturn [{type: 'text',text:`Fireworks launched: ${result.message} — ` + `${result.bursts} bursts over ${result.durationMs} ms.`, }] },}默认调用示例:
{"message": "为你绽放","bursts": 8,"durationMs": 5200}模型收到的文本是:
Fireworks launched: 为你绽放 — 8 bursts over 5200 ms.
规范 JSON 供持久化、程序处理和 Web 渲染使用; 文本结果供模型继续推理和回复用户。
注意:没有加载烟花 UI 的 headless 或 ACP 组合仍然能够完整执行工具。浏览器动画只是同一条工具记录的一种表现,不存在强关联。
4. Web 插件注册渲染器
浏览器端使用 tool.call.toolview keyed slot 注册 FireworksRow:
import type { ClientContext }from '@deepseek-ai/dsh-client-runtime/client'import { FireworksRow } from './FireworksRow.tsx'export const inject = ['slots']export function apply(ctx: ClientContext): void { ctx.slots.inject('tool.call.toolview',() => ctx.slots.register( {name: 'tool.call.toolview',key: 'launch_fireworks', },FireworksRow, ), )}Host 和 Web 使用同一个键:
ToolDefinition.name = launch_fireworksSlot key = launch_fireworksTool 视图遇到这项调用时,会选择 FireworksRow;其他工具继续使用各自的专用组件或通用工具卡片。
ctx.slots.inject() 还处理了加载顺序。烟花插件只有在 tool.call.toolview 已声明时才进行注册;槽位或插件被释放时,注册也随之释放。业务插件可以复用相同方式:
每个插件只注册自己拥有的工具名称,不需要修改 Tool 视图的集中分发代码。
5. 从会话记录推导执行状态
工具调用在流式阶段可能只包含不完整的参数,例如:
{"message":FireworksRow 解析失败时使用默认值,等待后续完整记录:
function modelFor(block: ToolCallViewProps['block'],): FireworksModel {let parsed: Record<string, unknown> = {}try {const candidate = JSON.parse(argsRaw(block)) as unknownif ( candidate !== null && typeof candidate === 'object' && !Array.isArray(candidate) ) { parsed = candidate as Record<string, unknown> } } catch {// 流式调用可能暂时只提供不完整的 JSON 前缀。 }const settled = 'kind' in blockreturn {message: resolveMessage(parsed.message),bursts: resolveBursts(parsed.bursts),durationMs: resolveDuration(parsed.duration_ms),running: !settled,failed: settled && block.isError, }}组件据此展示三种状态:
if (model.failed) {return (<section data-tool="launch_fireworks" data-state="error"> 烟花未能点燃</section> )}return (<sectiondata-tool="launch_fireworks"data-state={model.running ? 'running' : 'ok'} ><span> {model.running ? '正在点燃烟花' : '烟花已点亮'}</span></section>)界面状态完全来自工具调用和结果记录。刷新页面或重新打开历史会话时,相同记录可以重新生成相同的工具卡片。
6. 确定性布局与手动重放
烟花位置由带种子的随机数生成器计算。组件使用 callId 和重放次数构造种子:
const [cycle, setCycle] = useState(0)const bursts = useMemo(() => burstLayout(`${callId}:${cycle}`, model.bursts, model.durationMs, ), [callId, cycle, model.bursts, model.durationMs],)同一条调用在同一个 cycle 下会得到稳定布局。用户点击「再放一次」后增加 cycle,组件才计算下一组位置:
<buttontype="button" onClick={() => setCycle(value => value + 1)}> 再放一次</button>这个选择是为了两个问题:
React 重新渲染不会随机改变粒子位置; 历史记录重新挂载时,不需要启动永久运行的动画循环。
烟花舞台通过 overflow: hidden 限制在工具卡片内:
.stage {position: relative;height: clamp(220px, 34vh, 320px);overflow: hidden;border-radius: 16px;}用户启用减少动态效果时,CSS 停止动画并显示静态粒子:
@media (prefers-reduced-motion: reduce) {.flash,.particle {animation: none; }.particle {opacity: 0.48;transform:rotate(var(--firework-angle))translateY(calc(-0.55 * var(--firework-distance)))scale(0.7); }}7. 交付 Host 与 Web 入口
package.json 声明两个导出:
{"name": "dsh-fireworks-demo","type": "module","main": "lib/index.js","exports": {".": {"types": "./lib/types/index.d.ts","default": "./lib/index.js"},"./client": {"types": "./lib/types/client/index.d.ts","default": "./lib/client.js"}}}其中:
.指向 Host 插件;./client指向浏览器插件。
dsh manifest 描述 Bundle 和浏览器模块:
{"dsh": {"bundle": {"patch": "./cordis.patch.yml"},"client": {"inject": ["@deepseek-ai/dsh-client-runtime","@deepseek-ai/dsh-client-ui-tool"],"platform": "web"}}}Bundle 的配置层只插入一个 Loader 行:
- insert:- id: fireworks-demoname: dsh-fireworks-demoHost 扫描到该包的 dsh.client 声明后,会把 ./client 对应的构建产物加入 Web 启动图。用户安装或移除一个包,就能同时控制工具与工具卡片。
8. 构建和安装
在仓库根目录构建独立 Demo:
pnpm --dir fireworks-plugin install --ignore-workspacepnpm --dir fireworks-plugin run buildpnpm --dir fireworks-plugin run test构建生成两个主要文件:
lib/index.jslib/client.js把插件加入 Web profile:
pnpm dsh plugin --profile web add ./fireworks-pluginpnpm dsh --profile web --dump-configpnpm dsh --profile web随后可以在对话中发送:
请调用 launch_fireworks,用 6 簇烟花庆祝插件发布成功。移除插件:
pnpm dsh plugin --profile web remove dsh-fireworks-demo插件通过 profile 参与运行时组合,不需要修改默认 Web Bundle。
9. 业务借鉴
Agent = Model + Harness
烟花 Demo 对应的是一个可视化工具,但它展示的结构可以迁移到业务插件上。
例如:
订单查询工具返回订单 JSON,Web 端展示状态卡片; 部署工具返回阶段和日志摘要,Web 端展示进度; 数据分析工具返回序列和结论,Web 端绘制图表; 审批工具返回请求状态,Web 端提供对应操作; 文件处理工具返回产物信息,Web 端展示预览和下载入口。
Demo插件 的规模有限,但覆盖了 DeepSeek Harness 插件从模型调用到 Web 展示的主要扩展点。保留这套结构,只替换工具执行逻辑、结果类型和专用组件,逐步实现面向实际业务的 Agent 能力。当然也可能还有一些文档读的不到位,走了歪路。后续可以在此基础优化。
蹲一蹲,感觉最近插件市场会很热闹。
夜雨聆风