⚠️ 自动镜像 · 此页由
docs-site/scripts/mirror-changelog.mjs从docs/changelogs/0.7.x.md生成,请勿直接编辑此处;改源文件后pnpm docs:build会自动同步。
Changelog — 0.7.x
[0.7.8] - 2026-05-06
登录注册改进 + 安全加固 + 治理合规。
修复
- InviteRegisterForm 密码校验对齐后端:前端从 6 位放行修正为 8 位 + 大小写 + 数字,两个注册表单均添加实时密码强度指示器。
- LoginPage 测试账号 production 隐藏:
import.meta.env.MODE !== 'production'条件渲染,production 构建中完全不含测试账号信息。 - 测试账号简化:seed.py 测试邮箱去掉
@test.com域名,改用短标识符(admin、pm、qa、anno、viewer);LoginPage input type 从 email 改为 text 以支持非邮箱格式登录。
安全
- 邀请频率限流:
InvitationService.check_daily_limit24h 内同一 actor 上限MAX_INVITATIONS_PER_DAY(默认 30),超限返回 429。 - 会话管理:JWT payload 新增
jti(唯一标识)+gen(代际号)声明;新模块core/token_blacklist.py基于 Redis SETEX 实现;新端点POST /auth/logout(黑名单当前 token)、POST /auth/logout-all(递增代际号使所有旧 token 失效并返回新 token)。 - CORS 收紧:production 环境
allow_methods/allow_headers从通配符改为显式白名单(CORS_ALLOW_METHODS/CORS_ALLOW_HEADERS可配置)。 - 审计日志不可变:Alembic 0032 迁移添加 PostgreSQL BEFORE UPDATE/DELETE trigger 拒绝直接修改
audit_logs;GDPR 脱敏路径通过SET LOCAL "app.allow_audit_update" = 'true'豁免。
治理
- 数据导出审计:项目导出
GET /projects/{id}/export和批次导出现记录到audit_logs(action:project.export/batch.export),含 format 和 display_id。 - 最后登录追踪:User 模型新增
last_login_at字段(Alembic 0033),每次成功登录更新;UserOutschema 透出。 - 失败登录详情增强:审计日志
detail_json新增user_agent字段(截取前 256 字符)。 - ADR-0007 审计日志月分区:设计文档就位(
docs/adr/0007-audit-log-partitioning.md),实际迁移推迟到数据量触发条件。
文档
- DEV.md:新增「测试账号」速查表。
[0.7.7] - 2026-05-06
登录注册机制完善。 新增开放注册路径,允许用户自助注册为 Viewer(最低权限角色),管理员后续可提升角色。
新增
- 开放注册端点
POST /auth/register-open:rate-limit 3/min,注册后立即签发 JWT,默认角色 viewer(零写权限)。通过ALLOW_OPEN_REGISTRATIONenv 开关控制(默认关闭)。 - 注册状态查询
GET /auth/registration-status:公开端点,前端用于判断是否显示自助注册入口。 - RegisterPage 双模式:有
?token=走邀请流程(原行为不变);无 token 且开放注册已启用时显示自助注册表单(email + name + password)。 - LoginPage 注册链接:当开放注册启用时,登录页显示"没有账号?立即注册"链接。
- SettingsPage 系统信息:展示开放注册开关状态(只读)。
- 后端测试
test_open_registration.py:7 个用例覆盖开关关闭 403、成功注册 201、重复邮箱 409、弱密码 422 等场景。
[0.7.6] - 2026-05-06
功能补缺 + 治理深化。 v0.7.4/0.7.5 把测试与文档基建收尾后,本期回到 ROADMAP 三大主轴:A 节项目模块(Wizard 升级到 settings 完整组件 + 属性 schema 步骤)、批次状态机二阶段(reset → draft 终极重置)、v0.7.x 写时观察一次清(NotificationsPopover usePopover 迁移 / ProjectsPage 卡片 DropdownMenu 收编 /
task.reopen通知 fan-out / 批次 Kanban 看板视图);B 节性能 / 治理(AuditMiddleware Celery 异步 + AUDIT_ASYNC 开关 + sync fallback /GET /tasks/{id}/annotations/pagekeyset cursor 分页 + 复合索引 / predictions.created_at 索引 + ADR-0006 partition 迁移设计)。规划与决策记录见 docs/plans/2026-05-06-v0.7.6-a-logical-bubble.md 与 docs/adr/0006-predictions-partition-by-month.md。
项目模块(A 节)
- CreateProjectWizard 扩为 6 步:在原 5 步(类型 / 类别 / AI / 数据 / 成员)之间新增「属性 schema」步骤;从
AttributesSection抽出纯受控组件<AttributeSchemaEditor>(含 type / required / options / min-max / applies_to 完整能力 +validateAttributeFields共享校验)供 wizard 与 settings 复用,避免 onboarding 流程断裂。后端ProjectCreateschema 新增attribute_schema: AttributeSchema | None字段,POST /projects创建时一并落库。 - 批次 reset → draft 终极重置:
apps/api/app/services/batch.py新增BatchService.reset_to_draft(batch_id),复用reject_batch的「task 全回 pending、保留 annotation 与 is_active」语义,同时删除 task_locks 释放标注员锁、清review_feedback / reviewed_at / reviewed_by,绕过VALID_TRANSITIONS字典。新增POST /projects/{pid}/batches/{bid}/reset端点(owner-only,BatchReset.reason≥ 10 字强制)+ 新AuditAction.BATCH_RESET_TO_DRAFT。前端ResetBatchModal二次确认 + 显示影响 task 数 + 大字号警告。
v0.7.x 写时观察一次清
- NotificationsPopover 自包含 usePopover 迁移:组件从父级
open / onClose受控模式重构为自带 trigger button 的自包含组件(含 unread badge),TopBar 简化为<NotificationsPopover />单元素调用,不再管notifOpenstate。useNotifications(enabled)接受布尔参数,关闭时停 30s polling。 - ProjectGrid 卡片 DropdownMenu:项目卡片右下角次级动作「项目设置 / 导出 COCO/VOC/YOLO」从 inline 按钮收编到
⋮DropdownMenu(复用 v0.5.5 通用组件),主操作「打开」保留独立。Icon 体系新增more(MoreVertical from lucide-react)。 task.reopen通知 fan-out:POST /tasks/{id}/reopen端点在 audit 写入后调NotificationService.notify_many(type='task.reopened', user_ids=[原 reviewer_id]),payload 含 actor name + task display_id + reopened_count。test_task_reopen_notification解 v0.7.0 的 skip 重新启用(验证GET /api/v1/notifications命中)。- 批次 Kanban 看板视图:新建
BatchesKanbanView,按 7 个 BatchStatus 分列展示批次 mini-card(含 display_id / progress / annotator+reviewer stack),owner 视角支持 HTML5 drag-and-drop 拖拽迁移(前端镜像后端VALID_TRANSITIONS字典做 dryrun,非法目标列 drop 显示 toast,最终鉴权由后端)。BatchesSection 顶栏加[列表 \| 看板]toggle,URL?batch_view=kanban持久化。
性能 / 扩展(B 节)
- AuditMiddleware Celery 异步化:
apps/api/app/middleware/audit.py把 dispatch 同步 INSERT 改为persist_audit_entry.delay(payload)投递到 Celeryaudit队列;新建apps/api/app/workers/audit.pytask body。Settings.audit_async: bool = True开关;broker 不可用或 enqueue 异常时自动 fallback 到原同步路径,主请求路径耗时 < 0.1ms(vs. 旧 1-3ms)。 - Annotation keyset 分页:
GET /tasks/{id}/annotations/page?limit=200&cursor=新端点返回{ items, next_cursor },cursor 编码 base64({ts}|{id}),排序created_at DESC, id DESC,移植自 audit_logs 端点(apps/api/app/api/v1/audit_logs.py:76-143)。原GET /tasks/{id}/annotations数组返回保持兼容。alembic 0031 加ix_annotations_task_created_id (task_id, created_at, id)复合索引覆盖排序键。 - predictions.created_at 索引(Stage 1)+ ADR-0006:alembic 0031 同时加
ix_predictions_created_at单列索引解决 80% 时间过滤痛点。完整 RANGE(created_at) 月分区迁移因annotations.parent_prediction_id/prediction_metas.prediction_id复合 FK 化代价高,落到 ADR-0006 Stage 2 设计文档,触发条件:单月 INSERT > 100k 或 总行数 > 1M。
测试与基建
- 前端单测 +29 / 后端单测 +14:前端覆盖率 4.27% → 8.68%(新增 AttributeSchemaEditor / Modal / DropdownMenu / BatchesKanbanView / useClipboard 的 29 个测试,覆盖核心新组件 + 关键 hook)。后端新增
tests/test_v0_7_6.py覆盖 attribute_schema 创建 / reset → draft 6 起始状态矩阵 / reset 鉴权与 reason 校验 / annotation keyset 分页三页 + invalid cursor / audit task body 等 14 条用例;test_task_reopen_notification重启用。后端总数 109 → 146 PASS。 - codecov.yml 落地:仓库根新增
codecov.yml显式化 backend 60% / frontend 8% target(基线值反映现状)+ patch backend 70% / frontend 50%,全部 informational 不阻断 PR;硬阻断切换到 v0.7.7(前端持续 ≥ 25% 后)。flag_management配置 backend / frontend 两 flag 路径映射,ignore 列表覆盖 alembic / generated / e2e / .test.。
文档
- ADR-0006 predictions 表按月 RANGE 分区:记录两阶段实施(Stage 1 索引 + Stage 2 partition by RANGE)、为什么本期不直接做 Stage 2(FK 复合化代价 + 行数未到瓶颈)、监控触发条件。
- plan 文件 v0.7.6 实施计划:完整实施步骤 + 工程取舍记录(S3.5 / S8 推迟到 v0.7.7、S7 实际 8.68% < 30% 原目标 / S9 informational 而非硬阻断)。
[0.7.5] - 2026-05-05
性能 & DX 收尾。 v0.7.4 把测试与文档体系一次性建齐后留下若干"半成品 / 待激活"项(codecov 完全 informational、ruff-format 进 pre-commit 引发 121 文件 churn、CI 缺独立 typecheck、
prebuild每次跑 codegen)+ v0.6/v0.7 累积的几条小型治理 / 性能项(/health/celery缺、CORS 硬编码、predictions cache 5min GC)。本版本 6 项一次性收尾,让 v0.7.4 那波"封顶",不引入新主题。规划全文见 docs/plans/2026-05-05-ticklish-flask.md。
安全 / 运维
- CORS 配置化:
apps/api/app/main.py三个 localhost origin 硬编码 + 全本机端口 regex 放行 → 走Settings.cors_allow_origins+cors_allow_origin_regex,env 支持 JSON list 或逗号分隔字符串(@field_validator兼容两种格式)。environment="production"时 regex 被强制None(effective_cors_origin_regex),且cors_allow_origins为空时启动期 raise,避免误把 dev 正则带上线。.env.example加示例段。 /health/celery端点:apps/api/app/api/health.py加_check_celery():celery_app.control.inspect(timeout=2).ping()拿活 worker 列表,返回{status, latency_ms, active_count, workers};ping 返回None(无 worker 响应)→ status="error"。同时进/health聚合(version 字段从0.6.0修到0.7.5,apps/api/app/main.py的 FastAPI version 从0.6.7一并修到0.7.5)。
性能
usePredictionsgcTime: 30_000:apps/web/src/hooks/usePredictions.tsquery key 含minConfidence,工作台连续调阈值产生新 key,旧 key 默认 5min GC 会堆积内存。改 30s GC(react-query v5 字段名gcTime)。useAcceptPrediction的invalidateQueries不动;WorkbenchShell.tsx相邻题 prefetch 不动(同 key 命中 GC 自动延期)。
开发体验 / CI
- codecov per-flag target 软启用:
.codecov.yml拆分project.default→project.{backend,frontend},target 后端 60% / 前端 30%,threshold 5% → 2%;保持informational: true观察 1-2 周后切硬阻断。patch.default仍 auto。 - CI lint job 加
ruff format --check+ 独立pnpm typecheck:.github/workflows/ci.ymllint job 在ruff check之后追加ruff format --check apps/api/app apps/api/tests(兜底 ruff-format 移出 pre-commit);同步把注释里的 "等 snapshot 落盘后加 typecheck" 落地——lint job 跑pnpm codegen(吃 snapshot)+pnpm typecheck独立 step,不再依赖 vitest job 的pnpm build兜底。 prebuildmtime if-changed:新建apps/web/scripts/codegen-if-changed.mjs(54 行)比较apps/api/openapi.snapshot.json与src/api/generated/types.gen.tsmtime;前者新或后者缺即跑pnpm codegen,否则 skip 并打印 "snapshot unchanged, skipping codegen"。OPENAPI_URL环境变量被显式设置时仍强制重新生成(CI 场景)。package.jsonprebuild改用此脚本。ruff-format移出 pre-commit:.pre-commit-config.yaml移除ruff-formathook 条目(保留rufflint --fix)。本地 commit 速度提升、与编辑器 format on save 不冲突;CI 上述新增ruff format --check兜底。
文档
- ROADMAP.md 同步:删除 A 节「
Project.in_progress_tasks改 stored counter」(v0.7.0 alembic 0028 +_sync_project_counters()早已落,描述过时);删除 B 节「/health/celery」「OpenAPI codegen 加速」「ruff-format 移出 pre-commit」「覆盖率门槛软启用」「useInfiniteQuery 缓存 GC」(本期落地);优先级表移除对应 P3 / P2 行;新增「覆盖率门槛硬阻断」作为 v0.7.5 后续观察项。 apps/web/package.jsonversion:0.7.4→0.7.5。
文件清单
apps/api/app/api/health.py # +/celery 路由 + 进 health_all + version 0.7.5
apps/api/app/config.py # +cors_allow_origins / cors_allow_origin_regex + validator
apps/api/app/main.py # CORS 改读 settings + prod 守卫 + FastAPI version 0.7.5
apps/api/tests/test_health.py # 新建:7 用例(路由注册 + celery mock×2 + CORS×4)
apps/web/package.json # prebuild 改 if-changed 脚本 + version 0.7.5
apps/web/scripts/codegen-if-changed.mjs # 新建(54 行)
apps/web/src/hooks/usePredictions.ts # +gcTime: 30_000
.env.example # +CORS_ALLOW_ORIGINS / CORS_ALLOW_ORIGIN_REGEX / ENVIRONMENT
.pre-commit-config.yaml # 移除 ruff-format hook
.github/workflows/ci.yml # lint 加 ruff format --check + codegen + typecheck
.codecov.yml # target 60%/30% per-flag
ROADMAP.md / CHANGELOG.md # 同步
apps/api/openapi.snapshot.json # 刷新(含 /health/celery + version 0.7.5)
docs-site/api/openapi.json # 刷新验证
- 后端:
uv run pytest tests/test_health.py→ 7 PASS(路由注册 + celery mock + CORS 4 用例);全测试套未跑回归(与本期改动正交,仅依赖 settings 默认值不变) - 前端:
pnpm typecheckPASS;node scripts/codegen-if-changed.mjs首次重生成 / 二次 skip 行为符合预期 - pre-commit:本地
pre-commit run ruff-format --all-files仍 PASS(v0.7.4 存量已格式化),后续 commit 不再触发 format hook
[0.7.4] - 2026-05-05
测试与文档体系一次性建齐。 v0.7+ 阶段(CHANGELOG 2300+ 行、77 后端测试、216 前端文件)质量与知识传递的基础设施一直滞后于代码增长,本版本一次性把 4 块(测试 / 用户文档 / 开发文档 / API 文档)的骨架与红线都立起来,后续日常开发只填内容、不再补地基。规划全文见 docs/plans/2026-05-05-api-tender-moore.md。
测试
后端
- 接
pytest-cov+coverage[branch]:apps/api/pyproject.toml加[tool.coverage.run/report];addopts -q --cov=app --cov-report=xml;CI 上传 codecov(backendflag) - 新增
apps/api/tests/test_openapi_contract.py契约测试 +apps/api/openapi.snapshot.json(326KB,13340 行)作为前后端契约真值源;运行时与 snapshot 不一致即 fail - 新增
scripts/export_openapi.py:uv run python ../../scripts/export_openapi.py [--check]用于刷 / 校验 snapshot
前端
- 接 MSW(
apps/web/src/mocks/{server,handlers}.ts)+vitest.setup.ts自动 listen / resetHandlers / close;vite.config.ts 加 v8 coverage(lcov / text / html);CI 上传 codecov(frontendflag) - Playwright E2E 骨架:
apps/web/playwright.config.ts+e2e/tests/{auth,annotation,batch-flow}.spec.ts(spec 全.skip占位 +e2e/README.md详述工作流) - ESLint flat config(
apps/web/eslint.config.js)+ devDeps(eslint 9 / typescript-eslint / react-hooks / react-refresh / globals);新增pnpm typecheck/pnpm test:coverage/pnpm test:e2e脚本 - vitest exclude e2e(避免 Playwright spec 被当单测跑);codegen 默认输入改为本地
apps/api/openapi.snapshot.json(CI 不再依赖运行时 API)
CI / 工具链
.github/workflows/ci.yml重构:lint job 去掉|| true(ruff / eslint 真正阻断);vitest job 用 snapshot 替代运行时 dump;新增e2ejob(Postgres + Redis service + 启 uvicorn + Playwright,continue-on-error: true待 spec 写实后摘);pytest / vitest 都接 codecov.github/workflows/docs.yml(新建):push 到 main 自动构建并发到 GitHub Pages(用actions/configure-pages@v5的enablement: true自动激活 Pages).pre-commit-config.yaml(新建):trailing-whitespace / EOF / yaml / large-files / merge-conflict / ruff / ruff-format / eslint / tsc 全套.codecov.yml(新建):informational 模式(不阻断 PR),按 backend / frontend flag 分组上报- 顶层
package.json加docs:dev/docs:build/docs:preview/openapi:export/openapi:check/typecheck/test:e2e等脚本
文档
VitePress 文档站(新建 docs-site/)
三栏导航:用户手册 / 开发文档 / API 文档。pnpm docs:dev 本地预览,自动同步 OpenAPI snapshot;CI 自动发到 GitHub Pages
user-guide/(11 篇骨架):getting-started / workbench{bbox,polygon,keypoint,index} / projects{index,batch} / review / export / faqdev/(11 篇骨架):local-dev / testing / conventions / release / architecture{overview,backend-layers,frontend-layers,data-flow} / how-toapi/:iframe 嵌 Scalar 渲染 OpenAPI(standalone HTML inpublic/api-reference.html);构建期predev/prebuild自动从apps/api/openapi.snapshot.json同步到public/openapi.json
架构决策记录(新建 docs/adr/)
README.md写明 ADR 协议(Michael Nygard 模板、命名、何时写 / 何时不写)0001-record-architecture-decisions.md元决策落地,规划 0002-0005 待回填(FastAPI 选型 / OpenAPI codegen 工具 / Konva canvas / 任务锁状态机)
顶层文档
- 新建
README.md(仓库入口,含技术栈表 / 快速开始 / 测试命令 / 目录结构) DEV.md加 pre-commit setup 章节,索引指向 docs-siteCLAUDE.md文档索引扩到 README + docs-site + ADR
修真 bug(验证过程中顺手)
apps/web/src/components/CommandPalette.tsx:273— 隐藏的\u3000全角空格(eslintno-irregular-whitespace)apps/web/src/pages/Workbench/shell/CommentInput.tsx:167,170— 正则字符类内的 NBSP(/\s| /改/[\s\u00A0]/保留语义)apps/api/app/services/task_lock.py— 6 处 E741 模糊变量名l→lockapps/api/app/api/v1/datasets.py— 2 处 F841 未用变量apps/api/tests/test_notifications.py— 1 处 F841 未用变量apps/api/app/main.py— E402 noqa 位置错(多行 import 的 noqa 应在首行)apps/api/tests/test_task_reopen_notification.py— 3 处 E402(pytestmark.skip 后的 import 加 noqa)
Migration / Deploy 注意事项
- 无 alembic 迁移:本版纯基础设施 / 文档变更
- 新增 GitHub Actions secret:
CODECOV_TOKEN(去 codecov.io 取,加到 repo Settings → Secrets and variables → Actions);不加 CI 不阻断,仅 codecov 没数据 - GitHub Pages:repo 必须 public 才能用免费 Pages;Settings → Pages → Source 选 GitHub Actions(v0.7.4 通过
enablement: true自动激活) - 首次 clone:
pnpm install+pre-commit install(启用 git hooks)+pnpm exec playwright install chromium(首次跑 E2E 前装浏览器) - 存量代码 ruff-format churn:激活 ruff-format pre-commit hook 后存量代码首次走完整规范化,本版伴随一个
style: ruff-format 存量代码统一格式提交(121 文件 +8536 / -1395,纯格式无业务逻辑变化)
验证
pnpm openapi:check # snapshot 与运行时一致
pnpm test # 8 文件 / 64 测试
pnpm typecheck # tsc 0 错
pnpm exec eslint . --quiet # 0 错(35 warnings)
cd apps/api && uv run pytest tests/test_openapi_contract.py # 契约 2 通过
pnpm docs:build # VitePress 构建通过
pnpm exec playwright test --list # 配置有效(spec 全 .skip)CI 全绿(pytest / vitest / lint / e2e / openapi-contract),docs deploy 成功,文档站 yyq19990828.github.io/ai-annotation-platform 可访问。
后续 follow-ups
接续工作(E2E spec 写实、前端覆盖率拉到 30%、ADR 0002-0005 回填、用户手册关键页填实)已转移至 ROADMAP.md 的 P2 / P3 段。
[0.7.3] - 2026-05-05
批次状态机扩展 + 多选批量操作 + 操作历史 + 数据集关联简化。 当前批次状态机是严格单向流转(仅
rejected → active一条逆向边)。Owner / 超管误操作(错归档、漏审、误判)只能改库兜底,运维成本高且无审计;批次列表只支持单批次操作,项目尾期清理 / 跨批次调岗体验差。本版本一次性补齐 owner 逆向迁移 + 多选批量 + 批次操作历史 + 数据集 link/unlink 简化。
后端 — 管理员逆向迁移(owner-only)
新增 3 条逆向迁移到 VALID_TRANSITIONS(apps/api/app/services/batch.py),全部强制 reason(1-500 字,写入 audit_log.detail_json.reason):
| From → To | 副作用 | 通知 |
|---|---|---|
archived → active | 不动 task;调度器在下一次 task 操作时自动推进到正确阶段 | annotator + reviewer 收 batch.unarchived |
approved → reviewing | 清空 reviewed_at / reviewed_by / review_feedback | reviewer 收 batch.review_reopened |
rejected → reviewing | 不清反馈(reviewer 复审需看上次原因) | reviewer 收 batch.review_reopened |
assert_can_transition 增加 REVERSE_TRANSITIONS 集合 + owner-only 早返回;非 owner 直接 403。/transition 端点:当 (from, to) 命中逆向集合且 reason 缺失,400 拒绝;audit detail 加 reverse=true, reason=... 字段。
后端 — 多选批量操作
BatchService 新增 4 个 bulk 服务函数 + 4 个 POST /projects/{id}/batches/bulk-* 端点(owner-only):
bulk-archive— 已 archived 的算 skipped,语法不允许的迁移算 failedbulk-delete— B-DEFAULT 必跳过;task 接管复用单个删除路径的逻辑bulk-reassign— 单事务原子;annotator_id / reviewer_id 任一可省(不传 = 不改),传null= 清空;同步 cascadeTask.assignee_id/Task.reviewer_idbulk-activate— 逐个draft → active;前置不满足(无 annotator / 0 task)→ failed,不影响其他
统一返回 BulkBatchActionResponse{succeeded, skipped, failed};每个端点写一条聚合 audit(AuditAction.BULK_BATCH_*,新增 4 个),detail 含 batch_ids + 三组结果。
后端 — 批次操作历史端点
GET /projects/{id}/batches/{batch_id}/audit-logs?limit=50 —— 返回该批次相关的所有 audit_log,倒序,包含两类:
- 直接
target_type='batch' AND target_id={id}的事件(创建 / 状态迁移 / 驳回 / 删除) - bulk 类操作中提及到该批次的项目级事件(
detail_json @> {"batch_ids": [{id}]},PG JSONB 子集匹配)
依赖 require_project_visible,所有项目可见者皆可查看历史。
前端 — 多选 + 批量操作 UI
BatchesSection.tsx:
- 表头第一列加全选 Checkbox,每行加多选 Checkbox(B-DEFAULT 不可选,仅 owner 可见)
- 选中后表格上方出现浮层操作条:
已选 N 条 | 激活 | 改派 | 归档 | 删除 | 取消 - 归档 / 删除 / 激活:现有风格的二次确认 Modal
- 改派:新建
BulkReassignModal,标注员 / 审核员各一栏,「保留不变 / 清空指派 / 选某成员」三种语义 - 操作完成后展示「成功 N / 跳过 M / 失败 K」,失败 / 跳过原因走 inline 折叠面板(不刷屏 toast)
前端 — 逆向按钮 + 操作历史抽屉
BatchesSection.tsx行操作区为 owner 增加:archived 行「↩ 撤销归档」、approved 行「↩ 重开审核」、rejected 行「↩ 直接复审」(与现有「重新激活」并列)- 新建
ReverseTransitionModal:reason 输入(1-500 字,必填),与RejectBatchModal同款样式 - 行操作区增加「📜」按钮(所有角色可见)→ 打开新建
BatchAuditLogDrawer:时间 / 操作人(含角色徽章)/ 动作(i18n 映射)/ 详情(JSON 折叠);逆向迁移用reason醒目展示 Icon.tsx新增clock图标
测试
apps/api/tests/test_batch_lifecycle.py 新增 8 个用例(共 26 个,全过):
TestReverseTransitions— owner 撤销归档 / 缺 reason 拒绝 / approved → reviewing 字段清理 / rejected → reviewing 反馈保留 / 非 owner 拒绝TestBulkOperations— bulk archive 部分跳过 / bulk activate partial-success / bulk reassign 原子 + task 同步 / 非 owner 拒绝TestBatchAuditLogs— 直接 + bulk 事件都能在抽屉端点中返回
延后到 v0.7.x+:* → draft 终极重置、annotating → active 暂停(需 task 联动复位 + 调度器锁机制设计)。
Fix · 数据集取消关联后空壳 batch 残留
症状: link 自建的「{数据集} 默认包」batch(或用户从该 dataset 任务切出去的子 batch),在 unlink 后 task 被清空但 batch 自身保留为 total_tasks=0 的空壳挂在批次列表里。
修复: DatasetService.unlink_project(apps/api/app/services/dataset.py)在删 task 前记下「即将失去 task 的 batch 集合」(affected_batch_ids),重算计数器后把其中 total_tasks==0 且 display_id != 'B-DEFAULT' 的批次也删掉。返回值新增 deleted_batches / deleted_batch_ids,写入 audit dataset.unlink 的 detail。
preview-unlink 端点同步预测 will_delete_batches 数(与真实 unlink 行为对齐);前端 UnlinkConfirmModal 文案补「并清理 N 个失去全部任务的空批次」,toast 也带上数量。
新增 2 个测试(test_dataset_link.py):
test_unlink_cascades_user_split_batches— 用户把默认 task 池切成 2 个 batch 后 unlink,2 个 batch 都被清;管理员手工建的空草稿不被误删test_unlink_cascades_legacy_default_batch— 历史遗留的「默认包」batch 也能被新逻辑清理
改 · 关联数据集不再自建「默认包」+ 项目侧关联面板
问题:
- 每次关联数据集都自动建一个「{数据集} 默认包」batch,对不需要立刻分包的项目是噪音。
- 新建项目向导里「选了批次分包」实际不生效 —— 向导先 link(task 全进默认包),再调 split(split 只看
batch_id IS NULL或 B-DEFAULT),找不到任何 task → split 失败但被 try/catch 吞掉,结果只剩默认包。 - 项目设置里没有反向关联数据集的入口,要去数据集页找。
修改:
- 后端:
DatasetService.link_project(apps/api/app/services/dataset.py)不再自动创建「默认包」batch,新建 task 全部batch_id=NULL,走「未归类任务」语义。历史已存在的默认包不动(向后兼容;unlink走 v0.7.3 级联清理时同样能清掉)。 - 后端:新增
GET /projects/{id}/batches/unclassified-count—— 返回项目下batch_id IS NULL的任务数,给 BatchesSection 顶部横带用。 - 后端:新增
GET /projects/{id}/datasets—— 列出本项目已关联的所有数据集(含items_count/tasks_in_project/linked_at)。 - 前端:
BatchesSection顶部加「未归类 N 条 · 去分包」横带,点击「去分包」直接打开「创建批次 → 随机切分」流,用户选 N 一键完成。 - 前端:
ProjectSettingsPage增加「关联数据集」section(DatasetsSection.tsx),列出已关联 dataset + 关联新 dataset + 取消关联(复用 v0.7.3 unlink 级联 + 二次确认)。 - 前端:
CreateProjectWizard文案从「自动为每个数据集建一个独立批次」改为「任务作为未归类加入,可一并选择随机切分」。
副作用: 向导问题(#2)由此自然修复 —— link 不再占用 batch_id,split 找得到 task 了。
新增 4 个测试:
test_link_project_no_default_batch— link 不再自建 batch,task 全部 batch_id=NULLtest_unclassified_count_endpoint— 未归类计数端点正确test_project_datasets_endpoint— 项目侧 dataset 列表端点(含 task 数)- 旧
test_link_project_auto_creates_named_batch改为反向断言,作为test_link_project_no_default_batch
[0.7.2] - 2026-05-03
治理可视化 + 全局导航。一次性收口 5 项 ROADMAP open 项:批次单值分派 + 项目级圆周分派、责任人头像组、标注框历史可追溯、⌘K 全局搜索、Dashboard 高级筛选 + 网格视图。一次 alembic 迁移(0030)把批次分派从「list 多人」语义切换到「一 batch = 1 标注员 + 1 审核员」单值语义。
治理可视化
批次分派单值语义 + 项目级圆周分派(A · 批次相关延伸)
理念变更:每个 batch 是一个明确的工作单元,由 1 名标注员 负责标注 + 1 名审核员 负责审核。先前 v0.6.7 的 assigned_user_ids: list 多选语义被收紧。
数据模型(alembic 0030):
task_batches加annotator_id/reviewer_id单值列(FK users,ON DELETE SET NULL,加索引)- 数据迁移:JOIN
project_members把现有assigned_user_ids拆分到两列(按 role 取「第一个」),多人分派的批次只保留首位 assigned_user_ids列保留为派生兼容(BatchService._sync_assigned_user_ids维护[annotator_id, reviewer_id] filter None)
后端 API:
- 删除
POST /batches/{id}/distribute-evenly(task 级圆周打散与单值理念冲突) - 新增
POST /projects/{id}/batches/distribute-batches:把项目下未分派 / 全部 batch 在所选 annotator / reviewer 间圆周分派,每 batch 落到 1 个 annotator + 1 个 reviewer;同步级联更新Task.assignee_id/Task.reviewer_id BatchUpdate/BatchCreate/BatchSplitRequest字段从assigned_user_ids: list改为annotator_id+reviewer_id单值BatchOut增加annotator/reviewerUserBrief 字段(apps/api/app/schemas/batch.py)_is_annotator_assigned、batch_visibility_clause、/dashboard/annotator/batches等可见性路径全部从assigned_user_ids.contains(...)改为annotator_id == user.id
前端:
BatchAssignmentModal改为单选 radio(标注员段 + 审核员段),写annotator_id/reviewer_id- 新建
ProjectDistributeBatchesModal:勾选参与的 annotator / reviewer + 选「仅未分派 / 覆盖全部」+ 一键圆周分派 BatchesSection顶部新增「按项目分派批次」按钮触发上述 Modal
责任人可视化(A · Annotator/Reviewer 工作台 + Dashboard)
新建通用组件 apps/web/src/components/ui/AssigneeAvatarStack.tsx(最多 N 个头像 + 计数 + 角色 label),抽自 BatchesSection 行内实现,接入 4 处:
BatchesSection:分派列直接渲染[b.annotator, b.reviewer]头像MyBatchesCard(标注员 dashboard):行内显示「审核员」头像ReviewerDashboard(审核员 dashboard):审核中批次行内显示标注员头像Workbench Topbar:当前 task 顶部加「标注 @张三 · 审核 @李四」胶囊
后端:
TaskOut增加assignee/reviewerUserBrief 字段(apps/api/app/schemas/task.py)MyBatchItem/ReviewingBatchItem加单值reviewer/annotator(apps/api/app/schemas/dashboard.py)- 新建
apps/api/app/services/user_brief.py提供resolve_briefs/resolve_briefs_with_project_role一次 IN 解析,避免 N+1。
标注框编辑历史 / 审核历史可追溯(A · v0.7.x 后续观察)
后端把 annotation 完整生命周期落到 audit_logs:
AnnotationService.create / update / delete(在apps/api/app/api/v1/tasks.pyroute 层调AuditService.log(),target_type=annotation)- 评论 add / delete 升级为
ANNOTATION_COMMENT_ADD/ANNOTATION_COMMENT_DELETE(替代旧annotation.comment字符串) - 新增枚举
AuditAction.ANNOTATION_CREATE / UPDATE / DELETE / COMMENT_ADD / COMMENT_DELETE
新增端点 GET /annotations/{id}/history(apps/api/app/api/v1/annotation_history.py),合并三类事件按时间升序:
- 该 annotation 的 audit_logs(target_type='annotation')
- 关联 task 的 6 个关键 action(
task.submit/withdraw/review_claim/approve/reject/reopen) - 该 annotation 的所有 comments(含软删的,前端区分显示)
前端工作台 CommentsPanel 加 Tabs(评论 / 历史),切到「历史」tab 渲染新组件 AnnotationHistoryTimeline:纵向时间线 + 头像 + 角色 label + diff 缩略 + 相对时间。命名上避开 useAnnotationHistory(本地 undo/redo 栈),新 hook 叫 useAnnotationAuditHistory。
全局导航
⌘K Command Palette(A · TopBar / Dashboard 控件)
新增 GET /search?q=...&limit=5 跨实体聚合搜索端点(apps/api/app/api/v1/search.py),按当前用户可见性返回 4 类分组:projects / tasks / datasets / members:
- 项目:复用
_visible_project_filter - 任务:约束在可见项目下,按 display_id / file_name
ilike - 数据集:登录可见
- 成员:super_admin 全局;其他角色仅返回与自己同项目的成员
前端 apps/web/src/components/CommandPalette.tsx Modal palette:⌘K / Ctrl+K 全局触发(TopBar 注册 keydown,input/textarea 内不拦截),TopBar <SearchInput> 改为点击触发。键盘 ↑↓ 切换 / ↵ 跳转 / Esc 关闭。debounce 200ms(useGlobalSearch)。
Dashboard 高级筛选 + 网格视图(A · TopBar / Dashboard 控件)
GET /projects 扩展 4 个 query 参数(apps/api/app/api/v1/projects.py):
type_key(多值):按Project.type_key过滤member_id:JOINproject_members找该用户参与的项目created_from/created_to:Project.created_at区间
前端 pages/Dashboard/FilterDrawer.tsx 4 个 section(状态 / 类型 / 成员 / 创建时间):状态 / 类型用 chip 多选;成员段提供「我参与的」快捷 + 全部成员列表;时间段用原生 <input type="date">。Apply / Clear / Cancel 三键。pages/Dashboard/ProjectGrid.tsx 响应式 3 列项目卡,与 list 视图共享同一份 useProjects hook;视图切换状态写入 URL ?view=grid,刷新保持。Card 组件加 onClick prop。
测试
新增 apps/api/tests/test_v0_7_2.py:
TestProjectDistributeBatches:7 batch / 3 annotator / 2 reviewer 圆周 [3, 2, 2] 计数 + 每 batch 一人 + task 联动;only_unassigned 跳过已分派TestAnnotationAuditTrail:create/update/delete 各产出 1 条 auditTestGlobalSearch:super_admin 通过 name 搜到项目TestAnnotationHistoryEndpoint:合并 audit + comment 时间线
tests/test_batch_lifecycle.py、tests/test_task_batch_visibility.py 同步迁移到单值语义(seed 时同时写 annotator_id)。
兼容性
数据库迁移(alembic 0030)一次性把现有 assigned_user_ids 列拆到 annotator_id / reviewer_id 单值列。多人分派的批次仅保留首位。assigned_user_ids 列继续存在做向后兼容(service 层维护派生写入)。
[0.7.0] - 2026-05-03
两阶段集中收口:① 批次状态机重设计 epic(v0.6.10 调研立项的 P1)—— transition 鉴权矩阵、reviewer 可见性、批次级 review UI、reject_batch 软重置、空批次拦截、状态语义 + 通知接入、
test_batch_lifecycle.py16 例覆盖;② v0.6.x 后续观察 / 下版候选章节全部收尾(涉及 LLM 的留白)。共 3 个 alembic 迁移(0027/0028/0029),16 项功能 + 修复 + polish。
Phase 1 · 批次状态机重设计 epic(v0.6.10 调研立项)
transition 鉴权矩阵(P1)
PATCH /batches/{id}/transition 之前仅 require_project_visible 把关,任何项目成员都能任意推动状态。apps/api/app/services/batch.py:_assert_can_transition 抽出按 (from, to) → 角色 鉴权矩阵:
draft → active:仅 owner / super_adminactive → annotating:仅check_auto_transitions自动驱动,REST 一律 403annotating → reviewing:标注员(仅自己被分派的批次)/ owner / super_adminreviewing → approved / rejected:reviewer / owner / super_adminrejected → active/ 任意→ archived:owner / super_admin
403 错误明确返回 {"detail": "<role> cannot transition <from> -> <to>"} 便于前端 toast。reject 端点(apps/api/app/api/v1/batches.py)复用同一 helper,与 require_roles(*_REVIEWERS) 双重把关。
reviewer 可见性修复(P1)
apps/api/app/services/scheduler.py 拆出两个常量 + 角色感知 batch_visibility_clause(user):
ANNOTATOR_VISIBLE_BATCH_STATUSES = ['active', 'annotating', 'rejected']REVIEWER_VISIBLE_BATCH_STATUSES = ['active', 'annotating', 'reviewing']
reviewer 不受 assigned_user_ids 约束(跨批次审核场景)。rejected 状态对被分派的标注员特例放行——让标注员看到 reviewer 留言并继续重做(在 SQL 子句和 REST helper _assert_task_visible 双路径强制)。同步暴露 visible_batch_statuses_for(user) 给点查路径。apps/web/src/pages/Workbench/shell/WorkbenchShell.tsx:88-102 的 activeBatches 过滤同步纳入 rejected,让标注员可见 reviewer 反馈并重做。
批次级 review UI 全缺(P1)
apps/web/src/pages/Projects/sections/BatchesSection.tsx:235-261 之前仅 4 按钮(▶ 激活 / ↻ 重激活 / 🗄 归档 / 🗑 删除)。新增:
- 「✓ 提交质检」 (annotating → reviewing):owner / 被分派标注员可主动整批提交,不必等所有任务自动跳转
- 「✓ 通过」 (reviewing → approved):reviewer / owner,绿色按钮
- 「✗ 驳回」 (reviewing → rejected):弹
RejectBatchModal(新组件,500 字必填 textarea + 红色二次确认),调POST /rejectbody 带 feedback - rejected 批次行内联反馈:批次驳回后行下方显示 reviewer feedback 摘要(80 字截断 + tooltip 全文)
ReviewPage.tsx 整批退回按钮同步升级为 prompt 收集 feedback;useRejectBatch mutationFn 改为 { batchId, feedback },自动 invalidate notifications query。
reject_batch 软重置(方案 A,alembic 0027)
task_batches 新增 review_feedback / reviewed_at / reviewed_by 三列。reject_batch 改写为:
# 仅把 review/completed 任务回退到 pending;不动 is_labeled,不清 annotations.is_active
update(Task).where(Task.batch_id == batch_id, Task.status.in_(["review", "completed"])).values(status="pending")
batch.review_feedback = feedback; batch.reviewed_at = now; batch.reviewed_by = reviewer_id旧 v0.6.x 行为(status='pending', is_labeled=False,annotations 数据保留但 UI 与 DB 状态不一致)改为:标注员重进任务能看到自己之前画的框 + 顶部 reviewer 留言,自决改不改。批次驳回后 fan-out batch.rejected 通知给所有 assigned_user_ids,payload {batch_display_id, batch_name, project_id, feedback, affected_tasks}。
0-task 批次拦截
之前 owner 创建空批次后能直接「▶ 激活」永远卡在 active(check_auto_transitions 不处理空池)。前端 BatchesSection 「▶ 激活」按钮 disabled = assigned===0 || total_tasks===0 + hover title 提示原因;后端 transition 在 draft → active 分支前校验 SELECT COUNT(*) WHERE batch_id = ?,否则 400 cannot activate empty batch。
状态语义前端展示 + 通知路由
NotificationsPopover 加 batch.rejected type label「驳回了批次」+ 路由感知跳转:reporter 跳 /projects/{pid}/annotate?batch={id};同时改造 bug_report.* 通知 — admin 跳 /bugs,提交者打开「我的反馈」抽屉并定位到该条(v0.7.0 新建 useBugDrawerStore zustand 控制器,App.tsx + FullScreenWorkbench 改用 store 替代 local state,BugReportDrawer 接 focusBugId prop 自动 loadDetail)。
测试覆盖(apps/api/tests/test_batch_lifecycle.py 16 例)
5 个 test class:
TestTransitionAuth(6 例)— 标注员不能跳 approved;annotator 可主动 reviewing;reviewer 可 approved;owner 可 archive;annotator 不能 archiveTestRejectBatchSoftReset(4 例)— 软重置语义、通知 fan-out、feedback 必填校验、annotator 不能 rejectTestEmptyBatchActivation(2 例)— 空批次拒绝激活;非空可激活TestWithdrawCascade(1 例)— check_auto_transitions 在 reviewing 不主动反推TestReviewerVisibility(3 例)— reviewer 跨批次可见 reviewing;annotator 在 rejected 批次特例放行;未分派 annotator 不可见
Phase 2 · v0.6.x 收尾(18 项)
后端
Project.in_progress_tasks改 stored 列(alembic 0028):v0.6.7-hotfix 即时 COUNT 改为持久化列 + 一次性回填;batch._sync_project_counters在状态机变迁时同步维护;_serialize_project直接读字段,列 N 项目消除 N 次 COUNT 查询POST /orphan-tasks/cleanupCTE 优化:7 条ANY(:ids)数组序列化改为单子查询联查(WHERE id IN (orphan_subquery)),避免 10 万级孤儿场景下的 array overflow- link_project 同名 batch 去重命名:unlink → re-link 同 dataset 时新批次自动加
#N+1后缀(之前硬编码{ds.name} 默认包撞名) - 删 dead code
GET /auth/me/notifications:apps/api/app/api/v1/me.py:47-130端点 + audit-derived 派生函数全删,前端已切到新/notifications - bug_reports reopen 单独限流:评论 60/h 整体限流保留;reopen 路径加独立 5/day/user/report Redis 计数器,防止提交者刷 reopen 计数
- WS ConnectionPool + 心跳:
/ws/notifications之前每连接aioredis.from_url新建 socket,副本数 ↑ 时 Redis 连接数 = WS 连接数。引入模块级ConnectionPool.from_url(max_connections=200)共享池 + 30s 服务端 ping 帧防 LB idle timeout(默认 60s)。前端useNotificationSocket.ts识别 ping 帧不触发 invalidate - 通知偏好(基础静音 · alembic 0029):新建
notification_preferences (user_id, type)PK 表,channels JSONB;NotificationService.notify在 INSERT 前查偏好,channels.in_app=false跳过插入 + 不发 pubsub。新建GET/PUT /notification-preferencesREST,设置页加「通知偏好」段(4 个已知 type 的 in_app 开关)。email 字段保留但 UI 不显示(等 LLM 聚类去重 + SMTP 落地) - celery beat 软删附件清理:新建
apps/api/app/workers/cleanup.py+purge_soft_deleted_attachmentstask;celery_app 加beat_schedule(每日 03:00 UTC),扫 7 天前软删的annotation_comments附件并从 MinIO 删除。运维侧需 deploycelery -A app.workers.celery_app beat(或 worker --beat 单进程)
前端
- Wizard step 2 升级到完整 ClassEditor:从
ClassesSection抽出<ClassEditor>受控组件(颜色 + 排序 + 删除 + 限额,~150 行),CreateProjectWizardstep 2 把form.classes: string[]升级为form.classRows: ClassRow[],提交时序列化为classes + classes_config。ProjectCreateschema 加classes_config字段;create_project改用model_dump(exclude_none=True) - ProgressBar aiPct 真实化:
ProjectStats/_serialize_project加ai_completed_tasks字段(COUNT DISTINCT(task_id) WHERE parent_prediction_id IS NOT NULL AND is_active),列项目时单 GROUP BY 批量预查避免 N+1。DashboardPage:46删除pct * 0.6启发式,改Math.round(ai_completed_tasks / total * 100) - 批次级 reviewer dashboard:
ReviewerDashboardStats加reviewing_batches列表(reviewer 跨批次审核),ReviewerDashboard 新增「审核中批次」段(卡片 row 显示display_id · project · 任务数 · review N · 完成 K · 进度%),单击跳/review?project=...&batch=...。ReviewPage接 query param 自动预选项目 + 批次 - 项目卡批次概览:
ProjectStats加batch_summary: {total, assigned, in_review},单 GROUP BY 批量查询。DashboardPage项目行进度列下方加 mini 文案「N 个批次 · K 已分派 · M 审核中」(M 用 warning 色高亮) - UnlinkConfirmModal 输入名称二次确认:
DatasetsPage:UnlinkConfirmModal当影响 task 数 > 0 时强制要求输入数据集名称才能确认(与 DangerSection 删项目强度对齐) - AuditPage 折叠 sessionStorage 持久化:
expandedReqIdsSet 持久化到 sessionStorage(30min TTL),刷新页面后自动恢复展开状态 - uploadBugScreenshot 失败 retry UI:v0.6.6 失败时静默降级为 toast warning + 无截图提交,改为停在表单内联红色 alert + 「重试上传 / 跳过截图提交」三按钮
usePopover迁移:AttributeForm.DescriptionPopover迁移到统一usePopoverhook(NotificationsPopover 因父级 onClose 控制流不同,保留手写 click-outside;CanvasToolbar 实测无 popover 不需迁移;ROADMAP 写「4 处」与现状不符,CHANGELOG 中记录修正)
未做 / 留白(标注 v0.7.x)
- Wizard 新增「属性 schema」步骤:抽出
<AttributeSchemaEditor>给 Wizard 6 步流程使用 — 由于 Wizard 已 1009 行 + AttributeSection 250 行抽取链较深,本版仅完成类别步骤升级,属性 schema 步骤推迟 - NotificationsPopover usePopover 迁移:父级以
open / onClose控制流,迁移到usePopover需重构 TopBar 集成模式,本版保留现状 - ProjectsPage 卡片操作菜单收编 DropdownMenu:3 按钮(导出 / 设置 / 打开)合并到
⋮触发的 DropdownMenu,本版未做 on_batch_approvedhook:仍 no-op + TODO 注释;active learning 闭环依赖 ML backend / 训练队列基座(ROADMAP A · AI/模型 区列出)- 通知偏好邮件 digest:
notification_preferences.channels.email字段就位但 UI 不显示,依赖 LLM 聚类去重 + SMTP 落地 - task.reopen 通知:
/auth/me/notifications删除后,test_task_reopen_notification暂跳过;将来如需复活,应改写为 reopen 端点 fan-outtask.reopenedtype 到 NotificationService(已为通知偏好基础静音留好接口)
Migration / Deploy 注意事项
- alembic 0027/0028/0029 三个迁移彼此独立,可单独 downgrade。0028(in_progress_tasks 回填)在大表上需 monitor;建议生产 deploy 时 alembic 单独跑 + 观察。
- transition 鉴权收紧:v0.6.x 期间任何成员都能推任意状态;本版收紧后历史脏数据保留,仅对新动作生效。SQL 检测:
SELECT * FROM audit_logs WHERE action='batch.status_changed' AND actor_role='annotator' AND detail_json->>'after' NOT IN ('reviewing')。 - celery beat 启用:
docker-compose或 K8s 需新增 beat 服务(或共享worker --beat);不启用则 celery 仅作 broker,软删附件清理不会触发(MinIO bucket lifecycle 180 天硬兜底仍生效)。 - 通知偏好默认 in_app=true:现网用户无
notification_preferences记录时按全部接收处理,不会突然静音。