Skip to content

⚠️ 自动镜像 · 此页由 docs-site/scripts/mirror-changelog.mjsdocs/changelogs/0.5.x.md 生成,请勿直接编辑此处;改源文件后 pnpm docs:build 会自动同步。

Changelog — 0.5.x

[0.5.5 phase 2] - 2026-04-30 — Floating Noodle

一次性收口 phase 1(治理 / 基建)与 v0.5.4(工作台 polish)累计的 12 项遗留。不引入新功能,每一行改动都对应一条已立项的尾巴。9 项一次落到位,3 项核心动作落地、UI 抽屉 / 评论富文本等大件留作 v0.5.6。

治理 / 基建

A.1 OpenAPI → TypeScript codegen 基建

  • 新增 @hey-api/openapi-ts@^0.55.0 开发依赖 + apps/web/openapi-ts.config.ts(默认 OPENAPI_URL=http://localhost:8000/openapi.json,输出 src/api/generated/)。
  • package.json 加脚本 codegen / codegen:watchapps/web/.gitignore 屏蔽生成产物;不强加 prebuild gate(避免 CI 与 dev 启动循环依赖)。
  • 触发场景就是 phase 1 漏暴露 UserOut.is_active 的事故。手写 type → generated 的逐字段迁移走渐进路径,本期仅落基建。

A.2 后端 pytest 脚手架(轻量)

  • apps/api/pyproject.toml[project.optional-dependencies] test(pytest + pytest-asyncio + pytest-mock)+ [tool.pytest.ini_options](asyncio_mode=auto)。
  • apps/api/tests/{__init__,conftest,test_smoke}.pyapp_module / httpx_client fixture + 5 例 sanity(router 注册、attribute_schema hotkey 校验、iou 阈值范围、_build_base_query detail_filter、_PENDING_TASK_STATUSES 与 TaskStatus 同源)。
  • DB SAVEPOINT fixture / 真 PG client 留下一期(需独立 TEST_DATABASE_URL + alembic upgrade 配置)。

A.3 audit detail_json GIN 索引 + 字段级过滤

  • alembic 0015_audit_detail_gin_index:PG 创建 ix_audit_logs_detail_json_gin USING GIN;其它方言 noop。
  • _build_base_query()detail_key + detail_value 入参(JSONB @> 子集匹配,走 GIN)。/audit-logs/audit-logs/export 端点暴露同名 query 参数;export self-audit 行 detail 加 target_id_filter / actor_id_filter / detail_key_filter / detail_value_filter 字段。
  • AuditPage:筛选区加两个 detail 键名 / 键值 输入框(仅 super_admin),追溯 banner 显示 detail.role = super_admin 徽章。
  • 双行 UI(按 request_id 合并 metadata + business detail)留作 v0.5.6(实现成本中等,UI 风险大)。

A.4 IoU 去重阈值项目级可配

  • alembic 0014_project_iou_dedup_thresholdprojectsiou_dedup_threshold FLOAT DEFAULT 0.7 NOT NULL
  • Project model + ProjectOut/ProjectUpdate 加字段(pydantic Field(ge=0.3, le=0.95) 范围守卫)。
  • WorkbenchShell.tsx:218 硬编码 0.7currentProject?.iou_dedup_threshold ?? 0.7
  • GeneralSection(项目设置)加滑块 0.30 ~ 0.95(步长 0.05)+ 实时数值显示。

用户 / 权限完整化

B.1 project_admin 视角 UsersPage 按管理项目过滤

  • list_users() 后端按 actor 角色分流:super_admin 默认全量(可选 project_id 过滤);project_admin 强制限定到 Project.owner_id == actor.id 项目内 ProjectMember 集合 ∪ 自身
  • 前端 usersApi.list({ project_id }) 接受新参数;useUsers 类型同步。

B.2 删除带未完成任务用户先转交(409 二阶段)

  • DELETE /users/{id} 软删前查询 Task.assignee_id == target_id AND status in (pending, in_progress, review) 的 count + 5 个示例 id + TaskLock.user_id == target_id count;任一非零且未传 transfer_to_user_id409 + {reason:"has_pending_tasks", pending_task_count, locked_task_count, sample_task_ids, message}
  • 接受 body.transfer_to_user_id:校验 receiver active + 角色合法 + project_admin 时只能转给同管理项目内成员;UPDATE 全部 pending tasks assignee_id → 转交目标,DELETE 原 user 的 task_locks,再走原软删;audit_log user.delete detail 加 transferred_to / transferred_count / released_locks
  • apps/web/src/api/client.tsApiError.detailRaw 暴露后端结构化 detail(之前 dict 类型 detail 被吞),apiClient.delete 支持可选 body。
  • apps/web/src/api/users.tsusersApi.remove(id, opts?: { transfer_to_user_id })useDeleteUser 接受 { userId, transferToUserId } 形态。
  • UsersPage 删除 Modal 二阶段:检测到 ApiError.status === 409 && reason === "has_pending_tasks" → 切到"先转交"视图,显示 pending/locked 数 + 示例 ID + UserPicker 选接收者 + 二次提交。

响应式与组件抽取

C.1 窄屏 hamburger drawer

  • 新建 apps/web/src/components/shell/SidebarDrawer.tsx:Portal + 左滑动画(220ms ease-out)+ 遮罩点击 / Esc / 路由变化自动关闭 + body 滚动锁。
  • App.tsx:窄屏 < 1024px 时同时渲染占位 aside(保持 grid 完整)+ SidebarDrawer,复用同一 <Sidebar> 组件。
  • TopBarshowHamburger / onOpenDrawer props;窄屏时显示左侧 menu 按钮。
  • Icon.tsxmenu → Menu(lucide)。

C.2 通用 DropdownMenu 组件

  • 新建 apps/web/src/components/ui/DropdownMenu.tsx:trigger render-prop + items 数组 + outside-mousedown / Esc 关闭 + ↑↓ Home End 键盘导航 + role="menu" / "menuitem" + active 项 check 标记 + 支持 footer slot。
  • TopBar 主题切换 dropdown:60+ 行内联实现 → <DropdownMenu> 三选一 + 系统模式 footer hint。
  • 工作台 Topbar.tsx 智能切题 + 溢出菜单:双 useState/useRef/useEffect outside-close 重复实现 → 两个 <DropdownMenu>;老 menuItemStyle / kbdStyle 两个游离常量删除。
  • Button 组件改为 forwardRef(DropdownMenu trigger 需要 ref)。

工作台 polish

D.1 属性 schema hotkey 实际绑定

  • dispatchKey() ctx 加 attributeHotkey?: (digit) => AttributeHotkeyHit | null;数字键分支:选中态下 hotkey 命中且 type ∈ {boolean, select} → 返回 { type: "setAttribute", key, value }(boolean 取反 / select cycle 下一项),否则保留 setClassByDigit fallback。
  • WorkbenchShell 注入 lookup(合并 applies_to 过滤 + 取选中 annotation 的当前值)+ 处理 setAttribute action(走 handleUpdateAttributes 与现有表单 PATCH 路径同源)。
  • AttributeForm 字段 label 旁加 <KeyBadge>{f.hotkey}</KeyBadge>(仅 boolean / select 显示)。
  • 后端 _validate_attribute_schema 加 hotkey 守卫:必须单字符 1-9 + 仅 boolean / select 类型 + 全 schema 唯一。
  • hotkeys.test.ts 加 5 例(无选中 fallback / boolean toggle / select cycle / cycle 末尾绕回 / hotkey 不命中)。vitest 42 全过(27 hotkey + 10 iou + 5 新增)。

D.2 离线队列:多 tab 同步 + history tmp_id 替换

  • offlineQueue.tsBroadcastChannel("anno.offline-queue.v1"):persist 后 broadcast;监听其它 tab message → 重读 idb + 触发本 tab 订阅者;OfflineOp.create 加可选 tmpId 字段。
  • useAnnotationHistoryreplaceAnnotationId(tmpId, realId):扫 undo + redo 双栈,把 create / update / delete / acceptPrediction / batch 内嵌的 annotation id 整体替换,drain 后 history 不再误指 tmp_id。
  • 完整接入(WorkbenchShell create 路径分配 tmpId + drain 后调用 replaceAnnotationId + queue 详情抽屉 UI)留 v0.5.6;本期落核心管线,下期组装 UI。

导出器扩展

E.1 COCO / YOLO / VOC 导出读 attributes

  • ExportService.export_coco:每条 annotation 输出加 "attributes": ann.attributes or {},顶层 info.attribute_schema 写项目 schema。
  • ExportService.export_yolo:YOLO 文本不可扩展 → 伴生 <image_basename>.attrs.json per-image(行索引与 .txt 行号对齐)+ zip 根目录 attribute_schema.json
  • ExportService.export_voc<object> 下插 <extra> 节点。
  • GET /projects/{id}/export?include_attributes=bool(默认 true)入参;include_attributes=false 输出原版兼容格式(无属性扩展字段)。
  • 前端 projectsApi.exportProject(id, format, { includeAttributes }):默认携带;UI 复选框待 ExportSection 抽出后再加。

验证

  • apps/web/pnpm tsc -b ✅ 0 errors;pnpm vitest run42/42(hotkey 32 + iou 10)。
  • apps/api/from app.main import app ✅;pyproject.toml [project.optional-dependencies] test 解析正常;新增 5 例 pytest 用例语法正确(运行需先 pip install -e '.[test]')。
  • alembic:新增 0014 / 0015 两条 migration,按链 0013→0014→0015 接续;GIN 索引在 SQLite 测试库走 noop。
  • migration 与 model 字段一致:Project model + ProjectOut + ProjectUpdate 三处 iou_dedup_threshold 同步。

不在本期范围(明确 defer 到 v0.5.6+)

  • A.1:手写 type → generated/* 的逐字段迁移;prebuild gate;
  • A.2:DB-backed pytest fixture(SAVEPOINT 嵌入事务、alembic upgrade head per-session)+ 完整 audit_logs / 角色矩阵 / 删除转交端到端测试;
  • A.3:双行 UI 合并视图(按 request_id 把 metadata 行 + business detail 行折叠为单行 + 详情双栏);
  • C.1:通用 溢出菜单组件全站第 3 个使用方(如 ProjectsPage 卡片菜单);
  • D.2:OfflineQueueDrawer 抽屉 UI + WorkbenchShell create 路径接入 tmpId + drain 完成后调 replaceAnnotationId 与 queryClient.setQueryData;
  • D.3:评论 polish 整组(@ 提及 popover + 图片附件 presigned 上传 + alembic 0016 加 mentions / attachments / canvas_drawing 占位 + CommentInput contenteditable);
  • E.1:导出 UI「包含属性数据」复选框(待 ExportSection 抽出)。

[0.5.5] - 2026-04-30

v0.5.5 phase 1:把治理与底盘一次性补齐 —— 分级权限管理、审计正反向追溯、主题三档、响应式收尾、图标体系迁 Lucide。用户与权限页 / 审计日志页 / TopBar / 工作台 / 全站图标全部受影响,171 处 <Icon> 调用零改动平滑迁移。

新增

A · 分级权限管理(变更角色 / 删除账号)

  • 后端守卫矩阵apps/api/app/api/v1/users.py):
    • PATCH /users/{id}/role 入口从 SUPER_ADMIN 放宽到 _MANAGERS(super_admin + project_admin),内部按 actor.role × target.role 显式守卫:
      • super_admin:可改任意 target 为任意角色(自己 / 最后一名超管除外)。
      • project_admin:仅允许在 annotator ↔ reviewer 之间切换;target 必须出现在 actor 管理(Project.owner_id == actor.id)的项目里;不可造 project_admin / super_admin。
    • 新增 DELETE /users/{id} 软删除端点(is_active = False):同矩阵守卫;project_admin 还要求 target 仅在其管理项目里出现(跨项目用户 403 提示由 super_admin 处理);最后一名 active super_admin 不可被删 / 降级(兜底防自锁)。
    • POST /{id}/deactivate 入口同步放宽并复用同一组守卫,与 delete 行为对齐。
    • AuditAction.USER_DELETE = "user.delete";role_change / delete / deactivate 均记 actor / target / old / new 进 audit_logs。
    • 新增三个内部辅助:_count_active_super_admins() / _project_admin_manages_target() / _target_only_in_actor_projects()
  • 前端 EditUserModal 重写apps/web/src/components/users/EditUserModal.tsx):
    • ASSIGNABLE_ROLES_BY_ACTOR + DELETABLE_TARGET_ROLES_BY_ACTOR 双矩阵驱动 UI;下拉里允许出现的选项 = 当前角色 ∪ actor 可指派集合;project_admin 视角下显示「项目管理员仅能在审核员 / 标注员 之间切换」hint。
    • 「停用」按钮替换为「删除账号」(变体保持 danger + 二次确认 inline)。
    • 错误回显走 changeRole.error || assignGroup.error || deleteUser.error,复用 ApiClient 全局 toast。
  • UsersPage 行级操作apps/web/src/pages/Users/UsersPage.tsx):
    • 每行右侧三个按钮:📊 审计追溯(跳 /audit?actor_id=X) / ✏️ 编辑(仅 actor × target 命中矩阵时启用) / 🗑️ 删除账号(红色,独立 Modal 二次确认);自己那行不显示删除。
    • 独立删除 Modal:头像 / 邮箱 / 角色徽章 + 后端错误回显(不重弹 toast,避免双通知)。
    • 新增 useDeleteUser hook + usersApi.remove
  • UserOut schema 暴露 is_activeapps/api/app/schemas/user.py):之前未透传导致前端 u.is_active === undefined、删除按钮短路;现 is_active: bool = True 一并返回。

B · 审计正向反向追溯视图

  • 后端apps/api/app/api/v1/audit_logs.py):GET /audit-logs/audit-logs/export 新增 target_id 精确过滤;_build_base_query() 形参改为 (action, target_type, target_id, actor_id, from_, to)
  • AuditPage 入口与高亮apps/web/src/pages/Audit/AuditPage.tsx):
    • 启动读 URL ?action / ?target_type / ?target_id / ?actor_id 并初始化筛选;URL 变化(如 UsersPage 跳过来)触发 useEffect 重置筛选 + 回到第 1 页。
    • 顶部追溯模式 banner:紫色高亮带 Icon target + 当前过滤项徽章(操作人 / 对象类型 / 对象 ID / 动作)+ 「清除追溯」按钮。
    • 筛选区加 target_id 等宽输入框(精确匹配)。
  • 行内点击即追溯:表格 actor_email 列点击 → 用该 actor_id 进入追溯模式;target_type / target_id 列点击 → 联合追溯该对象。
  • 详情 Modal 双向跳转:「该操作人完整时间线」 + 「该对象完整时间线」按钮,关闭 Modal 同时切换筛选。
  • 入口辐射:UsersPage 行级 + ProjectSettingsPage 头部「审计追溯」按钮(仅 super_admin 可见,跳 /audit?target_type=project&target_id=X)。
  • api/audit.tsAuditQuerytarget_id?: string

C · 主题切换三档(日间 / 夜间 / 跟随系统)

  • TopBar 主题入口apps/web/src/components/shell/TopBar.tsx):通知按钮前新增主题切换按钮 + dropdown,显式列出三档 + 当前态 check 标记;system 模式下底部 hint 显示当前 resolved 主题。
  • 图标随 pref 切换:sun(日间)/ moon(夜间)/ monitor(跟随系统),点击外部自动关闭。
  • 复用 v0.5.3 已落的 useTheme hook(light / dark / system 三档 + localStorage 持久化 + prefers-color-scheme 监听 + initThemeFromStorage() 防首屏闪烁)。

D · 响应式收尾

  • AppShell 折叠apps/web/src/App.tsx):< 1024px 时 sidebar grid 列宽折为 0(保留布局完整性),用 useMediaQuery("(max-width: 1023px)") 切换。
  • 工作台移动端阻挡FullScreenWorkbench< 768px 时叠加全屏遮罩 <MobileWorkbenchBlock>:「请切换到桌面端 · 标注工作台依赖快捷键、画布鼠标交互和大屏侧栏」 + 建议宽度 ≥ 1024px。底层工作台仍渲染(保留只读视图),但所有交互被遮挡防误操作。

E · 图标体系迁移到 Lucide React

  • Icon.tsx 内部重写apps/web/src/components/ui/Icon.tsx):删除 ~70 行手写 SVG path 字符串,改为 60 项 name → Lucide 组件 映射表;对外 <Icon name="..." size stroke style className /> API 完全保留 → 177 处调用零改动
  • forwardRef 兼容:保留 ref 透传给 SVG 元素,不破坏现有 ref 用法。
  • 新增 icons:sun / moon / monitor / inbox(主题切换 + 追溯 banner 等新 UI 用);polygon → Hexagon、cube → Box、warning → AlertTriangle、edit → Pencil 等 Lucide 标准映射。
  • 新代码约定:写新功能直接 import { Layers, Sparkles, ... } from "lucide-react",不必走中间层;Icon.tsx 仅作存量兼容。
  • 依赖与体积pnpm --filter @anno/web add lucide-reactpnpm build 后 gzip 主 chunk 261.92 KB(迁移前 ~256 KB),增量 ~6 KB,符合 ROADMAP < 10 KB 验收。

验证

  • tsc --noEmit(apps/web)✅ 0 errors
  • vitest run ✅ 37/37 passed(hotkeys 27 + iou 10)
  • pnpm vite build ✅ 2022 modules, 1.79s
  • 后端 from app.main import app
  • 后端冒烟(router / schema / depends 导入)✅

不在本期范围

  • 前后端 schema 自动同步(OpenAPI → TS 类型生成)—— 本次 UserOut.is_active 漏暴露暴露的就是这个隐患的实例,留给 v0.5.x 续期。
  • project_admin 视角下 UsersPage 列表按管理项目自动过滤;删除带未完成任务的用户先转交 / 跨项目用户的精确显示。
  • 窄屏 hamburger drawer 触发完整 sidebar;通用 溢出菜单组件抽取(多页面共享)。
  • detail_json 字段级 PG GIN 索引筛选;审计中间件双行 UI 合并视图。
  • 后端 audit.export 端点 target_id 入参的端到端单测(手工验证已通过)。

[0.5.4] - 2026-04-30

新增

A · Polygon 顶点编辑(v0.5.3 polygon MVP 收尾)

  • 顶点拖动:选中已落库 polygon 后,圆点 handle 显式可拖;<ImageStage> Drag 联合类型新增 { kind:"polyVertex", id, vidx, start, cur },与 move/resize 通道并列;commit 走 useAnnotationHistory.push({kind:"update"}) 单条命令,Ctrl+Z 一键还原。
  • Alt+点击边新增顶点:边上挂 10px 透明 hit-stroke,alt 时 cursor 切 copy;点中即在最近边后插入鼠标投影点。
  • Shift+点击顶点删除:≤3 顶点时 toast 拒绝。
  • 自相交校验stage/polygonGeom.ts 提供 isSelfIntersecting()(O(n²) segment-pair 暴力,n 通常 < 50);commit 路径统一调用,违规时 stroke 切红 + 标签加 ⚠ + toast「polygon 自相交,已撤销」+ 几何回退。
  • 精确 IoU:引入 polygon-clipping@0.15.7stage/iou.tsiouShape()intersection() 求精确交并,polygon-vs-bbox 把 bbox 转 4 顶点同分支,bbox-vs-bbox 仍走原 iou()WorkbenchShell 视觉去重处把 iou() 调用换成 iouShape()iou.test.ts 新增 4 例 polygon 用例(identical / disjoint / 半重叠 / triangle vs bbox = 0.5)。
  • HotkeyCheatSheet:补三行说明(拖动顶点 / Alt+click 边 / Shift+click 顶点)。

B · 项目级属性 schema + Annotation.attributes(P1)

  • 数据模型:alembic 0012 一次性加 annotations.attributes JSONB DEFAULT '{}' + projects.attribute_schema JSONB DEFAULT '{"fields":[]}' + projects.classes_config JSONB DEFAULT '{}'(B + E 共一份 migration)。存量 0 影响。
  • Schema DSL:项目级声明属性列表 { key, label, type, required?, options?, min?, max?, applies_to?: "*"|string[], visible_if?, hotkey? }type 支持 text / number / boolean / select / multiselect / range;后端 pydantic _validate_attribute_schema 校验 key 唯一、type 枚举、select-必有 options。
  • 后端 APIPATCH /projects/{id}attribute_schemaPATCH /tasks/{id}/annotations/{aid}attributes 直接覆盖。AnnotationOut 新增 attributes 字段。
  • AttributeForm 组件shell/AttributeForm.tsx 根据 schema × class_name × annotation.attributes 动态渲染表单(react-hook 风格,无新 deps);visible_if 单层条件级联;改完防抖 400ms 上抛 PATCH。
  • AIInspectorPanel 接驳:选中态下方挂 <AttributeForm> + 缺失必填提示。
  • AttributesSection(项目设置页 tab):可视化增删改字段、上下移、导入 / 导出 JSON;保存时整包 PATCH /projects/{id}attribute_schema
  • 必填校验getMissingRequired() 工具函数;<Topbar> 提交质检按钮在任意 annotation 缺必填时 toast「存在必填属性未填,无法提交」+ 阻塞提交。

C · 逐框评论 annotation_comments

  • 数据模型:alembic 0013 建表(id, annotation_id, project_id, author_id, body, is_resolved, is_active, ts)+ 复合索引 (annotation_id, created_at desc)
  • 后端 APIapi/v1/annotation_comments.py 注册到 router,提供 GET/POST /annotations/{aid}/commentsPATCH/DELETE /comments/{id}。create 时写 audit_log action="annotation.comment" + target_type="annotation",自动经现有通知中心 30s 轮询可见。权限:作者或管理员可编辑/软删。
  • CommentsPanel 组件shell/CommentsPanel.tsx 渲染输入区 + 历史列表(作者名、时间、已解决徽章),「✓」切解决态、「🗑」作者可删。
  • AIInspectorPanel 接驳:选中态属性表单下方再挂评论区。
  • 通知中心映射utils/auditLabels.tsannotation.comment: "评论标注" + annotation.update: "编辑标注"AUDIT_TARGET_TYPESannotation

D · 自动保存 / 离线队列(P2)

  • idb-keyval@6 引入:仅 ~2KB;state/offlineQueue.ts 提供 enqueue / count / drain / subscribe / isOfflineCandidate 纯函数 API;持久化到 idb key anno.offline-queue.v1,incognito quota 失败时 try/catch 静默降级。
  • useOnlineStatus hook:监听 navigator.online/offline + 队列长度变更,输出 { online, queueCount }
  • WorkbenchShell mutation 包裹handlePickPendingClass / handleCommitMove / handleCommitResize / handleCommitPolygonGeometry / handleDeleteBox 的 onError 走 enqueueOnErrorisOfflineCandidate(err) (TypeError 网络断 / 5xx)时 enqueue + toast「已暂存到离线队列」;普通错误按原路径报错。
  • flushOffline 自动触发online 事件由 useEffect 监听,online 切回时自动 drain 队列;逐条 replay create/update/delete API;ok > 0 时 invalidate annotations / tasks query 并 toast「已同步 N 条离线操作」。
  • StatusBar 徽章:右侧加「离线 · N 操作待同步」/「暂存 · N」按钮(offline 红色 / online 黄色),点击立即重试。

E · Classes 升级(color + order + 拖排)

  • 数据模型:alembic 0012 加 projects.classes_config JSONB DEFAULT '{}',存量 classes 仍是 string[] 零变动;{name: {color, order}} 形式存元信息。
  • 后端校验:pydantic _validate_classes_config 校验 color 是 #RRGGBB、order 非负唯一。
  • 前端调色板stage/colors.ts 重构:classColor(name, config?) / classColorForCanvas(name, config?) 优先级 传入 config > 模块级 _activeConfig(项目当前) > 内置预设 > FNV-1a hash 派生;新增 setActiveClassesConfig() 模块级 setter,避免 ImageStage / SelectionOverlay / KonvaBox 等 10+ 处逐层透传 prop。
  • WorkbenchShell:项目加载时 useEffect(() => setActiveClassesConfig(currentProject?.classes_config), ...)classes 数组在工作台维度按 sortClassesByConfig() 重排。
  • ClassPalette:新增可选 classesConfig prop,左侧常驻图例颜色块走真实 hex。
  • ClassesSection(项目设置页新 tab):每个类别一行 [#] [色块] [名称] [color picker] [↑↓🗑];拖排(上下移)+ HTML5 <input type="color">;新增 / 删除;保存时 PATCH /projects/{id} 同时带 classes + classes_config

测试 / 工程

  • vitest 37 例全绿(v0.5.3 = 33;v0.5.4 = +4 polygon IoU)。
  • tsc -b 全绿
  • alembic upgrade/downgrade 双向通过:0010 ↔ 0011 ↔ 0012 ↔ 0013 全部往返成功。
  • 新依赖polygon-clipping@^0.15.7idb-keyval@^6.2.1(前端各 ~2-3KB gzip)。

调整 / 重构

  • KonvaPolygon 加 4 个新 props:points / selfIntersect / editable / onVertexMouseDown / onEdgeMouseDown;选中态自动渲染 vertex circle + 透明 edge hit-area。
  • <ImageStage> 新增 prop onCommitPolygonGeometry?: (id, before, after) => void
  • useTasks.useUpdateAnnotation 的乐观 onMutate 把 attributes 字段也合并到缓存,避免表单回显抖动。
  • auditLabels.ts 行业新增 annotation.update / annotation.commentAUDIT_TARGET_TYPESannotation
  • Icon.tsx 新增 chevUp / tag 两个 SVG 路径。

已知限制 / 待 v0.5.5

  • 属性 schema 的 hotkey 字段当前仅声明,不绑定(与 1-9 类别快捷键冲突协调归 v0.5.5)。
  • 离线队列:多 tab 同步、queue 详情抽屉 UI、history undo 链与 tmp_id 替换归 v0.5.5。
  • 评论:@ 提及、附件、画布层手绘批注归 v0.5.5。
  • classes 升级:导出器(COCO / YOLO)暂未读 attributes,schema 导出迁移归 v0.5.5。
  • polygon 精确 IoU 的项目级阈值(project.iou_dedup_threshold)仍硬编码 0.7。

[0.5.3] - 2026-04-30

新增

C.2 工作台 UI 信息架构重构(ToolDock + FloatingDock + 三段 Topbar)

  • 左侧 ToolDock(垂直工具栏):新建 apps/web/src/pages/Workbench/shell/ToolDock.tsx,从 tools/registry 自动渲染按钮(icon + hotkey tooltip + active 高亮)。新增工具仅需在 tools/ 注册并加入 ALL_TOOLS,外壳无需改动。
  • 画布右下 FloatingDock(悬浮工具岛):新建 apps/web/src/pages/Workbench/shell/FloatingDock.tsx,承载撤销 / 重做 / 缩放 / 缩放百分比 / 适应。锚定到画布容器左下角(避开右下 Minimap),与 Konva viewport 贴合。
  • Topbar 三段重构:原单行 9+ 控件挤爆 → grid 1fr auto 1fr 三段:左 = 标题 + index 徽章 (n / total);中 = 上一 / 提交质检 / 下一 / ⌄ 智能切题;右 = AI 一键预标 + ⋯ 溢出菜单(快捷键 + 主题切换)。1280px 单行不换行;行内元素 ≤ 8。
  • WorkbenchShell 主 grid 调整260/32 → 48 → 1fr → 280/32 四列,ToolDock 始终 48px 不随侧栏折叠。

C.2 暗色模式(与 B「主题切换」打底一并落地)

  • tokens.css [data-theme="dark"]:暗色覆盖 --color-bg / -elev / -sunken / -hover / -panel--color-fg / -muted / -subtle / -faint--color-border / -strong、accent / success / warning / danger / ai 系列(保持色相、提亮 lightness、降饱和度)、shadow(加深 alpha)、画布棋盘格新增 --color-canvas-checker-a/b 双 token。
  • useTheme hooklight | dark | system 三档,写 <html data-theme>,持久化到 localStorage["anno.theme"]system 模式监听 prefers-color-scheme 自动切换。
  • 启动注入 initThemeFromStorage()main.tsxcreateRoot 前应用初始主题,避免 first-paint 闪白。
  • Topbar 溢出菜单 <ThemeSwitcher>:三选一按钮组,亮色 / 暗色 / 跟随系统。

C.1 Konva 分层 hit-detection

  • <ImageStage> 拆 4 个 Layerbg(图像,listening:false 独立缓存)+ ai(AI 预测框)+ user(人工框 + 选中态)+ overlay(绘制预览 / pending 框 / polygon 草稿,listening:false)。user 框 move/resize 重绘不再连带触发 AI 层。

C.3 多边形工具(polygon)

  • 数据模型升级 → discriminated union geometryAnnotation.geometry / PredictionShape.geometry / AnnotationPayload.geometry 改为 {type:'bbox',x,y,w,h} | {type:'polygon',points:[[x,y],...]}。前端 Annotation 类型仍保留 x/y/w/h(包围盒)+ 新增可选 polygon: [number, number][],对存量读 .x/.y/.w/.h 的代码路径完全兼容;polygon 包围盒由 transforms.polygonBounds() 自动派生。
  • alembic 0011 migration:一次性给存量 annotations.geometrypredictions.result[*].geometrytype:"bbox" 字段;downgrade 反向移除。
  • 后端 pydantic geometry validatorAnnotationCreate / AnnotationUpdatefield_validator,校验 bbox 必有 x/y/w/h、polygon 必有 ≥ 3 个 [x,y] 顶点;兼容旧客户端无 type 时按 bbox 处理。
  • PolygonTool(hotkey P:实现 CanvasTool 接口;左键逐点落点 / 距首点 < 0.008 自动闭合 / 双击或 Enter 闭合 / Esc 取消 / Backspace 撤销最后一点。onPointerDown 通过 ToolPointerContext.polygonDraft 句柄 mutate Shell 维护的草稿状态,不走 setDrag 路径。
  • 画布渲染:新增 KonvaPolygon 组件(Konva Line closed=true + 半透明填充 + 标签锚到第一个顶点);ImageStage user/AI 层按 b.polygon 存在条件分流到 KonvaBox 或 KonvaPolygon。overlay 层渲染 polygon 草稿:已落点 + 跟随光标的预览段 + 顶点圆点 + 首点高亮(提示可闭合)。
  • 复用流程useClipboard 解耦硬编码 annotation_type:"bbox",按 source 是否含 polygon 分流:polygon 整体平移所有点;history useAnnotationHistory 命令模式已抽象 AnnotationPayload,无需改动;iou.ts 新增 iouShape() 形状无关入口(polygon 暂走包围盒近似,TODO 后续接 polygon-clipping)。
  • 限定:v0.5.3 polygon MVP = 创建 / 渲染 / 删除 / 改类别 / 撤销重做。顶点拖动 / Alt+点击边新增顶点 / Shift+点击删除顶点 / 自相交校验 / polygon-vs-bbox/polygon 精确 IoU 留 v0.5.4+。

Phase 1 · 工具层抽离(多工具基础设施)

  • tools/index.ts 接口激活CanvasTool 接口扩展 label / icon / cursor + onPointerDown(ctx) → DragInit | nullTOOL_REGISTRYALL_TOOLS 导出。新增 BboxTool.ts / HandTool.ts / PolygonTool.ts 独立模块。
  • ImageStage.handleStageMouseDown 改为委派:从 if-else 工具分支 → TOOL_REGISTRY[tool].onPointerDown(ctx),spacePan 时强制走 hand 工具。Drag init 类型 {kind:'draw'|'pan'}
  • hotkeys.tsdispatchKey() 纯函数:把键盘事件 + 简单 ctx 映射为 HotkeyAction 离散指令;WorkbenchShell 大 useEffect 改为 switch (action.type)。新增 apps/web/src/pages/Workbench/state/hotkeys.test.ts(27 例),覆盖修饰键 / 单键 / 上下文相关三组分支。
  • 快捷键补齐P = 多边形工具;Enter 闭合 polygon;Backspace 在 polygon 草稿态删最后一点(其余态删除选中框)。HotkeyCheatSheet 同步。

测试 / 工程

  • vitest 33 例全绿(v0.5.2 = 6 例 IoU;v0.5.3 = +27 例 hotkey dispatch)。
  • tsc -b 全绿:geometry discriminated union 升级未泄漏到任何调用点(通过 bboxGeom() / polygonGeom() 包装 helper 局部化)。

调整 / 重构

  • Topbar.tsx props 大幅精简:移除 tool / scale / canUndo / canRedo / onSetTool / onZoom* / onUndo / onRedo(已迁出到 ToolDock + FloatingDock);新增 taskIdx / taskTotal / overflowSlot
  • useWorkbenchStateTool 联合类型扩展:"box" | "hand""box" | "hand" | "polygon"
  • transforms.ts 新增 bboxGeom() / polygonGeom() / polygonBounds() / geometryToShape() 工具函数;annotationToBox / predictionsToBoxes 内部统一走 geometryToShape 派生包围盒。
  • useClipboard.ts:41 删除硬编码 annotation_type:"bbox";payload 形状按 source 形状自适应。

已知限制

  • polygon 顶点拖动 / 编辑 / 自相交校验 留 v0.5.4。
  • polygon 精确 IoU 暂用包围盒近似(视觉去重场景精度足够;TODO 接入 polygon-clipping 库)。
  • AI 模型当前不输出 polygon predictions(GroundingDINO 只 bbox);polygon 主要服务手工标。后续 SAM 接入会自然填补。

[0.5.2] - 2026-04-30

新增

C.3 多选 / 批量编辑 / 复制粘贴

  • 多选状态层useWorkbenchStateselectedId: string | null 之外新增 selectedIds: string[]selectedId 作为 primary(驱动 SelectionOverlay 浮按钮锚点 / 单体快捷键),selectedIds 包含 primary 在内的全部选中。新增 toggleSelected(id) / replaceSelected(ids[]) / setSelectedId(id)(后者会同步收敛 selectedIds 到 [id] 或 [])。AI 框始终单选。
  • Shift+点击叠加多选:画布与 AIInspectorPanel 列表都支持;onSelectBox(id, { shift }) 统一 API 由 Shell 层判断走 toggle 还是 replace。Shift+点空白不清空选择;普通点空白清空。
  • Ctrl+A 全选当前帧 user 框Esc 清空。
  • 批量删除(Delete)handleBatchDelete 并发 deleteAnnotation,全部 settled 后聚合 1 条 kind: "batch" 命令进 history 栈,单次 Ctrl+Z 一键还原;toast 报告 已删除 N/M 个标注
  • 批量改类(C 键 / SelectionOverlay "批量改类" 浮按钮):复用 <ClassPickerPopover>(标题切换为「批量改类别 (N 个)」),锚定到第一个选中框;commit 时并发 PATCH 每个选中框 class_name,settled 后聚合 batch update 命令。
  • 方向键平移:选中 ≥ 1 个 user 框时,方向键 1px / Shift+方向键 10px 平移;keydown 期间维护 nudgeMap: Map<id, Geom> 作为画布临时 override(与 drag 共享 overrideGeom 通道,drag > nudgeMap > 原值 优先级),keyup 时一次性把所有 nudge 落库为单条 batch update 命令。
  • 复制 / 粘贴 / 复制副本(Ctrl+C / Ctrl+V / Ctrl+D)useClipboard hook 维护本会话内存剪贴板(不跨任务,避免跨项目类别污染);粘贴 / 复制副本均走 (+10px, +10px) 偏移、clamp 到 [0,1] 边界;落库后批量进 history 栈,新副本自动成为多选。

C.3 / C.2 智能切题 + AI 框 IoU 视觉去重 + 撤销栈批量化

  • N / U 智能切题:Topbar 「智能切题」下拉(同时绑 N/U 单字母键)。
    • N(下一未标注):在已加载列表里找 idx > current && total_annotations === 0 && status !== "completed";列表末尾时自动 fetchNextPage。
    • U(下一最不确定):启发式 = 已加载列表中 total_predictions > 0 && total_annotations === 0,按 total_predictions desc 排第一名;后端精排(list_tasks ?order=conf_asc)作为 P2 待办留在 ROADMAP。
  • AI 框 IoU 视觉去重:与已确认 user 框 IoU > 0.7 且同类的 AI 框 → 画布层 stroke opacity 0.35(复用现有 fadedAiIds 通道)+ AIInspectorPanel 列表项整行 opacity 0.55 + 「已被覆盖」灰 tag。不删除,保留用户反悔空间。stage/iou.ts 纯函数 + 6 例 vitest 单测。
  • useAnnotationHistory batch 命令:新增 kind: "batch"(包裹一组非 batch 子命令),undo 时反序应用、redo 时正序应用;新增 pushBatch(commands[]):长度 1 时退化为 push 单条,> 1 时进 batch 命令。所有批量操作(删除、改类、复制粘贴、Ctrl+D、方向键平移)共享同一栈条目。

C.2 类别面板纯预览 + 快捷键补齐 + ETA + 阈值反馈

  • <ClassPalette>readOnly propTaskQueuePanel 左侧常驻类别面板改为 readOnly —— 鼠标 cursor: default,悬浮无 hover 着色,行点击与最近使用 chip 点击均无效;语义退化为「图例 + 快捷键速查」。<ClassPickerPopover> 内部仍保持交互态(popover 本身就是选类场景)。activeClass 写入权交给数字/字母键、popover、最近使用 record。
  • 快捷键补齐(11 条新增 + HotkeyCheatSheet 同步):Tab/Shift+Tab(user 框间循环)、J/K(不循环到边界停)、[/](阈值 ±0.05,clamp 0~1)、Ctrl+A/Ctrl+C/Ctrl+V/Ctrl+DN/U、方向键。hotkeys.ts 是 SoT,速查面板自动渲染。
  • useSessionStats ETA:每次切题记录与上次的间隔,ring buffer size 20,过滤 < 1.5s 误触和 > 30min 离座;< 10 题样本显示 ,达到后 StatusBar 输出 avg/题 · 剩 N · 约 mm:ssformatDuration 工具函数支持小时降级。
  • Topbar 阈值数值浮出反馈[/] 键调整阈值时右上角浮出 阈值 55%(1.5s 自动消失),便于盲调。Topbar 也接 confThreshold prop。

测试基座

  • vitest 引入pnpm --filter web add -D vitest@^2.1.0,第一组单测 stage/iou.test.ts(identical / disjoint / touching / half overlap / contained / zero-area 六例全过)。后续 hooks(useAnnotationHistory batch、useClipboard 偏移、useSessionStats ring buffer)单测扩展归 ROADMAP。

改动文件

  • 新建:apps/web/src/pages/Workbench/state/{useSessionStats,useClipboard}.tsapps/web/src/pages/Workbench/stage/{iou,iou.test}.ts
  • 重构:apps/web/src/pages/Workbench/state/{useWorkbenchState,useAnnotationHistory,hotkeys}.tsapps/web/src/pages/Workbench/shell/{WorkbenchShell,Topbar,StatusBar,AIInspectorPanel,TaskQueuePanel,ClassPalette}.tsxapps/web/src/pages/Workbench/stage/{ImageStage,SelectionOverlay,BoxListItem}.tsx
  • 文档:ROADMAP.md 删除 v0.5.1 / 当前迭代已完成的小项;新增 § A 协作并发(任务锁续约 + 编辑冲突 ETag)作为 P0;增补 ML Backend 协议契约文档 / Annotation 列表分页 / Konva 分层 hit / HotkeyCheatSheet 升级 / History sessionStorage 持久化 / IoU 阈值项目级可配等若干新发现项;优先级表整体翻新

兼容性

  • <ImageStage> 新增 selectedIds?: string[] / nudgeMap?: Map<string, Geom> / onBatchDelete? / onBatchChangeClass? 均为可选,onSelectBox 第二参数 opts?: { shift?: boolean } 也可选;<ReviewWorkbench> 走只读路径,零改动即可继续工作(多选在 readOnly 时被收敛到单选)。
  • <ClassPalette>onPick 现在是可选 prop(readOnly 时不需要);<TaskQueuePanel> 移除了 onSetActiveClass prop,对应外部调用同步迁移。

升级备注

  • 前端:pnpm install(拉 vitest);运行测试 pnpm --filter web exec vitest run iou.test.ts
  • 无后端 / DB / Docker 改动;版本号升至 0.5.2。

[0.5.1] - 2026-04-30

新增

标注流程重构 — 工具/类别解耦 + 类别选择 popover

  • 画完框再选类:原本"选类别 = 选工具",现在改为"选 bbox 工具 → 画框 → 弹 <ClassPickerPopover> 选类别 → 落库"。中间态 pendingDrawing 落在 useWorkbenchState,未确认前框以琥珀色虚线渲染(带 "? 待选类别" 标签);Esc / 点画布外 / 切工具 = 取消;Enter = 落到默认类别;1-9 / A-Z 键直选。
  • 已落库框可改类别:选中 user 框 → 三入口(① SelectionOverlay 浮按钮显示当前类色块 + "改类" 标签;② AIInspectorPanel 列表项内 "改类" 按钮;③ C 快捷键)→ 复用 <ClassPickerPopover>(标题切换为 "改类别 (当前: X)")→ 选定后走 PATCH /annotations/{id} 改 class_name 并 history.push({kind:"update"}) 进撤销栈,可 Ctrl+Z 还原。useWorkbenchState.editingClass 持有改类中间态。
  • useRecentClasses:localStorage 持久化每个项目的最近 5 个类别(key=recent-classes:${projectId}),popover 顶部 chip 行置顶;落库后自动 record。新进项目时默认类别选 recent 头部(如该项目仍存在)而非永远首类。
  • <ClassPalette> 共享组件:popover 与 TaskQueuePanel 类别面板共用一份组件;> 9 类自动启用搜索框 + 字母键 a-z 映射到 classes[9..];recent chip 行;快捷键徽章统一渲染。

C.1 渲染细节收尾

  • Minimap<Minimap> 缩略图导航(160×120,自动按图像 aspect 适配),仅当图像视口可视率 < 85% 时显示;点击 minimap 把视口中心移到该位置;视口矩形高亮当前可见区域。ImageStage 通过 onStageGeometry 把 imgW/imgH/vpSize 上抛,父级在 overlay slot 渲染。
  • AIInspectorPanel 虚拟化@tanstack/react-virtual 单列表合并 AI 段 + Header + 用户段;500 框 DOM 节点压到 < 30;滚到末尾自动 fetchNextPage 拉更多预测;底部 "加载更多 / 加载中" 兜底。
  • rAF 节流ImageStage 的 pointermove(draw / move / resize / pan)用 requestAnimationFrame 合并;240Hz 屏拉框 react-render 频率从 ~240/s 降到 60/s,1000 框 + 4K 图 pan/zoom 稳定 60fps。
  • 按需加载预测:后端 /tasks/{id}/predictionslimit + offset 参数,跨 Prediction 按 shape 置信度 desc 排序后切片再回到原 Prediction 容器;前端 usePredictionsuseInfiniteQuery,pageSize=100;阈值变更(debounce 300ms)触发 reset 重拉。

C.2 体验收尾

  • 响应式可折叠useMediaQuery hook(useSyncExternalStore + matchMedia);< 1024px 强制收两侧 sidebar;Topbar 按钮分组(视图/绘制/历史 + AI/导航)+ flexWrap 兜底,狭长窗口自动换行。
  • 骨架屏 + blurhash 占位<WorkbenchSkeleton> 三栏骨架(shimmer 动画)替代 isProjectLoading 文字;ImageStage 在真图加载前用 32×24 blurhash 解码 + blur(8px) 占位(v0.5.0 已注入 blurhash 字段,本次接通到画布层)。
  • 类别面板增强:TaskQueuePanel 类别区改用 <ClassPalette>,> 9 类启用搜索;最大高度 320px + 内部滚动避免挤占任务列表。

改动文件

  • 新建:apps/web/src/pages/Workbench/state/useRecentClasses.tsapps/web/src/pages/Workbench/shell/{ClassPalette,ClassPickerPopover,WorkbenchSkeleton}.tsxapps/web/src/pages/Workbench/stage/Minimap.tsxapps/web/src/hooks/useMediaQuery.ts
  • 重构:apps/web/src/pages/Workbench/state/useWorkbenchState.ts(pendingDrawing)、apps/web/src/pages/Workbench/stage/ImageStage.tsx(rAF 节流 + blurhash + pendingDrawing 渲染 + onStageGeometry / overlay slot)、apps/web/src/pages/Workbench/shell/{WorkbenchShell,TaskQueuePanel,AIInspectorPanel,Topbar}.tsxapps/web/src/hooks/usePredictions.ts(useInfiniteQuery)、apps/web/src/api/predictions.tsapps/web/src/pages/Review/ReviewWorkbench.tsx
  • 后端:apps/api/app/api/v1/tasks.py(predictions endpoint 加 limit/offset,跨 Prediction 按 shape 置信度排序)

兼容性

  • usePredictions(taskId) 返回 data: PredictionResponse[] → 现在返回 useInfiniteQuery 结果。两个调用点(WorkbenchShell / ReviewWorkbench)已同步迁移到 data.pages.flatMap(p => p)

[0.5.0] - 2026-04-29

新增

画布引擎 — Konva 切换

  • ImageStage 全面切 Konva:底层从 DOM <div> 矩形切换为 react-konva@18 Stage/Layer/Rect,解除 200+ 框掉帧的硬天花板;KonvaImage 配合 use-image hook 加载原图,stage.scaleX/Y/x/y 接管视口变换
  • SelectionOverlay HTML 浮层:采纳 / 驳回 / 删除浮动按钮移出 Stage,以绝对定位 React div 叠在 Stage 外层,按 bbox 右下角 (box.x+w)*imgW*scale+tx 投影到容器坐标;支持 Tab 聚焦 + 键盘可达
  • BboxTool / HandTool 抽象stage/tools/index.ts 定义 CanvasTool 接口(id / hotkey / onMouseDown / onMouseMove / onMouseUp);v0.5.1 增 polygon / keypoint 只需添加 Tool 模块,不动 ImageStage
  • 颜色兼容classColorForCanvas() 通过 Canvas 2D fillStyle 把 oklch 转为 Konva 可用的 hex;resize 锚点按 HANDLE_SCREEN_PX / vp.scale 保持屏幕像素恒定大小
  • ReviewWorkbench 复用<ImageStage readOnly /> 入参形状不变,审核页无需改动即获 Konva 性能收益

任务列表 — keyset 分页 + 虚拟化

  • 修复 5k 任务截断useTaskList 改为 useInfiniteQuery + 后端已有 _encode_task_cursor / next_cursor,前 50 之后的任务现在可见
  • @tanstack/react-virtual 虚拟化TaskQueuePanel 固定高度虚拟列表(estimateSize=84px),滚到末尾前 10 条自动 fetchNextPage
  • navigateTask 跨页预取:切到倒数第 10 条时触发 fetchNextPageReviewPage 同一 hook,零改动

预取 + 体感优化

  • 相邻任务预取:切题时对前后各一条 task 的 annotationspredictionsqueryClient.prefetchQuery,并插入 new Image().src 预热图像字节;连续翻题第 2 题起无白屏
  • 服务端置信度过滤GET /tasks/{id}/predictions?min_confidence=0.7 服务端裁剪载荷;前端 confThreshold 变更 300ms debounce 再发请求;减少大预测集的 JSON 体积
  • WS 连接灯:StatusBar 新增 6px 圆点状态指示(绿/橙/灰)+ 文案「实时同步 / 重连中 / 实时进度暂停」

缩略图基础设施

  • alembic 0009dataset_items 新增 thumbnail_path(VARCHAR 512) + blurhash(VARCHAR 64);顺带修复 v0.4.8 alembic 漂移(content_hash 列用 IF NOT EXISTS 补迁移)
  • alembic 0010tasks 新增 thumbnail_path(VARCHAR 512) + blurhash(VARCHAR 64),支持直传(非数据集)任务的缩略图
  • Celery media 队列:新建 workers/media.pygenerate_thumbnail 任务处理数据集条目缩略图;generate_task_thumbnail 任务处理直传任务(从 annotations bucket 拉图,写回 tasks 表);失败写 metadata_['thumbnail_error'],重试 3 次
  • 自动触发dataset 路径(upload-complete / upload-zip / scan-import)写库后各自 generate_thumbnail.delay(item_id)files.py upload-complete 写库后派发 generate_task_thumbnail.delay(task_id)
  • POST /datasets/{id}/backfill-media:触发 backfill_media Celery 任务,对存量无缩略图 dataset image 补生成
  • POST /files/projects/{project_id}/backfill-thumbnails:触发 backfill_tasks Celery 任务,对存量直传无缩略图任务补生成
  • _attach_dimensions / _attach_dimensions_batch 双路回落:有 dataset_item_id 时走 DatasetItem 取宽高 + 缩略图;dataset_item_id = NULL 时回落到 tasks.thumbnail_path / blurhash 字段,两条路径统一透出
  • 前端 Thumbnail 组件:blurhash canvas 占位 → <img loading=lazy decoding=async> 淡入替换;TaskQueuePanel 左侧 40×40 缩略图,DatasetsPage 文件列表 32×32 缩略图
  • TaskOut / DatasetItemOut schema:新增 thumbnail_url + blurhash 字段透出到前端

基础设施

  • Docker Compose 完整化:新增 api(alembic upgrade + uvicorn,Python healthcheck)、web(Nginx SPA)服务;celery-worker 升级为监听 default,ml,media 三队列
  • MINIO_PUBLIC_URL 配置config.py 新增 minio_public_url 字段;StorageService._public_url() 在生成 presigned URL 后将内部 endpoint 替换为外部可访问地址;docker-compose 注入 http://localhost:9000,浏览器可直接访问缩略图与原图

UI 改进

  • Button 圆角优化:sm 尺寸改为 --radius-pill(胶囊形),md 尺寸改为 --radius-lg;增加 primary/danger/ghost 各自的 box-shadow 与 border-color 差异化;加 transition: opacity 0.1s 悬停反馈

修复

  • 删除标注框闪烁useDeleteAnnotationonMutate 乐观更新,立即从 React Query 缓存中过滤掉目标 annotation,onError 回滚快照;消除 API round-trip 期间的「框消失 → 重现」闪烁
  • 移动标注框回弹useUpdateAnnotation 同理加 onMutate 乐观更新,mutation 发出前立即更新缓存中的 geometry;消除「框跳回原位再移到新位置」的视觉抖动
  • 默认缩放 125% 问题ImageStage 初次 fit 加 imageLoaded = !!image?.naturalWidth 守卫,确保在真实图像尺寸加载完成后才执行 fitNow();防止以 900×600 fallback 计算出错误的初始 scale

依赖

  • 前端新增:blurhash^2.0.5react-konvause-image(Konva 画布切换)、@tanstack/react-virtual(任务列表虚拟化)
  • 后端新增:blurhash-python>=1.2.0(pyproject.toml)

升级备注

  • alembic upgrade head:执行 0009(dataset_items thumbnail + content_hash 漂移)+ 0010(tasks thumbnail_path + blurhash)
  • Python 依赖uv syncblurhash-python
  • Celery:启动 media worker:celery -A app.workers.celery_app worker -Q media -c 4 --loglevel=info
  • 存量 dataset 数据:调 POST /datasets/{id}/backfill-media 补全历史 dataset_items 缩略图
  • 存量直传任务:调 POST /files/projects/{project_id}/backfill-thumbnails 补全直传任务缩略图
  • Docker 部署MINIO_PUBLIC_URL 设为浏览器可访问的 MinIO 地址(本地开发 http://localhost:9000
  • 版本号升至 0.5.0

Released under the MIT License.