dsb 客户端:Go 版本、多目标与后端服务管理
03-1 介绍的是 Python 客户端 client/dsb.py 与 PowerShell 客户端 browse.ps1。当前工程已经把命令行客户端换成 Go 实现的单文件二进制 dsb(源码在仓库 dsb/ 目录),Python 客户端与它的两个包装脚本(client/dsb、client/dsb.cmd)已经删除。
这一章讲三件事:为什么换、怎么装、以及新版本多出来的两块能力 —— 连别的主机/多个端口 与 自己管后端服务。
1. 为什么换成 Go
原来的 Python 客户端只有一个文件、只用标准库,本身没问题。换掉它有三个具体理由:
| 原因 | 具体表现 |
|---|---|
| 少一层运行时依赖 | 原来要先有 Python 3,再由 dsb(macOS/Linux)或 dsb.cmd(Windows)两层包装去挑解释器、转发参数与退出码;现在是一个静态二进制,装进 PATH 就能在任何目录直接敲 |
| 服务没起时不用手工处理 | 原来必须记得先跑 scripts/run/start-server.cmd;现在客户端发现连不上会自己把后端拉起来 |
| 去掉脱敏这一层 | 原来的默认脱敏把手机号、证件号、长号码都打码,排查时看到的不是原文;现在留档与输出都是原样 |
还有一条是排查上的: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. 命令行入口与子命令
与 Python 版本参数完全一致的部分不再重复,见 03-1。子命令清单:
| 子命令 | 作用 |
|---|---|
health / config / tasks / methods [过滤词] | 运维自省:健康检查、生效配置、活着的任务、命令清单 |
start [--browser chrome] [--headful] | 起任务 |
close / shutdown | 关掉本任务 / 关掉所有任务与共享浏览器 |
run <method> [-p k=v] [--params @文件] | 发一条命令 |
batch [文件|-] [--async] [--wait] | 批量命令 |
job <jobId> [--wait] | 查/等异步批次 |
recipes [--run 名字] | 列配方 / 跑配方 |
state [--full] [--text-only] | 页面状态摘要 |
js <脚本|@脚本.js|-> [--var k=v] | 执行 JS |
upload / uploads [--delete 名字] | 文件送到服务端暂存区 |
last | 重放本会话最近一次的响应 |
server <动作> | 后端服务与目标管理(本章第 5、6 节) |
selftest [--browser chrome] | 端到端自检 |
dsb --help 与 dsb <子命令> --help 都有说明。退出码沿用原约定:0 成功 / 1 传输或协议错 / 2 业务失败 / 3 用法错。
4. 不做脱敏
留档与终端输出都是原文,手机号、证件号、邮箱、长号码一律原样。原来默认脱敏的一个附带理由是「要回填的凭据不能被掩掉」,现在这层顾虑直接消失了:requestId、jobId 本来就该是原文。
留档格式不变:
logs/agent/<会话>/001.req.json 发出去的完整请求
logs/agent/<会话>/001.res.json 收到的完整响应
logs/agent/<会话>/steps.log 一行一次调用
dsb last 仍然重放最近一份响应。
5. 连别的主机、连同一台机器上的多个实例
后端可能在别的机器上(团队共用一台跑浏览器),本机也可能同时开着好几个实例 —— 不同端口意味着不同 profile、不同登录态。两种都用同一组开关。
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 会标出每条是本机还是远端:
已登记的目标(C:\Users\Administrator\.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 # 重启
start、stop、status 都认 --port,所以同一台机器上的多个实例可以逐个管理:
dsb --port 10050 server stop
dsb --port 10050 server status
配置文件
~/.dsb/config.json 同时管「仓库在哪」与「默认连哪台主机」:
{
"repoDir": "D:\\code\\project\\project-browser-use\\deepseek-browser-use",
"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 D:\code\project\project-browser-use\deepseek-browser-use
# 2. 本机起两个实例:10049 用默认 profile,10050 也用同一份(端口不同即可共存)
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
