Explorar el Código

feat(agent): integrate agent chat panel with references

Mochocyang hace 2 meses
padre
commit
6b0827571a

+ 39 - 0
agent-duihua-分支说明.md

@@ -0,0 +1,39 @@
+# agent-duihua 分支说明
+
+## 分支目标
+
+本分支用于将 AI 会话面板接入 Agent 工具调用流程,并接入 @引用输入与引用弹窗。
+
+## 使用要求
+
+1. 不合并到 main,等待用户完成全面软件测试后再决定合并。
+2. 本分支只处理 AI 会话面板,不处理 AI 大纲面板。
+3. 保留旧 `ChatInput` 组件源码,不删除不修改。
+4. 面向用户的提示语保持中文。
+5. 修改完成后必须完成源码启动、旧功能测试、构建和便携版打包验证。
+
+## 本次更新
+
+### 20260701-092043
+
+- 将 `chat-panel` 的发送流程接入 `AgentRunner`。
+- 将 AI 会话输入区替换为 `ReferenceInput`,并接入 `ReferencePickerDialog`。
+- 用户消息保存 `attachedReferences`,assistant 占位消息保存 `isAgentRunning` 与 `agentToolCalls`。
+- 用户消息区域展示只读引用芯片。
+- `useAgentConfig` 返回已加载技能配置,并修正内置工具注册使用的 wiki 目录路径。
+- 保留章节生成相关的 QM-QUAI、目标章节解析和角色灵魂确认逻辑,避免旧功能回退。
+
+## 验证记录
+
+- `npm.cmd exec -- vitest run src/components/chat/chat-panel.spec.tsx src/components/chat/chat-message.spec.tsx src/hooks/use-agent-config.spec.ts`:17 个用例通过。
+- `npm.cmd exec -- vitest run src/components/layout/chat-layout.test.ts`:11 个用例通过。
+- `npm.cmd run typecheck`:通过。
+- `npm.cmd run build`:通过,存在既有 Vite chunk/动态导入警告。
+- `npm.cmd run test:mocks`:2088/2089 个用例通过,剩余 1 个既有失败为 `src/lib/changelog.spec.ts` 未包含当前 `2.2.31`。
+- 源码启动:Vite ready,`http://127.0.0.1:1420/` 返回 HTTP 200。
+- `npm.cmd run build:portable`:通过,生成 `release-portable\QMaiWrite.exe`。
+
+## Git 状态
+
+- 本次更新随当前分支提交入库。
+- 不合并 main。

+ 28 - 1
src/components/chat/chat-message.spec.tsx

@@ -2,8 +2,9 @@ import { renderToStaticMarkup } from "react-dom/server"
 import { readFileSync } from "node:fs"
 import { resolve } from "node:path"
 import { describe, expect, it } from "vitest"
-import { StreamingMessage } from "./chat-message"
+import { ChatMessage, StreamingMessage } from "./chat-message"
 import { getDeepChapterToggleButtonClass } from "./chat-panel"
+import type { DisplayMessage } from "@/stores/chat-store"
 
 function tenThinkingLines(): string {
   return Array.from({ length: 10 }, (_value, index) => `stage line ${index + 1}`).join("\n")
@@ -47,6 +48,32 @@ describe("deep chapter thinking toggle style", () => {
   })
 })
 
+describe("chat message references", () => {
+  it("renders attached reference chips on user messages", () => {
+    const message: DisplayMessage = {
+      id: "msg-1",
+      role: "user",
+      content: "请参考这一章",
+      timestamp: 1,
+      conversationId: "conv-1",
+      attachedReferences: [{
+        id: "ref-1",
+        category: "chapter",
+        title: "第一章",
+        path: "C:/Novel/wiki/chapters/第一章.md",
+        displayTitle: "第一章",
+      }],
+    }
+
+    const html = renderToStaticMarkup(<ChatMessage message={message} />)
+
+    expect(html).toContain("请参考这一章")
+    expect(html).toContain("@第一章")
+    expect(html).toContain('data-reference-id="ref-1"')
+    expect(html).not.toContain("移除引用")
+  })
+})
+
 describe("chapter save preview sync regression", () => {
   it("always routes AI chapter saves to the next chapter instead of reusing the current chapter", () => {
     const source = readFileSync(resolve(__dirname, "chat-panel.tsx"), "utf8")

+ 11 - 1
src/components/chat/chat-message.tsx

@@ -16,6 +16,7 @@ import { refreshProjectState } from "@/lib/project-refresh"
 import { getLastQueryPages } from "@/components/chat/chat-shared"
 import { FileEditPreview } from "@/components/chat/file-edit-preview"
 import { AgentToolCallMessage } from "@/components/chat/agent-tool-call-message"
+import { ReferenceChip } from "@/components/reference/ReferenceChip"
 import type { DisplayMessage } from "@/stores/chat-store"
 
 import { convertLatexToUnicode } from "@/lib/latex-to-unicode"
@@ -81,7 +82,16 @@ export function ChatMessage({ message, isLastAssistant, onRegenerate, novelMode,
           {message.discarded ? (
             <span className="italic text-xs opacity-60">已废弃</span>
           ) : isUser ? (
-            <p dir="auto" className="whitespace-pre-wrap break-words">{message.content}</p>
+            <>
+              <p dir="auto" className="whitespace-pre-wrap break-words">{message.content}</p>
+              {message.attachedReferences && message.attachedReferences.length > 0 && (
+                <div className="mt-1 flex flex-wrap gap-1">
+                  {message.attachedReferences.map((token) => (
+                    <ReferenceChip key={token.id} token={token} readonly />
+                  ))}
+                </div>
+              )}
+            </>
           ) : (
             <>
               {message.agentToolCalls && message.agentToolCalls.length > 0 && (

+ 19 - 0
src/components/chat/chat-panel.spec.tsx

@@ -24,3 +24,22 @@ describe("chat-panel de-AI skill handling", () => {
     expect(source).not.toContain('          {t(novelMode ? "novel.chat.newChat" : "chat.newChat")}')
   })
 })
+
+describe("chat-panel agent reference integration", () => {
+  it("replaces the legacy chat input with the reference input and picker", () => {
+    expect(source).toContain("<ReferenceInput")
+    expect(source).toContain("<ReferencePickerDialog")
+    expect(source).toContain("insertTokensRef")
+    expect(source).not.toContain("<ChatInput")
+    expect(source).not.toContain('from "./chat-input"')
+  })
+
+  it("routes sends through AgentRunner and stores reference/tool metadata", () => {
+    expect(source).toContain("useAgentConfig")
+    expect(source).toContain("new AgentRunner()")
+    expect(source).toContain("attachedReferences")
+    expect(source).toContain("isAgentRunning")
+    expect(source).toContain("agentToolCalls")
+    expect(source).toContain("当前模型不支持Agent功能,请更换模型")
+  })
+})

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 452 - 709
src/components/chat/chat-panel.tsx


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

@@ -179,6 +179,7 @@ describe("useAgentConfig", () => {
     expect(result.supportsTools).toBe(true)
     expect(result.skillConfigLoaded).toBe(true)
     expect(result.config).not.toBeNull()
+    expect(result.skillConfig?.defaultSkillId).toBe("built-in:comprehensive")
     expect(result.config?.tools.length).toBeGreaterThan(0)
     expect(result.registry.list().some((tool) => tool.name === "read_chapter")).toBe(true)
     expect(result.registry.list().some((tool) => tool.name === "apply_skill")).toBe(true)

+ 6 - 1
src/hooks/use-agent-config.ts

@@ -4,6 +4,7 @@ import { useChatStore } from "@/stores/chat-store"
 import { useOutlineChatStore } from "@/stores/outline-chat-store"
 import { loadDeAiSkillConfig, type DeAiSkillConfig } from "@/lib/novel/de-ai-skill-library"
 import { resolveModelConfig } from "@/lib/novel/model-resolver"
+import { normalizePath } from "@/lib/path-utils"
 import { ToolRegistry } from "@/lib/agent/registry"
 import { buildAgentConfig, modelSupportsTools } from "@/lib/agent/config"
 import type { AgentConfig } from "@/lib/agent/types"
@@ -13,6 +14,7 @@ export interface UseAgentConfigResult {
   registry: ToolRegistry
   supportsTools: boolean
   skillConfigLoaded: boolean
+  skillConfig: DeAiSkillConfig | null
 }
 
 export function useAgentConfig(systemPrompt: string): UseAgentConfigResult {
@@ -90,13 +92,15 @@ export function useAgentConfig(systemPrompt: string): UseAgentConfigResult {
         registry: new ToolRegistry(),
         supportsTools,
         skillConfigLoaded: false,
+        skillConfig,
       }
     }
 
     const llmConfig = resolveModelConfig(aiChatModel, baseLlmConfig, providerConfigs)
     const registry = new ToolRegistry()
+    const wikiPath = `${normalizePath(projectPath)}/wiki`
     const config = buildAgentConfig(aiChatModel, systemPrompt, registry, {
-      wikiPath: projectPath,
+      wikiPath,
       getSkillConfig,
       getChatConversations,
       getOutlineConversations,
@@ -108,6 +112,7 @@ export function useAgentConfig(systemPrompt: string): UseAgentConfigResult {
       registry,
       supportsTools: true,
       skillConfigLoaded: true,
+      skillConfig,
     }
   }, [
     aiChatModel,

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio