Skip to content

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. 安装 ​

bash
cd dsb
go build -o "$(go env GOPATH)/bin/dsb.exe" .     # macOS/Linux 去掉 .exe

装完在任意目录验证:

bash
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。

自动请求/响应留档会将结构化 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>。显式配置目录仍需避免共享占用。两种都用同一组开关。

shell
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 和端口太啰嗦,可以给「主机 + 端口」起个名字:

shell
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. 后端服务管理 ​

shell
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,所以同一台机器上的多个实例可以逐个管理:

shell
dsb --port 10050 server stop
dsb --port 10050 server status

配置文件 ​

~/.dsb/config.json 同时管「仓库在哪」与「默认连哪台主机」:

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 是兜底,平时不会用到。

构建 ​

shell
dsb server build

等价于在仓库根执行:

shell
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 等)。

构建完要重启才会用上:

shell
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)…

关掉它:

shell
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 保留插入顺序
数字默认解成 float64jobId 是雪花号(1790232350369123456,19 位),过一遍 float64 就丢精度,回填给 get_job 直接失败用 json.Number 保留字面量
缩进与分隔符不可控留档文件、--compact 的单行输出都有人和脚本在依赖自己控制缩进与分隔符

换句话说:协议是标准的,问题出在 Go 标准库的默认解码目标上。换用 json.Decoder + UseNumber 能解决 数字精度,但解决不了顺序;而顺序恰恰是排查时的第一手线索。代价是一个约 300 行的文件,换来的是留档与 服务端原文一致。

8. 自检与本地测试 ​

对当前服务跑一遍端到端自检(用自己的任务 ID 990001,不会撞上业务任务,自检页面是本地生成的 HTML、不依赖外网):

shell
dsb --port 10049 selftest --browser chrome

它依次验证:健康检查、命令清单里新命令是否都在、start 的 engineHonored、打开自检页、wait_for_count、 get_modals/close_modal、点击降级到真实鼠标、expect 断言能报出不一致、异步批次与 get_job、 任务清单/配置/配方可读、cleanup 默认只预演。

客户端自身的测试不连服务:

bash
cd dsb && go test ./...

覆盖参数解析、JSON 保序与编码、留档编号接续、输出策略、信封级字段、jar 候选优先级、仓库定位、 目标解析优先级,以及「远端目标不自动拉起」这条约束。

9. 一次完整的多实例演练 ​

bash
# 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

下一步 ​

  • 想了解协议本身(一个端点、JSON 进 JSON 出),看 统一命令接口。
  • 想了解当前 dsb 客户端的参数与结构化留档,看 03-1。
  • 想看命令清单与每个命令的参数,看 命令清单。