feat: 多项目并行监听 + 项目切换 UI(参考 codex app)(#35) - #63
Conversation
契约重塑(批次1,多项目并行监听基础):
- config/model.rs: 新增 Project struct(id/name/enabled + 11 per-project 字段);
AppConfig 改为 { projects, activeProjectId, +7 全局 webhook };validate 拆
validate_project + 顶层 validate(per-enabled 校验 + 拒重复 id/repo);重写
serde golden(projects/activeProjectId/Project camelCase)+ 首启 marker 改
default_has_no_projects。
- config/service.rs: 纯 migrate_value(detect-by-key:旧扁平→单 default 项目 +
webhook 提升;幂等);project(app,id) 查找;5 条迁移表驱动测试。
- events.rs: PrEvent/ReviewEvent 全 7 arm 加 project_id + golden 更新。
- src/types.ts / config/types.ts / fields.ts(+test) / projects.ts: TS 契约镜像,
PROJECT_GROUPS/GLOBAL_GROUPS 拆分 + useProjects 根模块。
注:跨切片下游(scheduler/ledger/registry/session/webhook/lib)尚未适配,
本 commit 单独不编译,后续批次补齐。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
批次2后端 + 批次3组装:
- pr/scheduler.rs: 单例 Scheduler → SchedulerSet(project_id→Arc<Scheduler> 映射 +
reconcile 起停/重配);ProjectDispatcher 载体(替代旧 Dispatcher,带 project_id);
per-project cycle 发 PrEvent{projectId} + 按项目派发。
- pr/commands.rs: discover/get_prs/set_pr_archived/poll_now 带 project_id;
start_polling/stop_polling/reschedule = reconcile/stop_all;gate_dispatchable 按项目;
start_webhook 从 enabled 项目构建 ProjectRoute 列表。
- pr/ledger.rs: 键 dispatched:{pid}/events:{pid} 分区 + LEDGER_WRITE_LOCK;dedup-key
形状不变。pr/registry.rs: 键 tracked:{pid},grace 取该项目 period。
- pr/webhook.rs: 单端点按 incoming repo 路由到匹配项目,payload_to_candidate→
(project_id, Candidate)。
- review/session.rs: reserved 按 (projectId,pr,kind),SessionInfo 加 project_id,
active_pairs(pid) 按项目过滤,pump 注入 projectId。engine.rs: CodexEngine 加
project_id。commands.rs: start_review(projectId,…) + 用 active_repo_root/
project_validated。
- config/service.rs: project_validated + active_repo_root + 惰性 set_active_project
helper。config/commands.rs + lib.rs: set_active_project 命令;run_auto_dispatch
(project_id) per-project engine/ledger/active;调度+webhook dispatcher 闭包带
project_id;state.rs scheduler → SchedulerSet。
后端全绿:build + 200 tests + fmt + clippy(-D warnings) 0 告警。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
批次2前端 + 批次3 App 组装: - pr/usePrStore.ts: prs/loading/error/polling/hasNewPr/snapshotLoaded 按 projectId 分区;参数化 …For(id) getter + active 包装(读 useProjects);subscribe 按 e.projectId 路由 + 非 active 项目 hasNewPr 标记;init 订阅在先 + loadSnapshot(active) 兜底;switchTo 懒加载。pr/api.ts 命令带 projectId(poll_now/get_prs/set_pr_archived)。 - pr/ProjectSwitcher.vue(新): 最左项目侧栏(名称 + 状态点 + PR 计数 + active 高亮), 点击 switchTo。PrList/PrRow/PollControls 读 active 项目 getter。 - review/useReviewStore.ts: dispatchError 按 projectId 分区;start(projectId,…); threadId 归属不变(assertNever 穷尽性保持)。review/api.ts startReview 带 projectId; types.ts ReviewSession 加 projectId;ReviewSessions 按 active 过滤;ReviewPanel start 传 active projectId。 - config: ProjectsManager.vue + ProjectCard.vue(新,增删编辑项目卡 + per-card authorsInput);SettingsView 左导加项目段(webhook 组仍全局);OnboardingWizard 单 Project draft(首项目 id=default)。projects.ts useProjects 根模块。 - App.vue: ProjectSwitcher 入 layout;selectedPr 作用域到 active 项目;切项目重置 selection;首启 gate 改 projects.length===0;config 存/onboarding 后 hydrate + per-project polling;dispatchError 按 active 项目分流。 前端全绿:vue-tsc + vite build + 106 vitest(含 slice-boundary)通过。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6 维 reviewer findings 的 small+IN_SCOPE 修复: - F1 [P1·安全] config/model.rs: project.id 字符集校验——对所有项目(含 disabled) 拒含 `:`/空白的 id(id 作 ledger/registry store-key 前缀,`:` 会污染分区→去重失效 →重复 review storm);加 validate 测试。Medium 载体(id 格式锁;Hard 路径 ProjectId newtype,future)。 - F2 [测试] pr/webhook.rs: 加「repo 不在任何路由→None」fail-closed 用例。 - F6 [DX] 单源化 "default":service.rs `MIGRATED_PROJECT_ID` 常量;新 src/config/ defaults.ts(DEFAULT_PROJECT_ID + NEW_PROJECT_DEFAULTS),OnboardingWizard + ProjectsManager 共用。 - F7 [DX] pr/registry.rs `project` → `project_snapshot`(消与 config::service::project 重名歧义)+ commands.rs 调用点。 - F8 [DX] config/api.ts 加 setActiveProject wrapper,projects.ts 改 import(命令名 集中,Soft→Medium)。 - F9 [架构] config::service `pub use Project` re-export;pr/scheduler.rs 经 service 依赖 Project(不直接 reach config::model)。 - F3 [UX] PrList 折叠态随切项目重置。F4 [UX] hydrateActiveSession 按 activeProjectId 过滤。F5 [UX] 删项目加确认。F11 [UX] loadSnapshot 置 loading 防空态闪现。F13 [UX] WebhookPanel 加「增删项目需重启隧道」静态提示。 - F10 [测试] session.rs SessionInfo golden 加 kind 断言。F12 [可靠性] 4 处并发/生命周期 假设补注释(ledger stale-read+registry 兜底 / stop project_id="" / codex spawn cwd / set_active 无锁 trade-off)。 全绿:cargo test 202 + fmt + clippy(-D warnings) 0;前端 vitest 106 + vue-tsc + vite build。 遗留 large(人工决策):broadcast ring 容量并发洪泛(见 PR pm:ship 评论)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🛠 ship review + fixreviewer 6(按 diff 4277 行 → 六维度各 1)· Findings 16(已修 small 13 · 遗留 large 1 · OOS/已文档化 2 + 数条 P3 延后)
完整详表(根因 + 证据 + 建议 + 方案种子,/fix 读此)L1 [P2·large·可靠性]
L2 [P2·large·UX]
A3 [P3·DX]
其余 P3 延后(非阻塞,低价值/可后续顺带):ProjectCard 项目名空值校验、ProjectsManager 重复 repo 的主动 inline 提示(后端 validate 已拒+save 报错)、store-key 前缀常量布局、usePrStore active-getter 样板、 全量门: 下一步:切 🤖 PR #63 · Generated with Claude Code · branch Feature/35-multi-project · worktree worktrees/Feature/35-multi-project |
✅ CI 收敛 — 全绿
run: https://github.com/ghbvf/prmonitor/actions/runs/27705438969 遗留 large finding L1(broadcast ring 并发洪泛)已按决策登 follow-up issue #64,本 PR 不改(非阻塞)。 状态: |
🔍 pr-review(六维度分级审查)根因簇 4 · Findings 6(P0 0·P1 1·P2 4·P3 1 | small 4·large 2)· 结论 需修复 根因簇
Findings(每条带 file:line,/fix 无损提取)
完整详表(证据 + 建议 + 根因 + 方案种子,/fix 读此)F1 [P1·large·架构/切片边界]
F2 [P2·small·安全/健壮]
F3 [P2·small·安全/健壮]
F4 [P2·small·安全/健壮]
F5 [P2·large·可靠性/生命周期/产品]
F6 [P3·small·产品/用户体验]
复杂度汇总:按根因簇 small 2 / large 2;按 Finding small 4 / large 2。 修复分流:small → 结论:需修复。PR 的多项目主路径方向成立,但当前跨切片契约、输入边界和 review 焦点隔离都有会影响后续演进或用户操作正确性的缺口。 🤖 PR #63 · Generated with Claude Code · branch Feature/35-multi-project · worktree /Users/shengming/Documents/code/prmonitor/worktrees/Feature/35-multi-project · session — |
/pr-review #63 最新一轮 small+IN_SCOPE findings: - F2 [P2·安全] config/model.rs: repo 去重按小写规范化(GitHub repo 大小写不敏感, webhook 已用 eq_ignore_ascii_case;Owner/Repo 与 owner/repo 视为同仓拒重)+ 测试。 - F3 [P2·安全] config/model.rs: projects 非空时校验 activeProjectId 命中现有项目 (悬空指针会 strand UI / 丢失 codex 握手 cwd);空 projects 保留首启语义 + 测试。 - F4 [P2·安全] review/commands.rs: start_review 命令边界白名单 kind ∈ {review,check} (session.rs 把非 check 一律当 review,未校验会以伪 key 跑 review)+ validate_kind 纯函数测试。 - F5 [P2·可靠性/产品] useReviewStore 加 clearFocus();App.vue watch(activeProjectId) 切项目时清焦点——焦点流是全局单例,否则 ReviewPanel 仍显示/可停止前一项目的 session(最小修;per-project 焦点分区彻底方案 deferred)。 - F6 [P3·产品] projects.ts setActive 改 persist-first(setActiveProject 成功后再更新 本地 activeProjectId),持久化失败不切走 UI + projects.test.ts 锁定。 遗留 F1 [P1·large·架构] Project 跨切片 re-export——不可自动执行,surface 人工决策。 全绿:cargo test 205 + fmt + clippy 0;前端 vitest 109 + vue-tsc + vite build。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🔁 fix(findings triage + fix)Findings 6(已修 small 5 · 遗留 large 1 · OUT_OF_SCOPE 0)· 来源
完整详表(triage 依据 + 证据 + 建议,下次 fix 读此)F2 [P2·small·安全]
F3 [P2·small·安全]
F4 [P2·small·安全]
F5 [P2·large·可靠性/产品]
F6 [P3·small·产品]
F1 [P1·large·架构]
下一步:切 🤖 PR #63 · Generated with Claude Code · branch Feature/35-multi-project · worktree worktrees/Feature/35-multi-project |
✅ CI 收敛 — 全绿(fix 轮)
run: https://github.com/ghbvf/prmonitor/actions/runs/27709055054
状态: |
🔍 pr-review(--check 修复验证)Findings 6(已修复 5·已转 follow-up 1·未修复 0·回归 0·部分 0)· 结论 通过 验证表
完整详表(验证证据 + 测试)F1 [P1·large·架构/切片边界]
F2 [P2·small·安全/健壮]
F3 [P2·small·安全/健壮]
F4 [P2·small·安全/健壮]
F5 [P2·large·可靠性/生命周期/产品]
F6 [P3·small·产品/用户体验]
测试
汇总:已修复 5 / 已转 follow-up 1 / 未修复 0 / 回归 0 / 部分 0。 结论:通过。F2-F6 的修复已验证通过,未看到对应站点回归;F1 是已决策转 follow-up 的 large 架构项,issue #65 持续跟踪,非本 PR 阻塞。 🤖 PR #63 · Generated with Claude Code · branch Feature/35-multi-project · worktree /Users/shengming/Documents/code/prmonitor/worktrees/Feature/35-multi-project · session — |
Summary
多项目并行监听 + 项目切换 UI:单项目硬结构 →
projects: [Project]+activeProjectId,N 个 enabled 项目各自独立 poll 循环 / ledger / review 会话并行互不阻塞,事件按projectId分流,codex-app 风格项目侧栏切换视图,旧单项目config.json自动迁移。Why / 背景
现状配置与监听是单项目硬结构(扁平
AppConfig单repo/repoRoot、单例Scheduler、全局ledger.json/prs.json/SessionRegistry、事件无projectId),用户监控多仓只能反复改配置重启、无法并行、UI 无多项目概念。这是 epic #33 中架构改动最大的一项。目标:可同时监控 ≥2 个仓库,每项目独立调度/去重/review 会话,UI 可切项目且视图随切。Refs
Closes #35(epic #33 子项)。计划见
~/.claude/plans/35-memoized-barto.md。对标本仓既有切片结构、trait seam(PrSource/ReviewEngine未改、仅按项目透传)、serde camelCase golden、assertNever穷尽性;项目侧栏对标 codex-app 多会话侧栏。Risk / 兼容性
AppConfig结构变形(projects[]+activeProjectId+7 全局 webhook 字段)、新增Project、PrEvent/ReviewEvent/SessionInfo全部加projectId。已同步更新所有 serde golden(config/model.rs、events.rs、review/session.rs)+src/types.ts/src/config/types.ts/src/review/types.ts,golden 测试锁 camelCase 形状。config.json经纯函数migrate_value(detect-by-key、幂等)自动升级为projects:[{id:"default",…}]+ 顶层 webhook 字段;{}→空projects触发 onboarding。5 条表驱动测试覆盖。#[serde(default)]前向兼容保留。get_prs/set_pr_archived/poll_now/start_review加projectId;start_polling/stop_polling/reschedule= reconcile/stop_all 全项目;新增set_active_project。ledger.json/prs.json按:{projectId}store-key 前缀分区(record 形状不变,dedup/tracked golden 未动);各保留单文件单全局写锁(整文件重写,跨项目同锁)。start_webhook时的 enabled 项目快照——运行中新增项目需手动重启 webhook 刷新路由(quick 隧道重启会换公网 URL,故不自动重启)。已在代码注释标注。Test plan
pnpm build本地通过(vue-tsc 类型检查 + vite build)cargo build --manifest-path src-tauri/Cargo.toml --locked本地通过cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets --locked -- -D warnings0 告警cargo fmt --manifest-path src-tauri/Cargo.toml --all -- --check干净cargo test --manifest-path src-tauri/Cargo.toml --locked通过(200 单测 + 3 集成;含迁移表驱动 / golden / session 按项目隔离 / ledger 按项目去重)usePrStore/useReviewStore分区、fields.test.ts、slice-boundary)🤖 Generated with Claude Code