From e7123e56e3bed19decc796710b3638b8b2d060f3 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Tue, 23 Jun 2026 15:52:11 +0800 Subject: [PATCH 001/101] =?UTF-8?q?refactor:=201=E3=80=81=E9=87=8D?= =?UTF-8?q?=E6=9E=84ai-agent=E6=95=B0=E6=8D=AE=E5=B1=82=E7=BB=93=E6=9E=84?= =?UTF-8?q?=EF=BC=8C=E6=8B=86=E5=88=86=E9=A2=97=E7=B2=92=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E9=99=8D=E4=BD=8E=E6=B8=B2=E6=9F=93=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E5=8E=8B=E5=8A=9B=202=E3=80=81=E8=B0=83=E6=95=B4=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=EF=BC=8C=E5=B9=B6=E5=85=A8=E5=B1=80=E6=9B=BF=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/aiAgentType.d.ts | 2 +- .../ConcurrentStreamCard.tsx | 4 +- .../concurrentStreamFrame.ts | 4 +- .../ai-agent/components/ToolInvokerCard.tsx | 3 +- .../StreamingChatContent.tsx | 8 +- .../hooks/useStreamingChatContent.ts | 4 +- .../hooks/useTypedStream.ts | 4 +- .../aiGroupStreamCard/AIGroupStreamCard.tsx | 4 +- .../src/pages/ai-agent/store/ChatDataStore.ts | 4 +- .../main/src/pages/ai-agent/type/aiChat.d.ts | 103 +- .../aiReActTaskChat/AIReActTaskChatType.d.ts | 3 +- .../ai-re-act/hooks/AIAgentLogEmitter.ts | 90 + .../hooks/ChatMultiSessionController.ts | 346 ++++ .../src/pages/ai-re-act/hooks/aiRender.ts | 399 ++++- .../src/pages/ai-re-act/hooks/chatStore.ts | 207 +++ .../ai-re-act/hooks/explame/ChatNode.tsx | 92 + .../hooks/explame/ChatNotification.tsx | 26 + .../ai-re-act/hooks/explame/ChatReview.tsx | 48 + .../ai-re-act/hooks/explame/ChatWindow.tsx | 97 ++ .../hooks/explame/SessionRuntime.tsx | 5 + .../pages/ai-re-act/hooks/explame/layout.tsx | 50 + .../ai-re-act/hooks/grpcAIMessageHandlers.ts | 1487 +---------------- .../hooks/grpcStreamHandler/aiOther.ts | 516 ++++++ .../hooks/grpcStreamHandler/aiPerf.ts | 220 +++ .../hooks/grpcStreamHandler/aiReview.ts | 630 +++++++ .../hooks/grpcStreamHandler/aiSingleItem.ts | 494 ++++++ .../hooks/grpcStreamHandler/aiStream.ts | 510 ++++++ .../hooks/grpcStreamHandler/aiTaskDetail.ts | 213 +++ .../hooks/grpcStreamHandler/aiToolResult.ts | 276 +++ .../grpcAIOutputEventHandlers.ts | 25 + .../hooks/grpcStreamHandler/yakExecResult.ts | 115 ++ .../main/src/pages/ai-re-act/hooks/type.ts | 79 +- .../pages/ai-re-act/hooks/useAIPerfData.ts | 255 --- .../pages/ai-re-act/hooks/useCasualChat.ts | 193 +-- .../src/pages/ai-re-act/hooks/useChatIPC.ts | 316 +--- .../pages/ai-re-act/hooks/useNewChatIPC.ts | 41 + .../src/pages/ai-re-act/hooks/useTaskChat.ts | 156 +- .../pages/ai-re-act/hooks/useYakExecResult.ts | 170 -- .../main/src/pages/ai-re-act/hooks/utils.ts | 58 +- 39 files changed, 4502 insertions(+), 2755 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/AIAgentLogEmitter.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiPerf.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/grpcAIOutputEventHandlers.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useAIPerfData.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useYakExecResult.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts b/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts index 0a1c856ea9..757d4b201b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts @@ -4,7 +4,7 @@ import { AITreeNodeProps } from './aiTree/type' import { HoldGRPCStreamProps, StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' import { AITabsEnum } from './defaultConstant' import { AIAgentGrpcApi, AIStartParams } from '../ai-re-act/hooks/grpcApi' -import { AIChatQSData, AIStreamOutput, AITaskInfoProps } from '../ai-re-act/hooks/aiRender' +import { AIChatQSData, AIStreamOutput } from '../ai-re-act/hooks/aiRender' import { UseYakExecResultState, PlanLoadingStatus, CurrentExecTaskTree } from '../ai-re-act/hooks/type' import { ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' import { UseChatIPCEvents } from '@/pages/ai-re-act/hooks/type' diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index 83c3202926..b03826dc9a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -9,7 +9,7 @@ import { AIChatQSDataTypeEnum, ChatTaskDefaultGroup, type ChatTaskNodeGroup, - type ReActChatElement, + type ChatListRenderType, type ReActChatTaskElementSub, } from '@/pages/ai-re-act/hooks/aiRender' import { getAIStatusPresentation } from '../../utils/AIStatusUtils' @@ -27,7 +27,7 @@ const ConcurrentStreamCard: FC<{ elements: ReActChatTaskElementSub[] session: string token: string - chatType: ReActChatElement['chatType'] + chatType: ChatListRenderType isChildWindow?: boolean onRefresh?: () => void }> = ({ session, elements, chatType, token, isChildWindow, onRefresh }) => { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts index 5e3983aa35..997444b5d5 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts @@ -1,9 +1,9 @@ -import type { ReActChatElement, ReActChatTaskElementSub } from '@/pages/ai-re-act/hooks/aiRender' +import type { ChatListRenderType, ReActChatTaskElementSub } from '@/pages/ai-re-act/hooks/aiRender' export interface ConcurrentStreamFramePayload { session: string token: string - chatType: ReActChatElement['chatType'] + chatType: ChatListRenderType elements: ReActChatTaskElementSub[] taskName?: string } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx index 5d54983b2c..85eb94d9c1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx @@ -9,6 +9,7 @@ import { AIChatQSDataTypeEnum, AIToolResult, AIYakExecFileRecord, + ChatListRenderType, ReActChatBaseInfo, } from '@/pages/ai-re-act/hooks/aiRender' import FileList from './FileList' @@ -56,7 +57,7 @@ interface ToolInvokerCardProps { modalInfo?: ModalInfoProps operationInfo: OperationCardFooterProps data: AIToolResult - chatType: ReActChatBaseInfo['chatType'] + chatType: ChatListRenderType token: string } interface PreWrapperProps { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx index 2f5989b1ef..1fadc5553a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx @@ -1,5 +1,9 @@ import { AIStreamNode } from '@/pages/ai-re-act/aiReActChatContents/AIReActChatContents' -import { AIChatQSDataTypeEnum, type ReActChatElement, type ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' +import { + AIChatQSDataTypeEnum, + type ChatListRenderType, + type ReActChatRenderItem, +} from '@/pages/ai-re-act/hooks/aiRender' import { memo, type FC } from 'react' import { useTypedStream } from './hooks/useTypedStream' import AIGroupStreamCard from '../../aiGroupStreamCard/AIGroupStreamCard' @@ -16,7 +20,7 @@ type StreamingChatContentProps = ReActChatRenderItem & { } type SingleStreamProps = { - chatType: ReActChatElement['chatType'] + chatType: ChatListRenderType token: string streamClassName?: StreamCls session: string diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts index 560b25c06d..82d54317dd 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts @@ -1,11 +1,11 @@ import { useCallback, useRef, useState } from 'react' -import { ChatStream, ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' +import { ChatListRenderType, ChatStream } from '@/pages/ai-re-act/hooks/aiRender' import { useRafPolling } from '@/hook/useRafPolling/useRafPolling' import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' export interface UseStreamingChatContentParams { - chatType: ReActChatRenderItem['chatType'] + chatType: ChatListRenderType token: string session: string } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts index 4ad14da370..e381cb71ae 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts @@ -1,11 +1,11 @@ // hooks/useTypedStream.ts import { useMemo } from 'react' -import { ChatStream, ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' +import { ChatListRenderType, ChatStream } from '@/pages/ai-re-act/hooks/aiRender' import { useStreamingTypewriter } from './useStreamingTypewriter' import { useStreamingChatContent } from './useStreamingChatContent' export interface UseTypedStreamOptions { - chatType: ReActChatRenderItem['chatType'] + chatType: ChatListRenderType token: string session: string /** 单步最小输出字符数(下限),默认 2 */ diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index 158c4365cc..c1c9669e2a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -1,4 +1,4 @@ -import { ChatReferenceMaterialPayload, ReActChatElement } from '@/pages/ai-re-act/hooks/aiRender' +import { ChatListRenderType, ChatReferenceMaterialPayload, ReActChatElement } from '@/pages/ai-re-act/hooks/aiRender' import { type CSSProperties, useState, type FC, useRef, useEffect, useMemo } from 'react' import styles from './AIGroupStreamCard.module.scss' import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' @@ -23,7 +23,7 @@ export const Code: FC<{ code: ChatReferenceMaterialPayload; style: CSSProperties } const AIStreamNode: FC<{ - chatType: ReActChatElement['chatType'] + chatType: ChatListRenderType token: string index: number session: string diff --git a/app/renderer/src/main/src/pages/ai-agent/store/ChatDataStore.ts b/app/renderer/src/main/src/pages/ai-agent/store/ChatDataStore.ts index 172050a66b..1382b1229d 100644 --- a/app/renderer/src/main/src/pages/ai-agent/store/ChatDataStore.ts +++ b/app/renderer/src/main/src/pages/ai-agent/store/ChatDataStore.ts @@ -1,6 +1,6 @@ import { SetStateAction } from 'react' import type { AIChatData } from '../type/aiChat' -import { AIChatQSData, ReActChatBaseInfo } from '@/pages/ai-re-act/hooks/aiRender' +import type { AIChatQSData, ChatListRenderType } from '@/pages/ai-re-act/hooks/aiRender' import { AIModelTypeEnum } from '../defaultConstant' import { cloneDeep } from 'lodash' import { DefaultCurrentExecTaskTree, DefaultPlanItemDetailsData } from '@/pages/ai-re-act/hooks/defaultConstant' @@ -11,7 +11,7 @@ export type DeepPartial = { interface GetContentMapParams { session: string - chatType: ReActChatBaseInfo['chatType'] + chatType: ChatListRenderType mapKey: string } diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index fced7003fb..5cacd3deb6 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -1,8 +1,9 @@ -import { UseChatIPCState } from '@/pages/ai-re-act/hooks/type' -import { AIAgentGrpcApi, AIStartParams } from '@/pages/ai-re-act/hooks/grpcApi' -import { PlanItemDetailsData, ReActChatRenderItem, TodoListCardData } from '@/pages/ai-re-act/hooks/aiRender' -import { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' +import type { AIFileSystemPin, TaskChatTaskInfo, UseChatIPCState } from '@/pages/ai-re-act/hooks/type' +import type { AIAgentGrpcApi, AIInputEvent, AIStartParams } from '@/pages/ai-re-act/hooks/grpcApi' +import type { AIYakExecFileRecord, PlanItemDetailsData, ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' +import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' import type { AISource } from '@/pages/ai-re-act/hooks/grpcApi' +import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' /** 上下文字节统计 */ export interface AIContextStatsDetail { @@ -164,3 +165,97 @@ export interface QueryAISessionRequest { Source?: AISource[] } } + +export interface AIAgentChatData { + /** http_fuzz_request_change事件通知数据 */ + httpFuzzRequest?: AIAgentGrpcApi.HttpFuzzRequestChange + /** http_flow_fuzz_status事件通知数据 */ + httpFlowFuzzStatus?: AIAgentGrpcApi.GetHttpFlowFuzzStatus + /** 更新会话的标题 */ + sessionTitle?: string + /** 记忆列表 */ + memoryList: AIAgentGrpcApi.MemoryEntryList + /** 系统流信息(isSystem=true&type=stream) */ + systemStream: string + /** yaklang_code_change 数据 */ + yaklangCodeChange?: AIAgentGrpcApi.YaklangCodeChange + + /** 获取历史数据时的最早ID节点 */ + beforeID: { + timelineID: number + chatID: number + } + + /** 记录数据里所有的httpRunTimeIDs */ + httpRunTimeIDs: string[] + /** 记录数据里所有的riskRunTimeIDs */ + riskRunTimeIDs: string[] + /** 性能相关数据 */ + aiPerfData: { + /** 消耗Token */ + consumption: AIAgentGrpcApi.Consumption + /** 上下文压力 */ + pressure: Record + /** 首字符响应耗时 */ + firstCost: Record + /** 总对话耗时 */ + totalCost: Record + /** 上下文字节统计 */ + contextStats: AIContextStatsDetail + /** 上下文成分 */ + contextSections: AIContextSectionsDetail + } + /** 自由对话(ReAct)会话 */ + casualChat: { + /** react 任务对应的详情数据 */ + planDetails: PlanItemDetailsData + } + taskChat: { + /** 任务列表的子任务对应的详情数据 */ + planDetailsMap: Map + } + /** 会话内每条信息的详情 */ + contents: Map +} +export interface AIAgentChatMetaData { + /** 通过用户问题创建会话时的问题 */ + createChatQuestion?: AIInputEvent + /** 自由对话的实时记忆列表 */ + casualMemoryList: AIAgentGrpcApi.MemoryEntryList + /** 会话通信流建立成功后的UI回调触发事件 */ + onSessionStartSuccess?: () => void + /** 任务规划的实时记忆列表 */ + taskMemoryList: AIAgentGrpcApi.MemoryEntryList + /** 当前专注场景ID */ + focusOfTaskID: string + /** 通知消息的消失定时器 */ + notifyMessageTimer: NodeJS.Timeout | null + /** 当前自由对话问题的re_act_task_id */ + currentCasualTaskID: string + /** 当前任务规划问题的re_act_task_id|coordinatorId|status */ + currentTaskPlanID?: TaskChatTaskInfo + /** 当前任务规划正在进行中的节点uuid */ + currentTaskPlanActiveNode: Set + + /** 历史数据: review_release先出现的历史review数据的id-release */ + historyReviewReleaseID: Record + /** 当前plan_review对应的扩展数据ID */ + currentPlanReviewId: string + /** 当前进行中的review数据(包含自由对话和任务规划) */ + reviewData: Map + /** 当前plan_review的异步详细数据 */ + planReviewExtraData: Map + + /** 记录tool_xxx_stderr的stream数据 */ + toolStderrStreamData: Map + + /** 记录都存在过的系统信息uuid, 只展示最新的一条系统信息 */ + systemEventUUID: string[] + + /** 顶部卡片临时缓冲区 */ + cardKVPair: Map + cardKVPaidTimer: NodeJS.Timeout | null + + /** 用于文件操作记录的计数器 */ + execFileRecordOrder: number +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatType.d.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatType.d.ts index 4599013704..4b38e19835 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatType.d.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatType.d.ts @@ -1,6 +1,7 @@ import { YakitButtonProp } from '@/components/yakitUI/YakitButton/YakitButton' import { ReactNode } from 'react' import { AIStartParams } from '../hooks/grpcApi' +import { ChatListRenderType } from '../hooks/aiRender' export interface AIReActTaskChatProps { setShowFreeChat: (show: boolean) => void @@ -69,6 +70,6 @@ export interface AIPlanPromptProps { } export interface AIManualAdditionProps { - chatType: ReActChatBaseInfo['chatType'] + chatType: ChatListRenderType onCancel: () => void } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/AIAgentLogEmitter.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/AIAgentLogEmitter.ts new file mode 100644 index 0000000000..6289e6058d --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/AIAgentLogEmitter.ts @@ -0,0 +1,90 @@ +import { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' +import { formatTimestamp } from '@/utils/timeUtil' + +export class AIAgentLogEmitter { + // 临时缓冲区:存储还没接收完的 stream 字符串 + // 键名设计:由于要区分 session,使用 `${sessionId}_${eventUUID}` 作为唯一 Key + private streamBuffer = new Map() + + /** 发送日志数据到日志窗口的逻辑 */ + private emitToOtherWindow( + sessionId: string, + Timestamp: AIOutputEvent['Timestamp'], + level: string, + message: string, + isStream?: boolean, + ) { + const { ipcRenderer } = window.require('electron') + ipcRenderer.send('forward-ai-chat-log-data', { + sessionId, + level: level, + message: message, + timestamp: formatTimestamp(Timestamp), + isStream: isStream, + }) + } + + /** 🪐 核心分发入口:接收数据处理层传来的数据,决定是暂存还是直接发送 */ + public dispatch(params: { + session: string + type: 'log' | 'stream' + Timestamp: AIOutputEvent['Timestamp'] + /** log类型专属,stream类型该字段值为空 */ + log?: AIAgentGrpcApi.Log + /** stream类型专属,log类型该字段值为空 */ + stream?: { + NodeId: AIOutputEvent['NodeId'] + EventUUID: AIOutputEvent['EventUUID'] + content: string + status: 'start' | 'end' + } + }) { + const { session, type, Timestamp, log, stream } = params + + if (type === 'log') { + if (!log) return + this.emitToOtherWindow(session, Timestamp, log.level, log.message) + } + + if (type === 'stream') { + if (!stream) return + + const { NodeId, EventUUID, content, status } = stream + const mapKey = `${session}_${EventUUID}` + const currentText = this.streamBuffer.get(mapKey) || '' + const newChunk = currentText + content + if (status === 'end') { + // 拼接完成,直接发送 + this.emitToOtherWindow(session, Timestamp, NodeId, newChunk, true) + this.streamBuffer.delete(mapKey) + } else { + // 追加到临时缓冲区 + this.streamBuffer.set(mapKey, newChunk) + } + } + } + + /** 🛑 当会话被关闭时,清理可能因为异常断开而残留的 Buffer 内存 */ + public clearSessionBuffer(sessionId: string) { + for (const key of this.streamBuffer.keys()) { + if (key.startsWith(`${sessionId}_`)) { + this.streamBuffer.delete(key) + } + } + } + + /** 清空日志窗口里的所有内容 */ + public clearLogsWindow(sessionId: string) { + const { ipcRenderer } = window.require('electron') + ipcRenderer.invoke('clear-ai-chat-log-data') + } + /** 关闭日志窗口 */ + public closeLogsWindow(sessionId: string) { + this.clearSessionBuffer(sessionId) + const { ipcRenderer } = window.require('electron') + ipcRenderer.send('close-ai-chat-window') + } +} + +// 导出单例 +export const aiAgentLogEmitter = new AIAgentLogEmitter() diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts new file mode 100644 index 0000000000..9aef0bd7fc --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -0,0 +1,346 @@ +import type { AIAgentChatData, AIAgentChatMetaData } from '@/pages/ai-agent/type/aiChat' +import type { AIAgentGrpcApi, AIInputEvent, AIOutputEvent, AIStartParams } from './grpcApi' +import { createChatStore } from './chatStore' +import { Uint8ArrayToString } from '@/utils/str' +import { AIAgentSettingDefault, AIModelTypeEnum } from '@/pages/ai-agent/defaultConstant' +import cloneDeep from 'lodash/cloneDeep' +import { DefaultMemoryList, DefaultTodoListCardData } from './defaultConstant' +import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHandlers' +import { genExecTasks, handleGrpcDataPushLog } from './utils' +import type { AIChatIPCStartParams, AIChatSendParams } from './type' +import { yakitNotify } from '@/utils/notification' +import { AIChatQSDataTypeEnum, AIReviewType } from './aiRender' +import { aiAgentLogEmitter } from './AIAgentLogEmitter' + +const { ipcRenderer } = window.require('electron') + +/** 生成AI-Agent会话数据实例 */ +const genAIAgentChatData = (): AIAgentChatData => { + const defaultData: AIAgentChatData = { + httpFuzzRequest: undefined, + httpFlowFuzzStatus: undefined, + sessionTitle: '', + memoryList: cloneDeep(DefaultMemoryList), + systemStream: '', + yaklangCodeChange: undefined, + + beforeID: { + timelineID: -1, + chatID: -1, + }, + + httpRunTimeIDs: [], + riskRunTimeIDs: [], + aiPerfData: { + consumption: { + cache_hit_token: 0, + input_consumption: 0, + output_consumption: 0, + consumption_uuid: '', + tier_consumption: { + [AIModelTypeEnum.TierIntelligent]: { + cache_hit_token: 0, + input_consumption: 0, + output_consumption: 0, + }, + [AIModelTypeEnum.TierLightweight]: { + cache_hit_token: 0, + input_consumption: 0, + output_consumption: 0, + }, + [AIModelTypeEnum.TierVision]: { + cache_hit_token: 0, + input_consumption: 0, + output_consumption: 0, + }, + }, + }, + pressure: { + [AIModelTypeEnum.TierIntelligent]: [], + [AIModelTypeEnum.TierLightweight]: [], + [AIModelTypeEnum.TierVision]: [], + }, + firstCost: { + [AIModelTypeEnum.TierIntelligent]: [], + [AIModelTypeEnum.TierLightweight]: [], + [AIModelTypeEnum.TierVision]: [], + }, + totalCost: { + [AIModelTypeEnum.TierIntelligent]: [], + [AIModelTypeEnum.TierLightweight]: [], + [AIModelTypeEnum.TierVision]: [], + }, + contextStats: { + prompt_bytes: 0, + prompt_tokens: 0, + data: { + times: [], + total_prompt_bytes: [], + total_prompt_tokens: [], + role_order: [], + role_labels: {}, + role_series: {}, + role_tokens: {}, + }, + }, + contextSections: { summary: new Map(), sections: [] }, + }, + + casualChat: { + todoList: cloneDeep(DefaultTodoListCardData), + }, + taskChat: { + todoListMap: new Map(), + }, + contents: new Map(), + } + return cloneDeep(defaultData) +} +/** 生成AI-Agent会话的临时记录数据 */ +const genAIAgentChatMetaData = (): AIAgentChatMetaData => { + return { + createChatQuestion: undefined, + casualMemoryList: cloneDeep(DefaultMemoryList), + taskMemoryList: cloneDeep(DefaultMemoryList), + focusOfTaskID: '', + notifyMessageTimer: null, + currentCasualTaskID: '', + currentTaskPlanID: undefined, + currentTaskPlanActiveNode: new Set(), + historyReviewReleaseID: {}, + currentPlanReviewId: '', + reviewData: new Map(), + planReviewExtraData: new Map(), + toolStderrStreamData: new Map(), + systemEventUUID: [], + cardKVPair: new Map(), + cardKVPaidTimer: null, + execFileRecordOrder: 1, + } +} + +export class ChatMultiSessionController { + private storePool = new Map>() + private rawDataPool = new Map() + private requestPool = new Map() + private metaPool = new Map() + + private readyChannels = new Set() + + /** 获取对应会话的所有数据集 */ + public ensureSession(sessionId: string) { + if (!this.storePool.has(sessionId)) { + this.storePool.set(sessionId, createChatStore()) + this.rawDataPool.set(sessionId, genAIAgentChatData()) + this.requestPool.set(sessionId, cloneDeep(AIAgentSettingDefault)) + this.metaPool.set(sessionId, genAIAgentChatMetaData()) + } + return { + store: this.storePool.get(sessionId)!, + rawData: this.rawDataPool.get(sessionId)!, + request: this.requestPool.get(sessionId)!, + meta: this.metaPool.get(sessionId)!, + } + } + + /** UI 层建立连接时,登记并绑定当前渲染周期的 UI 状态操纵函数 */ + public registerActiveChannel(sessionId: string) { + this.readyChannels.add(sessionId) + } + + /** 建立指定session会话的连接 */ + public handleStartSession(sessionId: string, requestParams: AIChatIPCStartParams, cb?: () => void) { + const { params } = requestParams + const { request, meta } = this.ensureSession(sessionId) + if (params.Params?.UserQuery) { + meta.createChatQuestion = { + IsFreeInput: true, + FreeInput: params.Params.UserQuery, + AttachedResourceInfo: params.AttachedResourceInfo, + FocusModeLoop: params.FocusModeLoop, + } + } + Object.assign(request, params.Params) + meta.onSessionStartSuccess = cb + + ipcRenderer.invoke('start-ai-re-act', sessionId, params) + } + + /** 主动向grpc发送请求 */ + public handleSendMessage(payload: AIChatSendParams) { + try { + const { token, type, params, optionValue, extraValue } = payload + const isExist = this.readyChannels.has(token) + if (!isExist) { + yakitNotify('warning', `[大管家] 会话 ${token} 未 Ready,请求已进队列排队...`) + return + } + + const { store, rawData, meta } = this.ensureSession(token) + if (params.IsFreeInput) { + store.getState().updateState({ casualTitle: '等待回复中...' }) + } + + switch (type) { + case 'casual': + if (params.IsInteractiveMessage && params.InteractiveId) { + const review = meta.reviewData.get(params.InteractiveId) + if (!review) { + yakitNotify('error', '未获取到 review 信息, 操作无效') + return + } + + ;(review.data as AIReviewType).selected = params.InteractiveJSONInput + ;(review.data as AIReviewType).optionValue = optionValue + + const chatData = cloneDeep(review) + meta.reviewData.delete(params.InteractiveId) + rawData.contents.set(params.InteractiveId, chatData) + store.getState().incrementNodeVersion(params.InteractiveId, 'item') + } + break + case 'task': + if (params.IsInteractiveMessage && params.InteractiveId) { + const review = meta.reviewData.get(params.InteractiveId) + if (!review) { + yakitNotify('error', '未获取到 review 信息, 操作无效') + return + } + + ;(review.data as AIReviewType).selected = params.InteractiveJSONInput + ;(review.data as AIReviewType).optionValue = optionValue + + const chatData = cloneDeep(review) + if (chatData.type === AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { + const tasks = chatData.data + const plans = genExecTasks(tasks.plans.root_task) + store.getState().updatePlanTree({ + task_tree: cloneDeep(plans), + root_task_name: tasks.plans.root_task.name, + }) + } + meta.reviewData.delete(params.InteractiveId) + rawData.contents.set(params.InteractiveId, chatData) + store.getState().dispatchStreamingNode({ + chatType: 'reAct', + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } + break + + default: + this.requestMessage(token, params) + break + } + } catch (error) {} + } + /** 向连接中的会话发送请求 */ + private requestMessage(sessionId: string, request: AIInputEvent) { + ipcRenderer.invoke('send-ai-re-act', sessionId, request) + } + + // 💥 核心替换:完美接管原 useChatIPC 里的巨型数据分发逻辑! + public handleGrpcOutputEvent(sessionId: string, res: AIOutputEvent) { + // 所有数据,均抄送一份到日志中 + handleGrpcDataPushLog({ info: res, pushLog: () => {} }) + + const { store, rawData, request, meta } = this.ensureSession(sessionId) + + if (res.Type === 'pong') { + const { createChatQuestion } = meta + if (createChatQuestion) { + this.requestMessage(sessionId, createChatQuestion) + meta.createChatQuestion = undefined + store.getState().updateState({ casualTitle: '等待回复中...' }) + } else { + // 调用历史数据恢复方法 + } + meta?.onSessionStartSuccess?.() + return + } + + if (res.Type === 'structured') { + let ipcContent = Uint8ArrayToString(res.Content) || '' + const obj = JSON.parse(ipcContent) || '' + + if (obj?.level) { + // 日志类型数据 + const data = obj as AIAgentGrpcApi.Log + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: res.Timestamp, + log: data, + }) + return + } + } + + let funcKey = res.Type + if ( + res.Type === 'structured' && + [ + 'session_title', + 'timeline_item', + 'react_task_enqueue', + 'react_task_dequeue', + 'queue_info', + 'react_task_status_changed', + 'status', + 'stream-finished', + ].includes(res.NodeId) + ) { + funcKey = res.NodeId + } else if (res.Type === 'api_request_failed' && res.NodeId === 'ai_call_failure') { + funcKey = res.NodeId + } else if (res.Type === 'report_finish' || res.NodeId === 'report-finish') { + funcKey = res.NodeId + } else if (res.Type === 'structured' && res.NodeId === 'system') { + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) || '' + if (data && typeof data === 'object' && data?.type === 'push_task') { + funcKey = 'push_task' + } else if (data && typeof data === 'object' && data?.type === 'pop_task') { + funcKey = 'pop_task' + } + } + const handleFunc = grpcAIMessageHandlers[funcKey || ''] + if (handleFunc) { + handleFunc({ + res, + chatType: meta.currentTaskPlanID?.coordinatorId === res.CoordinatorId ? 'task' : 'reAct', + store, + rawData, + request, + meta, + pushLog: () => {}, + sendRequest: (request) => this.requestMessage(sessionId, request), + }) + return + } + } + + // 监听 session-error 事件 + public handleSessionError(sessionId: string, error: any) {} + // 监听 session-end 事件 + public handleSessionEnd(sessionId: string, res: any) {} + + /** 关闭指定session的连接 */ + public forceCloseSession(sessionId: string) { + ipcRenderer.invoke('cancel-ai-re-act', sessionId).catch(() => {}) + // ipcRenderer.removeAllListeners(`${sessionId}-data`) + // ipcRenderer.removeAllListeners(`${sessionId}-end`) + // ipcRenderer.removeAllListeners(`${sessionId}-error`) + + this.readyChannels.delete(sessionId) + // crossWindowEmitter.clearSessionBuffer(sessionId) // 清理日志内容 + } +} + +export const globalSessionEngine = new ChatMultiSessionController() diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index fefe930b66..4a1e755459 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -1,7 +1,167 @@ import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' import type { AIAgentGrpcApi, AIOutputEvent, AITaskStatusType, AIOutputI18n, AIInputEvent } from './grpcApi' -import type { AIChatIPCStartParams } from './type' +import type { + AIChatIPCStartParams, + AIFileSystemPin, + AIQuestionQueues, + CurrentExecTaskTree, + PlanLoadingStatus, + UseAIMessageDataState, +} from './type' +// #region AI-Agent 非会话列表外的渲染数据 +/** 插件执行中的文件操作记录 */ +export interface AIYakExecFileRecord extends StreamResult.Log { + /** 前端主动对接口流输出的文件记录进行先后操作的记录 */ + order: number +} + +/** 任务规划-可执行任务的数据结构 */ +export interface AITaskInfoProps extends AIAgentGrpcApi.PlanTask { + /** 层级(代表在树里的第几层) */ + level: number + /** 是否是叶子任务节点 */ + isLeaf: boolean +} + +/** UI:待办清单卡片数据*/ +export interface TodoListCardData { + items: AIAgentGrpcApi.TodoListUpdateItem[] + stats: AIAgentGrpcApi.TodoListUpdateStats + /** UI定时刷新数据渲染,用于确定数据是否有更新 */ + uuid: string +} + +/** 用户输入框上的AI提示消息 */ +export interface AIInputNotifyMessage { + type: 'notify' | 'rate-limit' + content: string + label: AIOutputI18n +} + +export type ForgesAndSkillsDynamicItem = Omit & + Omit & { + category: 'forge' | 'skill' + } + +/** 任务树节点的详情数据 */ +export interface PlanItemDetailsData { + /** UI定时刷新数据渲染,用于确定数据是否有更新 */ + uuid: string + /** 任务id */ + taskId: string + todoList: TodoListCardData + tool: { + fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] + dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicToolItem[] + } + forges: { + fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] + dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicForgesItem[] + } + skills: { + fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] + dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicSkillsItem[] + } + plugins: { + fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] + dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicToolItem[] + } + mcp: { + fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] + dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicToolItem[] + } + /** 目前没有这个数据 */ + mcpServices: { + fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] + dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicToolItem[] + } + perception: AIAgentGrpcApi.PerceptionData + execution: AIAgentGrpcApi.SessionSnapshot['execution'] + backgroundProcesses: AIAgentGrpcApi.SessionSnapshot['background_processes'] +} +// #endregion + +// #region AI-Agent 用于触发会话列表的渲染数据 +/** 会话列表类型(任务规划|自由对话) */ +export type ChatListRenderType = 'reAct' | 'task' + +/** 会话列表的数据来源 */ +export enum ReActChatDataOriginEnum { + IndexedDB = 'indexedDB', + GrpcHistoryData = 'grpc_history_data', + GrpcRealtimeData = 'grpc_realtime_data', +} +export type ReActChatDataOrigin = `${ReActChatDataOriginEnum}` + +/** 响应式骨架模型 (纯净的字典与关系树) */ +export interface ReActChatBaseMeta { + token: string + type: AIChatQSDataType + renderNum: number // 精准驱动打字机刷新的原子计数器,历史记录中永远为 0 +} +export interface ReActChatItemMeta extends ReActChatBaseMeta { + kind: 'item' +} +export interface ReActChatGroupMeta extends ReActChatBaseMeta { + kind: 'group' + childrenTokens: string[] +} +export interface ReActChatTaskMeta extends ReActChatBaseMeta { + kind: 'task' + childrenTokens: string[] +} + +export type ReActChatMeta = ReActChatItemMeta | ReActChatGroupMeta | ReActChatTaskMeta + +/** UI 视图渲染路标 (控制元素怎么画、要不要动画、数据从哪来) */ +export interface ReActChatRenderElement { + kind: 'item' | 'group' | 'task' + token: string + chatType: ChatListRenderType + isCached: boolean // 核心结界:决定是否触发工具链折叠与渐入动画 + cacheOrder: number // 前端纯视觉排序权重 + dataOrigin: ReActChatDataOrigin // 业务物理来源 +} + +/** 控制UI渲染的数据数组元素 */ +export interface ReActChatBaseInfo { + chatType: ChatListRenderType + token: string + /** 决定具体样式的组件路由 */ + type: AIChatQSDataType + /** 高频更新时精准触发单项重渲染的“版本号” */ + renderNum: number + + /** 是否是已加载的历史缓存数据(用于拦截重复写入 DB) */ + isCached?: boolean + /** 缓存数据里的顺序 */ + cacheOrder?: number +} + +/** 独立 UI 节点 */ +export interface ReActChatElement extends ReActChatBaseInfo { + kind: 'item' +} +/** stream合成组group的节点 */ +export interface ReActChatGroupElement extends ReActChatBaseInfo { + kind: 'group' + children: ReActChatElement[] +} + +/** 进入 task 容器内部的子节点类型集合 */ +export type ReActChatTaskElementSub = ReActChatElement | ReActChatGroupElement +/** 任务内的所有节点 */ +export interface ReActChatTaskElement extends ReActChatBaseInfo { + kind: 'task' + children: ReActChatTaskElementSub[] +} + +export type ReActChatRenderItem = ReActChatElement | ReActChatGroupElement | ReActChatTaskElement + +// #endregion + +// #region UI元素渲染的详细数据结构 /** 工具流式输出里的可选操作列表 */ export interface ToolStreamSelectors { callToolId: string @@ -108,12 +268,6 @@ export type AIReviewType = | UIRequireUserInteractive | UIExecAIForgeReview -/** 插件执行中的文件操作记录 */ -export interface AIYakExecFileRecord extends StreamResult.Log { - /** 前端主动对接口流输出的文件记录进行先后操作的记录 */ - order: number -} - /** 工具执行结果的决策展示数据 */ export interface AIToolCallDecision extends Omit { i18n: AIOutputI18n @@ -159,56 +313,6 @@ export interface ReportFinishCardData { content: string } -/** UI:待办清单卡片数据*/ -export interface TodoListCardData { - items: AIAgentGrpcApi.TodoListUpdateItem[] - stats: AIAgentGrpcApi.TodoListUpdateStats - /** UI定时刷新数据渲染,用于确定数据是否有更新 */ - uuid: string -} - -export type ForgesAndSkillsDynamicItem = Omit & - Omit & { - category: 'forge' | 'skill' - } - -/** 任务树节点的详情数据 */ -export interface PlanItemDetailsData { - /** UI定时刷新数据渲染,用于确定数据是否有更新 */ - uuid: string - /** 任务id */ - taskId: string - todoList: TodoListCardData - tool: { - fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] - dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicToolItem[] - } - forges: { - fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] - dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicForgesItem[] - } - skills: { - fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] - dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicSkillsItem[] - } - plugins: { - fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] - dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicToolItem[] - } - mcp: { - fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] - dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicToolItem[] - } - /** 目前没有这个数据 */ - mcpServices: { - fixed: AIAgentGrpcApi.PlanItemDetailsFixedItem[] - dynamic: AIAgentGrpcApi.PlanItemDetailsDynamicToolItem[] - } - perception: AIAgentGrpcApi.PerceptionData - execution: AIAgentGrpcApi.SessionSnapshot['execution'] - backgroundProcesses: AIAgentGrpcApi.SessionSnapshot['background_processes'] -} - /** UI:发包统计卡片数据(由 http_flow_fuzz_status 事件驱动) */ export interface HttpFlowFuzzStatusCardData { fuzz_id: string @@ -220,7 +324,9 @@ export interface HttpFlowFuzzStatusCardData { /** working 推送的进度;finish 时保留最后一次 */ progress?: AIAgentGrpcApi.HttpFlowFuzzStatusProgress } +// #endregion +// #region chat 问答内容组件的类型集合(包括了类型推导) export enum AIChatQSDataTypeEnum { /**用户的自由输入 */ QUESTION = 'question', @@ -273,43 +379,8 @@ export enum AIChatQSDataTypeEnum { /** 任务规划-未标识组的默认组 */ TASK_DEFAULT_GROUP = 'task_default_group', } - export type AIChatQSDataType = `${AIChatQSDataTypeEnum}` -/** 控制UI渲染的数据数组元素 */ -export interface ReActChatBaseInfo { - chatType: 'reAct' | 'task' - token: string - type: AIChatQSDataType - /** 触发渲染的次数, 无实际逻辑意义 */ - renderNum: number - /** 是否是已缓存数据 */ - isCached?: boolean - /** 缓存数据里的顺序 */ - cacheOrder?: number -} - -/** 独立 UI 节点 */ -export interface ReActChatElement extends ReActChatBaseInfo { - kind: 'item' -} -/** stream合成组group的节点 */ -export interface ReActChatGroupElement extends ReActChatBaseInfo { - kind: 'group' - children: ReActChatElement[] -} - -/** 进入 task 容器内部的子节点类型集合 */ -export type ReActChatTaskElementSub = ReActChatElement | ReActChatGroupElement -/** 任务内的所有节点 */ -export interface ReActChatTaskElement extends ReActChatBaseInfo { - kind: 'task' - children: ReActChatTaskElementSub[] -} - -export type ReActChatRenderItem = ReActChatElement | ReActChatGroupElement | ReActChatTaskElement - -// #region chat 问答内容组件的类型集合(包括了类型推导) export interface AIChatQSDataBase { type: T data: U @@ -394,3 +465,147 @@ export type AIChatQSData = | ChatReportFinish | ChatTaskDefaultGroup // #endregion + +/** @name 数据状态机定义 */ +export interface ChatStoreState { + /** 会话执行状态 */ + execute: boolean + + httpFuzzRequestUpdate: number + httpFlowFuzzStatusUpdate: number + sessionTitleUpdate: number + /** 记忆列表 */ + memoryListUpdate: number + /** 系统流信息(isSystem=true&type=stream) */ + updateSystemStream: number + /** yaklang_code_change 更新 */ + yaklangCodeChangeUpdate: number + + /** 接口运行过程中的数据文件夹合集 */ + grpcFolders: AIFileSystemPin[] + /** 时间线 */ + reActTimelines: AIAgentGrpcApi.TimelineItem[] + + /** 流推送的提示文案(notify / rate-limit),展示时长由 duration 系列字段控制,到期自动清空 */ + notifyMessage: AIInputNotifyMessage | null + /** 任务规划历史数据-任务树 */ + planHistoryList: AIAgentGrpcApi.PlanHistoryList + /** 问题队列信息 */ + questionQueue: AIQuestionQueues + + /** 运行时产生http数据(是否显示tab,是否刷新数据) */ + httpTabShow: boolean + httpTabUpdate: number + /** 运行时产生risk数据(是否显示tab,是否刷新数据) */ + riskTabShow: boolean + riskTabUpdate: number + + // #region 会话列表相关数据 + /** 自由对话的loading 显示的文案 */ + casualTitle: string + /** 自由对话的是否进行中 */ + casualLoading: boolean + /** 场景状态(仅供自由对话[reAct])使用 */ + focusMode: string + /** UI是否显示中间的任务规划列表 */ + showPlanList: boolean + /** 任务规划的loading状态信息 */ + taskStatus: PlanLoadingStatus + /** 任务规划当前显示的review数据 */ + currentPlanReviewData: AIChatQSData | undefined + + items: Record + groups: Record + tasks: Record + + casualChat: { + elements: ReActChatRenderElement[] + todoListUpdate: number + } + taskChat: { + elements: ReActChatRenderElement[] + plan: CurrentExecTaskTree + } + // #endregion + + card: AIAgentGrpcApi.AIInfoCard[] + execFileRecord: Map + yakExecResultLogs: StreamResult.Log[] + + /** 切换session时的loading状态 */ + switchLoading: boolean + /** 用户主动取消问题的loading状态(自由对话) */ + cancelCasualLoading: boolean + /** 用户主动取消问题的loading状态(任务规划) */ + cancelTaskLoading: boolean + /** 请求历史数据相关State */ + requestHistoryState: UseAIMessageDataState + + /** 更新精准字段数据依赖的渲染版本号 */ + updateStateCount: ( + type: + | 'httpFuzzRequestUpdate' + | 'httpFlowFuzzStatusUpdate' + | 'sessionTitleUpdate' + | 'memoryListUpdate' + | 'updateSystemStream' + | 'yaklangCodeChangeUpdate', + ) => void + + updateFolders: (info: AIFileSystemPin) => void + updateTimeLineItem: (item: AIAgentGrpcApi.TimelineItem) => void + + /** 更新http数据 */ + updateHttpData: () => void + /** 更新risk数据 */ + updateRiskData: () => void + + /** 精准字段级数据修改(除去列表数据外的其他数据) */ + updateState: ( + partial: Partial< + Omit< + ChatStoreState, + | 'httpFuzzRequestUpdate' + | 'httpFlowFuzzStatusUpdate' + | 'sessionTitleUpdate' + | 'memoryListUpdate' + | 'updateSystemStream' + | 'yaklangCodeChangeUpdate' + | 'httpTabShow' + | 'httpTabUpdate' + | 'riskTabShow' + | 'riskTabUpdate' + | 'grpcFolders' + | 'reActTimelines' + | 'items' + | 'groups' + | 'tasks' + | 'casualChat' + | 'taskChat' + > + >, + ) => void + + /** 更新自由对话列表的todoList,真实数据存放在内存池中 */ + updateCasualTodoList: () => void + updateTaskLoadingStatus: (status: Partial) => void + updatePlanTree: (planTree: CurrentExecTaskTree) => void + + /** 更新 每个工具执行过程中-文件的操作记录 */ + updateExecFileRecord: (callToolID: string, info: StreamResult.Log, order: number) => void + + dispatchStreamingNode: (params: { + chatType: ChatListRenderType + parentTaskId?: string + node: { + token: string + kind: 'item' | 'group' | 'task' + type: AIChatQSDataType + dataOrigin: ReActChatDataOrigin + isCached?: boolean + cacheOrder?: number + } + groupTokenGenerator: () => string + }) => void + incrementNodeVersion: (token: string, kind: 'item' | 'group' | 'task') => void +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts new file mode 100644 index 0000000000..f19b9ce5d6 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -0,0 +1,207 @@ +import { AIChatQSDataTypeEnum, ReActChatDataOriginEnum, type ChatStoreState } from './aiRender' +import { createStore } from 'zustand/vanilla' +import { immer } from 'zustand/middleware/immer' +import cloneDeep from 'lodash/cloneDeep' +import { + DefaultAIQuestionQueues, + DefaultCurrentExecTaskTree, + DefaultPlanHistoryList, + DefaultPlanLoadingStatus, +} from './defaultConstant' +import { CurrentExecTaskTree } from './type' +import { v4 as uuidv4 } from 'uuid' + +export const createChatStore = () => { + return createStore()( + immer((set) => ({ + execute: false, + + httpFuzzRequestUpdate: 0, + httpFlowFuzzStatusUpdate: 0, + sessionTitleUpdate: 0, + memoryListUpdate: 0, + updateSystemStream: 0, + yaklangCodeChangeUpdate: 0, + + grpcFolders: [], + reActTimelines: [], + + notifyMessage: null, + planHistoryList: cloneDeep(DefaultPlanHistoryList), + questionQueue: cloneDeep(DefaultAIQuestionQueues), + + httpTabShow: false, + httpTabUpdate: 0, + riskTabShow: false, + riskTabUpdate: 0, + + casualTitle: '', + casualLoading: false, + focusMode: '', + showPlanList: false, + taskStatus: cloneDeep(DefaultPlanLoadingStatus), + currentPlanReviewData: undefined, + + items: {}, + groups: {}, + tasks: {}, + + casualChat: { + elements: [], + todoListUpdate: 0, + }, + taskChat: { + elements: [], + plan: cloneDeep(DefaultCurrentExecTaskTree), + todoListMap: new Map(), + }, + + card: [], + execFileRecord: new Map(), + yakExecResultLogs: [], + + switchLoading: false, + cancelCasualLoading: false, + cancelTaskLoading: false, + requestHistoryState: { + initLoading: false, + casualLoadMoreLoading: false, + taskLoadMoreLoading: false, + saveLoading: false, + timelinesLoading: false, + }, + + updateStateCount: (type) => + set((state) => { + state[type] += 1 + }), + updateFolders: (info) => + set((state) => { + const isExist = state.grpcFolders.find((item) => item.path === info.path) + if (!isExist) state.grpcFolders.push(info) + }), + updateTimeLineItem: (item) => + set((state) => { + state.reActTimelines.push(item) + }), + + updateHttpData: () => { + set((state) => { + if (!state.httpTabShow) state.httpTabShow = true + state.httpTabUpdate += 1 + }) + }, + updateRiskData: () => { + set((state) => { + if (!state.riskTabShow) state.riskTabShow = true + state.riskTabUpdate += 1 + }) + }, + + updateState: (partial) => + set((state) => { + Object.assign(state, partial) + }), + + updateCasualTodoList: () => { + set((state) => { + state.casualChat.todoListUpdate += 1 + }) + }, + updateTaskLoadingStatus: (partial) => + set((state) => { + Object.assign(state.taskStatus, partial) + }), + updatePlanTree: (planTree: CurrentExecTaskTree) => + set((state) => { + Object.assign(state.taskChat.plan, planTree) + }), + + updateExecFileRecord: (callToolID, info, order) => + set((state) => { + const keyName = callToolID || 'system' + const keyList = state.execFileRecord.get(keyName) || [] + keyList.push({ ...info, id: uuidv4(), order: order }) + state.execFileRecord.set(keyName, keyList) + }), + + dispatchStreamingNode: ({ chatType, parentTaskId, node, groupTokenGenerator }) => + set((state) => { + const isCached = node.isCached ?? false + const cacheOrder = node.cacheOrder ?? 0 + const dataOrigin = node.dataOrigin ?? ReActChatDataOriginEnum.GrpcRealtimeData + // 1. 实体字典动态初始化守卫 + if (node.kind === 'item' && !state.items[node.token]) { + state.items[node.token] = { token: node.token, kind: 'item', type: node.type || 'assistant', renderNum: 0 } + } else if (node.kind === 'task' && !state.tasks[node.token]) { + state.tasks[node.token] = { + token: node.token, + kind: 'task', + type: node.type || 'assistant', + childrenTokens: [], + renderNum: 0, + } + } + + // 2. 路由分发决策(以普通看板为例,去掉了大任务内部分支以精简示范) + const targetElements = chatType === 'reAct' ? state.casualChat.elements : state.taskChat.elements + + if (targetElements.length === 0) { + targetElements.push({ kind: node.kind, token: node.token, chatType, isCached, cacheOrder, dataOrigin }) + return + } + + const lastNode = targetElements[targetElements.length - 1] + const lastToken = lastNode.token + const lastItem = state.items[lastToken] + const lastGroup = state.groups[lastToken] + + // 🌟 核心结界拦截:如果是 IndexedDB 或后端的历史,严禁触碰折叠逻辑 + if (!isCached) { + // 合并组吞噬逻辑 + // 需要调整,因为只有stream会有聚合group的情况 + if (lastGroup && lastGroup.kind === 'group' && node.kind === 'item') { + lastGroup.childrenTokens.push(node.token) + lastGroup.renderNum += 1 + return + } + // 偷梁换柱成组逻辑 + if ( + lastItem && + lastItem.type === AIChatQSDataTypeEnum.STREAM && + node.type === AIChatQSDataTypeEnum.STREAM && + node.kind === 'item' + ) { + const newGroupToken = groupTokenGenerator() + state.groups[newGroupToken] = { + token: newGroupToken, + kind: 'group', + type: AIChatQSDataTypeEnum.STREAM_GROUP, + childrenTokens: [lastToken, node.token], + renderNum: 0, + } + targetElements[targetElements.length - 1] = { + kind: 'group', + token: newGroupToken, + chatType, + isCached, + cacheOrder, + dataOrigin, + } + return + } + } + + targetElements.push({ kind: node.kind, token: node.token, chatType, isCached, cacheOrder, dataOrigin }) + }), + + /** 高频更新节点渲染 */ + incrementNodeVersion: (token, kind) => + set((state) => { + if (kind === 'item' && state.items[token]) state.items[token].renderNum += 1 + if (kind === 'group' && state.groups[token]) state.groups[token].renderNum += 1 + if (kind === 'task' && state.tasks[token]) state.tasks[token].renderNum += 1 + }), + })), + ) +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx new file mode 100644 index 0000000000..44ec358797 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx @@ -0,0 +1,92 @@ +// ChatNodes.tsx +import React, { useState, useContext, useMemo } from 'react' +import { SessionIdContext } from './SessionRuntime' +import { globalSessionEngine } from './ChatMultiSessionController' + +// ========================================== +// 1. 叶子节点 (原子渲染层) - 性能爆发点 +// ========================================== +export const ChatItem = ({ element }: { element: any }) => { + const sessionId = useContext(SessionIdContext) + const { rawData } = globalSessionEngine.ensureSession(sessionId) + + // 🎯 依赖 element.renderNum 强制去内存池捞取新鲜文本 + const content = useMemo(() => { + if (element.type === 'stream' || element.type === 'thought') { + const nodeData = rawData.contents.get(element.token || element.id) + return nodeData ? nodeData.content : '' + } + return element.content || JSON.stringify(element.data || {}) + }, [element.token, element.id, element.type, element.content, element.data, element.renderNum, rawData]) + + if (element.type === 'thought') { + return
🤔 {content}
+ } + if (element.type === 'stream') { + return
💬 {content}
+ } + if (element.type === 'yak_risk_count') { + return ( +
+ 🚨 高危流量: {element.data?.riskFlowDataCount || 0} +
+ ) + } + if (element.type === 'yak_httpflow_count') { + return ( +
+ 🌐 HTTP流: {element.data?.httpFlowDataCount || 0} +
+ ) + } + + return
{content}
+} + +// ========================================== +// 2. 任务节点 (Task 包裹层) +// ========================================== +export const ChatTask = ({ taskData }: { taskData: any }) => { + return ( +
+
+ ⚙️ {taskData.toolName || '工具调用'} (Task ID: {taskData.token || taskData.id}) +
+ +
+ {taskData.children?.map((child: any) => ( + + ))} +
+
+ ) +} + +// ========================================== +// 3. 编组节点 (Group 顶层折叠层) +// ========================================== +export const ChatGroup = ({ groupData }: { groupData: any }) => { + const [collapsed, setCollapsed] = useState(false) + + return ( +
+
setCollapsed(!collapsed)} + style={{ cursor: 'pointer', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between' }} + > + 📂 规划编组: {groupData.title || groupData.token || groupData.id} + {collapsed ? '展开 ▼' : '折叠 ▲'} +
+ + {!collapsed && ( +
+ {groupData.children?.map((child: any) => { + if (child.type === 'task') return + if (child.type === 'group') return // 支持无限嵌套 + return + })} +
+ )} +
+ ) +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx new file mode 100644 index 0000000000..2ecd1e8fa3 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx @@ -0,0 +1,26 @@ +// GlobalNotification.tsx +import React, { useContext, useEffect } from 'react' +import { useStore } from 'zustand' +import { message } from 'antd' +import { SessionIdContext } from './SessionRuntime' +import { globalSessionEngine } from './ChatMultiSessionController' + +export const GlobalNotification = () => { + const sessionId = useContext(SessionIdContext) + const { store } = globalSessionEngine.ensureSession(sessionId) + + const notification = useStore(store, (state) => state.globalNotification) + + useEffect(() => { + if (notification) { + if (notification.type === 'warning') message.warning(notification.message) + if (notification.type === 'error') message.error(notification.message) + if (notification.type === 'info') message.info(notification.message) + + // 显示完毕后立即清除状态,防止重复触发 + store.getState().setGlobalNotification(null) + } + }, [notification, store]) + + return null +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx new file mode 100644 index 0000000000..ac24d7ec2d --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx @@ -0,0 +1,48 @@ +// ChatReviewModal.tsx +import React, { useContext } from 'react' +import { useStore } from 'zustand' +import { Modal, Button, Space } from 'antd' +import { SessionIdContext } from './SessionRuntime' +import { globalSessionEngine } from './ChatMultiSessionController' + +export const ChatReviewModal = () => { + const sessionId = useContext(SessionIdContext) + const { store } = globalSessionEngine.ensureSession(sessionId) + + const reviewData = useStore(store, (state) => state.reviewModalData) + + const handleClose = () => store.getState().setReviewModalData(null) + + const handleConfirm = (decisionValue: string) => { + globalSessionEngine.sendMessage(sessionId, { + action: 'review_decision', + id: reviewData.id, + decision: decisionValue, + }) + handleClose() + } + + if (!reviewData) return null + + return ( + +

节点事件 ID: {reviewData.id}

+ + {reviewData.selectors?.map((sel: any) => ( + + ))} + + +
+ ) +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx new file mode 100644 index 0000000000..17e3425a83 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx @@ -0,0 +1,97 @@ +// ChatWindow.tsx +import React, { useContext, useState } from 'react' +import { useStore } from 'zustand' +import { useShallow } from 'zustand/react/shallow' +import { SessionIdContext } from './SessionRuntime' +import { globalSessionEngine } from './ChatMultiSessionController' +import { useChatIPC } from './useChatIPC' +import { ChatGroup, ChatTask, ChatItem } from './ChatNodes' +import { ChatReviewModal } from './ChatReviewModal' + +export const ChatWindow = () => { + const sessionId = useContext(SessionIdContext) + const { onStart, onSend } = useChatIPC() + + // 订阅当前专属 Store + const { store } = globalSessionEngine.ensureSession(sessionId) + + // 🎯 按需精准订阅 + const casualElements = useStore( + store, + useShallow((state) => state.casualElements), + ) + const taskElements = useStore( + store, + useShallow((state) => state.taskElements), + ) + const casualTitle = useStore(store, (state) => state.casualTitle) + const taskStatus = useStore(store, (state) => state.taskStatus) + + const [inputValue, setInputValue] = useState('') + + const handleSendMessage = () => { + if (!inputValue.trim()) return + onSend({ text: inputValue }) + setInputValue('') + } + + // 辅助渲染函数:递归解析根节点并路由给对应的组件 + const renderNode = (node: any) => { + const key = node.token || node.id + if (node.type === 'group') return + if (node.type === 'task') return + return + } + + return ( +
+ {/* 人工审批拦截弹窗 */} + + + {/* 头部状态栏 */} +
+

{casualTitle || '安全分析终端'}

+ {taskStatus.loading && ( +
+ ⏳ 正在执行: {taskStatus.task} {taskStatus.plan ? `-> ${taskStatus.plan}` : ''} +
+ )} +
+ + {/* 核心双轨渲染区 */} +
+ {/* 第一轨:自由对话流 */} +
{casualElements.map(renderNode)}
+ + {/* 第二轨:任务规划流 (带有视觉隔离) */} + {taskElements.length > 0 && ( +
+

📡 任务执行审计日志

+ {taskElements.map(renderNode)} +
+ )} +
+ + {/* 底部输入区 */} +
+ + setInputValue(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} + placeholder="输入分析指令..." + /> + +
+
+ ) +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx new file mode 100644 index 0000000000..b821497c2c --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx @@ -0,0 +1,5 @@ +// SessionRuntime.tsx +import React, { createContext } from 'react' + +// 初始值给空字符串,实际由外层 Tabs 组件注入 +export const SessionIdContext = createContext('') diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx new file mode 100644 index 0000000000..f0ae3aa49f --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx @@ -0,0 +1,50 @@ +// ChatTabsLayout.tsx +import React, { useState, useEffect, useCallback } from 'react' +import { Tabs } from 'antd' +import { globalSessionEngine } from './ChatMultiSessionController' +import { SessionIdContext } from './SessionRuntime' +import { ChatWindow } from './ChatWindow' +import { GlobalNotification } from './GlobalNotification' + +export const ChatTabsLayout = () => { + const [activeTabId, setActiveTabId] = useState('session-1') + + // 模拟你的多会话列表 + const [sessions, setSessions] = useState([ + { id: 'session-1', title: '分析会话 A' }, + { id: 'session-2', title: '分析会话 B' }, + ]) + + // 💥 焦点同步:只要 Tab 切换,立刻告诉大管家,用于拦截后台弹窗 + useEffect(() => { + globalSessionEngine.setActiveSession(activeTabId) + + // 切回 Tab 时,顺手清空该会话的未读小红点状态 + const { store } = globalSessionEngine.ensureSession(activeTabId) + store.getState().setHasUnreadAttention(false) + }, [activeTabId]) + + const onChange = useCallback((newActiveKey: string) => { + setActiveTabId(newActiveKey) + }, []) + + return ( +
+ ({ + key: session.id, + label: session.title, + children: ( + // 注入上下文,包裹具体的聊天界面 + + + + + ), + }))} + /> +
+ ) +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcAIMessageHandlers.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcAIMessageHandlers.ts index bd5fc47277..932b955889 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcAIMessageHandlers.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcAIMessageHandlers.ts @@ -1,46 +1,22 @@ import type { AIMessageHandler, AIMessageHandlerParams, UpdateRenderDataParams } from './type' -import type { AIAgentGrpcApi, AIInputEvent, AIOutputEvent } from './grpcApi' import type { AIChatQSData, AIChatQSDataType, - AIReviewType, - AIToolResult, ChatStream, - HttpFlowFuzzStatusCardData, - ReportFinishCardData, ReActChatBaseInfo, ReActChatGroupElement, ReActChatElement, ReActChatRenderItem, ReActChatTaskElement, ReActChatTaskElementSub, - PlanItemDetailsData, } from './aiRender' -import { Uint8ArrayToString } from '@/utils/str' -import { - genBaseAIChatData, - generateTaskId, - genErrorLogData, - handleTodoListData, - isAutoExecuteReviewContinue, - isToolStderrStream, - isToolStdoutStream, -} from './utils' +import { genErrorLogData } from './utils' import { AIChatQSDataTypeEnum } from './aiRender' -import { - AIReviewJudgeLevelMap, - AIStreamContentType, - convertNodeIdToVerbose, - DefaultAIToolResult, - DefaultPlanItemDetailsData, - DefaultToolResultSummary, -} from './defaultConstant' +import { AIStreamContentType } from './defaultConstant' import cloneDeep from 'lodash/cloneDeep' -import { v4 as uuidv4 } from 'uuid' -import isEmpty from 'lodash/isEmpty' -import isArray from 'lodash/isArray' -import type { AIChatData } from '@/pages/ai-agent/type/aiChat' +import { AIChatData } from '@/pages/ai-agent/type/aiChat' +import { AIOutputEvent } from './grpcApi' // #region Common Utils /** 自由对话:是否为子 agent 任务(planDetailsMap 用子任务 ID;contents 用复合 ID) */ const isCasualSubAgentTask = ( @@ -1017,1462 +993,9 @@ const handleIsGroupDisplayForStream: ( } } -/** Type='stream' stream数据 */ -const handleStream: AIMessageHandler = (request) => { - const { res, setElements, getContentMap, pushLog } = request - if (res.Type !== 'stream') return - // 属于日志数据的不进入UI展示 - if (res.IsSystem || res.IsReason) return - - const { CallToolID, EventUUID, NodeId } = res - if (!EventUUID || !NodeId) return - - const content = (Uint8ArrayToString(res.Content) || '') + (Uint8ArrayToString(res.StreamDelta) || '') - - // tool-xxx-stderr 数据单独处理逻辑 - if (isToolStderrStream(NodeId)) { - if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), - ) - return - } - const errorResult = ToolResultForStreamError.get(CallToolID) - if (errorResult) errorResult.content += content - return - } - // tool-xxx-stdout 数据单独处理逻辑 - if (isToolStdoutStream(NodeId)) { - if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), - ) - return - } - const toolResult = getContentMap(CallToolID) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `NodeID: ${NodeId} 的stream数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, - ), - ) - return - } - const toolForStreamData = getContentMap(toolResult.data.stream.EventUUID) - if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `EventUUID: ${toolResult.data.stream.EventUUID} 的stream数据没有对应的初始化`), - ) - return - } - const isRender = !toolForStreamData.data.content - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - toolForStreamData.data.content += content - if (isRender) { - handleUpdateUISingleState(setElements, getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) - } - return - } - - // 数据集合中对应的数据 - const streamData = getContentMap(EventUUID) - - // 数据不存在 - if (!streamData || streamData.type !== AIChatQSDataTypeEnum.STREAM) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `异常 stream 类型, NodeId: ${NodeId}, eventuuid: (${EventUUID})`), - ) - return - } - - const isRender = !streamData.data.content - // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - streamData.Timestamp = isRender ? res.Timestamp : streamData.Timestamp - streamData.data.content += content - - if (isRender) { - // 判断是否成为组UI数据展示 - setElements((old) => { - const list = handleIsGroupDisplayForStream(res, streamData, old, getContentMap) - return list - }) - } -} - -/** Type='structured'&NodeId='stream-finished' stream数据结束标识 */ -const handleStreamFinished: AIMessageHandler = (request) => { - const { res, setElements, getElements, getContentMap, pushLog } = request - if (res.Type !== 'structured' || res.NodeId !== 'stream-finished') return - - let ipcContent = Uint8ArrayToString(res.Content) || '' - const { event_writer_id, node_id, is_reason, is_system } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStreamFinished - // 属于日志数据的不进入UI展示 - if (is_reason || is_system) return - if (!event_writer_id) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `stream-finished数据, event_writer_id 为空`), - ) - return - } - - const { CallToolID } = res - // tool-xxx-stderr 数据单独结束逻辑 - if (isToolStderrStream(node_id)) { - if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.NodeId}数据(NodeId: ${node_id}), CallToolID 为空`), - ) - return - } - - const toolErrorResult = ToolResultForStreamError.get(CallToolID) - if (!toolErrorResult) return - - const toolResult = getContentMap(CallToolID) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - // 工具执行结果卡片UI没有展示时 - toolErrorResult.status = 'end' - } else { - const showUI = getElements().find((item) => item.token === toolResult.id && item.type === toolResult.type) - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - toolResult.data.tool.execError = toolErrorResult.content - if (showUI) { - handleUpdateUISingleState(setElements, getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) - } - ToolResultForStreamError.delete(CallToolID) - } - return - } - // tool-xxx-stdout 数据单独结束逻辑 - if (isToolStdoutStream(node_id)) { - if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.NodeId}数据(NodeId: ${node_id}), CallToolID 为空`), - ) - return - } - - const toolResult = getContentMap(res.CallToolID) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { - return - } - const toolForStreamData = getContentMap(toolResult.data.stream.EventUUID) - if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { - return - } - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - toolForStreamData.data.status = 'end' - const isShowAll = toolForStreamData.data.content.length > 25600 // 50KB大概字符数25600 - const displayContent = isShowAll - ? '...' + toolForStreamData.data.content.slice(-25600) + '...' - : toolForStreamData.data.content - toolResult.data.tool.toolStdoutContent = { content: displayContent, isShowAll } - handleUpdateUISingleState(setElements, getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) - return - } - - // 数据集合中对应的数据 - const streamData = getContentMap(event_writer_id) - // 数据不存在 不输出到日志,因为日志的流数据也有该类型数据 - if (!streamData || streamData.type !== AIChatQSDataTypeEnum.STREAM) return - - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - streamData.data.status = 'end' - if (streamData.parentGroupKey) { - handleUpdateUIGroupState( - { mapKey: streamData.parentGroupKey, type: AIChatQSDataTypeEnum.STREAM_GROUP }, - { mapKey: event_writer_id, type: AIChatQSDataTypeEnum.STREAM }, - setElements, - streamData.taskId, - ) - } else { - handleUpdateUISingleState(setElements, getContentMap, res.IsSync, { - mapKey: streamData.id, - type: streamData.type, - chatType: streamData.chatType, - }) - } -} - -/** Type='reference_material' 参考资料, 可独立或追加到别的类型数据中展示 */ -const handleReferenceMaterial: AIMessageHandler = (request) => { - const { res, info, setElements, setContentMap, getContentMap } = request - if (res.Type !== 'reference_material') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ReferenceMaterialPayload - - const chatData = getContentMap(data.event_uuid) - const toolResult = getContentMap(res.CallToolID || '') - if (chatData) { - // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - chatData.reference = (chatData.reference || []).concat([data]) - if (chatData.parentGroupKey) { - handleUpdateUIGroupState( - { mapKey: chatData.parentGroupKey, type: AIChatQSDataTypeEnum.STREAM_GROUP }, - { mapKey: chatData.id, type: chatData.type }, - setElements, - chatData.taskId, - ) - } else if (chatData.type === AIChatQSDataTypeEnum.STREAM) { - if (toolResult && isToolStdoutStream(chatData.data.NodeId)) { - // 特殊情况,更新stdout流对应的工具执行结果卡片UI - handleUpdateUISingleState(setElements, getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) - } else { - setElements((old) => { - const list = handleIsGroupDisplayForStream( - { ...res, ContentType: chatData.data.ContentType }, - chatData, - old, - getContentMap, - ) - return list - }) - } - return - } else { - handleUpdateUISingleState(setElements, getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) - } - } else if ( - toolResult && - toolResult.type === AIChatQSDataTypeEnum.TOOL_RESULT && - toolResult.data.stream.EventUUID === data.event_uuid - ) { - toolResult.reference = (toolResult.reference || []).concat([data]) - handleUpdateUISingleState(setElements, getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) - } - // else { - // const chatData: AIChatQSData = { - // ...genBaseAIChatData(res), - // id: data.event_uuid, - // chatType: info.chatType, - // type: AIChatQSDataTypeEnum.Reference_Material, - // data: { - // NodeId: res.NodeId, - // NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), - // }, - // reference: [data], - // taskIndex: generateTaskId({ - // chatType: info.chatType, - // res, - // getCurrentTaskPlanID: request.getCurrentTaskPlanID, - // getContentMap: request.getContentMap, - // }), - // } - // setContentMap(chatData.id, chatData) - // handleUpdateUISingleState(setElements, getContentMap, res.IsSync, { - // mapKey: chatData.id, - // type: chatData.type, - // chatType: chatData.chatType, - // }) - // } -} -// #endregion - -// #region 工具执行结果卡牌 -/** Type='tool_call_start' 工具执行-开始标识(OK) */ -const handleToolCallStart: AIMessageHandler = (request) => { - const { res, info, setContentMap, pushLog } = request - if (res.Type !== 'tool_call_start') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { call_tool_id, tool, start_time, start_time_ms } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall - if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) - return - } - - const toolResult: AIToolResult = { - ...cloneDeep(DefaultAIToolResult), - callToolId: call_tool_id, - toolName: tool?.name || '-', - verboseName: tool?.verbose_name_i18n, - toolDescription: tool?.description || '', - startTime: start_time || 0, - startTimeMS: start_time_ms || 0, - } - - setContentMap(call_tool_id, { - ...genBaseAIChatData(res), - id: call_tool_id, - chatType: info.chatType, - type: AIChatQSDataTypeEnum.TOOL_RESULT, - data: toolResult, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - }) -} - -/** Type='tool_call_param' 工具执行-review参数信息 */ -const handleToolCallParam: AIMessageHandler = (request) => { - const { res, getContentMap, pushLog } = request - if (res.Type !== 'tool_call_param') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { call_tool_id, params } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallParams - if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) - return - } - - const toolResult = getContentMap(call_tool_id) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) - return - } - - toolResult.data.tool.reviewParams = cloneDeep(params) - - if (toolResult.data.type === 'result') { - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) - } -} - -/** Type='tool_call_watcher' 工具执行中-可操作选项 */ -const handleToolCallWatcher: AIMessageHandler = (request) => { - const { res, getContentMap, pushLog } = request - if (res.Type !== 'tool_call_watcher') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { call_tool_id, id, selectors } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallWatcher - - if (!call_tool_id || !id || !selectors || !selectors?.length) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id: ${call_tool_id || '为空'} | id: ${id || '为空'}`), - ) - return - } - - // 先获取工具结果数据,从里面拿到stream的EventUUID - const toolResult = getContentMap(call_tool_id) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) - return - } - // 通过上面获取到的EventUUID,获取stream数据 - const toolForStreamData = getContentMap(toolResult.data.stream.EventUUID) - if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `EventUUID: ${toolResult.data.stream.EventUUID} 的stream数据没有对应的初始化`), - ) - return - } - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - toolForStreamData.data.selectors = { - callToolId: call_tool_id, - InteractiveId: id, - selectors: selectors, - } - - if (toolResult.data.type === 'stream') { - // 历史数据-该类型不出发渲染更新 - if (res.IsSync) return - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) - } -} - -/** Type='tool_call_log_dir' 工具执行-工作目录路径 */ -const handleToolCallLogDir: AIMessageHandler = (request) => { - const { res, getContentMap, pushLog } = request - if (res.Type !== 'tool_call_log_dir') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { call_tool_id, dir_path } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallDirPath - if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) - return - } - - const toolResult = getContentMap(call_tool_id) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) - return - } - if (toolResult.data.tool.dirPath) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(call_tool_id:${call_tool_id}), dir_path已存在,不能重复设置`), - ) - return - } - - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - toolResult.data.tool.dirPath = dir_path || '' - if (toolResult.data.tool.status !== 'default') { - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) - } -} - -/** 工具执行结果的处理逻辑 */ -const handleToolCallResult: ( - request: AIMessageHandlerParams, - status: 'user_cancelled' | 'success' | 'failed', -) => void = (request, status) => { - const { res, getContentMap, pushLog } = request - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { call_tool_id, ...rest } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall - - if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) - return - } - - const toolResult = getContentMap(call_tool_id) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) - return - } - - // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - // 设置工具执行的开始时间、结束时间和持续时间等数据 - toolResult.data.type = 'result' - toolResult.data.startTime = rest.start_time || 0 - toolResult.data.startTimeMS = rest.start_time_ms || 0 - toolResult.data.endTime = rest.end_time || 0 - toolResult.data.endTimeMS = rest.end_time_ms || 0 - toolResult.data.durationMS = rest.duration_ms || 0 - toolResult.data.durationSeconds = rest.duration_seconds || 0 - toolResult.data.tool.status = status - /** 触发这个函数说明状态一定不是 processing_params */ - toolResult.data.isProcessingParams = false - // 设置总结内容,没有就设置成获取中,有就使用获取到的内容 - toolResult.data.tool.summary = toolResult.data.tool.summary || DefaultToolResultSummary[status]?.wait || '' - // 设置执行结果错误数据内容(std_xxx_stderr) - const errorResult = ToolResultForStreamError.get(call_tool_id) - if (errorResult && errorResult.status === 'end') { - toolResult.data.tool.execError = errorResult.content - // error数据先出但未存在对应的工具执行结果,工具结果出现后直接使用并删除map中的缓存数据 - ToolResultForStreamError.delete(call_tool_id) - } - - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) -} - -/** Type='tool_call_user_cancel' 工具执行结果-用户取消 */ -const handleToolCallUserCancel: AIMessageHandler = (request) => { - if (request.res.Type !== 'tool_call_user_cancel') return - handleToolCallResult(request, 'user_cancelled') -} - -/** Type='tool_call_done' 工具执行结果-成功 */ -const handleToolCallDone: AIMessageHandler = (request) => { - if (request.res.Type !== 'tool_call_done') return - handleToolCallResult(request, 'success') -} - -/** Type='tool_call_error' 工具执行结果-失败 */ -const handleToolCallError: AIMessageHandler = (request) => { - if (request.res.Type !== 'tool_call_error') return - handleToolCallResult(request, 'failed') -} - -/** Type='tool_call_status' 工具执行结果-状态 */ -const handleToolCallStatus: AIMessageHandler = (request) => { - const { res, getContentMap, pushLog } = request - if (res.Type !== 'tool_call_status') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { call_tool_id, status } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall - if (status !== 'processing_params') return - if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) - return - } - - const toolResult = getContentMap(call_tool_id) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) - return - } - - const isProcessingParams = status === 'processing_params' - if (toolResult.data.isProcessingParams === isProcessingParams) return - - toolResult.data.isProcessingParams = status === 'processing_params' - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) -} - -/** Type='tool_call_summary' 工具执行结果-总结 */ -const handleToolCallSummary: AIMessageHandler = (request) => { - const { res, getContentMap, pushLog } = request - if (res.Type !== 'tool_call_summary') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { call_tool_id, summary } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall - - if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) - return - } - - const toolResult = getContentMap(call_tool_id) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) - return - } - - const statusInfo = toolResult.data.tool.status - const summaryContent = !summary || summary === 'null' ? '' : summary - // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - // 设置总结内容,没有就设置成默认的状态展示内容,有就使用获取到的内容 - toolResult.data.tool.summary = - statusInfo === 'user_cancelled' - ? '当前工具调用已被取消,会使用当前输出结果进行后续工作决策' - : summaryContent || DefaultToolResultSummary[toolResult.data.tool.status]?.result || '' - // 设置执行结果错误数据内容(std_xxx_stderr) - const errorResult = ToolResultForStreamError.get(call_tool_id) - if (errorResult && errorResult.status === 'end') { - toolResult.data.tool.execError = errorResult.content - // error数据先出但未存在对应的工具执行结果,工具结果出现后直接使用并删除map中的缓存数据 - ToolResultForStreamError.delete(call_tool_id) - } - if (statusInfo !== 'default') { - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) - } -} - -/** Type='tool_call_reason' 工具调用理由 */ -const handleToolCallReason: AIMessageHandler = (request) => { - const { res, getContentMap, pushLog } = request - if (res.Type !== 'tool_call_reason') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { call_tool_id, reason } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall - - if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, call_tool_id 为空`)) - return - } - - const toolResult = getContentMap(call_tool_id) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) - return - } - - toolResult.data.tool.reason = reason || '' - if (toolResult.data.type === 'stream' || toolResult.data.type === 'result') { - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) - } -} - -/** 工具执行结果的流量数据计数逻辑 */ -const handleTrafficCount: AIMessageHandler = (request) => { - const { res, getContentMap, pushLog } = request - // 历史数据中的流量计数无效 - // if (res.IsSync) return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.HTTPTrafficNotice & AIAgentGrpcApi.RiskTrafficNotice - - if (!data.runtime_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, runtime_id 为空`)) - return - } - const toolResult = getContentMap(data.runtime_id) - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(call_tool_id:${data.runtime_id}), 没有对应的工具执行结果数据`), - ) - return - } - - let update = false - switch (res.Type) { - case 'yak_httpflow_count': - if (data.http_flow_count !== toolResult.data.httpFlowDataCount) { - toolResult.data.httpFlowDataCount = data.http_flow_count ?? 0 - update = true - } - break - case 'yak_risk_count': - if (data.risk_count !== toolResult.data.riskFlowDataCount) { - toolResult.data.riskFlowDataCount = data.risk_count ?? 0 - update = true - } - break - default: - break - } - if (!update) return - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: toolResult.id, - type: toolResult.type, - chatType: toolResult.chatType, - }) -} - -/** Type='yak_httpflow_count' 新增流量数据 */ -const handleYakHttpFlow: AIMessageHandler = (request) => { - if (request.res.Type !== 'yak_httpflow_count') return - handleTrafficCount(request) -} -/** Type='yak_risk_count' 新增风险数据 */ -const handleYakRisk: AIMessageHandler = (request) => { - if (request.res.Type !== 'yak_risk_count') return - handleTrafficCount(request) -} -// #endregion - -// #region review相关的处理逻辑 -/** 保存那些review_release先出现的历史review数据的id */ -let reviewReleaseID: Record = {} -/** 记录plan_review补充信息的唯一ID */ -let currentPlanReviewId = '' - -/** 自动提交 review continue 给后端,并清理本地缓存数据 */ -const handleAutoSubmitReviewContinue = (request: AIMessageHandlerParams, chatData: AIChatQSData) => { - const { review } = request - const input: AIInputEvent = { - IsInteractiveMessage: true, - InteractiveId: chatData.id, - InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), - } - - review?.sendRequest?.(input) - review?.handleSetReview?.(undefined) -} - -/** Type='plan_review_require' plan-review */ -const handlePlanReview: AIMessageHandler = (request) => { - const { res, info, getRequest, setContentMap, pushLog, review } = request - if (res.Type !== 'plan_review_require') return - // 历史数据-grpc流数据在任务规划下无效,不处理 - if (res.IsSync && info.chatType === 'task') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequire - if (!data?.id || !data?.plans || !data?.plans?.root_task || !data?.selectors || !data?.selectors?.length) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据异常: id:${data?.id || '-'}; selectors:${JSON.stringify(data?.selectors || '-')}; plans:${ - !!data?.plans?.root_task ? 'valid' : 'invalid' - } data`, - ), - ) - return - } - - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - id: data.id, - type: AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE, - data: { ...cloneDeep(data) }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - if (res.IsSync) { - // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 - const target = reviewReleaseID[data.id] - if (target) { - chatData.data.selected = JSON.stringify(target.params) - chatData.data.optionValue = target.params?.suggestion || 'continue' - } - setContentMap(chatData.id, cloneDeep(chatData)) - if (target) { - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) - } - return - } - - // 实时数据处理逻辑 - const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: getRequest }) - if (isAuto) { - chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) - chatData.data.optionValue = 'continue' - } - // 将数据存入hook里的缓存变量中 - review?.handleSetReview && review.handleSetReview(isAuto ? undefined : chatData) - if (info.chatType === 'task') { - // 该类型的实时数据只有任务规划才有 - if (isAuto) { - setContentMap(chatData.id, cloneDeep(chatData)) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) - review?.handleReviewDataToUI && review.handleReviewDataToUI(cloneDeep(chatData)) - } else { - currentPlanReviewId = '' - review?.onReview && review.onReview(cloneDeep(chatData)) - } - } -} -/** Type='detached_plan_require' detached plan review */ -const handleDetachedPlanReview: AIMessageHandler = (request) => { - const { res, info, pushLog, review } = request - if (res.Type !== 'detached_plan_require' || res.NodeId !== 'detached-plan') return - - // 历史数据-grpc流数据在任务规划下无效,不处理 - if (res.IsSync) return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.DetachedPlanRequire - if (!data?.id || !data?.plans?.root_task || !data?.selectors?.length) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据异常: id:${data?.id || '-'}; selectors:${JSON.stringify(data?.selectors || '-')}; plans:${ - !!data?.plans?.root_task ? 'valid' : 'invalid' - } data`, - ), - ) - return - } - - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - id: data.id, - type: AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE, - data: { ...cloneDeep(data) }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - if (res.IsSync) return - - if (info.chatType === 'reAct') { - review?.onReview && review.onReview(cloneDeep(chatData)) - } -} -/** Type='plan_task_analysis' plan-review的补充信息 */ -const handlePlanReviewAnalysis: AIMessageHandler = (request) => { - const { res, info, getRequest, pushLog, review } = request - // 历史数据-该类型数据无用 - if (res.IsSync) return - if (res.Type !== 'plan_task_analysis') return - // 该类型数据只有任务规划才有 - if (info.chatType !== 'task') return - - const reviewDetail = review?.handleGetReview?.() - if (!reviewDetail || reviewDetail.type !== AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据异常: 未找到对应plan_review_require数据`), - ) - return - } - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequireExtra - if ( - !data?.plans_id || - !data?.task_id || - !data?.keywords?.length || - (currentPlanReviewId && currentPlanReviewId !== data.plans_id) - ) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据异常: plans_id:${data?.plans_id || '-'};task_id:${data?.task_id || '-'};keywords:${JSON.stringify( - data?.keywords || '-', - )}`, - ), - ) - return - } - - if (!currentPlanReviewId) currentPlanReviewId = data.plans_id - const reviewInfo = reviewDetail.data - if (!reviewInfo.taskExtra) reviewInfo.taskExtra = new Map() - reviewInfo.taskExtra.set(data.task_id, data) - - const isAuto = isAutoExecuteReviewContinue({ getFunc: getRequest }) - if (!isAuto && review?.onReviewExtra) review.onReviewExtra(cloneDeep(data)) -} - -/** Type='task_review_require' task-review */ -const handleTaskReview: AIMessageHandler = (request) => { - const { res, info, getRequest, setContentMap, pushLog, review } = request - if (res.Type !== 'task_review_require') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.TaskReviewRequire - if (!data?.id || !data?.selectors || !data?.selectors?.length) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据异常: id:${data?.id || '-'}; selectors:${JSON.stringify(data?.selectors || '-')}`, - ), - ) - return - } - - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - id: data.id, - type: AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE, - data: { ...cloneDeep(data) }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - if (res.IsSync) { - // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 - // const target = reviewReleaseID[data.id] - // if (target) { - // chatData.data.selected = JSON.stringify(target.params) - // chatData.data.optionValue = target.params?.suggestion || 'continue' - // } - // setContentMap(chatData.id, cloneDeep(chatData)) - // if (target) { - // handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - // mapKey: chatData.id, - // type: chatData.type, - // chatType: chatData.chatType, - // }) - // } - return - } - - // 实时数据处理逻辑 - const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: getRequest }) - if (isAuto) { - chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) - chatData.data.optionValue = 'continue' - } - if (info.chatType === 'task') { - if (isAuto) { - // setContentMap(chatData.id, cloneDeep(chatData)) - // handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - // mapKey: chatData.id, - // type: chatData.type, - // chatType: chatData.chatType, - // }) - } else { - handleAutoSubmitReviewContinue(request, chatData) - } - } else if (info.chatType === 'reAct') { - if (isAuto) return - // 将数据存入hook里的缓存变量中 - review?.handleSetReview && review.handleSetReview(isAuto ? undefined : chatData) - setContentMap(chatData.id, cloneDeep(chatData)) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) - } -} - -/** Type='tool_use_review_require' tool-review */ -const handleToolReview: AIMessageHandler = (request) => { - const { res, info, getRequest, setContentMap, pushLog, review } = request - if (res.Type !== 'tool_use_review_require') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ToolUseReviewRequire - if (!data?.id || !data?.selectors || !data?.selectors?.length) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据异常: id:${data?.id || '-'}; selectors:${JSON.stringify(data?.selectors || '-')}`, - ), - ) - return - } - - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - id: data.id, - type: AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE, - data: { ...cloneDeep(data) }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - if (res.IsSync) { - // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 - // const target = reviewReleaseID[data.id] - // if (target) { - // chatData.data.selected = JSON.stringify(target.params) - // chatData.data.optionValue = target.params?.suggestion || 'continue' - // } - // setContentMap(chatData.id, cloneDeep(chatData)) - // if (target) { - // handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - // mapKey: chatData.id, - // type: chatData.type, - // chatType: chatData.chatType, - // }) - // } - return - } - - // 实时数据处理逻辑 - const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: getRequest }) - if (isAuto) { - chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) - chatData.data.optionValue = 'continue' - } - if (info.chatType === 'task') { - if (isAuto) { - // setContentMap(chatData.id, cloneDeep(chatData)) - // handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - // mapKey: chatData.id, - // type: chatData.type, - // chatType: chatData.chatType, - // }) - } else { - handleAutoSubmitReviewContinue(request, chatData) - } - } else if (info.chatType === 'reAct') { - if (isAuto) return - // 将数据存入hook里的缓存变量中 - review?.handleSetReview && review.handleSetReview(isAuto ? undefined : chatData) - setContentMap(chatData.id, cloneDeep(chatData)) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) - } -} - -/** Type='require_user_interactive' AI人机交互 */ -const handleUserInteractive: AIMessageHandler = (request) => { - const { res, info, setContentMap, pushLog, review } = request - if (res.Type !== 'require_user_interactive') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewRequire - if (!data?.id) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据异常: id:${data?.id || '-'}`), - ) - return - } - - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - id: data.id, - type: AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE, - data: cloneDeep(data), - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - if (res.IsSync) { - // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 - const target = reviewReleaseID[data.id] - if (target) { - chatData.data.selected = JSON.stringify(target.params) - chatData.data.optionValue = target.params?.suggestion || 'continue' - } - setContentMap(chatData.id, cloneDeep(chatData)) - if (target) { - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) - } - return - } - - // 将数据存入hook里的缓存变量中 - review?.handleSetReview && review.handleSetReview(chatData) - if (info.chatType === 'task') { - review?.onReview && review.onReview(cloneDeep(chatData)) - } else if (info.chatType === 'reAct') { - setContentMap(chatData.id, cloneDeep(chatData)) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) - } -} - -/** Type='exec_aiforge_review_require' 智能体review */ -const handleAIForgeReviewRequire: AIMessageHandler = (request) => { - const { res, info, getRequest, setContentMap, pushLog, review } = request - // 任务规划不存在该类型数据 - if (info.chatType === 'task') return - if (res.Type !== 'exec_aiforge_review_require') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ExecForgeReview - if (!data?.id || !data?.selectors || !data?.selectors?.length) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据异常: id:${data?.id || '-'}; selectors:${JSON.stringify(data?.selectors || '-')}`, - ), - ) - return - } - - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - id: data.id, - type: AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE, - data: { ...cloneDeep(data) }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - if (res.IsSync) { - // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 - const target = reviewReleaseID[data.id] - if (target) { - chatData.data.selected = JSON.stringify(target.params) - chatData.data.optionValue = target.params?.suggestion || 'continue' - } - setContentMap(chatData.id, cloneDeep(chatData)) - if (target) { - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) - } - return - } - - // 实时数据处理逻辑 - const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: getRequest }) - if (isAuto) { - chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) - chatData.data.optionValue = 'continue' - } - // 将数据存入hook里的缓存变量中 - review?.handleSetReview && review.handleSetReview(isAuto ? undefined : chatData) - setContentMap(chatData.id, cloneDeep(chatData)) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) -} - -/** AI对review信息的评分和自动化操作 */ -const handleAIReviewJudgement: AIMessageHandler = (request) => { - const { res, info, getRequest, getContentMap, pushLog, review } = request - // 历史数据不处理该类型数据 - if (res.IsSync) return - - const reviewDetail = review?.handleGetReview?.() - if (!reviewDetail) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据异常: 没有对应的review数据`), - ) - return - } - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const score = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewJudgement - if (!score?.interactive_id) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据异常: interactive_id:${score?.interactive_id || '-'}`), - ) - return - } - - const { interactive_id } = score - score.levelLabel = AIReviewJudgeLevelMap[score?.level || '']?.label || undefined - - if (info.chatType === 'task') { - if ( - reviewDetail.type !== AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || - reviewDetail.data.id !== score.interactive_id - ) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据异常(interactive_id:${score?.interactive_id || '-'})未找到对应review`, - ), - ) - return - } - - if ( - !reviewDetail.data.aiReview || - (reviewDetail.data.aiReview && typeof reviewDetail.data.aiReview.seconds === 'undefined') - ) { - // aiReview 没有或者 aiReview 的 seconds 为空时可以赋值 - reviewDetail.data.aiReview = cloneDeep(score) - } - const isAuto = isAutoExecuteReviewContinue({ getFunc: getRequest }) - if (!isAuto && review?.onReview) review.onReview(cloneDeep(reviewDetail)) - } else if (info.chatType === 'reAct') { - if ( - reviewDetail.type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || - reviewDetail.type === AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE - ) { - if ( - !reviewDetail.data.aiReview || - (reviewDetail.data.aiReview && typeof reviewDetail.data.aiReview.seconds === 'undefined') - ) { - reviewDetail.data.aiReview = cloneDeep(score) - } - } - - const chatData = getContentMap(interactive_id) - if ( - chatData && - (chatData.type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || - chatData.type === AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE) - ) { - if ( - !chatData.data.aiReview || - (chatData.data.aiReview && typeof chatData.data.aiReview.seconds === 'undefined') - ) { - // aiReview 没有或者 aiReview 的 seconds 为空时可以赋值 - chatData.data.aiReview = cloneDeep(score) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) - } - } else { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(interactive_id:${score?.interactive_id || '-'})未找到对应review`, - ), - ) - } - } -} -/** Type='ai_review_start' AI评分-开始 */ -const handleAIReviewStart: AIMessageHandler = (request) => { - if (request.res.Type !== 'ai_review_start') return - handleAIReviewJudgement(request) -} -/** Type='ai_review_countdown' AI评分-倒计时 */ -const handleAIReviewCountdown: AIMessageHandler = (request) => { - if (request.res.Type !== 'ai_review_countdown') return - handleAIReviewJudgement(request) -} -/** Type='ai_review_end' AI评分-结束 */ -const handleAIReviewEnd: AIMessageHandler = (request) => { - if (request.res.Type !== 'ai_review_end') return - handleAIReviewJudgement(request) -} - -/** Type='review_release' review释放通知 */ -const handleReviewRelease: AIMessageHandler = (request) => { - const { res, info, getRequest, setContentMap, getContentMap, pushLog, review } = request - if (res.Type !== 'review_release') return - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ReviewRelease - if (!data?.id) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据异常: id:${data?.id || '-'}`), - ) - return - } - - if (res.IsSync) { - const reviewDetail = getContentMap(data.id) - if (!reviewDetail) { - reviewReleaseID[data.id] = data - return - } - switch (reviewDetail.type) { - case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: - case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: - case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: - case AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE: - case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: - reviewDetail.data.selected = JSON.stringify(data.params) - reviewDetail.data.optionValue = data.params?.suggestion || 'continue' - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: reviewDetail.id, - type: reviewDetail.type, - chatType: reviewDetail.chatType, - }) - return - } - } else { - const reviewDetail = review?.handleGetReview?.() - if (!reviewDetail) return - - const reviewInfo = cloneDeep(reviewDetail?.data) as AIReviewType - if (reviewInfo?.id !== data.id) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(id:${data?.id || '-'})和当前展示review数据(id:${reviewInfo?.id || '-'})不匹配`, - ), - ) - return - } - - reviewInfo.selected = JSON.stringify({ suggestion: 'continue' }) - reviewInfo.optionValue = 'continue' - const chatData: AIChatQSData = { - ...reviewDetail, - data: reviewInfo as any, - } - review?.handleSetReview && review.handleSetReview(undefined) - if (info.chatType === 'task') { - currentPlanReviewId = '' - // review自动释放后,还需进行的额外逻辑处理 - review?.handleReviewDataToUI && review.handleReviewDataToUI(cloneDeep(chatData)) - } - setContentMap(chatData.id, cloneDeep(chatData)) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) - - const isAuto = isAutoExecuteReviewContinue({ type: chatData.type, getFunc: getRequest }) - if (!isAuto) review?.onReviewRelease && review.onReviewRelease(data.id) - } -} -// #endregion - -/** 切换session会话后的重置逻辑 */ -export const handleResetForNewSession: () => void = () => { - ToolResultForStreamError.clear() - reviewReleaseID = {} - currentPlanReviewId = '' -} - /** * grpc流数据的各种类型处理逻辑集合 * 该逻辑集合里的方法处理,没有使用try-catch拦截,因为在hook层进行了同一try-catch拦截 * 注意!别的地方单独使用时,请自行加入try-catch拦截错误 */ -export const grpcAIMessageHandlers: Record = { - thought: handleThought, - result: handleResult, - fail_react_task: handleFailReactTask, - tool_call_decision: handleToolCallDecision, - fail_plan_and_execution: handleFailPlanAndExecution, - tool_call_start: handleToolCallStart, - tool_call_param: handleToolCallParam, - tool_call_watcher: handleToolCallWatcher, - tool_call_log_dir: handleToolCallLogDir, - tool_call_user_cancel: handleToolCallUserCancel, - tool_call_done: handleToolCallDone, - tool_call_error: handleToolCallError, - tool_call_status: handleToolCallStatus, - tool_call_summary: handleToolCallSummary, - tool_call_reason: handleToolCallReason, - yak_httpflow_count: handleYakHttpFlow, - yak_risk_count: handleYakRisk, - stream_start: handleStreamStart, - stream: handleStream, - 'stream-finished': handleStreamFinished, - reference_material: handleReferenceMaterial, - plan_review_require: handlePlanReview, - detached_plan_require: handleDetachedPlanReview, - plan_task_analysis: handlePlanReviewAnalysis, - task_review_require: handleTaskReview, - tool_use_review_require: handleToolReview, - require_user_interactive: handleUserInteractive, - exec_aiforge_review_require: handleAIForgeReviewRequire, - ai_review_start: handleAIReviewStart, - ai_review_countdown: handleAIReviewCountdown, - ai_review_end: handleAIReviewEnd, - review_release: handleReviewRelease, - react_task_dequeue: handleReactTaskDequeue, - api_request_failed: handleApiRequestFailed, - http_flow_fuzz_status: handleHttpFlowFuzzStatus, - 'report-finish': handleReportFinish, - current_task_todo_list_update: handleCurrentTaskTodoListUpdate, - capability_inventory: handleCapabilityInventory, - perception: handlePerception, - session_snapshot: handleSessionSnapshot, -} +export const grpcAIMessageHandlers: Record = {} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts new file mode 100644 index 0000000000..59932f3ab0 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -0,0 +1,516 @@ +import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi, type AIOutputEvent } from '../grpcApi' +import { Uint8ArrayToString } from '@/utils/str' +import { genBaseAIChatData, generateTaskNodeID, genErrorLogData, genExecTasks } from '../utils' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' +import cloneDeep from 'lodash/cloneDeep' +import { DefaultCurrentExecTaskTree, DefaultPlanLoadingStatus, DefaultTodoListCardData } from '../defaultConstant' +import has from 'lodash/has' + +/** grpc流数据转换成错误信息输出到日志中 */ +const handleErrorGRPCToLog: ( + /** 该条grpc流数据是历史数据 */ + isHistory: AIMessageHandlerParams['res']['IsSync'], + pushLog: AIMessageHandlerParams['pushLog'], + error: ReturnType, +) => void = (isHistory, pushLog, error) => { + if (isHistory) return + pushLog(error) +} + +const handleHttpFuzzRequestChange: AIMessageHandler = (request) => { + const { res, store, rawData } = request + if (res.Type !== 'http_fuzz_request_change') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const httpFuzzRequest = JSON.parse(ipcContent) as AIAgentGrpcApi.HttpFuzzRequestChange + rawData.httpFuzzRequest = httpFuzzRequest + store.getState().updateStateCount('httpFuzzRequestUpdate') +} + +const handleHttpFlowFuzzStatus: AIMessageHandler = (request) => { + const { res, store, rawData } = request + if (res.Type !== 'http_flow_fuzz_status') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const httpFlowFuzzStatus = JSON.parse(ipcContent) as AIAgentGrpcApi.GetHttpFlowFuzzStatus + rawData.httpFlowFuzzStatus = httpFlowFuzzStatus + store.getState().updateStateCount('httpFlowFuzzStatusUpdate') +} + +const handleSessionTitle: AIMessageHandler = (request) => { + const { res, store, rawData, pushLog } = request + if (res.Type !== 'structured' || res.NodeId !== 'session_title') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const nameInfo = JSON.parse(ipcContent) as { title: string } + if (!nameInfo || !nameInfo.title) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.NodeId}数据, 数据错误: ${JSON.stringify(nameInfo.title)}`), + ) + return + } + rawData.sessionTitle = nameInfo.title + store.getState().updateStateCount('sessionTitleUpdate') +} + +const handleStartPlanAndExecution: AIMessageHandler = (request) => { + const { res, store, rawData, meta, pushLog, sendRequest } = request + if (res.Type !== 'start_plan_and_execution') return + if (res.IsSync) return + + // 清空任务规划的todo-list数据 + rawData.taskChat.planDetailsMap.clear() + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution + if (!startInfo.coordinator_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, coordinator_id 为空`)) + return + } + meta.currentTaskPlanID = { + taskID: startInfo['re-act_task'], + status: AITaskStatus.inProgress, + // 取消任务规划需要的数据id + coordinatorId: startInfo.coordinator_id, + } + // 开始任务规划后,刷新历史任务树 + sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS }) + /** 获取最新任务树状态 */ + sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) + store.getState().updateState({ + taskStatus: { loading: true, plan: '加载中...', task: '加载中...' }, + showPlanList: true, + cancelTaskLoading: false, + }) + + // 生成任务规划里的默认任务聚合组 + const taskID = generateTaskNodeID(meta.currentTaskPlanID.taskID, 'unknown') + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + id: taskID, + chatType: 'task', + type: AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP, + } as AIChatQSData + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: 'task', + node: { + token: chatData.id, + kind: 'task', + type: chatData.type, + dataOrigin: 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) +} +const handleEndPlanAndExecution: AIMessageHandler = (request) => { + const { res, store, rawData, meta, pushLog } = request + if (res.Type !== 'end_plan_and_execution') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution + if (!startInfo.coordinator_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, coordinator_id 为空`)) + return + } + if (startInfo.coordinator_id === meta.currentTaskPlanID?.coordinatorId) { + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: 'task', + type: AIChatQSDataTypeEnum.END_PLAN_AND_EXECUTION, + data: '', + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: 'task', + node: { token: chatData.id, kind: 'item', type: chatData.type, dataOrigin: 'grpc_realtime_data' }, + groupTokenGenerator: () => '', + }) + const actives = Array.from(meta.currentTaskPlanActiveNode) + meta.currentTaskPlanActiveNode.clear() + for (const active of actives) { + const taskNodeInfo = rawData.contents.get(active) + if (!taskNodeInfo || taskNodeInfo.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { + continue + } + taskNodeInfo.data.status = AITaskStatus.error + store.getState().incrementNodeVersion(taskNodeInfo.id, 'task') + } + const newPlanTree = store.getState().taskChat.plan + newPlanTree.task_tree = newPlanTree.task_tree.map((item) => { + if (item.progress === AITaskStatus.inProgress) item.progress = AITaskStatus.error + return item + }) + store.getState().updatePlanTree(newPlanTree) + store.getState().updateState({ taskStatus: cloneDeep(DefaultPlanLoadingStatus) }) + } +} + +const handleMemoryContext: AIMessageHandler = (request) => { + const { res, chatType, store, rawData, meta } = request + if (res.Type !== 'memory_context') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const lists = JSON.parse(ipcContent) as AIAgentGrpcApi.MemoryEntryList + if (chatType === 'reAct') { + meta.casualMemoryList = lists + } else { + meta.taskMemoryList = lists + } + try { + const newMemoryEntryList: AIAgentGrpcApi.MemoryEntryList = { + memories: [...(meta.taskMemoryList.memories || []), ...(meta.casualMemoryList.memories || [])], + memory_pool_limit: + Number(meta.taskMemoryList.memory_pool_limit) + Number(meta.casualMemoryList.memory_pool_limit), + memory_session_id: meta.casualMemoryList.memory_session_id, + total_memories: Number(meta.taskMemoryList.total_memories) + Number(meta.casualMemoryList.total_memories), + total_size: Number(meta.taskMemoryList.total_size) + Number(meta.casualMemoryList.total_size), + score_overview: { + A_total: + Number(meta.taskMemoryList.score_overview.A_total) + Number(meta.casualMemoryList.score_overview.A_total), + C_total: + Number(meta.taskMemoryList.score_overview.C_total) + Number(meta.casualMemoryList.score_overview.C_total), + E_total: + Number(meta.taskMemoryList.score_overview.E_total) + Number(meta.casualMemoryList.score_overview.E_total), + + O_total: + Number(meta.taskMemoryList.score_overview.O_total) + Number(meta.casualMemoryList.score_overview.O_total), + P_total: + Number(meta.taskMemoryList.score_overview.P_total) + Number(meta.casualMemoryList.score_overview.P_total), + R_total: + Number(meta.taskMemoryList.score_overview.R_total) + Number(meta.casualMemoryList.score_overview.R_total), + T_total: + Number(meta.taskMemoryList.score_overview.T_total) + Number(meta.casualMemoryList.score_overview.T_total), + }, + } + rawData.memoryList = newMemoryEntryList + store.getState().updateStateCount('memoryListUpdate') + } catch (error) {} +} + +const handleFileSystemPin: AIMessageHandler = (request) => { + const { res, store } = request + if (res.Type !== 'filesystem_pin_directory' && res.Type !== 'filesystem_pin_filename') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { path } = JSON.parse(ipcContent) as AIAgentGrpcApi.FileSystemPin + store.getState().updateFolders({ path, isFolder: res.Type === 'filesystem_pin_directory' }) +} + +const handleTimelineItem: AIMessageHandler = (request) => { + const { res, store } = request + if (res.Type !== 'structured' || res.NodeId !== 'timeline_item') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const timelineItem = JSON.parse(ipcContent) as AIAgentGrpcApi.TimelineItem + store.getState().updateTimeLineItem(timelineItem) +} + +const handleReactTaskEnqueue: AIMessageHandler = (request) => { + const { res, chatType, sendRequest } = request + if (res.Type !== 'structured' || res.NodeId !== 'react_task_enqueue') return + if (res.IsSync || chatType === 'task') return + + sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) +} +const handleReactTaskDequeue: AIMessageHandler = (request) => { + const { res, chatType, store, rawData, meta, sendRequest } = request + if (res.Type !== 'structured' || res.NodeId !== 'react_task_dequeue') return + if (chatType === 'task') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.QuestionQueueStatusChange + + // 实时数据里,记录用户问题的状态和专注模式信息 + if (!res.IsSync) { + sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) + meta.currentCasualTaskID = data.react_task_id + rawData.casualChat.planDetails = cloneDeep(DefaultPlanItemDetailsData) + meta.focusOfTaskID = data.focus_mode ? data.react_task_id : '' + store.getState().updateState({ focusMode: data.focus_mode ? data.focus_mode : '', casualLoading: true }) + } + + // 用户问题的UI回显 + const chatData: AIChatQSData = { + id: data.react_task_id, + chatType: 'reAct', + type: AIChatQSDataTypeEnum.QUESTION, + Timestamp: res.Timestamp, + data: { qs: data.react_task_input || '', setting: {} }, + AIService: '', + AIModelName: '', + // showQS为了UI渲染方便,重新构建的字段 + extraValue: { showQS: data.react_task_input || '' }, + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: 'reAct', + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) +} + +const handleNotify: AIMessageHandler = (request) => { + const { res, store, meta } = request + if (res.Type !== 'notify') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Notify + if (meta.notifyMessageTimer !== null) { + clearTimeout(meta.notifyMessageTimer) + meta.notifyMessageTimer = null + } + const { type, content } = data + store.getState().updateState({ notifyMessage: { type, content, label: res.NodeIdVerbose } }) + + let durationMs = 0 + if (typeof data.duration_ms === 'number' && !Number.isNaN(data.duration_ms) && data.duration_ms > 0) { + durationMs = data.duration_ms + } else if ( + typeof data.duration_seconds === 'number' && + !Number.isNaN(data.duration_seconds) && + data.duration_seconds > 0 + ) { + durationMs = data.duration_seconds * 1000 + } else if (typeof data.duration === 'number' && !Number.isNaN(data.duration) && data.duration > 0) { + durationMs = data.duration * 1000 + } + if (durationMs > 0) { + meta.notifyMessageTimer = setTimeout(() => { + meta.notifyMessageTimer = null + store.getState().updateState({ notifyMessage: null }) + }, durationMs) + } +} + +const handlePlanExecTasks: AIMessageHandler = (request) => { + const { res, store } = request + if (res.Type !== 'plan_exec_tasks') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const list = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanHistoryList + try { + const arr = cloneDeep(list.records) + if (!arr || arr.length === 0) { + store.getState().updateState({ planHistoryList: { ...list } }) + return + } + const newArr = arr + .map((item) => { + // 因为后端给过来的task_progress是一个json的string类型数据 + item.task_progress = JSON.parse(item.task_progress as unknown as string) as AIAgentGrpcApi.PlanHistoryProgress + // 因为后端给过来的task_tree是一个json的string类型数据,所以需要转换成树形结构的数据,供UI展示使用 + const tree = JSON.parse(item.task_tree as unknown as string) as AIAgentGrpcApi.PlanTask + // 记录任务树根节点的名字,供UI展示使用 + item.root_task_name = tree.name + item.task_tree = genExecTasks(tree) + return item + }) + .filter((item) => item.task_progress.phase !== 'Completed') + store.getState().updateState({ planHistoryList: { ...list, records: newArr } }) + } catch (error) {} +} + +const handleQueueInfo: AIMessageHandler = (request) => { + const { res, chatType, store } = request + if (res.Type !== 'structured' || res.NodeId !== 'queue_info') return + if (chatType === 'task') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { tasks, total_tasks } = JSON.parse(ipcContent) as AIAgentGrpcApi.QuestionQueues + store.getState().updateState({ + questionQueue: { + total: total_tasks, + data: tasks ?? [], + }, + }) +} + +const handleReactTaskStatusChanged: AIMessageHandler = (request) => { + const { res, chatType, store, rawData, meta } = request + if (res.Type !== 'structured' || res.NodeId !== 'react_task_status_changed') return + if (res.IsSync || chatType === 'task') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { react_task_id, react_task_now_status } = JSON.parse(ipcContent) as AIAgentGrpcApi.ReactTaskChanged + if (['completed', 'aborted'].includes(react_task_now_status)) { + if (meta.currentCasualTaskID && meta.currentCasualTaskID === react_task_id) { + // 问题任务完成或者者被中止后,重置当前问题任务id + meta.currentCasualTaskID = '' + store.getState().updateState({ cancelCasualLoading: false }) + } + if (meta.focusOfTaskID === react_task_id) { + // 取消专注模式 + meta.focusOfTaskID = '' + store.getState().updateState({ focusMode: '' }) + } + store.getState().updateState({ casualLoading: false }) + rawData.casualChat.todoList = cloneDeep(DefaultTodoListCardData) + store.getState().updateCasualTodoList() + if (meta.currentTaskPlanID?.taskID === react_task_id) { + // 该问题触发了任务规划, 所以需要将任务规划状态也调整 + meta.currentTaskPlanID.status = react_task_now_status as AITaskStatus + store.getState().updateState({ cancelTaskLoading: false }) + } + } +} + +const handleStatus: AIMessageHandler = (request) => { + const { res, chatType, store } = request + if (res.Type !== 'structured' || res.NodeId !== 'status') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as { key: string; value: string } + if (data.key === 're-act-loading-status-key') { + if (chatType === 'task') { + // 任务规划-loading展示标题 + store.getState().updateTaskLoadingStatus({ task: data.value || '加载中...' }) + } else { + // 自由对话-loading展示标题 + store.getState().updateState({ casualTitle: data.value }) + } + } else if (data.key === 'plan-executing-loading-status-key') { + if (chatType === 'task') { + // 任务规划-loading展示标题 + store.getState().updateTaskLoadingStatus({ plan: data.value || '加载中...' }) + } + } else { + // 执行状态卡片处理 + // yakExecResultEvent.handleSetData(res) + } +} + +const handleTrafficCount: AIMessageHandler = (request) => { + const { res, chatType, store, rawData, pushLog } = request + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.HTTPTrafficNotice & AIAgentGrpcApi.RiskTrafficNotice + if (!data.runtime_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + // 更新流量表和风险表数据, 历史数据不处理 + if (!res.IsSync) { + if (res.Type === 'yak_httpflow_count' && !rawData.httpRunTimeIDs.includes(data.runtime_id)) { + rawData.httpRunTimeIDs.push(data.runtime_id) + store.getState().updateHttpData() + } else if (res.Type === 'yak_risk_count' && !rawData.riskRunTimeIDs.includes(data.runtime_id)) { + rawData.riskRunTimeIDs.push(data.runtime_id) + store.getState().updateRiskData() + } + } + + // 更新工具执行结果卡片里的流量和风险数量 + const toolResult = rawData.contents.get(data.runtime_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || toolResult.chatType !== chatType) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据(call_tool_id:${data.runtime_id})工具结果卡片数据不存在`), + ) + return + } + let update = false + switch (res.Type) { + case 'yak_httpflow_count': + if (data.http_flow_count !== toolResult.data.httpFlowDataCount) { + toolResult.data.httpFlowDataCount = data.http_flow_count ?? 0 + update = true + } + break + case 'yak_risk_count': + if (data.risk_count !== toolResult.data.riskFlowDataCount) { + toolResult.data.riskFlowDataCount = data.risk_count ?? 0 + update = true + } + break + default: + break + } + if (!update) return + store.getState().incrementNodeVersion(toolResult.id, 'item') +} + +const handlePlan: AIMessageHandler = (requestInfo) => { + const { res, chatType, store } = requestInfo + if (res.Type !== 'plan' || chatType === 'reAct') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const tasks = JSON.parse(ipcContent) as { root_task: AIAgentGrpcApi.PlanTask } + if (has(tasks, 'root_task')) { + const plans = genExecTasks(tasks.root_task) + store.getState().updatePlanTree({ task_tree: cloneDeep(plans), root_task_name: tasks.root_task.name }) + } else { + store.getState().updatePlanTree(cloneDeep(DefaultCurrentExecTaskTree)) + } +} + +const handleYaklangCodeChange: AIMessageHandler = (requestInfo) => { + const { res, rawData, store } = requestInfo + if (res.Type !== 'yaklang_code_change') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const yaklangCodeChange = JSON.parse(ipcContent) as AIAgentGrpcApi.YaklangCodeChange + rawData.yaklangCodeChange = yaklangCodeChange + store.getState().updateStateCount('yaklangCodeChangeUpdate') +} + +export const aiOtherDataHandlers = { + http_fuzz_request_change: handleHttpFuzzRequestChange, + http_flow_fuzz_status: handleHttpFlowFuzzStatus, + session_title: handleSessionTitle, + start_plan_and_execution: handleStartPlanAndExecution, + end_plan_and_execution: handleEndPlanAndExecution, + memory_context: handleMemoryContext, + filesystem_pin_directory: handleFileSystemPin, + filesystem_pin_filename: handleFileSystemPin, + timeline_item: handleTimelineItem, + react_task_enqueue: handleReactTaskEnqueue, + react_task_dequeue: handleReactTaskDequeue, + notify: handleNotify, + plan_exec_tasks: handlePlanExecTasks, + queue_info: handleQueueInfo, + react_task_status_changed: handleReactTaskStatusChanged, + status: handleStatus, + yak_httpflow_count: handleTrafficCount, + yak_risk_count: handleTrafficCount, + plan: handlePlan, + yaklang_code_change: handleYaklangCodeChange, +} as const + +const exampleHandle = (res: AIOutputEvent) => { + let funcKey = res.Type + if ( + res.Type === 'structured' && + [ + 'session_title', + 'timeline_item', + 'react_task_enqueue', + 'react_task_dequeue', + 'queue_info', + 'react_task_status_changed', + 'status', + ].includes(res.NodeId) + ) { + // stream数据结束标识 + funcKey = res.NodeId + } +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiPerf.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiPerf.ts new file mode 100644 index 0000000000..85e45f2aee --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiPerf.ts @@ -0,0 +1,220 @@ +import type { AIMessageHandler } from '../type' +import type { AIAgentGrpcApi } from '../grpcApi' +import type { AIContextStatsDetail } from '@/pages/ai-agent/type/aiChat' +import { Uint8ArrayToString } from '@/utils/str' + +const handleConsumption: AIMessageHandler = (request) => { + const { res, rawData } = request + if (res.Type !== 'consumption') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Consumption + rawData.aiPerfData.consumption.input_consumption = data.input_consumption + rawData.aiPerfData.consumption.output_consumption = data.output_consumption + rawData.aiPerfData.consumption.cache_hit_token = data.cache_hit_token + rawData.aiPerfData.consumption.tier_consumption = { ...data.tier_consumption } +} + +const handlePressure: AIMessageHandler = (request) => { + const { res, rawData } = request + if (res.Type !== 'pressure') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Pressure + const target = rawData.aiPerfData.pressure[data.model_tier] + if (target) { + target.push({ ...data, timestamp: Number(res.Timestamp) || 0 }) + } else { + rawData.aiPerfData.pressure[data.model_tier] = [{ ...data, timestamp: Number(res.Timestamp) || 0 }] + } +} + +const handleaAIFirstByteCostMS: AIMessageHandler = (request) => { + const { res, rawData } = request + if (res.Type !== 'ai_first_byte_cost_ms') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIFirstCostMS + const target = rawData.aiPerfData.firstCost[data.model_tier] + if (target) { + target.push({ ...data, timestamp: Number(res.Timestamp) || 0 }) + } else { + rawData.aiPerfData.firstCost[data.model_tier] = [{ ...data, timestamp: Number(res.Timestamp) || 0 }] + } +} + +const handleAITotalCostMS: AIMessageHandler = (request) => { + const { res, rawData } = request + if (res.Type !== 'ai_total_cost_ms') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AITotalCostMS + const target = rawData.aiPerfData.totalCost[data.model_tier] + if (target) { + target.push({ ...data, timestamp: Number(res.Timestamp) || 0 }) + } else { + rawData.aiPerfData.totalCost[data.model_tier] = [{ ...data, timestamp: Number(res.Timestamp) || 0 }] + } +} + +const CONTEXT_STATS_SERIES_MAX = 50 +/** + * 首次建立 role_order 时,若存在这些 role_name 则按此顺序排在前面。 + * + * P1.1 之后 aireact 已经把老的 semi_dynamic 段彻底拆成 semi_dynamic_1 + * (Skills Context + CacheToolCall) 与 semi_dynamic_2 (Persistent + + * Schema + OutputExample) 两个独立 role;老的 semi_dynamic 已被移除, + * 不再保留兜底位。 + * + * 字节统计图 "从下往上" 的堆叠物理顺序固定为: + * high_static -> frozen_block -> semi_dynamic_1 -> semi_dynamic_2 -> + * timelineOpen -> dynamic + */ +const CONTEXT_STATS_ROLE_NAME_ORDER = [ + 'high_static', + 'frozen_block', + 'semi_dynamic_1', + 'semi_dynamic_2', + 'timelineOpen', + 'dynamic', +] as const +const trimContextStatsSeries = (d: AIContextStatsDetail['data']) => { + if (!Array.isArray(d.total_prompt_bytes)) d.total_prompt_bytes = [] + if (!Array.isArray(d.total_prompt_tokens)) d.total_prompt_tokens = [] + while (d.times.length > CONTEXT_STATS_SERIES_MAX) { + d.times.shift() + d.total_prompt_bytes.shift() + d.total_prompt_tokens.shift() + for (const name of d.role_order) { + d.role_series[name]?.shift() + d.role_tokens[name]?.shift() + } + } +} + +/** 递归上下文成分里的summary并归类到map对象后消除summary字段内容 */ +const handleSummarySectionsSummary = ( + sections: AIAgentGrpcApi.AIContextSections[], + summaryMap: Map, +) => { + for (let item of sections) { + summaryMap.set(item.key, item.summary || '') + delete item.summary + if (item.children) handleSummarySectionsSummary(item.children, summaryMap) + } +} + +const handlePromptProfile: AIMessageHandler = (request) => { + const { res, rawData } = request + if (res.Type !== 'prompt_profile') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ContextStatsSections + + // 上下文字节统计 + if (rawData.aiPerfData.contextStats) { + const d = rawData.aiPerfData.contextStats.data + const incomingRoles = Array.isArray(data.role_stats) ? data.role_stats : [] + const ts = Number(res.Timestamp) || 0 + + if (incomingRoles.length > 0) { + // 每个 turn 后端都会按 promptSectionRolesInOrder 推全集 6 个 role + // (P1.1: high_static / frozen_block / semi_dynamic_1 / semi_dynamic_2 / + // timelineOpen / dynamic), 即便 bytes 为 0 也会预填. 所以这里每次 + // 都用 incoming 重新 reconcile role_order, 既能让新出现的 role + // (如 semi_dynamic_1/2) 按 CONTEXT_STATS_ROLE_NAME_ORDER 物理顺序 + // 插入, 也能把 incoming 里彻底消失的老 role (如老的 'semi_dynamic') + // 自动 evict 掉, 避免历史会话残留的图例顺序污染. + const incomingMap = new Map() + for (const r of incomingRoles) { + if (!r.role_name || incomingMap.has(r.role_name)) continue + incomingMap.set(r.role_name, r) + } + + const preferred = new Set(CONTEXT_STATS_ROLE_NAME_ORDER) + const desiredOrder: string[] = [] + for (const name of CONTEXT_STATS_ROLE_NAME_ORDER) { + if (incomingMap.has(name)) desiredOrder.push(name) + } + for (const name of incomingMap.keys()) { + if (!preferred.has(name) && !desiredOrder.includes(name)) { + desiredOrder.push(name) + } + } + + const oldOrderSet = new Set(d.role_order) + const newOrderSet = new Set(desiredOrder) + + // 新出现的 role: 用 0 补齐已积累的 d.times 长度, 让历史时间轴上 + // 这条线从 0 起步, 而不是凭空错位 + for (const name of desiredOrder) { + const r = incomingMap.get(name) + if (!oldOrderSet.has(name)) { + d.role_labels[name] = r?.role_name_zh || name + d.role_series[name] = new Array(d.times.length).fill(0) + } else if (r?.role_name_zh) { + d.role_labels[name] = r.role_name_zh + } + } + // 老 role (如 'semi_dynamic') 在 incoming 中彻底消失 -> 释放 series + for (const name of d.role_order) { + if (!newOrderSet.has(name)) { + delete d.role_labels[name] + delete d.role_series[name] + } + } + + d.role_order = desiredOrder + } + + rawData.aiPerfData.contextStats.prompt_bytes = data.prompt_bytes ?? 0 + rawData.aiPerfData.contextStats.prompt_tokens = data.prompt_tokens ?? 0 + d.times.push(ts) + d.total_prompt_bytes.push(data.prompt_bytes ?? 0) + d.total_prompt_tokens.push(data.prompt_tokens ?? 0) + + if (d.role_order.length > 0) { + const map = new Map() + const tokenMap = new Map() + for (const r of incomingRoles) { + if (!d.role_order.includes(r.role_name)) continue + map.set(r.role_name, r.role_bytes ?? 0) + tokenMap.set(r.role_name, r.role_tokens ?? 0) + } + for (const name of d.role_order) { + if (!d.role_series[name]) d.role_series[name] = [] + d.role_series[name].push(map.get(name) ?? 0) + } + for (const name of d.role_order) { + if (!d.role_tokens[name]) d.role_tokens[name] = [] + d.role_tokens[name].push(tokenMap.get(name) ?? 0) + } + } + + trimContextStatsSeries(d) + } + + const sections = rawData.aiPerfData.contextSections + const sectionsData = data.sections || [] + if (sections) { + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + // 每次新的后端数据进来,需要重置上次数据逻辑处理的map对象,避免之前数据对当前数据处理造成影响 + const summaryMap = new Map() + handleSummarySectionsSummary(sectionsData, summaryMap) + sections.summary = summaryMap + sections.sections = sectionsData + } +} + +export const aiPerfDataHandlers = { + consumption: handleConsumption, + pressure: handlePressure, + ai_first_byte_cost_ms: handleaAIFirstByteCostMS, + ai_total_cost_ms: handleAITotalCostMS, + prompt_profile: handlePromptProfile, +} as const diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts new file mode 100644 index 0000000000..36b06e9b57 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -0,0 +1,630 @@ +import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import type { AIAgentGrpcApi } from '../grpcApi' +import { Uint8ArrayToString } from '@/utils/str' +import { + genBaseAIChatData, + generateTaskNodeDataID, + genErrorLogData, + genExecTasks, + isAutoExecuteReviewContinue, +} from '../utils' +import { type AIChatQSData, AIChatQSDataTypeEnum, type AIReviewType } from '../aiRender' +import cloneDeep from 'lodash/cloneDeep' +import { AIReviewJudgeLevelMap } from '../defaultConstant' + +/** grpc流数据转换成错误信息输出到日志中 */ +const handleErrorGRPCToLog: ( + /** 该条grpc流数据是历史数据 */ + isHistory: AIMessageHandlerParams['res']['IsSync'], + pushLog: AIMessageHandlerParams['pushLog'], + error: ReturnType, +) => void = (isHistory, pushLog, error) => { + if (isHistory) return + pushLog(error) +} + +const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + if (res.Type !== 'plan_review_require') return + // 历史数据-grpc流数据在任务规划下无效,不处理 + if (res.IsSync && chatType === 'task') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequire + if (!data?.id || !data?.plans || !data?.plans?.root_task || !data?.selectors || !data?.selectors?.length) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + id: data.id, + type: AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE, + data: { ...cloneDeep(data) }, + } + if (res.IsSync) { + // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 + const target = meta.historyReviewReleaseID[data.id] + if (target) { + chatData.data.selected = JSON.stringify(target.params) + chatData.data.optionValue = target.params?.suggestion || 'continue' + } + rawData.contents.set(chatData.id, cloneDeep(chatData)) + if (target) { + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } + return + } + + // 实时数据处理逻辑 + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) + if (isAuto) { + chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) + chatData.data.optionValue = 'continue' + } + // 将数据存入hook里的缓存变量中 + if (!isAuto) meta.reviewData.set(chatData.id, cloneDeep(chatData)) + if (chatType === 'task') { + // 该类型的实时数据只有任务规划才有 + if (isAuto) { + rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + const tasks = chatData.data + const plans = genExecTasks(tasks.plans.root_task) + store.getState().updatePlanTree({ + task_tree: cloneDeep(plans), + root_task_name: tasks.plans.root_task.name, + }) + } else { + meta.currentPlanReviewId = '' + store.getState().updateState({ currentPlanReviewData: cloneDeep(chatData) }) + } + } +} +const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + if (res.Type !== 'plan_task_analysis') return + // 历史数据-grpc流数据在任务规划下无效,不处理 + if (res.IsSync || chatType !== 'task') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequireExtra + const reviewDetail = meta.reviewData.get(data.plans_id) + if (!reviewDetail || reviewDetail.type !== AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据异常: 未找到对应plan_review_require数据`), + ) + return + } + + if ( + !data?.plans_id || + !data?.index || + !data?.keywords?.length || + (meta.currentPlanReviewId && meta.currentPlanReviewId !== data.plans_id) + ) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + if (!meta.currentPlanReviewId) meta.currentPlanReviewId = data.plans_id + const reviewInfo = reviewDetail.data + if (!reviewInfo.taskExtra) reviewInfo.taskExtra = new Map() + reviewInfo.taskExtra.set(data.index, data) + + const isAuto = isAutoExecuteReviewContinue({ getFunc: () => request }) + if (!isAuto) meta.planReviewExtraData.set(data.index, cloneDeep(data)) +} + +const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + if (res.Type !== 'task_review_require') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.TaskReviewRequire + if (!data?.id || !data?.selectors || !data?.selectors?.length) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + id: data.id, + type: AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE, + data: { ...cloneDeep(data) }, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + if (res.IsSync) { + // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 + const target = meta.historyReviewReleaseID[data.id] + if (target) { + chatData.data.selected = JSON.stringify(target.params) + chatData.data.optionValue = target.params?.suggestion || 'continue' + } + rawData.contents.set(chatData.id, cloneDeep(chatData)) + if (target) { + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } + return + } + + // 实时数据处理逻辑 + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) + if (isAuto) { + chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) + chatData.data.optionValue = 'continue' + } + // 将数据存入hook里的缓存变量中 + if (!isAuto) meta.reviewData.set(chatData.id, cloneDeep(chatData)) + if (chatType === 'task') { + if (isAuto) { + rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } else { + // 有问题,需要调整 + store.getState().updateState({ currentPlanReviewData: cloneDeep(chatData) }) + } + } else if (chatType === 'reAct') { + rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } +} + +const handleToolReview: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + if (res.Type !== 'tool_use_review_require') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ToolUseReviewRequire + if (!data?.id || !data?.selectors || !data?.selectors?.length) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + id: data.id, + type: AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE, + data: { ...cloneDeep(data) }, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + if (res.IsSync) { + // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 + const target = meta.historyReviewReleaseID[data.id] + if (target) { + chatData.data.selected = JSON.stringify(target.params) + chatData.data.optionValue = target.params?.suggestion || 'continue' + } + rawData.contents.set(chatData.id, cloneDeep(chatData)) + if (target) { + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } + return + } + + // 实时数据处理逻辑 + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) + if (isAuto) { + chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) + chatData.data.optionValue = 'continue' + } + // 将数据存入hook里的缓存变量中 + if (!isAuto) meta.reviewData.set(chatData.id, cloneDeep(chatData)) + if (chatType === 'task') { + if (isAuto) { + rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } else { + // 有问题,需要调整 + store.getState().updateState({ currentPlanReviewData: cloneDeep(chatData) }) + } + } else if (chatType === 'reAct') { + rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } +} + +const handleUserInteractive: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + if (res.Type !== 'require_user_interactive') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewRequire + if (!data?.id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + id: data.id, + type: AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE, + data: cloneDeep(data), + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + if (res.IsSync) { + // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 + const target = meta.historyReviewReleaseID[data.id] + if (target) { + chatData.data.selected = JSON.stringify(target.params) + chatData.data.optionValue = target.params?.suggestion || 'continue' + } + rawData.contents.set(chatData.id, cloneDeep(chatData)) + if (target) { + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } + return + } + + // 将数据存入hook里的缓存变量中 + meta.reviewData.set(chatData.id, cloneDeep(chatData)) + if (chatType === 'task') { + // 有问题,需要调整 + store.getState().updateState({ currentPlanReviewData: cloneDeep(chatData) }) + } else if (chatType === 'reAct') { + rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } +} + +const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + // 任务规划不存在该类型数据 + if (chatType === 'task') return + if (res.Type !== 'exec_aiforge_review_require') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ExecForgeReview + if (!data?.id || !data?.selectors || !data?.selectors?.length) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + id: data.id, + type: AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE, + data: { ...cloneDeep(data) }, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + if (res.IsSync) { + // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 + const target = meta.historyReviewReleaseID[data.id] + if (target) { + chatData.data.selected = JSON.stringify(target.params) + chatData.data.optionValue = target.params?.suggestion || 'continue' + } + rawData.contents.set(chatData.id, cloneDeep(chatData)) + if (target) { + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } + return + } + + // 实时数据处理逻辑 + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) + if (isAuto) { + chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) + chatData.data.optionValue = 'continue' + } + // 将数据存入hook里的缓存变量中 + if (!isAuto) meta.reviewData.set(chatData.id, cloneDeep(chatData)) + rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) +} + +const handleAIReviewJudgement: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + if (!['ai_review_start', 'ai_review_countdown', 'ai_review_end'].includes(res.Type)) return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const score = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewJudgement + const reviewDetail = meta.reviewData.get(score.interactive_id) + if (!reviewDetail || reviewDetail.id !== score.interactive_id) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据异常: 没有对应的review数据`), + ) + return + } + if (!score?.interactive_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + score.levelLabel = AIReviewJudgeLevelMap[score?.level || '']?.label || undefined + if (chatType === 'task') { + if (reviewDetail.type !== AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData( + res.Timestamp, + `${res.Type}数据异常(interactive_id:${score?.interactive_id || '-'})未找到对应review`, + ), + ) + return + } + + if ( + !reviewDetail.data.aiReview || + (reviewDetail.data.aiReview && typeof reviewDetail.data.aiReview.seconds === 'undefined') + ) { + // aiReview 没有或者 aiReview 的 seconds 为空时可以赋值 + reviewDetail.data.aiReview = cloneDeep(score) + } + const isAuto = isAutoExecuteReviewContinue({ getFunc: () => request }) + if (!isAuto) store.getState().incrementNodeVersion(reviewDetail.id, 'item') + } else if (chatType === 'reAct') { + if ( + reviewDetail.type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || + reviewDetail.type === AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE + ) { + if ( + !reviewDetail.data.aiReview || + (reviewDetail.data.aiReview && typeof reviewDetail.data.aiReview.seconds === 'undefined') + ) { + reviewDetail.data.aiReview = cloneDeep(score) + } + } + + const chatData = rawData.contents.get(score.interactive_id) + if ( + chatData && + (chatData.type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || + chatData.type === AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE) + ) { + if ( + !chatData.data.aiReview || + (chatData.data.aiReview && typeof chatData.data.aiReview.seconds === 'undefined') + ) { + // aiReview 没有或者 aiReview 的 seconds 为空时可以赋值 + chatData.data.aiReview = cloneDeep(score) + store.getState().incrementNodeVersion(reviewDetail.id, 'item') + } + } else { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData( + res.Timestamp, + `${res.Type}数据(interactive_id:${score?.interactive_id || '-'})未找到对应review`, + ), + ) + } + } +} + +const handleReviewRelease: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + if (res.Type !== 'review_release') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ReviewRelease + if (!data?.id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + if (res.IsSync) { + const reviewDetail = rawData.contents.get(data.id) + if (!reviewDetail) { + meta.historyReviewReleaseID[data.id] = data + return + } + switch (reviewDetail.type) { + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: + reviewDetail.data.selected = JSON.stringify(data.params) + reviewDetail.data.optionValue = data.params?.suggestion || 'continue' + store.getState().incrementNodeVersion(reviewDetail.id, 'item') + return + } + } else { + const reviewDetail = meta.reviewData.get(data.id) + if (!reviewDetail) return + + const reviewInfo = cloneDeep(reviewDetail?.data) as AIReviewType + if (reviewInfo?.id !== data.id) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData( + res.Timestamp, + `${res.Type}数据(id:${data?.id || '-'})和当前展示review数据(id:${reviewInfo?.id || '-'})不匹配`, + ), + ) + return + } + + reviewInfo.selected = JSON.stringify({ suggestion: 'continue' }) + reviewInfo.optionValue = 'continue' + const chatData: AIChatQSData = { + ...reviewDetail, + data: reviewInfo as any, + } + meta.reviewData.delete(data.id) + if (chatType === 'task') { + meta.currentPlanReviewId = '' + // review自动释放后,还需进行的额外逻辑处理 + if (reviewDetail.type === AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { + const tasks = reviewDetail.data + const plans = genExecTasks(tasks.plans.root_task) + store.getState().updatePlanTree({ + task_tree: cloneDeep(plans), + root_task_name: tasks.plans.root_task.name, + }) + } + } + rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + + // 是否要释放掉控制review弹窗的状态变量??? + } +} + +export const aiReviewDataHandlers = { + plan_review_require: handlePlanReviewRequire, + plan_task_analysis: handlePlanTaskAnalysis, + task_review_require: handleTaskReviewRequire, + tool_use_review_require: handleToolReview, + require_user_interactive: handleUserInteractive, + exec_aiforge_review_require: handleAIForgeReviewRequire, + ai_review_start: handleAIReviewJudgement, + ai_review_countdown: handleAIReviewJudgement, + ai_review_end: handleAIReviewJudgement, + review_release: handleReviewRelease, +} as const diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts new file mode 100644 index 0000000000..2ca8d9f8f0 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -0,0 +1,494 @@ +import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi, type AIOutputEvent } from '../grpcApi' +import { Uint8ArrayToString } from '@/utils/str' +import { + genBaseAIChatData, + generateTaskNodeDataID, + generateTaskNodeID, + genErrorLogData, + isValidTaskIndex, +} from '../utils' +import { type AIChatQSData, AIChatQSDataTypeEnum, type ReportFinishCardData } from '../aiRender' +import { convertNodeIdToVerbose } from '../defaultConstant' + +/** grpc流数据转换成错误信息输出到日志中 */ +const handleErrorGRPCToLog: ( + /** 该条grpc流数据是历史数据 */ + isHistory: AIMessageHandlerParams['res']['IsSync'], + pushLog: AIMessageHandlerParams['pushLog'], + error: ReturnType, +) => void = (isHistory, pushLog, error) => { + if (isHistory) return + pushLog(error) +} + +const handleThought: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo + if (res.Type !== 'thought') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { thought } = (JSON.parse(ipcContent) as AIAgentGrpcApi.AIChatThought) || {} + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + type: AIChatQSDataTypeEnum.THOUGHT, + data: thought || '', + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) +} + +const handleResult: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo + if (res.Type !== 'result') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { result, after_stream } = (JSON.parse(ipcContent) as AIAgentGrpcApi.AIChatResult) || {} + if (after_stream) return + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + type: AIChatQSDataTypeEnum.RESULT, + data: result || '', + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) +} + +const handleFailReactTask: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo + if (res.Type !== 'fail_react_task') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + type: AIChatQSDataTypeEnum.FAIL_REACT, + data: { + content: ipcContent, + NodeId: res.NodeId, + NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), + }, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) +} + +const handleToolCallDecision: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo + if (res.Type !== 'tool_call_decision') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ToolCallDecision + const i18n = data?.i18n || { zh: data.action, en: data.action } + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + type: AIChatQSDataTypeEnum.TOOL_CALL_DECISION, + data: { + ...data, + i18n: { + Zh: i18n.zh, + En: i18n.en, + }, + }, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) +} + +const handleFailPlanAndExecution: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo + if (res.Type !== 'fail_plan_and_execution') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + type: AIChatQSDataTypeEnum.FAIL_PLAN_AND_EXECUTION, + data: { + content: ipcContent, + NodeId: res.NodeId, + NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), + }, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) +} + +const handleApiRequestFailed: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo + if (res.Type !== 'api_request_failed' || res.NodeId !== 'ai_call_failure') return + // 历史数据无用-不处理 + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIApiRequestFailedPayload + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + type: AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED, + data, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) +} + +const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta, pushLog } = requestInfo + if (res.Type !== 'http_flow_fuzz_status') return + // 历史数据无用-不处理 + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const payload = JSON.parse(ipcContent) as AIAgentGrpcApi.GetHttpFlowFuzzStatus + const { fuzz_id, runtime_id, reason, status } = payload + if (!fuzz_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + const chatDetail = rawData.contents.get(fuzz_id) + + if (chatDetail && chatDetail.type === AIChatQSDataTypeEnum.HTTP_FLOW_FUZZ_STATUS) { + chatDetail.data.runtime_id = runtime_id + chatDetail.data.reason = reason + chatDetail.data.engine_status = status + chatDetail.data.progress = status === 'working' ? payload.progress : chatDetail.data.progress + store.getState().incrementNodeVersion(chatDetail.id, 'item') + } else { + // 引擎结束态没有对应卡片时直接丢弃,保留原行为 + if (status === 'finish') return + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + id: fuzz_id, + chatType: chatType, + type: AIChatQSDataTypeEnum.HTTP_FLOW_FUZZ_STATUS, + data: { + fuzz_id, + runtime_id, + reason, + engine_status: status, + // 仅 `working` 覆盖 progress;其它状态保留上一次(新建时默认 undefined) + progress: status === 'working' ? payload.progress : undefined, + }, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } +} + +const handleReportFinish: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta, pushLog } = requestInfo + if (res.Type !== 'report_finish' || res.NodeId !== 'report-finish') return + + const ipcContent = Uint8ArrayToString(res.Content) || '{}' + + const parsed = JSON.parse(ipcContent) as AIAgentGrpcApi.ReportFinishPayload + let report_path = parsed?.report_path ?? '' + let title = parsed?.title ?? '' + let content = parsed?.summary_markdown ?? '' + + if (!report_path) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type} 数据缺少 report_path`)) + return + } + + const cardType = AIChatQSDataTypeEnum.REPORT_FINISH + const nextData: ReportFinishCardData = { reportPath: report_path, title, content } + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + type: cardType, + data: nextData, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) +} + +const handlePushTask: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta, pushLog } = requestInfo + if (res.Type !== 'structured' || res.NodeId !== 'system') return + // 历史数据和自由对话数据不处理 + if (res.IsSync || chatType === 'reAct') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) || '' + if (!data || typeof data !== 'object' || data?.type !== 'push_task') return + + const info = data as AIAgentGrpcApi.ChangeTask + const newPlanTree = store.getState().taskChat.plan + newPlanTree.task_tree = newPlanTree.task_tree.map((item) => { + if (item.index === info.task.index) item.progress = AITaskStatus.inProgress + return item + }) + store.getState().updatePlanTree(newPlanTree) + + if (isValidTaskIndex(info.task.index) && meta.currentTaskPlanID?.taskID) { + const taskID = generateTaskNodeID(meta.currentTaskPlanID.taskID, info.task.index) + const chatDetail = rawData.contents.get(taskID) + if (chatDetail && chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${info.task.index}-push_task数据已存在`), + ) + return + } + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + id: taskID, + chatType: 'task', + type: AIChatQSDataTypeEnum.TASK_NODE_GROUP, + data: { + taskIndex: info.task.index, + taskId: info.task.task_id, + taskName: info.task.name, + goal: info.task.goal, + status: info.task.task_status || AITaskStatus.inProgress, + }, + } + rawData.contents.set(chatData.id, chatData) + meta.currentTaskPlanActiveNode.add(chatData.id) + // 这里要判断是否是最后一个默认任务组,直接添加不行 + // setElements((old) => { + // const exists = old.some((item) => item.token === chatData.id && item.type === chatData.type) + // if (exists) return old + // const last = old[old.length - 1] + // if (last.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP) { + // // 实时数据下,将默认任务聚合组置底 + // old.splice(old.length - 1, 0, { + // token: chatData.id, + // type: chatData.type, + // renderNum: 1, + // chatType: 'task', + // kind: 'task', + // children: [], + // }) + // return [...old] + // } + // return [ + // ...old, + // { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task', kind: 'task', children: [] }, + // ] + // }) + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'task', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } +} + +const handlePopTask: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta, pushLog, sendRequest } = requestInfo + if (res.Type !== 'structured' || res.NodeId !== 'system') return + // 历史数据和自由对话数据不处理 + if (res.IsSync || chatType === 'reAct') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) || '' + if (!data || typeof data !== 'object' || data?.type !== 'pop_task') return + + const info = data as AIAgentGrpcApi.ChangeTask + const newPlanTree = store.getState().taskChat.plan + newPlanTree.task_tree = newPlanTree.task_tree.map((item) => { + if (item.index === info.task.index) item.progress = info.task.task_status + return item + }) + store.getState().updatePlanTree(newPlanTree) + + if (isValidTaskIndex(info.task.index) && meta.currentTaskPlanID?.taskID) { + const taskID = generateTaskNodeID(meta.currentTaskPlanID.taskID, info.task.index) + const chatDetail = rawData.contents.get(taskID) + if (!chatDetail || chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${info.task.index}-pop_task数据不存在`)) + return + } + meta.currentTaskPlanActiveNode.delete(info.task.index) + chatDetail.data.status = info.task.task_status + store.getState().incrementNodeVersion(chatDetail.id, 'task') + + sendRequest && sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) + } +} + +/** 单条grpc流数据对应一个独立的UI数据 */ +export const aiSingleItemDataHandlers = { + thought: handleThought, + result: handleResult, + fail_react_task: handleFailReactTask, + tool_call_decision: handleToolCallDecision, + fail_plan_and_execution: handleFailPlanAndExecution, + ai_call_failure: handleApiRequestFailed, + http_flow_fuzz_status: handleHttpFlowFuzzStatus, + 'report-finish': handleReportFinish, + push_task: handlePushTask, + pop_task: handlePopTask, +} as const + +const exampleHandle = (res: AIOutputEvent) => { + let funcKey = res.Type + if ( + res.Type === 'structured' && + [ + 'session_title', + 'timeline_item', + 'react_task_enqueue', + 'react_task_dequeue', + 'queue_info', + 'react_task_status_changed', + 'status', + ].includes(res.NodeId) + ) { + // stream数据结束标识 + funcKey = res.NodeId + } else if (res.Type === 'api_request_failed' && res.NodeId === 'ai_call_failure') { + funcKey = res.NodeId + } else if (res.Type === 'report_finish' || res.NodeId === 'report-finish') { + funcKey = res.NodeId + } else if (res.Type === 'structured' && res.NodeId === 'system') { + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) || '' + if (data && typeof data === 'object' && data?.type === 'push_task') { + funcKey = 'push_task' + } else if (data && typeof data === 'object' && data?.type === 'pop_task') { + funcKey = 'pop_task' + } + } +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts new file mode 100644 index 0000000000..ee4e288e6a --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -0,0 +1,510 @@ +import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import type { AIAgentGrpcApi, AIOutputEvent } from '../grpcApi' +import { Uint8ArrayToString } from '@/utils/str' +import { + genBaseAIChatData, + generateTaskNodeDataID, + genErrorLogData, + isToolStderrStream, + isToolStdoutStream, +} from '../utils' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' +import { convertNodeIdToVerbose } from '../defaultConstant' +import { aiAgentLogEmitter } from '../AIAgentLogEmitter' + +/** grpc流数据转换成错误信息输出到日志中 */ +const handleErrorGRPCToLog: ( + /** 该条grpc流数据是历史数据 */ + isHistory: AIMessageHandlerParams['res']['IsSync'], + pushLog: AIMessageHandlerParams['pushLog'], + error: ReturnType, +) => void = (isHistory, pushLog, error) => { + if (isHistory) return + pushLog(error) +} + +const handleStreamStart: AIMessageHandler = (requestInfo) => { + const { res, chatType, rawData, meta, pushLog } = requestInfo + if (res.Type !== 'stream_start') return + + if (res.IsSystem || res.IsReason) return + + const { CallToolID, NodeId } = res + if (!NodeId) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { event_writer_id } = JSON.parse(ipcContent) as { event_writer_id: string } + // event_writer_id为空 + if (!event_writer_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + + // tool-xxx-stderr 数据单独初始化逻辑 + if (isToolStderrStream(NodeId) && CallToolID) { + if (!CallToolID) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), + ) + return + } + if (!meta.toolStderrStreamData.has(CallToolID)) { + meta.toolStderrStreamData.set(CallToolID, { + content: '', + uuid: event_writer_id, + status: 'start', + }) + } + return + } + // tool-xxx-stdout 数据单独初始化逻辑 + if (isToolStdoutStream(NodeId)) { + if (!CallToolID) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), + ) + return + } + let toolResult = rawData.contents.get(CallToolID) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData( + res.Timestamp, + `NodeID: ${NodeId} 的stream数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, + ), + ) + return + } + + rawData.contents.set(event_writer_id, { + ...genBaseAIChatData(res), + id: event_writer_id, + chatType: chatType, + type: AIChatQSDataTypeEnum.STREAM, + data: { + NodeId, + NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(NodeId), + CallToolID, + EventUUID: event_writer_id, + status: 'start', + content: '', + ContentType: res.ContentType, + }, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + }) + toolResult.data.stream.EventUUID = event_writer_id + toolResult.data.type = 'stream' + return + } + + // 数据集合中对应的数据 + const streamData = rawData.contents.get(event_writer_id) + + // 数据已存在,流数据输出顺序不对, 视为异常 + if (streamData) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `Stream-NodeId: ${NodeId}, EventUUID: (${event_writer_id}), 已存在对应的数据`), + ) + return + } + + rawData.contents.set(event_writer_id, { + ...genBaseAIChatData(res), + id: event_writer_id, + chatType: chatType, + type: AIChatQSDataTypeEnum.STREAM, + data: { + NodeId, + NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(NodeId), + CallToolID, + EventUUID: event_writer_id, + status: 'start', + content: '', + ContentType: res.ContentType, + }, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + }) +} + +const handleStream: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta, pushLog } = requestInfo + if (res.Type !== 'stream') return + // 历史数据-(系统信息|推理信息)数据,不处理 + if (res.IsSync && (res.IsSystem || res.IsReason)) return + + const { CallToolID, EventUUID, NodeId } = res + const content = (Uint8ArrayToString(res.Content) || '') + (Uint8ArrayToString(res.StreamDelta) || '') + if (!EventUUID || !NodeId) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据缺失: EventUUID(${EventUUID}) NodeId(${NodeId})`), + ) + return + } + + if (res.IsSystem) { + // 实时数据-系统信息 + const lastUUID = meta.systemEventUUID[meta.systemEventUUID.length - 1] + if (lastUUID) { + if (lastUUID === EventUUID) { + // 由UI进行定时获取渲染(打字机效果) + rawData.systemStream += content + store.getState().updateStateCount('updateSystemStream') + } else { + if (meta.systemEventUUID.includes(EventUUID)) return + meta.systemEventUUID.push(EventUUID) + rawData.systemStream = content + store.getState().updateStateCount('updateSystemStream') + } + } else { + meta.systemEventUUID.push(EventUUID) + rawData.systemStream = content + store.getState().updateStateCount('updateSystemStream') + } + return + } + if (res.IsReason) { + // 实时数据-推理信息-系统信息输出到日志中 + aiAgentLogEmitter.dispatch({ + session: '', + type: 'stream', + Timestamp: res.Timestamp, + stream: { + NodeId: res.NodeId, + EventUUID: res.EventUUID, + content: (Uint8ArrayToString(res.Content) || '') + (Uint8ArrayToString(res.StreamDelta) || ''), + status: 'start', + }, + }) + return + } + + // tool-xxx-stderr 数据单独处理逻辑 + if (isToolStderrStream(NodeId)) { + if (!CallToolID) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), + ) + return + } + const errorResult = meta.toolStderrStreamData.get(CallToolID) + if (errorResult) errorResult.content += content + return + } + // tool-xxx-stdout 数据单独处理逻辑 + if (isToolStdoutStream(NodeId)) { + if (!CallToolID) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), + ) + return + } + const toolResult = rawData.contents.get(CallToolID) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData( + res.Timestamp, + `Stream-NodeID: ${NodeId} 数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, + ), + ) + return + } + const toolForStreamData = rawData.contents.get(toolResult.data.stream.EventUUID) + if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `Stream-EventUUID: ${toolResult.data.stream.EventUUID} 数据没有对应的初始化`), + ) + return + } + const isRender = !toolForStreamData.data.content + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolForStreamData.data.content += content + if (isRender) { + // 先确定是UI定时拿数据还是流数据触发渲染,还有就是,UI已经渲染出来工具卡片了吗 + store.getState().incrementNodeVersion(toolForStreamData.id, 'item') + } + return + } + + // 数据集合中对应的数据 + const streamData = rawData.contents.get(EventUUID) + + // 数据不存在 + if (!streamData || streamData.type !== AIChatQSDataTypeEnum.STREAM) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `Stream-NodeId: ${NodeId}, EventUUID: (${EventUUID}) 数据未初始化`), + ) + return + } + + const isRender = !streamData.data.content + // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + streamData.Timestamp = isRender ? res.Timestamp : streamData.Timestamp + streamData.data.content += content + + if (isRender) { + // 判断是否成为组UI数据展示 + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: streamData.taskIndex, + node: { + token: streamData.id, + kind: 'item', + type: streamData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } +} + +const handleStreamFinished: AIMessageHandler = (requestInfo) => { + const { res, store, rawData, meta, pushLog } = requestInfo + if (res.Type !== 'structured' || res.NodeId !== 'stream-finished') return + // 历史数据-(系统信息|推理信息)数据,不处理 + if (res.IsSync && (res.IsSystem || res.IsReason)) return + + let ipcContent = Uint8ArrayToString(res.Content) || '' + const { event_writer_id, node_id, is_reason, is_system } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStreamFinished + if (!event_writer_id) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `stream-finished数据, event_writer_id 为空`), + ) + return + } + + // 实时数据-系统信息(不需要结束处理, 由下一个自动顶替) + if (is_system) return + if (is_reason) { + // 实时数据-推理信息-系统信息输出到日志中 + aiAgentLogEmitter.dispatch({ + session: '', + type: 'stream', + Timestamp: res.Timestamp, + stream: { + NodeId: node_id, + EventUUID: event_writer_id, + content: '', + status: 'end', + }, + }) + return + } + + const { CallToolID } = res + // tool-xxx-stderr 数据单独结束逻辑 + if (isToolStderrStream(node_id)) { + if (!CallToolID) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `数据(NodeId: ${node_id}), CallToolID 为空`), + ) + return + } + + const toolErrorResult = meta.toolStderrStreamData.get(CallToolID) + if (!toolErrorResult) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `NodeId(${node_id})&CallToolID(${CallToolID}) 数据没有初始化`), + ) + return + } + + const toolResult = rawData.contents.get(CallToolID) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + // 工具执行结果卡片UI没有展示时 + toolErrorResult.status = 'end' + } else { + const showUI = store.getState().items[toolResult.id] + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolResult.data.tool.execError = toolErrorResult.content + if (showUI) store.getState().incrementNodeVersion(toolResult.id, 'item') + meta.toolStderrStreamData.delete(CallToolID) + } + return + } + // tool-xxx-stdout 数据单独结束逻辑 + if (isToolStdoutStream(node_id)) { + if (!CallToolID) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `(NodeId: ${node_id})的数据, CallToolID 为空`), + ) + return + } + + const toolResult = rawData.contents.get(res.CallToolID) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { + return + } + const toolForStreamData = rawData.contents.get(toolResult.data.stream.EventUUID) + if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { + return + } + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolForStreamData.data.status = 'end' + const isShowAll = toolForStreamData.data.content.length > 25600 // 50KB大概字符数25600 + const displayContent = isShowAll + ? '...' + toolForStreamData.data.content.slice(-25600) + '...' + : toolForStreamData.data.content + toolResult.data.tool.toolStdoutContent = { content: displayContent, isShowAll } + store.getState().incrementNodeVersion(toolResult.id, 'item') + return + } + + // 数据集合中对应的数据 + const streamData = rawData.contents.get(event_writer_id) + // 数据不存在 不输出到日志,因为日志的流数据也有该类型数据 + if (!streamData || streamData.type !== AIChatQSDataTypeEnum.STREAM) return + + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + streamData.data.status = 'end' + store.getState().incrementNodeVersion(streamData.id, 'item') +} + +const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + if (res.Type !== 'reference_material') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ReferenceMaterialPayload + + const chatData = rawData.contents.get(data.event_uuid) + const toolResult = rawData.contents.get(res.CallToolID || '') + if (chatData) { + // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + chatData.reference = (chatData.reference || []).concat([data]) + if (chatData.parentGroupKey) { + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } else if (chatData.type === AIChatQSDataTypeEnum.STREAM) { + if (toolResult && isToolStdoutStream(chatData.data.NodeId)) { + // 特殊情况,更新stdout流对应的工具执行结果卡片UI + store.getState().incrementNodeVersion(toolResult.id, 'item') + } else { + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } + return + } else { + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } + } else if ( + toolResult && + toolResult.type === AIChatQSDataTypeEnum.TOOL_RESULT && + toolResult.data.stream.EventUUID === data.event_uuid + ) { + toolResult.reference = (toolResult.reference || []).concat([data]) + store.getState().incrementNodeVersion(toolResult.id, 'item') + } else { + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + id: data.event_uuid, + chatType: chatType, + type: AIChatQSDataTypeEnum.Reference_Material, + data: { + NodeId: res.NodeId, + NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), + }, + reference: [data], + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } +} +// #endregion + +/** stream数据相关处理逻辑集合 */ +export const aiStreamDataHandlers = { + stream_start: handleStreamStart, + stream: handleStream, + 'stream-finished': handleStreamFinished, + reference_material: handleReferenceMaterial, +} as const + +const exampleHandle = (res: AIOutputEvent) => { + let funcKey = res.Type + if (res.Type === 'structured' && ['stream-finished'].includes(res.NodeId)) { + // stream数据结束标识 + funcKey = res.NodeId + } +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts new file mode 100644 index 0000000000..85330282fd --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts @@ -0,0 +1,213 @@ +import type { AIMessageHandler } from '../type' +import type { AIAgentGrpcApi, AIOutputEvent } from '../grpcApi' +import { Uint8ArrayToString } from '@/utils/str' +import { handleTodoListData } from '../utils' +import cloneDeep from 'lodash/cloneDeep' +import isArray from 'lodash/isArray' +import isEmpty from 'lodash/isEmpty' +import { v4 as uuidv4 } from 'uuid' + +const handleCapabilityInventory: AIMessageHandler = (requestInfo) => { + const { res, chatType, rawData } = requestInfo + if (res.Type !== 'structured' || res.NodeId !== 'capability_inventory') return + if (!res.TaskId) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const payload = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanItemDetails + if (isEmpty(payload)) return + + const { fixed, dynamic } = payload + const itemData: Pick = { + uuid: uuidv4(), + tool: { + fixed: [], + dynamic: [], + }, + forges: { + fixed: [], + dynamic: [], + }, + skills: { + fixed: [], + dynamic: [], + }, + plugins: { + fixed: [], + dynamic: [], + }, + mcp: { + fixed: [], + dynamic: [], + }, + } + if (!!fixed?.tools) { + for (const item of fixed.tools) { + switch (item.category) { + case 'tool': + itemData.tool.fixed.push(item) + break + case 'yak_plugin': + itemData.plugins.fixed.push(item) + break + case 'mcp': + itemData.mcp.fixed.push(item) + break + default: + break + } + } + } + /** 暂时目前没有这个数据 */ + // if (!!fixed?.mcp_servers) { + // itemData.mcpServices.fixed = fixed.mcp_servers + // } + if (!!fixed?.forges) { + itemData.forges.fixed = fixed.forges + } + if (!!fixed?.skills) { + itemData.skills.fixed = fixed.skills + } + + if (!!dynamic?.tools) { + for (const item of dynamic.tools) { + switch (item.category) { + case 'tool': + itemData.tool.dynamic.push(item) + break + case 'yak_plugin': + itemData.plugins.dynamic.push(item) + break + case 'mcp': + itemData.mcp.dynamic.push(item) + break + default: + break + } + } + } + if (!!dynamic?.skills) { + itemData.skills.dynamic = dynamic.skills + } + if (!!dynamic?.forges) { + itemData.forges.dynamic = dynamic.forges + } + + if (chatType === 'task') { + // 只能通过字段重新赋值的方式修改,不能解构赋值 + const oldData = rawData.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + oldData.uuid = itemData.uuid + oldData.taskId = oldData?.taskId || res.TaskId + oldData.tool = itemData.tool + oldData.forges = itemData.forges + oldData.skills = itemData.skills + oldData.plugins = itemData.plugins + oldData.mcp = itemData.mcp + rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) + } else if (chatType === 'reAct') { + // 只能通过字段重新赋值的方式修改,不能解构赋值 + const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) + chatDetail.uuid = itemData.uuid + chatDetail.taskId = chatDetail?.taskId || res.TaskId + chatDetail.tool = itemData.tool + chatDetail.forges = itemData.forges + chatDetail.skills = itemData.skills + chatDetail.plugins = itemData.plugins + chatDetail.mcp = itemData.mcp + rawData.casualChat.planDetails = chatDetail + } +} + +const handlePerception: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo + if (res.Type !== 'perception' || res.NodeId !== 'perception') return + if (!res.TaskId) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const perception = (JSON.parse(ipcContent) as AIAgentGrpcApi.PerceptionData) || {} + if (isEmpty(perception)) return + + perception.summary = isArray(perception.summary) ? perception.summary.join(',') : perception.summary + if (chatType === 'task') { + const oldData = rawData.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + oldData.taskId = oldData?.taskId || res.TaskId + oldData.uuid = uuidv4() + oldData.perception = perception + rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) + } else if (chatType === 'reAct') { + const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) + chatDetail.uuid = uuidv4() + chatDetail.taskId = chatDetail.taskId || res.TaskId + chatDetail.perception = perception + rawData.casualChat.planDetails = chatDetail + } +} + +const handleCurrentTaskTodoListUpdate: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo + if (res.Type !== 'current_task_todo_list_update' || res.NodeId !== 'current_task_todo_list') return + if (!res.TaskId) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + // 更新待办清单卡片数据 + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.TodoListUpdate + if (isEmpty(data)) return + + const newData = handleTodoListData(data.items, data.task_id, data.task_index) + if (chatType === 'task') { + const oldData = rawData.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + oldData.uuid = uuidv4() + oldData.taskId = oldData.taskId || res.TaskId + oldData.todoList = newData + rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) + } else if (chatType === 'reAct') { + const chatDetail = rawData.casualChat?.planDetails + if (!chatDetail) return + chatDetail.taskId = chatDetail.taskId || res.TaskId + chatDetail.todoList = newData + store.getState().updateCasualTodoList() + } +} + +const handleSessionSnapshot: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo + if (res.NodeId !== 'session_snapshot') return + if (!res.TaskId) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const snapshot = (JSON.parse(ipcContent) as AIAgentGrpcApi.SessionSnapshot) || {} + if (isEmpty(snapshot)) return + if (chatType === 'task') { + const oldData = rawData.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + oldData.taskId = oldData?.taskId || res.TaskId + oldData.uuid = uuidv4() + oldData.execution = snapshot.execution + rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) + } else if (chatType === 'reAct') { + const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) + chatDetail.uuid = uuidv4() + chatDetail.taskId = chatDetail.taskId || res.TaskId + chatDetail.execution = snapshot.execution + rawData.casualChat.planDetails = chatDetail + } +} + +/** 单条grpc流数据对应一个独立的UI数据 */ +export const aiTaskDetailDataHandlers = { + capability_inventory: handleCapabilityInventory, + perception: handlePerception, + current_task_todo_list_update: handleCurrentTaskTodoListUpdate, + session_snapshot: handleSessionSnapshot, +} as const + +const exampleHandle = (res: AIOutputEvent) => { + let funcKey = res.Type + if (res.Type === 'structured' && res.NodeId === 'capability_inventory') { + funcKey = res.NodeId + } else if (res.Type === 'perception' && res.NodeId === 'perception') { + funcKey = res.Type + } else if (res.Type === 'current_task_todo_list_update' && res.NodeId === 'current_task_todo_list') { + funcKey = res.Type + } else if (res.NodeId === 'session_snapshot') { + funcKey = res.NodeId + } +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts new file mode 100644 index 0000000000..b7ea85a843 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -0,0 +1,276 @@ +import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import type { AIAgentGrpcApi } from '../grpcApi' +import { Uint8ArrayToString } from '@/utils/str' +import { genBaseAIChatData, generateTaskNodeDataID, genErrorLogData } from '../utils' +import { AIChatQSDataTypeEnum, type AIToolResult } from '../aiRender' +import cloneDeep from 'lodash/cloneDeep' +import { DefaultAIToolResult, DefaultToolResultSummary } from '../defaultConstant' + +/** grpc流数据转换成错误信息输出到日志中 */ +const handleErrorGRPCToLog: ( + /** 该条grpc流数据是历史数据 */ + isHistory: AIMessageHandlerParams['res']['IsSync'], + pushLog: AIMessageHandlerParams['pushLog'], + error: ReturnType, +) => void = (isHistory, pushLog, error) => { + if (isHistory) return + pushLog(error) +} + +const handleToolCallStart: AIMessageHandler = (requestInfo) => { + const { res, chatType, rawData, meta, pushLog } = requestInfo + if (res.Type !== 'tool_call_start') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { call_tool_id, tool, start_time, start_time_ms } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall + if (!call_tool_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + return + } + + const toolResult: AIToolResult = { + ...cloneDeep(DefaultAIToolResult), + callToolId: call_tool_id, + toolName: tool?.name || '-', + toolDescription: tool?.description || '', + startTime: start_time || 0, + startTimeMS: start_time_ms || 0, + } + + rawData.contents.set(call_tool_id, { + ...genBaseAIChatData(res), + id: call_tool_id, + chatType: chatType, + type: AIChatQSDataTypeEnum.TOOL_RESULT, + data: toolResult, + taskIndex: generateTaskNodeDataID({ + chatType, + planID: meta.currentTaskPlanID?.taskID, + taskID: res.TaskIndex, + isExist: (key) => rawData.contents.has(key), + }), + }) +} + +const handleToolCallParam: AIMessageHandler = (requestInfo) => { + const { res, store, rawData, pushLog } = requestInfo + if (res.Type !== 'tool_call_param') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { call_tool_id, params } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallParams + if (!call_tool_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + return + } + + const toolResult = rawData.contents.get(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData( + res.Timestamp, + `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + ), + ) + return + } + + toolResult.data.tool.reviewParams = cloneDeep(params) + if (toolResult.data.type === 'result') store.getState().incrementNodeVersion(toolResult.id, 'item') +} + +const handleToolCallWatcher: AIMessageHandler = (requestInfo) => { + const { res, store, rawData, pushLog } = requestInfo + if (res.Type !== 'tool_call_watcher') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { call_tool_id, id, selectors } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallWatcher + + if (!call_tool_id || !id || !selectors || !selectors?.length) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + return + } + + // 先获取工具结果数据,从里面拿到stream的EventUUID + const toolResult = rawData.contents.get(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData( + res.Timestamp, + `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + ), + ) + return + } + // 通过上面获取到的EventUUID,获取stream数据 + const toolForStreamData = rawData.contents.get(toolResult.data.stream.EventUUID) + if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `EventUUID: ${toolResult.data.stream.EventUUID} 的stream数据没有对应的初始化`), + ) + return + } + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolForStreamData.data.selectors = { + callToolId: call_tool_id, + InteractiveId: id, + selectors: selectors, + } + + if (toolResult.data.type === 'stream') { + // 历史数据-该类型不出发渲染更新 + if (res.IsSync) return + store.getState().incrementNodeVersion(toolResult.id, 'item') + } +} + +const handleToolCallLogDir: AIMessageHandler = (requestInfo) => { + const { res, store, rawData, pushLog } = requestInfo + if (res.Type !== 'tool_call_log_dir') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { call_tool_id, dir_path } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallDirPath + if (!call_tool_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + return + } + + const toolResult = rawData.contents.get(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData( + res.Timestamp, + `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + ), + ) + return + } + if (toolResult.data.tool.dirPath) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据(call_tool_id:${call_tool_id}), dir_path已存在,不能重复设置`), + ) + return + } + + // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + toolResult.data.tool.dirPath = dir_path || '' + if (toolResult.data.tool.status !== 'default') store.getState().incrementNodeVersion(toolResult.id, 'item') +} + +const handleToolCallResult: AIMessageHandler = (requestInfo) => { + const { res, store, rawData, meta, pushLog } = requestInfo + if (!['tool_call_user_cancel', 'tool_call_done', 'tool_call_error'].includes(res.Type)) return + const status = + { tool_call_user_cancel: 'user_cancelled', tool_call_done: 'success', tool_call_error: 'failed' }[res.Type] || + 'default' + if (status === 'default') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { call_tool_id, ...rest } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall + + if (!call_tool_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + return + } + + const toolResult = rawData.contents.get(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData( + res.Timestamp, + `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + ), + ) + return + } + + // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + // 设置工具执行的开始时间、结束时间和持续时间等数据 + toolResult.data.type = 'result' + toolResult.data.startTime = rest.start_time || 0 + toolResult.data.startTimeMS = rest.start_time_ms || 0 + toolResult.data.endTime = rest.end_time || 0 + toolResult.data.endTimeMS = rest.end_time_ms || 0 + toolResult.data.durationMS = rest.duration_ms || 0 + toolResult.data.durationSeconds = rest.duration_seconds || 0 + toolResult.data.tool.status = status as 'user_cancelled' | 'success' | 'failed' | 'default' + + // 设置总结内容,没有就设置成获取中,有就使用获取到的内容 + toolResult.data.tool.summary = toolResult.data.tool.summary || DefaultToolResultSummary[status]?.wait || '' + // 设置执行结果错误数据内容(std_xxx_stderr) + const errorResult = meta.toolStderrStreamData.get(call_tool_id) + if (errorResult && errorResult.status === 'end') { + toolResult.data.tool.execError = errorResult.content + // error数据先出但未存在对应的工具执行结果,工具结果出现后直接使用并删除map中的缓存数据 + meta.toolStderrStreamData.delete(call_tool_id) + } + + store.getState().incrementNodeVersion(toolResult.id, 'item') +} + +const handleToolCallSummary: AIMessageHandler = (requestInfo) => { + const { res, store, rawData, meta, pushLog } = requestInfo + if (res.Type !== 'tool_call_summary') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { call_tool_id, summary } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall + + if (!call_tool_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + return + } + + const toolResult = rawData.contents.get(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData( + res.Timestamp, + `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + ), + ) + return + } + + const statusInfo = toolResult.data.tool.status + const summaryContent = !summary || summary === 'null' ? '' : summary + // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 + // 设置总结内容,没有就设置成默认的状态展示内容,有就使用获取到的内容 + toolResult.data.tool.summary = + statusInfo === 'user_cancelled' + ? '当前工具调用已被取消,会使用当前输出结果进行后续工作决策' + : summaryContent || DefaultToolResultSummary[toolResult.data.tool.status]?.result || '' + // 设置执行结果错误数据内容(std_xxx_stderr) + const errorResult = meta.toolStderrStreamData.get(call_tool_id) + if (errorResult && errorResult.status === 'end') { + toolResult.data.tool.execError = errorResult.content + // error数据先出但未存在对应的工具执行结果,工具结果出现后直接使用并删除map中的缓存数据 + meta.toolStderrStreamData.delete(call_tool_id) + } + if (statusInfo !== 'default') { + store.getState().incrementNodeVersion(toolResult.id, 'item') + } +} + +export const aiToolResultDataHandlers = { + tool_call_start: handleToolCallStart, + tool_call_param: handleToolCallParam, + tool_call_watcher: handleToolCallWatcher, + tool_call_log_dir: handleToolCallLogDir, + tool_call_user_cancel: handleToolCallResult, + tool_call_done: handleToolCallResult, + tool_call_error: handleToolCallResult, + tool_call_summary: handleToolCallSummary, +} as const diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/grpcAIOutputEventHandlers.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/grpcAIOutputEventHandlers.ts new file mode 100644 index 0000000000..dcaee10006 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/grpcAIOutputEventHandlers.ts @@ -0,0 +1,25 @@ +import type { AIMessageHandler } from '../type' +import { aiPerfDataHandlers } from './aiPerf' +import { aiOtherDataHandlers } from './aiOther' +import { aiReviewDataHandlers } from './aiReview' +import { aiToolResultDataHandlers } from './aiToolResult' +import { aiSingleItemDataHandlers } from './aiSingleItem' +import { aiStreamDataHandlers } from './aiStream' +import { aiYakExecResultDataHandlers } from './yakExecResult' +import { aiTaskDetailDataHandlers } from './aiTaskDetail' + +/** + * grpc流数据的各种类型处理逻辑集合 + * 该逻辑集合里的方法处理,没有使用try-catch拦截,因为在hook层进行了同一try-catch拦截 + * 注意!别的地方单独使用时,请自行加入try-catch拦截错误 + */ +export const grpcAIMessageHandlers: Record = { + ...aiPerfDataHandlers, + ...aiOtherDataHandlers, + ...aiReviewDataHandlers, + ...aiToolResultDataHandlers, + ...aiSingleItemDataHandlers, + ...aiStreamDataHandlers, + ...aiYakExecResultDataHandlers, + ...aiTaskDetailDataHandlers, +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts new file mode 100644 index 0000000000..e4c2892d5d --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts @@ -0,0 +1,115 @@ +import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import type { AIAgentGrpcApi, AIOutputEvent } from '../grpcApi' +import { Uint8ArrayToString } from '@/utils/str' +import { checkStreamValidity, convertCardInfo } from '@/hook/useHoldGRPCStream/useHoldGRPCStream' +import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' + +const handleStatus: AIMessageHandler = (request) => { + const { res, chatType, store, meta } = request + if (res.Type !== 'structured' || res.NodeId !== 'status') return + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as { key: string; value: string } + if (data.key === 're-act-loading-status-key') { + if (chatType === 'task') { + // 任务规划-loading展示标题 + store.getState().updateTaskLoadingStatus({ task: data.value || '加载中...' }) + } else { + // 自由对话-loading展示标题 + store.getState().updateState({ casualTitle: data.value }) + } + } else if (data.key === 'plan-executing-loading-status-key') { + if (chatType === 'task') { + // 任务规划-loading展示标题 + store.getState().updateTaskLoadingStatus({ plan: data.value || '加载中...' }) + } + } else { + const originData = meta.cardKVPair.get(data.key) + if (originData && originData.Timestamp > res.Timestamp) return + + meta.cardKVPair.set(data.key, { + Id: data.key, + Data: data.value, + Timestamp: res.Timestamp, + Tags: [], + }) + + if (meta.cardKVPaidTimer) return + meta.cardKVPaidTimer = setTimeout(() => { + const cacheCard: AIAgentGrpcApi.AIInfoCard[] = convertCardInfo(meta.cardKVPair) + store.getState().updateState({ card: cacheCard }) + meta.cardKVPaidTimer = null + }, 500) + } +} + +const handleCard: (value: AIAgentGrpcApi.AICardMessage, requestInfo: AIMessageHandlerParams) => void = ( + value, + requestInfo, +) => { + const { store, meta } = requestInfo + + const logData = value.content as StreamResult.Log + const checkInfo: AIAgentGrpcApi.AICard = checkStreamValidity(value.content as StreamResult.Log) + if (!checkInfo) return + const { id, data, tags } = checkInfo + const { timestamp } = logData + const originData = meta.cardKVPair.get(id) + if (originData && originData.Timestamp > timestamp) { + return + } + meta.cardKVPair.set(id, { + Id: id, + Data: data, + Timestamp: timestamp, + Tags: Array.isArray(tags) ? tags : [], + }) + + if (meta.cardKVPaidTimer) return + meta.cardKVPaidTimer = setTimeout(() => { + const cacheCard: AIAgentGrpcApi.AIInfoCard[] = convertCardInfo(meta.cardKVPair) + store.getState().updateState({ card: cacheCard }) + meta.cardKVPaidTimer = null + }, 500) +} + +const handleYakExecResult: AIMessageHandler = (requestInfo) => { + const { res, store, meta } = requestInfo + if (res.Type !== 'yak_exec_result') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIPluginExecResult + + if (!data?.IsMessage) return + const message = data?.Message || '' + const obj: AIAgentGrpcApi.AICardMessage = JSON.parse(Buffer.from(message, 'base64').toString('utf8')) + + if (obj.type !== 'log') return + const content = obj.content as StreamResult.Log + switch (content.level) { + case 'feature-status-card-data': + handleCard(obj, requestInfo) + break + case 'file': + meta.execFileRecordOrder += 1 + store.getState().updateExecFileRecord(res.CallToolID, content, meta.execFileRecordOrder) + break + default: + break + } +} + +/** 插件执行过程的卡片数据和文件操作记录处理逻辑 */ +export const aiYakExecResultDataHandlers = { + status: handleStatus, + yak_exec_result: handleYakExecResult, +} as const + +const exampleHandle = (res: AIOutputEvent) => { + let funcKey = res.Type + if (res.Type === 'structured' && ['status'].includes(res.NodeId)) { + // stream数据结束标识 + funcKey = res.NodeId + } +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts index 24ab97f588..d8f2a1e393 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts @@ -2,27 +2,20 @@ import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamTyp import type { AIChatQSData, AIChatQSDataType, + AIInputNotifyMessage, AIStreamOutput, AIYakExecFileRecord, - ReActChatBaseInfo, + ChatListRenderType, ReActChatRenderItem, - TodoListCardData, } from './aiRender' import type { Dispatch, SetStateAction } from 'react' import type { Domain } from '@/pages/ai-agent/store/constants' -import type { - AIAgentGrpcApi, - AIInputEvent, - AIOutputEvent, - AIOutputI18n, - AISource, - AIStartParams, - AITaskStatusType, -} from './grpcApi' +import type { AIAgentGrpcApi, AIInputEvent, AIOutputEvent, AISource, AIStartParams, AITaskStatusType } from './grpcApi' import type { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' import type { CustomPluginExecuteFormValue } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType' -import type { AIChatData } from '@/pages/ai-agent/type/aiChat' +import type { AIAgentChatData, AIAgentChatMetaData, AIChatData } from '@/pages/ai-agent/type/aiChat' import type { ChatDataStore } from '@/pages/ai-agent/store/ChatDataStore' +import { createChatStore } from './chatStore' // #region 公共 hooks 事件 export interface UseHookBaseParams { @@ -56,14 +49,6 @@ export interface UpdateRenderDataParams { } // #endregion -// #region useAIPerfData相关定义 -export interface UseAIPerfDataParams extends UseHookBaseParams {} - -export interface UseAIPerfDataEvents { - handleSetData: (res: AIOutputEvent) => void -} -// #endregion - // #region useYakExecResult相关定义 export interface UseYakExecResultParams extends UseHookBaseParams {} @@ -226,8 +211,6 @@ export interface UseChatIPCState { httpRunTimeIDs: string[] /** 运行时产生risk数据的run_time_id合集 */ riskRunTimeIDs: string[] - /** 插件输出的卡片数据 */ - yakExecResult: UseYakExecResultState /** 自由对话相关数据 */ casualChat: UseCasualChatState /** 任务规划相关数据 */ @@ -259,7 +242,7 @@ export interface UseChatIPCState { /** 用户主动取消问题的loading状态(任务规划) */ cancelTaskLoading: boolean /** 流推送的提示文案(notify / rate-limit),展示时长由 duration 系列字段控制,到期自动清空 */ - notifyMessage: { type: 'notify' | 'rate-limit'; content: string; label: AIOutputI18n } | null + notifyMessage: AIInputNotifyMessage | null /** 请求历史数据相关State */ requestHistoryState: UseAIMessageDataState } @@ -319,7 +302,7 @@ export interface UseChatIPCEvents { /** 删除会话操作的关联逻辑 */ onDelChats: (session: string[]) => void /** 用户主动取消问题的loading状态变换 */ - handleCancelLoadingChange: (type: ReActChatBaseInfo['chatType'], status: boolean) => void + handleCancelLoadingChange: (type: ChatListRenderType, status: boolean) => void /** 清空指定变量数据 */ handleResetTarget: (target: 'memoryList') => void /** 用户手动干预的执行事件 */ @@ -358,42 +341,17 @@ export interface UseAIChatLogEvents { // #endregion // #region AI-Agent相关grpc流数据处理逻辑 -export interface AIMessageHandlerParams extends UseHookStateFunc { +export interface AIMessageHandlerParams { /** grpc流原始数据 */ res: AIOutputEvent - /** 处理数据的额外补充数据 */ - info: { - chatType: ReActChatRenderItem['chatType'] - } - /** 获取当前任务规划的问题ID信息 */ - getCurrentTaskPlanID?: () => TaskChatTaskInfo | undefined - /** 获取流接口请求参数 */ - getRequest: () => AIAgentSetting | undefined + chatType: ChatListRenderType + store: ReturnType + rawData: AIAgentChatData + request: AIStartParams + meta: AIAgentChatMetaData /** 将数据推送到日志集合中 */ - pushLog: UseHookBaseParams['pushLog'] - /** review 类型相关变量和方法 */ - review?: { - /** 设置review数据 */ - handleSetReview?: (newReview: AIChatQSData | undefined) => void - /** 获取review数据 */ - handleGetReview?: () => AIChatQSData | undefined - /** review 触发回调事件 */ - onReview?: UseTaskChatParams['onReview'] - /** plan_review 补充数据 */ - onReviewExtra?: UseTaskChatParams['onReviewExtra'] - /** 触发 review-release 后的回调事件 */ - onReviewRelease?: (id: string) => void - /** 将 review 数据处理成需要展示的UI数据 */ - handleReviewDataToUI?: (reviewInfo: AIChatQSData) => void - /** 向进行中的 grpc 流发送交互消息 */ - sendRequest?: UseTaskChatParams['sendRequest'] - } - /** store数据 */ - getChatDataStore: UseHookBaseParams['getChatDataStore'] - /** 回调方法 */ - callback?: (data: AIOutputEvent) => void - /** 当前自由对话父任务 ID(任务规划请用 getCurrentTaskPlanID) */ - getTaskId?: () => string + pushLog: (log: AIChatLogData) => void + sendRequest: (request: AIInputEvent) => void } export type AIMessageHandler = (params: AIMessageHandlerParams) => void // #endregion @@ -404,10 +362,7 @@ export type loadMoreType = keyof AIChatData['beforeID'] export interface AIMessageDataProps { type: Domain getChatStore: UseHookBaseParams['getChatDataStore'] - setContentMap: ( - chatType: ReActChatBaseInfo['chatType'], - ...args: Parameters - ) => void + setContentMap: (chatType: ChatListRenderType, ...args: Parameters) => void setCasualElements: UseHookStateFunc['setElements'] setTaskElements: UseHookStateFunc['setElements'] grpcLoadMore?: (request: { limit: number; start_id?: number }) => void @@ -433,7 +388,7 @@ export interface UseAIMessageDataState { timelinesLoading: boolean } -export type HistoryChatType = ReActChatBaseInfo['chatType'] | 'timelines' +export type HistoryChatType = ChatListRenderType | 'timelines' export interface UseAIMessageDataEvents { /** 给UI使用的hasMore获取方法 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useAIPerfData.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useAIPerfData.ts deleted file mode 100644 index d4bc5fa4ef..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useAIPerfData.ts +++ /dev/null @@ -1,255 +0,0 @@ -import { useCreation, useMemoizedFn } from 'ahooks' -import { Uint8ArrayToString } from '@/utils/str' -import type { AIContextStatsDetail } from '@/pages/ai-agent/type/aiChat' -import { AIChatLogData, UseAIPerfDataEvents, UseAIPerfDataParams } from './type' -import { handleGrpcDataPushLog } from './utils' -import { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' - -const CONTEXT_STATS_SERIES_MAX = 50 - -/** - * 首次建立 role_order 时,若存在这些 role_name 则按此顺序排在前面。 - * - * P1.1 之后 aireact 已经把老的 semi_dynamic 段彻底拆成 semi_dynamic_1 - * (Skills Context + CacheToolCall) 与 semi_dynamic_2 (Persistent + - * Schema + OutputExample) 两个独立 role;老的 semi_dynamic 已被移除, - * 不再保留兜底位。 - * - * 字节统计图 "从下往上" 的堆叠物理顺序固定为: - * high_static -> frozen_block -> semi_dynamic_1 -> semi_dynamic_2 -> - * timelineOpen -> dynamic - */ -const CONTEXT_STATS_ROLE_NAME_ORDER = [ - 'high_static', - 'frozen_block', - 'semi_dynamic_1', - 'semi_dynamic_2', - 'timelineOpen', - 'dynamic', -] as const - -const trimContextStatsSeries = (d: AIContextStatsDetail['data']) => { - if (!Array.isArray(d.total_prompt_bytes)) d.total_prompt_bytes = [] - if (!Array.isArray(d.total_prompt_tokens)) d.total_prompt_tokens = [] - while (d.times.length > CONTEXT_STATS_SERIES_MAX) { - d.times.shift() - d.total_prompt_bytes.shift() - d.total_prompt_tokens.shift() - for (const name of d.role_order) { - d.role_series[name]?.shift() - d.role_tokens[name]?.shift() - } - } -} - -/** 递归上下文成分里的summary并归类到map对象后消除summary字段内容 */ -const handleSummarySectionsSummary = ( - sections: AIAgentGrpcApi.AIContextSections[], - summaryMap: Map, -) => { - for (let item of sections) { - summaryMap.set(item.key, item.summary || '') - delete item.summary - if (item.children) handleSummarySectionsSummary(item.children, summaryMap) - } -} - -// 属于该 hook 处理数据的类型 -export const UseAIPerfDataTypes = [ - 'consumption', - 'pressure', - 'ai_first_byte_cost_ms', - 'ai_total_cost_ms', - 'prompt_profile', -] - -function useAIPerfData(params?: UseAIPerfDataParams): UseAIPerfDataEvents - -/** 提供 AI 硬件相关性能数据 */ -function useAIPerfData(params?: UseAIPerfDataParams) { - const { pushLog, getChatDataStore } = params || {} - - const handlePushLog = useMemoizedFn((log: AIChatLogData) => { - pushLog?.(log) - }) - - const handleSetData = useMemoizedFn((res: AIOutputEvent) => { - try { - let ipcContent = Uint8ArrayToString(res.Content) || '' - - if (res.Type === 'consumption') { - // 消耗Token - // 因为可能存在多个 ai 并发输出,所以这里的 token 量是一个集合 - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Consumption - const consumption = getChatDataStore?.()?.aiPerfData?.consumption - if (consumption) { - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - consumption.input_consumption = data.input_consumption - consumption.output_consumption = data.output_consumption - consumption.cache_hit_token = data.cache_hit_token - consumption.tier_consumption = { ...data.tier_consumption } - } - return - } - - if (res.Type === 'pressure') { - // 上下文压力 - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Pressure - const pressure = getChatDataStore?.()?.aiPerfData?.pressure - if (pressure) { - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - const target = pressure[data.model_tier] - if (!target) { - pressure[data.model_tier] = [{ ...data, timestamp: Number(res.Timestamp) || 0 }] - } else { - target.push({ ...data, timestamp: Number(res.Timestamp) || 0 }) - // if (target.length > 100) target.shift() - } - } - return - } - - if (res.Type === 'ai_first_byte_cost_ms') { - // 首字符响应耗时 - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIFirstCostMS - const firstCost = getChatDataStore?.()?.aiPerfData?.firstCost - if (firstCost) { - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - const target = firstCost[data.model_tier] - if (!target) { - firstCost[data.model_tier] = [{ ...data, timestamp: Number(res.Timestamp) || 0 }] - } else { - target.push({ ...data, timestamp: Number(res.Timestamp) || 0 }) - // if (target.length > 100) target.shift() - } - } - return - } - - if (res.Type === 'ai_total_cost_ms') { - // 总对话耗时 - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AITotalCostMS - const totalCost = getChatDataStore?.()?.aiPerfData?.totalCost - if (totalCost) { - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - const target = totalCost[data.model_tier] - if (!target) { - totalCost[data.model_tier] = [{ ...data, timestamp: Number(res.Timestamp) || 0 }] - } else { - target.push({ ...data, timestamp: Number(res.Timestamp) || 0 }) - // if (target.length > 100) target.shift() - } - } - return - } - - if (res.Type === 'prompt_profile') { - // 上下文字节统计 & 上下文成分(源头:AI 流式输出事件 prompt_profile,经 useChatIPC → handleSetData) - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ContextStatsSections - const stats = getChatDataStore?.()?.aiPerfData?.contextStats - if (stats) { - const d = stats.data - const incomingRoles = Array.isArray(data.role_stats) ? data.role_stats : [] - const ts = Number(res.Timestamp) || 0 - - if (incomingRoles.length > 0) { - // 每个 turn 后端都会按 promptSectionRolesInOrder 推全集 6 个 role - // (P1.1: high_static / frozen_block / semi_dynamic_1 / semi_dynamic_2 / - // timelineOpen / dynamic), 即便 bytes 为 0 也会预填. 所以这里每次 - // 都用 incoming 重新 reconcile role_order, 既能让新出现的 role - // (如 semi_dynamic_1/2) 按 CONTEXT_STATS_ROLE_NAME_ORDER 物理顺序 - // 插入, 也能把 incoming 里彻底消失的老 role (如老的 'semi_dynamic') - // 自动 evict 掉, 避免历史会话残留的图例顺序污染. - const incomingMap = new Map() - for (const r of incomingRoles) { - if (!r.role_name || incomingMap.has(r.role_name)) continue - incomingMap.set(r.role_name, r) - } - - const preferred = new Set(CONTEXT_STATS_ROLE_NAME_ORDER) - const desiredOrder: string[] = [] - for (const name of CONTEXT_STATS_ROLE_NAME_ORDER) { - if (incomingMap.has(name)) desiredOrder.push(name) - } - for (const name of incomingMap.keys()) { - if (!preferred.has(name) && !desiredOrder.includes(name)) { - desiredOrder.push(name) - } - } - - const oldOrderSet = new Set(d.role_order) - const newOrderSet = new Set(desiredOrder) - - // 新出现的 role: 用 0 补齐已积累的 d.times 长度, 让历史时间轴上 - // 这条线从 0 起步, 而不是凭空错位 - for (const name of desiredOrder) { - const r = incomingMap.get(name) - if (!oldOrderSet.has(name)) { - d.role_labels[name] = r?.role_name_zh || name - d.role_series[name] = new Array(d.times.length).fill(0) - } else if (r?.role_name_zh) { - d.role_labels[name] = r.role_name_zh - } - } - // 老 role (如 'semi_dynamic') 在 incoming 中彻底消失 -> 释放 series - for (const name of d.role_order) { - if (!newOrderSet.has(name)) { - delete d.role_labels[name] - delete d.role_series[name] - } - } - - d.role_order = desiredOrder - } - - stats.prompt_bytes = data.prompt_bytes ?? 0 - stats.prompt_tokens = data.prompt_tokens ?? 0 - d.times.push(ts) - d.total_prompt_bytes.push(data.prompt_bytes ?? 0) - d.total_prompt_tokens.push(data.prompt_tokens ?? 0) - - if (d.role_order.length > 0) { - const map = new Map() - const tokenMap = new Map() - for (const r of incomingRoles) { - if (!d.role_order.includes(r.role_name)) continue - map.set(r.role_name, r.role_bytes ?? 0) - tokenMap.set(r.role_name, r.role_tokens ?? 0) - } - for (const name of d.role_order) { - if (!d.role_series[name]) d.role_series[name] = [] - d.role_series[name].push(map.get(name) ?? 0) - } - for (const name of d.role_order) { - if (!d.role_tokens[name]) d.role_tokens[name] = [] - d.role_tokens[name].push(tokenMap.get(name) ?? 0) - } - } - - trimContextStatsSeries(d) - } - - const sections = getChatDataStore?.()?.aiPerfData?.contextSections - const sectionsData = data.sections || [] - if (sections) { - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - // 每次新的后端数据进来,需要重置上次数据逻辑处理的map对象,避免之前数据对当前数据处理造成影响 - const summaryMap = new Map() - handleSummarySectionsSummary(sectionsData, summaryMap) - sections.summary = summaryMap - sections.sections = sectionsData - } - } - } catch (error) { - handleGrpcDataPushLog({ info: res, pushLog: handlePushLog }) - } - }) - - const events: UseAIPerfDataEvents = useCreation(() => { - return { handleSetData } - }, []) - - return events -} - -export default useAIPerfData diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts index ebb364f82f..a183c7d552 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts @@ -5,18 +5,13 @@ import type { UseCasualChatParams, UseCasualChatState, } from './type' -import type { AIChatQSData, AIReviewType, ReActChatRenderItem } from './aiRender' -import type { AIAgentGrpcApi, AIOutputEvent, AITaskStatusType } from './grpcApi' +import type { AIChatQSData, ReActChatRenderItem } from './aiRender' +import type { AIOutputEvent } from './grpcApi' import { useRef, useState } from 'react' import { useCreation, useMemoizedFn } from 'ahooks' -import { Uint8ArrayToString } from '@/utils/str' import cloneDeep from 'lodash/cloneDeep' -import { genBaseAIChatData, handleGrpcDataPushLog } from './utils' -import { AIChatQSDataTypeEnum } from './aiRender' import { yakitNotify } from '@/utils/notification' import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' -import { grpcAIMessageHandlers } from './grpcAIMessageHandlers' -import { DefaultTodoListCardData, DefaultPlanItemDetailsData } from './defaultConstant' function useCasualChat(params: UseCasualChatParams): [UseCasualChatState, UseCasualChatEvents] @@ -54,194 +49,16 @@ function useCasualChat(params: UseCasualChatParams) { contentMap && contentMap.set(mapKey, value) }) - /** 子 agent 任务创建时生成聚合卡片 */ - const handleReactTaskCreated = useMemoizedFn((res: AIOutputEvent, info: AIAgentGrpcApi.CasualCreated) => { - if (!info.react_task_is_sub_agent) return - - const ownTaskId = info.react_task_id - const taskId = getCurrentCasualTaskID?.() - if (!taskId || !ownTaskId) return - const taskKey = `${taskId}-${ownTaskId}` - onSubTaskID?.(ownTaskId) - - const existing = getContentMap(taskKey) - if (existing) return - - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - id: taskKey, - chatType: 'reAct', - type: AIChatQSDataTypeEnum.TASK_NODE_GROUP, - data: { - taskId: ownTaskId, - taskName: info.react_task_name || info.react_user_input || ownTaskId, - goal: info.react_user_input || '', - status: info.react_task_status, - }, - } as AIChatQSData - - setContentMap(chatData.id, chatData) - const chatStore = getChatDataStore?.() - // planDetailsMap / 子任务收集仍按后端子任务 ID(res.TaskId)索引 - if (chatStore && !chatStore.casualChat.planDetailsMap.has(ownTaskId)) { - chatStore.casualChat.planDetailsMap.set(ownTaskId, cloneDeep(DefaultPlanItemDetailsData)) - } - setElements((old) => { - const exists = old.some((item) => item.token === chatData.id && item.type === chatData.type) - if (exists) return old - return [ - ...old, - { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'reAct', kind: 'task', children: [] }, - ] - }) - }) - - /** 子 agent 任务状态变更时更新聚合卡片 */ - const handleReactTaskStatusChanged = useMemoizedFn((res: AIOutputEvent, info: AIAgentGrpcApi.ReactTaskChanged) => { - const taskId = getCurrentCasualTaskID?.() - const ownTaskId = res.TaskId || info.react_task_id - const taskKey = taskId && ownTaskId ? `${taskId}-${ownTaskId}` : '' - if (!taskKey) return - - const existing = getContentMap(taskKey) - if (!existing || existing.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) return - - existing.data.status = info.react_task_now_status as AITaskStatusType - setContentMap(taskKey, existing) - setElements((old) => - old.map((item) => { - if (item.token === taskKey && item.type === existing.type) { - return { ...item, renderNum: item.renderNum + 1 } - } - return item - }), - ) - }) - // #region review数据-hook缓存数据 const review = useRef() - const handleGetReview = useMemoizedFn(() => { - return review.current - }) - const handleSetReview = useMemoizedFn((newReview: AIChatQSData | undefined) => { - review.current = cloneDeep(newReview) - }) + // #endregion // 处理数据方法 - const handleSetData = useMemoizedFn((res: AIOutputEvent) => { - try { - const ipcContent = Uint8ArrayToString(res.Content) || '' - - if (res.Type === 'structured' && res.NodeId === 'react_task_created') { - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.CasualCreated - handleReactTaskCreated(res, data) - return - } - - if (res.Type === 'structured' && res.NodeId === 'react_task_status_changed') { - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ReactTaskChanged - handleReactTaskStatusChanged(res, data) - return - } - - let funcKey = res.Type - if (res.Type === 'report_finish' && res.NodeId === 'report-finish') { - funcKey = res.NodeId - } - if (res.Type === 'structured' && res.NodeId === 'stream-finished') { - // stream数据结束标识 - funcKey = res.NodeId - } else if (res.Type === 'structured' && res.NodeId === 'react_task_dequeue') { - // 用户问题开始执行标识 - funcKey = res.NodeId - } else if (res.Type === 'api_request_failed' && res.NodeId === 'ai_call_failure') { - funcKey = res.Type - } else if (res.Type === 'structured' && res.NodeId === 'capability_inventory') { - funcKey = res.NodeId - } else if (res.Type === 'perception' && res.NodeId === 'perception') { - funcKey = res.Type - } else if (res.Type === 'current_task_todo_list_update' && res.NodeId === 'current_task_todo_list') { - funcKey = res.Type - } else if (res.NodeId === 'session_snapshot') { - funcKey = res.NodeId - } - const handleFunc = grpcAIMessageHandlers[funcKey || ''] - if (handleFunc) { - handleFunc({ - res, - info: { chatType: 'reAct' }, - getRequest, - setElements, - getElements, - setContentMap, - getContentMap, - pushLog: handlePushLog, - review: { - handleGetReview, - handleSetReview, - onReview, - onReviewRelease, - }, - getTaskId: getCurrentCasualTaskID, - getChatDataStore, - callback: (data) => { - if (data.Type === 'current_task_todo_list_update' && data.NodeId === 'current_task_todo_list') { - setToolListRenderNumber((old) => old + 1) - } - }, - }) - return - } - - // 未识别类型全部归档到日志处理 - handleGrpcDataPushLog({ info: res, pushLog: handlePushLog }) - } catch (error) { - handleGrpcDataPushLog({ - info: res, - pushLog: handlePushLog, - }) - } - }) + const handleSetData = useMemoizedFn((res: AIOutputEvent) => {}) // 用户问题或review的主动操作 - const handleSend: handleSendFunc = useMemoizedFn(({ request, optionValue, cb }) => { - try { - const { IsInteractiveMessage, InteractiveId } = request - if (IsInteractiveMessage && InteractiveId) { - if (!review.current || (review.current.data as AIReviewType)?.id !== InteractiveId) { - yakitNotify('error', '未获取到 review 信息, 操作无效') - return - } - - ;(review.current.data as AIReviewType).selected = request.InteractiveJSONInput - ;(review.current.data as AIReviewType).optionValue = optionValue - - const chatData = cloneDeep(review.current) - review.current = undefined - setContentMap(chatData.id, chatData) - setElements((old) => { - const newArr = old.map((item) => { - if (chatData.taskId && item.token === chatData.taskId && item.kind === 'task') { - const newChild = item.children.map((subItem) => { - if (subItem.token === chatData.id && subItem.type === chatData.type) { - return { ...subItem, renderNum: subItem.renderNum + 1 } - } - return subItem - }) - return { ...item, children: newChild, renderNum: item.renderNum + 1 } - } else if (item.token === chatData.id && item.type === chatData.type) { - return { ...item, renderNum: item.renderNum + 1 } - } else { - return item - } - }) - return newArr - }) - } - cb && cb() - } catch (error) {} - }) + const handleSend: handleSendFunc = useMemoizedFn(({ request, optionValue, cb }) => {}) const handleResetData = useMemoizedFn(() => { review.current = undefined diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts index 6a7b1ef55b..bdf8bb0f1f 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts @@ -15,33 +15,23 @@ import type { UseHookBaseParams, UseTaskChatEvents, } from './type' -import type { - AIAgentGrpcApi, - AIEventQueryRequest, - AIInputEvent, - AIOutputEvent, - AIOutputI18n, - AIStartParams, -} from './grpcApi' +import type { AIAgentGrpcApi, AIEventQueryRequest, AIInputEvent, AIOutputEvent, AIStartParams } from './grpcApi' import type { AIChatData } from '@/pages/ai-agent/type/aiChat' import type { DeepPartial } from '@/pages/ai-agent/store/ChatDataStore' -import { AIChatQSDataTypeEnum, type AIChatQSData, type ReActChatBaseInfo } from './aiRender' +import { AIChatQSDataTypeEnum, type AIChatQSData, type ChatListRenderType } from './aiRender' import { useEffect, useRef, useState } from 'react' import { yakitNotify } from '@/utils/notification' import { useCreation, useInterval, useMemoizedFn, useThrottleFn } from 'ahooks' import { Uint8ArrayToString } from '@/utils/str' import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' -import useAIPerfData, { UseAIPerfDataTypes } from './useAIPerfData' import useCasualChat from './useCasualChat' -import useYakExecResult, { UseYakExecResultTypes } from './useYakExecResult' import useTaskChat from './useTaskChat' -import { genBaseAIChatData, genErrorLogData, genExecTasks, handleGrpcDataPushLog } from './utils' -import { AITaskStatus, AIInputEventSyncTypeEnum } from './grpcApi' +import { handleGrpcDataPushLog } from './utils' +import { AIInputEventSyncTypeEnum } from './grpcApi' import useAIChatLog from './useAIChatLog' import cloneDeep from 'lodash/cloneDeep' import { - convertNodeIdToVerbose, DefaultAIQuestionQueues, DefaultMemoryList, DefaultPlanHistoryList, @@ -52,7 +42,6 @@ import { aiSystemStreamStore } from '@/store/aiSystemStream' import { grpcQueryAIEvent } from '@/pages/ai-agent/grpc' import useAINodeLabel from './useAINodeLabel' import { formatAIAgentSetting } from '@/pages/ai-agent/utils' -import { handleResetForNewSession } from './grpcAIMessageHandlers' import useAIMessageData from './useAIMessageData' import { getDomainFromAISource } from './useGetChatDataStoreKey' import { v4 as uuidv4 } from 'uuid' @@ -205,15 +194,8 @@ function useChatIPC(params?: UseChatIPCParams) { // #endregion // #region 实时记忆列表相关逻辑 - const reactMemorys = useRef(cloneDeep(DefaultMemoryList)) - const taskMemorys = useRef(cloneDeep(DefaultMemoryList)) const [memoryList, setMemoryList] = useState(cloneDeep(DefaultMemoryList)) - const handleResetMemoryList = useMemoizedFn(() => { - reactMemorys.current = cloneDeep(DefaultMemoryList) - taskMemorys.current = cloneDeep(DefaultMemoryList) - setMemoryList(cloneDeep(DefaultMemoryList)) - }) // #endregion // #region 时间线相关逻辑 @@ -227,9 +209,9 @@ function useChatIPC(params?: UseChatIPCParams) { // #region 系统信息流展示相关逻辑 /** 记录都存在过的系统信息uuid, 只展示最新的一条系统信息 */ - const handleSetSystemStream = useMemoizedFn((uuid: string, content: string) => { - aiSystemStreamStore.appendChunk(uuid, content) - }) + const systemEventUUID = useRef([]) + const [systemStream, setSystemStream] = useState('') + const handleResetSystemStream = useMemoizedFn(() => { aiSystemStreamStore.reset() }) @@ -237,99 +219,24 @@ function useChatIPC(params?: UseChatIPCParams) { // #region 场景状态相关逻辑 const [focusMode, setFocusMode] = useState('') - const handleFocusModeChange = useMemoizedFn((mode: string) => { - setFocusMode(mode) - }) - - const handleResetFocusMode = useMemoizedFn(() => { - setFocusMode('') - }) // #endregion // #region 通知消息相关逻辑 - const [notifyMessage, setNotifyMessage] = useState(null) - const notifyMessageTimerRef = useRef | null>(null) - const handleSetNotifyMessage = useMemoizedFn((raw: AIAgentGrpcApi.Notify, label: AIOutputI18n) => { - if (notifyMessageTimerRef.current !== null) { - clearTimeout(notifyMessageTimerRef.current) - notifyMessageTimerRef.current = null - } - const { type, content } = raw - setNotifyMessage({ type, content, label }) - - let durationMs = 0 - if (typeof raw.duration_ms === 'number' && !Number.isNaN(raw.duration_ms) && raw.duration_ms > 0) { - durationMs = raw.duration_ms - } else if ( - typeof raw.duration_seconds === 'number' && - !Number.isNaN(raw.duration_seconds) && - raw.duration_seconds > 0 - ) { - durationMs = raw.duration_seconds * 1000 - } else if (typeof raw.duration === 'number' && !Number.isNaN(raw.duration) && raw.duration > 0) { - durationMs = raw.duration * 1000 - } - if (durationMs > 0) { - notifyMessageTimerRef.current = setTimeout(() => { - notifyMessageTimerRef.current = null - setNotifyMessage(null) - }, durationMs) - } - }) - const handleResetNotifyMessage = useMemoizedFn(() => { - if (notifyMessageTimerRef.current !== null) { - clearTimeout(notifyMessageTimerRef.current) - notifyMessageTimerRef.current = null - } - setNotifyMessage(null) - }) + const [notifyMessage, setNotifyMessage] = useState(null) + // #endregion // #region 历史任务规划列表相关逻辑 const [planHistoryList, setPlanHistoryList] = useState( cloneDeep(DefaultPlanHistoryList), ) - const handlePlanHistoryListChange = useMemoizedFn((list: AIAgentGrpcApi.PlanHistoryList) => { - try { - const arr = cloneDeep(list.records) - if (!arr || arr.length === 0) { - setPlanHistoryList({ ...list }) - return - } - const newArr = arr - .map((item) => { - // 因为后端给过来的task_progress是一个json的string类型数据 - item.task_progress = JSON.parse(item.task_progress as unknown as string) as AIAgentGrpcApi.PlanHistoryProgress - // 因为后端给过来的task_tree是一个json的string类型数据,所以需要转换成树形结构的数据,供UI展示使用 - const tree = JSON.parse(item.task_tree as unknown as string) as AIAgentGrpcApi.PlanTask - // 记录任务虎根节点的名字,供UI展示使用 - item.root_task_name = tree.name - item.task_tree = genExecTasks(tree) - return item - }) - .filter((item) => item.task_progress.phase !== 'Completed') - setPlanHistoryList({ ...list, records: newArr }) - } catch (error) {} - }) - const handleResetPlanHistoryList = useMemoizedFn(() => { - setPlanHistoryList(cloneDeep(DefaultPlanHistoryList)) - }) + // #endregion // #region 单次流执行时的输出展示数据 // 日志 const logEvents = useAIChatLog() - // AI性能相关数据和逻辑 - const aiPerfDataEvent = useAIPerfData({ - pushLog: logEvents.pushLog, - getChatDataStore, - }) - // 执行过程中插件输出的卡片 - const [yakExecResult, yakExecResultEvent] = useYakExecResult({ - pushLog: logEvents.pushLog, - getChatDataStore, - }) // #endregion // #region 自由对话(ReAct)相关变量和hook @@ -440,7 +347,7 @@ function useChatIPC(params?: UseChatIPCParams) { // #endregion /** 用户主动取消问题的loading状态变换 */ - const handleCancelLoadingChange = useMemoizedFn((type: ReActChatBaseInfo['chatType'], status: boolean) => { + const handleCancelLoadingChange = useMemoizedFn((type: ChatListRenderType, status: boolean) => { if (type === 'reAct') { setCancelCasualLoading(status) } else { @@ -583,8 +490,6 @@ function useChatIPC(params?: UseChatIPCParams) { /** 流接口开始前需要重置的一些状态 */ const handleResetBeforeStart = useMemoizedFn(() => { - // 清空场景 - handleResetFocusMode() // 清空自由对话相关的ID currentCasualTaskID.current = '' // 清空任务规划相关的ID @@ -600,12 +505,8 @@ function useChatIPC(params?: UseChatIPCParams) { handleResetGrpcFile() handleResetRunTimeIDs() handleResetQuestionQueue() - handleResetMemoryList() handleResetReActTimelines() handleResetSystemStream() - handleResetFocusMode() - handleResetNotifyMessage() - handleResetPlanHistoryList() currentCasualTaskID.current = '' casualSubTaskIDs.current.clear() handleResetCasualStatus() @@ -614,12 +515,9 @@ function useChatIPC(params?: UseChatIPCParams) { setCancelCasualLoading(false) setCancelTaskLoading(false) - yakExecResultEvent.handleResetData() casualChatEvent.handleResetData() taskChatEvent.handleResetData() - // 清除类型处理方法库里的临时数据 - handleResetForNewSession() // 重置历史数据请求 requestEvents.handleReset() }) @@ -649,7 +547,6 @@ function useChatIPC(params?: UseChatIPCParams) { const answer: DeepPartial = { httpRunTimeIDs: cloneDeep(httpRunTimeIDs), riskRunTimeIDs: cloneDeep(riskRunTimeIDs), - yakExecResult: cloneDeep(yakExecResult), casualChat: cloneDeep(casualChat), taskChat: cloneDeep(taskChat), grpcFolders: cloneDeep(grpcFolders), @@ -950,183 +847,6 @@ function useChatIPC(params?: UseChatIPCParams) { return } - if (res.Type === 'notify') { - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Notify - handleSetNotifyMessage(data, res.NodeIdVerbose) - return - } - - if (res.Type === 'structured' && res.NodeId === 'plan_exec_tasks') { - // 任务规划历史数据列表 - const list = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanHistoryList - handlePlanHistoryListChange(list) - return - } - - if (UseAIPerfDataTypes.includes(res.Type)) { - if (res.IsSync) return - // AI性能数据处理 - aiPerfDataEvent.handleSetData(res) - return - } - - if (UseYakExecResultTypes.includes(res.Type)) { - if (res.IsSync) return - // 执行过程中插件输出的卡片 - yakExecResultEvent.handleSetData(res) - return - } - - if (res.Type === 'structured' && res.NodeId === 'queue_info') { - // 因为问题队列也分自由对话和任务规划队列,所以需要先屏蔽处理任务规划的队列信息 - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) return - // 问题队列信息由chatIPC-hook进行收集 - const { tasks, total_tasks } = JSON.parse(ipcContent) as AIAgentGrpcApi.QuestionQueues - setQuestionQueue({ - total: total_tasks, - data: tasks ?? [], - }) - return - } - - if (res.Type === 'structured' && res.NodeId === 'timeline_item') { - if (res.IsSync) return - // 自由对话子任务的time-line-item不展示 - if (casualSubTaskIDs.current.has(res.TaskId)) return - /* 实时时间线单条 */ - const timelineItem = JSON.parse(ipcContent) as AIAgentGrpcApi.TimelineItem - setReActTimelines((old) => [...old, timelineItem]) - return - } - - if (res.Type === 'structured') { - const obj = JSON.parse(ipcContent) || '' - - if (obj?.level) { - // 执行日志信息 - const data = obj as AIAgentGrpcApi.Log - logEvents.pushLog({ - type: 'log', - Timestamp: res.Timestamp, - data: data, - }) - } else if (res.NodeId === 'react_task_status_changed') { - if (res.IsSync) return - // 只负责获取自由对话的任务状态 - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) return - /* 问题的状态变化 */ - - const params = JSON.parse(ipcContent) as AIAgentGrpcApi.ReactTaskChanged - const { react_task_now_status } = params - const react_task_id = res.TaskId || params.react_task_id - if (['completed', 'aborted', 'skipped'].includes(react_task_now_status)) { - if (currentCasualTaskID.current && currentCasualTaskID.current === react_task_id) { - setCancelCasualLoading(false) - setCasualLoading(false) - } - if (currentCasualTaskID.current === react_task_id) handleResetFocusMode() - if (currentTaskPlanID.current?.taskID === react_task_id) { - currentTaskPlanID.current.status = react_task_now_status as AITaskStatus - setCancelTaskLoading(false) - } - } - casualChatEvent.handleSetData(res) - return - } else if (res.NodeId === 'status') { - if (res.IsSync) return - const data = JSON.parse(ipcContent) as { key: string; value: string } - if (data.key === 're-act-loading-status-key') { - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) { - // 任务规划-loading展示标题 - setTaskStatus((old) => { - if (old.loading) { - return { ...old, task: data.value || '加载中...' } - } - return old - }) - } else { - if (res.TaskId !== currentCasualTaskID.current) return - // 自由对话-loading展示标题 - setCasualTitle(data.value) - } - } else if (data.key === 'plan-executing-loading-status-key') { - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) { - // 任务规划-loading展示标题 - setTaskStatus((old) => { - if (old.loading) { - return { ...old, plan: data.value || '加载中...' } - } - return old - }) - } - } else { - // 执行状态卡片处理 - yakExecResultEvent.handleSetData(res) - } - } else { - // 因为流数据有日志类型,所以都放入日志逻辑过滤一遍 - if (res.NodeId === 'stream-finished') { - const { event_writer_id, is_reason, is_system } = JSON.parse( - ipcContent, - ) as AIAgentGrpcApi.AIStreamFinished - if (!event_writer_id) { - logEvents.pushLog(genErrorLogData(res.Timestamp, `stream-finished数据异常, event_writer_id缺失`)) - return - } - logEvents.sendStreamLog(event_writer_id) - - // 非stream数据不需要进行后续的流结束处理逻辑 - if (is_reason || is_system) return - } - - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) { - taskChatEvent.handleSetData(res) - } else { - casualChatEvent.handleSetData(res) - } - } - return - } - - if (res.Type === 'stream') { - if (res.IsSystem || res.IsReason) { - const { CallToolID, NodeId, NodeIdVerbose, EventUUID, StreamDelta, ContentType } = res - if (!NodeId || !EventUUID) return - let ipcStreamDelta = Uint8ArrayToString(StreamDelta) || '' - const content = ipcContent + ipcStreamDelta - logEvents.pushLog({ - type: 'stream', - Timestamp: res.Timestamp, - data: { - CallToolID, - NodeId, - NodeIdVerbose: NodeIdVerbose || convertNodeIdToVerbose(NodeId), - EventUUID, - status: 'start', - content: content, - ContentType, - }, - }) - - // 输出实时系统信息流 - if (res.IsSystem) handleSetSystemStream(EventUUID, content) - return - } - - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) { - taskChatEvent.handleSetData(res) - } else { - casualChatEvent.handleSetData(res) - } - return - } - - // 自由对话和任务规划共用的类型 - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) { - taskChatEvent.handleSetData(res) - } else { - casualChatEvent.handleSetData(res) - } return } catch (error) { handleGrpcDataPushLog({ info: res, pushLog: logEvents.pushLog }) @@ -1235,7 +955,6 @@ function useChatIPC(params?: UseChatIPCParams) { setHttpRunTimeIDs(chatData.httpRunTimeIDs || []) setRiskRunTimeIDs(chatData.riskRunTimeIDs || []) setReActTimelines(() => chatData.reActTimelines || []) - yakExecResultEvent.handleSetYakResult(chatData.yakExecResult || {}) casualChatEvent.setElements(chatData.casualChat?.elements || []) taskChatEvent.setElements(chatData.taskChat?.elements || []) } else { @@ -1315,16 +1034,7 @@ function useChatIPC(params?: UseChatIPCParams) { }, []) /** 清空指定变量数据 */ - const handleResetTarget = useMemoizedFn((target: 'memoryList') => { - switch (target) { - case 'memoryList': - handleResetMemoryList() - break - - default: - break - } - }) + const handleResetTarget = useMemoizedFn((target: 'memoryList') => {}) /** 用户手动创建内容的执行方法 */ const handleUserManualIntervention = useMemoizedFn((chatInfo: AIChatQSData) => { @@ -1345,7 +1055,6 @@ function useChatIPC(params?: UseChatIPCParams) { execute, httpRunTimeIDs, riskRunTimeIDs, - yakExecResult, casualChat, taskChat, grpcFolders, @@ -1369,7 +1078,6 @@ function useChatIPC(params?: UseChatIPCParams) { execute, httpRunTimeIDs, riskRunTimeIDs, - yakExecResult, casualChat, taskChat, grpcFolders, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts new file mode 100644 index 0000000000..734302a75b --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts @@ -0,0 +1,41 @@ +// useChatIPC.ts +import { useEffect, useContext, useCallback, useRef } from 'react' +import { globalSessionEngine } from './ChatMultiSessionController' +import { AIChatIPCStartParams, AIChatSendParams } from './type' +import { useMemoizedFn } from 'ahooks' +import { SessionIdContext } from './explame/SessionRuntime' +const { ipcRenderer } = window.require('electron') + +export function useChatIPC() { + const sessionId = useContext(SessionIdContext) + const currentSessionIdRef = useRef(sessionId) + + useEffect(() => { + currentSessionIdRef.current = sessionId + }, [sessionId]) + + const onStart = useCallback( + (sessionID: string, args: AIChatIPCStartParams, onSuccess?: () => void) => { + // 监听网络,直接丢给大脑处理 + ipcRenderer.on(`${sessionId}-data`, (e, res: any) => { + globalSessionEngine.handleGrpcOutputEvent(sessionId, res) + }) + + globalSessionEngine.handleStartSession(sessionID, args, onSuccess) + }, + [sessionId], + ) + + const onSend = useMemoizedFn((payload: AIChatSendParams) => { + globalSessionEngine.handleSendMessage(payload) + }) + + // 组件卸载时拔插头,清理闭环 + useEffect(() => { + return () => { + globalSessionEngine.forceCloseSession(currentSessionIdRef.current) + } + }, []) + + return { onStart, onSend } +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts index e8be500aff..c9312e28f4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts @@ -6,20 +6,17 @@ import type { UseTaskChatParams, UseTaskChatState, } from './type' -import type { AIChatQSData, AIReviewType, ReActChatRenderItem, AITaskInfoProps } from './aiRender' +import type { AIChatQSData, ReActChatRenderItem, AITaskInfoProps } from './aiRender' import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' import { useRef, useState } from 'react' import { useCreation, useMemoizedFn } from 'ahooks' -import { Uint8ArrayToString } from '@/utils/str' import cloneDeep from 'lodash/cloneDeep' import { DefaultCurrentExecTaskTree } from './defaultConstant' import { genBaseAIChatData, genExecTasks, handleGrpcDataPushLog } from './utils' import { yakitNotify } from '@/utils/notification' -import { AIInputEventSyncTypeEnum, AITaskStatus } from './grpcApi' +import { AITaskStatus } from './grpcApi' import { AIChatQSDataTypeEnum } from './aiRender' import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' -import { has } from 'lodash' -import { grpcAIMessageHandlers } from './grpcAIMessageHandlers' function useTaskChat(params: UseTaskChatParams): [UseTaskChatState, UseTaskChatEvents] @@ -190,15 +187,6 @@ function useTaskChat(params: UseTaskChatParams) { // #region review数据-hook缓存数据 const review = useRef() - const handleGetReview = useMemoizedFn(() => { - return review.current - }) - const handleSetReview = useMemoizedFn((newReview: AIChatQSData | undefined) => { - review.current = cloneDeep(newReview) - }) - const handleResetReview = useMemoizedFn(() => { - review.current = undefined - }) // 将 review 数据处理成需要展示的UI数据 const handleReviewDataToUI = useMemoizedFn((reviewInfo: AIChatQSData) => { @@ -216,90 +204,7 @@ function useTaskChat(params: UseTaskChatParams) { // #endregion /** 处理数据方法 */ - const handleSetData = useMemoizedFn((res: AIOutputEvent) => { - try { - let funcKey = res.Type - if (res.Type === 'structured' && res.NodeId === 'stream-finished') { - // stream数据结束标识 - funcKey = res.NodeId - } else if (res.Type === 'api_request_failed' && res.NodeId === 'ai_call_failure') { - funcKey = res.Type - } else if (res.Type === 'structured' && res.NodeId === 'capability_inventory') { - funcKey = res.NodeId - } else if (res.Type === 'perception' && res.NodeId === 'perception') { - funcKey = res.Type - } else if (res.Type === 'current_task_todo_list_update' && res.NodeId === 'current_task_todo_list') { - funcKey = res.Type - } else if (res.NodeId === 'session_snapshot') { - funcKey = res.NodeId - } - - const handleFunc = grpcAIMessageHandlers[funcKey || ''] - if (handleFunc) { - handleFunc({ - res, - info: { chatType: 'task' }, - getCurrentTaskPlanID, - getRequest, - setElements, - getElements, - setContentMap, - getContentMap, - pushLog: handlePushLog, - review: { - handleGetReview, - handleSetReview, - onReview, - onReviewExtra, - onReviewRelease, - handleReviewDataToUI, - sendRequest, - }, - getChatDataStore, - }) - return - } - - const ipcContent = Uint8ArrayToString(res.Content) || '' - - if (res.Type === 'structured' && res.NodeId === 'system') { - const data = JSON.parse(ipcContent) || '' - - if (data && typeof data === 'object' && data?.type === 'push_task') { - // 开始任务的执行 - const info = data as AIAgentGrpcApi.ChangeTask - handleUpdateTaskState(info.task.task_id || res.TaskId, AITaskStatus.inProgress) - handleTaskNode(res, info) - } - - if (data && typeof data === 'object' && data?.type === 'pop_task') { - // 结束任务 & 请求更新任务树最新状态数据 - const info = data as AIAgentGrpcApi.ChangeTask - handleTaskNode(res, info) - sendRequest && sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) - } - return - } else if (res.Type === 'plan') { - // 更新正在执行的任务树 - const tasks = JSON.parse(ipcContent) as { root_task: AIAgentGrpcApi.PlanTask } - if (has(tasks, 'root_task')) { - const plans = genExecTasks(tasks.root_task) - setPlan({ task_tree: cloneDeep(plans), root_task_name: tasks.root_task.name }) - } else { - setPlan(cloneDeep(DefaultCurrentExecTaskTree)) - } - return - } - - // 未识别类型全部归档到日志处理 - handleGrpcDataPushLog({ info: res, pushLog: handlePushLog }) - } catch (error) { - handleGrpcDataPushLog({ - info: res, - pushLog: handlePushLog, - }) - } - }) + const handleSetData = useMemoizedFn((res: AIOutputEvent) => {}) const handleResetData = useMemoizedFn(() => { handleResetPlanTree() @@ -308,46 +213,7 @@ function useTaskChat(params: UseTaskChatParams) { }) /** review 界面选项触发事件 */ - const handleSend: handleSendFunc = useMemoizedFn(({ request, optionValue, cb }) => { - try { - const { InteractiveId, InteractiveJSONInput } = request - if (!review.current || review.current.id !== InteractiveId) { - yakitNotify('error', '未获取到 review 信息, 操作无效') - return - } - - const chatData = cloneDeep(review.current) - ;(chatData.data as AIReviewType).selected = InteractiveJSONInput || '' - ;(chatData.data as AIReviewType).optionValue = optionValue - - handleResetReview() - handleReviewDataToUI(chatData) - setContentMap(chatData.id, chatData) - setElements((old) => { - let isUpdate = false - const newArr = old.map((item) => { - if (chatData.taskId && item.token === chatData.taskId && item.kind === 'task') { - item.children.push({ - token: chatData.id, - type: chatData.type, - renderNum: 1, - chatType: 'task', - kind: 'item', - }) - isUpdate = true - return { ...item, renderNum: item.renderNum + 1 } - } - return item - }) - if (!isUpdate) { - newArr.push({ token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task', kind: 'item' }) - } - return newArr - }) - - cb && cb() - } catch (error) {} - }) + const handleSend: handleSendFunc = useMemoizedFn(({ request, optionValue, cb }) => {}) /** grpc接口关闭后的后续处理逻辑 */ const handleCloseGrpc = useMemoizedFn(() => { @@ -356,19 +222,7 @@ function useTaskChat(params: UseTaskChatParams) { }) /** 当前任务规划结束-触发UI展示结束标识 */ - const handlePlanExecEnd = useMemoizedFn((res: AIOutputEvent) => { - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: 'task', - type: AIChatQSDataTypeEnum.END_PLAN_AND_EXECUTION, - data: '', - } - setContentMap(chatData.id, chatData) - setElements((old) => [ - ...old, - { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task', kind: 'item' }, - ]) - }) + const handlePlanExecEnd = useMemoizedFn((res: AIOutputEvent) => {}) /** 用户手动介入逻辑 */ const handleUserManualIntervention = useMemoizedFn((chatInfo: AIChatQSData) => { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useYakExecResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useYakExecResult.ts deleted file mode 100644 index 38f3ff73ae..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useYakExecResult.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { useRef, useState } from 'react' -import { useCreation, useMemoizedFn } from 'ahooks' -import { Uint8ArrayToString } from '@/utils/str' -import { checkStreamValidity, convertCardInfo } from '@/hook/useHoldGRPCStream/useHoldGRPCStream' -import { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' -import { AIChatLogData, UseYakExecResultEvents, UseYakExecResultParams, UseYakExecResultState } from './type' -import { handleGrpcDataPushLog } from './utils' -import { v4 as uuidv4 } from 'uuid' -import { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' -import { AIYakExecFileRecord } from './aiRender' -import useThrottleState from '@/hook/useThrottleState' - -// 属于该 hook 处理数据的类型 -export const UseYakExecResultTypes = ['yak_exec_result'] - -function useYakExecResult(params?: UseYakExecResultParams): [UseYakExecResultState, UseYakExecResultEvents] - -function useYakExecResult(params?: UseYakExecResultParams) { - const handlePushLog = useMemoizedFn((log: AIChatLogData) => { - if (params?.pushLog) { - params.pushLog(log) - } - }) - - // card - const cardKVPair = useRef>(new Map()) - const cardTimeRef = useRef(null) - const [card, setCard] = useState([]) - const [yakExecResultLogs, setYakExecResultLogs] = useState([]) // log - - const execFileRecordOrder = useRef(1) - /** 插件执行过程中的文件操作记录 */ - const [execFileRecord, setExecFileRecord] = useThrottleState>(new Map(), { - wait: 2000, - }) - const updateExecFileRecord = useMemoizedFn((CallToolID: string, info: StreamResult.Log) => { - try { - setExecFileRecord((old) => { - const newMap = new Map(old) - const keyName = CallToolID || 'system' - const record = newMap.get(keyName) || [] - record.push({ ...info, id: uuidv4(), order: execFileRecordOrder.current++ }) - newMap.set(keyName, [...record]) - return newMap - }) - } catch (error) {} - }) - - const onHandleCard = useMemoizedFn((value: AIAgentGrpcApi.AICardMessage) => { - const logData = value.content as StreamResult.Log - const checkInfo: AIAgentGrpcApi.AICard = checkStreamValidity(value.content as StreamResult.Log) - if (!checkInfo) return - const { id, data, tags } = checkInfo - const { timestamp } = logData - const originData = cardKVPair.current.get(id) - if (originData && originData.Timestamp > timestamp) { - return - } - cardKVPair.current.set(id, { - Id: id, - Data: data, - Timestamp: timestamp, - Tags: Array.isArray(tags) ? tags : [], - }) - onSetCard() - }) - const onSetCard = useMemoizedFn(() => { - if (cardTimeRef.current) return - cardTimeRef.current = setTimeout(() => { - const cacheCard: AIAgentGrpcApi.AIInfoCard[] = convertCardInfo(cardKVPair.current) - setCard(() => [...cacheCard]) - cardTimeRef.current = null - }, 500) - }) - - /** - * @description 该方法可以记录yak_exec_result中所有的日志,根但是目前只对接level:file;后续据可需求更改 - */ - const onHandleYakExecResultLogs = useMemoizedFn((obj: AIAgentGrpcApi.AICardMessage) => { - const log = obj.content as StreamResult.Log - setYakExecResultLogs((perLog) => [...perLog, { ...log, id: uuidv4() }]) - }) - - const onHandleYakExecResult = useMemoizedFn((CallToolID: string, value: AIAgentGrpcApi.AIPluginExecResult) => { - try { - if (!value?.IsMessage) return - const message = value?.Message || '' - const obj: AIAgentGrpcApi.AICardMessage = JSON.parse(Buffer.from(message, 'base64').toString('utf8')) - - if (obj.type !== 'log') return - const content = obj.content as StreamResult.Log - switch (content.level) { - case 'feature-status-card-data': - onHandleCard(obj) - break - case 'file': - updateExecFileRecord(CallToolID, content) - break - default: - break - } - } catch (error) {} - }) - - /** AI执行过程中的status状态卡片化处理 */ - const onAIStatusHandleCard = useMemoizedFn((info: { key: string; value: string; timestamp: number }) => { - const { key, value, timestamp } = info - const originData = cardKVPair.current.get(key) - if (originData && originData.Timestamp > timestamp) { - return - } - cardKVPair.current.set(key, { - Id: key, - Data: value, - Timestamp: timestamp, - Tags: [], - }) - onSetCard() - }) - - const handleSetData = useMemoizedFn((res: AIOutputEvent) => { - try { - let ipcContent = Uint8ArrayToString(res.Content) || '' - - if (res.Type === 'yak_exec_result') { - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIPluginExecResult - onHandleYakExecResult(res.CallToolID || '', data) - return - } - if (res.Type === 'structured' && res.NodeId === 'status') { - // 执行状态卡片处理 - const data = JSON.parse(ipcContent) as { key: string; value: string } - if (data.key !== 're-act-loading-status-key') { - onAIStatusHandleCard({ ...data, timestamp: res.Timestamp }) - } - return - } - } catch (error) { - handleGrpcDataPushLog({ info: res, pushLog: handlePushLog }) - } - }) - - /** 重置所有数据 */ - const handleResetData = useMemoizedFn(() => { - cardKVPair.current = new Map() - cardTimeRef.current = null - setCard([]) - setYakExecResultLogs([]) - execFileRecordOrder.current = 1 - setExecFileRecord(new Map()) - }) - - const handleSetYakResult = useMemoizedFn((newData: UseYakExecResultState) => { - setCard(newData.card || []) - setExecFileRecord(newData.execFileRecord || new Map()) - setYakExecResultLogs(newData.yakExecResultLogs || []) - }) - - const state: UseYakExecResultState = useCreation(() => { - return { card, execFileRecord, yakExecResultLogs } - }, [card, execFileRecord, yakExecResultLogs]) - - const events: UseYakExecResultEvents = useCreation(() => { - return { handleSetData, handleResetData, handleSetYakResult } - }, []) - - return [state, events] as const -} - -export default useYakExecResult diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts index 5d93d7362a..aa73bb5907 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts @@ -3,42 +3,40 @@ */ import type { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' import type { DialogueRecord } from '@/pages/ai-agent/store/type' -import type { AITaskInfoProps, ReActChatRenderItem, AIChatQSDataType, TodoListCardData } from './aiRender' -import type { AIChatLogToInfo, AIChatLogData, TaskChatTaskInfo, AIMessageHandlerParams } from './type' +import type { + AITaskInfoProps, + ReActChatRenderItem, + AIChatQSDataType, + TodoListCardData, + ChatListRenderType, +} from './aiRender' +import type { AIChatLogToInfo, AIChatLogData } from './type' import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' -import { AIChatQSDataTypeEnum } from './aiRender' import { AIToDoListStatusEnum, generateTaskChatExecution } from '@/pages/ai-agent/defaultConstant' import { Uint8ArrayToString } from '@/utils/str' import { v4 as uuidv4 } from 'uuid' import { JSONParseLog } from '@/utils/tool' -/** 生成任务的唯一标识 */ -export const generateTaskId = (params: { - chatType: ReActChatRenderItem['chatType'] - res: AIOutputEvent - /** 获取当前任务规划的问题ID信息 */ - getCurrentTaskPlanID?: () => TaskChatTaskInfo | undefined - /** 获取当前自由对话父任务 ID */ - getTaskId?: () => string - getContentMap: AIMessageHandlerParams['getContentMap'] +/** 生成任务节点的唯一ID */ +export const generateTaskNodeID = (planID: string, taskID: string) => { + return `${planID}-${taskID || 'unknown'}` +} +/** + * 任务节点内的数据生成任务节点ID + * @param isExist 生成的任务节点是否已经存在,不存在则不是任务节点数据,归为默认节点内的数据 + */ +export const generateTaskNodeDataID = (params: { + chatType: ChatListRenderType + planID?: string + taskID: AIOutputEvent['TaskIndex'] + isExist: (key: string) => boolean }) => { - const { chatType, res, getCurrentTaskPlanID, getTaskId, getContentMap } = params - /** 任务规划走 getCurrentTaskPlanID;自由对话走 getTaskId */ - const parentTaskId = (chatType === 'task' ? getCurrentTaskPlanID?.()?.taskID : getTaskId?.()) || '' - if (res.TaskId) { - if (!parentTaskId) return undefined - const taskGroup = getContentMap(`${parentTaskId}-${res.TaskId}`) - if (taskGroup?.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - return taskGroup.id - } - } - if (chatType === 'task' && getCurrentTaskPlanID?.()?.taskID) { - const defaultKey = `${getCurrentTaskPlanID()?.taskID}-unknown` - if (getContentMap(defaultKey)?.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP) { - return defaultKey - } - } - return undefined + const { chatType, planID, taskID, isExist } = params + // 自由对话或者没有任务规划ID,则不生成任务节点ID + if (chatType === 'reAct' || !planID) return undefined + const keyLabel = `${planID}-${taskID || 'unknown'}` + if (isExist(keyLabel)) return keyLabel + return `${planID}-unknown` } /** 生成AI-UI展示的必须基础数据 */ @@ -171,7 +169,7 @@ export const isToolExecStream = (nodeID: string) => { * indexedDB 数据库数据转 ReActChatRenderItem */ export const indexedDBDataToReActChatRenderItem = ( - chatType: ReActChatRenderItem['chatType'], + chatType: ChatListRenderType, data: DialogueRecord[], ): ReActChatRenderItem[] => data.map((item) => { From 808eaf198e0b2a761b3d5858e5e3134620ce96ea Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Tue, 23 Jun 2026 17:37:44 +0800 Subject: [PATCH 002/101] =?UTF-8?q?=E4=BE=8B=E5=AD=90=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-re-act/hooks/explame/ChatNode.tsx | 157 +++++++++--------- 1 file changed, 81 insertions(+), 76 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx index 44ec358797..c2cb9fb94a 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx @@ -1,92 +1,97 @@ -// ChatNodes.tsx -import React, { useState, useContext, useMemo } from 'react' -import { SessionIdContext } from './SessionRuntime' -import { globalSessionEngine } from './ChatMultiSessionController' +import React, { memo } from 'react' +import { useChatUIStore, useChatContent } from './ChatContext' -// ========================================== -// 1. 叶子节点 (原子渲染层) - 性能爆发点 -// ========================================== -export const ChatItem = ({ element }: { element: any }) => { - const sessionId = useContext(SessionIdContext) - const { rawData } = globalSessionEngine.ensureSession(sessionId) +// --- 5.1 叶子节点组件 (Item) --- +export const ChatItem = memo(({ token }: { token: string }) => { + // 只订阅自己节点下的 renderNum 变动 + const renderNum = usestore((state) => state.items[token]?.renderNum) - // 🎯 依赖 element.renderNum 强制去内存池捞取新鲜文本 - const content = useMemo(() => { - if (element.type === 'stream' || element.type === 'thought') { - const nodeData = rawData.contents.get(element.token || element.id) - return nodeData ? nodeData.content : '' - } - return element.content || JSON.stringify(element.data || {}) - }, [element.token, element.id, element.type, element.content, element.data, element.renderNum, rawData]) + const contentManager = useChatContent() + = + const session=usecontex + const{rawdata}=glkoencinstance(session) - if (element.type === 'thought') { - return
🤔 {content}
- } - if (element.type === 'stream') { - return
💬 {content}
- } - if (element.type === 'yak_risk_count') { - return ( -
- 🚨 高危流量: {element.data?.riskFlowDataCount || 0} -
- ) - } - if (element.type === 'yak_httpflow_count') { - return ( -
- 🌐 HTTP流: {element.data?.httpFlowDataCount || 0} -
- ) - } + if (renderNum === undefined) return null - return
{content}
-} + // 拿着 token 去数据池取真实的大段文本数据 + const realData = contentManager.get(token) -// ========================================== -// 2. 任务节点 (Task 包裹层) -// ========================================== -export const ChatTask = ({ taskData }: { taskData: any }) => { return ( -
-
- ⚙️ {taskData.toolName || '工具调用'} (Task ID: {taskData.token || taskData.id}) -
+
+ {realData?.type}: + {realData?.content} + (RenderVersion: {renderNum}) +
+ ) +}) + +// --- 5.2 容器组组件 (Group) --- +export const ChatGroup = memo(({ token }: { token: string }) => { + // 仅订阅 childrenTokens 数组指针的变动(即内部子项增减会刷新,某个子项打字更新时这里完全不刷) + const childrenTokens = useChatUIStore((state) => state.groups[token]?.childrenTokens) -
- {taskData.children?.map((child: any) => ( - - ))} -
+ if (!childrenTokens) return null + + return ( +
+
组容器 [{token}]
+ {childrenTokens.map((childToken) => ( + + ))}
) -} +}) + +// --- 5.3 复杂任务树组件 (Task) --- +export const ChatTask = memo(({ token }: { token: string }) => { + const childrenTokens = useChatUIStore((state) => state.tasks[token]?.childrenTokens) -// ========================================== -// 3. 编组节点 (Group 顶层折叠层) -// ========================================== -export const ChatGroup = ({ groupData }: { groupData: any }) => { - const [collapsed, setCollapsed] = useState(false) + if (!childrenTokens) return null return ( -
-
setCollapsed(!collapsed)} - style={{ cursor: 'pointer', fontWeight: 'bold', display: 'flex', justifyContent: 'space-between' }} - > - 📂 规划编组: {groupData.title || groupData.token || groupData.id} - {collapsed ? '展开 ▼' : '折叠 ▲'} -
+
+

大任务卡片 [{token}]

+ + ==list + {childrenTokens.map((childToken) => ( + + ))} +
+ ) +}) + +// --- 5.4 顶层异构节点分发器 --- +const RootNodeDispatcher = memo(({ token }: { token: string }) => { + // 只在节点刚创建判断类型或类型突变时触发,返回纯字符串,彻底隔绝下层的 Streaming 流高频刷新 + const nodeKind = useChatUIStore((state) => { + if (state.items[token]) return 'item' + if (state.groups[token]) return 'group' + if (state.tasks[token]) return 'task' + return null + }) - {!collapsed && ( -
- {groupData.children?.map((child: any) => { - if (child.type === 'task') return - if (child.type === 'group') return // 支持无限嵌套 - return - })} -
- )} + switch (nodeKind) { + case 'task': + return + case 'group': + return + case 'item': + return + default: + return null + } +}) + +// --- 5.5 页面最外层总看板 --- +export const ChatList = () => { + const rootOrder = useChatUIStore((state) => state.taskchatelemnt) + + return ( +
+

AI 聊天多实例面板

+ {rootOrder.map((token) => ( + + ))}
) } From 5f1bb94033f5610a43f726864ae509b57ee535cd Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Wed, 24 Jun 2026 14:40:14 +0800 Subject: [PATCH 003/101] =?UTF-8?q?feat:=E8=B0=83=E6=95=B4=E9=83=A8?= =?UTF-8?q?=E5=88=86=E6=96=B9=E6=B3=95=E9=80=BB=E8=BE=91=E5=92=8C=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/type/aiChat.d.ts | 2 +- .../hooks/ChatMultiSessionController.ts | 64 +++++++++++++------ .../main/src/pages/ai-re-act/hooks/type.ts | 11 +--- .../src/pages/ai-re-act/hooks/useChatIPC.ts | 2 +- .../pages/ai-re-act/hooks/useNewChatIPC.ts | 13 +++- 5 files changed, 60 insertions(+), 32 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 5cacd3deb6..953e165b45 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -223,7 +223,7 @@ export interface AIAgentChatMetaData { /** 自由对话的实时记忆列表 */ casualMemoryList: AIAgentGrpcApi.MemoryEntryList /** 会话通信流建立成功后的UI回调触发事件 */ - onSessionStartSuccess?: () => void + onSessionStartSuccess?: (sessionId: string) => void /** 任务规划的实时记忆列表 */ taskMemoryList: AIAgentGrpcApi.MemoryEntryList /** 当前专注场景ID */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 9aef0bd7fc..de33847ce9 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -4,12 +4,12 @@ import { createChatStore } from './chatStore' import { Uint8ArrayToString } from '@/utils/str' import { AIAgentSettingDefault, AIModelTypeEnum } from '@/pages/ai-agent/defaultConstant' import cloneDeep from 'lodash/cloneDeep' -import { DefaultMemoryList, DefaultTodoListCardData } from './defaultConstant' +import { DefaultMemoryList, DefaultPlanItemDetailsData } from './defaultConstant' import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHandlers' import { genExecTasks, handleGrpcDataPushLog } from './utils' import type { AIChatIPCStartParams, AIChatSendParams } from './type' import { yakitNotify } from '@/utils/notification' -import { AIChatQSDataTypeEnum, AIReviewType } from './aiRender' +import { AIChatQSDataTypeEnum, type AIReviewType } from './aiRender' import { aiAgentLogEmitter } from './AIAgentLogEmitter' const { ipcRenderer } = window.require('electron') @@ -87,10 +87,10 @@ const genAIAgentChatData = (): AIAgentChatData => { }, casualChat: { - todoList: cloneDeep(DefaultTodoListCardData), + planDetails: cloneDeep(DefaultPlanItemDetailsData), }, taskChat: { - todoListMap: new Map(), + planDetailsMap: new Map(), }, contents: new Map(), } @@ -120,12 +120,13 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { } export class ChatMultiSessionController { + private requestPool = new Map() private storePool = new Map>() private rawDataPool = new Map() - private requestPool = new Map() private metaPool = new Map() private readyChannels = new Set() + private activeShowSession: string | null = null /** 获取对应会话的所有数据集 */ public ensureSession(sessionId: string) { @@ -136,9 +137,9 @@ export class ChatMultiSessionController { this.metaPool.set(sessionId, genAIAgentChatMetaData()) } return { + request: this.requestPool.get(sessionId)!, store: this.storePool.get(sessionId)!, rawData: this.rawDataPool.get(sessionId)!, - request: this.requestPool.get(sessionId)!, meta: this.metaPool.get(sessionId)!, } } @@ -147,11 +148,34 @@ export class ChatMultiSessionController { public registerActiveChannel(sessionId: string) { this.readyChannels.add(sessionId) } + /** 设置当前展示的会话 Session */ + public setActiveShowSession(sessionId: string) { + this.activeShowSession = sessionId + } + /** 判断指定会话是否当前正在展示 */ + public isActiveShowSession(sessionId: string) { + return this.activeShowSession === sessionId + } + + /** 更新指定会话的配置参数 */ + public updateSessionConfig(sessionId: string, config: Partial) { + const { request } = this.ensureSession(sessionId) + Object.assign(request, config) + } /** 建立指定session会话的连接 */ - public handleStartSession(sessionId: string, requestParams: AIChatIPCStartParams, cb?: () => void) { - const { params } = requestParams - const { request, meta } = this.ensureSession(sessionId) + public handleStartSession(requestParams: AIChatIPCStartParams, cb?: (sessionId: string) => void) { + const { token: sessionId, params } = requestParams + const isExec = this.readyChannels.has(sessionId) + if (isExec) { + yakitNotify('warning', '会话已经存在,请勿重复建立!') + return + } + + const { request, store, meta } = this.ensureSession(sessionId) + + store.getState().updateState({ execute: true, casualTitle: '发送问题,开启会话...' }) + if (params.Params?.UserQuery) { meta.createChatQuestion = { IsFreeInput: true, @@ -169,10 +193,10 @@ export class ChatMultiSessionController { /** 主动向grpc发送请求 */ public handleSendMessage(payload: AIChatSendParams) { try { - const { token, type, params, optionValue, extraValue } = payload + const { token, type, params, optionValue } = payload const isExist = this.readyChannels.has(token) if (!isExist) { - yakitNotify('warning', `[大管家] 会话 ${token} 未 Ready,请求已进队列排队...`) + yakitNotify('warning', '会话不存在,无法发送消息') return } @@ -235,9 +259,9 @@ export class ChatMultiSessionController { break default: - this.requestMessage(token, params) break } + this.requestMessage(token, params) } catch (error) {} } /** 向连接中的会话发送请求 */ @@ -261,7 +285,7 @@ export class ChatMultiSessionController { } else { // 调用历史数据恢复方法 } - meta?.onSessionStartSuccess?.() + meta?.onSessionStartSuccess?.(sessionId) return } @@ -326,20 +350,24 @@ export class ChatMultiSessionController { } } + private closeIPCListeners(sessionId: string) { + ipcRenderer.removeAllListeners(`${sessionId}-data`) + ipcRenderer.removeAllListeners(`${sessionId}-end`) + ipcRenderer.removeAllListeners(`${sessionId}-error`) + } + // 监听 session-error 事件 public handleSessionError(sessionId: string, error: any) {} // 监听 session-end 事件 - public handleSessionEnd(sessionId: string, res: any) {} + public handleSessionEnd(sessionId: string, res: any) { + this.closeIPCListeners(sessionId) + } /** 关闭指定session的连接 */ public forceCloseSession(sessionId: string) { ipcRenderer.invoke('cancel-ai-re-act', sessionId).catch(() => {}) - // ipcRenderer.removeAllListeners(`${sessionId}-data`) - // ipcRenderer.removeAllListeners(`${sessionId}-end`) - // ipcRenderer.removeAllListeners(`${sessionId}-error`) this.readyChannels.delete(sessionId) - // crossWindowEmitter.clearSessionBuffer(sessionId) // 清理日志内容 } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts index d8f2a1e393..bdcb1554b9 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts @@ -12,7 +12,6 @@ import type { Dispatch, SetStateAction } from 'react' import type { Domain } from '@/pages/ai-agent/store/constants' import type { AIAgentGrpcApi, AIInputEvent, AIOutputEvent, AISource, AIStartParams, AITaskStatusType } from './grpcApi' import type { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' -import type { CustomPluginExecuteFormValue } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType' import type { AIAgentChatData, AIAgentChatMetaData, AIChatData } from '@/pages/ai-agent/type/aiChat' import type { ChatDataStore } from '@/pages/ai-agent/store/ChatDataStore' import { createChatStore } from './chatStore' @@ -28,12 +27,7 @@ interface UseHookBaseEvents { handleSetData: (res: AIOutputEvent) => void handleResetData: () => void } -export type handleSendFunc = (params: { - request: AIInputEvent - optionValue?: string - extraValue?: AIChatIPCStartParams['extraValue'] - cb?: () => void -}) => void +export type handleSendFunc = (params: { request: AIInputEvent; optionValue?: string; cb?: () => void }) => void interface UseHookStateFunc { getContentMap: (token: string) => AIChatQSData | undefined @@ -251,8 +245,6 @@ export interface UseChatIPCState { export interface AIChatIPCStartParams { token: string params: AIInputEvent - /** 供前端处理逻辑和UI的额外参数 */ - extraValue?: CustomPluginExecuteFormValue | Record } /** 执行流途中发送消息的参数 */ @@ -261,7 +253,6 @@ export interface AIChatSendParams { type: ChatIPCSendType params: AIInputEvent optionValue?: string - extraValue?: AIChatIPCStartParams['extraValue'] } /** 任务规划的taskID和状态 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts index bdf8bb0f1f..75fb8bd1b4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts @@ -386,7 +386,7 @@ function useChatIPC(params?: UseChatIPCParams) { // #region review事件相关方法 /** review 界面选项触发事件 */ - const onSend = useMemoizedFn(({ token, type, params, optionValue, extraValue }: AIChatSendParams) => { + const onSend = useMemoizedFn(({ token, type, params, optionValue }: AIChatSendParams) => { try { if (!getExecute()) { yakitNotify('warning', 'AI 未执行任务,无法发送选项') diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts index 734302a75b..ca35189f43 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts @@ -4,6 +4,7 @@ import { globalSessionEngine } from './ChatMultiSessionController' import { AIChatIPCStartParams, AIChatSendParams } from './type' import { useMemoizedFn } from 'ahooks' import { SessionIdContext } from './explame/SessionRuntime' + const { ipcRenderer } = window.require('electron') export function useChatIPC() { @@ -14,14 +15,22 @@ export function useChatIPC() { currentSessionIdRef.current = sessionId }, [sessionId]) + // 这里一定要使用useCallback,通过依赖的sessionId变化,将上一个sessionId的on监听事件进行闭包 + // 多个sessionId就会生成多个自己的监听闭包,从而实现多会话的独立监听 const onStart = useCallback( - (sessionID: string, args: AIChatIPCStartParams, onSuccess?: () => void) => { + (args: AIChatIPCStartParams, onSuccess?: (sessionId: string) => void) => { // 监听网络,直接丢给大脑处理 ipcRenderer.on(`${sessionId}-data`, (e, res: any) => { globalSessionEngine.handleGrpcOutputEvent(sessionId, res) }) + ipcRenderer.on(`${sessionId}-error`, (e, res: any) => { + globalSessionEngine.handleSessionError(sessionId, res) + }) + ipcRenderer.on(`${sessionId}-end`, (e, res: any) => { + globalSessionEngine.handleSessionEnd(sessionId, res) + }) - globalSessionEngine.handleStartSession(sessionID, args, onSuccess) + globalSessionEngine.handleStartSession(args, onSuccess) }, [sessionId], ) From ff96d5d809203c87970b4ce31d554f01a4347296 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 24 Jun 2026 16:56:57 +0800 Subject: [PATCH 004/101] =?UTF-8?q?feat:chatipc=E7=A7=BB=E5=8A=A8=E5=88=B0?= =?UTF-8?q?=E9=A1=B6=E5=B1=82=EF=BC=9Bfeat:=E5=8E=86=E5=8F=B2=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=E6=96=B0=E7=89=88;perf:ai=E5=88=A0=E9=99=A4=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E7=B2=98=E8=B4=B4=E7=9A=84=E5=9B=BE=E7=89=87=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E4=BC=98=E5=8C=96;del:=E5=88=A0=E9=99=A4=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7onDelChats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/withHistoryAIReActChat.tsx | 23 --- .../src/main/src/pages/ai-agent/AIAgent.tsx | 8 + .../ai-agent/aiAgentChat/AIAgentChat.tsx | 180 ++++++++---------- .../hooks/useDeleteAIImageByNode.ts | 62 +++--- .../ai-agent/historyChat/HistoryChat.tsx | 44 +++-- .../HistoryChatList/HistoryChatList.tsx | 18 +- .../src/pages/ai-agent/historyChat/utils.ts | 36 ++++ .../ai-agent/useContext/AIAgentContext.ts | 20 ++ .../ai-re-act/aiReActChat/AIReActChat.tsx | 3 +- .../hooks/ChatMultiSessionController.ts | 4 +- .../ai-re-act/hooks/useCurrentSessionId.ts | 13 ++ .../ai-re-act/hooks/useGetChatDataStoreKey.ts | 42 +++- .../pages/ai-re-act/hooks/useNewChatIPC.ts | 24 ++- .../main/src/utils/eventBus/events/aiAgent.ts | 2 - 14 files changed, 295 insertions(+), 184 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentSessionId.ts diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index 4d56a67b9c..46b95c7bfe 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -375,10 +375,6 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid onGetHttpFlowFuzzStatus, onYaklangCodeChange, }) - - const imageStoreKey = useCreation(() => getChatDataStoreKey(cacheDataStore), [cacheDataStore]) - const [, { onClearImage }] = useDeleteAIImageByNode() - const { execute, casualLoading } = chatIPCData useEffect(() => { @@ -484,25 +480,6 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid aiReActChatRef.current?.setValue('') }) - const handleDelChats = useMemoizedFn((jsonString: string) => { - try { - const sessions: string[] = JSON.parse(jsonString) - if (!sessions.length || imageStoreKey === 'unknown') return - onClearImage({ - chatDataStoreKey: imageStoreKey, - sessionID: sessions, - }) - events.onDelChats(sessions) - } catch (error) {} - }) - - useEffect(() => { - emiter.on('onDelChats', handleDelChats) - return () => { - emiter.off('onDelChats', handleDelChats) - } - }, [handleDelChats]) - const onStop = useMemoizedFn(() => { if (execute && activeID) { events.onClose(activeID) diff --git a/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx b/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx index 2c98dee4c1..d07c7d3487 100644 --- a/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx @@ -29,6 +29,8 @@ import { AIBottomDetails } from './aiBottomDetails/AIBottomDetails' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { omit } from 'lodash' import { grpcDeleteAISession } from './grpc' +import { useChatIPC } from '../ai-re-act/hooks/useNewChatIPC' +import { AISourceEnum } from '../ai-re-act/hooks/grpcApi' /** 清空用户缓存的固定值 */ export const AIAgentCacheClearValue = '20260113' @@ -94,6 +96,8 @@ export const AIAgent: React.FC = (props) => { setRemoteValue(RemoteAIAgentGV.AIAgentChatSetting, JSON.stringify(cache)) }, [setting]) + const { onStart, onSend, onClose, fetchAISource } = useChatIPC({ source: AISourceEnum.aiAgent }) + const store: AIAgentContextStore = useMemo(() => { return { setting: setting, @@ -105,6 +109,10 @@ export const AIAgent: React.FC = (props) => { getSetting: getSetting, setSetting: setSetting, setActiveChat: setActiveChat, + onStart, + onSend, + onClose, + fetchAISource, } }, []) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index a3e6b62525..9150bcac6a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -1,22 +1,19 @@ -import React, { memo, useEffect, useMemo, useRef, useState } from 'react' +import React, { memo, useEffect, useRef, useState } from 'react' import { AIAgentChatMode, AIAgentChatProps, AIReActTaskChatReviewProps, HandleStartParams } from './type' -import { useCreation, useDebounceFn, useInViewport, useMap, useMemoizedFn, useSafeState, useUpdateEffect } from 'ahooks' +import { useDebounceFn, useInViewport, useMap, useMemoizedFn, useSafeState, useUpdateEffect } from 'ahooks' import emiter from '@/utils/eventBus/eventBus' import { AIAgentTriggerEventInfo } from '../aiAgentType' import useAIAgentStore from '../useContext/useStore' import { getRemoteValue, setRemoteValue } from '@/utils/kv' import { RemoteAIAgentGV } from '@/enums/aiAgent' import { isForcedSetAIModal } from '../aiModelList/utils' -import useChatIPC from '@/pages/ai-re-act/hooks/useChatIPC' import useAIAgentDispatcher from '../useContext/useDispatcher' import cloneDeep from 'lodash/cloneDeep' import { randomString } from '@/utils/randomUtil' -import ChatIPCContent, { +import { AIChatIPCSendParams, AISendConfigHotpatchParams, AISendSyncMessageParams, - ChatIPCContextDispatcher, - ChatIPCContextStore, } from '../useContext/ChatIPCContent/ChatIPCContent' import { AIReActChatReview } from '@/pages/ai-agent/components/aiReActChatReview/AIReActChatReview' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' @@ -50,6 +47,9 @@ import { AIChatContentRefProps } from '../aiChatContent/type' import { PageNodeItemProps } from '@/store/pageInfo' import { Trans } from 'react-i18next' import { AIInputWithParamsTemplate, aiInputWithParamsTemplate } from '../components/aiMilkdownInput/utils' +import { useStore } from 'zustand' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' +import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' const AIChatWelcome = React.lazy(() => import('../aiChatWelcome/AIChatWelcome')) @@ -65,7 +65,12 @@ export const AIAgentChat: React.FC = memo((props) => { const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const { activeChat } = useAIAgentStore() - const { setActiveChat, getSetting, setSetting } = useAIAgentDispatcher() + const { setActiveChat, getSetting, setSetting, onSend, onClose } = useAIAgentDispatcher() + + /** 当前对话唯一ID */ + const sessionId = useCurrentSessionId() + const { store } = globalSessionEngine.ensureSession(sessionId) + const execute = useStore(store, (state) => state.execute) const aiReActChatRef = useRef(null) const aiChatWelcomeRef = useRef(null) @@ -138,11 +143,6 @@ export const AIAgentChat: React.FC = memo((props) => { } }) - /** 当前对话唯一ID */ - const activeID = useCreation(() => { - return activeChat?.SessionID - }, [activeChat]) - // 提问结束后缓存数据 const handleChatingEnd = useMemoizedFn(() => { handleStopAfterChangeState() @@ -175,19 +175,6 @@ export const AIAgentChat: React.FC = memo((props) => { removeSyncIdInfoMap(syncID) } }) - const [chatIPCData, events] = useChatIPC({ - autoConnect: true, - onEnd: handleChatingEnd, - onTaskReview: handleShowReview, - onTaskReviewExtra: handleShowReviewExtra, - onReviewRelease: handleReleaseReview, - onTaskStart: handleTaskStart, - setSessionChatName, - onSyncIDChange, - cacheDataStore: aiChatDataStore, - getSetting, - }) - const { execute } = chatIPCData const handleStart = useMemoizedFn((value: HandleStartParams) => { setTimeout(() => { @@ -207,7 +194,7 @@ export const AIAgentChat: React.FC = memo((props) => { /**发送 IsInteractiveMessage 消息 */ const handleSendInteractiveMessage = useMemoizedFn((params: AIChatIPCSendParams, type: ChatIPCSendType) => { const { value, id, optionValue } = params - if (!activeID) return + if (!sessionId) return if (!id) return const info: AIInputEvent = { @@ -215,12 +202,12 @@ export const AIAgentChat: React.FC = memo((props) => { InteractiveId: id, InteractiveJSONInput: value, } - events.onSend({ token: activeID, type, params: info, optionValue }) + onSend({ token: sessionId, type, params: info, optionValue }) handleStopAfterChangeState() }) /**发送 IsSyncMessage 消息 */ const handleSendSyncMessage = useMemoizedFn((data: AISendSyncMessageParams) => { - if (!activeID) return + if (!sessionId) return const { syncType, SyncJsonInput, params, syncID } = data const info: AIInputEvent = { IsSyncMessage: true, @@ -230,12 +217,12 @@ export const AIAgentChat: React.FC = memo((props) => { SyncID: syncID || randomString(8), } info.SyncID && setSyncIdInfoMap(info.SyncID, true) - events.onSend({ token: activeID, type: '', params: info }) + onSend({ token: sessionId, type: '', params: info }) }) /**发送 IsConfigHotpatch 消息 */ const handleSendConfigHotpatch = useMemoizedFn((data: AISendConfigHotpatchParams) => { - if (!activeID) return + if (!sessionId) return const { hotpatchType, params, taskId } = data const info: AIInputEvent = { IsConfigHotpatch: true, @@ -245,12 +232,12 @@ export const AIAgentChat: React.FC = memo((props) => { if (!!taskId) { info.TaskId = taskId } - events.onSend({ token: activeID, type: '', params: info }) + onSend({ token: sessionId, type: '', params: info }) }) const onStop = useMemoizedFn(() => { - if (execute && activeID) { - events.onClose(activeID) + if (execute && sessionId) { + onClose([sessionId]) handleStopAfterChangeState() } }) @@ -322,7 +309,7 @@ export const AIAgentChat: React.FC = memo((props) => { useUpdateEffect(() => { onHistoryAfter() - events.onSwitchChat(activeChat?.SessionID, activeChat?.isCreate) + // events.onSwitchChat(activeChat?.SessionID, activeChat?.isCreate) }, [activeChat]) /**切换历史后的处理逻辑 */ @@ -330,19 +317,6 @@ export const AIAgentChat: React.FC = memo((props) => { if (mode === 'welcome') setMode('re-act') }) - const handleDelChats = useMemoizedFn((jsonString: string) => { - try { - const sessions: string[] = JSON.parse(jsonString) - events.onDelChats(sessions) - } catch (error) {} - }) - useEffect(() => { - emiter.on('onDelChats', handleDelChats) - return () => { - emiter.off('onDelChats', handleDelChats) - } - }, []) - //#region 使用 AI-Forge 模板/Tool 相关逻辑 const [activeTool, setActiveTool] = useState() const [replaceToolShow, setReplaceToolShow] = useState(false) @@ -369,7 +343,7 @@ export const AIAgentChat: React.FC = memo((props) => { yakitNotify('error', t('AIAgentChat.templateDataError')) return } - if (!chatIPCData.execute) { + if (!execute) { handleReplaceActiveForge(forge, useForge) } else { const m = YakitModalConfirm({ @@ -409,7 +383,7 @@ export const AIAgentChat: React.FC = memo((props) => { yakitNotify('error', t('AIAgentChat.templateDataError')) return } - if (!chatIPCData.execute) { + if (!execute) { handleReplaceActiveTool(toolValue.ID) } else { const m = YakitModalConfirm({ @@ -419,7 +393,7 @@ export const AIAgentChat: React.FC = memo((props) => { footerStyle: { padding: '0 24px 24px' }, content: (modalT) => (
- {!!chatIPCData.execute ? ( + {!!execute ? ( <> = memo((props) => { setReplaceToolShow(false) }) // #endregion - const store: ChatIPCContextStore = useCreation(() => { - return { - chatIPCData, - planReviewTreeKeywordsMap, - reviewInfo, - reviewExpand, - syncIdInfoMap, - } - }, [chatIPCData, planReviewTreeKeywordsMap, reviewInfo, reviewExpand, syncIdInfoMap]) - const dispatcher: ChatIPCContextDispatcher = useCreation(() => { - return { - chatIPCEvents: events, - handleSendCasual, - handleSendTask, - handleStop: onStop, - handleSend, - handleSendSyncMessage, - handleSendConfigHotpatch, - } - }, [events]) + // const store: ChatIPCContextStore = useCreation(() => { + // return { + // chatIPCData, + // planReviewTreeKeywordsMap, + // reviewInfo, + // reviewExpand, + // syncIdInfoMap, + // } + // }, [chatIPCData, planReviewTreeKeywordsMap, reviewInfo, reviewExpand, syncIdInfoMap]) + // const dispatcher: ChatIPCContextDispatcher = useCreation(() => { + // return { + // chatIPCEvents: events, + // handleSendCasual, + // handleSendTask, + // handleStop: onStop, + // handleSend, + // handleSendSyncMessage, + // handleSendConfigHotpatch, + // } + // }, [events]) const [visible, setVisible] = useSafeState(false) const { clearAll } = useKnowledgeBase() @@ -686,45 +660,41 @@ export const AIAgentChat: React.FC = memo((props) => { { leading: true }, ).run - const chatIPCContextValue = useMemo(() => ({ store, dispatcher }), [store, dispatcher]) - return (
- -
- {mode === 'welcome' ? ( - loading...
}> - - - ) : ( - +
+ {mode === 'welcome' ? ( + loading...
}> + + + ) : ( + + )} +
+ {activeForge && ( + + )} + {activeTool && ( + )} -
- {activeForge && ( - - )} - {activeTool && ( - - )} -
-
+
void } +export interface ClearAIImageEvents { + onData?: (progress: number) => void + onError?: () => void + onFinish?: () => void +} +export const handleClearAIImage = (params: AIClearImageParams, events?: ClearAIImageEvents) => { + const token = randomString(8) + + // 提取公共的清除监听器方法,避免内存泄漏 + const removeListeners = () => { + ipcRenderer.removeAllListeners(`delete-ai-image-progress-${token}`) + ipcRenderer.removeAllListeners(`delete-ai-image-finish-${token}`) + ipcRenderer.removeAllListeners(`delete-ai-image-err-${token}`) + } + ipcRenderer.on(`delete-ai-image-progress-${token}`, (e, progress: number) => { + events?.onData?.(progress) + }) + ipcRenderer.on(`delete-ai-image-err-${token}`, (e, err) => { + events?.onError?.() + }) + ipcRenderer.on(`delete-ai-image-finish-${token}`, (e) => { + events?.onFinish?.() + removeListeners() + }) + + const newParams: DeleteAIImageByNodeRequest = { + ...params, + token, + } + deleteAIImageByNode(newParams) +} + function useDeleteAIImageByNode( params?: UseDeleteAIImageByNodeParams, ): [UseDeleteAIImageByNodeState, UseDeleteAIImageByNodeEvents] - function useDeleteAIImageByNode(params?: UseDeleteAIImageByNodeParams) { const { isShowProgress, onFinish, onError } = params || {} const [progress, setProgress] = useState(0) const onClearImage = useMemoizedFn((params: AIClearImageParams) => { - const token = randomString(8) - - // 提取公共的清除监听器方法,避免内存泄漏 - const removeListeners = () => { - ipcRenderer.removeAllListeners(`delete-ai-image-progress-${token}`) - ipcRenderer.removeAllListeners(`delete-ai-image-finish-${token}`) - ipcRenderer.removeAllListeners(`delete-ai-image-err-${token}`) - } - ipcRenderer.on(`delete-ai-image-progress-${token}`, (e, progress: number) => { - if (isShowProgress) setProgress(progress) + handleClearAIImage(params, { + onData: (progress) => { + if (isShowProgress) setProgress(progress) + }, + onFinish: () => onFinish?.(), + onError: () => onError?.(), }) - ipcRenderer.on(`delete-ai-image-err-${token}`, (e, err) => { - onError?.() - }) - ipcRenderer.on(`delete-ai-image-finish-${token}`, (e) => { - onFinish?.() - removeListeners() - }) - - const newParams: DeleteAIImageByNodeRequest = { - ...params, - token, - } - deleteAIImageByNode(newParams) }) const state: UseDeleteAIImageByNodeState = useCreation(() => { return { diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx index 2821e777a2..4b2ee6e903 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx @@ -21,15 +21,14 @@ import { SideSettingButton } from '../aiChatWelcome/AIChatWelcome' import HistoryChatList, { DAY_MS, getChatTimestamp } from './HistoryChatList/HistoryChatList' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import useSessionList from './HistoryChatList/hook/useSessionList' -import type { AISource } from '@/pages/ai-re-act/hooks/grpcApi' +import { type AISource } from '@/pages/ai-re-act/hooks/grpcApi' import { JSONParseLog } from '@/utils/tool' import { getMainOperatorPageBodyContainer } from '@/utils/getMainOperatorPageBodyContainer' +import { handAIHistoryChatRemove } from './utils' +import { getImageStoreKeyByAISource } from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' import classNames from 'classnames' import { filterHistorySessionsBySource, getHistorySourceQuerySources, type HistorySourceFilter } from './source' -const clearLocalChats = (sessions: AISession[]) => - emiter.emit('onDelChats', JSON.stringify(sessions.map((item) => item.SessionID))) - const HISTORY_SOURCE_FILTER_OPTIONS: { key: HistorySourceFilter title: string @@ -92,6 +91,7 @@ interface HistoryChatProps { } const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { + const { setActiveChat, fetchAISource, onClose } = useAIAgentDispatcher() const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const [historySourceFilter, setHistorySourceFilter] = useState('local') const enableHistorySourceFilter = useMemo(() => aiSource.includes('im'), [aiSource]) @@ -104,7 +104,6 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { }, [aiSource, enableHistorySourceFilter, historySourceFilter]) const [{ sessions }, dispatcher] = useSessionList(historyQuerySources) const { activeChat } = useAIAgentStore() - const { setActiveChat } = useAIAgentDispatcher() const getPopupContainer = useMemoizedFn(() => getMainOperatorPageBodyContainer() || document.body) const popupContainer = embedded ? getPopupContainer : undefined const embeddedOverlayClass = styles['history-chat-embedded-overlay'] @@ -133,12 +132,17 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { setClearLoading(true) try { - if (isGlobalAIAgentHistory) { - await grpcDeleteAISession({ DeleteAll: true }, true) - } else { - await grpcDeleteAISession({ Filter: { Source: historyQuerySources } }, true) - } - clearLocalChats(visibleSessions) + const source = fetchAISource() + const filter = isGlobalAIAgentHistory ? { DeleteAll: true } : { Filter: { Source: historyQuerySources } } + await handAIHistoryChatRemove({ + grpcDeleteAISessionParams: filter, + handleClearAIImageParams: { chatDataStoreKey: getImageStoreKeyByAISource(source), sessionID: [] }, //删除全部只需要传chatDataStoreKey + forceCloseSessionParams: { + aiSource: source, + sessionIds: [], + }, + }) + onClose([]) onNewChat() setActiveChat?.(undefined) dispatcher.setSessions?.([]) @@ -175,12 +179,18 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { BeforeTimestamp: beforeTimestamp, Source: historyQuerySources, } - await grpcDeleteAISession({ Filter: filter }, true) - - clearLocalChats(deletedChats) - - const deletedSessionIds = new Set(deletedChats.map((item) => item.SessionID)) - const nextChats = sessions.filter((item) => !deletedSessionIds.has(item.SessionID)) + const sessionIds = sessions.map((item) => item.SessionID) + const source = fetchAISource() + await handAIHistoryChatRemove({ + grpcDeleteAISessionParams: { Filter: filter }, + handleClearAIImageParams: { chatDataStoreKey: getImageStoreKeyByAISource(source), sessionID: sessionIds }, + forceCloseSessionParams: { + aiSource: source, + sessionIds: sessionIds, + }, + }) + onClose(sessionIds) + const nextChats = sessions.filter((item) => getChatTimestamp(item) > beforeTimestamp) const activeDeleted = !!activeChat && deletedChats.some((item) => item.SessionID === activeChat.SessionID) if (nextChats.length === 0) { diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx index fe8ccecb23..8ed41aedca 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx @@ -10,16 +10,17 @@ import { YakitAIAgentPageID } from '../../defaultConstant' import { EditChatNameModal } from '../../UtilModals' import { AISession } from '../../type/aiChat' import { useInfiniteScroll, useMemoizedFn } from 'ahooks' -import { grpcDeleteAISession, grpcUpdateAISessionTitle } from '../../grpc' +import { grpcUpdateAISessionTitle } from '../../grpc' import useAIAgentStore from '../../useContext/useStore' import useAIAgentDispatcher from '../../useContext/useDispatcher' import { yakitNotify } from '@/utils/notification' import { onNewChat } from '../HistoryChat' -import emiter from '@/utils/eventBus/eventBus' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import type { SessionListDispatcher } from './hook/useSessionList' import type { AISource } from '@/pages/ai-re-act/hooks/grpcApi' import { getHistorySessionIconMeta, getSessionDisplayTitle } from '../source' +import { handAIHistoryChatRemove } from '../utils' +import { getImageStoreKeyByAISource } from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' export const HOUR_MS = 60 * 60 * 1000 export const DAY_MS = 24 * HOUR_MS @@ -194,6 +195,7 @@ const HistoryChatList: FC<{ editInfo.current = undefined }) + const { onClose, fetchAISource } = useAIAgentDispatcher() const handleDeleteChat = useMemoizedFn(async (info: AISession) => { const { SessionID } = info const isLoading = delLoading.includes(SessionID) @@ -220,8 +222,16 @@ const HistoryChatList: FC<{ } try { - await grpcDeleteAISession({ Filter: { SessionID: [SessionID], Source: aiSource } }, true) - emiter.emit('onDelChats', JSON.stringify([SessionID])) + const sessionIds = [SessionID] + const source = fetchAISource() + await handAIHistoryChatRemove({ + grpcDeleteAISessionParams: { Filter: { SessionID: [SessionID], Source: aiSource } }, + handleClearAIImageParams: { chatDataStoreKey: getImageStoreKeyByAISource(source), sessionID: sessionIds }, + forceCloseSessionParams: { + aiSource: source, + sessionIds: sessionIds, + }, + }) } catch (error) { setSessions?.(sessionList) if (activeSessionId === SessionID) { diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts b/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts new file mode 100644 index 0000000000..75ec70a7a9 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts @@ -0,0 +1,36 @@ +import { AISource } from '@/pages/ai-re-act/hooks/grpcApi' +import { + AIClearImageParams, + handleClearAIImage, +} from '../components/aiMilkdownInput/aiCustomFile/hooks/useDeleteAIImageByNode' +import { grpcDeleteAISession } from '../grpc' +import { DeleteAISessionRequest } from '../type/aiChat' +import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' + +export interface HandAIHistoryChatRemoveParams { + /** 删除grpc数据 */ + grpcDeleteAISessionParams: DeleteAISessionRequest + /** 删除图片数据 */ + handleClearAIImageParams: AIClearImageParams + /** 删除store缓存数据 */ + forceCloseSessionParams: { sessionIds: string[]; aiSource: AISource } +} +/** + * @description 删除历史会话数据 + * 1.删除grpc数据 + * 2.删除图片数据 + * 3.删除store缓存数据 + * 4.删除indexdb + */ +export const handAIHistoryChatRemove = async (params: HandAIHistoryChatRemoveParams) => { + try { + const { grpcDeleteAISessionParams, handleClearAIImageParams, forceCloseSessionParams } = params + // 1.删除grpc数据 + await grpcDeleteAISession(grpcDeleteAISessionParams, true) + // 2.删除图片数据 + handleClearAIImage(handleClearAIImageParams) + // 3.删除store缓存数据 + globalSessionEngine.forceCloseSession(forceCloseSessionParams) + // TODO - 4.删除indexdb + } catch (error) {} +} diff --git a/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts b/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts index 7a357936a0..afbde9d8fe 100644 --- a/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts +++ b/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts @@ -1,6 +1,8 @@ import { Dispatch, SetStateAction, createContext } from 'react' import { AIAgentSetting } from '../aiAgentType' import { AISession } from '../type/aiChat' +import { AIChatIPCStartParams, AIChatSendParams } from '@/pages/ai-re-act/hooks/type' +import { AISource } from '@/pages/ai-re-act/hooks/grpcApi' export interface AIAgentContextStore { /** 全局配置 */ @@ -9,10 +11,23 @@ export interface AIAgentContextStore { activeChat?: AISession } +export interface UseChatIPCStartParams { + args: AIChatIPCStartParams + onSuccess?: (sessionId: string) => void +} export interface AIAgentContextDispatcher { setSetting?: Dispatch> getSetting?: () => AIAgentSetting setActiveChat?: Dispatch> + + /** 开始会话 */ + onStart: (UseChatIPCStartParams) => void + /** 会话过程中向后端发送消息 */ + onSend: (params: AIChatSendParams) => void + /** 关闭指定会话 */ + onClose: (sessionId: string[]) => void + /** 获取当前ai来源 */ + fetchAISource: () => AISource } export interface AIAgentContextValue { @@ -30,5 +45,10 @@ export default createContext({ getSetting: undefined, setActiveChat: undefined, + + onStart: () => {}, + onSend: () => {}, + onClose: () => {}, + fetchAISource: () => 'ai', }, }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index 49730a206f..e05050931e 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -56,8 +56,7 @@ export const AIReActChat: React.FC = React.memo( startRequest, externalParameters, } = props - const { setActiveChat } = useAIAgentDispatcher() - + const { setActiveChat, fetchAISource } = useAIAgentDispatcher() const { chatDataStoreKey } = useGetChatDataStoreKey() const historyChatAISource = useCreation( () => getAISourceListFromChatDataStoreKey(chatDataStoreKey), diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index de33847ce9..936de25be9 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -363,7 +363,9 @@ export class ChatMultiSessionController { this.closeIPCListeners(sessionId) } - /** 关闭指定session的连接 */ + /** 关闭指定session的连接 + * TODO - 期望传一个对象,{ sessionIds: string[]; aiSource: AISource } + */ public forceCloseSession(sessionId: string) { ipcRenderer.invoke('cancel-ai-re-act', sessionId).catch(() => {}) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentSessionId.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentSessionId.ts new file mode 100644 index 0000000000..1b996eda6f --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentSessionId.ts @@ -0,0 +1,13 @@ +import { useContext } from 'react' +import AIAgentContext from '@/pages/ai-agent/useContext/AIAgentContext' + +/** + * @description 获取当前会话的 SessionID + * @returns {string} 当前会话的 SessionID,如果没有则返回空字符串 + */ +function useCurrentSessionId() { + const sessionId = useContext(AIAgentContext)?.store?.activeChat?.SessionID || '' + return sessionId +} + +export default useCurrentSessionId diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts index d7e160a453..934bcfd55e 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts @@ -12,8 +12,36 @@ import { yakRunnerPageAiStore, } from '@/pages/ai-agent/store/ChatDataStore' import type { Domain } from '@/pages/ai-agent/store/constants' -import type { AISource } from './grpcApi' +import { AISourceEnum, type AISource } from './grpcApi' +/** + * 根据后端返回的会话数据来源转换为前端对应的数据存储路径key + */ +export const getImageStoreKeyByAISource = (source: AISource): ChatDataStoreKey => { + switch (source) { + case AISourceEnum.aiAgent: + case AISourceEnum.other: + return 'aiChatDataStore' + case AISourceEnum.history: + return 'histroyAiStore' + case AISourceEnum.flow: + return 'FlowAiStore' + case AISourceEnum.knowledgeBase: + return 'knowledgeBaseDataStore' + case AISourceEnum.webFuzzer: + return 'WebFuzzerAiStore' + case AISourceEnum.irify: + return 'irifyAiCodeAuditPageAiStore' + case AISourceEnum.yakRunner: + return 'yakRunnerPageAiStore' + default: + return 'unknown' + } +} + +/** + * TODO - 待验证是否还需要此方法 + */ export const getChatDataStoreKey = (store?: ChatDataStore): ChatDataStoreKey => { switch (store) { case histroyAiStore: @@ -34,6 +62,9 @@ export const getChatDataStoreKey = (store?: ChatDataStore): ChatDataStoreKey => } } +/** + * TODO - 待验证是否还需要此方法 + */ function useGetChatDataStoreKey() { const { chatIPCEvents } = useChatIPCDispatcher() const chatDataStoreKey = useCreation((): ChatDataStoreKey => { @@ -42,6 +73,9 @@ function useGetChatDataStoreKey() { return { chatDataStoreKey } as const } +/** + * TODO - 待验证是否还需要此方法 + */ export const getAISourceFromChatDataStoreKey = (key: ChatDataStoreKey): AISource | undefined => { switch (key) { case 'histroyAiStore': @@ -63,6 +97,9 @@ export const getAISourceFromChatDataStoreKey = (key: ChatDataStoreKey): AISource } } +/** + * TODO - 待验证是否还需要此方法 + */ /** AISource 映射到 IndexedDB 消息存储域(空 source 兼容老数据,归入 ai 域;im 归入 ai 域复用) */ export const getDomainFromAISource = (source?: AISource): Domain => { if (!source) return 'ai' @@ -73,6 +110,9 @@ export const getDomainFromAISource = (source?: AISource): Domain => { /** AI Agent 侧栏历史会话:包含 ai、im 来源与兼容老数据的空 source */ export const AI_AGENT_HISTORY_AI_SOURCES: AISource[] = ['ai', 'im', ''] +/** + * TODO - 待验证是否还需要此方法 + */ /** 各业务页嵌入历史会话:仅查询对应单一 source */ export const getAISourceListFromChatDataStoreKey = (key: ChatDataStoreKey): AISource[] => { const source = getAISourceFromChatDataStoreKey(key) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts index ca35189f43..daadd408b3 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts @@ -1,14 +1,15 @@ // useChatIPC.ts -import { useEffect, useContext, useCallback, useRef } from 'react' +import { useEffect, useCallback, useRef } from 'react' import { globalSessionEngine } from './ChatMultiSessionController' import { AIChatIPCStartParams, AIChatSendParams } from './type' import { useMemoizedFn } from 'ahooks' -import { SessionIdContext } from './explame/SessionRuntime' +import useCurrentSessionId from './useCurrentSessionId' +import { AISource } from './grpcApi' const { ipcRenderer } = window.require('electron') -export function useChatIPC() { - const sessionId = useContext(SessionIdContext) +export function useChatIPC(params: { source: AISource }) { + const sessionId = useCurrentSessionId() const currentSessionIdRef = useRef(sessionId) useEffect(() => { @@ -17,6 +18,7 @@ export function useChatIPC() { // 这里一定要使用useCallback,通过依赖的sessionId变化,将上一个sessionId的on监听事件进行闭包 // 多个sessionId就会生成多个自己的监听闭包,从而实现多会话的独立监听 + // TODO - 需要AISource const onStart = useCallback( (args: AIChatIPCStartParams, onSuccess?: (sessionId: string) => void) => { // 监听网络,直接丢给大脑处理 @@ -39,6 +41,18 @@ export function useChatIPC() { globalSessionEngine.handleSendMessage(payload) }) + const onClose = useMemoizedFn((sessionId: string[]) => { + /** globalSessionEngine.forceCloseSession + * TODO - 需要AISource且支持多个会话关闭 + * TODO - sessionId传[]:清除source下的所有会话 + */ + globalSessionEngine.forceCloseSession(sessionId, params.source) + }) + + const fetchAISource = useMemoizedFn(() => { + return params.source + }) + // 组件卸载时拔插头,清理闭环 useEffect(() => { return () => { @@ -46,5 +60,5 @@ export function useChatIPC() { } }, []) - return { onStart, onSend } + return { onStart, onSend, onClose, fetchAISource } } diff --git a/app/renderer/src/main/src/utils/eventBus/events/aiAgent.ts b/app/renderer/src/main/src/utils/eventBus/events/aiAgent.ts index b325108084..82b570cd1b 100644 --- a/app/renderer/src/main/src/utils/eventBus/events/aiAgent.ts +++ b/app/renderer/src/main/src/utils/eventBus/events/aiAgent.ts @@ -38,8 +38,6 @@ export type AIAgentEventProps = { */ onRefreshAIReviewRuleSelect: string - /** 删除会话列表里的会话时, 通知useChatIPC-hook对应会话数据已被删除,不需断开保存操作 */ - onDelChats: string /** 嵌入侧栏 ReviewPolicy 变更,通知其他已挂载 Provider 同步 */ onRefreshHistoryAIEmbeddedSetting: string /** From c460279e7e121660dd5f6a2f63ac4597f4b285b4 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 24 Jun 2026 17:54:23 +0800 Subject: [PATCH 005/101] =?UTF-8?q?perf:review=E7=9B=B8=E5=85=B3=E7=9A=84?= =?UTF-8?q?=E4=B8=A4=E4=B8=AA=E5=AD=97=E6=AE=B5=E7=9A=84=E6=96=B0=E7=89=88?= =?UTF-8?q?=E7=83=AD=E6=9B=B4=E6=96=B0;del:=E5=88=A0=E9=99=A4=E4=BF=A1?= =?UTF-8?q?=E5=8F=B7:onRefreshHistoryAIEmbeddedSetting=E5=92=8ConRefreshAI?= =?UTF-8?q?ReviewRuleSelect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/withHistoryAIReActChat.tsx | 34 +++++--- .../ai-agent/AIChatSetting/AIChatSetting.tsx | 11 +-- .../ai-agent/aiAgentChat/AIAgentChat.tsx | 11 ++- .../ChatIPCContent/ChatIPCContent.ts | 2 + .../aiReviewRuleSelect/AIReviewRuleSelect.tsx | 79 ++++++++----------- .../main/src/utils/eventBus/events/aiAgent.ts | 8 -- 6 files changed, 67 insertions(+), 78 deletions(-) diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index 46b95c7bfe..b45a714e7f 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -254,23 +254,12 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid applyHistoryAIEmbeddedReviewPolicy() }, [inViewport, applyHistoryAIEmbeddedReviewPolicy]) - useEffect(() => { - const onRefreshEmbeddedSetting = () => { - applyHistoryAIEmbeddedReviewPolicy() - } - emiter.on('onRefreshHistoryAIEmbeddedSetting', onRefreshEmbeddedSetting) - return () => { - emiter.off('onRefreshHistoryAIEmbeddedSetting', onRefreshEmbeddedSetting) - } - }, [applyHistoryAIEmbeddedReviewPolicy]) - useUpdateEffect(() => { if (!embeddedSettingCacheReadyRef.current) return const policy = setting.ReviewPolicy ?? AIAgentSettingDefault.ReviewPolicy ?? 'manual' if (lastPersistedEmbeddedSettingRef.current.ReviewPolicy === policy) return setHistoryAIReviewPolicy(policy).then(() => { lastPersistedEmbeddedSettingRef.current = { ReviewPolicy: policy } - emiter.emit('onRefreshHistoryAIEmbeddedSetting', '') }) }, [setting.ReviewPolicy]) @@ -375,6 +364,10 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid onGetHttpFlowFuzzStatus, onYaklangCodeChange, }) + + const imageStoreKey = useCreation(() => getChatDataStoreKey(cacheDataStore), [cacheDataStore]) + const [, { onClearImage }] = useDeleteAIImageByNode() + const { execute, casualLoading } = chatIPCData useEffect(() => { @@ -480,6 +473,25 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid aiReActChatRef.current?.setValue('') }) + const handleDelChats = useMemoizedFn((jsonString: string) => { + try { + const sessions: string[] = JSON.parse(jsonString) + if (!sessions.length || imageStoreKey === 'unknown') return + onClearImage({ + chatDataStoreKey: imageStoreKey, + sessionID: sessions, + }) + events.onDelChats(sessions) + } catch (error) {} + }) + + useEffect(() => { + emiter.on('onDelChats', handleDelChats) + return () => { + emiter.off('onDelChats', handleDelChats) + } + }, [handleDelChats]) + const onStop = useMemoizedFn(() => { if (execute && activeID) { events.onClose(activeID) diff --git a/app/renderer/src/main/src/pages/ai-agent/AIChatSetting/AIChatSetting.tsx b/app/renderer/src/main/src/pages/ai-agent/AIChatSetting/AIChatSetting.tsx index 14395926c2..db0b6fbf79 100644 --- a/app/renderer/src/main/src/pages/ai-agent/AIChatSetting/AIChatSetting.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/AIChatSetting/AIChatSetting.tsx @@ -37,16 +37,7 @@ const AIChatSetting: React.FC = memo((props) => { form && form.setFieldsValue({ ...(setting || {}) }) }, [setting]) - const handleFormChange = useMemoizedFn((changedValues, value) => { - if (!!changedValues.ReviewPolicy) { - emiter.emit('onRefreshAIReviewRuleSelect', JSON.stringify({ reviewPolicy: changedValues.ReviewPolicy })) - } - if (changedValues.AIReviewRiskControlScore !== undefined) { - emiter.emit( - 'onRefreshAIReviewRuleSelect', - JSON.stringify({ AIReviewRiskControlScore: changedValues.AIReviewRiskControlScore }), - ) - } + const handleFormChange = useMemoizedFn((changedValues) => { setSetting && setSetting((old) => ({ ...old, ...changedValues })) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index 9150bcac6a..e00893b1b2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -181,17 +181,19 @@ export const AIAgentChat: React.FC = memo((props) => { aiReActChatRef.current?.handleStart(value) // 等自由对话渲染出来再发送 }) }) - + // @deprecated const handleSendCasual = useMemoizedFn((params: AIChatIPCSendParams) => { handleSendInteractiveMessage(params, 'casual') }) + // @deprecated const handleSendTask = useMemoizedFn((params: AIChatIPCSendParams) => { handleSendInteractiveMessage(params, 'task') }) + // @deprecated const handleSend = useMemoizedFn((params: AIChatIPCSendParams) => { handleSendInteractiveMessage(params, '') }) - /**发送 IsInteractiveMessage 消息 */ + /**发送 @deprecated IsInteractiveMessage 消息 */ const handleSendInteractiveMessage = useMemoizedFn((params: AIChatIPCSendParams, type: ChatIPCSendType) => { const { value, id, optionValue } = params if (!sessionId) return @@ -205,7 +207,7 @@ export const AIAgentChat: React.FC = memo((props) => { onSend({ token: sessionId, type, params: info, optionValue }) handleStopAfterChangeState() }) - /**发送 IsSyncMessage 消息 */ + /** @deprecated 发送 IsSyncMessage 消息 */ const handleSendSyncMessage = useMemoizedFn((data: AISendSyncMessageParams) => { if (!sessionId) return const { syncType, SyncJsonInput, params, syncID } = data @@ -220,7 +222,8 @@ export const AIAgentChat: React.FC = memo((props) => { onSend({ token: sessionId, type: '', params: info }) }) - /**发送 IsConfigHotpatch 消息 */ + /** + * @deprecated 发送 IsConfigHotpatch 消息 */ const handleSendConfigHotpatch = useMemoizedFn((data: AISendConfigHotpatchParams) => { if (!sessionId) return const { hotpatchType, params, taskId } = data diff --git a/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts b/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts index 4aae505e00..9d2fa71960 100644 --- a/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts +++ b/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts @@ -27,6 +27,8 @@ export interface AISendSyncMessageParams { params?: MakeOptional syncID?: string } + +// @deprecated export interface AISendConfigHotpatchParams { hotpatchType: AIInputEvent['HotpatchType'] params: MakeOptional diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx index 3af925326e..b21fb6154b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx @@ -4,7 +4,7 @@ import styles from './AIReviewRuleSelect.module.scss' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' import classNames from 'classnames' -import { useClickAway, useControllableValue, useCreation, useInViewport, useMemoizedFn } from 'ahooks' +import { useClickAway, useControllableValue, useCreation, useInViewport, useMemoizedFn, useUpdateEffect } from 'ahooks' import { YakitSelect } from '@/components/yakitUI/YakitSelect/YakitSelect' import { AIAgentSettingDefault, @@ -18,20 +18,22 @@ import { FormItemSlider } from '@/pages/ai-agent/AIChatSetting/AIChatSetting' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' -import { AIInputEventHotPatchTypeEnum, AIStartParams } from '../hooks/grpcApi' +import { AIInputEvent, AIInputEventHotPatchTypeEnum, AIStartParams } from '../hooks/grpcApi' import isEqual from 'lodash/isEqual' import emiter from '@/utils/eventBus/eventBus' import { JSONParseLog } from '@/utils/tool' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { isNil } from 'lodash' +import useCurrentSessionId from '../hooks/useCurrentSessionId' const AIReviewRuleSelect: React.FC = React.memo((props) => { const { t } = useI18nNamespaces(['aiAgent']) + /** 当前对话唯一ID */ + const sessionId = useCurrentSessionId() const { setting } = useAIAgentStore() - const { setSetting } = useAIAgentDispatcher() + const { setSetting, onSend } = useAIAgentDispatcher() const { chatIPCData } = useChatIPCStore() - const { handleSendConfigHotpatch } = useChatIPCDispatcher() const [visible, setVisible] = useState(false) const [open, setOpen] = useState(false) @@ -48,9 +50,6 @@ const AIReviewRuleSelect: React.FC = React.memo((props) = return modelValue }, [open, selectReviewPolicy, modelValue]) - const refRef = useRef(null) - const [inViewport = true] = useInViewport(refRef) - const aiReviewRiskControlScore = useCreation(() => { return setting?.AIReviewRiskControlScore || AIAgentSettingDefault.AIReviewRiskControlScore }, [setting?.AIReviewRiskControlScore]) @@ -67,51 +66,41 @@ const AIReviewRuleSelect: React.FC = React.memo((props) = } }, [aiReviewRiskControlScore, visible]) - const onRefreshAIReviewRuleSelect = useMemoizedFn((res: string) => { - if (!chatIPCData.execute) return - const data = JSONParseLog(res) - if (!!data?.reviewPolicy) { - handHotpatchReviewPolicy(data?.reviewPolicy as AIStartParams['ReviewPolicy']) - } - if (!!data?.aiReviewRiskControlScore) { - handHotpatchAIReviewRiskControlScore(data?.aiReviewRiskControlScore) - } - }) - - const onRefreshHistoryAIEmbeddedSetting = useMemoizedFn(() => { - if (open) return - setSelectReviewPolicy(undefined) - if (visible) return - setAIReviewRiskControlScore(undefined) - }) - - useEffect(() => { - if (!inViewport) return - emiter.on('onRefreshAIReviewRuleSelect', onRefreshAIReviewRuleSelect) - emiter.on('onRefreshHistoryAIEmbeddedSetting', onRefreshHistoryAIEmbeddedSetting) - return () => { - emiter.off('onRefreshAIReviewRuleSelect', onRefreshAIReviewRuleSelect) - emiter.off('onRefreshHistoryAIEmbeddedSetting', onRefreshHistoryAIEmbeddedSetting) - } - }, [inViewport, onRefreshAIReviewRuleSelect, onRefreshHistoryAIEmbeddedSetting]) - + //#region 热更新 Review 规则 + useUpdateEffect(() => { + if (!!setting.ReviewPolicy) handHotpatchReviewPolicy(setting.ReviewPolicy) + }, [setting.ReviewPolicy]) + /** 热更新 Review 规则 */ const handHotpatchReviewPolicy = useMemoizedFn((value: AIStartParams['ReviewPolicy']) => { - handleSendConfigHotpatch({ - hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_AgreePolicy, - params: { + if (!sessionId) return + const info: AIInputEvent = { + IsConfigHotpatch: true, + HotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_AgreePolicy, + Params: { ReviewPolicy: value, }, - }) + } + onSend({ token: sessionId, type: '', params: info }) }) + //#endregion + //#region 热更新 AIReviewRiskControlScore + useUpdateEffect(() => { + if (!!setting.AIReviewRiskControlScore) handHotpatchAIReviewRiskControlScore(setting.AIReviewRiskControlScore) + }, [setting.AIReviewRiskControlScore]) + /** 热更新 AIReviewRiskControlScore */ const handHotpatchAIReviewRiskControlScore = useMemoizedFn((value: number) => { - handleSendConfigHotpatch({ - hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_RiskControlScore, - params: { - AIReviewRiskControlScore: value, + if (!sessionId) return + const info: AIInputEvent = { + IsConfigHotpatch: true, + HotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_RiskControlScore, + Params: { + AIReviewRiskControlScore: value ?? 0, }, - }) + } + onSend({ token: sessionId, type: '', params: info }) }) + //#endregion const onSelectModel = useMemoizedFn((value: AIStartParams['ReviewPolicy']) => { setSetting && setSetting((old) => ({ ...old, ReviewPolicy: value })) @@ -187,7 +176,7 @@ const AIReviewRuleSelect: React.FC = React.memo((props) = ) }) return ( -
+
{ diff --git a/app/renderer/src/main/src/utils/eventBus/events/aiAgent.ts b/app/renderer/src/main/src/utils/eventBus/events/aiAgent.ts index 82b570cd1b..bc466a31e2 100644 --- a/app/renderer/src/main/src/utils/eventBus/events/aiAgent.ts +++ b/app/renderer/src/main/src/utils/eventBus/events/aiAgent.ts @@ -32,14 +32,6 @@ export type AIAgentEventProps = { /** 文件系统默认展开路径 */ fileSystemDefaultExpand: string - /** AIReviewRule相关数据发生编发,是否热更新 - * @name reviewPolicy {AIStartParams["ReviewPolicy"] - * @name aiReviewRiskControlScore {AIStartParams["AIReviewRiskControlScore"] - */ - onRefreshAIReviewRuleSelect: string - - /** 嵌入侧栏 ReviewPolicy 变更,通知其他已挂载 Provider 同步 */ - onRefreshHistoryAIEmbeddedSetting: string /** * 会话数据管理 * @name type "refresh" | "clear" | "prependSession" | "updateSession" | "loadNextPage" From 4606e9825465c484fac999e94e3143a3325eecd3 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 24 Jun 2026 17:56:54 +0800 Subject: [PATCH 006/101] =?UTF-8?q?del:=E5=88=A0=E9=99=A4=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E4=BD=BF=E7=94=A8=E7=9A=84=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/renderer/src/main/src/pages/ai-agent/AIAgentSideList.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/AIAgentSideList.tsx b/app/renderer/src/main/src/pages/ai-agent/AIAgentSideList.tsx index ccaac441a5..2fb8653b63 100644 --- a/app/renderer/src/main/src/pages/ai-agent/AIAgentSideList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/AIAgentSideList.tsx @@ -12,8 +12,6 @@ import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { AI_AGENT_HISTORY_AI_SOURCES } from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' const AIChatSetting = React.lazy(() => import('./AIChatSetting/AIChatSetting')) -const ForgeName = React.lazy(() => import('./forgeName/ForgeName')) -const AIToolList = React.lazy(() => import('./aiToolList/AIToolList')) const AIModelList = React.lazy(() => import('./aiModelList/AIModelList')) const HistoryChat = React.lazy(() => import('./historyChat/HistoryChat')) const AIMCP = React.lazy(() => import('./aiMCP/AIMCP')) From 048e676d9465a9a6c326b3db3bc871295da0d14d Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Thu, 25 Jun 2026 17:04:55 +0800 Subject: [PATCH 007/101] =?UTF-8?q?feat:=201.=20=E5=B0=86=E8=87=AA?= =?UTF-8?q?=E7=94=B1=E5=AF=B9=E8=AF=9Dreview=E6=94=BE=E5=85=A5=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=9C=BA=E3=80=81=E5=B0=86=E4=BB=BB=E5=8A=A1=E8=A7=84?= =?UTF-8?q?=E5=88=92=E5=BC=82=E6=AD=A5=E6=89=A9=E5=B1=95=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E6=94=BE=E5=85=A5meta=E9=87=8C=EF=BC=8C=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=9C=BA=E6=96=B0=E5=A2=9E=E4=B8=80=E4=B8=AAupdate:number?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E6=95=B0=E6=8D=AE=E6=9B=B4=E6=96=B0=202.=20?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E6=9C=BA=E6=96=B0=E5=A2=9E=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=85=83=E7=B4=A0=E7=9A=84=E6=96=B9=E6=B3=95(=E6=9C=AA?= =?UTF-8?q?=E5=AE=8C=E6=88=90)=203.=20=E6=9B=B4=E6=96=B0=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E7=9A=84request=E6=95=B0=E6=8D=AE=E6=96=B9=E6=B3=95=EF=BC=8C?= =?UTF-8?q?=E5=B1=8F=E8=94=BD=E4=BA=86source=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/type/aiChat.d.ts | 5 +- .../hooks/ChatMultiSessionController.ts | 119 +++-- .../src/pages/ai-re-act/hooks/aiRender.ts | 21 +- .../src/pages/ai-re-act/hooks/chatStore.ts | 46 +- .../hooks/grpcStreamHandler/aiReview.ts | 410 ++++++++---------- 5 files changed, 319 insertions(+), 282 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 953e165b45..9e28b074ae 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -239,10 +239,9 @@ export interface AIAgentChatMetaData { /** 历史数据: review_release先出现的历史review数据的id-release */ historyReviewReleaseID: Record + /** 当前plan_review对应的扩展数据ID */ - currentPlanReviewId: string - /** 当前进行中的review数据(包含自由对话和任务规划) */ - reviewData: Map + currentPlanReviewExtraId: string /** 当前plan_review的异步详细数据 */ planReviewExtraData: Map diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 936de25be9..284e3e4cec 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -108,8 +108,7 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { currentTaskPlanID: undefined, currentTaskPlanActiveNode: new Set(), historyReviewReleaseID: {}, - currentPlanReviewId: '', - reviewData: new Map(), + currentPlanReviewExtraId: '', planReviewExtraData: new Map(), toolStderrStreamData: new Map(), systemEventUUID: [], @@ -157,9 +156,13 @@ export class ChatMultiSessionController { return this.activeShowSession === sessionId } - /** 更新指定会话的配置参数 */ - public updateSessionConfig(sessionId: string, config: Partial) { + /** + * 更新指定会话的配置参数 + * Source 字段连接会话时锁死,后续不允许热更新 + */ + public updateSessionConfig(sessionId: string, config: Partial>) { const { request } = this.ensureSession(sessionId) + delete config['Source'] Object.assign(request, config) } @@ -200,61 +203,97 @@ export class ChatMultiSessionController { return } - const { store, rawData, meta } = this.ensureSession(token) - if (params.IsFreeInput) { + const { store, rawData } = this.ensureSession(token) + // 自由对话没有问题进行中时,才改变loading的title + if (params.IsFreeInput && !store.getState().casualLoading) { store.getState().updateState({ casualTitle: '等待回复中...' }) } switch (type) { case 'casual': if (params.IsInteractiveMessage && params.InteractiveId) { - const review = meta.reviewData.get(params.InteractiveId) - if (!review) { + const isExist = store.getState().currentCasualReview.includes(params.InteractiveId) + const review = rawData.contents.get(params.InteractiveId) + if (!isExist || !review) { yakitNotify('error', '未获取到 review 信息, 操作无效') return } - ;(review.data as AIReviewType).selected = params.InteractiveJSONInput - ;(review.data as AIReviewType).optionValue = optionValue - - const chatData = cloneDeep(review) - meta.reviewData.delete(params.InteractiveId) - rawData.contents.set(params.InteractiveId, chatData) - store.getState().incrementNodeVersion(params.InteractiveId, 'item') + store.getState().updateCasualReview(params.InteractiveId, 'remove') + switch (review.type) { + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + // review操作后不展示在UI上,直接删除 + rawData.contents.delete(review.id) + store.getState().deleteListElement('reAct', review.id) + break + case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + // review操作后正常展示在UI上 + review.data.selected = params.InteractiveJSONInput + review.data.optionValue = optionValue + store.getState().incrementNodeVersion(review.id, 'item') + break + default: + break + } } break case 'task': if (params.IsInteractiveMessage && params.InteractiveId) { - const review = meta.reviewData.get(params.InteractiveId) - if (!review) { + const isExist = store.getState().currentPlanReviewToken === params.InteractiveId + const review = rawData.contents.get(params.InteractiveId) + if (!isExist || !review) { yakitNotify('error', '未获取到 review 信息, 操作无效') return } - ;(review.data as AIReviewType).selected = params.InteractiveJSONInput - ;(review.data as AIReviewType).optionValue = optionValue - - const chatData = cloneDeep(review) - if (chatData.type === AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { - const tasks = chatData.data - const plans = genExecTasks(tasks.plans.root_task) - store.getState().updatePlanTree({ - task_tree: cloneDeep(plans), - root_task_name: tasks.plans.root_task.name, - }) + store.getState().updateState({ currentPlanReviewToken: '' }) + switch (review.type) { + case AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP: + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + // review操作后不展示在UI上,直接删除 + rawData.contents.delete(review.id) + store.getState().deleteListElement('reAct', review.id) + break + case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + // review操作后正常展示在UI上 + review.data.selected = params.InteractiveJSONInput + review.data.optionValue = optionValue + store.getState().dispatchStreamingNode({ + chatType: 'task', + node: { + token: review.id, + kind: 'item', + type: review.type, + dataOrigin: 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + break + case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: + review.data.selected = params.InteractiveJSONInput + review.data.optionValue = optionValue + const tasks = review.data + const plans = genExecTasks(tasks.plans.root_task) + store.getState().updatePlanTree({ + task_tree: cloneDeep(plans), + root_task_name: tasks.plans.root_task.name, + }) + store.getState().dispatchStreamingNode({ + chatType: 'task', + node: { + token: review.id, + kind: 'item', + type: review.type, + dataOrigin: 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + break + default: + break } - meta.reviewData.delete(params.InteractiveId) - rawData.contents.set(params.InteractiveId, chatData) - store.getState().dispatchStreamingNode({ - chatType: 'reAct', - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - dataOrigin: 'grpc_realtime_data', - }, - groupTokenGenerator: () => '', - }) } break diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 4a1e755459..084f0cd995 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -511,8 +511,13 @@ export interface ChatStoreState { showPlanList: boolean /** 任务规划的loading状态信息 */ taskStatus: PlanLoadingStatus + + /** 自由对话的当前review(未操作) */ + currentCasualReview: string[] /** 任务规划当前显示的review数据 */ - currentPlanReviewData: AIChatQSData | undefined + currentPlanReviewToken: string + /** 当前review是plan时,异步数据的更新版本 */ + currentPlanReviewExtraUpdate: number items: Record groups: Record @@ -549,7 +554,8 @@ export interface ChatStoreState { | 'sessionTitleUpdate' | 'memoryListUpdate' | 'updateSystemStream' - | 'yaklangCodeChangeUpdate', + | 'yaklangCodeChangeUpdate' + | 'currentPlanReviewExtraUpdate', ) => void updateFolders: (info: AIFileSystemPin) => void @@ -577,6 +583,8 @@ export interface ChatStoreState { | 'riskTabUpdate' | 'grpcFolders' | 'reActTimelines' + | 'currentCasualReview' + | 'currentPlanReviewExtraUpdate' | 'items' | 'groups' | 'tasks' @@ -586,9 +594,13 @@ export interface ChatStoreState { >, ) => void + updateTaskLoadingStatus: (status: Partial) => void + + /** 正在等待用户操作的reviewId列表 */ + updateCasualReview: (id: string, status: 'add' | 'remove') => void + /** 更新自由对话列表的todoList,真实数据存放在内存池中 */ updateCasualTodoList: () => void - updateTaskLoadingStatus: (status: Partial) => void updatePlanTree: (planTree: CurrentExecTaskTree) => void /** 更新 每个工具执行过程中-文件的操作记录 */ @@ -608,4 +620,7 @@ export interface ChatStoreState { groupTokenGenerator: () => string }) => void incrementNodeVersion: (token: string, kind: 'item' | 'group' | 'task') => void + + /** 删除指定列表的某项元素 */ + deleteListElement: (chatType: ChatListRenderType, token: string) => void } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index f19b9ce5d6..99e066c7fb 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -40,7 +40,10 @@ export const createChatStore = () => { focusMode: '', showPlanList: false, taskStatus: cloneDeep(DefaultPlanLoadingStatus), - currentPlanReviewData: undefined, + + currentCasualReview: [], + currentPlanReviewToken: '', + currentPlanReviewExtraUpdate: 0, items: {}, groups: {}, @@ -103,15 +106,27 @@ export const createChatStore = () => { Object.assign(state, partial) }), + updateTaskLoadingStatus: (partial) => + set((state) => { + Object.assign(state.taskStatus, partial) + }), + + updateCasualReview: (id: string, status: 'add' | 'remove') => + set((state) => { + if (status === 'add') { + if (!state.currentCasualReview.includes(id)) state.currentCasualReview.push(id) + } else if (status === 'remove') { + if (state.currentCasualReview.includes(id)) { + state.currentCasualReview = state.currentCasualReview.filter((item) => item !== id) + } + } + }), + updateCasualTodoList: () => { set((state) => { state.casualChat.todoListUpdate += 1 }) }, - updateTaskLoadingStatus: (partial) => - set((state) => { - Object.assign(state.taskStatus, partial) - }), updatePlanTree: (planTree: CurrentExecTaskTree) => set((state) => { Object.assign(state.taskChat.plan, planTree) @@ -202,6 +217,27 @@ export const createChatStore = () => { if (kind === 'group' && state.groups[token]) state.groups[token].renderNum += 1 if (kind === 'task' && state.tasks[token]) state.tasks[token].renderNum += 1 }), + + deleteListElement: (chatType, token) => + set((state) => { + const taskItem = state.tasks[token] + if (taskItem) { + } + const groupItem = state.groups[token] + if (groupItem) { + state.groups[token].childrenTokens = state.groups[token].childrenTokens.filter((item) => item !== token) + } + const itemItem = state.items[token] + if (itemItem) { + state.items[token].childrenTokens = state.items[token].childrenTokens.filter((item) => item !== token) + } + + if (chatType === 'reAct') { + state.casualChat.elements = state.casualChat.elements.filter((item) => item.token !== token) + } else { + state.taskChat.elements = state.taskChat.elements.filter((item) => item.token !== token) + } + }), })), ) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index 36b06e9b57..43bab286ac 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -8,7 +8,7 @@ import { genExecTasks, isAutoExecuteReviewContinue, } from '../utils' -import { type AIChatQSData, AIChatQSDataTypeEnum, type AIReviewType } from '../aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { AIReviewJudgeLevelMap } from '../defaultConstant' @@ -72,32 +72,29 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) chatData.data.optionValue = 'continue' } + if (chatType !== 'task') return // 将数据存入hook里的缓存变量中 - if (!isAuto) meta.reviewData.set(chatData.id, cloneDeep(chatData)) - if (chatType === 'task') { - // 该类型的实时数据只有任务规划才有 - if (isAuto) { - rawData.contents.set(chatData.id, cloneDeep(chatData)) - store.getState().dispatchStreamingNode({ - chatType: chatType, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', - }, - groupTokenGenerator: () => '', - }) - const tasks = chatData.data - const plans = genExecTasks(tasks.plans.root_task) - store.getState().updatePlanTree({ - task_tree: cloneDeep(plans), - root_task_name: tasks.plans.root_task.name, - }) - } else { - meta.currentPlanReviewId = '' - store.getState().updateState({ currentPlanReviewData: cloneDeep(chatData) }) - } + rawData.contents.set(chatData.id, cloneDeep(chatData)) + // 该类型的实时数据只有任务规划才有 + if (isAuto) { + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + const tasks = chatData.data + const plans = genExecTasks(tasks.plans.root_task) + store.getState().updatePlanTree({ + task_tree: cloneDeep(plans), + root_task_name: tasks.plans.root_task.name, + }) + } else { + store.getState().updateState({ currentPlanReviewToken: chatData.id }) } } const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { @@ -108,37 +105,40 @@ const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequireExtra - const reviewDetail = meta.reviewData.get(data.plans_id) - if (!reviewDetail || reviewDetail.type !== AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据异常: 未找到对应plan_review_require数据`), - ) - return - } - if ( !data?.plans_id || !data?.index || !data?.keywords?.length || - (meta.currentPlanReviewId && meta.currentPlanReviewId !== data.plans_id) + (meta.currentPlanReviewExtraId && meta.currentPlanReviewExtraId !== data.plans_id) ) { handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) return } - if (!meta.currentPlanReviewId) meta.currentPlanReviewId = data.plans_id + const reviewDetail = rawData.contents.get(store.getState().currentPlanReviewToken) + if (!reviewDetail || reviewDetail.type !== AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据异常: 未找到对应plan_review_require数据`), + ) + return + } + + if (!meta.currentPlanReviewExtraId) meta.currentPlanReviewExtraId = data.plans_id const reviewInfo = reviewDetail.data if (!reviewInfo.taskExtra) reviewInfo.taskExtra = new Map() reviewInfo.taskExtra.set(data.index, data) const isAuto = isAutoExecuteReviewContinue({ getFunc: () => request }) - if (!isAuto) meta.planReviewExtraData.set(data.index, cloneDeep(data)) + if (!isAuto) { + meta.planReviewExtraData.set(data.index, cloneDeep(data)) + store.getState().updateStateCount('currentPlanReviewExtraUpdate') + } } const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, request, meta, pushLog, sendRequest } = requestInfo if (res.Type !== 'task_review_require') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -161,28 +161,8 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { isExist: (key) => rawData.contents.has(key), }), } - if (res.IsSync) { - // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 - const target = meta.historyReviewReleaseID[data.id] - if (target) { - chatData.data.selected = JSON.stringify(target.params) - chatData.data.optionValue = target.params?.suggestion || 'continue' - } - rawData.contents.set(chatData.id, cloneDeep(chatData)) - if (target) { - store.getState().dispatchStreamingNode({ - chatType: chatType, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', - }, - groupTokenGenerator: () => '', - }) - } - return - } + // 历史数据-task-review数据不进行展示 + if (res.IsSync) return // 实时数据处理逻辑 const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) @@ -190,27 +170,23 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) chatData.data.optionValue = 'continue' } + // 实时数据-task-review-自动执行,不展示在UI上 + if (isAuto) return + // 将数据存入hook里的缓存变量中 - if (!isAuto) meta.reviewData.set(chatData.id, cloneDeep(chatData)) + rawData.contents.set(chatData.id, cloneDeep(chatData)) if (chatType === 'task') { - if (isAuto) { - rawData.contents.set(chatData.id, cloneDeep(chatData)) - store.getState().dispatchStreamingNode({ - chatType: chatType, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', - }, - groupTokenGenerator: () => '', - }) - } else { - // 有问题,需要调整 - store.getState().updateState({ currentPlanReviewData: cloneDeep(chatData) }) - } + // 实时-任务规划里, 自动请求执行 + // const info: AIInputEvent = { + // IsInteractiveMessage: true, + // InteractiveId: chatData.id, + // InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), + // } + // sendRequest(info) + // rawData.contents.delete(chatData.id) + store.getState().updateState({ currentPlanReviewToken: chatData.id }) } else if (chatType === 'reAct') { - rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ chatType: chatType, node: { @@ -248,29 +224,8 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { isExist: (key) => rawData.contents.has(key), }), } - if (res.IsSync) { - // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 - const target = meta.historyReviewReleaseID[data.id] - if (target) { - chatData.data.selected = JSON.stringify(target.params) - chatData.data.optionValue = target.params?.suggestion || 'continue' - } - rawData.contents.set(chatData.id, cloneDeep(chatData)) - if (target) { - store.getState().dispatchStreamingNode({ - chatType: chatType, - parentTaskId: chatData.taskIndex, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', - }, - groupTokenGenerator: () => '', - }) - } - return - } + // 历史数据-task-review数据不进行展示 + if (res.IsSync) return // 实时数据处理逻辑 const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) @@ -278,27 +233,23 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) chatData.data.optionValue = 'continue' } + // 实时数据-task-review-自动执行,不展示在UI上 + if (isAuto) return + // 将数据存入hook里的缓存变量中 - if (!isAuto) meta.reviewData.set(chatData.id, cloneDeep(chatData)) + rawData.contents.set(chatData.id, cloneDeep(chatData)) if (chatType === 'task') { - if (isAuto) { - rawData.contents.set(chatData.id, cloneDeep(chatData)) - store.getState().dispatchStreamingNode({ - chatType: chatType, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', - }, - groupTokenGenerator: () => '', - }) - } else { - // 有问题,需要调整 - store.getState().updateState({ currentPlanReviewData: cloneDeep(chatData) }) - } + // 后面要改成自动请求执行 + // const info: AIInputEvent = { + // IsInteractiveMessage: true, + // InteractiveId: chatData.id, + // InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), + // } + // sendRequest(info) + // rawData.contents.delete(chatData.id) + store.getState().updateState({ currentPlanReviewToken: chatData.id }) } else if (chatType === 'reAct') { - rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ chatType: chatType, node: { @@ -313,7 +264,7 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { } const handleUserInteractive: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, meta, pushLog } = requestInfo if (res.Type !== 'require_user_interactive') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -361,12 +312,10 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { } // 将数据存入hook里的缓存变量中 - meta.reviewData.set(chatData.id, cloneDeep(chatData)) + rawData.contents.set(chatData.id, cloneDeep(chatData)) if (chatType === 'task') { - // 有问题,需要调整 - store.getState().updateState({ currentPlanReviewData: cloneDeep(chatData) }) + store.getState().updateState({ currentPlanReviewToken: chatData.id }) } else if (chatType === 'reAct') { - rawData.contents.set(chatData.id, cloneDeep(chatData)) store.getState().dispatchStreamingNode({ chatType: chatType, node: { @@ -382,9 +331,9 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + if (res.Type !== 'exec_aiforge_review_require') return // 任务规划不存在该类型数据 if (chatType === 'task') return - if (res.Type !== 'exec_aiforge_review_require') return const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ExecForgeReview @@ -437,7 +386,6 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { chatData.data.optionValue = 'continue' } // 将数据存入hook里的缓存变量中 - if (!isAuto) meta.reviewData.set(chatData.id, cloneDeep(chatData)) rawData.contents.set(chatData.id, cloneDeep(chatData)) store.getState().dispatchStreamingNode({ chatType: chatType, @@ -453,13 +401,17 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { } const handleAIReviewJudgement: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + const { res, store, rawData, pushLog } = requestInfo if (!['ai_review_start', 'ai_review_countdown', 'ai_review_end'].includes(res.Type)) return if (res.IsSync) return const ipcContent = Uint8ArrayToString(res.Content) || '' const score = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewJudgement - const reviewDetail = meta.reviewData.get(score.interactive_id) + if (!score?.interactive_id) { + handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + return + } + const reviewDetail = rawData.contents.get(score.interactive_id) if (!reviewDetail || reviewDetail.id !== score.interactive_id) { handleErrorGRPCToLog( res.IsSync, @@ -468,76 +420,37 @@ const handleAIReviewJudgement: AIMessageHandler = (requestInfo) => { ) return } - if (!score?.interactive_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) - return - } score.levelLabel = AIReviewJudgeLevelMap[score?.level || '']?.label || undefined - if (chatType === 'task') { - if (reviewDetail.type !== AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据异常(interactive_id:${score?.interactive_id || '-'})未找到对应review`, - ), - ) - return - } - if ( - !reviewDetail.data.aiReview || - (reviewDetail.data.aiReview && typeof reviewDetail.data.aiReview.seconds === 'undefined') - ) { - // aiReview 没有或者 aiReview 的 seconds 为空时可以赋值 - reviewDetail.data.aiReview = cloneDeep(score) - } - const isAuto = isAutoExecuteReviewContinue({ getFunc: () => request }) - if (!isAuto) store.getState().incrementNodeVersion(reviewDetail.id, 'item') - } else if (chatType === 'reAct') { - if ( - reviewDetail.type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || - reviewDetail.type === AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE - ) { + switch (reviewDetail.type) { + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: if ( !reviewDetail.data.aiReview || (reviewDetail.data.aiReview && typeof reviewDetail.data.aiReview.seconds === 'undefined') ) { + // aiReview 没有或者 aiReview 的 seconds 为空时可以赋值 reviewDetail.data.aiReview = cloneDeep(score) } - } + store.getState().incrementNodeVersion(reviewDetail.id, 'item') + break - const chatData = rawData.contents.get(score.interactive_id) - if ( - chatData && - (chatData.type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || - chatData.type === AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE) - ) { - if ( - !chatData.data.aiReview || - (chatData.data.aiReview && typeof chatData.data.aiReview.seconds === 'undefined') - ) { - // aiReview 没有或者 aiReview 的 seconds 为空时可以赋值 - chatData.data.aiReview = cloneDeep(score) - store.getState().incrementNodeVersion(reviewDetail.id, 'item') - } - } else { + default: handleErrorGRPCToLog( res.IsSync, pushLog, genErrorLogData( res.Timestamp, - `${res.Type}数据(interactive_id:${score?.interactive_id || '-'})未找到对应review`, + `${res.Type}数据异常(interactive_id:${score?.interactive_id || '-'})未找到对应review`, ), ) - } + break } } const handleReviewRelease: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, meta, pushLog } = requestInfo if (res.Type !== 'review_release') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -547,72 +460,107 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { return } + const reviewDetail = rawData.contents.get(data.id) + if (res.IsSync) { - const reviewDetail = rawData.contents.get(data.id) if (!reviewDetail) { meta.historyReviewReleaseID[data.id] = data return } switch (reviewDetail.type) { - case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: - case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: - case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: reviewDetail.data.selected = JSON.stringify(data.params) reviewDetail.data.optionValue = data.params?.suggestion || 'continue' - store.getState().incrementNodeVersion(reviewDetail.id, 'item') - return + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: reviewDetail.id, + kind: 'item', + type: reviewDetail.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + break + case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + // 历史数据还没有渲染在UI上,所以不需要从UI中删除的逻辑 + rawData.contents.delete(reviewDetail.id) + break + default: + break } - } else { - const reviewDetail = meta.reviewData.get(data.id) - if (!reviewDetail) return - const reviewInfo = cloneDeep(reviewDetail?.data) as AIReviewType - if (reviewInfo?.id !== data.id) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(id:${data?.id || '-'})和当前展示review数据(id:${reviewInfo?.id || '-'})不匹配`, - ), - ) - return - } + return + } - reviewInfo.selected = JSON.stringify({ suggestion: 'continue' }) - reviewInfo.optionValue = 'continue' - const chatData: AIChatQSData = { - ...reviewDetail, - data: reviewInfo as any, - } - meta.reviewData.delete(data.id) - if (chatType === 'task') { - meta.currentPlanReviewId = '' - // review自动释放后,还需进行的额外逻辑处理 - if (reviewDetail.type === AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { - const tasks = reviewDetail.data - const plans = genExecTasks(tasks.plans.root_task) - store.getState().updatePlanTree({ - task_tree: cloneDeep(plans), - root_task_name: tasks.plans.root_task.name, - }) + if (!reviewDetail) { + handleErrorGRPCToLog( + res.IsSync, + pushLog, + genErrorLogData(res.Timestamp, `${res.Type}数据(id:${data?.id || '-'})没有对应的review数据`), + ) + return + } + switch (reviewDetail.type) { + case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: + reviewDetail.data.selected = JSON.stringify(data.params) + reviewDetail.data.optionValue = data.params?.suggestion || 'continue' + // 清空plan-review的异步拓展信息 + meta.currentPlanReviewExtraId = '' + meta.planReviewExtraData.clear() + // 生成执行的任务树并更新到UI上 + const tasks = reviewDetail.data + const plans = genExecTasks(tasks.plans.root_task) + store.getState().updatePlanTree({ + task_tree: cloneDeep(plans), + root_task_name: tasks.plans.root_task.name, + }) + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: reviewDetail.id, + kind: 'item', + type: reviewDetail.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + break + case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + reviewDetail.data.selected = JSON.stringify(data.params) + reviewDetail.data.optionValue = data.params?.suggestion || 'continue' + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: reviewDetail.id, + kind: 'item', + type: reviewDetail.type, + dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + break + case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + let isDelete = false + if (chatType === 'reAct') { + isDelete = store.getState().currentCasualReview.includes(reviewDetail.id) + store.getState().updateCasualReview(reviewDetail.id, 'remove') + } else if (chatType === 'task') { + isDelete = store.getState().currentPlanReviewToken === reviewDetail.id + store.getState().updateState({ currentPlanReviewToken: '' }) } - } - rawData.contents.set(chatData.id, cloneDeep(chatData)) - store.getState().dispatchStreamingNode({ - chatType: chatType, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', - }, - groupTokenGenerator: () => '', - }) - - // 是否要释放掉控制review弹窗的状态变量??? + if (isDelete) { + rawData.contents.delete(reviewDetail.id) + store.getState().deleteListElement(chatType, reviewDetail.id) + } + break + default: + break } } From ac10f574f83829c3fa73199b9739ecdcc35089aa Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Thu, 25 Jun 2026 11:05:24 +0800 Subject: [PATCH 008/101] =?UTF-8?q?perf:=E6=AF=8F=E4=B8=AAai=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E7=9A=84=E6=9D=A5=E6=BA=90=E6=94=B9=E4=B8=BA=E4=BB=8E?= =?UTF-8?q?setting=E4=B8=AD=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/src/components/HTTPHistory.tsx | 3 +- .../src/components/HistroryAIReActChat.tsx | 5 ++ .../src/components/withHistoryAIReActChat.tsx | 50 +++++-------------- .../compoment/KnowledgeBaseContent.tsx | 5 +- .../src/main/src/pages/ai-agent/AIAgent.tsx | 4 +- .../src/pages/ai-agent/defaultConstant.tsx | 2 +- .../ai-agent/historyChat/HistoryChat.tsx | 6 +-- .../HistoryChatList/HistoryChatList.tsx | 4 +- .../ai-agent/useContext/AIAgentContext.ts | 19 +++---- .../ai-re-act/aiReActChat/AIReActChat.tsx | 18 ++++--- .../main/src/pages/ai-re-act/hooks/grpcApi.ts | 2 +- .../pages/ai-re-act/hooks/useNewChatIPC.ts | 8 +-- .../main/src/pages/fuzzer/HTTPFuzzerPage.tsx | 14 ++---- .../HTTPHistory/HTTPHistoryFilter.tsx | 9 ++-- .../irifyAiCodeAudit/IrifyAiCodeAuditPage.tsx | 5 +- .../main/src/pages/yakRunner/YakRunner.tsx | 4 +- 16 files changed, 63 insertions(+), 95 deletions(-) diff --git a/app/renderer/src/main/src/components/HTTPHistory.tsx b/app/renderer/src/main/src/components/HTTPHistory.tsx index c3f36572d2..aacdf71557 100644 --- a/app/renderer/src/main/src/components/HTTPHistory.tsx +++ b/app/renderer/src/main/src/components/HTTPHistory.tsx @@ -87,6 +87,7 @@ import { FiltersItemProps } from './TableVirtualResize/TableVirtualResizeType' import { HTTPFlowRuleDataFilter } from './HTTPFlowTable/HTTPFlowRuleDataFilter' import { useCampare } from '@/hook/useCompare/useCompare' import { useBuiltinTagList } from './HTTPFlowTable/useBuiltinTagList' +import { AISourceEnum } from '@/pages/ai-re-act/hooks/grpcApi' const { ipcRenderer } = window.require('electron') const { YakitPanel } = YakitCollapse @@ -444,7 +445,7 @@ const HTTPHistoryInner: React.FC = (props) => { export const HTTPHistory: React.FC = (props) => { return ( - + ) diff --git a/app/renderer/src/main/src/components/HistroryAIReActChat.tsx b/app/renderer/src/main/src/components/HistroryAIReActChat.tsx index 9ee8abdab5..03004c1a59 100644 --- a/app/renderer/src/main/src/components/HistroryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/HistroryAIReActChat.tsx @@ -22,6 +22,7 @@ import { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' import { getRemoteValue } from '@/utils/kv' import { AIModelForm } from '@/pages/ai-agent/aiModelList/aiModelForm/AIModelForm' import useListenWidth from '@/pages/pluginHub/hooks/useListenWidth' +import { AISource } from '@/pages/ai-re-act/hooks/grpcApi' interface HistoryAIReActChatProps { refRef: React.RefObject @@ -37,6 +38,8 @@ interface HistoryAIReActChatProps { mergeRemoteAIAgentSetting?: (cache: AIAgentSetting, prev: AIAgentSetting) => AIAgentSetting onChatReady?: () => void externalParameters: NonNullable + + source: AISource } const HistroryAIReActChat: FC = (props) => { @@ -54,6 +57,7 @@ const HistroryAIReActChat: FC = (props) => { mergeRemoteAIAgentSetting, onChatReady, externalParameters, + source, } = props const [_, setGlobalNetworkConfig] = useSafeState(defaultParams) @@ -109,6 +113,7 @@ const HistroryAIReActChat: FC = (props) => { return { ...next, ReviewPolicy: prev.ReviewPolicy, + Source: source, } }) } catch (error) {} diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index b45a714e7f..21fbacde30 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -17,7 +17,6 @@ import ChatIPCContent, { import { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' import { AIMentionCommandParams } from '@/pages/ai-agent/components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin' import { AIAgentSettingDefault } from '@/pages/ai-agent/defaultConstant' -import { ChatDataStore } from '@/pages/ai-agent/store/ChatDataStore' import { createActiveChatSessionId, getAIReActRequestParams } from '@/pages/ai-agent/utils' import { AISession } from '@/pages/ai-agent/type/aiChat' import { HandleStartParams } from '@/pages/ai-agent/aiAgentChat/type' @@ -30,7 +29,7 @@ import { AISendParams, AISendResProps, } from '@/pages/ai-re-act/aiReActChat/AIReActChatType' -import { AIAgentGrpcApi, AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' +import { AIAgentGrpcApi, AIInputEvent, AISource } from '@/pages/ai-re-act/hooks/grpcApi' import { applyHttpFuzzRequestChangeToWebFuzzerPage, getWebFuzzerPageIsHttps, @@ -52,7 +51,6 @@ import { } from '../pages/yakRunner/yakRunnerAiCodePatchApply' import { ChatIPCSendType, UseChatIPCEvents } from '@/pages/ai-re-act/hooks/type' import useChatIPC from '@/pages/ai-re-act/hooks/useChatIPC' -import { getAISourceFromChatDataStoreKey, getChatDataStoreKey } from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' import useDeleteAIImageByNode from '@/pages/ai-agent/components/aiMilkdownInput/aiCustomFile/hooks/useDeleteAIImageByNode' import emiter from '@/utils/eventBus/eventBus' @@ -180,7 +178,7 @@ function normalizeStartUserQueryToTextDescription(event: AIInputEvent): AIInputE } export interface HistoryAIReActChatProviderProps { - cacheDataStore: ChatDataStore + source: AISource focusModeLoop: HistoryAIReActFocusModeLoop children: React.ReactNode /** Web Fuzzer 页签 id:AI 改包回写、请求附件、fuzz 状态推送等桥接用,与 SessionID 无关 */ @@ -199,7 +197,7 @@ export interface HistoryAIReActChatProviderProps { } export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProviderInner({ - cacheDataStore, + source, focusModeLoop, children, httpFuzzTabPageId, @@ -350,22 +348,16 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid pushAIFuzzStatusRuntimeIdToWebFuzzerPage(httpFuzzTabPageId, runtimeId, { source: 'auto' }) }) - const aiSource = useCreation( - () => getAISourceFromChatDataStoreKey(getChatDataStoreKey(cacheDataStore)) ?? 'ai', - [cacheDataStore], - ) - const [chatIPCData, events] = useChatIPC({ - autoConnect: true, - aiSource, - cacheDataStore, - getSetting, - onHttpFuzzRequestChange, - onGetHttpFlowFuzzStatus, - onYaklangCodeChange, + // autoConnect: true, + // aiSource, + // cacheDataStore, + // getSetting, + // onHttpFuzzRequestChange, + // onGetHttpFlowFuzzStatus, + // onYaklangCodeChange, }) - const imageStoreKey = useCreation(() => getChatDataStoreKey(cacheDataStore), [cacheDataStore]) const [, { onClearImage }] = useDeleteAIImageByNode() const { execute, casualLoading } = chatIPCData @@ -473,25 +465,6 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid aiReActChatRef.current?.setValue('') }) - const handleDelChats = useMemoizedFn((jsonString: string) => { - try { - const sessions: string[] = JSON.parse(jsonString) - if (!sessions.length || imageStoreKey === 'unknown') return - onClearImage({ - chatDataStoreKey: imageStoreKey, - sessionID: sessions, - }) - events.onDelChats(sessions) - } catch (error) {} - }) - - useEffect(() => { - emiter.on('onDelChats', handleDelChats) - return () => { - emiter.off('onDelChats', handleDelChats) - } - }, [handleDelChats]) - const onStop = useMemoizedFn(() => { if (execute && activeID) { events.onClose(activeID) @@ -680,9 +653,10 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid mergeRemoteAIAgentSetting={mergeRemoteAIAgentSetting} onChatReady={flushPendingMention} externalParameters={externalParameters} + source={source} /> ), - [inViewport, flushPendingMention, mergeRemoteAIAgentSetting, onSendRequest, onStartRequest, showFreeChat], + [inViewport, flushPendingMention, mergeRemoteAIAgentSetting, onSendRequest, onStartRequest, showFreeChat, source], ) const contextValue = useMemo( diff --git a/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx b/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx index 6079625583..223622d0f6 100644 --- a/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx +++ b/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx @@ -35,7 +35,6 @@ import { apiCancelDebugPlugin } from '@/pages/plugins/utils' import { KnowledgeBaseTableHeaderProps } from './KnowledgeBaseTableHeader' import { CreateKnowledgeBaseData } from '../TKnowledgeBase' -import { knowledgeBaseDataStore } from '@/pages/ai-agent/store/ChatDataStore' import { HistoryAIReActChatProvider, useHistoryAIReActChat } from '@/components/historyAIReActChat' import { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' import { AIHandleStartParams } from '@/pages/ai-re-act/aiReActChat/AIReActChatType' @@ -46,7 +45,6 @@ import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import { GuideFooter } from './GuideFooter' import { YakitResizeBox } from '@/components/yakitUI/YakitResizeBox/YakitResizeBox' import { OutlineMessageCirclePlusIcon, OutlineXIcon } from '@/assets/icon/outline' -import { OutlinePlusIcon } from '@/assets/newIcon' import { HoldGRPCStreamInfo } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' import { InstallPluginModal } from './InstallPluginModal/InstallPluginModal' import { reseultKnowledgePlugin, useCheckKnowledgePlugin } from '../hooks/useCheckKnowledgePlugin' @@ -60,6 +58,7 @@ import { ImportModal } from './ImportModal' import { grpcFetchLocalPluginDetail } from '@/pages/pluginHub/utils/grpc' import { YakitModal } from '@/components/yakitUI/YakitModal/YakitModal' import { PluginExecuteResult } from '@/pages/plugins/operator/pluginExecuteResult/PluginExecuteResult' +import { AISourceEnum } from '@/pages/ai-re-act/hooks/grpcApi' interface KnowledgeBaseContentProps { knowledgeBaseID: string @@ -684,7 +683,7 @@ const KnowledgeBaseContent = forwardRef(func return ( = (props) => { setRemoteValue(RemoteAIAgentGV.AIAgentChatSetting, JSON.stringify(cache)) }, [setting]) - const { onStart, onSend, onClose, fetchAISource } = useChatIPC({ source: AISourceEnum.aiAgent }) + const { onStart, onSend, onClose } = useChatIPC() const store: AIAgentContextStore = useMemo(() => { return { @@ -112,7 +112,6 @@ export const AIAgent: React.FC = (props) => { onStart, onSend, onClose, - fetchAISource, } }, []) @@ -144,6 +143,7 @@ export const AIAgent: React.FC = (props) => { EnableGoalMode: false, GoalMinIterations: AIAgentSettingDefault.Strategy?.GoalMinIterations, }, + Source: AISourceEnum.aiAgent, }) } catch (error) {} }) diff --git a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx index 2c951dd66e..39dbc8b68e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx @@ -5,7 +5,6 @@ import { OutlineCogIcon, OutlineMCPIcon, OutlineSparklesIcon, - OutlineTemplateIcon, OutlineWrenchIcon, OutlineBookOpenTextIcon, OutlineBotIcon, @@ -122,6 +121,7 @@ export const AIAgentSettingDefault: AIAgentSetting = { EnableGoalMode: false, GoalMinIterations: 0, }, + Source: 'ai', } /** mcp 自定义服务器配置类型选项 */ diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx index 4b2ee6e903..d352760251 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx @@ -91,7 +91,7 @@ interface HistoryChatProps { } const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { - const { setActiveChat, fetchAISource, onClose } = useAIAgentDispatcher() + const { setActiveChat, onClose, getSetting } = useAIAgentDispatcher() const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const [historySourceFilter, setHistorySourceFilter] = useState('local') const enableHistorySourceFilter = useMemo(() => aiSource.includes('im'), [aiSource]) @@ -132,7 +132,7 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { setClearLoading(true) try { - const source = fetchAISource() + const source = getSetting().Source const filter = isGlobalAIAgentHistory ? { DeleteAll: true } : { Filter: { Source: historyQuerySources } } await handAIHistoryChatRemove({ grpcDeleteAISessionParams: filter, @@ -180,7 +180,7 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { Source: historyQuerySources, } const sessionIds = sessions.map((item) => item.SessionID) - const source = fetchAISource() + const source = getSetting().Source await handAIHistoryChatRemove({ grpcDeleteAISessionParams: { Filter: filter }, handleClearAIImageParams: { chatDataStoreKey: getImageStoreKeyByAISource(source), sessionID: sessionIds }, diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx index 8ed41aedca..185ea10c59 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx @@ -195,7 +195,7 @@ const HistoryChatList: FC<{ editInfo.current = undefined }) - const { onClose, fetchAISource } = useAIAgentDispatcher() + const { getSetting } = useAIAgentDispatcher() const handleDeleteChat = useMemoizedFn(async (info: AISession) => { const { SessionID } = info const isLoading = delLoading.includes(SessionID) @@ -223,7 +223,7 @@ const HistoryChatList: FC<{ try { const sessionIds = [SessionID] - const source = fetchAISource() + const source = getSetting().Source await handAIHistoryChatRemove({ grpcDeleteAISessionParams: { Filter: { SessionID: [SessionID], Source: aiSource } }, handleClearAIImageParams: { chatDataStoreKey: getImageStoreKeyByAISource(source), sessionID: sessionIds }, diff --git a/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts b/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts index afbde9d8fe..7157c60ef7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts +++ b/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts @@ -3,6 +3,7 @@ import { AIAgentSetting } from '../aiAgentType' import { AISession } from '../type/aiChat' import { AIChatIPCStartParams, AIChatSendParams } from '@/pages/ai-re-act/hooks/type' import { AISource } from '@/pages/ai-re-act/hooks/grpcApi' +import { AIAgentSettingDefault } from '../defaultConstant' export interface AIAgentContextStore { /** 全局配置 */ @@ -16,9 +17,9 @@ export interface UseChatIPCStartParams { onSuccess?: (sessionId: string) => void } export interface AIAgentContextDispatcher { - setSetting?: Dispatch> - getSetting?: () => AIAgentSetting - setActiveChat?: Dispatch> + setSetting: Dispatch> + getSetting: () => AIAgentSetting + setActiveChat: Dispatch> /** 开始会话 */ onStart: (UseChatIPCStartParams) => void @@ -26,8 +27,6 @@ export interface AIAgentContextDispatcher { onSend: (params: AIChatSendParams) => void /** 关闭指定会话 */ onClose: (sessionId: string[]) => void - /** 获取当前ai来源 */ - fetchAISource: () => AISource } export interface AIAgentContextValue { @@ -37,18 +36,16 @@ export interface AIAgentContextValue { export default createContext({ store: { - setting: {}, + setting: { ...AIAgentSettingDefault }, activeChat: undefined, }, dispatcher: { - setSetting: undefined, - getSetting: undefined, - - setActiveChat: undefined, + setSetting: () => {}, + getSetting: () => AIAgentSettingDefault, + setActiveChat: () => {}, onStart: () => {}, onSend: () => {}, onClose: () => {}, - fetchAISource: () => 'ai', }, }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index e05050931e..fce5d82180 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -18,7 +18,7 @@ import { Tooltip } from 'antd' import { ClockIcon } from '@/assets/newIcon' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import HistoryChat from '@/pages/ai-agent/historyChat/HistoryChat' -import { AIInputEvent, AIInputEventSyncTypeEnum, AIStartParams } from '../hooks/grpcApi' +import { AIInputEvent, AIInputEventSyncTypeEnum, AISourceEnum, AIStartParams } from '../hooks/grpcApi' import { AITaskQuery } from '@/pages/ai-agent/components/aiTaskQuery/AITaskQuery' import { HandleStartParams } from '@/pages/ai-agent/aiAgentChat/type' import { formatAIAgentSetting, getAIReActRequestParams } from '@/pages/ai-agent/utils' @@ -28,10 +28,7 @@ import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' import useAINodeLabel from '../hooks/useAINodeLabel' import useSessionId from '../hooks/useSessionId' -import useGetChatDataStoreKey, { - getAISourceFromChatDataStoreKey, - getAISourceListFromChatDataStoreKey, -} from '../hooks/useGetChatDataStoreKey' +import useGetChatDataStoreKey from '../hooks/useGetChatDataStoreKey' import { AISendSyncMessageParams } from '@/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent' import emiter from '@/utils/eventBus/eventBus' import { omit } from 'lodash' @@ -56,7 +53,7 @@ export const AIReActChat: React.FC = React.memo( startRequest, externalParameters, } = props - const { setActiveChat, fetchAISource } = useAIAgentDispatcher() + const { setActiveChat, getSetting } = useAIAgentDispatcher() const { chatDataStoreKey } = useGetChatDataStoreKey() const historyChatAISource = useCreation( () => getAISourceListFromChatDataStoreKey(chatDataStoreKey), @@ -80,6 +77,10 @@ export const AIReActChat: React.FC = React.memo( const { activeChat, setting } = useAIAgentStore() const { getSession } = useSessionId() + const source = useCreation(() => { + return setting.Source + }, [setting.Source]) + const contextTokenSession = useCreation(() => { return activeChat?.SessionID || setting.TimelineSessionID }, [activeChat?.SessionID, setting.TimelineSessionID]) @@ -138,13 +139,14 @@ export const AIReActChat: React.FC = React.memo( const { qs, sessionId } = value const sessionID = activeChat?.SessionID || '' // 判断历史还是新建 + const source = getSetting().Source ?? AISourceEnum.aiAgent // getSetting保证最新 const request: AIStartParams = { ...formatAIAgentSetting(setting), UserQuery: qs, CoordinatorId: '', Sequence: 1, PreferSessionCachedConfig: true, - Source: getAISourceFromChatDataStoreKey(chatDataStoreKey), + Source: source, } const session = getSession(sessionId) @@ -173,7 +175,7 @@ export const AIReActChat: React.FC = React.memo( StartParams: request, SessionID: session, TitleInitialized: false, - Source: request.Source ?? 'ai', + Source: request.Source, LastUsedAt: new Date().getTime(), isCreate: true, } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts index 34ff6034d7..0929fe02b9 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts @@ -151,7 +151,7 @@ export interface AIStartParams { /** * */ - Source?: AISource + Source: AISource EnabledCapabilities?: AIEnabledCapability[] diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts index daadd408b3..afb9536dc6 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts @@ -8,7 +8,7 @@ import useCurrentSessionId from './useCurrentSessionId' import { AISource } from './grpcApi' const { ipcRenderer } = window.require('electron') -export function useChatIPC(params: { source: AISource }) { +export function useChatIPC() { const sessionId = useCurrentSessionId() const currentSessionIdRef = useRef(sessionId) @@ -49,10 +49,6 @@ export function useChatIPC(params: { source: AISource }) { globalSessionEngine.forceCloseSession(sessionId, params.source) }) - const fetchAISource = useMemoizedFn(() => { - return params.source - }) - // 组件卸载时拔插头,清理闭环 useEffect(() => { return () => { @@ -60,5 +56,5 @@ export function useChatIPC(params: { source: AISource }) { } }, []) - return { onStart, onSend, onClose, fetchAISource } + return { onStart, onSend, onClose } } diff --git a/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx b/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx index 8554dd658c..7b3b295581 100644 --- a/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx +++ b/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx @@ -1,5 +1,5 @@ -import React, { CSSProperties, memo, useEffect, useLayoutEffect, useMemo, useRef, useState, createRef } from 'react' -import { Form, Result, Space, Popover, Tooltip, Divider, Descriptions } from 'antd' +import React, { CSSProperties, useEffect, useLayoutEffect, useMemo, useRef, useState, createRef } from 'react' +import { Form, Result, Space, Tooltip, Divider, Descriptions } from 'antd' import { IMonacoEditor, NewHTTPPacketEditor, @@ -105,7 +105,6 @@ import { } from '@/assets/icon/outline' import emiter from '@/utils/eventBus/eventBus' import { HistoryAIReActChatProvider, useHistoryAIReActChat } from '@/components/historyAIReActChat' -import { WebFuzzerAiStore } from '@/pages/ai-agent/store/ChatDataStore' import { applyHttpFuzzRequestChangeToWebFuzzerPage, registerWebFuzzerPageApplyRequestFromCard, @@ -115,9 +114,8 @@ import { registerWebFuzzerPageOnAIFuzzStatus, type WebFuzzerCasualReplaceReviewPayload, } from './webFuzzerAiRequestApplyBridge' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' -import { AIInputFooterRightEnum, AIInputInnerFeatureEnum } from '@/pages/ai-agent/template/type' -import { AIAgentGrpcApi } from '@/pages/ai-re-act/hooks/grpcApi' +import { AIInputFooterRightEnum } from '@/pages/ai-agent/template/type' +import { AISourceEnum } from '@/pages/ai-re-act/hooks/grpcApi' import { shallow } from 'zustand/shallow' import { usePageInfo, PageNodeItemProps, WebFuzzerPageInfoProps, getFuzzerProcessedCacheData } from '@/store/pageInfo' import { YakitCopyText } from '@/components/yakitUI/YakitCopyText/YakitCopyText' @@ -189,7 +187,6 @@ import { YakitDrawer } from '@/components/yakitUI/YakitDrawer/YakitDrawer' import { PublicHTTPHistoryIcon } from '@/routes/publicIcon' import { useProxy } from '@/hook/useProxy' import { MITMConsts } from '../mitm/MITMConsts' -import { RemoteGV } from '@/yakitGV' import { YakitSwitch } from '@/components/yakitUI/YakitSwitch/YakitSwitch' import { useChunkAutoScrollToBottom } from './hooks/useAutoScrollToBottom' const tOriginal = i18n.getFixedT(null, ['yakitUi', 'webFuzzer']) @@ -3464,10 +3461,9 @@ const HTTPFuzzerPageCore: React.FC = (props) => { /** 每个 Web Fuzzer 页签独立 WebFuzzerAiStore,避免多开时共用内存缓存导致会话数据互相覆盖 */ const HTTPFuzzerPage: React.FC = (props) => { - const fuzzerAiChatDataStore = useCreation(() => new WebFuzzerAiStore(props.id), [props.id]) return ( diff --git a/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx b/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx index 439ada3c33..1cde120bee 100644 --- a/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx +++ b/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx @@ -18,7 +18,6 @@ import { OutlineCogIcon, OutlineFilterIcon, OutlineMessageCirclePlusIcon, - OutlinePlusIcon, OutlineRefreshIcon, OutlineReplyIcon, OutlineSearchIcon, @@ -84,7 +83,7 @@ import { HistoryProcess, HistoryTab } from '@/components/HTTPHistory' import { useBuiltinTagList } from '@/components/HTTPFlowTable/useBuiltinTagList' import { useCampare } from '@/hook/useCompare/useCompare' import { v4 as uuidv4 } from 'uuid' -import { cloneDeep, isEqual, toArray } from 'lodash' +import { cloneDeep, isEqual } from 'lodash' import { MitmExtractAggregateFlowFilterRow } from '@/utils/yakQueryHTTPFlow' import { showByRightContext } from '@/components/yakitUI/YakitMenu/showByRightContext' import { randomString } from '@/utils/randomUtil' @@ -141,11 +140,11 @@ import { FilterConfig, HTTPFlowTableFormConfiguration, } from '@/components/HTTPFlowTable/HTTPFlowTableFormConfiguration/HTTPFlowTableFormConfiguration' -import { FlowAiStore } from '@/pages/ai-agent/store/ChatDataStore' -import { AIInputFooterRightEnum, AIInputInnerFeatureEnum } from '@/pages/ai-agent/template/type' +import { AIInputFooterRightEnum } from '@/pages/ai-agent/template/type' import { HistoryAIReActChatProvider, useHistoryAIReActChat } from '@/components/historyAIReActChat' import { HTTPFlowRuleDataFilter } from '@/components/HTTPFlowTable/HTTPFlowRuleDataFilter' import { isFilterSectionActive, safeParse } from '../HTTPHistoryAnalysis.utils' +import { AISourceEnum } from '@/pages/ai-re-act/hooks/grpcApi' const { ipcRenderer } = window.require('electron') interface HTTPHistoryFilterProps { @@ -425,7 +424,7 @@ HTTPHistoryFilterInner.displayName = 'HTTPHistoryFilterInner' export const HTTPHistoryFilter: React.FC = (props) => { return ( - + ) diff --git a/app/renderer/src/main/src/pages/irifyAiCodeAudit/IrifyAiCodeAuditPage.tsx b/app/renderer/src/main/src/pages/irifyAiCodeAudit/IrifyAiCodeAuditPage.tsx index 9db6e1ba7c..3e3b63d99a 100644 --- a/app/renderer/src/main/src/pages/irifyAiCodeAudit/IrifyAiCodeAuditPage.tsx +++ b/app/renderer/src/main/src/pages/irifyAiCodeAudit/IrifyAiCodeAuditPage.tsx @@ -2,10 +2,9 @@ import React, { useEffect, useRef, useState } from 'react' import { useMemoizedFn } from 'ahooks' import emiter from '@/utils/eventBus/eventBus' import { AuditCodePageInfoProps } from '@/store/pageInfo' -import { irifyAiCodeAuditPageAiStore } from '@/pages/ai-agent/store/ChatDataStore' import { IrifyAiCodeAuditStyle, resolveIrifyFocusModeLoop, isIrifyAuditStyleConfirmed } from './irifyAiCodeAuditStyle' import { HistoryAIReActChatProvider } from '@/components/historyAIReActChat' -import { AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' +import { AIInputEvent, AISourceEnum } from '@/pages/ai-re-act/hooks/grpcApi' import { IrifyAiCodeAuditSidePanelLayout } from './IrifyAiCodeAuditSidePanelLayout' import { appendIrifyWorkbenchAttachments, @@ -133,7 +132,7 @@ const IrifyAiCodeAuditPageInner: React.FC = ({ auditC return ( diff --git a/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx b/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx index 4eaba639b3..fba38c66e4 100644 --- a/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx +++ b/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx @@ -68,7 +68,6 @@ import { randomString } from '@/utils/randomUtil' import { YakitTabsProps } from '@/components/yakitSideTab/YakitSideTabType' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { HistoryAIReActChatProvider } from '@/components/historyAIReActChat' -import { yakRunnerPageAiStore } from '@/pages/ai-agent/store/ChatDataStore' import { YAK_RUNNER_FOCUS_MODE_CODE_SECURITY_AUDIT } from '@/constants/focusMode' import { YakRunnerAiAttachProvider, YakRunnerAiAttachRef, useYakRunnerAiAttachRef } from './YakRunnerAiAttachContext' import { YakRunnerAiSidePanel } from './YakRunnerAiSidePanel' @@ -86,6 +85,7 @@ import { type YakRunnerCasualCodeReplaceReviewPayload, } from './yakRunnerAiCodeApplyBridge' import { syncYakRunnerPatchWorkingDraft } from './yakRunnerAiCodePatchApply' +import { AISourceEnum } from '../ai-re-act/hooks/grpcApi' const { ipcRenderer } = window.require('electron') // 模拟tabs分块及对应文件 @@ -1278,7 +1278,7 @@ const YakRunnerWorkbench: React.FC = (props) => { const YakRunnerWithAIInner: React.FC = (props) => { return ( From 07deaeb816929d0302aecc5b70088f792e7dc85b Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Thu, 25 Jun 2026 15:40:47 +0800 Subject: [PATCH 009/101] =?UTF-8?q?feat:ai=E6=89=A7=E8=A1=8C=E8=BF=87?= =?UTF-8?q?=E7=A8=8B=E4=B8=AD=E7=9A=84=E9=A1=B6=E9=83=A8UI=E6=9E=81?= =?UTF-8?q?=E5=85=B6=E5=8A=9F=E8=83=BD;feat:=E5=AF=B9=E6=8E=A5AIChatConten?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-agent/aiAgentChat/AIAgentChat.tsx | 112 ++++-------- .../src/pages/ai-agent/aiAgentChat/type.ts | 4 +- .../aiChatContent/AIChatContent.module.scss | 132 -------------- .../ai-agent/aiChatContent/AIChatContent.tsx | 172 ++++-------------- .../AIHorizontalScrollCard.module.scss | 85 +++++++++ .../AIHorizontalScrollCard.tsx | 134 ++++++++++++++ .../aiHorizontalScrollCard/type.ts | 1 + .../aiChatContent/hooks/useAIChatResizeBox.ts | 12 +- .../ai-agent/aiChatWelcome/AIChatWelcome.tsx | 18 +- .../AITriageChatTemplate.tsx | 19 +- .../ai-agent/aiTriageChatTemplate/type.ts | 8 +- .../main/src/pages/ai-agent/type/aiChat.d.ts | 5 +- .../ChatIPCContent/ChatIPCContent.ts | 7 +- .../ai-re-act/aiReActChat/AIReActChat.tsx | 9 +- .../ai-re-act/aiReActChat/AIReActChatType.ts | 3 +- .../src/pages/ai-re-act/hooks/aiRender.ts | 7 +- .../hooks/useCurrentDataBySession.ts | 43 +++++ 17 files changed, 369 insertions(+), 402 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentDataBySession.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index e00893b1b2..c2db4e564e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -18,10 +18,10 @@ import { import { AIReActChatReview } from '@/pages/ai-agent/components/aiReActChatReview/AIReActChatReview' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { OutlineChevrondoubledownIcon, OutlineChevrondoubleupIcon } from '@/assets/icon/outline' -import { AIChatIPCStartParams, ChatIPCSendType, UseTaskChatState } from '@/pages/ai-re-act/hooks/type' +import { ChatIPCSendType } from '@/pages/ai-re-act/hooks/type' import useChatIPCDispatcher from '../useContext/ChatIPCContent/useDispatcher' import useChatIPCStore from '../useContext/ChatIPCContent/useStore' -import { AIAgentGrpcApi, AIInputEvent, AIStartParams } from '@/pages/ai-re-act/hooks/grpcApi' +import { AIAgentGrpcApi, AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' import { AIChatQSData, AIReviewType } from '@/pages/ai-re-act/hooks/aiRender' import { failed, yakitNotify } from '@/utils/notification' import { AIForgeForm, AIToolForm } from '../aiTriageChatTemplate/AITriageChatTemplate' @@ -39,7 +39,6 @@ import useMultipleHoldGRPCStream from '@/pages/KnowledgeBase/hooks/useMultipleHo import { useKnowledgeBase } from '@/pages/KnowledgeBase/hooks/useKnowledgeBase' import { YakitRoute } from '@/enums/yakitRoute' import { apiCancelDebugPlugin } from '@/pages/plugins/utils' -import { aiChatDataStore } from '@/pages/ai-agent/store/ChatDataStore' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import classNames from 'classnames' import styles from './AIAgentChat.module.scss' @@ -48,28 +47,19 @@ import { PageNodeItemProps } from '@/store/pageInfo' import { Trans } from 'react-i18next' import { AIInputWithParamsTemplate, aiInputWithParamsTemplate } from '../components/aiMilkdownInput/utils' import { useStore } from 'zustand' -import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' -import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' +import { AIForgeFormSubmitParamsProps } from '../aiTriageChatTemplate/type' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' const AIChatWelcome = React.lazy(() => import('../aiChatWelcome/AIChatWelcome')) -const taskChatIsEmpty = (taskChat?: UseTaskChatState) => { - if (!taskChat) return false - - const isHavePlan = !!taskChat.plan?.task_tree?.length - const isHaveStreams = !!taskChat.elements?.length - return isHavePlan || isHaveStreams -} - export const AIAgentChat: React.FC = memo((props) => { const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const { activeChat } = useAIAgentStore() - const { setActiveChat, getSetting, setSetting, onSend, onClose } = useAIAgentDispatcher() + const { setActiveChat, setSetting, onSend, onClose } = useAIAgentDispatcher() /** 当前对话唯一ID */ - const sessionId = useCurrentSessionId() - const { store } = globalSessionEngine.ensureSession(sessionId) + const store = useCurrentStore() const execute = useStore(store, (state) => state.execute) const aiReActChatRef = useRef(null) @@ -86,31 +76,10 @@ export const AIAgentChat: React.FC = memo((props) => { }) useEffect(() => { - const chatData = aiChatDataStore.get(activeChat?.SessionID || '') - if (taskChatIsEmpty(chatData?.taskChat)) { - onSetKeyTask() - } else if (!!activeChat?.Id) { + if (!!activeChat?.SessionID) { onSetReAct() } - }, [activeChat]) - - useEffect(() => { - if (mode === 'welcome') { - events.onReset() - } - }, [mode]) - - /**自由对话中触发任务开始 */ - const handleTaskStart = useMemoizedFn(() => { - onSetKeyTask() - }) - - const onSetKeyTask = useMemoizedFn(() => { - setMode('task') - setTimeout(() => { - emiter.emit('switchAIActTab', JSON.stringify({ key: AITabsEnum.Task_Content })) - }, 100) - }) + }, [activeChat?.SessionID]) const onSetReAct = useMemoizedFn(() => { setMode('re-act') @@ -128,13 +97,16 @@ export const AIAgentChat: React.FC = memo((props) => { const [reviewInfo, setReviewInfo] = useState() const [reviewExpand, setReviewExpand] = useState(true) + // @deprecated const handleShowReview = useMemoizedFn((info: AIChatQSData) => { setReviewExpand(true) setReviewInfo(cloneDeep(info)) }) + // @deprecated const handleShowReviewExtra = useMemoizedFn((info: AIAgentGrpcApi.PlanReviewRequireExtra) => { setPlanReviewTreeKeywords(info.task_id, info) }) + // @deprecated const handleReleaseReview = useMemoizedFn((type: ChatIPCSendType, id: string) => { if (!reviewInfo) return if ((reviewInfo.data as AIReviewType).id === id) { @@ -143,11 +115,12 @@ export const AIAgentChat: React.FC = memo((props) => { } }) - // 提问结束后缓存数据 + // @deprecated 提问结束后缓存数据 const handleChatingEnd = useMemoizedFn(() => { handleStopAfterChangeState() }) + // @deprecated const setSessionChatName = (session: string, name: string) => { setActiveChat?.((prev) => { if (!prev) return prev @@ -164,11 +137,13 @@ export const AIAgentChat: React.FC = memo((props) => { ) } + // @deprecated const [syncIdInfoMap, { set: setSyncIdInfoMap, get: getSyncIdInfoMap, remove: removeSyncIdInfoMap }] = useMap< string, boolean >(new Map()) + // @deprecated const onSyncIDChange = useMemoizedFn((syncID: string) => { const item = getSyncIdInfoMap(syncID) if (!!item) { @@ -176,9 +151,10 @@ export const AIAgentChat: React.FC = memo((props) => { } }) + /** 等自由对话渲染出来再发送 */ const handleStart = useMemoizedFn((value: HandleStartParams) => { setTimeout(() => { - aiReActChatRef.current?.handleStart(value) // 等自由对话渲染出来再发送 + aiReActChatRef.current?.handleStart(value) }) }) // @deprecated @@ -241,10 +217,9 @@ export const AIAgentChat: React.FC = memo((props) => { const onStop = useMemoizedFn(() => { if (execute && sessionId) { onClose([sessionId]) - handleStopAfterChangeState() } }) - /** 停止回答后的状态调整||清空Review状态 */ + /** @deprecated 停止回答后的状态调整||清空Review状态 */ const handleStopAfterChangeState = useMemoizedFn(() => { // 清空review信息 setReviewInfo(undefined) @@ -312,12 +287,26 @@ export const AIAgentChat: React.FC = memo((props) => { useUpdateEffect(() => { onHistoryAfter() - // events.onSwitchChat(activeChat?.SessionID, activeChat?.isCreate) - }, [activeChat]) + }, [activeChat?.SessionID]) - /**切换历史后的处理逻辑 */ + /** 切换历史后的处理逻辑 + * 切换历史后,如果选中的会话没有建立链接,需要启动grpc链接 + */ const onHistoryAfter = useMemoizedFn(() => { - if (mode === 'welcome') setMode('re-act') + if (mode === 'welcome') { + setMode('re-act') + if (!execute) { + handleStart({ + qs: '', + }) + } + } else { + if (!execute) { + aiReActChatRef.current?.handleStart({ + qs: '', + }) + } + } }) //#region 使用 AI-Forge 模板/Tool 相关逻辑 @@ -438,7 +427,8 @@ export const AIAgentChat: React.FC = memo((props) => { setActiveTool(undefined) }) - const handleSubmitForge = useMemoizedFn((request: AIStartParams, formValue: AIChatIPCStartParams['extraValue']) => { + const handleSubmitForge = useMemoizedFn((data: AIForgeFormSubmitParamsProps) => { + const { request, formValue } = data setMode('re-act') const description = `${t('AIAgentChat.useForgeTask', { name: request.ForgeName || '' })}${!!formValue ? t('AIAgentChat.params') : ''}` @@ -449,9 +439,6 @@ export const AIAgentChat: React.FC = memo((props) => { const qs = aiInputWithParamsTemplate(params) handleStart({ qs, - extraValue: { - showQS: qs, - }, }) handleClearActiveForge() }) @@ -467,9 +454,6 @@ export const AIAgentChat: React.FC = memo((props) => { })}${question ? `${t('AIAgentChat.input')}${question}` : ''}` handleStart({ qs, - extraValue: { - showQS: qs, - }, }) handleClearActiveTool() }) @@ -559,26 +543,6 @@ export const AIAgentChat: React.FC = memo((props) => { setReplaceToolShow(false) }) // #endregion - // const store: ChatIPCContextStore = useCreation(() => { - // return { - // chatIPCData, - // planReviewTreeKeywordsMap, - // reviewInfo, - // reviewExpand, - // syncIdInfoMap, - // } - // }, [chatIPCData, planReviewTreeKeywordsMap, reviewInfo, reviewExpand, syncIdInfoMap]) - // const dispatcher: ChatIPCContextDispatcher = useCreation(() => { - // return { - // chatIPCEvents: events, - // handleSendCasual, - // handleSendTask, - // handleStop: onStop, - // handleSend, - // handleSendSyncMessage, - // handleSendConfigHotpatch, - // } - // }, [events]) const [visible, setVisible] = useSafeState(false) const { clearAll } = useKnowledgeBase() diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/type.ts b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/type.ts index 192b0c5274..3cb4efd1e0 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/type.ts @@ -1,11 +1,10 @@ import { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' import { AIAgentGrpcApi, AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' import { AIChatTextareaSubmit } from '../template/type' -import { AIChatIPCStartParams } from '@/pages/ai-re-act/hooks/type' import { ReactNode } from 'react' export interface AIAgentChatProps {} -export type AIAgentChatMode = 'welcome' | 're-act' | 'task' +export type AIAgentChatMode = 'welcome' | 're-act' export interface AIReActTaskChatReviewProps { reviewInfo: AIChatQSData planReviewTreeKeywordsMap: Map @@ -15,5 +14,4 @@ export interface AIReActTaskChatReviewProps { export interface HandleStartParams extends AIChatTextareaSubmit { attachedResourceInfo?: AIInputEvent['AttachedResourceInfo'] - extraValue?: AIChatIPCStartParams['extraValue'] } diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.module.scss b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.module.scss index 2a45597a81..353f052d41 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.module.scss @@ -14,139 +14,7 @@ position: relative; flex: 1; overflow: hidden; - .expand-retract-wrapper { - width: 100%; - border-bottom: 0; - z-index: 8; - .expand-retract-content { - @extend %display-column-center; - width: 100%; - flex: 1; - } - &:hover { - background-color: var(--Colors-Use-Neutral-Bg); - .expand-retract-animation-wrapper { - width: 54px; - } - } - } - .expand-retract-wrapper-collapsed { - &:hover { - background-color: var(--Colors-Use-Basic-Background); - cursor: default; - } - } - .header { - @extend %display-flex-center; - justify-content: space-between; - gap: 12px; - width: 100%; - flex: 1; - .title { - @extend %display-flex-center; - gap: 4px; - color: var(--Colors-Use-Neutral-Text-1-Title); - font-size: 14px; - font-weight: 500; - line-height: 20px; - .chat-alt-icon { - svg { - width: 16px; - height: 16px; - color: var(--Colors-Use-Neutral-Disable); - } - } - .chat-title { - max-width: 250px; - @include mixin.text-ellipsis(); - } - } - .extra { - @extend %display-flex-center; - gap: 16px; - overflow-x: auto; - } - .info-token { - display: flex; - align-items: center; - gap: 4px; - color: var(--Colors-Use-Neutral-Text-1-Title); - font-size: 12px; - font-weight: 400; - .token { - color: var(--Colors-Use-Neutral-Text-4-Help-text); - font-size: 12px; - font-weight: 400; - line-height: 16px; - } - } - - .echarts-wrapper { - @extend %display-flex-center; - gap: 4px; - color: var(--Colors-Use-Neutral-Text-4-Help-text); - font-size: 12px; - font-weight: 400; - line-height: 16px; - - .title { - @extend %display-flex-center; - gap: 4px; - .text { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - word-break: break-all; - } - - .tip { - color: var(--Colors-Use-Neutral-Text-4-Help-text); - line-height: 16px; - letter-spacing: 0.5px; - } - } - .intelligent { - color: var(--Colors-Use-Purple-Primary); - } - .intelligent-height { - color: var(--Colors-Use-Error-Primary); - } - .lightweight { - color: var(--Colors-Use-Warning-Primary); - } - .lightweight-height { - color: var(--Colors-Use-Error-Primary); - } - } - .divider-style { - width: 1px; - height: 12px; - border-left: 1px solid var(--Colors-Use-Neutral-Border); - } - } - .card-list-wrapper { - width: 100%; - opacity: 1; - transition: all 0.3s ease; - padding-top: 12px; - .card-item { - height: 50px; - font-size: 14px; - } - } - .card-list-wrapper-hidden { - padding-top: 0; - height: 0; - opacity: 0; - } - .expand-retract-animation-wrapper { - top: unset; - bottom: -12px; - } - .expand-retract-animation-wrapper-hidden { - display: none; - } .ai-chat-tab-wrapper { @extend %display-column-center; flex: 1; diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx index ef5e64eb0a..6c7a1b1d93 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx @@ -1,9 +1,7 @@ import React, { forwardRef, ReactNode, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { AIAgentTabPayload, AIChatContentProps } from './type' import styles from './AIChatContent.module.scss' -import { ExpandAndRetract } from '@/pages/plugins/operator/expandAndRetract/ExpandAndRetract' import { useCreation, useMemoizedFn } from 'ahooks' -import { HorizontalScrollCard } from '@/pages/plugins/operator/horizontalScrollCard/HorizontalScrollCard' import classNames from 'classnames' import { YakitSideTab } from '@/components/yakitSideTab/YakitSideTab' import { AITabs, AITabsEnum } from '../defaultConstant' @@ -18,40 +16,42 @@ import { import { YakitEmpty } from '@/components/yakitUI/YakitEmpty/YakitEmpty' import AIReActTaskChat from '@/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat' import emiter from '@/utils/eventBus/eventBus' -import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' -import { OutlineClouddownloadIcon, OutlineNewspaperIcon, OutlinePlussmIcon } from '@/assets/icon/outline' -import { SolidChatalt2Icon } from '@/assets/icon/solid' -import useAiChatLog from '@/hook/useAiChatLog/useAiChatLog.ts' import { YakitResizeBox } from '@/components/yakitUI/YakitResizeBox/YakitResizeBox' -import { grpcExportAILogs } from '../grpc' -import useChatIPCStore from '../useContext/ChatIPCContent/useStore' import { YakitTag } from '@/components/yakitUI/YakitTag/YakitTag' -import { onNewChat } from '../historyChat/HistoryChat' // import {SideSettingButton} from "../aiChatWelcome/AIChatWelcome" -import { Divider } from 'antd' import useAIAgentStore from '../useContext/useStore' import { useAIChatResizeBox } from './hooks/useAIChatResizeBox' -import { ExportAILogsModal } from '../components/ExportAILogsModal/ExportAILogsModal' -import { failed, yakitNotify } from '@/utils/notification' import { AIHandleStartParams, AIHandleStartResProps, AIReActChatRefProps, } from '@/pages/ai-re-act/aiReActChat/AIReActChatType' -import AIContextToken from './AIContextToken/AIContextToken' import OperationLog from '../components/aiFileSystemList/OperationLog/OperationLog' import AIGlobalLoading from '../aiGlobalLoading/AIGlobalLoading' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import { AIHorizontalScrollCard } from './aiHorizontalScrollCard/AIHorizontalScrollCard' export const AIChatContent: React.FC = React.memo( forwardRef((props, ref) => { const { onChat, onChatFromHistory } = props const { t, i18nRefresh } = useI18nNamespaces(['aiAgent', 'yakitUi', 'yakitRoute']) - const chatIPCStore = useChatIPCStore() - const { httpRunTimeIDs, riskRunTimeIDs, yakExecResult, taskChat, grpcFolders, execute, requestHistoryState } = - chatIPCStore.chatIPCData + + const store = useCurrentStore() + const rawData = useCurrentRawData() + const taskChatElementLength = useStore(store, (state) => state.taskChat.elements.length) + const execFileRecord = useStore(store, (state) => state.execFileRecord) + const grpcFolders = useStore(store, (state) => state.grpcFolders) + + const httpTabShow = useStore(store, (state) => state.httpTabShow) + const httpTabUpdate = useStore(store, (state) => state.httpTabUpdate) + const riskTabShow = useStore(store, (state) => state.riskTabShow) + const riskTabUpdate = useStore(store, (state) => state.riskTabUpdate) + const requestHistoryState = useStore(store, (state) => state.requestHistoryState) + const { activeChat } = useAIAgentStore() - const [isExpand, setIsExpand] = useState(true) + const [activeKey, setActiveKey] = useState(AITabsEnum.Task_Content) const [showFreeChat, setShowFreeChat] = useState(true) //自由对话展开收起 @@ -62,9 +62,6 @@ export const AIChatContent: React.FC = React.memo( const [hasFilePreview, setHasFilePreview] = useState(false) const [runTimeId, setRunTimeId] = useState() // 工具卡片跳转自带runTimeID - const [exportModalVisible, setExportModalVisible] = useState(false) - const [exportLoading, setExportLoading] = useState(false) - const RelatedRuntimeIDs = useMemo(() => { return activeChat?.RelatedRuntimeIDs ?? [] }, [activeChat?.RelatedRuntimeIDs]) @@ -85,40 +82,6 @@ export const AIChatContent: React.FC = React.memo( // #region 问题相关逻辑 - const onOpenExportModal = useMemoizedFn((e) => { - e.stopPropagation() - setExportModalVisible(true) - }) - - const onExportCancel = useMemoizedFn(() => { - setExportModalVisible(false) - }) - - const onExportOk = useMemoizedFn(async (data: { types: string[]; outputPath: string }) => { - if (!activeChat?.Id) { - failed(t('AIChatContent.noActiveChat')) - return - } - setExportLoading(true) - // - try { - await grpcExportAILogs( - { - SessionID: activeChat.SessionID, - ExportDataTypes: data.types, - OutputPath: data.outputPath, - }, - true, - ) - yakitNotify('success', t('YakitNotification.exportSuccess')) - setExportModalVisible(false) - } catch (error) { - failed(t('YakitNotification.exportFailed', { error: error + '' })) - } finally { - setExportLoading(false) - } - }) - const handleTabStateChange = useMemoizedFn((key: AITabsEnumType, value: AIAgentTabPayload['value']) => { setActiveKey(key) if (!value) { @@ -139,8 +102,8 @@ export const AIChatContent: React.FC = React.memo( } const { key, value } = payload - if (key === AITabsEnum.HTTP && httpRunTimeIDs.length === 0 && RelatedRuntimeIDs.length === 0) return - if (key === AITabsEnum.Risk && riskRunTimeIDs.length === 0 && RelatedRuntimeIDs.length === 0) return + if (key === AITabsEnum.HTTP && !httpTabShow && RelatedRuntimeIDs.length === 0) return + if (key === AITabsEnum.Risk && !riskTabShow && RelatedRuntimeIDs.length === 0) return handleTabStateChange(key, value) }) @@ -160,26 +123,22 @@ export const AIChatContent: React.FC = React.memo( {showId} ) - }, [httpRunTimeIDs, runTimeId]) + }, [runTimeId]) - const onExpand = useMemoizedFn((e) => { - e.stopPropagation() - setIsExpand(!isExpand) - }) const yakitTabs = useCreation(() => { let tab: YakitSideTabProps['yakitTabs'] = [AITabs[AITabsEnum.Task_Content], AITabs[AITabsEnum.File_System]] - if ((httpRunTimeIDs.length || RelatedRuntimeIDs.length) > 0) { + if (httpTabShow || !!RelatedRuntimeIDs.length) { tab.push(AITabs[AITabsEnum.HTTP]) } - if ((riskRunTimeIDs.length || RelatedRuntimeIDs.length) > 0) { + if (riskTabUpdate || !!RelatedRuntimeIDs.length) { tab.push(AITabs[AITabsEnum.Risk]) } - if (yakExecResult.execFileRecord.size > 0) { + if (execFileRecord.size > 0) { tab.push(AITabs[AITabsEnum.Operation_Log]) } return tab - }, [httpRunTimeIDs, riskRunTimeIDs, yakExecResult.execFileRecord, taskChat?.elements?.length]) + }, [httpTabShow, riskTabUpdate, execFileRecord.size, taskChatElementLength]) const [showHot, setShowHot] = useState(false) const prevRef = useRef<{ @@ -228,15 +187,15 @@ export const AIChatContent: React.FC = React.memo( }) const OperationLogList = useCreation(() => { - return Array.from(yakExecResult.execFileRecord.values()) + return Array.from(execFileRecord.values()) .flat() .sort((a, b) => b.order - a.order) - }, [yakExecResult.execFileRecord]) + }, [execFileRecord]) const tabContent = useMemo(() => { if (!activeKey) return null - const runTimeIds = [...new Set(!!runTimeId ? [runTimeId] : httpRunTimeIDs.concat(RelatedRuntimeIDs))] - const riskRunTimeIds = [...new Set(!!runTimeId ? [runTimeId] : riskRunTimeIDs.concat(RelatedRuntimeIDs))] + const runTimeIds = [...new Set(!!runTimeId ? [runTimeId] : rawData.httpRunTimeIDs.concat(RelatedRuntimeIDs))] + const riskRunTimeIds = [...new Set(!!runTimeId ? [runTimeId] : rawData.riskRunTimeIDs.concat(RelatedRuntimeIDs))] switch (activeKey) { case AITabsEnum.Task_Content: return ( @@ -269,9 +228,7 @@ export const AIChatContent: React.FC = React.memo( default: return null } - }, [activeKey, runTimeId, httpRunTimeIDs, riskRunTimeIDs, RelatedRuntimeIDs, filterTagDom, OperationLogList]) - - const { onOpenLogWindow } = useAiChatLog() + }, [activeKey, runTimeId, httpTabUpdate, riskTabUpdate, RelatedRuntimeIDs, filterTagDom, OperationLogList]) const onActiveKey = useMemoizedFn((key: AITabsEnumType) => { if (activeKey === key) { @@ -286,31 +243,15 @@ export const AIChatContent: React.FC = React.memo( setHasFilePreview(false) } }) - const onOpenLog = useMemoizedFn((e) => { - e.stopPropagation() - onOpenLogWindow() - }) - const { resizeBoxProps, emitResizeBox } = useAIChatResizeBox({ + const { resizeBoxProps } = useAIChatResizeBox({ activeKey, showFreeChat, timeLine, - taskChat, hasTaskTabs, hasFilePreview, }) - // useMount(() => { - // const onFilePreviewReady = () => { - // emitResizeBox({ - // secondRatio: "432px" - // }) - // } - // emiter.on("filePreviewReady", onFilePreviewReady) - // return () => { - // emiter.off("filePreviewReady", onFilePreviewReady) - // } - // }) const startRequest = useMemoizedFn((data: AIHandleStartParams) => { return new Promise((resolve) => { resolve({ @@ -324,51 +265,7 @@ export const AIChatContent: React.FC = React.memo( return (
- -
-
-
- -
{activeChat?.Title || t('AIChatContent.newChatTitle')}
- - } onClick={() => onNewChat()}> - {t('AIChatContent.newChat')} - - {/* */} -
-
- - } onClick={onOpenLog}> - {t('AIChatContent.log')} - - } onClick={onOpenExportModal}> - {t('AIChatContent.exportLog')} - -
-
- {yakExecResult.card.length > 0 ? ( - - ) : null} -
-
+
= React.memo( chatContainerHeaderClassName={classNames({ [styles['re-act-chat-container-header']]: !activeKey, })} - mode={!!activeKey ? 'task' : 'welcome'} showFreeChat={showFreeChat} setShowFreeChat={setShowFreeChat} startRequest={startRequest} @@ -410,12 +306,6 @@ export const AIChatContent: React.FC = React.memo(
-
) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss new file mode 100644 index 0000000000..70bb3bc41a --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss @@ -0,0 +1,85 @@ +.expand-retract-wrapper { + width: 100%; + border-bottom: 0; + z-index: 8; + .expand-retract-content { + @extend %display-column-center; + width: 100%; + flex: 1; + + .header { + @extend %display-flex-center; + justify-content: space-between; + gap: 12px; + width: 100%; + flex: 1; + .title { + @extend %display-flex-center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 14px; + font-weight: 500; + line-height: 20px; + .chat-alt-icon { + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Disable); + } + } + .chat-title { + max-width: 250px; + @include mixin.text-ellipsis(); + } + } + .extra { + @extend %display-flex-center; + gap: 16px; + overflow-x: auto; + } + + .divider-style { + width: 1px; + height: 12px; + border-left: 1px solid var(--Colors-Use-Neutral-Border); + } + } + } + &:hover { + background-color: var(--Colors-Use-Neutral-Bg); + .expand-retract-animation-wrapper { + width: 54px; + } + } +} +.expand-retract-wrapper-collapsed { + &:hover { + background-color: var(--Colors-Use-Basic-Background); + cursor: default; + } +} + +.expand-retract-animation-wrapper { + top: unset; + bottom: -12px; +} + +.expand-retract-animation-wrapper-hidden { + display: none; +} + +.card-list-wrapper { + width: 100%; + opacity: 1; + transition: all 0.3s ease; + padding-top: 12px; + .card-item { + height: 50px; + font-size: 14px; + } +} +.card-list-wrapper-hidden { + padding-top: 0; + height: 0; + opacity: 0; +} diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.tsx new file mode 100644 index 0000000000..6b3ae4d647 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.tsx @@ -0,0 +1,134 @@ +import React, { useState } from 'react' +import { AIHorizontalScrollCardProps } from './type' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { HorizontalScrollCard } from '@/pages/plugins/operator/horizontalScrollCard/HorizontalScrollCard' +import { useStore } from 'zustand' +import classNames from 'classnames' +import styles from './AIHorizontalScrollCard.module.scss' +import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import { OutlinePlussmIcon, OutlineNewspaperIcon, OutlineClouddownloadIcon } from '@/assets/icon/outline' +import { SolidChatalt2Icon } from '@/assets/icon/solid' +import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' +import { ExpandAndRetract } from '@/pages/plugins/operator/expandAndRetract/ExpandAndRetract' +import { useMemoizedFn } from 'ahooks' +import { Divider } from 'antd' +import { onNewChat } from '../../historyChat/HistoryChat' +import AIContextToken from '../AIContextToken/AIContextToken' +import useAIAgentStore from '../../useContext/useStore' +import useAiChatLog from '@/hook/useAiChatLog/useAiChatLog.ts' +import { ExportAILogsModal } from '../../components/ExportAILogsModal/ExportAILogsModal' +import { failed, yakitNotify } from '@/utils/notification' +import { grpcExportAILogs } from '../../grpc' + +export const AIHorizontalScrollCard: React.FC = React.memo((props) => { + const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) + + const [isExpand, setIsExpand] = useState(true) + + const [exportModalVisible, setExportModalVisible] = useState(false) + const [exportLoading, setExportLoading] = useState(false) + + const { activeChat } = useAIAgentStore() + const { onOpenLogWindow } = useAiChatLog() + + const store = useCurrentStore() + const yakExecResultCard = useStore(store, (state) => state.card) + const execute = useStore(store, (state) => state.execute) + + const onExpand = useMemoizedFn((e) => { + e.stopPropagation() + setIsExpand(!isExpand) + }) + + const onOpenLog = useMemoizedFn((e) => { + e.stopPropagation() + onOpenLogWindow() + }) + const onOpenExportModal = useMemoizedFn((e) => { + e.stopPropagation() + setExportModalVisible(true) + }) + + const onExportCancel = useMemoizedFn(() => { + setExportModalVisible(false) + }) + + const onExportOk = useMemoizedFn(async (data: { types: string[]; outputPath: string }) => { + if (!activeChat?.Id) { + failed(t('AIChatContent.noActiveChat')) + return + } + setExportLoading(true) + // + try { + await grpcExportAILogs( + { + SessionID: activeChat.SessionID, + ExportDataTypes: data.types, + OutputPath: data.outputPath, + }, + true, + ) + yakitNotify('success', t('YakitNotification.exportSuccess')) + setExportModalVisible(false) + } catch (error) { + failed(t('YakitNotification.exportFailed', { error: error + '' })) + } finally { + setExportLoading(false) + } + }) + return ( + <> + +
+
+
+ +
{activeChat?.Title || t('AIChatContent.newChatTitle')}
+ + } onClick={() => onNewChat()}> + {t('AIChatContent.newChat')} + +
+
+ + } onClick={onOpenLog}> + {t('AIChatContent.log')} + + } onClick={onOpenExportModal}> + {t('AIChatContent.exportLog')} + +
+
+ {yakExecResultCard.length > 0 ? ( + + ) : null} +
+
+ + + ) +}) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/type.ts new file mode 100644 index 0000000000..db54642c60 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/type.ts @@ -0,0 +1 @@ +export interface AIHorizontalScrollCardProps {} diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/hooks/useAIChatResizeBox.ts b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/hooks/useAIChatResizeBox.ts index a3c00a689f..bca26403fb 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/hooks/useAIChatResizeBox.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/hooks/useAIChatResizeBox.ts @@ -3,7 +3,6 @@ import { useCreation } from 'ahooks' import { useRef, useState } from 'react' import { AITabsEnumType } from '../../aiAgentType' import { AITabsEnum } from '../../defaultConstant' -import type { UseTaskChatState } from '@/pages/ai-re-act/hooks/type' type ResizeBoxProps = Omit @@ -13,7 +12,6 @@ interface Params { activeKey?: AITabsEnumType showFreeChat: boolean timeLine: boolean - taskChat: UseTaskChatState /** 任务规划 tabs 是否有内容 */ hasTaskTabs: boolean /** 文件系统是否有预览文件 */ @@ -87,15 +85,7 @@ export function useAIChatResizeBox(params: Params) { ...computed, ...override, } - }, [ - params.activeKey, - params.showFreeChat, - params.timeLine, - params.hasTaskTabs, - params.hasFilePreview, - params.taskChat.elements?.length, - version, - ]) + }, [params.activeKey, params.showFreeChat, params.timeLine, params.hasTaskTabs, params.hasFilePreview, version]) return { resizeBoxProps, diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatWelcome/AIChatWelcome.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatWelcome/AIChatWelcome.tsx index 80ff98b7e5..f1bcd2ceb3 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatWelcome/AIChatWelcome.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatWelcome/AIChatWelcome.tsx @@ -48,22 +48,6 @@ import { reseultKnowledgePlugin, useCheckKnowledgePlugin } from '@/pages/Knowled import useGetAIMaterialsData, { getAIRecommendIconByType } from '@/pages/ai-re-act/hooks/useGetAIMaterialsData' import { AIMentionCommandParams } from '../components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin' -// const sideberRadioOptions = [ -// { -// value: "fileTree", -// label: "文件树" -// }, -// { -// value: "knoledge", -// label: "知识库" -// } -// ] - -// const getRandomItems = (array, count = 3) => { -// const shuffled = [...array].sort(() => 0.5 - Math.random()) -// return shuffled.slice(0, count) -// } - const randomAIMaterialsDataIsEmpty = (randObj) => { try { return ( @@ -83,7 +67,7 @@ enum AIChatWelcomeTabKeyEnum { const AIChatWelcome: React.FC = React.memo( forwardRef((props, ref) => { const { t, i18nRefresh } = useI18nNamespaces(['aiAgent']) - const { onTriageSubmit, onSetReAct, streams, api } = props + const { onTriageSubmit, streams, api } = props const aiChatTextareaRef = useRef({ setMention: () => {}, diff --git a/app/renderer/src/main/src/pages/ai-agent/aiTriageChatTemplate/AITriageChatTemplate.tsx b/app/renderer/src/main/src/pages/ai-agent/aiTriageChatTemplate/AITriageChatTemplate.tsx index 86e1983cae..016d783a58 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiTriageChatTemplate/AITriageChatTemplate.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiTriageChatTemplate/AITriageChatTemplate.tsx @@ -14,7 +14,7 @@ import { QSInputTextarea } from '../template/template' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { yakitNotify } from '@/utils/notification' import { CustomPluginExecuteFormValue } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType' -import { AIStartParams } from '@/pages/ai-re-act/hooks/grpcApi' +import { AISourceEnum, AIStartParams } from '@/pages/ai-re-act/hooks/grpcApi' import classNames from 'classnames' import styles from './AITriageChatTemplate.module.scss' @@ -213,6 +213,7 @@ export const AIForgeForm: React.FC = memo((props) => { const request: AIStartParams = { ForgeName: `${info.ForgeVerboseName || info.ForgeName}`, UserQuery: '', + Source: AISourceEnum.aiAgent, } if (isUIParams) { @@ -221,26 +222,26 @@ export const AIForgeForm: React.FC = memo((props) => { .validateFields() .then(async (value: any) => { const kvPair = getYakExecutorParam({ ...value }) - onSubmit( - { + onSubmit({ + request: { ...request, ForgeParams: kvPair, }, - value, - ) + formValue: value, + }) }) .catch(() => {}) } } else { - onSubmit( - { + onSubmit({ + request: { ...request, UserQuery: question.trim() || '', }, - { + formValue: { UserQuery: question.trim() || '', }, - ) + }) } setTimeout(() => { diff --git a/app/renderer/src/main/src/pages/ai-agent/aiTriageChatTemplate/type.ts b/app/renderer/src/main/src/pages/ai-agent/aiTriageChatTemplate/type.ts index 11fde671de..11599a9d34 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiTriageChatTemplate/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiTriageChatTemplate/type.ts @@ -2,7 +2,7 @@ import { RefObject } from 'react' import { AIStartParams } from '@/pages/ai-re-act/hooks/grpcApi' import { AIForge } from '../type/forge' import { AITool } from '../type/aiTool' -import { AIChatIPCStartParams } from '@/pages/ai-re-act/hooks/type' +import { CustomPluginExecuteFormValue } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType' export interface AIForgeInfoOptProps { info: AIForge @@ -10,11 +10,15 @@ export interface AIForgeInfoOptProps { onClick?: (info: AIForge) => void } +export interface AIForgeFormSubmitParamsProps { + request: AIStartParams + formValue: CustomPluginExecuteFormValue | Record +} export interface AIForgeFormProps { wrapperRef?: RefObject info: AIForge onBack: () => void - onSubmit: (request: AIStartParams, form: AIChatIPCStartParams['extraValue']) => void + onSubmit: (params: AIForgeFormSubmitParamsProps) => void } export interface AIToolFormProps { diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 9e28b074ae..953527a968 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -1,9 +1,8 @@ -import type { AIFileSystemPin, TaskChatTaskInfo, UseChatIPCState } from '@/pages/ai-re-act/hooks/type' +import type { TaskChatTaskInfo, UseChatIPCState } from '@/pages/ai-re-act/hooks/type' import type { AIAgentGrpcApi, AIInputEvent, AIStartParams } from '@/pages/ai-re-act/hooks/grpcApi' -import type { AIYakExecFileRecord, PlanItemDetailsData, ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' +import type { PlanItemDetailsData } from '@/pages/ai-re-act/hooks/aiRender' import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' import type { AISource } from '@/pages/ai-re-act/hooks/grpcApi' -import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' /** 上下文字节统计 */ export interface AIContextStatsDetail { diff --git a/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts b/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts index 9d2fa71960..b21333e5df 100644 --- a/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts +++ b/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts @@ -1,3 +1,4 @@ +/** @deprecated */ import { createContext } from 'react' import { defaultChatIPCData } from '../../defaultConstant' import { cloneDeep } from 'lodash' @@ -21,6 +22,7 @@ export interface AIChatIPCSendParams { optionValue?: string } type MakeOptional = Omit & Partial> + export interface AISendSyncMessageParams { syncType: AIInputEvent['SyncType'] SyncJsonInput?: AIInputEvent['SyncJsonInput'] @@ -34,6 +36,7 @@ export interface AISendConfigHotpatchParams { params: MakeOptional taskId?: AIInputEvent['TaskId'] } +/** @deprecated */ export interface ChatIPCContextDispatcher { /** useChatIPC的各种事件 */ chatIPCEvents: UseChatIPCEvents @@ -49,11 +52,12 @@ export interface ChatIPCContextDispatcher { /**发送 Config-Hotpatch */ handleSendConfigHotpatch: (params: AISendConfigHotpatchParams) => void } - +/** @deprecated */ export interface ChatIPCContextValue { store: ChatIPCContextStore dispatcher: ChatIPCContextDispatcher } +/** @deprecated */ export const defaultDispatcherOfChatIPC: ChatIPCContextDispatcher = { chatIPCEvents: { fetchToken: () => '', @@ -83,6 +87,7 @@ export const defaultDispatcherOfChatIPC: ChatIPCContextDispatcher = { handleSendSyncMessage: () => {}, handleSendConfigHotpatch: () => {}, } +/** @deprecated */ export default createContext({ store: { chatIPCData: cloneDeep(defaultChatIPCData), diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index fce5d82180..dededc0e7e 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -45,7 +45,6 @@ import { SolidChatIcon } from '@/assets/icon/solid' export const AIReActChat: React.FC = React.memo( forwardRef((props, ref) => { const { - mode, chatContainerClassName, chatContainerHeaderClassName, title = '自由对话', @@ -266,11 +265,11 @@ export const AIReActChat: React.FC = React.memo( // #endregion const isShowRetract = useCreation(() => { - return mode === 'task' && showFreeChat - }, [mode, showFreeChat]) + return showFreeChat + }, [showFreeChat]) const isShowExpand = useCreation(() => { - return mode === 'task' && !showFreeChat - }, [mode, showFreeChat]) + return !showFreeChat + }, [showFreeChat]) const handleSwitchShowFreeChat = useMemoizedFn((v) => { setShowFreeChat(v) }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts index e13e4031f3..a937d607d4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts @@ -1,4 +1,4 @@ -import { AIAgentChatMode, HandleStartParams } from '@/pages/ai-agent/aiAgentChat/type' +import { HandleStartParams } from '@/pages/ai-agent/aiAgentChat/type' import { AIChatQSData } from '../hooks/aiRender' import { AIInputEvent } from '../hooks/grpcApi' import React from 'react' @@ -46,7 +46,6 @@ type ExternalParametersRightIcon = Partial<{ [RightIconType.taskDetails]: boolean }> export interface AIReActChatProps { - mode: AIAgentChatMode chatContainerClassName?: string chatContainerHeaderClassName?: string showFreeChat: boolean diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 084f0cd995..365af03ef3 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -1,13 +1,13 @@ import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' import type { AIAgentGrpcApi, AIOutputEvent, AITaskStatusType, AIOutputI18n, AIInputEvent } from './grpcApi' import type { - AIChatIPCStartParams, AIFileSystemPin, AIQuestionQueues, CurrentExecTaskTree, PlanLoadingStatus, UseAIMessageDataState, } from './type' +import { CustomPluginExecuteFormValue } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType' // #region AI-Agent 非会话列表外的渲染数据 /** 插件执行中的文件操作记录 */ @@ -392,7 +392,7 @@ export interface AIChatQSDataBase { /** 节点信息所属的任务聚合组 key(parentTaskId-taskId) */ taskId?: string /** 前端专属数据,供前端逻辑和UI处理使用 */ - extraValue?: AIChatIPCStartParams['extraValue'] + extraValue?: CustomPluginExecuteFormValue | Record /** 参考资料 */ reference?: ChatReferenceMaterialPayload /** 父集合组的key(如果被收集到集合组中, 则存在该字段) */ @@ -533,8 +533,11 @@ export interface ChatStoreState { } // #endregion + /** UI上的头部的card横向滚动列表数据 */ card: AIAgentGrpcApi.AIInfoCard[] + /** 工具卡片相关的文件数据 */ execFileRecord: Map + /** 文件操作列表 */ yakExecResultLogs: StreamResult.Log[] /** 切换session时的loading状态 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentDataBySession.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentDataBySession.ts new file mode 100644 index 0000000000..05aef6f8d6 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentDataBySession.ts @@ -0,0 +1,43 @@ +import { globalSessionEngine } from './ChatMultiSessionController' +import useCurrentSessionId from './useCurrentSessionId' + +/** + * @description 获取当前会话的数据 + */ +export function useCurrentDataBySession() { + const sessionId = useCurrentSessionId() + return globalSessionEngine.ensureSession(sessionId) +} + +/** + * 获取当前数据的 store + */ + +export function useCurrentStore() { + const data = useCurrentDataBySession() + return data.store +} + +/** + * 获取当前数据的 rawData + */ +export function useCurrentRawData() { + const data = useCurrentDataBySession() + return data.rawData +} + +/** + * 获取当前数据的 meta + */ +export function useCurrentMeta() { + const data = useCurrentDataBySession() + return data.meta +} + +/** + * 获取当前数据的 request + */ +export function useCurrentRequest() { + const data = useCurrentDataBySession() + return data.request +} From 5384ad77f0127dc666f90e615bed34bb8a0b6a17 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Thu, 25 Jun 2026 17:16:25 +0800 Subject: [PATCH 010/101] =?UTF-8?q?feat:=E8=A7=84=E5=88=92=E5=86=85?= =?UTF-8?q?=E5=AE=B9=E5=92=8C=E8=87=AA=E7=94=B1=E5=AF=B9=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/aiAgentType.d.ts | 14 +- .../components/aiChatListItem/type.ts | 4 +- .../main/src/pages/ai-agent/type/aiChat.d.ts | 1 - .../ChatIPCContent/ChatIPCContent.ts | 99 ------------- .../ChatIPCContent/useDispatcher.ts | 7 - .../useContext/ChatIPCContent/useStore.ts | 7 - .../ai-re-act/aiReActChat/AIReActChat.tsx | 100 +++++++------ .../AIReActChatContents.tsx | 41 +++--- .../AIReActChatContentsType.ts | 5 +- .../aiReActTaskChat/AIReActTaskChat.tsx | 137 +++++++++++------- .../aiReActTaskChat/useTaskChatExtraAction.ts | 104 ++++++++----- .../src/pages/ai-re-act/hooks/aiRender.ts | 1 + .../main/src/pages/ai-re-act/hooks/grpcApi.ts | 2 +- .../pages/ai-re-act/hooks/useNewChatIPC.ts | 2 - 14 files changed, 237 insertions(+), 287 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts delete mode 100644 app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/useDispatcher.ts delete mode 100644 app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/useStore.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts b/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts index 757d4b201b..c1a65ac70a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts @@ -1,13 +1,9 @@ -import { CSSProperties, Dispatch, ReactNode, SetStateAction, MutableRefObject } from 'react' +import { Dispatch, SetStateAction } from 'react' import { AISession } from './type/aiChat' -import { AITreeNodeProps } from './aiTree/type' -import { HoldGRPCStreamProps, StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' import { AITabsEnum } from './defaultConstant' -import { AIAgentGrpcApi, AIStartParams } from '../ai-re-act/hooks/grpcApi' -import { AIChatQSData, AIStreamOutput } from '../ai-re-act/hooks/aiRender' -import { UseYakExecResultState, PlanLoadingStatus, CurrentExecTaskTree } from '../ai-re-act/hooks/type' -import { ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' -import { UseChatIPCEvents } from '@/pages/ai-re-act/hooks/type' +import { AIStartParams } from '../ai-re-act/hooks/grpcApi' +import { PlanLoadingStatus, CurrentExecTaskTree } from '../ai-re-act/hooks/type' +import type { ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' export interface AIAgentProps { pageId: string } @@ -56,7 +52,7 @@ export interface AIChatLeftSideProps { export type AITabsEnumType = `${AITabsEnum}` export interface AIAgentChatStreamProps { - streams: ReActChatRenderItem[] + streams: ReActChatRenderElement[] session: string defaultExpand?: boolean scrollToBottom: boolean diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/type.ts index 1bb086b5c3..ea74f2dd52 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/type.ts @@ -1,9 +1,9 @@ -import type { ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' +import type { ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' import { AIAgentGrpcApi } from '@/pages/ai-re-act/hooks/grpcApi' import { UseYakExecResultState } from '@/pages/ai-re-act/hooks/type' export interface AIChatListItemProps { - item: ReActChatRenderItem + item: ReActChatRenderElement type: 're-act' | 'task-agent' tasksProps?: { tasks: AIAgentGrpcApi.PlanTask[]; yakExecResult: UseYakExecResultState } hasNext?: boolean diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 953527a968..ae815bd40a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -164,7 +164,6 @@ export interface QueryAISessionRequest { Source?: AISource[] } } - export interface AIAgentChatData { /** http_fuzz_request_change事件通知数据 */ httpFuzzRequest?: AIAgentGrpcApi.HttpFuzzRequestChange diff --git a/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts b/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts deleted file mode 100644 index b21333e5df..0000000000 --- a/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** @deprecated */ -import { createContext } from 'react' -import { defaultChatIPCData } from '../../defaultConstant' -import { cloneDeep } from 'lodash' -import { UseChatIPCEvents, UseChatIPCState } from '@/pages/ai-re-act/hooks/type' -import { AIAgentGrpcApi, AIInputEvent, AIStartParams } from '@/pages/ai-re-act/hooks/grpcApi' -import { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' - -export interface ChatIPCContextStore { - chatIPCData: UseChatIPCState - reviewInfo?: AIChatQSData - planReviewTreeKeywordsMap: Map - reviewExpand: boolean - syncIdInfoMap?: Map -} - -export interface AIChatIPCSendParams { - /**InteractiveJSONInput */ - value: string - id: string - /**用户审阅过程中选择得btn */ - optionValue?: string -} -type MakeOptional = Omit & Partial> - -export interface AISendSyncMessageParams { - syncType: AIInputEvent['SyncType'] - SyncJsonInput?: AIInputEvent['SyncJsonInput'] - params?: MakeOptional - syncID?: string -} - -// @deprecated -export interface AISendConfigHotpatchParams { - hotpatchType: AIInputEvent['HotpatchType'] - params: MakeOptional - taskId?: AIInputEvent['TaskId'] -} -/** @deprecated */ -export interface ChatIPCContextDispatcher { - /** useChatIPC的各种事件 */ - chatIPCEvents: UseChatIPCEvents - /** 发送自由对话 */ - handleSendCasual: (params: AIChatIPCSendParams) => void - /** 任务规划 */ - handleSendTask: (params: AIChatIPCSendParams) => void - /** 停止ai */ - handleStop: () => void - handleSend: (params: AIChatIPCSendParams) => void - /**发送 Sync-Type */ - handleSendSyncMessage: (params: AISendSyncMessageParams) => void - /**发送 Config-Hotpatch */ - handleSendConfigHotpatch: (params: AISendConfigHotpatchParams) => void -} -/** @deprecated */ -export interface ChatIPCContextValue { - store: ChatIPCContextStore - dispatcher: ChatIPCContextDispatcher -} -/** @deprecated */ -export const defaultDispatcherOfChatIPC: ChatIPCContextDispatcher = { - chatIPCEvents: { - fetchToken: () => '', - fetchAIRequest: () => undefined, - fetchCurrentCasualTaskID: () => '', - fetchCurrentTaskPlanID: () => undefined, - onSwitchChat: () => {}, - onStart: () => {}, - onSend: () => {}, - onClose: () => {}, - onReset: () => {}, - handleTaskReviewRelease: () => {}, - fetchChatDataStore: () => undefined, - onDelChats: () => {}, - handleCancelLoadingChange: () => {}, - handleResetTarget: () => {}, - handleUserManualIntervention: () => {}, - handleLoadMoreHistory: () => {}, - handleHasMoreHistory: () => false, - resetCurrentTaskPlanID: () => {}, - }, - handleSendCasual: () => {}, - handleSendTask: () => {}, - handleSend: () => {}, - // handleStart: () => {}, - handleStop: () => {}, - handleSendSyncMessage: () => {}, - handleSendConfigHotpatch: () => {}, -} -/** @deprecated */ -export default createContext({ - store: { - chatIPCData: cloneDeep(defaultChatIPCData), - reviewInfo: undefined, - planReviewTreeKeywordsMap: new Map(), - reviewExpand: false, - }, - dispatcher: defaultDispatcherOfChatIPC, -}) diff --git a/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/useDispatcher.ts b/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/useDispatcher.ts deleted file mode 100644 index 2dba4b2c52..0000000000 --- a/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/useDispatcher.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useContext } from 'react' -import ChatIPCContext, { ChatIPCContextDispatcher } from './ChatIPCContent' - -export default function useChatIPCDispatcher(): ChatIPCContextDispatcher { - const { dispatcher } = useContext(ChatIPCContext) - return dispatcher -} diff --git a/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/useStore.ts b/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/useStore.ts deleted file mode 100644 index 3f17a57293..0000000000 --- a/app/renderer/src/main/src/pages/ai-agent/useContext/ChatIPCContent/useStore.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useContext } from 'react' -import ChatIPCContext, { ChatIPCContextStore } from './ChatIPCContent' - -export default function useChatIPCStore(): ChatIPCContextStore { - const { store } = useContext(ChatIPCContext) - return store -} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index dededc0e7e..a4f945cbc6 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -11,8 +11,6 @@ import { yakitNotify } from '@/utils/notification' import { ColorsChatIcon } from '@/assets/icon/colors' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import classNames from 'classnames' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' import { ChevrondownButton, ChevronleftButton, RoundedStopButton } from './AIReActComponent' import { Tooltip } from 'antd' import { ClockIcon } from '@/assets/newIcon' @@ -29,7 +27,6 @@ import { randomString } from '@/utils/randomUtil' import useAINodeLabel from '../hooks/useAINodeLabel' import useSessionId from '../hooks/useSessionId' import useGetChatDataStoreKey from '../hooks/useGetChatDataStoreKey' -import { AISendSyncMessageParams } from '@/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent' import emiter from '@/utils/eventBus/eventBus' import { omit } from 'lodash' import AIContextToken from '@/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken' @@ -41,6 +38,9 @@ import { OutlineLandPlotIcon, OutlineListTodoIcon } from '@/assets/icon/outline' import TaskDetailsPopover from '@/components/historyAIReActChat/TaskDetailsPopover' import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' import { SolidChatIcon } from '@/assets/icon/solid' +import { useCurrentMeta, useCurrentRawData, useCurrentStore } from '../hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import useCurrentSessionId from '../hooks/useCurrentSessionId' export const AIReActChat: React.FC = React.memo( forwardRef((props, ref) => { @@ -52,18 +52,26 @@ export const AIReActChat: React.FC = React.memo( startRequest, externalParameters, } = props - const { setActiveChat, getSetting } = useAIAgentDispatcher() + const { setActiveChat, getSetting, onStart, onSend } = useAIAgentDispatcher() const { chatDataStoreKey } = useGetChatDataStoreKey() const historyChatAISource = useCreation( () => getAISourceListFromChatDataStoreKey(chatDataStoreKey), [chatDataStoreKey], ) const sessionRef = useRef(undefined) - const { chatIPCData } = useChatIPCStore() - const { chatIPCEvents, handleSendSyncMessage } = useChatIPCDispatcher() - const execute = useCreation(() => chatIPCData.execute, [chatIPCData.execute]) - const focusMode = useCreation(() => chatIPCData.focusMode, [chatIPCData.focusMode]) - const notifyMessage = useCreation(() => chatIPCData.notifyMessage, [chatIPCData.notifyMessage]) + + const sessionId = useCurrentSessionId() + const store = useCurrentStore() + const rawData = useCurrentRawData() + const meta = useCurrentMeta() + const execute = useStore(store, (state) => state.execute) + const focusMode = useStore(store, (state) => state.focusMode) + const notifyMessage = useStore(store, (state) => state.notifyMessage) + const questionQueue = useStore(store, (state) => state.questionQueue) + const cancelCasualLoading = useStore(store, (state) => state.cancelCasualLoading) + const casualLoading = useStore(store, (state) => state.casualLoading) + const todoListUpdate = useStore(store, (state) => state.casualChat?.todoListUpdate) + const casualChat = useStore(store, (state) => state.casualChat) const wrapperRef = useRef(null) @@ -84,8 +92,6 @@ export const AIReActChat: React.FC = React.memo( return activeChat?.SessionID || setting.TimelineSessionID }, [activeChat?.SessionID, setting.TimelineSessionID]) - const questionQueue = useCreation(() => chatIPCData.questionQueue, [chatIPCData.questionQueue]) - const aiChatTextareaRef = useRef({ setMention: () => {}, setValue: () => {}, @@ -161,7 +167,7 @@ export const AIReActChat: React.FC = React.memo( AttachedResourceInfo: attachedResourceInfo, FocusModeLoop: value.focusMode, } - const onStart = (res: AIHandleStartResProps) => { + const onStartChat = (res: AIHandleStartResProps) => { const { params, extraParams, onChat, onChatFromHistory } = res if (!sessionID) { // 创建新的聊天记录 @@ -174,7 +180,7 @@ export const AIReActChat: React.FC = React.memo( StartParams: request, SessionID: session, TitleInitialized: false, - Source: request.Source, + Source: request.Source ?? 'ai', LastUsedAt: new Date().getTime(), isCreate: true, } @@ -195,22 +201,24 @@ export const AIReActChat: React.FC = React.memo( mentionType: 'focusMode', mentionName: params.FocusModeLoop || '', }) - chatIPCEvents.onStart({ token: request.TimelineSessionID!, params, extraValue: extra }) + onStart({ + params, + }) } if (!!startRequest) { startRequest({ params: aiInputEvent, }) .then((res) => { - onStart(res) + onStartChat(res) }) .catch(() => { - onStart({ + onStartChat({ params: aiInputEvent, }) }) } else { - onStart({ + onStartChat({ params: aiInputEvent, }) } @@ -227,16 +235,15 @@ export const AIReActChat: React.FC = React.memo( AttachedResourceInfo: attachedResourceInfo, FocusModeLoop: data.focusMode, } - const onSend = (res: AISendResProps) => { + const onSendChat = (res: AISendResProps) => { const { params } = res - chatIPCEvents.onSend({ + onSend({ token: activeChat.SessionID, type: 'casual', params: { IsFreeInput: true, ...params, }, - extraValue: extra, }) emiter.emit('sessionData', JSON.stringify({ type: 'refresh', sessionId: activeChat.SessionID })) } @@ -245,17 +252,17 @@ export const AIReActChat: React.FC = React.memo( .then((res) => { const { params } = res // 发送到服务端 - onSend({ + onSendChat({ params, }) }) .catch(() => { - onSend({ + onSendChat({ params: chatMessage, }) }) } else { - onSend({ + onSendChat({ params: chatMessage, }) } @@ -278,49 +285,48 @@ export const AIReActChat: React.FC = React.memo( aiChatTextareaRef?.current?.setValue(value ?? '') }) - const cancelCasualLoading = useCreation(() => { - return chatIPCData.cancelCasualLoading - }, [chatIPCData.cancelCasualLoading]) - const casualLoading = useCreation(() => { - return chatIPCData.casualLoading - }, [chatIPCData.casualLoading]) const handleStopCasualTask = useMemoizedFn(() => { - const currentCasualTaskID = chatIPCEvents.fetchCurrentCasualTaskID() - if (!chatIPCData.execute || !currentCasualTaskID) return - - chatIPCEvents.handleCancelLoadingChange('reAct', true) - const params: AISendSyncMessageParams = { - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, + const currentCasualTaskID = meta.currentCasualTaskID + if (!execute || !currentCasualTaskID) return + + store.getState().updateState({ + cancelCasualLoading: true, + }) + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, SyncJsonInput: JSON.stringify({ task_id: currentCasualTaskID }), + SyncID: randomString(8), } - handleSendSyncMessage(params) + onSend({ token: sessionId, type: 'casual', params: info }) }) const getPlanDetails = useMemoizedFn(() => { if (!activeChat?.SessionID) return - return chatIPCEvents.fetchChatDataStore()?.get(activeChat?.SessionID)?.casualChat?.planDetails + return rawData.casualChat.planDetails }) const reActTaskId: string = useCreation(() => { if (!activeChat?.SessionID) return '' return getPlanDetails()?.taskId ?? '' - }, [chatIPCData.casualChat?.toolListRenderNumber, activeChat?.SessionID]) + }, [todoListUpdate, activeChat?.SessionID]) const todoData: TodoListCardData = useCreation(() => { if (!activeChat?.SessionID) return cloneDeep(DefaultTodoListCardData) try { - return getPlanDetails()?.todoList || cloneDeep(DefaultTodoListCardData) + return rawData.casualChat.planDetails?.todoList || cloneDeep(DefaultTodoListCardData) } catch (error) { return cloneDeep(DefaultTodoListCardData) } - }, [chatIPCData.casualChat?.toolListRenderNumber, activeChat?.SessionID]) + }, [todoListUpdate, activeChat?.SessionID]) const aiReActChatContentsRef = useRef(null) + const casualConcurrentTaskList = useCreation(() => { - return chatIPCData.casualChat.elements + return casualChat.elements .filter((item) => item.kind === 'task' && item.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP) .map((item) => item.token) - }, [chatIPCData.casualChat.elements]) + }, [casualChat.elements.length]) const getCasualConcurrentTaskName = useMemoizedFn((token: string) => { const contentMap = chatIPCEvents.fetchChatDataStore()?.get(activeChat?.SessionID || '')?.casualChat?.contents @@ -453,7 +459,11 @@ export const AIReActChat: React.FC = React.memo( )} {externalParameters?.rightIcon ? ( <> - {getTaskId() && externalParameters.rightIcon.taskDetails && } + {/* + 自由对话的taskid(在常量中)存在的时候,casualLoading(state)一定为true + TODO - taskid需要改为 store + */} + {casualLoading && externalParameters.rightIcon.taskDetails && } {externalParameters.rightIcon.dataDetails && ( = React.memo( ) : ( <> - {getTaskId() && ( + {casualLoading && ( = React.memo(
)} - +
diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index 5c8ef93fbf..a43eccacd8 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -1,11 +1,6 @@ -import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react' +import React, { forwardRef, useCallback, useImperativeHandle, useMemo } from 'react' import classNames from 'classnames' -import { - AIReActChatContentsPProps, - AIReferenceNodeProps, - AIReActChatContentsRef, - AIStreamNodeProps, -} from './AIReActChatContentsType' +import { AIReActChatContentsPProps, AIReferenceNodeProps, AIStreamNodeProps } from './AIReActChatContentsType' import styles from './AIReActChatContents.module.scss' import { useCreation } from 'ahooks' import { AIMarkdown } from '@/pages/ai-agent/components/aiMarkdown/AIMarkdown' @@ -21,16 +16,16 @@ import { Virtuoso } from 'react-virtuoso' import useVirtuosoAutoScroll from '../hooks/useVirtuosoAutoScroll' import useChatStreamLocateHighlight from '../hooks/useChatStreamLocateHighlight' import { ChatReferenceMaterialPayload, ReActChatRenderItem } from '../hooks/aiRender' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' import Loading from '@/components/Loading/Loading' import { ScrollText } from '@/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading' import { showYakitModal } from '@/components/yakitUI/YakitModal/YakitModalConfirm' import { YakitEditor } from '@/components/yakitUI/YakitEditor/YakitEditor' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import useLoadHistory from '../hooks/useLoadHistory' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import AITextSyntaxFlow from '@/pages/ai-agent/components/aiTextSyntaxFlow/AITextSyntaxFlow' +import { useCurrentStore } from '../hooks/useCurrentDataBySession' +import { useStore } from 'zustand' const getAIReferenceNodeByType = (contentType?: string) => { switch (contentType) { @@ -53,7 +48,8 @@ export const AIStreamNode: React.FC = React.memo((props) => { const { NodeId, content, NodeIdVerbose, CallToolID, ContentType, status } = stream.data // 是否仍在流式输出(结束态 status 为 'end',历史消息亦为 'end',据此控制流式淡入效果) const streaming = status !== 'end' - const { yakExecResult } = useChatIPCStore().chatIPCData + const store = useCurrentStore() + const execFileRecord = useStore(store, (state) => state.execFileRecord) const { nodeLabel } = useAINodeLabel(NodeIdVerbose) const modalInfo: ModalInfoProps = useCreation(() => { @@ -93,7 +89,6 @@ export const AIStreamNode: React.FC = React.memo((props) => { /> ) case AIStreamContentType.TEXT_PLAIN: { - const { execFileRecord } = yakExecResult const fileList = execFileRecord.get(CallToolID) return ( = React.memo((props) => { } }) const TYPE = 'reAct' -export const AIReActChatContents = React.memo( - forwardRef((props, ref) => { - const { chats } = props - const listRootRef = useRef(null) - const { - casualTitle, - requestHistoryState: { casualLoadMoreLoading }, - execute, - } = useChatIPCStore().chatIPCData +export const AIReActChatContents: React.FC = React.memo( + forwardRef((props, ref) => { const { activeChat } = useAIAgentStore() + const store = useCurrentStore() + const casualChat = useStore(store, (state) => state.casualChat) + const chatLength = useStore(store, (state) => state.casualChat.elements.length) + const casualTitle = useStore(store, (state) => state.casualTitle) + const execute = useStore(store, (state) => state.execute) + const casualLoadMoreLoading = useStore(store, (state) => state.requestHistoryState.casualLoadMoreLoading) + + /** TODO - 新版中历史逻辑暂未补充 */ const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents - const chatLength = useCreation(() => chats.elements.length, [chats.elements.length]) // 向上滚动加载 const { firstItemIndex, handleLoadMore, isPrependingRef } = useLoadHistory({ loading: casualLoadMoreLoading, @@ -223,10 +218,10 @@ export const AIReActChatContents = React.memo( scrollerRef={setScrollerRef} firstItemIndex={firstItemIndex} atBottomStateChange={setIsAtBottomRef} - data={chats.elements} + data={casualChat} totalListHeightChanged={handleTotalListHeightChanged} itemContent={renderItem} - initialTopMostItemIndex={chats.elements.length > 1 ? chats.elements.length - 1 : 0} + initialTopMostItemIndex={chatLength > 1 ? chatLength - 1 : 0} components={components} atBottomThreshold={50} skipAnimationFrameInResizeObserver diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts index a45adba03d..80db8ea0f9 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts @@ -1,9 +1,6 @@ import type { ChatReferenceMaterialPayload, ChatStream } from '../hooks/aiRender' -import type { UseCasualChatState } from '../hooks/type' -export interface AIReActChatContentsPProps { - chats: UseCasualChatState -} +export interface AIReActChatContentsPProps {} export interface AIReActChatContentsRef { scrollToItemIndex: (arrayIndex: number, behavior?: 'auto' | 'smooth') => void diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx index 0769052a27..d281b51a37 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx @@ -17,9 +17,8 @@ import { } from './AIReActTaskChatType' import styles from './AIReActTaskChat.module.scss' import { AIAgentChatStream, AIChatLeftSide } from '@/pages/ai-agent/chatTemplate/AIAgentChatTemplate' -import { useControllableValue, useCreation, useMemoizedFn, useUpdateEffect } from 'ahooks' +import { useControllableValue, useMemoizedFn, useUpdateEffect } from 'ahooks' import classNames from 'classnames' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' import { ChevrondownButton } from '../aiReActChat/AIReActComponent' import { OutlineArrowscollapseIcon, @@ -30,13 +29,11 @@ import { OutlineInformationcircleIcon, OutlinePlay2Icon, OutlinePositionIcon, - RedoDotIcon, } from '@/assets/icon/outline' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' -import { AIChatQSData, AIChatQSDataTypeEnum, AIReviewType } from '../hooks/aiRender' +import { AIChatQSData, AIChatQSDataTypeEnum } from '../hooks/aiRender' import { YakitPopconfirm } from '@/components/yakitUI/YakitPopconfirm/YakitPopconfirm' -import { AIInputEventHotPatchTypeEnum, AIInputEventSyncTypeEnum, AITaskStatus } from '../hooks/grpcApi' +import { AIInputEvent, AIInputEventHotPatchTypeEnum, AIInputEventSyncTypeEnum, AITaskStatus } from '../hooks/grpcApi' import { Form, Tooltip } from 'antd' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import emiter from '@/utils/eventBus/eventBus' @@ -53,6 +50,9 @@ import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' import { has } from 'lodash' import { AITaskContent } from '../aiTaskContent/AITaskContent' import { useTaskChatExtraAction } from './useTaskChatExtraAction' +import { useCurrentMeta, useCurrentStore } from '../hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import useCurrentSessionId from '../hooks/useCurrentSessionId' const AIReActTaskChat: React.FC = React.memo((props) => { const { setShowFreeChat, setTimeLine, onTaskTabsChange } = props @@ -148,15 +148,17 @@ export default AIReActTaskChat export const AIReActTaskChatContent: React.FC = React.memo((props) => { const { scrollToBottom, onScrollToBottom } = props - const { reviewInfo, chatIPCData } = useChatIPCStore() + /** TODO - 数据未对接 */ + const { reviewInfo } = useChatIPCStore() const { t } = useI18nNamespaces(['aiAgent']) const { activeChat } = useAIAgentStore() - const { taskChat } = chatIPCData - const { onExtraAction, getTaskId } = useTaskChatExtraAction() + const { onExtraAction } = useTaskChatExtraAction() - const streams = useCreation(() => { - return taskChat.elements - }, [taskChat.elements]) + const store = useCurrentStore() + const meta = useCurrentMeta() + const streams = useStore(store, (state) => state.taskChat.elements) + const execute = useStore(store, (state) => state.execute) + const taskStatus = useStore(store, (state) => state.taskStatus) return ( <> @@ -165,12 +167,12 @@ export const AIReActTaskChatContent: React.FC = Rea streams={streams} session={activeChat?.SessionID || ''} scrollToBottom={scrollToBottom} - taskStatus={chatIPCData.taskStatus} + taskStatus={taskStatus} />
{!reviewInfo && streams.length > 0 && (
- {chatIPCData.execute && ( + {execute && ( = Rea {t('AIReActTaskChatContent.globalDirective')} - {chatIPCData.execute && !!getTaskId() && } + {execute && !!meta.currentTaskPlanID && } } @@ -228,9 +230,12 @@ export const AIManualAdditionPopover: React.FC = R export const AIInputSettingPopover: React.FC = React.memo((props) => { const { children } = props + const { onSend } = useAIAgentDispatcher() + + const sessionId = useCurrentSessionId() + const { setting, activeChat } = useAIAgentStore() const { setSetting } = useAIAgentDispatcher() - const { handleSendConfigHotpatch } = useChatIPCDispatcher() const [visible, setVisible] = useControllableValue(props, { defaultValue: false, valuePropName: 'visible', @@ -239,12 +244,14 @@ export const AIInputSettingPopover: React.FC = React const [form] = Form.useForm() const onHotSyncPerceptionTrigger = useMemoizedFn((value: boolean) => { - handleSendConfigHotpatch({ - hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_SyncPerceptionTrigger, - params: { + const info: AIInputEvent = { + IsConfigHotpatch: true, + HotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_SyncPerceptionTrigger, + Params: { SyncPerceptionTrigger: value, }, - }) + } + onSend({ token: sessionId, type: 'casual', params: info }) if (activeChat?.SessionID) { emiter.emit( 'sessionData', @@ -311,16 +318,23 @@ export const AIInputSettingPopover: React.FC = React const AIManualAddition: React.FC = React.memo((props) => { const { chatType, onCancel } = props - const { handleSendSyncMessage, chatIPCEvents } = useChatIPCDispatcher() - const { chatIPCData, syncIdInfoMap } = useChatIPCStore() + + const { onSend } = useAIAgentDispatcher() + + const sessionId = useCurrentSessionId() + const meta = useCurrentMeta() + const store = useCurrentStore() + const taskStatus = useStore(store, (state) => state.taskStatus) + const execute = useStore(store, (state) => state.execute) + + /** TODO - syncIdInfoMap新版未补充 */ + const { syncIdInfoMap } = useChatIPCStore() const [prompt, setPrompt] = useState() const currentCoordinatorIdRef = useRef('') const syncIdOfAddToContext = useRef('') const syncIdOfAddAndReExecute = useRef('') - const taskStatus = useCreation(() => chatIPCData?.taskStatus, [chatIPCData?.taskStatus]) - useUpdateEffect(() => { if (!taskStatus.loading && currentCoordinatorIdRef.current) { onSendRecover(currentCoordinatorIdRef.current) @@ -337,9 +351,9 @@ const AIManualAddition: React.FC = React.memo((props) => }, [syncIdInfoMap]) useEffect(() => { - if (chatIPCData.execute) return + if (execute) return onReset() - }, [chatIPCData.execute]) + }, [execute]) const onReset = useMemoizedFn(() => { onCancel() @@ -353,27 +367,39 @@ const AIManualAddition: React.FC = React.memo((props) => // 加入上下文后,停止任务再恢复任务 syncIdOfAddAndReExecute.current = randomString(8) onAddToContext(syncIdOfAddAndReExecute.current) - const info = chatIPCEvents.fetchCurrentTaskPlanID() + const info = meta.currentTaskPlanID const taskId = info?.taskID const coordinatorId = info?.coordinatorId if (!coordinatorId) return currentCoordinatorIdRef.current = coordinatorId - chatIPCEvents.handleCancelLoadingChange('task', true) + + store.getState().updateState({ + cancelTaskLoading: true, + }) + if (taskStatus?.loading && taskId) { // 选停止当前任务,等待任务停止成功后,再发送恢复的数据 - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, SyncJsonInput: JSON.stringify({ task_id: taskId }), - }) + + SyncID: randomString(8), + } + onSend({ token: sessionId, type: 'task', params: info }) } else { onSendRecover(coordinatorId) } }) const onSendRecover = useMemoizedFn((coordinatorId: string) => { - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_PLAN_AND_EXEC, + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_PLAN_AND_EXEC, SyncJsonInput: JSON.stringify({ coordinator_id: coordinatorId }), - }) + + SyncID: randomString(8), + } + onSend({ token: sessionId, type: 'task', params: info }) currentCoordinatorIdRef.current = '' }) const getTypeBySyncID = useMemoizedFn(() => { @@ -383,11 +409,14 @@ const AIManualAddition: React.FC = React.memo((props) => }) const onAddToContext = useMemoizedFn((syncID: string) => { if (!prompt?.trim()) return - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_USER_INTERVENTION, + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_USER_INTERVENTION, SyncJsonInput: JSON.stringify({ content: prompt }), - syncID: syncID, - }) + + SyncID: randomString(8), + } + onSend({ token: sessionId, type: 'task', params: info }) onAddToList() }) const onAddToList = useMemoizedFn(() => { @@ -400,7 +429,8 @@ const AIManualAddition: React.FC = React.memo((props) => AIService: '', AIModelName: '', } - chatIPCEvents.handleUserManualIntervention(chatData) + /** TODO - 数据暂无对接 */ + // chatIPCEvents.handleUserManualIntervention(chatData) }) return (
e.stopPropagation()}> @@ -593,22 +623,15 @@ const AIPlanPrompt: React.FC = React.memo( export const AIRenderTaskFooterExtra: React.FC = React.memo((props) => { const { onExtraAction, btnProps, children } = props const { t } = useI18nNamespaces(['aiAgent']) - const { chatIPCEvents } = useChatIPCDispatcher() - const { chatIPCData } = useChatIPCStore() - const taskChat = useCreation(() => { - return chatIPCData.taskChat - }, [chatIPCData.taskChat]) + const store = useCurrentStore() + const meta = useCurrentMeta() - const taskStatus = useCreation(() => { - return chatIPCData.taskStatus - }, [chatIPCData.taskStatus]) + const taskStatus = useStore(store, (state) => state.taskStatus) + const cancelTaskLoading = useStore(store, (state) => state.cancelTaskLoading) - const cancelTaskLoading = useCreation(() => { - return chatIPCData.cancelTaskLoading - }, [chatIPCData.cancelTaskLoading]) const getTaskInfo = useMemoizedFn(() => { - return chatIPCEvents.fetchCurrentTaskPlanID() + return meta.currentTaskPlanID }) const renderBtn = useMemoizedFn(() => { @@ -617,7 +640,9 @@ export const AIRenderTaskFooterExtra: React.FC = R return ( { - chatIPCEvents.handleCancelLoadingChange('task', true) + store.getState().updateState({ + cancelTaskLoading: true, + }) onExtraAction('stopTask', '') }} title={t('AIRenderTaskFooterExtra.cancelTaskConfirm')} @@ -645,7 +670,9 @@ export const AIRenderTaskFooterExtra: React.FC = R radius="28px" size="large" onClick={() => { - chatIPCEvents.handleCancelLoadingChange('task', true) + store.getState().updateState({ + cancelTaskLoading: true, + }) onExtraAction('recover', '') }} loading={cancelTaskLoading} @@ -703,7 +730,9 @@ export const AIRenderTaskFooterExtra: React.FC = R }) export const AIReActTaskChatLeftSide: React.FC = React.memo((props) => { - const { taskChat } = useChatIPCStore().chatIPCData + const store = useCurrentStore() + const taskChat = useStore(store, (state) => state.taskChat) + const [leftExpand, setLeftExpand] = useControllableValue(props, { defaultValue: true, valuePropName: 'leftExpand', diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts index 2d599e2972..3f54297274 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts @@ -1,42 +1,75 @@ import { useMemoizedFn } from 'ahooks' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' -import { AIReviewType } from '../hooks/aiRender' -import { AIInputEventSyncTypeEnum } from '../hooks/grpcApi' +import { AIInputEvent, AIInputEventSyncTypeEnum } from '../hooks/grpcApi' +import useCurrentSessionId from '../hooks/useCurrentSessionId' +import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' +import { randomString } from '@/utils/randomUtil' +import { useStore } from 'zustand' +import { useCurrentStore, useCurrentMeta } from '../hooks/useCurrentDataBySession' -/** 只适用于任务规划的content footer下,不适用于子任务的上的继续 */ +/** + * 只适用于任务规划的content footer下,不适用于子任务的上的继续 + * TODO -需要更新为最新的 + */ export const useTaskChatExtraAction = () => { - const { reviewInfo, chatIPCData } = useChatIPCStore() - const { handleSendSyncMessage, chatIPCEvents } = useChatIPCDispatcher() - const getTaskInfo = useMemoizedFn(() => chatIPCEvents.fetchCurrentTaskPlanID()) - const getTaskId = useMemoizedFn(() => getTaskInfo()?.taskID) + /** TODO - 数据未对接 */ + // const { reviewInfo, chatIPCData } = useChatIPCStore() + + const { onSend } = useAIAgentDispatcher() + + const sessionId = useCurrentSessionId() + const store = useCurrentStore() + const meta = useCurrentMeta() + const taskStatus = useStore(store, (state) => state.taskStatus) + const execute = useStore(store, (state) => state.execute) + + const sendReactCancelTask = useMemoizedFn(() => { + const taskId = meta.currentTaskPlanID?.taskID + if (!taskId) return + + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, + SyncJsonInput: JSON.stringify({ task_id: taskId }), + + SyncID: randomString(8), + } + onSend({ token: sessionId, type: 'task', params: info }) + }) const onSendPlayHistoryList = useMemoizedFn(() => { - chatIPCData.execute && handleSendSyncMessage({ syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS }) + if (execute) { + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS, + + SyncID: randomString(8), + } + onSend({ token: sessionId, type: 'task', params: info }) + } }) /**取消当前指定任务 */ const onStopTask = useMemoizedFn(() => { - const taskId = getTaskId() - if (!taskId) return - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, - SyncJsonInput: JSON.stringify({ task_id: taskId }), - }) - if (!!reviewInfo) { - chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) - } + sendReactCancelTask() + /** TODO - */ + // if (!!reviewInfo) { + // chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) + // } onSendPlayHistoryList() }) /**取消当前执行的子任务 */ const onStopSubTask = useMemoizedFn((syncID: string) => { - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_SKIP_SUBTASK_IN_PLAN, + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_SKIP_SUBTASK_IN_PLAN, SyncJsonInput: JSON.stringify({ reason: '用户认为这个任务不需要执行', skip_current_task: true }), - syncID, - }) - if (!!reviewInfo) { - chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) + + SyncID: randomString(8), } + onSend({ token: sessionId, type: 'task', params: info }) + /** TODO - */ + // if (!!reviewInfo) { + // chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) + // } setTimeout(() => { onSendPlayHistoryList() }, 500) @@ -44,18 +77,23 @@ export const useTaskChatExtraAction = () => { /** @description 在任务规划的content footer下,继续按钮的出现在UI上意味着该任务肯定已经停止 */ const onRecover = useMemoizedFn(() => { - const info = getTaskInfo() + const info = meta.currentTaskPlanID const coordinatorId = info?.coordinatorId if (!coordinatorId) return - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_PLAN_AND_EXEC, + const params: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_PLAN_AND_EXEC, SyncJsonInput: JSON.stringify({ coordinator_id: coordinatorId }), - }) - chatIPCEvents.resetCurrentTaskPlanID() - if (!!reviewInfo) { - chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) + SyncID: randomString(8), } + onSend({ token: sessionId, type: 'task', params }) + meta.currentTaskPlanID = undefined + + /** TODO - */ + // if (!!reviewInfo) { + // chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) + // } }) const onExtraAction = useMemoizedFn((type: 'stopTask' | 'stopSubTask' | 'recover', syncID: string) => { @@ -74,5 +112,5 @@ export const useTaskChatExtraAction = () => { } }) - return { onExtraAction, getTaskId } + return { onExtraAction } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 365af03ef3..c2197f48ae 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -473,6 +473,7 @@ export interface ChatStoreState { httpFuzzRequestUpdate: number httpFlowFuzzStatusUpdate: number + /** 更新会话标题名 */ sessionTitleUpdate: number /** 记忆列表 */ memoryListUpdate: number diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts index 0929fe02b9..34ff6034d7 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts @@ -151,7 +151,7 @@ export interface AIStartParams { /** * */ - Source: AISource + Source?: AISource EnabledCapabilities?: AIEnabledCapability[] diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts index afb9536dc6..d3916c90f7 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts @@ -5,7 +5,6 @@ import { AIChatIPCStartParams, AIChatSendParams } from './type' import { useMemoizedFn } from 'ahooks' import useCurrentSessionId from './useCurrentSessionId' -import { AISource } from './grpcApi' const { ipcRenderer } = window.require('electron') export function useChatIPC() { @@ -18,7 +17,6 @@ export function useChatIPC() { // 这里一定要使用useCallback,通过依赖的sessionId变化,将上一个sessionId的on监听事件进行闭包 // 多个sessionId就会生成多个自己的监听闭包,从而实现多会话的独立监听 - // TODO - 需要AISource const onStart = useCallback( (args: AIChatIPCStartParams, onSuccess?: (sessionId: string) => void) => { // 监听网络,直接丢给大脑处理 From adf9a79dcb39e734d7ab12bb2f86f058f1ed0be3 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Mon, 29 Jun 2026 10:46:56 +0800 Subject: [PATCH 011/101] =?UTF-8?q?feat:=E6=97=A7=E7=89=88=E5=8F=96?= =?UTF-8?q?=E5=80=BC=E6=9B=B4=E6=8D=A2=E4=B8=BA=E6=96=B0=E7=89=88,?= =?UTF-8?q?=E6=97=A7=E7=89=88=E5=8F=91=E9=80=81=E6=B6=88=E6=81=AF=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=96=B0=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../buildConcurrentStreamContext.ts | 1 - .../ai-agent/aiAgentChat/AIAgentChat.tsx | 20 +-- .../AIHorizontalScrollCard.module.scss | 2 + .../aiModelSelect/AIModelSelect.tsx | 6 +- .../chatTemplate/AIAgentChatTemplate.tsx | 131 +++++++++----- .../TimelineCard/TimelineCard.tsx | 18 +- .../aiMemoryList/AIMemoryList.tsx | 42 +++-- .../AITaskExecutionDetails.tsx | 63 ++++--- .../historyTaskTree/HistoryTaskTree.tsx | 115 +++++++------ .../ai-agent/components/ToolInvokerCard.tsx | 4 - .../AIFileChatContent.module.scss | 59 ------- .../aIFileChatContent/AIFileChatContent.tsx | 160 ------------------ .../aiChatListItem/AIChatListItem.tsx | 62 ++----- .../StreamingChatContent.tsx | 19 +-- .../hooks/useStreamingChatContent.ts | 21 +-- .../hooks/useTypedStream.ts | 8 +- .../FileTreeSystem/FileTreeSystem.tsx | 9 +- .../aiGroupStreamCard/AIGroupStreamCard.tsx | 75 +------- .../aiReActChatReview/AIReActChatReview.tsx | 69 ++++---- .../AIReActChatReviewType.d.ts | 4 +- .../main/src/pages/ai-agent/utils/index.ts | 9 +- .../ai-re-act/aiReActChat/AIReActChat.tsx | 4 +- .../aiReActTaskChat/AIReActTaskEmpty.tsx | 145 ++++++++++++++++ .../ai-re-act/aiTaskContent/AITaskContent.tsx | 16 +- 24 files changed, 468 insertions(+), 594 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aIFileChatContent/AIFileChatContent.module.scss delete mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aIFileChatContent/AIFileChatContent.tsx create mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskEmpty.tsx diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/buildConcurrentStreamContext.ts b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/buildConcurrentStreamContext.ts index 88ad8de766..e25480491e 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/buildConcurrentStreamContext.ts +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/buildConcurrentStreamContext.ts @@ -32,7 +32,6 @@ export function buildConcurrentStreamContext({ chatIPCData: cloneDeep(defaultChatIPCData), reviewInfo: undefined, planReviewTreeKeywordsMap: new Map(), - reviewExpand: false, }, dispatcher: { ...defaultDispatcherOfChatIPC, diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index c2db4e564e..9831fe5719 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -10,17 +10,10 @@ import { isForcedSetAIModal } from '../aiModelList/utils' import useAIAgentDispatcher from '../useContext/useDispatcher' import cloneDeep from 'lodash/cloneDeep' import { randomString } from '@/utils/randomUtil' -import { - AIChatIPCSendParams, - AISendConfigHotpatchParams, - AISendSyncMessageParams, -} from '../useContext/ChatIPCContent/ChatIPCContent' import { AIReActChatReview } from '@/pages/ai-agent/components/aiReActChatReview/AIReActChatReview' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { OutlineChevrondoubledownIcon, OutlineChevrondoubleupIcon } from '@/assets/icon/outline' import { ChatIPCSendType } from '@/pages/ai-re-act/hooks/type' -import useChatIPCDispatcher from '../useContext/ChatIPCContent/useDispatcher' -import useChatIPCStore from '../useContext/ChatIPCContent/useStore' import { AIAgentGrpcApi, AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' import { AIChatQSData, AIReviewType } from '@/pages/ai-re-act/hooks/aiRender' import { failed, yakitNotify } from '@/utils/notification' @@ -95,11 +88,10 @@ export const AIAgentChat: React.FC = memo((props) => { >(new Map()) const [reviewInfo, setReviewInfo] = useState() - const [reviewExpand, setReviewExpand] = useState(true) // @deprecated const handleShowReview = useMemoizedFn((info: AIChatQSData) => { - setReviewExpand(true) + // setReviewExpand(true) setReviewInfo(cloneDeep(info)) }) // @deprecated @@ -224,7 +216,7 @@ export const AIAgentChat: React.FC = memo((props) => { // 清空review信息 setReviewInfo(undefined) resetPlanReviewTreeKeywords() - setReviewExpand(true) + // setReviewExpand(true) }) useEffect(() => { @@ -709,12 +701,7 @@ export const AIAgentChat: React.FC = memo((props) => { export const AIReActTaskChatReview: React.FC = React.memo((props) => { const { t } = useI18nNamespaces(['aiAgent']) const { reviewInfo, planReviewTreeKeywordsMap, footerExtra } = props - const { reviewExpand } = useChatIPCStore() - const { handleSendTask, handleSendSyncMessage } = useChatIPCDispatcher() const [expand, setReviewExpand] = useState(true) - useEffect(() => { - setReviewExpand(reviewExpand) - }, [reviewExpand]) const handleExpand = useMemoizedFn(() => { setReviewExpand((old) => !old) }) @@ -741,9 +728,8 @@ export const AIReActTaskChatReview: React.FC = React >
= React.memo((props) => const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const { isOpen = true, mountContainer, className } = props //#region AI model - const { chatIPCData } = useChatIPCStore() const [aiType, setAIType] = useState('online') //暂时只有online,后续会加"local" @@ -278,9 +276,7 @@ export const AIModelSelect: React.FC = React.memo((props) => visionItem && list.push(visionItem) return list }, [selectIntelligentItem, lightweightModels, visionModels]) - const execute = useCreation(() => { - return chatIPCData.execute - }, [chatIPCData.execute]) + const onSelectPolicy = useMemoizedFn((value) => { setAIModelOptions((old) => { return { diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx index 3813358f49..f7d60a3b6d 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx @@ -5,13 +5,13 @@ import { OutlineChevronrightIcon } from '@/assets/icon/outline' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import { grpcQueryAIToolDetails } from '../grpc' +import { AIChatQSData, AIChatQSDataTypeEnum, ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' import { - AIChatQSData, - AIChatQSDataTypeEnum, - AITaskStartInfo, - ReActChatRenderItem, -} from '@/pages/ai-re-act/hooks/aiRender' -import { AIAgentGrpcApi, AIEventQueryRequest, AIInputEventSyncTypeEnum } from '@/pages/ai-re-act/hooks/grpcApi' + AIAgentGrpcApi, + AIEventQueryRequest, + AIInputEvent, + AIInputEventSyncTypeEnum, +} from '@/pages/ai-re-act/hooks/grpcApi' import { taskAnswerToIconMap } from '../defaultConstant' import { AIChatListItem } from '../components/aiChatListItem/AIChatListItem' import StreamCard from '../components/StreamCard' @@ -27,14 +27,17 @@ import { PreWrapper } from '../components/ToolInvokerCard' import { YakitRadioButtons } from '@/components/yakitUI/YakitRadioButtons/YakitRadioButtons' import TimelineCard from './TimelineCard/TimelineCard' import AIMemoryList from './aiMemoryList/AIMemoryList' -import useChatIPCStore from '../useContext/ChatIPCContent/useStore' import TaskLoading from './TaskLoading/TaskLoading' import { YakitResizeBox, YakitResizeBoxProps } from '@/components/yakitUI/YakitResizeBox/YakitResizeBox' -import useChatIPCDispatcher from '../useContext/ChatIPCContent/useDispatcher' import { HistoryTaskTree } from './historyTaskTree/HistoryTaskTree' import { AIReviewParams } from '../components/aiReviewResult/AIReviewResult' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import useLoadHistory from '@/pages/ai-re-act/hooks/useLoadHistory' +import { useCurrentMeta, useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import useAIAgentDispatcher from '../useContext/useDispatcher' +import { randomString } from '@/utils/randomUtil' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' export enum AIChatLeft { TaskTree = 'task-tree', @@ -46,10 +49,18 @@ export const AIChatLeftSide: React.FC = memo((props) => { const { taskTree, taskName } = props const { t, i18nRefresh } = useI18nNamespaces(['aiAgent']) - const { chatIPCData } = useChatIPCStore() - const { handleSendSyncMessage, chatIPCEvents } = useChatIPCDispatcher() + const { onSend } = useAIAgentDispatcher() + const sessionId = useCurrentSessionId() + + const store = useCurrentStore() + const rawData = useCurrentRawData() + const meta = useCurrentMeta() + + const taskChat = useStore(store, (state) => state.taskChat) + const execute = useStore(store, (state) => state.execute) + const memoryListUpdate = useStore(store, (state) => state.memoryListUpdate) + const planHistoryList = useStore(store, (state) => state.planHistoryList) - const { taskChat, memoryList } = useChatIPCStore().chatIPCData const [activeTab, setActiveTab] = useState(AIChatLeft.Timeline) const [expand, setExpand] = useControllableValue(props, { defaultValue: true, @@ -65,34 +76,28 @@ export const AIChatLeftSide: React.FC = memo((props) => { } }, [hasTaskTree]) - const planHistoryList = useCreation(() => { - return ( - chatIPCData.planHistoryList || { - total: 0, - records: [], - session_id: '', - } - ) - }, [chatIPCData.planHistoryList]) const length = useCreation(() => { - return memoryList?.memories?.length - }, [memoryList?.memories?.length]) - const getTaskInfo = useMemoizedFn(() => { - return chatIPCEvents.fetchCurrentTaskPlanID() - }) + return rawData?.memoryList?.memories?.length || 0 + }, [memoryListUpdate]) const handleCancelExpand = useMemoizedFn(() => { setExpand(false) }) const onSendPlayHistoryList = useMemoizedFn(() => { - handleSendSyncMessage({ syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS }) + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS, + + SyncID: randomString(8), + } + onSend({ token: sessionId, type: 'task', params: info }) }) const renderDom = useMemoizedFn(() => { switch (activeTab) { case AIChatLeft.TaskTree: - const coordinatorId = getTaskInfo()?.coordinatorId || '' + const coordinatorId = meta.currentTaskPlanID?.coordinatorId || '' const currentTaskItem: AIAgentGrpcApi.PlanHistory = { coordinator_id: coordinatorId, created_at: '', @@ -125,7 +130,7 @@ export const AIChatLeftSide: React.FC = memo((props) => { const handleTabChange = useMemoizedFn((value: AIChatLeft) => { setActiveTab(value) - if (chatIPCData.execute && value === AIChatLeft.TaskTree) { + if (execute && value === AIChatLeft.TaskTree) { onSendPlayHistoryList() } }) @@ -201,11 +206,32 @@ export const AIAgentChatStream: React.FC = memo((props) const { streams, scrollToBottom, taskStatus, session } = props const listRootRef = useRef(null) + const [highlightedItem, setHighlightedItem] = useState<{ index: number; token: number } | null>(null) + const highlightRafRef = useRef(0) + const highlightObserverRef = useRef(null) + + const store = useCurrentStore() + const rawData = useCurrentRawData() + const meta = useCurrentMeta() + /** TODO - hooks未写 */ const { handleLoadMoreHistory, handleHasMoreHistory, fetchChatDataStore } = useChatIPCDispatcher().chatIPCEvents + useUpdateEffect(() => { + scrollToIndex('LAST') + }, [scrollToBottom]) - const { - requestHistoryState: { taskLoadMoreLoading }, - } = useChatIPCStore().chatIPCData + const taskLoadMoreLoading = useStore(store, (state) => state.requestHistoryState.taskLoadMoreLoading) + + useEffect(() => { + if (!highlightedItem) return + + const clearTimer = window.setTimeout(() => { + setHighlightedItem(null) + }, 1600) + + return () => { + window.clearTimeout(clearTimer) + } + }, [highlightedItem]) // 向上滚动加载 const { firstItemIndex, handleLoadMore, isPrependingRef } = useLoadHistory({ @@ -235,7 +261,7 @@ export const AIAgentChatStream: React.FC = memo((props) }, [scrollToBottom]) const renderItem = useCallback( - (index: number, stream: ReActChatRenderItem) => { + (index: number, stream: ReActChatRenderElement) => { if (!stream.token) return null const arrayIndex = index - firstItemIndex const hasNext = streams.length - arrayIndex > 1 @@ -273,20 +299,31 @@ export const AIAgentChatStream: React.FC = memo((props) }), [Footer, Header, Item], ) + /** TODO - 需验证一下 */ const onTreeLocate = useMemoizedFn((id?: string) => { if (!id) return - const index = streams.findIndex((item) => { - if (item.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) return false - const chatItem = fetchChatDataStore()?.getContentMap({ - session, - chatType: item.chatType, - mapKey: item.token, - }) - if (!chatItem) return false - return (chatItem.data as AITaskStartInfo).taskId === id - }) - locateToIndex(index, 'auto') + // const index = streams.findIndex((item) => { + // if (item.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) return false + // const chatItem = fetchChatDataStore()?.getContentMap({ + // session, + // chatType: item.chatType, + // mapKey: item.token, + // }) + // if (!chatItem) return false + // return (chatItem.data as AITaskStartInfo).taskId === id + // }) + const index = streams.findIndex((item) => item.token === id) + if (index !== -1) locateToIndex(index, 'auto') }) + // const onScrollToIndex = useMemoizedFn((id) => { + // if (!meta?.currentTaskPlanID?.taskID) return false + // const taskID = generateTaskNodeID(meta?.currentTaskPlanID?.taskID, id) + // const index = streams.findIndex((item) => taskID === item.token) + // if (index !== -1) { + // scrollToIndex(index, 'auto') + // waitAndHighlight(index) + // } + // }) useMount(() => { // 仅监听 Ready:由 AITaskContent 保证深度规划已可见后再发 emiter.on('onAITreeLocatePlanningListReady', onTreeLocate) @@ -295,8 +332,8 @@ export const AIAgentChatStream: React.FC = memo((props) } }) return ( -
- +
+ ref={virtuosoRef} key={session} scrollerRef={setScrollerRef} @@ -325,7 +362,8 @@ export const AIChatToolDrawerContent: React.FC = m const [toolList, setToolList] = useState([]) const [loading, setLoading] = useState(false) - const { yakExecResult } = useChatIPCStore().chatIPCData + const store = useCurrentStore() + const execFileRecord = useStore(store, (state) => state.execFileRecord) const getList = useMemoizedFn(() => { if (!callToolId) return @@ -352,7 +390,6 @@ export const AIChatToolDrawerContent: React.FC = m <> {toolList.map((info) => { const { id, Timestamp, type, data } = info - const { execFileRecord } = yakExecResult switch (type) { case AIChatQSDataTypeEnum.STREAM: case AIChatQSDataTypeEnum.TOOL_CALL_RESULT: { diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx index 527a2edb35..13484704de 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx @@ -11,9 +11,9 @@ import { OutlineInformationcircleIcon } from '@/assets/icon/outline' import { useMemoizedFn, useSize } from 'ahooks' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import useAIAgentStore from '../../useContext/useStore' -import useChatIPCStore from '../../useContext/ChatIPCContent/useStore' -import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' import useLoadHistory from '@/pages/ai-re-act/hooks/useLoadHistory' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' const TYPE_COLOR_MAP: Record = { user_input: 'info', @@ -82,15 +82,15 @@ const VirtuosoListContainer = forwardRef(({ children, VirtuosoListContainer.displayName = 'VirtuosoListContainer' -const TYPE = 'timelineID' - const TimelineCard: FC = () => { const { activeChat } = useAIAgentStore() - const { - reActTimelines, - // historyState: { timelinesLoading }, - requestHistoryState: { timelinesLoading }, - } = useChatIPCStore().chatIPCData + + const store = useCurrentStore() + + const reActTimelines = useStore(store, (state) => state.reActTimelines) + const timelinesLoading = useStore(store, (state) => state.requestHistoryState.timelinesLoading) + + /** TODO - 等待hooks */ const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents const { virtuosoRef, handleTotalListHeightChanged, setScrollerRef, setIsAtBottomRef } = useVirtuosoAutoScroll({ total: reActTimelines.length, diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx index 689ad3e8bf..775198c4f3 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx @@ -1,6 +1,5 @@ -import React, { useEffect, useRef, useState } from 'react' +import React, { useState } from 'react' import { AIMemoryContentProps, AIMemoryEchartsProps, AIMemoryListProps, AIMemoryScoreEchartsProps } from './type' -import useChatIPCStore from '../../useContext/ChatIPCContent/useStore' import { useCreation, useDebounceEffect, useDebounceFn, useMemoizedFn, useUpdateEffect } from 'ahooks' import styles from './AIMemoryList.module.scss' import { RollingLoadList } from '@/components/RollingLoadList/RollingLoadList' @@ -13,9 +12,14 @@ import ReactResizeDetector from 'react-resize-detector' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { OutlineTrashIcon } from '@/assets/icon/outline' import { YakitPopconfirm } from '@/components/yakitUI/YakitPopconfirm/YakitPopconfirm' -import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' import { grpcDeleteAIMemoryEntity } from '@/pages/memoryBase/utils' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' +import useAIAgentDispatcher from '../../useContext/useDispatcher' +import cloneDeep from 'lodash/cloneDeep' +import { DefaultMemoryList } from '@/pages/ai-re-act/hooks/defaultConstant' const { ipcRenderer } = window.require('electron') const getScoreList = (data: AIAgentGrpcApi.MemoryEntry) => { @@ -52,9 +56,16 @@ const getScoreList = (data: AIAgentGrpcApi.MemoryEntry) => { } const AIMemoryList: React.FC = React.memo((props) => { const { t } = useI18nNamespaces(['aiAgent']) - const { chatIPCData } = useChatIPCStore() - const { handleStop } = useChatIPCDispatcher() - const { chatIPCEvents } = useChatIPCDispatcher() + + const sessionId = useCurrentSessionId() + const store = useCurrentStore() + const rawData = useCurrentRawData() + + const { onClose } = useAIAgentDispatcher() + + const memoryListUpdate = useStore(store, (state) => state.memoryListUpdate) + const execute = useStore(store, (state) => state.execute) + const [width, setWidth] = useState() const [loading, setLoading] = useState(false) @@ -64,23 +75,23 @@ const AIMemoryList: React.FC = React.memo((props) => { useDebounceEffect( () => { if (!isClearMemory) return - const token = chatIPCEvents.fetchToken() - ipcRenderer.once(`${token}-end`, () => { + /** TODO - 需要hooks告诉UI层,此次关闭的type(来源) */ + ipcRenderer.once(`${sessionId}-end`, () => { onClearMemory() setIsClearMemory(false) }) - handleStop() + onClose([sessionId]) }, [isClearMemory], { wait: 1000 }, ) const list = useCreation(() => { - return chatIPCData?.memoryList?.memories || [] - }, [chatIPCData?.memoryList?.memories]) + return rawData?.memoryList?.memories || [] + }, [memoryListUpdate]) const echartsData: AIMemoryEchartsProps['data'] = useCreation(() => { - const data = chatIPCData?.memoryList?.score_overview + const data = rawData?.memoryList?.score_overview const xData: string[] = [] const yData: number[] = [] Object.entries(data || {}).forEach(([key, value]) => { @@ -88,11 +99,11 @@ const AIMemoryList: React.FC = React.memo((props) => { yData.push(value) }) return { xData, yData } - }, [chatIPCData?.memoryList?.score_overview]) + }, [memoryListUpdate]) const onClearMemoryConfirm = useMemoizedFn((e) => { e.stopPropagation() - if (chatIPCData.execute) { + if (execute) { setIsClearMemory(true) } else { onClearMemory() @@ -100,11 +111,12 @@ const AIMemoryList: React.FC = React.memo((props) => { }) const onClearMemory = useMemoizedFn(() => { setLoading(true) + /** TODO - 多会话清空记忆库,数据清空后其余会话会有新数据往记忆库中增加,导致看起来清空失败 */ grpcDeleteAIMemoryEntity({ Filter: {}, }) .then(() => { - chatIPCEvents.handleResetTarget('memoryList') + rawData.memoryList = cloneDeep(DefaultMemoryList) }) .finally(() => { setTimeout(() => { diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx index 0767f753c4..f8c5df98f1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx @@ -18,8 +18,6 @@ import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { YakitPopconfirm } from '@/components/yakitUI/YakitPopconfirm/YakitPopconfirm' import { AIToDoListItem } from '@/pages/ai-re-act/aiReActChat/aiToDoList/AIToDoList' import { useCreation, useInterval, useMemoizedFn, useSelections } from 'ahooks' -import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' -import useAIAgentStore from '../../useContext/useStore' import { ForgesAndSkillsDynamicItem, PlanItemDetailsData, TodoListCardData } from '@/pages/ai-re-act/hooks/aiRender' import cloneDeep from 'lodash/cloneDeep' import isEqual from 'lodash/isEqual' @@ -37,6 +35,7 @@ import { TableTotalAndSelectNumber } from '@/components/TableTotalAndSelectNumbe import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import { AIAgentGrpcApi, + AIInputEvent, AIInputEventHotPatchTypeEnum, AIInputEventSyncTypeEnum, AIStartParams, @@ -52,11 +51,15 @@ import { YakitRadioButtons } from '@/components/yakitUI/YakitRadioButtons/YakitR import { timeDiffWithMoment } from '@/utils/timeUtil' import { AITaskActionItem, AITaskExecutionList } from './aiTaskExecutionList/AITaskExecutionList' import { AIToDoListDetail } from '@/pages/ai-re-act/aiReActChat/aiToDoList/AIToDoListDetail' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' +import useAIAgentDispatcher from '../../useContext/useDispatcher' +import { randomString } from '@/utils/randomUtil' export const AITaskExecutionDetails: React.FC = React.memo((props) => { const { taskId, taskGoal, taskName, onClose } = props - const { chatIPCEvents } = useChatIPCDispatcher() - const { activeChat } = useAIAgentStore() + const rawData = useCurrentRawData() + const store = useCurrentStore() const [planItemDetailsData, setPlanItemDetailsData] = useState() const perPlanItemDetailsDataUUIdRef = useRef('') @@ -395,7 +398,7 @@ export const AITaskExecutionDetails: React.FC = Rea const AIBrowserProcesses: React.FC = React.memo((props) => { const { list } = props - const { handleSendSyncMessage } = useChatIPCDispatcher() + const { handleSendSyncMessage } = useChatIPCDispatcher() // TODO - const onRemove = useMemoizedFn((processes: AIBrowserProcessesProps['list'][number]) => { handleSendSyncMessage({ @@ -429,7 +432,9 @@ const AIBrowserProcesses: React.FC = React.memo((props) const AITaskDetailsAddPopover: React.FC = React.memo((props) => { const { title, type, onClose, taskId } = props - const { handleSendConfigHotpatch, handleSendSyncMessage } = useChatIPCDispatcher() + + const sessionId = useCurrentSessionId() + const { onSend } = useAIAgentDispatcher() const [keyword, setKeyword] = useState() const [loading, setLoading] = useState(false) @@ -650,17 +655,22 @@ const AITaskDetailsAddPopover: React.FC = React.me Type: item.type, } }) - handleSendConfigHotpatch({ - hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_EnabledCapabilities, - params: { + const info: AIInputEvent = { + IsConfigHotpatch: true, + HotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_EnabledCapabilities, + Params: { EnabledCapabilities: enabledCapabilities, }, - taskId, - }) + TaskId: taskId, + } + onSend({ token: sessionId, type: '', params: info }) setTimeout(() => { - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_CAPABILITY_INVENTORY, - }) + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_CAPABILITY_INVENTORY, + SyncID: randomString(8), + } + onSend({ token: sessionId, type: '', params: info }) }, 1000) onClose() }) @@ -760,14 +770,17 @@ const typeOptions = [ const AITaskDetailsCardList: React.FC = React.memo((props) => { const { type, colTitle, fixedList, dynamicList, taskId } = props - const { handleSendConfigHotpatch, handleSendSyncMessage } = useChatIPCDispatcher() + const sessionId = useCurrentSessionId() + const { onSend } = useAIAgentDispatcher() + const [configType, setConfigType] = useState<'fixed' | 'dynamic'>('fixed') const [visible, setVisible] = useState(false) const onRemove = useMemoizedFn((dynamicItem) => { - handleSendConfigHotpatch({ - hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_DisabledCapabilities, - params: { + const info: AIInputEvent = { + IsConfigHotpatch: true, + HotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_DisabledCapabilities, + Params: { EnabledCapabilities: dynamicList .filter((ele) => isEqual(ele, dynamicItem)) .map((item) => ({ @@ -775,12 +788,16 @@ const AITaskDetailsCardList: React.FC = React.memo(( Type: getType(item.category), })), }, - taskId, - }) + TaskId: taskId, + } + onSend({ token: sessionId, type: '', params: info }) setTimeout(() => { - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_CAPABILITY_INVENTORY, - }) + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_CAPABILITY_INVENTORY, + SyncID: randomString(8), + } + onSend({ token: sessionId, type: '', params: info }) }, 1000) }) const renderHeader = useMemoizedFn(() => { diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx index ce9a3703f4..3a45500ae1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx @@ -7,10 +7,8 @@ import { SendRecoverParams, } from './HistoryTaskTreeType' import { useCreation, useMemoizedFn, useUpdateEffect } from 'ahooks' -import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' -import { AIInputEventSyncTypeEnum, AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' +import { AIInputEvent, AIInputEventSyncTypeEnum, AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' import { AITree } from '../../aiTree/AITree' -import useChatIPCStore from '../../useContext/ChatIPCContent/useStore' import YakitCollapse from '@/components/yakitUI/YakitCollapse/YakitCollapse' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { formatTimestamp } from '@/utils/timeUtil' @@ -24,6 +22,9 @@ import useAIAgentStore from '../../useContext/useStore' import { formatAIAgentSetting } from '../../utils' import useAIAgentDispatcher from '../../useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' +import { useCurrentMeta, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' export const HistoryTaskTree: React.FC = memo((props) => { const { data, currentTaskItem } = props @@ -107,19 +108,21 @@ export const HistoryTaskTree: React.FC = memo((props) => { export const AIHistoryContinueTask: React.FC = React.memo((props) => { const { coordinatorId, taskId } = props const { t } = useI18nNamespaces(['aiAgent']) - const { chatIPCData } = useChatIPCStore() - const { chatIPCEvents, handleSendSyncMessage } = useChatIPCDispatcher() + + const sessionId = useCurrentSessionId() + const meta = useCurrentMeta() + const store = useCurrentStore() + const taskStatus = useStore(store, (state) => state.taskStatus) + const cancelTaskLoading = useStore(store, (state) => state.cancelTaskLoading) + const execute = useStore(store, (state) => state.execute) + const { activeChat } = useAIAgentStore() - const { getSetting } = useAIAgentDispatcher() + const { getSetting, onSend, onStart } = useAIAgentDispatcher() const [visible, setVisible] = useState(false) const sendRecoverParamsRef = useRef() - const taskStatus = useCreation(() => { - return chatIPCData.taskStatus - }, [chatIPCData.taskStatus]) - const isExecuting = useCreation(() => { return taskStatus.loading }, [taskStatus.loading]) @@ -129,7 +132,7 @@ export const AIHistoryContinueTask: React.FC = React }, [isExecuting, taskId]) const getTaskInfo = useMemoizedFn(() => { - return chatIPCEvents.fetchCurrentTaskPlanID() + return meta.currentTaskPlanID }) const getTaskId = useMemoizedFn(() => { const taskInfo = getTaskInfo() @@ -144,9 +147,9 @@ export const AIHistoryContinueTask: React.FC = React const currentCoordinatorId = getTaskInfo()?.coordinatorId || '' const taskInfo = getTaskInfo() let show = true - if (!chatIPCData.execute) return true + if (!execute) return true if (coordinatorId === currentCoordinatorId) { - show = taskInfo?.status !== AITaskStatus.inProgress && !chatIPCData?.taskStatus?.loading + show = taskInfo?.status !== AITaskStatus.inProgress } // 如果当前有任务正在等待被恢复 if (sendRecoverParamsRef.current) { @@ -158,19 +161,24 @@ export const AIHistoryContinueTask: React.FC = React const isStopping = taskInfo?.status !== AITaskStatus.inProgress && taskStatus.loading // 如果系统正处于正在停止/取消任务的全局 Loading 状态,或当前任务本身正处于停止进行中的状态 - if (chatIPCData.cancelTaskLoading || isStopping) { + if (cancelTaskLoading || isStopping) { return false } return show }) const onSendRecover = useMemoizedFn((params: SendRecoverParams) => { - const { coordinatorId, taskId } = params - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_PLAN_AND_EXEC, + const { coordinatorId } = params + + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_PLAN_AND_EXEC, SyncJsonInput: JSON.stringify({ coordinator_id: coordinatorId, start_task_id: taskId }), - }) - chatIPCEvents.resetCurrentTaskPlanID() + SyncID: randomString(8), + } + onSend({ token: sessionId, type: 'task', params: info }) + meta.currentTaskPlanID = undefined + sendRecoverParamsRef.current = undefined }) const onRecover = useMemoizedFn(() => { @@ -181,35 +189,38 @@ export const AIHistoryContinueTask: React.FC = React coordinatorId, taskId, } - chatIPCEvents.handleCancelLoadingChange('task', true) + store.getState().updateState({ + cancelCasualLoading: true, + }) if (taskStatus.loading && currentTaskId) { - // 先停止当前任务,等待任务停止成功后,再发送恢复的数据 - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, + // 选停止当前任务,等待任务停止成功后,再发送恢复的数据 + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, SyncJsonInput: JSON.stringify({ task_id: currentTaskId }), - }) - } else if (chatIPCData.execute) { + + SyncID: randomString(8), + } + onSend({ token: sessionId, type: 'task', params: info }) + } else if (execute) { onSendRecover(sendRecoverParamsRef.current) } else if (activeChat?.SessionID && getSetting) { const session = activeChat?.SessionID - chatIPCEvents.onStart( - { - token: session, - params: { - IsStart: true, - Params: { - ...formatAIAgentSetting(getSetting()), - UserQuery: '', - TimelineSessionID: session, - CoordinatorId: '', - Sequence: 1, - }, + onStart({ + params: { + IsStart: true, + Params: { + ...formatAIAgentSetting(getSetting()), + UserQuery: '', + TimelineSessionID: session, + CoordinatorId: '', + Sequence: 1, }, }, - () => { + onSuccess: () => { sendRecoverParamsRef.current && onSendRecover(sendRecoverParamsRef.current) }, - ) + }) } }) return isShow() ? ( @@ -253,24 +264,28 @@ export const AIHistorySkipTask: React.FC<{ taskId?: string | null; isTask?: bool ({ taskId, isTask = true }) => { const { t } = useI18nNamespaces(['aiAgent']) const syncIdOfStopSubTask = useRef('') - const { syncIdInfoMap } = useChatIPCStore() - const { handleSendSyncMessage } = useChatIPCDispatcher() - + // const { syncIdInfoMap } = useChatIPCStore() + const sessionId = useCurrentSessionId() + const { onSend } = useAIAgentDispatcher() const onCancelTask = useMemoizedFn(() => { if (isTask) { if (!taskId) return syncIdOfStopSubTask.current = randomString(8) - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_SKIP_SUBTASK_IN_PLAN, + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_SKIP_SUBTASK_IN_PLAN, SyncJsonInput: JSON.stringify({ reason: '用户认为这个任务不需要执行', subtask_id: taskId }), - syncID: syncIdOfStopSubTask.current, - }) + + SyncID: syncIdOfStopSubTask.current, + } + onSend({ token: sessionId, type: 'task', params: info }) } else { - if (!taskId) return - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, SyncJsonInput: JSON.stringify({ task_id: taskId }), - }) + } + onSend({ token: sessionId, type: 'task', params: info }) } }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx index 85eb94d9c1..f70d9f4e37 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx @@ -10,7 +10,6 @@ import { AIToolResult, AIYakExecFileRecord, ChatListRenderType, - ReActChatBaseInfo, } from '@/pages/ai-re-act/hooks/aiRender' import FileList from './FileList' import type { ModalInfoProps } from './ModelInfo' @@ -158,14 +157,11 @@ const ToolStdoutCard: React.FC = memo((props) => { const { operationInfo, fileList, chatType, data, titleText } = props const { t } = useI18nNamespaces(['aiAgent']) - const { activeChat } = useAIAgentStore() const { handleSend } = useChatIPCDispatcher() // 获取流数据 const { stream } = useStreamingChatContent({ - chatType, token: data.stream.EventUUID, - session: activeChat?.SessionID || '', }) const selectors = useCreation(() => { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aIFileChatContent/AIFileChatContent.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aIFileChatContent/AIFileChatContent.module.scss deleted file mode 100644 index fcec741a65..0000000000 --- a/app/renderer/src/main/src/pages/ai-agent/components/aIFileChatContent/AIFileChatContent.module.scss +++ /dev/null @@ -1,59 +0,0 @@ -@use '../../../../styles/mixin.scss' as mixin; - -.wrapper { - width: 100%; - display: flex; - gap: 4px; - flex-direction: column; - .qs { - width: fit-content; - border-radius: 8px; - background-color: var(--Colors-Use-Main-Focus); - } - .file { - width: fit-content; - width: 80%; - padding: 8px; - border-radius: 8px; - - border-radius: 8px; - background: var(--Colors-Use-Neutral-Bg-Hover); - &-content { - width: 100%; - &-item { - display: flex; - align-items: center; - width: 100%; - height: 24px; - border-bottom: 1px solid var(--Colors-Use-Neutral-Border); - &-left { - flex: 1; - width: 0; - gap: 4px; - display: flex; - cursor: pointer; - align-items: center; - &:hover p { - color: var(--Colors-Use-Main-Primary); - } - p { - color: var(--Colors-Use-Neutral-Text-2-Primary); - margin: 0; - @include mixin.text-ellipsis; - } - - svg { - width: 14px; - height: 14px; - } - } - .content-item-left { - cursor: default; - &:hover p { - color: var(--Colors-Use-Neutral-Text-2-Primary); - } - } - } - } - } -} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aIFileChatContent/AIFileChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aIFileChatContent/AIFileChatContent.tsx deleted file mode 100644 index 7ce446c9d7..0000000000 --- a/app/renderer/src/main/src/pages/ai-agent/components/aIFileChatContent/AIFileChatContent.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import type { AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' -import { FC } from 'react' -import styles from './AIFileChatContent.module.scss' -import { IconNotepadFileTypeDir } from '@/components/MilkdownEditor/icon/icon' -import { renderFileTypeIcon } from '@/components/MilkdownEditor/CustomFile/CustomFile' -import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' -import { OutlineChevronrightIcon } from '@/assets/icon/outline' -import { onOpenLocalFileByPath } from '@/pages/notepadManage/notepadManage/utils' -import { HandleStartParams } from '../../aiAgentChat/type' -import { useCreation, useMemoizedFn } from 'ahooks' -import { - isHaveFreeDialogFileList, - isHaveSelectForges, - isHaveSelectKnowledgeBases, - isHaveSelectTools, -} from '../aiChatListItem/AIChatListItem' -import { AIChatMentionSelectItem, AIMentionTypeItem } from '../aiChatMention/type' -import { iconMap } from '../../defaultConstant' -import emiter from '@/utils/eventBus/eventBus' -import { YakitRoute } from '@/enums/yakitRoute' -import classNames from 'classnames' -import { FileToChatQuestionList } from '../../template/type' - -interface AIFileChatContentProps { - qs: string - setting: AIInputEvent - extraValue?: HandleStartParams['extraValue'] -} - -function getFileExt(path: string): string { - const index = path.lastIndexOf('.') - return index > -1 ? path.slice(index + 1).toLowerCase() : '' -} - -const getFileIcon = (data: FileToChatQuestionList) => { - if (data.isFolder) return - return renderFileTypeIcon({ type: getFileExt(data.path) }) -} -/**@deprecated 废弃 由md编辑器代替 */ -const AIFileChatContent: FC = ({ qs, setting, extraValue }) => { - const attachedFilePathList: FileToChatQuestionList[] = useCreation(() => { - return isHaveFreeDialogFileList(extraValue) - }, [extraValue?.['freeDialogFileList']]) - - const selectForges: AIChatMentionSelectItem[] = useCreation(() => { - return isHaveSelectForges(extraValue) - }, [extraValue?.['selectForges']]) - - const selectTools: AIChatMentionSelectItem[] = useCreation(() => { - return isHaveSelectTools(extraValue) - }, [extraValue?.['selectTools']]) - const selectKnowledgeBases: AIChatMentionSelectItem[] = useCreation(() => { - return isHaveSelectKnowledgeBases(extraValue) - }, [extraValue?.['selectKnowledgeBases']]) - - const onOpenKnowledgeBases = useMemoizedFn(() => { - emiter.emit('menuOpenPage', JSON.stringify({ route: YakitRoute.AI_REPOSITORY })) - }) - const isPadding = useCreation(() => { - return ( - !!attachedFilePathList.length || !!selectForges.length || !!selectTools.length || !!selectKnowledgeBases.length - ) - }, [attachedFilePathList.length, selectForges.length, selectTools.length, selectKnowledgeBases.length]) - const renderList = useMemoizedFn( - (params: { title: string; list: AIChatMentionSelectItem[]; type: AIMentionTypeItem }) => { - const { title, list, type } = params - return ( -
-
{title}
- -
- {list.map((file) => { - return ( -
-
- {iconMap[type]} -

{file.name}

-
-
- ) - })} -
-
- ) - }, - ) - return ( -
-
- {qs} -
- {!!attachedFilePathList?.length && ( -
-
相关操作文件
-
- {attachedFilePathList.map((file) => { - return ( -
onOpenLocalFileByPath(file.path)} - > -
- {getFileIcon(file)} -

{file.path}

-
- - } /> -
- ) - })} -
-
- )} - {!!selectForges?.length && - renderList({ - title: '相关智能体', - list: selectForges, - type: 'forge', - })} - - {!!selectTools?.length && - renderList({ - title: '相关工具', - list: selectTools, - type: 'tool', - })} - {!!selectKnowledgeBases?.length && ( -
-
相关知识库
- -
- {selectKnowledgeBases.map((item) => { - return ( -
-
- {iconMap['knowledgeBase']} -

{item.name}

-
- - } /> -
- ) - })} -
-
- )} -
- ) -} - -export default AIFileChatContent diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx index 0705406089..ae5e2276ce 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react' +import React from 'react' import { AIChatListItemProps } from './type' import { useCreation, useMemoizedFn } from 'ahooks' import { AIReActChatReview } from '../aiReActChatReview/AIReActChatReview' @@ -6,73 +6,34 @@ import { AIReviewResult } from '../aiReviewResult/AIReviewResult' import { AITriageChatContent } from '../aiTriageChat/AITriageChat' import ToolInvokerCard from '../ToolInvokerCard' import styles from './AIChatListItem.module.scss' -import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' import DividerCard from '../DividerCard' import { AIToolDecision } from '../aiToolDecision/AIToolDecision' import { AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' import AiFailPlanCard from '../aiFailPlanCard/AiFailPlanCard' -import classNames from 'classnames' -import { has, isArray } from 'lodash' -import { HandleStartParams } from '../../aiAgentChat/type' -import { AIChatMentionSelectItem } from '../aiChatMention/type' import { AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' -import { FileToChatQuestionList } from '../../template/type' import StreamingChatContent from './StreamingChatContent/StreamingChatContent' import StaticChatContent from './StaticChatContent/StaticChatContent' -import useChatIPCStore from '../../useContext/ChatIPCContent/useStore' import useAIAgentStore from '../../useContext/useStore' import { AIManualIntervention } from '../aiManualIntervention/AIManualIntervention' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { AIModelErrorPrompt } from './aiModelErrorPrompt/AIModelErrorPrompt' import { AIHttpFlowFuzzStatusCard } from '../aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard' import { AIReportFinishCard } from '../aiReportFinishCard/AIReportFinishCard' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' const chatContentExtraProps = { contentClassName: styles['content-wrapper'], chatClassName: styles['question-wrapper'], } -/**@description 额外参数中获取文件列表数据 */ -export const isHaveFreeDialogFileList = (extraValue: HandleStartParams['extraValue']): FileToChatQuestionList[] => { - if (has(extraValue, 'freeDialogFileList') && isArray(extraValue.freeDialogFileList)) { - return extraValue.freeDialogFileList - } - return [] -} -/**@description 额外参数中获取选中的forge */ -export const isHaveSelectForges = (extraValue: HandleStartParams['extraValue']): AIChatMentionSelectItem[] => { - if (has(extraValue, 'selectForges') && isArray(extraValue.selectForges)) { - return extraValue.selectForges - } - return [] -} -/**@description 额外参数中获取选中的 tool */ -export const isHaveSelectTools = (extraValue: HandleStartParams['extraValue']): AIChatMentionSelectItem[] => { - if (has(extraValue, 'selectTools') && isArray(extraValue.selectTools)) { - return extraValue.selectTools - } - return [] -} -/**@description 额外参数中获取选中的 KnowledgeBases */ -export const isHaveSelectKnowledgeBases = (extraValue: HandleStartParams['extraValue']): AIChatMentionSelectItem[] => { - if (has(extraValue, 'selectKnowledgeBases') && isArray(extraValue.selectKnowledgeBases)) { - return extraValue.selectKnowledgeBases - } - return [] -} -const isExtraShow = (extraValue: HandleStartParams['extraValue']) => { - return ( - isHaveFreeDialogFileList(extraValue).length > 0 || - isHaveSelectForges(extraValue).length > 0 || - isHaveSelectTools(extraValue).length > 0 || - isHaveSelectKnowledgeBases(extraValue).length > 0 - ) -} + export const AIChatListItem: React.FC = React.memo((props) => { const { item, type, hasNext, itemIndex, session: sessionProp } = props const { t } = useI18nNamespaces(['aiAgent']) - const { handleSendCasual } = useChatIPCDispatcher() - const { yakExecResult } = useChatIPCStore().chatIPCData + const store = useCurrentStore() + const execFileRecord = useStore(store, (state) => state.execFileRecord) + const { activeChat } = useAIAgentStore() const session = sessionProp || activeChat?.SessionID const aiStreamNodeProps = useCreation(() => { @@ -108,9 +69,9 @@ export const AIChatListItem: React.FC = React.memo((props) content={data?.qs} extraValue={extraValue} {...chatContentExtraProps} - contentClassName={classNames({ - [styles['file-content-wrapper']]: isExtraShow(extraValue), - })} + // contentClassName={classNames({ + // [styles['file-content-wrapper']]: isExtraShow(extraValue), + // })} /> ) // case AIChatQSDataTypeEnum.STREAM: @@ -126,7 +87,6 @@ export const AIChatListItem: React.FC = React.memo((props) /> ) case AIChatQSDataTypeEnum.TOOL_RESULT: - const { execFileRecord } = yakExecResult const fileList = execFileRecord.get(data.callToolId) return ( = React.memo((props) } else { return ( = ({ chatType, token, streamClassName, session, listItemIndex }) => { - const { stream } = useTypedStream({ chatType, token, session }) +const AIStreamCard: FC = ({ token, streamClassName, session, listItemIndex }) => { + const { stream } = useTypedStream({ token }) if (!stream) return null return ( @@ -57,13 +52,7 @@ const StreamingChatContent: FC = (props) => { return } return ( - + ) } export default memo(StreamingChatContent) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts index 82d54317dd..7147f444c9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts @@ -1,13 +1,11 @@ import { useCallback, useRef, useState } from 'react' -import { ChatListRenderType, ChatStream } from '@/pages/ai-re-act/hooks/aiRender' +import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' import { useRafPolling } from '@/hook/useRafPolling/useRafPolling' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' export interface UseStreamingChatContentParams { - chatType: ChatListRenderType token: string - session: string } export interface UseStreamingChatContentResult { @@ -20,16 +18,19 @@ export interface UseStreamingChatContentResult { } export function useStreamingChatContent(params: UseStreamingChatContentParams): UseStreamingChatContentResult { - const { chatType, token, session } = params + const { token } = params + + const store = useCurrentStore() + const rawData = useCurrentRawData() + + const tokenRenderNum = useStore(store, (state) => state.items[token]?.renderNum) const hasStartedRef = useRef(false) const [shouldType, setShouldType] = useState(false) - const fetchChatDataStore = useChatIPCDispatcher().chatIPCEvents.fetchChatDataStore() - const { execute } = useChatIPCStore().chatIPCData const getData = useCallback((): ChatStream | null => { - return fetchChatDataStore?.getContentMap({ session, chatType, mapKey: token }) as ChatStream - }, [fetchChatDataStore, session, chatType, token]) + return rawData.contents.get(token) as ChatStream + }, [tokenRenderNum]) const { renderNumber, aiDataRef } = useRafPolling({ getData, diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts index e381cb71ae..2bc1938e05 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts @@ -1,13 +1,11 @@ // hooks/useTypedStream.ts import { useMemo } from 'react' -import { ChatListRenderType, ChatStream } from '@/pages/ai-re-act/hooks/aiRender' +import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' import { useStreamingTypewriter } from './useStreamingTypewriter' import { useStreamingChatContent } from './useStreamingChatContent' export interface UseTypedStreamOptions { - chatType: ChatListRenderType token: string - session: string /** 单步最小输出字符数(下限),默认 2 */ step?: number /** 单步最大输出字符数(上限,保证每次渲染长度不会突然过大),默认 18 */ @@ -35,10 +33,10 @@ export interface UseTypedStreamResult { * - 历史记录(直接 end):禁用打字效果,直接显示 */ export function useTypedStream(options: UseTypedStreamOptions): UseTypedStreamResult { - const { chatType, token, session, step = 2, maxStep = 18, interval = 30, catchUpFrames = 9 } = options + const { token, step = 2, maxStep = 18, interval = 30, catchUpFrames = 9 } = options // 获取流数据和是否需要打字效果 - const { renderNumber, stream: rawStream, shouldType } = useStreamingChatContent({ chatType, token, session }) + const { renderNumber, stream: rawStream, shouldType } = useStreamingChatContent({ token }) const content = rawStream?.data?.content || '' // 后端流是否已结束。结束后必须保证最终渲染的是真实完整内容,不能被打字机中间状态污染 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiFileSystemList/FileTreeSystem/FileTreeSystem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiFileSystemList/FileTreeSystem/FileTreeSystem.tsx index da5c6eb2d4..8a8d81a245 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiFileSystemList/FileTreeSystem/FileTreeSystem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiFileSystemList/FileTreeSystem/FileTreeSystem.tsx @@ -7,9 +7,10 @@ import { useCustomFolder } from '../store/useCustomFolder' import styles from './FileTreeSystem.module.scss' import FileTreeDrop from '@/pages/ai-agent/aiChatWelcome/FileTreeDrop/FileTreeDrop' import { Divider } from 'antd' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { useCreation } from 'ahooks' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' interface FileTreeSystemProps { /** 是否有文件预览数据,用于外层自由对话变大 */ @@ -19,10 +20,14 @@ interface FileTreeSystemProps { const FileTreeSystem = (props: FileTreeSystemProps) => { const { onFilePreviewChange } = props const { t } = useI18nNamespaces(['aiAgent']) + + const store = useCurrentStore() + // 单选 const [selected, setSelected] = useState() // ai的文件夹 - const { grpcFolders } = useChatIPCStore().chatIPCData + + const grpcFolders = useStore(store, (state) => state.grpcFolders) // 用户文件夹 const customFolder = useCustomFolder() diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index c1c9669e2a..2f264b3caf 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -1,9 +1,9 @@ -import { ChatListRenderType, ChatReferenceMaterialPayload, ReActChatElement } from '@/pages/ai-re-act/hooks/aiRender' +import { ChatReferenceMaterialPayload, ReActChatElement } from '@/pages/ai-re-act/hooks/aiRender' import { type CSSProperties, useState, type FC, useRef, useEffect, useMemo } from 'react' import styles from './AIGroupStreamCard.module.scss' import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' -import { OutlineArrowsexpandIcon, OutlineChevronsDownUpIcon, OutlineChevronsUpDownIcon } from '@/assets/icon/outline' +import { OutlineArrowsexpandIcon } from '@/assets/icon/outline' import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' import { YakitModal } from '@/components/yakitUI/YakitModal/YakitModal' import { useTypedStream } from '../aiChatListItem/StreamingChatContent/hooks/useTypedStream' @@ -23,14 +23,12 @@ export const Code: FC<{ code: ChatReferenceMaterialPayload; style: CSSProperties } const AIStreamNode: FC<{ - chatType: ChatListRenderType token: string index: number - session: string nodeLabel?: string -}> = ({ chatType, token, index, session, nodeLabel }) => { +}> = ({ token, index, nodeLabel }) => { const { t } = useI18nNamespaces(['aiAgent']) - const { stream } = useTypedStream({ chatType, token, session }) + const { stream } = useTypedStream({ token }) const [open, setOpen] = useState(false) const [openPopover, setOpenPopover] = useState(false) @@ -96,7 +94,7 @@ const AIGroupStreamCard: FC<{ session: string }> = ({ elements, hasNext, session }) => { const lastElement = elements[elements.length - 1] - const { stream } = useTypedStream({ chatType: lastElement?.chatType, token: lastElement?.token ?? '', session }) + const { stream } = useTypedStream({ token: lastElement?.token ?? '' }) const { nodeLabel } = useAINodeLabel(stream?.data.NodeIdVerbose) const [expand, setExpand] = useState(true) const content = stream?.data.content || '' @@ -190,66 +188,9 @@ const AIGroupStreamCard: FC<{ setExpand(!expand) }} > -
- {/* */} - {nodeLabel} -
-
- {shouldShowMask &&
} -

- {content} -

-
- - } - className={classNames(styles['expand-btn'], { - [styles['hidden-expand-btn']]: expand, - })} - /> - - - } - className={classNames(styles['expand-btn'], { - [styles['hidden-expand-btn']]: !expand, - })} - /> - -
-
-
setIsScroll(true)} - className={styles['content-inner']} - style={{ - overflow: isScroll ? 'overlay' : 'hidden', - }} - > - {elements.map((el, index) => ( - - ))} -
+ {elements.map((el, index) => ( + + ))}
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx index 2cf0b9fb53..7c9531d5a3 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx @@ -1,4 +1,4 @@ -import React, { type FC, forwardRef, ReactNode, useEffect, useImperativeHandle, useRef, useState } from 'react' +import React, { forwardRef, ReactNode, useEffect, useImperativeHandle, useRef, useState } from 'react' import { AIReActChatReviewProps, ForgeReviewFormProps, ForgeReviewFormRefProps } from './AIReActChatReviewType' import { OutlineArrowrightIcon, OutlineQuestionmarkcircleIcon, OutlineXIcon } from '@/assets/icon/outline' import { useCountDown, useCreation, useMemoizedFn, useUpdateEffect } from 'ahooks' @@ -18,40 +18,34 @@ import { YakParamProps } from '@/pages/plugins/pluginsType' import { ExecuteEnterNodeByPluginParams } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeard' import { CustomPluginExecuteFormValue } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType' import { getValueByType } from '@/pages/plugins/editDetails/utils' -import { AIAgentGrpcApi, AIInputEventSyncTypeEnum } from '../../../ai-re-act/hooks/grpcApi' +import { AIAgentGrpcApi, AIInputEventSyncTypeEnum, AIInputEvent } from '../../../ai-re-act/hooks/grpcApi' import classNames from 'classnames' import styles from './AIReActChatReview.module.scss' -import { AIChatIPCSendParams } from '@/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent' import { OutlineHandleColorsIcon, ColorsOutlineWarpIcon } from '@/assets/icon/colors' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' -import { - AIChatQSData, - AIChatQSDataTypeEnum, - AIReviewType, - AITaskInfoProps, - UIDetachedPlanReview, -} from '../../../ai-re-act/hooks/aiRender' +import { AIChatQSDataTypeEnum, AIReviewType, AITaskInfoProps } from '../../../ai-re-act/hooks/aiRender' import { AIForge } from '@/pages/ai-agent/type/forge' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import useAIAgentDispatcher from '../../useContext/useDispatcher' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' import { randomString } from '@/utils/randomUtil' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' +/** + * TODO - onSendAI在内部自己通过类型判断发送 + */ export const AIReActChatReview: React.FC = React.memo((props) => { - const { - info, - onSendAI, - onSendSyncMessage, - planReviewTreeKeywordsMap, - isEmbedded, - renderFooterExtra, - expand, - className, - } = props + const { info, planReviewTreeKeywordsMap, isEmbedded, renderFooterExtra, expand, className, chatType } = props const { type, data: review } = info const { t, i18nRefresh } = useI18nNamespaces(['aiAgent', 'yakitUi']) - const { chatIPCData } = useChatIPCStore() - const { chatIPCEvents } = useChatIPCDispatcher() + + const { onSend } = useAIAgentDispatcher() + + const sessionId = useCurrentSessionId() + const store = useCurrentStore() + const execute = useStore(store, (state) => state.execute) + const [reviewTreeOption, setReviewTreeOption] = useState() const [reviewTrees, setReviewTrees] = useState([]) const [currentPlansId, setCurrentPlansId] = useState('') @@ -92,12 +86,12 @@ export const AIReActChatReview: React.FC = React.memo((p targetDate, }) useEffect(() => { - if (!chatIPCData.execute) return + if (!execute) return const data = review as AIAgentGrpcApi.ToolUseReviewRequire if (!!data?.aiReview?.seconds) { setTargetDate(Date.now() + data.aiReview.seconds * 1000) } - }, [review, chatIPCData.execute]) + }, [review, execute]) //#endregion const reviewTitle = useCreation(() => { const subTitle = !!countdown ? ( @@ -265,12 +259,14 @@ export const AIReActChatReview: React.FC = React.memo((p } } } - onSendSyncMessage?.({ - syncType: AIInputEventSyncTypeEnum.SYNC_EXECUTE_DETACHED_PLAN, - syncID: randomString(8), + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_EXECUTE_DETACHED_PLAN, + SyncID: randomString(8), SyncJsonInput: JSON.stringify(syncPayload), - }) - chatIPCEvents.handleTaskReviewRelease(detachedReview.id) + } + // chatIPCEvents.handleTaskReviewRelease(detachedReview.id) + onSend({ token: sessionId, type: '', params: info }) pendingDetachedPlanSubmitRef.current = false }) @@ -361,6 +357,7 @@ export const AIReActChatReview: React.FC = React.memo((p break case 'close': if (type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) { + /** TODO - 新版需要逻辑调整*/ chatIPCEvents.handleTaskReviewRelease((review as AIReviewType).id) return } @@ -496,12 +493,12 @@ export const AIReActChatReview: React.FC = React.memo((p return findIndex !== -1 }, [review, type]) const onSendAIByValue = useMemoizedFn((value: string, optionValue?: string) => { - const params: AIChatIPCSendParams = { - value, - id: (review as AIReviewType).id, - optionValue, + const info: AIInputEvent = { + IsInteractiveMessage: true, + InteractiveId: (review as AIReviewType).id, + InteractiveJSONInput: value, } - onSendAI(params) + onSend({ token: sessionId, type: chatType, params: info, optionValue }) }) const footerNode = useCreation(() => { const renderFooterRightExtra = () => { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts index 529105c16c..dad814c84a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts @@ -1,11 +1,11 @@ -import { AIChatIPCSendParams } from '@/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent' import { AIChatQSData } from '../../../ai-re-act/hooks/aiRender' import { AIAgentGrpcApi } from '../../../ai-re-act/hooks/grpcApi' import { ChatIPCContextDispatcher } from '@/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent' +import { ChatIPCSendType } from '@/pages/ai-re-act/hooks/type' export interface AIReActChatReviewProps { + chatType: ChatIPCSendType info: AIChatQSData - onSendAI: (params: AIChatIPCSendParams) => void planReviewTreeKeywordsMap?: Map isEmbedded?: boolean renderFooterExtra?: (node: React.ReactNode) => React.ReactNode diff --git a/app/renderer/src/main/src/pages/ai-agent/utils/index.ts b/app/renderer/src/main/src/pages/ai-agent/utils/index.ts index 4d3e6b8147..af3512d1d5 100644 --- a/app/renderer/src/main/src/pages/ai-agent/utils/index.ts +++ b/app/renderer/src/main/src/pages/ai-agent/utils/index.ts @@ -180,8 +180,8 @@ const getResourceInfoByMention = (mention: AIMentionCommandParams): AttachedReso } /** @name 将前端的结构转化为符合定义的结构 */ export const getAIReActRequestParams = (value: HandleStartParams) => { - const { extraValue, mentionList = [], imageList = [], httpFlowList = [], codeBlockList = [], showQS } = value - let extra: HandleStartParams['extraValue'] = {} + const { mentionList = [], imageList = [], httpFlowList = [], codeBlockList = [] } = value + let attachedResourceInfo: AIInputEvent['AttachedResourceInfo'] = [] for (let item of mentionList) { const addItem = getResourceInfoByMention(item) @@ -254,12 +254,7 @@ export const getAIReActRequestParams = (value: HandleStartParams) => { attachedResourceInfo = [...attachedResourceInfo, ...addItem] } - if (!!showQS) { - extra.showQS = showQS - } - extra = Object.assign(extraValue || {}, extra) return { - extra, attachedResourceInfo, } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index a4f945cbc6..7434313fb4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -157,7 +157,7 @@ export const AIReActChat: React.FC = React.memo( const session = getSession(sessionId) request.TimelineSessionID = session - const { extra, attachedResourceInfo } = getAIReActRequestParams(value) + const { attachedResourceInfo } = getAIReActRequestParams(value) // 发送初始化参数 const aiInputEvent: AIInputEvent = { IsStart: true, @@ -228,7 +228,7 @@ export const AIReActChat: React.FC = React.memo( const handleSend = useMemoizedFn((data: HandleStartParams) => { if (!activeChat?.SessionID) return try { - const { extra, attachedResourceInfo } = getAIReActRequestParams(data) + const { attachedResourceInfo } = getAIReActRequestParams(data) const chatMessage: AIInputEvent = { IsFreeInput: true, FreeInput: data.qs, diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskEmpty.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskEmpty.tsx new file mode 100644 index 0000000000..3c5469bc8a --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskEmpty.tsx @@ -0,0 +1,145 @@ +import React, { useEffect, useRef, useState } from 'react' +import { useCreation, useInViewport, useMemoizedFn, useUpdateEffect } from 'ahooks' +import { OutlinePlussmIcon } from '@/assets/icon/outline' +import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' +import styles from './AIReActTaskChat.module.scss' +import { AIMaterialsData } from '@/pages/ai-agent/aiChatWelcome/type' +import { YakitRoute } from '@/enums/yakitRoute' +import emiter from '@/utils/eventBus/eventBus' +import { getAIRecommendIconByType } from '../hooks/useGetAIMaterialsData' +import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' +import { AIReActTaskEmptyProps, AIReActTaskRecommendProps } from './AIReActTaskChatType' +import FilePreviewRecentList from '@/pages/ai-agent/components/aiFileSystemList/FilePreview/components/FilePreviewRecentList' +import { getNameByPath } from '@/pages/yakRunner/utils' +import { AITabsEnum } from '@/pages/ai-agent/defaultConstant' +import { useStore } from 'zustand' +import { useCurrentStore } from '../hooks/useCurrentDataBySession' + +const AIReActTaskEmpty: React.FC = React.memo((props) => { + const { loadingAIMaterials, randomAIMaterialsData, onRefresh, onClickItem } = props + + const store = useCurrentStore() + const grpcFolders = useStore(store, (state) => state.grpcFolders) + + const taskEmptyRef = useRef(null) + const [inViewPort = true] = useInViewport(taskEmptyRef) + + const [sessionFiles, setSessionFiles] = useState>([]) + + useEffect(() => { + let isActive = true + + const fetchSessionFiles = async () => { + const files = await Promise.all( + grpcFolders.slice(-5).map(async (item) => ({ + ...item, + name: await getNameByPath(item.path), + })), + ) + + if (isActive) { + setSessionFiles(files) + } + } + + fetchSessionFiles() + + return () => { + isActive = false + } + }, [grpcFolders]) + + useUpdateEffect(() => { + if (inViewPort) { + onRefresh() + } + }, [inViewPort]) + + const handleViewFile = useMemoizedFn((item) => { + emiter.emit('switchAIActTab', JSON.stringify({ key: AITabsEnum.File_System })) + setTimeout(() => { + emiter.emit('fileSystemDefaultExpand', item.path) + }, 800) + }) + + return ( + +
+
+
扩展资源
+
专注于安全编码与漏洞分析的智能助手
+
+
+ {Object.keys(randomAIMaterialsData).map((key) => { + const aiItem: AIMaterialsData = randomAIMaterialsData[key as keyof typeof randomAIMaterialsData] + return aiItem.data.length > 0 ? ( + onClickItem(item, aiItem.mentionType)} + /> + ) : ( + + ) + })} +
+ {sessionFiles.length > 0 && ( +
+ +
+ )} +
+
+ ) +}) + +export default AIReActTaskEmpty + +const AIReActTaskRecommend: React.FC = React.memo((props) => { + const { title, data, onClickItem } = props + const icons = useCreation(() => { + return getAIRecommendIconByType(title) + }, [title]) + const onAdd = useMemoizedFn(() => { + switch (title) { + case '技能': + emiter.emit('menuOpenPage', JSON.stringify({ route: YakitRoute.AddAIForge })) + break + case '知识库': + emiter.emit('menuOpenPage', JSON.stringify({ route: YakitRoute.AI_REPOSITORY })) + break + case '工具': + emiter.emit('menuOpenPage', JSON.stringify({ route: YakitRoute.AddAITool })) + break + + default: + break + } + }) + return ( +
+
+
+
{icons.icon}
+
{icons.hoverIcon}
+ {title} +
+ } className={styles['add-btn']} type="text" onClick={onAdd}> + 新建 + +
+
+ {data.map((item, index) => ( +
onClickItem(item)} + > + {item.name} +
+ ))} +
+
+ ) +}) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiTaskContent/AITaskContent.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiTaskContent/AITaskContent.tsx index 3e5da65145..9f1e32c320 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiTaskContent/AITaskContent.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiTaskContent/AITaskContent.tsx @@ -2,7 +2,7 @@ import React, { ReactNode, useEffect, useRef, useState } from 'react' import { AITaskContentProps } from './type' import { YakitSideTab } from '@/components/yakitSideTab/YakitSideTab' import { AIAgentTriggerEventInfo, AITabsEnumType } from '@/pages/ai-agent/aiAgentType' -import { YakitTabsProps } from '@/components/yakitSideTab/YakitSideTabType' +import { YakitSideTabProps, YakitTabsProps } from '@/components/yakitSideTab/YakitSideTabType' import styles from './AITaskContent.module.scss' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { useInViewport, useMemoizedFn } from 'ahooks' @@ -12,8 +12,10 @@ import { AITaskExecutionDetails } from '@/pages/ai-agent/chatTemplate/aiTaskExec import { AIReActTaskChatContent } from '../aiReActTaskChat/AIReActTaskChat' import { AIReActTaskChatReviewBar } from '../aiReActTaskChat/AIReActTaskChatReviewBar' import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' + import { useEnsureTaskPlanLocate } from './hooks/useEnsureTaskPlanLocate' +import { useStore } from 'zustand' +import { useCurrentStore } from '../hooks/useCurrentDataBySession' interface TabsItemProps extends YakitTabsProps { taskId: string @@ -26,11 +28,11 @@ export const AITaskContent: React.FC = React.memo((props) => const { tabBarExtraContent, onTabsChange } = props const { t, i18nRefresh } = useI18nNamespaces(['aiAgent', 'yakitUi', 'yakitRoute']) - const { - chatIPCData: { taskChat }, - } = useChatIPCStore() - const [tabs, setTabs, getTabs] = useGetSetState([]) - const [activeKey, setActiveKey] = useState(TASK_CONTENT_KEY) + const store = useCurrentStore() + const taskChat = useStore(store, (state) => state.taskChat) + + const [tabs, setTabs, getTabs] = useGetSetState([]) + const [activeKey, setActiveKey] = useState('taskContent') const [scrollToBottom, setScrollToBottom] = useState(false) /** 任务规划关闭后用 display:none 保留,不销毁 */ const [taskPlanMounted, setTaskPlanMounted] = useState(false) From 33a8d085b9a599fbaf57368046c5442943bb8560 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Fri, 3 Jul 2026 16:25:34 +0800 Subject: [PATCH 012/101] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9aiPerfData?= =?UTF-8?q?=E7=9B=B8=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AIContextToken/ContextCostPanel.tsx | 9 ++++----- .../AIContextToken/ContextDetailPopover.tsx | 4 ++-- .../AIContextToken/ContextPressurePanel.tsx | 8 ++++---- .../AIContextToken/ContextTokenSummary.tsx | 7 +++---- .../AIContextToken/useContextPerfStore.ts | 15 +++++---------- 5 files changed, 18 insertions(+), 25 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextCostPanel.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextCostPanel.tsx index 78873e7fa2..c26ad5667b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextCostPanel.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextCostPanel.tsx @@ -1,4 +1,4 @@ -import React, { memo, useCallback } from 'react' +import React, { memo } from 'react' import { useCreation } from 'ahooks' import { cloneDeep } from 'lodash' import { ResponseSpeedEcharts } from '../../chatTemplate/AIEcharts' @@ -10,13 +10,12 @@ import { useRafPolling } from '@/hook/useRafPolling/useRafPolling' import { CONTEXT_PERF_POLL_INTERVAL, ContextPerfPanelProps, useContextPerfStore } from './useContextPerfStore' import styles from '../AIChatContent.module.scss' -const ContextCostPanel: React.FC = ({ session, execute }) => { +const ContextCostPanel: React.FC = ({ execute }) => { const { t } = useI18nNamespaces(['aiAgent']) - const getPerfData = useContextPerfStore(session) - const getData = useCallback(() => getPerfData()?.firstCost ?? null, [getPerfData]) + const aiPerfData = useContextPerfStore() const { renderNumber, aiDataRef: firstCost } = useRafPolling({ - getData, + getData: () => aiPerfData.firstCost ?? null, interval: CONTEXT_PERF_POLL_INTERVAL, shouldStop: () => !execute, resetDeps: [execute], diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextDetailPopover.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextDetailPopover.tsx index aa23147b81..91a59e52b8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextDetailPopover.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextDetailPopover.tsx @@ -21,10 +21,10 @@ interface ContextDetailPopoverProps extends ContextPerfPanelProps { const ContextDetailPopover: React.FC = ({ session, execute, buttonProps }) => { const { t } = useI18nNamespaces(['yakitUi']) const [visible, setVisible] = useState(false) - const getPerfData = useContextPerfStore(session) + const aiPerfData = useContextPerfStore() const { renderNumber, aiDataRef: perfData } = useRafPolling({ - getData: getPerfData, + getData: () => aiPerfData ?? null, interval: CONTEXT_PERF_POLL_INTERVAL, shouldStop: () => !execute, resetDeps: [execute], diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextPressurePanel.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextPressurePanel.tsx index ec16fa258c..6b8ab0e4cd 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextPressurePanel.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextPressurePanel.tsx @@ -1,4 +1,4 @@ -import React, { memo, useCallback } from 'react' +import React, { memo } from 'react' import { useCreation } from 'ahooks' import { cloneDeep } from 'lodash' import { ContextPressureEcharts } from '../../chatTemplate/AIEcharts' @@ -13,11 +13,11 @@ import styles from '../AIChatContent.module.scss' const ContextPressurePanel: React.FC = ({ session, execute }) => { const { t } = useI18nNamespaces(['aiAgent']) - const getPerfData = useContextPerfStore(session) - const getData = useCallback(() => getPerfData()?.pressure ?? null, [getPerfData]) + + const getPerfData = useContextPerfStore() const { renderNumber, aiDataRef: pressure } = useRafPolling({ - getData, + getData: () => getPerfData.pressure ?? null, interval: CONTEXT_PERF_POLL_INTERVAL, shouldStop: () => !execute, resetDeps: [execute], diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextTokenSummary.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextTokenSummary.tsx index 5a5a9e354b..1a1c5b27c9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextTokenSummary.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextTokenSummary.tsx @@ -1,4 +1,4 @@ -import React, { memo, useCallback } from 'react' +import React, { memo } from 'react' import { useCreation } from 'ahooks' import { cloneDeep, isEmpty } from 'lodash' import { formatNumberUnits } from '../../utils' @@ -11,11 +11,10 @@ import { AIAgentGrpcApi } from '@/pages/ai-re-act/hooks/grpcApi' import styles from '../AIChatContent.module.scss' const ContextTokenSummary: React.FC = ({ session, execute }) => { - const getPerfData = useContextPerfStore(session) - const getData = useCallback(() => getPerfData()?.consumption ?? null, [getPerfData]) + const getPerfData = useContextPerfStore() const { renderNumber, aiDataRef: consumption } = useRafPolling({ - getData, + getData: () => getPerfData.consumption ?? null, interval: CONTEXT_PERF_POLL_INTERVAL, shouldStop: () => !execute, resetDeps: [execute], diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts index 3abd9aee5f..2301f5ef47 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts @@ -1,7 +1,5 @@ -import { useCallback } from 'react' -import { aiChatDataStore } from '../../store/ChatDataStore' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' -import { AIChatData } from '../../type/aiChat' +// import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' +import { useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' export const CONTEXT_PERF_POLL_INTERVAL = 2000 @@ -11,10 +9,7 @@ export interface ContextPerfPanelProps { } /** 读取当前 session 的性能数据 */ -export function useContextPerfStore(session?: string) { - const { chatIPCEvents } = useChatIPCDispatcher() - return useCallback((): AIChatData['aiPerfData'] | null => { - const store = chatIPCEvents.fetchChatDataStore?.() ?? aiChatDataStore - return store?.get(session ?? '')?.aiPerfData ?? null - }, [session, chatIPCEvents]) +export function useContextPerfStore() { + const rawData = useCurrentRawData() + return rawData.aiPerfData } From 9289d58d4f6de5bd569cb196c58973308c37f27d Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Fri, 3 Jul 2026 17:39:29 +0800 Subject: [PATCH 013/101] =?UTF-8?q?feat:=E4=BF=AE=E6=94=B9=E5=90=88master?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AIContextToken/useContextPerfStore.ts | 1 - .../aiRunModeSelect/AIRunModeSelect.tsx | 1 + .../AITaskExecutionDetails.tsx | 12 +- .../AITaskDefaultGroupCard.tsx | 21 ++-- .../ConcurrentStreamCard.tsx | 24 ++-- .../ai-agent/components/ToolInvokerCard.tsx | 35 +++--- .../aiChatListItem/AIChatListItem.tsx | 2 +- .../StaticChatContent/StaticChatContent.tsx | 32 ++--- .../AIHttpFlowFuzzStatusCard.tsx | 3 - .../aiReviewResult/AIReviewResult.tsx | 13 +- .../components/aiTaskQuery/AITaskQuery.tsx | 111 ++++++++++++------ .../components/aiTriageChat/AITriageChat.tsx | 16 ++- .../src/pages/ai-agent/template/template.tsx | 8 +- .../AIReActTaskChatReviewBar.tsx | 2 +- .../aiReviewRuleSelect/AIReviewRuleSelect.tsx | 19 ++- 15 files changed, 172 insertions(+), 128 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts index 2301f5ef47..19574a6fe8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts @@ -1,4 +1,3 @@ -// import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' import { useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' export const CONTEXT_PERF_POLL_INTERVAL = 2000 diff --git a/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx b/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx index 2bc23671fd..5d10f98862 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx @@ -46,6 +46,7 @@ const ModeOptionList: { }, ] +/** TODO - 待修改为新版 */ const AIRunModeSelect: React.FC = memo(() => { const { setting, activeChat } = useAIAgentStore() const { setSetting } = useAIAgentDispatcher() diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx index f8c5df98f1..54cf641ec6 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx @@ -398,15 +398,19 @@ export const AITaskExecutionDetails: React.FC = Rea const AIBrowserProcesses: React.FC = React.memo((props) => { const { list } = props - const { handleSendSyncMessage } = useChatIPCDispatcher() // TODO - + const sessionId = useCurrentSessionId() + const { onSend } = useAIAgentDispatcher() const onRemove = useMemoizedFn((processes: AIBrowserProcessesProps['list'][number]) => { - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_CLOSE_BROWSER, + const info: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_CLOSE_BROWSER, + SyncID: randomString(8), SyncJsonInput: JSON.stringify({ process_id: processes.process_id, }), - }) + } + onSend({ token: sessionId, type: '', params: info }) }) return (
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx index dcec6dbd13..b6dcae0e75 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx @@ -6,14 +6,15 @@ import { import { type FC, useEffect, useMemo, useState } from 'react' import classNames from 'classnames' import { OutlineInformationcircleIcon } from '@/assets/icon/outline' -import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import styles from './AITaskDefaultGroupCard.module.scss' import ConcurrentStreamCardActions from '../ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions' -import { useBoolean } from 'ahooks' +import { useBoolean, useCreation } from 'ahooks' import { formatTimestamp } from '@/utils/timeUtil' import { useConcurrentStreamRefreshListener } from '../ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener' import AITaskDefaultGroupContent from './AITaskDefaultGroupContent' +import { useCurrentStore, useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' const AITaskDefaultGroupCard: FC<{ elements: ReActChatTaskElementSub[] @@ -25,15 +26,19 @@ const AITaskDefaultGroupCard: FC<{ onRefresh?: () => void }> = ({ elements, session, token, chatType, hasNext, isChildWindow, onRefresh }) => { const { t } = useI18nNamespaces(['aiAgent']) - const { fetchChatDataStore } = useChatIPCDispatcher().chatIPCEvents const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean(true) const [contentFocused, setContentFocused] = useState(false) - const raw = fetchChatDataStore()?.getContentMap({ - session, - chatType, - mapKey: token, - }) as ChatTaskDefaultGroup | undefined + const store = useCurrentStore() + const renderNum = useStore(store, (state) => state.items[token].renderNum) + const rawData = useCurrentRawData() + + const raw = useCreation(() => { + if (!rawData) return null + const itemData = rawData.contents.get(token) + if (!itemData) return null + return { ...itemData } as ChatTaskDefaultGroup + }, [renderNum]) const framePayload = useMemo( () => ({ diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index b03826dc9a..a094dc54e0 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -1,10 +1,9 @@ import { useEffect, useMemo, type FC } from 'react' import ChatCard from '../ChatCard' import ModalInfo from '../ModelInfo' -import { useBoolean, useMemoizedFn } from 'ahooks' +import { useBoolean, useCreation, useMemoizedFn } from 'ahooks' import styles from './ConcurrentStreamCard.module.scss' import ConcurrentStreamContent from './ConcurrentStreamContent/ConcurrentStreamContent' -import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' import { AIChatQSDataTypeEnum, ChatTaskDefaultGroup, @@ -22,6 +21,8 @@ import { useConcurrentStreamRefreshListener } from './concurrentStream/useConcur import { AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' import emiter from '@/utils/eventBus/eventBus' import { yakitNotify } from '@/utils/notification' +import { useCurrentStore, useCurrentRawData, useCurrentMeta } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' const ConcurrentStreamCard: FC<{ elements: ReActChatTaskElementSub[] @@ -32,15 +33,20 @@ const ConcurrentStreamCard: FC<{ onRefresh?: () => void }> = ({ session, elements, chatType, token, isChildWindow, onRefresh }) => { const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean(isChildWindow || chatType !== 'reAct') - const { fetchChatDataStore, fetchCurrentTaskPlanID } = useChatIPCDispatcher().chatIPCEvents const { t } = useI18nNamespaces(['aiAgent']) - const raw = fetchChatDataStore()?.getContentMap({ - session, - chatType, - mapKey: token, - }) as ChatTaskNodeGroup | ChatTaskDefaultGroup | undefined + const store = useCurrentStore() + const renderNum = useStore(store, (state) => state.items[token].renderNum) + const rawData = useCurrentRawData() + const metaData = useCurrentMeta() + + const raw = useCreation(() => { + if (!rawData) return null + const itemData = rawData.contents.get(token) + if (!itemData) return null + return { ...itemData } as ChatTaskNodeGroup | ChatTaskDefaultGroup | undefined + }, [renderNum]) // 是否是默认任务分组 const isTaskDefaultGroup = raw?.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP @@ -80,7 +86,7 @@ const ConcurrentStreamCard: FC<{ return { time: isTaskDefaultGroup ? 0 : raw.Timestamp, title: raw.AIModelName, icon: raw.AIService } }, [isTaskDefaultGroup, raw]) - const coordinatorId = fetchCurrentTaskPlanID()?.coordinatorId + const coordinatorId = metaData.currentTaskPlanID?.coordinatorId const taskId = raw?.data?.taskId const showContinueTask = !!raw && !!coordinatorId && !!taskId && !isChildWindow const showCancelTask = raw?.data?.status === 'processing' && !!taskId && !isChildWindow diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx index f70d9f4e37..99159e1b28 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx @@ -1,4 +1,4 @@ -import { FC, memo, ReactNode, useEffect, useMemo, useRef, useState } from 'react' +import { FC, memo, ReactNode, useEffect, useRef, useState } from 'react' import ChatCard from './ChatCard' import styles from './ToolInvokerCard.module.scss' import classNames from 'classnames' @@ -16,7 +16,7 @@ import type { ModalInfoProps } from './ModelInfo' import emiter from '@/utils/eventBus/eventBus' import { AITabsEnum } from '../defaultConstant' import { useClickAway, useCreation, useMemoizedFn } from 'ahooks' -import { AIAgentGrpcApi, AIEventQueryRequest } from '@/pages/ai-re-act/hooks/grpcApi' +import { AIAgentGrpcApi, AIEventQueryRequest, AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' import { isToolStdoutStream } from '@/pages/ai-re-act/hooks/utils' import { OutlineArrownarrowrightIcon, @@ -32,10 +32,8 @@ import { Divider, Tooltip } from 'antd' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import { formatTimestamp } from '@/utils/timeUtil' import { OperationCardFooter, OperationCardFooterProps } from './OperationCardFooter/OperationCardFooter' -import useChatIPCDispatcher from '../useContext/ChatIPCContent/useDispatcher' import useAIAgentStore from '../useContext/useStore' import { YakitPopconfirm } from '@/components/yakitUI/YakitPopconfirm/YakitPopconfirm' -import { AIChatIPCSendParams } from '../useContext/ChatIPCContent/ChatIPCContent' import { AIReferenceNode } from '@/pages/ai-re-act/aiReActChatContents/AIReActChatContents' import { useStreamingChatContent } from './aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' @@ -44,6 +42,9 @@ import { YakitModal } from '@/components/yakitUI/YakitModal/YakitModal' import { setClipboardText } from '@/utils/clipboard' import { success } from '@/utils/notification' import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' +import useAIAgentDispatcher from '../useContext/useDispatcher' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' /** @name AI工具按钮对应图标 */ const AIToolToIconMap: Record = { @@ -157,7 +158,8 @@ const ToolStdoutCard: React.FC = memo((props) => { const { operationInfo, fileList, chatType, data, titleText } = props const { t } = useI18nNamespaces(['aiAgent']) - const { handleSend } = useChatIPCDispatcher() + const sessionId = useCurrentSessionId() + const { onSend } = useAIAgentDispatcher() // 获取流数据 const { stream } = useStreamingChatContent({ @@ -186,11 +188,12 @@ const ToolStdoutCard: React.FC = memo((props) => { const jsonInput = { suggestion: item.value, } - const params: AIChatIPCSendParams = { - value: JSON.stringify(jsonInput), - id: selectors.InteractiveId, + const info: AIInputEvent = { + IsInteractiveMessage: true, + InteractiveId: selectors.InteractiveId, + InteractiveJSONInput: JSON.stringify(jsonInput), } - handleSend(params) + onSend({ token: sessionId, type: '', params: info }) }) const referenceNode = useCreation(() => { return !!stream?.reference ? : <> @@ -245,7 +248,6 @@ const ToolResultCard: React.FC = memo((props) => { const { modalInfo, operationInfo, fileList, data, chatType, token, isChildWindow, titleText } = props const { t } = useI18nNamespaces(['aiAgent']) const { activeChat } = useAIAgentStore() - const { fetchChatDataStore } = useChatIPCDispatcher().chatIPCEvents const [loading, setLoading] = useState(false) @@ -288,10 +290,9 @@ const ToolResultCard: React.FC = memo((props) => { const duration = useCreation(() => { return Math.round(data.durationSeconds * 10) / 10 }, [data.durationSeconds]) - const startTime = useCreation(() => { - return formatTimestamp(data.startTime) - }, [data.startTime]) + const store = useCurrentStore() + const rawData = useCurrentRawData() const getListToolList = useMemoizedFn(() => { if (!data?.callToolId || !activeChat) return setLoading(true) @@ -300,14 +301,12 @@ const ToolResultCard: React.FC = memo((props) => { } grpcQueryAIToolDetails(params) .then((res) => { - const chatItem = fetchChatDataStore()?.getContentMap({ - session: activeChat?.SessionID, - chatType, - mapKey: token, - }) + /** TODO - 工具卡片刷新后,需要更新这个item里面的数据以及其刷新逻辑 */ + const chatItem = rawData.contents.get(token) if (!!chatItem && chatItem.type === AIChatQSDataTypeEnum.TOOL_RESULT) { chatItem.data.tool.resultDetails = getResultDetails(res) } + store.getState().incrementNodeVersion(token, 'item') }) .finally(() => setTimeout(() => { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx index ae5e2276ce..e56139d073 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx @@ -183,7 +183,7 @@ export const AIChatListItem: React.FC = React.memo((props) streamClassName={aiStreamNodeProps} /> ) - return ChatItemRenderer(contentItem)} /> + return ChatItemRenderer(contentItem)} /> }) return {renderContent()} }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx index eb5816d75a..d13a0883bf 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx @@ -1,24 +1,26 @@ -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' -import type { AIChatQSData, ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' -import { FC, memo, ReactNode, useRef, useMemo } from 'react' +import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' +import { useCurrentStore, useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { FC, memo, ReactNode, useMemo } from 'react' +import { useStore } from 'zustand' -type StaticChatContentProps = ReActChatRenderItem & { +type StaticChatContentProps = { + token: string render?: (contentItem: AIChatQSData) => ReactNode - session: string } -const StaticChatContent: FC = ({ chatType, token, render, session, renderNum }) => { - const { fetchChatDataStore } = useChatIPCDispatcher().chatIPCEvents - +const StaticChatContent: FC = ({ token, render }) => { + const store = useCurrentStore() + const renderNum = useStore(store, (state) => state.items[token].renderNum) + const rawData = useCurrentRawData() const chatItem = useMemo(() => { - const raw = fetchChatDataStore()?.getContentMap({ session, chatType, mapKey: token }) - if (!raw) return null - if (raw.data != null && typeof raw.data === 'object') { - return { ...raw, data: Object.assign({}, raw.data) } as AIChatQSData + if (!rawData) return null + const itemData = rawData.contents.get(token) + if (!itemData) return null + if (itemData.data != null && typeof itemData.data === 'object') { + return { ...itemData, data: Object.assign({}, itemData.data) } as AIChatQSData } - return { ...raw } as AIChatQSData - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [renderNum, session, chatType, token]) + return { ...itemData } as AIChatQSData + }, [renderNum]) if (!chatItem) return null return <>{render?.(chatItem)} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx index 3394997952..de9c76007f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx @@ -9,7 +9,6 @@ import { Tooltip } from 'antd' import YakitSolidLoading from '@/components/yakitUI/YakitSolidLoading/YakitSolidLoading' import ChatCard from '../ChatCard' import ModalInfo, { type ModalInfoProps } from '../ModelInfo' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' import { WebFuzzerAiStore } from '@/pages/ai-agent/store/ChatDataStore' import { hasWebFuzzerPageOnAIFuzzStatus, @@ -44,8 +43,6 @@ export const AIHttpFlowFuzzStatusCard: React.FC = const fail = p?.failed_requests ?? 0 const avgMs = p?.average_response_ms - const { chatIPCEvents } = useChatIPCDispatcher() - // 「查看详情」点击: // - 若卡片所在的会话绑定了某个 Web Fuzzer 页签(`WebFuzzerAiStore`), // 则把本卡片的 `runtime_id` 显式推送到该页签,并打开 traffic analysis 抽屉。 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx index 83ad691b72..a1d982b1b2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx @@ -9,19 +9,19 @@ import { PreWrapper } from '../ToolInvokerCard' import { Tooltip } from 'antd' import { OutlineChevronsDownUpIcon, OutlineChevronsUpDownIcon } from '@/assets/icon/outline' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' -import useChatIPCStore from '../../useContext/ChatIPCContent/useStore' import { CopyComponents } from '@/components/yakitUI/YakitTag/YakitTag' import { setClipboardText } from '@/utils/clipboard' import { success } from '@/utils/notification' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { isEmpty } from 'lodash' import classNames from 'classnames' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' export const AIReviewResult: React.FC = memo((props) => { const { info, timestamp } = props const { t, i18nRefresh } = useI18nNamespaces(['aiAgent', 'yakitUi']) const { type, data } = info - const { chatIPCData } = useChatIPCStore() const getChatType = useMemoizedFn(() => { return info.chatType @@ -29,12 +29,9 @@ export const AIReviewResult: React.FC = memo((props) => { const [expand, setExpand] = useState(false) - const taskLength = useCreation(() => { - return chatIPCData?.taskChat?.elements?.length - }, [chatIPCData?.taskChat?.elements?.length]) - const casualLength = useCreation(() => { - return chatIPCData?.casualChat?.elements?.length - }, [chatIPCData?.casualChat?.elements?.length]) + const store = useCurrentStore() + const taskLength = useStore(store, (state) => state.taskChat.elements.length) + const casualLength = useStore(store, (state) => state.casualChat.elements.length) const isInit = useRef(true) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx index ff6e50d288..f5dc760c58 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react' +import React, { useState } from 'react' import { AITaskQueryItemProps, AITaskQueryProps } from './type' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { @@ -11,37 +11,52 @@ import { } from '@/assets/icon/outline' import styles from './AITaskQuery.module.scss' import { YakitTag } from '@/components/yakitUI/YakitTag/YakitTag' -import useChatIPCStore from '../../useContext/ChatIPCContent/useStore' -import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' -import { useCreation, useDebounceFn, useMemoizedFn } from 'ahooks' -import { AIInputEventSyncTypeEnum } from '@/pages/ai-re-act/hooks/grpcApi' +import { useDebounceFn, useMemoizedFn } from 'ahooks' +import { AIInputEvent, AIInputEventSyncTypeEnum } from '@/pages/ai-re-act/hooks/grpcApi' import { Tooltip } from 'antd' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import useAIAgentDispatcher from '../../useContext/useDispatcher' +import { randomString } from '@/utils/randomUtil' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' export const AITaskQuery: React.FC = React.memo((props) => { const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) - const { chatIPCData } = useChatIPCStore() - const { handleSendSyncMessage } = useChatIPCDispatcher() const [loading, setLoading] = useState(false) - const questionQueue = useMemo(() => { - return chatIPCData.questionQueue - }, [chatIPCData.questionQueue]) + const sessionId = useCurrentSessionId() + const store = useCurrentStore() + const questionQueue = useStore(store, (state) => state.questionQueue) + const execute = useStore(store, (state) => state.execute) + const { onSend } = useAIAgentDispatcher() const [showList, setShowList] = useState(true) const onClearTaskQueue = useMemoizedFn(() => { - if (!chatIPCData.execute) return + if (!execute) return + if (!sessionId) return setLoading(true) - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CLEAR_TASK, - params: {}, - }) - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, - params: {}, - }) + + const clearTaskInfo: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CLEAR_TASK, + + Params: {}, + SyncID: randomString(8), + } + onSend({ token: sessionId, type: '', params: clearTaskInfo }) + + const queueInfo: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, + + Params: {}, + SyncID: randomString(8), + } + onSend({ token: sessionId, type: '', params: queueInfo }) + setTimeout(() => { setLoading(false) setShowList(false) @@ -91,24 +106,35 @@ export const AITaskQuery: React.FC = React.memo((props) => { const AITaskQueryItem: React.FC = React.memo((props) => { const { item } = props const { t } = useI18nNamespaces(['aiAgent']) - const { chatIPCData } = useChatIPCStore() const [upLoading, setUpLoading] = useState(false) const [removeLoading, setRemoveLoading] = useState(false) - const execute = useCreation(() => chatIPCData.execute, [chatIPCData.execute]) - const { handleSendSyncMessage } = useChatIPCDispatcher() + + const sessionId = useCurrentSessionId() + const store = useCurrentStore() + const execute = useStore(store, (state) => state.execute) + const { onSend } = useAIAgentDispatcher() + const onTaskUp = useDebounceFn( () => { if (!execute || upLoading) return setUpLoading(true) - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_JUMP_QUEUE, + const jumpInfo: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_JUMP_QUEUE, SyncJsonInput: JSON.stringify({ task_id: item.id }), - params: {}, - }) - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, - params: {}, - }) + Params: {}, + SyncID: randomString(8), + } + onSend({ token: sessionId, type: '', params: jumpInfo }) + + const queueInfo: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, + Params: {}, + SyncID: randomString(8), + } + onSend({ token: sessionId, type: '', params: queueInfo }) + setTimeout(() => { setUpLoading(false) }, 500) @@ -119,15 +145,24 @@ const AITaskQueryItem: React.FC = React.memo((props) => { () => { if (!execute || removeLoading) return setRemoveLoading(true) - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_REMOVE_TASK, + + const jumpInfo: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_REMOVE_TASK, SyncJsonInput: JSON.stringify({ task_id: item.id }), - params: {}, - }) - handleSendSyncMessage({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, - params: {}, - }) + Params: {}, + SyncID: randomString(8), + } + onSend({ token: sessionId, type: '', params: jumpInfo }) + + const queueInfo: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, + Params: {}, + SyncID: randomString(8), + } + onSend({ token: sessionId, type: '', params: queueInfo }) + setTimeout(() => { setRemoveLoading(false) }, 500) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.tsx index 4132e173a8..60278064f1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.tsx @@ -20,8 +20,8 @@ import { AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' import { AIChatTextareaSubmit } from '../../template/type' import { getAIReActRequestParams } from '../../utils' import { extractDataWithMilkdown } from '../aiMilkdownInput/utils' -import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' +import useAIAgentDispatcher from '../../useContext/useDispatcher' export const AITriageChatContent: React.FC = memo((props) => { const { isAnswer, content, contentClassName, chatClassName, extraValue } = props @@ -79,9 +79,8 @@ export const AITriageChatContent: React.FC = memo((pro const AITriageChatContentEdit: React.FC = React.memo((props) => { const { extraValue, content, onCancel } = props const { activeChat } = useAIAgentStore() - const { chatIPCEvents } = useChatIPCDispatcher() const { chatDataStoreKey } = useGetChatDataStoreKey() - + const { onSend } = useAIAgentDispatcher() const defaultValue = useCreation(() => { if (!!extraValue?.showQS) { return `${extraValue?.showQS}` @@ -117,7 +116,7 @@ const AITriageChatContentEdit: React.FC = React.me e.stopPropagation() onCancel() }) - const onSend = useMemoizedFn(() => { + const onSendEdit = useMemoizedFn(() => { // 发送消息逻辑 if (!editorMilkdown.current || !activeChat) return const qs = getMarkdownValue() @@ -131,7 +130,7 @@ const AITriageChatContentEdit: React.FC = React.me showQS: qs, focusMode: '', } - const { extra, attachedResourceInfo } = getAIReActRequestParams(value) + const { attachedResourceInfo } = getAIReActRequestParams(value) const chatMessage: AIInputEvent = { IsFreeInput: true, @@ -139,14 +138,13 @@ const AITriageChatContentEdit: React.FC = React.me AttachedResourceInfo: attachedResourceInfo, FocusModeLoop: value.focusMode, } - chatIPCEvents.onSend({ + onSend({ token: activeChat.SessionID, type: 'casual', params: { IsFreeInput: true, ...chatMessage, }, - extraValue: extra, }) onCancel() }) @@ -159,7 +157,7 @@ const AITriageChatContentEdit: React.FC = React.me if (!e.nativeEvent?.isComposing && keys?.join() === YakitKeyBoard.Enter) { e.stopPropagation() e.preventDefault() - onSend() + onSendEdit() } }) @@ -187,7 +185,7 @@ const AITriageChatContentEdit: React.FC = React.me disabled={disabled} onClick={(e) => { e.stopPropagation() - onSend() + onSendEdit() }} > 发送 diff --git a/app/renderer/src/main/src/pages/ai-agent/template/template.tsx b/app/renderer/src/main/src/pages/ai-agent/template/template.tsx index 4339acc243..f9150af966 100644 --- a/app/renderer/src/main/src/pages/ai-agent/template/template.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/template/template.tsx @@ -55,7 +55,6 @@ import OpenFileDropdown, { OpenFileDropdownItem } from '../aiChatWelcome/OpenFil import { UploadFileButton } from '@/pages/ai-re-act/aiReActChat/AIReActComponent' import { insertAtCurrentPosition } from '../components/aiMilkdownInput/customPlugin' import { YakitTag } from '@/components/yakitUI/YakitTag/YakitTag' -import useChatIPCStore from '../useContext/ChatIPCContent/useStore' import useAIGlobalConfig from '@/pages/ai-re-act/hooks/useAIGlobalConfig' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import { @@ -68,6 +67,8 @@ import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { AIMilkdownInputRef } from '../components/aiMilkdownInput/type' import { AICodeBlockCommandParams } from '../components/aiMilkdownInput/aiCodeBlock/aiCustomCodeBlockPlugin' import AIRunModeSelect from '../aiRunModeSelect/AIRunModeSelect' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' /** @name AI-Agent专用Textarea组件,行高为20px */ export const QSInputTextarea: React.FC> = memo( @@ -107,8 +108,9 @@ export const AIChatTextarea: React.FC = memo( onHttpFlowRemove, } = props const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) - const { chatIPCData } = useChatIPCStore() - const execute = useCreation(() => chatIPCData.execute, [chatIPCData.execute]) + + const store = useCurrentStore() + const execute = useStore(store, (state) => state.execute) const [manualAdditionVisible, setManualAdditionVisible] = useState(false) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatReviewBar.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatReviewBar.tsx index 1d612b2034..48319fba82 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatReviewBar.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatReviewBar.tsx @@ -1,4 +1,4 @@ -import React, { type FC, type ReactNode } from 'react' +import { type FC, type ReactNode } from 'react' import { AIReActTaskChatReview } from '@/pages/ai-agent/aiAgentChat/AIAgentChat' import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' import { AIRenderTaskFooterExtra } from './AIReActTaskChat' diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx index b21fb6154b..674f523f43 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReviewRuleSelect/AIReviewRuleSelect.tsx @@ -4,7 +4,7 @@ import styles from './AIReviewRuleSelect.module.scss' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' import classNames from 'classnames' -import { useClickAway, useControllableValue, useCreation, useInViewport, useMemoizedFn, useUpdateEffect } from 'ahooks' +import { useClickAway, useControllableValue, useCreation, useMemoizedFn, useUpdateEffect } from 'ahooks' import { YakitSelect } from '@/components/yakitUI/YakitSelect/YakitSelect' import { AIAgentSettingDefault, @@ -16,25 +16,24 @@ import { OutlineSirenIcon } from '@/assets/icon/outline' import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' import { FormItemSlider } from '@/pages/ai-agent/AIChatSetting/AIChatSetting' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' import { AIInputEvent, AIInputEventHotPatchTypeEnum, AIStartParams } from '../hooks/grpcApi' import isEqual from 'lodash/isEqual' -import emiter from '@/utils/eventBus/eventBus' -import { JSONParseLog } from '@/utils/tool' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { isNil } from 'lodash' import useCurrentSessionId from '../hooks/useCurrentSessionId' +import { useCurrentStore } from '../hooks/useCurrentDataBySession' +import { useStore } from 'zustand' const AIReviewRuleSelect: React.FC = React.memo((props) => { const { t } = useI18nNamespaces(['aiAgent']) /** 当前对话唯一ID */ const sessionId = useCurrentSessionId() + const store = useCurrentStore() + const execute = useStore(store, (state) => state.execute) + const { setting } = useAIAgentStore() const { setSetting, onSend } = useAIAgentDispatcher() - const { chatIPCData } = useChatIPCStore() - const [visible, setVisible] = useState(false) const [open, setOpen] = useState(false) const [selectAIReviewRiskControlScore, setAIReviewRiskControlScore] = @@ -42,7 +41,7 @@ const AIReviewRuleSelect: React.FC = React.memo((props) = const modelValue = useCreation(() => { return setting?.ReviewPolicy || AIAgentSettingDefault.ReviewPolicy - }, [setting?.ReviewPolicy, chatIPCData.execute]) + }, [setting?.ReviewPolicy, execute]) const [selectReviewPolicy, setSelectReviewPolicy] = useState() const displayReviewPolicy = useCreation(() => { @@ -114,7 +113,7 @@ const AIReviewRuleSelect: React.FC = React.memo((props) = setVisible(v) if ( !v && - chatIPCData.execute && + execute && !isNil(selectAIReviewRiskControlScore) && !isEqual(selectAIReviewRiskControlScore, aiReviewRiskControlScore) ) { @@ -134,7 +133,7 @@ const AIReviewRuleSelect: React.FC = React.memo((props) = }) const onSetOpen = useMemoizedFn((v: boolean) => { setOpen(v) - if (!v && chatIPCData.execute && !isEqual(selectReviewPolicy, modelValue)) { + if (!v && execute && !isEqual(selectReviewPolicy, modelValue)) { handHotpatchReviewPolicy(selectReviewPolicy) } if (v) { From 619cf3756ebbbac48d316e42ddb5a717ce8371c5 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Mon, 29 Jun 2026 11:09:10 +0800 Subject: [PATCH 014/101] =?UTF-8?q?feat:=201.=20=E5=B0=86=E6=9C=AA?= =?UTF-8?q?=E5=BC=95=E5=85=A5=E4=BD=BF=E7=94=A8=E7=9A=84=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E5=BC=95=E5=85=A5=202.=20=E5=B0=86=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=9C=BA=E9=87=8C=E7=9A=84update=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E5=AF=B9=E5=BA=94=E7=9A=84=E6=95=B0=E6=8D=AE=E5=AD=97=E6=AE=B5?= =?UTF-8?q?=E8=BF=9B=E8=A1=8C=E6=B3=A8=E9=87=8A=203.=20=E6=96=B0=E5=A2=9Es?= =?UTF-8?q?yncId=E6=95=B0=E6=8D=AE=E5=92=8C=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=8C=204.=20=E7=BB=99handleGrpcOutputEvent?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=8C=85=E8=A3=B9try-catch=205.=20=E5=B0=86?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E6=95=B0=E6=8D=AE=E9=83=BD=E6=8A=84=E9=80=81?= =?UTF-8?q?=E8=87=B3=E6=97=A5=E5=BF=97=E7=AA=97=E5=8F=A3=E4=B8=AD=206.=20?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=97=A5=E5=BF=97=E6=95=B0=E6=8D=AE=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91=E5=92=8C=E5=8F=91=E9=80=81=E9=80=BB?= =?UTF-8?q?=E8=BE=91=207.=20=E6=96=B0=E5=A2=9EUI=E4=B8=BB=E5=8A=A8?= =?UTF-8?q?=E5=BE=80=E5=88=97=E8=A1=A8=E9=87=8C=E6=94=BE=E5=85=A5=E4=B8=80?= =?UTF-8?q?=E6=9D=A1=E6=95=B0=E6=8D=AE=E6=96=B9=E6=B3=95=208.=20=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E8=80=81=E7=89=88=E7=9A=84grpcAIMessageHandlers?= =?UTF-8?q?=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/type/aiChat.d.ts | 4 + .../hooks/ChatMultiSessionController.ts | 192 ++-- .../src/pages/ai-re-act/hooks/aiRender.ts | 36 +- .../src/pages/ai-re-act/hooks/chatStore.ts | 1 + .../ai-re-act/hooks/grpcAIMessageHandlers.ts | 1001 ----------------- .../hooks/grpcStreamHandler/aiOther.ts | 71 +- .../hooks/grpcStreamHandler/aiReview.ts | 146 ++- .../hooks/grpcStreamHandler/aiSingleItem.ts | 59 +- .../hooks/grpcStreamHandler/aiStream.ts | 211 ++-- .../hooks/grpcStreamHandler/aiToolResult.ts | 174 +-- .../main/src/pages/ai-re-act/hooks/type.ts | 3 +- .../pages/ai-re-act/hooks/useCasualChat.ts | 15 +- .../src/pages/ai-re-act/hooks/useChatIPC.ts | 10 +- .../src/pages/ai-re-act/hooks/useTaskChat.ts | 29 +- .../main/src/pages/ai-re-act/hooks/utils.ts | 48 +- app/renderer/src/main/tsconfig.json | 2 +- 16 files changed, 566 insertions(+), 1436 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/grpcAIMessageHandlers.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index ae815bd40a..606c456f9b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -3,6 +3,7 @@ import type { AIAgentGrpcApi, AIInputEvent, AIStartParams } from '@/pages/ai-re- import type { PlanItemDetailsData } from '@/pages/ai-re-act/hooks/aiRender' import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' import type { AISource } from '@/pages/ai-re-act/hooks/grpcApi' +import { PaginationSchema } from '@/pages/invoker/schema' /** 上下文字节统计 */ export interface AIContextStatsDetail { @@ -255,4 +256,7 @@ export interface AIAgentChatMetaData { /** 用于文件操作记录的计数器 */ execFileRecordOrder: number + + /** 同步ID-是否已处理 */ + syncIDMap: Map } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 284e3e4cec..74db10eab2 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -6,10 +6,10 @@ import { AIAgentSettingDefault, AIModelTypeEnum } from '@/pages/ai-agent/default import cloneDeep from 'lodash/cloneDeep' import { DefaultMemoryList, DefaultPlanItemDetailsData } from './defaultConstant' import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHandlers' -import { genExecTasks, handleGrpcDataPushLog } from './utils' +import { genExecTasks } from './utils' import type { AIChatIPCStartParams, AIChatSendParams } from './type' import { yakitNotify } from '@/utils/notification' -import { AIChatQSDataTypeEnum, type AIReviewType } from './aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum } from './aiRender' import { aiAgentLogEmitter } from './AIAgentLogEmitter' const { ipcRenderer } = window.require('electron') @@ -115,10 +115,14 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { cardKVPair: new Map(), cardKVPaidTimer: null, execFileRecordOrder: 1, + syncIDMap: new Map(), } } export class ChatMultiSessionController { + // 存放各个页面下的session集合 + private pageSessionMap = new Map>() + private requestPool = new Map() private storePool = new Map>() private rawDataPool = new Map() @@ -178,6 +182,7 @@ export class ChatMultiSessionController { const { request, store, meta } = this.ensureSession(sessionId) store.getState().updateState({ execute: true, casualTitle: '发送问题,开启会话...' }) + this.pageSessionMap.get(params.Params?.Source ?? 'ai')?.add(sessionId) if (params.Params?.UserQuery) { meta.createChatQuestion = { @@ -203,12 +208,18 @@ export class ChatMultiSessionController { return } - const { store, rawData } = this.ensureSession(token) + const { store, rawData, meta } = this.ensureSession(token) // 自由对话没有问题进行中时,才改变loading的title if (params.IsFreeInput && !store.getState().casualLoading) { store.getState().updateState({ casualTitle: '等待回复中...' }) } + if (params.IsSyncMessage && params.SyncID) { + // 记录发送请求里的syncId-标识开始处理中 + meta.syncIDMap.set(params.SyncID, true) + store.getState().updateStateCount('syncIDUpdate') + } + switch (type) { case 'casual': if (params.IsInteractiveMessage && params.InteractiveId) { @@ -308,87 +319,123 @@ export class ChatMultiSessionController { ipcRenderer.invoke('send-ai-re-act', sessionId, request) } - // 💥 核心替换:完美接管原 useChatIPC 里的巨型数据分发逻辑! + /** 💥 核心替换:接管原 useChatIPC 里的巨型数据分发逻辑! */ public handleGrpcOutputEvent(sessionId: string, res: AIOutputEvent) { - // 所有数据,均抄送一份到日志中 - handleGrpcDataPushLog({ info: res, pushLog: () => {} }) + try { + let ipcContent = Uint8ArrayToString(res.Content) || '' + // 所有数据,均抄送一份到日志中 + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: res.Timestamp, + log: { level: 'log', message: ipcContent }, + }) - const { store, rawData, request, meta } = this.ensureSession(sessionId) + const { store, rawData, request, meta } = this.ensureSession(sessionId) - if (res.Type === 'pong') { - const { createChatQuestion } = meta - if (createChatQuestion) { - this.requestMessage(sessionId, createChatQuestion) - meta.createChatQuestion = undefined - store.getState().updateState({ casualTitle: '等待回复中...' }) - } else { - // 调用历史数据恢复方法 + if (res.SyncID && meta.syncIDMap.has(res.SyncID)) { + // 标识同步ID已处理 + meta.syncIDMap.delete(res.SyncID) + store.getState().updateStateCount('syncIDUpdate') } - meta?.onSessionStartSuccess?.(sessionId) - return - } - if (res.Type === 'structured') { - let ipcContent = Uint8ArrayToString(res.Content) || '' - const obj = JSON.parse(ipcContent) || '' - - if (obj?.level) { - // 日志类型数据 - const data = obj as AIAgentGrpcApi.Log - aiAgentLogEmitter.dispatch({ - session: sessionId, - type: 'log', - Timestamp: res.Timestamp, - log: data, - }) + if (res.Type === 'pong') { + const { createChatQuestion } = meta + if (createChatQuestion) { + this.requestMessage(sessionId, createChatQuestion) + meta.createChatQuestion = undefined + store.getState().updateState({ casualTitle: '等待回复中...' }) + } else { + // 调用历史数据恢复方法 + } + meta?.onSessionStartSuccess?.(sessionId) return } - } - let funcKey = res.Type - if ( - res.Type === 'structured' && - [ - 'session_title', - 'timeline_item', - 'react_task_enqueue', - 'react_task_dequeue', - 'queue_info', - 'react_task_status_changed', - 'status', - 'stream-finished', - ].includes(res.NodeId) - ) { - funcKey = res.NodeId - } else if (res.Type === 'api_request_failed' && res.NodeId === 'ai_call_failure') { - funcKey = res.NodeId - } else if (res.Type === 'report_finish' || res.NodeId === 'report-finish') { - funcKey = res.NodeId - } else if (res.Type === 'structured' && res.NodeId === 'system') { - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) || '' - if (data && typeof data === 'object' && data?.type === 'push_task') { - funcKey = 'push_task' - } else if (data && typeof data === 'object' && data?.type === 'pop_task') { - funcKey = 'pop_task' + if (res.Type === 'structured') { + const obj = JSON.parse(ipcContent) || '' + + if (obj?.level) { + // 日志类型数据 + const data = obj as AIAgentGrpcApi.Log + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: res.Timestamp, + log: data, + }) + return + } } - } - const handleFunc = grpcAIMessageHandlers[funcKey || ''] - if (handleFunc) { - handleFunc({ - res, - chatType: meta.currentTaskPlanID?.coordinatorId === res.CoordinatorId ? 'task' : 'reAct', - store, - rawData, - request, - meta, - pushLog: () => {}, - sendRequest: (request) => this.requestMessage(sessionId, request), + + let funcKey = res.Type + if ( + res.Type === 'structured' && + [ + 'session_title', + 'timeline_item', + 'react_task_enqueue', + 'react_task_dequeue', + 'queue_info', + 'react_task_status_changed', + 'status', + 'stream-finished', + ].includes(res.NodeId) + ) { + funcKey = res.NodeId + } else if (res.Type === 'api_request_failed' && res.NodeId === 'ai_call_failure') { + funcKey = res.NodeId + } else if (res.Type === 'report_finish' || res.NodeId === 'report-finish') { + funcKey = res.NodeId + } else if (res.Type === 'structured' && res.NodeId === 'system') { + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) || '' + if (data && typeof data === 'object' && data?.type === 'push_task') { + funcKey = 'push_task' + } else if (data && typeof data === 'object' && data?.type === 'pop_task') { + funcKey = 'pop_task' + } + } + const handleFunc = grpcAIMessageHandlers[funcKey || ''] + if (handleFunc) { + handleFunc({ + sessionId, + res, + chatType: meta.currentTaskPlanID?.coordinatorId === res.CoordinatorId ? 'task' : 'reAct', + store, + rawData, + request, + meta, + sendRequest: (request) => this.requestMessage(sessionId, request), + }) + return + } + } catch (error) { + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: res.Timestamp, + log: { level: 'try-error', message: `${res.Type}-${res.NodeId}: ${error}` }, }) - return } } + /** 主动往列表里放入一条数据 */ + public pushDataToSession(sessionId: string, data: AIChatQSData) { + const { store, rawData } = this.ensureSession(sessionId) + rawData.contents.set(data.id, data) + store.getState().dispatchStreamingNode({ + chatType: data.chatType, + node: { + token: data.id, + kind: 'item', + type: data.type, + dataOrigin: 'grpc_realtime_data', + }, + groupTokenGenerator: () => '', + }) + } + private closeIPCListeners(sessionId: string) { ipcRenderer.removeAllListeners(`${sessionId}-data`) ipcRenderer.removeAllListeners(`${sessionId}-end`) @@ -405,7 +452,8 @@ export class ChatMultiSessionController { /** 关闭指定session的连接 * TODO - 期望传一个对象,{ sessionIds: string[]; aiSource: AISource } */ - public forceCloseSession(sessionId: string) { + public forceCloseSession(params: { sessionIds: string[]; source: string }) { + const { sessionIds, source } = params ipcRenderer.invoke('cancel-ai-re-act', sessionId).catch(() => {}) this.readyChannels.delete(sessionId) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index c2197f48ae..0a6b8043be 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -471,16 +471,32 @@ export interface ChatStoreState { /** 会话执行状态 */ execute: boolean + /** aiChat.d.ts AIAgentChatData httpFuzzRequest */ httpFuzzRequestUpdate: number + /** aiChat.d.ts AIAgentChatData httpFlowFuzzStatus */ httpFlowFuzzStatusUpdate: number - /** 更新会话标题名 */ + /** + * 更新会话标题名 + * aiChat.d.ts AIAgentChatData sessionTitle + */ sessionTitleUpdate: number - /** 记忆列表 */ + /** + * 记忆列表 + * aiChat.d.ts AIAgentChatData memoryList + */ memoryListUpdate: number - /** 系统流信息(isSystem=true&type=stream) */ + /** + * 系统流信息(isSystem=true&type=stream) + * aiChat.d.ts AIAgentChatData systemStream + */ updateSystemStream: number - /** yaklang_code_change 更新 */ + /** + * yaklang_code_change 更新 + * aiChat.d.ts AIAgentChatData yaklangCodeChange + */ yaklangCodeChangeUpdate: number + /** aiChat.d.ts AIAgentChatMetaData syncID */ + syncIDUpdate: number /** 接口运行过程中的数据文件夹合集 */ grpcFolders: AIFileSystemPin[] @@ -496,9 +512,11 @@ export interface ChatStoreState { /** 运行时产生http数据(是否显示tab,是否刷新数据) */ httpTabShow: boolean + /** aiChat.d.ts AIAgentChatData httpRunTimeIDs */ httpTabUpdate: number /** 运行时产生risk数据(是否显示tab,是否刷新数据) */ riskTabShow: boolean + /** aiChat.d.ts AIAgentChatData riskRunTimeIDs */ riskTabUpdate: number // #region 会话列表相关数据 @@ -517,7 +535,10 @@ export interface ChatStoreState { currentCasualReview: string[] /** 任务规划当前显示的review数据 */ currentPlanReviewToken: string - /** 当前review是plan时,异步数据的更新版本 */ + /** + * 当前review是plan时,异步数据的更新版本 + * aiChat.d.ts AIAgentChatMetaData planReviewExtraData + */ currentPlanReviewExtraUpdate: number items: Record @@ -526,6 +547,7 @@ export interface ChatStoreState { casualChat: { elements: ReActChatRenderElement[] + /** aiChat.d.ts AIAgentChatData casualChat['planDetails'] */ todoListUpdate: number } taskChat: { @@ -559,7 +581,8 @@ export interface ChatStoreState { | 'memoryListUpdate' | 'updateSystemStream' | 'yaklangCodeChangeUpdate' - | 'currentPlanReviewExtraUpdate', + | 'currentPlanReviewExtraUpdate' + | 'syncIDUpdate', ) => void updateFolders: (info: AIFileSystemPin) => void @@ -581,6 +604,7 @@ export interface ChatStoreState { | 'memoryListUpdate' | 'updateSystemStream' | 'yaklangCodeChangeUpdate' + | 'syncIDUpdate' | 'httpTabShow' | 'httpTabUpdate' | 'riskTabShow' diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index 99e066c7fb..e594d66108 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -22,6 +22,7 @@ export const createChatStore = () => { memoryListUpdate: 0, updateSystemStream: 0, yaklangCodeChangeUpdate: 0, + syncIDUpdate: 0, grpcFolders: [], reActTimelines: [], diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcAIMessageHandlers.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcAIMessageHandlers.ts deleted file mode 100644 index 932b955889..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcAIMessageHandlers.ts +++ /dev/null @@ -1,1001 +0,0 @@ -import type { AIMessageHandler, AIMessageHandlerParams, UpdateRenderDataParams } from './type' -import type { - AIChatQSData, - AIChatQSDataType, - ChatStream, - ReActChatBaseInfo, - ReActChatGroupElement, - ReActChatElement, - ReActChatRenderItem, - ReActChatTaskElement, - ReActChatTaskElementSub, -} from './aiRender' - -import { genErrorLogData } from './utils' -import { AIChatQSDataTypeEnum } from './aiRender' -import { AIStreamContentType } from './defaultConstant' -import cloneDeep from 'lodash/cloneDeep' -import { AIChatData } from '@/pages/ai-agent/type/aiChat' -import { AIOutputEvent } from './grpcApi' -// #region Common Utils -/** 自由对话:是否为子 agent 任务(planDetailsMap 用子任务 ID;contents 用复合 ID) */ -const isCasualSubAgentTask = ( - chatStore: AIChatData, - res: AIOutputEvent, - getTaskId?: AIMessageHandlerParams['getTaskId'], -) => { - if (chatStore.casualChat.planDetailsMap.has(res.TaskId)) return true - const parentTaskId = getTaskId?.() - if (!parentTaskId || !res.TaskId) return false - return ( - chatStore.casualChat.contents.get(`${parentTaskId}-${res.TaskId}`)?.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP - ) -} - -type ApplyCasualPlanDetailsFields = ( - target: PlanItemDetailsData, - ctx: { isMainCasualTask: boolean; taskId: string }, -) => void - -/** 主任务 planDetails 为单例,新一轮提问后 taskId 必须覆盖;子 agent 走 planDetailsMap */ -const resolvePlanDetailsTaskId = (target: PlanItemDetailsData, ctx: { isMainCasualTask: boolean; taskId: string }) => { - target.taskId = ctx.isMainCasualTask ? ctx.taskId : target.taskId || ctx.taskId -} - -const updateCasualPlanItemDetails = ( - chatStore: AIChatData, - res: AIOutputEvent, - getTaskId: AIMessageHandlerParams['getTaskId'], - applyFields: ApplyCasualPlanDetailsFields, -) => { - if (!res.TaskId) return - const isSubAgentTask = isCasualSubAgentTask(chatStore, res, getTaskId) - const chatDetail = isSubAgentTask - ? chatStore.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - : chatStore.casualChat.planDetails || cloneDeep(DefaultPlanItemDetailsData) - applyFields(chatDetail, { isMainCasualTask: !isSubAgentTask, taskId: res.TaskId }) - if (isSubAgentTask) { - chatStore.casualChat.planDetailsMap.set(res.TaskId, chatDetail) - } else { - chatStore.casualChat.planDetails = chatDetail - } -} -/** grpc流数据转换成错误信息输出到日志中 */ -const handleErrorGRPCToLog: ( - /** 该条grpc流数据是历史数据 */ - isHistory: AIMessageHandlerParams['res']['IsSync'], - pushLog: AIMessageHandlerParams['pushLog'], - error: ReturnType, -) => void = (isHistory, pushLog, error) => { - if (isHistory) return - pushLog(error) -} - -/** - * 在 elements 树中查找并更新指定渲染项(含 Task 组 / stream 组嵌套) - * 只是更新renderNum,不包含新建逻辑 - */ -const bumpRenderItem = (list: ReActChatRenderItem[], info: UpdateRenderDataParams): boolean => { - for (const item of list) { - if (item.token === info.mapKey && item.type === info.type) { - item.renderNum += 1 - return true - } - if (item.kind === 'task') { - for (const child of item.children) { - if (child.token === info.mapKey && child.type === info.type) { - child.renderNum += 1 - item.renderNum += 1 - return true - } - if (child.kind === 'group') { - const sub = child.children.find((c) => c.token === info.mapKey && c.type === info.type) - if (sub) { - sub.renderNum += 1 - child.renderNum += 1 - item.renderNum += 1 - return true - } - } - } - } - if (item.kind === 'group') { - const sub = item.children.find((c) => c.token === info.mapKey && c.type === info.type) - if (sub) { - sub.renderNum += 1 - item.renderNum += 1 - return true - } - } - } - return false -} - -/** - * 将新渲染项追加到 elements(只包含普通节点 和 Task 任务组,单节点不会出现在 Stream 组内) - * 只是新建逻辑,不包含更新renderNum - */ -const appendRenderItem = ( - old: ReActChatRenderItem[], - element: ReActChatRenderItem, - isHistory: boolean, - options?: { taskId: AIChatQSData['taskId']; getContentMap: AIMessageHandlerParams['getContentMap'] }, -): ReActChatRenderItem[] => { - if (options?.taskId) { - const taskGroupKey = options?.taskId - const groupIndex = old.findIndex((item) => item.kind === 'task' && item.token === taskGroupKey) - if (groupIndex >= 0) { - const list = [...old] - const group = list[groupIndex] as ReActChatTaskElement - const children = ( - isHistory ? [element, ...group.children] : [...group.children, element] - ) as ReActChatTaskElementSub[] - list[groupIndex] = { ...group, children, renderNum: group.renderNum + 1 } - return list - } - } - return isHistory ? [element, ...old] : [...old, element] -} - -/** 更新UI-State变量数据(独立单条数据) */ -const handleUpdateUISingleState = ( - setElements: AIMessageHandlerParams['setElements'], - getContentMap: AIMessageHandlerParams['getContentMap'], - isHistory: AIMessageHandlerParams['res']['IsSync'], - info: UpdateRenderDataParams & { chatType: ReActChatBaseInfo['chatType'] }, -) => { - try { - setElements((old) => { - if (bumpRenderItem(old, info)) { - return [...old] - } - - const element: ReActChatRenderItem = { - chatType: info.chatType, - token: info.mapKey, - type: info.type, - kind: 'item', - renderNum: 1, - } - const chatDetail = getContentMap(info.mapKey) - if (!chatDetail || chatDetail.id !== info.mapKey) return old - return appendRenderItem(old, element, isHistory, { - taskId: chatDetail?.taskId, - getContentMap, - }) - }) - } catch {} -} - -/** 更新UI-State变量数据(组数据) */ -const handleUpdateUIGroupState: ( - /** group数据 */ - group: { mapKey: string; type: AIChatQSDataType }, - /** sub数据 */ - sub: { mapKey: string; type: AIChatQSDataType }, - setElement: AIMessageHandlerParams['setElements'], - /** 父 TaskId 集合组 token */ - taskNodeKey?: string, -) => void = (group, sub, setElement, taskNodeKey) => { - try { - setElement((old) => { - const scope: ReActChatRenderItem[] = taskNodeKey - ? (old.find((item) => item.kind === 'task' && item.token === taskNodeKey) as ReActChatTaskElement | undefined) - ?.children || [] - : old - - const find = scope.find((item) => item.token === group.mapKey && item.type === group.type) - if (find && find.kind === 'group') { - const subFind = find.children.find((item) => item.token === sub.mapKey && item.type === sub.type) - if (subFind) subFind.renderNum += 1 - find.renderNum += 1 - if (taskNodeKey) { - const taskGroup = old.find((item) => item.kind === 'task' && item.token === taskNodeKey) as - | ReActChatTaskElement - | undefined - if (taskGroup) taskGroup.renderNum += 1 - } - return [...old] - } - - return old - }) - } catch {} -} -// #endregion - -// #region 单项流数据转换为独立UI数据 -/** Type='thought' 问题的思考 */ -const handleThought: AIMessageHandler = (request) => { - const { res, info, setContentMap } = request - if (res.Type !== 'thought') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { thought } = (JSON.parse(ipcContent) as AIAgentGrpcApi.AIChatThought) || {} - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - type: AIChatQSDataTypeEnum.THOUGHT, - data: thought || '', - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - setContentMap(chatData.id, chatData) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) -} - -/** Type='result' 问题一次性的结果输出 */ -const handleResult: AIMessageHandler = (request) => { - const { res, info, setContentMap } = request - if (res.Type !== 'result') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { result, after_stream } = (JSON.parse(ipcContent) as AIAgentGrpcApi.AIChatResult) || {} - if (after_stream) return - - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - type: AIChatQSDataTypeEnum.THOUGHT, - data: result || '', - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - setContentMap(chatData.id, chatData) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) -} - -/** Type='fail_react_task' ReAct任务(自由对话)崩溃的错误信息 */ -const handleFailReactTask: AIMessageHandler = (request) => { - const { res, info, setContentMap } = request - if (res.Type !== 'fail_react_task') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - type: AIChatQSDataTypeEnum.FAIL_REACT, - data: { - content: ipcContent, - NodeId: res.NodeId, - NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), - }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - setContentMap(chatData.id, chatData) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) -} - -/** Type='tool_call_decision' 工具决策 */ -const handleToolCallDecision: AIMessageHandler = (request) => { - const { res, info, setContentMap } = request - if (res.Type !== 'tool_call_decision') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ToolCallDecision - const i18n = data?.i18n || { zh: data.action, en: data.action } - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - type: AIChatQSDataTypeEnum.TOOL_CALL_DECISION, - data: { - ...data, - i18n: { - Zh: i18n.zh, - En: i18n.en, - }, - }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - setContentMap(chatData.id, chatData) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) -} - -/** Type='fail_plan_and_execution' 任务规划崩溃的错误信息[在任务规划启动就崩溃时,出现在自由对话中] */ -const handleFailPlanAndExecution: AIMessageHandler = (request) => { - const { res, info, setContentMap } = request - if (res.Type !== 'fail_plan_and_execution') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - type: AIChatQSDataTypeEnum.FAIL_PLAN_AND_EXECUTION, - data: { - content: ipcContent, - NodeId: res.NodeId, - NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), - }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - setContentMap(chatData.id, chatData) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) -} - -/** Type='structured'&NodeId='react_task_dequeue' 生成用户问题到自由对话的UI上展示 */ -const handleReactTaskDequeue: AIMessageHandler = (request) => { - const { res, info, setContentMap, getContentMap, setElements, getChatDataStore } = request - if (res.Type !== 'structured' || res.NodeId !== 'react_task_dequeue') return - // 任务规划-该类型数据为无效数据 - if (info.chatType === 'task') return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.QuestionQueueStatusChange - const chatData: AIChatQSData = { - id: res.TaskId || data.react_task_id, - chatType: 'reAct', - type: AIChatQSDataTypeEnum.QUESTION, - Timestamp: res.Timestamp, - data: { qs: data.react_task_input || '', setting: {} }, - AIService: '', - AIModelName: '', - // showQS为了UI渲染方便,重新构建的字段 - extraValue: { showQS: data.react_task_input || '' }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - setContentMap(chatData.id, chatData) - - if (data.react_task_user_input_uuid) { - const qsDetail = getContentMap(data.react_task_user_input_uuid) - if (qsDetail && qsDetail.type === AIChatQSDataTypeEnum.QUESTION) { - // getChatDataStore()?.casualChat.contents.delete(data.react_task_user_input_uuid) - // setElements((old) => - // old.map((item) => { - // if (item.token === data.react_task_user_input_uuid) { - // return { ...item, token: data.react_task_id } - // } - // return item - // }), - // ) - return - } - } - - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) -} - -/** Type='api_request_failed'&NodeId='ai_call_failure' 模型/API 请求失败 */ -const handleApiRequestFailed: AIMessageHandler = (request) => { - const { res, info, setContentMap } = request - if (res.Type !== 'api_request_failed' || res.NodeId !== 'ai_call_failure') return - // 历史数据无用-不处理 - if (res.IsSync) return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIApiRequestFailedPayload - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - type: AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED, - data, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - setContentMap(chatData.id, chatData) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: chatData.type, - chatType: chatData.chatType, - }) -} - -/** Type='http_flow_fuzz_status' 发包统计卡片:按 fuzz_id 维护一张 HTTP_FLOW_FUZZ_STATUS 卡片 */ -const handleHttpFlowFuzzStatus: AIMessageHandler = (request) => { - const { res, info, setContentMap, getContentMap, pushLog } = request - if (res.Type !== 'http_flow_fuzz_status') return - // 历史数据无用-不处理 - if (res.IsSync) return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const payload = JSON.parse(ipcContent) as AIAgentGrpcApi.GetHttpFlowFuzzStatus - const { fuzz_id, runtime_id, reason, status } = payload - if (!fuzz_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type} 数据缺少 fuzz_id`)) - return - } - - const cardType = AIChatQSDataTypeEnum.HTTP_FLOW_FUZZ_STATUS - const existing = getContentMap(fuzz_id) - const isExistingCard = existing?.type === cardType - - // 引擎结束态没有对应卡片时直接丢弃,保留原行为 - if (status === 'finish' && !isExistingCard) return - - const nextData: HttpFlowFuzzStatusCardData = { - fuzz_id, - runtime_id, - reason, - engine_status: status, - // 仅 `working` 覆盖 progress;其它状态保留上一次(新建时默认 undefined) - progress: status === 'working' ? payload.progress : isExistingCard ? existing!.data.progress : undefined, - } - - if (isExistingCard) { - Object.assign(existing!.data, nextData) - } else { - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - id: fuzz_id, - chatType: info.chatType, - type: cardType, - data: nextData, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - } - setContentMap(fuzz_id, chatData) - } - - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: fuzz_id, - type: cardType, - chatType: info.chatType, - }) -} - -/** Type='report_finish' NodeId='report-finish' 报告生成完成:展示报告路径 */ -const handleReportFinish: AIMessageHandler = (request) => { - const { res, info, setContentMap, pushLog } = request - if (res.Type !== 'report_finish' || res.NodeId !== 'report-finish') return - - const ipcContent = Uint8ArrayToString(res.Content) || '{}' - - const parsed = JSON.parse(ipcContent) as AIAgentGrpcApi.ReportFinishPayload - let report_path = parsed?.report_path ?? '' - let title = parsed?.title ?? '' - let content = parsed?.summary_markdown ?? '' - - if (!report_path) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type} 数据缺少 report_path`)) - return - } - - const cardType = AIChatQSDataTypeEnum.REPORT_FINISH - const nextData: ReportFinishCardData = { reportPath: report_path, title, content } - - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: info.chatType, - type: cardType, - data: nextData, - } - setContentMap(chatData.id, chatData) - handleUpdateUISingleState(request.setElements, request.getContentMap, res.IsSync, { - mapKey: chatData.id, - type: cardType, - chatType: info.chatType, - }) -} -/** Type='current_task_todo_list_update'&NodeId='current_task_todo_list' todolist */ -const handleCurrentTaskTodoListUpdate: AIMessageHandler = (request) => { - const { res, info, getChatDataStore, getTaskId, callback } = request - if (!res.TaskId) return - if (res.Type !== 'current_task_todo_list_update' || res.NodeId !== 'current_task_todo_list') return - - const chatStore = getChatDataStore?.() - if (!chatStore) return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - // 更新待办清单卡片数据 - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.TodoListUpdate - if (isEmpty(data)) return - - const newData = handleTodoListData(data.items, data.task_id) - const applyTodoListFields: ApplyCasualPlanDetailsFields = (target, ctx) => { - target.uuid = uuidv4() - resolvePlanDetailsTaskId(target, ctx) - target.todoList = newData - } - - if (info.chatType === 'task') { - const oldData = chatStore.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - applyTodoListFields(oldData, { isMainCasualTask: false, taskId: res.TaskId }) - chatStore.taskChat.planDetailsMap.set(res.TaskId, oldData) - } else if (info.chatType === 'reAct') { - updateCasualPlanItemDetails(chatStore, res, getTaskId, applyTodoListFields) - callback?.(res) - } -} -/** Type='structured'&NodeId='capability_inventory' 能力清单(tool/skills/forge/yak_plugin/mac) */ -const handleCapabilityInventory: AIMessageHandler = (request) => { - const { res, info, getChatDataStore, getTaskId } = request - if (!res.TaskId) return - if (res.Type !== 'structured' || res.NodeId !== 'capability_inventory') return - - const chatStore = getChatDataStore?.() - if (!chatStore) return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const payload = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanItemDetails - if (isEmpty(payload)) return - const { fixed, dynamic } = payload - - const itemData: Pick = { - uuid: uuidv4(), - tool: { - fixed: [], - dynamic: [], - }, - forges: { - fixed: [], - dynamic: [], - }, - skills: { - fixed: [], - dynamic: [], - }, - plugins: { - fixed: [], - dynamic: [], - }, - mcp: { - fixed: [], - dynamic: [], - }, - } - - if (!!fixed?.tools) { - for (const item of fixed.tools) { - switch (item.category) { - case 'tool': - itemData.tool.fixed.push(item) - break - case 'yak_plugin': - itemData.plugins.fixed.push(item) - break - case 'mcp': - itemData.mcp.fixed.push(item) - break - default: - break - } - } - } - /** 暂时目前没有这个数据 */ - // if (!!fixed?.mcp_servers) { - // itemData.mcpServices.fixed = fixed.mcp_servers - // } - if (!!fixed?.forges) { - itemData.forges.fixed = fixed.forges - } - if (!!fixed?.skills) { - itemData.skills.fixed = fixed.skills - } - - if (!!dynamic?.tools) { - for (const item of dynamic.tools) { - switch (item.category) { - case 'tool': - itemData.tool.dynamic.push(item) - break - case 'yak_plugin': - itemData.plugins.dynamic.push(item) - break - case 'mcp': - itemData.mcp.dynamic.push(item) - break - default: - break - } - } - } - if (!!dynamic?.skills) { - itemData.skills.dynamic = dynamic.skills - } - if (!!dynamic?.forges) { - itemData.forges.dynamic = dynamic.forges - } - const applyCapabilityFields: ApplyCasualPlanDetailsFields = (target, ctx) => { - target.uuid = itemData.uuid - resolvePlanDetailsTaskId(target, ctx) - target.tool = itemData.tool - target.forges = itemData.forges - target.skills = itemData.skills - target.plugins = itemData.plugins - target.mcp = itemData.mcp - } - if (info.chatType === 'task') { - const oldData = chatStore.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - applyCapabilityFields(oldData, { isMainCasualTask: false, taskId: res.TaskId }) - chatStore.taskChat.planDetailsMap.set(res.TaskId, oldData) - } else if (info.chatType === 'reAct') { - updateCasualPlanItemDetails(chatStore, res, getTaskId, applyCapabilityFields) - } -} -/** Type='perception'&NodeId='perception' 意图感知 */ -const handlePerception: AIMessageHandler = (request) => { - const { res, info, getChatDataStore, getTaskId } = request - if (!res.TaskId) return - if (res.Type !== 'perception' || res.NodeId !== 'perception') return - const chatStore = getChatDataStore?.() - if (!chatStore) return - const ipcContent = Uint8ArrayToString(res.Content) || '' - const perception = (JSON.parse(ipcContent) as AIAgentGrpcApi.PerceptionData) || {} - if (isEmpty(perception)) return - perception.summary = isArray(perception.summary) ? perception.summary.join(',') : perception.summary - const applyPerceptionFields: ApplyCasualPlanDetailsFields = (target, ctx) => { - target.uuid = uuidv4() - resolvePlanDetailsTaskId(target, ctx) - target.perception = perception - } - if (info.chatType === 'task') { - const oldData = chatStore.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - applyPerceptionFields(oldData, { isMainCasualTask: false, taskId: res.TaskId }) - chatStore.taskChat.planDetailsMap.set(res.TaskId, oldData) - } else if (info.chatType === 'reAct') { - updateCasualPlanItemDetails(chatStore, res, getTaskId, applyPerceptionFields) - } -} - -const handleSessionSnapshot: AIMessageHandler = (request) => { - const { res, info, getChatDataStore, getTaskId } = request - if (!res.TaskId) return - if (res.NodeId !== 'session_snapshot') return - - const chatStore = getChatDataStore?.() - if (!chatStore) return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const snapshot = (JSON.parse(ipcContent) as AIAgentGrpcApi.SessionSnapshot) || {} - if (isEmpty(snapshot)) return - const applySnapshotFields: ApplyCasualPlanDetailsFields = (target, ctx) => { - target.uuid = uuidv4() - resolvePlanDetailsTaskId(target, ctx) - target.execution = snapshot.execution - target.backgroundProcesses = snapshot.background_processes - } - if (info.chatType === 'task') { - const oldData = chatStore.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - applySnapshotFields(oldData, { isMainCasualTask: false, taskId: res.TaskId }) - chatStore.taskChat.planDetailsMap.set(res.TaskId, oldData) - } else if (info.chatType === 'reAct') { - updateCasualPlanItemDetails(chatStore, res, getTaskId, applySnapshotFields) - } -} -// #endregion - -// #region stream数据相关逻辑 -/** - * - 存放 Type:stream NodeId:tool-xxx-stderr 的内容数据 - * - call_tool_id => {content:string uuid:string status:"start" | "end"} - * - 当stream-finished触发后,将内容全部设置到工具结果对象中的execError字段中 - * - 本NodeId和stream类型中的其他NodeId有一样的后端逻辑,但是前端需要将其区分出来 - */ -const ToolResultForStreamError: Map = new Map() - -/** Type='stream_start' stream类型数据初始化 */ -const handleStreamStart: AIMessageHandler = (request) => { - const { res, info, setContentMap, getContentMap, pushLog } = request - if (res.Type !== 'stream_start') return - // 属于日志数据的不进入UI展示 - if (res.IsSystem || res.IsReason) return - - const { CallToolID, NodeId } = res - if (!NodeId) return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const { event_writer_id } = JSON.parse(ipcContent) as { event_writer_id: string } - // event_writer_id为空 - if (!event_writer_id) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), event_writer_id 为空`), - ) - return - } - - // tool-xxx-stdout 数据单独初始化逻辑 - if (isToolStdoutStream(NodeId)) { - if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), - ) - return - } - let toolResult = getContentMap(CallToolID || '') - if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `NodeID: ${NodeId} 的stream数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, - ), - ) - return - } - - // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 - toolResult.data.stream.EventUUID = event_writer_id - toolResult.data.type = 'stream' - setContentMap(event_writer_id, { - ...genBaseAIChatData(res), - id: event_writer_id, - chatType: info.chatType, - type: AIChatQSDataTypeEnum.STREAM, - data: { - NodeId, - NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(NodeId), - CallToolID, - EventUUID: event_writer_id, - status: 'start', - content: '', - ContentType: res.ContentType, - }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - }) - return - } - // tool-xxx-stderr 数据单独初始化逻辑 - if (isToolStderrStream(NodeId) && CallToolID) { - if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), - ) - return - } - if (!ToolResultForStreamError.has(CallToolID)) { - ToolResultForStreamError.set(CallToolID, { - content: '', - uuid: event_writer_id, - status: 'start', - }) - } - return - } - - // 数据集合中对应的数据 - const streamData = getContentMap(event_writer_id) - - // 数据已存在,流数据输出顺序不对, 视为异常 - if (streamData) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `异常 ${res.Type} 类型, NodeId: ${NodeId}, eventuuid: (${event_writer_id}), 已存在对应的数据`, - ), - ) - return - } - - setContentMap(event_writer_id, { - ...genBaseAIChatData(res), - id: event_writer_id, - chatType: info.chatType, - type: AIChatQSDataTypeEnum.STREAM, - data: { - NodeId, - NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(NodeId), - CallToolID, - EventUUID: event_writer_id, - status: 'start', - content: '', - ContentType: res.ContentType, - }, - taskId: generateTaskId({ - chatType: info.chatType, - res, - getCurrentTaskPlanID: request.getCurrentTaskPlanID, - getTaskId: request.getTaskId, - getContentMap: request.getContentMap, - }), - }) -} - -/** 将 task 容器内 children 写回顶层 list,并给renderNum加一 */ -const writeBackTaskGroupChildren = ( - list: ReActChatRenderItem[], - taskGroupIndex: number, - children: ReActChatTaskElementSub[], -): ReActChatRenderItem[] => { - const taskGroup = list[taskGroupIndex] as ReActChatTaskElement - list[taskGroupIndex] = { ...taskGroup, children, renderNum: taskGroup.renderNum + 1 } - return list -} - -/** stream数据初始化到UI上的逻辑处理 */ -const handleIsGroupDisplayForStream: ( - /** grpc流数据 */ - res: AIMessageHandlerParams['res'], - /** stream类型对应的详细数据 */ - streamDetail: ChatStream, - /** 当前渲染的数据列表 */ - data: ReActChatRenderItem[], - /** 获取详情数据映射的函数 */ - getContentMap: AIMessageHandlerParams['getContentMap'], -) => ReActChatRenderItem[] = (res, streamDetail, data, getContentMap) => { - const taskNodeKey = streamDetail.taskId - const list = [...data] - /** 任务节点的索引 */ - let taskNodeIndex = -1 - /** task 容器内操作的列表;未命中时为顶层 list */ - let targetList: ReActChatTaskElementSub[] | ReActChatRenderItem[] = list - - if (taskNodeKey) { - taskNodeIndex = list.findIndex((item) => item.kind === 'task' && item.token === taskNodeKey) - if (taskNodeIndex >= 0) { - const taskGroup = list[taskNodeIndex] as ReActChatTaskElement - targetList = [...taskGroup.children] - } - } - - const { ContentType, IsSync } = res - const element: ReActChatElement = { - chatType: streamDetail.chatType, - token: streamDetail.id, - type: streamDetail.type, - kind: 'item', - renderNum: 1, - } - - const commitIfInTaskGroup = (): ReActChatRenderItem[] | null => { - if (taskNodeIndex < 0) return null - return writeBackTaskGroupChildren(list, taskNodeIndex, targetList as ReActChatTaskElementSub[]) - } - - // 以下 判断stream数据已经渲染在UI上的逻辑处理 - const find = targetList.find((item) => item.token === element.token) - if (find) { - // 已经渲染到UI上, 是单个节点,或者是task节点下的单个节点/组数据的节点key命中 - if (find.kind === 'group') { - const subFind = find.children.find((item) => item.token === element.token && item.type === element.type) - if (subFind) subFind.renderNum += 1 - } - find.renderNum += 1 - return commitIfInTaskGroup() ?? [...targetList] - } - if (streamDetail && streamDetail.parentGroupKey) { - // 已经渲染到UI上, 不是组数据的key, 但是是组内数据, 找到组信息,并触发渲染更新 - const group = targetList.find( - (item) => item.token === streamDetail.parentGroupKey && item.type === AIChatQSDataTypeEnum.STREAM_GROUP, - ) - if (group && group.kind === 'group') { - const subFind = group.children.find((item) => item.token === element.token && item.type === element.type) - if (subFind) subFind.renderNum += 1 - group.renderNum += 1 - } - return commitIfInTaskGroup() ?? [...targetList] - } - - // 以下 stream数据没有渲染在UI上的逻辑处理 - if (ContentType !== AIStreamContentType.DEFAULT || !targetList.length) { - // 新增不可成组类型数据 - IsSync ? targetList.unshift(element) : targetList.push(element) - return commitIfInTaskGroup() ?? [...targetList] - } - - const active = IsSync ? targetList[0] : targetList[targetList.length - 1] - const activeDetail = getContentMap(active.token) - if (!activeDetail || activeDetail.type !== AIChatQSDataTypeEnum.STREAM) { - // UI详细数据没有或不是可成组类型,新增数据到UI上 - IsSync ? targetList.unshift(element) : targetList.push(element) - return commitIfInTaskGroup() ?? [...targetList] - } - - if (active.type === AIChatQSDataTypeEnum.STREAM && active.kind !== 'group') { - if (activeDetail.data.NodeId === streamDetail.data.NodeId) { - // 命中单项,准备整合成组数据,将原有单项的token当成组token - const groupInfo: ReActChatGroupElement = { - chatType: active.chatType, - token: active.token, - type: AIChatQSDataTypeEnum.STREAM_GROUP, - renderNum: 1, - kind: 'group', - children: [], - } - groupInfo.children = IsSync - ? [element, cloneDeep(active) as ReActChatElement] - : [cloneDeep(active) as ReActChatElement, element] - const arr = groupInfo.children.map((item) => item.token) - for (let el of arr) { - const info = getContentMap(el) - if (info) info.parentGroupKey = active.token - } - IsSync ? targetList.shift() : targetList.pop() - IsSync ? targetList.unshift(groupInfo) : targetList.push(groupInfo) - } else { - IsSync ? targetList.unshift(element) : targetList.push(element) - } - return commitIfInTaskGroup() ?? [...targetList] - } else if (active.type === AIChatQSDataTypeEnum.STREAM_GROUP && active.kind === 'group') { - if (activeDetail.data.NodeId === streamDetail.data.NodeId) { - // 命中组内数据,追加到组内 - streamDetail.parentGroupKey = active.token - IsSync ? active.children.unshift(element) : active.children.push(element) - active.renderNum += 1 - } else { - IsSync ? targetList.unshift(element) : targetList.push(element) - } - return commitIfInTaskGroup() ?? [...targetList] - } else { - IsSync ? targetList.unshift(element) : targetList.push(element) - return commitIfInTaskGroup() ?? [...targetList] - } -} - -/** - * grpc流数据的各种类型处理逻辑集合 - * 该逻辑集合里的方法处理,没有使用try-catch拦截,因为在hook层进行了同一try-catch拦截 - * 注意!别的地方单独使用时,请自行加入try-catch拦截错误 - */ -export const grpcAIMessageHandlers: Record = {} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index 59932f3ab0..01af529437 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -1,23 +1,12 @@ -import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import type { AIMessageHandler } from '../type' import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi, type AIOutputEvent } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { genBaseAIChatData, generateTaskNodeID, genErrorLogData, genExecTasks } from '../utils' +import { genBaseAIChatData, generateTaskNodeID, genExecTasks, pushLogToOtherWindow } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { DefaultCurrentExecTaskTree, DefaultPlanLoadingStatus, DefaultTodoListCardData } from '../defaultConstant' import has from 'lodash/has' -/** grpc流数据转换成错误信息输出到日志中 */ -const handleErrorGRPCToLog: ( - /** 该条grpc流数据是历史数据 */ - isHistory: AIMessageHandlerParams['res']['IsSync'], - pushLog: AIMessageHandlerParams['pushLog'], - error: ReturnType, -) => void = (isHistory, pushLog, error) => { - if (isHistory) return - pushLog(error) -} - const handleHttpFuzzRequestChange: AIMessageHandler = (request) => { const { res, store, rawData } = request if (res.Type !== 'http_fuzz_request_change') return @@ -41,18 +30,20 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (request) => { } const handleSessionTitle: AIMessageHandler = (request) => { - const { res, store, rawData, pushLog } = request + const { res, store, rawData } = request if (res.Type !== 'structured' || res.NodeId !== 'session_title') return if (res.IsSync) return const ipcContent = Uint8ArrayToString(res.Content) || '' const nameInfo = JSON.parse(ipcContent) as { title: string } if (!nameInfo || !nameInfo.title) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.NodeId}数据, 数据错误: ${JSON.stringify(nameInfo.title)}`), - ) + pushLogToOtherWindow({ + sessionId: request.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.NodeId}数据错误: ${ipcContent}`, + }) return } rawData.sessionTitle = nameInfo.title @@ -60,7 +51,7 @@ const handleSessionTitle: AIMessageHandler = (request) => { } const handleStartPlanAndExecution: AIMessageHandler = (request) => { - const { res, store, rawData, meta, pushLog, sendRequest } = request + const { res, store, rawData, meta, sendRequest } = request if (res.Type !== 'start_plan_and_execution') return if (res.IsSync) return @@ -70,7 +61,13 @@ const handleStartPlanAndExecution: AIMessageHandler = (request) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution if (!startInfo.coordinator_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, coordinator_id 为空`)) + pushLogToOtherWindow({ + sessionId: request.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据, coordinator_id 为空`, + }) return } meta.currentTaskPlanID = { @@ -110,14 +107,20 @@ const handleStartPlanAndExecution: AIMessageHandler = (request) => { }) } const handleEndPlanAndExecution: AIMessageHandler = (request) => { - const { res, store, rawData, meta, pushLog } = request + const { res, store, rawData, meta } = request if (res.Type !== 'end_plan_and_execution') return if (res.IsSync) return const ipcContent = Uint8ArrayToString(res.Content) || '' const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution if (!startInfo.coordinator_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据, coordinator_id 为空`)) + pushLogToOtherWindow({ + sessionId: request.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据, coordinator_id 为空`, + }) return } if (startInfo.coordinator_id === meta.currentTaskPlanID?.coordinatorId) { @@ -397,12 +400,18 @@ const handleStatus: AIMessageHandler = (request) => { } const handleTrafficCount: AIMessageHandler = (request) => { - const { res, chatType, store, rawData, pushLog } = request + const { res, chatType, store, rawData } = request const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.HTTPTrafficNotice & AIAgentGrpcApi.RiskTrafficNotice if (!data.runtime_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: request.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } @@ -420,11 +429,13 @@ const handleTrafficCount: AIMessageHandler = (request) => { // 更新工具执行结果卡片里的流量和风险数量 const toolResult = rawData.contents.get(data.runtime_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || toolResult.chatType !== chatType) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(call_tool_id:${data.runtime_id})工具结果卡片数据不存在`), - ) + pushLogToOtherWindow({ + sessionId: request.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(call_tool_id:${data.runtime_id})工具结果卡片数据不存在`, + }) return } let update = false diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index 43bab286ac..541b1ae422 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -1,10 +1,10 @@ -import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import type { AIMessageHandler } from '../type' import type { AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { genBaseAIChatData, generateTaskNodeDataID, - genErrorLogData, + pushLogToOtherWindow, genExecTasks, isAutoExecuteReviewContinue, } from '../utils' @@ -12,19 +12,8 @@ import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { AIReviewJudgeLevelMap } from '../defaultConstant' -/** grpc流数据转换成错误信息输出到日志中 */ -const handleErrorGRPCToLog: ( - /** 该条grpc流数据是历史数据 */ - isHistory: AIMessageHandlerParams['res']['IsSync'], - pushLog: AIMessageHandlerParams['pushLog'], - error: ReturnType, -) => void = (isHistory, pushLog, error) => { - if (isHistory) return - pushLog(error) -} - const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, request, meta } = requestInfo if (res.Type !== 'plan_review_require') return // 历史数据-grpc流数据在任务规划下无效,不处理 if (res.IsSync && chatType === 'task') return @@ -32,7 +21,13 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequire if (!data?.id || !data?.plans || !data?.plans?.root_task || !data?.selectors || !data?.selectors?.length) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } @@ -98,7 +93,7 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { } } const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, request, meta } = requestInfo if (res.Type !== 'plan_task_analysis') return // 历史数据-grpc流数据在任务规划下无效,不处理 if (res.IsSync || chatType !== 'task') return @@ -111,17 +106,25 @@ const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { !data?.keywords?.length || (meta.currentPlanReviewExtraId && meta.currentPlanReviewExtraId !== data.plans_id) ) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } const reviewDetail = rawData.contents.get(store.getState().currentPlanReviewToken) if (!reviewDetail || reviewDetail.type !== AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据异常: 未找到对应plan_review_require数据`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: 未找到对应plan_review_require数据`, + }) return } @@ -138,13 +141,19 @@ const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { } const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta, pushLog, sendRequest } = requestInfo + const { res, chatType, store, rawData, request, meta } = requestInfo if (res.Type !== 'task_review_require') return const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.TaskReviewRequire if (!data?.id || !data?.selectors || !data?.selectors?.length) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } @@ -201,13 +210,19 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { } const handleToolReview: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, request, meta } = requestInfo if (res.Type !== 'tool_use_review_require') return const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ToolUseReviewRequire if (!data?.id || !data?.selectors || !data?.selectors?.length) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } @@ -264,13 +279,19 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { } const handleUserInteractive: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'require_user_interactive') return const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewRequire if (!data?.id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } @@ -330,7 +351,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { } const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, request, meta } = requestInfo if (res.Type !== 'exec_aiforge_review_require') return // 任务规划不存在该类型数据 if (chatType === 'task') return @@ -338,7 +359,13 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ExecForgeReview if (!data?.id || !data?.selectors || !data?.selectors?.length) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } @@ -401,23 +428,31 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { } const handleAIReviewJudgement: AIMessageHandler = (requestInfo) => { - const { res, store, rawData, pushLog } = requestInfo + const { res, store, rawData } = requestInfo if (!['ai_review_start', 'ai_review_countdown', 'ai_review_end'].includes(res.Type)) return if (res.IsSync) return const ipcContent = Uint8ArrayToString(res.Content) || '' const score = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewJudgement if (!score?.interactive_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } const reviewDetail = rawData.contents.get(score.interactive_id) if (!reviewDetail || reviewDetail.id !== score.interactive_id) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据异常: 没有对应的review数据`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: 没有对应的review数据`, + }) return } @@ -437,26 +472,31 @@ const handleAIReviewJudgement: AIMessageHandler = (requestInfo) => { break default: - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据异常(interactive_id:${score?.interactive_id || '-'})未找到对应review`, - ), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常(interactive_id:${score?.interactive_id || '-'})未找到对应review`, + }) break } } const handleReviewRelease: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'review_release') return const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ReviewRelease if (!data?.id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } @@ -497,11 +537,13 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { } if (!reviewDetail) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(id:${data?.id || '-'})没有对应的review数据`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(id:${data?.id || '-'})没有对应的review数据`, + }) return } switch (reviewDetail.type) { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index 2ca8d9f8f0..28374c1c28 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -1,27 +1,16 @@ -import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import type { AIMessageHandler } from '../type' import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi, type AIOutputEvent } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { genBaseAIChatData, generateTaskNodeDataID, generateTaskNodeID, - genErrorLogData, + pushLogToOtherWindow, isValidTaskIndex, } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum, type ReportFinishCardData } from '../aiRender' import { convertNodeIdToVerbose } from '../defaultConstant' -/** grpc流数据转换成错误信息输出到日志中 */ -const handleErrorGRPCToLog: ( - /** 该条grpc流数据是历史数据 */ - isHistory: AIMessageHandlerParams['res']['IsSync'], - pushLog: AIMessageHandlerParams['pushLog'], - error: ReturnType, -) => void = (isHistory, pushLog, error) => { - if (isHistory) return - pushLog(error) -} - const handleThought: AIMessageHandler = (requestInfo) => { const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'thought') return @@ -232,7 +221,7 @@ const handleApiRequestFailed: AIMessageHandler = (requestInfo) => { } const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'http_flow_fuzz_status') return // 历史数据无用-不处理 if (res.IsSync) return @@ -241,7 +230,13 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { const payload = JSON.parse(ipcContent) as AIAgentGrpcApi.GetHttpFlowFuzzStatus const { fuzz_id, runtime_id, reason, status } = payload if (!fuzz_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } @@ -292,7 +287,7 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { } const handleReportFinish: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'report_finish' || res.NodeId !== 'report-finish') return const ipcContent = Uint8ArrayToString(res.Content) || '{}' @@ -303,7 +298,13 @@ const handleReportFinish: AIMessageHandler = (requestInfo) => { let content = parsed?.summary_markdown ?? '' if (!report_path) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type} 数据缺少 report_path`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据缺少 report_path`, + }) return } @@ -337,7 +338,7 @@ const handleReportFinish: AIMessageHandler = (requestInfo) => { } const handlePushTask: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'structured' || res.NodeId !== 'system') return // 历史数据和自由对话数据不处理 if (res.IsSync || chatType === 'reAct') return @@ -358,11 +359,13 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { const taskID = generateTaskNodeID(meta.currentTaskPlanID.taskID, info.task.index) const chatDetail = rawData.contents.get(taskID) if (chatDetail && chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${info.task.index}-push_task数据已存在`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${info.task.index}-push_task数据已存在`, + }) return } const chatData: AIChatQSData = { @@ -416,7 +419,7 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { } const handlePopTask: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta, pushLog, sendRequest } = requestInfo + const { res, chatType, store, rawData, meta, sendRequest } = requestInfo if (res.Type !== 'structured' || res.NodeId !== 'system') return // 历史数据和自由对话数据不处理 if (res.IsSync || chatType === 'reAct') return @@ -437,7 +440,13 @@ const handlePopTask: AIMessageHandler = (requestInfo) => { const taskID = generateTaskNodeID(meta.currentTaskPlanID.taskID, info.task.index) const chatDetail = rawData.contents.get(taskID) if (!chatDetail || chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${info.task.index}-pop_task数据不存在`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${info.task.index}-pop_task数据不存在`, + }) return } meta.currentTaskPlanActiveNode.delete(info.task.index) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index ee4e288e6a..f55d12bdc1 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -1,10 +1,10 @@ -import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import type { AIMessageHandler } from '../type' import type { AIAgentGrpcApi, AIOutputEvent } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { genBaseAIChatData, generateTaskNodeDataID, - genErrorLogData, + pushLogToOtherWindow, isToolStderrStream, isToolStdoutStream, } from '../utils' @@ -12,19 +12,8 @@ import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import { convertNodeIdToVerbose } from '../defaultConstant' import { aiAgentLogEmitter } from '../AIAgentLogEmitter' -/** grpc流数据转换成错误信息输出到日志中 */ -const handleErrorGRPCToLog: ( - /** 该条grpc流数据是历史数据 */ - isHistory: AIMessageHandlerParams['res']['IsSync'], - pushLog: AIMessageHandlerParams['pushLog'], - error: ReturnType, -) => void = (isHistory, pushLog, error) => { - if (isHistory) return - pushLog(error) -} - const handleStreamStart: AIMessageHandler = (requestInfo) => { - const { res, chatType, rawData, meta, pushLog } = requestInfo + const { res, chatType, rawData, meta } = requestInfo if (res.Type !== 'stream_start') return if (res.IsSystem || res.IsReason) return @@ -36,18 +25,26 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { const { event_writer_id } = JSON.parse(ipcContent) as { event_writer_id: string } // event_writer_id为空 if (!event_writer_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常: ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常: ${ipcContent}`, + }) return } // tool-xxx-stderr 数据单独初始化逻辑 if (isToolStderrStream(NodeId) && CallToolID) { if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`, + }) return } if (!meta.toolStderrStreamData.has(CallToolID)) { @@ -62,23 +59,24 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { // tool-xxx-stdout 数据单独初始化逻辑 if (isToolStdoutStream(NodeId)) { if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`, + }) return } let toolResult = rawData.contents.get(CallToolID) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `NodeID: ${NodeId} 的stream数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, - ), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `NodeID: ${NodeId} 的stream数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, + }) return } @@ -113,11 +111,13 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { // 数据已存在,流数据输出顺序不对, 视为异常 if (streamData) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `Stream-NodeId: ${NodeId}, EventUUID: (${event_writer_id}), 已存在对应的数据`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `Stream-NodeId: ${NodeId}, EventUUID: (${event_writer_id}), 已存在对应的数据`, + }) return } @@ -145,7 +145,7 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { } const handleStream: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'stream') return // 历史数据-(系统信息|推理信息)数据,不处理 if (res.IsSync && (res.IsSystem || res.IsReason)) return @@ -153,11 +153,13 @@ const handleStream: AIMessageHandler = (requestInfo) => { const { CallToolID, EventUUID, NodeId } = res const content = (Uint8ArrayToString(res.Content) || '') + (Uint8ArrayToString(res.StreamDelta) || '') if (!EventUUID || !NodeId) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据缺失: EventUUID(${EventUUID}) NodeId(${NodeId})`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据缺失: EventUUID(${EventUUID}) NodeId(${NodeId})`, + }) return } @@ -185,7 +187,7 @@ const handleStream: AIMessageHandler = (requestInfo) => { if (res.IsReason) { // 实时数据-推理信息-系统信息输出到日志中 aiAgentLogEmitter.dispatch({ - session: '', + session: requestInfo.sessionId, type: 'stream', Timestamp: res.Timestamp, stream: { @@ -201,11 +203,13 @@ const handleStream: AIMessageHandler = (requestInfo) => { // tool-xxx-stderr 数据单独处理逻辑 if (isToolStderrStream(NodeId)) { if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`, + }) return } const errorResult = meta.toolStderrStreamData.get(CallToolID) @@ -215,32 +219,35 @@ const handleStream: AIMessageHandler = (requestInfo) => { // tool-xxx-stdout 数据单独处理逻辑 if (isToolStdoutStream(NodeId)) { if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`, + }) return } const toolResult = rawData.contents.get(CallToolID) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `Stream-NodeID: ${NodeId} 数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, - ), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `Stream-NodeID: ${NodeId} 数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, + }) return } const toolForStreamData = rawData.contents.get(toolResult.data.stream.EventUUID) if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `Stream-EventUUID: ${toolResult.data.stream.EventUUID} 数据没有对应的初始化`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `Stream-EventUUID: ${toolResult.data.stream.EventUUID} 数据没有对应的初始化`, + }) return } const isRender = !toolForStreamData.data.content @@ -258,11 +265,13 @@ const handleStream: AIMessageHandler = (requestInfo) => { // 数据不存在 if (!streamData || streamData.type !== AIChatQSDataTypeEnum.STREAM) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `Stream-NodeId: ${NodeId}, EventUUID: (${EventUUID}) 数据未初始化`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `Stream-NodeId: ${NodeId}, EventUUID: (${EventUUID}) 数据未初始化`, + }) return } @@ -288,7 +297,7 @@ const handleStream: AIMessageHandler = (requestInfo) => { } const handleStreamFinished: AIMessageHandler = (requestInfo) => { - const { res, store, rawData, meta, pushLog } = requestInfo + const { res, store, rawData, meta } = requestInfo if (res.Type !== 'structured' || res.NodeId !== 'stream-finished') return // 历史数据-(系统信息|推理信息)数据,不处理 if (res.IsSync && (res.IsSystem || res.IsReason)) return @@ -296,20 +305,22 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { let ipcContent = Uint8ArrayToString(res.Content) || '' const { event_writer_id, node_id, is_reason, is_system } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStreamFinished if (!event_writer_id) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `stream-finished数据, event_writer_id 为空`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `stream-finished数据, event_writer_id 为空`, + }) return } // 实时数据-系统信息(不需要结束处理, 由下一个自动顶替) if (is_system) return + // 实时数据-推理信息-系统信息输出到日志中 if (is_reason) { - // 实时数据-推理信息-系统信息输出到日志中 aiAgentLogEmitter.dispatch({ - session: '', + session: requestInfo.sessionId, type: 'stream', Timestamp: res.Timestamp, stream: { @@ -326,21 +337,25 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { // tool-xxx-stderr 数据单独结束逻辑 if (isToolStderrStream(node_id)) { if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `数据(NodeId: ${node_id}), CallToolID 为空`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `数据(NodeId: ${node_id}), CallToolID 为空`, + }) return } const toolErrorResult = meta.toolStderrStreamData.get(CallToolID) if (!toolErrorResult) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `NodeId(${node_id})&CallToolID(${CallToolID}) 数据没有初始化`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `NodeId(${node_id})&CallToolID(${CallToolID}) 数据没有初始化`, + }) return } @@ -360,11 +375,13 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { // tool-xxx-stdout 数据单独结束逻辑 if (isToolStdoutStream(node_id)) { if (!CallToolID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `(NodeId: ${node_id})的数据, CallToolID 为空`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `(NodeId: ${node_id})的数据, CallToolID 为空`, + }) return } @@ -398,7 +415,7 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { } const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta, pushLog } = requestInfo + const { res, chatType, store, rawData, request, meta } = requestInfo if (res.Type !== 'reference_material') return const ipcContent = Uint8ArrayToString(res.Content) || '' diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts index b7ea85a843..4dd82aeb3a 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -1,30 +1,25 @@ -import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import type { AIMessageHandler } from '../type' import type { AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { genBaseAIChatData, generateTaskNodeDataID, genErrorLogData } from '../utils' +import { genBaseAIChatData, generateTaskNodeDataID, pushLogToOtherWindow } from '../utils' import { AIChatQSDataTypeEnum, type AIToolResult } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { DefaultAIToolResult, DefaultToolResultSummary } from '../defaultConstant' -/** grpc流数据转换成错误信息输出到日志中 */ -const handleErrorGRPCToLog: ( - /** 该条grpc流数据是历史数据 */ - isHistory: AIMessageHandlerParams['res']['IsSync'], - pushLog: AIMessageHandlerParams['pushLog'], - error: ReturnType, -) => void = (isHistory, pushLog, error) => { - if (isHistory) return - pushLog(error) -} - const handleToolCallStart: AIMessageHandler = (requestInfo) => { - const { res, chatType, rawData, meta, pushLog } = requestInfo + const { res, chatType, rawData, meta } = requestInfo if (res.Type !== 'tool_call_start') return const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, tool, start_time, start_time_ms } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常, ${ipcContent}`, + }) return } @@ -53,26 +48,31 @@ const handleToolCallStart: AIMessageHandler = (requestInfo) => { } const handleToolCallParam: AIMessageHandler = (requestInfo) => { - const { res, store, rawData, pushLog } = requestInfo + const { res, store, rawData } = requestInfo if (res.Type !== 'tool_call_param') return const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, params } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallParams if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常, ${ipcContent}`, + }) return } const toolResult = rawData.contents.get(call_tool_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + }) return } @@ -81,38 +81,45 @@ const handleToolCallParam: AIMessageHandler = (requestInfo) => { } const handleToolCallWatcher: AIMessageHandler = (requestInfo) => { - const { res, store, rawData, pushLog } = requestInfo + const { res, store, rawData } = requestInfo if (res.Type !== 'tool_call_watcher') return const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, id, selectors } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallWatcher if (!call_tool_id || !id || !selectors || !selectors?.length) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常, ${ipcContent}`, + }) return } // 先获取工具结果数据,从里面拿到stream的EventUUID const toolResult = rawData.contents.get(call_tool_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + }) return } // 通过上面获取到的EventUUID,获取stream数据 const toolForStreamData = rawData.contents.get(toolResult.data.stream.EventUUID) if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `EventUUID: ${toolResult.data.stream.EventUUID} 的stream数据没有对应的初始化`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `EventUUID: ${toolResult.data.stream.EventUUID} 的stream数据没有对应的初始化`, + }) return } // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 @@ -130,34 +137,41 @@ const handleToolCallWatcher: AIMessageHandler = (requestInfo) => { } const handleToolCallLogDir: AIMessageHandler = (requestInfo) => { - const { res, store, rawData, pushLog } = requestInfo + const { res, store, rawData } = requestInfo if (res.Type !== 'tool_call_log_dir') return const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, dir_path } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallDirPath if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常, ${ipcContent}`, + }) return } const toolResult = rawData.contents.get(call_tool_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + }) return } if (toolResult.data.tool.dirPath) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData(res.Timestamp, `${res.Type}数据(call_tool_id:${call_tool_id}), dir_path已存在,不能重复设置`), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(call_tool_id:${call_tool_id}), dir_path已存在,不能重复设置`, + }) return } @@ -167,7 +181,7 @@ const handleToolCallLogDir: AIMessageHandler = (requestInfo) => { } const handleToolCallResult: AIMessageHandler = (requestInfo) => { - const { res, store, rawData, meta, pushLog } = requestInfo + const { res, store, rawData, meta } = requestInfo if (!['tool_call_user_cancel', 'tool_call_done', 'tool_call_error'].includes(res.Type)) return const status = { tool_call_user_cancel: 'user_cancelled', tool_call_done: 'success', tool_call_error: 'failed' }[res.Type] || @@ -178,20 +192,25 @@ const handleToolCallResult: AIMessageHandler = (requestInfo) => { const { call_tool_id, ...rest } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常, ${ipcContent}`, + }) return } const toolResult = rawData.contents.get(call_tool_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + }) return } @@ -220,27 +239,32 @@ const handleToolCallResult: AIMessageHandler = (requestInfo) => { } const handleToolCallSummary: AIMessageHandler = (requestInfo) => { - const { res, store, rawData, meta, pushLog } = requestInfo + const { res, store, rawData, meta } = requestInfo if (res.Type !== 'tool_call_summary') return const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, summary } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall if (!call_tool_id) { - handleErrorGRPCToLog(res.IsSync, pushLog, genErrorLogData(res.Timestamp, `${res.Type}数据异常, ${ipcContent}`)) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据异常, ${ipcContent}`, + }) return } const toolResult = rawData.contents.get(call_tool_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - handleErrorGRPCToLog( - res.IsSync, - pushLog, - genErrorLogData( - res.Timestamp, - `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, - ), - ) + pushLogToOtherWindow({ + sessionId: requestInfo.sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + level: 'error', + message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + }) return } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts index bdcb1554b9..40d3ff2986 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts @@ -333,6 +333,7 @@ export interface UseAIChatLogEvents { // #region AI-Agent相关grpc流数据处理逻辑 export interface AIMessageHandlerParams { + sessionId: string /** grpc流原始数据 */ res: AIOutputEvent chatType: ChatListRenderType @@ -340,8 +341,6 @@ export interface AIMessageHandlerParams { rawData: AIAgentChatData request: AIStartParams meta: AIAgentChatMetaData - /** 将数据推送到日志集合中 */ - pushLog: (log: AIChatLogData) => void sendRequest: (request: AIInputEvent) => void } export type AIMessageHandler = (params: AIMessageHandlerParams) => void diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts index a183c7d552..47198ba4d2 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts @@ -1,10 +1,4 @@ -import type { - AIChatLogData, - handleSendFunc, - UseCasualChatEvents, - UseCasualChatParams, - UseCasualChatState, -} from './type' +import type { handleSendFunc, UseCasualChatEvents, UseCasualChatParams, UseCasualChatState } from './type' import type { AIChatQSData, ReActChatRenderItem } from './aiRender' import type { AIOutputEvent } from './grpcApi' import { useRef, useState } from 'react' @@ -16,12 +10,7 @@ import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' function useCasualChat(params: UseCasualChatParams): [UseCasualChatState, UseCasualChatEvents] function useCasualChat(params: UseCasualChatParams) { - const { pushLog, getChatDataStore, getRequest, getCurrentCasualTaskID, onReview, onReviewRelease, onSubTaskID } = - params || {} - - const handlePushLog = useMemoizedFn((logInfo: AIChatLogData) => { - pushLog && pushLog(logInfo) - }) + const { getChatDataStore, getRequest, onReviewRelease } = params || {} const [elements, setElements, getElements] = useGetSetState([]) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts index 75fb8bd1b4..b3a4ff3b70 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts @@ -27,7 +27,6 @@ import { Uint8ArrayToString } from '@/utils/str' import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' import useCasualChat from './useCasualChat' import useTaskChat from './useTaskChat' -import { handleGrpcDataPushLog } from './utils' import { AIInputEventSyncTypeEnum } from './grpcApi' import useAIChatLog from './useAIChatLog' import cloneDeep from 'lodash/cloneDeep' @@ -264,7 +263,6 @@ function useChatIPC(params?: UseChatIPCParams) { }) const [casualChat, casualChatEvent] = useCasualChat({ - pushLog: logEvents.pushLog, getChatDataStore, getRequest: fetchAIRequest, getCurrentCasualTaskID: fetchCurrentCasualTaskID, @@ -296,7 +294,6 @@ function useChatIPC(params?: UseChatIPCParams) { }) const [taskChat, taskChatEvent] = useTaskChat({ - pushLog: logEvents.pushLog, getChatDataStore, getRequest: fetchAIRequest, getCurrentTaskPlanID: fetchCurrentTaskPlanID, @@ -376,10 +373,7 @@ function useChatIPC(params?: UseChatIPCParams) { Content: '已清空所有任务队列数据', }) } catch (error) { - handleGrpcDataPushLog({ - info: res, - pushLog: logEvents.pushLog, - }) + // push log } }) // #endregion @@ -849,7 +843,7 @@ function useChatIPC(params?: UseChatIPCParams) { return } catch (error) { - handleGrpcDataPushLog({ info: res, pushLog: logEvents.pushLog }) + // push log } }) ipcRenderer.on(`${token}-end`, (e, res: any) => { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts index c9312e28f4..faf7071583 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts @@ -1,5 +1,4 @@ import type { - AIChatLogData, CurrentExecTaskTree, handleSendFunc, UseTaskChatEvents, @@ -12,7 +11,7 @@ import { useRef, useState } from 'react' import { useCreation, useMemoizedFn } from 'ahooks' import cloneDeep from 'lodash/cloneDeep' import { DefaultCurrentExecTaskTree } from './defaultConstant' -import { genBaseAIChatData, genExecTasks, handleGrpcDataPushLog } from './utils' +import { genBaseAIChatData, genExecTasks } from './utils' import { yakitNotify } from '@/utils/notification' import { AITaskStatus } from './grpcApi' import { AIChatQSDataTypeEnum } from './aiRender' @@ -21,20 +20,8 @@ import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' function useTaskChat(params: UseTaskChatParams): [UseTaskChatState, UseTaskChatEvents] function useTaskChat(params: UseTaskChatParams) { - const { - pushLog, - getChatDataStore, - getRequest, - getCurrentTaskPlanID, - onReview, - onReviewExtra, - onReviewRelease, - sendRequest, - } = params || {} - - const handlePushLog = useMemoizedFn((logInfo: AIChatLogData) => { - pushLog && pushLog(logInfo) - }) + const { getChatDataStore, getRequest, getCurrentTaskPlanID, onReview, onReviewExtra, onReviewRelease, sendRequest } = + params || {} const [elements, setElements, getElements] = useGetSetState([]) @@ -71,10 +58,7 @@ function useTaskChat(params: UseTaskChatParams) { const taskKey = `${taskId}-${ownTaskId}` const existing = getContentMap(taskKey) if (existing && existing.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - handleGrpcDataPushLog({ - info: res, - pushLog: handlePushLog, - }) + // push log return } const chatData: AIChatQSData = @@ -132,10 +116,7 @@ function useTaskChat(params: UseTaskChatParams) { }) } } catch { - handleGrpcDataPushLog({ - info: res, - pushLog: handlePushLog, - }) + // push log } }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts index aa73bb5907..46672a14da 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts @@ -10,12 +10,11 @@ import type { TodoListCardData, ChatListRenderType, } from './aiRender' -import type { AIChatLogToInfo, AIChatLogData } from './type' import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' import { AIToDoListStatusEnum, generateTaskChatExecution } from '@/pages/ai-agent/defaultConstant' -import { Uint8ArrayToString } from '@/utils/str' import { v4 as uuidv4 } from 'uuid' import { JSONParseLog } from '@/utils/tool' +import { aiAgentLogEmitter } from './AIAgentLogEmitter' /** 生成任务节点的唯一ID */ export const generateTaskNodeID = (planID: string, taskID: string) => { @@ -49,35 +48,24 @@ export const genBaseAIChatData = (info: AIOutputEvent) => { } } -/** 生成一个异常日志数据的对象 */ -export const genErrorLogData = ( - Timestamp: AIChatLogToInfo['Timestamp'], - message: AIChatLogToInfo['data']['message'], -): AIChatLogToInfo => { - return { - type: 'log', - Timestamp, - data: { level: 'error', message: message }, - } -} +/** Agent 往日志窗口推送日志数据 */ +export const pushLogToOtherWindow = (params: { + sessionId: string + /** 是否是历史数据 */ + isHistory: AIOutputEvent['IsSync'] + Timestamp: AIOutputEvent['Timestamp'] + level: string + message: string +}) => { + const { sessionId, isHistory, Timestamp, level, message } = params + if (isHistory) return -/** 将接口数据(AIOutputEvent)转换为日志数据(AIAgentGrpcApi.Log), 并push到日志队列中 */ -export const handleGrpcDataPushLog = (params: { info: AIOutputEvent; pushLog: (log: AIChatLogData) => void }) => { - try { - const { info, pushLog } = params - // 这类类型的数据从日志数据中屏蔽掉,后续的stream类型逻辑会使用到 - if (info.Type === 'stream_start') return - let ipcContent = Uint8ArrayToString(info.Content) || '' - const logInfo: AIChatLogData = { - type: 'log', - Timestamp: info.Timestamp, - data: { - level: `${info.Type}-${info.NodeId}`, - message: ipcContent, - }, - } - pushLog(logInfo) - } catch (error) {} + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: Timestamp, + log: { level: level, message: message }, + }) } // #region 处理任务规划-任务树相关方法 diff --git a/app/renderer/src/main/tsconfig.json b/app/renderer/src/main/tsconfig.json index b0388ced05..a1bd72f6cd 100644 --- a/app/renderer/src/main/tsconfig.json +++ b/app/renderer/src/main/tsconfig.json @@ -3,7 +3,7 @@ "target": "es2017", "lib": ["dom", "dom.iterable", "esnext", "ES2015.Promise", "ES2021.String"], "allowJs": true, - "skipLibCheck": true, + "skipLibCheck": false, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, From 8b87251422c3fd09473702102c4e845f764d9051 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Mon, 29 Jun 2026 14:48:58 +0800 Subject: [PATCH 015/101] =?UTF-8?q?=E9=A2=84=E5=85=88=E8=A7=A3=E5=86=B3?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/type/aiChat.d.ts | 2 -- .../hooks/ChatMultiSessionController.ts | 1 - .../hooks/grpcStreamHandler/aiOther.ts | 11 ++++------- .../hooks/grpcStreamHandler/aiReview.ts | 18 +++++++++++++++++- .../hooks/grpcStreamHandler/yakExecResult.ts | 1 + 5 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 606c456f9b..63733e041d 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -225,8 +225,6 @@ export interface AIAgentChatMetaData { onSessionStartSuccess?: (sessionId: string) => void /** 任务规划的实时记忆列表 */ taskMemoryList: AIAgentGrpcApi.MemoryEntryList - /** 当前专注场景ID */ - focusOfTaskID: string /** 通知消息的消失定时器 */ notifyMessageTimer: NodeJS.Timeout | null /** 当前自由对话问题的re_act_task_id */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 74db10eab2..58f9df770b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -102,7 +102,6 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { createChatQuestion: undefined, casualMemoryList: cloneDeep(DefaultMemoryList), taskMemoryList: cloneDeep(DefaultMemoryList), - focusOfTaskID: '', notifyMessageTimer: null, currentCasualTaskID: '', currentTaskPlanID: undefined, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index 01af529437..ea79164d72 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -4,7 +4,7 @@ import { Uint8ArrayToString } from '@/utils/str' import { genBaseAIChatData, generateTaskNodeID, genExecTasks, pushLogToOtherWindow } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' -import { DefaultCurrentExecTaskTree, DefaultPlanLoadingStatus, DefaultTodoListCardData } from '../defaultConstant' +import { DefaultCurrentExecTaskTree, DefaultPlanItemDetailsData, DefaultPlanLoadingStatus } from '../defaultConstant' import has from 'lodash/has' const handleHttpFuzzRequestChange: AIMessageHandler = (request) => { @@ -236,9 +236,8 @@ const handleReactTaskDequeue: AIMessageHandler = (request) => { // 实时数据里,记录用户问题的状态和专注模式信息 if (!res.IsSync) { sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) - meta.currentCasualTaskID = data.react_task_id + meta.currentCasualTaskID = res.TaskId || data.react_task_id rawData.casualChat.planDetails = cloneDeep(DefaultPlanItemDetailsData) - meta.focusOfTaskID = data.focus_mode ? data.react_task_id : '' store.getState().updateState({ focusMode: data.focus_mode ? data.focus_mode : '', casualLoading: true }) } @@ -356,14 +355,11 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { // 问题任务完成或者者被中止后,重置当前问题任务id meta.currentCasualTaskID = '' store.getState().updateState({ cancelCasualLoading: false }) - } - if (meta.focusOfTaskID === react_task_id) { // 取消专注模式 - meta.focusOfTaskID = '' store.getState().updateState({ focusMode: '' }) } store.getState().updateState({ casualLoading: false }) - rawData.casualChat.todoList = cloneDeep(DefaultTodoListCardData) + rawData.casualChat.planDetails = cloneDeep(DefaultPlanItemDetailsData) store.getState().updateCasualTodoList() if (meta.currentTaskPlanID?.taskID === react_task_id) { // 该问题触发了任务规划, 所以需要将任务规划状态也调整 @@ -373,6 +369,7 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { } } +// TODO: ? const handleStatus: AIMessageHandler = (request) => { const { res, chatType, store } = request if (res.Type !== 'structured' || res.NodeId !== 'status') return diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index 541b1ae422..e58f51cc1d 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -195,6 +195,14 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { // rawData.contents.delete(chatData.id) store.getState().updateState({ currentPlanReviewToken: chatData.id }) } else if (chatType === 'reAct') { + // 实时-自由对话里, 自动请求执行 + // const info: AIInputEvent = { + // IsInteractiveMessage: true, + // InteractiveId: chatData.id, + // InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), + // } + // sendRequest(info) + // rawData.contents.delete(chatData.id) store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ chatType: chatType, @@ -239,7 +247,7 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { isExist: (key) => rawData.contents.has(key), }), } - // 历史数据-task-review数据不进行展示 + // 历史数据-tool-review数据不进行展示 if (res.IsSync) return // 实时数据处理逻辑 @@ -264,6 +272,14 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { // rawData.contents.delete(chatData.id) store.getState().updateState({ currentPlanReviewToken: chatData.id }) } else if (chatType === 'reAct') { + // 实时-自由对话里, 自动请求执行 + // const info: AIInputEvent = { + // IsInteractiveMessage: true, + // InteractiveId: chatData.id, + // InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), + // } + // sendRequest(info) + // rawData.contents.delete(chatData.id) store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ chatType: chatType, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts index e4c2892d5d..9b8d673352 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts @@ -4,6 +4,7 @@ import { Uint8ArrayToString } from '@/utils/str' import { checkStreamValidity, convertCardInfo } from '@/hook/useHoldGRPCStream/useHoldGRPCStream' import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' +// TODO: ? const handleStatus: AIMessageHandler = (request) => { const { res, chatType, store, meta } = request if (res.Type !== 'structured' || res.NodeId !== 'status') return From e0f974c14882e21fc21fffa227f9ac1a489ad029 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Mon, 29 Jun 2026 17:06:17 +0800 Subject: [PATCH 016/101] =?UTF-8?q?refactor:=20=E9=87=8D=E6=96=B0=E6=95=B4?= =?UTF-8?q?=E5=90=88=E6=8E=A8=E9=80=81=E6=97=A5=E5=BF=97=E7=9A=84=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/ChatMultiSessionController.ts | 11 +- .../hooks/grpcStreamHandler/aiOther.ts | 39 +----- .../hooks/grpcStreamHandler/aiReview.ts | 101 ++------------- .../hooks/grpcStreamHandler/aiSingleItem.ts | 40 +----- .../hooks/grpcStreamHandler/aiStream.ts | 116 +++--------------- .../hooks/grpcStreamHandler/aiToolResult.ts | 85 +++---------- .../main/src/pages/ai-re-act/hooks/type.ts | 2 +- 7 files changed, 64 insertions(+), 330 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 58f9df770b..fd90eb6dbc 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -6,7 +6,7 @@ import { AIAgentSettingDefault, AIModelTypeEnum } from '@/pages/ai-agent/default import cloneDeep from 'lodash/cloneDeep' import { DefaultMemoryList, DefaultPlanItemDetailsData } from './defaultConstant' import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHandlers' -import { genExecTasks } from './utils' +import { genExecTasks, pushLogToOtherWindow } from './utils' import type { AIChatIPCStartParams, AIChatSendParams } from './type' import { yakitNotify } from '@/utils/notification' import { type AIChatQSData, AIChatQSDataTypeEnum } from './aiRender' @@ -398,7 +398,6 @@ export class ChatMultiSessionController { const handleFunc = grpcAIMessageHandlers[funcKey || ''] if (handleFunc) { handleFunc({ - sessionId, res, chatType: meta.currentTaskPlanID?.coordinatorId === res.CoordinatorId ? 'task' : 'reAct', store, @@ -406,6 +405,14 @@ export class ChatMultiSessionController { request, meta, sendRequest: (request) => this.requestMessage(sessionId, request), + pushLog: (log) => { + pushLogToOtherWindow({ + sessionId: sessionId, + isHistory: res.IsSync, + Timestamp: res.Timestamp, + ...log, + }) + }, }) return } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index ea79164d72..e59b89eb26 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -1,7 +1,7 @@ import type { AIMessageHandler } from '../type' import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi, type AIOutputEvent } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { genBaseAIChatData, generateTaskNodeID, genExecTasks, pushLogToOtherWindow } from '../utils' +import { genBaseAIChatData, generateTaskNodeID, genExecTasks } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { DefaultCurrentExecTaskTree, DefaultPlanItemDetailsData, DefaultPlanLoadingStatus } from '../defaultConstant' @@ -37,13 +37,7 @@ const handleSessionTitle: AIMessageHandler = (request) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const nameInfo = JSON.parse(ipcContent) as { title: string } if (!nameInfo || !nameInfo.title) { - pushLogToOtherWindow({ - sessionId: request.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.NodeId}数据错误: ${ipcContent}`, - }) + request.pushLog({ level: 'error', message: `${res.NodeId}数据错误: ${ipcContent}` }) return } rawData.sessionTitle = nameInfo.title @@ -61,13 +55,7 @@ const handleStartPlanAndExecution: AIMessageHandler = (request) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution if (!startInfo.coordinator_id) { - pushLogToOtherWindow({ - sessionId: request.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据, coordinator_id 为空`, - }) + request.pushLog({ level: 'error', message: `${res.Type}数据, coordinator_id 为空` }) return } meta.currentTaskPlanID = { @@ -114,13 +102,7 @@ const handleEndPlanAndExecution: AIMessageHandler = (request) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution if (!startInfo.coordinator_id) { - pushLogToOtherWindow({ - sessionId: request.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据, coordinator_id 为空`, - }) + request.pushLog({ level: 'error', message: `${res.Type}数据, coordinator_id 为空` }) return } if (startInfo.coordinator_id === meta.currentTaskPlanID?.coordinatorId) { @@ -402,13 +384,7 @@ const handleTrafficCount: AIMessageHandler = (request) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.HTTPTrafficNotice & AIAgentGrpcApi.RiskTrafficNotice if (!data.runtime_id) { - pushLogToOtherWindow({ - sessionId: request.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + request.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } @@ -426,10 +402,7 @@ const handleTrafficCount: AIMessageHandler = (request) => { // 更新工具执行结果卡片里的流量和风险数量 const toolResult = rawData.contents.get(data.runtime_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || toolResult.chatType !== chatType) { - pushLogToOtherWindow({ - sessionId: request.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + request.pushLog({ level: 'error', message: `${res.Type}数据(call_tool_id:${data.runtime_id})工具结果卡片数据不存在`, }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index e58f51cc1d..650f17d575 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -1,13 +1,7 @@ import type { AIMessageHandler } from '../type' import type { AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { - genBaseAIChatData, - generateTaskNodeDataID, - pushLogToOtherWindow, - genExecTasks, - isAutoExecuteReviewContinue, -} from '../utils' +import { genBaseAIChatData, generateTaskNodeDataID, genExecTasks, isAutoExecuteReviewContinue } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { AIReviewJudgeLevelMap } from '../defaultConstant' @@ -21,13 +15,7 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequire if (!data?.id || !data?.plans || !data?.plans?.root_task || !data?.selectors || !data?.selectors?.length) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } @@ -106,25 +94,13 @@ const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { !data?.keywords?.length || (meta.currentPlanReviewExtraId && meta.currentPlanReviewExtraId !== data.plans_id) ) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } const reviewDetail = rawData.contents.get(store.getState().currentPlanReviewToken) if (!reviewDetail || reviewDetail.type !== AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: 未找到对应plan_review_require数据`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: 未找到对应plan_review_require数据` }) return } @@ -147,13 +123,7 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.TaskReviewRequire if (!data?.id || !data?.selectors || !data?.selectors?.length) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } @@ -224,13 +194,7 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ToolUseReviewRequire if (!data?.id || !data?.selectors || !data?.selectors?.length) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } @@ -301,13 +265,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewRequire if (!data?.id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } @@ -375,13 +333,7 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ExecForgeReview if (!data?.id || !data?.selectors || !data?.selectors?.length) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } @@ -451,24 +403,12 @@ const handleAIReviewJudgement: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const score = JSON.parse(ipcContent) as AIAgentGrpcApi.AIReviewJudgement if (!score?.interactive_id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } const reviewDetail = rawData.contents.get(score.interactive_id) if (!reviewDetail || reviewDetail.id !== score.interactive_id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: 没有对应的review数据`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: 没有对应的review数据` }) return } @@ -488,10 +428,7 @@ const handleAIReviewJudgement: AIMessageHandler = (requestInfo) => { break default: - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常(interactive_id:${score?.interactive_id || '-'})未找到对应review`, }) @@ -506,13 +443,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ReviewRelease if (!data?.id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } @@ -553,13 +484,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { } if (!reviewDetail) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据(id:${data?.id || '-'})没有对应的review数据`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(id:${data?.id || '-'})没有对应的review数据` }) return } switch (reviewDetail.type) { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index 28374c1c28..4eb9a505cb 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -1,13 +1,7 @@ import type { AIMessageHandler } from '../type' import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi, type AIOutputEvent } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { - genBaseAIChatData, - generateTaskNodeDataID, - generateTaskNodeID, - pushLogToOtherWindow, - isValidTaskIndex, -} from '../utils' +import { genBaseAIChatData, generateTaskNodeDataID, generateTaskNodeID, isValidTaskIndex } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum, type ReportFinishCardData } from '../aiRender' import { convertNodeIdToVerbose } from '../defaultConstant' @@ -230,13 +224,7 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { const payload = JSON.parse(ipcContent) as AIAgentGrpcApi.GetHttpFlowFuzzStatus const { fuzz_id, runtime_id, reason, status } = payload if (!fuzz_id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } @@ -298,13 +286,7 @@ const handleReportFinish: AIMessageHandler = (requestInfo) => { let content = parsed?.summary_markdown ?? '' if (!report_path) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据缺少 report_path`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据缺少 report_path` }) return } @@ -359,13 +341,7 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { const taskID = generateTaskNodeID(meta.currentTaskPlanID.taskID, info.task.index) const chatDetail = rawData.contents.get(taskID) if (chatDetail && chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${info.task.index}-push_task数据已存在`, - }) + requestInfo.pushLog({ level: 'error', message: `${info.task.index}-push_task数据已存在` }) return } const chatData: AIChatQSData = { @@ -440,13 +416,7 @@ const handlePopTask: AIMessageHandler = (requestInfo) => { const taskID = generateTaskNodeID(meta.currentTaskPlanID.taskID, info.task.index) const chatDetail = rawData.contents.get(taskID) if (!chatDetail || chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${info.task.index}-pop_task数据不存在`, - }) + requestInfo.pushLog({ level: 'error', message: `${info.task.index}-pop_task数据不存在` }) return } meta.currentTaskPlanActiveNode.delete(info.task.index) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index f55d12bdc1..f2894f7fde 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -1,13 +1,7 @@ import type { AIMessageHandler } from '../type' import type { AIAgentGrpcApi, AIOutputEvent } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { - genBaseAIChatData, - generateTaskNodeDataID, - pushLogToOtherWindow, - isToolStderrStream, - isToolStdoutStream, -} from '../utils' +import { genBaseAIChatData, generateTaskNodeDataID, isToolStderrStream, isToolStdoutStream } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import { convertNodeIdToVerbose } from '../defaultConstant' import { aiAgentLogEmitter } from '../AIAgentLogEmitter' @@ -25,26 +19,14 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { const { event_writer_id } = JSON.parse(ipcContent) as { event_writer_id: string } // event_writer_id为空 if (!event_writer_id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常: ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) return } // tool-xxx-stderr 数据单独初始化逻辑 if (isToolStderrStream(NodeId) && CallToolID) { if (!CallToolID) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空` }) return } if (!meta.toolStderrStreamData.has(CallToolID)) { @@ -59,21 +41,12 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { // tool-xxx-stdout 数据单独初始化逻辑 if (isToolStdoutStream(NodeId)) { if (!CallToolID) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空` }) return } let toolResult = rawData.contents.get(CallToolID) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `NodeID: ${NodeId} 的stream数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, }) @@ -111,10 +84,7 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { // 数据已存在,流数据输出顺序不对, 视为异常 if (streamData) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `Stream-NodeId: ${NodeId}, EventUUID: (${event_writer_id}), 已存在对应的数据`, }) @@ -153,13 +123,7 @@ const handleStream: AIMessageHandler = (requestInfo) => { const { CallToolID, EventUUID, NodeId } = res const content = (Uint8ArrayToString(res.Content) || '') + (Uint8ArrayToString(res.StreamDelta) || '') if (!EventUUID || !NodeId) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据缺失: EventUUID(${EventUUID}) NodeId(${NodeId})`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据缺失: EventUUID(${EventUUID}) NodeId(${NodeId})` }) return } @@ -203,13 +167,7 @@ const handleStream: AIMessageHandler = (requestInfo) => { // tool-xxx-stderr 数据单独处理逻辑 if (isToolStderrStream(NodeId)) { if (!CallToolID) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空` }) return } const errorResult = meta.toolStderrStreamData.get(CallToolID) @@ -219,21 +177,12 @@ const handleStream: AIMessageHandler = (requestInfo) => { // tool-xxx-stdout 数据单独处理逻辑 if (isToolStdoutStream(NodeId)) { if (!CallToolID) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(NodeId: ${NodeId}), CallToolID 为空` }) return } const toolResult = rawData.contents.get(CallToolID) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `Stream-NodeID: ${NodeId} 数据没有对应的工具结果(CallToolID: ${CallToolID})初始化`, }) @@ -241,10 +190,7 @@ const handleStream: AIMessageHandler = (requestInfo) => { } const toolForStreamData = rawData.contents.get(toolResult.data.stream.EventUUID) if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `Stream-EventUUID: ${toolResult.data.stream.EventUUID} 数据没有对应的初始化`, }) @@ -265,13 +211,7 @@ const handleStream: AIMessageHandler = (requestInfo) => { // 数据不存在 if (!streamData || streamData.type !== AIChatQSDataTypeEnum.STREAM) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `Stream-NodeId: ${NodeId}, EventUUID: (${EventUUID}) 数据未初始化`, - }) + requestInfo.pushLog({ level: 'error', message: `Stream-NodeId: ${NodeId}, EventUUID: (${EventUUID}) 数据未初始化` }) return } @@ -305,13 +245,7 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { let ipcContent = Uint8ArrayToString(res.Content) || '' const { event_writer_id, node_id, is_reason, is_system } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStreamFinished if (!event_writer_id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `stream-finished数据, event_writer_id 为空`, - }) + requestInfo.pushLog({ level: 'error', message: `stream-finished数据, event_writer_id 为空` }) return } @@ -337,25 +271,13 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { // tool-xxx-stderr 数据单独结束逻辑 if (isToolStderrStream(node_id)) { if (!CallToolID) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `数据(NodeId: ${node_id}), CallToolID 为空`, - }) + requestInfo.pushLog({ level: 'error', message: `数据(NodeId: ${node_id}), CallToolID 为空` }) return } const toolErrorResult = meta.toolStderrStreamData.get(CallToolID) if (!toolErrorResult) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `NodeId(${node_id})&CallToolID(${CallToolID}) 数据没有初始化`, - }) + requestInfo.pushLog({ level: 'error', message: `NodeId(${node_id})&CallToolID(${CallToolID}) 数据没有初始化` }) return } @@ -375,13 +297,7 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { // tool-xxx-stdout 数据单独结束逻辑 if (isToolStdoutStream(node_id)) { if (!CallToolID) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `(NodeId: ${node_id})的数据, CallToolID 为空`, - }) + requestInfo.pushLog({ level: 'error', message: `(NodeId: ${node_id})的数据, CallToolID 为空` }) return } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts index 4dd82aeb3a..c5711088ec 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -1,7 +1,7 @@ import type { AIMessageHandler } from '../type' import type { AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { genBaseAIChatData, generateTaskNodeDataID, pushLogToOtherWindow } from '../utils' +import { genBaseAIChatData, generateTaskNodeDataID } from '../utils' import { AIChatQSDataTypeEnum, type AIToolResult } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { DefaultAIToolResult, DefaultToolResultSummary } from '../defaultConstant' @@ -13,13 +13,7 @@ const handleToolCallStart: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, tool, start_time, start_time_ms } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall if (!call_tool_id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常, ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) return } @@ -54,22 +48,13 @@ const handleToolCallParam: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, params } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallParams if (!call_tool_id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常, ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) return } const toolResult = rawData.contents.get(call_tool_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, }) @@ -88,23 +73,14 @@ const handleToolCallWatcher: AIMessageHandler = (requestInfo) => { const { call_tool_id, id, selectors } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallWatcher if (!call_tool_id || !id || !selectors || !selectors?.length) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常, ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) return } // 先获取工具结果数据,从里面拿到stream的EventUUID const toolResult = rawData.contents.get(call_tool_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT || !toolResult.data.stream.EventUUID) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, }) @@ -113,10 +89,7 @@ const handleToolCallWatcher: AIMessageHandler = (requestInfo) => { // 通过上面获取到的EventUUID,获取stream数据 const toolForStreamData = rawData.contents.get(toolResult.data.stream.EventUUID) if (!toolForStreamData || toolForStreamData.type !== AIChatQSDataTypeEnum.STREAM) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `EventUUID: ${toolResult.data.stream.EventUUID} 的stream数据没有对应的初始化`, }) @@ -143,32 +116,20 @@ const handleToolCallLogDir: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, dir_path } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallDirPath if (!call_tool_id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常, ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) return } const toolResult = rawData.contents.get(call_tool_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, }) return } if (toolResult.data.tool.dirPath) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(call_tool_id:${call_tool_id}), dir_path已存在,不能重复设置`, }) @@ -192,22 +153,13 @@ const handleToolCallResult: AIMessageHandler = (requestInfo) => { const { call_tool_id, ...rest } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall if (!call_tool_id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常, ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) return } const toolResult = rawData.contents.get(call_tool_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, }) @@ -246,22 +198,13 @@ const handleToolCallSummary: AIMessageHandler = (requestInfo) => { const { call_tool_id, summary } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall if (!call_tool_id) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - level: 'error', - message: `${res.Type}数据异常, ${ipcContent}`, - }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) return } const toolResult = rawData.contents.get(call_tool_id) if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { - pushLogToOtherWindow({ - sessionId: requestInfo.sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts index 40d3ff2986..5ee2b2a348 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts @@ -333,7 +333,6 @@ export interface UseAIChatLogEvents { // #region AI-Agent相关grpc流数据处理逻辑 export interface AIMessageHandlerParams { - sessionId: string /** grpc流原始数据 */ res: AIOutputEvent chatType: ChatListRenderType @@ -342,6 +341,7 @@ export interface AIMessageHandlerParams { request: AIStartParams meta: AIAgentChatMetaData sendRequest: (request: AIInputEvent) => void + pushLog: (log: AIAgentGrpcApi.Log) => void } export type AIMessageHandler = (params: AIMessageHandlerParams) => void // #endregion From c031f84306fbd65a8b8f8beb17b7010f3ab013ae Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Fri, 3 Jul 2026 17:51:43 +0800 Subject: [PATCH 017/101] =?UTF-8?q?doc:=E8=A1=A5=E5=85=85=E6=B3=A8?= =?UTF-8?q?=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/src/pages/ai-re-act/hooks/aiRender.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 0a6b8043be..0abad99df2 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -495,7 +495,7 @@ export interface ChatStoreState { * aiChat.d.ts AIAgentChatData yaklangCodeChange */ yaklangCodeChangeUpdate: number - /** aiChat.d.ts AIAgentChatMetaData syncID */ + /** aiChat.d.ts AIAgentChatMetaData syncIDMap */ syncIDUpdate: number /** 接口运行过程中的数据文件夹合集 */ @@ -531,9 +531,15 @@ export interface ChatStoreState { /** 任务规划的loading状态信息 */ taskStatus: PlanLoadingStatus - /** 自由对话的当前review(未操作) */ + /** + * 自由对话的当前review(未操作) + * 通过token去rawData中contents里取数据,token是唯一的 + */ currentCasualReview: string[] - /** 任务规划当前显示的review数据 */ + /** + * 任务规划当前显示的review数据 + * 通过token去rawData中contents里取数据,token是唯一的 + */ currentPlanReviewToken: string /** * 当前review是plan时,异步数据的更新版本 From e63346276659348b6ab66e3c5076abe875217a3b Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Fri, 3 Jul 2026 17:36:51 +0800 Subject: [PATCH 018/101] =?UTF-8?q?feat:=201.=20=E5=B0=86=E5=86=85?= =?UTF-8?q?=E5=AD=98=E6=95=B0=E6=8D=AEcurrentCasualTaskID=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E5=88=B0=E7=8A=B6=E6=80=81=E6=9C=BA=E4=B8=AD=202.=20?= =?UTF-8?q?=E7=BB=99item=E5=92=8Cgroup=E6=B8=B2=E6=9F=93=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E6=96=B0=E5=A2=9EnodeId=E5=AD=97=E6=AE=B5=EF=BC=8C=E4=BB=A3?= =?UTF-8?q?=E8=A1=A8=E7=BB=84=E7=9A=84=E6=A0=87=E8=AF=86=203.=20=E5=88=A0?= =?UTF-8?q?=E9=99=A4ReActChatRenderElement=E5=AE=9A=E4=B9=89=E9=87=8C?= =?UTF-8?q?=E7=9A=84cacheOrder=E5=92=8CdataOrigin=EF=BC=8C=E4=BB=A5?= =?UTF-8?q?=E5=8F=8A=E7=9B=B8=E5=85=B3=E5=AE=9A=E4=B9=89=E6=9E=9A=E4=B8=BE?= =?UTF-8?q?=204.=20=E5=AE=8C=E5=96=84=E4=BA=86=E7=8A=B6=E6=80=81=E6=9C=BA?= =?UTF-8?q?=E9=87=8CdispatchStreamingNode=E5=AD=97=E6=AE=B5=E7=9A=84?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=E5=92=8C=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=205.=20=E8=B0=83=E6=95=B4=E6=95=B0=E6=8D=AE=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E6=9C=BA=E9=87=8C=E7=9A=84handleSendMessage=E6=96=B9=E6=B3=95(?= =?UTF-8?q?=E6=9C=AA=E5=AE=8C=E6=88=90)=206.=20=E6=96=B0=E5=A2=9E=E5=85=AC?= =?UTF-8?q?=E5=85=B1=E6=96=B9=E6=B3=95handleTaskPlanEnd=EF=BC=8C=E5=A4=84?= =?UTF-8?q?=E7=90=86=E4=BB=BB=E5=8A=A1=E8=A7=84=E5=88=92=E7=BB=93=E6=9D=9F?= =?UTF-8?q?=E5=90=8E=E7=9A=84=E6=89=80=E6=9C=89=E6=95=B0=E6=8D=AE=E5=92=8C?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E7=9A=84=E5=8F=98=E5=8C=96=E3=80=81=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E4=BA=86pushLogToOtherWindow=E9=80=BB=E8=BE=91=207.?= =?UTF-8?q?=20=E8=B0=83=E6=95=B4=E6=89=80=E6=9C=89grpc=E6=B5=81=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=9A=84=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/type/aiChat.d.ts | 11 +- .../hooks/ChatMultiSessionController.ts | 100 ++++++++---- .../src/pages/ai-re-act/hooks/aiRender.ts | 60 ++++--- .../src/pages/ai-re-act/hooks/chatStore.ts | 150 +++++++++++------- .../hooks/grpcStreamHandler/aiOther.ts | 101 ++---------- .../hooks/grpcStreamHandler/aiPerf.ts | 4 +- .../hooks/grpcStreamHandler/aiReview.ts | 150 +++++------------- .../hooks/grpcStreamHandler/aiSingleItem.ts | 58 ++----- .../hooks/grpcStreamHandler/aiStream.ts | 25 +-- .../hooks/grpcStreamHandler/aiTaskDetail.ts | 21 +-- .../hooks/grpcStreamHandler/yakExecResult.ts | 9 -- .../main/src/pages/ai-re-act/hooks/type.ts | 11 +- .../pages/ai-re-act/hooks/useNewChatIPC.ts | 7 +- .../main/src/pages/ai-re-act/hooks/utils.ts | 56 +++++-- 14 files changed, 319 insertions(+), 444 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 63733e041d..e7ef849ba8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -217,21 +217,20 @@ export interface AIAgentChatData { contents: Map } export interface AIAgentChatMetaData { + /** 会话通信流建立成功后的UI回调触发事件 */ + onSessionStartSuccess?: (sessionId: string) => void /** 通过用户问题创建会话时的问题 */ createChatQuestion?: AIInputEvent /** 自由对话的实时记忆列表 */ casualMemoryList: AIAgentGrpcApi.MemoryEntryList - /** 会话通信流建立成功后的UI回调触发事件 */ - onSessionStartSuccess?: (sessionId: string) => void /** 任务规划的实时记忆列表 */ taskMemoryList: AIAgentGrpcApi.MemoryEntryList /** 通知消息的消失定时器 */ notifyMessageTimer: NodeJS.Timeout | null - /** 当前自由对话问题的re_act_task_id */ - currentCasualTaskID: string + /** 当前任务规划问题的re_act_task_id|coordinatorId|status */ currentTaskPlanID?: TaskChatTaskInfo - /** 当前任务规划正在进行中的节点uuid */ + /** 当前任务规划正在进行中的节点taskID */ currentTaskPlanActiveNode: Set /** 历史数据: review_release先出现的历史review数据的id-release */ @@ -252,7 +251,7 @@ export interface AIAgentChatMetaData { cardKVPair: Map cardKVPaidTimer: NodeJS.Timeout | null - /** 用于文件操作记录的计数器 */ + /** 用于文件操作记录的计数器, 主要给文件操作记录加上order属性 */ execFileRecordOrder: number /** 同步ID-是否已处理 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index fd90eb6dbc..193e853c75 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -1,12 +1,18 @@ import type { AIAgentChatData, AIAgentChatMetaData } from '@/pages/ai-agent/type/aiChat' -import type { AIAgentGrpcApi, AIInputEvent, AIOutputEvent, AIStartParams } from './grpcApi' +import { + AIInputEventSyncTypeEnum, + type AIAgentGrpcApi, + type AIInputEvent, + type AIOutputEvent, + type AIStartParams, +} from './grpcApi' import { createChatStore } from './chatStore' import { Uint8ArrayToString } from '@/utils/str' import { AIAgentSettingDefault, AIModelTypeEnum } from '@/pages/ai-agent/defaultConstant' import cloneDeep from 'lodash/cloneDeep' import { DefaultMemoryList, DefaultPlanItemDetailsData } from './defaultConstant' import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHandlers' -import { genExecTasks, pushLogToOtherWindow } from './utils' +import { genExecTasks, handleTaskPlanEnd, pushLogToOtherWindow } from './utils' import type { AIChatIPCStartParams, AIChatSendParams } from './type' import { yakitNotify } from '@/utils/notification' import { type AIChatQSData, AIChatQSDataTypeEnum } from './aiRender' @@ -103,7 +109,6 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { casualMemoryList: cloneDeep(DefaultMemoryList), taskMemoryList: cloneDeep(DefaultMemoryList), notifyMessageTimer: null, - currentCasualTaskID: '', currentTaskPlanID: undefined, currentTaskPlanActiveNode: new Set(), historyReviewReleaseID: {}, @@ -161,6 +166,7 @@ export class ChatMultiSessionController { /** * 更新指定会话的配置参数 + * * Source 字段连接会话时锁死,后续不允许热更新 */ public updateSessionConfig(sessionId: string, config: Partial>) { @@ -183,6 +189,7 @@ export class ChatMultiSessionController { store.getState().updateState({ execute: true, casualTitle: '发送问题,开启会话...' }) this.pageSessionMap.get(params.Params?.Source ?? 'ai')?.add(sessionId) + Object.assign(request, params.Params) if (params.Params?.UserQuery) { meta.createChatQuestion = { IsFreeInput: true, @@ -191,7 +198,6 @@ export class ChatMultiSessionController { FocusModeLoop: params.FocusModeLoop, } } - Object.assign(request, params.Params) meta.onSessionStartSuccess = cb ipcRenderer.invoke('start-ai-re-act', sessionId, params) @@ -201,6 +207,7 @@ export class ChatMultiSessionController { public handleSendMessage(payload: AIChatSendParams) { try { const { token, type, params, optionValue } = payload + // TODO - 这里的判断存活写的有些问题 const isExist = this.readyChannels.has(token) if (!isExist) { yakitNotify('warning', '会话不存在,无法发送消息') @@ -213,8 +220,8 @@ export class ChatMultiSessionController { store.getState().updateState({ casualTitle: '等待回复中...' }) } + // 记录发送请求里的syncId-标识开始处理中 if (params.IsSyncMessage && params.SyncID) { - // 记录发送请求里的syncId-标识开始处理中 meta.syncIDMap.set(params.SyncID, true) store.getState().updateStateCount('syncIDUpdate') } @@ -229,18 +236,18 @@ export class ChatMultiSessionController { return } - store.getState().updateCasualReview(params.InteractiveId, 'remove') switch (review.type) { case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - // review操作后不展示在UI上,直接删除 - rawData.contents.delete(review.id) - store.getState().deleteListElement('reAct', review.id) + // tool_review 数据自动执行continue操作,并且不在UI上展示 + // 如果能进入该逻辑,说明有问题 + // TODO - 这里需要处理问题 break case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: // review操作后正常展示在UI上 review.data.selected = params.InteractiveJSONInput review.data.optionValue = optionValue + store.getState().updateCasualReview(params.InteractiveId, 'remove') store.getState().incrementNodeVersion(review.id, 'item') break default: @@ -252,7 +259,7 @@ export class ChatMultiSessionController { if (params.IsInteractiveMessage && params.InteractiveId) { const isExist = store.getState().currentPlanReviewToken === params.InteractiveId const review = rawData.contents.get(params.InteractiveId) - if (!isExist || !review) { + if (isExist || !review) { yakitNotify('error', '未获取到 review 信息, 操作无效') return } @@ -261,24 +268,22 @@ export class ChatMultiSessionController { switch (review.type) { case AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP: case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - // review操作后不展示在UI上,直接删除 - rawData.contents.delete(review.id) - store.getState().deleteListElement('reAct', review.id) + // task_review 和 tool_review 数据自动执行continue操作,并且不在UI上展示 + // 如果能进入该逻辑,说明有问题 + // TODO - 这里需要处理问题 break - case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: // review操作后正常展示在UI上 review.data.selected = params.InteractiveJSONInput review.data.optionValue = optionValue store.getState().dispatchStreamingNode({ chatType: 'task', + parentTaskId: review.taskIndex, node: { token: review.id, kind: 'item', type: review.type, - dataOrigin: 'grpc_realtime_data', }, - groupTokenGenerator: () => '', }) break case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: @@ -292,13 +297,12 @@ export class ChatMultiSessionController { }) store.getState().dispatchStreamingNode({ chatType: 'task', + parentTaskId: review.taskIndex, node: { token: review.id, kind: 'item', type: review.type, - dataOrigin: 'grpc_realtime_data', }, - groupTokenGenerator: () => '', }) break default: @@ -318,6 +322,23 @@ export class ChatMultiSessionController { ipcRenderer.invoke('send-ai-re-act', sessionId, request) } + /** 会话建立成功后, 需要做的额外操作 */ + private handleSessionStartSuccess(sessionId: string) { + // 获取任务规划历史任务树 + this.requestMessage(sessionId, { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS, + }) + /** + * TODO - 轮询最新的问题队列,考虑触发的时机之类的 + * TODO - 获取最新的记忆列表数据,这个好像没法考虑触发时机 + */ + // 获取最新问题队列数据 + // sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) + // 获取最新记忆列表数据 + // sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_MEMORY_CONTEXT }) + } + /** 💥 核心替换:接管原 useChatIPC 里的巨型数据分发逻辑! */ public handleGrpcOutputEvent(sessionId: string, res: AIOutputEvent) { try { @@ -332,22 +353,23 @@ export class ChatMultiSessionController { const { store, rawData, request, meta } = this.ensureSession(sessionId) + // 标识同步ID已处理 if (res.SyncID && meta.syncIDMap.has(res.SyncID)) { - // 标识同步ID已处理 meta.syncIDMap.delete(res.SyncID) store.getState().updateStateCount('syncIDUpdate') } if (res.Type === 'pong') { - const { createChatQuestion } = meta - if (createChatQuestion) { - this.requestMessage(sessionId, createChatQuestion) + if (meta.createChatQuestion) { + this.requestMessage(sessionId, meta.createChatQuestion) meta.createChatQuestion = undefined store.getState().updateState({ casualTitle: '等待回复中...' }) } else { // 调用历史数据恢复方法 } + this.handleSessionStartSuccess(sessionId) meta?.onSessionStartSuccess?.(sessionId) + meta.onSessionStartSuccess = undefined return } @@ -367,6 +389,14 @@ export class ChatMultiSessionController { } } + // if (res.Type === 'structured' && res.NodeId === 'recovery_history') { + // const recoveryHistory = JSON.parse(ipcContent) as AIAgentGrpcApi.RecoveryHistory + // const chatStore = getChatDataStore() + // if (chatStore) chatStore.beforeID.chatID = recoveryHistory.next_start_id + // requestEvents.handleGrpcLoadMore(recoveryHistory) + // return + // } + let funcKey = res.Type if ( res.Type === 'structured' && @@ -379,6 +409,7 @@ export class ChatMultiSessionController { 'react_task_status_changed', 'status', 'stream-finished', + 'capability_inventory', ].includes(res.NodeId) ) { funcKey = res.NodeId @@ -394,10 +425,17 @@ export class ChatMultiSessionController { } else if (data && typeof data === 'object' && data?.type === 'pop_task') { funcKey = 'pop_task' } + } else if (res.Type === 'perception' || res.NodeId === 'perception') { + funcKey = 'perception' + } else if (res.Type === 'current_task_todo_list_update' || res.NodeId === 'current_task_todo_list') { + funcKey = 'current_task_todo_list_update' + } else if (res.NodeId === 'session_snapshot') { + funcKey = res.NodeId } const handleFunc = grpcAIMessageHandlers[funcKey || ''] if (handleFunc) { handleFunc({ + sessionId, res, chatType: meta.currentTaskPlanID?.coordinatorId === res.CoordinatorId ? 'task' : 'reAct', store, @@ -406,12 +444,8 @@ export class ChatMultiSessionController { meta, sendRequest: (request) => this.requestMessage(sessionId, request), pushLog: (log) => { - pushLogToOtherWindow({ - sessionId: sessionId, - isHistory: res.IsSync, - Timestamp: res.Timestamp, - ...log, - }) + if (res.IsSync) return + pushLogToOtherWindow({ sessionId: sessionId, Timestamp: res.Timestamp, ...log }) }, }) return @@ -432,13 +466,12 @@ export class ChatMultiSessionController { rawData.contents.set(data.id, data) store.getState().dispatchStreamingNode({ chatType: data.chatType, + parentTaskId: data.taskIndex, node: { token: data.id, kind: 'item', type: data.type, - dataOrigin: 'grpc_realtime_data', }, - groupTokenGenerator: () => '', }) } @@ -449,9 +482,14 @@ export class ChatMultiSessionController { } // 监听 session-error 事件 - public handleSessionError(sessionId: string, error: any) {} + public handleSessionError(sessionId: string, error: any) { + // 暂无业务逻辑处理 + } // 监听 session-end 事件 public handleSessionEnd(sessionId: string, res: any) { + const { store } = this.ensureSession(sessionId) + handleTaskPlanEnd(this.ensureSession(sessionId)) + store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) this.closeIPCListeners(sessionId) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 0abad99df2..be48259422 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -86,14 +86,6 @@ export interface PlanItemDetailsData { /** 会话列表类型(任务规划|自由对话) */ export type ChatListRenderType = 'reAct' | 'task' -/** 会话列表的数据来源 */ -export enum ReActChatDataOriginEnum { - IndexedDB = 'indexedDB', - GrpcHistoryData = 'grpc_history_data', - GrpcRealtimeData = 'grpc_realtime_data', -} -export type ReActChatDataOrigin = `${ReActChatDataOriginEnum}` - /** 响应式骨架模型 (纯净的字典与关系树) */ export interface ReActChatBaseMeta { token: string @@ -102,9 +94,13 @@ export interface ReActChatBaseMeta { } export interface ReActChatItemMeta extends ReActChatBaseMeta { kind: 'item' + /** 组的标识符 */ + nodeId: AIOutputEvent['NodeId'] } export interface ReActChatGroupMeta extends ReActChatBaseMeta { kind: 'group' + /** 组的标识符 */ + nodeId: AIOutputEvent['NodeId'] childrenTokens: string[] } export interface ReActChatTaskMeta extends ReActChatBaseMeta { @@ -112,16 +108,13 @@ export interface ReActChatTaskMeta extends ReActChatBaseMeta { childrenTokens: string[] } -export type ReActChatMeta = ReActChatItemMeta | ReActChatGroupMeta | ReActChatTaskMeta - /** UI 视图渲染路标 (控制元素怎么画、要不要动画、数据从哪来) */ export interface ReActChatRenderElement { kind: 'item' | 'group' | 'task' token: string chatType: ChatListRenderType - isCached: boolean // 核心结界:决定是否触发工具链折叠与渐入动画 - cacheOrder: number // 前端纯视觉排序权重 - dataOrigin: ReActChatDataOrigin // 业务物理来源 + /** 决定是否触发工具链折叠与渐入动画 */ + isHistory: boolean } /** 控制UI渲染的数据数组元素 */ @@ -520,6 +513,8 @@ export interface ChatStoreState { riskTabUpdate: number // #region 会话列表相关数据 + /** 当前自由对话问题的re_act_task_id */ + currentCasualTaskID: string /** 自由对话的loading 显示的文案 */ casualTitle: string /** 自由对话的是否进行中 */ @@ -575,7 +570,10 @@ export interface ChatStoreState { cancelCasualLoading: boolean /** 用户主动取消问题的loading状态(任务规划) */ cancelTaskLoading: boolean - /** 请求历史数据相关State */ + + /** + * TODO - 有问题,需要调整 请求历史数据相关State + */ requestHistoryState: UseAIMessageDataState /** 更新精准字段数据依赖的渲染版本号 */ @@ -587,8 +585,8 @@ export interface ChatStoreState { | 'memoryListUpdate' | 'updateSystemStream' | 'yaklangCodeChangeUpdate' - | 'currentPlanReviewExtraUpdate' - | 'syncIDUpdate', + | 'syncIDUpdate' + | 'currentPlanReviewExtraUpdate', ) => void updateFolders: (info: AIFileSystemPin) => void @@ -611,12 +609,12 @@ export interface ChatStoreState { | 'updateSystemStream' | 'yaklangCodeChangeUpdate' | 'syncIDUpdate' + | 'grpcFolders' + | 'reActTimelines' | 'httpTabShow' | 'httpTabUpdate' | 'riskTabShow' | 'riskTabUpdate' - | 'grpcFolders' - | 'reActTimelines' | 'currentCasualReview' | 'currentPlanReviewExtraUpdate' | 'items' @@ -642,19 +640,29 @@ export interface ChatStoreState { dispatchStreamingNode: (params: { chatType: ChatListRenderType + /** 是否属于任务组的子节点 */ parentTaskId?: string node: { - token: string kind: 'item' | 'group' | 'task' + token: string type: AIChatQSDataType - dataOrigin: ReActChatDataOrigin - isCached?: boolean - cacheOrder?: number + isHistory?: boolean + /** + * 专供合并成组的逻辑使用 + * + * 是否能合并成组的标识符-AIOutputEvent['NodeId'] + * 只有在AIOutputEvent['ContentType']为default时,才能合并成组,并且才会有值 + */ + nodeId?: AIOutputEvent['NodeId'] + /** + * 专供合并成组的逻辑使用 + * + * 比如 合并成组、添加到组中时,需要对非状态机里数据做的额外处理操作 + * @param groupToken 组token + * @param tokens 新添加到组中的token集合 + */ + groupExtra?: (groupToken: string, tokens: string[]) => void } - groupTokenGenerator: () => string }) => void incrementNodeVersion: (token: string, kind: 'item' | 'group' | 'task') => void - - /** 删除指定列表的某项元素 */ - deleteListElement: (chatType: ChatListRenderType, token: string) => void } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index e594d66108..ec7f340fe2 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -1,4 +1,4 @@ -import { AIChatQSDataTypeEnum, ReActChatDataOriginEnum, type ChatStoreState } from './aiRender' +import { AIChatQSDataTypeEnum, type ChatStoreState } from './aiRender' import { createStore } from 'zustand/vanilla' import { immer } from 'zustand/middleware/immer' import cloneDeep from 'lodash/cloneDeep' @@ -8,7 +8,7 @@ import { DefaultPlanHistoryList, DefaultPlanLoadingStatus, } from './defaultConstant' -import { CurrentExecTaskTree } from './type' +import type { CurrentExecTaskTree } from './type' import { v4 as uuidv4 } from 'uuid' export const createChatStore = () => { @@ -36,6 +36,7 @@ export const createChatStore = () => { riskTabShow: false, riskTabUpdate: 0, + currentCasualTaskID: '', casualTitle: '', casualLoading: false, focusMode: '', @@ -57,7 +58,6 @@ export const createChatStore = () => { taskChat: { elements: [], plan: cloneDeep(DefaultCurrentExecTaskTree), - todoListMap: new Map(), }, card: [], @@ -79,6 +79,7 @@ export const createChatStore = () => { set((state) => { state[type] += 1 }), + updateFolders: (info) => set((state) => { const isExist = state.grpcFolders.find((item) => item.path === info.path) @@ -130,7 +131,7 @@ export const createChatStore = () => { }, updatePlanTree: (planTree: CurrentExecTaskTree) => set((state) => { - Object.assign(state.taskChat.plan, planTree) + state.taskChat.plan = planTree }), updateExecFileRecord: (callToolID, info, order) => @@ -141,74 +142,122 @@ export const createChatStore = () => { state.execFileRecord.set(keyName, keyList) }), - dispatchStreamingNode: ({ chatType, parentTaskId, node, groupTokenGenerator }) => + dispatchStreamingNode: ({ chatType, parentTaskId, node }) => set((state) => { - const isCached = node.isCached ?? false - const cacheOrder = node.cacheOrder ?? 0 - const dataOrigin = node.dataOrigin ?? ReActChatDataOriginEnum.GrpcRealtimeData - // 1. 实体字典动态初始化守卫 + const isHistory = node.isHistory ?? false + + /** + * 添加节点数据到实体字典中 + * + * 这里为什么没有group类型,因为group是通过两个item碰撞自动合成的group + * 暂不支持代码手动新增group类型数据 + */ if (node.kind === 'item' && !state.items[node.token]) { - state.items[node.token] = { token: node.token, kind: 'item', type: node.type || 'assistant', renderNum: 0 } + state.items[node.token] = { + kind: 'item', + token: node.token, + type: node.type, + renderNum: 0, + nodeId: node.nodeId || '', + } } else if (node.kind === 'task' && !state.tasks[node.token]) { state.tasks[node.token] = { - token: node.token, kind: 'task', - type: node.type || 'assistant', - childrenTokens: [], + token: node.token, + type: node.type, renderNum: 0, + childrenTokens: [], } } // 2. 路由分发决策(以普通看板为例,去掉了大任务内部分支以精简示范) const targetElements = chatType === 'reAct' ? state.casualChat.elements : state.taskChat.elements - if (targetElements.length === 0) { - targetElements.push({ kind: node.kind, token: node.token, chatType, isCached, cacheOrder, dataOrigin }) - return - } + if (isHistory) { + // 历史数据处理逻辑 + } else { + // 实时数据处理逻辑 + if (targetElements.length === 0) { + targetElements.push({ kind: node.kind, token: node.token, chatType, isHistory }) + return + } - const lastNode = targetElements[targetElements.length - 1] - const lastToken = lastNode.token - const lastItem = state.items[lastToken] - const lastGroup = state.groups[lastToken] + let lastToken = '' + if (parentTaskId) { + // 属于某个子任务组内的数据 + const targetList = state.tasks[parentTaskId]?.childrenTokens + // 异常数据,暂不警告处理,直接无视 + if (!targetList) return + lastToken = targetList[targetList.length - 1] + } else { + // 属于顶层或者group内的数据 + const lastElement = targetElements[targetElements.length - 1] + lastToken = lastElement.token + } - // 🌟 核心结界拦截:如果是 IndexedDB 或后端的历史,严禁触碰折叠逻辑 - if (!isCached) { - // 合并组吞噬逻辑 - // 需要调整,因为只有stream会有聚合group的情况 - if (lastGroup && lastGroup.kind === 'group' && node.kind === 'item') { + const lastItem = state.items[lastToken] + const lastGroup = state.groups[lastToken] + + // 已经存在组,并且和组标识一致,直接添加到组中 + if ( + lastGroup && + lastGroup.kind === 'group' && + lastGroup.nodeId && + node.kind === 'item' && + node.type === AIChatQSDataTypeEnum.STREAM && + node.nodeId && + lastGroup.nodeId === node.nodeId + ) { lastGroup.childrenTokens.push(node.token) - lastGroup.renderNum += 1 + // lastGroup.renderNum += 1 + node?.groupExtra?.(lastGroup.token, [node.token]) return } - // 偷梁换柱成组逻辑 + + // 两个stream-item数据合并成组的逻辑 if ( lastItem && lastItem.type === AIChatQSDataTypeEnum.STREAM && + lastItem.nodeId && node.type === AIChatQSDataTypeEnum.STREAM && - node.kind === 'item' + node.kind === 'item' && + node.nodeId && + lastItem.nodeId === node.nodeId ) { - const newGroupToken = groupTokenGenerator() + const newGroupToken = `${node.nodeId}-${uuidv4()}` state.groups[newGroupToken] = { - token: newGroupToken, kind: 'group', + token: newGroupToken, type: AIChatQSDataTypeEnum.STREAM_GROUP, - childrenTokens: [lastToken, node.token], renderNum: 0, + nodeId: node.nodeId, + childrenTokens: [lastToken, node.token], } - targetElements[targetElements.length - 1] = { - kind: 'group', - token: newGroupToken, - chatType, - isCached, - cacheOrder, - dataOrigin, + if (parentTaskId && state.tasks[parentTaskId]?.childrenTokens) { + // 更新子任务组最后一个token为新组token + state.tasks[parentTaskId].childrenTokens[state.tasks[parentTaskId].childrenTokens.length - 1] = + newGroupToken + } else { + // 添加到顶层或者group内的数据 + targetElements[targetElements.length - 1] = { + kind: 'group', + token: newGroupToken, + chatType, + isHistory, + } } + node?.groupExtra?.(newGroupToken, [lastToken, node.token]) return } } - targetElements.push({ kind: node.kind, token: node.token, chatType, isCached, cacheOrder, dataOrigin }) + if (parentTaskId && state.tasks[parentTaskId]?.childrenTokens) { + // 剩余情况,直接添加到子任务组中 + state.tasks[parentTaskId].childrenTokens.push(node.token) + } else { + // 添加到顶层数据中 + targetElements.push({ kind: node.kind, token: node.token, chatType, isHistory }) + } }), /** 高频更新节点渲染 */ @@ -218,27 +267,6 @@ export const createChatStore = () => { if (kind === 'group' && state.groups[token]) state.groups[token].renderNum += 1 if (kind === 'task' && state.tasks[token]) state.tasks[token].renderNum += 1 }), - - deleteListElement: (chatType, token) => - set((state) => { - const taskItem = state.tasks[token] - if (taskItem) { - } - const groupItem = state.groups[token] - if (groupItem) { - state.groups[token].childrenTokens = state.groups[token].childrenTokens.filter((item) => item !== token) - } - const itemItem = state.items[token] - if (itemItem) { - state.items[token].childrenTokens = state.items[token].childrenTokens.filter((item) => item !== token) - } - - if (chatType === 'reAct') { - state.casualChat.elements = state.casualChat.elements.filter((item) => item.token !== token) - } else { - state.taskChat.elements = state.taskChat.elements.filter((item) => item.token !== token) - } - }), })), ) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index e59b89eb26..ca1c179e94 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -1,10 +1,10 @@ import type { AIMessageHandler } from '../type' -import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi, type AIOutputEvent } from '../grpcApi' +import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { genBaseAIChatData, generateTaskNodeID, genExecTasks } from '../utils' +import { genBaseAIChatData, generateTaskNodeID, genExecTasks, handleTaskPlanEnd } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' -import { DefaultCurrentExecTaskTree, DefaultPlanItemDetailsData, DefaultPlanLoadingStatus } from '../defaultConstant' +import { DefaultCurrentExecTaskTree, DefaultPlanItemDetailsData } from '../defaultConstant' import has from 'lodash/has' const handleHttpFuzzRequestChange: AIMessageHandler = (request) => { @@ -85,24 +85,18 @@ const handleStartPlanAndExecution: AIMessageHandler = (request) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: 'task', - node: { - token: chatData.id, - kind: 'task', - type: chatData.type, - dataOrigin: 'grpc_realtime_data', - }, - groupTokenGenerator: () => '', + node: { token: chatData.id, kind: 'task', type: chatData.type }, }) } -const handleEndPlanAndExecution: AIMessageHandler = (request) => { - const { res, store, rawData, meta } = request +const handleEndPlanAndExecution: AIMessageHandler = (requestInfo) => { + const { res, store, rawData, meta } = requestInfo if (res.Type !== 'end_plan_and_execution') return if (res.IsSync) return const ipcContent = Uint8ArrayToString(res.Content) || '' const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution if (!startInfo.coordinator_id) { - request.pushLog({ level: 'error', message: `${res.Type}数据, coordinator_id 为空` }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据, coordinator_id 为空` }) return } if (startInfo.coordinator_id === meta.currentTaskPlanID?.coordinatorId) { @@ -115,26 +109,9 @@ const handleEndPlanAndExecution: AIMessageHandler = (request) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: 'task', - node: { token: chatData.id, kind: 'item', type: chatData.type, dataOrigin: 'grpc_realtime_data' }, - groupTokenGenerator: () => '', - }) - const actives = Array.from(meta.currentTaskPlanActiveNode) - meta.currentTaskPlanActiveNode.clear() - for (const active of actives) { - const taskNodeInfo = rawData.contents.get(active) - if (!taskNodeInfo || taskNodeInfo.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - continue - } - taskNodeInfo.data.status = AITaskStatus.error - store.getState().incrementNodeVersion(taskNodeInfo.id, 'task') - } - const newPlanTree = store.getState().taskChat.plan - newPlanTree.task_tree = newPlanTree.task_tree.map((item) => { - if (item.progress === AITaskStatus.inProgress) item.progress = AITaskStatus.error - return item + node: { token: chatData.id, kind: 'item', type: chatData.type }, }) - store.getState().updatePlanTree(newPlanTree) - store.getState().updateState({ taskStatus: cloneDeep(DefaultPlanLoadingStatus) }) + handleTaskPlanEnd({ store, rawData, meta, request: requestInfo.request }) } } @@ -218,9 +195,12 @@ const handleReactTaskDequeue: AIMessageHandler = (request) => { // 实时数据里,记录用户问题的状态和专注模式信息 if (!res.IsSync) { sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) - meta.currentCasualTaskID = res.TaskId || data.react_task_id rawData.casualChat.planDetails = cloneDeep(DefaultPlanItemDetailsData) - store.getState().updateState({ focusMode: data.focus_mode ? data.focus_mode : '', casualLoading: true }) + store.getState().updateState({ + currentCasualTaskID: res.TaskId || data.react_task_id, + focusMode: data.focus_mode ? data.focus_mode : '', + casualLoading: true, + }) } // 用户问题的UI回显 @@ -242,9 +222,7 @@ const handleReactTaskDequeue: AIMessageHandler = (request) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', }, - groupTokenGenerator: () => '', }) } @@ -333,9 +311,7 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const { react_task_id, react_task_now_status } = JSON.parse(ipcContent) as AIAgentGrpcApi.ReactTaskChanged if (['completed', 'aborted'].includes(react_task_now_status)) { - if (meta.currentCasualTaskID && meta.currentCasualTaskID === react_task_id) { - // 问题任务完成或者者被中止后,重置当前问题任务id - meta.currentCasualTaskID = '' + if (store.getState().currentCasualTaskID && store.getState().currentCasualTaskID === react_task_id) { store.getState().updateState({ cancelCasualLoading: false }) // 取消专注模式 store.getState().updateState({ focusMode: '' }) @@ -351,33 +327,6 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { } } -// TODO: ? -const handleStatus: AIMessageHandler = (request) => { - const { res, chatType, store } = request - if (res.Type !== 'structured' || res.NodeId !== 'status') return - if (res.IsSync) return - - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) as { key: string; value: string } - if (data.key === 're-act-loading-status-key') { - if (chatType === 'task') { - // 任务规划-loading展示标题 - store.getState().updateTaskLoadingStatus({ task: data.value || '加载中...' }) - } else { - // 自由对话-loading展示标题 - store.getState().updateState({ casualTitle: data.value }) - } - } else if (data.key === 'plan-executing-loading-status-key') { - if (chatType === 'task') { - // 任务规划-loading展示标题 - store.getState().updateTaskLoadingStatus({ plan: data.value || '加载中...' }) - } - } else { - // 执行状态卡片处理 - // yakExecResultEvent.handleSetData(res) - } -} - const handleTrafficCount: AIMessageHandler = (request) => { const { res, chatType, store, rawData } = request @@ -470,28 +419,8 @@ export const aiOtherDataHandlers = { plan_exec_tasks: handlePlanExecTasks, queue_info: handleQueueInfo, react_task_status_changed: handleReactTaskStatusChanged, - status: handleStatus, yak_httpflow_count: handleTrafficCount, yak_risk_count: handleTrafficCount, plan: handlePlan, yaklang_code_change: handleYaklangCodeChange, } as const - -const exampleHandle = (res: AIOutputEvent) => { - let funcKey = res.Type - if ( - res.Type === 'structured' && - [ - 'session_title', - 'timeline_item', - 'react_task_enqueue', - 'react_task_dequeue', - 'queue_info', - 'react_task_status_changed', - 'status', - ].includes(res.NodeId) - ) { - // stream数据结束标识 - funcKey = res.NodeId - } -} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiPerf.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiPerf.ts index 85e45f2aee..f2885ad987 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiPerf.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiPerf.ts @@ -31,7 +31,7 @@ const handlePressure: AIMessageHandler = (request) => { } } -const handleaAIFirstByteCostMS: AIMessageHandler = (request) => { +const handleAIFirstByteCostMS: AIMessageHandler = (request) => { const { res, rawData } = request if (res.Type !== 'ai_first_byte_cost_ms') return if (res.IsSync) return @@ -214,7 +214,7 @@ const handlePromptProfile: AIMessageHandler = (request) => { export const aiPerfDataHandlers = { consumption: handleConsumption, pressure: handlePressure, - ai_first_byte_cost_ms: handleaAIFirstByteCostMS, + ai_first_byte_cost_ms: handleAIFirstByteCostMS, ai_total_cost_ms: handleAITotalCostMS, prompt_profile: handlePromptProfile, } as const diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index 650f17d575..c720142cc4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -1,5 +1,5 @@ import type { AIMessageHandler } from '../type' -import type { AIAgentGrpcApi } from '../grpcApi' +import type { AIAgentGrpcApi, AIInputEvent } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { genBaseAIChatData, generateTaskNodeDataID, genExecTasks, isAutoExecuteReviewContinue } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' @@ -41,9 +41,8 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: true, }, - groupTokenGenerator: () => '', }) } return @@ -66,9 +65,7 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', }, - groupTokenGenerator: () => '', }) const tasks = chatData.data const plans = genExecTasks(tasks.plans.root_task) @@ -117,7 +114,7 @@ const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { } const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta } = requestInfo + const { res, chatType, rawData, meta, sendRequest } = requestInfo if (res.Type !== 'task_review_require') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -144,51 +141,18 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { if (res.IsSync) return // 实时数据处理逻辑 - const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) - if (isAuto) { - chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) - chatData.data.optionValue = 'continue' - } - // 实时数据-task-review-自动执行,不展示在UI上 - if (isAuto) return - - // 将数据存入hook里的缓存变量中 - rawData.contents.set(chatData.id, cloneDeep(chatData)) - if (chatType === 'task') { - // 实时-任务规划里, 自动请求执行 - // const info: AIInputEvent = { - // IsInteractiveMessage: true, - // InteractiveId: chatData.id, - // InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), - // } - // sendRequest(info) - // rawData.contents.delete(chatData.id) - store.getState().updateState({ currentPlanReviewToken: chatData.id }) - } else if (chatType === 'reAct') { - // 实时-自由对话里, 自动请求执行 - // const info: AIInputEvent = { - // IsInteractiveMessage: true, - // InteractiveId: chatData.id, - // InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), - // } - // sendRequest(info) - // rawData.contents.delete(chatData.id) - store.getState().updateCasualReview(chatData.id, 'add') - store.getState().dispatchStreamingNode({ - chatType: chatType, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', - }, - groupTokenGenerator: () => '', - }) + // task_review 和 tool_review 在任何review模式下,都是自动执行continue操作 + // 操作的review,在UI上不显示操作历史 + const info: AIInputEvent = { + IsInteractiveMessage: true, + InteractiveId: chatData.id, + InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), } + sendRequest(info) } const handleToolReview: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta } = requestInfo + const { res, chatType, rawData, meta, sendRequest } = requestInfo if (res.Type !== 'tool_use_review_require') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -215,47 +179,14 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { if (res.IsSync) return // 实时数据处理逻辑 - const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) - if (isAuto) { - chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) - chatData.data.optionValue = 'continue' - } - // 实时数据-task-review-自动执行,不展示在UI上 - if (isAuto) return - - // 将数据存入hook里的缓存变量中 - rawData.contents.set(chatData.id, cloneDeep(chatData)) - if (chatType === 'task') { - // 后面要改成自动请求执行 - // const info: AIInputEvent = { - // IsInteractiveMessage: true, - // InteractiveId: chatData.id, - // InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), - // } - // sendRequest(info) - // rawData.contents.delete(chatData.id) - store.getState().updateState({ currentPlanReviewToken: chatData.id }) - } else if (chatType === 'reAct') { - // 实时-自由对话里, 自动请求执行 - // const info: AIInputEvent = { - // IsInteractiveMessage: true, - // InteractiveId: chatData.id, - // InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), - // } - // sendRequest(info) - // rawData.contents.delete(chatData.id) - store.getState().updateCasualReview(chatData.id, 'add') - store.getState().dispatchStreamingNode({ - chatType: chatType, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', - }, - groupTokenGenerator: () => '', - }) + // task_review 和 tool_review 在任何review模式下,都是自动执行continue操作 + // 操作的review,在UI上不显示操作历史 + const info: AIInputEvent = { + IsInteractiveMessage: true, + InteractiveId: chatData.id, + InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), } + sendRequest(info) } const handleUserInteractive: AIMessageHandler = (requestInfo) => { @@ -298,9 +229,8 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: true, }, - groupTokenGenerator: () => '', }) } return @@ -311,15 +241,15 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { if (chatType === 'task') { store.getState().updateState({ currentPlanReviewToken: chatData.id }) } else if (chatType === 'reAct') { + store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ chatType: chatType, + parentTaskId: chatData.taskIndex, node: { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', }, - groupTokenGenerator: () => '', }) } } @@ -366,9 +296,8 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: true, }, - groupTokenGenerator: () => '', }) } return @@ -379,6 +308,8 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { if (isAuto) { chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) chatData.data.optionValue = 'continue' + } else { + store.getState().updateCasualReview(chatData.id, 'add') } // 将数据存入hook里的缓存变量中 rawData.contents.set(chatData.id, cloneDeep(chatData)) @@ -389,9 +320,7 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', }, - groupTokenGenerator: () => '', }) } @@ -462,19 +391,18 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { reviewDetail.data.optionValue = data.params?.suggestion || 'continue' store.getState().dispatchStreamingNode({ chatType: chatType, + parentTaskId: reviewDetail.taskIndex, node: { token: reviewDetail.id, kind: 'item', type: reviewDetail.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: true, }, - groupTokenGenerator: () => '', }) break case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - // 历史数据还没有渲染在UI上,所以不需要从UI中删除的逻辑 - rawData.contents.delete(reviewDetail.id) + // 历史数据-不展示,直接跳过处理逻辑 break default: break @@ -501,16 +429,17 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { task_tree: cloneDeep(plans), root_task_name: tasks.plans.root_task.name, }) + // 将操作记录渲染到列表上 store.getState().dispatchStreamingNode({ chatType: chatType, node: { token: reviewDetail.id, kind: 'item', type: reviewDetail.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', }, - groupTokenGenerator: () => '', }) + // 关闭review的弹窗 + if (chatType === 'task') store.getState().updateState({ currentPlanReviewToken: '' }) break case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: @@ -518,29 +447,22 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { reviewDetail.data.optionValue = data.params?.suggestion || 'continue' store.getState().dispatchStreamingNode({ chatType: chatType, + parentTaskId: reviewDetail.taskIndex, node: { token: reviewDetail.id, kind: 'item', type: reviewDetail.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', }, - groupTokenGenerator: () => '', }) - break - case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: - case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - let isDelete = false if (chatType === 'reAct') { - isDelete = store.getState().currentCasualReview.includes(reviewDetail.id) store.getState().updateCasualReview(reviewDetail.id, 'remove') - } else if (chatType === 'task') { - isDelete = store.getState().currentPlanReviewToken === reviewDetail.id + } else { store.getState().updateState({ currentPlanReviewToken: '' }) } - if (isDelete) { - rawData.contents.delete(reviewDetail.id) - store.getState().deleteListElement(chatType, reviewDetail.id) - } + break + case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + // 实时数据-直接自动执行了continue,并且不需要展示在UI上,所以该处逻辑直接跳过 break default: break diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index 4eb9a505cb..ec7eb7ccf4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -31,9 +31,8 @@ const handleThought: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } @@ -65,9 +64,8 @@ const handleResult: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } @@ -100,9 +98,8 @@ const handleFailReactTask: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } @@ -139,9 +136,8 @@ const handleToolCallDecision: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } @@ -174,9 +170,8 @@ const handleFailPlanAndExecution: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } @@ -208,9 +203,8 @@ const handleApiRequestFailed: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } @@ -267,9 +261,8 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } } @@ -313,9 +306,8 @@ const handleReportFinish: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } @@ -387,9 +379,8 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'task', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } } @@ -440,34 +431,3 @@ export const aiSingleItemDataHandlers = { push_task: handlePushTask, pop_task: handlePopTask, } as const - -const exampleHandle = (res: AIOutputEvent) => { - let funcKey = res.Type - if ( - res.Type === 'structured' && - [ - 'session_title', - 'timeline_item', - 'react_task_enqueue', - 'react_task_dequeue', - 'queue_info', - 'react_task_status_changed', - 'status', - ].includes(res.NodeId) - ) { - // stream数据结束标识 - funcKey = res.NodeId - } else if (res.Type === 'api_request_failed' && res.NodeId === 'ai_call_failure') { - funcKey = res.NodeId - } else if (res.Type === 'report_finish' || res.NodeId === 'report-finish') { - funcKey = res.NodeId - } else if (res.Type === 'structured' && res.NodeId === 'system') { - const ipcContent = Uint8ArrayToString(res.Content) || '' - const data = JSON.parse(ipcContent) || '' - if (data && typeof data === 'object' && data?.type === 'push_task') { - funcKey = 'push_task' - } else if (data && typeof data === 'object' && data?.type === 'pop_task') { - funcKey = 'pop_task' - } - } -} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index f2894f7fde..deb60c1e3c 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -229,9 +229,8 @@ const handleStream: AIMessageHandler = (requestInfo) => { token: streamData.id, kind: 'item', type: streamData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } } @@ -331,7 +330,7 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { } const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, request, meta } = requestInfo + const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'reference_material') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -350,9 +349,8 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } else if (chatData.type === AIChatQSDataTypeEnum.STREAM) { if (toolResult && isToolStdoutStream(chatData.data.NodeId)) { @@ -366,9 +364,8 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } return @@ -380,9 +377,8 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } } else if ( @@ -418,9 +414,8 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { token: chatData.id, kind: 'item', type: chatData.type, - dataOrigin: res.IsSync ? 'grpc_history_data' : 'grpc_realtime_data', + isHistory: res.IsSync, }, - groupTokenGenerator: () => '', }) } } @@ -433,11 +428,3 @@ export const aiStreamDataHandlers = { 'stream-finished': handleStreamFinished, reference_material: handleReferenceMaterial, } as const - -const exampleHandle = (res: AIOutputEvent) => { - let funcKey = res.Type - if (res.Type === 'structured' && ['stream-finished'].includes(res.NodeId)) { - // stream数据结束标识 - funcKey = res.NodeId - } -} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts index 85330282fd..25abc58011 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts @@ -6,6 +6,8 @@ import cloneDeep from 'lodash/cloneDeep' import isArray from 'lodash/isArray' import isEmpty from 'lodash/isEmpty' import { v4 as uuidv4 } from 'uuid' +import type { PlanItemDetailsData } from '../aiRender' +import { DefaultPlanItemDetailsData } from '../defaultConstant' const handleCapabilityInventory: AIMessageHandler = (requestInfo) => { const { res, chatType, rawData } = requestInfo @@ -118,7 +120,7 @@ const handleCapabilityInventory: AIMessageHandler = (requestInfo) => { } const handlePerception: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta } = requestInfo + const { res, chatType, rawData } = requestInfo if (res.Type !== 'perception' || res.NodeId !== 'perception') return if (!res.TaskId) return @@ -143,7 +145,7 @@ const handlePerception: AIMessageHandler = (requestInfo) => { } const handleCurrentTaskTodoListUpdate: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta } = requestInfo + const { res, chatType, store, rawData } = requestInfo if (res.Type !== 'current_task_todo_list_update' || res.NodeId !== 'current_task_todo_list') return if (!res.TaskId) return @@ -169,7 +171,7 @@ const handleCurrentTaskTodoListUpdate: AIMessageHandler = (requestInfo) => { } const handleSessionSnapshot: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta } = requestInfo + const { res, chatType, rawData } = requestInfo if (res.NodeId !== 'session_snapshot') return if (!res.TaskId) return @@ -198,16 +200,3 @@ export const aiTaskDetailDataHandlers = { current_task_todo_list_update: handleCurrentTaskTodoListUpdate, session_snapshot: handleSessionSnapshot, } as const - -const exampleHandle = (res: AIOutputEvent) => { - let funcKey = res.Type - if (res.Type === 'structured' && res.NodeId === 'capability_inventory') { - funcKey = res.NodeId - } else if (res.Type === 'perception' && res.NodeId === 'perception') { - funcKey = res.Type - } else if (res.Type === 'current_task_todo_list_update' && res.NodeId === 'current_task_todo_list') { - funcKey = res.Type - } else if (res.NodeId === 'session_snapshot') { - funcKey = res.NodeId - } -} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts index 9b8d673352..0bf9c1d713 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts @@ -4,7 +4,6 @@ import { Uint8ArrayToString } from '@/utils/str' import { checkStreamValidity, convertCardInfo } from '@/hook/useHoldGRPCStream/useHoldGRPCStream' import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' -// TODO: ? const handleStatus: AIMessageHandler = (request) => { const { res, chatType, store, meta } = request if (res.Type !== 'structured' || res.NodeId !== 'status') return @@ -106,11 +105,3 @@ export const aiYakExecResultDataHandlers = { status: handleStatus, yak_exec_result: handleYakExecResult, } as const - -const exampleHandle = (res: AIOutputEvent) => { - let funcKey = res.Type - if (res.Type === 'structured' && ['status'].includes(res.NodeId)) { - // stream数据结束标识 - funcKey = res.NodeId - } -} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts index 5ee2b2a348..e053405f63 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts @@ -12,9 +12,9 @@ import type { Dispatch, SetStateAction } from 'react' import type { Domain } from '@/pages/ai-agent/store/constants' import type { AIAgentGrpcApi, AIInputEvent, AIOutputEvent, AISource, AIStartParams, AITaskStatusType } from './grpcApi' import type { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' -import type { AIAgentChatData, AIAgentChatMetaData, AIChatData } from '@/pages/ai-agent/type/aiChat' +import type { AIChatData } from '@/pages/ai-agent/type/aiChat' import type { ChatDataStore } from '@/pages/ai-agent/store/ChatDataStore' -import { createChatStore } from './chatStore' +import { ChatMultiSessionController } from './ChatMultiSessionController' // #region 公共 hooks 事件 export interface UseHookBaseParams { @@ -332,14 +332,11 @@ export interface UseAIChatLogEvents { // #endregion // #region AI-Agent相关grpc流数据处理逻辑 -export interface AIMessageHandlerParams { +export interface AIMessageHandlerParams extends ReturnType { + sessionId: string /** grpc流原始数据 */ res: AIOutputEvent chatType: ChatListRenderType - store: ReturnType - rawData: AIAgentChatData - request: AIStartParams - meta: AIAgentChatMetaData sendRequest: (request: AIInputEvent) => void pushLog: (log: AIAgentGrpcApi.Log) => void } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts index d3916c90f7..0608a31b2d 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts @@ -1,7 +1,7 @@ // useChatIPC.ts import { useEffect, useCallback, useRef } from 'react' import { globalSessionEngine } from './ChatMultiSessionController' -import { AIChatIPCStartParams, AIChatSendParams } from './type' +import type { AIChatIPCStartParams, AIChatSendParams } from './type' import { useMemoizedFn } from 'ahooks' import useCurrentSessionId from './useCurrentSessionId' @@ -9,6 +9,7 @@ const { ipcRenderer } = window.require('electron') export function useChatIPC() { const sessionId = useCurrentSessionId() + // TODO: 我需要知道这个hook所在的页面,从而在生命周期的卸载时期关闭所有会话 const currentSessionIdRef = useRef(sessionId) useEffect(() => { @@ -44,13 +45,13 @@ export function useChatIPC() { * TODO - 需要AISource且支持多个会话关闭 * TODO - sessionId传[]:清除source下的所有会话 */ - globalSessionEngine.forceCloseSession(sessionId, params.source) + // globalSessionEngine.forceCloseSession(sessionId, params.source) }) // 组件卸载时拔插头,清理闭环 useEffect(() => { return () => { - globalSessionEngine.forceCloseSession(currentSessionIdRef.current) + // globalSessionEngine.forceCloseSession(currentSessionIdRef.current) } }, []) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts index 46672a14da..6297340ade 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts @@ -3,18 +3,22 @@ */ import type { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' import type { DialogueRecord } from '@/pages/ai-agent/store/type' -import type { - AITaskInfoProps, - ReActChatRenderItem, - AIChatQSDataType, - TodoListCardData, - ChatListRenderType, +import { + type AITaskInfoProps, + type ReActChatRenderItem, + type AIChatQSDataType, + type TodoListCardData, + type ChatListRenderType, + AIChatQSDataTypeEnum, } from './aiRender' -import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' +import { AITaskStatus, type AIAgentGrpcApi, type AIOutputEvent } from './grpcApi' import { AIToDoListStatusEnum, generateTaskChatExecution } from '@/pages/ai-agent/defaultConstant' import { v4 as uuidv4 } from 'uuid' import { JSONParseLog } from '@/utils/tool' import { aiAgentLogEmitter } from './AIAgentLogEmitter' +import cloneDeep from 'lodash/cloneDeep' +import { DefaultPlanLoadingStatus } from './defaultConstant' +import { ChatMultiSessionController } from './ChatMultiSessionController' /** 生成任务节点的唯一ID */ export const generateTaskNodeID = (planID: string, taskID: string) => { @@ -48,23 +52,45 @@ export const genBaseAIChatData = (info: AIOutputEvent) => { } } +/** 任务规划结束后的所有数据处理 */ +export const handleTaskPlanEnd: (requestInfo: ReturnType) => void = ( + requestInfo, +) => { + const { store, rawData, meta } = requestInfo + + // 将UI列表里正在执行中的任务组状态变成error + const actives = Array.from(meta.currentTaskPlanActiveNode) + meta.currentTaskPlanActiveNode.clear() + for (const active of actives) { + const taskNodeInfo = rawData.contents.get(active) + if (!taskNodeInfo || taskNodeInfo.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { + continue + } + taskNodeInfo.data.status = AITaskStatus.error + store.getState().incrementNodeVersion(taskNodeInfo.id, 'task') + } + // 将当前正在执行的任务树里, 进行中的节点状态变成error + const newPlanTree = cloneDeep(store.getState().taskChat.plan) + newPlanTree.task_tree = newPlanTree.task_tree.map((item) => { + if (item.progress === AITaskStatus.inProgress) item.progress = AITaskStatus.error + return item + }) + store.getState().updatePlanTree(newPlanTree) + store.getState().updateState({ taskStatus: cloneDeep(DefaultPlanLoadingStatus) }) +} + /** Agent 往日志窗口推送日志数据 */ export const pushLogToOtherWindow = (params: { sessionId: string - /** 是否是历史数据 */ - isHistory: AIOutputEvent['IsSync'] Timestamp: AIOutputEvent['Timestamp'] level: string message: string }) => { - const { sessionId, isHistory, Timestamp, level, message } = params - if (isHistory) return - aiAgentLogEmitter.dispatch({ - session: sessionId, + session: params.sessionId, type: 'log', - Timestamp: Timestamp, - log: { level: level, message: message }, + Timestamp: params.Timestamp, + log: { level: params.level, message: params.message }, }) } From 2c86ae140637de09718755c3cc31b7087876ede7 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Mon, 6 Jul 2026 12:08:02 +0800 Subject: [PATCH 019/101] =?UTF-8?q?feat:=201.=20=E5=B0=86AITree=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E9=87=8C=E7=9A=84TODO=E6=B3=A8=E9=87=8A=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E8=B0=83=E6=95=B4,=20=E5=B9=B6=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=202.=20=E5=88=A0=E9=99=A4=E4=BA=86=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E6=89=80=E6=9C=89=E4=BD=BF=E7=94=A8generateTaskNodeID?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E7=9A=84=E9=80=BB=E8=BE=91=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E8=AF=A5=E6=96=B9=E6=B3=95=EF=BC=8C=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=BD=BF=E7=94=A8=E4=BA=86=E4=BB=BB=E5=8A=A1=E7=9A=84?= =?UTF-8?q?taskID=203.=20=E5=88=A0=E9=99=A4isValidTaskIndex=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E5=92=8C=E5=AF=B9=E5=BA=94=E6=89=80=E6=9C=89=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E5=88=B0=E7=9A=84=E9=80=BB=E8=BE=91=E4=BD=8D=E7=BD=AE?= =?UTF-8?q?=204.=20=E8=A7=A3=E5=86=B3master=E6=96=B0=E5=A2=9EAI=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=9C=A8=E6=97=A7=E7=89=88=E6=9C=AC=E7=9A=84=E5=86=B2?= =?UTF-8?q?=E7=AA=81=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatTemplate/AIAgentChatTemplate.tsx | 9 -- .../hooks/ChatMultiSessionController.ts | 3 + .../hooks/grpcStreamHandler/aiOther.ts | 58 ++++++- .../hooks/grpcStreamHandler/aiReview.ts | 48 +++++- .../hooks/grpcStreamHandler/aiSingleItem.ts | 150 +++++++++--------- .../hooks/grpcStreamHandler/aiStream.ts | 8 +- .../hooks/grpcStreamHandler/aiTaskDetail.ts | 5 +- .../hooks/grpcStreamHandler/aiToolResult.ts | 58 ++++++- .../main/src/pages/ai-re-act/hooks/utils.ts | 20 +-- 9 files changed, 245 insertions(+), 114 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx index f7d60a3b6d..af8575e95e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx @@ -315,15 +315,6 @@ export const AIAgentChatStream: React.FC = memo((props) const index = streams.findIndex((item) => item.token === id) if (index !== -1) locateToIndex(index, 'auto') }) - // const onScrollToIndex = useMemoizedFn((id) => { - // if (!meta?.currentTaskPlanID?.taskID) return false - // const taskID = generateTaskNodeID(meta?.currentTaskPlanID?.taskID, id) - // const index = streams.findIndex((item) => taskID === item.token) - // if (index !== -1) { - // scrollToIndex(index, 'auto') - // waitAndHighlight(index) - // } - // }) useMount(() => { // 仅监听 Ready:由 AITaskContent 保证深度规划已可见后再发 emiter.on('onAITreeLocatePlanningListReady', onTreeLocate) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 193e853c75..b32c1aad07 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -410,6 +410,7 @@ export class ChatMultiSessionController { 'status', 'stream-finished', 'capability_inventory', + 'react_task_created', ].includes(res.NodeId) ) { funcKey = res.NodeId @@ -431,6 +432,8 @@ export class ChatMultiSessionController { funcKey = 'current_task_todo_list_update' } else if (res.NodeId === 'session_snapshot') { funcKey = res.NodeId + } else if (res.Type === 'detached_plan_require' && res.NodeId === 'detached-plan') { + funcKey = res.Type } const handleFunc = grpcAIMessageHandlers[funcKey || ''] if (handleFunc) { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index ca1c179e94..6cc5830d7a 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -1,7 +1,7 @@ import type { AIMessageHandler } from '../type' -import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi } from '../grpcApi' +import { AIInputEventSyncTypeEnum, AITaskStatus, AITaskStatusType, type AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { genBaseAIChatData, generateTaskNodeID, genExecTasks, handleTaskPlanEnd } from '../utils' +import { genBaseAIChatData, genExecTasks, handleTaskPlanEnd } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { DefaultCurrentExecTaskTree, DefaultPlanItemDetailsData } from '../defaultConstant' @@ -75,7 +75,7 @@ const handleStartPlanAndExecution: AIMessageHandler = (request) => { }) // 生成任务规划里的默认任务聚合组 - const taskID = generateTaskNodeID(meta.currentTaskPlanID.taskID, 'unknown') + const taskID = `${meta.currentTaskPlanID.taskID}-default` const chatData: AIChatQSData = { ...genBaseAIChatData(res), id: taskID, @@ -205,7 +205,7 @@ const handleReactTaskDequeue: AIMessageHandler = (request) => { // 用户问题的UI回显 const chatData: AIChatQSData = { - id: data.react_task_id, + id: res.TaskId || data.react_task_id, chatType: 'reAct', type: AIChatQSDataTypeEnum.QUESTION, Timestamp: res.Timestamp, @@ -309,8 +309,9 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { if (res.IsSync || chatType === 'task') return const ipcContent = Uint8ArrayToString(res.Content) || '' - const { react_task_id, react_task_now_status } = JSON.parse(ipcContent) as AIAgentGrpcApi.ReactTaskChanged - if (['completed', 'aborted'].includes(react_task_now_status)) { + const info = JSON.parse(ipcContent) as AIAgentGrpcApi.ReactTaskChanged + const react_task_id = res.TaskId || info.react_task_id + if (['completed', 'aborted'].includes(info.react_task_now_status)) { if (store.getState().currentCasualTaskID && store.getState().currentCasualTaskID === react_task_id) { store.getState().updateState({ cancelCasualLoading: false }) // 取消专注模式 @@ -321,10 +322,15 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { store.getState().updateCasualTodoList() if (meta.currentTaskPlanID?.taskID === react_task_id) { // 该问题触发了任务规划, 所以需要将任务规划状态也调整 - meta.currentTaskPlanID.status = react_task_now_status as AITaskStatus + meta.currentTaskPlanID.status = info.react_task_now_status as AITaskStatus store.getState().updateState({ cancelTaskLoading: false }) } } + // 更新自由对话-执行任务组的状态 + const taskDetail = rawData.contents.get(react_task_id) + if (!taskDetail || taskDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) return + taskDetail.data.status = info.react_task_now_status as AITaskStatusType + store.getState().incrementNodeVersion(taskDetail.id, 'task') } const handleTrafficCount: AIMessageHandler = (request) => { @@ -403,6 +409,43 @@ const handleYaklangCodeChange: AIMessageHandler = (requestInfo) => { store.getState().updateStateCount('yaklangCodeChangeUpdate') } +const handleReactTaskCreated: AIMessageHandler = (request) => { + const { res, chatType, store, rawData } = request + if (res.Type !== 'structured' || res.NodeId !== 'react_task_created') return + if (res.IsSync || chatType === 'task') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const info = JSON.parse(ipcContent) as AIAgentGrpcApi.CasualCreated + + // 非聚合任务不处理 + if (!info.react_task_is_sub_agent) return + if (!info.react_task_id) return + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + id: info.react_task_id, + chatType: 'reAct', + type: AIChatQSDataTypeEnum.TASK_NODE_GROUP, + data: { + taskId: info.react_task_id, + taskIndex: info.react_task_id, + taskName: info.react_task_name || info.react_user_input || info.react_task_id, + goal: info.react_user_input || '', + status: info.react_task_status, + }, + } as AIChatQSData + + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: 'reAct', + node: { + token: chatData.id, + kind: 'task', + type: chatData.type, + }, + }) +} + export const aiOtherDataHandlers = { http_fuzz_request_change: handleHttpFuzzRequestChange, http_flow_fuzz_status: handleHttpFlowFuzzStatus, @@ -423,4 +466,5 @@ export const aiOtherDataHandlers = { yak_risk_count: handleTrafficCount, plan: handlePlan, yaklang_code_change: handleYaklangCodeChange, + react_task_created: handleReactTaskCreated, } as const diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index c720142cc4..0a92b0c307 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -133,7 +133,7 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -171,7 +171,7 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -209,7 +209,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -276,7 +276,7 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -443,6 +443,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { break case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + case AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE: reviewDetail.data.selected = JSON.stringify(data.params) reviewDetail.data.optionValue = data.params?.suggestion || 'continue' store.getState().dispatchStreamingNode({ @@ -455,7 +456,11 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { }, }) if (chatType === 'reAct') { - store.getState().updateCasualReview(reviewDetail.id, 'remove') + if (reviewDetail.type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) { + store.getState().updateState({ currentPlanReviewToken: '' }) + } else { + store.getState().updateCasualReview(reviewDetail.id, 'remove') + } } else { store.getState().updateState({ currentPlanReviewToken: '' }) } @@ -469,6 +474,38 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { } } +const handleDetachedPlanReview: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo + if (res.Type !== 'detached_plan_require' || res.NodeId !== 'detached-plan') return + + // 历史数据-grpc流数据在任务规划下无效,不处理 + if (res.IsSync) return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.DetachedPlanRequire + if (!data?.id || !data?.plans?.root_task || !data?.selectors?.length) { + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: ${ipcContent}` }) + return + } + + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + chatType: chatType, + id: data.id, + type: AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE, + data: { ...cloneDeep(data) }, + taskIndex: generateTaskNodeDataID({ + chatType: chatType, + planID: meta.currentTaskPlanID?.coordinatorId, + taskID: res.TaskId, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, cloneDeep(chatData)) + + if (chatType === 'reAct') store.getState().updateState({ currentPlanReviewToken: chatData.id }) +} + export const aiReviewDataHandlers = { plan_review_require: handlePlanReviewRequire, plan_task_analysis: handlePlanTaskAnalysis, @@ -480,4 +517,5 @@ export const aiReviewDataHandlers = { ai_review_countdown: handleAIReviewJudgement, ai_review_end: handleAIReviewJudgement, review_release: handleReviewRelease, + detached_plan_require: handleDetachedPlanReview, } as const diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index ec7eb7ccf4..ca34ce20e1 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -1,7 +1,7 @@ import type { AIMessageHandler } from '../type' -import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi, type AIOutputEvent } from '../grpcApi' +import { AIInputEventSyncTypeEnum, AITaskStatus, type AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { genBaseAIChatData, generateTaskNodeDataID, generateTaskNodeID, isValidTaskIndex } from '../utils' +import { genBaseAIChatData, generateTaskNodeDataID } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum, type ReportFinishCardData } from '../aiRender' import { convertNodeIdToVerbose } from '../defaultConstant' @@ -19,7 +19,7 @@ const handleThought: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -52,7 +52,7 @@ const handleResult: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -86,7 +86,7 @@ const handleFailReactTask: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -124,7 +124,7 @@ const handleToolCallDecision: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -158,7 +158,7 @@ const handleFailPlanAndExecution: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -191,7 +191,7 @@ const handleApiRequestFailed: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -249,7 +249,7 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -294,7 +294,7 @@ const handleReportFinish: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } @@ -324,65 +324,63 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { const info = data as AIAgentGrpcApi.ChangeTask const newPlanTree = store.getState().taskChat.plan newPlanTree.task_tree = newPlanTree.task_tree.map((item) => { - if (item.index === info.task.index) item.progress = AITaskStatus.inProgress + if (item.task_id === info.task.task_id) item.progress = AITaskStatus.inProgress return item }) store.getState().updatePlanTree(newPlanTree) - if (isValidTaskIndex(info.task.index) && meta.currentTaskPlanID?.taskID) { - const taskID = generateTaskNodeID(meta.currentTaskPlanID.taskID, info.task.index) - const chatDetail = rawData.contents.get(taskID) - if (chatDetail && chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - requestInfo.pushLog({ level: 'error', message: `${info.task.index}-push_task数据已存在` }) - return - } - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - id: taskID, - chatType: 'task', - type: AIChatQSDataTypeEnum.TASK_NODE_GROUP, - data: { - taskIndex: info.task.index, - taskId: info.task.task_id, - taskName: info.task.name, - goal: info.task.goal, - status: info.task.task_status || AITaskStatus.inProgress, - }, - } - rawData.contents.set(chatData.id, chatData) - meta.currentTaskPlanActiveNode.add(chatData.id) - // 这里要判断是否是最后一个默认任务组,直接添加不行 - // setElements((old) => { - // const exists = old.some((item) => item.token === chatData.id && item.type === chatData.type) - // if (exists) return old - // const last = old[old.length - 1] - // if (last.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP) { - // // 实时数据下,将默认任务聚合组置底 - // old.splice(old.length - 1, 0, { - // token: chatData.id, - // type: chatData.type, - // renderNum: 1, - // chatType: 'task', - // kind: 'task', - // children: [], - // }) - // return [...old] - // } - // return [ - // ...old, - // { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task', kind: 'task', children: [] }, - // ] - // }) - store.getState().dispatchStreamingNode({ - chatType: chatType, - node: { - token: chatData.id, - kind: 'task', - type: chatData.type, - isHistory: res.IsSync, - }, - }) + const taskID = info.task.task_id + const chatDetail = rawData.contents.get(taskID) + if (chatDetail && chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { + requestInfo.pushLog({ level: 'error', message: `${info.task.index}-push_task数据已存在` }) + return + } + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), + id: taskID, + chatType: 'task', + type: AIChatQSDataTypeEnum.TASK_NODE_GROUP, + data: { + taskIndex: info.task.index, + taskId: info.task.task_id, + taskName: info.task.name, + goal: info.task.goal, + status: info.task.task_status || AITaskStatus.inProgress, + }, } + rawData.contents.set(chatData.id, chatData) + meta.currentTaskPlanActiveNode.add(chatData.id) + // TODO - 这里要判断是否是最后一个默认任务组,直接添加不行 + // setElements((old) => { + // const exists = old.some((item) => item.token === chatData.id && item.type === chatData.type) + // if (exists) return old + // const last = old[old.length - 1] + // if (last.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP) { + // // 实时数据下,将默认任务聚合组置底 + // old.splice(old.length - 1, 0, { + // token: chatData.id, + // type: chatData.type, + // renderNum: 1, + // chatType: 'task', + // kind: 'task', + // children: [], + // }) + // return [...old] + // } + // return [ + // ...old, + // { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task', kind: 'task', children: [] }, + // ] + // }) + store.getState().dispatchStreamingNode({ + chatType: chatType, + node: { + token: chatData.id, + kind: 'task', + type: chatData.type, + isHistory: res.IsSync, + }, + }) } const handlePopTask: AIMessageHandler = (requestInfo) => { @@ -398,24 +396,22 @@ const handlePopTask: AIMessageHandler = (requestInfo) => { const info = data as AIAgentGrpcApi.ChangeTask const newPlanTree = store.getState().taskChat.plan newPlanTree.task_tree = newPlanTree.task_tree.map((item) => { - if (item.index === info.task.index) item.progress = info.task.task_status + if (item.task_id === info.task.task_id) item.progress = info.task.task_status return item }) store.getState().updatePlanTree(newPlanTree) - if (isValidTaskIndex(info.task.index) && meta.currentTaskPlanID?.taskID) { - const taskID = generateTaskNodeID(meta.currentTaskPlanID.taskID, info.task.index) - const chatDetail = rawData.contents.get(taskID) - if (!chatDetail || chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - requestInfo.pushLog({ level: 'error', message: `${info.task.index}-pop_task数据不存在` }) - return - } - meta.currentTaskPlanActiveNode.delete(info.task.index) - chatDetail.data.status = info.task.task_status - store.getState().incrementNodeVersion(chatDetail.id, 'task') - - sendRequest && sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) + const taskID = info.task.task_id + const chatDetail = rawData.contents.get(taskID) + if (!chatDetail || chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { + requestInfo.pushLog({ level: 'error', message: `${info.task.index}-pop_task数据不存在` }) + return } + meta.currentTaskPlanActiveNode.delete(chatDetail.id) + chatDetail.data.status = info.task.task_status + store.getState().incrementNodeVersion(chatDetail.id, 'task') + + sendRequest && sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) } /** 单条grpc流数据对应一个独立的UI数据 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index deb60c1e3c..46fc1fa0af 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -1,5 +1,5 @@ import type { AIMessageHandler } from '../type' -import type { AIAgentGrpcApi, AIOutputEvent } from '../grpcApi' +import type { AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { genBaseAIChatData, generateTaskNodeDataID, isToolStderrStream, isToolStdoutStream } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' @@ -70,7 +70,7 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), }) @@ -108,7 +108,7 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), }) @@ -402,7 +402,7 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts index 25abc58011..d84494cdc2 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts @@ -1,5 +1,5 @@ import type { AIMessageHandler } from '../type' -import type { AIAgentGrpcApi, AIOutputEvent } from '../grpcApi' +import type { AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { handleTodoListData } from '../utils' import cloneDeep from 'lodash/cloneDeep' @@ -164,6 +164,7 @@ const handleCurrentTaskTodoListUpdate: AIMessageHandler = (requestInfo) => { } else if (chatType === 'reAct') { const chatDetail = rawData.casualChat?.planDetails if (!chatDetail) return + chatDetail.uuid = uuidv4() chatDetail.taskId = chatDetail.taskId || res.TaskId chatDetail.todoList = newData store.getState().updateCasualTodoList() @@ -183,12 +184,14 @@ const handleSessionSnapshot: AIMessageHandler = (requestInfo) => { oldData.taskId = oldData?.taskId || res.TaskId oldData.uuid = uuidv4() oldData.execution = snapshot.execution + oldData.backgroundProcesses = snapshot.background_processes rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) } else if (chatType === 'reAct') { const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) chatDetail.uuid = uuidv4() chatDetail.taskId = chatDetail.taskId || res.TaskId chatDetail.execution = snapshot.execution + chatDetail.backgroundProcesses = snapshot.background_processes rawData.casualChat.planDetails = chatDetail } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts index c5711088ec..715c79a043 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -35,7 +35,7 @@ const handleToolCallStart: AIMessageHandler = (requestInfo) => { taskIndex: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskIndex, + taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), }) @@ -176,6 +176,8 @@ const handleToolCallResult: AIMessageHandler = (requestInfo) => { toolResult.data.durationMS = rest.duration_ms || 0 toolResult.data.durationSeconds = rest.duration_seconds || 0 toolResult.data.tool.status = status as 'user_cancelled' | 'success' | 'failed' | 'default' + /** 触发这个函数说明状态一定不是 processing_params */ + toolResult.data.isProcessingParams = false // 设置总结内容,没有就设置成获取中,有就使用获取到的内容 toolResult.data.tool.summary = toolResult.data.tool.summary || DefaultToolResultSummary[status]?.wait || '' @@ -231,6 +233,58 @@ const handleToolCallSummary: AIMessageHandler = (requestInfo) => { } } +const handleToolCallStatus: AIMessageHandler = (requestInfo) => { + const { res, store, rawData } = requestInfo + if (res.Type !== 'tool_call_status') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { call_tool_id, status } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall + if (!call_tool_id) { + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) + return + } + + const toolResult = rawData.contents.get(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + requestInfo.pushLog({ + level: 'error', + message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + }) + return + } + + const isProcessingParams = status === 'processing_params' + if (toolResult.data.isProcessingParams === isProcessingParams) return + + toolResult.data.isProcessingParams = status === 'processing_params' + store.getState().incrementNodeVersion(toolResult.id, 'item') +} + +const handleToolCallReason: AIMessageHandler = (requestInfo) => { + const { res, store, rawData } = requestInfo + if (res.Type !== 'tool_call_reason') return + + const ipcContent = Uint8ArrayToString(res.Content) || '' + const { call_tool_id, reason } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall + + if (!call_tool_id) { + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) + return + } + + const toolResult = rawData.contents.get(call_tool_id) + if (!toolResult || toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) { + requestInfo.pushLog({ + level: 'error', + message: `${res.Type}数据(call_tool_id:${call_tool_id}), 没有对应的tool_call_start类型初始化`, + }) + return + } + + toolResult.data.tool.reason = reason || '' + if (toolResult.data.type !== 'create') store.getState().incrementNodeVersion(toolResult.id, 'item') +} + export const aiToolResultDataHandlers = { tool_call_start: handleToolCallStart, tool_call_param: handleToolCallParam, @@ -240,4 +294,6 @@ export const aiToolResultDataHandlers = { tool_call_done: handleToolCallResult, tool_call_error: handleToolCallResult, tool_call_summary: handleToolCallSummary, + tool_call_status: handleToolCallStatus, + tool_call_reason: handleToolCallReason, } as const diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts index 6297340ade..45c195af26 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts @@ -20,10 +20,6 @@ import cloneDeep from 'lodash/cloneDeep' import { DefaultPlanLoadingStatus } from './defaultConstant' import { ChatMultiSessionController } from './ChatMultiSessionController' -/** 生成任务节点的唯一ID */ -export const generateTaskNodeID = (planID: string, taskID: string) => { - return `${planID}-${taskID || 'unknown'}` -} /** * 任务节点内的数据生成任务节点ID * @param isExist 生成的任务节点是否已经存在,不存在则不是任务节点数据,归为默认节点内的数据 @@ -31,15 +27,19 @@ export const generateTaskNodeID = (planID: string, taskID: string) => { export const generateTaskNodeDataID = (params: { chatType: ChatListRenderType planID?: string - taskID: AIOutputEvent['TaskIndex'] + taskID: AIOutputEvent['TaskId'] isExist: (key: string) => boolean }) => { const { chatType, planID, taskID, isExist } = params - // 自由对话或者没有任务规划ID,则不生成任务节点ID - if (chatType === 'reAct' || !planID) return undefined - const keyLabel = `${planID}-${taskID || 'unknown'}` - if (isExist(keyLabel)) return keyLabel - return `${planID}-unknown` + + // 不管自由对话还是任务规划, 只要是明确的执行任务组,都会有唯一的taskID值 + if (taskID && isExist(taskID)) return taskID + // 任务规划的默认任务组 + if (chatType === 'task' && planID) { + const defaultKey = `${planID}-default` + if (isExist(defaultKey)) return defaultKey + } + return undefined } /** 生成AI-UI展示的必须基础数据 */ From 53065b88c1f5cb3929fbd7a8b730e167864baf1d Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Mon, 6 Jul 2026 16:16:49 +0800 Subject: [PATCH 020/101] =?UTF-8?q?feat:=E8=87=AA=E7=94=B1=E5=AF=B9?= =?UTF-8?q?=E8=AF=9D=E6=8A=BD=E7=A6=BBheader/content/footer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AIContextToken/AIContextToken.tsx | 14 +- .../AIContextToken/ContextCostPanel.tsx | 8 +- .../AIContextToken/ContextDetailPopover.tsx | 11 +- .../AIContextToken/ContextPressurePanel.tsx | 11 +- .../AIContextToken/ContextTokenSummary.tsx | 12 +- .../AIContextToken/useContextPerfStore.ts | 5 +- .../AIHorizontalScrollCard.module.scss | 2 +- .../AIHorizontalScrollCard.tsx | 5 +- .../components/aiTaskQuery/AITaskQuery.tsx | 8 +- .../ai-agent/historyChat/HistoryChat.tsx | 4 +- .../aiReActChat/AIReActChat.module.scss | 165 ------------------ .../ai-re-act/aiReActChat/AIReActChat.tsx | 73 +++----- .../ai-re-act/aiReActChat/AIReActChatType.ts | 5 +- .../AIReActChatHeader.module.scss | 153 ++++++++++++++++ .../aiReActChatHeader/AIReActChatHeader.tsx | 151 ++++++++++++++++ .../aiReActChat/aiReActChatHeader/type.ts | 17 ++ .../AIReactChatTextarea.module.scss | 0 .../AIReactChatTextarea.tsx | 47 +++++ .../aiReActChat/aiReactChatTextarea/type.ts | 9 + .../AIToDoListWrapper.module.scss | 7 + .../aiToDoListWrapper/AIToDoListWrapper.tsx | 38 ++++ .../aiReActChat/aiToDoListWrapper/type.ts | 1 + .../ai-re-act/hooks/useGetChatDataStoreKey.ts | 15 -- .../main/src/pages/fuzzer/HTTPFuzzerPage.tsx | 3 - 24 files changed, 503 insertions(+), 261 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx create mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.tsx create mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.tsx create mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/type.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken.tsx index 52acaa8fc5..5cb8d99aac 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken.tsx @@ -1,4 +1,4 @@ -import React, { FC, memo } from 'react' +import type { FC, memo } from 'react' import ContextPressurePanel from './ContextPressurePanel' import ContextCostPanel from './ContextCostPanel' import ContextTokenSummary from './ContextTokenSummary' @@ -7,19 +7,17 @@ import { YakitButtonProp } from '@/components/yakitUI/YakitButton/YakitButton' import styles from '../AIChatContent.module.scss' const AIContextToken: FC<{ - session?: string - execute: boolean /** 仅展示详情 icon(嵌入 AI 侧栏 header 使用) */ iconOnly?: boolean /** 详情 icon 对应 YakitButton 的 props */ buttonProps?: Omit -}> = ({ session, execute, iconOnly, buttonProps }) => { +}> = ({ iconOnly, buttonProps }) => { return ( <> - {!iconOnly && } - {!iconOnly && } - {!iconOnly && } - + {!iconOnly && } + {!iconOnly && } + {!iconOnly && } + {!iconOnly &&
} ) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextCostPanel.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextCostPanel.tsx index c26ad5667b..2d6b9cef0b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextCostPanel.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextCostPanel.tsx @@ -9,9 +9,15 @@ import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { useRafPolling } from '@/hook/useRafPolling/useRafPolling' import { CONTEXT_PERF_POLL_INTERVAL, ContextPerfPanelProps, useContextPerfStore } from './useContextPerfStore' import styles from '../AIChatContent.module.scss' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' -const ContextCostPanel: React.FC = ({ execute }) => { +const ContextCostPanel: React.FC = () => { const { t } = useI18nNamespaces(['aiAgent']) + + const store = useCurrentStore() + const execute = useStore(store, (state) => state.execute) + const aiPerfData = useContextPerfStore() const { renderNumber, aiDataRef: firstCost } = useRafPolling({ diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextDetailPopover.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextDetailPopover.tsx index 91a59e52b8..6758f3d958 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextDetailPopover.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextDetailPopover.tsx @@ -1,6 +1,5 @@ import React, { memo, useCallback, useMemo, useState } from 'react' import { useCreation } from 'ahooks' -import { cloneDeep, isEmpty } from 'lodash' import { OutlinePresentationchartlineIcon } from '@/assets/icon/outline' import { Tooltip } from 'antd' import { YakitButton, YakitButtonProp } from '@/components/yakitUI/YakitButton/YakitButton' @@ -13,14 +12,22 @@ import { isPerfDataChanged } from './utils' import { CONTEXT_PERF_POLL_INTERVAL, ContextPerfPanelProps, useContextPerfStore } from './useContextPerfStore' import AIEchartsDetails from './AIEchartsDetails' import styles from '../AIChatContent.module.scss' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import cloneDeep from 'lodash/cloneDeep' +import isEmpty from 'lodash/isEmpty' interface ContextDetailPopoverProps extends ContextPerfPanelProps { buttonProps?: Omit } -const ContextDetailPopover: React.FC = ({ session, execute, buttonProps }) => { +const ContextDetailPopover: React.FC = ({ buttonProps }) => { const { t } = useI18nNamespaces(['yakitUi']) const [visible, setVisible] = useState(false) + + const store = useCurrentStore() + const execute = useStore(store, (state) => state.execute) + const aiPerfData = useContextPerfStore() const { renderNumber, aiDataRef: perfData } = useRafPolling({ diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextPressurePanel.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextPressurePanel.tsx index 6b8ab0e4cd..fabf659b39 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextPressurePanel.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextPressurePanel.tsx @@ -1,6 +1,4 @@ import React, { memo } from 'react' -import { useCreation } from 'ahooks' -import { cloneDeep } from 'lodash' import { ContextPressureEcharts } from '../../chatTemplate/AIEcharts' import { formatNumberUnits } from '../../utils' import { Tooltip } from 'antd' @@ -10,10 +8,17 @@ import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { useRafPolling } from '@/hook/useRafPolling/useRafPolling' import { CONTEXT_PERF_POLL_INTERVAL, ContextPerfPanelProps, useContextPerfStore } from './useContextPerfStore' import styles from '../AIChatContent.module.scss' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import cloneDeep from 'lodash/cloneDeep' +import useCreation from 'ahooks/lib/useCreation' -const ContextPressurePanel: React.FC = ({ session, execute }) => { +const ContextPressurePanel: React.FC = () => { const { t } = useI18nNamespaces(['aiAgent']) + const store = useCurrentStore() + const execute = useStore(store, (state) => state.execute) + const getPerfData = useContextPerfStore() const { renderNumber, aiDataRef: pressure } = useRafPolling({ diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextTokenSummary.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextTokenSummary.tsx index 1a1c5b27c9..3d85fefcc1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextTokenSummary.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/ContextTokenSummary.tsx @@ -1,6 +1,4 @@ import React, { memo } from 'react' -import { useCreation } from 'ahooks' -import { cloneDeep, isEmpty } from 'lodash' import { formatNumberUnits } from '../../utils' import { OutlineArrowdownIcon, OutlineArrowupIcon } from '@/assets/icon/outline' import classNames from 'classnames' @@ -9,8 +7,16 @@ import { useRafPolling } from '@/hook/useRafPolling/useRafPolling' import { CONTEXT_PERF_POLL_INTERVAL, ContextPerfPanelProps, useContextPerfStore } from './useContextPerfStore' import { AIAgentGrpcApi } from '@/pages/ai-re-act/hooks/grpcApi' import styles from '../AIChatContent.module.scss' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import useCreation from 'ahooks/lib/useCreation' +import cloneDeep from 'lodash/cloneDeep' +import isEmpty from 'lodash/isEmpty' + +const ContextTokenSummary: React.FC = () => { + const store = useCurrentStore() + const execute = useStore(store, (state) => state.execute) -const ContextTokenSummary: React.FC = ({ session, execute }) => { const getPerfData = useContextPerfStore() const { renderNumber, aiDataRef: consumption } = useRafPolling({ diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts index 19574a6fe8..d73279ab1f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/useContextPerfStore.ts @@ -2,10 +2,7 @@ import { useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySessi export const CONTEXT_PERF_POLL_INTERVAL = 2000 -export interface ContextPerfPanelProps { - session?: string - execute: boolean -} +export interface ContextPerfPanelProps {} /** 读取当前 session 的性能数据 */ export function useContextPerfStore() { diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss index 92506c11db..735c121c4b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss @@ -1,4 +1,4 @@ -@use '../../../styles/mixin.scss' as mixin; +@use '../../../../styles/mixin.scss' as mixin; .expand-retract-wrapper { width: 100%; diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.tsx index 6b3ae4d647..3a51d9d44d 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.tsx @@ -10,7 +10,6 @@ import { OutlinePlussmIcon, OutlineNewspaperIcon, OutlineClouddownloadIcon } fro import { SolidChatalt2Icon } from '@/assets/icon/solid' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { ExpandAndRetract } from '@/pages/plugins/operator/expandAndRetract/ExpandAndRetract' -import { useMemoizedFn } from 'ahooks' import { Divider } from 'antd' import { onNewChat } from '../../historyChat/HistoryChat' import AIContextToken from '../AIContextToken/AIContextToken' @@ -19,6 +18,7 @@ import useAiChatLog from '@/hook/useAiChatLog/useAiChatLog.ts' import { ExportAILogsModal } from '../../components/ExportAILogsModal/ExportAILogsModal' import { failed, yakitNotify } from '@/utils/notification' import { grpcExportAILogs } from '../../grpc' +import useMemoizedFn from 'ahooks/lib/useMemoizedFn' export const AIHorizontalScrollCard: React.FC = React.memo((props) => { const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) @@ -33,7 +33,6 @@ export const AIHorizontalScrollCard: React.FC = Rea const store = useCurrentStore() const yakExecResultCard = useStore(store, (state) => state.card) - const execute = useStore(store, (state) => state.execute) const onExpand = useMemoizedFn((e) => { e.stopPropagation() @@ -102,7 +101,7 @@ export const AIHorizontalScrollCard: React.FC = Rea
- + } onClick={onOpenLog}> {t('AIChatContent.log')} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx index f5dc760c58..61b41689b8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx @@ -11,7 +11,6 @@ import { } from '@/assets/icon/outline' import styles from './AITaskQuery.module.scss' import { YakitTag } from '@/components/yakitUI/YakitTag/YakitTag' -import { useDebounceFn, useMemoizedFn } from 'ahooks' import { AIInputEvent, AIInputEventSyncTypeEnum } from '@/pages/ai-re-act/hooks/grpcApi' import { Tooltip } from 'antd' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' @@ -20,8 +19,9 @@ import { useStore } from 'zustand' import useAIAgentDispatcher from '../../useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' +import useMemoizedFn from 'ahooks/lib/useMemoizedFn' -export const AITaskQuery: React.FC = React.memo((props) => { +export const AITaskQuery: React.FC = React.memo(() => { const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const [loading, setLoading] = useState(false) @@ -62,7 +62,7 @@ export const AITaskQuery: React.FC = React.memo((props) => { setShowList(false) }, 500) }) - return ( + return execute && questionQueue?.total > 0 ? (
{showList ? (
@@ -100,6 +100,8 @@ export const AITaskQuery: React.FC = React.memo((props) => { )}
+ ) : ( + <> ) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx index d352760251..cacd614dd0 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx @@ -132,7 +132,7 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { setClearLoading(true) try { - const source = getSetting().Source + const source = getSetting().Source || 'ai' const filter = isGlobalAIAgentHistory ? { DeleteAll: true } : { Filter: { Source: historyQuerySources } } await handAIHistoryChatRemove({ grpcDeleteAISessionParams: filter, @@ -180,7 +180,7 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { Source: historyQuerySources, } const sessionIds = sessions.map((item) => item.SessionID) - const source = getSetting().Source + const source = getSetting().Source || 'ai' await handAIHistoryChatRemove({ grpcDeleteAISessionParams: { Filter: filter }, handleClearAIImageParams: { chatDataStoreKey: getImageStoreKeyByAISource(source), sessionID: sessionIds }, diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.module.scss b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.module.scss index 14fc82295a..1faca87a6f 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.module.scss +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.module.scss @@ -88,10 +88,6 @@ gap: 8px; background: var(--Colors-Use-Basic-Background); .chat-container { - .chat-header { - padding: 12px 8px 12px 0px; - margin-left: 8px; - } } } .ai-re-act-chat-hidden { @@ -103,111 +99,6 @@ overflow: hidden; height: 100%; @extend %display-column-center; - .chat-header { - @extend %display-flex-center; - // position: relative; - // min-width: 0; - gap: 12px; - padding: 12px 8px; - height: 48px; - .chat-header-title { - color: var(--Colors-Use-Neutral-Text-1-Title); - font-size: 14px; - font-weight: 500; - line-height: 20px; - letter-spacing: 0.1px; - @extend %display-flex-center; - gap: 4px; - flex: 1; - min-width: 0; - overflow: hidden; - .chat-header-title-text { - flex-shrink: 0; - } - .chat-header-focus-mode { - overflow: hidden; - .text { - min-width: 0; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex-shrink: 1; - } - } - } - .chat-header-extra { - @extend %display-flex-center; - justify-content: flex-end; - gap: 8px; - flex-shrink: 0; - // position: absolute; - // right: 8px; - // top: 0; - // bottom: 0; - // z-index: 2; - // padding-left: 12px; - background: linear-gradient( - to right, - transparent, - var(--Colors-Use-Basic-Background) 12px, - var(--Colors-Use-Basic-Background) - ); - .info-token { - display: flex; - align-items: center; - gap: 4px; - color: var(--Colors-Use-Neutral-Text-1-Title); - font-size: 12px; - font-weight: 400; - } - .token-tag { - height: 16px; - padding: 2px 6px; - display: flex; - align-items: center; - gap: 4px; - border-radius: 2px; - svg { - width: 12px; - height: 12px; - } - } - .upload-token { - background: var(--Colors-Use-Error-Bg-Hover); - color: var(--Colors-Use-Error-Primary); - svg { - color: var(--Colors-Use-Error-Primary); - } - } - .download-token { - background: var(--Colors-Use-Neutral-Bg-Hover); - color: var(--Colors-Use-Neutral-Text-3-Secondary); - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - } - - .divider-style { - width: 1px; - height: 12px; - border-left: 1px solid var(--Colors-Use-Neutral-Border); - } - .info-time { - color: var(--Colors-Use-Neutral-Text-1-Title); - font-size: 12px; - font-weight: 400; - line-height: 16px; - } - } - } - .todoList-wrapper { - height: 33px; - .to-do-list { - margin-left: 4px; - margin-right: 12px; - } - } } .chat-footer { @@ -349,53 +240,6 @@ transform: translateX(-50%); } } - -.history-chat-tooltip { - max-width: none; - - &:global(.ant-tooltip-placement-top), - &:global(.ant-tooltip-placement-topLeft), - &:global(.ant-tooltip-placement-topRight) { - padding-bottom: 4px; - - :global(.ant-tooltip-arrow) { - bottom: 1px; - } - } - - &:global(.ant-tooltip-placement-bottom), - &:global(.ant-tooltip-placement-bottomLeft), - &:global(.ant-tooltip-placement-bottomRight) { - padding-top: 4px; - - :global(.ant-tooltip-arrow) { - top: 1px; - } - } - - :global { - .ant-tooltip-inner { - padding: 0; - display: block; - min-height: unset; - background: var(--Colors-Use-Basic-Background); - border: 1px solid var(--Colors-Use-Neutral-Border); - color: inherit; - box-shadow: 0 4px 16px var(--Colors-Use-Basic-Shadow); - overflow: hidden; - } - - .ant-tooltip-arrow-content { - border: 1px solid var(--Colors-Use-Neutral-Border); - background-color: var(--Colors-Use-Basic-Background); - box-shadow: 0 4px 16px var(--Colors-Use-Basic-Shadow); - } - - .ant-tooltip-arrow-content::before { - opacity: 0; - } - } -} .chat-locate-popover { :global { .ant-popover-inner-content { @@ -435,12 +279,3 @@ } } } - -.history-chat-tooltip-content { - width: 280px; - height: 300px; - display: flex; - flex-direction: column; - min-height: 0; - overflow: hidden; -} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index 7434313fb4..f770ba599b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -2,33 +2,24 @@ import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react import styles from './AIReActChat.module.scss' import { AIHandleStartResProps, AINotifyMessageProps, AIReActChatProps, AISendResProps } from './AIReActChatType' -import { AIChatTextarea } from '@/pages/ai-agent/template/template' import { AIReActChatContents } from '../aiReActChatContents/AIReActChatContents' import type { AIReActChatContentsRef } from '../aiReActChatContents/AIReActChatContentsType' import { AIChatTextareaRefProps, AIChatTextareaSubmit } from '@/pages/ai-agent/template/type' import { useControllableValue, useCreation, useMemoizedFn } from 'ahooks' import { yakitNotify } from '@/utils/notification' -import { ColorsChatIcon } from '@/assets/icon/colors' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import classNames from 'classnames' -import { ChevrondownButton, ChevronleftButton, RoundedStopButton } from './AIReActComponent' -import { Tooltip } from 'antd' -import { ClockIcon } from '@/assets/newIcon' -import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' -import HistoryChat from '@/pages/ai-agent/historyChat/HistoryChat' +import { ChevrondownButton } from './AIReActComponent' import { AIInputEvent, AIInputEventSyncTypeEnum, AISourceEnum, AIStartParams } from '../hooks/grpcApi' import { AITaskQuery } from '@/pages/ai-agent/components/aiTaskQuery/AITaskQuery' import { HandleStartParams } from '@/pages/ai-agent/aiAgentChat/type' import { formatAIAgentSetting, getAIReActRequestParams } from '@/pages/ai-agent/utils' -import { YakitTag } from '@/components/yakitUI/YakitTag/YakitTag' import { AISession } from '@/pages/ai-agent/type/aiChat' import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' import useAINodeLabel from '../hooks/useAINodeLabel' import useSessionId from '../hooks/useSessionId' -import useGetChatDataStoreKey from '../hooks/useGetChatDataStoreKey' import emiter from '@/utils/eventBus/eventBus' -import { omit } from 'lodash' import AIContextToken from '@/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken' import { AIToDoList } from './aiToDoList/AIToDoList' import { cloneDeep } from 'lodash' @@ -38,9 +29,10 @@ import { OutlineLandPlotIcon, OutlineListTodoIcon } from '@/assets/icon/outline' import TaskDetailsPopover from '@/components/historyAIReActChat/TaskDetailsPopover' import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' import { SolidChatIcon } from '@/assets/icon/solid' -import { useCurrentMeta, useCurrentRawData, useCurrentStore } from '../hooks/useCurrentDataBySession' +import { useCurrentMeta, useCurrentStore } from '../hooks/useCurrentDataBySession' import { useStore } from 'zustand' import useCurrentSessionId from '../hooks/useCurrentSessionId' +import { AIReactChatTextarea } from './aiReactChatTextarea/AIReactChatTextarea' export const AIReActChat: React.FC = React.memo( forwardRef((props, ref) => { @@ -62,7 +54,6 @@ export const AIReActChat: React.FC = React.memo( const sessionId = useCurrentSessionId() const store = useCurrentStore() - const rawData = useCurrentRawData() const meta = useCurrentMeta() const execute = useStore(store, (state) => state.execute) const focusMode = useStore(store, (state) => state.focusMode) @@ -84,14 +75,6 @@ export const AIReActChat: React.FC = React.memo( const { activeChat, setting } = useAIAgentStore() const { getSession } = useSessionId() - const source = useCreation(() => { - return setting.Source - }, [setting.Source]) - - const contextTokenSession = useCreation(() => { - return activeChat?.SessionID || setting.TimelineSessionID - }, [activeChat?.SessionID, setting.TimelineSessionID]) - const aiChatTextareaRef = useRef({ setMention: () => {}, setValue: () => {}, @@ -131,7 +114,7 @@ export const AIReActChat: React.FC = React.memo( yakitNotify('error', '请先配置 AI ReAct 参数') return } - if (execute) { + if (store.getState().execute) { handleSend(value) } else { handleStart(value) @@ -287,7 +270,7 @@ export const AIReActChat: React.FC = React.memo( const handleStopCasualTask = useMemoizedFn(() => { const currentCasualTaskID = meta.currentCasualTaskID - if (!execute || !currentCasualTaskID) return + if (!store.getState().execute || !currentCasualTaskID) return store.getState().updateState({ cancelCasualLoading: true, @@ -518,31 +501,27 @@ export const AIReActChat: React.FC = React.memo(
)} + {/* + + */}
- {execute && questionQueue?.total > 0 && } - {execute && notifyMessage?.content && } - + +
- - {casualLoading && ( - - )} -
- } - chatDataStoreKey={chatDataStoreKey} - {...omit(externalParameters, 'rightIcon')} + handleSubmit={handleSubmit} + externalParameters={externalParameters} + handleStopCasualTask={handleStopCasualTask} />
@@ -559,10 +538,14 @@ export const AIReActChat: React.FC = React.memo( }), ) -const AINotifyMessage: React.FC = React.memo((props) => { - const { notifyMessage } = props +const AINotifyMessage: React.FC = React.memo(() => { + const store = useCurrentStore() + const execute = useStore(store, (state) => state.execute) + const notifyMessage = useStore(store, (state) => state.notifyMessage) + const { nodeLabel } = useAINodeLabel(notifyMessage?.label) - return ( + + return execute && notifyMessage?.content ? (
{nodeLabel}
@@ -574,5 +557,7 @@ const AINotifyMessage: React.FC = React.memo((props) => {
+ ) : ( + <> ) }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts index a937d607d4..d64f63654d 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts @@ -3,7 +3,6 @@ import { AIChatQSData } from '../hooks/aiRender' import { AIInputEvent } from '../hooks/grpcApi' import React from 'react' import { AIChatTextareaProps, AIChatTextareaRefProps } from '@/pages/ai-agent/template/type' -import { UseChatIPCState } from '../hooks/type' import { YakitButtonProp } from '@/components/yakitUI/YakitButton/YakitButton' export type DataDetailsButtonProps = Omit @@ -67,9 +66,7 @@ export interface AIReActChatProps { } } -export interface AINotifyMessageProps { - notifyMessage: UseChatIPCState['notifyMessage'] -} +export interface AINotifyMessageProps {} export interface AIReActLogProps { logs: AIChatQSData[] setLogVisible: (visible: boolean) => void diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss new file mode 100644 index 0000000000..c32c12333b --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss @@ -0,0 +1,153 @@ +%display-flex-center { + display: flex; + align-items: center; +} +.chat-header { + @extend %display-flex-center; + // position: relative; + // min-width: 0; + gap: 12px; + padding: 12px 8px 12px 0px; + margin-left: 8px; + height: 48px; + .chat-header-title { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.1px; + @extend %display-flex-center; + gap: 4px; + flex: 1; + min-width: 0; + overflow: hidden; + .chat-header-title-text { + flex-shrink: 0; + } + .chat-header-focus-mode { + overflow: hidden; + .text { + min-width: 0; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 1; + } + } + } + .chat-header-extra { + @extend %display-flex-center; + justify-content: flex-end; + gap: 8px; + flex-shrink: 0; + background: linear-gradient( + to right, + transparent, + var(--Colors-Use-Basic-Background) 12px, + var(--Colors-Use-Basic-Background) + ); + .info-token { + display: flex; + align-items: center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + } + .token-tag { + height: 16px; + padding: 2px 6px; + display: flex; + align-items: center; + gap: 4px; + border-radius: 2px; + svg { + width: 12px; + height: 12px; + } + } + .upload-token { + background: var(--Colors-Use-Error-Bg-Hover); + color: var(--Colors-Use-Error-Primary); + svg { + color: var(--Colors-Use-Error-Primary); + } + } + .download-token { + background: var(--Colors-Use-Neutral-Bg-Hover); + color: var(--Colors-Use-Neutral-Text-3-Secondary); + svg { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } + } + + .divider-style { + width: 1px; + height: 12px; + border-left: 1px solid var(--Colors-Use-Neutral-Border); + } + .info-time { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + line-height: 16px; + } + } +} + +.history-chat-tooltip { + max-width: none; + + &:global(.ant-tooltip-placement-top), + &:global(.ant-tooltip-placement-topLeft), + &:global(.ant-tooltip-placement-topRight) { + padding-bottom: 4px; + + :global(.ant-tooltip-arrow) { + bottom: 1px; + } + } + + &:global(.ant-tooltip-placement-bottom), + &:global(.ant-tooltip-placement-bottomLeft), + &:global(.ant-tooltip-placement-bottomRight) { + padding-top: 4px; + + :global(.ant-tooltip-arrow) { + top: 1px; + } + } + + :global { + .ant-tooltip-inner { + padding: 0; + display: block; + min-height: unset; + background: var(--Colors-Use-Basic-Background); + border: 1px solid var(--Colors-Use-Neutral-Border); + color: inherit; + box-shadow: 0 4px 16px var(--Colors-Use-Basic-Shadow); + overflow: hidden; + } + + .ant-tooltip-arrow-content { + border: 1px solid var(--Colors-Use-Neutral-Border); + background-color: var(--Colors-Use-Basic-Background); + box-shadow: 0 4px 16px var(--Colors-Use-Basic-Shadow); + } + + .ant-tooltip-arrow-content::before { + opacity: 0; + } + } +} + +.history-chat-tooltip-content { + width: 280px; + height: 300px; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx new file mode 100644 index 0000000000..3dab71af83 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx @@ -0,0 +1,151 @@ +import React from 'react' +import classNames from 'classnames' +import { Tooltip } from 'antd' +import { useStore } from 'zustand' + +import styles from '../AIReActChatHeader.module.scss' +import { ColorsChatIcon } from '@/assets/icon/colors' +import { YakitTag } from '@/components/yakitUI/YakitTag/YakitTag' +import TaskDetailsPopover from '@/components/historyAIReActChat/TaskDetailsPopover' +import AIContextToken from '@/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken' +import HistoryChat from '@/pages/ai-agent/historyChat/HistoryChat' +import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' +import { ClockIcon } from '@/assets/newIcon' +import { OutlineListTodoIcon } from '@/assets/icon/outline' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { AIReActChatHeaderExternalRightIconProps, AIReActChatHeaderProps } from './type' +import { ChevronleftButton } from '../AIReActComponent' +import useMemoizedFn from 'ahooks/lib/useMemoizedFn' +import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' +import useCreation from 'ahooks/lib/useCreation' +import { yakitNotify } from '@/utils/notification' +import emiter from '@/utils/eventBus/eventBus' +import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' +import { AISource } from '../../hooks/grpcApi' + +export const AIReActChatHeader: React.FC = React.memo((props) => { + const { title, chatContainerHeaderClassName, isShowRetract, externalParameters, handleSwitchShowFreeChat } = props + + const { activeChat } = useAIAgentStore() + const { getSetting } = useAIAgentDispatcher() + + // 内部订阅 Store 数据 + const store = useCurrentStore() + const rawData = useCurrentRawData() + const focusMode = useStore(store, (state) => state.focusMode) + const casualLoading = useStore(store, (state) => state.casualLoading) + const todoListUpdate = useStore(store, (state) => state.casualChat?.todoListUpdate) + + const taskId = useCreation(() => { + if (!activeChat?.SessionID) return '' + try { + return rawData.casualChat.planDetails?.taskId || '' + } catch (error) { + return '' + } + }, [todoListUpdate, activeChat?.SessionID, casualLoading]) + + const onDetails = useMemoizedFn((e) => { + e.stopPropagation() + if (!taskId) { + yakitNotify('error', 'taskId不存在') + return + } + if (getSetting()?.Source === 'ai') { + emiter.emit( + 'actionAITaskContentTab', + JSON.stringify({ + type: 'add', + params: { + key: taskId, + label: '自由对话', + goal: '', + }, + }), + ) + } else { + yakitNotify('info', '当前会话不属于 AIAgent 数据源,无法查看任务详情') + } + }) + + return ( +
+
+ + {title} + {focusMode && ( + + 场景:{focusMode} + + )} +
+
+ {isShowRetract ? ( + + ) : ( + <> + {casualLoading && ( + } onClick={onDetails}> + 任务详情 + + )} + handleSwitchShowFreeChat(false)} /> + + )} +
+
+ ) +}) + +const AIReActChatHeaderExternalRightIcon: React.FC = React.memo((props) => { + const { rightIcon } = props + + const store = useCurrentStore() + const casualLoading = useStore(store, (state) => state.casualLoading) + + const { setting } = useAIAgentStore() + + const aiSource: AISource[] = useCreation(() => { + switch (setting?.Source) { + case 'flow': + return ['flow'] + case 'knowledgeBase': + return ['knowledgeBase'] + case 'webFuzzer': + return ['webFuzzer'] + case 'irify': + return ['irify'] + case 'yakRunner': + return ['yakRunner'] + default: + return ['ai', ''] // AI Agent 侧栏历史会话:包含 ai 与兼容老数据的空 source + } + }, [setting?.Source]) + + return !!rightIcon ? ( + <> + {casualLoading && rightIcon.taskDetails && } + {rightIcon.dataDetails && ( + + )} + {rightIcon.history && ( + + +
+ } + > + } title="" /> + + )} + {rightIcon.add} + {rightIcon.close} + + ) : ( + <> + ) +}) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/type.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/type.ts new file mode 100644 index 0000000000..ac3d589f54 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/type.ts @@ -0,0 +1,17 @@ +import { AIReActChatProps } from '../AIReActChatType' +import React from 'react' + +export interface AIReActChatHeaderProps { + title?: React.ReactNode + chatContainerHeaderClassName?: string + isShowRetract?: boolean + externalParameters?: AIReActChatProps['externalParameters'] + + source?: string + onDetails?: (e: React.MouseEvent) => void + handleSwitchShowFreeChat: (show: boolean) => void +} + +export interface AIReActChatHeaderExternalRightIconProps { + rightIcon?: NonNullable['rightIcon'] +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.module.scss b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.module.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.tsx new file mode 100644 index 0000000000..c9bd601119 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.tsx @@ -0,0 +1,47 @@ +import React, { forwardRef } from 'react' +import { AIReactChatTextareaProps } from './type' +import { AIChatTextarea } from '@/pages/ai-agent/template/template' +import styles from '../AIReActChat.module.scss' +import { RoundedStopButton } from '../AIReActComponent' +import omit from 'lodash/omit' +import useCreation from 'ahooks/lib/useCreation' +import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' +import { useStore } from 'zustand' +import { useCurrentStore } from '../../hooks/useCurrentDataBySession' +import { getImageStoreKeyByAISource } from '../../hooks/useGetChatDataStoreKey' + +export const AIReactChatTextarea: React.FC = React.memo( + forwardRef((props, ref) => { + const { handleSubmit, externalParameters, handleStopCasualTask } = props + + const { setting } = useAIAgentStore() + + const store = useCurrentStore() + const cancelCasualLoading = useStore(store, (state) => state.cancelCasualLoading) + const casualLoading = useStore(store, (state) => state.casualLoading) + + const chatDataStoreKey = useCreation(() => { + return getImageStoreKeyByAISource(setting.Source || 'ai') + }, [setting.Source]) + return ( + + {casualLoading && ( + + )} +
+ } + chatDataStoreKey={chatDataStoreKey} + {...omit(externalParameters, 'rightIcon')} + /> + ) + }), +) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/type.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/type.ts new file mode 100644 index 0000000000..744795bfd5 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/type.ts @@ -0,0 +1,9 @@ +import { AIChatTextareaRefProps, AIChatTextareaSubmit } from '@/pages/ai-agent/template/type' +import { AIReActChatProps } from '../AIReActChatType' + +export interface AIReactChatTextareaProps { + ref?: React.ForwardedRef + handleSubmit: (v: AIChatTextareaSubmit) => void + externalParameters: AIReActChatProps['externalParameters'] + handleStopCasualTask: () => void +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.module.scss b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.module.scss new file mode 100644 index 0000000000..eb3a06b360 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.module.scss @@ -0,0 +1,7 @@ +.todoList-wrapper { + height: 33px; + .to-do-list { + margin-left: 4px; + margin-right: 12px; + } +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.tsx new file mode 100644 index 0000000000..76e4036566 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import { AIToDoListWrapperProps } from './type' +import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' +import { useCurrentRawData, useCurrentStore } from '../../hooks/useCurrentDataBySession' +import useCreation from 'ahooks/lib/useCreation' +import cloneDeep from 'lodash/cloneDeep' +import { useStore } from 'zustand' +import { TodoListCardData } from '../../hooks/aiRender' +import { DefaultTodoListCardData } from '../../hooks/defaultConstant' +import { AIToDoList } from '../aiToDoList/AIToDoList' +import styles from './AIToDoListWrapper.module.scss' + +export const AIToDoListWrapper: React.FC = React.memo((props) => { + const { activeChat } = useAIAgentStore() + const store = useCurrentStore() + const rawData = useCurrentRawData() + + const todoListUpdate = useStore(store, (state) => state.casualChat?.todoListUpdate) + + const todoData: TodoListCardData = useCreation(() => { + if (!activeChat?.SessionID) return cloneDeep(DefaultTodoListCardData) + try { + return rawData.casualChat.planDetails?.todoList || cloneDeep(DefaultTodoListCardData) + } catch (error) { + return cloneDeep(DefaultTodoListCardData) + } + }, [todoListUpdate, activeChat?.SessionID]) + + return ( + <> + {todoData?.items?.length > 0 && ( +
+ +
+ )} + + ) +}) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/type.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/type.ts new file mode 100644 index 0000000000..3df5c11ffc --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/type.ts @@ -0,0 +1 @@ +export interface AIToDoListWrapperProps {} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts index 934bcfd55e..a93b2f276e 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts @@ -1,5 +1,3 @@ -import { useCreation } from 'ahooks' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' import { ChatDataStore, ChatDataStoreKey, @@ -62,17 +60,6 @@ export const getChatDataStoreKey = (store?: ChatDataStore): ChatDataStoreKey => } } -/** - * TODO - 待验证是否还需要此方法 - */ -function useGetChatDataStoreKey() { - const { chatIPCEvents } = useChatIPCDispatcher() - const chatDataStoreKey = useCreation((): ChatDataStoreKey => { - return getChatDataStoreKey(chatIPCEvents.fetchChatDataStore()) - }, [chatIPCEvents]) - return { chatDataStoreKey } as const -} - /** * TODO - 待验证是否还需要此方法 */ @@ -118,5 +105,3 @@ export const getAISourceListFromChatDataStoreKey = (key: ChatDataStoreKey): AISo const source = getAISourceFromChatDataStoreKey(key) return source ? [source] : [] } - -export default useGetChatDataStoreKey diff --git a/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx b/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx index 7b3b295581..cba369847a 100644 --- a/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx +++ b/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx @@ -2658,9 +2658,6 @@ const HTTPFuzzerPageCore: React.FC = (props) => { historyAIReActChatBridge.handleStart({ qs: template.prompt, focusMode: focusModeLoop, - extraValue: { - showQS: template.label, - }, }) }) From 78fa479937629e1e477b9ec1671d26fd9c02cc3a Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Tue, 7 Jul 2026 18:10:45 +0800 Subject: [PATCH 021/101] =?UTF-8?q?feat:=E6=96=B0=E7=89=88nodeitem;feat:?= =?UTF-8?q?=E8=87=AA=E7=94=B1=E5=AF=B9=E8=AF=9D=E6=8A=BD=E7=A6=BB=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AITaskDefaultGroupCard.tsx | 7 +- .../ai-agent/components/ToolInvokerCard.tsx | 149 ++++---- .../aiChatListItem/AIChatListItem.module.scss | 18 - .../aiChatListItem/AIChatListItem.tsx | 323 +++++++++--------- .../StaticChatContent/StaticChatContent.tsx | 10 +- .../StreamingChatContent.module.scss | 5 + .../StreamingChatContent.tsx | 52 +-- .../aiGroupItem/AIGroupItem.module.scss | 0 .../aiGroupItem/AIGroupItem.tsx | 8 + .../aiChatListItem/aiGroupItem/type.ts | 5 + .../aiModelErrorPrompt/AIModelErrorPrompt.tsx | 34 +- .../aiChatListItem/aiModelErrorPrompt/type.ts | 1 + .../aiNodeItem/AINodeItem.module.scss | 13 + .../aiChatListItem/aiNodeItem/AINodeItem.tsx | 95 ++++++ .../aiChatListItem/aiNodeItem/type.ts | 7 + .../aiTaskItem/AITaskItem.module.scss | 0 .../aiChatListItem/aiTaskItem/AITaskItem.tsx | 8 + .../aiChatListItem/aiTaskItem/type.ts | 5 + .../components/aiChatListItem/type.ts | 7 - .../aiFailPlanCard/AiFailPlanCard.tsx | 20 +- .../aiGroupStreamCard/AIGroupStreamCard.tsx | 27 +- .../AIHttpFlowFuzzStatusCard.tsx | 26 +- .../aiHttpFlowFuzzStatusCard/type.ts | 1 + .../AIItemContentWrapper.module.scss | 0 .../AIItemContentWrapper.tsx | 23 ++ .../components/aiItemContentWrapper/type.ts | 12 + .../AIManualIntervention.tsx | 7 +- .../components/aiManualIntervention/type.ts | 6 +- .../aiReActChatReview/AIReActChatReview.tsx | 127 +++---- .../AIReActChatReviewType.d.ts | 1 + .../aiReportFinishCard/AIReportFinishCard.tsx | 18 +- .../AIReportFinishCardType.d.ts | 1 + .../aiReviewResult/AIReviewResult.tsx | 67 ++-- .../components/aiReviewResult/type.ts | 2 +- .../components/aiToolDecision/type.ts | 1 + .../aiTriageChat/AITriageChat.module.scss | 6 +- .../components/aiTriageChat/AITriageChat.tsx | 28 +- .../ai-agent/components/aiTriageChat/type.ts | 15 +- .../ai-agent/historyChat/HistoryChat.tsx | 10 +- .../HistoryChatList/HistoryChatList.tsx | 8 +- .../ai-re-act/aiReActChat/AIReActChat.tsx | 2 +- .../aiReActChatHeader/AIReActChatHeader.tsx | 10 +- .../AIReactChatTextarea.tsx | 10 +- .../AIReActChatContents.tsx | 13 +- .../src/pages/ai-re-act/hooks/aiRender.ts | 23 +- .../hooks/grpcStreamHandler/aiOther.ts | 2 +- .../hooks/useCurrentDataBySession.ts | 2 +- .../ai-re-act/hooks/useGetChatDataStoreKey.ts | 11 + 48 files changed, 739 insertions(+), 487 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/type.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx index b6dcae0e75..2e67bb2b58 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx @@ -21,10 +21,9 @@ const AITaskDefaultGroupCard: FC<{ session: string token: string chatType: ReActChatElement['chatType'] - hasNext?: boolean isChildWindow?: boolean onRefresh?: () => void -}> = ({ elements, session, token, chatType, hasNext, isChildWindow, onRefresh }) => { +}> = ({ elements, session, token, chatType, isChildWindow, onRefresh }) => { const { t } = useI18nNamespaces(['aiAgent']) const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean(true) const [contentFocused, setContentFocused] = useState(false) @@ -52,9 +51,9 @@ const AITaskDefaultGroupCard: FC<{ ) useEffect(() => { - if (isChildWindow || !hasNext) return + if (isChildWindow) return collapseExpand() - }, [collapseExpand, hasNext, isChildWindow]) + }, [collapseExpand, isChildWindow]) useConcurrentStreamRefreshListener(framePayload, session, token, chatType, !isChildWindow) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx index 99159e1b28..edea32fed6 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx @@ -9,10 +9,9 @@ import { AIChatQSDataTypeEnum, AIToolResult, AIYakExecFileRecord, - ChatListRenderType, + ChatToolResult, } from '@/pages/ai-re-act/hooks/aiRender' import FileList from './FileList' -import type { ModalInfoProps } from './ModelInfo' import emiter from '@/utils/eventBus/eventBus' import { AITabsEnum } from '../defaultConstant' import { useClickAway, useCreation, useMemoizedFn } from 'ahooks' @@ -31,7 +30,7 @@ import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { Divider, Tooltip } from 'antd' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import { formatTimestamp } from '@/utils/timeUtil' -import { OperationCardFooter, OperationCardFooterProps } from './OperationCardFooter/OperationCardFooter' +import { OperationCardFooter } from './OperationCardFooter/OperationCardFooter' import useAIAgentStore from '../useContext/useStore' import { YakitPopconfirm } from '@/components/yakitUI/YakitPopconfirm/YakitPopconfirm' import { AIReferenceNode } from '@/pages/ai-re-act/aiReActChatContents/AIReActChatContents' @@ -45,6 +44,7 @@ import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' import useAIAgentDispatcher from '../useContext/useDispatcher' import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' /** @name AI工具按钮对应图标 */ const AIToolToIconMap: Record = { @@ -52,13 +52,8 @@ const AIToolToIconMap: Record = { } interface ToolInvokerCardProps { - titleText?: string - fileList?: AIYakExecFileRecord[] - modalInfo?: ModalInfoProps - operationInfo: OperationCardFooterProps - data: AIToolResult - chatType: ChatListRenderType - token: string + itemData: ChatToolResult + renderNum: number } interface PreWrapperProps { code: string @@ -71,69 +66,53 @@ interface ToolStatusCardProps { children?: ReactNode } interface ToolStdoutCardProps extends ToolInvokerCardProps { - isChildWindow: boolean + fileList: AIYakExecFileRecord[] } interface ToolResultCardProps extends ToolInvokerCardProps { - isChildWindow: boolean + fileList: AIYakExecFileRecord[] } const ToolInvokerCard: FC = (props) => { - const { data } = props - - // 判断路由,子窗口有些功能不展示 - const isChildWindow = useRef(isAuxOrChildWindow()) - const { nodeLabel } = useAINodeLabel(data.verboseName) - const titleText = nodeLabel || data.toolName - const renderContent = useMemoizedFn(() => { - // 过滤掉打开文件 - const operationInfo = { - ...props.operationInfo, - aiFilePath: isChildWindow.current ? undefined : props.operationInfo.aiFilePath, - } - switch (data.type) { - case 'stream': - return ( - - ) - case 'result': - return ( - - ) - case 'create': - return - default: - return null - } - }) - - return renderContent() + const { itemData, renderNum } = props + const store = useCurrentStore() + const execFileRecord = useStore(store, (state) => state.execFileRecord) + const fileList = useCreation(() => { + if (!itemData?.data?.callToolId) return [] + return execFileRecord.get(itemData?.data?.callToolId) || [] + }, [renderNum, itemData?.data?.callToolId]) + + switch (itemData?.data?.type) { + case 'stream': + return + case 'result': + return + case 'create': + return + default: + return null + } } export default memo(ToolInvokerCard) /** tool loading - processing params */ -const ToolLoadingCard: React.FC = memo((props) => { - const { data, titleText } = props +const ToolLoadingCard: React.FC> = memo((props) => { + const { itemData, renderNum } = props const { t } = useI18nNamespaces(['aiAgent']) + const data = useCreation(() => { + return itemData.data + }, [renderNum]) + + const { nodeLabel } = useAINodeLabel(data.verboseName) const reason = useCreation(() => { return data?.tool?.reason || '' - }, [data?.tool?.reason]) + }, [renderNum]) return ( } - titleText={titleText} + titleText={nodeLabel || data.toolName} titleExtra={ !!reason ? ( @@ -155,12 +134,26 @@ const ToolLoadingCard: React.FC = memo((props) => { /**tool_**_stdout */ const ToolStdoutCard: React.FC = memo((props) => { - const { operationInfo, fileList, chatType, data, titleText } = props + const { fileList, itemData, renderNum } = props const { t } = useI18nNamespaces(['aiAgent']) - + // 判断路由,子窗口有些功能不展示 + const isChildWindow = useRef(isAuxOrChildWindow()) const sessionId = useCurrentSessionId() const { onSend } = useAIAgentDispatcher() + const data = useCreation(() => { + return itemData.data + }, [renderNum]) + + const { nodeLabel } = useAINodeLabel(data.verboseName) + + const operationInfo = useCreation(() => { + return { + callToolId: data.callToolId, + aiFilePath: isChildWindow.current ? '' : data.tool.dirPath, + } + }, [renderNum]) + // 获取流数据 const { stream } = useStreamingChatContent({ token: data.stream.EventUUID, @@ -172,7 +165,7 @@ const ToolStdoutCard: React.FC = memo((props) => { const reason = useCreation(() => { return data?.tool?.reason || '' - }, [data?.tool?.reason]) + }, [renderNum]) const onToolExtra = useMemoizedFn((item: AIAgentGrpcApi.ReviewSelector) => { switch (item.value) { @@ -200,7 +193,7 @@ const ToolStdoutCard: React.FC = memo((props) => { }, [stream?.reference]) return ( } titleMore={
@@ -245,13 +238,38 @@ const ToolStdoutCard: React.FC = memo((props) => { /**tool result status:error/success/cancel */ const ToolResultCard: React.FC = memo((props) => { - const { modalInfo, operationInfo, fileList, data, chatType, token, isChildWindow, titleText } = props - const { t } = useI18nNamespaces(['aiAgent']) + const { renderNum, fileList, itemData } = props + const { t, i18n } = useI18nNamespaces(['aiAgent']) const { activeChat } = useAIAgentStore() const [loading, setLoading] = useState(false) const [expand, setExpand] = useState(false) + + // 判断路由,子窗口有些功能不展示 + const isChildWindow = useRef(isAuxOrChildWindow()) + + const data = useCreation(() => { + return itemData.data + }, [renderNum]) + + const { nodeLabel } = useAINodeLabel(data.verboseName) + + const modalInfo = useCreation(() => { + return { + time: itemData.Timestamp, + title: itemData.AIModelName, + icon: itemData.AIService, + } + }, []) + + const operationInfo = useCreation(() => { + return { + callToolId: data.callToolId, + aiFilePath: isChildWindow.current ? '' : data.tool.dirPath, + } + }, [renderNum]) + const expandToggle = useMemoizedFn(() => { setExpand((v) => !v) }) @@ -302,11 +320,11 @@ const ToolResultCard: React.FC = memo((props) => { grpcQueryAIToolDetails(params) .then((res) => { /** TODO - 工具卡片刷新后,需要更新这个item里面的数据以及其刷新逻辑 */ - const chatItem = rawData.contents.get(token) + const chatItem = rawData.contents.get(itemData.id) if (!!chatItem && chatItem.type === AIChatQSDataTypeEnum.TOOL_RESULT) { chatItem.data.tool.resultDetails = getResultDetails(res) } - store.getState().incrementNodeVersion(token, 'item') + store.getState().incrementNodeVersion(itemData.id, 'item') }) .finally(() => setTimeout(() => { @@ -354,7 +372,7 @@ const ToolResultCard: React.FC = memo((props) => { return ( } titleMore={
@@ -379,7 +397,8 @@ const ToolResultCard: React.FC = memo((props) => { {t('ToolInvokerCard.httpTraffic')} {httpFlowDataCount} )} - {isChildWindow || ( + {/*
*/} + {isChildWindow.current || ( } onClick={getListToolList} /> diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.module.scss index 37abbdb93a..4b35d28978 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.module.scss @@ -1,21 +1,3 @@ -.question-wrapper.question-wrapper { - justify-content: flex-start; -} -.content-wrapper.content-wrapper { - max-width: 100%; - max-height: 150px; - overflow-y: auto; -} -.review-wrapper { - background: var(--Colors-Use-Neutral-Bg-Hover); - flex: none; -} - -.ai-mark-down-wrapper { - padding: 8px 12px; - background-color: var(--Colors-Use-Neutral-Bg-Hover); - border-radius: 8px; -} // 加权重 .file-content-wrapper.file-content-wrapper { background-color: transparent; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx index e56139d073..56e29985f8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx @@ -1,189 +1,176 @@ import React from 'react' import { AIChatListItemProps } from './type' -import { useCreation, useMemoizedFn } from 'ahooks' -import { AIReActChatReview } from '../aiReActChatReview/AIReActChatReview' -import { AIReviewResult } from '../aiReviewResult/AIReviewResult' -import { AITriageChatContent } from '../aiTriageChat/AITriageChat' -import ToolInvokerCard from '../ToolInvokerCard' -import styles from './AIChatListItem.module.scss' -import DividerCard from '../DividerCard' -import { AIToolDecision } from '../aiToolDecision/AIToolDecision' -import { AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' -import AiFailPlanCard from '../aiFailPlanCard/AiFailPlanCard' -import { AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' -import StreamingChatContent from './StreamingChatContent/StreamingChatContent' -import StaticChatContent from './StaticChatContent/StaticChatContent' -import useAIAgentStore from '../../useContext/useStore' -import { AIManualIntervention } from '../aiManualIntervention/AIManualIntervention' -import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' -import { AIModelErrorPrompt } from './aiModelErrorPrompt/AIModelErrorPrompt' -import { AIHttpFlowFuzzStatusCard } from '../aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard' -import { AIReportFinishCard } from '../aiReportFinishCard/AIReportFinishCard' import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useMemoizedFn } from 'ahooks' import { useStore } from 'zustand' - -const chatContentExtraProps = { - contentClassName: styles['content-wrapper'], - chatClassName: styles['question-wrapper'], -} +import AIGroupItem from './aiGroupItem/AIGroupItem' +import AITaskItem from './aiTaskItem/AITaskItem' +import StaticChatContent from './StaticChatContent/StaticChatContent' export const AIChatListItem: React.FC = React.memo((props) => { - const { item, type, hasNext, itemIndex, session: sessionProp } = props - const { t } = useI18nNamespaces(['aiAgent']) + const { item } = props const store = useCurrentStore() - const execFileRecord = useStore(store, (state) => state.execFileRecord) - const { activeChat } = useAIAgentStore() - const session = sessionProp || activeChat?.SessionID - const aiStreamNodeProps = useCreation(() => { - switch (type) { - case 're-act': - return { - aiMarkdownProps: { - className: styles['ai-mark-down-wrapper'], - }, - } + // 防止没数据出错,所以没从item中取kind + const kind = useStore(store, (state) => { + if (state.items[item.token]) return 'item' + if (state.groups[item.token]) return 'group' + if (state.tasks[item.token]) return 'task' + return null + }) + // const execFileRecord = useStore(store, (state) => state.execFileRecord) - default: - return {} - } - }, [type]) + // const { activeChat } = useAIAgentStore() + // const session = sessionProp || activeChat?.SessionID - const isStream = useCreation(() => { - return ( - item.type === AIChatQSDataTypeEnum.STREAM || - item.type === AIChatQSDataTypeEnum.STREAM_GROUP || - item.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP || - item.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP - ) - }, [item.type]) + // const isStream = useCreation(() => { + // return ( + // item.type === AIChatQSDataTypeEnum.STREAM || + // item.type === AIChatQSDataTypeEnum.STREAM_GROUP || + // item.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP || + // item.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP + // ) + // }, [item.type]) - const ChatItemRenderer = useMemoizedFn((itemData: AIChatQSData) => { - const { type, Timestamp, data, extraValue } = itemData - switch (type) { - case AIChatQSDataTypeEnum.QUESTION: - return ( - - ) - // case AIChatQSDataTypeEnum.STREAM: - // return - case AIChatQSDataTypeEnum.RESULT: - return - case AIChatQSDataTypeEnum.THOUGHT: - return ( - - ) - case AIChatQSDataTypeEnum.TOOL_RESULT: - const fileList = execFileRecord.get(data.callToolId) - return ( - - ) - case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: - case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: - case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: - case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: - if (!!itemData.data.selected) { - if ( - type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || - type === AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE - ) - return null - return - } else { - return ( - - ) - } - case AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION: - return - /** 该UI已无效,可以删除 */ - // case AIChatQSDataTypeEnum.TASK_INDEX_NODE: - // const dividerCardProps = { - // status: data?.status as AITaskStatus, - // desc: data?.goal, - // name: data?.taskName, - // success: 0, - // error: 0, - // } - // return + // const ChatItemRenderer = useMemoizedFn((itemData: AIChatQSData) => { + // const { type, Timestamp, data, extraValue } = itemData + // switch (type) { + // case AIChatQSDataTypeEnum.QUESTION: + // return ( + // + // ) + // // case AIChatQSDataTypeEnum.STREAM: + // // return + // case AIChatQSDataTypeEnum.RESULT: + // return + // case AIChatQSDataTypeEnum.THOUGHT: + // return ( + // + // ) + // case AIChatQSDataTypeEnum.TOOL_RESULT: + // const fileList = execFileRecord.get(data.callToolId) + // return ( + // + // ) + // case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: + // case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + // case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: + // case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + // case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: + // if (!!itemData.data.selected) { + // if ( + // type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || + // type === AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE + // ) + // return null + // return + // } else { + // return ( + // + // ) + // } + // case AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION: + // return + // /** 该UI已无效,可以删除 */ + // // case AIChatQSDataTypeEnum.TASK_INDEX_NODE: + // // const dividerCardProps = { + // // status: data?.status as AITaskStatus, + // // desc: data?.goal, + // // name: data?.taskName, + // // success: 0, + // // error: 0, + // // } + // // return - case AIChatQSDataTypeEnum.TOOL_CALL_DECISION: - return + // case AIChatQSDataTypeEnum.TOOL_CALL_DECISION: + // return - case AIChatQSDataTypeEnum.HTTP_FLOW_FUZZ_STATUS: - return + // case AIChatQSDataTypeEnum.HTTP_FLOW_FUZZ_STATUS: + // return - case AIChatQSDataTypeEnum.REPORT_FINISH: - return + // case AIChatQSDataTypeEnum.REPORT_FINISH: + // return - case AIChatQSDataTypeEnum.END_PLAN_AND_EXECUTION: - return ( - - ) - case AIChatQSDataTypeEnum.FAIL_PLAN_AND_EXECUTION: - case AIChatQSDataTypeEnum.FAIL_REACT: - return - case AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED: - return - default: - return <> - } - }) + // case AIChatQSDataTypeEnum.END_PLAN_AND_EXECUTION: + // return ( + // + // ) + // case AIChatQSDataTypeEnum.FAIL_PLAN_AND_EXECUTION: + // case AIChatQSDataTypeEnum.FAIL_REACT: + // return + // case AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED: + // return + // default: + // return <> + // } + // }) + // const renderContent = useMemoizedFn(() => { + // if (session === undefined) return null + // if (isStream) + // return ( + // + // ) + // return ChatItemRenderer(contentItem)} /> + // }) const renderContent = useMemoizedFn(() => { - if (session === undefined) return null - if (isStream) - return ( - - ) - return ChatItemRenderer(contentItem)} /> + switch (kind) { + case 'item': + return + case 'group': + return + + case 'task': + return + default: + return null + } }) return {renderContent()} }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx index d13a0883bf..58306ff19e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx @@ -1,14 +1,14 @@ import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' import { useCurrentStore, useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' -import { FC, memo, ReactNode, useMemo } from 'react' +import { FC, memo, useMemo } from 'react' import { useStore } from 'zustand' +import AINodeItem from '../aiNodeItem/AINodeItem' type StaticChatContentProps = { token: string - render?: (contentItem: AIChatQSData) => ReactNode } -const StaticChatContent: FC = ({ token, render }) => { +const StaticChatContent: FC = ({ token }) => { const store = useCurrentStore() const renderNum = useStore(store, (state) => state.items[token].renderNum) const rawData = useCurrentRawData() @@ -19,10 +19,10 @@ const StaticChatContent: FC = ({ token, render }) => { if (itemData.data != null && typeof itemData.data === 'object') { return { ...itemData, data: Object.assign({}, itemData.data) } as AIChatQSData } - return { ...itemData } as AIChatQSData + return { ...itemData } as AIChatQSData // 浅拷贝,深层数据更新引用没变,需要依赖renderNum }, [renderNum]) if (!chatItem) return null - return <>{render?.(chatItem)} + return } export default memo(StaticChatContent) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.module.scss new file mode 100644 index 0000000000..0752e6ef02 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.module.scss @@ -0,0 +1,5 @@ +.ai-mark-down-wrapper { + padding: 8px 12px; + background-color: var(--Colors-Use-Neutral-Bg-Hover); + border-radius: 8px; +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx index a008ddcdab..928ed5e5a9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx @@ -1,10 +1,12 @@ import { AIStreamNode } from '@/pages/ai-re-act/aiReActChatContents/AIReActChatContents' -import { AIChatQSDataTypeEnum, type ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' +import { AIChatQSDataTypeEnum, ChatStream, type ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' import { memo, type FC } from 'react' import { useTypedStream } from './hooks/useTypedStream' import AIGroupStreamCard from '../../aiGroupStreamCard/AIGroupStreamCard' import AITaskDefaultGroupCard from '../../AITaskDefaultGroupCard/AITaskDefaultGroupCard' import ConcurrentStreamCard from '../../ConcurrentStreamCard/ConcurrentStreamCard' +import { useCreation } from 'ahooks' +import styles from './StreamingChatContent.module.scss' type StreamCls = { className: string } | { aiMarkdownProps?: { className: string } } @@ -16,43 +18,43 @@ type StreamingChatContentProps = ReActChatRenderItem & { } type SingleStreamProps = { - token: string - streamClassName?: StreamCls - session: string - listItemIndex?: number + itemData: ChatStream + renderNum: number } -const AIStreamCard: FC = ({ token, streamClassName, session, listItemIndex }) => { - const { stream } = useTypedStream({ token }) - if (!stream) return null +export const AIStreamCard: FC = ({ itemData }) => { + const { stream } = useTypedStream({ token: itemData.id }) - return ( - - ) + const aiStreamNodeProps = useCreation(() => { + switch (itemData.chatType) { + case 'reAct': + return { + className: styles['ai-mark-down-wrapper'], + } + + default: + return { + className: '', + } + } + }, [itemData.chatType]) + if (!stream) return null + return } const StreamingChatContent: FC = (props) => { - const { streamClassName, chatType, token, hasNext, session, itemIndex: listItemIndex } = props + const { streamClassName, chatType, token, session } = props if (props.kind === 'task') { if (props.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP) { - return ( - - ) + return } else { return } } if (props.kind === 'group') { - return + return } - return ( - - ) + return null + // return } export default memo(StreamingChatContent) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.module.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx new file mode 100644 index 0000000000..e0c991f976 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { AIGroupItemProps } from './type' + +const AIGroupItem: React.FC = React.memo((props) => { + return
AIGroupItem
+}) + +export default AIGroupItem diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/type.ts new file mode 100644 index 0000000000..665c22c87e --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/type.ts @@ -0,0 +1,5 @@ +import type { ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AIGroupItemProps { + item: ReActChatRenderElement +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx index f975de0d38..11eb42862c 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx @@ -9,38 +9,26 @@ import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { OutlinePencilaltIcon } from '@/assets/icon/outline' import useAIGlobalConfig from '@/pages/ai-re-act/hooks/useAIGlobalConfig' import { onEditAIModel } from '@/pages/ai-agent/aiModelList/AIModelList' -import { AIAgentGrpcApi } from '@/pages/ai-re-act/hooks/grpcApi' import { getFileNameByModelType, getModelLabelByModelType } from '@/pages/ai-agent/aiModelList/aiModelForm/AIModelForm' import styles from './AIModelErrorPrompt.module.scss' import { Tooltip } from 'antd' export const AIModelErrorPrompt: React.FC = React.memo((props) => { - const { item } = props + const { item, renderNum } = props const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const [aiGlobalConfigData, event] = useAIGlobalConfig() const aiGlobalConfig = useCreation(() => aiGlobalConfigData.aiGlobalConfig, [aiGlobalConfigData.aiGlobalConfig]) - const data: AIAgentGrpcApi.AIApiRequestFailedPayload = useCreation(() => { - return ( - item?.data || { - error_code: '-', - model_tier: '', - provider_name: '', - model_name: '', - cause: '', - liteforge_action: '', - } - ) - }, [item?.data]) + const modalInfo = useCreation(() => { return { - title: data.model_name, + title: item.data.model_name, time: item.Timestamp, - icon: data.provider_name, + icon: item.data.provider_name, } - }, [item.Timestamp, data]) + }, []) const onEdit = useMemoizedFn((e) => { e.stopPropagation() - const fileName = getFileNameByModelType(data.model_tier) + const fileName = getFileNameByModelType(item.data.model_tier) if (!fileName) return // todo 编辑事件,index为0是因为当前使用的ai模型是单选的且选中项一定是在第一个 onEditAIModel({ @@ -54,6 +42,12 @@ export const AIModelErrorPrompt: React.FC = React.memo( }, }) }) + const code = useCreation(() => { + return item.data.cause + }, [renderNum]) + const modelTier = useCreation(() => { + return item.data.model_tier + }, [renderNum]) return ( = React.memo( >
- {getModelLabelByModelType(data.model_tier)}使用错误,请点击右上角编辑进行处理 + {getModelLabelByModelType(modelTier)}使用错误,请点击右上角编辑进行处理
- +
) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts index 79ac63cade..e70a207b1c 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts @@ -2,4 +2,5 @@ import { ChatApiRequestFailed } from '@/pages/ai-re-act/hooks/aiRender' export interface AIModelErrorPromptProps { item: ChatApiRequestFailed + renderNum: number } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.module.scss new file mode 100644 index 0000000000..37816612d9 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.module.scss @@ -0,0 +1,13 @@ +.review-wrapper { + background: var(--Colors-Use-Neutral-Bg-Hover); + flex: none; +} + +.question-wrapper.question-wrapper { + justify-content: flex-start; +} +.content-wrapper.content-wrapper { + max-width: 100%; + max-height: 150px; + overflow-y: auto; +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx new file mode 100644 index 0000000000..cc6b0dacdf --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx @@ -0,0 +1,95 @@ +import React from 'react' +import { AINodeItemProps } from './type' +import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' +import { AITriageChatContentWrapper, AIThought } from '../../aiItemContentWrapper/AIItemContentWrapper' +import ToolInvokerCard from '../../ToolInvokerCard' +import { AIReviewResult } from '../../aiReviewResult/AIReviewResult' +import { AIReActChatReview } from '../../aiReActChatReview/AIReActChatReview' +import styles from './AINodeItem.module.scss' +import { AIManualIntervention } from '../../aiManualIntervention/AIManualIntervention' +import { AIToolDecision } from '../../aiToolDecision/AIToolDecision' +import { AIHttpFlowFuzzStatusCard } from '../../aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard' +import { AIReportFinishCard } from '../../aiReportFinishCard/AIReportFinishCard' +import DividerCard from '../../DividerCard' +import { AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' +import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import AiFailPlanCard from '../../aiFailPlanCard/AiFailPlanCard' +import { AIModelErrorPrompt } from '../aiModelErrorPrompt/AIModelErrorPrompt' +import { AIStreamCard } from '../StreamingChatContent/StreamingChatContent' + +const AINodeItem: React.FC = React.memo((props) => { + const { itemData, renderNum } = props + const { t } = useI18nNamespaces(['aiAgent']) + switch (itemData.type) { + case AIChatQSDataTypeEnum.QUESTION: + return + case AIChatQSDataTypeEnum.RESULT: + return + case AIChatQSDataTypeEnum.THOUGHT: + return + + case AIChatQSDataTypeEnum.TOOL_RESULT: + return + + case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: + if (!!itemData.data?.selected) { + if ( + itemData.type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || + itemData.type === AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE + ) + return null + return + } else { + return ( + + ) + } + case AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION: + return + + case AIChatQSDataTypeEnum.TOOL_CALL_DECISION: + return + + case AIChatQSDataTypeEnum.HTTP_FLOW_FUZZ_STATUS: + return + + case AIChatQSDataTypeEnum.REPORT_FINISH: + return + + case AIChatQSDataTypeEnum.END_PLAN_AND_EXECUTION: + return ( + + ) + + case AIChatQSDataTypeEnum.FAIL_PLAN_AND_EXECUTION: + case AIChatQSDataTypeEnum.FAIL_REACT: + return + + case AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED: + return + + case AIChatQSDataTypeEnum.STREAM: + return + default: + return null + } +}) + +export default AINodeItem diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/type.ts new file mode 100644 index 0000000000..217b4b1e1e --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/type.ts @@ -0,0 +1,7 @@ +import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AINodeItemProps { + itemData: AIChatQSData + /** 控制渲染,必传 */ + renderNum: number +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.module.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx new file mode 100644 index 0000000000..a1e9c76ea0 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { AITaskItemProps } from './type' + +const AITaskItem: React.FC = React.memo((props) => { + return
AITaskItem
+}) + +export default AITaskItem diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/type.ts new file mode 100644 index 0000000000..3d1c8ff048 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/type.ts @@ -0,0 +1,5 @@ +import type { ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AITaskItemProps { + item: ReActChatRenderElement +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/type.ts index ea74f2dd52..28715565c2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/type.ts @@ -1,12 +1,5 @@ import type { ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' -import { AIAgentGrpcApi } from '@/pages/ai-re-act/hooks/grpcApi' -import { UseYakExecResultState } from '@/pages/ai-re-act/hooks/type' export interface AIChatListItemProps { item: ReActChatRenderElement - type: 're-act' | 'task-agent' - tasksProps?: { tasks: AIAgentGrpcApi.PlanTask[]; yakExecResult: UseYakExecResultState } - hasNext?: boolean - itemIndex?: number - session?: string } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiFailPlanCard/AiFailPlanCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiFailPlanCard/AiFailPlanCard.tsx index 62a8da799f..f7ee3d0f93 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiFailPlanCard/AiFailPlanCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiFailPlanCard/AiFailPlanCard.tsx @@ -1,5 +1,5 @@ -import type { FailTaskChatError } from '@/pages/ai-re-act/hooks/aiRender' -import type { FC } from 'react' +import type { ChatFailPlanAndExecution, ChatFailReact } from '@/pages/ai-re-act/hooks/aiRender' +import { memo, type FC } from 'react' import ChatCard from '../ChatCard' import styles from './AiFailPlanCard.module.scss' import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' @@ -8,14 +8,20 @@ import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { OutlineChevronsDownUpIcon, OutlineChevronsUpDownIcon } from '@/assets/icon/outline' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { Tooltip } from 'antd' -import { useToggle } from 'ahooks' +import useCreation from 'ahooks/lib/useCreation' +import useToggle from 'ahooks/lib/useToggle' -const AiFailPlanCard: FC<{ item: FailTaskChatError }> = ({ item }) => { +const AiFailPlanCard: FC<{ itemData: ChatFailReact | ChatFailPlanAndExecution; renderNum: number }> = ({ + itemData, + renderNum, +}) => { const { t } = useI18nNamespaces(['aiAgent']) - const { content } = item - const { nodeLabel } = useAINodeLabel(item.NodeIdVerbose) const [expand, { toggle }] = useToggle(false) + const { nodeLabel } = useAINodeLabel(itemData.data.NodeIdVerbose) + const content = useCreation(() => { + return itemData.data.content + }, [renderNum]) return ( = ({ item }) => { ) } -export default AiFailPlanCard +export default memo(AiFailPlanCard) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index 2f264b3caf..1ad8de1583 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -11,6 +11,9 @@ import classNames from 'classnames' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import useClickFocus from '../../../ai-re-act/hooks/useClickFocus' import { Tooltip } from 'antd' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import useCreation from 'ahooks/lib/useCreation' +import { useStore } from 'zustand' export const Code: FC<{ code: ChatReferenceMaterialPayload; style: CSSProperties }> = ({ code, style }) => { return ( @@ -90,9 +93,8 @@ const STREAM_MASK_THRESHOLD = 170 const AIGroupStreamCard: FC<{ elements: ReActChatElement[] - hasNext?: boolean - session: string -}> = ({ elements, hasNext, session }) => { + token: string +}> = ({ elements, token }) => { const lastElement = elements[elements.length - 1] const { stream } = useTypedStream({ token: lastElement?.token ?? '' }) const { nodeLabel } = useAINodeLabel(stream?.data.NodeIdVerbose) @@ -100,11 +102,30 @@ const AIGroupStreamCard: FC<{ const content = stream?.data.content || '' const shouldShowMask = useMemo(() => content.length > STREAM_MASK_THRESHOLD, [content]) const contentRef = useRef(null) + const perHasNext = useRef(true) const { ref: containerRef, isFocus } = useClickFocus() const [isScroll, setIsScroll] = useState(false) const allowAutoScrollRef = useRef(true) + const store = useCurrentStore() + const rawData = useCurrentRawData() + + const chatLength = useStore(store, (state) => state.casualChat.elements.length) + const taskChatLength = useStore(store, (state) => state.taskChat.elements.length) + const hasNext = useCreation(() => { + if (perHasNext.current === false) return false + + const item = rawData.contents.get(token) + const chatType = item?.chatType + if (chatType === 'reAct') { + perHasNext.current = store.getState().casualChat.elements[chatLength - 1].token === token + return perHasNext.current + } + perHasNext.current = store.getState().taskChat.elements[taskChatLength - 1].token === token + return perHasNext.current + }, [chatLength, taskChatLength]) + // 点击其他地方取消滚动 useEffect(() => { const handleClickOutside = (event: MouseEvent): void => { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx index de9c76007f..7f26b7948e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx @@ -1,6 +1,6 @@ import React, { memo, useLayoutEffect, useMemo, useRef, useState } from 'react' import classNames from 'classnames' -import { useMemoizedFn } from 'ahooks' +import { useCreation, useMemoizedFn } from 'ahooks' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import type { AIHttpFlowFuzzStatusCardProps } from './type' @@ -24,7 +24,7 @@ const STATS_GAP_PX = 4 const STATS_ROW_4_MIN_INNER_PX = 4 * STATS_TILE_PX + 3 * STATS_GAP_PX export const AIHttpFlowFuzzStatusCard: React.FC = memo((props) => { - const { item } = props + const { item, renderNum } = props const { data, Timestamp, AIService, AIModelName } = item const { t } = useI18nNamespaces(['aiAgent']) @@ -34,14 +34,24 @@ export const AIHttpFlowFuzzStatusCard: React.FC = title: AIModelName, icon: AIService, }), - [Timestamp, AIModelName, AIService], + [], ) - const p = data.progress - const total = p?.total_requests ?? 0 - const ok = p?.successful_responses ?? 0 - const fail = p?.failed_requests ?? 0 - const avgMs = p?.average_response_ms + const total = useCreation(() => { + return data.progress?.total_requests ?? 0 + }, [renderNum]) + + const ok = useCreation(() => { + return data.progress?.successful_responses ?? 0 + }, [renderNum]) + + const fail = useCreation(() => { + return data.progress?.failed_requests ?? 0 + }, [renderNum]) + + const avgMs = useCreation(() => { + return data.progress?.average_response_ms ?? 0 + }, [renderNum]) // 「查看详情」点击: // - 若卡片所在的会话绑定了某个 Web Fuzzer 页签(`WebFuzzerAiStore`), diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/type.ts index 0e0ef4c6d5..36911a79b7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/type.ts @@ -2,4 +2,5 @@ import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' export interface AIHttpFlowFuzzStatusCardProps { item: Extract + renderNum: number } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.module.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.tsx new file mode 100644 index 0000000000..08269cdbb6 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' +import { AITriageChatContent } from '../aiTriageChat/AITriageChat' +import { AIThoughtProps, AITriageChatContentWrapperProps } from './type' +import { useCreation } from 'ahooks' +import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' + +export const AITriageChatContentWrapper: React.FC = React.memo((props) => { + const chatDataStoreKey = useGetChatDataStoreKey() + return +}) + +export const AIThought: React.FC = React.memo((props) => { + const { itemData, renderNum } = props + const { t, i18n } = useI18nNamespaces(['aiAgent']) + const newItemData = useCreation(() => { + return { + ...itemData, + data: `${t('AIChatListItem.thinking')}${itemData?.data}`, + } + }, [renderNum, i18n.language]) + return +}) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/type.ts new file mode 100644 index 0000000000..bda8f95423 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/type.ts @@ -0,0 +1,12 @@ +import { ChatThought } from '@/pages/ai-re-act/hooks/aiRender' +import { AINodeItemProps } from '../aiChatListItem/aiNodeItem/type' +import { AITriageChatContentProps } from '../aiTriageChat/type' + +interface AIItemContentWrapperProps { + renderNum: AINodeItemProps['renderNum'] +} +export interface AITriageChatContentWrapperProps extends Omit {} + +export interface AIThoughtProps extends AIItemContentWrapperProps { + itemData: ChatThought +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiManualIntervention/AIManualIntervention.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiManualIntervention/AIManualIntervention.tsx index 57719f5fae..c5cfa4581c 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiManualIntervention/AIManualIntervention.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiManualIntervention/AIManualIntervention.tsx @@ -9,7 +9,7 @@ import ChatCard from '../ChatCard' import { formatTimestamp } from '@/utils/timeUtil' export const AIManualIntervention: React.FC = memo((props) => { - const { info, timestamp } = props + const { info, renderNum } = props const { t } = useI18nNamespaces(['aiAgent']) const containerRef = useRef(null) @@ -19,6 +19,9 @@ export const AIManualIntervention: React.FC = memo((p setIsScroll(false) }, containerRef) + const timestamp = useCreation(() => { + return info.Timestamp + }, [renderNum]) const data: UserManualInterventionContext = useCreation(() => { if (info.type === AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION) { return info.data @@ -27,7 +30,7 @@ export const AIManualIntervention: React.FC = memo((p type: '', content: '', } - }, [info.data, info.type]) + }, [renderNum]) return ( = React.memo((props) => { - const { info, planReviewTreeKeywordsMap, isEmbedded, renderFooterExtra, expand, className, chatType } = props - const { type, data: review } = info + const { info, planReviewTreeKeywordsMap, isEmbedded, renderFooterExtra, expand, className, chatType, renderNum } = + props const { t, i18nRefresh } = useI18nNamespaces(['aiAgent', 'yakitUi']) const { onSend } = useAIAgentDispatcher() @@ -68,8 +65,8 @@ export const AIReActChatReview: React.FC = React.memo((p setReviewTrees(list) setCurrentPlansId(data.plans_id) break - case 'require_user_interactive': - const { options } = review as AIAgentGrpcApi.AIReviewRequire + case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + const { options } = info.data if (options && options.length > 0) { const value = options[0].prompt || options[0].prompt_title setRequireQS(value ? `${value}:` : '') @@ -79,7 +76,7 @@ export const AIReActChatReview: React.FC = React.memo((p default: break } - }, [type, review]) + }, [renderNum]) //#region ai评分 const [targetDate, setTargetDate] = useState() const [countdown] = useCountDown({ @@ -87,11 +84,17 @@ export const AIReActChatReview: React.FC = React.memo((p }) useEffect(() => { if (!execute) return - const data = review as AIAgentGrpcApi.ToolUseReviewRequire - if (!!data?.aiReview?.seconds) { - setTargetDate(Date.now() + data.aiReview.seconds * 1000) + switch (info.type) { + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + const data = info.data + if (!!data?.aiReview?.seconds) { + setTargetDate(Date.now() + data.aiReview.seconds * 1000) + } + break + default: + break } - }, [review, execute]) + }, [renderNum, execute]) //#endregion const reviewTitle = useCreation(() => { const subTitle = !!countdown ? ( @@ -103,7 +106,7 @@ export const AIReActChatReview: React.FC = React.memo((p <> ) let title = t('AIReActChatReview.error') - switch (type) { + switch (info.type) { case 'tool_use_review_require': title = t('AIReActChatReview.toolCall') break @@ -124,10 +127,11 @@ export const AIReActChatReview: React.FC = React.memo((p break } return { title: {title}, subTitle } - }, [type, countdown, i18nRefresh]) + }, [countdown, i18nRefresh]) const toolReview = useCreation(() => { - if (type !== 'tool_use_review_require') return null - const { tool, tool_description, reason, params } = review as AIAgentGrpcApi.ToolUseReviewRequire + if (info.type !== 'tool_use_review_require') return null + + const { tool, tool_description, reason, params } = info.data as AIAgentGrpcApi.ToolUseReviewRequire let paramsValue = '-' try { paramsValue = !!params ? JSON.stringify(params, null, 2) : '-' @@ -158,24 +162,24 @@ export const AIReActChatReview: React.FC = React.memo((p
) - }, [review, i18nRefresh]) + }, [renderNum, i18nRefresh]) const forgeReview = useCreation(() => { - if (type !== 'exec_aiforge_review_require') return null - const data = review as AIAgentGrpcApi.ExecForgeReview + if (info.type !== 'exec_aiforge_review_require') return null + const data = info.data as AIAgentGrpcApi.ExecForgeReview return - }, [review, forgeOption]) + }, [renderNum, forgeOption]) const aiRequireReview = useCreation(() => { - if (type === 'require_user_interactive') { - const data = review as AIAgentGrpcApi.AIReviewRequire + if (info.type === 'require_user_interactive') { + const data = info.data as AIAgentGrpcApi.AIReviewRequire const { prompt } = data return
{prompt}
} return null - }, [review]) + }, [renderNum]) const taskReview = useCreation(() => { - if (type === 'task_review_require') { - const data = review as AIAgentGrpcApi.TaskReviewRequire + if (info.type === 'task_review_require') { + const data = info.data as AIAgentGrpcApi.TaskReviewRequire const { task, short_summary, long_summary } = data return (
@@ -209,7 +213,7 @@ export const AIReActChatReview: React.FC = React.memo((p ) } return null - }, [review, i18nRefresh]) + }, [renderNum, i18nRefresh]) const planReview = useCreation(() => { if (reviewTrees.length > 0) { const list = !!reviewTreeOption ? reviewTrees : initReviewTreesRef.current @@ -245,7 +249,7 @@ export const AIReActChatReview: React.FC = React.memo((p }) const executeDetachedPlan = useMemoizedFn(() => { - const detachedReview = review as AIAgentGrpcApi.DetachedPlanRequire + const detachedReview = info.data as AIAgentGrpcApi.DetachedPlanRequire const syncPayload: { coordinator_id: string; plans?: AIAgentGrpcApi.DetachedPlan } = { coordinator_id: detachedReview.coordinator_id, } @@ -259,7 +263,7 @@ export const AIReActChatReview: React.FC = React.memo((p } } } - const info: AIInputEvent = { + const params: AIInputEvent = { IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_EXECUTE_DETACHED_PLAN, SyncID: randomString(8), @@ -297,7 +301,7 @@ export const AIReActChatReview: React.FC = React.memo((p return } if (!isContinue) return - const find = ((review as AIAgentGrpcApi.ToolUseReviewRequire)?.selectors || []).find( + const find = ((info.data as AIAgentGrpcApi.ToolUseReviewRequire)?.selectors || []).find( (item) => item.value === 'continue', ) if (!find) return @@ -307,7 +311,7 @@ export const AIReActChatReview: React.FC = React.memo((p }) const noAIOptionsList = useCreation(() => { - const { selectors } = review as AIAgentGrpcApi.ToolUseReviewRequire + const { selectors } = info.data as AIAgentGrpcApi.ToolUseReviewRequire const allowShowInput: AIAgentGrpcApi.ReviewSelector[] = [] const showButton: AIAgentGrpcApi.ReviewSelector[] = [] if ( @@ -317,7 +321,7 @@ export const AIReActChatReview: React.FC = React.memo((p 'detached_plan_require', 'task_review_require', 'exec_aiforge_review_require', - ].includes(type) + ].includes(info.type) ) { selectors ?.filter((item) => item.value !== 'continue') @@ -330,7 +334,7 @@ export const AIReActChatReview: React.FC = React.memo((p }) } return { allowShowInput, showButton } - }, [review]) + }, [renderNum]) const noAIOptionsAllowShowInput = useCreation(() => { return ( @@ -347,29 +351,29 @@ export const AIReActChatReview: React.FC = React.memo((p ) ) }, [noAIOptionsList.allowShowInput]) - const handleShowEdit = useMemoizedFn((info: AIAgentGrpcApi.ReviewSelector) => { - switch (info.value) { + const handleShowEdit = useMemoizedFn((item: AIAgentGrpcApi.ReviewSelector) => { + switch (item.value) { case 'freedom-review': - setReviewTreeOption(info) + setReviewTreeOption(item) break case 'input_params': - setForgeOption(info) + setForgeOption(item) break case 'close': - if (type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) { + if (info.type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) { /** TODO - 新版需要逻辑调整*/ - chatIPCEvents.handleTaskReviewRelease((review as AIReviewType).id) + chatIPCEvents.handleTaskReviewRelease((info.data as AIReviewType).id) return } break default: if (editShow) return - if (!info.allow_extra_prompt) { - const jsonInput: Record = { suggestion: info.value } - onSendAIByValue(JSON.stringify(jsonInput), info.value) + if (!item.allow_extra_prompt) { + const jsonInput: Record = { suggestion: item.value } + onSendAIByValue(JSON.stringify(jsonInput), item.value) return } - editInfo.current = cloneDeep(info) + editInfo.current = cloneDeep(item) setEditShow(true) break } @@ -428,25 +432,25 @@ export const AIReActChatReview: React.FC = React.memo((p } }) const aiOptionsLength = useCreation(() => { - if (type !== 'require_user_interactive') return 0 + if (info.type !== 'require_user_interactive') return 0 try { - const { options } = review as AIAgentGrpcApi.AIReviewRequire + const { options } = info.data as AIAgentGrpcApi.AIReviewRequire if (!options || options.length === 0) return 0 return options.length } catch (error) { return 0 } - }, [review]) + }, [renderNum]) const onSetAIOptionsSelect = useMemoizedFn((value?: string) => { setAIOptionsSelect(value) setRequireQS(value ? `${value}:` : '') }) const aiOptions = useCreation(() => { - if (type !== 'require_user_interactive') { + if (info.type !== 'require_user_interactive') { return null } - const { options } = review as AIAgentGrpcApi.AIReviewRequire + const { options } = info.data as AIAgentGrpcApi.AIReviewRequire return ( <>
@@ -477,28 +481,26 @@ export const AIReActChatReview: React.FC = React.memo((p
) - }, [review, requireQS, aiOptionsSelect, i18nRefresh]) + }, [renderNum, requireQS, aiOptionsSelect, i18nRefresh]) //#endregion // 是否显示继续执行按钮 const isContinue = useCreation(() => { - if (type === 'require_user_interactive') return false + if (info.type === 'require_user_interactive') return false - if (!review) return - const { selectors } = review as AIAgentGrpcApi.ToolUseReviewRequire + if (!info.data) return + const { selectors } = info.data as AIAgentGrpcApi.ToolUseReviewRequire if (!selectors || !Array.isArray(selectors) || selectors.length === 0) return false - const findIndex = (review as AIAgentGrpcApi.ToolUseReviewRequire).selectors.findIndex( - (item) => item.value === 'continue', - ) + const findIndex = selectors.findIndex((item) => item.value === 'continue') return findIndex !== -1 - }, [review, type]) + }, [renderNum]) const onSendAIByValue = useMemoizedFn((value: string, optionValue?: string) => { - const info: AIInputEvent = { + const params: AIInputEvent = { IsInteractiveMessage: true, - InteractiveId: (review as AIReviewType).id, + InteractiveId: (info.data as AIReviewType).id, InteractiveJSONInput: value, } - onSend({ token: sessionId, type: chatType, params: info, optionValue }) + onSend({ token: sessionId, type: chatType, params, optionValue }) }) const footerNode = useCreation(() => { const renderFooterRightExtra = () => { @@ -550,7 +552,7 @@ export const AIReActChatReview: React.FC = React.memo((p return (
{isContinue && renderFooterRightExtra()} - {type === 'require_user_interactive' && ( + {info.type === 'require_user_interactive' && ( {t('YakitButton.submitted')} @@ -561,7 +563,6 @@ export const AIReActChatReview: React.FC = React.memo((p isContinue, reviewTreeOption, forgeOption, - type, aiOptionsLength, isRequireQS, requireLoading, @@ -571,11 +572,11 @@ export const AIReActChatReview: React.FC = React.memo((p const reviewHeardExtra = useCreation(() => { let node: ReactNode = <> - switch (type) { + switch (info.type) { case 'tool_use_review_require': case 'exec_aiforge_review_require': /**NOTE 定义问题 */ - const toolReviewData = review as AIAgentGrpcApi.ToolUseReviewRequire + const toolReviewData = info.data as AIAgentGrpcApi.ToolUseReviewRequire if (!!toolReviewData.aiReview) { const { interactive_id, score, level } = toolReviewData.aiReview node = ( @@ -605,7 +606,7 @@ export const AIReActChatReview: React.FC = React.memo((p break } return node - }, [type, review, countdown, i18nRefresh]) + }, [renderNum, countdown, i18nRefresh]) return ( <> diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts index dad814c84a..f141d9acef 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts @@ -12,6 +12,7 @@ export interface AIReActChatReviewProps { expand: boolean className?: string onSendSyncMessage?: ChatIPCContextDispatcher['handleSendSyncMessage'] + renderNum: number } export interface ForgeReviewFormRefProps { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCard.tsx index 88d7244762..7f325a5ab9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCard.tsx @@ -1,5 +1,5 @@ import React, { memo, useState } from 'react' -import { useMemoizedFn } from 'ahooks' +import { useCreation, useMemoizedFn } from 'ahooks' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { OutlineDocumentIcon, OutlineDownloadIcon } from '@/assets/icon/outline' @@ -18,14 +18,22 @@ import ChatCard from '../ChatCard' const { ipcRenderer } = window.require('electron') export const AIReportFinishCard: React.FC = memo((props) => { - const { item } = props + const { item, renderNum } = props const { data } = item const { t } = useI18nNamespaces(['aiAgent']) const [downloadLoading, setDownloadLoading] = useState(false) - const reportPath = data.reportPath - const title = data.title - const content = data.content + const reportPath = useCreation(() => { + return data.reportPath + }, [renderNum]) + + const title = useCreation(() => { + return data.title + }, [renderNum]) + + const content = useCreation(() => { + return data.content + }, [renderNum]) const handleOpenReport = useMemoizedFn(() => { if (!reportPath) return diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCardType.d.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCardType.d.ts index 373c99c83e..7c5231c3fa 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCardType.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCardType.d.ts @@ -2,4 +2,5 @@ import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' export interface AIReportFinishCardProps { item: Extract + renderNum: number } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx index a1d982b1b2..9f1de571ff 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx @@ -17,11 +17,11 @@ import { isEmpty } from 'lodash' import classNames from 'classnames' import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' +import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' export const AIReviewResult: React.FC = memo((props) => { - const { info, timestamp } = props + const { info, renderNum } = props const { t, i18nRefresh } = useI18nNamespaces(['aiAgent', 'yakitUi']) - const { type, data } = info const getChatType = useMemoizedFn(() => { return info.chatType @@ -48,42 +48,45 @@ export const AIReviewResult: React.FC = memo((props) => { } }, [casualLength]) const title = useCreation(() => { - switch (type) { - case 'plan_review_require': + switch (info.type) { + case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: return t('AIReviewResult.planReview') - case 'task_review_require': + case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: return t('AIReviewResult.taskReview') - case 'tool_use_review_require': + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: return t('AIReviewResult.toolReview') - case 'exec_aiforge_review_require': + case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: return t('AIReviewResult.appReview') - case 'require_user_interactive': + case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: return t('AIReviewResult.userPrompt') default: return t('AIReviewResult.reviewDecision') } - }, [type, i18nRefresh]) + }, [i18nRefresh]) + const userAction = useCreation(() => { let btnText: string = '' let userInput: string = '' try { - switch (type) { - case 'plan_review_require': - case 'task_review_require': - case 'tool_use_review_require': - case 'exec_aiforge_review_require': - const userSelected = JSON.parse(data.selected || '') - if (data.optionValue === 'continue') { + switch (info.type) { + case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: + const userSelected = JSON.parse(info.data?.selected || '') + if (info.data.optionValue === 'continue') { btnText = t('YakitButton.runNow') } else { - const selectBtn = data.selectors.find((item) => item.value === data.optionValue) + const selectBtn = info.data.selectors.find((item) => item.value === info.data.optionValue) btnText = selectBtn ? selectBtn.prompt : t('AIReviewResult.unknownAction') } userInput = userSelected.extra_prompt || '' break - case 'require_user_interactive': - const aiSelected = JSON.parse(data.selected || '') - const aiSelectType = data.options.find((item) => (item.prompt || item.prompt_title) === data.optionValue) + case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + const aiSelected = JSON.parse(info.data.selected || '') + const aiSelectType = info.data.options.find( + (item) => (item.prompt || item.prompt_title) === info.data.optionValue, + ) btnText = aiSelectType?.prompt || aiSelectType?.prompt_title || t('AIReviewResult.unknownAction') userInput = aiSelected.suggestion || '' break @@ -96,12 +99,12 @@ export const AIReviewResult: React.FC = memo((props) => { btnText, userInput, } - }, [type, data, i18nRefresh]) + }, [i18nRefresh]) const renderContent = useMemoizedFn(() => { let paramsValue = !!userAction.userInput ? : null - switch (type) { + switch (info.type) { case 'tool_use_review_require': - const { params } = data + const { params } = info.data try { paramsValue = !!paramsValue ? paramsValue : } catch (error) {} @@ -112,24 +115,26 @@ export const AIReviewResult: React.FC = memo((props) => { return paramsValue }) const isShowExpandBtn = useCreation(() => { - switch (type) { + switch (info.type) { case 'tool_use_review_require': return true default: return !!userAction.userInput } - }, [type, userAction.userInput]) + }, [userAction.userInput]) + const modalInfo = useCreation(() => { + return { + title: info.AIModelName, + time: info.Timestamp, + icon: info.AIService, + } + }, [renderNum]) return ( } title={title} subTitle={userAction.btnText} tip="" - modalInfo={{ - title: info.AIModelName, - time: timestamp, - icon: info.AIService, - }} + modalInfo={modalInfo} titleMore={ isShowExpandBtn ? (
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/type.ts index 153127e353..de40e9c846 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/type.ts @@ -5,7 +5,7 @@ import { ModalInfoProps } from '../ModelInfo' export interface AIReviewResultProps { info: AIChatQSData - timestamp: number + renderNum: number } export interface AISingHaveColorTextProps extends ChatCardProps { title: ReactNode diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiToolDecision/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiToolDecision/type.ts index c3500a9bb9..3f57fa259c 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiToolDecision/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiToolDecision/type.ts @@ -2,4 +2,5 @@ import { ChatToolCallDecision } from '@/pages/ai-re-act/hooks/aiRender' export interface AIToolDecisionProps { item: ChatToolCallDecision + renderNum: number } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.module.scss index ee71ec4d17..efd629de66 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.module.scss @@ -23,6 +23,10 @@ word-break: break-all; white-space: pre-wrap; + max-width: 100%; + max-height: 150px; + overflow-y: auto; + :global(.ai-milkdown-input) { word-break: normal; white-space: normal; @@ -35,7 +39,7 @@ } } .triage-chat-question { - justify-content: flex-end; + justify-content: flex-start; .content-wrapper { background: var(--Colors-Use-Main-Focus); } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.tsx index 60278064f1..e6e848e65d 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/AITriageChat.tsx @@ -20,14 +20,26 @@ import { AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' import { AIChatTextareaSubmit } from '../../template/type' import { getAIReActRequestParams } from '../../utils' import { extractDataWithMilkdown } from '../aiMilkdownInput/utils' -import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' import useAIAgentDispatcher from '../../useContext/useDispatcher' export const AITriageChatContent: React.FC = memo((props) => { - const { isAnswer, content, contentClassName, chatClassName, extraValue } = props - const { chatDataStoreKey } = useGetChatDataStoreKey() + const { isAnswer, contentClassName, chatClassName, itemData, chatDataStoreKey, renderNum } = props + const [edit, setEdit] = useState(false) + const content = useCreation(() => { + if (itemData?.data) { + return itemData.data + } + return '' + }, [renderNum]) + + const extraValue = useCreation(() => { + if (itemData?.extraValue) { + return itemData.extraValue + } + return undefined + }, [renderNum]) const renderContent = useMemoizedFn(() => { if (!!extraValue?.showQS) { return ( @@ -42,7 +54,12 @@ export const AITriageChatContent: React.FC = memo((pro return (
{edit ? ( - setEdit(false)} /> + setEdit(false)} + chatDataStoreKey={chatDataStoreKey} + /> ) : ( <>
= memo((pro ) }) const AITriageChatContentEdit: React.FC = React.memo((props) => { - const { extraValue, content, onCancel } = props + const { extraValue, content, onCancel, chatDataStoreKey } = props const { activeChat } = useAIAgentStore() - const { chatDataStoreKey } = useGetChatDataStoreKey() const { onSend } = useAIAgentDispatcher() const defaultValue = useCreation(() => { if (!!extraValue?.showQS) { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts index f49b4b8c2f..1ba26f819e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts @@ -1,15 +1,22 @@ +import { ChatQuestion, ChatResult, ChatThought } from '@/pages/ai-re-act/hooks/aiRender' import { AIChatIPCStartParams } from '@/pages/ai-re-act/hooks/type' +import { AINodeItemProps } from '../aiChatListItem/aiNodeItem/type' +import { ChatDataStoreKey } from '../../store/ChatDataStore' export interface AITriageChatContentProps { isAnswer?: boolean - content: string contentClassName?: string chatClassName?: string - extraValue?: AIChatIPCStartParams['extraValue'] + itemData: ChatQuestion | ChatResult | ChatThought + + renderNum: AINodeItemProps['renderNum'] + + chatDataStoreKey: ChatDataStoreKey } export interface AITriageChatContentEditProps { onCancel: () => void - content: AITriageChatContentProps['content'] - extraValue?: AITriageChatContentProps['extraValue'] + content: string + extraValue?: AIChatIPCStartParams['extraValue'] + chatDataStoreKey: AITriageChatContentProps['chatDataStoreKey'] } diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx index cacd614dd0..0649490744 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx @@ -1,7 +1,7 @@ import { memo, useEffect, useMemo, useState, type ReactNode } from 'react' import useAIAgentStore from '../useContext/useStore' import useAIAgentDispatcher from '../useContext/useDispatcher' -import { useDebounce, useMemoizedFn, useUpdateEffect } from 'ahooks' +import { useUpdateEffect } from 'ahooks' import { yakitNotify } from '@/utils/notification' import { ReActChatEventEnum } from '../defaultConstant' import { OutlineDesktopcomputerIcon, OutlineMessageCirclePlusIcon, OutlineSearchIcon } from '@/assets/icon/outline' @@ -28,6 +28,9 @@ import { handAIHistoryChatRemove } from './utils' import { getImageStoreKeyByAISource } from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' import classNames from 'classnames' import { filterHistorySessionsBySource, getHistorySourceQuerySources, type HistorySourceFilter } from './source' +import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' +import useMemoizedFn from 'ahooks/lib/useMemoizedFn' +import useDebounce from 'ahooks/lib/useDebounce' const HISTORY_SOURCE_FILTER_OPTIONS: { key: HistorySourceFilter @@ -123,6 +126,9 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { }, [enableHistorySourceFilter, historySourceFilter, sessions]) const [clearLoading, setClearLoading] = useState(false) + + const chatDataStoreKey = useGetChatDataStoreKey() + const handleClearAllChat = useMemoizedFn(async () => { if (clearLoading) return if (!isGlobalAIAgentHistory && visibleSessions.length === 0) { @@ -136,7 +142,7 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { const filter = isGlobalAIAgentHistory ? { DeleteAll: true } : { Filter: { Source: historyQuerySources } } await handAIHistoryChatRemove({ grpcDeleteAISessionParams: filter, - handleClearAIImageParams: { chatDataStoreKey: getImageStoreKeyByAISource(source), sessionID: [] }, //删除全部只需要传chatDataStoreKey + handleClearAIImageParams: { chatDataStoreKey, sessionID: [] }, //删除全部只需要传chatDataStoreKey forceCloseSessionParams: { aiSource: source, sessionIds: [], diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx index 185ea10c59..a932ac4514 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx @@ -20,7 +20,7 @@ import type { SessionListDispatcher } from './hook/useSessionList' import type { AISource } from '@/pages/ai-re-act/hooks/grpcApi' import { getHistorySessionIconMeta, getSessionDisplayTitle } from '../source' import { handAIHistoryChatRemove } from '../utils' -import { getImageStoreKeyByAISource } from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' +import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' export const HOUR_MS = 60 * 60 * 1000 export const DAY_MS = 24 * HOUR_MS @@ -132,6 +132,8 @@ const HistoryChatList: FC<{ const [delLoading, setDelLoading] = useState([]) const [editShow, setEditShow] = useState(false) + const chatDataStoreKey = useGetChatDataStoreKey() + const activeSessionId = useMemo(() => { return activeChat?.SessionID || '' }, [activeChat]) @@ -223,10 +225,10 @@ const HistoryChatList: FC<{ try { const sessionIds = [SessionID] - const source = getSetting().Source + const source = getSetting().Source || 'ai' await handAIHistoryChatRemove({ grpcDeleteAISessionParams: { Filter: { SessionID: [SessionID], Source: aiSource } }, - handleClearAIImageParams: { chatDataStoreKey: getImageStoreKeyByAISource(source), sessionID: sessionIds }, + handleClearAIImageParams: { chatDataStoreKey, sessionID: sessionIds }, forceCloseSessionParams: { aiSource: source, sessionIds: sessionIds, diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index f770ba599b..0c8a33b660 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -269,7 +269,7 @@ export const AIReActChat: React.FC = React.memo( }) const handleStopCasualTask = useMemoizedFn(() => { - const currentCasualTaskID = meta.currentCasualTaskID + const currentCasualTaskID = store.getState().currentCasualTaskID if (!store.getState().execute || !currentCasualTaskID) return store.getState().updateState({ diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx index 3dab71af83..eb40f4a087 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx @@ -33,7 +33,7 @@ export const AIReActChatHeader: React.FC = React.memo((p const store = useCurrentStore() const rawData = useCurrentRawData() const focusMode = useStore(store, (state) => state.focusMode) - const casualLoading = useStore(store, (state) => state.casualLoading) + const currentCasualTaskID = useStore(store, (state) => state.currentCasualTaskID) const todoListUpdate = useStore(store, (state) => state.casualChat?.todoListUpdate) const taskId = useCreation(() => { @@ -43,7 +43,7 @@ export const AIReActChatHeader: React.FC = React.memo((p } catch (error) { return '' } - }, [todoListUpdate, activeChat?.SessionID, casualLoading]) + }, [todoListUpdate, activeChat?.SessionID, currentCasualTaskID]) const onDetails = useMemoizedFn((e) => { e.stopPropagation() @@ -84,7 +84,7 @@ export const AIReActChatHeader: React.FC = React.memo((p ) : ( <> - {casualLoading && ( + {currentCasualTaskID && ( } onClick={onDetails}> 任务详情 @@ -101,7 +101,7 @@ const AIReActChatHeaderExternalRightIcon: React.FC state.casualLoading) + const currentCasualTaskID = useStore(store, (state) => state.currentCasualTaskID) const { setting } = useAIAgentStore() @@ -124,7 +124,7 @@ const AIReActChatHeaderExternalRightIcon: React.FC - {casualLoading && rightIcon.taskDetails && } + {currentCasualTaskID && rightIcon.taskDetails && } {rightIcon.dataDetails && ( )} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.tsx index c9bd601119..0756382325 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReactChatTextarea/AIReactChatTextarea.tsx @@ -4,25 +4,19 @@ import { AIChatTextarea } from '@/pages/ai-agent/template/template' import styles from '../AIReActChat.module.scss' import { RoundedStopButton } from '../AIReActComponent' import omit from 'lodash/omit' -import useCreation from 'ahooks/lib/useCreation' -import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import { useStore } from 'zustand' import { useCurrentStore } from '../../hooks/useCurrentDataBySession' -import { getImageStoreKeyByAISource } from '../../hooks/useGetChatDataStoreKey' +import useGetChatDataStoreKey from '../../hooks/useGetChatDataStoreKey' export const AIReactChatTextarea: React.FC = React.memo( forwardRef((props, ref) => { const { handleSubmit, externalParameters, handleStopCasualTask } = props - const { setting } = useAIAgentStore() - const store = useCurrentStore() const cancelCasualLoading = useStore(store, (state) => state.cancelCasualLoading) const casualLoading = useStore(store, (state) => state.casualLoading) - const chatDataStoreKey = useCreation(() => { - return getImageStoreKeyByAISource(setting.Source || 'ai') - }, [setting.Source]) + const chatDataStoreKey = useGetChatDataStoreKey() return ( = React.me useImperativeHandle(ref, () => ({ scrollToItemIndex: locateToIndex }), [locateToIndex]) - const renderItem = useCallback( - (index: number, item?: ReActChatRenderItem) => { - if (!item?.token) return null - const arrayIndex = index - firstItemIndex - const hasNext = chatLength - arrayIndex > 1 - return - }, - [chatLength, firstItemIndex], - ) + const renderItem = useCallback((_, item?: ReActChatRenderElement) => { + if (!item?.token) return null + return + }, []) const Item = useCallback( ({ children, style, 'data-index': dataIndex }) => (
diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index be48259422..7777d7fd66 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -1,5 +1,5 @@ import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' -import type { AIAgentGrpcApi, AIOutputEvent, AITaskStatusType, AIOutputI18n, AIInputEvent } from './grpcApi' +import type { AIAgentGrpcApi, AIOutputEvent, AITaskStatusType, AIOutputI18n } from './grpcApi' import type { AIFileSystemPin, AIQuestionQueues, @@ -377,6 +377,7 @@ export type AIChatQSDataType = `${AIChatQSDataTypeEnum}` export interface AIChatQSDataBase { type: T data: U + /** id就是token */ id: string chatType: ReActChatBaseInfo['chatType'] AIService: AIOutputEvent['AIService'] @@ -392,7 +393,7 @@ export interface AIChatQSDataBase { parentGroupKey?: string } -type ChatQuestion = AIChatQSDataBase +export type ChatQuestion = AIChatQSDataBase export type ChatStream = AIChatQSDataBase type ChatToolCallResult = AIChatQSDataBase type ChatToolCallParams = AIChatQSDataBase @@ -400,14 +401,14 @@ export type ChatApiRequestFailed = AIChatQSDataBase< AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED, AIAgentGrpcApi.AIApiRequestFailedPayload > -type ChatThought = AIChatQSDataBase -type ChatResult = AIChatQSDataBase -type ChatToolResult = AIChatQSDataBase -type ChatPlanReviewRequire = AIChatQSDataBase +export type ChatThought = AIChatQSDataBase +export type ChatResult = AIChatQSDataBase +export type ChatToolResult = AIChatQSDataBase +export type ChatPlanReviewRequire = AIChatQSDataBase type ChatDetachedPlanReviewRequire = AIChatQSDataBase -type ChatTaskReviewRequire = AIChatQSDataBase -type ChatToolUseReviewRequire = AIChatQSDataBase -type ChatRequireUserInteractive = AIChatQSDataBase< +export type ChatTaskReviewRequire = AIChatQSDataBase +export type ChatToolUseReviewRequire = AIChatQSDataBase +export type ChatRequireUserInteractive = AIChatQSDataBase< AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE, UIRequireUserInteractive > @@ -415,8 +416,8 @@ type ChatExecAIForgeReview = AIChatQSDataBase export type ChatToolCallDecision = AIChatQSDataBase type ChatPlanExecEnd = AIChatQSDataBase -type ChatFailPlanAndExecution = AIChatQSDataBase -type ChatFailReact = AIChatQSDataBase +export type ChatFailPlanAndExecution = AIChatQSDataBase +export type ChatFailReact = AIChatQSDataBase type ChatReferenceMaterial = AIChatQSDataBase< AIChatQSDataTypeEnum.Reference_Material, { NodeId: AIOutputEvent['NodeId']; NodeIdVerbose: AIOutputEvent['NodeIdVerbose'] } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index 6cc5830d7a..4bdcdc2100 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -209,7 +209,7 @@ const handleReactTaskDequeue: AIMessageHandler = (request) => { chatType: 'reAct', type: AIChatQSDataTypeEnum.QUESTION, Timestamp: res.Timestamp, - data: { qs: data.react_task_input || '', setting: {} }, + data: data.react_task_input || '', AIService: '', AIModelName: '', // showQS为了UI渲染方便,重新构建的字段 diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentDataBySession.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentDataBySession.ts index 05aef6f8d6..85f87c5373 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentDataBySession.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCurrentDataBySession.ts @@ -4,7 +4,7 @@ import useCurrentSessionId from './useCurrentSessionId' /** * @description 获取当前会话的数据 */ -export function useCurrentDataBySession() { +function useCurrentDataBySession() { const sessionId = useCurrentSessionId() return globalSessionEngine.ensureSession(sessionId) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts index a93b2f276e..e95b48cdb4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts @@ -11,7 +11,18 @@ import { } from '@/pages/ai-agent/store/ChatDataStore' import type { Domain } from '@/pages/ai-agent/store/constants' import { AISourceEnum, type AISource } from './grpcApi' +import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' +import { useCreation } from 'ahooks' +function useGetChatDataStoreKey() { + const { setting } = useAIAgentStore() + const chatDataStoreKey: ChatDataStoreKey = useCreation(() => { + return getImageStoreKeyByAISource(setting.Source || 'ai') + }, [setting.Source]) + return chatDataStoreKey +} + +export default useGetChatDataStoreKey /** * 根据后端返回的会话数据来源转换为前端对应的数据存储路径key */ From 9cff4eed2c0fc16730e8cdbddc151ff4462e1346 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Wed, 8 Jul 2026 13:05:24 +0800 Subject: [PATCH 022/101] =?UTF-8?q?feat:=201.=20meta=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=97=AE=E9=A2=98=E9=98=9F=E5=88=97=E5=92=8C?= =?UTF-8?q?=E8=AE=B0=E5=BF=86=E5=BA=93=E6=95=B0=E6=8D=AE=E7=9A=84=E8=BD=AE?= =?UTF-8?q?=E8=AF=A2=E5=99=A8=EF=BC=8C=E5=B9=B6=E5=BC=80=E5=8F=91=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E7=9A=84=E9=80=BB=E8=BE=91=202.=20=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E4=BA=86STREAM=5FGROUP=E5=AF=B9=E5=BA=94=E7=9A=84ChatStreamGro?= =?UTF-8?q?up=E5=AE=9A=E4=B9=89=EF=BC=8C=E5=9B=A0=E4=B8=BA=E8=AF=A5?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E5=9C=A8=E6=95=B0=E6=8D=AE=E5=86=85=E6=97=A0?= =?UTF-8?q?=E6=84=8F=E4=B9=89=203.=20=E8=B0=83=E6=95=B4=E4=BA=86dispatchSt?= =?UTF-8?q?reamingNode=E7=9A=84=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9EdeleteElementNode=E6=96=B9=E6=B3=95=204.=20=E8=A1=A5?= =?UTF-8?q?=E5=85=85=E4=BA=86=E4=BC=9A=E8=AF=9D=E7=9A=84=E8=BF=9B=E8=A1=8C?= =?UTF-8?q?=E9=9B=86=E5=90=88=EF=BC=8C=E4=BC=9A=E8=AF=9D=E7=9A=84=E6=89=80?= =?UTF-8?q?=E5=B1=9E=E9=A1=B5=E9=9D=A2=E6=9D=A5=E6=BA=90=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E4=BA=86=E4=BC=9A=E8=AF=9D=E5=BC=80=E5=A7=8B=E5=90=8E?= =?UTF-8?q?=E7=9A=84=E5=90=AF=E5=8A=A8=E5=BF=85=E9=A1=BB=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E8=B0=83=E6=95=B4=E4=BA=86sendMessage=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E9=87=8C=E7=9A=84review=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=205.=20=E8=B0=83=E6=95=B4task=5Freview=E5=92=8Ctool?= =?UTF-8?q?=5Freview=E7=9A=84=E9=80=BB=E8=BE=91=206.=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/type/aiChat.d.ts | 11 + .../hooks/ChatMultiSessionController.ts | 110 +++++++-- .../src/pages/ai-re-act/hooks/aiRender.ts | 15 +- .../src/pages/ai-re-act/hooks/chatStore.ts | 213 +++++++++++------- .../hooks/grpcStreamHandler/aiOther.ts | 9 +- .../hooks/grpcStreamHandler/aiReview.ts | 110 ++++++--- .../hooks/grpcStreamHandler/aiSingleItem.ts | 22 -- .../hooks/grpcStreamHandler/aiStream.ts | 4 +- .../hooks/grpcStreamHandler/yakExecResult.ts | 2 +- 9 files changed, 332 insertions(+), 164 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index e7ef849ba8..847b09e530 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -256,4 +256,15 @@ export interface AIAgentChatMetaData { /** 同步ID-是否已处理 */ syncIDMap: Map + + /** + * 轮询获取最新问题队列的空队列次数 + * 如果超过3次,直接关闭轮询器,等待用户问问题后重启 + */ + queuePollingEmptyCount: number + /** 轮询获取最新问题队列的定时器 */ + queuePollingTimer: NodeJS.Timeout | null + + /** 轮询获取最新记忆列表的定时器 */ + memoryPollingTimer: NodeJS.Timeout | null } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index b32c1aad07..5318627cb1 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -1,6 +1,7 @@ import type { AIAgentChatData, AIAgentChatMetaData } from '@/pages/ai-agent/type/aiChat' import { AIInputEventSyncTypeEnum, + AISource, type AIAgentGrpcApi, type AIInputEvent, type AIOutputEvent, @@ -120,6 +121,9 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { cardKVPaidTimer: null, execFileRecordOrder: 1, syncIDMap: new Map(), + queuePollingEmptyCount: 0, + queuePollingTimer: null, + memoryPollingTimer: null, } } @@ -132,7 +136,9 @@ export class ChatMultiSessionController { private rawDataPool = new Map() private metaPool = new Map() + /** 存放所有已建立连接的会话session */ private readyChannels = new Set() + /** 存放当前正在展示的会话session */ private activeShowSession: string | null = null /** 获取对应会话的所有数据集 */ @@ -151,10 +157,18 @@ export class ChatMultiSessionController { } } - /** UI 层建立连接时,登记并绑定当前渲染周期的 UI 状态操纵函数 */ - public registerActiveChannel(sessionId: string) { + /** session会话建立时,注册进队列中,标识当前会话已建立连接 */ + public registerSessionChannel(sessionId: string, source: AIStartParams['Source']) { this.readyChannels.add(sessionId) + + if (this.pageSessionMap.has(source ?? 'ai')) { + this.pageSessionMap.get(source ?? 'ai')?.add(sessionId) + } else { + const sessionSet = new Set([sessionId]) + this.pageSessionMap.set(source ?? 'ai', sessionSet) + } } + /** 设置当前展示的会话 Session */ public setActiveShowSession(sessionId: string) { this.activeShowSession = sessionId @@ -187,7 +201,8 @@ export class ChatMultiSessionController { const { request, store, meta } = this.ensureSession(sessionId) store.getState().updateState({ execute: true, casualTitle: '发送问题,开启会话...' }) - this.pageSessionMap.get(params.Params?.Source ?? 'ai')?.add(sessionId) + this.registerSessionChannel(sessionId, params.Params?.Source) + this.setActiveShowSession(sessionId) Object.assign(request, params.Params) if (params.Params?.UserQuery) { @@ -207,9 +222,8 @@ export class ChatMultiSessionController { public handleSendMessage(payload: AIChatSendParams) { try { const { token, type, params, optionValue } = payload - // TODO - 这里的判断存活写的有些问题 - const isExist = this.readyChannels.has(token) - if (!isExist) { + if (!this.readyChannels.has(token)) { + if (!this.isActiveShowSession(token)) return yakitNotify('warning', '会话不存在,无法发送消息') return } @@ -220,6 +234,19 @@ export class ChatMultiSessionController { store.getState().updateState({ casualTitle: '等待回复中...' }) } + if (params.IsFreeInput) { + // 因为有用户问题发送,所以注册 获取问题队列轮询器 + if (!meta.queuePollingTimer) { + meta.queuePollingEmptyCount = 0 + meta.queuePollingTimer = setInterval(() => { + this.requestMessage(token, { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, + }) + }, 5000) + } + } + // 记录发送请求里的syncId-标识开始处理中 if (params.IsSyncMessage && params.SyncID) { meta.syncIDMap.set(params.SyncID, true) @@ -238,9 +265,19 @@ export class ChatMultiSessionController { switch (review.type) { case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - // tool_review 数据自动执行continue操作,并且不在UI上展示 - // 如果能进入该逻辑,说明有问题 - // TODO - 这里需要处理问题 + // 非执行任务组的tool_review,并且review模式不是yolo,才能展示到UI上供用户主动操作 + // 用户操作后,review结果不会展示到UI上,所以需要删除该review的所有数据 + rawData.contents.delete(review.id) + store.getState().updateCasualReview(review.id, 'remove') + store.getState().deleteElementNode({ + chatType: 'reAct', + token: review.id, + kind: 'item', + taskID: review.taskIndex || undefined, + onDelContent: (mapKey) => { + rawData.contents.delete(mapKey) + }, + }) break case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: @@ -268,9 +305,9 @@ export class ChatMultiSessionController { switch (review.type) { case AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP: case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - // task_review 和 tool_review 数据自动执行continue操作,并且不在UI上展示 + // 任务规划的task_review和tool_review会在自动执行continue操作,不会在UI上展示 // 如果能进入该逻辑,说明有问题 - // TODO - 这里需要处理问题 + console.error(`未知错误[handleSendMessage]: ${JSON.stringify(payload)}`) break case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: // review操作后正常展示在UI上 @@ -324,19 +361,23 @@ export class ChatMultiSessionController { /** 会话建立成功后, 需要做的额外操作 */ private handleSessionStartSuccess(sessionId: string) { + const { meta } = this.ensureSession(sessionId) + // 获取任务规划历史任务树 this.requestMessage(sessionId, { IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS, }) - /** - * TODO - 轮询最新的问题队列,考虑触发的时机之类的 - * TODO - 获取最新的记忆列表数据,这个好像没法考虑触发时机 - */ - // 获取最新问题队列数据 - // sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) - // 获取最新记忆列表数据 - // sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_MEMORY_CONTEXT }) + + // 获取最新记忆列表数据, 并注册轮询定时器 + this.requestMessage(sessionId, { IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_MEMORY_CONTEXT }) + if (meta.memoryPollingTimer) clearTimeout(meta.memoryPollingTimer) + meta.memoryPollingTimer = setInterval(() => { + this.requestMessage(sessionId, { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_MEMORY_CONTEXT, + }) + }, 5000) } /** 💥 核心替换:接管原 useChatIPC 里的巨型数据分发逻辑! */ @@ -364,8 +405,18 @@ export class ChatMultiSessionController { this.requestMessage(sessionId, meta.createChatQuestion) meta.createChatQuestion = undefined store.getState().updateState({ casualTitle: '等待回复中...' }) + + // 因为有用户问题发送,所以注册 获取问题队列轮询器 + if (meta.queuePollingTimer) clearTimeout(meta.queuePollingTimer) + meta.queuePollingEmptyCount = 0 + meta.queuePollingTimer = setInterval(() => { + this.requestMessage(sessionId, { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, + }) + }, 5000) } else { - // 调用历史数据恢复方法 + // TODO - 调用历史数据恢复方法 } this.handleSessionStartSuccess(sessionId) meta?.onSessionStartSuccess?.(sessionId) @@ -493,18 +544,27 @@ export class ChatMultiSessionController { const { store } = this.ensureSession(sessionId) handleTaskPlanEnd(this.ensureSession(sessionId)) store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) + this.readyChannels.delete(sessionId) this.closeIPCListeners(sessionId) } /** 关闭指定session的连接 * TODO - 期望传一个对象,{ sessionIds: string[]; aiSource: AISource } + * TODO - 需要区分删除和关闭会话两种操作,不然数据的关闭和删除会混乱 */ - public forceCloseSession(params: { sessionIds: string[]; source: string }) { - const { sessionIds, source } = params - ipcRenderer.invoke('cancel-ai-re-act', sessionId).catch(() => {}) - - this.readyChannels.delete(sessionId) + public forceCloseSession(params: { sessionIds: string[] }) { + const { sessionIds } = params + for (let session of sessionIds) { + ipcRenderer.invoke('cancel-ai-re-act', session).catch(() => {}) + } } + + /** + * 删除指定的session会话 + * sessionIds 需要删除的session集合,如果需要全删,这该值传空数组 + * source 需要删除的session来源页面 + */ + public deleteSessions(sessionIds: string[], source: AISource) {} } export const globalSessionEngine = new ChatMultiSessionController() diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 7777d7fd66..e56bf15a97 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -422,8 +422,6 @@ type ChatReferenceMaterial = AIChatQSDataBase< AIChatQSDataTypeEnum.Reference_Material, { NodeId: AIOutputEvent['NodeId']; NodeIdVerbose: AIOutputEvent['NodeIdVerbose'] } > -/** 用于渲染State定义使用, 无实际逻辑意义 */ -type ChatStreamGroup = AIChatQSDataBase export type ChatUserManualIntervention = AIChatQSDataBase< AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION, UserManualInterventionContext @@ -451,7 +449,6 @@ export type AIChatQSData = | ChatFailReact | ChatToolCallResult | ChatReferenceMaterial - | ChatStreamGroup | ChatUserManualIntervention | ChatToolCallParams | ChatApiRequestFailed @@ -644,7 +641,7 @@ export interface ChatStoreState { /** 是否属于任务组的子节点 */ parentTaskId?: string node: { - kind: 'item' | 'group' | 'task' + kind: ReActChatRenderElement['kind'] token: string type: AIChatQSDataType isHistory?: boolean @@ -665,5 +662,13 @@ export interface ChatStoreState { groupExtra?: (groupToken: string, tokens: string[]) => void } }) => void - incrementNodeVersion: (token: string, kind: 'item' | 'group' | 'task') => void + incrementNodeVersion: (token: string, kind: ReActChatRenderElement['kind']) => void + + deleteElementNode( + params: { + taskID?: string + groupID?: string + onDelContent: (mapKey: string) => void + } & Omit, + ) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index ec7f340fe2..d6572bb9d7 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -115,12 +115,10 @@ export const createChatStore = () => { updateCasualReview: (id: string, status: 'add' | 'remove') => set((state) => { - if (status === 'add') { - if (!state.currentCasualReview.includes(id)) state.currentCasualReview.push(id) - } else if (status === 'remove') { - if (state.currentCasualReview.includes(id)) { - state.currentCasualReview = state.currentCasualReview.filter((item) => item !== id) - } + if (status === 'add' && !state.currentCasualReview.includes(id)) { + state.currentCasualReview.push(id) + } else if (status === 'remove' && state.currentCasualReview.includes(id)) { + state.currentCasualReview = state.currentCasualReview.filter((item) => item !== id) } }), @@ -145,13 +143,10 @@ export const createChatStore = () => { dispatchStreamingNode: ({ chatType, parentTaskId, node }) => set((state) => { const isHistory = node.isHistory ?? false + const elementRef = { kind: node.kind, token: node.token, chatType, isHistory } + const targetElements = chatType === 'reAct' ? state.casualChat.elements : state.taskChat.elements - /** - * 添加节点数据到实体字典中 - * - * 这里为什么没有group类型,因为group是通过两个item碰撞自动合成的group - * 暂不支持代码手动新增group类型数据 - */ + // 注册实体(group 由连续 stream item 碰撞自动生成,不支持手动注册) if (node.kind === 'item' && !state.items[node.token]) { state.items[node.token] = { kind: 'item', @@ -170,93 +165,82 @@ export const createChatStore = () => { } } - // 2. 路由分发决策(以普通看板为例,去掉了大任务内部分支以精简示范) - const targetElements = chatType === 'reAct' ? state.casualChat.elements : state.taskChat.elements - if (isHistory) { // 历史数据处理逻辑 } else { - // 实时数据处理逻辑 + // 空列表直接追加 if (targetElements.length === 0) { - targetElements.push({ kind: node.kind, token: node.token, chatType, isHistory }) + targetElements.push(elementRef) return } - let lastToken = '' - if (parentTaskId) { - // 属于某个子任务组内的数据 - const targetList = state.tasks[parentTaskId]?.childrenTokens - // 异常数据,暂不警告处理,直接无视 - if (!targetList) return - lastToken = targetList[targetList.length - 1] - } else { - // 属于顶层或者group内的数据 - const lastElement = targetElements[targetElements.length - 1] - lastToken = lastElement.token - } + // 定位同级上一个节点(parentChildren 为空数组时表示无兄弟节点,不做合并) + const parentChildren = parentTaskId ? state.tasks[parentTaskId]?.childrenTokens : undefined + if (parentTaskId && !parentChildren) return - const lastItem = state.items[lastToken] - const lastGroup = state.groups[lastToken] - - // 已经存在组,并且和组标识一致,直接添加到组中 - if ( - lastGroup && - lastGroup.kind === 'group' && - lastGroup.nodeId && - node.kind === 'item' && - node.type === AIChatQSDataTypeEnum.STREAM && - node.nodeId && - lastGroup.nodeId === node.nodeId - ) { - lastGroup.childrenTokens.push(node.token) - // lastGroup.renderNum += 1 - node?.groupExtra?.(lastGroup.token, [node.token]) - return - } + const lastToken = parentChildren?.length + ? parentChildren.at(-1) + : parentTaskId + ? undefined + : targetElements.at(-1)?.token + + if (lastToken) { + const lastItem = state.items[lastToken] + const lastGroup = state.groups[lastToken] + const isStreamItem = node.kind === 'item' && node.type === AIChatQSDataTypeEnum.STREAM && !!node.nodeId - // 两个stream-item数据合并成组的逻辑 - if ( - lastItem && - lastItem.type === AIChatQSDataTypeEnum.STREAM && - lastItem.nodeId && - node.type === AIChatQSDataTypeEnum.STREAM && - node.kind === 'item' && - node.nodeId && - lastItem.nodeId === node.nodeId - ) { - const newGroupToken = `${node.nodeId}-${uuidv4()}` - state.groups[newGroupToken] = { - kind: 'group', - token: newGroupToken, - type: AIChatQSDataTypeEnum.STREAM_GROUP, - renderNum: 0, - nodeId: node.nodeId, - childrenTokens: [lastToken, node.token], + // stream 合并:追加到已有组 + if (isStreamItem && lastGroup?.kind === 'group' && lastGroup.nodeId === node.nodeId) { + lastGroup.childrenTokens.push(node.token) + node.groupExtra?.(lastGroup.token, [node.token]) + return } - if (parentTaskId && state.tasks[parentTaskId]?.childrenTokens) { - // 更新子任务组最后一个token为新组token - state.tasks[parentTaskId].childrenTokens[state.tasks[parentTaskId].childrenTokens.length - 1] = - newGroupToken - } else { - // 添加到顶层或者group内的数据 - targetElements[targetElements.length - 1] = { + + // stream 合并:两个连续 item 合成新组 + if (isStreamItem && lastItem?.type === AIChatQSDataTypeEnum.STREAM && lastItem.nodeId === node.nodeId) { + const newGroupToken = `${node.nodeId}-${uuidv4()}` + state.groups[newGroupToken] = { kind: 'group', token: newGroupToken, - chatType, - isHistory, + type: AIChatQSDataTypeEnum.STREAM_GROUP, + renderNum: 0, + nodeId: node.nodeId, + childrenTokens: [lastToken, node.token], + } + if (parentChildren?.length) { + parentChildren[parentChildren.length - 1] = newGroupToken + } else { + targetElements[targetElements.length - 1] = { + kind: 'group', + token: newGroupToken, + chatType, + isHistory, + } } + node.groupExtra?.(newGroupToken, [lastToken, node.token]) + return } - node?.groupExtra?.(newGroupToken, [lastToken, node.token]) - return } } - if (parentTaskId && state.tasks[parentTaskId]?.childrenTokens) { - // 剩余情况,直接添加到子任务组中 - state.tasks[parentTaskId].childrenTokens.push(node.token) + // 默认追加(实时数据合并失败也走这里) + const parentChildren = parentTaskId ? state.tasks[parentTaskId]?.childrenTokens : undefined + if (parentChildren) { + parentChildren.push(node.token) + return + } + + const lastEl = state.taskChat.elements.at(-1) + if ( + chatType === 'task' && + node.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP && + lastEl?.kind === 'task' && + state.tasks[lastEl.token]?.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP + ) { + // 非组数据放入默认任务组,且默认任务组保持在最下面 + state.taskChat.elements.splice(state.taskChat.elements.length - 1, 0, elementRef) } else { - // 添加到顶层数据中 - targetElements.push({ kind: node.kind, token: node.token, chatType, isHistory }) + targetElements.push(elementRef) } }), @@ -267,6 +251,73 @@ export const createChatStore = () => { if (kind === 'group' && state.groups[token]) state.groups[token].renderNum += 1 if (kind === 'task' && state.tasks[token]) state.tasks[token].renderNum += 1 }), + + /** 删除指定token的节点,并将关联节点一并更新 */ + deleteElementNode: (params) => + set((state) => { + const { token, kind, chatType, taskID, groupID, onDelContent } = params + + const removeChatElement = (targetToken: string) => { + const target = chatType === 'reAct' ? state.casualChat : state.taskChat + target.elements = target.elements.filter((item) => item.token !== targetToken) + } + + const removeFromChildrenTokens = ( + container: { childrenTokens: string[] } | undefined, + targetToken: string, + ) => { + if (container) { + container.childrenTokens = container.childrenTokens.filter((t) => t !== targetToken) + } + } + + /** 如果是group类型, 则清除整个group里的所有item数据 */ + const purgeGroup = (groupToken: string) => { + const group = state.groups[groupToken] + if (!group) return + group.childrenTokens.forEach(onDelContent) + delete state.groups[groupToken] + } + + const detachFromParentOrTopLevel = () => { + if (taskID) { + removeFromChildrenTokens(state.tasks[taskID], token) + } else { + removeChatElement(token) + } + } + + switch (kind) { + case 'item': + onDelContent(token) + if (groupID) { + removeFromChildrenTokens(state.groups[groupID], token) + } else if (taskID) { + removeFromChildrenTokens(state.tasks[taskID], token) + } else { + removeChatElement(token) + } + break + case 'group': + onDelContent(token) + purgeGroup(token) + detachFromParentOrTopLevel() + break + case 'task': { + onDelContent(token) + const task = state.tasks[token] + if (task) { + for (const childToken of task.childrenTokens) { + purgeGroup(childToken) + onDelContent(childToken) + } + } + delete state.tasks[token] + removeChatElement(token) + break + } + } + }), })), ) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index 4bdcdc2100..59aea1d466 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -289,12 +289,19 @@ const handlePlanExecTasks: AIMessageHandler = (request) => { } const handleQueueInfo: AIMessageHandler = (request) => { - const { res, chatType, store } = request + const { res, chatType, store, meta } = request if (res.Type !== 'structured' || res.NodeId !== 'queue_info') return if (chatType === 'task') return const ipcContent = Uint8ArrayToString(res.Content) || '' const { tasks, total_tasks } = JSON.parse(ipcContent) as AIAgentGrpcApi.QuestionQueues + // 记录最新问题队列的数量,4次为空,则关闭轮询器 + if (tasks.length === 0) meta.queuePollingEmptyCount += 1 + if (meta.queuePollingEmptyCount > 3 && meta.queuePollingTimer) { + clearTimeout(meta.queuePollingTimer) + meta.queuePollingTimer = null + } + store.getState().updateState({ questionQueue: { total: total_tasks, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index 0a92b0c307..7ca1239b0a 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -114,7 +114,7 @@ const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { } const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { - const { res, chatType, rawData, meta, sendRequest } = requestInfo + const { res, chatType, store, rawData, meta, request, sendRequest } = requestInfo if (res.Type !== 'task_review_require') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -141,18 +141,40 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { if (res.IsSync) return // 实时数据处理逻辑 - // task_review 和 tool_review 在任何review模式下,都是自动执行continue操作 - // 操作的review,在UI上不显示操作历史 - const info: AIInputEvent = { - IsInteractiveMessage: true, - InteractiveId: chatData.id, - InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) + // 实时数据-(自由对话|任务规划)的 review 自动执行,并且不展示在UI上 + if (isAuto) return + + if (chatType === 'task') { + // 任务规划下,task_review在非yolo模式时,自动执行continue操作,并且不在UI上展示操作结果 + const info: AIInputEvent = { + IsInteractiveMessage: true, + InteractiveId: chatData.id, + InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), + } + sendRequest(info) + } else { + const taskGroupDetail = rawData.contents.get(chatData.taskIndex || '') + // 自由对话下,如果属于执行任务组里的task_review,在任何review模式下,后端都会自动执行continue操作,并且不在UI上展示操作结果 + // 非执行任务组的review,正常显示到UI上,根据review模式和用户主动操作,决定结果,并且操作后,也不在UI上展示结果 + if (!taskGroupDetail || taskGroupDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { + rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().updateCasualReview(chatData.id, 'add') + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + }, + }) + } } - sendRequest(info) } const handleToolReview: AIMessageHandler = (requestInfo) => { - const { res, chatType, rawData, meta, sendRequest } = requestInfo + const { res, chatType, store, rawData, meta, request, sendRequest } = requestInfo if (res.Type !== 'tool_use_review_require') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -179,14 +201,36 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { if (res.IsSync) return // 实时数据处理逻辑 - // task_review 和 tool_review 在任何review模式下,都是自动执行continue操作 - // 操作的review,在UI上不显示操作历史 - const info: AIInputEvent = { - IsInteractiveMessage: true, - InteractiveId: chatData.id, - InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), + const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) + // 实时数据-(自由对话|任务规划)的 review 自动执行,并且不展示在UI上 + if (isAuto) return + + if (chatType === 'task') { + // 任务规划下,tool_review在非yolo模式时,自动执行continue操作,并且不在UI上展示操作结果 + const info: AIInputEvent = { + IsInteractiveMessage: true, + InteractiveId: chatData.id, + InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), + } + sendRequest(info) + } else { + const taskGroupDetail = rawData.contents.get(chatData.taskIndex || '') + // 自由对话下,如果属于执行任务组里的task_review,在任何review模式下,后端都会自动执行continue操作,并且不在UI上展示操作结果 + // 非执行任务组的review,正常显示到UI上,根据review模式和用户主动操作,决定结果,并且操作后,也不在UI上展示结果 + if (!taskGroupDetail || taskGroupDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { + rawData.contents.set(chatData.id, cloneDeep(chatData)) + store.getState().updateCasualReview(chatData.id, 'add') + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: chatData.taskIndex, + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + }, + }) + } } - sendRequest(info) } const handleUserInteractive: AIMessageHandler = (requestInfo) => { @@ -446,28 +490,42 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { case AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE: reviewDetail.data.selected = JSON.stringify(data.params) reviewDetail.data.optionValue = data.params?.suggestion || 'continue' - store.getState().dispatchStreamingNode({ - chatType: chatType, - parentTaskId: reviewDetail.taskIndex, - node: { - token: reviewDetail.id, - kind: 'item', - type: reviewDetail.type, - }, - }) + if (chatType === 'reAct') { + store.getState().incrementNodeVersion(reviewDetail.id, 'item') if (reviewDetail.type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) { store.getState().updateState({ currentPlanReviewToken: '' }) } else { store.getState().updateCasualReview(reviewDetail.id, 'remove') } } else { + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: reviewDetail.taskIndex, + node: { + token: reviewDetail.id, + kind: 'item', + type: reviewDetail.type, + }, + }) store.getState().updateState({ currentPlanReviewToken: '' }) } break case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - // 实时数据-直接自动执行了continue,并且不需要展示在UI上,所以该处逻辑直接跳过 + // 实时数据-任务规划-所有的task_review和tool_review都会自动执行,不展示到UI上 + // 但是自由对话里,没有在执行任务组的tool_review会展示到UI上,需要处理数据并在UI上删除掉 + rawData.contents.delete(reviewDetail.id) + store.getState().updateCasualReview(reviewDetail.id, 'remove') + store.getState().deleteElementNode({ + chatType: chatType, + token: reviewDetail.id, + kind: 'item', + taskID: reviewDetail.taskIndex || undefined, + onDelContent: (mapKey) => { + rawData.contents.delete(mapKey) + }, + }) break default: break diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index ca34ce20e1..aed5f8f5cb 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -350,28 +350,6 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { } rawData.contents.set(chatData.id, chatData) meta.currentTaskPlanActiveNode.add(chatData.id) - // TODO - 这里要判断是否是最后一个默认任务组,直接添加不行 - // setElements((old) => { - // const exists = old.some((item) => item.token === chatData.id && item.type === chatData.type) - // if (exists) return old - // const last = old[old.length - 1] - // if (last.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP) { - // // 实时数据下,将默认任务聚合组置底 - // old.splice(old.length - 1, 0, { - // token: chatData.id, - // type: chatData.type, - // renderNum: 1, - // chatType: 'task', - // kind: 'task', - // children: [], - // }) - // return [...old] - // } - // return [ - // ...old, - // { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task', kind: 'task', children: [] }, - // ] - // }) store.getState().dispatchStreamingNode({ chatType: chatType, node: { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index 46fc1fa0af..d2608e5431 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -134,18 +134,16 @@ const handleStream: AIMessageHandler = (requestInfo) => { if (lastUUID === EventUUID) { // 由UI进行定时获取渲染(打字机效果) rawData.systemStream += content - store.getState().updateStateCount('updateSystemStream') } else { if (meta.systemEventUUID.includes(EventUUID)) return meta.systemEventUUID.push(EventUUID) rawData.systemStream = content - store.getState().updateStateCount('updateSystemStream') } } else { meta.systemEventUUID.push(EventUUID) rawData.systemStream = content - store.getState().updateStateCount('updateSystemStream') } + store.getState().updateStateCount('updateSystemStream') return } if (res.IsReason) { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts index 0bf9c1d713..4fd320ea37 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts @@ -1,5 +1,5 @@ import type { AIMessageHandler, AIMessageHandlerParams } from '../type' -import type { AIAgentGrpcApi, AIOutputEvent } from '../grpcApi' +import type { AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { checkStreamValidity, convertCardInfo } from '@/hook/useHoldGRPCStream/useHoldGRPCStream' import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' From bddda21de25b569bf078a62dc5860af91864db6e Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Wed, 8 Jul 2026 14:07:29 +0800 Subject: [PATCH 023/101] =?UTF-8?q?feat:=20=E5=B0=86AIChatQSDataBase?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E9=87=8CtaskIndex=E5=AD=97=E6=AE=B5=E6=94=B9?= =?UTF-8?q?=E4=B8=BATaskID=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/ChatMultiSessionController.ts | 8 ++--- .../src/pages/ai-re-act/hooks/aiRender.ts | 4 +-- .../hooks/grpcStreamHandler/aiReview.ts | 32 +++++++++---------- .../hooks/grpcStreamHandler/aiSingleItem.ts | 32 +++++++++---------- .../hooks/grpcStreamHandler/aiStream.ts | 16 +++++----- .../hooks/grpcStreamHandler/aiToolResult.ts | 2 +- 6 files changed, 47 insertions(+), 47 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 5318627cb1..47fb8085e2 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -273,7 +273,7 @@ export class ChatMultiSessionController { chatType: 'reAct', token: review.id, kind: 'item', - taskID: review.taskIndex || undefined, + taskID: review.TaskId || undefined, onDelContent: (mapKey) => { rawData.contents.delete(mapKey) }, @@ -315,7 +315,7 @@ export class ChatMultiSessionController { review.data.optionValue = optionValue store.getState().dispatchStreamingNode({ chatType: 'task', - parentTaskId: review.taskIndex, + parentTaskId: review.TaskId, node: { token: review.id, kind: 'item', @@ -334,7 +334,7 @@ export class ChatMultiSessionController { }) store.getState().dispatchStreamingNode({ chatType: 'task', - parentTaskId: review.taskIndex, + parentTaskId: review.TaskId, node: { token: review.id, kind: 'item', @@ -520,7 +520,7 @@ export class ChatMultiSessionController { rawData.contents.set(data.id, data) store.getState().dispatchStreamingNode({ chatType: data.chatType, - parentTaskId: data.taskIndex, + parentTaskId: data.TaskId, node: { token: data.id, kind: 'item', diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index e56bf15a97..39f78141c8 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -383,8 +383,8 @@ export interface AIChatQSDataBase { AIService: AIOutputEvent['AIService'] AIModelName: AIOutputEvent['AIModelName'] Timestamp: AIOutputEvent['Timestamp'] - /** 节点信息所属的任务聚合组 key(parentTaskId-taskId) */ - taskId?: string + /** 节点信息所属的任务节点索引 */ + TaskId?: AIOutputEvent['TaskId'] /** 前端专属数据,供前端逻辑和UI处理使用 */ extraValue?: CustomPluginExecuteFormValue | Record /** 参考资料 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index 7ca1239b0a..3409671b8b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -130,7 +130,7 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { id: data.id, type: AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE, data: { ...cloneDeep(data) }, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -154,7 +154,7 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { } sendRequest(info) } else { - const taskGroupDetail = rawData.contents.get(chatData.taskIndex || '') + const taskGroupDetail = rawData.contents.get(chatData.TaskId || '') // 自由对话下,如果属于执行任务组里的task_review,在任何review模式下,后端都会自动执行continue操作,并且不在UI上展示操作结果 // 非执行任务组的review,正常显示到UI上,根据review模式和用户主动操作,决定结果,并且操作后,也不在UI上展示结果 if (!taskGroupDetail || taskGroupDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { @@ -162,7 +162,7 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -190,7 +190,7 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { id: data.id, type: AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE, data: { ...cloneDeep(data) }, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -214,7 +214,7 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { } sendRequest(info) } else { - const taskGroupDetail = rawData.contents.get(chatData.taskIndex || '') + const taskGroupDetail = rawData.contents.get(chatData.TaskId || '') // 自由对话下,如果属于执行任务组里的task_review,在任何review模式下,后端都会自动执行continue操作,并且不在UI上展示操作结果 // 非执行任务组的review,正常显示到UI上,根据review模式和用户主动操作,决定结果,并且操作后,也不在UI上展示结果 if (!taskGroupDetail || taskGroupDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { @@ -222,7 +222,7 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -250,7 +250,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { id: data.id, type: AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE, data: cloneDeep(data), - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -268,7 +268,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { if (target) { store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -288,7 +288,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -317,7 +317,7 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { id: data.id, type: AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE, data: { ...cloneDeep(data) }, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -335,7 +335,7 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { if (target) { store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -359,7 +359,7 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, cloneDeep(chatData)) store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -435,7 +435,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { reviewDetail.data.optionValue = data.params?.suggestion || 'continue' store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: reviewDetail.taskIndex, + parentTaskId: reviewDetail.TaskId, node: { token: reviewDetail.id, kind: 'item', @@ -501,7 +501,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { } else { store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: reviewDetail.taskIndex, + parentTaskId: reviewDetail.TaskId, node: { token: reviewDetail.id, kind: 'item', @@ -521,7 +521,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { chatType: chatType, token: reviewDetail.id, kind: 'item', - taskID: reviewDetail.taskIndex || undefined, + taskID: reviewDetail.TaskId || undefined, onDelContent: (mapKey) => { rawData.contents.delete(mapKey) }, @@ -552,7 +552,7 @@ const handleDetachedPlanReview: AIMessageHandler = (requestInfo) => { id: data.id, type: AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE, data: { ...cloneDeep(data) }, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType: chatType, planID: meta.currentTaskPlanID?.coordinatorId, taskID: res.TaskId, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index aed5f8f5cb..58e7106845 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -16,7 +16,7 @@ const handleThought: AIMessageHandler = (requestInfo) => { chatType: chatType, type: AIChatQSDataTypeEnum.THOUGHT, data: thought || '', - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -26,7 +26,7 @@ const handleThought: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -49,7 +49,7 @@ const handleResult: AIMessageHandler = (requestInfo) => { chatType: chatType, type: AIChatQSDataTypeEnum.RESULT, data: result || '', - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -59,7 +59,7 @@ const handleResult: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -83,7 +83,7 @@ const handleFailReactTask: AIMessageHandler = (requestInfo) => { NodeId: res.NodeId, NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), }, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -93,7 +93,7 @@ const handleFailReactTask: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -121,7 +121,7 @@ const handleToolCallDecision: AIMessageHandler = (requestInfo) => { En: i18n.en, }, }, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -131,7 +131,7 @@ const handleToolCallDecision: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -155,7 +155,7 @@ const handleFailPlanAndExecution: AIMessageHandler = (requestInfo) => { NodeId: res.NodeId, NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), }, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -165,7 +165,7 @@ const handleFailPlanAndExecution: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -188,7 +188,7 @@ const handleApiRequestFailed: AIMessageHandler = (requestInfo) => { chatType: chatType, type: AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED, data, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -198,7 +198,7 @@ const handleApiRequestFailed: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -246,7 +246,7 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { // 仅 `working` 覆盖 progress;其它状态保留上一次(新建时默认 undefined) progress: status === 'working' ? payload.progress : undefined, }, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -256,7 +256,7 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -291,7 +291,7 @@ const handleReportFinish: AIMessageHandler = (requestInfo) => { chatType: chatType, type: cardType, data: nextData, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -301,7 +301,7 @@ const handleReportFinish: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index d2608e5431..985258d07a 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -67,7 +67,7 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { content: '', ContentType: res.ContentType, }, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -105,7 +105,7 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { content: '', ContentType: res.ContentType, }, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -222,7 +222,7 @@ const handleStream: AIMessageHandler = (requestInfo) => { // 判断是否成为组UI数据展示 store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: streamData.taskIndex, + parentTaskId: streamData.TaskId, node: { token: streamData.id, kind: 'item', @@ -342,7 +342,7 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { if (chatData.parentGroupKey) { store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -357,7 +357,7 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { } else { store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -370,7 +370,7 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { } else { store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -397,7 +397,7 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), }, reference: [data], - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, @@ -407,7 +407,7 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, - parentTaskId: chatData.taskIndex, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts index 715c79a043..198acb3ba2 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -32,7 +32,7 @@ const handleToolCallStart: AIMessageHandler = (requestInfo) => { chatType: chatType, type: AIChatQSDataTypeEnum.TOOL_RESULT, data: toolResult, - taskIndex: generateTaskNodeDataID({ + TaskId: generateTaskNodeDataID({ chatType, planID: meta.currentTaskPlanID?.taskID, taskID: res.TaskId, From f0eb90572d2ae8ea336363228191efb67e3c760f Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Thu, 9 Jul 2026 14:16:28 +0800 Subject: [PATCH 024/101] =?UTF-8?q?feat:=E7=BB=84=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E7=9A=84=E7=BB=84=E4=BB=B6=E5=92=8C=E9=80=BB=E8=BE=91=20perf:?= =?UTF-8?q?=E6=8A=BD=E7=A6=BB=E7=BB=84=E5=8D=95=E7=BA=AF=E7=9A=84UI?= =?UTF-8?q?=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AITaskDefaultGroupCard.tsx | 6 +- .../aiChatListItem/AIChatListItem.tsx | 2 +- .../StreamingChatContent.tsx | 7 +- .../hooks/useTypedStream.ts | 1 + .../aiGroupItem/AIGroupItem.tsx | 17 +- .../aiChatListItem/aiGroupItem/type.ts | 4 +- .../aiChatListItem/aiNodeItem/AINodeItem.tsx | 7 +- .../AIGroupStreamCard.module.scss | 100 ------ .../aiGroupStreamCard/AIGroupStreamCard.tsx | 310 ++++++++++-------- .../AIGroupStreamCardHeard.module.scss | 34 ++ .../AIGroupStreamCardHeard.tsx | 57 ++++ .../aiGroupStreamCardHeard/type.ts | 0 .../AIGroupStreamCardList.module.scss | 66 ++++ .../AIGroupStreamCardList.tsx | 95 ++++++ .../aiGroupStreamCardList/type.ts | 0 .../components/aiGroupStreamCard/type.ts | 27 ++ .../AIReActChatContents.tsx | 12 +- .../src/pages/ai-re-act/hooks/aiRender.ts | 7 +- .../hooks/grpcStreamHandler/aiStream.ts | 2 +- 19 files changed, 497 insertions(+), 257 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx index 2e67bb2b58..9f72dfab23 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx @@ -3,7 +3,7 @@ import { type ReActChatElement, type ReActChatTaskElementSub, } from '@/pages/ai-re-act/hooks/aiRender' -import { type FC, useEffect, useMemo, useState } from 'react' +import { type FC, memo, useEffect, useMemo, useState } from 'react' import classNames from 'classnames' import { OutlineInformationcircleIcon } from '@/assets/icon/outline' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' @@ -23,7 +23,7 @@ const AITaskDefaultGroupCard: FC<{ chatType: ReActChatElement['chatType'] isChildWindow?: boolean onRefresh?: () => void -}> = ({ elements, session, token, chatType, isChildWindow, onRefresh }) => { +}> = memo(({ elements, session, token, chatType, isChildWindow, onRefresh }) => { const { t } = useI18nNamespaces(['aiAgent']) const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean(true) const [contentFocused, setContentFocused] = useState(false) @@ -97,6 +97,6 @@ const AITaskDefaultGroupCard: FC<{ ) : null}
) -} +}) export default AITaskDefaultGroupCard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx index 56e29985f8..ce76127510 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx @@ -164,7 +164,7 @@ export const AIChatListItem: React.FC = React.memo((props) case 'item': return case 'group': - return + return case 'task': return diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx index 928ed5e5a9..7d55ba9445 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx @@ -2,7 +2,6 @@ import { AIStreamNode } from '@/pages/ai-re-act/aiReActChatContents/AIReActChatC import { AIChatQSDataTypeEnum, ChatStream, type ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' import { memo, type FC } from 'react' import { useTypedStream } from './hooks/useTypedStream' -import AIGroupStreamCard from '../../aiGroupStreamCard/AIGroupStreamCard' import AITaskDefaultGroupCard from '../../AITaskDefaultGroupCard/AITaskDefaultGroupCard' import ConcurrentStreamCard from '../../ConcurrentStreamCard/ConcurrentStreamCard' import { useCreation } from 'ahooks' @@ -51,9 +50,9 @@ const StreamingChatContent: FC = (props) => { return } } - if (props.kind === 'group') { - return - } + // if (props.kind === 'group') { + // return + // } return null // return } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts index 2bc1938e05..e89244a85f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts @@ -31,6 +31,7 @@ export interface UseTypedStreamResult { * 获取流式聊天内容并应用平滑打字效果 * - 实时流式(start → end):启用打字效果 * - 历史记录(直接 end):禁用打字效果,直接显示 + * TODO - 看这个hooks是否需要改成只传content(显示在页面的数据)内容出去,其余的值通过useCurrentRawData获取 */ export function useTypedStream(options: UseTypedStreamOptions): UseTypedStreamResult { const { token, step = 2, maxStep = 18, interval = 30, catchUpFrames = 9 } = options diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx index e0c991f976..d9cc6733dc 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx @@ -1,8 +1,23 @@ import React from 'react' import { AIGroupItemProps } from './type' +import AIGroupStreamCard from '../../aiGroupStreamCard/AIGroupStreamCard' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' const AIGroupItem: React.FC = React.memo((props) => { - return
AIGroupItem
+ const { token } = props + + const store = useCurrentStore() + const type = useStore(store, (state) => state.groups[token].type) + + switch (type) { + case AIChatQSDataTypeEnum.STREAM_GROUP: + return + + default: + return <> + } }) export default AIGroupItem diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/type.ts index 665c22c87e..396240cff7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/type.ts @@ -1,5 +1,3 @@ -import type { ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' - export interface AIGroupItemProps { - item: ReActChatRenderElement + token: string } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx index cc6b0dacdf..11e53cc637 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx @@ -16,6 +16,7 @@ import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import AiFailPlanCard from '../../aiFailPlanCard/AiFailPlanCard' import { AIModelErrorPrompt } from '../aiModelErrorPrompt/AIModelErrorPrompt' import { AIStreamCard } from '../StreamingChatContent/StreamingChatContent' +import { AIGroupStreamNode } from '../../aiGroupStreamCard/AIGroupStreamCard' const AINodeItem: React.FC = React.memo((props) => { const { itemData, renderNum } = props @@ -86,7 +87,11 @@ const AINodeItem: React.FC = React.memo((props) => { return case AIChatQSDataTypeEnum.STREAM: - return + if (!!itemData.parentGroupToken) { + return + } else { + return + } default: return null } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss index f47c1124be..2e0f4a824a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss @@ -54,106 +54,6 @@ } } } - .title { - display: flex; - align-items: center; - cursor: pointer; - gap: 2px; - .title-node-label { - margin-right: 8px; - display: flex; - align-items: center; - gap: 2px; - color: var(--Colors-Use-Neutral-Text-3-Secondary); - font-size: 11px; - font-weight: 500; - line-height: 14px; - .brain-icon { - width: 16px; - height: 16px; - color: var(--Colors-Use-Neutral-Disable); - svg { - width: 16px; - height: 16px; - } - } - } - & > :last-child { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - .expand-btn { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - .hidden-expand-btn { - display: none; - } - } - .content { - cursor: pointer; - display: grid; - grid-template-rows: 0fr; - // transition: grid-template-rows 0.3s ease-in-out; - // overflow-y: overlay; - max-height: 156px; - position: relative; - overflow: hidden; - - &:not(.noMask)::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 30%; - background: linear-gradient( - 180deg, - var(--Colors-Use-Basic-Background) 0%, - var(--Colors-Use-Basic-Background) 4%, - rgba(255, 255, 255, 0) 96% - ); - pointer-events: none; - z-index: 1; - } - - &:not(.noMask)::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 30%; - background: linear-gradient( - 180deg, - rgba(255, 255, 255, 0) 4%, - var(--Colors-Use-Basic-Background) 96%, - var(--Colors-Use-Basic-Background) 100% - ); - pointer-events: none; - z-index: 1; - } - - &-inner { - height: 100%; - width: 100%; - min-height: 0; - flex-direction: column; - padding-top: 0; - // transition: padding-top 0.3s ease-in-out; - > * { - color: var(--Colors-Use-Neutral-Text-4-Help-text); - } - } - &.expand { - border-radius: 8px; - grid-template-rows: 1fr; - .content-inner { - font-size: 12px; - font-weight: 400; - letter-spacing: 0.5px; - scrollbar-gutter: stable; - } - } - } } .container-focus { border-color: var(--Colors-Use-Main-Hover); diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index 1ad8de1583..4a0bdec310 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -1,7 +1,6 @@ -import { ChatReferenceMaterialPayload, ReActChatElement } from '@/pages/ai-re-act/hooks/aiRender' -import { type CSSProperties, useState, type FC, useRef, useEffect, useMemo } from 'react' +import { AIChatQSDataTypeEnum, ChatReferenceMaterialPayload, ChatStream } from '@/pages/ai-re-act/hooks/aiRender' +import { type CSSProperties, useState, type FC, useRef, useEffect, useMemo, memo } from 'react' import styles from './AIGroupStreamCard.module.scss' -import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { OutlineArrowsexpandIcon } from '@/assets/icon/outline' import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' @@ -10,10 +9,14 @@ import { useTypedStream } from '../aiChatListItem/StreamingChatContent/hooks/use import classNames from 'classnames' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import useClickFocus from '../../../ai-re-act/hooks/useClickFocus' -import { Tooltip } from 'antd' import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import useCreation from 'ahooks/lib/useCreation' import { useStore } from 'zustand' +import { AIGroupStreamCardHeardWrapperProps, AIGroupStreamCardListWrapperProps } from './type' +import { useMemoizedFn } from 'ahooks' +import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' +import AIGroupStreamCardHeard from './aiGroupStreamCardHeard/AIGroupStreamCardHeard' +import AIGroupStreamCardList from './aiGroupStreamCardList/AIGroupStreamCardList' export const Code: FC<{ code: ChatReferenceMaterialPayload; style: CSSProperties }> = ({ code, style }) => { return ( @@ -25,13 +28,12 @@ export const Code: FC<{ code: ChatReferenceMaterialPayload; style: CSSProperties ) } -const AIStreamNode: FC<{ - token: string - index: number - nodeLabel?: string -}> = ({ token, index, nodeLabel }) => { +export const AIGroupStreamNode: FC<{ + itemData: ChatStream + renderNum: number +}> = memo(({ itemData, renderNum }) => { const { t } = useI18nNamespaces(['aiAgent']) - const { stream } = useTypedStream({ token }) + const { stream } = useTypedStream({ token: itemData.id }) const [open, setOpen] = useState(false) const [openPopover, setOpenPopover] = useState(false) @@ -39,85 +41,152 @@ const AIStreamNode: FC<{ setOpen(false) } + const store = useCurrentStore() + const { getLabelByParams } = useAINodeLabel() + + const nodeLabel = useCreation(() => { + if (!itemData) return + switch (itemData.type) { + case AIChatQSDataTypeEnum.STREAM: + return getLabelByParams(itemData.data?.NodeIdVerbose) + + default: + return '' + } + }, []) + + const seqNo = useCreation(() => { + if (!itemData.parentGroupToken) return '' + const parentGroupTokens = store.getState().groups[itemData.parentGroupToken].childrenTokens + const index = parentGroupTokens.findIndex((ele) => ele === itemData.id) + return index === -1 ? '' : `${index}. ` + }, [renderNum]) + + const modelCode = useCreation(() => { + if (!open) return [] + return stream?.reference || [] + }, [open]) + const popoverCode = useCreation(() => { + if (!openPopover) return [] + return stream?.reference || [] + }, [openPopover]) + + const hidden = useCreation(() => { + return !!stream?.reference?.length + }, [stream?.reference?.length]) + if (!stream) return null return (
- - - - {index}. {stream.data.content} + {open && ( + + + + )} + {seqNo} + {stream.data.content} -
- {index}. {nodeLabel} - { - setOpenPopover(false) - setOpen(true) - }} - type="text2" - icon={} - size="small" - /> + openPopover && ( +
+
+ {seqNo} + {nodeLabel} + { + setOpenPopover(false) + setOpen(true) + }} + type="text2" + icon={} + size="small" + /> +
+ {!!popoverCode.length && }
- {!!stream.reference && } -
+ ) } + destroyTooltipOnHide={true} > -
) -} +}) -const BOTTOM_THRESHOLD = 10 const STREAM_MASK_THRESHOLD = 170 const AIGroupStreamCard: FC<{ - elements: ReActChatElement[] token: string -}> = ({ elements, token }) => { - const lastElement = elements[elements.length - 1] - const { stream } = useTypedStream({ token: lastElement?.token ?? '' }) - const { nodeLabel } = useAINodeLabel(stream?.data.NodeIdVerbose) - const [expand, setExpand] = useState(true) - const content = stream?.data.content || '' - const shouldShowMask = useMemo(() => content.length > STREAM_MASK_THRESHOLD, [content]) - const contentRef = useRef(null) - const perHasNext = useRef(true) +}> = memo(({ token }) => { const { ref: containerRef, isFocus } = useClickFocus() + const [expand, setExpand] = useState(true) - const [isScroll, setIsScroll] = useState(false) - const allowAutoScrollRef = useRef(true) + return ( +
+ + +
+ ) +}) +export default AIGroupStreamCard + +const AIGroupStreamCardListWrapper: React.FC = memo((props) => { + const { expand, token } = props + const store = useCurrentStore() + const childrenTokens = useStore(store, (state) => state.groups[token].childrenTokens || []) + + return +}) +const AIGroupStreamCardHeardWrapper: React.FC = memo((props) => { + const { expand, setExpand, token } = props + + const { getLabelByParams } = useAINodeLabel() + + const perHasNext = useRef(true) const store = useCurrentStore() const rawData = useCurrentRawData() + const childrenTokensLength = useStore(store, (state) => state.groups[token].childrenTokens.length) + const chatLength = useStore(store, (state) => state.casualChat.elements.length) const taskChatLength = useStore(store, (state) => state.taskChat.elements.length) + + const getFirstItem = useMemoizedFn(() => { + const group = store.getState().groups[token] + if (!group) return + // 有组数据的时候,childrenTokens肯定会有数据 + const firstItemToken = group.childrenTokens[0] + const firstItem = rawData.contents.get(firstItemToken) + return firstItem + }) + + const chatType = useCreation(() => { + const firstItem = getFirstItem() + if (!firstItem) return + return firstItem.chatType + }, []) + const hasNext = useCreation(() => { + if (expand === false) return false if (perHasNext.current === false) return false - - const item = rawData.contents.get(token) - const chatType = item?.chatType if (chatType === 'reAct') { perHasNext.current = store.getState().casualChat.elements[chatLength - 1].token === token return perHasNext.current @@ -126,95 +195,62 @@ const AIGroupStreamCard: FC<{ return perHasNext.current }, [chatLength, taskChatLength]) - // 点击其他地方取消滚动 - useEffect(() => { - const handleClickOutside = (event: MouseEvent): void => { - const target = event.target as Node | null - if (!contentRef.current) return - if (!target) return - - if (!contentRef.current.contains(target)) { - setIsScroll(false) - } - } - document.addEventListener('mousedown', handleClickOutside) - return () => { - document.removeEventListener('mousedown', handleClickOutside) + const nodeLabel = useCreation(() => { + const firstItem = getFirstItem() + if (!firstItem) return + switch (firstItem.type) { + case AIChatQSDataTypeEnum.STREAM: + return getLabelByParams(firstItem.data?.NodeIdVerbose) + + default: + return '' } }, []) - // 有滚动条的时候自动滚动到底部 useEffect(() => { - const el = contentRef.current - if (!el || !expand) return - - const onScroll = (): void => { - const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight - allowAutoScrollRef.current = distanceToBottom <= BOTTOM_THRESHOLD + if (hasNext) { + setExpand(false) } + }, [hasNext]) - el.addEventListener('scroll', onScroll, { passive: true }) - return () => el.removeEventListener('scroll', onScroll) - }, [expand]) + const getLastItem = useMemoizedFn(() => { + const group = store.getState().groups[token] + if (!group) return + const length = group.childrenTokens.length + // 有组数据的时候,childrenTokens肯定会有数据 + const lastItemToken = group.childrenTokens[length - 1] + if (!lastItemToken) return + const lastItem = rawData.contents.get(lastItemToken) + return lastItem + }) - useEffect(() => { - if (!expand) return - allowAutoScrollRef.current = true - }, [expand]) + const lastToken = useCreation(() => { + const lastItem = getLastItem() + if (!lastItem) return '' + return lastItem.id + }, []) - // 无滚动条的时候自动滚动到底部 - useEffect(() => { - const el = contentRef.current - if (!el || !expand) return - if (!allowAutoScrollRef.current) return - if (hasNext) return - requestAnimationFrame(() => { - el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }) - }) - let rafId = 0 - const observer = new ResizeObserver(() => { - if (!allowAutoScrollRef.current) return - cancelAnimationFrame(rafId) - rafId = requestAnimationFrame(() => { - el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }) - }) - }) - observer.observe(el) - - return () => { - cancelAnimationFrame(rafId) - observer.disconnect() - } - }, [elements.length, expand, hasNext]) + const shouldShowMask = useMemo(() => { + const lastItem = getLastItem() + if (!lastItem) return false + switch (lastItem.type) { + case AIChatQSDataTypeEnum.STREAM: + const contentLength = lastItem.data?.content?.length || 0 + return contentLength > STREAM_MASK_THRESHOLD - useEffect(() => { - if (hasNext) { - setExpand(false) + default: + return false } - }, [hasNext]) - if (!stream) return null - const collapseTooltip = !expand && elements.length > 1 ? `折叠${elements.length}条信息` : '' + }, [childrenTokensLength]) return ( - -
-
{ - setExpand(!expand) - }} - > - {elements.map((el, index) => ( - - ))} -
-
-
+ ) -} -export default AIGroupStreamCard +}) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss new file mode 100644 index 0000000000..6609074ff3 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss @@ -0,0 +1,34 @@ +.title { + display: flex; + align-items: center; + cursor: pointer; + gap: 2px; + .title-node-label { + margin-right: 8px; + display: flex; + align-items: center; + gap: 2px; + color: var(--Colors-Use-Neutral-Text-3-Secondary); + font-size: 11px; + font-weight: 500; + line-height: 14px; + .brain-icon { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Disable); + svg { + width: 16px; + height: 16px; + } + } + } + & > :last-child { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } + .expand-btn { + color: var(--Colors-Use-Neutral-Text-3-Secondary); + } + .hidden-expand-btn { + display: none; + } +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx new file mode 100644 index 0000000000..957be3bf60 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx @@ -0,0 +1,57 @@ +import { OutlineChevronsUpDownIcon, OutlineChevronsDownUpIcon } from '@/assets/icon/outline' +import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' +import { Tooltip } from 'antd' +import classNames from 'classnames' +import { memo } from 'react' +import { useTypedStream } from '../../aiChatListItem/StreamingChatContent/hooks/useTypedStream' +import { AIGroupStreamCardHeardProps } from '../type' +import styles from './AIGroupStreamCardHeard.module.scss' + +const AIGroupStreamCardHeard: React.FC = memo((props) => { + const { expand, setExpand, lastToken, nodeLabel, shouldShowMask } = props + + const { stream } = useTypedStream({ token: lastToken ?? '' }) + + return ( +
{ + setExpand(!expand) + }} + > +
{nodeLabel}
+
+ {shouldShowMask &&
} +

+ {stream?.data?.content} +

+
+ + } + className={classNames(styles['expand-btn'], { + [styles['hidden-expand-btn']]: expand, + })} + /> + + + } + className={classNames(styles['expand-btn'], { + [styles['hidden-expand-btn']]: !expand, + })} + /> + +
+ ) +}) + +export default AIGroupStreamCardHeard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/type.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.module.scss new file mode 100644 index 0000000000..9fd95c8218 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.module.scss @@ -0,0 +1,66 @@ +.content { + cursor: pointer; + display: grid; + grid-template-rows: 0fr; + // transition: grid-template-rows 0.3s ease-in-out; + // overflow-y: overlay; + max-height: 156px; + position: relative; + overflow: hidden; + + &:not(.noMask)::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 30%; + background: linear-gradient( + 180deg, + var(--Colors-Use-Basic-Background) 0%, + var(--Colors-Use-Basic-Background) 4%, + rgba(255, 255, 255, 0) 96% + ); + pointer-events: none; + z-index: 1; + } + + &:not(.noMask)::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 30%; + background: linear-gradient( + 180deg, + rgba(255, 255, 255, 0) 4%, + var(--Colors-Use-Basic-Background) 96%, + var(--Colors-Use-Basic-Background) 100% + ); + pointer-events: none; + z-index: 1; + } + + &-inner { + height: 100%; + width: 100%; + min-height: 0; + flex-direction: column; + padding-top: 0; + // transition: padding-top 0.3s ease-in-out; + > * { + color: var(--Colors-Use-Neutral-Text-4-Help-text); + } + } + &.expand { + border-radius: 8px; + grid-template-rows: 1fr; + .content-inner { + font-size: 12px; + font-weight: 400; + letter-spacing: 0.5px; + scrollbar-gutter: stable; + } + } +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx new file mode 100644 index 0000000000..6c465eed84 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx @@ -0,0 +1,95 @@ +import { useClickAway, useCreation, useThrottleFn } from 'ahooks' +import classNames from 'classnames' +import { memo, useRef, useState, useEffect } from 'react' +import StaticChatContent from '../../aiChatListItem/StaticChatContent/StaticChatContent' +import { AIGroupStreamCardListProps } from '../type' +import styles from './AIGroupStreamCardList.module.scss' +import { Tooltip } from 'antd' + +const BOTTOM_THRESHOLD = 10 + +const AIGroupStreamCardList: React.FC = memo((props) => { + const { expand, childrenTokens } = props + + const contentRef = useRef(null) + const [isScroll, setIsScroll] = useState(false) + + const allowAutoScrollRef = useRef(true) + + useClickAway(() => { + if (isScroll) setIsScroll(false) + }, contentRef) + + /** 监听当前容器得滚动条是否在底部 */ + useEffect(() => { + const el = contentRef.current + if (!el || !expand) return + allowAutoScrollRef.current = true + el.addEventListener('scroll', onScroll, { passive: true }) + return () => el.removeEventListener('scroll', onScroll) + }, [expand]) + + const onScroll = useThrottleFn( + () => { + const el = contentRef.current + if (!el || !expand) return + const distanceToBottom = el.scrollHeight - el.scrollTop - el.clientHeight + allowAutoScrollRef.current = distanceToBottom <= BOTTOM_THRESHOLD + }, + { wait: 500 }, + ).run + + /** 展开得情况下,数据发生变化,滚动到底部 */ + useEffect(() => { + const el = contentRef.current + if (!el || !expand) return + if (!allowAutoScrollRef.current) return + requestAnimationFrame(() => { + el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }) + }) + let rafId = 0 + const observer = new ResizeObserver(() => { + if (!allowAutoScrollRef.current) return + cancelAnimationFrame(rafId) + rafId = requestAnimationFrame(() => { + el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' }) + }) + }) + observer.observe(el) + + return () => { + cancelAnimationFrame(rafId) + observer.disconnect() + } + }, [childrenTokens.length, expand]) + + const collapseTooltip = useCreation(() => { + return !expand && childrenTokens.length > 1 ? `折叠${childrenTokens.length}条信息` : '' + }, [expand, childrenTokens.length]) + + return ( + +
+
setIsScroll(true)} + className={styles['content-inner']} + style={{ + overflow: isScroll ? 'overlay' : 'hidden', + }} + > + {childrenTokens.map((token) => ( + + ))} +
+
+
+ ) +}) + +export default AIGroupStreamCardList diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/type.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts new file mode 100644 index 0000000000..801b400d12 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts @@ -0,0 +1,27 @@ +import { ChatTaskDefaultGroup } from '@/pages/ai-re-act/hooks/aiRender' +import { Dispatch, SetStateAction } from 'react' + +export interface AIGroupStreamCardHeardProps { + expand: boolean + setExpand: Dispatch> + itemData?: ChatTaskDefaultGroup + lastToken: string + nodeLabel: string + shouldShowMask: boolean +} + +export interface AIGroupStreamCardHeardWrapperProps { + expand: boolean + setExpand: Dispatch> + token: string +} + +export interface AIGroupStreamCardListWrapperProps { + expand: boolean + token: string +} + +export interface AIGroupStreamCardListProps { + expand: boolean + childrenTokens: string[] +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index 5825cc9a64..04b2208626 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, useCallback, useImperativeHandle, useMemo } from 'react' +import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react' import classNames from 'classnames' import { AIReActChatContentsPProps, AIReferenceNodeProps, AIStreamNodeProps } from './AIReActChatContentsType' import styles from './AIReActChatContents.module.scss' @@ -15,7 +15,7 @@ import { AIStreamContentType } from '../hooks/defaultConstant' import { Virtuoso } from 'react-virtuoso' import useVirtuosoAutoScroll from '../hooks/useVirtuosoAutoScroll' import useChatStreamLocateHighlight from '../hooks/useChatStreamLocateHighlight' -import { ChatReferenceMaterialPayload, ReActChatRenderItem } from '../hooks/aiRender' +import { ChatReferenceMaterialPayload, ReActChatRenderElement } from '../hooks/aiRender' import Loading from '@/components/Loading/Loading' import { ScrollText } from '@/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading' import { showYakitModal } from '@/components/yakitUI/YakitModal/YakitModalConfirm' @@ -120,6 +120,7 @@ const TYPE = 'reAct' export const AIReActChatContents: React.FC = React.memo( forwardRef((props, ref) => { + const listRootRef = useRef(null) const { activeChat } = useAIAgentStore() const store = useCurrentStore() @@ -151,10 +152,13 @@ export const AIReActChatContents: React.FC = React.me listRootRef, }) - useImperativeHandle(ref, () => ({ scrollToItemIndex: locateToIndex }), [locateToIndex]) + useImperativeHandle(ref, () => ({ scrollToItemIndex: locateToIndex }), []) const renderItem = useCallback((_, item?: ReActChatRenderElement) => { if (!item?.token) return null + // TODO - + // 如果token变化,可能存在以下情况 + // 例如group中list监听数组长度变化确认更新,会出现长度没变token变化,list层不会渲染,token变化的组件拿不到最新的token一直是旧的 return }, []) const Item = useCallback( @@ -213,7 +217,7 @@ export const AIReActChatContents: React.FC = React.me scrollerRef={setScrollerRef} firstItemIndex={firstItemIndex} atBottomStateChange={setIsAtBottomRef} - data={casualChat} + data={casualChat.elements} totalListHeightChanged={handleTotalListHeightChanged} itemContent={renderItem} initialTopMostItemIndex={chatLength > 1 ? chatLength - 1 : 0} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 39f78141c8..16b3d5008b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -389,8 +389,8 @@ export interface AIChatQSDataBase { extraValue?: CustomPluginExecuteFormValue | Record /** 参考资料 */ reference?: ChatReferenceMaterialPayload - /** 父集合组的key(如果被收集到集合组中, 则存在该字段) */ - parentGroupKey?: string + /** 父集合组的key/token(如果被收集到集合组中, 则存在该字段) */ + parentGroupToken?: string } export type ChatQuestion = AIChatQSDataBase @@ -426,6 +426,8 @@ export type ChatUserManualIntervention = AIChatQSDataBase< AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION, UserManualInterventionContext > +export type ChatStreamGroup = AIChatQSDataBase + type ChatHttpFlowFuzzStatus = AIChatQSDataBase type ChatReportFinish = AIChatQSDataBase export type ChatTaskDefaultGroup = AIChatQSDataBase @@ -455,6 +457,7 @@ export type AIChatQSData = | ChatHttpFlowFuzzStatus | ChatReportFinish | ChatTaskDefaultGroup + | ChatStreamGroup // #endregion /** @name 数据状态机定义 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index 985258d07a..7ac9cc64c1 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -339,7 +339,7 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { if (chatData) { // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 chatData.reference = (chatData.reference || []).concat([data]) - if (chatData.parentGroupKey) { + if (chatData.parentGroupToken) { store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, From 764cbeaa3c0c148f678b4739cea3fd609cc16a40 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Mon, 13 Jul 2026 15:36:13 +0800 Subject: [PATCH 025/101] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E7=9A=84?= =?UTF-8?q?=E6=97=A7=E7=89=88UI=E9=80=BB=E8=BE=91=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E6=96=B0=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aiReActChatReview/AIReActChatReview.tsx | 33 ++- .../aiReActChat/AIReActChat.module.scss | 39 --- .../ai-re-act/aiReActChat/AIReActChat.tsx | 233 +----------------- .../AIReActChatHeader.module.scss | 38 +++ .../aiReActChatHeader/AIReActChatHeader.tsx | 182 +++++++++++--- .../aiReActChat/aiReActChatHeader/type.ts | 7 + .../aiToDoListWrapper/AIToDoListWrapper.tsx | 9 +- .../AIReActChatContents.tsx | 2 +- .../AIReActChatContentsType.ts | 5 +- .../ai-re-act/aiTaskContent/AITaskContent.tsx | 4 +- .../pages/ai-re-act/hooks/useAIItemKind.ts | 17 ++ .../ai-re-act/hooks/useGetChatDataStoreKey.ts | 1 + 12 files changed, 251 insertions(+), 319 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useAIItemKind.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx index b62a406849..b5563d7758 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx @@ -26,7 +26,7 @@ import { OutlineHandleColorsIcon, ColorsOutlineWarpIcon } from '@/assets/icon/co import { AIChatQSDataTypeEnum, AIReviewType, AITaskInfoProps } from '../../../ai-re-act/hooks/aiRender' import { AIForge } from '@/pages/ai-agent/type/forge' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' -import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useCurrentMeta, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' import useAIAgentDispatcher from '../../useContext/useDispatcher' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' @@ -40,8 +40,10 @@ export const AIReActChatReview: React.FC = React.memo((p const { onSend } = useAIAgentDispatcher() const sessionId = useCurrentSessionId() + const metaData = useCurrentMeta() const store = useCurrentStore() const execute = useStore(store, (state) => state.execute) + const taskStatusLoading = useStore(store, (state) => state.taskStatus.loading) const [reviewTreeOption, setReviewTreeOption] = useState() const [reviewTrees, setReviewTrees] = useState([]) @@ -55,11 +57,11 @@ export const AIReActChatReview: React.FC = React.memo((p useEffect(() => { pendingDetachedPlanSubmitRef.current = false - switch (type) { + switch (info.type) { case 'plan_review_require': case 'detached_plan_require': - const data = review as AIAgentGrpcApi.PlanReviewRequire - const list: AITaskInfoProps[] = [] + const data = info.data as AIAgentGrpcApi.PlanReviewRequire + const list: AIAgentGrpcApi.PlanTask[] = [] handleFlatAITree(list, data.plans.root_task) initReviewTreesRef.current = [...list] setReviewTrees(list) @@ -270,29 +272,34 @@ export const AIReActChatReview: React.FC = React.memo((p SyncJsonInput: JSON.stringify(syncPayload), } // chatIPCEvents.handleTaskReviewRelease(detachedReview.id) - onSend({ token: sessionId, type: '', params: info }) + onSend({ token: sessionId, type: '', params }) pendingDetachedPlanSubmitRef.current = false }) const submitDetachedPlan = useMemoizedFn(() => { - const taskId = chatIPCEvents.fetchCurrentTaskPlanID()?.taskID - if (chatIPCData.taskStatus.loading && taskId) { + const taskId = metaData.currentTaskPlanID + if (taskStatusLoading && taskId) { pendingDetachedPlanSubmitRef.current = true - chatIPCEvents.handleCancelLoadingChange('task', true) - onSendSyncMessage?.({ - syncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, - SyncJsonInput: JSON.stringify({ task_id: taskId }), + store.getState().updateState({ + cancelTaskLoading: true, }) + const params: AIInputEvent = { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_EXECUTE_DETACHED_PLAN, + SyncID: randomString(8), + SyncJsonInput: JSON.stringify({ task_id: taskId }), + } + onSend({ token: sessionId, type: '', params }) return } executeDetachedPlan() }) useUpdateEffect(() => { - if (!chatIPCData.taskStatus.loading && pendingDetachedPlanSubmitRef.current) { + if (!taskStatusLoading && pendingDetachedPlanSubmitRef.current) { executeDetachedPlan() } - }, [chatIPCData.taskStatus.loading]) + }, [taskStatusLoading]) /** 继续执行 */ const handleContinue = useMemoizedFn(() => { diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.module.scss b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.module.scss index 1faca87a6f..c34ab444b5 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.module.scss +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.module.scss @@ -240,42 +240,3 @@ transform: translateX(-50%); } } -.chat-locate-popover { - :global { - .ant-popover-inner-content { - padding: 12px; - } - } - .chat-locate-list { - display: flex; - flex-direction: column; - max-height: 240px; - overflow-y: auto; - width: 200px; - // gap: 4px; - } - - .chat-locate-item { - padding: 4px; - font-size: 14px; - line-height: 20px; - color: var(--Colors-Use-Neutral-Text-1-Title); - cursor: pointer; - max-width: 280px; - align-items: center; - border-radius: 4px; - @include mixin.text-ellipsis; - svg { - width: 16px; - height: 16px; - color: var(--Colors-Use-Neutral-Disable); - } - &:hover { - background-color: var(--Colors-Use-Neutral-Bg-Hover); - color: var(--Colors-Use-Main-Primary); - svg { - color: var(--Colors-Use-Main-Primary); - } - } - } -} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index 0c8a33b660..fdddd4afb6 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -20,19 +20,12 @@ import { randomString } from '@/utils/randomUtil' import useAINodeLabel from '../hooks/useAINodeLabel' import useSessionId from '../hooks/useSessionId' import emiter from '@/utils/eventBus/eventBus' -import AIContextToken from '@/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken' -import { AIToDoList } from './aiToDoList/AIToDoList' -import { cloneDeep } from 'lodash' -import { DefaultTodoListCardData } from '../hooks/defaultConstant' -import { TodoListCardData, AIChatQSDataTypeEnum } from '../hooks/aiRender' -import { OutlineLandPlotIcon, OutlineListTodoIcon } from '@/assets/icon/outline' -import TaskDetailsPopover from '@/components/historyAIReActChat/TaskDetailsPopover' -import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' -import { SolidChatIcon } from '@/assets/icon/solid' -import { useCurrentMeta, useCurrentStore } from '../hooks/useCurrentDataBySession' +import { useCurrentStore } from '../hooks/useCurrentDataBySession' import { useStore } from 'zustand' import useCurrentSessionId from '../hooks/useCurrentSessionId' import { AIReactChatTextarea } from './aiReactChatTextarea/AIReactChatTextarea' +import { AIReActChatHeader } from './aiReActChatHeader/AIReActChatHeader' +import { AIToDoListWrapper } from './aiToDoListWrapper/AIToDoListWrapper' export const AIReActChat: React.FC = React.memo( forwardRef((props, ref) => { @@ -45,24 +38,9 @@ export const AIReActChat: React.FC = React.memo( externalParameters, } = props const { setActiveChat, getSetting, onStart, onSend } = useAIAgentDispatcher() - const { chatDataStoreKey } = useGetChatDataStoreKey() - const historyChatAISource = useCreation( - () => getAISourceListFromChatDataStoreKey(chatDataStoreKey), - [chatDataStoreKey], - ) - const sessionRef = useRef(undefined) const sessionId = useCurrentSessionId() const store = useCurrentStore() - const meta = useCurrentMeta() - const execute = useStore(store, (state) => state.execute) - const focusMode = useStore(store, (state) => state.focusMode) - const notifyMessage = useStore(store, (state) => state.notifyMessage) - const questionQueue = useStore(store, (state) => state.questionQueue) - const cancelCasualLoading = useStore(store, (state) => state.cancelCasualLoading) - const casualLoading = useStore(store, (state) => state.casualLoading) - const todoListUpdate = useStore(store, (state) => state.casualChat?.todoListUpdate) - const casualChat = useStore(store, (state) => state.casualChat) const wrapperRef = useRef(null) @@ -284,111 +262,8 @@ export const AIReActChat: React.FC = React.memo( onSend({ token: sessionId, type: 'casual', params: info }) }) - const getPlanDetails = useMemoizedFn(() => { - if (!activeChat?.SessionID) return - return rawData.casualChat.planDetails - }) - - const reActTaskId: string = useCreation(() => { - if (!activeChat?.SessionID) return '' - return getPlanDetails()?.taskId ?? '' - }, [todoListUpdate, activeChat?.SessionID]) - - const todoData: TodoListCardData = useCreation(() => { - if (!activeChat?.SessionID) return cloneDeep(DefaultTodoListCardData) - try { - return rawData.casualChat.planDetails?.todoList || cloneDeep(DefaultTodoListCardData) - } catch (error) { - return cloneDeep(DefaultTodoListCardData) - } - }, [todoListUpdate, activeChat?.SessionID]) - const aiReActChatContentsRef = useRef(null) - const casualConcurrentTaskList = useCreation(() => { - return casualChat.elements - .filter((item) => item.kind === 'task' && item.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP) - .map((item) => item.token) - }, [casualChat.elements.length]) - - const getCasualConcurrentTaskName = useMemoizedFn((token: string) => { - const contentMap = chatIPCEvents.fetchChatDataStore()?.get(activeChat?.SessionID || '')?.casualChat?.contents - const chatData = contentMap?.get(token) - return ( - (chatData?.data as { taskName?: string; goal?: string })?.taskName || - (chatData?.data as { goal?: string })?.goal || - token - ) - }) - - const onScrollToConcurrentTask = useMemoizedFn((token: string) => { - const index = chatIPCData.casualChat.elements.findIndex((item) => item.kind === 'task' && item.token === token) - if (index !== -1) { - aiReActChatContentsRef.current?.scrollToItemIndex(index, 'smooth') - } - }) - - const getTaskId = useMemoizedFn(() => { - return chatIPCEvents.fetchCurrentCasualTaskID() - }) - - const defaultTaskTabLabel = useCreation(() => { - return typeof title === 'string' ? title : '自由对话' - }, [title]) - - const emitTaskContentTab = useMemoizedFn((type: 'add' | 'update', label?: string, nextTaskId?: string) => { - const taskId = nextTaskId || getTaskId() - const sessionId = activeChat?.SessionID - if (!taskId || !sessionId) return false - if (chatDataStoreKey !== 'aiChatDataStore') return false - emiter.emit( - 'actionAITaskContentTab', - JSON.stringify({ - type, - params: { - key: sessionId, - taskId, - label: label || activeChat?.Title || defaultTaskTabLabel, - goal: '', - }, - }), - ) - return true - }) - - const syncCasualTaskTab = useMemoizedFn(() => { - const sessionId = activeChat?.SessionID - if (!getTaskId() || !sessionId) return - if (chatDataStoreKey !== 'aiChatDataStore') return - emitTaskContentTab('add') - sessionRef.current = sessionId - }) - - const onDetails = useMemoizedFn(() => { - if (!getTaskId()) { - yakitNotify('error', 'taskId不存在') - return - } - if (chatDataStoreKey !== 'aiChatDataStore') { - yakitNotify('info', '当前会话不属于 AIAgent 数据源,无法查看任务详情') - return - } - syncCasualTaskTab() - }) - - useEffect(() => { - if (sessionRef.current && sessionRef.current !== activeChat?.SessionID) { - sessionRef.current = undefined - } - }, [activeChat?.SessionID]) - - useEffect(() => { - if (!activeChat?.Title || !activeChat?.SessionID) return - if (sessionRef.current !== activeChat.SessionID) return - if (!reActTaskId) return - emitTaskContentTab('update', activeChat.Title, reActTaskId) - }, [activeChat?.Title, activeChat?.SessionID, emitTaskContentTab, reActTaskId]) - return ( <>
= React.memo( })} >
-
-
- - {title} - {focusMode && ( - - 场景:{focusMode} - - )} -
-
- {isShowRetract && ( - <> - {!!casualConcurrentTaskList.length && ( - - {casualConcurrentTaskList.map((token) => ( -
onScrollToConcurrentTask(token)} - > - {getCasualConcurrentTaskName(token)} -
- ))} -
- } - placement="bottom" - > - }> - 子Agent任务 - - - )} - {externalParameters?.rightIcon ? ( - <> - {/* - 自由对话的taskid(在常量中)存在的时候,casualLoading(state)一定为true - TODO - taskid需要改为 store - */} - {casualLoading && externalParameters.rightIcon.taskDetails && } - {externalParameters.rightIcon.dataDetails && ( - - )} - {externalParameters.rightIcon.history && ( - - -
- } - > - } title="" /> - - )} - {externalParameters.rightIcon.add} - {externalParameters.rightIcon.close} - - ) : ( - <> - {casualLoading && ( - } - onClick={onDetails} - > - 任务详情 - - )} - handleSwitchShowFreeChat(false)} /> - - )} - - )} -
-
- {reActTaskId === chatIPCEvents.fetchCurrentCasualTaskID() && todoData?.items?.length > 0 && ( -
- -
- )} - - {/* - */} +
diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss index c32c12333b..45b2f92828 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss @@ -142,7 +142,45 @@ } } } +.chat-locate-popover { + :global { + .ant-popover-inner-content { + padding: 12px; + } + } + .chat-locate-list { + display: flex; + flex-direction: column; + max-height: 240px; + overflow-y: auto; + width: 200px; + // gap: 4px; + } + .chat-locate-item { + padding: 4px; + font-size: 14px; + line-height: 20px; + color: var(--Colors-Use-Neutral-Text-1-Title); + cursor: pointer; + max-width: 280px; + align-items: center; + border-radius: 4px; + @include mixin.text-ellipsis; + svg { + width: 16px; + height: 16px; + color: var(--Colors-Use-Neutral-Disable); + } + &:hover { + background-color: var(--Colors-Use-Neutral-Bg-Hover); + color: var(--Colors-Use-Main-Primary); + svg { + color: var(--Colors-Use-Main-Primary); + } + } + } +} .history-chat-tooltip-content { width: 280px; height: 300px; diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx index eb40f4a087..cc7b19152d 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect, useRef } from 'react' import classNames from 'classnames' import { Tooltip } from 'antd' import { useStore } from 'zustand' @@ -11,9 +11,9 @@ import AIContextToken from '@/pages/ai-agent/aiChatContent/AIContextToken/AICont import HistoryChat from '@/pages/ai-agent/historyChat/HistoryChat' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { ClockIcon } from '@/assets/newIcon' -import { OutlineListTodoIcon } from '@/assets/icon/outline' +import { OutlineLandPlotIcon, OutlineListTodoIcon } from '@/assets/icon/outline' import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' -import { AIReActChatHeaderExternalRightIconProps, AIReActChatHeaderProps } from './type' +import { AIReActChatHeaderExternalRightIconProps, AIReActChatHeaderProps, AIReActSubAgentTaskProps } from './type' import { ChevronleftButton } from '../AIReActComponent' import useMemoizedFn from 'ahooks/lib/useMemoizedFn' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' @@ -21,51 +21,93 @@ import useCreation from 'ahooks/lib/useCreation' import { yakitNotify } from '@/utils/notification' import emiter from '@/utils/eventBus/eventBus' import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' -import { AISource } from '../../hooks/grpcApi' +import { AISource, AISourceEnum } from '../../hooks/grpcApi' +import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' +import { SolidChatIcon } from '@/assets/icon/solid' +import useAIItemKind from '../../hooks/useAIItemKind' +import { AIChatQSDataTypeEnum } from '../../hooks/aiRender' +import { AI_AGENT_HISTORY_AI_SOURCES } from '../../hooks/useGetChatDataStoreKey' export const AIReActChatHeader: React.FC = React.memo((props) => { - const { title, chatContainerHeaderClassName, isShowRetract, externalParameters, handleSwitchShowFreeChat } = props + const { + title, + chatContainerHeaderClassName, + isShowRetract, + externalParameters, + handleSwitchShowFreeChat, + scrollToItemIndex, + } = props const { activeChat } = useAIAgentStore() const { getSetting } = useAIAgentDispatcher() // 内部订阅 Store 数据 const store = useCurrentStore() - const rawData = useCurrentRawData() const focusMode = useStore(store, (state) => state.focusMode) const currentCasualTaskID = useStore(store, (state) => state.currentCasualTaskID) - const todoListUpdate = useStore(store, (state) => state.casualChat?.todoListUpdate) - - const taskId = useCreation(() => { - if (!activeChat?.SessionID) return '' - try { - return rawData.casualChat.planDetails?.taskId || '' - } catch (error) { - return '' + const casualLoading = useStore(store, (state) => state.casualLoading) + + const sessionRef = useRef(undefined) + + useEffect(() => { + if (sessionRef.current && sessionRef.current !== activeChat?.SessionID) { + sessionRef.current = undefined } - }, [todoListUpdate, activeChat?.SessionID, currentCasualTaskID]) + }, [activeChat?.SessionID]) + + useEffect(() => { + if (!casualLoading) return + syncCasualTaskTab() + }, [casualLoading]) + + useEffect(() => { + if (!activeChat?.Title || !activeChat?.SessionID) return + if (sessionRef.current !== activeChat.SessionID) return + emitTaskContentTab('update', activeChat.Title) + }, [activeChat?.Title, activeChat?.SessionID]) + + const defaultTaskTabLabel = useCreation(() => { + return typeof title === 'string' ? title : '自由对话' + }, [title]) + + const emitTaskContentTab = useMemoizedFn((type: 'add' | 'update', label?: string) => { + const taskId = currentCasualTaskID + const sessionId = activeChat?.SessionID + if (!taskId || !sessionId) return false + if (getSetting().Source !== AISourceEnum.aiAgent) return false + emiter.emit( + 'actionAITaskContentTab', + JSON.stringify({ + type, + params: { + key: sessionId, + taskId, + label: label || activeChat?.Title || defaultTaskTabLabel, + goal: '', + }, + }), + ) + return true + }) + + const syncCasualTaskTab = useMemoizedFn(() => { + const sessionId = activeChat?.SessionID + if (!currentCasualTaskID || !sessionId) return + if (getSetting().Source !== AISourceEnum.aiAgent) return false + emitTaskContentTab('add') + sessionRef.current = sessionId + }) - const onDetails = useMemoizedFn((e) => { - e.stopPropagation() - if (!taskId) { - yakitNotify('error', 'taskId不存在') + const onDetails = useMemoizedFn(() => { + if (!currentCasualTaskID) { + yakitNotify('error', 'currentCasualTaskID不存在') return } - if (getSetting()?.Source === 'ai') { - emiter.emit( - 'actionAITaskContentTab', - JSON.stringify({ - type: 'add', - params: { - key: taskId, - label: '自由对话', - goal: '', - }, - }), - ) - } else { + if (getSetting().Source !== AISourceEnum.aiAgent) { yakitNotify('info', '当前会话不属于 AIAgent 数据源,无法查看任务详情') + return } + syncCasualTaskTab() }) return ( @@ -81,7 +123,10 @@ export const AIReActChatHeader: React.FC = React.memo((p
{isShowRetract ? ( - + <> + + + ) : ( <> {currentCasualTaskID && ( @@ -97,6 +142,73 @@ export const AIReActChatHeader: React.FC = React.memo((p ) }) +const AIReActSubAgentTask: React.FC = React.memo((props) => { + const { scrollToItemIndex } = props + const store = useCurrentStore() + const rawData = useCurrentRawData() + const getKind = useAIItemKind() + const casualChatElementLength = useStore(store, (state) => state.casualChat?.elements?.length || 0) + + const onScrollToConcurrentTask = useMemoizedFn((token: string) => { + const elements = store.getState().casualChat?.elements || [] + const index = elements.findIndex((item) => item.token === token) + if (index !== -1) { + scrollToItemIndex?.(index, 'smooth') + } + }) + + const casualConcurrentTaskList = useCreation(() => { + const list: string[] = [] + const elements = store.getState().casualChat?.elements || [] + for (const token in elements) { + const item = elements[token] + const kind = getKind(item.token) + if (kind !== 'task') continue + const itemContent = rawData.contents.get(token) + if (itemContent?.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP) { + list.push(token) + } + } + return list + }, [casualChatElementLength]) + + const getCasualConcurrentTaskName = useMemoizedFn((token: string) => { + const contentMap = rawData?.contents + const chatData = contentMap?.get(token) + switch (chatData?.type) { + case AIChatQSDataTypeEnum.TASK_NODE_GROUP: + return chatData?.data?.taskName || chatData?.data?.goal || token + + default: + return token + } + }) + + return ( + <> + {!!casualConcurrentTaskList.length && ( + + {casualConcurrentTaskList.map((token) => ( +
onScrollToConcurrentTask(token)}> + {getCasualConcurrentTaskName(token)} +
+ ))} +
+ } + placement="bottom" + > + }> + 子Agent任务 + + + )} + + ) +}) + const AIReActChatHeaderExternalRightIcon: React.FC = React.memo((props) => { const { rightIcon } = props @@ -117,8 +229,10 @@ const AIReActChatHeaderExternalRightIcon: React.FC void handleSwitchShowFreeChat: (show: boolean) => void + + scrollToItemIndex?: AIReActSubAgentTaskProps['scrollToItemIndex'] } export interface AIReActChatHeaderExternalRightIconProps { rightIcon?: NonNullable['rightIcon'] } + +export interface AIReActSubAgentTaskProps { + scrollToItemIndex?: AIReActChatContentsRef['scrollToItemIndex'] +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.tsx index 76e4036566..2b8dd31fc8 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoListWrapper/AIToDoListWrapper.tsx @@ -14,7 +14,7 @@ export const AIToDoListWrapper: React.FC = React.memo((p const { activeChat } = useAIAgentStore() const store = useCurrentStore() const rawData = useCurrentRawData() - + const currentCasualTaskID = useStore(store, (state) => state.currentCasualTaskID) const todoListUpdate = useStore(store, (state) => state.casualChat?.todoListUpdate) const todoData: TodoListCardData = useCreation(() => { @@ -26,9 +26,14 @@ export const AIToDoListWrapper: React.FC = React.memo((p } }, [todoListUpdate, activeChat?.SessionID]) + const reActTaskId: string = useCreation(() => { + if (!activeChat?.SessionID) return '' + return rawData.casualChat.planDetails?.taskId ?? '' + }, [todoListUpdate, activeChat?.SessionID]) + return ( <> - {todoData?.items?.length > 0 && ( + {reActTaskId === currentCasualTaskID && todoData?.items?.length > 0 && (
diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index 04b2208626..b125551ff8 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -152,7 +152,7 @@ export const AIReActChatContents: React.FC = React.me listRootRef, }) - useImperativeHandle(ref, () => ({ scrollToItemIndex: locateToIndex }), []) + useImperativeHandle(ref, () => ({ scrollToItemIndex: (index, behavior) => locateToIndex(index, behavior) }), []) const renderItem = useCallback((_, item?: ReActChatRenderElement) => { if (!item?.token) return null diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts index 80db8ea0f9..42e409fc8c 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts @@ -1,6 +1,9 @@ +import type { ForwardedRef } from 'react' import type { ChatReferenceMaterialPayload, ChatStream } from '../hooks/aiRender' -export interface AIReActChatContentsPProps {} +export interface AIReActChatContentsPProps { + ref: ForwardedRef +} export interface AIReActChatContentsRef { scrollToItemIndex: (arrayIndex: number, behavior?: 'auto' | 'smooth') => void diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiTaskContent/AITaskContent.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiTaskContent/AITaskContent.tsx index 9f1e32c320..d672b46c50 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiTaskContent/AITaskContent.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiTaskContent/AITaskContent.tsx @@ -2,7 +2,7 @@ import React, { ReactNode, useEffect, useRef, useState } from 'react' import { AITaskContentProps } from './type' import { YakitSideTab } from '@/components/yakitSideTab/YakitSideTab' import { AIAgentTriggerEventInfo, AITabsEnumType } from '@/pages/ai-agent/aiAgentType' -import { YakitSideTabProps, YakitTabsProps } from '@/components/yakitSideTab/YakitSideTabType' +import { YakitTabsProps } from '@/components/yakitSideTab/YakitSideTabType' import styles from './AITaskContent.module.scss' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { useInViewport, useMemoizedFn } from 'ahooks' @@ -31,7 +31,7 @@ export const AITaskContent: React.FC = React.memo((props) => const store = useCurrentStore() const taskChat = useStore(store, (state) => state.taskChat) - const [tabs, setTabs, getTabs] = useGetSetState([]) + const [tabs, setTabs, getTabs] = useGetSetState([]) const [activeKey, setActiveKey] = useState('taskContent') const [scrollToBottom, setScrollToBottom] = useState(false) /** 任务规划关闭后用 display:none 保留,不销毁 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useAIItemKind.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useAIItemKind.ts new file mode 100644 index 0000000000..c3a79a13e1 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useAIItemKind.ts @@ -0,0 +1,17 @@ +import { useMemoizedFn } from 'ahooks' +import { useCurrentStore } from './useCurrentDataBySession' + +function useAIItemKind() { + const store = useCurrentStore() + + const getKind = useMemoizedFn((token: string) => { + const state = store.getState() + if (state.items[token]) return 'item' + if (state.groups[token]) return 'group' + if (state.tasks[token]) return 'task' + return null + }) + return getKind +} + +export default useAIItemKind diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts index e95b48cdb4..8c341c482a 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts @@ -30,6 +30,7 @@ export const getImageStoreKeyByAISource = (source: AISource): ChatDataStoreKey = switch (source) { case AISourceEnum.aiAgent: case AISourceEnum.other: + case AISourceEnum.im: return 'aiChatDataStore' case AISourceEnum.history: return 'histroyAiStore' From 82ad167a3165f3223d5296f627516aa22ce286aa Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Mon, 13 Jul 2026 16:12:29 +0800 Subject: [PATCH 026/101] =?UTF-8?q?1.feat:=E9=BB=98=E8=AE=A4=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=EF=BC=8CTASK=5FDEFAULT=5FGROUP=202.feat:=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E7=BB=84=E7=BB=84=E4=BB=B6=E6=8A=BD=E7=A6=BB=EF=BC=8C?= =?UTF-8?q?TASK=5FNODE=5FGROUP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AITaskDefaultGroupCard.module.scss | 50 ---- .../AITaskDefaultGroupCard.tsx | 122 ++++----- .../AITaskDefaultGroupContent.tsx | 11 +- .../AITaskDefaultGroupCardHeard.module.scss | 49 ++++ .../AITaskDefaultGroupCardHeard.tsx | 36 +++ .../aiTaskDefaultGroupCardHeard/type.ts | 8 + .../components/AITaskDefaultGroupCard/type.ts | 13 + .../ConcurrentStreamCard.module.scss | 24 +- .../ConcurrentStreamCard.tsx | 233 ++++++++---------- .../ConcurrentStreamCardActions.tsx | 105 +++++++- .../ConcurrentStreamContent.module.scss | 14 ++ .../ConcurrentStreamContent.tsx | 94 ++++--- .../ConcurrentStreamContent/type.ts | 3 + .../ConcurrentStreamCardHeard.module.scss | 63 +++++ .../ConcurrentStreamCardHeard.tsx | 63 +++++ .../concurrentStreamCardHeard/type.ts | 12 + .../components/ConcurrentStreamCard/type.ts | 14 ++ .../aiChatListItem/AIChatListItem.tsx | 146 +---------- .../aiChatListItem/aiTaskItem/AITaskItem.tsx | 20 +- .../aiChatListItem/aiTaskItem/type.ts | 4 +- .../aiReActTaskChat/useTaskChatExtraAction.ts | 8 +- 21 files changed, 615 insertions(+), 477 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/type.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.module.scss index d264971022..5dcd10b0f2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.module.scss @@ -11,56 +11,6 @@ flex-direction: column; gap: 4px; - &-title { - height: 20px; - display: flex; - justify-content: space-between; - align-items: center; - cursor: pointer; - - &-left { - display: flex; - align-items: center; - gap: 4px; - min-width: 0; - flex: 1; - - .icon, - .icon svg { - width: 16px; - height: 16px; - flex-shrink: 0; - color: var(--Colors-Use-Neutral-Disable); - } - - .text { - color: var(--Colors-Use-Neutral-Text-1-Title); - font-size: 12px; - font-style: normal; - font-weight: 500; - line-height: 16px; - letter-spacing: 0.5px; - flex-shrink: 0; - } - - .time { - color: var(--Colors-Use-Neutral-Disable); - font-size: 11px; - font-style: normal; - font-weight: 400; - line-height: 14px; - letter-spacing: 0.5px; - @include mixin.text-ellipsis(1, false, false, true); - } - } - - &-right { - display: flex; - align-items: center; - flex-shrink: 0; - } - } - &-content { max-height: 156px; min-height: 0; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx index 9f72dfab23..38c673aa05 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx @@ -1,62 +1,25 @@ -import { - type ChatTaskDefaultGroup, - type ReActChatElement, - type ReActChatTaskElementSub, -} from '@/pages/ai-re-act/hooks/aiRender' -import { type FC, memo, useEffect, useMemo, useState } from 'react' +import { type FC, memo, useEffect, useState } from 'react' import classNames from 'classnames' -import { OutlineInformationcircleIcon } from '@/assets/icon/outline' -import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import styles from './AITaskDefaultGroupCard.module.scss' -import ConcurrentStreamCardActions from '../ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions' import { useBoolean, useCreation } from 'ahooks' -import { formatTimestamp } from '@/utils/timeUtil' -import { useConcurrentStreamRefreshListener } from '../ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener' import AITaskDefaultGroupContent from './AITaskDefaultGroupContent' import { useCurrentStore, useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' +import AITaskDefaultGroupCardHeard from './aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard' +import { AITaskDefaultGroupCardHeardWrapperProps, AITaskDefaultGroupCardListWrapperProps } from './type' const AITaskDefaultGroupCard: FC<{ - elements: ReActChatTaskElementSub[] - session: string token: string - chatType: ReActChatElement['chatType'] - isChildWindow?: boolean - onRefresh?: () => void -}> = memo(({ elements, session, token, chatType, isChildWindow, onRefresh }) => { - const { t } = useI18nNamespaces(['aiAgent']) + isChildWindow: boolean +}> = memo(({ token, isChildWindow }) => { const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean(true) const [contentFocused, setContentFocused] = useState(false) - const store = useCurrentStore() - const renderNum = useStore(store, (state) => state.items[token].renderNum) - const rawData = useCurrentRawData() - - const raw = useCreation(() => { - if (!rawData) return null - const itemData = rawData.contents.get(token) - if (!itemData) return null - return { ...itemData } as ChatTaskDefaultGroup - }, [renderNum]) - - const framePayload = useMemo( - () => ({ - session, - token, - chatType, - elements, - taskName: t('ConcurrentStreamCard.systemInfo'), - }), - [chatType, elements, session, t, token], - ) - useEffect(() => { if (isChildWindow) return collapseExpand() }, [collapseExpand, isChildWindow]) - useConcurrentStreamRefreshListener(framePayload, session, token, chatType, !isChildWindow) - return (
-
-
- - - - {t('ConcurrentStreamCard.systemInfo')} - {raw?.Timestamp ? {formatTimestamp(raw.Timestamp)} : null} -
-
e.stopPropagation()}> - -
-
- {expand ? ( - - ) : null} + + + {expand ? : null}
) }) export default AITaskDefaultGroupCard + +const AITaskDefaultGroupCardListWrapper: FC = memo((props) => { + const { token, setContentFocused } = props + const store = useCurrentStore() + const childrenTokens = useStore(store, (state) => state.tasks[token].childrenTokens || []) + + return ( + + ) +}) + +const AITaskDefaultGroupCardHeardWrapper: FC = memo((props) => { + const { token, expandToggle, expand, isChildWindow } = props + const store = useCurrentStore() + const renderNum = useStore(store, (state) => state.tasks[token].renderNum) + const rawData = useCurrentRawData() + const timeStamp = useCreation(() => { + if (!rawData) return 0 + const itemData = rawData.contents.get(token) + if (!itemData) return 0 + return itemData.Timestamp || 0 + }, [renderNum]) + return ( + + ) +}) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupContent.tsx index a8142483e7..98dea18c99 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupContent.tsx @@ -1,4 +1,3 @@ -import { type ReActChatTaskElementSub } from '@/pages/ai-re-act/hooks/aiRender' import useClickFocus from '@/pages/ai-re-act/hooks/useClickFocus' import { type FC, useEffect, useLayoutEffect } from 'react' import classNames from 'classnames' @@ -6,11 +5,10 @@ import styles from './AITaskDefaultGroupCard.module.scss' import ConcurrentStreamContent from '../ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent' const AITaskDefaultGroupContent: FC<{ - elements: ReActChatTaskElementSub[] - session: string + childrenTokens: string[] isChildWindow?: boolean onContentFocusChange?: (focused: boolean) => void -}> = ({ elements, session, isChildWindow, onContentFocusChange }) => { +}> = ({ childrenTokens, isChildWindow, onContentFocusChange }) => { const { ref: contentRef, isFocus } = useClickFocus() useEffect(() => { @@ -31,7 +29,7 @@ const AITaskDefaultGroupContent: FC<{ requestAnimationFrame(scrollToBottom) }) return () => cancelAnimationFrame(rafId) - }, [contentRef, elements.length]) + }, [contentRef, childrenTokens.length]) return (
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.module.scss new file mode 100644 index 0000000000..f7f8e8636f --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.module.scss @@ -0,0 +1,49 @@ +.ai-task-default-group-card-title { + height: 20px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + + &-left { + display: flex; + align-items: center; + gap: 4px; + min-width: 0; + flex: 1; + + .icon, + .icon svg { + width: 16px; + height: 16px; + flex-shrink: 0; + color: var(--Colors-Use-Neutral-Disable); + } + + .text { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.5px; + flex-shrink: 0; + } + + .time { + color: var(--Colors-Use-Neutral-Disable); + font-size: 11px; + font-style: normal; + font-weight: 400; + line-height: 14px; + letter-spacing: 0.5px; + @include mixin.text-ellipsis(1, false, false, true); + } + } + + &-right { + display: flex; + align-items: center; + flex-shrink: 0; + } +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx new file mode 100644 index 0000000000..503cb7b7de --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx @@ -0,0 +1,36 @@ +import { memo } from 'react' +import { AITaskDefaultGroupCardHeardProps } from './type' +import styles from './AITaskDefaultGroupCardHeard.module.scss' +import { OutlineInformationcircleIcon } from '@/assets/icon/outline' +import { formatTimestamp } from '@/utils/timeUtil' +import { t } from 'i18next' +import ConcurrentStreamCardActions from '../../ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions' + +const AITaskDefaultGroupCardHeard: React.FC = memo((props) => { + const { isChildWindow, expandToggle, timeStamp, expand, onRefresh, token } = props + return ( +
+
+ + + + {t('ConcurrentStreamCard.systemInfo')} + {timeStamp ? {formatTimestamp(timeStamp)} : null} +
+
e.stopPropagation()}> + +
+
+ ) +}) + +export default AITaskDefaultGroupCardHeard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/type.ts new file mode 100644 index 0000000000..70cb7c8a8b --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/type.ts @@ -0,0 +1,8 @@ +export interface AITaskDefaultGroupCardHeardProps { + isChildWindow: boolean + expandToggle: () => void + timeStamp: number + expand: boolean + onRefresh?: () => void + token: string +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts new file mode 100644 index 0000000000..484d6720d0 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts @@ -0,0 +1,13 @@ +import { Dispatch, SetStateAction } from 'react' + +export interface AITaskDefaultGroupCardHeardWrapperProps { + expand: boolean + isChildWindow: boolean + token: string + expandToggle: () => void +} + +export interface AITaskDefaultGroupCardListWrapperProps { + token: string + setContentFocused: Dispatch> +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.module.scss index 227478960c..f177855c9a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.module.scss @@ -22,27 +22,13 @@ padding: 4px 0; @include mixin.text-ellipsis; } -.content { + +.chat-card { + position: relative; width: 100%; - max-height: 500px; - background-color: var(--Colors-Use-Basic-Background); - padding: 4px; + padding: 4px 8px; + background-color: var(--Colors-Use-Neutral-Bg-Hover); border-radius: 8px; -} - -.concurrent-stream-content { - height: 100%; display: flex; flex-direction: column; } - -.task-name { - width: 100%; - color: var(--Colors-Use-Neutral-Text-1-Title); - font-size: 12px; - font-weight: 500; - line-height: 16px; - letter-spacing: 0.5px; - min-width: 0; - @include mixin.text-ellipsis(1, false, false, true); -} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index a094dc54e0..7188c5d157 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -1,45 +1,33 @@ -import { useEffect, useMemo, type FC } from 'react' -import ChatCard from '../ChatCard' -import ModalInfo from '../ModelInfo' +import { memo, useEffect, useMemo, type FC } from 'react' import { useBoolean, useCreation, useMemoizedFn } from 'ahooks' import styles from './ConcurrentStreamCard.module.scss' -import ConcurrentStreamContent from './ConcurrentStreamContent/ConcurrentStreamContent' -import { - AIChatQSDataTypeEnum, - ChatTaskDefaultGroup, - type ChatTaskNodeGroup, - type ChatListRenderType, - type ReActChatTaskElementSub, -} from '@/pages/ai-re-act/hooks/aiRender' +import { AIChatQSDataTypeEnum, ChatTaskDefaultGroup, type ChatTaskNodeGroup } from '@/pages/ai-re-act/hooks/aiRender' import { getAIStatusPresentation } from '../../utils/AIStatusUtils' -import { CHILD_CONTENT_WINDOW_STYLE } from './constants' import { useVectorStripeBg } from './hooks/useVectorStripeBg' import { useConcurrentStreamCardStyle } from './hooks/useConcurrentStreamCardStyle' -import ConcurrentStreamCardActions from './ConcurrentStreamCardActions/ConcurrentStreamCardActions' -import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' -import { useConcurrentStreamRefreshListener } from './concurrentStream/useConcurrentStreamRefreshListener' -import { AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' -import emiter from '@/utils/eventBus/eventBus' -import { yakitNotify } from '@/utils/notification' import { useCurrentStore, useCurrentRawData, useCurrentMeta } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' +import type { + ConcurrentStreamCardHeardWrapperProps, + ConcurrentStreamCardListWrapperProps, + ConcurrentStreamCardTipProps, +} from './type' +import classNames from 'classnames' +import ConcurrentStreamCardHeard from './concurrentStreamCardHeard/ConcurrentStreamCardHeard' +import ConcurrentStreamContent from './ConcurrentStreamContent/ConcurrentStreamContent' const ConcurrentStreamCard: FC<{ - elements: ReActChatTaskElementSub[] - session: string token: string - chatType: ChatListRenderType - isChildWindow?: boolean - onRefresh?: () => void -}> = ({ session, elements, chatType, token, isChildWindow, onRefresh }) => { - const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean(isChildWindow || chatType !== 'reAct') - - const { t } = useI18nNamespaces(['aiAgent']) - + isChildWindow: boolean +}> = memo(({ token, isChildWindow }) => { const store = useCurrentStore() - const renderNum = useStore(store, (state) => state.items[token].renderNum) const rawData = useCurrentRawData() - const metaData = useCurrentMeta() + + const renderNum = useStore(store, (state) => state.tasks[token].renderNum) + + const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean( + isChildWindow || rawData.contents.get(token)?.chatType !== 'reAct', + ) const raw = useCreation(() => { if (!rawData) return null @@ -48,50 +36,22 @@ const ConcurrentStreamCard: FC<{ return { ...itemData } as ChatTaskNodeGroup | ChatTaskDefaultGroup | undefined }, [renderNum]) - // 是否是默认任务分组 - const isTaskDefaultGroup = raw?.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP - const presentation = useMemo(() => getAIStatusPresentation(raw?.data?.status), [raw?.data?.status]) - const vectorBg = useVectorStripeBg(presentation.stripeColor) - - const titleText = useMemo(() => { - if (isTaskDefaultGroup) { - return t('ConcurrentStreamCard.systemInfo') - } - return raw?.data?.taskName || '' - }, [isTaskDefaultGroup, raw?.data?.taskName, t]) - - const framePayload = useMemo( - () => ({ - session, - token, - chatType, - elements, - taskName: titleText, - }), - [chatType, elements, session, titleText, token], - ) - useEffect(() => { if (isChildWindow) return if (!raw?.data?.status) return if (raw.data.status !== 'processing') { collapseExpand() } - }, [collapseExpand, isChildWindow, raw?.data?.status]) + }, [isChildWindow, raw?.data?.status]) - useConcurrentStreamRefreshListener(framePayload, session, token, chatType, !isChildWindow) + // useConcurrentStreamRefreshListener(framePayload, session, token, chatType, !isChildWindow) - const modalInfo = useMemo(() => { - if (!raw) return undefined - return { time: isTaskDefaultGroup ? 0 : raw.Timestamp, title: raw.AIModelName, icon: raw.AIService } - }, [isTaskDefaultGroup, raw]) + const presentation = useMemo(() => getAIStatusPresentation(raw?.data?.status), [raw?.data?.status]) - const coordinatorId = metaData.currentTaskPlanID?.coordinatorId - const taskId = raw?.data?.taskId - const showContinueTask = !!raw && !!coordinatorId && !!taskId && !isChildWindow - const showCancelTask = raw?.data?.status === 'processing' && !!taskId && !isChildWindow + const vectorBg = useVectorStripeBg(presentation.stripeColor) const showStripeBg = !expand && !isChildWindow && !!vectorBg + // TODO - 使用className来控制样式,避免使用style const cardStyle = useConcurrentStreamCardStyle({ bgColor: presentation.bgColor, vectorBg, @@ -99,78 +59,87 @@ const ConcurrentStreamCard: FC<{ isChildWindow, }) - const newElements = useMemo(() => { - if (elements.length === 0) return elements - return elements.filter((item) => item.type !== ('task-dependency-graph' as AIChatQSDataTypeEnum)) - }, [elements]) - - const showDetails = useMemo(() => { - if (isChildWindow) return false - if (!raw) return false - const status = raw?.data?.status - return status === AITaskStatus.created || status === AITaskStatus.inProgress - }, [raw?.data?.status]) - - const onDetails = useMemoizedFn(() => { - const data = raw?.data - if (!data) return - if (!data.taskId) { - yakitNotify('error', 'taskId为空') - return - } - emiter.emit( - 'actionAITaskContentTab', - JSON.stringify({ - type: 'add', - params: { - key: data.taskId, - label: data.taskName, - goal: data.goal, - }, - }), - ) - }) return ( - {titleText}
} - titleExtra={modalInfo && } - style={cardStyle} - childStyle={isChildWindow ? CHILD_CONTENT_WINDOW_STYLE : undefined} - onClickTitle={() => { - if (!isChildWindow) expandToggle() - }} - titleMore={ - - } - > +
+ {expand && ( -
-
{raw?.data?.goal}
- -
+ <> + + + )} - +
) -} +}) export default ConcurrentStreamCard + +const ConcurrentStreamCardHeardWrapper: FC = memo((props) => { + const { token, isChildWindow, expand, expandToggle } = props + const store = useCurrentStore() + const rawData = useCurrentRawData() + const metaData = useCurrentMeta() + + const renderNum = useStore(store, (state) => state.tasks[token].renderNum) + + const raw = useCreation(() => { + if (!rawData) return undefined + const itemData = rawData.contents.get(token) + if (!itemData) return undefined + switch (itemData.type) { + case AIChatQSDataTypeEnum.TASK_NODE_GROUP: + return itemData + + default: + return undefined + } + }, [renderNum]) + + const onClickTitle = useMemoizedFn(() => { + if (!isChildWindow) expandToggle() + }) + return ( + + ) +}) + +const ConcurrentStreamCardTip: FC = memo((props) => { + const { token } = props + const store = useCurrentStore() + const rawData = useCurrentRawData() + + const renderNum = useStore(store, (state) => state.tasks[token].renderNum) + + const goal = useCreation(() => { + const itemData = rawData.contents.get(token) + if (!itemData) return null + switch (itemData.type) { + case AIChatQSDataTypeEnum.TASK_NODE_GROUP: + return itemData?.data?.goal || '' + + default: + return '' + } + }, [renderNum]) + return
{goal}
+}) +const ConcurrentStreamCardListWrapper: FC = memo((props) => { + const { token } = props + const store = useCurrentStore() + const childrenTokens = useStore(store, (state) => state.tasks[token].childrenTokens || []) + + return +}) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx index 18bc540b55..8af0869fd9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx @@ -9,10 +9,13 @@ import { OutlineRefreshIcon, } from '@/assets/icon/outline' import { AIHistoryContinueTask, AIHistorySkipTask } from '../../../chatTemplate/historyTaskTree/HistoryTaskTree' -import type { OpenAIConcurrentStreamPayload } from '@/utils/openWebsite' -import { openAIConcurrentStream } from '@/utils/openWebsite' +import { openAIConcurrentStream, OpenAIConcurrentStreamPayload } from '@/utils/openWebsite' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import styles from '../ConcurrentStreamCard.module.scss' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useCreation, useMemoizedFn } from 'ahooks' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' +import { ReActChatElement, ReActChatTaskElementSub } from '@/pages/ai-re-act/hooks/aiRender' /** 卡片标题栏右侧操作区 */ interface ConcurrentStreamCardActionsProps { @@ -21,7 +24,9 @@ interface ConcurrentStreamCardActionsProps { onExpandToggle: () => void onRefresh?: () => void onDetails?: () => void - framePayload: OpenAIConcurrentStreamPayload + + token: string + showContinueTask: boolean showCancelTask: boolean showDetails: boolean @@ -35,15 +40,100 @@ const ConcurrentStreamCardActions: FC = ({ onExpandToggle, onRefresh, onDetails, - framePayload, showContinueTask, showCancelTask, showDetails, coordinatorId, taskId, + token, }) => { const { t } = useI18nNamespaces(['aiAgent']) + const store = useCurrentStore() + const session = useCurrentSessionId() + const rawData = useCurrentRawData() + const chatType = useCreation(() => { + if (!rawData) return + const itemData = rawData.contents.get(token) + if (!itemData) return + return itemData.chatType + }, []) + + const getKind = useMemoizedFn((childToken) => { + const state = store.getState() + if (state.items[childToken]) return 'item' + if (state.groups[childToken]) return 'group' + return null + }) + + // TODO - 待优化 + const openChildWindow = useMemoizedFn((e) => { + e?.stopPropagation() + if (!chatType) return + const elements: ReActChatTaskElementSub[] = [] + const childrenTokens = store.getState().tasks[token]?.childrenTokens || [] + + const length = childrenTokens.length + + const getItem = (tokenList) => { + const items: ReActChatElement[] = [] + for (const token in tokenList) { + const childData = rawData.contents.get(token) + if (!childData) continue + const item: ReActChatElement = { + kind: 'item', + token, + type: childData?.type, + chatType: childData?.chatType, + renderNum: store.getState().tasks[token]?.renderNum, + } + items.push(item) + } + return items + } + for (let index = 0; index < length; index++) { + const childToken = childrenTokens[index] + const kind = getKind(childToken) + if (!kind) continue + const childData = rawData.contents.get(childToken) + + if (!childData) continue + switch (kind) { + case 'group': + const groupItem: ReActChatTaskElementSub = { + kind: 'group', + token: childToken, + type: childData?.type, + chatType: childData?.chatType, + children: getItem(store.getState().groups[token]?.childrenTokens), + renderNum: store.getState().tasks[childToken]?.renderNum, + } + elements.push(groupItem) + break + case 'item': + const item: ReActChatTaskElementSub = { + kind: 'item', + token: childToken, + type: childData?.type, + chatType: childData?.chatType, + renderNum: store.getState().tasks[childToken]?.renderNum, + } + elements.push(item) + break + default: + break + } + } + + const framePayload: OpenAIConcurrentStreamPayload = { + session, + token, + chatType, + elements, + } + openAIConcurrentStream(framePayload) + }) + if (isChildWindow) { return ( @@ -62,7 +152,7 @@ const ConcurrentStreamCardActions: FC = ({ {showContinueTask && coordinatorId != null && !!taskId && ( )} - {showCancelTask && !!taskId && } + {showCancelTask && !!taskId && } {showDetails && ( } type="text2" onClick={onDetails} /> @@ -73,10 +163,7 @@ const ConcurrentStreamCardActions: FC = ({ size="small" type="text" icon={} - onClick={(e) => { - e.stopPropagation() - openAIConcurrentStream(framePayload) - }} + onClick={openChildWindow} className={styles['expand-btn']} /> diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss index 7a292cf42f..6c3298bac9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss @@ -25,3 +25,17 @@ max-height: none; overflow: visible; } + +.concurrent-stream-content-wrapper { + height: 100%; + display: flex; + flex-direction: column; + flex: 1; + .content { + width: 100%; + max-height: 500px; + background-color: var(--Colors-Use-Basic-Background); + padding: 4px; + border-radius: 8px; + } +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx index ddd6e4c1ba..d9f4d532b8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx @@ -1,29 +1,30 @@ -import type { ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' import { type FC, memo, type RefObject, useEffect, useLayoutEffect, useRef, useState } from 'react' -import { useLatest, useMemoizedFn } from 'ahooks' +import { useCreation, useLatest, useMemoizedFn } from 'ahooks' import classNames from 'classnames' import useClickFocus from '../../../../ai-re-act/hooks/useClickFocus' import styles from './ConcurrentStreamContent.module.scss' import { AIChatListItem } from '../../aiChatListItem/AIChatListItem' +import type { ConcurrentStreamContentItemProps } from './type' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' const PAGE_SIZE = 20 interface ConcurrentStreamContentProps { - session: string - elements: ReActChatRenderItem[] + childrenTokens: string[] isChildWindow?: boolean /** 由父级承担滚动时传入,避免嵌套双滚动容器 */ scrollContainerRef?: RefObject } const ConcurrentStreamContent: FC = memo( - ({ elements, session, isChildWindow, scrollContainerRef }) => { + ({ childrenTokens, isChildWindow, scrollContainerRef }) => { const { ref: scrollRef, isFocus } = useClickFocus() const contentMeasureRef = useRef(null) const embedInParentScroll = !!scrollContainerRef /** 当前渲染起始下标,初始从末尾 PAGE_SIZE 处开始,新增元素始终可见 */ - const [startIndex, setStartIndex] = useState(() => Math.max(0, elements.length - PAGE_SIZE)) + const [startIndex, setStartIndex] = useState(() => Math.max(0, childrenTokens.length - PAGE_SIZE)) /** 加载更多时保存旧滚动高度,用于恢复位置 */ const prevScrollHeightRef = useRef(0) const loadingMoreRef = useRef(false) @@ -89,7 +90,7 @@ const ConcurrentStreamContent: FC = memo( cancelAnimationFrame(rafId2) cancelAnimationFrame(rafId) } - }, [elements.length, getScrollEl, scrollContainerRef]) + }, [childrenTokens.length, getScrollEl, scrollContainerRef]) /** 加载旧数据后保持滚动位置不跳动 */ useLayoutEffect(() => { @@ -120,39 +121,62 @@ const ConcurrentStreamContent: FC = memo( return () => scrollEl.removeEventListener('scroll', handleScroll) }, [embedInParentScroll, handleScroll, scrollContainerRef]) - const visibleElements = elements.slice(startIndex) - return ( -
-
- {visibleElements.map((item, index) => ( -
- +
+
) }, ) export default ConcurrentStreamContent + +const ConcurrentStreamContentItem: FC = memo(({ token }) => { + const store = useCurrentStore() + const rawData = useCurrentRawData() + const getKind = useMemoizedFn((childToken: string): ReActChatRenderElement['kind'] => { + const state = store.getState() + if (state.items[childToken]) return 'item' + if (state.groups[childToken]) return 'group' + return 'item' + }) + const item = useCreation(() => { + const data = rawData.contents.get(token) + return { + token, + kind: getKind(token), + chatType: data?.chatType ?? 'reAct', + isHistory: false, + } + }, [token]) + return ( +
+ +
+ ) +}) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/type.ts new file mode 100644 index 0000000000..f9e80e5dd4 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/type.ts @@ -0,0 +1,3 @@ +export interface ConcurrentStreamContentItemProps { + token: string +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.module.scss new file mode 100644 index 0000000000..b5693867d7 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.module.scss @@ -0,0 +1,63 @@ +.chat-card-title { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0 8px; + + &-left { + display: flex; + align-items: center; + gap: 4px; + // padding: 4px 8px 0 0; + flex: 1; + min-width: 0; + } + + &-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + + svg { + width: 100%; + height: 100%; + color: var(--Colors-Use-Neutral-Text-2-Primary); + } + } + + &-text { + flex: 1; + min-width: 0; + overflow: hidden; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.5px; + color: var(--Colors-Use-Neutral-Text-1-Title); + @include mixin.text-ellipsis(1, false, false, true); + + width: 100%; + } + + &-extra { + flex-shrink: 0; + min-width: 0; + max-width: 100%; + @include mixin.text-ellipsis(1, false, false, true); + + &:empty { + display: none; + } + } + + &-more { + display: flex; + flex-shrink: 0; + justify-content: flex-end; + align-items: center; + overflow: hidden; + } +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx new file mode 100644 index 0000000000..938316cfdf --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx @@ -0,0 +1,63 @@ +import { FC, memo, useMemo } from 'react' +import ModalInfo from '../../ModelInfo' +import ConcurrentStreamCardActions from '../ConcurrentStreamCardActions/ConcurrentStreamCardActions' +import type { ConcurrentStreamCardHeardProps } from './type' +import styles from './ConcurrentStreamCardHeard.module.scss' +import { getAIStatusPresentation } from '@/pages/ai-agent/utils/AIStatusUtils' +import { AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' +import useCreation from 'ahooks/lib/useCreation' + +const ConcurrentStreamCardHeard: FC = memo((props) => { + const { token, isChildWindow, onClickTitle, rowData, coordinatorId, expand, expandToggle } = props + + const titleText = useMemo(() => { + return rowData?.data?.taskName || '' + }, [rowData?.data?.taskName]) + + const presentation = useMemo(() => getAIStatusPresentation(rowData?.data?.status), [rowData?.data?.status]) + + const showContinueTask = useCreation(() => { + return !!rowData && !!coordinatorId && !!rowData?.data?.taskId && !isChildWindow + }, [rowData?.data, coordinatorId]) + + const showCancelTask = useCreation(() => { + return rowData?.data?.status === 'processing' && !!rowData?.data?.taskId && !isChildWindow + }, [rowData?.data]) + + const modalInfo = useMemo(() => { + if (!rowData) return undefined + return { time: rowData.Timestamp, title: rowData.AIModelName, icon: rowData.AIService } + }, [rowData?.Timestamp, rowData?.AIModelName, rowData?.AIService]) + + const showDetails = useMemo(() => { + if (isChildWindow) return false + if (!rowData) return false + const status = rowData?.data?.status + return status === AITaskStatus.created || status === AITaskStatus.inProgress + }, [rowData?.data?.status]) + + return ( +
+
+ {presentation.icon &&
{presentation.icon}
} +
{titleText}
+
{modalInfo && }
+
+
+ +
+
+ ) +}) + +export default ConcurrentStreamCardHeard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/type.ts new file mode 100644 index 0000000000..b44caa0171 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/type.ts @@ -0,0 +1,12 @@ +import { ChatTaskNodeGroup } from '@/pages/ai-re-act/hooks/aiRender' + +export interface ConcurrentStreamCardHeardProps { + token: string + isChildWindow: boolean + expand: boolean + expandToggle: () => void + onClickTitle: () => void + rowData?: ChatTaskNodeGroup + coordinatorId?: string + taskIndex?: string | null +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/type.ts new file mode 100644 index 0000000000..0997259568 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/type.ts @@ -0,0 +1,14 @@ +export interface ConcurrentStreamCardListWrapperProps { + token: string +} + +export interface ConcurrentStreamCardTipProps { + token: string +} + +export interface ConcurrentStreamCardHeardWrapperProps { + token: string + isChildWindow: boolean + expand: boolean + expandToggle: () => void +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx index ce76127510..c9476145cf 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/AIChatListItem.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { AIChatListItemProps } from './type' +import type { AIChatListItemProps } from './type' import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useMemoizedFn } from 'ahooks' import { useStore } from 'zustand' @@ -12,162 +12,22 @@ export const AIChatListItem: React.FC = React.memo((props) const store = useCurrentStore() - // 防止没数据出错,所以没从item中取kind + // 防止没有数据的时候出错,所以没从item中取kind const kind = useStore(store, (state) => { if (state.items[item.token]) return 'item' if (state.groups[item.token]) return 'group' if (state.tasks[item.token]) return 'task' return null }) - // const execFileRecord = useStore(store, (state) => state.execFileRecord) - // const { activeChat } = useAIAgentStore() - // const session = sessionProp || activeChat?.SessionID - - // const isStream = useCreation(() => { - // return ( - // item.type === AIChatQSDataTypeEnum.STREAM || - // item.type === AIChatQSDataTypeEnum.STREAM_GROUP || - // item.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP || - // item.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP - // ) - // }, [item.type]) - - // const ChatItemRenderer = useMemoizedFn((itemData: AIChatQSData) => { - // const { type, Timestamp, data, extraValue } = itemData - // switch (type) { - // case AIChatQSDataTypeEnum.QUESTION: - // return ( - // - // ) - // // case AIChatQSDataTypeEnum.STREAM: - // // return - // case AIChatQSDataTypeEnum.RESULT: - // return - // case AIChatQSDataTypeEnum.THOUGHT: - // return ( - // - // ) - // case AIChatQSDataTypeEnum.TOOL_RESULT: - // const fileList = execFileRecord.get(data.callToolId) - // return ( - // - // ) - // case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: - // case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: - // case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: - // case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - // case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: - // if (!!itemData.data.selected) { - // if ( - // type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || - // type === AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE - // ) - // return null - // return - // } else { - // return ( - // - // ) - // } - // case AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION: - // return - // /** 该UI已无效,可以删除 */ - // // case AIChatQSDataTypeEnum.TASK_INDEX_NODE: - // // const dividerCardProps = { - // // status: data?.status as AITaskStatus, - // // desc: data?.goal, - // // name: data?.taskName, - // // success: 0, - // // error: 0, - // // } - // // return - - // case AIChatQSDataTypeEnum.TOOL_CALL_DECISION: - // return - - // case AIChatQSDataTypeEnum.HTTP_FLOW_FUZZ_STATUS: - // return - - // case AIChatQSDataTypeEnum.REPORT_FINISH: - // return - - // case AIChatQSDataTypeEnum.END_PLAN_AND_EXECUTION: - // return ( - // - // ) - // case AIChatQSDataTypeEnum.FAIL_PLAN_AND_EXECUTION: - // case AIChatQSDataTypeEnum.FAIL_REACT: - // return - // case AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED: - // return - // default: - // return <> - // } - // }) - - // const renderContent = useMemoizedFn(() => { - // if (session === undefined) return null - // if (isStream) - // return ( - // - // ) - // return ChatItemRenderer(contentItem)} /> - // }) const renderContent = useMemoizedFn(() => { switch (kind) { case 'item': return case 'group': return - case 'task': - return + return default: return null } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx index a1e9c76ea0..2d2f57e061 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx @@ -1,8 +1,24 @@ import React from 'react' -import { AITaskItemProps } from './type' +import type { AITaskItemProps } from './type' +import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import ConcurrentStreamCard from '../../ConcurrentStreamCard/ConcurrentStreamCard' +import AITaskDefaultGroupCard from '../../AITaskDefaultGroupCard/AITaskDefaultGroupCard' const AITaskItem: React.FC = React.memo((props) => { - return
AITaskItem
+ const { token } = props + const store = useCurrentStore() + + const type = useStore(store, (state) => state.groups[token].type) + switch (type) { + case AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP: + return + case AIChatQSDataTypeEnum.TASK_NODE_GROUP: + return + default: + return <> + } }) export default AITaskItem diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/type.ts index 3d1c8ff048..1a9aa996c2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/type.ts @@ -1,5 +1,3 @@ -import type { ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' - export interface AITaskItemProps { - item: ReActChatRenderElement + token: string } diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts index 3f54297274..298b8fb7e4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts @@ -1,5 +1,5 @@ import { useMemoizedFn } from 'ahooks' -import { AIInputEvent, AIInputEventSyncTypeEnum } from '../hooks/grpcApi' +import { type AIInputEvent, AIInputEventSyncTypeEnum } from '../hooks/grpcApi' import useCurrentSessionId from '../hooks/useCurrentSessionId' import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' @@ -50,7 +50,7 @@ export const useTaskChatExtraAction = () => { /**取消当前指定任务 */ const onStopTask = useMemoizedFn(() => { sendReactCancelTask() - /** TODO - */ + /** TODO - 可以删除该代码,原因:停止任务后会返回结束标识,然后清空review id*/ // if (!!reviewInfo) { // chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) // } @@ -66,7 +66,7 @@ export const useTaskChatExtraAction = () => { SyncID: randomString(8), } onSend({ token: sessionId, type: 'task', params: info }) - /** TODO - */ + /** TODO - 目前多任务并发,出现子任务后review自动走的继续执行,不会出现review */ // if (!!reviewInfo) { // chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) // } @@ -90,7 +90,7 @@ export const useTaskChatExtraAction = () => { onSend({ token: sessionId, type: 'task', params }) meta.currentTaskPlanID = undefined - /** TODO - */ + /** TODO - 可以删除该代码,原因:停止任务后会返回结束标识,然后清空review id,则在review弹窗中不会出现继续执行的按钮 */ // if (!!reviewInfo) { // chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) // } From 1556a6547f18c267c17f0049177a98bc863e8829 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Tue, 14 Jul 2026 13:44:17 +0800 Subject: [PATCH 027/101] =?UTF-8?q?feat:=201.=20=E5=B0=86=E4=BD=BF?= =?UTF-8?q?=E7=94=A8ReActChatElement['chatType']=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E7=9A=84=E5=85=A8=E5=B1=80=E6=9B=BF=E6=8D=A2=E6=88=90ChatListR?= =?UTF-8?q?enderType=202.=20=E6=96=B0=E5=A2=9EAIChatQSDataTypeEnum.STREAM?= =?UTF-8?q?=5FGROUP=E7=B1=BB=E5=9E=8B=E7=9A=84=E8=AF=A6=E6=83=85=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=AE=9A=E4=B9=89=EF=BC=8C=E5=B9=B6=E5=BC=80=E5=8F=91?= =?UTF-8?q?=E5=AF=B9=E5=BA=94=E7=94=9F=E6=88=90=E6=95=B0=E6=8D=AE=E7=9A=84?= =?UTF-8?q?=E9=80=BB=E8=BE=91=203.=20=E8=B0=83=E6=95=B4=E4=BA=86=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=89=80=E5=B1=9EtaskID=E7=9A=84=E7=94=9F=E6=88=90?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E4=B8=8D=E5=9C=A8=E7=94=A8=E5=8D=95?= =?UTF-8?q?=E4=B8=80taskid=EF=BC=8C=E8=80=8C=E6=98=AF=E9=97=AE=E9=A2=98-ta?= =?UTF-8?q?skid=204.=20=E8=B0=83=E6=95=B4=E4=BA=86=E5=8F=82=E8=80=83?= =?UTF-8?q?=E8=B5=84=E6=96=99=E7=B1=BB=E5=9E=8B=E6=95=B0=E6=8D=AE=E7=9A=84?= =?UTF-8?q?=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B9=E6=B3=95?= =?UTF-8?q?handleReferenceMaterial=205.=20=E9=80=BB=E8=BE=91=E5=BC=95?= =?UTF-8?q?=E6=93=8E=E7=B1=BB=E6=96=B0=E5=A2=9E=E4=B8=A4=E4=B8=AA=E6=96=B9?= =?UTF-8?q?=E6=B3=95=EF=BC=9A=20=E5=85=B3=E9=97=AD=E6=AD=A3=E5=9C=A8?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E7=9A=84reviewUI-closeChatReview=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=9F=90=E4=B8=AA=E5=B7=A5=E5=85=B7=E5=8D=A1=E7=89=87?= =?UTF-8?q?-updateToolResult?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../useConcurrentStreamRefreshListener.ts | 4 +- .../hooks/ChatMultiSessionController.ts | 46 +++++- .../src/pages/ai-re-act/hooks/aiRender.ts | 6 +- .../src/pages/ai-re-act/hooks/chatStore.ts | 4 +- .../hooks/grpcStreamHandler/aiOther.ts | 12 +- .../hooks/grpcStreamHandler/aiReview.ts | 10 +- .../hooks/grpcStreamHandler/aiSingleItem.ts | 22 +-- .../hooks/grpcStreamHandler/aiStream.ts | 151 ++++++++++++------ .../hooks/grpcStreamHandler/aiToolResult.ts | 4 +- .../main/src/pages/ai-re-act/hooks/utils.ts | 15 +- 10 files changed, 189 insertions(+), 85 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts index a92ec789e0..3e0ebcf9cd 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import type { ReActChatElement } from '@/pages/ai-re-act/hooks/aiRender' +import type { ChatListRenderType } from '@/pages/ai-re-act/hooks/aiRender' import { openAIConcurrentStream } from '@/utils/openWebsite' import type { ConcurrentStreamFramePayload } from '../concurrentStreamFrame' @@ -10,7 +10,7 @@ export function useConcurrentStreamRefreshListener( framePayload: ConcurrentStreamFramePayload, session: string, token: string, - chatType: ReActChatElement['chatType'], + chatType: ChatListRenderType, enabled = true, ) { useEffect(() => { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 47fb8085e2..9f4040b075 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -16,7 +16,7 @@ import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHand import { genExecTasks, handleTaskPlanEnd, pushLogToOtherWindow } from './utils' import type { AIChatIPCStartParams, AIChatSendParams } from './type' import { yakitNotify } from '@/utils/notification' -import { type AIChatQSData, AIChatQSDataTypeEnum } from './aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum, AIToolResult, ChatListRenderType } from './aiRender' import { aiAgentLogEmitter } from './AIAgentLogEmitter' const { ipcRenderer } = window.require('electron') @@ -529,6 +529,50 @@ export class ChatMultiSessionController { }) } + /** + * 主动关闭展示给用户操作的review + * + * 一般来说,触发这个事件的情况,都是当前review数据无效了 + * 别的处理review数据事件,都由 handleSendMessage 进行处理了 + */ + public closeChatReview(sessionId: string, chatType: ChatListRenderType, reviewToken: string) { + const { store, rawData } = this.ensureSession(sessionId) + + if (chatType === 'reAct') { + const reviewDetail = rawData.contents.get(reviewToken) + rawData.contents.delete(reviewToken) + store.getState().updateCasualReview(reviewToken, 'remove') + if (reviewDetail) { + store.getState().deleteElementNode({ + chatType: 'reAct', + token: reviewDetail.id, + kind: 'item', + taskID: reviewDetail.TaskId || undefined, + onDelContent: (mapKey) => { + rawData.contents.delete(mapKey) + }, + }) + } + } else if (chatType === 'task') { + const currentReview = store.getState().currentPlanReviewToken + if (!currentReview || currentReview !== reviewToken) return + + rawData.contents.delete(currentReview) + store.getState().updateState({ currentPlanReviewToken: '' }) + } + } + + /** 更新某一个指定的工具卡片内容(AIChatQSDataTypeEnum.TOOL_RESULT) */ + public updateToolResult(sessionId: string, mapToken: string, toolResult: Partial) { + const { store, rawData } = this.ensureSession(sessionId) + + const chatDetail = rawData.contents.get(mapToken) + if (!chatDetail || chatDetail.type !== AIChatQSDataTypeEnum.TOOL_RESULT) return + + Object.assign(chatDetail.data.tool, toolResult) + store.getState().incrementNodeVersion(chatDetail.id, 'item') + } + private closeIPCListeners(sessionId: string) { ipcRenderer.removeAllListeners(`${sessionId}-data`) ipcRenderer.removeAllListeners(`${sessionId}-end`) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 16b3d5008b..66b77c72e4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -379,7 +379,7 @@ export interface AIChatQSDataBase { data: U /** id就是token */ id: string - chatType: ReActChatBaseInfo['chatType'] + chatType: ChatListRenderType AIService: AIOutputEvent['AIService'] AIModelName: AIOutputEvent['AIModelName'] Timestamp: AIOutputEvent['Timestamp'] @@ -431,6 +431,10 @@ export type ChatStreamGroup = AIChatQSDataBase type ChatReportFinish = AIChatQSDataBase export type ChatTaskDefaultGroup = AIChatQSDataBase +type ChatStreamGroup = AIChatQSDataBase< + AIChatQSDataTypeEnum.STREAM_GROUP, + { NodeId: AIOutputEvent['NodeId']; NodeIdVerbose: AIOutputEvent['NodeIdVerbose']; lastToken: string } +> export type AIChatQSData = | ChatQuestion diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index d6572bb9d7..292debc9a1 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -191,14 +191,15 @@ export const createChatStore = () => { // stream 合并:追加到已有组 if (isStreamItem && lastGroup?.kind === 'group' && lastGroup.nodeId === node.nodeId) { - lastGroup.childrenTokens.push(node.token) node.groupExtra?.(lastGroup.token, [node.token]) + lastGroup.childrenTokens.push(node.token) return } // stream 合并:两个连续 item 合成新组 if (isStreamItem && lastItem?.type === AIChatQSDataTypeEnum.STREAM && lastItem.nodeId === node.nodeId) { const newGroupToken = `${node.nodeId}-${uuidv4()}` + node.groupExtra?.(newGroupToken, [lastToken, node.token]) state.groups[newGroupToken] = { kind: 'group', token: newGroupToken, @@ -217,7 +218,6 @@ export const createChatStore = () => { isHistory, } } - node.groupExtra?.(newGroupToken, [lastToken, node.token]) return } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index 59aea1d466..a8d791f065 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -317,7 +317,7 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const info = JSON.parse(ipcContent) as AIAgentGrpcApi.ReactTaskChanged - const react_task_id = res.TaskId || info.react_task_id + const react_task_id = info.react_task_id if (['completed', 'aborted'].includes(info.react_task_now_status)) { if (store.getState().currentCasualTaskID && store.getState().currentCasualTaskID === react_task_id) { store.getState().updateState({ cancelCasualLoading: false }) @@ -334,6 +334,10 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { } } // 更新自由对话-执行任务组的状态 + const taskKey = store.getState().currentCasualTaskID + ? `${store.getState().currentCasualTaskID}-${react_task_id}` + : undefined + if (!taskKey) return const taskDetail = rawData.contents.get(react_task_id) if (!taskDetail || taskDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) return taskDetail.data.status = info.react_task_now_status as AITaskStatusType @@ -428,9 +432,13 @@ const handleReactTaskCreated: AIMessageHandler = (request) => { if (!info.react_task_is_sub_agent) return if (!info.react_task_id) return + const taskKey = store.getState().currentCasualTaskID + ? `${store.getState().currentCasualTaskID}-${info.react_task_id}` + : undefined + if (!taskKey) return const chatData: AIChatQSData = { ...genBaseAIChatData(res), - id: info.react_task_id, + id: taskKey, chatType: 'reAct', type: AIChatQSDataTypeEnum.TASK_NODE_GROUP, data: { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index 3409671b8b..a2a7f84d6f 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -132,7 +132,7 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { data: { ...cloneDeep(data) }, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -192,7 +192,7 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { data: { ...cloneDeep(data) }, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -252,7 +252,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { data: cloneDeep(data), TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -319,7 +319,7 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { data: { ...cloneDeep(data) }, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -554,7 +554,7 @@ const handleDetachedPlanReview: AIMessageHandler = (requestInfo) => { data: { ...cloneDeep(data) }, TaskId: generateTaskNodeDataID({ chatType: chatType, - planID: meta.currentTaskPlanID?.coordinatorId, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.coordinatorId, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index 58e7106845..2f0a92e6c1 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -18,7 +18,7 @@ const handleThought: AIMessageHandler = (requestInfo) => { data: thought || '', TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -51,7 +51,7 @@ const handleResult: AIMessageHandler = (requestInfo) => { data: result || '', TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -85,7 +85,7 @@ const handleFailReactTask: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -123,7 +123,7 @@ const handleToolCallDecision: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -157,7 +157,7 @@ const handleFailPlanAndExecution: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -190,7 +190,7 @@ const handleApiRequestFailed: AIMessageHandler = (requestInfo) => { data, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -248,7 +248,7 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -293,7 +293,7 @@ const handleReportFinish: AIMessageHandler = (requestInfo) => { data: nextData, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -329,7 +329,8 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { }) store.getState().updatePlanTree(newPlanTree) - const taskID = info.task.task_id + const taskID = meta.currentTaskPlanID?.taskID ? `${meta.currentTaskPlanID.taskID}-${info.task.task_id}` : undefined + if (!taskID) return const chatDetail = rawData.contents.get(taskID) if (chatDetail && chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { requestInfo.pushLog({ level: 'error', message: `${info.task.index}-push_task数据已存在` }) @@ -379,7 +380,8 @@ const handlePopTask: AIMessageHandler = (requestInfo) => { }) store.getState().updatePlanTree(newPlanTree) - const taskID = info.task.task_id + const taskID = meta.currentTaskPlanID?.taskID ? `${meta.currentTaskPlanID.taskID}-${info.task.task_id}` : undefined + if (!taskID) return const chatDetail = rawData.contents.get(taskID) if (!chatDetail || chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { requestInfo.pushLog({ level: 'error', message: `${info.task.index}-pop_task数据不存在` }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index 7ac9cc64c1..877f297579 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -1,13 +1,54 @@ -import type { AIMessageHandler } from '../type' +import type { AIMessageHandler, AIMessageHandlerParams } from '../type' import type { AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { genBaseAIChatData, generateTaskNodeDataID, isToolStderrStream, isToolStdoutStream } from '../utils' -import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' -import { convertNodeIdToVerbose } from '../defaultConstant' +import { AIChatQSDataTypeEnum } from '../aiRender' +import { AIStreamContentType, convertNodeIdToVerbose } from '../defaultConstant' import { aiAgentLogEmitter } from '../AIAgentLogEmitter' +/** 生成stream_group组数据 */ +const genStreamGroupData = ( + params: { + group: string + tokens: string[] + } & AIMessageHandlerParams, +) => { + const { group, tokens, res, chatType, rawData, store, meta } = params + + const groupDetail = rawData.contents.get(group) + // 设置组数据详情 + if (groupDetail && groupDetail.type === AIChatQSDataTypeEnum.STREAM_GROUP) { + groupDetail.data.lastToken = tokens[tokens.length - 1] + } else { + rawData.contents.set(group, { + id: group, + chatType: chatType, + type: AIChatQSDataTypeEnum.STREAM_GROUP, + data: { + NodeId: res.NodeId, + NodeIdVerbose: res.NodeIdVerbose, + lastToken: tokens[tokens.length - 1], + }, + TaskId: generateTaskNodeDataID({ + chatType, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + taskID: res.TaskId, + isExist: (key) => rawData.contents.has(key), + }), + AIService: '', + AIModelName: '', + Timestamp: res.Timestamp, + }) + } + + tokens.forEach((mapKey) => { + const mapValue = rawData.contents.get(mapKey) + if (mapValue) mapValue.parentGroupKey = group + }) +} + const handleStreamStart: AIMessageHandler = (requestInfo) => { - const { res, chatType, rawData, meta } = requestInfo + const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'stream_start') return if (res.IsSystem || res.IsReason) return @@ -69,7 +110,7 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -107,7 +148,7 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -228,6 +269,10 @@ const handleStream: AIMessageHandler = (requestInfo) => { kind: 'item', type: streamData.type, isHistory: res.IsSync, + nodeId: res.ContentType === AIStreamContentType.DEFAULT ? res.NodeId : undefined, + groupExtra: (group: string, tokens: string[]) => { + genStreamGroupData({ group, tokens, ...requestInfo }) + }, }, }) } @@ -336,25 +381,25 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { const chatData = rawData.contents.get(data.event_uuid) const toolResult = rawData.contents.get(res.CallToolID || '') + if (chatData) { - // 下面的设置: 是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 chatData.reference = (chatData.reference || []).concat([data]) - if (chatData.parentGroupToken) { - store.getState().dispatchStreamingNode({ - chatType: chatType, - parentTaskId: chatData.TaskId, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - isHistory: res.IsSync, - }, - }) + if (store.getState().items[chatData.id]) { + // 属于item元素,已经在UI上渲染了 + store.getState().incrementNodeVersion(chatData.id, 'item') + } else if (store.getState().groups[chatData.id]) { + // 属于group元素,已经在UI上渲染了 + store.getState().incrementNodeVersion(chatData.id, 'group') + } else if (store.getState().tasks[chatData.id]) { + // 属于task元素,已经在UI上渲染了 + store.getState().incrementNodeVersion(chatData.id, 'task') } else if (chatData.type === AIChatQSDataTypeEnum.STREAM) { + // 属于stream类型数据,但未在UI上渲染 if (toolResult && isToolStdoutStream(chatData.data.NodeId)) { // 特殊情况,更新stdout流对应的工具执行结果卡片UI store.getState().incrementNodeVersion(toolResult.id, 'item') } else { + // 触发UI渲染 store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -363,11 +408,16 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { kind: 'item', type: chatData.type, isHistory: res.IsSync, + nodeId: chatData.data.ContentType === AIStreamContentType.DEFAULT ? chatData.data.NodeId : undefined, + groupExtra: (group: string, tokens: string[]) => { + genStreamGroupData({ group, tokens, ...requestInfo }) + }, }, }) } return } else { + // 其余类型,触发UI渲染 store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -379,42 +429,37 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { }, }) } - } else if ( - toolResult && - toolResult.type === AIChatQSDataTypeEnum.TOOL_RESULT && - toolResult.data.stream.EventUUID === data.event_uuid - ) { - toolResult.reference = (toolResult.reference || []).concat([data]) - store.getState().incrementNodeVersion(toolResult.id, 'item') } else { - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - id: data.event_uuid, - chatType: chatType, - type: AIChatQSDataTypeEnum.Reference_Material, - data: { - NodeId: res.NodeId, - NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), - }, - reference: [data], - TaskId: generateTaskNodeDataID({ - chatType, - planID: meta.currentTaskPlanID?.taskID, - taskID: res.TaskId, - isExist: (key) => rawData.contents.has(key), - }), - } - rawData.contents.set(chatData.id, chatData) - store.getState().dispatchStreamingNode({ - chatType: chatType, - parentTaskId: chatData.TaskId, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - isHistory: res.IsSync, - }, - }) + // 数据不存在,直接生成一个参考资料的UI元素 + // 现阶段直接注释,因为UI还没有写入专门的参考资料元素 + // const chatData: AIChatQSData = { + // ...genBaseAIChatData(res), + // id: data.event_uuid, + // chatType: chatType, + // type: AIChatQSDataTypeEnum.Reference_Material, + // data: { + // NodeId: res.NodeId, + // NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), + // }, + // reference: [data], + // TaskId: generateTaskNodeDataID({ + // chatType, + // planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + // taskID: res.TaskId, + // isExist: (key) => rawData.contents.has(key), + // }), + // } + // rawData.contents.set(chatData.id, chatData) + // store.getState().dispatchStreamingNode({ + // chatType: chatType, + // parentTaskId: chatData.TaskId, + // node: { + // token: chatData.id, + // kind: 'item', + // type: chatData.type, + // isHistory: res.IsSync, + // }, + // }) } } // #endregion diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts index 198acb3ba2..19cc7d640d 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -7,7 +7,7 @@ import cloneDeep from 'lodash/cloneDeep' import { DefaultAIToolResult, DefaultToolResultSummary } from '../defaultConstant' const handleToolCallStart: AIMessageHandler = (requestInfo) => { - const { res, chatType, rawData, meta } = requestInfo + const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'tool_call_start') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -34,7 +34,7 @@ const handleToolCallStart: AIMessageHandler = (requestInfo) => { data: toolResult, TaskId: generateTaskNodeDataID({ chatType, - planID: meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts index 45c195af26..edbf249720 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts @@ -31,15 +31,16 @@ export const generateTaskNodeDataID = (params: { isExist: (key: string) => boolean }) => { const { chatType, planID, taskID, isExist } = params + const taskKey = planID ? `${planID}-${taskID}` : undefined - // 不管自由对话还是任务规划, 只要是明确的执行任务组,都会有唯一的taskID值 - if (taskID && isExist(taskID)) return taskID - // 任务规划的默认任务组 - if (chatType === 'task' && planID) { - const defaultKey = `${planID}-default` - if (isExist(defaultKey)) return defaultKey + if (chatType === 'reAct') { + if (taskKey && isExist(taskKey)) return taskKey + return undefined + } else { + if (taskKey && isExist(taskKey)) return taskKey + if (planID) return `${planID}-default` + return undefined } - return undefined } /** 生成AI-UI展示的必须基础数据 */ From a9dcb8d9569bd624a68942ad66ecbb235bc85bb9 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Tue, 14 Jul 2026 16:23:33 +0800 Subject: [PATCH 028/101] =?UTF-8?q?feat:=20=E5=88=A0=E9=99=A4=E6=97=A0?= =?UTF-8?q?=E6=95=88=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 66b77c72e4..2931926c29 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -426,7 +426,6 @@ export type ChatUserManualIntervention = AIChatQSDataBase< AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION, UserManualInterventionContext > -export type ChatStreamGroup = AIChatQSDataBase type ChatHttpFlowFuzzStatus = AIChatQSDataBase type ChatReportFinish = AIChatQSDataBase From 9152033da6c76d8dc30fb517a1ef2e29b58ffe47 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Tue, 14 Jul 2026 16:17:13 +0800 Subject: [PATCH 029/101] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E8=80=81=E7=89=88?= =?UTF-8?q?=E7=9A=84=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD=E5=92=8C=E6=96=B0?= =?UTF-8?q?=E7=89=88=E7=9A=84=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/type/aiChat.d.ts | 10 ++ .../hooks/ChatMultiSessionController.ts | 124 ++++++++++++++++-- .../src/pages/ai-re-act/hooks/aiRender.ts | 4 +- .../src/pages/ai-re-act/hooks/chatStore.ts | 7 + .../hooks/grpcStreamHandler/aiOther.ts | 47 ++++--- .../hooks/grpcStreamHandler/aiSingleItem.ts | 8 +- .../hooks/grpcStreamHandler/aiTaskDetail.ts | 118 ++++++++++++----- .../hooks/grpcStreamHandler/aiToolResult.ts | 1 + .../hooks/grpcStreamHandler/yakExecResult.ts | 2 + 9 files changed, 259 insertions(+), 62 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 847b09e530..91a1fcbac8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -208,6 +208,8 @@ export interface AIAgentChatData { casualChat: { /** react 任务对应的详情数据 */ planDetails: PlanItemDetailsData + /** 自由会话啊的子任务对应的详情数据 */ + planDetailsMap: Map } taskChat: { /** 任务列表的子任务对应的详情数据 */ @@ -221,6 +223,11 @@ export interface AIAgentChatMetaData { onSessionStartSuccess?: (sessionId: string) => void /** 通过用户问题创建会话时的问题 */ createChatQuestion?: AIInputEvent + + /** 建立会话后的ping请求测试连通性-ping的唯一ID和轮询定时器 */ + pingSyncID: string + pingTimer: ReturnType | null + /** 自由对话的实时记忆列表 */ casualMemoryList: AIAgentGrpcApi.MemoryEntryList /** 任务规划的实时记忆列表 */ @@ -267,4 +274,7 @@ export interface AIAgentChatMetaData { /** 轮询获取最新记忆列表的定时器 */ memoryPollingTimer: NodeJS.Timeout | null + + /** 记录自由对话下成组agent任务的taskID */ + casualSubTaskIDs: Set } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 9f4040b075..cf30bf08c9 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -9,7 +9,12 @@ import { } from './grpcApi' import { createChatStore } from './chatStore' import { Uint8ArrayToString } from '@/utils/str' -import { AIAgentSettingDefault, AIModelTypeEnum } from '@/pages/ai-agent/defaultConstant' +import { + AIAgentSettingDefault, + AIModelTypeEnum, + AttachedResourceKeyEnum, + AttachedResourceTypeEnum, +} from '@/pages/ai-agent/defaultConstant' import cloneDeep from 'lodash/cloneDeep' import { DefaultMemoryList, DefaultPlanItemDetailsData } from './defaultConstant' import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHandlers' @@ -18,6 +23,8 @@ import type { AIChatIPCStartParams, AIChatSendParams } from './type' import { yakitNotify } from '@/utils/notification' import { type AIChatQSData, AIChatQSDataTypeEnum, AIToolResult, ChatListRenderType } from './aiRender' import { aiAgentLogEmitter } from './AIAgentLogEmitter' +import { v4 as uuidv4 } from 'uuid' +import moment from 'moment' const { ipcRenderer } = window.require('electron') @@ -95,6 +102,7 @@ const genAIAgentChatData = (): AIAgentChatData => { casualChat: { planDetails: cloneDeep(DefaultPlanItemDetailsData), + planDetailsMap: new Map(), }, taskChat: { planDetailsMap: new Map(), @@ -107,6 +115,8 @@ const genAIAgentChatData = (): AIAgentChatData => { const genAIAgentChatMetaData = (): AIAgentChatMetaData => { return { createChatQuestion: undefined, + pingSyncID: '', + pingTimer: null, casualMemoryList: cloneDeep(DefaultMemoryList), taskMemoryList: cloneDeep(DefaultMemoryList), notifyMessageTimer: null, @@ -124,6 +134,7 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { queuePollingEmptyCount: 0, queuePollingTimer: null, memoryPollingTimer: null, + casualSubTaskIDs: new Set(), } } @@ -198,7 +209,7 @@ export class ChatMultiSessionController { return } - const { request, store, meta } = this.ensureSession(sessionId) + const { request, store, rawData, meta } = this.ensureSession(sessionId) store.getState().updateState({ execute: true, casualTitle: '发送问题,开启会话...' }) this.registerSessionChannel(sessionId, params.Params?.Source) @@ -206,16 +217,63 @@ export class ChatMultiSessionController { Object.assign(request, params.Params) if (params.Params?.UserQuery) { + // 判断建立grpc连接时是否附带问题 + // 如有,需要剥离出来,在grpc建立成功后再执行 + const chatID = uuidv4() + + const AttachedResourceInfos = params.AttachedResourceInfo || [] + AttachedResourceInfos.push({ + Key: AttachedResourceKeyEnum.CONTEXT_PROVIDER_KEY_DEFAULT, + Type: AttachedResourceTypeEnum.USER_FREE_INPUT_UUID, + Value: chatID, + }) + meta.createChatQuestion = { IsFreeInput: true, FreeInput: params.Params.UserQuery, - AttachedResourceInfo: params.AttachedResourceInfo, + AttachedResourceInfo: AttachedResourceInfos, FocusModeLoop: params.FocusModeLoop, } + + // 用户问了问题后,立即显示到UI上 + // 问题对应的re_act_task_id先由前端生成,并发送给后端 + // 后续生成re_act_task_id时,会把前端生成的uuid替换为后端生成的re_act_task_id + const chatData: AIChatQSData = { + id: chatID, + chatType: 'reAct', + type: AIChatQSDataTypeEnum.QUESTION, + Timestamp: moment().unix(), + data: params.Params?.UserQuery || '', + AIService: '', + AIModelName: '', + // showQS为了UI渲染方便,重新构建的字段 + extraValue: { showQS: params.Params?.UserQuery || '' }, + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: 'reAct', + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + }, + }) } meta.onSessionStartSuccess = cb ipcRenderer.invoke('start-ai-re-act', sessionId, params) + + // 建立会话连接时,在主进程进行了一次ping请求 + // 如果五秒没有返回pong消息,则再次进行ping请求 + if (meta.pingTimer) clearInterval(meta.pingTimer) + meta.pingTimer = setInterval(() => { + meta.pingSyncID = uuidv4() + this.requestMessage(sessionId, { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PING, + SyncID: meta.pingSyncID, + }) + }, 5000) } /** 主动向grpc发送请求 */ @@ -229,12 +287,46 @@ export class ChatMultiSessionController { } const { store, rawData, meta } = this.ensureSession(token) - // 自由对话没有问题进行中时,才改变loading的title - if (params.IsFreeInput && !store.getState().casualLoading) { - store.getState().updateState({ casualTitle: '等待回复中...' }) - } if (params.IsFreeInput) { + const { casualLoading, currentCasualTaskID } = store.getState() + // 如果自由对话引起了任务规划,那么自由对话其实是空闲状态 + const isCasualIdle = casualLoading && currentCasualTaskID === meta.currentTaskPlanID?.taskID + + if (!casualLoading || isCasualIdle) { + // 自由对话没有问题进行中时,才改变loading的title + store.getState().updateState({ casualTitle: '等待回复中...' }) + + const chatID = uuidv4() + const AttachedResourceInfos = params.AttachedResourceInfo || [] + AttachedResourceInfos.push({ + Key: AttachedResourceKeyEnum.CONTEXT_PROVIDER_KEY_DEFAULT, + Type: AttachedResourceTypeEnum.USER_FREE_INPUT_UUID, + Value: chatID, + }) + params.AttachedResourceInfo = AttachedResourceInfos + const chatData: AIChatQSData = { + id: chatID, + chatType: 'reAct', + type: AIChatQSDataTypeEnum.QUESTION, + Timestamp: moment().unix(), + data: params.FreeInput || '', + AIService: '', + AIModelName: '', + // showQS为了UI渲染方便,重新构建的字段 + extraValue: { showQS: params.FreeInput || '' }, + } + rawData.contents.set(chatData.id, chatData) + store.getState().dispatchStreamingNode({ + chatType: 'reAct', + node: { + token: chatData.id, + kind: 'item', + type: chatData.type, + }, + }) + } + // 因为有用户问题发送,所以注册 获取问题队列轮询器 if (!meta.queuePollingTimer) { meta.queuePollingEmptyCount = 0 @@ -401,6 +493,15 @@ export class ChatMultiSessionController { } if (res.Type === 'pong') { + // 如果返回的pong没有值,但是pingSyncID有值,说明该条消息已经过期 + if (!res.SyncID && meta.pingSyncID) return + // 如果返回的pong有值,但是和pingSyncID不一样,说明该条消息已经过期 + if (res.SyncID && res.SyncID !== meta.pingSyncID) return + // 该条消息有效,不需要在轮询ping请求了 + if (meta.pingTimer) clearInterval(meta.pingTimer) + meta.pingTimer = null + meta.pingSyncID = '' + if (meta.createChatQuestion) { this.requestMessage(sessionId, meta.createChatQuestion) meta.createChatQuestion = undefined @@ -585,8 +686,15 @@ export class ChatMultiSessionController { } // 监听 session-end 事件 public handleSessionEnd(sessionId: string, res: any) { - const { store } = this.ensureSession(sessionId) + const { store, meta } = this.ensureSession(sessionId) + + // 取消ping请求相关逻辑 + if (meta.pingTimer) clearInterval(meta.pingTimer) + meta.pingTimer = null + meta.pingSyncID = '' + // 任务规划结束后的相关逻辑 handleTaskPlanEnd(this.ensureSession(sessionId)) + // 核心状态改变 store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) this.readyChannels.delete(sessionId) this.closeIPCListeners(sessionId) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 2931926c29..5f464d5771 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -178,7 +178,7 @@ export interface AIStreamOutput { export interface AIToolResult { type: 'create' | 'stream' | 'result' callToolId: string - /**工具名称 */ + /** 工具名称 */ toolName: string /** 工具显示名称(多语言) */ verboseName?: AIOutputI18n @@ -677,4 +677,6 @@ export interface ChatStoreState { onDelContent: (mapKey: string) => void } & Omit, ) + + replaceItemToken(oldToken: string, newToken: string) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index 292debc9a1..47e909fef7 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -318,6 +318,13 @@ export const createChatStore = () => { } } }), + + /** 将指定item的token换成新token */ + replaceItemToken: (oldToken, newToken) => + set((state) => { + state.items[newToken] = state.items[oldToken] + delete state.items[oldToken] + }), })), ) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index a8d791f065..c513879540 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -168,9 +168,11 @@ const handleFileSystemPin: AIMessageHandler = (request) => { } const handleTimelineItem: AIMessageHandler = (request) => { - const { res, store } = request + const { res, store, meta } = request if (res.Type !== 'structured' || res.NodeId !== 'timeline_item') return if (res.IsSync) return + // 自由对话的成组子agent任务的timeline-item不展示 + if (meta.casualSubTaskIDs.has(res.TaskId)) return const ipcContent = Uint8ArrayToString(res.Content) || '' const timelineItem = JSON.parse(ipcContent) as AIAgentGrpcApi.TimelineItem @@ -216,6 +218,16 @@ const handleReactTaskDequeue: AIMessageHandler = (request) => { extraValue: { showQS: data.react_task_input || '' }, } rawData.contents.set(chatData.id, chatData) + + if (data.react_task_user_input_uuid) { + const qsDetail = rawData.contents.get(data.react_task_user_input_uuid) + if (qsDetail && qsDetail.type === AIChatQSDataTypeEnum.QUESTION) { + rawData.contents.delete(data.react_task_user_input_uuid) + store.getState().replaceItemToken(data.react_task_user_input_uuid, data.react_task_id) + return + } + } + store.getState().dispatchStreamingNode({ chatType: 'reAct', node: { @@ -320,13 +332,8 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { const react_task_id = info.react_task_id if (['completed', 'aborted'].includes(info.react_task_now_status)) { if (store.getState().currentCasualTaskID && store.getState().currentCasualTaskID === react_task_id) { - store.getState().updateState({ cancelCasualLoading: false }) - // 取消专注模式 - store.getState().updateState({ focusMode: '' }) + store.getState().updateState({ focusMode: '', cancelCasualLoading: false, casualLoading: false }) } - store.getState().updateState({ casualLoading: false }) - rawData.casualChat.planDetails = cloneDeep(DefaultPlanItemDetailsData) - store.getState().updateCasualTodoList() if (meta.currentTaskPlanID?.taskID === react_task_id) { // 该问题触发了任务规划, 所以需要将任务规划状态也调整 meta.currentTaskPlanID.status = info.react_task_now_status as AITaskStatus @@ -334,11 +341,9 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { } } // 更新自由对话-执行任务组的状态 - const taskKey = store.getState().currentCasualTaskID - ? `${store.getState().currentCasualTaskID}-${react_task_id}` - : undefined - if (!taskKey) return - const taskDetail = rawData.contents.get(react_task_id) + if (!store.getState().currentCasualTaskID || !react_task_id) return + const taskKey = `${store.getState().currentCasualTaskID}-${react_task_id}` + const taskDetail = rawData.contents.get(taskKey) if (!taskDetail || taskDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) return taskDetail.data.status = info.react_task_now_status as AITaskStatusType store.getState().incrementNodeVersion(taskDetail.id, 'task') @@ -421,7 +426,7 @@ const handleYaklangCodeChange: AIMessageHandler = (requestInfo) => { } const handleReactTaskCreated: AIMessageHandler = (request) => { - const { res, chatType, store, rawData } = request + const { res, chatType, store, rawData, meta } = request if (res.Type !== 'structured' || res.NodeId !== 'react_task_created') return if (res.IsSync || chatType === 'task') return @@ -430,12 +435,14 @@ const handleReactTaskCreated: AIMessageHandler = (request) => { // 非聚合任务不处理 if (!info.react_task_is_sub_agent) return - if (!info.react_task_id) return - const taskKey = store.getState().currentCasualTaskID - ? `${store.getState().currentCasualTaskID}-${info.react_task_id}` - : undefined - if (!taskKey) return + if (!store.getState().currentCasualTaskID || !info.react_task_id) return + const taskKey = `${store.getState().currentCasualTaskID}-${info.react_task_id}` + meta.casualSubTaskIDs.add(info.react_task_id) + + const existing = rawData.contents.get(taskKey) + if (existing) return + const chatData: AIChatQSData = { ...genBaseAIChatData(res), id: taskKey, @@ -451,6 +458,10 @@ const handleReactTaskCreated: AIMessageHandler = (request) => { } as AIChatQSData rawData.contents.set(chatData.id, chatData) + // planDetailsMap / 子任务收集仍按后端子任务 ID(res.TaskId)索引 + if (rawData.casualChat.planDetailsMap.has(info.react_task_id)) { + rawData.casualChat.planDetailsMap.set(info.react_task_id, cloneDeep(DefaultPlanItemDetailsData)) + } store.getState().dispatchStreamingNode({ chatType: 'reAct', node: { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index 2f0a92e6c1..e0d23dc350 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -329,8 +329,8 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { }) store.getState().updatePlanTree(newPlanTree) - const taskID = meta.currentTaskPlanID?.taskID ? `${meta.currentTaskPlanID.taskID}-${info.task.task_id}` : undefined - if (!taskID) return + if (!meta.currentTaskPlanID?.taskID || !info.task.task_id) return + const taskID = `${meta.currentTaskPlanID.taskID}-${info.task.task_id}` const chatDetail = rawData.contents.get(taskID) if (chatDetail && chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { requestInfo.pushLog({ level: 'error', message: `${info.task.index}-push_task数据已存在` }) @@ -380,8 +380,8 @@ const handlePopTask: AIMessageHandler = (requestInfo) => { }) store.getState().updatePlanTree(newPlanTree) - const taskID = meta.currentTaskPlanID?.taskID ? `${meta.currentTaskPlanID.taskID}-${info.task.task_id}` : undefined - if (!taskID) return + if (!meta.currentTaskPlanID?.taskID || !info.task.task_id) return + const taskID = `${meta.currentTaskPlanID.taskID}-${info.task.task_id}` const chatDetail = rawData.contents.get(taskID) if (!chatDetail || chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { requestInfo.pushLog({ level: 'error', message: `${info.task.index}-pop_task数据不存在` }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts index d84494cdc2..3890cfe544 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts @@ -1,16 +1,31 @@ -import type { AIMessageHandler } from '../type' -import type { AIAgentGrpcApi } from '../grpcApi' +import type { AIMessageHandler, AIMessageHandlerParams } from '../type' +import type { AIAgentGrpcApi, AIOutputEvent } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { handleTodoListData } from '../utils' import cloneDeep from 'lodash/cloneDeep' import isArray from 'lodash/isArray' import isEmpty from 'lodash/isEmpty' import { v4 as uuidv4 } from 'uuid' -import type { PlanItemDetailsData } from '../aiRender' +import { AIChatQSDataTypeEnum, type PlanItemDetailsData } from '../aiRender' import { DefaultPlanItemDetailsData } from '../defaultConstant' +/** 自由对话:是否为子 agent 任务(planDetailsMap 用子任务 ID;contents 用复合 ID) */ +const isCasualSubAgentTask = ( + rawData: AIMessageHandlerParams['rawData'], + store: AIMessageHandlerParams['store'], + res: AIOutputEvent, +) => { + if (rawData.casualChat.planDetailsMap.has(res.TaskId)) return true + + if (!store.getState().currentCasualTaskID || !res.TaskId) return false + return ( + rawData.contents.get(`${store.getState().currentCasualTaskID}-${res.TaskId}`)?.type === + AIChatQSDataTypeEnum.TASK_NODE_GROUP + ) +} + const handleCapabilityInventory: AIMessageHandler = (requestInfo) => { - const { res, chatType, rawData } = requestInfo + const { res, chatType, rawData, store } = requestInfo if (res.Type !== 'structured' || res.NodeId !== 'capability_inventory') return if (!res.TaskId) return @@ -106,21 +121,34 @@ const handleCapabilityInventory: AIMessageHandler = (requestInfo) => { oldData.mcp = itemData.mcp rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) } else if (chatType === 'reAct') { - // 只能通过字段重新赋值的方式修改,不能解构赋值 - const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) - chatDetail.uuid = itemData.uuid - chatDetail.taskId = chatDetail?.taskId || res.TaskId - chatDetail.tool = itemData.tool - chatDetail.forges = itemData.forges - chatDetail.skills = itemData.skills - chatDetail.plugins = itemData.plugins - chatDetail.mcp = itemData.mcp - rawData.casualChat.planDetails = chatDetail + const isSubAgentTask = isCasualSubAgentTask(rawData, store, res) + if (isSubAgentTask) { + const oldData = rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + oldData.uuid = itemData.uuid + oldData.taskId = oldData?.taskId || res.TaskId + oldData.tool = itemData.tool + oldData.forges = itemData.forges + oldData.skills = itemData.skills + oldData.plugins = itemData.plugins + oldData.mcp = itemData.mcp + rawData.casualChat.planDetailsMap.set(res.TaskId, oldData) + } else { + // 只能通过字段重新赋值的方式修改,不能解构赋值 + const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) + chatDetail.uuid = itemData.uuid + chatDetail.taskId = chatDetail?.taskId || res.TaskId + chatDetail.tool = itemData.tool + chatDetail.forges = itemData.forges + chatDetail.skills = itemData.skills + chatDetail.plugins = itemData.plugins + chatDetail.mcp = itemData.mcp + rawData.casualChat.planDetails = chatDetail + } } } const handlePerception: AIMessageHandler = (requestInfo) => { - const { res, chatType, rawData } = requestInfo + const { res, chatType, rawData, store } = requestInfo if (res.Type !== 'perception' || res.NodeId !== 'perception') return if (!res.TaskId) return @@ -136,11 +164,20 @@ const handlePerception: AIMessageHandler = (requestInfo) => { oldData.perception = perception rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) } else if (chatType === 'reAct') { - const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) - chatDetail.uuid = uuidv4() - chatDetail.taskId = chatDetail.taskId || res.TaskId - chatDetail.perception = perception - rawData.casualChat.planDetails = chatDetail + const isSubAgentTask = isCasualSubAgentTask(rawData, store, res) + if (isSubAgentTask) { + const oldData = rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + oldData.uuid = uuidv4() + oldData.taskId = oldData.taskId || res.TaskId + oldData.perception = perception + rawData.casualChat.planDetailsMap.set(res.TaskId, oldData) + } else { + const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) + chatDetail.uuid = uuidv4() + chatDetail.taskId = chatDetail.taskId || res.TaskId + chatDetail.perception = perception + rawData.casualChat.planDetails = chatDetail + } } } @@ -163,16 +200,25 @@ const handleCurrentTaskTodoListUpdate: AIMessageHandler = (requestInfo) => { rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) } else if (chatType === 'reAct') { const chatDetail = rawData.casualChat?.planDetails - if (!chatDetail) return - chatDetail.uuid = uuidv4() - chatDetail.taskId = chatDetail.taskId || res.TaskId - chatDetail.todoList = newData + const isSubAgentTask = isCasualSubAgentTask(rawData, store, res) + if (isSubAgentTask) { + const oldData = rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + oldData.uuid = uuidv4() + oldData.taskId = oldData.taskId || res.TaskId + oldData.todoList = newData + rawData.casualChat.planDetailsMap.set(res.TaskId, oldData) + } else { + if (!chatDetail) return + chatDetail.uuid = uuidv4() + chatDetail.taskId = chatDetail.taskId || res.TaskId + chatDetail.todoList = newData + } store.getState().updateCasualTodoList() } } const handleSessionSnapshot: AIMessageHandler = (requestInfo) => { - const { res, chatType, rawData } = requestInfo + const { res, chatType, rawData, store } = requestInfo if (res.NodeId !== 'session_snapshot') return if (!res.TaskId) return @@ -187,12 +233,22 @@ const handleSessionSnapshot: AIMessageHandler = (requestInfo) => { oldData.backgroundProcesses = snapshot.background_processes rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) } else if (chatType === 'reAct') { - const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) - chatDetail.uuid = uuidv4() - chatDetail.taskId = chatDetail.taskId || res.TaskId - chatDetail.execution = snapshot.execution - chatDetail.backgroundProcesses = snapshot.background_processes - rawData.casualChat.planDetails = chatDetail + const isSubAgentTask = isCasualSubAgentTask(rawData, store, res) + if (isSubAgentTask) { + const oldData = rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + oldData.uuid = uuidv4() + oldData.taskId = oldData.taskId || res.TaskId + oldData.execution = snapshot.execution + oldData.backgroundProcesses = snapshot.background_processes + rawData.casualChat.planDetailsMap.set(res.TaskId, oldData) + } else { + const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) + chatDetail.uuid = uuidv4() + chatDetail.taskId = chatDetail.taskId || res.TaskId + chatDetail.execution = snapshot.execution + chatDetail.backgroundProcesses = snapshot.background_processes + rawData.casualChat.planDetails = chatDetail + } } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts index 19cc7d640d..4fc8dd5faa 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -21,6 +21,7 @@ const handleToolCallStart: AIMessageHandler = (requestInfo) => { ...cloneDeep(DefaultAIToolResult), callToolId: call_tool_id, toolName: tool?.name || '-', + verboseName: tool?.verbose_name, toolDescription: tool?.description || '', startTime: start_time || 0, startTimeMS: start_time_ms || 0, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts index 4fd320ea37..adeb0115a0 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/yakExecResult.ts @@ -16,6 +16,8 @@ const handleStatus: AIMessageHandler = (request) => { // 任务规划-loading展示标题 store.getState().updateTaskLoadingStatus({ task: data.value || '加载中...' }) } else { + // 只展示自由对话问题的相关title + if (res.TaskId !== store.getState().currentCasualTaskID) return // 自由对话-loading展示标题 store.getState().updateState({ casualTitle: data.value }) } From f8291a0705eb05850eb01db7c562e1f97aee4591 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Tue, 14 Jul 2026 18:04:43 +0800 Subject: [PATCH 030/101] =?UTF-8?q?feat:=20=E4=BB=BB=E5=8A=A1=E8=A7=84?= =?UTF-8?q?=E5=88=92review=E7=9A=84=E7=8A=B6=E6=80=81=E6=9C=BA=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=95=B0=E6=8D=AE=E6=9B=B4=E6=96=B0=E8=A7=A6=E5=8F=91?= =?UTF-8?q?UI=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/ChatMultiSessionController.ts | 10 +-- .../src/pages/ai-re-act/hooks/aiRender.ts | 2 +- .../src/pages/ai-re-act/hooks/chatStore.ts | 2 +- .../hooks/grpcStreamHandler/aiReview.ts | 61 ++++++++++++------- 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index cf30bf08c9..9412945de9 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -386,14 +386,14 @@ export class ChatMultiSessionController { break case 'task': if (params.IsInteractiveMessage && params.InteractiveId) { - const isExist = store.getState().currentPlanReviewToken === params.InteractiveId + const isExist = store.getState().currentPlanReviewToken.token === params.InteractiveId const review = rawData.contents.get(params.InteractiveId) if (isExist || !review) { yakitNotify('error', '未获取到 review 信息, 操作无效') return } - store.getState().updateState({ currentPlanReviewToken: '' }) + store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) switch (review.type) { case AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP: case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: @@ -656,10 +656,10 @@ export class ChatMultiSessionController { } } else if (chatType === 'task') { const currentReview = store.getState().currentPlanReviewToken - if (!currentReview || currentReview !== reviewToken) return + if (!currentReview.token || currentReview.token !== reviewToken) return - rawData.contents.delete(currentReview) - store.getState().updateState({ currentPlanReviewToken: '' }) + rawData.contents.delete(currentReview.token) + store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 5f464d5771..7b4a40feef 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -539,7 +539,7 @@ export interface ChatStoreState { * 任务规划当前显示的review数据 * 通过token去rawData中contents里取数据,token是唯一的 */ - currentPlanReviewToken: string + currentPlanReviewToken: { token: string; renderNum: number } /** * 当前review是plan时,异步数据的更新版本 * aiChat.d.ts AIAgentChatMetaData planReviewExtraData diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index 47e909fef7..b39438cd0d 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -44,7 +44,7 @@ export const createChatStore = () => { taskStatus: cloneDeep(DefaultPlanLoadingStatus), currentCasualReview: [], - currentPlanReviewToken: '', + currentPlanReviewToken: { token: '', renderNum: 0 }, currentPlanReviewExtraUpdate: 0, items: {}, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index a2a7f84d6f..4e95c3a431 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -74,7 +74,7 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { root_task_name: tasks.plans.root_task.name, }) } else { - store.getState().updateState({ currentPlanReviewToken: chatData.id }) + store.getState().updateState({ currentPlanReviewToken: { token: chatData.id, renderNum: 0 } }) } } const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { @@ -95,7 +95,8 @@ const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { return } - const reviewDetail = rawData.contents.get(store.getState().currentPlanReviewToken) + if (!store.getState().currentPlanReviewToken.token) return + const reviewDetail = rawData.contents.get(store.getState().currentPlanReviewToken.token) if (!reviewDetail || reviewDetail.type !== AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE) { requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常: 未找到对应plan_review_require数据` }) return @@ -283,7 +284,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { // 将数据存入hook里的缓存变量中 rawData.contents.set(chatData.id, cloneDeep(chatData)) if (chatType === 'task') { - store.getState().updateState({ currentPlanReviewToken: chatData.id }) + store.getState().updateState({ currentPlanReviewToken: { token: chatData.id, renderNum: 0 } }) } else if (chatType === 'reAct') { store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ @@ -369,7 +370,7 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { } const handleAIReviewJudgement: AIMessageHandler = (requestInfo) => { - const { res, store, rawData } = requestInfo + const { res, chatType, store, rawData } = requestInfo if (!['ai_review_start', 'ai_review_countdown', 'ai_review_end'].includes(res.Type)) return if (res.IsSync) return @@ -397,7 +398,13 @@ const handleAIReviewJudgement: AIMessageHandler = (requestInfo) => { // aiReview 没有或者 aiReview 的 seconds 为空时可以赋值 reviewDetail.data.aiReview = cloneDeep(score) } - store.getState().incrementNodeVersion(reviewDetail.id, 'item') + if (chatType === 'reAct') { + store.getState().incrementNodeVersion(reviewDetail.id, 'item') + } else { + const reviewStore = store.getState().currentPlanReviewToken + reviewStore.renderNum += 1 + store.getState().updateState({ currentPlanReviewToken: { ...reviewStore } }) + } break default: @@ -483,7 +490,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { }, }) // 关闭review的弹窗 - if (chatType === 'task') store.getState().updateState({ currentPlanReviewToken: '' }) + if (chatType === 'task') store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) break case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: @@ -494,7 +501,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { if (chatType === 'reAct') { store.getState().incrementNodeVersion(reviewDetail.id, 'item') if (reviewDetail.type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) { - store.getState().updateState({ currentPlanReviewToken: '' }) + store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) } else { store.getState().updateCasualReview(reviewDetail.id, 'remove') } @@ -508,7 +515,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { type: reviewDetail.type, }, }) - store.getState().updateState({ currentPlanReviewToken: '' }) + store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) } break case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: @@ -546,22 +553,32 @@ const handleDetachedPlanReview: AIMessageHandler = (requestInfo) => { return } - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: chatType, - id: data.id, - type: AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE, - data: { ...cloneDeep(data) }, - TaskId: generateTaskNodeDataID({ + const reviewDetail = rawData.contents.get(data.id) + if (reviewDetail) { + if (reviewDetail.type !== AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) return + reviewDetail.data = { ...data } + const reviewStore = store.getState().currentPlanReviewToken + reviewStore.renderNum += 1 + store.getState().updateState({ currentPlanReviewToken: { ...reviewStore } }) + } else { + const chatData: AIChatQSData = { + ...genBaseAIChatData(res), chatType: chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.coordinatorId, - taskID: res.TaskId, - isExist: (key) => rawData.contents.has(key), - }), + id: data.id, + type: AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE, + data: { ...cloneDeep(data) }, + TaskId: generateTaskNodeDataID({ + chatType: chatType, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.coordinatorId, + taskID: res.TaskId, + isExist: (key) => rawData.contents.has(key), + }), + } + rawData.contents.set(chatData.id, cloneDeep(chatData)) + if (chatType === 'reAct') { + store.getState().updateState({ currentPlanReviewToken: { token: chatData.id, renderNum: 0 } }) + } } - rawData.contents.set(chatData.id, cloneDeep(chatData)) - - if (chatType === 'reAct') store.getState().updateState({ currentPlanReviewToken: chatData.id }) } export const aiReviewDataHandlers = { From 9c71697efd9146b55a4cf3c1af9a71dd65341792 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Tue, 14 Jul 2026 14:47:28 +0800 Subject: [PATCH 031/101] =?UTF-8?q?1.feat:=E5=AF=B9=E6=8E=A5review?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E7=9A=84=E9=83=A8=E5=88=86=E6=95=B0=E6=8D=AE?= =?UTF-8?q?;fix:=E4=BF=AE=E5=A4=8D=E4=B8=80=E4=BA=9B=E5=B0=8F=E9=97=AE?= =?UTF-8?q?=E9=A2=98=202.fix:=E5=85=88=E6=B3=A8=E9=87=8A=E6=97=A0=E7=94=A8?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=203.del:=E5=A4=9A=E4=BD=99=E7=9A=84=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-agent/aiAgentChat/AIAgentChat.tsx | 241 ++++++++++-------- .../src/pages/ai-agent/aiAgentChat/type.ts | 5 +- .../AIHorizontalScrollCard.module.scss | 9 + .../aiReActChatReview/AIReActChatReview.tsx | 10 +- .../src/pages/ai-agent/defaultConstant.tsx | 5 - .../aiReActChat/AIReActChatType.d.ts | 28 -- .../aiReActChatHeader/AIReActChatHeader.tsx | 9 +- .../ai-re-act/aiReActChat/aiToDoList/icon.tsx | 1 - .../AIReActChatContents.tsx | 25 +- .../aiReActTaskChat/AIReActTaskChat.tsx | 36 +-- .../AIReActTaskChatReviewBar.tsx | 12 +- .../ai-re-act/hooks/explame/ChatNode.tsx | 164 ++++++------ .../hooks/explame/ChatNotification.tsx | 42 +-- .../ai-re-act/hooks/explame/ChatReview.tsx | 84 +++--- .../ai-re-act/hooks/explame/ChatWindow.tsx | 172 ++++++------- .../hooks/explame/SessionRuntime.tsx | 8 +- .../pages/ai-re-act/hooks/explame/layout.tsx | 86 +++---- .../pages/ai-re-act/hooks/useCasualChat.ts | 1 + .../src/pages/ai-re-act/hooks/useChatIPC.ts | 10 +- 19 files changed, 466 insertions(+), 482 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.d.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index 9831fe5719..a43050672e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -1,6 +1,6 @@ import React, { memo, useEffect, useRef, useState } from 'react' import { AIAgentChatMode, AIAgentChatProps, AIReActTaskChatReviewProps, HandleStartParams } from './type' -import { useDebounceFn, useInViewport, useMap, useMemoizedFn, useSafeState, useUpdateEffect } from 'ahooks' +import { useCreation, useDebounceFn, useInViewport, useMemoizedFn, useSafeState, useUpdateEffect } from 'ahooks' import emiter from '@/utils/eventBus/eventBus' import { AIAgentTriggerEventInfo } from '../aiAgentType' import useAIAgentStore from '../useContext/useStore' @@ -9,13 +9,9 @@ import { RemoteAIAgentGV } from '@/enums/aiAgent' import { isForcedSetAIModal } from '../aiModelList/utils' import useAIAgentDispatcher from '../useContext/useDispatcher' import cloneDeep from 'lodash/cloneDeep' -import { randomString } from '@/utils/randomUtil' import { AIReActChatReview } from '@/pages/ai-agent/components/aiReActChatReview/AIReActChatReview' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { OutlineChevrondoubledownIcon, OutlineChevrondoubleupIcon } from '@/assets/icon/outline' -import { ChatIPCSendType } from '@/pages/ai-re-act/hooks/type' -import { AIAgentGrpcApi, AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' -import { AIChatQSData, AIReviewType } from '@/pages/ai-re-act/hooks/aiRender' import { failed, yakitNotify } from '@/utils/notification' import { AIForgeForm, AIToolForm } from '../aiTriageChatTemplate/AITriageChatTemplate' import { grpcGetAIForge } from '../grpc' @@ -41,7 +37,8 @@ import { Trans } from 'react-i18next' import { AIInputWithParamsTemplate, aiInputWithParamsTemplate } from '../components/aiMilkdownInput/utils' import { useStore } from 'zustand' import { AIForgeFormSubmitParamsProps } from '../aiTriageChatTemplate/type' -import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useCurrentMeta, useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' const AIChatWelcome = React.lazy(() => import('../aiChatWelcome/AIChatWelcome')) @@ -52,6 +49,7 @@ export const AIAgentChat: React.FC = memo((props) => { const { setActiveChat, setSetting, onSend, onClose } = useAIAgentDispatcher() /** 当前对话唯一ID */ + const sessionId = useCurrentSessionId() const store = useCurrentStore() const execute = useStore(store, (state) => state.execute) @@ -82,66 +80,66 @@ export const AIAgentChat: React.FC = memo((props) => { }) // review数据中树的数据中需要的解释和关键词工具 - const [planReviewTreeKeywordsMap, { set: setPlanReviewTreeKeywords, reset: resetPlanReviewTreeKeywords }] = useMap< - string, - AIAgentGrpcApi.PlanReviewRequireExtra - >(new Map()) + // const [planReviewTreeKeywordsMap, { set: setPlanReviewTreeKeywords, reset: resetPlanReviewTreeKeywords }] = useMap< + // string, + // AIAgentGrpcApi.PlanReviewRequireExtra + // >(new Map()) - const [reviewInfo, setReviewInfo] = useState() + // const [reviewInfo, setReviewInfo] = useState() // @deprecated - const handleShowReview = useMemoizedFn((info: AIChatQSData) => { - // setReviewExpand(true) - setReviewInfo(cloneDeep(info)) - }) + // const handleShowReview = useMemoizedFn((info: AIChatQSData) => { + // // setReviewExpand(true) + // setReviewInfo(cloneDeep(info)) + // }) // @deprecated - const handleShowReviewExtra = useMemoizedFn((info: AIAgentGrpcApi.PlanReviewRequireExtra) => { - setPlanReviewTreeKeywords(info.task_id, info) - }) + // const handleShowReviewExtra = useMemoizedFn((info: AIAgentGrpcApi.PlanReviewRequireExtra) => { + // setPlanReviewTreeKeywords(info.index, info) + // }) // @deprecated - const handleReleaseReview = useMemoizedFn((type: ChatIPCSendType, id: string) => { - if (!reviewInfo) return - if ((reviewInfo.data as AIReviewType).id === id) { - // if (!delayLoading) yakitNotify("warning", "审阅自动执行,弹框将自动关闭") - handleStopAfterChangeState() - } - }) + // const handleReleaseReview = useMemoizedFn((type: ChatIPCSendType, id: string) => { + // if (!reviewInfo) return + // if ((reviewInfo.data as AIReviewType).id === id) { + // // if (!delayLoading) yakitNotify("warning", "审阅自动执行,弹框将自动关闭") + // handleStopAfterChangeState() + // } + // }) // @deprecated 提问结束后缓存数据 - const handleChatingEnd = useMemoizedFn(() => { - handleStopAfterChangeState() - }) + // const handleChatingEnd = useMemoizedFn(() => { + // handleStopAfterChangeState() + // }) // @deprecated - const setSessionChatName = (session: string, name: string) => { - setActiveChat?.((prev) => { - if (!prev) return prev - if (prev.SessionID !== session) return prev - return { ...prev, Title: name } - }) - emiter.emit( - 'sessionData', - JSON.stringify({ - type: 'updateSession', - sessionId: session, - updates: { Title: name }, - }), - ) - } + // const setSessionChatName = (session: string, name: string) => { + // setActiveChat?.((prev) => { + // if (!prev) return prev + // if (prev.SessionID !== session) return prev + // return { ...prev, Title: name } + // }) + // emiter.emit( + // 'sessionData', + // JSON.stringify({ + // type: 'updateSession', + // sessionId: session, + // updates: { Title: name }, + // }), + // ) + // } // @deprecated - const [syncIdInfoMap, { set: setSyncIdInfoMap, get: getSyncIdInfoMap, remove: removeSyncIdInfoMap }] = useMap< - string, - boolean - >(new Map()) + // const [syncIdInfoMap, { set: setSyncIdInfoMap, get: getSyncIdInfoMap, remove: removeSyncIdInfoMap }] = useMap< + // string, + // boolean + // >(new Map()) // @deprecated - const onSyncIDChange = useMemoizedFn((syncID: string) => { - const item = getSyncIdInfoMap(syncID) - if (!!item) { - removeSyncIdInfoMap(syncID) - } - }) + // const onSyncIDChange = useMemoizedFn((syncID: string) => { + // const item = getSyncIdInfoMap(syncID) + // if (!!item) { + // removeSyncIdInfoMap(syncID) + // } + // }) /** 等自由对话渲染出来再发送 */ const handleStart = useMemoizedFn((value: HandleStartParams) => { @@ -150,61 +148,61 @@ export const AIAgentChat: React.FC = memo((props) => { }) }) // @deprecated - const handleSendCasual = useMemoizedFn((params: AIChatIPCSendParams) => { - handleSendInteractiveMessage(params, 'casual') - }) + // const handleSendCasual = useMemoizedFn((params: AIChatIPCSendParams) => { + // handleSendInteractiveMessage(params, 'casual') + // }) // @deprecated - const handleSendTask = useMemoizedFn((params: AIChatIPCSendParams) => { - handleSendInteractiveMessage(params, 'task') - }) + // const handleSendTask = useMemoizedFn((params: AIChatIPCSendParams) => { + // handleSendInteractiveMessage(params, 'task') + // }) // @deprecated - const handleSend = useMemoizedFn((params: AIChatIPCSendParams) => { - handleSendInteractiveMessage(params, '') - }) - /**发送 @deprecated IsInteractiveMessage 消息 */ - const handleSendInteractiveMessage = useMemoizedFn((params: AIChatIPCSendParams, type: ChatIPCSendType) => { - const { value, id, optionValue } = params - if (!sessionId) return - if (!id) return - - const info: AIInputEvent = { - IsInteractiveMessage: true, - InteractiveId: id, - InteractiveJSONInput: value, - } - onSend({ token: sessionId, type, params: info, optionValue }) - handleStopAfterChangeState() - }) - /** @deprecated 发送 IsSyncMessage 消息 */ - const handleSendSyncMessage = useMemoizedFn((data: AISendSyncMessageParams) => { - if (!sessionId) return - const { syncType, SyncJsonInput, params, syncID } = data - const info: AIInputEvent = { - IsSyncMessage: true, - SyncType: syncType, - SyncJsonInput, - Params: params, - SyncID: syncID || randomString(8), - } - info.SyncID && setSyncIdInfoMap(info.SyncID, true) - onSend({ token: sessionId, type: '', params: info }) - }) - - /** - * @deprecated 发送 IsConfigHotpatch 消息 */ - const handleSendConfigHotpatch = useMemoizedFn((data: AISendConfigHotpatchParams) => { - if (!sessionId) return - const { hotpatchType, params, taskId } = data - const info: AIInputEvent = { - IsConfigHotpatch: true, - HotpatchType: hotpatchType, - Params: params, - } - if (!!taskId) { - info.TaskId = taskId - } - onSend({ token: sessionId, type: '', params: info }) - }) + // const handleSend = useMemoizedFn((params: AIChatIPCSendParams) => { + // handleSendInteractiveMessage(params, '') + // }) + // /**发送 @deprecated IsInteractiveMessage 消息 */ + // const handleSendInteractiveMessage = useMemoizedFn((params: AIChatIPCSendParams, type: ChatIPCSendType) => { + // const { value, id, optionValue } = params + // if (!sessionId) return + // if (!id) return + + // const info: AIInputEvent = { + // IsInteractiveMessage: true, + // InteractiveId: id, + // InteractiveJSONInput: value, + // } + // onSend({ token: sessionId, type, params: info, optionValue }) + // handleStopAfterChangeState() + // }) + // /** @deprecated 发送 IsSyncMessage 消息 */ + // const handleSendSyncMessage = useMemoizedFn((data: AISendSyncMessageParams) => { + // if (!sessionId) return + // const { syncType, SyncJsonInput, params, syncID } = data + // const info: AIInputEvent = { + // IsSyncMessage: true, + // SyncType: syncType, + // SyncJsonInput, + // Params: params, + // SyncID: syncID || randomString(8), + // } + // info.SyncID && setSyncIdInfoMap(info.SyncID, true) + // onSend({ token: sessionId, type: '', params: info }) + // }) + + // /** + // * @deprecated 发送 IsConfigHotpatch 消息 */ + // const handleSendConfigHotpatch = useMemoizedFn((data: AISendConfigHotpatchParams) => { + // if (!sessionId) return + // const { hotpatchType, params, taskId } = data + // const info: AIInputEvent = { + // IsConfigHotpatch: true, + // HotpatchType: hotpatchType, + // Params: params, + // } + // if (!!taskId) { + // info.TaskId = taskId + // } + // onSend({ token: sessionId, type: '', params: info }) + // }) const onStop = useMemoizedFn(() => { if (execute && sessionId) { @@ -212,12 +210,12 @@ export const AIAgentChat: React.FC = memo((props) => { } }) /** @deprecated 停止回答后的状态调整||清空Review状态 */ - const handleStopAfterChangeState = useMemoizedFn(() => { - // 清空review信息 - setReviewInfo(undefined) - resetPlanReviewTreeKeywords() - // setReviewExpand(true) - }) + // const handleStopAfterChangeState = useMemoizedFn(() => { + // // 清空review信息 + // setReviewInfo(undefined) + // resetPlanReviewTreeKeywords() + // // setReviewExpand(true) + // }) useEffect(() => { getRemoteValue(RemoteAIAgentGV.AIAgentReplaceForgeNoPrompt) @@ -700,8 +698,23 @@ export const AIAgentChat: React.FC = memo((props) => { export const AIReActTaskChatReview: React.FC = React.memo((props) => { const { t } = useI18nNamespaces(['aiAgent']) - const { reviewInfo, planReviewTreeKeywordsMap, footerExtra } = props + const { footerExtra } = props const [expand, setReviewExpand] = useState(true) + + const store = useCurrentStore() + const rawData = useCurrentRawData() + const meta = useCurrentMeta() + const currentPlanReviewToken = useStore(store, (state) => state.currentPlanReviewToken) + const currentPlanReviewExtraUpdate = useStore(store, (state) => state.currentPlanReviewExtraUpdate) + /** TODO - review:工具的倒计时,目前没有类似renderNum的更新逻辑 */ + const reviewInfo = useCreation(() => { + return rawData.contents.get(currentPlanReviewToken) + }, [currentPlanReviewToken]) + + const planReviewTreeKeywordsMap = useCreation(() => { + return meta.planReviewExtraData + }, [currentPlanReviewExtraUpdate]) + const handleExpand = useMemoizedFn(() => { setReviewExpand((old) => !old) }) @@ -719,6 +732,7 @@ export const AIReActTaskChatReview: React.FC = React
) }) + if (!reviewInfo) return null return (
= React renderFooterExtra={renderFooter} expand={expand} className={styles['review-body']} + renderNum={0} />
diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/type.ts b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/type.ts index 3cb4efd1e0..57429514f5 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/type.ts @@ -1,13 +1,10 @@ -import { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' -import { AIAgentGrpcApi, AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' +import { AIInputEvent } from '@/pages/ai-re-act/hooks/grpcApi' import { AIChatTextareaSubmit } from '../template/type' import { ReactNode } from 'react' export interface AIAgentChatProps {} export type AIAgentChatMode = 'welcome' | 're-act' export interface AIReActTaskChatReviewProps { - reviewInfo: AIChatQSData - planReviewTreeKeywordsMap: Map setScrollToBottom: (v: boolean) => void footerExtra: (v: ReactNode) => ReactNode } diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss index 735c121c4b..cfe79360e9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/aiHorizontalScrollCard/AIHorizontalScrollCard.module.scss @@ -1,5 +1,14 @@ @use '../../../../styles/mixin.scss' as mixin; +%display-flex-center { + display: flex; + align-items: center; +} +%display-column-center { + display: flex; + align-items: center; + flex-direction: column; +} .expand-retract-wrapper { width: 100%; border-bottom: 0; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx index b5563d7758..02cbd85eb1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx @@ -367,11 +367,11 @@ export const AIReActChatReview: React.FC = React.memo((p setForgeOption(item) break case 'close': - if (info.type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) { - /** TODO - 新版需要逻辑调整*/ - chatIPCEvents.handleTaskReviewRelease((info.data as AIReviewType).id) - return - } + // if (info.type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) { + // /** TODO - 新版需要逻辑调整*/ + // chatIPCEvents.handleTaskReviewRelease((info.data as AIReviewType).id) + // return + // } break default: if (editShow) return diff --git a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx index 39dbc8b68e..7d4df676b2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx @@ -276,11 +276,6 @@ export const defaultChatIPCData: UseChatIPCState = { elements: [], toolListRenderNumber: 0, }, - yakExecResult: { - card: [], - execFileRecord: new Map(), - yakExecResultLogs: [], - }, taskChat: { plan: cloneDeep(DefaultCurrentExecTaskTree), elements: [], diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.d.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.d.ts deleted file mode 100644 index e7437e2b9a..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { AIAgentChatMode } from '@/pages/ai-agent/aiAgentChat/type' -import { AIChatQSData } from '../hooks/aiRender' - -export interface AIReActChatProps { - mode: AIAgentChatMode - chatContainerClassName?: string - chatContainerHeaderClassName?: string - showFreeChat: boolean - setShowFreeChat: (show: boolean) => void - title?: React.ReactNode - aiChatTextareaRef: ForwardedRef - handleSendAfter?: () => void - otherComponentCallsProps?: Partial<{ - chevronleftButton: React.ReactNode - remoteButtion: React.ReactNode - }> -} - -export interface AIReActLogProps { - logs: AIChatQSData[] - setLogVisible: (visible: boolean) => void -} - -export interface AIReActTimelineMessageProps { - message?: string - loading: boolean - setLoading: (loading: boolean) => void -} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx index cc7b19152d..fe7da7720a 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames' import { Tooltip } from 'antd' import { useStore } from 'zustand' -import styles from '../AIReActChatHeader.module.scss' +import styles from './AIReActChatHeader.module.scss' import { ColorsChatIcon } from '@/assets/icon/colors' import { YakitTag } from '@/components/yakitUI/YakitTag/YakitTag' import TaskDetailsPopover from '@/components/historyAIReActChat/TaskDetailsPopover' @@ -160,13 +160,12 @@ const AIReActSubAgentTask: React.FC = React.memo((prop const casualConcurrentTaskList = useCreation(() => { const list: string[] = [] const elements = store.getState().casualChat?.elements || [] - for (const token in elements) { - const item = elements[token] + for (const item of elements) { const kind = getKind(item.token) if (kind !== 'task') continue - const itemContent = rawData.contents.get(token) + const itemContent = rawData.contents.get(item.token) if (itemContent?.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - list.push(token) + list.push(item.token) } } return list diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoList/icon.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoList/icon.tsx index 90ef8d4fff..1d8bb5dd55 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoList/icon.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiToDoList/icon.tsx @@ -1,7 +1,6 @@ import Icon from '@ant-design/icons' import { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon' import React from 'react' -import { v4 as uuidv4 } from 'uuid' interface IconProps extends CustomIconComponentProps { onClick: (e: React.MouseEvent) => void diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index b125551ff8..0f5e54e060 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -21,7 +21,6 @@ import { ScrollText } from '@/pages/ai-agent/chatTemplate/TaskLoading/TaskLoadin import { showYakitModal } from '@/components/yakitUI/YakitModal/YakitModalConfirm' import { YakitEditor } from '@/components/yakitUI/YakitEditor/YakitEditor' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' -import useLoadHistory from '../hooks/useLoadHistory' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import AITextSyntaxFlow from '@/pages/ai-agent/components/aiTextSyntaxFlow/AITextSyntaxFlow' import { useCurrentStore } from '../hooks/useCurrentDataBySession' @@ -131,20 +130,20 @@ export const AIReActChatContents: React.FC = React.me const casualLoadMoreLoading = useStore(store, (state) => state.requestHistoryState.casualLoadMoreLoading) /** TODO - 新版中历史逻辑暂未补充 */ - const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents + // const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents // 向上滚动加载 - const { firstItemIndex, handleLoadMore, isPrependingRef } = useLoadHistory({ - loading: casualLoadMoreLoading, - dataLength: chatLength, - SessionID: activeChat?.SessionID || '', - fetchHasMore: () => handleHasMoreHistory(TYPE), - loadMore: () => handleLoadMoreHistory(TYPE), - }) + // const { firstItemIndex, handleLoadMore, isPrependingRef } = useLoadHistory({ + // loading: casualLoadMoreLoading, + // dataLength: chatLength, + // SessionID: activeChat?.SessionID || '', + // fetchHasMore: () => handleHasMoreHistory(TYPE), + // loadMore: () => handleLoadMoreHistory(TYPE), + // }) const { virtuosoRef, setScrollerRef, setIsAtBottomRef, handleTotalListHeightChanged, scrollToItemIndex } = useVirtuosoAutoScroll({ total: chatLength, - isPrependingRef, + // isPrependingRef, }) const { locateToIndex } = useChatStreamLocateHighlight({ @@ -215,7 +214,7 @@ export const AIReActChatContents: React.FC = React.me key={activeChat?.SessionID} ref={virtuosoRef} scrollerRef={setScrollerRef} - firstItemIndex={firstItemIndex} + // firstItemIndex={firstItemIndex} atBottomStateChange={setIsAtBottomRef} data={casualChat.elements} totalListHeightChanged={handleTotalListHeightChanged} @@ -224,9 +223,7 @@ export const AIReActChatContents: React.FC = React.me components={components} atBottomThreshold={50} skipAnimationFrameInResizeObserver - // atTopStateChange={handleAtTopStateChange} - startReached={handleLoadMore} - // increaseViewportBy={{ top: 200, bottom: 0 }} + // startReached={handleLoadMore} className={styles['re-act-contents-list']} />
diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx index d281b51a37..1973648a12 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx @@ -17,7 +17,7 @@ import { } from './AIReActTaskChatType' import styles from './AIReActTaskChat.module.scss' import { AIAgentChatStream, AIChatLeftSide } from '@/pages/ai-agent/chatTemplate/AIAgentChatTemplate' -import { useControllableValue, useMemoizedFn, useUpdateEffect } from 'ahooks' +import { useControllableValue, useCreation, useMemoizedFn, useUpdateEffect } from 'ahooks' import classNames from 'classnames' import { ChevrondownButton } from '../aiReActChat/AIReActComponent' import { @@ -53,6 +53,7 @@ import { useTaskChatExtraAction } from './useTaskChatExtraAction' import { useCurrentMeta, useCurrentStore } from '../hooks/useCurrentDataBySession' import { useStore } from 'zustand' import useCurrentSessionId from '../hooks/useCurrentSessionId' +import { globalSessionEngine } from '../hooks/ChatMultiSessionController' const AIReActTaskChat: React.FC = React.memo((props) => { const { setShowFreeChat, setTimeLine, onTaskTabsChange } = props @@ -148,8 +149,6 @@ export default AIReActTaskChat export const AIReActTaskChatContent: React.FC = React.memo((props) => { const { scrollToBottom, onScrollToBottom } = props - /** TODO - 数据未对接 */ - const { reviewInfo } = useChatIPCStore() const { t } = useI18nNamespaces(['aiAgent']) const { activeChat } = useAIAgentStore() const { onExtraAction } = useTaskChatExtraAction() @@ -159,6 +158,7 @@ export const AIReActTaskChatContent: React.FC = Rea const streams = useStore(store, (state) => state.taskChat.elements) const execute = useStore(store, (state) => state.execute) const taskStatus = useStore(store, (state) => state.taskStatus) + const currentPlanReviewToken = useStore(store, (state) => state.currentPlanReviewToken) return ( <> @@ -170,7 +170,7 @@ export const AIReActTaskChatContent: React.FC = Rea taskStatus={taskStatus} />
- {!reviewInfo && streams.length > 0 && ( + {!currentPlanReviewToken && streams.length > 0 && (
{execute && ( @@ -326,9 +326,8 @@ const AIManualAddition: React.FC = React.memo((props) => const store = useCurrentStore() const taskStatus = useStore(store, (state) => state.taskStatus) const execute = useStore(store, (state) => state.execute) + const syncIDUpdate = useStore(store, (state) => state.syncIDUpdate) - /** TODO - syncIdInfoMap新版未补充 */ - const { syncIdInfoMap } = useChatIPCStore() const [prompt, setPrompt] = useState() const currentCoordinatorIdRef = useRef('') @@ -343,12 +342,12 @@ const AIManualAddition: React.FC = React.memo((props) => useEffect(() => { if ( - (syncIdOfAddToContext.current && !syncIdInfoMap?.get(syncIdOfAddToContext.current)) || - (syncIdOfAddAndReExecute.current && !syncIdInfoMap?.get(syncIdOfAddAndReExecute.current)) + (syncIdOfAddToContext.current && !meta.syncIDMap?.get(syncIdOfAddToContext.current)) || + (syncIdOfAddAndReExecute.current && !meta.syncIDMap?.get(syncIdOfAddAndReExecute.current)) ) { onReset() } - }, [syncIdInfoMap]) + }, [syncIDUpdate]) useEffect(() => { if (execute) return @@ -429,9 +428,16 @@ const AIManualAddition: React.FC = React.memo((props) => AIService: '', AIModelName: '', } - /** TODO - 数据暂无对接 */ - // chatIPCEvents.handleUserManualIntervention(chatData) + globalSessionEngine.pushDataToSession(sessionId, chatData) }) + + const addAndReExecuteLoading = useCreation(() => { + return !!meta.syncIDMap?.get(syncIdOfAddAndReExecute.current) + }, [syncIDUpdate]) + + const addAndToContextLoading = useCreation(() => { + return !!meta.syncIDMap?.get(syncIdOfAddToContext.current) + }, [syncIDUpdate]) return (
e.stopPropagation()}>
人工介入
@@ -449,9 +455,9 @@ const AIManualAddition: React.FC = React.memo((props) => e.stopPropagation()} - loading={!!syncIdInfoMap?.get(syncIdOfAddAndReExecute.current)} + loading={addAndReExecuteLoading} className={styles['add-and-reexecute-btn']} - disabled={!!syncIdInfoMap?.get(syncIdOfAddToContext.current)} + disabled={!addAndToContextLoading} > 加入并重新执行 @@ -461,8 +467,8 @@ const AIManualAddition: React.FC = React.memo((props) => syncIdOfAddToContext.current = randomString(8) onAddToContext(syncIdOfAddToContext.current) }} - loading={!!syncIdInfoMap?.get(syncIdOfAddToContext.current)} - disabled={!!syncIdInfoMap?.get(syncIdOfAddAndReExecute.current)} + loading={addAndToContextLoading} + disabled={!addAndReExecuteLoading} > 加入上下文 diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatReviewBar.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatReviewBar.tsx index 48319fba82..ec1548f58c 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatReviewBar.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChatReviewBar.tsx @@ -1,6 +1,5 @@ import { type FC, type ReactNode } from 'react' import { AIReActTaskChatReview } from '@/pages/ai-agent/aiAgentChat/AIAgentChat' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' import { AIRenderTaskFooterExtra } from './AIReActTaskChat' import { useTaskChatExtraAction } from './useTaskChatExtraAction' @@ -26,17 +25,8 @@ const renderReviewFooterExtra = (onExtraAction: ReturnType void }> = ({ setScrollToBottom }) => { - const { reviewInfo, planReviewTreeKeywordsMap } = useChatIPCStore() const { onExtraAction } = useTaskChatExtraAction() - - if (!reviewInfo) return null - return ( - + ) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx index c2cb9fb94a..e17ddf62ba 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx @@ -1,97 +1,97 @@ -import React, { memo } from 'react' -import { useChatUIStore, useChatContent } from './ChatContext' +// import React, { memo } from 'react' +// import { useChatUIStore, useChatContent } from './ChatContext' -// --- 5.1 叶子节点组件 (Item) --- -export const ChatItem = memo(({ token }: { token: string }) => { - // 只订阅自己节点下的 renderNum 变动 - const renderNum = usestore((state) => state.items[token]?.renderNum) +// // --- 5.1 叶子节点组件 (Item) --- +// export const ChatItem = memo(({ token }: { token: string }) => { +// // 只订阅自己节点下的 renderNum 变动 +// const renderNum = usestore((state) => state.items[token]?.renderNum) - const contentManager = useChatContent() - = - const session=usecontex - const{rawdata}=glkoencinstance(session) +// const contentManager = useChatContent() +// = +// const session=usecontex +// const{rawdata}=glkoencinstance(session) - if (renderNum === undefined) return null +// if (renderNum === undefined) return null - // 拿着 token 去数据池取真实的大段文本数据 - const realData = contentManager.get(token) +// // 拿着 token 去数据池取真实的大段文本数据 +// const realData = contentManager.get(token) - return ( -
- {realData?.type}: - {realData?.content} - (RenderVersion: {renderNum}) -
- ) -}) +// return ( +//
+// {realData?.type}: +// {realData?.content} +// (RenderVersion: {renderNum}) +//
+// ) +// }) -// --- 5.2 容器组组件 (Group) --- -export const ChatGroup = memo(({ token }: { token: string }) => { - // 仅订阅 childrenTokens 数组指针的变动(即内部子项增减会刷新,某个子项打字更新时这里完全不刷) - const childrenTokens = useChatUIStore((state) => state.groups[token]?.childrenTokens) +// // --- 5.2 容器组组件 (Group) --- +// export const ChatGroup = memo(({ token }: { token: string }) => { +// // 仅订阅 childrenTokens 数组指针的变动(即内部子项增减会刷新,某个子项打字更新时这里完全不刷) +// const childrenTokens = useChatUIStore((state) => state.groups[token]?.childrenTokens) - if (!childrenTokens) return null +// if (!childrenTokens) return null - return ( -
-
组容器 [{token}]
- {childrenTokens.map((childToken) => ( - - ))} -
- ) -}) +// return ( +//
+//
组容器 [{token}]
+// {childrenTokens.map((childToken) => ( +// +// ))} +//
+// ) +// }) -// --- 5.3 复杂任务树组件 (Task) --- -export const ChatTask = memo(({ token }: { token: string }) => { - const childrenTokens = useChatUIStore((state) => state.tasks[token]?.childrenTokens) +// // --- 5.3 复杂任务树组件 (Task) --- +// export const ChatTask = memo(({ token }: { token: string }) => { +// const childrenTokens = useChatUIStore((state) => state.tasks[token]?.childrenTokens) - if (!childrenTokens) return null +// if (!childrenTokens) return null - return ( -
-

大任务卡片 [{token}]

- - ==list - {childrenTokens.map((childToken) => ( - - ))} -
- ) -}) +// return ( +//
+//

大任务卡片 [{token}]

+// +// ==list +// {childrenTokens.map((childToken) => ( +// +// ))} +//
+// ) +// }) -// --- 5.4 顶层异构节点分发器 --- -const RootNodeDispatcher = memo(({ token }: { token: string }) => { - // 只在节点刚创建判断类型或类型突变时触发,返回纯字符串,彻底隔绝下层的 Streaming 流高频刷新 - const nodeKind = useChatUIStore((state) => { - if (state.items[token]) return 'item' - if (state.groups[token]) return 'group' - if (state.tasks[token]) return 'task' - return null - }) +// // --- 5.4 顶层异构节点分发器 --- +// const RootNodeDispatcher = memo(({ token }: { token: string }) => { +// // 只在节点刚创建判断类型或类型突变时触发,返回纯字符串,彻底隔绝下层的 Streaming 流高频刷新 +// const nodeKind = useChatUIStore((state) => { +// if (state.items[token]) return 'item' +// if (state.groups[token]) return 'group' +// if (state.tasks[token]) return 'task' +// return null +// }) - switch (nodeKind) { - case 'task': - return - case 'group': - return - case 'item': - return - default: - return null - } -}) +// switch (nodeKind) { +// case 'task': +// return +// case 'group': +// return +// case 'item': +// return +// default: +// return null +// } +// }) -// --- 5.5 页面最外层总看板 --- -export const ChatList = () => { - const rootOrder = useChatUIStore((state) => state.taskchatelemnt) +// // --- 5.5 页面最外层总看板 --- +// export const ChatList = () => { +// const rootOrder = useChatUIStore((state) => state.taskchatelemnt) - return ( -
-

AI 聊天多实例面板

- {rootOrder.map((token) => ( - - ))} -
- ) -} +// return ( +//
+//

AI 聊天多实例面板

+// {rootOrder.map((token) => ( +// +// ))} +//
+// ) +// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx index 2ecd1e8fa3..2233196909 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx @@ -1,26 +1,26 @@ -// GlobalNotification.tsx -import React, { useContext, useEffect } from 'react' -import { useStore } from 'zustand' -import { message } from 'antd' -import { SessionIdContext } from './SessionRuntime' -import { globalSessionEngine } from './ChatMultiSessionController' +// // GlobalNotification.tsx +// import React, { useContext, useEffect } from 'react' +// import { useStore } from 'zustand' +// import { message } from 'antd' +// import { SessionIdContext } from './SessionRuntime' +// import { globalSessionEngine } from './ChatMultiSessionController' -export const GlobalNotification = () => { - const sessionId = useContext(SessionIdContext) - const { store } = globalSessionEngine.ensureSession(sessionId) +// export const GlobalNotification = () => { +// const sessionId = useContext(SessionIdContext) +// const { store } = globalSessionEngine.ensureSession(sessionId) - const notification = useStore(store, (state) => state.globalNotification) +// const notification = useStore(store, (state) => state.globalNotification) - useEffect(() => { - if (notification) { - if (notification.type === 'warning') message.warning(notification.message) - if (notification.type === 'error') message.error(notification.message) - if (notification.type === 'info') message.info(notification.message) +// useEffect(() => { +// if (notification) { +// if (notification.type === 'warning') message.warning(notification.message) +// if (notification.type === 'error') message.error(notification.message) +// if (notification.type === 'info') message.info(notification.message) - // 显示完毕后立即清除状态,防止重复触发 - store.getState().setGlobalNotification(null) - } - }, [notification, store]) +// // 显示完毕后立即清除状态,防止重复触发 +// store.getState().setGlobalNotification(null) +// } +// }, [notification, store]) - return null -} +// return null +// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx index ac24d7ec2d..d7fb1c70fe 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx @@ -1,48 +1,48 @@ -// ChatReviewModal.tsx -import React, { useContext } from 'react' -import { useStore } from 'zustand' -import { Modal, Button, Space } from 'antd' -import { SessionIdContext } from './SessionRuntime' -import { globalSessionEngine } from './ChatMultiSessionController' +// // ChatReviewModal.tsx +// import React, { useContext } from 'react' +// import { useStore } from 'zustand' +// import { Modal, Button, Space } from 'antd' +// import { SessionIdContext } from './SessionRuntime' +// import { globalSessionEngine } from './ChatMultiSessionController' -export const ChatReviewModal = () => { - const sessionId = useContext(SessionIdContext) - const { store } = globalSessionEngine.ensureSession(sessionId) +// export const ChatReviewModal = () => { +// const sessionId = useContext(SessionIdContext) +// const { store } = globalSessionEngine.ensureSession(sessionId) - const reviewData = useStore(store, (state) => state.reviewModalData) +// const reviewData = useStore(store, (state) => state.reviewModalData) - const handleClose = () => store.getState().setReviewModalData(null) +// const handleClose = () => store.getState().setReviewModalData(null) - const handleConfirm = (decisionValue: string) => { - globalSessionEngine.sendMessage(sessionId, { - action: 'review_decision', - id: reviewData.id, - decision: decisionValue, - }) - handleClose() - } +// const handleConfirm = (decisionValue: string) => { +// globalSessionEngine.sendMessage(sessionId, { +// action: 'review_decision', +// id: reviewData.id, +// decision: decisionValue, +// }) +// handleClose() +// } - if (!reviewData) return null +// if (!reviewData) return null - return ( - -

节点事件 ID: {reviewData.id}

- - {reviewData.selectors?.map((sel: any) => ( - - ))} - - -
- ) -} +// return ( +// +//

节点事件 ID: {reviewData.id}

+// +// {reviewData.selectors?.map((sel: any) => ( +// +// ))} +// +// +//
+// ) +// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx index 17e3425a83..119278bfbb 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx @@ -1,97 +1,97 @@ -// ChatWindow.tsx -import React, { useContext, useState } from 'react' -import { useStore } from 'zustand' -import { useShallow } from 'zustand/react/shallow' -import { SessionIdContext } from './SessionRuntime' -import { globalSessionEngine } from './ChatMultiSessionController' -import { useChatIPC } from './useChatIPC' -import { ChatGroup, ChatTask, ChatItem } from './ChatNodes' -import { ChatReviewModal } from './ChatReviewModal' +// // ChatWindow.tsx +// import React, { useContext, useState } from 'react' +// import { useStore } from 'zustand' +// import { useShallow } from 'zustand/react/shallow' +// import { SessionIdContext } from './SessionRuntime' +// import { globalSessionEngine } from './ChatMultiSessionController' +// import { useChatIPC } from './useChatIPC' +// import { ChatGroup, ChatTask, ChatItem } from './ChatNodes' +// import { ChatReviewModal } from './ChatReviewModal' -export const ChatWindow = () => { - const sessionId = useContext(SessionIdContext) - const { onStart, onSend } = useChatIPC() +// export const ChatWindow = () => { +// const sessionId = useContext(SessionIdContext) +// const { onStart, onSend } = useChatIPC() - // 订阅当前专属 Store - const { store } = globalSessionEngine.ensureSession(sessionId) +// // 订阅当前专属 Store +// const { store } = globalSessionEngine.ensureSession(sessionId) - // 🎯 按需精准订阅 - const casualElements = useStore( - store, - useShallow((state) => state.casualElements), - ) - const taskElements = useStore( - store, - useShallow((state) => state.taskElements), - ) - const casualTitle = useStore(store, (state) => state.casualTitle) - const taskStatus = useStore(store, (state) => state.taskStatus) +// // 🎯 按需精准订阅 +// const casualElements = useStore( +// store, +// useShallow((state) => state.casualElements), +// ) +// const taskElements = useStore( +// store, +// useShallow((state) => state.taskElements), +// ) +// const casualTitle = useStore(store, (state) => state.casualTitle) +// const taskStatus = useStore(store, (state) => state.taskStatus) - const [inputValue, setInputValue] = useState('') +// const [inputValue, setInputValue] = useState('') - const handleSendMessage = () => { - if (!inputValue.trim()) return - onSend({ text: inputValue }) - setInputValue('') - } +// const handleSendMessage = () => { +// if (!inputValue.trim()) return +// onSend({ text: inputValue }) +// setInputValue('') +// } - // 辅助渲染函数:递归解析根节点并路由给对应的组件 - const renderNode = (node: any) => { - const key = node.token || node.id - if (node.type === 'group') return - if (node.type === 'task') return - return - } +// // 辅助渲染函数:递归解析根节点并路由给对应的组件 +// const renderNode = (node: any) => { +// const key = node.token || node.id +// if (node.type === 'group') return +// if (node.type === 'task') return +// return +// } - return ( -
- {/* 人工审批拦截弹窗 */} - +// return ( +//
+// {/* 人工审批拦截弹窗 */} +// - {/* 头部状态栏 */} -
-

{casualTitle || '安全分析终端'}

- {taskStatus.loading && ( -
- ⏳ 正在执行: {taskStatus.task} {taskStatus.plan ? `-> ${taskStatus.plan}` : ''} -
- )} -
+// {/* 头部状态栏 */} +//
+//

{casualTitle || '安全分析终端'}

+// {taskStatus.loading && ( +//
+// ⏳ 正在执行: {taskStatus.task} {taskStatus.plan ? `-> ${taskStatus.plan}` : ''} +//
+// )} +//
- {/* 核心双轨渲染区 */} -
- {/* 第一轨:自由对话流 */} -
{casualElements.map(renderNode)}
+// {/* 核心双轨渲染区 */} +//
+// {/* 第一轨:自由对话流 */} +//
{casualElements.map(renderNode)}
- {/* 第二轨:任务规划流 (带有视觉隔离) */} - {taskElements.length > 0 && ( -
-

📡 任务执行审计日志

- {taskElements.map(renderNode)} -
- )} -
+// {/* 第二轨:任务规划流 (带有视觉隔离) */} +// {taskElements.length > 0 && ( +//
+//

📡 任务执行审计日志

+// {taskElements.map(renderNode)} +//
+// )} +//
- {/* 底部输入区 */} -
- - setInputValue(e.target.value)} - onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} - placeholder="输入分析指令..." - /> - -
-
- ) -} +// {/* 底部输入区 */} +//
+// +// setInputValue(e.target.value)} +// onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} +// placeholder="输入分析指令..." +// /> +// +//
+//
+// ) +// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx index b821497c2c..651cc28459 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx @@ -1,5 +1,5 @@ -// SessionRuntime.tsx -import React, { createContext } from 'react' +// // SessionRuntime.tsx +// import React, { createContext } from 'react' -// 初始值给空字符串,实际由外层 Tabs 组件注入 -export const SessionIdContext = createContext('') +// // 初始值给空字符串,实际由外层 Tabs 组件注入 +// export const SessionIdContext = createContext('') diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx index f0ae3aa49f..80298454f6 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx @@ -1,50 +1,50 @@ // ChatTabsLayout.tsx -import React, { useState, useEffect, useCallback } from 'react' -import { Tabs } from 'antd' -import { globalSessionEngine } from './ChatMultiSessionController' -import { SessionIdContext } from './SessionRuntime' -import { ChatWindow } from './ChatWindow' -import { GlobalNotification } from './GlobalNotification' +// import React, { useState, useEffect, useCallback } from 'react' +// import { Tabs } from 'antd' +// import { globalSessionEngine } from './ChatMultiSessionController' +// import { SessionIdContext } from './SessionRuntime' +// import { ChatWindow } from './ChatWindow' +// import { GlobalNotification } from './GlobalNotification' -export const ChatTabsLayout = () => { - const [activeTabId, setActiveTabId] = useState('session-1') +// export const ChatTabsLayout = () => { +// const [activeTabId, setActiveTabId] = useState('session-1') - // 模拟你的多会话列表 - const [sessions, setSessions] = useState([ - { id: 'session-1', title: '分析会话 A' }, - { id: 'session-2', title: '分析会话 B' }, - ]) +// // 模拟你的多会话列表 +// const [sessions, setSessions] = useState([ +// { id: 'session-1', title: '分析会话 A' }, +// { id: 'session-2', title: '分析会话 B' }, +// ]) - // 💥 焦点同步:只要 Tab 切换,立刻告诉大管家,用于拦截后台弹窗 - useEffect(() => { - globalSessionEngine.setActiveSession(activeTabId) +// // 💥 焦点同步:只要 Tab 切换,立刻告诉大管家,用于拦截后台弹窗 +// useEffect(() => { +// globalSessionEngine.setActiveSession(activeTabId) - // 切回 Tab 时,顺手清空该会话的未读小红点状态 - const { store } = globalSessionEngine.ensureSession(activeTabId) - store.getState().setHasUnreadAttention(false) - }, [activeTabId]) +// // 切回 Tab 时,顺手清空该会话的未读小红点状态 +// const { store } = globalSessionEngine.ensureSession(activeTabId) +// store.getState().setHasUnreadAttention(false) +// }, [activeTabId]) - const onChange = useCallback((newActiveKey: string) => { - setActiveTabId(newActiveKey) - }, []) +// const onChange = useCallback((newActiveKey: string) => { +// setActiveTabId(newActiveKey) +// }, []) - return ( -
- ({ - key: session.id, - label: session.title, - children: ( - // 注入上下文,包裹具体的聊天界面 - - - - - ), - }))} - /> -
- ) -} +// return ( +//
+// ({ +// key: session.id, +// label: session.title, +// children: ( +// // 注入上下文,包裹具体的聊天界面 +// +// +// +// +// ), +// }))} +// /> +//
+// ) +// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts index 47198ba4d2..ce932515b1 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts @@ -6,6 +6,7 @@ import { useCreation, useMemoizedFn } from 'ahooks' import cloneDeep from 'lodash/cloneDeep' import { yakitNotify } from '@/utils/notification' import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' +import { DefaultTodoListCardData } from './defaultConstant' function useCasualChat(params: UseCasualChatParams): [UseCasualChatState, UseCasualChatEvents] diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts index b3a4ff3b70..bec758e5c6 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts @@ -271,6 +271,7 @@ function useChatIPC(params?: UseChatIPCParams) { onSubTaskID: (taskID) => { casualSubTaskIDs.current.add(taskID) }, + pushLog: () => {}, }) // #endregion @@ -301,6 +302,7 @@ function useChatIPC(params?: UseChatIPCParams) { onReviewExtra: onTaskReviewExtra, onReviewRelease: handleTaskReviewRelease, sendRequest: sendRequest, + pushLog: () => {}, }) // #endregion @@ -412,7 +414,8 @@ function useChatIPC(params?: UseChatIPCParams) { chatType: 'reAct', type: AIChatQSDataTypeEnum.QUESTION, Timestamp: moment().unix(), - data: { qs: params.FreeInput || '', setting: {} }, + // data: { qs: params.FreeInput || '', setting: {} }, + data: '', AIService: '', AIModelName: '', // showQS为了UI渲染方便,重新构建的字段 @@ -435,7 +438,7 @@ function useChatIPC(params?: UseChatIPCParams) { events.handleSend({ request: params, optionValue, - extraValue, + // extraValue, cb: () => { sendRequest(params) }, @@ -893,7 +896,8 @@ function useChatIPC(params?: UseChatIPCParams) { chatType: 'reAct', type: AIChatQSDataTypeEnum.QUESTION, Timestamp: moment().unix(), - data: { qs: firstQS.current.FreeInput || '', setting: {} }, + // data: { qs: firstQS.current.FreeInput || '', setting: {} }, + data: '', AIService: '', AIModelName: '', // showQS为了UI渲染方便,重新构建的字段 From c7c14dd87f2718a56332924438ada3bd45d35b34 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 15 Jul 2026 12:12:29 +0800 Subject: [PATCH 032/101] =?UTF-8?q?feat:=E4=BE=A7=E8=BE=B9=E6=A0=8Fai?= =?UTF-8?q?=E5=AF=B9=E6=8E=A5=E6=96=B0=E7=89=88(=E4=B8=BB=E8=A6=81?= =?UTF-8?q?=E6=98=AF=E5=A4=84=E7=90=86=E6=8A=A5=E9=94=99=EF=BC=8CwithHisto?= =?UTF-8?q?ryAIReActChat=E3=80=81HistroryAIReActChat)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/HistroryAIReActChat.tsx | 112 ++++++---- .../src/components/withHistoryAIReActChat.tsx | 192 ++++-------------- .../compoment/KnowledgeBaseContent.tsx | 2 +- .../AIHttpFlowFuzzStatusCard.tsx | 14 +- .../aiYaklangCode/AIYaklangCode.tsx | 30 ++- 5 files changed, 139 insertions(+), 211 deletions(-) diff --git a/app/renderer/src/main/src/components/HistroryAIReActChat.tsx b/app/renderer/src/main/src/components/HistroryAIReActChat.tsx index 03004c1a59..853ae135de 100644 --- a/app/renderer/src/main/src/components/HistroryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/HistroryAIReActChat.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react' +import React, { memo, useEffect, useMemo, useRef } from 'react' import { FC } from 'react' import { AIReActChat } from '@/pages/ai-re-act/aiReActChat/AIReActChat' @@ -14,7 +14,7 @@ import { AISendResProps, } from '@/pages/ai-re-act/aiReActChat/AIReActChatType' import { getAIModelAvailableInfo, isForcedSetAIModal } from '@/pages/ai-agent/aiModelList/utils' -import { useDebounceFn, useMemoizedFn, useRequest, useSafeState } from 'ahooks' +import { useDebounceFn, useInViewport, useMemoizedFn, useRequest, useSafeState, useUpdateEffect } from 'ahooks' import { apiGetGlobalNetworkConfig } from '@/pages/spaceEngine/utils' import { defaultParams, GlobalNetworkConfig } from './configNetwork/ConfigNetworkPage' import { RemoteAIAgentGV } from '@/enums/aiAgent' @@ -23,16 +23,17 @@ import { getRemoteValue } from '@/utils/kv' import { AIModelForm } from '@/pages/ai-agent/aiModelList/aiModelForm/AIModelForm' import useListenWidth from '@/pages/pluginHub/hooks/useListenWidth' import { AISource } from '@/pages/ai-re-act/hooks/grpcApi' +import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' +import { loadHistoryAIEmbeddedReviewPolicy, setHistoryAIReviewPolicy } from './historyAIReActChatStorage' +import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' +import { AIAgentSettingDefault } from '@/pages/ai-agent/defaultConstant' interface HistoryAIReActChatProps { - refRef: React.RefObject showFreeChat: boolean setShowFreeChat: React.Dispatch> aiReActChatRef: React.RefObject onStartRequest: (data: AIHandleStartParams) => Promise onSendRequest: (data: AISendParams) => Promise - setSetting: React.Dispatch> - inViewport: boolean className?: string title?: React.ReactNode mergeRemoteAIAgentSetting?: (cache: AIAgentSetting, prev: AIAgentSetting) => AIAgentSetting @@ -42,16 +43,14 @@ interface HistoryAIReActChatProps { source: AISource } -const HistroryAIReActChat: FC = (props) => { +/** TODO - @whale 修改确认 */ +const HistroryAIReActChat: FC = memo((props) => { const { - refRef, showFreeChat, setShowFreeChat, aiReActChatRef, onStartRequest, onSendRequest, - inViewport, - setSetting, className, title = 'AI', mergeRemoteAIAgentSetting, @@ -60,10 +59,21 @@ const HistroryAIReActChat: FC = (props) => { source, } = props + const { setSetting } = useAIAgentDispatcher() + const { setting } = useAIAgentStore() + const [_, setGlobalNetworkConfig] = useSafeState(defaultParams) + const refRef = useRef(null) + + const [inViewport = true] = useInViewport(refRef) const chatWidth = useListenWidth(refRef) + const embeddedSettingCacheReadyRef = useRef(false) + const lastPersistedEmbeddedSettingRef = useRef<{ + ReviewPolicy?: AIAgentSetting['ReviewPolicy'] + }>({}) + const { data, run, loading } = useRequest( async () => { const res = await getAIModelAvailableInfo() @@ -78,9 +88,40 @@ const HistroryAIReActChat: FC = (props) => { run() onGetGlobalNetworkConfig() getAIModelListOption() + applyHistoryAIEmbeddedReviewPolicy() + getAIAgentChatSetting() } }, [inViewport]) + const applyHistoryAIEmbeddedReviewPolicy = useMemoizedFn(async () => { + const reviewPolicy = await loadHistoryAIEmbeddedReviewPolicy() + lastPersistedEmbeddedSettingRef.current = { ReviewPolicy: reviewPolicy } + setSetting((prev) => ({ + ...prev, + ReviewPolicy: reviewPolicy, + })) + }) + + useEffect(() => { + applyHistoryAIEmbeddedReviewPolicy().finally(() => { + embeddedSettingCacheReadyRef.current = true + }) + }, [applyHistoryAIEmbeddedReviewPolicy]) + + useUpdateEffect(() => { + if (!showFreeChat) return + applyHistoryAIEmbeddedReviewPolicy() + }, [showFreeChat, applyHistoryAIEmbeddedReviewPolicy]) + + useUpdateEffect(() => { + if (!embeddedSettingCacheReadyRef.current) return + const policy = setting.ReviewPolicy ?? AIAgentSettingDefault.ReviewPolicy ?? 'manual' + if (lastPersistedEmbeddedSettingRef.current.ReviewPolicy === policy) return + setHistoryAIReviewPolicy(policy).then(() => { + lastPersistedEmbeddedSettingRef.current = { ReviewPolicy: policy } + }) + }, [setting.ReviewPolicy]) + const getAIModelListOption = useDebounceFn( () => { isForcedSetAIModal({ @@ -96,32 +137,29 @@ const HistroryAIReActChat: FC = (props) => { apiGetGlobalNetworkConfig().then(setGlobalNetworkConfig) }) - useEffect(() => { - if (inViewport) { - // 获取缓存的全局配置数据 - getRemoteValue(RemoteAIAgentGV.AIAgentChatSetting) - .then((res) => { - if (!res) return - try { - const cache = JSON.parse(res) as AIAgentSetting - if (typeof cache !== 'object') return - const { ReviewPolicy: _ignoredPolicy, ...cacheWithoutReviewPolicy } = cache - setSetting((prev) => { - const next = mergeRemoteAIAgentSetting - ? mergeRemoteAIAgentSetting(cacheWithoutReviewPolicy as AIAgentSetting, prev) - : { ...prev, ...cacheWithoutReviewPolicy } - return { - ...next, - ReviewPolicy: prev.ReviewPolicy, - Source: source, - } - }) - } catch (error) {} - }) - .catch(() => {}) - } - return () => {} - }, [inViewport]) + /** 获取缓存的全局配置数据 */ + const getAIAgentChatSetting = useMemoizedFn(async () => { + getRemoteValue(RemoteAIAgentGV.AIAgentChatSetting) + .then((res) => { + if (!res) return + try { + const cache = JSON.parse(res) as AIAgentSetting + if (typeof cache !== 'object') return + const { ReviewPolicy: _ignoredPolicy, ...cacheWithoutReviewPolicy } = cache + setSetting((prev) => { + const next = mergeRemoteAIAgentSetting + ? mergeRemoteAIAgentSetting(cacheWithoutReviewPolicy as AIAgentSetting, prev) + : { ...prev, ...cacheWithoutReviewPolicy } + return { + ...next, + ReviewPolicy: prev.ReviewPolicy, + Source: source, + } + }) + } catch (error) {} + }) + .catch(() => {}) + }) useEffect(() => { if (!inViewport || loading || data === undefined || data) return @@ -167,7 +205,6 @@ const HistroryAIReActChat: FC = (props) => { // 有模型 → 正常聊天 return ( = (props) => { data, externalParameters, loading, - mergeRemoteAIAgentSetting, onSendRequest, onStartRequest, run, @@ -198,6 +234,6 @@ const HistroryAIReActChat: FC = (props) => { {resultRender}
) -} +}) export { HistroryAIReActChat } diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index 21fbacde30..7505aea4c4 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -1,19 +1,11 @@ import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef } from 'react' -import { useCreation, useInViewport, useMemoizedFn, useSafeState, useUpdateEffect } from 'ahooks' +import { useCreation, useMemoizedFn, useSafeState } from 'ahooks' import { cloneDeep } from 'lodash' import AIAgentContext, { AIAgentContextDispatcher, AIAgentContextStore, } from '@/pages/ai-agent/useContext/AIAgentContext' -import ChatIPCContent, { - AIChatIPCSendParams, - AISendConfigHotpatchParams, - AISendSyncMessageParams, - ChatIPCContextDispatcher, - ChatIPCContextStore, - defaultDispatcherOfChatIPC, -} from '@/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent' import { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' import { AIMentionCommandParams } from '@/pages/ai-agent/components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin' import { AIAgentSettingDefault } from '@/pages/ai-agent/defaultConstant' @@ -49,20 +41,20 @@ import { normalizeYaklangCodeChangeForReview, resetYakRunnerPatchWorkingDraft, } from '../pages/yakRunner/yakRunnerAiCodePatchApply' -import { ChatIPCSendType, UseChatIPCEvents } from '@/pages/ai-re-act/hooks/type' -import useChatIPC from '@/pages/ai-re-act/hooks/useChatIPC' import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' -import useDeleteAIImageByNode from '@/pages/ai-agent/components/aiMilkdownInput/aiCustomFile/hooks/useDeleteAIImageByNode' import emiter from '@/utils/eventBus/eventBus' import { HistroryAIReActChat } from './HistroryAIReActChat' -import { loadHistoryAIEmbeddedReviewPolicy, setHistoryAIReviewPolicy } from './historyAIReActChatStorage' +import { useChatIPC } from '@/pages/ai-re-act/hooks/useNewChatIPC' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' export type HistoryAIReActChatExternalParameters = NonNullable export interface HistoryAIReActChatBridge { activeID?: string - events: UseChatIPCEvents + // events: UseChatIPCEvents onStop: () => void onNewChat: () => void onChatFromHistory: (session: string) => void @@ -196,6 +188,7 @@ export interface HistoryAIReActChatProviderProps { mergeRemoteAIAgentSetting?: (cache: AIAgentSetting, prev: AIAgentSetting) => AIAgentSetting } +/** TODO - @whale 修改确认 */ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProviderInner({ source, focusModeLoop, @@ -208,9 +201,6 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid }: HistoryAIReActChatProviderProps) { const aiReActChatRef = useRef(null) const [showFreeChat, setShowFreeChat] = useSafeState(false) - const refRef = useRef(null) - - const [inViewport = true] = useInViewport(refRef) const [setting, setSetting, getSetting] = useGetSetState(() => cloneDeep(AIAgentSettingDefault)) const [activeChat, setActiveChat] = useSafeState() @@ -222,44 +212,6 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const pendingMentionRef = useRef(null) const chatReadyRef = useRef(false) const yakRunnerLastAttachedResourceInfoRef = useRef([]) - const embeddedSettingCacheReadyRef = useRef(false) - const lastPersistedEmbeddedSettingRef = useRef<{ - ReviewPolicy?: AIAgentSetting['ReviewPolicy'] - }>({}) - - const applyHistoryAIEmbeddedReviewPolicy = useMemoizedFn(async () => { - const reviewPolicy = await loadHistoryAIEmbeddedReviewPolicy() - lastPersistedEmbeddedSettingRef.current = { ReviewPolicy: reviewPolicy } - setSetting((prev) => ({ - ...prev, - ReviewPolicy: reviewPolicy, - })) - }) - - useEffect(() => { - applyHistoryAIEmbeddedReviewPolicy().finally(() => { - embeddedSettingCacheReadyRef.current = true - }) - }, [applyHistoryAIEmbeddedReviewPolicy]) - - useUpdateEffect(() => { - if (!showFreeChat) return - applyHistoryAIEmbeddedReviewPolicy() - }, [showFreeChat, applyHistoryAIEmbeddedReviewPolicy]) - - useUpdateEffect(() => { - if (!inViewport) return - applyHistoryAIEmbeddedReviewPolicy() - }, [inViewport, applyHistoryAIEmbeddedReviewPolicy]) - - useUpdateEffect(() => { - if (!embeddedSettingCacheReadyRef.current) return - const policy = setting.ReviewPolicy ?? AIAgentSettingDefault.ReviewPolicy ?? 'manual' - if (lastPersistedEmbeddedSettingRef.current.ReviewPolicy === policy) return - setHistoryAIReviewPolicy(policy).then(() => { - lastPersistedEmbeddedSettingRef.current = { ReviewPolicy: policy } - }) - }, [setting.ReviewPolicy]) useEffect(() => { if (!showFreeChat) { @@ -348,19 +300,11 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid pushAIFuzzStatusRuntimeIdToWebFuzzerPage(httpFuzzTabPageId, runtimeId, { source: 'auto' }) }) - const [chatIPCData, events] = useChatIPC({ - // autoConnect: true, - // aiSource, - // cacheDataStore, - // getSetting, - // onHttpFuzzRequestChange, - // onGetHttpFlowFuzzStatus, - // onYaklangCodeChange, - }) - - const [, { onClearImage }] = useDeleteAIImageByNode() + const { onStart, onSend, onClose } = useChatIPC() - const { execute, casualLoading } = chatIPCData + const store = useCurrentStore() + const execute = useStore(store, (state) => state.execute) + const casualLoading = useStore(store, (state) => state.casualLoading) useEffect(() => { if (!httpFuzzTabPageId && !yakRunnerPageId) { @@ -393,27 +337,10 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid return activeChat?.SessionID }, [activeChat]) - useUpdateEffect(() => { - events.onSwitchChat(activeChat?.SessionID, activeChat?.isCreate) - }, [activeChat]) - - const handleSendInteractiveMessage = useMemoizedFn((params: AIChatIPCSendParams, type: ChatIPCSendType) => { - const { value, id, optionValue } = params - if (!activeID) return - if (!id) return - - const info: AIInputEvent = { - IsInteractiveMessage: true, - InteractiveId: id, - InteractiveJSONInput: value, - } - events.onSend({ token: activeID, type, params: info, optionValue }) - }) - - const handleSendCasual = useMemoizedFn((params: AIChatIPCSendParams) => { - const targetParams = { ...params, FocusModeLoop: focusModeLoop } - handleSendInteractiveMessage(targetParams, 'casual') - }) + /** 切换逻辑已改为新版,待确认后即可删除该代码 */ + // useUpdateEffect(() => { + // events.onSwitchChat(activeChat?.SessionID, activeChat?.isCreate) + // }, [activeChat]) const onStartRequest = useMemoizedFn((data: AIHandleStartParams) => { const newChat: AIHandleStartExtraProps = resolveStartExtraParams?.(data) ?? { @@ -445,16 +372,19 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid }) const onChatFromHistory = useMemoizedFn((session: string) => { - events.onDelChats([session]) + // events.onDelChats([session]) + globalSessionEngine.forceCloseSession({ + sessionIds: [session], + }) }) /** 新建会话:清空 UI、断开旧连接,并预生成新的 TimelineSessionID */ const onNewChat = useMemoizedFn(() => { const currentID = activeChat?.SessionID if (execute && currentID) { - events.onClose(currentID) + onClose([currentID]) } - events.onReset() + // events.onReset() setActiveChat(undefined) setSetting((prev) => ({ ...prev, @@ -467,15 +397,10 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const onStop = useMemoizedFn(() => { if (execute && activeID) { - events.onClose(activeID) + onClose([activeID]) } }) - const handleSend = useMemoizedFn((params: AIChatIPCSendParams) => { - const targetParams = { ...params, FocusModeLoop: focusModeLoop } - handleSendInteractiveMessage(targetParams, '') - }) - const onSendRequest = useMemoizedFn((data: AISendParams) => { let params: AIInputEvent = { ...data.params, FocusModeLoop: focusModeLoop } if (httpFuzzTabPageId) { @@ -502,83 +427,36 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const handleSubmitQuery = useMemoizedFn((value: HandleStartParams) => { const sessionID = activeChat?.SessionID if (execute && sessionID) { - const { extra, attachedResourceInfo } = getAIReActRequestParams(value) + const { attachedResourceInfo } = getAIReActRequestParams(value) const chatMessage: AIInputEvent = { IsFreeInput: true, FreeInput: value.qs, AttachedResourceInfo: attachedResourceInfo, FocusModeLoop: value.focusMode ?? focusModeLoop, } - const onSend = (res: AISendResProps) => { + const onSendChat = (res: AISendResProps) => { const { params } = res - events.onSend({ + onSend({ token: sessionID, type: 'casual', params: { IsFreeInput: true, ...params, }, - extraValue: extra, }) emiter.emit('sessionData', JSON.stringify({ type: 'refresh', sessionId: sessionID })) aiReActChatRef.current?.setValue('') } onSendRequest({ params: chatMessage }) - .then(onSend) + .then(onSendChat) .catch(() => { - onSend({ params: chatMessage }) + onSendChat({ params: chatMessage }) }) return } aiReActChatRef.current?.handleStart(value) }) - const handleSendSyncMessage = useMemoizedFn((data: AISendSyncMessageParams) => { - if (!activeID) return - const { syncType, SyncJsonInput } = data - const params = { ...data.params, FocusModeLoop: focusModeLoop } - const info: AIInputEvent = { - IsSyncMessage: true, - SyncType: syncType, - SyncJsonInput, - Params: params, - } - events.onSend({ token: activeID, type: '', params: info }) - }) - - const handleSendConfigHotpatch = useMemoizedFn((data: AISendConfigHotpatchParams) => { - if (!activeID) return - const { hotpatchType } = data - - const params = { ...data.params, FocusModeLoop: focusModeLoop } - const info: AIInputEvent = { - IsConfigHotpatch: true, - HotpatchType: hotpatchType, - Params: params, - } - events.onSend({ token: activeID, type: '', params: info }) - }) - - const store: ChatIPCContextStore = useCreation(() => { - return { - chatIPCData, - planReviewTreeKeywordsMap: new Map(), - reviewExpand: false, - } - }, [chatIPCData]) - - const dispatcher: ChatIPCContextDispatcher = useCreation(() => { - return { - ...defaultDispatcherOfChatIPC, - chatIPCEvents: events, - handleSendCasual, - handleStop: onStop, - handleSend, - handleSendSyncMessage, - handleSendConfigHotpatch, - } - }, [events]) - const stores: AIAgentContextStore = useMemo(() => { return { setting: setting, @@ -591,13 +469,16 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid getSetting: getSetting, setSetting: setSetting, setActiveChat: setActiveChat, + onStart, + onSend, + onClose, } }, []) const historyAIReActChatBridge: HistoryAIReActChatBridge = useMemo( () => ({ activeID, - events, + // events, onStop, onNewChat, onChatFromHistory, @@ -634,7 +515,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid }) }, }), - [activeID, events, onStop, onNewChat, onChatFromHistory, setActiveChat, handleSubmitQuery], + [activeID, onStop, onNewChat, onChatFromHistory, setActiveChat, handleSubmitQuery], ) const renderHistoryAIReActChat = useCallback( @@ -642,21 +523,18 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid ), - [inViewport, flushPendingMention, mergeRemoteAIAgentSetting, onSendRequest, onStartRequest, showFreeChat, source], + [flushPendingMention, mergeRemoteAIAgentSetting, onSendRequest, onStartRequest, showFreeChat, source], ) const contextValue = useMemo( @@ -672,9 +550,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid return ( - - {children} - + {children} ) }) diff --git a/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx b/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx index 223622d0f6..a0d962dd9a 100644 --- a/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx +++ b/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx @@ -678,7 +678,7 @@ const KnowledgeBaseContent = forwardRef(func const mergeRemoteAIAgentSetting = useMemoizedFn((cache: AIAgentSetting, prev: AIAgentSetting) => ({ ...cache, - TimelineSessionID: prev.TimelineSessionID, + TimelineSessionID: prev.TimelineSessionID, // TODO @whale 这个字段需要跟nonight确认,他说这个字段没用 })) return ( diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx index 7f26b7948e..1bd4fc23af 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx @@ -9,7 +9,6 @@ import { Tooltip } from 'antd' import YakitSolidLoading from '@/components/yakitUI/YakitSolidLoading/YakitSolidLoading' import ChatCard from '../ChatCard' import ModalInfo, { type ModalInfoProps } from '../ModelInfo' -import { WebFuzzerAiStore } from '@/pages/ai-agent/store/ChatDataStore' import { hasWebFuzzerPageOnAIFuzzStatus, pushAIFuzzStatusRuntimeIdToWebFuzzerPage, @@ -17,6 +16,8 @@ import { import emiter from '@/utils/eventBus/eventBus' import { YakitRoute } from '@/enums/yakitRoute' import { yakitNotify } from '@/utils/notification' +import { usePageInfo } from '@/store/pageInfo' +import { shallow } from 'zustand/shallow' const STATS_TILE_PX = 91 const STATS_GAP_PX = 4 @@ -53,6 +54,13 @@ export const AIHttpFlowFuzzStatusCard: React.FC = return data.progress?.average_response_ms ?? 0 }, [renderNum]) + const { getCurrentSelectPageId, currentPageTabRouteKey } = usePageInfo( + (s) => ({ + getCurrentSelectPageId: s.getCurrentSelectPageId, + currentPageTabRouteKey: s.currentPageTabRouteKey, + }), + shallow, + ) // 「查看详情」点击: // - 若卡片所在的会话绑定了某个 Web Fuzzer 页签(`WebFuzzerAiStore`), // 则把本卡片的 `runtime_id` 显式推送到该页签,并打开 traffic analysis 抽屉。 @@ -63,8 +71,8 @@ export const AIHttpFlowFuzzStatusCard: React.FC = yakitNotify('error', '该发包统计缺少 runtime_id,无法查看详情') return } - const store = chatIPCEvents.fetchChatDataStore() - const fuzzerPageId = store instanceof WebFuzzerAiStore ? store.fuzzerPageId : '' + const fuzzerPageId = + currentPageTabRouteKey === YakitRoute.WebsocketFuzzer ? getCurrentSelectPageId(currentPageTabRouteKey) : '' if (fuzzerPageId && hasWebFuzzerPageOnAIFuzzStatus(fuzzerPageId)) { // Web Fuzzer 页内:直接把本卡片的 runtime_id 推过去; // 当右侧无本地发包响应时空状态会切换为 history 表,已可见的 history 表则会按新 key 重新加载。 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx index b043828c95..f43e382c0e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/AIYaklangCode.tsx @@ -10,9 +10,10 @@ import styles from './AIYaklangCode.module.scss' import { useCreation, useMemoizedFn, useThrottleEffect } from 'ahooks' import { NewHTTPPacketEditor } from '@/utils/editors' import { monaco as monacoApi } from 'react-monaco-editor' -import useChatIPCDispatcher from '../../useContext/ChatIPCContent/useDispatcher' -import { WebFuzzerAiStore } from '@/pages/ai-agent/store/ChatDataStore' -import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' +import useAIAgentStore from '../../useContext/useStore' +import { AISourceEnum } from '@/pages/ai-re-act/hooks/grpcApi' +import { usePageInfo } from '@/store/pageInfo' +import { shallow } from 'zustand/shallow' import { YakitMonacoDiffInline } from '@/components/yakitUI/YakitMonacoDiffInline/YakitMonacoDiffInline' const CODE_BLOCK_MAX_HEIGHT = 200 @@ -141,17 +142,24 @@ export const AIYaklangCode: React.FC = React.memo((props) => ) } }) - const { chatIPCEvents } = useChatIPCDispatcher() - + // const { chatIPCEvents } = useChatIPCDispatcher() + const { setting } = useAIAgentStore() + + const { getCurrentSelectPageId, currentPageTabRouteKey } = usePageInfo( + (s) => ({ + getCurrentSelectPageId: s.getCurrentSelectPageId, + currentPageTabRouteKey: s.currentPageTabRouteKey, + }), + shallow, + ) + /** TODO - 这个pageId获取待修改@whale */ const webFuzzerAiStoreFuzzerPageId = useMemo((): string | undefined => { - const store = chatIPCEvents.fetchChatDataStore() - return store instanceof WebFuzzerAiStore ? store.fuzzerPageId : undefined - }, [chatIPCEvents]) - const { chatDataStoreKey } = useGetChatDataStoreKey() + return getCurrentSelectPageId(currentPageTabRouteKey) + }, [currentPageTabRouteKey]) const isWebFuzzerAiStore = useMemo(() => { - return chatDataStoreKey === 'WebFuzzerAiStore' - }, [chatDataStoreKey]) + return setting.Source === AISourceEnum.webFuzzer + }, [setting.Source]) const titleExtra = useMemo(() => { if (!modalInfo) return null From ff35e2fe9329329a9dcc0c60e96f8bd1e6e3a42a Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 15 Jul 2026 12:16:50 +0800 Subject: [PATCH 033/101] =?UTF-8?q?1.feat:=E5=AD=90=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E7=9A=84AIChildWindowTaskDefaultGroupCard=E3=80=81AIChildWindo?= =?UTF-8?q?wConcurrentStreamCard=202.feat:=E6=89=93=E5=BC=80=E5=AD=90?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86=EF=BC=9B?= =?UTF-8?q?=E5=AD=90=E7=AA=97=E5=8F=A3=E5=88=B7=E6=96=B0=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E7=9B=B4=E6=8E=A5=E4=BB=8E=E4=B8=BB=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E4=B8=AD=E6=8E=A8=E9=80=81=203.del:=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=AD=90=E7=AA=97=E5=8F=A3=E6=B2=A1=E6=9C=89=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E7=9A=84=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../openAIConcurrentStream/index.js | 9 +- .../AIConcurrentStream/AIConcurrentStream.tsx | 163 ++++++++++-------- .../buildConcurrentStreamContext.ts | 44 ----- .../fetchConcurrentStreamContents.ts | 12 +- .../useContext/AIConcurrentStreamContent.ts | 30 ++++ .../useContext/useDispatcher.ts | 7 + .../AIConcurrentStream/useContext/useStore.ts | 7 + .../src/main/src/auxWindow/types/types.ts | 8 +- .../AITaskDefaultGroupCard.tsx | 19 +- ...hildWindowTaskDefaultGroupCard.module.scss | 2 + .../AIChildWindowTaskDefaultGroupCard.tsx | 153 ++++++++++++++++ .../aiChildWindowTaskDefaultGroupCard/type.ts | 5 + .../AITaskDefaultGroupCardHeard.module.scss | 10 ++ .../components/AITaskDefaultGroupCard/type.ts | 1 - .../ConcurrentStreamCard.tsx | 3 +- .../ConcurrentStreamCardActions.tsx | 77 +-------- .../ConcurrentStreamContent.module.scss | 26 +-- .../ConcurrentStreamContent.tsx | 46 +++-- ...hildWindowConcurrentStreamCard.module.scss | 7 + .../AIChildWindowConcurrentStreamCard.tsx | 157 +++++++++++++++++ .../aiChildWindowConcurrentStreamCard/type.ts | 12 ++ .../buildConcurrentStreamFramePayload.ts | 91 ++++++++++ .../collectConcurrentStreamContentEntries.ts | 56 +++--- .../concurrentStreamMainBridge.ts | 5 +- .../useConcurrentStreamRefreshListener.ts | 38 ++-- .../ConcurrentStreamCardHeard.module.scss | 11 ++ .../ConcurrentStreamCardHeard.tsx | 3 +- .../concurrentStreamCardHeard/type.ts | 3 +- .../concurrentStreamFrame.ts | 8 +- .../hooks/useConcurrentStreamCardStyle.ts | 9 +- .../aiChatListItem/aiTaskItem/AITaskItem.tsx | 2 +- .../AIReActChatHeader.module.scss | 2 + 32 files changed, 731 insertions(+), 295 deletions(-) delete mode 100644 app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/buildConcurrentStreamContext.ts create mode 100644 app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/AIConcurrentStreamContent.ts create mode 100644 app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/useDispatcher.ts create mode 100644 app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/useStore.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts diff --git a/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js b/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js index ca749ac843..ced9c0ed45 100644 --- a/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js +++ b/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js @@ -29,9 +29,10 @@ function register(manager, mainWindow) { } } + /** @deprecated 获取数据的逻辑改为直接从主窗口推送,如需下面逻辑,需要按新版获取数据逻辑补充 */ ipcMain.handle(FETCH_CONTENTS, async (_event, frame) => { if (!frame?.session || !frame?.token || !mainWindow || mainWindow.isDestroyed()) { - return { contentEntries: [] } + return { rawData: [] } } const requestId = crypto.randomUUID() @@ -40,12 +41,12 @@ function register(manager, mainWindow) { return new Promise((resolve) => { const timeout = setTimeout(() => { ipcMain.removeAllListeners(responseChannel) - resolve({ contentEntries: [] }) + resolve({ rawData: [] }) }, 15000) ipcMain.once(responseChannel, (_responseEvent, data) => { clearTimeout(timeout) - resolve(data ?? { contentEntries: [] }) + resolve(data ?? { rawData: [] }) }) safeSendMain('fetch-concurrent-stream-contents-request', { requestId, ...frame }) @@ -53,7 +54,7 @@ function register(manager, mainWindow) { }) ipcMain.handle('open-ai-concurrent-stream-window', async (_event, data) => { - if (!data || typeof data !== 'object' || !Array.isArray(data.elements)) return + if (!data || typeof data !== 'object' || !Array.isArray(data.childrenTokens)) return const singletonKey = buildSingletonKey(data) const title = typeof data.taskName === 'string' && data.taskName ? data.taskName : 'Concurrent Stream' return manager.create({ diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx index 321ab5b91c..aae81ca556 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx @@ -1,20 +1,24 @@ -import React, { Suspense, lazy, startTransition, useEffect, useMemo, useState } from 'react' +import React, { Suspense, memo, startTransition, useEffect, useMemo, useRef, useState } from 'react' import { yakitAuxWindow } from '@/services/electronBridge' -import ChatIPCContext from '@/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent' import ConcurrentStreamSkeleton from '@/auxWindow/components/ConcurrentStreamSkeleton/ConcurrentStreamSkeleton' import { type ConcurrentStreamFramePayload, isConcurrentStreamFrame, } from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' import { AIChatQSDataTypeEnum, type AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' -import { buildConcurrentStreamContext } from './buildConcurrentStreamContext' -import { fetchConcurrentStreamContents } from './fetchConcurrentStreamContents' import styles from './AIConcurrentStream.module.scss' - -const ConcurrentStreamCard = lazy(() => import('@/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard')) -const AITaskDefaultGroupCard = lazy( - () => import('@/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard'), -) +import AIChildWindowTaskDefaultGroupCard from '@/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard' +import AIChildWindowConcurrentStreamCard from '@/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard' +import AIConcurrentStreamContent, { + AIConcurrentStreamDispatcher, + AIConcurrentStreamStore, +} from './useContext/AIConcurrentStreamContent' +import useMemoizedFn from 'ahooks/lib/useMemoizedFn' + +// const ConcurrentStreamCard = lazy(() => import('@/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard')) +// const AITaskDefaultGroupCard = lazy( +// () => import('@/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard'), +// ) const { ipcRenderer } = window.require('electron') @@ -22,20 +26,27 @@ interface AIConcurrentStreamProps { windowId: string } -const AIConcurrentStream: React.FC = ({ windowId }) => { +const AIConcurrentStream: React.FC = memo(({ windowId }) => { const [frame, setFrame] = useState(null) const [contentVersion, setContentVersion] = useState(0) - const [contentEntries, setContentEntries] = useState | null>(null) - const [loadingContents, setLoadingContents] = useState(false) + + // rawData 用 ref 存储,更新不触发渲染; + // 组件及子组件的重渲染由 contentVersion(renderNum)驱动 + const rawDataRef = useRef>(new Map()) useEffect(() => { if (!windowId) return - const applyFrame = (payload: Record) => { + const applyFrame = (payload: ConcurrentStreamFramePayload) => { if (!isConcurrentStreamFrame(payload)) return + const framePayload = payload + // 直接使用 frame 里的 rawData 存入 ref + rawDataRef.current = framePayload.rawData ?? new Map() startTransition(() => { - setFrame(payload) - setContentEntries(null) + setFrame({ + ...framePayload, + rawData: new Map(), + }) setContentVersion((v) => v + 1) }) } @@ -58,42 +69,37 @@ const AIConcurrentStream: React.FC = ({ windowId }) => } }, [windowId]) - useEffect(() => { - if (!frame || contentVersion === 0) return - - let cancelled = false - setLoadingContents(true) - - fetchConcurrentStreamContents(frame) - .then((entries) => { - if (!cancelled) setContentEntries(entries) - }) - .catch(() => { - if (!cancelled) setContentEntries([]) - }) - .finally(() => { - if (!cancelled) setLoadingContents(false) - }) - - return () => { - cancelled = true - } - }, [frame, contentVersion]) - - const contextValue = useMemo(() => { - if (!frame || !contentEntries) return null - return buildConcurrentStreamContext({ ...frame, contentEntries }) - }, [contentEntries, frame]) + /** deprecated 目前数据都是通过 openAIConcurrentStream发送到子窗口,如需沿用下面的逻辑,需要将获取数据的方法改为新版 */ + // useEffect(() => { + // if (!frame || contentVersion === 0) return + + // let cancelled = false + // setLoadingContents(true) + + // fetchConcurrentStreamContents(frame) + // .then((entries) => { + // if (!cancelled) setContentEntries(entries) + // }) + // .catch(() => { + // if (!cancelled) setContentEntries([]) + // }) + // .finally(() => { + // if (!cancelled) setLoadingContents(false) + // }) + + // return () => { + // cancelled = true + // } + // }, [frame, contentVersion]) const isTaskDefaultGroup = useMemo(() => { - if (!frame || !contentEntries) return false - const root = contentEntries.find(([key]) => key === frame.token)?.[1] + if (!frame) return false + const root = rawDataRef.current.get(frame.token) return root?.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP - }, [contentEntries, frame]) - - const cardKey = `${frame?.session}:${frame?.token}:${frame?.chatType}` + }, [frame, contentVersion]) - const requestRefresh = () => { + // 刷新:通过 IPC 通知主窗口重新构建并推送最新 frame(含最新 rawData) + const requestRefresh = useMemoizedFn(() => { if (!frame) return ipcRenderer.send('request-ai-concurrent-stream-refresh', { type: 'openAIConcurrentStream', @@ -103,44 +109,59 @@ const AIConcurrentStream: React.FC = ({ windowId }) => chatType: frame.chatType, }, }) - } - - if (!frame || loadingContents || !contextValue) { + }) + const store: AIConcurrentStreamStore = useMemo(() => { + return { + session: frame?.session, + token: frame?.token, + chatType: frame?.chatType, + childrenTokens: frame?.childrenTokens, + rawData: rawDataRef.current, + renderNum: contentVersion, + } + }, [frame, contentVersion]) + const dispatcher: AIConcurrentStreamDispatcher = useMemo(() => { + return { + requestRefresh, + } + }, []) + if (!frame) { return } return ( - +
}> {isTaskDefaultGroup ? ( - + // + ) : ( - + // + )}
- + ) -} +}) export default AIConcurrentStream diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/buildConcurrentStreamContext.ts b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/buildConcurrentStreamContext.ts deleted file mode 100644 index e25480491e..0000000000 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/buildConcurrentStreamContext.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { cloneDeep } from 'lodash' -import { - defaultDispatcherOfChatIPC, - type ChatIPCContextValue, -} from '@/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent' -import { defaultChatIPCData } from '@/pages/ai-agent/defaultConstant' -import { ChatDataStore } from '@/pages/ai-agent/store/ChatDataStore' -import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' -import type { ConcurrentStreamFramePayload } from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' - -export function buildConcurrentStreamContext({ - session, - contentEntries, -}: ConcurrentStreamFramePayload & { - contentEntries: Array<[string, AIChatQSData]> -}): ChatIPCContextValue { - const chatDataStore = new ChatDataStore() - chatDataStore.create(session) - - contentEntries.forEach(([mapKey, content]) => { - const chatData = chatDataStore.get(session) - if (!chatData) return - if (content.chatType === 'task') { - chatData.taskChat.contents.set(mapKey, content) - } else { - chatData.casualChat.contents.set(mapKey, content) - } - }) - - return { - store: { - chatIPCData: cloneDeep(defaultChatIPCData), - reviewInfo: undefined, - planReviewTreeKeywordsMap: new Map(), - }, - dispatcher: { - ...defaultDispatcherOfChatIPC, - chatIPCEvents: { - ...defaultDispatcherOfChatIPC.chatIPCEvents, - fetchChatDataStore: () => chatDataStore, - }, - }, - } -} diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts index ef516b38aa..83324c13a3 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts @@ -3,9 +3,17 @@ import type { ConcurrentStreamFramePayload } from '@/pages/ai-agent/components/C const { ipcRenderer } = window.require('electron') +/** + * @deprecated + * 如果沿用这个逻辑,参考: + * 子窗口通过 IPC 向主窗口拉取 task 相关的全部 content 数据。 + * 主窗口从 ChatDataStore 中收集 task 自身、childrenTokens 各节点、group 内子节点的数据, + * 返回为 Map(token → 原始数据)。 + */ export async function fetchConcurrentStreamContents( frame: ConcurrentStreamFramePayload, -): Promise> { +): Promise> { const result = await ipcRenderer.invoke('fetch-concurrent-stream-contents', frame) - return result?.contentEntries ?? [] + const entries: Array<[string, AIChatQSData]> = result?.rawData ?? [] + return new Map(entries) } diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/AIConcurrentStreamContent.ts b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/AIConcurrentStreamContent.ts new file mode 100644 index 0000000000..162df4cb9d --- /dev/null +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/AIConcurrentStreamContent.ts @@ -0,0 +1,30 @@ +import { ConcurrentStreamFramePayload } from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' +import { createContext } from 'react' + +export interface AIConcurrentStreamStore extends Partial { + /** 子窗口刷新版本号,点击刷新按钮自增,驱动子组件重渲染 */ + renderNum?: number +} + +export interface AIConcurrentStreamDispatcher { + requestRefresh: () => void +} + +export interface AIConcurrentStreamValue { + store: AIConcurrentStreamStore + dispatcher: AIConcurrentStreamDispatcher +} + +export default createContext({ + store: { + session: '', + token: '', + chatType: 'reAct', + childrenTokens: [], + rawData: new Map(), + renderNum: 0, + }, + dispatcher: { + requestRefresh: () => {}, + }, +}) diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/useDispatcher.ts b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/useDispatcher.ts new file mode 100644 index 0000000000..8a95f4682a --- /dev/null +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/useDispatcher.ts @@ -0,0 +1,7 @@ +import { useContext } from 'react' +import AIConcurrentStreamValue, { AIConcurrentStreamDispatcher } from './AIConcurrentStreamContent' + +export default function useAIConcurrentStreamDispatcher(): AIConcurrentStreamDispatcher { + const { dispatcher } = useContext(AIConcurrentStreamValue) + return dispatcher +} diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/useStore.ts b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/useStore.ts new file mode 100644 index 0000000000..47c4341db3 --- /dev/null +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/useStore.ts @@ -0,0 +1,7 @@ +import { useContext } from 'react' +import AIConcurrentStreamValue, { AIConcurrentStreamStore } from './AIConcurrentStreamContent' + +export default function useAIConcurrentStreamStore(): AIConcurrentStreamStore { + const { store } = useContext(AIConcurrentStreamValue) + return store +} diff --git a/app/renderer/src/main/src/auxWindow/types/types.ts b/app/renderer/src/main/src/auxWindow/types/types.ts index 1b88080d6b..4c3a997d97 100644 --- a/app/renderer/src/main/src/auxWindow/types/types.ts +++ b/app/renderer/src/main/src/auxWindow/types/types.ts @@ -1,6 +1,8 @@ +import { ConcurrentStreamFramePayload } from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' + export interface AuxWindowCreateOptions { route: string - payload?: Record + payload?: ConcurrentStreamFramePayload singletonKey?: string title?: string width?: number @@ -16,13 +18,13 @@ export interface AuxWindowInitPayload { windowId: string route: string title?: string - payload: Record + payload: ConcurrentStreamFramePayload } export interface AuxWindowPushPayload { windowId: string route: string - payload: Record + payload: ConcurrentStreamFramePayload } declare global { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx index 38c673aa05..bdad97bc05 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx @@ -10,29 +10,22 @@ import { AITaskDefaultGroupCardHeardWrapperProps, AITaskDefaultGroupCardListWrap const AITaskDefaultGroupCard: FC<{ token: string - isChildWindow: boolean -}> = memo(({ token, isChildWindow }) => { +}> = memo(({ token }) => { const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean(true) const [contentFocused, setContentFocused] = useState(false) useEffect(() => { - if (isChildWindow) return collapseExpand() - }, [collapseExpand, isChildWindow]) + }, [collapseExpand]) return (
- + {expand ? : null}
@@ -56,7 +49,7 @@ const AITaskDefaultGroupCardListWrapper: FC = memo((props) => { - const { token, expandToggle, expand, isChildWindow } = props + const { token, expandToggle, expand } = props const store = useCurrentStore() const renderNum = useStore(store, (state) => state.tasks[token].renderNum) const rawData = useCurrentRawData() @@ -68,7 +61,7 @@ const AITaskDefaultGroupCardHeardWrapper: FC = memo((props) => { + const { token } = props + const [expand, { toggle: expandToggle }] = useBoolean(true) + + const { rawData } = useAIConcurrentStreamStore() + const { requestRefresh } = useAIConcurrentStreamDispatcher() + const timeStamp = useCreation(() => { + if (!rawData) return 0 + const itemData = rawData.get(token) + if (!itemData) return 0 + return itemData.Timestamp || 0 + }, []) + + return ( +
+ + + {expand ? : null} +
+ ) +}) + +export default AIChildWindowTaskDefaultGroupCard + +const AIChildWindowTaskDefaultGroupContent: FC = memo((props) => { + const { childrenTokens, rawData } = useAIConcurrentStreamStore() + + const { ref: scrollRef, isFocus } = useClickFocus() + return ( +
+
+
+ +
+
+
+ ) +}) + +// #region 子窗口 group 渲染 + +/** 子窗口版 group item,数据从 rawData Map 获取 */ +const AIChildWindowGroupItem: FC<{ token: string; rawData: Map }> = memo(({ token, rawData }) => { + const itemData = rawData.get(token) + if (!itemData) return null + + switch (itemData.type) { + case AIChatQSDataTypeEnum.STREAM_GROUP: + return + default: + return null + } +}) + +/** 子窗口版 stream group 卡片,从 rawData 中按 parentGroupToken 查找子节点 */ +const AIChildWindowGroupStreamCard: FC<{ token: string; rawData: Map }> = memo( + ({ token, rawData }) => { + // 从 rawData 中查找属于该 group 的子节点(parentGroupToken === token) + const childItems: AIChatQSData[] = [] + rawData.forEach((value) => { + if (value.parentGroupToken === token) { + childItems.push(value) + } + }) + + return ( +
+ {childItems.map((item) => ( + + ))} +
+ ) + }, +) + +// #endregion + +// #region 子窗口 node item 渲染 + +/** + * 子窗口版 node item 包装器。 + * STREAM 类型依赖 streaming hooks(useTypedStream → useCurrentStore), + * 子窗口无 store,故 STREAM 直接展示纯文本内容;其余类型复用 AINodeItem。 + */ +const AIChildWindowNodeItemWrapper: FC<{ itemData: AIChatQSData }> = memo(({ itemData }) => { + if (itemData.type === AIChatQSDataTypeEnum.STREAM) { + const streamData = itemData as ChatStream + return ( +
+
{streamData.data?.content || ''}
+
+ ) + } + return ( +
+ +
+ ) +}) + +// #endregion diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/type.ts new file mode 100644 index 0000000000..35168153bf --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/type.ts @@ -0,0 +1,5 @@ +export interface AIChildWindowTaskDefaultGroupCardProps { + token: string +} + +export interface AIChildWindowTaskDefaultGroupContentProps {} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.module.scss index f7f8e8636f..f6327a251f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.module.scss @@ -1,3 +1,13 @@ +@use '../../../../../styles/mixin.scss' as mixin; +%display-flex-center { + display: flex; + align-items: center; +} +%display-column-center { + display: flex; + align-items: center; + flex-direction: column; +} .ai-task-default-group-card-title { height: 20px; display: flex; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts index 484d6720d0..e169b2c574 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts @@ -2,7 +2,6 @@ import { Dispatch, SetStateAction } from 'react' export interface AITaskDefaultGroupCardHeardWrapperProps { expand: boolean - isChildWindow: boolean token: string expandToggle: () => void } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index 7188c5d157..8c10afe0c8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -15,6 +15,7 @@ import type { import classNames from 'classnames' import ConcurrentStreamCardHeard from './concurrentStreamCardHeard/ConcurrentStreamCardHeard' import ConcurrentStreamContent from './ConcurrentStreamContent/ConcurrentStreamContent' +import { useConcurrentStreamRefreshListener } from './concurrentStream/useConcurrentStreamRefreshListener' const ConcurrentStreamCard: FC<{ token: string @@ -44,7 +45,7 @@ const ConcurrentStreamCard: FC<{ } }, [isChildWindow, raw?.data?.status]) - // useConcurrentStreamRefreshListener(framePayload, session, token, chatType, !isChildWindow) + useConcurrentStreamRefreshListener(token, !isChildWindow) const presentation = useMemo(() => getAIStatusPresentation(raw?.data?.status), [raw?.data?.status]) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx index 8af0869fd9..74c9f5dd52 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx @@ -9,13 +9,13 @@ import { OutlineRefreshIcon, } from '@/assets/icon/outline' import { AIHistoryContinueTask, AIHistorySkipTask } from '../../../chatTemplate/historyTaskTree/HistoryTaskTree' -import { openAIConcurrentStream, OpenAIConcurrentStreamPayload } from '@/utils/openWebsite' +import { openAIConcurrentStream } from '@/utils/openWebsite' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import styles from '../ConcurrentStreamCard.module.scss' import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useCreation, useMemoizedFn } from 'ahooks' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' -import { ReActChatElement, ReActChatTaskElementSub } from '@/pages/ai-re-act/hooks/aiRender' +import { buildConcurrentStreamFramePayload } from '../concurrentStream/buildConcurrentStreamFramePayload' /** 卡片标题栏右侧操作区 */ interface ConcurrentStreamCardActionsProps { @@ -59,79 +59,12 @@ const ConcurrentStreamCardActions: FC = ({ return itemData.chatType }, []) - const getKind = useMemoizedFn((childToken) => { - const state = store.getState() - if (state.items[childToken]) return 'item' - if (state.groups[childToken]) return 'group' - return null - }) - - // TODO - 待优化 const openChildWindow = useMemoizedFn((e) => { e?.stopPropagation() - if (!chatType) return - const elements: ReActChatTaskElementSub[] = [] - const childrenTokens = store.getState().tasks[token]?.childrenTokens || [] - - const length = childrenTokens.length - - const getItem = (tokenList) => { - const items: ReActChatElement[] = [] - for (const token in tokenList) { - const childData = rawData.contents.get(token) - if (!childData) continue - const item: ReActChatElement = { - kind: 'item', - token, - type: childData?.type, - chatType: childData?.chatType, - renderNum: store.getState().tasks[token]?.renderNum, - } - items.push(item) - } - return items - } - for (let index = 0; index < length; index++) { - const childToken = childrenTokens[index] - const kind = getKind(childToken) - if (!kind) continue - const childData = rawData.contents.get(childToken) - - if (!childData) continue - switch (kind) { - case 'group': - const groupItem: ReActChatTaskElementSub = { - kind: 'group', - token: childToken, - type: childData?.type, - chatType: childData?.chatType, - children: getItem(store.getState().groups[token]?.childrenTokens), - renderNum: store.getState().tasks[childToken]?.renderNum, - } - elements.push(groupItem) - break - case 'item': - const item: ReActChatTaskElementSub = { - kind: 'item', - token: childToken, - type: childData?.type, - chatType: childData?.chatType, - renderNum: store.getState().tasks[childToken]?.renderNum, - } - elements.push(item) - break - default: - break - } - } - - const framePayload: OpenAIConcurrentStreamPayload = { - session, - token, - chatType, - elements, + const framePayload = buildConcurrentStreamFramePayload({ token, session, chatType, store, rawData }) + if (framePayload) { + openAIConcurrentStream(framePayload) } - openAIConcurrentStream(framePayload) }) if (isChildWindow) { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss index 6c3298bac9..f21547c5fa 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss @@ -16,6 +16,14 @@ } } +// 子窗口内容区外层容器:撑满父级高度 +.concurrent-stream-content-wrapper { + height: 100%; + display: flex; + flex-direction: column; + flex: 1; +} + .focused { overflow-y: overlay; } @@ -26,16 +34,10 @@ overflow: visible; } -.concurrent-stream-content-wrapper { - height: 100%; - display: flex; - flex-direction: column; - flex: 1; - .content { - width: 100%; - max-height: 500px; - background-color: var(--Colors-Use-Basic-Background); - padding: 4px; - border-radius: 8px; - } +.content { + width: 100%; + max-height: 500px; + background-color: var(--Colors-Use-Basic-Background); + padding: 4px; + border-radius: 8px; } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx index d9f4d532b8..b212aece61 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx @@ -122,32 +122,30 @@ const ConcurrentStreamContent: FC = memo( }, [embedInParentScroll, handleScroll, scrollContainerRef]) return ( -
+
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/type.ts index b44caa0171..47705f6285 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/type.ts @@ -5,8 +5,9 @@ export interface ConcurrentStreamCardHeardProps { isChildWindow: boolean expand: boolean expandToggle: () => void - onClickTitle: () => void + onClickTitle?: () => void rowData?: ChatTaskNodeGroup coordinatorId?: string taskIndex?: string | null + onRefresh?: () => void } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts index 997444b5d5..d536deb878 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts @@ -1,10 +1,12 @@ -import type { ChatListRenderType, ReActChatTaskElementSub } from '@/pages/ai-re-act/hooks/aiRender' +import type { AIChatQSData, ChatListRenderType } from '@/pages/ai-re-act/hooks/aiRender' export interface ConcurrentStreamFramePayload { session: string token: string chatType: ChatListRenderType - elements: ReActChatTaskElementSub[] + childrenTokens: string[] + /** childrenTokens 中各节点 token 对应的原始数据 */ + rawData: Map taskName?: string } @@ -15,6 +17,6 @@ export function isConcurrentStreamFrame(data: unknown): data is ConcurrentStream typeof record.session === 'string' && typeof record.token === 'string' && typeof record.chatType === 'string' && - Array.isArray(record.elements) + Array.isArray(record.childrenTokens) ) } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/hooks/useConcurrentStreamCardStyle.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/hooks/useConcurrentStreamCardStyle.ts index 4df1fa5eeb..613b144948 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/hooks/useConcurrentStreamCardStyle.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/hooks/useConcurrentStreamCardStyle.ts @@ -1,5 +1,5 @@ import { useMemo, type CSSProperties } from 'react' -import { CHILD_WINDOW_STYLE } from '../constants' +// import { CHILD_WINDOW_STYLE } from '../constants' interface BuildCardStyleOptions { bgColor: string @@ -8,15 +8,16 @@ interface BuildCardStyleOptions { isChildWindow?: boolean } -/** 收起时叠加斜纹背景,展开或子窗口仅显示状态色 */ +/** + * 收起时叠加斜纹背景,展开 + * 子窗口不需要这个样式 + */ export function buildConcurrentStreamCardStyle({ bgColor, vectorBg, showStripe, isChildWindow, }: BuildCardStyleOptions): CSSProperties { - if (isChildWindow) return CHILD_WINDOW_STYLE - const isGradientBg = bgColor.includes('gradient(') if (!showStripe) { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx index 2d2f57e061..b7cf9cdcfe 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx @@ -13,7 +13,7 @@ const AITaskItem: React.FC = React.memo((props) => { const type = useStore(store, (state) => state.groups[token].type) switch (type) { case AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP: - return + return case AIChatQSDataTypeEnum.TASK_NODE_GROUP: return default: diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss index 45b2f92828..095aa72d70 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss @@ -1,3 +1,5 @@ +@use '../../../../styles/mixin.scss' as mixin; + %display-flex-center { display: flex; align-items: center; From 92001015d7b465111ba7ed8b5c142f13fa6f1b56 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Thu, 16 Jul 2026 14:26:00 +0800 Subject: [PATCH 034/101] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=8A=A5?= =?UTF-8?q?=E9=94=99,=E6=9A=82=E6=97=B6=E5=B1=8F=E8=94=BDai=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E7=9A=84=E6=95=B0=E6=8D=AE=E8=8E=B7=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../historyAIReActChat/TaskDetailsPopover.tsx | 6 +-- .../AIContextToken/AIContextToken.tsx | 2 +- .../chatTemplate/AIAgentChatTemplate.tsx | 42 ++++++++----------- .../TimelineCard/TimelineCard.tsx | 21 +++++----- .../historyTaskTree/HistoryTaskTree.tsx | 10 ++++- .../concurrentStreamFrame.ts | 4 +- .../StreamingChatContent.tsx | 30 +------------ .../aiCustomFile/AICustomFile.tsx | 12 +----- .../components/aiMilkdownInput/utils.ts | 4 +- .../components/aiTaskQuery/AITaskQuery.tsx | 1 + .../ai-agent/components/aiTriageChat/type.ts | 5 +-- .../src/pages/ai-agent/store/ChatDataStore.ts | 1 - .../main/src/pages/ai-agent/type/aiChat.d.ts | 1 - 13 files changed, 51 insertions(+), 88 deletions(-) diff --git a/app/renderer/src/main/src/components/historyAIReActChat/TaskDetailsPopover.tsx b/app/renderer/src/main/src/components/historyAIReActChat/TaskDetailsPopover.tsx index dfcd6d960a..1cc5747342 100644 --- a/app/renderer/src/main/src/components/historyAIReActChat/TaskDetailsPopover.tsx +++ b/app/renderer/src/main/src/components/historyAIReActChat/TaskDetailsPopover.tsx @@ -9,8 +9,8 @@ import { AITaskExecutionDetails } from '@/pages/ai-agent/chatTemplate/aiTaskExec import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { yakitNotify } from '@/utils/notification' -import { useHistoryAIReActChat } from '../withHistoryAIReActChat' import styles from './historyAIReActChat.module.scss' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' export interface TaskDetailsPopoverProps { taskLabel?: string @@ -18,13 +18,13 @@ export interface TaskDetailsPopoverProps { const TaskDetailsPopover: React.FC = ({ taskLabel = '自由对话' }) => { const { t } = useI18nNamespaces(['yakitUi']) - const { historyAIReActChatBridge } = useHistoryAIReActChat() const [visible, setVisible] = useState(false) const [aiTaskDetails, setAITaskDetails] = useState<{ key: string; label: string; goal: string }>() + const store = useCurrentStore() const handleOpen = useMemoizedFn(() => { if (visible) return - const taskId = historyAIReActChatBridge.events.fetchCurrentCasualTaskID() + const taskId = store.getState().currentCasualTaskID if (!taskId) { yakitNotify('error', 'taskId不存在') return diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken.tsx index 5cb8d99aac..cb5f9ff1fe 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIContextToken/AIContextToken.tsx @@ -1,4 +1,4 @@ -import type { FC, memo } from 'react' +import { type FC, memo } from 'react' import ContextPressurePanel from './ContextPressurePanel' import ContextCostPanel from './ContextCostPanel' import ContextTokenSummary from './ContextTokenSummary' diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx index af8575e95e..ceb44c3f08 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx @@ -32,7 +32,6 @@ import { YakitResizeBox, YakitResizeBoxProps } from '@/components/yakitUI/YakitR import { HistoryTaskTree } from './historyTaskTree/HistoryTaskTree' import { AIReviewParams } from '../components/aiReviewResult/AIReviewResult' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' -import useLoadHistory from '@/pages/ai-re-act/hooks/useLoadHistory' import { useCurrentMeta, useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' import useAIAgentDispatcher from '../useContext/useDispatcher' @@ -214,7 +213,7 @@ export const AIAgentChatStream: React.FC = memo((props) const rawData = useCurrentRawData() const meta = useCurrentMeta() /** TODO - hooks未写 */ - const { handleLoadMoreHistory, handleHasMoreHistory, fetchChatDataStore } = useChatIPCDispatcher().chatIPCEvents + // const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents useUpdateEffect(() => { scrollToIndex('LAST') }, [scrollToBottom]) @@ -234,13 +233,13 @@ export const AIAgentChatStream: React.FC = memo((props) }, [highlightedItem]) // 向上滚动加载 - const { firstItemIndex, handleLoadMore, isPrependingRef } = useLoadHistory({ - loading: taskLoadMoreLoading, - dataLength: streams.length, - SessionID: session, - fetchHasMore: () => handleHasMoreHistory(TYPE), - loadMore: () => handleLoadMoreHistory(TYPE), - }) + // const { firstItemIndex, handleLoadMore, isPrependingRef } = useLoadHistory({ + // loading: taskLoadMoreLoading, + // dataLength: streams.length, + // SessionID: session, + // fetchHasMore: () => handleHasMoreHistory(TYPE), + // loadMore: () => handleLoadMoreHistory(TYPE), + // }) const { virtuosoRef, @@ -249,7 +248,10 @@ export const AIAgentChatStream: React.FC = memo((props) scrollToIndex, scrollToItemIndex: scrollToListItem, handleTotalListHeightChanged, - } = useVirtuosoAutoScroll({ total: streams.length, isPrependingRef }) + } = useVirtuosoAutoScroll({ + total: streams.length, + // isPrependingRef + }) const { locateToIndex } = useChatStreamLocateHighlight({ scrollToIndex: scrollToListItem, @@ -260,15 +262,10 @@ export const AIAgentChatStream: React.FC = memo((props) scrollToIndex('LAST') }, [scrollToBottom]) - const renderItem = useCallback( - (index: number, stream: ReActChatRenderElement) => { - if (!stream.token) return null - const arrayIndex = index - firstItemIndex - const hasNext = streams.length - arrayIndex > 1 - return - }, - [firstItemIndex, streams.length], - ) + const renderItem = useCallback((index: number, stream: ReActChatRenderElement) => { + if (!stream.token) return null + return + }, []) const Item = useCallback( ({ children, style, 'data-index': dataIndex }) => (
@@ -328,7 +325,7 @@ export const AIAgentChatStream: React.FC = memo((props) ref={virtuosoRef} key={session} scrollerRef={setScrollerRef} - firstItemIndex={firstItemIndex} + // firstItemIndex={firstItemIndex} atBottomStateChange={setIsAtBottomRef} style={{ height: '100%', width: '100%' }} data={streams} @@ -338,10 +335,7 @@ export const AIAgentChatStream: React.FC = memo((props) atBottomThreshold={100} initialTopMostItemIndex={streams.length > 1 ? streams.length - 1 : 0} skipAnimationFrameInResizeObserver - // overscan={20} - // atTopStateChange={handleAtTopStateChange} - startReached={handleLoadMore} - // increaseViewportBy={{top: 160, bottom: 160}} + // startReached={handleLoadMore} components={components} />
diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx index 13484704de..f7cf3a72a3 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx @@ -11,7 +11,6 @@ import { OutlineInformationcircleIcon } from '@/assets/icon/outline' import { useMemoizedFn, useSize } from 'ahooks' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import useAIAgentStore from '../../useContext/useStore' -import useLoadHistory from '@/pages/ai-re-act/hooks/useLoadHistory' import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' @@ -91,20 +90,20 @@ const TimelineCard: FC = () => { const timelinesLoading = useStore(store, (state) => state.requestHistoryState.timelinesLoading) /** TODO - 等待hooks */ - const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents + // const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents const { virtuosoRef, handleTotalListHeightChanged, setScrollerRef, setIsAtBottomRef } = useVirtuosoAutoScroll({ total: reActTimelines.length, }) const containerRef = useRef(null) const size = useSize(containerRef) - const { firstItemIndex, handleLoadMore } = useLoadHistory({ - loading: timelinesLoading, - dataLength: reActTimelines.length, - SessionID: activeChat?.SessionID || '', - fetchHasMore: () => handleHasMoreHistory('timelines'), - loadMore: () => handleLoadMoreHistory('timelines'), - }) + // const { firstItemIndex, handleLoadMore } = useLoadHistory({ + // loading: timelinesLoading, + // dataLength: reActTimelines.length, + // SessionID: activeChat?.SessionID || '', + // fetchHasMore: () => handleHasMoreHistory('timelines'), + // loadMore: () => handleLoadMoreHistory('timelines'), + // }) const components = useMemo>( () => ({ @@ -129,7 +128,7 @@ const TimelineCard: FC = () => { { increaseViewportBy={{ top: 300, bottom: 300 }} atBottomThreshold={100} skipAnimationFrameInResizeObserver - startReached={handleLoadMore} + // startReached={handleLoadMore} itemContent={itemContent} /> diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx index 3a45500ae1..1403537a38 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx @@ -264,7 +264,10 @@ export const AIHistorySkipTask: React.FC<{ taskId?: string | null; isTask?: bool ({ taskId, isTask = true }) => { const { t } = useI18nNamespaces(['aiAgent']) const syncIdOfStopSubTask = useRef('') - // const { syncIdInfoMap } = useChatIPCStore() + const store = useCurrentStore() + const syncIDUpdate = useStore(store, (state) => state.syncIDUpdate) + + const meta = useCurrentMeta() const sessionId = useCurrentSessionId() const { onSend } = useAIAgentDispatcher() const onCancelTask = useMemoizedFn(() => { @@ -289,6 +292,9 @@ export const AIHistorySkipTask: React.FC<{ taskId?: string | null; isTask?: bool } }) + const skipLoading = useCreation(() => { + return !!meta.syncIDMap?.get(syncIdOfStopSubTask.current) + }, [syncIDUpdate]) return ( } type="text" - loading={!!syncIdInfoMap?.get(syncIdOfStopSubTask.current)} + loading={skipLoading} onClick={(e) => { e.stopPropagation() }} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts index d536deb878..a309a9cc7f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts @@ -1,4 +1,5 @@ import type { AIChatQSData, ChatListRenderType } from '@/pages/ai-re-act/hooks/aiRender' +import { isMap } from 'lodash' export interface ConcurrentStreamFramePayload { session: string @@ -17,6 +18,7 @@ export function isConcurrentStreamFrame(data: unknown): data is ConcurrentStream typeof record.session === 'string' && typeof record.token === 'string' && typeof record.chatType === 'string' && - Array.isArray(record.childrenTokens) + Array.isArray(record.childrenTokens) && + isMap(record.rawData) ) } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx index 7d55ba9445..2dbbf6b7e2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx @@ -1,21 +1,12 @@ import { AIStreamNode } from '@/pages/ai-re-act/aiReActChatContents/AIReActChatContents' -import { AIChatQSDataTypeEnum, ChatStream, type ReActChatRenderItem } from '@/pages/ai-re-act/hooks/aiRender' -import { memo, type FC } from 'react' +import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' +import { type FC } from 'react' import { useTypedStream } from './hooks/useTypedStream' -import AITaskDefaultGroupCard from '../../AITaskDefaultGroupCard/AITaskDefaultGroupCard' -import ConcurrentStreamCard from '../../ConcurrentStreamCard/ConcurrentStreamCard' import { useCreation } from 'ahooks' import styles from './StreamingChatContent.module.scss' type StreamCls = { className: string } | { aiMarkdownProps?: { className: string } } -type StreamingChatContentProps = ReActChatRenderItem & { - streamClassName?: StreamCls - hasNext?: boolean - session: string - itemIndex?: number -} - type SingleStreamProps = { itemData: ChatStream renderNum: number @@ -40,20 +31,3 @@ export const AIStreamCard: FC = ({ itemData }) => { if (!stream) return null return } - -const StreamingChatContent: FC = (props) => { - const { streamClassName, chatType, token, session } = props - if (props.kind === 'task') { - if (props.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP) { - return - } else { - return - } - } - // if (props.kind === 'group') { - // return - // } - return null - // return -} -export default memo(StreamingChatContent) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiMilkdownInput/aiCustomFile/AICustomFile.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiMilkdownInput/aiCustomFile/AICustomFile.tsx index 1d425e3067..01e91c8d24 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiMilkdownInput/aiCustomFile/AICustomFile.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiMilkdownInput/aiCustomFile/AICustomFile.tsx @@ -8,13 +8,11 @@ import classNames from 'classnames' import { yakitNotify } from '@/utils/notification' import { randomString } from '@/utils/randomUtil' import { Progress } from 'antd' -import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' const { ipcRenderer } = window.require('electron') export const AICustomFile: React.FC = React.memo((props) => { - const { sessionId, chatDataStoreKey: chatDataStoreKeyProp } = props - const { chatDataStoreKey: chatDataStoreKeyFromStore } = useGetChatDataStoreKey() + const { sessionId, chatDataStoreKey } = props const { node, contentRef, view, selected, setAttrs } = useNodeViewContext() const [showSrc, setShowSrc] = useState('') @@ -58,13 +56,6 @@ export const AICustomFile: React.FC = React.memo((props) => { } }, []) - const resolveChatDataStoreKey = useMemoizedFn(() => { - if (chatDataStoreKeyFromStore && chatDataStoreKeyFromStore !== 'unknown') { - return chatDataStoreKeyFromStore - } - return chatDataStoreKeyProp || '' - }) - const onSaveLocal = useMemoizedFn(async (blobUrl: string) => { setShowSrc(blobUrl) try { @@ -79,7 +70,6 @@ export const AICustomFile: React.FC = React.memo((props) => { setProgress(0) const filename = `image_${Date.now()}.${suffix}` setAttrs({ alt: filename }) - const chatDataStoreKey = resolveChatDataStoreKey() if (!chatDataStoreKey) { yakitNotify('error', '图片保存失败: 无法识别当前 AI 存储路径') return diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiMilkdownInput/utils.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiMilkdownInput/utils.ts index 36ff93cc7a..34335bcb79 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiMilkdownInput/utils.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiMilkdownInput/utils.ts @@ -3,8 +3,8 @@ import { editorViewCtx, parserCtx } from '@milkdown/kit/core' import { AIMentionCommandParams, aiMentionCustomId } from './aiMilkdownMention/aiMentionPlugin' import { AIHttpFlowCommandParams, aiHttpFlowCustomId } from './aiMilkdownHttpFlow/aiHttpFlowPlugin' import { AICodeBlockCommandParams, aiCodeBlockCustomId } from './aiCodeBlock/aiCustomCodeBlockPlugin' -import { AIChatIPCStartParams } from '@/pages/ai-re-act/hooks/type' import { imgTypes } from '@/components/MilkdownEditor/utils/utils' +import { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' /**md编辑器中匹配出提及相关数据/纯文本 */ export const extractDataWithMilkdown = (editor: EditorMilkdownProps) => { @@ -122,7 +122,7 @@ export const setEditorValue = (editor: EditorMilkdownProps, value: string) => { export interface AIInputWithParamsTemplate { description: string - param: AIChatIPCStartParams['extraValue'] + param: AIChatQSData['extraValue'] } /** * diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx index 61b41689b8..f472ce7164 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiTaskQuery/AITaskQuery.tsx @@ -20,6 +20,7 @@ import useAIAgentDispatcher from '../../useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' import useMemoizedFn from 'ahooks/lib/useMemoizedFn' +import useDebounceFn from 'ahooks/lib/useDebounceFn' export const AITaskQuery: React.FC = React.memo(() => { const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts index 1ba26f819e..7ce283b8b0 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts @@ -1,5 +1,4 @@ -import { ChatQuestion, ChatResult, ChatThought } from '@/pages/ai-re-act/hooks/aiRender' -import { AIChatIPCStartParams } from '@/pages/ai-re-act/hooks/type' +import { AIChatQSData, ChatQuestion, ChatResult, ChatThought } from '@/pages/ai-re-act/hooks/aiRender' import { AINodeItemProps } from '../aiChatListItem/aiNodeItem/type' import { ChatDataStoreKey } from '../../store/ChatDataStore' @@ -17,6 +16,6 @@ export interface AITriageChatContentProps { export interface AITriageChatContentEditProps { onCancel: () => void content: string - extraValue?: AIChatIPCStartParams['extraValue'] + extraValue?: AIChatQSData['extraValue'] chatDataStoreKey: AITriageChatContentProps['chatDataStoreKey'] } diff --git a/app/renderer/src/main/src/pages/ai-agent/store/ChatDataStore.ts b/app/renderer/src/main/src/pages/ai-agent/store/ChatDataStore.ts index 1382b1229d..9f972100ff 100644 --- a/app/renderer/src/main/src/pages/ai-agent/store/ChatDataStore.ts +++ b/app/renderer/src/main/src/pages/ai-agent/store/ChatDataStore.ts @@ -32,7 +32,6 @@ export class ChatDataStore { }, httpRunTimeIDs: [], riskRunTimeIDs: [], - yakExecResult: { card: [], execFileRecord: new Map(), yakExecResultLogs: [] }, aiPerfData: { consumption: { cache_hit_token: 0, diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 91a1fcbac8..fab5542a28 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -39,7 +39,6 @@ export interface AIChatData { httpRunTimeIDs: UseChatIPCState['httpRunTimeIDs'] /** 记录数据里所有的riskRunTimeIDs */ riskRunTimeIDs: UseChatIPCState['riskRunTimeIDs'] - yakExecResult: UseChatIPCState['yakExecResult'] /** 性能相关数据 */ aiPerfData: { /** 消耗Token */ From cf941d34d1b923a0a4a8a2ae67f0f6350275119e Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Thu, 16 Jul 2026 15:58:56 +0800 Subject: [PATCH 035/101] =?UTF-8?q?1.feat:=E4=BF=AE=E6=94=B9=E4=B8=80?= =?UTF-8?q?=E4=BA=9Btodo=E9=9C=80=E6=B1=82=202.del:=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E5=86=97=E4=BD=99=E6=96=87=E4=BB=B6=203.perf:=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E8=8E=B7=E5=8F=96kind=20hooks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/withHistoryAIReActChat.tsx | 5 - .../ai-agent/aiAgentChat/AIAgentChat.tsx | 8 +- .../aiMemoryList/AIMemoryList.tsx | 4 +- .../AITaskExecutionDetails.tsx | 12 +- .../ConcurrentStreamContent.tsx | 16 +- .../buildConcurrentStreamFramePayload.ts | 15 +- .../ai-agent/components/ToolInvokerCard.tsx | 10 +- .../hooks/useTypedStream.ts | 2 +- .../aiModelErrorPrompt/AIModelErrorPrompt.tsx | 2 +- .../aiGroupStreamCard/AIGroupStreamCard.tsx | 80 +- .../components/aiGroupStreamCard/type.ts | 2 - .../aiReActChatReview/AIReActChatReview.tsx | 7 +- .../aiReActTaskChat/useTaskChatExtraAction.ts | 46 +- .../ai-re-act/hooks/explame/ChatNode.tsx | 97 -- .../hooks/explame/ChatNotification.tsx | 26 - .../ai-re-act/hooks/explame/ChatReview.tsx | 48 - .../ai-re-act/hooks/explame/ChatWindow.tsx | 97 -- .../hooks/explame/SessionRuntime.tsx | 5 - .../pages/ai-re-act/hooks/explame/layout.tsx | 50 - .../hooks/grpcStreamHandler/aiStream.ts | 2 +- .../pages/ai-re-act/hooks/useAIItemKind.ts | 22 +- .../src/pages/ai-re-act/hooks/useChatIPC.ts | 1125 ----------------- .../ai-re-act/hooks/useGetChatDataStoreKey.ts | 79 +- .../pages/ai-re-act/hooks/useNewChatIPC.ts | 4 +- 24 files changed, 108 insertions(+), 1656 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index 7505aea4c4..ad96e9de30 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -337,11 +337,6 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid return activeChat?.SessionID }, [activeChat]) - /** 切换逻辑已改为新版,待确认后即可删除该代码 */ - // useUpdateEffect(() => { - // events.onSwitchChat(activeChat?.SessionID, activeChat?.isCreate) - // }, [activeChat]) - const onStartRequest = useMemoizedFn((data: AIHandleStartParams) => { const newChat: AIHandleStartExtraProps = resolveStartExtraParams?.(data) ?? { chatId: activeChat?.SessionID, diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index a43050672e..ab4827b198 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -706,10 +706,10 @@ export const AIReActTaskChatReview: React.FC = React const meta = useCurrentMeta() const currentPlanReviewToken = useStore(store, (state) => state.currentPlanReviewToken) const currentPlanReviewExtraUpdate = useStore(store, (state) => state.currentPlanReviewExtraUpdate) - /** TODO - review:工具的倒计时,目前没有类似renderNum的更新逻辑 */ + const reviewInfo = useCreation(() => { - return rawData.contents.get(currentPlanReviewToken) - }, [currentPlanReviewToken]) + return rawData.contents.get(currentPlanReviewToken.token) + }, [currentPlanReviewToken.renderNum]) const planReviewTreeKeywordsMap = useCreation(() => { return meta.planReviewExtraData @@ -748,7 +748,7 @@ export const AIReActTaskChatReview: React.FC = React renderFooterExtra={renderFooter} expand={expand} className={styles['review-body']} - renderNum={0} + renderNum={currentPlanReviewToken.renderNum} />
diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx index 775198c4f3..efdb3cf5f2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx @@ -75,7 +75,9 @@ const AIMemoryList: React.FC = React.memo((props) => { useDebounceEffect( () => { if (!isClearMemory) return - /** TODO - 需要hooks告诉UI层,此次关闭的type(来源) */ + /** TODO - 需要hooks告诉UI层,此次关闭的type(来源) + * 二次讨论:改为onClose中传回调事件 + */ ipcRenderer.once(`${sessionId}-end`, () => { onClearMemory() setIsClearMemory(false) diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx index 54cf641ec6..595b96be4b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiTaskExecutionDetails/AITaskExecutionDetails.tsx @@ -76,21 +76,17 @@ export const AITaskExecutionDetails: React.FC = Rea }) const getData = useMemoizedFn(() => { if (!taskId) return - const sessionId = activeChat?.SessionID || '' - const chatData = chatIPCEvents.fetchChatDataStore()?.get(sessionId) - if (!chatData) return - let itemData: PlanItemDetailsData | undefined - const subTaskData = chatData.casualChat.planDetailsMap.get(taskId) + const subTaskData = rawData.casualChat.planDetailsMap.get(taskId) if (subTaskData) { itemData = subTaskData } else { - const mainPlanDetails = chatData.casualChat.planDetails - const currentCasualTaskId = chatIPCEvents.fetchCurrentCasualTaskID() + const mainPlanDetails = rawData.casualChat.planDetails + const currentCasualTaskId = store.getState().currentCasualTaskID if (mainPlanDetails.taskId === taskId || currentCasualTaskId === taskId) { itemData = mainPlanDetails } else { - itemData = chatData.taskChat.planDetailsMap.get(taskId) + itemData = rawData.taskChat.planDetailsMap.get(taskId) } } if (!itemData) return diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx index b212aece61..468e38709a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx @@ -5,7 +5,8 @@ import useClickFocus from '../../../../ai-re-act/hooks/useClickFocus' import styles from './ConcurrentStreamContent.module.scss' import { AIChatListItem } from '../../aiChatListItem/AIChatListItem' import type { ConcurrentStreamContentItemProps } from './type' -import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import useAIItemKind from '@/pages/ai-re-act/hooks/useAIItemKind' import { ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' const PAGE_SIZE = 20 @@ -155,19 +156,14 @@ const ConcurrentStreamContent: FC = memo( export default ConcurrentStreamContent const ConcurrentStreamContentItem: FC = memo(({ token }) => { - const store = useCurrentStore() const rawData = useCurrentRawData() - const getKind = useMemoizedFn((childToken: string): ReActChatRenderElement['kind'] => { - const state = store.getState() - if (state.items[childToken]) return 'item' - if (state.groups[childToken]) return 'group' - return 'item' - }) - const item = useCreation(() => { + const getKind = useAIItemKind() + + const item: ReActChatRenderElement = useCreation(() => { const data = rawData.contents.get(token) return { token, - kind: getKind(token), + kind: getKind(token) ?? 'item', chatType: data?.chatType ?? 'reAct', isHistory: false, } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts index a930b6166f..706e75cef5 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts @@ -1,13 +1,10 @@ import type { OpenAIConcurrentStreamPayload } from '@/utils/openWebsite' -import { AIChatQSDataTypeEnum, type AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' +import { AIChatQSDataTypeEnum, ChatStoreState, type AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' +import { AIItemKind, getAIItemKind } from '@/pages/ai-re-act/hooks/useAIItemKind' /** store 的最小依赖接口 */ interface BuildFrameStore { - getState: () => { - tasks: Record - items: Record - groups: Record - } + getState: () => ChatStoreState } interface BuildFrameRawData { @@ -23,11 +20,9 @@ export interface BuildConcurrentStreamFramePayloadParams { } /** 判断 childToken 在 store 中的类型 */ -function getKind(store: BuildFrameStore, childToken: string): 'item' | 'group' | null { +function getKind(store: BuildFrameStore, childToken: string): AIItemKind | null { const state = store.getState() - if (state.items[childToken]) return 'item' - if (state.groups[childToken]) return 'group' - return null + return getAIItemKind(state, childToken) } /** 获取 task 节点的名称 */ diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx index edea32fed6..d5f1a52ee4 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx @@ -45,6 +45,7 @@ import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' import useAIAgentDispatcher from '../useContext/useDispatcher' import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' +import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' /** @name AI工具按钮对应图标 */ const AIToolToIconMap: Record = { @@ -309,7 +310,7 @@ const ToolResultCard: React.FC = memo((props) => { return Math.round(data.durationSeconds * 10) / 10 }, [data.durationSeconds]) - const store = useCurrentStore() + const sessionId = useCurrentSessionId() const rawData = useCurrentRawData() const getListToolList = useMemoizedFn(() => { if (!data?.callToolId || !activeChat) return @@ -319,12 +320,7 @@ const ToolResultCard: React.FC = memo((props) => { } grpcQueryAIToolDetails(params) .then((res) => { - /** TODO - 工具卡片刷新后,需要更新这个item里面的数据以及其刷新逻辑 */ - const chatItem = rawData.contents.get(itemData.id) - if (!!chatItem && chatItem.type === AIChatQSDataTypeEnum.TOOL_RESULT) { - chatItem.data.tool.resultDetails = getResultDetails(res) - } - store.getState().incrementNodeVersion(itemData.id, 'item') + globalSessionEngine.updateToolResult(sessionId, itemData.id, { resultDetails: getResultDetails(res) }) }) .finally(() => setTimeout(() => { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts index e89244a85f..a1f2a9fa32 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts @@ -31,7 +31,7 @@ export interface UseTypedStreamResult { * 获取流式聊天内容并应用平滑打字效果 * - 实时流式(start → end):启用打字效果 * - 历史记录(直接 end):禁用打字效果,直接显示 - * TODO - 看这个hooks是否需要改成只传content(显示在页面的数据)内容出去,其余的值通过useCurrentRawData获取 + * TODO - 优化: 看这个hooks是否需要改成只传content(显示在页面的数据)内容出去,其余的值通过useCurrentRawData获取 */ export function useTypedStream(options: UseTypedStreamOptions): UseTypedStreamResult { const { token, step = 2, maxStep = 18, interval = 30, catchUpFrames = 9 } = options diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx index 11eb42862c..cc95b30052 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx @@ -30,7 +30,7 @@ export const AIModelErrorPrompt: React.FC = React.memo( e.stopPropagation() const fileName = getFileNameByModelType(item.data.model_tier) if (!fileName) return - // todo 编辑事件,index为0是因为当前使用的ai模型是单选的且选中项一定是在第一个 + // NOTE - 编辑事件,index为0是因为当前使用的ai模型是单选的且选中项一定是在第一个 onEditAIModel({ aiGlobalConfig, index: 0, diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index 4a0bdec310..54428eb740 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -13,7 +13,6 @@ import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useC import useCreation from 'ahooks/lib/useCreation' import { useStore } from 'zustand' import { AIGroupStreamCardHeardWrapperProps, AIGroupStreamCardListWrapperProps } from './type' -import { useMemoizedFn } from 'ahooks' import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' import AIGroupStreamCardHeard from './aiGroupStreamCardHeard/AIGroupStreamCardHeard' import AIGroupStreamCardList from './aiGroupStreamCardList/AIGroupStreamCardList' @@ -164,74 +163,58 @@ const AIGroupStreamCardHeardWrapper: React.FC state.groups[token].childrenTokens.length) - const chatLength = useStore(store, (state) => state.casualChat.elements.length) const taskChatLength = useStore(store, (state) => state.taskChat.elements.length) + const renderNum = useStore(store, (state) => state.groups[token]?.renderNum) - const getFirstItem = useMemoizedFn(() => { - const group = store.getState().groups[token] - if (!group) return - // 有组数据的时候,childrenTokens肯定会有数据 - const firstItemToken = group.childrenTokens[0] - const firstItem = rawData.contents.get(firstItemToken) - return firstItem - }) + /** 可能存在第一次拿到的数据为undefined */ + const groupData = useCreation(() => { + return rawData.contents.get(token) + }, [renderNum]) - const chatType = useCreation(() => { - const firstItem = getFirstItem() - if (!firstItem) return - return firstItem.chatType - }, []) + const lastToken = useCreation(() => { + if (!groupData) return '' + switch (groupData.type) { + case AIChatQSDataTypeEnum.STREAM_GROUP: + return groupData.data.lastToken + + default: + return '' + } + }, [renderNum]) - const hasNext = useCreation(() => { + const lastItemRenderNum = useStore(store, (state) => state.items[lastToken]?.renderNum) + + const isLastActiveGroup = useCreation(() => { if (expand === false) return false if (perHasNext.current === false) return false - if (chatType === 'reAct') { - perHasNext.current = store.getState().casualChat.elements[chatLength - 1].token === token + if (groupData?.chatType === 'reAct') { + perHasNext.current = store.getState().casualChat.elements[chatLength - 1]?.token === token return perHasNext.current } - perHasNext.current = store.getState().taskChat.elements[taskChatLength - 1].token === token + perHasNext.current = store.getState().taskChat.elements[taskChatLength - 1]?.token === token return perHasNext.current - }, [chatLength, taskChatLength]) + }, [chatLength, taskChatLength, groupData?.chatType]) const nodeLabel = useCreation(() => { - const firstItem = getFirstItem() - if (!firstItem) return - switch (firstItem.type) { - case AIChatQSDataTypeEnum.STREAM: - return getLabelByParams(firstItem.data?.NodeIdVerbose) + if (!groupData) return '' + switch (groupData.type) { + case AIChatQSDataTypeEnum.STREAM_GROUP: + return getLabelByParams(groupData.data?.NodeIdVerbose) default: return '' } - }, []) + }, [renderNum]) useEffect(() => { - if (hasNext) { + if (isLastActiveGroup) { setExpand(false) } - }, [hasNext]) - - const getLastItem = useMemoizedFn(() => { - const group = store.getState().groups[token] - if (!group) return - const length = group.childrenTokens.length - // 有组数据的时候,childrenTokens肯定会有数据 - const lastItemToken = group.childrenTokens[length - 1] - if (!lastItemToken) return - const lastItem = rawData.contents.get(lastItemToken) - return lastItem - }) - - const lastToken = useCreation(() => { - const lastItem = getLastItem() - if (!lastItem) return '' - return lastItem.id - }, []) + }, [isLastActiveGroup]) const shouldShowMask = useMemo(() => { - const lastItem = getLastItem() + const lastItem = rawData.contents.get(lastToken) if (!lastItem) return false switch (lastItem.type) { case AIChatQSDataTypeEnum.STREAM: @@ -241,11 +224,10 @@ const AIGroupStreamCardHeardWrapper: React.FC> - itemData?: ChatTaskDefaultGroup lastToken: string nodeLabel: string shouldShowMask: boolean diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx index 02cbd85eb1..b3aab99ead 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx @@ -31,6 +31,7 @@ import { useStore } from 'zustand' import useAIAgentDispatcher from '../../useContext/useDispatcher' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' import { randomString } from '@/utils/randomUtil' +import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' export const AIReActChatReview: React.FC = React.memo((props) => { const { info, planReviewTreeKeywordsMap, isEmbedded, renderFooterExtra, expand, className, chatType, renderNum } = @@ -367,11 +368,7 @@ export const AIReActChatReview: React.FC = React.memo((p setForgeOption(item) break case 'close': - // if (info.type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) { - // /** TODO - 新版需要逻辑调整*/ - // chatIPCEvents.handleTaskReviewRelease((info.data as AIReviewType).id) - // return - // } + globalSessionEngine.closeChatReview(sessionId, info.chatType, info.id) break default: if (editShow) return diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts index 298b8fb7e4..8e585786d0 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts @@ -1,27 +1,42 @@ -import { useMemoizedFn } from 'ahooks' import { type AIInputEvent, AIInputEventSyncTypeEnum } from '../hooks/grpcApi' import useCurrentSessionId from '../hooks/useCurrentSessionId' import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' import { useStore } from 'zustand' -import { useCurrentStore, useCurrentMeta } from '../hooks/useCurrentDataBySession' +import { useCurrentStore, useCurrentMeta, useCurrentRawData } from '../hooks/useCurrentDataBySession' +import useCreation from 'ahooks/lib/useCreation' +import useMemoizedFn from 'ahooks/lib/useMemoizedFn' +import { globalSessionEngine } from '../hooks/ChatMultiSessionController' /** * 只适用于任务规划的content footer下,不适用于子任务的上的继续 - * TODO -需要更新为最新的 */ export const useTaskChatExtraAction = () => { - /** TODO - 数据未对接 */ - // const { reviewInfo, chatIPCData } = useChatIPCStore() - const { onSend } = useAIAgentDispatcher() const sessionId = useCurrentSessionId() const store = useCurrentStore() + + const rawData = useCurrentRawData() const meta = useCurrentMeta() - const taskStatus = useStore(store, (state) => state.taskStatus) const execute = useStore(store, (state) => state.execute) + const currentPlanReviewToken = useStore(store, (state) => state.currentPlanReviewToken) + + const reviewInfo = useCreation(() => { + return rawData.contents.get(currentPlanReviewToken.token) + }, [currentPlanReviewToken.renderNum]) + + /** + * 停止任务后会返回结束标识,然后清空review id + * 防止hooks出现意外,UI层暂时保留该逻辑 + */ + const closeChatReview = useMemoizedFn(() => { + if (!!reviewInfo) { + globalSessionEngine.closeChatReview(sessionId, reviewInfo.chatType, reviewInfo.id) + } + }) + const sendReactCancelTask = useMemoizedFn(() => { const taskId = meta.currentTaskPlanID?.taskID if (!taskId) return @@ -50,10 +65,7 @@ export const useTaskChatExtraAction = () => { /**取消当前指定任务 */ const onStopTask = useMemoizedFn(() => { sendReactCancelTask() - /** TODO - 可以删除该代码,原因:停止任务后会返回结束标识,然后清空review id*/ - // if (!!reviewInfo) { - // chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) - // } + closeChatReview() onSendPlayHistoryList() }) /**取消当前执行的子任务 */ @@ -66,10 +78,8 @@ export const useTaskChatExtraAction = () => { SyncID: randomString(8), } onSend({ token: sessionId, type: 'task', params: info }) - /** TODO - 目前多任务并发,出现子任务后review自动走的继续执行,不会出现review */ - // if (!!reviewInfo) { - // chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) - // } + /** 目前多任务并发,出现子任务后review自动走的继续执行,不会出现review */ + closeChatReview() setTimeout(() => { onSendPlayHistoryList() }, 500) @@ -89,11 +99,7 @@ export const useTaskChatExtraAction = () => { } onSend({ token: sessionId, type: 'task', params }) meta.currentTaskPlanID = undefined - - /** TODO - 可以删除该代码,原因:停止任务后会返回结束标识,然后清空review id,则在review弹窗中不会出现继续执行的按钮 */ - // if (!!reviewInfo) { - // chatIPCEvents.handleTaskReviewRelease((reviewInfo.data as AIReviewType).id) - // } + closeChatReview() }) const onExtraAction = useMemoizedFn((type: 'stopTask' | 'stopSubTask' | 'recover', syncID: string) => { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx deleted file mode 100644 index e17ddf62ba..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNode.tsx +++ /dev/null @@ -1,97 +0,0 @@ -// import React, { memo } from 'react' -// import { useChatUIStore, useChatContent } from './ChatContext' - -// // --- 5.1 叶子节点组件 (Item) --- -// export const ChatItem = memo(({ token }: { token: string }) => { -// // 只订阅自己节点下的 renderNum 变动 -// const renderNum = usestore((state) => state.items[token]?.renderNum) - -// const contentManager = useChatContent() -// = -// const session=usecontex -// const{rawdata}=glkoencinstance(session) - -// if (renderNum === undefined) return null - -// // 拿着 token 去数据池取真实的大段文本数据 -// const realData = contentManager.get(token) - -// return ( -//
-// {realData?.type}: -// {realData?.content} -// (RenderVersion: {renderNum}) -//
-// ) -// }) - -// // --- 5.2 容器组组件 (Group) --- -// export const ChatGroup = memo(({ token }: { token: string }) => { -// // 仅订阅 childrenTokens 数组指针的变动(即内部子项增减会刷新,某个子项打字更新时这里完全不刷) -// const childrenTokens = useChatUIStore((state) => state.groups[token]?.childrenTokens) - -// if (!childrenTokens) return null - -// return ( -//
-//
组容器 [{token}]
-// {childrenTokens.map((childToken) => ( -// -// ))} -//
-// ) -// }) - -// // --- 5.3 复杂任务树组件 (Task) --- -// export const ChatTask = memo(({ token }: { token: string }) => { -// const childrenTokens = useChatUIStore((state) => state.tasks[token]?.childrenTokens) - -// if (!childrenTokens) return null - -// return ( -//
-//

大任务卡片 [{token}]

-// -// ==list -// {childrenTokens.map((childToken) => ( -// -// ))} -//
-// ) -// }) - -// // --- 5.4 顶层异构节点分发器 --- -// const RootNodeDispatcher = memo(({ token }: { token: string }) => { -// // 只在节点刚创建判断类型或类型突变时触发,返回纯字符串,彻底隔绝下层的 Streaming 流高频刷新 -// const nodeKind = useChatUIStore((state) => { -// if (state.items[token]) return 'item' -// if (state.groups[token]) return 'group' -// if (state.tasks[token]) return 'task' -// return null -// }) - -// switch (nodeKind) { -// case 'task': -// return -// case 'group': -// return -// case 'item': -// return -// default: -// return null -// } -// }) - -// // --- 5.5 页面最外层总看板 --- -// export const ChatList = () => { -// const rootOrder = useChatUIStore((state) => state.taskchatelemnt) - -// return ( -//
-//

AI 聊天多实例面板

-// {rootOrder.map((token) => ( -// -// ))} -//
-// ) -// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx deleted file mode 100644 index 2233196909..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatNotification.tsx +++ /dev/null @@ -1,26 +0,0 @@ -// // GlobalNotification.tsx -// import React, { useContext, useEffect } from 'react' -// import { useStore } from 'zustand' -// import { message } from 'antd' -// import { SessionIdContext } from './SessionRuntime' -// import { globalSessionEngine } from './ChatMultiSessionController' - -// export const GlobalNotification = () => { -// const sessionId = useContext(SessionIdContext) -// const { store } = globalSessionEngine.ensureSession(sessionId) - -// const notification = useStore(store, (state) => state.globalNotification) - -// useEffect(() => { -// if (notification) { -// if (notification.type === 'warning') message.warning(notification.message) -// if (notification.type === 'error') message.error(notification.message) -// if (notification.type === 'info') message.info(notification.message) - -// // 显示完毕后立即清除状态,防止重复触发 -// store.getState().setGlobalNotification(null) -// } -// }, [notification, store]) - -// return null -// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx deleted file mode 100644 index d7fb1c70fe..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatReview.tsx +++ /dev/null @@ -1,48 +0,0 @@ -// // ChatReviewModal.tsx -// import React, { useContext } from 'react' -// import { useStore } from 'zustand' -// import { Modal, Button, Space } from 'antd' -// import { SessionIdContext } from './SessionRuntime' -// import { globalSessionEngine } from './ChatMultiSessionController' - -// export const ChatReviewModal = () => { -// const sessionId = useContext(SessionIdContext) -// const { store } = globalSessionEngine.ensureSession(sessionId) - -// const reviewData = useStore(store, (state) => state.reviewModalData) - -// const handleClose = () => store.getState().setReviewModalData(null) - -// const handleConfirm = (decisionValue: string) => { -// globalSessionEngine.sendMessage(sessionId, { -// action: 'review_decision', -// id: reviewData.id, -// decision: decisionValue, -// }) -// handleClose() -// } - -// if (!reviewData) return null - -// return ( -// -//

节点事件 ID: {reviewData.id}

-// -// {reviewData.selectors?.map((sel: any) => ( -// -// ))} -// -// -//
-// ) -// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx deleted file mode 100644 index 119278bfbb..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/ChatWindow.tsx +++ /dev/null @@ -1,97 +0,0 @@ -// // ChatWindow.tsx -// import React, { useContext, useState } from 'react' -// import { useStore } from 'zustand' -// import { useShallow } from 'zustand/react/shallow' -// import { SessionIdContext } from './SessionRuntime' -// import { globalSessionEngine } from './ChatMultiSessionController' -// import { useChatIPC } from './useChatIPC' -// import { ChatGroup, ChatTask, ChatItem } from './ChatNodes' -// import { ChatReviewModal } from './ChatReviewModal' - -// export const ChatWindow = () => { -// const sessionId = useContext(SessionIdContext) -// const { onStart, onSend } = useChatIPC() - -// // 订阅当前专属 Store -// const { store } = globalSessionEngine.ensureSession(sessionId) - -// // 🎯 按需精准订阅 -// const casualElements = useStore( -// store, -// useShallow((state) => state.casualElements), -// ) -// const taskElements = useStore( -// store, -// useShallow((state) => state.taskElements), -// ) -// const casualTitle = useStore(store, (state) => state.casualTitle) -// const taskStatus = useStore(store, (state) => state.taskStatus) - -// const [inputValue, setInputValue] = useState('') - -// const handleSendMessage = () => { -// if (!inputValue.trim()) return -// onSend({ text: inputValue }) -// setInputValue('') -// } - -// // 辅助渲染函数:递归解析根节点并路由给对应的组件 -// const renderNode = (node: any) => { -// const key = node.token || node.id -// if (node.type === 'group') return -// if (node.type === 'task') return -// return -// } - -// return ( -//
-// {/* 人工审批拦截弹窗 */} -// - -// {/* 头部状态栏 */} -//
-//

{casualTitle || '安全分析终端'}

-// {taskStatus.loading && ( -//
-// ⏳ 正在执行: {taskStatus.task} {taskStatus.plan ? `-> ${taskStatus.plan}` : ''} -//
-// )} -//
- -// {/* 核心双轨渲染区 */} -//
-// {/* 第一轨:自由对话流 */} -//
{casualElements.map(renderNode)}
- -// {/* 第二轨:任务规划流 (带有视觉隔离) */} -// {taskElements.length > 0 && ( -//
-//

📡 任务执行审计日志

-// {taskElements.map(renderNode)} -//
-// )} -//
- -// {/* 底部输入区 */} -//
-// -// setInputValue(e.target.value)} -// onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} -// placeholder="输入分析指令..." -// /> -// -//
-//
-// ) -// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx deleted file mode 100644 index 651cc28459..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/SessionRuntime.tsx +++ /dev/null @@ -1,5 +0,0 @@ -// // SessionRuntime.tsx -// import React, { createContext } from 'react' - -// // 初始值给空字符串,实际由外层 Tabs 组件注入 -// export const SessionIdContext = createContext('') diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx b/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx deleted file mode 100644 index 80298454f6..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/explame/layout.tsx +++ /dev/null @@ -1,50 +0,0 @@ -// ChatTabsLayout.tsx -// import React, { useState, useEffect, useCallback } from 'react' -// import { Tabs } from 'antd' -// import { globalSessionEngine } from './ChatMultiSessionController' -// import { SessionIdContext } from './SessionRuntime' -// import { ChatWindow } from './ChatWindow' -// import { GlobalNotification } from './GlobalNotification' - -// export const ChatTabsLayout = () => { -// const [activeTabId, setActiveTabId] = useState('session-1') - -// // 模拟你的多会话列表 -// const [sessions, setSessions] = useState([ -// { id: 'session-1', title: '分析会话 A' }, -// { id: 'session-2', title: '分析会话 B' }, -// ]) - -// // 💥 焦点同步:只要 Tab 切换,立刻告诉大管家,用于拦截后台弹窗 -// useEffect(() => { -// globalSessionEngine.setActiveSession(activeTabId) - -// // 切回 Tab 时,顺手清空该会话的未读小红点状态 -// const { store } = globalSessionEngine.ensureSession(activeTabId) -// store.getState().setHasUnreadAttention(false) -// }, [activeTabId]) - -// const onChange = useCallback((newActiveKey: string) => { -// setActiveTabId(newActiveKey) -// }, []) - -// return ( -//
-// ({ -// key: session.id, -// label: session.title, -// children: ( -// // 注入上下文,包裹具体的聊天界面 -// -// -// -// -// ), -// }))} -// /> -//
-// ) -// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index 877f297579..1c091317b3 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -43,7 +43,7 @@ const genStreamGroupData = ( tokens.forEach((mapKey) => { const mapValue = rawData.contents.get(mapKey) - if (mapValue) mapValue.parentGroupKey = group + if (mapValue) mapValue.parentGroupToken = group }) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useAIItemKind.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useAIItemKind.ts index c3a79a13e1..75708c373a 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useAIItemKind.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useAIItemKind.ts @@ -1,17 +1,31 @@ import { useMemoizedFn } from 'ahooks' import { useCurrentStore } from './useCurrentDataBySession' +import { ReActChatItemMeta, ReActChatGroupMeta, ReActChatTaskMeta } from './aiRender' function useAIItemKind() { const store = useCurrentStore() const getKind = useMemoizedFn((token: string) => { const state = store.getState() - if (state.items[token]) return 'item' - if (state.groups[token]) return 'group' - if (state.tasks[token]) return 'task' - return null + return getAIItemKind(state, token) }) return getKind } export default useAIItemKind + +export type AIItemKind = 'item' | 'group' | 'task' + +export function getAIItemKind( + state: { + items: Record + groups: Record + tasks: Record + }, + token: string, +): AIItemKind | null { + if (state.items[token]) return 'item' + if (state.groups[token]) return 'group' + if (state.tasks[token]) return 'task' + return null +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts deleted file mode 100644 index bec758e5c6..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts +++ /dev/null @@ -1,1125 +0,0 @@ -import type { - AIChatIPCNotifyMessage, - AIChatIPCStartParams, - AIChatSendParams, - AIFileSystemPin, - AIMessageDataProps, - AIQuestionQueues, - HistoryChatType, - PlanLoadingStatus, - TaskChatTaskInfo, - UseCasualChatEvents, - UseChatIPCEvents, - UseChatIPCParams, - UseChatIPCState, - UseHookBaseParams, - UseTaskChatEvents, -} from './type' -import type { AIAgentGrpcApi, AIEventQueryRequest, AIInputEvent, AIOutputEvent, AIStartParams } from './grpcApi' -import type { AIChatData } from '@/pages/ai-agent/type/aiChat' -import type { DeepPartial } from '@/pages/ai-agent/store/ChatDataStore' -import { AIChatQSDataTypeEnum, type AIChatQSData, type ChatListRenderType } from './aiRender' - -import { useEffect, useRef, useState } from 'react' -import { yakitNotify } from '@/utils/notification' -import { useCreation, useInterval, useMemoizedFn, useThrottleFn } from 'ahooks' -import { Uint8ArrayToString } from '@/utils/str' -import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' -import useCasualChat from './useCasualChat' -import useTaskChat from './useTaskChat' -import { AIInputEventSyncTypeEnum } from './grpcApi' -import useAIChatLog from './useAIChatLog' -import cloneDeep from 'lodash/cloneDeep' -import { - DefaultAIQuestionQueues, - DefaultMemoryList, - DefaultPlanHistoryList, - DefaultPlanLoadingStatus, -} from './defaultConstant' -import useThrottleState from '@/hook/useThrottleState' -import { aiSystemStreamStore } from '@/store/aiSystemStream' -import { grpcQueryAIEvent } from '@/pages/ai-agent/grpc' -import useAINodeLabel from './useAINodeLabel' -import { formatAIAgentSetting } from '@/pages/ai-agent/utils' -import useAIMessageData from './useAIMessageData' -import { getDomainFromAISource } from './useGetChatDataStoreKey' -import { v4 as uuidv4 } from 'uuid' -import moment from 'moment' -import { AttachedResourceKeyEnum, AttachedResourceTypeEnum } from '@/pages/ai-agent/defaultConstant' - -const { ipcRenderer } = window.require('electron') -function useChatIPC(params?: UseChatIPCParams): [UseChatIPCState, UseChatIPCEvents] - -function useChatIPC(params?: UseChatIPCParams) { - const { - autoConnect, - cacheDataStore, - setSessionChatName, - onTaskStart, - onTaskReview, - onTaskReviewExtra, - onReviewRelease, - onEnd, - onSyncIDChange, - getSetting, - onHttpFuzzRequestChange, - onGetHttpFlowFuzzStatus, - onYaklangCodeChange, - aiSource = 'ai', - } = params || {} - - const { getLabelByParams } = useAINodeLabel() - - // #region 全局公共方法集合 - /** 自由对话(ReAct)-review 信息的自动释放 */ - const handleCasualReviewRelease = useMemoizedFn((id: string) => { - onReviewRelease && onReviewRelease('casual', id) - }) - // 任务规划-review 信息的自动释放 - const handleTaskReviewRelease = useMemoizedFn((id: string) => { - onReviewRelease && onReviewRelease('task', id) - }) - - /** 消息通知提醒弹框 */ - const handleNotifyMessage = useMemoizedFn((message: AIChatIPCNotifyMessage) => { - const { NodeIdVerbose, Content } = message - const verbose = getLabelByParams(NodeIdVerbose) - yakitNotify('info', { - message: verbose, - description: Content, - }) - }) - - /** 向进行中的grpc流接口发送请求 */ - const sendRequest = useMemoizedFn((request: AIInputEvent) => { - if (!chatID.current) return - // console.log('send-ai-re-act---\n', chatID.current, request) - ipcRenderer.invoke('send-ai-re-act', chatID.current, request) - }) - - /** 获取当前会话数据集类实例 */ - const fetchChatDataStore = useMemoizedFn(() => { - return cacheDataStore - }) - // #endregion - - // #region 全局状态变量 - /** 通信的唯一标识符 */ - const chatID = useRef('') - const fetchToken = useMemoizedFn(() => { - return chatID.current - }) - - /** 启动流接口的请求参数 */ - const aiRequest = useRef() - const fetchAIRequest = useMemoizedFn(() => { - return cloneDeep(aiRequest.current) - }) - const handleResetAIRequest = useMemoizedFn(() => { - aiRequest.current = undefined - }) - - /** 建立grpc连接的初次问题数据 */ - const firstQS = useRef() - - /** ping请求的计时器 */ - const pingTimerRef = useRef | null>(null) - /** ping请求的syncID - * 初次ping请求没有id,因为初次在主进程进行的 - * 后续两次ping都有对应的syncID,在该文件进行的 - */ - const pingSyncID = useRef('') - const handlePingTimer = useMemoizedFn(() => { - if (pingTimerRef.current) clearInterval(pingTimerRef.current) - pingTimerRef.current = setInterval(() => { - pingSyncID.current = uuidv4() - sendRequest({ - IsSyncMessage: true, - SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PING, - SyncID: pingSyncID.current, - }) - }, 5000) - }) - const resetPingTimer = useMemoizedFn(() => { - if (pingTimerRef.current) clearInterval(pingTimerRef.current) - pingTimerRef.current = null - pingSyncID.current = '' - }) - - /** 获取全部聊天数据 */ - const getChatDataStore: UseHookBaseParams['getChatDataStore'] = useMemoizedFn(() => { - if (!chatID.current) return - return cacheDataStore?.get(chatID.current) - }) - - // 通信的状态 - const [execute, setExecute, getExecute] = useGetSetState(false) - // #endregion - - // #region 接口更新的(文件|文件夹)数据集合 - const [grpcFolders, setGrpcFolders] = useState([]) - const handleSetGrpcFolders = useMemoizedFn((info: AIFileSystemPin) => { - setGrpcFolders((old) => { - const isExist = old.find((item) => item.path === info.path) - if (!!isExist) return old - return [...old, info] - }) - }) - - const handleResetGrpcFile = useMemoizedFn(() => { - setGrpcFolders([]) - }) - // #endregion - - // #region grpc流里所有的runtimeIDs集合 - // http数据的run_time_id合集 - const [httpRunTimeIDs, setHttpRunTimeIDs] = useState([]) - // risk数据的run_time_id合集 - const [riskRunTimeIDs, setRiskRunTimeIDs] = useState([]) - - const handleResetRunTimeIDs = useMemoizedFn(() => { - setHttpRunTimeIDs([]) - setRiskRunTimeIDs([]) - }) - // #endregion - - // #region 问题队列相关逻辑 - // 问题队列(自由对话专属)[todo: 后续存在任务规划的问题队列后,需要放入对应的hook中进行处理和储存] - const [questionQueue, setQuestionQueue] = useState(cloneDeep(DefaultAIQuestionQueues)) - - const handleResetQuestionQueue = useMemoizedFn(() => { - setQuestionQueue(cloneDeep(DefaultAIQuestionQueues)) - }) - // #endregion - - // #region 实时记忆列表相关逻辑 - const [memoryList, setMemoryList] = useState(cloneDeep(DefaultMemoryList)) - - // #endregion - - // #region 时间线相关逻辑 - // 实时时间线 - const [reActTimelines, setReActTimelines] = useThrottleState([], { wait: 100 }) - - const handleResetReActTimelines = useMemoizedFn(() => { - setReActTimelines(() => []) - }) - // #endregion - - // #region 系统信息流展示相关逻辑 - /** 记录都存在过的系统信息uuid, 只展示最新的一条系统信息 */ - const systemEventUUID = useRef([]) - const [systemStream, setSystemStream] = useState('') - - const handleResetSystemStream = useMemoizedFn(() => { - aiSystemStreamStore.reset() - }) - // #endregion - - // #region 场景状态相关逻辑 - const [focusMode, setFocusMode] = useState('') - // #endregion - - // #region 通知消息相关逻辑 - const [notifyMessage, setNotifyMessage] = useState(null) - - // #endregion - - // #region 历史任务规划列表相关逻辑 - const [planHistoryList, setPlanHistoryList] = useState( - cloneDeep(DefaultPlanHistoryList), - ) - - // #endregion - - // #region 单次流执行时的输出展示数据 - // 日志 - const logEvents = useAIChatLog() - - // #endregion - - // #region 自由对话(ReAct)相关变量和hook - /** 当前执行问题的task_id */ - const currentCasualTaskID = useRef('') - const fetchCurrentCasualTaskID = useMemoizedFn(() => { - return currentCasualTaskID.current - }) - - /** 记录所有自由对话的子任务-taskID */ - const casualSubTaskIDs = useRef>(new Set()) - - /** 用户主动关闭当前问题的loading状态(自由对话) */ - const [cancelCasualLoading, setCancelCasualLoading] = useState(false) - - /** 自由对话(ReAct)的loading状态 */ - /** 自由对话loading状态中的显示文案 */ - const [casualTitle, setCasualTitle] = useState('') - /** 自由对话是否在进行中 */ - const [casualLoading, setCasualLoading] = useState(false) - const handleResetCasualStatus = useMemoizedFn(() => { - setCasualLoading(false) - setCasualTitle('') - return - }) - - const [casualChat, casualChatEvent] = useCasualChat({ - getChatDataStore, - getRequest: fetchAIRequest, - getCurrentCasualTaskID: fetchCurrentCasualTaskID, - onReview: onTaskReview, - onReviewRelease: handleCasualReviewRelease, - onSubTaskID: (taskID) => { - casualSubTaskIDs.current.add(taskID) - }, - pushLog: () => {}, - }) - // #endregion - - // #region 任务规划相关变量和hook - /** 任务规划对应的问题信息, 供UI使用,因为任务结束后,该变量不会清空 */ - const currentTaskPlanID = useRef() - const fetchCurrentTaskPlanID = useMemoizedFn(() => { - return currentTaskPlanID.current - }) - const resetCurrentTaskPlanID = useMemoizedFn(() => { - currentTaskPlanID.current = undefined - }) - - /** 用户主动(关闭/恢复)当前问题的loading状态(任务规划) */ - const [cancelTaskLoading, setCancelTaskLoading] = useState(false) - - /** 任务规划的loading状态 */ - const [taskStatus, setTaskStatus] = useState(cloneDeep(DefaultPlanLoadingStatus)) - const handleResetTaskStatus = useMemoizedFn(() => { - setTaskStatus(cloneDeep(DefaultPlanLoadingStatus)) - }) - - const [taskChat, taskChatEvent] = useTaskChat({ - getChatDataStore, - getRequest: fetchAIRequest, - getCurrentTaskPlanID: fetchCurrentTaskPlanID, - onReview: onTaskReview, - onReviewExtra: onTaskReviewExtra, - onReviewRelease: handleTaskReviewRelease, - sendRequest: sendRequest, - pushLog: () => {}, - }) - // #endregion - - /** 向对应列表(自由对话|任务规划)里的map设置数据 */ - const handleSetContentMap: AIMessageDataProps['setContentMap'] = useMemoizedFn((chatType, token, content) => { - if (chatType === 'reAct') { - casualChatEvent.setContentMap(token, content) - } else if (chatType === 'task') { - taskChatEvent.setContentMap(token, content) - } - }) - - // #region 历史数据的请求hook - const [requestState, requestEvents] = useAIMessageData({ - type: getDomainFromAISource(aiSource), - getChatStore: getChatDataStore, - setContentMap: handleSetContentMap, - setCasualElements: casualChatEvent.setElements, - setTaskElements: taskChatEvent.setElements, - grpcLoadMore: (request) => { - sendRequest({ - IsSyncMessage: true, - SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_HISTORY, - SyncJsonInput: JSON.stringify(request), - }) - }, - setGrpcFiles: setGrpcFolders, - setTimelines: setReActTimelines, - }) - - /** 请求更多数据加载 */ - const handleLoadMore: UseChatIPCEvents['handleLoadMoreHistory'] = useMemoizedFn((chatType: HistoryChatType) => { - if (!chatID.current) return - return requestEvents.handleLoadMore(chatID.current, chatType) - }) - /** 是否还有更多历史数据 */ - const handleHasMore: UseChatIPCEvents['handleHasMoreHistory'] = useMemoizedFn((chatType: HistoryChatType) => { - if (!chatID.current) return false - return requestEvents.handleHasMore(chatType) - }) - // #endregion - - /** 用户主动取消问题的loading状态变换 */ - const handleCancelLoadingChange = useMemoizedFn((type: ChatListRenderType, status: boolean) => { - if (type === 'reAct') { - setCancelCasualLoading(status) - } else { - setCancelTaskLoading(status) - } - }) - - // #region 问题和问题队列相关逻辑 - /** 更新问题队列状态 */ - const handleTriggerQuestionQueueRequest = useThrottleFn( - () => { - sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) - }, - { wait: 50, leading: false }, - ).run - - // 问题队列清空操作-进行通知逻辑 - const handleReActTaskCleared = useMemoizedFn((res: AIOutputEvent) => { - try { - const { Type, NodeId, NodeIdVerbose, Timestamp } = res - handleNotifyMessage({ - Type, - NodeId, - NodeIdVerbose, - Timestamp, - Content: '已清空所有任务队列数据', - }) - } catch (error) { - // push log - } - }) - // #endregion - - // #region review事件相关方法 - /** review 界面选项触发事件 */ - const onSend = useMemoizedFn(({ token, type, params, optionValue }: AIChatSendParams) => { - try { - if (!getExecute()) { - yakitNotify('warning', 'AI 未执行任务,无法发送选项') - return - } - if (!chatID.current || chatID.current !== token) { - yakitNotify('warning', '该选项非本次 AI 执行的回答选项') - return - } - - if (params.IsConfigHotpatch) { - aiRequest.current = { ...(aiRequest.current || {}), ...(params.Params || {}) } - } - if (params.IsFreeInput) { - setCasualTitle('等待回复中...') - // 如果自由对话引起了任务规划,那么自由对话其实是空闲状态 - const isCasualIdle = casualLoading && currentCasualTaskID.current === currentTaskPlanID.current?.taskID - if (!casualLoading || isCasualIdle) { - const chatID = uuidv4() - const AttachedResourceInfos = params.AttachedResourceInfo || [] - AttachedResourceInfos.push({ - Key: AttachedResourceKeyEnum.CONTEXT_PROVIDER_KEY_DEFAULT, - Type: AttachedResourceTypeEnum.USER_FREE_INPUT_UUID, - Value: chatID, - }) - params.AttachedResourceInfo = AttachedResourceInfos - const chatData: AIChatQSData = { - id: chatID, - chatType: 'reAct', - type: AIChatQSDataTypeEnum.QUESTION, - Timestamp: moment().unix(), - // data: { qs: params.FreeInput || '', setting: {} }, - data: '', - AIService: '', - AIModelName: '', - // showQS为了UI渲染方便,重新构建的字段 - extraValue: { showQS: params.FreeInput || '' }, - } - casualChatEvent.setContentMap(chatData.id, chatData) - casualChatEvent.setElements((old) => { - return [ - ...old, - { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'reAct', kind: 'task', children: [] }, - ] - }) - } - } - - switch (type) { - case 'casual': - case 'task': - const events: UseCasualChatEvents | UseTaskChatEvents = type === 'casual' ? casualChatEvent : taskChatEvent - events.handleSend({ - request: params, - optionValue, - // extraValue, - cb: () => { - sendRequest(params) - }, - }) - break - - default: - sendRequest(params) - break - } - } catch (error) {} - }) - // #endregion - - // #region 外界进行删除会话数据操作时的重置逻辑 - const delChats = useRef([]) - const onDelChats = useMemoizedFn(async (session: string[]) => { - const filterSessions = session.filter((item) => !delChats.current.includes(item)) - delChats.current.push(...filterSessions) - - let failedSessions: string[] = [] - let err: any = null - for (let item of filterSessions) { - try { - cacheDataStore?.remove(item) - } catch (error) { - failedSessions.push(item) - err = error - } - } - await requestEvents.handleDeleteSession(filterSessions) - if (failedSessions.length > 0 && !!err) { - yakitNotify('error', `删除会话(${failedSessions.join(',')})失败: ${err}`) - } - }) - // #endregion - - /** grpc接口流断开瞬间, 需要将状态相关变量进行重置 */ - const handleResetGrpcStatus = useMemoizedFn(() => { - resetPingTimer() - taskChatEvent.handleCloseGrpc() - setExecute(false) - setCasualLoading(false) - handleResetTaskStatus() - }) - - /** 流接口开始前需要重置的一些状态 */ - const handleResetBeforeStart = useMemoizedFn(() => { - // 清空自由对话相关的ID - currentCasualTaskID.current = '' - // 清空任务规划相关的ID - resetCurrentTaskPlanID() - taskChatEvent.handleResetPlanTree() - }) - - /** 重置所有数据 */ - const onReset = useMemoizedFn(() => { - chatID.current = '' - handleResetAIRequest() - setExecute(false) - handleResetGrpcFile() - handleResetRunTimeIDs() - handleResetQuestionQueue() - handleResetReActTimelines() - handleResetSystemStream() - currentCasualTaskID.current = '' - casualSubTaskIDs.current.clear() - handleResetCasualStatus() - resetCurrentTaskPlanID() - handleResetTaskStatus() - - setCancelCasualLoading(false) - setCancelTaskLoading(false) - casualChatEvent.handleResetData() - taskChatEvent.handleResetData() - - // 重置历史数据请求 - requestEvents.handleReset() - }) - - /** 建立会话连接后需要同步的数据 */ - const handleSyncDataAfterConnect = useMemoizedFn(() => { - // 获取任务规划历史任务树 - sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS }) - }) - - /** 需要轮询获取最新的数据请求 */ - const handleStartSyncDataInterval = useMemoizedFn(() => { - // 获取最新问题队列数据 - sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) - // 获取最新记忆列表数据 - sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_MEMORY_CONTEXT }) - }) - - /** 保存state类型的数据 */ - const saveStateDataOfEnd = useMemoizedFn((session: string) => { - if (delChats.current.includes(session)) { - // 该session对应的会话数据实例已被删除 - delChats.current = delChats.current.filter((item) => item !== session) - return - } - - const answer: DeepPartial = { - httpRunTimeIDs: cloneDeep(httpRunTimeIDs), - riskRunTimeIDs: cloneDeep(riskRunTimeIDs), - casualChat: cloneDeep(casualChat), - taskChat: cloneDeep(taskChat), - grpcFolders: cloneDeep(grpcFolders), - reActTimelines: cloneDeep(reActTimelines), - } - try { - cacheDataStore?.updater(session, answer) - const store = getChatDataStore() - if (!store) return - requestEvents.handleSave(session, { - casualElements: store.casualChat.elements, - taskElements: store.taskChat.elements, - casualContentMap: store.casualChat.contents, - taskContentMap: store.taskChat.contents, - }) - } catch {} - }) - - /** 获取对应session会话的流数据最新ID, 用于获取历史数据的偏移量 */ - const handleGetLatestID = useMemoizedFn(async (session: string) => { - let lastID = 0 - try { - const request: AIEventQueryRequest = { - Filter: { - SessionID: session, - }, - Pagination: { Page: 1, Limit: 1, OrderBy: 'created_at', Order: 'desc' }, - } - const { Events } = await grpcQueryAIEvent(request) - if (Events.length) lastID = Number(Events[0].ID) || 0 - } catch { - } finally { - const chatStore = getChatDataStore() - if (chatStore) chatStore.beforeID.chatID = lastID - return lastID - } - }) - - const onStart = useMemoizedFn(async (args: AIChatIPCStartParams, cb?: () => void) => { - const { token, params } = args - - if (getExecute()) { - yakitNotify('warning', 'useChatIPC AI任务正在执行中,请稍后再试!') - return - } - - const isInit = chatID.current !== token - if (isInit) { - onReset() - try { - cacheDataStore?.create(token) - } catch (error) {} - } - handleResetBeforeStart() - chatID.current = token - - /** 先设置等待文案再设置为执行中 */ - setCasualTitle('发送问题,开启会话...') - setExecute(true) - - aiRequest.current = params.Params - - const nextID = await handleGetLatestID(token) - - ipcRenderer.on(`${token}-data`, (e, res: AIOutputEvent) => { - try { - if (res.Type === 'pong') { - // 如果返回的pong没有值,但是pingSyncID有值,说明该条消息已经过期 - if (!res.SyncID && pingSyncID.current) return - // 如果返回的pong有值,但是和pingSyncID不一样,说明该条消息已经过期 - if (res.SyncID && res.SyncID !== pingSyncID.current) return - // 该条消息有效,不需要在轮询ping请求了 - resetPingTimer() - // pong类型消息是用来检测grpc连接是否成功的,不需要展示在界面上 - // 以及把需要在grpc连接后的操作进行触发的地方 - if (firstQS.current) { - sendRequest(firstQS.current) - firstQS.current = undefined - setCasualTitle('等待回复中...') - } else { - // 如果建立流时,已经初始化过,则不在进行历史初始化 - // 场景: 删除记忆库,会断开流,如果用户在当前会话继续问问题,就会建立新的流,这时候就不需要再进行一次历史数据的初始化了 - if (isInit) requestEvents.handleLoadInit(token, nextID) - } - handleSyncDataAfterConnect() - handleStartSyncDataInterval() - cb?.() - return - } - - if (res.SyncID) { - onSyncIDChange?.(res.SyncID) - } - - let ipcContent = Uint8ArrayToString(res.Content) || '' - // console.log('ipcContent', res, `------${res.TaskId}`, ipcContent) - if (res.Type === 'structured' && res.NodeId === 'recovery_history') { - const recoveryHistory = JSON.parse(ipcContent) as AIAgentGrpcApi.RecoveryHistory - const chatStore = getChatDataStore() - if (chatStore) chatStore.beforeID.chatID = recoveryHistory.next_start_id - requestEvents.handleGrpcLoadMore(recoveryHistory) - return - } - - if (res.Type === 'yak_httpflow_count') { - // 产生一条http流量数据时的通知 - // 不能在这个if里return,因为这个数据在工具卡片中还要进行计数逻辑使用 - const httpNotice = JSON.parse(ipcContent) as AIAgentGrpcApi.HTTPTrafficNotice - if (!res.IsSync) { - setHttpRunTimeIDs((old) => { - if (old.includes(httpNotice.runtime_id)) return old - return [...old, httpNotice.runtime_id] - }) - } - } - - if (res.Type === 'yak_risk_count') { - // 产生一条risk流量数据时的通知 - // 不能在这个if里return,因为这个数据在工具卡片中还要进行计数逻辑使用 - const riskNotice = JSON.parse(ipcContent) as AIAgentGrpcApi.RiskTrafficNotice - if (!res.IsSync) { - setRiskRunTimeIDs((old) => { - if (old.includes(riskNotice.runtime_id)) return old - return [...old, riskNotice.runtime_id] - }) - } - } - - if (res.Type === 'http_fuzz_request_change') { - if (res.IsSync) return - // http_fuzz_request_change - const httpFuzzRequest = JSON.parse(ipcContent) as AIAgentGrpcApi.HttpFuzzRequestChange - onHttpFuzzRequestChange?.(httpFuzzRequest) - return - } - - if (res.Type === 'http_flow_fuzz_status') { - const httpFlowFuzzStatus = JSON.parse(ipcContent) as AIAgentGrpcApi.GetHttpFlowFuzzStatus - onGetHttpFlowFuzzStatus?.(httpFlowFuzzStatus) - } - - if (res.Type === 'yaklang_code_change') { - if (res.IsSync) return - const yaklangCodeChange = JSON.parse(ipcContent) as AIAgentGrpcApi.YaklangCodeChange - onYaklangCodeChange?.(yaklangCodeChange) - return - } - - if (res.Type === 'structured' && res.NodeId === 'session_title') { - if (res.IsSync) return - // 生成会话的名称 - const nameInfo = JSON.parse(ipcContent) as { title: string } - if (nameInfo && nameInfo.title && !!setSessionChatName) setSessionChatName(chatID.current, nameInfo.title) - return - } - - if (res.Type === 'start_plan_and_execution') { - if (res.IsSync) return - /** 清空任务规划的todolist详情 */ - getChatDataStore()?.taskChat.planDetailsMap.clear() - // 触发任务规划,并传出任务规划流的标识 coordinator_id - const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution - if (startInfo.coordinator_id && currentTaskPlanID.current?.coordinatorId !== startInfo.coordinator_id) { - // 设置任务规划对应的问题ID, 并清除自由对话(ReAct)的loading状态 - currentTaskPlanID.current = { - taskID: startInfo['re-act_task'], - status: AITaskStatus.inProgress, - // 取消任务规划需要的数据id - coordinatorId: startInfo.coordinator_id, - } - // 开始任务规划后,刷新历史任务树 - sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS }) - // 任务规划的loading开始置为true - setTaskStatus(() => ({ loading: true, plan: '加载中...', task: '加载中...' })) - // 触发任务规划UI展示的回调 - onTaskStart && onTaskStart() - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - id: `${currentTaskPlanID.current.taskID}-unknown`, - chatType: 'task', - type: AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP, - } as AIChatQSData - getChatDataStore?.()?.taskChat?.contents.set(chatData.id, chatData) - taskChatEvent.setElements((old) => { - const exists = old.some((item) => item.token === chatData.id && item.type === chatData.type) - if (exists) return old - return [ - ...old, - { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task', kind: 'task', children: [] }, - ] - }) - } - /** 获取最新任务树状态 */ - sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) - /** 恢复任务规划的时候,这个指令执行成功后,在这里取消loading */ - setCancelTaskLoading(false) - return - } - if (res.Type === 'end_plan_and_execution') { - if (res.IsSync) return - // 结束任务规划,并传出任务规划流的标识 coordinator_id - const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution - if (startInfo.coordinator_id && currentTaskPlanID.current?.coordinatorId === startInfo.coordinator_id) { - taskChatEvent.handlePlanExecEnd(res) - /**先修改任务状态loading,再改变任务树的状态 */ - handleResetTaskStatus() - taskChatEvent.handleCloseGrpc() - } - return - } - - if (res.Type === 'memory_context') { - // 实时记忆列表 - const lists = JSON.parse(ipcContent) as AIAgentGrpcApi.MemoryEntryList - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) { - taskMemorys.current = lists - } else { - reactMemorys.current = lists - } - try { - const newMemoryEntryList: AIAgentGrpcApi.MemoryEntryList = { - memories: [...(taskMemorys.current.memories || []), ...(reactMemorys.current.memories || [])], - memory_pool_limit: - Number(taskMemorys.current.memory_pool_limit) + Number(reactMemorys.current.memory_pool_limit), - memory_session_id: reactMemorys.current.memory_session_id, - total_memories: Number(taskMemorys.current.total_memories) + Number(reactMemorys.current.total_memories), - total_size: Number(taskMemorys.current.total_size) + Number(reactMemorys.current.total_size), - score_overview: { - A_total: - Number(taskMemorys.current.score_overview.A_total) + - Number(reactMemorys.current.score_overview.A_total), - C_total: - Number(taskMemorys.current.score_overview.C_total) + - Number(reactMemorys.current.score_overview.C_total), - E_total: - Number(taskMemorys.current.score_overview.E_total) + - Number(reactMemorys.current.score_overview.E_total), - - O_total: - Number(taskMemorys.current.score_overview.O_total) + - Number(reactMemorys.current.score_overview.O_total), - P_total: - Number(taskMemorys.current.score_overview.P_total) + - Number(reactMemorys.current.score_overview.P_total), - R_total: - Number(taskMemorys.current.score_overview.R_total) + - Number(reactMemorys.current.score_overview.R_total), - T_total: - Number(taskMemorys.current.score_overview.T_total) + - Number(reactMemorys.current.score_overview.T_total), - }, - } - setMemoryList(newMemoryEntryList) - } catch (error) {} - - return - } - - if (['filesystem_pin_directory', 'filesystem_pin_filename'].includes(res.Type)) { - if (res.IsSync) return - // 会话在本地缓存数据的(文件夹/文件)路径-更新就通知[不区分自由对话和任务规划] - const { path } = JSON.parse(ipcContent) as AIAgentGrpcApi.FileSystemPin - handleSetGrpcFolders({ path, isFolder: res.Type === 'filesystem_pin_directory' }) - return - } - - if (res.Type === 'structured' && res.NodeId === 'react_task_enqueue') { - if (res.IsSync) return - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) return - handleTriggerQuestionQueueRequest() - } - if (res.Type === 'structured' && res.NodeId === 'react_task_dequeue') { - // 自由对话里的问题出队消息 - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) return - - if (!res.IsSync) { - handleTriggerQuestionQueueRequest() - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.QuestionQueueStatusChange - const newCasualTaskId = res.TaskId || data.react_task_id - currentCasualTaskID.current = newCasualTaskId - casualChatEvent.resetPlanDetailsForNewTask(newCasualTaskId) - if (data.focus_mode) { - // 记录场景状态 - handleFocusModeChange(data.focus_mode) - } else { - // 非场景状态 - handleResetFocusMode() - } - setCasualLoading(true) - } - // 不能return,因为自由对话的hook要进行问题的UI渲染逻辑处理 - } - if (res.Type === 'structured' && res.NodeId === 'react_task_cleared') { - // 自由对话里的问题队列清空消息 - if (currentTaskPlanID.current?.coordinatorId === res.CoordinatorId) return - // 问题队列清空操作 - handleReActTaskCleared(res) - return - } - - return - } catch (error) { - // push log - } - }) - ipcRenderer.on(`${token}-end`, (e, res: any) => { - // console.log("end", res) - handleResetGrpcStatus() - saveStateDataOfEnd(token) - setCasualTitle('会话已停止') - onEnd && onEnd() - if (endAfterSession.current) { - handleSwitchSessionData(endAfterSession.current) - } - - ipcRenderer.invoke('cancel-ai-re-act', token).catch(() => {}) - ipcRenderer.removeAllListeners(`${token}-data`) - ipcRenderer.removeAllListeners(`${token}-end`) - ipcRenderer.removeAllListeners(`${token}-error`) - }) - // ipcRenderer.on(`${token}-error`, (e, err: any) => { - // console.log('error', err) - // yakitNotify('error', `AI执行失败: ${err}`) - // }) - // console.log('start-ai-re-act', token, params) - - if (params.Params?.UserQuery) { - // 判断建立grpc连接时是否附带问题 - // 如有,需要剥离出来,在grpc建立成功后再执行 - - const chatID = uuidv4() - - const AttachedResourceInfos = params.AttachedResourceInfo || [] - AttachedResourceInfos.push({ - Key: AttachedResourceKeyEnum.CONTEXT_PROVIDER_KEY_DEFAULT, - Type: AttachedResourceTypeEnum.USER_FREE_INPUT_UUID, - Value: chatID, - }) - firstQS.current = { - IsFreeInput: true, - FreeInput: params.Params.UserQuery, - AttachedResourceInfo: AttachedResourceInfos, - FocusModeLoop: params.FocusModeLoop, - } - // 用户问了问题后,立即显示到UI上 - // 问题对应的re_act_task_id先由前端生成,并发送给后端 - // 后续生成re_act_task_id时,会把前端生成的uuid替换为后端生成的re_act_task_id - const chatData: AIChatQSData = { - id: chatID, - chatType: 'reAct', - type: AIChatQSDataTypeEnum.QUESTION, - Timestamp: moment().unix(), - // data: { qs: firstQS.current.FreeInput || '', setting: {} }, - data: '', - AIService: '', - AIModelName: '', - // showQS为了UI渲染方便,重新构建的字段 - extraValue: { showQS: firstQS.current.FreeInput || '' }, - } - casualChatEvent.setContentMap(chatData.id, chatData) - casualChatEvent.setElements((old) => { - return [ - ...old, - { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'reAct', kind: 'task', children: [] }, - ] - }) - } - ipcRenderer.invoke('start-ai-re-act', token, params) - - // 建立会话连接时,在主进程进行了一次ping请求 - // 如果五秒没有返回pong消息,则再次进行ping请求 - handlePingTimer() - }) - - /** - * 切换session会话的数据 - * @param isCreate - * 该参数主要识别,切换是欢迎页切换到历史会话,还是欢迎页直接新建会话的情况 - * 新建会话不触发自动连接逻辑,否则UI条用的连接逻辑直接被拦截失效 - */ - const handleSwitchSessionData = useMemoizedFn((session: string, isCreate?: boolean) => { - if (!session) { - setTimeout(() => { - setSwitchLoading(false) - }, 200) - return - } - - onReset() - - if (session === 'clear') { - setTimeout(() => { - setSwitchLoading(false) - }, 200) - endAfterSession.current = '' - chatID.current = '' - cacheDataStore?.clear() - return - } - - const chatData = cacheDataStore?.get(session) - if (chatData) { - // 后续只有当前会话的数据存放在类实例中 - // 切换会话时,会自动把类实例的数据clear掉,所以这个判断下面代码不可能执行 - chatID.current = session - setGrpcFolders(chatData.grpcFolders || []) - setHttpRunTimeIDs(chatData.httpRunTimeIDs || []) - setRiskRunTimeIDs(chatData.riskRunTimeIDs || []) - setReActTimelines(() => chatData.reActTimelines || []) - casualChatEvent.setElements(chatData.casualChat?.elements || []) - taskChatEvent.setElements(chatData.taskChat?.elements || []) - } else { - cacheDataStore?.clear() - } - endAfterSession.current = '' - setSwitchLoading(false) - if (autoConnect && getSetting && !getExecute()) { - if (isCreate) return - onStart({ - token: session, - params: { - IsStart: true, - Params: { - ...formatAIAgentSetting(getSetting()), - UserQuery: '', - TimelineSessionID: session, - CoordinatorId: '', - Sequence: 1, - }, - }, - }) - } - }) - - const [switchLoading, setSwitchLoading] = useState(false) - /** - * 标记session会话切换后,是否设置新的session - * @return clear 代表清空数据并不设置数据 - * @return session 代表清空数据并设置新session对应的数据 - */ - const endAfterSession = useRef('') - const onSwitchChat: UseChatIPCEvents['onSwitchChat'] = useMemoizedFn((session, isCreate) => { - if (!chatID.current && getExecute()) { - yakitNotify('warning', 'AI异常, 未记录session却处于执行状态, 请关闭AI页面重试!') - return - } - if (!chatID.current && !session) return - if (session && chatID.current && chatID.current === session) return - - setSwitchLoading(true) - if (getExecute()) { - endAfterSession.current = session || 'clear' - // 这里使用chatID是因为session是替换chatID的新值,所以需要先取消旧session的会话 - onClose(chatID.current) - } else { - endAfterSession.current = '' - // 直接切换数据逻辑 - handleSwitchSessionData(session || 'clear', isCreate) - } - }) - - const onClose = useMemoizedFn((token: string, option?: { tip: () => void }) => { - ipcRenderer.invoke('cancel-ai-re-act', token).catch(() => {}) - if (option?.tip) { - option.tip() - } else { - // yakitNotify("info", "useChatIPC AI 任务已取消") - } - }) - - useInterval( - () => { - handleStartSyncDataInterval() - }, - execute ? 5000 : undefined, - ) - - useEffect(() => { - return () => { - if (getExecute() && chatID.current) { - onClose(chatID.current) - } - // 多个接口流不会清空,只在页面卸载时触发清空并关闭页面 - logEvents.cancelLogsWin() - } - }, []) - - /** 清空指定变量数据 */ - const handleResetTarget = useMemoizedFn((target: 'memoryList') => {}) - - /** 用户手动创建内容的执行方法 */ - const handleUserManualIntervention = useMemoizedFn((chatInfo: AIChatQSData) => { - try { - if (chatInfo.chatType === 'reAct') { - casualChatEvent.handleUserManualIntervention(chatInfo) - } - if (chatInfo.chatType === 'task') { - taskChatEvent.handleUserManualIntervention(chatInfo) - } - } catch (error) { - yakitNotify('error', `用户手动干预操作失败: ${error}`) - } - }) - - const state: UseChatIPCState = useCreation(() => { - return { - execute, - httpRunTimeIDs, - riskRunTimeIDs, - casualChat, - taskChat, - grpcFolders, - questionQueue, - reActTimelines, - memoryList, - - taskStatus, - casualTitle, - casualLoading, - - focusMode, - switchLoading, - planHistoryList, - cancelCasualLoading, - cancelTaskLoading, - notifyMessage, - requestHistoryState: requestState, - } - }, [ - execute, - httpRunTimeIDs, - riskRunTimeIDs, - casualChat, - taskChat, - grpcFolders, - questionQueue, - reActTimelines, - memoryList, - - taskStatus, - casualTitle, - casualLoading, - - focusMode, - switchLoading, - planHistoryList, - cancelCasualLoading, - cancelTaskLoading, - notifyMessage, - requestState, - ]) - - const event: UseChatIPCEvents = useCreation(() => { - return { - fetchToken, - fetchAIRequest, - fetchCurrentCasualTaskID, - fetchCurrentTaskPlanID, - fetchChatDataStore, - onSwitchChat, - onStart, - onSend, - onClose, - onReset, - handleTaskReviewRelease, - onDelChats, - handleCancelLoadingChange, - handleResetTarget, - handleUserManualIntervention, - handleLoadMoreHistory: handleLoadMore, - handleHasMoreHistory: handleHasMore, - resetCurrentTaskPlanID, - } - }, []) - - return [state, event] as const -} - -export default useChatIPC diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts index 8c341c482a..8e08bc7ee4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useGetChatDataStoreKey.ts @@ -1,15 +1,4 @@ -import { - ChatDataStore, - ChatDataStoreKey, - histroyAiStore, - FlowAiStore, - aiChatDataStore, - knowledgeBaseDataStore, - WebFuzzerAiStore, - irifyAiCodeAuditPageAiStore, - yakRunnerPageAiStore, -} from '@/pages/ai-agent/store/ChatDataStore' -import type { Domain } from '@/pages/ai-agent/store/constants' +import { ChatDataStoreKey } from '@/pages/ai-agent/store/ChatDataStore' import { AISourceEnum, type AISource } from './grpcApi' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import { useCreation } from 'ahooks' @@ -49,71 +38,5 @@ export const getImageStoreKeyByAISource = (source: AISource): ChatDataStoreKey = } } -/** - * TODO - 待验证是否还需要此方法 - */ -export const getChatDataStoreKey = (store?: ChatDataStore): ChatDataStoreKey => { - switch (store) { - case histroyAiStore: - return 'histroyAiStore' - case FlowAiStore: - return 'FlowAiStore' - case aiChatDataStore: - return 'aiChatDataStore' - case knowledgeBaseDataStore: - return 'knowledgeBaseDataStore' - case irifyAiCodeAuditPageAiStore: - return 'irifyAiCodeAuditPageAiStore' - case yakRunnerPageAiStore: - return 'yakRunnerPageAiStore' - default: - if (store instanceof WebFuzzerAiStore) return 'WebFuzzerAiStore' - return 'unknown' - } -} - -/** - * TODO - 待验证是否还需要此方法 - */ -export const getAISourceFromChatDataStoreKey = (key: ChatDataStoreKey): AISource | undefined => { - switch (key) { - case 'histroyAiStore': - return 'history' - case 'FlowAiStore': - return 'flow' - case 'aiChatDataStore': - return 'ai' - case 'knowledgeBaseDataStore': - return 'knowledgeBase' - case 'WebFuzzerAiStore': - return 'webFuzzer' - case 'irifyAiCodeAuditPageAiStore': - return 'irify' - case 'yakRunnerPageAiStore': - return 'yakRunner' - default: - return undefined - } -} - -/** - * TODO - 待验证是否还需要此方法 - */ -/** AISource 映射到 IndexedDB 消息存储域(空 source 兼容老数据,归入 ai 域;im 归入 ai 域复用) */ -export const getDomainFromAISource = (source?: AISource): Domain => { - if (!source) return 'ai' - if (source === 'im') return 'ai' - return source as Domain -} - /** AI Agent 侧栏历史会话:包含 ai、im 来源与兼容老数据的空 source */ export const AI_AGENT_HISTORY_AI_SOURCES: AISource[] = ['ai', 'im', ''] - -/** - * TODO - 待验证是否还需要此方法 - */ -/** 各业务页嵌入历史会话:仅查询对应单一 source */ -export const getAISourceListFromChatDataStoreKey = (key: ChatDataStoreKey): AISource[] => { - const source = getAISourceFromChatDataStoreKey(key) - return source ? [source] : [] -} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts index 0608a31b2d..5f04bb27fd 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts @@ -40,12 +40,12 @@ export function useChatIPC() { globalSessionEngine.handleSendMessage(payload) }) - const onClose = useMemoizedFn((sessionId: string[]) => { + const onClose = useMemoizedFn((sessionIds: string[]) => { /** globalSessionEngine.forceCloseSession * TODO - 需要AISource且支持多个会话关闭 * TODO - sessionId传[]:清除source下的所有会话 */ - // globalSessionEngine.forceCloseSession(sessionId, params.source) + // globalSessionEngine.forceCloseSession({ sessionIds }) }) // 组件卸载时拔插头,清理闭环 From fcf32043fd4685013d17ced8cd1f67ac24f81efd Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Thu, 16 Jul 2026 17:33:54 +0800 Subject: [PATCH 036/101] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20ChatMultiSe?= =?UTF-8?q?ssionController=20=E5=A4=9A=E5=A4=84=E9=80=BB=E8=BE=91=E4=B8=8E?= =?UTF-8?q?=E8=B5=84=E6=BA=90=E6=B8=85=E7=90=86=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 修复 handleSendMessage task 分支校验条件反转(!isExist),避免阻断任务规划 review 用户交互 2. 新增 closeSessionTimers 集中清理会话所有定时器(ping/notify/cardKV/queuePolling/memoryPolling),修复 memoryPollingTimer 未清理导致的泄漏 3. 调整 handleGrpcOutputEvent 日志判别逻辑,structured 日志优先解析返回,避免重复 dispatch 4. report_finish/perception/current_task_todo_list 事件改为 Type 与 NodeId 精准匹配(&&) 5. handleSessionEnd 复用 ensureSession 返回值,避免重复调用 6. 修复 registerSessionChannel 中 source ?? 'ai' 对空串(other)不生效,改为 source || 'ai' 7. closeChatReview task 分支补充注释说明不调 deleteElementNode 的原因 --- .../hooks/ChatMultiSessionController.ts | 96 +++++++++++-------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 9412945de9..6bc36ef58f 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -34,7 +34,7 @@ const genAIAgentChatData = (): AIAgentChatData => { httpFuzzRequest: undefined, httpFlowFuzzStatus: undefined, sessionTitle: '', - memoryList: cloneDeep(DefaultMemoryList), + memoryList: DefaultMemoryList, systemStream: '', yaklangCodeChange: undefined, @@ -101,7 +101,7 @@ const genAIAgentChatData = (): AIAgentChatData => { }, casualChat: { - planDetails: cloneDeep(DefaultPlanItemDetailsData), + planDetails: DefaultPlanItemDetailsData, planDetailsMap: new Map(), }, taskChat: { @@ -172,11 +172,11 @@ export class ChatMultiSessionController { public registerSessionChannel(sessionId: string, source: AIStartParams['Source']) { this.readyChannels.add(sessionId) - if (this.pageSessionMap.has(source ?? 'ai')) { - this.pageSessionMap.get(source ?? 'ai')?.add(sessionId) + if (this.pageSessionMap.has(source || 'ai')) { + this.pageSessionMap.get(source || 'ai')?.add(sessionId) } else { const sessionSet = new Set([sessionId]) - this.pageSessionMap.set(source ?? 'ai', sessionSet) + this.pageSessionMap.set(source || 'ai', sessionSet) } } @@ -388,7 +388,7 @@ export class ChatMultiSessionController { if (params.IsInteractiveMessage && params.InteractiveId) { const isExist = store.getState().currentPlanReviewToken.token === params.InteractiveId const review = rawData.contents.get(params.InteractiveId) - if (isExist || !review) { + if (!isExist || !review) { yakitNotify('error', '未获取到 review 信息, 操作无效') return } @@ -476,13 +476,25 @@ export class ChatMultiSessionController { public handleGrpcOutputEvent(sessionId: string, res: AIOutputEvent) { try { let ipcContent = Uint8ArrayToString(res.Content) || '' - // 所有数据,均抄送一份到日志中 - aiAgentLogEmitter.dispatch({ - session: sessionId, - type: 'log', - Timestamp: res.Timestamp, - log: { level: 'log', message: ipcContent }, - }) + if (res.Type === 'structured' && ipcContent.indexOf('level') > -1 && ipcContent.indexOf('message') > -1) { + // 日志类型数据 + const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Log + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: res.Timestamp, + log: data, + }) + return + } else { + // 所有数据,均抄送一份到日志中 + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: res.Timestamp, + log: { level: 'log', message: ipcContent }, + }) + } const { store, rawData, request, meta } = this.ensureSession(sessionId) @@ -520,27 +532,11 @@ export class ChatMultiSessionController { // TODO - 调用历史数据恢复方法 } this.handleSessionStartSuccess(sessionId) - meta?.onSessionStartSuccess?.(sessionId) + meta.onSessionStartSuccess?.(sessionId) meta.onSessionStartSuccess = undefined return } - if (res.Type === 'structured') { - const obj = JSON.parse(ipcContent) || '' - - if (obj?.level) { - // 日志类型数据 - const data = obj as AIAgentGrpcApi.Log - aiAgentLogEmitter.dispatch({ - session: sessionId, - type: 'log', - Timestamp: res.Timestamp, - log: data, - }) - return - } - } - // if (res.Type === 'structured' && res.NodeId === 'recovery_history') { // const recoveryHistory = JSON.parse(ipcContent) as AIAgentGrpcApi.RecoveryHistory // const chatStore = getChatDataStore() @@ -568,19 +564,18 @@ export class ChatMultiSessionController { funcKey = res.NodeId } else if (res.Type === 'api_request_failed' && res.NodeId === 'ai_call_failure') { funcKey = res.NodeId - } else if (res.Type === 'report_finish' || res.NodeId === 'report-finish') { + } else if (res.Type === 'report_finish' && res.NodeId === 'report-finish') { funcKey = res.NodeId } else if (res.Type === 'structured' && res.NodeId === 'system') { - const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) || '' if (data && typeof data === 'object' && data?.type === 'push_task') { funcKey = 'push_task' } else if (data && typeof data === 'object' && data?.type === 'pop_task') { funcKey = 'pop_task' } - } else if (res.Type === 'perception' || res.NodeId === 'perception') { + } else if (res.Type === 'perception' && res.NodeId === 'perception') { funcKey = 'perception' - } else if (res.Type === 'current_task_todo_list_update' || res.NodeId === 'current_task_todo_list') { + } else if (res.Type === 'current_task_todo_list_update' && res.NodeId === 'current_task_todo_list') { funcKey = 'current_task_todo_list_update' } else if (res.NodeId === 'session_snapshot') { funcKey = res.NodeId @@ -658,6 +653,7 @@ export class ChatMultiSessionController { const currentReview = store.getState().currentPlanReviewToken if (!currentReview.token || currentReview.token !== reviewToken) return + // 不用调用deleteElementNode,因为能触发这个方法的地方,说明review还没有进入list列表中 rawData.contents.delete(currentReview.token) store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) } @@ -674,6 +670,28 @@ export class ChatMultiSessionController { store.getState().incrementNodeVersion(chatDetail.id, 'item') } + /** 关闭会话的所有定时器 */ + private closeSessionTimers(meta: ReturnType['meta']) { + // 取消ping请求相关逻辑 + if (meta.pingTimer) clearInterval(meta.pingTimer) + meta.pingTimer = null + meta.pingSyncID = '' + // 清除通知消息消失的定时器 + if (meta.notifyMessageTimer) clearTimeout(meta.notifyMessageTimer) + meta.notifyMessageTimer = null + // 清除插件执行卡片处理的定时器 + if (meta.cardKVPaidTimer) clearTimeout(meta.cardKVPaidTimer) + meta.cardKVPaidTimer = null + // 清除获取最新问题队列的轮询器 + if (meta.queuePollingTimer) clearInterval(meta.queuePollingTimer) + meta.queuePollingTimer = null + meta.queuePollingEmptyCount = 0 + // 清除获取最新记忆库数据的轮询器 + if (meta.memoryPollingTimer) clearInterval(meta.memoryPollingTimer) + meta.memoryPollingTimer = null + } + + // 关闭ipc通道连接 private closeIPCListeners(sessionId: string) { ipcRenderer.removeAllListeners(`${sessionId}-data`) ipcRenderer.removeAllListeners(`${sessionId}-end`) @@ -686,14 +704,12 @@ export class ChatMultiSessionController { } // 监听 session-end 事件 public handleSessionEnd(sessionId: string, res: any) { - const { store, meta } = this.ensureSession(sessionId) + const data = this.ensureSession(sessionId) + const { store, meta } = data - // 取消ping请求相关逻辑 - if (meta.pingTimer) clearInterval(meta.pingTimer) - meta.pingTimer = null - meta.pingSyncID = '' + this.closeSessionTimers(meta) // 任务规划结束后的相关逻辑 - handleTaskPlanEnd(this.ensureSession(sessionId)) + handleTaskPlanEnd(data) // 核心状态改变 store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) this.readyChannels.delete(sessionId) From b8a31a614e0daf312666ed7692b6d8a06498692d Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Thu, 16 Jul 2026 17:54:59 +0800 Subject: [PATCH 037/101] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E5=8F=91?= =?UTF-8?q?=E9=80=81=E6=B6=88=E6=81=AF=E5=90=8E=E6=B2=A1=E6=9C=89=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9Bfix:=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=B7=B2=E7=BB=8F=E5=8D=B8=E8=BD=BD=E7=9A=84=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=B2=A1=E6=9C=89=E8=A2=AB=E5=85=B3=E9=97=AD=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-agent/aiAgentChat/AIAgentChat.tsx | 152 +----------------- .../historyTaskTree/HistoryTaskTree.tsx | 1 + .../ai-agent/useContext/AIAgentContext.ts | 6 +- .../ai-re-act/aiReActChat/AIReActChat.tsx | 2 + .../AIReActChatContents.tsx | 5 +- .../pages/ai-re-act/hooks/useNewChatIPC.ts | 12 +- 6 files changed, 17 insertions(+), 161 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index ab4827b198..887aa2ba23 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -1,6 +1,6 @@ import React, { memo, useEffect, useRef, useState } from 'react' import { AIAgentChatMode, AIAgentChatProps, AIReActTaskChatReviewProps, HandleStartParams } from './type' -import { useCreation, useDebounceFn, useInViewport, useMemoizedFn, useSafeState, useUpdateEffect } from 'ahooks' +import { useCreation, useDebounceFn, useInViewport, useMemoizedFn, useSafeState } from 'ahooks' import emiter from '@/utils/eventBus/eventBus' import { AIAgentTriggerEventInfo } from '../aiAgentType' import useAIAgentStore from '../useContext/useStore' @@ -78,144 +78,18 @@ export const AIAgentChat: React.FC = memo((props) => { emiter.emit('switchAIActTab', JSON.stringify({ key: AITabsEnum.Task_Content })) }, 100) }) - - // review数据中树的数据中需要的解释和关键词工具 - // const [planReviewTreeKeywordsMap, { set: setPlanReviewTreeKeywords, reset: resetPlanReviewTreeKeywords }] = useMap< - // string, - // AIAgentGrpcApi.PlanReviewRequireExtra - // >(new Map()) - - // const [reviewInfo, setReviewInfo] = useState() - - // @deprecated - // const handleShowReview = useMemoizedFn((info: AIChatQSData) => { - // // setReviewExpand(true) - // setReviewInfo(cloneDeep(info)) - // }) - // @deprecated - // const handleShowReviewExtra = useMemoizedFn((info: AIAgentGrpcApi.PlanReviewRequireExtra) => { - // setPlanReviewTreeKeywords(info.index, info) - // }) - // @deprecated - // const handleReleaseReview = useMemoizedFn((type: ChatIPCSendType, id: string) => { - // if (!reviewInfo) return - // if ((reviewInfo.data as AIReviewType).id === id) { - // // if (!delayLoading) yakitNotify("warning", "审阅自动执行,弹框将自动关闭") - // handleStopAfterChangeState() - // } - // }) - - // @deprecated 提问结束后缓存数据 - // const handleChatingEnd = useMemoizedFn(() => { - // handleStopAfterChangeState() - // }) - - // @deprecated - // const setSessionChatName = (session: string, name: string) => { - // setActiveChat?.((prev) => { - // if (!prev) return prev - // if (prev.SessionID !== session) return prev - // return { ...prev, Title: name } - // }) - // emiter.emit( - // 'sessionData', - // JSON.stringify({ - // type: 'updateSession', - // sessionId: session, - // updates: { Title: name }, - // }), - // ) - // } - - // @deprecated - // const [syncIdInfoMap, { set: setSyncIdInfoMap, get: getSyncIdInfoMap, remove: removeSyncIdInfoMap }] = useMap< - // string, - // boolean - // >(new Map()) - - // @deprecated - // const onSyncIDChange = useMemoizedFn((syncID: string) => { - // const item = getSyncIdInfoMap(syncID) - // if (!!item) { - // removeSyncIdInfoMap(syncID) - // } - // }) - /** 等自由对话渲染出来再发送 */ const handleStart = useMemoizedFn((value: HandleStartParams) => { setTimeout(() => { aiReActChatRef.current?.handleStart(value) }) }) - // @deprecated - // const handleSendCasual = useMemoizedFn((params: AIChatIPCSendParams) => { - // handleSendInteractiveMessage(params, 'casual') - // }) - // @deprecated - // const handleSendTask = useMemoizedFn((params: AIChatIPCSendParams) => { - // handleSendInteractiveMessage(params, 'task') - // }) - // @deprecated - // const handleSend = useMemoizedFn((params: AIChatIPCSendParams) => { - // handleSendInteractiveMessage(params, '') - // }) - // /**发送 @deprecated IsInteractiveMessage 消息 */ - // const handleSendInteractiveMessage = useMemoizedFn((params: AIChatIPCSendParams, type: ChatIPCSendType) => { - // const { value, id, optionValue } = params - // if (!sessionId) return - // if (!id) return - - // const info: AIInputEvent = { - // IsInteractiveMessage: true, - // InteractiveId: id, - // InteractiveJSONInput: value, - // } - // onSend({ token: sessionId, type, params: info, optionValue }) - // handleStopAfterChangeState() - // }) - // /** @deprecated 发送 IsSyncMessage 消息 */ - // const handleSendSyncMessage = useMemoizedFn((data: AISendSyncMessageParams) => { - // if (!sessionId) return - // const { syncType, SyncJsonInput, params, syncID } = data - // const info: AIInputEvent = { - // IsSyncMessage: true, - // SyncType: syncType, - // SyncJsonInput, - // Params: params, - // SyncID: syncID || randomString(8), - // } - // info.SyncID && setSyncIdInfoMap(info.SyncID, true) - // onSend({ token: sessionId, type: '', params: info }) - // }) - - // /** - // * @deprecated 发送 IsConfigHotpatch 消息 */ - // const handleSendConfigHotpatch = useMemoizedFn((data: AISendConfigHotpatchParams) => { - // if (!sessionId) return - // const { hotpatchType, params, taskId } = data - // const info: AIInputEvent = { - // IsConfigHotpatch: true, - // HotpatchType: hotpatchType, - // Params: params, - // } - // if (!!taskId) { - // info.TaskId = taskId - // } - // onSend({ token: sessionId, type: '', params: info }) - // }) const onStop = useMemoizedFn(() => { if (execute && sessionId) { onClose([sessionId]) } }) - /** @deprecated 停止回答后的状态调整||清空Review状态 */ - // const handleStopAfterChangeState = useMemoizedFn(() => { - // // 清空review信息 - // setReviewInfo(undefined) - // resetPlanReviewTreeKeywords() - // // setReviewExpand(true) - // }) useEffect(() => { getRemoteValue(RemoteAIAgentGV.AIAgentReplaceForgeNoPrompt) @@ -275,30 +149,6 @@ export const AIAgentChat: React.FC = memo((props) => { } }, []) - useUpdateEffect(() => { - onHistoryAfter() - }, [activeChat?.SessionID]) - - /** 切换历史后的处理逻辑 - * 切换历史后,如果选中的会话没有建立链接,需要启动grpc链接 - */ - const onHistoryAfter = useMemoizedFn(() => { - if (mode === 'welcome') { - setMode('re-act') - if (!execute) { - handleStart({ - qs: '', - }) - } - } else { - if (!execute) { - aiReActChatRef.current?.handleStart({ - qs: '', - }) - } - } - }) - //#region 使用 AI-Forge 模板/Tool 相关逻辑 const [activeTool, setActiveTool] = useState() const [replaceToolShow, setReplaceToolShow] = useState(false) diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx index 1403537a38..21de0ff866 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx @@ -207,6 +207,7 @@ export const AIHistoryContinueTask: React.FC = React } else if (activeChat?.SessionID && getSetting) { const session = activeChat?.SessionID onStart({ + token: session, params: { IsStart: true, Params: { diff --git a/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts b/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts index 7157c60ef7..c12646241f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts +++ b/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts @@ -2,7 +2,6 @@ import { Dispatch, SetStateAction, createContext } from 'react' import { AIAgentSetting } from '../aiAgentType' import { AISession } from '../type/aiChat' import { AIChatIPCStartParams, AIChatSendParams } from '@/pages/ai-re-act/hooks/type' -import { AISource } from '@/pages/ai-re-act/hooks/grpcApi' import { AIAgentSettingDefault } from '../defaultConstant' export interface AIAgentContextStore { @@ -12,8 +11,7 @@ export interface AIAgentContextStore { activeChat?: AISession } -export interface UseChatIPCStartParams { - args: AIChatIPCStartParams +export interface UseChatIPCStartParams extends AIChatIPCStartParams { onSuccess?: (sessionId: string) => void } export interface AIAgentContextDispatcher { @@ -22,7 +20,7 @@ export interface AIAgentContextDispatcher { setActiveChat: Dispatch> /** 开始会话 */ - onStart: (UseChatIPCStartParams) => void + onStart: (params: UseChatIPCStartParams) => void /** 会话过程中向后端发送消息 */ onSend: (params: AIChatSendParams) => void /** 关闭指定会话 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index fdddd4afb6..125178ba3d 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -162,7 +162,9 @@ export const AIReActChat: React.FC = React.memo( mentionType: 'focusMode', mentionName: params.FocusModeLoop || '', }) + onStart({ + token: session, params, }) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index 0f5e54e060..59318da3f3 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -123,7 +123,7 @@ export const AIReActChatContents: React.FC = React.me const { activeChat } = useAIAgentStore() const store = useCurrentStore() - const casualChat = useStore(store, (state) => state.casualChat) + const casualChatElements = useStore(store, (state) => state.casualChat.elements) const chatLength = useStore(store, (state) => state.casualChat.elements.length) const casualTitle = useStore(store, (state) => state.casualTitle) const execute = useStore(store, (state) => state.execute) @@ -208,6 +208,7 @@ export const AIReActChatContents: React.FC = React.me }), [Footer, Header, Item], ) + console.log('casualChat.elements', casualChatElements) return (
= React.me scrollerRef={setScrollerRef} // firstItemIndex={firstItemIndex} atBottomStateChange={setIsAtBottomRef} - data={casualChat.elements} + data={casualChatElements} totalListHeightChanged={handleTotalListHeightChanged} itemContent={renderItem} initialTopMostItemIndex={chatLength > 1 ? chatLength - 1 : 0} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts index 5f04bb27fd..e37925643d 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts @@ -1,8 +1,9 @@ // useChatIPC.ts import { useEffect, useCallback, useRef } from 'react' import { globalSessionEngine } from './ChatMultiSessionController' -import type { AIChatIPCStartParams, AIChatSendParams } from './type' +import type { AIChatSendParams } from './type' import { useMemoizedFn } from 'ahooks' +import type { UseChatIPCStartParams } from '@/pages/ai-agent/useContext/AIAgentContext' import useCurrentSessionId from './useCurrentSessionId' const { ipcRenderer } = window.require('electron') @@ -19,7 +20,7 @@ export function useChatIPC() { // 这里一定要使用useCallback,通过依赖的sessionId变化,将上一个sessionId的on监听事件进行闭包 // 多个sessionId就会生成多个自己的监听闭包,从而实现多会话的独立监听 const onStart = useCallback( - (args: AIChatIPCStartParams, onSuccess?: (sessionId: string) => void) => { + ({ token, params, onSuccess }: UseChatIPCStartParams) => { // 监听网络,直接丢给大脑处理 ipcRenderer.on(`${sessionId}-data`, (e, res: any) => { globalSessionEngine.handleGrpcOutputEvent(sessionId, res) @@ -31,7 +32,8 @@ export function useChatIPC() { globalSessionEngine.handleSessionEnd(sessionId, res) }) - globalSessionEngine.handleStartSession(args, onSuccess) + console.log('onStart----------args', { sessionId, token, params }) + globalSessionEngine.handleStartSession({ token, params }, onSuccess) }, [sessionId], ) @@ -51,7 +53,9 @@ export function useChatIPC() { // 组件卸载时拔插头,清理闭环 useEffect(() => { return () => { - // globalSessionEngine.forceCloseSession(currentSessionIdRef.current) + globalSessionEngine.forceCloseSession({ + sessionIds: [currentSessionIdRef.current], + }) } }, []) From bd60f2d329b07e221b4a0899c2694aac5921b20f Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Fri, 17 Jul 2026 17:39:48 +0800 Subject: [PATCH 038/101] =?UTF-8?q?fix:aiagent=E9=A1=B6=E9=83=A8=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E9=97=AE=E9=A2=98;fix:=E4=BF=AE=E5=A4=8D=E8=87=AA?= =?UTF-8?q?=E7=94=B1=E5=AF=B9=E8=AF=9D=E5=A4=B4=E9=83=A8=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E8=AF=A6=E6=83=85=E7=9A=84taskid=E9=97=AE=E9=A2=98=EF=BC=9Bfix?= =?UTF-8?q?:useChatIPC=E4=B8=ADseesionid=E4=B8=BA=E7=A9=BA=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aiChatContent/AIChatContent.module.scss | 59 +++++++++++++++++++ .../AIReActChatHeader.module.scss | 46 --------------- .../aiReActChatHeader/AIReActChatHeader.tsx | 6 +- .../AIReActChatContents.tsx | 3 +- .../pages/ai-re-act/hooks/useNewChatIPC.ts | 12 ++-- 5 files changed, 70 insertions(+), 56 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.module.scss b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.module.scss index 353f052d41..283e7fc64d 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.module.scss @@ -344,3 +344,62 @@ } } } + +.info-token { + display: flex; + align-items: center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-weight: 400; + .token { + color: var(--Colors-Use-Neutral-Text-4-Help-text); + font-size: 12px; + font-weight: 400; + line-height: 16px; + } +} + +.echarts-wrapper { + @extend %display-flex-center; + gap: 4px; + color: var(--Colors-Use-Neutral-Text-4-Help-text); + font-size: 12px; + font-weight: 400; + line-height: 16px; + + .title { + @extend %display-flex-center; + gap: 4px; + .text { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + word-break: break-all; + } + + .tip { + color: var(--Colors-Use-Neutral-Text-4-Help-text); + line-height: 16px; + letter-spacing: 0.5px; + } + } + + .intelligent { + color: var(--Colors-Use-Purple-Primary); + } + .intelligent-height { + color: var(--Colors-Use-Error-Primary); + } + .lightweight { + color: var(--Colors-Use-Warning-Primary); + } + .lightweight-height { + color: var(--Colors-Use-Error-Primary); + } +} +.divider-style { + width: 1px; + height: 12px; + border-left: 1px solid var(--Colors-Use-Neutral-Border); +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss index 095aa72d70..6a56be2ccd 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.module.scss @@ -49,52 +49,6 @@ var(--Colors-Use-Basic-Background) 12px, var(--Colors-Use-Basic-Background) ); - .info-token { - display: flex; - align-items: center; - gap: 4px; - color: var(--Colors-Use-Neutral-Text-1-Title); - font-size: 12px; - font-weight: 400; - } - .token-tag { - height: 16px; - padding: 2px 6px; - display: flex; - align-items: center; - gap: 4px; - border-radius: 2px; - svg { - width: 12px; - height: 12px; - } - } - .upload-token { - background: var(--Colors-Use-Error-Bg-Hover); - color: var(--Colors-Use-Error-Primary); - svg { - color: var(--Colors-Use-Error-Primary); - } - } - .download-token { - background: var(--Colors-Use-Neutral-Bg-Hover); - color: var(--Colors-Use-Neutral-Text-3-Secondary); - svg { - color: var(--Colors-Use-Neutral-Text-3-Secondary); - } - } - - .divider-style { - width: 1px; - height: 12px; - border-left: 1px solid var(--Colors-Use-Neutral-Border); - } - .info-time { - color: var(--Colors-Use-Neutral-Text-1-Title); - font-size: 12px; - font-weight: 400; - line-height: 16px; - } } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx index fe7da7720a..aeff2296c1 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx @@ -73,15 +73,15 @@ export const AIReActChatHeader: React.FC = React.memo((p const emitTaskContentTab = useMemoizedFn((type: 'add' | 'update', label?: string) => { const taskId = currentCasualTaskID const sessionId = activeChat?.SessionID - if (!taskId || !sessionId) return false - if (getSetting().Source !== AISourceEnum.aiAgent) return false + if (!currentCasualTaskID || !sessionId) return false + if (getSetting()?.Source !== 'ai') return false emiter.emit( 'actionAITaskContentTab', JSON.stringify({ type, params: { key: sessionId, - taskId, + taskId: currentCasualTaskID, label: label || activeChat?.Title || defaultTaskTabLabel, goal: '', }, diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index 59318da3f3..1a0081e7b4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -208,7 +208,8 @@ export const AIReActChatContents: React.FC = React.me }), [Footer, Header, Item], ) - console.log('casualChat.elements', casualChatElements) + // const rawData = useCurrentRawData() + // console.log('casualChat.elements', casualChatElements, store.getState().items) return (
{ // 监听网络,直接丢给大脑处理 - ipcRenderer.on(`${sessionId}-data`, (e, res: any) => { - globalSessionEngine.handleGrpcOutputEvent(sessionId, res) + ipcRenderer.on(`${token}-data`, (e, res: any) => { + globalSessionEngine.handleGrpcOutputEvent(token, res) }) - ipcRenderer.on(`${sessionId}-error`, (e, res: any) => { - globalSessionEngine.handleSessionError(sessionId, res) + ipcRenderer.on(`${token}-error`, (e, res: any) => { + globalSessionEngine.handleSessionError(token, res) }) - ipcRenderer.on(`${sessionId}-end`, (e, res: any) => { - globalSessionEngine.handleSessionEnd(sessionId, res) + ipcRenderer.on(`${token}-end`, (e, res: any) => { + globalSessionEngine.handleSessionEnd(token, res) }) console.log('onStart----------args', { sessionId, token, params }) From 7c785504cc51c1528f07f85a20f2bb4c0c5d11c2 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Mon, 20 Jul 2026 11:37:19 +0800 Subject: [PATCH 039/101] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E5=86=B2?= =?UTF-8?q?=E7=AA=81=E5=90=8E=E6=97=A7=E7=89=88=E6=8A=A5=E9=94=99=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aiRunModeSelect/AIRunModeSelect.tsx | 25 +++++++++------- .../hooks/useStreamingChatContent.ts | 12 ++++---- .../src/pages/ai-re-act/hooks/chatStore.ts | 30 ++++++++++++++++++- .../hooks/grpcStreamHandler/aiSingleItem.ts | 2 +- .../hooks/grpcStreamHandler/aiTaskDetail.ts | 2 +- .../hooks/grpcStreamHandler/aiToolResult.ts | 2 +- 6 files changed, 53 insertions(+), 20 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx b/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx index 5d10f98862..e4968f9db2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiRunModeSelect/AIRunModeSelect.tsx @@ -16,11 +16,12 @@ import styles from './AIRunModeSelect.module.scss' import { YakitInputNumber } from '@/components/yakitUI/YakitInputNumber/YakitInputNumber' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' -import { AIExecutionStrategy, AIInputEventHotPatchTypeEnum } from '@/pages/ai-re-act/hooks/grpcApi' -import useChatIPCDispatcher from '@/pages/ai-agent/useContext/ChatIPCContent/useDispatcher' -import useChatIPCStore from '@/pages/ai-agent/useContext/ChatIPCContent/useStore' +import { AIExecutionStrategy, AIInputEvent, AIInputEventHotPatchTypeEnum } from '@/pages/ai-re-act/hooks/grpcApi' import emiter from '@/utils/eventBus/eventBus' import { OutlineViewGridIcon } from '@/components/yakChat/icon' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' type ModeOptionKey = 'plan' | 'multiAgent' | 'goal' @@ -49,10 +50,10 @@ const ModeOptionList: { /** TODO - 待修改为新版 */ const AIRunModeSelect: React.FC = memo(() => { const { setting, activeChat } = useAIAgentStore() - const { setSetting } = useAIAgentDispatcher() - const { handleSendConfigHotpatch } = useChatIPCDispatcher() - const { chatIPCData } = useChatIPCStore() - const execute = useCreation(() => !!chatIPCData.execute, [chatIPCData.execute]) + const { setSetting, onSend } = useAIAgentDispatcher() + const store = useCurrentStore() + const sessionId = useCurrentSessionId() + const execute = useStore(store, (state) => state.execute) const enablePlan = useCreation(() => { return !!setting?.EnablePlan @@ -61,12 +62,14 @@ const AIRunModeSelect: React.FC = memo(() => { useMemoizedFn((checked) => { if (execute) { // ai运行才能热更新 plan - handleSendConfigHotpatch({ - hotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_EnablePlan, - params: { + const info: AIInputEvent = { + IsConfigHotpatch: true, + HotpatchType: AIInputEventHotPatchTypeEnum.HotPatchType_EnablePlan, + Params: { EnablePlan: checked, }, - }) + } + onSend({ token: sessionId, type: '', params: info }) } setSetting?.((v) => ({ ...v, diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts index 7147f444c9..4ceee68210 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react' +import { useRef, useState } from 'react' import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' import { useRafPolling } from '@/hook/useRafPolling/useRafPolling' import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' @@ -28,12 +28,14 @@ export function useStreamingChatContent(params: UseStreamingChatContentParams): const hasStartedRef = useRef(false) const [shouldType, setShouldType] = useState(false) - const getData = useCallback((): ChatStream | null => { - return rawData.contents.get(token) as ChatStream - }, [tokenRenderNum]) + // const getData = useCallback((): ChatStream | null => { + // return rawData.contents.get(token) as ChatStream + // }, [tokenRenderNum]) const { renderNumber, aiDataRef } = useRafPolling({ - getData, + getData: () => { + return rawData.contents.get(token) as ChatStream + }, interval: 300, shouldStop: (data) => { if (data.data.status === 'start') { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index b39438cd0d..b64ca13998 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -319,11 +319,39 @@ export const createChatStore = () => { } }), - /** 将指定item的token换成新token */ + /** 将指定item的token换成新token,并同步更新 elements / childrenTokens 中的引用 */ replaceItemToken: (oldToken, newToken) => set((state) => { + if (!state.items[oldToken]) return + + // 同步 items 实体 state.items[newToken] = state.items[oldToken] + state.items[newToken].token = newToken delete state.items[oldToken] + + // 同步 casualChat.elements 中的 token 引用 + for (const el of state.casualChat.elements) { + if (el.token === oldToken) el.token = newToken + } + + // 同步 taskChat.elements 中的 token 引用 + for (const el of state.taskChat.elements) { + if (el.token === oldToken) el.token = newToken + } + + // 同步 tasks[*].childrenTokens 中的 token 引用 + for (const task of Object.values(state.tasks)) { + if (task.childrenTokens.includes(oldToken)) { + task.childrenTokens = task.childrenTokens.map((t) => (t === oldToken ? newToken : t)) + } + } + + // 同步 groups[*].childrenTokens 中的 token 引用 + for (const group of Object.values(state.groups)) { + if (group.childrenTokens.includes(oldToken)) { + group.childrenTokens = group.childrenTokens.map((t) => (t === oldToken ? newToken : t)) + } + } }), })), ) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index e0d23dc350..5d2d91d952 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -342,7 +342,7 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { chatType: 'task', type: AIChatQSDataTypeEnum.TASK_NODE_GROUP, data: { - taskIndex: info.task.index, + // taskIndex: info.task.index, taskId: info.task.task_id, taskName: info.task.name, goal: info.task.goal, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts index 3890cfe544..1787d236e5 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts @@ -191,7 +191,7 @@ const handleCurrentTaskTodoListUpdate: AIMessageHandler = (requestInfo) => { const data = JSON.parse(ipcContent) as AIAgentGrpcApi.TodoListUpdate if (isEmpty(data)) return - const newData = handleTodoListData(data.items, data.task_id, data.task_index) + const newData = handleTodoListData(data.items, data.task_id) if (chatType === 'task') { const oldData = rawData.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) oldData.uuid = uuidv4() diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts index 4fc8dd5faa..f512ca1b8b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -21,7 +21,7 @@ const handleToolCallStart: AIMessageHandler = (requestInfo) => { ...cloneDeep(DefaultAIToolResult), callToolId: call_tool_id, toolName: tool?.name || '-', - verboseName: tool?.verbose_name, + verboseName: tool?.verbose_name_i18n, toolDescription: tool?.description || '', startTime: start_time || 0, startTimeMS: start_time_ms || 0, From 77c3d74649c0671c12b7832767ea767d9c7e70c7 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Mon, 20 Jul 2026 18:04:23 +0800 Subject: [PATCH 040/101] =?UTF-8?q?perf:=E6=B5=81=E6=8A=BD=E7=A6=BBWrapper?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=8C=E5=AE=9A=E6=97=B6=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=EF=BC=8C=E4=B8=8D=E9=80=9A=E8=BF=87renderNum?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=EF=BC=9B=20fix:=E4=BF=AE=E5=A4=8D=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E5=85=B6=E4=BB=96=E9=97=AE=E9=A2=98=20perf:=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E4=BB=BB=E5=8A=A1=E6=A0=91=E6=94=B9=E4=B8=BA=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E5=86=85=E9=83=A8=E8=8E=B7=E5=8F=96=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-agent/aiAgentChat/AIAgentChat.tsx | 10 +- .../main/src/pages/ai-agent/aiAgentType.d.ts | 7 +- .../chatTemplate/AIAgentChatTemplate.tsx | 56 +---- .../historyTaskTree/HistoryTaskTree.tsx | 45 +++- .../historyTaskTree/HistoryTaskTreeType.ts | 7 +- .../AITaskDefaultGroupCard.tsx | 4 +- .../ConcurrentStreamCard.tsx | 8 +- .../ai-agent/components/ToolInvokerCard.tsx | 2 +- .../StaticChatContent/StaticChatContent.tsx | 4 +- .../StreamingChatContent.tsx | 32 ++- .../hooks/useStreamingChatContent.ts | 53 +++-- .../hooks/useTypedStream.ts | 210 +++++++++++++----- .../aiGroupItem/AIGroupItem.tsx | 2 +- .../aiChatListItem/aiNodeItem/AINodeItem.tsx | 4 +- .../aiChatListItem/aiTaskItem/AITaskItem.tsx | 2 +- .../aiGroupStreamCard/AIGroupStreamCard.tsx | 45 +++- .../AIGroupStreamCardHeard.module.scss | 46 ++++ .../AIGroupStreamCardHeard.tsx | 11 +- .../components/aiGroupStreamCard/type.ts | 3 +- .../aiStreamCardWrapper.tsx | 44 ++++ .../components/aiStreamCardWrapper/type.ts | 3 + .../aiReActChatHeader/AIReActChatHeader.tsx | 1 - .../aiReActTaskChat/AIReActTaskChat.tsx | 25 +-- .../aiReActTaskChat/useTaskChatExtraAction.ts | 10 +- 24 files changed, 439 insertions(+), 195 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/aiStreamCardWrapper.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/type.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index 887aa2ba23..21601ffe39 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -554,12 +554,14 @@ export const AIReActTaskChatReview: React.FC = React const store = useCurrentStore() const rawData = useCurrentRawData() const meta = useCurrentMeta() - const currentPlanReviewToken = useStore(store, (state) => state.currentPlanReviewToken) + const currentPlanReviewTokenRenderNum = useStore(store, (state) => state.currentPlanReviewToken?.renderNum) + const currentPlanReviewToken = useStore(store, (state) => state.currentPlanReviewToken?.token) const currentPlanReviewExtraUpdate = useStore(store, (state) => state.currentPlanReviewExtraUpdate) const reviewInfo = useCreation(() => { - return rawData.contents.get(currentPlanReviewToken.token) - }, [currentPlanReviewToken.renderNum]) + const currentPlanReviewToken = store.getState().currentPlanReviewToken + return rawData.contents.get(currentPlanReviewToken?.token) + }, [currentPlanReviewToken, currentPlanReviewTokenRenderNum]) const planReviewTreeKeywordsMap = useCreation(() => { return meta.planReviewExtraData @@ -598,7 +600,7 @@ export const AIReActTaskChatReview: React.FC = React renderFooterExtra={renderFooter} expand={expand} className={styles['review-body']} - renderNum={currentPlanReviewToken.renderNum} + renderNum={currentPlanReviewTokenRenderNum} />
diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts b/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts index c1a65ac70a..f3b7633258 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts @@ -2,8 +2,7 @@ import { Dispatch, SetStateAction } from 'react' import { AISession } from './type/aiChat' import { AITabsEnum } from './defaultConstant' import { AIStartParams } from '../ai-re-act/hooks/grpcApi' -import { PlanLoadingStatus, CurrentExecTaskTree } from '../ai-re-act/hooks/type' -import type { ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' +import { PlanLoadingStatus } from '../ai-re-act/hooks/type' export interface AIAgentProps { pageId: string } @@ -44,16 +43,12 @@ export interface EditChatNameModalProps { export interface AIChatLeftSideProps { expand: boolean setExpand: Dispatch> - taskTree: CurrentExecTaskTree['task_tree'] - taskName: CurrentExecTaskTree['root_task_name'] } // 对话框回答 export type AITabsEnumType = `${AITabsEnum}` export interface AIAgentChatStreamProps { - streams: ReActChatRenderElement[] - session: string defaultExpand?: boolean scrollToBottom: boolean taskStatus: PlanLoadingStatus diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx index ceb44c3f08..52c94db562 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx @@ -6,12 +6,7 @@ import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import { grpcQueryAIToolDetails } from '../grpc' import { AIChatQSData, AIChatQSDataTypeEnum, ReActChatRenderElement } from '@/pages/ai-re-act/hooks/aiRender' -import { - AIAgentGrpcApi, - AIEventQueryRequest, - AIInputEvent, - AIInputEventSyncTypeEnum, -} from '@/pages/ai-re-act/hooks/grpcApi' +import { AIEventQueryRequest, AIInputEvent, AIInputEventSyncTypeEnum } from '@/pages/ai-re-act/hooks/grpcApi' import { taskAnswerToIconMap } from '../defaultConstant' import { AIChatListItem } from '../components/aiChatListItem/AIChatListItem' import StreamCard from '../components/StreamCard' @@ -32,7 +27,7 @@ import { YakitResizeBox, YakitResizeBoxProps } from '@/components/yakitUI/YakitR import { HistoryTaskTree } from './historyTaskTree/HistoryTaskTree' import { AIReviewParams } from '../components/aiReviewResult/AIReviewResult' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' -import { useCurrentMeta, useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' import useAIAgentDispatcher from '../useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' @@ -45,7 +40,6 @@ export enum AIChatLeft { /** @name chat-左侧侧边栏 */ export const AIChatLeftSide: React.FC = memo((props) => { - const { taskTree, taskName } = props const { t, i18nRefresh } = useI18nNamespaces(['aiAgent']) const { onSend } = useAIAgentDispatcher() @@ -53,12 +47,10 @@ export const AIChatLeftSide: React.FC = memo((props) => { const store = useCurrentStore() const rawData = useCurrentRawData() - const meta = useCurrentMeta() const taskChat = useStore(store, (state) => state.taskChat) const execute = useStore(store, (state) => state.execute) const memoryListUpdate = useStore(store, (state) => state.memoryListUpdate) - const planHistoryList = useStore(store, (state) => state.planHistoryList) const [activeTab, setActiveTab] = useState(AIChatLeft.Timeline) const [expand, setExpand] = useControllableValue(props, { @@ -96,30 +88,7 @@ export const AIChatLeftSide: React.FC = memo((props) => { const renderDom = useMemoizedFn(() => { switch (activeTab) { case AIChatLeft.TaskTree: - const coordinatorId = meta.currentTaskPlanID?.coordinatorId || '' - const currentTaskItem: AIAgentGrpcApi.PlanHistory = { - coordinator_id: coordinatorId, - created_at: '', - created_at_unix: 0, - session_id: '', - task_progress: { - total_tasks: 0, - completed_tasks: 0, - skipped_tasks: 0, - aborted_tasks: 0, - current_index: 0, - current_task_index: '', - current_task: '', - current_goal: '', - phase: 'NotCompleted', - updated_at: 0, - }, - task_tree: taskTree, - updated_at: '', - updated_at_unix: 0, - root_task_name: taskName, - } - return + return case AIChatLeft.Timeline: return default: @@ -202,16 +171,18 @@ export const AIChatLeftSide: React.FC = memo((props) => { /** @name chat-信息流展示 */ const TYPE = 'task' export const AIAgentChatStream: React.FC = memo((props) => { - const { streams, scrollToBottom, taskStatus, session } = props + const { scrollToBottom, taskStatus } = props const listRootRef = useRef(null) const [highlightedItem, setHighlightedItem] = useState<{ index: number; token: number } | null>(null) const highlightRafRef = useRef(0) const highlightObserverRef = useRef(null) + const session = useCurrentSessionId() const store = useCurrentStore() - const rawData = useCurrentRawData() - const meta = useCurrentMeta() + + const streams = useStore(store, (state) => state.taskChat.elements) + /** TODO - hooks未写 */ // const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents useUpdateEffect(() => { @@ -296,19 +267,8 @@ export const AIAgentChatStream: React.FC = memo((props) }), [Footer, Header, Item], ) - /** TODO - 需验证一下 */ const onTreeLocate = useMemoizedFn((id?: string) => { if (!id) return - // const index = streams.findIndex((item) => { - // if (item.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) return false - // const chatItem = fetchChatDataStore()?.getContentMap({ - // session, - // chatType: item.chatType, - // mapKey: item.token, - // }) - // if (!chatItem) return false - // return (chatItem.data as AITaskStartInfo).taskId === id - // }) const index = streams.findIndex((item) => item.token === id) if (index !== -1) locateToIndex(index, 'auto') }) diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx index 21de0ff866..86aa68db7e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx @@ -7,7 +7,7 @@ import { SendRecoverParams, } from './HistoryTaskTreeType' import { useCreation, useMemoizedFn, useUpdateEffect } from 'ahooks' -import { AIInputEvent, AIInputEventSyncTypeEnum, AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' +import { AIAgentGrpcApi, AIInputEvent, AIInputEventSyncTypeEnum, AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' import { AITree } from '../../aiTree/AITree' import YakitCollapse from '@/components/yakitUI/YakitCollapse/YakitCollapse' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' @@ -27,22 +27,55 @@ import { useStore } from 'zustand' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' export const HistoryTaskTree: React.FC = memo((props) => { - const { data, currentTaskItem } = props + const store = useCurrentStore() + const meta = useCurrentMeta() + const planHistoryList = useStore(store, (state) => state.planHistoryList ?? []) + const taskTree = useStore(store, (state) => state.taskChat.plan.task_tree ?? []) + const taskName = useStore(store, (state) => state.taskChat.plan.root_task_name ?? '') + + const currentTaskItem = useCreation(() => { + const coordinatorId = meta.currentTaskPlanID?.coordinatorId || '' + const item: AIAgentGrpcApi.PlanHistory = { + coordinator_id: coordinatorId, + created_at: '', + created_at_unix: 0, + session_id: '', + task_progress: { + total_tasks: 0, + completed_tasks: 0, + skipped_tasks: 0, + aborted_tasks: 0, + current_index: 0, + current_task_index: '', + current_task: '', + current_goal: '', + phase: 'NotCompleted', + updated_at: 0, + }, + task_tree: taskTree, + updated_at: '', + updated_at_unix: 0, + root_task_name: taskName, + } + return item + }, [taskTree, taskName]) const currentCoordinatorId = useCreation(() => { return currentTaskItem?.coordinator_id || '' }, [currentTaskItem?.coordinator_id]) - const [activeKey, setActiveKey] = useState(currentCoordinatorId || data.records[0]?.coordinator_id || '') + const [activeKey, setActiveKey] = useState( + currentCoordinatorId || planHistoryList.records[0]?.coordinator_id || '', + ) const historyContainerRef = useRef(null) useUpdateEffect(() => { - const firstItemId = data.records[0]?.coordinator_id || '' + const firstItemId = planHistoryList.records[0]?.coordinator_id || '' if (!!currentCoordinatorId) { setActiveKey(currentCoordinatorId) } else if (!!firstItemId) { setActiveKey(firstItemId) } - }, [currentCoordinatorId, data.records[0]]) + }, [currentCoordinatorId, planHistoryList.records[0]]) return (
= memo((props) => { /> )} - {data.records + {planHistoryList.records // 历史任务树会包含当前正在执行的任务树,需要将其过滤 .filter((ele) => ele.coordinator_id !== currentCoordinatorId) .map((item) => { diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTreeType.ts b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTreeType.ts index d0f29953dd..ba94fcf337 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTreeType.ts +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTreeType.ts @@ -1,12 +1,7 @@ import { AIAgentGrpcApi } from '@/pages/ai-re-act/hooks/grpcApi' import { AITreeNodeProps } from '../../aiTree/type' -export interface HistoryTaskTreeProps { - /** 当前任务 */ - currentTaskItem: AIAgentGrpcApi.PlanHistory - // 历史任务数据源 - data: AIAgentGrpcApi.PlanHistoryList -} +export interface HistoryTaskTreeProps {} export interface HistoryTaskTreeItemProps { // 历史任务的单条记录 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx index bdad97bc05..ae61f15ea1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx @@ -37,7 +37,7 @@ export default AITaskDefaultGroupCard const AITaskDefaultGroupCardListWrapper: FC = memo((props) => { const { token, setContentFocused } = props const store = useCurrentStore() - const childrenTokens = useStore(store, (state) => state.tasks[token].childrenTokens || []) + const childrenTokens = useStore(store, (state) => state.tasks[token]?.childrenTokens || []) return ( = memo((props) => { const { token, expandToggle, expand } = props const store = useCurrentStore() - const renderNum = useStore(store, (state) => state.tasks[token].renderNum) + const renderNum = useStore(store, (state) => state.tasks[token]?.renderNum) const rawData = useCurrentRawData() const timeStamp = useCreation(() => { if (!rawData) return 0 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index 8c10afe0c8..68ef9111c7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -24,7 +24,7 @@ const ConcurrentStreamCard: FC<{ const store = useCurrentStore() const rawData = useCurrentRawData() - const renderNum = useStore(store, (state) => state.tasks[token].renderNum) + const renderNum = useStore(store, (state) => state.tasks[token]?.renderNum) const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean( isChildWindow || rawData.contents.get(token)?.chatType !== 'reAct', @@ -86,7 +86,7 @@ const ConcurrentStreamCardHeardWrapper: FC state.tasks[token].renderNum) + const renderNum = useStore(store, (state) => state.tasks[token]?.renderNum) const raw = useCreation(() => { if (!rawData) return undefined @@ -122,7 +122,7 @@ const ConcurrentStreamCardTip: FC = memo((props) = const store = useCurrentStore() const rawData = useCurrentRawData() - const renderNum = useStore(store, (state) => state.tasks[token].renderNum) + const renderNum = useStore(store, (state) => state.tasks[token]?.renderNum) const goal = useCreation(() => { const itemData = rawData.contents.get(token) @@ -140,7 +140,7 @@ const ConcurrentStreamCardTip: FC = memo((props) = const ConcurrentStreamCardListWrapper: FC = memo((props) => { const { token } = props const store = useCurrentStore() - const childrenTokens = useStore(store, (state) => state.tasks[token].childrenTokens || []) + const childrenTokens = useStore(store, (state) => state.tasks[token]?.childrenTokens || []) return }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx index d5f1a52ee4..2caae287e4 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx @@ -155,7 +155,7 @@ const ToolStdoutCard: React.FC = memo((props) => { } }, [renderNum]) - // 获取流数据 + // TODO - 可以修改为新版 获取流数据 const { stream } = useStreamingChatContent({ token: data.stream.EventUUID, }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx index 58306ff19e..145512b4e2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx @@ -10,7 +10,7 @@ type StaticChatContentProps = { const StaticChatContent: FC = ({ token }) => { const store = useCurrentStore() - const renderNum = useStore(store, (state) => state.items[token].renderNum) + const renderNum = useStore(store, (state) => state.items[token]?.renderNum) const rawData = useCurrentRawData() const chatItem = useMemo(() => { if (!rawData) return null @@ -20,7 +20,7 @@ const StaticChatContent: FC = ({ token }) => { return { ...itemData, data: Object.assign({}, itemData.data) } as AIChatQSData } return { ...itemData } as AIChatQSData // 浅拷贝,深层数据更新引用没变,需要依赖renderNum - }, [renderNum]) + }, [token, renderNum]) if (!chatItem) return null return diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx index 2dbbf6b7e2..4650ac8ba7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx @@ -1,19 +1,39 @@ import { AIStreamNode } from '@/pages/ai-re-act/aiReActChatContents/AIReActChatContents' import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' -import { type FC } from 'react' +import { type FC, useMemo } from 'react' import { useTypedStream } from './hooks/useTypedStream' -import { useCreation } from 'ahooks' +import { useCreation, useMemoizedFn } from 'ahooks' import styles from './StreamingChatContent.module.scss' -type StreamCls = { className: string } | { aiMarkdownProps?: { className: string } } - type SingleStreamProps = { itemData: ChatStream renderNum: number } -export const AIStreamCard: FC = ({ itemData }) => { - const { stream } = useTypedStream({ token: itemData.id }) +export const AIStreamCard: FC = ({ itemData, renderNum }) => { + const getContent = useMemoizedFn(() => { + return itemData.data.content + }) + + const getStatus = useMemoizedFn(() => { + return itemData.data.status + }) + // 仅获取用于显示的 content(已应用打字效果) + const { content } = useTypedStream({ + getContent: () => getContent(), + getStatus: () => getStatus(), + }) + + const stream = useMemo(() => { + // 用打字机处理后的 content 覆盖原始 content,其余字段保持原始引用 + return { + ...itemData, + data: { + ...itemData.data, + content, + }, + } + }, [content, renderNum]) const aiStreamNodeProps = useCreation(() => { switch (itemData.chatType) { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts index 4ceee68210..ebe8953613 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useStreamingChatContent.ts @@ -1,42 +1,60 @@ -import { useRef, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' import { useRafPolling } from '@/hook/useRafPolling/useRafPolling' -import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' -import { useStore } from 'zustand' +import { useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' export interface UseStreamingChatContentParams { token: string } export interface UseStreamingChatContentResult { - /**渲染 */ - renderNumber: number - /** 流数据 */ + /** 流数据(原始引用,供需要 reference / selectors 等字段的调用方使用) */ stream: ChatStream | null /** 是否需要打字效果(经历过 start 状态) */ shouldType: boolean + /** 后端流是否已结束。结束后必须保证最终渲染的是真实完整内容,不能被打字机中间状态污染 */ + isFinished: boolean + /** 用于页面显示的最新 content(已通过轮询从 rawData 捞出,无需调用方再读 stream.data.content) */ + content: string } +/** + * 获取流式聊天内容并维护打字所需的状态语义。 + * + * 后端正常流式 chunk 不会 bump renderNum(见 aiStream.ts 的 handleStream), + * 而是直接原地累加 data.content,React 无法感知,因此这里用 useRafPolling 周期性 + * 从 rawData.contents 重新读取最新内容,直到 status === 'end' 停止轮询。 + * + * 本 hook 只负责"取数 + 状态语义化",打字平滑效果由 useStreamingTypewriter 负责。 + */ export function useStreamingChatContent(params: UseStreamingChatContentParams): UseStreamingChatContentResult { const { token } = params - const store = useCurrentStore() const rawData = useCurrentRawData() - const tokenRenderNum = useStore(store, (state) => state.items[token]?.renderNum) - const hasStartedRef = useRef(false) const [shouldType, setShouldType] = useState(false) - // const getData = useCallback((): ChatStream | null => { - // return rawData.contents.get(token) as ChatStream - // }, [tokenRenderNum]) + // 关键:后端 stream chunk 是对 rawData.contents 中的"同一个对象引用"做原地累加 + // (见 aiStream.ts 的 handleStream:streamData.data.content += content),引用永远不变。 + // 若不 clone,shouldUpdate 比较的是同一个对象,prev.data.content === next.data.content 恒为 true, + // 轮询永远检测不到内容增长 → 不会触发重渲染 → 打字机吃不到后续批次 → 平滑效果失效。 + // 这里通过 clone 拍下快照,让 shouldUpdate 按值比较 content/status 字符串,从而正确感知增量。 + const cloneStream = useCallback((chatStreamItem: ChatStream): ChatStream => { + return { + ...chatStreamItem, + data: { ...chatStreamItem.data }, + reference: chatStreamItem.reference ? [...chatStreamItem.reference] : undefined, + } + }, []) - const { renderNumber, aiDataRef } = useRafPolling({ + const { aiDataRef } = useRafPolling({ getData: () => { - return rawData.contents.get(token) as ChatStream + const item = rawData.contents.get(token) as ChatStream + return item }, interval: 300, + clone: cloneStream, shouldStop: (data) => { if (data.data.status === 'start') { hasStartedRef.current = true @@ -49,5 +67,10 @@ export function useStreamingChatContent(params: UseStreamingChatContentParams): return prev.data.content !== next.data.content || prev.data.status !== next.data.status }, }) - return { renderNumber, stream: aiDataRef, shouldType } + + const stream = aiDataRef + const content = stream?.data?.content || '' + const isFinished = stream?.data?.status === 'end' + + return { stream, shouldType, isFinished, content } } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts index a1f2a9fa32..25aba38e1f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/hooks/useTypedStream.ts @@ -1,11 +1,21 @@ // hooks/useTypedStream.ts -import { useMemo } from 'react' -import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' -import { useStreamingTypewriter } from './useStreamingTypewriter' -import { useStreamingChatContent } from './useStreamingChatContent' +import { useEffect, useRef, useState } from 'react' +import { useMemoizedFn } from 'ahooks' export interface UseTypedStreamOptions { - token: string + /** + * 实时读取最新目标内容的 getter。 + * 必须是 getter 而非字符串 prop:逻辑上 stream chunk 对 rawData.contents 中的同一对象做原地累加 + * (见 aiStream.ts 的 handleStream:itemData.data.content += chunk),引用不变、不触发重渲染。 + * 若传字符串 prop,闭包会被冻结在挂载那一刻的快照,自驱动打字链与 poll 都读不到增长 → 打字失效。 + * getter 形式(如 () => itemData.data.content)每次调用都读到当前累加后的最新长度,无需父组件重渲染。 + */ + getContent: () => string + /** + * 实时读取最新流状态的 getter('start' | 'end' | undefined)。 + * 同样必须是 getter,理由同 getContent。 + */ + getStatus: () => 'start' | 'end' | undefined /** 单步最小输出字符数(下限),默认 2 */ step?: number /** 单步最大输出字符数(上限,保证每次渲染长度不会突然过大),默认 18 */ @@ -14,72 +24,170 @@ export interface UseTypedStreamOptions { interval?: number /** * 目标排空帧数,默认 9。 - * catchUpFrames * interval ≈ 270ms,接近后端 300ms 轮询间隔, + * catchUpFrames * interval ≈ 270ms,接近逻辑数据轮询间隔, * 让每批数据连续地铺满到下一批到来,消除"卡一会→突然输出一大段"的卡顿。 */ catchUpFrames?: number } export interface UseTypedStreamResult { - /** 处理后的流数据 */ - stream: ChatStream | null + /** 用于页面显示的 content(已应用打字效果) */ + content: string /** 是否正在打字 */ isTyping: boolean } /** - * 获取流式聊天内容并应用平滑打字效果 - * - 实时流式(start → end):启用打字效果 - * - 历史记录(直接 end):禁用打字效果,直接显示 - * TODO - 优化: 看这个hooks是否需要改成只传content(显示在页面的数据)内容出去,其余的值通过useCurrentRawData获取 + * 获取流式聊天内容并应用平滑打字效果,仅返回用于显示的 content。 + * 其余原始字段(reference、NodeId、status、ContentType、selectors 等)请通过 useStreamingChatContent 获取。 + * + * 数据通道设计(关键): + * - 调用方传入 getter(getContent / getStatus),而非字符串快照。 + * - 逻辑上 stream chunk 对 rawData.contents 中的同一对象做原地累加(见 aiStream.ts 的 handleStream), + * 既不换引用也不 bump renderNum,React 感知不到 content 增长。 + * - 若传字符串 prop,本 hook 闭包会被冻结在挂载那一刻的快照,自驱动打字链与 poll 都读不到增长 → 打字失效, + * 且只有 status: start→end 时 effect 才重跑 → 命中终态对齐 → 长文一次性爆发。 + * - getter 形式每次调用都读到当前累加后的最新长度,无需父组件重渲染,零 clone、零值比较。 + * + * 行为语义: + * - 实时流式(status === 'start'):启用打字效果,逐字揭示。 + * - 历史记录(挂载时已是 'end'):跳过打字,直接展示完整内容。 + * - 流结束(status === 'end'):交给打字机按 maxStep 节奏把剩余积压逐步打完,自然收尾,杜绝一帧爆发。 + * - 挂载时数据未到位(item 不存在 / status 为 undefined):不提前退出,靠心跳探测拉起。 + * + * 追上不停摆:打字追上当前目标后,若流未结束,scheduleStep 以 interval 节奏持续心跳探测 + * rawData.contents 中原地累加(引用不变、不 bump renderNum)的新 chunk,下一拍即恢复推进。 + * 原先靠 300ms setInterval poll 拉起,导致"追上→静默最多 300ms→突然输出一大段"的卡顿;心跳把探测延迟降到 interval。 */ export function useTypedStream(options: UseTypedStreamOptions): UseTypedStreamResult { - const { token, step = 2, maxStep = 18, interval = 30, catchUpFrames = 9 } = options - - // 获取流数据和是否需要打字效果 - const { renderNumber, stream: rawStream, shouldType } = useStreamingChatContent({ token }) - - const content = rawStream?.data?.content || '' - // 后端流是否已结束。结束后必须保证最终渲染的是真实完整内容,不能被打字机中间状态污染 - const isFinished = rawStream?.data?.status === 'end' - - // 平滑流式输出:将后端大块推送的内容逐字显示 - const { displayedContent, isTyping } = useStreamingTypewriter(content, { - step, - maxStep, - interval, - catchUpFrames, - enabled: shouldType, - finished: isFinished, + const { getContent, getStatus, step = 2, maxStep = 18, interval = 30, catchUpFrames = 9 } = options + + const [displayed, setDisplayed] = useState(() => getContent()) + const [isTyping, setIsTyping] = useState(false) + + // refs:避免在自驱动定时器闭包里依赖会变的 state,消除 effect 抖动与重复订阅 + const finishedRef = useRef(false) // 流已结束 → 排空后停止 + const displayedLenRef = useRef(0) // 已显示长度(与 displayed 同步,供闭包读取) + const typingRef = useRef(false) // isTyping 的 ref 镜像,切换时才 setState + const timerRef = useRef(null) // 打字单步定时器 + + // getter 用 ref 承载:防御性同步最新闭包,使本 hook 不受父组件重渲染时机影响 + const getContentRef = useRef(getContent) + const getStatusRef = useRef(getStatus) + getContentRef.current = getContent + getStatusRef.current = getStatus + + // 配置项用 ref 承载:effect 不依赖配置,变化在下一步即生效,无需重建整个调度 + const stepRef = useRef(step) + const maxStepRef = useRef(maxStep) + const intervalRef = useRef(interval) + const catchUpFramesRef = useRef(catchUpFrames) + stepRef.current = step + maxStepRef.current = maxStep + intervalRef.current = interval + catchUpFramesRef.current = catchUpFrames + + // 实时读最新内容与状态(通过 getter 读活属性,无 clone) + const readLatest = useMemoizedFn((): { content: string; status: 'start' | 'end' | undefined } => { + return { content: getContentRef.current(), status: getStatusRef.current() } + }) + + // 仅在状态真正切换时 setState,避免无谓重渲染 + const setTyping = useMemoizedFn((v: boolean) => { + if (typingRef.current !== v) { + typingRef.current = v + setIsTyping(v) + } }) - const stream = useMemo(() => { - if (!rawStream) return null + const clearTimer = useMemoizedFn(() => { + if (timerRef.current !== null) { + window.clearTimeout(timerRef.current) + timerRef.current = null + } + }) - // 如果禁用打字效果,直接返回原始流 - if (!shouldType) return rawStream + // 单步推进:读最新目标 → 自适应步长揭示 → 未追上则继续调度,追上则按节奏心跳探测 + // 追上时不停摆:流未结束期间以 interval 节奏继续 scheduleStep 探测逻辑数据原地累加的新 chunk, + // 一旦 content.length 增长,下一拍立即恢复推进,消除"追上→静默等 poll→突然输出一大段"的卡顿。 + // finishedRef 为 true 时(流已结束)排空即彻底停,按 maxStep 节奏收尾,杜绝一帧爆发。 + const scheduleStep = useMemoizedFn(() => { + clearTimer() + timerRef.current = window.setTimeout(() => { + timerRef.current = null + const { content, status } = readLatest() + if (status === 'end') finishedRef.current = true - // 流已结束:直接使用原始完整内容与原始状态,确保不被打字机截断状态污染 - if (isFinished) { - return { - ...rawStream, - data: { - ...rawStream.data, - content, - }, + const total = content.length + const len = displayedLenRef.current + if (len >= total) { + // 已追上当前目标 + if (finishedRef.current) { + // 流已结束:彻底停 + setTyping(false) + return + } + // 流未结束:停 typing,但保持心跳探测逻辑数据新 chunk(原地累加不触发重渲染,靠这里感知) + // 探测频率为 interval(约 30ms),远低于原 pollInterval(300ms),卡顿感消除 + setTyping(false) + scheduleStep() + return } + // 自适应步长:clamp(ceil(剩余 / catchUpFrames), step, maxStep) + // 既追得上逻辑数据批次,又保证单帧不超过 maxStep,杜绝"一帧爆发一大段" + const remaining = total - len + const dynamicStep = Math.min( + maxStepRef.current, + Math.max(stepRef.current, Math.ceil(remaining / catchUpFramesRef.current)), + ) + const next = Math.min(len + dynamicStep, total) + displayedLenRef.current = next + setDisplayed(content.slice(0, next)) + setTyping(next < total) + // 自驱动:仍积压则继续,追上则交由上面分支的心跳保持节奏 + scheduleStep() + }, intervalRef.current) + }) + + // 把历史态直接对齐到完整内容(仅用于挂载即 end 的历史记录分支,不进入打字循环) + const alignToFull = useMemoizedFn((content: string) => { + if (displayedLenRef.current !== content.length) { + displayedLenRef.current = content.length + setDisplayed(content) } + setTyping(false) + }) + + useEffect(() => { + // 重置本次调度的运行态(每次 effect 重跑都从干净状态开始) + finishedRef.current = false + displayedLenRef.current = 0 + typingRef.current = false + + // 初始采样:判定本组件是否需要打字(历史记录已是 end → 跳过打字直接对齐) + const initial = readLatest() + if (initial.status === 'end') finishedRef.current = true + + // 历史记录或已结束:直接展示完整,不启用任何调度 + if (finishedRef.current) { + alignToFull(initial.content) + return + } + + // 先跑一帧(挂载即可能已有积压);打字追上后由 scheduleStep 自身心跳保持探测, + // 无需独立的 setInterval poll——心跳节奏即 interval(约 30ms),远比原 pollInterval(300ms) 贴合, + // 消除"追上→静默等 poll→突然输出一大段"的卡顿。 + scheduleStep() - return { - ...rawStream, - data: { - ...rawStream.data, - content: displayedContent, - // 如果正在打字,保持 start 状态;否则使用原始状态 - status: isTyping ? 'start' : rawStream.data.status, - }, + return () => { + clearTimer() } - }, [renderNumber, rawStream, content, displayedContent, shouldType, isTyping, isFinished]) + // 不依赖 content/status:通过 getter 实时读活属性,自驱动链 + 心跳探测跨渲染持续运行。 + // 不依赖配置项:通过 ref 承载,变化在下一步即生效。 + // readLatest/scheduleStep/alignToFull/clearTimer 均为 useMemoizedFn 稳定引用,无需列入。 + // 仅挂载时建立一次调度;token 切换由父组件卸载/重建实例承担(key 绑定 token)。 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - return { stream, isTyping } + return { content: displayed, isTyping } } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx index d9cc6733dc..2f4b4a3c5a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiGroupItem/AIGroupItem.tsx @@ -9,7 +9,7 @@ const AIGroupItem: React.FC = React.memo((props) => { const { token } = props const store = useCurrentStore() - const type = useStore(store, (state) => state.groups[token].type) + const type = useStore(store, (state) => state.groups[token]?.type) switch (type) { case AIChatQSDataTypeEnum.STREAM_GROUP: diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx index 11e53cc637..4131cedef0 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx @@ -15,8 +15,8 @@ import { AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import AiFailPlanCard from '../../aiFailPlanCard/AiFailPlanCard' import { AIModelErrorPrompt } from '../aiModelErrorPrompt/AIModelErrorPrompt' -import { AIStreamCard } from '../StreamingChatContent/StreamingChatContent' import { AIGroupStreamNode } from '../../aiGroupStreamCard/AIGroupStreamCard' +import AIStreamCardWrapper from '../../aiStreamCardWrapper/aiStreamCardWrapper' const AINodeItem: React.FC = React.memo((props) => { const { itemData, renderNum } = props @@ -90,7 +90,7 @@ const AINodeItem: React.FC = React.memo((props) => { if (!!itemData.parentGroupToken) { return } else { - return + return } default: return null diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx index b7cf9cdcfe..74a58dafcf 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx @@ -10,7 +10,7 @@ const AITaskItem: React.FC = React.memo((props) => { const { token } = props const store = useCurrentStore() - const type = useStore(store, (state) => state.groups[token].type) + const type = useStore(store, (state) => state.tasks[token].type) switch (type) { case AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP: return diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index 54428eb740..af2f0231e5 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -32,15 +32,34 @@ export const AIGroupStreamNode: FC<{ renderNum: number }> = memo(({ itemData, renderNum }) => { const { t } = useI18nNamespaces(['aiAgent']) - const { stream } = useTypedStream({ token: itemData.id }) + // 仅获取用于显示的 content(已应用打字效果) + const { content } = useTypedStream({ + getContent: () => itemData.data.content, + getStatus: () => itemData.data.status, + }) const [open, setOpen] = useState(false) const [openPopover, setOpenPopover] = useState(false) + // 其余原始字段通过 useCurrentRawData 获取,并订阅 renderNum 驱动重渲染 + const store = useCurrentStore() + const rawData = useCurrentRawData() + const tokenRenderNum = useStore(store, (state) => state.items[itemData.id]?.renderNum) + + const stream = useCreation(() => { + const rawStream = rawData.contents.get(itemData.id) + if (!rawStream) return null + switch (rawStream.type) { + case AIChatQSDataTypeEnum.STREAM: + return rawStream + + default: + return null + } + }, [tokenRenderNum]) const onClose = () => { setOpen(false) } - const store = useCurrentStore() const { getLabelByParams } = useAINodeLabel() const nodeLabel = useCreation(() => { @@ -58,7 +77,7 @@ export const AIGroupStreamNode: FC<{ if (!itemData.parentGroupToken) return '' const parentGroupTokens = store.getState().groups[itemData.parentGroupToken].childrenTokens const index = parentGroupTokens.findIndex((ele) => ele === itemData.id) - return index === -1 ? '' : `${index}. ` + return index === -1 ? '' : `${index + 1}. ` }, [renderNum]) const modelCode = useCreation(() => { @@ -89,7 +108,7 @@ export const AIGroupStreamNode: FC<{ )} {seqNo} - {stream.data.content} + {content} = memo((props) => { const { expand, token } = props const store = useCurrentStore() - const childrenTokens = useStore(store, (state) => state.groups[token].childrenTokens || []) - + const childrenTokens = useStore(store, (state) => state.groups[token]?.childrenTokens || []) return }) const AIGroupStreamCardHeardWrapper: React.FC = memo((props) => { @@ -162,7 +180,6 @@ const AIGroupStreamCardHeardWrapper: React.FC state.casualChat.elements.length) const taskChatLength = useStore(store, (state) => state.taskChat.elements.length) const renderNum = useStore(store, (state) => state.groups[token]?.renderNum) @@ -226,11 +243,23 @@ const AIGroupStreamCardHeardWrapper: React.FC { + const lastItem = rawData.contents.get(lastToken) + if (!lastItem) return undefined + switch (lastItem.type) { + case AIChatQSDataTypeEnum.STREAM: + return lastItem + + default: + return undefined + } + }, [lastItemRenderNum]) + return ( diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss index 6609074ff3..4f8a999b29 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss @@ -31,4 +31,50 @@ .hidden-expand-btn { display: none; } + .stream-text { + overflow: hidden; + text-align: left; + position: relative; + flex: 1; + width: 0; + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 14px; + line-height: 20px; + display: flex; + align-items: center; + + .ai-mask { + width: 100%; + height: 100%; + position: absolute; + top: 0; + background: linear-gradient( + to right, + var(--Colors-Use-Basic-Background) 0%, + rgba(255, 255, 255, 0) 10%, + rgba(255, 255, 255, 0) 90%, + transparent 100% + ); + } + p { + margin: 0; + max-width: 100%; + display: inline-block; + } + span { + display: flex; + white-space: nowrap; + font-weight: 400; + justify-content: flex-end; + color: var(--Colors-Use-Neutral-Text-4-Help-text); + font-size: 11px; + font-style: normal; + font-weight: 400; + line-height: 14px; + letter-spacing: 0.5px; + } + } + .stream-text-hidden { + display: none; + } } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx index 957be3bf60..91c5605c04 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx @@ -8,13 +8,16 @@ import { AIGroupStreamCardHeardProps } from '../type' import styles from './AIGroupStreamCardHeard.module.scss' const AIGroupStreamCardHeard: React.FC = memo((props) => { - const { expand, setExpand, lastToken, nodeLabel, shouldShowMask } = props + const { expand, setExpand, lastItem, nodeLabel, shouldShowMask } = props - const { stream } = useTypedStream({ token: lastToken ?? '' }) + const { content } = useTypedStream({ + getContent: () => lastItem?.data.content ?? '', + getStatus: () => lastItem?.data.status ?? 'end', + }) return (
{ setExpand(!expand) }} @@ -27,7 +30,7 @@ const AIGroupStreamCardHeard: React.FC = memo((prop [styles['stream-text-hidden']]: expand, })} > - {stream?.data?.content} + {content}

diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts index 5466c4f137..cb82c6c3fc 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts @@ -1,9 +1,10 @@ +import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' import { Dispatch, SetStateAction } from 'react' export interface AIGroupStreamCardHeardProps { expand: boolean setExpand: Dispatch> - lastToken: string + lastItem?: ChatStream nodeLabel: string shouldShowMask: boolean } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/aiStreamCardWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/aiStreamCardWrapper.tsx new file mode 100644 index 0000000000..b6614bf4c6 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/aiStreamCardWrapper.tsx @@ -0,0 +1,44 @@ +import { type FC, memo, useEffect, useState } from 'react' +import type { AIStreamCardWrapperProps } from './type' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import useRafInterval from 'ahooks/lib/useRafInterval' +import { AIStreamCard } from '../aiChatListItem/StreamingChatContent/StreamingChatContent' +import { AIChatQSDataTypeEnum, ChatStream } from '@/pages/ai-re-act/hooks/aiRender' +import { useStore } from 'zustand' +import useMemoizedFn from 'ahooks/lib/useMemoizedFn' + +const AIStreamCardWrapper: FC = memo((props) => { + const { token } = props + const [interval, setInterval] = useState(1000) + const [itemData, setItemData] = useState() + + const store = useCurrentStore() + const renderNum = useStore(store, (state) => state.items[token]?.renderNum) + const rawData = useCurrentRawData() + + useRafInterval(() => { + syncData() + }, interval) + + useEffect(() => { + syncData() + }, [token, renderNum]) + + /** TODO - 需要区分历史数据,历史数据没有打字机效果 */ + const syncData = useMemoizedFn(() => { + const item = rawData?.contents.get(token) + if (item?.type === AIChatQSDataTypeEnum.STREAM) { + setItemData(item) + if (item.data.status === 'end') { + setInterval(undefined) + } else if (item.data.status === 'start') { + setInterval(1000) + } + } + }) + + if (!itemData) return null + return +}) + +export default AIStreamCardWrapper diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/type.ts new file mode 100644 index 0000000000..7dcc443e91 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/type.ts @@ -0,0 +1,3 @@ +export interface AIStreamCardWrapperProps { + token: string +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx index aeff2296c1..a6957a62c2 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx @@ -71,7 +71,6 @@ export const AIReActChatHeader: React.FC = React.memo((p }, [title]) const emitTaskContentTab = useMemoizedFn((type: 'add' | 'update', label?: string) => { - const taskId = currentCasualTaskID const sessionId = activeChat?.SessionID if (!currentCasualTaskID || !sessionId) return false if (getSetting()?.Source !== 'ai') return false diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx index 1973648a12..db634300a4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx @@ -150,7 +150,6 @@ export default AIReActTaskChat export const AIReActTaskChatContent: React.FC = React.memo((props) => { const { scrollToBottom, onScrollToBottom } = props const { t } = useI18nNamespaces(['aiAgent']) - const { activeChat } = useAIAgentStore() const { onExtraAction } = useTaskChatExtraAction() const store = useCurrentStore() @@ -159,18 +158,12 @@ export const AIReActTaskChatContent: React.FC = Rea const execute = useStore(store, (state) => state.execute) const taskStatus = useStore(store, (state) => state.taskStatus) const currentPlanReviewToken = useStore(store, (state) => state.currentPlanReviewToken) - return ( <>
- +
- {!currentPlanReviewToken && streams.length > 0 && ( + {!currentPlanReviewToken.token && streams.length > 0 && (
{execute && ( @@ -639,16 +632,12 @@ export const AIRenderTaskFooterExtra: React.FC = R const getTaskInfo = useMemoizedFn(() => { return meta.currentTaskPlanID }) - const renderBtn = useMemoizedFn(() => { switch (getTaskInfo()?.status) { case AITaskStatus.inProgress: return ( { - store.getState().updateState({ - cancelTaskLoading: true, - }) onExtraAction('stopTask', '') }} title={t('AIRenderTaskFooterExtra.cancelTaskConfirm')} @@ -736,9 +725,6 @@ export const AIRenderTaskFooterExtra: React.FC = R }) export const AIReActTaskChatLeftSide: React.FC = React.memo((props) => { - const store = useCurrentStore() - const taskChat = useStore(store, (state) => state.taskChat) - const [leftExpand, setLeftExpand] = useControllableValue(props, { defaultValue: true, valuePropName: 'leftExpand', @@ -751,12 +737,7 @@ export const AIReActTaskChatLeftSide: React.FC = R [styles['content-left-side-hidden']]: !leftExpand, })} > - +
setLeftExpand(true)}>
任务列表
diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts index 8e585786d0..19d4f102ad 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts @@ -40,7 +40,9 @@ export const useTaskChatExtraAction = () => { const sendReactCancelTask = useMemoizedFn(() => { const taskId = meta.currentTaskPlanID?.taskID if (!taskId) return - + store.getState().updateState({ + cancelTaskLoading: true, + }) const info: AIInputEvent = { IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, @@ -69,7 +71,7 @@ export const useTaskChatExtraAction = () => { onSendPlayHistoryList() }) /**取消当前执行的子任务 */ - const onStopSubTask = useMemoizedFn((syncID: string) => { + const onStopSubTask = useMemoizedFn(() => { const info: AIInputEvent = { IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_SKIP_SUBTASK_IN_PLAN, @@ -102,13 +104,13 @@ export const useTaskChatExtraAction = () => { closeChatReview() }) - const onExtraAction = useMemoizedFn((type: 'stopTask' | 'stopSubTask' | 'recover', syncID: string) => { + const onExtraAction = useMemoizedFn((type: 'stopTask' | 'stopSubTask' | 'recover') => { switch (type) { case 'stopTask': onStopTask() break case 'stopSubTask': - onStopSubTask(syncID) + onStopSubTask() break case 'recover': onRecover() From c3409da21c7f3f557a844134c9016a30f798d178 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Tue, 21 Jul 2026 14:57:50 +0800 Subject: [PATCH 041/101] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20review=20?= =?UTF-8?q?=E6=B5=81=E5=A4=84=E7=90=86=E4=B8=8E=E4=BB=BB=E5=8A=A1=E6=A0=91?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修正 plan review 挂载 parentTaskId、review_release 守卫逻辑,并修复 push/pop task 更新 Immer 冻结任务树时报错。 --- .../hooks/grpcStreamHandler/aiReview.ts | 120 +++++++++--------- .../hooks/grpcStreamHandler/aiSingleItem.ts | 7 +- 2 files changed, 67 insertions(+), 60 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index 4e95c3a431..e82cfdc6ed 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -10,7 +10,7 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { const { res, chatType, store, rawData, request, meta } = requestInfo if (res.Type !== 'plan_review_require') return // 历史数据-grpc流数据在任务规划下无效,不处理 - if (res.IsSync && chatType === 'task') return + if ((res.IsSync && chatType === 'task') || (!res.IsSync && chatType !== 'task')) return const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequire @@ -25,6 +25,7 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { id: data.id, type: AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE, data: { ...cloneDeep(data) }, + TaskId: meta.currentTaskPlanID?.taskID ? `${meta.currentTaskPlanID.taskID}-default` : undefined, } if (res.IsSync) { // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 @@ -37,6 +38,7 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { if (target) { store.getState().dispatchStreamingNode({ chatType: chatType, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -54,13 +56,13 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { chatData.data.selected = JSON.stringify({ suggestion: 'continue' }) chatData.data.optionValue = 'continue' } - if (chatType !== 'task') return // 将数据存入hook里的缓存变量中 rawData.contents.set(chatData.id, cloneDeep(chatData)) // 该类型的实时数据只有任务规划才有 if (isAuto) { store.getState().dispatchStreamingNode({ chatType: chatType, + parentTaskId: chatData.TaskId, node: { token: chatData.id, kind: 'item', @@ -115,8 +117,12 @@ const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { } const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta, request, sendRequest } = requestInfo + const { res, chatType, request, sendRequest } = requestInfo if (res.Type !== 'task_review_require') return + // 实时数据-没有task_review类型 + if (!res.IsSync && chatType !== 'task') return + // 历史数据-task-review数据不进行展示 + if (res.IsSync) return const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.TaskReviewRequire @@ -125,58 +131,25 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { return } - const chatData: AIChatQSData = { - ...genBaseAIChatData(res), - chatType: chatType, - id: data.id, - type: AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE, - data: { ...cloneDeep(data) }, - TaskId: generateTaskNodeDataID({ - chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, - taskID: res.TaskId, - isExist: (key) => rawData.contents.has(key), - }), - } - // 历史数据-task-review数据不进行展示 - if (res.IsSync) return - // 实时数据处理逻辑 const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) // 实时数据-(自由对话|任务规划)的 review 自动执行,并且不展示在UI上 if (isAuto) return - if (chatType === 'task') { - // 任务规划下,task_review在非yolo模式时,自动执行continue操作,并且不在UI上展示操作结果 - const info: AIInputEvent = { - IsInteractiveMessage: true, - InteractiveId: chatData.id, - InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), - } - sendRequest(info) - } else { - const taskGroupDetail = rawData.contents.get(chatData.TaskId || '') - // 自由对话下,如果属于执行任务组里的task_review,在任何review模式下,后端都会自动执行continue操作,并且不在UI上展示操作结果 - // 非执行任务组的review,正常显示到UI上,根据review模式和用户主动操作,决定结果,并且操作后,也不在UI上展示结果 - if (!taskGroupDetail || taskGroupDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - rawData.contents.set(chatData.id, cloneDeep(chatData)) - store.getState().updateCasualReview(chatData.id, 'add') - store.getState().dispatchStreamingNode({ - chatType: chatType, - parentTaskId: chatData.TaskId, - node: { - token: chatData.id, - kind: 'item', - type: chatData.type, - }, - }) - } + // 任务规划下,task_review在非yolo模式时,自动执行continue操作,并且不在UI上展示操作结果 + const info: AIInputEvent = { + IsInteractiveMessage: true, + InteractiveId: data.id, + InteractiveJSONInput: JSON.stringify({ suggestion: 'continue' }), } + sendRequest(info) } const handleToolReview: AIMessageHandler = (requestInfo) => { const { res, chatType, store, rawData, meta, request, sendRequest } = requestInfo if (res.Type !== 'tool_use_review_require') return + // 历史数据-tool-review数据不进行展示 + if (res.IsSync) return const ipcContent = Uint8ArrayToString(res.Content) || '' const data = JSON.parse(ipcContent) as AIAgentGrpcApi.ToolUseReviewRequire @@ -198,8 +171,6 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { isExist: (key) => rawData.contents.has(key), }), } - // 历史数据-tool-review数据不进行展示 - if (res.IsSync) return // 实时数据处理逻辑 const isAuto = isAutoExecuteReviewContinue({ type: res.Type, getFunc: () => request }) @@ -258,6 +229,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { isExist: (key) => rawData.contents.has(key), }), } + // 历史数据 if (res.IsSync) { // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 const target = meta.historyReviewReleaseID[data.id] @@ -429,6 +401,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { const reviewDetail = rawData.contents.get(data.id) + // 历史数据 if (res.IsSync) { if (!reviewDetail) { meta.historyReviewReleaseID[data.id] = data @@ -466,8 +439,17 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { requestInfo.pushLog({ level: 'error', message: `${res.Type}数据(id:${data?.id || '-'})没有对应的review数据` }) return } + // 任务规划下没有该review + const noTaskReview = + !store.getState().currentPlanReviewToken.token || store.getState().currentPlanReviewToken.token !== reviewDetail.id + // 自由对话下没有该review + const noCasualReview = !store.getState().currentCasualReview.includes(reviewDetail.id) + + // 实时数据 switch (reviewDetail.type) { case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: + if (noTaskReview) return + reviewDetail.data.selected = JSON.stringify(data.params) reviewDetail.data.optionValue = data.params?.suggestion || 'continue' // 清空plan-review的异步拓展信息 @@ -493,19 +475,41 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { if (chatType === 'task') store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) break case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: - case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + if (chatType === 'reAct') { + if (noCasualReview) return + reviewDetail.data.selected = JSON.stringify(data.params) + reviewDetail.data.optionValue = data.params?.suggestion || 'continue' + store.getState().updateCasualReview(reviewDetail.id, 'remove') + store.getState().incrementNodeVersion(reviewDetail.id, 'item') + } + break case AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE: - reviewDetail.data.selected = JSON.stringify(data.params) - reviewDetail.data.optionValue = data.params?.suggestion || 'continue' - if (chatType === 'reAct') { + if (noTaskReview) return + reviewDetail.data.selected = JSON.stringify(data.params) + reviewDetail.data.optionValue = data.params?.suggestion || 'continue' + store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) + store.getState().dispatchStreamingNode({ + chatType: chatType, + parentTaskId: reviewDetail.TaskId, + node: { + token: reviewDetail.id, + kind: 'item', + type: reviewDetail.type, + isHistory: true, + }, + }) + } + break + case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + if (chatType === 'reAct') { + if (noCasualReview) return + reviewDetail.data.selected = JSON.stringify(data.params) + reviewDetail.data.optionValue = data.params?.suggestion || 'continue' + store.getState().updateCasualReview(reviewDetail.id, 'remove') store.getState().incrementNodeVersion(reviewDetail.id, 'item') - if (reviewDetail.type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) { - store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) - } else { - store.getState().updateCasualReview(reviewDetail.id, 'remove') - } } else { + if (noTaskReview) return store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: reviewDetail.TaskId, @@ -519,9 +523,11 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { } break case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: + // 实时数据-不会有task_review_release出现,出现则舍弃无效数据 + break case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: - // 实时数据-任务规划-所有的task_review和tool_review都会自动执行,不展示到UI上 - // 但是自由对话里,没有在执行任务组的tool_review会展示到UI上,需要处理数据并在UI上删除掉 + // 实时数据-任务规划-tool_review都会自动执行,不展示到UI上 + // 自由对话里,没有在执行任务组的tool_review会展示到UI上,需要处理数据并在UI上删除掉 rawData.contents.delete(reviewDetail.id) store.getState().updateCasualReview(reviewDetail.id, 'remove') store.getState().deleteElementNode({ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index 5d2d91d952..225abec29b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -4,6 +4,7 @@ import { Uint8ArrayToString } from '@/utils/str' import { genBaseAIChatData, generateTaskNodeDataID } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum, type ReportFinishCardData } from '../aiRender' import { convertNodeIdToVerbose } from '../defaultConstant' +import cloneDeep from 'lodash/cloneDeep' const handleThought: AIMessageHandler = (requestInfo) => { const { res, chatType, store, rawData, meta } = requestInfo @@ -322,7 +323,7 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { if (!data || typeof data !== 'object' || data?.type !== 'push_task') return const info = data as AIAgentGrpcApi.ChangeTask - const newPlanTree = store.getState().taskChat.plan + const newPlanTree = cloneDeep(store.getState().taskChat.plan) newPlanTree.task_tree = newPlanTree.task_tree.map((item) => { if (item.task_id === info.task.task_id) item.progress = AITaskStatus.inProgress return item @@ -332,7 +333,7 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { if (!meta.currentTaskPlanID?.taskID || !info.task.task_id) return const taskID = `${meta.currentTaskPlanID.taskID}-${info.task.task_id}` const chatDetail = rawData.contents.get(taskID) - if (chatDetail && chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { + if (chatDetail) { requestInfo.pushLog({ level: 'error', message: `${info.task.index}-push_task数据已存在` }) return } @@ -373,7 +374,7 @@ const handlePopTask: AIMessageHandler = (requestInfo) => { if (!data || typeof data !== 'object' || data?.type !== 'pop_task') return const info = data as AIAgentGrpcApi.ChangeTask - const newPlanTree = store.getState().taskChat.plan + const newPlanTree = cloneDeep(store.getState().taskChat.plan) newPlanTree.task_tree = newPlanTree.task_tree.map((item) => { if (item.task_id === info.task.task_id) item.progress = info.task.task_status return item From 1a5eb4c6e7b791e0a04c773d13c7e4ad49bf4849 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Tue, 21 Jul 2026 15:35:33 +0800 Subject: [PATCH 042/101] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20token=20?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E4=B8=8E=20stream=20group=20=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E5=90=8E=20UI=20=E4=B8=8D=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QUESTION 替换 token 时对齐 contents key,并在 replaceItemToken 与 stream group 合并时递增 renderNum 触发重渲染。 --- app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts | 4 +++- .../src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index b64ca13998..b58d13532c 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -193,6 +193,7 @@ export const createChatStore = () => { if (isStreamItem && lastGroup?.kind === 'group' && lastGroup.nodeId === node.nodeId) { node.groupExtra?.(lastGroup.token, [node.token]) lastGroup.childrenTokens.push(node.token) + lastGroup.renderNum += 1 return } @@ -204,7 +205,7 @@ export const createChatStore = () => { kind: 'group', token: newGroupToken, type: AIChatQSDataTypeEnum.STREAM_GROUP, - renderNum: 0, + renderNum: 1, nodeId: node.nodeId, childrenTokens: [lastToken, node.token], } @@ -327,6 +328,7 @@ export const createChatStore = () => { // 同步 items 实体 state.items[newToken] = state.items[oldToken] state.items[newToken].token = newToken + state.items[newToken].renderNum += 1 delete state.items[oldToken] // 同步 casualChat.elements 中的 token 引用 diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index c513879540..9438415eea 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -223,7 +223,7 @@ const handleReactTaskDequeue: AIMessageHandler = (request) => { const qsDetail = rawData.contents.get(data.react_task_user_input_uuid) if (qsDetail && qsDetail.type === AIChatQSDataTypeEnum.QUESTION) { rawData.contents.delete(data.react_task_user_input_uuid) - store.getState().replaceItemToken(data.react_task_user_input_uuid, data.react_task_id) + store.getState().replaceItemToken(data.react_task_user_input_uuid, chatData.id) return } } From e14cabd7e248c5765e9f48c637463bee4862753d Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Sun, 19 Jul 2026 15:08:07 +0800 Subject: [PATCH 043/101] =?UTF-8?q?=E5=B0=86=E6=97=A7=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E8=B0=83=E6=95=B4=E4=BB=A3=E7=A0=81=E6=9B=B4=E6=96=B0=E5=88=B0?= =?UTF-8?q?=E6=96=B0=E9=80=BB=E8=BE=91=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/type/aiChat.d.ts | 5 +- .../src/pages/ai-re-act/hooks/aiRender.ts | 7 +- .../hooks/grpcStreamHandler/aiOther.ts | 4 +- .../hooks/grpcStreamHandler/aiSingleItem.ts | 1 - .../hooks/grpcStreamHandler/aiTaskDetail.ts | 122 ++++++++---------- 5 files changed, 67 insertions(+), 72 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index fab5542a28..48758aecda 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -274,6 +274,9 @@ export interface AIAgentChatMetaData { /** 轮询获取最新记忆列表的定时器 */ memoryPollingTimer: NodeJS.Timeout | null - /** 记录自由对话下成组agent任务的taskID */ + /** + * 记录自由对话下成组agent任务的taskID + * 这里记录的是任务的taskID,不是任务的唯一值(问题id-任务id) + */ casualSubTaskIDs: Set } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 7b4a40feef..b3b2a2ab30 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -383,7 +383,12 @@ export interface AIChatQSDataBase { AIService: AIOutputEvent['AIService'] AIModelName: AIOutputEvent['AIModelName'] Timestamp: AIOutputEvent['Timestamp'] - /** 节点信息所属的任务节点索引 */ + /** + * 节点信息所属的任务节点索引 + * 索引结构 + * 问题: 问题id-default + * 任务: 问题id-任务id + */ TaskId?: AIOutputEvent['TaskId'] /** 前端专属数据,供前端逻辑和UI处理使用 */ extraValue?: CustomPluginExecuteFormValue | Record diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index 9438415eea..84ab06b5b7 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -329,8 +329,9 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const info = JSON.parse(ipcContent) as AIAgentGrpcApi.ReactTaskChanged + const react_task_id = info.react_task_id - if (['completed', 'aborted'].includes(info.react_task_now_status)) { + if (['completed', 'aborted', 'skipped'].includes(info.react_task_now_status)) { if (store.getState().currentCasualTaskID && store.getState().currentCasualTaskID === react_task_id) { store.getState().updateState({ focusMode: '', cancelCasualLoading: false, casualLoading: false }) } @@ -450,7 +451,6 @@ const handleReactTaskCreated: AIMessageHandler = (request) => { type: AIChatQSDataTypeEnum.TASK_NODE_GROUP, data: { taskId: info.react_task_id, - taskIndex: info.react_task_id, taskName: info.react_task_name || info.react_user_input || info.react_task_id, goal: info.react_user_input || '', status: info.react_task_status, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index 225abec29b..ae04416620 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -343,7 +343,6 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { chatType: 'task', type: AIChatQSDataTypeEnum.TASK_NODE_GROUP, data: { - // taskIndex: info.task.index, taskId: info.task.task_id, taskName: info.task.name, goal: info.task.goal, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts index 1787d236e5..49a8b66df1 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts @@ -109,39 +109,30 @@ const handleCapabilityInventory: AIMessageHandler = (requestInfo) => { itemData.forges.dynamic = dynamic.forges } + // 只能通过字段重新赋值的方式修改,不能解构赋值 + const applyCapabilityFields = (target: PlanItemDetailsData) => { + target.uuid = itemData.uuid + target.taskId = target.taskId || res.TaskId + target.tool = itemData.tool + target.forges = itemData.forges + target.skills = itemData.skills + target.plugins = itemData.plugins + target.mcp = itemData.mcp + } + if (chatType === 'task') { - // 只能通过字段重新赋值的方式修改,不能解构赋值 const oldData = rawData.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - oldData.uuid = itemData.uuid - oldData.taskId = oldData?.taskId || res.TaskId - oldData.tool = itemData.tool - oldData.forges = itemData.forges - oldData.skills = itemData.skills - oldData.plugins = itemData.plugins - oldData.mcp = itemData.mcp + applyCapabilityFields(oldData) rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) } else if (chatType === 'reAct') { const isSubAgentTask = isCasualSubAgentTask(rawData, store, res) + const chatDetail = isSubAgentTask + ? rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + : rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) + applyCapabilityFields(chatDetail) if (isSubAgentTask) { - const oldData = rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - oldData.uuid = itemData.uuid - oldData.taskId = oldData?.taskId || res.TaskId - oldData.tool = itemData.tool - oldData.forges = itemData.forges - oldData.skills = itemData.skills - oldData.plugins = itemData.plugins - oldData.mcp = itemData.mcp - rawData.casualChat.planDetailsMap.set(res.TaskId, oldData) + rawData.casualChat.planDetailsMap.set(res.TaskId, chatDetail) } else { - // 只能通过字段重新赋值的方式修改,不能解构赋值 - const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) - chatDetail.uuid = itemData.uuid - chatDetail.taskId = chatDetail?.taskId || res.TaskId - chatDetail.tool = itemData.tool - chatDetail.forges = itemData.forges - chatDetail.skills = itemData.skills - chatDetail.plugins = itemData.plugins - chatDetail.mcp = itemData.mcp rawData.casualChat.planDetails = chatDetail } } @@ -157,25 +148,25 @@ const handlePerception: AIMessageHandler = (requestInfo) => { if (isEmpty(perception)) return perception.summary = isArray(perception.summary) ? perception.summary.join(',') : perception.summary + const applyPerceptionFields = (target: PlanItemDetailsData) => { + target.uuid = uuidv4() + target.taskId = target.taskId || res.TaskId + target.perception = perception + } + if (chatType === 'task') { const oldData = rawData.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - oldData.taskId = oldData?.taskId || res.TaskId - oldData.uuid = uuidv4() - oldData.perception = perception + applyPerceptionFields(oldData) rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) } else if (chatType === 'reAct') { const isSubAgentTask = isCasualSubAgentTask(rawData, store, res) + const chatDetail = isSubAgentTask + ? rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + : rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) + applyPerceptionFields(chatDetail) if (isSubAgentTask) { - const oldData = rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - oldData.uuid = uuidv4() - oldData.taskId = oldData.taskId || res.TaskId - oldData.perception = perception - rawData.casualChat.planDetailsMap.set(res.TaskId, oldData) + rawData.casualChat.planDetailsMap.set(res.TaskId, chatDetail) } else { - const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) - chatDetail.uuid = uuidv4() - chatDetail.taskId = chatDetail.taskId || res.TaskId - chatDetail.perception = perception rawData.casualChat.planDetails = chatDetail } } @@ -192,26 +183,25 @@ const handleCurrentTaskTodoListUpdate: AIMessageHandler = (requestInfo) => { if (isEmpty(data)) return const newData = handleTodoListData(data.items, data.task_id) + const applyTodoListFields = (target: PlanItemDetailsData) => { + target.uuid = uuidv4() + target.taskId = target.taskId || res.TaskId + target.todoList = newData + } + if (chatType === 'task') { const oldData = rawData.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - oldData.uuid = uuidv4() - oldData.taskId = oldData.taskId || res.TaskId - oldData.todoList = newData + applyTodoListFields(oldData) rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) } else if (chatType === 'reAct') { - const chatDetail = rawData.casualChat?.planDetails const isSubAgentTask = isCasualSubAgentTask(rawData, store, res) + const chatDetail = isSubAgentTask + ? rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + : rawData.casualChat?.planDetails + if (!chatDetail) return + applyTodoListFields(chatDetail) if (isSubAgentTask) { - const oldData = rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - oldData.uuid = uuidv4() - oldData.taskId = oldData.taskId || res.TaskId - oldData.todoList = newData - rawData.casualChat.planDetailsMap.set(res.TaskId, oldData) - } else { - if (!chatDetail) return - chatDetail.uuid = uuidv4() - chatDetail.taskId = chatDetail.taskId || res.TaskId - chatDetail.todoList = newData + rawData.casualChat.planDetailsMap.set(res.TaskId, chatDetail) } store.getState().updateCasualTodoList() } @@ -225,28 +215,26 @@ const handleSessionSnapshot: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const snapshot = (JSON.parse(ipcContent) as AIAgentGrpcApi.SessionSnapshot) || {} if (isEmpty(snapshot)) return + const applySnapshotFields = (target: PlanItemDetailsData) => { + target.uuid = uuidv4() + target.taskId = target.taskId || res.TaskId + target.execution = snapshot.execution + target.backgroundProcesses = snapshot.background_processes + } + if (chatType === 'task') { const oldData = rawData.taskChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - oldData.taskId = oldData?.taskId || res.TaskId - oldData.uuid = uuidv4() - oldData.execution = snapshot.execution - oldData.backgroundProcesses = snapshot.background_processes + applySnapshotFields(oldData) rawData.taskChat.planDetailsMap.set(res.TaskId, oldData) } else if (chatType === 'reAct') { const isSubAgentTask = isCasualSubAgentTask(rawData, store, res) + const chatDetail = isSubAgentTask + ? rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) + : rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) + applySnapshotFields(chatDetail) if (isSubAgentTask) { - const oldData = rawData.casualChat.planDetailsMap.get(res.TaskId) || cloneDeep(DefaultPlanItemDetailsData) - oldData.uuid = uuidv4() - oldData.taskId = oldData.taskId || res.TaskId - oldData.execution = snapshot.execution - oldData.backgroundProcesses = snapshot.background_processes - rawData.casualChat.planDetailsMap.set(res.TaskId, oldData) + rawData.casualChat.planDetailsMap.set(res.TaskId, chatDetail) } else { - const chatDetail = rawData.casualChat?.planDetails || cloneDeep(DefaultPlanItemDetailsData) - chatDetail.uuid = uuidv4() - chatDetail.taskId = chatDetail.taskId || res.TaskId - chatDetail.execution = snapshot.execution - chatDetail.backgroundProcesses = snapshot.background_processes rawData.casualChat.planDetails = chatDetail } } From 51ccf54447b0932cd28badc7a7a603dc8b059d98 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 22 Jul 2026 10:35:35 +0800 Subject: [PATCH 044/101] =?UTF-8?q?perf:=E4=BB=BB=E5=8A=A1=E5=AD=90?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E6=95=B0=E6=8D=AE=E5=A4=84=E7=90=86=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=9B=E5=AD=90=E4=BB=BB=E5=8A=A1=E7=9A=84=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../openAIConcurrentStream/index.js | 5 +- .../AIConcurrentStream/AIConcurrentStream.tsx | 72 +++++++++-------- .../fetchConcurrentStreamContents.ts | 5 +- .../AIChildWindowTaskDefaultGroupCard.tsx | 17 ++-- .../ConcurrentStreamCard.tsx | 4 +- .../ConcurrentStreamCardActions.tsx | 10 ++- .../AIChildWindowConcurrentStreamCard.tsx | 18 +++-- .../buildConcurrentStreamFramePayload.ts | 45 ++++++----- .../collectConcurrentStreamContentEntries.ts | 46 ----------- .../concurrentStreamMainBridge.ts | 77 +++++++------------ .../useConcurrentStreamRefreshListener.ts | 10 ++- .../ConcurrentStreamCardHeard.tsx | 24 ++++++ 12 files changed, 165 insertions(+), 168 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/collectConcurrentStreamContentEntries.ts diff --git a/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js b/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js index ced9c0ed45..173d5e4fe7 100644 --- a/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js +++ b/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js @@ -29,7 +29,10 @@ function register(manager, mainWindow) { } } - /** @deprecated 获取数据的逻辑改为直接从主窗口推送,如需下面逻辑,需要按新版获取数据逻辑补充 */ + /** + * 子窗口打开后通过 fetch-concurrent-stream-contents 主动向主窗口拉取 rawData。 + * 这里做 requestId 中转:转发请求到主窗口,等待响应后再 resolve 子窗口的 invoke。 + */ ipcMain.handle(FETCH_CONTENTS, async (_event, frame) => { if (!frame?.session || !frame?.token || !mainWindow || mainWindow.isDestroyed()) { return { rawData: [] } diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx index aae81ca556..7f50407ee0 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, memo, startTransition, useEffect, useMemo, useRef, useState } from 'react' +import React, { Suspense, lazy, memo, startTransition, useEffect, useMemo, useRef, useState } from 'react' import { yakitAuxWindow } from '@/services/electronBridge' import ConcurrentStreamSkeleton from '@/auxWindow/components/ConcurrentStreamSkeleton/ConcurrentStreamSkeleton' import { @@ -6,19 +6,24 @@ import { isConcurrentStreamFrame, } from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' import { AIChatQSDataTypeEnum, type AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' +import { fetchConcurrentStreamContents } from './fetchConcurrentStreamContents' import styles from './AIConcurrentStream.module.scss' -import AIChildWindowTaskDefaultGroupCard from '@/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard' -import AIChildWindowConcurrentStreamCard from '@/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard' import AIConcurrentStreamContent, { AIConcurrentStreamDispatcher, AIConcurrentStreamStore, } from './useContext/AIConcurrentStreamContent' import useMemoizedFn from 'ahooks/lib/useMemoizedFn' -// const ConcurrentStreamCard = lazy(() => import('@/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard')) -// const AITaskDefaultGroupCard = lazy( -// () => import('@/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard'), -// ) +// 子卡片按需加载,避免重型卡片(AINodeItem 及其下游 review/report/fuzz 等子卡) +// 全量进入 aux bundle,拉长 did-finish-load 与首次开窗耗时。 +const AIChildWindowTaskDefaultGroupCard = lazy( + () => + import('@/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard'), +) +const AIChildWindowConcurrentStreamCard = lazy( + () => + import('@/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard'), +) const { ipcRenderer } = window.require('electron') @@ -29,6 +34,8 @@ interface AIConcurrentStreamProps { const AIConcurrentStream: React.FC = memo(({ windowId }) => { const [frame, setFrame] = useState(null) const [contentVersion, setContentVersion] = useState(0) + // rawData 是否仍在通过 fetch-concurrent-stream-contents 拉取中 + const [loadingContents, setLoadingContents] = useState(false) // rawData 用 ref 存储,更新不触发渲染; // 组件及子组件的重渲染由 contentVersion(renderNum)驱动 @@ -40,13 +47,16 @@ const AIConcurrentStream: React.FC = memo(({ windowId } const applyFrame = (payload: ConcurrentStreamFramePayload) => { if (!isConcurrentStreamFrame(payload)) return const framePayload = payload - // 直接使用 frame 里的 rawData 存入 ref - rawDataRef.current = framePayload.rawData ?? new Map() + // 开窗时 frame 只携带轻量元数据(rawData 为空 Map), + // 真正的 rawData 由下方的懒拉取 effect 通过 IPC 向主窗口请求 startTransition(() => { - setFrame({ + const newFrame = { ...framePayload, rawData: new Map(), - }) + } + setFrame(newFrame) + // 收到 frame 后,主动向主窗口拉取本次需要渲染的 rawData。 + getRawData(newFrame) setContentVersion((v) => v + 1) }) } @@ -69,28 +79,21 @@ const AIConcurrentStream: React.FC = memo(({ windowId } } }, [windowId]) - /** deprecated 目前数据都是通过 openAIConcurrentStream发送到子窗口,如需沿用下面的逻辑,需要将获取数据的方法改为新版 */ - // useEffect(() => { - // if (!frame || contentVersion === 0) return - - // let cancelled = false - // setLoadingContents(true) - - // fetchConcurrentStreamContents(frame) - // .then((entries) => { - // if (!cancelled) setContentEntries(entries) - // }) - // .catch(() => { - // if (!cancelled) setContentEntries([]) - // }) - // .finally(() => { - // if (!cancelled) setLoadingContents(false) - // }) - - // return () => { - // cancelled = true - // } - // }, [frame, contentVersion]) + const getRawData = useMemoizedFn((frame) => { + setLoadingContents(true) + + fetchConcurrentStreamContents(frame) + .then((entries) => { + rawDataRef.current = entries + setContentVersion((v) => v + 1) + }) + .catch(() => { + rawDataRef.current = new Map() + }) + .finally(() => { + setLoadingContents(false) + }) + }) const isTaskDefaultGroup = useMemo(() => { if (!frame) return false @@ -125,7 +128,8 @@ const AIConcurrentStream: React.FC = memo(({ windowId } requestRefresh, } }, []) - if (!frame) { + // isTaskDefaultGroup 需要读 rawData 判断根节点类型,必须等 rawData 拉取完成 + if (!frame || loadingContents) { return } diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts index 83324c13a3..db5db7a2fb 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts @@ -4,10 +4,9 @@ import type { ConcurrentStreamFramePayload } from '@/pages/ai-agent/components/C const { ipcRenderer } = window.require('electron') /** - * @deprecated - * 如果沿用这个逻辑,参考: * 子窗口通过 IPC 向主窗口拉取 task 相关的全部 content 数据。 - * 主窗口从 ChatDataStore 中收集 task 自身、childrenTokens 各节点、group 内子节点的数据, + * 主窗口从 globalSessionEngine 取最新 store + rawData,复用 buildConcurrentStreamFramePayload + * 收集 task 自身、childrenTokens 各节点、group 内子节点的数据, * 返回为 Map(token → 原始数据)。 */ export async function fetchConcurrentStreamContents( diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx index 45f9fb20ba..9675b6aec0 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx @@ -107,13 +107,16 @@ const AIChildWindowGroupItem: FC<{ token: string; rawData: Map }> = memo( ({ token, rawData }) => { - // 从 rawData 中查找属于该 group 的子节点(parentGroupToken === token) - const childItems: AIChatQSData[] = [] - rawData.forEach((value) => { - if (value.parentGroupToken === token) { - childItems.push(value) - } - }) + // 按 token + rawData 缓存该 group 的子节点,避免每次渲染都全量 forEach + const childItems = useCreation(() => { + const items: AIChatQSData[] = [] + rawData.forEach((value) => { + if (value.parentGroupToken === token) { + items.push(value) + } + }) + return items + }, [rawData, token]) return (
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index 68ef9111c7..74212bbe63 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -94,12 +94,12 @@ const ConcurrentStreamCardHeardWrapper: FC { if (!isChildWindow) expandToggle() diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx index 74c9f5dd52..3039f1437f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx @@ -61,7 +61,15 @@ const ConcurrentStreamCardActions: FC = ({ const openChildWindow = useMemoizedFn((e) => { e?.stopPropagation() - const framePayload = buildConcurrentStreamFramePayload({ token, session, chatType, store, rawData }) + // 开窗只传轻量元数据(rawData 为空 Map),rawData 由子窗 mount 后再拉取,避免开窗瞬间克隆大 Map + const framePayload = buildConcurrentStreamFramePayload({ + token, + session, + chatType, + store, + rawData, + withRawData: false, + }) if (framePayload) { openAIConcurrentStream(framePayload) } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx index 68394f99fb..153dfe836b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx @@ -112,13 +112,17 @@ const AIChildWindowGroupItem: FC = memo(({ token }) /** 子窗口版 stream group 卡片,从 rawData 中按 parentGroupToken 查找子节点 */ const AIChildWindowGroupStreamCard: FC = memo(({ token }) => { const { rawData, renderNum } = useAIConcurrentStreamStore() - // 从 rawData 中查找属于该 group 的子节点(parentGroupToken === token) - const childItems: AIChatQSData[] = [] - rawData?.forEach((value) => { - if (value.parentGroupToken === token) { - childItems.push(value) - } - }) + // 按 token + renderNum 缓存该 group 的子节点,避免每次渲染都全量 forEach + const childItems = useCreation(() => { + if (!rawData) return [] + const items: AIChatQSData[] = [] + rawData.forEach((value) => { + if (value.parentGroupToken === token) { + items.push(value) + } + }) + return items + }, [rawData, token, renderNum]) return (
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts index 706e75cef5..5dd47cfeaa 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts @@ -17,6 +17,13 @@ export interface BuildConcurrentStreamFramePayloadParams { chatType?: string store: BuildFrameStore rawData: BuildFrameRawData | null | undefined + /** + * 是否在返回的 frame 中填充 rawData(task 自身 + children + group 孙节点)。 + * - true(默认):用于需要立即拿到全部 rawData 的场景。 + * - false:仅返回元数据(rawData 为空 Map),用于打开子窗口时只发轻量 payload, + * 子窗 mount 后再通过 fetch-concurrent-stream-contents 拉取,避免开窗瞬间克隆大 Map。 + */ + withRawData?: boolean } /** 判断 childToken 在 store 中的类型 */ @@ -45,32 +52,34 @@ function getTaskName(rawData: BuildFrameRawData | null | undefined, token: strin export function buildConcurrentStreamFramePayload( params: BuildConcurrentStreamFramePayloadParams, ): OpenAIConcurrentStreamPayload | null { - const { token, session, chatType, store, rawData } = params + const { token, session, chatType, store, rawData, withRawData = false } = params if (!chatType || !rawData) return null const frameRawData = new Map() const state = store.getState() const childrenTokens = state.tasks[token]?.childrenTokens || [] - // task 自身数据 - const taskData = rawData.contents.get(token) - if (taskData) frameRawData.set(token, taskData) + if (withRawData) { + // task 自身数据 + const taskData = rawData.contents.get(token) + if (taskData) frameRawData.set(token, taskData) - // 遍历所有子节点 - for (const childToken of childrenTokens) { - const kind = getKind(store, childToken) - if (!kind) continue - const childData = rawData.contents.get(childToken) - if (!childData) continue - frameRawData.set(childToken, childData) + // 遍历所有子节点 + for (const childToken of childrenTokens) { + const kind = getKind(store, childToken) + if (!kind) continue + const childData = rawData.contents.get(childToken) + if (!childData) continue + frameRawData.set(childToken, childData) - // group 下的所有子节点数据 - if (kind === 'group') { - const groupData = state.groups[childToken] - for (const grandChildToken of groupData?.childrenTokens || []) { - const grandChildData = rawData.contents.get(grandChildToken) - if (!grandChildData) continue - frameRawData.set(grandChildToken, grandChildData) + // group 下的所有子节点数据 + if (kind === 'group') { + const groupData = state.groups[childToken] + for (const grandChildToken of groupData?.childrenTokens || []) { + const grandChildData = rawData.contents.get(grandChildToken) + if (!grandChildData) continue + frameRawData.set(grandChildToken, grandChildData) + } } } } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/collectConcurrentStreamContentEntries.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/collectConcurrentStreamContentEntries.ts deleted file mode 100644 index 2a0904453f..0000000000 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/collectConcurrentStreamContentEntries.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { ChatDataStore } from '@/pages/ai-agent/store/ChatDataStore' -import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' -import type { ConcurrentStreamFramePayload } from '../concurrentStreamFrame' - -/** - * @deprecated 不适合新版 - */ -export function collectConcurrentStreamContentEntries( - store: ChatDataStore | undefined, - frame: ConcurrentStreamFramePayload, -): Map { - const result = new Map() - if (!store) return result - - const { session, token, chatType, childrenTokens } = frame - - /** 收集单个 token 对应的数据 */ - const collectOne = (mapKey: string) => { - if (result.has(mapKey)) return - const content = store.getContentMap({ session, chatType, mapKey }) - if (content) { - result.set(mapKey, content) - } - } - - // task 自身 - collectOne(token) - - // childrenTokens 各节点 - for (const childToken of childrenTokens) { - collectOne(childToken) - - // 如果该节点是 group,收集 group 内所有子节点(通过 parentGroupToken === childToken 关联) - const chatData = store.get(session) - if (chatData) { - const contentsMap = chatType === 'reAct' ? chatData.casualChat.contents : chatData.taskChat.contents - contentsMap.forEach((value, key) => { - if (value.parentGroupToken === childToken) { - collectOne(key) - } - }) - } - } - - return result -} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts index 85403d3f57..d821d50a85 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts @@ -1,62 +1,43 @@ -import type { ChatDataStore } from '@/pages/ai-agent/store/ChatDataStore' -import { - aiChatDataStore, - FlowAiStore, - histroyAiStore, - irifyAiCodeAuditPageAiStore, - knowledgeBaseDataStore, -} from '@/pages/ai-agent/store/ChatDataStore' -import { collectConcurrentStreamContentEntries } from './collectConcurrentStreamContentEntries' +import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' +import { buildConcurrentStreamFramePayload } from './buildConcurrentStreamFramePayload' import type { ConcurrentStreamFramePayload } from '../concurrentStreamFrame' const { ipcRenderer } = window.require('electron') const FETCH_REQUEST = 'fetch-concurrent-stream-contents-request' -const defaultStoreProviders: Array<() => ChatDataStore | undefined> = [ - () => aiChatDataStore, - () => knowledgeBaseDataStore, - () => histroyAiStore, - () => FlowAiStore, - () => irifyAiCodeAuditPageAiStore, -] - -const storeProviders = new Set<() => ChatDataStore | undefined>(defaultStoreProviders) - let bridgeReady = false +let teardown: (() => void) | null = null + +/** + * 主窗口:响应 aux 子窗的 rawData 拉取。 + * 子窗通过 fetch-concurrent-stream-contents 拿到的是仅含元数据的 frame, + * 收到请求后从 globalSessionEngine 取最新 store + rawData,复用 buildConcurrentStreamFramePayload + * 收集 task 自身 / children / group 孙节点的原始数据,回传给子窗。 + * + * @returns teardown 函数,调用后卸载监听并允许重新 setup。 + */ +export function setupConcurrentStreamMainBridge() { + if (bridgeReady) return teardown -function resolveStoreForSession(session: string): ChatDataStore | undefined { - for (const provider of storeProviders) { - const store = provider() - if (store?.get(session)) return store - } - for (const provider of storeProviders) { - const store = provider() - if (store) return store - } - return undefined -} + const handler = (_event: unknown, payload: ConcurrentStreamFramePayload & { requestId: string }) => { + const { requestId, session, token, chatType } = payload + if (!requestId || !session || !token) return -export function registerConcurrentStreamStoreProvider(provider: () => ChatDataStore | undefined) { - storeProviders.add(provider) - return () => { - storeProviders.delete(provider) + const { store, rawData } = globalSessionEngine.ensureSession(session) + const full = buildConcurrentStreamFramePayload({ token, session, chatType, store, rawData }) + const entries = full ? Array.from(full.rawData.entries()) : [] + + ipcRenderer.send(`fetch-concurrent-stream-contents-response-${requestId}`, { rawData: entries }) } -} -/** 主窗口:响应 aux 子窗的内容拉取 */ -export function setupConcurrentStreamMainBridge() { - if (bridgeReady) return + ipcRenderer.on(FETCH_REQUEST, handler) bridgeReady = true - /** @deprecated 下面方法不适合新版了,目前数据都是通过 openAIConcurrentStream发送到子窗口,如需沿用下面的逻辑,需要将获取数据的方法改为新版 */ - ipcRenderer.on(FETCH_REQUEST, (_event, payload: ConcurrentStreamFramePayload & { requestId: string }) => { - const { requestId, ...frame } = payload - if (!requestId || !frame.session || !frame.token) return - - const store = resolveStoreForSession(frame.session) - const rawData = collectConcurrentStreamContentEntries(store, frame) - - ipcRenderer.send(`fetch-concurrent-stream-contents-response-${requestId}`, { rawData }) - }) + teardown = () => { + ipcRenderer.removeListener(FETCH_REQUEST, handler) + bridgeReady = false + teardown = null + } + return teardown } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts index 7404cf0d4a..1d648742bc 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts @@ -35,7 +35,15 @@ export function useConcurrentStreamRefreshListener(token: string, enabled = true return } - const framePayload = buildConcurrentStreamFramePayload({ token, session, chatType, store, rawData }) + // 刷新同样只推元数据,rawData 由子窗重新拉取,与开窗路径保持一致 + const framePayload = buildConcurrentStreamFramePayload({ + token, + session, + chatType, + store, + rawData, + withRawData: false, + }) if (framePayload) { openAIConcurrentStream(framePayload, { silent: true }) } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx index fd536bf114..3215d1dce7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx @@ -6,6 +6,9 @@ import styles from './ConcurrentStreamCardHeard.module.scss' import { getAIStatusPresentation } from '@/pages/ai-agent/utils/AIStatusUtils' import { AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' import useCreation from 'ahooks/lib/useCreation' +import useMemoizedFn from 'ahooks/lib/useMemoizedFn' +import { yakitNotify } from '@/utils/notification' +import emiter from '@/utils/eventBus/eventBus' const ConcurrentStreamCardHeard: FC = memo((props) => { const { token, isChildWindow, onClickTitle, rowData, coordinatorId, expand, expandToggle, onRefresh } = props @@ -36,6 +39,26 @@ const ConcurrentStreamCardHeard: FC = memo((prop return status === AITaskStatus.created || status === AITaskStatus.inProgress }, [rowData?.data?.status]) + const onDetails = useMemoizedFn(() => { + if (!rowData) return + const { data } = rowData + if (!data.taskId) { + yakitNotify('error', 'taskId为空') + return + } + emiter.emit( + 'actionAITaskContentTab', + JSON.stringify({ + type: 'add', + params: { + key: data.taskId, + label: data.taskName || '', + goal: data.goal, + }, + }), + ) + }) + return (
@@ -55,6 +78,7 @@ const ConcurrentStreamCardHeard: FC = memo((prop coordinatorId={coordinatorId} taskId={rowData?.data?.taskId} onRefresh={onRefresh} + onDetails={onDetails} />
From e5057efb4b8fcf8b24b0d1188cf5aebaaf261715 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Tue, 21 Jul 2026 17:57:11 +0800 Subject: [PATCH 045/101] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E5=86=97?= =?UTF-8?q?=E4=BD=99=20useCasualChat/useTaskChat=EF=BC=8C=E6=94=B6?= =?UTF-8?q?=E6=95=9B=20CurrentExecTaskTree=20=E4=B8=8E=20ChatStore=20?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/aiAgentType.d.ts | 2 +- .../components/aiFileSystemList/type.ts | 4 +- .../src/pages/ai-re-act/hooks/aiRender.ts | 14 +- .../src/pages/ai-re-act/hooks/chatStore.ts | 3 +- .../pages/ai-re-act/hooks/defaultConstant.ts | 5 +- .../main/src/pages/ai-re-act/hooks/type.ts | 136 +--------- .../pages/ai-re-act/hooks/useCasualChat.ts | 94 ------- .../src/pages/ai-re-act/hooks/useTaskChat.ts | 244 ------------------ 8 files changed, 20 insertions(+), 482 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts b/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts index f3b7633258..d35a26e8b0 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts @@ -2,7 +2,7 @@ import { Dispatch, SetStateAction } from 'react' import { AISession } from './type/aiChat' import { AITabsEnum } from './defaultConstant' import { AIStartParams } from '../ai-re-act/hooks/grpcApi' -import { PlanLoadingStatus } from '../ai-re-act/hooks/type' +import type { PlanLoadingStatus } from '../ai-re-act/hooks/type' export interface AIAgentProps { pageId: string } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiFileSystemList/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiFileSystemList/type.ts index cf56cda08b..2980d95b1e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiFileSystemList/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiFileSystemList/type.ts @@ -1,5 +1,5 @@ import type { YakitMenuItemType } from '@/components/yakitUI/YakitMenu/YakitMenu' -import type { UseYakExecResultState } from '@/pages/ai-re-act/hooks/type' +import { ChatStoreState } from '@/pages/ai-re-act/hooks/aiRender' import type { UseFileTreeEvents } from '@/pages/ai-re-act/hooks/useFileTree' import type { FileNodeProps } from '@/pages/yakRunner/FileTree/FileTreeType' import type { Dispatch, SetStateAction } from 'react' @@ -9,7 +9,7 @@ export enum TabKey { OperationLog = 'operation-log', } export interface AIFileSystemListProps { - execFileRecord: UseYakExecResultState['execFileRecord'] + execFileRecord: ChatStoreState['execFileRecord'] activeKey?: TabKey setActiveKey?: (key: TabKey) => void } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index b3b2a2ab30..3618a34b53 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -1,12 +1,6 @@ import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' import type { AIAgentGrpcApi, AIOutputEvent, AITaskStatusType, AIOutputI18n } from './grpcApi' -import type { - AIFileSystemPin, - AIQuestionQueues, - CurrentExecTaskTree, - PlanLoadingStatus, - UseAIMessageDataState, -} from './type' +import type { AIFileSystemPin, AIQuestionQueues, PlanLoadingStatus, UseAIMessageDataState } from './type' import { CustomPluginExecuteFormValue } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType' // #region AI-Agent 非会话列表外的渲染数据 @@ -468,6 +462,12 @@ export type AIChatQSData = | ChatStreamGroup // #endregion +/** 当前正在执行的任务树 */ +export interface CurrentExecTaskTree { + task_tree: AIAgentGrpcApi.PlanHistory['task_tree'] + root_task_name: AIAgentGrpcApi.PlanHistory['root_task_name'] +} + /** @name 数据状态机定义 */ export interface ChatStoreState { /** 会话执行状态 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index b58d13532c..0d21180591 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -1,4 +1,4 @@ -import { AIChatQSDataTypeEnum, type ChatStoreState } from './aiRender' +import { AIChatQSDataTypeEnum, CurrentExecTaskTree, type ChatStoreState } from './aiRender' import { createStore } from 'zustand/vanilla' import { immer } from 'zustand/middleware/immer' import cloneDeep from 'lodash/cloneDeep' @@ -8,7 +8,6 @@ import { DefaultPlanHistoryList, DefaultPlanLoadingStatus, } from './defaultConstant' -import type { CurrentExecTaskTree } from './type' import { v4 as uuidv4 } from 'uuid' export const createChatStore = () => { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts index 916aafa519..0c9ac66879 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts @@ -1,7 +1,6 @@ -import { cloneDeep } from 'lodash' -import type { AIToolResult, PlanItemDetailsData, TodoListCardData } from './aiRender' +import type { AIToolResult, CurrentExecTaskTree, PlanItemDetailsData, TodoListCardData } from './aiRender' import type { AIOutputI18n, AIAgentGrpcApi } from './grpcApi' -import type { AIQuestionQueues, PlanLoadingStatus, CurrentExecTaskTree } from './type' +import type { AIQuestionQueues, PlanLoadingStatus } from './type' /** 工具执行结果-默认值 */ export const DefaultAIToolResult: AIToolResult = { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts index e053405f63..8899202c30 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts @@ -1,10 +1,8 @@ -import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' import type { AIChatQSData, AIChatQSDataType, AIInputNotifyMessage, AIStreamOutput, - AIYakExecFileRecord, ChatListRenderType, ReActChatRenderItem, } from './aiRender' @@ -15,6 +13,7 @@ import type { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' import type { AIChatData } from '@/pages/ai-agent/type/aiChat' import type { ChatDataStore } from '@/pages/ai-agent/store/ChatDataStore' import { ChatMultiSessionController } from './ChatMultiSessionController' +import type { YakitRouteType } from '@/enums/yakitRoute' // #region 公共 hooks 事件 export interface UseHookBaseParams { @@ -43,87 +42,6 @@ export interface UpdateRenderDataParams { } // #endregion -// #region useYakExecResult相关定义 -export interface UseYakExecResultParams extends UseHookBaseParams {} - -export interface UseYakExecResultState { - card: AIAgentGrpcApi.AIInfoCard[] - execFileRecord: Map - yakExecResultLogs: StreamResult.Log[] -} -export interface UseYakExecResultEvents extends UseHookBaseEvents { - /** 设置UI展示的列表数据 */ - handleSetYakResult: (newData: UseYakExecResultState) => void -} -// #endregion - -// #region useCasualChat相关定义 -export interface UseCasualChatParams extends UseHookBaseParams { - /** 获取流接口请求参数 */ - getRequest: () => AIAgentSetting | undefined - /** 获取当前自由对话父任务 ID */ - getCurrentCasualTaskID: () => string - /** review 触发回调事件 */ - onReview?: (data: AIChatQSData) => void - /** 触发 review-release 后的回调事件 */ - onReviewRelease: (id: string) => void - /** 收集自由对话里的子任务-taskID */ - onSubTaskID: (taskID: string) => void -} - -export interface UseCasualChatState { - elements: ReActChatRenderItem[] - toolListRenderNumber: number -} -export interface UseCasualChatEvents extends UseHookBaseEvents, UseHookStateFunc { - handleSend: handleSendFunc - /** 用户手动介入逻辑 */ - handleUserManualIntervention: (chatInfo: AIChatQSData) => void - /** 清空todo-list数据 */ - resetPlanDetailsForNewTask: (newTaskId: string) => void -} -// #endregion - -// #region useTaskChat相关定义 -export interface UseTaskChatParams extends UseHookBaseParams { - /** 获取流接口请求参数 */ - getRequest: () => AIAgentSetting | undefined - /** 获取当前任务规划的问题ID信息 */ - getCurrentTaskPlanID: () => TaskChatTaskInfo | undefined - /** review 触发回调事件 */ - onReview?: (data: AIChatQSData) => void - /** plan_review 补充数据 */ - onReviewExtra?: (data: AIAgentGrpcApi.PlanReviewRequireExtra) => void - /** 触发 review-release 后的回调事件 */ - onReviewRelease?: (id: string) => void - /** 向接口发送消息 */ - sendRequest?: (request: AIInputEvent) => void -} - -/** 当前正在执行的任务树 */ -export interface CurrentExecTaskTree { - task_tree: AIAgentGrpcApi.PlanHistory['task_tree'] - root_task_name: AIAgentGrpcApi.PlanHistory['root_task_name'] -} - -export interface UseTaskChatState { - /** 正在执行的任务列表 */ - plan: CurrentExecTaskTree - elements: ReActChatRenderItem[] -} -export interface UseTaskChatEvents extends UseHookBaseEvents, UseHookStateFunc { - handleSend: handleSendFunc - /** grpc接口关闭后的后续处理逻辑 */ - handleCloseGrpc: () => void - /** 当前任务规划结束-触发UI展示结束标识 */ - handlePlanExecEnd: (res: AIOutputEvent) => void - /** 用户手动介入逻辑 */ - handleUserManualIntervention: (chatInfo: AIChatQSData) => void - /** 清空当前任务树 */ - handleResetPlanTree: () => void -} -// #endregion - // #region useChatIPC相关定义 /** 会话类型 */ export type ChatIPCSendType = 'casual' | 'task' | '' @@ -206,9 +124,9 @@ export interface UseChatIPCState { /** 运行时产生risk数据的run_time_id合集 */ riskRunTimeIDs: string[] /** 自由对话相关数据 */ - casualChat: UseCasualChatState + casualChat: any /** 任务规划相关数据 */ - taskChat: UseTaskChatState + taskChat: any /** 接口运行过程中的数据文件夹合集 */ grpcFolders: AIFileSystemPin[] /** 问题队列信息 */ @@ -245,6 +163,10 @@ export interface UseChatIPCState { export interface AIChatIPCStartParams { token: string params: AIInputEvent + /** 会话归属路由(不可变) */ + route: YakitRouteType + /** 会话初始归属 pageId(后续可 rebind) */ + pageId: string } /** 执行流途中发送消息的参数 */ @@ -261,50 +183,6 @@ export interface TaskChatTaskInfo { status: AITaskStatusType coordinatorId: AIOutputEvent['CoordinatorId'] } - -export interface UseChatIPCEvents { - /** 获取当前执行接口流的唯一标识符 */ - fetchToken: () => string - /** 获取当前执行接口流的请求参数 */ - fetchAIRequest: () => AIStartParams | undefined - /** 获取当前执行的自由对话问题(这个问题可能引起的是任务规划)对应的任务ID */ - fetchCurrentCasualTaskID: () => string - /** 获取当前执行任务规划的问题详情 */ - fetchCurrentTaskPlanID: () => TaskChatTaskInfo | undefined - /** 获取当前外界传入的数据类实例 */ - fetchChatDataStore: () => UseChatIPCParams['cacheDataStore'] - /** 切换历史会话展示 */ - onSwitchChat: (session?: string, isCreate?: boolean) => void - /** 开始执行接口流 */ - onStart: (params: AIChatIPCStartParams, cb?: () => void) => void - /** 向执行中的接口流主动输入信息 */ - onSend: (params: AIChatSendParams) => void - /** 主动结束正在执行中的接口流 */ - onClose: ( - token: string, - option?: { - tip: () => void - }, - ) => void - /** 重置所有数据 */ - onReset: () => void - /** 取消任务规划当前的Review */ - handleTaskReviewRelease: (id: string) => void - /** 删除会话操作的关联逻辑 */ - onDelChats: (session: string[]) => void - /** 用户主动取消问题的loading状态变换 */ - handleCancelLoadingChange: (type: ChatListRenderType, status: boolean) => void - /** 清空指定变量数据 */ - handleResetTarget: (target: 'memoryList') => void - /** 用户手动干预的执行事件 */ - handleUserManualIntervention: (chatInfo: AIChatQSData) => void - /** 加载更多历史数据 */ - handleLoadMoreHistory: (chatType: HistoryChatType) => void - /** 是否还有更多历史数据 */ - handleHasMoreHistory: (type: HistoryChatType) => boolean - /** 清除当前任务规划的ID信息 */ - resetCurrentTaskPlanID: () => void -} // #endregion // #region useAIChatLog相关定义 diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts deleted file mode 100644 index ce932515b1..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useCasualChat.ts +++ /dev/null @@ -1,94 +0,0 @@ -import type { handleSendFunc, UseCasualChatEvents, UseCasualChatParams, UseCasualChatState } from './type' -import type { AIChatQSData, ReActChatRenderItem } from './aiRender' -import type { AIOutputEvent } from './grpcApi' -import { useRef, useState } from 'react' -import { useCreation, useMemoizedFn } from 'ahooks' -import cloneDeep from 'lodash/cloneDeep' -import { yakitNotify } from '@/utils/notification' -import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' -import { DefaultTodoListCardData } from './defaultConstant' - -function useCasualChat(params: UseCasualChatParams): [UseCasualChatState, UseCasualChatEvents] - -function useCasualChat(params: UseCasualChatParams) { - const { getChatDataStore, getRequest, onReviewRelease } = params || {} - - const [elements, setElements, getElements] = useGetSetState([]) - - const getCasualChat = useMemoizedFn(() => { - return getChatDataStore?.()?.casualChat - }) - const [toolListRenderNumber, setToolListRenderNumber] = useState(0) - /** 新一轮自由对话出队时,重置主任务详情(避免 taskId 仍指向上一次提问) */ - const resetPlanDetailsForNewTask = useMemoizedFn((newTaskId: string) => { - const chatDetail = getCasualChat() - if (!chatDetail) return - chatDetail.planDetails = { - ...cloneDeep(DefaultPlanItemDetailsData), - taskId: newTaskId, - } - setToolListRenderNumber((old) => old + 1) - }) - const getContentMap = useMemoizedFn((mapKey: string) => { - const contentMap = getChatDataStore?.()?.casualChat?.contents - if (!contentMap) return undefined - return contentMap.get(mapKey) - }) - const setContentMap = useMemoizedFn((mapKey: string, value: AIChatQSData) => { - const contentMap = getChatDataStore?.()?.casualChat?.contents - contentMap && contentMap.set(mapKey, value) - }) - - // #region review数据-hook缓存数据 - const review = useRef() - - // #endregion - - // 处理数据方法 - const handleSetData = useMemoizedFn((res: AIOutputEvent) => {}) - - // 用户问题或review的主动操作 - const handleSend: handleSendFunc = useMemoizedFn(({ request, optionValue, cb }) => {}) - - const handleResetData = useMemoizedFn(() => { - review.current = undefined - getChatDataStore?.()?.casualChat.planDetailsMap.clear() - setElements([]) - setToolListRenderNumber(0) - }) - - /** 用户手动介入逻辑 */ - const handleUserManualIntervention = useMemoizedFn((chatInfo: AIChatQSData) => { - try { - setContentMap(chatInfo.id, cloneDeep(chatInfo)) - setElements((old) => [ - ...old, - { token: chatInfo.id, type: chatInfo.type, renderNum: 1, chatType: 'reAct', kind: 'item' }, - ]) - } catch (error) { - yakitNotify('error', `用户手动干预操作失败: ${error}`) - } - }) - - const state: UseCasualChatState = useCreation(() => { - return { elements, toolListRenderNumber } - }, [elements, toolListRenderNumber]) - - const events: UseCasualChatEvents = useCreation(() => { - return { - handleSetData, - handleResetData, - handleSend, - getContentMap, - setContentMap, - setElements: setElements, - getElements: getElements, - handleUserManualIntervention, - resetPlanDetailsForNewTask, - } - }, []) - - return [state, events] as const -} - -export default useCasualChat diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts deleted file mode 100644 index faf7071583..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useTaskChat.ts +++ /dev/null @@ -1,244 +0,0 @@ -import type { - CurrentExecTaskTree, - handleSendFunc, - UseTaskChatEvents, - UseTaskChatParams, - UseTaskChatState, -} from './type' -import type { AIChatQSData, ReActChatRenderItem, AITaskInfoProps } from './aiRender' -import type { AIAgentGrpcApi, AIOutputEvent } from './grpcApi' -import { useRef, useState } from 'react' -import { useCreation, useMemoizedFn } from 'ahooks' -import cloneDeep from 'lodash/cloneDeep' -import { DefaultCurrentExecTaskTree } from './defaultConstant' -import { genBaseAIChatData, genExecTasks } from './utils' -import { yakitNotify } from '@/utils/notification' -import { AITaskStatus } from './grpcApi' -import { AIChatQSDataTypeEnum } from './aiRender' -import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' - -function useTaskChat(params: UseTaskChatParams): [UseTaskChatState, UseTaskChatEvents] - -function useTaskChat(params: UseTaskChatParams) { - const { getChatDataStore, getRequest, getCurrentTaskPlanID, onReview, onReviewExtra, onReviewRelease, sendRequest } = - params || {} - - const [elements, setElements, getElements] = useGetSetState([]) - - const getContentMap = useMemoizedFn((mapKey: string) => { - const contentMap = getChatDataStore?.()?.taskChat?.contents - if (!contentMap) return undefined - return contentMap.get(mapKey) - }) - const setContentMap = useMemoizedFn((mapKey: string, value: AIChatQSData) => { - const contentMap = getChatDataStore?.()?.taskChat?.contents - contentMap && contentMap.set(mapKey, value) - }) - - // #region 任务树相关逻辑 - const [plan, setPlan] = useState(cloneDeep(DefaultCurrentExecTaskTree)) - const handleResetPlanTree = useMemoizedFn(() => { - setPlan(cloneDeep(DefaultCurrentExecTaskTree)) - }) - - /** - * 正在执行中的叶子任务的mapKey集合(已结束的叶子任务会被移除) - * 主要为了在任务中断时,手动设置为error状态使用 - */ - const activeLeafTasks = useRef>(new Set()) - const handleResetActiveLeafTasks = useMemoizedFn(() => { - activeLeafTasks.current.clear() - }) - - const handleTaskNode = useMemoizedFn((res: AIOutputEvent, info: AIAgentGrpcApi.ChangeTask) => { - try { - const taskId = getCurrentTaskPlanID?.()?.taskID - const ownTaskId = info.task.task_id || res.TaskId - if (!taskId || !ownTaskId) return - const taskKey = `${taskId}-${ownTaskId}` - const existing = getContentMap(taskKey) - if (existing && existing.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - // push log - return - } - const chatData: AIChatQSData = - existing ?? - ({ - ...genBaseAIChatData(res), - id: taskKey, - chatType: 'task', - type: AIChatQSDataTypeEnum.TASK_NODE_GROUP, - data: { - taskId: ownTaskId, - taskName: info.task.name, - goal: info.task.goal, - status: info.task.task_status || AITaskStatus.inProgress, - }, - } as AIChatQSData) - - setContentMap(chatData.id, chatData) - if (info.type === 'push_task') { - activeLeafTasks.current.add(chatData.id) - setElements((old) => { - const exists = old.some((item) => item.token === chatData.id && item.type === chatData.type) - if (exists) return old - const last = old[old.length - 1] - if (last.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP) { - // 实时数据下,将默认任务聚合组置底 - old.splice(old.length - 1, 0, { - token: chatData.id, - type: chatData.type, - renderNum: 1, - chatType: 'task', - kind: 'task', - children: [], - }) - return [...old] - } - return [ - ...old, - { token: chatData.id, type: chatData.type, renderNum: 1, chatType: 'task', kind: 'task', children: [] }, - ] - }) - } else if (info.type === 'pop_task') { - // 删除正在执行队列里的叶子任务, 因为当前任务已经结束了 - activeLeafTasks.current.delete(chatData.id) - if (chatData.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - chatData.data.status = info.task.task_status - } - setElements((old) => { - return old.map((item) => { - if (item.token === chatData.id && item.type === chatData.type) { - return { ...item, renderNum: item.renderNum + 1 } - } - return item - }) - }) - } - } catch { - // push log - } - }) - - /** 更新任务树指定任务节点的状态 */ - const handleUpdateTaskState = useMemoizedFn((taskId: string, state: AITaskInfoProps['progress']) => { - setPlan((old) => { - return { - ...old, - task_tree: old.task_tree.map((item) => { - if (item.task_id === taskId) item.progress = state - return item - }), - } - }) - }) - - /** 将任务树中, 所有进行中的任务, 变更为中止状态 */ - const handleAbortTaskState = useMemoizedFn(() => { - const leafTasks = activeLeafTasks.current - for (let mapKey of leafTasks) { - const taskNodeInfo = getContentMap(mapKey) - if (!taskNodeInfo || taskNodeInfo.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - continue - } - taskNodeInfo.data.status = AITaskStatus.error - setElements((old) => { - return old.map((item) => { - if (item.token === taskNodeInfo.id && item.type === taskNodeInfo.type) { - return { ...item, renderNum: item.renderNum + 1 } - } - return item - }) - }) - } - handleResetActiveLeafTasks() - setPlan((old) => { - return { - ...old, - task_tree: old.task_tree.map((item) => { - if (item.progress === AITaskStatus.inProgress) { - item.progress = AITaskStatus.error - } - return item - }), - } - }) - }) - // #endregion - - // #region review数据-hook缓存数据 - const review = useRef() - - // 将 review 数据处理成需要展示的UI数据 - const handleReviewDataToUI = useMemoizedFn((reviewInfo: AIChatQSData) => { - if (reviewInfo.type === AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE && reviewInfo.data.optionValue === 'continue') { - // plan_review, 选择是continue选项, 则进行UI任务树的生成 - const tasks = reviewInfo.data - const plans = genExecTasks(tasks.plans.root_task) - setPlan({ - task_tree: cloneDeep(plans), - root_task_name: tasks.plans.root_task.name, - }) - } - }) - - // #endregion - - /** 处理数据方法 */ - const handleSetData = useMemoizedFn((res: AIOutputEvent) => {}) - - const handleResetData = useMemoizedFn(() => { - handleResetPlanTree() - handleResetActiveLeafTasks() - setElements([]) - }) - - /** review 界面选项触发事件 */ - const handleSend: handleSendFunc = useMemoizedFn(({ request, optionValue, cb }) => {}) - - /** grpc接口关闭后的后续处理逻辑 */ - const handleCloseGrpc = useMemoizedFn(() => { - // 将进行中的任务变更为中止状态 - handleAbortTaskState() - }) - - /** 当前任务规划结束-触发UI展示结束标识 */ - const handlePlanExecEnd = useMemoizedFn((res: AIOutputEvent) => {}) - - /** 用户手动介入逻辑 */ - const handleUserManualIntervention = useMemoizedFn((chatInfo: AIChatQSData) => { - try { - setContentMap(chatInfo.id, cloneDeep(chatInfo)) - setElements((old) => [ - ...old, - { token: chatInfo.id, type: chatInfo.type, renderNum: 1, chatType: 'task', kind: 'item' }, - ]) - } catch (error) { - yakitNotify('error', `用户手动干预操作失败: ${error}`) - } - }) - - const state: UseTaskChatState = useCreation(() => { - return { plan, elements } - }, [plan, elements]) - - const events: UseTaskChatEvents = useCreation(() => { - return { - handleSetData, - handleResetData, - handleSend, - handleCloseGrpc, - handlePlanExecEnd, - getContentMap, - setContentMap, - setElements: setElements, - getElements: getElements, - handleUserManualIntervention, - handleResetPlanTree, - } - }, []) - - return [state, events] as const -} - -export default useTaskChat From c7234679ee060fa9ba626797bdf249b0b91ef840 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Wed, 22 Jul 2026 09:54:36 +0800 Subject: [PATCH 046/101] =?UTF-8?q?refactor:=20=E4=BB=A5=20route+pageId=20?= =?UTF-8?q?=E5=BD=92=E5=B1=9E=20AI=20session=EF=BC=8C=E7=BB=9F=E4=B8=80=20?= =?UTF-8?q?useChatIPC=20=E5=B9=B6=E6=94=AF=E6=8C=81=E6=8D=A2=E7=BB=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用 ChatMultiSessionController 双索引管理会话归属,替换 useNewChatIPC;各业务页显式传入 route/pageId,暴露 onUpdatePageId 按 session 换绑。 --- .../src/main/src/components/HTTPHistory.tsx | 14 +- .../src/components/withHistoryAIReActChat.tsx | 48 +-- app/renderer/src/main/src/enums/yakitRoute.ts | 2 + .../compoment/KnowledgeBaseContent.tsx | 2 + .../src/main/src/pages/ai-agent/AIAgent.tsx | 6 +- .../aiMemoryList/AIMemoryList.tsx | 7 +- .../ai-agent/historyChat/HistoryChat.tsx | 21 +- .../HistoryChatList/HistoryChatList.tsx | 13 +- .../src/pages/ai-agent/historyChat/utils.ts | 14 +- .../main/src/pages/ai-agent/type/aiChat.d.ts | 4 +- .../ai-agent/useContext/AIAgentContext.ts | 14 +- .../hooks/ChatMultiSessionController.ts | 304 +++++++++++++++--- .../src/pages/ai-re-act/hooks/useChatIPC.ts | 49 +++ .../pages/ai-re-act/hooks/useNewChatIPC.ts | 63 ---- .../main/src/pages/fuzzer/HTTPFuzzerPage.tsx | 3 +- .../HTTPHistory/HTTPHistoryFilter.tsx | 7 +- .../irifyAiCodeAudit/IrifyAiCodeAuditPage.tsx | 3 + .../main/src/pages/yakRunner/YakRunner.tsx | 2 + 18 files changed, 407 insertions(+), 169 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts diff --git a/app/renderer/src/main/src/components/HTTPHistory.tsx b/app/renderer/src/main/src/components/HTTPHistory.tsx index aacdf71557..b636602bc6 100644 --- a/app/renderer/src/main/src/components/HTTPHistory.tsx +++ b/app/renderer/src/main/src/components/HTTPHistory.tsx @@ -24,7 +24,6 @@ import { OutlineFilterIcon, OutlineLog2Icon, OutlineMessageCirclePlusIcon, - OutlinePlusIcon, OutlineSearchIcon, OutlineXIcon, } from '@/assets/icon/outline' @@ -63,9 +62,9 @@ import { } from '@/assets/commonProcessIcons' import { YakitSpin } from './yakitUI/YakitSpin/YakitSpin' import { YakitButton } from './yakitUI/YakitButton/YakitButton' -import { ClockIcon, RefreshIcon } from '@/assets/newIcon' +import { RefreshIcon } from '@/assets/newIcon' import { Tooltip } from 'antd' -import { AIInputFooterRightEnum, AIInputInnerFeatureEnum } from '@/pages/ai-agent/template/type' +import { AIInputFooterRightEnum } from '@/pages/ai-agent/template/type' import { YakitCheckbox } from './yakitUI/YakitCheckbox/YakitCheckbox' import ReactResizeDetector from 'react-resize-detector' import { RemoteHistoryGV } from '@/enums/history' @@ -78,7 +77,6 @@ import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { YakitSideTab } from './yakitSideTab/YakitSideTab' import { YakitTabsProps } from './yakitSideTab/YakitSideTabType' import { JSONParseLog } from '@/utils/tool' -import { histroyAiStore } from '@/pages/ai-agent/store/ChatDataStore' import { HistoryAIReActChatProvider, useHistoryAIReActChat } from './historyAIReActChat' import YakitCollapse from './yakitUI/YakitCollapse/YakitCollapse' import { YakitPopover } from './yakitUI/YakitPopover/YakitPopover' @@ -88,6 +86,7 @@ import { HTTPFlowRuleDataFilter } from './HTTPFlowTable/HTTPFlowRuleDataFilter' import { useCampare } from '@/hook/useCompare/useCompare' import { useBuiltinTagList } from './HTTPFlowTable/useBuiltinTagList' import { AISourceEnum } from '@/pages/ai-re-act/hooks/grpcApi' +import { YakitRoute } from '@/enums/yakitRoute' const { ipcRenderer } = window.require('electron') const { YakitPanel } = YakitCollapse @@ -445,7 +444,12 @@ const HTTPHistoryInner: React.FC = (props) => { export const HTTPHistory: React.FC = (props) => { return ( - + ) diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index ad96e9de30..b3d6792b90 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -22,6 +22,7 @@ import { AISendResProps, } from '@/pages/ai-re-act/aiReActChat/AIReActChatType' import { AIAgentGrpcApi, AIInputEvent, AISource } from '@/pages/ai-re-act/hooks/grpcApi' +import type { YakitRouteType } from '@/enums/yakitRoute' import { applyHttpFuzzRequestChangeToWebFuzzerPage, getWebFuzzerPageIsHttps, @@ -45,7 +46,7 @@ import useGetSetState from '@/pages/pluginHub/hooks/useGetSetState' import emiter from '@/utils/eventBus/eventBus' import { HistroryAIReActChat } from './HistroryAIReActChat' -import { useChatIPC } from '@/pages/ai-re-act/hooks/useNewChatIPC' +import { useChatIPC } from '@/pages/ai-re-act/hooks/useChatIPC' import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' @@ -171,10 +172,14 @@ function normalizeStartUserQueryToTextDescription(event: AIInputEvent): AIInputE export interface HistoryAIReActChatProviderProps { source: AISource + /** 会话归属路由(不可变) */ + route: YakitRouteType + /** 会话当前归属 pageId(可变,可 rebind) */ + pageId: string focusModeLoop: HistoryAIReActFocusModeLoop children: React.ReactNode /** Web Fuzzer 页签 id:AI 改包回写、请求附件、fuzz 状态推送等桥接用,与 SessionID 无关 */ - httpFuzzTabPageId?: string + // httpFuzzTabPageId?: string /** Yak Runner 工作区 id:AI `yaklang_code_change` 审阅/写回桥接 */ yakRunnerPageId?: string /** @@ -191,9 +196,10 @@ export interface HistoryAIReActChatProviderProps { /** TODO - @whale 修改确认 */ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProviderInner({ source, + route, + pageId, focusModeLoop, children, - httpFuzzTabPageId, yakRunnerPageId, transformInputEvent, resolveStartExtraParams, @@ -230,13 +236,13 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid }) const onHttpFuzzRequestChange = useMemoizedFn((data: AIAgentGrpcApi.HttpFuzzRequestChange) => { - if (!httpFuzzTabPageId) return + if (!pageId) return // casual 问答期间:有完整 raw 时不自动写包,入队审阅(`op` 仅占位描述,不作为筛选项) if (casualLoadingRef.current) { const nextRaw = data?.request?.raw if (nextRaw != null && String(nextRaw).trim() !== '' && initialRequestInCasualRef.current != null) { - enqueueWebFuzzerCasualReplaceReview(httpFuzzTabPageId, { + enqueueWebFuzzerCasualReplaceReview(pageId, { original: initialRequestInCasualRef.current ?? '', change: data, }) @@ -244,7 +250,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid } } - applyHttpFuzzRequestChangeToWebFuzzerPage(httpFuzzTabPageId, data) + applyHttpFuzzRequestChangeToWebFuzzerPage(pageId, data) }) const onYaklangCodeChange = useMemoizedFn((data: AIAgentGrpcApi.YaklangCodeChange) => { @@ -294,20 +300,21 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid // AI `http_flow_fuzz_status` 推送:把每次最新的 `runtime_id` 静默推到当前 fuzzer 页签的处理器中。 // 用户点击「查看详情」会显式再次推送并要求打开抽屉,所以这里不主动打开。 const onGetHttpFlowFuzzStatus = useMemoizedFn((data: AIAgentGrpcApi.GetHttpFlowFuzzStatus) => { - if (!httpFuzzTabPageId) return + if (!pageId) return const runtimeId = data?.runtime_id if (!runtimeId) return - pushAIFuzzStatusRuntimeIdToWebFuzzerPage(httpFuzzTabPageId, runtimeId, { source: 'auto' }) + pushAIFuzzStatusRuntimeIdToWebFuzzerPage(pageId, runtimeId, { source: 'auto' }) }) - const { onStart, onSend, onClose } = useChatIPC() + const { onStart, onSend, onClose, onUpdatePageId } = useChatIPC(route, pageId) const store = useCurrentStore() const execute = useStore(store, (state) => state.execute) const casualLoading = useStore(store, (state) => state.casualLoading) + // TODO - @whale 修改确认 useEffect => httpFuzzTabPageId->pageId useEffect(() => { - if (!httpFuzzTabPageId && !yakRunnerPageId) { + if (!pageId && !yakRunnerPageId) { casualLoadingRef.current = false initialRequestInCasualRef.current = null initialCodeInCasualRef.current = null @@ -315,8 +322,8 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid } if (!casualLoadingRef.current && casualLoading) { - if (httpFuzzTabPageId) { - initialRequestInCasualRef.current = getWebFuzzerPageRequestString(httpFuzzTabPageId) ?? '' + if (pageId) { + initialRequestInCasualRef.current = getWebFuzzerPageRequestString(pageId) ?? '' } if (yakRunnerPageId) { resetYakRunnerPatchWorkingDraft(yakRunnerPageId) @@ -331,7 +338,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid } casualLoadingRef.current = casualLoading - }, [casualLoading, httpFuzzTabPageId, yakRunnerPageId]) + }, [casualLoading, pageId, yakRunnerPageId]) const activeID = useCreation(() => { return activeChat?.SessionID @@ -344,9 +351,10 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid return new Promise((resolve) => { let params: AIInputEvent = { ...data.params, FocusModeLoop: focusModeLoop } - if (httpFuzzTabPageId) { - const raw = getWebFuzzerPageRequestString(httpFuzzTabPageId) - const isHttps = getWebFuzzerPageIsHttps(httpFuzzTabPageId) ?? false + // TODO - @whale 修改确认 httpFuzzTabPageId->pageId + if (pageId) { + const raw = getWebFuzzerPageRequestString(pageId) + const isHttps = getWebFuzzerPageIsHttps(pageId) ?? false const sessionId = data.params.Params?.TimelineSessionID || activeChat?.SessionID || getSetting().TimelineSessionID params = attachWebFuzzerHttpRequestToEvent(params, sessionId, raw, isHttps) @@ -398,9 +406,10 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const onSendRequest = useMemoizedFn((data: AISendParams) => { let params: AIInputEvent = { ...data.params, FocusModeLoop: focusModeLoop } - if (httpFuzzTabPageId) { - const raw = getWebFuzzerPageRequestString(httpFuzzTabPageId) - const isHttps = getWebFuzzerPageIsHttps(httpFuzzTabPageId) ?? false + // TODO - @whale 修改确认 httpFuzzTabPageId->pageId + if (pageId) { + const raw = getWebFuzzerPageRequestString(pageId) + const isHttps = getWebFuzzerPageIsHttps(pageId) ?? false params = attachWebFuzzerHttpRequestToEvent(params, activeChat?.SessionID, raw, isHttps) } if (transformInputEvent) { @@ -467,6 +476,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid onStart, onSend, onClose, + onUpdatePageId, } }, []) diff --git a/app/renderer/src/main/src/enums/yakitRoute.ts b/app/renderer/src/main/src/enums/yakitRoute.ts index a44e2a5490..d9531f866e 100644 --- a/app/renderer/src/main/src/enums/yakitRoute.ts +++ b/app/renderer/src/main/src/enums/yakitRoute.ts @@ -131,3 +131,5 @@ export enum YakitRoute { /** 技能 */ AI_Forge = 'ai-forge', } + +export type YakitRouteType = `${YakitRoute}` diff --git a/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx b/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx index a0d962dd9a..1d9fa130d0 100644 --- a/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx +++ b/app/renderer/src/main/src/pages/KnowledgeBase/compoment/KnowledgeBaseContent.tsx @@ -684,6 +684,8 @@ const KnowledgeBaseContent = forwardRef(func return ( = (props) => { setRemoteValue(RemoteAIAgentGV.AIAgentChatSetting, JSON.stringify(cache)) }, [setting]) - const { onStart, onSend, onClose } = useChatIPC() + const { onStart, onSend, onClose, onUpdatePageId } = useChatIPC(YakitRoute.AI_Agent, YakitRoute.AI_Agent) const store: AIAgentContextStore = useMemo(() => { return { @@ -112,6 +113,7 @@ export const AIAgent: React.FC = (props) => { onStart, onSend, onClose, + onUpdatePageId, } }, []) diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx index efdb3cf5f2..7d3142eb4b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/aiMemoryList/AIMemoryList.tsx @@ -21,7 +21,6 @@ import useAIAgentDispatcher from '../../useContext/useDispatcher' import cloneDeep from 'lodash/cloneDeep' import { DefaultMemoryList } from '@/pages/ai-re-act/hooks/defaultConstant' -const { ipcRenderer } = window.require('electron') const getScoreList = (data: AIAgentGrpcApi.MemoryEntry) => { return [ { @@ -75,14 +74,10 @@ const AIMemoryList: React.FC = React.memo((props) => { useDebounceEffect( () => { if (!isClearMemory) return - /** TODO - 需要hooks告诉UI层,此次关闭的type(来源) - * 二次讨论:改为onClose中传回调事件 - */ - ipcRenderer.once(`${sessionId}-end`, () => { + onClose([sessionId], () => { onClearMemory() setIsClearMemory(false) }) - onClose([sessionId]) }, [isClearMemory], { wait: 1000 }, diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx index 0649490744..8e6ed82218 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx @@ -22,6 +22,7 @@ import HistoryChatList, { DAY_MS, getChatTimestamp } from './HistoryChatList/His import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import useSessionList from './HistoryChatList/hook/useSessionList' import { type AISource } from '@/pages/ai-re-act/hooks/grpcApi' +import type { YakitRouteType } from '@/enums/yakitRoute' import { JSONParseLog } from '@/utils/tool' import { getMainOperatorPageBodyContainer } from '@/utils/getMainOperatorPageBodyContainer' import { handAIHistoryChatRemove } from './utils' @@ -31,6 +32,8 @@ import { filterHistorySessionsBySource, getHistorySourceQuerySources, type Histo import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' import useMemoizedFn from 'ahooks/lib/useMemoizedFn' import useDebounce from 'ahooks/lib/useDebounce' +import { usePageInfo } from '@/store/pageInfo' +import { shallow } from 'zustand/shallow' const HISTORY_SOURCE_FILTER_OPTIONS: { key: HistorySourceFilter @@ -107,6 +110,10 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { }, [aiSource, enableHistorySourceFilter, historySourceFilter]) const [{ sessions }, dispatcher] = useSessionList(historyQuerySources) const { activeChat } = useAIAgentStore() + + const currentRouteKey = usePageInfo((state) => state.getCurrentPageTabRouteKey(), shallow) + const currentPageId = usePageInfo((state) => state.getCurrentSelectPageId(state.getCurrentPageTabRouteKey()), shallow) + const getPopupContainer = useMemoizedFn(() => getMainOperatorPageBodyContainer() || document.body) const popupContainer = embedded ? getPopupContainer : undefined const embeddedOverlayClass = styles['history-chat-embedded-overlay'] @@ -143,9 +150,11 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { await handAIHistoryChatRemove({ grpcDeleteAISessionParams: filter, handleClearAIImageParams: { chatDataStoreKey, sessionID: [] }, //删除全部只需要传chatDataStoreKey - forceCloseSessionParams: { - aiSource: source, + deleteSessionsParams: { + source, sessionIds: [], + route: currentRouteKey as YakitRouteType, + pageId: currentPageId || currentRouteKey, }, }) onClose([]) @@ -190,9 +199,11 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { await handAIHistoryChatRemove({ grpcDeleteAISessionParams: { Filter: filter }, handleClearAIImageParams: { chatDataStoreKey: getImageStoreKeyByAISource(source), sessionID: sessionIds }, - forceCloseSessionParams: { - aiSource: source, - sessionIds: sessionIds, + deleteSessionsParams: { + source, + sessionIds, + route: currentRouteKey as YakitRouteType, + pageId: currentPageId || currentRouteKey, }, }) onClose(sessionIds) diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx index a932ac4514..4dc3a117a3 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx @@ -21,6 +21,9 @@ import type { AISource } from '@/pages/ai-re-act/hooks/grpcApi' import { getHistorySessionIconMeta, getSessionDisplayTitle } from '../source' import { handAIHistoryChatRemove } from '../utils' import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' +import { usePageInfo } from '@/store/pageInfo' +import { shallow } from 'zustand/shallow' +import type { YakitRouteType } from '@/enums/yakitRoute' export const HOUR_MS = 60 * 60 * 1000 export const DAY_MS = 24 * HOUR_MS @@ -126,6 +129,8 @@ const HistoryChatList: FC<{ const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const { activeChat } = useAIAgentStore() const { setActiveChat, setSetting } = useAIAgentDispatcher() + const currentRouteKey = usePageInfo((state) => state.getCurrentPageTabRouteKey(), shallow) + const currentPageId = usePageInfo((state) => state.getCurrentSelectPageId(state.getCurrentPageTabRouteKey()), shallow) const listRef = useRef(null) const chatTotalRef = useRef(0) const editInfo = useRef() @@ -229,9 +234,11 @@ const HistoryChatList: FC<{ await handAIHistoryChatRemove({ grpcDeleteAISessionParams: { Filter: { SessionID: [SessionID], Source: aiSource } }, handleClearAIImageParams: { chatDataStoreKey, sessionID: sessionIds }, - forceCloseSessionParams: { - aiSource: source, - sessionIds: sessionIds, + deleteSessionsParams: { + source, + sessionIds, + route: currentRouteKey as YakitRouteType, + pageId: currentPageId || currentRouteKey, }, }) } catch (error) { diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts b/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts index 75ec70a7a9..f0b961ac37 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts @@ -6,14 +6,20 @@ import { import { grpcDeleteAISession } from '../grpc' import { DeleteAISessionRequest } from '../type/aiChat' import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' +import type { YakitRouteType } from '@/enums/yakitRoute' export interface HandAIHistoryChatRemoveParams { /** 删除grpc数据 */ grpcDeleteAISessionParams: DeleteAISessionRequest /** 删除图片数据 */ handleClearAIImageParams: AIClearImageParams - /** 删除store缓存数据 */ - forceCloseSessionParams: { sessionIds: string[]; aiSource: AISource } + /** 删除 Controller 内存中的 session(含双索引与业务池) */ + deleteSessionsParams: { + sessionIds: string[] + source: AISource + route: YakitRouteType + pageId: string + } } /** * @description 删除历史会话数据 @@ -24,13 +30,13 @@ export interface HandAIHistoryChatRemoveParams { */ export const handAIHistoryChatRemove = async (params: HandAIHistoryChatRemoveParams) => { try { - const { grpcDeleteAISessionParams, handleClearAIImageParams, forceCloseSessionParams } = params + const { grpcDeleteAISessionParams, handleClearAIImageParams, deleteSessionsParams } = params // 1.删除grpc数据 await grpcDeleteAISession(grpcDeleteAISessionParams, true) // 2.删除图片数据 handleClearAIImage(handleClearAIImageParams) // 3.删除store缓存数据 - globalSessionEngine.forceCloseSession(forceCloseSessionParams) + globalSessionEngine.deleteSessions(deleteSessionsParams) // TODO - 4.删除indexdb } catch (error) {} } diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 48758aecda..08165c9502 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -219,7 +219,9 @@ export interface AIAgentChatData { } export interface AIAgentChatMetaData { /** 会话通信流建立成功后的UI回调触发事件 */ - onSessionStartSuccess?: (sessionId: string) => void + onLinkSuccess?: (sessionId: string) => void + /** forceClose 传入的回调,在 grpc session-end 移除监听前执行 */ + onEnd?: () => void /** 通过用户问题创建会话时的问题 */ createChatQuestion?: AIInputEvent diff --git a/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts b/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts index c12646241f..d1180170bd 100644 --- a/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts +++ b/app/renderer/src/main/src/pages/ai-agent/useContext/AIAgentContext.ts @@ -3,6 +3,7 @@ import { AIAgentSetting } from '../aiAgentType' import { AISession } from '../type/aiChat' import { AIChatIPCStartParams, AIChatSendParams } from '@/pages/ai-re-act/hooks/type' import { AIAgentSettingDefault } from '../defaultConstant' +import { useChatIPC } from '@/pages/ai-re-act/hooks/useChatIPC' export interface AIAgentContextStore { /** 全局配置 */ @@ -11,20 +12,14 @@ export interface AIAgentContextStore { activeChat?: AISession } -export interface UseChatIPCStartParams extends AIChatIPCStartParams { +/** 上层 onStart 入参:route/pageId 由 useChatIPC 注入,调用方无需传递 */ +export interface UseChatIPCStartParams extends Omit { onSuccess?: (sessionId: string) => void } -export interface AIAgentContextDispatcher { +export interface AIAgentContextDispatcher extends ReturnType { setSetting: Dispatch> getSetting: () => AIAgentSetting setActiveChat: Dispatch> - - /** 开始会话 */ - onStart: (params: UseChatIPCStartParams) => void - /** 会话过程中向后端发送消息 */ - onSend: (params: AIChatSendParams) => void - /** 关闭指定会话 */ - onClose: (sessionId: string[]) => void } export interface AIAgentContextValue { @@ -45,5 +40,6 @@ export default createContext({ onStart: () => {}, onSend: () => {}, onClose: () => {}, + onUpdatePageId: () => {}, }, }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 6bc36ef58f..5904253e42 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -25,6 +25,7 @@ import { type AIChatQSData, AIChatQSDataTypeEnum, AIToolResult, ChatListRenderTy import { aiAgentLogEmitter } from './AIAgentLogEmitter' import { v4 as uuidv4 } from 'uuid' import moment from 'moment' +import type { YakitRouteType } from '@/enums/yakitRoute' const { ipcRenderer } = window.require('electron') @@ -115,6 +116,7 @@ const genAIAgentChatData = (): AIAgentChatData => { const genAIAgentChatMetaData = (): AIAgentChatMetaData => { return { createChatQuestion: undefined, + onEnd: undefined, pingSyncID: '', pingTimer: null, casualMemoryList: cloneDeep(DefaultMemoryList), @@ -138,20 +140,193 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { } } +/** page 归属键:`${route}::${pageId}`,pageId 为当前归属 */ +type PageKey = string + +interface SessionOwner { + /** 不可变:注册后锁死 */ + readonly route: YakitRouteType + /** 不可变:注册后锁死 */ + readonly source: AISource + /** 可变:始终存当前 page */ + pageId: string +} + +/** 生成 route::pageId 的唯一标识 */ +const makePageKey = (route: YakitRouteType, pageId: string): PageKey => `${route}::${pageId}` + export class ChatMultiSessionController { - // 存放各个页面下的session集合 - private pageSessionMap = new Map>() + /** + * 正向索引:按「当前」page 关页 / 全删 + * pageId 换绑后旧 PageKey 不再包含该 session + */ + private pageSessionMap = new Map>>() + /** + * 反向索引:按 sessionId O(1) 定位;换绑时只改 pageId 并搬动正向索引 + */ + private sessionOwnerMap = new Map() + /** 存放已建立连接的会话session集合 */ + private readyChannels = new Set() + + // #region session-source-route-pageId 索引管理相关逻辑 + /** 将 session 写入 pageSessionMap 正向索引 */ + private addToPageSessionMap(owner: SessionOwner, sessionId: string) { + const pageKey = makePageKey(owner.route, owner.pageId) + let sourceMap = this.pageSessionMap.get(pageKey) + if (!sourceMap) { + sourceMap = new Map() + this.pageSessionMap.set(pageKey, sourceMap) + } + let sessionSet = sourceMap.get(owner.source) + if (!sessionSet) { + sessionSet = new Set() + sourceMap.set(owner.source, sessionSet) + } + sessionSet.add(sessionId) + } + + /** 从 pageSessionMap 正向索引摘除;空 Set/Map 则清理 */ + private removeFromPageSessionMap(owner: SessionOwner, sessionId: string) { + const pageKey = makePageKey(owner.route, owner.pageId) + const sourceMap = this.pageSessionMap.get(pageKey) + if (!sourceMap) return + + const sessionSet = sourceMap.get(owner.source) + if (!sessionSet) return + + sessionSet.delete(sessionId) + if (sessionSet.size === 0) { + sourceMap.delete(owner.source) + } + if (sourceMap.size === 0) { + this.pageSessionMap.delete(pageKey) + } + } + + /** + * session 会话建立时注册归属索引,并标识当前会话已建立连接 + * + * - route / source 建立后不可变 + * - 若 session 已存在且仅 pageId 不同,走 rebind 而非重复注册 + */ + public registerSessionChannel( + sessionId: string, + owner: { route: YakitRouteType; pageId: string; source?: AIStartParams['Source'] }, + ) { + const source: AISource = owner.source || 'ai' + const existing = this.sessionOwnerMap.get(sessionId) + + if (existing) { + // 禁止改 route / source + if (existing.route !== owner.route || existing.source !== source) { + console.error(`[ChatMultiSessionController] registerSessionChannel: session 已存在且 route/source 不可变`, { + sessionId, + existing, + next: { route: owner.route, pageId: owner.pageId, source }, + }) + this.readyChannels.add(sessionId) + return + } + // 仅 pageId 不同 → rebind + if (existing.pageId !== owner.pageId) { + this.rebindSessionPageId(sessionId, owner.pageId) + } + this.readyChannels.add(sessionId) + return + } + + const sessionOwner: SessionOwner = { + route: owner.route, + source, + pageId: owner.pageId, + } + this.sessionOwnerMap.set(sessionId, sessionOwner) + this.addToPageSessionMap(sessionOwner, sessionId) + this.readyChannels.add(sessionId) + } + + /** + * 同 route 下换绑 pageId:更新 sessionOwnerMap.pageId,从旧 PageKey 摘除、写入新 PageKey + * route / source 不变;newPageId 与旧相同或 session 已 dispose 则 no-op + */ + public rebindSessionPageId(sessionId: string, newPageId: string) { + const owner = this.sessionOwnerMap.get(sessionId) + if (!owner || owner.pageId === newPageId) return + + this.removeFromPageSessionMap(owner, sessionId) + owner.pageId = newPageId + this.addToPageSessionMap(owner, sessionId) + } + + /** 全删:用目标 route + pageId + source 查当前索引;非空 sessionIds 则直接用集合 */ + private resolveSessionIds(params: { + sessionIds?: string[] + source: AISource + route: YakitRouteType + pageId: string + }): string[] { + const { sessionIds, source, route, pageId } = params + if (sessionIds?.length) return [...sessionIds] + return [...(this.pageSessionMap.get(makePageKey(route, pageId))?.get(source) ?? [])] + } + + /** 该 PageKey 下所有 source 的 session 并集 */ + private resolvePageSessionIds(route: YakitRouteType, pageId: string): string[] { + const sourceMap = this.pageSessionMap.get(makePageKey(route, pageId)) + if (!sourceMap) return [] + const ids: string[] = [] + for (const sessionSet of sourceMap.values()) { + for (const id of sessionSet) { + ids.push(id) + } + } + return ids + } + + /** + * 删除内存数据(仅 deleteSessions / onPageUnload 调用) + * 1. 先 forceClose(停流,幂等;关闭 ≠ 删除) + * 2. 主动摘 IPC(删除不等 end)并清业务池与归属索引 + */ + private disposeSessionMemory(sessionId: string) { + this.forceCloseSession({ sessionIds: [sessionId] }) + // 删除路径不等 session-end,主动摘监听,避免 end 回来 ensureSession 重建池 + this.closeIPCListeners(sessionId) + this.readyChannels.delete(sessionId) + + if (this.activeShowSession === sessionId) { + this.activeShowSession = null + } + + this.requestPool.delete(sessionId) + this.storePool.delete(sessionId) + this.rawDataPool.delete(sessionId) + this.metaPool.delete(sessionId) + + const owner = this.sessionOwnerMap.get(sessionId) + if (owner) { + this.removeFromPageSessionMap(owner, sessionId) + this.sessionOwnerMap.delete(sessionId) + } + } + // #endregion + + /** 存放当前正在展示的会话session */ + private activeShowSession: string | null = null + /** 设置当前展示的会话 Session */ + public setActiveShowSession(sessionId: string) { + this.activeShowSession = sessionId + } + /** 判断指定会话是否当前正在展示 */ + public isActiveShowSession(sessionId: string) { + return this.activeShowSession === sessionId + } private requestPool = new Map() private storePool = new Map>() private rawDataPool = new Map() private metaPool = new Map() - /** 存放所有已建立连接的会话session */ - private readyChannels = new Set() - /** 存放当前正在展示的会话session */ - private activeShowSession: string | null = null - /** 获取对应会话的所有数据集 */ public ensureSession(sessionId: string) { if (!this.storePool.has(sessionId)) { @@ -168,27 +343,6 @@ export class ChatMultiSessionController { } } - /** session会话建立时,注册进队列中,标识当前会话已建立连接 */ - public registerSessionChannel(sessionId: string, source: AIStartParams['Source']) { - this.readyChannels.add(sessionId) - - if (this.pageSessionMap.has(source || 'ai')) { - this.pageSessionMap.get(source || 'ai')?.add(sessionId) - } else { - const sessionSet = new Set([sessionId]) - this.pageSessionMap.set(source || 'ai', sessionSet) - } - } - - /** 设置当前展示的会话 Session */ - public setActiveShowSession(sessionId: string) { - this.activeShowSession = sessionId - } - /** 判断指定会话是否当前正在展示 */ - public isActiveShowSession(sessionId: string) { - return this.activeShowSession === sessionId - } - /** * 更新指定会话的配置参数 * @@ -202,7 +356,7 @@ export class ChatMultiSessionController { /** 建立指定session会话的连接 */ public handleStartSession(requestParams: AIChatIPCStartParams, cb?: (sessionId: string) => void) { - const { token: sessionId, params } = requestParams + const { token: sessionId, params, route, pageId } = requestParams const isExec = this.readyChannels.has(sessionId) if (isExec) { yakitNotify('warning', '会话已经存在,请勿重复建立!') @@ -212,7 +366,11 @@ export class ChatMultiSessionController { const { request, store, rawData, meta } = this.ensureSession(sessionId) store.getState().updateState({ execute: true, casualTitle: '发送问题,开启会话...' }) - this.registerSessionChannel(sessionId, params.Params?.Source) + this.registerSessionChannel(sessionId, { + route, + pageId, + source: params.Params?.Source, + }) this.setActiveShowSession(sessionId) Object.assign(request, params.Params) @@ -259,7 +417,7 @@ export class ChatMultiSessionController { }, }) } - meta.onSessionStartSuccess = cb + meta.onLinkSuccess = cb ipcRenderer.invoke('start-ai-re-act', sessionId, params) @@ -532,8 +690,8 @@ export class ChatMultiSessionController { // TODO - 调用历史数据恢复方法 } this.handleSessionStartSuccess(sessionId) - meta.onSessionStartSuccess?.(sessionId) - meta.onSessionStartSuccess = undefined + meta.onLinkSuccess?.(sessionId) + meta.onLinkSuccess = undefined return } @@ -631,14 +789,24 @@ export class ChatMultiSessionController { * 一般来说,触发这个事件的情况,都是当前review数据无效了 * 别的处理review数据事件,都由 handleSendMessage 进行处理了 */ - public closeChatReview(sessionId: string, chatType: ChatListRenderType, reviewToken: string) { + public closeChatReview(sessionId: string, reviewToken: string) { const { store, rawData } = this.ensureSession(sessionId) + const reviewDetail = rawData.contents.get(reviewToken) + if (!reviewDetail) { + yakitNotify('warning', '未获取到 review 信息, 操作无效') + return + } - if (chatType === 'reAct') { - const reviewDetail = rawData.contents.get(reviewToken) + if (reviewDetail.chatType === 'reAct') { rawData.contents.delete(reviewToken) - store.getState().updateCasualReview(reviewToken, 'remove') - if (reviewDetail) { + if ( + reviewDetail.type === AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE && + store.getState().currentPlanReviewToken.token === reviewDetail.id + ) { + // 该类型在任务规划的review弹窗显示,需要清空当前任务规划的review + store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) + } else { + store.getState().updateCasualReview(reviewToken, 'remove') store.getState().deleteElementNode({ chatType: 'reAct', token: reviewDetail.id, @@ -649,9 +817,9 @@ export class ChatMultiSessionController { }, }) } - } else if (chatType === 'task') { + } else if (reviewDetail.chatType === 'task') { const currentReview = store.getState().currentPlanReviewToken - if (!currentReview.token || currentReview.token !== reviewToken) return + if (!currentReview.token || currentReview.token !== reviewDetail.id) return // 不用调用deleteElementNode,因为能触发这个方法的地方,说明review还没有进入list列表中 rawData.contents.delete(currentReview.token) @@ -713,26 +881,62 @@ export class ChatMultiSessionController { // 核心状态改变 store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) this.readyChannels.delete(sessionId) + + const onEnd = meta.onEnd + if (onEnd) { + onEnd() + meta.onEnd = undefined + } this.closeIPCListeners(sessionId) } - /** 关闭指定session的连接 - * TODO - 期望传一个对象,{ sessionIds: string[]; aiSource: AISource } - * TODO - 需要区分删除和关闭会话两种操作,不然数据的关闭和删除会混乱 + /** + * 关闭会话连接(停流) + * - cancel IPC、更新 execute;不摘 IPC 监听(交给 handleSessionEnd) + * - 有 onEnd 时写入 meta,在 session-end 移除监听前执行 + * - **不会**删除业务池与归属索引;关闭 ≠ 删除 */ - public forceCloseSession(params: { sessionIds: string[] }) { - const { sessionIds } = params - for (let session of sessionIds) { + public forceCloseSession(params: { sessionIds: string[]; onEnd?: () => void }) { + const { sessionIds, onEnd } = params + for (const session of sessionIds) { + const meta = this.metaPool.get(session) + if (meta && onEnd) { + meta.onEnd = onEnd + } ipcRenderer.invoke('cancel-ai-re-act', session).catch(() => {}) + const store = this.storePool.get(session) + if (store) { + store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) + } + if (meta) this.closeSessionTimers(meta) + } + } + + /** + * 删除指定的 session(必须清除内存数据) + * - sessionIds 非空:删集合 + * - sessionIds 空数组:按当前归属全删该 page 下该 source + * - pageId 为空:按当前归属全删该 route 下所有 page 的 session + * - 内部会先 forceClose 再卸业务池与双索引 + * - grpc / IndexedDB 删除由上层负责 + */ + public deleteSessions(params: { sessionIds: string[]; source: AISource; route: YakitRouteType; pageId: string }) { + const ids = this.resolveSessionIds(params) + for (const sessionId of ids) { + this.disposeSessionMemory(sessionId) } } /** - * 删除指定的session会话 - * sessionIds 需要删除的session集合,如果需要全删,这该值传空数组 - * source 需要删除的session来源页面 + * 页面生命周期卸载:卸该「当前」page 下所有 source 的 session 内存(同 delete,非仅 forceClose) + * 已 rebind 走的 session 不会被旧页清掉 */ - public deleteSessions(sessionIds: string[], source: AISource) {} + public onPageUnload(route: YakitRouteType, pageId: string) { + const ids = this.resolvePageSessionIds(route, pageId) + for (const sessionId of ids) { + this.disposeSessionMemory(sessionId) + } + } } export const globalSessionEngine = new ChatMultiSessionController() diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts new file mode 100644 index 0000000000..2a2368dd76 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts @@ -0,0 +1,49 @@ +// useChatIPC.ts +import { useEffect } from 'react' +import { globalSessionEngine } from './ChatMultiSessionController' +import type { AIChatSendParams } from './type' +import { useMemoizedFn } from 'ahooks' +import type { UseChatIPCStartParams } from '@/pages/ai-agent/useContext/AIAgentContext' +import type { YakitRouteType } from '@/enums/yakitRoute' +const { ipcRenderer } = window.require('electron') + +export function useChatIPC(route: YakitRouteType, pageId: string) { + // IPC 监听的 channel 用入参 token 注册,与 handleStartSession 的 token 完全对齐 + // 这样在欢迎页"从无到有建立会话"时,不会出现 setActiveChat 异步未生效、闭包 sessionId 仍为空导致监听错位的问题 + const onStart = useMemoizedFn(({ token, params, onSuccess }: UseChatIPCStartParams) => { + // 监听网络,直接丢给大脑处理 + ipcRenderer.on(`${token}-data`, (e, res: any) => { + globalSessionEngine.handleGrpcOutputEvent(token, res) + }) + ipcRenderer.on(`${token}-error`, (e, res: any) => { + globalSessionEngine.handleSessionError(token, res) + }) + ipcRenderer.on(`${token}-end`, (e, res: any) => { + globalSessionEngine.handleSessionEnd(token, res) + }) + + globalSessionEngine.handleStartSession({ token, params, route, pageId }, onSuccess) + }) + + const onSend = useMemoizedFn((payload: AIChatSendParams) => { + globalSessionEngine.handleSendMessage(payload) + }) + + const onClose = useMemoizedFn((sessionIds: string[], onEnd?: () => void) => { + globalSessionEngine.forceCloseSession({ sessionIds, onEnd }) + }) + + /** 将指定 session 换绑到本 hook 入参 pageId(pageId 为定值) */ + const onUpdatePageId = useMemoizedFn((sessionId: string) => { + globalSessionEngine.rebindSessionPageId(sessionId, pageId) + }) + + // 组件卸载时拔插头,清理闭环 + useEffect(() => { + return () => { + globalSessionEngine.onPageUnload(route, pageId) + } + }, []) + + return { onStart, onSend, onClose, onUpdatePageId } +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts deleted file mode 100644 index b27ae44dc3..0000000000 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useNewChatIPC.ts +++ /dev/null @@ -1,63 +0,0 @@ -// useChatIPC.ts -import { useEffect, useCallback, useRef } from 'react' -import { globalSessionEngine } from './ChatMultiSessionController' -import type { AIChatSendParams } from './type' -import { useMemoizedFn } from 'ahooks' -import type { UseChatIPCStartParams } from '@/pages/ai-agent/useContext/AIAgentContext' - -import useCurrentSessionId from './useCurrentSessionId' -const { ipcRenderer } = window.require('electron') - -export function useChatIPC() { - const sessionId = useCurrentSessionId() - // TODO: 我需要知道这个hook所在的页面,从而在生命周期的卸载时期关闭所有会话 - const currentSessionIdRef = useRef(sessionId) - - useEffect(() => { - currentSessionIdRef.current = sessionId - }, [sessionId]) - - // 这里一定要使用useCallback,通过依赖的sessionId变化,将上一个sessionId的on监听事件进行闭包 - // 多个sessionId就会生成多个自己的监听闭包,从而实现多会话的独立监听 - const onStart = useCallback( - ({ token, params, onSuccess }: UseChatIPCStartParams) => { - // 监听网络,直接丢给大脑处理 - ipcRenderer.on(`${token}-data`, (e, res: any) => { - globalSessionEngine.handleGrpcOutputEvent(token, res) - }) - ipcRenderer.on(`${token}-error`, (e, res: any) => { - globalSessionEngine.handleSessionError(token, res) - }) - ipcRenderer.on(`${token}-end`, (e, res: any) => { - globalSessionEngine.handleSessionEnd(token, res) - }) - - console.log('onStart----------args', { sessionId, token, params }) - globalSessionEngine.handleStartSession({ token, params }, onSuccess) - }, - [sessionId], - ) - - const onSend = useMemoizedFn((payload: AIChatSendParams) => { - globalSessionEngine.handleSendMessage(payload) - }) - - const onClose = useMemoizedFn((sessionIds: string[]) => { - /** globalSessionEngine.forceCloseSession - * TODO - 需要AISource且支持多个会话关闭 - * TODO - sessionId传[]:清除source下的所有会话 - */ - // globalSessionEngine.forceCloseSession({ sessionIds }) - }) - - // 组件卸载时拔插头,清理闭环 - useEffect(() => { - return () => { - globalSessionEngine.forceCloseSession({ - sessionIds: [currentSessionIdRef.current], - }) - } - }, []) - - return { onStart, onSend, onClose } -} diff --git a/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx b/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx index cba369847a..5c2bd59260 100644 --- a/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx +++ b/app/renderer/src/main/src/pages/fuzzer/HTTPFuzzerPage.tsx @@ -3461,8 +3461,9 @@ const HTTPFuzzerPage: React.FC = (props) => { return ( diff --git a/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx b/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx index 1cde120bee..e3263949d9 100644 --- a/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx +++ b/app/renderer/src/main/src/pages/hTTPHistoryAnalysis/HTTPHistory/HTTPHistoryFilter.tsx @@ -424,7 +424,12 @@ HTTPHistoryFilterInner.displayName = 'HTTPHistoryFilterInner' export const HTTPHistoryFilter: React.FC = (props) => { return ( - + ) diff --git a/app/renderer/src/main/src/pages/irifyAiCodeAudit/IrifyAiCodeAuditPage.tsx b/app/renderer/src/main/src/pages/irifyAiCodeAudit/IrifyAiCodeAuditPage.tsx index 3e3b63d99a..084cc8231f 100644 --- a/app/renderer/src/main/src/pages/irifyAiCodeAudit/IrifyAiCodeAuditPage.tsx +++ b/app/renderer/src/main/src/pages/irifyAiCodeAudit/IrifyAiCodeAuditPage.tsx @@ -5,6 +5,7 @@ import { AuditCodePageInfoProps } from '@/store/pageInfo' import { IrifyAiCodeAuditStyle, resolveIrifyFocusModeLoop, isIrifyAuditStyleConfirmed } from './irifyAiCodeAuditStyle' import { HistoryAIReActChatProvider } from '@/components/historyAIReActChat' import { AIInputEvent, AISourceEnum } from '@/pages/ai-re-act/hooks/grpcApi' +import { YakitRoute } from '@/enums/yakitRoute' import { IrifyAiCodeAuditSidePanelLayout } from './IrifyAiCodeAuditSidePanelLayout' import { appendIrifyWorkbenchAttachments, @@ -133,6 +134,8 @@ const IrifyAiCodeAuditPageInner: React.FC = ({ auditC return ( diff --git a/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx b/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx index fba38c66e4..149f631d60 100644 --- a/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx +++ b/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx @@ -1279,6 +1279,8 @@ const YakRunnerWithAIInner: React.FC = (props) => { return ( From 690793733734dfbd277b5faed02cd28720b96069 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 22 Jul 2026 11:07:28 +0800 Subject: [PATCH 047/101] =?UTF-8?q?del:=E5=88=A0=E9=99=A4onChatFromHistory?= =?UTF-8?q?=20feat:=E5=AD=90=E4=BB=BB=E5=8A=A1=E7=AA=97=E5=8F=A3=E5=A4=A7?= =?UTF-8?q?=E9=83=A8=E5=88=86=E5=9F=BA=E6=9C=AC=E5=AF=B9=E5=AE=8C;=20feat:?= =?UTF-8?q?=E9=83=A8=E5=88=86=E7=BB=84=E4=BB=B6=E6=93=8D=E4=BD=9C=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E5=A2=9E=E5=8A=A0=E5=AD=90=E7=AA=97=E5=8F=A3=E5=88=A4?= =?UTF-8?q?=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../openAIConcurrentStream/index.js | 6 +- .../AIConcurrentStream/AIConcurrentStream.tsx | 21 +-- .../fetchConcurrentStreamContents.ts | 16 +- .../src/components/withHistoryAIReActChat.tsx | 12 +- .../ai-agent/aiAgentChat/AIAgentChat.tsx | 3 +- .../ai-agent/aiChatContent/AIChatContent.tsx | 3 +- .../src/pages/ai-agent/aiChatContent/type.ts | 1 - .../AIChildWindowTaskDefaultGroupCard.tsx | 156 ------------------ .../AITaskDefaultGroupCardHeard.tsx | 4 +- .../AIChildWindowConcurrentStreamCard.tsx | 119 +------------ .../aiChildWindowConcurrentStreamCard/type.ts | 4 +- .../buildConcurrentStreamFramePayload.ts | 17 +- .../concurrentStreamMainBridge.ts | 6 +- .../concurrentStreamFrame.ts | 4 +- .../ai-agent/components/ToolInvokerCard.tsx | 24 +-- .../StaticChatContent/StaticChatContent.tsx | 6 +- .../aiModelErrorPrompt/AIModelErrorPrompt.tsx | 13 +- .../aiChatListItem/aiModelErrorPrompt/type.ts | 1 + .../aiChatListItem/aiNodeItem/AINodeItem.tsx | 28 ++-- .../aiChatListItem/aiNodeItem/type.ts | 2 + .../AIGroupStreamNodeWrapper.tsx | 34 ++++ .../aiGroupStreamNodeWrapper/type.ts | 8 + .../AIItemContentWrapper.module.scss | 0 .../AIItemContentWrapper.tsx | 2 +- .../aiItemContentWrapper/type.ts | 4 +- .../AIReviewResultWrapper.tsx | 15 ++ .../aiReviewResultWrapper/type.ts | 6 + .../aiStreamCardWrapper.tsx | 2 +- .../aiStreamCardWrapper/type.ts | 0 .../AIToolInvokerCardWrapper.tsx | 20 +++ .../aiToolInvokerCardWrapper/type.ts | 6 + ...dWindowConcurrentStreamContent.module.scss | 1 + .../AIChildWindowConcurrentStreamContent.tsx | 43 +++++ .../AIChildWindowGroupItem.tsx | 22 +++ .../aiChildWindowGroupItem/type.ts | 0 .../AIChildWindowGroupStreamCard.module.scss} | 1 - .../AIChildWindowGroupStreamCard.tsx | 32 ++++ .../aiChildWindowGroupStreamCard/type.ts | 3 + .../AIChildWindowGroupStreamNode.tsx | 33 ++++ .../aiChildWindowGroupStreamNode/type.ts | 8 + .../AIChildWindowNodeItemWrapper.tsx | 67 ++++++++ .../aiChildWindowNodeItemWrapper/type.ts | 0 .../AIChildWindowStreamCard.tsx | 21 +++ .../aiChildWindowStreamCard/type.ts | 6 + ...hildWindowTaskDefaultGroupCard.module.scss | 2 + .../AIChildWindowTaskDefaultGroupCard.tsx | 44 +++++ .../aiChildWindowTaskDefaultGroupCard/type.ts | 2 - .../AIChildWindowToolInvokerCard.tsx | 17 ++ .../aiChildWindowToolInvokerCard/type.ts | 6 + .../AIGroupStreamCard.module.scss | 67 +------- .../aiGroupStreamCard/AIGroupStreamCard.tsx | 43 ++--- .../AIGroupStreamCardHeard.module.scss | 2 +- .../AIGroupStreamCardList.module.scss | 8 +- .../AIGroupStreamCardList.tsx | 4 +- .../AIHttpFlowFuzzStatusCard.tsx | 11 +- .../aiHttpFlowFuzzStatusCard/type.ts | 1 + .../aiReportFinishCard/AIReportFinishCard.tsx | 32 ++-- .../AIReportFinishCardType.d.ts | 1 + .../aiReviewResult/AIReviewResult.tsx | 8 +- .../components/aiReviewResult/type.ts | 2 + .../ai-re-act/aiReActChat/AIReActChat.tsx | 5 +- .../ai-re-act/aiReActChat/AIReActChatType.ts | 1 - 62 files changed, 546 insertions(+), 490 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/AIGroupStreamNodeWrapper.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/type.ts rename app/renderer/src/main/src/pages/ai-agent/components/{ => aiChatListItemWrapper}/aiItemContentWrapper/AIItemContentWrapper.module.scss (100%) rename app/renderer/src/main/src/pages/ai-agent/components/{ => aiChatListItemWrapper}/aiItemContentWrapper/AIItemContentWrapper.tsx (93%) rename app/renderer/src/main/src/pages/ai-agent/components/{ => aiChatListItemWrapper}/aiItemContentWrapper/type.ts (71%) create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiReviewResultWrapper/AIReviewResultWrapper.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiReviewResultWrapper/type.ts rename app/renderer/src/main/src/pages/ai-agent/components/{ => aiChatListItemWrapper}/aiStreamCardWrapper/aiStreamCardWrapper.tsx (94%) rename app/renderer/src/main/src/pages/ai-agent/components/{ => aiChatListItemWrapper}/aiStreamCardWrapper/type.ts (100%) create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiToolInvokerCardWrapper/AIToolInvokerCardWrapper.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiToolInvokerCardWrapper/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupItem/AIChildWindowGroupItem.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupItem/type.ts rename app/renderer/src/main/src/pages/ai-agent/components/{AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss => aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss} (67%) create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/AIChildWindowGroupStreamNode.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/AIChildWindowStreamCard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx rename app/renderer/src/main/src/pages/ai-agent/components/{AITaskDefaultGroupCard => aiChildWindowItem}/aiChildWindowTaskDefaultGroupCard/type.ts (54%) create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowToolInvokerCard/AIChildWindowToolInvokerCard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowToolInvokerCard/type.ts diff --git a/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js b/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js index 173d5e4fe7..d10605474b 100644 --- a/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js +++ b/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js @@ -35,7 +35,7 @@ function register(manager, mainWindow) { */ ipcMain.handle(FETCH_CONTENTS, async (_event, frame) => { if (!frame?.session || !frame?.token || !mainWindow || mainWindow.isDestroyed()) { - return { rawData: [] } + return { rawData: [], execFileRecord: [] } } const requestId = crypto.randomUUID() @@ -44,12 +44,12 @@ function register(manager, mainWindow) { return new Promise((resolve) => { const timeout = setTimeout(() => { ipcMain.removeAllListeners(responseChannel) - resolve({ rawData: [] }) + resolve({ rawData: [], execFileRecord: [] }) }, 15000) ipcMain.once(responseChannel, (_responseEvent, data) => { clearTimeout(timeout) - resolve(data ?? { rawData: [] }) + resolve(data ?? { rawData: [], execFileRecord: [] }) }) safeSendMain('fetch-concurrent-stream-contents-request', { requestId, ...frame }) diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx index 7f50407ee0..3ae83d444b 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx @@ -5,7 +5,7 @@ import { type ConcurrentStreamFramePayload, isConcurrentStreamFrame, } from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' -import { AIChatQSDataTypeEnum, type AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' +import { AIChatQSDataTypeEnum, AIYakExecFileRecord, type AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' import { fetchConcurrentStreamContents } from './fetchConcurrentStreamContents' import styles from './AIConcurrentStream.module.scss' import AIConcurrentStreamContent, { @@ -18,7 +18,7 @@ import useMemoizedFn from 'ahooks/lib/useMemoizedFn' // 全量进入 aux bundle,拉长 did-finish-load 与首次开窗耗时。 const AIChildWindowTaskDefaultGroupCard = lazy( () => - import('@/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard'), + import('@/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard'), ) const AIChildWindowConcurrentStreamCard = lazy( () => @@ -34,12 +34,13 @@ interface AIConcurrentStreamProps { const AIConcurrentStream: React.FC = memo(({ windowId }) => { const [frame, setFrame] = useState(null) const [contentVersion, setContentVersion] = useState(0) - // rawData 是否仍在通过 fetch-concurrent-stream-contents 拉取中 + // rawData/execFileRecord 是否仍在通过 fetch-concurrent-stream-contents 拉取中 const [loadingContents, setLoadingContents] = useState(false) - // rawData 用 ref 存储,更新不触发渲染; + // rawData/execFileRecord 用 ref 存储,更新不触发渲染; // 组件及子组件的重渲染由 contentVersion(renderNum)驱动 const rawDataRef = useRef>(new Map()) + const execFileRecordRef = useRef>(new Map()) useEffect(() => { if (!windowId) return @@ -50,11 +51,13 @@ const AIConcurrentStream: React.FC = memo(({ windowId } // 开窗时 frame 只携带轻量元数据(rawData 为空 Map), // 真正的 rawData 由下方的懒拉取 effect 通过 IPC 向主窗口请求 startTransition(() => { - const newFrame = { + const newFrame: ConcurrentStreamFramePayload = { ...framePayload, rawData: new Map(), + execFileRecord: new Map(), } setFrame(newFrame) + // 收到 frame 后,主动向主窗口拉取本次需要渲染的 rawData。 getRawData(newFrame) setContentVersion((v) => v + 1) @@ -81,15 +84,12 @@ const AIConcurrentStream: React.FC = memo(({ windowId } const getRawData = useMemoizedFn((frame) => { setLoadingContents(true) - fetchConcurrentStreamContents(frame) .then((entries) => { - rawDataRef.current = entries + rawDataRef.current = entries.rawData + execFileRecordRef.current = entries.execFileRecord setContentVersion((v) => v + 1) }) - .catch(() => { - rawDataRef.current = new Map() - }) .finally(() => { setLoadingContents(false) }) @@ -120,6 +120,7 @@ const AIConcurrentStream: React.FC = memo(({ windowId } chatType: frame?.chatType, childrenTokens: frame?.childrenTokens, rawData: rawDataRef.current, + execFileRecord: execFileRecordRef.current, renderNum: contentVersion, } }, [frame, contentVersion]) diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts index db5db7a2fb..7b171517e0 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts @@ -1,8 +1,12 @@ -import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' +import type { AIChatQSData, AIYakExecFileRecord } from '@/pages/ai-re-act/hooks/aiRender' import type { ConcurrentStreamFramePayload } from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' const { ipcRenderer } = window.require('electron') +export interface FetchConcurrentStreamContentsResponse { + rawData: Map + execFileRecord: Map +} /** * 子窗口通过 IPC 向主窗口拉取 task 相关的全部 content 数据。 * 主窗口从 globalSessionEngine 取最新 store + rawData,复用 buildConcurrentStreamFramePayload @@ -11,8 +15,12 @@ const { ipcRenderer } = window.require('electron') */ export async function fetchConcurrentStreamContents( frame: ConcurrentStreamFramePayload, -): Promise> { +): Promise { const result = await ipcRenderer.invoke('fetch-concurrent-stream-contents', frame) - const entries: Array<[string, AIChatQSData]> = result?.rawData ?? [] - return new Map(entries) + const rawData: Array<[string, AIChatQSData]> = result?.rawData ?? [] + const execFileRecord: Array<[string, AIYakExecFileRecord[]]> = result?.execFileRecord ?? [] + return { + rawData: new Map(rawData), + execFileRecord: new Map(execFileRecord), + } } diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index b3d6792b90..7c987c6e72 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -49,7 +49,6 @@ import { HistroryAIReActChat } from './HistroryAIReActChat' import { useChatIPC } from '@/pages/ai-re-act/hooks/useChatIPC' import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' -import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' export type HistoryAIReActChatExternalParameters = NonNullable @@ -58,7 +57,6 @@ export interface HistoryAIReActChatBridge { // events: UseChatIPCEvents onStop: () => void onNewChat: () => void - onChatFromHistory: (session: string) => void setActiveChat: React.Dispatch> syncSelectedHttpFlowIds: (ids: string[]) => void registerClearTableSelection: (fn: () => void) => void @@ -374,13 +372,6 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid }) }) - const onChatFromHistory = useMemoizedFn((session: string) => { - // events.onDelChats([session]) - globalSessionEngine.forceCloseSession({ - sessionIds: [session], - }) - }) - /** 新建会话:清空 UI、断开旧连接,并预生成新的 TimelineSessionID */ const onNewChat = useMemoizedFn(() => { const currentID = activeChat?.SessionID @@ -486,7 +477,6 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid // events, onStop, onNewChat, - onChatFromHistory, setActiveChat, syncSelectedHttpFlowIds: (ids: string[]) => { aiReActChatRef.current?.setHttpFlow?.(ids) @@ -520,7 +510,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid }) }, }), - [activeID, onStop, onNewChat, onChatFromHistory, setActiveChat, handleSubmitQuery], + [activeID, onStop, onNewChat, setActiveChat, handleSubmitQuery], ) const renderHistoryAIReActChat = useCallback( diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index 21601ffe39..d4d8a79aca 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -422,7 +422,6 @@ export const AIAgentChat: React.FC = memo((props) => { const onChat = useMemoizedFn(() => { onSetReAct() }) - const onChatFromHistory = useMemoizedFn((session: string) => {}) useEffect(() => { emiter.on('defualtAIMentionCommandParams', konwledgeInputStringFn) @@ -481,7 +480,7 @@ export const AIAgentChat: React.FC = memo((props) => { /> ) : ( - + )}
{activeForge && ( diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx index 6c7a1b1d93..227f5b0bc8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx @@ -35,7 +35,7 @@ import { AIHorizontalScrollCard } from './aiHorizontalScrollCard/AIHorizontalScr export const AIChatContent: React.FC = React.memo( forwardRef((props, ref) => { - const { onChat, onChatFromHistory } = props + const { onChat } = props const { t, i18nRefresh } = useI18nNamespaces(['aiAgent', 'yakitUi', 'yakitRoute']) const store = useCurrentStore() @@ -257,7 +257,6 @@ export const AIChatContent: React.FC = React.memo( resolve({ params: data.params, onChat, - onChatFromHistory, }) }) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/type.ts b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/type.ts index fa05bf5916..d88d676d3b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/type.ts @@ -6,7 +6,6 @@ export interface AIChatContentRefProps extends AIReActChatRefProps {} export interface AIChatContentProps { ref?: React.ForwardedRef onChat: () => void - onChatFromHistory: (sessionID: string) => void } export type AIAgentTabPayload = TabHTTPPayload | TabFileSystemPayload interface TabHTTPPayload { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx deleted file mode 100644 index 9675b6aec0..0000000000 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { useBoolean, useCreation } from 'ahooks' -import classNames from 'classnames' -import { type FC, memo } from 'react' -import AITaskDefaultGroupCardHeard from '../aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard' -import { AIChildWindowTaskDefaultGroupCardProps, AIChildWindowTaskDefaultGroupContentProps } from './type' -import styles from './AIChildWindowTaskDefaultGroupCard.module.scss' -import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' -import useAIConcurrentStreamDispatcher from '@/auxWindow/pages/AIConcurrentStream/useContext/useDispatcher' -import useClickFocus from '@/pages/ai-re-act/hooks/useClickFocus' -import AINodeItem from '../../aiChatListItem/aiNodeItem/AINodeItem' -import { AIChatQSDataTypeEnum, type AIChatQSData, type ChatStream } from '@/pages/ai-re-act/hooks/aiRender' -import React from 'react' - -const AIChildWindowTaskDefaultGroupCard: FC = memo((props) => { - const { token } = props - const [expand, { toggle: expandToggle }] = useBoolean(true) - - const { rawData } = useAIConcurrentStreamStore() - const { requestRefresh } = useAIConcurrentStreamDispatcher() - const timeStamp = useCreation(() => { - if (!rawData) return 0 - const itemData = rawData.get(token) - if (!itemData) return 0 - return itemData.Timestamp || 0 - }, []) - - return ( -
- - - {expand ? : null} -
- ) -}) - -export default AIChildWindowTaskDefaultGroupCard - -const AIChildWindowTaskDefaultGroupContent: FC = memo((props) => { - const { childrenTokens, rawData } = useAIConcurrentStreamStore() - - const { ref: scrollRef, isFocus } = useClickFocus() - return ( -
-
-
- -
-
-
- ) -}) - -// #region 子窗口 group 渲染 - -/** 子窗口版 group item,数据从 rawData Map 获取 */ -const AIChildWindowGroupItem: FC<{ token: string; rawData: Map }> = memo(({ token, rawData }) => { - const itemData = rawData.get(token) - if (!itemData) return null - - switch (itemData.type) { - case AIChatQSDataTypeEnum.STREAM_GROUP: - return - default: - return null - } -}) - -/** 子窗口版 stream group 卡片,从 rawData 中按 parentGroupToken 查找子节点 */ -const AIChildWindowGroupStreamCard: FC<{ token: string; rawData: Map }> = memo( - ({ token, rawData }) => { - // 按 token + rawData 缓存该 group 的子节点,避免每次渲染都全量 forEach - const childItems = useCreation(() => { - const items: AIChatQSData[] = [] - rawData.forEach((value) => { - if (value.parentGroupToken === token) { - items.push(value) - } - }) - return items - }, [rawData, token]) - - return ( -
- {childItems.map((item) => ( - - ))} -
- ) - }, -) - -// #endregion - -// #region 子窗口 node item 渲染 - -/** - * 子窗口版 node item 包装器。 - * STREAM 类型依赖 streaming hooks(useTypedStream → useCurrentStore), - * 子窗口无 store,故 STREAM 直接展示纯文本内容;其余类型复用 AINodeItem。 - */ -const AIChildWindowNodeItemWrapper: FC<{ itemData: AIChatQSData }> = memo(({ itemData }) => { - if (itemData.type === AIChatQSDataTypeEnum.STREAM) { - const streamData = itemData as ChatStream - return ( -
-
{streamData.data?.content || ''}
-
- ) - } - return ( -
- -
- ) -}) - -// #endregion diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx index 503cb7b7de..d0dbd07673 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx @@ -3,10 +3,12 @@ import { AITaskDefaultGroupCardHeardProps } from './type' import styles from './AITaskDefaultGroupCardHeard.module.scss' import { OutlineInformationcircleIcon } from '@/assets/icon/outline' import { formatTimestamp } from '@/utils/timeUtil' -import { t } from 'i18next' + import ConcurrentStreamCardActions from '../../ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions' +import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' const AITaskDefaultGroupCardHeard: React.FC = memo((props) => { + const { t } = useI18nNamespaces(['aiAgent']) const { isChildWindow, expandToggle, timeStamp, expand, onRefresh, token } = props return (
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx index 153dfe836b..5edf028d21 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx @@ -2,23 +2,12 @@ import classNames from 'classnames' import { type FC, memo } from 'react' import ConcurrentStreamCardHeard from '../concurrentStreamCardHeard/ConcurrentStreamCardHeard' import styles from './AIChildWindowConcurrentStreamCard.module.scss' -import AINodeItem from '../../aiChatListItem/aiNodeItem/AINodeItem' -import { - AIChatQSDataTypeEnum, - type AIChatQSData, - type ChatStream, - type ChatTaskNodeGroup, -} from '@/pages/ai-re-act/hooks/aiRender' +import { type ChatTaskNodeGroup } from '@/pages/ai-re-act/hooks/aiRender' import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' import useAIConcurrentStreamDispatcher from '@/auxWindow/pages/AIConcurrentStream/useContext/useDispatcher' -import useClickFocus from '@/pages/ai-re-act/hooks/useClickFocus' -import { - AIChildWindowGroupItemProps, - AIChildWindowGroupStreamCardProps, - AIChildWindowNodeItemWrapperProps, -} from './type' import useBoolean from 'ahooks/lib/useBoolean' import useCreation from 'ahooks/lib/useCreation' +import AIChildWindowConcurrentStreamContent from '../../aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent' export interface AIChildWindowConcurrentStreamCardProps { token: string @@ -55,107 +44,3 @@ const AIChildWindowConcurrentStreamCard: FC { - const { childrenTokens, rawData, renderNum } = useAIConcurrentStreamStore() - const { ref: scrollRef, isFocus } = useClickFocus() - - return ( -
- -
- ) -}) - -// #endregion - -// #region 子窗口 group 渲染 - -/** 子窗口版 group item,数据从 rawData Map 获取 */ -const AIChildWindowGroupItem: FC = memo(({ token }) => { - const { rawData } = useAIConcurrentStreamStore() - const itemData = rawData?.get(token) - if (!itemData || !rawData) return null - - switch (itemData.type) { - case AIChatQSDataTypeEnum.STREAM_GROUP: - return - default: - return null - } -}) - -/** 子窗口版 stream group 卡片,从 rawData 中按 parentGroupToken 查找子节点 */ -const AIChildWindowGroupStreamCard: FC = memo(({ token }) => { - const { rawData, renderNum } = useAIConcurrentStreamStore() - // 按 token + renderNum 缓存该 group 的子节点,避免每次渲染都全量 forEach - const childItems = useCreation(() => { - if (!rawData) return [] - const items: AIChatQSData[] = [] - rawData.forEach((value) => { - if (value.parentGroupToken === token) { - items.push(value) - } - }) - return items - }, [rawData, token, renderNum]) - - return ( -
- {childItems.map((item) => ( - - ))} -
- ) -}) - -// #endregion - -// #region 子窗口 node item 渲染 - -/** - * 子窗口版 node item 包装器。 - * STREAM 类型依赖 streaming hooks(useTypedStream → useCurrentStore), - * 子窗口无 store,故 STREAM 直接展示纯文本内容;其余类型复用 AINodeItem。 - */ -const AIChildWindowNodeItemWrapper: FC = memo(({ itemData, renderNum }) => { - if (itemData.type === AIChatQSDataTypeEnum.STREAM) { - const streamData = itemData as ChatStream - return ( -
-
{streamData.data?.content || ''}
-
- ) - } - return ( -
- -
- ) -}) - -// #endregion diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/type.ts index af8f11a6fc..6a1f15115d 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/type.ts @@ -3,9 +3,7 @@ import { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' export interface AIChildWindowGroupItemProps { token: string } -export interface AIChildWindowGroupStreamCardProps { - token: string -} + export interface AIChildWindowNodeItemWrapperProps { itemData: AIChatQSData renderNum: number diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts index 5dd47cfeaa..f71a2d7914 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts @@ -1,6 +1,12 @@ import type { OpenAIConcurrentStreamPayload } from '@/utils/openWebsite' -import { AIChatQSDataTypeEnum, ChatStoreState, type AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' +import { + AIChatQSDataTypeEnum, + AIYakExecFileRecord, + ChatStoreState, + type AIChatQSData, +} from '@/pages/ai-re-act/hooks/aiRender' import { AIItemKind, getAIItemKind } from '@/pages/ai-re-act/hooks/useAIItemKind' +import i18n from '@/i18n/i18n' /** store 的最小依赖接口 */ interface BuildFrameStore { @@ -32,11 +38,14 @@ function getKind(store: BuildFrameStore, childToken: string): AIItemKind | null return getAIItemKind(state, childToken) } +const tOriginal = i18n.getFixedT(null, 'aiAgent') /** 获取 task 节点的名称 */ function getTaskName(rawData: BuildFrameRawData | null | undefined, token: string): string { const itemData = rawData?.contents.get(token) if (!itemData) return '' switch (itemData.type) { + case AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP: + return tOriginal('ConcurrentStreamCard.systemInfo') case AIChatQSDataTypeEnum.TASK_NODE_GROUP: return itemData.data?.taskName ?? '' default: @@ -52,10 +61,11 @@ function getTaskName(rawData: BuildFrameRawData | null | undefined, token: strin export function buildConcurrentStreamFramePayload( params: BuildConcurrentStreamFramePayloadParams, ): OpenAIConcurrentStreamPayload | null { - const { token, session, chatType, store, rawData, withRawData = false } = params + const { token, session, chatType, store, rawData, withRawData = true } = params if (!chatType || !rawData) return null const frameRawData = new Map() + let execFileRecord = new Map() const state = store.getState() const childrenTokens = state.tasks[token]?.childrenTokens || [] @@ -82,6 +92,8 @@ export function buildConcurrentStreamFramePayload( } } } + + execFileRecord = new Map(state.execFileRecord) } return { @@ -90,6 +102,7 @@ export function buildConcurrentStreamFramePayload( chatType: chatType as OpenAIConcurrentStreamPayload['chatType'], childrenTokens, rawData: frameRawData, + execFileRecord, taskName: getTaskName(rawData, token), } } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts index d821d50a85..345b715e6b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts @@ -25,10 +25,12 @@ export function setupConcurrentStreamMainBridge() { if (!requestId || !session || !token) return const { store, rawData } = globalSessionEngine.ensureSession(session) - const full = buildConcurrentStreamFramePayload({ token, session, chatType, store, rawData }) + // 子窗主动拉取全量数据,必须填充 rawData + execFileRecord + const full = buildConcurrentStreamFramePayload({ token, session, chatType, store, rawData, withRawData: true }) const entries = full ? Array.from(full.rawData.entries()) : [] + const execFileRecord = full ? Array.from(full.execFileRecord.entries()) : [] - ipcRenderer.send(`fetch-concurrent-stream-contents-response-${requestId}`, { rawData: entries }) + ipcRenderer.send(`fetch-concurrent-stream-contents-response-${requestId}`, { rawData: entries, execFileRecord }) } ipcRenderer.on(FETCH_REQUEST, handler) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts index a309a9cc7f..a17602f5a4 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts @@ -1,4 +1,4 @@ -import type { AIChatQSData, ChatListRenderType } from '@/pages/ai-re-act/hooks/aiRender' +import type { AIChatQSData, AIYakExecFileRecord, ChatListRenderType } from '@/pages/ai-re-act/hooks/aiRender' import { isMap } from 'lodash' export interface ConcurrentStreamFramePayload { @@ -8,6 +8,8 @@ export interface ConcurrentStreamFramePayload { childrenTokens: string[] /** childrenTokens 中各节点 token 对应的原始数据 */ rawData: Map + + execFileRecord: Map taskName?: string } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx index 2caae287e4..f0c7781300 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx @@ -43,8 +43,7 @@ import { success } from '@/utils/notification' import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' import useAIAgentDispatcher from '../useContext/useDispatcher' -import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' -import { useStore } from 'zustand' +import { useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' /** @name AI工具按钮对应图标 */ @@ -55,6 +54,7 @@ const AIToolToIconMap: Record = { interface ToolInvokerCardProps { itemData: ChatToolResult renderNum: number + fileList: AIYakExecFileRecord[] } interface PreWrapperProps { code: string @@ -66,27 +66,17 @@ interface ToolStatusCardProps { status: AIToolResult['tool']['status'] | 'purple' | 'neutral' children?: ReactNode } -interface ToolStdoutCardProps extends ToolInvokerCardProps { - fileList: AIYakExecFileRecord[] -} -interface ToolResultCardProps extends ToolInvokerCardProps { - fileList: AIYakExecFileRecord[] -} +interface ToolStdoutCardProps extends ToolInvokerCardProps {} +interface ToolResultCardProps extends ToolInvokerCardProps {} const ToolInvokerCard: FC = (props) => { - const { itemData, renderNum } = props - const store = useCurrentStore() - const execFileRecord = useStore(store, (state) => state.execFileRecord) - const fileList = useCreation(() => { - if (!itemData?.data?.callToolId) return [] - return execFileRecord.get(itemData?.data?.callToolId) || [] - }, [renderNum, itemData?.data?.callToolId]) + const { itemData } = props switch (itemData?.data?.type) { case 'stream': - return + return case 'result': - return + return case 'create': return default: diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx index 145512b4e2..85d28fdeb7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StaticChatContent/StaticChatContent.tsx @@ -6,9 +6,11 @@ import AINodeItem from '../aiNodeItem/AINodeItem' type StaticChatContentProps = { token: string + /** 组内 0-based 下标,由组列表 map 时透传;非组内场景不传 */ + groupIndex?: number } -const StaticChatContent: FC = ({ token }) => { +const StaticChatContent: FC = ({ token, groupIndex }) => { const store = useCurrentStore() const renderNum = useStore(store, (state) => state.items[token]?.renderNum) const rawData = useCurrentRawData() @@ -23,6 +25,6 @@ const StaticChatContent: FC = ({ token }) => { }, [token, renderNum]) if (!chatItem) return null - return + return } export default memo(StaticChatContent) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx index cc95b30052..388416d4af 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt.tsx @@ -13,7 +13,7 @@ import { getFileNameByModelType, getModelLabelByModelType } from '@/pages/ai-age import styles from './AIModelErrorPrompt.module.scss' import { Tooltip } from 'antd' export const AIModelErrorPrompt: React.FC = React.memo((props) => { - const { item, renderNum } = props + const { item, renderNum, isChildWindow } = props const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const [aiGlobalConfigData, event] = useAIGlobalConfig() @@ -28,6 +28,7 @@ export const AIModelErrorPrompt: React.FC = React.memo( }, []) const onEdit = useMemoizedFn((e) => { e.stopPropagation() + if (isChildWindow) return const fileName = getFileNameByModelType(item.data.model_tier) if (!fileName) return // NOTE - 编辑事件,index为0是因为当前使用的ai模型是单选的且选中项一定是在第一个 @@ -53,14 +54,16 @@ export const AIModelErrorPrompt: React.FC = React.memo( titleText="模型错误" titleExtra={modalInfo && } titleMore={ - - } onClick={onEdit} /> - + !isChildWindow && ( + + } onClick={onEdit} /> + + ) } >
- {getModelLabelByModelType(modelTier)}使用错误,请点击右上角编辑进行处理 + {getModelLabelByModelType(modelTier)}使用错误{!isChildWindow && ',请点击右上角编辑进行处理'}
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts index e70a207b1c..ad31d16d99 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts @@ -3,4 +3,5 @@ import { ChatApiRequestFailed } from '@/pages/ai-re-act/hooks/aiRender' export interface AIModelErrorPromptProps { item: ChatApiRequestFailed renderNum: number + isChildWindow: boolean } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx index 4131cedef0..c8db1e76b7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx @@ -1,9 +1,10 @@ import React from 'react' import { AINodeItemProps } from './type' import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' -import { AITriageChatContentWrapper, AIThought } from '../../aiItemContentWrapper/AIItemContentWrapper' -import ToolInvokerCard from '../../ToolInvokerCard' -import { AIReviewResult } from '../../aiReviewResult/AIReviewResult' +import { + AITriageChatContentWrapper, + AIThought, +} from '../../aiChatListItemWrapper/aiItemContentWrapper/AIItemContentWrapper' import { AIReActChatReview } from '../../aiReActChatReview/AIReActChatReview' import styles from './AINodeItem.module.scss' import { AIManualIntervention } from '../../aiManualIntervention/AIManualIntervention' @@ -15,11 +16,13 @@ import { AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import AiFailPlanCard from '../../aiFailPlanCard/AiFailPlanCard' import { AIModelErrorPrompt } from '../aiModelErrorPrompt/AIModelErrorPrompt' -import { AIGroupStreamNode } from '../../aiGroupStreamCard/AIGroupStreamCard' -import AIStreamCardWrapper from '../../aiStreamCardWrapper/aiStreamCardWrapper' +import AIStreamCardWrapper from '../../aiChatListItemWrapper/aiStreamCardWrapper/aiStreamCardWrapper' +import AIToolInvokerCardWrapper from '../../aiChatListItemWrapper/aiToolInvokerCardWrapper/AIToolInvokerCardWrapper' +import AIReviewResultWrapper from '../../aiChatListItemWrapper/aiReviewResultWrapper/AIReviewResultWrapper' +import AIGroupStreamNodeWrapper from '../../aiChatListItemWrapper/aiGroupStreamNodeWrapper/AIGroupStreamNodeWrapper' const AINodeItem: React.FC = React.memo((props) => { - const { itemData, renderNum } = props + const { itemData, renderNum, groupIndex } = props const { t } = useI18nNamespaces(['aiAgent']) switch (itemData.type) { case AIChatQSDataTypeEnum.QUESTION: @@ -30,7 +33,7 @@ const AINodeItem: React.FC = React.memo((props) => { return case AIChatQSDataTypeEnum.TOOL_RESULT: - return + return case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: @@ -43,7 +46,7 @@ const AINodeItem: React.FC = React.memo((props) => { itemData.type === AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE ) return null - return + return } else { return ( = React.memo((props) => { return case AIChatQSDataTypeEnum.HTTP_FLOW_FUZZ_STATUS: - return + return case AIChatQSDataTypeEnum.REPORT_FINISH: - return + return case AIChatQSDataTypeEnum.END_PLAN_AND_EXECUTION: return ( @@ -84,12 +87,13 @@ const AINodeItem: React.FC = React.memo((props) => { return case AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED: - return + return case AIChatQSDataTypeEnum.STREAM: if (!!itemData.parentGroupToken) { - return + return } else { + // 组 return } default: diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/type.ts index 217b4b1e1e..e9854e5b13 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/type.ts @@ -4,4 +4,6 @@ export interface AINodeItemProps { itemData: AIChatQSData /** 控制渲染,必传 */ renderNum: number + /** 组内 0-based 下标,由组列表 map 时透传;仅 STREAM + parentGroupToken 分支使用 */ + groupIndex?: number } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/AIGroupStreamNodeWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/AIGroupStreamNodeWrapper.tsx new file mode 100644 index 0000000000..bb52b9be0f --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/AIGroupStreamNodeWrapper.tsx @@ -0,0 +1,34 @@ +import { FC, memo } from 'react' +import type { AIGroupStreamNodeWrapperProps } from './type' +import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' +import { useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useCreation } from 'ahooks' +import { AIGroupStreamNode } from '../../aiGroupStreamCard/AIGroupStreamCard' + +const AIGroupStreamNodeWrapper: FC = memo((props) => { + const { itemData, renderNum, groupIndex } = props + // 其余原始字段通过 useCurrentRawData 获取,并订阅 renderNum 驱动重渲染 + const rawData = useCurrentRawData() + + const stream = useCreation(() => { + const rawStream = rawData.contents.get(itemData.id) + if (!rawStream) return null + switch (rawStream.type) { + case AIChatQSDataTypeEnum.STREAM: + return rawStream + + default: + return null + } + }, [renderNum]) + // seqNo 直接使用父组件在 map 时透传的 groupIndex 拼装, + // 下标由唯一负责排序的父组件决定,与渲染顺序天然一致;未传则不显示序号 + const seqNo = useCreation(() => { + if (!itemData.parentGroupToken || groupIndex == null) return '' + return `${groupIndex + 1}. ` + }, [renderNum, groupIndex]) + if (!stream) return null + return +}) + +export default AIGroupStreamNodeWrapper diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/type.ts new file mode 100644 index 0000000000..f98098a8e9 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/type.ts @@ -0,0 +1,8 @@ +import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AIGroupStreamNodeWrapperProps { + itemData: ChatStream + renderNum: number + /** 该节点在所属 group 子节点列表中的 0-based 下标,由负责排序的父组件透传,用于拼 seqNo */ + groupIndex?: number +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/AIItemContentWrapper.module.scss similarity index 100% rename from app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.module.scss rename to app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/AIItemContentWrapper.module.scss diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/AIItemContentWrapper.tsx similarity index 93% rename from app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.tsx rename to app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/AIItemContentWrapper.tsx index 08269cdbb6..f88ef72c22 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/AIItemContentWrapper.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/AIItemContentWrapper.tsx @@ -1,6 +1,6 @@ import React from 'react' import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' -import { AITriageChatContent } from '../aiTriageChat/AITriageChat' +import { AITriageChatContent } from '../../aiTriageChat/AITriageChat' import { AIThoughtProps, AITriageChatContentWrapperProps } from './type' import { useCreation } from 'ahooks' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/type.ts similarity index 71% rename from app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/type.ts rename to app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/type.ts index bda8f95423..77bd7f869f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiItemContentWrapper/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/type.ts @@ -1,6 +1,6 @@ import { ChatThought } from '@/pages/ai-re-act/hooks/aiRender' -import { AINodeItemProps } from '../aiChatListItem/aiNodeItem/type' -import { AITriageChatContentProps } from '../aiTriageChat/type' +import { AINodeItemProps } from '../../aiChatListItem/aiNodeItem/type' +import { AITriageChatContentProps } from '../../aiTriageChat/type' interface AIItemContentWrapperProps { renderNum: AINodeItemProps['renderNum'] diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiReviewResultWrapper/AIReviewResultWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiReviewResultWrapper/AIReviewResultWrapper.tsx new file mode 100644 index 0000000000..08288d59d5 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiReviewResultWrapper/AIReviewResultWrapper.tsx @@ -0,0 +1,15 @@ +import { FC, memo } from 'react' +import type { AIReviewResultWrapperProps } from './type' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' +import { AIReviewResult } from '../../aiReviewResult/AIReviewResult' + +const AIReviewResultWrapper: FC = memo((props) => { + const { itemData, renderNum } = props + const store = useCurrentStore() + const taskLength = useStore(store, (state) => state.taskChat.elements.length) + const casualLength = useStore(store, (state) => state.casualChat.elements.length) + return +}) + +export default AIReviewResultWrapper diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiReviewResultWrapper/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiReviewResultWrapper/type.ts new file mode 100644 index 0000000000..831dd21ed4 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiReviewResultWrapper/type.ts @@ -0,0 +1,6 @@ +import { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AIReviewResultWrapperProps { + itemData: AIChatQSData + renderNum: number +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/aiStreamCardWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiStreamCardWrapper/aiStreamCardWrapper.tsx similarity index 94% rename from app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/aiStreamCardWrapper.tsx rename to app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiStreamCardWrapper/aiStreamCardWrapper.tsx index b6614bf4c6..675f51d82e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/aiStreamCardWrapper.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiStreamCardWrapper/aiStreamCardWrapper.tsx @@ -2,7 +2,7 @@ import { type FC, memo, useEffect, useState } from 'react' import type { AIStreamCardWrapperProps } from './type' import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import useRafInterval from 'ahooks/lib/useRafInterval' -import { AIStreamCard } from '../aiChatListItem/StreamingChatContent/StreamingChatContent' +import { AIStreamCard } from '../../aiChatListItem/StreamingChatContent/StreamingChatContent' import { AIChatQSDataTypeEnum, ChatStream } from '@/pages/ai-re-act/hooks/aiRender' import { useStore } from 'zustand' import useMemoizedFn from 'ahooks/lib/useMemoizedFn' diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiStreamCardWrapper/type.ts similarity index 100% rename from app/renderer/src/main/src/pages/ai-agent/components/aiStreamCardWrapper/type.ts rename to app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiStreamCardWrapper/type.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiToolInvokerCardWrapper/AIToolInvokerCardWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiToolInvokerCardWrapper/AIToolInvokerCardWrapper.tsx new file mode 100644 index 0000000000..ec581b2fc8 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiToolInvokerCardWrapper/AIToolInvokerCardWrapper.tsx @@ -0,0 +1,20 @@ +import { FC, memo } from 'react' +import type { AIToolInvokerCardWrapperProps } from './type' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useCreation } from 'ahooks' +import { useStore } from 'zustand' +import ToolInvokerCard from '../../ToolInvokerCard' + +const AIToolInvokerCardWrapper: FC = memo((props) => { + const { itemData, renderNum } = props + const store = useCurrentStore() + const execFileRecord = useStore(store, (state) => state.execFileRecord) + const fileList = useCreation(() => { + if (!itemData?.data?.callToolId) return [] + return execFileRecord.get(itemData?.data?.callToolId) || [] + }, [renderNum, itemData?.data?.callToolId]) + + return +}) + +export default AIToolInvokerCardWrapper diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiToolInvokerCardWrapper/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiToolInvokerCardWrapper/type.ts new file mode 100644 index 0000000000..013bf29299 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiToolInvokerCardWrapper/type.ts @@ -0,0 +1,6 @@ +import { ChatToolResult } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AIToolInvokerCardWrapperProps { + itemData: ChatToolResult + renderNum: number +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.module.scss new file mode 100644 index 0000000000..d46076b6d8 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.module.scss @@ -0,0 +1 @@ +@use '../../ConcurrentStreamCard/ConcurrentStreamCard.module.scss'; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx new file mode 100644 index 0000000000..ef1925e2a8 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx @@ -0,0 +1,43 @@ +import classNames from 'classnames' +import styles from './AIChildWindowConcurrentStreamContent.module.scss' +import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' +import useClickFocus from '@/pages/ai-re-act/hooks/useClickFocus' +import React, { type FC, memo } from 'react' +import AIChildWindowGroupItem from '../aiChildWindowGroupItem/AIChildWindowGroupItem' +import AIChildWindowNodeItemWrapper from '../aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper' + +const AIChildWindowConcurrentStreamContent: FC = memo(() => { + const { childrenTokens, rawData } = useAIConcurrentStreamStore() + const { ref: scrollRef, isFocus } = useClickFocus() + + return ( +
+ +
+ ) +}) + +export default AIChildWindowConcurrentStreamContent diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupItem/AIChildWindowGroupItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupItem/AIChildWindowGroupItem.tsx new file mode 100644 index 0000000000..ed5f3d6158 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupItem/AIChildWindowGroupItem.tsx @@ -0,0 +1,22 @@ +import { useCreation } from 'ahooks' +import { type FC, memo } from 'react' +import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' +import AIChildWindowGroupStreamCard from '../aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard' +import { AIChatQSDataTypeEnum } from '../../../../ai-re-act/hooks/aiRender' + +/** 子窗口版 group item,数据从 rawData Map 获取 */ +const AIChildWindowGroupItem: FC<{ token: string }> = memo(({ token }) => { + const { renderNum, rawData } = useAIConcurrentStreamStore() + const itemData = useCreation(() => { + return rawData?.get(token) + }, [renderNum, token]) + if (!itemData) return null + + switch (itemData.type) { + case AIChatQSDataTypeEnum.STREAM_GROUP: + return + default: + return null + } +}) +export default AIChildWindowGroupItem diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupItem/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupItem/type.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss similarity index 67% rename from app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss rename to app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss index 6df4145c5d..060fcfb8ea 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss @@ -1,2 +1 @@ -@use '../AITaskDefaultGroupCard.module.scss'; @use '../../ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss'; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx new file mode 100644 index 0000000000..c5a3b7bcee --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx @@ -0,0 +1,32 @@ +import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' +import { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' +import { useCreation } from 'ahooks' +import { FC, memo } from 'react' +import AIChildWindowNodeItemWrapper from '../aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper' +import { AIChildWindowGroupStreamCardProps } from './type' +import styles from './AIChildWindowGroupStreamCard.module.scss' +/** 子窗口版 stream group 卡片,从 rawData 中按 parentGroupToken 查找子节点 */ +const AIChildWindowGroupStreamCard: FC = memo(({ token }) => { + const { rawData, renderNum } = useAIConcurrentStreamStore() + // 按 token + renderNum 缓存该 group 的子节点,避免每次渲染都全量 forEach + const childItems = useCreation(() => { + if (!rawData) return [] + const items: AIChatQSData[] = [] + rawData.forEach((value) => { + if (value.parentGroupToken === token) { + items.push(value) + } + }) + return items + }, [token, renderNum]) + + return ( +
+ {childItems.map((item, index) => ( + + ))} +
+ ) +}) + +export default AIChildWindowGroupStreamCard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/type.ts new file mode 100644 index 0000000000..5e1b9319a5 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/type.ts @@ -0,0 +1,3 @@ +export interface AIChildWindowGroupStreamCardProps { + token: string +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/AIChildWindowGroupStreamNode.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/AIChildWindowGroupStreamNode.tsx new file mode 100644 index 0000000000..e960ece1a1 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/AIChildWindowGroupStreamNode.tsx @@ -0,0 +1,33 @@ +import { FC, memo } from 'react' +import type { AIChildWindowGroupStreamNodeProps } from './type' +import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' +import useCreation from 'ahooks/lib/useCreation' +import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' +import { AIGroupStreamNode } from '../../aiGroupStreamCard/AIGroupStreamCard' + +const AIChildWindowGroupStreamNode: FC = memo((props) => { + const { itemData, renderNum, groupIndex } = props + const { rawData } = useAIConcurrentStreamStore() + // 其余原始字段通过 useCurrentRawData 获取,并订阅 renderNum 驱动重渲染 + const stream = useCreation(() => { + const rawStream = rawData?.get(itemData.id) + if (!rawStream) return null + switch (rawStream.type) { + case AIChatQSDataTypeEnum.STREAM: + return rawStream + + default: + return null + } + }, [renderNum]) + // seqNo 直接使用父组件(AIChildWindowGroupStreamCard)在 map 时透传的 groupIndex, + // 下标由唯一负责排序的父组件决定,与渲染顺序天然一致;未传则不显示序号 + const seqNo = useCreation(() => { + if (!itemData.parentGroupToken || groupIndex == null) return '' + return `${groupIndex + 1}. ` + }, [renderNum, groupIndex]) + if (!stream) return null + return +}) + +export default AIChildWindowGroupStreamNode diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/type.ts new file mode 100644 index 0000000000..37c49ee678 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/type.ts @@ -0,0 +1,8 @@ +import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AIChildWindowGroupStreamNodeProps { + itemData: ChatStream + renderNum: number + /** 该节点在所属 group 子节点列表中的 0-based 下标,由负责排序的父组件透传,用于拼 seqNo */ + groupIndex?: number +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper.tsx new file mode 100644 index 0000000000..695dd9cdd0 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper.tsx @@ -0,0 +1,67 @@ +import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' +import { type FC, memo } from 'react' +import { AIModelErrorPrompt } from '../../aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt' +import AINodeItem from '../../aiChatListItem/aiNodeItem/AINodeItem' +import { AIHttpFlowFuzzStatusCard } from '../../aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard' +import { AIReportFinishCard } from '../../aiReportFinishCard/AIReportFinishCard' +import { AIReviewResult } from '../../aiReviewResult/AIReviewResult' +import AIChildWindowGroupStreamNode from '../aiChildWindowGroupStreamNode/AIChildWindowGroupStreamNode' +import AIChildWindowStreamCard from '../aiChildWindowStreamCard/AIChildWindowStreamCard' +import AIChildWindowToolInvokerCard from '../aiChildWindowToolInvokerCard/AIChildWindowToolInvokerCard' + +/** + * 子窗口版 node item 包装器。 + * STREAM 类型依赖 streaming hooks(useTypedStream → useCurrentStore), + * 子窗口无 store,故 STREAM 直接展示纯文本内容;其余类型复用 AINodeItem。 + */ +const AIChildWindowNodeItemWrapper: FC<{ itemData: AIChatQSData; groupIndex?: number }> = memo( + ({ itemData, groupIndex }) => { + const { renderNum = 0 } = useAIConcurrentStreamStore() + switch (itemData.type) { + case AIChatQSDataTypeEnum.TOOL_RESULT: + return + case AIChatQSDataTypeEnum.EXEC_AIFORGE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE: + case AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE: + if (!!itemData.data?.selected) { + if ( + itemData.type === AIChatQSDataTypeEnum.TOOL_USE_REVIEW_REQUIRE || + itemData.type === AIChatQSDataTypeEnum.TASK_REVIEW_REQUIRE + ) + return null + return + } + // 任务子窗口中没有未review的数据 + return null + + case AIChatQSDataTypeEnum.HTTP_FLOW_FUZZ_STATUS: + return + case AIChatQSDataTypeEnum.REPORT_FINISH: + return + case AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED: + return + case AIChatQSDataTypeEnum.STREAM: + if (!!itemData.parentGroupToken) { + return + } else { + // 组 + return + } + case AIChatQSDataTypeEnum.QUESTION: + case AIChatQSDataTypeEnum.RESULT: + case AIChatQSDataTypeEnum.THOUGHT: + case AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION: + case AIChatQSDataTypeEnum.TOOL_CALL_DECISION: + case AIChatQSDataTypeEnum.END_PLAN_AND_EXECUTION: + case AIChatQSDataTypeEnum.FAIL_PLAN_AND_EXECUTION: + case AIChatQSDataTypeEnum.FAIL_REACT: + return + default: + return null + } + }, +) +export default AIChildWindowNodeItemWrapper diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/type.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/AIChildWindowStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/AIChildWindowStreamCard.tsx new file mode 100644 index 0000000000..8b61644d6e --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/AIChildWindowStreamCard.tsx @@ -0,0 +1,21 @@ +import { type FC, memo } from 'react' +import type { AIChildWindowStreamCardProps } from './type' +import { AIStreamCard } from '../../aiChatListItem/StreamingChatContent/StreamingChatContent' +import useCreation from 'ahooks/lib/useCreation' +import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' + +const AIChildWindowStreamCard: FC = memo((props) => { + const { itemData, renderNum } = props + const data: ChatStream = useCreation(() => { + return { + ...itemData, + data: { + ...itemData.data, + status: 'end', + }, + } + }, [renderNum]) + return +}) + +export default AIChildWindowStreamCard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/type.ts new file mode 100644 index 0000000000..c0bf610940 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/type.ts @@ -0,0 +1,6 @@ +import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AIChildWindowStreamCardProps { + itemData: ChatStream + renderNum: number +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss new file mode 100644 index 0000000000..6d42ceb4fa --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss @@ -0,0 +1,2 @@ +@use '../../AITaskDefaultGroupCard/AITaskDefaultGroupCard.module.scss'; +@use '../../ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss'; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx new file mode 100644 index 0000000000..adb39ee8dd --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx @@ -0,0 +1,44 @@ +import { useBoolean, useCreation } from 'ahooks' +import classNames from 'classnames' +import { type FC, memo } from 'react' +import AITaskDefaultGroupCardHeard from '../../AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard' +import { AIChildWindowTaskDefaultGroupCardProps } from './type' +import styles from './AIChildWindowTaskDefaultGroupCard.module.scss' +import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' +import useAIConcurrentStreamDispatcher from '@/auxWindow/pages/AIConcurrentStream/useContext/useDispatcher' +import AIChildWindowConcurrentStreamContent from '../aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent' + +const AIChildWindowTaskDefaultGroupCard: FC = memo((props) => { + const { token } = props + const [expand, { toggle: expandToggle }] = useBoolean(true) + + const { rawData, renderNum } = useAIConcurrentStreamStore() + const { requestRefresh } = useAIConcurrentStreamDispatcher() + const timeStamp = useCreation(() => { + if (!rawData) return 0 + const itemData = rawData.get(token) + if (!itemData) return 0 + return itemData.Timestamp || 0 + }, [renderNum]) + + return ( +
+ + + {expand ? : null} +
+ ) +}) + +export default AIChildWindowTaskDefaultGroupCard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/type.ts similarity index 54% rename from app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/type.ts rename to app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/type.ts index 35168153bf..ffe791de62 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/type.ts @@ -1,5 +1,3 @@ export interface AIChildWindowTaskDefaultGroupCardProps { token: string } - -export interface AIChildWindowTaskDefaultGroupContentProps {} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowToolInvokerCard/AIChildWindowToolInvokerCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowToolInvokerCard/AIChildWindowToolInvokerCard.tsx new file mode 100644 index 0000000000..6d70619938 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowToolInvokerCard/AIChildWindowToolInvokerCard.tsx @@ -0,0 +1,17 @@ +import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' +import { useCreation } from 'ahooks' +import { type FC, memo } from 'react' +import ToolInvokerCard from '../../ToolInvokerCard' +import type { AIChildWindowToolInvokerCardProps } from './type' + +const AIChildWindowToolInvokerCard: FC = memo((props) => { + const { itemData, renderNum } = props + const { execFileRecord } = useAIConcurrentStreamStore() + const fileList = useCreation(() => { + if (!itemData?.data?.callToolId) return [] + return execFileRecord?.get(itemData?.data?.callToolId) || [] + }, [renderNum, itemData?.data?.callToolId]) + return +}) + +export default AIChildWindowToolInvokerCard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowToolInvokerCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowToolInvokerCard/type.ts new file mode 100644 index 0000000000..830b729b6b --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowToolInvokerCard/type.ts @@ -0,0 +1,6 @@ +import { ChatToolResult } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AIChildWindowToolInvokerCardProps { + itemData: ChatToolResult + renderNum: number +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss index 2e0f4a824a..abbf14b605 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss @@ -8,75 +8,18 @@ flex-direction: column; // gap: 4px; border: 1px solid transparent; + // 遮罩底色变量:默认走基础背景,hover/focus 时由父级切换,子模块消费 + --group-stream-mask-bg: var(--Colors-Use-Basic-Background); &:hover { background: var(--Colors-Use-Neutral-Bg); - .ai-mask { - background: linear-gradient( - to right, - var(--Colors-Use-Neutral-Bg) 0%, - rgba(255, 255, 255, 0) 10%, - rgba(255, 255, 255, 0) 90%, - transparent 100% - ); - } - .content { - &:not(.noMask)::before { - background: linear-gradient( - 180deg, - var(--Colors-Use-Neutral-Bg) 0%, - var(--Colors-Use-Neutral-Bg) 30.21%, - transparent 80% - ); - @supports (background: color-mix(in srgb, #000 50%, #fff)) { - background: linear-gradient( - 180deg, - var(--Colors-Use-Neutral-Bg) 0%, - color-mix(in srgb, var(--Colors-Use-Neutral-Bg) 60%, transparent) 30.21%, - transparent 80% - ); - } - } - &:not(.noMask)::after { - background: linear-gradient( - 180deg, - transparent 20%, - var(--Colors-Use-Neutral-Bg) 69.79%, - var(--Colors-Use-Neutral-Bg) 100% - ); - @supports (background: color-mix(in srgb, #000 50%, #fff)) { - background: linear-gradient( - 180deg, - transparent 20%, - color-mix(in srgb, var(--Colors-Use-Neutral-Bg) 60%, transparent) 69.79%, - var(--Colors-Use-Neutral-Bg) 100% - ); - } - } - } + --group-stream-mask-bg: var(--Colors-Use-Neutral-Bg); } } .container-focus { border-color: var(--Colors-Use-Main-Hover); background: var(--Colors-Use-Neutral-Bg); - .stream-text { - .ai-mask { - background: linear-gradient( - to right, - var(--Colors-Use-Neutral-Bg) 0%, - rgba(255, 255, 255, 0) 10%, - rgba(255, 255, 255, 0) 90%, - transparent 100% - ); - } - } - .content { - &:not(.noMask)::before { - background: initial; - } - &:not(.noMask)::after { - background: initial; - } - } + // focus 时取消遮罩:变量置透明,子模块渐变自然透明,等效原 background: initial + --group-stream-mask-bg: transparent; } .stream-text { overflow: hidden; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index af2f0231e5..724b62895e 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -5,7 +5,6 @@ import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { OutlineArrowsexpandIcon } from '@/assets/icon/outline' import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' import { YakitModal } from '@/components/yakitUI/YakitModal/YakitModal' -import { useTypedStream } from '../aiChatListItem/StreamingChatContent/hooks/useTypedStream' import classNames from 'classnames' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import useClickFocus from '../../../ai-re-act/hooks/useClickFocus' @@ -16,6 +15,7 @@ import { AIGroupStreamCardHeardWrapperProps, AIGroupStreamCardListWrapperProps } import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' import AIGroupStreamCardHeard from './aiGroupStreamCardHeard/AIGroupStreamCardHeard' import AIGroupStreamCardList from './aiGroupStreamCardList/AIGroupStreamCardList' +import { useTypedStream } from '../aiChatListItem/StreamingChatContent/hooks/useTypedStream' export const Code: FC<{ code: ChatReferenceMaterialPayload; style: CSSProperties }> = ({ code, style }) => { return ( @@ -30,32 +30,19 @@ export const Code: FC<{ code: ChatReferenceMaterialPayload; style: CSSProperties export const AIGroupStreamNode: FC<{ itemData: ChatStream renderNum: number -}> = memo(({ itemData, renderNum }) => { + seqNo: string +}> = memo(({ itemData, renderNum, seqNo }) => { const { t } = useI18nNamespaces(['aiAgent']) + + const [open, setOpen] = useState(false) + const [openPopover, setOpenPopover] = useState(false) + // 仅获取用于显示的 content(已应用打字效果) const { content } = useTypedStream({ getContent: () => itemData.data.content, getStatus: () => itemData.data.status, }) - const [open, setOpen] = useState(false) - const [openPopover, setOpenPopover] = useState(false) - // 其余原始字段通过 useCurrentRawData 获取,并订阅 renderNum 驱动重渲染 - const store = useCurrentStore() - const rawData = useCurrentRawData() - const tokenRenderNum = useStore(store, (state) => state.items[itemData.id]?.renderNum) - - const stream = useCreation(() => { - const rawStream = rawData.contents.get(itemData.id) - if (!rawStream) return null - switch (rawStream.type) { - case AIChatQSDataTypeEnum.STREAM: - return rawStream - - default: - return null - } - }, [tokenRenderNum]) const onClose = () => { setOpen(false) } @@ -73,27 +60,19 @@ export const AIGroupStreamNode: FC<{ } }, []) - const seqNo = useCreation(() => { - if (!itemData.parentGroupToken) return '' - const parentGroupTokens = store.getState().groups[itemData.parentGroupToken].childrenTokens - const index = parentGroupTokens.findIndex((ele) => ele === itemData.id) - return index === -1 ? '' : `${index + 1}. ` - }, [renderNum]) - const modelCode = useCreation(() => { if (!open) return [] - return stream?.reference || [] + return itemData?.reference || [] }, [open]) const popoverCode = useCreation(() => { if (!openPopover) return [] - return stream?.reference || [] + return itemData?.reference || [] }, [openPopover]) const hidden = useCreation(() => { - return !!stream?.reference?.length - }, [stream?.reference?.length]) + return !!itemData?.reference?.length + }, [renderNum, itemData?.reference?.length]) - if (!stream) return null return (
{open && ( diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss index 4f8a999b29..d08ef5ee67 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss @@ -50,7 +50,7 @@ top: 0; background: linear-gradient( to right, - var(--Colors-Use-Basic-Background) 0%, + var(--group-stream-mask-bg, var(--Colors-Use-Basic-Background)) 0%, rgba(255, 255, 255, 0) 10%, rgba(255, 255, 255, 0) 90%, transparent 100% diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.module.scss index 9fd95c8218..4e693a214f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.module.scss @@ -17,8 +17,8 @@ height: 30%; background: linear-gradient( 180deg, - var(--Colors-Use-Basic-Background) 0%, - var(--Colors-Use-Basic-Background) 4%, + var(--group-stream-mask-bg, var(--Colors-Use-Basic-Background)) 0%, + var(--group-stream-mask-bg, var(--Colors-Use-Basic-Background)) 4%, rgba(255, 255, 255, 0) 96% ); pointer-events: none; @@ -35,8 +35,8 @@ background: linear-gradient( 180deg, rgba(255, 255, 255, 0) 4%, - var(--Colors-Use-Basic-Background) 96%, - var(--Colors-Use-Basic-Background) 100% + var(--group-stream-mask-bg, var(--Colors-Use-Basic-Background)) 96%, + var(--group-stream-mask-bg, var(--Colors-Use-Basic-Background)) 100% ); pointer-events: none; z-index: 1; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx index 6c465eed84..0a22188303 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx @@ -83,8 +83,8 @@ const AIGroupStreamCardList: React.FC = memo((props) overflow: isScroll ? 'overlay' : 'hidden', }} > - {childrenTokens.map((token) => ( - + {childrenTokens.map((token, index) => ( + ))}
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx index 1bd4fc23af..224a714d01 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard.tsx @@ -25,7 +25,7 @@ const STATS_GAP_PX = 4 const STATS_ROW_4_MIN_INNER_PX = 4 * STATS_TILE_PX + 3 * STATS_GAP_PX export const AIHttpFlowFuzzStatusCard: React.FC = memo((props) => { - const { item, renderNum } = props + const { item, renderNum, isChildWindow } = props const { data, Timestamp, AIService, AIModelName } = item const { t } = useI18nNamespaces(['aiAgent']) @@ -66,6 +66,7 @@ export const AIHttpFlowFuzzStatusCard: React.FC = // 则把本卡片的 `runtime_id` 显式推送到该页签,并打开 traffic analysis 抽屉。 // - 否则回退到全局打开「流量分析」路由页,并通过 `pageInfo` 携带本卡片的 `runtime_id`。 const handleViewDetail = useMemoizedFn(() => { + if (isChildWindow) return const runtimeId = data?.runtime_id if (!runtimeId) { yakitNotify('error', '该发包统计缺少 runtime_id,无法查看详情') @@ -134,9 +135,11 @@ export const AIHttpFlowFuzzStatusCard: React.FC =
{data?.reason ?? t('AIHttpFlowFuzzStatusCard.title')}
- - {t('AIHttpFlowFuzzStatusCard.viewDetail')} - + {!isChildWindow && ( + + {t('AIHttpFlowFuzzStatusCard.viewDetail')} + + )}
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/type.ts index 36911a79b7..7111f04c3c 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiHttpFlowFuzzStatusCard/type.ts @@ -3,4 +3,5 @@ import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' export interface AIHttpFlowFuzzStatusCardProps { item: Extract renderNum: number + isChildWindow: boolean } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCard.tsx index 7f325a5ab9..d016af9dba 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCard.tsx @@ -18,7 +18,7 @@ import ChatCard from '../ChatCard' const { ipcRenderer } = window.require('electron') export const AIReportFinishCard: React.FC = memo((props) => { - const { item, renderNum } = props + const { item, renderNum, isChildWindow } = props const { data } = item const { t } = useI18nNamespaces(['aiAgent']) const [downloadLoading, setDownloadLoading] = useState(false) @@ -36,7 +36,7 @@ export const AIReportFinishCard: React.FC = memo((props }, [renderNum]) const handleOpenReport = useMemoizedFn(() => { - if (!reportPath) return + if (!reportPath || isChildWindow) return if (getCurrentPageTabRouteKey() === YakitRoute.Irify_AI_Code_Audit) { emiter.emit( 'onAiCodeAuditOpenTemporaryFile', @@ -55,7 +55,7 @@ export const AIReportFinishCard: React.FC = memo((props const handleDownloadReport = useMemoizedFn(async () => { try { - if (!reportPath) return + if (!reportPath || isChildWindow) return let code = await getCodeByPath(reportPath) if (!code) { yakitNotify('error', t('AIReportFinishCard.reportContentEmpty')) @@ -85,18 +85,20 @@ export const AIReportFinishCard: React.FC = memo((props - - } onClick={handleOpenReport} /> - - } - onClick={handleDownloadReport} - loading={downloadLoading} - /> -
+ !isChildWindow && ( +
+ + } onClick={handleOpenReport} /> + + } + onClick={handleDownloadReport} + loading={downloadLoading} + /> +
+ ) } > {content && } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCardType.d.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCardType.d.ts index 7c5231c3fa..ea0d98a534 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCardType.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReportFinishCard/AIReportFinishCardType.d.ts @@ -3,4 +3,5 @@ import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' export interface AIReportFinishCardProps { item: Extract renderNum: number + isChildWindow: boolean } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx index 9f1de571ff..3d466b30aa 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/AIReviewResult.tsx @@ -15,12 +15,10 @@ import { success } from '@/utils/notification' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { isEmpty } from 'lodash' import classNames from 'classnames' -import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' -import { useStore } from 'zustand' import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' export const AIReviewResult: React.FC = memo((props) => { - const { info, renderNum } = props + const { info, renderNum, taskLength, casualLength } = props const { t, i18nRefresh } = useI18nNamespaces(['aiAgent', 'yakitUi']) const getChatType = useMemoizedFn(() => { @@ -29,10 +27,6 @@ export const AIReviewResult: React.FC = memo((props) => { const [expand, setExpand] = useState(false) - const store = useCurrentStore() - const taskLength = useStore(store, (state) => state.taskChat.elements.length) - const casualLength = useStore(store, (state) => state.casualChat.elements.length) - const isInit = useRef(true) useUpdateEffect(() => { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/type.ts index de40e9c846..8bff7509ea 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReviewResult/type.ts @@ -6,6 +6,8 @@ import { ModalInfoProps } from '../ModelInfo' export interface AIReviewResultProps { info: AIChatQSData renderNum: number + casualLength?: number + taskLength?: number } export interface AISingHaveColorTextProps extends ChatCardProps { title: ReactNode diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index 125178ba3d..e16f28a3ee 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -129,7 +129,7 @@ export const AIReActChat: React.FC = React.memo( FocusModeLoop: value.focusMode, } const onStartChat = (res: AIHandleStartResProps) => { - const { params, extraParams, onChat, onChatFromHistory } = res + const { params, extraParams, onChat } = res if (!sessionID) { // 创建新的聊天记录 const newChat: AISession = { @@ -153,9 +153,6 @@ export const AIReActChat: React.FC = React.memo( ) // 新建的额外操作 onChat?.() - } else { - // 历史中的额外操作 - onChatFromHistory?.(sessionID) } aiChatTextareaRef.current.setMention({ mentionId: params.FocusModeLoop || randomString(8), diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts index d64f63654d..5048f094de 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChatType.ts @@ -20,7 +20,6 @@ export interface AIHandleStartResProps { params: AIInputEvent extraParams?: AIHandleStartExtraProps onChat?: () => void - onChatFromHistory?: (sessionID: string) => void } export interface AISendParams { params: AIInputEvent From 7e46d483996856b5ceed718e60427f9bfa27bd0f Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Thu, 23 Jul 2026 15:16:27 +0800 Subject: [PATCH 048/101] =?UTF-8?q?feta:HistoryAIReActChatProvider?= =?UTF-8?q?=E4=B8=AD=E6=94=B9=E4=B8=BAsource=E5=92=8Cpageid=E5=88=A4?= =?UTF-8?q?=E6=96=AD=E5=85=B7=E4=BD=93=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/withHistoryAIReActChat.tsx | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index 7c987c6e72..c4f843424b 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -22,7 +22,7 @@ import { AISendResProps, } from '@/pages/ai-re-act/aiReActChat/AIReActChatType' import { AIAgentGrpcApi, AIInputEvent, AISource } from '@/pages/ai-re-act/hooks/grpcApi' -import type { YakitRouteType } from '@/enums/yakitRoute' +import { YakitRoute, type YakitRouteType } from '@/enums/yakitRoute' import { applyHttpFuzzRequestChangeToWebFuzzerPage, getWebFuzzerPageIsHttps, @@ -176,10 +176,6 @@ export interface HistoryAIReActChatProviderProps { pageId: string focusModeLoop: HistoryAIReActFocusModeLoop children: React.ReactNode - /** Web Fuzzer 页签 id:AI 改包回写、请求附件、fuzz 状态推送等桥接用,与 SessionID 无关 */ - // httpFuzzTabPageId?: string - /** Yak Runner 工作区 id:AI `yaklang_code_change` 审阅/写回桥接 */ - yakRunnerPageId?: string /** * - 在 `onStartRequest` / `onSendRequest` 内置(WebFuzzer 请求附件)处理之后执行 * - Irify「代码审计」用它把工程根路径附件追加到 `AttachedResourceInfo` @@ -198,7 +194,6 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid pageId, focusModeLoop, children, - yakRunnerPageId, transformInputEvent, resolveStartExtraParams, mergeRemoteAIAgentSetting, @@ -217,6 +212,16 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const chatReadyRef = useRef(false) const yakRunnerLastAttachedResourceInfoRef = useRef([]) + // Web Fuzzer 页签 id:AI 改包回写、请求附件、fuzz 状态推送等桥接用,与 SessionID 无关 + const isHaveWebFuzzerPageId = useCreation(() => { + return route === YakitRoute.HTTPFuzzer && !!pageId + }, [route, pageId]) + + // Yak Runner 工作区 id:AI `yaklang_code_change` 审阅/写回桥接 + const isHaveYakRunnerPageId = useCreation(() => { + return route === YakitRoute.YakScript && !!pageId + }, [route, pageId]) + useEffect(() => { if (!showFreeChat) { chatReadyRef.current = false @@ -234,7 +239,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid }) const onHttpFuzzRequestChange = useMemoizedFn((data: AIAgentGrpcApi.HttpFuzzRequestChange) => { - if (!pageId) return + if (!isHaveWebFuzzerPageId) return // casual 问答期间:有完整 raw 时不自动写包,入队审阅(`op` 仅占位描述,不作为筛选项) if (casualLoadingRef.current) { @@ -252,9 +257,9 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid }) const onYaklangCodeChange = useMemoizedFn((data: AIAgentGrpcApi.YaklangCodeChange) => { - if (!yakRunnerPageId) return + if (!isHaveYakRunnerPageId) return - const editorNow = getYakRunnerPageActiveCodeString(yakRunnerPageId) ?? '' + const editorNow = getYakRunnerPageActiveCodeString(pageId) ?? '' const original = data.op === 'create' ? '' @@ -265,7 +270,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid : '' // op=patch:后端只给片段,这里合并成全量 replace,再走原有 diff UI - const normalized = normalizeYaklangCodeChangeForReview(yakRunnerPageId, data, original) + const normalized = normalizeYaklangCodeChangeForReview(pageId, data, original) if (!normalized) return const nextCode = normalized.code?.content @@ -275,7 +280,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const isCreate = normalized.op === 'create' const createFileName = isCreate ? createYakRunnerGeneratedCodeFileName() : undefined const createPath = isCreate - ? resolveYaklangCreateTargetPath(yakRunnerPageId, yakRunnerLastAttachedResourceInfoRef.current, createFileName) + ? resolveYaklangCreateTargetPath(pageId, yakRunnerLastAttachedResourceInfoRef.current, createFileName) : undefined const change = isCreate ? { @@ -287,7 +292,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid } : normalized - enqueueYakRunnerCasualCodeReplaceReview(yakRunnerPageId, { + enqueueYakRunnerCasualCodeReplaceReview(pageId, { original, change, fileName: createFileName, @@ -298,7 +303,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid // AI `http_flow_fuzz_status` 推送:把每次最新的 `runtime_id` 静默推到当前 fuzzer 页签的处理器中。 // 用户点击「查看详情」会显式再次推送并要求打开抽屉,所以这里不主动打开。 const onGetHttpFlowFuzzStatus = useMemoizedFn((data: AIAgentGrpcApi.GetHttpFlowFuzzStatus) => { - if (!pageId) return + if (!isHaveWebFuzzerPageId) return const runtimeId = data?.runtime_id if (!runtimeId) return pushAIFuzzStatusRuntimeIdToWebFuzzerPage(pageId, runtimeId, { source: 'auto' }) @@ -310,9 +315,9 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const execute = useStore(store, (state) => state.execute) const casualLoading = useStore(store, (state) => state.casualLoading) - // TODO - @whale 修改确认 useEffect => httpFuzzTabPageId->pageId + // TODO - @whale 修改确认 useEffect => httpFuzzTabPageId->isHaveWebFuzzerPageId useEffect(() => { - if (!pageId && !yakRunnerPageId) { + if (!isHaveWebFuzzerPageId && !isHaveYakRunnerPageId) { casualLoadingRef.current = false initialRequestInCasualRef.current = null initialCodeInCasualRef.current = null @@ -320,23 +325,23 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid } if (!casualLoadingRef.current && casualLoading) { - if (pageId) { + if (isHaveWebFuzzerPageId) { initialRequestInCasualRef.current = getWebFuzzerPageRequestString(pageId) ?? '' } - if (yakRunnerPageId) { - resetYakRunnerPatchWorkingDraft(yakRunnerPageId) - initialCodeInCasualRef.current = getYakRunnerPageActiveCodeString(yakRunnerPageId) ?? '' + if (isHaveYakRunnerPageId) { + resetYakRunnerPatchWorkingDraft(pageId) + initialCodeInCasualRef.current = getYakRunnerPageActiveCodeString(pageId) ?? '' } } else if (casualLoadingRef.current && !casualLoading) { initialRequestInCasualRef.current = null initialCodeInCasualRef.current = null - if (yakRunnerPageId) { - resetYakRunnerPatchWorkingDraft(yakRunnerPageId) + if (isHaveYakRunnerPageId) { + resetYakRunnerPatchWorkingDraft(pageId) } } casualLoadingRef.current = casualLoading - }, [casualLoading, pageId, yakRunnerPageId]) + }, [casualLoading, pageId, isHaveWebFuzzerPageId, isHaveYakRunnerPageId]) const activeID = useCreation(() => { return activeChat?.SessionID @@ -349,8 +354,8 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid return new Promise((resolve) => { let params: AIInputEvent = { ...data.params, FocusModeLoop: focusModeLoop } - // TODO - @whale 修改确认 httpFuzzTabPageId->pageId - if (pageId) { + // TODO - @whale 修改确认 httpFuzzTabPageId->isHaveWebFuzzerPageId + if (isHaveWebFuzzerPageId) { const raw = getWebFuzzerPageRequestString(pageId) const isHttps = getWebFuzzerPageIsHttps(pageId) ?? false const sessionId = @@ -360,8 +365,8 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid if (transformInputEvent) { params = transformInputEvent(params) } - if (yakRunnerPageId) { - params = appendYakRunnerWorkspaceContextToEvent(yakRunnerPageId, params) + if (isHaveYakRunnerPageId) { + params = appendYakRunnerWorkspaceContextToEvent(pageId, params) params = normalizeStartUserQueryToTextDescription(params) yakRunnerLastAttachedResourceInfoRef.current = params.AttachedResourceInfo || [] } @@ -397,8 +402,8 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const onSendRequest = useMemoizedFn((data: AISendParams) => { let params: AIInputEvent = { ...data.params, FocusModeLoop: focusModeLoop } - // TODO - @whale 修改确认 httpFuzzTabPageId->pageId - if (pageId) { + // TODO - @whale 修改确认 httpFuzzTabPageId->isHaveWebFuzzerPageId + if (isHaveWebFuzzerPageId) { const raw = getWebFuzzerPageRequestString(pageId) const isHttps = getWebFuzzerPageIsHttps(pageId) ?? false params = attachWebFuzzerHttpRequestToEvent(params, activeChat?.SessionID, raw, isHttps) @@ -406,8 +411,8 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid if (transformInputEvent) { params = transformInputEvent(params) } - if (yakRunnerPageId) { - params = appendYakRunnerWorkspaceContextToEvent(yakRunnerPageId, params) + if (isHaveYakRunnerPageId) { + params = appendYakRunnerWorkspaceContextToEvent(pageId, params) yakRunnerLastAttachedResourceInfoRef.current = params.AttachedResourceInfo || [] } From af030cf08e214112a6b626f1f42b1eaffc600808 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Thu, 23 Jul 2026 15:26:29 +0800 Subject: [PATCH 049/101] =?UTF-8?q?fix-style:=E4=BF=AE=E5=A4=8D=E6=B5=81?= =?UTF-8?q?=E7=BB=84=E9=81=AE=E7=BD=A9=E5=B1=82=E6=A0=B7=E5=BC=8F=E4=B8=8A?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9Bperf:TASK=5FNODE=5FGROUP?= =?UTF-8?q?=E4=B8=BB=E7=AA=97=E5=8F=A3=E7=BB=84=E4=BB=B6=E5=8E=BB=E6=8E=89?= =?UTF-8?q?isChildWindow;del:=E6=96=B0=E5=BB=BA=E4=BC=9A=E8=AF=9Dmode?= =?UTF-8?q?=E5=BB=B6=E6=97=B6=E4=BF=AE=E6=94=B9=E5=8E=BB=E6=8E=89;fix:?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=B5=81=E7=BB=84=E5=B1=95=E5=BC=80=E7=9A=84?= =?UTF-8?q?=E6=97=B6=E5=80=99=E5=A4=B4=E9=83=A8=E6=95=B0=E6=8D=AE=E6=B2=A1?= =?UTF-8?q?=E6=9C=89=E9=9A=90=E8=97=8F=E7=9A=84=E9=97=AE=E9=A2=98=EF=BC=9B?= =?UTF-8?q?feat:=E6=B5=81=E7=BB=84=E6=94=B6=E8=B5=B7=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E4=B8=8Bhover=E6=98=BE=E7=A4=BA=E9=9A=90=E8=97=8F=E7=9A=84?= =?UTF-8?q?=E6=9D=A1=E6=95=B0=EF=BC=9Bfix:DetachedPlan=E9=9C=80=E8=A6=81UI?= =?UTF-8?q?=E4=B8=8A=E6=89=8B=E5=8A=A8=E9=87=8A=E6=94=BEreview=EF=BC=9Bfix?= =?UTF-8?q?:=E4=BB=BB=E5=8A=A1=E5=AD=90=E7=AA=97=E5=8F=A3=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0goal=E6=98=BE=E7=A4=BA=EF=BC=9Bfix:=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=BB=84=E4=BB=B6=E5=9C=A8=E9=9D=9E=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E4=B8=AD=E6=98=BE=E7=A4=BA=E8=B7=B3=E8=BF=87=E6=8C=89?= =?UTF-8?q?=E9=92=AE=E9=97=AE=E9=A2=98=EF=BC=9Bperf:=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E5=8F=82=E8=80=83=E8=B5=84=E6=96=99=E5=A2=9E=E5=8A=A0=E9=98=B2?= =?UTF-8?q?=E6=8A=96=EF=BC=9Bfix:plan=20review=E5=8F=AA=E6=9C=89=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E7=BB=A7=E7=BB=AD=E6=89=A7=E8=A1=8C=E7=9A=84=E6=97=B6?= =?UTF-8?q?=E5=80=99=E6=89=8D=E6=9B=B4=E6=96=B0=E5=BD=93=E5=89=8D=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=A0=91=E6=95=B0=E6=8D=AE=EF=BC=9Bperf:plan=20review?= =?UTF-8?q?=E9=87=8A=E6=94=BE=E7=9A=84=E6=97=B6=E5=80=99=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?parentTaskId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AIConcurrentStream/AIConcurrentStream.tsx | 19 +---- .../ai-agent/aiAgentChat/AIAgentChat.tsx | 7 +- .../ConcurrentStreamCard.tsx | 29 +++---- ...hildWindowConcurrentStreamCard.module.scss | 14 +++- .../AIChildWindowConcurrentStreamCard.tsx | 7 +- .../ConcurrentStreamCardHeard.tsx | 4 +- .../components/ConcurrentStreamCard/type.ts | 1 - .../aiChatListItem/aiNodeItem/AINodeItem.tsx | 2 +- .../aiChatListItem/aiTaskItem/AITaskItem.tsx | 2 +- ...dWindowConcurrentStreamContent.module.scss | 41 ++++++++- .../AIChildWindowConcurrentStreamContent.tsx | 15 ++-- .../AIChildWindowGroupStreamCard.module.scss | 16 +++- ...hildWindowTaskDefaultGroupCard.module.scss | 23 ++++- .../AIChildWindowTaskDefaultGroupCard.tsx | 2 +- .../AIGroupStreamCard.module.scss | 4 +- .../aiGroupStreamCard/AIGroupStreamCard.tsx | 34 ++++---- .../AIGroupStreamCardHeard.module.scss | 1 - .../AIGroupStreamCardHeard.tsx | 83 ++++++++++--------- .../AIGroupStreamCardList.tsx | 41 ++++----- .../components/aiGroupStreamCard/type.ts | 1 + .../aiReActChatReview/AIReActChatReview.tsx | 4 +- .../AIReActChatContents.tsx | 19 +++-- .../aiReActTaskChat/AIReActTaskChat.tsx | 9 +- .../AIReActTaskChatReviewBar.tsx | 6 +- .../aiReActTaskChat/useTaskChatExtraAction.ts | 2 +- .../hooks/ChatMultiSessionController.ts | 19 +++-- .../hooks/grpcStreamHandler/aiReview.ts | 1 + .../main/src/pages/yakRunner/YakRunner.tsx | 1 - 28 files changed, 238 insertions(+), 169 deletions(-) diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx index 3ae83d444b..26b35eb301 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, lazy, memo, startTransition, useEffect, useMemo, useRef, useState } from 'react' +import React, { lazy, memo, startTransition, Suspense, useEffect, useMemo, useRef, useState } from 'react' import { yakitAuxWindow } from '@/services/electronBridge' import ConcurrentStreamSkeleton from '@/auxWindow/components/ConcurrentStreamSkeleton/ConcurrentStreamSkeleton' import { @@ -141,25 +141,8 @@ const AIConcurrentStream: React.FC = memo(({ windowId }
}> {isTaskDefaultGroup ? ( - // ) : ( - // )} diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index d4d8a79aca..468a7280a4 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -121,9 +121,10 @@ export const AIAgentChat: React.FC = memo((props) => { Strategy: { EnableMultiAgent: false, EnableGoalMode: false, GoalMinIterations: 0 }, })) setActiveChat?.(undefined) - setTimeout(() => { - setMode('welcome') - }, 100) + setMode('welcome') + // setTimeout(() => { + // setMode('welcome') + // }, 100) break // 替换当前使用的 forge 模板 case ReActChatEventEnum.OPEN_FORGE_FORM: diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index 74212bbe63..dcaef42a20 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -19,15 +19,14 @@ import { useConcurrentStreamRefreshListener } from './concurrentStream/useConcur const ConcurrentStreamCard: FC<{ token: string - isChildWindow: boolean -}> = memo(({ token, isChildWindow }) => { +}> = memo(({ token }) => { const store = useCurrentStore() const rawData = useCurrentRawData() const renderNum = useStore(store, (state) => state.tasks[token]?.renderNum) const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean( - isChildWindow || rawData.contents.get(token)?.chatType !== 'reAct', + rawData.contents.get(token)?.chatType !== 'reAct', ) const raw = useCreation(() => { @@ -38,36 +37,30 @@ const ConcurrentStreamCard: FC<{ }, [renderNum]) useEffect(() => { - if (isChildWindow) return if (!raw?.data?.status) return if (raw.data.status !== 'processing') { collapseExpand() } - }, [isChildWindow, raw?.data?.status]) + }, [raw?.data?.status]) - useConcurrentStreamRefreshListener(token, !isChildWindow) + useConcurrentStreamRefreshListener(token, true) const presentation = useMemo(() => getAIStatusPresentation(raw?.data?.status), [raw?.data?.status]) const vectorBg = useVectorStripeBg(presentation.stripeColor) - const showStripeBg = !expand && !isChildWindow && !!vectorBg + const showStripeBg = !expand && !!vectorBg // TODO - 使用className来控制样式,避免使用style const cardStyle = useConcurrentStreamCardStyle({ bgColor: presentation.bgColor, vectorBg, showStripe: showStripeBg, - isChildWindow, + isChildWindow: false, }) return (
- + {expand && ( <> @@ -81,7 +74,7 @@ const ConcurrentStreamCard: FC<{ export default ConcurrentStreamCard const ConcurrentStreamCardHeardWrapper: FC = memo((props) => { - const { token, isChildWindow, expand, expandToggle } = props + const { token, expand, expandToggle } = props const store = useCurrentStore() const rawData = useCurrentRawData() const metaData = useCurrentMeta() @@ -102,13 +95,13 @@ const ConcurrentStreamCardHeardWrapper: FC { - if (!isChildWindow) expandToggle() + expandToggle() }) return ( = memo((props) => { +export const ConcurrentStreamCardTip: FC = memo((props) => { const { token } = props const store = useCurrentStore() const rawData = useCurrentRawData() diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.module.scss index 2c060013c9..372aa6c5c5 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.module.scss @@ -1,5 +1,15 @@ -@use '../ConcurrentStreamCard.module.scss'; -@use '../ConcurrentStreamContent/ConcurrentStreamContent.module.scss'; +// 子窗口并发流卡片样式。 +// 内容对齐 ConcurrentStreamCard.module.scss / ConcurrentStreamContent.module.scss。 + +.chat-card { + position: relative; + width: 100%; + padding: 4px 8px; + background-color: var(--Colors-Use-Neutral-Bg-Hover); + border-radius: 8px; + display: flex; + flex-direction: column; +} .child-chat-card { height: 100%; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx index 5edf028d21..7cbf63ab47 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx @@ -38,7 +38,12 @@ const AIChildWindowConcurrentStreamCard: FC - {expand ? : null} + {expand ? ( + <> +
{itemData?.data.goal}
+ + + ) : null}
) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx index 3215d1dce7..a767fc1d36 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx @@ -21,11 +21,11 @@ const ConcurrentStreamCardHeard: FC = memo((prop const showContinueTask = useCreation(() => { return !!rowData && !!coordinatorId && !!rowData?.data?.taskId && !isChildWindow - }, [rowData?.data, coordinatorId]) + }, [rowData?.data?.taskId]) const showCancelTask = useCreation(() => { return rowData?.data?.status === 'processing' && !!rowData?.data?.taskId && !isChildWindow - }, [rowData?.data]) + }, [rowData?.data?.status]) const modalInfo = useMemo(() => { if (!rowData) return undefined diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/type.ts index 0997259568..5e60e7da0c 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/type.ts @@ -8,7 +8,6 @@ export interface ConcurrentStreamCardTipProps { export interface ConcurrentStreamCardHeardWrapperProps { token: string - isChildWindow: boolean expand: boolean expandToggle: () => void } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx index c8db1e76b7..b76986670b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiNodeItem/AINodeItem.tsx @@ -91,9 +91,9 @@ const AINodeItem: React.FC = React.memo((props) => { case AIChatQSDataTypeEnum.STREAM: if (!!itemData.parentGroupToken) { + // 组 return } else { - // 组 return } default: diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx index 74a58dafcf..0458dd3582 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiTaskItem/AITaskItem.tsx @@ -15,7 +15,7 @@ const AITaskItem: React.FC = React.memo((props) => { case AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP: return case AIChatQSDataTypeEnum.TASK_NODE_GROUP: - return + return default: return <> } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.module.scss index d46076b6d8..4d86c5ab00 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.module.scss @@ -1 +1,40 @@ -@use '../../ConcurrentStreamCard/ConcurrentStreamCard.module.scss'; +// 子窗口并发流内容区样式。 +// 内容对齐 ConcurrentStreamContent.module.scss。 + +.concurrent-stream-content { + width: 100%; + max-height: 300px; + overflow-y: hidden; + &-item { + margin-bottom: 8px; + // 连续流卡片之间去掉间距(concurrent-stream-card 本身排除在外保留间距) + &:has(:global(.ai-stream-chat-content-wrapper)) { + & + + .concurrent-stream-content-item:has(:global(.ai-stream-chat-content-wrapper)):not( + :has(:global(.concurrent-stream-card)) + ) { + margin-top: -8px; + } + } + } +} + +// 子窗口内容区外层容器:撑满父级高度 +.concurrent-stream-content-wrapper { + height: 100%; + display: flex; + flex-direction: column; + flex: 1; +} + +.focused { + overflow-y: overlay; +} + +.content { + width: 100%; + max-height: 500px; + background-color: var(--Colors-Use-Basic-Background); + padding: 4px; + border-radius: 8px; +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx index ef1925e2a8..07679c71a9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx @@ -27,12 +27,15 @@ const AIChildWindowConcurrentStreamContent: FC = memo(() => { {childrenTokens?.map((token) => { const item = rawData?.get(token) if (!item) return - if (!!item.parentGroupToken) { - if (!rawData) return - return - } else { - return - } + return ( +
+ {!!item.parentGroupToken ? ( + + ) : ( + + )} +
+ ) })}
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss index 060fcfb8ea..933a5211d4 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss @@ -1 +1,15 @@ -@use '../../ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss'; +// 子窗口 stream group 卡片样式。 +// 内容对齐 ConcurrentStreamContent.module.scss。 + +.concurrent-stream-content-item { + margin-bottom: 8px; + // 连续流卡片之间去掉间距(concurrent-stream-card 本身排除在外保留间距) + &:has(:global(.ai-stream-chat-content-wrapper)) { + & + + .concurrent-stream-content-item:has(:global(.ai-stream-chat-content-wrapper)):not( + :has(:global(.concurrent-stream-card)) + ) { + margin-top: -8px; + } + } +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss index 6d42ceb4fa..86028356b4 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.module.scss @@ -1,2 +1,21 @@ -@use '../../AITaskDefaultGroupCard/AITaskDefaultGroupCard.module.scss'; -@use '../../ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.module.scss'; +// 子窗口 task default group 卡片样式。 +// 内容对齐 AITaskDefaultGroupCard.module.scss。 + +.ai-task-default-group-card { + width: 100%; + border-radius: 8px; + border: 1px solid var(--Colors-Use-Neutral-Bg-Hover); + background: var(--Colors-Use-Basic-Background); + max-height: 600px; + padding: 4px 8px; + display: flex; + flex-direction: column; + gap: 4px; +} + +.child-window-card { + max-height: none; + height: 100%; + flex: 1; + min-height: 0; +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx index adb39ee8dd..9ad3eb2eae 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx @@ -2,11 +2,11 @@ import { useBoolean, useCreation } from 'ahooks' import classNames from 'classnames' import { type FC, memo } from 'react' import AITaskDefaultGroupCardHeard from '../../AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard' -import { AIChildWindowTaskDefaultGroupCardProps } from './type' import styles from './AIChildWindowTaskDefaultGroupCard.module.scss' import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' import useAIConcurrentStreamDispatcher from '@/auxWindow/pages/AIConcurrentStream/useContext/useDispatcher' import AIChildWindowConcurrentStreamContent from '../aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent' +import type { AIChildWindowTaskDefaultGroupCardProps } from './type' const AIChildWindowTaskDefaultGroupCard: FC = memo((props) => { const { token } = props diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss index abbf14b605..c266c06c53 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.module.scss @@ -64,9 +64,7 @@ letter-spacing: 0.5px; } } -.stream-text-hidden { - display: none; -} + .single-stream-text { border-left: 2px solid var(--Colors-Use-Neutral-Bg-Hover); margin: 0; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index 724b62895e..3caeb5ff95 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -93,24 +93,22 @@ export const AIGroupStreamNode: FC<{ visible={openPopover} onVisibleChange={setOpenPopover} content={ - openPopover && ( -
-
- {seqNo} - {nodeLabel} - { - setOpenPopover(false) - setOpen(true) - }} - type="text2" - icon={} - size="small" - /> -
- {!!popoverCode.length && } +
+
+ {seqNo} + {nodeLabel} + { + setOpenPopover(false) + setOpen(true) + }} + type="text2" + icon={} + size="small" + />
- ) + {!!popoverCode.length && } +
} destroyTooltipOnHide={true} > @@ -162,6 +160,7 @@ const AIGroupStreamCardHeardWrapper: React.FC state.casualChat.elements.length) const taskChatLength = useStore(store, (state) => state.taskChat.elements.length) const renderNum = useStore(store, (state) => state.groups[token]?.renderNum) + const childrenTokensLength = useStore(store, (state) => state.groups[token]?.childrenTokens.length || 0) /** 可能存在第一次拿到的数据为undefined */ const groupData = useCreation(() => { @@ -241,6 +240,7 @@ const AIGroupStreamCardHeardWrapper: React.FC ) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss index d08ef5ee67..82c4d124a5 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.module.scss @@ -59,7 +59,6 @@ p { margin: 0; max-width: 100%; - display: inline-block; } span { display: flex; diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx index 91c5605c04..48a9a59648 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard.tsx @@ -6,54 +6,59 @@ import { memo } from 'react' import { useTypedStream } from '../../aiChatListItem/StreamingChatContent/hooks/useTypedStream' import { AIGroupStreamCardHeardProps } from '../type' import styles from './AIGroupStreamCardHeard.module.scss' +import useCreation from 'ahooks/lib/useCreation' const AIGroupStreamCardHeard: React.FC = memo((props) => { - const { expand, setExpand, lastItem, nodeLabel, shouldShowMask } = props + const { expand, setExpand, lastItem, nodeLabel, shouldShowMask, childrenTokensLength } = props const { content } = useTypedStream({ getContent: () => lastItem?.data.content ?? '', getStatus: () => lastItem?.data.status ?? 'end', }) - + const collapseTooltip = useCreation(() => { + return !expand && childrenTokensLength > 1 ? `折叠${childrenTokensLength}条信息` : '' + }, [expand, childrenTokensLength]) return ( -
{ - setExpand(!expand) - }} - > -
{nodeLabel}
-
- {shouldShowMask &&
} -

- {content} -

+ +
{ + setExpand(!expand) + }} + > +
{nodeLabel}
+
+ {shouldShowMask &&
} +

+ {content} +

+
+ + } + className={classNames(styles['expand-btn'], { + [styles['hidden-expand-btn']]: expand, + })} + /> + + + } + className={classNames(styles['expand-btn'], { + [styles['hidden-expand-btn']]: !expand, + })} + /> +
- - } - className={classNames(styles['expand-btn'], { - [styles['hidden-expand-btn']]: expand, - })} - /> - - - } - className={classNames(styles['expand-btn'], { - [styles['hidden-expand-btn']]: !expand, - })} - /> - -
+
) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx index 0a22188303..855f4b9c34 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx @@ -1,10 +1,9 @@ -import { useClickAway, useCreation, useThrottleFn } from 'ahooks' +import { useClickAway, useThrottleFn } from 'ahooks' import classNames from 'classnames' import { memo, useRef, useState, useEffect } from 'react' import StaticChatContent from '../../aiChatListItem/StaticChatContent/StaticChatContent' import { AIGroupStreamCardListProps } from '../type' import styles from './AIGroupStreamCardList.module.scss' -import { Tooltip } from 'antd' const BOTTOM_THRESHOLD = 10 @@ -63,32 +62,26 @@ const AIGroupStreamCardList: React.FC = memo((props) } }, [childrenTokens.length, expand]) - const collapseTooltip = useCreation(() => { - return !expand && childrenTokens.length > 1 ? `折叠${childrenTokens.length}条信息` : '' - }, [expand, childrenTokens.length]) - return ( - +
setIsScroll(true)} + className={styles['content-inner']} + style={{ + overflow: isScroll ? 'overlay' : 'hidden', + }} > -
setIsScroll(true)} - className={styles['content-inner']} - style={{ - overflow: isScroll ? 'overlay' : 'hidden', - }} - > - {childrenTokens.map((token, index) => ( - - ))} -
+ {childrenTokens.map((token, index) => ( + + ))}
- +
) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts index cb82c6c3fc..1c529c3eb7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts @@ -7,6 +7,7 @@ export interface AIGroupStreamCardHeardProps { lastItem?: ChatStream nodeLabel: string shouldShowMask: boolean + childrenTokensLength: number } export interface AIGroupStreamCardHeardWrapperProps { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx index b3aab99ead..db2bc6f51d 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx @@ -272,7 +272,7 @@ export const AIReActChatReview: React.FC = React.memo((p SyncID: randomString(8), SyncJsonInput: JSON.stringify(syncPayload), } - // chatIPCEvents.handleTaskReviewRelease(detachedReview.id) + globalSessionEngine.closeChatReview(sessionId, info.id) onSend({ token: sessionId, type: '', params }) pendingDetachedPlanSubmitRef.current = false }) @@ -368,7 +368,7 @@ export const AIReActChatReview: React.FC = React.memo((p setForgeOption(item) break case 'close': - globalSessionEngine.closeChatReview(sessionId, info.chatType, info.id) + globalSessionEngine.closeChatReview(sessionId, info.id) break default: if (editShow) return diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index 1a0081e7b4..7864b2ab04 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -2,7 +2,6 @@ import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } import classNames from 'classnames' import { AIReActChatContentsPProps, AIReferenceNodeProps, AIStreamNodeProps } from './AIReActChatContentsType' import styles from './AIReActChatContents.module.scss' -import { useCreation } from 'ahooks' import { AIMarkdown } from '@/pages/ai-agent/components/aiMarkdown/AIMarkdown' import { AIStreamChatContent } from '@/pages/ai-agent/components/aiStreamChatContent/AIStreamChatContent' import StreamCard from '@/pages/ai-agent/components/StreamCard' @@ -25,7 +24,8 @@ import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import AITextSyntaxFlow from '@/pages/ai-agent/components/aiTextSyntaxFlow/AITextSyntaxFlow' import { useCurrentStore } from '../hooks/useCurrentDataBySession' import { useStore } from 'zustand' - +import useCreation from 'ahooks/lib/useCreation' +import useDebounceFn from 'ahooks/lib/useDebounceFn' const getAIReferenceNodeByType = (contentType?: string) => { switch (contentType) { case AIStreamContentType.TEXT_MARKDOWN: @@ -247,14 +247,15 @@ export const openAIReferenceModal = (referenceList: ChatReferenceMaterialPayload export const AIReferenceNode: React.FC = React.memo((props) => { const { referenceList, className } = props + const onClick = useDebounceFn( + (e) => { + e.stopPropagation() + openAIReferenceModal(referenceList) + }, + { wait: 500, leading: true }, + ).run return ( - { - e.stopPropagation() - openAIReferenceModal(referenceList) - }} - > + [参考资料] ) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx index db634300a4..777132c4e6 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx @@ -629,11 +629,12 @@ export const AIRenderTaskFooterExtra: React.FC = R const taskStatus = useStore(store, (state) => state.taskStatus) const cancelTaskLoading = useStore(store, (state) => state.cancelTaskLoading) - const getTaskInfo = useMemoizedFn(() => { - return meta.currentTaskPlanID - }) + const status = useCreation(() => { + return meta.currentTaskPlanID?.status + }, [taskStatus.loading]) + const renderBtn = useMemoizedFn(() => { - switch (getTaskInfo()?.status) { + switch (status) { case AITaskStatus.inProgress: return ( void -}> = ({ setScrollToBottom }) => { +}> = memo(({ setScrollToBottom }) => { const { onExtraAction } = useTaskChatExtraAction() return ( ) -} +}) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts index 19d4f102ad..ab773b264f 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts @@ -33,7 +33,7 @@ export const useTaskChatExtraAction = () => { */ const closeChatReview = useMemoizedFn(() => { if (!!reviewInfo) { - globalSessionEngine.closeChatReview(sessionId, reviewInfo.chatType, reviewInfo.id) + globalSessionEngine.closeChatReview(sessionId, reviewInfo.id) } }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 5904253e42..9697c2bcb4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -21,7 +21,7 @@ import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHand import { genExecTasks, handleTaskPlanEnd, pushLogToOtherWindow } from './utils' import type { AIChatIPCStartParams, AIChatSendParams } from './type' import { yakitNotify } from '@/utils/notification' -import { type AIChatQSData, AIChatQSDataTypeEnum, AIToolResult, ChatListRenderType } from './aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum, AIToolResult } from './aiRender' import { aiAgentLogEmitter } from './AIAgentLogEmitter' import { v4 as uuidv4 } from 'uuid' import moment from 'moment' @@ -436,6 +436,7 @@ export class ChatMultiSessionController { /** 主动向grpc发送请求 */ public handleSendMessage(payload: AIChatSendParams) { + // console.log('handleSendMessage', payload) try { const { token, type, params, optionValue } = payload if (!this.readyChannels.has(token)) { @@ -576,12 +577,14 @@ export class ChatMultiSessionController { case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: review.data.selected = params.InteractiveJSONInput review.data.optionValue = optionValue - const tasks = review.data - const plans = genExecTasks(tasks.plans.root_task) - store.getState().updatePlanTree({ - task_tree: cloneDeep(plans), - root_task_name: tasks.plans.root_task.name, - }) + if (optionValue === 'continue') { + const tasks = review.data + const plans = genExecTasks(tasks.plans.root_task) + store.getState().updatePlanTree({ + task_tree: cloneDeep(plans), + root_task_name: tasks.plans.root_task.name, + }) + } store.getState().dispatchStreamingNode({ chatType: 'task', parentTaskId: review.TaskId, @@ -606,6 +609,7 @@ export class ChatMultiSessionController { } /** 向连接中的会话发送请求 */ private requestMessage(sessionId: string, request: AIInputEvent) { + // console.log('requestMessage', sessionId, request) ipcRenderer.invoke('send-ai-re-act', sessionId, request) } @@ -634,6 +638,7 @@ export class ChatMultiSessionController { public handleGrpcOutputEvent(sessionId: string, res: AIOutputEvent) { try { let ipcContent = Uint8ArrayToString(res.Content) || '' + // console.log('handleGrpcOutputEvent--', sessionId, res, ipcContent) if (res.Type === 'structured' && ipcContent.indexOf('level') > -1 && ipcContent.indexOf('message') > -1) { // 日志类型数据 const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Log diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index e82cfdc6ed..c8cdb811bf 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -465,6 +465,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { // 将操作记录渲染到列表上 store.getState().dispatchStreamingNode({ chatType: chatType, + parentTaskId: reviewDetail.TaskId, node: { token: reviewDetail.id, kind: 'item', diff --git a/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx b/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx index 149f631d60..b583aaa9c3 100644 --- a/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx +++ b/app/renderer/src/main/src/pages/yakRunner/YakRunner.tsx @@ -1282,7 +1282,6 @@ const YakRunnerWithAIInner: React.FC = (props) => { route={YakitRoute.YakScript} pageId={YAK_RUNNER_AI_PAGE_ID} focusModeLoop={YAK_RUNNER_FOCUS_MODE_CODE_SECURITY_AUDIT} - yakRunnerPageId={YAK_RUNNER_AI_PAGE_ID} > From e4137798855ec1c04d886766d12680d8dd382bec Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Fri, 24 Jul 2026 14:45:41 +0800 Subject: [PATCH 050/101] =?UTF-8?q?feat:=E9=87=8D=E5=90=AF=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D;fix:=E4=BF=AE=E5=A4=8DopenPopover=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E6=97=B6=E6=95=B0=E6=8D=AE=E5=85=88=E6=B8=85=E7=A9=BA=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E9=A1=B5=E9=9D=A2=E9=97=AA=E4=B8=80=E4=B8=8B=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=9Bfix:=E4=BF=AE=E5=A4=8D=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=8E=86=E5=8F=B2=E6=97=B6=E4=BC=A0=E7=9A=84=E6=89=80?= =?UTF-8?q?=E6=9C=89sessionid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/withHistoryAIReActChat.tsx | 9 +++-- .../src/main/src/pages/ai-agent/AIAgent.tsx | 5 +++ .../historyTaskTree/HistoryTaskTree.tsx | 36 ++++++++++--------- .../aiGroupStreamCard/AIGroupStreamCard.tsx | 12 ++++--- .../ai-agent/historyChat/HistoryChat.tsx | 2 +- .../main/src/pages/ai-agent/utils/index.ts | 36 ++++++++++++++++++- .../ai-re-act/aiReActChat/AIReActChat.tsx | 9 +++-- .../aiReActTaskChat/AIReActTaskChat.tsx | 1 - 8 files changed, 81 insertions(+), 29 deletions(-) diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index c4f843424b..81232ae2bd 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -1,5 +1,5 @@ import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef } from 'react' -import { useCreation, useMemoizedFn, useSafeState } from 'ahooks' +import { useCreation, useMemoizedFn, useSafeState, useUpdateEffect } from 'ahooks' import { cloneDeep } from 'lodash' import AIAgentContext, { @@ -9,7 +9,7 @@ import AIAgentContext, { import { AIAgentSetting } from '@/pages/ai-agent/aiAgentType' import { AIMentionCommandParams } from '@/pages/ai-agent/components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin' import { AIAgentSettingDefault } from '@/pages/ai-agent/defaultConstant' -import { createActiveChatSessionId, getAIReActRequestParams } from '@/pages/ai-agent/utils' +import { createActiveChatSessionId, getAIReActRequestParams, onReStart } from '@/pages/ai-agent/utils' import { AISession } from '@/pages/ai-agent/type/aiChat' import { HandleStartParams } from '@/pages/ai-agent/aiAgentChat/type' import { @@ -347,6 +347,11 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid return activeChat?.SessionID }, [activeChat]) + /** 切换会话 */ + useUpdateEffect(() => { + if (activeChat) onReStart({ activeChat, onStart }) + }, [activeID]) + const onStartRequest = useMemoizedFn((data: AIHandleStartParams) => { const newChat: AIHandleStartExtraProps = resolveStartExtraParams?.(data) ?? { chatId: activeChat?.SessionID, diff --git a/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx b/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx index 807de55430..859e5ad683 100644 --- a/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx @@ -32,6 +32,7 @@ import { grpcDeleteAISession } from './grpc' import { useChatIPC } from '../ai-re-act/hooks/useChatIPC' import { AISourceEnum } from '../ai-re-act/hooks/grpcApi' import { YakitRoute } from '@/enums/yakitRoute' +import { onReStart } from './utils' /** 清空用户缓存的固定值 */ export const AIAgentCacheClearValue = '20260113' @@ -99,6 +100,10 @@ export const AIAgent: React.FC = (props) => { const { onStart, onSend, onClose, onUpdatePageId } = useChatIPC(YakitRoute.AI_Agent, YakitRoute.AI_Agent) + useUpdateEffect(() => { + if (activeChat) onReStart({ activeChat, onStart }) + }, [activeChat?.SessionID]) + const store: AIAgentContextStore = useMemo(() => { return { setting: setting, diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx index 86aa68db7e..eb6ccd23dd 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx @@ -19,12 +19,12 @@ import { Tooltip } from 'antd' import { YakitTag } from '@/components/yakitUI/YakitTag/YakitTag' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import useAIAgentStore from '../../useContext/useStore' -import { formatAIAgentSetting } from '../../utils' import useAIAgentDispatcher from '../../useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' import { useCurrentMeta, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' +import { formatAIAgentSetting, onReStart } from '../../utils' export const HistoryTaskTree: React.FC = memo((props) => { const store = useCurrentStore() @@ -237,26 +237,28 @@ export const AIHistoryContinueTask: React.FC = React onSend({ token: sessionId, type: 'task', params: info }) } else if (execute) { onSendRecover(sendRecoverParamsRef.current) - } else if (activeChat?.SessionID && getSetting) { - const session = activeChat?.SessionID - onStart({ - token: session, - params: { - IsStart: true, - Params: { - ...formatAIAgentSetting(getSetting()), - UserQuery: '', - TimelineSessionID: session, - CoordinatorId: '', - Sequence: 1, - }, - }, - onSuccess: () => { - sendRecoverParamsRef.current && onSendRecover(sendRecoverParamsRef.current) + } else if (activeChat?.SessionID) { + onReStart({ + setting: { + ...formatAIAgentSetting(getSetting()), + UserQuery: '', + TimelineSessionID: activeChat?.SessionID, + CoordinatorId: '', + Sequence: 1, }, + activeChat, + onStart: (data) => onChatStart(data), }) } }) + const onChatStart = useMemoizedFn((data) => { + onStart({ + ...data, + onSuccess: () => { + sendRecoverParamsRef.current && onSendRecover(sendRecoverParamsRef.current) + }, + }) + }) return isShow() ? ( ([]) // 仅获取用于显示的 content(已应用打字效果) const { content } = useTypedStream({ @@ -65,12 +67,14 @@ export const AIGroupStreamNode: FC<{ return itemData?.reference || [] }, [open]) const popoverCode = useCreation(() => { - if (!openPopover) return [] - return itemData?.reference || [] - }, [openPopover]) + if (!openPopover) return prevPopoverCodeRef.current + const code = itemData?.reference || [] + prevPopoverCodeRef.current = code + return code + }, [openPopover, renderNum]) const hidden = useCreation(() => { - return !!itemData?.reference?.length + return !itemData?.reference?.length }, [renderNum, itemData?.reference?.length]) return ( diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx index 8e6ed82218..aeec39509a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx @@ -194,7 +194,7 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { BeforeTimestamp: beforeTimestamp, Source: historyQuerySources, } - const sessionIds = sessions.map((item) => item.SessionID) + const sessionIds = deletedChats.map((item) => item.SessionID) const source = getSetting().Source || 'ai' await handAIHistoryChatRemove({ grpcDeleteAISessionParams: { Filter: filter }, diff --git a/app/renderer/src/main/src/pages/ai-agent/utils/index.ts b/app/renderer/src/main/src/pages/ai-agent/utils/index.ts index af3512d1d5..664a17fa05 100644 --- a/app/renderer/src/main/src/pages/ai-agent/utils/index.ts +++ b/app/renderer/src/main/src/pages/ai-agent/utils/index.ts @@ -1,13 +1,16 @@ import { AIAgentSetting } from '../aiAgentType' import isNil from 'lodash/isNil' import { AIAgentSettingDefault, AttachedResourceKeyEnum, AttachedResourceTypeEnum } from '../defaultConstant' -import { AIAgentGrpcApi, AIInputEvent, AttachedResourceInfo } from '../../ai-re-act/hooks/grpcApi' +import { AIAgentGrpcApi, AIInputEvent, AIStartParams, AttachedResourceInfo } from '../../ai-re-act/hooks/grpcApi' import { AITaskInfoProps } from '../../ai-re-act/hooks/aiRender' import { HandleStartParams } from '../aiAgentChat/type' import { AIMentionCommandParams } from '../components/aiMilkdownInput/aiMilkdownMention/aiMentionPlugin' import { omit } from 'lodash' import { randomString } from '@/utils/randomUtil' import { isIRify } from '@/utils/envfile' +import { UseChatIPCStartParams } from '../useContext/AIAgentContext' +import { AISession } from '../type/aiChat' +import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' export const getPlanTaskLevel = (task: Pick) => task.level @@ -264,3 +267,34 @@ export const getAIReActRequestParams = (value: HandleStartParams) => { export const createActiveChatSessionId = () => { return randomString(40) } + +interface ReStartParams { + /** 优先级高于 activeChat.StartParams */ + setting?: AIStartParams + activeChat: AISession + onStart: (params: UseChatIPCStartParams) => void +} +/** 重启会话 */ +export const onReStart = (props: ReStartParams) => { + const { setting, activeChat, onStart } = props + if (!activeChat?.SessionID) return + const execute = globalSessionEngine.ensureSession(activeChat.SessionID)?.store?.getState()?.execute ?? false + if (!execute) { + const request: AIStartParams = setting ?? { + ...AIAgentSettingDefault, + ...activeChat.StartParams, + PreferSessionCachedConfig: true, + } + // 发送初始化参数 + const aiInputEvent: AIInputEvent = { + IsStart: true, + Params: { + ...request, + }, + } + onStart({ + token: activeChat?.SessionID, + params: aiInputEvent, + }) + } +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index e16f28a3ee..b5d39c85a6 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -130,9 +130,10 @@ export const AIReActChat: React.FC = React.memo( } const onStartChat = (res: AIHandleStartResProps) => { const { params, extraParams, onChat } = res + let newChat: AISession | undefined = undefined if (!sessionID) { // 创建新的聊天记录 - const newChat: AISession = { + newChat = { Id: extraParams?.chatId || session, Title: qs || `AI Agent - ${new Date().toLocaleString()}`, question: qs, @@ -145,8 +146,7 @@ export const AIReActChat: React.FC = React.memo( LastUsedAt: new Date().getTime(), isCreate: true, } - - setActiveChat && setActiveChat(newChat) + // setActiveChat && setActiveChat(newChat) emiter.emit( 'sessionData', JSON.stringify({ type: 'prependSession', payload: { ...newChat, isCreate: false } }), @@ -163,6 +163,9 @@ export const AIReActChat: React.FC = React.memo( onStart({ token: session, params, + onSuccess: () => { + if (!sessionID && newChat) setActiveChat && setActiveChat(newChat) + }, }) } if (!!startRequest) { diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx index 777132c4e6..eaacd00ffa 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx @@ -632,7 +632,6 @@ export const AIRenderTaskFooterExtra: React.FC = R const status = useCreation(() => { return meta.currentTaskPlanID?.status }, [taskStatus.loading]) - const renderBtn = useMemoizedFn(() => { switch (status) { case AITaskStatus.inProgress: From 3d3a09e11bf51dabb436769da1164b30b2323ce8 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Thu, 23 Jul 2026 17:13:37 +0800 Subject: [PATCH 051/101] =?UTF-8?q?feat:=20=E6=8E=A5=E5=85=A5=20AI=20?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=20IndexedDB=20=E6=8C=81=E4=B9=85=E5=8C=96?= =?UTF-8?q?=E4=B8=8E=E7=BB=9F=E4=B8=80=E5=BB=BA=E8=BF=9E=E6=81=A2=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 支持渲染树/正文按需 hydrate,start 时对齐 grpcOffset;修复恢复后自由对话空白,cancel 后 5s 兜底等待 session-end。 --- .../src/main/src/pages/MainOperator.tsx | 26 +- .../aiGroupStreamCard/AIGroupStreamCard.tsx | 3 +- .../main/src/pages/ai-agent/type/aiChat.d.ts | 18 +- .../AIReActChatContents.tsx | 11 +- .../AIReActChatContentsType.ts | 5 +- .../hooks/ChatMultiSessionController.ts | 522 +++++++++++++++--- .../src/pages/ai-re-act/hooks/aiRender.ts | 30 +- .../src/pages/ai-re-act/hooks/chatStore.ts | 24 +- .../hooks/grpcStreamHandler/aiOther.ts | 30 +- .../hooks/grpcStreamHandler/aiReview.ts | 25 +- .../hooks/grpcStreamHandler/aiSingleItem.ts | 12 + .../hooks/grpcStreamHandler/aiStream.ts | 50 +- .../hooks/grpcStreamHandler/aiToolResult.ts | 7 + .../hooks/persist/aiChatPersistStore.ts | 353 ++++++++++++ .../ai-re-act/hooks/persist/constants.ts | 20 + .../hooks/persist/contentPersistHelper.ts | 107 ++++ .../src/pages/ai-re-act/hooks/persist/type.ts | 43 ++ .../main/src/pages/ai-re-act/hooks/type.ts | 2 +- .../main/src/pages/ai-re-act/hooks/utils.ts | 10 +- 19 files changed, 1181 insertions(+), 117 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/persist/aiChatPersistStore.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/persist/constants.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/persist/contentPersistHelper.ts create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/persist/type.ts diff --git a/app/renderer/src/main/src/pages/MainOperator.tsx b/app/renderer/src/main/src/pages/MainOperator.tsx index 9a0e68143e..d3028e27d3 100644 --- a/app/renderer/src/main/src/pages/MainOperator.tsx +++ b/app/renderer/src/main/src/pages/MainOperator.tsx @@ -71,7 +71,9 @@ import { JSONParseLog } from '@/utils/tool' import { apiGetGlobalNetworkConfig } from './spaceEngine/utils' import { setAIModal } from './ai-agent/aiModelList/AIModelList' import { Trans } from 'react-i18next' -import aiChatMessageStore from './ai-agent/store/aiChatMessageStore' +// 旧 AI 消息 IndexedDB 生命周期已屏蔽,改由 aiChatPersistStore 接管预热/关闭 +// import aiChatMessageStore from './ai-agent/store/aiChatMessageStore' +import aiChatPersistStore from './ai-re-act/hooks/persist/aiChatPersistStore' const { ipcRenderer } = window.require('electron') @@ -297,14 +299,26 @@ const Main: React.FC = React.memo((props) => { checkAndShowDataMigration() }, []) + // 旧 aiChatMessageStore 预热/关闭已屏蔽 + // useEffect(() => { + // aiChatMessageStore.open().catch((err) => { + // yakitFailed('IndexedDB打开失败: ' + (err instanceof Error ? err.message : String(err))) + // }) + // return () => { + // aiChatMessageStore.close().catch((err) => { + // yakitFailed('IndexedDB关闭失败: ' + (err instanceof Error ? err.message : String(err))) + // }) + // } + // }, []) + + // 新 AI 会话持久化库:挂载预热、卸载关闭 useEffect(() => { - // 打开indexedDB - aiChatMessageStore.open().catch((err) => { - yakitFailed('IndexedDB打开失败: ' + (err instanceof Error ? err.message : String(err))) + aiChatPersistStore.open().catch((err) => { + yakitFailed('AI会话IndexedDB打开失败: ' + (err instanceof Error ? err.message : String(err))) }) return () => { - aiChatMessageStore.close().catch((err) => { - yakitFailed('IndexedDB关闭失败: ' + (err instanceof Error ? err.message : String(err))) + aiChatPersistStore.close().catch((err) => { + yakitFailed('AI会话IndexedDB关闭失败: ' + (err instanceof Error ? err.message : String(err))) }) } }, []) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index 60b171e8d4..12bc29723c 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -63,8 +63,9 @@ export const AIGroupStreamNode: FC<{ }, []) const modelCode = useCreation(() => { + // TODO - reference 现为 token[];按 token 查 IDB 展示完整交互后置,弹框内容暂空 if (!open) return [] - return itemData?.reference || [] + return [] as ChatReferenceMaterialPayload }, [open]) const popoverCode = useCreation(() => { if (!openPopover) return prevPopoverCodeRef.current diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 08165c9502..9f74d33cc1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -1,6 +1,6 @@ import type { TaskChatTaskInfo, UseChatIPCState } from '@/pages/ai-re-act/hooks/type' import type { AIAgentGrpcApi, AIInputEvent, AIStartParams } from '@/pages/ai-re-act/hooks/grpcApi' -import type { PlanItemDetailsData } from '@/pages/ai-re-act/hooks/aiRender' +import type { PlanItemDetailsData, SessionRenderContent } from '@/pages/ai-re-act/hooks/aiRender' import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' import type { AISource } from '@/pages/ai-re-act/hooks/grpcApi' import { PaginationSchema } from '@/pages/invoker/schema' @@ -178,11 +178,11 @@ export interface AIAgentChatData { /** yaklang_code_change 数据 */ yaklangCodeChange?: AIAgentGrpcApi.YaklangCodeChange - /** 获取历史数据时的最早ID节点 */ - beforeID: { - timelineID: number - chatID: number - } + /** + * 与 IDB sessionRender.grpcOffset 同步的事件游标。 + * start 时对齐后写入;仅加载后端存量历史(recovery 等)时更新。 + */ + grpcOffset: number /** 记录数据里所有的httpRunTimeIDs */ httpRunTimeIDs: string[] @@ -276,6 +276,12 @@ export interface AIAgentChatMetaData { /** 轮询获取最新记忆列表的定时器 */ memoryPollingTimer: NodeJS.Timeout | null + /** + * start 时从 IDB 读出的渲染树暂存,pong 后消费 hydrate,避免二次读库。 + * 消费后应置 undefined。 + */ + pendingSessionRender?: SessionRenderContent + /** * 记录自由对话下成组agent任务的taskID * 这里记录的是任务的taskID,不是任务的唯一值(问题id-任务id) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index 7864b2ab04..6e95a2c0a9 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -14,7 +14,7 @@ import { AIStreamContentType } from '../hooks/defaultConstant' import { Virtuoso } from 'react-virtuoso' import useVirtuosoAutoScroll from '../hooks/useVirtuosoAutoScroll' import useChatStreamLocateHighlight from '../hooks/useChatStreamLocateHighlight' -import { ChatReferenceMaterialPayload, ReActChatRenderElement } from '../hooks/aiRender' +import { ReActChatRenderElement } from '../hooks/aiRender' import Loading from '@/components/Loading/Loading' import { ScrollText } from '@/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading' import { showYakitModal } from '@/components/yakitUI/YakitModal/YakitModalConfirm' @@ -233,14 +233,15 @@ export const AIReActChatContents: React.FC = React.me }), ) -/** 挂到 body,避免 Virtuoso 滚出视口时卸载列表项导致弹窗消失 */ -export const openAIReferenceModal = (referenceList: ChatReferenceMaterialPayload, title = '参考资料') => { - const code = referenceList.map((item) => item.payload).join('\n') +/** 挂到 body,避免 Virtuoso 滚出视口时卸载列表项导致弹窗消失。 + * TODO -本阶段 referenceList 为 token 列表;按 token 查 IDB 展示完整交互后置。 + */ +export const openAIReferenceModal = (_referenceList: string[], title = '参考资料') => { const modal = showYakitModal({ title, cancelButtonProps: { style: { display: 'none' } }, bodyStyle: { height: 500 }, - content: , + content: , onOk: () => modal.destroy(), }) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts index 42e409fc8c..89984056b1 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts @@ -1,5 +1,5 @@ import type { ForwardedRef } from 'react' -import type { ChatReferenceMaterialPayload, ChatStream } from '../hooks/aiRender' +import type { ChatStream } from '../hooks/aiRender' export interface AIReActChatContentsPProps { ref: ForwardedRef @@ -19,6 +19,7 @@ export interface AIStreamNodeProps { } export interface AIReferenceNodeProps { - referenceList: ChatReferenceMaterialPayload + /** 参考资料 token 列表(payload 存 IDB,完整弹框交互后置) */ + referenceList: string[] className?: string } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 9697c2bcb4..3c0a537b8b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -21,14 +21,31 @@ import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHand import { genExecTasks, handleTaskPlanEnd, pushLogToOtherWindow } from './utils' import type { AIChatIPCStartParams, AIChatSendParams } from './type' import { yakitNotify } from '@/utils/notification' -import { type AIChatQSData, AIChatQSDataTypeEnum, AIToolResult } from './aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum, type AIToolResult, type SessionRenderContent } from './aiRender' import { aiAgentLogEmitter } from './AIAgentLogEmitter' import { v4 as uuidv4 } from 'uuid' import moment from 'moment' import type { YakitRouteType } from '@/enums/yakitRoute' +import { grpcQueryAIEvent } from '@/pages/ai-agent/grpc' +import aiChatPersistStore from './persist/aiChatPersistStore' +import { persistIndependentItem, persistToolResultIfTerminal } from './persist/contentPersistHelper' const { ipcRenderer } = window.require('electron') +/** recovery_history 单次拉取条数 */ +const RECOVERY_HISTORY_LIMIT = 60 + +const hasSessionRenderTree = (content?: SessionRenderContent): boolean => { + if (!content) return false + return ( + (content.casualElements?.length || 0) > 0 || + (content.taskElements?.length || 0) > 0 || + Object.keys(content.items || {}).length > 0 || + Object.keys(content.groups || {}).length > 0 || + Object.keys(content.tasks || {}).length > 0 + ) +} + /** 生成AI-Agent会话数据实例 */ const genAIAgentChatData = (): AIAgentChatData => { const defaultData: AIAgentChatData = { @@ -39,10 +56,7 @@ const genAIAgentChatData = (): AIAgentChatData => { systemStream: '', yaklangCodeChange: undefined, - beforeID: { - timelineID: -1, - chatID: -1, - }, + grpcOffset: 0, httpRunTimeIDs: [], riskRunTimeIDs: [], @@ -155,6 +169,30 @@ interface SessionOwner { /** 生成 route::pageId 的唯一标识 */ const makePageKey = (route: YakitRouteType, pageId: string): PageKey => `${route}::${pageId}` +/** + * 从渲染树快照收集「首屏」正文 token: + * casual / task 两侧顶层 elements 各取最后 topCount 条,并展开 group/task 的 childrenTokens。 + */ +const collectTopLevelContentTokens = (content: SessionRenderContent, topCount: number): string[] => { + const tokenSet = new Set() + const appendFromElements = (elements: SessionRenderContent['casualElements']) => { + const top = elements.slice(-topCount) + for (const el of top) { + tokenSet.add(el.token) + if (el.kind === 'group') { + const group = content.groups[el.token] + group?.childrenTokens?.forEach((t) => tokenSet.add(t)) + } else if (el.kind === 'task') { + const task = content.tasks[el.token] + task?.childrenTokens?.forEach((t) => tokenSet.add(t)) + } + } + } + appendFromElements(content.casualElements || []) + appendFromElements(content.taskElements || []) + return [...tokenSet] +} + export class ChatMultiSessionController { /** * 正向索引:按「当前」page 关页 / 全删 @@ -167,6 +205,31 @@ export class ChatMultiSessionController { private sessionOwnerMap = new Map() /** 存放已建立连接的会话session集合 */ private readyChannels = new Set() + /** 渲染树待落库 dirty 标记 */ + private renderPersistDirty = new Set() + /** 渲染树 debounce 定时器 */ + private renderPersistTimers = new Map>() + /** + * cancel 后等待 session-end 的兜底定时器:超时则手动走 handleSessionEnd(摘监听 + 收尾) + * 避免 end 丢失导致监听泄漏 / onEnd 永不触发 + */ + private sessionEndFallbackTimers = new Map>() + /** + * 待卸池的 session:forceClose 后保留监听与业务池,等 end / 兜底超时再 teardown + * value 为 dispose 时的 deletePersist 标记 + */ + private pendingDisposeSessions = new Map() + + private static readonly RENDER_PERSIST_DEBOUNCE_MS = 3000 + /** cancel 后等待真实 session-end 的最长时间,超时则合成 end */ + private static readonly SESSION_END_FALLBACK_MS = 5000 + /** 恢复会话时首屏灌入 contents 的顶层条数(两侧列表各自截取) */ + private static readonly INITIAL_CONTENT_TOP_COUNT = 20 + /** + * 无 UserQuery 建连进入恢复态:switchLoading 为 true, + * 待 hydrate / recovery_history 结束后再关 + */ + private sessionRestoreLoading = new Set() // #region session-source-route-pageId 索引管理相关逻辑 /** 将 session 写入 pageSessionMap 正向索引 */ @@ -285,13 +348,30 @@ export class ChatMultiSessionController { /** * 删除内存数据(仅 deleteSessions / onPageUnload 调用) - * 1. 先 forceClose(停流,幂等;关闭 ≠ 删除) - * 2. 主动摘 IPC(删除不等 end)并清业务池与归属索引 + * 1. 标记 pendingDispose + forceClose(cancel,保留 IPC 等 end) + * 2. 真实 session-end 或 5s 兜底后,再摘监听并清业务池与归属索引 + * @param deletePersist 是否同步删除 IDB。页面销毁只卸内存时应为 false,并先 flush 渲染树;显式删会话时为 true。 */ - private disposeSessionMemory(sessionId: string) { + private disposeSessionMemory(sessionId: string, deletePersist = false) { + if (deletePersist) { + // 显式删除:取消待写 debounce 即可,无需再刷进 IDB + const timer = this.renderPersistTimers.get(sessionId) + if (timer) { + clearTimeout(timer) + this.renderPersistTimers.delete(sessionId) + } + this.renderPersistDirty.delete(sessionId) + } else { + // 页面卸载:卸内存前先把渲染树刷进 IDB,保留可恢复数据 + this.flushSessionRender(sessionId) + } + + this.pendingDisposeSessions.set(sessionId, deletePersist) this.forceCloseSession({ sessionIds: [sessionId] }) - // 删除路径不等 session-end,主动摘监听,避免 end 回来 ensureSession 重建池 - this.closeIPCListeners(sessionId) + } + + /** end / 兜底超时后:摘池与归属索引(可选删 IDB) */ + private teardownDisposedSession(sessionId: string, deletePersist: boolean) { this.readyChannels.delete(sessionId) if (this.activeShowSession === sessionId) { @@ -308,6 +388,146 @@ export class ChatMultiSessionController { this.removeFromPageSessionMap(owner, sessionId) this.sessionOwnerMap.delete(sessionId) } + + if (deletePersist) { + aiChatPersistStore.deleteSessionPersist(sessionId).catch(() => {}) + } + } + // #endregion + + // #region 渲染树 dirty debounce 落库 + /** 渲染树结构变更后标记 dirty,3s 无新变更再写入 sessionRender */ + private markSessionRenderDirty(sessionId: string) { + this.renderPersistDirty.add(sessionId) + const prev = this.renderPersistTimers.get(sessionId) + if (prev) clearTimeout(prev) + const timer = setTimeout(() => { + // 到期后统一走 flush:摘 timer、清 dirty、写 IDB(外部强制 flush 也走同一套) + this.flushSessionRender(sessionId) + }, ChatMultiSessionController.RENDER_PERSIST_DEBOUNCE_MS) + this.renderPersistTimers.set(sessionId, timer) + } + + /** 立即刷写渲染树快照并清除 dirty/timer */ + private flushSessionRender(sessionId: string) { + const timer = this.renderPersistTimers.get(sessionId) + if (timer) { + clearTimeout(timer) + this.renderPersistTimers.delete(sessionId) + } + this.renderPersistDirty.delete(sessionId) + + const store = this.storePool.get(sessionId) + const rawData = this.rawDataPool.get(sessionId) + if (!store || !rawData) return + const state = store.getState() + const content: SessionRenderContent = { + items: { ...state.items }, + groups: { ...state.groups }, + tasks: { ...state.tasks }, + casualElements: [...state.casualChat.elements], + taskElements: [...state.taskChat.elements], + } + void this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) + } + // #endregion + + // #region IndexedDB 持久化门面(薄封装 aiChatPersistStore,错误兜底不抛穿 UI) + /** 从 sessionOwnerMap 取 source,兜底 'ai' */ + private resolvePersistSource(sessionId: string): AISource { + return this.sessionOwnerMap.get(sessionId)?.source || 'ai' + } + + /** 保存会话渲染树和grpcOffset */ + private async persistSetSessionRender(sessionId: string, content: SessionRenderContent, grpcOffset?: number) { + try { + const offset = grpcOffset ?? this.rawDataPool.get(sessionId)?.grpcOffset ?? 0 + await aiChatPersistStore.setSessionRender(sessionId, this.resolvePersistSource(sessionId), content, offset) + } catch { + // 持久化失败不打断主流程 + } + } + /** 获取会话渲染树和grpcOffset */ + private async persistGetSessionRender(sessionId: string) { + try { + return await aiChatPersistStore.getSessionRender(sessionId, this.resolvePersistSource(sessionId)) + } catch { + return undefined + } + } + + /** 按 token 获取会话消息内容 */ + private async persistGetSessionContent(sessionId: string, token: string) { + try { + return await aiChatPersistStore.getSessionContent(sessionId, token) + } catch { + return undefined + } + } + /** 按 token 列表批量获取会话消息内容 */ + private async persistGetSessionContents(sessionId: string, tokens: string[]) { + try { + return await aiChatPersistStore.getSessionContents(sessionId, tokens) + } catch { + return [] + } + } + + /** 按 token 列表批量获取会话参考资料(按落库时间正序) */ + public async getSessionReferenceMaterials(sessionId: string, tokens: string[]) { + try { + return await aiChatPersistStore.getSessionReferences(sessionId, tokens) + } catch { + return [] + } + } + + /** 按 source 清除该来源下所有 session 的持久化数据 */ + public async persistDeleteBySource(source: AISource) { + try { + await aiChatPersistStore.deletePersistBySource(source) + } catch { + // 持久化失败不打断主流程 + } + } + + /** + * 将渲染树写入 zustand,并批量灌回首屏 contents。 + * @param content 优先用 start 时暂存的树;未传则再读 IDB 整行 + * + * 注意:必须先把 contents 灌进 Map,再 hydrate 渲染树。 + * UI(StaticChatContent 等)靠 renderNum 订阅,rawData.contents 原地 set 不会触发重渲染; + * 若先 hydrate,组件会在 contents 仍空时读一次并卡住空白。 + * 任务规划树不走本方法,由 handleSessionStartSuccess 的 PLAN_EXEC_TASKS sync 拉取。 + */ + private async loadSessionRenderToMemory(sessionId: string, content?: SessionRenderContent) { + let tree = content + if (!tree) { + const row = await this.persistGetSessionRender(sessionId) + tree = row?.content + } + if (!hasSessionRenderTree(tree)) return false + + const { store, rawData } = this.ensureSession(sessionId) + + const tokens = collectTopLevelContentTokens(tree!, ChatMultiSessionController.INITIAL_CONTENT_TOP_COUNT) + if (tokens.length) { + const rows = await this.persistGetSessionContents(sessionId, tokens) + for (const row of rows) { + rawData.contents.set(row.token, row.content) + } + } + + store.getState().hydrateRenderTree(tree!) + return true + } + + /** 仅删除内存 contents 中的条目,不删渲染树 / IDB */ + public removeContentsFromMemory(sessionId: string, tokens: string[]) { + const { rawData } = this.ensureSession(sessionId) + for (const token of tokens) { + rawData.contents.delete(token) + } } // #endregion @@ -330,7 +550,12 @@ export class ChatMultiSessionController { /** 获取对应会话的所有数据集 */ public ensureSession(sessionId: string) { if (!this.storePool.has(sessionId)) { - this.storePool.set(sessionId, createChatStore()) + this.storePool.set( + sessionId, + createChatStore({ + onRenderStructureChange: () => this.markSessionRenderDirty(sessionId), + }), + ) this.rawDataPool.set(sessionId, genAIAgentChatData()) this.requestPool.set(sessionId, cloneDeep(AIAgentSettingDefault)) this.metaPool.set(sessionId, genAIAgentChatMetaData()) @@ -354,7 +579,11 @@ export class ChatMultiSessionController { Object.assign(request, config) } - /** 建立指定session会话的连接 */ + /** + * 建立指定 session 连接(新会话首问 / 打开历史 / 无问侧重连 共用)。 + * - 有 UserQuery:立刻上屏首问,pong 后发问;无树时不强制 recovery_history + * - 无 UserQuery:视为恢复态,置 switchLoading,pong 后 hydrate 或发 recovery_history + */ public handleStartSession(requestParams: AIChatIPCStartParams, cb?: (sessionId: string) => void) { const { token: sessionId, params, route, pageId } = requestParams const isExec = this.readyChannels.has(sessionId) @@ -364,8 +593,16 @@ export class ChatMultiSessionController { } const { request, store, rawData, meta } = this.ensureSession(sessionId) + const userQuery = (params.Params?.UserQuery || '').trim() + + // 恢复态:遮罩防止 hydrate / recovery 期间误点(UI 订阅 store.switchLoading) + if (userQuery) { + store.getState().updateState({ execute: true, casualTitle: '发送问题,开启会话...' }) + } else { + store.getState().updateState({ execute: true, switchLoading: true, casualTitle: '加载会话中...' }) + this.sessionRestoreLoading.add(sessionId) + } - store.getState().updateState({ execute: true, casualTitle: '发送问题,开启会话...' }) this.registerSessionChannel(sessionId, { route, pageId, @@ -374,7 +611,7 @@ export class ChatMultiSessionController { this.setActiveShowSession(sessionId) Object.assign(request, params.Params) - if (params.Params?.UserQuery) { + if (userQuery) { // 判断建立grpc连接时是否附带问题 // 如有,需要剥离出来,在grpc建立成功后再执行 const chatID = uuidv4() @@ -388,7 +625,7 @@ export class ChatMultiSessionController { meta.createChatQuestion = { IsFreeInput: true, - FreeInput: params.Params.UserQuery, + FreeInput: userQuery, AttachedResourceInfo: AttachedResourceInfos, FocusModeLoop: params.FocusModeLoop, } @@ -401,13 +638,14 @@ export class ChatMultiSessionController { chatType: 'reAct', type: AIChatQSDataTypeEnum.QUESTION, Timestamp: moment().unix(), - data: params.Params?.UserQuery || '', + data: userQuery, AIService: '', AIModelName: '', // showQS为了UI渲染方便,重新构建的字段 - extraValue: { showQS: params.Params?.UserQuery || '' }, + extraValue: { showQS: userQuery }, } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: 'reAct', node: { @@ -419,19 +657,114 @@ export class ChatMultiSessionController { } meta.onLinkSuccess = cb - ipcRenderer.invoke('start-ai-re-act', sessionId, params) + // 读 IDB + 查最新事件 id(不依赖本会话流),完成后再 IPC start + void this.prepareSessionPersistBeforeStart(sessionId).finally(() => { + ipcRenderer.invoke('start-ai-re-act', sessionId, params) + + // 建立会话连接时,在主进程进行了一次ping请求 + // 如果五秒没有返回pong消息,则再次进行ping请求 + if (meta.pingTimer) clearInterval(meta.pingTimer) + meta.pingTimer = setInterval(() => { + meta.pingSyncID = uuidv4() + this.requestMessage(sessionId, { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PING, + SyncID: meta.pingSyncID, + }) + }, 5000) + }) + } - // 建立会话连接时,在主进程进行了一次ping请求 - // 如果五秒没有返回pong消息,则再次进行ping请求 - if (meta.pingTimer) clearInterval(meta.pingTimer) - meta.pingTimer = setInterval(() => { - meta.pingSyncID = uuidv4() - this.requestMessage(sessionId, { - IsSyncMessage: true, - SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PING, - SyncID: meta.pingSyncID, - }) - }, 5000) + /** + * start 时立刻对齐 grpcOffset,并暂存 IDB 渲染树供 pong 后 hydrate。 + * 不依赖会话 gRPC 已连通。 + */ + private async prepareSessionPersistBeforeStart(sessionId: string) { + const { rawData, meta } = this.ensureSession(sessionId) + + try { + const [eventRes, row] = await Promise.all([ + grpcQueryAIEvent( + { + Filter: { SessionID: sessionId }, + Pagination: { Page: 1, Limit: 1, OrderBy: 'created_at', Order: 'desc' }, + }, + true, + ).catch(() => ({ Events: [] as AIOutputEvent[] })), + this.persistGetSessionRender(sessionId), + ]) + + const latestId = eventRes?.Events?.[0]?.ID ?? 0 + const final = row?.grpcOffset !== undefined && row.grpcOffset !== null ? row.grpcOffset : latestId + rawData.grpcOffset = final + + if (hasSessionRenderTree(row?.content)) { + meta.pendingSessionRender = row!.content + } else { + meta.pendingSessionRender = undefined + } + } catch { + rawData.grpcOffset = rawData.grpcOffset || 0 + meta.pendingSessionRender = undefined + } + } + + /** 关闭恢复态 loading(hydrate 完成或 recovery 结束时调用) */ + private finishSessionRestoreLoading(sessionId: string) { + if (!this.sessionRestoreLoading.has(sessionId)) return + this.sessionRestoreLoading.delete(sessionId) + const store = this.storePool.get(sessionId) + store?.getState().updateState({ switchLoading: false }) + } + + /** + * pong 后:消费暂存树或发 recovery_history。 + * @param needRecoveryHistory 无首问建连(恢复态)时为 true:空树则向后端拉历史 + */ + private async restoreSessionAfterPong(sessionId: string, needRecoveryHistory: boolean) { + const { store, rawData, meta } = this.ensureSession(sessionId) + const pending = meta.pendingSessionRender + meta.pendingSessionRender = undefined + + try { + if (hasSessionRenderTree(pending)) { + await this.loadSessionRenderToMemory(sessionId, pending) + const state = store.getState() + const content: SessionRenderContent = { + items: { ...state.items }, + groups: { ...state.groups }, + tasks: { ...state.tasks }, + casualElements: [...state.casualChat.elements], + taskElements: [...state.taskChat.elements], + } + await this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) + this.finishSessionRestoreLoading(sessionId) + } else if (needRecoveryHistory) { + // 保持 switchLoading,等 recovery_history 再关,避免 UI 提前可点 + this.requestMessage(sessionId, { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_HISTORY, + SyncJsonInput: JSON.stringify({ + start_id: rawData.grpcOffset, + limit: RECOVERY_HISTORY_LIMIT, + }), + }) + } else { + // 带首问的新会话:用当前 store 快照(可能已有首问)+ offset + const state = store.getState() + const content: SessionRenderContent = { + items: { ...state.items }, + groups: { ...state.groups }, + tasks: { ...state.tasks }, + casualElements: [...state.casualChat.elements], + taskElements: [...state.taskChat.elements], + } + await this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) + this.finishSessionRestoreLoading(sessionId) + } + } catch { + this.finishSessionRestoreLoading(sessionId) + } } /** 主动向grpc发送请求 */ @@ -476,6 +809,7 @@ export class ChatMultiSessionController { extraValue: { showQS: params.FreeInput || '' }, } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(token, chatData) store.getState().dispatchStreamingNode({ chatType: 'reAct', node: { @@ -537,6 +871,7 @@ export class ChatMultiSessionController { review.data.optionValue = optionValue store.getState().updateCasualReview(params.InteractiveId, 'remove') store.getState().incrementNodeVersion(review.id, 'item') + persistIndependentItem(token, review) break default: break @@ -564,6 +899,7 @@ export class ChatMultiSessionController { // review操作后正常展示在UI上 review.data.selected = params.InteractiveJSONInput review.data.optionValue = optionValue + persistIndependentItem(token, review) store.getState().dispatchStreamingNode({ chatType: 'task', parentTaskId: review.TaskId, @@ -577,6 +913,7 @@ export class ChatMultiSessionController { case AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE: review.data.selected = params.InteractiveJSONInput review.data.optionValue = optionValue + persistIndependentItem(token, review) if (optionValue === 'continue') { const tasks = review.data const plans = genExecTasks(tasks.plans.root_task) @@ -691,22 +1028,44 @@ export class ChatMultiSessionController { SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO, }) }, 5000) + + void this.restoreSessionAfterPong(sessionId, false).finally(() => { + this.handleSessionStartSuccess(sessionId) + meta.onLinkSuccess?.(sessionId) + meta.onLinkSuccess = undefined + }) } else { - // TODO - 调用历史数据恢复方法 + void this.restoreSessionAfterPong(sessionId, true).finally(() => { + this.handleSessionStartSuccess(sessionId) + meta.onLinkSuccess?.(sessionId) + meta.onLinkSuccess = undefined + }) } - this.handleSessionStartSuccess(sessionId) - meta.onLinkSuccess?.(sessionId) - meta.onLinkSuccess = undefined return } - // if (res.Type === 'structured' && res.NodeId === 'recovery_history') { - // const recoveryHistory = JSON.parse(ipcContent) as AIAgentGrpcApi.RecoveryHistory - // const chatStore = getChatDataStore() - // if (chatStore) chatStore.beforeID.chatID = recoveryHistory.next_start_id - // requestEvents.handleGrpcLoadMore(recoveryHistory) - // return - // } + if (res.Type === 'structured' && res.NodeId === 'recovery_history') { + try { + const recoveryHistory = JSON.parse(ipcContent) as AIAgentGrpcApi.RecoveryHistory + if (typeof recoveryHistory.next_start_id === 'number') { + rawData.grpcOffset = recoveryHistory.next_start_id + const state = store.getState() + const content: SessionRenderContent = { + items: { ...state.items }, + groups: { ...state.groups }, + tasks: { ...state.tasks }, + casualElements: [...state.casualChat.elements], + taskElements: [...state.taskChat.elements], + } + void this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) + } + } catch { + // ignore parse error + } + // recovery 批次结束:关闭旧会话 UI/逻辑 loading + this.finishSessionRestoreLoading(sessionId) + return + } let funcKey = res.Type if ( @@ -777,6 +1136,7 @@ export class ChatMultiSessionController { public pushDataToSession(sessionId: string, data: AIChatQSData) { const { store, rawData } = this.ensureSession(sessionId) rawData.contents.set(data.id, data) + persistIndependentItem(sessionId, data) store.getState().dispatchStreamingNode({ chatType: data.chatType, parentTaskId: data.TaskId, @@ -841,6 +1201,7 @@ export class ChatMultiSessionController { Object.assign(chatDetail.data.tool, toolResult) store.getState().incrementNodeVersion(chatDetail.id, 'item') + persistToolResultIfTerminal(sessionId, chatDetail) } /** 关闭会话的所有定时器 */ @@ -871,34 +1232,67 @@ export class ChatMultiSessionController { ipcRenderer.removeAllListeners(`${sessionId}-error`) } + /** 取消已有的 session-end 兜底定时器 */ + private clearSessionEndFallback(sessionId: string) { + const timer = this.sessionEndFallbackTimers.get(sessionId) + if (!timer) return + clearTimeout(timer) + this.sessionEndFallbackTimers.delete(sessionId) + } + + /** + * cancel 后武装 5s 兜底:若真实 -end 未到,手动走 handleSessionEnd + * 重复 cancel 会重置计时 + */ + private armSessionEndFallback(sessionId: string) { + this.clearSessionEndFallback(sessionId) + const timer = setTimeout(() => { + this.sessionEndFallbackTimers.delete(sessionId) + this.handleSessionEnd(sessionId) + }, ChatMultiSessionController.SESSION_END_FALLBACK_MS) + this.sessionEndFallbackTimers.set(sessionId, timer) + } + // 监听 session-error 事件 public handleSessionError(sessionId: string, error: any) { // 暂无业务逻辑处理 } - // 监听 session-end 事件 - public handleSessionEnd(sessionId: string, res: any) { - const data = this.ensureSession(sessionId) - const { store, meta } = data + // 监听 session-end 事件(含 cancel 后 5s 兜底合成) + public handleSessionEnd(sessionId: string, res?: any) { + this.clearSessionEndFallback(sessionId) + + // 池仍在:走完整收尾;若已 teardown 则只保证摘监听 + if (this.storePool.has(sessionId)) { + const data = this.ensureSession(sessionId) + const { store, meta } = data + + this.closeSessionTimers(meta) + // 任务规划结束后的相关逻辑 + handleTaskPlanEnd({ ...data, sessionId }) + // 核心状态改变 + store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) + this.readyChannels.delete(sessionId) + + const onEnd = meta.onEnd + if (onEnd) { + onEnd() + meta.onEnd = undefined + } + } - this.closeSessionTimers(meta) - // 任务规划结束后的相关逻辑 - handleTaskPlanEnd(data) - // 核心状态改变 - store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) - this.readyChannels.delete(sessionId) + this.closeIPCListeners(sessionId) - const onEnd = meta.onEnd - if (onEnd) { - onEnd() - meta.onEnd = undefined + const pendingDeletePersist = this.pendingDisposeSessions.get(sessionId) + if (pendingDeletePersist !== undefined) { + this.pendingDisposeSessions.delete(sessionId) + this.teardownDisposedSession(sessionId, pendingDeletePersist) } - this.closeIPCListeners(sessionId) } /** * 关闭会话连接(停流) - * - cancel IPC、更新 execute;不摘 IPC 监听(交给 handleSessionEnd) - * - 有 onEnd 时写入 meta,在 session-end 移除监听前执行 + * - cancel IPC、更新 execute;不立刻摘 IPC(等 session-end,或 5s 兜底合成 end) + * - 有 onEnd 时写入 meta,在 session-end / 兜底 移除监听前执行 * - **不会**删除业务池与归属索引;关闭 ≠ 删除 */ public forceCloseSession(params: { sessionIds: string[]; onEnd?: () => void }) { @@ -914,6 +1308,8 @@ export class ChatMultiSessionController { store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) } if (meta) this.closeSessionTimers(meta) + // 等真实 -end;超时则手动 handleSessionEnd,避免监听泄漏 / onEnd 挂死 + this.armSessionEndFallback(session) } } @@ -922,24 +1318,24 @@ export class ChatMultiSessionController { * - sessionIds 非空:删集合 * - sessionIds 空数组:按当前归属全删该 page 下该 source * - pageId 为空:按当前归属全删该 route 下所有 page 的 session - * - 内部会先 forceClose 再卸业务池与双索引 - * - grpc / IndexedDB 删除由上层负责 + * - 内部会先 forceClose 再卸业务池与双索引,并同步删除该会话 IDB 三表 + * - grpc 删除由上层负责 */ public deleteSessions(params: { sessionIds: string[]; source: AISource; route: YakitRouteType; pageId: string }) { const ids = this.resolveSessionIds(params) for (const sessionId of ids) { - this.disposeSessionMemory(sessionId) + this.disposeSessionMemory(sessionId, true) } } /** - * 页面生命周期卸载:卸该「当前」page 下所有 source 的 session 内存(同 delete,非仅 forceClose) - * 已 rebind 走的 session 不会被旧页清掉 + * 页面生命周期卸载:卸该「当前」page 下所有 source 的 session 内存(非仅 forceClose) + * 会 flush 渲染树后保留 IDB,供后续恢复;已 rebind 走的 session 不会被旧页清掉 */ public onPageUnload(route: YakitRouteType, pageId: string) { const ids = this.resolvePageSessionIds(route, pageId) for (const sessionId of ids) { - this.disposeSessionMemory(sessionId) + this.disposeSessionMemory(sessionId, false) } } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 3618a34b53..8c77a1b6ec 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -111,6 +111,15 @@ export interface ReActChatRenderElement { isHistory: boolean } +/** 会话渲染树快照(zustand items/groups/tasks + 两侧 elements) */ +export interface SessionRenderContent { + items: Record + groups: Record + tasks: Record + casualElements: ReActChatRenderElement[] + taskElements: ReActChatRenderElement[] +} + /** 控制UI渲染的数据数组元素 */ export interface ReActChatBaseInfo { chatType: ChatListRenderType @@ -386,8 +395,8 @@ export interface AIChatQSDataBase { TaskId?: AIOutputEvent['TaskId'] /** 前端专属数据,供前端逻辑和UI处理使用 */ extraValue?: CustomPluginExecuteFormValue | Record - /** 参考资料 */ - reference?: ChatReferenceMaterialPayload + /** 参考资料 token 列表(正文与资料分离;payload 存 sessionReference 表) */ + reference?: string[] /** 父集合组的key/token(如果被收集到集合组中, 则存在该字段) */ parentGroupToken?: string } @@ -551,17 +560,17 @@ export interface ChatStoreState { */ currentPlanReviewExtraUpdate: number - items: Record - groups: Record - tasks: Record + items: SessionRenderContent['items'] + groups: SessionRenderContent['groups'] + tasks: SessionRenderContent['tasks'] casualChat: { - elements: ReActChatRenderElement[] + elements: SessionRenderContent['casualElements'] /** aiChat.d.ts AIAgentChatData casualChat['planDetails'] */ todoListUpdate: number } taskChat: { - elements: ReActChatRenderElement[] + elements: SessionRenderContent['taskElements'] plan: CurrentExecTaskTree } // #endregion @@ -574,6 +583,7 @@ export interface ChatStoreState { yakExecResultLogs: StreamResult.Log[] /** 切换session时的loading状态 */ + /** 切换/恢复会话 loading(供 UI 遮罩与禁用交互,防止加载期间误点) */ switchLoading: boolean /** 用户主动取消问题的loading状态(自由对话) */ cancelCasualLoading: boolean @@ -635,6 +645,12 @@ export interface ChatStoreState { >, ) => void + /** + * 用持久化渲染树快照整体替换 items/groups/tasks/elements + * 供 ChatMultiSessionController 从 IDB 恢复会话使用 + */ + hydrateRenderTree: (content: SessionRenderContent) => void + updateTaskLoadingStatus: (status: Partial) => void /** 正在等待用户操作的reviewId列表 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index 0d21180591..b35f19df05 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -10,7 +10,13 @@ import { } from './defaultConstant' import { v4 as uuidv4 } from 'uuid' -export const createChatStore = () => { +export type CreateChatStoreOptions = { + /** 渲染树结构变更时回调(dispatchStreamingNode 改 elements/children 后),用于 dirty debounce 落库 */ + onRenderStructureChange?: () => void +} + +export const createChatStore = (options?: CreateChatStoreOptions) => { + const onRenderStructureChange = options?.onRenderStructureChange return createStore()( immer((set) => ({ execute: false, @@ -107,6 +113,16 @@ export const createChatStore = () => { Object.assign(state, partial) }), + /** 用持久化渲染树快照整体替换 items/groups/tasks/elements(供 Controller 恢复会话) */ + hydrateRenderTree: (content) => + set((state) => { + state.items = content.items || {} + state.groups = content.groups || {} + state.tasks = content.tasks || {} + state.casualChat.elements = content.casualElements || [] + state.taskChat.elements = content.taskElements || [] + }), + updateTaskLoadingStatus: (partial) => set((state) => { Object.assign(state.taskStatus, partial) @@ -139,7 +155,7 @@ export const createChatStore = () => { state.execFileRecord.set(keyName, keyList) }), - dispatchStreamingNode: ({ chatType, parentTaskId, node }) => + dispatchStreamingNode: ({ chatType, parentTaskId, node }) => { set((state) => { const isHistory = node.isHistory ?? false const elementRef = { kind: node.kind, token: node.token, chatType, isHistory } @@ -242,7 +258,9 @@ export const createChatStore = () => { } else { targetElements.push(elementRef) } - }), + }) + onRenderStructureChange?.() + }, /** 高频更新节点渲染 */ incrementNodeVersion: (token, kind) => diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index 84ab06b5b7..ec7ead6129 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -6,6 +6,11 @@ import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { DefaultCurrentExecTaskTree, DefaultPlanItemDetailsData } from '../defaultConstant' import has from 'lodash/has' +import { + persistIndependentItem, + persistToolResultIfTerminal, + deletePersistedContent, +} from '../persist/contentPersistHelper' const handleHttpFuzzRequestChange: AIMessageHandler = (request) => { const { res, store, rawData } = request @@ -44,8 +49,8 @@ const handleSessionTitle: AIMessageHandler = (request) => { store.getState().updateStateCount('sessionTitleUpdate') } -const handleStartPlanAndExecution: AIMessageHandler = (request) => { - const { res, store, rawData, meta, sendRequest } = request +const handleStartPlanAndExecution: AIMessageHandler = (requestInfo) => { + const { res, store, rawData, meta, sendRequest } = requestInfo if (res.Type !== 'start_plan_and_execution') return if (res.IsSync) return @@ -55,7 +60,7 @@ const handleStartPlanAndExecution: AIMessageHandler = (request) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const startInfo = JSON.parse(ipcContent) as AIAgentGrpcApi.AIStartPlanAndExecution if (!startInfo.coordinator_id) { - request.pushLog({ level: 'error', message: `${res.Type}数据, coordinator_id 为空` }) + requestInfo.pushLog({ level: 'error', message: `${res.Type}数据, coordinator_id 为空` }) return } meta.currentTaskPlanID = { @@ -83,6 +88,7 @@ const handleStartPlanAndExecution: AIMessageHandler = (request) => { type: AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP, } as AIChatQSData rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: 'task', node: { token: chatData.id, kind: 'task', type: chatData.type }, @@ -107,11 +113,12 @@ const handleEndPlanAndExecution: AIMessageHandler = (requestInfo) => { data: '', } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: 'task', node: { token: chatData.id, kind: 'item', type: chatData.type }, }) - handleTaskPlanEnd({ store, rawData, meta, request: requestInfo.request }) + handleTaskPlanEnd({ store, rawData, meta, request: requestInfo.request, sessionId: requestInfo.sessionId }) } } @@ -186,8 +193,8 @@ const handleReactTaskEnqueue: AIMessageHandler = (request) => { sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) } -const handleReactTaskDequeue: AIMessageHandler = (request) => { - const { res, chatType, store, rawData, meta, sendRequest } = request +const handleReactTaskDequeue: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, sendRequest } = requestInfo if (res.Type !== 'structured' || res.NodeId !== 'react_task_dequeue') return if (chatType === 'task') return @@ -219,10 +226,13 @@ const handleReactTaskDequeue: AIMessageHandler = (request) => { } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) if (data.react_task_user_input_uuid) { const qsDetail = rawData.contents.get(data.react_task_user_input_uuid) if (qsDetail && qsDetail.type === AIChatQSDataTypeEnum.QUESTION) { rawData.contents.delete(data.react_task_user_input_uuid) + // 清掉前端临时 uuid 对应的 IDB 孤儿行 + deletePersistedContent(requestInfo.sessionId, data.react_task_user_input_uuid) store.getState().replaceItemToken(data.react_task_user_input_uuid, chatData.id) return } @@ -348,6 +358,7 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { if (!taskDetail || taskDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) return taskDetail.data.status = info.react_task_now_status as AITaskStatusType store.getState().incrementNodeVersion(taskDetail.id, 'task') + persistIndependentItem(request.sessionId, taskDetail) } const handleTrafficCount: AIMessageHandler = (request) => { @@ -399,6 +410,8 @@ const handleTrafficCount: AIMessageHandler = (request) => { } if (!update) return store.getState().incrementNodeVersion(toolResult.id, 'item') + // 流量/风险计数:仅工具已终态时追加写正文 + persistToolResultIfTerminal(request.sessionId, toolResult) } const handlePlan: AIMessageHandler = (requestInfo) => { @@ -426,8 +439,8 @@ const handleYaklangCodeChange: AIMessageHandler = (requestInfo) => { store.getState().updateStateCount('yaklangCodeChangeUpdate') } -const handleReactTaskCreated: AIMessageHandler = (request) => { - const { res, chatType, store, rawData, meta } = request +const handleReactTaskCreated: AIMessageHandler = (requestInfo) => { + const { res, chatType, store, rawData, meta } = requestInfo if (res.Type !== 'structured' || res.NodeId !== 'react_task_created') return if (res.IsSync || chatType === 'task') return @@ -458,6 +471,7 @@ const handleReactTaskCreated: AIMessageHandler = (request) => { } as AIChatQSData rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) // planDetailsMap / 子任务收集仍按后端子任务 ID(res.TaskId)索引 if (rawData.casualChat.planDetailsMap.has(info.react_task_id)) { rawData.casualChat.planDetailsMap.set(info.react_task_id, cloneDeep(DefaultPlanItemDetailsData)) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index c8cdb811bf..d242125699 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -5,6 +5,7 @@ import { genBaseAIChatData, generateTaskNodeDataID, genExecTasks, isAutoExecuteR import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { AIReviewJudgeLevelMap } from '../defaultConstant' +import { persistIndependentItem } from '../persist/contentPersistHelper' const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { const { res, chatType, store, rawData, request, meta } = requestInfo @@ -35,7 +36,9 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { chatData.data.optionValue = target.params?.suggestion || 'continue' } rawData.contents.set(chatData.id, cloneDeep(chatData)) + // 仅已 release(会进渲染树)时落库;仅等待 release 的中间态不落库 if (target) { + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -60,6 +63,8 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { rawData.contents.set(chatData.id, cloneDeep(chatData)) // 该类型的实时数据只有任务规划才有 if (isAuto) { + // auto:立刻进渲染树,此时落库 + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -76,6 +81,7 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { root_task_name: tasks.plans.root_task.name, }) } else { + // 仅弹窗展示,操作完成(release)后再落库 store.getState().updateState({ currentPlanReviewToken: { token: chatData.id, renderNum: 0 } }) } } @@ -191,6 +197,7 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { // 非执行任务组的review,正常显示到UI上,根据review模式和用户主动操作,决定结果,并且操作后,也不在UI上展示结果 if (!taskGroupDetail || taskGroupDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { rawData.contents.set(chatData.id, cloneDeep(chatData)) + // 操作后会从列表删除,展示态不落库 store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ chatType: chatType, @@ -239,6 +246,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { } rawData.contents.set(chatData.id, cloneDeep(chatData)) if (target) { + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -256,8 +264,11 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { // 将数据存入hook里的缓存变量中 rawData.contents.set(chatData.id, cloneDeep(chatData)) if (chatType === 'task') { + // 仅弹窗,操作完成后再落库 store.getState().updateState({ currentPlanReviewToken: { token: chatData.id, renderNum: 0 } }) } else if (chatType === 'reAct') { + // 立刻进渲染树,需要落库 + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().updateCasualReview(chatData.id, 'add') store.getState().dispatchStreamingNode({ chatType: chatType, @@ -306,6 +317,7 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { } rawData.contents.set(chatData.id, cloneDeep(chatData)) if (target) { + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -330,6 +342,8 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { } // 将数据存入hook里的缓存变量中 rawData.contents.set(chatData.id, cloneDeep(chatData)) + // 立刻进渲染树,需要落库 + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -413,6 +427,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: reviewDetail.data.selected = JSON.stringify(data.params) reviewDetail.data.optionValue = data.params?.suggestion || 'continue' + persistIndependentItem(requestInfo.sessionId, reviewDetail) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: reviewDetail.TaskId, @@ -452,6 +467,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { reviewDetail.data.selected = JSON.stringify(data.params) reviewDetail.data.optionValue = data.params?.suggestion || 'continue' + persistIndependentItem(requestInfo.sessionId, reviewDetail) // 清空plan-review的异步拓展信息 meta.currentPlanReviewExtraId = '' meta.planReviewExtraData.clear() @@ -482,6 +498,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { reviewDetail.data.optionValue = data.params?.suggestion || 'continue' store.getState().updateCasualReview(reviewDetail.id, 'remove') store.getState().incrementNodeVersion(reviewDetail.id, 'item') + persistIndependentItem(requestInfo.sessionId, reviewDetail) } break case AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE: @@ -489,6 +506,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { if (noTaskReview) return reviewDetail.data.selected = JSON.stringify(data.params) reviewDetail.data.optionValue = data.params?.suggestion || 'continue' + persistIndependentItem(requestInfo.sessionId, reviewDetail) store.getState().updateState({ currentPlanReviewToken: { token: '', renderNum: 0 } }) store.getState().dispatchStreamingNode({ chatType: chatType, @@ -503,14 +521,16 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { } break case AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE: + reviewDetail.data.selected = JSON.stringify(data.params) + reviewDetail.data.optionValue = data.params?.suggestion || 'continue' if (chatType === 'reAct') { if (noCasualReview) return - reviewDetail.data.selected = JSON.stringify(data.params) - reviewDetail.data.optionValue = data.params?.suggestion || 'continue' store.getState().updateCasualReview(reviewDetail.id, 'remove') store.getState().incrementNodeVersion(reviewDetail.id, 'item') + persistIndependentItem(requestInfo.sessionId, reviewDetail) } else { if (noTaskReview) return + persistIndependentItem(requestInfo.sessionId, reviewDetail) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: reviewDetail.TaskId, @@ -564,6 +584,7 @@ const handleDetachedPlanReview: AIMessageHandler = (requestInfo) => { if (reviewDetail) { if (reviewDetail.type !== AIChatQSDataTypeEnum.DETACHED_PLAN_REQUIRE) return reviewDetail.data = { ...data } + // 弹窗展示态不落库,操作完成(release)后再写 const reviewStore = store.getState().currentPlanReviewToken reviewStore.renderNum += 1 store.getState().updateState({ currentPlanReviewToken: { ...reviewStore } }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index ae04416620..0fa50d8162 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -5,6 +5,7 @@ import { genBaseAIChatData, generateTaskNodeDataID } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum, type ReportFinishCardData } from '../aiRender' import { convertNodeIdToVerbose } from '../defaultConstant' import cloneDeep from 'lodash/cloneDeep' +import { persistIndependentItem } from '../persist/contentPersistHelper' const handleThought: AIMessageHandler = (requestInfo) => { const { res, chatType, store, rawData, meta } = requestInfo @@ -25,6 +26,7 @@ const handleThought: AIMessageHandler = (requestInfo) => { }), } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -58,6 +60,7 @@ const handleResult: AIMessageHandler = (requestInfo) => { }), } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -92,6 +95,7 @@ const handleFailReactTask: AIMessageHandler = (requestInfo) => { }), } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -130,6 +134,7 @@ const handleToolCallDecision: AIMessageHandler = (requestInfo) => { }), } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -164,6 +169,7 @@ const handleFailPlanAndExecution: AIMessageHandler = (requestInfo) => { }), } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -197,6 +203,7 @@ const handleApiRequestFailed: AIMessageHandler = (requestInfo) => { }), } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -231,6 +238,7 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { chatDetail.data.engine_status = status chatDetail.data.progress = status === 'working' ? payload.progress : chatDetail.data.progress store.getState().incrementNodeVersion(chatDetail.id, 'item') + persistIndependentItem(requestInfo.sessionId, chatDetail) } else { // 引擎结束态没有对应卡片时直接丢弃,保留原行为 if (status === 'finish') return @@ -255,6 +263,7 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { }), } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -300,6 +309,7 @@ const handleReportFinish: AIMessageHandler = (requestInfo) => { }), } rawData.contents.set(chatData.id, chatData) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, parentTaskId: chatData.TaskId, @@ -351,6 +361,7 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { } rawData.contents.set(chatData.id, chatData) meta.currentTaskPlanActiveNode.add(chatData.id) + persistIndependentItem(requestInfo.sessionId, chatData) store.getState().dispatchStreamingNode({ chatType: chatType, node: { @@ -390,6 +401,7 @@ const handlePopTask: AIMessageHandler = (requestInfo) => { meta.currentTaskPlanActiveNode.delete(chatDetail.id) chatDetail.data.status = info.task.task_status store.getState().incrementNodeVersion(chatDetail.id, 'task') + persistIndependentItem(requestInfo.sessionId, chatDetail) sendRequest && sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index 1c091317b3..23f01eb1ac 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -2,9 +2,17 @@ import type { AIMessageHandler, AIMessageHandlerParams } from '../type' import type { AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { genBaseAIChatData, generateTaskNodeDataID, isToolStderrStream, isToolStdoutStream } from '../utils' -import { AIChatQSDataTypeEnum } from '../aiRender' +import { AIChatQSDataTypeEnum, type AIChatQSData } from '../aiRender' import { AIStreamContentType, convertNodeIdToVerbose } from '../defaultConstant' import { aiAgentLogEmitter } from '../AIAgentLogEmitter' +import { v4 as uuidv4 } from 'uuid' +import aiChatPersistStore from '../persist/aiChatPersistStore' +import { + appendReferenceToContent, + persistIndependentItem, + persistToolResultIfTerminal, + upsertSessionContent, +} from '../persist/contentPersistHelper' /** 生成stream_group组数据 */ const genStreamGroupData = ( @@ -13,14 +21,15 @@ const genStreamGroupData = ( tokens: string[] } & AIMessageHandlerParams, ) => { - const { group, tokens, res, chatType, rawData, store, meta } = params + const { group, tokens, sessionId, res, chatType, rawData, store, meta } = params const groupDetail = rawData.contents.get(group) // 设置组数据详情 if (groupDetail && groupDetail.type === AIChatQSDataTypeEnum.STREAM_GROUP) { groupDetail.data.lastToken = tokens[tokens.length - 1] + persistIndependentItem(sessionId, groupDetail) } else { - rawData.contents.set(group, { + const chatData: AIChatQSData = { id: group, chatType: chatType, type: AIChatQSDataTypeEnum.STREAM_GROUP, @@ -38,12 +47,19 @@ const genStreamGroupData = ( AIService: '', AIModelName: '', Timestamp: res.Timestamp, - }) + } + rawData.contents.set(group, chatData) + persistIndependentItem(sessionId, chatData) } tokens.forEach((mapKey) => { const mapValue = rawData.contents.get(mapKey) - if (mapValue) mapValue.parentGroupToken = group + if (!mapValue) return + mapValue.parentGroupToken = group + // 流已结束后才入组:需补写 parentGroupToken + if (mapValue.type === AIChatQSDataTypeEnum.STREAM && mapValue.data.status === 'end') { + upsertSessionContent(sessionId, mapValue.id, mapValue) + } }) } @@ -332,6 +348,7 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 toolResult.data.tool.execError = toolErrorResult.content if (showUI) store.getState().incrementNodeVersion(toolResult.id, 'item') + persistToolResultIfTerminal(requestInfo.sessionId, toolResult) meta.toolStderrStreamData.delete(CallToolID) } return @@ -359,6 +376,9 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { : toolForStreamData.data.content toolResult.data.tool.toolStdoutContent = { content: displayContent, isShowAll } store.getState().incrementNodeVersion(toolResult.id, 'item') + // stdout 流结束:落库该 STREAM;若工具已终态则同步刷新 TOOL_RESULT + upsertSessionContent(requestInfo.sessionId, toolForStreamData.id, toolForStreamData) + persistToolResultIfTerminal(requestInfo.sessionId, toolResult) return } @@ -370,10 +390,11 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 streamData.data.status = 'end' store.getState().incrementNodeVersion(streamData.id, 'item') + upsertSessionContent(requestInfo.sessionId, streamData.id, streamData) } const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta } = requestInfo + const { sessionId, res, chatType, store, rawData } = requestInfo if (res.Type !== 'reference_material') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -382,8 +403,18 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { const chatData = rawData.contents.get(data.event_uuid) const toolResult = rawData.contents.get(res.CallToolID || '') + // 收数时自动生成 refToken,立刻落表3;内存只挂 token,不存完整 payload + const refToken = uuidv4() + aiChatPersistStore.setSessionReference(sessionId, refToken, data).catch(() => {}) + if (chatData) { - chatData.reference = (chatData.reference || []).concat([data]) + chatData.reference = [...(chatData.reference || []), refToken] + // STREAM:仅 status=end 后才追加写正文;未 end 只挂内存,等 finished 一并落库 + // 非 STREAM:有内存则立刻 upsert(独立单条晚到参考资料) + const shouldUpsertContent = chatData.type === AIChatQSDataTypeEnum.STREAM ? chatData.data.status === 'end' : true + if (shouldUpsertContent) { + upsertSessionContent(sessionId, chatData.id, chatData) + } if (store.getState().items[chatData.id]) { // 属于item元素,已经在UI上渲染了 store.getState().incrementNodeVersion(chatData.id, 'item') @@ -434,14 +465,14 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { // 现阶段直接注释,因为UI还没有写入专门的参考资料元素 // const chatData: AIChatQSData = { // ...genBaseAIChatData(res), - // id: data.event_uuid, + // id: refToken, // chatType: chatType, // type: AIChatQSDataTypeEnum.Reference_Material, // data: { // NodeId: res.NodeId, // NodeIdVerbose: res.NodeIdVerbose || convertNodeIdToVerbose(res.NodeId), // }, - // reference: [data], + // reference: [refToken], // TaskId: generateTaskNodeDataID({ // chatType, // planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, @@ -450,6 +481,7 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { // }), // } // rawData.contents.set(chatData.id, chatData) + // persistIndependentItem(requestInfo.sessionId, chatData) // store.getState().dispatchStreamingNode({ // chatType: chatType, // parentTaskId: chatData.TaskId, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts index f512ca1b8b..428e781f51 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -5,6 +5,7 @@ import { genBaseAIChatData, generateTaskNodeDataID } from '../utils' import { AIChatQSDataTypeEnum, type AIToolResult } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { DefaultAIToolResult, DefaultToolResultSummary } from '../defaultConstant' +import { persistToolResultIfTerminal, upsertSessionContent } from '../persist/contentPersistHelper' const handleToolCallStart: AIMessageHandler = (requestInfo) => { const { res, chatType, store, rawData, meta } = requestInfo @@ -64,6 +65,7 @@ const handleToolCallParam: AIMessageHandler = (requestInfo) => { toolResult.data.tool.reviewParams = cloneDeep(params) if (toolResult.data.type === 'result') store.getState().incrementNodeVersion(toolResult.id, 'item') + persistToolResultIfTerminal(requestInfo.sessionId, toolResult) } const handleToolCallWatcher: AIMessageHandler = (requestInfo) => { @@ -140,6 +142,7 @@ const handleToolCallLogDir: AIMessageHandler = (requestInfo) => { // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 toolResult.data.tool.dirPath = dir_path || '' if (toolResult.data.tool.status !== 'default') store.getState().incrementNodeVersion(toolResult.id, 'item') + persistToolResultIfTerminal(requestInfo.sessionId, toolResult) } const handleToolCallResult: AIMessageHandler = (requestInfo) => { @@ -191,6 +194,7 @@ const handleToolCallResult: AIMessageHandler = (requestInfo) => { } store.getState().incrementNodeVersion(toolResult.id, 'item') + upsertSessionContent(requestInfo.sessionId, toolResult.id, toolResult) } const handleToolCallSummary: AIMessageHandler = (requestInfo) => { @@ -232,6 +236,7 @@ const handleToolCallSummary: AIMessageHandler = (requestInfo) => { if (statusInfo !== 'default') { store.getState().incrementNodeVersion(toolResult.id, 'item') } + persistToolResultIfTerminal(requestInfo.sessionId, toolResult) } const handleToolCallStatus: AIMessageHandler = (requestInfo) => { @@ -259,6 +264,7 @@ const handleToolCallStatus: AIMessageHandler = (requestInfo) => { toolResult.data.isProcessingParams = status === 'processing_params' store.getState().incrementNodeVersion(toolResult.id, 'item') + persistToolResultIfTerminal(requestInfo.sessionId, toolResult) } const handleToolCallReason: AIMessageHandler = (requestInfo) => { @@ -284,6 +290,7 @@ const handleToolCallReason: AIMessageHandler = (requestInfo) => { toolResult.data.tool.reason = reason || '' if (toolResult.data.type !== 'create') store.getState().incrementNodeVersion(toolResult.id, 'item') + persistToolResultIfTerminal(requestInfo.sessionId, toolResult) } export const aiToolResultDataHandlers = { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/aiChatPersistStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/aiChatPersistStore.ts new file mode 100644 index 0000000000..18ccc0f135 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/aiChatPersistStore.ts @@ -0,0 +1,353 @@ +import type { AISource, AIAgentGrpcApi } from '../grpcApi' +import type { AIChatQSData, SessionRenderContent } from '../aiRender' +import { + DB_NAME, + DB_VERSION, + INDEX_BY_SESSION_ID, + INDEX_BY_SOURCE, + SESSION_CONTENT_STORE, + SESSION_REFERENCE_STORE, + SESSION_RENDER_STORE, +} from './constants' +import type { + SessionContentUpdater, + SessionReferenceItem, + SessionReferenceRecord, + SessionRenderRecord, + SessionContentRecord, + SessionContentItem, +} from './type' + +/** + * AI 会话独立持久化 Store(IndexedDB) + * - 三表:sessionRender / sessionContent / sessionReference + * - 直接存结构化对象(structured clone),不做整包 JSON.stringify + * - 单例 lazy-open:缓存 dbPromise,读写内 await open() 兜底 + */ +class AIChatPersistStore { + private dbPromise: Promise | null = null + + /** + * 打开(或复用)数据库连接;首次调用时建库建表。 + */ + open(): Promise { + if (!this.dbPromise) { + this.dbPromise = new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VERSION) + req.onerror = () => reject(req.error) + req.onsuccess = () => resolve(req.result) + req.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result + + // 表1:会话渲染树,主键 [sessionId, source];bySource 便于按来源批量查/删 + if (!db.objectStoreNames.contains(SESSION_RENDER_STORE)) { + const renderStore = db.createObjectStore(SESSION_RENDER_STORE, { + keyPath: ['sessionId', 'source'], + }) + renderStore.createIndex(INDEX_BY_SOURCE, 'source', { unique: false }) + } + + // 表2:会话正文,主键 [sessionId, token];按 session 批量删用 bySessionId + if (!db.objectStoreNames.contains(SESSION_CONTENT_STORE)) { + const contentStore = db.createObjectStore(SESSION_CONTENT_STORE, { + keyPath: ['sessionId', 'token'], + }) + contentStore.createIndex(INDEX_BY_SESSION_ID, 'sessionId', { unique: false }) + } + + // 表3:参考资料,主键 [sessionId, token];按 session 批量删用 bySessionId + if (!db.objectStoreNames.contains(SESSION_REFERENCE_STORE)) { + const refStore = db.createObjectStore(SESSION_REFERENCE_STORE, { + keyPath: ['sessionId', 'token'], + }) + refStore.createIndex(INDEX_BY_SESSION_ID, 'sessionId', { unique: false }) + } + } + }) + } + return this.dbPromise + } + + /** 关闭数据库连接并清空缓存的 Promise */ + async close(): Promise { + if (!this.dbPromise) return + const db = await this.dbPromise + db.close() + this.dbPromise = null + } + + /** 写入/覆盖会话渲染快照(content + grpcOffset 同写) */ + async setSessionRender( + sessionId: string, + source: AISource, + content: SessionRenderContent, + grpcOffset: number, + ): Promise { + const db = await this.open() + return new Promise((resolve, reject) => { + const tx = db.transaction(SESSION_RENDER_STORE, 'readwrite') + const store = tx.objectStore(SESSION_RENDER_STORE) + const record: SessionRenderRecord = { sessionId, source, content, grpcOffset } + const req = store.put(record) + req.onsuccess = () => resolve() + req.onerror = () => reject(req.error) + }) + } + + /** 读取会话渲染整行(含 grpcOffset) */ + async getSessionRender(sessionId: string, source: AISource): Promise { + const db = await this.open() + return new Promise((resolve, reject) => { + const tx = db.transaction(SESSION_RENDER_STORE, 'readonly') + const store = tx.objectStore(SESSION_RENDER_STORE) + const req = store.get([sessionId, source]) + req.onsuccess = () => { + resolve(req.result as SessionRenderRecord | undefined) + } + req.onerror = () => reject(req.error) + }) + } + + /** 写入/覆盖单条参考资料(token 由调用方在收数时 uuidv4 生成;写入时记录 createdAt) */ + async setSessionReference( + sessionId: string, + token: string, + content: AIAgentGrpcApi.ReferenceMaterialPayload, + ): Promise { + const db = await this.open() + return new Promise((resolve, reject) => { + const tx = db.transaction(SESSION_REFERENCE_STORE, 'readwrite') + const store = tx.objectStore(SESSION_REFERENCE_STORE) + const record: SessionReferenceRecord = { + sessionId, + token, + createdAt: Date.now(), + content, + } + const req = store.put(record) + req.onsuccess = () => resolve() + req.onerror = () => reject(req.error) + }) + } + + /** + * 按 token 列表批量取参考资料;缺的跳过,返回按存入时间 createdAt 正序。 + * 主键并行 get(适合一次最多约几十个 token,避免 getAll 整 session 过读)。 + */ + async getSessionReferences(sessionId: string, tokens: string[]): Promise { + if (!tokens.length) return [] + const db = await this.open() + return new Promise((resolve, reject) => { + const tx = db.transaction(SESSION_REFERENCE_STORE, 'readonly') + const store = tx.objectStore(SESSION_REFERENCE_STORE) + const result: SessionReferenceItem[] = [] + let pending = tokens.length + let failed = false + + tokens.forEach((token) => { + const req = store.get([sessionId, token]) + req.onsuccess = () => { + if (failed) return + const row = req.result as SessionReferenceRecord | undefined + if (row) { + result.push({ + token: row.token, + createdAt: row.createdAt ?? 0, + content: row.content, + }) + } + pending -= 1 + if (pending === 0) { + result.sort((a, b) => a.createdAt - b.createdAt) + resolve(result) + } + } + req.onerror = () => { + if (failed) return + failed = true + reject(req.error) + } + }) + }) + } + + /** 读取单条会话正文 */ + async getSessionContent(sessionId: string, token: string): Promise { + const db = await this.open() + return new Promise((resolve, reject) => { + const tx = db.transaction(SESSION_CONTENT_STORE, 'readonly') + const store = tx.objectStore(SESSION_CONTENT_STORE) + const req = store.get([sessionId, token]) + req.onsuccess = () => { + const row = req.result as SessionContentRecord | undefined + resolve(row?.content) + } + req.onerror = () => reject(req.error) + }) + } + + /** 删除单条会话正文(如 QUESTION 前端 uuid 被后端 id 替换后清孤儿行) */ + async deleteSessionContent(sessionId: string, token: string): Promise { + const db = await this.open() + return new Promise((resolve, reject) => { + const tx = db.transaction(SESSION_CONTENT_STORE, 'readwrite') + const store = tx.objectStore(SESSION_CONTENT_STORE) + const req = store.delete([sessionId, token]) + req.onsuccess = () => resolve() + req.onerror = () => reject(req.error) + }) + } + + /** + * 按 token 列表批量取会话正文;缺的跳过,返回顺序尽量跟入参 tokens 一致。 + * 主键并行 get(适合一次最多约几十个 token)。 + */ + async getSessionContents(sessionId: string, tokens: string[]): Promise { + if (!tokens.length) return [] + const db = await this.open() + return new Promise((resolve, reject) => { + const tx = db.transaction(SESSION_CONTENT_STORE, 'readonly') + const store = tx.objectStore(SESSION_CONTENT_STORE) + const result: Array = new Array(tokens.length) + let pending = tokens.length + let failed = false + + tokens.forEach((token, index) => { + const req = store.get([sessionId, token]) + req.onsuccess = () => { + if (failed) return + const row = req.result as SessionContentRecord | undefined + if (row) { + result[index] = { token: row.token, content: row.content } + } + pending -= 1 + if (pending === 0) { + resolve(result.filter((item): item is SessionContentItem => !!item)) + } + } + req.onerror = () => { + if (failed) return + failed = true + reject(req.error) + } + }) + }) + } + + /** + * 同事务 get → updater → put,写入/更新会话正文。 + * updater 收到旧值(可能 undefined),必须返回完整 AIChatQSData。 + */ + async setSessionContent(sessionId: string, token: string, updater: SessionContentUpdater): Promise { + const db = await this.open() + return new Promise((resolve, reject) => { + const tx = db.transaction(SESSION_CONTENT_STORE, 'readwrite') + const store = tx.objectStore(SESSION_CONTENT_STORE) + const getReq = store.get([sessionId, token]) + + getReq.onsuccess = () => { + const oldRow = getReq.result as SessionContentRecord | undefined + try { + const next = updater(oldRow?.content) + const record: SessionContentRecord = { sessionId, token, content: next } + const putReq = store.put(record) + putReq.onsuccess = () => resolve(next) + putReq.onerror = () => reject(putReq.error) + } catch (err) { + reject(err) + } + } + getReq.onerror = () => reject(getReq.error) + }) + } + + /** + * 清除指定 session 在三表中的全部记录。 + * - sessionRender:复合主键前缀无法直接 range 删,用游标扫 sessionId + * - sessionContent / sessionReference:走 bySessionId 索引 + */ + async deleteSessionPersist(sessionId: string): Promise { + const db = await this.open() + return new Promise((resolve, reject) => { + const tx = db.transaction([SESSION_RENDER_STORE, SESSION_CONTENT_STORE, SESSION_REFERENCE_STORE], 'readwrite') + + tx.oncomplete = () => resolve() + tx.onerror = () => reject(tx.error) + + // 表1:复合主键 [sessionId, source],用前缀范围删除该 session 全部 source + const renderStore = tx.objectStore(SESSION_RENDER_STORE) + const renderRange = IDBKeyRange.bound([sessionId, ''], [sessionId, '\uffff']) + const renderKeysReq = renderStore.getAllKeys(renderRange) + renderKeysReq.onsuccess = () => { + for (const key of renderKeysReq.result) { + renderStore.delete(key) + } + } + + // 表2 / 表3:索引批量删 + const deleteBySessionIndex = (storeName: string) => { + const store = tx.objectStore(storeName) + const index = store.index(INDEX_BY_SESSION_ID) + const keysReq = index.getAllKeys(IDBKeyRange.only(sessionId)) + keysReq.onsuccess = () => { + for (const key of keysReq.result) { + store.delete(key) + } + } + } + deleteBySessionIndex(SESSION_CONTENT_STORE) + deleteBySessionIndex(SESSION_REFERENCE_STORE) + }) + } + + /** + * 按 source 删除该来源下所有 session 在三表中的全部数据。 + * - 用 sessionRender.bySource 索引直接取该 source 的主键列表(不必全表扫) + * - 删渲染行后,再按 sessionId 清 sessionContent / sessionReference + */ + async deletePersistBySource(source: AISource): Promise { + const db = await this.open() + return new Promise((resolve, reject) => { + const tx = db.transaction([SESSION_RENDER_STORE, SESSION_CONTENT_STORE, SESSION_REFERENCE_STORE], 'readwrite') + + tx.oncomplete = () => resolve() + tx.onerror = () => reject(tx.error) + + const renderStore = tx.objectStore(SESSION_RENDER_STORE) + const sourceIndex = renderStore.index(INDEX_BY_SOURCE) + + // 等价 SQL: SELECT primaryKey FROM sessionRender WHERE source = ? + const keysReq = sourceIndex.getAllKeys(IDBKeyRange.only(source)) + keysReq.onsuccess = () => { + const keys = keysReq.result as Array<[string, AISource]> + const sessionIds = new Set() + + for (const key of keys) { + sessionIds.add(key[0]) + renderStore.delete(key) + } + + const deleteBySessionIndex = (storeName: string, sessionId: string) => { + const store = tx.objectStore(storeName) + const index = store.index(INDEX_BY_SESSION_ID) + const sessionKeysReq = index.getAllKeys(IDBKeyRange.only(sessionId)) + sessionKeysReq.onsuccess = () => { + for (const key of sessionKeysReq.result) { + store.delete(key) + } + } + } + + for (const sessionId of sessionIds) { + deleteBySessionIndex(SESSION_CONTENT_STORE, sessionId) + deleteBySessionIndex(SESSION_REFERENCE_STORE, sessionId) + } + } + keysReq.onerror = () => reject(keysReq.error) + }) + } +} + +const aiChatPersistStore = new AIChatPersistStore() + +export default aiChatPersistStore diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/constants.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/constants.ts new file mode 100644 index 0000000000..0d34bb8a6f --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/constants.ts @@ -0,0 +1,20 @@ +/** AI 会话持久化 IndexedDB 库名(与旧 aiChatMessageDB 隔离) */ +export const DB_NAME = 'aiChatPersistDB' + +/** 数据库版本号(升版本会触发 onupgradeneeded,用于加表/加索引) */ +export const DB_VERSION = 1 + +/** 会话渲染树表:items / groups / tasks / 列表元素 */ +export const SESSION_RENDER_STORE = 'sessionRender' + +/** 会话正文表:整条 AIChatQSData(reference 仅为 token 列表) */ +export const SESSION_CONTENT_STORE = 'sessionContent' + +/** 会话参考资料表:单条 ReferenceMaterialPayload */ +export const SESSION_REFERENCE_STORE = 'sessionReference' + +/** 按 sessionId 批量列出/删除用的索引名(建在 sessionContent / sessionReference) */ +export const INDEX_BY_SESSION_ID = 'bySessionId' + +/** 按 source 查询/删除用的索引名(建在 sessionRender) */ +export const INDEX_BY_SOURCE = 'bySource' diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/contentPersistHelper.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/contentPersistHelper.ts new file mode 100644 index 0000000000..2899068c8e --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/contentPersistHelper.ts @@ -0,0 +1,107 @@ +import cloneDeep from 'lodash/cloneDeep' +import { AIChatQSDataTypeEnum, type AIChatQSData } from '../aiRender' +import aiChatPersistStore from './aiChatPersistStore' +import type { SessionContentUpdater } from './type' + +/** 同一 sessionId::token 的串行写队列,避免异步 put 未完成又来更新导致丢写 */ +const contentWriteChains = new Map>() + +const contentKey = (sessionId: string, token: string) => `${sessionId}::${token}` + +function enqueueContentWrite(sessionId: string, token: string, task: () => Promise): Promise { + const key = contentKey(sessionId, token) + const next = (contentWriteChains.get(key) || Promise.resolve()).then(task, task) + contentWriteChains.set( + key, + next.finally(() => { + if (contentWriteChains.get(key) === next) { + contentWriteChains.delete(key) + } + }), + ) + return next +} + +/** 生成可 structured-clone 的正文快照 */ +export const clonePersistableContent = (data: AIChatQSData): AIChatQSData => { + return cloneDeep(data) +} + +/** + * 写入/覆盖会话正文(入队串行)。 + * next 为完整对象时直接 put;为 updater 时走同事务 get→update→put。 + */ +export const upsertSessionContent = ( + sessionId: string, + token: string, + next: AIChatQSData | SessionContentUpdater, +): Promise => { + return enqueueContentWrite(sessionId, token, async () => { + try { + if (typeof next === 'function') { + await aiChatPersistStore.setSessionContent(sessionId, token, next) + } else { + const snapshot = clonePersistableContent(next) + await aiChatPersistStore.setSessionContent(sessionId, token, () => snapshot) + } + } catch { + // 持久化失败不打断主流程 + } + }) +} + +/** 独立单条首次/更新落库(薄封装,便于各 handler 统一调用) */ +export const persistIndependentItem = (sessionId: string, data: AIChatQSData): Promise => { + return upsertSessionContent(sessionId, data.id, data) +} + +/** + * 删除已落库正文(走同 token 串行队列,避免未完成的 put 在 delete 后又写回孤儿行)。 + * 典型场景:QUESTION 前端 uuid → 后端 taskId 替换。 + */ +export const deletePersistedContent = (sessionId: string, token: string): Promise => { + return enqueueContentWrite(sessionId, token, async () => { + try { + await aiChatPersistStore.deleteSessionContent(sessionId, token) + } catch { + // 持久化失败不打断主流程 + } + }) +} + +/** + * 向已落库正文的 reference 追加 refToken。 + * 无旧记录时跳过(stream 尚未 end 时参考资料只挂内存,等 end 一并写入)。 + */ +export const appendReferenceToContent = ( + sessionId: string, + contentToken: string, + refToken: string, +): Promise => { + return enqueueContentWrite(sessionId, contentToken, async () => { + try { + const old = await aiChatPersistStore.getSessionContent(sessionId, contentToken) + if (!old) return + const reference = [...(old.reference || [])] + if (!reference.includes(refToken)) { + reference.push(refToken) + } + const next: AIChatQSData = { ...clonePersistableContent(old), reference } + await aiChatPersistStore.setSessionContent(sessionId, contentToken, () => next) + } catch { + // 持久化失败不打断主流程 + } + }) +} + +/** TOOL_RESULT 终态:success / failed / user_cancelled */ +export const isToolResultTerminalStatus = (status: string | undefined): boolean => { + return status === 'success' || status === 'failed' || status === 'user_cancelled' +} + +/** 工具已终态时追加写正文;未终态不落库 */ +export const persistToolResultIfTerminal = (sessionId: string, toolResult: AIChatQSData): Promise | void => { + if (toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) return + if (!isToolResultTerminalStatus(toolResult.data.tool.status)) return + return upsertSessionContent(sessionId, toolResult.id, toolResult) +} diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/type.ts new file mode 100644 index 0000000000..e60c939e2f --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/type.ts @@ -0,0 +1,43 @@ +import type { AISource, AIAgentGrpcApi } from '../grpcApi' +import type { AIChatQSData, SessionRenderContent } from '../aiRender' + +/** sessionRender 表一行记录 */ +export interface SessionRenderRecord { + sessionId: string + source: AISource + content: SessionRenderContent + /** 与后端事件游标对齐;写树时一并 put */ + grpcOffset?: number +} + +/** sessionContent 表一行记录(content 内 reference 仅为 token 列表) */ +export interface SessionContentRecord { + sessionId: string + token: string + content: AIChatQSData +} + +/** getSessionContents 单条返回 */ +export interface SessionContentItem { + token: string + content: AIChatQSData +} + +/** sessionReference 表一行记录 */ +export interface SessionReferenceRecord { + sessionId: string + token: string + /** 存入时间(毫秒时间戳),取数时按此正序 */ + createdAt: number + content: AIAgentGrpcApi.ReferenceMaterialPayload +} + +/** getSessionReferences 单条返回 */ +export interface SessionReferenceItem { + token: string + createdAt: number + content: AIAgentGrpcApi.ReferenceMaterialPayload +} + +/** setSessionContent 的 updater:同事务内 get → update → put */ +export type SessionContentUpdater = (old: AIChatQSData | undefined) => AIChatQSData diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts index 8899202c30..8a6a162267 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts @@ -145,7 +145,7 @@ export interface UseChatIPCState { /** 场景状态(仅供自由对话[reAct])使用 */ focusMode: string - /** 切换session时的loading状态 */ + /** 切换/恢复会话 loading(UI 遮罩与禁用交互;无 UserQuery 建连时置 true,hydrate/recovery 结束后 false) */ switchLoading: boolean /** 任务规划历史数据-任务树 */ planHistoryList: AIAgentGrpcApi.PlanHistoryList diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts index edbf249720..f4699f0795 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts @@ -19,6 +19,7 @@ import { aiAgentLogEmitter } from './AIAgentLogEmitter' import cloneDeep from 'lodash/cloneDeep' import { DefaultPlanLoadingStatus } from './defaultConstant' import { ChatMultiSessionController } from './ChatMultiSessionController' +import { persistIndependentItem } from './persist/contentPersistHelper' /** * 任务节点内的数据生成任务节点ID @@ -54,10 +55,10 @@ export const genBaseAIChatData = (info: AIOutputEvent) => { } /** 任务规划结束后的所有数据处理 */ -export const handleTaskPlanEnd: (requestInfo: ReturnType) => void = ( - requestInfo, -) => { - const { store, rawData, meta } = requestInfo +export const handleTaskPlanEnd: ( + requestInfo: ReturnType & { sessionId: string }, +) => void = (requestInfo) => { + const { sessionId, store, rawData, meta } = requestInfo // 将UI列表里正在执行中的任务组状态变成error const actives = Array.from(meta.currentTaskPlanActiveNode) @@ -69,6 +70,7 @@ export const handleTaskPlanEnd: (requestInfo: ReturnType Date: Fri, 24 Jul 2026 17:16:47 +0800 Subject: [PATCH 052/101] =?UTF-8?q?feat:=E5=B0=86meta=E4=B8=AD=E7=9A=84cur?= =?UTF-8?q?rentTaskPlanID=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E5=88=B0ChatStoreState=E4=B8=AD=E5=B9=B6=E4=B8=8Etask?= =?UTF-8?q?Status=E8=9E=8D=E4=B8=BA=E4=B8=80=E4=BD=93;fix:onReStart?= =?UTF-8?q?=E9=BB=98=E8=AE=A4=E9=87=8D=E7=BD=AEUserQuery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/aiAgentType.d.ts | 2 -- .../chatTemplate/AIAgentChatTemplate.tsx | 7 ++-- .../chatTemplate/TaskLoading/TaskLoading.tsx | 16 +++++---- .../historyTaskTree/HistoryTaskTree.tsx | 35 +++++++------------ .../ConcurrentStreamCard.tsx | 6 ++-- .../aiReActChatReview/AIReActChatReview.tsx | 5 ++- .../src/pages/ai-agent/defaultConstant.tsx | 2 +- .../main/src/pages/ai-agent/type/aiChat.d.ts | 4 +-- .../main/src/pages/ai-agent/utils/index.ts | 1 + .../aiReActTaskChat/AIReActTaskChat.tsx | 18 ++++------ .../aiReActTaskChat/useTaskChatExtraAction.ts | 9 +++-- .../hooks/ChatMultiSessionController.ts | 5 ++- .../pages/ai-re-act/hooks/defaultConstant.ts | 4 +++ .../main/src/pages/ai-re-act/hooks/grpcApi.ts | 2 +- .../hooks/grpcStreamHandler/aiOther.ts | 25 +++++++------ .../hooks/grpcStreamHandler/aiReview.ts | 11 +++--- .../hooks/grpcStreamHandler/aiSingleItem.ts | 26 +++++++------- .../hooks/grpcStreamHandler/aiStream.ts | 6 ++-- .../hooks/grpcStreamHandler/aiToolResult.ts | 2 +- .../main/src/pages/ai-re-act/hooks/type.ts | 6 ++++ 20 files changed, 96 insertions(+), 96 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts b/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts index d35a26e8b0..e2e0c789b1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentType.d.ts @@ -2,7 +2,6 @@ import { Dispatch, SetStateAction } from 'react' import { AISession } from './type/aiChat' import { AITabsEnum } from './defaultConstant' import { AIStartParams } from '../ai-re-act/hooks/grpcApi' -import type { PlanLoadingStatus } from '../ai-re-act/hooks/type' export interface AIAgentProps { pageId: string } @@ -51,7 +50,6 @@ export type AITabsEnumType = `${AITabsEnum}` export interface AIAgentChatStreamProps { defaultExpand?: boolean scrollToBottom: boolean - taskStatus: PlanLoadingStatus } // #endregion diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx index 52c94db562..e7d2b840e5 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx @@ -171,7 +171,7 @@ export const AIChatLeftSide: React.FC = memo((props) => { /** @name chat-信息流展示 */ const TYPE = 'task' export const AIAgentChatStream: React.FC = memo((props) => { - const { scrollToBottom, taskStatus } = props + const { scrollToBottom } = props const listRootRef = useRef(null) const [highlightedItem, setHighlightedItem] = useState<{ index: number; token: number } | null>(null) @@ -246,10 +246,7 @@ export const AIAgentChatStream: React.FC = memo((props) [], ) - const Footer = useCallback( - () => , - [taskStatus], - ) + const Footer = useCallback(() => , []) const Header = useCallback( () => taskLoadMoreLoading ? ( diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading.tsx index b8b0b3872e..0e533d8d3b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading.tsx @@ -1,10 +1,11 @@ import Loading from '@/components/Loading/Loading' -import { PlanLoadingStatus } from '@/pages/ai-re-act/hooks/type' import { FC, memo, useEffect, useRef, useState } from 'react' import styles from './TaskLoading.module.scss' import useAISystemStream from '@/pages/ai-re-act/hooks/useAISystemStream' import { useAISystemStreamText } from '@/store/aiSystemStream' import classNames from 'classnames' +import { useStore } from 'zustand' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' export const ScrollText: FC<{ text?: string }> = ({ text = '' }) => { const wrapperRef = useRef(null) @@ -42,17 +43,20 @@ export const ScrollText: FC<{ text?: string }> = ({ text = '' }) => { } const TaskLoading: FC<{ - taskStatus: PlanLoadingStatus className?: string -}> = ({ taskStatus, className }) => { +}> = ({ className }) => { + const store = useCurrentStore() + const task = useStore(store, (state) => state.taskStatus.task) + const loading = useStore(store, (state) => state.taskStatus.loading) + const plan = useStore(store, (state) => state.taskStatus.plan) const systemStream = useAISystemStreamText() const { displayValue, mode } = useAISystemStream({ - value: taskStatus.task, + value: task, systemStream, }) return (
- {taskStatus.loading && ( + {loading && ( <> -
{taskStatus.plan}
+
{plan}
{mode === 'value' ? displayValue : } diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx index eb6ccd23dd..0ffc24eb77 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx @@ -28,13 +28,12 @@ import { formatAIAgentSetting, onReStart } from '../../utils' export const HistoryTaskTree: React.FC = memo((props) => { const store = useCurrentStore() - const meta = useCurrentMeta() const planHistoryList = useStore(store, (state) => state.planHistoryList ?? []) const taskTree = useStore(store, (state) => state.taskChat.plan.task_tree ?? []) const taskName = useStore(store, (state) => state.taskChat.plan.root_task_name ?? '') const currentTaskItem = useCreation(() => { - const coordinatorId = meta.currentTaskPlanID?.coordinatorId || '' + const coordinatorId = store.getState().taskStatus.coordinatorId || '' const item: AIAgentGrpcApi.PlanHistory = { coordinator_id: coordinatorId, created_at: '', @@ -143,9 +142,8 @@ export const AIHistoryContinueTask: React.FC = React const { t } = useI18nNamespaces(['aiAgent']) const sessionId = useCurrentSessionId() - const meta = useCurrentMeta() const store = useCurrentStore() - const taskStatus = useStore(store, (state) => state.taskStatus) + const isExecuting = useStore(store, (state) => state.taskStatus.loading) const cancelTaskLoading = useStore(store, (state) => state.cancelTaskLoading) const execute = useStore(store, (state) => state.execute) @@ -156,33 +154,26 @@ export const AIHistoryContinueTask: React.FC = React const sendRecoverParamsRef = useRef() - const isExecuting = useCreation(() => { - return taskStatus.loading - }, [taskStatus.loading]) - const loading = useCreation(() => { return sendRecoverParamsRef.current?.taskId === taskId && isExecuting }, [isExecuting, taskId]) - const getTaskInfo = useMemoizedFn(() => { - return meta.currentTaskPlanID - }) - const getTaskId = useMemoizedFn(() => { - const taskInfo = getTaskInfo() - return taskInfo?.taskID || '' - }) useUpdateEffect(() => { if (!isExecuting && sendRecoverParamsRef.current) { onSendRecover(sendRecoverParamsRef.current) } }, [isExecuting]) + /** + * TODO - 现在的版本中,任务中断后状态不一定是error + */ const isShow = useMemoizedFn(() => { - const currentCoordinatorId = getTaskInfo()?.coordinatorId || '' - const taskInfo = getTaskInfo() + const taskStatus = store.getState().taskStatus + const currentCoordinatorId = taskStatus?.coordinatorId || '' + let show = true if (!execute) return true if (coordinatorId === currentCoordinatorId) { - show = taskInfo?.status !== AITaskStatus.inProgress + show = taskStatus?.status !== AITaskStatus.inProgress } // 如果当前有任务正在等待被恢复 if (sendRecoverParamsRef.current) { @@ -191,10 +182,9 @@ export const AIHistoryContinueTask: React.FC = React sendRecoverParamsRef.current.coordinatorId === coordinatorId && sendRecoverParamsRef.current.taskId === taskId ) } - const isStopping = taskInfo?.status !== AITaskStatus.inProgress && taskStatus.loading // 如果系统正处于正在停止/取消任务的全局 Loading 状态,或当前任务本身正处于停止进行中的状态 - if (cancelTaskLoading || isStopping) { + if (cancelTaskLoading) { return false } @@ -210,12 +200,13 @@ export const AIHistoryContinueTask: React.FC = React SyncID: randomString(8), } onSend({ token: sessionId, type: 'task', params: info }) - meta.currentTaskPlanID = undefined + store.getState().updateTaskLoadingStatus({ taskID: '', status: AITaskStatus.created, coordinatorId: '' }) sendRecoverParamsRef.current = undefined }) const onRecover = useMemoizedFn(() => { - const currentTaskId = getTaskId() + const taskStatus = store.getState().taskStatus + const currentTaskId = taskStatus.taskID if (!coordinatorId) return sendRecoverParamsRef.current = { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index dcaef42a20..ad12a2c837 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -5,7 +5,7 @@ import { AIChatQSDataTypeEnum, ChatTaskDefaultGroup, type ChatTaskNodeGroup } fr import { getAIStatusPresentation } from '../../utils/AIStatusUtils' import { useVectorStripeBg } from './hooks/useVectorStripeBg' import { useConcurrentStreamCardStyle } from './hooks/useConcurrentStreamCardStyle' -import { useCurrentStore, useCurrentRawData, useCurrentMeta } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useCurrentStore, useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' import type { ConcurrentStreamCardHeardWrapperProps, @@ -77,9 +77,9 @@ const ConcurrentStreamCardHeardWrapper: FC state.tasks[token]?.renderNum) + const coordinatorId = useStore(store, (state) => state.taskStatus.coordinatorId) const raw = useCreation(() => { if (!rawData) return undefined @@ -99,7 +99,7 @@ const ConcurrentStreamCardHeardWrapper: FC = React.memo((p const { onSend } = useAIAgentDispatcher() const sessionId = useCurrentSessionId() - const metaData = useCurrentMeta() const store = useCurrentStore() const execute = useStore(store, (state) => state.execute) const taskStatusLoading = useStore(store, (state) => state.taskStatus.loading) @@ -278,7 +277,7 @@ export const AIReActChatReview: React.FC = React.memo((p }) const submitDetachedPlan = useMemoizedFn(() => { - const taskId = metaData.currentTaskPlanID + const taskId = store.getState().taskStatus.taskID if (taskStatusLoading && taskId) { pendingDetachedPlanSubmitRef.current = true store.getState().updateState({ diff --git a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx index 7d4df676b2..32d54b01f6 100644 --- a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx @@ -287,7 +287,7 @@ export const defaultChatIPCData: UseChatIPCState = { }, reActTimelines: [], memoryList: { ...DefaultMemoryList }, - taskStatus: { loading: false, plan: '', task: '' }, + taskStatus: { loading: false, plan: '', task: '', taskID: '', status: AITaskStatus.created, coordinatorId: '' }, focusMode: '', switchLoading: false, planHistoryList: cloneDeep(DefaultPlanHistoryList), diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 9f74d33cc1..5c41e6869f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -1,4 +1,4 @@ -import type { TaskChatTaskInfo, UseChatIPCState } from '@/pages/ai-re-act/hooks/type' +import type { UseChatIPCState } from '@/pages/ai-re-act/hooks/type' import type { AIAgentGrpcApi, AIInputEvent, AIStartParams } from '@/pages/ai-re-act/hooks/grpcApi' import type { PlanItemDetailsData, SessionRenderContent } from '@/pages/ai-re-act/hooks/aiRender' import type { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' @@ -236,8 +236,6 @@ export interface AIAgentChatMetaData { /** 通知消息的消失定时器 */ notifyMessageTimer: NodeJS.Timeout | null - /** 当前任务规划问题的re_act_task_id|coordinatorId|status */ - currentTaskPlanID?: TaskChatTaskInfo /** 当前任务规划正在进行中的节点taskID */ currentTaskPlanActiveNode: Set diff --git a/app/renderer/src/main/src/pages/ai-agent/utils/index.ts b/app/renderer/src/main/src/pages/ai-agent/utils/index.ts index 664a17fa05..8d99539268 100644 --- a/app/renderer/src/main/src/pages/ai-agent/utils/index.ts +++ b/app/renderer/src/main/src/pages/ai-agent/utils/index.ts @@ -284,6 +284,7 @@ export const onReStart = (props: ReStartParams) => { ...AIAgentSettingDefault, ...activeChat.StartParams, PreferSessionCachedConfig: true, + UserQuery: '', } // 发送初始化参数 const aiInputEvent: AIInputEvent = { diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx index eaacd00ffa..afa9b4635e 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx @@ -153,15 +153,15 @@ export const AIReActTaskChatContent: React.FC = Rea const { onExtraAction } = useTaskChatExtraAction() const store = useCurrentStore() - const meta = useCurrentMeta() const streams = useStore(store, (state) => state.taskChat.elements) const execute = useStore(store, (state) => state.execute) - const taskStatus = useStore(store, (state) => state.taskStatus) + const taskId = useStore(store, (state) => state.taskStatus.taskID) const currentPlanReviewToken = useStore(store, (state) => state.currentPlanReviewToken) + return ( <>
- +
{!currentPlanReviewToken.token && streams.length > 0 && (
@@ -186,7 +186,7 @@ export const AIReActTaskChatContent: React.FC = Rea {t('AIReActTaskChatContent.globalDirective')} - {execute && !!meta.currentTaskPlanID && } + {execute && !!taskId && } } @@ -359,9 +359,8 @@ const AIManualAddition: React.FC = React.memo((props) => // 加入上下文后,停止任务再恢复任务 syncIdOfAddAndReExecute.current = randomString(8) onAddToContext(syncIdOfAddAndReExecute.current) - const info = meta.currentTaskPlanID - const taskId = info?.taskID - const coordinatorId = info?.coordinatorId + const taskId = taskStatus.taskID + const coordinatorId = taskStatus.coordinatorId if (!coordinatorId) return currentCoordinatorIdRef.current = coordinatorId @@ -624,14 +623,11 @@ export const AIRenderTaskFooterExtra: React.FC = R const { t } = useI18nNamespaces(['aiAgent']) const store = useCurrentStore() - const meta = useCurrentMeta() const taskStatus = useStore(store, (state) => state.taskStatus) const cancelTaskLoading = useStore(store, (state) => state.cancelTaskLoading) - const status = useCreation(() => { - return meta.currentTaskPlanID?.status - }, [taskStatus.loading]) + const status = useStore(store, (state) => state.taskStatus.status) const renderBtn = useMemoizedFn(() => { switch (status) { case AITaskStatus.inProgress: diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts index ab773b264f..c64225987c 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts @@ -1,4 +1,4 @@ -import { type AIInputEvent, AIInputEventSyncTypeEnum } from '../hooks/grpcApi' +import { type AIInputEvent, AIInputEventSyncTypeEnum, AITaskStatus } from '../hooks/grpcApi' import useCurrentSessionId from '../hooks/useCurrentSessionId' import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' @@ -38,7 +38,7 @@ export const useTaskChatExtraAction = () => { }) const sendReactCancelTask = useMemoizedFn(() => { - const taskId = meta.currentTaskPlanID?.taskID + const taskId = store.getState().taskStatus.taskID if (!taskId) return store.getState().updateState({ cancelTaskLoading: true, @@ -89,8 +89,7 @@ export const useTaskChatExtraAction = () => { /** @description 在任务规划的content footer下,继续按钮的出现在UI上意味着该任务肯定已经停止 */ const onRecover = useMemoizedFn(() => { - const info = meta.currentTaskPlanID - const coordinatorId = info?.coordinatorId + const coordinatorId = store.getState().taskStatus.coordinatorId if (!coordinatorId) return const params: AIInputEvent = { @@ -100,7 +99,7 @@ export const useTaskChatExtraAction = () => { SyncID: randomString(8), } onSend({ token: sessionId, type: 'task', params }) - meta.currentTaskPlanID = undefined + store.getState().updateTaskLoadingStatus({ taskID: '', status: AITaskStatus.created, coordinatorId: '' }) closeChatReview() }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 3c0a537b8b..e197892494 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -136,7 +136,6 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { casualMemoryList: cloneDeep(DefaultMemoryList), taskMemoryList: cloneDeep(DefaultMemoryList), notifyMessageTimer: null, - currentTaskPlanID: undefined, currentTaskPlanActiveNode: new Set(), historyReviewReleaseID: {}, currentPlanReviewExtraId: '', @@ -783,7 +782,7 @@ export class ChatMultiSessionController { if (params.IsFreeInput) { const { casualLoading, currentCasualTaskID } = store.getState() // 如果自由对话引起了任务规划,那么自由对话其实是空闲状态 - const isCasualIdle = casualLoading && currentCasualTaskID === meta.currentTaskPlanID?.taskID + const isCasualIdle = casualLoading && currentCasualTaskID === store.getState().taskStatus.taskID if (!casualLoading || isCasualIdle) { // 自由对话没有问题进行中时,才改变loading的title @@ -1109,7 +1108,7 @@ export class ChatMultiSessionController { handleFunc({ sessionId, res, - chatType: meta.currentTaskPlanID?.coordinatorId === res.CoordinatorId ? 'task' : 'reAct', + chatType: store.getState().taskStatus.coordinatorId === res.CoordinatorId ? 'task' : 'reAct', store, rawData, request, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts index 0c9ac66879..3d989206f2 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts @@ -1,4 +1,5 @@ import type { AIToolResult, CurrentExecTaskTree, PlanItemDetailsData, TodoListCardData } from './aiRender' +import { AITaskStatus } from './grpcApi' import type { AIOutputI18n, AIAgentGrpcApi } from './grpcApi' import type { AIQuestionQueues, PlanLoadingStatus } from './type' @@ -134,6 +135,9 @@ export const DefaultPlanLoadingStatus: PlanLoadingStatus = { loading: false, plan: '加载中...', task: '加载中...', + taskID: '', + status: AITaskStatus.created, + coordinatorId: '', } export const DefaultPlanHistoryList: AIAgentGrpcApi.PlanHistoryList = { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts index 34ff6034d7..0040d2732f 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts @@ -878,7 +878,7 @@ export declare namespace AIAgentGrpcApi { /** 问题状态变化消息 */ export interface ReactTaskChanged { react_task_id: string - react_task_now_status: string + react_task_now_status: AITaskStatusType react_task_old_status: string } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index ec7ead6129..b188562719 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -63,24 +63,29 @@ const handleStartPlanAndExecution: AIMessageHandler = (requestInfo) => { requestInfo.pushLog({ level: 'error', message: `${res.Type}数据, coordinator_id 为空` }) return } - meta.currentTaskPlanID = { + + // 开始任务规划后,刷新历史任务树 + sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS }) + /** 获取最新任务树状态 */ + sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) + + const taskStatus = { taskID: startInfo['re-act_task'], status: AITaskStatus.inProgress, // 取消任务规划需要的数据id coordinatorId: startInfo.coordinator_id, + loading: true, + plan: '加载中...', + task: '加载中...', } - // 开始任务规划后,刷新历史任务树 - sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS }) - /** 获取最新任务树状态 */ - sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) store.getState().updateState({ - taskStatus: { loading: true, plan: '加载中...', task: '加载中...' }, + taskStatus, showPlanList: true, cancelTaskLoading: false, }) // 生成任务规划里的默认任务聚合组 - const taskID = `${meta.currentTaskPlanID.taskID}-default` + const taskID = `${taskStatus.taskID}-default` const chatData: AIChatQSData = { ...genBaseAIChatData(res), id: taskID, @@ -105,7 +110,7 @@ const handleEndPlanAndExecution: AIMessageHandler = (requestInfo) => { requestInfo.pushLog({ level: 'error', message: `${res.Type}数据, coordinator_id 为空` }) return } - if (startInfo.coordinator_id === meta.currentTaskPlanID?.coordinatorId) { + if (startInfo.coordinator_id === store.getState().taskStatus.coordinatorId) { const chatData: AIChatQSData = { ...genBaseAIChatData(res), chatType: 'task', @@ -345,9 +350,9 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { if (store.getState().currentCasualTaskID && store.getState().currentCasualTaskID === react_task_id) { store.getState().updateState({ focusMode: '', cancelCasualLoading: false, casualLoading: false }) } - if (meta.currentTaskPlanID?.taskID === react_task_id) { + if (store.getState().taskStatus.taskID === react_task_id) { // 该问题触发了任务规划, 所以需要将任务规划状态也调整 - meta.currentTaskPlanID.status = info.react_task_now_status as AITaskStatus + store.getState().updateTaskLoadingStatus({ status: info.react_task_now_status }) store.getState().updateState({ cancelTaskLoading: false }) } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index d242125699..2e4023267a 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -20,13 +20,14 @@ const handlePlanReviewRequire: AIMessageHandler = (requestInfo) => { return } + const taskID = store.getState().taskStatus.taskID const chatData: AIChatQSData = { ...genBaseAIChatData(res), chatType: chatType, id: data.id, type: AIChatQSDataTypeEnum.PLAN_REVIEW_REQUIRE, data: { ...cloneDeep(data) }, - TaskId: meta.currentTaskPlanID?.taskID ? `${meta.currentTaskPlanID.taskID}-default` : undefined, + TaskId: taskID ? `${taskID}-default` : undefined, } if (res.IsSync) { // 历史review数据,直接存入map里,等待review_release出现后渲染到UI上 @@ -172,7 +173,7 @@ const handleToolReview: AIMessageHandler = (requestInfo) => { data: { ...cloneDeep(data) }, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -231,7 +232,7 @@ const handleUserInteractive: AIMessageHandler = (requestInfo) => { data: cloneDeep(data), TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -303,7 +304,7 @@ const handleAIForgeReviewRequire: AIMessageHandler = (requestInfo) => { data: { ...cloneDeep(data) }, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -597,7 +598,7 @@ const handleDetachedPlanReview: AIMessageHandler = (requestInfo) => { data: { ...cloneDeep(data) }, TaskId: generateTaskNodeDataID({ chatType: chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.coordinatorId, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index 0fa50d8162..23e71a6e07 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -20,7 +20,7 @@ const handleThought: AIMessageHandler = (requestInfo) => { data: thought || '', TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -54,7 +54,7 @@ const handleResult: AIMessageHandler = (requestInfo) => { data: result || '', TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -89,7 +89,7 @@ const handleFailReactTask: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -128,7 +128,7 @@ const handleToolCallDecision: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -163,7 +163,7 @@ const handleFailPlanAndExecution: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -197,7 +197,7 @@ const handleApiRequestFailed: AIMessageHandler = (requestInfo) => { data, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -257,7 +257,7 @@ const handleHttpFlowFuzzStatus: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -303,7 +303,7 @@ const handleReportFinish: AIMessageHandler = (requestInfo) => { data: nextData, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -340,8 +340,9 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { }) store.getState().updatePlanTree(newPlanTree) - if (!meta.currentTaskPlanID?.taskID || !info.task.task_id) return - const taskID = `${meta.currentTaskPlanID.taskID}-${info.task.task_id}` + const taskId = store.getState().taskStatus.taskID + if (!taskId || !info.task.task_id) return + const taskID = `${taskId}-${info.task.task_id}` const chatDetail = rawData.contents.get(taskID) if (chatDetail) { requestInfo.pushLog({ level: 'error', message: `${info.task.index}-push_task数据已存在` }) @@ -391,8 +392,9 @@ const handlePopTask: AIMessageHandler = (requestInfo) => { }) store.getState().updatePlanTree(newPlanTree) - if (!meta.currentTaskPlanID?.taskID || !info.task.task_id) return - const taskID = `${meta.currentTaskPlanID.taskID}-${info.task.task_id}` + const taskId = store.getState().taskStatus.taskID + if (!taskId || !info.task.task_id) return + const taskID = `${taskId}-${info.task.task_id}` const chatDetail = rawData.contents.get(taskID) if (!chatDetail || chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { requestInfo.pushLog({ level: 'error', message: `${info.task.index}-pop_task数据不存在` }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index 23f01eb1ac..e92e1b870b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -40,7 +40,7 @@ const genStreamGroupData = ( }, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -126,7 +126,7 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), @@ -164,7 +164,7 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { }, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts index 428e781f51..55cfcc5eba 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -36,7 +36,7 @@ const handleToolCallStart: AIMessageHandler = (requestInfo) => { data: toolResult, TaskId: generateTaskNodeDataID({ chatType, - planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : meta.currentTaskPlanID?.taskID, + planID: chatType === 'reAct' ? store.getState().currentCasualTaskID : store.getState().taskStatus.taskID, taskID: res.TaskId, isExist: (key) => rawData.contents.has(key), }), diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts index 8a6a162267..aaa592389c 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts @@ -114,6 +114,12 @@ export interface PlanLoadingStatus { loading: boolean plan: string task: string + /** 当前任务规划的 re_act_task_id,'' 表示无活动任务规划 */ + taskID: string + /** 当前任务规划状态,'' (AITaskStatus.created) 表示无 */ + status: AITaskStatusType + /** 当前任务规划的 coordinatorId,'' 表示无 */ + coordinatorId: string } export interface UseChatIPCState { From 814d01ad95ea30b930b53891d3b833cb36b1edec Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Mon, 27 Jul 2026 14:27:55 +0800 Subject: [PATCH 053/101] =?UTF-8?q?feat:=E5=8F=82=E8=80=83=E8=B5=84?= =?UTF-8?q?=E6=96=99=E8=AF=A5=E4=B8=BA=E4=BB=8E=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E4=B8=AD=E8=AF=BB=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../historyTaskTree/HistoryTaskTree.tsx | 4 +- .../ai-agent/components/ToolInvokerCard.tsx | 9 +- .../StreamingChatContent.tsx | 9 +- .../AIGroupStreamNodeWrapper.tsx | 5 +- .../aiStreamCardWrapper.tsx | 5 +- .../AIChildWindowGroupStreamNode.tsx | 4 +- .../AIChildWindowStreamCard.tsx | 4 +- .../aiGroupStreamCard/AIGroupStreamCard.tsx | 78 ++--------- .../AIStreamChatContent.module.scss | 2 +- .../ai-agent/components/aiYaklangCode/type.ts | 1 - .../AIReActChatContents.module.scss | 12 -- .../AIReActChatContents.tsx | 128 +++++++++++------- .../AIReActChatContentsType.ts | 9 +- 13 files changed, 117 insertions(+), 153 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx index 0ffc24eb77..f503be08b9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx @@ -31,9 +31,9 @@ export const HistoryTaskTree: React.FC = memo((props) => { const planHistoryList = useStore(store, (state) => state.planHistoryList ?? []) const taskTree = useStore(store, (state) => state.taskChat.plan.task_tree ?? []) const taskName = useStore(store, (state) => state.taskChat.plan.root_task_name ?? '') + const coordinatorId = useStore(store, (state) => state.taskStatus.coordinatorId ?? '') const currentTaskItem = useCreation(() => { - const coordinatorId = store.getState().taskStatus.coordinatorId || '' const item: AIAgentGrpcApi.PlanHistory = { coordinator_id: coordinatorId, created_at: '', @@ -57,7 +57,7 @@ export const HistoryTaskTree: React.FC = memo((props) => { root_task_name: taskName, } return item - }, [taskTree, taskName]) + }, [coordinatorId, taskTree, taskName]) const currentCoordinatorId = useCreation(() => { return currentTaskItem?.coordinator_id || '' diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx index f0c7781300..81450a1a3b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx @@ -179,9 +179,6 @@ const ToolStdoutCard: React.FC = memo((props) => { } onSend({ token: sessionId, type: '', params: info }) }) - const referenceNode = useCreation(() => { - return !!stream?.reference ? : <> - }, [stream?.reference]) return ( = memo((props) => { - {referenceNode} + {!!fileList?.length && } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx index 4650ac8ba7..0bc539d8cc 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/StreamingChatContent/StreamingChatContent.tsx @@ -1,6 +1,6 @@ import { AIStreamNode } from '@/pages/ai-re-act/aiReActChatContents/AIReActChatContents' import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' -import { type FC, useMemo } from 'react' +import { type FC, memo, useMemo } from 'react' import { useTypedStream } from './hooks/useTypedStream' import { useCreation, useMemoizedFn } from 'ahooks' import styles from './StreamingChatContent.module.scss' @@ -8,9 +8,10 @@ import styles from './StreamingChatContent.module.scss' type SingleStreamProps = { itemData: ChatStream renderNum: number + sessionId: string } -export const AIStreamCard: FC = ({ itemData, renderNum }) => { +export const AIStreamCard: FC = memo(({ itemData, renderNum, sessionId }) => { const getContent = useMemoizedFn(() => { return itemData.data.content }) @@ -49,5 +50,5 @@ export const AIStreamCard: FC = ({ itemData, renderNum }) => } }, [itemData.chatType]) if (!stream) return null - return -} + return +}) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/AIGroupStreamNodeWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/AIGroupStreamNodeWrapper.tsx index bb52b9be0f..49007de795 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/AIGroupStreamNodeWrapper.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiGroupStreamNodeWrapper/AIGroupStreamNodeWrapper.tsx @@ -4,12 +4,13 @@ import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' import { useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useCreation } from 'ahooks' import { AIGroupStreamNode } from '../../aiGroupStreamCard/AIGroupStreamCard' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' const AIGroupStreamNodeWrapper: FC = memo((props) => { const { itemData, renderNum, groupIndex } = props // 其余原始字段通过 useCurrentRawData 获取,并订阅 renderNum 驱动重渲染 const rawData = useCurrentRawData() - + const currentSessionId = useCurrentSessionId() const stream = useCreation(() => { const rawStream = rawData.contents.get(itemData.id) if (!rawStream) return null @@ -28,7 +29,7 @@ const AIGroupStreamNodeWrapper: FC = memo((props) return `${groupIndex + 1}. ` }, [renderNum, groupIndex]) if (!stream) return null - return + return }) export default AIGroupStreamNodeWrapper diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiStreamCardWrapper/aiStreamCardWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiStreamCardWrapper/aiStreamCardWrapper.tsx index 675f51d82e..d9d2d8e2a1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiStreamCardWrapper/aiStreamCardWrapper.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiStreamCardWrapper/aiStreamCardWrapper.tsx @@ -6,6 +6,7 @@ import { AIStreamCard } from '../../aiChatListItem/StreamingChatContent/Streamin import { AIChatQSDataTypeEnum, ChatStream } from '@/pages/ai-re-act/hooks/aiRender' import { useStore } from 'zustand' import useMemoizedFn from 'ahooks/lib/useMemoizedFn' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' const AIStreamCardWrapper: FC = memo((props) => { const { token } = props @@ -15,7 +16,7 @@ const AIStreamCardWrapper: FC = memo((props) => { const store = useCurrentStore() const renderNum = useStore(store, (state) => state.items[token]?.renderNum) const rawData = useCurrentRawData() - + const sessionId = useCurrentSessionId() useRafInterval(() => { syncData() }, interval) @@ -38,7 +39,7 @@ const AIStreamCardWrapper: FC = memo((props) => { }) if (!itemData) return null - return + return }) export default AIStreamCardWrapper diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/AIChildWindowGroupStreamNode.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/AIChildWindowGroupStreamNode.tsx index e960ece1a1..2cac9a3e61 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/AIChildWindowGroupStreamNode.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamNode/AIChildWindowGroupStreamNode.tsx @@ -7,7 +7,7 @@ import { AIGroupStreamNode } from '../../aiGroupStreamCard/AIGroupStreamCard' const AIChildWindowGroupStreamNode: FC = memo((props) => { const { itemData, renderNum, groupIndex } = props - const { rawData } = useAIConcurrentStreamStore() + const { rawData, session } = useAIConcurrentStreamStore() // 其余原始字段通过 useCurrentRawData 获取,并订阅 renderNum 驱动重渲染 const stream = useCreation(() => { const rawStream = rawData?.get(itemData.id) @@ -27,7 +27,7 @@ const AIChildWindowGroupStreamNode: FC = memo return `${groupIndex + 1}. ` }, [renderNum, groupIndex]) if (!stream) return null - return + return }) export default AIChildWindowGroupStreamNode diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/AIChildWindowStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/AIChildWindowStreamCard.tsx index 8b61644d6e..3041d782b1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/AIChildWindowStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowStreamCard/AIChildWindowStreamCard.tsx @@ -3,9 +3,11 @@ import type { AIChildWindowStreamCardProps } from './type' import { AIStreamCard } from '../../aiChatListItem/StreamingChatContent/StreamingChatContent' import useCreation from 'ahooks/lib/useCreation' import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' +import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' const AIChildWindowStreamCard: FC = memo((props) => { const { itemData, renderNum } = props + const { session } = useAIConcurrentStreamStore() const data: ChatStream = useCreation(() => { return { ...itemData, @@ -15,7 +17,7 @@ const AIChildWindowStreamCard: FC = memo((props) = }, } }, [renderNum]) - return + return }) export default AIChildWindowStreamCard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index 12bc29723c..41de70bc7c 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -1,12 +1,7 @@ import { AIChatQSDataTypeEnum, ChatReferenceMaterialPayload, ChatStream } from '@/pages/ai-re-act/hooks/aiRender' import { type CSSProperties, useState, type FC, useRef, useEffect, useMemo, memo } from 'react' import styles from './AIGroupStreamCard.module.scss' -import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' -import { OutlineArrowsexpandIcon } from '@/assets/icon/outline' -import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' -import { YakitModal } from '@/components/yakitUI/YakitModal/YakitModal' import classNames from 'classnames' -import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import useClickFocus from '../../../ai-re-act/hooks/useClickFocus' import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import useCreation from 'ahooks/lib/useCreation' @@ -16,6 +11,7 @@ import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' import AIGroupStreamCardHeard from './aiGroupStreamCardHeard/AIGroupStreamCardHeard' import AIGroupStreamCardList from './aiGroupStreamCardList/AIGroupStreamCardList' import { useTypedStream } from '../aiChatListItem/StreamingChatContent/hooks/useTypedStream' +import { AIReferenceNode } from '@/pages/ai-re-act/aiReActChatContents/AIReActChatContents' export const Code: FC<{ code: ChatReferenceMaterialPayload; style: CSSProperties }> = ({ code, style }) => { return ( @@ -31,24 +27,14 @@ export const AIGroupStreamNode: FC<{ itemData: ChatStream renderNum: number seqNo: string -}> = memo(({ itemData, renderNum, seqNo }) => { - const { t } = useI18nNamespaces(['aiAgent']) - - const [open, setOpen] = useState(false) - const [openPopover, setOpenPopover] = useState(false) - // popover 关闭过渡期间保留上一次的 reference 内容,避免关闭动画途中内容先清空 - const prevPopoverCodeRef = useRef([]) - + sessionId: string +}> = memo(({ itemData, renderNum, seqNo, sessionId }) => { // 仅获取用于显示的 content(已应用打字效果) const { content } = useTypedStream({ getContent: () => itemData.data.content, getStatus: () => itemData.data.status, }) - const onClose = () => { - setOpen(false) - } - const { getLabelByParams } = useAINodeLabel() const nodeLabel = useCreation(() => { @@ -62,65 +48,21 @@ export const AIGroupStreamNode: FC<{ } }, []) - const modelCode = useCreation(() => { - // TODO - reference 现为 token[];按 token 查 IDB 展示完整交互后置,弹框内容暂空 - if (!open) return [] - return [] as ChatReferenceMaterialPayload - }, [open]) - const popoverCode = useCreation(() => { - if (!openPopover) return prevPopoverCodeRef.current - const code = itemData?.reference || [] - prevPopoverCodeRef.current = code - return code - }, [openPopover, renderNum]) - const hidden = useCreation(() => { return !itemData?.reference?.length }, [renderNum, itemData?.reference?.length]) return (
- {open && ( - - - - )} {seqNo} {content} - -
- {seqNo} - {nodeLabel} - { - setOpenPopover(false) - setOpen(true) - }} - type="text2" - icon={} - size="small" - /> -
- {!!popoverCode.length && } -
- } - destroyTooltipOnHide={true} - > - - + {!hidden && ( + + )}
) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiStreamChatContent/AIStreamChatContent.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiStreamChatContent/AIStreamChatContent.module.scss index f875886e5d..3e8b49894b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiStreamChatContent/AIStreamChatContent.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiStreamChatContent/AIStreamChatContent.module.scss @@ -25,7 +25,6 @@ // @include mixin.mpb-color-text(); } .ai-stream-content { - padding: 4px 12px 4px 0; border-radius: 0 8px 8px 0; color: var(--Colors-Use-Neutral-Text-4-Help-text); font-size: 11px; @@ -34,6 +33,7 @@ white-space: nowrap; overflow: hidden; display: flex; + align-items: center; justify-content: flex-end; position: relative; .ai-mask { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/type.ts index 42e185dfe9..4a8f36b213 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiYaklangCode/type.ts @@ -5,7 +5,6 @@ import { ReactNode } from 'react' export interface AIYaklangCodeProps { content: string autoApplyStreamId?: string - autoApplyChatSessionId?: string listItemIndex?: number nodeLabel: string modalInfo: ModalInfoProps diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.module.scss b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.module.scss index f3d483f1ca..74c5139fef 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.module.scss +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.module.scss @@ -63,15 +63,3 @@ cursor: pointer; z-index: 1; } -// .ai-text-markdown-reference-node { -// } -// .ai-yaklang-reference-node { -// padding-top: 8px; -// } -// .ai-text-plain-reference-node { -// } -// .ai-log-tool-reference-node { -// } -// .ai-stream-chat-reference-node { -// padding: 0 12px 8px 12px; -// } diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index 6e95a2c0a9..c8e15508c6 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -1,5 +1,4 @@ -import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react' -import classNames from 'classnames' +import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef, useState, useEffect } from 'react' import { AIReActChatContentsPProps, AIReferenceNodeProps, AIStreamNodeProps } from './AIReActChatContentsType' import styles from './AIReActChatContents.module.scss' import { AIMarkdown } from '@/pages/ai-agent/components/aiMarkdown/AIMarkdown' @@ -14,35 +13,24 @@ import { AIStreamContentType } from '../hooks/defaultConstant' import { Virtuoso } from 'react-virtuoso' import useVirtuosoAutoScroll from '../hooks/useVirtuosoAutoScroll' import useChatStreamLocateHighlight from '../hooks/useChatStreamLocateHighlight' -import { ReActChatRenderElement } from '../hooks/aiRender' +import { ReActChatRenderElement, ChatReferenceMaterialPayload } from '../hooks/aiRender' import Loading from '@/components/Loading/Loading' import { ScrollText } from '@/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading' -import { showYakitModal } from '@/components/yakitUI/YakitModal/YakitModalConfirm' -import { YakitEditor } from '@/components/yakitUI/YakitEditor/YakitEditor' +import { YakitModal } from '@/components/yakitUI/YakitModal/YakitModal' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' import AITextSyntaxFlow from '@/pages/ai-agent/components/aiTextSyntaxFlow/AITextSyntaxFlow' import { useCurrentStore } from '../hooks/useCurrentDataBySession' import { useStore } from 'zustand' import useCreation from 'ahooks/lib/useCreation' -import useDebounceFn from 'ahooks/lib/useDebounceFn' -const getAIReferenceNodeByType = (contentType?: string) => { - switch (contentType) { - case AIStreamContentType.TEXT_MARKDOWN: - return styles['ai-text-markdown-reference-node'] - case AIStreamContentType.CODE_YAKLANG: - case AIStreamContentType.CODE_HTTP_REQUEST: - return styles['ai-yaklang-reference-node'] - case AIStreamContentType.TEXT_PLAIN: - return styles['ai-text-plain-reference-node'] - case AIStreamContentType.LOG_TOOL: - return styles['ai-log-tool-reference-node'] - default: - return styles['ai-stream-chat-reference-node'] - } -} +import useMemoizedFn from 'ahooks/lib/useMemoizedFn' +import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' +import { globalSessionEngine } from '../hooks/ChatMultiSessionController' +import { Code } from '@/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard' + export const AIStreamNode: React.FC = React.memo((props) => { - const { stream, aiMarkdownProps, listItemIndex, streamChatSessionId } = props + const { stream, aiMarkdownProps, listItemIndex, sessionId } = props const { reference } = stream const { NodeId, content, NodeIdVerbose, CallToolID, ContentType, status } = stream.data // 是否仍在流式输出(结束态 status 为 'end',历史消息亦为 'end',据此控制流式淡入效果) @@ -59,16 +47,14 @@ export const AIStreamNode: React.FC = React.memo((props) => { } }, [stream.Timestamp, stream.AIModelName, stream.AIService]) const referenceNode = useCreation(() => { - const className = getAIReferenceNodeByType(ContentType) - return !!reference ? : <> - }, [reference, ContentType]) + return !!reference ? : <> + }, [reference, sessionId]) if (ContentType?.startsWith('code/')) { return ( = React.me }), ) -/** 挂到 body,避免 Virtuoso 滚出视口时卸载列表项导致弹窗消失。 - * TODO -本阶段 referenceList 为 token 列表;按 token 查 IDB 展示完整交互后置。 - */ -export const openAIReferenceModal = (_referenceList: string[], title = '参考资料') => { - const modal = showYakitModal({ - title, - cancelButtonProps: { style: { display: 'none' } }, - bodyStyle: { height: 500 }, - content: , - onOk: () => modal.destroy(), +export const AIReferenceNode: React.FC = React.memo((props) => { + const { referenceList, sessionId, title = '' } = props + const { t } = useI18nNamespaces(['aiAgent']) + + const [open, setOpen] = useState(false) + const [modelCode, setModelCode] = useState([]) + const [modelLoading, setModelLoading] = useState(false) + + const hidden = useCreation(() => { + return !referenceList?.length + }, [referenceList?.length]) + + const onClose = useMemoizedFn(() => { + setOpen(false) }) -} -export const AIReferenceNode: React.FC = React.memo((props) => { - const { referenceList, className } = props - const onClick = useDebounceFn( - (e) => { - e.stopPropagation() - openAIReferenceModal(referenceList) - }, - { wait: 500, leading: true }, - ).run - return ( - - [参考资料] - - ) + /** 按 token 列表异步获取参考资料完整数据 */ + const fetchReference = useMemoizedFn(async (): Promise => { + if (!referenceList.length || !sessionId) return [] + try { + const items = await globalSessionEngine.getSessionReferenceMaterials(sessionId, referenceList) + return items.map((item) => item.content) + } catch { + return [] + } + }) + + // modal 打开时拉取数据 + useEffect(() => { + if (!open) return + let cancelled = false + setModelLoading(true) + fetchReference() + .then((code) => { + if (!cancelled) setModelCode(code) + }) + .finally(() => { + if (!cancelled) setModelLoading(false) + }) + return () => { + cancelled = true + } + }, [open]) + + const openModel = useMemoizedFn(() => { + setOpen(true) + }) + return !hidden ? ( + <> + {open && ( + + + + + + )} + + {t('AIStreamNode.viewReference')} + + + ) : null }) diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts index 89984056b1..984525b73e 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContentsType.ts @@ -14,12 +14,15 @@ export interface AIStreamNodeProps { aiMarkdownProps?: { className: string } /** 当前流所在列表项在 `chats.elements` 中的下标(用于 WebFuzzer 自动改包防覆盖) */ listItemIndex?: number - /** 当前 ReAct 的 SessionID */ - streamChatSessionId?: string + /** 当前 SessionID */ + sessionId: string } export interface AIReferenceNodeProps { /** 参考资料 token 列表(payload 存 IDB,完整弹框交互后置) */ referenceList: string[] - className?: string + /** 当前会话 sessionId,必传 */ + sessionId: string + /** 弹窗标题前缀(如序号+节点标签),默认空 */ + title?: string } From 83c7fa77411238587766aebdb18f02ddeb99523a Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Mon, 27 Jul 2026 15:36:19 +0800 Subject: [PATCH 054/101] =?UTF-8?q?fix:=E8=A7=A3=E5=86=B3=E5=86=B2?= =?UTF-8?q?=E7=AA=81=E5=90=8E=E7=9A=84=E4=B8=80=E4=BA=9B=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AIConcurrentStream/AIConcurrentStream.tsx | 3 +- .../buildConcurrentStreamFramePayload.ts | 2 +- .../ai-agent/components/ToolInvokerCard.tsx | 6 +- .../AIReActChatReviewType.d.ts | 4 +- .../AIReActChatContents.tsx | 2 +- .../aiReActTaskChat/AIReActTaskEmpty.tsx | 145 ------------------ .../hooks/grpcStreamHandler/aiReview.ts | 6 +- .../hooks/grpcStreamHandler/aiSingleItem.ts | 4 +- 8 files changed, 10 insertions(+), 162 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskEmpty.tsx diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx index 26b35eb301..39ed610e9a 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx @@ -60,7 +60,6 @@ const AIConcurrentStream: React.FC = memo(({ windowId } // 收到 frame 后,主动向主窗口拉取本次需要渲染的 rawData。 getRawData(newFrame) - setContentVersion((v) => v + 1) }) } @@ -88,9 +87,9 @@ const AIConcurrentStream: React.FC = memo(({ windowId } .then((entries) => { rawDataRef.current = entries.rawData execFileRecordRef.current = entries.execFileRecord - setContentVersion((v) => v + 1) }) .finally(() => { + setContentVersion((v) => v + 1) setLoadingContents(false) }) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts index f71a2d7914..bc34f07079 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts @@ -29,7 +29,7 @@ export interface BuildConcurrentStreamFramePayloadParams { * - false:仅返回元数据(rawData 为空 Map),用于打开子窗口时只发轻量 payload, * 子窗 mount 后再通过 fetch-concurrent-stream-contents 拉取,避免开窗瞬间克隆大 Map。 */ - withRawData?: boolean + withRawData: boolean } /** 判断 childToken 在 store 中的类型 */ diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx index 81450a1a3b..9e9f21d4c7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ToolInvokerCard.tsx @@ -217,11 +217,7 @@ const ToolStdoutCard: React.FC = memo((props) => { - + {!!fileList?.length && } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts index f141d9acef..9f072fa61f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReviewType.d.ts @@ -1,6 +1,5 @@ import { AIChatQSData } from '../../../ai-re-act/hooks/aiRender' import { AIAgentGrpcApi } from '../../../ai-re-act/hooks/grpcApi' -import { ChatIPCContextDispatcher } from '@/pages/ai-agent/useContext/ChatIPCContent/ChatIPCContent' import { ChatIPCSendType } from '@/pages/ai-re-act/hooks/type' export interface AIReActChatReviewProps { @@ -11,12 +10,11 @@ export interface AIReActChatReviewProps { renderFooterExtra?: (node: React.ReactNode) => React.ReactNode expand: boolean className?: string - onSendSyncMessage?: ChatIPCContextDispatcher['handleSendSyncMessage'] renderNum: number } export interface ForgeReviewFormRefProps { - validateFields: Promise + validateFields: () => Promise> } export interface ForgeReviewFormProps extends AIAgentGrpcApi.ExecForgeReview { ref: React.ForwardedRef diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index c8e15508c6..3206722b29 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -271,7 +271,7 @@ export const AIReferenceNode: React.FC = React.memo((props {open && ( = React.memo((props) => { - const { loadingAIMaterials, randomAIMaterialsData, onRefresh, onClickItem } = props - - const store = useCurrentStore() - const grpcFolders = useStore(store, (state) => state.grpcFolders) - - const taskEmptyRef = useRef(null) - const [inViewPort = true] = useInViewport(taskEmptyRef) - - const [sessionFiles, setSessionFiles] = useState>([]) - - useEffect(() => { - let isActive = true - - const fetchSessionFiles = async () => { - const files = await Promise.all( - grpcFolders.slice(-5).map(async (item) => ({ - ...item, - name: await getNameByPath(item.path), - })), - ) - - if (isActive) { - setSessionFiles(files) - } - } - - fetchSessionFiles() - - return () => { - isActive = false - } - }, [grpcFolders]) - - useUpdateEffect(() => { - if (inViewPort) { - onRefresh() - } - }, [inViewPort]) - - const handleViewFile = useMemoizedFn((item) => { - emiter.emit('switchAIActTab', JSON.stringify({ key: AITabsEnum.File_System })) - setTimeout(() => { - emiter.emit('fileSystemDefaultExpand', item.path) - }, 800) - }) - - return ( - -
-
-
扩展资源
-
专注于安全编码与漏洞分析的智能助手
-
-
- {Object.keys(randomAIMaterialsData).map((key) => { - const aiItem: AIMaterialsData = randomAIMaterialsData[key as keyof typeof randomAIMaterialsData] - return aiItem.data.length > 0 ? ( - onClickItem(item, aiItem.mentionType)} - /> - ) : ( - - ) - })} -
- {sessionFiles.length > 0 && ( -
- -
- )} -
-
- ) -}) - -export default AIReActTaskEmpty - -const AIReActTaskRecommend: React.FC = React.memo((props) => { - const { title, data, onClickItem } = props - const icons = useCreation(() => { - return getAIRecommendIconByType(title) - }, [title]) - const onAdd = useMemoizedFn(() => { - switch (title) { - case '技能': - emiter.emit('menuOpenPage', JSON.stringify({ route: YakitRoute.AddAIForge })) - break - case '知识库': - emiter.emit('menuOpenPage', JSON.stringify({ route: YakitRoute.AI_REPOSITORY })) - break - case '工具': - emiter.emit('menuOpenPage', JSON.stringify({ route: YakitRoute.AddAITool })) - break - - default: - break - } - }) - return ( -
-
-
-
{icons.icon}
-
{icons.hoverIcon}
- {title} -
- } className={styles['add-btn']} type="text" onClick={onAdd}> - 新建 - -
-
- {data.map((item, index) => ( -
onClickItem(item)} - > - {item.name} -
- ))} -
-
- ) -}) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index 2e4023267a..26da1480a0 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -96,7 +96,7 @@ const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { const data = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanReviewRequireExtra if ( !data?.plans_id || - !data?.index || + !data?.task_id || !data?.keywords?.length || (meta.currentPlanReviewExtraId && meta.currentPlanReviewExtraId !== data.plans_id) ) { @@ -114,11 +114,11 @@ const handlePlanTaskAnalysis: AIMessageHandler = (requestInfo) => { if (!meta.currentPlanReviewExtraId) meta.currentPlanReviewExtraId = data.plans_id const reviewInfo = reviewDetail.data if (!reviewInfo.taskExtra) reviewInfo.taskExtra = new Map() - reviewInfo.taskExtra.set(data.index, data) + reviewInfo.taskExtra.set(data.task_id, data) const isAuto = isAutoExecuteReviewContinue({ getFunc: () => request }) if (!isAuto) { - meta.planReviewExtraData.set(data.index, cloneDeep(data)) + meta.planReviewExtraData.set(data.task_id, cloneDeep(data)) store.getState().updateStateCount('currentPlanReviewExtraUpdate') } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index 23e71a6e07..5d93d619d5 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -345,7 +345,7 @@ const handlePushTask: AIMessageHandler = (requestInfo) => { const taskID = `${taskId}-${info.task.task_id}` const chatDetail = rawData.contents.get(taskID) if (chatDetail) { - requestInfo.pushLog({ level: 'error', message: `${info.task.index}-push_task数据已存在` }) + requestInfo.pushLog({ level: 'error', message: `${info.task.task_id}-push_task数据已存在` }) return } const chatData: AIChatQSData = { @@ -397,7 +397,7 @@ const handlePopTask: AIMessageHandler = (requestInfo) => { const taskID = `${taskId}-${info.task.task_id}` const chatDetail = rawData.contents.get(taskID) if (!chatDetail || chatDetail.type !== AIChatQSDataTypeEnum.TASK_NODE_GROUP) { - requestInfo.pushLog({ level: 'error', message: `${info.task.index}-pop_task数据不存在` }) + requestInfo.pushLog({ level: 'error', message: `${info.task.task_id}-pop_task数据不存在` }) return } meta.currentTaskPlanActiveNode.delete(chatDetail.id) From aec60fb609a4d7975a8c14c40a14e5f4c6c48f66 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Mon, 27 Jul 2026 15:51:49 +0800 Subject: [PATCH 055/101] =?UTF-8?q?=E8=BF=81=E7=A7=BB=E6=97=A7=E7=89=88?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=88=B0=E6=96=B0=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aiReActChatReview/AIReActChatReview.tsx | 5 ++--- .../src/main/src/pages/ai-re-act/hooks/aiRender.ts | 1 + .../ai-re-act/hooks/grpcStreamHandler/aiOther.ts | 1 + .../hooks/grpcStreamHandler/aiSingleItem.ts | 2 +- .../hooks/grpcStreamHandler/aiTaskDetail.ts | 2 ++ .../src/main/src/pages/ai-re-act/hooks/utils.ts | 14 +------------- 6 files changed, 8 insertions(+), 17 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx index 78ea1098a9..88fe9ec1f8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx @@ -10,7 +10,7 @@ import cloneDeep from 'lodash/cloneDeep' import isEqual from 'lodash/isEqual' import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' import AIPlanReviewTree from '@/pages/ai-agent/aiPlanReviewTree/AIPlanReviewTree' -import { handleFlatAITree } from '../../../ai-re-act/hooks/utils' +import { genExecTasks } from '../../../ai-re-act/hooks/utils' import { reviewListToTrees } from '@/pages/ai-agent/utils' import { grpcGetAIForge } from '@/pages/ai-agent/grpc' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' @@ -61,8 +61,7 @@ export const AIReActChatReview: React.FC = React.memo((p case 'plan_review_require': case 'detached_plan_require': const data = info.data as AIAgentGrpcApi.PlanReviewRequire - const list: AIAgentGrpcApi.PlanTask[] = [] - handleFlatAITree(list, data.plans.root_task) + const list = genExecTasks(data.plans.root_task) initReviewTreesRef.current = [...list] setReviewTrees(list) setCurrentPlansId(data.plans_id) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 8c77a1b6ec..642de1f245 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -16,6 +16,7 @@ export interface AITaskInfoProps extends AIAgentGrpcApi.PlanTask { level: number /** 是否是叶子任务节点 */ isLeaf: boolean + subtasks?: AITaskInfoProps[] } /** UI:待办清单卡片数据*/ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index b188562719..e0cada40b0 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -210,6 +210,7 @@ const handleReactTaskDequeue: AIMessageHandler = (requestInfo) => { if (!res.IsSync) { sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_QUEUE_INFO }) rawData.casualChat.planDetails = cloneDeep(DefaultPlanItemDetailsData) + store.getState().updateCasualTodoList() store.getState().updateState({ currentCasualTaskID: res.TaskId || data.react_task_id, focusMode: data.focus_mode ? data.focus_mode : '', diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts index 5d93d619d5..ebcb2496d5 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiSingleItem.ts @@ -404,7 +404,7 @@ const handlePopTask: AIMessageHandler = (requestInfo) => { chatDetail.data.status = info.task.task_status store.getState().incrementNodeVersion(chatDetail.id, 'task') persistIndependentItem(requestInfo.sessionId, chatDetail) - + // 更新任务树状态 sendRequest && sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts index 49a8b66df1..0bbd0fca6e 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiTaskDetail.ts @@ -202,6 +202,8 @@ const handleCurrentTaskTodoListUpdate: AIMessageHandler = (requestInfo) => { applyTodoListFields(chatDetail) if (isSubAgentTask) { rawData.casualChat.planDetailsMap.set(res.TaskId, chatDetail) + } else { + rawData.casualChat.planDetails = chatDetail } store.getState().updateCasualTodoList() } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts index f4699f0795..75a63da2c4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts @@ -12,7 +12,7 @@ import { AIChatQSDataTypeEnum, } from './aiRender' import { AITaskStatus, type AIAgentGrpcApi, type AIOutputEvent } from './grpcApi' -import { AIToDoListStatusEnum, generateTaskChatExecution } from '@/pages/ai-agent/defaultConstant' +import { AIToDoListStatusEnum } from '@/pages/ai-agent/defaultConstant' import { v4 as uuidv4 } from 'uuid' import { JSONParseLog } from '@/utils/tool' import { aiAgentLogEmitter } from './AIAgentLogEmitter' @@ -134,18 +134,6 @@ export const genExecTasks = (taskTree: AIAgentGrpcApi.PlanTask) => { } // #endregion -/** 将树结构任务列表转换成一维数组 */ -export const handleFlatAITree = (sum: AITaskInfoProps[], task: AIAgentGrpcApi.PlanTask, level = 1) => { - if (!Array.isArray(sum)) return null - const hasSubtasks = !!(task.subtasks && task.subtasks.length > 0) - sum.push({ ...generateTaskChatExecution(task), level, isLeaf: !hasSubtasks }) - if (hasSubtasks) { - for (let subtask of task.subtasks!) { - handleFlatAITree(sum, subtask, level + 1) - } - } -} - /** 是否自动执行review的continue操作 */ export const isAutoExecuteReviewContinue = (params: { type?: string; getFunc?: () => AIAgentSetting | undefined }) => { try { From ce11942aee3e5b950f178425a8d5ea8cfbab1b25 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Mon, 27 Jul 2026 16:09:20 +0800 Subject: [PATCH 056/101] =?UTF-8?q?fix:=E8=A7=A3=E5=86=B3=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/aiReActChatReview/AIReActChatReview.tsx | 2 +- app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx | 4 ++-- app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx index 88fe9ec1f8..beb783cbdb 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx @@ -49,7 +49,7 @@ export const AIReActChatReview: React.FC = React.memo((p const [reviewTrees, setReviewTrees] = useState([]) const [currentPlansId, setCurrentPlansId] = useState('') const [forgeOption, setForgeOption] = useState() - const forgeReviewFormRef = useRef({ validateFields: () => {} }) + const forgeReviewFormRef = useRef({ validateFields: () => new Promise(() => {}) }) const initReviewTreesRef = useRef([]) /** 取消当前任务后,等待 taskStatus.loading 置为 false 再提交 detached plan */ diff --git a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx index 32d54b01f6..ca69fbd5f6 100644 --- a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx @@ -31,7 +31,7 @@ import { MemfitIcon, } from './aiModelList/icon' import { UseChatIPCState } from '../ai-re-act/hooks/type' -import { AIAgentGrpcApi, AITaskStatus } from '../ai-re-act/hooks/grpcApi' +import { AIAgentGrpcApi, AISourceEnum, AITaskStatus } from '../ai-re-act/hooks/grpcApi' import { SolidCursorclickIcon, SolidHashtagIcon, @@ -121,7 +121,7 @@ export const AIAgentSettingDefault: AIAgentSetting = { EnableGoalMode: false, GoalMinIterations: 0, }, - Source: 'ai', + Source: AISourceEnum.aiAgent, } /** mcp 自定义服务器配置类型选项 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts index 0040d2732f..4c6d7b494b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcApi.ts @@ -505,6 +505,8 @@ export declare namespace AIAgentGrpcApi { export interface PlanTask { /** 任务id */ task_id: string + /** 审阅树扁平化后的层级(仅前端使用) */ + level?: number /** 任务名 */ name: string /** 正文 */ From a78be713f3528113ad5a931cbe30f60c890f7011 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Mon, 27 Jul 2026 17:49:46 +0800 Subject: [PATCH 057/101] =?UTF-8?q?feat:=20=E4=BB=BB=E5=8A=A1=E8=A7=84?= =?UTF-8?q?=E5=88=92=E8=BF=90=E8=A1=8C=E6=80=81=E6=94=B6=E6=95=9B=E4=B8=BA?= =?UTF-8?q?=20TaskPlanStatus=EF=BC=8C=E5=B9=B6=E9=BD=90=E5=A5=97=20settle?= =?UTF-8?q?=20=E7=BB=88=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 去掉 loading 字段改用 status;end 与 react_task_status_changed 双到齐后才落终态。 --- .../chatTemplate/TaskLoading/TaskLoading.tsx | 5 +-- .../historyTaskTree/HistoryTaskTree.tsx | 6 ++-- .../aiReActChatReview/AIReActChatReview.tsx | 14 ++++---- .../src/pages/ai-agent/defaultConstant.tsx | 2 +- .../main/src/pages/ai-agent/type/aiChat.d.ts | 9 +++++ .../aiReActTaskChat/AIReActTaskChat.tsx | 23 +++--------- .../aiReActTaskChat/useTaskChatExtraAction.ts | 3 +- .../hooks/ChatMultiSessionController.ts | 23 +++++++++--- .../src/pages/ai-re-act/hooks/aiRender.ts | 25 ++++++++++--- .../src/pages/ai-re-act/hooks/chatStore.ts | 4 +-- .../pages/ai-re-act/hooks/defaultConstant.ts | 26 ++++++++++---- .../hooks/grpcStreamHandler/aiOther.ts | 23 +++++++----- .../main/src/pages/ai-re-act/hooks/type.ts | 18 ++-------- .../main/src/pages/ai-re-act/hooks/utils.ts | 35 ++++++++++++++++--- 14 files changed, 136 insertions(+), 80 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading.tsx index 0e533d8d3b..0ce80bdf0a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TaskLoading/TaskLoading.tsx @@ -6,6 +6,7 @@ import { useAISystemStreamText } from '@/store/aiSystemStream' import classNames from 'classnames' import { useStore } from 'zustand' import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { AITaskStatus } from '@/pages/ai-re-act/hooks/grpcApi' export const ScrollText: FC<{ text?: string }> = ({ text = '' }) => { const wrapperRef = useRef(null) @@ -47,7 +48,7 @@ const TaskLoading: FC<{ }> = ({ className }) => { const store = useCurrentStore() const task = useStore(store, (state) => state.taskStatus.task) - const loading = useStore(store, (state) => state.taskStatus.loading) + const isRunning = useStore(store, (state) => state.taskStatus.status === AITaskStatus.inProgress) const plan = useStore(store, (state) => state.taskStatus.plan) const systemStream = useAISystemStreamText() const { displayValue, mode } = useAISystemStream({ @@ -56,7 +57,7 @@ const TaskLoading: FC<{ }) return (
- {loading && ( + {isRunning && ( <> = React const sessionId = useCurrentSessionId() const store = useCurrentStore() - const isExecuting = useStore(store, (state) => state.taskStatus.loading) + const isExecuting = useStore(store, (state) => state.taskStatus.status === AITaskStatus.inProgress) const cancelTaskLoading = useStore(store, (state) => state.cancelTaskLoading) const execute = useStore(store, (state) => state.execute) @@ -183,7 +183,7 @@ export const AIHistoryContinueTask: React.FC = React ) } - // 如果系统正处于正在停止/取消任务的全局 Loading 状态,或当前任务本身正处于停止进行中的状态 + // 停止/取消进行中:status 仍为 processing,用 cancelTaskLoading 表示停止中 if (cancelTaskLoading) { return false } @@ -216,7 +216,7 @@ export const AIHistoryContinueTask: React.FC = React store.getState().updateState({ cancelCasualLoading: true, }) - if (taskStatus.loading && currentTaskId) { + if (taskStatus.status === AITaskStatus.inProgress && currentTaskId) { // 选停止当前任务,等待任务停止成功后,再发送恢复的数据 const info: AIInputEvent = { IsSyncMessage: true, diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx index beb783cbdb..38322e8fb3 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiReActChatReview/AIReActChatReview.tsx @@ -18,7 +18,7 @@ import { YakParamProps } from '@/pages/plugins/pluginsType' import { ExecuteEnterNodeByPluginParams } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeard' import { CustomPluginExecuteFormValue } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType' import { getValueByType } from '@/pages/plugins/editDetails/utils' -import { AIAgentGrpcApi, AIInputEventSyncTypeEnum, AIInputEvent } from '../../../ai-re-act/hooks/grpcApi' +import { AIAgentGrpcApi, AIInputEventSyncTypeEnum, AIInputEvent, AITaskStatus } from '../../../ai-re-act/hooks/grpcApi' import classNames from 'classnames' import styles from './AIReActChatReview.module.scss' @@ -43,7 +43,7 @@ export const AIReActChatReview: React.FC = React.memo((p const sessionId = useCurrentSessionId() const store = useCurrentStore() const execute = useStore(store, (state) => state.execute) - const taskStatusLoading = useStore(store, (state) => state.taskStatus.loading) + const taskStatusRunning = useStore(store, (state) => state.taskStatus.status === AITaskStatus.inProgress) const [reviewTreeOption, setReviewTreeOption] = useState() const [reviewTrees, setReviewTrees] = useState([]) @@ -52,7 +52,7 @@ export const AIReActChatReview: React.FC = React.memo((p const forgeReviewFormRef = useRef({ validateFields: () => new Promise(() => {}) }) const initReviewTreesRef = useRef([]) - /** 取消当前任务后,等待 taskStatus.loading 置为 false 再提交 detached plan */ + /** 取消当前任务后,等待 status 离开 processing 再提交 detached plan */ const pendingDetachedPlanSubmitRef = useRef(false) useEffect(() => { @@ -277,14 +277,14 @@ export const AIReActChatReview: React.FC = React.memo((p const submitDetachedPlan = useMemoizedFn(() => { const taskId = store.getState().taskStatus.taskID - if (taskStatusLoading && taskId) { + if (taskStatusRunning && taskId) { pendingDetachedPlanSubmitRef.current = true store.getState().updateState({ cancelTaskLoading: true, }) const params: AIInputEvent = { IsSyncMessage: true, - SyncType: AIInputEventSyncTypeEnum.SYNC_EXECUTE_DETACHED_PLAN, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, SyncID: randomString(8), SyncJsonInput: JSON.stringify({ task_id: taskId }), } @@ -295,10 +295,10 @@ export const AIReActChatReview: React.FC = React.memo((p }) useUpdateEffect(() => { - if (!taskStatusLoading && pendingDetachedPlanSubmitRef.current) { + if (!taskStatusRunning && pendingDetachedPlanSubmitRef.current) { executeDetachedPlan() } - }, [taskStatusLoading]) + }, [taskStatusRunning]) /** 继续执行 */ const handleContinue = useMemoizedFn(() => { diff --git a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx index ca69fbd5f6..4683444810 100644 --- a/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/defaultConstant.tsx @@ -287,7 +287,7 @@ export const defaultChatIPCData: UseChatIPCState = { }, reActTimelines: [], memoryList: { ...DefaultMemoryList }, - taskStatus: { loading: false, plan: '', task: '', taskID: '', status: AITaskStatus.created, coordinatorId: '' }, + taskStatus: { plan: '', task: '', taskID: '', status: AITaskStatus.created, coordinatorId: '' }, focusMode: '', switchLoading: false, planHistoryList: cloneDeep(DefaultPlanHistoryList), diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 5c41e6869f..577ed768c5 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -239,6 +239,15 @@ export interface AIAgentChatMetaData { /** 当前任务规划正在进行中的节点taskID */ currentTaskPlanActiveNode: Set + /** + * 任务规划结束门闩:end_plan_and_execution 与 react_task_status_changed 终态都到后才把 status 落到终态 + * 任一未到则 status 保持 processing(等待中) + */ + taskPlanEndGate: { + endReceived: boolean + pendingStatus?: 'completed' | 'aborted' | 'skipped' + } + /** 历史数据: review_release先出现的历史review数据的id-release */ historyReviewReleaseID: Record diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx index afa9b4635e..56380c4540 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/AIReActTaskChat.tsx @@ -328,10 +328,10 @@ const AIManualAddition: React.FC = React.memo((props) => const syncIdOfAddAndReExecute = useRef('') useUpdateEffect(() => { - if (!taskStatus.loading && currentCoordinatorIdRef.current) { + if (taskStatus.status !== AITaskStatus.inProgress && currentCoordinatorIdRef.current) { onSendRecover(currentCoordinatorIdRef.current) } - }, [taskStatus.loading]) + }, [taskStatus.status]) useEffect(() => { if ( @@ -368,13 +368,12 @@ const AIManualAddition: React.FC = React.memo((props) => cancelTaskLoading: true, }) - if (taskStatus?.loading && taskId) { + if (taskStatus.status === AITaskStatus.inProgress && taskId) { // 选停止当前任务,等待任务停止成功后,再发送恢复的数据 const info: AIInputEvent = { IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_REACT_CANCEL_TASK, SyncJsonInput: JSON.stringify({ task_id: taskId }), - SyncID: randomString(8), } onSend({ token: sessionId, type: 'task', params: info }) @@ -624,9 +623,7 @@ export const AIRenderTaskFooterExtra: React.FC = R const store = useCurrentStore() - const taskStatus = useStore(store, (state) => state.taskStatus) const cancelTaskLoading = useStore(store, (state) => state.cancelTaskLoading) - const status = useStore(store, (state) => state.taskStatus.status) const renderBtn = useMemoizedFn(() => { switch (status) { @@ -654,7 +651,7 @@ export const AIRenderTaskFooterExtra: React.FC = R case AITaskStatus.error: case AITaskStatus.skipped: case AITaskStatus.cancel: - return !taskStatus.loading ? ( + return ( } @@ -671,18 +668,6 @@ export const AIRenderTaskFooterExtra: React.FC = R > {t('AIRenderTaskFooterExtra.continueTask')} - ) : ( - } - className={styles['task-button']} - radius="28px" - size="large" - colors="danger" - loading={true} - > - {t('AIRenderTaskFooterExtra.stoppingTask')} - ) default: return null diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts index c64225987c..735708aa03 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActTaskChat/useTaskChatExtraAction.ts @@ -3,7 +3,7 @@ import useCurrentSessionId from '../hooks/useCurrentSessionId' import useAIAgentDispatcher from '@/pages/ai-agent/useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' import { useStore } from 'zustand' -import { useCurrentStore, useCurrentMeta, useCurrentRawData } from '../hooks/useCurrentDataBySession' +import { useCurrentStore, useCurrentRawData } from '../hooks/useCurrentDataBySession' import useCreation from 'ahooks/lib/useCreation' import useMemoizedFn from 'ahooks/lib/useMemoizedFn' import { globalSessionEngine } from '../hooks/ChatMultiSessionController' @@ -18,7 +18,6 @@ export const useTaskChatExtraAction = () => { const store = useCurrentStore() const rawData = useCurrentRawData() - const meta = useCurrentMeta() const execute = useStore(store, (state) => state.execute) const currentPlanReviewToken = useStore(store, (state) => state.currentPlanReviewToken) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index e197892494..5e7c03dab9 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -16,7 +16,12 @@ import { AttachedResourceTypeEnum, } from '@/pages/ai-agent/defaultConstant' import cloneDeep from 'lodash/cloneDeep' -import { DefaultMemoryList, DefaultPlanItemDetailsData } from './defaultConstant' +import { + DefaultMemoryList, + DefaultPlanItemDetailsData, + DefaultTaskPlanEndGate, + DefaultTaskPlanStatus, +} from './defaultConstant' import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHandlers' import { genExecTasks, handleTaskPlanEnd, pushLogToOtherWindow } from './utils' import type { AIChatIPCStartParams, AIChatSendParams } from './type' @@ -137,6 +142,7 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { taskMemoryList: cloneDeep(DefaultMemoryList), notifyMessageTimer: null, currentTaskPlanActiveNode: new Set(), + taskPlanEndGate: cloneDeep(DefaultTaskPlanEndGate), historyReviewReleaseID: {}, currentPlanReviewExtraId: '', planReviewExtraData: new Map(), @@ -940,6 +946,7 @@ export class ChatMultiSessionController { default: break } + this.requestMessage(token, params) } catch (error) {} } @@ -951,7 +958,7 @@ export class ChatMultiSessionController { /** 会话建立成功后, 需要做的额外操作 */ private handleSessionStartSuccess(sessionId: string) { - const { meta } = this.ensureSession(sessionId) + const { store, meta } = this.ensureSession(sessionId) // 获取任务规划历史任务树 this.requestMessage(sessionId, { @@ -968,6 +975,9 @@ export class ChatMultiSessionController { SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_MEMORY_CONTEXT, }) }, 5000) + + // 如果任务规划运行态有数据,则置空 + store.getState().updateState({ taskStatus: cloneDeep(DefaultTaskPlanStatus) }) } /** 💥 核心替换:接管原 useChatIPC 里的巨型数据分发逻辑! */ @@ -1267,9 +1277,12 @@ export class ChatMultiSessionController { this.closeSessionTimers(meta) // 任务规划结束后的相关逻辑 - handleTaskPlanEnd({ ...data, sessionId }) - // 核心状态改变 - store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) + handleTaskPlanEnd({ ...data, sessionId }, true) + store.getState().updateState({ + execute: false, + casualLoading: false, + casualTitle: '会话已停止', + }) this.readyChannels.delete(sessionId) const onEnd = meta.onEnd diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 642de1f245..c3a82ce1c7 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -1,6 +1,6 @@ import type { StreamResult } from '@/hook/useHoldGRPCStream/useHoldGRPCStreamType' import type { AIAgentGrpcApi, AIOutputEvent, AITaskStatusType, AIOutputI18n } from './grpcApi' -import type { AIFileSystemPin, AIQuestionQueues, PlanLoadingStatus, UseAIMessageDataState } from './type' +import type { AIFileSystemPin, AIQuestionQueues, UseAIMessageDataState } from './type' import { CustomPluginExecuteFormValue } from '@/pages/plugins/operator/localPluginExecuteDetailHeard/LocalPluginExecuteDetailHeardType' // #region AI-Agent 非会话列表外的渲染数据 @@ -472,6 +472,22 @@ export type AIChatQSData = | ChatStreamGroup // #endregion +// #region 状态机定义及其相关字段的定义 +/** 任务规划运行态:展示文案 + 当前活动任务身份/状态(是否在跑只看 status) */ +export interface TaskPlanStatus { + plan: string + task: string + /** 当前任务规划的 re_act_task_id,'' 表示无活动任务规划 */ + taskID: string + /** + * 当前任务规划状态,AITaskStatus.created 表示无; + * processing 表示执行中或等待 end∧change 齐套;终态表示已结束 + */ + status: AITaskStatusType + /** 当前任务规划的 coordinatorId,'' 表示无 */ + coordinatorId: string +} + /** 当前正在执行的任务树 */ export interface CurrentExecTaskTree { task_tree: AIAgentGrpcApi.PlanHistory['task_tree'] @@ -542,8 +558,8 @@ export interface ChatStoreState { focusMode: string /** UI是否显示中间的任务规划列表 */ showPlanList: boolean - /** 任务规划的loading状态信息 */ - taskStatus: PlanLoadingStatus + /** 任务规划运行态(展示文案 + 当前活动任务;是否在跑看 status) */ + taskStatus: TaskPlanStatus /** * 自由对话的当前review(未操作) @@ -652,7 +668,7 @@ export interface ChatStoreState { */ hydrateRenderTree: (content: SessionRenderContent) => void - updateTaskLoadingStatus: (status: Partial) => void + updateTaskLoadingStatus: (status: Partial) => void /** 正在等待用户操作的reviewId列表 */ updateCasualReview: (id: string, status: 'add' | 'remove') => void @@ -702,3 +718,4 @@ export interface ChatStoreState { replaceItemToken(oldToken: string, newToken: string) } +// #endregion diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index b35f19df05..7759bfc39b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -6,7 +6,7 @@ import { DefaultAIQuestionQueues, DefaultCurrentExecTaskTree, DefaultPlanHistoryList, - DefaultPlanLoadingStatus, + DefaultTaskPlanStatus, } from './defaultConstant' import { v4 as uuidv4 } from 'uuid' @@ -46,7 +46,7 @@ export const createChatStore = (options?: CreateChatStoreOptions) => { casualLoading: false, focusMode: '', showPlanList: false, - taskStatus: cloneDeep(DefaultPlanLoadingStatus), + taskStatus: cloneDeep(DefaultTaskPlanStatus), currentCasualReview: [], currentPlanReviewToken: { token: '', renderNum: 0 }, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts index 3d989206f2..816b000320 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/defaultConstant.ts @@ -1,7 +1,14 @@ -import type { AIToolResult, CurrentExecTaskTree, PlanItemDetailsData, TodoListCardData } from './aiRender' +import type { + AIToolResult, + CurrentExecTaskTree, + PlanItemDetailsData, + TaskPlanStatus, + TodoListCardData, +} from './aiRender' import { AITaskStatus } from './grpcApi' import type { AIOutputI18n, AIAgentGrpcApi } from './grpcApi' -import type { AIQuestionQueues, PlanLoadingStatus } from './type' +import type { AIQuestionQueues } from './type' +import type { AIAgentChatMetaData } from '@/pages/ai-agent/type/aiChat' /** 工具执行结果-默认值 */ export const DefaultAIToolResult: AIToolResult = { @@ -130,16 +137,21 @@ export const DefaultMemoryList: AIAgentGrpcApi.MemoryEntryList = { }, } -/** 任务规划loading-默认值 */ -export const DefaultPlanLoadingStatus: PlanLoadingStatus = { - loading: false, - plan: '加载中...', - task: '加载中...', +/** 任务规划运行态-默认值 */ +export const DefaultTaskPlanStatus: TaskPlanStatus = { + plan: '', + task: '', taskID: '', status: AITaskStatus.created, coordinatorId: '', } +/** end_plan_and_execution & react_task_status_changed 齐了才 settle status */ +export const DefaultTaskPlanEndGate: AIAgentChatMetaData['taskPlanEndGate'] = { + endReceived: false, + pendingStatus: undefined, +} + export const DefaultPlanHistoryList: AIAgentGrpcApi.PlanHistoryList = { records: [], total: 0, diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index e0cada40b0..11db72cf10 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -1,16 +1,17 @@ import type { AIMessageHandler } from '../type' import { AIInputEventSyncTypeEnum, AITaskStatus, AITaskStatusType, type AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' -import { genBaseAIChatData, genExecTasks, handleTaskPlanEnd } from '../utils' +import { genBaseAIChatData, genExecTasks, handleTaskPlanEnd, trySettleTaskPlanEnd } from '../utils' import { type AIChatQSData, AIChatQSDataTypeEnum } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' -import { DefaultCurrentExecTaskTree, DefaultPlanItemDetailsData } from '../defaultConstant' +import { DefaultCurrentExecTaskTree, DefaultPlanItemDetailsData, DefaultTaskPlanEndGate } from '../defaultConstant' import has from 'lodash/has' import { persistIndependentItem, persistToolResultIfTerminal, deletePersistedContent, } from '../persist/contentPersistHelper' +import type { AIAgentChatMetaData } from '@/pages/ai-agent/type/aiChat' const handleHttpFuzzRequestChange: AIMessageHandler = (request) => { const { res, store, rawData } = request @@ -68,22 +69,24 @@ const handleStartPlanAndExecution: AIMessageHandler = (requestInfo) => { sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN_EXEC_TASKS }) /** 获取最新任务树状态 */ sendRequest({ IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PLAN }) - + // 重置任务规划结束的中间状态 + meta.taskPlanEndGate = cloneDeep(DefaultTaskPlanEndGate) const taskStatus = { taskID: startInfo['re-act_task'], status: AITaskStatus.inProgress, // 取消任务规划需要的数据id coordinatorId: startInfo.coordinator_id, - loading: true, plan: '加载中...', task: '加载中...', } + // 初始化任务规划运行态数据和部分UI状态 store.getState().updateState({ taskStatus, showPlanList: true, cancelTaskLoading: false, }) - + // 重置当前任务树详情 + store.getState().updatePlanTree(cloneDeep(DefaultCurrentExecTaskTree)) // 生成任务规划里的默认任务聚合组 const taskID = `${taskStatus.taskID}-default` const chatData: AIChatQSData = { @@ -290,7 +293,7 @@ const handleNotify: AIMessageHandler = (request) => { const handlePlanExecTasks: AIMessageHandler = (request) => { const { res, store } = request - if (res.Type !== 'plan_exec_tasks') return + if (res.Type !== 'structured' || res.NodeId !== 'plan_exec_tasks') return const ipcContent = Uint8ArrayToString(res.Content) || '' const list = JSON.parse(ipcContent) as AIAgentGrpcApi.PlanHistoryList @@ -352,9 +355,11 @@ const handleReactTaskStatusChanged: AIMessageHandler = (request) => { store.getState().updateState({ focusMode: '', cancelCasualLoading: false, casualLoading: false }) } if (store.getState().taskStatus.taskID === react_task_id) { - // 该问题触发了任务规划, 所以需要将任务规划状态也调整 - store.getState().updateTaskLoadingStatus({ status: info.react_task_now_status }) - store.getState().updateState({ cancelTaskLoading: false }) + // 只推进中间状态;与 end 齐套后才落到 taskStatus.status + meta.taskPlanEndGate.pendingStatus = info.react_task_now_status as NonNullable< + AIAgentChatMetaData['taskPlanEndGate']['pendingStatus'] + > + trySettleTaskPlanEnd(store, meta) } } // 更新自由对话-执行任务组的状态 diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts index aaa592389c..9ca1480bc5 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts @@ -5,6 +5,7 @@ import type { AIStreamOutput, ChatListRenderType, ReActChatRenderItem, + TaskPlanStatus, } from './aiRender' import type { Dispatch, SetStateAction } from 'react' import type { Domain } from '@/pages/ai-agent/store/constants' @@ -109,19 +110,6 @@ export interface AIQuestionQueues { data: AIAgentGrpcApi.QuestionQueueItem[] } -/** 任务规划-loading状态信息 */ -export interface PlanLoadingStatus { - loading: boolean - plan: string - task: string - /** 当前任务规划的 re_act_task_id,'' 表示无活动任务规划 */ - taskID: string - /** 当前任务规划状态,'' (AITaskStatus.created) 表示无 */ - status: AITaskStatusType - /** 当前任务规划的 coordinatorId,'' 表示无 */ - coordinatorId: string -} - export interface UseChatIPCState { /** 流执行状态 */ execute: boolean @@ -142,8 +130,8 @@ export interface UseChatIPCState { /** 记忆列表 */ memoryList: AIAgentGrpcApi.MemoryEntryList - /** 任务规划的loading状态信息 */ - taskStatus: PlanLoadingStatus + /** 任务规划运行态(loading 文案 + 当前活动任务) */ + taskStatus: TaskPlanStatus /** 自由对话的loading 显示的文案 */ casualTitle: string /** 自由对话的是否进行中 */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts index 75a63da2c4..765ac5cf38 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/utils.ts @@ -17,9 +17,10 @@ import { v4 as uuidv4 } from 'uuid' import { JSONParseLog } from '@/utils/tool' import { aiAgentLogEmitter } from './AIAgentLogEmitter' import cloneDeep from 'lodash/cloneDeep' -import { DefaultPlanLoadingStatus } from './defaultConstant' +import { DefaultTaskPlanEndGate } from './defaultConstant' import { ChatMultiSessionController } from './ChatMultiSessionController' import { persistIndependentItem } from './persist/contentPersistHelper' +import type { AIAgentChatMetaData } from '@/pages/ai-agent/type/aiChat' /** * 任务节点内的数据生成任务节点ID @@ -54,10 +55,27 @@ export const genBaseAIChatData = (info: AIOutputEvent) => { } } -/** 任务规划结束后的所有数据处理 */ +/** + * end_plan_and_execution & react_task_status_changed 终态齐套后,才把 pendingStatus 落到 taskStatus.status + * 任一未到则保持 processing(等待中) + */ +export const trySettleTaskPlanEnd = ( + store: ReturnType['store'], + meta: AIAgentChatMetaData, +) => { + const gate = meta.taskPlanEndGate + if (!gate.endReceived || !gate.pendingStatus) return + store.getState().updateTaskLoadingStatus({ status: gate.pendingStatus }) + store.getState().updateState({ cancelTaskLoading: false }) + meta.taskPlanEndGate = cloneDeep(DefaultTaskPlanEndGate) +} + +/** 任务规划 end 事件:结构收尾 + 清展示文案 + 推进门闩(不清 id / 不直接改 status) */ export const handleTaskPlanEnd: ( requestInfo: ReturnType & { sessionId: string }, -) => void = (requestInfo) => { + /** 是否是聊天结束事件,如果是则清展示文案为已结束 */ + isChatEnd?: boolean, +) => void = (requestInfo, isChatEnd = false) => { const { sessionId, store, rawData, meta } = requestInfo // 将UI列表里正在执行中的任务组状态变成error @@ -72,6 +90,7 @@ export const handleTaskPlanEnd: ( store.getState().incrementNodeVersion(taskNodeInfo.id, 'task') persistIndependentItem(sessionId, taskNodeInfo) } + // 将当前正在执行的任务树里, 进行中的节点状态变成error const newPlanTree = cloneDeep(store.getState().taskChat.plan) newPlanTree.task_tree = newPlanTree.task_tree.map((item) => { @@ -79,7 +98,15 @@ export const handleTaskPlanEnd: ( return item }) store.getState().updatePlanTree(newPlanTree) - store.getState().updateState({ taskStatus: cloneDeep(DefaultPlanLoadingStatus) }) + + // end 只清展示文案,保留 taskID / coordinatorId / status(status 由 settle 写) + store.getState().updateTaskLoadingStatus({ plan: '已结束', task: '已结束' }) + if (isChatEnd) { + meta.taskPlanEndGate = cloneDeep(DefaultTaskPlanEndGate) + } else { + meta.taskPlanEndGate.endReceived = true + trySettleTaskPlanEnd(store, meta) + } } /** Agent 往日志窗口推送日志数据 */ From b8614caf8df27feb4c81aea55aafbaa3c49a48d3 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Tue, 28 Jul 2026 12:05:01 +0800 Subject: [PATCH 058/101] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../historyTaskTree/HistoryTaskTree.tsx | 4 ++- ...hildWindowConcurrentStreamCard.module.scss | 14 ++++++++ .../concurrentStreamFrame.ts | 13 ++++--- .../aiReActChatHeader/AIReActChatHeader.tsx | 36 +++++++++---------- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx index fd29addce7..40270cf1cc 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx @@ -25,10 +25,12 @@ import { useCurrentMeta, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurr import { useStore } from 'zustand' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' import { formatAIAgentSetting, onReStart } from '../../utils' +import { DefaultPlanHistoryList } from '@/pages/ai-re-act/hooks/defaultConstant' +import cloneDeep from 'lodash/cloneDeep' export const HistoryTaskTree: React.FC = memo((props) => { const store = useCurrentStore() - const planHistoryList = useStore(store, (state) => state.planHistoryList ?? []) + const planHistoryList = useStore(store, (state) => state.planHistoryList ?? cloneDeep(DefaultPlanHistoryList)) const taskTree = useStore(store, (state) => state.taskChat.plan.task_tree ?? []) const taskName = useStore(store, (state) => state.taskChat.plan.root_task_name ?? '') const coordinatorId = useStore(store, (state) => state.taskStatus.coordinatorId ?? '') diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.module.scss index 372aa6c5c5..3e96408faf 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.module.scss @@ -15,3 +15,17 @@ height: 100%; background-color: var(--Colors-Use-Basic-Background); } + +.goal { + color: var(--Colors-Use-Neutral-Text-2-Primary); + font-size: 12px; + font-weight: 400; + line-height: 16px; + letter-spacing: 0.5px; + padding: 4px 0; + + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + word-break: break-all; +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts index a17602f5a4..8c1c0b0574 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts @@ -1,5 +1,4 @@ import type { AIChatQSData, AIYakExecFileRecord, ChatListRenderType } from '@/pages/ai-re-act/hooks/aiRender' -import { isMap } from 'lodash' export interface ConcurrentStreamFramePayload { session: string @@ -13,14 +12,20 @@ export interface ConcurrentStreamFramePayload { taskName?: string } -export function isConcurrentStreamFrame(data: unknown): data is ConcurrentStreamFramePayload { +export interface FramePayload extends Pick< + ConcurrentStreamFramePayload, + 'session' | 'token' | 'chatType' | 'childrenTokens' +> { + renderNum?: number +} +export function isConcurrentStreamFrame(data: unknown): data is FramePayload { if (!data || typeof data !== 'object') return false const record = data as Record + // 只校验元数据字段;rawData/execFileRecord 在跨 IPC 传输时可能从 Map 退化为普通对象 return ( typeof record.session === 'string' && typeof record.token === 'string' && typeof record.chatType === 'string' && - Array.isArray(record.childrenTokens) && - isMap(record.rawData) + Array.isArray(record.childrenTokens) ) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx index a6957a62c2..bed9accc18 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx @@ -45,7 +45,6 @@ export const AIReActChatHeader: React.FC = React.memo((p const store = useCurrentStore() const focusMode = useStore(store, (state) => state.focusMode) const currentCasualTaskID = useStore(store, (state) => state.currentCasualTaskID) - const casualLoading = useStore(store, (state) => state.casualLoading) const sessionRef = useRef(undefined) @@ -55,16 +54,11 @@ export const AIReActChatHeader: React.FC = React.memo((p } }, [activeChat?.SessionID]) - useEffect(() => { - if (!casualLoading) return - syncCasualTaskTab() - }, [casualLoading]) - useEffect(() => { if (!activeChat?.Title || !activeChat?.SessionID) return if (sessionRef.current !== activeChat.SessionID) return emitTaskContentTab('update', activeChat.Title) - }, [activeChat?.Title, activeChat?.SessionID]) + }, [activeChat?.Title, activeChat?.SessionID, currentCasualTaskID]) const defaultTaskTabLabel = useCreation(() => { return typeof title === 'string' ? title : '自由对话' @@ -72,7 +66,8 @@ export const AIReActChatHeader: React.FC = React.memo((p const emitTaskContentTab = useMemoizedFn((type: 'add' | 'update', label?: string) => { const sessionId = activeChat?.SessionID - if (!currentCasualTaskID || !sessionId) return false + const taskId = currentCasualTaskID + if (!taskId || !sessionId) return false if (getSetting()?.Source !== 'ai') return false emiter.emit( 'actionAITaskContentTab', @@ -80,7 +75,7 @@ export const AIReActChatHeader: React.FC = React.memo((p type, params: { key: sessionId, - taskId: currentCasualTaskID, + taskId, label: label || activeChat?.Title || defaultTaskTabLabel, goal: '', }, @@ -108,7 +103,6 @@ export const AIReActChatHeader: React.FC = React.memo((p } syncCasualTaskTab() }) - return (
@@ -121,19 +115,21 @@ export const AIReActChatHeader: React.FC = React.memo((p )}
- {isShowRetract ? ( + {isShowRetract && ( <> - - - ) : ( - <> - {currentCasualTaskID && ( - } onClick={onDetails}> - 任务详情 - + {!!externalParameters?.rightIcon ? ( + + ) : ( + <> + {currentCasualTaskID && ( + } onClick={onDetails}> + 任务详情 + + )} + handleSwitchShowFreeChat(false)} /> + )} - handleSwitchShowFreeChat(false)} /> )}
From ca7e670624ee46ee918f0b8eeae8b1c79dd8b2bf Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Tue, 28 Jul 2026 15:38:10 +0800 Subject: [PATCH 059/101] =?UTF-8?q?perf:=E5=AD=90=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E6=89=80=E6=9C=89=E5=AF=B9=E8=B1=A1=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=83=BD=E5=9C=A8=E7=AA=97=E5=8F=A3=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E5=A5=BD=E5=90=8E=E5=86=8D=E5=90=91=E4=B8=BB=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E8=BF=9B=E7=A8=8B=E6=8B=BF=E6=95=B0=E6=8D=AE=20feat:=E5=88=B7?= =?UTF-8?q?=E6=96=B0=E6=95=B0=E6=8D=AE=E6=94=B9=E4=B8=BAgetRawData=20fix:?= =?UTF-8?q?=E5=AD=90=E4=BB=BB=E5=8A=A1=E7=AA=97=E5=8F=A3=E6=B5=81=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E6=B2=A1=E6=9C=89=E5=B1=95=E7=A4=BA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../openAIConcurrentStream/index.js | 18 +-- .../AIConcurrentStream/AIConcurrentStream.tsx | 66 +++++----- .../fetchConcurrentStreamContents.ts | 34 +++-- .../useContext/AIConcurrentStreamContent.ts | 11 +- .../ConcurrentStreamCard.tsx | 3 - .../ConcurrentStreamCardActions.tsx | 15 +-- .../buildConcurrentStreamFramePayload.ts | 45 ++++--- .../concurrentStreamMainBridge.ts | 7 +- .../useConcurrentStreamRefreshListener.ts | 58 --------- .../concurrentStreamFrame.ts | 26 ++-- .../AIChildWindowConcurrentStreamContent.tsx | 4 +- .../AIChildWindowGroupStreamCard.module.scss | 31 +++-- .../AIChildWindowGroupStreamCard.tsx | 122 ++++++++++++++++-- .../aiChildWindowGroupStreamCard/type.ts | 15 +++ .../aiGroupStreamCard/AIGroupStreamCard.tsx | 2 +- .../AIGroupStreamCardList.tsx | 8 +- .../components/aiGroupStreamCard/type.ts | 3 +- .../src/main/src/utils/openWebsite.tsx | 7 +- 18 files changed, 270 insertions(+), 205 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts diff --git a/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js b/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js index d10605474b..3fc3ff4dea 100644 --- a/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js +++ b/app/main/handlers/auxWindowManager/openAIConcurrentStream/index.js @@ -35,7 +35,7 @@ function register(manager, mainWindow) { */ ipcMain.handle(FETCH_CONTENTS, async (_event, frame) => { if (!frame?.session || !frame?.token || !mainWindow || mainWindow.isDestroyed()) { - return { rawData: [], execFileRecord: [] } + return { rawData: [], execFileRecord: [], childrenTokens: [] } } const requestId = crypto.randomUUID() @@ -44,12 +44,12 @@ function register(manager, mainWindow) { return new Promise((resolve) => { const timeout = setTimeout(() => { ipcMain.removeAllListeners(responseChannel) - resolve({ rawData: [], execFileRecord: [] }) + resolve({ rawData: [], execFileRecord: [], childrenTokens: [] }) }, 15000) ipcMain.once(responseChannel, (_responseEvent, data) => { clearTimeout(timeout) - resolve(data ?? { rawData: [], execFileRecord: [] }) + resolve(data ?? { rawData: [], execFileRecord: [], childrenTokens: [] }) }) safeSendMain('fetch-concurrent-stream-contents-request', { requestId, ...frame }) @@ -57,7 +57,7 @@ function register(manager, mainWindow) { }) ipcMain.handle('open-ai-concurrent-stream-window', async (_event, data) => { - if (!data || typeof data !== 'object' || !Array.isArray(data.childrenTokens)) return + if (!data || typeof data !== 'object') return const singletonKey = buildSingletonKey(data) const title = typeof data.taskName === 'string' && data.taskName ? data.taskName : 'Concurrent Stream' return manager.create({ @@ -69,16 +69,6 @@ function register(manager, mainWindow) { height: 800, }) }) - - ipcMain.on('request-ai-concurrent-stream-refresh', (event, params) => { - for (const entry of manager.windows.values()) { - if (entry.win.isDestroyed()) continue - if (entry.meta.route !== ROUTE) continue - if (entry.win.webContents !== event.sender) continue - safeSendMain('refresh-ai-concurrent-stream', params) - return - } - }) } module.exports = { register, ROUTE } diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx index 39ed610e9a..d3e60302cf 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx @@ -13,6 +13,7 @@ import AIConcurrentStreamContent, { AIConcurrentStreamStore, } from './useContext/AIConcurrentStreamContent' import useMemoizedFn from 'ahooks/lib/useMemoizedFn' +import { useDebounceFn } from 'ahooks' // 子卡片按需加载,避免重型卡片(AINodeItem 及其下游 review/report/fuzz 等子卡) // 全量进入 aux bundle,拉长 did-finish-load 与首次开窗耗时。 @@ -34,29 +35,31 @@ interface AIConcurrentStreamProps { const AIConcurrentStream: React.FC = memo(({ windowId }) => { const [frame, setFrame] = useState(null) const [contentVersion, setContentVersion] = useState(0) - // rawData/execFileRecord 是否仍在通过 fetch-concurrent-stream-contents 拉取中 + // rawData/execFileRecord/childrenTokens 是否仍在通过 fetch-concurrent-stream-contents 拉取中 const [loadingContents, setLoadingContents] = useState(false) // rawData/execFileRecord 用 ref 存储,更新不触发渲染; // 组件及子组件的重渲染由 contentVersion(renderNum)驱动 const rawDataRef = useRef>(new Map()) const execFileRecordRef = useRef>(new Map()) + const childrenTokensRef = useRef([]) useEffect(() => { if (!windowId) return const applyFrame = (payload: ConcurrentStreamFramePayload) => { if (!isConcurrentStreamFrame(payload)) return - const framePayload = payload - // 开窗时 frame 只携带轻量元数据(rawData 为空 Map), - // 真正的 rawData 由下方的懒拉取 effect 通过 IPC 向主窗口请求 + // 开窗时 frame 只携带轻量元数据 + // 真正的数据由下方的懒拉取 effect 通过 IPC 向主窗口请求 startTransition(() => { const newFrame: ConcurrentStreamFramePayload = { - ...framePayload, - rawData: new Map(), - execFileRecord: new Map(), + ...payload, } - setFrame(newFrame) + setFrame((v) => ({ + ...v, + ...newFrame, + renderNum: (v?.renderNum || newFrame.renderNum || 0) + 1, + })) // 收到 frame 后,主动向主窗口拉取本次需要渲染的 rawData。 getRawData(newFrame) @@ -81,18 +84,22 @@ const AIConcurrentStream: React.FC = memo(({ windowId } } }, [windowId]) - const getRawData = useMemoizedFn((frame) => { - setLoadingContents(true) - fetchConcurrentStreamContents(frame) - .then((entries) => { - rawDataRef.current = entries.rawData - execFileRecordRef.current = entries.execFileRecord - }) - .finally(() => { - setContentVersion((v) => v + 1) - setLoadingContents(false) - }) - }) + const getRawData = useDebounceFn( + (frame) => { + setLoadingContents(true) + fetchConcurrentStreamContents(frame) + .then((entries) => { + rawDataRef.current = entries.rawData + execFileRecordRef.current = entries.execFileRecord + childrenTokensRef.current = entries.childrenTokens + }) + .finally(() => { + setContentVersion((v) => v + 1) + setLoadingContents(false) + }) + }, + { wait: 500, leading: true }, + ).run const isTaskDefaultGroup = useMemo(() => { if (!frame) return false @@ -103,26 +110,19 @@ const AIConcurrentStream: React.FC = memo(({ windowId } // 刷新:通过 IPC 通知主窗口重新构建并推送最新 frame(含最新 rawData) const requestRefresh = useMemoizedFn(() => { if (!frame) return - ipcRenderer.send('request-ai-concurrent-stream-refresh', { - type: 'openAIConcurrentStream', - data: { - session: frame.session, - token: frame.token, - chatType: frame.chatType, - }, - }) + getRawData(frame) }) const store: AIConcurrentStreamStore = useMemo(() => { return { - session: frame?.session, - token: frame?.token, - chatType: frame?.chatType, - childrenTokens: frame?.childrenTokens, + session: frame?.session ?? '', + token: frame?.token ?? '', + chatType: frame?.chatType ?? 'task', + childrenTokens: [...childrenTokensRef.current], rawData: rawDataRef.current, execFileRecord: execFileRecordRef.current, renderNum: contentVersion, } - }, [frame, contentVersion]) + }, [contentVersion]) const dispatcher: AIConcurrentStreamDispatcher = useMemo(() => { return { requestRefresh, diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts index 7b171517e0..833cfe6d32 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/fetchConcurrentStreamContents.ts @@ -1,26 +1,32 @@ import type { AIChatQSData, AIYakExecFileRecord } from '@/pages/ai-re-act/hooks/aiRender' -import type { ConcurrentStreamFramePayload } from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' +import type { + ConcurrentStreamFramePayload, + FramePayload, +} from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' const { ipcRenderer } = window.require('electron') -export interface FetchConcurrentStreamContentsResponse { - rawData: Map - execFileRecord: Map -} +export interface FetchConcurrentStreamContentsResponse extends Omit {} /** * 子窗口通过 IPC 向主窗口拉取 task 相关的全部 content 数据。 - * 主窗口从 globalSessionEngine 取最新 store + rawData,复用 buildConcurrentStreamFramePayload - * 收集 task 自身、childrenTokens 各节点、group 内子节点的数据, - * 返回为 Map(token → 原始数据)。 */ export async function fetchConcurrentStreamContents( frame: ConcurrentStreamFramePayload, ): Promise { - const result = await ipcRenderer.invoke('fetch-concurrent-stream-contents', frame) - const rawData: Array<[string, AIChatQSData]> = result?.rawData ?? [] - const execFileRecord: Array<[string, AIYakExecFileRecord[]]> = result?.execFileRecord ?? [] - return { - rawData: new Map(rawData), - execFileRecord: new Map(execFileRecord), + try { + const result = await ipcRenderer.invoke('fetch-concurrent-stream-contents', frame) + const rawData: Array<[string, AIChatQSData]> = result?.rawData ?? [] + const execFileRecord: Array<[string, AIYakExecFileRecord[]]> = result?.execFileRecord ?? [] + return { + rawData: new Map(rawData), + execFileRecord: new Map(execFileRecord), + childrenTokens: result?.childrenTokens ?? [], + } + } catch (error) { + return { + rawData: new Map(), + execFileRecord: new Map(), + childrenTokens: [], + } } } diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/AIConcurrentStreamContent.ts b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/AIConcurrentStreamContent.ts index 162df4cb9d..fff6d8d11e 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/AIConcurrentStreamContent.ts +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/useContext/AIConcurrentStreamContent.ts @@ -1,10 +1,10 @@ -import { ConcurrentStreamFramePayload } from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' +import { + ConcurrentStreamFramePayload, + FramePayload, +} from '@/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame' import { createContext } from 'react' -export interface AIConcurrentStreamStore extends Partial { - /** 子窗口刷新版本号,点击刷新按钮自增,驱动子组件重渲染 */ - renderNum?: number -} +export interface AIConcurrentStreamStore extends ConcurrentStreamFramePayload, FramePayload {} export interface AIConcurrentStreamDispatcher { requestRefresh: () => void @@ -23,6 +23,7 @@ export default createContext({ childrenTokens: [], rawData: new Map(), renderNum: 0, + execFileRecord: new Map(), }, dispatcher: { requestRefresh: () => {}, diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index ad12a2c837..19e6943a53 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -15,7 +15,6 @@ import type { import classNames from 'classnames' import ConcurrentStreamCardHeard from './concurrentStreamCardHeard/ConcurrentStreamCardHeard' import ConcurrentStreamContent from './ConcurrentStreamContent/ConcurrentStreamContent' -import { useConcurrentStreamRefreshListener } from './concurrentStream/useConcurrentStreamRefreshListener' const ConcurrentStreamCard: FC<{ token: string @@ -43,8 +42,6 @@ const ConcurrentStreamCard: FC<{ } }, [raw?.data?.status]) - useConcurrentStreamRefreshListener(token, true) - const presentation = useMemo(() => getAIStatusPresentation(raw?.data?.status), [raw?.data?.status]) const vectorBg = useVectorStripeBg(presentation.stripeColor) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx index 3039f1437f..655e633f42 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx @@ -15,7 +15,7 @@ import styles from '../ConcurrentStreamCard.module.scss' import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useCreation, useMemoizedFn } from 'ahooks' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' -import { buildConcurrentStreamFramePayload } from '../concurrentStream/buildConcurrentStreamFramePayload' +import { getTaskName } from '../concurrentStream/buildConcurrentStreamFramePayload' /** 卡片标题栏右侧操作区 */ interface ConcurrentStreamCardActionsProps { @@ -61,18 +61,15 @@ const ConcurrentStreamCardActions: FC = ({ const openChildWindow = useMemoizedFn((e) => { e?.stopPropagation() - // 开窗只传轻量元数据(rawData 为空 Map),rawData 由子窗 mount 后再拉取,避免开窗瞬间克隆大 Map - const framePayload = buildConcurrentStreamFramePayload({ + if (!chatType) return + // 开窗只传轻量元数据 + // 只传基础类型数据 + openAIConcurrentStream({ token, session, chatType, - store, - rawData, - withRawData: false, + taskName: getTaskName(rawData, token), }) - if (framePayload) { - openAIConcurrentStream(framePayload) - } }) if (isChildWindow) { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts index bc34f07079..e30d2b8ebe 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/buildConcurrentStreamFramePayload.ts @@ -1,4 +1,3 @@ -import type { OpenAIConcurrentStreamPayload } from '@/utils/openWebsite' import { AIChatQSDataTypeEnum, AIYakExecFileRecord, @@ -7,6 +6,7 @@ import { } from '@/pages/ai-re-act/hooks/aiRender' import { AIItemKind, getAIItemKind } from '@/pages/ai-re-act/hooks/useAIItemKind' import i18n from '@/i18n/i18n' +import { FramePayload } from '../concurrentStreamFrame' /** store 的最小依赖接口 */ interface BuildFrameStore { @@ -40,7 +40,7 @@ function getKind(store: BuildFrameStore, childToken: string): AIItemKind | null const tOriginal = i18n.getFixedT(null, 'aiAgent') /** 获取 task 节点的名称 */ -function getTaskName(rawData: BuildFrameRawData | null | undefined, token: string): string { +export function getTaskName(rawData: BuildFrameRawData | null | undefined, token: string): string { const itemData = rawData?.contents.get(token) if (!itemData) return '' switch (itemData.type) { @@ -56,32 +56,45 @@ function getTaskName(rawData: BuildFrameRawData | null | undefined, token: strin /** * 从主窗口 store + rawData 构建 ConcurrentStreamFramePayload。 * 收集 task 自身、所有 childrenTokens 节点、group 内子节点的原始数据, - * 逻辑由 openChildWindow 与 useConcurrentStreamRefreshListener 共用。 */ export function buildConcurrentStreamFramePayload( params: BuildConcurrentStreamFramePayloadParams, -): OpenAIConcurrentStreamPayload | null { - const { token, session, chatType, store, rawData, withRawData = true } = params +): FramePayload | null { + const { token, chatType, store, rawData, withRawData = true } = params if (!chatType || !rawData) return null const frameRawData = new Map() let execFileRecord = new Map() const state = store.getState() - const childrenTokens = state.tasks[token]?.childrenTokens || [] + let childrenTokens: string[] = [] + const handFileRecord = (record) => { + switch (record.type) { + case AIChatQSDataTypeEnum.TOOL_RESULT: + const fileRecord = state.execFileRecord.get(record.data.callToolId) + if (fileRecord) execFileRecord.set(record.data.callToolId, fileRecord) + break + + default: + break + } + } if (withRawData) { // task 自身数据 const taskData = rawData.contents.get(token) if (taskData) frameRawData.set(token, taskData) - + const tokens = state.tasks[token]?.childrenTokens // 遍历所有子节点 - for (const childToken of childrenTokens) { + for (const childToken of tokens) { const kind = getKind(store, childToken) if (!kind) continue const childData = rawData.contents.get(childToken) if (!childData) continue frameRawData.set(childToken, childData) + if (kind === 'item') { + handFileRecord(childData) + } // group 下的所有子节点数据 if (kind === 'group') { const groupData = state.groups[childToken] @@ -89,20 +102,20 @@ export function buildConcurrentStreamFramePayload( const grandChildData = rawData.contents.get(grandChildToken) if (!grandChildData) continue frameRawData.set(grandChildToken, grandChildData) + handFileRecord(grandChildData) } } } - - execFileRecord = new Map(state.execFileRecord) + childrenTokens = state.tasks[token]?.childrenTokens || [] } return { - session, - token, - chatType: chatType as OpenAIConcurrentStreamPayload['chatType'], - childrenTokens, + // session, + // token, + // chatType: chatType as OpenAIConcurrentStreamPayload['chatType'], + childrenTokens: [...childrenTokens], rawData: frameRawData, - execFileRecord, - taskName: getTaskName(rawData, token), + execFileRecord: execFileRecord, + // taskName: getTaskName(rawData, token), } } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts index 345b715e6b..b6bdc528b5 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/concurrentStreamMainBridge.ts @@ -29,8 +29,13 @@ export function setupConcurrentStreamMainBridge() { const full = buildConcurrentStreamFramePayload({ token, session, chatType, store, rawData, withRawData: true }) const entries = full ? Array.from(full.rawData.entries()) : [] const execFileRecord = full ? Array.from(full.execFileRecord.entries()) : [] + const childrenTokens = full ? full.childrenTokens : [] - ipcRenderer.send(`fetch-concurrent-stream-contents-response-${requestId}`, { rawData: entries, execFileRecord }) + ipcRenderer.send(`fetch-concurrent-stream-contents-response-${requestId}`, { + rawData: entries, + execFileRecord, + childrenTokens, + }) } ipcRenderer.on(FETCH_REQUEST, handler) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts deleted file mode 100644 index 1d648742bc..0000000000 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStream/useConcurrentStreamRefreshListener.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { useEffect } from 'react' -import { openAIConcurrentStream } from '@/utils/openWebsite' -import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' -import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' -import { useCreation } from 'ahooks' -import { buildConcurrentStreamFramePayload } from './buildConcurrentStreamFramePayload' - -const { ipcRenderer } = window.require('electron') - -/** - * 主窗口卡片监听子窗刷新请求。 - * 收到请求后从主窗口 store 重新构建最新的 framePayload(含最新 rawData), - * 静默推送到子窗,逻辑与 openChildWindow 共用 buildConcurrentStreamFramePayload。 - */ -export function useConcurrentStreamRefreshListener(token: string, enabled = true) { - const store = useCurrentStore() - const session = useCurrentSessionId() - const rawData = useCurrentRawData() - - const chatType = useCreation(() => { - if (!rawData) return - const itemData = rawData.contents.get(token) - if (!itemData) return - return itemData.chatType - }, []) - - useEffect(() => { - if (!enabled) return - - const handleRefresh = (_event: unknown, params: { type?: string; data?: Record }) => { - if (params?.type !== 'openAIConcurrentStream') return - - const refreshData = params.data - if (refreshData?.session !== session || refreshData?.token !== token || refreshData?.chatType !== chatType) { - return - } - - // 刷新同样只推元数据,rawData 由子窗重新拉取,与开窗路径保持一致 - const framePayload = buildConcurrentStreamFramePayload({ - token, - session, - chatType, - store, - rawData, - withRawData: false, - }) - if (framePayload) { - openAIConcurrentStream(framePayload, { silent: true }) - } - } - - ipcRenderer.on('refresh-ai-concurrent-stream', handleRefresh) - - return () => { - ipcRenderer.removeListener('refresh-ai-concurrent-stream', handleRefresh) - } - }, [enabled, session, token, chatType, store, rawData]) -} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts index 8c1c0b0574..e2b7225238 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts @@ -4,28 +4,24 @@ export interface ConcurrentStreamFramePayload { session: string token: string chatType: ChatListRenderType + taskName?: string + renderNum?: number +} + +export interface FramePayload { childrenTokens: string[] /** childrenTokens 中各节点 token 对应的原始数据 */ rawData: Map - execFileRecord: Map - taskName?: string -} - -export interface FramePayload extends Pick< - ConcurrentStreamFramePayload, - 'session' | 'token' | 'chatType' | 'childrenTokens' -> { - renderNum?: number } -export function isConcurrentStreamFrame(data: unknown): data is FramePayload { - if (!data || typeof data !== 'object') return false - const record = data as Record - // 只校验元数据字段;rawData/execFileRecord 在跨 IPC 传输时可能从 Map 退化为普通对象 +export function isConcurrentStreamFrame(record: unknown): record is ConcurrentStreamFramePayload { + if (!record || typeof record !== 'object') return false return ( + 'session' in record && + 'token' in record && + 'chatType' in record && typeof record.session === 'string' && typeof record.token === 'string' && - typeof record.chatType === 'string' && - Array.isArray(record.childrenTokens) + typeof record.chatType === 'string' ) } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx index 07679c71a9..7bd05070df 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx @@ -5,11 +5,11 @@ import useClickFocus from '@/pages/ai-re-act/hooks/useClickFocus' import React, { type FC, memo } from 'react' import AIChildWindowGroupItem from '../aiChildWindowGroupItem/AIChildWindowGroupItem' import AIChildWindowNodeItemWrapper from '../aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper' +import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' const AIChildWindowConcurrentStreamContent: FC = memo(() => { const { childrenTokens, rawData } = useAIConcurrentStreamStore() const { ref: scrollRef, isFocus } = useClickFocus() - return (
{ if (!item) return return (
- {!!item.parentGroupToken ? ( + {item.type === AIChatQSDataTypeEnum.STREAM_GROUP ? ( ) : ( diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss index 933a5211d4..615a69103d 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.module.scss @@ -1,15 +1,24 @@ // 子窗口 stream group 卡片样式。 -// 内容对齐 ConcurrentStreamContent.module.scss。 +// 内容对齐 AIGroupStreamCard.module.scss。 -.concurrent-stream-content-item { - margin-bottom: 8px; - // 连续流卡片之间去掉间距(concurrent-stream-card 本身排除在外保留间距) - &:has(:global(.ai-stream-chat-content-wrapper)) { - & - + .concurrent-stream-content-item:has(:global(.ai-stream-chat-content-wrapper)):not( - :has(:global(.concurrent-stream-card)) - ) { - margin-top: -8px; - } +.container { + width: 100%; + border-radius: 8px; + padding: 2px 4px 0; + display: flex; + flex-direction: column; + // gap: 4px; + border: 1px solid transparent; + // 遮罩底色变量:默认走基础背景,hover/focus 时由父级切换,子模块消费 + --group-stream-mask-bg: var(--Colors-Use-Basic-Background); + &:hover { + background: var(--Colors-Use-Neutral-Bg); + --group-stream-mask-bg: var(--Colors-Use-Neutral-Bg); } } +.container-focus { + border-color: var(--Colors-Use-Main-Hover); + background: var(--Colors-Use-Neutral-Bg); + // focus 时取消遮罩:变量置透明,子模块渐变自然透明,等效原 background: initial + --group-stream-mask-bg: transparent; +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx index c5a3b7bcee..d8efbba7db 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx @@ -1,32 +1,130 @@ import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' -import { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' +import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' import { useCreation } from 'ahooks' -import { FC, memo } from 'react' +import { type FC, memo, useState } from 'react' import AIChildWindowNodeItemWrapper from '../aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper' -import { AIChildWindowGroupStreamCardProps } from './type' +import type { + AIChildWindowGroupStreamCardHeardWrapperProps, + AIChildWindowGroupStreamCardListWrapperProps, + AIChildWindowGroupStreamCardProps, +} from './type' import styles from './AIChildWindowGroupStreamCard.module.scss' +import AIGroupStreamCardHeard from '../../aiGroupStreamCard/aiGroupStreamCardHeard/AIGroupStreamCardHeard' +import useAINodeLabel from '@/pages/ai-re-act/hooks/useAINodeLabel' +import { STREAM_MASK_THRESHOLD } from '../../aiGroupStreamCard/AIGroupStreamCard' +import classNames from 'classnames' +import useClickFocus from '@/pages/ai-re-act/hooks/useClickFocus' +import AIGroupStreamCardList from '../../aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList' +import React from 'react' /** 子窗口版 stream group 卡片,从 rawData 中按 parentGroupToken 查找子节点 */ -const AIChildWindowGroupStreamCard: FC = memo(({ token }) => { +const AIChildWindowGroupStreamCard: FC = memo((props) => { + const { token } = props const { rawData, renderNum } = useAIConcurrentStreamStore() + const { ref: containerRef, isFocus } = useClickFocus() + + const [expand, setExpand] = useState(true) // 按 token + renderNum 缓存该 group 的子节点,避免每次渲染都全量 forEach - const childItems = useCreation(() => { + const childItemTokens = useCreation(() => { if (!rawData) return [] - const items: AIChatQSData[] = [] + const items: string[] = [] rawData.forEach((value) => { if (value.parentGroupToken === token) { - items.push(value) + items.push(value.id) } }) return items }, [token, renderNum]) - + const lastToken = useCreation(() => { + return childItemTokens.length > 0 ? childItemTokens[childItemTokens.length - 1] : '' + }, [childItemTokens.length]) return ( -
- {childItems.map((item, index) => ( - - ))} +
+ +
) }) export default AIChildWindowGroupStreamCard + +const AIChildWindowGroupStreamCardHeardWrapper: FC = memo((props) => { + const { token, lastToken, childrenTokensLength, setExpand, expand } = props + const { rawData, renderNum } = useAIConcurrentStreamStore() + const { getLabelByParams } = useAINodeLabel() + + const lastItem = useCreation(() => { + const lastItem = rawData.get(lastToken) + if (!lastItem) return undefined + switch (lastItem.type) { + case AIChatQSDataTypeEnum.STREAM: + return lastItem + + default: + return undefined + } + }, [lastToken]) + const shouldShowMask = useCreation(() => { + const lastItem = rawData.get(lastToken) + if (!lastItem) return false + switch (lastItem.type) { + case AIChatQSDataTypeEnum.STREAM: + const contentLength = lastItem.data?.content?.length || 0 + return contentLength > STREAM_MASK_THRESHOLD + + default: + return false + } + }, [lastToken]) + const nodeLabel = useCreation(() => { + const groupData = rawData.get(token) + if (!groupData) return '' + switch (groupData.type) { + case AIChatQSDataTypeEnum.STREAM_GROUP: + return getLabelByParams(groupData.data?.NodeIdVerbose) + + default: + return '' + } + }, [renderNum]) + return ( + + ) +}) + +const AIChildWindowGroupStreamCardListWrapper: React.FC = memo( + (props) => { + const { childItemTokens, expand } = props + const { rawData } = useAIConcurrentStreamStore() + return ( + <> + { + const itemData = rawData.get(token) + if (!itemData) return + return + }} + /> + + ) + }, +) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/type.ts index 5e1b9319a5..7497388cc9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/type.ts @@ -1,3 +1,18 @@ +import type { Dispatch, SetStateAction } from 'react' + export interface AIChildWindowGroupStreamCardProps { token: string } + +export interface AIChildWindowGroupStreamCardListWrapperProps { + childItemTokens: string[] + expand: boolean +} + +export interface AIChildWindowGroupStreamCardHeardWrapperProps { + token: string + lastToken: string + childrenTokensLength: number + expand: boolean + setExpand: Dispatch> +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx index 41de70bc7c..1bca0f5468 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard.tsx @@ -67,7 +67,7 @@ export const AIGroupStreamNode: FC<{ ) }) -const STREAM_MASK_THRESHOLD = 170 +export const STREAM_MASK_THRESHOLD = 170 const AIGroupStreamCard: FC<{ token: string diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx index 855f4b9c34..1a1a21dc34 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/aiGroupStreamCardList/AIGroupStreamCardList.tsx @@ -8,7 +8,7 @@ import styles from './AIGroupStreamCardList.module.scss' const BOTTOM_THRESHOLD = 10 const AIGroupStreamCardList: React.FC = memo((props) => { - const { expand, childrenTokens } = props + const { expand, childrenTokens, rendItem } = props const contentRef = useRef(null) const [isScroll, setIsScroll] = useState(false) @@ -77,9 +77,9 @@ const AIGroupStreamCardList: React.FC = memo((props) overflow: isScroll ? 'overlay' : 'hidden', }} > - {childrenTokens.map((token, index) => ( - - ))} + {childrenTokens.map((token, index) => + !!rendItem ? rendItem(token, index) : , + )}
) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts index 1c529c3eb7..c0b1282089 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiGroupStreamCard/type.ts @@ -1,5 +1,5 @@ import { ChatStream } from '@/pages/ai-re-act/hooks/aiRender' -import { Dispatch, SetStateAction } from 'react' +import { Dispatch, ReactNode, SetStateAction } from 'react' export interface AIGroupStreamCardHeardProps { expand: boolean @@ -24,4 +24,5 @@ export interface AIGroupStreamCardListWrapperProps { export interface AIGroupStreamCardListProps { expand: boolean childrenTokens: string[] + rendItem?: (token: string, index: number) => ReactNode } diff --git a/app/renderer/src/main/src/utils/openWebsite.tsx b/app/renderer/src/main/src/utils/openWebsite.tsx index cfe89a3258..61148e0fe1 100644 --- a/app/renderer/src/main/src/utils/openWebsite.tsx +++ b/app/renderer/src/main/src/utils/openWebsite.tsx @@ -14,8 +14,6 @@ import type { ConcurrentStreamFramePayload } from '@/pages/ai-agent/components/C import { yakitDialog, yakitShell, yakitWindow } from '@/services/electronBridge' const tOriginal = i18n.getFixedT(null, ['utils', 'yakitUi']) -export type OpenAIConcurrentStreamPayload = ConcurrentStreamFramePayload - const { ipcRenderer } = window.require('electron') const toWritableText = (data?: Uint8Array | string) => { @@ -76,10 +74,7 @@ export interface OpenAIConcurrentStreamOptions { } /** 打开并发流 aux 子窗,创建时传入 elements 等帧数据 */ -export const openAIConcurrentStream = ( - data: OpenAIConcurrentStreamPayload, - options?: OpenAIConcurrentStreamOptions, -) => { +export const openAIConcurrentStream = (data: ConcurrentStreamFramePayload, options?: OpenAIConcurrentStreamOptions) => { if (!options?.silent) { yakitNotify('info', tOriginal('OpenWebsite.openingNewWindow')) } From a10626f54205b38209668f0251b840bc259d2c30 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Tue, 28 Jul 2026 17:34:24 +0800 Subject: [PATCH 060/101] =?UTF-8?q?feat:=E9=85=8D=E7=BD=AE=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=90=8E=E9=9C=80=E8=A6=81=E6=9B=B4=E6=96=B0session?= =?UTF-8?q?=E5=AF=B9=E5=BA=94=E7=9A=84=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/src/components/withHistoryAIReActChat.tsx | 6 ++++++ app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx | 3 +++ 2 files changed, 9 insertions(+) diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index 81232ae2bd..e890fe54c1 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -49,6 +49,7 @@ import { HistroryAIReActChat } from './HistroryAIReActChat' import { useChatIPC } from '@/pages/ai-re-act/hooks/useChatIPC' import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' +import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' export type HistoryAIReActChatExternalParameters = NonNullable @@ -222,6 +223,11 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid return route === YakitRoute.YakScript && !!pageId }, [route, pageId]) + useUpdateEffect(() => { + // 只有配置变化了才更新,SessionID不管 + if (activeChat?.SessionID) globalSessionEngine.updateSessionConfig(activeChat?.SessionID, getSetting()) + }, [setting]) + useEffect(() => { if (!showFreeChat) { chatReadyRef.current = false diff --git a/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx b/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx index 859e5ad683..a4a830c314 100644 --- a/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx @@ -33,6 +33,7 @@ import { useChatIPC } from '../ai-re-act/hooks/useChatIPC' import { AISourceEnum } from '../ai-re-act/hooks/grpcApi' import { YakitRoute } from '@/enums/yakitRoute' import { onReStart } from './utils' +import { globalSessionEngine } from '../ai-re-act/hooks/ChatMultiSessionController' /** 清空用户缓存的固定值 */ export const AIAgentCacheClearValue = '20260113' @@ -95,6 +96,8 @@ export const AIAgent: React.FC = (props) => { // 缓存全局配置数据 useUpdateEffect(() => { const cache = omit(getSetting(), ['AIService', 'AIModelName']) + // 只有配置变化了才更新,SessionID不管 + if (activeChat?.SessionID) globalSessionEngine.updateSessionConfig(activeChat?.SessionID, getSetting()) setRemoteValue(RemoteAIAgentGV.AIAgentChatSetting, JSON.stringify(cache)) }, [setting]) From 25197a58b2747f268f74d2c67152ac1747de17c5 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Tue, 28 Jul 2026 18:14:30 +0800 Subject: [PATCH 061/101] =?UTF-8?q?fix:=20=E5=8A=A0=E5=9B=BA=20AI=20?= =?UTF-8?q?=E5=A4=9A=E4=BC=9A=E8=AF=9D=E5=BB=BA=E8=BF=9E/=E5=8D=B8?= =?UTF-8?q?=E6=B1=A0=E4=B8=8E=E6=81=A2=E5=A4=8D=E8=90=BD=E5=BA=93=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit onStart 先判 isSessionReady 再挂监听,避免重复建连叠 IPC;dispose 期间禁止 dirty 落库并在 teardown 清 timer;grpcOffset 为 0 时跳过 recovery_history;问题队列非空时重置空轮询计数。 --- .../hooks/ChatMultiSessionController.ts | 631 ++++++++++-------- .../hooks/grpcStreamHandler/aiOther.ts | 1 + .../src/pages/ai-re-act/hooks/useChatIPC.ts | 16 +- 3 files changed, 362 insertions(+), 286 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 5e7c03dab9..0452d4c12e 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -2,6 +2,7 @@ import type { AIAgentChatData, AIAgentChatMetaData } from '@/pages/ai-agent/type import { AIInputEventSyncTypeEnum, AISource, + AISourceEnum, type AIAgentGrpcApi, type AIInputEvent, type AIOutputEvent, @@ -37,9 +38,7 @@ import { persistIndependentItem, persistToolResultIfTerminal } from './persist/c const { ipcRenderer } = window.require('electron') -/** recovery_history 单次拉取条数 */ -const RECOVERY_HISTORY_LIMIT = 60 - +/** 检查渲染树(element) 是否存在有效数据 */ const hasSessionRenderTree = (content?: SessionRenderContent): boolean => { if (!content) return false return ( @@ -51,6 +50,7 @@ const hasSessionRenderTree = (content?: SessionRenderContent): boolean => { ) } +// #region 生成初始化数据 /** 生成AI-Agent会话数据实例 */ const genAIAgentChatData = (): AIAgentChatData => { const defaultData: AIAgentChatData = { @@ -131,6 +131,7 @@ const genAIAgentChatData = (): AIAgentChatData => { } return cloneDeep(defaultData) } + /** 生成AI-Agent会话的临时记录数据 */ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { return { @@ -158,10 +159,11 @@ const genAIAgentChatMetaData = (): AIAgentChatMetaData => { casualSubTaskIDs: new Set(), } } +// #endregion +// #region session-source-route-pageId 索引管理相关公共定义和工具方法 /** page 归属键:`${route}::${pageId}`,pageId 为当前归属 */ type PageKey = string - interface SessionOwner { /** 不可变:注册后锁死 */ readonly route: YakitRouteType @@ -197,8 +199,23 @@ const collectTopLevelContentTokens = (content: SessionRenderContent, topCount: n appendFromElements(content.taskElements || []) return [...tokenSet] } +// #endregion export class ChatMultiSessionController { + // #region 常量定义 + /** 渲染树-element debounce 落库 IDB 延迟时间 */ + private static readonly RENDER_PERSIST_DEBOUNCE_MS = 3000 + /** cancel 后等待真实 session-end 的最长时间,超时则合成 end */ + private static readonly SESSION_END_FALLBACK_MS = 5000 + /** 恢复会话时首屏灌入 contents 的顶层条数(两侧列表各自截取) */ + private static readonly INITIAL_CONTENT_TOP_COUNT = 20 + /** recovery_history 单次拉取条数 */ + private static readonly RECOVERY_HISTORY_LIMIT = 60 + /** ping请求探连成功的轮询时间 */ + private static readonly PING_POLLING_INTERVAL = 1000 + // #endregion + + // #region session-source-route-pageId 索引管理相关变量和逻辑 /** * 正向索引:按「当前」page 关页 / 全删 * pageId 换绑后旧 PageKey 不再包含该 session @@ -208,35 +225,7 @@ export class ChatMultiSessionController { * 反向索引:按 sessionId O(1) 定位;换绑时只改 pageId 并搬动正向索引 */ private sessionOwnerMap = new Map() - /** 存放已建立连接的会话session集合 */ - private readyChannels = new Set() - /** 渲染树待落库 dirty 标记 */ - private renderPersistDirty = new Set() - /** 渲染树 debounce 定时器 */ - private renderPersistTimers = new Map>() - /** - * cancel 后等待 session-end 的兜底定时器:超时则手动走 handleSessionEnd(摘监听 + 收尾) - * 避免 end 丢失导致监听泄漏 / onEnd 永不触发 - */ - private sessionEndFallbackTimers = new Map>() - /** - * 待卸池的 session:forceClose 后保留监听与业务池,等 end / 兜底超时再 teardown - * value 为 dispose 时的 deletePersist 标记 - */ - private pendingDisposeSessions = new Map() - - private static readonly RENDER_PERSIST_DEBOUNCE_MS = 3000 - /** cancel 后等待真实 session-end 的最长时间,超时则合成 end */ - private static readonly SESSION_END_FALLBACK_MS = 5000 - /** 恢复会话时首屏灌入 contents 的顶层条数(两侧列表各自截取) */ - private static readonly INITIAL_CONTENT_TOP_COUNT = 20 - /** - * 无 UserQuery 建连进入恢复态:switchLoading 为 true, - * 待 hydrate / recovery_history 结束后再关 - */ - private sessionRestoreLoading = new Set() - // #region session-source-route-pageId 索引管理相关逻辑 /** 将 session 写入 pageSessionMap 正向索引 */ private addToPageSessionMap(owner: SessionOwner, sessionId: string) { const pageKey = makePageKey(owner.route, owner.pageId) @@ -277,7 +266,7 @@ export class ChatMultiSessionController { * - route / source 建立后不可变 * - 若 session 已存在且仅 pageId 不同,走 rebind 而非重复注册 */ - public registerSessionChannel( + private registerSessionChannel( sessionId: string, owner: { route: YakitRouteType; pageId: string; source?: AIStartParams['Source'] }, ) { @@ -286,6 +275,7 @@ export class ChatMultiSessionController { if (existing) { // 禁止改 route / source + // 但是session已经存在,直接启动即可,不需要rebind if (existing.route !== owner.route || existing.source !== source) { console.error(`[ChatMultiSessionController] registerSessionChannel: session 已存在且 route/source 不可变`, { sessionId, @@ -314,28 +304,27 @@ export class ChatMultiSessionController { } /** - * 同 route 下换绑 pageId:更新 sessionOwnerMap.pageId,从旧 PageKey 摘除、写入新 PageKey - * route / source 不变;newPageId 与旧相同或 session 已 dispose 则 no-op + * 全删:非空 sessionIds 直接用集合;否则按 route + pageId 收集 sources 数组中每个 source 的 session + * @param sources 指定要删除的来源集合(agent 页面下可能同时含 ai 与 im,支持精确删其一或全部) */ - public rebindSessionPageId(sessionId: string, newPageId: string) { - const owner = this.sessionOwnerMap.get(sessionId) - if (!owner || owner.pageId === newPageId) return - - this.removeFromPageSessionMap(owner, sessionId) - owner.pageId = newPageId - this.addToPageSessionMap(owner, sessionId) - } - - /** 全删:用目标 route + pageId + source 查当前索引;非空 sessionIds 则直接用集合 */ private resolveSessionIds(params: { sessionIds?: string[] - source: AISource + sources: AISource[] route: YakitRouteType pageId: string }): string[] { - const { sessionIds, source, route, pageId } = params + const { sessionIds, sources, route, pageId } = params if (sessionIds?.length) return [...sessionIds] - return [...(this.pageSessionMap.get(makePageKey(route, pageId))?.get(source) ?? [])] + const sourceMap = this.pageSessionMap.get(makePageKey(route, pageId)) + if (!sourceMap) return [] + const ids: string[] = [] + for (const source of sources) { + const sessionSet = sourceMap.get(source) + if (sessionSet) { + for (const id of sessionSet) ids.push(id) + } + } + return ids } /** 该 PageKey 下所有 source 的 session 并集 */ @@ -352,98 +341,84 @@ export class ChatMultiSessionController { } /** - * 删除内存数据(仅 deleteSessions / onPageUnload 调用) - * 1. 标记 pendingDispose + forceClose(cancel,保留 IPC 等 end) - * 2. 真实 session-end 或 5s 兜底后,再摘监听并清业务池与归属索引 - * @param deletePersist 是否同步删除 IDB。页面销毁只卸内存时应为 false,并先 flush 渲染树;显式删会话时为 true。 + * 同 route 下换绑 pageId:更新 sessionOwnerMap.pageId,从旧 PageKey 摘除、写入新 PageKey + * route / source 不变;newPageId 与旧相同或 session 已 dispose 则 no-op */ - private disposeSessionMemory(sessionId: string, deletePersist = false) { - if (deletePersist) { - // 显式删除:取消待写 debounce 即可,无需再刷进 IDB - const timer = this.renderPersistTimers.get(sessionId) - if (timer) { - clearTimeout(timer) - this.renderPersistTimers.delete(sessionId) - } - this.renderPersistDirty.delete(sessionId) - } else { - // 页面卸载:卸内存前先把渲染树刷进 IDB,保留可恢复数据 - this.flushSessionRender(sessionId) - } - - this.pendingDisposeSessions.set(sessionId, deletePersist) - this.forceCloseSession({ sessionIds: [sessionId] }) - } - - /** end / 兜底超时后:摘池与归属索引(可选删 IDB) */ - private teardownDisposedSession(sessionId: string, deletePersist: boolean) { - this.readyChannels.delete(sessionId) - - if (this.activeShowSession === sessionId) { - this.activeShowSession = null - } - - this.requestPool.delete(sessionId) - this.storePool.delete(sessionId) - this.rawDataPool.delete(sessionId) - this.metaPool.delete(sessionId) - + public rebindSessionPageId(sessionId: string, newPageId: string) { const owner = this.sessionOwnerMap.get(sessionId) - if (owner) { - this.removeFromPageSessionMap(owner, sessionId) - this.sessionOwnerMap.delete(sessionId) - } + if (!owner || owner.pageId === newPageId) return - if (deletePersist) { - aiChatPersistStore.deleteSessionPersist(sessionId).catch(() => {}) - } + this.removeFromPageSessionMap(owner, sessionId) + owner.pageId = newPageId + this.addToPageSessionMap(owner, sessionId) } // #endregion - // #region 渲染树 dirty debounce 落库 - /** 渲染树结构变更后标记 dirty,3s 无新变更再写入 sessionRender */ - private markSessionRenderDirty(sessionId: string) { - this.renderPersistDirty.add(sessionId) - const prev = this.renderPersistTimers.get(sessionId) - if (prev) clearTimeout(prev) - const timer = setTimeout(() => { - // 到期后统一走 flush:摘 timer、清 dirty、写 IDB(外部强制 flush 也走同一套) - this.flushSessionRender(sessionId) - }, ChatMultiSessionController.RENDER_PERSIST_DEBOUNCE_MS) - this.renderPersistTimers.set(sessionId, timer) + /** 存放已建立连接的会话session集合 */ + private readyChannels = new Set() + /** 会话是否仍占坑(已 start 且尚未 end,含 cancel 等待 end 的窗口) */ + public isSessionReady(sessionId: string) { + return this.readyChannels.has(sessionId) } + /** + * 待卸池的 session:forceClose 后保留监听与业务池,等 end / 兜底超时再 teardown + * value 为 dispose 时的 deletePersist 标记 + */ + private pendingDisposeSessions = new Map() - /** 立即刷写渲染树快照并清除 dirty/timer */ - private flushSessionRender(sessionId: string) { - const timer = this.renderPersistTimers.get(sessionId) - if (timer) { - clearTimeout(timer) - this.renderPersistTimers.delete(sessionId) + private requestPool = new Map() + private storePool = new Map>() + private rawDataPool = new Map() + private metaPool = new Map() + /** 获取对应会话的所有数据集 */ + public ensureSession(sessionId: string) { + if (!this.storePool.has(sessionId)) { + this.storePool.set( + sessionId, + createChatStore({ + onRenderStructureChange: () => this.markSessionRenderDirty(sessionId), + }), + ) + this.rawDataPool.set(sessionId, genAIAgentChatData()) + this.requestPool.set(sessionId, cloneDeep(AIAgentSettingDefault)) + this.metaPool.set(sessionId, genAIAgentChatMetaData()) } - this.renderPersistDirty.delete(sessionId) - - const store = this.storePool.get(sessionId) - const rawData = this.rawDataPool.get(sessionId) - if (!store || !rawData) return - const state = store.getState() - const content: SessionRenderContent = { - items: { ...state.items }, - groups: { ...state.groups }, - tasks: { ...state.tasks }, - casualElements: [...state.casualChat.elements], - taskElements: [...state.taskChat.elements], + return { + request: this.requestPool.get(sessionId)!, + store: this.storePool.get(sessionId)!, + rawData: this.rawDataPool.get(sessionId)!, + meta: this.metaPool.get(sessionId)!, } - void this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) } - // #endregion + /** + * 更新指定会话的配置参数 + * + * Source 字段连接会话时锁死,后续不允许热更新 + */ + public updateSessionConfig(sessionId: string, config: Partial>) { + const { request } = this.ensureSession(sessionId) + delete config['Source'] + Object.assign(request, config) + } + + /** TODO - 存放当前正在展示的会话session */ + private activeShowSession: string | null = null + /** 设置当前展示的会话 Session */ + public setActiveShowSession(sessionId: string) { + this.activeShowSession = sessionId + } + /** 判断指定会话是否当前正在展示 */ + public isActiveShowSession(sessionId: string) { + return this.activeShowSession === sessionId + } // #region IndexedDB 持久化门面(薄封装 aiChatPersistStore,错误兜底不抛穿 UI) /** 从 sessionOwnerMap 取 source,兜底 'ai' */ private resolvePersistSource(sessionId: string): AISource { - return this.sessionOwnerMap.get(sessionId)?.source || 'ai' + return this.sessionOwnerMap.get(sessionId)?.source || AISourceEnum.aiAgent } - /** 保存会话渲染树和grpcOffset */ + /** 保存会话渲染树(element)和grpcOffset */ private async persistSetSessionRender(sessionId: string, content: SessionRenderContent, grpcOffset?: number) { try { const offset = grpcOffset ?? this.rawDataPool.get(sessionId)?.grpcOffset ?? 0 @@ -497,7 +472,7 @@ export class ChatMultiSessionController { } /** - * 将渲染树写入 zustand,并批量灌回首屏 contents。 + * 将渲染树写入 chatStore,并批量灌回首屏 contents。 * @param content 优先用 start 时暂存的树;未传则再读 IDB 整行 * * 注意:必须先把 contents 灌进 Map,再 hydrate 渲染树。 @@ -536,67 +511,126 @@ export class ChatMultiSessionController { } // #endregion - /** 存放当前正在展示的会话session */ - private activeShowSession: string | null = null - /** 设置当前展示的会话 Session */ - public setActiveShowSession(sessionId: string) { - this.activeShowSession = sessionId + // #region 渲染树-element debounce 落库 IDB + /** 渲染树(element相关数据) debounce 定时器 */ + private renderPersistTimers = new Map>() + + /** 取消该 session 的渲染树 debounce(不写 IDB) */ + private clearSessionRenderPersistTimer(sessionId: string) { + const timer = this.renderPersistTimers.get(sessionId) + if (timer) { + clearTimeout(timer) + this.renderPersistTimers.delete(sessionId) + } } - /** 判断指定会话是否当前正在展示 */ - public isActiveShowSession(sessionId: string) { - return this.activeShowSession === sessionId + + /** chatStore.dispatchStreamingNode触发后防抖3s,无新变更再写入 sessionRender */ + private markSessionRenderDirty(sessionId: string) { + // 已进入卸池流程:禁止再入内存,避免覆盖 dispose 时已 flush 的 IDB + if (this.pendingDisposeSessions.has(sessionId)) return + + this.clearSessionRenderPersistTimer(sessionId) + const timer = setTimeout(() => { + // 到期后统一走 flush:摘 timer、清 dirty、写 IDB(外部强制 flush 也走同一套) + this.flushSessionRender(sessionId) + }, ChatMultiSessionController.RENDER_PERSIST_DEBOUNCE_MS) + this.renderPersistTimers.set(sessionId, timer) } - private requestPool = new Map() - private storePool = new Map>() - private rawDataPool = new Map() - private metaPool = new Map() + /** 立即刷写渲染树快照并清除 dirty/timer */ + private flushSessionRender(sessionId: string) { + this.clearSessionRenderPersistTimer(sessionId) - /** 获取对应会话的所有数据集 */ - public ensureSession(sessionId: string) { - if (!this.storePool.has(sessionId)) { - this.storePool.set( - sessionId, - createChatStore({ - onRenderStructureChange: () => this.markSessionRenderDirty(sessionId), - }), - ) - this.rawDataPool.set(sessionId, genAIAgentChatData()) - this.requestPool.set(sessionId, cloneDeep(AIAgentSettingDefault)) - this.metaPool.set(sessionId, genAIAgentChatMetaData()) - } - return { - request: this.requestPool.get(sessionId)!, - store: this.storePool.get(sessionId)!, - rawData: this.rawDataPool.get(sessionId)!, - meta: this.metaPool.get(sessionId)!, + const store = this.storePool.get(sessionId) + const rawData = this.rawDataPool.get(sessionId) + if (!store || !rawData) return + const state = store.getState() + const content: SessionRenderContent = { + items: { ...state.items }, + groups: { ...state.groups }, + tasks: { ...state.tasks }, + casualElements: [...state.casualChat.elements], + taskElements: [...state.taskChat.elements], } + void this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) } + // #endregion /** - * 更新指定会话的配置参数 - * - * Source 字段连接会话时锁死,后续不允许热更新 + * 无 UserQuery 建连进入恢复态:switchLoading 为 true, + * 待 hydrate / recovery_history 结束后再关 */ - public updateSessionConfig(sessionId: string, config: Partial>) { - const { request } = this.ensureSession(sessionId) - delete config['Source'] - Object.assign(request, config) + private sessionRestoreLoading = new Set() + + /** + * cancel 后等待 session-end 的兜底定时器:超时则手动走 handleSessionEnd(摘监听 + 收尾) + * 避免 end 丢失导致监听泄漏 / onEnd 永不触发 + */ + private sessionEndFallbackTimers = new Map>() + /** 取消已有的 session-end 兜底定时器 */ + private clearSessionEndFallback(sessionId: string) { + const timer = this.sessionEndFallbackTimers.get(sessionId) + if (!timer) return + clearTimeout(timer) + this.sessionEndFallbackTimers.delete(sessionId) + } + + /** + * 建立会话时,获取grpc库中最新数据ID和IDB里的数据 + * 对齐offset,设置出最新的grpcOffset数据 + * 将IDB里渲染树(element)暂存, 供 pong 后 hydrate。 + * 不依赖会话 gRPC 已连通。 + */ + private async prepareSessionPersistBeforeStart(sessionId: string) { + const { rawData, meta } = this.ensureSession(sessionId) + + try { + const [eventRes, row] = await Promise.all([ + grpcQueryAIEvent( + { + Filter: { SessionID: sessionId }, + Pagination: { Page: 1, Limit: 1, OrderBy: 'created_at', Order: 'desc' }, + }, + true, + ).catch(() => ({ Events: [] as AIOutputEvent[] })), + this.persistGetSessionRender(sessionId), + ]) + + const latestId = eventRes?.Events?.[0]?.ID ?? 0 + const final = row?.grpcOffset !== undefined && row.grpcOffset !== null ? row.grpcOffset : latestId + rawData.grpcOffset = final + + if (hasSessionRenderTree(row?.content)) { + meta.pendingSessionRender = row!.content + } else { + meta.pendingSessionRender = undefined + } + } catch { + rawData.grpcOffset = rawData.grpcOffset || 0 + meta.pendingSessionRender = undefined + } } /** * 建立指定 session 连接(新会话首问 / 打开历史 / 无问侧重连 共用)。 + * 调用方应先用 isSessionReady 判重并挂好 IPC 监听,再调本方法(prepare 异步,invoke 晚于监听)。 * - 有 UserQuery:立刻上屏首问,pong 后发问;无树时不强制 recovery_history * - 无 UserQuery:视为恢复态,置 switchLoading,pong 后 hydrate 或发 recovery_history + * @returns 是否真正发起了建连 */ - public handleStartSession(requestParams: AIChatIPCStartParams, cb?: (sessionId: string) => void) { + public handleStartSession(requestParams: AIChatIPCStartParams, cb?: (sessionId: string) => void): boolean { const { token: sessionId, params, route, pageId } = requestParams - const isExec = this.readyChannels.has(sessionId) - if (isExec) { + if (this.readyChannels.has(sessionId)) { yakitNotify('warning', '会话已经存在,请勿重复建立!') - return + return false } + this.registerSessionChannel(sessionId, { + route, + pageId, + source: params.Params?.Source, + }) + const { request, store, rawData, meta } = this.ensureSession(sessionId) const userQuery = (params.Params?.UserQuery || '').trim() @@ -608,11 +642,6 @@ export class ChatMultiSessionController { this.sessionRestoreLoading.add(sessionId) } - this.registerSessionChannel(sessionId, { - route, - pageId, - source: params.Params?.Source, - }) this.setActiveShowSession(sessionId) Object.assign(request, params.Params) @@ -676,100 +705,9 @@ export class ChatMultiSessionController { SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_PING, SyncID: meta.pingSyncID, }) - }, 5000) + }, ChatMultiSessionController.PING_POLLING_INTERVAL) }) - } - - /** - * start 时立刻对齐 grpcOffset,并暂存 IDB 渲染树供 pong 后 hydrate。 - * 不依赖会话 gRPC 已连通。 - */ - private async prepareSessionPersistBeforeStart(sessionId: string) { - const { rawData, meta } = this.ensureSession(sessionId) - - try { - const [eventRes, row] = await Promise.all([ - grpcQueryAIEvent( - { - Filter: { SessionID: sessionId }, - Pagination: { Page: 1, Limit: 1, OrderBy: 'created_at', Order: 'desc' }, - }, - true, - ).catch(() => ({ Events: [] as AIOutputEvent[] })), - this.persistGetSessionRender(sessionId), - ]) - - const latestId = eventRes?.Events?.[0]?.ID ?? 0 - const final = row?.grpcOffset !== undefined && row.grpcOffset !== null ? row.grpcOffset : latestId - rawData.grpcOffset = final - - if (hasSessionRenderTree(row?.content)) { - meta.pendingSessionRender = row!.content - } else { - meta.pendingSessionRender = undefined - } - } catch { - rawData.grpcOffset = rawData.grpcOffset || 0 - meta.pendingSessionRender = undefined - } - } - - /** 关闭恢复态 loading(hydrate 完成或 recovery 结束时调用) */ - private finishSessionRestoreLoading(sessionId: string) { - if (!this.sessionRestoreLoading.has(sessionId)) return - this.sessionRestoreLoading.delete(sessionId) - const store = this.storePool.get(sessionId) - store?.getState().updateState({ switchLoading: false }) - } - - /** - * pong 后:消费暂存树或发 recovery_history。 - * @param needRecoveryHistory 无首问建连(恢复态)时为 true:空树则向后端拉历史 - */ - private async restoreSessionAfterPong(sessionId: string, needRecoveryHistory: boolean) { - const { store, rawData, meta } = this.ensureSession(sessionId) - const pending = meta.pendingSessionRender - meta.pendingSessionRender = undefined - - try { - if (hasSessionRenderTree(pending)) { - await this.loadSessionRenderToMemory(sessionId, pending) - const state = store.getState() - const content: SessionRenderContent = { - items: { ...state.items }, - groups: { ...state.groups }, - tasks: { ...state.tasks }, - casualElements: [...state.casualChat.elements], - taskElements: [...state.taskChat.elements], - } - await this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) - this.finishSessionRestoreLoading(sessionId) - } else if (needRecoveryHistory) { - // 保持 switchLoading,等 recovery_history 再关,避免 UI 提前可点 - this.requestMessage(sessionId, { - IsSyncMessage: true, - SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_HISTORY, - SyncJsonInput: JSON.stringify({ - start_id: rawData.grpcOffset, - limit: RECOVERY_HISTORY_LIMIT, - }), - }) - } else { - // 带首问的新会话:用当前 store 快照(可能已有首问)+ offset - const state = store.getState() - const content: SessionRenderContent = { - items: { ...state.items }, - groups: { ...state.groups }, - tasks: { ...state.tasks }, - casualElements: [...state.casualChat.elements], - taskElements: [...state.taskChat.elements], - } - await this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) - this.finishSessionRestoreLoading(sessionId) - } - } catch { - this.finishSessionRestoreLoading(sessionId) - } + return true } /** 主动向grpc发送请求 */ @@ -786,9 +724,10 @@ export class ChatMultiSessionController { const { store, rawData, meta } = this.ensureSession(token) if (params.IsFreeInput) { - const { casualLoading, currentCasualTaskID } = store.getState() + const { casualLoading, currentCasualTaskID, taskStatus } = store.getState() // 如果自由对话引起了任务规划,那么自由对话其实是空闲状态 - const isCasualIdle = casualLoading && currentCasualTaskID === store.getState().taskStatus.taskID + const isCasualIdle = + casualLoading && currentCasualTaskID && taskStatus.taskID && currentCasualTaskID === taskStatus.taskID if (!casualLoading || isCasualIdle) { // 自由对话没有问题进行中时,才改变loading的title @@ -948,7 +887,9 @@ export class ChatMultiSessionController { } this.requestMessage(token, params) - } catch (error) {} + } catch (error) { + console.error('handleSendMessage error', error) + } } /** 向连接中的会话发送请求 */ private requestMessage(sessionId: string, request: AIInputEvent) { @@ -980,14 +921,95 @@ export class ChatMultiSessionController { store.getState().updateState({ taskStatus: cloneDeep(DefaultTaskPlanStatus) }) } + /** 关闭恢复态 loading(hydrate 完成或 recovery 结束时调用) */ + private finishSessionRestoreLoading(sessionId: string) { + if (!this.sessionRestoreLoading.has(sessionId)) return + this.sessionRestoreLoading.delete(sessionId) + const store = this.storePool.get(sessionId) + store?.getState().updateState({ switchLoading: false }) + } + + /** + * pong 后:消费暂存树或发 recovery_history。 + * @param needRecoveryHistory 无首问建连(恢复态)时为 true:空树则向后端拉历史 + */ + private async restoreSessionAfterPong(sessionId: string, needRecoveryHistory: boolean) { + const { store, rawData, meta } = this.ensureSession(sessionId) + const pending = meta.pendingSessionRender + meta.pendingSessionRender = undefined + + try { + if (hasSessionRenderTree(pending)) { + await this.loadSessionRenderToMemory(sessionId, pending) + const state = store.getState() + const content: SessionRenderContent = { + items: { ...state.items }, + groups: { ...state.groups }, + tasks: { ...state.tasks }, + casualElements: [...state.casualChat.elements], + taskElements: [...state.taskChat.elements], + } + await this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) + this.finishSessionRestoreLoading(sessionId) + } else if (needRecoveryHistory) { + // grpcOffset 为 0:无历史游标可续,不发 recovery_history + if (!rawData.grpcOffset) { + const state = store.getState() + const content: SessionRenderContent = { + items: { ...state.items }, + groups: { ...state.groups }, + tasks: { ...state.tasks }, + casualElements: [...state.casualChat.elements], + taskElements: [...state.taskChat.elements], + } + await this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) + this.finishSessionRestoreLoading(sessionId) + } else { + // 保持 switchLoading,等 recovery_history 再关,避免 UI 提前可点 + this.requestMessage(sessionId, { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_HISTORY, + SyncJsonInput: JSON.stringify({ + start_id: rawData.grpcOffset, + limit: ChatMultiSessionController.RECOVERY_HISTORY_LIMIT, + }), + }) + } + } else { + // 带首问的新会话:用当前 store 快照(可能已有首问)+ offset + const state = store.getState() + const content: SessionRenderContent = { + items: { ...state.items }, + groups: { ...state.groups }, + tasks: { ...state.tasks }, + casualElements: [...state.casualChat.elements], + taskElements: [...state.taskChat.elements], + } + await this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) + this.finishSessionRestoreLoading(sessionId) + } + } catch { + this.finishSessionRestoreLoading(sessionId) + } + } + /** 💥 核心替换:接管原 useChatIPC 里的巨型数据分发逻辑! */ public handleGrpcOutputEvent(sessionId: string, res: AIOutputEvent) { try { + if (!this.readyChannels.has(sessionId)) return + let ipcContent = Uint8ArrayToString(res.Content) || '' + const parsed = JSON.parse(ipcContent) // console.log('handleGrpcOutputEvent--', sessionId, res, ipcContent) - if (res.Type === 'structured' && ipcContent.indexOf('level') > -1 && ipcContent.indexOf('message') > -1) { + if ( + res.Type === 'structured' && + parsed && + typeof parsed === 'object' && + 'level' in parsed && + 'message' in parsed + ) { // 日志类型数据 - const data = JSON.parse(ipcContent) as AIAgentGrpcApi.Log + const data = parsed as AIAgentGrpcApi.Log aiAgentLogEmitter.dispatch({ session: sessionId, type: 'log', @@ -1213,6 +1235,54 @@ export class ChatMultiSessionController { persistToolResultIfTerminal(sessionId, chatDetail) } + /** + * 删除内存数据(仅 deleteSessions / onPageUnload 调用) + * 1. 标记 pendingDispose + forceClose(cancel,保留 IPC 等 end) + * 2. 真实 session-end 或 5s 兜底后,再摘监听并清业务池与归属索引 + * @param deletePersist 是否同步删除 IDB。页面销毁只卸内存时应为 false,并先 flush 渲染树;显式删会话时为 true。 + */ + private disposeSessionMemory(sessionId: string, deletePersist = false) { + if (deletePersist) { + // 显式删除:取消待写 debounce 即可,无需再刷进 IDB + this.clearSessionRenderPersistTimer(sessionId) + } else { + // 页面卸载:卸内存前先把渲染树刷进 IDB,保留可恢复数据 + this.flushSessionRender(sessionId) + } + + // 先标记 pending,再 forceClose:关停窗口内迟到的结构变更不再 arm dirty + this.pendingDisposeSessions.set(sessionId, deletePersist) + this.forceCloseSession({ sessionIds: [sessionId] }) + } + + /** end / 兜底超时后:摘池与归属索引(可选删 IDB) */ + private teardownDisposedSession(sessionId: string, deletePersist: boolean) { + // 卸池前清 debounce,避免空 ensureSession 后迟到 timer 把 IDB 盖成空树 + this.clearSessionRenderPersistTimer(sessionId) + this.sessionRestoreLoading.delete(sessionId) + + this.readyChannels.delete(sessionId) + + if (this.activeShowSession === sessionId) { + this.activeShowSession = null + } + + this.requestPool.delete(sessionId) + this.storePool.delete(sessionId) + this.rawDataPool.delete(sessionId) + this.metaPool.delete(sessionId) + + const owner = this.sessionOwnerMap.get(sessionId) + if (owner) { + this.removeFromPageSessionMap(owner, sessionId) + this.sessionOwnerMap.delete(sessionId) + } + + if (deletePersist) { + aiChatPersistStore.deleteSessionPersist(sessionId).catch(() => {}) + } + } + /** 关闭会话的所有定时器 */ private closeSessionTimers(meta: ReturnType['meta']) { // 取消ping请求相关逻辑 @@ -1241,14 +1311,6 @@ export class ChatMultiSessionController { ipcRenderer.removeAllListeners(`${sessionId}-error`) } - /** 取消已有的 session-end 兜底定时器 */ - private clearSessionEndFallback(sessionId: string) { - const timer = this.sessionEndFallbackTimers.get(sessionId) - if (!timer) return - clearTimeout(timer) - this.sessionEndFallbackTimers.delete(sessionId) - } - /** * cancel 后武装 5s 兜底:若真实 -end 未到,手动走 handleSessionEnd * 重复 cancel 会重置计时 @@ -1266,10 +1328,14 @@ export class ChatMultiSessionController { public handleSessionError(sessionId: string, error: any) { // 暂无业务逻辑处理 } + // 监听 session-end 事件(含 cancel 后 5s 兜底合成) public handleSessionEnd(sessionId: string, res?: any) { this.clearSessionEndFallback(sessionId) + // 先取出 onEnd:须在 teardown 之后再调,避免回调里重启时池已被卸掉 / 仍占坑 + let onEnd: (() => void) | undefined + // 池仍在:走完整收尾;若已 teardown 则只保证摘监听 if (this.storePool.has(sessionId)) { const data = this.ensureSession(sessionId) @@ -1285,11 +1351,8 @@ export class ChatMultiSessionController { }) this.readyChannels.delete(sessionId) - const onEnd = meta.onEnd - if (onEnd) { - onEnd() - meta.onEnd = undefined - } + onEnd = meta.onEnd + meta.onEnd = undefined } this.closeIPCListeners(sessionId) @@ -1299,6 +1362,8 @@ export class ChatMultiSessionController { this.pendingDisposeSessions.delete(sessionId) this.teardownDisposedSession(sessionId, pendingDeletePersist) } + + onEnd?.() } /** @@ -1317,7 +1382,7 @@ export class ChatMultiSessionController { ipcRenderer.invoke('cancel-ai-re-act', session).catch(() => {}) const store = this.storePool.get(session) if (store) { - store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话已停止' }) + store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话关闭中...' }) } if (meta) this.closeSessionTimers(meta) // 等真实 -end;超时则手动 handleSessionEnd,避免监听泄漏 / onEnd 挂死 @@ -1333,7 +1398,7 @@ export class ChatMultiSessionController { * - 内部会先 forceClose 再卸业务池与双索引,并同步删除该会话 IDB 三表 * - grpc 删除由上层负责 */ - public deleteSessions(params: { sessionIds: string[]; source: AISource; route: YakitRouteType; pageId: string }) { + public deleteSessions(params: { sessionIds: string[]; sources: AISource[]; route: YakitRouteType; pageId: string }) { const ids = this.resolveSessionIds(params) for (const sessionId of ids) { this.disposeSessionMemory(sessionId, true) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index 11db72cf10..e6c8fec267 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -328,6 +328,7 @@ const handleQueueInfo: AIMessageHandler = (request) => { const { tasks, total_tasks } = JSON.parse(ipcContent) as AIAgentGrpcApi.QuestionQueues // 记录最新问题队列的数量,4次为空,则关闭轮询器 if (tasks.length === 0) meta.queuePollingEmptyCount += 1 + else meta.queuePollingEmptyCount = 0 if (meta.queuePollingEmptyCount > 3 && meta.queuePollingTimer) { clearTimeout(meta.queuePollingTimer) meta.queuePollingTimer = null diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts index 2a2368dd76..30ba6b9b32 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useChatIPC.ts @@ -5,13 +5,23 @@ import type { AIChatSendParams } from './type' import { useMemoizedFn } from 'ahooks' import type { UseChatIPCStartParams } from '@/pages/ai-agent/useContext/AIAgentContext' import type { YakitRouteType } from '@/enums/yakitRoute' +import { yakitNotify } from '@/utils/notification' const { ipcRenderer } = window.require('electron') export function useChatIPC(route: YakitRouteType, pageId: string) { - // IPC 监听的 channel 用入参 token 注册,与 handleStartSession 的 token 完全对齐 - // 这样在欢迎页"从无到有建立会话"时,不会出现 setActiveChat 异步未生效、闭包 sessionId 仍为空导致监听错位的问题 + /** + * isSessionReady 已连则直接返回(不动已有监听)→ 用入参 token 挂监听 → handleStartSession + * prepare 异步,invoke 晚于本同步栈挂监听,不会丢流;token 不依赖 React 闭包里的 SessionID + */ const onStart = useMemoizedFn(({ token, params, onSuccess }: UseChatIPCStartParams) => { - // 监听网络,直接丢给大脑处理 + if (globalSessionEngine.isSessionReady(token)) { + yakitNotify('warning', '会话已经存在,请勿重复建立!') + return + } + + ipcRenderer.removeAllListeners(`${token}-data`) + ipcRenderer.removeAllListeners(`${token}-error`) + ipcRenderer.removeAllListeners(`${token}-end`) ipcRenderer.on(`${token}-data`, (e, res: any) => { globalSessionEngine.handleGrpcOutputEvent(token, res) }) From f8d56d94ccf61e219bcc1a50d66df773bfcb8ba4 Mon Sep 17 00:00:00 2001 From: hubo Date: Wed, 29 Jul 2026 10:47:32 +0800 Subject: [PATCH 062/101] =?UTF-8?q?feat:=20=E4=B8=8A=E6=BB=91=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=8E=86=E5=8F=B2=20+=20=E8=A7=86=E7=AA=97=E5=86=85?= =?UTF-8?q?=E5=AD=98=E5=9B=9E=E6=94=B6=20+=20gRPC=20=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 实现 casual/task 侧上滑加载更旧历史,由 rangeChanged 统一驱动加载+淘汰: - 新增 useLoadOlder hook:视窗内缺正文从 IDB 补灌,方向侧预取, 触顶且 grpcOffset>0 时发 recovery_history 拉更旧事件,视窗外 contents 淘汰 - Controller: persistGetSessionContents 改 public、新增 public requestRecoveryHistory - chatStore: 填充 dispatchStreamingNode 的 isHistory 前插分支(修复历史事件双插) - 修复 expandTokens/collectTopLevelContentTokens 两层展开(task→group→item 嵌套) - StaticChatContent: contents 缺失时返回占位 div 减少滚动条抖动 - 两个组件接线 useLoadOlder,去掉 useLoadHistory prepend 机制 --- .../chatTemplate/AIAgentChatTemplate.tsx | 43 ++-- .../AIReActChatContents.tsx | 44 ++-- .../hooks/ChatMultiSessionController.ts | 33 ++- .../src/pages/ai-re-act/hooks/chatStore.ts | 10 +- .../src/pages/ai-re-act/hooks/useLoadOlder.ts | 227 ++++++++++++++++++ 5 files changed, 294 insertions(+), 63 deletions(-) create mode 100644 app/renderer/src/main/src/pages/ai-re-act/hooks/useLoadOlder.ts diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx index e7d2b840e5..a758afbb08 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx @@ -28,6 +28,7 @@ import { HistoryTaskTree } from './historyTaskTree/HistoryTaskTree' import { AIReviewParams } from '../components/aiReviewResult/AIReviewResult' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import useLoadOlder from '@/pages/ai-re-act/hooks/useLoadOlder' import { useStore } from 'zustand' import useAIAgentDispatcher from '../useContext/useDispatcher' import { randomString } from '@/utils/randomUtil' @@ -183,8 +184,8 @@ export const AIAgentChatStream: React.FC = memo((props) const streams = useStore(store, (state) => state.taskChat.elements) - /** TODO - hooks未写 */ - // const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents + const { onRangeChange } = useLoadOlder(TYPE) + useUpdateEffect(() => { scrollToIndex('LAST') }, [scrollToBottom]) @@ -203,15 +204,6 @@ export const AIAgentChatStream: React.FC = memo((props) } }, [highlightedItem]) - // 向上滚动加载 - // const { firstItemIndex, handleLoadMore, isPrependingRef } = useLoadHistory({ - // loading: taskLoadMoreLoading, - // dataLength: streams.length, - // SessionID: session, - // fetchHasMore: () => handleHasMoreHistory(TYPE), - // loadMore: () => handleLoadMoreHistory(TYPE), - // }) - const { virtuosoRef, setIsAtBottomRef, @@ -221,7 +213,6 @@ export const AIAgentChatStream: React.FC = memo((props) handleTotalListHeightChanged, } = useVirtuosoAutoScroll({ total: streams.length, - // isPrependingRef }) const { locateToIndex } = useChatStreamLocateHighlight({ @@ -233,7 +224,7 @@ export const AIAgentChatStream: React.FC = memo((props) scrollToIndex('LAST') }, [scrollToBottom]) - const renderItem = useCallback((index: number, stream: ReActChatRenderElement) => { + const renderItem = useCallback((_: number, stream: ReActChatRenderElement) => { if (!stream.token) return null return }, []) @@ -247,22 +238,22 @@ export const AIAgentChatStream: React.FC = memo((props) ) const Footer = useCallback(() => , []) - const Header = useCallback( - () => - taskLoadMoreLoading ? ( -
- -
- ) : null, - [taskLoadMoreLoading], - ) + // const Header = useCallback( + // () => + // taskLoadMoreLoading ? ( + //
+ // + //
+ // ) : null, + // [taskLoadMoreLoading], + // ) const components = useMemo( () => ({ Item, Footer, - Header, + // Header, }), - [Footer, Header, Item], + [Footer, Item], ) const onTreeLocate = useMemoizedFn((id?: string) => { if (!id) return @@ -282,7 +273,7 @@ export const AIAgentChatStream: React.FC = memo((props) ref={virtuosoRef} key={session} scrollerRef={setScrollerRef} - // firstItemIndex={firstItemIndex} + defaultItemHeight={120} atBottomStateChange={setIsAtBottomRef} style={{ height: '100%', width: '100%' }} data={streams} @@ -292,7 +283,7 @@ export const AIAgentChatStream: React.FC = memo((props) atBottomThreshold={100} initialTopMostItemIndex={streams.length > 1 ? streams.length - 1 : 0} skipAnimationFrameInResizeObserver - // startReached={handleLoadMore} + rangeChanged={onRangeChange} components={components} />
diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx index 3206722b29..236cef07c6 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChatContents/AIReActChatContents.tsx @@ -27,6 +27,7 @@ import useMemoizedFn from 'ahooks/lib/useMemoizedFn' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' import { globalSessionEngine } from '../hooks/ChatMultiSessionController' +import useLoadOlder from '../hooks/useLoadOlder' import { Code } from '@/pages/ai-agent/components/aiGroupStreamCard/AIGroupStreamCard' export const AIStreamNode: React.FC = React.memo((props) => { @@ -104,7 +105,7 @@ export const AIStreamNode: React.FC = React.memo((props) => { const TYPE = 'reAct' export const AIReActChatContents: React.FC = React.memo( - forwardRef((props, ref) => { + forwardRef((_, ref) => { const listRootRef = useRef(null) const { activeChat } = useAIAgentStore() @@ -113,23 +114,13 @@ export const AIReActChatContents: React.FC = React.me const chatLength = useStore(store, (state) => state.casualChat.elements.length) const casualTitle = useStore(store, (state) => state.casualTitle) const execute = useStore(store, (state) => state.execute) - const casualLoadMoreLoading = useStore(store, (state) => state.requestHistoryState.casualLoadMoreLoading) + // const casualLoadMoreLoading = useStore(store, (state) => state.requestHistoryState.casualLoadMoreLoading) - /** TODO - 新版中历史逻辑暂未补充 */ - // const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents + const { onRangeChange } = useLoadOlder(TYPE) - // 向上滚动加载 - // const { firstItemIndex, handleLoadMore, isPrependingRef } = useLoadHistory({ - // loading: casualLoadMoreLoading, - // dataLength: chatLength, - // SessionID: activeChat?.SessionID || '', - // fetchHasMore: () => handleHasMoreHistory(TYPE), - // loadMore: () => handleLoadMoreHistory(TYPE), - // }) const { virtuosoRef, setScrollerRef, setIsAtBottomRef, handleTotalListHeightChanged, scrollToItemIndex } = useVirtuosoAutoScroll({ total: chatLength, - // isPrependingRef, }) const { locateToIndex } = useChatStreamLocateHighlight({ @@ -177,22 +168,22 @@ export const AIReActChatContents: React.FC = React.me
当前会话已停止
) : null }, [casualTitle, execute, chatLength]) - const Header = useCallback( - () => - casualLoadMoreLoading ? ( -
- -
- ) : null, - [casualLoadMoreLoading], - ) + // const Header = useCallback( + // () => + // casualLoadMoreLoading ? ( + //
+ // + //
+ // ) : null, + // [casualLoadMoreLoading], + // ) const components = useMemo( () => ({ Item, Footer, - Header, + // Header, }), - [Footer, Header, Item], + [Footer, Item], ) // const rawData = useCurrentRawData() // console.log('casualChat.elements', casualChatElements, store.getState().items) @@ -202,16 +193,17 @@ export const AIReActChatContents: React.FC = React.me key={activeChat?.SessionID} ref={virtuosoRef} scrollerRef={setScrollerRef} - // firstItemIndex={firstItemIndex} + defaultItemHeight={120} atBottomStateChange={setIsAtBottomRef} data={casualChatElements} totalListHeightChanged={handleTotalListHeightChanged} itemContent={renderItem} initialTopMostItemIndex={chatLength > 1 ? chatLength - 1 : 0} components={components} + increaseViewportBy={{ top: 1200, bottom: 0 }} atBottomThreshold={50} skipAnimationFrameInResizeObserver - // startReached={handleLoadMore} + rangeChanged={onRangeChange} className={styles['re-act-contents-list']} />
diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 0452d4c12e..9767e7f84c 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -179,6 +179,8 @@ const makePageKey = (route: YakitRouteType, pageId: string): PageKey => `${route /** * 从渲染树快照收集「首屏」正文 token: * casual / task 两侧顶层 elements 各取最后 topCount 条,并展开 group/task 的 childrenTokens。 + * 最多两层:group 的 children 是叶子 item;task 的 children 可能是 group 或 item, + * group 下不再嵌套 group,所以固定两层展开即可。 */ const collectTopLevelContentTokens = (content: SessionRenderContent, topCount: number): string[] => { const tokenSet = new Set() @@ -191,7 +193,12 @@ const collectTopLevelContentTokens = (content: SessionRenderContent, topCount: n group?.childrenTokens?.forEach((t) => tokenSet.add(t)) } else if (el.kind === 'task') { const task = content.tasks[el.token] - task?.childrenTokens?.forEach((t) => tokenSet.add(t)) + task?.childrenTokens?.forEach((childToken) => { + tokenSet.add(childToken) + // task 的 child 可能是 group,再展开一层 group 的 children(叶子 item) + const childGroup = content.groups[childToken] + childGroup?.childrenTokens?.forEach((t) => tokenSet.add(t)) + }) } } } @@ -445,7 +452,7 @@ export class ChatMultiSessionController { } } /** 按 token 列表批量获取会话消息内容 */ - private async persistGetSessionContents(sessionId: string, tokens: string[]) { + async persistGetSessionContents(sessionId: string, tokens: string[]) { try { return await aiChatPersistStore.getSessionContents(sessionId, tokens) } catch { @@ -897,6 +904,19 @@ export class ChatMultiSessionController { ipcRenderer.invoke('send-ai-re-act', sessionId, request) } + /** 发 recovery_history 拉更旧事件(grpcOffset 为起点,向前回溯 RECOVERY_HISTORY_LIMIT 条) */ + public requestRecoveryHistory(sessionId: string) { + const { rawData } = this.ensureSession(sessionId) + this.requestMessage(sessionId, { + IsSyncMessage: true, + SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_HISTORY, + SyncJsonInput: JSON.stringify({ + start_id: rawData.grpcOffset, + limit: ChatMultiSessionController.RECOVERY_HISTORY_LIMIT, + }), + }) + } + /** 会话建立成功后, 需要做的额外操作 */ private handleSessionStartSuccess(sessionId: string) { const { store, meta } = this.ensureSession(sessionId) @@ -966,14 +986,7 @@ export class ChatMultiSessionController { this.finishSessionRestoreLoading(sessionId) } else { // 保持 switchLoading,等 recovery_history 再关,避免 UI 提前可点 - this.requestMessage(sessionId, { - IsSyncMessage: true, - SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_HISTORY, - SyncJsonInput: JSON.stringify({ - start_id: rawData.grpcOffset, - limit: ChatMultiSessionController.RECOVERY_HISTORY_LIMIT, - }), - }) + this.requestRecoveryHistory(sessionId) } } else { // 带首问的新会话:用当前 store 快照(可能已有首问)+ offset diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index 7759bfc39b..0662839f64 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -181,7 +181,15 @@ export const createChatStore = (options?: CreateChatStoreOptions) => { } if (isHistory) { - // 历史数据处理逻辑 + // 历史数据:实体已在上方注册,这里只做 elements 前插(旧→新顺序重放,unshift 到头部) + // 前插完必须 return,否则会走到下面"默认追加"再 push 一次导致双插 + const parentChildren = parentTaskId ? state.tasks[parentTaskId]?.childrenTokens : undefined + if (parentChildren) { + parentChildren.unshift(node.token) + } else { + targetElements.unshift(elementRef) + } + return } else { // 空列表直接追加 if (targetElements.length === 0) { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useLoadOlder.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useLoadOlder.ts new file mode 100644 index 0000000000..5052dfe7d2 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useLoadOlder.ts @@ -0,0 +1,227 @@ +import { useMemoizedFn } from 'ahooks' +import { useEffect, useRef } from 'react' +import type { ListRange } from 'react-virtuoso' +import { globalSessionEngine } from './ChatMultiSessionController' +import { useCurrentStore, useCurrentRawData } from './useCurrentDataBySession' +import useCurrentSessionId from './useCurrentSessionId' +import type { ChatListRenderType } from './aiRender' + +/** 向上/向下固定预取条数 */ +const LOAD_AHEAD = 10 +/** 向后保留条数(已滚过的不立即淘汰,保留 5 条缓冲) */ +const KEEP_BEHIND = 5 +/** 强制保留最新尾部条数(人在中间看历史时,底部流式输出不丢展示) */ +const TAIL_KEEP = 30 + +/** + * 上滑加载更旧历史 + 视窗内存回收(casual / task 通用)。 + * + * 触发机制:rangeChanged(视窗变化驱动一切)。 + * elements 全量常驻,contents 按需缓存。 + * + * onRangeChange 一次回调做四件事: + * 1. 可见区 [start,end] 缺正文 → 立即 hydrate(IDB → 内存 + bump renderNum) + * 2. 按方向额外预取 LOAD_AHEAD 条 + * 3. 触顶(startIndex===0)且可见区全齐且 grpcOffset>0 → requestRecoveryHistory(gRPC 兜底) + * 4. 淘汰:keep = 视窗 + 向前 LOAD_AHEAD + 向后 KEEP_BEHIND + 最新尾部 TAIL_KEEP; + * contents 里不在 keep 的删除 + * + * @param chatType 'reAct'(casual 空闲对话)/ 'task'(任务对话) + */ +const useLoadOlder = (chatType: ChatListRenderType) => { + const sessionId = useCurrentSessionId() + const store = useCurrentStore() + const rawData = useCurrentRawData() + + /** 上次 startIndex,用于判断滚动方向 */ + const lastStartIndexRef = useRef(-1) + + /** 置/关 loading(gRPC recovery 异步等待时给 Header 转圈提示) */ + const setLoading = useMemoizedFn((on: boolean) => { + const state = store.getState() + const key = chatType === 'reAct' ? 'casualLoadMoreLoading' : 'taskLoadMoreLoading' + store.getState().updateState({ + requestHistoryState: { ...state.requestHistoryState, [key]: on }, + }) + }) + + // 切会话时重置方向跟踪 + 清空旧 contents(旧 session Map 占内存) + useEffect(() => { + lastStartIndexRef.current = -1 + // 清空旧 session 的 contents 视窗(保留 IDB,可恢复) + if (sessionId && rawData.contents.size > 0) { + const tokens = [...rawData.contents.keys()] + globalSessionEngine.removeContentsFromMemory(sessionId, tokens) + } + }, [sessionId]) + + /** 取当前 chatType 的 elements */ + const getElements = useMemoizedFn(() => { + const state = store.getState() + return chatType === 'reAct' ? state.casualChat.elements : state.taskChat.elements + }) + + /** + * 展开 element 为 token 列表(含 group/task 的 children,最多两层:task→group→item)。 + * 真实结构:group 的 children 是叶子 item;task 的 children 可能是 group 或 item, + * group 下不再嵌套 group,所以固定两层展开即可,不需要通用递归。 + */ + const expandTokens = useMemoizedFn((el: { kind: string; token: string }): string[] => { + const state = store.getState() + const tokens = [el.token] + if (el.kind === 'group') { + const children = state.groups[el.token]?.childrenTokens || [] + tokens.push(...children) + } else if (el.kind === 'task') { + const children = state.tasks[el.token]?.childrenTokens || [] + for (const childToken of children) { + tokens.push(childToken) + // task 的 child 可能是 group,再展开一层 group 的 children(叶子 item) + const childGroup = state.groups[childToken] + if (childGroup?.childrenTokens?.length) { + tokens.push(...childGroup.childrenTokens) + } + } + } + return tokens + }) + + /** 收集指定 index 区间内"树里有、contents 缺正文"的 token */ + const collectMissingByRange = useMemoizedFn((startIndex: number, endIndex: number): string[] => { + const elements = getElements() + const lo = Math.max(0, startIndex) + const hi = Math.min(elements.length - 1, endIndex) + const missing: string[] = [] + for (let i = lo; i <= hi; i++) { + const el = elements[i] + if (!el) continue + for (const tok of expandTokens(el)) { + if (!rawData.contents.has(tok)) missing.push(tok) + } + } + return missing + }) + + /** 按 token 列表从 IDB 补灌 + 灌内存 + bump renderNum(按 kind) */ + const hydrateTokens = useMemoizedFn(async (tokens: string[]) => { + if (!tokens.length || !sessionId) return + const rows = await globalSessionEngine.persistGetSessionContents(sessionId, tokens) + const state = store.getState() + for (const row of rows) { + rawData.contents.set(row.token, row.content) + // 按 kind bump renderNum:item→items,group→groups,task→tasks + if (state.items[row.token]) { + store.getState().incrementNodeVersion(row.token, 'item') + } else if (state.groups[row.token]) { + store.getState().incrementNodeVersion(row.token, 'group') + } else if (state.tasks[row.token]) { + store.getState().incrementNodeVersion(row.token, 'task') + } else { + store.getState().incrementNodeVersion(row.token, 'item') + } + } + // IDB 仍缺的 token 不在此触发 gRPC——recovery_history 是前插更旧事件,不是按 token 补正文。 + // 树外更旧历史由 onRangeChange 里的"触顶 + grpcOffset>0"分支专门处理。 + }) + + /** 计算 keep 集合(视窗 + 向前 LOAD_AHEAD + 向后 KEEP_BEHIND + 最新尾部 TAIL_KEEP) */ + const computeKeepTokens = useMemoizedFn((startIndex: number, endIndex: number): Set => { + const elements = getElements() + if (!elements.length) return new Set() + + const keep = new Set() + // 视窗 + 向前预取(LOAD_AHEAD) + 向后保留(KEEP_BEHIND) + const lo = Math.max(0, startIndex - LOAD_AHEAD) + const hi = Math.min(elements.length - 1, endIndex + KEEP_BEHIND) + for (let i = lo; i <= hi; i++) { + const el = elements[i] + if (!el) continue + for (const tok of expandTokens(el)) keep.add(tok) + } + // 强制保留最新尾部 + const tailStart = Math.max(0, elements.length - TAIL_KEEP) + for (let i = tailStart; i < elements.length; i++) { + const el = elements[i] + if (!el) continue + for (const tok of expandTokens(el)) keep.add(tok) + } + return keep + }) + + /** 淘汰视窗外 contents */ + const evictOutsideViewport = useMemoizedFn((startIndex: number, endIndex: number) => { + if (!sessionId) return + const keep = computeKeepTokens(startIndex, endIndex) + const toEvict: string[] = [] + for (const token of rawData.contents.keys()) { + if (!keep.has(token)) toEvict.push(token) + } + if (toEvict.length) { + globalSessionEngine.removeContentsFromMemory(sessionId, toEvict) + } + }) + + /** + * 视窗变化驱动加载 + 淘汰(接 Virtuoso rangeChanged)。 + */ + const onRangeChange = useMemoizedFn(({ startIndex, endIndex }: ListRange) => { + if (!sessionId) return + const elements = getElements() + if (!elements.length) return + + // 跟踪滚动方向(startIndex 变小 = 向上滚) + let direction: 'up' | 'down' | 'none' = 'none' + if (lastStartIndexRef.current >= 0) { + if (startIndex < lastStartIndexRef.current) { + direction = 'up' + } else if (startIndex > lastStartIndexRef.current) { + direction = 'down' + } + } + lastStartIndexRef.current = startIndex + + // 1. 可见区缺正文 → 立即 hydrate(保证不空白) + const visibleMissing = collectMissingByRange(startIndex, endIndex) + if (visibleMissing.length) { + void hydrateTokens(visibleMissing) + } + + // 2. 方向侧预取 LOAD_AHEAD 条 + let prefetchTokens: string[] = [] + if (direction === 'up') { + const preLo = Math.max(0, startIndex - LOAD_AHEAD) + const preHi = startIndex - 1 + if (preLo <= preHi) { + prefetchTokens = collectMissingByRange(preLo, preHi) + } + } else if (direction === 'down') { + const preLo = endIndex + 1 + const preHi = Math.min(elements.length - 1, endIndex + LOAD_AHEAD) + if (preLo <= preHi) { + prefetchTokens = collectMissingByRange(preLo, preHi) + } + } + if (prefetchTokens.length > LOAD_AHEAD) { + prefetchTokens = prefetchTokens.slice(0, LOAD_AHEAD) + } + if (prefetchTokens.length) { + void hydrateTokens(prefetchTokens) + } + + // 3. 触顶 + 可见区全齐 + 树外还有更旧 → gRPC recovery_history + if (startIndex === 0 && direction === 'up' && rawData.grpcOffset > 0) { + if (visibleMissing.length === 0) { + setLoading(true) + globalSessionEngine.requestRecoveryHistory(sessionId) + setTimeout(() => setLoading(false), 3000) + } + } + + // 4. 淘汰视窗外 + evictOutsideViewport(startIndex, endIndex) + }) + + return { onRangeChange } +} + +export default useLoadOlder From ad2c0f68d0f7dcc6fddb9be5bb4e682610e9276a Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 29 Jul 2026 11:27:23 +0800 Subject: [PATCH 063/101] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=A0=91=E5=AE=9A=E4=BD=8D=E5=88=97=E8=A1=A8=E9=97=AE?= =?UTF-8?q?=E9=A2=98;fix:=E4=BF=AE=E5=A4=8D=E4=BE=A7=E8=BE=B9=E6=A0=8F?= =?UTF-8?q?=E5=88=9D=E5=A7=8B=E6=9D=A5=E6=BA=90=E9=97=AE=E9=A2=98=EF=BC=9B?= =?UTF-8?q?feat:=E8=AE=BE=E7=BD=AE=E5=BD=93=E5=89=8D=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E7=9A=84seesion;fix:=E4=BF=AE=E5=A4=8D=E9=80=BB=E8=BE=91?= =?UTF-8?q?=E4=B8=8A=E7=9A=84=E5=AE=9A=E6=97=B6=E5=99=A8=E6=B8=85=E9=99=A4?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/withHistoryAIReActChat.tsx | 5 +++- .../chatTemplate/AIAgentChatTemplate.tsx | 12 +++++++- .../ai-re-act/aiReActChat/AIReActChat.tsx | 28 ++++++++++++++++++- .../aiReActChatHeader/AIReActChatHeader.tsx | 14 ++++++---- .../hooks/ChatMultiSessionController.ts | 4 +-- .../hooks/grpcStreamHandler/aiOther.ts | 2 +- 6 files changed, 53 insertions(+), 12 deletions(-) diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index e890fe54c1..2d2e6e4200 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -202,7 +202,10 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const aiReActChatRef = useRef(null) const [showFreeChat, setShowFreeChat] = useSafeState(false) - const [setting, setSetting, getSetting] = useGetSetState(() => cloneDeep(AIAgentSettingDefault)) + const [setting, setSetting, getSetting] = useGetSetState(() => ({ + ...cloneDeep(AIAgentSettingDefault), + Source: source, + })) const [activeChat, setActiveChat] = useSafeState() const casualLoadingRef = useRef(false) const initialRequestInCasualRef = useRef(null) diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx index a758afbb08..8b91eb3d29 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/AIAgentChatTemplate.tsx @@ -181,6 +181,7 @@ export const AIAgentChatStream: React.FC = memo((props) const session = useCurrentSessionId() const store = useCurrentStore() + const rawData = useCurrentRawData() const streams = useStore(store, (state) => state.taskChat.elements) @@ -257,7 +258,16 @@ export const AIAgentChatStream: React.FC = memo((props) ) const onTreeLocate = useMemoizedFn((id?: string) => { if (!id) return - const index = streams.findIndex((item) => item.token === id) + const index = streams.findLastIndex((item) => { + const itemData = rawData.contents.get(item.token) + switch (itemData?.type) { + case AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP: + case AIChatQSDataTypeEnum.TASK_NODE_GROUP: + return itemData.data?.taskId === id + default: + return false + } + }) if (index !== -1) locateToIndex(index, 'auto') }) useMount(() => { diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx index b5d39c85a6..3cf28136bf 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/AIReActChat.tsx @@ -5,7 +5,7 @@ import { AIHandleStartResProps, AINotifyMessageProps, AIReActChatProps, AISendRe import { AIReActChatContents } from '../aiReActChatContents/AIReActChatContents' import type { AIReActChatContentsRef } from '../aiReActChatContents/AIReActChatContentsType' import { AIChatTextareaRefProps, AIChatTextareaSubmit } from '@/pages/ai-agent/template/type' -import { useControllableValue, useCreation, useMemoizedFn } from 'ahooks' +import { useControllableValue, useCreation, useInViewport, useMemoizedFn } from 'ahooks' import { yakitNotify } from '@/utils/notification' import useAIAgentStore from '@/pages/ai-agent/useContext/useStore' import classNames from 'classnames' @@ -26,6 +26,7 @@ import useCurrentSessionId from '../hooks/useCurrentSessionId' import { AIReactChatTextarea } from './aiReactChatTextarea/AIReactChatTextarea' import { AIReActChatHeader } from './aiReActChatHeader/AIReActChatHeader' import { AIToDoListWrapper } from './aiToDoListWrapper/AIToDoListWrapper' +import { globalSessionEngine } from '../hooks/ChatMultiSessionController' export const AIReActChat: React.FC = React.memo( forwardRef((props, ref) => { @@ -43,6 +44,7 @@ export const AIReActChat: React.FC = React.memo( const store = useCurrentStore() const wrapperRef = useRef(null) + const [inViewPort = true] = useInViewport(wrapperRef) const [showFreeChat, setShowFreeChat] = useControllableValue(props, { defaultValue: true, @@ -85,6 +87,30 @@ export const AIReActChat: React.FC = React.memo( } } }, [activeChat?.SessionID]) + + // #region + /** + * 1.切换Session后设置当前选中的 SessionID ,如果该组件被卸载意外着当前没有任何对话在显示 + * 2.当该组件从不可见变可见的时候,需要设置当前选中的 SessionID + * */ + useEffect(() => { + if (activeChat?.SessionID) { + globalSessionEngine?.setActiveShowSession(activeChat?.SessionID) + } + return () => { + globalSessionEngine?.setActiveShowSession('') + } + }, [activeChat?.SessionID]) + useEffect(() => { + if (inViewPort) { + globalSessionEngine?.setActiveShowSession(activeChat?.SessionID ?? '') + + return () => { + globalSessionEngine?.setActiveShowSession('') + } + } + }, [inViewPort]) + //#endregion // #region 问题相关逻辑 // 初始化 AI ReAct const handleSubmit = useMemoizedFn((value: AIChatTextareaSubmit) => { diff --git a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx index bed9accc18..bf3f6523e8 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx +++ b/app/renderer/src/main/src/pages/ai-re-act/aiReActChat/aiReActChatHeader/AIReActChatHeader.tsx @@ -213,18 +213,20 @@ const AIReActChatHeaderExternalRightIcon: React.FC { switch (setting?.Source) { - case 'flow': + case AISourceEnum.flow: return ['flow'] - case 'knowledgeBase': + case AISourceEnum.knowledgeBase: return ['knowledgeBase'] - case 'webFuzzer': + case AISourceEnum.webFuzzer: return ['webFuzzer'] - case 'irify': + case AISourceEnum.irify: return ['irify'] - case 'yakRunner': + case AISourceEnum.yakRunner: return ['yakRunner'] - case 'im': + case AISourceEnum.im: return ['im'] + case AISourceEnum.history: + return ['history'] default: return AI_AGENT_HISTORY_AI_SOURCES // AI Agent 侧栏历史会话:包含 ai、im 来源与兼容老数据的空 source } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 9767e7f84c..5593071dad 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -409,7 +409,7 @@ export class ChatMultiSessionController { } /** TODO - 存放当前正在展示的会话session */ - private activeShowSession: string | null = null + private activeShowSession: string = '' /** 设置当前展示的会话 Session */ public setActiveShowSession(sessionId: string) { this.activeShowSession = sessionId @@ -1064,7 +1064,7 @@ export class ChatMultiSessionController { store.getState().updateState({ casualTitle: '等待回复中...' }) // 因为有用户问题发送,所以注册 获取问题队列轮询器 - if (meta.queuePollingTimer) clearTimeout(meta.queuePollingTimer) + if (meta.queuePollingTimer) clearInterval(meta.queuePollingTimer) meta.queuePollingEmptyCount = 0 meta.queuePollingTimer = setInterval(() => { this.requestMessage(sessionId, { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index e6c8fec267..0bc40bc86b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -330,7 +330,7 @@ const handleQueueInfo: AIMessageHandler = (request) => { if (tasks.length === 0) meta.queuePollingEmptyCount += 1 else meta.queuePollingEmptyCount = 0 if (meta.queuePollingEmptyCount > 3 && meta.queuePollingTimer) { - clearTimeout(meta.queuePollingTimer) + clearInterval(meta.queuePollingTimer) meta.queuePollingTimer = null } From 290ae4f1a2e4348c79455c8e0c343fcd84ca5645 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 29 Jul 2026 12:13:50 +0800 Subject: [PATCH 064/101] =?UTF-8?q?feat:=E5=A6=82=E6=9E=9C=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E5=8E=86=E5=8F=B2=E5=9C=A8aiagent=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E4=B8=AD=EF=BC=8C=E7=9B=B4=E6=8E=A5=E5=88=87=E6=8D=A2=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=EF=BC=9B=E5=85=B6=E4=BD=99=E9=A1=B5=E9=9D=A2=E9=9C=80?= =?UTF-8?q?=E8=A6=81=E5=88=A4=E6=96=AD=E5=AF=B9=E8=AF=9D=E6=98=AF=E5=90=A6?= =?UTF-8?q?=E5=9C=A8=E6=89=A7=E8=A1=8C=EF=BC=8C=E6=89=A7=E8=A1=8C=E4=B8=AD?= =?UTF-8?q?=E9=9C=80=E8=A6=81=E5=85=88=E6=96=AD=E5=BC=80=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=86=8D=E8=AE=BE=E7=BD=AE=E6=96=B0=E4=BC=9A=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/src/pages/ai-agent/AIAgent.tsx | 5 ---- .../ai-agent/aiAgentChat/AIAgentChat.tsx | 6 +++- .../HistoryChatList/HistoryChatList.tsx | 29 +++++++++++++------ .../hooks/ChatMultiSessionController.ts | 2 +- 4 files changed, 26 insertions(+), 16 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx b/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx index a4a830c314..de2834a911 100644 --- a/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/AIAgent.tsx @@ -32,7 +32,6 @@ import { grpcDeleteAISession } from './grpc' import { useChatIPC } from '../ai-re-act/hooks/useChatIPC' import { AISourceEnum } from '../ai-re-act/hooks/grpcApi' import { YakitRoute } from '@/enums/yakitRoute' -import { onReStart } from './utils' import { globalSessionEngine } from '../ai-re-act/hooks/ChatMultiSessionController' /** 清空用户缓存的固定值 */ @@ -103,10 +102,6 @@ export const AIAgent: React.FC = (props) => { const { onStart, onSend, onClose, onUpdatePageId } = useChatIPC(YakitRoute.AI_Agent, YakitRoute.AI_Agent) - useUpdateEffect(() => { - if (activeChat) onReStart({ activeChat, onStart }) - }, [activeChat?.SessionID]) - const store: AIAgentContextStore = useMemo(() => { return { setting: setting, diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index 468a7280a4..eae839840f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -39,6 +39,7 @@ import { useStore } from 'zustand' import { AIForgeFormSubmitParamsProps } from '../aiTriageChatTemplate/type' import { useCurrentMeta, useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' +import { onReStart } from '../utils' const AIChatWelcome = React.lazy(() => import('../aiChatWelcome/AIChatWelcome')) @@ -46,7 +47,7 @@ export const AIAgentChat: React.FC = memo((props) => { const { t } = useI18nNamespaces(['aiAgent', 'yakitUi']) const { activeChat } = useAIAgentStore() - const { setActiveChat, setSetting, onSend, onClose } = useAIAgentDispatcher() + const { setActiveChat, setSetting, onStart, onClose } = useAIAgentDispatcher() /** 当前对话唯一ID */ const sessionId = useCurrentSessionId() @@ -55,6 +56,7 @@ export const AIAgentChat: React.FC = memo((props) => { const aiReActChatRef = useRef(null) const aiChatWelcomeRef = useRef(null) + const initRef = useRef(true) // 插件并发构建流 hooks const [streams, api] = useMultipleHoldGRPCStream() @@ -69,6 +71,8 @@ export const AIAgentChat: React.FC = memo((props) => { useEffect(() => { if (!!activeChat?.SessionID) { onSetReAct() + if (!initRef.current) onReStart({ activeChat, onStart }) + initRef.current = false } }, [activeChat?.SessionID]) diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx index 4dc3a117a3..7da62792a7 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx @@ -20,10 +20,11 @@ import type { SessionListDispatcher } from './hook/useSessionList' import type { AISource } from '@/pages/ai-re-act/hooks/grpcApi' import { getHistorySessionIconMeta, getSessionDisplayTitle } from '../source' import { handAIHistoryChatRemove } from '../utils' -import useGetChatDataStoreKey from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' +import useGetChatDataStoreKey, { AI_AGENT_HISTORY_AI_SOURCES } from '@/pages/ai-re-act/hooks/useGetChatDataStoreKey' import { usePageInfo } from '@/store/pageInfo' import { shallow } from 'zustand/shallow' import type { YakitRouteType } from '@/enums/yakitRoute' +import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' export const HOUR_MS = 60 * 60 * 1000 export const DAY_MS = 24 * HOUR_MS @@ -202,7 +203,7 @@ const HistoryChatList: FC<{ editInfo.current = undefined }) - const { getSetting } = useAIAgentDispatcher() + const { getSetting, onClose } = useAIAgentDispatcher() const handleDeleteChat = useMemoizedFn(async (info: AISession) => { const { SessionID } = info const isLoading = delLoading.includes(SessionID) @@ -251,13 +252,7 @@ const HistoryChatList: FC<{ setDelLoading((old) => old.filter((el) => el !== SessionID)) } }) - - const handleSetActiveChat = useMemoizedFn((info: AISession) => { - // 暂时性逻辑,因为老版本的对话信息里没有请求参数,导致在新版本无法使用对话里的重新执行功能 - // 所以会提示警告,由用户决定是否删除历史对话 - // if (!info.request) { - // yakitNotify("warning", "当前对话无请求参数信息,无法使用重新执行功能") - // } + const onSetChat = useMemoizedFn((info: AISession) => { setSetting?.((old) => ({ ...old, SyncPerceptionTrigger: info?.StartParams?.SyncPerceptionTrigger ?? false, @@ -270,6 +265,22 @@ const HistoryChatList: FC<{ })) setActiveChat && setActiveChat(info) }) + // 如果当前历史在aiagent页面中,直接切换会话;其余页面需要判断对话是否在执行,执行中需要先断开会话再设置新会话 + const handleSetActiveChat = useMemoizedFn((info: AISession) => { + if (aiSource.some((source) => AI_AGENT_HISTORY_AI_SOURCES.includes(source))) { + onSetChat(info) + return + } + const execute = globalSessionEngine?.ensureSession(info.SessionID)?.store?.getState()?.execute + if (!!execute) { + yakitNotify('info', '在其他页面中会话正在执行中') + onClose([info.SessionID], () => { + onSetChat(info) + }) + } else { + onSetChat(info) + } + }) return (
diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 5593071dad..13bdbf6336 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -1277,7 +1277,7 @@ export class ChatMultiSessionController { this.readyChannels.delete(sessionId) if (this.activeShowSession === sessionId) { - this.activeShowSession = null + this.activeShowSession = '' } this.requestPool.delete(sessionId) From bd6fe86c061aa852abf54a429cc331d19a1b5023 Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 29 Jul 2026 14:19:34 +0800 Subject: [PATCH 065/101] =?UTF-8?q?fix:handAIHistoryChatRemove=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E4=B8=ADdeleteSessionsParams>source=E6=94=B9=E4=B8=BA?= =?UTF-8?q?sources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/src/pages/ai-agent/historyChat/HistoryChat.tsx | 8 ++++---- .../historyChat/HistoryChatList/HistoryChatList.tsx | 2 +- .../src/main/src/pages/ai-agent/historyChat/utils.ts | 5 ++--- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx index aeec39509a..9df67d2cd8 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChat.tsx @@ -90,7 +90,7 @@ const isSessionMatchSource = (session: AISession, sources: AISource[]) => { } interface HistoryChatProps { - /** 会话来源过滤,AI Agent 侧栏为 ['ai', ''],各业务页为 [source] */ + /** 会话来源过滤,AI Agent 侧栏为 ['ai', '',"im"],各业务页为 [source] */ aiSource: AISource[] /** 嵌入 Tooltip 等浮层场景:隐藏新建/固定按钮,弹层挂载到当前页面容器 */ embedded?: boolean @@ -145,13 +145,13 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { setClearLoading(true) try { - const source = getSetting().Source || 'ai' const filter = isGlobalAIAgentHistory ? { DeleteAll: true } : { Filter: { Source: historyQuerySources } } await handAIHistoryChatRemove({ grpcDeleteAISessionParams: filter, handleClearAIImageParams: { chatDataStoreKey, sessionID: [] }, //删除全部只需要传chatDataStoreKey deleteSessionsParams: { - source, + // 全局历史页删全部来源;否则删当前 tab 对应来源(local=ai, feishu/dingtalk=im) + sources: isGlobalAIAgentHistory ? aiSource : historyQuerySources, sessionIds: [], route: currentRouteKey as YakitRouteType, pageId: currentPageId || currentRouteKey, @@ -200,7 +200,7 @@ const HistoryChat = memo(({ aiSource, embedded }: HistoryChatProps) => { grpcDeleteAISessionParams: { Filter: filter }, handleClearAIImageParams: { chatDataStoreKey: getImageStoreKeyByAISource(source), sessionID: sessionIds }, deleteSessionsParams: { - source, + sources: [source], sessionIds, route: currentRouteKey as YakitRouteType, pageId: currentPageId || currentRouteKey, diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx index 7da62792a7..612a6ab306 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/HistoryChatList/HistoryChatList.tsx @@ -236,7 +236,7 @@ const HistoryChatList: FC<{ grpcDeleteAISessionParams: { Filter: { SessionID: [SessionID], Source: aiSource } }, handleClearAIImageParams: { chatDataStoreKey, sessionID: sessionIds }, deleteSessionsParams: { - source, + sources: [source], sessionIds, route: currentRouteKey as YakitRouteType, pageId: currentPageId || currentRouteKey, diff --git a/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts b/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts index f0b961ac37..8c2089acd2 100644 --- a/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts +++ b/app/renderer/src/main/src/pages/ai-agent/historyChat/utils.ts @@ -16,7 +16,7 @@ export interface HandAIHistoryChatRemoveParams { /** 删除 Controller 内存中的 session(含双索引与业务池) */ deleteSessionsParams: { sessionIds: string[] - source: AISource + sources: AISource[] route: YakitRouteType pageId: string } @@ -35,8 +35,7 @@ export const handAIHistoryChatRemove = async (params: HandAIHistoryChatRemovePar await grpcDeleteAISession(grpcDeleteAISessionParams, true) // 2.删除图片数据 handleClearAIImage(handleClearAIImageParams) - // 3.删除store缓存数据 + // 3.删除store缓存数据同时删除indexdb globalSessionEngine.deleteSessions(deleteSessionsParams) - // TODO - 4.删除indexdb } catch (error) {} } From d57b025b647854170338c0b91693264d5af5e7c1 Mon Sep 17 00:00:00 2001 From: whale <1745132665@qq.com> Date: Wed, 29 Jul 2026 14:35:39 +0800 Subject: [PATCH 066/101] =?UTF-8?q?fix:=20=E6=81=A2=E5=A4=8D=20AI=20?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=A1=A5=E6=8E=A5=E4=BA=8B=E4=BB=B6=E6=B6=88?= =?UTF-8?q?=E8=B4=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 订阅当前会话的流事件更新,并同步改包、Fuzz 状态和代码变更至页面。 Co-authored-by: Cursor --- .../src/components/withHistoryAIReActChat.tsx | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index 2d2e6e4200..fcc6423e79 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -1,6 +1,6 @@ import React, { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef } from 'react' import { useCreation, useMemoizedFn, useSafeState, useUpdateEffect } from 'ahooks' -import { cloneDeep } from 'lodash' +import { clone, cloneDeep } from 'lodash' import AIAgentContext, { AIAgentContextDispatcher, @@ -47,7 +47,7 @@ import emiter from '@/utils/eventBus/eventBus' import { HistroryAIReActChat } from './HistroryAIReActChat' import { useChatIPC } from '@/pages/ai-re-act/hooks/useChatIPC' -import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' @@ -321,8 +321,12 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const { onStart, onSend, onClose, onUpdatePageId } = useChatIPC(route, pageId) const store = useCurrentStore() + const rawData = useCurrentRawData() const execute = useStore(store, (state) => state.execute) const casualLoading = useStore(store, (state) => state.casualLoading) + const httpFuzzRequestUpdate = useStore(store, (state) => state.httpFuzzRequestUpdate) + const httpFlowFuzzStatusUpdate = useStore(store, (state) => state.httpFlowFuzzStatusUpdate) + const yaklangCodeChangeUpdate = useStore(store, (state) => state.yaklangCodeChangeUpdate) // TODO - @whale 修改确认 useEffect => httpFuzzTabPageId->isHaveWebFuzzerPageId useEffect(() => { @@ -352,6 +356,23 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid casualLoadingRef.current = casualLoading }, [casualLoading, pageId, isHaveWebFuzzerPageId, isHaveYakRunnerPageId]) + // 新版流处理器将页面桥接事件写入当前 session 的 rawData,并通过计数通知订阅者。 + // 在此消费这些更新,保持 Web Fuzzer / Yak Runner 的页面回写能力。 + useEffect(() => { + if (!rawData.httpFuzzRequest) return + onHttpFuzzRequestChange(clone(rawData.httpFuzzRequest)) + }, [httpFuzzRequestUpdate]) + + useEffect(() => { + if (!rawData.httpFlowFuzzStatus) return + onGetHttpFlowFuzzStatus(clone(rawData.httpFlowFuzzStatus)) + }, [httpFlowFuzzStatusUpdate]) + + useEffect(() => { + if (!rawData.yaklangCodeChange) return + onYaklangCodeChange(clone(rawData.yaklangCodeChange)) + }, [yaklangCodeChangeUpdate]) + const activeID = useCreation(() => { return activeChat?.SessionID }, [activeChat]) From 2a0437fdea743b7b7cb942f15eea178ccd6ef4bb Mon Sep 17 00:00:00 2001 From: hubo Date: Wed, 29 Jul 2026 15:43:56 +0800 Subject: [PATCH 067/101] =?UTF-8?q?feat:=20=E6=81=A2=E5=A4=8D=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E7=BA=BF=E4=B8=8E=E6=96=87=E4=BB=B6=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=8E=86=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Controller 新增 loadTimelineHistory/hasMoreTimeline/loadFileSystemHistory 三个 public 方法 - handleSessionStartSuccess 建连后自动拉 timeline 首批 + filesystem 全量 - timeline 用 BeforeId 游标分页(每页200条),filesystem 全量去重合并 - AIAgentChatData 新增 timelineBeforeId 游标字段 - chatStore 新增 setGrpcFolders/setReActTimelines 批量替换方法 - TimelineCard 解注释 useLoadHistory 接线,上滑调 globalSessionEngine 翻页 --- .../TimelineCard/TimelineCard.tsx | 30 +++--- .../main/src/pages/ai-agent/type/aiChat.d.ts | 3 + .../hooks/ChatMultiSessionController.ts | 98 ++++++++++++++++++- .../src/pages/ai-re-act/hooks/aiRender.ts | 4 + .../src/pages/ai-re-act/hooks/chatStore.ts | 10 ++ .../pages/ai-re-act/hooks/useLoadHistory.ts | 2 +- 6 files changed, 130 insertions(+), 17 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx index f7cf3a72a3..4132e2c538 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/TimelineCard/TimelineCard.tsx @@ -10,8 +10,10 @@ import { YakitPopover } from '@/components/yakitUI/YakitPopover/YakitPopover' import { OutlineInformationcircleIcon } from '@/assets/icon/outline' import { useMemoizedFn, useSize } from 'ahooks' import { YakitSpin } from '@/components/yakitUI/YakitSpin/YakitSpin' -import useAIAgentStore from '../../useContext/useStore' import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' +import useLoadHistory from '@/pages/ai-re-act/hooks/useLoadHistory' +import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' import { useStore } from 'zustand' const TYPE_COLOR_MAP: Record = { @@ -82,29 +84,27 @@ const VirtuosoListContainer = forwardRef(({ children, VirtuosoListContainer.displayName = 'VirtuosoListContainer' const TimelineCard: FC = () => { - const { activeChat } = useAIAgentStore() - const store = useCurrentStore() + const sessionId = useCurrentSessionId() const reActTimelines = useStore(store, (state) => state.reActTimelines) const timelinesLoading = useStore(store, (state) => state.requestHistoryState.timelinesLoading) - /** TODO - 等待hooks */ - // const { handleLoadMoreHistory, handleHasMoreHistory } = useChatIPCDispatcher().chatIPCEvents + // 向上滚动加载历史 timeline + const { firstItemIndex, handleLoadMore, isPrependingRef } = useLoadHistory({ + loading: timelinesLoading, + dataLength: reActTimelines.length, + SessionID: sessionId, + fetchHasMore: () => globalSessionEngine.hasMoreTimeline(sessionId), + loadMore: () => globalSessionEngine.loadTimelineHistory(sessionId), + }) const { virtuosoRef, handleTotalListHeightChanged, setScrollerRef, setIsAtBottomRef } = useVirtuosoAutoScroll({ total: reActTimelines.length, + isPrependingRef, }) const containerRef = useRef(null) const size = useSize(containerRef) - // const { firstItemIndex, handleLoadMore } = useLoadHistory({ - // loading: timelinesLoading, - // dataLength: reActTimelines.length, - // SessionID: activeChat?.SessionID || '', - // fetchHasMore: () => handleHasMoreHistory('timelines'), - // loadMore: () => handleLoadMoreHistory('timelines'), - // }) - const components = useMemo>( () => ({ Item: VirtuosoItemContainer, @@ -128,7 +128,7 @@ const TimelineCard: FC = () => { { increaseViewportBy={{ top: 300, bottom: 300 }} atBottomThreshold={100} skipAnimationFrameInResizeObserver - // startReached={handleLoadMore} + startReached={handleLoadMore} itemContent={itemContent} /> diff --git a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts index 577ed768c5..07c5a9e8ac 100644 --- a/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts +++ b/app/renderer/src/main/src/pages/ai-agent/type/aiChat.d.ts @@ -184,6 +184,9 @@ export interface AIAgentChatData { */ grpcOffset: number + /** timeline 历史分页游标(grpcQueryAIEvent 的 BeforeId),0 表示未拉过 */ + timelineBeforeId: number + /** 记录数据里所有的httpRunTimeIDs */ httpRunTimeIDs: string[] /** 记录数据里所有的riskRunTimeIDs */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 13bdbf6336..17f29c641b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -4,6 +4,7 @@ import { AISource, AISourceEnum, type AIAgentGrpcApi, + type AIEventQueryRequest, type AIInputEvent, type AIOutputEvent, type AIStartParams, @@ -25,7 +26,7 @@ import { } from './defaultConstant' import { grpcAIMessageHandlers } from './grpcStreamHandler/grpcAIOutputEventHandlers' import { genExecTasks, handleTaskPlanEnd, pushLogToOtherWindow } from './utils' -import type { AIChatIPCStartParams, AIChatSendParams } from './type' +import type { AIChatIPCStartParams, AIChatSendParams, AIFileSystemPin } from './type' import { yakitNotify } from '@/utils/notification' import { type AIChatQSData, AIChatQSDataTypeEnum, type AIToolResult, type SessionRenderContent } from './aiRender' import { aiAgentLogEmitter } from './AIAgentLogEmitter' @@ -63,6 +64,8 @@ const genAIAgentChatData = (): AIAgentChatData => { grpcOffset: 0, + timelineBeforeId: 0, + httpRunTimeIDs: [], riskRunTimeIDs: [], aiPerfData: { @@ -917,6 +920,95 @@ export class ChatMultiSessionController { }) } + /** timeline 历史单次拉取条数 */ + private static readonly TIMELINE_PAGE_LIMIT = 200 + + /** + * 拉取 timeline 历史(grpcQueryAIEvent 按 NodeId=timeline_item 过滤,BeforeId 游标分页)。 + * 拉回后 reverse 为时间正序,前插到 store.reActTimelines;更新 timelineBeforeId 游标。 + * @returns 是否还有更旧历史(Events.length === LIMIT) + */ + public async loadTimelineHistory(sessionId: string): Promise { + const { rawData, store } = this.ensureSession(sessionId) + const state = store.getState() + // 置 loading(驱动 TimelineCard 的 YakitSpin) + store.getState().updateState({ + requestHistoryState: { ...state.requestHistoryState, timelinesLoading: true }, + }) + try { + const request: AIEventQueryRequest = { + Filter: { SessionID: sessionId, NodeId: ['timeline_item'] }, + Pagination: { + Page: 1, + Limit: ChatMultiSessionController.TIMELINE_PAGE_LIMIT, + OrderBy: 'created_at', + Order: 'desc', + }, + } + if (rawData.timelineBeforeId > 0) { + request.Pagination!.BeforeId = rawData.timelineBeforeId + } + const { Events, Total } = await grpcQueryAIEvent(request, true) + if (Number(Total) === 0) return false + + // 更新游标为最后一条(最旧)的 ID + rawData.timelineBeforeId = Number(Events[Events.length - 1].ID) + // 解析为 TimelineItem,reverse 为时间正序(旧→新),前插到已有列表头部 + const timelineItems: AIAgentGrpcApi.TimelineItem[] = Events.map((item) => { + const ipcContent = Uint8ArrayToString(item.Content) || '' + return JSON.parse(ipcContent) as AIAgentGrpcApi.TimelineItem + }).reverse() + // 前插:旧批次插到已有列表前面 + store.getState().setReActTimelines([...timelineItems, ...state.reActTimelines]) + return Events.length === ChatMultiSessionController.TIMELINE_PAGE_LIMIT + } catch { + return false + } finally { + const curState = store.getState() + store.getState().updateState({ + requestHistoryState: { ...curState.requestHistoryState, timelinesLoading: false }, + }) + } + } + + /** 是否还有更旧 timeline 历史可加载 */ + public hasMoreTimeline(sessionId: string): boolean { + const { rawData } = this.ensureSession(sessionId) + // 游标为 0 表示未拉过 → 可加载;上次拉满一页 → 可加载 + // 简化:只要没拉到过空结果就允许尝试(loadTimelineHistory 内部会判 Total===0) + return rawData.timelineBeforeId >= 0 + } + + /** + * 拉取文件系统历史(grpcQueryAIEvent 按 EventType=filesystem_pin_* 过滤,Limit=-1 全量)。 + * 按 path 去重合并到 store.grpcFolders。无分页。 + */ + public async loadFileSystemHistory(sessionId: string) { + const { store } = this.ensureSession(sessionId) + try { + const request: AIEventQueryRequest = { + Filter: { SessionID: sessionId, EventType: ['filesystem_pin_directory', 'filesystem_pin_filename'] }, + Pagination: { Page: 1, Limit: -1, OrderBy: 'created_at', Order: 'desc' }, + } + const { Events, Total } = await grpcQueryAIEvent(request, true) + if (Total === 0) return + + const files: AIFileSystemPin[] = Events.map((item) => { + const ipcContent = Uint8ArrayToString(item.Content) || '' + const { path } = JSON.parse(ipcContent) as AIAgentGrpcApi.FileSystemPin + return { path, isFolder: item.Type === 'filesystem_pin_directory' } + }) + // 本批次去重 + const filterFiles: AIFileSystemPin[] = [...new Map(files.map((item) => [item.path, item])).values()] + // 与已有 grpcFolders 合并去重 + const state = store.getState() + const merged = [...new Map([...filterFiles, ...state.grpcFolders].map((item) => [item.path, item])).values()] + store.getState().setGrpcFolders(merged) + } catch { + // 持久化失败不打断主流程 + } + } + /** 会话建立成功后, 需要做的额外操作 */ private handleSessionStartSuccess(sessionId: string) { const { store, meta } = this.ensureSession(sessionId) @@ -939,6 +1031,10 @@ export class ChatMultiSessionController { // 如果任务规划运行态有数据,则置空 store.getState().updateState({ taskStatus: cloneDeep(DefaultTaskPlanStatus) }) + + // 拉取 timeline 历史(首批)+ 文件系统历史(全量),不阻塞建连主流程 + void this.loadTimelineHistory(sessionId) + void this.loadFileSystemHistory(sessionId) } /** 关闭恢复态 loading(hydrate 完成或 recovery 结束时调用) */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index c3a82ce1c7..19f736979b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -626,7 +626,11 @@ export interface ChatStoreState { ) => void updateFolders: (info: AIFileSystemPin) => void + /** 批量替换 grpcFolders(历史恢复用) */ + setGrpcFolders: (folders: AIFileSystemPin[]) => void updateTimeLineItem: (item: AIAgentGrpcApi.TimelineItem) => void + /** 批量替换 reActTimelines(历史恢复用) */ + setReActTimelines: (timelines: AIAgentGrpcApi.TimelineItem[]) => void /** 更新http数据 */ updateHttpData: () => void diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index 0662839f64..5dcbc43092 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -90,10 +90,20 @@ export const createChatStore = (options?: CreateChatStoreOptions) => { const isExist = state.grpcFolders.find((item) => item.path === info.path) if (!isExist) state.grpcFolders.push(info) }), + /** 批量替换 grpcFolders(历史恢复用) */ + setGrpcFolders: (folders) => + set((state) => { + state.grpcFolders = folders + }), updateTimeLineItem: (item) => set((state) => { state.reActTimelines.push(item) }), + /** 批量替换 reActTimelines(历史恢复用) */ + setReActTimelines: (timelines) => + set((state) => { + state.reActTimelines = timelines + }), updateHttpData: () => { set((state) => { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/useLoadHistory.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/useLoadHistory.ts index a7a58030ef..21934225da 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/useLoadHistory.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/useLoadHistory.ts @@ -1,5 +1,5 @@ import { useMemoizedFn } from 'ahooks' -import { useEffect, useLayoutEffect, useRef, useState } from 'react' +import { useEffect, useRef, useState } from 'react' const PREPEND_OFFSET = 1000000 From 11f78547e5694aa1e93cd3b4455ed41fb08999ba Mon Sep 17 00:00:00 2001 From: hubo Date: Wed, 29 Jul 2026 16:30:51 +0800 Subject: [PATCH 068/101] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20timeline=20?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E4=B8=8E=E5=AE=9E=E6=97=B6=E6=B5=81=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E9=87=8D=E5=A4=8D/=E4=B8=A2=E5=A4=B1=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20-=20setReActTimelines/setGrpcFolders=20=E5=90=88?= =?UTF-8?q?=E5=B9=B6=E5=8E=BB=E9=87=8D=E4=B8=8B=E6=B2=89=E5=88=B0=20store?= =?UTF-8?q?=20=E7=9A=84=20set=20=E5=9B=9E=E8=B0=83=EF=BC=8C=20=20=20?= =?UTF-8?q?=E9=81=BF=E5=85=8D=20Controller=20=E7=94=A8=20stale=20state=20?= =?UTF-8?q?=E6=8B=BC=E6=8E=A5=E5=AF=BC=E8=87=B4=20await=20=E6=9C=9F?= =?UTF-8?q?=E9=97=B4=E5=AE=9E=E6=97=B6=E6=8E=A8=E5=85=A5=E7=9A=84=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E8=A2=AB=E8=A6=86=E7=9B=96=E4=B8=A2=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/ChatMultiSessionController.ts | 13 ++++++------- .../main/src/pages/ai-re-act/hooks/chatStore.ts | 16 ++++++++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 17f29c641b..9a548d7192 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -953,13 +953,14 @@ export class ChatMultiSessionController { // 更新游标为最后一条(最旧)的 ID rawData.timelineBeforeId = Number(Events[Events.length - 1].ID) - // 解析为 TimelineItem,reverse 为时间正序(旧→新),前插到已有列表头部 + // 解析为 TimelineItem,reverse 为时间正序(旧→新) const timelineItems: AIAgentGrpcApi.TimelineItem[] = Events.map((item) => { const ipcContent = Uint8ArrayToString(item.Content) || '' return JSON.parse(ipcContent) as AIAgentGrpcApi.TimelineItem }).reverse() - // 前插:旧批次插到已有列表前面 - store.getState().setReActTimelines([...timelineItems, ...state.reActTimelines]) + // 前插合并去重:在 store set 回调里拿最新 state 合并, + // 避免 await 期间实时流推入的新数据被 stale state 覆盖丢失 + store.getState().setReActTimelines(timelineItems) return Events.length === ChatMultiSessionController.TIMELINE_PAGE_LIMIT } catch { return false @@ -1000,10 +1001,8 @@ export class ChatMultiSessionController { }) // 本批次去重 const filterFiles: AIFileSystemPin[] = [...new Map(files.map((item) => [item.path, item])).values()] - // 与已有 grpcFolders 合并去重 - const state = store.getState() - const merged = [...new Map([...filterFiles, ...state.grpcFolders].map((item) => [item.path, item])).values()] - store.getState().setGrpcFolders(merged) + // 合并去重交给 store(在 set 回调里拿最新 state,避免 stale state 丢失实时数据) + store.getState().setGrpcFolders(filterFiles) } catch { // 持久化失败不打断主流程 } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index 5dcbc43092..d589fa2bb8 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -90,19 +90,27 @@ export const createChatStore = (options?: CreateChatStoreOptions) => { const isExist = state.grpcFolders.find((item) => item.path === info.path) if (!isExist) state.grpcFolders.push(info) }), - /** 批量替换 grpcFolders(历史恢复用) */ + /** + * 批量合并文件系统 pin 记录,按 path 去重。 + * 在 set 回调里拿最新 state 合并,避免 Controller 用 stale state 拼接。 + */ setGrpcFolders: (folders) => set((state) => { - state.grpcFolders = folders + state.grpcFolders = [...new Map([...folders, ...state.grpcFolders].map((item) => [item.path, item])).values()] }), updateTimeLineItem: (item) => set((state) => { state.reActTimelines.push(item) }), - /** 批量替换 reActTimelines(历史恢复用) */ + /** + * 批量前插 timeline 历史并按 id 去重。 + * 在 set 回调里拿最新 state 合并,避免 Controller 用 stale state 拼接导致丢失实时数据。 + */ setReActTimelines: (timelines) => set((state) => { - state.reActTimelines = timelines + const existingIds = new Set(state.reActTimelines.map((t) => t.id)) + const deduped = timelines.filter((t) => !existingIds.has(t.id)) + state.reActTimelines = [...deduped, ...state.reActTimelines] }), updateHttpData: () => { From d4d97d79798514329259c8dcc19eb2eb0d0fcc1c Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Wed, 29 Jul 2026 15:16:28 +0800 Subject: [PATCH 069/101] =?UTF-8?q?fix:=20=E5=8A=A0=E5=9B=BA=20AI=20?= =?UTF-8?q?=E4=BC=9A=E8=AF=9D=E6=8C=81=E4=B9=85=E5=8C=96=E5=86=99=E9=98=9F?= =?UTF-8?q?=E5=88=97=E4=B8=8E=20structured=20=E6=97=A5=E5=BF=97=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 render/content/reference 写入串行排队,删会话前先 drain 再 delete,避免迟到 put 写回孤儿行;reference 统一走 setSessionReferencePersist;structured Log 仅在 Type 匹配时安全 JSON.parse,避免纯文本 Content 误抛错打断分发。 --- .../hooks/ChatMultiSessionController.ts | 122 +++++++++++------- .../hooks/grpcStreamHandler/aiStream.ts | 5 +- .../hooks/persist/contentPersistHelper.ts | 61 +++++---- 3 files changed, 117 insertions(+), 71 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 9a548d7192..1cf8366953 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -35,7 +35,11 @@ import moment from 'moment' import type { YakitRouteType } from '@/enums/yakitRoute' import { grpcQueryAIEvent } from '@/pages/ai-agent/grpc' import aiChatPersistStore from './persist/aiChatPersistStore' -import { persistIndependentItem, persistToolResultIfTerminal } from './persist/contentPersistHelper' +import { + drainSessionContentWrites, + persistIndependentItem, + persistToolResultIfTerminal, +} from './persist/contentPersistHelper' const { ipcRenderer } = window.require('electron') @@ -423,19 +427,50 @@ export class ChatMultiSessionController { } // #region IndexedDB 持久化门面(薄封装 aiChatPersistStore,错误兜底不抛穿 UI) + /** + * sessionRender 写串行链:同一 session 的渲染树写排队执行。 + * 作用有二: + * 1. 保证 hydrate 快照写与随后的流式 flush 写不并发交叠(后写覆盖先写语义成立) + * 2. teardownDisposedSession 删除 IDB 前 drainRenderWrites 排干在飞写, + * 避免 delete 后迟到的 put 又写回孤儿行 + */ + private renderWriteChains = new Map>() + private enqueueRenderWrite(sessionId: string, task: () => Promise): Promise { + const next = (this.renderWriteChains.get(sessionId) || Promise.resolve()).then(task, task) + this.renderWriteChains.set( + sessionId, + next.finally(() => { + if (this.renderWriteChains.get(sessionId) === next) { + this.renderWriteChains.delete(sessionId) + } + }), + ) + return next + } + /** 排干该 session 所有在飞的渲染树写;resolve 时链已排空 */ + private drainRenderWrites(sessionId: string): Promise { + return this.renderWriteChains.get(sessionId)?.catch(() => {}) || Promise.resolve() + } + /** 从 sessionOwnerMap 取 source,兜底 'ai' */ private resolvePersistSource(sessionId: string): AISource { return this.sessionOwnerMap.get(sessionId)?.source || AISourceEnum.aiAgent } /** 保存会话渲染树(element)和grpcOffset */ - private async persistSetSessionRender(sessionId: string, content: SessionRenderContent, grpcOffset?: number) { - try { - const offset = grpcOffset ?? this.rawDataPool.get(sessionId)?.grpcOffset ?? 0 - await aiChatPersistStore.setSessionRender(sessionId, this.resolvePersistSource(sessionId), content, offset) - } catch { - // 持久化失败不打断主流程 - } + private persistSetSessionRender( + sessionId: string, + content: SessionRenderContent, + grpcOffset?: number, + ): Promise { + // 同步计算 offset / source 并闭包捕获,避免 enqueue 后 session 被 teardown 导致取不到 + const offset = grpcOffset ?? this.rawDataPool.get(sessionId)?.grpcOffset ?? 0 + const source = this.resolvePersistSource(sessionId) + return this.enqueueRenderWrite(sessionId, () => + aiChatPersistStore.setSessionRender(sessionId, source, content, offset).catch(() => { + // 持久化失败不打断主流程 + }), + ) } /** 获取会话渲染树和grpcOffset */ private async persistGetSessionRender(sessionId: string) { @@ -1020,7 +1055,7 @@ export class ChatMultiSessionController { // 获取最新记忆列表数据, 并注册轮询定时器 this.requestMessage(sessionId, { IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_MEMORY_CONTEXT }) - if (meta.memoryPollingTimer) clearTimeout(meta.memoryPollingTimer) + if (meta.memoryPollingTimer) clearInterval(meta.memoryPollingTimer) meta.memoryPollingTimer = setInterval(() => { this.requestMessage(sessionId, { IsSyncMessage: true, @@ -1094,7 +1129,6 @@ export class ChatMultiSessionController { taskElements: [...state.taskChat.elements], } await this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) - this.finishSessionRestoreLoading(sessionId) } } catch { this.finishSessionRestoreLoading(sessionId) @@ -1107,33 +1141,33 @@ export class ChatMultiSessionController { if (!this.readyChannels.has(sessionId)) return let ipcContent = Uint8ArrayToString(res.Content) || '' - const parsed = JSON.parse(ipcContent) // console.log('handleGrpcOutputEvent--', sessionId, res, ipcContent) - if ( - res.Type === 'structured' && - parsed && - typeof parsed === 'object' && - 'level' in parsed && - 'message' in parsed - ) { - // 日志类型数据 - const data = parsed as AIAgentGrpcApi.Log - aiAgentLogEmitter.dispatch({ - session: sessionId, - type: 'log', - Timestamp: res.Timestamp, - log: data, - }) - return - } else { - // 所有数据,均抄送一份到日志中 - aiAgentLogEmitter.dispatch({ - session: sessionId, - type: 'log', - Timestamp: res.Timestamp, - log: { level: 'log', message: ipcContent }, - }) + // 仅 structured 才尝试按 Log 结构解析;其它 type 的 Content 常为纯文本,不可无条件 JSON.parse + if (res.Type === 'structured') { + try { + const parsed = JSON.parse(ipcContent) + if (parsed && typeof parsed === 'object' && 'level' in parsed && 'message' in parsed) { + // 日志类型数据 + const data = parsed as AIAgentGrpcApi.Log + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: res.Timestamp, + log: data, + }) + return + } + } catch { + // 非合法 JSON / 非 Log 结构,走下方通用抄送 + } } + // 所有数据,均抄送一份到日志中 + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: res.Timestamp, + log: { level: 'log', message: ipcContent }, + }) const { store, rawData, request, meta } = this.ensureSession(sessionId) @@ -1262,12 +1296,7 @@ export class ChatMultiSessionController { return } } catch (error) { - aiAgentLogEmitter.dispatch({ - session: sessionId, - type: 'log', - Timestamp: res.Timestamp, - log: { level: 'try-error', message: `${res.Type}-${res.NodeId}: ${error}` }, - }) + console.error('handleGrpcOutputEvent error', error) } } @@ -1387,7 +1416,12 @@ export class ChatMultiSessionController { } if (deletePersist) { - aiChatPersistStore.deleteSessionPersist(sessionId).catch(() => {}) + // 先排干该 session 所有在飞的 IDB 写(render / content / reference), + // 再发 delete 事务,确保 delete 排在所有 put 之后,避免 delete 后迟到的 put 又写回孤儿行。 + // 内存池已在上文清空,此处只对 IDB 排干;队列本身在写完成 / finally 时自清。 + Promise.all([this.drainRenderWrites(sessionId), drainSessionContentWrites(sessionId)]) + .then(() => aiChatPersistStore.deleteSessionPersist(sessionId)) + .catch(() => {}) } } @@ -1487,14 +1521,14 @@ export class ChatMultiSessionController { if (meta && onEnd) { meta.onEnd = onEnd } + // 等真实 -end;超时则手动 handleSessionEnd,避免监听泄漏 / onEnd 挂死 + this.armSessionEndFallback(session) ipcRenderer.invoke('cancel-ai-re-act', session).catch(() => {}) const store = this.storePool.get(session) if (store) { store.getState().updateState({ execute: false, casualLoading: false, casualTitle: '会话关闭中...' }) } if (meta) this.closeSessionTimers(meta) - // 等真实 -end;超时则手动 handleSessionEnd,避免监听泄漏 / onEnd 挂死 - this.armSessionEndFallback(session) } } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index e92e1b870b..cdc87df9f6 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -6,11 +6,10 @@ import { AIChatQSDataTypeEnum, type AIChatQSData } from '../aiRender' import { AIStreamContentType, convertNodeIdToVerbose } from '../defaultConstant' import { aiAgentLogEmitter } from '../AIAgentLogEmitter' import { v4 as uuidv4 } from 'uuid' -import aiChatPersistStore from '../persist/aiChatPersistStore' import { - appendReferenceToContent, persistIndependentItem, persistToolResultIfTerminal, + setSessionReferencePersist, upsertSessionContent, } from '../persist/contentPersistHelper' @@ -405,7 +404,7 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { // 收数时自动生成 refToken,立刻落表3;内存只挂 token,不存完整 payload const refToken = uuidv4() - aiChatPersistStore.setSessionReference(sessionId, refToken, data).catch(() => {}) + setSessionReferencePersist(sessionId, refToken, data) if (chatData) { chatData.reference = [...(chatData.reference || []), refToken] diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/contentPersistHelper.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/contentPersistHelper.ts index 2899068c8e..0c1562af8d 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/contentPersistHelper.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/persist/contentPersistHelper.ts @@ -1,9 +1,14 @@ import cloneDeep from 'lodash/cloneDeep' import { AIChatQSDataTypeEnum, type AIChatQSData } from '../aiRender' +import type { AIAgentGrpcApi } from '../grpcApi' import aiChatPersistStore from './aiChatPersistStore' import type { SessionContentUpdater } from './type' -/** 同一 sessionId::token 的串行写队列,避免异步 put 未完成又来更新导致丢写 */ +/** + * 同一 sessionId::token 的串行写队列,避免异步 put 未完成又来更新导致丢写。 + * 同时承载 sessionContent 与 sessionReference 两类写(主键结构一致 [sessionId, token]), + * drainSessionContentWrites 按 sessionId 前缀一并排干。 + */ const contentWriteChains = new Map>() const contentKey = (sessionId: string, token: string) => `${sessionId}::${token}` @@ -69,39 +74,47 @@ export const deletePersistedContent = (sessionId: string, token: string): Promis }) } +/** TOOL_RESULT 终态:success / failed / user_cancelled */ +export const isToolResultTerminalStatus = (status: string | undefined): boolean => { + return status === 'success' || status === 'failed' || status === 'user_cancelled' +} + +/** 工具已终态时追加写正文;未终态不落库 */ +export const persistToolResultIfTerminal = (sessionId: string, toolResult: AIChatQSData): Promise | void => { + if (toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) return + if (!isToolResultTerminalStatus(toolResult.data.tool.status)) return + return upsertSessionContent(sessionId, toolResult.id, toolResult) +} + /** - * 向已落库正文的 reference 追加 refToken。 - * 无旧记录时跳过(stream 尚未 end 时参考资料只挂内存,等 end 一并写入)。 + * 写入/覆盖参考资料(入队串行,纳入 session 排干)。 + * 复用 contentWriteChains(key=sessionId::refToken),drainSessionContentWrites 按 sessionId 前缀一并排干。 */ -export const appendReferenceToContent = ( +export const setSessionReferencePersist = ( sessionId: string, - contentToken: string, refToken: string, + data: AIAgentGrpcApi.ReferenceMaterialPayload, ): Promise => { - return enqueueContentWrite(sessionId, contentToken, async () => { + return enqueueContentWrite(sessionId, refToken, async () => { try { - const old = await aiChatPersistStore.getSessionContent(sessionId, contentToken) - if (!old) return - const reference = [...(old.reference || [])] - if (!reference.includes(refToken)) { - reference.push(refToken) - } - const next: AIChatQSData = { ...clonePersistableContent(old), reference } - await aiChatPersistStore.setSessionContent(sessionId, contentToken, () => next) + await aiChatPersistStore.setSessionReference(sessionId, refToken, data) } catch { // 持久化失败不打断主流程 } }) } -/** TOOL_RESULT 终态:success / failed / user_cancelled */ -export const isToolResultTerminalStatus = (status: string | undefined): boolean => { - return status === 'success' || status === 'failed' || status === 'user_cancelled' -} - -/** 工具已终态时追加写正文;未终态不落库 */ -export const persistToolResultIfTerminal = (sessionId: string, toolResult: AIChatQSData): Promise | void => { - if (toolResult.type !== AIChatQSDataTypeEnum.TOOL_RESULT) return - if (!isToolResultTerminalStatus(toolResult.data.tool.status)) return - return upsertSessionContent(sessionId, toolResult.id, toolResult) +/** + * 排干某 session 所有在飞的正文/参考资料写,resolve 时该 session 的 token 写队列已排空。 + * 整 session 删除前调用,确保 delete 事务排在所有 put 之后,避免 delete 后迟到的 put 又写回孤儿行。 + */ +export const drainSessionContentWrites = (sessionId: string): Promise => { + const prefix = `${sessionId}::` + const chains: Promise[] = [] + for (const [key, chain] of contentWriteChains) { + if (key.startsWith(prefix)) { + chains.push(chain.catch(() => {})) + } + } + return Promise.all(chains) } From 75e72d0cfb8fe4faf03f22cd4981c9be3197cfb8 Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Wed, 29 Jul 2026 16:48:48 +0800 Subject: [PATCH 070/101] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=8D=A1=E7=89=87=E6=9C=AA=E4=B8=8A=E6=A0=91=E4=B8=8E?= =?UTF-8?q?=20grpc=20=E4=BA=8B=E4=BB=B6=E5=88=86=E5=8F=91=E6=BC=8F?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 工具结果路径此前只 incrementNodeVersion、从不 dispatchStreamingNode,未挂树时 UI 不出现;新增 ensureToolResultOnUI 并接到 tool/stream/流量更新。同时调整 handleGrpcOutputEvent:业务 handler 优先于纯日志解析,并补上 plan_exec_tasks 映射。 --- .../hooks/ChatMultiSessionController.ts | 80 +++++++++++-------- .../hooks/grpcStreamHandler/aiOther.ts | 3 +- .../hooks/grpcStreamHandler/aiStream.ts | 15 +++- .../hooks/grpcStreamHandler/aiToolResult.ts | 64 ++++++++++----- 4 files changed, 104 insertions(+), 58 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 1cf8366953..262bb33654 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -1142,32 +1142,6 @@ export class ChatMultiSessionController { let ipcContent = Uint8ArrayToString(res.Content) || '' // console.log('handleGrpcOutputEvent--', sessionId, res, ipcContent) - // 仅 structured 才尝试按 Log 结构解析;其它 type 的 Content 常为纯文本,不可无条件 JSON.parse - if (res.Type === 'structured') { - try { - const parsed = JSON.parse(ipcContent) - if (parsed && typeof parsed === 'object' && 'level' in parsed && 'message' in parsed) { - // 日志类型数据 - const data = parsed as AIAgentGrpcApi.Log - aiAgentLogEmitter.dispatch({ - session: sessionId, - type: 'log', - Timestamp: res.Timestamp, - log: data, - }) - return - } - } catch { - // 非合法 JSON / 非 Log 结构,走下方通用抄送 - } - } - // 所有数据,均抄送一份到日志中 - aiAgentLogEmitter.dispatch({ - session: sessionId, - type: 'log', - Timestamp: res.Timestamp, - log: { level: 'log', message: ipcContent }, - }) const { store, rawData, request, meta } = this.ensureSession(sessionId) @@ -1177,6 +1151,15 @@ export class ChatMultiSessionController { store.getState().updateStateCount('syncIDUpdate') } + const mirrorToLogWindow = () => { + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: res.Timestamp, + log: { level: 'log', message: ipcContent }, + }) + } + if (res.Type === 'pong') { // 如果返回的pong没有值,但是pingSyncID有值,说明该条消息已经过期 if (!res.SyncID && meta.pingSyncID) return @@ -1240,6 +1223,7 @@ export class ChatMultiSessionController { return } + // 先解析业务 funcKey(对齐旧 useChatIPC:业务 NodeId 优先于纯日志) let funcKey = res.Type if ( res.Type === 'structured' && @@ -1254,6 +1238,7 @@ export class ChatMultiSessionController { 'stream-finished', 'capability_inventory', 'react_task_created', + 'plan_exec_tasks', ].includes(res.NodeId) ) { funcKey = res.NodeId @@ -1262,11 +1247,15 @@ export class ChatMultiSessionController { } else if (res.Type === 'report_finish' && res.NodeId === 'report-finish') { funcKey = res.NodeId } else if (res.Type === 'structured' && res.NodeId === 'system') { - const data = JSON.parse(ipcContent) || '' - if (data && typeof data === 'object' && data?.type === 'push_task') { - funcKey = 'push_task' - } else if (data && typeof data === 'object' && data?.type === 'pop_task') { - funcKey = 'pop_task' + try { + const data = JSON.parse(ipcContent) || '' + if (data && typeof data === 'object' && data?.type === 'push_task') { + funcKey = 'push_task' + } else if (data && typeof data === 'object' && data?.type === 'pop_task') { + funcKey = 'pop_task' + } + } catch { + // system 非合法 JSON 时保持 funcKey=structured } } else if (res.Type === 'perception' && res.NodeId === 'perception') { funcKey = 'perception' @@ -1277,7 +1266,35 @@ export class ChatMultiSessionController { } else if (res.Type === 'detached_plan_require' && res.NodeId === 'detached-plan') { funcKey = res.Type } + const handleFunc = grpcAIMessageHandlers[funcKey || ''] + + // 纯日志:structured + Log 结构 + 无业务 handler;不可无条件 JSON.parse(stream 等为纯文本) + if (!handleFunc && res.Type === 'structured') { + try { + const parsed = JSON.parse(ipcContent) + if ( + parsed && + typeof parsed === 'object' && + typeof (parsed as AIAgentGrpcApi.Log).level === 'string' && + typeof (parsed as AIAgentGrpcApi.Log).message === 'string' + ) { + aiAgentLogEmitter.dispatch({ + session: sessionId, + type: 'log', + Timestamp: res.Timestamp, + log: parsed as AIAgentGrpcApi.Log, + }) + return + } + } catch { + // 非合法 JSON / 非 Log 结构,走下方通用抄送 + } + } + + // 所有业务数据,均抄送一份到日志中 + mirrorToLogWindow() + if (handleFunc) { handleFunc({ sessionId, @@ -1293,7 +1310,6 @@ export class ChatMultiSessionController { pushLogToOtherWindow({ sessionId: sessionId, Timestamp: res.Timestamp, ...log }) }, }) - return } } catch (error) { console.error('handleGrpcOutputEvent error', error) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts index 0bc40bc86b..c53b27d7a7 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiOther.ts @@ -11,6 +11,7 @@ import { persistToolResultIfTerminal, deletePersistedContent, } from '../persist/contentPersistHelper' +import { ensureToolResultOnUI } from './aiToolResult' import type { AIAgentChatMetaData } from '@/pages/ai-agent/type/aiChat' const handleHttpFuzzRequestChange: AIMessageHandler = (request) => { @@ -421,7 +422,7 @@ const handleTrafficCount: AIMessageHandler = (request) => { break } if (!update) return - store.getState().incrementNodeVersion(toolResult.id, 'item') + ensureToolResultOnUI(request, toolResult) // 流量/风险计数:仅工具已终态时追加写正文 persistToolResultIfTerminal(request.sessionId, toolResult) } diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts index cdc87df9f6..f9cce5bfe8 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiStream.ts @@ -12,6 +12,7 @@ import { setSessionReferencePersist, upsertSessionContent, } from '../persist/contentPersistHelper' +import { ensureToolResultOnUI } from './aiToolResult' /** 生成stream_group组数据 */ const genStreamGroupData = ( @@ -132,6 +133,8 @@ const handleStreamStart: AIMessageHandler = (requestInfo) => { }) toolResult.data.stream.EventUUID = event_writer_id toolResult.data.type = 'stream' + // stdout 流开始:工具卡片首次上树(create 阶段故意不上屏) + if (!res.IsSync) ensureToolResultOnUI(requestInfo, toolResult) return } @@ -374,7 +377,7 @@ const handleStreamFinished: AIMessageHandler = (requestInfo) => { ? '...' + toolForStreamData.data.content.slice(-25600) + '...' : toolForStreamData.data.content toolResult.data.tool.toolStdoutContent = { content: displayContent, isShowAll } - store.getState().incrementNodeVersion(toolResult.id, 'item') + ensureToolResultOnUI(requestInfo, toolResult) // stdout 流结束:落库该 STREAM;若工具已终态则同步刷新 TOOL_RESULT upsertSessionContent(requestInfo.sessionId, toolForStreamData.id, toolForStreamData) persistToolResultIfTerminal(requestInfo.sessionId, toolResult) @@ -425,9 +428,13 @@ const handleReferenceMaterial: AIMessageHandler = (requestInfo) => { store.getState().incrementNodeVersion(chatData.id, 'task') } else if (chatData.type === AIChatQSDataTypeEnum.STREAM) { // 属于stream类型数据,但未在UI上渲染 - if (toolResult && isToolStdoutStream(chatData.data.NodeId)) { - // 特殊情况,更新stdout流对应的工具执行结果卡片UI - store.getState().incrementNodeVersion(toolResult.id, 'item') + if ( + toolResult && + toolResult.type === AIChatQSDataTypeEnum.TOOL_RESULT && + isToolStdoutStream(chatData.data.NodeId) + ) { + // 特殊情况,更新stdout流对应的工具执行结果卡片UI(未挂树则首次挂树) + ensureToolResultOnUI(requestInfo, toolResult) } else { // 触发UI渲染 store.getState().dispatchStreamingNode({ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts index 55cfcc5eba..5982c8e8a4 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiToolResult.ts @@ -1,14 +1,39 @@ -import type { AIMessageHandler } from '../type' +import type { AIMessageHandler, AIMessageHandlerParams } from '../type' import type { AIAgentGrpcApi } from '../grpcApi' import { Uint8ArrayToString } from '@/utils/str' import { genBaseAIChatData, generateTaskNodeDataID } from '../utils' -import { AIChatQSDataTypeEnum, type AIToolResult } from '../aiRender' +import { AIChatQSDataTypeEnum, type AIChatQSData, type AIToolResult } from '../aiRender' import cloneDeep from 'lodash/cloneDeep' import { DefaultAIToolResult, DefaultToolResultSummary } from '../defaultConstant' import { persistToolResultIfTerminal, upsertSessionContent } from '../persist/contentPersistHelper' +/** + * 工具卡片首次上树用 dispatchStreamingNode;已在 items 里则只 bump renderNum。 + * 全文件此前只调 incrementNodeVersion,未挂树时 UI 永远不出现。 + */ +export const ensureToolResultOnUI = ( + requestInfo: Pick, + toolResult: Extract, +) => { + const { res, chatType, store } = requestInfo + if (store.getState().items[toolResult.id]) { + store.getState().incrementNodeVersion(toolResult.id, 'item') + return + } + store.getState().dispatchStreamingNode({ + chatType, + parentTaskId: toolResult.TaskId, + node: { + token: toolResult.id, + kind: 'item', + type: toolResult.type, + isHistory: res.IsSync, + }, + }) +} + const handleToolCallStart: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta } = requestInfo + const { res, chatType, store, rawData } = requestInfo if (res.Type !== 'tool_call_start') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -44,7 +69,7 @@ const handleToolCallStart: AIMessageHandler = (requestInfo) => { } const handleToolCallParam: AIMessageHandler = (requestInfo) => { - const { res, store, rawData } = requestInfo + const { res, rawData } = requestInfo if (res.Type !== 'tool_call_param') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -64,17 +89,17 @@ const handleToolCallParam: AIMessageHandler = (requestInfo) => { } toolResult.data.tool.reviewParams = cloneDeep(params) - if (toolResult.data.type === 'result') store.getState().incrementNodeVersion(toolResult.id, 'item') + // 晚到 param(已 result):刷新已上树卡片;未上树则首次挂树 + if (toolResult.data.type === 'result') ensureToolResultOnUI(requestInfo, toolResult) persistToolResultIfTerminal(requestInfo.sessionId, toolResult) } const handleToolCallWatcher: AIMessageHandler = (requestInfo) => { - const { res, store, rawData } = requestInfo + const { res, rawData } = requestInfo if (res.Type !== 'tool_call_watcher') return const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, id, selectors } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCallWatcher - if (!call_tool_id || !id || !selectors || !selectors?.length) { requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) return @@ -108,12 +133,12 @@ const handleToolCallWatcher: AIMessageHandler = (requestInfo) => { if (toolResult.data.type === 'stream') { // 历史数据-该类型不出发渲染更新 if (res.IsSync) return - store.getState().incrementNodeVersion(toolResult.id, 'item') + ensureToolResultOnUI(requestInfo, toolResult) } } const handleToolCallLogDir: AIMessageHandler = (requestInfo) => { - const { res, store, rawData } = requestInfo + const { res, rawData } = requestInfo if (res.Type !== 'tool_call_log_dir') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -141,12 +166,12 @@ const handleToolCallLogDir: AIMessageHandler = (requestInfo) => { // 这里是直接使用引用设置的值,所以不需要在使用setContentMap设置回去 toolResult.data.tool.dirPath = dir_path || '' - if (toolResult.data.tool.status !== 'default') store.getState().incrementNodeVersion(toolResult.id, 'item') + if (toolResult.data.tool.status !== 'default') ensureToolResultOnUI(requestInfo, toolResult) persistToolResultIfTerminal(requestInfo.sessionId, toolResult) } const handleToolCallResult: AIMessageHandler = (requestInfo) => { - const { res, store, rawData, meta } = requestInfo + const { res, rawData, meta } = requestInfo if (!['tool_call_user_cancel', 'tool_call_done', 'tool_call_error'].includes(res.Type)) return const status = { tool_call_user_cancel: 'user_cancelled', tool_call_done: 'success', tool_call_error: 'failed' }[res.Type] || @@ -155,7 +180,6 @@ const handleToolCallResult: AIMessageHandler = (requestInfo) => { const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, ...rest } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall - if (!call_tool_id) { requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) return @@ -193,17 +217,16 @@ const handleToolCallResult: AIMessageHandler = (requestInfo) => { meta.toolStderrStreamData.delete(call_tool_id) } - store.getState().incrementNodeVersion(toolResult.id, 'item') + ensureToolResultOnUI(requestInfo, toolResult) upsertSessionContent(requestInfo.sessionId, toolResult.id, toolResult) } const handleToolCallSummary: AIMessageHandler = (requestInfo) => { - const { res, store, rawData, meta } = requestInfo + const { res, rawData, meta } = requestInfo if (res.Type !== 'tool_call_summary') return const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, summary } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall - if (!call_tool_id) { requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) return @@ -234,13 +257,13 @@ const handleToolCallSummary: AIMessageHandler = (requestInfo) => { meta.toolStderrStreamData.delete(call_tool_id) } if (statusInfo !== 'default') { - store.getState().incrementNodeVersion(toolResult.id, 'item') + ensureToolResultOnUI(requestInfo, toolResult) } persistToolResultIfTerminal(requestInfo.sessionId, toolResult) } const handleToolCallStatus: AIMessageHandler = (requestInfo) => { - const { res, store, rawData } = requestInfo + const { res, rawData } = requestInfo if (res.Type !== 'tool_call_status') return const ipcContent = Uint8ArrayToString(res.Content) || '' @@ -263,17 +286,16 @@ const handleToolCallStatus: AIMessageHandler = (requestInfo) => { if (toolResult.data.isProcessingParams === isProcessingParams) return toolResult.data.isProcessingParams = status === 'processing_params' - store.getState().incrementNodeVersion(toolResult.id, 'item') + ensureToolResultOnUI(requestInfo, toolResult) persistToolResultIfTerminal(requestInfo.sessionId, toolResult) } const handleToolCallReason: AIMessageHandler = (requestInfo) => { - const { res, store, rawData } = requestInfo + const { res, rawData } = requestInfo if (res.Type !== 'tool_call_reason') return const ipcContent = Uint8ArrayToString(res.Content) || '' const { call_tool_id, reason } = JSON.parse(ipcContent) as AIAgentGrpcApi.AIToolCall - if (!call_tool_id) { requestInfo.pushLog({ level: 'error', message: `${res.Type}数据异常, ${ipcContent}` }) return @@ -289,7 +311,7 @@ const handleToolCallReason: AIMessageHandler = (requestInfo) => { } toolResult.data.tool.reason = reason || '' - if (toolResult.data.type !== 'create') store.getState().incrementNodeVersion(toolResult.id, 'item') + if (toolResult.data.type !== 'create') ensureToolResultOnUI(requestInfo, toolResult) persistToolResultIfTerminal(requestInfo.sessionId, toolResult) } From a6ba796ef3c39095cd30a50b58ca9463f1f3cb7e Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Wed, 29 Jul 2026 16:34:34 +0800 Subject: [PATCH 071/101] =?UTF-8?q?perf:=E5=AD=90=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E7=9A=84item=E4=B8=AD=E7=9A=84renderNum?= =?UTF-8?q?=E4=BB=8Eprops=E4=BC=A0=EF=BC=9B=20perf:=E5=AD=90=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E7=9A=84item=E7=BB=84=E4=BB=B6=E4=B8=8E=E4=B8=BB?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E7=8B=AC=E7=AB=8B=EF=BC=9B=20perf:=E4=B8=BB?= =?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=88=A0=E9=99=A4=E4=B8=80=E4=BA=9Bwrapper?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=20perf:=E4=BB=BB=E5=8A=A1=E5=AD=90=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E5=88=9D=E6=AC=A1=E6=89=93=E5=BC=80=E6=85=A2=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AIConcurrentStream/AIConcurrentStream.tsx | 65 ++++++++-------- .../AITaskDefaultGroupCard.tsx | 46 +----------- .../AITaskDefaultGroupContent.tsx | 8 +- .../AITaskDefaultGroupCardHeard.tsx | 18 ++++- .../aiTaskDefaultGroupCardHeard/type.ts | 3 - .../components/AITaskDefaultGroupCard/type.ts | 12 --- .../ConcurrentStreamCardActions.tsx | 24 +----- .../AIChildWindowConcurrentStreamCard.tsx | 21 +----- ...indowConcurrentStreamCardHeard.module.scss | 74 +++++++++++++++++++ ...AIChildWindowConcurrentStreamCardHeard.tsx | 47 ++++++++++++ .../type.ts | 6 ++ .../ConcurrentStreamCardHeard.tsx | 2 - .../concurrentStreamFrame.ts | 7 ++ .../AIChildWindowConcurrentStreamContent.tsx | 4 +- .../AIChildWindowGroupStreamCard.tsx | 11 ++- .../AIChildWindowNodeItemWrapper.tsx | 26 ++++--- .../aiChildWindowNodeItemWrapper/type.ts | 7 ++ .../AIChildWindowTaskDefaultGroupCard.tsx | 18 ++--- ...indowTaskDefaultGroupCardHeard.module.scss | 62 ++++++++++++++++ ...AIChildWindowTaskDefaultGroupCardHeard.tsx | 37 ++++++++++ .../type.ts | 4 + 21 files changed, 341 insertions(+), 161 deletions(-) delete mode 100644 app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/AIChildWindowConcurrentStreamCardHeard.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/AIChildWindowConcurrentStreamCardHeard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/type.ts create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/AIChildWindowTaskDefaultGroupCardHeard.module.scss create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/AIChildWindowTaskDefaultGroupCardHeard.tsx create mode 100644 app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/type.ts diff --git a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx index d3e60302cf..22f113b59c 100644 --- a/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx +++ b/app/renderer/src/main/src/auxWindow/pages/AIConcurrentStream/AIConcurrentStream.tsx @@ -1,6 +1,5 @@ import React, { lazy, memo, startTransition, Suspense, useEffect, useMemo, useRef, useState } from 'react' import { yakitAuxWindow } from '@/services/electronBridge' -import ConcurrentStreamSkeleton from '@/auxWindow/components/ConcurrentStreamSkeleton/ConcurrentStreamSkeleton' import { type ConcurrentStreamFramePayload, isConcurrentStreamFrame, @@ -14,6 +13,7 @@ import AIConcurrentStreamContent, { } from './useContext/AIConcurrentStreamContent' import useMemoizedFn from 'ahooks/lib/useMemoizedFn' import { useDebounceFn } from 'ahooks' +import ConcurrentStreamSkeleton from '@/auxWindow/components/ConcurrentStreamSkeleton/ConcurrentStreamSkeleton' // 子卡片按需加载,避免重型卡片(AINodeItem 及其下游 review/report/fuzz 等子卡) // 全量进入 aux bundle,拉长 did-finish-load 与首次开窗耗时。 @@ -26,8 +26,6 @@ const AIChildWindowConcurrentStreamCard = lazy( import('@/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard'), ) -const { ipcRenderer } = window.require('electron') - interface AIConcurrentStreamProps { windowId: string } @@ -35,10 +33,9 @@ interface AIConcurrentStreamProps { const AIConcurrentStream: React.FC = memo(({ windowId }) => { const [frame, setFrame] = useState(null) const [contentVersion, setContentVersion] = useState(0) - // rawData/execFileRecord/childrenTokens 是否仍在通过 fetch-concurrent-stream-contents 拉取中 - const [loadingContents, setLoadingContents] = useState(false) + const [loading, setLoading] = useState(true) - // rawData/execFileRecord 用 ref 存储,更新不触发渲染; + // rawData/execFileRecord/childrenTokens 用 ref 存储,更新不触发渲染; // 组件及子组件的重渲染由 contentVersion(renderNum)驱动 const rawDataRef = useRef>(new Map()) const execFileRecordRef = useRef>(new Map()) @@ -49,21 +46,19 @@ const AIConcurrentStream: React.FC = memo(({ windowId } const applyFrame = (payload: ConcurrentStreamFramePayload) => { if (!isConcurrentStreamFrame(payload)) return + const newFrame: ConcurrentStreamFramePayload = { + ...payload, + } // 开窗时 frame 只携带轻量元数据 - // 真正的数据由下方的懒拉取 effect 通过 IPC 向主窗口请求 startTransition(() => { - const newFrame: ConcurrentStreamFramePayload = { - ...payload, - } setFrame((v) => ({ ...v, ...newFrame, renderNum: (v?.renderNum || newFrame.renderNum || 0) + 1, })) - - // 收到 frame 后,主动向主窗口拉取本次需要渲染的 rawData。 - getRawData(newFrame) }) + // 收到 frame 后,主动向主窗口拉取本次需要渲染的 rawData。 + fetchContents(newFrame) } const offInit = yakitAuxWindow.onInit((msg) => { @@ -84,25 +79,35 @@ const AIConcurrentStream: React.FC = memo(({ windowId } } }, [windowId]) - const getRawData = useDebounceFn( - (frame) => { - setLoadingContents(true) - fetchConcurrentStreamContents(frame) - .then((entries) => { - rawDataRef.current = entries.rawData - execFileRecordRef.current = entries.execFileRecord - childrenTokensRef.current = entries.childrenTokens - }) - .finally(() => { + const fetchContents = useMemoizedFn((frame) => { + setLoading(true) + fetchConcurrentStreamContents(frame) + .then((entries) => { + rawDataRef.current = entries.rawData + execFileRecordRef.current = entries.execFileRecord + childrenTokensRef.current = entries.childrenTokens + }) + .finally(() => { + setTimeout(() => { setContentVersion((v) => v + 1) - setLoadingContents(false) - }) - }, - { wait: 500, leading: true }, - ).run + setLoading(false) + }, 200) + }) + }) + + // 首次拉取立即执行;后续刷新走 500ms 去抖,合并短时间内的多次推送 + const getRawDataDebounced = useDebounceFn((frame) => fetchContents(frame), { + wait: 500, + leading: true, + }).run + const getRawData = useMemoizedFn((frame) => { + getRawDataDebounced(frame) + }) const isTaskDefaultGroup = useMemo(() => { if (!frame) return false + // 优先用 frame 随身携带的 rootType,无需等待 rawData 拉取完成 + if (frame.rootType != null) return frame.rootType === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP const root = rawDataRef.current.get(frame.token) return root?.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP }, [frame, contentVersion]) @@ -128,8 +133,8 @@ const AIConcurrentStream: React.FC = memo(({ windowId } requestRefresh, } }, []) - // isTaskDefaultGroup 需要读 rawData 判断根节点类型,必须等 rawData 拉取完成 - if (!frame || loadingContents) { + // frame 到达即可渲染卡片:rootType 已随 frame 下发,懒加载 chunk 与 rawData 拉取并行解析 + if (!frame || loading) { return } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx index ae61f15ea1..8d7b2bdce9 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx @@ -1,12 +1,9 @@ import { type FC, memo, useEffect, useState } from 'react' import classNames from 'classnames' import styles from './AITaskDefaultGroupCard.module.scss' -import { useBoolean, useCreation } from 'ahooks' +import { useBoolean } from 'ahooks' import AITaskDefaultGroupContent from './AITaskDefaultGroupContent' -import { useCurrentStore, useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' -import { useStore } from 'zustand' import AITaskDefaultGroupCardHeard from './aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard' -import { AITaskDefaultGroupCardHeardWrapperProps, AITaskDefaultGroupCardListWrapperProps } from './type' const AITaskDefaultGroupCard: FC<{ token: string @@ -22,50 +19,13 @@ const AITaskDefaultGroupCard: FC<{
- + - {expand ? : null} + {expand ? : null}
) }) export default AITaskDefaultGroupCard - -const AITaskDefaultGroupCardListWrapper: FC = memo((props) => { - const { token, setContentFocused } = props - const store = useCurrentStore() - const childrenTokens = useStore(store, (state) => state.tasks[token]?.childrenTokens || []) - - return ( - - ) -}) - -const AITaskDefaultGroupCardHeardWrapper: FC = memo((props) => { - const { token, expandToggle, expand } = props - const store = useCurrentStore() - const renderNum = useStore(store, (state) => state.tasks[token]?.renderNum) - const rawData = useCurrentRawData() - const timeStamp = useCreation(() => { - if (!rawData) return 0 - const itemData = rawData.contents.get(token) - if (!itemData) return 0 - return itemData.Timestamp || 0 - }, [renderNum]) - return ( - - ) -}) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupContent.tsx index 98dea18c99..87d428bb15 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupContent.tsx @@ -3,12 +3,16 @@ import { type FC, useEffect, useLayoutEffect } from 'react' import classNames from 'classnames' import styles from './AITaskDefaultGroupCard.module.scss' import ConcurrentStreamContent from '../ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent' +import { useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' const AITaskDefaultGroupContent: FC<{ - childrenTokens: string[] + token: string isChildWindow?: boolean onContentFocusChange?: (focused: boolean) => void -}> = ({ childrenTokens, isChildWindow, onContentFocusChange }) => { +}> = ({ token, isChildWindow, onContentFocusChange }) => { + const store = useCurrentStore() + const childrenTokens = useStore(store, (state) => state.tasks[token]?.childrenTokens || []) const { ref: contentRef, isFocus } = useClickFocus() useEffect(() => { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx index d0dbd07673..b48421c55f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard.tsx @@ -1,4 +1,5 @@ import { memo } from 'react' +import { useCreation } from 'ahooks' import { AITaskDefaultGroupCardHeardProps } from './type' import styles from './AITaskDefaultGroupCardHeard.module.scss' import { OutlineInformationcircleIcon } from '@/assets/icon/outline' @@ -6,12 +7,23 @@ import { formatTimestamp } from '@/utils/timeUtil' import ConcurrentStreamCardActions from '../../ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' +import { useCurrentStore, useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useStore } from 'zustand' const AITaskDefaultGroupCardHeard: React.FC = memo((props) => { const { t } = useI18nNamespaces(['aiAgent']) - const { isChildWindow, expandToggle, timeStamp, expand, onRefresh, token } = props + const { expandToggle, expand, token } = props + const store = useCurrentStore() + const renderNum = useStore(store, (state) => state.tasks[token]?.renderNum) + const rawData = useCurrentRawData() + const timeStamp = useCreation(() => { + if (!rawData) return 0 + const itemData = rawData.contents.get(token) + if (!itemData) return 0 + return itemData.Timestamp || 0 + }, [renderNum]) return ( -
+
@@ -21,10 +33,8 @@ const AITaskDefaultGroupCardHeard: React.FC =
e.stopPropagation()}> void - timeStamp: number expand: boolean - onRefresh?: () => void token: string } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts deleted file mode 100644 index e169b2c574..0000000000 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/type.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Dispatch, SetStateAction } from 'react' - -export interface AITaskDefaultGroupCardHeardWrapperProps { - expand: boolean - token: string - expandToggle: () => void -} - -export interface AITaskDefaultGroupCardListWrapperProps { - token: string - setContentFocused: Dispatch> -} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx index 655e633f42..8204d4f4a6 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCardActions/ConcurrentStreamCardActions.tsx @@ -6,27 +6,22 @@ import { OutlineChevronsUpDownIcon, OutlineListOneIcon, OutlineListTodoIcon, - OutlineRefreshIcon, } from '@/assets/icon/outline' import { AIHistoryContinueTask, AIHistorySkipTask } from '../../../chatTemplate/historyTaskTree/HistoryTaskTree' import { openAIConcurrentStream } from '@/utils/openWebsite' import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' import styles from '../ConcurrentStreamCard.module.scss' -import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' +import { useCurrentRawData } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useCreation, useMemoizedFn } from 'ahooks' import useCurrentSessionId from '@/pages/ai-re-act/hooks/useCurrentSessionId' import { getTaskName } from '../concurrentStream/buildConcurrentStreamFramePayload' /** 卡片标题栏右侧操作区 */ interface ConcurrentStreamCardActionsProps { - isChildWindow?: boolean expand: boolean onExpandToggle: () => void - onRefresh?: () => void onDetails?: () => void - token: string - showContinueTask: boolean showCancelTask: boolean showDetails: boolean @@ -35,10 +30,8 @@ interface ConcurrentStreamCardActionsProps { } const ConcurrentStreamCardActions: FC = ({ - isChildWindow, expand, onExpandToggle, - onRefresh, onDetails, showContinueTask, showCancelTask, @@ -49,7 +42,6 @@ const ConcurrentStreamCardActions: FC = ({ }) => { const { t } = useI18nNamespaces(['aiAgent']) - const store = useCurrentStore() const session = useCurrentSessionId() const rawData = useCurrentRawData() const chatType = useCreation(() => { @@ -68,23 +60,11 @@ const ConcurrentStreamCardActions: FC = ({ token, session, chatType, + rootType: rawData.contents.get(token)?.type, taskName: getTaskName(rawData, token), }) }) - if (isChildWindow) { - return ( - - } - onClick={onRefresh} - className={styles['expand-btn']} - /> - - ) - } return ( <> {showContinueTask && coordinatorId != null && !!taskId && ( diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx index 7cbf63ab47..d282d8c151 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/AIChildWindowConcurrentStreamCard.tsx @@ -1,13 +1,12 @@ import classNames from 'classnames' import { type FC, memo } from 'react' -import ConcurrentStreamCardHeard from '../concurrentStreamCardHeard/ConcurrentStreamCardHeard' import styles from './AIChildWindowConcurrentStreamCard.module.scss' import { type ChatTaskNodeGroup } from '@/pages/ai-re-act/hooks/aiRender' import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' import useAIConcurrentStreamDispatcher from '@/auxWindow/pages/AIConcurrentStream/useContext/useDispatcher' -import useBoolean from 'ahooks/lib/useBoolean' import useCreation from 'ahooks/lib/useCreation' import AIChildWindowConcurrentStreamContent from '../../aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent' +import AIChildWindowConcurrentStreamCardHeard from './aiChildWindowConcurrentStreamCardHeard/AIChildWindowConcurrentStreamCardHeard' export interface AIChildWindowConcurrentStreamCardProps { token: string @@ -16,7 +15,6 @@ export interface AIChildWindowConcurrentStreamCardProps { /** 子窗口版并发流卡片(task_node_group 类型),数据从 auxWindow context 读取 */ const AIChildWindowConcurrentStreamCard: FC = memo((props) => { const { token } = props - const [expand, { toggle: expandToggle }] = useBoolean(true) const { rawData, renderNum } = useAIConcurrentStreamStore() const { requestRefresh } = useAIConcurrentStreamDispatcher() @@ -30,20 +28,9 @@ const AIChildWindowConcurrentStreamCard: FC - - {expand ? ( - <> -
{itemData?.data.goal}
- - - ) : null} + +
{itemData?.data.goal}
+
) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/AIChildWindowConcurrentStreamCardHeard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/AIChildWindowConcurrentStreamCardHeard.module.scss new file mode 100644 index 0000000000..74b0d06d15 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/AIChildWindowConcurrentStreamCardHeard.module.scss @@ -0,0 +1,74 @@ +@use '../../../../../../styles/mixin.scss' as mixin; +%display-flex-center { + display: flex; + align-items: center; +} +%display-column-center { + display: flex; + align-items: center; + flex-direction: column; +} + +.chat-card-title { + width: 100%; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 0 8px; + + &-left { + display: flex; + align-items: center; + gap: 4px; + // padding: 4px 8px 0 0; + flex: 1; + min-width: 0; + } + + &-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + + svg { + width: 100%; + height: 100%; + color: var(--Colors-Use-Neutral-Text-2-Primary); + } + } + + &-text { + flex: 1; + min-width: 0; + overflow: hidden; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.5px; + color: var(--Colors-Use-Neutral-Text-1-Title); + @include mixin.text-ellipsis(1, false, false, true); + + width: 100%; + } + + &-extra { + flex-shrink: 0; + min-width: 0; + max-width: 100%; + @include mixin.text-ellipsis(1, false, false, true); + + &:empty { + display: none; + } + } + + &-more { + display: flex; + flex-shrink: 0; + justify-content: flex-end; + align-items: center; + overflow: hidden; + } +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/AIChildWindowConcurrentStreamCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/AIChildWindowConcurrentStreamCardHeard.tsx new file mode 100644 index 0000000000..e863824284 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/AIChildWindowConcurrentStreamCardHeard.tsx @@ -0,0 +1,47 @@ +import { FC, memo, useMemo } from 'react' +import type { AIChildWindowConcurrentStreamCardHeardProps } from './type' +import styles from './AIChildWindowConcurrentStreamCardHeard.module.scss' +import { OutlineRefreshIcon } from '@/assets/icon/outline' +import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' +import { getAIStatusPresentation } from '@/pages/ai-agent/utils/AIStatusUtils' +import { Tooltip } from 'antd' +import ModalInfo from '../../../ModelInfo' +import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' + +const AIChildWindowConcurrentStreamCardHeard: FC = memo((props) => { + const { rowData, onRefresh } = props + const { t } = useI18nNamespaces(['aiAgent']) + + const presentation = useMemo(() => getAIStatusPresentation(rowData?.data?.status), [rowData?.data?.status]) + const titleText = useMemo(() => { + return rowData?.data?.taskName || '' + }, [rowData?.data?.taskName]) + const modalInfo = useMemo(() => { + if (!rowData) return undefined + return { time: rowData.Timestamp, title: rowData.AIModelName, icon: rowData.AIService } + }, [rowData?.Timestamp, rowData?.AIModelName, rowData?.AIService]) + return ( +
+
+
+ {presentation.icon &&
{presentation.icon}
} +
{titleText}
+
{modalInfo && }
+
+
+ + } + onClick={onRefresh} + className={styles['expand-btn']} + /> + +
+
+
+ ) +}) + +export default AIChildWindowConcurrentStreamCardHeard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/type.ts new file mode 100644 index 0000000000..1ab7f3b563 --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/aiChildWindowConcurrentStreamCard/aiChildWindowConcurrentStreamCardHeard/type.ts @@ -0,0 +1,6 @@ +import { ChatTaskNodeGroup } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AIChildWindowConcurrentStreamCardHeardProps { + rowData?: ChatTaskNodeGroup + onRefresh?: () => void +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx index a767fc1d36..0ec77d84b3 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamCardHeard/ConcurrentStreamCardHeard.tsx @@ -69,7 +69,6 @@ const ConcurrentStreamCardHeard: FC = memo((prop
= memo((prop showDetails={showDetails} coordinatorId={coordinatorId} taskId={rowData?.data?.taskId} - onRefresh={onRefresh} onDetails={onDetails} />
diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts index e2b7225238..97f9bc8592 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/concurrentStreamFrame.ts @@ -1,10 +1,17 @@ import type { AIChatQSData, AIYakExecFileRecord, ChatListRenderType } from '@/pages/ai-re-act/hooks/aiRender' +import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' export interface ConcurrentStreamFramePayload { session: string token: string chatType: ChatListRenderType taskName?: string + /** + * 根节点类型(token 在 rawData 中的 AIChatQSDataTypeEnum)。 + * 由主窗口开窗/推送时填入,子窗口无需等待 rawData 拉取完成即可据此选择卡片组件, + * 使懒加载 chunk 与 IPC 拉取并行、骨架屏提前切到 card 变体。 + */ + rootType?: AIChatQSDataTypeEnum renderNum?: number } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx index 7bd05070df..efe5dbef30 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent.tsx @@ -8,7 +8,7 @@ import AIChildWindowNodeItemWrapper from '../aiChildWindowNodeItemWrapper/AIChil import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' const AIChildWindowConcurrentStreamContent: FC = memo(() => { - const { childrenTokens, rawData } = useAIConcurrentStreamStore() + const { childrenTokens, rawData, renderNum } = useAIConcurrentStreamStore() const { ref: scrollRef, isFocus } = useClickFocus() return (
@@ -32,7 +32,7 @@ const AIChildWindowConcurrentStreamContent: FC = memo(() => { {item.type === AIChatQSDataTypeEnum.STREAM_GROUP ? ( ) : ( - + )}
) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx index d8efbba7db..7102b03e94 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowGroupStreamCard/AIChildWindowGroupStreamCard.tsx @@ -112,7 +112,7 @@ const AIChildWindowGroupStreamCardHeardWrapper: FC = memo( (props) => { const { childItemTokens, expand } = props - const { rawData } = useAIConcurrentStreamStore() + const { rawData, renderNum } = useAIConcurrentStreamStore() return ( <> { const itemData = rawData.get(token) if (!itemData) return - return + return ( + + ) }} /> diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper.tsx index 695dd9cdd0..893983585b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/AIChildWindowNodeItemWrapper.tsx @@ -1,23 +1,25 @@ -import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' -import { type AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' +import { AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' import { type FC, memo } from 'react' import { AIModelErrorPrompt } from '../../aiChatListItem/aiModelErrorPrompt/AIModelErrorPrompt' -import AINodeItem from '../../aiChatListItem/aiNodeItem/AINodeItem' import { AIHttpFlowFuzzStatusCard } from '../../aiHttpFlowFuzzStatusCard/AIHttpFlowFuzzStatusCard' import { AIReportFinishCard } from '../../aiReportFinishCard/AIReportFinishCard' import { AIReviewResult } from '../../aiReviewResult/AIReviewResult' import AIChildWindowGroupStreamNode from '../aiChildWindowGroupStreamNode/AIChildWindowGroupStreamNode' import AIChildWindowStreamCard from '../aiChildWindowStreamCard/AIChildWindowStreamCard' import AIChildWindowToolInvokerCard from '../aiChildWindowToolInvokerCard/AIChildWindowToolInvokerCard' +import { + AIThought, + AITriageChatContentWrapper, +} from '../../aiChatListItemWrapper/aiItemContentWrapper/AIItemContentWrapper' +import { AIToolDecision } from '../../aiToolDecision/AIToolDecision' +import AiFailPlanCard from '../../aiFailPlanCard/AiFailPlanCard' +import { AIChildWindowNodeItemWrapperProps } from './type' /** * 子窗口版 node item 包装器。 - * STREAM 类型依赖 streaming hooks(useTypedStream → useCurrentStore), - * 子窗口无 store,故 STREAM 直接展示纯文本内容;其余类型复用 AINodeItem。 */ -const AIChildWindowNodeItemWrapper: FC<{ itemData: AIChatQSData; groupIndex?: number }> = memo( - ({ itemData, groupIndex }) => { - const { renderNum = 0 } = useAIConcurrentStreamStore() +const AIChildWindowNodeItemWrapper: FC = memo( + ({ itemData, groupIndex, renderNum }) => { switch (itemData.type) { case AIChatQSDataTypeEnum.TOOL_RESULT: return @@ -51,14 +53,20 @@ const AIChildWindowNodeItemWrapper: FC<{ itemData: AIChatQSData; groupIndex?: nu return } case AIChatQSDataTypeEnum.QUESTION: + return null case AIChatQSDataTypeEnum.RESULT: + return case AIChatQSDataTypeEnum.THOUGHT: + return case AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION: + return null case AIChatQSDataTypeEnum.TOOL_CALL_DECISION: + return case AIChatQSDataTypeEnum.END_PLAN_AND_EXECUTION: + return null case AIChatQSDataTypeEnum.FAIL_PLAN_AND_EXECUTION: case AIChatQSDataTypeEnum.FAIL_REACT: - return + return default: return null } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/type.ts index e69de29bb2..ced719be90 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowNodeItemWrapper/type.ts @@ -0,0 +1,7 @@ +import { AIChatQSData } from '@/pages/ai-re-act/hooks/aiRender' + +export interface AIChildWindowNodeItemWrapperProps { + itemData: AIChatQSData + groupIndex?: number + renderNum: number +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx index 9ad3eb2eae..b5179c6bd1 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/AIChildWindowTaskDefaultGroupCard.tsx @@ -1,16 +1,15 @@ -import { useBoolean, useCreation } from 'ahooks' +import { useCreation } from 'ahooks' import classNames from 'classnames' import { type FC, memo } from 'react' -import AITaskDefaultGroupCardHeard from '../../AITaskDefaultGroupCard/aiTaskDefaultGroupCardHeard/AITaskDefaultGroupCardHeard' import styles from './AIChildWindowTaskDefaultGroupCard.module.scss' import useAIConcurrentStreamStore from '@/auxWindow/pages/AIConcurrentStream/useContext/useStore' import useAIConcurrentStreamDispatcher from '@/auxWindow/pages/AIConcurrentStream/useContext/useDispatcher' -import AIChildWindowConcurrentStreamContent from '../aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent' import type { AIChildWindowTaskDefaultGroupCardProps } from './type' +import AIChildWindowTaskDefaultGroupCardHeard from './aiChildWindowTaskDefaultGroupCardHeard/AIChildWindowTaskDefaultGroupCardHeard' +import AIChildWindowConcurrentStreamContent from '../aiChildWindowConcurrentStreamContent/AIChildWindowConcurrentStreamContent' const AIChildWindowTaskDefaultGroupCard: FC = memo((props) => { const { token } = props - const [expand, { toggle: expandToggle }] = useBoolean(true) const { rawData, renderNum } = useAIConcurrentStreamStore() const { requestRefresh } = useAIConcurrentStreamDispatcher() @@ -27,16 +26,9 @@ const AIChildWindowTaskDefaultGroupCard: FC - + - {expand ? : null} +
) }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/AIChildWindowTaskDefaultGroupCardHeard.module.scss b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/AIChildWindowTaskDefaultGroupCardHeard.module.scss new file mode 100644 index 0000000000..c3c733f37e --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/AIChildWindowTaskDefaultGroupCardHeard.module.scss @@ -0,0 +1,62 @@ +// 子窗口并发流内容区样式。 +// 内容对齐 AITaskDefaultGroupCardHeard.module.scss。 + +@use '../../../../../../styles/mixin.scss' as mixin; +%display-flex-center { + display: flex; + align-items: center; +} +%display-column-center { + display: flex; + align-items: center; + flex-direction: column; +} +.ai-task-default-group-card-title { + height: 20px; + display: flex; + justify-content: space-between; + align-items: center; + cursor: pointer; + + &-left { + display: flex; + align-items: center; + gap: 4px; + min-width: 0; + flex: 1; + + .icon, + .icon svg { + width: 16px; + height: 16px; + flex-shrink: 0; + color: var(--Colors-Use-Neutral-Disable); + } + + .text { + color: var(--Colors-Use-Neutral-Text-1-Title); + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.5px; + flex-shrink: 0; + } + + .time { + color: var(--Colors-Use-Neutral-Disable); + font-size: 11px; + font-style: normal; + font-weight: 400; + line-height: 14px; + letter-spacing: 0.5px; + @include mixin.text-ellipsis(1, false, false, true); + } + } + + &-right { + display: flex; + align-items: center; + flex-shrink: 0; + } +} diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/AIChildWindowTaskDefaultGroupCardHeard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/AIChildWindowTaskDefaultGroupCardHeard.tsx new file mode 100644 index 0000000000..4d156c723e --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/AIChildWindowTaskDefaultGroupCardHeard.tsx @@ -0,0 +1,37 @@ +import { FC, memo } from 'react' +import type { AIChildWindowTaskDefaultGroupCardHeardProps } from './type' +import styles from './AIChildWindowTaskDefaultGroupCardHeard.module.scss' +import { OutlineInformationcircleIcon, OutlineRefreshIcon } from '@/assets/icon/outline' +import { Tooltip } from 'antd' +import { YakitButton } from '@/components/yakitUI/YakitButton/YakitButton' +import { formatTimestamp } from '@/utils/timeUtil' +import { useI18nNamespaces } from '@/i18n/useI18nNamespaces' + +const AIChildWindowTaskDefaultGroupCardHeard: FC = memo((props) => { + const { t } = useI18nNamespaces(['aiAgent']) + const { timeStamp, onRefresh } = props + return ( +
+
+ + + + {t('ConcurrentStreamCard.systemInfo')} + {timeStamp ? {formatTimestamp(timeStamp)} : null} +
+
e.stopPropagation()}> + + } + onClick={onRefresh} + className={styles['expand-btn']} + /> + +
+
+ ) +}) + +export default AIChildWindowTaskDefaultGroupCardHeard diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/type.ts new file mode 100644 index 0000000000..798332c85d --- /dev/null +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChildWindowItem/aiChildWindowTaskDefaultGroupCard/aiChildWindowTaskDefaultGroupCardHeard/type.ts @@ -0,0 +1,4 @@ +export interface AIChildWindowTaskDefaultGroupCardHeardProps { + timeStamp: number + onRefresh: () => void +} From fc1572d14e2bda0bc3ad2c392687d4280c573c1a Mon Sep 17 00:00:00 2001 From: hubo Date: Wed, 29 Jul 2026 17:27:16 +0800 Subject: [PATCH 072/101] =?UTF-8?q?refactor:=20switchLoading=20=E9=87=8D?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E4=B8=BA=20initLoading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chatStore/aiRender/type/defaultConstant: 字段名 switchLoading → initLoading - ChatMultiSessionController: 4 处代码 + 3 处注释同步改名 - AIChatContent: 订阅 initLoading 驱动 AIGlobalLoading 遮罩 --- .../pages/ai-agent/aiChatContent/AIChatContent.tsx | 4 ++-- .../src/main/src/pages/ai-agent/defaultConstant.tsx | 2 +- .../ai-re-act/hooks/ChatMultiSessionController.ts | 12 ++++++------ .../src/main/src/pages/ai-re-act/hooks/aiRender.ts | 2 +- .../src/main/src/pages/ai-re-act/hooks/chatStore.ts | 2 +- .../src/main/src/pages/ai-re-act/hooks/type.ts | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx index 227f5b0bc8..e8e9c75c6f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiChatContent/AIChatContent.tsx @@ -48,7 +48,7 @@ export const AIChatContent: React.FC = React.memo( const httpTabUpdate = useStore(store, (state) => state.httpTabUpdate) const riskTabShow = useStore(store, (state) => state.riskTabShow) const riskTabUpdate = useStore(store, (state) => state.riskTabUpdate) - const requestHistoryState = useStore(store, (state) => state.requestHistoryState) + const initLoading = useStore(store, (state) => state.initLoading) const { activeChat } = useAIAgentStore() @@ -263,7 +263,7 @@ export const AIChatContent: React.FC = React.memo( return (
- +
() @@ -660,7 +660,7 @@ export class ChatMultiSessionController { * 建立指定 session 连接(新会话首问 / 打开历史 / 无问侧重连 共用)。 * 调用方应先用 isSessionReady 判重并挂好 IPC 监听,再调本方法(prepare 异步,invoke 晚于监听)。 * - 有 UserQuery:立刻上屏首问,pong 后发问;无树时不强制 recovery_history - * - 无 UserQuery:视为恢复态,置 switchLoading,pong 后 hydrate 或发 recovery_history + * - 无 UserQuery:视为恢复态,置 initLoading,pong 后 hydrate 或发 recovery_history * @returns 是否真正发起了建连 */ public handleStartSession(requestParams: AIChatIPCStartParams, cb?: (sessionId: string) => void): boolean { @@ -679,11 +679,11 @@ export class ChatMultiSessionController { const { request, store, rawData, meta } = this.ensureSession(sessionId) const userQuery = (params.Params?.UserQuery || '').trim() - // 恢复态:遮罩防止 hydrate / recovery 期间误点(UI 订阅 store.switchLoading) + // 恢复态:遮罩防止 hydrate / recovery 期间误点(UI 订阅 store.initLoading) if (userQuery) { store.getState().updateState({ execute: true, casualTitle: '发送问题,开启会话...' }) } else { - store.getState().updateState({ execute: true, switchLoading: true, casualTitle: '加载会话中...' }) + store.getState().updateState({ execute: true, initLoading: true, casualTitle: '加载会话中...' }) this.sessionRestoreLoading.add(sessionId) } @@ -1076,7 +1076,7 @@ export class ChatMultiSessionController { if (!this.sessionRestoreLoading.has(sessionId)) return this.sessionRestoreLoading.delete(sessionId) const store = this.storePool.get(sessionId) - store?.getState().updateState({ switchLoading: false }) + store?.getState().updateState({ initLoading: false }) } /** @@ -1115,7 +1115,7 @@ export class ChatMultiSessionController { await this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) this.finishSessionRestoreLoading(sessionId) } else { - // 保持 switchLoading,等 recovery_history 再关,避免 UI 提前可点 + // 保持 initLoading,等 recovery_history 再关,避免 UI 提前可点 this.requestRecoveryHistory(sessionId) } } else { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index 19f736979b..d3ea423bc9 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -601,7 +601,7 @@ export interface ChatStoreState { /** 切换session时的loading状态 */ /** 切换/恢复会话 loading(供 UI 遮罩与禁用交互,防止加载期间误点) */ - switchLoading: boolean + initLoading: boolean /** 用户主动取消问题的loading状态(自由对话) */ cancelCasualLoading: boolean /** 用户主动取消问题的loading状态(任务规划) */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index d589fa2bb8..3688a5078b 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -69,7 +69,7 @@ export const createChatStore = (options?: CreateChatStoreOptions) => { execFileRecord: new Map(), yakExecResultLogs: [], - switchLoading: false, + initLoading: false, cancelCasualLoading: false, cancelTaskLoading: false, requestHistoryState: { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts index 9ca1480bc5..2b70d9ed9a 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/type.ts @@ -140,7 +140,7 @@ export interface UseChatIPCState { /** 场景状态(仅供自由对话[reAct])使用 */ focusMode: string /** 切换/恢复会话 loading(UI 遮罩与禁用交互;无 UserQuery 建连时置 true,hydrate/recovery 结束后 false) */ - switchLoading: boolean + initLoading: boolean /** 任务规划历史数据-任务树 */ planHistoryList: AIAgentGrpcApi.PlanHistoryList /** 用户主动取消问题的loading状态(自由对话) */ From 2f5789b558c1d0ea3489fdaf3b481f9e3a6dd6ef Mon Sep 17 00:00:00 2001 From: whale <1745132665@qq.com> Date: Wed, 29 Jul 2026 17:52:34 +0800 Subject: [PATCH 073/101] =?UTF-8?q?fix:=20=E6=8C=89=20session=20=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E8=AE=A2=E9=98=85=20AI=20=E9=A1=B5=E9=9D=A2=E6=A1=A5?= =?UTF-8?q?=E6=8E=A5=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 避免 Provider 外层依赖 AIAgentContext,确保改包/代码变更能同步到对应页面。 Co-authored-by: Cursor --- .../src/components/withHistoryAIReActChat.tsx | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx index fcc6423e79..52227e2cb6 100644 --- a/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx +++ b/app/renderer/src/main/src/components/withHistoryAIReActChat.tsx @@ -47,7 +47,6 @@ import emiter from '@/utils/eventBus/eventBus' import { HistroryAIReActChat } from './HistroryAIReActChat' import { useChatIPC } from '@/pages/ai-re-act/hooks/useChatIPC' -import { useCurrentRawData, useCurrentStore } from '@/pages/ai-re-act/hooks/useCurrentDataBySession' import { useStore } from 'zustand' import { globalSessionEngine } from '@/pages/ai-re-act/hooks/ChatMultiSessionController' @@ -215,6 +214,8 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const pendingMentionRef = useRef(null) const chatReadyRef = useRef(false) const yakRunnerLastAttachedResourceInfoRef = useRef([]) + const bridgeSessionIdRef = useRef('') + const bridgeUnsubscribeRef = useRef<(() => void) | null>(null) // Web Fuzzer 页签 id:AI 改包回写、请求附件、fuzz 状态推送等桥接用,与 SessionID 无关 const isHaveWebFuzzerPageId = useCreation(() => { @@ -320,13 +321,9 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid const { onStart, onSend, onClose, onUpdatePageId } = useChatIPC(route, pageId) - const store = useCurrentStore() - const rawData = useCurrentRawData() + const store = globalSessionEngine.ensureSession(activeChat?.SessionID || '').store const execute = useStore(store, (state) => state.execute) const casualLoading = useStore(store, (state) => state.casualLoading) - const httpFuzzRequestUpdate = useStore(store, (state) => state.httpFuzzRequestUpdate) - const httpFlowFuzzStatusUpdate = useStore(store, (state) => state.httpFlowFuzzStatusUpdate) - const yaklangCodeChangeUpdate = useStore(store, (state) => state.yaklangCodeChangeUpdate) // TODO - @whale 修改确认 useEffect => httpFuzzTabPageId->isHaveWebFuzzerPageId useEffect(() => { @@ -356,22 +353,35 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid casualLoadingRef.current = casualLoading }, [casualLoading, pageId, isHaveWebFuzzerPageId, isHaveYakRunnerPageId]) - // 新版流处理器将页面桥接事件写入当前 session 的 rawData,并通过计数通知订阅者。 - // 在此消费这些更新,保持 Web Fuzzer / Yak Runner 的页面回写能力。 - useEffect(() => { - if (!rawData.httpFuzzRequest) return - onHttpFuzzRequestChange(clone(rawData.httpFuzzRequest)) - }, [httpFuzzRequestUpdate]) + const unsubscribeBridgeEvents = useMemoizedFn(() => { + bridgeUnsubscribeRef.current?.() + bridgeUnsubscribeRef.current = null + bridgeSessionIdRef.current = '' + }) - useEffect(() => { - if (!rawData.httpFlowFuzzStatus) return - onGetHttpFlowFuzzStatus(clone(rawData.httpFlowFuzzStatus)) - }, [httpFlowFuzzStatusUpdate]) + // 新版流处理器会更新 session store 内的版本字段;直接订阅该 session,避免依赖 Provider 的 context。 + const subscribeBridgeEvents = useMemoizedFn((sessionId: string) => { + if (!sessionId || bridgeSessionIdRef.current === sessionId) return - useEffect(() => { - if (!rawData.yaklangCodeChange) return - onYaklangCodeChange(clone(rawData.yaklangCodeChange)) - }, [yaklangCodeChangeUpdate]) + unsubscribeBridgeEvents() + const { store: sessionStore, rawData } = globalSessionEngine.ensureSession(sessionId) + bridgeSessionIdRef.current = sessionId + bridgeUnsubscribeRef.current = sessionStore.subscribe((state, previousState) => { + if (bridgeSessionIdRef.current !== sessionId) return + + if (state.httpFuzzRequestUpdate !== previousState.httpFuzzRequestUpdate && rawData.httpFuzzRequest) { + onHttpFuzzRequestChange(clone(rawData.httpFuzzRequest)) + } + if (state.httpFlowFuzzStatusUpdate !== previousState.httpFlowFuzzStatusUpdate && rawData.httpFlowFuzzStatus) { + onGetHttpFlowFuzzStatus(clone(rawData.httpFlowFuzzStatus)) + } + if (state.yaklangCodeChangeUpdate !== previousState.yaklangCodeChangeUpdate && rawData.yaklangCodeChange) { + onYaklangCodeChange(clone(rawData.yaklangCodeChange)) + } + }) + }) + + useEffect(() => unsubscribeBridgeEvents, [unsubscribeBridgeEvents]) const activeID = useCreation(() => { return activeChat?.SessionID @@ -379,10 +389,16 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid /** 切换会话 */ useUpdateEffect(() => { - if (activeChat) onReStart({ activeChat, onStart }) - }, [activeID]) + if (activeChat) { + subscribeBridgeEvents(activeChat.SessionID) + onReStart({ activeChat, onStart }) + } + }, [activeID, subscribeBridgeEvents]) const onStartRequest = useMemoizedFn((data: AIHandleStartParams) => { + const sessionId = data.params.Params?.TimelineSessionID || activeChat?.SessionID + if (sessionId) subscribeBridgeEvents(sessionId) + const newChat: AIHandleStartExtraProps = resolveStartExtraParams?.(data) ?? { chatId: activeChat?.SessionID, } @@ -419,6 +435,7 @@ export const HistoryAIReActChatProvider = memo(function HistoryAIReActChatProvid onClose([currentID]) } // events.onReset() + unsubscribeBridgeEvents() setActiveChat(undefined) setSetting((prev) => ({ ...prev, From 27c59b5ede0a40c15cd4261f54c16601b4c38ab3 Mon Sep 17 00:00:00 2001 From: hubo Date: Wed, 29 Jul 2026 17:57:17 +0800 Subject: [PATCH 074/101] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20timeline=20?= =?UTF-8?q?=E4=B8=8A=E6=BB=91=E5=8A=A0=E8=BD=BD=E6=97=A0=E9=99=90=E7=A9=BA?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=20+=20=E7=BB=9F=E4=B8=80=20state=20=E5=8F=96?= =?UTF-8?q?=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AIAgentChatData 新增 timelineNoMore 标记,loadTimelineHistory 在 Total===0 或 Events.length { grpcOffset: 0, timelineBeforeId: 0, + timelineNoMore: false, httpRunTimeIDs: [], riskRunTimeIDs: [], @@ -965,10 +966,9 @@ export class ChatMultiSessionController { */ public async loadTimelineHistory(sessionId: string): Promise { const { rawData, store } = this.ensureSession(sessionId) - const state = store.getState() - // 置 loading(驱动 TimelineCard 的 YakitSpin) + // 置 loading(驱动 TimelineCard 的 YakitSpin);与 finally 一致用 store.getState() 取最新 store.getState().updateState({ - requestHistoryState: { ...state.requestHistoryState, timelinesLoading: true }, + requestHistoryState: { ...store.getState().requestHistoryState, timelinesLoading: true }, }) try { const request: AIEventQueryRequest = { @@ -984,7 +984,11 @@ export class ChatMultiSessionController { request.Pagination!.BeforeId = rawData.timelineBeforeId } const { Events, Total } = await grpcQueryAIEvent(request, true) - if (Number(Total) === 0) return false + if (Number(Total) === 0) { + // 已到最旧,置尽头标记,避免 hasMoreTimeline 误判导致无限空查询 + rawData.timelineNoMore = true + return false + } // 更新游标为最后一条(最旧)的 ID rawData.timelineBeforeId = Number(Events[Events.length - 1].ID) @@ -996,7 +1000,10 @@ export class ChatMultiSessionController { // 前插合并去重:在 store set 回调里拿最新 state 合并, // 避免 await 期间实时流推入的新数据被 stale state 覆盖丢失 store.getState().setReActTimelines(timelineItems) - return Events.length === ChatMultiSessionController.TIMELINE_PAGE_LIMIT + const hasMore = Events.length === ChatMultiSessionController.TIMELINE_PAGE_LIMIT + // 没拉满一页说明已到尽头,置标记 + if (!hasMore) rawData.timelineNoMore = true + return hasMore } catch { return false } finally { @@ -1007,12 +1014,10 @@ export class ChatMultiSessionController { } } - /** 是否还有更旧 timeline 历史可加载 */ + /** 是否还有更旧 timeline 历史可加载(用 timelineNoMore 标记,避免无限空查询) */ public hasMoreTimeline(sessionId: string): boolean { const { rawData } = this.ensureSession(sessionId) - // 游标为 0 表示未拉过 → 可加载;上次拉满一页 → 可加载 - // 简化:只要没拉到过空结果就允许尝试(loadTimelineHistory 内部会判 Total===0) - return rawData.timelineBeforeId >= 0 + return !rawData.timelineNoMore } /** From 40cce206ff3269410629f631bebcbee0d6af932c Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Thu, 30 Jul 2026 00:10:58 +0800 Subject: [PATCH 075/101] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=20dispat?= =?UTF-8?q?chStreamingNode=20=E6=8C=82=E6=A0=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 direction 合并历史前插与实时追加分支 - 增加 isAlreadyInTree 挂树幂等检查 - 完善 history stream 分组合并与 parentChildren 索引修复 - recovery_history 落库后关闭 session restore loading --- .../hooks/ChatMultiSessionController.ts | 1 + .../src/pages/ai-re-act/hooks/chatStore.ts | 139 ++++++++++-------- 2 files changed, 78 insertions(+), 62 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 34869fa145..6d0e33b91d 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -1219,6 +1219,7 @@ export class ChatMultiSessionController { taskElements: [...state.taskChat.elements], } void this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) + this.finishSessionRestoreLoading(sessionId) } } catch { // ignore parse error diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index 3688a5078b..d3bca1ca0f 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -176,6 +176,7 @@ export const createChatStore = (options?: CreateChatStoreOptions) => { dispatchStreamingNode: ({ chatType, parentTaskId, node }) => { set((state) => { const isHistory = node.isHistory ?? false + const direction = isHistory ? 'prepend' : 'append' const elementRef = { kind: node.kind, token: node.token, chatType, isHistory } const targetElements = chatType === 'reAct' ? state.casualChat.elements : state.taskChat.elements @@ -198,91 +199,105 @@ export const createChatStore = (options?: CreateChatStoreOptions) => { } } - if (isHistory) { - // 历史数据:实体已在上方注册,这里只做 elements 前插(旧→新顺序重放,unshift 到头部) - // 前插完必须 return,否则会走到下面"默认追加"再 push 一次导致双插 - const parentChildren = parentTaskId ? state.tasks[parentTaskId]?.childrenTokens : undefined - if (parentChildren) { - parentChildren.unshift(node.token) - } else { - targetElements.unshift(elementRef) + /** 挂树幂等:同一 token 重复 dispatch 时跳过,避免 elements / childrenTokens 双插 */ + const isAlreadyInTree = () => { + /** stream 合并后 item 挂在 group.childrenTokens,不在 siblings 顶层列表里 */ + const isInGroupChildren = (tokens: string[] | undefined) => + !!tokens?.some((t) => state.groups[t]?.childrenTokens.includes(node.token)) + + if (parentTaskId) { + const children = state.tasks[parentTaskId]?.childrenTokens + if (!children) return false + if (children.includes(node.token)) return true + return isInGroupChildren(children) } + + // 顶层 item / task / group + if (targetElements.some((el) => el.token === node.token)) return true + // 顶层 stream 组内的 item + return targetElements.some((el) => state.groups[el.token]?.childrenTokens.includes(node.token)) + } + if (isAlreadyInTree()) return + + if (targetElements.length === 0) { + if (direction === 'append') targetElements.push(elementRef) + else targetElements.unshift(elementRef) return - } else { - // 空列表直接追加 - if (targetElements.length === 0) { - targetElements.push(elementRef) - return - } + } - // 定位同级上一个节点(parentChildren 为空数组时表示无兄弟节点,不做合并) - const parentChildren = parentTaskId ? state.tasks[parentTaskId]?.childrenTokens : undefined - if (parentTaskId && !parentChildren) return + const parentChildren = parentTaskId ? state.tasks[parentTaskId]?.childrenTokens : undefined + if (parentTaskId && !parentChildren) return - const lastToken = parentChildren?.length + const siblingToken = parentChildren?.length + ? direction === 'append' ? parentChildren.at(-1) - : parentTaskId - ? undefined - : targetElements.at(-1)?.token - - if (lastToken) { - const lastItem = state.items[lastToken] - const lastGroup = state.groups[lastToken] - const isStreamItem = node.kind === 'item' && node.type === AIChatQSDataTypeEnum.STREAM && !!node.nodeId - - // stream 合并:追加到已有组 - if (isStreamItem && lastGroup?.kind === 'group' && lastGroup.nodeId === node.nodeId) { - node.groupExtra?.(lastGroup.token, [node.token]) - lastGroup.childrenTokens.push(node.token) - lastGroup.renderNum += 1 - return - } + : parentChildren[0] + : parentTaskId + ? undefined + : direction === 'append' + ? targetElements.at(-1)?.token + : targetElements[0]?.token + + if (siblingToken) { + const siblingItem = state.items[siblingToken] + const siblingGroup = state.groups[siblingToken] + const isStreamItem = node.kind === 'item' && node.type === AIChatQSDataTypeEnum.STREAM && !!node.nodeId + + if (isStreamItem && siblingGroup?.kind === 'group' && siblingGroup.nodeId === node.nodeId) { + if (siblingGroup.childrenTokens.includes(node.token)) return + node.groupExtra?.(siblingGroup.token, [node.token]) + if (direction === 'append') siblingGroup.childrenTokens.push(node.token) + else siblingGroup.childrenTokens.unshift(node.token) + siblingGroup.renderNum += 1 + return + } - // stream 合并:两个连续 item 合成新组 - if (isStreamItem && lastItem?.type === AIChatQSDataTypeEnum.STREAM && lastItem.nodeId === node.nodeId) { - const newGroupToken = `${node.nodeId}-${uuidv4()}` - node.groupExtra?.(newGroupToken, [lastToken, node.token]) - state.groups[newGroupToken] = { - kind: 'group', - token: newGroupToken, - type: AIChatQSDataTypeEnum.STREAM_GROUP, - renderNum: 1, - nodeId: node.nodeId, - childrenTokens: [lastToken, node.token], - } - if (parentChildren?.length) { - parentChildren[parentChildren.length - 1] = newGroupToken - } else { - targetElements[targetElements.length - 1] = { - kind: 'group', - token: newGroupToken, - chatType, - isHistory, - } - } - return + if ( + isStreamItem && + siblingItem?.type === AIChatQSDataTypeEnum.STREAM && + siblingItem.nodeId === node.nodeId + ) { + const newGroupToken = `${node.nodeId}-${uuidv4()}` + const groupChildrenTokens = + direction === 'append' ? [siblingToken, node.token] : [node.token, siblingToken] + node.groupExtra?.(newGroupToken, groupChildrenTokens) + state.groups[newGroupToken] = { + kind: 'group', + token: newGroupToken, + type: AIChatQSDataTypeEnum.STREAM_GROUP, + renderNum: 1, + nodeId: node.nodeId, + childrenTokens: groupChildrenTokens, } + const groupElementRef = { kind: 'group' as const, token: newGroupToken, chatType, isHistory } + if (parentChildren?.length) { + parentChildren[direction === 'append' ? parentChildren.length - 1 : 0] = newGroupToken + } else { + targetElements[direction === 'append' ? targetElements.length - 1 : 0] = groupElementRef + } + return } } - // 默认追加(实时数据合并失败也走这里) - const parentChildren = parentTaskId ? state.tasks[parentTaskId]?.childrenTokens : undefined if (parentChildren) { - parentChildren.push(node.token) + if (direction === 'append') parentChildren.push(node.token) + else parentChildren.unshift(node.token) return } const lastEl = state.taskChat.elements.at(-1) if ( + direction === 'append' && chatType === 'task' && node.type === AIChatQSDataTypeEnum.TASK_NODE_GROUP && lastEl?.kind === 'task' && state.tasks[lastEl.token]?.type === AIChatQSDataTypeEnum.TASK_DEFAULT_GROUP ) { - // 非组数据放入默认任务组,且默认任务组保持在最下面 + // 任务规划最新一个元素,一定是默认任务组,所以别的元素需要往前插入 state.taskChat.elements.splice(state.taskChat.elements.length - 1, 0, elementRef) } else { - targetElements.push(elementRef) + if (direction === 'append') targetElements.push(elementRef) + else targetElements.unshift(elementRef) } }) onRenderStructureChange?.() From 0ce5e6b562d438c3b4297b6bfc8aa4ce420669ae Mon Sep 17 00:00:00 2001 From: nonight <378099757@qq.com> Date: Thu, 30 Jul 2026 14:29:05 +0800 Subject: [PATCH 076/101] =?UTF-8?q?refactor:=20=E6=94=B6=E6=95=9B=20Chat*?= =?UTF-8?q?=20=E7=B1=BB=E5=9E=8B=E5=AF=BC=E5=87=BA=E5=B9=B6=E8=A1=A5?= =?UTF-8?q?=E5=85=85=20grpcLoadMoreLoading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 组件侧改为 Extract 收窄类型,减少 aiRender 细粒度类型导出;新增 grpcLoadMoreLoading,recovery_history 请求时防重入并在结束时关闭。 --- .../ConcurrentStreamCard.tsx | 6 ++-- .../aiChatListItem/aiModelErrorPrompt/type.ts | 4 +-- .../aiItemContentWrapper/type.ts | 4 +-- .../aiFailPlanCard/AiFailPlanCard.tsx | 13 +++++---- .../components/aiManualIntervention/type.ts | 4 +-- .../components/aiToolDecision/type.ts | 4 +-- .../ai-agent/components/aiTriageChat/type.ts | 7 +++-- .../hooks/ChatMultiSessionController.ts | 10 +++++-- .../src/pages/ai-re-act/hooks/aiRender.ts | 29 ++++++++++--------- .../src/pages/ai-re-act/hooks/chatStore.ts | 2 ++ .../hooks/grpcStreamHandler/aiReview.ts | 5 ++-- 11 files changed, 53 insertions(+), 35 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index 19e6943a53..13afe2d1fb 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -1,7 +1,7 @@ import { memo, useEffect, useMemo, type FC } from 'react' import { useBoolean, useCreation, useMemoizedFn } from 'ahooks' import styles from './ConcurrentStreamCard.module.scss' -import { AIChatQSDataTypeEnum, ChatTaskDefaultGroup, type ChatTaskNodeGroup } from '@/pages/ai-re-act/hooks/aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' import { getAIStatusPresentation } from '../../utils/AIStatusUtils' import { useVectorStripeBg } from './hooks/useVectorStripeBg' import { useConcurrentStreamCardStyle } from './hooks/useConcurrentStreamCardStyle' @@ -32,7 +32,9 @@ const ConcurrentStreamCard: FC<{ if (!rawData) return null const itemData = rawData.contents.get(token) if (!itemData) return null - return { ...itemData } as ChatTaskNodeGroup | ChatTaskDefaultGroup | undefined + return { ...itemData } as + | Extract + | undefined }, [renderNum]) useEffect(() => { diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts index ad31d16d99..0aafb2397f 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItem/aiModelErrorPrompt/type.ts @@ -1,7 +1,7 @@ -import { ChatApiRequestFailed } from '@/pages/ai-re-act/hooks/aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' export interface AIModelErrorPromptProps { - item: ChatApiRequestFailed + item: Extract renderNum: number isChildWindow: boolean } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/type.ts index 77bd7f869f..860866c4de 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiChatListItemWrapper/aiItemContentWrapper/type.ts @@ -1,4 +1,4 @@ -import { ChatThought } from '@/pages/ai-re-act/hooks/aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' import { AINodeItemProps } from '../../aiChatListItem/aiNodeItem/type' import { AITriageChatContentProps } from '../../aiTriageChat/type' @@ -8,5 +8,5 @@ interface AIItemContentWrapperProps { export interface AITriageChatContentWrapperProps extends Omit {} export interface AIThoughtProps extends AIItemContentWrapperProps { - itemData: ChatThought + itemData: Extract } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiFailPlanCard/AiFailPlanCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/aiFailPlanCard/AiFailPlanCard.tsx index f7ee3d0f93..c1fbbee39a 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiFailPlanCard/AiFailPlanCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiFailPlanCard/AiFailPlanCard.tsx @@ -1,4 +1,4 @@ -import type { ChatFailPlanAndExecution, ChatFailReact } from '@/pages/ai-re-act/hooks/aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' import { memo, type FC } from 'react' import ChatCard from '../ChatCard' import styles from './AiFailPlanCard.module.scss' @@ -11,10 +11,13 @@ import { Tooltip } from 'antd' import useCreation from 'ahooks/lib/useCreation' import useToggle from 'ahooks/lib/useToggle' -const AiFailPlanCard: FC<{ itemData: ChatFailReact | ChatFailPlanAndExecution; renderNum: number }> = ({ - itemData, - renderNum, -}) => { +const AiFailPlanCard: FC<{ + itemData: Extract< + AIChatQSData, + { type: AIChatQSDataTypeEnum.FAIL_REACT | AIChatQSDataTypeEnum.FAIL_PLAN_AND_EXECUTION } + > + renderNum: number +}> = ({ itemData, renderNum }) => { const { t } = useI18nNamespaces(['aiAgent']) const [expand, { toggle }] = useToggle(false) const { nodeLabel } = useAINodeLabel(itemData.data.NodeIdVerbose) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiManualIntervention/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiManualIntervention/type.ts index 69f3b65933..38d205954b 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiManualIntervention/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiManualIntervention/type.ts @@ -1,6 +1,6 @@ -import { ChatUserManualIntervention } from '@/pages/ai-re-act/hooks/aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' export interface AIManualInterventionProps { - info: ChatUserManualIntervention + info: Extract renderNum: number } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiToolDecision/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiToolDecision/type.ts index 3f57fa259c..d3eb306637 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiToolDecision/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiToolDecision/type.ts @@ -1,6 +1,6 @@ -import { ChatToolCallDecision } from '@/pages/ai-re-act/hooks/aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' export interface AIToolDecisionProps { - item: ChatToolCallDecision + item: Extract renderNum: number } diff --git a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts index 7ce283b8b0..0df6f482bc 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts +++ b/app/renderer/src/main/src/pages/ai-agent/components/aiTriageChat/type.ts @@ -1,4 +1,4 @@ -import { AIChatQSData, ChatQuestion, ChatResult, ChatThought } from '@/pages/ai-re-act/hooks/aiRender' +import { type AIChatQSData, AIChatQSDataTypeEnum } from '@/pages/ai-re-act/hooks/aiRender' import { AINodeItemProps } from '../aiChatListItem/aiNodeItem/type' import { ChatDataStoreKey } from '../../store/ChatDataStore' @@ -6,7 +6,10 @@ export interface AITriageChatContentProps { isAnswer?: boolean contentClassName?: string chatClassName?: string - itemData: ChatQuestion | ChatResult | ChatThought + itemData: Extract< + AIChatQSData, + { type: AIChatQSDataTypeEnum.QUESTION | AIChatQSDataTypeEnum.RESULT | AIChatQSDataTypeEnum.THOUGHT } + > renderNum: AINodeItemProps['renderNum'] diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts index 6d0e33b91d..758b4fd1c8 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/ChatMultiSessionController.ts @@ -945,7 +945,11 @@ export class ChatMultiSessionController { /** 发 recovery_history 拉更旧事件(grpcOffset 为起点,向前回溯 RECOVERY_HISTORY_LIMIT 条) */ public requestRecoveryHistory(sessionId: string) { - const { rawData } = this.ensureSession(sessionId) + const { store, rawData } = this.ensureSession(sessionId) + const initLoading = store.getState().initLoading + const grpcLoadMoreLoading = store.getState().grpcLoadMoreLoading + if (initLoading || grpcLoadMoreLoading) return + store.getState().updateState({ grpcLoadMoreLoading: true }) this.requestMessage(sessionId, { IsSyncMessage: true, SyncType: AIInputEventSyncTypeEnum.SYNC_TYPE_RECOVERY_HISTORY, @@ -1219,7 +1223,9 @@ export class ChatMultiSessionController { taskElements: [...state.taskChat.elements], } void this.persistSetSessionRender(sessionId, content, rawData.grpcOffset) - this.finishSessionRestoreLoading(sessionId) + } + if (store.getState().grpcLoadMoreLoading) { + store.getState().updateState({ grpcLoadMoreLoading: false }) } } catch { // ignore parse error diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts index d3ea423bc9..82bb32f16c 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/aiRender.ts @@ -402,43 +402,43 @@ export interface AIChatQSDataBase { parentGroupToken?: string } -export type ChatQuestion = AIChatQSDataBase +type ChatQuestion = AIChatQSDataBase export type ChatStream = AIChatQSDataBase type ChatToolCallResult = AIChatQSDataBase type ChatToolCallParams = AIChatQSDataBase -export type ChatApiRequestFailed = AIChatQSDataBase< +type ChatApiRequestFailed = AIChatQSDataBase< AIChatQSDataTypeEnum.AI_API_REQUEST_FAILED, AIAgentGrpcApi.AIApiRequestFailedPayload > -export type ChatThought = AIChatQSDataBase -export type ChatResult = AIChatQSDataBase +type ChatThought = AIChatQSDataBase +type ChatResult = AIChatQSDataBase export type ChatToolResult = AIChatQSDataBase -export type ChatPlanReviewRequire = AIChatQSDataBase +type ChatPlanReviewRequire = AIChatQSDataBase type ChatDetachedPlanReviewRequire = AIChatQSDataBase -export type ChatTaskReviewRequire = AIChatQSDataBase -export type ChatToolUseReviewRequire = AIChatQSDataBase -export type ChatRequireUserInteractive = AIChatQSDataBase< +type ChatTaskReviewRequire = AIChatQSDataBase +type ChatToolUseReviewRequire = AIChatQSDataBase +type ChatRequireUserInteractive = AIChatQSDataBase< AIChatQSDataTypeEnum.REQUIRE_USER_INTERACTIVE, UIRequireUserInteractive > type ChatExecAIForgeReview = AIChatQSDataBase export type ChatTaskNodeGroup = AIChatQSDataBase -export type ChatToolCallDecision = AIChatQSDataBase +type ChatToolCallDecision = AIChatQSDataBase type ChatPlanExecEnd = AIChatQSDataBase -export type ChatFailPlanAndExecution = AIChatQSDataBase -export type ChatFailReact = AIChatQSDataBase +type ChatFailPlanAndExecution = AIChatQSDataBase +type ChatFailReact = AIChatQSDataBase type ChatReferenceMaterial = AIChatQSDataBase< AIChatQSDataTypeEnum.Reference_Material, { NodeId: AIOutputEvent['NodeId']; NodeIdVerbose: AIOutputEvent['NodeIdVerbose'] } > -export type ChatUserManualIntervention = AIChatQSDataBase< +type ChatUserManualIntervention = AIChatQSDataBase< AIChatQSDataTypeEnum.USER_MANUAL_INTERVENTION, UserManualInterventionContext > type ChatHttpFlowFuzzStatus = AIChatQSDataBase type ChatReportFinish = AIChatQSDataBase -export type ChatTaskDefaultGroup = AIChatQSDataBase +type ChatTaskDefaultGroup = AIChatQSDataBase type ChatStreamGroup = AIChatQSDataBase< AIChatQSDataTypeEnum.STREAM_GROUP, { NodeId: AIOutputEvent['NodeId']; NodeIdVerbose: AIOutputEvent['NodeIdVerbose']; lastToken: string } @@ -602,6 +602,9 @@ export interface ChatStoreState { /** 切换session时的loading状态 */ /** 切换/恢复会话 loading(供 UI 遮罩与禁用交互,防止加载期间误点) */ initLoading: boolean + /** 非初始化的请求grpc数据 loading */ + grpcLoadMoreLoading: boolean + /** 用户主动取消问题的loading状态(自由对话) */ cancelCasualLoading: boolean /** 用户主动取消问题的loading状态(任务规划) */ diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts index d3bca1ca0f..de8fee7bd2 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/chatStore.ts @@ -70,6 +70,8 @@ export const createChatStore = (options?: CreateChatStoreOptions) => { yakExecResultLogs: [], initLoading: false, + grpcLoadMoreLoading: false, + cancelCasualLoading: false, cancelTaskLoading: false, requestHistoryState: { diff --git a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts index 26da1480a0..6ad7277a6c 100644 --- a/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts +++ b/app/renderer/src/main/src/pages/ai-re-act/hooks/grpcStreamHandler/aiReview.ts @@ -153,7 +153,7 @@ const handleTaskReviewRequire: AIMessageHandler = (requestInfo) => { } const handleToolReview: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta, request, sendRequest } = requestInfo + const { res, chatType, store, rawData, request, sendRequest } = requestInfo if (res.Type !== 'tool_use_review_require') return // 历史数据-tool-review数据不进行展示 if (res.IsSync) return @@ -516,7 +516,6 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { token: reviewDetail.id, kind: 'item', type: reviewDetail.type, - isHistory: true, }, }) } @@ -568,7 +567,7 @@ const handleReviewRelease: AIMessageHandler = (requestInfo) => { } const handleDetachedPlanReview: AIMessageHandler = (requestInfo) => { - const { res, chatType, store, rawData, meta } = requestInfo + const { res, chatType, store, rawData } = requestInfo if (res.Type !== 'detached_plan_require' || res.NodeId !== 'detached-plan') return // 历史数据-grpc流数据在任务规划下无效,不处理 From bdf979e288c29abb5890d2f23a221b53be6afddd Mon Sep 17 00:00:00 2001 From: luoluo <1425735414@qq.com> Date: Thu, 30 Jul 2026 11:15:31 +0800 Subject: [PATCH 077/101] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ai-agent/aiAgentChat/AIAgentChat.tsx | 4 +- .../aiModelList/aiModelForm/AIModelForm.tsx | 4 +- .../historyTaskTree/HistoryTaskTree.tsx | 6 +- .../AITaskDefaultGroupCard.tsx | 8 +- .../AITaskDefaultGroupContent.tsx | 11 +- .../ConcurrentStreamCard.tsx | 2 +- .../ConcurrentStreamContent.tsx | 226 ++++++++---------- ...hildWindowConcurrentStreamCard.module.scss | 2 + ...dWindowConcurrentStreamContent.module.scss | 2 + ...hildWindowTaskDefaultGroupCard.module.scss | 1 + .../ai-re-act/aiReActChat/AIReActChat.tsx | 1 + .../aiReActTaskChat/AIReActTaskChat.tsx | 8 +- .../src/pages/ai-re-act/hooks/aiRender.ts | 3 +- .../aiTool/AIToolEditor/AIToolEditor.tsx | 1 - .../aiTool/AIToolEditor/AIToolEditorType.d.ts | 3 +- 15 files changed, 129 insertions(+), 153 deletions(-) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx index eae839840f..7e4438abdd 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiAgentChat/AIAgentChat.tsx @@ -56,7 +56,6 @@ export const AIAgentChat: React.FC = memo((props) => { const aiReActChatRef = useRef(null) const aiChatWelcomeRef = useRef(null) - const initRef = useRef(true) // 插件并发构建流 hooks const [streams, api] = useMultipleHoldGRPCStream() @@ -71,8 +70,7 @@ export const AIAgentChat: React.FC = memo((props) => { useEffect(() => { if (!!activeChat?.SessionID) { onSetReAct() - if (!initRef.current) onReStart({ activeChat, onStart }) - initRef.current = false + onReStart({ activeChat, onStart }) } }, [activeChat?.SessionID]) diff --git a/app/renderer/src/main/src/pages/ai-agent/aiModelList/aiModelForm/AIModelForm.tsx b/app/renderer/src/main/src/pages/ai-agent/aiModelList/aiModelForm/AIModelForm.tsx index 2f68be455d..d351f45a0c 100644 --- a/app/renderer/src/main/src/pages/ai-agent/aiModelList/aiModelForm/AIModelForm.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/aiModelList/aiModelForm/AIModelForm.tsx @@ -4,7 +4,6 @@ import { AIModelCheckResultProps, AIModelFormAddOptions, AIModelFormProps, - AIModelFormSetAIGlobalConfigOptions, AIModelFormUpdateOptions, } from './AIModelFormType' import { useCreation, useDebounceFn, useInViewport, useMemoizedFn } from 'ahooks' @@ -19,7 +18,6 @@ import { AIModelConfig, AIModelTypeFileName, AIProvider, - DEFAULT_AI_API_TYPE, grpcQueryAIProviderAll, grpcAIConfigHealthCheck, normalizeAIAPIType, @@ -50,7 +48,7 @@ import useAIGlobalConfig from '@/pages/ai-re-act/hooks/useAIGlobalConfig' const defaultFormValues = { Type: '', - api_type: DEFAULT_AI_API_TYPE, + api_type: 'chat_completions', model_type: AIModelTypeEnum.TierIntelligent, } /**是否认为是同一个ai model */ diff --git a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx index 40270cf1cc..01d11dd546 100644 --- a/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/chatTemplate/historyTaskTree/HistoryTaskTree.tsx @@ -157,8 +157,8 @@ export const AIHistoryContinueTask: React.FC = React const sendRecoverParamsRef = useRef() const loading = useCreation(() => { - return sendRecoverParamsRef.current?.taskId === taskId && isExecuting - }, [isExecuting, taskId]) + return sendRecoverParamsRef.current?.taskId === taskId && cancelTaskLoading + }, [taskId, cancelTaskLoading]) useUpdateEffect(() => { if (!isExecuting && sendRecoverParamsRef.current) { @@ -216,7 +216,7 @@ export const AIHistoryContinueTask: React.FC = React taskId, } store.getState().updateState({ - cancelCasualLoading: true, + cancelTaskLoading: true, }) if (taskStatus.status === AITaskStatus.inProgress && currentTaskId) { // 选停止当前任务,等待任务停止成功后,再发送恢复的数据 diff --git a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx index 8d7b2bdce9..a3d4b1f209 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/AITaskDefaultGroupCard/AITaskDefaultGroupCard.tsx @@ -1,4 +1,4 @@ -import { type FC, memo, useEffect, useState } from 'react' +import { type FC, memo, useState } from 'react' import classNames from 'classnames' import styles from './AITaskDefaultGroupCard.module.scss' import { useBoolean } from 'ahooks' @@ -8,13 +8,9 @@ import AITaskDefaultGroupCardHeard from './aiTaskDefaultGroupCardHeard/AITaskDef const AITaskDefaultGroupCard: FC<{ token: string }> = memo(({ token }) => { - const [expand, { toggle: expandToggle, setFalse: collapseExpand }] = useBoolean(true) + const [expand, { toggle: expandToggle }] = useBoolean(true) const [contentFocused, setContentFocused] = useState(false) - useEffect(() => { - collapseExpand() - }, [collapseExpand]) - return (
void -}> = ({ token, isChildWindow, onContentFocusChange }) => { +}> = ({ token, onContentFocusChange }) => { const store = useCurrentStore() const childrenTokens = useStore(store, (state) => state.tasks[token]?.childrenTokens || []) const { ref: contentRef, isFocus } = useClickFocus() @@ -34,21 +33,15 @@ const AITaskDefaultGroupContent: FC<{ }) return () => cancelAnimationFrame(rafId) }, [contentRef, childrenTokens.length]) - return (
- +
) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx index 13afe2d1fb..d59df5a894 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamCard.tsx @@ -134,5 +134,5 @@ const ConcurrentStreamCardListWrapper: FC const store = useCurrentStore() const childrenTokens = useStore(store, (state) => state.tasks[token]?.childrenTokens || []) - return + return }) diff --git a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx index 468e38709a..fafa740b78 100644 --- a/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx +++ b/app/renderer/src/main/src/pages/ai-agent/components/ConcurrentStreamCard/ConcurrentStreamContent/ConcurrentStreamContent.tsx @@ -13,146 +13,132 @@ const PAGE_SIZE = 20 interface ConcurrentStreamContentProps { childrenTokens: string[] - isChildWindow?: boolean /** 由父级承担滚动时传入,避免嵌套双滚动容器 */ scrollContainerRef?: RefObject } -const ConcurrentStreamContent: FC = memo( - ({ childrenTokens, isChildWindow, scrollContainerRef }) => { - const { ref: scrollRef, isFocus } = useClickFocus() - const contentMeasureRef = useRef(null) - const embedInParentScroll = !!scrollContainerRef - - /** 当前渲染起始下标,初始从末尾 PAGE_SIZE 处开始,新增元素始终可见 */ - const [startIndex, setStartIndex] = useState(() => Math.max(0, childrenTokens.length - PAGE_SIZE)) - /** 加载更多时保存旧滚动高度,用于恢复位置 */ - const prevScrollHeightRef = useRef(0) - const loadingMoreRef = useRef(false) - const startIndexLatest = useLatest(startIndex) - /** 用户是否在底部附近,用于决定新元素到来时是否跟随滚动 */ - const isAtBottomRef = useRef(true) - const isFirstMountRef = useRef(true) - - const getScrollEl = useMemoizedFn(() => scrollContainerRef?.current ?? scrollRef.current) - - /** 流式增高时,仅在底部则自动跟随 */ - useLayoutEffect(() => { - const scrollEl = getScrollEl() - const measureEl = contentMeasureRef.current - if (!scrollEl || !measureEl) return - - let rafId = 0 - const scrollToBottom = () => { - if (!isAtBottomRef.current) return - scrollEl.scrollTop = scrollEl.scrollHeight - } +const ConcurrentStreamContent: FC = memo(({ childrenTokens, scrollContainerRef }) => { + const { ref: scrollRef, isFocus } = useClickFocus() + const contentMeasureRef = useRef(null) + const embedInParentScroll = !!scrollContainerRef + + /** 当前渲染起始下标,初始从末尾 PAGE_SIZE 处开始,新增元素始终可见 */ + const [startIndex, setStartIndex] = useState(() => Math.max(0, childrenTokens.length - PAGE_SIZE)) + /** 加载更多时保存旧滚动高度,用于恢复位置 */ + const prevScrollHeightRef = useRef(0) + const loadingMoreRef = useRef(false) + const startIndexLatest = useLatest(startIndex) + /** 用户是否在底部附近,用于决定新元素到来时是否跟随滚动 */ + const isAtBottomRef = useRef(true) + const isFirstMountRef = useRef(true) + + const getScrollEl = useMemoizedFn(() => scrollContainerRef?.current ?? scrollRef.current) + + /** 流式增高时,仅在底部则自动跟随 */ + useLayoutEffect(() => { + const scrollEl = getScrollEl() + const measureEl = contentMeasureRef.current + if (!scrollEl || !measureEl) return + + let rafId = 0 + const scrollToBottom = () => { + if (!isAtBottomRef.current) return + scrollEl.scrollTop = scrollEl.scrollHeight + } - const observer = new ResizeObserver(() => { - cancelAnimationFrame(rafId) - rafId = requestAnimationFrame(scrollToBottom) - }) - observer.observe(measureEl) + const observer = new ResizeObserver(() => { + cancelAnimationFrame(rafId) + rafId = requestAnimationFrame(scrollToBottom) + }) + observer.observe(measureEl) - return () => { - cancelAnimationFrame(rafId) - observer.disconnect() - } - }, [getScrollEl, scrollContainerRef]) + return () => { + cancelAnimationFrame(rafId) + observer.disconnect() + } + }, [getScrollEl, scrollContainerRef]) - /** 挂载 / 新元素时置底:首次强制,后续仅在底部时跟随 */ - useLayoutEffect(() => { - const scrollEl = getScrollEl() - if (!scrollEl) return + /** 挂载 / 新元素时置底:首次强制,后续仅在底部时跟随 */ + useLayoutEffect(() => { + const scrollEl = getScrollEl() + if (!scrollEl) return - const force = isFirstMountRef.current - isFirstMountRef.current = false + const force = isFirstMountRef.current + isFirstMountRef.current = false - const scrollToBottom = (forceScroll = false) => { - if (!forceScroll && !isAtBottomRef.current) return - scrollEl.scrollTop = scrollEl.scrollHeight - } + const scrollToBottom = (forceScroll = false) => { + if (!forceScroll && !isAtBottomRef.current) return + scrollEl.scrollTop = scrollEl.scrollHeight + } - scrollToBottom(force) + scrollToBottom(force) - if (!force) { - if (isAtBottomRef.current) { - requestAnimationFrame(() => scrollToBottom(false)) - } - return + if (!force) { + if (isAtBottomRef.current) { + requestAnimationFrame(() => scrollToBottom(false)) } + return + } - let rafId = 0 - const rafId2 = requestAnimationFrame(() => { - rafId = requestAnimationFrame(() => scrollToBottom(true)) - }) - - return () => { - cancelAnimationFrame(rafId2) - cancelAnimationFrame(rafId) - } - }, [childrenTokens.length, getScrollEl, scrollContainerRef]) - - /** 加载旧数据后保持滚动位置不跳动 */ - useLayoutEffect(() => { - if (!loadingMoreRef.current) return - const scrollEl = getScrollEl() - if (!scrollEl) return - scrollEl.scrollTop = scrollEl.scrollHeight - prevScrollHeightRef.current - loadingMoreRef.current = false - }, [getScrollEl, startIndex]) - - const handleScroll = useMemoizedFn(() => { - const scrollEl = getScrollEl() - if (!scrollEl) return - isAtBottomRef.current = scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 50 - // 滚动到顶部附近时加载更多旧数据 - if (scrollEl.scrollTop <= 50 && startIndexLatest.current > 0 && !loadingMoreRef.current) { - prevScrollHeightRef.current = scrollEl.scrollHeight - loadingMoreRef.current = true - setStartIndex((prev) => Math.max(0, prev - PAGE_SIZE)) - } + let rafId = 0 + const rafId2 = requestAnimationFrame(() => { + rafId = requestAnimationFrame(() => scrollToBottom(true)) }) - useEffect(() => { - if (!embedInParentScroll) return - const scrollEl = scrollContainerRef?.current - if (!scrollEl) return - scrollEl.addEventListener('scroll', handleScroll, { passive: true }) - return () => scrollEl.removeEventListener('scroll', handleScroll) - }, [embedInParentScroll, handleScroll, scrollContainerRef]) + return () => { + cancelAnimationFrame(rafId2) + cancelAnimationFrame(rafId) + } + }, [childrenTokens.length, getScrollEl, scrollContainerRef]) + + /** 加载旧数据后保持滚动位置不跳动 */ + useLayoutEffect(() => { + if (!loadingMoreRef.current) return + const scrollEl = getScrollEl() + if (!scrollEl) return + scrollEl.scrollTop = scrollEl.scrollHeight - prevScrollHeightRef.current + loadingMoreRef.current = false + }, [getScrollEl, startIndex]) + + const handleScroll = useMemoizedFn(() => { + const scrollEl = getScrollEl() + if (!scrollEl) return + isAtBottomRef.current = scrollEl.scrollHeight - scrollEl.scrollTop - scrollEl.clientHeight < 50 + // 滚动到顶部附近时加载更多旧数据 + if (scrollEl.scrollTop <= 50 && startIndexLatest.current > 0 && !loadingMoreRef.current) { + prevScrollHeightRef.current = scrollEl.scrollHeight + loadingMoreRef.current = true + setStartIndex((prev) => Math.max(0, prev - PAGE_SIZE)) + } + }) + + useEffect(() => { + if (!embedInParentScroll) return + const scrollEl = scrollContainerRef?.current + if (!scrollEl) return + scrollEl.addEventListener('scroll', handleScroll, { passive: true }) + return () => scrollEl.removeEventListener('scroll', handleScroll) + }, [embedInParentScroll, handleScroll, scrollContainerRef]) - return ( + return ( +