Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/electron/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -376,6 +379,15 @@ export function registerIpcHandlers(): void {
}
)

// ===== Chat → Agent 迁移 =====

ipcMain.handle(
CHAT_IPC_CHANNELS.MIGRATE_TO_AGENT,
async (_, input: MigrateToAgentInput): Promise<MigrateToAgentResult> => {
return migrateToAgent(input)
}
)

// ===== 附件管理相关 =====

// 保存附件到本地
Expand Down
165 changes: 165 additions & 0 deletions apps/electron/src/main/lib/chat-migration-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* 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)
}
}
}

/**
* 添加迁移提示消息
*/
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 迁移
*
* 1. 读取对话元数据和消息
* 2. 过滤有效上下文(contextDividers + stopped)
* 3. 创建 Agent 会话
* 4. 将历史消息逐条保存到 Agent 会话
* 5. 返回迁移结果
*/
export async function migrateToAgent(
input: MigrateToAgentInput,
): Promise<MigrateToAgentResult> {
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. 添加迁移提示消息
addMigrationNotice(session.id)

// 8. 构建用户继续对话的提示
const followUpPrompt = taskSummary
? taskSummary
: '请继续上面的对话,帮我完成这个任务。'

return {
sessionId: session.id,
contextPrompt: followUpPrompt,
title: session.title,
}
}
82 changes: 73 additions & 9 deletions apps/electron/src/main/lib/chat-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<agent_mode_hint>
你可以使用 suggest_agent_mode 工具。当用户的任务涉及多步骤执行、文件操作、
代码生成、数据分析等复杂场景时,建议用户切换到 Agent 模式以获得更好的体验。
Agent 模式可以执行命令、读写文件、使用 MCP 工具,适合需要多轮探索的任务。
每次对话中最多建议一次,避免反复打扰用户。
</agent_mode_hint>`

// ===== 平台相关:图片附件读取器 =====

/**
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
22 changes: 22 additions & 0 deletions apps/electron/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ import type {
SystemPromptCreateInput,
SystemPromptUpdateInput,
MemoryConfig,
MigrateToAgentInput,
MigrateToAgentResult,
StreamAgentSuggestionEvent,
} from '@proma/shared'
import type { UserProfile, AppSettings } from '../types'

Expand Down Expand Up @@ -165,6 +168,11 @@ export interface ElectronAPI {
/** 生成对话标题 */
generateTitle: (input: GenerateTitleInput) => Promise<string | null>

// ===== Chat → Agent 迁移 =====

/** 将 Chat 对话迁移到 Agent 模式 */
migrateToAgent: (input: MigrateToAgentInput) => Promise<MigrateToAgentResult>

// ===== 附件管理相关 =====

/** 保存附件到本地 */
Expand Down Expand Up @@ -237,6 +245,9 @@ export interface ElectronAPI {
/** 订阅流式工具活动事件 */
onStreamToolActivity: (callback: (event: StreamToolActivityEvent) => void) => () => void

/** 订阅 Agent 模式建议事件 */
onStreamAgentSuggestion: (callback: (event: StreamAgentSuggestionEvent) => void) => () => void

// ===== Agent 会话管理相关 =====

/** 获取 Agent 会话列表 */
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 16 additions & 1 deletion apps/electron/src/renderer/atoms/chat-atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -203,3 +203,18 @@ export const currentConversationDraftAtom = atom(
})
}
)

// ===== Chat → Agent 迁移相关 =====

/** 是否正在执行迁移(防重复点击) */
export const migratingToAgentAtom = atom<boolean>(false)

/** Agent 模式建议 Map — 以 conversationId 为 key */
export const agentModeSuggestionsAtom = atom<Map<string, AgentModeSuggestion>>(new Map())

/** 当前对话的 Agent 模式建议(派生只读原子) */
export const currentAgentModeSuggestionAtom = atom<AgentModeSuggestion | null>((get) => {
const currentId = get(currentConversationIdAtom)
if (!currentId) return null
return get(agentModeSuggestionsAtom).get(currentId) ?? null
})
Loading