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用于定义配置 schemadeepseek-ai/dsh-tools提供defineTooldeepseek-ai/dsh-llm提供createUserMessage用于自动触发时注入上下文。2.2 可复制的 tsconfig.jsondsh 插件运行在 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、依赖声明和applyimport 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_backgroundctx.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 里重设 HOMEconst 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_codexconst injectedFor new Setstring() 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 与 OAuth5.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/apiKey 在控制台创建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_KEYsk-你的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 无限的可能性。把它写成一个目录、一行配置、一个工具剩下的交给组合。