Files
xiaozhi-esp32-server/docs/manager-api-fastapi-test-report.md
T

583 lines
32 KiB
Markdown
Raw Normal View History

# manager-api FastAPI 迁移测试报告
> 执行日期:2026-07-20Asia/Shanghai
>
> 工作目录:`/Users/mie/Desktop/Repo/xiaozhi-esp32-server`
>
> FastAPI 目标:`main/manager-api-fastapi`
> Java 基线:`main/manager-api`
本报告只记录实际执行并有输出或落盘证据的检查。结构路由闭合、154 条未认证/非法请求面
差分、154 条已认证安全业务/校验差分、领域测试和 49 条深度 Java/FastAPI 差分是不同强度
的证据,不互相替代。生产外部服务和真实硬件没有验证的部分,均不会写成通过。
## 1. 结果摘要
| 检查项 | 通过 | 失败/错误 | 跳过 | 结论 |
|---|---:|---:|---:|---|
| Java 基线测试 | 98 | 0 | 0 | 最终复跑 `BUILD SUCCESS`15.602 秒 |
| FastAPI 全量 pytest(最终回归) | 139 | 0 | 0 | 12.75 秒;含上传卷 readiness 与证据脱敏回归 |
| 隔离 MySQL/Redis 集成测试复跑 | 7 | 0 | 0 | 事务、锁、TTL、job 单实例与 watchdog 全绿 |
| Java→FastAPI 未认证/非法请求面差分 | 154 | 0 | 0 | 每条 Java 路由各 1 个无成功写入的缺认证或非法请求,逐项比较 status/body/Content-Type |
| Java→FastAPI 已认证安全业务/校验差分 | 154 | 0 | 0 | 每条 Java 路由各 1 个带正确认证的安全业务或校验请求;runner 有意不执行成功写入 |
| Java→FastAPI 深度差分契约 | 49 | 0 | 0 | 成功、主要错误与数据库副作用;直接覆盖 21/154 条 Java 路由 |
| 简单性能测试 | 480 请求 | 0 请求错误 | 不适用 | 4 场景 × 2 服务 × 60 次计量请求 |
| Java 路由结构清单 | 154/154 | 0 | 0 | FastAPI 注册闭合;结构证据,不等同逐接口行为证据 |
| 三端消费者调用点 | 188/188 | 0 | 0 | Web 134、Mobile 46、xiaozhi-server 8 个调用点均可解析 |
| FastAPI Ruff | 通过 | 0 | 不适用 | `app tests scripts` |
| FastAPI mypy | 70 个源文件 | 0 | 不适用 | strict 配置下通过 |
| FastAPI compileall | 通过 | 0 | 不适用 | `app tests scripts` |
| 锁文件与依赖同步 | 通过 | 0 | 不适用 | locked 环境共 55 packages |
| Python sdist/wheel | 2 个产物 | 0 | 不适用 | 冷环境完整依赖安装后可导入,路由数 163 |
| manager-web | i18n、5 unit、13 snapshot、build 全过 | 0 | 0 | build 有 4 条既有 size/precache warning |
| manager-mobile | type、lint、14 snapshot、mp-weixin build 全过 | 0 | 0 | 使用仓库声明的 pnpm 10.10.0 |
| xiaozhi-server | compileall 通过 | 0 | 不适用 | 除性能脚本外没有自动单测;8 个调用由 consumer 契约检查 |
| 真实付费/生产外部服务 | 0 | 不适用 | 不适用 | 无真实凭证,不声称联调通过 |
| 容器迁移、API、jobs 与 Nginx | 通过 | 0 | 0 | Apple Container 1.0.0 实际 build/runCompose 仅做静态验证,未伪装为 `docker compose up` |
最终可重复执行的全量测试、隔离差分、集成、构建和容器运行验证均为绿色。以下范围限制必须
与绿色测试分开陈述:
1. 全部 154 条 Java 路由均执行了两次差分:一次未认证/非法请求,一次已认证安全业务/校验
请求。第二个 runner 为保护隔离 fixture,有意不执行成功写入;49 个深度 checks 直接命中
21 条路由并覆盖代表性成功、错误和数据库副作用。因此两层全路由差分仍不等同于每条路由
的完整成功写入生命周期和全部错误路径差分。
2. 没有真实凭证、生产网络或硬件的外部集成未被计入通过。
## 2. 验证环境
| 组件 | 实际版本/配置 |
|---|---|
| 主机 | macOS 27.0arm64 |
| Java | Oracle JDK 21.0.11 LTS |
| Maven | 3.9.9,使用仓库 `.runtime/m2` |
| Python | 3.10.20`main/manager-api-fastapi/.venv` |
| MySQL | Community Server 8.0.46;隔离端口 `13316` |
| Redis | 8.8.0;隔离端口 `16379` |
| Node.js | v24.18.0 |
| npm | 11.16.0 |
| manager-mobile pnpm | Corepack 解析的 10.10.0 |
| OCI runtime | Apple Container 1.0.0;本机没有可用 Docker/Podman daemon |
| 容器架构 | linux/arm64 |
| 时区 | Asia/Shanghai |
隔离测试只重置 `manager_java_test``manager_fastapi_test` 两个测试 schema 和端口
`16379` 上的专用 Redis;没有连接、修改或清空开发 MySQL/Redis。Java 差分使用 Redis DB 1
FastAPI 使用 DB 2Java 单元验证显式使用 DB 3。
## 3. 实际执行命令
### 3.1 Java 基线
```bash
cd main/manager-api && \
JAVA_HOME=../../.runtime/jdk \
PATH="../../.runtime/jdk/bin:../../.runtime/maven/bin:$PATH" \
../../.runtime/maven/bin/mvn -o \
-Dmaven.repo.local=../../.runtime/m2 \
-Dspring.datasource.druid.url='jdbc:mysql://127.0.0.1:13316/manager_java_test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowMultiQueries=true' \
-Dspring.datasource.druid.username=xiaozhi_test \
-Dspring.datasource.druid.password=isolated-test-only \
-Dspring.data.redis.host=127.0.0.1 \
-Dspring.data.redis.port=16379 \
-Dspring.data.redis.database=3 \
-Dspring.data.redis.password= \
-DskipTests=false test
```
最终复跑结果:98 tests0 failures0 errors0 skipped`BUILD SUCCESS`15.602 秒。Surefire
XML 位于 `main/manager-api/target/surefire-reports/`,各 suite 的 tests 合计为 98。
### 3.2 FastAPI 全量测试
```bash
cd main/manager-api-fastapi && \
eval "$(./scripts/isolated-env.sh env)" && \
APP_DATABASE_URL="$TEST_FASTAPI_DATABASE_URL" \
APP_REDIS_URL="$TEST_FASTAPI_REDIS_URL" \
APP_ENVIRONMENT=test \
.venv/bin/pytest -q
```
最终结果:139 passed、0 failed、0 skipped12.75 秒。
### 3.3 隔离集成、两层全路由差分、深度差分和性能测试
```bash
cd main/manager-api-fastapi && ./scripts/run-isolated-contract-tests.sh
```
最终执行结果为 exit 0。脚本实际完成以下阶段:
- 启动并重置隔离 MySQL `13316`、Redis `16379`
- 对 Java 与 FastAPI 两个 schema 分别执行原 Liquibase 101 个 changeSets
- 启动 Java 基线 `18082`、FastAPI `18083`、确定性外部 mock `18084`
- 生成并镜像固定用户、DB Token、Long ID、设备、agent、模型、纠错词和 OTA fixture
- 执行 7 条隔离集成测试;
- 对 154 条 Java 路由各执行一条未认证或非法的安全请求面差分,不产生成功写入;
- 对 154 条 Java 路由各执行一条带正确认证的安全业务或校验差分,仍不产生成功写入;
- 执行 49 条 Java/FastAPI 差分检查并写入 JSON
- 执行 480 次计量性能请求并写入 JSON;
- 对 FastAPI/mock 日志执行 warning、traceback、error 门禁;
- 退出时关闭 Java、FastAPI 和 mock,最终 `18082``18084` 没有监听进程。
最终阶段摘要(两行 154 分别对应未认证/非法和已认证安全业务/校验):
```text
7 passed
{"total": 154, "passed": 154, "failed": 0, "skipped": 0}
{"total": 154, "passed": 154, "failed": 0, "skipped": 0}
{"total": 49, "passed": 49, "failed": 0, "skipped": 0}
{"measurements": 8, "requests_measured": 480, "errors": 0}
Isolated integration, two 154-route surfaces, deep differential, and performance tests passed.
```
机器结果:
- `main/manager-api-fastapi/compatibility/route-surface-results.json`
- 生成时间:`2026-07-20T07:12:28.099864+00:00`
- 154 passed、0 failed、0 skipped
- `main/manager-api-fastapi/compatibility/authenticated-route-results.json`
- 生成时间:`2026-07-20T07:12:30.818073+00:00`
- 154 passed、0 failed、0 skipped
- `main/manager-api-fastapi/compatibility/contract-results.json`
- 生成时间:`2026-07-20T07:12:31.520810+00:00`
- 49 passed、0 failed、0 skipped
- `main/manager-api-fastapi/compatibility/performance-results.json`
- 生成时间:`2026-07-20T07:12:33.158826+00:00`
- 480 requests、0 errors
### 3.4 FastAPI 静态、依赖和构建验证
```bash
cd main/manager-api-fastapi
.venv/bin/ruff check app tests scripts
.venv/bin/mypy app
.venv/bin/python -m compileall -q app tests scripts
uv lock --check && uv sync --locked
uv build --no-cache
```
结果:
- Ruff 通过;
- mypy70 个源文件无问题;
- compileallexit 0
- lock check 与 locked syncexit 0,共 55 packages
- 无缓存构建 sdist 与 wheel 均成功。
构建产物另在临时冷虚拟环境验证。首次执行 `uv pip install --python
<临时环境>/bin/python --no-deps <wheel>` 后直接 import,因刻意没有安装 FastAPI 等运行依赖而
失败;这暴露的是冷 wheel 检查命令不完整,不是把失败隐藏为通过。随后执行带依赖的安装:
```bash
uv pip install --python <临时环境>/bin/python <wheel>
```
共安装 39 个锁定依赖。从仓库外 `/tmp` 导入成功,输出
`xiaozhi-manager-api 163`,证明不是依赖当前工作目录导入源码。
### 3.5 manager-web
```bash
cd main/manager-web && \
npm run check:i18n && \
npm run test:unit && \
npm run test:snapshot && \
npm run build
```
结果:
- i18n6 个 locale,每个 1527 keyskey 结构一致;
- unit5/5
- snapshot13/13
- Vue 生产构建 exit 0hash `71b64d002eb434ee`1069 ms);
- 输出有 4 条既有 bundle size/precache warning,没有将 warning 写成失败,也没有删除或放宽
测试来取得绿色结果;另有 `caniuse-lite` 数据过期 17 个月提示,未擅自更新依赖。
### 3.6 manager-mobile
```bash
cd main/manager-mobile && \
corepack pnpm type-check && \
corepack pnpm lint && \
corepack pnpm test:snapshot && \
corepack pnpm build:mp
```
结果:type-check exit 0、lint exit 0、snapshot 14/14、mp-weixin build exit 0。构建提示
`caniuse-lite` 数据过期 20 个月及 uni-app 有新版本;两项均不影响退出码,未擅自更新依赖。
一次较早的失败尝试直接调用普通 `pnpm`,环境解析到 v11,并因非 TTY 下依赖目录清理提示而
终止;没有把该次尝试写成通过。最终命令显式使用 Corepack,解析到仓库声明的 pnpm 10.10.0。
### 3.7 xiaozhi-server
```bash
cd main/xiaozhi-server && \
../manager-api-fastapi/.venv/bin/python --version && \
PYTHONPYCACHEPREFIX=/tmp/xiaozhi-server-pycache \
../manager-api-fastapi/.venv/bin/python -m compileall -q . && \
rg --files -g '*test*.py' -g '!performance_tester/**'
```
结果:Python 3.10.20compileall exit 0。排除性能测试目录后没有自动单测文件,因此没有虚构
pytest 通过数量;`xiaozhi-server` 的 8 个 manager-api 调用点由 consumer manifest 和 FastAPI
兼容测试验证为可解析。
首次误用不存在的 `../../.runtime/python/bin/python3.10`,结果为 exit 127;改用上面实际存在的
FastAPI Python 3.10.20 后通过。
## 4. 两层全路由差分与深度差分覆盖
### 4.1 154 条未认证/非法安全请求面差分
`tests/compatibility/route_surface_runner.py` 从 Java route manifest 逐条构造不发生成功写入的请求:
133 条 DB Token 路由省略 Token,14 条匿名路由发送安全非法输入,7 条内部路由省略
server-secret。Java 与 FastAPI 精确比较 HTTP status、解析后的 body 和 Content-Type;最终
154 passed、0 failed、0 skipped。它证明每条 Java 路由至少有一个请求路径兼容,不代表每条
路由的成功、全部错误与数据库副作用均已逐项对照。
### 4.2 154 条已认证安全业务/校验差分
`tests/compatibility/authenticated_route_runner.py` 使用与 Java 基线语义一致的 DB Token、
server-secret 或匿名认证方式,对同一份 154 路由清单逐条发送安全业务/校验请求。runner 对
动态管理员密码和 OTA 下载 UUID 先独立验证格式再做最小归一化,并同步会影响响应的固定审计
时间;最终 154 passed、0 failed、0 skipped。为不污染 fixture 或产生不可逆副作用,该 runner
有意选择资源不存在、单一约束失败、幂等空操作等不会成功写入的路径。因此这里的“已认证”
证明请求已经越过认证层并进入业务/校验逻辑,不表示每条写接口都完成了一次成功写入。
### 4.3 49 项深度结果分布
| 类别 | 通过 | 失败 |
|---|---:|---:|
| configuration | 1 | 0 |
| authentication-i18n | 7 | 0 |
| authentication | 1 | 0 |
| authorization | 1 | 0 |
| serialization | 2 | 0 |
| agent | 1 | 0 |
| device | 1 | 0 |
| model | 1 | 0 |
| correct-word | 1 | 0 |
| binary-download | 6 | 0 |
| validation | 5 | 0 |
| server-secret | 3 | 0 |
| ota | 1 | 0 |
| ota-validation | 2 | 0 |
| ota-signing | 2 | 0 |
| activation | 3 | 0 |
| external-mock | 2 | 0 |
| crud | 3 | 0 |
| database-side-effect | 4 | 0 |
| upload | 1 | 0 |
| upload-validation | 1 | 0 |
| **合计** | **49** | **0** |
直接覆盖内容包括:
- 默认语言、`zh-CN``zh-TW``en-US``de-DE``vi-VN``pt-BR` 七种
`Accept-Language` 情形;
- 未登录、DB Token 过期、普通用户访问管理员接口;
- Long ID 字符串、日期、Asia/Shanghai/UTC 兼容、null、别名和分页;
- 缺字段、数字格式、最小/最大边界和 Java UTF-16 长度语义;
- server-secret 缺失、错误和正确三条路径;
- OTA health、缺失/非法 `Device-Id`、激活、WS/MQTT credential
- HMAC-SHA256、URL-safe Base64、MQTT Base64 密码的独立密码学验证;
- 纠错词创建、更新、下载、删除及数据库副作用;
- OTA multipart 上传、扩展名错误、元数据副作用、三次下载与第四次 404;
- 二进制 MIME、`Content-Disposition``Content-Length` 和字节摘要;
- MQTT mock 的请求 body 和按日期生成的 Authorization。
### 4.4 有意的动态值处理
- OTA timestamp、WebSocket token 和生成 UUID 先做最小范围归一化,再分别验证格式与 HMAC;
不是把动态字段全部忽略。
- Hibernate Validator 使用无序 `ConstraintViolation Set`。模型 provider 空 body 用例要求
Java/FastAPI 均返回 HTTP 200、错误码 10034,消息必须属于 Java DTO 声明的五个精确约束,
不强行固定 Java 本身不稳定的首条消息。
- 报告落盘前递归脱敏 `private_key`、server secret、MQTT signature key、Token、password 和
Authorization;比较与密码学验证仍使用未脱敏的内存值。
### 4.5 覆盖边界
Java 清单共有 154 条路由,FastAPI 结构注册为 154/154;另有 3 条消费者兼容路由。全部 154
条均有一次未认证/非法差分和一次已认证安全业务/校验差分;49 个深度 checks 直接命中
21/154 条 Java 路由,并对代表性成功、主要错误和数据库副作用做更完整的生命周期验证。
其余路由虽有两层逐路由差分及相关领域 service/repository/protocol 测试,仍不能宣称其全部
成功写入和错误路径均已逐项深度差分。逐行状态见
`docs/manager-api-fastapi-compatibility.md`
## 5. 隔离数据库、Redis 和 job 测试
7 条集成测试均连接隔离 MySQL/Redis,而不是纯 mock
1. MySQL 事务异常回滚;
2. `SELECT FOR UPDATE` 在并发写入下串行化;
3. Redis key TTL 到期且不删除无关 key
4. Redis 分布式锁只允许一个并发 job 执行;
5. watchdog 在任务超过原始 lease 后续租,仍保持单实例;
6. 实际 knowledge job 函数在 Redis 锁下只执行一次;
7. Java hash 兼容写入的默认 TTL 为 86400 秒。
差分 CRUD 另检查 Java/FastAPI 各自隔离 schema 的行级副作用;没有实施双写,也没有操作开发
数据库。最终 FastAPI 与 external mock 日志没有 warning/error/traceback。两层全路由差分会
让 Java 基线按其既有全局异常处理记录 36 条预期 ERROR,严格门禁将其精确分为 8 类:缺 body
13 条、缺 `Device-Id` 5 条、对象/数组反序列化 8 条、缺 query 4 条、非 multipart 3 条、
`callerMac` 空值 1 条、空消息 1 条及 `null` 消息 1 条。未分类 Java ERROR 为 0;任一类别数量
变化或出现未分类日志都会使脚本失败。
## 6. 性能对比
参数:每个服务每场景先顺序 warmup 10 次,再以并发 6 计量 60 次;4 个场景、2 个服务共
480 次计量请求。结果来自最终 `performance-results.json`
| 场景 | 服务 | p50 ms | p95 ms | 吞吐 req/s | 错误 |
|---|---|---:|---:|---:|---:|
| representative-read | Java | 7.662 | 16.239 | 601.128 | 0 |
| representative-read | FastAPI | 6.749 | 12.552 | 751.538 | 0 |
| representative-crud-update | Java | 9.331 | 15.302 | 543.013 | 0 |
| representative-crud-update | FastAPI | 11.252 | 20.599 | 476.870 | 0 |
| runtime-configuration | Java | 8.746 | 16.127 | 568.863 | 0 |
| runtime-configuration | FastAPI | 6.488 | 13.722 | 765.126 | 0 |
| ota-check-and-signing | Java | 11.301 | 19.854 | 454.655 | 0 |
| ota-check-and-signing | FastAPI | 16.208 | 20.344 | 354.807 | 0 |
FastAPI/Java 比率:读取 p50 `0.881`、p95 `0.773`、吞吐 `1.250`CRUD p50 `1.206`
p95 `1.346`、吞吐 `0.878`;配置 p50 `0.742`、p95 `0.851`、吞吐 `1.345`OTA p50
`1.434`、p95 `1.025`、吞吐 `0.780`。因此不能笼统声称所有 FastAPI 接口都更快:本轮读取和
配置场景更快,CRUD p50/p95 分别较慢约 20.6%/34.6%OTA p50 较慢约 43.4%、p95 接近、
吞吐低约 22.0%。
这是同机、短时、固定 fixture 的简单对比,用于发现数量级回退,不是容量、长稳、生产网络或
多 worker 极限测试。
## 7. 三端兼容验证
- `manager-web`:134 个调用点、130 条唯一结构路由;i18n、unit、snapshot 与 production build
均通过,现有调用无需修改 URL。
- `manager-mobile`:46 个调用点、40 条唯一结构路由;type-check、lint、snapshot 与微信小程序
build 均通过。
- `xiaozhi-server`:8 个调用点、8 条唯一结构路由;compileall 通过,consumer manifest 确认
全部能解析到 FastAPI。该模块没有可执行的一般单测,不能把 compileall 写成运行时集成通过。
三端合计 188 个调用点、140 条唯一结构路由。此结论证明 path/method 解析闭合;全部 Java
路由另有未认证/非法和已认证安全业务/校验两层差分,参与 49 项深度差分或领域测试的调用拥有
更完整的成功、错误或副作用证据。
## 8. 迁移验证过程中发现并修正的问题
测试没有通过删除、跳过或放宽失败用例获得绿色。差分在实现过程中实际暴露并促成修复的兼容
问题包括:分页 `total` 类型、运行时配置字段查询、设备日期时区、认证与 OTA MIME、缺失
`Device-Id` envelope、provider 校验语义、非法分页消息、运行时配置 key 命名。修复后才生成
当前 49/49 深度报告。
154 路由请求面 runner 首次执行只有 149/154 通过,准确暴露 5 条“整个 JSON body 缺失”差异:
`POST /ota/``POST /user/login``POST /user/register`
`POST /user/retrieve-password``POST /user/smsVerification`。Java 对整个 body 缺失返回
HTTP 200、`code=500` 的通用 envelopeFastAPI 当时返回 `code=10034`。全局校验兼容层随后
只对定位恰为 body 根节点的 missing 错误映射 `code=500`,字段级 missing 仍保持 `10034`
新增回归用例并完整重跑后才得到 154/154。
已认证安全业务/校验 runner 首次执行为 111/154,通过实际差分先后修正了根 body 类型、必填
query/multipart 的 Java envelope、knowledge/device/agent/voice resource 的检查顺序、权限与资源
不存在语义,以及 DTO 单约束消息等差异;后续结果依次为 149/154、152/154,最终才达到
154/154。对 Hibernate Validator 无序约束的请求,runner 改用只触发一个约束的定向 payload,
没有跳过路由、忽略响应字段或放宽比较。该 runner 始终保持“已认证但不成功写入”的安全边界。
另有测试基础设施、运行时和构建问题被明确记录:
- Pydantic 2.13 与 FastAPI 0.116 的 TypeAdapter alias 路径产生
`UnsupportedFieldAttributeWarning`;依赖锁定到 Pydantic 2.11.7/core 2.33.2 后,实际 OTA
body alias 验证与最终日志门禁均无 warning。
- 第一版日志门禁把 Logback 初始化文本 `ERROR_FILE` 误判为运行时 ERROR,虽然 7、49、480
阶段均绿,脚本仍按门禁返回 1。正则收紧为带完整日期的 Java 应用 ERROR 后,重新从头执行
完整流程并获得 exit 0;没有直接忽略门禁失败。
- fixture 的 MySQL `VALUES()` upsert 产生 8.0 弃用 warning;改为 row alias 语法并重新执行后
无该 warning。
- 已认证差分报告最初虽未含真实凭证,但 `paramCode=server.secret` / `paramValue` 结构仍写入了
隔离 fixture 值;递归脱敏器补充键值对识别及回归测试后,再次完整执行差分。最终四份 JSON
`contract-server-secret`、测试 Token 和测试数据库密码的扫描均为 0 命中。
- Python 3.10 下 fixed-delay jobs 等待超时抛出 `asyncio.TimeoutError`;原捕获路径导致 worker
首轮后退出。worker 改为捕获该异常并增加 Python 3.10 回归测试;实际 jobs 容器随后观察到
knowledge job 跨 30 秒重复运行,snapshot redaction 多轮执行,SIGTERM 后干净退出。
- API 镜像构建初期遇到 uv/pip registry 传输失败;Dockerfile 固定 uv 版本并增加 timeout、retry
与缓存后完成构建。迁移镜像的 Maven Central 并发下载两次卡住,改为串行 resolver、超时与
retry 后成功。Nginx 初版配置在镜像 build 期校验失败,改用 template + `envsubst` 并在 build
内执行 `nginx -t` 后通过。
- Apple Container 自定义网络没有提供本次验证所需的容器名 DNS,host publish 和单文件挂载也
与 Docker 行为不同;验证改用容器 IP、显式 TCP bridge、named volume 和运行时 upstream 模板。它们
是测试 runtime 限制,不被记作应用通过或失败,也没有据此声称 Docker Compose 已实际启动。
## 9. 外部服务与真实联调状态
所有自动化外部调用只访问本地确定性 mock/fixture,不访问真实付费服务。
| 外部能力 | 自动化证据 | 真实联调状态 |
|---|---|---|
| RAGFlow dataset/document/chunk/retrieval/upload | 请求 JSON/query/header、30 秒 timeout、强 DTO、Long/null、错误映射与补偿路径测试 | 无真实 RAGFlow 凭证/实例,未联调 |
| 阿里云短信 | 配置、错误 envelope 与业务路径测试 | 无真实 AccessKey,不发送短信,未联调 |
| 火山语音克隆/音频 | multipart/JSON、状态及错误映射 mock | 无真实付费凭证,未联调 |
| 声纹 HTTP | Java multipart 形状与错误映射 mock | 无真实声纹服务,未联调 |
| OpenAI-compatible LLM | 请求格式、thinking policy、摘要/标题相关 mock | 无真实模型 key,不访问付费模型,未联调 |
| MQTT gateway HTTP | 差分验证 body、按日期 Authorization 和 401 retry 语义 | 无真实 MQTT broker/gateway,未联调 |
| MCP/管理 WebSocket | token、URL、path/scheme/form 兼容测试 | 无真实远端 MCP/WS,未联调 |
| OTA/WS/MQTT credential | 本地 HMAC/Base64/时间戳和下载行为实测 | 无 ESP32 真机和生产 broker,不属于硬件联调 |
因此,本报告只证明 mock 下已覆盖的请求格式、超时、错误映射、重试和本地密码学行为;不能把
任何一项写成供应商或生产环境端到端通过。
## 10. 已知行为/部署差异
- Java 的 Hibernate Validator 首条约束消息顺序不稳定;FastAPI 保持相同 envelope、错误码和
声明消息集合,而不是伪造固定顺序。
- Java 在 Spring 进程内运行定时任务;FastAPI 把 jobs 分离为独立进程,并用 Redis 锁和
watchdog 防止多 worker 重复执行。集成测试验证单实例和续租语义,但部署拓扑有意不同。
- FastAPI 增加 3 条消费者兼容路由和 live/ready health endpoints;它们没有 Java Controller
基线,属于明确的加法差异。
- 49 项已执行深度差分中没有观测到响应、所选 header 或数据库副作用差异;这句话只适用于
报告中的 49 项,不外推为全部 154 条路由均完成了成功写入和全部错误路径生命周期验证。
## 11. 实际容器与 Nginx 验证
### 11.1 Runtime、镜像与 Compose 口径
本机没有可用的 Docker/Podman daemon,实际 OCI build/run 使用 Apple Container 1.0.0 的
linux/arm64 VM,并显式使用隔离 app/log/install root
```bash
CLI=/Users/mie/.cache/xiaozhi-migration-tools/container-1.0.0-prefix/bin/container
ROOT=/Users/mie/.cache/xiaozhi-migration-tools/container-1.0.0-prefix
"$CLI" system start \
--app-root "$ROOT/runtime-data" \
--install-root "$ROOT" \
--log-root "$ROOT/runtime-logs" \
--disable-kernel-install
"$CLI" builder start
"$CLI" build --tag xiaozhi/manager-api-fastapi:0.1.0 \
--file main/manager-api-fastapi/Dockerfile .
"$CLI" build --tag xiaozhi/manager-api-migrate:fastapi-0.1.0 \
--file main/manager-api-fastapi/Dockerfile.migrations .
"$CLI" build --tag xiaozhi/manager-api-nginx:fastapi-0.1.0 \
--file main/manager-api-fastapi/Dockerfile.nginx .
```
三张镜像均实际构建并运行。迁移镜像 OCI index 为
`sha256:613faace4314b03392e65b64d9b4a9ba7a694cdd751c1a45009824d55f0647f7`,其 arm64
manifest 为 `sha256:6a10850841370d033a3b521fbb1100cb64b5cc6837fac35d00c4257343c0f2f9`
Nginx 镜像 OCI index 为
`sha256:2e6a188ad6d38b62fa4e77329a629ada00c4e773da9ded73c5f3289e40da477a`,其 arm64
manifest 为 `sha256:ff653bc2d11d4a3b1640747626055d6551fb33324500fb2e65b9333142da8526`
API 镜像在上传目录 readiness 最后一处源码变更后重新 build;最终 OCI index 为
`sha256:04ae1a98307b7369368b9665c6caf9f0911c8b2a967f5a91f23c6dde7c7baa16`,其 arm64
manifest 为 `sha256:c3267d307c9898975372539121f118549bfe51012c6c9bfdc3e84f99f3e56214`
config 为 `sha256:7c0b13757da041c0d118d14342e92c5310e4fb2140f029e385e97de9fe21d8cc`
manifest size 为 84,551,252 bytes,镜像配置创建时间为 `2026-07-20T07:04:45Z`
`docker-compose.yml` 已由 `tests/test_deployment_artifacts.py` 静态验证 migration dependency、
read-only root、tmpfs、upload volume、healthcheck、graceful timeout 与可切换 upstreamNginx
镜像 build 内也实际执行 `nginx -t`。由于本机没有 Docker Compose runtime,本报告明确只把
Compose 记为静态通过,不声称执行过 `docker compose up`
### 11.2 Liquibase migration
迁移镜像以 UID 10001 一次性运行,只读取原 Java resources 内的 Liquibase 历史。目标为隔离
schema `manager_container_test`;最终容器回归中再次运行并报告 101 个 changeSets 均
up-to-date。随后实查 `DATABASECHANGELOG` 为 101 条、业务及 Liquibase 表合计 30 张,
`DATABASECHANGELOGLOCK.LOCKED=0`,证明历史完整且锁已释放。没有连接、修改或清空开发数据库。
### 11.3 API、jobs、health、卷与优雅关闭
Apple Container VM 访问 host-only MySQL/Redis 时使用仓库内 TCP bridge,而不是暴露开发服务:
```bash
cd main/manager-api-fastapi
.venv/bin/python -m tests.compatibility.tcp_proxy \
--listen-port 13317 --target-host 127.0.0.1 --target-port 13316
.venv/bin/python -m tests.compatibility.tcp_proxy \
--listen-port 16380 --target-host 127.0.0.1 --target-port 16379
```
API 容器使用 `APP_WORKERS=2`、隔离 schema、Redis DB 4、read-only root、`/tmp` tmpfs 和
named upload volume 启动。实测结果:
- 容器内 UID 为 10001,最终层没有 `/bin/uv``/usr/bin/gcc`,应用路由数为 163
- 日志确认 2 个 Uvicorn worker(容器内 PID 3、4);`/xiaozhi/health/live` 为 HTTP 200
- `Accept-Language: en-US` 的未认证业务请求保持 HTTP 200、英文 `{code:401,...}`
`POST /xiaozhi/user/login` 整个 JSON body 缺失保持 Java 的 HTTP 200、`code=500`
- read-only root 生效。Apple Container 新建空 named volume 首次以其默认 root ownership 挂载,
新增 readiness 检查准确返回 HTTP 503、`database=true``redis=true``uploads=false`,没有让
无法上传的实例接流量;该失败没有伪装为通过。随后用一次性 root 容器仅对卷执行 `chown`
ownership 初始化,ready 变为 HTTP 200 且 `uploads=true`UID 10001 的 API 成功写入,重启后
文件 SHA256 `1ad4cb4f879aa1ddf43a14e1a84cc5dbf8f65e91295165e126b8b08be3cd9a50` 保持不变;
- 发送 SIGTERM 后 worker 完成 lifespan shutdown 并以 exit 0 退出,无 traceback/error。
同一 API 镜像另以 `python -m app.jobs.worker`、read-only root 和 `/tmp` tmpfs 启动。实际等待
超过 31 秒后,knowledge fixed-delay job 执行两次且相隔 30 秒,snapshot redaction 多次执行;
这验证 Python 3.10 timeout 修复与真实调度循环。SIGTERM 后 jobs 也干净退出。API 多 worker
本身不加载 jobs,独立 worker 再由 Redis lock/watchdog 保证单实例。
上述 ownership 初始化是 Apple Container 空 named volume 的实测处理;本机没有 Docker
Compose runtime,因此 Docker Compose 的 named-volume copy-up 行为没有实际验证,不能用
Apple Container 的结果代替。
### 11.4 Nginx 切流与 Java 回滚
Nginx 镜像以 read-only root 和 `/var/cache/nginx``/var/run``/tmp` 三个 tmpfs 运行;其
entrypoint 将 `MANAGER_API_UPSTREAM` 注入模板后 `exec nginx`。Apple Container 自定义网络在
本次环境没有容器名 DNS,因此实测使用 runtime 分配的 API/Java 容器 IP,语义与生产 hostname
upstream 相同。FastAPI upstream 下实际验证:
- `/xiaozhi/health/ready` 为 HTTP 200
- `/xiaozhi` 精确返回 308 到 `/xiaozhi/`
- `Accept-Language: en-US` 的未认证 envelope 由 Nginx 转发后与直连 FastAPI 一致,整个 JSON
body 缺失也保持 HTTP 200、`code=500`
- Nginx、API 均在 SIGTERM 下以 exit 0 干净退出。
随后仅替换 `MANAGER_API_UPSTREAM` 指向保留的 Java 容器并重建 Nginx 运行实例;`/xiaozhi/ota/`
回滚探针的 response body 与直连 Java 按字节完全一致。此步骤证明回滚不需要删除 Java 服务、
改数据库或双写,只需切换 upstream。Nginx 基础镜像未声明非 root USER,因此这里不虚构其
non-root 属性;实际硬化证据是 read-only root、最小 tmpfs 和无持久写入。应用与迁移镜像则
均以 UID 10001 运行。
## 12. 证据文件
- 逐接口矩阵:`docs/manager-api-fastapi-compatibility.md`
- 迁移、切流与回滚说明:`docs/manager-api-fastapi-migration.md`
- Java 路由清单:`main/manager-api-fastapi/compatibility/java-routes.json`
- 三端调用清单:`main/manager-api-fastapi/compatibility/consumer-routes.json`
- 154 路由未认证/非法请求面机器报告:
`main/manager-api-fastapi/compatibility/route-surface-results.json`
- 154 路由已认证安全业务/校验机器报告:
`main/manager-api-fastapi/compatibility/authenticated-route-results.json`
- 深度差分机器报告:`main/manager-api-fastapi/compatibility/contract-results.json`
- 性能机器报告:`main/manager-api-fastapi/compatibility/performance-results.json`
- 一键隔离脚本:`main/manager-api-fastapi/scripts/run-isolated-contract-tests.sh`
- 未认证/非法请求面 runner
`main/manager-api-fastapi/tests/compatibility/route_surface_runner.py`
- 已认证安全业务/校验 runner:
`main/manager-api-fastapi/tests/compatibility/authenticated_route_runner.py`
- 深度差分 runner`main/manager-api-fastapi/tests/compatibility/differential_runner.py`
- 外部 mock`main/manager-api-fastapi/tests/compatibility/external_mock.py`
- 集成测试:`main/manager-api-fastapi/tests/integration/test_isolated_runtime.py`
- 容器静态断言:`main/manager-api-fastapi/tests/test_deployment_artifacts.py`
- 容器网络 bridge`main/manager-api-fastapi/tests/compatibility/tcp_proxy.py`
- API/migration/Nginx 构建定义:`main/manager-api-fastapi/Dockerfile`
`main/manager-api-fastapi/Dockerfile.migrations``main/manager-api-fastapi/Dockerfile.nginx`
- Nginx runtime 配置:`main/manager-api-fastapi/deploy/nginx.conf`
`main/manager-api-fastapi/deploy/nginx-entrypoint.sh`
- Java Surefire`main/manager-api/target/surefire-reports/`
## 13. 当前结论
Java 98、FastAPI 全量 139、隔离集成 7、未认证/非法请求面 154/154、已认证安全业务/校验
154/154、深度差分 49/49、性能 480/0,以及 Web/Mobile 构建、xiaozhi-server compileall 和
实际容器/Nginx 验证均按上述命令完成;各测试集合均为 0 failed、0 errors、0 skipped。原 Java
服务和 Liquibase 历史均未删除。
本地可安全执行的兼容、集成、构建、消费者和容器验证已经通过。每条 Java 路由虽已有两次
全覆盖差分,但已认证 runner 有意不执行成功写入,所以不能将其表述为 154 条全部成功、错误
和副作用生命周期均已深度验证;真实 RAGFlow、短信、语音克隆、声纹、模型、MQTT/MCP/WS
及 ESP32 硬件因没有真实凭证或设备而未联调,也没有在本报告中描述为已通过。