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: 添加技能学习和工具执行的单元测试

增加测试用例验证并发草案合成、重复外部写入抑制和无变化修订检测等功能。
```
This commit is contained in:
2026-06-16 15:58:42 +08:00
parent f07ce019fe
commit 83d9d8c200
15 changed files with 615 additions and 29 deletions

View File

@ -1,11 +1,14 @@
# Beaver Project 本机部署指南
最后更新2026-06-16。
这份文档用于在一台 Linux 或 WSL2 Ubuntu 机器上跑完整链路:
- `auth-portal`
- `authz-service`
- `deploy-control`
- `router-proxy`
- `MinIO` 用户文件后端
- 可选的 `external-connector` sidecar
- 自动创建出来的 `app-instance`
@ -17,6 +20,14 @@
如果你只单独启动某个前端页面,页面可以打开,但注册、登录、创建实例这些动作不一定能通。
当前部署链路的几个关键状态:
- 注册阶段只创建实例和账号,不再写入模型 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. 前提
推荐环境:
@ -184,6 +195,8 @@ beaver-deploy-control:8090
如果改的是 `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`。推荐准备一个真实域名,并把通配子域名解析到服务器,例如:
@ -427,12 +440,15 @@ docker run -d \
-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。它不会传给前端。
@ -441,6 +457,8 @@ docker run -d \
`DEFAULT_INITIAL_SKILLS_DIR` 需要和 `skills/` 的只读挂载路径一致。否则新实例能启动,但 workspace 里不会自动种入初始 published skills。
如果是在实例创建后才更新 `$PROJECT_ROOT/skills` 里的初始 skills已有实例不会自动同步这批初始文件。需要按实例使用 `scripts/deploy-initial-skills.sh` 或在实例内走 skills 管理/发布流程。
## 11. 启动 auth-portal
```bash
@ -477,6 +495,8 @@ 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`
@ -715,7 +735,7 @@ cd "$PROJECT_ROOT/app-instance"
docker ps --format 'table {{.Names}}\t{{.Status}}' | grep app-instance
```
排查 URL 变量:
排查部署变量:
```bash
docker inspect beaver-authz-service --format '{{range .Config.Env}}{{println .}}{{end}}' \
@ -725,10 +745,10 @@ docker inspect beaver-auth-portal --format '{{range .Config.Env}}{{println .}}{{
| egrep '^(AUTHZ_API_BASE_URL|DEPLOY_API_BASE_URL)='
docker inspect beaver-deploy-control --format '{{range .Config.Env}}{{println .}}{{end}}' \
| egrep '^(DEFAULT_EXTERNAL_CONNECTOR_BASE_URL|DEFAULT_EXTERNAL_CONNECTOR_TOKEN|DEFAULT_BEAVER_BRIDGE_TOKEN|DEFAULT_INITIAL_SKILLS_DIR)='
| 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)='
```
它们都必须是完整 URL不能是空字符串也不能是裸 `host:port`
其中 `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. 常见问题
@ -857,6 +877,22 @@ 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