From a484c808d3e7cce3f05e678fd8de131fa219b15a Mon Sep 17 00:00:00 2001 From: weichen Date: Sat, 12 Sep 2026 03:14:30 +0000 Subject: [PATCH] feat(web): poll stage ready notifications to refresh advance banner. Poll project notifications every 15s and reload readiness when stage_ready_to_advance arrives. Co-authored-by: Cursor --- ...project-workflow-governance-test-report.md | 11 ++++++- web/src/api/projectSettings.ts | 21 ++++++++++++ .../workflow/StageAdvanceBanner.tsx | 32 ++++++++++++++++++- web/src/types/projectSettings.ts | 17 ++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/.doc/test-reports/2026-09-12-phase3-project-workflow-governance-test-report.md b/.doc/test-reports/2026-09-12-phase3-project-workflow-governance-test-report.md index c7e16047..5a394172 100644 --- a/.doc/test-reports/2026-09-12-phase3-project-workflow-governance-test-report.md +++ b/.doc/test-reports/2026-09-12-phase3-project-workflow-governance-test-report.md @@ -94,7 +94,8 @@ ok chat-agent-system/internal/server 0.792s (TestProjectWorkflow*) - **2026-09-12 续(顺序推荐执行)**: 1. ✅ **PG 迁移 + 集成实测**(`00017` 已升至 v17;`TestPG_ActivationAndStageAdvance` PASS) 2. ✅ **阶段级 ABAC MVP**(文档 list/content 按 active 阶段 + 三权过滤;`TestResolveStageAccess_ActiveStageDutyOnly` PASS) - 3. ⏳ **Phase 4 通知**(文档批准后自动刷新 readiness、Outbox、聊天/SSE 全面接 ABAC) + 3. ✅ **Phase 4 MVP 通知**(`00018` Outbox + 去重通知;文档批准联动 readiness;前端 15s 轮询刷新推进卡片) + 4. ⏳ **Phase 4 续**(聊天/搜索/SSE 全面接 ABAC;24h 提醒升级;真实 Agent E2E) ### 顺序影响说明 @@ -105,3 +106,11 @@ ok chat-agent-system/internal/server 0.792s (TestProjectWorkflow*) | 3 | Outbox/自动刷新 | 稳定 stage gate + ABAC | 通知推送后仍可能越权读文档,验收失真 | **推荐路径**:先固化数据面(迁移实测)→ 再补安全门禁(ABAC)→ 最后做体验增强(通知)。 + +### Phase 4 MVP 实测(2026-09-12) + +| 用例 | 判定 | 说明 | +|------|------|------| +| `TestHandleDocumentApproved_StageReady` | PASS | 文档批准后同步 task 状态并评估 stage ready | +| `TestPG_EmitStageReadyDedupe` | PASS | `(stage_instance_id, readiness_revision)` 去重,重复 emit 不产生第二条通知 | +| `TestPG_ActivationAndStageAdvance`(含 notify 段) | PASS | 批准 → HandleDocumentApproved → EmitStageReadyToAdvance 全链路 | diff --git a/web/src/api/projectSettings.ts b/web/src/api/projectSettings.ts index 93f52ab4..28f6161a 100644 --- a/web/src/api/projectSettings.ts +++ b/web/src/api/projectSettings.ts @@ -5,6 +5,7 @@ import { ActivateResult, StageReadiness, AdvanceStageResult, + ProjectNotification, } from '../types/projectSettings'; import { getBearerAuthHeaders } from './authHeaders'; @@ -109,6 +110,26 @@ export async function fetchStageReadiness( return res.json(); } +export async function fetchProjectNotifications( + projectId: number, + sinceId?: number +): Promise { + const params = new URLSearchParams(); + if (sinceId && sinceId > 0) params.set('since_id', String(sinceId)); + const qs = params.toString(); + const res = await fetch( + `${API_BASE}/api/v1/projects/${projectId}/notifications${qs ? `?${qs}` : ''}`, + { headers: getBearerAuthHeaders() } + ); + if (!res.ok) { + if (res.status === 503) return []; + const text = await res.text(); + throw new Error(parseError(text) || `获取项目通知失败: ${res.status}`); + } + const data = await res.json(); + return Array.isArray(data.items) ? data.items : []; +} + export async function advanceStage( instanceId: number, stageKey: string, diff --git a/web/src/components/workflow/StageAdvanceBanner.tsx b/web/src/components/workflow/StageAdvanceBanner.tsx index b9ac3fa5..fc4f5297 100644 --- a/web/src/components/workflow/StageAdvanceBanner.tsx +++ b/web/src/components/workflow/StageAdvanceBanner.tsx @@ -1,6 +1,11 @@ import React, { useCallback, useEffect, useState } from 'react'; import { ArrowRight, CheckCircle2, AlertTriangle, Loader2 } from 'lucide-react'; -import { fetchProjectSettings, fetchStageReadiness, advanceStage } from '../../api/projectSettings'; +import { + fetchProjectSettings, + fetchStageReadiness, + fetchProjectNotifications, + advanceStage, +} from '../../api/projectSettings'; import { StageReadiness } from '../../types/projectSettings'; interface StageAdvanceBannerProps { @@ -19,6 +24,7 @@ export const StageAdvanceBanner: React.FC = ({ const [error, setError] = useState(null); const [instanceId, setInstanceId] = useState(null); const [readiness, setReadiness] = useState(null); + const [lastNotificationId, setLastNotificationId] = useState(0); const load = useCallback(async () => { setLoading(true); @@ -47,6 +53,30 @@ export const StageAdvanceBanner: React.FC = ({ load(); }, [load]); + useEffect(() => { + let cancelled = false; + const poll = async () => { + try { + const items = await fetchProjectNotifications(projectId, lastNotificationId); + if (cancelled || items.length === 0) return; + const stageReady = items.some((n) => n.type === 'stage_ready_to_advance'); + const maxId = items.reduce((max, n) => Math.max(max, n.id), lastNotificationId); + setLastNotificationId(maxId); + if (stageReady) { + await load(); + } + } catch { + // 通知服务未就绪时静默降级,仍依赖手动刷新 + } + }; + const timer = window.setInterval(poll, 15000); + poll(); + return () => { + cancelled = true; + window.clearInterval(timer); + }; + }, [projectId, lastNotificationId, load]); + const handleAdvance = async () => { if (!readiness || !instanceId || !readiness.can_advance) return; setAdvancing(true); diff --git a/web/src/types/projectSettings.ts b/web/src/types/projectSettings.ts index ef31aa59..bf2ebb43 100644 --- a/web/src/types/projectSettings.ts +++ b/web/src/types/projectSettings.ts @@ -70,6 +70,23 @@ export interface ActivateResult { active_workflow_key: string; } +export interface ProjectNotification { + id: number; + user_id: number; + project_id: number; + type: string; + title: string; + body: string; + status: string; + created_at: string; + payload?: { + stage_key?: string; + stage_name?: string; + config_revision?: number; + workflow_instance_id?: number; + }; +} + export interface StageReadiness { instance_id: number; stage_key: string;