dsh-web-search-ddg
aooyoo
Zero-token DuckDuckGo search provider for the DeepSeek Harness (DSH) web seam — local headless browser, no API key, no m…
PROJECT TOPICS
PROJECT README
目标:让 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.registerpackage.jsoncordis.patch.ymlsrc/index.ts 实现透传工具dsh-plugin topic)| 语义封装(不要) | 原生注册(本仓库) | |
|---|---|---|
| 工具名 | playwright_version / playwright_test / … |
playwright |
| 参数 | 每个子命令一套自订字段 | 一个 args: string[],原样透传 |
| 行为 | 把 CLI 选项映射成自定义 schema | Agent 敲什么就 playwright 什么 |
| Agent 视角 | 调一个"别人打包过的工具" | 调原生命令 |
一句话:CLI 是能力,ctx.shell 是执行缝,defineTool + cordis.patch.yml 是注册。 我们要的是最后两样,不重写第一样。
| 项 | 要求 |
|---|---|
| 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
ctx.tools.registerDSH 的模型可见工具全部来自插件注册(或 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。dsh-playwright-native/
├── package.json # dsh.bundle.patch + exports + peerDeps
├── tsconfig.json # src/ → lib/
├── cordis.patch.yml # 插件配置层(注册的核心)
├── LICENSE
└── src/
└── index.ts # apply() + defineTool('playwright', 透传)
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 读取。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"]
}
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 是整段替换(不是深合并),覆盖时要重述所需键。
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 ...] 标记,用最窄更宽模式重试、执行前弹审批)。pnpm install
pnpm build # tsc → lib/index.js + lib/index.d.ts
把 lib/ 提交进 Git(本仓库即如此),Git 分发时 pnpm 不执行任何 prepare 脚本、零交互、无需 allowBuilds。
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
注册后,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/ 下的用例并打开报告"
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。
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。playwright test 声明 isConcurrencySafe: () => false(共享 test-results/report 目录)。MIT(见 LICENSE)。
CLASSIFICATION EVIDENCE
系统优先读取 GitHub Topics,再与站内分类词典和词根规则比对。