feat: continue CreatorHub plan01 implementation

This commit is contained in:
2026-09-12 20:17:29 +08:00
parent 1fbf126b21
commit 860ecaf7ea
52 changed files with 9441 additions and 269 deletions
+37 -26
View File
@@ -1,36 +1,48 @@
# 浏览器容器控制面
> 当前实现说明,核对基线 `main@1fbf126`;不是新业务完成证明。目标范围与验收以 [plan01](../plan01.md) 为准,运行步骤见[部署说明](../deployment.md)。现状限制不自动成为新产品约束。
## 技术选型
- 前端:React 19 + react-adminra-core+ MUI,包含运行环境、镜像版本、网关管理三个页面
- 前端:React 19 + Vite 8 + Refine + shadcn/ui + Tailwind CSS,图标 RemixIcon;项目自有 Layout 与 HashRouter,当前页面见 [main.jsx](../../web/src/main.jsx),依赖见 [package.json](../../web/package.json)
- 后端:Go 模块化单体,Fiber v3 提供 HTTP 路由,Viper 读取并校验启动配置,Logrus 输出 JSON 结构化日志,Cobra 保持当前两个服务入口。控制面提供同源 API 和静态文件,并编排网关;受限网关单独封装 Docker Engine API,是纯执行器。
- 数据:环境配置(别名、中文名、网关、镜像版本、指纹参数)持久化在 Postgres,运行态实时查询网关;Profile 使用命名卷持久化;阶段 A 账号、凭据引用、确认、任务、尝试和审计实体同样由控制面持久化到 Postgres。
- 数据:环境配置(别名、中文名、网关、镜像版本、指纹参数)持久化在 Postgres,列表/详情读取持久运行记录,不触发网关实时探测;后台独立维护运行租约;Profile 使用命名卷持久化;阶段 A 账号、凭据引用、确认、任务、尝试和审计实体同样由控制面持久化到 Postgres。
- 部署:Docker Compose 启动控制面和受限网关;浏览器容器由网关按平台下发的镜像引用动态创建,缺失时自动拉取。
## 调用链与契约
```text
React ──> control-plane ── /api/browsers ──(Bearer token)──> docker-gateway ──> docker.sock
│ │
└─> browser container
└─ /api/phase-a, /api/browser-images, /api/gateways ──> PostgreSQL
React ── /api/* (Basic Auth) ──> control-plane
├─ /v1/browsers (Bearer token) ──> docker-gateway ──> docker.sock
└─> browser container
└─ 账号/环境/任务等持久记录 ──> PostgreSQL
```
控制面是唯一事实源:网关不持有镜像清单和业务规则,镜像引用、启动命令和卷名均随请求下发。
- `POST /api/browsers` 接受 `{alias, name, gateway, image_version, fingerprint}`(严格 JSON,未知字段拒绝),校验后先落库,再调网关创建并启动;网关失败时回滚数据库行。`name` 为中文环境名,`alias``^[a-z0-9][a-z0-9-]{0,31}$`,容器名 `creatorhub-browser-<alias>`Profile 卷 `creatorhub-profile-<alias>`
- `GET /api/browsers` 合并数据库环境与网关实时状态;环境在网关无容器时显示为未部署。
- `POST /api/browsers/{alias}/start|stop` 改变状态;`POST /api/browsers/{alias}/upgrade``{version}`,由平台编排:停止并删除旧容器(保留 Profile 卷)→ 用新镜像引用与原指纹参数重建 → 启动;失败直接重试,不做自动回滚。
- `DELETE /api/browsers/{alias}` 回收容器并删除数据库行,保留 Profile 数据卷。
当前生命周期契约集中如下(代码:[控制面](../../cmd/control-plane/hub.go)、[环境存储](../../internal/hub/environment.go)):
| 接口/动作 | 当前行为与失败语义 |
| --- | --- |
| `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 与 browser 网络,浏览器只拿到无凭据的内存转发代理地址,`/v1` 仍必须通过容器内不可见的网关令牌;
- 只有 `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;
@@ -40,24 +52,17 @@ React ──> control-plane ── /api/browsers ──(Bearer token)──> doc
- 控制面发布到宿主机所有网卡;控制网络为固定名称的 Compose 网络;浏览器 bridge 按 ownership、role、driver、Internal 失败关闭校验,且拒绝复用 control 网络;
- Compose 基础镜像锁定 digest;浏览器镜像推荐使用 `@sha256:` 摘要引用以获得不可变性,tag 引用由部署者自行把控。
网关自身一旦被攻破,socket 仍允许接管宿主机;应用内校验不能消除这个平台级风险。开发阶段控制面不做认证或访问限制,安全由部署者自行把控。
网关自身一旦被攻破,socket 仍允许接管宿主机;应用内校验不能消除这个平台级风险。开发目标不新增认证或访问限制,安全由部署者自行把控;但当前代码仍强制 HTTP Basic Auth(除 `/healthz` 外的 API 与静态页面),Compose 仍要求用户名/密码。见 [main.go](../../cmd/control-plane/main.go) 与 [compose.yaml](../../compose.yaml);本轮未移除现有策略,也不新增 RBAC 或认证 profile
## 运行
Docker socket GID 因宿主机而异:
使用[部署说明](../deployment.md)中的完整变量及启动命令(含 Basic Auth、凭据主密钥与 socket GID),不维护另一套省略必填配置的命令。首次注册网关和镜像、创建账号、再创建停止态环境;恢复账号后显式启动。网关拉取镜像上限约 10 分钟;失败查看审计和保留的环境,不能按“数据库已回滚”直接假定没有资源。
```bash
export GATEWAY_TOKEN="$(openssl rand -hex 24)" # 亦可在 .env 中设置
DOCKER_GID=$(stat -c %g /var/run/docker.sock) docker compose up --build
```
## 现有账号与阶段 A 离线闭环
打开 <http://127.0.0.1:8080>,局域网内用宿主机 IP 访问同一端口。首次使用:在「网关管理」用 `GATEWAY_TOKEN` 注册 `http://docker-gateway:8081`,在「镜像版本」添加镜像引用(缺失时网关自动拉取,拉取上限 10 分钟)。浏览器容器可访问外网
本节描述已运行的基础及历史 schema,不规定 plan01 的新业务范围。旧阶段 A 的 Mock 任务验证不等于 G0 平台能力或 G1 抖音完整验收;当前抖音受限读取连接器也未接成完整竞品/监听/发送流程
创建成功但启动失败时,网关会立即删除失败容器并保留命名 Profile 卷,控制面回滚数据库行,允许同名请求安全重试
## 阶段 A 离线闭环
`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`,只有具备 binding 和 healthy 出口的未撤销账号才能 resume。账号与浏览器环境通过一对一 `environment_binding` 关联,出口可复用;运行实例保留历史,并以 binding 和外部 runtime id 的部分唯一索引限制活动实例。
`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` 只导出账号、确认版本、尝试和结果等非秘密证据。
@@ -65,10 +70,16 @@ DOCKER_GID=$(stat -c %g /var/run/docker.sock) docker compose up --build
`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`,控制面不持久化解析值。
`POST /api/browsers` 必须同时给出 `account_id``network_exit_id`环境创建启动和升级都会重新检查出口身份,只有 `healthy` 才调用网关;控制面强制下发代理 `disable_non_proxied_udp`fingerprint 中的代理字段会被拒绝。显式 `POST /api/browsers/:alias/rebind` 只允许 paused、无 executing task 且无活动 runtime 的账号。`DELETE /api/browsers/:alias` 回收容器但保留稳定 binding、环境和命名 Profile 卷,后续 create 复用它们。create/start/stop/upgrade/recycle 均写共享 operation ID 的 requested/finished 审计对;网关断连且无法调和时 outcome 为 `unknown`
环境创建/启动/回收及直连边界以上方生命周期表为唯一说明。已配置代理时由控制面下发代理信息与 `disable_non_proxied_udp`;直连并非代理失败后的替代路径
解析后的出口凭据只存在于控制面单次请求和网关内存转发器中;Docker inspect、容器环境、标签、挂载、`Config.Cmd` 与进程参数只包含 `docker-gateway` 的无凭据本地代理地址。网关内存代理以 alias、binding version 和 exit ID 共同标识 generation;生命周期操作按 alias 串行,重启恢复或重建必须重新核对该 generation,旧出口代理不能被新容器复用。
stopped 环境启动时先删除旧容器并确认 runtime lease 释放,再按当前 binding 重建;控制面每 20 秒及列表读取时调和网关,续租 running runtime、释放 stopped/missing runtime,过期 lease 也会在绑定事务中回收。控制面用 PostgreSQL advisory transaction lock 按 alias 协调多副本;每个 Store 最多允许 5 个锁会话占用 10 连接池的一半,为锁内数据库调用保留连接。create 同时锁定账号 ID、alias、请求出口和请求镜像;start、reconcile/rebuild、rebind 和 upgrade 锁定 alias、当前出口及当前镜像(upgrade 还锁目标镜像),拿锁后重新读取出口与镜像版本。账号 pause/resume/revoke 使用账号 ID 与当前 binding alias 加入同一协调域;镜像禁用、引用更新或账号状态变更不能穿透在途生命周期。
控制面后台每 20 秒调和网关[runtimeLeaseHeartbeat](../../cmd/control-plane/main.go),续租 running runtime、释放 stopped/missing runtime;列表/详情读取不触发调和,过期 lease 也会在绑定事务中回收。控制面用 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.go](../../cmd/docker-gateway/douyin.go)。这些是现状,不是竞品分页或媒体下载的完成证据;后续媒体按 plan01 C4 分步保存产物引用,不复用通用 JSON 响应承载二进制。
- 当前账号凭据入口处理 Cookie;plan01 要求的可选登录密码应允许输入现有凭据保存流程,读取不回显、不进入日志,并非新增“禁止收密码”的接口限制。
- 新业务平台监听、前端业务推送和现有后台运行租约是三件事;前两者要求见 plan01 A6,列表不主动探测的约定不禁止业务事件推送。现有生命周期/租约可能核验出口,不应误写成已完成 plan01 的手动代理管理目标。