DSH Plugin Store
Back to home

mitao-su

dsh-playwright-native

把原生 Playwright CLI 注册为 DeepSeek Harness 透传工具(dsh-plugin)

Stars
1
Language
TypeScript
Created
Aug 14, 2026
Updated
Aug 14, 2026
Other
GitHub repo

Introduction

把 Playwright 原生 CLI 注册成 DeepSeek Harness 工具

目标:让 Agent 直接调用本机安装的原生 playwright 命令——playwright testplaywright installplaywright show-reportplaywright --version……——而不是某个二次封装后的语义工具。

DSH 里"给 Agent 加一个工具"只有一条正路:在插件里用 ctx.tools.register(defineTool(...)) 注册。所以"把 Playwright CLI 注册成 DSH 插件"的正确做法是:注册一个名叫 playwright 的透传工具,把 Agent 给的参数原样、按序转发给本机 playwright 二进制。它不拆分 install/test/show-report 子工具、不做任何参数映射——Agent 敲什么,就跑什么。

本仓库就是这样一个插件(dsh-playwright-native),可直接安装:

dsh plugin --profile web add github:mitao-su/dsh-playwright-native

目录

  1. 它和"封装"的区别(先看清要什么)
  2. 前置要求
  3. 原理:工具注册 = ctx.tools.register
  4. 工程结构
  5. Step 1 package.json
  6. Step 2 cordis.patch.yml
  7. Step 3 src/index.ts 实现透传工具
  8. Step 4 构建
  9. Step 5 注册进 profile
  10. Step 6 使用
  11. Step 7 发布到 GitHub(带 dsh-plugin topic)
  12. 常见问题

1. 它和"封装"的区别(先看清要什么)

语义封装(不要)原生注册(本仓库)
工具名playwright_version / playwright_test / …playwright
参数每个子命令一套自订字段一个 args: string[],原样透传
行为把 CLI 选项映射成自定义 schemaAgent 敲什么就 playwright 什么
Agent 视角调一个"别人打包过的工具"原生命令

一句话:CLI 是能力,ctx.shell 是执行缝,defineTool + cordis.patch.yml 是注册。 我们要的是最后两样,不重写第一样。


2. 前置要求

要求
Playwright本机已安装 playwright 命令(playwright --version 可跑)。默认用它;也可在配置里改成 npx --no-install playwright
DSHdsh 可用(本教程验证于 0.1.0-rc.6);目标 profile 已挂载 shell 执行器(标准 web/headless 自带)
pnpm用于 dsh plugin add(pnpm ≥ 10 对 Git 依赖默认拦截构建脚本,见 §11)

验证环境:

playwright --version     # 例如 Version 1.62.1
dsh --version            # 例如 0.1.0-rc.6

3. 原理:工具注册 = ctx.tools.register

DSH 的模型可见工具全部来自插件注册(或 MCP server)。核心契约:

export const name = 'playwright-native'
export const inject = ['tools', 'shell', 'systemPrompt']   // 必需 service

export function apply(ctx: Context, config: Config): void {
  ctx.tools.register(defineTool({
    name: 'playwright',            // ← Agent 看到的工具名,就是原生命令名
    description: '...',
    parameters: { args: { type: 'array', items: { type: 'string' }, required: true } },
    output: { schema, render },
    execute: async (args, exec) => run(exec, ['playwright', ...args.args]),  // 原样转发
  }))
}
  • 执行缝走 ctx.shell(与官方 bash/pwsh 工具同源),自动获得 sandbox 策略、DSH_* 环境、输出截断、超时/中止分类。
  • cordis.patch.yml + dsh.bundle.patch 让这个包成为 profile 配置树里的一层,dsh plugin add 安装后自动把它写进 dsh.profile.bundles

4. 工程结构

dsh-playwright-native/
├── package.json          # dsh.bundle.patch + exports + peerDeps
├── tsconfig.json         # src/ → lib/
├── cordis.patch.yml      # 插件配置层(注册的核心)
├── LICENSE
└── src/
    └── index.ts          # apply() + defineTool('playwright', 透传)

5. Step 1 package.json

{
  "name": "dsh-playwright-native",
  "version": "0.1.0",
  "type": "module",
  "main": "lib/index.js",
  "types": "lib/index.d.ts",
  "exports": {
    ".": { "types": "./lib/index.d.ts", "default": "./lib/index.js" },
    "./cordis.patch.yml": "./cordis.patch.yml",
    "./package.json": "./package.json"
  },
  "files": ["lib", "cordis.patch.yml", "README.md", "LICENSE"],
  "dsh": { "bundle": { "patch": "./cordis.patch.yml" } },
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "typecheck": "tsc -p tsconfig.json --noEmit"
  },
  "keywords": ["dsh-plugin", "deepseek-harness", "playwright", "e2e", "testing"],
  "license": "MIT",
  "peerDependencies": {
    "@deepseek-ai/cordis": "^4.0.1",
    "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
    "@deepseek-ai/dsh-shell": "0.1.0-rc.6",
    "@deepseek-ai/dsh-tools": "0.1.0-rc.6",
    "@deepseek-ai/schemastery": "3.18.1"
  }
}

要点:

  • dsh.bundle.patch 是注册的关键:没有它,包只是普通依赖,不会成为 profile 层。
  • exports 必须暴露 ./cordis.patch.yml./package.json,供 loader 读取。
  • DSH/Cordis 运行时声明为 peerDependencies,避免复制运行时身份。
  • keywords 里的 dsh-plugin 服务 npm 搜索;GitHub 仓库的 topic 是另一处(§11)。

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "declaration": true,
    "outDir": "lib",
    "rootDir": "src",
    "skipLibCheck": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "types": ["node"]
  },
  "include": ["src"]
}

6. Step 2 cordis.patch.yml

顶层数组;id 是配置树里稳定的行身份,name 是 Node 可解析的包名:

# dsh-playwright-native bundle layer.
- insert:
    - id: playwright-native
      name: dsh-playwright-native
      config:
        command: playwright        # 可改成 "npx --no-install playwright" 强制本地副本
        timeoutMs: 600000          # 每次调用的默认超时(毫秒)

注意:后层按 id 覆盖前层,且 config整段替换(不是深合并),覆盖时要重述所需键。


7. Step 3 src/index.ts 实现透传工具

完整实现见 src/index.ts。核心只有两点——args 原样拼到命令上ctx.shell

export const name = 'playwright-native'
export const inject = ['tools', 'shell', 'systemPrompt']

export interface Config { command: string; timeoutMs: number }
export const Config = z.object({
  command: z.string().default('playwright'),      // 默认直呼本机原生命令
  timeoutMs: z.number().default(600_000),
})

export function apply(ctx: Context, config: Config): void {
  const run = async (exec: ToolRunContext, argv: string[]) => {
    const command = argv.map(quote).join(' ')
    const spec = ctx.shell.resolve({              // 走 shell 执行器,不做 child_process 手搓
      command,
      workdir: resolveWorkdir(args.workdir, exec),  // 默认会话工作区
      timeoutMs: config.timeoutMs,
      signal: exec.signal,                          // 转发中止
    })
    const result = await ctx.shell.run(spec)
    return canonical(result)                        // { exitCode, signal, stdout, stderr, ... }
  }

  ctx.systemPrompt.section({
    name: 'tool:playwright',
    order: 106,
    text: 'The `playwright` tool runs the native Playwright CLI ... check the [exit code: N] and [sandbox: ...] markers.',
  })

  ctx.tools.register(defineTool({
    name: 'playwright',
    description:
      'Run the native Playwright CLI with the exact arguments you would type on the command line. ' +
      'Arguments are forwarded verbatim and in order — do not prepend `playwright` yourself. ...',
    parameters: {
      args: {                        // ← 唯一核心参数:原生命令参数数组
        type: 'array',
        items: { type: 'string' },
        required: true,
        description: 'Playwright CLI arguments in order, e.g. ["test", "tests/", "--reporter", "html"].',
      },
      workdir: { type: 'string', description: 'Working directory (default: session workspace).' },
      timeoutMs: { type: 'integer', description: 'Timeout override in milliseconds.' },
    },
    output: { schema: runSchema, render: (_, v) => textBlock(renderRun(v)) },
    isConcurrencySafe: () => false,  // playwright test 共享 test-results/report 目录,不并发
    execute: async (args, exec) =>
      run(exec, [...config.command.split(/\s+/).filter(Boolean), ...args.args]),  // 原样透传
    presentCall: (args) => ({
      card: 'terminal',
      title: `playwright ${(args.args ?? []).join(' ')}`,
      description: 'Run the native Playwright CLI',
    }),
  }))
}

几个"原生"的关键点:

  • 只有一个 args 数组,没有把 install/test/show-report 拆成不同工具,也没有把 CLI 选项映射成自定义 schema。
  • 非零退出 = 报告,不是报错:结果带 [exit code: N] 标记,由模型自行决定下一步。
  • command 可配置:默认 playwright(直呼本机原生命令);要强制走项目本地副本时改成 npx --no-install playwright
  • 沙箱升级playwright test 会 fork worker 子进程、playwright install 会下载浏览器,都可能被文件沙箱拒绝。完整实现里保留了与官方 shell 工具一致的 sandbox_permissions + justification 升级出口(拒绝后带 [sandbox: file access denied ...] 标记,用最窄更宽模式重试、执行前弹审批)。

8. Step 4 构建

pnpm install
pnpm build        # tsc → lib/index.js + lib/index.d.ts

lib/ 提交进 Git(本仓库即如此),Git 分发时 pnpm 不执行任何 prepare 脚本、零交互、无需 allowBuilds


9. Step 5 注册进 profile

dsh plugin 是 profile 目录里的 pnpm 转发层:首次使用时初始化 profile → 转发 pnpm add → 按安装状态把声明了 dsh.bundle 的包对账进 dsh.profile.bundles

# 本地路径(先这样验证)
dsh plugin --profile web add ./dsh-playwright-native

# GitHub(本机 git 可访问 GitHub)
dsh plugin --profile web add github:mitao-su/dsh-playwright-native

# 受限网络备选:GitHub tarball
dsh plugin --profile web add https://github.com/mitao-su/dsh-playwright-native/archive/refs/heads/main.tar.gz

安装后重启目标 profile。验证注册生效:

dsh --profile web --dump-config

应能看到这一层(本教程实测):

# == dsh-playwright-native
- id: playwright-native
  name: dsh-playwright-native
  config:
    command: playwright
    timeoutMs: 600000

实测过程:全新 profile 先被初始化为 @deepseek-ai/dsh-baseadddsh.profile.bundles 变成 ["@deepseek-ai/dsh-base", "dsh-playwright-native"]--dump-config 里出现上面这层。

配置覆盖(profile 的 cordis.patch.ymlid 整段替换):

- insert:
    - id: playwright-native
      name: dsh-playwright-native
      config:
        command: npx --no-install playwright
        timeoutMs: 600000

10. Step 6 使用

注册后,Agent 的工具列表里多了一个 playwright,直接传原生命令参数:

Agent 想做的事调用 playwrightargs
看版本["--version"]
装浏览器["install", "chromium"]
跑测试["test", "tests/login.spec.ts", "--reporter", "html"]
打开报告["show-report"]
录脚本["codegen", "https://example.com"]
截图["screenshot", "https://example.com", "shot.png"]

无 GUI 的一次真实任务验证:

dsh --profile headless "用 playwright 跑 tests/ 下的用例并打开报告"

11. Step 7 发布到 GitHub(带 dsh-plugin topic)

keywords 里的 dsh-plugin 只服务 npm 搜索;GitHub 仓库的 topic 是另一处

cd dsh-playwright-native
git init -b main && git add . && git commit -m "register the native Playwright CLI as a DSH tool"

# 1) 建公开仓库并从本地推上去(owner 省略时默认当前登录账号)
gh repo create dsh-playwright-native --public --source . --remote origin --push \
  --description "把原生 Playwright CLI 注册为 DeepSeek Harness 透传工具"

# 2) 打 topic
gh repo edit mitao-su/dsh-playwright-native --add-topic dsh-plugin

# 3) 验证
gh repo view mitao-su/dsh-playwright-native --json name,url,repositoryTopics

Git 分发说明:Git 获取的是源码,不是构建产物。本仓库走"把 lib/ 提交进 Git"的无交互安装路径(无 prepare 脚本),所以无需 allowBuilds;若你的插件要跑构建脚本,pnpm ≥ 10 会默认拦截,需在 profile 的 pnpm-workspace.yaml 显式 allowBuilds 后重跑 add


12. 常见问题

  • 沙箱拒绝playwright test fork worker / playwright install 下载浏览器可能被文件沙箱拒绝,结果带 [sandbox: file access denied ...] 标记。用 sandbox_permissions + justification 以最窄更宽模式重试同一命令(执行前弹审批,仅本次生效)。
  • 双副本:若把 command 配成 npx --no-install playwright,它只认 workdir 里的本地 @playwright/test;spec 文件与 CLI 解析到不同副本会出现 test.describe() not expected here。解决:workdir 指向装有本地 @playwright/test 的项目目录,或直接保持默认 command: playwright
  • 引号:参数按挂载 shell 方言(bash/pwsh)自动安全引用;含单引号的路径请先重命名。
  • 并发playwright test 声明 isConcurrencySafe: () => false(共享 test-results/report 目录)。

License

MIT(见 LICENSE)。