16 KiB
浏览器容器控制面
当前实现说明,核对基线
main@1fbf126;不是新业务完成证明。目标范围与验收以 plan01 为准,运行步骤见部署说明。现状限制不自动成为新产品约束。
技术选型
- 前端:React 19 + Vite 8 + Refine + shadcn/ui + Tailwind CSS,图标 RemixIcon;项目自有 Layout 与 HashRouter,当前页面见 main.jsx,依赖见 package.json。
- 后端:Go 模块化单体,Fiber v3 提供 HTTP 路由,Viper 读取并校验启动配置,Logrus 输出 JSON 结构化日志,Cobra 保持当前两个服务入口。控制面提供同源 API 和静态文件,并编排网关;受限网关单独封装 Docker Engine API,是纯执行器。
- 数据:环境配置(别名、中文名、网关、镜像版本、指纹参数)持久化在 Postgres,列表/详情读取持久运行记录,不触发网关实时探测;后台独立维护运行租约;Profile 使用命名卷持久化;阶段 A 账号、凭据引用、确认、任务、尝试和审计实体同样由控制面持久化到 Postgres。
- 部署:Docker Compose 启动控制面和受限网关;浏览器容器由网关按平台下发的镜像引用动态创建,缺失时自动拉取。
调用链与契约
React ── /api/* (Basic Auth) ──> control-plane
├─ /v1/browsers (Bearer token) ──> docker-gateway ──> docker.sock
│ └─> browser container
├─ /v1/browsers/.../douyin/events (Bearer token) ──> docker-gateway ──> browser event stream
└─ 账号/环境/任务/互动事件持久记录 ──> PostgreSQL
控制面是唯一事实源:网关不持有镜像清单和业务规则,镜像引用、启动命令和卷名均随请求下发。
| 接口/动作 | 当前行为与失败语义 |
|---|---|
POST /api/browsers / create |
严格接收 {alias, name, gateway, image_version, fingerprint, account_id, network_exit_id},未知字段拒绝;account_id 必填,network_exit_id 可空表示明确选择直连。新绑定要求账号 authorized/paused、镜像启用,指定出口须健康并再次核验。先保存环境与稳定 binding,创建停止态容器;不是创建即启动。相同绑定及配置可复用,冲突拒绝;已存在且账号可运行的环境可调和/恢复运行。网关失败不删除已保存环境/binding,保留以供核验/恢复 |
GET /api/browsers、GET /api/browsers/{alias} |
只读数据库环境/运行记录;列表的 running 表示已记录运行实例,不保证实时存活。页面可直接展示返回 status,不主动探测或轮询。后台租约调和与列表读取独立 |
POST /api/browsers/{alias}/start |
要求账号可运行;有出口时重新核验,不健康则失败而非直连。当前匹配且就绪的运行容器可复用;停止/缺失容器按当前 binding 重建并激活租约;支持预先选定的直连 |
POST /api/browsers/{alias}/stop |
停止容器并处理租约/清理状态;失败或结果不明可见,不据请求发出即声称已停止 |
POST /api/browsers/{alias}/upgrade |
接收 {version},要求启用镜像;删除旧容器并保留 Profile,更新版本后按原指纹/binding 重建,账号可运行才启动,否则为停止态。无自动回滚;失败保留结果,人工重试进入现有调和流程。当前实现无条件核验出口,空出口的直连环境不能据 create/start 成功推断 upgrade 可用 |
POST /api/browsers/{alias}/rebind |
接收 {network_exit_id};要求 paused、无 executing task、无活动 runtime,验证目标出口后变更。当前要求有效出口 ID,不支持以空值切回直连 |
DELETE /api/browsers/{alias} / recycle |
回收容器、处理运行记录,但保留环境、稳定 binding 与命名 Profile 卷;后续 create/start 复用。不是永久删除账号/环境或素材;没有新增永久删除 API |
name 是环境展示名,alias 限 ^[a-z0-9][a-z0-9-]{0,31}$;容器名 creatorhub-browser-<alias>,Profile 卷 creatorhub-profile-<alias>。指纹中的代理字段拒绝,使用所选出口;已绑定代理失败不能静默直连。create/start/stop/upgrade/recycle 写同一 operation ID 的 requested/finished 审计对,网关断连且无法调和时 outcome 为 unknown;幂等/重试是生命周期核验,不等于 plan01 中人工/自动业务发送可重发。
GET/POST /api/browser-images维护可用镜像版本(版本号不可改,PUT /{version}仅接受image_ref/note/enabled);仅启用版本可用于创建与升级;被环境引用时拒绝删除。GET/POST /api/gateways注册网关(POST可携带令牌,否则平台生成 48 位十六进制令牌并明文存储),DELETE /api/gateways/{name}删除;仍被环境引用时拒绝删除。- 别名唯一约束由数据库保证;控制面不自动重试结果不明的用户生命周期请求,后台租约恢复的现状另见下文。
docker.sock 安全边界
将 socket 以只读文件挂载不会限制 Docker API 的写操作;拥有 socket 等价于拥有宿主机 root 权限。因此:
- 只有
docker-gateway挂载 socket,控制面和浏览器容器均不可见;网关加入 control 并按需接入浏览器隔离网络,浏览器在代理模式只拿到无凭据的内存转发代理地址,/v1仍必须通过容器内不可见的网关令牌; - 网关只暴露面向领域的路由,不提供通用 Docker 代理;
/v1全部接口校验Authorization: Bearer <GATEWAY_TOKEN>(常数时间比较),令牌由部署者在网关环境变量与平台注册表中保持一致; - 网关直连的
POST /v1/browsers/{alias}/start|stop仅供内部维护使用,必须提交并精确匹配容器标签中的{binding_version,runtime_id,network_id};直连 start 拒绝空network_id,generation 不匹配返回409,控制面生命周期编排不依赖无 fence 的直连 start; - 网关恢复内存代理时会同时 fence 隔离网络成员及网关成员 IPv4,地址变化返回
409并关闭刚恢复的监听;代理移除或代际替换会立即关闭所有已 hijack 的 CONNECT 双向连接,任一端先关闭也会关闭隧道两端,不等待优雅 drain; - 网关固定命令、网络、挂载和资源限制;外部输入是受校验的别名,以及平台下发的镜像引用、启动参数和卷名——镜像引用来自平台维护的版本表,新增/变更由人工在页面审核启用,不再写死在代码中;
- 启停和删除前必须同时匹配固定名称前缀及
io.creatorhub.managed、io.creatorhub.runtime-id标签; - 动态容器使用只读根文件系统、非 root
1000:1000与固定镜像入口、全部 capability drop、no-new-privileges、CPU/内存/PID 限制,且无宿主机端口和目录挂载; - 控制面发布到宿主机所有网卡;控制网络为固定名称的 Compose 网络;浏览器 bridge 按 ownership、role、driver、Internal 失败关闭校验,且拒绝复用 control 网络;
- Compose 基础镜像锁定 digest;浏览器镜像推荐使用
@sha256:摘要引用以获得不可变性,tag 引用由部署者自行把控。
网关自身一旦被攻破,socket 仍允许接管宿主机;应用内校验不能消除这个平台级风险。开发目标不新增认证或访问限制,安全由部署者自行把控;但当前代码仍强制 HTTP Basic Auth(除 /healthz 外的 API 与静态页面),Compose 仍要求用户名/密码。见 main.go 与 compose.yaml;本轮未移除现有策略,也不新增 RBAC 或认证 profile。
运行
使用部署说明中的完整变量及启动命令(含 Basic Auth、凭据主密钥与 socket GID),不维护另一套省略必填配置的命令。首次注册网关和镜像、创建账号、再创建停止态环境;恢复账号后显式启动。网关拉取镜像上限约 10 分钟;失败查看审计和保留的环境,不能按“数据库已回滚”直接假定没有资源。
现有账号与阶段 A 离线闭环
本节描述已运行的基础及历史 schema,不规定 plan01 的新业务范围。旧阶段 A 的 Mock 任务验证不等于 G0 平台能力或 G1 抖音完整验收;当前抖音受限读取连接器也未接成完整竞品/监听/发送流程。
POST /api/phase-a/accounts 只接受 {name, platform, platform_account_key, tags, cookies};platform 限定为 douyin、xiaohongshu、wechat-official、kuaishou,cookies 可空,非空时须为浏览器 Cookie Header 格式。控制面通过持久 provider bridge 安全写入凭据:部署侧 Secret Manager/OS Keyring 注入 32 字节主密钥,独立凭据卷只保存 AES-GCM 密文,数据库只记录凭据引用;外部 API 不返回 Cookies、provider 或 reference_key。数据库明确回滚时清理凭据,提交结果未知时保留凭据并返回 account_creation_result_unknown,不自动破坏可能已提交的账号。内部账号 ID 由服务端生成,新账号默认 paused,(platform, platform_account_key) 全局唯一。pause/revoke 会递增账号版本并将 queued 任务置为 policy_hold,resume 要求稳定 binding,若绑定出口则需 healthy,并满足无活动 runtime/待清理等条件;未绑定出口的显式直连不要求出口记录。账号与浏览器环境通过一对一 environment_binding 关联,出口可复用;运行实例保留历史,并以 binding 和外部 runtime id 的部分唯一索引限制活动实例。
草稿经 POST /api/phase-a/confirmations 显式确认后才可投递到 /api/phase-a/tasks。任务由幂等键去重;POST /api/phase-a/mock/execute 使用 FOR UPDATE SKIP LOCKED 领取一分钟租约,执行前统一核对账号、草稿和确认版本。缺少确认或版本不一致会进入 needs_confirmation,暂停账号或 Mock 策略结果会进入 policy_hold,不确定结果与过期租约进入 needs_confirmation;这些状态都不会自动重试。GET /api/phase-a/audit 只导出账号、确认版本、尝试和结果等非秘密证据。
启动时控制面先应用 Phase A v1,再由 Hub runner 顺序应用 v2 至 v14;CreatorHub 业务迁移继续顺序应用至 v26(竞品同步 lease token、事件消息正文);每一步都在事务和 advisory lock 下前向执行。v3 保留旧表、列和历史记录,旧账号回填为 platform=mock 并暂停,仅账号 ID 与环境 alias 相同的记录自动建立 binding;v4 追加环境动作审计字段与索引,v5 清理持久 fingerprint 中的旧代理字段,v6 增加可重试的 runtime cleanup 状态,v7 为 runtime lease 增加 binding version 并回填可确定的既有记录,v8 至 v10 补齐 cleanup/runtime 的不可变 generation 与兼容约束,v11、v12 增加任务恢复状态并修复兼容约束,v13 增加账号名称和 TAGS;v14 仅前向修复旧 PR v13 的空数据 schema。若旧 v13 已产生空引用或明文 Cookies,v14 会在删除前阻断启动,必须先将凭据迁入 provider。其余记录等待显式绑定。本阶段不提供破坏性自动回滚。
POST /api/network-exits 只接受协议、主机、端口、已有 credential_reference: {id} 和预期出口身份;新出口为 unchecked,由 POST /api/network-exits/:id/check 经实际代理链路变为 healthy 或 unhealthy,disable 不可被检查重新启用。credential reference 的 reference_key 不出现在 API、日志或审计中;OS Keyring/Secret Manager bridge 在控制面进程启动前注入 CREATORHUB_CREDENTIAL_<SHA256(reference_key)>(大写十六进制),值为请求期解析的 username:password,控制面不持久化解析值。
环境创建/启动/回收及直连边界以上方生命周期表为唯一说明。已配置代理时由控制面下发代理信息与 disable_non_proxied_udp;直连并非代理失败后的替代路径。
解析后的出口凭据只存在于控制面单次请求和网关内存转发器中;Docker inspect、容器环境、标签、挂载、Config.Cmd 与进程参数只包含 docker-gateway 的无凭据本地代理地址。网关内存代理以 alias、binding version 和 exit ID 共同标识 generation;生命周期操作按 alias 串行,重启恢复或重建必须重新核对该 generation,旧出口代理不能被新容器复用。
控制面后台每 20 秒调和网关(runtimeLeaseHeartbeat),续租 running runtime、释放 stopped/missing runtime;列表/详情读取不触发调和,过期 lease 也会在绑定事务中回收。控制面还按账号调和抖音事件监听(creator_events.go):每个已授权且有有效运行代际的账号只有一个监听,网关断连或代际改变时停止旧监听并退避重连;事件通知在控制面转换为互动事件后进入 ProcessAutomaticEvent,数据库的事件唯一键负责去重,写入结果不明不会由监听器重复发送。当前通知边界、平台事件游标/基线连续性和真实写操作仍需真机证据,不能把网关轮询队列视为平台监听验收。控制面用 PostgreSQL advisory transaction lock 按 alias 协调多副本;每个 Store 最多允许 5 个锁会话占用 10 连接池的一半,为锁内数据库调用保留连接。create 同时锁定账号 ID、alias、请求出口和请求镜像;start、reconcile/rebuild、rebind 和 upgrade 锁定 alias、当前出口及当前镜像(upgrade 还锁目标镜像),拿锁后重新读取出口与镜像版本。账号 pause/resume/revoke 使用账号 ID 与当前 binding alias 加入同一协调域;镜像禁用、引用更新或账号状态变更不能穿透在途生命周期。
非法 upgrade/rebind 目标在进入 advisory lock key 前按公开格式校验;审计仅保留环境原有的非秘密资源关联,并以 upgrade_input_rejected / rebind_input_rejected 写同一 operation ID 的 requested/finished 对。reconcile 恢复或重建后会重新读取 context,finished 事件关联实际激活的 runtime instance、binding version 与出口;后台释放 runtime 的成功或失败也写独立的 reconcile 审计对。
与新业务计划的边界
- 当前受限抖音读取仅允许自身身份与
count=20/max_cursor=0首批作品,JSON 响应限制为 1 MiB,见 douyin.py。竞品同步仍需外部平台证据;媒体通过POST /api/creator/works/:id/material/process下载到持久卷、用 FFmpeg 提取音轨,再调用显式配置的CREATOR_TRANSCRIPTION_BIN。未配置转写入口时明确记为 failed,不接受手写 succeeded 或二进制塞入通用 JSON。 - 当前账号凭据入口处理 Cookie;登录密码可按现有凭据保存流程保存,读取不回显、不进入日志,但绝不由系统自动注入或用于绕过人工登录。
- 新业务平台监听、前端业务推送和现有后台运行租约是三件事;前两者要求见 plan01 A6,列表不主动探测的约定不禁止业务事件推送。现有生命周期/租约可能核验出口,不应误写成已完成 plan01 的手动代理管理目标。