From 166016a741d2eff451fe13120d289588d1030a8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Sun, 17 May 2026 22:01:29 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A3=80=E7=B4=A2=E7=BB=93=E6=9E=9C=E8=BF=87?= =?UTF-8?q?=E7=A8=8B=E6=B7=BB=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/open-canvas/api/ai-writing.ts | 2 +- .../components/ai-writing/AIWritingPanel.tsx | 6 ++-- .../ai-writing/WritingSkeletonCard.tsx | 21 +++++++---- .../src/open-canvas/hooks/useAIWriting.ts | 9 ++++- .../src/open-canvas/types/ai-writing.ts | 3 ++ .../agents/ai_writing/nodes/researcher.py | 35 ++++++++++++++++++- 6 files changed, 65 insertions(+), 11 deletions(-) diff --git a/frontend-web/src/open-canvas/api/ai-writing.ts b/frontend-web/src/open-canvas/api/ai-writing.ts index ac475dc2..31b4f4b5 100644 --- a/frontend-web/src/open-canvas/api/ai-writing.ts +++ b/frontend-web/src/open-canvas/api/ai-writing.ts @@ -29,7 +29,7 @@ export async function startWritingSession(req: AIWritingRequest): Promise 0) && (
- +
)} @@ -376,7 +376,9 @@ export function AIWritingPanel() { {isRunning && ( )} - {STATUS_LABEL[state.status] ?? state.status} + {state.status === 'researching' && state.researchProgress + ? state.researchProgress + : STATUS_LABEL[state.status] ?? state.status} )} diff --git a/frontend-web/src/open-canvas/components/ai-writing/WritingSkeletonCard.tsx b/frontend-web/src/open-canvas/components/ai-writing/WritingSkeletonCard.tsx index 3d19f61c..d68414f4 100644 --- a/frontend-web/src/open-canvas/components/ai-writing/WritingSkeletonCard.tsx +++ b/frontend-web/src/open-canvas/components/ai-writing/WritingSkeletonCard.tsx @@ -3,6 +3,8 @@ import type { WritingStatus } from '../../types/ai-writing'; interface WritingSkeletonCardProps { status: WritingStatus; + /** 素材收集阶段的实时进度文案(仅 researching 阶段使用) */ + researchProgress?: string; } // ── 基础骨架元素 ────────────────────────────────────────────────────────────── @@ -29,13 +31,13 @@ function ThinkingDots({ color = 'bg-blue-400' }: { color?: string }) { // ── 素材收集骨架屏 ──────────────────────────────────────────────────────────── -function MaterialsSkeleton() { +function MaterialsSkeleton({ researchProgress }: { researchProgress?: string }) { return (
- {/* 标题行 */} + {/* 标题行:展示素材收集专家的实时进度 */}
- 素材收集专家搜索中… + {researchProgress || '素材收集专家搜索中…'}
{/* 关键词标签占位 */} @@ -198,21 +200,28 @@ function RevisingSkeleton() { // ── 状态映射 ────────────────────────────────────────────────────────────────── +// researching / awaiting_material_confirm 由 WritingSkeletonCard 单独处理(需透传进度文案) const SKELETON_MAP: Partial> = { // 实际运行状态 - researching: MaterialsSkeleton, writing_outline: OutlineSkeleton, writing_draft: DraftSkeleton, reviewing: ReviewSkeleton, revising: RevisingSkeleton, // 过渡等待状态(用户提交后、后端事件到达前的空白期) - awaiting_material_confirm: MaterialsSkeleton, // 重新搜索 / 过渡到大纲 awaiting_outline_confirm: OutlineSkeleton, // 过渡到写作 awaiting_draft_confirm: DraftSkeleton, // 提交修改意见或编辑审核后 awaiting_review_confirm: RevisingSkeleton, // 采纳编辑意见后 }; -export function WritingSkeletonCard({ status }: WritingSkeletonCardProps) { +export function WritingSkeletonCard({ status, researchProgress }: WritingSkeletonCardProps) { + // 素材收集阶段单独渲染,以便透传实时进度文案 + if (status === 'researching' || status === 'awaiting_material_confirm') { + return ( +
+ +
+ ); + } const Skeleton = SKELETON_MAP[status]; if (!Skeleton) return null; return ( diff --git a/frontend-web/src/open-canvas/hooks/useAIWriting.ts b/frontend-web/src/open-canvas/hooks/useAIWriting.ts index 8293d3e1..994d7d4c 100644 --- a/frontend-web/src/open-canvas/hooks/useAIWriting.ts +++ b/frontend-web/src/open-canvas/hooks/useAIWriting.ts @@ -34,6 +34,7 @@ const INITIAL_STATE: AIWritingState = { streamingDraftSections: [], outlineStreamingText: '', outlineThinkingText: '', + researchProgress: '', streamingReviewItems: [], revisionSummary: null, pendingRevisionContext: null, @@ -64,14 +65,19 @@ export function useAIWriting() { case 'step_done': return base; + case 'research_progress': + // 素材收集专家实时进度:仅更新最新提示文案 + return { ...base, researchProgress: event.message ?? '' }; + case 'resumed': return { ...base, currentPause: null }; case 'materials_ready': - // 仅缓存素材数据,不弹干预卡(等 await_user 事件再弹) + // 仅缓存素材数据,不弹干预卡(等 await_user 事件再弹);进度文案已完成可清空 return { ...base, materials: (event.materials ?? []) as AIMaterial[], + researchProgress: '', }; case 'outline_chunk': { @@ -345,6 +351,7 @@ export function useAIWriting() { currentPause: null, status: nextStatus, // Clear stale streaming state so new-cycle detection in chunk handlers works correctly + ...(nextStatus === 'researching' ? { researchProgress: '' } : {}), ...(nextStatus === 'writing_outline' ? { outlineStreamingText: '', outlineThinkingText: '' } : {}), ...(nextStatus === 'writing_draft' ? { streamingDraftSections: [], streamingDraftTitle: '' } : {}), ...(nextStatus === 'reviewing' ? { streamingReviewItems: [] } : {}), diff --git a/frontend-web/src/open-canvas/types/ai-writing.ts b/frontend-web/src/open-canvas/types/ai-writing.ts index 938dabdc..eb051a3f 100644 --- a/frontend-web/src/open-canvas/types/ai-writing.ts +++ b/frontend-web/src/open-canvas/types/ai-writing.ts @@ -67,6 +67,7 @@ export type ProgressEventType = | 'connected' | 'step_start' | 'step_done' + | 'research_progress' | 'materials_ready' | 'outline_chunk' | 'outline_ready' @@ -201,6 +202,8 @@ export interface AIWritingState { // 流式大纲(正文 / 思考分开) outlineStreamingText: string; outlineThinkingText: string; + // 素材收集实时进度(最新一条提示文案) + researchProgress: string; // 流式审核(逐条到达) streamingReviewItems: StreamingReviewItem[]; // 修改摘要 diff --git a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py index 7d73a7ed..59639aa2 100644 --- a/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py +++ b/offline-backend-20260512/backend/packages/harness/deerflow/agents/ai_writing/nodes/researcher.py @@ -30,7 +30,21 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict intervention = state.get("last_user_intervention") or {} extra_keywords: list[str] = intervention.get("_extra_keywords") or intervention.get("extra_keywords") or [] + # SSE 实时进度推送(与 outline/draft 节点共用 sse_queue 机制) + cfg = config.get("configurable", {}) + sse_queue = cfg.get("sse_queue") + sse_session_id: str = cfg.get("sse_session_id", "") + + async def _emit(message: str) -> None: + if sse_queue is None: + return + await sse_queue.put(("research_progress", { + "session_id": sse_session_id, + "message": message, + })) + # Step 1: 提取关键词 + await _emit("正在生成检索词…") kw_result = await llm_json( EXTRACT_KEYWORDS_SYSTEM, EXTRACT_KEYWORDS_USER.format( @@ -49,7 +63,25 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict ai_writing_config = config.get("configurable", {}).get("ai_writing_config", {}) provider = get_search_provider(ai_writing_config) - search_tasks = [provider.search(kw, max_results=3) for kw in keywords[:6]] + search_keywords = keywords[:6] + total = len(search_keywords) + await _emit(f"检索词已生成,开始检索 {total} 个方向…") + + # 并发搜索,但每完成一个就推送进度(计数在单线程 asyncio 下无需加锁) + completed = 0 + + async def _search_one(kw: str) -> list[SearchResult]: + nonlocal completed + try: + return await provider.search(kw, max_results=3) + finally: + completed += 1 + if completed < total: + await _emit(f"已完成 {completed}/{total} 次检索,正在继续检索相关内容…") + else: + await _emit(f"已完成全部 {total} 次检索") + + search_tasks = [_search_one(kw) for kw in search_keywords] all_results_nested: list[list[SearchResult]] = await asyncio.gather(*search_tasks, return_exceptions=True) # 合并去重 @@ -82,6 +114,7 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict )) # Step 3: 相关度排序(如素材较多,让 LLM 排序) + await _emit("检索完成,正在筛选排序素材…") if len(raw_materials) > _MAX_MATERIALS: materials_list = "\n".join( f"- ID: {m['id']} 标题: {m['title']} 摘要: {m['content'][:100]}"