From 344aab37f5e5e836636507c7af3686f5dedb6c96 Mon Sep 17 00:00:00 2001 From: digua Date: Fri, 5 Dec 2025 23:04:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=A7=81=E8=81=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/ai/agent.ts | 147 +++++- electron/main/ipc/ai.ts | 51 +- electron/preload/index.d.ts | 12 +- electron/preload/index.ts | 29 +- src/components.d.ts | 3 + src/components/analysis/AITab.vue | 2 + .../analysis/PrivateOverviewTab.vue | 437 ++++++++++++++++++ .../analysis/PrivateTimelineTab.vue | 227 +++++++++ src/components/analysis/ai/ChatExplorer.vue | 104 ++++- src/components/analysis/ai/ChatMessage.vue | 4 +- src/components/common/SettingModal.vue | 7 + src/components/common/Sidebar.vue | 26 +- .../common/settings/AIConfigTab.vue | 14 +- .../common/settings/AIPromptConfigTab.vue | 201 ++++++++ .../common/settings/AIPromptEditModal.vue | 275 +++++++++++ src/components/common/settings/index.ts | 2 + src/composables/useAIChat.ts | 48 +- src/pages/group-chat.vue | 1 + src/pages/index.vue | 13 +- src/pages/private-chat.vue | 332 +++++++++++++ src/routes/index.ts | 5 + src/stores/chat.ts | 175 ++++++- src/types/chat.ts | 29 ++ 23 files changed, 2057 insertions(+), 87 deletions(-) create mode 100644 src/components/analysis/PrivateOverviewTab.vue create mode 100644 src/components/analysis/PrivateTimelineTab.vue create mode 100644 src/components/common/settings/AIPromptConfigTab.vue create mode 100644 src/components/common/settings/AIPromptEditModal.vue create mode 100644 src/pages/private-chat.vue diff --git a/electron/main/ai/agent.ts b/electron/main/ai/agent.ts index b50c5c9..646e5a9 100644 --- a/electron/main/ai/agent.ts +++ b/electron/main/ai/agent.ts @@ -111,10 +111,23 @@ export interface AgentResult { toolRounds: number } +// ==================== 提示词配置类型 ==================== + /** - * 获取系统提示词 + * 用户自定义提示词配置 */ -function getSystemPrompt(): string { +export interface PromptConfig { + /** 角色定义(可编辑区) */ + roleDefinition: string + /** 回答要求(可编辑区) */ + responseRules: string +} + +/** + * 获取系统锁定部分的提示词(工具说明、时间处理等) + * @param chatType 聊天类型 ('group' | 'private') + */ +function getLockedPromptSection(chatType: 'group' | 'private'): string { const now = new Date() const currentDate = now.toLocaleDateString('zh-CN', { year: 'numeric', @@ -123,39 +136,95 @@ function getSystemPrompt(): string { weekday: 'long', }) - return `你是一个群聊记录分析助手。当前日期是 ${currentDate}。 + const isPrivate = chatType === 'private' + const chatTypeDesc = isPrivate ? '私聊记录' : '群聊记录' -你可以使用以下工具来获取群聊数据: + // 成员说明(私聊只有2人) + const memberNote = isPrivate + ? `成员查询策略: +- 私聊只有两个人,可以直接获取成员列表 +- 当用户提到"对方"、"他/她"时,通过 get_group_members 获取另一方信息 +` + : `成员查询策略: +- 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_group_members 获取成员列表 +- 群成员有三种名称:accountName(QQ原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名) +- 通过 get_group_members 的 search 参数可以模糊搜索这三种名称 +- 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言 +` + + return `当前日期是 ${currentDate}。 + +你可以使用以下工具来获取${chatTypeDesc}数据: 1. search_messages - 根据关键词搜索聊天记录,可指定 year/month 筛选时间段,也可指定 sender_id 筛选特定成员的发言 2. get_recent_messages - 获取指定时间段的聊天消息,可指定 year 和 month 3. get_member_stats - 获取成员活跃度统计 4. get_time_stats - 获取时间分布统计 -5. get_group_members - 获取群成员列表,包括 id、QQ号、账号名称、群昵称、别名和消息统计 +5. get_group_members - 获取成员列表,包括 id、QQ号、账号名称、昵称、别名和消息统计 6. get_member_name_history - 获取成员的昵称变更历史,需要先通过 get_group_members 获取成员 ID 7. get_conversation_between - 获取两个成员之间的对话记录,需要先通过 get_group_members 获取两人的成员 ID -成员查询策略: -- 当用户提到特定群成员(如"张三说过什么"、"小明的发言"等)时,应先调用 get_group_members 获取成员列表 -- 群成员有三种名称:accountName(QQ原始昵称)、groupNickname(群昵称)、aliases(用户自定义别名) -- 通过 get_group_members 的 search 参数可以模糊搜索这三种名称 -- 找到成员后,使用其 id 字段作为 search_messages 的 sender_id 参数来获取该成员的发言 - +${memberNote} 时间处理要求: - 如果用户提到"X月"但没有指定年份,默认使用当前年份(${now.getFullYear()}年) - 如果当前月份还没到用户提到的月份,则使用去年 - 例如:现在是${now.getFullYear()}年${now.getMonth() + 1}月,用户问"10月的聊天"应该查询${now.getMonth() + 1 >= 10 ? now.getFullYear() : now.getFullYear() - 1}年10月 -根据用户的问题,选择合适的工具获取数据,然后基于数据给出回答。 +根据用户的问题,选择合适的工具获取数据,然后基于数据给出回答。` +} -回答要求: -1. 基于工具返回的数据回答,不要编造信息 +/** + * 获取默认的角色定义 + */ +function getDefaultRoleDefinition(chatType: 'group' | 'private'): string { + if (chatType === 'private') { + return `你是一个专业的私聊记录分析助手。 +你的任务是帮助用户理解和分析他们的私聊记录数据。 + +注意:这是一个私聊对话,只有两个人参与。你的分析应该关注: +- 两人之间的对话互动 +- 谁更主动、谁回复更多 +- 对话的主题和内容变化 +- 不要使用"群"这个词,使用"对话"或"聊天"` + } + + return `你是一个专业的群聊记录分析助手。 +你的任务是帮助用户理解和分析他们的群聊记录数据。` +} + +/** + * 获取默认的回答要求 + */ +function getDefaultResponseRules(): string { + return `1. 基于工具返回的数据回答,不要编造信息 2. 如果数据不足以回答问题,请说明 3. 回答要简洁明了,使用 Markdown 格式 4. 可以引用具体的发言作为证据 5. 对于统计数据,可以适当总结趋势和特点` } +/** + * 构建完整的系统提示词 + * @param chatType 聊天类型 ('group' | 'private') + * @param promptConfig 用户自定义提示词配置(可选) + */ +function buildSystemPrompt(chatType: 'group' | 'private' = 'group', promptConfig?: PromptConfig): string { + // 使用用户配置或默认配置 + const roleDefinition = promptConfig?.roleDefinition || getDefaultRoleDefinition(chatType) + const responseRules = promptConfig?.responseRules || getDefaultResponseRules() + + // 获取锁定的系统部分 + const lockedSection = getLockedPromptSection(chatType) + + // 组合完整提示词 + return `${roleDefinition} + +${lockedSection} + +回答要求: +${responseRules}` +} + /** * Agent 执行器类 * 处理带 Function Calling 的对话流程 @@ -167,10 +236,22 @@ export class Agent { private toolsUsed: string[] = [] private toolRounds: number = 0 private abortSignal?: AbortSignal + private historyMessages: ChatMessage[] = [] + private chatType: 'group' | 'private' = 'group' + private promptConfig?: PromptConfig - constructor(context: ToolContext, config: AgentConfig = {}) { + constructor( + context: ToolContext, + config: AgentConfig = {}, + historyMessages: ChatMessage[] = [], + chatType: 'group' | 'private' = 'group', + promptConfig?: PromptConfig + ) { this.context = context this.abortSignal = config.abortSignal + this.historyMessages = historyMessages + this.chatType = chatType + this.promptConfig = promptConfig this.config = { maxToolRounds: config.maxToolRounds ?? 5, llmOptions: config.llmOptions ?? { temperature: 0.7, maxTokens: 2048 }, @@ -189,7 +270,10 @@ export class Agent { * @param userMessage 用户消息 */ async execute(userMessage: string): Promise { - aiLogger.info('Agent', '开始执行', { userMessage: userMessage.slice(0, 100) }) + aiLogger.info('Agent', '开始执行', { + userMessage: userMessage.slice(0, 100), + historyLength: this.historyMessages.length, + }) // 检查是否已中止 if (this.isAborted()) { @@ -197,9 +281,10 @@ export class Agent { return { content: '', toolsUsed: [], toolRounds: 0 } } - // 初始化消息 + // 初始化消息(包含历史记录) this.messages = [ - { role: 'system', content: getSystemPrompt() }, + { role: 'system', content: buildSystemPrompt(this.chatType, this.promptConfig) }, + ...this.historyMessages, // 插入历史对话 { role: 'user', content: userMessage }, ] this.toolsUsed = [] @@ -307,7 +392,10 @@ export class Agent { * @param onChunk 流式回调 */ async executeStream(userMessage: string, onChunk: (chunk: AgentStreamChunk) => void): Promise { - aiLogger.info('Agent', '开始流式执行', { userMessage: userMessage.slice(0, 100) }) + aiLogger.info('Agent', '开始流式执行', { + userMessage: userMessage.slice(0, 100), + historyLength: this.historyMessages.length, + }) // 检查是否已中止 if (this.isAborted()) { @@ -316,9 +404,10 @@ export class Agent { return { content: '', toolsUsed: [], toolRounds: 0 } } - // 初始化消息 + // 初始化消息(包含历史记录) this.messages = [ - { role: 'system', content: getSystemPrompt() }, + { role: 'system', content: buildSystemPrompt(this.chatType, this.promptConfig) }, + ...this.historyMessages, // 插入历史对话 { role: 'user', content: userMessage }, ] this.toolsUsed = [] @@ -579,8 +668,14 @@ export class Agent { /** * 创建 Agent 并执行对话(便捷函数) */ -export async function runAgent(userMessage: string, context: ToolContext, config?: AgentConfig): Promise { - const agent = new Agent(context, config) +export async function runAgent( + userMessage: string, + context: ToolContext, + config?: AgentConfig, + historyMessages?: ChatMessage[], + chatType?: 'group' | 'private' +): Promise { + const agent = new Agent(context, config, historyMessages, chatType) return agent.execute(userMessage) } @@ -591,8 +686,10 @@ export async function runAgentStream( userMessage: string, context: ToolContext, onChunk: (chunk: AgentStreamChunk) => void, - config?: AgentConfig + config?: AgentConfig, + historyMessages?: ChatMessage[], + chatType?: 'group' | 'private' ): Promise { - const agent = new Agent(context, config) + const agent = new Agent(context, config, historyMessages, chatType) return agent.executeStream(userMessage, onChunk) } diff --git a/electron/main/ipc/ai.ts b/electron/main/ipc/ai.ts index af551f3..44d64c5 100644 --- a/electron/main/ipc/ai.ts +++ b/electron/main/ipc/ai.ts @@ -3,7 +3,7 @@ import { ipcMain, BrowserWindow } from 'electron' import * as aiConversations from '../ai/conversations' import * as llm from '../ai/llm' import { aiLogger } from '../ai/logger' -import { Agent, type AgentStreamChunk } from '../ai/agent' +import { Agent, type AgentStreamChunk, type PromptConfig } from '../ai/agent' import type { ToolContext } from '../ai/tools/types' import type { IpcContext } from './types' @@ -424,19 +424,48 @@ export function registerAIHandlers({ win }: IpcContext): void { /** * 执行 Agent 对话(流式) * Agent 会自动调用工具并返回最终结果 + * @param historyMessages 对话历史(可选,用于上下文关联) + * @param chatType 聊天类型('group' | 'private') + * @param promptConfig 用户自定义提示词配置(可选) */ - ipcMain.handle('agent:runStream', async (_, requestId: string, userMessage: string, context: ToolContext) => { - aiLogger.info('IPC', `收到 Agent 流式请求: ${requestId}`, { - userMessage: userMessage.slice(0, 100), - sessionId: context.sessionId, - }) + ipcMain.handle( + 'agent:runStream', + async ( + _, + requestId: string, + userMessage: string, + context: ToolContext, + historyMessages?: Array<{ role: 'user' | 'assistant'; content: string }>, + chatType?: 'group' | 'private', + promptConfig?: PromptConfig + ) => { + aiLogger.info('IPC', `收到 Agent 流式请求: ${requestId}`, { + userMessage: userMessage.slice(0, 100), + sessionId: context.sessionId, + historyLength: historyMessages?.length ?? 0, + chatType: chatType ?? 'group', + hasPromptConfig: !!promptConfig, + }) - try { - // 创建 AbortController 并存储 - const abortController = new AbortController() - activeAgentRequests.set(requestId, abortController) + try { + // 创建 AbortController 并存储 + const abortController = new AbortController() + activeAgentRequests.set(requestId, abortController) - const agent = new Agent(context, { abortSignal: abortController.signal }) + // 转换历史消息格式 + const formattedHistory = + historyMessages?.map((msg) => ({ + role: msg.role as 'user' | 'assistant', + content: msg.content, + })) ?? [] + + const agent = new Agent( + context, + { abortSignal: abortController.signal }, + formattedHistory, + chatType ?? 'group', + promptConfig + ) // 异步执行,通过事件发送流式数据 ;(async () => { diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 41f4dbb..ef2683c 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -269,11 +269,20 @@ interface ToolContext { timeFilter?: { startTs: number; endTs: number } } +// 用户自定义提示词配置 +interface PromptConfig { + roleDefinition: string + responseRules: string +} + interface AgentApi { runStream: ( userMessage: string, context: ToolContext, - onChunk?: (chunk: AgentStreamChunk) => void + onChunk?: (chunk: AgentStreamChunk) => void, + historyMessages?: Array<{ role: 'user' | 'assistant'; content: string }>, + chatType?: 'group' | 'private', + promptConfig?: PromptConfig ) => { requestId: string; promise: Promise<{ success: boolean; result?: AgentResult; error?: string }> } abort: (requestId: string) => Promise<{ success: boolean; error?: string }> } @@ -336,6 +345,7 @@ export { AgentStreamChunk, AgentResult, ToolContext, + PromptConfig, CacheDirectoryInfo, CacheInfo, } diff --git a/electron/preload/index.ts b/electron/preload/index.ts index ac75217..8e46875 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -723,20 +723,41 @@ const llmApi = { }, } +// 用户自定义提示词配置 +interface PromptConfig { + roleDefinition: string + responseRules: string +} + // Agent API - AI Agent 功能(带 Function Calling) const agentApi = { /** * 执行 Agent 对话(流式) * Agent 会自动调用工具获取数据并生成回答 + * @param historyMessages 对话历史(可选,用于上下文关联) + * @param chatType 聊天类型('group' | 'private') + * @param promptConfig 用户自定义提示词配置(可选) * @returns 返回 { requestId, promise },requestId 可用于中止请求 */ runStream: ( userMessage: string, context: ToolContext, - onChunk?: (chunk: AgentStreamChunk) => void + onChunk?: (chunk: AgentStreamChunk) => void, + historyMessages?: Array<{ role: 'user' | 'assistant'; content: string }>, + chatType?: 'group' | 'private', + promptConfig?: PromptConfig ): { requestId: string; promise: Promise<{ success: boolean; result?: AgentResult; error?: string }> } => { const requestId = `agent_${Date.now()}_${Math.random().toString(36).slice(2, 8)}` - console.log('[preload] Agent runStream 开始,requestId:', requestId) + console.log( + '[preload] Agent runStream 开始,requestId:', + requestId, + 'historyLength:', + historyMessages?.length ?? 0, + 'chatType:', + chatType ?? 'group', + 'hasPromptConfig:', + !!promptConfig + ) const promise = new Promise<{ success: boolean; result?: AgentResult; error?: string }>((resolve) => { // 监听流式 chunks @@ -764,9 +785,9 @@ const agentApi = { ipcRenderer.on('agent:streamChunk', chunkHandler) ipcRenderer.on('agent:complete', completeHandler) - // 发起请求 + // 发起请求(传递历史消息、聊天类型和提示词配置) ipcRenderer - .invoke('agent:runStream', requestId, userMessage, context) + .invoke('agent:runStream', requestId, userMessage, context, historyMessages, chatType, promptConfig) .then((result) => { console.log('[preload] Agent invoke 返回:', result) if (!result.success) { diff --git a/src/components.d.ts b/src/components.d.ts index cfcc8f6..267f5ba 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -13,6 +13,7 @@ declare module 'vue' { export interface GlobalComponents { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + UAccordion: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Accordion.vue')['default'] UAlert: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Alert.vue')['default'] UApp: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/App.vue')['default'] UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default'] @@ -24,9 +25,11 @@ declare module 'vue' { UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default'] UInputTags: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/InputTags.vue')['default'] UModal: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Modal.vue')['default'] + UPopover: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Popover.vue')['default'] UProgress: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Progress.vue')['default'] USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default'] UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default'] + UTextarea: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Textarea.vue')['default'] UTooltip: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.2.1_@babel+parser@7.28.5_axios@1.13.2_embla-carousel@8.6.0_typescript@5.9.3__1572391ae10a8169a5c9784ec5cec455/node_modules/@nuxt/ui/dist/runtime/components/Tooltip.vue')['default'] } } diff --git a/src/components/analysis/AITab.vue b/src/components/analysis/AITab.vue index 6ea5330..53774f3 100644 --- a/src/components/analysis/AITab.vue +++ b/src/components/analysis/AITab.vue @@ -8,6 +8,7 @@ defineProps<{ sessionId: string sessionName: string timeFilter?: { startTs: number; endTs: number } + chatType?: 'group' | 'private' }>() // 子 Tab 配置 @@ -50,6 +51,7 @@ defineExpose({ :session-id="sessionId" :session-name="sessionName" :time-filter="timeFilter" + :chat-type="chatType" /> diff --git a/src/components/analysis/PrivateOverviewTab.vue b/src/components/analysis/PrivateOverviewTab.vue new file mode 100644 index 0000000..ee7d0b9 --- /dev/null +++ b/src/components/analysis/PrivateOverviewTab.vue @@ -0,0 +1,437 @@ + + + diff --git a/src/components/analysis/PrivateTimelineTab.vue b/src/components/analysis/PrivateTimelineTab.vue new file mode 100644 index 0000000..db75604 --- /dev/null +++ b/src/components/analysis/PrivateTimelineTab.vue @@ -0,0 +1,227 @@ + + + diff --git a/src/components/analysis/ai/ChatExplorer.vue b/src/components/analysis/ai/ChatExplorer.vue index 579bccd..1b1d8fa 100644 --- a/src/components/analysis/ai/ChatExplorer.vue +++ b/src/components/analysis/ai/ChatExplorer.vue @@ -1,6 +1,7 @@