HH-900: document manual startup and business validation (#35)
This commit is contained in:
+2
-2
@@ -11,8 +11,8 @@ WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
COPY cmd/ ./cmd/
|
||||
COPY internal/ ./internal/
|
||||
RUN CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/control-plane ./cmd/control-plane \
|
||||
&& CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' -o /out/docker-gateway ./cmd/docker-gateway
|
||||
RUN CGO_ENABLED=0 go build -buildvcs=false -trimpath -ldflags='-s -w' -o /out/control-plane ./cmd/control-plane \
|
||||
&& CGO_ENABLED=0 go build -buildvcs=false -trimpath -ldflags='-s -w' -o /out/docker-gateway ./cmd/docker-gateway
|
||||
|
||||
FROM alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce
|
||||
RUN addgroup -g 65532 app && adduser -D -u 65532 -G app app
|
||||
|
||||
+160
-7
@@ -21,7 +21,8 @@
|
||||
- Docker Compose v2;
|
||||
- 当前用户可访问 Docker daemon;
|
||||
- 可访问镜像仓库(如 `git.ipao.vip`),并已完成登录(如仓库要求认证);镜像也可不在宿主机预拉取,网关会在缺失时按引用自动拉取;
|
||||
- `curl`,用于部署后检查。
|
||||
- `curl`、`jq`、`openssl` 和 GNU `stat`,用于启动与业务验证命令;
|
||||
- 若验证真实浏览器运行环境:Linux x86_64、一个可访问的固定 HTTP/HTTPS/SOCKS 出口,以及可被 Docker daemon 拉取的 `fingerprint-chromium` 镜像。
|
||||
|
||||
在仓库根目录执行预检:
|
||||
|
||||
@@ -29,19 +30,19 @@
|
||||
test -S /var/run/docker.sock
|
||||
docker info >/dev/null
|
||||
docker compose version
|
||||
: "${CONTROL_PLANE_USERNAME:?set CONTROL_PLANE_USERNAME or fill .env}"
|
||||
: "${CONTROL_PLANE_PASSWORD:?set CONTROL_PLANE_PASSWORD or fill .env}"
|
||||
: "${CONTROL_PLANE_USERNAME:?export CONTROL_PLANE_USERNAME in this shell}"
|
||||
: "${CONTROL_PLANE_PASSWORD:?export CONTROL_PLANE_PASSWORD in this shell}"
|
||||
docker compose config --quiet
|
||||
```
|
||||
|
||||
## 首次部署
|
||||
|
||||
以下命令应在同一个 shell、仓库根目录执行。`DOCKER_GID` 必须与宿主机 Docker socket 的组一致;`CREATORHUB_PORT` 控制控制面的宿主机端口。
|
||||
以下命令应在同一个 shell、仓库根目录执行。本说明采用显式导出模式:Compose 启动和手工验证必须读取当前 shell 中导出的同一组变量;验证脚本不会自行读取 `.env`,也不会使用 Compose 默认值。若变量只写在 `.env` 中,请先将相同值 export 到当前 shell。`DOCKER_GID` 必须与宿主机 Docker socket 的组一致;`CREATORHUB_PORT` 控制控制面的宿主机端口。
|
||||
|
||||
```bash
|
||||
export DOCKER_GID="$(stat -c '%g' /var/run/docker.sock)"
|
||||
export CREATORHUB_PORT=8080
|
||||
export GATEWAY_TOKEN="$(openssl rand -hex 24)" # 亦可在 .env 中设置
|
||||
export GATEWAY_TOKEN="$(openssl rand -hex 24)"
|
||||
export CONTROL_PLANE_USERNAME=creatorhub
|
||||
export CONTROL_PLANE_PASSWORD="$(openssl rand -hex 24)"
|
||||
|
||||
@@ -87,6 +88,158 @@ docker compose ps
|
||||
docker compose logs --tail=200 creator-hub docker-gateway postgres
|
||||
```
|
||||
|
||||
## 手工业务验证(阶段 A Mock)
|
||||
|
||||
当前阶段 A 是单用户、虚拟平台 mock 的离线闭环,不连接真实社交平台。最小业务路径是:账号 → 草稿 → 显式确认 → 任务入队 → Mock 执行 → 审计回溯。
|
||||
|
||||
页面路径如下:
|
||||
|
||||
1. 登录后在「网关管理」注册 http://docker-gateway:8081,令牌必须等于 GATEWAY_TOKEN。
|
||||
2. 在「镜像版本」添加并启用一个可拉取的 fingerprint-chromium 镜像。
|
||||
3. 在「网络出口」创建出口并点击「检测」,健康状态必须为「健康」;这里只填写凭据引用 ID,不填写密码、Cookie 或 token。
|
||||
4. 在「账号池」创建平台为 mock 的账号;创建后默认暂停。
|
||||
5. 在「运行环境」选择该账号、健康出口和镜像,使用正整数 Fingerprint Seed 创建环境;随后在账号详情点击「恢复账号」,再在「运行环境」点击「启动」。
|
||||
6. 在账号详情创建文本草稿,点击「核对草稿」,勾选“我已核对当前账号、草稿内容、运行环境和固定出口”,依次执行「确认当前快照」→「保存确认」→「加入队列」。
|
||||
7. Mock 执行器没有独立页面,使用下面的 POST /api/phase-a/mock/execute,再到「任务中心」和「审计」核对结果。
|
||||
|
||||
以下命令可在已经启动的仓库根目录直接执行,并且必须在启动 Compose 的同一个 shell 中执行。开始前确认 `CREATORHUB_PORT`、`GATEWAY_TOKEN`、`CONTROL_PLANE_USERNAME` 和 `CONTROL_PLANE_PASSWORD` 都已 export;脚本不会读取未 export 的 `.env` 值或 Compose 默认值。出口参数必须替换成已批准且可从 creator-hub 容器访问的无认证代理;若代理需要认证,先按部署规范准备 credential_reference,不要把秘密值写进命令或仓库。镜像引用也可改成已发布的 registry 引用;本地 smoke test 可以使用宿主机上已有的镜像标签。
|
||||
|
||||
~~~bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
: "${CREATORHUB_PORT:?export CREATORHUB_PORT (same value used by Compose)}"
|
||||
: "${CONTROL_PLANE_USERNAME:?set CONTROL_PLANE_USERNAME}"
|
||||
: "${CONTROL_PLANE_PASSWORD:?set CONTROL_PLANE_PASSWORD}"
|
||||
: "${GATEWAY_TOKEN:?export GATEWAY_TOKEN (same value registered in the gateway)}"
|
||||
: "${PROXY_HOST:?set PROXY_HOST to an approved fixed proxy host}"
|
||||
: "${PROXY_PORT:?set PROXY_PORT to an approved fixed proxy port}"
|
||||
BASE_URL="http://127.0.0.1:${CREATORHUB_PORT}"
|
||||
|
||||
api() {
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${CONTROL_PLANE_USERNAME}:${CONTROL_PLANE_PASSWORD}" \
|
||||
-H 'Content-Type: application/json' "$@"
|
||||
}
|
||||
|
||||
[[ "$PROXY_PORT" =~ ^[0-9]+$ ]] && (( PROXY_PORT >= 1 && PROXY_PORT <= 65535 ))
|
||||
|
||||
run_id="manual-$(date +%s)"
|
||||
gateway_name="gw-${run_id}"
|
||||
image_version="${IMAGE_VERSION:-148.0.7778.215}"
|
||||
image_ref="${IMAGE_REF:-git.ipao.vip/rogee/fingerprint-chromium:${image_version}}"
|
||||
exit_protocol="${PROXY_PROTOCOL:-http}"
|
||||
account_id="${run_id}-account"
|
||||
account_key="${run_id}-platform"
|
||||
credential_id="${run_id}-credential"
|
||||
credential_key="manual/${account_id}"
|
||||
env_alias="${run_id}-env"
|
||||
|
||||
gateway_json="$(api -X POST "$BASE_URL/api/gateways" --data "$(jq -n \
|
||||
--arg name "$gateway_name" --arg token "$GATEWAY_TOKEN" \
|
||||
'{name:$name,endpoint:"http://docker-gateway:8081",token:$token}')")"
|
||||
jq -e --arg name "$gateway_name" '.name == $name' <<<"$gateway_json" >/dev/null
|
||||
|
||||
image_json="$(api -X POST "$BASE_URL/api/browser-images" --data "$(jq -n \
|
||||
--arg version "$image_version" --arg ref "$image_ref" \
|
||||
'{version:$version,image_ref:$ref,note:"manual validation",enabled:true}')")"
|
||||
jq -e --arg version "$image_version" '.version == $version and .enabled == true' <<<"$image_json" >/dev/null
|
||||
|
||||
exit_json="$(api -X POST "$BASE_URL/api/network-exits" --data "$(jq -n \
|
||||
--arg protocol "$exit_protocol" --arg host "$PROXY_HOST" --argjson port "$PROXY_PORT" \
|
||||
'{protocol:$protocol,host:$host,port:$port}')")"
|
||||
exit_id="$(jq -er '.id' <<<"$exit_json")"
|
||||
checked_exit="$(api -X POST "$BASE_URL/api/network-exits/$exit_id/check")"
|
||||
jq -e '.health_status == "healthy"' <<<"$checked_exit" >/dev/null
|
||||
|
||||
account_json="$(api -X POST "$BASE_URL/api/phase-a/accounts" --data "$(jq -n \
|
||||
--arg id "$account_id" --arg key "$account_key" --arg credential_id "$credential_id" --arg credential_key "$credential_key" \
|
||||
'{id:$id,platform:"mock",platform_account_key:$key,authorization_kind:"owned",credential_reference:{id:$credential_id,provider:"os_keyring",key:$credential_key}}')")"
|
||||
jq -e --arg id "$account_id" '.id == $id and .authorization_status == "authorized" and .runtime_status == "paused"' <<<"$account_json" >/dev/null
|
||||
|
||||
created_env="$(api -X POST "$BASE_URL/api/browsers" --data "$(jq -n \
|
||||
--arg alias "$env_alias" --arg name "手工验证环境" --arg gateway "$gateway_name" \
|
||||
--arg version "$image_version" --arg account "$account_id" --arg exit "$exit_id" \
|
||||
'{alias:$alias,name:$name,gateway:$gateway,image_version:$version,account_id:$account,network_exit_id:$exit,fingerprint:{seed:1000}}')")"
|
||||
jq -e --arg alias "$env_alias" '.alias == $alias' <<<"$created_env" >/dev/null
|
||||
|
||||
api -X POST "$BASE_URL/api/phase-a/accounts/$account_id/resume" >/dev/null
|
||||
api -X POST "$BASE_URL/api/browsers/$env_alias/start" >/dev/null
|
||||
|
||||
for _ in $(seq 1 30); do
|
||||
runtime_json="$(api "$BASE_URL/api/browsers/$env_alias")"
|
||||
if jq -e '.runtime_id != "" and .runtime_instance_id != ""' <<<"$runtime_json" >/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
jq -e '.runtime_id != "" and .runtime_instance_id != ""' <<<"$runtime_json" >/dev/null
|
||||
|
||||
account_json="$(api "$BASE_URL/api/phase-a/accounts/$account_id")"
|
||||
account_version="$(jq -er '.version' <<<"$account_json")"
|
||||
draft_json="$(api -X POST "$BASE_URL/api/phase-a/drafts" --data "$(jq -n \
|
||||
--arg account "$account_id" '{account_id:$account,content:"阶段 A 手工验证内容"}')")"
|
||||
draft_id="$(jq -er '.id' <<<"$draft_json")"
|
||||
draft_version="$(jq -er '.version' <<<"$draft_json")"
|
||||
|
||||
confirmation_json="$(api -X POST "$BASE_URL/api/phase-a/confirmations" --data "$(jq -n \
|
||||
--arg draft "$draft_id" --argjson account_version "$account_version" --argjson draft_version "$draft_version" \
|
||||
'{draft_id:$draft,account_version:$account_version,draft_version:$draft_version}')")"
|
||||
confirmation_id="$(jq -er '.id' <<<"$confirmation_json")"
|
||||
|
||||
task_json="$(api -X POST "$BASE_URL/api/phase-a/tasks" --data "$(jq -n --arg confirmation "$confirmation_id" '{confirmation_id:$confirmation}')")"
|
||||
task_id="$(jq -er '.id' <<<"$task_json")"
|
||||
jq -e '.state == "queued"' <<<"$task_json" >/dev/null
|
||||
duplicate_task_json="$(api -X POST "$BASE_URL/api/phase-a/tasks" --data "$(jq -n --arg confirmation "$confirmation_id" '{confirmation_id:$confirmation}')")"
|
||||
jq -e --arg task_id "$task_id" '.id == $task_id' <<<"$duplicate_task_json" >/dev/null
|
||||
|
||||
execution_json="$(api -X POST "$BASE_URL/api/phase-a/mock/execute" --data \
|
||||
'{"worker_id":"manual-worker","outcome":"succeeded"}')"
|
||||
jq -e '.was_claimed == true and .state == "succeeded"' <<<"$execution_json" >/dev/null
|
||||
|
||||
task_detail="$(api "$BASE_URL/api/phase-a/tasks/$task_id")"
|
||||
jq -e '(.state == "succeeded") and ((.attempts | length) >= 1) and (.attempts[0].evidence.mock_outcome == "succeeded")' <<<"$task_detail" >/dev/null
|
||||
|
||||
audit_json="$(api "$BASE_URL/api/phase-a/audit?task_id=$task_id&page_size=100")"
|
||||
for event_type in task_queued task_claimed task_finished; do
|
||||
jq -e --arg event_type "$event_type" '.data | any(.[]; .event_type == $event_type)' <<<"$audit_json" >/dev/null
|
||||
done
|
||||
|
||||
printf 'PASS account=%s environment=%s task=%s state=succeeded audit=task_queued,task_claimed,task_finished\n' \
|
||||
"$account_id" "$env_alias" "$task_id"
|
||||
~~~
|
||||
|
||||
通过标准:/healthz 返回 204;受保护的 /api/browsers 返回 JSON;网关、控制面、PostgreSQL 均为运行状态;运行环境有 runtime_id 和 runtime_instance_id;任务最终为 succeeded,且执行尝试的脱敏证据为 mock_outcome=succeeded;审计至少包含 task_queued、task_claimed、task_finished。重复提交同一确认时应返回同一任务 ID,不应产生第二条任务。
|
||||
|
||||
验证完成后的安全清理(不删除 PostgreSQL 或 Profile 卷):
|
||||
|
||||
~~~bash
|
||||
api() {
|
||||
curl --fail-with-body --silent --show-error \
|
||||
--user "${CONTROL_PLANE_USERNAME}:${CONTROL_PLANE_PASSWORD}" \
|
||||
-H 'Content-Type: application/json' "$@"
|
||||
}
|
||||
api -X POST "$BASE_URL/api/browsers/$env_alias/stop" >/dev/null
|
||||
api -X DELETE "$BASE_URL/api/browsers/$env_alias" >/dev/null
|
||||
docker compose stop
|
||||
~~~
|
||||
|
||||
## 常见故障排查
|
||||
|
||||
| 现象 | 先检查 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| docker compose config 报 required | CONTROL_PLANE_USERNAME、CONTROL_PLANE_PASSWORD 是否在当前 shell 非空 | 重新 export 两个变量;用户名不能含冒号,密码至少 16 字节 |
|
||||
| 手工验证脚本在 `:?` 处退出 | CREATORHUB_PORT、GATEWAY_TOKEN、CONTROL_PLANE_USERNAME、CONTROL_PLANE_PASSWORD 是否都已 export | 在启动 Compose 的同一个 shell 中 export 完整变量集;不要只依赖 `.env` 或 Compose 默认值 |
|
||||
| creator-hub 未启动 | docker compose ps、docker compose logs --tail=200 postgres docker-gateway creator-hub | 先确认 PostgreSQL 与网关 health 为 healthy;网关需能访问 /var/run/docker.sock,DOCKER_GID 使用 stat -c '%g' /var/run/docker.sock 的实际值 |
|
||||
| API 返回 401 | curl 是否带 --user CONTROL_PLANE_USERNAME:CONTROL_PLANE_PASSWORD | /healthz 不需要认证,其余 /api/* 需要控制面 Basic Auth |
|
||||
| /api/browsers 返回 503 或网关不可用 | 网关注册的 Endpoint、令牌与 Compose 的 GATEWAY_TOKEN | Endpoint 在 Compose 网络内应为 http://docker-gateway:8081;重新注册时令牌必须完全一致 |
|
||||
| 出口一直是 unchecked/unhealthy | 出口协议、主机、端口;控制面容器到代理的连通性;last_check_reason | 先用无认证代理完成最小验证;有认证时只提供已配置的凭据引用,不把认证值放到请求、日志或文档 |
|
||||
| 创建环境时报 image_unavailable 或拉取超时 | image_ref 格式、镜像架构、Docker daemon 的 registry 登录和网络 | 版本表中的镜像必须可被 Docker daemon 拉取;缺失镜像会由网关按引用拉取,最长约 10 分钟 |
|
||||
| 恢复/入队返回 503 | readiness、GET /api/browsers/<alias>、GET /api/network-exits/<id> | binding_missing、network_exit_unhealthy、runtime_missing 表示固定资源未就绪;先修复出口并启动原环境,不要换出口重试 |
|
||||
| Mock 执行没有领取任务 | GET /api/phase-a/tasks/<id> 的 state、hold_reason | POST /api/phase-a/mock/execute 只领取满足账号、确认、健康出口、活动 runtime 和租约条件的 queued 任务;policy_hold/needs_confirmation 不会自动重试 |
|
||||
| 停止 Compose 后浏览器仍在运行 | docker ps --filter 'name=^creatorhub-browser-' | 动态浏览器不由 Compose 管理;先通过「运行环境」停止/回收,再 docker compose stop,不要误删 Profile 卷 |
|
||||
|
||||
不要执行 docker compose down --volumes 作为普通排障手段;它会删除 PostgreSQL 数据卷。不要把控制面密码、网关令牌、代理密码、Cookie 或 token 写入仓库、截图、日志或审计查询。
|
||||
|
||||
## 配置
|
||||
|
||||
Compose 部署时通常只需设置以下宿主机变量:
|
||||
@@ -95,11 +248,11 @@ Compose 部署时通常只需设置以下宿主机变量:
|
||||
| --- | --- | --- |
|
||||
| `CREATORHUB_PORT` | `8080` | 控制面宿主机端口,局域网可访问 |
|
||||
| `DOCKER_GID` | `999` | Docker socket 的宿主机组 ID;必须按实际值设置 |
|
||||
| `GATEWAY_TOKEN` | `dev-creatorhub-gateway-token` | 网关与控制面共享的 Bearer 令牌;生产须改为随机值,并同步填入网关注册表单 |
|
||||
| `GATEWAY_TOKEN` | `dev-creatorhub-gateway-token` | Compose 未提供变量时的默认值;本说明要求显式 export 随机值,并同步填入网关注册表单 |
|
||||
| `CONTROL_PLANE_USERNAME` | 无(必填) | 控制面唯一用户;不能包含冒号 |
|
||||
| `CONTROL_PLANE_PASSWORD` | 无(必填) | 控制面密码,至少 16 字节;使用随机值 |
|
||||
|
||||
服务本身支持并校验以下环境变量;`compose.yaml` 会在控制面凭据缺失或为空时拒绝渲染:
|
||||
服务本身支持并校验以下环境变量;`compose.yaml` 会在控制面凭据缺失或为空时拒绝渲染。下表中的 Compose 默认值不由手工验证脚本隐式读取;手工验证沿用上文的显式 export 要求:
|
||||
|
||||
| 服务 | 变量 | 当前 Compose 值 |
|
||||
| --- | --- | --- |
|
||||
|
||||
Reference in New Issue
Block a user