소스 검색

fix(chat): 快速模式对话使用所选写作模型 (#66)

* fix(chat): 快速模式对话改用聊天框所选写作模型

快速模式由主 Agent 直接出稿,此前却走默认编排模型。现与大纲绘制一致,会话输出使用聊天框选中的写作模型;标准/严格模式仍用默认模型做编排。

Co-authored-by: darknessomi <darknessomi@users.noreply.github.com>

* revert(changelog): 不要改已发布的 3.2.2 更新日志

这条修复应等下次发版再记,不能回写进已发布版本。

Co-authored-by: darknessomi <darknessomi@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: darknessomi <darknessomi@users.noreply.github.com>
darknessomi 4 주 전
부모
커밋
220d17b31f
6개의 변경된 파일108개의 추가작업 그리고 6개의 파일을 삭제
  1. 36 0
      src/hooks/use-agent-config.spec.ts
  2. 4 4
      src/hooks/use-agent-config.ts
  3. 1 1
      src/lib/agent/tools/index.ts
  4. 1 1
      src/lib/novel/mod.ts
  5. 49 0
      src/lib/novel/model-resolver.spec.ts
  6. 17 0
      src/lib/novel/model-resolver.ts

+ 36 - 0
src/hooks/use-agent-config.spec.ts

@@ -361,6 +361,42 @@ describe("useAgentConfig", () => {
     await cleanup()
   }, 15000)
 
+  it("uses the selected chat model for fast-mode conversation output instead of the default agent model", async () => {
+    const providerConfigs: ProviderConfigs = {
+      custom: {
+        enabled: true,
+        apiKey: "test-key",
+        savedModels: [
+          { id: "writer", name: "Writer", model: "writer-model", createdAt: 1 },
+          { id: "workflow", name: "Workflow", model: "workflow-model", createdAt: 2 },
+        ],
+      },
+    }
+    const { result, cleanup } = await renderHook("test prompt", {
+      wiki: {
+        aiChatModel: "custom/writer-model",
+        defaultLlmModel: "custom/workflow-model",
+        novelConfig: { ...DEFAULT_NOVEL_CONFIG, defaultLlmModel: "custom/workflow-model" },
+        providerConfigs,
+        project: { path: "/tmp/project" } as WikiProject,
+        aiWorkflowMode: "fast",
+      },
+      skillConfig: {
+        version: 1,
+        defaultSkillId: "built-in:comprehensive",
+        disabledSkillIds: [],
+        projectSkills: [],
+        builtInSkillOverrides: [],
+        lastChapterDeAiSkillId: null,
+      },
+    })
+
+    expect(result.config?.modelId).toBe("writer-model")
+    expect(result.config?.llmConfig.model).toBe("writer-model")
+
+    await cleanup()
+  }, 15000)
+
   it("passes Web Search settings into the web_search agent tool", async () => {
     webSearchMock.mockResolvedValueOnce([])
     const searchApiConfig: SearchApiConfig = {

+ 4 - 4
src/hooks/use-agent-config.ts

@@ -5,7 +5,7 @@ import { useOutlineChatStore } from "@/stores/outline-chat-store"
 import { loadDeAiSkillConfig, type DeAiSkillConfig } from "@/lib/novel/de-ai-skill-library"
 import { loadAllLinkedSkillsContent, loadUserSkillConfig, resolveEnabledWritingSkills } from "@/lib/novel/user-skill-store"
 import type { UserSkill } from "@/lib/novel/skill-library"
-import { resolveDefaultModel, resolveModelConfig } from "@/lib/novel/model-resolver"
+import { resolveAgentSessionModel, resolveModelConfig } from "@/lib/novel/model-resolver"
 import { runDeepChapterGeneration } from "@/lib/novel/deep-chapter-generation"
 import { normalizePath } from "@/lib/path-utils"
 import { ToolRegistry } from "@/lib/agent/registry"
@@ -29,7 +29,7 @@ export interface UseAgentConfigResult {
 export function useAgentConfig(systemPrompt: string, getPlanBlueprint?: () => string | undefined): UseAgentConfigResult {
   const aiChatModel = useWikiStore((s) => s.aiChatModel)
   const defaultLlmModel = useWikiStore((s) => s.defaultLlmModel)
-  const novelDefaultLlmModel = useWikiStore((s) => s.novelConfig.defaultLlmModel)
+  const novelConfig = useWikiStore((s) => s.novelConfig)
   const projectPath = useWikiStore((s) => s.project?.path)
   const dataVersion = useWikiStore((s) => s.dataVersion)
   const baseLlmConfig = useWikiStore((s) => s.llmConfig)
@@ -110,7 +110,7 @@ export function useAgentConfig(systemPrompt: string, getPlanBlueprint?: () => st
   )
 
   return useMemo(() => {
-    const agentLlmConfig = resolveDefaultModel(baseLlmConfig)
+    const agentLlmConfig = resolveAgentSessionModel(baseLlmConfig, novelConfig, aiWorkflowMode)
     const modelOk = modelSupportsTools(agentLlmConfig.model, agentLlmConfig.provider)
     const fcEnabled = isFunctionCallingEnabled(agentLlmConfig)
     const supportsTools = modelOk && fcEnabled
@@ -171,7 +171,7 @@ export function useAgentConfig(systemPrompt: string, getPlanBlueprint?: () => st
   }, [
     aiChatModel,
     defaultLlmModel,
-    novelDefaultLlmModel,
+    novelConfig,
     projectPath,
     skillConfigLoaded,
     baseLlmConfig,

+ 1 - 1
src/lib/agent/tools/index.ts

@@ -59,7 +59,7 @@ export interface ToolFactoryOptions {
   enabledToolNames?: string[]
   disabledTools?: string[]
   llmConfig?: LlmConfig
-  /** 章节正文专用模型。Agent 调度可使用默认模型,但正文仍必须使用聊天框模型。 */
+  /** 章节正文专用模型。Agent 调度可使用默认模型,但正文仍必须使用聊天框模型。快速模式主 Agent 直接出稿,会话模型与此相同。 */
   chapterWritingLlmConfig?: LlmConfig
   aiWorkflowMode?: AiWorkflowMode
   runDeepChapterGeneration?: RunDeepChapterGeneration

+ 1 - 1
src/lib/novel/mod.ts

@@ -6,7 +6,7 @@ export { buildContextPack, contextPackToPrompt, type ContextPack } from "./conte
 export { ingestChapter, ingestChapterPipeline, ingestOutline, loadSnapshot, listSnapshots, deleteChapterSnapshots, finalizeProjectMemoryRebuild, syncSnapshotToMemory, type ChapterSnapshot, type CharacterDetail, type LocationDetail, type OrganizationDetail, type ItemDetail, type EventDetail, type IngestResult, type IngestFailReason, type OutlineIngestResult, type IngestOutlineOptions, type SyncSnapshotToMemoryOptions } from "./chapter-ingest"
 export { reviewChapter, type NovelReviewResult } from "./review-adapter"
 export { runNovelLint, buildNovelLintPrompt, type NovelLintResult } from "./lint"
-export { resolveNovelModel, type NovelTaskType } from "./model-resolver"
+export { resolveNovelModel, resolveAgentSessionModel, type NovelTaskType } from "./model-resolver"
 export { resolveReviewModel } from "./review-model"
 export { novelMixedSearch, searchPlot, type NovelSearchParams, type NovelSearchResult } from "./search-adapter"
 export { PROMPTS } from "./prompt-templates"

+ 49 - 0
src/lib/novel/model-resolver.spec.ts

@@ -0,0 +1,49 @@
+import { afterEach, describe, expect, it } from "vitest"
+import { DEFAULT_NOVEL_CONFIG, useWikiStore, type LlmConfig, type ProviderConfigs } from "@/stores/wiki-store"
+import { resolveAgentSessionModel } from "./model-resolver"
+
+const baseConfig: LlmConfig = {
+  provider: "custom",
+  apiKey: "test-key",
+  model: "",
+  ollamaUrl: "",
+  customEndpoint: "https://example.test/v1",
+  maxContextSize: 8192,
+}
+
+const providerConfigs: ProviderConfigs = {
+  custom: {
+    enabled: true,
+    apiKey: "test-key",
+    savedModels: [
+      { id: "writer", name: "Writer", model: "writer-model", createdAt: 1 },
+      { id: "workflow", name: "Workflow", model: "workflow-model", createdAt: 2 },
+    ],
+  },
+}
+
+describe("resolveAgentSessionModel", () => {
+  afterEach(() => {
+    useWikiStore.setState({
+      aiChatModel: "",
+      defaultLlmModel: "",
+      providerConfigs: {},
+      novelConfig: { ...DEFAULT_NOVEL_CONFIG },
+    })
+  })
+
+  it("uses the selected chat model in fast mode instead of the default orchestration model", () => {
+    const previous = useWikiStore.getState()
+    useWikiStore.setState({
+      aiChatModel: "custom/writer-model",
+      defaultLlmModel: "custom/workflow-model",
+      providerConfigs,
+      novelConfig: { ...previous.novelConfig, defaultLlmModel: "custom/workflow-model" },
+    })
+
+    const novelConfig = useWikiStore.getState().novelConfig
+    expect(resolveAgentSessionModel(baseConfig, novelConfig, "fast").model).toBe("writer-model")
+    expect(resolveAgentSessionModel(baseConfig, novelConfig, "standard").model).toBe("workflow-model")
+    expect(resolveAgentSessionModel(baseConfig, novelConfig, "strict").model).toBe("workflow-model")
+  })
+})

+ 17 - 0
src/lib/novel/model-resolver.ts

@@ -5,6 +5,7 @@ import { hasUsableLlm } from "@/lib/has-usable-llm"
 import { getEffectiveMaxContextSize, getEffectiveMaxOutputTokens } from "@/lib/llm-providers"
 import { getStableAvailableModelKey, getEffectiveSavedModels } from "@/lib/llm-model-keys"
 import { normalizeUserLlmConfig } from "@/lib/llm-context-size"
+import type { AiWorkflowMode } from "@/lib/agent/workflow-mode"
 
 export type NovelTaskType = "writing" | "review" | "summary" | "extract" | "lint" | "deAi"
 
@@ -190,6 +191,22 @@ export function resolveNovelModel(
   return toUnusableConfig(llmConfig)
 }
 
+/**
+ * 解析 AI 会话主 Agent 使用的模型。
+ * 快速模式像普通对话/大纲绘制一样由主 Agent 直接出稿,必须用聊天框选中的写作模型。
+ * 标准/严格模式主 Agent 只负责编排,继续用默认模型;正文由 chapterWritingLlmConfig 走聊天模型。
+ */
+export function resolveAgentSessionModel(
+  baseConfig: LlmConfig,
+  novelConfig: NovelConfig,
+  workflowMode: AiWorkflowMode,
+): LlmConfig {
+  if (workflowMode === "fast") {
+    return resolveNovelModel(baseConfig, novelConfig, "writing")
+  }
+  return resolveDefaultModel(baseConfig)
+}
+
 export function formatResolvedModelLabel(
   config: LlmConfig,
   providerConfigs: Record<string, ProviderOverride>,