Explorar o código

fix: pass providerConfigs to hasUsableLlm at all call sites

Mochocyang hai 2 meses
pai
achega
a3582d3a28

+ 1 - 1
src/components/dashboard/dashboard-view.tsx

@@ -310,7 +310,7 @@ export function DashboardView({ headerActions }: DashboardViewProps = {}) {
       return
     }
     const llmConfig = resolveDefaultModel(useWikiStore.getState().llmConfig)
-    if (!hasUsableLlm(llmConfig)) {
+    if (!hasUsableLlm(llmConfig, useWikiStore.getState().providerConfigs)) {
       showAiRewriteAlert("请先在设置里配置可用的 AI 模型。")
       return
     }

+ 9 - 6
src/components/layout/preview-panel.tsx

@@ -622,8 +622,9 @@ export function PreviewPanel() {
       setFileContent(syncResult.markdown)
 
       if (novelConfig.autoIngestOnSave) {
-        const llmConfig = useWikiStore.getState().llmConfig
-        if (!hasUsableLlm(llmConfig)) {
+        const state = useWikiStore.getState()
+        const llmConfig = state.llmConfig
+        if (!hasUsableLlm(llmConfig, state.providerConfigs)) {
           updatePhase(false, "ingest_no_llm")
         } else {
           const verifyContent = await readFile(targetPath)
@@ -752,8 +753,9 @@ export function PreviewPanel() {
   const handleDeAiProcess = useCallback(async () => {
     if (!fileContent.trim()) return
     setDeAiProcessing(true)
-    const llmConfig = resolveDefaultModel(useWikiStore.getState().llmConfig)
-    if (!hasUsableLlm(llmConfig)) {
+    const state = useWikiStore.getState()
+    const llmConfig = resolveDefaultModel(state.llmConfig)
+    if (!hasUsableLlm(llmConfig, state.providerConfigs)) {
       setDeAiProcessing(false)
       return
     }
@@ -816,8 +818,9 @@ export function PreviewPanel() {
 
   const handleSelectionAction = useCallback(async (action: ChapterSelectionAction, selection: ChapterBodySelection) => {
     if (!selection.text.trim()) return
-    const llmConfig = resolveDefaultModel(useWikiStore.getState().llmConfig)
-    if (!hasUsableLlm(llmConfig)) {
+    const state = useWikiStore.getState()
+    const llmConfig = resolveDefaultModel(state.llmConfig)
+    if (!hasUsableLlm(llmConfig, state.providerConfigs)) {
       setSaveStatus("未配置可用的 AI 模型,无法处理选中文本")
       return
     }

+ 3 - 2
src/components/lint/lint-view.tsx

@@ -35,6 +35,7 @@ export function LintView() {
   const novelMode = useWikiStore((s) => s.novelMode)
   const project = useWikiStore((s) => s.project)
   const llmConfig = useWikiStore((s) => s.llmConfig)
+  const providerConfigs = useWikiStore((s) => s.providerConfigs)
   const selectedFile = useWikiStore((s) => s.selectedFile)
   const fileContent = useWikiStore((s) => s.fileContent)
   const setSelectedFile = useWikiStore((s) => s.setSelectedFile)
@@ -110,7 +111,7 @@ export function LintView() {
       const structural = await runStructuralLint(pp)
       let all = structural
 
-      if (runSemantic && hasUsableLlm(llmConfig)) {
+      if (runSemantic && hasUsableLlm(llmConfig, providerConfigs)) {
         const semantic = await runSemanticLint(pp, llmConfig, {
           chapterContent: novelMode && selectedFile ? fileContent : undefined,
           chapterNumber: meta?.chapterNumber,
@@ -150,7 +151,7 @@ export function LintView() {
         })
       }
     }
-  }, [project, llmConfig, running, runSemantic, fileContent, novelMode, selectedFile, t, loadHistory, setLintRun])
+  }, [project, llmConfig, providerConfigs, running, runSemantic, fileContent, novelMode, selectedFile, t, loadHistory, setLintRun])
 
   async function handleOpenPage(page: string) {
     if (!project) return

+ 1 - 1
src/components/review/review-view.tsx

@@ -288,7 +288,7 @@ export function ReviewView({
       return
     }
     const llmConfig = resolveDefaultModel(useWikiStore.getState().llmConfig)
-    if (!hasUsableLlm(llmConfig)) {
+    if (!hasUsableLlm(llmConfig, useWikiStore.getState().providerConfigs)) {
       showAiRewriteAlert("请先在设置里配置可用的 AI 模型。")
       return
     }

+ 2 - 1
src/components/settings/sections/maintenance-section.tsx

@@ -82,6 +82,7 @@ function findTaskForGroup(
 export function MaintenanceSection() {
   const { t } = useTranslation()
   const llmConfig = useWikiStore((s) => s.llmConfig)
+  const providerConfigs = useWikiStore((s) => s.providerConfigs)
   const project = useWikiStore((s) => s.project)
 
   const [scanState, setScanState] = useState<MaintenanceScanState>(sharedScanState)
@@ -99,7 +100,7 @@ export function MaintenanceSection() {
     return () => clearInterval(id)
   }, [])
 
-  const llmReady = hasUsableLlm(llmConfig)
+  const llmReady = hasUsableLlm(llmConfig, providerConfigs)
   const projectReady = !!project
   const projectScanState = project && scanState.projectPath === project.path ? scanState : emptyScanState
   const { scanning, scanError, groups, scanCompleted } = projectScanState

+ 2 - 2
src/components/sources/outline-chat-panel.tsx

@@ -269,7 +269,7 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
     if (activeConv?.modelId) {
       effectiveLlmConfig = resolveModelConfig(activeConv.modelId, effectiveLlmConfig, providerConfigs)
     }
-    if (!hasUsableLlm(effectiveLlmConfig)) {
+    if (!hasUsableLlm(effectiveLlmConfig, providerConfigs)) {
       const convId = activeConversationId ?? createConversation()
       addMessage(convId, { id: crypto.randomUUID(), role: "assistant", content: "请先在设置中配置可用的AI模型(API Key 和模型名称),或在AI会话中选择一个模型。" })
       return
@@ -432,7 +432,7 @@ export function OutlineChatPanel({ onClose }: { onClose: () => void }) {
     if (activeConv?.modelId) {
       effectiveLlmConfig = resolveModelConfig(activeConv.modelId, effectiveLlmConfig, providerConfigs)
     }
-    if (!hasUsableLlm(effectiveLlmConfig)) {
+    if (!hasUsableLlm(effectiveLlmConfig, providerConfigs)) {
       addMessage(activeConversationId, { id: crypto.randomUUID(), role: "assistant", content: "请先在设置中配置可用的AI模型(API Key 和模型名称),或在AI会话中选择一个模型。" })
       return
     }

+ 3 - 2
src/lib/dedup-queue.ts

@@ -313,9 +313,10 @@ async function processNext(projectId: string): Promise<void> {
   await saveQueue(pp)
   if (currentProjectId !== projectId) return
 
-  const llmConfig = resolveDefaultModel(useWikiStore.getState().llmConfig)
+  const state = useWikiStore.getState()
+  const llmConfig = resolveDefaultModel(state.llmConfig)
 
-  if (!hasUsableLlm(llmConfig)) {
+  if (!hasUsableLlm(llmConfig, state.providerConfigs)) {
     next.status = "failed"
     next.error = "LLM not configured — set API key in Settings"
     processing = false

+ 3 - 2
src/lib/ingest-queue.ts

@@ -514,10 +514,11 @@ async function processNext(projectId: string): Promise<void> {
   await saveQueue(pp)
   if (currentProjectId !== projectId) return
 
-  const llmConfig = resolveDefaultModel(useWikiStore.getState().llmConfig)
+  const state = useWikiStore.getState()
+  const llmConfig = resolveDefaultModel(state.llmConfig)
 
   // Check if LLM is configured
-  if (!hasUsableLlm(llmConfig)) {
+  if (!hasUsableLlm(llmConfig, state.providerConfigs)) {
     next.status = "failed"
     next.error = "LLM not configured — set API key in Settings"
     processing = false

+ 9 - 8
src/lib/novel/chapter-ingest.ts

@@ -300,14 +300,14 @@ export async function ingestChapter(
   signal?: AbortSignal,
 ): Promise<IngestResult> {
   const pp = normalizePath(projectPath)
-  const novelMode = useWikiStore.getState().novelMode
-  if (!novelMode) return { snapshot: null }
+  const state = useWikiStore.getState()
+  if (!state.novelMode) return { snapshot: null }
 
-  const llmConfig = useWikiStore.getState().llmConfig
-  const novelConfig = useWikiStore.getState().novelConfig
+  const llmConfig = state.llmConfig
+  const novelConfig = state.novelConfig
   // 使用 resolveNovelModel 正确解析提取模型(含供应商配置切换)
   const runtimeLlmConfig = resolveNovelModel(llmConfig, novelConfig, "extract")
-  if (!hasUsableLlm(runtimeLlmConfig)) return { snapshot: null, failReason: "no_llm" }
+  if (!hasUsableLlm(runtimeLlmConfig, state.providerConfigs)) return { snapshot: null, failReason: "no_llm" }
 
   const content = await readFile(chapterPath)
   const parsed = parseFrontmatter(content)
@@ -1396,11 +1396,12 @@ export async function ingestOutline(
   signal?: AbortSignal,
 ): Promise<ChapterSnapshot | null> {
   const pp = normalizePath(projectPath)
-  const llmConfig = useWikiStore.getState().llmConfig
-  const novelConfig = useWikiStore.getState().novelConfig
+  const state = useWikiStore.getState()
+  const llmConfig = state.llmConfig
+  const novelConfig = state.novelConfig
   // 使用 resolveNovelModel 正确解析提取模型(含供应商配置切换),与 ingestChapter 保持一致
   const runtimeLlmConfig = resolveNovelModel(llmConfig, novelConfig, "extract")
-  if (!hasUsableLlm(runtimeLlmConfig)) return null
+  if (!hasUsableLlm(runtimeLlmConfig, state.providerConfigs)) return null
 
   const content = await readFile(outlinePath)
   const body = content.length > 8000 ? content.slice(0, 8000) : content

+ 6 - 4
src/lib/novel/character-aura.ts

@@ -989,8 +989,9 @@ async function buildAuraResearchStage(
   input: CustomCharacterAuraGenerationInput,
   previousResearchFiles: Partial<Record<CharacterAuraResearchFileName, string>>,
 ): Promise<string> {
-  const llmConfig = resolveDefaultModel(useWikiStore.getState().llmConfig)
-  if (hasUsableLlm(llmConfig)) {
+  const state = useWikiStore.getState()
+  const llmConfig = resolveDefaultModel(state.llmConfig)
+  if (hasUsableLlm(llmConfig, state.providerConfigs)) {
     try {
       const raw = await runAuraModelPrompt(
         "你是一名小说角色灵魂研究工作流助手。必须只输出用户要求的 Markdown 正文,不要输出解释,不要输出代码围栏。",
@@ -1008,8 +1009,9 @@ async function synthesizeCustomAuraFields(
   input: CustomCharacterAuraGenerationInput,
   researchFiles: Partial<Record<CharacterAuraResearchFileName, string>>,
 ): Promise<CustomAuraGeneratedFields> {
-  const llmConfig = resolveDefaultModel(useWikiStore.getState().llmConfig)
-  if (hasUsableLlm(llmConfig)) {
+  const state = useWikiStore.getState()
+  const llmConfig = resolveDefaultModel(state.llmConfig)
+  if (hasUsableLlm(llmConfig, state.providerConfigs)) {
     try {
       const raw = await runAuraModelPrompt(
         "你是一名小说角色灵魂总结助手。只输出 JSON,不要解释,不要代码围栏。",

+ 1 - 0
src/lib/novel/dimension-review-adapter.spec.ts

@@ -61,6 +61,7 @@ vi.mock("@/stores/wiki-store", () => ({
       llmConfig,
       novelConfig: { reviewModel: "" },
       novelMode: true,
+      providerConfigs: {},
     }),
   },
 }))

+ 4 - 3
src/lib/novel/dimension-review-adapter.ts

@@ -187,12 +187,13 @@ export async function runSixDimensionReview({
   dimensionKeys?: SixReviewDimensionKey[]
   callbacks?: SixDimensionReviewCallbacks
 }): Promise<Partial<Record<SixReviewDimensionKey, DimensionReviewResult>>> {
+  const state = useWikiStore.getState()
   const llmConfig = resolveNovelModel(
-    useWikiStore.getState().llmConfig,
-    useWikiStore.getState().novelConfig,
+    state.llmConfig,
+    state.novelConfig,
     "review",
   )
-  if (!hasUsableLlm(llmConfig) || !useWikiStore.getState().novelMode) return {}
+  if (!hasUsableLlm(llmConfig, state.providerConfigs) || !state.novelMode) return {}
 
   const contextPack = await buildContextPack(
     projectPath,

+ 1 - 1
src/lib/novel/fact-snapshot.ts

@@ -445,7 +445,7 @@ export async function verifyFactCheckLlm(
       useWikiStore.getState().novelConfig,
       "review",
     )
-    if (!hasUsableLlm(llmConfig)) return results
+    if (!hasUsableLlm(llmConfig, useWikiStore.getState().providerConfigs)) return results
 
     const pendingItems = pendingResults.slice(0, 5)
     const itemsText = pendingItems.map((item, index) => {

+ 5 - 4
src/lib/novel/lint.ts

@@ -53,14 +53,15 @@ export async function runNovelLint(
   chapterContent: string,
   chapterNumber?: number,
 ): Promise<NovelLintResult[]> {
+  const state = useWikiStore.getState()
   const llmConfig = resolveNovelModel(
-    useWikiStore.getState().llmConfig,
-    useWikiStore.getState().novelConfig,
+    state.llmConfig,
+    state.novelConfig,
     "lint",
   )
-  if (!hasUsableLlm(llmConfig)) return []
+  if (!hasUsableLlm(llmConfig, state.providerConfigs)) return []
 
-  const novelMode = useWikiStore.getState().novelMode
+  const novelMode = state.novelMode
   if (!novelMode) return []
 
   const contextPack = await buildContextPack(

+ 1 - 0
src/lib/novel/review-adapter.spec.ts

@@ -54,6 +54,7 @@ vi.mock("@/stores/wiki-store", () => ({
       llmConfig,
       novelConfig: mocks.novelConfig,
       novelMode: true,
+      providerConfigs: {},
     }),
   },
 }))

+ 5 - 4
src/lib/novel/review-adapter.ts

@@ -169,14 +169,15 @@ export async function reviewChapter(
   signal?: AbortSignal,
 ): Promise<NovelReviewResult[]> {
   if (signal?.aborted) throw new Error("已停止生成")
+  const state = useWikiStore.getState()
   const llmConfig = resolveNovelModel(
-    useWikiStore.getState().llmConfig,
-    useWikiStore.getState().novelConfig,
+    state.llmConfig,
+    state.novelConfig,
     "review",
   )
-  if (!hasUsableLlm(llmConfig)) return []
+  if (!hasUsableLlm(llmConfig, state.providerConfigs)) return []
 
-  const novelMode = useWikiStore.getState().novelMode
+  const novelMode = state.novelMode
   if (!novelMode) return []
 
   // 复用调用方已构建的 contextPack;没有才自行构建。

+ 2 - 1
src/lib/source-lifecycle.ts

@@ -10,6 +10,7 @@ import {
 } from "@/commands/fs"
 import type { WikiProject, FileNode } from "@/types/wiki"
 import type { LlmConfig } from "@/stores/wiki-store"
+import { useWikiStore } from "@/stores/wiki-store"
 import { enqueueBatch } from "@/lib/ingest-queue"
 import { hasUsableLlm } from "@/lib/has-usable-llm"
 import { getFileName, getFileStem, normalizePath } from "@/lib/path-utils"
@@ -104,7 +105,7 @@ export async function enqueueSourceIngest(
   llmConfig: LlmConfig,
   options: { sourceRoot?: string; rootContext?: string } = {},
 ): Promise<string[]> {
-  if (!hasUsableLlm(llmConfig)) return []
+  if (!hasUsableLlm(llmConfig, useWikiStore.getState().providerConfigs)) return []
   const files = sourcePaths
     .filter(isIngestableSourcePath)
     .map((sourcePath) => ({

+ 3 - 2
src/lib/sweep-reviews.ts

@@ -196,8 +196,9 @@ async function judgeBatch(
 ): Promise<Set<string>> {
   if (batch.length === 0 || signal?.aborted) return new Set()
 
-  const llmConfig = resolveDefaultModel(useWikiStore.getState().llmConfig)
-  if (!hasUsableLlm(llmConfig)) return new Set()
+  const state = useWikiStore.getState()
+  const llmConfig = resolveDefaultModel(state.llmConfig)
+  if (!hasUsableLlm(llmConfig, state.providerConfigs)) return new Set()
 
   const pages = index.pages.slice(0, MAX_PAGES_IN_PROMPT)