dsb 客户端:Go 版本、多目标与后端服务管理
03-1 介绍当前命令行参数、结构化输出与留档。本章介绍 Go 实现的单文件二进制 dsb 的安装、多目标与服务管理;源码在仓库 dsb/ 目录。
这一章讲三件事:为什么换、怎么装、以及新版本多出来的两块能力 —— 连别的主机/多个端口 与 自己管后端服务。
1. 为什么换成 Go
原来的 Python 客户端只有一个文件、只用标准库,本身没问题。换掉它有三个具体理由:
| 原因 | 具体表现 |
|---|---|
| 少一层运行时依赖 | 原来要先有 Python 3,再由 dsb(macOS/Linux)或 dsb.cmd(Windows)两层包装去挑解释器、转发参数与退出码;现在是一个静态二进制,装进 PATH 就能在任何目录直接敲 |
| 服务没起时不用手工处理 | 原来必须记得先跑 scripts/run/start-server.cmd;现在客户端发现连不上会自己把后端拉起来 |
| 兼顾可复核性与 Cookie 保护 | 普通结构化响应保留原字段,便于后续处理;自动留档定向遮蔽 Cookie 值,显式授权导出仍可保留原值 |
还有一条是排查上的:Go 版本每次调用都会在 stderr 说清「连的是哪台机器、用的是哪份 jar」,这在多主机、多实例的场景里是刚需。
2. 安装
cd dsb
go build -o "$(go env GOPATH)/bin/dsb.exe" . # macOS/Linux 去掉 .exe装完在任意目录验证:
dsb --version
dsb --help目录结构(源码在仓库 dsb/):
dsb/
├── main.go 入口:解析命令行 → 拼客户端 → 派发子命令 → 退出码
├── args.go 命令行解析
├── endpoint.go 目标解析(多主机/多端口、具名目标)+ ~/.dsb/config.json 读写
├── client.go HTTP 层、响应信封、留档
├── commands.go health / run / batch / state / js 等子命令
├── server.go dsb server 子命令族(后端服务管理与目标登记)
├── backend.go 仓库定位、jar 候选与查找、构建、拉起/停止后端
├── process_windows.go / process_unix.go 脱离父进程启动、结束进程树
├── input.go 参数文件、标准输入、批量命令来源
├── jsonval.go 保序 JSON 对象与可控的编解码
├── output.go 输出策略(摘要、JSON、--out、--grep)
├── selftest.go 端到端自检
└── errors.go 用法错(3)与传输错(1)3. 命令行入口与子命令
常用参数与结构化留档见 03-1。子命令清单:
| 子命令 | 作用 |
|---|---|
health / config / tasks / methods [过滤词] | 运维自省:健康检查、生效配置、活着的任务、命令清单 |
start [--browser chrome] [--headless] [--profile 名称] | 起任务,默认显示窗口;也可用 --user-data-dir / --profile-directory 显式选目录 |
profiles / profile list | 调用 list_profiles,列出服务端可发现的 profile |
profile clone --name 名称 --source-user-data-dir 路径 --source-profile-directory Default | 调用 clone_profile,离线克隆为命名 profile;见 39 |
close [--close-browser] / shutdown | 普通结束保留最后的数据页;显式关闭浏览器 / 关闭全部任务与浏览器 |
run <method> [-p k=v] [--params @文件] [--wait-until STAGE] | 发一条命令;导航等待阶段见 首个任务 |
batch [文件|-] [--async] [--wait] | 批量命令 |
job <jobId> [--wait] | 查/等异步批次 |
recipes [--run 名字] | 列配方 / 跑配方 |
state [--full] [--text-only] [--highlight] [--screenshot] | 索引正常生成;高亮与截图默认关闭,需要时分别启用 |
js <脚本|@脚本.js|-> [--var k=v] | 执行 JS |
upload / uploads [--delete 名字] | 文件送到服务端暂存区 |
last | 重放本会话最近一次的响应 |
server <动作> | 后端服务与目标管理(本章第 5、6 节) |
selftest [--browser chrome] | 端到端自检 |
dsb --help 与 dsb <子命令> --help 都有说明。退出码沿用原约定:0 成功 / 1 传输或协议错 / 2 业务失败 / 3 用法错。
单路径选择与防重复执行
--select 接受一个点分隔路径,例如 data.text。严格模式会在创建客户端、发送请求或启动服务之前,拒绝形如 data.title,data.url 的逗号分隔多路径,退出码为 3,并明确提示没有发送请求。检查规则是每个逗号分段均包含点;它不是所有 JSON 键的通用语法验证。重复提供选项时,前面的此类错误值也会被检查。
兼容性:data.a,b 仍可选择字面键 a,b;如果字面键形成看似多路径的表达式,显式使用 --select-lenient 保留原解释。此开关还会在路径缺失时回退输出完整响应,不用于真正的多字段选择。优先选择父对象(如 --select data),或用 --out 保存后本地投影。
只有语法前置拒绝才能明确断言“请求未发送”。合法路径在回执中不存在时,错误发生在响应之后,提示会提醒动作可能已完成。使用相同 session 与留档目录读取 dsb last 或原始记录,不重发动作。--out 的完整落盘优先级不变。
--id 或 DSB_TASK_ID 必须是数字;命名标签用 --session,标签不替代浏览器任务 ID。dsb methods 只列出方法,方法调用使用 dsb run METHOD。
4. 留档复核与 Cookie 保护
自动请求/响应留档会将结构化 cookies 数组及 set_cookie 参数中的 Cookie 值替换为 [REDACTED],不修改实时请求、stdout 或显式 --out 导出。任意脚本、页面文本、错误消息、HTTP 头、服务端 trace 与历史记录不在此功能覆盖范围,不能把它当成通用脱敏。requestId、jobId 等后续处理所需字段保持原样。日志不用于恢复被遮蔽的 Cookie;显式导出与终端输出需单独保护。
留档格式不变:
logs/agent/<会话>/001.req.json 请求结构(Cookie 值定向遮蔽)
logs/agent/<会话>/001.res.json 响应结构(Cookie 值定向遮蔽)
logs/agent/<会话>/steps.log 一行一次调用dsb last 不发送业务请求。不带显式 ID 时读取目录最新响应;显式 --id 按配对请求筛选该任务,无匹配报错。目录选择与完整示例见 留档与复核。
5. 连别的主机、连同一台机器上的多个实例
后端可能在别的机器上(团队共用一台跑浏览器),本机也可能同时开着好几个实例 —— CLI 托管目录默认按端口隔离:10049 使用 .dsb-backend/profile,其他端口使用 .dsb-backend/profile-<port>。显式配置目录仍需避免共享占用。两种都用同一组开关。
dsb --host 10.0.0.5 --port 10049 health # 直接给主机与端口
dsb --base-url http://10.0.0.5:10049 health # 等价,写全了更直观(IPv6 用这个)
dsb --port 10050 start --headful # 本机第二个实例地址的决定顺序:
| 优先级 | 来源 |
|---|---|
| 1 | --base-url |
| 2 | --use <名字>(具名目标) |
| 3 | --host / --port |
| 4 | 环境变量 DSB_BASE_URL / DSB_HOST / DSB_PORT |
| 5 | ~/.dsb/config.json 里的 host / port |
| 6 | 内置默认 localhost:10049 |
dsb server status 会把最终地址以及它的来源一并打出来,避免「我以为连 A,其实连的是 B」:
服务地址:http://localhost:10050(健康,本机;来源:命令行 --port)具名目标
每次都敲 IP 和端口太啰嗦,可以给「主机 + 端口」起个名字:
dsb server target add lab --host 10.0.0.5 --port 10049 --note "实验机"
dsb server target add local2 --host 127.0.0.1 --port 10050
dsb server target list # 列出全部(--json 给脚本用)
dsb server target remove lab
dsb --use lab methods # 之后就不用敲 IP 了target list 会标出每条是本机还是远端:
已登记的目标(<用户主目录>/.dsb/config.json):
lab 10.0.0.5:10049(远端) # 实验机
local2 127.0.0.1:10050(本机)
用法:dsb --use <名字> <子命令>远端目标不自动拉起
只有明确的本地形态(localhost、127.0.0.0/8、::1、0.0.0.0、本机主机名)才算本机;其余一律当远端。 远端服务必须在那台机器上自己起 —— 客户端没有理由去启动别人的进程。硬要拉起只会把「地址写错了」 变成「在本机起了个没用的服务,然后照样失败」。所以远端目标连不上时得到的是明确提示:
服务地址:http://10.0.0.5:10049(没起,远端;来源:具名目标 lab)
远端目标:进程、jar、日志都在那台机器上,这里看不到
连不上:确认那台机器上服务起了、端口开着、防火墙放行(远端不能自动拉起)6. 后端服务管理
dsb server init --repo-dir <仓库根> # 记下仓库在哪(一次性)
dsb server build # 构建当前代码的后端 jar
dsb server start # 起服务(已在跑就复用)
dsb server status # 地址、健康、pid、仓库、全部可用 jar、日志
dsb server logs --lines 80 # 看日志尾部(排查启动失败最常用)
dsb server stop # 停服务;有活动任务时拒绝执行
dsb server restart # 重启;有活动任务时拒绝执行
dsb server restart --when-idle # 等任务都结束再重启,等不到就放弃(--idle-timeout 默认 600 秒)
dsb server restart --force # 中断活动任务并重启
dsb server wipe-profile --yes # 服务停止后,删除这个端口的 profile 和登录状态start、stop、status 都认 --port,所以同一台机器上的多个实例可以逐个管理:
dsb --port 10050 server stop
dsb --port 10050 server status配置文件
~/.dsb/config.json 同时管「仓库在哪」与「默认连哪台主机」:
{
"repoDir": "<仓库根目录>",
"host": "127.0.0.1",
"port": 10049,
"targets": {
"lab": { "host": "10.0.0.5", "port": 10049, "note": "实验机" },
"local2": { "host": "127.0.0.1", "port": 10050, "repoDir": "…" }
}
}为什么要有这个文件:dsb 装在 PATH 上,离仓库十万八千里,推不出仓库在哪。装到别的机器、换了仓库目录、 多了一台跑浏览器的机器,都只改这一个文件。旧版本只有 repoDir 一个字段的配置照常能读。
仓库位置的决定顺序:--repo-dir > DSB_REPO_DIR 环境变量 > 配置文件 > 从当前目录向上探测。 仓库位置只对本机目标有意义 —— 远端主机上的 jar 在远端。
jar 从哪来
按优先级:
| 优先级 | 位置 | 说明 |
|---|---|---|
| 1 | 配置文件里的 jar | 显式指定,最优先 |
| 2 | <repo>/.dsb-backend/releases/<commit>/backend.jar | 按 commit 归档,多个时取最新的 |
| 3 | <repo>/playwright-server/target/playwright-server-*.jar | 本地 mvn package 的直接产物 |
| 4 | <repo>/dist/*-windows-x64.jar | 发行包,通常是几周前的快照 |
一个都没有时报用法错并提示跑 dsb server build。
dsb server status 会把全部候选列出来(带来源、commit / playwright 版本、时间、大小), 用 ★ 标出启动时会选的那份:
后端 jar(按优先级,★ = 启动时会用的那份):
★ …\.dsb-backend\releases\879906f4…\backend.jar(releases, 879906f4, 2026-10-07 12:32, 51.6 MB)
…\.dsb-backend\releases\87a5e4d7…\backend.jar(releases, 87a5e4d7, 2026-10-06 10:52, 51.6 MB)
…\playwright-server\target\playwright-server-1.0.0.jar(target, 2026-10-07 12:32, 51.6 MB)
…\dist\deepseek-browser-use-1.0.0-windows-x64.jar(dist, 2026-09-25 20:01, 199.5 MB)为什么 releases 排在 target 前面:前者是「按 commit 存档、可复现」的产物,后者是随便哪次本地构建 留下的(可能来自半路中断的构建,也可能改了代码没提交)。想用最新代码跑,先 dsb server build —— 它会把 当前 commit 的产物归进 releases/<commit>/,自然就成了最新的那份;target 与 dist 是兜底,平时不会用到。
构建
dsb server build等价于在仓库根执行:
mvn -B -ntp -Pproduction -pl playwright-server -am clean package -DskipTests -Ddriver.platform=<当前平台>产物拷进 .dsb-backend/releases/<当前 commit>/backend.jar。
-Ddriver.platform 不能省:Playwright 的 driver-bundle 里有 5 个平台的 node(合计约 194MB), 一个发行版只需要自己那个平台的;打包脚本与插件的 backend.ps1 都传这个属性,传了 jar 才只有 51 MB 上下。 客户端按当前系统自动映射(windows/amd64 → win32_x64、darwin/arm64 → mac11-arm64 等)。
构建完要重启才会用上:
dsb server restart启动参数
dsb server start 拉起的命令行与 plugins/deepseek-browser-use/scripts/backend.ps1 对齐:
java -Dserver.port=10049
-Djdk.net.unixdomain.tmpdir=<仓库>/logs/server/run-10049
-Dbrowser.profileDir=<仓库>/.dsb-backend/profile
-Dbrowser.chrome.cdpProfileDir=<仓库>/.dsb-backend/profile
-Dbrowser.chrome.useUserProfile=false
-jar <jar>两点值得记:
-Djdk.net.unixdomain.tmpdir必须给。 不给时 JDK 21 建Selector会去连默认位置的 unix domain socket, 在 Windows 上直接Invalid argument: connect/Unable to establish loopback connection,服务起不来。- 工作目录是仓库根,所以
data/、logs/都落在仓库里,与既有脚本一致。 - 进程脱离父进程启动(Windows 用
DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP,类 Unix 用setsid),dsb退出不会把后端带走;pid 记在logs/server/server-<端口>.pid,stdout/stderr 进同目录的.out.log/.err.log。
自动拉起
默认开:普通命令(health、run、state …)连不上本机服务时,客户端按上面的配置把后端拉起来, 然后重试一次。触发时 stderr 会打一行说明:
服务没起,正在按配置文件拉起后端(仓库:…,端口:10049)…关掉它:
dsb --no-auto-start health # 单次
DSB_AUTO_START=0 dsb health # 环境变量只在「本机 + 连不上(连接被拒)」时触发。服务超时、业务失败都不触发 —— 超时说明服务在跑,只是慢, 这时再起一个只会更糟。重试只做一次,避免「起不来 → 无限重试」。
启动失败时不会干等满超时:进程中途退出就立刻放弃,并把日志尾部打印出来。
7. 为什么自己实现 JSON 值层
dsb/jsonval.go 没有直接用 encoding/json,而是自己写了「保序对象 + 编解码」。这不是因为 Java 后端返回的 JSON 不标准 —— 服务端回的是完全标准的 JSON。原因是 encoding/json 面向 map 的设计会丢掉三样东西:
| 问题 | 后果 | 本项目的处理 |
|---|---|---|
map[string]any 按键名排序 | 回执与留档的字段顺序被重排,和「第几步开始不对」的排查对不上;日志与原始响应不再逐字一致 | 用 Obj 保留插入顺序 |
数字默认解成 float64 | jobId 是雪花号(1790232350369123456,19 位),过一遍 float64 就丢精度,回填给 get_job 直接失败 | 用 json.Number 保留字面量 |
| 缩进与分隔符不可控 | 留档文件、--compact 的单行输出都有人和脚本在依赖 | 自己控制缩进与分隔符 |
换句话说:协议是标准的,问题出在 Go 标准库的默认解码目标上。换用 json.Decoder + UseNumber 能解决 数字精度,但解决不了顺序;而顺序恰恰是排查时的第一手线索。代价是一个约 300 行的文件,换来的是留档与 服务端原文一致。
8. 自检与本地测试
对当前服务跑一遍端到端自检(用自己的任务 ID 990001,不会撞上业务任务,自检页面是本地生成的 HTML、不依赖外网):
dsb --port 10049 selftest --browser chrome它依次验证:健康检查、命令清单里新命令是否都在、start 的 engineHonored、打开自检页、wait_for_count、 get_modals/close_modal、点击降级到真实鼠标、expect 断言能报出不一致、异步批次与 get_job、 任务清单/配置/配方可读、cleanup 默认只预演。
客户端自身的测试不连服务:
cd dsb && go test ./...覆盖参数解析、JSON 保序与编码、留档编号接续、输出策略、信封级字段、jar 候选优先级、仓库定位、 目标解析优先级,以及「远端目标不自动拉起」这条约束。
9. 一次完整的多实例演练
# 1. 记下仓库位置(一次性)
dsb server init --repo-dir "<仓库根目录>"
# 2. 起两个服务;若要同时启动浏览器,须先为它们配置互不重叠的用户数据目录
dsb --port 10049 server start
dsb --port 10050 server start
# 3. 给第二个实例起个名字,之后用名字连
dsb server target add local2 --host 127.0.0.1 --port 10050
dsb --use local2 health
dsb --use local2 --id 2002 start --browser chrome --headful
# 4. 登记一台远端机器(需要那台机器上自己把服务起好)
dsb server target add lab --host 10.0.0.5 --port 10049 --note "实验机"
dsb --use lab server status # 会明确说「远端」,连不上也不会在本机乱起服务
# 5. 收尾
dsb --use local2 --id 2002 close
dsb --port 10050 server stop
dsb --port 10049 server stop