From cf00dc5e8e9e1f75c52fc595b267a2f79e14e1f8 Mon Sep 17 00:00:00 2001 From: limitcool Date: Fri, 3 Apr 2026 15:48:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20AI=20=E7=B4=A2?= =?UTF-8?q?=E5=BC=95=E9=87=8D=E5=BB=BA=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E7=9B=B8=E5=85=B3=20API=20=E5=92=8C=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E6=B5=81=EF=BC=8C=E5=A2=9E=E5=BC=BA=E5=86=85=E5=AD=98=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin/src/lib/api.ts | 3 +- admin/src/lib/types.ts | 5 - admin/src/pages/site-settings-page.tsx | 99 +++++++++++++++++++- backend/src/app.rs | 6 +- backend/src/controllers/admin_api.rs | 33 +++++-- backend/src/controllers/ai.rs | 21 +++-- backend/src/services/ai.rs | 122 +++++++++++++------------ backend/src/services/worker_jobs.rs | 81 ++++++++++++++++ backend/src/workers/ai_reindex.rs | 55 +++++++++++ backend/src/workers/mod.rs | 1 + deploy/docker/.env.example | 11 +++ deploy/docker/README.md | 13 +++ deploy/docker/compose.package.yml | 19 ++++ deploy/docker/config.yaml.example | 8 ++ mcp-server/server.js | 2 +- 15 files changed, 391 insertions(+), 88 deletions(-) create mode 100644 backend/src/workers/ai_reindex.rs diff --git a/admin/src/lib/api.ts b/admin/src/lib/api.ts index 5e7c8d1..945dcd6 100644 --- a/admin/src/lib/api.ts +++ b/admin/src/lib/api.ts @@ -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('/api/admin/ai/reindex', { + request('/api/admin/ai/reindex', { method: 'POST', }), testAiProvider: (provider: { diff --git a/admin/src/lib/types.ts b/admin/src/lib/types.ts index 8db3c3f..95eb587 100644 --- a/admin/src/lib/types.ts +++ b/admin/src/lib/types.ts @@ -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 diff --git a/admin/src/pages/site-settings-page.tsx b/admin/src/pages/site-settings-page.tsx index a100da9..f5c7233 100644 --- a/admin/src/pages/site-settings-page.tsx +++ b/admin/src/pages/site-settings-page.tsx @@ -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(null) + const [reindexJobStatus, setReindexJobStatus] = useState(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 | 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 = ( key: K, value: AdminSiteSettingsResponse[K], @@ -498,25 +570,38 @@ export function SiteSettingsPage() { 刷新 + {reindexJobId ? ( + + ) : null}