Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/system-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,8 @@ jobs:
run: TEST_USERNAME="$E2E_USERNAME" TEST_PASSWORD="$E2E_PASSWORD" bash backend/test/run_tests.sh e2e
- name: Verify identity transaction and replayable secret publication
run: docker compose exec -T -e TEST_USERNAME="$E2E_USERNAME" -e TEST_PASSWORD="$E2E_PASSWORD" api uv run --no-sync --no-dev pytest test/integration/services/test_identity_admin_service.py test/integration/services/test_api_key_user_lifecycle.py test/integration/api/test_apikey_router.py -q
- name: Verify department HTTP counts and lifecycle
run: docker compose exec -T -e TEST_USERNAME="$E2E_USERNAME" -e TEST_PASSWORD="$E2E_PASSWORD" api uv run --no-sync --no-dev pytest test/integration/api/test_department_router.py -q
- name: Verify OIDC and MCP OAuth proxy log and cache boundaries
run: python3 scripts/verify_oidc_proxy.py
- name: Verify Workdir and Skill authorization
Expand Down
10 changes: 10 additions & 0 deletions backend/test/unit/sandbox/test_user_sandbox_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,13 @@ def _spy(*args, _name=name, _original=original, **kwargs):
assert (await backend.agrep("changed", f"{WORKSPACE_PATH}/project")).matches

assert calls == ["write", "read", "edit", "ls", "glob", "grep"]


def test_agent_filesystem_tools_do_not_expose_delete(backend):
"""检查真实注册的模型工具,恢复删除暴露时必须失败。"""
from yuxi.modules.agents.runtime.middlewares.filesystem import create_agent_filesystem_middleware

middleware = create_agent_filesystem_middleware(backend=backend)
names = {tool.name for tool in middleware.tools}
assert "read_file" in names
assert "delete" not in names
13 changes: 13 additions & 0 deletions docs/advanced/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,19 @@ docker compose --env-file .env.prod -f docker-compose.prod.yml logs --tail=200 a
docker compose --env-file .env.prod -f docker-compose.prod.yml logs -f api worker
```

### PostgreSQL 共享内存配额

开发与生产 Compose 为 PostgreSQL 配置 `shm_size: 1g`,这是 `/dev/shm` 的使用上限,不是启动时预占内存。并行查询共享内存不足时,可能出现 `could not resize shared memory segment ... No space left on device`;该错误不等同于数据盘已满。

已有部署修改该配置后需要重建 PostgreSQL 容器,`restart` 不会更新配额。重建会中断 API 与 worker 的数据库连接;在维护窗口停止接收任务、等待活动 Run 和后台作业收敛后执行:

```bash
docker compose --env-file .env.prod -f docker-compose.prod.yml up -d --no-deps --force-recreate postgres
docker compose --env-file .env.prod -f docker-compose.prod.yml exec postgres df -h /dev/shm
```

确认 `/dev/shm` 容量为 1.0G、PostgreSQL 健康且 `/api/system/ready` 恢复后,再开放入口。开发环境使用同样的服务命令并去掉生产配置参数。

### Redis 重建后恢复 worker

ARQ worker 不会在 Redis 容器重建后自动恢复连接。重建 Redis 后重启 worker:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Owner:backend/yuxi/modules/identity/repositories/departments.py

### 实现方案

`DepartmentRepository.list_with_user_count` 保持部门列表查询与 `created_at desc` 排序不变,改为追加一次 `SELECT department_id, COUNT(id) ... WHERE is_deleted = 0 AND department_id IS NOT NULL GROUP BY department_id` 聚合,在内存中按部门 id 回填 `user_count`(无用户部门计 0)。总计固定 2 次查询,与部门数无关。计数口径不变:仅统计有效用户;响应字段、结构与排序不变,前端与管理端无需同步变更。
`DepartmentRepository.list_with_user_count` 保持部门列表查询与 `created_at desc` 排序不变,改为追加一次 `SELECT department_id, COUNT(id) ... WHERE is_deleted = 0 AND department_id IS NOT NULL GROUP BY department_id` 聚合,在内存中按部门 id 回填 `user_count`(无用户部门计 0)。总计固定 2 次查询,与部门数无关。真实 HTTP 回归由 `system-tests.yml` 的部门 HTTP 步骤运行,回读新增与删除用户后的部门计数。计数口径不变:仅统计有效用户;响应字段、结构与排序不变,前端与管理端无需同步变更。

## 替代方案

Expand All @@ -28,7 +28,7 @@ Owner:backend/yuxi/modules/identity/repositories/departments.py
| 验收主张 | 失败面 | 语义 Owner | 直接证据 / 命令 | 负向案例 | 当前结果 |
|---|---|---|---|---|---|
| 计数不随部门数放大(固定 2 次查询) | 恢复逐部门 COUNT 循环 | `list_with_user_count` | `backend/test/unit/repositories/test_department_repository.py::test_list_with_user_count_uses_bounded_queries`(`_CountingSession` 计 execute 次数并断言各部门取值 1/0) | 恢复 N+1 循环后 `execute_count == 2` 断言失败(实测 1 failed) | Passed |
| `user_count` 增量等于新增有效用户数,删除用户不计入 | 聚合漏掉 `is_deleted` 过滤 | `GET /api/departments` 投影 | `backend/test/integration/api/test_department_router.py::test_departments_list_user_counts_aggregate_active_users`(建 3 用户删 1 后断言 baseline+2) | 移除 `is_deleted == 0` 过滤时断言失败 | Not run |
| `user_count` 增量等于新增有效用户数,删除用户不计入 | 聚合漏掉 `is_deleted` 过滤 | `GET /api/departments` 投影 | `backend/test/integration/api/test_department_router.py::test_departments_list_user_counts_aggregate_active_users`(独占 PostgreSQL、真实 TCP 路由与 JWT 鉴权;建 3 用户删 1 后断言 baseline+2) | 移除 `is_deleted == 0` 过滤时相同 HTTP 用例在最终计数断言失败(实测) | Passed |
| 回归不破坏既有部门行为 | 排序/字段投影漂移 | 部门路由与仓储既有单测 | `test_department_repository.py` 6 passed;`ruff check`/`format` 干净 | 不适用 | Passed |

未验证范围:真实 HTTP 集成用例已随 PR 提供,但本次重提环境未起新版服务栈,`Not run`;合并前 CI 的 PostgreSQL/API 集成任务会覆盖。
未验证范围:完整 Compose 部门 HTTP 步骤的新增部门用例尚待 CI 执行,`Not run`;独立 HTTP 与 PostgreSQL 验证不替代完整拓扑回归。
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ Owner:frontend/src/modules/settings/model/modelDirectory.js
| 失效后乱序完成的在途旧响应不污染共享目录 | epoch 缺失时旧响应覆盖新缓存 | `load` epoch 守卫 | `modelDirectory.test.js`「失效后乱序完成的在途旧响应不回写共享目录」 | 移除 epoch 守卫后用例失败于 `旧别名 !== 新别名`(实测);恢复后通过 | Passed |
| 加载期间切换 spec 不回写过期别名 | 过期结果覆盖新选中态 | `syncResolvedModelAlias` 捕获序检查 | `modelSelectorAlias.test.js`「慢目录返回期间切换 spec 时不回写过期别名」 | 移除捕获序检查后用例失败(实测 2 failed);恢复后通过 | Passed |

未验证范围:浏览器内真实页面视觉表现(浅/深色与响应式)`Not run`;引用栏"加载期间原始名变化放弃过期结果"与选择器同型,因 SSR 单实例内切换 props 受限,组件级负向用例 `Not run`(composable 层 epoch 与选择器层已覆盖同型缺陷)。
浏览器验证:独立页面装配真实选择器与消息引用组件,所有业务请求由浏览器合成响应;浅/深色、768/1440 像素宽度及中文长别名的 DOM 与截图已核对,目录未命中与 503 错误均回退原始名称,`Passed`。

未验证范围:完整登录会话中的真实后端模型目录链路 `Not run`;引用栏"加载期间原始名变化放弃过期结果"与选择器同型,因 SSR 单实例内切换 props 受限,组件级负向用例 `Not run`(composable 层 epoch 与选择器层已覆盖同型缺陷)。
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ Owner:backend/yuxi/infrastructure/document_parsing/engines/rapid_ocr.py

### 实现方案

`RapidOCRParser._get_model_params()` 请求 `PP-OCRv6` + `ModelType.SMALL`(rapidocr 3.9 wheel 自带的 det/rec 模型;cls 沿用包内 PP-OCRv4 方向分类模型),health 与日志文案同步。`docker/api.Dockerfile` 在创建运行身份时把 site-packages 内 `rapidocr/models/*.onnx` 预置到 `RAPIDOCR_MODEL_DIR` 指向目录,guard 测试同时交叉校验两份 Compose 注入的 `RAPIDOCR_MODEL_DIR` 与镜像预置目录一致(漂移即失败)。依赖下限提升为 `rapidocr>=3.9.0`(v6/SMALL 枚举自 3.9 才有)。健康检查保持"不加载引擎"的行为不变。
`RapidOCRParser._get_model_params()` 请求 `PP-OCRv6` + `ModelType.SMALL`(rapidocr 3.9 wheel 自带的 det/rec 模型;cls 沿用包内 PP-OCRv2 方向分类模型),health 与日志文案同步。`docker/api.Dockerfile` 在创建运行身份时把 site-packages 内 `rapidocr/models/*.onnx` 预置到 `RAPIDOCR_MODEL_DIR` 指向目录,guard 测试同时交叉校验两份 Compose 注入的 `RAPIDOCR_MODEL_DIR` 与镜像预置目录一致(漂移即失败)。依赖下限提升为 `rapidocr>=3.9.0`(v6/SMALL 枚举自 3.9 才有)。健康检查保持"不加载引擎"的行为不变。

## 替代方案

- 构建期预下载 PP-OCRv5 的 det/rec ONNX:拒绝。需为每个 rapidocr 版本维护 URL 与 SHA256,模型仍不在依赖闭包里,且 3.9 起的 API 本就不提供 v5 资源。
- 构建期预下载 PP-OCRv5 的 det/rec ONNX:拒绝。需为每个 rapidocr 版本维护 URL 与 SHA256,模型仍不在依赖闭包里,3.9.2 仍保留 v5 下载定义,但 wheel 未预置对应模型。
- 给 `~/.cache/rapidocr` 挂持久卷:拒绝。保留运行时网络依赖,只是把首次下载推迟到部署机,离线环境仍失败。
- 让 `check_health()` 真正实例化引擎:拒绝作为本次修复。它能提前暴露问题,但不能让离线环境解析成功,还会让配置页刷新反复加载模型。

Expand All @@ -33,7 +33,7 @@ Owner:backend/yuxi/infrastructure/document_parsing/engines/rapid_ocr.py
| 解析器请求的 det/rec 模型由 wheel 自带,无需下载 | 运行时联网 modelscope 下载,离线失败 | `_get_model_params` | `test/unit/knowledge/test_rapid_ocr_models.py::test_rapid_ocr_requests_models_bundled_in_wheel`(3.9.2 wheel 实测含 `PP-OCRv6_det_small.onnx`/`PP-OCRv6_rec_small.onnx`) | `::test_wheel_bundled_check_rejects_ppocrv5_mobile`:回退 v5+MOBILE 时 wheel 中不存在对应 ONNX,检查在正确原因上失败 | Passed |
| api 镜像预置且与 Compose 注入目录一致 | 目录为空或两处配置漂移使预置失效 | `docker/api.Dockerfile` + Compose `x-api-worker-env` | `test/unit/config/test_docker_compose_service_boundaries.py::test_api_image_preinstalls_bundled_rapidocr_models` | `::test_rapidocr_preinstall_guard_detects_missing_or_mismatched_copy`:删除拷贝步骤、拷错目录、改任一侧 `RAPIDOCR_MODEL_DIR` 时分别报对应缺陷 | Passed |
| 预置目录下加载引擎零网络请求并真实识别 | 首次解析隐式联网 | rapidocr DownloadFile SHA 命中跳过 | 预置 3 个包内 ONNX 到临时目录、`HTTP(S)_PROXY` 指向黑洞后 `RapidOCRParser().process_image("test/data/测试图片.png")` 成功识别中文(模型日志 `File exists and is valid`) | 同条件空目录请求 v5 抛 `DownloadFileException`(modelscope URL)——缺陷存在性 | Passed |
| 依赖下限与代码匹配 | 非 lock 环境安装 3.8 缺枚举/模型 | `pyproject.toml` / lock | `rapidocr>=3.9.0`;`uv lock` 仅更新该 specifier;`uv lock --check` 通过 | 3.8.4 wheel 实测无 v6 模型与枚举 | Passed |
| 依赖下限与代码匹配 | 非 lock 环境安装 3.8 缺枚举/模型 | `pyproject.toml` / lock | `rapidocr>=3.9.0`;lock 同时包含[文本解码](2026-10-26-parser-text-encoding.md)使用的 chardet 直接依赖及 typer marker 规范化;`uv lock --check` 通过 | 3.8.4 wheel 实测无 v6 模型与枚举 | Passed |
| 回归 | 既有解析行为漂移 | `test/unit/knowledge`、boundaries | `pytest test/unit/knowledge` → 390 passed / 1 skipped;rapidocr guard 2 passed;`ruff check`/`format` 干净 | 不适用 | Passed |

未验证范围:无出口网络目标环境的端到端 PDF 入库 `Not run`;v5/v6 同批文档逐份文本对比未做(发布说明口径差异已声明)。
Loading