mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 23:23:55 +08:00
feat: add FastAPI manager API compatibility baseline
This commit is contained in:
@@ -0,0 +1,267 @@
|
||||
# manager-api FastAPI 兼容性矩阵
|
||||
|
||||
> 生成依据:`main/manager-api-fastapi/compatibility/java-routes.json`、
|
||||
> `main/manager-api-fastapi/compatibility/consumer-routes.json`、`route-surface-results.json`、
|
||||
> `authenticated-route-results.json`、`contract-results.json` 和当前 Java 源码。接口路径均省略
|
||||
> 共同前缀 `/xiaozhi`。
|
||||
|
||||
## 结论与状态口径
|
||||
|
||||
Java 基线共有 **154** 条 Spring MVC 路由;FastAPI 已注册 **154/154(100%)**,并由
|
||||
`tests/test_java_route_manifest.py` 对源码清单 freshness、数量和 method/path 注册闭合进行检查。
|
||||
此外实现 3 条仅由仓库消费者使用、Java Controller 中不存在的兼容路由,因此这 3 条不计入
|
||||
154 条 Java 覆盖率。三端 188 个调用点均能解析到 FastAPI 路由。
|
||||
|
||||
矩阵状态必须按下列含义阅读:
|
||||
|
||||
- `结构✓`:method/path 已注册且清单闭合;它不等于业务行为逐接口实测。
|
||||
- `请求面差分✓1`:本行已向隔离 Java/FastAPI 各发送一次缺少鉴权或安全非法输入,精确比较
|
||||
HTTP status、body 与 Content-Type;最终为 **154/154 通过、0 失败、0 跳过**,且不发送成功写请求。
|
||||
- `认证业务面差分✓1`:本行已使用有效 DB Token、server-secret 或匿名身份,再向隔离
|
||||
Java/FastAPI 各发送一次安全业务/校验请求,精确比较 HTTP status、body 与 Content-Type;
|
||||
最终为 **154/154 通过、0 失败、0 跳过**,且不主动发送成功写请求。该状态不等于每条路由的
|
||||
完整成功生命周期均已差分,完整副作用证据仍以 `差分✓N` 为准。
|
||||
- `领域✓(x,域级)`:该领域有 service/repository/协议自动测试,但不保证本行每条成功与错误路径
|
||||
都被直接请求。`领域—` 表示除结构测试外没有可归属的域级直接测试证据。
|
||||
- `差分✓N`:本行除安全请求面外,还参与了成功、主要错误、协议或数据库副作用的深度对照;
|
||||
括号说明覆盖面。深度结果为 **49/49 checks 通过、0 失败、0 跳过**,覆盖 **21/154** 条路由。
|
||||
`差分间接✓` 表示 J125 作为下载链路的 URL 生成步骤被间接覆盖;`差分—` 表示没有深度对照,
|
||||
不能把 154/154 请求面差分误读成 154 条全部成功路径与副作用都已逐接口对照。
|
||||
- 所有 `Result<T>` 均表示 `{code,msg,data}` envelope;原 Java 为 HTTP 200 的认证、权限、业务和
|
||||
参数错误由全局兼容层维持 HTTP 200。二进制/OTA 裸响应在“响应类型”列单独标明。
|
||||
|
||||
## 三端消费者闭合
|
||||
|
||||
| 消费者 | 调用点 | 唯一结构路由 | 方法分布 |
|
||||
|---|---:|---:|---|
|
||||
| `manager-web` | 134 | 130 | DELETE 12、GET 59、POST 40、PUT 23 |
|
||||
| `manager-mobile` | 46 | 40 | DELETE 3、GET 26、POST 12、PUT 5 |
|
||||
| `xiaozhi-server` | 8 | 8 | GET 2、POST 6 |
|
||||
| **合计** | **188** | **140** | — |
|
||||
|
||||
### 3 条消费者孤儿兼容路由
|
||||
|
||||
| Method/path | 来源 | FastAPI 语义 | 鉴权 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| `GET /api/ping` | manager-mobile 环境设置探活 | `{code:0,msg:"success",data:"pong"}` | 匿名 | 实现✓;consumer resolve✓ |
|
||||
| `PUT /user/configDevice/{device_id}` | manager-web 遗留设备配置调用 | 按现有设备更新契约处理 body | DB Token | 实现✓;consumer resolve✓ |
|
||||
| `GET /device/address-book/lookup` | xiaozhi-server 管理客户端 | `callerMac/nickname/answer` 地址簿查询/呼叫兼容别名 | server-secret | 实现✓;consumer resolve✓;device 域测试✓ |
|
||||
|
||||
`GET /admin/dict/data/type/FIRMWARE_TYPE` 是动态 Java 路由
|
||||
`GET /admin/dict/data/type/{dictType}` 的一个字面调用,不是第四条孤儿路由。
|
||||
|
||||
## Java 基线静态盘点
|
||||
|
||||
- Controller:24 个、154 条映射。按 Controller 的路由数为:`AdminController`(5)、`AgentChatHistoryController`(4)、`AgentController`(21)、`AgentMcpAccessPointController`(2)、`AgentSnapshotController`(4)、`AgentTemplateController`(6)、`AgentVoicePrintController`(4)、`ConfigController`(3)、`CorrectWordController`(7)、`DeviceController`(13)、`KnowledgeBaseController`(7)、`KnowledgeFilesController`(8)、`LoginController`(8)、`ModelController`(11)、`ModelProviderController`(5)、`OTAController`(3)、`OTAMagController`(9)、`ServerSideManageController`(2)、`SysDictDataController`(6)、`SysDictTypeController`(5)、`SysParamsController`(5)、`TimbreController`(4)、`VoiceCloneController`(6)、`VoiceResourceController`(6)。
|
||||
- 数据分层:`entity/` 29 个 Java 文件(28 个 `*Entity.java` 加 `BaseEntity`)、`dto/` 58 个、
|
||||
`vo/` 14 个、`dao/` 29 个、`service/` 树 78 个文件(其中
|
||||
`service/impl/` 38 个)。FastAPI 对应落在 `schemas/`、`repositories/`、
|
||||
`services/`、`routers/`、`integrations/` 与 `jobs/`,没有把跨表事务放进路由。
|
||||
- MyBatis XML:20 个,分别是 `mapper/agent/AgentCorrectWordMappingDao.xml`、`mapper/agent/AgentDao.xml`、`mapper/agent/AgentPluginMappingMapper.xml`、`mapper/agent/AgentSnapshotDao.xml`、`mapper/agent/AgentTagDao.xml`、`mapper/agent/AgentTagRelationDao.xml`、`mapper/agent/AgentTemplateMapper.xml`、`mapper/agent/AiAgentChatHistoryDao.xml`、`mapper/correctword/CorrectWordItemDao.xml`、`mapper/device/DeviceAddressBookDao.xml`、`mapper/device/DeviceDao.xml`、`mapper/knowledge/KnowledgeBaseDao.xml`、`mapper/model/ModelConfigDao.xml`、`mapper/model/ModelProviderDao.xml`、`mapper/security/SysUserTokenDao.xml`、`mapper/sys/SysDictDataDao.xml`、`mapper/sys/SysDictTypeDao.xml`、`mapper/sys/SysParamsDao.xml`、`mapper/sys/SysUserDao.xml`、`mapper/voiceclone/VoiceCloneDao.xml`。
|
||||
- Liquibase:`db.changelog-master.yaml` 含 101 个 `changeSet` 引用,目录中恰有
|
||||
101 个 SQL;Python 部署继续执行这 101 个原始 SQL,不改写历史。
|
||||
- 定时工作:`DocumentStatusSyncTask` 每次完成后延迟 30 秒,扫描 RAGFlow RUNNING 文档并
|
||||
回写 SUCCESS/FAIL/CANCEL 与统计;当前 Java 源码另有 `AgentSnapshotRedactionRunner`,启动时
|
||||
执行一次并在滚动部署期每 15 秒补偿脱敏旧快照。FastAPI 将工作移到独立 jobs 进程,并以
|
||||
Redis 分布式锁/watchdog 防止多 worker 重复执行。
|
||||
- 外部集成:RAGFlow dataset/document/chunk/retrieval/upload;阿里云短信;火山语音克隆训练与
|
||||
音频;声纹 HTTP;OpenAI-compatible LLM 摘要/标题;MQTT gateway HTTP;MCP/管理动作
|
||||
WebSocket;OTA/WS/MQTT 的 HMAC、Base64、时间戳与下载文件存储。自动测试只访问可重复 mock,
|
||||
未使用真实付费凭证。
|
||||
|
||||
## 154 条 Java→FastAPI 逐接口矩阵
|
||||
|
||||
副作用缩写:`DB-R/W`=数据库读/写,`Redis-R/W/DEL`=缓存读/写/失效,`文件-R/W`=文件
|
||||
读取/写入;外部调用均在 service/integration 层。权限为空时表示只需对应鉴权身份。
|
||||
|
||||
| # | Method/path | Java Controller.handler | 请求面 | 响应类型 | 鉴权 / 权限 | DB/Redis/文件/外部副作用 | 实现与测试状态 |
|
||||
|---:|---|---|---|---|---|---|---|
|
||||
| J001 | `GET /admin/device/all` | `AdminController.pageDevice` | Query:params:Map<String, Object> | envelope <PageData<UserShowDeviceListVO>> | DB Token / `sys:role:superAdmin` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J002 | `POST /admin/dict/data/delete` | `SysDictDataController.delete` | Body:Long[] | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J003 | `GET /admin/dict/data/page` | `SysDictDataController.page` | Query:params:Map<String, Object> | envelope <PageData<SysDictDataVO>> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R/W(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J004 | `POST /admin/dict/data/save` | `SysDictDataController.save` | Body:SysDictDataDTO | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J005 | `GET /admin/dict/data/type/{dictType}` | `SysDictDataController.getDictDataByType` | Path:dictType | envelope <List<SysDictDataItem>> | DB Token / `sys:role:normal` | DB-R; Redis-R/W(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J006 | `PUT /admin/dict/data/update` | `SysDictDataController.update` | Body:SysDictDataDTO | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J007 | `GET /admin/dict/data/{id}` | `SysDictDataController.get` | Path:id | envelope <SysDictDataVO> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R/W(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J008 | `POST /admin/dict/type/delete` | `SysDictTypeController.delete` | Body:Long[] | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J009 | `GET /admin/dict/type/page` | `SysDictTypeController.page` | Query:params:Map<String, Object> | envelope <PageData<SysDictTypeVO>> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R/W(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J010 | `POST /admin/dict/type/save` | `SysDictTypeController.save` | Body:SysDictTypeDTO | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J011 | `PUT /admin/dict/type/update` | `SysDictTypeController.update` | Body:SysDictTypeDTO | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J012 | `GET /admin/dict/type/{id}` | `SysDictTypeController.get` | Path:id | envelope <SysDictTypeVO> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R/W(dict cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J013 | `POST /admin/params` | `SysParamsController.save` | Body:SysParamsDTO | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-W/DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J014 | `PUT /admin/params` | `SysParamsController.update` | Body:SysParamsDTO | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-W; 外部-配置端点探测(按 paramCode) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J015 | `POST /admin/params/delete` | `SysParamsController.delete` | Body:String[] | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-W/DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J016 | `GET /admin/params/page` | `SysParamsController.page` | Query:params:Map<String, Object> | envelope <PageData<SysParamsDTO>> | DB Token / `sys:role:superAdmin` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J017 | `GET /admin/params/{id}` | `SysParamsController.get` | Path:id | envelope <SysParamsDTO> | DB Token / `sys:role:superAdmin` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J018 | `POST /admin/server/emit-action` | `ServerSideManageController.emitServerAction` | Body:EmitSeverActionDTO | envelope <Boolean> | DB Token / `sys:role:superAdmin` | DB/Redis-R(secret/WS); Redis-W(one-shot); 外部-WebSocket | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J019 | `GET /admin/server/server-list` | `ServerSideManageController.getWsServerList` | — | envelope <List<String>> | DB Token / `sys:role:superAdmin` | DB/Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J020 | `GET /admin/users` | `AdminController.pageUser` | Query:params:Map<String, Object> | envelope <PageData<AdminPageUserVO>> | DB Token / `sys:role:superAdmin` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分✓3(权限/序列化/非法分页) |
|
||||
| J021 | `PUT /admin/users/changeStatus/{status}` | `AdminController.changeStatus` | Path:status; Body:String[] | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W(user/password/status/token) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J022 | `DELETE /admin/users/{id}` | `AdminController.delete` | Path:id | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W(用户/token/device/agent 级联) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J023 | `PUT /admin/users/{id}` | `AdminController.update` | Path:id | envelope <String> | DB Token / `sys:role:superAdmin` | DB-W(user/password/status/token) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(sys,域级);差分— |
|
||||
| J024 | `POST /agent` | `AgentController.save` | Body:AgentCreateDTO | envelope <String> | DB Token / `sys:role:normal` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J025 | `GET /agent/all` | `AgentController.adminAgentList` | Query:params:Map<String, Object> | envelope <PageData<AgentEntity>> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J026 | `POST /agent/audio/{audioId}` | `AgentController.getAudioId` | Path:audioId | envelope <String> | DB Token / `sys:role:normal` | DB-R(audio); Redis-W(one-shot URL) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J027 | `GET /agent/chat-history/download/{uuid}/current` | `AgentChatHistoryController.downloadCurrentSession` | Path:uuid | 流式/二进制 + 原下载 headers | 匿名 / — | DB/Redis-R(one-shot); 文件-R/流式 | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J028 | `GET /agent/chat-history/download/{uuid}/previous` | `AgentChatHistoryController.downloadCurrentSessionWithPrevious` | Path:uuid | 流式/二进制 + 原下载 headers | 匿名 / — | DB/Redis-R(one-shot); 文件-R/流式 | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J029 | `POST /agent/chat-history/getDownloadUrl/{agentId}/{sessionId}` | `AgentChatHistoryController.getDownloadUrl` | Path:agentId,sessionId | envelope <String> | DB Token / — | DB-R(chat/session); Redis-W(download token TTL) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J030 | `POST /agent/chat-history/report` | `AgentChatHistoryController.uploadFile` | Body:AgentChatHistoryReportDTO | envelope <Boolean> | server-secret / — | DB-W(chat/session); server-secret | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J031 | `POST /agent/chat-summary/{sessionId}/save` | `AgentController.generateAndSaveChatSummary` | Path:sessionId | envelope <Void> | server-secret / — | DB-R/W(chat); 外部-OpenAI-compatible LLM | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J032 | `POST /agent/chat-title/{sessionId}/generate` | `AgentController.generateAndSaveChatTitle` | Path:sessionId | envelope <Void> | server-secret / — | DB-R/W(chat); 外部-OpenAI-compatible LLM | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J033 | `GET /agent/list` | `AgentController.getUserAgents` | Query:keyword:String,searchType:String | envelope <List<AgentDTO>> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分✓1 |
|
||||
| J034 | `GET /agent/mcp/address/{agentId}` | `AgentMcpAccessPointController.getAgentMcpAccessAddress` | Path:agentId | envelope <String> | DB Token / `sys:role:normal` | DB/Redis-R; AES token 生成 | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J035 | `GET /agent/mcp/tools/{agentId}` | `AgentMcpAccessPointController.getAgentMcpToolsList` | Path:agentId | envelope <List<String>> | DB Token / `sys:role:normal` | DB/Redis-R; 外部-WebSocket MCP | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J036 | `GET /agent/play/{uuid}` | `AgentController.playAudio` | Path:uuid | 流式/二进制 + 原下载 headers | 匿名 / — | DB/Redis-R(one-shot); 文件-R/流式 | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J037 | `PUT /agent/saveMemory/{macAddress}` | `AgentController.updateByDeviceId` | Path:macAddress; Body:AgentMemoryDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J038 | `POST /agent/tag` | `AgentController.createTag` | Body:Map<String, String> | envelope <AgentTagEntity> | DB Token / `sys:role:normal` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J039 | `GET /agent/tag/list` | `AgentController.getAllTags` | — | envelope <List<AgentTagDTO>> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J040 | `DELETE /agent/tag/{id}` | `AgentController.deleteTag` | Path:id | envelope <Void> | DB Token / `sys:role:normal` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J041 | `GET /agent/template` | `AgentController.templateList` | — | envelope <List<AgentTemplateEntity>> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J042 | `POST /agent/template` | `AgentTemplateController.createAgentTemplate` | Body:AgentTemplateEntity | envelope <AgentTemplateEntity> | DB Token / `sys:role:superAdmin` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J043 | `PUT /agent/template` | `AgentTemplateController.updateAgentTemplate` | Body:AgentTemplateEntity | envelope <AgentTemplateEntity> | DB Token / `sys:role:superAdmin` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J044 | `POST /agent/template/batch-remove` | `AgentTemplateController.batchRemoveAgentTemplates` | Body:List<String> | envelope <String> | DB Token / `sys:role:superAdmin` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J045 | `GET /agent/template/page` | `AgentTemplateController.getAgentTemplatesPage` | Query:params:Map<String, Object> | envelope <PageData<AgentTemplateVO>> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J046 | `DELETE /agent/template/{id}` | `AgentTemplateController.deleteAgentTemplate` | Path:id | envelope <String> | DB Token / `sys:role:superAdmin` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J047 | `GET /agent/template/{id}` | `AgentTemplateController.getAgentTemplateById` | Path:id | envelope <AgentTemplateVO> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J048 | `POST /agent/voice-print` | `AgentVoicePrintController.save` | Body:AgentVoicePrintSaveDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W; 外部-voiceprint HTTP | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J049 | `PUT /agent/voice-print` | `AgentVoicePrintController.update` | Body:AgentVoicePrintUpdateDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W; 外部-voiceprint HTTP | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J050 | `GET /agent/voice-print/list/{id}` | `AgentVoicePrintController.list` | Path:id | envelope <List<AgentVoicePrintVO>> | DB Token / `sys:role:normal` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J051 | `DELETE /agent/voice-print/{id}` | `AgentVoicePrintController.delete` | Path:id | envelope <Void> | DB Token / `sys:role:normal` | DB-W; 外部-voiceprint HTTP | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J052 | `GET /agent/{agentId}/snapshots` | `AgentSnapshotController.page` | Path:agentId; Query:params:AgentSnapshotPageDTO | envelope <PageData<AgentSnapshotVO>> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J053 | `DELETE /agent/{agentId}/snapshots/{snapshotId}` | `AgentSnapshotController.deleteSnapshot` | Path:agentId,snapshotId | envelope <Void> | DB Token / `sys:role:normal` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J054 | `GET /agent/{agentId}/snapshots/{snapshotId}` | `AgentSnapshotController.getSnapshot` | Path:agentId,snapshotId | envelope <AgentSnapshotVO> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J055 | `POST /agent/{agentId}/snapshots/{snapshotId}/restore` | `AgentSnapshotController.restore` | Path:agentId,snapshotId; Body:AgentSnapshotRestoreDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J056 | `DELETE /agent/{id}` | `AgentController.delete` | Path:id | envelope <Void> | DB Token / `sys:role:normal` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J057 | `GET /agent/{id}` | `AgentController.getAgentById` | Path:id | envelope <AgentInfoVO> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J058 | `PUT /agent/{id}` | `AgentController.update` | Path:id; Body:AgentUpdateDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J059 | `GET /agent/{id}/chat-history/audio` | `AgentController.getContentByAudioId` | Path:id | envelope <String> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J060 | `GET /agent/{id}/chat-history/user` | `AgentController.getRecentlyFiftyByAgentId` | Path:id | envelope <List<AgentChatHistoryUserVO>> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J061 | `GET /agent/{id}/chat-history/{sessionId}` | `AgentController.getAgentChatHistory` | Path:id,sessionId | envelope <List<AgentChatHistoryDTO>> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J062 | `GET /agent/{id}/sessions` | `AgentController.getAgentSessions` | Path:id; Query:params:Map<String, Object> | envelope <PageData<AgentChatSessionDTO>> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J063 | `GET /agent/{id}/tags` | `AgentController.getAgentTags` | Path:id | envelope <List<AgentTagDTO>> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J064 | `PUT /agent/{id}/tags` | `AgentController.saveAgentTags` | Path:id; Body:Map<String, Object> | envelope <Void> | DB Token / `sys:role:normal` | DB-W(含快照/映射/标签事务); Redis-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(agent,域级);差分— |
|
||||
| J065 | `POST /config/agent-models` | `ConfigController.getAgentModels` | Body:AgentModelsDTO | envelope <Object> | server-secret / — | DB-R; Redis-R/W(runtime/model/timbre cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(config,域级);差分— |
|
||||
| J066 | `POST /config/correct-words` | `ConfigController.getCorrectWords` | Body:CorrectWordsDTO | envelope <Object> | server-secret / — | DB-R; Redis-R/W(runtime/model/timbre cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(config,域级);差分— |
|
||||
| J067 | `POST /config/server-base` | `ConfigController.getConfig` | — | envelope <Object> | server-secret / — | DB-R; Redis-R/W(runtime/model/timbre cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(config,域级);差分✓3(缺失/错误/正确 secret) |
|
||||
| J068 | `POST /correct-word/file` | `CorrectWordController.createFile` | Body:CorrectWordFileCreateDTO | envelope <CorrectWordFileVO> | DB Token / `sys:role:normal` | DB-W(file/items/mapping 事务) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(correctword,域级);差分✓2(响应/DB) |
|
||||
| J069 | `POST /correct-word/file/batch-delete` | `CorrectWordController.batchDeleteFiles` | Body:List<String> | envelope <Void> | DB Token / `sys:role:normal` | DB-W(file/items/mapping 事务) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(correctword,域级);差分— |
|
||||
| J070 | `GET /correct-word/file/download/{fileId}` | `CorrectWordController.downloadFile` | Path:fileId | 流式/二进制 + 原下载 headers | DB Token / `sys:role:normal` | DB-R(content); 二进制 | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(correctword,域级);差分✓2(二进制/更新后下载) |
|
||||
| J071 | `GET /correct-word/file/list` | `CorrectWordController.listFiles` | Query:params:Map<String, Object> | envelope <PageData<CorrectWordFileVO>> | DB Token / `sys:role:normal` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(correctword,域级);差分✓1 |
|
||||
| J072 | `GET /correct-word/file/select` | `CorrectWordController.listAllFiles` | — | envelope <List<CorrectWordFileVO>> | DB Token / `sys:role:normal` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(correctword,域级);差分— |
|
||||
| J073 | `DELETE /correct-word/file/{fileId}` | `CorrectWordController.deleteFile` | Path:fileId | envelope <Void> | DB Token / `sys:role:normal` | DB-W(file/items/mapping 事务) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(correctword,域级);差分✓1(级联副作用) |
|
||||
| J074 | `PUT /correct-word/file/{fileId}` | `CorrectWordController.updateFile` | Path:fileId; Body:CorrectWordFileCreateDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W(file/items/mapping 事务) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(correctword,域级);差分✓2(响应/DB) |
|
||||
| J075 | `GET /datasets` | `KnowledgeBaseController.getPageList` | Query:name:String,page:Integer,page_size:Integer | envelope <PageData<KnowledgeBaseDTO>> | DB Token / `sys:role:normal` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J076 | `POST /datasets` | `KnowledgeBaseController.save` | Body:KnowledgeBaseDTO | envelope <KnowledgeBaseDTO> | DB Token / `sys:role:normal` | DB-R/W; 外部-RAGFlow HTTP(upload/dataset/document/chunk/retrieval) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J077 | `DELETE /datasets/batch` | `KnowledgeBaseController.deleteBatch` | Query:ids:String | envelope <Void> | DB Token / `sys:role:normal` | DB-R/W; 外部-RAGFlow HTTP(upload/dataset/document/chunk/retrieval) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J078 | `GET /datasets/rag-models` | `KnowledgeBaseController.getRAGModels` | — | envelope <List<ModelConfigEntity>> | DB Token / `sys:role:normal` | DB-R(model config) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J079 | `DELETE /datasets/{dataset_id}` | `KnowledgeBaseController.delete` | Path:dataset_id | envelope <Void> | DB Token / `sys:role:normal` | DB-R/W; 外部-RAGFlow HTTP(upload/dataset/document/chunk/retrieval) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J080 | `GET /datasets/{dataset_id}` | `KnowledgeBaseController.getByDatasetId` | Path:dataset_id | envelope <KnowledgeBaseDTO> | DB Token / `sys:role:normal` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J081 | `PUT /datasets/{dataset_id}` | `KnowledgeBaseController.update` | Path:dataset_id; Body:KnowledgeBaseDTO | envelope <KnowledgeBaseDTO> | DB Token / `sys:role:normal` | DB-R/W; 外部-RAGFlow HTTP(upload/dataset/document/chunk/retrieval) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J082 | `POST /datasets/{dataset_id}/chunks` | `KnowledgeFilesController.parseDocuments` | Path:dataset_id; Body:Map<String, List<String>> | envelope <Void> | DB Token / `sys:role:normal` | DB-R/W; 外部-RAGFlow HTTP(upload/dataset/document/chunk/retrieval) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J083 | `DELETE /datasets/{dataset_id}/documents` | `KnowledgeFilesController.delete` | Path:dataset_id; Body:DocumentDTO.BatchIdReq | envelope <Void> | DB Token / `sys:role:normal` | DB-R/W; 外部-RAGFlow HTTP(upload/dataset/document/chunk/retrieval) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J084 | `GET /datasets/{dataset_id}/documents` | `KnowledgeFilesController.getPageList` | Path:dataset_id; Query:name:String,status:String,page:Integer,page_size:Integer | envelope <PageData<KnowledgeFilesDTO>> | DB Token / `sys:role:normal` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J085 | `POST /datasets/{dataset_id}/documents` | `KnowledgeFilesController.uploadDocument` | Path:dataset_id; Query:name:String,chunkMethod:String,metaFields:String,parserConfig:String; Multipart:file | envelope <KnowledgeFilesDTO> | DB Token / `sys:role:normal` | DB-R/W; 外部-RAGFlow HTTP(upload/dataset/document/chunk/retrieval) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J086 | `GET /datasets/{dataset_id}/documents/status/{status}` | `KnowledgeFilesController.getPageListByStatus` | Path:dataset_id,status; Query:page:Integer,page_size:Integer | envelope <PageData<KnowledgeFilesDTO>> | DB Token / `sys:role:normal` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J087 | `DELETE /datasets/{dataset_id}/documents/{document_id}` | `KnowledgeFilesController.deleteSingle` | Path:dataset_id,document_id | envelope <Void> | DB Token / `sys:role:normal` | DB-R/W; 外部-RAGFlow HTTP(upload/dataset/document/chunk/retrieval) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J088 | `GET /datasets/{dataset_id}/documents/{document_id}/chunks` | `KnowledgeFilesController.listChunks` | Path:dataset_id,document_id; Query:page:Integer,pageSize:Integer,keywords:String,id:String | envelope <ChunkDTO.ListVO> | DB Token / `sys:role:normal` | DB-R/W; 外部-RAGFlow HTTP(upload/dataset/document/chunk/retrieval) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J089 | `POST /datasets/{dataset_id}/retrieval-test` | `KnowledgeFilesController.retrievalTest` | Path:dataset_id; Body:RetrievalDTO.TestReq | envelope <RetrievalDTO.ResultVO> | DB Token / `sys:role:normal` | DB-R/W; 外部-RAGFlow HTTP(upload/dataset/document/chunk/retrieval) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(knowledge,域级);差分— |
|
||||
| J090 | `PUT /device/address-book/alias` | `DeviceController.updateAlias` | Body:DeviceAddressBookAliasDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W(device/bind/address-book); Redis-R/W | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J091 | `GET /device/address-book/call` | `DeviceController.callByNickname` | Query:callerMac:String,nickname:String,answer:boolean | envelope <Map<String, Object>> | server-secret / — | DB-R; 外部-MQTT gateway HTTP; server-secret | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J092 | `PUT /device/address-book/permission` | `DeviceController.updatePermission` | Body:DeviceAddressBookPermissionDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W(device/bind/address-book); Redis-R/W | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J093 | `GET /device/address-book/{macAddress}` | `DeviceController.getAddressBook` | Path:macAddress | envelope <Object> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J094 | `GET /device/bind/{agentId}` | `DeviceController.getUserDevices` | Path:agentId | envelope <List<UserShowDeviceListVO>> | DB Token / `sys:role:normal` | DB-R; Redis-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分✓1 |
|
||||
| J095 | `POST /device/bind/{agentId}` | `DeviceController.forwardToMqttGateway` | Path:agentId; Body:String | envelope <String> | DB Token / `sys:role:normal` | DB/Redis-R; 外部-MQTT gateway HTTP + daily auth | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J096 | `POST /device/bind/{agentId}/{deviceCode}` | `DeviceController.bindDevice` | Path:agentId,deviceCode | envelope <Void> | DB Token / `sys:role:normal` | DB-W(device/bind/address-book); Redis-R/W | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J097 | `POST /device/manual-add` | `DeviceController.manualAddDevice` | Body:DeviceManualAddDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W(device/bind/address-book); Redis-R/W | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J098 | `POST /device/register` | `DeviceController.registerDevice` | Body:DeviceRegisterDTO | envelope <String> | DB Token / — | DB-W(device/bind/address-book); Redis-R/W | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J099 | `POST /device/tools/call/{deviceId}` | `DeviceController.callDeviceTool` | Path:deviceId; Body:DeviceToolsCallReqDTO | envelope <Object> | DB Token / `sys:role:normal` | DB/Redis-R; 外部-MQTT gateway HTTP + daily auth | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J100 | `POST /device/tools/list/{deviceId}` | `DeviceController.getDeviceTools` | Path:deviceId | envelope <Object> | DB Token / `sys:role:normal` | DB/Redis-R; 外部-MQTT gateway HTTP + daily auth | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分✓2(响应/外呼格式) |
|
||||
| J101 | `POST /device/unbind` | `DeviceController.unbindDevice` | Body:DeviceUnBindDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W(device/bind/address-book); Redis-R/W | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J102 | `PUT /device/update/{id}` | `DeviceController.updateDeviceInfo` | Path:id; Body:DeviceUpdateDTO | envelope <Void> | DB Token / `sys:role:normal` | DB-W(device/bind/address-book); Redis-R/W | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分✓3(上下界/UTF-16 长度) |
|
||||
| J103 | `PUT /models/default/{id}` | `ModelController.setDefaultModel` | Path:id | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(model/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J104 | `PUT /models/enable/{id}/{status}` | `ModelController.enableModelConfig` | Path:id,status | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(model/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J105 | `GET /models/list` | `ModelController.getModelConfigList` | Query:modelType:String,modelName:String,page:String,limit:String | envelope <PageData<ModelConfigDTO>> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R/W(model cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J106 | `GET /models/llm/names` | `ModelController.getLlmModelCodeList` | Query:modelName:String | envelope <List<LlmModelBasicInfoDTO>> | DB Token / `sys:role:normal` | DB-R; Redis-R/W(model cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J107 | `GET /models/names` | `ModelController.getModelNames` | Query:modelType:String,modelName:String | envelope <List<ModelBasicInfoDTO>> | DB Token / `sys:role:normal` | DB-R; Redis-R/W(model cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J108 | `GET /models/provider` | `ModelProviderController.getListPage` | Query:modelProviderDTO:ModelProviderDTO,page:String,limit:String | envelope <PageData<ModelProviderDTO>> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R/W(model cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分✓1 |
|
||||
| J109 | `POST /models/provider` | `ModelProviderController.add` | Body:ModelProviderDTO | envelope <ModelProviderDTO> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(model/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分✓1(约束集合) |
|
||||
| J110 | `PUT /models/provider` | `ModelProviderController.edit` | Body:ModelProviderDTO | envelope <ModelProviderDTO> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(model/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J111 | `POST /models/provider/delete` | `ModelProviderController.delete` | Body:List<String> | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(model/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J112 | `GET /models/provider/plugin/names` | `ModelProviderController.getPluginNameList` | — | envelope <List<ModelProviderDTO>> | DB Token / — | DB-R; Redis-R/W(model cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J113 | `DELETE /models/{id}` | `ModelController.deleteModelConfig` | Path:id | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(model/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J114 | `GET /models/{id}` | `ModelController.getModelConfig` | Path:id | envelope <ModelConfigDTO> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R/W(model cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J115 | `GET /models/{modelId}/voices` | `ModelController.getVoiceList` | Path:modelId; Query:voiceName:String | envelope <List<VoiceDTO>> | DB Token / `sys:role:normal` | DB-R; Redis-R/W(model cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J116 | `GET /models/{modelType}/provideTypes` | `ModelController.getModelProviderList` | Path:modelType | envelope <List<ModelProviderDTO>> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R/W(model cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J117 | `POST /models/{modelType}/{provideCode}` | `ModelController.addModelConfig` | Path:modelType,provideCode; Body:ModelConfigBodyDTO | envelope <ModelConfigDTO> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(model/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J118 | `PUT /models/{modelType}/{provideCode}/{id}` | `ModelController.editModelConfig` | Path:modelType,provideCode,id; Body:ModelConfigBodyDTO | envelope <ModelConfigDTO> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(model/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(model,域级);差分— |
|
||||
| J119 | `GET /ota/` | `OTAController.getOTA` | — | 裸 text/plain | 匿名 / — | — | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分✓1(MIME/body) |
|
||||
| J120 | `POST /ota/` | `OTAController.checkOTAVersion` | Header:Device-Id,Client-Id; Body:DeviceReportReqDTO | 裸 application/json | 匿名 / — | DB/Redis-R(设备/固件/配置); HMAC/Base64/时间戳凭证 | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分✓4(必填/格式/凭证/密码学) |
|
||||
| J121 | `POST /ota/activate` | `OTAController.activateDevice` | Header:Device-Id,Client-Id | 裸 application/json | 匿名 / — | DB-R/W(device activation); Redis-R/W(TTL) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分✓3 |
|
||||
| J122 | `GET /otaMag` | `OTAMagController.page` | Query:params:Map<String, Object> | envelope <PageData<OtaEntity>> | DB Token / `sys:role:superAdmin` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J123 | `POST /otaMag` | `OTAMagController.save` | Body:OtaEntity | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W(OTA metadata) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分✓2(响应/DB) |
|
||||
| J124 | `GET /otaMag/download/{uuid}` | `OTAMagController.downloadFirmware` | Path:uuid | 流式/二进制 + 原下载 headers | 匿名 / — | Redis-R/W(一次性/次数); 文件-R/流式 | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分✓4(次数限制及二进制) |
|
||||
| J125 | `GET /otaMag/getDownloadUrl/{id}` | `OTAMagController.getDownloadUrl` | Path:id | envelope <String> | DB Token / `sys:role:superAdmin` | DB-R; Redis-W(download token TTL) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分间接✓(供下载链路) |
|
||||
| J126 | `POST /otaMag/upload` | `OTAMagController.uploadFirmware` | Multipart:file | envelope <String> | DB Token / `sys:role:superAdmin` | 文件-W(MD5/扩展名/大小) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分✓2(上传/扩展名错误) |
|
||||
| J127 | `POST /otaMag/uploadAssetsBin` | `OTAMagController.uploadAssetsBin` | Multipart:file | envelope <String> | DB Token / `sys:role:normal` | 文件-W(MD5/扩展名/大小) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J128 | `DELETE /otaMag/{id}` | `OTAMagController.delete` | Path:id | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W(OTA metadata); 文件-DEL | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J129 | `GET /otaMag/{id}` | `OTAMagController.get` | Path:id | envelope <OtaEntity> | DB Token / `sys:role:superAdmin` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J130 | `PUT /otaMag/{id}` | `OTAMagController.update` | Path:id; Body:OtaEntity | envelope <?> | DB Token / `sys:role:superAdmin` | DB-W(OTA metadata) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(device,域级);差分— |
|
||||
| J131 | `GET /ttsVoice` | `TimbreController.page` | Query:params:Map<String, Object> | envelope <PageData<TimbreDetailsVO>> | DB Token / `sys:role:superAdmin` | DB-R; Redis-R/W(timbre cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(timbre,域级);差分— |
|
||||
| J132 | `POST /ttsVoice` | `TimbreController.save` | Body:TimbreDataDTO | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(timbre/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(timbre,域级);差分— |
|
||||
| J133 | `POST /ttsVoice/delete` | `TimbreController.delete` | Body:String[] | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(timbre/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(timbre,域级);差分— |
|
||||
| J134 | `PUT /ttsVoice/{id}` | `TimbreController.update` | Path:id; Body:TimbreDataDTO | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W; Redis-DEL(timbre/config cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(timbre,域级);差分— |
|
||||
| J135 | `GET /user/captcha` | `LoginController.captcha` | Query:uuid:String | image/gif 二进制 | 匿名 / — | Redis-W(captcha TTL); GIF | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(security,域级);差分— |
|
||||
| J136 | `PUT /user/change-password` | `LoginController.changePassword` | Body:PasswordDTO | envelope <?> | DB Token / — | DB-W(user/token); Redis-R/DEL(SMS) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(security,域级);差分— |
|
||||
| J137 | `GET /user/info` | `LoginController.info` | — | envelope <UserDetail> | DB Token / — | DB-R; Redis-R/W(cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(security,域级);差分✓9(七语言/过期 Token/Long) |
|
||||
| J138 | `POST /user/login` | `LoginController.login` | Body:LoginDTO | envelope <TokenDTO> | 匿名 / — | DB-R/W(token); Redis-R/DEL(captcha) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(security,域级);差分— |
|
||||
| J139 | `GET /user/pub-config` | `LoginController.pubConfig` | — | envelope <Map<String, Object>> | 匿名 / — | DB-R; Redis-R/W(cache) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(security,域级);差分✓1 |
|
||||
| J140 | `POST /user/register` | `LoginController.register` | Body:LoginDTO | envelope <Void> | 匿名 / — | DB-W(user/token); Redis-R/DEL(SMS) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(security,域级);差分— |
|
||||
| J141 | `PUT /user/retrieve-password` | `LoginController.retrievePassword` | Body:RetrievePasswordDTO | envelope <?> | 匿名 / — | DB-W(user/token); Redis-R/DEL(SMS) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(security,域级);差分— |
|
||||
| J142 | `POST /user/smsVerification` | `LoginController.smsVerification` | Body:SmsVerificationDTO | envelope <Void> | 匿名 / — | Redis-R/W(TTL/频控); 外部-Aliyun SMS | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(security,域级);差分— |
|
||||
| J143 | `GET /voiceClone` | `VoiceCloneController.page` | Query:params:Map<String, Object> | envelope <PageData<VoiceCloneResponseDTO>> | DB Token / `sys:role:normal` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J144 | `POST /voiceClone/audio/{id}` | `VoiceCloneController.getAudioId` | Path:id | envelope <String> | DB Token / `sys:role:normal` | DB-R; Redis-W(one-shot URL) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J145 | `POST /voiceClone/cloneAudio` | `VoiceCloneController.cloneAudio` | Body:Map<String, String> | envelope <String> | DB Token / `sys:role:normal` | DB-R/W(train state); 文件-W; 外部-火山语音克隆 HTTP | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J146 | `GET /voiceClone/play/{uuid}` | `VoiceCloneController.playVoice` | Path:uuid | 流式/二进制 + 原下载 headers | 匿名 / — | Redis-R/DEL(one-shot); 文件/外部音频-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J147 | `POST /voiceClone/updateName` | `VoiceCloneController.updateName` | Body:Map<String, String> | envelope <String> | DB Token / `sys:role:normal` | DB-W(train record name) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J148 | `POST /voiceClone/upload` | `VoiceCloneController.uploadVoice` | Query:id:String; Multipart:voiceFile | envelope <String> | DB Token / `sys:role:normal` | DB-R/W(train state); 文件-W; 外部-火山语音克隆 HTTP | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J149 | `GET /voiceResource` | `VoiceResourceController.page` | Query:params:Map<String, Object> | envelope <PageData<VoiceCloneResponseDTO>> | DB Token / `sys:role:superAdmin` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J150 | `POST /voiceResource` | `VoiceResourceController.save` | Body:VoiceCloneDTO | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W(voice resource) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J151 | `GET /voiceResource/ttsPlatforms` | `VoiceResourceController.getTtsPlatformList` | — | envelope <List<Map<String, Object>>> | DB Token / `sys:role:superAdmin` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J152 | `GET /voiceResource/user/{userId}` | `VoiceResourceController.getByUserId` | Path:userId | envelope <List<VoiceCloneResponseDTO>> | DB Token / `sys:role:normal` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J153 | `DELETE /voiceResource/{id}` | `VoiceResourceController.delete` | Path:id | envelope <Void> | DB Token / `sys:role:superAdmin` | DB-W(voice resource) | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
| J154 | `GET /voiceResource/{id}` | `VoiceResourceController.get` | Path:id | envelope <VoiceCloneResponseDTO> | DB Token / `sys:role:superAdmin` | DB-R | 结构✓;请求面差分✓1;认证业务面差分✓1;领域✓(voiceclone,域级);差分— |
|
||||
|
||||
## 已观测差异与未覆盖面
|
||||
|
||||
- 154 条安全请求面差分最终全部一致。首轮曾发现 5 个空 Body 映射差异;修复 FastAPI 对
|
||||
Spring `HttpMessageNotReadableException` 的 code-500 语义后,重新从零执行才得到 154/154。
|
||||
- 154 条认证业务面差分最终全部一致;该轮使用有效鉴权与安全业务/校验输入,在不主动成功
|
||||
写入的前提下逐路由对照。证据是 `authenticated-route-results.json`,渲染器会在结果不是
|
||||
154/154、存在失败或跳过时硬失败。
|
||||
- 2026-07-20 的隔离差分报告未在 49 个 checks 中观测到响应/所选 headers/数据库副作用
|
||||
不一致;证据是 `main/manager-api-fastapi/compatibility/contract-results.json`,不是人工推断。
|
||||
- Hibernate Validator 的 `ConstraintViolation Set` 首条消息无稳定顺序;模型 provider 必填
|
||||
用例比较“消息属于 Java 声明约束集合”与相同错误码,而不伪造一个固定顺序。
|
||||
- OTA 时间戳/token 是动态值,差分先比较归一化结构,再分别校验两端 HMAC/Base64 密码学
|
||||
有效性;这属于有意的测试归一化,不是声称字节恒等。
|
||||
- 深度差分未直接命中的 133 条中,J125 是下载链路间接覆盖,另 132 条标为 `差分—`;
|
||||
它们有请求面、认证业务面与所属领域测试,但尚无逐路由成功+主要错误+副作用深度对照,不能据此宣称
|
||||
每一种业务状态均已逐接口行为等价。
|
||||
- FastAPI 额外提供上述 3 条消费者兼容路由与 live/ready 健康检查;它们没有 Java
|
||||
Controller 基线,属于明确、可回退的加法差异。
|
||||
- Java 把定时任务放在 Spring 进程;FastAPI 使用独立 jobs 进程和 Redis 分布式锁。这是
|
||||
部署拓扑差异,业务状态和幂等目标保持一致。
|
||||
- RAGFlow、阿里云短信、火山语音克隆、真实声纹、真实 LLM、真实 MQTT/MCP/WS 均未用
|
||||
生产凭证联调;自动化只证明 mock 请求格式、超时/错误映射/重试中的已覆盖场景。
|
||||
|
||||
## 可复现检查
|
||||
|
||||
```bash
|
||||
cd main/manager-api-fastapi
|
||||
.venv/bin/python scripts/extract_java_routes.py --output compatibility/java-routes.json
|
||||
.venv/bin/python scripts/extract_consumer_routes.py > /tmp/consumer-routes.json
|
||||
.venv/bin/pytest -q tests/test_java_route_manifest.py tests/test_consumer_route_manifest.py tests/test_compatibility_document.py
|
||||
```
|
||||
|
||||
逐接口差分的启动、隔离库、mock 与执行命令见 `docs/manager-api-fastapi-test-report.md`;
|
||||
本文件只陈述已落盘的结果,不把缺少真实密钥的外部联调列为通过。
|
||||
@@ -0,0 +1,290 @@
|
||||
# manager-api 到 FastAPI 迁移说明
|
||||
|
||||
## 目标与边界
|
||||
|
||||
`main/manager-api-fastapi` 是 `main/manager-api` 的兼容替代实现。迁移只替换管理 API
|
||||
进程,不改变 MySQL 表、Liquibase 历史、Redis 业务语义,也不要求 manager-web、
|
||||
manager-mobile 或 xiaozhi-server 修改现有 URL。Java 实现继续保留,作为行为基线和
|
||||
回滚实现。
|
||||
|
||||
本次迁移不采用双写。灰度期间,每个业务域在任一时刻只有一个写入方;读流量可以按
|
||||
请求切分,写流量必须按业务域整体切换。
|
||||
|
||||
## 架构
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
C["Web / Mobile / xiaozhi-server / Device"] --> N["Nginx /xiaozhi"]
|
||||
N --> A["FastAPI API workers"]
|
||||
A --> R["Routers"]
|
||||
R --> S["Services / transaction boundaries"]
|
||||
S --> P["Repositories / SQLAlchemy async"]
|
||||
S --> I["External integration clients"]
|
||||
P --> M[("Existing MySQL schema")]
|
||||
S --> D[("Redis Java compatibility layer")]
|
||||
J["Standalone jobs worker"] --> S
|
||||
L["Original Liquibase changelog"] --> X["Migration runner"]
|
||||
X --> M
|
||||
```
|
||||
|
||||
代码按职责分为:
|
||||
|
||||
- `app/routers`:URL、HTTP 方法、Header/Query/Body 绑定和响应形态。
|
||||
- `app/services`:权限后的业务规则、事务边界、跨表级联和外部调用编排。
|
||||
- `app/repositories`:现有 MySQL 表上的参数化 SQL、行锁和分页。
|
||||
- `app/schemas`:兼容现有字段别名及请求模型。
|
||||
- `app/integrations`:LLM、MQTT gateway、MCP、RAGFlow、语音克隆和声纹客户端。
|
||||
- `app/jobs`:独立的定时任务进程;API worker 不启动定时任务。
|
||||
- `app/core`:数据库 Token 认证、国际化、Java JSON/Long 兼容、SM2、Redis 编解码、
|
||||
Snowflake ID 和健康检查。
|
||||
|
||||
所有业务路由保留 `/xiaozhi` 前缀。普通 API 返回 `{code,msg,data}`;认证过滤器、
|
||||
业务异常和请求校验均由兼容处理器转换,不把 FastAPI 默认的 401、403 或 422 直接
|
||||
暴露给客户端。OTA、播放和文件下载等原本返回裸 JSON、文本或二进制的接口继续保持
|
||||
其原始响应类型。
|
||||
|
||||
## 数据库兼容策略
|
||||
|
||||
### Schema 与迁移
|
||||
|
||||
原目录 `main/manager-api/src/main/resources/db/changelog/` 仍是唯一 schema source of
|
||||
truth。不得把既有 changeset 改写成 Alembic,也不得修改已应用 changeset 的 ID、作者或
|
||||
校验和。
|
||||
|
||||
Python 部署必须先运行独立迁移镜像或 `scripts/run-migrations.sh`。迁移 runner 直接打包
|
||||
原 Java Liquibase 资源,因此与保留的 Java 服务使用相同的 `DATABASECHANGELOG` 历史。
|
||||
宿主机执行脚本时,`JAVA_RESOURCES_DIR` 必须指向仓库中的
|
||||
`main/manager-api/src/main/resources`;不要使用只存在于应用镜像内的
|
||||
`/opt/xiaozhi/java-resources`。本机隔离数据库的完整命令见“配置与启动 / 本地”。
|
||||
|
||||
`docker compose` 中 `manager-api-fastapi` 和 `manager-api-jobs` 都等待
|
||||
`manager-api-migrate` 成功退出,避免应用在未完成迁移时接流量。
|
||||
|
||||
### 事务、锁与 ID
|
||||
|
||||
- Service 层在一次事务中完成智能体创建/更新/删除、插件/标签/纠错词映射、设备解绑、
|
||||
快照恢复及其他多表操作;异常时显式回滚。
|
||||
- 智能体更新和恢复先对 `ai_agent` 执行 `SELECT ... FOR UPDATE`,序列化同一智能体的
|
||||
快照版本分配和状态令牌校验。
|
||||
- 快照版本仍使用 `(agent_id, version_no)` 唯一约束及同一事务内的 `MAX+1` 插入;行锁
|
||||
防止并发恢复或更新竞争。
|
||||
- 需要 Long 主键的管理表继续使用与 Java epoch/node/sequence 布局一致的 Snowflake
|
||||
生成器;原本使用 32 位 UUID 的业务表仍使用无连字符 UUID。
|
||||
- 不新增数据库外键,也不改表、索引、字符集或 MySQL 类型。
|
||||
|
||||
### Java/Python 并存
|
||||
|
||||
并存期必须给写请求建立确定的域路由,例如 `agent/*` 全部指向一个实现,不能把同一域
|
||||
中的增删改请求在 Java 与 Python 之间随机分配。建议域切换顺序为只读配置、系统管理、
|
||||
模型/音色、设备、智能体、知识库和外部集成。域回切前先停止该域的新写入并等待在途
|
||||
请求完成。
|
||||
|
||||
## Redis 兼容策略
|
||||
|
||||
默认继续使用 Java 已有 key 名称和 TTL。`app/core/redis.py` 实现 Spring Data
|
||||
`RedisSerializer.json()` 使用的 Jackson wire format,包括:
|
||||
|
||||
- Map 的 `@class`、List/Set 的 wrapper-array;
|
||||
- Object 槽位中 Long 的 `java.lang.Long` 包装;
|
||||
- `java.util.Date` 的 epoch 毫秒包装;
|
||||
- Java DTO/Entity 缓存所需的具体类名和字段类型;
|
||||
- Hash 缓存写入后的 86400 秒默认 TTL。
|
||||
|
||||
该兼容层使 Java 回滚进程可以读取 FastAPI 写入的缓存。应用启动和正常测试不会执行
|
||||
`FLUSHALL`;隔离测试脚本的 `reset` 只操作其自建 Redis 实例。定时任务使用 Redis
|
||||
分布式锁和自动续租 watchdog,即使启动多个 jobs 容器,同一个任务也只有一个执行者。
|
||||
|
||||
## 安全与协议兼容
|
||||
|
||||
- 用户 Token 仍保存在 `sys_user_token`,按数据库过期时间校验;没有替换为 JWT。
|
||||
- 登录密文继续使用 SM2 C1C3C2,黄金向量由 Java 和 Python 双向解密测试校验。
|
||||
- `/config/*`、聊天记录上报/摘要/标题及地址簿内部接口继续校验数据库中的
|
||||
`server.secret` Bearer 值。
|
||||
- OTA、WebSocket 和 MQTT 保留原 HMAC、Base64、时间戳、Client-Id/Device-Id 及 token
|
||||
格式。
|
||||
- 密钥只通过环境变量或原参数表提供;`.env.example` 和部署文档不包含真实凭证。
|
||||
|
||||
## 配置与启动
|
||||
|
||||
### 本地
|
||||
|
||||
`.env.example` 是容器 Compose 模板,其中的 `mysql`、`redis` 是 Compose 服务名,
|
||||
`/opt/xiaozhi/java-resources` 是镜像内路径,不能原样复制后用于宿主机进程。下面的流程
|
||||
显式使用 `127.0.0.1:13316` 上的隔离 MySQL、`127.0.0.1:16379` 上的隔离 Redis,以及
|
||||
仓库原始 Liquibase/i18n 资源;不会连接或迁移现有开发数据库:
|
||||
|
||||
```bash
|
||||
cd main/manager-api-fastapi
|
||||
uv sync --locked
|
||||
|
||||
./scripts/isolated-env.sh start
|
||||
|
||||
LIQUIBASE_URL='jdbc:mysql://127.0.0.1:13316/manager_fastapi_test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&allowMultiQueries=true' \
|
||||
LIQUIBASE_USERNAME='xiaozhi_test' \
|
||||
LIQUIBASE_PASSWORD='isolated-test-only' \
|
||||
JAVA_RESOURCES_DIR="$PWD/../manager-api/src/main/resources" \
|
||||
MAVEN_BIN="$PWD/../../.runtime/maven/bin/mvn" \
|
||||
MAVEN_LOCAL_REPOSITORY="$PWD/../../.runtime/m2" \
|
||||
JAVA_HOME="$PWD/../../.runtime/jdk" \
|
||||
./scripts/run-migrations.sh
|
||||
|
||||
eval "$(./scripts/isolated-env.sh env)"
|
||||
export APP_ENVIRONMENT=development
|
||||
export APP_DATABASE_URL="$TEST_FASTAPI_DATABASE_URL"
|
||||
export APP_REDIS_URL="$TEST_FASTAPI_REDIS_URL"
|
||||
export APP_JAVA_RESOURCES_DIR="$PWD/../manager-api/src/main/resources"
|
||||
export APP_UPLOAD_DIR="$PWD/.test-runtime/local-uploadfile"
|
||||
mkdir -p "$APP_UPLOAD_DIR"
|
||||
./scripts/start-api.sh
|
||||
```
|
||||
|
||||
上述 `eval` 会得到明确的 localhost URL(FastAPI 测试库和 Redis DB 2)。定时任务必须
|
||||
作为单独进程启动;新终端需要重复 `eval` 及四个 `APP_*` 路径/URL 导出,不能让 jobs
|
||||
进程落回 `.env.example` 的 Docker DNS:
|
||||
|
||||
```bash
|
||||
cd main/manager-api-fastapi
|
||||
eval "$(./scripts/isolated-env.sh env)"
|
||||
export APP_ENVIRONMENT=development
|
||||
export APP_DATABASE_URL="$TEST_FASTAPI_DATABASE_URL"
|
||||
export APP_REDIS_URL="$TEST_FASTAPI_REDIS_URL"
|
||||
export APP_JAVA_RESOURCES_DIR="$PWD/../manager-api/src/main/resources"
|
||||
export APP_UPLOAD_DIR="$PWD/.test-runtime/local-uploadfile"
|
||||
./scripts/start-jobs.sh
|
||||
```
|
||||
|
||||
API 默认监听 `0.0.0.0:8002`,兼容根路径为
|
||||
`http://127.0.0.1:8002/xiaozhi`。`APP_WORKERS` 可以大于 1;任务不会随 API worker
|
||||
复制。验证结束后运行 `./scripts/isolated-env.sh stop`。生产环境不得设置
|
||||
`APP_ALLOW_START_WITHOUT_DEPENDENCIES=true`。
|
||||
|
||||
仓库统一启动脚本继承当前 shell 的上述环境变量;FastAPI 是默认实现,保留的 Java
|
||||
实现可直接用于本地回滚:
|
||||
|
||||
```bash
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
scripts/restart-local-services.sh --manager-api fastapi --wait 180
|
||||
scripts/restart-local-services.sh --manager-api java --wait 180
|
||||
```
|
||||
|
||||
### 容器
|
||||
|
||||
下面的 `mysql`、`redis` 只在容器网络确实提供对应 DNS 名时有效,否则必须替换为该网络
|
||||
可访问的真实主机名。API 镜像内的 Java 资源路径是 `/opt/xiaozhi/java-resources`,迁移
|
||||
镜像则把同一仓库资源打包到 `/migration/java-resources`。以下示例中的凭证必须替换,
|
||||
并应通过部署平台的 secret 注入而不是提交到仓库:
|
||||
|
||||
```bash
|
||||
cd main/manager-api-fastapi
|
||||
export LIQUIBASE_URL='jdbc:mysql://mysql:3306/xiaozhi_esp32_server?serverTimezone=Asia/Shanghai'
|
||||
export MYSQL_USER='xiaozhi'
|
||||
export MYSQL_PASSWORD='replace-me'
|
||||
export FASTAPI_DATABASE_URL='mysql+asyncmy://xiaozhi:replace-me@mysql:3306/xiaozhi_esp32_server?charset=utf8mb4'
|
||||
export REDIS_URL='redis://redis:6379/0'
|
||||
export MANAGER_API_UPSTREAM='manager-api-fastapi:8002'
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
若 `MANAGER_API_UPLOAD_SOURCE` 指向保留 Java 服务的宿主机上传目录,必须在启动前让 Java
|
||||
运行用户与容器 UID 10001 都具备读写和目录遍历权限;不要盲目 `chown -R` 导致 Java 失去
|
||||
访问权,应使用部署环境的共享组或 ACL。空 named volume 在 Docker 通常会继承镜像中
|
||||
`/data/uploads` 的 UID,但并非所有 OCI runtime 都实现相同 copy-up 语义;必须以容器内
|
||||
UID 10001 做一次写入预检。`/xiaozhi/health/ready` 同时检查上传目录可写性,权限不正确时
|
||||
返回 HTTP 503 和 `data.uploads=false`,不得绕过该检查接入流量。Apple Container 的一次性
|
||||
卷初始化实测命令和结果记录在测试报告中。
|
||||
|
||||
`MANAGER_API_UPSTREAM` 默认值是 `manager-api-fastapi:8002`。修改变量后必须重建 Nginx
|
||||
容器才能重新渲染配置;切到 FastAPI 和整服务回滚到 Java 的命令分别为:
|
||||
|
||||
```bash
|
||||
MANAGER_API_UPSTREAM='manager-api-fastapi:8002' \
|
||||
docker compose up -d --no-deps --force-recreate manager-api-nginx
|
||||
|
||||
MANAGER_API_UPSTREAM='<Nginx 容器可访问的 Java 主机名或 IP>:8002' \
|
||||
docker compose up -d --no-deps --force-recreate manager-api-nginx
|
||||
```
|
||||
|
||||
Java 地址必须能从 Nginx 容器网络解析和访问。内置 Nginx 的这个变量会切换整个
|
||||
`/xiaozhi`;逐业务域灰度需要在上层网关按路径配置两个 upstream,仍须遵守“同一业务域
|
||||
只有一个写入方”。
|
||||
|
||||
API 镜像(包括 jobs 命令)和迁移镜像都声明 `USER 10001:10001`,以非 root 用户运行。
|
||||
Compose 还为 API/jobs 设置只读根文件系统和 `/tmp` tmpfs;`/data/uploads` 是它们唯一的
|
||||
持久写目录,并通过 `/app/uploadfile` 符号链接兼容数据库中的 Java 相对路径。迁移容器是
|
||||
一次性非 root 进程,但 Compose 没有把它标为只读根文件系统。Nginx 镜像没有声明
|
||||
非 root `USER`,不能将其描述为非 root 镜像;它通过 Compose 的 `read_only: true` 及
|
||||
`/var/cache/nginx`、`/var/run`、`/tmp` 三个 tmpfs 加固。Nginx 保留 `/xiaozhi/` 路径,
|
||||
关闭上传请求缓冲并设置 100 MiB 上限。健康检查分为:
|
||||
|
||||
- `/xiaozhi/health/live`:进程存活;
|
||||
- `/xiaozhi/health/ready`:MySQL、Redis 和上传目录写权限都可用才返回 HTTP 200,否则
|
||||
HTTP 503;`data.database`、`data.redis`、`data.uploads` 可直接定位失败项。
|
||||
|
||||
SIGTERM 触发 Uvicorn 优雅关闭;compose 给 API 40 秒清理在途请求和连接池。
|
||||
|
||||
### 容器验证边界
|
||||
|
||||
本次仓库内的实际容器运行验证使用 Apple Container 1.0.0,覆盖镜像构建、隔离 MySQL
|
||||
迁移、双 API worker、独立 jobs、Nginx 路由、只读文件系统、上传卷和 SIGTERM 优雅
|
||||
关闭。`docker-compose.yml` 已通过 YAML 解析和自动化部署断言,但当前验证主机没有执行
|
||||
`docker compose up`,因此不能把 Apple Container 的运行结果表述为 Docker Compose
|
||||
端到端通过。镜像摘要、实际命令和运行结果见
|
||||
`docs/manager-api-fastapi-test-report.md`;在目标 Docker/Compose 环境切流前,仍需按上面
|
||||
命令执行一次迁移、ready 检查和 Nginx upstream 冒烟测试。
|
||||
|
||||
## 灰度切流
|
||||
|
||||
1. 备份当前配置并确认 Java 基线健康;不要停止 Java。
|
||||
2. 对目标数据库执行原 Liquibase runner,确认 changeset 数量和校验和无差异。
|
||||
3. 启动 FastAPI API 但暂不接写流量,检查 live/ready、日志和外部 mock。Python jobs
|
||||
只在隔离环境验证后停止,生产中暂不持续运行,避免与 Java 调度器同时写入。
|
||||
4. 先镜像或回放脱敏的只读请求,比较状态、Body、Header、数据库读结果和缓存读取。
|
||||
5. 按业务域把只读流量从 1% 提升到 10%、50%、100%,监控错误码、P95、数据库连接、
|
||||
Redis 命中与外部服务错误。
|
||||
6. 对一个完整业务域建立维护窗口,停止该域 Java 新写入,等待在途事务结束,然后把该域
|
||||
写路由切到 FastAPI。记录切换时间和最后写入方。
|
||||
7. 逐域重复;稳定观察期内保留 Java 镜像、配置和回切路由。
|
||||
8. 所有域稳定后才把 jobs 所有权切给 Python;Java 的定时任务进程必须同时停用,避免
|
||||
两套调度器并行。
|
||||
|
||||
## 回滚
|
||||
|
||||
1. 冻结待回滚业务域的新写入,等待 FastAPI 在途请求和 jobs 当前轮次结束。
|
||||
2. 停止 Python jobs,确认 Redis 分布式锁已释放;不要清空 Redis。
|
||||
3. 将该域的 Nginx upstream 切回原 Java `manager-api`,保持 `/xiaozhi` 路径不变。
|
||||
4. 用 Java 健康检查和代表性读请求确认 Token、缓存、上传文件与数据库数据可读。
|
||||
5. 恢复 Java 写流量并记录回滚边界;不要让 FastAPI 继续写该域。
|
||||
6. 若问题来自新 changeset,只能新增一个经评审的 Liquibase 前向修复;不得删除或改写
|
||||
`DATABASECHANGELOG` 历史。
|
||||
|
||||
容器整服务回滚时,先把 `<JAVA_UPSTREAM>` 替换为 Nginx 容器可访问的真实地址,再只
|
||||
重建代理;`--no-deps` 可避免回滚命令意外重启 FastAPI 或重复执行迁移:
|
||||
|
||||
```bash
|
||||
MANAGER_API_UPSTREAM='<JAVA_UPSTREAM>:8002' \
|
||||
docker compose up -d --no-deps --force-recreate manager-api-nginx
|
||||
```
|
||||
|
||||
FastAPI 没有改变现有 schema,且 Redis 写入采用 Java 兼容格式,因此正常应用回滚不需要
|
||||
数据反向迁移。若外部系统已接收不可撤销操作,按对应供应商的业务补偿流程处理,不能用
|
||||
数据库回滚伪造外部成功或失败。
|
||||
|
||||
## 隔离验证
|
||||
|
||||
`scripts/isolated-env.sh` 只创建 `manager_java_test`、`manager_fastapi_test` 两个测试库和
|
||||
端口 `16379` 上的独立 Redis;其中的测试密码仅用于本机隔离环境。标准流程是:
|
||||
|
||||
```bash
|
||||
cd main/manager-api-fastapi
|
||||
./scripts/isolated-env.sh start
|
||||
./scripts/isolated-env.sh reset
|
||||
./scripts/isolated-env.sh migrate
|
||||
eval "$(./scripts/isolated-env.sh env)"
|
||||
.venv/bin/pytest -m integration -q
|
||||
./scripts/isolated-env.sh stop
|
||||
```
|
||||
|
||||
实际执行结果、差分用例和不能使用真实凭证完成的联调项记录在
|
||||
`docs/manager-api-fastapi-test-report.md`;逐接口状态记录在
|
||||
`docs/manager-api-fastapi-compatibility.md`。
|
||||
@@ -0,0 +1,582 @@
|
||||
# manager-api FastAPI 迁移测试报告
|
||||
|
||||
> 执行日期:2026-07-20(Asia/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/run;Compose 仅做静态验证,未伪装为 `docker compose up` |
|
||||
|
||||
最终可重复执行的全量测试、隔离差分、集成、构建和容器运行验证均为绿色。以下范围限制必须
|
||||
与绿色测试分开陈述:
|
||||
|
||||
1. 全部 154 条 Java 路由均执行了两次差分:一次未认证/非法请求,一次已认证安全业务/校验
|
||||
请求。第二个 runner 为保护隔离 fixture,有意不执行成功写入;49 个深度 checks 直接命中
|
||||
21 条路由并覆盖代表性成功、错误和数据库副作用。因此两层全路由差分仍不等同于每条路由
|
||||
的完整成功写入生命周期和全部错误路径差分。
|
||||
2. 没有真实凭证、生产网络或硬件的外部集成未被计入通过。
|
||||
|
||||
## 2. 验证环境
|
||||
|
||||
| 组件 | 实际版本/配置 |
|
||||
|---|---|
|
||||
| 主机 | macOS 27.0,arm64 |
|
||||
| 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 2;Java 单元验证显式使用 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 tests,0 failures,0 errors,0 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 skipped,12.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 通过;
|
||||
- mypy:70 个源文件无问题;
|
||||
- compileall:exit 0;
|
||||
- lock check 与 locked sync:exit 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
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- i18n:6 个 locale,每个 1527 keys,key 结构一致;
|
||||
- unit:5/5;
|
||||
- snapshot:13/13;
|
||||
- Vue 生产构建 exit 0(hash `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.20,compileall 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` 的通用 envelope,FastAPI 当时返回 `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 与可切换 upstream;Nginx
|
||||
镜像 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 硬件因没有真实凭证或设备而未联调,也没有在本报告中描述为已通过。
|
||||
Reference in New Issue
Block a user