乐于分享
好东西不私藏

DeepSeek Harness 插件初探:做一个放烟花的 Demo插件

DeepSeek Harness 插件初探:做一个放烟花的 Demo插件

引言

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(ctxContext): void {  ctx.tools.register(fireworksTool)}

inject = ['tools'] 声明该插件依赖工具注册表。Cordis 会等待 tools 服务可用,再执行 apply

通过 ctx.tools.register() 建立的注册与插件 fiber 具有相同生命周期。插件释放后,launch_fireworks 也会从工具注册表移除。

对实际业务插件而言,这项机制可以减少手动清理逻辑。工具、事件监听器等通过 ctx 完成的注册,都由插件生命周期管理。

2. 定义工具

烟花工具使用原始 ToolDefinition

const fireworksToolToolDefinition = {name'launch_fireworks',description:'在 Web 界面中播放一场庆祝烟花动画。当用户要求放烟花或进行庆祝时使用此工具。',parameters: {type'object',additionalPropertiesfalse,properties: {message: {type'string',description'要显示的庆祝短文本。',      },bursts: {type'integer',description'烟花爆炸次数,范围 1 ~ 12。',      },duration_ms: {type'integer',description'动画持续时间,范围 2000 ~ 8000 毫秒。',      },    },  },async execute(argsunknown) {return resolveArgs(args)  },isConcurrencySafe() => true,}

工具名称、描述和参数 schema 会进入模型可见的工具集合。模型根据这些信息决定是否调用工具,以及应该生成哪些参数。

烟花工具接受三个可选参数:

参数
含义
限制
默认值
message
庆祝文字
非空,最多 40 个字符
为你绽放
bursts
烟花簇数
1~12 的整数
8
duration_ms
动画时间
2000~8000 毫秒
5200

参数来自模型生成的 JSON,需要在运行时重新校验。Demo 使用 resolveArgs() 拒绝未知字段、错误类型和越界数值:

function resolveArgs(valueunknown): 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<stringunknown>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 {messageresolveMessage(record.message),bursts: bursts as number,durationMsresolveDuration(record.duration_ms),  }}

bursts 的上限也约束了浏览器资源使用。每簇烟花包含 18 个粒子,因此最大 12 簇对应 216 个粒子节点。

3. 规范结果与模型可见文本

工具执行返回一个规范 JSON 对象:

interface FireworksResult {readonly messagestringreadonly burstsnumberreadonly durationMsnumber}

output.schema 描述该结果,output.render 再把它转换为模型可读取的内容:

output: {schema: {type'object',additionalPropertiesfalse,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(ctxClientContext): 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_fireworks

Tool 视图遇到这项调用时,会选择 FireworksRow;其他工具继续使用各自的专用组件或通用工具卡片。

ctx.slots.inject() 还处理了加载顺序。烟花插件只有在 tool.call.toolview 已声明时才进行注册;槽位或插件被释放时,注册也随之释放。业务插件可以复用相同方式:

每个插件只注册自己拥有的工具名称,不需要修改 Tool 视图的集中分发代码。

5. 从会话记录推导执行状态

工具调用在流式阶段可能只包含不完整的参数,例如:

{"message":

FireworksRow 解析失败时使用默认值,等待后续完整记录:

function modelFor(blockToolCallViewProps['block'],): FireworksModel {let parsedRecord<stringunknown> = {}try {const candidate = JSON.parse(argsRaw(block)) as unknownif (      candidate !== null      && typeof candidate === 'object'      && !Array.isArray(candidate)    ) {      parsed = candidate as Record<stringunknown>    }  } catch {// 流式调用可能暂时只提供不完整的 JSON 前缀。  }const settled = 'kind' in blockreturn {messageresolveMessage(parsed.message),burstsresolveBursts(parsed.bursts),durationMsresolveDuration(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>

这个选择是为了两个问题:

  1. React 重新渲染不会随机改变粒子位置;
  2. 历史记录重新挂载时,不需要启动永久运行的动画循环。

烟花舞台通过 overflow: hidden 限制在工具卡片内:

.stage {position: relative;heightclamp(220px34vh320px);overflow: hidden;border-radius16px;}

用户启用减少动态效果时,CSS 停止动画并显示静态粒子:

@media (prefers-reduced-motion: reduce) {.flash,.particle {animation: none;  }.particle {opacity0.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-demo

Host 扫描到该包的 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 能力。当然也可能还有一些文档读的不到位,走了歪路。后续可以在此基础优化。 

蹲一蹲,感觉最近插件市场会很热闹。