byx-darwin/dsh-agent-kit ↗★ 0

@mc/dsh-agent-kit

为常驻智能体提供通道集成与任务管理服务 适合需要构建包含WebSocket、钉钉/飞书通知及任务调度能力的智能体开发者。

套件
@mc/dsh-agent-kit
相容性
待驗證
Harness 依賴範圍
0.1.5-rc.3 || 0.1.7-alpha.2
Cordis 依賴範圍
4.0.2 || 4.0.4
版本
0.1.0
授權
MIT
最近更新
2026年9月24日

安裝

$npx -p @deepseek-ai/dsh dsh plugin --profile web add github:byx-darwin/dsh-agent-kit

在 Profile 的 cordis.patch.yml 中启用需要的 Service(见下文「配置」)

dsh --profile my-agent --dump-config # 检查各层是否生效 dsh --profile my-agent --no-open


## 编写业务包

业务包把本包声明为 peer 依赖,保证一个 Profile 中只加载一份实例:

```jsonc
{
  "name": "@your-org/dsh-agent-your-project",
  "type": "module",
  "peerDependencies": {
    "@deepseek-ai/cordis": "4.0.2",
    "@mc/dsh-agent-kit": "^0.1.0"
  },
  "devDependencies": {
    "@deepseek-ai/cordis": "4.0.2",
    "@mc/dsh-agent-kit": "^0.1.0"
  },
  "dsh": { "bundle": { "patch": "./patch.yml" } }
}

@deepseek-ai/cordis 的版本与目标 dsh 依赖的版本保持一致(dsh 0.1.5-rc.3 对应 4.0.2,0.1.7-alpha.2 对应 4.0.4)。开发期可以用 link: 指向本包的本地 checkout。

插件示例(发通知推荐用 ctx.notify:业务包不关心发到钉钉还是飞书,运维可以随时切换渠道,不需要改业务代码):

import type { Context } from '@deepseek-ai/cordis'
import { choice, isKitError, noul, untrusted } from '@mc/dsh-agent-kit'

export const name = 'my-agent'
export const inject = ['agentWs', 'notify', 'agentTasks', 'jev']

export function apply(ctx: Context, config: Config) {
  const conn = ctx.agentWs.connect({
    url: config.url,
    headers: async () => ({ 'X-Token': await getToken() }), // 每次(重)连接前调用
    parse: parseFrame,                                       // 把 JsonValue 转成业务类型,抛错则丢弃该帧
    concurrency: 2,
    onMessage: async (frame, { signal }) => {
      if (frame.type === 'alert') {
        // 发往哪个渠道由 agent-kit-notify 的 channel 决定(或运行中的 ctx.notify.use())
        await ctx.notify.send({
          title: frame.title,
          markdown: renderAlert(frame),
          idempotencyKey: frame.id,
          traceId: frame.id,
        })
        return
      }

      const draft = await ctx.agentTasks.run({
        provider: 'claude-code',
        title: `classify ${frame.id}`,
        // 来自 WebSocket 的数据是不可信的,用 untrusted() 包裹,声明它不是指令
        prompt: [CLASSIFY_INSTRUCTIONS, untrusted('event', frame.input)],
        outputSchema: ResultSchema, // JSONSchemaType,draft.output 的类型为 Result
        signal,
        traceId: frame.id,
      })

      // Agent 输出同样不可信:触发副作用前按业务白名单校验
      if (!CATEGORIES_ALLOWLIST.has(draft.output.category)) throw new Error('unexpected category')

      const { answers } = await ctx.jev.judge({
        state: { input: frame.input, candidate: draft.output },
        questions: {
          wrong: noul('候选结果与输入证据不符', { true: '不符', false: '相符' }),
          pick: choice('输入最符合哪个类别', CATEGORIES),
        },
        signal,
        traceId: frame.id,
      })

      // noul 为回答"是"的概率;choice 带 confidence 与各选项概率
      await conn.send({ id: frame.id, result: draft.output, verified: answers.wrong.noul  {
      if (isKitError(err) && err.retryable) {
        // 例如:记录下来,交给对端系统重投
      }
    },
  })
}

通知:ctx.notify

ctx.notify 同一时间只发往一个渠道。send() 返回 { channel, results },channel 是实际发往的渠道,results 是该渠道的逐目标结果(多群时部分失败体现在这里,不抛错)。当前渠道的行没有运行(未启用或启动失败)时抛出 NotifyError(code: channel_unavailable);渠道自己发送失败时抛出渠道的错误,例如 FeishuSendError、DingtalkSendError,code 与 retryable 照常可用。不会自动改发另一个渠道。

需要按渠道指定目标或 @ 人时,两个渠道各给一项,发送时只用当前渠道那一项:

await ctx.notify.send({
  title: '需要人工复核',
  markdown: body,
  targets: { dingtalk: { chatId: 'cidxxxx' }, feishu: { chatId: 'oc_xxxx' } }, // 缺省用该渠道的 defaultTarget
  at: { feishu: { userIds: ['ou_xxxx'] } },                                    // 两个渠道的用户 id 体系不同
  idempotencyKey: frame.id,
})

切换渠道有两种方式,都不会重新加载 notify,也不会重新加载只 inject notify 的业务插件:

  • 改配置:修改 agent-kit-notify 的 channel(手动编辑 cordis.patch.yml,或用 admin 包的设置页 / setup)。notify 在自己的 internal/update 钩子里原地换上新值,拦截了 cordis 默认的插件重启。
  • 运行中切换:ctx.notify.use('feishu') 立即生效,只改内存中的选择;dsh 重启后回到配置值,期间配置被修改时以新配置为准(覆盖 use())。要长期切换请改配置。ctx.notify.channel 返回当前生效的渠道;传入未知渠道时抛出 NotifyError(code: invalid_channel)。

notify 不 inject 钉钉与飞书,而是在发送时查找它们,所以启用、停用渠道行也不会重新加载 notify。

ctx.notify.status() 返回当前渠道与两个渠道的实时登录状态:

const s = await ctx.notify.status()
// {
//   channel: 'feishu',
//   source: 'runtime',                      // 'config':来自配置;'runtime':被 use() 切换过
//   channels: {
//     feishu: { channel: 'feishu', running: true, identity: 'user', online: true, account: '张三', detail: '…', checkedAt: 1758… },
//     dingtalk: { channel: 'dingtalk', running: false, detail: 'agent-kit-dingtalk is not running' },
//   },
// }

渠道登录状态与登录

ctx.dingtalk 与 ctx.feishu 都提供 status()、login()、logout();ctx.notify.login(channel?) / logout(channel?) 转给当前渠道(或指定的渠道)。

  • status(): Promise:通过 CLI 实时检查,返回 { channel, identity, online, account?, detail, checkedAt }。dryRun 时只报告,不改变 health()。
  • login({ signal? }): Promise:设备流登录,拿到授权链接就返回 { channel, verificationUrl, userCode?, expiresAt, completed, cancel() }。链接怎么交给要登录的人(发到运维群、显示在业务包自己的页面……)由业务包决定。对方在浏览器里授权后 completed 以新的状态 resolve;链接过期(15 分钟)、被拒绝或调用 cancel() 时同样 resolve,online 为 false。登录进行中再次调用 login() 返回同一个会话。
  • logout(): Promise:退出登录,返回退出后的状态。
export const inject = ['notify']

const st = await ctx.notify.status()
const cur = st.channels[st.channel]                  // running 为 false 时没有 online 字段
if (cur.running && !cur.online) {
  const s = await ctx.notify.login()                 // 当前渠道的设备流登录
  await postToOps(`请在 15 分钟内打开链接完成登录:${s.verificationUrl}`) // 由业务包决定怎么交付链接
  const after = await s.completed                    // 授权完成,或过期 / 拒绝 / cancel()
  if (!after.online) ctx.logger('my-agent').warn('login not completed: %s', after.detail)
}
渠道身份login()logout()
钉钉user / botdws auth login --device --no-browserdws auth logout
钉钉webhook抛出 unsupported(无需登录,status() 总是 online)抛出 unsupported
飞书userlark-cli auth login --scope=im:message.send_as_user im:message --no-wait --json,随后在后台运行 --device-code= 等待授权lark-cli auth logout
飞书bot抛出 unsupported(bot 使用 lark-cli config init 配置的应用凭据,没有用户登录)抛出 unsupported

注意:dws 的登录态是本机共享的(属于运行 Profile 的系统用户),同一用户下使用 dws 的其他程序也会看到登录与退出的结果。钉钉 logout() 只退出当前账号(dws auth logout --profile=:),不用 dws 默认的「退出全部账号」;取不到当前账号时不做任何事。

登录相关的错误码:unsupported(该身份没有登录)、login_failed(CLI 在给出授权链接之前就失败了),都在 DingtalkSendError / FeishuSendError 上。相关类型 ChannelName、ChannelStatus、LoginSession、ChannelNotRunning、NotifyStatus 与常量 DEFAULT_LOGIN_TTL_MS 从根入口导出。

直接使用某个渠道

需要某个渠道特有的能力(例如钉钉的 openDingtalkId 目标、webhook 身份)时,直接 inject 对应的 Service,用法不变:

export const inject = ['dingtalk']            // 或 ['feishu']

await ctx.dingtalk.send({ title: frame.title, markdown: renderAlert(frame), idempotencyKey: frame.id, traceId: frame.id })
await ctx.feishu.send({ title: frame.title, markdown: renderAlert(frame), target: { chatId: 'oc_xxxx' }, idempotencyKey: frame.id })

本地开发与测试

没有 dws / lark-cli 或 Agent 登录态时,可以把 dingtalk / feishu 设为 dryRun: true,并使用 @mc/dsh-agent-kit/testing 导出的工具开发与测试:

import { createFakeDws, createFakeLark, createJevMock, FakeSubagentProvider, FakeSubagentRuntime, startTestWsServer } from '@mc/dsh-agent-kit/testing'

const server = await startTestWsServer()          // ws://127.0.0.1:
,可模拟 401、关闭 pong
const dws = createFakeDws({ send: [{ mode: 'partial', failTargets: ['cidB'] }] })
process.env.DWS_CONFIG_DIR = dws.dir              // 假 dws 从这里读取场景;dingtalk 配置 dwsPath: dws.path
const lark = createFakeLark({ auth: { bot: true }, send: [{ mode: 'success' }] })
Object.assign(process.env, lark.env)               // 假 lark-cli 从 LARKSUITE_CLI_CONFIG_DIR 读取场景;feishu 配置 larkPath: lark.path
lark.calls()                                       // 每次调用的参数与收到的环境变量名,可断言白名单
const restoreJev = createJevMock().install()      // jev 不发网络请求
const provider = new FakeSubagentProvider({ name: 'claude-code', handler: () => '```json\n{"category":"a"}\n```' })
await ctx.plugin(FakeSubagentRuntime, { providers: [provider] }) // 或注册到真实 ctx.subagents

完整接口、错误码、配置默认值和卸载语义见 设计文档。

配置

各 Service 的可变参数都是 cordis.yml 中经过校验的配置字段,均有默认值与取值范围。本包的 bundle 以禁用状态注册六个 loader 行(agent-kit-ws、agent-kit-dingtalk、agent-kit-feishu、agent-kit-notify、agent-kit-agent-tasks、agent-kit-jev),在 Profile 的 cordis.patch.yml 中按 id 启用并给出配置:

- id: agent-kit-ws
  disabled: false
- id: agent-kit-dingtalk
  disabled: false
  config:
    identity: bot
    robotCode: dingxxxx
    defaultTarget: { chatId: cidxxxx }
- id: agent-kit-feishu
  disabled: false                              # 需要 lark-cli,并已运行 lark-cli config init
  config:
    identity: bot
    defaultTarget: { chatId: oc_xxxx }
- id: agent-kit-notify
  disabled: false
  config:
    channel: feishu                            # dingtalk 或 feishu;对应的渠道行需要启用
- id: agent-kit-agent-tasks
  disabled: false
  config:
    workspaceDir: /var/lib/my-agent/tasks
    declaredPermissions:
      claude-code: read-only                   # claude-code 默认 permissionMode: dontAsk,不能执行命令或写文件
- id: agent-kit-jev
  disabled: false                              # 需要 @typesafe-ai/sdk 与环境变量 TYPESAFE_API_KEY
  config:
    keychainService: [ai.typesafe.api-key, gitflow-cli-typesafe]  # 可选:macOS 上未设置环境变量时按顺序从钥匙串读取;旧名仅用于迁移期

按 id 修改 config 时整段替换;未给出的字段使用默认值。

Service主要字段
agentWspingIntervalMs、readTimeoutMs、reconnect.initialDelayMs、reconnect.maxDelayMs、reconnect.jitter、stableResetMs、fatalRetryDelayMs、maxPayloadBytes、maxPendingMessages
dingtalkidentity(必填)、defaultTarget、robotCode、webhookTokenEnv、dwsPath、timeoutMs、killGraceMs、retry.maxAttempts、preflightIntervalMs、dryRun
feishuidentity(必填,bot / user)、defaultTarget、profile、larkPath、timeoutMs、killGraceMs、retry.maxAttempts、preflightIntervalMs、dryRun
notifychannel(必填,dingtalk / feishu)
agentTasksworkspaceDir(必填)、defaultTimeoutMs、maxConcurrency、maxQueueSize、keepWorkdir、declaredPermissions、toolAllowlist
jevmodel、timeoutMs、keychainService、keychainAccount
  • agentTasks.declaredPermissions:claude-code、codex 不支持按任务过滤工具,权限由 provider 实例自己的配置决定。运维在这里声明其实际权限上限(read-only / workspace-write);未声明或上限高于任务请求的档位时,任务以 unsupported_permissions 失败。
  • dingtalk 的 webhook 身份只能把 token 作为命令行参数传给 dws(会出现在 ps 中),不推荐使用。
  • feishu.defaultTarget 可以是 { chatId: 'oc_…' }(群)、{ userId: 'ou_…' }(用户 open_id,单聊)或 { chatIds: [...] }(多个群,逐个发送,最多 100 个);feishu.profile 对应 lark-cli --profile。
  • feishu 只在给出 idempotencyKey 时自动重试;lark-cli 的幂等键最长 50 个字符,多目标时本包逐目标派生。
  • notify.channel 指向的渠道行需要单独启用;未启用时发送报 channel_unavailable,admin 包的 doctor 检查 agent-kit-notify.channel 也会失败。
  • dingtalk.dwsPath 必须指向可直接执行的文件(可执行二进制、.exe 或 .js);不支持 Windows 的 .cmd/.bat 包装脚本(feishu.larkPath 同理),因为本包用 spawn(..., { shell: false }) 不经过 shell 启动子进程,无法解释批处理语法。

TypeSafe Key(TYPESAFE_API_KEY)按以下优先级解析,三个平台的落盘位置不同:

来源说明
环境变量进程自身的 TYPESAFE_API_KEY,优先级最高,admin 包的 CLI/Web 均不会覆盖或清除它
macOS 钥匙串通过 security 命令读写,服务名默认 ai.typesafe.api-key(keychainService 可配置为数组做迁移期兼容);仅 macOS 可用
dsh 凭据文件$DSH_HOME/.credentials.yaml,由 @deepseek-ai/dsh-credentials-local 管理;Linux/其他平台的默认落盘位置,macOS 上作为钥匙串之外的第二选择

安全

  • 密钥只从环境变量、macOS 钥匙串或 dsh 凭据文件读取,且不传给本包启动的子进程;钉钉凭据由 dws 自己的登录态管理,飞书的应用凭据与令牌由 lark-cli 自己的配置(lark-cli config init)与系统钥匙串管理,本包都不保存。
  • 启动 dws、lark-cli 时不经过 shell,子进程只继承环境变量白名单。lark-cli 额外只放行运行所需的 LARKSUITE_CLI_CONFIG_DIR、LARKSUITE_CLI_PROFILE、LARKSUITE_CLI_BRAND、LARKSUITE_CLI_CA_PATH、LARKSUITE_CLI_PROXY_ADDRESS、LARKSUITE_CLI_PROXY_ENABLE,以及定位配置目录的 XDG_CONFIG_HOME、USERPROFILE、APPDATA、LOCALAPPDATA(见导出的 LARK_ENV_WHITELIST),LARKSUITE_CLI_APP_SECRET、*_ACCESS_TOKEN 等凭据类变量不会传入。
  • 本包运行时从不安装任何 CLI;只有 admin 包的 setup 在你确认后才运行锁定版本的 npm i -g。
  • 设备流登录的授权链接只交给调用 login() 的业务包,本包不会把它发到任何地方;把链接发给谁、发到哪个群由业务包决定,请只发给应当登录的人。
  • 来自 WebSocket 的数据和 Agent 的输出都视为不可信:Agent 默认只读,在每个任务独立的空目录中运行;输出经过 Schema 与业务白名单校验后才应触发副作用。
  • 只允许 wss://,不关闭证书校验,不跟随重定向。
  • 日志和错误经过统一脱敏,不记录鉴权头、API Key、webhook token 和完整消息正文。
  • dsh Web 界面中保存了完整的 prompt 与输出,只应绑定 127.0.0.1 或放在鉴权代理之后。
  • 发送给 Claude Code、Codex、TypeSafe 的内容由业务包决定,请在业务包中做数据最小化。

运维工具(可选):@mc/dsh-agent-kit-admin

@mc/dsh-agent-kit-admin 提供配置引导与运维界面。它读写的仍然是 Profile 的 cordis.patch.yml,不装它不影响任何 Service。业务包不需要依赖它(只有用到下文的运行时登记时,Profile 里才必须有它)。

dsh plugin --profile my-agent add @mc/dsh-agent-kit @mc/dsh-agent-kit-admin

admin 包以常驻行 agent-kit-admin 注册(ctx.agentKitAdmin,即设置页调用的服务端接口),并让 dsh 发现它的前端模块;它不连接任何外部服务,也不影响基础包各行的启停。

doctor 与 setup

admin 包自带 CLI dsh-agent-kit(随包安装到 node_modules/.bin),不需要启动 dsh:


### 直接使用某个渠道

需要某个渠道特有的能力(例如钉钉的 `openDingtalkId` 目标、webhook 身份)时,直接 inject 对应的 Service,用法不变:

```ts
export const inject = ['dingtalk']            // 或 ['feishu']

await ctx.dingtalk.send({ title: frame.title, markdown: renderAlert(frame), idempotencyKey: frame.id, traceId: frame.id })
await ctx.feishu.send({ title: frame.title, markdown: renderAlert(frame), target: { chatId: 'oc_xxxx' }, idempotencyKey: frame.id })

配置

各 Service 的可变参数都是 cordis.yml 中经过校验的配置字段,均有默认值与取值范围。本包的 bundle 以禁用状态注册六个 loader 行(agent-kit-ws、agent-kit-dingtalk、agent-kit-feishu、agent-kit-notify、agent-kit-agent-tasks、agent-kit-jev),在 Profile 的 cordis.patch.yml 中按 id 启用并给出配置:

- id: agent-kit-ws
  disabled: false
- id: agent-kit-dingtalk
  disabled: false
  config:
    identity: bot
    robotCode: dingxxxx
    defaultTarget: { chatId: cidxxxx }
- id: agent-kit-feishu
  disabled: false                              # 需要 lark-cli,并已运行 lark-cli config init
  config:
    identity: bot
    defaultTarget: { chatId: oc_xxxx }
- id: agent-kit-notify
  disabled: false
  config:
    channel: feishu                            # dingtalk 或 feishu;对应的渠道行需要启用
- id: agent-kit-agent-tasks
  disabled: false
  config:
    workspaceDir: /var/lib/my-agent/tasks
    declaredPermissions:
      claude-code: read-only                   # claude-code 默认 permissionMode: dontAsk,不能执行命令或写文件
- id: agent-kit-jev
  disabled: false                              # 需要 @typesafe-ai/sdk 与环境变量 TYPESAFE_API_KEY
  config:
    keychainService: [ai.typesafe.api-key, gitflow-cli-typesafe]  # 可选:macOS 上未设置环境变量时按顺序从钥匙串读取;旧名仅用于迁移期

按 id 修改 config 时整段替换;未给出的字段使用默认值。

Service主要字段
agentWspingIntervalMs、readTimeoutMs、reconnect.initialDelayMs、reconnect.maxDelayMs、reconnect.jitter、stableResetMs、fatalRetryDelayMs、maxPayloadBytes、maxPendingMessages
dingtalkidentity(必填)、defaultTarget、robotCode、webhookTokenEnv、dwsPath、timeoutMs、killGraceMs、retry.maxAttempts、preflightIntervalMs、dryRun
feishuidentity(必填,bot / user)、defaultTarget、profile、larkPath、timeoutMs、killGraceMs、retry.maxAttempts、preflightIntervalMs、dryRun
notifychannel(必填,dingtalk / feishu)
agentTasksworkspaceDir(必填)、defaultTimeoutMs、maxConcurrency、maxQueueSize、keepWorkdir、declaredPermissions、toolAllowlist
jevmodel、timeoutMs、keychainService、keychainAccount
  • agentTasks.declaredPermissions:claude-code、codex 不支持按任务过滤工具,权限由 provider 实例自己的配置决定。运维在这里声明其实际权限上限(read-only / workspace-write);未声明或上限高于任务请求的档位时,任务以 unsupported_permissions 失败。
  • dingtalk 的 webhook 身份只能把 token 作为命令行参数传给 dws(会出现在 ps 中),不推荐使用。
  • feishu.defaultTarget 可以是 { chatId: 'oc_…' }(群)、{ userId: 'ou_…' }(用户 open_id,单聊)或 { chatIds: [...] }(多个群,逐个发送,最多 100 个);feishu.profile 对应 lark-cli --profile。
  • feishu 只在给出 idempotencyKey 时自动重试;lark-cli 的幂等键最长 50 个字符,多目标时本包逐目标派生。
  • notify.channel 指向的渠道行需要单独启用;未启用时发送报 channel_unavailable,admin 包的 doctor 检查 agent-kit-notify.channel 也会失败。
  • dingtalk.dwsPath 必须指向可直接执行的文件(可执行二进制、.exe 或 .js);不支持 Windows 的 .cmd/.bat 包装脚本(feishu.larkPath 同理),因为本包用 spawn(..., { shell: false }) 不经过 shell 启动子进程,无法解释批处理语法。

TypeSafe Key(TYPESAFE_API_KEY)按以下优先级解析,三个平台的落盘位置不同:

来源说明
环境变量