Просмотр исходного кода

feat: 采访支持继续对话,恢复agent状态追加到旧对话

Mochocyang 2 месяцев назад
Родитель
Сommit
165d90cf9d

+ 71 - 0
src/components/novel/story-simulation/interview-history-view.tsx

@@ -5,7 +5,10 @@ import { useStorySimulationStore } from "@/stores/story-simulation-store"
 import { useWikiStore } from "@/stores/wiki-store"
 import { loadInterviews, deleteInterview } from "@/lib/novel/story-simulation/interview-store"
 import { exportInterview } from "@/lib/novel/story-simulation/interview-export"
+import { deserializeSimulationSnapshot } from "@/lib/novel/story-simulation/simulation-serializer"
+import { loadSimulationResults } from "@/lib/novel/story-simulation/framework-store"
 import type { SavedInterview } from "@/lib/novel/story-simulation/interview-store"
+import type { NovelAgent } from "@/lib/novel/story-simulation/types"
 
 export function InterviewHistoryView() {
   const projectPath = useWikiStore((s) => s.project?.path)
@@ -16,10 +19,14 @@ export function InterviewHistoryView() {
   const setSavedInterviews = useStorySimulationStore((s) => s.setSavedInterviews)
   const setViewingInterview = useStorySimulationStore((s) => s.setViewingInterview)
   const setError = useStorySimulationStore((s) => s.setError)
+  const setContinuingInterviewId = useStorySimulationStore((s) => s.setContinuingInterviewId)
+  const setActiveChatAgent = useStorySimulationStore((s) => s.setActiveChatAgent)
+  const setAgentChatMessages = useStorySimulationStore((s) => s.setAgentChatMessages)
 
   const [loading, setLoading] = useState(false)
   const [deleting, setDeleting] = useState<string | null>(null)
   const [exporting, setExporting] = useState<string | null>(null)
+  const [resuming, setResuming] = useState(false)
 
   // 加载采访列表
   useEffect(() => {
@@ -86,6 +93,62 @@ export function InterviewHistoryView() {
     }
   }
 
+  const handleContinueInterview = async (interview: SavedInterview) => {
+    if (!projectPath) return
+    setResuming(true)
+    try {
+      let agents: NovelAgent[] = []
+
+      // 优先从采访记录的 agentSnapshot 恢复
+      if (interview.agentSnapshot) {
+        const { agents: deserializedAgents } = deserializeSimulationSnapshot(interview.agentSnapshot)
+        agents = deserializedAgents
+      }
+
+      // 若采访记录无快照,尝试从对应 frameworkId 的推演结果恢复
+      if (agents.length === 0 && interview.frameworkId) {
+        const results = await loadSimulationResults(projectPath, interview.frameworkId)
+        for (const r of results) {
+          if (r.agentSnapshot) {
+            const { agents: deserializedAgents } = deserializeSimulationSnapshot(r.agentSnapshot)
+            if (deserializedAgents.some((a) => a.name === interview.agentName)) {
+              agents = deserializedAgents
+              break
+            }
+          }
+        }
+      }
+
+      if (agents.length === 0) {
+        setError("无法恢复角色状态,仅支持只读查看")
+        setTimeout(() => setError(null), 3000)
+        return
+      }
+
+      // 找到对应角色的 agent
+      const targetAgent = agents.find((a) => a.name === interview.agentName)
+      if (!targetAgent) {
+        setError(`未找到角色「${interview.agentName}」的 agent 数据`)
+        setTimeout(() => setError(null), 3000)
+        return
+      }
+
+      // 加载旧对话消息到 store
+      setAgentChatMessages(interview.session.messages)
+      setActiveChatAgent({ id: targetAgent.characterId, name: targetAgent.name })
+      setContinuingInterviewId(interview.id)
+      setShowInterviewHistory(false)
+      setViewingInterview(null)
+      setError("已恢复采访,可继续对话")
+      setTimeout(() => setError(null), 2000)
+    } catch (err) {
+      setError(err instanceof Error ? err.message : "恢复失败")
+      setTimeout(() => setError(null), 3000)
+    } finally {
+      setResuming(false)
+    }
+  }
+
   const formatDate = (dateStr: string) => {
     try {
       const date = new Date(dateStr)
@@ -151,6 +214,14 @@ export function InterviewHistoryView() {
                       框架:{viewingInterview.frameworkTitle}
                     </span>
                   )}
+                  <Button
+                    variant="default"
+                    size="sm"
+                    onClick={() => handleContinueInterview(viewingInterview)}
+                    disabled={resuming}
+                  >
+                    {resuming ? "恢复中..." : "继续对话"}
+                  </Button>
                   <Button
                     variant="outline"
                     size="sm"

+ 12 - 0
src/components/novel/story-simulation/story-simulation-view.tsx

@@ -154,6 +154,8 @@ export function StorySimulationView() {
   const bumpListRefresh = useStorySimulationStore((s) => s.bumpListRefresh)
   const setSavedResults = useStorySimulationStore((s) => s.setSavedResults)
   const setShowInterviewHistory = useStorySimulationStore((s) => s.setShowInterviewHistory)
+  const continuingInterviewId = useStorySimulationStore((s) => s.continuingInterviewId)
+  const setContinuingInterviewId = useStorySimulationStore((s) => s.setContinuingInterviewId)
 
   // 保存仿真后的 agents 和 state 供采访使用
   const lastAgentsRef = useRef<NovelAgent[]>([])
@@ -562,11 +564,21 @@ export function StorySimulationView() {
       const agentSnapshot = lastSimulationStateRef.current && lastAgentsRef.current.length > 0
         ? serializeSimulationState(lastSimulationStateRef.current, lastAgentsRef.current)
         : undefined
+      // 续聊模式下询问覆盖原采访或另存为新采访
+      let existingId: string | undefined
+      if (continuingInterviewId) {
+        const choice = confirm("覆盖原采访对话?\n\n确定 = 覆盖原采访\n取消 = 另存为新采访")
+        if (choice) {
+          existingId = continuingInterviewId
+        }
+      }
       await saveInterview(projectPath, session, {
         frameworkId: currentFramework?.id,
         frameworkTitle: currentFramework?.title,
         agentSnapshot,
+        existingId,
       })
+      setContinuingInterviewId(null)
       setError(`采访对话已保存(${agentChatMessages.length}条消息)`)
       setTimeout(() => setError(null), 3000)
     } catch (err) {

+ 9 - 0
src/stores/story-simulation-store.ts

@@ -72,6 +72,8 @@ export interface StorySimulationState {
   viewingInterview: SavedInterview | null
   /** 对比模式下要对比的结果ID(null表示不对比) */
   compareWithResultId: string | null
+  /** 当前续聊的采访ID(用于保存时判断覆盖/另存) */
+  continuingInterviewId: string | null
 
   setPhase: (phase: SimulationPhase) => void
   setMode: (mode: SimulationMode) => void
@@ -100,6 +102,9 @@ export interface StorySimulationState {
   setSavedInterviews: (interviews: SavedInterview[]) => void
   setViewingInterview: (interview: SavedInterview | null) => void
   setCompareWithResultId: (id: string | null) => void
+  setContinuingInterviewId: (id: string | null) => void
+  /** 设置采访消息列表 */
+  setAgentChatMessages: (messages: AgentChatMessage[]) => void
   reset: () => void
 }
 
@@ -130,6 +135,7 @@ export const useStorySimulationStore = create<StorySimulationState>((set) => ({
   savedInterviews: [],
   viewingInterview: null,
   compareWithResultId: null,
+  continuingInterviewId: null,
 
   setPhase: (phase) => set({ phase }),
   setMode: (mode) => set({ mode }),
@@ -160,6 +166,8 @@ export const useStorySimulationStore = create<StorySimulationState>((set) => ({
   setSavedInterviews: (savedInterviews) => set({ savedInterviews }),
   setViewingInterview: (viewingInterview) => set({ viewingInterview }),
   setCompareWithResultId: (compareWithResultId) => set({ compareWithResultId }),
+  setContinuingInterviewId: (continuingInterviewId) => set({ continuingInterviewId }),
+  setAgentChatMessages: (agentChatMessages) => set({ agentChatMessages }),
   reset: () =>
     set({
       phase: "idle",
@@ -179,5 +187,6 @@ export const useStorySimulationStore = create<StorySimulationState>((set) => ({
       savedInterviews: [],
       viewingInterview: null,
       compareWithResultId: null,
+      continuingInterviewId: null,
     }),
 }))