feat: 添加 AI 索引重建功能,优化相关 API 和工作流,增强内存管理配置
Some checks failed
docker-images / resolve-build-targets (push) Successful in 6s
ui-regression / playwright-regression (push) Successful in 4m43s
docker-images / build-and-push (admin) (push) Successful in 42s
docker-images / submit-indexnow (push) Has been cancelled
docker-images / build-and-push (frontend) (push) Has been cancelled
docker-images / build-and-push (backend) (push) Has started running

This commit is contained in:
2026-04-03 15:48:33 +08:00
parent 1df179c327
commit cf00dc5e8e
15 changed files with 391 additions and 88 deletions

View File

@@ -1,7 +1,6 @@
import type {
AdminAnalyticsResponse,
AdminAiImageProviderTestResponse,
AdminAiReindexResponse,
AdminAiProviderTestResponse,
AdminImageUploadResponse,
AdminMediaBatchDeleteResponse,
@@ -362,7 +361,7 @@ export const adminApi = {
body: JSON.stringify(payload),
}),
reindexAi: () =>
request<AdminAiReindexResponse>('/api/admin/ai/reindex', {
request<WorkerTaskActionResponse>('/api/admin/ai/reindex', {
method: 'POST',
}),
testAiProvider: (provider: {

View File

@@ -545,11 +545,6 @@ export interface TaxonomyPayload {
seoDescription?: string | null
}
export interface AdminAiReindexResponse {
indexed_chunks: number
last_indexed_at: string | null
}
export interface AdminAiProviderTestResponse {
provider: string
endpoint: string

View File

@@ -1,6 +1,7 @@
import { Bot, Check, Plus, RefreshCcw, Save, Trash2 } from 'lucide-react'
import type { ReactNode } from 'react'
import { startTransition, useCallback, useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import { toast } from 'sonner'
import { MediaUrlControls } from '@/components/media-url-controls'
@@ -19,6 +20,7 @@ import type {
HumanVerificationMode,
MusicTrack,
SiteSettingsPayload,
WorkerJobRecord,
} from '@/lib/types'
function createEmptyMusicTrack(): MusicTrack {
@@ -237,6 +239,8 @@ export function SiteSettingsPage() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [reindexing, setReindexing] = useState(false)
const [reindexJobId, setReindexJobId] = useState<number | null>(null)
const [reindexJobStatus, setReindexJobStatus] = useState<WorkerJobRecord['status'] | null>(null)
const [testingProvider, setTestingProvider] = useState(false)
const [testingImageProvider, setTestingImageProvider] = useState(false)
const [testingR2Storage, setTestingR2Storage] = useState(false)
@@ -295,6 +299,74 @@ export function SiteSettingsPage() {
})
}, [form?.ai_active_provider_id, form?.ai_providers])
useEffect(() => {
if (!reindexJobId) {
return
}
let cancelled = false
let timer: ReturnType<typeof setTimeout> | null = null
const scheduleNextPoll = () => {
timer = setTimeout(() => {
void pollJob()
}, 4000)
}
const pollJob = async () => {
try {
const job = await adminApi.getWorkerJob(reindexJobId)
if (cancelled) {
return
}
setReindexJobStatus(job.status)
if (job.status === 'succeeded') {
setReindexing(false)
setReindexJobId(null)
setReindexJobStatus(null)
const indexedChunks = Number(job.result?.indexed_chunks ?? 0)
toast.success(
indexedChunks > 0
? `AI 索引重建完成,共生成 ${indexedChunks} 个分块。`
: 'AI 索引重建完成。',
)
await loadSettings(false)
return
}
if (job.status === 'failed' || job.status === 'cancelled') {
setReindexing(false)
setReindexJobId(null)
setReindexJobStatus(null)
toast.error(job.error_text?.trim() || 'AI 重建索引失败。')
return
}
scheduleNextPoll()
} catch (error) {
if (cancelled) {
return
}
scheduleNextPoll()
if (error instanceof ApiError && error.status === 401) {
return
}
}
}
void pollJob()
return () => {
cancelled = true
if (timer) {
clearTimeout(timer)
}
}
}, [loadSettings, reindexJobId])
const updateField = <K extends keyof AdminSiteSettingsResponse>(
key: K,
value: AdminSiteSettingsResponse[K],
@@ -498,25 +570,38 @@ export function SiteSettingsPage() {
<RefreshCcw className="h-4 w-4" />
</Button>
{reindexJobId ? (
<Button variant="outline" asChild data-testid="site-settings-reindex-job">
<Link to={`/workers?job=${reindexJobId}`}></Link>
</Button>
) : null}
<Button
variant="secondary"
disabled={reindexing}
data-testid="site-settings-reindex"
onClick={async () => {
let queued = false
try {
setReindexing(true)
setReindexJobStatus('queued')
const result = await adminApi.reindexAi()
toast.success(`AI 索引已重建,共生成 ${result.indexed_chunks} 个分块。`)
await loadSettings(false)
queued = true
setReindexJobId(result.job.id)
setReindexJobStatus(result.job.status)
toast.success(`AI 重建任务已入队:#${result.job.id}`)
} catch (error) {
setReindexJobId(null)
setReindexJobStatus(null)
toast.error(error instanceof ApiError ? error.message : 'AI 重建索引失败。')
} finally {
setReindexing(false)
if (!queued) {
setReindexing(false)
}
}
}}
>
<Bot className="h-4 w-4" />
{reindexing ? '重建中...' : '重建 AI 索引'}
{reindexing ? '任务进行中...' : '重建 AI 索引'}
</Button>
<Button
disabled={saving}
@@ -1653,6 +1738,12 @@ export function SiteSettingsPage() {
<p className="mt-3 text-sm leading-6 text-muted-foreground">
{form.ai_last_indexed_at ?? '索引尚未建立。'}
</p>
{reindexJobId ? (
<p className="mt-3 text-xs leading-6 text-muted-foreground">
#{reindexJobId}
{reindexJobStatus ? `,状态:${reindexJobStatus}` : ''}
</p>
) : null}
</div>
</CardContent>
</Card>