Skip to content

⚠️ 自动镜像 · 此页由 docs-site/scripts/mirror-changelog.mjsdocs/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 域名,改用短标识符(adminpmqaannoviewer);LoginPage input type 从 email 改为 text 以支持非邮箱格式登录。

安全

  • 邀请频率限流InvitationService.check_daily_limit 24h 内同一 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),每次成功登录更新;UserOut schema 透出。
  • 失败登录详情增强:审计日志 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_REGISTRATION env 开关控制(默认关闭)。
  • 注册状态查询 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/page keyset cursor 分页 + 复合索引 / predictions.created_at 索引 + ADR-0006 partition 迁移设计)。规划与决策记录见 docs/plans/2026-05-06-v0.7.6-a-logical-bubble.mddocs/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 流程断裂。后端 ProjectCreate schema 新增 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 /> 单元素调用,不再管 notifOpen state。useNotifications(enabled) 接受布尔参数,关闭时停 30s polling。
  • ProjectGrid 卡片 DropdownMenu:项目卡片右下角次级动作「项目设置 / 导出 COCO/VOC/YOLO」从 inline 按钮收编到 DropdownMenu(复用 v0.5.5 通用组件),主操作「打开」保留独立。Icon 体系新增 moreMoreVertical from lucide-react)。
  • task.reopen 通知 fan-outPOST /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) 投递到 Celery audit 队列;新建 apps/api/app/workers/audit.py task 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 被强制 Noneeffective_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.5apps/api/app/main.py 的 FastAPI version 从 0.6.7 一并修到 0.7.5)。

性能

  • usePredictions gcTime: 30_000apps/web/src/hooks/usePredictions.ts query key 含 minConfidence,工作台连续调阈值产生新 key,旧 key 默认 5min GC 会堆积内存。改 30s GC(react-query v5 字段名 gcTime)。useAcceptPredictioninvalidateQueries 不动;WorkbenchShell.tsx 相邻题 prefetch 不动(同 key 命中 GC 自动延期)。

开发体验 / CI

  • codecov per-flag target 软启用.codecov.yml 拆分 project.defaultproject.{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.yml lint 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 兜底。
  • prebuild mtime if-changed:新建 apps/web/scripts/codegen-if-changed.mjs(54 行)比较 apps/api/openapi.snapshot.jsonsrc/api/generated/types.gen.ts mtime;前者新或后者缺即跑 pnpm codegen,否则 skip 并打印 "snapshot unchanged, skipping codegen"。OPENAPI_URL 环境变量被显式设置时仍强制重新生成(CI 场景)。package.json prebuild 改用此脚本。
  • ruff-format 移出 pre-commit.pre-commit-config.yaml 移除 ruff-format hook 条目(保留 ruff lint --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.json version0.7.40.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 typecheck PASS;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(backend flag)
  • 新增 apps/api/tests/test_openapi_contract.py 契约测试 + apps/api/openapi.snapshot.json(326KB,13340 行)作为前后端契约真值源;运行时与 snapshot 不一致即 fail
  • 新增 scripts/export_openapi.pyuv 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(frontend flag)
  • 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;新增 e2e job(Postgres + Redis service + 启 uvicorn + Playwright,continue-on-error: true 待 spec 写实后摘);pytest / vitest 都接 codecov
  • .github/workflows/docs.yml(新建):push 到 main 自动构建并发到 GitHub Pages(用 actions/configure-pages@v5enablement: 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.jsondocs: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 / faq
  • dev/(11 篇骨架):local-dev / testing / conventions / release / architecture{overview,backend-layers,frontend-layers,data-flow} / how-to
  • api/:iframe 嵌 Scalar 渲染 OpenAPI(standalone HTML in public/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-site
  • CLAUDE.md 文档索引扩到 README + docs-site + ADR

修真 bug(验证过程中顺手)

  • apps/web/src/components/CommandPalette.tsx:273 — 隐藏的 \u3000 全角空格(eslint no-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 模糊变量名 llock
  • apps/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 secretCODECOV_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 自动激活)
  • 首次 clonepnpm 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,纯格式无业务逻辑变化)

验证

bash
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_TRANSITIONSapps/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_feedbackreviewer 收 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,语法不允许的迁移算 failed
  • bulk-delete — B-DEFAULT 必跳过;task 接管复用单个删除路径的逻辑
  • bulk-reassign — 单事务原子;annotator_id / reviewer_id 任一可省(不传 = 不改),传 null = 清空;同步 cascade Task.assignee_id / Task.reviewer_id
  • bulk-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,倒序,包含两类:

  1. 直接 target_type='batch' AND target_id={id} 的事件(创建 / 状态迁移 / 驳回 / 删除)
  2. 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_projectapps/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 也能被新逻辑清理

改 · 关联数据集不再自建「默认包」+ 项目侧关联面板

问题:

  1. 每次关联数据集都自动建一个「{数据集} 默认包」batch,对不需要立刻分包的项目是噪音。
  2. 新建项目向导里「选了批次分包」实际不生效 —— 向导先 link(task 全进默认包),再调 split(split 只看 batch_id IS NULL 或 B-DEFAULT),找不到任何 task → split 失败但被 try/catch 吞掉,结果只剩默认包。
  3. 项目设置里没有反向关联数据集的入口,要去数据集页找。

修改:

  • 后端:DatasetService.link_projectapps/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=NULL
  • test_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_batchesannotator_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 / reviewer UserBrief 字段(apps/api/app/schemas/batch.py
  • _is_annotator_assignedbatch_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 / reviewer UserBrief 字段(apps/api/app/schemas/task.py
  • MyBatchItem / ReviewingBatchItem 加单值 reviewer / annotatorapps/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.py route 层调 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}/historyapps/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:JOIN project_members 找该用户参与的项目
  • created_from / created_toProject.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 条 audit
  • TestGlobalSearch:super_admin 通过 name 搜到项目
  • TestAnnotationHistoryEndpoint:合并 audit + comment 时间线

tests/test_batch_lifecycle.pytests/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.py 16 例覆盖;② 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_admin
  • active → annotating check_auto_transitions 自动驱动,REST 一律 403
  • annotating → reviewing:标注员(仅自己被分派的批次)/ owner / super_admin
  • reviewing → approved / rejected:reviewer / owner / super_admin
  • rejected → 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-102activeBatches 过滤同步纳入 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 /reject body 带 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 改写为:

python
# 仅把 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 提示原因;后端 transitiondraft → active 分支前校验 SELECT COUNT(*) WHERE batch_id = ?,否则 400 cannot activate empty batch

状态语义前端展示 + 通知路由

NotificationsPopoverbatch.rejected type label「驳回了批次」+ 路由感知跳转:reporter 跳 /projects/{pid}/annotate?batch={id};同时改造 bug_report.* 通知 — admin 跳 /bugs,提交者打开「我的反馈」抽屉并定位到该条(v0.7.0 新建 useBugDrawerStore zustand 控制器,App.tsx + FullScreenWorkbench 改用 store 替代 local state,BugReportDrawerfocusBugId prop 自动 loadDetail)。

测试覆盖(apps/api/tests/test_batch_lifecycle.py 16 例)

5 个 test class:

  1. TestTransitionAuth(6 例)— 标注员不能跳 approved;annotator 可主动 reviewing;reviewer 可 approved;owner 可 archive;annotator 不能 archive
  2. TestRejectBatchSoftReset(4 例)— 软重置语义、通知 fan-out、feedback 必填校验、annotator 不能 reject
  3. TestEmptyBatchActivation(2 例)— 空批次拒绝激活;非空可激活
  4. TestWithdrawCascade(1 例)— check_auto_transitions 在 reviewing 不主动反推
  5. 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/cleanup CTE 优化: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/notificationsapps/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 JSONBNotificationService.notify 在 INSERT 前查偏好,channels.in_app=false 跳过插入 + 不发 pubsub。新建 GET/PUT /notification-preferences REST,设置页加「通知偏好」段(4 个已知 type 的 in_app 开关)。email 字段保留但 UI 不显示(等 LLM 聚类去重 + SMTP 落地)
  • celery beat 软删附件清理:新建 apps/api/app/workers/cleanup.py + purge_soft_deleted_attachments task;celery_app 加 beat_schedule(每日 03:00 UTC),扫 7 天前软删的 annotation_comments 附件并从 MinIO 删除。运维侧需 deploy celery -A app.workers.celery_app beat(或 worker --beat 单进程)

前端

  • Wizard step 2 升级到完整 ClassEditor:从 ClassesSection 抽出 <ClassEditor> 受控组件(颜色 + 排序 + 删除 + 限额,~150 行),CreateProjectWizard step 2 把 form.classes: string[] 升级为 form.classRows: ClassRow[],提交时序列化为 classes + classes_configProjectCreate schema 加 classes_config 字段;create_project 改用 model_dump(exclude_none=True)
  • ProgressBar aiPct 真实化ProjectStats / _serialize_projectai_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 dashboardReviewerDashboardStatsreviewing_batches 列表(reviewer 跨批次审核),ReviewerDashboard 新增「审核中批次」段(卡片 row 显示 display_id · project · 任务数 · review N · 完成 K · 进度%),单击跳 /review?project=...&batch=...ReviewPage 接 query param 自动预选项目 + 批次
  • 项目卡批次概览ProjectStatsbatch_summary: {total, assigned, in_review},单 GROUP BY 批量查询。DashboardPage 项目行进度列下方加 mini 文案「N 个批次 · K 已分派 · M 审核中」(M 用 warning 色高亮)
  • UnlinkConfirmModal 输入名称二次确认DatasetsPage:UnlinkConfirmModal 当影响 task 数 > 0 时强制要求输入数据集名称才能确认(与 DangerSection 删项目强度对齐)
  • AuditPage 折叠 sessionStorage 持久化expandedReqIds Set 持久化到 sessionStorage(30min TTL),刷新页面后自动恢复展开状态
  • uploadBugScreenshot 失败 retry UI:v0.6.6 失败时静默降级为 toast warning + 无截图提交,改为停在表单内联红色 alert + 「重试上传 / 跳过截图提交」三按钮
  • usePopover 迁移AttributeForm.DescriptionPopover 迁移到统一 usePopover hook(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_approved hook:仍 no-op + TODO 注释;active learning 闭环依赖 ML backend / 训练队列基座(ROADMAP A · AI/模型 区列出)
  • 通知偏好邮件 digestnotification_preferences.channels.email 字段就位但 UI 不显示,依赖 LLM 聚类去重 + SMTP 落地
  • task.reopen 通知/auth/me/notifications 删除后,test_task_reopen_notification 暂跳过;将来如需复活,应改写为 reopen 端点 fan-out task.reopened type 到 NotificationService(已为通知偏好基础静音留好接口)

Migration / Deploy 注意事项

  1. alembic 0027/0028/0029 三个迁移彼此独立,可单独 downgrade。0028(in_progress_tasks 回填)在大表上需 monitor;建议生产 deploy 时 alembic 单独跑 + 观察。
  2. 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')
  3. celery beat 启用docker-compose 或 K8s 需新增 beat 服务(或共享 worker --beat);不启用则 celery 仅作 broker,软删附件清理不会触发(MinIO bucket lifecycle 180 天硬兜底仍生效)。
  4. 通知偏好默认 in_app=true:现网用户无 notification_preferences 记录时按全部接收处理,不会突然静音。

Released under the MIT License.