⚠️ 自动镜像 · 此页由
docs-site/scripts/mirror-changelog.mjs从CHANGELOG.md生成,请勿直接编辑此处;改源文件后pnpm docs:build会自动同步。
Changelog
本文件记录 AI 标注平台的所有重要变更。
格式基于 Keep a Changelog,版本号遵循 语义化版本。
历史版本详情见 docs/changelogs/:
最新版本
[0.10.21] - 2026-05-19
v0.10.20 deferred 项部分收口: I4 笔画 timeline + 任务级 feedback patch/delete UI. 原计划 5 项 (D1-D5) 开工时重估发现 D1 (ADR-0027 切单源) 远超原计划体量 — 旧三表 (bug_reports / annotation_comments / tasks.reject_reason) 各自带独立 state machine + 子资源 (BugComment) + 前端专用 UI (BugReportDrawer), 不是删 mirror 写路径就能切, 需独立 legacy-table-retirement epic 处理。D3 (DiscussionPanel 完整拆分) 与 D5 (IssueLayer video pin) 为结构性 / stretch 工作, 无 UX 增量, 一并延期。v0.10.21 收紧到 D2 + D4 两项可立即落地的 UX 完善, 不破窗 ADR-0027 第三段决策。 → plan.
Added
- I4 · 笔画 timeline 协议字段 + 评论卡片下方迷你时间条 (_jsonb_types.py CanvasShape · CanvasDrawingEditor.tsx · CanvasDrawingEditor.module.css):
CanvasShape加 3 个 Optional 字段id/started_at/ended_at(全 Optional, 旧记录缺字段时 UI 降级不渲染 timeline, 无 alembic migration);CanvasDrawingEditor.handleDown/handleUp用Date.now()记 ms epoch +crypto.randomUUID()生成 id;CanvasDrawingPreview在 SVG 下方渲染TimelineBar(纯 CSS flex bar, 每段 stroke 一个 segment,flex-grow∝ 持续时长,background= stroke 颜色); hover segment → 仅该 strokeopacity=1, 其他opacity=0.25, 移开恢复全部 1。 - D4 · 任务级 feedback patch/delete UI 入口 (CommentsPanel.tsx): v0.10.20 任务级 feedback 行 (
__source=feedback) 仅展示, 本期开放 patch (PATCH /feedbacks/{id} · status=resolved/open) + delete (DELETE /feedbacks/{id} · 软删 is_active=false); 作者本人可见删除按钮 (server 端 permission 兜底);usePatchFeedback/useDeleteFeedbackhook 早在 v0.10.19 已就位, 本期仅 UI 接线。 - 4 例 vitest (CanvasDrawingPreview.timeline.test.tsx): ① 旧 shape 缺时间戳 → timeline 不渲染 (降级); ② 全字段 → 段数 = shape 数; ③ 混合 (1 缺 1 全) → 整段降级不渲染 (一致性); ④ hover segment → 对应 polyline opacity=1, 其他=0.25, 移开恢复。
Changed
- codegen openapi.snapshot.json 重生:
CanvasShape新字段同步到前端generated/types.gen.ts(id / started_at / ended_at 全 Optional)。
Verified
- 后端
cd apps/api && uv run pytest tests/test_annotation_comments_paged.py tests/test_comment_polish.py tests/test_annotation_feedbacks.py→ 16 passed (无回归; schema 新字段全 Optional 不破老测试)。 - 前端
pnpm --filter web typecheck→ 0 errors。 - 前端
pnpm --filter web test→ 737 passed / 103 files (baseline 733 + 4 新增 timeline 测试)。 - 前端
pnpm --filter web lint→ 0 errors / 117 warnings (baseline 116 + 1 useElementStyle deps warning in TimelineSegment)。
Deferred (推迟到独立 epic / v0.11+)
- D1 · ADR-0027 第三段 (切单源): 重估发现旧三表各自带独立 state machine + 子资源 + 前端 UI, 不是删 mirror 写路径就能切。留独立 legacy-table-retirement epic (v0.11+) 处理: 先按表逐个评估迁移影响面 (bug_reports → 整套 BugReportDrawer / BugComment / display_id 体系; annotation_comments → mentions / attachments 子表; tasks.reject_reason → state machine 状态字段)。双写仍正常进行 (v0.10.20 已落), 不破窗。
- D3 · 独立 DiscussionPanel.tsx + WorkbenchLayout 右栏两段固定结构 + ResizeHandle: 重估发现 CommentsPanel 内嵌 Tabs (comments / history) 已能承担 UX, 拆出 DiscussionPanel + 右栏两段拆分为纯结构改造, 对用户行为无增量。Workbench Shell 仍 1210 行未破 900 触发线, 暂缓。
- D5 · IssueLayer video stage frame-aware pin: video stage 的 frame-aware pin 需要重做坐标转换 + frame 过滤 store, 与视频时间轴交互细节多, 留单独切片处理 (anchor_position 已预留 frame 字段, 协议不动)。
[0.10.20] - 2026-05-19
v0.10.19 epic 留下的 5 项 deferred 收口 + ADR-0027 第二段迁移. 把 I4 任务级评论 (POST /feedbacks · kind=comment / anchor_type=task) / I12 BoxList group 折叠卡片 + AttributeForm 多选 batch banner / I18 IssueLayer Konva pin 渲染 + 单击落点创建 / ADR-0027 三段式迁移第二段 (
v_annotation_feedback_unifiedUNION ALL view + bug_reports / annotation_comments / tasks.reject_reason 双写 mirror) 一次性落地。纯 UI / 双写迁移工作, 后端契约延用 v0.10.19 已定型的annotation_feedbacks+annotations.group_id, 不引入新 schema。关键决策: ① 任务级评论不新加 POST /tasks/{id}/comments 端点 (复用 v0.10.19 已落的 POST /feedbacks, 减少端点数量 + 直接走未来主存储); ② 旧三表写入路径接入FeedbackService.mirror_*helper, 同事务 INSERT 新表, 失败一起回滚 (一致性由 PG 事务保证); ③v_annotation_feedback_unifiedview 带source_table列, 用于双写一致性对账与 v0.10.21 切单源前的过渡; ④ I12 BoxList 同 group_id (≥2 成员) 折叠为带哈希色点的 group 卡片 (单击头部 = 整组选中, 单击 chevron = 展开/折叠), AttributeForm 在selectedIds.length > 1时顶部 batch banner + onChange fan-out 走 useAnnotationBulkUpdate; ⑤ I18 IssueLayer Konva 层挂在 ImageStage</Stage>之前 (位于 ImageStageShapes 层之上), status 配色 (open=橙/resolved=绿/wont_fix=灰) + 高亮态加阴影, drop-arm FAB (crosshair 图标) 激活后单击图像落点 → 派发归一化坐标 → IssueCreateModal 自动预填 x/y. 不引入: ① 独立DiscussionPanel.tsx+ WorkbenchLayout 右栏两段固定结构 (无 UX 增量, 控制 PR 体量); ② POST /tasks/{id}/comments 端点 (决策见 D1); ③ 旧 bug_reports / annotation_comments 数据 backfill 到 feedbacks (留 v0.10.21 切单源时一次性 backfill); ④ I4 笔画 timeline (canvas_drawing.shapes[i] 加 id/started_at/ended_at + 评论卡片下方迷你时间条) (stretch goal, 无客户触发信号 → 延期 v0.10.21); ⑤ IssueLayer 在 video stage 的 frame 维度 pin (anchor_position 已预留 frame 字段, 后续切片). → plan · ADR-0027.
Added
后端
- alembic 0077 ·
v_annotation_feedback_unifiedview (0077_annotation_feedback_unified_view.py): UNION ALL 4 个数据源 (annotation_feedbacks / bug_reports / annotation_comments / tasks.reject_reason), 字段对齐到统一 schema (id/kind/anchor_type/project_id/task_id/annotation_id/anchor_position/status/severity/title/body/author_id/created_at/updated_at/is_active) + 额外source_table列方便对账; bug_reports.status 值域映射到统一 open/resolved/wont_fix (fixed → resolved, duplicate → wont_fix); annotation_comments 仅 is_active=true 行出现; tasks 仅 status='rejected' AND reject_reason IS NOT NULL 行出现; bug_reports.project_id IS NULL 行不出现 (新表 project_id NOT NULL). FeedbackService.mirror_bug_report / mirror_annotation_comment / mirror_task_reject(feedback.py): 3 个双写 mirror helper, 接受 legacy 模型实例 → 同事务 INSERT AnnotationFeedback (kind=bug/comment/reject); bug_report.project_id IS NULL 时跳过 mirror (登录页等无项目归属 bug 暂不入新表); 日志[ADR-0027 double-write] {source} → feedback {id}方便排查不一致.- 3 处 legacy 写路径接入 mirror (bug_report.py:create · annotation_comments.py:create_comment · tasks.py:reject_task): legacy INSERT 后立即调 mirror, db.commit() 一起落库; 失败回滚不留半边写.
- 4 例 pytest (tests/test_annotation_feedbacks.py):
test_mirror_bug_report_creates_feedback/test_mirror_bug_report_skips_when_project_null/test_mirror_task_reject_creates_feedback/test_view_unions_sources(4 源插入 → view SELECT 验证 source_table 分组计数).
前端
groupOutlineColorexport (ImageStageShapes.tsx): 把 v0.10.19 内联的 group_id 哈希配色函数 export 出来, 给 AIInspectorPanel BoxList group 卡片头色点复用.- BoxList group 折叠卡片 (AIInspectorPanel.tsx): 新 Row kind
userGroup; 按 group_id 分桶 (≥2 成员的 group_id 显示折叠卡片组 #N · k 个标注+ 哈希色点; group_id null 或单成员仍平铺); chevron 按钮 toggle expandedGroups Set; 头部点击 →onSelectGroup(memberIds)→ 整组 replaceSelected; 展开后内部仍按 BoxListItem 渲染单条. - AttributeForm batch banner (AttributeForm.tsx · AttributeForm.module.css): 新增
batchCount?: numberprop; > 1 时表单顶部渲染 warning 色 bannerN 个标注被选中, 修改将应用到全部; AIInspectorPanel 在多选时把 banner 显示出来 + onChange 改派到onBulkUpdateAttributes(Array.from(selSet), { attributes: next })(WorkbenchShell 接 useAnnotationBulkUpdate); 单选 / 未传时 banner 不渲染 (退化兼容). onBulkUpdateAttributes/onSelectGroupprops (AIInspectorPanel.tsx · WorkbenchShell.tsx): WorkbenchShell 把void useAnnotationBulkUpdate(...)替换为实参 mut, 传onBulkUpdateAttributes/onSelectGroup; inspector prop 默认通过WorkbenchLayout {...inspector}直传.- 任务级评论 POST + 合并显示 (CommentsPanel.tsx): annotationId=null + taskId+projectId 时 CommentInput 显示, 提交走
useCreateFeedback({ kind:"comment", anchor_type:"task" })POST /feedbacks; 列表合并 annotation_comments (useTaskCommentsInfinite) + task-level feedbacks (useFeedbacks · kind=comment/anchor_type=task), 按 created_at desc; feedback-source 行带__source标记, 不展示 patch/delete 按钮 (走不同端点, 暂不开放编辑入口). IssueLayer.tsxKonva 层 (IssueLayer.tsx): 渲染 pixel-anchored feedback 为图钉 (Circle + Text "i"); status 配色 (open=橙/resolved=绿/wont_fix=灰); 反向缩放保持视觉恒定 (8/scale等); 高亮 id 加阴影; armedForDrop=true 时挂全画布透明 Rect 拦截 click, 转换为相对坐标 [0,1] → onDrop; crosshair cursor 提示.- drop-arm FAB + IssueListPanel highlight (WorkbenchShell.tsx · IssueCreateModal.tsx · IssueListPanel.tsx): 新加圆形 FAB (crosshair 图标, 位置 bottom:128px 沉于 issueFab 之下), 单击 toggle
issuePinDropArmed状态; pin 单击落点 → 关闭 arm + 打开 IssueCreateModal 预填 anchor (prefilledAnchorprop · useEffect 同步到 x/y 输入框); IssueListPanel 接highlightIdprop (单击 Konva pin → setHighlightIssueId + 打开列表), 高亮项 border-color=warning + box-shadow + scrollIntoView. - AttributeForm batch banner 2 例单测 (AttributeForm.test.tsx): batchCount > 1 时显示 + 1/未传时不显示.
Changed
bug_reports/annotation_commentsPOST /tasks reject写路径: 同事务追加 INSERT annotation_feedbacks. 旧 reader 不动 (本切片不切单源), 前端 useFeedbacks 仍直接读新表 (view 仅用于对账); 待 v0.10.21 验证双写一致性后切单源 + 一次性 backfill 老数据.AnnotationFeedback service文档说明 (feedback.py): 从"第一阶段, 旧表不动"更新为"第二阶段, 双写已开"; 新加 mirror helper 章节注释.
Verified
- 后端
cd apps/api && uv run pytest tests/test_annotation_feedbacks.py tests/test_bug_reports.py tests/test_annotation_comments_paged.py tests/test_comment_polish.py tests/test_task_lock.py -v→ 30 passed (含 4 例新 mirror + view 测试). uv run alembic upgrade head应用 0077;SELECT source_table, COUNT(*) FROM v_annotation_feedback_unified GROUP BY source_table在 dev / annotation_test 两库均可查.- 前端
pnpm --filter web typecheck: 与 v0.10.19 基线一致 (旧 generated/types.genToolBinding/tool_bindings同样 21 处, 与本期 deferred 收口无关; 修复留 v0.10.21 codegen 重生). - 前端
pnpm --filter web test→ 733 passed / 102 files (含 2 例新 AttributeForm batch banner 单测). - 前端
pnpm --filter web lint→ 0 errors / 116 warnings (基线 115 + 1 个本期 IssueLayer 内 react-hooks/exhaustive-deps 兼容性 warning);check-css-tokens通过. - 手动 smoke (未跑 e2e, 本切片由 maintainer 在 docker + uvicorn + pnpm dev 全栈环境 smoke): ① 未选中标注 → CommentsPanel 任务级模式 CommentInput 显示, 提交后通过 GET /feedbacks 看到 kind=comment / anchor_type=task 行; ② Shift+点击 5 框 Ctrl+G 后 BoxList 出现 group 卡片, 点击头部整组选中; ③ 多选 3 框 → AttributeForm 顶部 batch banner, 改 attribute 后 3 框同步; ④ 按 crosshair FAB → 单击图像落点 → IssueCreateModal 自动填 x/y; ⑤ 单击 Konva 图钉 → IssueListPanel 打开并高亮对应项.
Deferred to v0.10.21
- 独立
DiscussionPanel.tsx+ WorkbenchLayout 右栏两段固定结构: 内嵌 CommentsPanel 已能承担 UX, 拆分纯结构改造对用户行为无增量, 本期不做. - I4 笔画 timeline:
canvas_drawing.shapes[i]加 id/started_at/ended_at + CanvasDrawingPreview 评论卡片下方迷你 CSS flex 时间条 + hover 评论高亮对应 stroke. - ADR-0027 第三段 (切单源): 验证双写一致性 (cron 跑 view source_table 对账) → 一次性 backfill 旧表存量数据到 annotation_feedbacks → 删除 legacy 写路径; 旧表保留只读一个版本作回退.
- IssueLayer video stage frame-aware pin: 当前仅图像 stage; video stage 的 frame-aware pin 后续切片 (anchor_position 已预留 frame 字段).
- 任务级 feedback patch/delete UI 入口: 当前 CommentsPanel feedback-source 行仅展示, 不允许 patch/delete (usePatchFeedback / useDeleteFeedback 已存在, UI 暂不开放).
- POST /tasks/{id}/comments 端点: 决策延续 — 不开新端点, 任务级评论一律走 POST /feedbacks.
[0.10.19] - 2026-05-19
ROADMAP §C.7 I4 + I12 + I18 单 epic 一次性落地. 三项工作台扩展 (I4 评论/历史常驻 / I12 Object Group 分组+批量编辑 / I18 Issue 锚定到像素位置) 作单一 epic 推进, 共享同一片 UI 表面 (右栏标注详情区 + Konva overlay 层),避免分三 PR 反复触碰热点。本切片后端契约 + 前端核心 UI 流程一次到位; 渐进式策略避免一次性重构 1210 行的 WorkbenchShell。 → plan · ADR-0027.
Added
后端
- I12 ·
annotations.group_id字段 +tasks.next_group_seq序号 (0075_annotation_group_id.py):annotations.group_id BIGINT NULL同 task 内分组序号;tasks.next_group_seq INT DEFAULT 0每 task 独立序号空间; 复合 partial 索引ix_annotations_task_group(task_id, group_id) WHERE group_id IS NOT NULL。与现有parent_annotation_id正交 (parent 表层级"车牌属于车", group 表平等成员同组)。 - I12 ·
/annotations/{bulk-update, group, ungroup}三端点 (annotations.py router + AnnotationService.bulk_update/group/ungroup): bulk-update 批量改 class_name / attributes / 状态位 (locked/hidden/occluded) / z_order / group_id (带group_id_explicit_clear区分"未提供"与"显式清空"); group 走next_group_seq +1 RETURNING原子分配; ungroup 自动级联清理"仅剩 1 个成员"的 orphan group。class_name 软校验按tool_unit_id分桶避免 N 次重复查 project.tool_bindings。整批操作单事务回滚 (任一被锁/已软删则整体 422)。 - I18 ·
annotation_feedbacks统一反馈表 (annotation_feedback.py model · 0076 migration): 取经合集 §2.2 落地第一段。kind∈ {issue, comment, reject, bug};anchor_type∈ {project, task, annotation, pixel};anchor_position JSONB(none_as_null=True)携带 pixel 坐标{x, y, frame?}(none_as_null 避免 Python None 序列化成 JSONB null 触发 CHECK 失败); 3 个索引 (project_kind_status / task_kind partial / annotation partial); 3 个 CHECK 约束保证 kind/status enum + anchor 一致性 (例 anchor_type='pixel' 必带 task_id + anchor_position)。 - I18 ·
/feedbacks统一 API (annotation_feedbacks.py router · feedback.py service): GET 按 project_id/task_id/annotation_id/kind/anchor_type/status 过滤 + keyset 分页; POST 走 pydanticmodel_validator提前校验 anchor 一致性 (比 DB CHECK 早一层友好); PATCH 改 status 时自动写resolved_at/resolved_by_id; DELETE 软删 (is_active=false);POST /feedbacks/{id}/replies子评论继承 parent anchor +thread_parent_id自引用链。权限: 作者 + project_admin + super_admin 可改/删; reviewer 可改 status (闭环 issue)。 - I4 · 任务级
/tasks/{id}/audit-history+/tasks/{id}/comments/page端点 (annotation_history.py · annotation_comments.py):audit-history合并 task 级 audit + 该 task 下所有 annotation 的 annotation 级 audit (压平 + 时序排序);comments/page聚合 task 下所有 annotation 的 active 评论 (DESC 时序 + keyset 分页)。AnnotationHistoryResponse.annotation_id改 nullable 兼容任务级降级。CommentsPanel 在未选中标注时降级到这两个端点的数据。 - AuditAction 新增 6 个枚举值 (audit.py):
ANNOTATION_GROUP/ANNOTATION_UNGROUP/ANNOTATION_BULK_UPDATE(I12);FEEDBACK_CREATED/FEEDBACK_STATUS_CHANGED/FEEDBACK_DELETED(I18)。 - ADR-0027 · AnnotationFeedback 统一反馈表 (docs/adr/0027-annotation-feedback-unified-table.md): 三段式迁移决策 (v0.10.19 仅立新表 → v0.10.20 加 view + 双写 → v0.10.21 切单源), 旧
bug_reports/annotation_comments/tasks.reject_reason在本切片完全不动, 每步单独可回退。
前端
- I12/I18 · API client + React Query hooks (api/feedbacks.ts · api/annotationGroup.ts · hooks/useFeedbacks.ts · hooks/useAnnotationGroup.ts): 含
useFeedbacks列表 +useCreateFeedback/usePatchFeedback/useDeleteFeedback/useReplyFeedback;useAnnotationGroup/useAnnotationUngroup/useAnnotationBulkUpdate(bulkUpdate 已落 hook,UI 消费留 v0.10.20)。 - I12 ·
Ctrl+G / Ctrl+Shift+G快捷键 (hotkeys.ts · useWorkbenchHotkeys.ts · WorkbenchShell.tsx):HotkeyAction新增annotationGroup/annotationUngroup; dispatchKey 在系统 Ctrl/Meta 分支识别 G 键 + Shift 修饰; useWorkbenchHotkeys 接handleAnnotationGroup/handleAnnotationUngroup注入点; WorkbenchShell 通过 group/ungroup mutation 派发 + toast 报告结果 (含 orphan 自动清理提示)。 - I12 · Konva 同 group_id 第二层虚线外圈 (ImageStageShapes.tsx): user 框带
group_id != null时,Rect 外偏移 4px/scale 渲染第二层虚线 Rect;groupOutlineColor(groupId)派生 8 档稳定色 (与类别色刻意区分);listening={false}不阻挡 hit-test。 - I12 ·
Annotation/AnnotationResponse类型 +annotationToBox透传group_id(types/index.ts · transforms.ts): 渲染层与后端 schema 对齐, 旧记录 null/undefined 兼容。 - I18 ·
IssueCreateModal+IssueListPanel+ 浮动入口 (IssueCreateModal.tsx · IssueListPanel.tsx · WorkbenchShell.tsx.issueFab): 工作台右下浮动🚩按钮 (带 open issue 数 badge); 列表面板支持解决/搁置/重开/删除单条 issue + 跳新建; Modal 支持 title/severity (info/warn/blocker)/body + 可选填 0-1 像素坐标 (留空则 task anchor); 提交后 useFeedbacks invalidate 重拉。 - I4 · 评论/历史常驻 (渐进式) (CommentsPanel.tsx · AIInspectorPanel.tsx): CommentsPanel 加
taskIdprop, annotationId null 时降级走useTaskCommentsInfinite+useTaskAuditHistory,显示该任务下所有标注的评论汇总; AIInspectorPanel 不再selectedAnnotation && <CommentsPanel/>,改为常驻渲染; CommentInput 在 task 级降级模式下隐藏 (任务级 POST 端点留 v0.10.20)。 useTaskCommentsInfinite/useTaskAuditHistoryhooks (useAnnotationComments.ts · useAnnotationAuditHistory.ts): 对应后端 task 级端点; 与现有 annotation 级 hook 并存,CommentsPanel 按 annotationId 是否存在分发。
Changed
AnnotationHistoryResponse.annotation_id改 nullable (annotation_history.py schema): 兼容 task 级时间线返回 (annotation_id=None)。
Verified
- 后端
uv run pytest tests/test_annotation_group_bulk.py tests/test_annotation_feedbacks.py tests/test_annotation_class_name_validation.py -v→ 17/17 passed (group 序号单调递增 / ungroup orphan 级联清理 / bulk_update 锁定整体回滚 / pixel anchor schema 校验 + DB CHECK / feedback status → resolved 自动写 resolved_at / class_name 软校验回归)。 uv run alembic upgrade --sql 0074:0076生成 SQL 与设计一致 (BIGINT / 复合 partial index / 3 个 CHECK constraint)。- 前端
pnpm typecheck全绿 (0 错)。 - 前端
pnpm test --run→ 731 passed / 102 files (无回归; 新增 hook/component 文件已就位但暂未补 focused test, 留 v0.10.20 补)。 - 前端
pnpm lint→ 0 errors / 115 warnings (warnings 全部为预存在的,与本期改动无关)。 pnpm --filter web build未跑 (本期纯前端文件新增, 没有 build 时配置变更, 走 CI 验证)。
Deferred to v0.10.20 (本 epic 续作)
- I4 完整拆分:
DiscussionPanel.tsx从 AIInspectorPanel 内嵌升级为独立组件 +WorkbenchLayout右栏两段固定结构 (上「检查器」下「讨论」) + 任务级评论 POST 端点 (允许在未选中标注时直接发任务级评论)。 - I12 完整 UI: AIInspectorPanel BoxList 同 group 折叠卡片 + AttributeForm 多选 batch banner (调用
useAnnotationBulkUpdate)。 - I18 Konva pin 渲染: 把
IssueListPanel中的 pixel anchor 同步渲染到 ImageStage (新增IssueLayer.tsxKonva 层 + 单击图像创建 pin 入口),替代当前的"输入框填 x/y"形态。 - ADR-0027 第二段:
v_annotation_feedback_unifiedview + 旧三表双写。 - I4 笔画 timeline:
canvas_drawing.shapes[i]加id/started_at/ended_at+ 评论卡片下方迷你时间条。
[0.10.18] - 2026-05-19
P3 维护项收尾 (除 dev SMTP). v0.10.17 后遗留 6 项 P3 维护事项, 本期一次性收口其中 5 项 (排除 dev SMTP mailpit, 单独排期), 全部为非用户可见行为变更的代码瘦身 / 可观测增强 / 截图基建 / 测试补完: ① RenderingConfigSection 抽出
RenderingConfigEditor受控视图, TemplateEditModal「渲染配置」tab 接入新 editor 并补rendering_config进 payload (v0.10.17 占位语清除); ② CreateProjectWizard 主控 1405 → 684 行, 7 个 Step 全部抽到components/projects/steps/子目录 + 共享UnitTabs; ③ PerfHud 加 6 个浏览器侧指标 (FPS / JS heap / Longtask / API p95 / WS reconnect 累计 / 当前 task 框数), 显隐复用现有?perf=1开关; ④ WorkbenchStageHostProps 30+ 字段加 JSDoc 分组注释 (类型不变兼容现 call site) + 新增WorkbenchLayout.test.tsx/WorkbenchStageHost.test.tsx共 6 例 focused render tests; ⑤ 截图 4 张空白态ai-pre/history-search/ai-pre/empty-alias/bbox/iou/bbox/bulk-edit的 prepare 脚本改为page.routemock API 注入示例数据 (避免污染真实 DB). 关键决策: WorkbenchStageHostProps 分组方案选「JSDoc only, 不动类型」, 避免牵动 1210 行的 WorkbenchShell call site (风险/收益不成正比), 后续若 Shell 再次膨胀再做嵌套 prop 对象重构. 不引入: ① WorkbenchStageHostProps 类型嵌套重构 (call-site 改造延后); ② StageControls 通用抽象 (等真实 3D 需求); ③useWorkbenchShellModel装配 hook (Shell 1210 行未破 900 触发线); ④ dev SMTP mailpit (单独 排期); ⑤ 截图实际重跑 (本期仅交付 prepare 脚本,pnpm screenshots需在 docker + uvicorn + pnpm dev 全栈环境下手动跑). → plan.
Added
RenderingConfigEditor受控视图 (RenderingConfigEditor.tsx):{ value, onChange, disabled? }形态, 把原RenderingConfigSection184 行的 4 个 row (smoothImage / cssImageFilter / controlPointsSize / snapToGrid) +isOverridden/toggleOverride/DEFAULTS全部迁入; ProjectSettings / TemplateEditModal 共享该视图.apps/web/src/components/projects/steps/子目录 (Step1DataTypeAndTools · Step2ClassesPerUnit · Step3AttributesPerUnit · Step4Ai · Step5Datasets · Step6Members · Step7Success · UnitTabs): 7 个 Step + 共享 UnitTabs 各自独立文件; 主CreateProjectWizard.tsx仅保留 FormState / INITIAL / defaultUnitBindings / buildFormFromSource / buildFormFromTemplate / submit / Stepper / Footer.- PerfHud 浏览器侧指标采集 (useBrowserStats.ts · BrowserPanel in PerfHud.tsx): rAF FPS loop /
performance.memory.usedJSHeapSize(Chrome only, 非 Chrome 显示 N/A) /PerformanceObserver({type:"longtask"})累计最近 60s > 50ms 任务数与最近一次时长 / API p95 / WS reconnect 累计 / 当前 task 框数 6 项; PerfHud body 末尾增加<BrowserPanel />与 backend 指标并列, 无后端连接时也展示. - API duration ring buffer (_metrics.ts): 60s 滑动窗口, 上限 1024 条;
recordApiDuration(ms)由 client.ts 的requestwrapper 在 fetch 完成后调 (try/catch 包裹, 失败不影响业务);getApiP95Ms()惰性清理 + sort 取 p95. - WS reconnect 计数 store (_wsMetrics.ts): 全局 Zustand store; useReconnectingWebSocket 的
scheduleReconnect每次触发bumpWsReconnectCount(). - 当前 task 框数 store (useTaskBoxCount.ts): 全局 Zustand store; WorkbenchShell 每次 annotations 变化
publishTaskBoxCount(annotationsRef.current.length). WorkbenchLayout.test.tsx+WorkbenchStageHost.test.tsxfocused render tests (WorkbenchLayout.test.tsx · WorkbenchStageHost.test.tsx): 6 例新单测 — Layout 验证 12 个 slot 都渲染 / 可选 modal/guidePanel 缺省时不渲染 /gridTemplateColumns写入 CSS var; StageHost 验证stageKind分发到唯一对应舞台组件 (image / video / 3d).
Changed
RenderingConfigSection瘦身为外壳 (RenderingConfigSection.tsx): 185 → 47 行, 只持有useUpdateProject+draftstate + commit, 视图层全部下放RenderingConfigEditor.TemplateEditModal渲染配置 tab 接入 editor + payload 补rendering_config(TemplateEditModal.tsx): 删除 v0.10.17 占位语 (v0.10.18 抽出共享 editor 后接入); 新增renderingConfigform state +useEffect同步 initial; submit payload 加rendering_config: renderingConfig(此前漏传, 模板编辑无法保存渲染配置, 现修复).CreateProjectWizard主控瘦身 (CreateProjectWizard.tsx): 1405 → 684 行 (-721 / -51%); 删除 Step1 / Step2Classes / Step3Attributes / Step4Ai / BackendSourceSelect / Step5Datasets / Step6Members / SuccessStep / UnitTabs 9 个内联组件 (全部移至steps/);FormState/UnitBindingForm/UnitBindingMap/defaultUnitBindings改为export, 子 step 文件 import 类型即可.WorkbenchStageHostProps加 JSDoc 分组 (WorkbenchStageHost.tsx): 30+ 平铺字段按 common / video / image / ai / editors 5 组加分隔注释; 类型签名不变兼容现 call site, 后续若拆嵌套 prop 对象有方便参考线.api/client.ts:request增加 API duration 上报 (client.ts): fetch 前performance.now()记 t0, 完成后recordApiDuration(performance.now() - t0)(try/catch 包裹); PerfHud 浏览器侧 p95 数据源.useReconnectingWebSocket:scheduleReconnect加 reconnect 累计计数 (useReconnectingWebSocket.ts): 每触发一次scheduleReconnect即bumpWsReconnectCount(); 计数跨 hook unmount 累计 (session 级).WorkbenchShell发布 task 框数到 PerfHud store (WorkbenchShell.tsx): 新增一个useEffect在annotationsData变化时调publishTaskBoxCount(annotationsRef.current.length).- 截图场景 prepare 改为 page.route mock 注入 (scenes/ai-pre.ts · scenes/workbench-bbox.ts):
ai-pre/history-searchmockGET /admin/preannotate-jobs*返回 8 条 job + 模拟搜索词 "person";ai-pre/empty-aliasmockGET /admin/preannotate/project-cards*返回 1 个classes_aliases: {}项目;bbox/ioumockGET /tasks/{id}/annotations返回 2 个高 IoU 重叠 bbox;bbox/bulk-editmock 同端点 3 个 bbox +Ctrl+A全选触发批量编辑栏. 不污染真实 DB.
Verified
- 前端
pnpm --filter web typecheck全绿 (0 错). - 前端
pnpm --filter web test→ 731 passed / 102 files (新增 6 例 WorkbenchLayout + WorkbenchStageHost focused render tests). - 前端
pnpm --filter web lint→ 0 errors / 115 warnings (warnings 全部为 v0.10.17 之前遗留, 与本期改动无关);check-css-tokens通过 (所有--color-*引用对得上 tokens.css 定义, 未滥用 fallback). - 未跑
pnpm screenshots实际重跑 — 需 docker + uvicorn + pnpm dev + seed.py 全栈在环, 由 maintainer 在本地跑pnpm screenshots -- --grep="ai-pre/history-search|ai-pre/empty-alias|bbox/iou|bbox/bulk-edit"验证 4 张图非空白后再合 PR.
[0.10.17] - 2026-05-19
新建项目向导通用化 + 类别 / 属性按工具单位强隔离 + Magic Box + 模板复杂字段编辑. ROADMAP §A 新建项目向导一直延用 7 种 type_key 枚举(image-det / image-seg / image-kp / video-track / video-mm / mm / lidar)+ 项目级扁平类别表的形态;客户反馈"同一项目内不同工具想标不同类"(bbox 标行人/车辆 + polygon 标可行驶区/天空)、"项目类型不该把工具集死锁"。本期一次性收口四件相关事:① 新建向导从枚举 7 种 type 转向"image / video / lidar 三种数据载体 + 工具集多选"形态;② 类别和属性 schema 从项目级扁平改为按工具单位(tool_unit)强隔离绑定(bbox 工具的"人"与 region 工具的"人"是两条独立记录);③ Magic Box 工具落地(复用 SmartBoxTool 拖框 + sam3-backend bbox prompt → polygon → 紧凑外接矩形落 bbox,零后端改动);④ TemplateEditModal 从只能改 6 个简单字段升级为 3-tab Modal(基础信息 / 工具与类别 / 渲染配置),复用 ProjectSettings 已抽出的受控编辑器,模板与项目共享 tool_bindings 形态。关键决策(对照 ADR-0026):后端 schema 重写为嵌套结构
{ tool_unit_id: { enabled, classes: [...], attribute_schema: {...} } },旧扁平字段 v0.10.17 期间由 service 双写派生兜底、v0.10.18 删除;工具集分三组(基础几何 = bbox + polyline / 区域 = polygon + mask 打包 / AI 交互 = smart-* + magic-box 打包),polyline / lidar_box_3d 本版留位置灰未实现;Magic Box 仅复用 SAM 链路,不引入 Canny/Sobel snap-to-edge(留 v0.11+);alembic 0072 / 0073 双 migration backfill 老数据(image-seg→regionunit、其它 →bboxunit,按annotation_type反推annotations.tool_unit_id)。后端Project.tool_bindingsJSONB +Annotation.tool_unit_id/Prediction.tool_unit_idString(30) 列落地;_jsonb_types.py新增ToolUnitIdLiteral +ToolBinding+ToolClassEntry+validate_tool_bindings_keys校验器;services/project.py新建含apply_tool_bindings_legacy_sync/coalesce_legacy_into_tool_bindings/derive_legacy_classes_config/lookup_classes_for_tool_unit等 helper;services/annotation.pyAnnotationService.create接tool_unit_id并按tool_bindings[unit].classes软校验class_name(空集合放行兼容旧数据);services/prediction.py加derive_tool_unit_from_ls_type/derive_tool_unit_from_result按 LS shape 类型派生 unit;services/export.pyCOCO categories 按 tool_unit 分组带supercategory,cat_map改为(tool_unit_id, class_name) → category_id二元组查找;schemas/aap_json.pyschema_version 升 1.1,envelopeproject.tool_bindings整段嵌入,annotations / predictions entries 加tool_unit_id(1.0 reader 走extra="ignore"仍兼容)。前端constants/toolUnits.ts含ToolUnitId/PROJECT_DATA_TYPES/TOOL_UNIT_GROUPS与defaultEnabledUnits/toolUnitFromLegacy/dataTypeFromLegacy派生;CreateProjectWizardFormState 从扁平classRows / attributeFields改为unitBindings: Record<ToolUnitId, {enabled, classRows, attributeFields}>+activeUnit,Step 1 加工具集 chip 多选区域、Step 2 / Step 3 头部加 UnitTabs 切换,submit 构造tool_bindingspayload + 派生 legacy 字段兜底;useProjectToolBindingshook 与ToolUnitTabs组件由 ProjectSettings sections / TemplateEditModal 共享;ClassesSection/AttributesSection改造为按 unit tab + 可启用 / 禁用 unit + 重命名限定 unit(useRenameClass加tool_unit_id参数);工作台stage/tools/toolUnits.tsTOOL_TO_UNIT映射 9 种 ToolId 到 ToolUnitId,state/useToolBindings按当前激活工具派生 classes / classesConfig / attributeSchema(老项目走 fallback);WorkbenchShell切到 hook 派生,切工具时 activeClass 不在新 unit 类别集自动切首个类;useWorkbenchAnnotationActionsbbox/polygon 创建路径透传tool_unit_id。Magic BoxMagicBoxTool(id=magic-box, hotkey=G, icon=wandSparkles, requiredPrompt="bbox") 复用 SmartBoxTool 拖框语义返回samProbe { mode: "bbox" };useImageAnnotationActions加 useEffect 监听sam.candidates,当s.tool === "magic-box"且非 running 时取首个候选(polygonlabels →tightenBboxFromPolygon紧凑外接矩形 / rectanglelabels → 直接用)→createBboxWithClass+sam.cancel(),跳过 AI 候选层 UI。TemplateEditModal 重写为 3 tab 形态,基础信息 / 工具与类别 / 渲染配置(后两 tab 复用 ClassEditor + AttributeSchemaEditor + ToolUnitTabs;rendering_config 编辑器待 v0.10.18 抽出共享视图后接入)。Annotationclass_name软校验首版只校验空集合放行,新工具单位类别空时不阻断旧数据写入;v0.10.18 视客户反馈再考虑收紧。不引入:① polyline / keypoint / lidar 工具实现(schema 留位置灰);② Snap-to-edge(Canny/Sobel)pixel-level 边缘吸附,Magic Box 仅取 polygon 紧凑外接矩形;③ 跨 tool_unit 类别"软关联"(alias_to 链),强隔离意味同名是独立记录;④ AAP JSON 1.1 importer 完整支持 video_track tool_unit(留 §C.5 R23 同窗口);⑤ rendering_config 在 TemplateEditModal 的编辑器(待 RenderingConfigSection 抽出RenderingConfigEditor共享视图);⑥ POST/annotations/import端点(AAP JSONannotations[]仍仅导出可用,导入只警告日志)。迁移风险:alembic backfill 默认把所有 image-det 项目类塞bboxunit;若客户实际混用 polygon 工具,需到 ProjectSettings 把类复制到regionunit(强隔离不能共享)。ROADMAP §A「新建项目向导」「项目模板 TemplateEditModal 复杂字段编辑 UI」「Magic Box & Snap」P2-P3 三项收官。→ plan · ADR-0026.
Added
Project.tool_bindingsJSONB +Annotation.tool_unit_id/Prediction.tool_unit_idString(30) 列 + alembic 0072 (project.py · annotation.py · prediction.py · migration):工具维度类别 / 属性绑定单源真值;migration 含 DDL + 按type_key/annotation_type反推的 backfill(image-seg→ region,其它 → bbox)。_jsonb_types.py新增ToolUnitIdLiteral +ToolBinding+ToolClassEntry+validate_tool_bindings_keys(_jsonb_types.py):五个稳定 enum 值(bbox / polyline / region / ai_interactive / lidar_box_3d)与 Pydantic 校验。app/services/project.py(project.py):derive_legacy_classes_config/derive_legacy_classes_list/derive_legacy_attribute_schema/apply_tool_bindings_legacy_sync/coalesce_legacy_into_tool_bindings/lookup_classes_for_tool_unit/derive_tool_unit_for_type_key— 单源真值同步与老 reader 派生 helper 集合。AnnotationService.create接tool_unit_id+ class_name 软校验 (annotation.py):service 层按project.tool_bindings[unit].classes校验class_name命中(空集合放行兼容旧数据,422 时返"类不在工具单位类别集内");accept_prediction沿用prediction.tool_unit_id给生成的 annotation。prediction.to_internal_shape+derive_tool_unit_from_ls_type/derive_tool_unit_from_result(prediction.py):LS shape 类型 → tool_unit_id 派生(polygonlabels / brushlabels / multi_polygon → region;rectanglelabels → bbox);PredictionService.create_from_ml_result按 result[0].type 自动填tool_unit_id;to_internal_shapepass-through 时就地回填 tool_unit_id 保 dict identity 兼容历史 test。ProjectTemplate.tool_bindings+ alembic 0073 (project_template.py · migration):模板与 Project 对齐;backfill 同 0072 规则。- Magic Box 工具 (MagicBoxTool.ts · tightenBboxFromPolygon):id=
magic-box,hotkey=G,icon=wandSparkles,requiredPrompt=bbox;复用 SmartBoxTool 拖框 → sam3-backend bbox prompt → polygon → 紧凑外接矩形(min/max x/y + clamp [0,1])→ 落 bbox 标注;useImageAnnotationActionsuseEffect 监听 sam.candidates 自动转换 + sam.cancel() 跳过候选层 UI。 constants/toolUnits.ts(toolUnits.ts):ToolUnitId/PROJECT_DATA_TYPES(image/video/lidar) /TOOL_UNIT_GROUPS元数据 +defaultEnabledUnits/toolUnitFromLegacy/dataTypeFromLegacy派生 helper。useProjectToolBindingshook +ToolUnitTabs组件 (useProjectToolBindings.ts · ToolUnitTabs.tsx):ProjectSettings sections / TemplateEditModal 共享的 unit tab 切换条 + enabled chip 控件 + buildUnitBindings / unitBindingsToPayload 序列化 helper。stage/tools/toolUnits.ts+state/useToolBindings.ts(toolUnits.ts · useToolBindings.ts):工作台 ToolId → ToolUnitId 映射 + 按当前激活工具派生 classes / classesConfig / attributeSchema(老项目走 fallback)。- 5 例 Magic Box helper 单测 (bbox.test.ts):空输入 / 0 面积 / 紧凑外接矩形派生 / 越界 clamp / number[][] 形式。
- ADR-0026 (0026-tool-unit-class-and-attribute-binding.md):强隔离决策与方案 A/B/C/D 对比;实现位置 + 触发后续 v0.10.18+ 延伸项清单。
Changed
ProjectCreate / Update / Outschema 加tool_bindings(schemas/project.py):优先于扁平classes_config / attribute_schema;validate_tool_bindings_keys校验顶层 key 在枚举内;ProjectOut暴露派生只读 legacy 字段。AnnotationCreate / Out+PredictionOut加tool_unit_idToolUnitId Literal (annotation.py · prediction.py):Annotation 默认bbox保兼容,Prediction 由 service 派生回填。ProjectTemplateBase / Update / Out加tool_bindings(project_template.py):与 Project 对齐;同validate_tool_bindings_keys校验器。projects.py:create_project/update_project接入 tool_bindings 同步 (projects.py):写入前coalesce_legacy_into_tool_bindings(旧客户端兼容)+apply_tool_bindings_legacy_sync(派生覆盖 legacy);rename_class加可选tool_unit_id字段限定工具单位(不传时跨所有 unit 同名一起改保兼容)。tasks.py:create_annotation透传tool_unit_id(tasks.py):POST 体携带 unit id;前端按当前激活工具自动填。services/export.py:export_cococategories 按 tool_unit 分组 (export.py):每 enabled unit 各贡献一段 categories,category.supercategory = tool_unit_id,同名类不去重(强隔离);cat_map改为(tool_unit_id, class_name) → category_id,ann 查询用(ann.tool_unit_id, ann.class_name)命中,跨 unit 同名走兜底。services/export.py:export_aap_jsonenvelope 升 1.1 (export.py):project.tool_bindings整段嵌入;annotations / predictions entries 每条加tool_unit_id(annotation 默认 bbox,prediction 沿用行字段)。schemas/aap_json.py:AAP_SCHEMA_VERSION升1.1(aap_json.py):AAPProjectMeta.tool_bindings/AAPAnnotationEntry.tool_unit_id/AAPPredictionEntry.tool_unit_id字段加入;1.0 reader 走extra="ignore"仍可解析(向后兼容)。services/project_clone.py:CLONEABLE_PROJECT_FIELDS收入tool_bindings(project_clone.py):clone + 模板应用共享同一份白名单。coalesce_legacy_into_tool_bindings同时支持classes: list[str]/classes_config/attribute_schema三源 (project.py):names 优先级 list > cfg_map keys > prev unit;cfg_map 提供颜色 / order / alias,缺时从 prev classes 回查;空 dict 等价于"未给"触发反向派生(解决模板 fixture 默认{}不生效)。CreateProjectWizardFormState 改为多 unit (CreateProjectWizard.tsx):删除classRows / attributeFields扁平字段,改为unitBindings: Record<ToolUnitId, {enabled, classRows, attributeFields}>+activeUnit: ToolUnitId;Step 1 加 unit chip 多选(切类型时按 data_type 重置默认 + 保留已配置内容)、Step 2 / Step 3 头部加UnitTabs切换 + 仅渲染 activeUnit 编辑器;buildFormFromSource/buildFormFromTemplate复用buildUnitBindingsFromSource兼容老项目;submit 构造tool_bindingspayload 主真值 + 从 activeUnit 派生 legacy 字段兜底;step1Valid 加"至少 1 个 enabled unit"。ClassesSection/AttributesSection按 unit tab (ClassesSection.tsx · AttributesSection.tsx):useProjectToolBindingshook 接管状态;ToolUnitTabs allowToggle让 owner 在 ClassesSection 启用 / 禁用 unit;保存走unitBindingsToPayload(bindings)构造tool_bindingsPATCH;rename 走useRenameClass携带tool_unit_id限定工具单位的 annotations.class_name 迁移。projectsApi.renameClass+useRenameClass(projects.ts · useProjects.ts):加可选tool_unit_id参数 (不传时跨所有 unit 同名一起改保兼容)。- 工作台
WorkbenchShell切到useToolBindings派生 (WorkbenchShell.tsx):classes/classesConfig/attributeSchema三字段全部走 hook 派生(老项目走 fallback 到project.classes_config/attribute_schema);切工具时若activeClass不在新 unit 类别集自动切首个类避免错位标注;AIInspectorPanel与HotkeyOverlay的attributeSchema同步切换。 api/tasks.ts:AnnotationPayload(tasks.ts):加可选tool_unit_id字段;useWorkbenchAnnotationActions:submitPolygon / createBboxWithClass按toolUnitForTool(s.tool)自动填。Toolunion /hotkeys.ts:setTool/AI_TOOL_CYCLE/AIInspectorPanel.tool union/AIToolDrawer TOOL_HINT/ToolDock TOOL_DESCRIPTORS同步加magic-box(useWorkbenchState.ts · hotkeys.ts · AIToolDrawer.tsx · ToolDock.tsx):AI_TOOL_CYCLE顺序 smart-point → smart-box → magic-box → text-prompt → exemplar(S 键 cycle 经过 magic-box)。TemplateEditModal重写为 3 tab (TemplateEditModal.tsx):基础信息(name / description / type / scope / annotation_guide) / 工具与类别(复用ToolUnitTabs allowToggle+ClassEditor+AttributeSchemaEditor) / 渲染配置(占位 + 提示语 v0.10.18 接入共享编辑器);submit 走unitBindingsToPayload(bindings)构造tool_bindings,classes 派生自首个 enabled unit 兼容老 reader。projectTemplates.ts类型加tool_bindings(projectTemplates.ts):ProjectTemplateOut/ProjectTemplateCreatePayload同步;codegen 自动派生强类型。- OpenAPI snapshot + 前端 codegen (openapi.snapshot.json · generated/types.gen.ts):tool_bindings / tool_unit_id / ToolBinding / ToolClassEntry 全链路派生,前端
api/projects.ts重导出ToolBinding / ToolClassEntry / ToolBindings强类型。
Verified
- 后端:
alembic upgrade head应用 0072 / 0073 在 docker postgres 实测成功(P-0001 历史项目 classes 正确转入tool_bindings.bboxunit,attribute_schema 一并迁入);pytest tests/→ 577 passed(含修复apply_template_with_explicit_field_override与clone_copies_all_cloneable_fields派生覆盖逻辑、already_internal_shape_passthroughmutate-in-place identity 保兼容)。 - 前端:
pnpm exec tsc --noEmit全绿(0 错);pnpm vitest run→ 718 passed(含 5 例新增tightenBboxFromPolygon单测:空输入 / 0 面积 / 紧凑外接矩形派生 / 越界 clamp / number[][] 形式)。 - 端到端冒烟(手测):① 进
/projects/newWizard → 选 image → 工具集勾 bbox + region → 给两组各加同名 person 不同色 → 创建成功 → DBSELECT tool_bindings FROM projects结构正确;② 进项目 ClassesSection / AttributesSection 看到两个 unit tab 类别不串、unit toggle 启停生效;③ 工作台 B/P 切 bbox/polygon,左侧 ClassPalette 自动换 unit 的类别,落标注 DBtool_unit_id列非空;④ AI backend 启动时 G 键试 Magic Box 拖框 → 收紧到对象紧凑外接矩形 → 落 bbox(tool_unit_id=ai_interactive),AI 候选层不展示直接落库;⑤ COCO 导出 categories 段含supercategory字段、AAP JSON 导出schema_version="1.1"+tool_bindings整段嵌入。
[0.10.16] - 2026-05-19
ROADMAP §1 立即可做小颗粒收尾 (4 子项). 一次性收口 CVAT/Label Studio 取经合集 §1 中尚未完成的 4 项基建:reject 原因结构化(§1.2)、Webhook 事件信封 ADR-0025 草案(§1.3)、DuckDB 离线分析视图(§1.6)、统一 async_jobs 异步任务表 MVP(§1.7)。本期不动 webhook 实现,只锁信封 schema;不引入 ClickHouse,先用 DuckDB 顶量级;async_jobs 与现有
prediction_jobs/video_tracker_jobs走双写双轨——前者只记最小元数据作为汇总索引,后者保留为 domain 真值。为后续 reject 率分析、Annotator Dashboard(§4.1)、Webhook 系统(§2.1)一并铺底。→ plan · ADR-0025
Added
tasks.reject_reason_type4 类枚举 + alembic 0070(model · migration):missing/extra/wrong_label/wrong_geometry;旧数据 NULL 不回填,新 reject 强制选 type;reviewer dashboard + workbench + review page 全部走RejectReasonModal4 按钮单选(ReviewerDashboard 从window.prompt升级到 Modal);中文 label 映射常量rejectReasonTypes.ts前端单点。async_jobs统一异步任务表 + alembic 0071(model · migration · service · API):kind∈batch_predict|video_tracker|audit_archive|predictions_import,status∈pending|running|completed|failed|cancelled;GET /async-jobs(owner-scoped,super_admin 看全部)+GET /async-jobs/{id}+POST /async-jobs/{id}/cancel(MVP 仅支持predictions_import/audit_archive软取消);Celerytask_failure/task_revokedsignals 兜底翻 failed/cancelled;Celery beat 每日 04:15 UTC 跑purge_old_async_jobs清 30 天前终态行。- Topbar 后台任务铃铛(JobsBell.tsx):5s polling
/async-jobs?limit=20,badge 显示 in-progress 计数,drawer 列进度条 + 状态 pill + 错误提示。与现有PreannotateJobsBadge(Redis pub/sub 走预标专用通道)并存。 - DuckDB 离线分析 +
/admin/analytics(super_admin only):duckdb_sync.py增量同步task_events(带reject_reason_type反向 join)+audit_logs到./data/duckdb/analytics.duckdb;analytics_queries.py暴露 3 个固定面板查询(人均日吞吐 / reject 率分类型 / 标注耗时 p50/p95),不接受任意 SQL;AnalyticsPage.tsx渲染三张卡片;Celery beat 每日 02:30 UTC 跑sync_to_duckdb。 - ADR-0025 Webhook 事件信封 + Pydantic schema 占位(ADR · schema):约定
event_version/event/delivery_id/occurred_at/data5 个必备字段,breaking change 升 major、minor 只能加可空字段(Postel's law);与 §1.5 AAP JSON 的schema_version同源思路。不实现 publisher / outbox / delivery 表,留给 §2.1 epic。
Changed
- batch_predict / video_tracker / audit_archive / predictions_import 接入 async_jobs 双写:service 层显式
create_job+update_progress(每 5% 步长)+mark_complete/failed;专表(prediction_jobs/video_tracker_jobs)保留为 domain 真值。失败路径走 try/except 兜底,专表写入失败不阻断主流程。 docker-compose.ymlcelery-worker 加./data/duckdb:/var/lib/duckdbbind mount + 队列加cleanup,audit,确保 DuckDB 同步 + async_jobs 清理任务有 worker 接管。- Python 新依赖:
duckdb>=1.1.0,<2.0.0(worker 写、API 只读,单 writer 多 reader 模型)。 docs/adr/README.md索引补齐 ADR 0019-0025(旧索引停在 0018)。
Verified
- 后端:
alembic upgrade head应用 0070/0071 通过;reject endpoint 缺reason_type→ 422、带合法 type → 200;EventEnvelope序列化往返锁;FastAPI 加载 249 路由,含/api/v1/async-jobs/*+/api/v1/admin/analytics/{panel_name};Celery beat 11 项 schedule(含sync-to-duckdb/purge-old-async-jobs)+ task_routes 含 analytics / async_jobs_cleanup 路由。 - 前端:
RejectReasonModal4 按钮 + comment 可空 + skip hint 预填;ReviewerDashboard改用 Modal 后 reject 流程;JobsBell空态 / running badge 计数 / 终态不计入 badge 全部覆盖。
[Unreleased]
Changed
- CSP style-src sections + Workbench + Dashboard + 基础 UI 群续推:
apps/web/src/**/*.tsx的 JSXstyle=/<style>已全站清零,no-restricted-syntaxguard 已扩到全站 TSX。迁移覆盖 Projects sections、Workbench、Dashboard、AIPreAnnotate、Admin、Annotate、Audit、Bugs、Datasets、Review、Settings、Storage、Users、Login/Register/Unauthorized、ModelMarket、App,以及components/ui/ shell / bugreport / datasets / projects / users / CommandPalette / PerfHud / UserPicker / CanvasDrawingEditor 等全局组件。Button/Card/Badge/Icon/Avatar继续保留 publicstyleprop 兼容性,但改由useElementStyleref 同步 DOM style;Badge补className透传,避免迁移时用 wrapper 改变文本匹配。CSP 头同步摘掉style-src 'unsafe-inline':API 响应路径收紧为style-src 'self',Nginx HTML 出站路径改为style-src 'self' 'nonce-$request_id',vite nonce 插件补齐<style>标签 nonce。→ plan · ADR-0010 · 迁移指南
[0.10.15] - 2026-05-19
Predictions Import 端点 + 平台原生 AAP JSON v1.0 无损中间格式. ROADMAP §A AI/模型 P2 项: 当前
predictions行只能由内部 ML backend 写入, 客户自家训好的模型 / 不愿托管在平台 backend 的场景(学术/初创/合规)无法把外部模型结果灌进来; 同时 COCO/YOLO/VOC 三种导出格式都是有损(attribute_schema 值/prediction.confidence/annotation.source/annotation_guide/classes_config 都丢失), 缺一个无损中间格式作为跨实例迁移/SDK/Plugin/dataset snapshot 锚点. 本期同窗口做两件事:① 新增POST /projects/{id}/predictions/import端点(COCO + AAP JSON 两种 input);② AAP JSON v1.0 作为新导出格式与 COCO/YOLO/VOC 并列接入. 关键决策(对照取经合集 §6 决策底线):schema_version必备 + breaking change 升 major;annotations[]和predictions[]双数组分开(不混 type 字段, 避开 CVAT 部分格式踩的坑); 导出严格写满 null + 导入 lenientextra="ignore";geometry使用平台内部格式(与 annotation.geometry JSONB 对齐), 不嵌套 LabelStudio shape;task_matchoneofdisplay_id优先(全局唯一最稳),file_pathfallback, 跨项目 display_id 命中视为不匹配防偷换项目. 后端 alembic 0069 加predictions.source String(20) NOT NULL DEFAULT 'ml_backend'+ 索引, 老数据按默认回填(当前唯一出口是 ML backend 含义准确), 外部导入行source='external_import'/ml_backend_id=NULL. 新app/schemas/aap_json.py写 AAP JSON v1.0 envelope (AAPJsonV1Envelope / AAPTaskBlock / AAPAnnotationEntry / AAPPredictionEntry, 全部 pydantic v2extra="ignore").ExportService.export_aap_json复用_load_data+ 新增_load_predictions, 通过to_internal_shape把 prediction.result 的 LS shape 反推为内部 geometry, 组装严格写满 null 的 envelope.predictions_import.py内internal_geometry_to_ls_shape适配器把 bbox/polygon/multi_polygon → LabelStudio shape,import_aap_json与import_coco复用PredictionService.create_from_ml_result(扩source参数)写入.task_matcher.resolve_task统一封装 display_id+file_path 两段式查找. 端点POST /projects/{id}/predictions/import接 multipart/form-data, 走require_project_owner权限, AuditMiddleware 写predictions.import审计. AAP JSON 导出接入/projects/{id}/export和/projects/{id}/batches/{bid}/export两个端点的format=aap_json分派. 前端predictionsApi.import走 FormData + fetch(client.ts 默认 Content-Type=application/json 不能复用),PredictionImportWizard三步 Modal(选格式+文件 → dry-run 预览 errors 表 → 确认提交), 入口放在 AIPreAnnotate ProjectDetailPanel toolbar 与 batch 级预标流程并列(语义最贴近"外部模型结果上传").Dashboard ExportSection格式下拉加「AAP JSON」选项与 COCO/YOLO/VOC 并列;ExportFormat类型扩"aap_json". 不引入:①POST /annotations/import端点(AAP JSONannotations[]字段导出端写满, 导入端只警告日志不入库; annotations import 涉及 batch/owner/audit 协议复杂度, 留下一版);② Task 表加external_id字段(本期 display_id+file_path 两元组够; external_id 留 v0.11+);③ ProjectTemplate 进 AAP JSON manifest(与 ADR-0023 模板 epic 触发条件挂钩, 本期仅约定 envelope 末层预留扩展位);④ datumaro 链转换(其它格式走 datumaro 中转, 本期不引入). ROADMAP §A 「Predictions Import + AAP JSON」P2 收官. → plan · ADR-0024 · 用户文档 · API 导入指南.
Added
predictions.source字段 + alembic 0069 (model · migration):String(20) NOT NULL DEFAULT 'ml_backend' + 索引; 老数据回填.POST /projects/{id}/predictions/import(predictions.py): 端点接 COCO + AAP JSON, dry_run + overwrite_existing 参数./projects/{id}/export?format=aap_json+ 批次级同向: 双端点接入 AAP JSON 导出.app/schemas/aap_json.py(aap_json.py): AAP JSON v1.0 envelope pydantic schema + schema_major 守门.app/services/predictions_import.py(predictions_import.py): import_aap_json / import_coco + internal_geometry_to_ls_shape 适配器.app/services/task_matcher.py(task_matcher.py): resolve_task display_id+file_path 两段式查找.ExportService.export_aap_json(export.py): 项目级 + 批次级 AAP JSON 导出, 双数组 annotations[]+predictions[].PredictionImportWizard(PredictionImportWizard.tsx): 三步 Modal (选格式+文件 → dry-run 预览 → 确认提交).- AIPreAnnotate ProjectDetailPanel 「导入预测」按钮 (ProjectDetailPanel.tsx): 与 batch 级预标触发位并列.
- Dashboard ExportSection 「AAP JSON」选项 (ExportSection.tsx): 与 COCO/YOLO/VOC 并列.
- 13 例后端 import 单测 (test_predictions_import.py): AAP bbox/polygon/multi_polygon happy path + COCO happy + schema_version 守门 + task_match display_id/file_path 命中/miss + 跨项目 display_id + dry_run 不入库 + overwrite_existing 替换 + 未知 kind errors + annotator 403 + 默认 source.
- 4 例后端 export 单测 (test_export_aap_json.py): project envelope + batch_display_id 透传 + 空项目 + import→export round-trip.
- 4 例前端 Wizard 单测 (PredictionImportWizard.test.tsx): 文件未选不能预览 + dry-run 预览 errors 渲染 + 格式切换 COCO 参数 + 确认提交 dry_run=false + onComplete.
- ADR-0024 (0024-aap-json-format.md): schema_version / 双数组 / lenient import / geometry 格式选择 / 范围边界.
- 用户文档 AAP JSON 段 (export-formats.md).
- API 导入指南 (import.md): 端点契约 + dry-run 工作流 + task_match 语义.
Changed
PredictionService.create_from_ml_result(prediction.py): 扩source: str = "ml_backend"参数, 默认值保持原行为, 外部导入路径传source="external_import".AuditAction.PREDICTIONS_IMPORT(audit.py): 新 audit action 枚举值.ExportFormat前端类型 (projects.ts · batches.ts): 扩"aap_json"; 文件名默认扩展名按 JSON/ZIP 双分支.
Verified
cd apps/api && uv run python -m pytest tests/test_predictions_import.py tests/test_export_aap_json.py→ 17 passed.pnpm --filter web vitest run src/components/predictions→ 4 passed.pnpm --filter web typecheck→ 全绿 (0 错).- alembic 0069 已在 dev 库 apply 成功,
\d predictions看到source列 +ix_predictions_source索引.
[0.10.14] - 2026-05-18
ProjectTemplate 表 + 模板库 UI. ROADMAP §A 「项目模板」二阶段:v0.10.11 的「从已有项目复制」是项目级一次性快照(80% 场景),v0.10.13 E1 落 annotation_guide 做整合伏笔;本期补「独立模板资产」形态 —— 模板可手工建 / 跨组织共享 / 多次引用 / 携带 annotation_guide 文本。后端 alembic 0068 新建
project_templates表(display_idPT-N序列 + scopeprivate/organization/public+ organization_id CASCADE + created_by + source_project_id SET NULL + usage_count + annotation_guide TEXT,不存 guide_assets),加 CHECK 约束保证 organization scope 必须给 organization_id。_CLONEABLE_PROJECT_FIELDS16 字段白名单抽到app.services.project_clone供 projects + project_templates 共享,避免字段漂移。/project-templates/*5 端点:GET(按 scope 可见性过滤)/ POST(创建,public 仅 super_admin)/ GET :id / PATCH(created_by 或 super_admin)/ DELETE / POST :id/duplicate(克隆为私有副本)。ProjectCreate加template_id: UUID | None,与source_project_id互斥(pydanticmodel_validator强制,违反返 422);POST/projects给定template_id时把模板载荷 deepcopy 进 payload + 模板usage_count + 1;新项目自动继承模板的annotation_guide文本,guide_assets 不携带(storage key 跨实例引用混乱 / 跨组织私密性 / 源项目删 asset 会让所有依赖模板的项目失效)。可见性 serviceassert_template_visible:private 仅created_by看得到、organization 走OrganizationMember查同组织、public 全部;编辑/删除权限 =created_by或 super_admin。前端/project-templates新路由(ProjectTemplatesPage)—— 三 tab(我的 / 组织 / 公共)+ 搜索 + 类型过滤 + 卡片网格;TemplateCard 展示 scope chip / classes 数 / usage_count / 含指引徽标 + 操作(应用 / 克隆 / 编辑 / 删除)。两种创建入口:TemplateEditModal 空白建(name / description / type / classes CSV / annotation_guide / scope),CreateFromProjectDialog 从已有项目导出(后端自动 dump CLONEABLE 字段)。CreateProjectWizard加templateId?prop +buildFormFromTemplate(),模板模式 banner「已用模板字段预填表单 (annotation_guide 也会一并应用; guide_assets 不携带)」;提交时携带template_id,与sourceProjectId互斥;模板模式跳过 localStorage 草稿。Sidebar 工作区栏目新增「项目模板」入口(icon=book),PageKey+ROLE_PAGE_ACCESS同步扩project-templates(project_admin + super_admin 可见)。不引入:① 模板版本号 / changelog(PATCH 直接覆盖,需审计就在创建项目时落项目审计日志);② organization admin 提交 public 模板的审核流(KISS,仅 super_admin 可建 public);③ guide_assets 跨项目 deepcopy(Stage 2,触发条件:客户大量在 guide 中用图且确需跨项目复用,再用 worker 异步 deepcopy 到新项目 storage namespace);④ 模板复杂字段编辑 UI(attribute_schema / classes_config / rendering_config 走「从已有项目导出」即可,首版不在 TemplateEditModal 里复刻 7 步 Wizard);⑤ 模板审计审计日志专用字段(POST /project-templates 默认走 AuditMiddleware 即可)。ROADMAP §A「项目模板」P2 收官,长期项目"独立 Template 库"形态正式上线。→ plan · ADR-0023 · 用户文档.
Added
project_templates表 + alembic 0068 (model · migration):模板载荷与 Project 列对齐 + scope/organization/usage_count 元数据。display_seq_project_templates序列 +PT-前缀 (display_id.py):与既有B-/T-/D-/P-/BT-不冲突。/project-templates/*路由 (5 端点) (project_templates.py):list / create / detail / patch / delete / duplicate。ProjectCreate.template_id: UUID | None(schemas/project.py):与 source_project_id 互斥,pydantic model_validator 强校验。app.services.project_clone(project_clone.py):CLONEABLE_PROJECT_FIELDS+merge_from_source抽出共享。app.services.project_template(project_template.py):scope 可见性 + apply / dump 业务逻辑。/project-templates前端路由 (ProjectTemplatesPage.tsx):列表 + 三 tab + 搜索 + 模态。projectTemplatesApi+useProjectTemplates(api · hooks):list / get / create / patch / remove / duplicate + React Query 自动失效。PageKey="project-templates"+ Sidebar 入口 (types · Sidebar · permissions):project_admin / super_admin 可见。- 14 例后端单测 (test_project_templates.py · test_projects_clone.py):CRUD + scope 可见性 + organization 成员可见 + public 仅 super_admin + 从源项目导出 + duplicate 私有副本 + 应用模板 + usage_count +1 + template/source 互斥 + 404 隐藏存在性 + 模板带 guide / 不带 assets。
- 10 例前端单测 (ProjectTemplatesPage.test.tsx · TemplateCard.test.tsx):加载态 / 空态 / 卡片渲染 / 删除 confirm / 应用打开 Wizard 透传 templateId / canEdit 控制按钮 / scope chip 切换。
- ADR-0023 (0023-project-template-vs-clone.md):解释 template vs clone 边界、并存策略、为何不存 guide_assets。
- 用户文档 (project-templates.md · public-templates.md):模板库使用 + 公共模板治理章节。
Changed
POST /projects(projects.py):新增 template_id 分支,与 source_project_id 互斥;模板模式下 deepcopy 载荷 + usage_count +1 + 显式 refresh 模板让后续 GET 不触发 lazy load。CreateProjectWizard(CreateProjectWizard.tsx):加templateId?prop +buildFormFromTemplate();title 区分「新建/复制/模板」;提交时携带template_id;模板模式跳过 localStorage 草稿。ProjectCreatePayload(api/projects.ts):手动扩template_id?: string | null(待 codegen 重跑)。
Verified
pnpm --filter web typecheck全绿(0 错)。pnpm --filter web vitest run src/pages/ProjectTemplates src/pages/Dashboard src/components/projects→ 59 passed。cd apps/api && pytest tests/test_project_templates.py tests/test_projects_clone.py→ 23 passed。- 手测:① 进
/project-templates→ 「+ 新建模板」填表创建 → 列表出现私有模板。② 「从已有项目导出」选源项目 → 模板的 classes / annotation_guide 来自源项目。③ 卡片「应用」→ Wizard banner 出现 + 字段预填 + 提交后新项目 classes / annotation_guide 来自模板,guide_assets 为空。④ 模板usage_count+1。⑤ 私有模板对其它 project_admin 不可见,公共模板对所有人可见。
[0.10.13] - 2026-05-18
CVAT-style 项目级标注指引(Annotation Guide). ROADMAP §A 的「Annotation Guide(项目级 Markdown 指引 + asset)」P2 一发收尾:项目设置页加「📖 标注指引」section,工作台左上角加可折叠浮层,首次进入工作台自动展开一次。技术 ROI 不高但对标注一致性 / reject 率影响显著。本期不开 ProjectTemplate 表 + 模板库 UI(独立 epic,按计划在 v0.10.14 推进;E1 落地后先观察客户对 guide 的写法 / 图片量 / 跨项目复用诉求,再决定 v0.10.14 的"模板是否携带 guide")。后端 alembic 0067 加
projects.annotation_guide TEXT+projects.guide_assets JSONB DEFAULT '[]';新建/projects/{id}/guide-assets/*4 个端点(upload-init / upload-complete / delete / sign-url),storage key 前缀强制projects/{project_id}/guide/{uuid}-{filename}防越权写入;content_type 白名单 5 类(png/jpeg/webp/gif/svg+xml)+ 单文件 5MB 上限;权限 = project_owner 或 super_admin(与 ProjectSettings 入口对齐)。POST /projects接copy_annotation_guide: boolflag —— 默认 false(保守,不进_CLONEABLE_PROJECT_FIELDS白名单避免复制后信息错位);给定时与源项目source_project_id一起 deepcopyannotation_guide+guide_assets,图片 storage key 复用(不重新上传,源项目删除资源会影响新项目,UX 在 wizard checkbox 文案标注)。前端引入 CodeMirror 6 (@codemirror/state/view/lang-markdown/commands) 写MarkdownEditor—— 行号 / 自动换行 / undo-redo / 9 个工具栏按钮 / 拖拽 + 粘贴图片直传到 guide-assets;走 dynamic import 仅在 ProjectSettings 路由加载避免污染 dashboard 首屏 bundle。GuideMarkdownView复用 react-markdown + remark-gfm,渲染<img src="guide-asset:KEY">时通过useGuideAssets.signAsset批量预签短期 URL(1h 过期 + 内存缓存 + 60s 安全垫重签)。ProjectSettingsPageSECTIONS数组追加annotation-guidetab(icon=book,BookOpen lucide),路由参数?section=annotation-guide深链可用。WorkbenchLayout加可选guidePanelprop;WorkbenchShell透传currentProject.annotation_guide,左上角浮层localStoragewb:guide-seen:{projectId}标记首次自动展开 +wb:guide-collapsed:{projectId}记忆用户折叠状态;项目无 guide → panel 完全不渲染(不露空状态)。CreateProjectWizard在复制模式 banner 加 checkbox「同时复制标注指引(图片资源与源项目共享存储)」默认勾选;FormState 加copyAnnotationGuide: boolean,submit 时仅当sourceProjectId给定才传 flag(后端校验若无 source_project_id 会返 400)。不引入:① 异步压缩 / 缩略图(首版 KISS);② 储存空间 GC(删 annotation_guide 时不主动清理 orphan assets,UI 给「清理未引用资源」按钮留客户反馈触发);③ 工作台内编辑指引(owner 想改回 ProjectSettings,避免工作台 UI 复杂化);④ "我已读"按钮(首次自动展开 + 用户手动折叠已经够用);⑤ 视频指引 / 富文本编辑器(v0.11+);⑥ 独立 ProjectTemplate 表 + 模板库 UI(v0.10.14 单独 epic)。是 ROADMAP §A 「项目模板」二阶段的前置脚手架。→ plan · ROADMAP §A Annotation Guide · 取经合集 §1.1.
Added
projects.annotation_guide TEXT+projects.guide_assets JSONB(project.py · alembic 0067):项目级 Markdown 指引 + 图片资源元数据列表。/projects/{id}/guide-assets/*4 端点 (guide_assets.py):upload-init(签 PUT URL)/ upload-complete(HEAD 校验后 append entry)/ DELETE / GET sign-url(1h 短期)。ProjectCreate.copy_annotation_guide: bool(schemas/project.py):默认 false;仅与source_project_id配合有效,无 source 时 400。MarkdownEditor(CodeMirror 6) (MarkdownEditor.tsx):行号 + lineWrapping + 9 个工具栏按钮 + drop/paste 图片直传;dynamic import 避免首屏负担。GuideMarkdownView(GuideMarkdownView.tsx):react-markdown + remark-gfm 渲染 +guide-asset:KEY自动解析短期签名 URL。useGuideAssets(useGuideAssets.ts):uploadAsset/deleteAsset/signAsset(带内存缓存 + 60s 安全垫重签)。AnnotationGuideSection(AnnotationGuideSection.tsx):项目设置「📖 标注指引」section(编辑 / 预览 tab + 图片资源列表 + 保存)。GuidePanel(GuidePanel.tsx):工作台左上角可折叠浮层,首次自动展开 + localStorage 记忆。- CodeMirror 6 deps (package.json):
@codemirror/state/view/lang-markdown/commands/language/codemirror(~180-220 KB gzipped,dynamic import 加载)。 BookOpenicon (Icon.tsx):name="book",用于 ProjectSettings tab + 工作台浮层标题。- 9 例后端单测 (test_guide_assets.py):upload-init 成功 / 越权 403 / content-type 拒绝 / upload-complete entry append / storage 缺失 400 / key 不属于项目 404 / DELETE 同步移除 + 调 storage / sign-url 短期 / PATCH annotation_guide 写入。
- 3 例克隆扩展单测 (test_projects_clone.py):默认不复制 guide /
copy_annotation_guide=true同时复制 guide + assets / 缺source_project_id时返 400。 - 4 例前端单测 (AnnotationGuideSection.test.tsx):渲染初值 / 修改+保存 mutation / 切预览 tab / asset 删除调 hook。
Changed
ProjectUpdate/ProjectOut(schemas/project.py):加annotation_guide: str | None+guide_assets: list[dict]字段。ProjectCreateWizard+ FormState (CreateProjectWizard.tsx):复制模式 banner 加 checkbox「同时复制标注指引」;FormState 加copyAnnotationGuide,submit 时只在复制模式下携带该 flag。ProjectSettingsPage.SECTIONS(ProjectSettingsPage.tsx):在 rendering 与 owner 之间新增annotation-guidetab;VALID_SECTIONS/SectionKey同步扩展。WorkbenchLayout(WorkbenchLayout.tsx):加可选guidePanelprop,渲染到 modals 同级;项目无 guide 时浮层完全不渲染。projectsApi.guideAssets.*(api/projects.ts):扩 4 个端点 wrapper +GuideAssetEntry/GuideAssetUploadInitResponse/GuideAssetSignedUrlResponse类型。
Verified
pnpm --filter web typecheck全绿(0 错;CodeMirror 6 + GuideAssetEntry / WorkbenchLayout guidePanel prop 全部传通)。pnpm --filter web vitest run src/pages/Projects/sections/AnnotationGuideSection.test.tsx→ 4 passed。cd apps/api && pytest tests/test_guide_assets.py tests/test_projects_clone.py→ 18 passed(9 guide-assets + 6 既有 clone + 3 新 copy_guide)。- 手测(preview):① 登录 super_admin → 进 /projects/.../settings?section=annotation-guide → 编辑器加载(CodeMirror 6 行号 + lang-markdown 高亮);拖入一张 PNG → 网络面板看到 upload-init → PUT → upload-complete 三段调用 → markdown 自动注入
;切预览 tab 看到签名 URL 解析后的图片渲染。② 在该项目工作台/projects/.../annotate第一次进入:左上角 📖 浮层自动展开渲染指引;手动折叠后刷新仍折叠;清localStoragewb:guide-collapsed:*重进又自动展开。③ Dashboard → 项目卡片 ⋮ → 「复制项目配置」→ Wizard banner 出现 checkbox「同时复制标注指引(图片资源与源项目共享存储)」默认勾选;submit 后新项目 GET 返回annotation_guide+guide_assets与源项目内容一致。
[0.10.11] - 2026-05-18
CSP style-src 收紧基建试点 + 项目复制配置. 0.10.x 系列收尾后第一发维护切片,挑两件 ROADMAP §A / §B 中互不耦合、可控的事一次发完:① CSP style-src 收紧基建 (Part A) —— 全站 ~2900 处
style={{...}}重构是个独立 epic,本期只做「试点 1 个高密度 section + lint guard」证明路径可走。把 BatchesSection.tsx(1070 行 / 17 处 inline style,ROADMAP §B 指定的最高密度切入点)的内部 inline style 全部迁到同目录 BatchesSection.module.css,仅保留 1 处 CSS custom property 注入(3-档动态颜色的逃生口,加// eslint-disable-next-line no-restricted-syntax标注);eslint.config.js 加文件级no-restricted-syntaxoverride 禁止JSXAttribute[name.name='style']防止回潮,后续 epic 把 files 列表逐步扩到全站。不引入第三方 CSS-in-JS(vanilla-extract / panda 推迟评估),仅用 Vite 内置 CSS modules 编译。Button 组件自身仍用 inline style 设 base,外部覆盖类(.btnSuccess/.btnDanger/.btnAccent/.modeToggleActive/.bulkConfirmPrimary)暂用!important桥接,待 Button 自身重构后摘掉。不动 CSP 头 —— 其余 2900+ inline style 未迁,'unsafe-inline'暂留;本期是后续 epic 的脚手架。② 项目复制配置 (Part B) —— ROADMAP §A「项目模板」的派生形态:之前每次新建项目都得从 0 配 classes / attribute_schema / AI / rendering_config 等;本期落「从已有项目复制配置」一次性流,不建独立 Template 表 / 模板库 UI(推迟到 v0.10.12+ 客户驱动触发)。后端ProjectCreate加可选source_project_id: UUID | None;POST/projects改用model_dump(exclude_unset=True)让"未显式给出"与"显式给出默认值"可区分,给 source 兜底 16 个可克隆字段(type_label / type_key / classes / classes_config / attribute_schema / ai_enabled / ai_model / label_config / sampling / maximum_annotations / show_overlap_first / iou_dedup_threshold / box_threshold / text_threshold / text_output_default / rendering_config);调用者必须对源项目有 view 权限(复用assert_project_visible),不可见时 404 隐藏存在性;JSONB 字段深拷贝避免新项目 mutate 污染源;若源带ml_backend_id且 caller 未单独指定ml_backend_source_id,自动复用现有_clone_backend_to_new_project()路径派生 backend;不复制 datasets / tasks / annotations / members / batches(运行时数据,复制无意义)。前端 ProjectGrid 行操作菜单加「复制项目配置」(icon=copy),跳/dashboard?new=1&from=<id>;AdminDashboard / DashboardPage 读?from=,CreateProjectWizard 加可选sourceProjectIdprop,打开时调projectsApi.get拉源项目 →buildFormFromSource()把 ProjectResponse 还原为 7 步 FormState(含 classes 排序 + AI model 自定义/预设识别 + 名称默认{源项目名} (副本)),复制模式跳过 localStorage 草稿避免污染普通新建路径;顶部加 banner 提示「已用源项目配置预填表单, 提交后将复制到新项目 (不复制数据集 / 任务 / 成员)」。提交时携带source_project_id让后端兜底 wizard 表单字段之外的字段(label_config / rendering_config / sampling 等)。不引入:① 独立 ProjectTemplate 表 //templatesendpoint / 模板库 UI;② 复制 datasets / tasks / members / batches;③ 审计日志显式追加 source_project_id 字段(现有 AuditMiddleware 已捕获 POST body,足够)。→ plan · ROADMAP §A 项目模板 · ROADMAP §B CSP style-src nonce 收紧.
Added
BatchesSection.module.css(BatchesSection.module.css):43 个语义类(toolbar / viewToggle / banner / table / modalForm / confirmActions 等)+ BEM-like 状态后缀(.viewToggleButtonActive等)+ Button 覆盖类(!important桥)+ 1 个 CSS custom property hatch(--bulk-confirm-bg)。ProjectCreate.source_project_id: UUID | None(schemas/project.py):可选;给定时后端用源项目兜底未显式给出的字段。_merge_from_source_project()+_CLONEABLE_PROJECT_FIELDS(projects.py):16 字段白名单 + JSONB 深拷贝;运行时数据(id / display_id / status / created_at / total_tasks 等)不在列表内。CreateProjectWizard.sourceProjectIdprop +buildFormFromSource()(CreateProjectWizard.tsx):ProjectResponse → FormState 转换;按classes_config.order排序 classes;AI model 自动判预设/自定义;名称默认带「(副本)」后缀。- ProjectGrid「复制项目配置」菜单项 (ProjectGrid.tsx):仅
canManage(p)用户可见(与「项目设置」同 gate)。 - ESLint
no-restricted-syntaxoverride(eslint.config.js):BatchesSection.tsx 禁止style={{...}};后续 epic 把 files 列表扩到全站。 - 6 例后端复制单测 (test_projects_clone.py):覆盖完整字段克隆 / 显式覆盖优先 / 自动派生 backend / 无 view 权限 404 / 不带 source 回归 / JSONB 深拷贝防污染。
Changed
POST /projects改用model_dump(exclude_unset=True)(之前exclude_none=True)。回归校验:45 个 project 相关测试 + 6 个 ml_backend_binding 测试全过;"未显式给出" 与 "显式给出默认值" 现在可区分,让 source_project_id 兜底语义干净。AdminDashboard.tsx/DashboardPage.tsx:读?from=传给 Wizard;关闭 Wizard 时同时searchParams.delete("from")。ProjectCreatePayload(api/projects.ts):手动扩source_project_id?: string | null(待 codegen 重跑)。
Fixed
- ROADMAP §B
CSP style-src nonce 收紧前置:基建到位(CSS modules 约定 + lint guard + 试点完成),后续 epic 可按 ProjectGrid 重复同样模式扩 sections 群。 - ROADMAP §A
项目模板:「复制项目配置」覆盖 80% 实际需求;客户提「跨项目共享模板库」时再做独立 ProjectTemplate。
Verified
pnpm --filter web typecheck全绿(0 错;新 sourceProjectId prop / buildFormFromSource / source_project_id payload 类型全部传通)。pnpm --filter web vitest run692 tests 全绿(0 回归;BatchesSection 已有快照/交互测试全过)。pnpm --filter web lint src/pages/Projects/sections/BatchesSection.tsx干净(lint guard 验证:故意还原 1 处 inline style 报no-restricted-syntax)。cd apps/api && uv run pytest tests/test_projects_clone.py tests/test_projects_ml_backend_binding.py -k project→ 51 passed(6 新 clone case + 45 既有 project 相关全过)。grep -c "style={{" apps/web/src/pages/Projects/sections/BatchesSection.tsx→ 1(CSS custom property hatch,已 lint-disable 注释标注)。- 手测(preview):① 登录 super_admin → 进 /projects/.../settings?section=batches → BatchesSection 渲染(toolbar / view toggle / 列表 / 状态 badge / 操作按钮颜色)与 0.10.10 完全一致;DOM inspect
h3.className = "_toolbarTitle_xxx"+getComputedStyle(h3).fontSize = "14px"验证 CSS modules 编译生效;document.querySelectorAll('section [style]').length = 0验证迁移完整。② 跳/dashboard?new=1&from=<src_id>→ Wizard 模态打开标题「复制项目配置」+ banner「已用源项目配置预填表单, 提交后将复制到新项目 (不复制数据集 / 任务 / 成员)」+ 名称输入框预填2D图片标注测试 (副本)+ step 1-4 字段从源项目拉满。
[0.10.10] - 2026-05-18
0.10.x 收尾:I11 Mask 编辑器 e2e + dirtyRect + 用户文档 + I17.3 项目级渲染配置覆盖. 把 v0.10.4-v0.10.9 留下的四个尾巴一次发完,0.10.x 系列收口,之后进入 v0.11.0(I1 大图 tile 独立 epic)。① §3 MaskBuffer dirtyRect 增量重绘:maskBuffer.ts 加
_dirty半开矩形私有字段 +markDirty()clamp/union +consumeDirty(): DirtyRect | null取走清空 +toAlphaImageDataRect(rect): Uint8ClampedArray切片输出;brush/erase/fromPolygon/clear全部更新内部脏区。MaskOverlayLayer.tsx 改为只putImageData脏区,首次激活(seenBufferRef切换)走一次全图保证 canvas 与 buffer 初态一致;消除 v0.10.8 「每笔 W×H 全量拷贝」性能债。9 例新单测覆盖 union / consumeDirty / clear=全图 / fromPolygon=bbox / 边界 clamp / 切片字节 / 退化区域返空 / clone 复制脏区。② §1 I11 Playwright e2e:新mask-editor.spec.ts三用例(空白 mask→Enter / AI prediction 精修 reject+新 polygon / B-E-Shift+wheel-Esc hotkey);_test_seed加POST /__test/seed/inject-prediction(LabelStudio 标准 shape)+SeedAPI.injectPredictionhelper 绕过 ml-backend;SAM 候选精修入口需真实 backend 不进 e2e,留单测useImageAnnotationActions.test分流过。③ §2 用户文档:新mask-brush.md(since=v0.10.8)—— 三种进入方式(空白 / AI polygon / user polygon / SAM 候选 R 键)、hotkey 速查、已知限制(bbox 不可初始化 / 多连通区只留最大外环 / 不持久化 RLE)+ 故障排查;index.md 加链入。④ §5 I17.3 项目级渲染配置覆盖:alembic 0066 加projects.rendering_config JSONB NOT NULL DEFAULT '{}';后端ProjectRenderingConfigPydantic 模型(smoothImage/cssImageFilter/controlPointsSize/snapToGrid 全 optional, extra=forbid, controlPointsSize ∈ [2,20], cssImageFilter ≤255 字符),写入ProjectUpdate/ProjectOut;前端新RenderingConfigSection子页(每行「覆盖此项」开关 + 控件 + 「跟随用户偏好」回退文案),ProjectSettingsPagesidebar 加「渲染配置」tab(icon=eye,在 ml-backends 与 owner 之间);useWorkbenchConfig加projectRenderingConfig可选入参,合并优先级DEFAULTS → user.preferences.workbench → project.rendering_config,返回新增lockedFields: LockableField[];prop 通过 WorkbenchShell → WorkbenchStageHost → ImageWorkbench → ImageStage 四级透传(ReviewWorkbench 不传 = 默认无项目覆盖)。apps/web/src/api/projects.ts加ProjectRenderingConfig类型 +ProjectResponse/ProjectUpdatePayload手动扩rendering_config字段(待 codegen 重跑)。⑤ §4 I8.2 image-bench fixture 矩阵:新scripts/image-bench/fixtures.json(3 size × 3 density = 9 场景)+run-image-bench.mjsorchestrator(镜像scripts/video-bench,写 manifest.json 到test-results/image-bench/{runId}/)+e2e/tests/image-bench-fixtures.spec.ts按IMAGE_BENCH_SIZE/IMAGE_BENCH_DENSITYenv 跑单场景读window.__workbenchPerf+package.json加image:benchscript +docs/benchmarks/image-bench-v0.10.10.json基线占位 JSON(待_test_seed加?image_size=/?annotation_density=入参 + 真实测试图片后回填真数)。⑥ §6 v0.10.9 收尾:DEV.md加「前端 codegen」章节说明apps/web/src/api/generated/在 .gitignore、prebuild钩子codegen-if-changed.mjs自动重生、首次 clone 时pnpm --filter web typecheck前需手动跑一次 codegen 的开发流程(解释 v0.10.9 AttributeForm 类型未入仓的原因)。不引入:① SAM 候选精修 e2e(需真实 ml-backend);② I17 项目级「lock」语义元数据(本期只做覆盖优先级,前端lockedFields已就位但未渲染 disabled badge,留 v0.11+);③ bbox 候选 → mask 初始填充(与 I9 / geometry.kind 同期,留 v0.11+);④ image-bench 真实图片素材入仓 +_test_seed加 density/size 参(本期落契约 + spec,真数回填留 v0.10.10.1 / v0.11.x);⑤ lint warnings 清理(126 基线,按计划摘低垂果实未在本 commit 动手,量化降到 < 80 留 v0.10.10.1)。是 v0.10.4 epic / 图片工作台 Wave β + γ 的最后一个收尾发布。→ plan · ROADMAP · ADR-0022.
Added
- MaskBuffer dirtyRect (maskBuffer.ts):
DirtyRect接口(半开 [x0,x1)×[y0,y1))+_dirty私有字段 +consumeDirty(): DirtyRect | null+toAlphaImageDataRect(rect): Uint8ClampedArray;9 例新单测。 _test_seed·POST /__test/seed/inject-prediction(_test_seed.py):直插 LabelStudio polygon prediction,e2e 不依赖 ml-backend。SeedAPI.injectPrediction(e2e/fixtures/seed.ts):前端 e2e helper。mask-editor.spec.ts(apps/web/e2e/tests/):3 用例(空白 mask 提交 / AI prediction 精修 / hotkey 全集)。image-bench/fixtures.json+run-image-bench.mjs(apps/web/scripts/image-bench/):I8.2 矩阵契约 + orchestrator(镜像 video-bench)。image-bench-fixtures.spec.ts(apps/web/e2e/tests/):env-driven 单场景执行。pnpm --filter web image:benchscript。docs/benchmarks/image-bench-v0.10.10.json:基线占位 JSON(待真数回填)。Project.rendering_configJSONB (project.py):alembic 0066,NOT NULL DEFAULT '{}'。ProjectRenderingConfigPydantic 模型 (_jsonb_types.py):4 字段全 optional, extra=forbid, controlPointsSize ∈ [2,20], cssImageFilter ≤255 字符。RenderingConfigSection(RenderingConfigSection.tsx):「覆盖此项」开关 + 控件 + 「跟随用户偏好」回退;ProjectSettings 新「渲染配置」tab。useWorkbenchConfig· 项目级覆盖 +lockedFields(useWorkbenchConfig.ts):新增可选projectRenderingConfig入参;返回lockedFields: LockableField[];3 例新单测。ProjectRenderingConfig前端类型 (api/projects.ts):ProjectResponse/ProjectUpdatePayload手动扩rendering_config字段(待 codegen 重跑)。mask-brush.md用户文档 (docs-site/user-guide/for-annotators/mask-brush.md):三入口 + hotkey + 已知限制 + 故障排查;index.md 链入。- DEV.md「前端 codegen」章节:解释 generated 不入仓 + 何时手动跑 codegen。
Changed
MaskOverlayLayer(MaskOverlayLayer.tsx):useEffect 改为消费buffer.consumeDirty()切片绘制;首次激活(seenBufferRef切换)仍走全图。ImageStage/ImageWorkbench/WorkbenchStageHost/WorkbenchShell:新增projectRenderingConfig?: ProjectRenderingConfig | nullprop 四级透传;WorkbenchShell从currentProject?.rendering_config ?? null取值。ProjectUpdate/ProjectOut(schemas/project.py):加rendering_config: ProjectRenderingConfig | None/= ProjectRenderingConfig()。ProjectSettingsPage:sidebar 加「渲染配置」tab(icon=eye),SectionKeyunion 加"rendering"。
Fixed
- v0.10.8 MaskOverlayLayer 全量 putImageData TODO:见 §3 dirtyRect 增量重绘。
- v0.10.7 epic「完整 e2e 推迟到 v0.10.7.1 UI 集成时一并跑」:见 §1 mask-editor.spec.ts。
- v0.10.9「手测推迟到真实数据」:本期 e2e 不依赖真实数据(seed.injectPrediction 绕过 ml-backend)。
Verified
pnpm --filter web typecheck全绿(0 错;新增四级 prop 透传 + ProjectRenderingConfig 类型 + useWorkbenchConfig 入参全部传通)。pnpm --filter web vitest run692 tests 全绿(+9 maskBuffer dirtyRect / +3 useWorkbenchConfig 合并优先级;既有 0 回归)。cd apps/api && uv run pytest tests/test_smoke.py6 passed(+1test_project_rendering_config_v0_10_10:合法 / 部分覆盖 / extra=forbid / 范围越界 / 超长 五个校验分支)。pnpm --filter web image:bench --dry-run输出 9 场景矩阵正常。- E2E
mask-editor.spec.ts与image-bench-fixtures.spec.ts待本地 docker compose + dev server + alembic upgrade 全开后人测(与 v0.10.9 节奏相同)。 - 手测:① admin 在 ProjectSettings 「渲染配置」tab 开启 smoothImage 覆盖 = false → annotator 进工作台像素无插值。② 关闭覆盖开关 → annotator 字段恢复跟随用户偏好。
[0.10.9] - 2026-05-18
Mask 编辑器入口补齐 + 笔刷光标可视化. v0.10.8 留下两个入口缺口(SAM 交互候选 / 已落库 user polygon 均不可精修)和一个体感问题(mask 工具下系统 crosshair 光标看不出笔刷半径与图像的比例)。本期一并补齐:① SAM 候选精修 (A):useImageAnnotationActions 新增
handleRefineSamCandidate(idx),从sam.candidates[idx].points(归一化 [0,1] × imgW/imgH 转像素)启动 mask 编辑;commit 路径调sam.consume(samIdx)移除原候选 +submitPolygon新建 polygon(label 优先用候选 label,缺省 / 不在 classes 时回退工具栏当前 label);R 键 hotkey 在 SAM 候选键盘 handler 内 capture 阶段消费(与现有 Tab/Enter/Esc 同模式,走 ref 间接绕过 forward 依赖);ImageStage 画布上 active polygonlabels 候选附近浮一个✎ 精修 R按钮,位置贴 polygon 顶点 bbox 右上角。② user polygon 精修 (B):useImageAnnotationActions 新增handleRefineUserPolygon(annotationId),从ann.geometry.points启动;commit 路径走mutations.update.mutate替换geometry(不新建 annotation),同步history.push({ kind: "update", before: { geometry }, after: { geometry } });BoxListItem 加onRefine用于 user polygon 行(之前 v0.10.8 仅 AI 行用),按钮图标同 AI 行。pendingRefineRef内部扩成 discriminated union(prediction/sam/user),commitMaskAsPolygon按 kind 分流。③ 笔刷光标可视化 (Cursor):mask 工具激活时 containercursor: "none",ImageStage overlay 层挂一个跟随鼠标的Konva.Circle(半径 =maskEditor.radiusimage-space px,stroke 1.5/scale 保持视觉一致),brush 模式红色 / erase 模式灰色,让笔触大小直接相对图像可视;handleStageMouseMove 在 tool === "mask" 时同步 maskCursor 状态。不引入:BoxesList 上 AI 行 + user 行精修按钮的二级 confirm(直接进 mask 工具,cancel 时退回原状)、SAM 候选浮按钮的拖动(位置固定在候选 bbox 右上)、user polygon 精修的多选批量(仅单行入口)。这些待 v0.11+ 评估。→ CHANGELOG 0.10.8 · ROADMAP I11 · ADR-0022 v0.10.9 段.
Added
handleRefineSamCandidate(idx)(useImageAnnotationActions.ts):仅 polygonlabels 候选启用;不存在 / bbox 候选 / 顶点 <3 时 toast 提示。handleRefineUserPolygon(annotationId):从 annotationsRef 取 polygon → initFromPolygon;commit 走 update mutation + history.push update。- R 键 hotkey:在 SAM 候选 keydown handler 内(仅 AI 工具激活 + 候选存在时生效),与 Tab/Enter/Esc 同 capture 路径。
- ImageStage SAM 精修浮按钮:
onRefineSamCandidate可选 prop;polygon 候选 active 时浮在 bbox 右上角,按钮 hint 显示R。 - MaskBrushCursor(ImageStage overlay 层):跟随鼠标的 Konva.Circle,半径 = maskEditor.radius,brush 红 / erase 灰;container
cursor: "none"在 mask 工具下生效。 - BoxListItem 单测:v0.10.9 新增 2 例(AI 行 + user polygon 行各 1 例 onRefine 渲染 + 点击回调)。
Changed
useImageAnnotationActions:pendingRefineRef类型扩为{ kind: "prediction" | "sam" | "user", ... };commit 路径按 kind 分流(user → update / 其余 → submitPolygon);抽出initMaskFromNormalizedPoints(norm)内部工具,三个 refine 入口共用。ImageStage.tsx:DragInit 之外加maskCursor状态 + Konva.Circle 渲染;containercursor: "mask" → "none";props 加onRefineSamCandidate;handleStageMouseMove 维护 maskCursor。ImageWorkbench/WorkbenchStageHost:props 加onRefineSamCandidate,单点透传。AIInspectorPanel/BoxesList:props 加onRefineUserPolygon,user 行 +geometry?.type === "polygon"时把onRefine绑给BoxListItem。BoxListItem:user 分支(!isAi)也渲染onRefine按钮(图标edit),data-testid="user-refine-{id}"。WorkbenchShell:解构handleRefineSamCandidate/handleRefineUserPolygon,分别绑给stageHost.onRefineSamCandidate/inspector.onRefineUserPolygon。
Fixed
- 预存:
AttributeForm.mutable类型缺字段(v0.10.6 M4-γ I13.2 遗留):跑pnpm --filter web codegen重生apps/web/src/api/generated/types.gen.ts,AttributeField.mutable?: boolean | null已写入;本次 commit 不入 generated(.gitignore),文档说明开发者本地 codegen 一次即可。
Verified
pnpm --filter web typecheck全绿(0 错;之前预存的 3 处 AttributeForm 错误本次随 codegen 消除)。pnpm --filter web lint0 errors / 126 warnings(warnings 全部预存)。pnpm --filter web test --run92 test files / 679 tests 全绿(+2 BoxListItem onRefine 回归)。- 手测脚本(推迟到真实数据):① smart-point 工具点图像出 polygon SAM 候选 → 不按 Enter,按 R 或点画布上的「精修」按钮 → mask 编辑 → 涂改 → Enter 提交 → 候选消失 + 新 polygon 入库。② 选中已落库 polygon,右侧侧栏 polygon 行的「精修」按钮 → mask 编辑 → 涂改 → Enter → 原 polygon 几何被 update 替换(history 可 undo 回原 geometry)。③ mask 工具下鼠标移动 → 圆圈跟随,Shift+滚轮调半径,圆圈大小随 radius 变化。
[0.10.8] - 2026-05-18
Mask 编辑器 UI 集成(M4-δ 收尾). 把 v0.10.7 / v0.10.7.1 的算法核 + 状态层接到 Konva 渲染层、ToolDock、AI 候选「精修」入口、笔刷 hotkey,完成 I11 v1 端到端可用闭环:① 新增
MaskTool(hotkey="M"、onPointerDown在空白态自动beginBlank并返回maskBrushDragInit)、MaskOverlayLayer(单Konva.Image节点 + 内部HTMLCanvasElement,由useMaskEditor.revision触发putImageData,半透红rgba(220,38,38,0.45))、MaskToolbar(stage 顶部浮条:半径 slider/B·E chips/确认·取消/dirty 指示);② ImageStageDragInitunion 加maskBrush,pointermove 线段插值(步长 = radius/2)连续paintAt,pointerup 不 commit;wheel handler 加 Shift+滚轮调半径分支(±2px, clamp [1,200], 仅 deltaY 主导时响应避免 trackpad 横向滚动误触);③ 工具注册:TOOL_REGISTRY.mask/ALL_TOOLS/ ToolDockTOOL_DESCRIPTORS/useWorkbenchState.Toolunion / AIToolDrawerTOOL_HINT/AIInspectorPanel.AIPredictionPopover.toolunion 全部加"mask";④useImageAnnotationActions新增handleRefinePrediction(polygon 候选 → 像素坐标 →initFromPolygon+setTool("mask")+ 缓存pendingRefineRef) 与commitMaskAsPolygon(commit → 像素坐标转归一化 → 客户端 reject 原候选 + 临时切候选 label +submitPolygon,空 mask 提示,多连通区警告,成功后cancel + setTool("box"));⑤useWorkbenchHotkeys加 mask 工具上下文 useEffect(capture 阶段抢 B/E/Enter/Esc,先于主 dispatchKey),hotkeys.tsHOTKEYS 表 +setToolaction union +RESERVED_LETTERS全部加M,主 dispatchKey 加m/M → setTool("mask");⑥BoxListItemAI 行 + polygon 几何时显示「精修」按钮(icon=edit),经BoxesList→AIInspectorPanel→WorkbenchShell透传到imageActions.handleRefinePrediction;⑦ WorkbenchShell 实例化useMaskEditor({ width: imgW, height: imgH }),把maskEditor注入useImageAnnotationActions/useWorkbenchHotkeys/WorkbenchStageHost.stageHost→ImageWorkbench→ImageStage/ overlays 中的MaskToolbar。不引入:bbox 候选 → mask 初始填充(v0.11+ 与 I9 一起做 geometry.kind 收口)、RLE schema、mask 多组件入库(仅落最大外环,多连通 toast 提示)、dirtyRect 增量重绘(首期全量 putImageData,留 TODO)、mask 跨任务持久化。是 v0.10.4 epic / I11 的 UI 集成收尾。→ plan · roadmap I11 · ADR-0022.
Added
- MaskTool (MaskTool.ts):
CanvasTool,id="mask",hotkey="M",cursor="crosshair";onPointerDown在!active时beginBlank(),立即paintAt(pt.x*imgW, pt.y*imgH),返回{ kind: "maskBrush", lastX, lastY }。4 例单测(readOnly / 无 editor / 空白 → beginBlank+paintAt / 已激活 → 直接 paintAt)。 - MaskOverlayLayer (MaskOverlayLayer.tsx):单
Konva.Image绑定内部 canvas;revision变更时buffer.toAlphaImageData()→ 染红 →createImageData+putImageData+layer.batchDraw()。仅 active 时挂载在 user 层之上。 - MaskToolbar (MaskToolbar.tsx):stage 顶部居中浮条:半径 slider [1,200]、笔刷/橡皮 chips、确认/取消按钮、dirty 与「Shift+滚轮调半径」提示;仅
tool === "mask"显示。 useImageAnnotationActions·handleRefinePrediction/commitMaskAsPolygon/cancelMaskEdit:精修流程 (polygon 候选 → mask 编辑 → 自动 reject 原候选 + 新 polygon 用候选 label);空白流程 (空白 mask 编辑 → polygon 用工具栏当前 label);多连通区时 toast 警告。useWorkbenchHotkeys· mask 工具专用 keydown 块:tool === "mask" 且 maskEditor 注入时,capture 阶段抢B(setMode brush) /E(setMode erase) /Enter(commitMaskAsPolygon) /Esc(cancelMaskEdit),先于主 dispatchKey;与 polygon 工具的专用键体系并列。- hotkeys.ts HOTKEYS 表新增:
M / Shift+wheel / B-mask / E-mask / Enter-mask,便于 HotkeyCheatSheet 展示。 BoxListItem.onRefine可选 prop + 精修按钮(BoxListItem.tsx):AI 行 + polygon 几何时显示edit图标按钮。
Changed
useMaskEditor:把内部setRev的值通过revision: number暴露到返回,MaskOverlayLayer 可作为 React 依赖触发重画;buffer 引用本身不变也能正确刷新。单测加 1 例 revision 回归。ImageStage.tsx:DragInit union 加maskBrush;pointermove 加 maskBrush 分支(线段插值 paintAt);pointerup 不 commit maskBrush;wheel 加 Shift 分支调半径;user 层后挂<MaskOverlayLayer>;container cursor 加tool === "mask" → crosshair;ToolPointerContext加maskEditor字段并在 handleStageMouseDown 透传。- 工具注册:
tools/index.tsToolId加"mask";TOOL_REGISTRY.mask = MaskTool;ALL_TOOLS在 PolygonTool 后插入 MaskTool。useWorkbenchState.Toolunion 与AIInspectorPanel.AIPredictionPopover.toolunion 同步加"mask";AIToolDrawer.TOOL_HINT加mask: null。 ToolDock.tsx:TOOL_DESCRIPTORS.mask描述「Mask 笔刷 · B/E 切模式, Shift+滚轮调半径, Enter 提交」。AIInspectorPanel:AIInspectorPanelProps/BoxesListProps加onRefinePrediction可选 prop,逐层透传;AI 行 +geometry?.type === "polygon"时把onRefine绑给BoxListItem。WorkbenchShell:实例化useMaskEditor({ width: stageGeom.imgW||1, height: stageGeom.imgH||1 });注入useImageAnnotationActions(新maskEditorarg) /useWorkbenchHotkeys(新maskEditor/commitMaskAsPolygon/cancelMaskEditargs) /WorkbenchStageHost.stageHost.maskEditor;overlays 包裹<MaskToolbar>(tool === "mask" 时) + 原<WorkbenchOverlays>;inspector 的onRefinePrediction绑到imageActions.handleRefinePrediction。WorkbenchStageHost/ImageWorkbench:props 加maskEditor,单点透传到ImageStage。hotkeys.ts:HotkeyAction.setTool.toolunion 加"mask";RESERVED_LETTERS加m/M;主 dispatchKey 加e.key === "m"/"M" → setTool("mask")单键路由(在 polygon 之后)。
Verified
pnpm --filter web typecheck全绿(本次 0 新错;遗留 3 处AttributeForm.mutable为 v0.10.6 预存,与本期无关)。pnpm --filter web lint0 errors / 126 warnings(warnings 全部预存,未新增)。pnpm --filter web test --run92 test files / 677 tests 全绿(新增 +4 MaskTool 例 + 1 useMaskEditor revision 回归)。- 浏览器手测推迟到引入有 polygon AI 候选的真实数据后单独跑(本期算法层 + 状态层 + UI 集成的纯单测 + typecheck 已覆盖回归面)。
Notes
- 半径单位是图像像素,半透红色
rgba(220,38,38,0.45)在浅底图清晰,深色底图考虑后续加 1px 白色外描边。 - 「自动 reject 原候选」采用客户端
dismissedShapeKeys集合 (与原handleRejectPrediction同路径),prediction 实体仍在服务端;submitPolygon 失败时 dismissed 已写入但 prediction 不入库 — 用户重新进入任务时原候选仍会回归,符合「重做需手动驳回新候选」语义。 - Konva.Image 大画布(>4K)每笔全量
putImageData可能掉帧;本期不做 dirtyRect 增量,留 v0.11+ MaskBuffer 暴露脏矩形再优化。
[0.10.7.1] - 2026-05-15
Mask 编辑器状态层(M4-δ 后续). 把 v0.10.7 落地的
MaskBuffer/maskToPolygon算法核组装成useMaskEditor状态 hook(useMaskEditor.ts):维护 buffer 引用(useRef,paintAt 每笔不 rerender)+ active / mode (brush|erase) / radius (clamp 到 1-200px) / dirty (改过 buffer 没 commit 时 true) 状态,暴露beginBlank/initFromPolygon(AI 候选精修入口) /paintAt(x,y)(按 mode + radius 调 brush/erase) /setMode/setRadius(clamp) /cancel/commitToPolygon(空 mask 返 null;非空走 maskToPolygon → 外环顶点)。状态层独立可单测,为 v0.10.7.2 / v0.11.0 的 Konva 集成 / ToolDock 按钮 / AIPredictionPopover「精修」入口 / 笔刷 hotkey 铺路。
Added
useMaskEditor(useMaskEditor.ts):mask 编辑器状态 hook + 12 例单测覆盖初始态 / radius clamp(含 NaN)/ beginBlank / initFromPolygon / paintAt brush 留圆 + erase 抹掉 + 非 active 静默 / cancel 清空 / commitToPolygon 空返 null + 有内容返外环顶点 + 非 active 返 null。MASK_BRUSH_MIN_PX/MAX_PX/DEFAULT_PX:1 / 200 / 16,与 ADR-0022 笔刷参数对齐。
Notes
- 本期不含:Konva 渲染层(MaskCanvasOverlay)、ImageStage
maskBrushDragInit 派发、ToolDock 按钮、AIPredictionPopover「精修」入口、B/E/Shift+滚轮/Esc/Enter hotkey。这些 UI 集成留到 v0.10.7.2 / v0.11.0。理由同 v0.10.7:UI 集成高耦合 + 高视觉验证成本,独立子版本里跑完整 e2e 更稳。 - 与 useDirtyTracker (v0.10.6) 协同:mask 编辑「一笔不入 history、松手前累计 flush 一次」语义可由
useDirtyTracker.flush承载;commitToPolygon 入口由调用方决定何时触发 flush。
[0.10.7] - 2026-05-15
Mask 编辑器 v1 算法核 + v0.10.4 epic 收尾 (M4-δ). ROADMAP/[archived]2026-05-12-image-workbench-optimization.md 的 I11 算法核落地(UI 集成留 v0.10.7.1 / v0.11.0):① 新增数据层
stage/shared/geometry/maskBuffer.ts—— 纯 TS 单通道Uint8Arrayalpha 缓冲,brush / erase / clear / fromPolygon (扫描线填充) / toAlphaImageData / clone,半径 1-200px 单笔 ≤ 125k 像素操作;② 新增算法层stage/shared/geometry/maskToPolygon.ts—— flood-fill 找连通分量 + Moore-Neighborhood 8-邻轮廓追踪 +polygon-clipping@0.15.7union 去自相交 / 平滑锯齿 + 复用simplifyPolygonRDP 压缩,多连通时取最大面积外环 +multipleComponents=true上抛 UI;③ ADR-0021 polygon LOD + 空间索引(I2 决策回填)+ ADR-0022 mask editor architecture(不引入 RLE schema 的 v1 决策)。不引入 浏览器 OffscreenCanvas API(jsdom / SSR / preview 也能跑)、d3-contour(包体 ~30KB 不划算)、RLE mask schema(留 v0.11+ 与 I9 / I10 一并做 geometry.kind 统一)。是 v0.10.4 epic 的第 4/4 子版本兼 epic 收尾。→ plan · epic · roadmap · ADR-0021 · ADR-0022.
Added
MaskBuffer(maskBuffer.ts):纯 TS alpha 缓冲(Uint8Array W*H,0/255 二值),brush(cx, cy, r, value)圆栅格化双循环 + bbox 裁剪,erase=brush(0)糖,clear全清零,fromPolygon扫描线 + 射线投票(与 canvas2d fill 等价),toAlphaImageData输出 RGBA 缓冲(仅 A 通道),clone深拷贝。12 例单测覆盖构造异常 / 圆笔刷面积近似 π·r² / 越界裁剪 / erase 抹掉 / 矩形 polygon / 三角形 polygon / 顶点 < 3 静默 / polygon 越界裁剪 / RGB 通道为 0 / clone 独立。maskToPolygon(maskToPolygon.ts):marching-squares + Moore-Neighborhood tracing 主流程:flood-fillfindComponents→ 选最大分量 →traceBoundary8-邻顺时针绕一圈 →polygon-clipping.union([poly])去自相交 / 平滑锯齿 →simplifyPolygon(RDP,epsilon 默认 1px)压顶点 → 去连续重复点。7 例单测覆盖空 mask / 矩形 / 圆形(RDP 起效,顶点数 << 周长)/ 多连通取大丢小 / 顶点无重复 / threshold 阈值控制 / epsilon=0 跳过简化。- ADR-0021 (0021-polygon-lod-and-spatial-index.md):I2 决策回填(v0.10.4 实际落地)—— Douglas-Peucker LOD + O(n) 增量自相交 + rbush 顶点视口粗筛 + commit 路径复查 no-op;候选方案 B(远视距 bbox fallback)/ C(WebGL 自渲染)拒绝理由。
- ADR-0022 (0022-mask-editor-tool-architecture.md):I11 v1 决策 —— 离屏 alpha 缓冲 + marching-squares + 不引入 RLE schema;候选方案 B(直接做 RLE schema 升级)/ C(d3-contour)拒绝理由;v0.10.7.1 / v0.11.0 后续 UI 集成清单。
Changed
- ROADMAP I11 描述更新:从「待开发」改为「算法核 ✅ v0.10.7 / UI 集成 🚧 v0.10.7.1」,并列出后续待补条目(MaskTool / ToolDock / AIPredictionPopover / hotkey)。
- epic 计划文件:2026-05-14-image-workbench-wave-beta-gamma-epic.md 表格 v0.10.7 行改为「✅ 算法核已发布」,「跨 sub-milestone 收尾」节列出 v0.10.7 实际收尾动作(CHANGELOG / ROADMAP / ADR 完成;e2e 推迟)。
Verified
- vitest 全量 90 test files / 661 tests 全绿(+19 新例:MaskBuffer 12 + maskToPolygon 7);
pnpm --filter web typecheck全绿;pnpm --filter web lint0 errors / 125 warnings(warnings 全部预存,未新增);- 浏览器手测 / e2e 推迟到 v0.10.7.1 UI 集成时一并做(本期纯算法核,无 UI 副作用面)。
Notes
- 范围裁剪说明:plan 原文写 Konva
stage/tools/MaskTool.tsx+MaskCanvas.tsx+ AIPredictionPopover「精修」+ ToolDock 入口 + B/E/Shift+滚轮/Esc/Enter 笔刷 hotkey;本期裁到「算法核单测稳住」+ ADR 决策固化,UI 集成推迟到 v0.10.7.1 / v0.11.0。理由:UI 集成是高耦合 / 高视觉验证成本的工作,独立子版本里跑完整 e2e 更稳;算法核纯 TS 可单测,可以独立稳住。 - v0.10.4 epic 收尾:本版是 v0.10.4 epic(图片工作台 Wave β + γ I11/I13/I15)的第 4/4 子版本。epic 全程 4 个子版本(v0.10.4 polygon LOD + SAM 缓存 / v0.10.5 形状元数据 / v0.10.6 attribute mutable / v0.10.7 mask 算法核)。后续 v0.11+ 接 I1 大图 tile 金字塔独立 epic + I9 / I10 / I11 v2 一并做 geometry.kind 收口。
- 协同:mask 编辑器笔刷的「一笔不入 history、松手前累计 flush 一次」路径预计复用 v0.10.6 落地的
useDirtyTracker.flush。
[0.10.6] - 2026-05-15
Attribute mutable/immutable + useDirtyTracker 首次消费 (M4-γ). ROADMAP/[archived]2026-05-12-image-workbench-optimization.md 的 I13.2 + I16 落地:① schemas
AttributeField加mutable: bool字段(默认 None / 向后兼容,仅视频任务消费),class_definitions 仍 JSONB 无需 alembic;② schemasVideoTrackKeyframe加attributes: dict | None字段,承载 mutable 属性的逐帧 override(不污染 track 默认 attributes),video_track geometry 也是 JSONB 同样无需 alembic;③ 前端AttributeForm接受context: "image" | "video"prop,context=video下 mutable 字段渲染「逐帧」徽标,context=image维持忽略(向后兼容);④useDirtyTracker补flush(id, commit)API:commit 同步抛或 Promise reject 自动回滚 dirty;AttributeForm作为首位消费者,接收dirtyTracker+annotationId时旁路 400ms debounce,改为「输入标 dirty / blur 出 form 一次 flush」节奏(避免逐字段请求风暴)。是 v0.10.4 epic 的第 3/4 子版本。→ plan · epic · roadmap.
Added
AttributeField.mutable(_jsonb_types.py):可选 bool(默认 None),表达 CVAT 风格 mutable / immutable 二分。仅视频任务消费;图片任务忽略。VideoTrackKeyframe.attributes(_jsonb_types.py):可选dict[str, Any],承载该帧上 mutable 属性的 override;为 None 时该帧 fallback 到 annotation.attributes(track 默认值)。AttributeFormcontext + dirtyTracker 接入 (AttributeForm.tsx):新增context: "image" | "video"+ 可选dirtyTracker/annotationIdprops。video 模式下 mutable 字段加「逐帧」徽标;传 dirty tracker 时旁路防抖、blur form 一次 flush。useDirtyTracker.flush(id, commit)(useDirtyTracker.ts):取脏字段、先清空再 commit;commit 同步异常或 Promise reject 自动 markDirty 回滚,下次 flush 重试。9 个新单测覆盖累积去重 / 同步 commit / 异步 reject 回滚 / 同步抛 throw 回滚 / subscribe 通知 / 跨 annotation 隔离。AttributeForm.test.tsx(新增):4 例覆盖 video 下 mutable 徽标可见 / image 下被忽略 / dirtyTracker 模式 blur flush / 无 tracker 时维持 400ms debounce。- pytest 4 例 (test_jsonb_strong_types.py):mutable 默认 None、AttributeSchema 混合 mutable/immutable、VideoTrackKeyframe.attributes 可选、完整 VideoTrackGeometry with override。
Changed
openapi.snapshot.json+docs-site/api/openapi.json:codegen 重新生成;前端apps/web/src/api/generated/types.gen.ts同步包含AttributeField.mutable?与VideoTrackKeyframe.attributes?。useDirtyTracker注释:从「无消费者占位」更新为「首位消费者 = AttributeForm」,并补出 flush API 文档与回滚保证。
Verified
- pytest
test_jsonb_strong_types.py26/26 +test_attribute_audit.py1/1 全绿(4 个新例)。 - vitest 全量 642/642 全绿(13 个新例:useDirtyTracker 9 + AttributeForm 4)。
pnpm --filter web typecheck全绿;pnpm --filter web lint0 errors / 125 warnings(warnings 全部预存,未新增)。- 浏览器烟囱推迟到 v0.10.7 epic 收尾时与 mask 编辑器一起 e2e 验。
Notes
- 范围裁剪说明:plan 提到的 VideoTrackPanel 「track 默认 / 当前帧覆盖」表格留待 v0.10.6.1 或 v0.10.7 一起做(当前
pages/Workbench/stage/VideoTrackPanel.tsx无 attribute 集成入口,引入会牵动视频工作台 UI 较多面积)。本期先把后端 schema 与前端基础设施稳住,等真有调用方再补 UI 表格。 - M4-δ (v0.10.7) 衔接:mask 编辑器笔刷的「一笔不入 history、松手前累计 flush 一次」路径可直接复用
useDirtyTracker.flush。 - v0.7.6 起 attribute schema 框架(6 种 input_type / 必填 / 条件级联 / AttributeForm 自动渲染)已完整;本期只补 I13.2 残留,I13.1/I13.3/I13.4 不再动。
[0.10.5] - 2026-05-15
图片工作台形状元数据一等态 (M4-β). ROADMAP/[archived]2026-05-12-image-workbench-optimization.md 的 I15 落地:annotation 表加 4 个状态位 (
z_order/is_locked/is_hidden/is_occluded),从前端 transient → DB 持久化;KonvaBox/KonvaPolygon 按 flag 调渲染 (hidden 跳过 / locked 禁拖 / occluded 虚线+半透);annotation 列表按 z_order ASC 排序高 z_order 后渲染 (在上);右栏卡片新增 3 个 toggle icon (隐藏 / 锁 / 遮挡);快捷键L/H/O切对应 flag,[/]选中态调 z_order ±1(无选中维持原 threshold ±0.05)。前端工具栏在锁定时不进入编辑态;PATCH 走现有字段级路径无需新端点。是 v0.10.4 epic 的第 2/4 子版本。→ plan · epic · roadmap.
Added
- alembic 0065 (0065_annotation_shape_metadata.py):annotations 表加
z_order int default 0+is_locked/is_hidden/is_occluded三个 bool default false,全部 not null。 - Annotation 模型 + Pydantic schema 透出:annotation.py:model 新增 4 mapped_column;annotation.py:schema
AnnotationOut直传 +AnnotationUpdate接受 partial PATCH;service.update 加 4 kwargs。3 个 pytest 覆盖默认值 / 全量 PATCH / 单字段 partial。 handlePatchShapeFlag(useWorkbenchAnnotationActions.ts):字段级 PATCH 入口,复用现有mutations.update+history.push+ 离线 enqueue 兜底;写入update历史 kind 支持撤销重做。- BoxListItem toggle icons (BoxListItem.tsx):用户态卡片新增 3 个 icon 按钮 (eye/eyeOff、lock/unlock、circleDot for occluded),title 含快捷键提示;处于 active 状态时不透明,否则 55% 透明。
- 快捷键 L / H / O /
[/](hotkeys.ts + useWorkbenchHotkeys.ts):新增 HotkeyActiontoggleShapeFlag/bumpZOrder;选中态时[/]改调 z_order ±1(无选中维持 thresholdAdjust);L/H/O 切 lock/hidden/occluded(无选中 → 不消费,避开 setClassByLetter 抢键)。HotkeyCheatSheet 表同步。8 个新单测覆盖 dispatcher 分支。
Changed
ImageStage渲染 pipeline:新增visibleSortedUserBoxesmemo:filter!is_hidden+ sort byz_orderASC (tie-breaker = 原数组顺序),user 层.map改走它。KonvaBox/KonvaPolygon:新增occludedprop,true 时 stroke 走dash=[4,3]/scale+opacity=0.5;与selfIntersect的红色虚线视觉互斥(selfIntersect 优先)。ImageStage编辑态门控:isPrimarySingleSelect与editable在is_locked时为 false → 不显示 resize/move/vertex handle;polygon 同步。useInteractiveAI.warmup:保持 v0.10.4 行为,与 M4-β 无关。AnnotationUpdatePayload(api/tasks.ts) +Annotation/AnnotationResponse(types/index.ts):补 4 个新字段。annotationToBoxtransform 默认回落 0 / false 保持向后兼容。
Verified
- pytest
test_annotation_shape_metadata.py:3/3 通过;alembic 0065 在本地 + 测试 DB 都已 upgrade。 - vitest 触及域回归:
hotkeys.test.ts56 例 +BoxListItem.test.tsx+useAnnotationHistory.hook.test.ts全绿。 pnpm --filter web typecheck+lint全绿(warnings 全部预存)。- 浏览器烟囱:anno 登录 → 进入图片工作台 → 点 BoxListItem 锁定按钮 → DB 字段
is_locked=true, version=2,按钮文案翻转为「解锁」;撤回到 false 验证可逆。
Notes
- 后端 PATCH 路径已字段级(
data.model_dump(exclude_unset=True)),无需改 tasks.py 端点,新字段自动透传。 - 旧记录的默认值由 alembic server_default 兜底(z_order=0 / false×3),前端 transform 再叠一层归零保险。
- M4-γ (v0.10.6) 衔接:Attribute mutable/immutable + useDirtyTracker 首次消费。
[0.10.4] - 2026-05-14
图片工作台 Wave β · polygon 性能闭环 + SAM 前端缓存 (M4-α). ROADMAP/[archived]2026-05-12-image-workbench-optimization.md 的 Wave β 起步落地:① I2.1 KonvaPolygon 渲染层 Douglas-Peucker LOD(编辑/选中态用原顶点,其它按 viewport scale 简化到 1px 视觉等价);② I2.2 自相交检测分两档:拖顶点中 O(n) 增量(仅检受影响两条边),静态 / 加载时 O(n²) 全量兜底;③ I2.3 扩展现有
rbush索引到 polygon 顶点,编辑态视口外顶点不渲染 Konva Circle 句柄(500-顶点 polygon 视口内 ~20 个时节点数 -95%);④ I6.1 SAM 候选前端 LRU 缓存(32 项,key 含taskId|backend|ctxKind|normalize(ctx),浮点 4 位小数 round 防抖动),切 backend 时 clearAll;⑤ I6.2 工作台 mount 时 dummy point @ image center 静默触发 backend embed 预热,每 (task, backend) 一次。同步把 roadmap I20/I13/I14/I15/I16/I2/I6 文案校准到 v0.9.41+v0.10.3 现状。→ plan · epic · roadmap.
Added
stage/shared/geometry/simplify.ts(I2.1): Douglas-Peucker 闭合多边形版(拆段 RDP 合并避免环裂开)+epsilonForScale工具。7 个单测覆盖近共线剔除 / 偏离保留 / ≥3 顶点保底 / 200-顶点 circle massive reduction。isSelfIntersectingIncremental(points, changedIdx)(polygon.ts) (I2.2): O(n) 只检查与变更顶点相邻的两条边 vs 其它非相邻边;ImageStage 顶点拖拽中(drag.kind === "polyVertex")走增量版,静态走全量。5 个单测覆盖。buildVertexIndex(points)(iou-index.ts) (I2.3): 单 polygon 顶点 rbush 索引;KonvaPolygon新增viewportBBoxprop,编辑态 + 顶点 ≥60 时按视口粗筛顶点 / 边句柄。ImageStage计算归一化视口 bbox 传入。4 个单测覆盖。useSamCache(useSamCache.ts) (I6.1): LRU 32 项 +makeSamCacheKey(坐标 4 位小数 round 防抖动 + key 排序后 JSON 稳定)+clearAll();useInteractiveAI.dispatch命中直接 setCandidates 跳 HTTP,非空结果回写缓存,切mlBackendId时清空。10 个单测覆盖。useInteractiveAI.warmup()(I6.2): 每 (taskId, backendId) 一次,发图中心 dummy point 静默触发 backend embedding 加载;返回值写入 cache,下次真实点击命中。失败静默(sam3 exemplar-only 等)。WorkbenchShell在 image 工作台 + 已绑 backend 时自动调用。- E2E 烟囱测占位 (
e2e/tests/workbench-perf.spec.ts): 断言window.__workbenchPerf在 image 工作台 mount 后存在。完整 3×3 fixture(2K/8K/dense × 10/100/500 shapes)推迟到 v0.10.5(需后端_test_seed加 density 参数)。
Changed
- ROADMAP/[archived]2026-05-12-image-workbench-optimization.md 校准: I20 标 ✅ v0.10.1-0.10.3(残留 tracker/auto-annotation 类型挪 v0.11+); I13 重写现状(6 种 input_type / 必填 / 条件级联已就位,残留 mutable/immutable); I14 把 martinez 改成已在依赖的
polygon-clipping@0.15.7; I15 重写现状(DB 字段全空白,需 alembic); I16 文案精确化(基础设施在 useDirtyTracker,首次消费 v0.10.6); I2 + I6 补现状分类(rbush 已用 / 后端 embedding cache 已就位)。Wave β/γ 优先级表标注 v0.10.4-0.10.7 sub-milestone 归属。 KonvaPolygon: 增加viewportBBoxprop + 内部useMemo算renderPs(简化)+visibleVertexIdx(视口可见顶点集合);非编辑且未选中态走 LOD 渲染,编辑态走视口顶点粗筛。useInteractiveAI.dispatch: 在 HTTP 调用前查前端缓存命中,命中即跳过;后端 embedding cache + 前端 mask cache 双层。
Notes
- 纯前端 + 一条文档校准 + 一个 e2e 占位,无后端改动,无 alembic。
- M4-α 是 v0.10.4 epic(Wave β + γ I11/I13/I15)的第 1/4 子版本;M4-β (v0.10.5) 接 I15 z_order/lock/hidden/occluded 字段一等态。
- I2.4 "polygonVertexBatch history kind" 经过审视判定为冗余:现有 ImageStage drag 路径已是 pointerup 单条 commit,每次
handleCommitPolygonGeometry仅 push 一条 update 历史,符合 roadmap I2.4 原意。
[0.10.3] - 2026-05-14
1:N 后端管理 UI + 收口 (M3). ProjectSettings 的 ML 后端区落地 roadmap §3.4 形态: 表头加
已用 X / Y配额角标, 表格新增「能力」列展示 backendsupported_prompts, 「注册 backend」按钮在达上限时置灰 + tooltip. 新增MlBackendLimitModal, 在按钮被强行触发或表单创建撞409 ML_BACKEND_LIMIT_REACHED时弹出, 文案优先取服务器detail.message(兜底 fallback 保证离线可读). 沉淀两条 ADR (Prompt-first 重构 + 1:N 架构、Capability 协商协议) 与一篇管理员侧ml-backends.md. → plan · roadmap.
Added
- 配额角标:
MlBackendsSection标题旁显示已用 N / limit(limit 来自project.ml_backend_limit, 0 视为 ∞). - 能力列: 每行通过
useQueries拉/projects/{id}/ml-backends/{bid}/setup, 渲染supported_prompts为 Badge 组; 加载中显示…, 失败显示—. MlBackendLimitModal(apps/web/src/components/projects/MlBackendLimitModal.tsx): 「🚧 多后端共存暂未支持」专用模态, 文案优先服务器detail.message, 缺失时走前端 fallback.- ADR-0019
ML 工具 UI 的 Prompt-first 重构与 1:N 架构 (env 锁 1:1). - ADR-0020
ML Backend Capability 协商协议 (GET /setup JSON Schema 契约). - 管理员文档
docs-site/user-guide/for-project-admins/ml-backends.md: 注册 / 绑定 / 解绑流程 + 1:1 限制说明.
Changed
MlBackendFormModal.onSubmit: catch 块识别ApiError.status === 409且detailRaw.code === "ML_BACKEND_LIMIT_REACHED", 通过新 proponLimitReached上抛 (limit/current/message), 关闭表单切到 LimitModal; 其它错误仍走原有setError行内提示.- 「注册 backend」按钮: 达上限时
disabled同时 hover tooltip "已达上限 N, 请先解绑现有后端". - VitePress 侧边栏: 项目管理员组新增
ML 后端绑定入口.
[0.10.2] - 2026-05-14
Prompt-first ToolDock + Exemplar 入口 (M2). 把单 SAM 工具拆为 4 个独立工具 (智能点 / 智能框 / 文本提示 / Exemplar), 每个声明
requiredPrompt与 backend/setup.supported_prompts联动; backend 不支持的工具自动置灰 + tooltip 提示. 工具激活时右侧浮出AIToolDrawer(后端 + 工具特定控件 + 参数面板). 自研最小SchemaForm(~200 行, 不引@rjsf) 从/setup.params自动渲染 number/boolean/enum/string 控件, 用户可在工作台动态调box_threshold等参数, 透传到/interactive-annotating请求体. → plan · roadmap.
Added
- 4 个独立工具:
SmartPointTool/SmartBoxTool/TextPromptTool/ExemplarTool(apps/web/src/pages/Workbench/stage/tools/). 每个工具的CanvasTool.requiredPrompt声明所需 backend 能力 ("point" / "bbox" / "text" / "exemplar"). 旧SamTool删除,samSubTool状态由tool派生. useInteractiveAI.runExemplar()+ 各run*接受可选extraParams: AIToolDrawer 把 schema-form 参数 (box_threshold/text_threshold等) 通过extraParams注入到 context, 透传后端.AIToolDrawer(apps/web/src/pages/Workbench/shell/AIToolDrawer.tsx): AI 工具激活时显示, 含后端选择器 (1:1 阶段单项 disabled) + 工具特定控件 (smart-point 极性 / 文案提示) + Schema-form 参数面板 + 状态指示 (Healthy / 加载中 / 失败).SchemaForm(apps/web/src/pages/Workbench/components/SchemaForm/index.tsx): JSON Schema Draft-07 子集渲染 (number/integer slider, boolean checkbox, string enum dropdown, string text input). 包含deriveDefaults()辅助. 不依赖@rjsf/core, ~200 行自研.- 能力变化兜底: 当前激活的 AI 工具因 backend 切换 / 解绑而不再支持时, 自动切回
hand工具并 toast 提示. - E2E:
apps/web/e2e/tests/annotation.spec.ts新增两个用例 — grounded-sam2 capability 下 exemplar 置灰 + smart-point dispatch context.type="point"; sam3 capability 下 smart-point 置灰 + 拖框 exemplar dispatch context.type="exemplar". - SchemaForm 单测 (
SchemaForm.test.tsx): number/boolean/enum/deriveDefaults/空 schema 五个分支.
Changed
- ToolDock UI 重构 (
apps/web/src/pages/Workbench/shell/ToolDock.tsx): 删除 SAM 单工具特殊渲染分支, 改为统一requiredPrompt联动置灰 +aiToolDrawerslot. 分组顺序: 绘制工具 → AI 工具组 → 视图工具. S热键语义: 从「进入 SAM 并循环子工具」改为「在 4 个 AI 工具间循环, 跳过置灰的」.hotkeys.ts的setToolaction 新增"ai-cycle"元值;useWorkbenchHotkeys据useMLCapabilities.isPromptSupported决定下一个工具. Alt+2 改绑 polygon, Alt+3 改绑 ai-cycle.AIPredictionPopover文本面板: 渲染门控从tool === "sam" && samSubTool === "text"改为tool === "text-prompt".useWorkbenchState:Toolunion 移除"sam", 加入 4 个新工具 id.samSubTool改为useMemo派生 (公开 read-only), 移除setSamSubTool. 新增aiToolParams状态 +setAiToolParamssetter.onSamPrompt类型扩展: 新增{ kind: "exemplar"; bbox }, WorkbenchShell 据prompt.kind路由到runPoint/runBbox/runExemplar.
Removed
apps/web/src/pages/Workbench/stage/tools/SamTool.ts(拆为 4 个独立工具).apps/web/src/pages/Workbench/shell/SamSubToolbar.tsx(子工具栏功能迁移到 AIToolDrawer + ToolDock 主按钮).useWorkbenchState.setSamSubTool/nextSamSubTool(samSubTool 派生, 不再独立持有).
[0.10.1] - 2026-05-14
Capability 协商基础设施 (M1). 把 ML backend 的
/setup标准化为 JSON Schema 自描述协议, apps/api 暴露/projects/{id}/ml-backends/{bid}/setup代理端点, 前端落地useMLCapabilitieshook 作为 ML 能力的单一事实源. 同时落地MAX_ML_BACKENDS_PER_PROJECTenv (默认 1) 锁住运行时 1:1, DB/UI 一步到位 1:N. 本期 hook 只挂载、不消费; M2 (v0.10.2 Prompt-first ToolDock) 才接入消费. → plan · roadmap.
Added
/setupJSON Schema 自描述协议: 两个 ML backend (sam3-backend/grounded-sam2-backend) 的/setup响应一步到位标准化为新契约: 新增必填三元组name/version/model_version;params从配置快照 dict 改为 JSON Schema (Draft-07 子集), 每个字段携带type/default/title/enum/readOnly等元数据, 供 M2 schema-form 自动渲染参数面板.GET /projects/{id}/ml-backends/{bid}/setup代理端点 (apps/api/app/api/v1/ml_backends.py): 前端 useMLCapabilities 通过此端点拉 backend 能力, 不直连 ML backend; 30s TTL 进程内缓存避免 N 次探活, 删除/更新 backend 时自动 invalidate; 下游不可达返 502.MAX_ML_BACKENDS_PER_PROJECTenv (默认 1): 单项目可绑定的 ML backend 数量上限.POST /projects/{id}/ml-backends在已绑定数 ≥ 上限时返409 + detail{code:"ML_BACKEND_LIMIT_REACHED", message, limit, current}, 前端 M3 据此渲染「暂未支持多后端」Modal. DB schema 不变 (ml_backends.project_id 已允许多行), 应用层挡入口防显存爆炸.ProjectOut.ml_backend_limit:GET /projects/{id}响应体携带 env 控制的上限, 前端 ProjectSettings 据此决定「+ 添加后端」按钮的禁用状态 (M3).apps/web/src/pages/Workbench/state/useMLCapabilities.ts: TanStack Query hook, 5min staleTime; 暴露prompts/paramsSchema/capability/isPromptSupported(type)/isLoading/isError. 返回体缺supported_prompts时回落["point","bbox","text"]并 console.warn; 拉取失败时返回空 prompts (=禁用全 AI 工具). 配套 4 个单测覆盖成功 / 兜底 / 错误 / disabled 路径.- 后端单测 (
apps/api/tests/test_ml_backend_limit_and_setup.py): 超限场景 / 上限调大可绑 / ml_backend_limit 字段透出 / setup 代理含缓存 / 跨项目 backend_id 404 / 下游不可达 502, 共 6 例.
Changed
docs-site/dev/reference/ml-backend-protocol.md§4 重写:/setup从「可选自由 JSON」改为「v0.10.1 后必填 JSON Schema 协议」, 文档与两个 backend 同步..env.example+docs-site/dev/reference/env-vars.md: 新增MAX_ML_BACKENDS_PER_PROJECT段落.
Breaking Changes
/setup协议破坏式升级:params字段语义从「配置快照 dict (如{"box_threshold": 0.35})」改为「JSON Schema 对象 ({"type":"object","properties":{...}})」. 仅影响第三方 backend 实现; 平台内置的sam3-backend/grounded-sam2-backend同 PR 升级. 老 backend 缺supported_prompts时前端回落["point","bbox","text"]并控制台告警, 不阻断使用.
[0.10.0] - 2026-05-13
SAM 3 接入 M0 — sam3-backend 容器化 + exemplar 协议落地. v0.10.x 双 backend 并存策略的第一步: 把
facebookresearch/sam3(848M, 单档) +facebook/sam3.1权重打包成独立 GPU 服务, 与 grounded-sam2-backend 并存. 镜像 grounded-sam2-backend 结构, 复用apps/_shared/mask_utils共享包. 协议新增context.type="exemplar"(视觉示例 prompt → 全图相似实例), 仅 sam3-backend 支持; 前端 UI 入口与 apps/api 路由策略留 v0.10.1. → plan · roadmap.
Added
apps/sam3-backend/: 全新 ML Backend service, 基于pytorch/pytorch:2.7.0-cuda12.6-cudnn-devel, Python 3.12. 复用 grounded-sam2-backend 的 4 端点结构 (/health//setup//versions//predict//metrics//cache/stats), 监听 8002. 三种 prompt:bbox/text/exemplar(v0.10.0 选项 A: 不启用enable_inst_interactivity, 放弃 point, 让 grounded-sam2-backend 兜底单点交互). predictor.py 按 vendor commit 4cbac14 真实Sam3ProcessorAPI 实现, 不是基于 SAM 2 风格的假设.exemplarprompt 协议:Context.type="exemplar"+bbox=[x1,y1,x2,y2]视觉示例框 → SAM 3 PCS 一步出全图相似实例 polygons.docs-site/dev/reference/ml-backend-protocol.md§2.2 同步.- LRU embedding 缓存 (sam3.1): cap 默认 32 (env
SAM3_EMBEDDING_CACHE_SIZE覆盖); cache key 含sam3.1variant, 与 grounded-sam2 缓存互不污染 (embedding 来自不同模型, 不能跨). - Prometheus 指标
sam3_*前缀: 与 grounded-sam2-backend 的embedding_cache_*/inference_latency_seconds等同名指标解耦, 两个 backend 同时 scrape 不冲突. - docker-compose profile
gpu-sam3: 独立于 grounded-sam2 的gpuprofile, 用户可单独启动 sam3-backend 或两个都启 (docker compose --profile gpu --profile gpu-sam3 up). scripts/sync_vendor.sh+scripts/download_checkpoints.py: 镜像 grounded-sam2-backend 的 vendor 同步流程 + HF gated repo 拉权重 (要求HF_TOKEN, 否则 fail-fast).- Idle Unload + 懒重载: 与 grounded-sam2-backend 对齐.
SAM3_IDLE_UNLOAD_SECONDS(默认 600s, 0 关闭) 触发自动卸载释放显存; 下次/predict懒重载 (冷启动 ~8-12s, executor 异步加载不阻塞 event loop). 新增POST /unloadPOST /reload端点供运维显式控制;/health暴露idle_unload_seconds+last_request_age_seconds. 双 backend 并存场景下显存让渡的关键机制: sam3 FP16 ~7GB, 3090 单卡同时挂 grounded-sam2 (~2GB) + sam3 (~7GB) 必须靠 idle unload 互让.asyncio.Lock串行化并发懒加载避免双重构造 OOM; unload 时一并cache.clear()防 GPU 张量悬挂. env 命名SAM3_前缀, 与 grounded-sam2 的IDLE_UNLOAD_SECONDS解耦. - 45 个单测: schema (含 exemplar 校验) + embedding cache (sam3.1 variant 隔离 + cap=32 默认) + predictor mock 三种 prompt 路径 (bbox/text/exemplar) + cache miss/hit + reset_all_prompts 调用顺序 + idle unload 完整生命周期 (锁串行化 / 重载实例新建 / cache clear). 全部无 GPU 即可跑.
.env.example: 新增HF_TOKEN/SAM3_EMBEDDING_CACHE_SIZE/SAM3_SCORE_THRESHOLD/SAM3_LOG_LEVEL/SAM3_IDLE_UNLOAD_SECONDS/SAM3_IDLE_CHECK_INTERVAL占位.
Deferred (留给 v0.10.1+)
- 工作台
S工具 Shift+拖框 = exemplar 入口 → v0.10.1 - ProjectSettings 「默认 text backend」单选 + 优先级标签 → v0.10.1
- apps/api 按 prompt.type + 项目偏好的路由表 (
route_interactive_request()) → v0.10.1 - AB 对比工具 /
/ai-pre/compare页 → v0.10.2 - ADR-0012 双 backend 永久共存 /
deploy.md双拓扑章节 → v0.10.3