merge release feedback p1

This commit is contained in:
Albert 2026-04-26 14:00:58 +08:00
commit caf5c12271
12 changed files with 685 additions and 13 deletions

View File

@ -114,7 +114,7 @@ frontend/
- `frontend/src/features/project-launch/`(首次进入页 / 新项目启动台;默认调用真实 `frontend/src/api/pmflowLaunch.ts` draft/confirm API 创建项目,只有显式 `?demo=1` 或 `?dataSource=demo` 才启用本地 mock 演示状态机)
- `frontend/src/features/account-settings/`(`/me` 个人中心与设置;通过 `frontend/src/api/accountSettings.ts` 管理个人资料、默认语言、AI 回答深度、证据展示偏好和通知偏好,移动端使用分组列表与 bottom sheet,不进入 auth core)
- `frontend/src/features/pmflow-os/`(PMFlow OS 前端产品域;共享 truth store 已支持 `?projectId=<id>` 读取真实后端 projection,真实模式下 message、`proposed_items` extraction、ChangeItem `accept/edit/defer` 与 ChangeSet `commit` 调后端,显式 `?demo=1` 才启用本地演示状态机)
- `backend/app/api/pmflow_os_api/` + `backend/app/services/pmflow_os_projection_service.py` + `backend/app/services/pmflow_change_review_service.py` + `backend/app/schemas/pmflow_os.py` + `backend/app/models/pmflow_change_review.py`(PMFlow OS A3/C1/C2 真实路径;提供 `truth-state`、`execution-summary`、thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer` 与 ChangeSet `commit`;复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync` 并投影 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C2 commit 默认 atomic,`ModelDecision / MetricSignal` 暂 fail-closed)
- `backend/app/api/pmflow_os_api/` + `backend/app/services/pmflow_os_projection_service.py` + `backend/app/services/pmflow_change_review_service.py` + `backend/app/schemas/pmflow_os.py` + `backend/app/models/pmflow_change_review.py`(PMFlow OS A3/C1/C2 真实路径;提供 `truth-state`、`execution-summary`、thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`、ChangeSet `commit`、execution handoff/progress writeback、release validation run 与 release feedback ingest;复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync / FeedbackIngest / IterationRecommendation` 并投影 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C2 commit 默认 atomic,`ModelDecision / MetricSignal` 暂 fail-closed)
- `backend/app/api/pmflow_launch_api/` + `backend/app/services/pmflow_launch_service.py` + `backend/app/schemas/pmflow_launch.py` + `backend/app/models/pmflow_launch.py`(Project Launch Phase B 真实创建链;`drafts` 只写 `pmflow_launch_drafts`,`confirm` 在同一事务写真实 Project、启动线程、启动消息、初始 truth_nodes、tasks 与 semantic_facts seed)
- `docs/architecture/PMFLOW_OS_PRODUCT_CONTRACT.md`(PMFlow OS 产品合同真源;真实后端接入前先读,锁定本地 truth store 对象与 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync` 的映射;Phase C1 review 表和审查 API 已实现,Phase C2 backend/frontend commit 已实现)
- `frontend/src/pages/inbox/`(消息台)
@ -134,7 +134,7 @@ frontend/
- `frontend/src/features/project-launch/` 是新项目启动台独立产品域:`types.ts / useProjectLaunchState.ts` 承载状态合同和真实 API 编排,`mockData.ts` 只在显式 demo 模式下使用;默认路径调用 `POST /pmflow-launch/drafts` 生成可审查草案,确认后调用 `POST /pmflow-launch/drafts/{draft_id}/confirm` 创建真实项目并跳转 `/pmflow-os?projectId=<id>`。`LaunchShell` 桌面端复用 `frontend/src/features/personal-space/components/SpaceChrome.tsx` 的空间左侧栏和顶部条,主画布仍由 `IntentInputPanel / AIProgressCanvas / HumanReviewCanvas / DraftEditCanvas / FailureStatePanel / SuccessTransitionPanel` 承接,移动端仍由 `MobileLaunchLayout` 承接;`frontend/src/pages/ProjectLaunchPage.tsx` 仅做薄入口。
- `frontend/src/features/personal-space/` 是空间层入口:`PersonalSpacePage.tsx` 承接桌面个人工作面与独立移动单手布局,`frontend/src/api/space.ts` 只读聚合 `projects` summary、PMFlow OS `truth-state`、`execution-summary` 并生成继续推进、待确认、发布关注、AI 正在推进、草案箱、最近项目摘要;Space P3 的发布关注优先消费 `truth-state.releases` 的 release validation 投影,没有验证投影时才回落 `execution-summary.releaseGate`,点击只跳 `/release-growth?projectId=<id>`;不在 Space 写 Project Memory、ProgressEvent、ReleaseValidationRun 或发布执行。`components/SpaceChrome.tsx` 承接空间层左侧栏、顶部条与空间用量组件,并被 `/project-launch` 桌面首次新建壳复用;`frontend/src/pages/PersonalSpacePage.tsx` 仅做薄入口。
- `frontend/src/features/account-settings/` 是 Account Settings 独立产品域:`AccountSettingsPanel.tsx` 承接 `/me` 桌面设置页、移动端分组列表与 bottom sheet,`frontend/src/api/accountSettings.ts` 定义个人资料、默认语言、AI 回答深度、证据展示偏好和四类通知偏好合同;当前已有账号同步 API fallback、本地 UI 通知消费模型和 Release/Growth 偏好 helper,`releaseGate / failureAlert` 只影响页面内提醒表达,不代表真实邮件、桌面推送或系统通知中心已接入,不改 OAuth/JWT/auth core;`frontend/src/features/pmflow-os/pages/AccountPage.tsx` 仅做薄入口并复用既有退出登录 bottom sheet。
- `frontend/src/features/pmflow-os/` 当前承载新的对话驱动前端产品域:`store.tsx` 提供共享 truth store;真实模式通过 `?projectId=<id>` 调用 `frontend/src/api/pmflowOs.ts` 读取后端 `ProjectTruthState` projection,并调用真实 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet `commit`、Execution Handoff create/consume、execution progress writeback 与 release validation run create API,成功后 refetch truth-state / execution-summary;显式 `?demo=1` 才启用 `command_idle -> ai_extracting -> extraction_ready -> review_open -> writeback_success` 本地演示状态机,以及本地 `promoteAcceptedToGraph` 等演示动作。`pages/` 下包含 `CommandCenter / StructuredExtraction / ChangeReview / MemoryWritebackResult / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主页面;`ExecutionBridgePage` 已支持创建 handoff、运行端领取确认和执行进度回写;`ReleaseGrowthPage` 真实模式展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作和 `latestValidationRun`,可创建 release validation run,并消费 Account notification preference 静默强调型提醒,但不隐藏真实证据或审计状态;静态发布/增长原型仅保留在显式 demo 模式;后端只记录 progress/release gate/release validation 元数据,不执行本机命令或真实发布命令;`ProjectMemoryPage` 在真实模式消费后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,真实模式下 accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`components/` 下除了 `ProjectStageRail / ThreadComposer / ExtractionRail / ChangeGroupPanel / ObjectGraphSection / ContextPacketPanel / ExecutionTimeline / ReleaseGatePanel / TeamFeedPanel` 之外,新增 `ProjectStructuredLoop / PMFlowOsFrame / AccountControls / FeedbackPrimitives` 等行为组件,用来承接项目指挥台、结构化提取、待确认审查、项目记忆写入结果、项目内面包屑、项目切换器、账号退出登录交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组契约化提示提醒组件;`components/MobileBottomNav.tsx` 是正式应用壳唯一移动底部导航,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`。
- `frontend/src/features/pmflow-os/` 当前承载新的对话驱动前端产品域:`store.tsx` 提供共享 truth store;真实模式通过 `?projectId=<id>` 调用 `frontend/src/api/pmflowOs.ts` 读取后端 `ProjectTruthState` projection,并调用真实 message、`proposed_items` extraction、ChangeItem `accept/edit/defer`、ChangeSet `commit`、Execution Handoff create/consume、execution progress writeback、release validation run create 与 release feedback ingest API,成功后 refetch truth-state / execution-summary;显式 `?demo=1` 才启用 `command_idle -> ai_extracting -> extraction_ready -> review_open -> writeback_success` 本地演示状态机,以及本地 `promoteAcceptedToGraph` 等演示动作。`pages/` 下包含 `CommandCenter / StructuredExtraction / ChangeReview / MemoryWritebackResult / WorkGraph / ExecutionBridge / ReleaseGrowth / TeamLayer / ProjectMemory / Account` 主页面;`ExecutionBridgePage` 已支持创建 handoff、运行端领取确认和执行进度回写;`ReleaseGrowthPage` 真实模式展示 `execution-summary.releaseGate`、execution evidence、阻塞原因、下一步动作、`latestValidationRun` 和 `releaseSignals`,可创建 release validation run、摄取发布后 feedback,并消费 Account notification preference 静默强调型提醒,但不隐藏真实证据、审计状态或发布后信号;静态发布/增长原型仅保留在显式 demo 模式;后端只记录 progress/release gate/release validation/release feedback 元数据,不执行本机命令、真实发布命令、外部支付或真实推送;`ProjectMemoryPage` 在真实模式消费后端 `truth_nodes + semantic_facts` 的 `projectMemory` 投影,真实模式下 accepted/edited ChangeItem 只有 commit 成功后才进入 Project Memory。`components/` 下除了 `ProjectStageRail / ThreadComposer / ExtractionRail / ChangeGroupPanel / ObjectGraphSection / ContextPacketPanel / ExecutionTimeline / ReleaseGatePanel / TeamFeedPanel` 之外,新增 `ProjectStructuredLoop / PMFlowOsFrame / AccountControls / FeedbackPrimitives` 等行为组件,用来承接项目指挥台、结构化提取、待确认审查、项目记忆写入结果、项目内面包屑、项目切换器、账号退出登录交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组契约化提示提醒组件;`components/MobileBottomNav.tsx` 是正式应用壳唯一移动底部导航,固定为 `空间 / 指挥台 / 待确认 / 动态 / 我的`。
- 旧登录实现 `frontend/src/components/ui/sign-in.tsx` 与 `frontend/src/components/ui/demo.tsx` 已移除,`frontend/src/components/login/` 是当前唯一登录真源。
### 5.1 `frontend/src/components/sstc/` 目录说明(模块化拆分后)

View File

@ -17,6 +17,7 @@ from ...schemas.pmflow_os import (
PMFlowExecutionHandoffRequest,
PMFlowExecutionProgressEventRequest,
PMFlowExtractionCreateRequest,
PMFlowReleaseFeedbackIngestRequest,
PMFlowReleaseValidationRunCreateRequest,
PMFlowThreadMessageCreateRequest,
)
@ -351,6 +352,39 @@ async def create_pmflow_os_release_validation_run(
return success_response(payload)
@router.post("/projects/{project_id}/release-feedback-ingests")
async def create_pmflow_os_release_feedback_ingest(
project_id: int,
body: PMFlowReleaseFeedbackIngestRequest,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
tenant_id = int(current_user.tenant_id)
await _ensure_project_member_or_manager(
db,
tenant_id=tenant_id,
project_id=int(project_id),
current_user=current_user,
)
service = PMFlowOSProjectionService(db, tenant_id=tenant_id)
try:
payload = await service.create_release_feedback_ingest(
project_id=int(project_id),
body=body,
current_user_id=int(current_user.id),
)
except ValueError as exc:
code = str(exc)
if code in {"feedback_content_empty", "反馈内容不能为空"}:
return JSONResponse(status_code=400, content=error_response(400, "feedback_content_empty"))
if code == "charter_not_found":
return JSONResponse(status_code=404, content=error_response(404, "charter_not_found"))
if code == "project_not_found":
raise HTTPException(status_code=404, detail="project_not_found") from exc
raise
return success_response(payload)
@router.post("/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume")
async def consume_pmflow_os_execution_handoff(
project_id: int,

View File

@ -137,6 +137,15 @@ class PMFlowReleaseValidationRunCreateRequest(BaseModel):
note: Optional[str] = Field(default=None, max_length=4000)
class PMFlowReleaseFeedbackIngestRequest(BaseModel):
source_type: str = Field(default="manual", min_length=1, max_length=32)
title: Optional[str] = Field(default=None, max_length=255)
content: str = Field(min_length=1, max_length=12000)
charter_id: Optional[str] = None
metrics: dict[str, Any] = Field(default_factory=dict)
samples: list[dict[str, Any]] = Field(default_factory=list)
class PMFlowChangeSetDetailResponse(BaseModel):
changeSet: dict[str, Any]
items: list[dict[str, Any]] = Field(default_factory=list)
@ -300,6 +309,7 @@ class ExecutionSummaryProjection(BaseModel):
projectId: str
execution: PMFlowExecutionContext
releaseGate: dict[str, Any] = Field(default_factory=dict)
releaseSignals: dict[str, Any] = Field(default_factory=dict)
devSync: dict[str, Any] = Field(default_factory=dict)
promptPackage: Optional[dict[str, Any]] = None
repoBindings: list[dict[str, Any]] = Field(default_factory=list)

View File

@ -12,6 +12,7 @@ from sqlalchemy.orm.attributes import flag_modified
from ..models import (
FeedbackIngest,
IterationRecommendation,
ModelCatalogItem,
ModelEvalRun,
ModelRecommendationSnapshot,
@ -47,6 +48,7 @@ from ..schemas.pmflow_os import (
PMFlowProjectMemory,
PMFlowProjectSummary,
PMFlowRelease,
PMFlowReleaseFeedbackIngestRequest,
PMFlowReleaseValidationRunCreateRequest,
PMFlowRequirement,
PMFlowRisk,
@ -196,6 +198,11 @@ class PMFlowOSProjectionService:
release_gate = self._build_release_gate(events=events, deliveries=deliveries)
if latest_release_validation is not None:
release_gate["latestValidationRun"] = self._serialize_release_validation_run(latest_release_validation)
release_signals = await self._build_release_signals(
project_id=project_id,
release_gate=release_gate,
latest_release_validation=latest_release_validation,
)
return ExecutionSummaryProjection(
projectId=str(project_id),
@ -209,6 +216,7 @@ class PMFlowOSProjectionService:
writebackStatus=writeback,
),
releaseGate=release_gate,
releaseSignals=release_signals,
devSync=dev_sync.get("summary") or {},
promptPackage=None if latest_package is None else self._serialize_prompt_package(latest_package),
repoBindings=repos,
@ -321,6 +329,54 @@ class PMFlowOSProjectionService:
"warnings": [] if status != "blocked" else list(release_gate.get("blockingReasons") or []),
}
async def create_release_feedback_ingest(
self,
*,
project_id: int,
body: PMFlowReleaseFeedbackIngestRequest,
current_user_id: Optional[int] = None,
) -> dict[str, Any]:
await self._load_project(project_id=project_id)
charter_id = self._optional_int(body.charter_id, error_code="charter_not_found")
control = AIProjectControlService(self.db, tenant_id=self.tenant_id)
try:
payload = await control.ingest_feedback(
project_id=int(project_id),
actor_user_id=current_user_id,
source_type=body.source_type,
title=body.title,
content=body.content,
charter_id=charter_id,
metrics=dict(body.metrics or {}),
samples=list(body.samples or []),
)
except ValueError as exc:
if str(exc) == "反馈内容不能为空":
raise ValueError("feedback_content_empty") from exc
raise
updated_summary = await self.get_execution_summary(project_id=project_id, current_user_id=current_user_id)
release_signals = dict(updated_summary.releaseSignals or {})
raw_ingest_id = str((payload.get("ingest") or {}).get("id") or "")
projected_ingest = next(
(item for item in list(release_signals.get("feedback") or []) if str(item.get("id")) == raw_ingest_id),
None,
)
projected_recommendation = next(
(
item
for item in list(release_signals.get("recommendations") or [])
if str(item.get("feedbackIngestId") or "") == raw_ingest_id
),
None,
)
return {
"ingest": projected_ingest or payload.get("ingest"),
"recommendation": projected_recommendation or payload.get("recommendation"),
"releaseSignals": release_signals,
"executionSummary": updated_summary.model_dump(mode="json"),
"warnings": list(release_signals.get("warnings") or []),
}
async def consume_execution_handoff(
self,
*,
@ -490,6 +546,79 @@ class PMFlowOSProjectionService:
)
return result.scalar_one_or_none()
async def _build_release_signals(
self,
*,
project_id: int,
release_gate: dict[str, Any],
latest_release_validation: Optional[ReleaseValidationRun],
) -> dict[str, Any]:
feedback_result = await self.db.execute(
select(FeedbackIngest)
.where(and_(FeedbackIngest.tenant_id == self.tenant_id, FeedbackIngest.project_id == int(project_id)))
.order_by(FeedbackIngest.updated_at.desc(), FeedbackIngest.created_at.desc(), FeedbackIngest.id.desc())
.limit(10)
)
feedback_rows = list(feedback_result.scalars().all())
feedback_items = [self._serialize_feedback_ingest(item) for item in feedback_rows]
recommendation_result = await self.db.execute(
select(IterationRecommendation)
.where(and_(IterationRecommendation.tenant_id == self.tenant_id, IterationRecommendation.project_id == int(project_id)))
.order_by(IterationRecommendation.updated_at.desc(), IterationRecommendation.created_at.desc(), IterationRecommendation.id.desc())
.limit(10)
)
recommendation_rows = list(recommendation_result.scalars().all())
recommendations = [self._serialize_iteration_recommendation(item) for item in recommendation_rows]
revenue_result = await self.db.execute(
select(RevenueEvent)
.where(and_(RevenueEvent.tenant_id == self.tenant_id, RevenueEvent.project_id == int(project_id)))
.order_by(RevenueEvent.occurred_at.desc(), RevenueEvent.created_at.desc(), RevenueEvent.id.desc())
.limit(10)
)
revenue_rows = list(revenue_result.scalars().all())
revenue_events = [self._serialize_revenue_event(item) for item in revenue_rows]
warnings: list[str] = []
if not feedback_items:
warnings.append("release_feedback_empty: no post-release feedback has been ingested.")
if not revenue_events:
warnings.append("release_revenue_empty: no revenue event has been recorded.")
if not recommendations:
warnings.append("iteration_recommendations_empty: no iteration recommendation has been generated.")
serialized_times = [
str(item.get("updatedAt") or item.get("createdAt"))
for item in [*feedback_items, *recommendations, *revenue_events]
if item.get("updatedAt") or item.get("createdAt")
]
return {
"status": "active" if feedback_items or revenue_events or recommendations else "empty",
"relationship": {
"releaseGateStatus": release_gate.get("status") or "empty",
"latestValidationRunId": None if latest_release_validation is None else str(latest_release_validation.id),
"latestValidationRunStatus": None if latest_release_validation is None else latest_release_validation.status,
"source": "pmflow_os_release_growth",
"changeSetCreated": False,
},
"summary": {
"feedbackCount": len(feedback_items),
"revenueEventCount": len(revenue_events),
"iterationRecommendationCount": len(recommendations),
"latestSignalAt": max(serialized_times) if serialized_times else None,
},
"feedback": feedback_items,
"revenueEvents": revenue_events,
"recommendations": recommendations,
"nextActions": self._release_signal_next_actions(
feedback_items=feedback_items,
recommendations=recommendations,
release_gate=release_gate,
),
"warnings": warnings,
}
async def _load_active_prompt_package_for_task(self, *, project_id: int, task_id: int) -> Optional[PromptPackage]:
result = await self.db.execute(
select(PromptPackage)
@ -1531,6 +1660,78 @@ class PMFlowOSProjectionService:
"updatedAt": run.updated_at.isoformat() if run.updated_at else None,
}
def _serialize_feedback_ingest(self, ingest: FeedbackIngest) -> dict[str, Any]:
return {
"id": str(ingest.id),
"projectId": str(ingest.project_id),
"charterId": None if ingest.charter_id is None else str(ingest.charter_id),
"sourceType": ingest.source_type,
"title": ingest.title,
"content": ingest.content,
"metrics": dict(ingest.metrics or {}),
"samples": list(ingest.samples or []),
"status": ingest.status,
"createdAt": ingest.created_at.isoformat() if ingest.created_at else None,
"updatedAt": ingest.updated_at.isoformat() if ingest.updated_at else None,
}
def _serialize_iteration_recommendation(self, recommendation: IterationRecommendation) -> dict[str, Any]:
return {
"id": str(recommendation.id),
"projectId": str(recommendation.project_id),
"charterId": None if recommendation.charter_id is None else str(recommendation.charter_id),
"feedbackIngestId": None if recommendation.feedback_ingest_id is None else str(recommendation.feedback_ingest_id),
"status": recommendation.status,
"priority": recommendation.priority,
"recommendationSummary": dict(recommendation.recommendation_summary or {}),
"proposedCharterPatch": dict(recommendation.proposed_charter_patch or {}),
"proposedTasks": list(recommendation.proposed_tasks or []),
"changeSetCreated": False,
"createdAt": recommendation.created_at.isoformat() if recommendation.created_at else None,
"updatedAt": recommendation.updated_at.isoformat() if recommendation.updated_at else None,
}
def _serialize_revenue_event(self, event: RevenueEvent) -> dict[str, Any]:
return {
"id": str(event.id),
"projectId": str(event.project_id),
"sourceType": event.source_type,
"category": event.category,
"currency": event.currency,
"amountUsd": self._jsonable(event.amount_usd),
"externalRef": event.external_ref,
"eventPayload": dict(event.event_payload or {}),
"occurredAt": event.occurred_at.isoformat() if event.occurred_at else None,
"createdAt": event.created_at.isoformat() if event.created_at else None,
"updatedAt": event.updated_at.isoformat() if event.updated_at else None,
}
def _release_signal_next_actions(
self,
*,
feedback_items: list[dict[str, Any]],
recommendations: list[dict[str, Any]],
release_gate: dict[str, Any],
) -> list[str]:
actions: list[str] = []
if not feedback_items:
actions.append("发布后先摄取用户反馈、运营记录或人工观察,避免用 mock 增长数据判断下一轮。")
if feedback_items and recommendations:
actions.append("基于真实反馈查看 iteration recommendation,但本阶段不自动创建 ChangeSet。")
if str(release_gate.get("status") or "") == "blocked":
actions.append("发布门禁仍阻塞时,先处理 release gate,再把反馈作为下一轮规划输入。")
if not actions:
actions.append("继续沉淀 feedback / revenue / compliance 信号,为下一轮 iteration recommendation 做准备。")
return actions
def _optional_int(self, value: Optional[Any], *, error_code: str) -> Optional[int]:
if value is None or str(value).strip() == "":
return None
try:
return int(str(value).strip())
except (TypeError, ValueError) as exc:
raise ValueError(error_code) from exc
def _serialize_prompt_package(self, package: PromptPackage) -> dict[str, Any]:
return {
"id": str(package.id),

View File

@ -21,6 +21,7 @@ from app.models import ( # noqa: E402
FeedbackIngest,
FeishuItem,
GitHubWebhookDelivery,
IterationRecommendation,
ModelCatalogItem,
ModelEvalRun,
ModelRecommendationSnapshot,
@ -77,6 +78,7 @@ TABLES = [
ProgressEvent.__table__,
ReleaseValidationRun.__table__,
FeedbackIngest.__table__,
IterationRecommendation.__table__,
RevenueEvent.__table__,
ROISnapshot.__table__,
GitHubWebhookDelivery.__table__,
@ -282,6 +284,8 @@ async def test_execution_summary_empty_state_is_pending_with_warnings(db_env) ->
assert payload["execution"]["testSummary"] == "pending"
assert payload["releaseGate"]["status"] == "empty"
assert "缺少执行证据" in payload["releaseGate"]["title"]
assert payload["releaseSignals"]["status"] == "empty"
assert "release_feedback_empty" in " ".join(payload["releaseSignals"]["warnings"])
assert "prompt_package_empty" in " ".join(payload["warnings"])
assert payload["activeDevTasks"] == []
@ -1015,6 +1019,44 @@ async def test_release_validation_run_rejects_empty_gate(db_env) -> None:
assert rows == []
@pytest.mark.asyncio
async def test_release_feedback_ingest_creates_signal_and_iteration_recommendation(db_env) -> None:
session = db_env
user, project = await _seed_user_project(session)
app = _build_app(session, SimpleNamespace(id=user.id, tenant_id=1, role="MEMBER"))
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
response = await client.post(
f"/pmflow-os/projects/{project.id}/release-feedback-ingests",
json={
"source_type": "manual",
"title": "首批试用用户反馈",
"content": "用户反馈上传入口难找,希望下一轮优化 onboarding。",
"metrics": {"negative_feedback_count": 3, "activation_drop": "12%"},
"samples": [{"channel": "访谈", "text": "入口难找"}],
},
)
summary_response = await client.get(f"/pmflow-os/projects/{project.id}/execution-summary")
assert response.status_code == 200
payload = response.json()["data"]
assert payload["ingest"]["title"] == "首批试用用户反馈"
assert payload["recommendation"]["feedbackIngestId"] == payload["ingest"]["id"]
assert payload["recommendation"]["changeSetCreated"] is False
assert payload["releaseSignals"]["status"] == "active"
assert payload["releaseSignals"]["feedback"][0]["metrics"]["negative_feedback_count"] == 3
assert payload["releaseSignals"]["recommendations"][0]["feedbackIngestId"] == payload["ingest"]["id"]
assert payload["releaseSignals"]["relationship"]["changeSetCreated"] is False
assert payload["executionSummary"]["releaseSignals"]["status"] == "active"
assert summary_response.json()["data"]["releaseSignals"]["feedback"][0]["title"] == "首批试用用户反馈"
feedback_rows = (await session.execute(select(FeedbackIngest).where(FeedbackIngest.project_id == project.id))).scalars().all()
recommendation_rows = (
await session.execute(select(IterationRecommendation).where(IterationRecommendation.project_id == project.id))
).scalars().all()
assert len(feedback_rows) == 1
assert len(recommendation_rows) == 1
@pytest.mark.asyncio
async def test_execution_progress_writeback_rejects_repo_outside_project(db_env) -> None:
session = db_env

View File

@ -807,6 +807,45 @@ P0 公共壳冻结已完成,下一批可以进入 `Batch P1` 三模块并行
## 8. 进度台账
### 2026-04-26 / release-feedback-p1-feedback-ingest-readmodel / implemented
完成内容:
- 在 PMFlow OS 后端新增轻量写入口 `POST /pmflow-os/projects/{project_id}/release-feedback-ingests`,复用现有 `AIProjectControlService.ingest_feedback` 与 `FeedbackIngest / IterationRecommendation` 模型,不新增 migration。
- `GET /pmflow-os/projects/{project_id}/execution-summary` 新增 `releaseSignals` 投影,聚合真实发布后 `feedback / revenueEvents / recommendations`,并显式返回 `releaseGateStatus / latestValidationRunId / changeSetCreated=false` 关系信息。
- `ReleaseGrowthPage` 真实模式新增“发布后反馈信号”区域和“摄取发布后反馈”表单;无信号时展示明确 empty/warning,不用 mock 成功数据补位。
- 前端 `PMFlowOsProvider` 新增 `createReleaseFeedbackIngest` 动作,真实模式调用 PMFlow OS API 并 refetch truth-state / execution-summary;demo 模式仍只在显式 `?demo=1` 下走本地演示 mutation。
- 本阶段只沉淀下一轮 iteration recommendation 所需数据,不自动创建 ChangeSet,不触发真实发布,不接外部支付,不接邮件/桌面推送。
涉及文件:
- `backend/app/api/pmflow_os_api/__init__.py`
- `backend/app/schemas/pmflow_os.py`
- `backend/app/services/pmflow_os_projection_service.py`
- `backend/tests/test_pmflow_os_projection_api.py`
- `frontend/src/api/pmflowOs.ts`
- `frontend/src/features/pmflow-os/store.tsx`
- `frontend/src/features/pmflow-os/store.api.test.tsx`
- `frontend/src/features/pmflow-os/pages/ReleaseGrowthPage.tsx`
- `docs/PMFLOW_PRODUCT_DELIVERY_STANDARD.md`
- `docs/architecture/CODE_REFERENCE_INDEX.md`
- `docs/architecture/ARCHITECTURE_INVENTORY.md`
- `CLAUDE.md`
验证:
- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m py_compile backend/app/api/pmflow_os_api/__init__.py backend/app/services/pmflow_os_projection_service.py backend/app/schemas/pmflow_os.py` 通过。
- `PYTHONPATH=backend /opt/homebrew/bin/python3.11 -m pytest backend/tests/test_pmflow_os_projection_api.py -q` 通过:`18 passed`;仍有既有 `backend/app/schemas/recipe.py` Pydantic V2 class-based config deprecation warning。
- `cd frontend && npm run test -- src/features/pmflow-os` 通过:`2 passed / 8 passed`。
- `cd frontend && npm run typecheck` 未通过:当前独立 worktree 没有被 `.gitignore` 忽略的 `frontend/src/lib/*` 本地 helper 文件,导致全仓 `@/lib/api`、`@/lib/utils`、`@/lib/datetime`、`@/lib/display`、`@/lib/rbac` 等 alias 解析失败;这些文件不在本 Packet write scope,未越界补入。
- `cd frontend && npm run build` 同样停在上述 `frontend/src/lib/*` 缺失的 TypeScript 解析错误,未进入 Vite build。
未完成或风险:
- 本阶段只接入 feedback ingest 与 releaseSignals 读模型;RevenueEvent / ComplianceCheckRun 已可被后续扩展继续纳入更完整发布后运营读模型。
- iteration recommendation 目前作为建议底座展示,不自动生成 ChangeSet;下一轮如果要从 recommendation 进入待确认中心,需要单独开 Main Flow packet。
- 全量前端 typecheck/build 需在包含 `frontend/src/lib/*` helper 的集成环境或由 Shell / Integration owner 修正该 ignored 文件边界后重新验证。
### 2026-04-26 / big-module-integration-p4 / done
完成内容:

View File

@ -197,9 +197,9 @@
- `frontend/src/features/personal-space/PersonalSpacePage.tsx` 是空间首页视图层,`frontend/src/api/space.ts` 是空间工作面只读聚合 API:从 `projects` summary、PMFlow OS `truth-state` 与 `execution-summary` 生成继续推进、待确认、发布关注、执行中 / AI 正在推进、暂存想法、草案箱、最近项目摘要;Space P3 的 `releaseAttention` 优先消费 `truth-state.releases` 中由 release validation run 投影出的 release 状态与检查项,把“发布验证阻塞 / 发布验证待确认”显示到 `/space`,没有发布验证投影时才回落到 `execution-summary.releaseGate` 的发布候选或发布门禁阻塞;发布验证缺失时显示明确空态,不虚构验证结果。项目主动作进入 `/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 写入、ReleaseValidationRun 写入或发布执行。
- `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/`,失败时回落浏览器本机暂存;P2 新增 `shouldShowAccountNotification` 与 `getAccountNotificationConsumption`,将四类通知偏好映射为本地 UI 的待确认、Execution Bridge、Release/Growth 和失败提醒显示/静默状态;P3 新增 `getReleaseGateNotificationPreference(settings, event)`,把 Release/Growth 的 `release_gate_* / release_validation_*` 事件映射到 `releaseGate` 偏好,把 `failed_event` 映射到 `failureAlert` 偏好,并返回 `inAppEnabled / desktopPreference / emailDigestPreference` 供后续 Main Flow 页面消费。该合同不代表邮件、桌面推送或系统通知中心已接入。`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 与 release validation run create 均调用 `/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` 与 `latestValidationRun`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因、下一步动作和发布验证记录创建动作,不执行发布命令、不展示 mock 发布结果;同时读取 Account notification preference helper,用 `releaseGate` 偏好控制发布门禁强调提醒,用 `failureAlert` 偏好控制失败强调提醒,偏好关闭时只静默提醒表达强度,不隐藏 release evidence、阻塞原因、审计状态或 validation run;静态发布/增长原型只保留在显式 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,Release Validation P1 已接入 `POST /release-validation-runs` 创建 `ReleaseValidationRun` 快照;当前仍不调用真实 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;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,复用现有 `ReleaseValidationRun` 模型固化当前 `releaseGate` 快照、人工决策、证据、阻塞原因和下一步动作,并在 `releaseGate.latestValidationRun` 返回最新记录;这些步骤都不直接执行本机命令或真实发布命令;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。
- `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、release validation run create 与 release feedback ingest 均调用 `/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`、`latestValidationRun` 与 `releaseSignals`,从 branch、commit、PR、test、failed evidence 展示 `可发布 / 阻塞 / 需要人工确认 / 缺少证据` 判断、阻塞原因、下一步动作、发布验证记录和发布后 feedback/revenue/recommendation 信号,缺少发布后信号时显示 empty/warning,不执行发布命令、不展示 mock 发布结果;同时读取 Account notification preference helper,用 `releaseGate` 偏好控制发布门禁强调提醒,用 `failureAlert` 偏好控制失败强调提醒,偏好关闭时只静默提醒表达强度,不隐藏 release evidence、阻塞原因、审计状态、validation run 或 releaseSignals;静态发布/增长原型只保留在显式 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,Release Validation P1 已接入 `POST /release-validation-runs` 创建 `ReleaseValidationRun` 快照,Release Feedback P1 已接入 `POST /release-feedback-ingests` 写入 `FeedbackIngest` 并生成 `IterationRecommendation`;当前仍不调用真实 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;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,复用现有 `ReleaseValidationRun` 模型固化当前 `releaseGate` 快照、人工决策、证据、阻塞原因和下一步动作,并在 `releaseGate.latestValidationRun` 返回最新记录;Release Feedback P1 新增 `POST /pmflow-os/projects/{project_id}/release-feedback-ingests`,复用现有 `FeedbackIngest / IterationRecommendation`,在 `ExecutionSummaryProjection.releaseSignals` 中返回真实发布后 feedback、revenue event 和 iteration recommendation 读模型;这些步骤都不直接执行本机命令、真实发布命令、外部支付或真实推送;`ModelDecision / MetricSignal` 在 C2 V1 仍 fail-closed。
- PMFlow Launch 后端 Phase B 创建链位于 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与 `backend/app/models/pmflow_launch.py`;`POST /pmflow-launch/drafts` 只写 `pmflow_launch_drafts`,`POST /pmflow-launch/drafts/{draft_id}/confirm` 在同一事务内写 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs`,并把 `launch_draft_id / thread_id / thread_message_id` 写入正式对象 metadata。
- `frontend/src/features/pmflow-os/components/ChangeReviewGroup.tsx`、`WorkbenchPanel.tsx`、`ExecutionBridgeCards.tsx`、`PMFlowOsFrame.tsx`、`AccountControls.tsx`、`FeedbackPrimitives.tsx` 是高保真原型新增的行为组件层:分别承载变更 diff 审查块、工作台对象卡组、执行桥上下文/日志卡片、全局上下文条与 AI 副驾、账号菜单/退出登录确认交互,以及 `PageBanner / InlineNotice / HumanDecisionBar / ConfirmDialog / MobileBottomSheet` 这组提示、提醒、确认与移动端 bottom sheet 基础组件,避免再把这些业务片段内联塞回 `pages/`。
- `frontend/vite.config.ts` 现补了固定 vendor chunk 分组(`framework / state-query / ui-radix / icons / charts / markdown`)与业务 chunk 分组(`ai-steward / sstc / governance`),目标是让登录页与基础导航不再背 AI Steward、SSTC、治理页和图表代码。

View File

@ -89,7 +89,7 @@
- SSTC 负责人推荐接口:参考 `POST /projects/{project_id}/tasks/{task_id}/recommend-assignee`
- SSTC 任务更新接口:参考 `PATCH /projects/{project_id}/tasks/{task_id}`
- SSTC 发布创建接口:参考 `POST /projects/{project_id}/releases`
- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit、Execution Handoff、Release Evidence Gate 与 Release Validation Run:参考 `backend/app/api/pmflow_os_api/`、`backend/app/services/pmflow_os_projection_service.py`、`backend/app/services/pmflow_change_review_service.py`、`backend/app/schemas/pmflow_os.py`;只读提供 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,并读取 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C1 写 API 包括 thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit`,默认 `mode=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`,对已确认任务生成/复用 `PromptPackage` 并创建 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,记录运行端领取、更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并创建 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 已在 `execution-summary.releaseGate` 中从 branch / commit / PR / test_passed / checks_passed / failed / blocked 等 `ProgressEvent` 推导 `ready / blocked / needs_confirmation / empty`;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,从当前 `releaseGate` 固化 `ReleaseValidationRun` 快照并在 `releaseGate.latestValidationRun` 返回最新记录,无 evidence 时返回 `release_validation_evidence_empty`,这些步骤均不执行发布命令
- PMFlow OS 真实读路径、C1 Change Review API、C2 backend commit、Execution Handoff、Release Evidence Gate、Release Validation Run 与 Release Feedback Signal:参考 `backend/app/api/pmflow_os_api/`、`backend/app/services/pmflow_os_projection_service.py`、`backend/app/services/pmflow_change_review_service.py`、`backend/app/schemas/pmflow_os.py`;只读提供 `GET /pmflow-os/projects/{project_id}/truth-state` 与 `GET /pmflow-os/projects/{project_id}/execution-summary`,复用 `truth_nodes / tasks / semantic_facts / ai_project_control / dev_sync`,并读取 `pmflow_threads / pmflow_thread_messages / pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`;C1 写 API 包括 thread message、`proposed_items` extraction、ChangeSet detail、ChangeItem `accept/edit/defer`;C2 backend 已实现 `POST /pmflow-os/change-sets/{change_set_id}/commit`,默认 `mode=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`,对已确认任务生成/复用 `PromptPackage` 并创建 `ProgressEvent(event_type='handoff_created')` 起点;P1 已实现 `POST /pmflow-os/projects/{project_id}/execution-handoffs/{prompt_package_id}/consume`,记录运行端领取、更新 `PromptPackage.package_payload.latest_handoff.handoff_state=consumed` 并创建 `ProgressEvent(event_type='handoff_consumed', derived_task_status='coding')`;Release Growth P1 已在 `execution-summary.releaseGate` 中从 branch / commit / PR / test_passed / checks_passed / failed / blocked 等 `ProgressEvent` 推导 `ready / blocked / needs_confirmation / empty`;Release Validation P1 新增 `POST /pmflow-os/projects/{project_id}/release-validation-runs`,从当前 `releaseGate` 固化 `ReleaseValidationRun` 快照并在 `releaseGate.latestValidationRun` 返回最新记录,无 evidence 时返回 `release_validation_evidence_empty`;Release Feedback P1 新增 `POST /pmflow-os/projects/{project_id}/release-feedback-ingests`,复用 `FeedbackIngest / IterationRecommendation` 写入真实发布后反馈,并在 `execution-summary.releaseSignals` 投影真实 `feedback / revenueEvents / recommendations` 与 release gate / validation run 关系;这些步骤均不执行发布命令、外部支付或真实推送
- Project Launch 真实创建链:参考 `backend/app/api/pmflow_launch_api/`、`backend/app/services/pmflow_launch_service.py`、`backend/app/schemas/pmflow_launch.py` 与模型 `backend/app/models/pmflow_launch.py`;接口为 `POST /pmflow-launch/drafts` 与 `POST /pmflow-launch/drafts/{draft_id}/confirm`,draft 只写 `pmflow_launch_drafts`,confirm 在同一事务内创建 `projects / user_projects / pmflow_threads / pmflow_thread_messages / truth_nodes / tasks / semantic_facts seed / operation_logs`
- PMFlow Change Review 模型与 migration:参考 `backend/app/models/pmflow_change_review.py` 与 `backend/alembic/versions/20260426_add_pmflow_change_review_tables.py`;只包含 `pmflow_change_sets / pmflow_change_items / pmflow_approvals / pmflow_evidence_links`,本阶段不得新增或依赖 `/commit`
@ -97,7 +97,7 @@
- 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、ChangeSet 状态、ProgressEvent 或 ReleaseValidationRun;Space P3 的 `releaseAttention` 优先从 `truth-state.releases` 的 release validation 投影生成“发布验证阻塞 / 发布验证待确认”,没有发布验证投影时才回落到 `PMFlowExecutionSummary.releaseGate` 的“发布候选 / 发布门禁阻塞”,所有动作只跳转 `/release-growth?projectId=<id>`;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowExecutionProgressEvent`、`createPMFlowReleaseValidationRun`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同,`releaseGate.latestValidationRun` 是最新发布验证记录前端合同;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 状态、ProgressEvent 或 ReleaseValidationRun;Space P3 的 `releaseAttention` 优先从 `truth-state.releases` 的 release validation 投影生成“发布验证阻塞 / 发布验证待确认”,没有发布验证投影时才回落到 `PMFlowExecutionSummary.releaseGate` 的“发布候选 / 发布门禁阻塞”,所有动作只跳转 `/release-growth?projectId=<id>`;PMFlow OS 真实接入参考 `frontend/src/api/pmflowOs.ts` 的 `getPMFlowTruthState(projectId)`、`getPMFlowExecutionSummary(projectId)`、`createPMFlowExecutionHandoff`、`consumePMFlowExecutionHandoff`、`createPMFlowExecutionProgressEvent`、`createPMFlowReleaseValidationRun`、`createPMFlowReleaseFeedbackIngest`、`createPMFlowThreadMessage`、`createPMFlowExtraction`、`getPMFlowChangeSet`、`acceptPMFlowChangeItem`、`editPMFlowChangeItem`、`deferPMFlowChangeItem`、`commitPMFlowChangeSet`;`PMFlowExecutionSummary.releaseGate` 是 ReleaseGrowth 真实发布门禁前端合同,`releaseGate.latestValidationRun` 是最新发布验证记录前端合同,`execution-summary.releaseSignals` 是 Release/Growth 发布后 feedback/revenue/recommendation 信号前端合同;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`
@ -107,8 +107,8 @@
- 个人空间首页 / 空间层入口前端产品域:参考 `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.releases / execution-summary.releaseGate`,发布验证缺失时显示明确空态,不虚构验证结果,不直接写 Project Memory、ChangeSet 状态、execution event 或 ReleaseValidationRun;暂存想法后端只读聚合尚未接入时必须显示 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/P2/P3 偏好合同:参考 `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`,把通知偏好映射为页面内提醒消费模型;P3 新增 `getReleaseGateNotificationPreference(settings, event)` 供 Release/Growth 后续消费:`release_gate_*` 与 `release_validation_*` 使用 `releaseGate` 偏好,`failed_event` 使用 `failureAlert` 偏好,返回 `inAppEnabled / desktopPreference / emailDigestPreference`;ReleaseGrowthPage 当前消费 `releaseGate` 与 `failureAlert` 两个偏好,只静默强调型提醒,不隐藏 release evidence、阻塞原因、审计状态或 validation run;这些 helper 只控制本地 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、release validation run create 均调后端并 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、阻塞原因、下一步动作和 `latestValidationRun`,缺证据时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run,但仍不执行部署命令;同时消费 Account notification preference helper,`releaseGate` 关闭时静默发布门禁强调提醒,`failureAlert` 关闭时静默失败强调提醒,所有真实证据和审计状态仍可见;静态高保真发布/增长数据仅保留在显式 demo 模式
- 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、release validation run create 与 release feedback ingest 均调后端并 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、阻塞原因、下一步动作、`latestValidationRun` 和 `releaseSignals`,缺证据或缺发布后信号时显示 empty/warning,不展示 mock 发布结果;页面可调用真实 API 创建 release validation run 和摄取 release feedback,但仍不执行部署命令、不自动创建 ChangeSet、不接外部支付;同时消费 Account notification preference helper,`releaseGate` 关闭时静默发布门禁强调提醒,`failureAlert` 关闭时静默失败强调提醒,所有真实证据和审计状态仍可见;静态高保真发布/增长数据仅保留在显式 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`

View File

@ -59,6 +59,7 @@ export interface PMFlowExecutionSummary {
projectId: string;
execution: PMFlowExecutionContext;
releaseGate: PMFlowReleaseGate;
releaseSignals?: PMFlowReleaseSignals;
devSync: Record<string, unknown>;
promptPackage?: PMFlowPromptPackage | null;
repoBindings: PMFlowRepoBinding[];
@ -145,6 +146,88 @@ export interface PMFlowReleaseValidationRunResult {
warnings: string[];
}
export interface PMFlowReleaseSignals {
status: "active" | "empty" | string;
relationship: {
releaseGateStatus?: string | null;
latestValidationRunId?: string | null;
latestValidationRunStatus?: string | null;
source?: string | null;
changeSetCreated?: boolean;
};
summary: {
feedbackCount?: number;
revenueEventCount?: number;
iterationRecommendationCount?: number;
latestSignalAt?: string | null;
};
feedback: PMFlowReleaseFeedbackIngest[];
revenueEvents: PMFlowReleaseRevenueEvent[];
recommendations: PMFlowIterationRecommendation[];
nextActions: string[];
warnings: string[];
}
export interface PMFlowReleaseFeedbackIngest {
id: string;
projectId: string;
charterId?: string | null;
sourceType: string;
title?: string | null;
content: string;
metrics: Record<string, unknown>;
samples: Array<Record<string, unknown>>;
status: string;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface PMFlowReleaseRevenueEvent {
id: string;
projectId: string;
sourceType: string;
category: string;
currency: string;
amountUsd: number;
externalRef?: string | null;
eventPayload: Record<string, unknown>;
occurredAt?: string | null;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface PMFlowIterationRecommendation {
id: string;
projectId: string;
charterId?: string | null;
feedbackIngestId?: string | null;
status: string;
priority: string;
recommendationSummary: Record<string, unknown>;
proposedCharterPatch: Record<string, unknown>;
proposedTasks: Array<Record<string, unknown>>;
changeSetCreated: boolean;
createdAt?: string | null;
updatedAt?: string | null;
}
export interface PMFlowReleaseFeedbackIngestRequest {
source_type?: string;
title?: string | null;
content: string;
charter_id?: string | null;
metrics?: Record<string, unknown>;
samples?: Array<Record<string, unknown>>;
}
export interface PMFlowReleaseFeedbackIngestResult {
ingest: PMFlowReleaseFeedbackIngest;
recommendation: PMFlowIterationRecommendation;
releaseSignals: PMFlowReleaseSignals;
executionSummary: PMFlowExecutionSummary;
warnings: string[];
}
export interface PMFlowExecutionHandoffRequest {
task_id: string;
repo_binding_id?: string | null;
@ -333,6 +416,13 @@ export async function createPMFlowReleaseValidationRun(
return post<PMFlowReleaseValidationRunResult>(`/pmflow-os/projects/${projectId}/release-validation-runs`, payload);
}
export async function createPMFlowReleaseFeedbackIngest(
projectId: string,
payload: PMFlowReleaseFeedbackIngestRequest
): Promise<PMFlowReleaseFeedbackIngestResult> {
return post<PMFlowReleaseFeedbackIngestResult>(`/pmflow-os/projects/${projectId}/release-feedback-ingests`, payload);
}
export async function createPMFlowThreadMessage(
projectId: string,
threadId: string,

View File

@ -19,10 +19,14 @@ import {
X,
} from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import type { ButtonHTMLAttributes, ReactNode } from "react";
import { useState, type ButtonHTMLAttributes, type ReactNode } from "react";
import { getAccountSettings, shouldShowAccountNotification } from "@/api/accountSettings";
import type {
PMFlowIterationRecommendation,
PMFlowReleaseFeedbackIngest,
PMFlowReleaseGateEvidence,
PMFlowReleaseRevenueEvent,
PMFlowReleaseSignals,
PMFlowReleaseValidationDecision,
PMFlowReleaseValidationRun,
} from "@/api/pmflowOs";
@ -378,6 +382,18 @@ function validationSummaryText(run?: PMFlowReleaseValidationRun | null) {
return `基于 ${sourceStatus} release gate 固化,部署执行:${summary.deploymentExecuted ? "已执行" : "未执行"}`;
}
function releaseSignalSummary(signals?: PMFlowReleaseSignals | null) {
if (!signals || signals.status === "empty") return "暂无真实发布后反馈、收入或合规信号。";
const summary = signals.summary || {};
return `反馈 ${summary.feedbackCount || 0} 条 · 收入事件 ${summary.revenueEventCount || 0} 条 · 迭代建议 ${summary.iterationRecommendationCount || 0} 条`;
}
function recommendationText(item: PMFlowIterationRecommendation) {
const summary = item.recommendationSummary || {};
const text = summary.summary || summary.recommendation || summary.next_step || summary.feedback_title;
return typeof text === "string" && text.trim() ? text : "已基于真实反馈生成下一轮建议,等待进入后续 iteration recommendation 主链。";
}
function checkTone(status?: string): StatusTone {
if (status === "passed") return "success";
if (status === "failed") return "danger";
@ -427,7 +443,44 @@ function EvidenceRow({ item }: { item: PMFlowReleaseGateEvidence }) {
);
}
function FeedbackSignalRow({ item }: { item: PMFlowReleaseFeedbackIngest }) {
const metrics = Object.entries(item.metrics || {});
return (
<div className="rounded-lg border border-[#e6edf7] bg-white p-4">
<div className="flex flex-wrap items-center gap-2">
<StatusChip label={item.sourceType || "manual"} tone="info" />
<span className="text-sm font-semibold text-slate-800">{item.title || "未命名反馈"}</span>
</div>
<p className="mt-2 text-sm leading-6 text-slate-600">{item.content}</p>
{metrics.length ? (
<div className="mt-3 flex flex-wrap gap-2">
{metrics.slice(0, 4).map(([key, value]) => (
<span key={key} className="rounded-md border border-[#e6edf7] bg-[#fbfdff] px-2 py-1 text-xs text-slate-500">
{key}: {String(value)}
</span>
))}
</div>
) : null}
<div className="mt-3 text-xs text-slate-400">{item.createdAt ? new Date(item.createdAt).toLocaleString("zh-CN") : "未记录时间"}</div>
</div>
);
}
function RevenueSignalRow({ item }: { item: PMFlowReleaseRevenueEvent }) {
return (
<div className="rounded-lg border border-[#e6edf7] bg-[#fbfdff] px-3 py-2 text-sm">
<div className="flex items-center justify-between gap-3">
<span className="font-semibold text-slate-700">{item.category || "revenue"}</span>
<span className="text-[#2463EB]">{item.currency} {item.amountUsd}</span>
</div>
<div className="mt-1 text-xs text-slate-400">{item.externalRef || item.sourceType || "manual"}</div>
</div>
);
}
function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
const [feedbackTitle, setFeedbackTitle] = useState("");
const [feedbackContent, setFeedbackContent] = useState("");
const {
state,
dataSourceMode,
@ -439,8 +492,10 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
projectionWarnings,
refetchExecutionSummary,
createReleaseValidationRun,
createReleaseFeedbackIngest,
} = store;
const gate = executionSummary?.releaseGate;
const releaseSignals = executionSummary?.releaseSignals;
const latestValidationRun = gate?.latestValidationRun;
const accountSettingsQuery = useQuery({
queryKey: ["account-settings", "release-growth-notifications"],
@ -456,6 +511,10 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
const showReleaseGateReminder = releaseGateReminderEnabled && !emptyMode;
const showFailureAlertReminder = failureAlertReminderEnabled && failureSignal;
const canCreateValidationRun = Boolean(!emptyMode && gate?.evidence?.length);
const hasReleaseSignals = Boolean(
releaseSignals &&
((releaseSignals.feedback || []).length || (releaseSignals.revenueEvents || []).length || (releaseSignals.recommendations || []).length)
);
const handleCreateValidationRun = (decision: PMFlowReleaseValidationDecision) => {
const decisionLabel = decision === "enter_release_candidate" ? "进入发布候选" : "保持阻塞";
createReleaseValidationRun({
@ -463,6 +522,19 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
note: `ReleaseGrowthPage 人工确认:${decisionLabel};PMFlow 仅记录发布前验证,不执行部署。`,
});
};
const handleCreateFeedback = () => {
const content = feedbackContent.trim();
if (!content) return;
createReleaseFeedbackIngest({
source_type: "manual",
title: feedbackTitle.trim() || "Release/Growth 人工反馈",
content,
metrics: {},
samples: [],
});
setFeedbackTitle("");
setFeedbackContent("");
};
return (
<div className="min-h-full bg-[#f7faff] text-slate-950">
@ -581,9 +653,67 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
{!gate?.evidence?.length ? <div className="rounded-lg border border-dashed border-[#d6e2f2] p-6 text-sm text-slate-500">暂无真实 execution evidence。</div> : null}
</div>
</Card>
<Card className="p-5">
<SectionTitle title="发布后反馈信号" icon={MessageSquareText} meta={releaseSignalSummary(releaseSignals)} />
<div className="mt-4 rounded-lg border border-[#e6edf7] bg-[#fbfdff] p-4 text-sm leading-6 text-slate-600">
<div className="font-semibold text-slate-800">信号关系</div>
<div className="mt-1">
release gate: {releaseSignals?.relationship?.releaseGateStatus || gate?.status || "empty"};validation run:{" "}
{releaseSignals?.relationship?.latestValidationRunId || "未固化"};本阶段不自动创建 ChangeSet。
</div>
</div>
<div className="mt-4 space-y-3">
{(releaseSignals?.feedback || []).map((item) => (
<FeedbackSignalRow key={item.id} item={item} />
))}
{(releaseSignals?.revenueEvents || []).map((item) => (
<RevenueSignalRow key={item.id} item={item} />
))}
{(releaseSignals?.recommendations || []).map((item) => (
<div key={item.id} className="rounded-lg border border-blue-100 bg-blue-50 px-4 py-3 text-sm leading-6 text-blue-700">
<div className="font-semibold">下一轮建议 · {item.priority}</div>
<div className="mt-1">{recommendationText(item)}</div>
</div>
))}
{!hasReleaseSignals ? (
<div className="rounded-lg border border-dashed border-[#d6e2f2] p-6 text-sm leading-6 text-slate-500">
暂无真实发布后信号。请摄取用户反馈、收入事件或合规检查结果;页面不会用 mock 成功数据冒充增长表现。
</div>
) : null}
</div>
</Card>
</main>
<aside className="space-y-5">
<Card className="p-5">
<SectionTitle title="摄取发布后反馈" icon={MessageSquareText} />
<div className="mt-4 space-y-3">
<input
value={feedbackTitle}
onChange={(event) => setFeedbackTitle(event.target.value)}
placeholder="反馈标题"
className="h-10 w-full rounded-lg border border-[#dbe6ff] bg-white px-3 text-sm outline-none focus:border-[#2463EB]"
/>
<textarea
value={feedbackContent}
onChange={(event) => setFeedbackContent(event.target.value)}
placeholder="记录真实用户反馈、运营观察或发布后问题..."
className="min-h-28 w-full resize-none rounded-lg border border-[#dbe6ff] bg-white px-3 py-2 text-sm leading-6 outline-none focus:border-[#2463EB]"
/>
<Button
className="h-10 w-full rounded-lg bg-[#2463EB] text-white hover:bg-[#1f57d1]"
disabled={!feedbackContent.trim() || isLoading}
onClick={handleCreateFeedback}
>
保存反馈信号
</Button>
<div className="text-xs leading-5 text-slate-500">
保存后会写入 FeedbackIngest 并生成 iteration recommendation 草稿;本阶段不创建 ChangeSet、不触发发布或外部推送。
</div>
</div>
</Card>
<Card className="p-5">
<SectionTitle title="阻塞原因" icon={AlertTriangle} />
<div className="mt-4 space-y-2">
@ -672,9 +802,9 @@ function RealReleaseGrowthPage({ store }: { store: PMFlowOsStoreSnapshot }) {
<Card className="p-5">
<SectionTitle title="运行警告" />
<div className="mt-4 space-y-2">
{warnings.length ? (
warnings.map((item) => (
<div key={item} className="rounded-lg border border-orange-100 bg-orange-50 px-3 py-2 text-xs leading-5 text-orange-700">
{[...warnings, ...(releaseSignals?.warnings || [])].length ? (
[...warnings, ...(releaseSignals?.warnings || [])].map((item, index) => (
<div key={`${item}-${index}`} className="rounded-lg border border-orange-100 bg-orange-50 px-3 py-2 text-xs leading-5 text-orange-700">
{item}
</div>
))

View File

@ -11,6 +11,7 @@ import {
createPMFlowExecutionHandoff,
createPMFlowExecutionProgressEvent,
createPMFlowReleaseValidationRun,
createPMFlowReleaseFeedbackIngest,
createPMFlowThreadMessage,
deferPMFlowChangeItem,
editPMFlowChangeItem,
@ -47,6 +48,7 @@ vi.mock("@/api/pmflowOs", () => ({
createPMFlowExecutionHandoff: vi.fn(),
createPMFlowExecutionProgressEvent: vi.fn(),
createPMFlowReleaseValidationRun: vi.fn(),
createPMFlowReleaseFeedbackIngest: vi.fn(),
editPMFlowChangeItem: vi.fn(),
deferPMFlowChangeItem: vi.fn(),
}));
@ -59,6 +61,7 @@ const createExecutionHandoffMock = vi.mocked(createPMFlowExecutionHandoff);
const consumeExecutionHandoffMock = vi.mocked(consumePMFlowExecutionHandoff);
const createExecutionProgressEventMock = vi.mocked(createPMFlowExecutionProgressEvent);
const createReleaseValidationRunMock = vi.mocked(createPMFlowReleaseValidationRun);
const createReleaseFeedbackIngestMock = vi.mocked(createPMFlowReleaseFeedbackIngest);
const acceptItemMock = vi.mocked(acceptPMFlowChangeItem);
const commitChangeSetMock = vi.mocked(commitPMFlowChangeSet);
const editItemMock = vi.mocked(editPMFlowChangeItem);
@ -579,6 +582,118 @@ describe("PMFlowOsProvider real change review actions", () => {
expect((await screen.findAllByText("测试失败")).length).toBeGreaterThan(0);
});
it("shows real release feedback signals and keeps empty state explicit", async () => {
getExecutionSummaryMock.mockResolvedValue({
...executionSummaryWithGate(releaseGate("ready")),
releaseSignals: {
status: "active",
relationship: {
releaseGateStatus: "ready",
latestValidationRunId: "900",
latestValidationRunStatus: "ready",
source: "pmflow_os_release_growth",
changeSetCreated: false,
},
summary: { feedbackCount: 1, revenueEventCount: 0, iterationRecommendationCount: 1, latestSignalAt: "2026-04-26T00:00:00" },
feedback: [
{
id: "feedback-1",
projectId: "123",
sourceType: "manual",
title: "首批试用反馈",
content: "用户反馈上传入口难找,希望下一轮优化 onboarding。",
metrics: { negative_feedback_count: 3 },
samples: [],
status: "active",
createdAt: "2026-04-26T00:00:00",
updatedAt: "2026-04-26T00:00:00",
},
],
revenueEvents: [],
recommendations: [
{
id: "rec-1",
projectId: "123",
feedbackIngestId: "feedback-1",
status: "draft",
priority: "high",
recommendationSummary: { summary: "优先优化 onboarding 上传入口。" },
proposedCharterPatch: {},
proposedTasks: [],
changeSetCreated: false,
createdAt: "2026-04-26T00:00:00",
updatedAt: "2026-04-26T00:00:00",
},
],
nextActions: ["基于真实反馈查看 iteration recommendation,但本阶段不自动创建 ChangeSet。"],
warnings: [],
},
});
const { default: ReleaseGrowthPage } = await import("@/features/pmflow-os/pages/ReleaseGrowthPage");
render(<ReleaseGrowthPage />, { wrapper: wrapper() });
expect(await screen.findByText("发布后反馈信号")).toBeTruthy();
expect(await screen.findByText("首批试用反馈")).toBeTruthy();
expect(screen.getByText("优先优化 onboarding 上传入口。")).toBeTruthy();
expect(screen.queryByText(/页面不会用 mock 成功数据/)).toBeNull();
});
it("writes release feedback through the PMFlow OS api in real mode", async () => {
createReleaseFeedbackIngestMock.mockResolvedValue({
ingest: {
id: "feedback-1",
projectId: "123",
sourceType: "manual",
title: "Release/Growth 人工反馈",
content: "用户反馈上传入口难找",
metrics: {},
samples: [],
status: "active",
},
recommendation: {
id: "rec-1",
projectId: "123",
feedbackIngestId: "feedback-1",
status: "draft",
priority: "high",
recommendationSummary: {},
proposedCharterPatch: {},
proposedTasks: [],
changeSetCreated: false,
},
releaseSignals: {
status: "active",
relationship: { releaseGateStatus: "ready", changeSetCreated: false },
summary: { feedbackCount: 1, revenueEventCount: 0, iterationRecommendationCount: 1 },
feedback: [],
revenueEvents: [],
recommendations: [],
nextActions: [],
warnings: [],
},
executionSummary: executionSummaryWithGate(releaseGate("ready")),
warnings: [],
});
const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() });
await waitFor(() => expect(result.current.dataSourceStatus).toBe("success"));
act(() =>
result.current.createReleaseFeedbackIngest({
source_type: "manual",
title: "Release/Growth 人工反馈",
content: "用户反馈上传入口难找",
})
);
await waitFor(() =>
expect(createReleaseFeedbackIngestMock).toHaveBeenCalledWith(
"123",
expect.objectContaining({ source_type: "manual", content: "用户反馈上传入口难找" })
)
);
});
it("still uses local demo mutation when demo mode is explicit", () => {
window.history.pushState({}, "", "/pmflow-os?demo=1");
const { result } = renderHook(() => usePMFlowOsStore(), { wrapper: wrapper() });
@ -591,6 +706,7 @@ describe("PMFlowOsProvider real change review actions", () => {
expect(consumeExecutionHandoffMock).not.toHaveBeenCalled();
expect(createExecutionProgressEventMock).not.toHaveBeenCalled();
expect(createReleaseValidationRunMock).not.toHaveBeenCalled();
expect(createReleaseFeedbackIngestMock).not.toHaveBeenCalled();
expect(getExecutionSummaryMock).not.toHaveBeenCalled();
expect(result.current.state.changeItems.find((item) => item.id === pending!.id)?.status).toBe("accepted");
});

View File

@ -7,6 +7,7 @@ import {
createPMFlowExecutionHandoff,
createPMFlowExecutionProgressEvent,
createPMFlowExtraction,
createPMFlowReleaseFeedbackIngest,
createPMFlowReleaseValidationRun,
createPMFlowThreadMessage,
deferPMFlowChangeItem,
@ -17,6 +18,7 @@ import {
type PMFlowExecutionProgressEventRequest,
type PMFlowExecutionSummary,
type PMFlowProposedChangeItem,
type PMFlowReleaseFeedbackIngestRequest,
type PMFlowReleaseValidationRunRequest,
} from "@/api/pmflowOs";
import { createInitialPMFlowState } from "@/features/pmflow-os/mockData";
@ -540,6 +542,7 @@ interface PMFlowOsStoreValue {
consumeExecutionHandoff: (promptPackageId: string) => void;
recordExecutionProgress: (payload: PMFlowExecutionProgressEventRequest) => void;
createReleaseValidationRun: (payload: PMFlowReleaseValidationRunRequest) => void;
createReleaseFeedbackIngest: (payload: PMFlowReleaseFeedbackIngestRequest) => void;
advanceExecution: () => void;
writeBackExecution: (outcome: "passed" | "failed") => void;
publishRelease: () => void;
@ -775,6 +778,13 @@ export function PMFlowOsProvider({ children }: { children: ReactNode }) {
runApiAction(() => createPMFlowReleaseValidationRun(dataSource.projectId || "", payload));
}
},
createReleaseFeedbackIngest: (payload) => {
if (dataSource.mode === "api" && dataSource.projectId) {
runApiAction(() => createPMFlowReleaseFeedbackIngest(dataSource.projectId || "", payload));
return;
}
mutateDemoOnly((current) => ingestGrowthSignalMutation(current));
},
advanceExecution: () => mutateDemoOnly((current) => advanceExecutionMutation(current)),
writeBackExecution: (outcome) => mutateDemoOnly((current) => writeBackExecutionMutation(current, outcome)),
publishRelease: () => mutateDemoOnly((current) => publishReleaseMutation(current)),