瀏覽代碼

fix(chat): 未选聊天模型时自动选第一个可用模型

模型设置完成后聊天选择器仍为空会直接报错,现自动选中并记住第一个可用模型。

Co-authored-by: Cursor <cursoragent@cursor.com>
darknessomi 1 月之前
父節點
當前提交
958880334f
共有 3 個文件被更改,包括 84 次插入 和 0 次删除
  1. 2 0
      src/App.tsx
  2. 55 0
      src/lib/ensure-ai-chat-model.spec.ts
  3. 27 0
      src/lib/ensure-ai-chat-model.ts

+ 2 - 0
src/App.tsx

@@ -30,8 +30,10 @@ import { countChapterBodyWords } from "@/lib/chapter-word-count"
 import { flattenMdFiles } from "@/lib/novel/chapter-utils"
 import { runUserMemoryMaintenance } from "@/lib/user-memory/maintenance"
 import { initializeProjectContextCache } from "@/lib/context-hub/context-hub"
+import { useEnsureAiChatModel } from "@/lib/ensure-ai-chat-model"
 
 function App() {
+  useEnsureAiChatModel()
   const project = useWikiStore((s) => s.project)
   const setProject = useWikiStore((s) => s.setProject)
   const setFileTree = useWikiStore((s) => s.setFileTree)

+ 55 - 0
src/lib/ensure-ai-chat-model.spec.ts

@@ -0,0 +1,55 @@
+import { beforeEach, describe, expect, it, vi } from "vitest"
+
+const saveAiChatModel = vi.fn(async () => {})
+
+vi.mock("@/lib/project-store", () => ({
+  saveAiChatModel: (...args: unknown[]) => saveAiChatModel(...args),
+}))
+
+import { ensureAiChatModelSelected } from "./ensure-ai-chat-model"
+import { useWikiStore } from "@/stores/wiki-store"
+import type { ProviderConfigs } from "@/stores/wiki-store"
+
+function saved(id: string) {
+  return { id, model: id, name: id, createdAt: 1 }
+}
+
+const availableConfigs: ProviderConfigs = {
+  openai: { enabled: true, apiKey: "key", savedModels: [saved("gpt-4o"), saved("gpt-4.1")] },
+  anthropic: { enabled: true, apiKey: "key", savedModels: [saved("claude-sonnet")] },
+}
+
+describe("ensureAiChatModelSelected", () => {
+  beforeEach(() => {
+    saveAiChatModel.mockClear()
+    useWikiStore.setState({
+      aiChatModel: "",
+      providerConfigs: {},
+    })
+  })
+
+  it("does nothing when no provider models are available", () => {
+    expect(ensureAiChatModelSelected()).toBe("")
+    expect(useWikiStore.getState().aiChatModel).toBe("")
+    expect(saveAiChatModel).not.toHaveBeenCalled()
+  })
+
+  it("keeps an already selected chat model", () => {
+    useWikiStore.setState({
+      aiChatModel: "anthropic/claude-sonnet",
+      providerConfigs: availableConfigs,
+    })
+
+    expect(ensureAiChatModelSelected()).toBe("anthropic/claude-sonnet")
+    expect(useWikiStore.getState().aiChatModel).toBe("anthropic/claude-sonnet")
+    expect(saveAiChatModel).not.toHaveBeenCalled()
+  })
+
+  it("selects the first available model when chat model is empty", () => {
+    useWikiStore.setState({ providerConfigs: availableConfigs })
+
+    expect(ensureAiChatModelSelected()).toBe("openai/gpt-4o")
+    expect(useWikiStore.getState().aiChatModel).toBe("openai/gpt-4o")
+    expect(saveAiChatModel).toHaveBeenCalledWith("openai/gpt-4o")
+  })
+})

+ 27 - 0
src/lib/ensure-ai-chat-model.ts

@@ -0,0 +1,27 @@
+import { useEffect } from "react"
+import { useWikiStore } from "@/stores/wiki-store"
+import { saveAiChatModel } from "@/lib/project-store"
+import { getFirstAvailableModelKey } from "@/lib/llm-model-keys"
+
+/** 聊天模型未选且已有可用模型时,自动选中第一个并持久化。 */
+export function ensureAiChatModelSelected(): string {
+  const { aiChatModel, providerConfigs, setAiChatModel } = useWikiStore.getState()
+  const current = aiChatModel.trim()
+  if (current) return current
+
+  const first = getFirstAvailableModelKey(providerConfigs)
+  if (!first) return ""
+
+  setAiChatModel(first)
+  void saveAiChatModel(first)
+  return first
+}
+
+export function useEnsureAiChatModel(): void {
+  const aiChatModel = useWikiStore((s) => s.aiChatModel)
+  const providerConfigs = useWikiStore((s) => s.providerConfigs)
+
+  useEffect(() => {
+    ensureAiChatModelSelected()
+  }, [aiChatModel, providerConfigs])
+}