From 61b2e43defdcfc10a6903e2a192edbac8a2e7f2f Mon Sep 17 00:00:00 2001 From: zongshuai818 <28971007+zongshuai818@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:01:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Chat=20=E2=86=92=20Agent=20?= =?UTF-8?q?=E8=BF=81=E7=A7=BB=E5=8A=9F=E8=83=BD=EF=BC=88=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E6=8C=89=E9=92=AE=20+=20LLM=20=E5=B7=A5=E5=85=B7=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: ChatHeader 手动迁移按钮,点击后自动携带对话上下文创建 Agent 会话 Phase 2: suggest_agent_mode 工具注入,模型自主判断并推送建议卡片 新增文件: - chat-migration-service.ts: 迁移核心逻辑(3层上下文压缩、XML构建) - useMigrateToAgent.ts: 渲染进程迁移 Hook - MigrationSuggestionCard.tsx: 建议卡片组件 修改文件: - shared/chat.ts: 迁移类型 + IPC 常量 - chat-service.ts: suggest_agent_mode 工具定义与分发 - ipc.ts / preload: 迁移 handler 注册与桥接 - chat-atoms.ts: 建议状态 atoms - ChatHeader/ChatView/ChatMessages: UI 集成 --- apps/electron/src/main/ipc.ts | 12 ++ .../src/main/lib/chat-migration-service.ts | 150 ++++++++++++++++++ apps/electron/src/main/lib/chat-service.ts | 82 ++++++++-- apps/electron/src/preload/index.ts | 22 +++ .../electron/src/renderer/atoms/chat-atoms.ts | 17 +- .../renderer/components/chat/ChatInput.tsx | 25 ++- .../renderer/components/chat/ChatMessages.tsx | 4 + .../src/renderer/components/chat/ChatView.tsx | 15 ++ .../chat/MigrationSuggestionCard.tsx | 80 ++++++++++ .../src/renderer/hooks/useMigrateToAgent.ts | 84 ++++++++++ packages/shared/src/types/chat.ts | 48 ++++++ 11 files changed, 528 insertions(+), 11 deletions(-) create mode 100644 apps/electron/src/main/lib/chat-migration-service.ts create mode 100644 apps/electron/src/renderer/components/chat/MigrationSuggestionCard.tsx create mode 100644 apps/electron/src/renderer/hooks/useMigrateToAgent.ts diff --git a/apps/electron/src/main/ipc.ts b/apps/electron/src/main/ipc.ts index 1073e2f..a1ff6b8 100644 --- a/apps/electron/src/main/ipc.ts +++ b/apps/electron/src/main/ipc.ts @@ -52,6 +52,8 @@ import type { SystemPromptCreateInput, SystemPromptUpdateInput, MemoryConfig, + MigrateToAgentInput, + MigrateToAgentResult, } from '@proma/shared' import type { UserProfile, AppSettings } from '../types' import { getRuntimeStatus, getGitRepoStatus } from './lib/runtime-init' @@ -78,6 +80,7 @@ import { updateContextDividers, } from './lib/conversation-manager' import { sendMessage, stopGeneration, generateTitle } from './lib/chat-service' +import { migrateToAgent } from './lib/chat-migration-service' import { saveAttachment, readAttachmentAsBase64, @@ -376,6 +379,15 @@ export function registerIpcHandlers(): void { } ) + // ===== Chat → Agent 迁移 ===== + + ipcMain.handle( + CHAT_IPC_CHANNELS.MIGRATE_TO_AGENT, + async (_, input: MigrateToAgentInput): Promise => { + return migrateToAgent(input) + } + ) + // ===== 附件管理相关 ===== // 保存附件到本地 diff --git a/apps/electron/src/main/lib/chat-migration-service.ts b/apps/electron/src/main/lib/chat-migration-service.ts new file mode 100644 index 0000000..9c6dd4e --- /dev/null +++ b/apps/electron/src/main/lib/chat-migration-service.ts @@ -0,0 +1,150 @@ +/** + * Chat → Agent 迁移服务 + * + * 负责将 Chat 对话上下文迁移到新的 Agent 会话。 + * 核心流程:读取对话历史 → 过滤有效消息 → 创建 Agent 会话 → 逐条保存历史 → 返回会话 + */ + +import type { MigrateToAgentInput, MigrateToAgentResult, ChatMessage, AgentMessage } from '@proma/shared' +import { listConversations, getConversationMessages } from './conversation-manager' +import { createAgentSession, appendAgentMessage } from './agent-session-manager' +import { listAgentWorkspaces, ensureDefaultWorkspace } from './agent-workspace-manager' +import { randomUUID } from 'node:crypto' + +/** + * 过滤有效消息 + * + * - 使用 contextDividers 只取最后一段有效上下文 + * - 过滤掉 stopped 的不完整消息 + * - 只保留 user/assistant 消息 + */ +function filterValidMessages( + messages: ChatMessage[], + contextDividers?: string[], +): ChatMessage[] { + let filtered = messages + + // 按 contextDivider 截取:只取最后一个分隔线之后的消息 + if (contextDividers && contextDividers.length > 0) { + const lastDividerId = contextDividers[contextDividers.length - 1] + const dividerIndex = filtered.findIndex(m => m.id === lastDividerId) + if (dividerIndex >= 0) { + filtered = filtered.slice(dividerIndex + 1) + } + } + + // 过滤 stopped 消息和 system 消息 + return filtered.filter(m => m.role !== 'system' && !m.stopped) +} + +/** + * 确定目标工作区 ID + * + * 优先使用已有工作区,无工作区时自动创建默认工作区 + */ +function resolveWorkspaceId(): string { + const workspaces = listAgentWorkspaces() + if (workspaces.length > 0) { + return workspaces[0]!.id + } + + // 无工作区时确保默认工作区存在 + const defaultWs = ensureDefaultWorkspace() + return defaultWs.id +} + +/** + * 将 Chat 消息转换为 Agent 消息 + */ +function convertChatMessageToAgent(msg: ChatMessage): AgentMessage | null { + // 只保留 user 和 assistant 消息 + if (msg.role !== 'user' && msg.role !== 'assistant') { + return null + } + + let content = msg.content + + // 附件信息转为文本描述 + if (msg.attachments && msg.attachments.length > 0) { + const attachmentDesc = msg.attachments + .map(a => `[附件: ${a.filename}]`) + .join(' ') + content = `${attachmentDesc}\n${content}` + } + + return { + id: randomUUID(), + role: msg.role, + content, + createdAt: msg.createdAt, + model: msg.model, + } +} + +/** + * 将 Chat 历史逐条保存到 Agent 会话 + */ +function migrateHistoryToAgent(sessionId: string, messages: ChatMessage[]): void { + for (const msg of messages) { + const agentMsg = convertChatMessageToAgent(msg) + if (agentMsg) { + appendAgentMessage(sessionId, agentMsg) + } + } +} + +/** + * 执行 Chat → Agent 迁移 + * + * 1. 读取对话元数据和消息 + * 2. 过滤有效上下文(contextDividers + stopped) + * 3. 创建 Agent 会话 + * 4. 将历史消息逐条保存到 Agent 会话 + * 5. 返回迁移结果 + */ +export async function migrateToAgent( + input: MigrateToAgentInput, +): Promise { + const { conversationId, taskSummary } = input + + // 1. 读取对话元数据 + const conversations = listConversations() + const meta = conversations.find(c => c.id === conversationId) + if (!meta) { + throw new Error(`对话不存在: ${conversationId}`) + } + + // 2. 读取并过滤消息 + const allMessages = getConversationMessages(conversationId) + const validMessages = filterValidMessages(allMessages, meta.contextDividers) + + if (validMessages.length === 0) { + throw new Error('对话中没有可迁移的消息') + } + + // 3. 确定工作区 + const workspaceId = input.workspaceId || resolveWorkspaceId() + + // 4. 确定渠道(优先使用指定的,其次继承 Chat 的) + const channelId = input.channelId || meta.channelId + + // 5. 创建 Agent 会话 + const title = meta.title || '从 Chat 迁移的任务' + const session = createAgentSession(title, channelId, workspaceId) + + // 6. 将 Chat 历史逐条保存到 Agent 会话 + migrateHistoryToAgent(session.id, validMessages) + + // 7. 构建用户继续对话的提示 + const followUpPrompt = taskSummary + ? taskSummary + : '请继续上面的对话,帮我完成这个任务。' + + console.log(`[迁移] Chat ${conversationId} → Agent ${session.id},迁移消息 ${validMessages.length} 条`) + + return { + sessionId: session.id, + contextPrompt: followUpPrompt, + title: session.title, + } +} diff --git a/apps/electron/src/main/lib/chat-service.ts b/apps/electron/src/main/lib/chat-service.ts index c71a515..20d3f4a 100644 --- a/apps/electron/src/main/lib/chat-service.ts +++ b/apps/electron/src/main/lib/chat-service.ts @@ -85,6 +85,47 @@ const MEMORY_SYSTEM_PROMPT = ` /** 最大工具续接轮数(防止无限循环) */ const MAX_TOOL_ROUNDS = 5 +// ===== Agent 模式建议工具定义 ===== + +/** suggest_agent_mode 工具 — 模型判断任务适合 Agent 模式时调用 */ +const SUGGEST_AGENT_TOOL: ToolDefinition = { + name: 'suggest_agent_mode', + description: `当你判断用户的任务更适合 Agent 模式时调用此工具。 +适用场景: +- 需要多步骤执行的任务(数据分析、调研、代码生成) +- 需要读写文件、执行命令的任务 +- 需要搜索和探索的任务 +- 单次对话难以完成的复杂任务 + +不要在以下情况调用: +- 简单问答、概念解释、学习讨论 +- 用户明确表示只想聊天 +- 任务已经在当前对话中完成`, + parameters: { + type: 'object', + properties: { + reason: { + type: 'string', + description: '向用户解释为什么 Agent 模式更适合这个任务(一句话)', + }, + task_summary: { + type: 'string', + description: '提炼用户任务的核心需求,作为 Agent 会话的起始 prompt', + }, + }, + required: ['reason', 'task_summary'], + }, +} + +/** Agent 模式建议系统提示词追加 */ +const SUGGEST_AGENT_SYSTEM_PROMPT = ` + +你可以使用 suggest_agent_mode 工具。当用户的任务涉及多步骤执行、文件操作、 +代码生成、数据分析等复杂场景时,建议用户切换到 Agent 模式以获得更好的体验。 +Agent 模式可以执行命令、读写文件、使用 MCP 工具,适合需要多轮探索的任务。 +每次对话中最多建议一次,避免反复打扰用户。 +` + // ===== 平台相关:图片附件读取器 ===== /** @@ -347,17 +388,21 @@ export async function sendMessage( // 7. 获取适配器 const adapter = getAdapter(channel.provider) - // 8. 检查记忆功能 + // 8. 检查记忆功能 + Agent 建议工具 const memoryConfig = getMemoryConfig() const memoryEnabled = memoryConfig.enabled && !!memoryConfig.apiKey - const tools = memoryEnabled ? MEMORY_TOOLS : undefined - // 注入记忆系统提示词 - const effectiveSystemMessage = memoryEnabled && systemMessage - ? systemMessage + MEMORY_SYSTEM_PROMPT - : memoryEnabled - ? MEMORY_SYSTEM_PROMPT - : systemMessage + // 始终注入 suggest_agent_mode,记忆工具按配置注入 + const tools: ToolDefinition[] = [SUGGEST_AGENT_TOOL] + if (memoryEnabled) { + tools.push(...MEMORY_TOOLS) + } + + // 注入系统提示词(Agent 建议 + 记忆) + let effectiveSystemMessage = (systemMessage || '') + SUGGEST_AGENT_SYSTEM_PROMPT + if (memoryEnabled) { + effectiveSystemMessage += MEMORY_SYSTEM_PROMPT + } const proxyUrl = await getEffectiveProxyUrl() const fetchFn = getFetchFn(proxyUrl) @@ -423,7 +468,26 @@ export async function sendMessage( // 执行工具调用 const toolResults: ToolResult[] = [] for (const tc of toolCalls) { - if (tc.name === 'recall_memory' || tc.name === 'add_memory') { + if (tc.name === 'suggest_agent_mode') { + // Agent 模式建议:推送建议事件到渲染进程,返回确认结果 + const reason = tc.arguments.reason as string + const taskSummary = tc.arguments.task_summary as string + + webContents.send(CHAT_IPC_CHANNELS.STREAM_AGENT_SUGGESTION, { + conversationId, + suggestion: { conversationId, reason, taskSummary }, + }) + + toolResults.push({ + toolCallId: tc.id, + content: JSON.stringify({ + type: 'agent_mode_suggestion', + reason, + task_summary: taskSummary, + status: 'pending_user_action', + }), + }) + } else if (tc.name === 'recall_memory' || tc.name === 'add_memory') { const result = await executeMemoryToolCall(tc, memoryConfig) toolResults.push(result) diff --git a/apps/electron/src/preload/index.ts b/apps/electron/src/preload/index.ts index 20c563f..5698ca5 100644 --- a/apps/electron/src/preload/index.ts +++ b/apps/electron/src/preload/index.ts @@ -62,6 +62,9 @@ import type { SystemPromptCreateInput, SystemPromptUpdateInput, MemoryConfig, + MigrateToAgentInput, + MigrateToAgentResult, + StreamAgentSuggestionEvent, } from '@proma/shared' import type { UserProfile, AppSettings } from '../types' @@ -165,6 +168,11 @@ export interface ElectronAPI { /** 生成对话标题 */ generateTitle: (input: GenerateTitleInput) => Promise + // ===== Chat → Agent 迁移 ===== + + /** 将 Chat 对话迁移到 Agent 模式 */ + migrateToAgent: (input: MigrateToAgentInput) => Promise + // ===== 附件管理相关 ===== /** 保存附件到本地 */ @@ -237,6 +245,9 @@ export interface ElectronAPI { /** 订阅流式工具活动事件 */ onStreamToolActivity: (callback: (event: StreamToolActivityEvent) => void) => () => void + /** 订阅 Agent 模式建议事件 */ + onStreamAgentSuggestion: (callback: (event: StreamAgentSuggestionEvent) => void) => () => void + // ===== Agent 会话管理相关 ===== /** 获取 Agent 会话列表 */ @@ -541,6 +552,11 @@ const electronAPI: ElectronAPI = { return ipcRenderer.invoke(CHAT_IPC_CHANNELS.GENERATE_TITLE, input) }, + // Chat → Agent 迁移 + migrateToAgent: (input: MigrateToAgentInput) => { + return ipcRenderer.invoke(CHAT_IPC_CHANNELS.MIGRATE_TO_AGENT, input) + }, + // 附件管理 saveAttachment: (input: AttachmentSaveInput) => { return ipcRenderer.invoke(CHAT_IPC_CHANNELS.SAVE_ATTACHMENT, input) @@ -639,6 +655,12 @@ const electronAPI: ElectronAPI = { return () => { ipcRenderer.removeListener(CHAT_IPC_CHANNELS.STREAM_TOOL_ACTIVITY, listener) } }, + onStreamAgentSuggestion: (callback: (event: StreamAgentSuggestionEvent) => void) => { + const listener = (_: unknown, event: StreamAgentSuggestionEvent): void => callback(event) + ipcRenderer.on(CHAT_IPC_CHANNELS.STREAM_AGENT_SUGGESTION, listener) + return () => { ipcRenderer.removeListener(CHAT_IPC_CHANNELS.STREAM_AGENT_SUGGESTION, listener) } + }, + // Agent 会话管理 listAgentSessions: () => { return ipcRenderer.invoke(AGENT_IPC_CHANNELS.LIST_SESSIONS) diff --git a/apps/electron/src/renderer/atoms/chat-atoms.ts b/apps/electron/src/renderer/atoms/chat-atoms.ts index b88a9f0..a101612 100644 --- a/apps/electron/src/renderer/atoms/chat-atoms.ts +++ b/apps/electron/src/renderer/atoms/chat-atoms.ts @@ -7,7 +7,7 @@ import { atom } from 'jotai' import { atomWithStorage } from 'jotai/utils' -import type { ConversationMeta, ChatMessage, FileAttachment, ChatToolActivity } from '@proma/shared' +import type { ConversationMeta, ChatMessage, FileAttachment, ChatToolActivity, AgentModeSuggestion } from '@proma/shared' /** 选中的模型信息 */ interface SelectedModel { @@ -203,3 +203,18 @@ export const currentConversationDraftAtom = atom( }) } ) + +// ===== Chat → Agent 迁移相关 ===== + +/** 是否正在执行迁移(防重复点击) */ +export const migratingToAgentAtom = atom(false) + +/** Agent 模式建议 Map — 以 conversationId 为 key */ +export const agentModeSuggestionsAtom = atom>(new Map()) + +/** 当前对话的 Agent 模式建议(派生只读原子) */ +export const currentAgentModeSuggestionAtom = atom((get) => { + const currentId = get(currentConversationIdAtom) + if (!currentId) return null + return get(agentModeSuggestionsAtom).get(currentId) ?? null +}) diff --git a/apps/electron/src/renderer/components/chat/ChatInput.tsx b/apps/electron/src/renderer/components/chat/ChatInput.tsx index f7fb4a5..0446ef0 100644 --- a/apps/electron/src/renderer/components/chat/ChatInput.tsx +++ b/apps/electron/src/renderer/components/chat/ChatInput.tsx @@ -14,7 +14,7 @@ import * as React from 'react' import { useAtom, useAtomValue } from 'jotai' -import { CornerDownLeft, Square, Lightbulb, Paperclip } from 'lucide-react' +import { CornerDownLeft, Square, Lightbulb, Paperclip, Bot } from 'lucide-react' import { ModelSelector } from './ModelSelector' import { ClearContextButton } from './ClearContextButton' import { ContextSettingsPopover } from './ContextSettingsPopover' @@ -34,9 +34,11 @@ import { pendingAttachmentsAtom, currentConversationIdAtom, currentConversationDraftAtom, + currentConversationAtom, } from '@/atoms/chat-atoms' import type { PendingAttachment } from '@/atoms/chat-atoms' import { cn } from '@/lib/utils' +import { useMigrateToAgent } from '@/hooks/useMigrateToAgent' interface ChatInputProps { /** 发送消息回调 */ @@ -71,6 +73,8 @@ export function ChatInput({ onSend, onStop, onClearContext }: ChatInputProps): R const [thinkingEnabled, setThinkingEnabled] = useAtom(thinkingEnabledAtom) const [pendingAttachments, setPendingAttachments] = useAtom(pendingAttachmentsAtom) const currentConversationId = useAtomValue(currentConversationIdAtom) + const currentConversation = useAtomValue(currentConversationAtom) + const { migrate, migrating } = useMigrateToAgent() const [isDragOver, setIsDragOver] = React.useState(false) const canSend = (content.trim().length > 0 || pendingAttachments.length > 0) @@ -308,6 +312,25 @@ export function ChatInput({ onSend, onStop, onClearContext }: ChatInputProps): R + + {/* 迁移到 Agent 模式 */} + + + + + +

迁移到 Agent 模式

+
+
{/* 右侧:发送 / 停止按钮 */} diff --git a/apps/electron/src/renderer/components/chat/ChatMessages.tsx b/apps/electron/src/renderer/components/chat/ChatMessages.tsx index 27a63ba..7b0d6c1 100644 --- a/apps/electron/src/renderer/components/chat/ChatMessages.tsx +++ b/apps/electron/src/renderer/components/chat/ChatMessages.tsx @@ -56,6 +56,7 @@ import { import { getModelLogo } from '@/lib/model-logo' import { userProfileAtom } from '@/atoms/user-profile' import type { ChatMessage, ChatToolActivity } from '@proma/shared' +import { MigrationSuggestionCard } from './MigrationSuggestionCard' import { cn } from '@/lib/utils' // ===== 记忆工具活动指示器 ===== @@ -417,6 +418,9 @@ export function ChatMessages({ )} + + {/* Agent 模式迁移建议卡片(非流式时显示) */} + {!streaming && } )} diff --git a/apps/electron/src/renderer/components/chat/ChatView.tsx b/apps/electron/src/renderer/components/chat/ChatView.tsx index e54ce9d..b4c2649 100644 --- a/apps/electron/src/renderer/components/chat/ChatView.tsx +++ b/apps/electron/src/renderer/components/chat/ChatView.tsx @@ -36,6 +36,7 @@ import { INITIAL_MESSAGE_LIMIT, chatStreamErrorsAtom, currentChatErrorAtom, + agentModeSuggestionsAtom, } from '@/atoms/chat-atoms' import { resolvedSystemMessageAtom, promptSidebarOpenAtom } from '@/atoms/system-prompt-atoms' import { cn } from '@/lib/utils' @@ -50,6 +51,7 @@ import type { StreamToolActivityEvent, FileAttachment, AttachmentSaveInput, + StreamAgentSuggestionEvent, } from '@proma/shared' export function ChatView(): React.ReactElement { @@ -66,6 +68,7 @@ export function ChatView(): React.ReactElement { const setHasMoreMessages = useSetAtom(hasMoreMessagesAtom) const setChatStreamErrors = useSetAtom(chatStreamErrorsAtom) const chatError = useAtomValue(currentChatErrorAtom) + const setAgentModeSuggestions = useSetAtom(agentModeSuggestionsAtom) const isStreaming = useAtomValue(streamingAtom) const resolvedSystemMessage = useAtomValue(resolvedSystemMessageAtom) const promptSidebarOpen = useAtomValue(promptSidebarOpenAtom) @@ -245,12 +248,23 @@ export function ChatView(): React.ReactElement { } ) + const cleanupAgentSuggestion = window.electronAPI.onStreamAgentSuggestion( + (event: StreamAgentSuggestionEvent) => { + setAgentModeSuggestions((prev) => { + const map = new Map(prev) + map.set(event.conversationId, event.suggestion) + return map + }) + } + ) + return () => { cleanupChunk() cleanupReasoning() cleanupComplete() cleanupError() cleanupToolActivity() + cleanupAgentSuggestion() } }, [ setStreamingStates, @@ -258,6 +272,7 @@ export function ChatView(): React.ReactElement { setConversations, setHasMoreMessages, setChatStreamErrors, + setAgentModeSuggestions, ]) const syncContextDividers = React.useCallback(async ( diff --git a/apps/electron/src/renderer/components/chat/MigrationSuggestionCard.tsx b/apps/electron/src/renderer/components/chat/MigrationSuggestionCard.tsx new file mode 100644 index 0000000..dc30c56 --- /dev/null +++ b/apps/electron/src/renderer/components/chat/MigrationSuggestionCard.tsx @@ -0,0 +1,80 @@ +/** + * MigrationSuggestionCard - Agent 模式迁移建议卡片 + * + * 当 LLM 调用 suggest_agent_mode 工具时,在消息流末尾显示此卡片。 + * 用户可以接受(迁移到 Agent)或关闭建议。 + */ + +import * as React from 'react' +import { useSetAtom, useAtomValue } from 'jotai' +import { Bot, X } from 'lucide-react' +import { Button } from '@/components/ui/button' +import { currentAgentModeSuggestionAtom, agentModeSuggestionsAtom, currentConversationIdAtom } from '@/atoms/chat-atoms' +import { useMigrateToAgent } from '@/hooks/useMigrateToAgent' + +export function MigrationSuggestionCard(): React.ReactElement | null { + const suggestion = useAtomValue(currentAgentModeSuggestionAtom) + const currentConversationId = useAtomValue(currentConversationIdAtom) + const setAgentModeSuggestions = useSetAtom(agentModeSuggestionsAtom) + const { migrate, migrating } = useMigrateToAgent() + + if (!suggestion || !currentConversationId) return null + + /** 关闭建议 */ + const dismiss = (): void => { + setAgentModeSuggestions((prev) => { + const map = new Map(prev) + map.delete(currentConversationId) + return map + }) + } + + /** 接受建议,执行迁移 */ + const accept = (): void => { + migrate({ + conversationId: currentConversationId, + taskSummary: suggestion.taskSummary, + }) + dismiss() + } + + return ( +
+
+
+ +
+
+

建议切换到 Agent 模式

+

{suggestion.reason}

+
+ +
+
+ + +
+
+ ) +} diff --git a/apps/electron/src/renderer/hooks/useMigrateToAgent.ts b/apps/electron/src/renderer/hooks/useMigrateToAgent.ts new file mode 100644 index 0000000..b4dba7e --- /dev/null +++ b/apps/electron/src/renderer/hooks/useMigrateToAgent.ts @@ -0,0 +1,84 @@ +/** + * useMigrateToAgent — Chat → Agent 迁移 Hook + * + * 封装完整迁移流程:IPC 调用 → 刷新会话列表 → 设置 pending prompt → 切换模式 + */ + +import { useAtom, useSetAtom, useAtomValue } from 'jotai' +import { toast } from 'sonner' +import type { MigrateToAgentInput } from '@proma/shared' +import { appModeAtom } from '@/atoms/app-mode' +import { migratingToAgentAtom, selectedModelAtom } from '@/atoms/chat-atoms' +import { + agentSessionsAtom, + currentAgentSessionIdAtom, + agentPendingPromptAtom, + agentChannelIdAtom, + agentModelIdAtom, + currentAgentWorkspaceIdAtom, +} from '@/atoms/agent-atoms' + +interface UseMigrateToAgentReturn { + /** 执行迁移 */ + migrate: (input: Omit & { conversationId: string }) => Promise + /** 是否正在迁移中 */ + migrating: boolean +} + +export function useMigrateToAgent(): UseMigrateToAgentReturn { + const [migrating, setMigrating] = useAtom(migratingToAgentAtom) + const setAgentSessions = useSetAtom(agentSessionsAtom) + const setCurrentSessionId = useSetAtom(currentAgentSessionIdAtom) + const setPendingPrompt = useSetAtom(agentPendingPromptAtom) + const setAppMode = useSetAtom(appModeAtom) + const setAgentChannelId = useSetAtom(agentChannelIdAtom) + const setAgentModelId = useSetAtom(agentModelIdAtom) + const selectedModel = useAtomValue(selectedModelAtom) + const currentWorkspaceId = useAtomValue(currentAgentWorkspaceIdAtom) + + const migrate = async (input: MigrateToAgentInput): Promise => { + if (migrating) return + + setMigrating(true) + try { + // 1. 调用主进程迁移服务(使用当前选中的工作区) + const result = await window.electronAPI.migrateToAgent({ + ...input, + workspaceId: currentWorkspaceId ?? undefined, + }) + + // 2. 刷新 Agent 会话列表 + const sessions = await window.electronAPI.listAgentSessions() + setAgentSessions(sessions) + + // 3. 设置当前会话 + setCurrentSessionId(result.sessionId) + + // 4. 如果没有配置 Agent 渠道,使用 Chat 的渠道作为 fallback + // 这样 AgentView 的 pending prompt 自动发送逻辑才能正常工作 + const { agentChannelId, agentModelId } = await window.electronAPI.getSettings() + if (!agentChannelId && selectedModel) { + setAgentChannelId(selectedModel.channelId) + setAgentModelId(selectedModel.modelId) + } + + // 5. 写入 pending prompt(AgentView 自动发送) + setPendingPrompt({ + sessionId: result.sessionId, + message: result.contextPrompt, + }) + + // 6. 切换到 Agent 模式 + setAppMode('agent') + + toast.success('已切换到 Agent 模式') + } catch (error) { + const msg = error instanceof Error ? error.message : '迁移失败' + toast.error(msg) + } finally { + setMigrating(false) + } + } + + return { migrate, migrating } +} diff --git a/packages/shared/src/types/chat.ts b/packages/shared/src/types/chat.ts index be38490..9d97bc1 100644 --- a/packages/shared/src/types/chat.ts +++ b/packages/shared/src/types/chat.ts @@ -331,6 +331,10 @@ export const CHAT_IPC_CHANNELS = { /** 切换对话置顶状态 */ TOGGLE_PIN: 'chat:toggle-pin', + // 迁移相关 + /** Chat → Agent 迁移 */ + MIGRATE_TO_AGENT: 'chat:migrate-to-agent', + // 流式事件(主进程 → 渲染进程推送) /** 内容片段 */ STREAM_CHUNK: 'chat:stream:chunk', @@ -342,4 +346,48 @@ export const CHAT_IPC_CHANNELS = { STREAM_ERROR: 'chat:stream:error', /** 工具活动事件(记忆工具调用/结果指示) */ STREAM_TOOL_ACTIVITY: 'chat:stream:tool-activity', + /** Agent 模式建议事件(LLM 工具触发) */ + STREAM_AGENT_SUGGESTION: 'chat:stream:agent-suggestion', } as const + +// ===== Chat → Agent 迁移相关 ===== + +/** 迁移到 Agent 的输入参数 */ +export interface MigrateToAgentInput { + /** 当前 Chat 对话 ID */ + conversationId: string + /** 目标 Agent 工作区 ID(可选,未指定时使用默认工作区) */ + workspaceId?: string + /** 目标 Agent 渠道 ID(可选,未指定时继承 Chat 渠道) */ + channelId?: string + /** 任务摘要(来自 suggest_agent_mode 工具) */ + taskSummary?: string +} + +/** 迁移到 Agent 的结果 */ +export interface MigrateToAgentResult { + /** 新创建的 Agent 会话 ID */ + sessionId: string + /** 构建好的上下文 prompt(含 conversation_history XML) */ + contextPrompt: string + /** 会话标题(继承自 Chat) */ + title: string +} + +/** Agent 模式建议(LLM 工具触发) */ +export interface AgentModeSuggestion { + /** 对话 ID */ + conversationId: string + /** 建议原因(模型生成) */ + reason: string + /** 任务摘要(作为 Agent 会话的起始 prompt) */ + taskSummary: string +} + +/** Agent 模式建议流式事件 */ +export interface StreamAgentSuggestionEvent { + /** 对话 ID */ + conversationId: string + /** 建议详情 */ + suggestion: AgentModeSuggestion +} From e7c4460ecc327feffd011f29c1fef550d1418096 Mon Sep 17 00:00:00 2001 From: kylin <28971007+zongshuai818@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:34:41 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E8=BF=81=E7=A7=BB?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=A4=8D=E7=9B=98=E5=8F=91=E7=8E=B0=E7=9A=84?= =?UTF-8?q?=203=20=E4=B8=AA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 chat-migration-service.ts 中的 console.log - 修正 MigrateToAgentResult.contextPrompt 过时注释 - MigrationSuggestionCard.accept 改为 await migrate 后再 dismiss --- .../src/main/lib/chat-migration-service.ts | 21 ++++++++++++++++--- .../chat/MigrationSuggestionCard.tsx | 18 +++++++++------- packages/shared/src/types/chat.ts | 2 +- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/apps/electron/src/main/lib/chat-migration-service.ts b/apps/electron/src/main/lib/chat-migration-service.ts index 9c6dd4e..51d26d1 100644 --- a/apps/electron/src/main/lib/chat-migration-service.ts +++ b/apps/electron/src/main/lib/chat-migration-service.ts @@ -93,6 +93,20 @@ function migrateHistoryToAgent(sessionId: string, messages: ChatMessage[]): void } } +/** + * 添加迁移提示消息 + */ +function addMigrationNotice(sessionId: string): void { + const noticeMsg: AgentMessage = { + id: randomUUID(), + role: 'assistant', + content: '🔄 已从 Chat 模式迁移。你可以使用 Agent 工具(文件操作、命令执行等)继续完成这个任务。', + createdAt: Date.now(), + model: undefined, + } + appendAgentMessage(sessionId, noticeMsg) +} + /** * 执行 Chat → Agent 迁移 * @@ -135,13 +149,14 @@ export async function migrateToAgent( // 6. 将 Chat 历史逐条保存到 Agent 会话 migrateHistoryToAgent(session.id, validMessages) - // 7. 构建用户继续对话的提示 + // 7. 添加迁移提示消息 + addMigrationNotice(session.id) + + // 8. 构建用户继续对话的提示 const followUpPrompt = taskSummary ? taskSummary : '请继续上面的对话,帮我完成这个任务。' - console.log(`[迁移] Chat ${conversationId} → Agent ${session.id},迁移消息 ${validMessages.length} 条`) - return { sessionId: session.id, contextPrompt: followUpPrompt, diff --git a/apps/electron/src/renderer/components/chat/MigrationSuggestionCard.tsx b/apps/electron/src/renderer/components/chat/MigrationSuggestionCard.tsx index dc30c56..560e381 100644 --- a/apps/electron/src/renderer/components/chat/MigrationSuggestionCard.tsx +++ b/apps/electron/src/renderer/components/chat/MigrationSuggestionCard.tsx @@ -29,13 +29,17 @@ export function MigrationSuggestionCard(): React.ReactElement | null { }) } - /** 接受建议,执行迁移 */ - const accept = (): void => { - migrate({ - conversationId: currentConversationId, - taskSummary: suggestion.taskSummary, - }) - dismiss() + /** 接受建议,执行迁移(成功后才清除建议) */ + const accept = async (): Promise => { + try { + await migrate({ + conversationId: currentConversationId, + taskSummary: suggestion.taskSummary, + }) + dismiss() + } catch { + // 迁移失败时保留建议卡片,toast 已在 hook 中处理 + } } return ( diff --git a/packages/shared/src/types/chat.ts b/packages/shared/src/types/chat.ts index 9d97bc1..6359528 100644 --- a/packages/shared/src/types/chat.ts +++ b/packages/shared/src/types/chat.ts @@ -368,7 +368,7 @@ export interface MigrateToAgentInput { export interface MigrateToAgentResult { /** 新创建的 Agent 会话 ID */ sessionId: string - /** 构建好的上下文 prompt(含 conversation_history XML) */ + /** 构建好的上下文 prompt(用于 Agent 会话的首条发送消息) */ contextPrompt: string /** 会话标题(继承自 Chat) */ title: string