Files
beaver_project/部署指南.md
steven_li 83d9d8c200 ```
feat(learning): 添加技能学习候选者合成锁定机制

添加了 DraftSynthesisInProgress 和 DraftHasNoChanges 异常来处理并发场景,
确保同一技能学习候选者的合成过程不会重复执行。实现了 claim_learning_candidate_for_synthesis
方法来原子性地锁定候选者进行合成。

fix(web): 为技能草案创建端点添加适当的HTTP状态码

当草案没有变化或正在合成时,现在正确返回409状态码而不是内部错误。

feat(skills): 实现技能修订内容比较以检测无变化情况

添加了 _is_noop_revision 方法来比较基础技能和提议的修订,
如果内容没有实际变化则抛出 NoDraftChanges 异常。

refactor(process): 修复任务证据记录后根运行状态更新逻辑

将任务证据记录事件后的状态从 waiting 更改为 done,并设置 finished_at 时间戳。

feat(tools): 防止在同一运行中重复执行外部写入操作

为邮件发送、日历创建等外部写入工具添加去重机制,避免重复的外部操作。

test: 添加技能学习和工具执行的单元测试

增加测试用例验证并发草案合成、重复外部写入抑制和无变化修订检测等功能。
```
2026-06-16 15:58:42 +08:00

910 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Beaver Project 本机部署指南
最后更新2026-06-16。
这份文档用于在一台 Linux 或 WSL2 Ubuntu 机器上跑完整链路:
- `auth-portal`
- `authz-service`
- `deploy-control`
- `router-proxy`
- `MinIO` 用户文件后端
- 可选的 `external-connector` sidecar
- 自动创建出来的 `app-instance`
目标结果:
- 浏览器能打开 `http://127.0.0.1:3081/register`
- 注册账号后自动创建专属实例
- 浏览器跳转到 `http://<slug>.localhost:8088`
如果你只单独启动某个前端页面,页面可以打开,但注册、登录、创建实例这些动作不一定能通。
当前部署链路的几个关键状态:
- 注册阶段只创建实例和账号,不再写入模型 provider、model 或 API key。
- 注册成功后由 `auth-portal` 的模型配置引导调用 `deploy-control /api/instances/configure-provider` 写入模型配置并重启实例;跳过引导也可以先进入实例。
- 用户文件系统由 Beaver API 代理到 MinIO/S3前端不会直接接触 bucket、prefix、access key 或 secret key。
- `external-connector` 是微信、飞书/Lark 等通道的 sidecar不使用这些通道时可以跳过但新实例是否带连接器环境变量取决于创建实例时的 `deploy-control` 环境。
- 新实例会从 `$PROJECT_ROOT/skills` 种入初始 published skills`deploy-control` 容器必须以相同绝对路径只读挂载该目录。
## 0. 前提
推荐环境:
- Linux
- WSL2 Ubuntu
需要工具:
- `docker`
- `git`
- `curl`
- `openssl`
- `python3`
检查:
```bash
docker --version
docker compose version
docker ps
python3 --version
openssl version
curl --version
```
如果 `docker ps` 报错,先启动 Docker。
## 1. 进入项目根目录
```bash
cd /home/ivan/xuan/beaver_project
pwd
```
预期目录:
```text
/home/ivan/xuan/beaver_project
```
## 2. 准备本机测试变量
本机测试推荐用 `localhost` 子域名。例如:
```text
alice.localhost -> 127.0.0.1 / ::1
```
这样 `router-proxy` 可以按子域名区分不同实例。
注意:
- `*.localhost` 只适合在部署机器本机浏览器里测试。
- 如果别的电脑访问 `alice.localhost`,它会指向那台电脑自己,不会指向 Beaver 服务器。
- 局域网、服务器或正式环境必须把 `BEAVER_BASE_DOMAIN` 改成客户端能解析到 Beaver 服务器的域名,例如 `apps.example.com`
- 旧版文档使用过 `127.0.0.1.nip.io`。它本来应该按域名里的 IP 解析到 `127.0.0.1`,但在部分 VPN、代理、DNS 网关或内网安全设备环境下会被改写到 `198.18.0.0/15` 这类假 IP 段,导致浏览器连不上本机服务。
直接执行:
```bash
export PROJECT_ROOT=/home/ivan/xuan/beaver_project
export BEAVER_NET=beaver-instance-edge
export BEAVER_PROXY_CONTAINER_NAME=beaver-router-proxy
export BEAVER_DEPLOY_TOKEN="$(openssl rand -hex 32)"
export BEAVER_AUTHZ_INTERNAL_TOKEN="$(openssl rand -hex 32)"
export BEAVER_BASE_DOMAIN=localhost
export BEAVER_AUTHZ_URL='http://beaver-authz-service:19090'
export BEAVER_DEPLOY_URL='http://beaver-deploy-control:8090'
export BEAVER_MINIO_ROOT_USER='beaver-minio-admin'
export BEAVER_MINIO_ROOT_PASSWORD="$(openssl rand -hex 32)"
export BEAVER_USER_FILES_BUCKET='beaver-user-files'
export BEAVER_USER_FILES_MINIO_ENDPOINT='beaver-minio:9000'
export BEAVER_USER_FILES_MAX_UPLOAD_BYTES=$((5 * 1024 * 1024 * 1024))
export BEAVER_OUTLOOK_MCP_URL=''
export BEAVER_OUTLOOK_MCP_SERVER_ID='outlook_mcp'
export EXTERNAL_CONNECTOR_BASE_URL='http://external-connector:8787'
export EXTERNAL_CONNECTOR_TOKEN="$(openssl rand -hex 32)"
export BEAVER_BRIDGE_TOKEN="$(openssl rand -hex 32)"
export EXTERNAL_CONNECTOR_PORT=8787
export CONNECTOR_PUBLIC_BASE_URL='http://127.0.0.1:8787'
export CONNECTOR_PROVIDER=fake
export CONNECTOR_COMMAND_TIMEOUT_SECONDS=120
export WEIXIN_CONNECT_COMMAND=''
export WEIXIN_STATUS_COMMAND=''
export WEIXIN_SEND_COMMAND=''
export FEISHU_CONNECT_COMMAND=''
export FEISHU_STATUS_COMMAND=''
export FEISHU_SEND_COMMAND=''
```
变量说明:
| 变量 | 作用 |
| --- | --- |
| `PROJECT_ROOT` | 仓库根目录 |
| `BEAVER_NET` | 所有容器共用的 Docker network |
| `BEAVER_DEPLOY_TOKEN` | `auth-portal` / `authz-service``deploy-control` 的 token |
| `BEAVER_AUTHZ_INTERNAL_TOKEN` | AuthZ 内部接口 token |
| `BEAVER_BASE_DOMAIN` | 新实例的基域名;本机测试用 `localhost`,服务器部署用真实域名 |
| `BEAVER_AUTHZ_URL` | 容器网络内访问 AuthZ 的地址 |
| `BEAVER_DEPLOY_URL` | 容器网络内访问 deploy-control 的地址 |
| `BEAVER_MINIO_ROOT_USER` / `BEAVER_MINIO_ROOT_PASSWORD` | 只给 provisioning 组件使用的 MinIO 管理凭据 |
| `BEAVER_USER_FILES_BUCKET` | 用户文件系统共用 bucket默认 `beaver-user-files` |
| `BEAVER_USER_FILES_MINIO_ENDPOINT` | 容器网络内访问 MinIO API 的地址 |
| `BEAVER_USER_FILES_MAX_UPLOAD_BYTES` | 用户文件系统上传上限,默认 5GB聊天附件和 workspace 上传仍保留当前小文件限制 |
| `BEAVER_OUTLOOK_MCP_URL` | 可选 Outlook MCP HTTP 地址 |
| `BEAVER_OUTLOOK_MCP_SERVER_ID` | Outlook MCP server id默认 `outlook_mcp` |
| `EXTERNAL_CONNECTOR_BASE_URL` | app-instance 容器访问外部连接器 sidecar 的地址 |
| `EXTERNAL_CONNECTOR_TOKEN` | app-instance 调用 sidecar 管理 API 的 bearer token |
| `BEAVER_BRIDGE_TOKEN` | sidecar 回调 app-instance bridge API 的 bearer token |
| `EXTERNAL_CONNECTOR_PORT` | sidecar 映射到宿主机的调试端口,默认 `8787` |
| `CONNECTOR_PUBLIC_BASE_URL` | sidecar 对外展示自身回调或资源地址时使用的 URL |
| `CONNECTOR_PROVIDER` | sidecar provider本机连通性测试用 `fake`,真实接入再改成 `weixin_ilink``feishu_bot``vendor_cli` |
| `WEIXIN_*_COMMAND` / `FEISHU_*_COMMAND` | `vendor_cli` 模式下调用厂商脚本的命令;`fake` 模式留空 |
如果接入外部正式 MinIO不需要启动本地 `beaver-minio`。把上面的 MinIO 变量改成正式服务即可:
```bash
export BEAVER_MINIO_ROOT_USER='<minio-admin-access-key>'
export BEAVER_MINIO_ROOT_PASSWORD='<minio-admin-secret-key>'
export BEAVER_USER_FILES_BUCKET='beaver-user-files-formal-test'
export BEAVER_USER_FILES_MINIO_ENDPOINT='10.6.80.98:19000'
```
注意:
- `BEAVER_USER_FILES_MINIO_ENDPOINT` 是给 Python MinIO SDK 用的 S3 API endpoint格式是 `host:port`,不要带 `http://`
- 操作员用 `mc``curl` 验证时才写成 `http://10.6.80.98:19000`
- MinIO Console 端口不是 S3 API 端口。例如 `19001` 如果返回 MinIO Console 页面,就不能填进 `USER_FILES_MINIO_ENDPOINT`
- 这个管理员账号只给 AuthZ provisioning 使用,用于创建共享 bucket、scoped user 和 scoped policy不要暴露给前端或普通用户。
`BEAVER_AUTHZ_URL``BEAVER_DEPLOY_URL` 必须带协议头。正确写法:
```text
http://beaver-authz-service:19090
http://beaver-deploy-control:8090
```
错误写法:
```text
beaver-authz-service:19090
beaver-deploy-control:8090
127.0.0.1:19090
127.0.0.1:8090
```
如果漏了 `http://`,注册页可能报:
```text
502: Request URL is missing an 'http://' or 'https://' protocol.
```
如果你改了 shell 里的变量,已经运行的容器不会自动更新。改完这些变量后,至少要重建:
- `beaver-authz-service`
- `beaver-auth-portal`
如果改的是 `BEAVER_BASE_DOMAIN`,还要重启 `beaver-deploy-control`。这个变量只影响之后新创建的实例;已经创建过的实例 URL 已经写入 `app-instance/runtime/registry/instances.json`,不会自动改成新域名。
不要把 `BEAVER_BASE_DOMAIN` 设置成裸 IP除非你明确想让实例走直连端口模式。`deploy-control` 检测到 `DEPLOY_PUBLIC_BASE_DOMAIN` 是 IP 时,会为每个实例分配 `20000-29999` 里的独立宿主机端口并生成 `http://<IP>:<host_port>` 形式的 URL这会绕过按 Host 分发的 `router-proxy` 域名入口。正式环境推荐使用真实域名,例如 `apps.example.com`
### 非本机访问怎么配置域名
如果 Beaver 部署在服务器上,而用户从其他机器访问,不要使用 `localhost`。推荐准备一个真实域名,并把通配子域名解析到服务器,例如:
```text
portal.example.com -> Beaver 服务器
*.apps.example.com -> Beaver 服务器
```
然后使用:
```bash
export BEAVER_BASE_DOMAIN=apps.example.com
```
新实例 URL 会变成:
```text
http://alice.apps.example.com:8088
```
如果外层 Nginx/Caddy/负载均衡已经把 `https://*.apps.example.com` 转发到 `router-proxy`,正式环境通常还会把 `DEPLOY_PUBLIC_SCHEME` 改为 `https`,并把 `DEPLOY_PUBLIC_PORT` 改成 `443`
## 3. 创建运行目录
```bash
mkdir -p \
"$PROJECT_ROOT/authz-service/runtime/data" \
"$PROJECT_ROOT/minio/runtime/data" \
"$PROJECT_ROOT/app-instance/runtime/instances" \
"$PROJECT_ROOT/app-instance/runtime/registry" \
"$PROJECT_ROOT/router-proxy/runtime/conf.d"
```
这些目录保存:
- AuthZ 数据
- MinIO 对象数据
- 实例注册表
- 每个用户实例的配置和数据
- `router-proxy` 生成的路由文件
## 4. 构建镜像
```bash
cd "$PROJECT_ROOT"
docker build -t beaver/app-instance:latest app-instance
docker build -t beaver/authz-service:latest authz-service
docker build -t beaver/deploy-control:latest deploy-control
docker build -t beaver/auth-portal:latest auth-portal/src
docker compose -f docker-compose.external-connectors.yml build external-connector
```
如果某个镜像构建失败,先修构建错误,不要继续往下跑。
## 5. 创建共享 Docker 网络
```bash
docker network inspect "$BEAVER_NET" >/dev/null 2>&1 || docker network create "$BEAVER_NET"
docker network ls | grep "$BEAVER_NET"
```
预期能看到:
```text
beaver-instance-edge
```
## 6. 启动 router-proxy
```bash
cd "$PROJECT_ROOT"
PROXY_NETWORK_NAME="$BEAVER_NET" \
PROXY_CONTAINER_NAME="$BEAVER_PROXY_CONTAINER_NAME" \
PROXY_HTTP_PORT=8088 \
./router-proxy/start-proxy.sh --replace
```
实例统一入口:
```text
http://<slug>.localhost:8088
```
示例:
```text
http://alice.localhost:8088
```
这里的 `localhost` 示例只表示本机测试。服务器部署时应替换为上面配置的真实基域名,例如:
```text
http://alice.apps.example.com:8088
```
## 7. 启动 external-connector sidecar可选
`external-connector` 用于微信、飞书/Lark 这类需要独立进程或厂商 SDK 的连接器。当前部署可以先用 `fake` provider 验证 sidecar、token、网络和 app-instance 回调链路;正式接入时再把 `CONNECTOR_PROVIDER` 和对应命令换成真实配置。
如果暂时不需要微信或飞书连接器,可以跳过本节。但建议至少在测试环境跑一次,确认部署变量没有断。
```bash
cd "$PROJECT_ROOT"
docker compose -f docker-compose.external-connectors.yml up -d external-connector
```
检查:
```bash
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep external-connector
curl -sS -H "Authorization: Bearer $EXTERNAL_CONNECTOR_TOKEN" \
"http://127.0.0.1:${EXTERNAL_CONNECTOR_PORT}/connectors"
```
预期 `/connectors` 返回连接器列表,至少包含 `weixin``feishu`。如果报 `401`,检查 `EXTERNAL_CONNECTOR_TOKEN` 是否和容器环境变量一致。
多实例部署时不要依赖 `BEAVER_BRIDGE_BASE_URL=http://app-instance:8080` 这种固定兜底地址。`deploy-control` 通过 `create-instance.sh --network "$BEAVER_NET"` 创建实例时,会让每个 app-instance 默认带自己的回调地址:
```text
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL=http://<app-instance-container-name>:8080
```
sidecar 会按每个连接 session 保存这个回调地址,入站消息才能回到正确的用户实例。
## 8. 启动 MinIO
MinIO 是用户文件系统的后端实现细节。用户和前端不会看到 bucket、access key 或 prefixBeaver 只通过 `/api/user-files/*` 暴露个人智能体文件系统。
如果使用外部正式 MinIO可以跳过本节本地 MinIO 容器启动,直接进入 `authz-service` 启动步骤。
```bash
docker rm -f beaver-minio >/dev/null 2>&1 || true
docker run -d \
--name beaver-minio \
--restart unless-stopped \
--network "$BEAVER_NET" \
-p 9000:9000 \
-p 9001:9001 \
-v "$PROJECT_ROOT/minio/runtime/data:/data" \
-e MINIO_ROOT_USER="$BEAVER_MINIO_ROOT_USER" \
-e MINIO_ROOT_PASSWORD="$BEAVER_MINIO_ROOT_PASSWORD" \
minio/minio:latest server /data --console-address ":9001"
```
用户文件采用共享 bucket + 用户 namespace
```text
bucket: beaver-user-files
namespace: users/{backend_id}
example object: users/alice/uploads/report.pdf
```
每个 backend/user 会由 AuthZ provisioning 生成 scoped MinIO 凭据policy 只允许访问自己的 `users/{backend_id}/*` prefix。
用户文件路径必须使用相对的虚拟根路径,例如 `uploads/input.txt``outputs/report.md``shared/profile.json``tasks/<task_id>/draft.md``/uploads/input.txt` 这类 leading-slash absolute-style path 会被拒绝,不会再被规范化成当前用户的 `uploads/input.txt`
用户文件上传由 Beaver 后端代理到 MinIO不暴露 bucket、prefix 或凭据。当前默认允许最大 5GB 的用户文件上传,业务上限由 app-instance 后端环境变量 `BEAVER_USER_FILES_MAX_UPLOAD_BYTES` 控制;反向代理默认 `client_max_body_size` 已提高到 5GB。MinIO 本身支持大对象和 multipart 上传,但 agent 对超大文件的读取/处理能力仍需要按具体任务另行验证。
## 9. 启动 authz-service
```bash
docker rm -f beaver-authz-service >/dev/null 2>&1 || true
docker run -d \
--name beaver-authz-service \
--restart unless-stopped \
--network "$BEAVER_NET" \
-p 19090:19090 \
-v "$PROJECT_ROOT/authz-service/runtime/data:/var/lib/authz-service/data" \
-e AUTHZ_ISSUER="$BEAVER_AUTHZ_URL" \
-e AUTHZ_INTERNAL_TOKEN="$BEAVER_AUTHZ_INTERNAL_TOKEN" \
-e DEPLOY_API_BASE_URL="$BEAVER_DEPLOY_URL" \
-e DEPLOY_API_TOKEN="$BEAVER_DEPLOY_TOKEN" \
-e USER_FILES_MINIO_PROVISIONING_ENABLED=1 \
-e USER_FILES_MINIO_ENDPOINT="$BEAVER_USER_FILES_MINIO_ENDPOINT" \
-e USER_FILES_MINIO_PUBLIC_ENDPOINT="$BEAVER_USER_FILES_MINIO_ENDPOINT" \
-e USER_FILES_MINIO_ADMIN_ACCESS_KEY="$BEAVER_MINIO_ROOT_USER" \
-e USER_FILES_MINIO_ADMIN_SECRET_KEY="$BEAVER_MINIO_ROOT_PASSWORD" \
-e USER_FILES_MINIO_BUCKET="$BEAVER_USER_FILES_BUCKET" \
-e USER_FILES_MINIO_SECURE=0 \
beaver/authz-service:latest
```
重点:
- `AUTHZ_ISSUER` 在当前部署里要写 `http://beaver-authz-service:19090`
- 不要写 `http://127.0.0.1:19090`
- 新创建的 `app-instance` 容器要通过 Docker network 访问 AuthZ
- `USER_FILES_MINIO_*` 只用于 AuthZ provisioning 创建 bucket、用户、policy并把 scoped settings 存入 AuthZ普通用户不会接触这些配置。
检查关键环境变量:
```bash
docker inspect beaver-authz-service --format '{{range .Config.Env}}{{println .}}{{end}}' \
| egrep '^(AUTHZ_ISSUER|DEPLOY_API_BASE_URL|USER_FILES_MINIO_)='
```
## 10. 启动 deploy-control
`deploy-control` 会挂载 Docker socket再创建新的 `app-instance` 容器。这里最容易错的是路径挂载:
- 要把宿主机真实路径按原路径挂进容器。
- 不要把 `app-instance` 挂到容器里的 `/app-instance` 这种短路径。
- `APP_INSTANCE_DIR``ROUTER_PROXY_DIR` 要和挂载路径一致。
直接执行:
```bash
docker rm -f beaver-deploy-control >/dev/null 2>&1 || true
docker run -d \
--name beaver-deploy-control \
--restart unless-stopped \
--network "$BEAVER_NET" \
-p 8090:8090 \
-v /var/run/docker.sock:/var/run/docker.sock \
-v "$PROJECT_ROOT/app-instance:$PROJECT_ROOT/app-instance" \
-v "$PROJECT_ROOT/router-proxy:$PROJECT_ROOT/router-proxy" \
-v "$PROJECT_ROOT/skills:$PROJECT_ROOT/skills:ro" \
-e APP_INSTANCE_DIR="$PROJECT_ROOT/app-instance" \
-e ROUTER_PROXY_DIR="$PROJECT_ROOT/router-proxy" \
-e DEFAULT_INITIAL_SKILLS_DIR="$PROJECT_ROOT/skills" \
-e PROXY_CONTAINER_NAME="$BEAVER_PROXY_CONTAINER_NAME" \
-e PROXY_NETWORK_NAME="$BEAVER_NET" \
-e DEPLOY_CONTROL_API_TOKEN="$BEAVER_DEPLOY_TOKEN" \
-e APP_INSTANCE_IMAGE="beaver/app-instance:latest" \
-e APP_INSTANCE_NETWORK_NAME="$BEAVER_NET" \
-e DEFAULT_AUTHZ_BASE_URL="$BEAVER_AUTHZ_URL" \
-e DEFAULT_AUTHZ_INTERNAL_TOKEN="$BEAVER_AUTHZ_INTERNAL_TOKEN" \
-e DEFAULT_AUTHZ_OUTLOOK_MCP_URL="$BEAVER_OUTLOOK_MCP_URL" \
-e DEFAULT_OUTLOOK_MCP_SERVER_ID="$BEAVER_OUTLOOK_MCP_SERVER_ID" \
-e DEFAULT_USER_FILES_MAX_UPLOAD_BYTES="$BEAVER_USER_FILES_MAX_UPLOAD_BYTES" \
-e DEFAULT_EXTERNAL_CONNECTOR_BASE_URL="$EXTERNAL_CONNECTOR_BASE_URL" \
-e DEFAULT_EXTERNAL_CONNECTOR_TOKEN="$EXTERNAL_CONNECTOR_TOKEN" \
-e DEFAULT_BEAVER_BRIDGE_TOKEN="$BEAVER_BRIDGE_TOKEN" \
-e DEPLOY_PUBLIC_SCHEME="http" \
-e DEPLOY_PUBLIC_BASE_DOMAIN="$BEAVER_BASE_DOMAIN" \
-e DEPLOY_PUBLIC_PORT="8088" \
-e DEPLOY_DIRECT_PUBLIC_HOST_BIND_IP="0.0.0.0" \
-e DEPLOY_AUTO_START_PROXY="1" \
beaver/deploy-control:latest
```
`DEPLOY_PUBLIC_BASE_DOMAIN` 来自 `BEAVER_BASE_DOMAIN`。本机测试时可以是 `localhost`;如果要让其他设备访问,必须换成它们能解析到 Beaver 服务器的真实域名。修改后需要重启 `beaver-deploy-control`,并重新创建实例或手动更新 registry 后重载 `router-proxy`
`DEPLOY_DIRECT_PUBLIC_HOST_BIND_IP` 只在 `DEPLOY_PUBLIC_BASE_DOMAIN` 是裸 IP 时生效,用来控制每个实例直连端口绑定在哪个宿主机地址。正常域名部署不依赖这个变量,实例流量应走 `router-proxy:8088`
当前版本创建实例时会传 `--skip-provider-config`,也就是先不写 provider、model 或 API key。注册成功后`auth-portal` 会进入模型配置引导页,再调用 `deploy-control /api/instances/configure-provider` 写入该实例的 `config.json` 并重启容器。
`DEFAULT_AUTHZ_INTERNAL_TOKEN` 会写入新建 app-instance 的后端 runtime env用于 app-instance 后端读取自己的 internal MinIO settings。它不会传给前端。
`DEFAULT_EXTERNAL_CONNECTOR_*` 会写入之后新创建的 app-instance 容器环境变量。改动这些变量后,要重启 `beaver-deploy-control` 并重新创建实例,或手工重建已有实例容器;仅重启 sidecar 不会更新已存在 app-instance 的环境变量。
`DEFAULT_INITIAL_SKILLS_DIR` 需要和 `skills/` 的只读挂载路径一致。否则新实例能启动,但 workspace 里不会自动种入初始 published skills。
如果是在实例创建后才更新 `$PROJECT_ROOT/skills` 里的初始 skills已有实例不会自动同步这批初始文件。需要按实例使用 `scripts/deploy-initial-skills.sh` 或在实例内走 skills 管理/发布流程。
## 11. 启动 auth-portal
```bash
docker rm -f beaver-auth-portal >/dev/null 2>&1 || true
docker run -d \
--name beaver-auth-portal \
--restart unless-stopped \
--network "$BEAVER_NET" \
-p 3081:3081 \
-e AUTHZ_API_BASE_URL="$BEAVER_AUTHZ_URL" \
-e DEPLOY_API_BASE_URL="$BEAVER_DEPLOY_URL" \
-e DEPLOY_API_TOKEN="$BEAVER_DEPLOY_TOKEN" \
beaver/auth-portal:latest
```
检查关键环境变量:
```bash
docker inspect beaver-auth-portal --format '{{range .Config.Env}}{{println .}}{{end}}' \
| egrep '^(AUTHZ_API_BASE_URL|DEPLOY_API_BASE_URL)='
```
## 12. 健康检查
```bash
curl http://127.0.0.1:19090/healthz
curl http://127.0.0.1:8090/healthz
curl -I http://127.0.0.1:3081
curl -I http://127.0.0.1:9001
curl -sS -H "Authorization: Bearer $EXTERNAL_CONNECTOR_TOKEN" \
"http://127.0.0.1:${EXTERNAL_CONNECTOR_PORT}/connectors"
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker logs --tail=50 beaver-router-proxy
```
公网或局域网正式部署时,通常只应该对外开放 `80/443`,由外层代理转发到 `3081``8088``8090``19090``9000/9001``8787` 以及实例直连端口 `20000-29999` 默认都应限制在本机、容器网络或可信内网。
至少应该看到这些容器:
- `beaver-authz-service`
- `beaver-minio`
- `beaver-deploy-control`
- `beaver-auth-portal`
- `beaver-router-proxy`
- `external-connector`(如果启用了连接器 sidecar
## 13. 浏览器首次测试
打开:
```text
http://127.0.0.1:3081/register
```
预期流程:
1. 注册一个新账号。
2. Portal 创建不含模型凭证的实例。
3. 页面进入模型配置引导。
4. 填 provider、model、API key 后确认,或暂时跳过。
5. 浏览器跳到你的实例地址。
跳转目标示例:
```text
http://alice.localhost:8088
```
也可以运行 Playwright 冒烟测试,自动验证注册、跳过模型配置、登录交接、文件页四个根目录、上传文件,并检查 `/api/user-files/*` 不泄露 bucket、namespace 或凭据字段:
```bash
cd "$PROJECT_ROOT"
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/home/worker/.cache/ms-playwright/chromium-1194/chrome-linux/chrome \
./scripts/smoke-auth-files.sh
```
如果要同时验证上传后的对象确实进入 MinIO 的 `users/{backend_id}/uploads/` namespace可以加上 MinIO 校验:
```bash
BEAVER_SMOKE_VERIFY_MINIO=1 \
BEAVER_SMOKE_MINIO_ENDPOINT=http://beaver-minio:9000 \
BEAVER_SMOKE_MINIO_ACCESS_KEY="$BEAVER_MINIO_ROOT_USER" \
BEAVER_SMOKE_MINIO_SECRET_KEY="$BEAVER_MINIO_ROOT_PASSWORD" \
BEAVER_SMOKE_MINIO_BUCKET="$BEAVER_USER_FILES_BUCKET" \
BEAVER_SMOKE_MINIO_NETWORK="$BEAVER_NET" \
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/home/worker/.cache/ms-playwright/chromium-1194/chrome-linux/chrome \
./scripts/smoke-auth-files.sh
```
如果你的机器没有这个 Chromium 路径,可以去掉 `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH`,让 Playwright 使用自己安装的浏览器。
外部正式 MinIO 的重复验证流程:
```bash
cd "$PROJECT_ROOT"
BEAVER_SMOKE_VERIFY_MINIO=1 \
BEAVER_SMOKE_MINIO_ENDPOINT=http://10.6.80.98:19000 \
BEAVER_SMOKE_MINIO_ACCESS_KEY="$BEAVER_MINIO_ROOT_USER" \
BEAVER_SMOKE_MINIO_SECRET_KEY="$BEAVER_MINIO_ROOT_PASSWORD" \
BEAVER_SMOKE_MINIO_BUCKET="$BEAVER_USER_FILES_BUCKET" \
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/home/worker/.cache/ms-playwright/chromium-1194/chrome-linux/chrome \
./scripts/smoke-auth-files.sh
```
预期结果:
- 新注册用户的 AuthZ MinIO settings 指向正式 endpoint 和 `users/{backend_id}` namespace。
- 文件页只展示 `uploads``outputs``shared``tasks` 四个根目录。
- `/api/user-files/*` 上传的对象出现在正式 bucket 的 `users/{backend_id}/uploads/` 下。
- 覆盖、下载、删除都由 Beaver API 完成,前端响应不包含 bucket、namespace、access key 或 secret key。
也可以运行更完整的用户文件系统验证自动化,覆盖 Files 页面四根目录 UI 删除、Logs/Subagents 等页面回归、MinIO 缺失对象验证和临时用户清理:
```bash
cd "$PROJECT_ROOT"
BEAVER_VALIDATE_VERIFY_MINIO=1 \
BEAVER_VALIDATE_MINIO_ENDPOINT=http://10.6.80.98:19000 \
BEAVER_VALIDATE_MINIO_ACCESS_KEY="$BEAVER_MINIO_ROOT_USER" \
BEAVER_VALIDATE_MINIO_SECRET_KEY="$BEAVER_MINIO_ROOT_PASSWORD" \
BEAVER_VALIDATE_MINIO_BUCKET="$BEAVER_USER_FILES_BUCKET" \
BEAVER_VALIDATE_DEPLOY_TOKEN="$BEAVER_DEPLOY_TOKEN" \
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/home/worker/.cache/ms-playwright/chromium-1194/chrome-linux/chrome \
./scripts/validate-filesystem-automation.sh
```
### 清理测试账号和 MinIO 用户文件
测试注册、Playwright smoke 或反复部署会创建 app-instance、本地实例目录、AuthZ MinIO settings以及 MinIO 里的 scoped user、policy 和 `users/{backend_id}/` 对象。不要只手工删除 local path否则 MinIO 里会留下无效测试资源。
优先使用清理脚本。默认是 dry-run只列出将要清理的测试账号
```bash
cd "$PROJECT_ROOT"
./scripts/cleanup-test-users.py --username-prefix smoke
```
确认列表无误后执行删除:
```bash
DEPLOY_CONTROL_API_TOKEN="$BEAVER_DEPLOY_TOKEN" \
./scripts/cleanup-test-users.py \
--username-prefix smoke \
--purge-data \
--execute
```
这会调用 `deploy-control` 的实例删除接口,并带上:
```text
X-Purge-Data: 1
X-Purge-User-Files: 1
```
其中 `X-Purge-Data` 删除本地实例数据,`X-Purge-User-Files` 让 deploy-control 调 AuthZ 内部接口清理 MinIO 用户文件资源。用户和普通 Files 页面不会看到 bucket、access key、secret key、policy 或 raw prefixMinIO 仍然只是后端实现细节。
如果同一个实例删除请求重复执行deploy-control 会把本地实例已不存在报告为 `already_absent` 的成功 no-op。带 `X-Purge-User-Files: 1` 时,它仍会对同名 backend id 调用 AuthZ 的 best-effort 用户文件清理AuthZ user-file cleanup 本身是幂等的,已不存在的 scoped user、policy、settings 或 namespace 会以 absent/no-op 状态返回。
如果只想清理一个明确实例,也可以直接调用:
```bash
curl -X DELETE "http://127.0.0.1:8090/api/instances/<instance_id>" \
-H "Authorization: Bearer $BEAVER_DEPLOY_TOKEN" \
-H "X-Purge-Data: 1" \
-H "X-Purge-User-Files: 1"
```
如果删除过程中只有一部分成功,可以按下面的方式手工恢复。先设置 MinIO alias
```bash
docker run --rm --network "$BEAVER_NET" --entrypoint /bin/sh minio/mc:latest -lc "
mc alias set beaver http://beaver-minio:9000 '$BEAVER_MINIO_ROOT_USER' '$BEAVER_MINIO_ROOT_PASSWORD'
"
```
然后针对某个 backend id 清理对象、policy 和 user
```bash
BACKEND_ID='<backend_id>'
ACCESS_KEY="beaver-$BACKEND_ID"
POLICY_NAME="beaver-user-files-$BACKEND_ID"
docker run --rm --network "$BEAVER_NET" --entrypoint /bin/sh minio/mc:latest -lc "
mc alias set beaver http://beaver-minio:9000 '$BEAVER_MINIO_ROOT_USER' '$BEAVER_MINIO_ROOT_PASSWORD' >/dev/null &&
mc rm --recursive --force 'beaver/$BEAVER_USER_FILES_BUCKET/users/$BACKEND_ID/' || true &&
mc admin policy detach beaver '$POLICY_NAME' --user '$ACCESS_KEY' || true &&
mc admin user remove beaver '$ACCESS_KEY' || true &&
mc admin policy remove beaver '$POLICY_NAME' || true
"
```
最后删除 AuthZ 里的 MinIO settings
```bash
curl -X DELETE "http://127.0.0.1:19090/backends/$BACKEND_ID/settings/minio"
```
## 14. 确认实例已创建
```bash
cd "$PROJECT_ROOT/app-instance"
./list-instances.sh
./list-instances.sh --json
docker ps --format 'table {{.Names}}\t{{.Status}}' | grep app-instance
```
注册表里应包含:
- `instance_id`
- `instance_slug`
- `container_name`
- `public_url`
- `instance_host`
确认新实例拿到了连接器环境变量:
```bash
INSTANCE_CONTAINER='<app-instance-container-name>'
docker inspect "$INSTANCE_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' \
| egrep '^(EXTERNAL_CONNECTOR_BASE_URL|EXTERNAL_CONNECTOR_TOKEN|EXTERNAL_CONNECTOR_CALLBACK_BASE_URL|BEAVER_BRIDGE_TOKEN)='
```
其中 `EXTERNAL_CONNECTOR_CALLBACK_BASE_URL` 应该指向这个实例自己的容器名,例如:
```text
http://app-instance-alice:8080
```
## 15. 只看 auth-portal 页面
如果只想看 Portal 页面,不跑全链路:
```bash
cd /home/ivan/xuan/beaver_project/auth-portal/src
npm install
npm run dev
```
打开:
```text
http://127.0.0.1:3081
```
注意:这只能看页面。注册、登录、创建实例仍依赖 `authz-service``deploy-control`
## 16. 常用排错命令
```bash
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
docker logs --tail=100 beaver-authz-service
docker logs --tail=100 beaver-deploy-control
docker logs --tail=100 beaver-auth-portal
docker logs --tail=100 beaver-router-proxy
docker logs --tail=100 external-connector
curl http://127.0.0.1:19090/healthz
curl http://127.0.0.1:8090/healthz
curl -I http://127.0.0.1:3081
curl -sS -H "Authorization: Bearer $EXTERNAL_CONNECTOR_TOKEN" \
"http://127.0.0.1:${EXTERNAL_CONNECTOR_PORT}/connectors"
```
实例创建失败时再看:
```bash
cd "$PROJECT_ROOT/app-instance"
./list-instances.sh --json
docker ps --format 'table {{.Names}}\t{{.Status}}' | grep app-instance
```
排查部署变量:
```bash
docker inspect beaver-authz-service --format '{{range .Config.Env}}{{println .}}{{end}}' \
| egrep '^(AUTHZ_ISSUER|DEPLOY_API_BASE_URL)='
docker inspect beaver-auth-portal --format '{{range .Config.Env}}{{println .}}{{end}}' \
| egrep '^(AUTHZ_API_BASE_URL|DEPLOY_API_BASE_URL)='
docker inspect beaver-deploy-control --format '{{range .Config.Env}}{{println .}}{{end}}' \
| egrep '^(DEPLOY_PUBLIC_|DEPLOY_DIRECT_PUBLIC_HOST_BIND_IP|DEFAULT_EXTERNAL_CONNECTOR_BASE_URL|DEFAULT_EXTERNAL_CONNECTOR_TOKEN|DEFAULT_BEAVER_BRIDGE_TOKEN|DEFAULT_INITIAL_SKILLS_DIR)='
```
其中 `AUTHZ_*_BASE_URL``DEPLOY_API_BASE_URL``DEFAULT_EXTERNAL_CONNECTOR_BASE_URL` 这类 URL 必须带 `http://``https://`,不能是裸 `host:port`。token 变量不能为空;`DEFAULT_INITIAL_SKILLS_DIR` 必须对应 `deploy-control` 容器里真实存在、且和宿主机一致的绝对路径。
## 17. 常见问题
### 注册页报 URL 缺少协议
现象:
```text
502: Request URL is missing an 'http://' or 'https://' protocol.
```
优先检查:
- `beaver-authz-service` 里的 `DEPLOY_API_BASE_URL`
- `beaver-auth-portal` 里的 `AUTHZ_API_BASE_URL`
- `beaver-auth-portal` 里的 `DEPLOY_API_BASE_URL`
如果你只是改了当前 shell 变量,但没有重建容器,旧值还会继续生效。
### `AUTHZ_ISSUER` 写成了 `127.0.0.1`
错误:
```text
http://127.0.0.1:19090
```
正确:
```text
http://beaver-authz-service:19090
```
原因是 `app-instance` 容器里的 `127.0.0.1` 指向它自己。
### deploy-control 路径挂载写错
错误思路:
```text
宿主机 app-instance -> 容器 /app-instance
```
正确思路:
```text
$PROJECT_ROOT/app-instance -> $PROJECT_ROOT/app-instance
$PROJECT_ROOT/router-proxy -> $PROJECT_ROOT/router-proxy
```
因为 `deploy-control` 会通过宿主机 Docker socket 再创建新容器,传给 Docker 的 bind mount 源路径必须是宿主机真实路径。
### 本地域名解析失败
检查:
```bash
getent hosts alice.localhost
```
如果浏览器或系统没有把 `<slug>.localhost` 解析到本机,可以临时在 `/etc/hosts` 添加当前测试账号的主机名:
```text
127.0.0.1 alice.localhost
```
旧版文档使用过 `127.0.0.1.nip.io`,但部分网络会把 `nip.io` / `sslip.io` / `lvh.me` 劫持到非本机地址。例如 `127.0.0.1.nip.io` 可能被解析成 `198.18.1.27`这通常是代理、VPN 或 DNS 网关返回的假 IP不是 Beaver 服务地址。遇到这种情况,本机测试优先使用 `localhost` 子域名。
### 服务器上不能用 `.localhost`
`localhost` 是保留域名,浏览器会把它解析到“当前这台客户端机器”。所以:
- 在 Beaver 服务器本机浏览器里打开 `alice.localhost`,访问的是 Beaver 服务器本机。
- 在另一台电脑浏览器里打开 `alice.localhost`,访问的是那台电脑自己。
因此远程访问、局域网访问和正式部署都不能使用 `*.localhost`。请改用真实域名或可被客户端解析到服务器的内部域名,并保证 `router-proxy` 能收到原始 `Host` 头。
### 端口被占用
默认端口:
- `3081`
- `8088`
- `8090`
- `19090`
- `8787`(如果启用了 `external-connector`
检查:
```bash
ss -ltnp | grep -E '3081|8088|8090|19090|8787'
```
### 连接器 sidecar 返回 401
检查 `docker-compose.external-connectors.yml` 里 sidecar 使用的是 `CONNECTOR_API_TOKEN`,主部署变量名是 `EXTERNAL_CONNECTOR_TOKEN`
```bash
docker inspect external-connector --format '{{range .Config.Env}}{{println .}}{{end}}' \
| egrep '^(CONNECTOR_API_TOKEN|BEAVER_BRIDGE_TOKEN|CONNECTOR_PROVIDER)='
```
请求 sidecar 管理 API 时必须使用:
```bash
curl -H "Authorization: Bearer $EXTERNAL_CONNECTOR_TOKEN" \
"http://127.0.0.1:${EXTERNAL_CONNECTOR_PORT}/connectors"
```
如果改过 token需要重启 `external-connector``beaver-deploy-control`,并重新创建或重建目标 app-instance。
### 微信或飞书连接成功但消息回不到实例
优先检查目标 app-instance 的回调地址:
```bash
docker inspect "$INSTANCE_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' \
| grep '^EXTERNAL_CONNECTOR_CALLBACK_BASE_URL='
```
多实例部署里它必须指向当前实例自己的容器名,例如:
```text
EXTERNAL_CONNECTOR_CALLBACK_BASE_URL=http://app-instance-alice:8080
```
如果它为空,通常是实例创建时没有传 `--network "$BEAVER_NET"`,或者旧实例是在连接器变量加入前创建的。重新创建实例,或用同样的实例数据目录手工重建容器。
### 使用裸 IP 做 BEAVER_BASE_DOMAIN 后 URL 变成直连端口
如果设置:
```bash
export BEAVER_BASE_DOMAIN=203.0.113.10
```
`deploy-control` 会把它识别成 IP生成类似
```text
http://203.0.113.10:20037
```
这是直连实例容器的宿主机端口模式,不是 `router-proxy` 的 Host 路由模式。要得到 `https://alice.apps.example.com` 这类地址,请改用真实域名并配置通配 DNS。
## 18. 重新部署基础容器
只重建基础容器和可选 sidecar
```bash
docker rm -f \
beaver-auth-portal \
beaver-authz-service \
beaver-deploy-control \
beaver-router-proxy \
external-connector 2>/dev/null || true
```
这不会自动删除实例数据。如果你还需要旧账号、旧实例或模型配置,不要删除 `runtime/` 目录。