mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-21 21:47:00 +08:00
refactor: 抽离 AI 搜索运行生命周期
This commit is contained in:
@@ -1,15 +1,10 @@
|
||||
import React, { useMemo, useRef, useState } from 'react'
|
||||
import * as Popover from '@radix-ui/react-popover'
|
||||
import { aiSearchIntentLabel, aiSearchRangeStart } from '../../../../shared/ai-search'
|
||||
import type {
|
||||
AiSearchAgentRun,
|
||||
AiSearchProgressEvent,
|
||||
AiSearchTimeRange
|
||||
} from '../../../../shared/ai-search'
|
||||
import type { AiSearchProgressEvent, AiSearchTimeRange } from '../../../../shared/ai-search'
|
||||
|
||||
import type {
|
||||
AISearchWorkspaceProps,
|
||||
SearchProgressByStage,
|
||||
SearchRange,
|
||||
SearchScope,
|
||||
SearchStage,
|
||||
@@ -42,6 +37,7 @@ import { useSearchHistory } from './hooks/useSearchHistory'
|
||||
import { useKnowledgeStatus } from './hooks/useKnowledgeStatus'
|
||||
import { useExternalProviderConsent } from './hooks/useExternalProviderConsent'
|
||||
import { EVIDENCE_PAGE_SIZE, useEvidenceCollection } from './hooks/useEvidenceCollection'
|
||||
import { useAiSearchRun } from './hooks/useAiSearchRun'
|
||||
|
||||
export function AISearchWorkspace({
|
||||
contacts,
|
||||
@@ -67,15 +63,12 @@ export function AISearchWorkspace({
|
||||
const [senderNames, setSenderNames] = useState<Record<string, string>>({})
|
||||
const [cachedAt, setCachedAt] = useState(0)
|
||||
const [searchTrace, setSearchTrace] = useState<SearchTrace | null>(null)
|
||||
const [searchProgress, setSearchProgress] = useState<SearchProgressByStage>({})
|
||||
const [agentTrace, setAgentTrace] = useState<AiSearchAgentRun['trace']>([])
|
||||
const [searchDetailsOpen, setSearchDetailsOpen] = useState(false)
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [debugEnabled, setDebugEnabled] = useState(false)
|
||||
const [debugPanelOpen, setDebugPanelOpen] = useState(false)
|
||||
const [debugEntries, setDebugEntries] = useState<string[]>([])
|
||||
const [appLogPath, setAppLogPath] = useState('')
|
||||
const searchRequestIdRef = useRef('')
|
||||
const composerRef = useRef<HTMLTextAreaElement>(null)
|
||||
const {
|
||||
evidence,
|
||||
@@ -150,6 +143,15 @@ export function AISearchWorkspace({
|
||||
settleExternalProviderConsent,
|
||||
clearExternalProviderConsent
|
||||
} = useExternalProviderConsent()
|
||||
const {
|
||||
requestId: searchRunRequestId,
|
||||
progress: searchProgress,
|
||||
agentTrace,
|
||||
createRequestId,
|
||||
startSearch,
|
||||
cancelSearch,
|
||||
resetSearchRun
|
||||
} = useAiSearchRun()
|
||||
|
||||
const resetSearchResult = (): void => {
|
||||
const reset = createSearchResultResetState()
|
||||
@@ -158,8 +160,7 @@ export function AISearchWorkspace({
|
||||
clearEvidenceCollection()
|
||||
setCachedAt(reset.cachedAt)
|
||||
setSearchTrace(reset.searchTrace)
|
||||
setSearchProgress(reset.searchProgress)
|
||||
setAgentTrace(reset.agentTrace)
|
||||
resetSearchRun()
|
||||
setSearchDetailsOpen(reset.searchDetailsOpen)
|
||||
}
|
||||
|
||||
@@ -172,22 +173,6 @@ export function AISearchWorkspace({
|
||||
)
|
||||
}, [])
|
||||
|
||||
React.useEffect(
|
||||
() =>
|
||||
window.api.onAiSearchProgress((progress) => {
|
||||
if (progress.requestId !== searchRequestIdRef.current) return
|
||||
setSearchProgress((current) => ({ ...current, [progress.stage]: progress }))
|
||||
if (progress.agentTrace) {
|
||||
setAgentTrace((current) =>
|
||||
current.some((item) => item.sequence === progress.agentTrace?.sequence)
|
||||
? current
|
||||
: [...current, progress.agentTrace as AiSearchAgentRun['trace'][number]]
|
||||
)
|
||||
}
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
const addDebugEntry = (message: string, details: Record<string, unknown> = {}): void => {
|
||||
const entry = `${new Date().toLocaleTimeString('zh-CN')} ${message} ${JSON.stringify(details)}`
|
||||
setDebugEntries((current) => [entry, ...current].slice(0, 80))
|
||||
@@ -235,18 +220,15 @@ export function AISearchWorkspace({
|
||||
|
||||
const cancelAnalysis = async (): Promise<void> => {
|
||||
clearExternalProviderConsent()
|
||||
const requestId = searchRequestIdRef.current
|
||||
const requestId = searchRunRequestId
|
||||
if (!requestId) return
|
||||
searchRequestIdRef.current = ''
|
||||
setStage('idle')
|
||||
setAnalysisError('')
|
||||
setSearchProgress({})
|
||||
setAgentTrace([])
|
||||
setSearchDetailsOpen(false)
|
||||
onNotice('已取消本次分析')
|
||||
composerRef.current?.focus()
|
||||
try {
|
||||
await window.api.cancelAiSearch(requestId)
|
||||
await cancelSearch()
|
||||
} catch (error) {
|
||||
addDebugEntry('取消检索请求失败', {
|
||||
requestId,
|
||||
@@ -284,7 +266,6 @@ export function AISearchWorkspace({
|
||||
effectiveRange,
|
||||
normalizedQuery
|
||||
)
|
||||
let requestId = ''
|
||||
try {
|
||||
const cached = consumeCacheBypass() ? null : readCachedResult(cacheKey)
|
||||
if (cached) {
|
||||
@@ -298,7 +279,7 @@ export function AISearchWorkspace({
|
||||
onNotice('已使用最近的检索缓存,可点击刷新数据读取最新消息')
|
||||
return
|
||||
}
|
||||
requestId = globalThis.crypto?.randomUUID?.() || `search-${Date.now()}`
|
||||
const requestId = createRequestId()
|
||||
try {
|
||||
if (!(await ensureAiSearchDataConsent(requestId))) {
|
||||
onNotice('已取消本次 AI Search,未执行检索,也未向远程 AI 服务发送聊天内容')
|
||||
@@ -314,8 +295,7 @@ export function AISearchWorkspace({
|
||||
}
|
||||
setStage('loading')
|
||||
resetSearchResult()
|
||||
searchRequestIdRef.current = requestId
|
||||
const searchResult = await window.api.runAiSearch({
|
||||
const outcome = await startSearch({
|
||||
requestId,
|
||||
text: normalizedQuery,
|
||||
scope,
|
||||
@@ -323,7 +303,19 @@ export function AISearchWorkspace({
|
||||
conversationId: scope === 'conversation' ? activeContact?.md5 : undefined,
|
||||
timeRangeOverride: effectiveTimeRangeOverride
|
||||
})
|
||||
if (searchRequestIdRef.current !== requestId) return
|
||||
if (outcome.kind === 'stale') return
|
||||
if (outcome.kind === 'cancelled') {
|
||||
onNotice('已取消本次分析')
|
||||
setStage('idle')
|
||||
return
|
||||
}
|
||||
if (outcome.kind === 'failed') {
|
||||
addDebugEntry('检索失败', { error: outcome.error })
|
||||
setAnalysisError(outcome.error)
|
||||
setStage('insufficient')
|
||||
return
|
||||
}
|
||||
const searchResult = outcome.result
|
||||
addDebugEntry('主进程搜索任务完成', {
|
||||
status: searchResult.status,
|
||||
candidateEvidenceCount: searchResult.candidateEvidenceCount,
|
||||
@@ -331,18 +323,12 @@ export function AISearchWorkspace({
|
||||
elapsedMs: searchResult.elapsedMs,
|
||||
errorStage: searchResult.errorStage
|
||||
})
|
||||
if (searchResult.status === 'cancelled') {
|
||||
onNotice('已取消本次分析')
|
||||
setStage('idle')
|
||||
return
|
||||
}
|
||||
const evidenceItems = mapPipelineEvidence(searchResult.evidence, allContacts)
|
||||
const collectionItems = mapPipelineEvidence(
|
||||
searchResult.evidenceCollection || searchResult.evidence,
|
||||
allContacts
|
||||
)
|
||||
setSearchTrace(mapSearchResultToTrace(searchResult, evidenceItems.length))
|
||||
setAgentTrace(searchResult.agent.trace)
|
||||
setEvidenceResult(evidenceItems, collectionItems)
|
||||
const nextSenderNames = mapEvidenceSenderNames(evidenceItems)
|
||||
setSenderNames(nextSenderNames)
|
||||
@@ -381,13 +367,10 @@ export function AISearchWorkspace({
|
||||
})
|
||||
setStage('result')
|
||||
} catch (error) {
|
||||
if (requestId && searchRequestIdRef.current !== requestId) return
|
||||
const errorMessage = error instanceof Error ? error.message : '读取聊天记录失败'
|
||||
addDebugEntry('检索失败', { error: errorMessage })
|
||||
setAnalysisError(errorMessage)
|
||||
setStage('insufficient')
|
||||
} finally {
|
||||
if (requestId && searchRequestIdRef.current === requestId) searchRequestIdRef.current = ''
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type {
|
||||
AiSearchAgentRun,
|
||||
AiSearchPipelineRequest,
|
||||
AiSearchPipelineResult,
|
||||
AiSearchProgressEvent
|
||||
} from '../../../../../shared/ai-search'
|
||||
import type { SearchProgressByStage } from '../searchTypes'
|
||||
|
||||
export type AiSearchRunStatus =
|
||||
| 'idle'
|
||||
| 'starting'
|
||||
| 'running'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'cancelled'
|
||||
|
||||
export type AiSearchRunOutcome =
|
||||
| { kind: 'completed'; requestId: string; result: AiSearchPipelineResult }
|
||||
| { kind: 'cancelled'; requestId: string; result?: AiSearchPipelineResult }
|
||||
| { kind: 'failed'; requestId: string; error: string }
|
||||
| { kind: 'stale'; requestId: string }
|
||||
|
||||
const errorMessage = (error: unknown): string =>
|
||||
error instanceof Error ? error.message : '读取聊天记录失败'
|
||||
|
||||
export function useAiSearchRun(): {
|
||||
status: AiSearchRunStatus
|
||||
requestId: string
|
||||
result: AiSearchPipelineResult | null
|
||||
error: string
|
||||
progress: SearchProgressByStage
|
||||
agentTrace: AiSearchAgentRun['trace']
|
||||
createRequestId: () => string
|
||||
startSearch: (request: AiSearchPipelineRequest) => Promise<AiSearchRunOutcome>
|
||||
cancelSearch: () => Promise<void>
|
||||
resetSearchRun: () => void
|
||||
isCurrentRequest: (requestId: string) => boolean
|
||||
} {
|
||||
const [status, setStatus] = useState<AiSearchRunStatus>('idle')
|
||||
const [requestId, setRequestId] = useState('')
|
||||
const [result, setResult] = useState<AiSearchPipelineResult | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
const [progress, setProgress] = useState<SearchProgressByStage>({})
|
||||
const [agentTrace, setAgentTrace] = useState<AiSearchAgentRun['trace']>([])
|
||||
const requestIdRef = useRef('')
|
||||
|
||||
const createRequestId = (): string => globalThis.crypto?.randomUUID?.() || `search-${Date.now()}`
|
||||
|
||||
const isCurrentRequest = (currentRequestId: string): boolean =>
|
||||
Boolean(currentRequestId) && requestIdRef.current === currentRequestId
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = window.api.onAiSearchProgress((event: AiSearchProgressEvent) => {
|
||||
if (!isCurrentRequest(event.requestId)) return
|
||||
setProgress((current) => ({ ...current, [event.stage]: event }))
|
||||
if (event.agentTrace) {
|
||||
setAgentTrace((current) =>
|
||||
current.some((item) => item.sequence === event.agentTrace?.sequence)
|
||||
? current
|
||||
: [...current, event.agentTrace as AiSearchAgentRun['trace'][number]]
|
||||
)
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
requestIdRef.current = ''
|
||||
unsubscribe()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const startSearch = async (request: AiSearchPipelineRequest): Promise<AiSearchRunOutcome> => {
|
||||
requestIdRef.current = request.requestId
|
||||
setRequestId(request.requestId)
|
||||
setStatus('starting')
|
||||
setError('')
|
||||
setResult(null)
|
||||
setProgress({})
|
||||
setAgentTrace([])
|
||||
setStatus('running')
|
||||
try {
|
||||
const searchResult = await window.api.runAiSearch(request)
|
||||
if (!isCurrentRequest(request.requestId))
|
||||
return { kind: 'stale', requestId: request.requestId }
|
||||
requestIdRef.current = ''
|
||||
setRequestId('')
|
||||
setResult(searchResult)
|
||||
setAgentTrace(searchResult.agent.trace)
|
||||
if (searchResult.status === 'cancelled') {
|
||||
setStatus('cancelled')
|
||||
return { kind: 'cancelled', requestId: request.requestId, result: searchResult }
|
||||
}
|
||||
if (searchResult.status === 'failed' || searchResult.status === 'ai_failed') {
|
||||
setStatus('failed')
|
||||
} else {
|
||||
setStatus('completed')
|
||||
}
|
||||
return { kind: 'completed', requestId: request.requestId, result: searchResult }
|
||||
} catch (caughtError) {
|
||||
if (!isCurrentRequest(request.requestId))
|
||||
return { kind: 'stale', requestId: request.requestId }
|
||||
requestIdRef.current = ''
|
||||
setRequestId('')
|
||||
const message = errorMessage(caughtError)
|
||||
setError(message)
|
||||
setStatus('failed')
|
||||
return { kind: 'failed', requestId: request.requestId, error: message }
|
||||
}
|
||||
}
|
||||
|
||||
const cancelSearch = async (): Promise<void> => {
|
||||
const currentRequestId = requestIdRef.current
|
||||
if (!currentRequestId) return
|
||||
requestIdRef.current = ''
|
||||
setRequestId('')
|
||||
setStatus('cancelled')
|
||||
setError('')
|
||||
setProgress({})
|
||||
setAgentTrace([])
|
||||
await window.api.cancelAiSearch(currentRequestId)
|
||||
}
|
||||
|
||||
const resetSearchRun = (): void => {
|
||||
requestIdRef.current = ''
|
||||
setRequestId('')
|
||||
setStatus('idle')
|
||||
setResult(null)
|
||||
setError('')
|
||||
setProgress({})
|
||||
setAgentTrace([])
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
requestId,
|
||||
result,
|
||||
error,
|
||||
progress,
|
||||
agentTrace,
|
||||
createRequestId,
|
||||
startSearch,
|
||||
cancelSearch,
|
||||
resetSearchRun,
|
||||
isCurrentRequest
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user