mitao-su
dsh-playwright-native
把原生 Playwright CLI 注册为 DeepSeek Harness 透传工具(dsh-plugin)
- Stars
- 1
- Language
- TypeScript
- Created
- Aug 14, 2026
- Updated
- Aug 14, 2026
Introduction
把 Playwright 原生 CLI 注册成 DeepSeek Harness 工具
目标:让 Agent 直接调用本机安装的原生
playwright命令——playwright test、playwright install、playwright show-report、playwright --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
目录
- 它和"封装"的区别(先看清要什么)
- 前置要求
- 原理:工具注册 =
ctx.tools.register - 工程结构
- Step 1
package.json - Step 2
cordis.patch.yml - Step 3
src/index.ts实现透传工具 - Step 4 构建
- Step 5 注册进 profile
- Step 6 使用
- Step 7 发布到 GitHub(带
dsh-plugintopic) - 常见问题
1. 它和"封装"的区别(先看清要什么)
| 语义封装(不要) | 原生注册(本仓库) | |
|---|---|---|
| 工具名 | playwright_version / playwright_test / … | playwright |
| 参数 | 每个子命令一套自订字段 | 一个 args: string[],原样透传 |
| 行为 | 把 CLI 选项映射成自定义 schema | Agent 敲什么就 playwright 什么 |
| Agent 视角 | 调一个"别人打包过的工具" | 调原生命令 |
一句话:CLI 是能力,ctx.shell 是执行缝,defineTool + cordis.patch.yml 是注册。 我们要的是最后两样,不重写第一样。
2. 前置要求
| 项 | 要求 |
|---|---|
| Playwright | 本机已安装 playwright 命令(playwright --version 可跑)。默认用它;也可在配置里改成 npx --no-install playwright |
| DSH | dsh 可用(本教程验证于 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-base,add后dsh.profile.bundles变成["@deepseek-ai/dsh-base", "dsh-playwright-native"],--dump-config里出现上面这层。
配置覆盖(profile 的 cordis.patch.yml 按 id 整段替换):
- insert:
- id: playwright-native
name: dsh-playwright-native
config:
command: npx --no-install playwright
timeoutMs: 600000
10. Step 6 使用
注册后,Agent 的工具列表里多了一个 playwright,直接传原生命令参数:
| Agent 想做的事 | 调用 playwright 的 args |
|---|---|
| 看版本 | ["--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 testfork 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)。