关于 windows-host-browser
windows-host-browser 是操作 Iliyas 的 Windows PC 上持久运行的 Chrome 的技能,供需要访问该浏览器会话的 Agent 使用。它通过 CDP 和隧道连接,并规定标签页、代理设置及账户操作的谨慎边界。
主要功能
- 01通过 connect.sh 建立 CDP 连接
- 02检查并复用现有标签页
- 03使用 CDP 执行脚本和监视网络
- 04按 Chrome 配置文件管理代理
- 05规定可见操作与账户变更的审批要求
适用场景
- 01访问仅在已登录浏览器中可用的页面
- 02排查无头浏览器无法处理的挑战
- 03检查页面脚本执行与网络请求
- 04为指定配置文件设置并恢复代理
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ iliyasone/ skills --skill windows-host-browser安装后在 Claude Code 中输入/windows-host-browser确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
cdp.pyconnect.shmk-shortcut.ps1详细介绍
Windows 主机浏览器
目标是 Iliyas 的 Windows PC 上长期运行的 Chrome,通过
--remote-debugging-port=<PORT> --user-data-dir="C:\chrome-debug" 启动。它不是
无头沙盒,而是 Iliyas 也在使用的真实浏览器,运行着他的登录会话(Vercel、Google Cloud 等),并显示在他的实体屏幕上。
以下两点决定了后续所有做法:
- 能力 — 无需询问凭据,就能访问他登录后才能访问的内容;还可以像他看到的那样调试页面:查看已打开的标签页、在其中运行 JS、监控网络流量、通过指定代理路由。
- 克制 — 他看得到你打开的每个窗口,也会看到你更改的每项设置。打开可见内容时要告诉他;除非他希望保留,否则关闭你打开的标签页;用完后撤销对整个浏览器生效的更改(例如代理设置)。如果通过已认证的 CLI 或无头浏览器也能获取数据,优先使用这些方式。
无需询问即可使用的情况
Iliyas 宁愿你使用此浏览器,也不愿你停下来等他。在成本更低的方式失败时,可自行使用:
- 无头浏览器被 Cloudflare 验证或验证码拦截;
- 页面需要他的某个登录账号,且没有 CLI 或令牌可用;
- 某些操作只会在他的浏览器或会话中发生,无头浏览器无法复现。
未经询问时,将页面作为后台标签页打开,以免影响他的当前标签页:在浏览器 WebSocket 上调用带有 background: true 的 Target.createTarget(GET /json/version → webSocketDebuggerUrl;连接时不要发送 Origin 标头,如 cdp.py 所做),而不是 PUT /json/new。只有需要他操作时(例如处理验证码),才通过 Target.activateTarget 将页面切到前台。如果 Chrome 没有打开任何窗口,仍会出现一个新窗口;要告知他。标签页会在默认配置文件中打开:如果所需登录账号位于其他配置文件中,请询问他。你可以在自己打开的标签页中,以正常速度读取和浏览任务所需的少量页面(你接管他一个空白标签页也算你自己的;参见“重用现有窗口”,你可以在其中浏览并关闭该标签页)。在聊天中说明你打开了哪个页面,以及原因。Cloudflare 验证通常会在此浏览器中自动通过;可以勾选 Turnstile 复选框;遇到其他验证码时,请他在打开的标签页中完成验证。
凡是会更改他任何账号的状态或产生费用的操作,都要事先询问:提交表单、发帖、购买、更改账号设置、点击确认、接受、授权或登出链接、OAuth 同意,以及通过页面 JS 发送任何非 GET 请求。复制浏览器中的 Cookie 或令牌,以及将页面显示的凭据(API 密钥、令牌、密码、环境变量值)粘贴到任何地方(包括聊天)之前,也要先询问。其他私人内容可以发到与他的聊天中,但不要放进 issue、PR 或与其他人的聊天。通过他的浏览器循环执行、轮询或批量获取数据,必须先取得他的同意:这些操作会使用他的家庭 IP 和账号。更改任何配置文件的代理设置之前也要先询问。即使他要求你使用浏览器,这些规则仍然适用,除非他的请求明确指定了该操作。
如果你是在未经询问的情况下操作,而 connect.sh 失败,请停止并告知他。下面的修复操作(结束 Chrome、重写 chromedebug 任务、运行 net stop winnat)会关闭他的窗口和其他 Agent 的标签页;必须先取得他的同意。
其他 Agent 可能同时在使用浏览器:请在自己的标签页中操作,绝不要关闭或浏览不属于你的标签页(见下方空白标签页规则)。
第 0 步 — 连接:运行 connect.sh,使用固定端点
在 dev-remote 上,端点是固定的:http://127.0.0.1:18800,无论 Chrome 在 Windows 端使用哪个端口。运行一条命令即可完成所有设置:
~/.agents/skills/windows-host-browser/connect.sh
# on success prints: CDP_HTTP=http://127.0.0.1:18800
export CDP_HTTP="http://127.0.0.1:18800"
该脚本具有幂等性,所有服务都已运行时,执行开销很小。如果服务未运行,脚本会从 chromedebug 计划任务中重新发现当前的 Windows 端口(这是事实来源——端口会变化,参见“端口问题”;绝不要将其写死),如果 Chrome 进程已停止,则通过计划任务重新启动 Chrome(无窗口——参见“启动”),并将两条 SSH 跳转都重新连接起来(dev-remote → wsl → windows),使用本地端口 18800。
dev-remote 上的 systemd 计时器每 2 分钟重新运行一次脚本,因此端点通常已在运行:使用 systemctl status cdp-tunnel.timer 查看状态,使用 journalctl -u cdp-tunnel.service 查看日志。
如果 connect.sh 输出 FAIL,其消息会指出发生故障的层级;以下各节介绍手动排查流程。截至 2026-09-01,Windows 端口为 9555(9222、9223、9250、9333 和 9444 均先后被 WinNAT 阻止)。
手动流程(connect.sh 自动执行的步骤)
调试端口仅在 Windows 主机自身的环回地址上。家用机器(WSL 机器 iliyasone)和 dev-remote 都在 Iliyas 的 Tailscale tailnet 上,这是两者之间稳定的连接路径。要访问该端口,需要经过两跳隧道——WSL 机器无法直接访问 Windows 环回地址,因此也必须连接到主机。先查找端口:
PORT=$(ssh wsl 'ssh windows "schtasks /query /tn chromedebug /xml"' \
| grep -aoE 'remote-debugging-port=[0-9]+' | grep -oE '[0-9]+')
-
在 WSL 机器
iliyasone上:将主机端口转发到 WSL 的环回地址:pgrep -f "ssh .*-L $PORT:127.0.0.1:$PORT windows" >/dev/null \ || ssh -f -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \ -L "$PORT:127.0.0.1:$PORT" windows(也可以使用
ssh windows "curl 127.0.0.1:$PORT/json/version",无需建立隧道,适用于临时检查。) -
在
dev-remote上:通过 tailnet(100.93.231.101)使用ssh wsl连接到 WSL shell。在上面 WSL→主机的转发基础上,再建立第二条转发:pgrep -f "ssh -N .*-L $PORT:127.0.0.1:$PORT wsl" >/dev/null \ || setsid ssh -N -o ExitOnForwardFailure=yes -o ServerAliveInterval=30 \ -L "$PORT:127.0.0.1:$PORT" wsl >/dev/null 2>&1 < /dev/null &
然后运行检查:
curl -s --max-time 4 "http://127.0.0.1:$PORT/json/version" || echo NO_CDP
如果返回 Chrome 版本 → 前往第 1 步。若返回 NO_CDP → 按排查成本从低到高检查原因:隧道未运行(重新执行上述转发);即使 Chrome 正在运行,端口也可能已被 WinNAT 占用(参见“端口问题”);家用 PC / WSL 机器处于离线状态(运行 tailscale status——如果 iliyasone 离线,说明机器已关闭,无法从这里修复);或者调试用 Chrome 未运行(参见“启动”)。不要捏造路径。
第 1 步 — 连接
有一点需要注意:这一规则适用于所有 WebSocket 连接,而不仅仅是辅助工具:Chrome 启动时未设置 --remote-allow-origins,因此打开 CDP WebSocket 时不要发送 Origin 标头,否则连接会被拒绝。
此后即可通过 $CDP_HTTP 直接使用 CDP:
GET /json/list— 获取标签页及其 WebSocket URL;使用任意 CDP 客户端连接,即可执行 JS、截取屏幕截图或监视网络事件(Network.enable+Network.requestWillBeSent/responseReceived)。PUT /json/new?url=…— 打开一个页面(该页面会作为可见标签页显示在 Iliyas 的屏幕上——执行前请说明)。GET /json/version→webSocketDebuggerUrl— 获取浏览器 WebSocket,用于调用Target.*等方法,例如createTarget。
复用现有窗口——不要不断新建窗口
每个窗口和标签页都显示在 Iliyas 的屏幕上,他要求 Agent 不要再不断新建窗口。打开任何内容之前,先执行 GET /json/list 并按以下顺序复用现有内容:
- 存在你自己打开的空白标签页,或 Iliyas 留下的空白标签页
(
about:blank或chrome://newtab/):将其导航到目标页面(连接到它的 websocket,执行Page.navigate),而不是新建 target。新打开的空白标签页 可能属于另一个即将导航的 Agent;如果无法确定,就视为不属于你,然后遵循规则 2。 - 打开的标签页是你自己的,或明显处于空闲状态(不是 Iliyas 正在
积极使用的内容):在同一个窗口中将你的页面作为标签页打开——
PUT /json/new会执行此操作,它会以现有窗口为目标。 - 仅在你有意需要时才新建窗口——例如,现有窗口里满是 Iliyas 正在处理的无关任务,或者你需要隔离(不同的窗口尺寸、需要他单独观察的流程)。 说明原因。
清理时也一样:对你打开的标签页执行 /json/close/<id>;绝不要关闭
你未打开的标签页。
代理控制——cdp.py(按配置文件区分)
除非 Iliyas 已要求更改代理,否则在任何配置文件上执行 set 或 direct 前都要先征求他的同意。将你自己获准设置的代理恢复为 direct 不需要再次征求同意;如果发现代理已设置,则由他决定如何处理。
Chrome 安装了 Proxy Switcher 扩展程序
(iejkjpdckomcjdhmkemlfdapjodcpgih),代理设置由它管理。代理按
Chrome 配置文件区分,而不是按窗口区分:浏览器可以同时运行多个配置文件(使用不同的窗口、Cookie 和扩展程序副本),每个配置文件都可以使用不同的代理。cdp.py(与此文件位于同一目录)负责驱动该扩展程序。它默认使用固定端点 http://127.0.0.1:18800(先运行 connect.sh);只有在其他位置运行时,$CDP_HTTP 才会覆盖此设置。
python3 cdp.py profiles # list running profiles by their open tabs
python3 cdp.py get -p 2 # proxy setting of profile 2 from that list
python3 cdp.py set 82.38.65.142 41196 http proxyuser 'PASSWORD' -p proton
python3 cdp.py egress -p proton # prove the exit IP/country through it
python3 cdp.py direct -p proton # revert that profile to direct
--profile / -p 接受 profiles 中的索引,或该配置文件中打开的标签页 URL/标题的子串。只有一个配置文件在运行时可以省略;有多个配置文件时则必须指定——该工具不会猜测,因为在错误的配置文件上设置代理会改变 Iliyas 浏览网页时使用的代理。
该工具隐藏了两个容易踩坑的地方:此扩展程序使用 MV3,因此其按配置文件区分的服务工作线程会休眠并从 target 列表中消失——而且,无法通过 Target.createTarget 唤醒某个特定配置文件中的休眠工作线程(CDP 会拒绝真实配置文件的 browserContextIds)。cdp.py 会通过该配置文件自己的页面执行 window.open 打开一个临时标签页,将其导航到扩展程序弹出窗口,然后关闭它,以此唤醒工作线程。
set 会为该配置文件调用 chrome.proxy.settings.set(与扩展程序弹出窗口执行的操作完全相同);如果提供了用户名,还会将 auth-username / auth-password 写入扩展程序的存储,并重新启用其 onAuthRequired 处理程序,以免经过身份验证的代理弹出对话框。
在 Iliyas 自己用于浏览网页的配置文件上操作完成后,始终执行 direct——遗留在该配置文件中的代理会改变他自己的浏览体验。专用代理配置文件可以保留其代理。
将扩展程序添加到新配置文件需要手动操作:在该配置文件的窗口中打开
https://chromewebstore.google.com/detail/iejkjpdckomcjdhmkemlfdapjodcpgih
并让 Iliyas 点击 “Add to Chrome”——安装确认是无法通过 CDP 访问的原生 UI。
代理来源
reverse-api 项目使用一张 proxy 表(Postgres,Heroku 应用
pinc000):包含 scheme, server, port, username, password, country_code 列。
这是使用用户名和密码进行身份验证的 HTTP 代理,通过 country_code(ISO-3166
alpha-2)选择。请查询该表,而不要将凭据硬编码:
# on dev-remote, heroku CLI is authed to app pinc000
heroku pg:psql -a pinc000 -c \
"select scheme,server,port,username,password,country_code from proxy where country_code='nl';"
代理不会轮换——每个账号使用的代理是固定的——因此浏览器使用时,选择目标国家的任意一行即可。
启动调试版 Chrome
如果步骤 0 显示 NO_CDP,但 ssh wsl 可以正常使用,请通过计划任务启动 Chrome——切勿通过普通 SSH 启动,因为这样会让 Chrome 进入不可见的会话 0,并在尚未绑定端口时退出:
# from the WSL box:
ssh windows 'schtasks /run /tn chromedebug'
# from dev-remote (hop through WSL):
ssh wsl 'ssh windows "schtasks /run /tn chromedebug"'
# then poll: curl -s "$CDP_HTTP/json/version"
该任务使用 --no-startup-window 启动 Chrome,因此启动过程不可见:Chrome 会在后台运行,CDP 已启动,但不会显示窗口,直到有东西打开一个页面(此时 PUT /json/new 会在他的屏幕上打开一个窗口——记得告诉他)。这样可以避免每次登录后 2 分钟计时器触发时,Chrome 都跳到 Iliyas 面前;重写任务操作时务必保留此标志。“Agent Chrome”快捷方式会在启动任务后自行打开窗口。该任务以 LogonType=InteractiveToken 运行(“常规”选项卡:“仅当用户登录时运行”),因此会在他的可见会话中启动,且不会存储密码。
如果重新启动成功,但几分钟后 Chrome 又退出了,不要反复重启——请参阅“退出问题”。
Iliyas 可以自行打开——桌面快捷方式
Windows 桌面上的 “Agent Chrome” 可手动打开此浏览器,例如登录某项服务,以便 Agent 随后使用该会话。它会运行 C:\chrome-debug\open-debug-chrome.ps1:如果调试版 Chrome 已在运行,就在其中打开一个新窗口;否则启动 chromedebug 任务,等待调试端口响应,然后打开窗口(如果任务实例仍在运行,普通的 schtasks /run 会被静默忽略,因为该任务使用 MultipleInstances IgnoreNew——因此脚本会先检查)。所以,他需要登录某处时,请让他使用该快捷方式,而不要替他打开标签页。
如果快捷方式或启动脚本缺失,请在主机上通过 -EncodedCommand 传输方式运行 mk-shortcut.ps1(与此文件位于同一目录)以重新创建二者。该脚本会在 C:\chrome-debug 中创建 .lnk 文件,然后将其移到桌面——由于桌面文件夹的名称是西里尔字母(Рабочий стол),WScript.Shell 无法直接将文件保存到该文件夹。
端口问题——WinNAT 可能会占用调试端口
**症状:**调试版 Chrome 正在运行且浏览正常,但
$CDP_HTTP/json/version 连接失败,也没有 DevTools 服务端。
使用 --enable-logging --v=1 启动后,C:\chrome-debug\chrome_debug.log
会显示 bind() ... Only one usage of each socket address ... (0x2740),随后显示
Cannot start http server for devtools。
**原因:**Hyper-V/WSL2 WinNAT/HNS 会预留一些 TCP 端口范围;WSL 或主机重启后,某个范围可能包含调试端口。即使 netstat 显示该端口无人占用,且该端口也不在
netsh int ipv4 show excludedportrange 的结果中,它仍会变得无法绑定。“昨天还能用,今天就坏了”=预留范围移到了该端口上。自定义 user-data-dir、策略和会话 0 都是干扰因素——请根据上述日志行确认。
恢复方法(无需管理员权限,也无需重启服务):
-
找一个空闲端口——尝试绑定候选端口,并选取第一个绑定成功的端口:
foreach ($p in 9223,9250,9333,9555,18222) { try { $l=[System.Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback,$p) $l.Start(); "OK $p"; $l.Stop() } catch { "FAIL $p" } } -
将任务改为使用该端口。使用 PowerShell
Set-ScheduledTask,不要 使用schtasks /change——即使任务没有存储密码,后者也会错误地提示输入 Windows 密码:$a = New-ScheduledTaskAction -Execute 'C:\Program Files\Google\Chrome\Application\chrome.exe' ` -Argument '--remote-debugging-port=<PORT> --user-data-dir=C:\chrome-debug --no-startup-window' Set-ScheduledTask -TaskName 'chromedebug' -Action $a通过
ssh wsl 'ssh windows "..."'传递引号很不可靠;请使用-EncodedCommand <base64-UTF16LE>运行 PowerShell,以避免此问题。 -
结束旧实例,重新启动并验证:
Get-CimInstance Win32_Process -Filter "name='chrome.exe'" | ? { $_.CommandLine -like '*chrome-debug*' } | % { Stop-Process $_.ProcessId -Force } schtasks /run /tn chromedebug (Invoke-WebRequest -UseBasicParsing http://127.0.0.1:<PORT>/json/version).Content -
使用新的
$PORT重新运行步骤 0 中的隧道命令。
**持久解决方法(可选,需要管理员权限):**预留该端口,避免 WinNAT 占用——net stop winnat; netsh int ipv4 add excludedportrange protocol=tcp startport=<PORT> numberofports=1 store=persistent; net start winnat。停止
winnat 会短暂中断 WSL2/Hyper-V NAT,因此经过它的 SSH 链路可能会短暂断开——只有在可以接受短暂中断时才执行。
Death 陷阱 — 电池状态切换时任务计划程序会终止 Chrome
**症状:**与端口陷阱相反,调试版 Chrome 进程会在每次启动几分钟后退出(命令行中没有 chrome-debug 的 chrome.exe),而调试端口空闲且可绑定。运行 schtasks /run /tn chromedebug 会恢复 CDP,但大约 2 分钟后又会退出。
**原因:**这台电脑是笔记本电脑,而使用默认设置创建的任务会设置
StopIfGoingOnBatteries=true / DisallowStartIfOnBatteries=true。即使插着充电器,Windows 的交流电/电池状态也可能每隔几分钟切换一次(电池养护导致的充电限制、接头松动),每次切换时,任务计划程序都会终止任务中的 Chrome。证据是任务计划程序操作日志事件 327:Task Scheduler stopped instance ... of task "\chromedebug" because the computer is switching to battery power. chromedebug 任务已经应用了固定设置,但如果通过 New-ScheduledTask/Register-ScheduledTask 重新创建任务,默认设置会悄悄重新引入这些问题,因此每次此症状再次出现时,都要重新检查设置。
诊断:
-
schtasks /query /tn chromedebug /xml— 在<Settings>中,StopIfGoingOnBatteries/DisallowStartIfOnBatteries必须为false,ExecutionTimeLimit必须为PT0S(无限制)。如果不是,问题就在这里——直接进行修复。 -
任务计划程序操作日志默认处于禁用状态;启用它并捕捉下次退出时的事件:
wevtutil sl Microsoft-Windows-TaskScheduler/Operational /e:true # after a death: Get-WinEvent -LogName 'Microsoft-Windows-TaskScheduler/Operational' -MaxEvents 100 | ? { $_.Message -match 'chromedebug' } | select TimeCreated, Id, Message事件 327 中出现“switching to battery power”表示这是此陷阱。如果完全没有 32x 停止事件,则说明 Chrome 自身崩溃了——在任务操作中添加
--enable-logging --v=1,重现问题,读取C:\chrome-debug\chrome_debug.log,然后撤销这些标志。 -
检查电池状态:运行
Get-CimInstance Win32_Battery(有结果表示这是笔记本电脑),并运行[System.Windows.Forms.SystemInformation]::PowerStatus(先运行Add-Type -AssemblyName System.Windows.Forms)查看当前交流电状态。
持久修复 — 替换任务的设置,同时保留操作和
InteractiveToken 主体(Set-ScheduledTask 不会触碰你未传入的部分)。将以下内容放入 .ps1,并通过 -EncodedCommand 传输,而不是在两次 SSH 跳转之间内联加引号:
$s = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
-ExecutionTimeLimit ([TimeSpan]::Zero) -MultipleInstances IgnoreNew
Set-ScheduledTask -TaskName 'chromedebug' -Settings $s
# from dev-remote: encode UTF-16LE+base64, run via the double hop
B64=$(iconv -f UTF-8 -t UTF-16LE /tmp/fix.ps1 | base64 -w0)
ssh wsl "ssh windows 'powershell -NoProfile -EncodedCommand $B64'"
(-ExecutionTimeLimit 设为零很重要:否则 New-ScheduledTaskSettingsSet 默认会设为 72 h,导致 Chrome 在三天后退出。)然后重新启动,并进行真正的测试——确认 4–5 分钟后进程数仍不为零,确保已超过状态切换间隔,而不只是确认 CDP 曾成功响应一次。
访问连接方式(及修复方法)
两台机器都是 Iliyas 的 Tailscale tailnet 上的节点:
iliyasone=100.93.231.101— 家用 PC 上的 WSL 机器。Tailscale 运行在 WSL 内部,而不是 Windows 上;Windows 主机通过 WSL 使用ssh windows访问(→127.0.0.1:2222,密钥~/.ssh/id_win)。Windows 环回地址不会共享到 WSL,因此要从 WSL 访问调试端口,需要使用步骤 0 中的ssh -L ... windows隧道。dev-remote=100.105.176.17。它的~/.ssh/config中有Host wsl→100.93.231.101,并且它的 root 密钥已加入 WSL 机器的authorized_keys, 因此可以通过 tailnet 使用ssh wsl。
tailnet 地址不会因家庭 IP 变化而改变,因此 Iliyas 的网络切换时无需重新配置。
当 connect.sh 失败(或手动检查显示 NO_CDP)时:
- 远端离线(PC 处于睡眠状态/WSL 未启动):
tailscale status会显示iliyasone离线。无法从 dev-remote 修复。 - Chrome 未运行,但
ssh wsl可以连接:参见“启动”。 - 端口被 WinNAT 占用:参见“端口问题”。
- Chrome 重启后又立即退出:参见“退出问题”。
- 隧道未启动:重新运行
connect.sh(它会重建两段连接)。 - tailnet 路径上的 MTU 黑洞(2026-08-15 发现):
ping正常,但ssh wsl卡在expecting SSH2_MSG_KEX_ECDH_REPLY,或者小型 CDP 调用 (/json/version)正常,而大型调用(/json/list)没有返回结果—— dev-remote 与iliyasone之间的大数据包被丢弃了。分两步修复: 对每次连接 wsl 的 ssh 强制使用较小的经典 KEX (-o [email protected],curve25519-sha256—— 默认的抗量子 sntrup761 KEX 会发送超大数据包),并降低 dev-remote 上接口的 MTU:ip link set dev tailscale0 mtu 1200(默认值 1280 超过了实际路径 MTU)。MTU 设置在 dev-remote 重启后不会保留,但connect.sh会在每次非快速路径运行时重新 应用;如果大数据传输仍然卡住,请检查ip link show tailscale0。MTU 修复前打开的 隧道仍使用错误的 MSS——更改 MTU 后请重启这些隧道。
目前 tailnet 上只有一个家用节点(iliyasone)。如果 Iliyas 之后改用另一台机器,
它会作为独立节点加入,并拥有自己的名称/IP——将 Host wsl(或新增
Host wsl-<name>)指向该节点。不要自行创建非 tailnet 路由。