检索结果过程添加
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[] = [
|
const SSE_EVENT_TYPES: ProgressEventType[] = [
|
||||||
'connected', 'step_start', 'step_done',
|
'connected', 'step_start', 'step_done', 'research_progress',
|
||||||
'materials_ready', 'outline_chunk', 'outline_ready',
|
'materials_ready', 'outline_chunk', 'outline_ready',
|
||||||
'draft_chunk', 'draft_ready',
|
'draft_chunk', 'draft_ready',
|
||||||
'review_chunk', 'review_item_done', 'review_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 === 'writing_outline' && (state.outlineStreamingText || state.outlineThinkingText)) &&
|
||||||
!(state.status === 'reviewing' && state.streamingReviewItems.length > 0) && (
|
!(state.status === 'reviewing' && state.streamingReviewItems.length > 0) && (
|
||||||
<div className="px-4 pb-4">
|
<div className="px-4 pb-4">
|
||||||
<WritingSkeletonCard status={state.status} />
|
<WritingSkeletonCard status={state.status} researchProgress={state.researchProgress} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -376,7 +376,9 @@ export function AIWritingPanel() {
|
|||||||
{isRunning && (
|
{isRunning && (
|
||||||
<span className="inline-block size-1.5 rounded-full bg-blue-500 animate-pulse" />
|
<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>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import type { WritingStatus } from '../../types/ai-writing';
|
|||||||
|
|
||||||
interface WritingSkeletonCardProps {
|
interface WritingSkeletonCardProps {
|
||||||
status: WritingStatus;
|
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 (
|
return (
|
||||||
<div className="space-y-3 p-4">
|
<div className="space-y-3 p-4">
|
||||||
{/* 标题行 */}
|
{/* 标题行:展示素材收集专家的实时进度 */}
|
||||||
<div className="flex items-center gap-2 text-xs text-blue-500 font-medium">
|
<div className="flex items-center gap-2 text-xs text-blue-500 font-medium">
|
||||||
<ThinkingDots color="bg-blue-400" />
|
<ThinkingDots color="bg-blue-400" />
|
||||||
<span>素材收集专家搜索中…</span>
|
<span>{researchProgress || '素材收集专家搜索中…'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 关键词标签占位 */}
|
{/* 关键词标签占位 */}
|
||||||
@@ -198,21 +200,28 @@ function RevisingSkeleton() {
|
|||||||
|
|
||||||
// ── 状态映射 ──────────────────────────────────────────────────────────────────
|
// ── 状态映射 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// researching / awaiting_material_confirm 由 WritingSkeletonCard 单独处理(需透传进度文案)
|
||||||
const SKELETON_MAP: Partial<Record<WritingStatus, React.FC>> = {
|
const SKELETON_MAP: Partial<Record<WritingStatus, React.FC>> = {
|
||||||
// 实际运行状态
|
// 实际运行状态
|
||||||
researching: MaterialsSkeleton,
|
|
||||||
writing_outline: OutlineSkeleton,
|
writing_outline: OutlineSkeleton,
|
||||||
writing_draft: DraftSkeleton,
|
writing_draft: DraftSkeleton,
|
||||||
reviewing: ReviewSkeleton,
|
reviewing: ReviewSkeleton,
|
||||||
revising: RevisingSkeleton,
|
revising: RevisingSkeleton,
|
||||||
// 过渡等待状态(用户提交后、后端事件到达前的空白期)
|
// 过渡等待状态(用户提交后、后端事件到达前的空白期)
|
||||||
awaiting_material_confirm: MaterialsSkeleton, // 重新搜索 / 过渡到大纲
|
|
||||||
awaiting_outline_confirm: OutlineSkeleton, // 过渡到写作
|
awaiting_outline_confirm: OutlineSkeleton, // 过渡到写作
|
||||||
awaiting_draft_confirm: DraftSkeleton, // 提交修改意见或编辑审核后
|
awaiting_draft_confirm: DraftSkeleton, // 提交修改意见或编辑审核后
|
||||||
awaiting_review_confirm: RevisingSkeleton, // 采纳编辑意见后
|
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];
|
const Skeleton = SKELETON_MAP[status];
|
||||||
if (!Skeleton) return null;
|
if (!Skeleton) return null;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ const INITIAL_STATE: AIWritingState = {
|
|||||||
streamingDraftSections: [],
|
streamingDraftSections: [],
|
||||||
outlineStreamingText: '',
|
outlineStreamingText: '',
|
||||||
outlineThinkingText: '',
|
outlineThinkingText: '',
|
||||||
|
researchProgress: '',
|
||||||
streamingReviewItems: [],
|
streamingReviewItems: [],
|
||||||
revisionSummary: null,
|
revisionSummary: null,
|
||||||
pendingRevisionContext: null,
|
pendingRevisionContext: null,
|
||||||
@@ -64,14 +65,19 @@ export function useAIWriting() {
|
|||||||
case 'step_done':
|
case 'step_done':
|
||||||
return base;
|
return base;
|
||||||
|
|
||||||
|
case 'research_progress':
|
||||||
|
// 素材收集专家实时进度:仅更新最新提示文案
|
||||||
|
return { ...base, researchProgress: event.message ?? '' };
|
||||||
|
|
||||||
case 'resumed':
|
case 'resumed':
|
||||||
return { ...base, currentPause: null };
|
return { ...base, currentPause: null };
|
||||||
|
|
||||||
case 'materials_ready':
|
case 'materials_ready':
|
||||||
// 仅缓存素材数据,不弹干预卡(等 await_user 事件再弹)
|
// 仅缓存素材数据,不弹干预卡(等 await_user 事件再弹);进度文案已完成可清空
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
materials: (event.materials ?? []) as AIMaterial[],
|
materials: (event.materials ?? []) as AIMaterial[],
|
||||||
|
researchProgress: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
case 'outline_chunk': {
|
case 'outline_chunk': {
|
||||||
@@ -345,6 +351,7 @@ export function useAIWriting() {
|
|||||||
currentPause: null,
|
currentPause: null,
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
// Clear stale streaming state so new-cycle detection in chunk handlers works correctly
|
// Clear stale streaming state so new-cycle detection in chunk handlers works correctly
|
||||||
|
...(nextStatus === 'researching' ? { researchProgress: '' } : {}),
|
||||||
...(nextStatus === 'writing_outline' ? { outlineStreamingText: '', outlineThinkingText: '' } : {}),
|
...(nextStatus === 'writing_outline' ? { outlineStreamingText: '', outlineThinkingText: '' } : {}),
|
||||||
...(nextStatus === 'writing_draft' ? { streamingDraftSections: [], streamingDraftTitle: '' } : {}),
|
...(nextStatus === 'writing_draft' ? { streamingDraftSections: [], streamingDraftTitle: '' } : {}),
|
||||||
...(nextStatus === 'reviewing' ? { streamingReviewItems: [] } : {}),
|
...(nextStatus === 'reviewing' ? { streamingReviewItems: [] } : {}),
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export type ProgressEventType =
|
|||||||
| 'connected'
|
| 'connected'
|
||||||
| 'step_start'
|
| 'step_start'
|
||||||
| 'step_done'
|
| 'step_done'
|
||||||
|
| 'research_progress'
|
||||||
| 'materials_ready'
|
| 'materials_ready'
|
||||||
| 'outline_chunk'
|
| 'outline_chunk'
|
||||||
| 'outline_ready'
|
| 'outline_ready'
|
||||||
@@ -201,6 +202,8 @@ export interface AIWritingState {
|
|||||||
// 流式大纲(正文 / 思考分开)
|
// 流式大纲(正文 / 思考分开)
|
||||||
outlineStreamingText: string;
|
outlineStreamingText: string;
|
||||||
outlineThinkingText: string;
|
outlineThinkingText: string;
|
||||||
|
// 素材收集实时进度(最新一条提示文案)
|
||||||
|
researchProgress: string;
|
||||||
// 流式审核(逐条到达)
|
// 流式审核(逐条到达)
|
||||||
streamingReviewItems: StreamingReviewItem[];
|
streamingReviewItems: StreamingReviewItem[];
|
||||||
// 修改摘要
|
// 修改摘要
|
||||||
|
|||||||
+34
-1
@@ -30,7 +30,21 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
intervention = state.get("last_user_intervention") or {}
|
intervention = state.get("last_user_intervention") or {}
|
||||||
extra_keywords: list[str] = intervention.get("_extra_keywords") or intervention.get("extra_keywords") 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: 提取关键词
|
# Step 1: 提取关键词
|
||||||
|
await _emit("正在生成检索词…")
|
||||||
kw_result = await llm_json(
|
kw_result = await llm_json(
|
||||||
EXTRACT_KEYWORDS_SYSTEM,
|
EXTRACT_KEYWORDS_SYSTEM,
|
||||||
EXTRACT_KEYWORDS_USER.format(
|
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", {})
|
ai_writing_config = config.get("configurable", {}).get("ai_writing_config", {})
|
||||||
provider = get_search_provider(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)
|
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 排序)
|
# Step 3: 相关度排序(如素材较多,让 LLM 排序)
|
||||||
|
await _emit("检索完成,正在筛选排序素材…")
|
||||||
if len(raw_materials) > _MAX_MATERIALS:
|
if len(raw_materials) > _MAX_MATERIALS:
|
||||||
materials_list = "\n".join(
|
materials_list = "\n".join(
|
||||||
f"- ID: {m['id']} 标题: {m['title']} 摘要: {m['content'][:100]}"
|
f"- ID: {m['id']} 标题: {m['title']} 摘要: {m['content'][:100]}"
|
||||||
|
|||||||
Reference in new issue
Block a user