From 9ba9876811c35e59b462284ca9971ea5b1b38655 Mon Sep 17 00:00:00 2001 From: Albert Date: Sun, 26 Apr 2026 12:36:01 +0800 Subject: [PATCH] feat(space): surface release attention read model --- docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md | 60 +++++++++ docs/architecture/ARCHITECTURE_INVENTORY.md | 4 +- docs/architecture/CODE_REFERENCE_INDEX.md | 8 +- frontend/src/api/space.ts | 89 ++++++++++++++ .../personal-space/PersonalSpacePage.test.tsx | 22 ++++ .../personal-space/PersonalSpacePage.tsx | 116 +++++++++++++++++- 6 files changed, 289 insertions(+), 10 deletions(-) diff --git a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md index 5f60d1ee..b387eaaa 100644 --- a/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md +++ b/docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md @@ -844,6 +844,36 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行 - ReleaseGrowth P1 只生成发布判断和下一步动作,不触发真实部署、不写 release validation run。 - 增长反馈、收入、ROI 和 compliance 的真实发布后反馈循环仍待后续 Packet 接入。 +### 2026-04-26 / space-p2-release-attention-readmodel / implemented + +完成内容: + +- 在 `frontend/src/api/space.ts` 新增 `releaseAttention` 只读聚合,从已有项目摘要、`truth-state` 与 `execution-summary.releaseGate` 派生个人空间发布关注项。 +- `/space` 桌面端和移动端新增“发布关注”区块,显示测试失败、PR 未合并、发布门禁阻塞、发布候选或需要人工确认的项目。 +- 发布关注项只展示 release gate 状态、阻塞原因、证据标签和更新时间;点击只跳转 `/release-growth?projectId=`。 +- 本轮不写 Project Memory、不写 ChangeSet、不写 ProgressEvent、不新增后端接口、不执行发布命令。 +- 暂存想法仍保持真实 empty/warning,不使用 mock 冒充。 + +涉及文件: + +- `frontend/src/api/space.ts` +- `frontend/src/features/personal-space/PersonalSpacePage.tsx` +- `frontend/src/features/personal-space/PersonalSpacePage.test.tsx` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` + +验证: + +- `cd frontend && npm run test -- src/features/personal-space/PersonalSpacePage.test.tsx` 通过:`2 passed`;仍有既有 React Router v7 future flag warning。 +- `cd frontend && npm run typecheck` 通过。 +- `cd frontend && npm run build` 通过;Vite 仍提示既有 manual chunk circular warning,但构建成功。 + +未完成或风险: + +- Space P2 依赖 Main Flow `release-growth-p1-execution-evidence-gate` 的 `execution-summary.releaseGate` 合同;最终合并需确保该合同先进入 integration base。 +- 本轮只做个人空间 read model,不做发布验证运行、真实部署、增长反馈或发布后指标聚合。 + ### 2026-04-26 / big-module-integration-p2 / done 完成内容: @@ -1033,6 +1063,36 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行 - 暂存想法真实只读聚合后端合同尚未实现;当前 Space 明确暴露为空态和 warning,不使用演示数据兜底。 +### 2026-04-26 / Account P2 Notification Preference Consumption / implemented + +完成内容: + +- `frontend/src/api/accountSettings.ts` 新增通知偏好消费模型,将 `pendingConfirmation / executionWriteback / releaseGate / failureAlert` 映射到 `待确认中心 / Execution Bridge / Release Growth / 全局失败提醒` 四类页面内提醒。 +- 新增 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,用于本地 UI 判断页面提醒显示或静默;不接入真实邮件、桌面推送或系统级通知中心。 +- 修正通知偏好合并逻辑:旧本机缓存或后端返回缺字段时,按默认偏好逐项补齐,避免浅合并导致 `desktop/emailDigest` 丢失。 +- `AccountSettingsPanel` 在通知偏好区域展示“页面内提醒消费说明”,明确哪些开关会影响待确认、执行回写、发布门禁和失败提醒,也明确桌面/邮件仅记录偏好、不代表系统级推送已接入。 +- 测试覆盖通知偏好读取、保存、消费模型和 fallback。 + +涉及文件: + +- `frontend/src/api/accountSettings.ts` +- `frontend/src/features/account-settings/AccountSettingsPanel.tsx` +- `frontend/src/features/account-settings/accountSettings.test.ts` +- `docs/architecture/ARCHITECTURE_INVENTORY.md` +- `docs/architecture/CODE_REFERENCE_INDEX.md` +- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` + +验证: + +- `cd frontend && npm run test -- src/features/account-settings/accountSettings.test.ts` 通过:`7 passed`。 +- `cd frontend && npm run typecheck` 通过。 +- `cd frontend && npm run build` 通过;Vite 仍输出既有 manual chunk circular warning,未阻断构建。 + +未完成或风险: + +- 本轮只在 Account Settings 合同和本地 UI 消费层落地通知偏好,不修改 PMFlow OS truth store、router、JWT/OAuth/auth core,也不新增后端表。 +- 由于本包 write scope 不包含 `ExecutionBridgePage / ReleaseGrowthPage / ChangeReviewPage`,本轮没有跨页直接接入这些页面;后续如要让各页面读取该 helper,应单独开对应 Main Flow/Integration packet。 + ### 2026-04-26 / Account P1 Preferences Backend Contract / implemented 完成内容: diff --git a/docs/architecture/ARCHITECTURE_INVENTORY.md b/docs/architecture/ARCHITECTURE_INVENTORY.md index 4d163833..4d57d370 100644 --- a/docs/architecture/ARCHITECTURE_INVENTORY.md +++ b/docs/architecture/ARCHITECTURE_INVENTORY.md @@ -194,9 +194,9 @@ - AI Steward 问答浮窗现分三层显性化:`FloatingQAChat.tsx` 负责即时回答/历史回放归一化,`QAConversationStatusBar.tsx` 展示对象/来源/仓库/执行通道/plan handoff,`QAResponseMetaCard.tsx` 展示 `fit_brief / external_recommendation / recommended_next_step`,对应前端回归由 `FloatingQAChat.test.tsx` 与 `QAResponseMetaCard.test.tsx` 锁定。 - 登录页与营销页仍共享 `frontend/src/components/marketing/` 的部分视觉基线;公开登录页现改由 `frontend/src/components/login/` 目录承载,其中 `LoginExperience.tsx` 负责登录壳层、`LoginWorkflowVisual.tsx` 负责左侧场景、`loginScene.ts` / `loginMath.ts` 拆出展示配置与路径几何、`login.css` 负责登录页专属样式边界,`frontend/src/pages/login/LoginPage.tsx` 继续负责真实鉴权、错误态和 redirect 提示;未登录访问 `/` 会经 `frontend/src/router/index.tsx` 直接重定向到 `/login`,无显式 `redirect` 的成功登录现在默认进入 `/command-center`。 - 前端主包治理已切到“同步首屏最小集 + route/page-section lazy”模式:`frontend/src/router/index.tsx` 保留 `LoginPage / Layout` 这组同步入口,其余后台重页面通过 `React.lazy + Suspense` 懒加载;统一 fallback 在 `frontend/src/components/layout/LazyBoundary.tsx`。P0 公共壳冻结后,大模块 route 归属为:`/space` 归 Space lane,`/project-launch` 是启动层入口,`/pmflow-os` 与 `/command-center / /change-review / /execution-bridge / /release-growth` 归 Main Flow lane,`/me` 归 Account Settings lane,`/settings` 当前仅为 reserved route、尚未实现,新增必须由 Shell / Integration owner 串行处理。`frontend/src/features/personal-space/` 作为空间层入口,通过 `/space` 提供可直接打开的本地预览入口,承载继续推进、待我确认、AI 正在推进、暂存想法、草案箱与新建项目入口。`frontend/src/features/project-launch/` 作为首次进入页 / 新项目启动台产品域,通过 `/project-launch` 提供可直接打开的入口;Phase B 后默认调用真实 `pmflow-launch` draft/confirm API,创建成功态主动作跳转后端返回的 `/pmflow-os?projectId=`,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用本地 mock 状态机。`frontend/src/features/account-settings/` 作为 Account Settings 产品域,通过 `/me` 提供个人资料、默认语言、AI 回答深度、证据展示偏好、待确认/执行回写/发布门禁/失败提醒通知偏好和账户操作入口;该域通过 `frontend/src/api/accountSettings.ts` 建立前端合同,有 token 时优先调用已挂入 `backend/app/api/router.py` 的 `backend/app/api/account_settings_api/` `GET /account-settings/me` / `PUT /account-settings/me` 同步当前账号偏好,后端不可用时明确回落本机暂存,不修改 OAuth/JWT/auth core。`frontend/src/features/pmflow-os/` 作为新产品域,通过 `PMFlowOsProvider` 在应用根部挂载共享 truth store,并提供 `CommandCenter / ChangeReview / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主入口;当前 PMFlow OS 页面已切到专属轻壳,桌面端默认隐藏旧 TopBar,并由 `frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx` 统一承接“左侧稳定导航 + 顶部上下文条 + 中央视图 + 右侧 AI 副驾”的全局骨架,同时承接项目内 `个人空间 / 当前项目名` 面包屑和项目切换器;账号退出登录由 `frontend/src/features/pmflow-os/components/AccountControls.tsx` 统一承接桌面头像下拉、退出确认与移动端 bottom sheet,`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 提供 `/me` 入口并委托 Account Settings 域渲染设置页;正式应用壳移动端底部导航统一由 `frontend/src/features/pmflow-os/components/MobileBottomNav.tsx` 承接,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`,其中 `动态` 当前仍是 `/command-center` 占位。`/command-center` 额外提供免登录产品预览壳,`/pmflow-os` 是真实项目上下文 alias。主视觉按 `#2463EB + 白色` 的 Linear-like 高密度原型展开。`UsersPage` 把创建/删除/重置/导入/批量分配对话框拆为按需 chunk,`UserSkillsPage` 与 `WorkloadDetailPage` 的图表也分别拆到了 `frontend/src/pages/user-skills/components/UserSkillsRadarPanel.tsx` 与 `frontend/src/pages/workload-detail/components/WorkloadTrendChart.tsx`。 -- `frontend/src/features/personal-space/PersonalSpacePage.tsx` 是空间首页视图层,`frontend/src/api/space.ts` 是空间工作面只读聚合 API:从 `projects` summary、PMFlow OS `truth-state` 与 `execution-summary` 生成继续推进、待确认、执行中 / AI 正在推进、暂存想法、草案箱、最近项目摘要;项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,执行中项进入 `/execution-bridge?projectId=`;暂存想法后端只读聚合尚未接入时只显示 empty/warning,不使用 mock 数据冒充真实内容;移动端为空间首页独立 App 布局,不直接压缩桌面卡片,并保留最近项目区块;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间和主流程导航,不做团队空间强切换、企业组织管理、Project Memory 写入、ChangeSet 状态写入或 execution event 写入。 +- `frontend/src/features/personal-space/PersonalSpacePage.tsx` 是空间首页视图层,`frontend/src/api/space.ts` 是空间工作面只读聚合 API:从 `projects` summary、PMFlow OS `truth-state` 与 `execution-summary` 生成继续推进、待确认、发布关注、执行中 / AI 正在推进、暂存想法、草案箱、最近项目摘要;Space P2 的 `releaseAttention` 只消费 `execution-summary.releaseGate`,把测试失败、PR 未合并、发布门禁阻塞或可发布候选显示到 `/space`,项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,发布关注项进入 `/release-growth?projectId=`,执行中项进入 `/execution-bridge?projectId=`;暂存想法后端只读聚合尚未接入时只显示 empty/warning,不使用 mock 数据冒充真实内容;移动端为空间首页独立 App 布局,不直接压缩桌面卡片,并保留最近项目区块;`frontend/src/pages/PersonalSpacePage.tsx` 是薄入口。该域只表达个人空间和主流程导航,不做团队空间强切换、企业组织管理、Project Memory 写入、ChangeSet 状态写入、ProgressEvent 写入或发布执行。 - `frontend/src/features/project-launch/types.ts`、`mockData.ts`、`useProjectLaunchState.ts` 是新项目启动台状态层;默认真实模式通过 `frontend/src/api/pmflowLaunch.ts` 生成草案并确认创建真实项目,API 失败进入明确失败态,不静默 fallback 成 mock 成功;`mockData.ts` 仅在显式 `?demo=1` 或 `?dataSource=demo` 下作为演示状态机使用。`components/LaunchShell.tsx` 负责桌面/移动分流,桌面端复用 `frontend/src/features/personal-space/components/SpaceChrome.tsx` 的空间左侧栏和顶部条,`LaunchCanvases.tsx` 和 `MobileLaunchLayout.tsx` 承接高保真图中的意图输入、AI 工作、人类确认、草案修改、生成失败、创建成功过渡与移动端 bottom sheet 交互。 -- `frontend/src/features/account-settings/AccountSettingsPanel.tsx` 是 `/me` 设置页实现:桌面端为个人资料摘要 + 分区设置面板,移动端为分组列表 + `MobileBottomSheet` 编辑;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好。`frontend/src/api/accountSettings.ts` 是前端合同层;有 token 时优先调用 `backend/app/api/account_settings_api/`,失败时回落浏览器本机暂存。`backend/app/schemas/account_settings.py` 定义账号偏好合同,`backend/app/services/account_settings_service.py` 当前复用既有 `integrations` 表按 `tenant_id + user_id` 保存 `account_settings_preferences` 配置包,`backend/app/api/router.py` 已挂载该 API;未改用户表、OAuth/JWT/auth core 或 PMFlow OS truth store。 +- `frontend/src/features/account-settings/AccountSettingsPanel.tsx` 是 `/me` 设置页实现:桌面端为个人资料摘要 + 分区设置面板,移动端为分组列表 + `MobileBottomSheet` 编辑;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,并在通知偏好区展示页面内提醒消费说明。`frontend/src/api/accountSettings.ts` 是前端合同层;有 token 时优先调用 `backend/app/api/account_settings_api/`,失败时回落浏览器本机暂存;P2 新增 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,将四类通知偏好映射为本地 UI 的待确认、Execution Bridge、Release/Growth 和失败提醒显示/静默状态,不代表邮件、桌面推送或系统通知中心已接入。`backend/app/schemas/account_settings.py` 定义账号偏好合同,`backend/app/services/account_settings_service.py` 当前复用既有 `integrations` 表按 `tenant_id + user_id` 保存 `account_settings_preferences` 配置包,`backend/app/api/router.py` 已挂载该 API;未改用户表、OAuth/JWT/auth core 或 PMFlow OS truth store。 - `frontend/src/features/pmflow-os/store.tsx` 当前是 PMFlow OS 共享 truth store:`?projectId=` 时通过 React Query 调用 `frontend/src/api/pmflowOs.ts` 读取真实 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,真实模式下项目内 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume 与 execution progress writeback 均调用 `/pmflow-os` 后端并 refetch;只有显式 `?demo=1` 或 `?dataSource=demo` 时才启用本地 mock 演示状态机。真实读取/动作失败会进入错误/空态,不静默 fallback 成 mock 成功。 - `frontend/src/features/pmflow-os/store.tsx` 仍保留演示用项目内 AI 结构化闭环状态机:`command_idle / ai_extracting / extraction_ready / review_open / writeback_success`。`frontend/src/features/pmflow-os/pages/StructuredExtractionPage.tsx` 是本轮结构化提取页,`ChangeReviewPage.tsx` 是决策与待确认中心,`MemoryWritebackResultPage.tsx` 是项目记忆写入结果页,`WorkGraphPage.tsx` 是项目工作台关系总览与对象投影视图,`ExecutionBridgePage.tsx` 是执行桥上下文包、交接按钮、运行端领取确认、PR/测试与回写状态页;`ReleaseGrowthPage.tsx` 真实模式读取 `execution-summary.releaseGate`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因和下一步动作,不执行发布命令、不展示 mock 发布结果;静态发布/增长原型只保留在显式 demo 模式。A3 已接入后端 truth-state,C1 已接入真实 message/extraction/review 状态机,C2 已接入真实 ChangeSet commit,Phase D 已接入真实 execution-summary/dev-sync,D.2 已接入 `POST /execution-handoffs` 创建 PromptPackage + handoff_created progress event,P1 已接入 `POST /execution-handoffs/{prompt_package_id}/consume` 创建 handoff_consumed progress event 并把 execution status 投影为 coding,Release Growth P1 已接入 execution evidence gate;当前仍不调用真实 AI 提取,不把 mock 当真实失败 fallback。`ProjectMemoryPage.tsx` 在真实模式下展示后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx` 提供 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel` 等闭环组件,并复用契约化 `InlineNotice` 表达“未确认不得写入”的局部提示。 - PMFlow OS 后端入口位于 `backend/app/api/pmflow_os_api/`,projection service 位于 `backend/app/services/pmflow_os_projection_service.py`,Change Review / commit 服务位于 `backend/app/services/pmflow_change_review_service.py`,Pydantic 合同位于 `backend/app/schemas/pmflow_os.py`;当前聚合 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,`Thread / Message` 来自 `pmflow_threads / pmflow_thread_messages`,`ChangeSet / ChangeItem / Approval / EvidenceLink` 来自 `backend/app/models/pmflow_change_review.py` 对应真实表。C1 已实现 message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend/frontend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit` 与真实模式前端接入,默认 atomic,将 `Requirement / Decision / Risk` 写入 `truth_nodes`,将 `Task` 写入 `truth_nodes(node_type='task') + tasks`,高风险项强制 approval gate;D.2 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs`,校验 task/repo project 边界,生成/复用 `PromptPackage(package_kind='execution_handoff')` 并写 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,校验 execution handoff package 后更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并写 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 在 `ExecutionSummaryProjection.releaseGate` 中从 `ProgressEvent` 推导发布门禁状态,失败事件 fail-closed 阻塞发布,无证据返回 empty/warning;这些步骤都不直接执行本机命令或真实发布命令;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。 diff --git a/docs/architecture/CODE_REFERENCE_INDEX.md b/docs/architecture/CODE_REFERENCE_INDEX.md index 35f022db..4c9cca13 100644 --- a/docs/architecture/CODE_REFERENCE_INDEX.md +++ b/docs/architecture/CODE_REFERENCE_INDEX.md @@ -97,22 +97,22 @@ - PMFlow UI 契约真源:先读 `docs/PMFLOW_UI_CONTRACT.md`,再修改 `project-launch`、`personal-space`、`pmflow-os` 相关页面;该文档锁定 Logo、色彩、移动端底部导航、状态标签、AI 操作/用户动作/待确认视觉语法和核心功能 canonical 契约 - PMFlow 大模块并行公共壳:参考 `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md` 的 `4.4.1-4.4.3` 与 `frontend/src/router/index.tsx`、`frontend/src/main.tsx`、`frontend/src/features/pmflow-os/components/MobileBottomNav.tsx`、`frontend/src/features/pmflow-os/components/PMFlowOsFrame.tsx`;`/space` 归 Space lane,`/pmflow-os / /command-center / /change-review / /execution-bridge / /release-growth` 归 Main Flow lane,`/me` 归 Account Settings lane,`/settings` 当前仅为 reserved route,新增或改动必须由 Shell / Integration owner 串行处理 - PMFlow OS 产品合同真源:先读 `docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md`,再实现 `pmflow-os` 真实后端接入;该文档锁定 `ProjectTruthState / Thread / ChangeSet / ChangeItem / Evidence / Approval / Project Memory / ExecutionContext` 与 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync` 的复用边界和后续 API 草案 -- 新增前端 API 调用:参考 `frontend/src/api/sstc.ts`;Space 工作面只读聚合参考 `frontend/src/api/space.ts`,只消费项目摘要与 PMFlow OS `truth-state / execution-summary`,不写 Project Memory 或 execution event;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowExecutionProgressEvent`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同;Project Launch 创建链参考 `frontend/src/api/pmflowLaunch.ts` 的 `createPMFlowLaunchDraft` 与 `confirmPMFlowLaunchDraft` +- 新增前端 API 调用:参考 `frontend/src/api/sstc.ts`;Space 工作面只读聚合参考 `frontend/src/api/space.ts`,只消费项目摘要与 PMFlow OS `truth-state / execution-summary`,不写 Project Memory、ChangeSet 状态或 execution event;Space P2 已从 `PMFlowExecutionSummary.releaseGate` 派生 `releaseAttention`,用于在 `/space` 显示测试失败、PR 未合并、发布门禁阻塞或可发布候选,所有动作只跳转 `/release-growth?projectId=`;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowExecutionProgressEvent`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同;Project Launch 创建链参考 `frontend/src/api/pmflowLaunch.ts` 的 `createPMFlowLaunchDraft` 与 `confirmPMFlowLaunchDraft` - AI Steward 页面入口:参考 `frontend/src/pages/ai-steward/` - AI Steward 问答浮窗与上下文显性化组件:参考 `frontend/src/components/ai-steward/FloatingQAChat.tsx`、`frontend/src/components/ai-steward/QAConversationStatusBar.tsx`、`frontend/src/components/ai-steward/QAResponseMetaCard.tsx` - AI Steward QA contract 前端类型(含 `analysis_packet`):参考 `frontend/src/api/aiSteward.ts` - AI Steward handoff 元卡片回归:参考 `frontend/src/components/ai-steward/FloatingQAChat.test.tsx`、`frontend/src/components/ai-steward/QAResponseMetaCard.test.tsx` - AI Steward / QA session 前端 contract:参考 `frontend/src/api/aiSteward.ts` - PMFlow OS 前端 MVP 产品域:参考 `frontend/src/features/pmflow-os/` -- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;本地预览路由入口为 `/space`,覆盖继续推进、待我确认、执行中 / AI 正在推进、暂存想法、草案箱与最近项目,点击项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,执行中项进入 `/execution-bridge?projectId=`;Space 只读聚合项目摘要、PMFlow OS `truth-state / execution-summary`,不直接写 Project Memory、ChangeSet 状态或 execution event;暂存想法后端只读聚合尚未接入时必须显示 empty/warning,不得用 mock 数据冒充真实内容;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该组件 +- 个人空间首页 / 空间层入口前端产品域:参考 `frontend/src/features/personal-space/`、`frontend/src/api/space.ts` 与薄入口 `frontend/src/pages/PersonalSpacePage.tsx`;本地预览路由入口为 `/space`,覆盖继续推进、待我确认、发布关注、执行中 / AI 正在推进、暂存想法、草案箱与最近项目,点击项目主动作进入 `/pmflow-os?projectId=`,待确认项进入 `/change-review?projectId=&changeSetId=`,执行中项进入 `/execution-bridge?projectId=`,发布关注项进入 `/release-growth?projectId=`;Space 只读聚合项目摘要、PMFlow OS `truth-state / execution-summary.releaseGate`,不直接写 Project Memory、ChangeSet 状态或 execution event;暂存想法后端只读聚合尚未接入时必须显示 empty/warning,不得用 mock 数据冒充真实内容;空间层共享左侧栏、顶部条与空间用量组件在 `frontend/src/features/personal-space/components/SpaceChrome.tsx`,`/project-launch` 桌面首次新建壳也复用该组件 - 新项目启动台 / 首次进入页前端产品域:参考 `frontend/src/features/project-launch/` 与薄入口 `frontend/src/pages/ProjectLaunchPage.tsx`;真实模式默认通过 `frontend/src/api/pmflowLaunch.ts` 调用后端 draft/confirm API,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用 `mockData.ts` 本地演示状态机;本地预览路由入口为 `/project-launch` -- Account Settings / `/me` 前端产品域与 P1 后端同步合同:参考 `frontend/src/features/account-settings/AccountSettingsPanel.tsx`、`frontend/src/features/pmflow-os/pages/AccountPage.tsx`、`frontend/src/api/accountSettings.ts`、`backend/app/api/account_settings_api/`、`backend/app/schemas/account_settings.py` 与 `backend/app/services/account_settings_service.py`;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,移动端使用分组列表与 bottom sheet,账户退出仍复用 `AccountControls.tsx`,不改 OAuth/JWT/auth core;P1 后端合同提供 `GET /account-settings/me` 与 `PUT /account-settings/me`,已由 `backend/app/api/router.py` 挂入核心路由组,按当前 `tenant_id + user_id` 保存账号偏好,前端有 token 时优先账号同步,后端不可用时明确回落 `仅本机暂存 / 保存失败`,不得假装云端账号保存 +- Account Settings / `/me` 前端产品域与 P1/P2 偏好合同:参考 `frontend/src/features/account-settings/AccountSettingsPanel.tsx`、`frontend/src/features/pmflow-os/pages/AccountPage.tsx`、`frontend/src/api/accountSettings.ts`、`backend/app/api/account_settings_api/`、`backend/app/schemas/account_settings.py` 与 `backend/app/services/account_settings_service.py`;当前覆盖个人资料、默认语言、AI 回答深度、证据展示偏好、待确认、执行回写、发布门禁和失败提醒通知偏好,移动端使用分组列表与 bottom sheet,账户退出仍复用 `AccountControls.tsx`,不改 OAuth/JWT/auth core;P1 后端合同提供 `GET /account-settings/me` 与 `PUT /account-settings/me`,已由 `backend/app/api/router.py` 挂入核心路由组,按当前 `tenant_id + user_id` 保存账号偏好,前端有 token 时优先账号同步,后端不可用时明确回落 `仅本机暂存 / 保存失败`;P2 在 `frontend/src/api/accountSettings.ts` 提供 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,把通知偏好映射为页面内提醒消费模型,只控制本地 UI 的待确认、执行回写、发布门禁和失败提醒展示,不代表真实邮件/桌面推送已接入 - PMFlow OS 共享 truth store 与主链状态动作:参考 `frontend/src/features/pmflow-os/store.tsx`;当前支持 `?projectId=` 走 React Query 读取真实 `truth-state` 与 `execution-summary`,真实模式下 thread message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet commit、Execution Handoff create/consume、execution progress writeback 均调后端并 refetch,不本地 promote 入 Project Memory 或伪造 execution event;`?demo=1` 才启用本地 mock 演示状态机和本地动作;项目内 AI 结构化闭环状态机为 `command_idle / ai_extracting / extraction_ready / review_open / writeback_success` - PMFlow OS 产品页入口:参考 `frontend/src/features/pmflow-os/pages/CommandCenterPage.tsx`、`StructuredExtractionPage.tsx`、`ChangeReviewPage.tsx`、`MemoryWritebackResultPage.tsx`、`WorkGraphPage.tsx`、`ExecutionBridgePage.tsx`、`ReleaseGrowthPage.tsx`、`TeamLayerPage.tsx`、`ProjectMemoryPage.tsx`、`AccountPage.tsx`;`ReleaseGrowthPage.tsx` 真实模式只展示 `execution-summary.releaseGate`、execution evidence、阻塞原因和下一步动作,缺证据时显示 empty/warning,不展示 mock 发布结果,静态高保真发布/增长数据仅保留在显式 demo 模式 - PMFlow OS 行为组件:参考 `frontend/src/features/pmflow-os/components/ProjectStructuredLoop.tsx`、`ProjectStageRail.tsx`、`ThreadComposer.tsx`、`ExtractionRail.tsx`、`ChangeGroupPanel.tsx`、`ObjectGraphSection.tsx`、`ContextPacketPanel.tsx`、`ExecutionTimeline.tsx`、`ReleaseGatePanel.tsx`、`TeamFeedPanel.tsx`、`ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx`、`MobileBottomNav.tsx`;其中 `ProjectStructuredLoop.tsx` 承接 `StatusLabel / EvidenceSnippet / ObjectChangeCard / ProjectConversationPanel / StructuredExtractionPanel / ProjectAICopilotPanel`,`PMFlowOsFrame.tsx` 承接项目内面包屑和项目切换器,`AccountControls.tsx` 承接桌面头像菜单、退出登录确认与移动端 bottom sheet,`FeedbackPrimitives.tsx` 承接 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet`,`MobileBottomNav.tsx` 承接正式应用壳移动端固定底部导航 - PMFlow OS 原型状态回归测试:参考 `frontend/src/features/pmflow-os/store.test.ts`;真实 API 动作回归参考 `frontend/src/features/pmflow-os/store.api.test.tsx` - Project Launch 真实/演示分支回归测试:参考 `frontend/src/features/project-launch/useProjectLaunchState.test.ts` -- Account Settings 前端合同回归测试:参考 `frontend/src/features/account-settings/accountSettings.test.ts` +- Account Settings 前端合同回归测试:参考 `frontend/src/features/account-settings/accountSettings.test.ts`,覆盖默认偏好、账号同步 fallback、通知偏好消费模型和缺字段 fallback。 - 网页问答显式锚点与 AI Steward URL 构造:参考 `frontend/src/components/ai-steward/qaPageAnchor.ts` - Inbox 页面入口:参考 `frontend/src/pages/inbox/` - Inbox `plan_mode` 展开卡片与映射:参考 `frontend/src/components/inbox/InboxListItem.tsx`、`frontend/src/lib/inbox.ts`、`frontend/src/api/aiPmo.ts` diff --git a/frontend/src/api/space.ts b/frontend/src/api/space.ts index b48828e1..982a80ff 100644 --- a/frontend/src/api/space.ts +++ b/frontend/src/api/space.ts @@ -47,6 +47,20 @@ export interface SpaceAIProgressItem { href: string; } +export interface SpaceReleaseAttentionItem { + id: string; + projectId: string; + projectTitle: string; + title: string; + summary: string; + gateStatus: "ready" | "blocked" | "needs_confirmation"; + severity: "low" | "medium" | "high"; + reasons: string[]; + evidenceLabels: string[]; + updatedAt: string; + href: string; +} + export interface SpaceDraftItem { id: string; projectId: string; @@ -71,6 +85,7 @@ export interface SpaceWorkbench { projects: SpaceWorkbenchProject[]; pendingConfirmations: SpacePendingConfirmation[]; aiProgress: SpaceAIProgressItem[]; + releaseAttention: SpaceReleaseAttentionItem[]; savedIdeas: SpaceSavedIdea[]; drafts: SpaceDraftItem[]; recentProjects: SpaceWorkbenchProject[]; @@ -169,6 +184,10 @@ function executionHref(projectId: string): string { return `/execution-bridge?projectId=${encodeURIComponent(projectId)}`; } +function releaseGrowthHref(projectId: string): string { + return `/release-growth?projectId=${encodeURIComponent(projectId)}`; +} + function pendingChangeItems(truth: ProjectTruthState | null): ChangeItem[] { if (!truth) return []; return truth.changeItems.filter((item) => item.status === "pending_review"); @@ -316,6 +335,75 @@ function deriveAIProgress(snapshot: ProjectSnapshot): SpaceAIProgressItem[] { return items; } +function releaseAttentionTitle(status: SpaceReleaseAttentionItem["gateStatus"]): string { + if (status === "blocked") return "发布门禁阻塞"; + if (status === "ready") return "可发布候选待确认"; + return "发布需要人工确认"; +} + +function releaseAttentionSeverity(status: SpaceReleaseAttentionItem["gateStatus"]): SpaceReleaseAttentionItem["severity"] { + if (status === "blocked") return "high"; + if (status === "ready") return "low"; + return "medium"; +} + +function releaseEvidenceLabel(type: string): string { + const labels: Record = { + branch_created: "分支", + commit: "提交", + pr_opened: "PR", + pr_synced: "PR 同步", + test_passed: "测试通过", + checks_passed: "检查通过", + test_failed: "测试失败", + failed: "失败事件", + blocked: "阻塞事件", + merged: "已合并", + accepted: "已确认", + }; + return labels[type] || type; +} + +function releaseAttentionReasons(execution: PMFlowExecutionSummary): string[] { + const gate = execution.releaseGate; + const checkReasons = gate.checks + .filter((check) => check.status !== "passed") + .map((check) => `${check.label}: ${check.detail || (check.status === "failed" ? "未通过" : "缺少证据")}`); + return [...gate.blockingReasons, ...checkReasons].filter(Boolean).slice(0, 4); +} + +function deriveReleaseAttention(snapshot: ProjectSnapshot): SpaceReleaseAttentionItem[] { + const { project, truth, execution } = snapshot; + const gate = execution?.releaseGate; + if (!gate || gate.status === "empty") return []; + + const projectId = String(project.id); + const gateStatus = gate.status; + const projectTitle = project.title || truth?.project.name || `项目 ${projectId}`; + const reasons = releaseAttentionReasons(execution); + const evidenceLabels = Array.from(new Set(gate.evidence.map((item) => releaseEvidenceLabel(item.type)))).slice(0, 5); + const fallbackSummary = + gateStatus === "blocked" + ? "执行证据显示发布门禁被阻塞,需要回到发布增长页查看失败事件。" + : "执行证据已进入发布判断,需要人工确认后再推进。"; + + return [ + { + id: `release-attention-${projectId}`, + projectId, + projectTitle, + title: gate.title || releaseAttentionTitle(gateStatus), + summary: gate.summary || fallbackSummary, + gateStatus, + severity: releaseAttentionSeverity(gateStatus), + reasons, + evidenceLabels, + updatedAt: gate.updatedAt || project.updated_at, + href: releaseGrowthHref(projectId), + }, + ]; +} + function deriveDrafts(snapshot: ProjectSnapshot): SpaceDraftItem[] { const { project, truth } = snapshot; if (!truth) return []; @@ -382,6 +470,7 @@ export const spaceApi = { projects: workbenchProjects.slice(0, 4), pendingConfirmations: sortByDate(snapshots.flatMap(derivePending)).slice(0, 8), aiProgress: sortByDate(snapshots.flatMap(deriveAIProgress)).slice(0, 8), + releaseAttention: sortByDate(snapshots.flatMap(deriveReleaseAttention)).slice(0, 8), savedIdeas: [], drafts: sortByDate(snapshots.flatMap(deriveDrafts)).slice(0, 8), recentProjects: sortByDate(workbenchProjects), diff --git a/frontend/src/features/personal-space/PersonalSpacePage.test.tsx b/frontend/src/features/personal-space/PersonalSpacePage.test.tsx index f522c946..b4802446 100644 --- a/frontend/src/features/personal-space/PersonalSpacePage.test.tsx +++ b/frontend/src/features/personal-space/PersonalSpacePage.test.tsx @@ -22,6 +22,7 @@ const emptyWorkbench: SpaceWorkbench = { projects: [], pendingConfirmations: [], aiProgress: [], + releaseAttention: [], savedIdeas: [], drafts: [], recentProjects: [], @@ -39,6 +40,7 @@ describe("PersonalSpacePage", () => { expect(screen.getAllByText("个人工作面")).not.toHaveLength(0); expect(screen.getAllByText("继续推进")).not.toHaveLength(0); expect(screen.getAllByText("待我确认")).not.toHaveLength(0); + expect(screen.getAllByText("发布关注")).not.toHaveLength(0); expect(screen.getAllByText("AI 正在推进")).not.toHaveLength(0); expect(screen.getAllByText("暂存想法")).not.toHaveLength(0); expect(screen.getAllByText("草案箱")).not.toHaveLength(0); @@ -94,6 +96,21 @@ describe("PersonalSpacePage", () => { href: "/execution-bridge?projectId=17", }, ], + releaseAttention: [ + { + id: "release-attention-17", + projectId: "17", + projectTitle: "真实项目", + title: "发布门禁阻塞", + summary: "测试失败,PR 未合并,不能发布。", + gateStatus: "blocked", + severity: "high", + reasons: ["测试失败: typecheck 未通过", "PR: 未合并"], + evidenceLabels: ["PR", "测试失败"], + updatedAt: "2026-04-26T08:00:00.000Z", + href: "/release-growth?projectId=17", + }, + ], drafts: [ { id: "17-需求-draft-1", @@ -139,5 +156,10 @@ describe("PersonalSpacePage", () => { "href", "/execution-bridge?projectId=17" ); + expect(screen.getAllByRole("link", { name: /发布门禁阻塞/ })[0]).toHaveAttribute( + "href", + "/release-growth?projectId=17" + ); + expect(screen.getAllByText("测试失败: typecheck 未通过")).not.toHaveLength(0); }); }); diff --git a/frontend/src/features/personal-space/PersonalSpacePage.tsx b/frontend/src/features/personal-space/PersonalSpacePage.tsx index e626ff21..68ce1f01 100644 --- a/frontend/src/features/personal-space/PersonalSpacePage.tsx +++ b/frontend/src/features/personal-space/PersonalSpacePage.tsx @@ -29,6 +29,7 @@ import { type SpaceAIProgressItem, type SpaceDraftItem, type SpacePendingConfirmation, + type SpaceReleaseAttentionItem, type SpaceSavedIdea, type SpaceWorkbenchProject, } from "@/api/space"; @@ -105,6 +106,7 @@ function DesktopSpace({ projectCount={data?.recentProjects.length ?? 0} pendingCount={data?.pendingConfirmations.length ?? 0} aiCount={data?.aiProgress.length ?? 0} + releaseCount={data?.releaseAttention.length ?? 0} /> {isLoading ? : error ? : null} @@ -137,6 +139,8 @@ function DesktopSpace({ + +
@@ -195,8 +199,8 @@ function MobileSpace({
+ -
@@ -228,6 +232,16 @@ function MobileSpace({ + +
+ {data.releaseAttention.length ? ( + data.releaseAttention.slice(0, 4).map((item) => ) + ) : ( + + )} +
+
+
{data.aiProgress.length ? ( @@ -282,12 +296,14 @@ function WorkbenchHeader({ projectCount, pendingCount, aiCount, + releaseCount, }: { generatedAt?: string; isFetching: boolean; projectCount: number; pendingCount: number; aiCount: number; + releaseCount: number; }) { return (
@@ -302,9 +318,10 @@ function WorkbenchHeader({ 聚合继续推进、待确认、执行中、暂存想法、草案箱和最近项目;点击项目后回到主流程处理。

-
+
+
@@ -413,6 +430,49 @@ function AIProgressRail({ items }: { items: SpaceAIProgressItem[] }) { ); } +function ReleaseAttentionPanel({ items }: { items: SpaceReleaseAttentionItem[] }) { + return ( +
+ +
+ {items.length ? ( + items.slice(0, 5).map((item) => ) + ) : ( + + )} +
+
+ ); +} + +function ReleaseAttentionRow({ item }: { item: SpaceReleaseAttentionItem }) { + const tone = releaseAttentionTone(item); + return ( + + + + + + + {item.title} + {releaseGateLabel(item.gateStatus)} + + {item.summary} + + {item.projectTitle} · {item.evidenceLabels.length ? item.evidenceLabels.join(" / ") : "缺少可用证据"} · {formatRelativeTime(item.updatedAt)} + + {item.reasons.length ? {item.reasons[0]} : null} + + 查看门禁 + + ); +} + function SavedIdeasBox({ ideas }: { ideas: SpaceSavedIdea[] }) { return (
@@ -558,8 +618,8 @@ function PanelHeader({ ); } -function MetricTile({ label, value, tone = "slate" }: { label: string; value: number; tone?: "slate" | "orange" | "blue" }) { - const toneClass = tone === "orange" ? "text-orange-500" : tone === "blue" ? "text-[#2463EB]" : "text-slate-950"; +function MetricTile({ label, value, tone = "slate" }: { label: string; value: number; tone?: "slate" | "orange" | "blue" | "rose" }) { + const toneClass = tone === "orange" ? "text-orange-500" : tone === "blue" ? "text-[#2463EB]" : tone === "rose" ? "text-rose-600" : "text-slate-950"; return (
{label}
@@ -643,6 +703,26 @@ function MobileAIProgress({ item }: { item: SpaceAIProgressItem }) { ); } +function MobileReleaseAttention({ item }: { item: SpaceReleaseAttentionItem }) { + const tone = releaseAttentionTone(item); + return ( + +
+ + + + + {item.title} + {item.summary} + {item.projectTitle} · {item.evidenceLabels.join(" / ") || "缺少可用证据"} + + {releaseGateLabel(item.gateStatus)} +
+ {item.reasons.length ?
{item.reasons[0]}
: null} + + ); +} + function MobileRecentProjectRow({ project }: { project: SpaceWorkbenchProject }) { return ( @@ -754,6 +834,34 @@ function executionStatusLabel(status: SpaceAIProgressItem["status"]): string { return labels[status] || "进行中"; } +function releaseGateLabel(status: SpaceReleaseAttentionItem["gateStatus"]): string { + const labels: Record = { + blocked: "阻塞", + needs_confirmation: "待确认", + ready: "可发布", + }; + return labels[status]; +} + +function releaseAttentionTone(item: SpaceReleaseAttentionItem): { iconClass: string; badgeClass: string } { + if (item.severity === "high") { + return { + iconClass: "bg-rose-50 text-rose-600", + badgeClass: "bg-rose-50 text-rose-600", + }; + } + if (item.severity === "medium") { + return { + iconClass: "bg-orange-50 text-orange-600", + badgeClass: "bg-orange-50 text-orange-600", + }; + } + return { + iconClass: "bg-emerald-50 text-emerald-600", + badgeClass: "bg-emerald-50 text-emerald-600", + }; +} + function formatRelativeTime(value?: string): string { if (!value) return "未记录时间"; const timestamp = new Date(value).getTime();