677 lines
62 KiB
Markdown
677 lines
62 KiB
Markdown
# 浏览器(camofox)与原站视图运行手册
|
||
|
||
对应 REQ v0.7 §4.4、§4.7.1–4.7.4;合同与 camofox OpenAPI 对照见 `docs/design/browser-provider.md`。
|
||
代码:`packages/adapters/src/browser/**`(适配器)、`apps/server/src/modules/browser/**`(会话/接管/能力)、
|
||
`apps/web/app/components/browser/**`(原站视图)。
|
||
|
||
> 当前状态:已对真实 camofox-browser **v1.17.0**(jo-inc,commit `389c996`)容器逐项核验(§7),十项能力全部通过;
|
||
> 探测到其它版本 / fork 时一律显示"未核验",需按 §6 重新核验。
|
||
|
||
## 1. 浏览器服务从哪来
|
||
|
||
### 1.1 部署自带(缺省)
|
||
|
||
`docker compose up -d` 同时起 `camofox` 服务(`docker-compose.yml`,只在内网、不映射端口,卷 `umt-camofox`)。
|
||
app 的 `BROWSER_PROVIDER_URL` 缺省 `http://camofox:9377`,启动后第一次用到浏览器时登记为 **`camofox-env`**
|
||
(`browser_provider.config.managedBy = "env"`,访问凭据取 `BROWSER_ACCESS_KEY` / `CAMOFOX_ACCESS_KEY`,只在进程内,不入库),
|
||
并在后台探测一次能力。地址与凭据只能改部署配置;设置页只能改它的启用。
|
||
|
||
```sh
|
||
# .env(部署目录)
|
||
CAMOFOX_ACCESS_KEY=$(openssl rand -hex 24) # camofox 与 app 共用
|
||
docker compose --env-file .env.deploy up -d --build
|
||
docker compose ps camofox # healthy 即可
|
||
```
|
||
|
||
上游不发布镜像。compose 缺省用的是**浏览器桌面镜像** `umt-browser-desktop:1.17.0`(`docker/browser-desktop`,§8),
|
||
它从纯截图镜像 `camofox-browser:1.17.0` 复制 camofox,所以**先构建旧镜像**:`docker/camofox/Dockerfile` 从 git tag `v1.17.0`
|
||
构建(修了上游 Dockerfile 的两个问题,见文件头)。首次构建要从 GitHub 拉源码与约 630 MB 的 Camoufox 浏览器包:
|
||
|
||
```sh
|
||
git clone --depth 1 --branch v1.17.0 https://github.com/jo-inc/camofox-browser.git /tmp/camofox-src
|
||
docker build -t camofox-browser:1.17.0 --build-arg APT_MIRROR=mirrors.ustc.edu.cn \
|
||
--build-context upstream=/tmp/camofox-src docker/camofox
|
||
# 走宿主代理时加 --network host --build-arg https_proxy=http://127.0.0.1:7890 --build-arg http_proxy=...
|
||
docker compose build camofox # 再构建桌面镜像(或 docker build -t umt-browser-desktop:1.17.0 docker/browser-desktop)
|
||
```
|
||
|
||
只要截图模式(不要桌面、省约 400 MB 常驻内存与 3 GB 磁盘):`.env` 里 `CAMOFOX_IMAGE=camofox-browser:1.17.0`,同一个卷,登录状态通用。
|
||
|
||
### 1.2 接自有实例
|
||
|
||
`.env` 里 `BROWSER_PROVIDER_URL=http://<host>:9377`、`BROWSER_ACCESS_KEY=<它的 CAMOFOX_ACCESS_KEY>`、`CAMOFOX_REPLICAS=0`
|
||
(不再起自带容器)。或者不设环境变量,在 **设置 → 站点默认 → 浏览器服务** 里"添加浏览器服务":名称、服务地址、访问凭据
|
||
(加密存凭证库,只写不回显)、启用。自有实例的版本不是 1.17.0 时能力一律显示"未核验"。
|
||
|
||
### 1.3 不用浏览器
|
||
|
||
`CAMOFOX_REPLICAS=0` + `BROWSER_ENABLED=false`:`camofox-env` 停用,原站视图显示"没有启用的浏览器服务"。
|
||
|
||
### 1.4 设置页与 API
|
||
|
||
设置 → 站点默认 → 浏览器服务:列表(地址、来源、凭据是否设置、启用、健康、核验)、测试连通(探测 `/health` 与
|
||
`/openapi.json`,凭据错误显示"访问凭据不对")、编辑、删除(有未关闭会话时拒绝;`camofox-env` 只能停用);
|
||
点行打开的抽屉里是逐项能力表(不可用 / 未核验 / 已核验 + 说明)。
|
||
|
||
| 端点 | 说明 |
|
||
| --- | --- |
|
||
| `GET /browser/providers[?probe=true]` | 列表;`probe=true` 探测全部启用的服务并写回 |
|
||
| `POST /browser/providers` | 登记 `{name, baseUrl, accessKey?, enabled?}`;`camofox-env` 名称保留 |
|
||
| `PATCH /browser/providers/{id}` | `{baseUrl?, accessKey?(null 清除), enabled?}` |
|
||
| `POST /browser/providers/{id}/probe` | 测试连通(停用的也可以测) |
|
||
| `DELETE /browser/providers/{id}` | 删除 |
|
||
|
||
`browser_provider.config` 可调项(都可省略):
|
||
|
||
| 键 | 缺省 | 作用 |
|
||
| --- | --- | --- |
|
||
| `verified` | `{}` | 逐项覆盖核验结果,如 `{"takeover": false}`;缺省取适配器内置的真机核验表(§7,按上游版本);探测永远不会把它置 true |
|
||
| `navigationTimeoutMs` | 30000 | 单次导航超时 |
|
||
| `maxRetries` | 2 | 导航只重试超时 / 网络 / 5xx / 503;401、白名单、控制权冲突不重试 |
|
||
| `snapshotMaxBytes` | 524288 | 快照文本上限(超出截断并标 truncated) |
|
||
| `requestTimeoutMs` | 30000 | 其它请求超时 |
|
||
|
||
站点域名白名单 = 站点 `base_url` 的主机(含子域)+ `site.config.browserDomains`(字符串数组,给跳转到独立下载域的站点用)。
|
||
站点限流与快照 / 实时搜索共用同一个限流器(导航、开标签页、历史导航取令牌)。
|
||
|
||
## 2. 会话与控制权
|
||
|
||
- 会话按站点 / 账号隔离:`profileKey = umt-site-<siteId>-acct-<accountId|anon>`,同时作为 camofox `userId`
|
||
(上游的登录状态隔离单元);`browser_session.id` 作 camofox `sessionKey`。同一 provider + profile 至多一个未关闭会话。
|
||
- 数据库为准:服务重启后按 `browser_session` / `browser_tab` 恢复适配器内存,`needsSnapshot` 置 true,旧元素引用全部作废。
|
||
上游回收的标签页(404 / 410)对账标 `lost`;整个会话的标签页都没了(camofox 重启、空闲回收)时自动重连,见 §10。
|
||
- 控制者:`automation`(Agent/任务)→ `human`(人工接管)→ `automation` 或 `waiting`;`closed` 终态。
|
||
|
||
| 事件 | 结果 |
|
||
| --- | --- |
|
||
| 接管 `POST /browser/sessions/{id}/control/acquire` | 先把适配器控制者切到 human(新的自动化输入立即被拒),等该 profile 上进行中的请求收尾,再写 `browser_control_lease`(缺省 30 分钟) |
|
||
| 他人已接管 | 409 |
|
||
| 交还 `.../control/release` | 租约 released;会话回 automation(或 `to: "waiting"`);`needsSnapshot = true`;待处理事项关闭 |
|
||
| 租约到期未交还 | 租约 expired,会话转 `waiting`(`lease_expired`),不自动从未知页面继续 |
|
||
| Agent 快照看到验证码 / 二次认证 / Cloudflare 挑战 | 会话转 `waiting`(`captcha` / `challenge`),生成待处理事项(kind `other`,subject `browser_session`) |
|
||
| Agent `browser.interact action=request_human` | 同上,原因 `agent_request` |
|
||
| 站点停用 / 浏览器服务停用 | 下一次访问时会话关闭(`site_disabled` / `provider_disabled`) |
|
||
|
||
HTTP 输入类端点(navigate / click / type / scroll / select / 开关标签页 / 带 target 的 download)只接受持有自己人工租约的调用者;
|
||
快照、截图、会话详情随时可读。Agent 看截图走 `browser.screenshot`(§3.1),敏感截图默认不进模型。
|
||
|
||
## 3. Agent 能力
|
||
|
||
| 能力 | 类别 | 说明 |
|
||
| --- | --- | --- |
|
||
| `browser.snapshot` | read | 结构化快照(已脱敏);`sessionId` 或 `siteId`;清掉 `needsSnapshot` |
|
||
| `browser.navigate` | execute(admin) | 站点域名内;没有会话时按站点打开 |
|
||
| `browser.interact` | execute(admin) | click / type / select / scroll / press / request_human;登录字段用 `credential` 由服务端注入 |
|
||
| `browser.download` | execute(organize.execute) | 捕获下载 → 暂存 → 解析种子、匹配本地资源;**不提交**,提交用 `download.plan` / `download.submit` |
|
||
|
||
execute 类需要持续授权(`automation_grant`)或审批。元素引用只来自最近一次快照:导航、点击、按键提交、控制权交接、
|
||
服务重启之后,旧的 `eN` 报 409 `元素引用已失效…先重新读取页面`。
|
||
|
||
### 3.1 视觉自动化(VIS-1)
|
||
|
||
截图作为图片交给视觉模型 → 模型给出截图上的坐标 → 服务端换算到页面并操作 → 再截图,循环。用于登录表单、勾选框 / 滑块 /
|
||
点选类挑战等快照引用够不着的页面;对应 REQ v0.7 §4.4、§10。代码:`apps/server/src/modules/browser/{service,capabilities}.ts`
|
||
(截图、坐标、证据)、`apps/server/src/modules/agent/challenge.ts`(有界子流程)、`packages/adapters/src/browser/image.ts`(缩放编码)。
|
||
|
||
| 能力 | 类别 | 说明 |
|
||
| --- | --- | --- |
|
||
| `browser.screenshot` | read | 截图缩到 ≤1280 像素宽(缺省 JPEG 质量 70),以 image content 交给模型;同时登记"模型坐标系"并写证据缩略图 |
|
||
| `browser.click_at` | execute | `{sessionId, x, y, button?}`:坐标 = 最近一张截图的像素;只支持左键;返回点击后的新截图 |
|
||
| `browser.type_at` | execute | `{sessionId, x?, y?, text, pressEnter?}`:先点再输(不给坐标 = 输入到当前焦点);账号密码仍用 `browser.interact` 的 `credential` |
|
||
| `browser.scroll` | execute | `{sessionId, dx, dy}` 像素,返回新截图 |
|
||
| `browser.wait` | read | `{sessionId, ms ≤ 5000}`,返回等待后的新截图 |
|
||
| `browser.solve_challenge` | execute | `{sessionId, goal, success, maxSteps ≤ 12, budget}`:有界视觉子流程,见下 |
|
||
|
||
- **上游必须支持图片输入**:按上游模型的 `input` 是否含 `image` 判定(内置 provider 的目录自带;自定义 openai-compatible 端点目前按纯文本处理)。
|
||
不支持时截图不发送,能力改给已脱敏的文字快照并提示;`solve_challenge` 直接判定需人工。fake 上游按支持图片处理。
|
||
- **坐标系**:服务端对每次截图读页面视口与 DPR(`evaluate`),`页面 CSS 像素 = 截图像素 × 视口宽 / 截图宽`;
|
||
输出的 `coordinates` 记录截图尺寸、原始尺寸、视口、DPR(取不到时 `assumed = true`,按原图、DPR 1)。动作之后旧坐标系作废,
|
||
必须用新截图的坐标。点击(CF-2 起)先走 umt 插件的 `POST /umt/tabs/{id}/mouse-click`——Playwright `page.mouse.click` 发真实鼠标事件,
|
||
**跨域 iframe 里的控件(Cloudflare Turnstile)和画布也点得到**;插件没有这个端点(旧镜像、纯截图镜像)时退回 1.17.0 的做法:
|
||
`elementFromPoint` 打标记再按选择器点(跨域 iframe / 画布点不到具体位置)。
|
||
- **动作前提**:会话在自动化控制下(人工接管 / 等待处理中一律拒绝),当前页面主机在站点域名白名单内。
|
||
- **敏感截图**:登录页地址、页面上有密码框、地址带凭据 → `sensitive = true`,默认不交给模型(不做像素遮蔽),改给已脱敏的文字快照;
|
||
要交给模型需要 `automation_grant.action = browser.sensitive_screenshot`(或 `*`)的持续授权,且调用时传 `allowSensitive: true`。
|
||
- **二次验证 / 短信 / 本人设备确认**:截图时读页面文字、快照时读快照文本,命中即把会话转等待处理(原因 `two_factor`),
|
||
之后的自动化动作全部被拒;系统提示也要求模型立即停止。视觉子流程**不能**接手 `two_factor` 等待。
|
||
- **证据**:每次截图(含动作后的截图)写 480 像素宽的 JPEG 缩略图到 `DATA_DIR/tmp/evidence/<id>.jpg`(0600,7 天后清理);
|
||
能力输出只给引用 `evidence/<id>.jpg`,随工具结果摘要进 `agent_run_event`。图片数据不进 JSON 文本、不进操作账本。
|
||
- **授权**:`click_at / type_at / scroll / solve_challenge` 由 `browser.interact` 的持续授权覆盖(也可单独授权);
|
||
父会话白名单里有 `browser.solve_challenge` 即覆盖子流程用到的截图与坐标能力;授权同理,solve_challenge 通过授权后,子流程(`spawn_reason = browser.solve_challenge`)内的坐标动作不再单独要持续授权。坐标动作按调用区分幂等键,崩溃恢复不重放。
|
||
- 交给模型的截图参数可在 `browser_provider.config` 调:`screenshotMaxWidth`(≤1280)、`screenshotFormat`(`jpeg` / `png`)、
|
||
`screenshotQuality`(20–95)。
|
||
|
||
`browser.solve_challenge` 流程:
|
||
|
||
1. 会话须在自动化控制下,或因 `captcha` / `challenge` / `challenge_unsolved` 在等待处理(收回自动化控制,原待处理事项保留);
|
||
其它等待原因(二次验证、租约过期、Agent 请求人工)与人工接管中一律拒绝。
|
||
2. 在调用进程里起一个子 `agent_run`(父会话 = 调用它的 Agent 运行,`spawn_reason = browser.solve_challenge`;
|
||
上游与父会话相同;白名单只有截图 / 坐标点击 / 输入 / 滚动 / 等待 / `browser.snapshot`),一步 = 一次工具调用,
|
||
`maxSteps` 即工具调用上限;token / 费用 / 时长按 `budget`(时长缺省 180 秒),用量计入父会话。
|
||
3. 子流程结束后服务端按 `success`(`urlContains` / `urlChanged` / `textPresent` / `textAbsent`,全部满足)读页面核实,
|
||
不信模型自述。结果:`solved`(关闭原待处理事项)/ `needs_human`(二次验证、模型最后一行写 `NEEDS_HUMAN: 原因`、上游不支持图片)/
|
||
`exhausted`(步数或预算用尽)/ `failed`。
|
||
4. 没成功:会话转等待处理(`two_factor` 或 `challenge_unsolved`),待处理事项的 `evidence.challenge` 附最近 12 张证据截图引用与子运行 ID,
|
||
返回 `takeover.path = /console/sites/<siteId>?tab=origin`(原站视图接管入口)。不自动重试;人可以再次调用或接管处理。
|
||
|
||
不承诺通过 Cloudflare 等挑战(REQ §4.4):上游浏览器指纹与点击方式都可能被识别,失败按上面第 4 步转人工。
|
||
|
||
## 4. 下载产物
|
||
|
||
- 暂存:`DATA_DIR/tmp/browser-downloads/<artifactId>.bin`(目录 0700、文件 0600),超过 24 小时的在下一次捕获时清理。
|
||
合同里只出现引用(`bytes.ref`),来源地址去掉 passkey。
|
||
- 核验:bencode 解析、infohash v1/v2、私有标记;按 `source_release.infohash_v1/v2`,再按下载地址 `id` 参数匹配本站资源。
|
||
- 提交:HTTP `POST /browser/sessions/{id}/download` 带 `submit` 时转调 `POST /acquisition-plans`(同一鉴权、计划、去重、
|
||
缓存阈值、授权检查);被拒时产物照常返回,`note` 写拒绝原因。同一会话同一 sha256 第二次捕获标 `duplicate`,不再提交。
|
||
- 浏览器没有媒体库写权限;提交链路下载的是站点适配器取到的种子,暂存文件只作核验证据。
|
||
|
||
## 5. 排障
|
||
|
||
| 现象 | 查 |
|
||
| --- | --- |
|
||
| 原站视图"没有启用的浏览器服务" | `browser_provider` 是否有 `enabled = true` 且 `kind = 'camofox'` 的行 |
|
||
| 412"浏览器服务认证失败" / camofox 401 | 访问凭据与 `CAMOFOX_ACCESS_KEY` 是否一致;`GET /browser/providers?probe=true` |
|
||
| 502 浏览器服务不可用 | camofox `/health`(503 = 恢复中);容器日志 |
|
||
| 409 先接管会话再操作 | 界面点"接管";或租约已被他人持有 / 已过期 |
|
||
| "连接中断" | 截图请求失败;看 API 日志 `component: browser` 的方法 / 路径 / 状态(日志不含正文与凭据) |
|
||
| 测试连通提示"访问凭据不对" | `/health` 免认证所以能连上,但 `/openapi.json` 401:`BROWSER_ACCESS_KEY` 与 camofox 的 `CAMOFOX_ACCESS_KEY` 不一致 |
|
||
| 原站视图"浏览器已重启,正在重新连接…" | camofox 重启 / 会话空闲回收后的自动重连(§10),正常几秒内恢复 |
|
||
| 503 `browser_reconnect_failed` / "浏览器连接已断开" | 自动重连失败,消息里是原因(camofox 没起来、`POST /tabs` 5xx 等);`docker compose ps camofox`、容器日志;恢复后点"重新连接" |
|
||
| 409"标签页已被浏览器服务回收" | 只在显式指定了已被回收的 `tabId` 时出现(会话里还有别的标签页活着);不指定 `tabId` 重试即可 |
|
||
| 重启 camofox 后要重新登录 | 容器是被 SIGKILL / OOM 杀掉的(`docker inspect` 看 `OOMKilled`、退出码 137):只有 SIGTERM 正常停止、会话空闲回收、关闭会话时才落盘登录状态;加大 `CAMOFOX_MEM_LIMIT` |
|
||
| 设置页显示"未核验" | 探测到的上游版本不在核验表里(不是 1.17.0,或 fork);按 §6 核验后在适配器 `CAMOFOX_VERIFIED_RELEASES` 加一行,或临时写 `config.verified` |
|
||
|
||
## 6. 真实实例核验清单(REQ §4.7.4)
|
||
|
||
先固定 camofox-browser 的**仓库、版本(`/openapi.json` 的 `info.version`)、启动方式与认证配置**,记在本节末尾。
|
||
通过的能力登记进适配器的真机核验表 `CAMOFOX_VERIFIED_RELEASES`(`packages/adapters/src/browser/camofox/capabilities.ts`,
|
||
按上游版本,只在探测到完全相同的版本时生效,并把 `verified_at` 写成核验日期);个别部署要撤销某项写
|
||
`browser_provider.config.verified = {"<键>": false}`。不通过的保持 false,界面继续标"未核验"。
|
||
不得用另一个浏览器窗口代替后宣称通过。
|
||
|
||
自动化核验脚本:`scripts/browser-verify/run.sh`(临时栈 `umtbr`:postgres 127.0.0.1:55447、camofox 127.0.0.1:49377、
|
||
compose 网络里的 mock NexusPHP 站 `mocknexus:8080`;服务层在宿主进程里装配,与生产同一套代码;
|
||
阶段之间重启 / SIGKILL camofox;结果写 `.browser-verify-out/results-*.json` 与截图)。跑完 `docker compose -p umtbr down -v`。
|
||
|
||
| # | 项目 | 做法 | 通过标准 | 对应 verified 键 |
|
||
| --- | --- | --- | --- | --- |
|
||
| 1 | 登录保持 | 观众 / 馒头 / HHClub / CHD 各一账号:原站视图接管登录;闲置超过 `SESSION_TIMEOUT_MS` 后再 `browser.snapshot` | 不需重新登录;过期时落到登录页、`loginRequired = true`,而不是空列表 | `sessions`、`persistentProfile` |
|
||
| 2 | 截图 / 结构化快照 | 列表页、详情页、登录页各取 `screenshot` 与 `snapshot` | 截图格式(JSON base64 还是 image/*)、尺寸与坐标一致;快照 `eN` 引用可点;密码框值、Cookie、passkey 不出现;大页面 `offset` 分页正确 | `screenshot`、`snapshot` |
|
||
| 3 | 列表 / 详情提取 | 快照 + `POST /extract`(属性带 `x-ref`)与现有 HTTP 适配器结果对比 | 标题、分类、副标题、促销、做种一致,原始字段不丢 | `extract` |
|
||
| 4 | 文件下载捕获 | 详情页点"下载"后 `POST /browser/sessions/{id}/download` | `GET /tabs/{id}/downloads` 的字段形状(filename / data / size / status)与适配器兼容;sha256 与直接 HTTP 下载一致;进暂存核验,不直写媒体库 | `downloadCapture` |
|
||
| 5 | 重启后会话 | 分别触发 `BROWSER_IDLE_TIMEOUT_MS` 回收与强制重启 camofox 容器 | 持久化 profile 恢复登录;旧 tabId 识别为 `lost`,不重放旧引用 | `persistentProfile`、`tabs` |
|
||
| 6 | 人与 Agent 同标签页连续操作 | Agent 打开详情页 → 原站视图接管,在**同一标签页**点击、输入 → 交还 → Agent 重新 `snapshot` | 人看到的是同一会话同一页面;交还后 Agent 读到人工操作后的状态;下载不重复提交 | `takeover`、`interact` |
|
||
|
||
核验记录:
|
||
|
||
| 日期 | 仓库 / fork | 版本 | 启动方式 / 认证 | 通过项 | 备注 |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| 2026-09-28 | jo-inc/camofox-browser | v1.17.0(`389c996`),Camoufox 152.0.4-beta.28 | `docker/camofox/Dockerfile` + compose;`CAMOFOX_ACCESS_KEY` Bearer | 十项全部 | mock NexusPHP 站 + example.com;四个真实站点未测,见 §7 |
|
||
|
||
## 7. 真机核验(版本 1.17.0)
|
||
|
||
环境:`camofox-browser:1.17.0` 容器(本机构建,镜像 3.6 GB,空闲内存约 200 MB,打开 2 个会话后约 390 MB),
|
||
`CAMOFOX_ACCESS_KEY` 已设置,`SESSION_TIMEOUT_MS=60000`、`BROWSER_IDLE_TIMEOUT_MS=30000`(空闲回收阶段用短超时);
|
||
目标页:testkit mock NexusPHP(观众风格,`mocknexus:8080`,与 camofox 同一 compose 网络)与公开静态页 `https://example.com/`。
|
||
浏览器服务经环境变量登记为 `camofox-env`。每项的原始输出见 `scripts/browser-verify/run.sh` 的结果文件。
|
||
|
||
| # | 项目 | 结果 | 实际观测 |
|
||
| --- | --- | --- | --- |
|
||
| 0 | 认证 / 探测 | 通过 | 无 Bearer 请求 401;`/openapi.json` `info.version = 1.17.0`,十项能力的端点都在 |
|
||
| 1 | 登录保持 | 通过 | 新 profile 打开 `torrents.php` → 302 到 `login.php?returnto=…`,`loginRequired = true`;人工接管登录后 Agent 直接打开受保护列表页,不再登录(站点 takelogin 只收到 1 次);Agent 用服务端凭证注入登录第二个账号也通过,快照与返回值里没有密码 |
|
||
| 2 | 截图 / 结构化快照 | 通过(修了遮蔽) | 截图直接返回 `image/png` 二进制(不是 OpenAPI 写的 JSON base64),视口随指纹变化(实测 1433×1145、1691×867),截图像素 = 点击坐标;登录页 `modelSafe = false`。快照是 Playwright ARIA 格式(`- textbox [e2]: 值`,引用写作 `[eN]`),**密码框与普通文本框同形且带明文值**,原有按标签名的遮蔽规则抓不到 → 已修(见下);列表页 45 个引用、8.6 KB,未触发分页 |
|
||
| 3 | 列表 / 详情提取 | 通过 | 列表页快照含 fixture 的标题、副标题、大小、做种数(5 条中 4 条标题完整出现,1 条是站点截断显示);`POST /extract` 按 `x-ref` 取回标题;详情页取回下载链接文字。与 HTTP 适配器同源 fixture 对照,未逐字段比对促销标记 |
|
||
| 4 | 下载捕获 | 通过(修了字段名) | Agent 点详情页 `.torrent` 链接 → `downloads` 返回 `{id, url, suggestedFilename, mimeType, bytes, createdAt, failure, dataBase64}`;sha256 与直接 HTTP 下载一致(`9fcf61f7…`),infohash 与站点一致并匹配到本地资源,约 2 秒;`consume=true` 后不再返回同一文件 |
|
||
| 5 | 重启后会话 | 通过(修了 3 处),有已知限制 | SIGTERM 重启:旧标签页返回 **410** `browser_restarted`(不是 404)→ 已按 lost 处理;登录状态恢复,Agent 再次导航在同一会话开新标签页(约 1.3 秒,含浏览器冷启动)。会话空闲回收(60 秒 + 上游 60 秒扫描周期):旧标签页 404 → lost,重新打开仍登录(约 0.4 秒)。**SIGKILL**:上次落盘之后的新登录丢失(需重新登录),这是上游落盘时机决定的(只在会话空闲回收、关闭、SIGTERM 停止时写),界面照常识别为登录页 |
|
||
| 6 | 人与 Agent 同标签页 | 通过(修了坐标点击与焦点输入) | Agent 打开登录页(上游 tabId T)→ 接管:自动化输入被拒(409)、只读快照允许 → 截图坐标点击用户名框 → 焦点输入 → 点密码框 → 输入 → 回车 → 交还;交还前的旧引用 `e1` 被拒("先重新读取页面");Agent 重新读取,**仍是 tabId T**,看到人工登录后的页面。后退 / 前进 / 刷新 / 滚动也在同一标签页通过。核验走原站视图调用的同一组 HTTP 端点,没有在浏览器里点原站视图界面 |
|
||
|
||
对接中发现并已修的差异:
|
||
|
||
1. **截图格式**:1.17.0 直接返回 `image/png`;适配器原本两种都兼容,无需改。
|
||
2. **坐标点击**:`POST /click` 只认 `ref` / `selector`,不接受 `{x, y}` → 适配器先 `POST /evaluate` 用 `elementFromPoint` 给该点元素打
|
||
`data-umt-pt` 标记,再按属性选择器点击(仍走上游的完整鼠标事件序列与下载守卫)。坐标处没有元素报 400。
|
||
3. **焦点输入**:`POST /type` 缺省 `mode=fill` 必须带 `ref`/`selector` → 无目标时发 `mode=keyboard`;回车用 `/press`。
|
||
4. **密码框遮蔽**:快照里出现带值的输入框时,经 `evaluate` 取页面上 `input[type=password]` 的当前值按字面量遮蔽;
|
||
`evaluate` 失败时遮蔽全部输入框的值。`extract` 结果同样处理。
|
||
5. **下载字段**:`suggestedFilename`、`bytes`(大小)、`dataBase64`、`dataSkipped`(超 `maxBytes`)、`failure`。
|
||
6. **410**:浏览器重启后 5 分钟内旧标签页返回 410,与 404 同样按"标签页已不存在"对账为 lost;Agent 下一次导航在同一会话开新标签页。
|
||
7. **上游镜像**:① v1.17.0 的 Dockerfile 在 `npm ci` 前没复制 `postinstall.js`,构建必然失败;② `CMD sh -c "node …"`
|
||
不转发 SIGTERM,`docker stop/restart` 等满宽限期后 SIGKILL,**关机落盘不执行,重启后登录全丢**(修前实测)。
|
||
`docker/camofox/Dockerfile` 两处都改了(`exec node`)。
|
||
8. 其它:关掉上游缺省开启的崩溃报告外发(`CAMOFOX_CRASH_REPORT_ENABLED=false`)与每次启动从 addons.mozilla.org 下载
|
||
uBlock(`CAMOFOX_DISABLE_DEFAULT_ADDONS=true`,内网下载失败会拖慢启动)。
|
||
|
||
`userId`(`umt-site-<uuid>-acct-<uuid>`,约 90 字符)上游原样使用(持久化目录按 SHA-256 取名),无字符限制问题;
|
||
`DELETE /tabs/{id}` 的 `userId` query / body 都认;`DELETE /sessions/{userId}` 关闭该 profile 全部标签页并落盘。
|
||
|
||
视觉自动化(VIS-1,2026-09-29,`scripts/browser-verify/vision-real.ts`,只读使用现有 `umt-camofox-1`):打开 `https://example.com/`,
|
||
截图 `image/png` 1920×992(本次指纹的视口 1920×992、DPR 1;此前两次观测到 1600×819、1433×1145,视口随指纹变化),
|
||
缩成 1280×661 JPEG 交给模型;在缩放图上取"Learn more"链接中心 (640, 343),按服务端换算得页面 (960, 515),
|
||
与 `getBoundingClientRect` 的中心 (960.0, 514.8) 一致;点击后跳到 `https://www.iana.org/help/example-domains`,通过。
|
||
另:1.17.0 的 `POST /click` 在 OpenAPI 里写了 `coordinates: {x, y}`,实测仍报 `ref or selector required`,坐标点击继续走 `elementFromPoint`。
|
||
|
||
没有核验的:四个真实站点(观众 / 馒头 / HHClub / CHD)的登录保持、Cloudflare / 验证码挑战、真实站点页面上的提取对比;
|
||
并发 1 / 2 / 4 会话的性能与资源;原站视图界面本身在浏览器里的点击体验(1.5 秒截图刷新);VNC 插件(镜像已装,未启用、未接入)。
|
||
|
||
## 8. 原站视图桌面模式(Selkies 远程桌面,WT-1)
|
||
|
||
方案与取舍见 `docs/design/webtop-remote-browser.md`。桌面镜像 = linuxserver `baseimage-selkies:debiantrixie`(Webtop 4.0 的底座,
|
||
pixelflux + labwc Wayland 桌面 + Selkies 2.0 Web 客户端)+ camofox 1.17.0 **有头**运行(`CAMOFOX_INTERACTIVE=desktop`,
|
||
`MOZ_ENABLE_WAYLAND=1`)。原站视图里看到的窗口就是 Agent 经 camofox API 驱动的同一个 Firefox 进程:Agent 导航,画面跟着变;
|
||
人在画面里点,Agent 下一次快照读到的就是点过之后的页面。
|
||
|
||
### 8.1 组成
|
||
|
||
| 部件 | 位置 | 说明 |
|
||
| --- | --- | --- |
|
||
| 镜像 | `docker/browser-desktop/Dockerfile` | 底座按 digest 锁定;从 `camofox-browser:1.17.0` 复制 node、`/app`、Camoufox;补 `fonts-noto-cjk-extra`;可选 fcitx5(拼音 + mozc,`--build-arg WITH_FCITX5=0` 不装) |
|
||
| 启动 | `root/etc/s6-overlay/s6-rc.d/svc-camofox` | s6 服务,等 labwc 的 `wayland-0` 出现后以 `abc` 身份 `exec node server.js`;依赖 `svc-de` 与 `svc-dbus`(Firefox 连着 D-Bus,dbus 先停会把浏览器带走,关机落盘失败) |
|
||
| 初始化 | `init-umt-desktop` | 由 `CAMOFOX_ACCESS_KEY` 派生 Selkies 控制 / 只读口令;覆盖 labwc autostart;把 `/root/.camofox` 交给 `abc` |
|
||
| 反代 | `apps/server/src/modules/desktop` | `/api/v1/browser/desktop/{sessionId}/*` → 容器 `:3000`(静态客户端 + `api/websockets`);只接受控制台会话 |
|
||
| 前端 | `apps/web/app/components/browser/desktop-view.tsx` | 原站视图 Tab:桌面可用且是安全上下文 → 桌面模式;否则截图模式并说明原因;可手动切换 |
|
||
| 自愈(INT-3 / FIX-5) | 同上 | 打开桌面先调一次幂等 `POST /browser/sessions/{id}/reconnect`(iframe 直连 Selkies 不触发被动重连),等它返回后才加载桌面 iframe(期间显示「正在确认浏览器会话…」,FIX-5 起不再先连上黑屏);画面 5 秒未加载或视频流未出帧 → 每个会话自动重连一次并重建桌面连接,之后靠「重新连接」按钮 |
|
||
| HTTPS | compose `caddy`(profile `tls`)+ `docker/browser-desktop/Caddyfile` | 可选;已有反代终结 TLS 时不用 |
|
||
|
||
窗口:无标题栏、最大化铺满 1280×900(`BROWSER_DESKTOP_WIDTH/HEIGHT`),`HARDEN_DESKTOP` 去掉终端 / sudo / xdg-open,
|
||
`HARDEN_OPENBOX` 去掉关闭按钮与右键 / 中键菜单、禁 Alt+F4 等。音频、麦克风、摄像头、手柄、文件传输、打印、命令、共享全部关闭并锁定,
|
||
剪贴板只允许文本。
|
||
|
||
### 8.2 鉴权与输入权限
|
||
|
||
- Selkies 端口(3000/8082)不映射到宿主,唯一入口是 app 的反代;外部 API Key 与 DEV key 一律 403。
|
||
- 口令不经浏览器:app 按请求者身份在上游请求上注入 `Authorization: Basic`。持有该会话 active 人工租约的本人 → 控制口令;
|
||
其他人(包括未接管的本人)→ 只读口令,Selkies 服务端丢弃只读连接的键鼠与剪贴板输入。
|
||
- 口令 = `HMAC-SHA256(CAMOFOX_ACCESS_KEY, "umt-desktop/control" | "umt-desktop/view")` 前 48 位 hex,app 与容器各算一遍;
|
||
没设 `CAMOFOX_ACCESS_KEY` 时容器用随机口令,桌面模式不可用(界面回退截图模式)。
|
||
- WebSocket 每 3 秒复查一次角色:交还 / 租约过期 / 被他人接管 / 控制台会话失效时服务端主动断开,Selkies 客户端自动重连并按新口令认证
|
||
(实测交还后 5 秒内点击已无效)。WebSocket 还要求 `Origin` 与 `Host` 同源(cookie 会话 + WS 无 CORS)。
|
||
- 反代响应带 `Content-Security-Policy: frame-ancestors 'self'` 与 `X-Frame-Options: SAMEORIGIN`,只能被本站 iframe 嵌入;
|
||
上游的 `WWW-Authenticate` 不外露(否则浏览器弹 Basic 登录框)。
|
||
- 只支持环境变量登记的 `camofox-env`(compose 自带容器);桌面地址缺省由 `BROWSER_PROVIDER_URL` 推出(端口换 3000),
|
||
或显式 `BROWSER_DESKTOP_URL`。
|
||
|
||
### 8.3 界面
|
||
|
||
原站视图 Tab 调 `GET /api/v1/browser/desktop/{sessionId}/_status` 决定模式:`available`(容器 3000 能连上且有访问凭据)
|
||
且 `window.isSecureContext` → 桌面模式(iframe);否则截图模式,顶部写原因:"桌面模式不可用:…" 或 "桌面模式需要通过 HTTPS 访问控制台"。
|
||
桌面模式顶栏:连接状态(已连接,只读 / 可操作)、当前控制者、接管 / 交还(沿用 `control/acquire|release`)、重新连接、截图模式、关闭会话。
|
||
「当前控制」(HH-2,两种模式共用 `coords.ts controllerLabel`):`controller = automation` 且 2 分钟内有过自动化动作(Agent 能力经
|
||
`BrowserService.run` 成功执行,写 `recovery_state.lastAutomationAt`,会话视图字段 `lastAutomationAt`)才显示「Agent」,说明写
|
||
「Agent 正在操作」;否则显示「自动化」,说明写「目前没有 Agent 在操作」。人工接管显示「人工接管(用户名)」,人工动作不刷新该时间。
|
||
接管后直接在画面里点和输入;中文、日文用**本机输入法**(Selkies 接收 composition 事件,按 Unicode 注入上屏文本);
|
||
复制粘贴用 Ctrl+C / Ctrl+V。
|
||
|
||
### 8.4 HTTPS
|
||
|
||
WebCodecs 视频解码与剪贴板 API 只在安全上下文(HTTPS 或 `localhost`)可用;用 IP / 局域网主机名经 HTTP 访问时只能用截图模式。
|
||
|
||
```sh
|
||
# 已有反代(nginx / Caddy / Traefik)终结 TLS:无需改动,WebSocket 需透传 Upgrade、读超时 ≥ 1 小时、不缓冲
|
||
# 没有反代:启用 compose 自带 Caddy
|
||
docker compose --profile tls up -d # 自签:Caddy 内部 CA 按访问的主机名现场签发,浏览器需放行或导入根证书
|
||
docker compose cp caddy:/data/caddy/pki/authorities/local/root.crt ./umt-root.crt # 导入到客户端受信任根
|
||
# 有公网域名:.env 里 TLS_DOMAIN=media.example.com、[email protected](ACME,80/443 需可达)
|
||
```
|
||
|
||
`TLS_PORT`(缺省 443)、`TLS_BIND` 可改;4380 仍可直连(截图模式)。
|
||
|
||
### 8.5 回退
|
||
|
||
- 桌面容器起不来 / 桌面异常:`.env` 设 `CAMOFOX_IMAGE=camofox-browser:1.17.0` 后 `docker compose up -d camofox`,原站视图自动回到截图模式,
|
||
Agent 能力不受影响(实测同一个卷,切换前后登录状态都在)。
|
||
- 只关桌面、保留镜像:不设 `CAMOFOX_ACCESS_KEY` 即可(桌面口令随机,app 探测为不可用)。
|
||
- 容器内输入法(本机输入法不可用时的备选):`BROWSER_DESKTOP_IME=fcitx5`,Ctrl+Space 切换;与本机输入法同时开会重复组字,缺省关闭。
|
||
|
||
### 8.6 原型核验(2026-09-29,临时栈 `umtwt`)
|
||
|
||
环境:本机 96 核、无 GPU(pixelflux 软件渲染 + CPU H.264);客户端 headless Chromium 145(Playwright),经 Caddy 自签 HTTPS;
|
||
上游 camofox 1.17.0 / Camoufox 152.0.4-beta.28,Selkies 2.0.0,labwc 0.8.3。脚本 `apps/web/test/screenshots/webtop/{p0,verify}.mjs`(截图不进 git)。
|
||
|
||
| # | 项 | 结果 | 观测 |
|
||
| --- | --- | --- | --- |
|
||
| 1 | 同会话 | 通过 | camofox API 导航 example.com → iana,桌面画面同步;在桌面里点链接,API 快照 URL 变为 `iana.org/domains/reserved`,tabId 不变 |
|
||
| 2 | 中日文显示 | 通过 | zh / ja 维基首页正文、标题栏、地址栏无豆腐块(Camoufox 自带 Noto CJK) |
|
||
| 3 | 本机输入法 | 通过 | CDP `Input.imeSetComposition` + `insertText` 模拟组字:搜索框得到「流浪地球」「進撃の巨人」(camofox `evaluate` 读回) |
|
||
| 4 | 剪贴板 | 通过 | 本机 → 远端:写本机剪贴板,焦点回到画面后 Ctrl+V 粘贴中日文;远端 → 本机:远端 Ctrl+C 后本机 `readText` 读回中文 |
|
||
| 5 | 只读口令 | 通过 | 未接管时在画面点链接,URL 不变;接管后同一坐标点击生效;API 交还后 5 秒内再点无效 |
|
||
| 6 | 重启保持 | 通过(修了 1 处) | `docker compose restart camofox` 后 httpbin cookie 仍在。修前 dbus 与 camofox 并行停止,Firefox 先退出导致关机落盘失败 |
|
||
| 7 | HTTP 回退 | 通过 | 经 HTTP 访问显示"桌面模式需要通过 HTTPS 访问控制台",截图模式正常;旧镜像下显示"浏览器服务没有桌面…" |
|
||
| 8 | 资源 | 见右 | 容器内存:1 会话无人观看约 570–670 MB,观看约 610 MB,滚动维基首页约 730 MB;CPU:无人观看 0.3%(不编码),观看静止页 1–7%,持续滚动约 75–100%(约 1 核) |
|
||
| 9 | 镜像 | — | `umt-browser-desktop:1.17.0` 解压 6.9 GB / 压缩 2.0 GB(旧镜像 3.6 GB / 1.1 GB) |
|
||
|
||
已知问题与未做:
|
||
|
||
- Firefox 工具栏里有 Playwright 的 "JUGGLER" 标识;用户可在画面里打开 `about:addons`、新标签页、输入任意网址——越界防护
|
||
(`policies.json`、`context.route` 白名单、交还时核对 URL,方案 §6)未做。
|
||
- 接管期间 camofox 看不到 API 请求:桌面连接期间与人工接管期间由 app 定期保活(§12,AG-4),不再被空闲回收;
|
||
浏览器里的心跳插件(方案 §5.5)不做。
|
||
- 一个桌面承载所有站点会话:持有 A 站租约的人在画面里能看到 / 操作切到前台的其它窗口;目前同一时刻一般只有一个会话窗口。
|
||
- 真实输入法(Windows 微软拼音、macOS 日文)、Firefox / Safari 客户端、真实 PT 站点未测;容器内 fcitx5 只验证能启动,未验证组字。
|
||
- 画面坐标与 camofox 截图坐标不同(桌面含 56 px 工具栏,Camoufox 伪装了 `screenX` / `outerHeight`),Agent 视觉操作继续用 camofox 截图。
|
||
|
||
## 9. 凭据统一:站点账号登录态 ⇄ 浏览器(BR-2)
|
||
|
||
一处管理凭据:站点账号里的 Cookie(手填,或账密自动登录得到的会话 Cookie)在打开浏览器会话时注入该站点 / 账号的 profile;
|
||
人在浏览器里登录后,登录态读回站点账号,HTTP 抓取、RSS、快照下一次就用新的。截图模式与桌面模式是同一个 camofox 进程,共用。
|
||
代码:`sources-identity/service/credentials.ts`(单一读写入口)、`modules/browser/service.ts`(`prepareCookies` / `syncCookies`)、
|
||
`packages/adapters/src/browser/{cookies.ts,camofox/client.ts}`、camofox 插件 `docker/browser-desktop/root/opt/camofox/plugins/umt-cookies`。
|
||
|
||
### 9.1 注入做法
|
||
|
||
camofox 1.17.0 只有 `POST /sessions/:userId/cookies`(导入),而且要另一个口令 `CAMOFOX_API_KEY`;没有读取端点。
|
||
所以桌面镜像带一个最小插件 **umt-cookies**(Dockerfile 把它加进 `camofox.config.json` 的插件清单),在访问凭据
|
||
(`CAMOFOX_ACCESS_KEY`)之下提供:
|
||
|
||
| 端点 | 说明 |
|
||
| --- | --- |
|
||
| `POST /umt/sessions/{userId}/cookies` `{cookies}` | `context.addCookies`,并触发持久化插件立即落盘(与上游导入端点相同) |
|
||
| `GET /umt/sessions/{userId}/cookies?urls=a,b` | 作用于这些地址的 Cookie:会话活着读 `context.cookies(urls)`,否则读 profile 落盘文件 |
|
||
| `GET /umt/instance` | `{instanceId, startedAt}`:插件加载时生成,camofox 进程重启即变(BR-3 重启检测,§10) |
|
||
| `GET /umt/identity` | 固定浏览器身份的公开部分(UA / OS / 版本,BR-4,§11.1) |
|
||
| `PUT /umt/sessions/{userId}/proxy` `{proxy}` | 该 profile 的出口:`{server, username?, password?}` 或 `null` 直连(BR-4,§11.5) |
|
||
| `POST /umt/tabs/{tabId}/mouse-click` `{userId, x, y}` | 按视口坐标发真实鼠标事件(`page.mouse.click`,CF-2,§13);标签页不在回 422 |
|
||
|
||
适配器先走插件;插件不在(404)再试上游导入端点(`CAMOFOX_API_KEY` 与访问凭据相同时可用,只能写不能读);都不行 → 会话照常打开,
|
||
`credential.status = unsupported` 并说明。profile 落盘在 `/root/.camofox/profiles/<sha256(userId) 前 32 位>/storage-state.json`
|
||
(Playwright storageState:cookies + localStorage;sessionStorage 不保存,IndexedDB 缺省不保存),桌面镜像里目录 0700、文件 0600
|
||
(`svc-camofox` 设 `umask 077`,`init-umt-desktop` 对旧卷 `chmod -R go-rwx`)。**改了镜像,需重建 `umt-browser-desktop` 后生效**
|
||
(`docker compose build camofox && docker compose up -d camofox`);纯截图镜像 `camofox-browser:1.17.0` 不带插件,注入 / 回写都显示不支持。
|
||
|
||
打开会话(`POST /browser/sessions`、Agent `browser.navigate` 首次打开)时:
|
||
|
||
1. 匿名会话、API Key 类站点 / 账号(馒头:网页登录态与 API Key 无关)→ 不注入,`not_applicable`;
|
||
2. 取账号 Cookie:用户填的 + 自动登录得到的会话 Cookie,同名以会话 Cookie 为准;账密账号还没有会话 Cookie → 先走一次 refresh-login;
|
||
3. Cookie 头串转成站点主机的 host-only Cookie(路径 `/`,https 站点标 secure,期限 30 天——头串里没有期限,站点响应会按自己的期限覆盖),
|
||
**在开第一个标签页之前**写进 profile,再导航;
|
||
4. 注入了仍落在登录页 → `credential.status = expired`;Cookie 账号(没有密码可自动登录)转等待处理 `login_required`
|
||
("登录已失效:接管后在画面里登录一次,交还后全站生效"),账密账号留给 Agent 用凭证注入登录。
|
||
|
||
会话详情 `credential`:`status`、`source`(`manual` 手填 / `login` 自动登录 / `browser` 浏览器回写)、`sourceAt`、`count`(个数,不含值)、
|
||
`message`、`syncedAt`;原站视图顶栏显示"登录态:来源 · 时间"。
|
||
|
||
### 9.2 回写
|
||
|
||
| 时机 | 做法 |
|
||
| --- | --- |
|
||
| 人工接管交还 | 先按上游 `GET /tabs` 同步当前地址(桌面里的导航不经 API),不在登录页才读回 |
|
||
| 关闭会话 | 关闭前读回一次(站点 / 浏览器服务停用导致的关闭不读) |
|
||
| 定时 | 会话有动作时,距上次读回(或创建)满 10 分钟,后台读回一次 |
|
||
| refresh-login 发现 Cookie 失效 | Cookie 账号:从浏览器 profile 读回(不要求会话开着,读落盘文件),与存的不同就拿它验证一次,有效才回写并标 active |
|
||
|
||
读回只取作用于站点主机的、未过期的 Cookie(统计 / CDN / 第三方域名一律丢弃),剔除 Cloudflare 的 `cf_clearance` / `__cf_bm`(仅浏览器,§11.4),
|
||
与存储比较(忽略顺序),有变化才写(同时结束 UA 待切换,§11.3):
|
||
Cookie 账号替换 `secret_ref`,账密账号替换(或新建)会话 Cookie;`site_account.user_info.cookieOrigin = {source: "browser", by: "browser", at}`,
|
||
`expired` / `invalid` 的账号回到 `unknown`。来源判定:`cookieOrigin.at` 不早于 secret 最近写入(`rotated_at`,留 5 秒)才算浏览器回写,
|
||
之后再手填或自动登录会让它变回 `manual` / `login`。
|
||
|
||
安全:Cookie 值只在服务端、secret 表与浏览器服务之间传;日志只记个数;会话详情 / 能力输出 / 模型上下文里没有值;快照遮蔽照旧(§3)。
|
||
|
||
### 9.3 真机核验(2026-09-29)
|
||
|
||
`scripts/browser-verify/cookie-real.ts`:临时容器 `camofox-browser:1.17.0` + 挂载 umt-cookies 插件(与用户栈同一网络、同一出口代理),
|
||
读用户栈数据库里憨憨(hhanclub)账号的手填 Cookie(5 个),注入**全新** profile 后打开首页一次:HTTP 200、落在 `/` 不是登录页、
|
||
页面有 `a[href*=logout.php]`(站点定义的登录成功标记)→ 已登录;插件读回 5 个、全部是 `hhanclub.net` 域名、包含注入的全部名字。
|
||
对真实站点只有这一次导航,未截图。
|
||
|
||
没有核验的:桌面镜像重建后的端到端(接管 → 画面里登录 → 交还回写)只在 mock 上测过;其它三站;sessionStorage / IndexedDB 登录的站点
|
||
(Cookie 注入不覆盖这类登录态)。站点账号列表里显示登录态来源待前端接 `userInfo.cookieOrigin`。
|
||
|
||
## 10. 重启恢复(BR-3)
|
||
|
||
camofox 容器重启(`docker compose up -d` 重建、OOM、手动 restart)后,上游的标签页全部消失:旧 tabId 返回 410 `browser_restarted`
|
||
(或 404),`GET /tabs` 为空。数据库里的会话还在,app 自动把它接回来,不需要关闭重开。代码:`modules/browser/service.ts`
|
||
(`checkInstance` / `reconnectIfNeeded` / `doReconnect`、`run` 里的自愈)、适配器 `reopenSession` / `instanceId`、
|
||
前端 `components/browser/reconnect.tsx`。
|
||
|
||
### 10.1 怎么发现
|
||
|
||
| 途径 | 做法 |
|
||
| --- | --- |
|
||
| 被动(总是生效) | 任一会话操作(截图、快照、导航、点击、输入、提取、下载、视觉动作)收到 404 / 410,或适配器里已没有打开的标签页 → 按 `GET /tabs` 对账;会话一个活标签页都不剩 → 标 `needsReconnect`(`trigger = tab_gone`)并立即重连 |
|
||
| 主动(需要 umt 插件) | 会话详情 / 操作前查实例标识(`GET /umt/instance`,插件不在时退回 `/health` 里的 `instanceId` / `bootId` / `startedAt`),每个浏览器服务 5 秒最多一次;和会话打开时记下的 `recovery_state.instanceId` 不同 → 该服务下全部未关闭会话标 `needsReconnect`(`trigger = provider_restarted`),下一次操作或原站视图触发重连 |
|
||
|
||
纯截图镜像 `camofox-browser:1.17.0` 不带插件,只有被动发现;桌面镜像(缺省)两种都有,桌面模式因此也能在不截图的情况下发现重启。
|
||
|
||
### 10.2 重连做什么
|
||
|
||
1. 按会话的站点 / 账号准备 Cookie(与打开会话同一套,§9.1:匿名 / API Key 类不注入;账密账号没有会话 Cookie 先 refresh-login);
|
||
2. 写进该 profile(`POST /umt/sessions/{userId}/cookies`),**再**用同一 `userId` / `sessionKey` 开新标签页;
|
||
3. 导航到会话记录的 `current_url`(不在站点域名内或是脱敏过的地址 → 站点首页);
|
||
4. 旧的打开中标签页标 `lost`,换当前标签页;`needsSnapshot = true`(旧元素引用作废),记 `recovery_state.instanceId` 与 `reconnect`;
|
||
5. 重试原操作**一次**。
|
||
|
||
同一会话并发的操作共用一次重连。控制者与租约不变:Agent 的会话仍归 Agent;人工接管中重连,租约保留,会话详情 `reconnect.humanNotice = true`,
|
||
原站视图提示"浏览器重启过,页面已重新加载,请核对"(表单里没提交的输入、滚动位置都没了)。
|
||
|
||
失败(camofox 还没起来、`POST /tabs` 5xx、浏览器服务停用等):会话保持 `needsReconnect`,`reconnect.status = failed` 记原因,
|
||
接口返回 **503 `browser_reconnect_failed`**(消息带原因,不含凭据)。之后 10 秒内的操作直接报上次的原因,不反复打上游;
|
||
显式 `POST /browser/sessions/{id}/reconnect` 不受这个限制。
|
||
|
||
### 10.3 接口与界面
|
||
|
||
| 端点 | 说明 |
|
||
| --- | --- |
|
||
| `POST /browser/sessions/{id}/reconnect` | 幂等:强制查一次实例标识;没标需要重连时先对账,当前标签页还活着就什么都不做;否则按 §10.2 重建。不要求接管。返回会话详情 |
|
||
| `GET /browser/sessions/{id}` | 多了 `needsReconnect` 与 `reconnect {status, trigger, at, reason, humanNotice}` |
|
||
|
||
原站视图:截图模式收到 404 / 409(已回收)/ 503,或会话详情 `needsReconnect = true` → 显示"浏览器已重启,正在重新连接…",
|
||
自动调一次 `reconnect`,期间停掉截图轮询;成功后继续轮询;失败显示原因与"重新连接"按钮,不再每 1.5 秒刷错误(浏览器控制台只打一次 debug)。
|
||
桌面模式:同样按 `needsReconnect` 自动重连一次,成功(或别处触发的重连成功)后重建 Selkies 连接(iframe 重新加载)。
|
||
|
||
### 10.4 核验
|
||
|
||
- 自动化:`apps/server/src/modules/browser/__tests__/browser-reconnect.test.ts`(mock camofox `restart()`:标签页清空、旧 tabId 410、
|
||
实例标识换新、可选丢 profile)——截图自愈并注入 Cookie、回到原地址;实例标识变化 + `reconnect` 幂等;接管中重连保持租约;
|
||
失败 503 与退避;前端冒烟"原站视图 → 浏览器重启后自动重连(mock)"。
|
||
- 真机:未对真实 camofox 容器重启核验。桌面镜像要重建后才有 `GET /umt/instance`(`docker compose build camofox && docker compose up -d camofox`);
|
||
重建前只有被动发现(截图 / 操作时自愈),桌面模式要等一次截图或 Agent 操作才会重连。
|
||
|
||
|
||
## 11. 浏览器身份、站点请求 UA 与出口(BR-4)
|
||
|
||
同一个站点账号的登录态在浏览器(camofox / Firefox)和 HTTP 端(Deno)之间互注(§9),所以两端要**看起来是同一台设备**:
|
||
同一个 UA、稳定的指纹、同一个出口 IP。用户决定(2026-09-30):对外伪装 **Windows**;全实例**一套固定指纹**;
|
||
浏览器与 HTTP **按站点统一出口**。调研见 `docs/design/browser-ua-research.md`。
|
||
|
||
代码:插件 `docker/browser-desktop/root/opt/camofox/plugins/umt-cookies/{index.js,identity.js}`;
|
||
`apps/server/src/modules/sources-identity/{runtime.ts,service/user-agent.ts,service/credentials.ts,service/account.ts}`;
|
||
`apps/server/src/modules/browser/service.ts`(`identityUserAgent` / `refreshIdentity` / `applyEgress`);迁移 `0056_browser_identity`。
|
||
|
||
### 11.1 固定浏览器身份(插件 `browser:launching`)
|
||
|
||
- **首次启动**:插件用 camoufox-js 的 `launchOptions({ os: "windows" })` 生成一份完整指纹——BrowserForge 的 Windows Firefox
|
||
指纹(navigator / screen / 硬件)、Camoufox 的 Windows 字族表、按 Windows 取样的 WebGL(vendor / renderer / 参数)、
|
||
字体间距 / 音频 / canvas 随机种子、`window.history.length`、canvas 抗锯齿偏移——去掉运行期字段(`humanize`、`locale:*`、
|
||
`geolocation:*`、`timezone`、`webrtc:*`、`addons`)后落盘 **`/root/.camofox/umt-identity.json`**(compose 的持久卷 `umt-camofox`,
|
||
与 profile 同一个卷;属主 abc、0600)。WebGL2 开关等配套 Firefox 首选项一起存。
|
||
- **之后每次启动**(容器重启、浏览器空闲 5 分钟退出后再启动):把上游随机生成的 config 里的指纹部分整份换成身份文件里的,
|
||
运行期字段仍取上游。浏览器进程日志 `umt identity applied {os, fingerprintId, revision}`。
|
||
- **升级**:Camoufox 版本或 Firefox 主版本与文件里记的不同 → 只改 `navigator.*` / `headers.*` 里的版本号(与 camoufox-js 生成时
|
||
同一个替换规则),新版本多出的指纹字段从同 OS 新样本补齐,其余一律保留;`revision + 1`,`fingerprintId` 不变
|
||
(日志 `umt identity upgraded`)。站点看到的是"同一台电脑升级了 Firefox"。
|
||
- **重置**(换一台"新设备",所有站点都会看到新设备):停 camofox,删 `umt-identity.json`,再启动。
|
||
- **开关与路径**:`UMT_BROWSER_IDENTITY=off` 关掉(回到上游每次随机的 Linux 指纹);`UMT_IDENTITY_FILE` / `UMT_STATE_DIR` 改路径。
|
||
文件损坏会改名 `.bad-<时间>` 后重新生成。
|
||
- **`GET /umt/identity`**(访问凭据保护):`{userAgent, os, platform, firefoxVersion, camoufoxVersion, fingerprintId, revision,
|
||
schema, createdAt, updatedAt}`,不含指纹细节。app 存进 `browser_provider.identity`(加 `fetchedAt`):打开会话时(10 分钟内读过就不读)、
|
||
重连时、设置页探测浏览器服务时读;站点请求取它时超过 6 小时会重读,读不到 5 分钟内不再试。
|
||
|
||
**Windows 伪装的一致性**(camoufox-js 0.11.5 源码 + 2026-09-30 本地临时容器实测,只开本地页面):
|
||
|
||
| 项 | 结果 |
|
||
| --- | --- |
|
||
| UA / 请求头 | `Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0`;HTTP `User-Agent` 头同值 |
|
||
| navigator | `platform = Win32`、`oscpu = Windows NT 10.0; Win64; x64`;并发数、语言等来自同一份 Windows 指纹 |
|
||
| WebGL | 按 Windows 取样:`Google Inc. (Intel)` / `ANGLE (Intel, Intel(R) HD Graphics 400 Direct3D11 vs_5_0 ps_5_0)`(D3D11,Windows 才有) |
|
||
| 字体 | 字族白名单 = Camoufox 的 Windows 字体表;字形来自 Camoufox 自带的 Windows 字体(`/opt/camoufox/fonts/windows`:Arial、Segoe UI、微软雅黑、宋体、等线……)。实测"微软雅黑"可用且宽度与回退字体不同,Noto / DejaVu 这类 Linux 字体不可见;中文页面以微软雅黑渲染 |
|
||
| 跨重启 | 重启容器后 `fingerprintId`、屏幕尺寸、WebGL 不变;模拟 140 → 152 升级只改了 UA 版本 |
|
||
|
||
发现并修正的上游问题:camoufox-js 0.11.5 把 `FONTCONFIG_PATH` 设成 `/opt/camoufox/fontconfig/lin`(不存在;Camoufox 152 的目录是
|
||
`linux` / `macos` / `windows`),而 fontconfig 里的字体目录写的是相对当前目录的 `fonts`(浏览器进程当前目录是 `/opt/camofox`)。
|
||
插件把 `fontconfig/windows/fonts.conf` 复制到临时目录、字体目录改成 `/opt/camoufox/fonts` 绝对路径,再指给 `FONTCONFIG_PATH`。
|
||
|
||
限制:渲染仍是 Linux 上的 Firefox——字体光栅化、canvas / 音频的像素级结果与真 Windows 不同(Camoufox 用固定种子加噪,稳定但不是
|
||
Windows 的真值);TLS / HTTP2 指纹是 Firefox 152 本身(与 OS 无关)。屏幕尺寸来自指纹(如 2560×1440),桌面模式窗口是 1280×900,
|
||
相当于没最大化的窗口。时区跟容器 `TZ`(缺省 Asia/Shanghai);语言跟指纹(缺省 en-US),要中文设 `CAMOFOX_LOCALE`。
|
||
指纹是进程级的:所有站点 / 账号共用这一套(用户决定;每账号一套要每账号一个浏览器进程,不做)。
|
||
|
||
### 11.2 站点请求 UA
|
||
|
||
站点请求的 UA 按账号解析,**优先级从高到低**:
|
||
|
||
| 来源(`SiteAccount.userAgentSource`) | 取值 | 界面 |
|
||
| --- | --- | --- |
|
||
| `manual` | 账号手动填的 UA(`site_account.user_agent`) | 手动填写 |
|
||
| `pending` | 待切换:取得当前登录态时用的旧 UA(`site_account.ua_pending`,§11.3) | 待切换:… |
|
||
| `global` | 设置 → 站点默认 → User-Agent(`settings.sites.userAgent`,非空时) | 使用设置里的全局 UA |
|
||
| `browser` | 浏览器固定身份的 UA(`browser_provider.identity.userAgent`);读不到时用常量 `FALLBACK_SITE_USER_AGENT`(同型 Windows Firefox 152) | 跟随浏览器 |
|
||
|
||
- 覆盖面:站点适配器(列表 / 搜索 / 详情 / 下载 .torrent / 健康检测)、RSS、账密登录、用户数据、Cookie 失效时的浏览器读回验证——
|
||
全部经 `SourcesRuntime.siteFetch`:请求自己没设 UA 就补上;适配器每条请求现场按"与站点凭证同一个账号"(第一个启用、未失效、非 Passkey)
|
||
取 UA,与 Cookie 同一时刻读库,不缓存。之前适配器与 RSS 发的是 Deno 缺省 UA `Deno/2.9.7`,已不再出现
|
||
(测试 `apps/server/test/sources-identity/user-agent.test.ts` 逐条断言)。不挂站点的请求(通用 RSS)用全局 UA / 跟随浏览器。
|
||
- 设置里的 User-Agent **缺省留空 = 跟随浏览器**(原缺省 Windows Chrome 140 已去掉);填了会让 HTTP 与浏览器 UA 不一致,只在确有需要时用。
|
||
- 账号"更新凭证"里的 UA 留空 = 不覆盖;Cookie 是从别的浏览器复制的、且站点校验 UA 时才填那个浏览器的 UA。
|
||
- 不在范围:TMDB、图片代理(不带凭据的公开请求)、下载器、浏览器服务本身。
|
||
- 升级 camofox 镜像(Firefox 大版本变)时:浏览器身份自动升级(§11.1),app 最迟 6 小时(或下次打开会话 / 重连)跟上;
|
||
同时把 `packages/contracts/src/settings.ts` 的 `FALLBACK_SITE_USER_AGENT` 改成新版本号。
|
||
|
||
### 11.3 平滑迁移(待切换)
|
||
|
||
改 UA 一般不会让 NexusPHP 的 Cookie 失效,但前置 Cloudflare / WAF 的站点可能要求重新过挑战。所以**不立即给存量账号换 UA**:
|
||
|
||
- 迁移 `0056_browser_identity`:Cookie / 账密账号(有凭证、没手动 UA)记 `ua_pending` = 取得当前登录态时的 UA——设置里改过全局 UA
|
||
就是那个值,否则是旧缺省 Chrome 140;API Key / Passkey 账号与网页登录态无关,直接跟随浏览器;设置里的 UA 仍是旧缺省值时删掉
|
||
(= 跟随浏览器),改过的保留为全局 UA。
|
||
- 待切换期间,这个账号的所有站点请求(包括原来发 Deno UA 的适配器 / RSS)都用旧 UA,与登录态一致。
|
||
- **切换时机**(Cookie 与 UA 同一事务写,下一条站点请求两者一起生效):
|
||
1. 重新登录:账密账号 refresh-login 走 takelogin 时用切换后的 UA,拿到会话 Cookie 后清 `ua_pending`;
|
||
2. 浏览器回写 Cookie(§9.2 的交还 / 关闭 / 定时 / refresh-login 读回):浏览器是用自己的 UA 取得 / 使用这份登录态的,回写时清 `ua_pending`;
|
||
Cookie 没变但浏览器不在登录页、正用着这份登录态,也清;
|
||
3. 换主凭证(填新 Cookie / 新密码):新登录态,直接跟随浏览器。
|
||
- Cookie 账号如果一直不开原站视图、也不换 Cookie,会一直待切换(界面写"待切换:下次刷新登录,或在原站视图里用过后改为跟随浏览器")。
|
||
刷新登录对 Cookie 账号只校验,不切换。
|
||
|
||
### 11.4 Cloudflare Cookie 仅浏览器(CF-2 起有条件给 HTTP)
|
||
|
||
`cf_clearance`、`__cf_bm`(及挑战过程中的 `cf_chl_*`)与取得它的浏览器绑定(UA + IP + TLS 指纹),Deno 的 TLS 指纹与 Firefox 必然不同,
|
||
HTTP 端拿去用多半无效还可能触发风控。所以:
|
||
|
||
- 浏览器回写账号时剔除这些 Cookie,只在 `site_account.user_info.browserOnlyCookies` 记名字(不含值);它们留在浏览器 profile 里照常用;
|
||
- 比较"有没有变化"时也不算它们(CF Cookie 轮换不会触发回写);refresh-login 从浏览器读回的 Cookie 同样剔除;
|
||
- 站点账号页技术信息里显示"仅浏览器使用的 Cookie";
|
||
- 用户手填的 Cookie 里带 `cf_clearance` 不处理(用户自己的选择)。
|
||
- **CF-2 例外**:站点遇 Cloudflare 挑战、浏览器过了之后,账号 UA 来源 = 跟随浏览器、浏览器 UA 与 HTTP UA 相同、两端出口相同时,
|
||
`cf_clearance`(只有它,`__cf_bm` 仍不带)单独存进 `site_account.user_info.cloudflare`(值在 secret)并由 HTTP 带上重试 1 次,
|
||
结果按站点记 `cfReuse`;普通回写仍剔除它(见 sites.md「Cloudflare(CF-2)」)。
|
||
|
||
### 11.5 出口 IP 按站点统一
|
||
|
||
实测(本地临时容器):**Firefox 会用容器的 `HTTP_PROXY` / `HTTPS_PROXY` 环境变量**(Firefox 缺省"使用系统代理",Linux 下没有
|
||
GSettings 就读环境变量)。所以本机栈此前的状态是:`docker-compose.override.yml` 给 camofox 设了 `HTTP_PROXY=http://172.17.0.1:7891`,
|
||
浏览器**所有**站点都走代理;HTTP 端只有开了"使用代理"的站点走代理——同一账号两端出口 IP 不同。
|
||
|
||
现在(`UMT_BROWSER_PROXY_MODE=site`,缺省):
|
||
|
||
- 插件在启动时设 `network.proxy.type = 0`:浏览器**不再用**容器环境变量里的代理,缺省直连;
|
||
- app 在打开会话 / 重连前调 `PUT /umt/sessions/{userId}/proxy`:站点开了"使用代理"且设置了代理地址 → 该站点 profile 的
|
||
BrowserContext 走同一个代理(Playwright 按上下文代理,Firefox / Juggler 原生支持;URL 里的账号口令拆成 username / password);
|
||
否则 `null` 直连。插件把这张表存在 `/root/.camofox/umt-proxies.json`(0600,代理带口令时含口令),camofox 重启后照用;
|
||
- 该 profile 已有活动上下文、出口不同 → 插件先关掉它(持久化插件先落盘登录状态),下一次操作按新出口重建;
|
||
- 实测:直连 profile 访问测试域名走不到环境变量里的代理;代理 profile 的请求到了指定代理;改出口时活动上下文被重建。
|
||
|
||
限制与注意:
|
||
|
||
- **代理地址要让 app 与 camofox 两个容器都能连**(如本机的 socat 中继 `http://172.17.0.1:7891` 可以;`http://127.0.0.1:7890` 在
|
||
camofox 容器里连不到)。
|
||
- 站点"使用代理"开关或代理地址改了,已打开的浏览器会话在**下一次打开 / 重连**时才按新出口;要立即生效就关掉会话重开。
|
||
- 匿名会话与 Agent 打开的站点会话同样按站点出口(浏览器会话都属于某个站点)。
|
||
- 旧镜像 / 插件没有出口接口:app 只记日志,浏览器照旧按环境变量出网。`UMT_BROWSER_PROXY_MODE=env` 可退回旧行为。
|
||
- SOCKS 代理的 DNS 解析位置未核验(HTTP 代理由代理端解析,与 Deno 的代理客户端一致)。
|
||
- 本机栈重建 camofox 后,憨憨(未开代理)的浏览器会话会从"经代理"变为**直连**,与 HTTP 端一致;override 里 camofox 的
|
||
`HTTP_PROXY` 可以留着(Firefox 不再用它),也可以删掉。
|
||
|
||
### 11.6 上线步骤与核验
|
||
|
||
需要重建 **app**(代码 + 迁移 0056)和 **camofox**(插件):
|
||
|
||
```sh
|
||
docker compose --env-file .env.deploy build app camofox
|
||
docker compose --env-file .env.deploy up -d app camofox
|
||
docker compose exec camofox curl -s -H "Authorization: Bearer $CAMOFOX_ACCESS_KEY" http://127.0.0.1:9377/umt/identity
|
||
```
|
||
|
||
camofox 镜像只有 `COPY root/ /` 与其后改插件清单的一层变化;前面的 apt 层与从 `camofox-browser:1.17.0` 复制 node / camofox /
|
||
Camoufox 浏览器包的层都走构建缓存,**不会重新下载 Camoufox(约 630 MB)**。2026-09-30 在本机用临时标签实测:带
|
||
`--build-arg APT_MIRROR=mirrors.ustc.edu.cn`(与 `.env.deploy` 相同)时 apt / 复制层全部 `CACHED`,只重跑插件两层(< 1 秒)。
|
||
注意**构建参数要与上次一致**(用同一个 `--env-file .env.deploy`):不带 `APT_MIRROR` 构建会让 apt 层缓存失效,重新 apt 安装(浏览器包
|
||
仍从本地 `camofox-browser:1.17.0` 复制,不从网上下)。首次启动生成身份,之后 profile 卷里的登录状态照常可用。
|
||
|
||
核验:`apps/server/test/sources-identity/user-agent.test.ts`(每条出站请求的 UA、优先级、待切换、CF Cookie)、
|
||
`migration-browser-identity.test.ts`、`apps/server/src/modules/browser/__tests__/browser-identity.test.ts`(出口下发、身份存库)、
|
||
`packages/adapters/src/browser/umt-plugin.test.ts`(插件纯函数、适配器接口);真机:本地临时容器(`--network none`,只开容器内本地
|
||
页面)核验了 §11.1 表格与 §11.5 出口行为,未对真实站点发请求。
|
||
|
||
## 12. 会话保活(AG-4)
|
||
|
||
camofox 按 **API 调用**判断活动,有三道空闲回收(`docker-compose.yml` 的 camofox 环境变量):
|
||
|
||
| 回收 | 缺省 | 判据 |
|
||
| --- | --- | --- |
|
||
| 标签页 `TAB_INACTIVITY_MS` | 5 分钟 | 每 60 秒看一次标签页的 `toolCalls` 有没有变;没变且超过 5 分钟 → 关标签页,会话空了连会话一起关 |
|
||
| 会话 `SESSION_TIMEOUT_MS` | 30 分钟 | 会话 `lastAccess`(大部分 `/tabs/{id}/*` 接口都会刷新) |
|
||
| 浏览器 `BROWSER_IDLE_TIMEOUT_MS` | 5 分钟 | 一个会话都没有之后浏览器进程退出 |
|
||
|
||
桌面模式里人的键鼠直接进 Selkies,原站视图截图(`GET /tabs/{id}/screenshot`)只刷新 `lastAccess`、不加 `toolCalls`——
|
||
接管 5 分钟后标签页被关掉,桌面画面里的窗口消失。app 在"正在用"的会话上定期保活,代码 `modules/browser/keepalive.ts`、
|
||
`BrowserService.keepAlive`、适配器 `keepAlive`。
|
||
|
||
**保活调用**:camofox 1.17 没有 keepalive / touch 接口;`GET /tabs/{id}/stats` 只刷新 `lastAccess`,挡不住标签页回收。
|
||
用 `GET /tabs/{id}/downloads?userId=…`(不带 `includeData` / `consume`):只列下载元数据,不读页面、不改页面、不清下载记录,
|
||
会让 `toolCalls` 加一、刷新 `lastAccess`。只对当前标签页做,不要求控制权,不记 `lastAutomationAt`。
|
||
|
||
**什么时候保活**(进程内登记,各进程保活自己登记的):
|
||
|
||
| 使用方 | 开始 | 停止 |
|
||
| --- | --- | --- |
|
||
| 桌面连接 | 桌面模式 WebSocket 建立(控制或只读都算,多个连接按引用计数) | 全部连接断开 |
|
||
| 人工接管 | `control/acquire` | 交还(`control/release`)、租约过期、会话关闭 |
|
||
| 智能体会话 | 智能体会话里的浏览器能力调用(`browser.*`,含 `browser.navigate` 新开会话) | 智能体会话不再是排队 / 运行中(结束、等待下载或等用户),或最后一次使用后 30 分钟 |
|
||
|
||
每 `BROWSER_KEEPALIVE_MS`(缺省 120000 = 2 分钟,0 = 关闭;要比 `TAB_INACTIVITY_MS` 短)对登记的会话各做一次;
|
||
每轮先复查登记是否还有效,没有使用方的会话不再保活,照常由 camofox 空闲回收。会话关闭(`DELETE`)立即撤销全部登记。
|
||
|
||
**保活失败**:保活走与其它动作相同的 `run()` 路径——浏览器重启过(实例标识变了)或标签页已被回收(404 / 410、适配器里没有打开的标签页)
|
||
→ 按 §10 自愈重连(注入 Cookie、同一会话新开标签页回到原地址)后在新标签页上再保活一次;重连也失败(浏览器服务不可用)只记 warn 日志
|
||
「浏览器会话保活失败」,会话照常标 `needsReconnect` / `reconnect.status = failed`,下一轮再试。
|
||
|
||
核验:`apps/server/src/modules/browser/__tests__/browser-keepalive.test.ts`(登记 / 引用计数 / 复查;对 mock camofox:接管期间保活、
|
||
不记自动化活动、交还后停;桌面连接期间标签页被回收 → 保活时自愈重连;智能体会话结束后停、会话关闭撤销)。真机未核验。
|
||
|
||
## 13. Cloudflare 挑战(CF-2)
|
||
|
||
站点 HTTP 遇挑战时由站点请求一侧(sources-identity `CloudflareGate`)经钩子调用浏览器模块,流程与状态见 sites.md「Cloudflare(CF-2)」。
|
||
浏览器这边:
|
||
|
||
- `solveSiteChallenge({siteId, accountId, url})`:该账号 profile 的自动化会话(没有就按站点打开;人工接管中 / 等人处理中不打扰)导航到
|
||
同一地址(取站点令牌)→ 用 `POST /tabs/{id}/evaluate` 读页面(地址、标题、outerHTML、正文样本、`navigator.userAgent`,适配器
|
||
`pageContent`)按 `classifyChallengePage` 分类:自动(JS 检查)/ 可点(Turnstile、「确认您是真人」)/ 真验证码(hCaptcha、reCAPTCHA、
|
||
选图、输码)→ ① 每 1.5 秒读一次,最多 15 秒 → ② 可点的派视觉子流程(`browser.solve_challenge`,系统发起:发起人 `system:cloudflare`、
|
||
trigger event、没有父会话;步数 4、时长 ≤ 120 秒、token / 费用取 `agent.maxTokensPerRun` / `maxCostUsdPerRun`;最多 2 次;
|
||
自动化总开关关闭或只读对账时不派)→ ③ 没过:会话转等待 `cf_challenge`,待处理事项「<站点>需要你在原站视图里过一次验证」
|
||
(视觉子流程失败留下的 `challenge_unsolved` 事项改成这个说法)。通过后读回 profile 里的 `cf_clearance`、UA 与会话出口(`recovery_state.egress`,
|
||
插件按站点下发出口成功才有值)。
|
||
- `fetchSiteHtml({siteId, accountId, url})`:同一会话导航(取站点令牌)后读 outerHTML;仍是挑战页 / 会话在人工处理 / 站点停用时不返回页面。
|
||
- 同一 profile 的过挑战与代抓在进程内串行(profile 锁);跨进程同站 30 分钟一次靠站点状态的条件更新。
|
||
- 交还会话(给自动化)时:站点在等人过验证 → 读回 `cf_clearance` / UA 通知站点请求继续(`CloudflareGate.afterHuman`),待处理事项随交还关闭。
|
||
- 插件新端点 `POST /umt/tabs/{tabId}/mouse-click`:`click_at`(与原站视图截图模式的坐标点击)都先走它。**需要重建 camofox 镜像**才有
|
||
(`docker compose build camofox && docker compose up -d camofox`,只重跑插件层);没重建时退回 elementFromPoint,Turnstile 点不到,
|
||
可点类挑战会转人工。
|
||
|
||
核验:mock camofox(`cloudflare` 模式:auto / interactive / captcha)见 `browser-cloudflare.test.ts`、`umt-plugin.test.ts`、E14。
|
||
插件鼠标点击的真实容器核验见下表(本地临时容器,只开本地页面,不访问真实验证页)。
|
||
|
||
| 日期 | 做法 | 结果 |
|
||
| --- | --- | --- |
|
||
| 2026-09-30 | 临时容器 `umt-cf2-camofox`(镜像 `umt-browser-desktop:1.17.0`,只读挂载新插件目录),容器内 node 起两个本地页面:父页 `localhost:8001`(标题 Just a moment...)内嵌**跨域** iframe `localhost:8002`(复选框,`change` 事件 `isTrusted` 才向父页 postMessage);适配器 `openSession` → `pageContent` → `click({x, y})` → `pageContent` | 插件路径:`POST /umt/tabs/{id}/mouse-click` 命中复选框,iframe 里收到可信事件,父页标题变为 passed;`pageContent` 读到 UA `…Windows NT 10.0; Win64; x64; rv:152.0…`。旧路径(elementFromPoint)只能标到 iframe 元素本身、按元素中心点击,点不到指定坐标。未访问任何外部站点,容器已删除 |
|