news 2026/10/24 14:24:10

DeepSeek Harness (dsh) 插件开发实战:从零写一个自动调用 Codex 的插件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepSeek Harness (dsh) 插件开发实战:从零写一个自动调用 Codex 的插件

1. 从零理解 dsh 插件:为什么“一切皆插件”值得你动手

DeepSeek Harness(下称 dsh)是一个把“一切皆插件”落到实处的智能体运行时。它构建在开源插件框架 Cordis 之上,内核极小,工具、LLM 适配器、沙箱、终端、UI、命令、skill、权限策略全部以插件形式存在,通过配置组合成完整能力。换句话说,你想给 dsh 加任何能力,路径只有一条:写一个插件,然后在配置里点亮它。

这篇教程面向想让 dsh 自动触发 Codex 完成代码任务的开发者。我会带你从初始化 TypeScript 工程开始,定义插件清单与生命周期钩子,封装 Codex 调用并处理流式返回,最后在本地加载插件、触发一次真实调用、核对返回结果。整个过程基于 dsh 0.1.0-rc.5 源码实测,示例是一个真实可用的插件:用户一提到 “codex”,dsh 就自动把任务交给 WSL 里的 Codex CLI 完成。

先明确一个概念:dsh 插件不是一次性的扩展点,而是 Citizen(一等公民),和官方内置插件共享同一套机制。一个插件就是一个导出apply(ctx)函数的 TypeScript 模块,框架加载时调用apply,把上下文对象ctx交给你,你通过它注册一切能力。没有框架启动代码,插件只描述自己的贡献,组合方式是配置的事。

插件有三种形态:函数形态最常用,导出apply(ctx);对象形态导出default { name, apply };类形态用于对外提供服务,继承Service。所有通过ctx做的注册——事件监听、工具注册、定时器——在插件卸载时都会被自动清理,你不需要手动removeListener或clearInterval。需要依赖其他服务时,声明inject,框架会等依赖就绪后再加载你的插件。

理解这些之后,你会发现 dsh 插件开发的门槛比想象中低。接下来我们从工程初始化开始,一步步把插件跑起来。

2. 初始化 TypeScript 工程与插件清单配置

2.1 创建工程与安装依赖

先建目录并初始化:

mkdir wsl-codex && cd wsl-codex pnpm init pnpm add -D typescript @types/node pnpm add @deepseek-ai/cordis @deepseek-ai/schemastery @deepseek-ai/dsh-tools @deepseek-ai/dsh-llm

这里的关键依赖有四个:@deepseek-ai/cordis提供Context与Service类型;@deepseek-ai/schemastery用于定义配置 schema;@deepseek-ai/dsh-tools提供defineTool;@deepseek-ai/dsh-llm提供createUserMessage,用于自动触发时注入上下文。

2.2 可复制的 tsconfig.json

dsh 插件运行在 Node ESM 环境,tsconfig 需要开启module: NodeNext,并确保strict打开,因为 schema 推导依赖严格类型:

{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "declaration": true, "outDir": "dist", "rootDir": "src", "types": ["node"] }, "include": ["src/**/*.ts"] }

moduleResolution: NodeNext是必须的,否则@deepseek-ai/*的子路径导出会解析失败。skipLibCheck建议打开,避免第三方类型定义拖慢编译。

2.3 插件入口与 manifest 配置

插件入口src/index.ts的最小骨架如下,包含名称、配置 schema、依赖声明和apply:

import type { Context } from '@deepseek-ai/cordis' import Schema from '@deepseek-ai/schemastery' export const name = 'wsl-codex' export interface Config { distro: string keyword: string codexBin: string timeoutMs: number skipGitRepoCheck: boolean } export const Config = Schema.object({ distro: Schema.string().default('Ubuntu-22.04'), keyword: Schema.string().default('codex'), codexBin: Schema.string().default('codex'), timeoutMs: Schema.number().default(600_000), skipGitRepoCheck: Schema.boolean().default(true), }) export const inject = ['tools', 'subprocess', 'agents'] export function apply(ctx: Context, config: Config) { // 工具注册与自动触发逻辑在下一节展开 }

凡是“不同部署可能需要不同取值”的参数,都必须定义成配置字段,这是 dsh 的硬性约定。默认值直接写在 schema 里,框架会在加载时校验并填充默认值。inject声明了插件依赖的服务,框架会等ctx.tools、ctx.subprocess、ctx.agents就绪后再调用apply。

如果你要打包分发给别人,还需要一个带dsh.bundlemanifest 的package.json:

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

cordis.patch.yml是这个包贡献的配置层,用户安装后dsh plugin --profile web add ./wsl-codex会把它追加到 profile 的dsh.profile.bundles列表。

3. 封装 Codex 调用:工具注册与流式返回处理

3.1 用 defineTool 注册工具

工具是模型能调用的函数。用defineTool定义,schema 自动推导出类型化参数并做运行时校验:

import { defineTool } from '@deepseek-ai/dsh-tools' ctx.tools.register(defineTool({ name: 'wsl_codex', description: 'Run OpenAI Codex inside WSL on a self-contained task and return its final answer. ' + 'Use this whenever the user mentions Codex or asks to delegate work to Codex.', parameters: { task: { type: 'string', required: true, description: 'The task to hand to Codex.' }, cwd: { type: 'string', description: 'Working dir (WSL path or Windows path, auto-mapped).' }, }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }], }, async execute(args, exec) { // 调用逻辑见下 }, }))

几个要点:execute(args)返回的是规范 JSON 值,人类可读的文案放在output.render里,不要混在一起;遵守exec.signal及时取消;长任务走run_in_background+ctx.jobs;部署策略(允许/拒绝/审批)不要写进工具,用tools/pre-execute等钩子实现。

3.2 路径映射与 HOME 重设

Windows 路径要转成 WSL 路径,否则cd会失败:

function winToWslPath(winPath: string): string { const normalized = winPath.replaceAll('\\', '/') const drive = /^([a-zA-Z]):\//.exec(normalized) if (drive) return `/mnt/${drive[1].toLowerCase()}/${normalized.slice(3)}` return normalized.startsWith('/') ? normalized : normalized }

关键坑:wsl.exe会把 Windows 的HOME传进 WSL,导致 codex 读到 Windows 侧配置。必须在 bash 里重设 HOME:

const wslCwd = winToWslPath(args.cwd ?? exec.agent?.session.header.cwd ?? process.cwd()) const script = [ 'export HOME="$(getent passwd "$(id -un)" | cut -d: -f6)"', `cd "${wslCwd}"`, `${config.codexBin} exec${config.skipGitRepoCheck ? ' --skip-git-repo-check' : ''}`, ].join(' && ')

3.3 流式返回与超时处理

用ctx.subprocess.spawn启动进程,stdin 写入任务,stdout 收集流式输出:

const controller = new AbortController() exec.signal.addEventListener('abort', () => controller.abort(exec.signal.reason), { once: true }) const timer = setTimeout(() => controller.abort(new Error(`timeout ${config.timeoutMs}ms`)), config.timeoutMs) try { const handle = ctx.subprocess.spawn({ argv: ['wsl.exe', '-d', config.distro, '--cd', wslCwd, '--', 'bash', '-lc', script], cwd: process.cwd(), stdio: { stdin: { data: args.task }, stdout: { maxBytes: 200_000 }, stderr: { maxBytes: 50_000 }, }, graceMs: 5_000, signal: controller.signal, }) const outcome = await handle.done const stdout = handle.collected.stdout?.readFrom(0).text ?? '' if (outcome.exitCode !== 0) throw new Error(`codex exited ${outcome.exitCode}`) return stdout.trim() } finally { clearTimeout(timer) exec.signal.removeEventListener('abort', () => controller.abort(exec.signal.reason)) }

maxBytes限制防止内存爆掉,graceMs给进程优雅退出时间,signal让取消能传导到子进程。

3.4 自动触发钩子

用户消息命中关键词时,注入上下文让模型调用wsl_codex:

const injectedFor = new Set<string>() ctx.on('session/event', (session, event) => { if (event.type !== 'user/message') return if (event.data.source.kind !== 'user') return const text = event.data.content .map(b => (b.type === 'text' ? b.text : '')) .join('\n') if (!text.toLowerCase().includes(config.keyword.toLowerCase())) return const agent = ctx.agents.get(session.id) if (!agent || injectedFor.has(session.id)) return injectedFor.add(session.id) agent.inject(createUserMessage({ content: [{ type: 'text', text: 'The user mentioned Codex. Use the wsl_codex tool to delegate the work.' }], source: { kind: 'plugin', plugin: name }, })) })

每个会话只提醒一次,避免重复注入。

4. 本地加载插件与验证 Codex 调用结果

4.1 在 profile 中点亮插件

插件写好后不会自动加载,必须显式声明在配置层里。在 profile 的cordis.patch.yml里插入一行:

- insert: - id: wsl-codex name: 'file:///F:/my-project/wsl-codex/src/index.ts' config: distro: Ubuntu-22.04 keyword: codex timeoutMs: 600000

注意 Windows 上路径必须是file:///URL,写F:/...会被当成f:协议抛ERR_UNSUPPORTED_ESM_URL_SCHEME。

4.2 用 --dump-config 核对配置来源

重启 dsh web 前,先用--dump-config看每一行来自哪个文件、被谁修改过:

pnpm dsh --profile web --dump-config

输出里应该能看到wsl-codex这一行,以及它的config对象。如果没出现,说明 patch 文件路径或层级不对。

4.3 触发一次真实调用

重启 dsh web 后,在会话里输入包含 “codex” 的消息,比如:

用 codex 帮我把 src/utils.ts 里的日期格式化函数改成支持时区参数

预期行为:dsh 自动注入提醒,模型调用wsl_codex工具,WSL 里的 Codex CLI 执行任务,最终答案返回会话。终端里能看到[wsl-codex] plugin loaded!说明插件已生效。

4.4 核对返回结果

检查三点:一是工具调用记录里wsl_codex的task参数是否是你输入的任务;二是stdout是否包含 Codex 的最终答案;三是exitCode是否为 0。如果exitCode非 0,看stderr里的错误信息。

5. 常见报错排查:401、local proxy failed 与 OAuth

5.1 401 Unauthorized

如果 Codex CLI 返回 401,通常是认证信息没读到。检查 WSL 里的~/.codex配置是否存在,以及 HOME 是否被正确重设。可以在 WSL 里手动跑一次codex exec --skip-git-repo-check确认认证正常。

5.2 local proxy failed

这个报错通常出现在网络请求层。检查 WSL 的网络配置是否能访问 Codex 服务,以及codexBin路径是否正确。如果 Codex 需要走特定端点,确认环境变量已传入。

5.3 reading choices 报错

reading choices一般出现在流式返回解析阶段,说明 stdout 格式不符合预期。检查maxBytes是否太小导致输出被截断,或者 Codex 版本不兼容。可以先把maxBytes调大再试。

5.4 OAuth 相关错误

如果 Codex 使用 OAuth 认证,WSL 里的浏览器回调可能无法完成。建议在 WSL 里先手动完成一次 OAuth 登录,确认 token 已缓存,再让插件调用。

5.5 配置层按行整替

patch 覆盖一行是替换整个config对象,不是深合并。覆盖内置插件时要重述它需要的全部键,否则会丢配置。

5.6 插件路径协议错误

Windows 上写F:/...会抛ERR_UNSUPPORTED_ESM_URL_SCHEME,必须写成file:///F:/...。

6. 接入 TaoToken 与长期编码方案

如果你希望 Codex 调用走更稳定的模型接入层,可以把 Codex 的 Base URL 指向 TaoToken 的 API 端点。TaoToken 提供兼容 OpenAI 的接口,适合作为长期编码与 Agent 场景的模型入口。

配置时三件套要写全:Base URL 填https://taotoken.net/api,Key 在控制台创建,Model ID 按你使用的模型填写。在 Codex 的配置里对应设置:

# ~/.codex/config.toml model = "your-model-id" model_provider = "taotoken" [model_providers.taotoken] base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"

然后在 WSL 里导出环境变量:

export TAOTOKEN_API_KEY="sk-你的key"

验证请求是否成功,可以直接用 curl 测一次:

curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"your-model-id","messages":[{"role":"user","content":"ping"}]}'

返回choices字段说明接入正常。如果要在 dsh 里长期跑编码任务,建议用 Coding Plan 管理额度与模型切换;需要调试模型行为时用模型对话页面快速验证;创建和管理 Key 在 API Keys 页面;接入细节看接入文档。

插件给了 dsh 无限的可能性。把它写成一个目录、一行配置、一个工具,剩下的交给组合。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/24 14:24:00

当光伏遇上大型工商业:解析主从机防逆流保护技术如何保障“自发自用”|TaoToken 统一 Key 通道下的 GOOSE 联调实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/24 14:24:05

驱动升级指南:从16.1656到16.1692的稳定性优化与问题修复

1. 从16.1656到16.1692&#xff1a;一次驱动版本升级背后的真实动机驱动版本号这种东西&#xff0c;平时没人会盯着看&#xff0c;只有当设备开始闹脾气——花屏、掉帧、外设时断时续、系统日志里反复刷错误码——才会有人翻出设备管理器&#xff0c;盯着那一串数字发呆。16.16…

作者头像 李华
网站建设 2026/10/16 17:30:31

生物医学基础模型强在哪里?我们真的知道它学到了什么吗

生物医学基础模型越来越强&#xff0c;刷榜速度一个季度比一个季度快。组会里最常听到的一句话是"这个模型又SOTA了"&#xff0c;但紧接着的问题往往是&#xff1a;它到底抓住了什么生物学信号&#xff1f;是真实学到了调控机制&#xff0c;还是只记住了数据里的批次…

作者头像 李华
网站建设 2026/10/16 17:30:33

AI Skills:从提示词到可复用技能包,一文学会开发与落地

你有没有过这种体验&#xff1a;在 Claude 或 Codex 里折腾一下午&#xff0c;反复调整提示词&#xff0c;结果它写出来的代码还是不像你团队的风格&#xff1b;让它处理批量文件&#xff0c;每次都得重新交代一遍规则&#xff1b;换一台电脑&#xff0c;之前调教好的工作流全部…

作者头像 李华
网站建设 2026/10/16 17:30:31

单片机C库运行时:链接布局、可重入与中断安全实战

1. 从一次HardFault说起&#xff1a;为什么C库运行时值得单独拎出来讲很多人写单片机代码&#xff0c;注意力几乎全放在外设寄存器、中断向量、时钟树上&#xff0c;觉得C库就是#include <string.h>之后随便调调memcpy、strlen的事。我早年也是这么想的&#xff0c;直到有…

作者头像 李华