feat(space): surface release attention read model

This commit is contained in:
Albert 2026-04-26 12:36:01 +08:00
parent e90adbc2eb
commit 9ba9876811
6 changed files with 289 additions and 10 deletions

View File

@ -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=<id>`。
- 本轮不写 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
完成内容:

View File

@ -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=<id>`,只有显式 `?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=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`;暂存想法后端只读聚合尚未接入时只显示 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=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,发布关注项进入 `/release-growth?projectId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`;暂存想法后端只读聚合尚未接入时只显示 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=<id>` 时通过 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。

View File

@ -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=<id>`;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=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`;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=<id>`,待确认项进入 `/change-review?projectId=<id>&changeSetId=<id>`,执行中项进入 `/execution-bridge?projectId=<id>`,发布关注项进入 `/release-growth?projectId=<id>`;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=<id>` 走 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`

View File

@ -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<string, string> = {
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),

View File

@ -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);
});
});

View File

@ -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 ? <WorkbenchLoading /> : error ? <WorkbenchError error={error} onRetry={onRetry} /> : null}
@ -137,6 +139,8 @@ function DesktopSpace({
</div>
</section>
<ReleaseAttentionPanel items={data.releaseAttention} />
<div className="grid gap-5 xl:grid-cols-3">
<SavedIdeasBox ideas={data.savedIdeas} />
<DraftBox drafts={data.drafts} />
@ -195,8 +199,8 @@ function MobileSpace({
</div>
<div className="mt-5 grid grid-cols-4 gap-2 text-center">
<MobileMetric label="待确认" value={data?.pendingConfirmations.length ?? 0} />
<MobileMetric label="发布" value={data?.releaseAttention.length ?? 0} />
<MobileMetric label="AI 推进" value={data?.aiProgress.length ?? 0} />
<MobileMetric label="想法" value={data?.savedIdeas.length ?? 0} />
<MobileMetric label="草案" value={data?.drafts.length ?? 0} />
</div>
</section>
@ -228,6 +232,16 @@ function MobileSpace({
</div>
</MobileSection>
<MobileSection title="发布关注" badge={data.releaseAttention.length}>
<div className="space-y-3">
{data.releaseAttention.length ? (
data.releaseAttention.slice(0, 4).map((item) => <MobileReleaseAttention key={item.id} item={item} />)
) : (
<InlineEmpty title="暂无发布关注" description="有测试失败、PR 未确认或发布门禁阻塞时会显示在这里。" />
)}
</div>
</MobileSection>
<MobileSection title="AI 正在推进" badge={data.aiProgress.length}>
<div className="space-y-3">
{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 (
<section className="rounded-[24px] border border-[#dbe5f2] bg-white px-6 py-5 shadow-[0_14px_40px_rgba(15,23,42,0.06)]">
@ -302,9 +318,10 @@ function WorkbenchHeader({
聚合继续推进、待确认、执行中、暂存想法、草案箱和最近项目;点击项目后回到主流程处理。
</p>
</div>
<div className="grid min-w-[420px] grid-cols-3 gap-3">
<div className="grid min-w-[520px] grid-cols-4 gap-3">
<MetricTile label="最近项目" value={projectCount} />
<MetricTile label="待确认" value={pendingCount} tone="orange" />
<MetricTile label="发布关注" value={releaseCount} tone="rose" />
<MetricTile label="执行中" value={aiCount} tone="blue" />
</div>
</div>
@ -413,6 +430,49 @@ function AIProgressRail({ items }: { items: SpaceAIProgressItem[] }) {
);
}
function ReleaseAttentionPanel({ items }: { items: SpaceReleaseAttentionItem[] }) {
return (
<section className="rounded-[20px] border border-[#dde6f2] bg-white">
<PanelHeader
title="发布关注"
description="只读聚合 execution-summary 的发布门禁;处理动作回到 Release Growth 主流程。"
badge={items.length}
className="px-5 pt-5"
/>
<div className="mt-4 divide-y divide-[#eef3f8]">
{items.length ? (
items.slice(0, 5).map((item) => <ReleaseAttentionRow key={item.id} item={item} />)
) : (
<InlineEmpty title="暂无发布关注" description="有测试失败、PR 未合并或发布门禁阻塞时会显示在这里。" />
)}
</div>
</section>
);
}
function ReleaseAttentionRow({ item }: { item: SpaceReleaseAttentionItem }) {
const tone = releaseAttentionTone(item);
return (
<Link to={item.href} className="grid grid-cols-[44px_minmax(0,1fr)_120px] items-center gap-4 px-5 py-4 transition hover:bg-[#f8fbff]">
<span className={cn("flex h-10 w-10 items-center justify-center rounded-xl", tone.iconClass)}>
<AlertCircle className="h-5 w-5" />
</span>
<span className="min-w-0">
<span className="flex items-center gap-2">
<span className="truncate text-sm font-semibold text-slate-950">{item.title}</span>
<span className={cn("shrink-0 rounded-md px-2 py-0.5 text-xs font-semibold", tone.badgeClass)}>{releaseGateLabel(item.gateStatus)}</span>
</span>
<span className="mt-1 block line-clamp-2 text-xs leading-5 text-slate-500">{item.summary}</span>
<span className="mt-1 block truncate text-xs text-slate-500">
{item.projectTitle} · {item.evidenceLabels.length ? item.evidenceLabels.join(" / ") : "缺少可用证据"} · {formatRelativeTime(item.updatedAt)}
</span>
{item.reasons.length ? <span className="mt-1 block truncate text-xs font-medium text-rose-600">{item.reasons[0]}</span> : null}
</span>
<span className="justify-self-end rounded-lg border border-[#cfe0ff] px-3 py-1.5 text-xs font-semibold text-[#2463EB]">查看门禁</span>
</Link>
);
}
function SavedIdeasBox({ ideas }: { ideas: SpaceSavedIdea[] }) {
return (
<section className="rounded-[20px] border border-[#dbe5f2] bg-white p-5">
@ -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 (
<div className="rounded-2xl border border-[#e3eaf5] bg-[#fbfcff] px-4 py-3">
<div className="text-xs text-slate-500">{label}</div>
@ -643,6 +703,26 @@ function MobileAIProgress({ item }: { item: SpaceAIProgressItem }) {
);
}
function MobileReleaseAttention({ item }: { item: SpaceReleaseAttentionItem }) {
const tone = releaseAttentionTone(item);
return (
<Link to={item.href} className="block rounded-[18px] border border-[#dbe5f2] bg-white p-4">
<div className="flex items-start gap-3">
<span className={cn("flex h-10 w-10 shrink-0 items-center justify-center rounded-xl", tone.iconClass)}>
<AlertCircle className="h-5 w-5" />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-semibold text-slate-950">{item.title}</span>
<span className="mt-1 block line-clamp-2 text-xs leading-5 text-slate-500">{item.summary}</span>
<span className="mt-2 block truncate text-xs text-slate-500">{item.projectTitle} · {item.evidenceLabels.join(" / ") || "缺少可用证据"}</span>
</span>
<span className={cn("shrink-0 rounded-md px-2 py-1 text-xs font-semibold", tone.badgeClass)}>{releaseGateLabel(item.gateStatus)}</span>
</div>
{item.reasons.length ? <div className="mt-3 rounded-xl bg-rose-50 px-3 py-2 text-xs font-medium leading-5 text-rose-700">{item.reasons[0]}</div> : null}
</Link>
);
}
function MobileRecentProjectRow({ project }: { project: SpaceWorkbenchProject }) {
return (
<Link to={project.href} className="flex items-center justify-between gap-3 py-3 first:pt-3 last:pb-3">
@ -754,6 +834,34 @@ function executionStatusLabel(status: SpaceAIProgressItem["status"]): string {
return labels[status] || "进行中";
}
function releaseGateLabel(status: SpaceReleaseAttentionItem["gateStatus"]): string {
const labels: Record<SpaceReleaseAttentionItem["gateStatus"], string> = {
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();