Bundle
dsh-playwright-native
DeepSeek Harness host plugin that registers the native Playwright CLI as a single passthrough `playwright` tool (args forwarded verbatim).
- Source
- mitao-su
- stars
- 1 stars
- License
- MIT
- Updated
- Updated 5 days ago
Readme
# 把 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`),可直接安装:
```sh
dsh plugin --profile web add github:mitao-su/dsh-playwright-native
```
---
## 目录
1. [它和"封装"的区别(先看清要什么)](#1-它和封装的区别先看清要什么)
2. [前置要求](#2-前置要求)
3. [原理:工具注册 = `ctx.tools.register`](#3-原理工具注册--ctxtoolsregister)
4. [工程结构](#4-工程结构)
5. [Step 1 `package.json`](#5-step-1-packagejson)
6. [Step 2 `cordis.patch.yml`](#6-step-2-cordispatchyml)
7. [Step 3 `src/index.ts` 实现透传工具](#7-step-3-srcindexts-实现透传工具)
8. [Step 4 构建](#8-step-4-构建)
9. [Step 5 注册进 profile](#9-step-5-注册进-profile)
10. [Step 6 使用](#10-step-6-使用)
11. [Step 7 发布到 GitHub(带 `dsh-plugin` topic)](#11-step-7-发布到-github带-dsh-plugin-topic)
12. [常见问题](#12-常见问题)
---
## 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) |
验证环境:
```sh
playwright --version # 例如 Version 1.62.1
dsh --version # 例如 0.1.0-rc.6
```
---
## 3. 原理:工具注册 = `ctx.tools.register`
DSH 的模型可见工具全部来自插件注册(或 MCP server)。核心契约:
```ts
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`
```jsonc
{
"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`:
```jsonc
{
"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 可解析的包名:
```yaml
# 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`](src/index.ts)。核心只有两点——**把 `args` 原样拼到命令上**,**用 `ctx.shell` 跑**:
```ts
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 构建
```sh
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`**。
```sh
# 本地路径(先这样验证)
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**。验证注册生效:
```sh
dsh --profile web --dump-config
```
应能看到这一层(本教程实测):
```yaml
# == 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` 整段替换):
```yaml
- 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 的一次真实任务验证:
```sh
dsh --profile headless "用 playwright 跑 tests/ 下的用例并打开报告"
```
---
## 11. Step 7 发布到 GitHub(带 `dsh-plugin` topic)
`keywords` 里的 `dsh-plugin` 只服务 npm 搜索;**GitHub 仓库的 topic 是另一处**。
```sh
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](LICENSE))。
Install
dsh plugin --profile web add github:mitao-su/dsh-playwright-native
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-playwright-native from the hub
- This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.