检索结果过程添加
This commit is contained in:
1 parent
20786e690d
commit
166016a741
6 files changed
+65
-11
No files matched your search
@@ -29,7 +29,7 @@ export async function startWritingSession(req: AIWritingRequest): Promise<string
|
||||
}
|
||||
|
||||
const SSE_EVENT_TYPES: ProgressEventType[] = [
|
||||
'connected', 'step_start', 'step_done',
|
||||
'connected', 'step_start', 'step_done', 'research_progress',
|
||||
'materials_ready', 'outline_chunk', 'outline_ready',
|
||||
'draft_chunk', 'draft_ready',
|
||||
'review_chunk', 'review_item_done', 'review_ready',
|
||||
|
||||
@@ -327,7 +327,7 @@ export function AIWritingPanel() {
|
||||
!(state.status === 'writing_outline' && (state.outlineStreamingText || state.outlineThinkingText)) &&
|
||||
!(state.status === 'reviewing' && state.streamingReviewItems.length > 0) && (
|
||||
<div className="px-4 pb-4">
|
||||
<WritingSkeletonCard status={state.status} />
|
||||
<WritingSkeletonCard status={state.status} researchProgress={state.researchProgress} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -376,7 +376,9 @@ export function AIWritingPanel() {
|
||||
{isRunning && (
|
||||
<span className="inline-block size-1.5 rounded-full bg-blue-500 animate-pulse" />
|
||||
)}
|
||||
{STATUS_LABEL[state.status] ?? state.status}
|
||||
{state.status === 'researching' && state.researchProgress
|
||||
? state.researchProgress
|
||||
: STATUS_LABEL[state.status] ?? state.status}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<div className="space-y-3 p-4">
|
||||
{/* 标题行 */}
|
||||
{/* 标题行:展示素材收集专家的实时进度 */}
|
||||
<div className="flex items-center gap-2 text-xs text-blue-500 font-medium">
|
||||
<ThinkingDots color="bg-blue-400" />
|
||||
<span>素材收集专家搜索中…</span>
|
||||
<span>{researchProgress || '素材收集专家搜索中…'}</span>
|
||||
</div>
|
||||
|
||||
{/* 关键词标签占位 */}
|
||||
@@ -198,21 +200,28 @@ function RevisingSkeleton() {
|
||||
|
||||
// ── 状态映射 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// researching / awaiting_material_confirm 由 WritingSkeletonCard 单独处理(需透传进度文案)
|
||||
const SKELETON_MAP: Partial<Record<WritingStatus, React.FC>> = {
|
||||
// 实际运行状态
|
||||
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 (
|
||||
<div className="rounded-xl border bg-card shadow-sm overflow-hidden">
|
||||
<MaterialsSkeleton researchProgress={researchProgress} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const Skeleton = SKELETON_MAP[status];
|
||||
if (!Skeleton) return null;
|
||||
return (
|
||||
|
||||
@@ -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: [] } : {}),
|
||||
|
||||
@@ -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[];
|
||||
// 修改摘要
|
||||
|
||||
+34
-1
@@ -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]}"
|
||||
|
||||
Reference in new issue
Block a user