# 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 或 prefix；Beaver 只通过 `/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 prefix；MinIO 仍然只是后端实现细节。

如果同一个实例删除请求重复执行，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/` 目录。
