跳转到主要内容

这是本节的多页打印视图。 .

返回本页常规视图.

扩展 Y8

从一个想改进的具体任务开始。Y8 使用 DSH 的插件机制,你可以采用已有扩展,也可以开发自己的插件,沿用同一种插件格式。

专业插件可以从两个问题开始:领域分析引擎需要处理什么数据、返回什么结果?操作面板需要让用户发起什么操作、检查哪些结果?按需求扩展其中一部分或两部分,复用 DSH 已有的运行能力。背后的分工见产品理念

选择合适的方式

你的需求从这里开始
Y8 已经包含的能力阅读默认插件及其使用前提
已有其他人维护的能力插件市场查找,检查 README 和兼容要求
固定流程、提示词或输出标准编写工作区 Skill;仅有操作说明时无需新建运行时插件
调整选项或组合现有能力先看设置,再看插件配置和 Bundle 说明
接入一个已有外部服务先检查该服务是否提供可用的 MCP 集成
增加操作、服务、设置卡片或界面进入下面的插件开发路径

定义一个小需求

把这份提纲复制到对话或项目 README。先填写已知信息,其余可以让助手检查现有项目;提供样例数据,不要填写凭据。

  • 要解决的任务:
  • 输入内容及来源:
  • 期望输出,以及在哪里检查:
  • 允许修改的范围,以及禁止执行的操作:
  • 需要的账号、服务和配置项:
  • 一份样例输入及预期结果:

例如:只读查询团队构建服务的失败任务,返回失败原因和链接,不触发重跑。提供一份脱敏响应作为验收样例。这是扩展需求示例,不表示 Y8 已经接入该服务。

开发并检查结果

  1. 借助 Creator 开发,查询可用接口,试验一个范围明确的需求。
  2. 第一个插件加载可编辑源码。本教程需要准备好的 DSH 源码 checkout,仅有桌面安装包还不够。
  3. 增加一个工具配置项,每次修改一种行为并检查可见结果。
  4. 打包与安装把源码交付给其他人。临时动态试验不等于已安装的软件包。

专业扩展示例见 Harbor 如何开发。框架、事件和服务细节见框架指南;尚未在本站发布的深入参考会打开 GitHub。

在团队内复用

共享项目或确切的软件包版本、支持的 DSH 版本、安装步骤、配置项名称和验收样例。同事使用自己的账号与目录,再重复执行样例。共享文件不携带私人会话或凭据。本地 checkout 链接只在作者机器上有效,交给他人时使用打包指南。

文档中的步骤失败时,通过故障排查提供具体步骤、版本与错误,帮助区分说明缺失和需要完善的开发能力。

参与 Y8 开发

构建桌面应用和维护站点属于贡献者工作。相关 GitHub 参考为桌面源码指南站点维护指南产品声明

1 - 借助 Creator 开发

Creator 帮助你检查正在运行的 DSH 组合,并试验一个小插件。任务需要新操作或界面时使用它;如果只是调整配置或固定流程,先看扩展方式选择

准备环境

本指南使用 DSH 源码 Web 应用。先完成 GitHub 上的源码环境准备,再在仓库根目录运行:

pnpm dsh web --no-open --port 0

打开命令打印的启动地址,包括其中的认证片段。系统会分配可用端口。选择工作目录并配置模型后再发送请求,在会话的 Agent Preset 选择器中选择“创造模式”(Creator,预设 ID 为 cordis)。如果当前安装的 Y8 未提供该预设,请使用这条源码路径;仅安装 Y8 不代表已具备独立插件开发环境。

描述改动

使用需求提纲,请助手加载 Creator 预设中的插件开发 Skill;涉及组合配置时,再使用该预设中的组合编辑 Skill。

首次试验可以要求一个临时只读工具,返回某项任务的简短检查清单,并要求助手先查询当前工具注册接口。写明预期结果,要求验证后停止试验。模型请求使用你配置的账号。

先查询再实现

Creator 可以列出检查提供方、查询实际接口,并读取已有试验的源码和诊断。请它将操作与服务放在 Host,将界面放在 Client,确有需要时再组合两者。缺少能力时应明确报告,不能猜测 API。

激活前先看源码预览和预期效果。动态代码是普通 JavaScript 函数体,不是 TypeScript 或 JSX 模块。定义试验不等于执行它。按提示处理审批,并检查最终运行状态;等待中或启动中都不代表成功。

检查并停止

调用新增工具或操作改动后的界面,与样例中的预期结果比较。失败时让 Creator 检查该试验的诊断和源码。完成后停止它,确认对应工具或界面贡献消失。停止会移除注册,不会撤销已经写出的文件或外部操作。

保留有用的成果

动态定义保存在进程内,Host 重启后会消失。会话中记录过源码,不代表会自动重新加载。重启前把有用源码和需求保存到项目,再通过第一个插件教程打包指南建立并验证普通插件。这条路径没有动态代码自动转包功能。

修改已有插件时,继续维护原项目及其身份和配置。自定义预设使用用户自己的副本,不要直接修改随应用提供的 Creator 预设,因为升级会替换它。确切运行规则和诊断说明见 GitHub 上的动态工具参考

2 - 第一个插件

本教程会创建一个最小的 Harness 插件,并将其加载到 Web UI 中。请从已完成从源码运行路径的仓库检出开始。

创建本地项目

在仓库根目录创建本教程使用的临时项目:

mkdir -p scratch-plugin/src

插件是什么

在 Harness 中,插件是一个导出 apply 函数的 TypeScript 模块。框架在加载时调用 apply,传入一个 ctx(上下文对象),你通过 ctx 注册能力:

import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-plugin'

export function apply(ctx: Context) {
  // Register capabilities here.
}

这就是完整配置。

创建插件文件

创建 scratch-plugin/src/my-plugin.ts

import type { Context } from '@deepseek-ai/cordis'

export const name = 'hello-plugin'

export function apply(ctx: Context) {
  // Required dependencies are ready before apply runs.
  console.log('[hello-plugin] plugin loaded!')
}

注册到 cordis.yml

在仓库根目录运行 pwd,然后创建 scratch-plugin/cordis.yml,作为插入本地插件的 Web 覆盖层。请将下文的 /absolute/path/to/deepseek-harness 替换为命令打印的路径:

- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'

插件路径必须是绝对路径。patch 文件只贡献配置,不会改变 loader 解析模块路径时使用的 profile 目录。

使用该覆盖层启动 Web UI:

pnpm dsh web --patch ./scratch-plugin/cordis.yml --no-open --port 0

打开终端打印的启动地址,包括其中的认证片段。启动期间,终端会打印 [hello-plugin] plugin loaded!

修改与停止

修改插件中的问候语,使用 Ctrl+C 停止开发命令,再运行同一条命令。检查终端是否打印新的问候语。每次重启使用新打印的启动地址;端口 0 表示由系统分配可用端口。完成后停止开发命令。

自动清理

通过 ctx 注册的任何东西——事件监听、工具、定时器——在插件卸载时都会被自动清理。你不需要手动 removeListener 或 clearInterval。

如果你有需要手动清理的资源(比如一个网络连接),用 ctx.effect() 告诉框架怎么清理:

import type { Context } from '@deepseek-ai/cordis'

export function apply(ctx: Context) {
  ctx.effect(() => {
    const timer = setInterval(() => {
      console.log('heartbeat')
    }, 5000)

    // The returned function runs when the plugin unloads.
    return () => clearInterval(timer)
  })
}

声明依赖

如果你的插件需要使用其他服务(如 toolsllm),需要声明 inject

import type { Context } from '@deepseek-ai/cordis'

export const name = 'my-tool-plugin'
export const inject = ['tools']

export function apply(ctx: Context) {
  // ctx.tools is ready here.
  ctx.tools.register(/* ... */)
}

框架会确保依赖的服务就绪后才加载你的插件。

插件的三种形态

除了函数形式,插件还支持对象形式和类形式:

对象形式

import type { Context } from '@deepseek-ai/cordis'

export default {
  name: 'my-plugin',
  inject: ['tools'],
  apply(ctx: Context) {
    // ...
  },
}

类形式

import { Service, type Context } from '@deepseek-ai/cordis'

export default class MyService extends Service {
  static inject = ['tools']

  constructor(ctx: Context) {
    super(ctx, 'myService')
    // Perform synchronous initialization in the constructor.
  }
}

大多数情况下,函数形式足够了。当插件需要向其他插件提供服务时,可使用类形式(见 服务与依赖)。

下一步

3 - 开发一个工具

本教程会在 Web UI 中添加一个 greet 工具。请先完成第一个插件,并保留其中的 scratch-plugin 目录。

创建工具插件

scratch-plugin/src/my-plugin.ts 替换为:

import type { Context } from '@deepseek-ai/cordis'
import { defineTool } from '@deepseek-ai/dsh-tools'

export const name = 'greet-tool'
export const inject = ['tools']

export function apply(ctx: Context) {
  ctx.tools.register(defineTool({
    name: 'greet',
    description: 'Greet someone by name.',
    parameters: {
      name: { type: 'string', required: true, description: 'The name to greet' },
    },
    output: {
      schema: { type: 'string' },
      render: (_args, value) => [{ type: 'text', text: value }],
    },
    async execute(args) {
      return `Hello, ${args.name}!`
    },
  }))
}

inject 让 Cordis 等待工具注册表就绪。defineTool 根据 parameters 推导并校验 argsexecute 返回 output.schema 声明的规范值,output.render 再将该值转换为面向模型的内容。

运行并调用工具

如果开发命令未在运行,请重新启动:

pnpm dsh web --patch ./scratch-plugin/cordis.yml --no-open --port 0

打开终端打印的启动地址,包括其中的认证片段,然后输入:Use the greet tool to greet Ada. 模型可以调用 greet,并收到 Hello, Ada! 这一工具结果。

下一步

  • 插件配置 — 让问候语可配置。
  • 工具编写参考 — 查阅嵌套 schema、规范值、后台工作、策略钩子、PTC mode 和 UI 卡片。
  • 能力分层 — 将可替换能力拆分为 Service Definition、Service Provider 和 Consumer 三类包。

4 - 插件配置

让你的插件接受用户在 cordis.yml 中传入的配置。

先完成第一个插件。和该教程一样,将下方绝对路径替换为自己的仓库路径;patch 不会改变插件模块的解析目录。

定义 Config 类型

在插件中导出一个 Config 类型和同名的 Schemastery schema;默认值直接写在 schema 中:

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'

export const name = 'my-plugin'

export interface Config {
  greeting: string
  maxRetries: number
  verbose?: boolean
}

export const Config: Schema<Config> = Schema.object({
  greeting: Schema.string().default('Hello'),
  maxRetries: Schema.number().default(3),
  verbose: Schema.boolean().default(false),
})

export function apply(ctx: Context, config: Config) {
  console.log(config.greeting)  // User value or schema default.
}

scratch-plugin/cordis.yml 新插入的本地插件行中添加配置:

- insert:
    - id: hello
      name: '/absolute/path/to/deepseek-harness/scratch-plugin/src/my-plugin.ts'
      config:
        greeting: 'Hi there'
        maxRetries: 5

插件加载时,Cordis 会通过导出的 schema 校验配置,并填充未提供字段的默认值。不要导出普通对象作为 Config,因为它不满足 Cordis 要求的 Standard Schema 接口。

Schema 校验

对于需要严格校验的场景,使用 Schemastery 定义 schema:

import type { Context } from '@deepseek-ai/cordis'
import Schema from '@deepseek-ai/schemastery'

export const name = 'validated-plugin'

export interface Config {
  apiKey: string
  timeout: number
  mode: 'fast' | 'accurate'
}

export const Config = Schema.object({
  apiKey: Schema.string().required(),
  timeout: Schema.number().default(30000),
  mode: Schema.union(['fast', 'accurate']).default('fast'),
})

export function apply(ctx: Context, config: Config) {
  // config is validated and type-safe.
}

Schema 在插件加载时执行校验。如果配置不合法,插件会加载失败并给出明确错误信息。

设计原则

无硬编码可调参数

Harness 的约定:凡是不同部署可能需要采用不同值的参数,都必须定义为配置字段

// Wrong: hardcoded timeout.
const TIMEOUT = 30000

// Correct: configurable.
export interface Config {
  timeoutMs: number  // Defaults to 30000.
}

检验标准:能否在 cordis.yml 中改变这个值,而不需要修改代码?

配置错误要响亮

在 schema 中表达自身完备的约束,使无效配置在插件加载时失败。对服务或已注册资源的引用需要依赖注入;服务教程 会介绍这项约定。

应用并验证

修改源码或 patch 后,停止并重启教程命令。使用上面的配置时,终端会打印 Hi there。把 maxRetries: 5 改为 maxRetries: wrong-type,重启后检查插件加载时的校验错误;继续前恢复有效值。这条路径不依赖源码或 overlay 自动重载。

下一步

5 - 打包与安装插件

前几篇教程通过 --patch overlay 加载本地插件。本教程把它打包成可安装的组合包(bundle),用 dsh plugin add 安装进一个 profile,并解释决定组合后配置的层顺序。本文假设 dsh CLI 已安装。请先完成插件配置

如果改用全新的源码 checkout,请先按照从源码运行章节完成准备,将本教程的 hello-plugin 目录放在仓库根目录,并从该目录把下文的 dsh ... 命令改为 pnpm dsh ...。构建与启动器行为见源码执行

两个概念,两种 manifest

安装机制建立在两个概念之上。二者都由一份 package.json 描述,但它们在 dsh 键下携带的 manifest(元数据清单)种类不同,回答的问题也不同:

  • 组合包是附带一个配置层的 npm 包。它的 manifest 声明 dsh.bundle,回答的是"这个包贡献什么?":一个插入或覆盖插件行的 patch 文件。
  • profile 是位于 $DSH_HOME/profiles/<name> 下、描述一份可启动组合的目录。它的 manifest 声明 dsh.profile,回答的是"这套配置由哪些组合包按什么顺序组成?"。

组合包是你编写并分发的东西;profile 是用户用 dsh --profile <name> 启动的东西。没有东西同时是两者。

组合包 manifest

创建包目录:

mkdir -p hello-plugin
hello-plugin/
├── package.json       # declares dsh.bundle
├── cordis.patch.yml   # the layer applied when a profile lists this bundle
└── index.js           # plugin modules the patch rows reference

创建 hello-plugin/package.json

{
  "name": "dsh-hello-plugin",
  "version": "0.1.0",
  "type": "module",
  "main": "index.js",
  "files": ["index.js", "cordis.patch.yml"],
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
}

创建 hello-plugin/index.js,写入插件入口:

export const name = 'hello-plugin'

export function apply() {
  console.log('[hello-plugin] plugin loaded!')
}

创建 hello-plugin/cordis.patch.yml。这个 patch 与你写过的 --patch overlay 一样,是一个 patch 条目的 YAML 数组;区别是插件行按包名而不是相对源码路径引用这个包,这样 Node 的模块解析才能找到已安装的代码:

- insert:
    - id: hello
      name: dsh-hello-plugin

没有 dsh.bundle 声明的包仍然可以安装,但只作为普通依赖:dsh plugin 会打印警告,且不激活任何层。如果一个库供插件包 import,而不是供用户启用,就使用这种包格式。

profile manifest

profile 目录包含两个文件:

  • package.json — profile 的树外插件依赖(由 pnpm 管理),加上 dsh.profile manifest 及其有序的 bundles 列表。
  • cordis.patch.yml — 用户自己的 patch 层,在每个组合包层之后应用。

profile manifest 从不需要手写:dsh --profile <name> --from-default-profile <template> 可以从随附应用模板创建 profile,dsh plugin 则创建一个以 base 为基础的 profile,并维护其中已安装的 bundle 列表。创建规则以 CLI(命令行界面)行为参考为准;下一节展示插件路径。

安装进 profile

dsh plugin --profile <name> <args...> 在 profile 目录内转发给 pnpm,因此所有 pnpm 子命令都可用。在包含 hello-plugin 的目录中安装该包的 checkout:

dsh plugin --profile demo add ./hello-plugin

首次使用会初始化 profile(@deepseek-ai/dsh-base 作为它的第一个组合包),pnpm 链接该 checkout,而 dsh 因为这个包声明了 dsh.bundle,把它追加进 dsh.profile.bundles

{
  "name": "dsh-profile-demo",
  "private": true,
  "dependencies": {
    "dsh-hello-plugin": "link:/path/to/hello-plugin"
  },
  "dsh": {
    "profile": {
      "bundles": [
        "@deepseek-ai/dsh-base",
        "dsh-hello-plugin"
      ]
    }
  }
}

先不启动、只验证该层,再启动:

dsh --profile demo --dump-config   # shows a "# == dsh-hello-plugin" layer
dsh --profile demo

dsh plugin --profile demo remove dsh-hello-plugin 会同时移除依赖和对应的层。

加载顺序

生效配置在空根之上按以下顺序逐层组合:

  1. profile 的 dsh.profile.bundles 列表所列的各个组合包 patch,按列表顺序——先是 @deepseek-ai/dsh-base,然后是每个已安装组合包,按其加入顺序。
  2. profile 自己的 cordis.patch.yml
  3. home 级的 $DSH_HOME/cordis.patch.yml——各 profile 共享的机器本地偏好。
  4. 每个 --patch <path> overlay,按 argv 顺序。

应用参数不是另一层 patch。表层组合包可以通过下文所述的普通应用自有服务解析它们。

后应用的层按行胜出,且 patch 会替换目标行的整个 config 值,而不是深度合并各键。这给组合包作者带来两个推论:

  • 你的 patch 可以按 id 覆盖前面各层的行——就像 dsh-web-app 组合包覆盖 dsh-base 的行那样——但必须重述该行需要的每一个键,而不是只写改动的那个。
  • 用户可以在自己 profile 的 cordis.patch.yml 中覆盖你的行,无需改动你的包,所以优先给出用户大概率会保留的配置默认值,其余交给 schema 承担。

内置组合包名称始终从 dsh 安装目录本身解析;pnpm 只管理树外的包,所以你的组合包可以放心依赖 @deepseek-ai/dsh-base 存在且与安装保持一致。

让表层组合包持有自己的命令行

定义了可运行应用的组合包挂载一个普通提供方插件:

- id: hello-startup
  name: 'dsh-hello-plugin/startup'

该插件导出 inject = ['cmdlineArgs'],使用自己的 commander program 调用 @deepseek-ai/dsh-cmdline 中的 parseCmdline,再在 program 自己的 action 中把应用自有服务提供出去。启动器把自身 flag 之后的同一份不可变参数交给每个插件,因此添加应用专属 flag 无需修改启动器,多个插件也可以解析该快照。Loader 行不需要启动器标记或特殊类型。

受这些参数配置的行会注入提供方服务,并在自己的 !!js 选项中读取它,同时把部署取值写在旁边作为回退:

- id: my-app
  name: '@example/my-app'
  inject: [myAppStartup]
  config:
    port: !!js ctx.myAppStartup.port ?? 8080

遇到 --help 时,提供方不会发布该服务,所以这些行不会激活。Loader 只挂载一次组合,等待每一行的普通注入,再基于其已注入的上下文求值该行的 !!js 配置。

从 GitHub 安装:构建脚本这道坎

发布到注册表不是必须的——用户可以直接从 git 托管安装:

dsh plugin --profile demo add github:you/hello-plugin

但 git 安装拉取的是源码,不是构建产物:没有任何环节运行你的 build 脚本,因此 TypeScript 包到手时没有 lib/ 输出,加载会失败。必须两边各做一件事:

  • 作者提供一个 prepare 脚本——pnpm 在 git 安装后运行它——从源码构建出发布入口,且必须自包含:不能假设仅开发环境才有的上下文,例如旁边有一份 monorepo checkout。turtle-ui 是一个可用的例子:它的 prepare 运行一份专用的 tsdown 配置,直接转译 src/,不用项目引用,也不做类型检查。

  • 用户为构建授权。pnpm ≥10 在得到显式允许之前拒绝运行 git 依赖的 prepare 脚本,所以第一次 add 会失败;dsh 会指出修法——把 pnpm 打印的确切包键复制进该 profile 的 pnpm-workspace.yaml

    allowBuilds:
      dsh-hello-plugin: true

    然后重新执行 add

请把这项授权视为允许该包的代码在安装时于你的机器上执行,且不在 agent 运行的任何沙箱之内。只对源码可信的包授权,并锁定 commit(github:you/hello-plugin#<sha>),让后续推送无法悄悄改变实际运行的内容。

如果不想让用户做这项授权,就改为分发构建产物——以下两种形式都不需要任何构建权限:

  • 发布到 npm,在 pnpm publish 时构建好 lib/dsh plugin add your-package 安装的就是预构建代码。
  • 交付 tarball:用 pnpm pack 打包;用户执行 dsh plugin add ./hello-plugin-0.1.0.tgz

下一步