diff --git a/src/renderer/src/components/common/UpdateNotification.tsx b/src/renderer/src/components/common/UpdateNotification.tsx index 9b2c05b..e6ccddd 100644 --- a/src/renderer/src/components/common/UpdateNotification.tsx +++ b/src/renderer/src/components/common/UpdateNotification.tsx @@ -49,6 +49,8 @@ export default function UpdateNotification() { const handleUpdateAvailable = (info: UpdateInfo) => { console.log('收到更新可用事件:', info) updateStore.handleUpdateAvailable(info) + // 重置启动检查标识(有更新可用时需要显示通知) + updateStore.setIsStartupCheck(false) showUpdateAvailableNotification(info) } @@ -61,6 +63,8 @@ export default function UpdateNotification() { const handleUpdateError = (error: string) => { console.error('收到更新错误事件:', error) updateStore.handleUpdateError(error) + // 重置启动检查标识 + updateStore.setIsStartupCheck(false) // 静默处理错误,避免过多通知打扰用户 console.warn('Update check failed:', error) } @@ -68,6 +72,15 @@ export default function UpdateNotification() { const handleUpdateNotAvailable = (info: any) => { console.log('收到无更新事件:', info) updateStore.handleUpdateNotAvailable(info) + + // 如果是启动检查且已是最新版本,不显示通知 + if (updateStore.isStartupCheck) { + console.log('启动检查:已是最新版本,不显示通知') + updateStore.setIsStartupCheck(false) // 重置标识 + return + } + + // 手动检查时显示通知 notification.info({ message: '当前已是最新版本', description: `当前版本: ${info.version || '未知'}`, @@ -114,12 +127,17 @@ export default function UpdateNotification() { return } + // 标记为启动检查 + updateStore.setIsStartupCheck(true) + const result = await window.api.updater.checkForUpdates() console.log('启动检查更新结果:', result) } catch (error) { console.error('启动检查更新失败:', error) // 静默失败,不打扰用户 console.warn('Startup update check failed:', error) + // 重置启动检查标识 + updateStore.setIsStartupCheck(false) } } diff --git a/src/renderer/src/components/layout/Layout.tsx b/src/renderer/src/components/layout/Layout.tsx index da3ee0a..6f6734d 100644 --- a/src/renderer/src/components/layout/Layout.tsx +++ b/src/renderer/src/components/layout/Layout.tsx @@ -6,7 +6,6 @@ import Sidebar from './sidebar/Sidebar' import ActivityBar, { ActivityBarTab } from './activitybar/ActivityBar' import TabsArea from './tabs/TabsArea' import ResizeHandle from './ResizeHandle' -import Settings from '../settings/Settings' import GlobalSearch from './sidebar_items/search/GlobalSearch' import TitleBar from './titlebar/TitleBar' @@ -17,7 +16,6 @@ export default function Layout() { const { clearSearch } = useSearchStore() const [activeTab, setActiveTab] = useState('explore') const [searchOpen, setSearchOpen] = useState(false) - const [settingsOpen, setSettingsOpen] = useState(false) // 处理键盘快捷键 useEffect(() => { @@ -59,10 +57,6 @@ export default function Layout() { setSearchOpen(true) } - const handleSettingsOpen = () => { - setSettingsOpen(true) - } - return (
{/* 自定义标题栏 */} @@ -74,7 +68,7 @@ export default function Layout() { {/* ActivityBar */} - + @@ -91,7 +85,7 @@ export default function Layout() { collapsed={sidebarCollapsed} activeTab={activeTab} onSearchOpen={handleSearchOpen} - onSettingsOpen={handleSettingsOpen} + onSettingsOpen={handleSearchOpen} /> @@ -103,7 +97,6 @@ export default function Layout() { {/* 全局模态框 */} - setSettingsOpen(false)} />
) } diff --git a/src/renderer/src/components/layout/activitybar/ActivityBar.tsx b/src/renderer/src/components/layout/activitybar/ActivityBar.tsx index 614a3d6..93e80ea 100644 --- a/src/renderer/src/components/layout/activitybar/ActivityBar.tsx +++ b/src/renderer/src/components/layout/activitybar/ActivityBar.tsx @@ -2,8 +2,11 @@ import React from 'react' import { Button, Badge, Tooltip } from 'antd' import { FolderOutlined, SearchOutlined, MonitorOutlined, SettingOutlined } from '@ant-design/icons' import { useAITasksStore } from '../../../stores/aiTasksStore' +import { usePagesStore } from '../../../stores/pagesStore' +import { useTabsStore } from '../../../stores/tabsStore' +import './activitybar.css' -export type ActivityBarTab = 'explore' | 'search' | 'tasks' | 'settings' +export type ActivityBarTab = 'explore' | 'search' | 'tasks' interface ActivityBarProps { activeTab: ActivityBarTab @@ -12,10 +15,18 @@ interface ActivityBarProps { export default function ActivityBar({ activeTab, onTabChange }: ActivityBarProps) { const { getRunningTasksCount } = useAITasksStore() + const { createAndOpenSettingsPage } = usePagesStore() + const { openTab } = useTabsStore() // 计算活跃任务数量 const activeTaskCount = getRunningTasksCount() + // 处理设置按钮点击 + const handleSettingsClick = () => { + const settingsPageId = createAndOpenSettingsPage('llm') // 默认打开LLM配置,因为这是用户最常需要的设置 + openTab(settingsPageId) + } + const items = [ { key: 'explore' as ActivityBarTab, @@ -35,12 +46,6 @@ export default function ActivityBar({ activeTab, onTabChange }: ActivityBarProps label: '任务监控', tooltip: '任务监控 - AI任务状态', badge: activeTaskCount > 0 ? activeTaskCount : undefined - }, - { - key: 'settings' as ActivityBarTab, - icon: , - label: '设置', - tooltip: '设置 - 应用程序设置' } ] @@ -59,6 +64,17 @@ export default function ActivityBar({ activeTab, onTabChange }: ActivityBarProps ))} + + {/* 设置按钮独立处理 */} + + + + + + +
- 导出包括设置、LLM配置、聊天记录等所有数据 + 您可以选择导出所有数据,或者分别导出设置和聊天记录
@@ -374,7 +583,7 @@ export default function DataManagement() {
- 导入数据将覆盖当前所有设置,请确保备份重要数据 + 支持导入完整数据文件、设置文件或聊天记录文件,系统会自动识别文件类型进行相应处理
@@ -385,6 +594,11 @@ export default function DataManagement() { 导入外部聊天历史 + + + - - -
@@ -415,21 +624,20 @@ export default function DataManagement() {
重置数据
- - - + + +
- 将应用恢复到初始状态,清除所有用户数据 + 您可以选择重置所有数据,或者分别清空聊天记录、重置设置配置
diff --git a/src/renderer/src/components/settings/LLMSettings.tsx b/src/renderer/src/components/settings/LLMSettings.tsx index 6748f31..9670583 100644 --- a/src/renderer/src/components/settings/LLMSettings.tsx +++ b/src/renderer/src/components/settings/LLMSettings.tsx @@ -116,7 +116,10 @@ function LLMConfigForm({ open, config, onSave, onCancel }: LLMConfigFormProps) { { + form.resetFields() + onCancel() + }} footer={[ - - - )} - - ) - if (!open) return null - if (embedded) { - return
{settingsContent}
- } - return ( - - - 应用设置 - - } - open={open} - onCancel={onClose} - width={700} - footer={[ - , - , - - ]} +
- {settingsContent} - +
+ + + +
) } diff --git a/src/renderer/src/components/settings/UpdateSettings.tsx b/src/renderer/src/components/settings/UpdateSettings.tsx index 6411cd3..8a7aae1 100644 --- a/src/renderer/src/components/settings/UpdateSettings.tsx +++ b/src/renderer/src/components/settings/UpdateSettings.tsx @@ -89,6 +89,8 @@ export default function UpdateSettings() { try { updateStore.setCheckingForUpdates(true) updateStore.setError(null) + // 确保手动检查时不是启动检查 + updateStore.setIsStartupCheck(false) console.log('开始检查更新...') @@ -309,6 +311,51 @@ export default function UpdateSettings() { + + {/* 关于信息 */} + + + 关于应用 + + +
+ 应用名称 + Pointer - AI聊天助手 +
+ +
+ 当前版本 + {currentVersion || '获取中...'} +
+ +
+ 构建框架 + Electron + React + TypeScript +
+ +
+ 许可证 + MIT 开源许可证 +
+ +
+ 项目地址 + +
+ + + 一个探索性的AI聊天应用,提供智能对话、交叉表分析、对象管理等功能, + 致力于提供更好的AI交互体验。 + +
+
) } diff --git a/src/renderer/src/stores/pagesStore.ts b/src/renderer/src/stores/pagesStore.ts index 7f98a58..571c3e4 100644 --- a/src/renderer/src/stores/pagesStore.ts +++ b/src/renderer/src/stores/pagesStore.ts @@ -50,6 +50,7 @@ export interface PagesActions { createAndOpenChat: (title: string, folderId?: string, lineage?: PageLineage) => string createAndOpenCrosstabChat: (title: string, folderId?: string, lineage?: PageLineage) => string createAndOpenObjectChat: (title: string, folderId?: string, lineage?: PageLineage) => string + createAndOpenSettingsPage: (defaultActiveTab?: string) => string // 复杂页面创建功能 createChatFromCell: (params: { @@ -442,6 +443,59 @@ export const usePagesStore = create()( } }, + createAndOpenSettingsPage: (defaultActiveTab = 'appearance') => { + try { + // 检查是否已经存在设置页面,如果存在就更新tab并打开 + const existingPage = get().pages.find((p) => p.type === 'settings') + if (existingPage) { + // 更新设置页面的默认tab + const updatedPage = { + ...existingPage, + data: { defaultActiveTab }, + updatedAt: Date.now() + } + set((state) => { + const pageIndex = state.pages.findIndex((p) => p.id === existingPage.id) + if (pageIndex !== -1) { + state.pages[pageIndex] = updatedPage + } + }) + + // 同时更新 IndexedDB + pagesStorage.savePage(updatedPage) + + const { setSelectedNode } = useUIStore.getState() + setSelectedNode(existingPage.id, 'chat') + return existingPage.id + } + + const newPage: Page = { + id: uuidv4(), + title: '应用设置', + type: 'settings', + createdAt: Date.now(), + updatedAt: Date.now(), + pinned: true, // 设置页面默认固定 + data: { defaultActiveTab } + } + + set((state) => { + state.pages.push(newPage) + }) + + // 同时保存到 IndexedDB + pagesStorage.savePage(newPage) + + const { setSelectedNode } = useUIStore.getState() + setSelectedNode(newPage.id, 'chat') + + return newPage.id + } catch (error) { + handleStoreError('pagesStore', 'createAndOpenSettingsPage', error) + throw error + } + }, + // 复杂页面创建功能 createChatFromCell: (params) => { try { diff --git a/src/renderer/src/stores/searchStore.ts b/src/renderer/src/stores/searchStore.ts index 855531d..d09768c 100644 --- a/src/renderer/src/stores/searchStore.ts +++ b/src/renderer/src/stores/searchStore.ts @@ -187,7 +187,8 @@ export const useSearchStore = create()( } pages.forEach((chat) => { - if (!chat.messages || chat.messages.length === 0) return + // 过滤掉设置页面和没有消息的页面 + if (chat.type === 'settings' || !chat.messages || chat.messages.length === 0) return chat.messages.forEach((message: ChatMessage) => { const content = message.content diff --git a/src/renderer/src/stores/updateStore.ts b/src/renderer/src/stores/updateStore.ts index 98d97b9..6528ddf 100644 --- a/src/renderer/src/stores/updateStore.ts +++ b/src/renderer/src/stores/updateStore.ts @@ -33,6 +33,9 @@ export interface UpdateState { notificationShown: boolean downloadNotificationKey: string | null downloadNotificationHidden: boolean + + // 检查类型标识(区分启动检查和手动检查) + isStartupCheck: boolean } export interface UpdateActions { @@ -44,6 +47,7 @@ export interface UpdateActions { setUpdateDownloaded: (downloaded: boolean) => void setError: (error: string | null) => void setAutoCheckEnabled: (enabled: boolean) => void + setIsStartupCheck: (isStartup: boolean) => void // 更新信息管理 setUpdateInfo: (info: UpdateInfo | null) => void @@ -75,7 +79,8 @@ const initialState: UpdateState = { downloadProgress: null, notificationShown: false, downloadNotificationKey: null, - downloadNotificationHidden: false + downloadNotificationHidden: false, + isStartupCheck: false } export const useUpdateStore = create()( @@ -136,6 +141,11 @@ export const useUpdateStore = create()( state.autoCheckEnabled = enabled }), + setIsStartupCheck: (isStartup) => + set((state) => { + state.isStartupCheck = isStartup + }), + // 更新信息管理 setUpdateInfo: (info) => set((state) => { @@ -176,6 +186,7 @@ export const useUpdateStore = create()( state.notificationShown = false state.downloadNotificationKey = null state.downloadNotificationHidden = false + state.isStartupCheck = false }), handleUpdateAvailable: (info) => diff --git a/src/renderer/src/types/type.ts b/src/renderer/src/types/type.ts index 57a8eb4..8af9334 100644 --- a/src/renderer/src/types/type.ts +++ b/src/renderer/src/types/type.ts @@ -66,7 +66,7 @@ export interface PageLineage { export interface PageBase { id: string title: string - type: 'regular' | 'crosstab' | 'object' + type: 'regular' | 'crosstab' | 'object' | 'settings' folderId?: string createdAt: number @@ -265,9 +265,14 @@ export interface ObjectChat extends PageBase { objectData: ObjectData } +// 设置页面类型 +export interface SettingsPage extends PageBase { + type: 'settings' +} + // 聊天类型 - 包含所有属性 export interface Page extends PageBase { - type: 'regular' | 'crosstab' | 'object' + type: 'regular' | 'crosstab' | 'object' | 'settings' // RegularChat 的属性 messages?: ChatMessage[] diff --git a/src/renderer/src/utils/externalChatImporter.ts b/src/renderer/src/utils/externalChatImporter.ts deleted file mode 100644 index 84ac437..0000000 --- a/src/renderer/src/utils/externalChatImporter.ts +++ /dev/null @@ -1,612 +0,0 @@ -import { Page, ChatMessage, RegularChat } from '../types/type' -import { v4 as uuidv4 } from 'uuid' -import { MessageTree } from '../components/pages/chat/messageTree' - -// 导入限制常量 -const MAX_IMPORT_LIMIT = 50 - -// DeepSeek导出格式的类型定义 -export interface DeepSeekMessage { - id: string - parent: string | null - children: string[] - message: { - files: any[] - search_results: any - model: string - reasoning_content: string | null - content: string - inserted_at: string - } | null -} - -export interface DeepSeekChat { - id: string - title: string - inserted_at: string - updated_at: string - mapping: { [key: string]: DeepSeekMessage } -} - -// OpenAI导出格式的类型定义 -export interface OpenAIMessage { - id: string - message: { - id: string - author: { - role: 'user' | 'assistant' | 'system' - name?: string | null - metadata?: any - } - create_time: number | null - update_time: number | null - content: { - content_type: string - parts: string[] - } - status: string - end_turn?: boolean | null - weight: number - metadata?: any - recipient?: string - channel?: string | null - } | null - parent: string | null - children: string[] -} - -export interface OpenAIChat { - title: string - create_time: number - update_time: number - mapping: { [key: string]: OpenAIMessage } - moderation_results?: any[] - current_node?: string - plugin_ids?: string[] | null - conversation_id: string - conversation_template_id?: string | null - gizmo_id?: string | null - gizmo_type?: string | null - is_archived?: boolean - is_starred?: boolean | null - safe_urls?: string[] - blocked_urls?: string[] - default_model_slug?: string - conversation_origin?: string | null - voice?: string | null - async_status?: string | null - disabled_tool_ids?: string[] - is_do_not_remember?: boolean | null - memory_scope?: string - sugar_item_id?: string | null - id: string -} - -// 支持的聊天格式类型 -export type ChatFormat = 'deepseek' | 'openai' | 'unknown' - -// 导入结果 -export interface ImportResult { - success: boolean - pages: Page[] - folder?: { id: string; name: string } - successCount: number - errorCount: number - message: string -} - -// 可选择的聊天项 -export interface SelectableChatItem { - id: string - title: string - messageCount: number - createTime: number - formatType: ChatFormat - originalData: DeepSeekChat | OpenAIChat -} - -// 解析结果 -export interface ParseResult { - success: boolean - formatType: ChatFormat - pages: SelectableChatItem[] - message: string -} - -/** - * 检测聊天历史的格式类型 - */ -export function detectChatFormat(data: any): ChatFormat { - if (Array.isArray(data)) { - const firstItem = data[0] - if (firstItem && firstItem.mapping && firstItem.title && firstItem.inserted_at) { - return 'deepseek' - } - // 检查OpenAI数组格式 - if ( - firstItem && - firstItem.mapping && - firstItem.title && - firstItem.create_time && - firstItem.conversation_id - ) { - return 'openai' - } - } - - if (data.title && data.mapping && data.create_time) { - return 'openai' - } - - return 'unknown' -} - -/** - * 将OpenAI格式的消息转换为应用内部格式 - */ -export function convertOpenAIMessages(mapping: { [key: string]: OpenAIMessage }): ChatMessage[] { - const messages: ChatMessage[] = [] - const processedIds = new Set() - - // 递归处理消息节点 - const processMessage = (nodeId: string, parentId?: string): void => { - if (processedIds.has(nodeId) || !mapping[nodeId]) return - - const node = mapping[nodeId] - if (!node.message || !node.message.content || !node.message.content.parts) return - - processedIds.add(nodeId) - - // 跳过系统消息和空消息 - if ( - node.message.author.role === 'system' || - node.message.content.parts.join('').trim() === '' - ) { - // 处理子消息 - node.children.forEach((childId) => { - processMessage(childId, parentId) - }) - return - } - - const message: ChatMessage = { - id: node.id, - role: node.message.author.role === 'assistant' ? 'assistant' : 'user', - content: node.message.content.parts.join('\n'), - timestamp: node.message.create_time ? node.message.create_time * 1000 : Date.now(), - parentId: parentId, - children: node.children.length > 0 ? node.children : undefined - } - - messages.push(message) - - // 处理子消息 - node.children.forEach((childId) => { - processMessage(childId, node.id) - }) - } - - // 找到根节点(parent为null的节点) - const rootNodes = Object.values(mapping).filter((node) => node.parent === null) - - rootNodes.forEach((rootNode) => { - rootNode.children.forEach((childId) => { - processMessage(childId) - }) - }) - - return messages -} - -/** - * 将DeepSeek格式的消息树转换为应用内部的消息数组 - */ -export function convertDeepSeekMessages(mapping: { - [key: string]: DeepSeekMessage -}): ChatMessage[] { - const messages: ChatMessage[] = [] - const processedIds = new Set() - - // 递归处理消息节点 - const processMessage = (nodeId: string, parentId?: string): void => { - if (processedIds.has(nodeId) || nodeId === 'root') return - - const node = mapping[nodeId] - if (!node || !node.message) return - - processedIds.add(nodeId) - - // 根据消息内容和模型信息推断角色 - let role: 'user' | 'assistant' = 'user' - - // 如果有模型信息是助手回复 - if (node.message.model) { - role = 'assistant' - } - - // 如果有父消息,根据层级关系判断角色 - if (parentId) { - const parentMessage = messages.find((m) => m.id === parentId) - if (parentMessage) { - role = parentMessage.role === 'user' ? 'assistant' : 'user' - } - } - - // 如果是根节点的直接子节点,通常是用户消息 - const rootNode = mapping['root'] - if (rootNode && rootNode.children.includes(nodeId)) { - role = 'user' - } - - // 转换为应用内部的消息格式 - const message: ChatMessage = { - id: nodeId, - role, - content: node.message.content, - timestamp: new Date(node.message.inserted_at).getTime(), - parentId: parentId, - children: node.children.length > 0 ? node.children : undefined, - modelId: node.message.model, - reasoning_content: node.message.reasoning_content || undefined - } - - messages.push(message) - - // 处理子消息 - node.children.forEach((childId) => { - processMessage(childId, nodeId) - }) - } - - // 从根节点开始处理 - const rootNode = mapping['root'] - if (rootNode && rootNode.children) { - rootNode.children.forEach((childId) => { - processMessage(childId) - }) - } - - return messages -} - -/** - * 处理DeepSeek格式的聊天数据 - */ -function processDeepSeekData( - data: DeepSeekChat[], - folderId?: string -): { pages: Page[]; successCount: number; errorCount: number; skippedCount: number } { - const pages: Page[] = [] - let successCount = 0 - let errorCount = 0 - let skippedCount = 0 - - // 限制处理的数据量 - const limitedData = data.slice(0, MAX_IMPORT_LIMIT) - skippedCount = Math.max(0, data.length - MAX_IMPORT_LIMIT) - - limitedData.forEach((chatData: DeepSeekChat) => { - try { - if (chatData.mapping && chatData.title) { - const messages = convertDeepSeekMessages(chatData.mapping) - - // 使用MessageTree来生成正确的currentPath - const messageTree = new MessageTree(messages) - const currentPath = messageTree.getCurrentPath() - - const chat: Page = { - id: uuidv4(), // 生成新的ID避免冲突 - type: 'regular', - title: chatData.title, - messages, - currentPath, // 设置正确的当前路径 - folderId, // 分配到指定文件夹 - createdAt: new Date(chatData.inserted_at).getTime(), - updatedAt: new Date(chatData.updated_at).getTime() - } - - pages.push(chat) - successCount++ - } - } catch (error) { - console.error('处理单个DeepSeek聊天时出错:', error) - errorCount++ - } - }) - - return { pages, successCount, errorCount, skippedCount } -} - -/** - * 处理OpenAI格式的聊天数据 - */ -function processOpenAIData( - data: OpenAIChat[] | OpenAIChat, - folderId?: string -): { pages: Page[]; successCount: number; errorCount: number; skippedCount: number } { - const pages: Page[] = [] - let successCount = 0 - let errorCount = 0 - let skippedCount = 0 - - // 统一处理为数组格式 - const chatArray = Array.isArray(data) ? data : [data] - - // 限制处理的数据量 - const limitedData = chatArray.slice(0, MAX_IMPORT_LIMIT) - skippedCount = Math.max(0, chatArray.length - MAX_IMPORT_LIMIT) - - limitedData.forEach((chatData: OpenAIChat) => { - try { - if (chatData.mapping && chatData.title) { - const messages = convertOpenAIMessages(chatData.mapping) - - // 使用MessageTree来生成正确的currentPath - const messageTree = new MessageTree(messages) - const currentPath = messageTree.getCurrentPath() - - const chat: Page = { - id: uuidv4(), - type: 'regular', - title: chatData.title, - messages, - currentPath, // 设置正确的当前路径 - folderId, - createdAt: chatData.create_time * 1000, - updatedAt: chatData.update_time * 1000 - } - - pages.push(chat) - successCount++ - } - } catch (error) { - console.error('处理单个OpenAI聊天时出错:', error) - errorCount++ - } - }) - - return { pages, successCount, errorCount, skippedCount } -} - -/** - * 生成导入文件夹名称 - */ -function generateFolderName(formatType: ChatFormat, data: any): string { - const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD格式 - - if (formatType === 'deepseek') { - // 尝试从DeepSeek数据中获取日期 - if (Array.isArray(data) && data.length > 0) { - const firstChat = data[0] - if (firstChat.inserted_at) { - const chatDate = new Date(firstChat.inserted_at).toISOString().split('T')[0] - return `DeepSeek导入-${chatDate}` - } - } - return `DeepSeek导入-${today}` - } else if (formatType === 'openai') { - // 尝试从OpenAI数据中获取日期 - if (Array.isArray(data) && data.length > 0 && data[0].create_time) { - const chatDate = new Date(data[0].create_time * 1000).toISOString().split('T')[0] - return `OpenAI导入-${chatDate}` - } else if (data.create_time) { - const chatDate = new Date(data.create_time * 1000).toISOString().split('T')[0] - return `OpenAI导入-${chatDate}` - } - return `OpenAI导入-${today}` - } else { - return `外部导入-${today}` - } -} - -/** - * 解析外部聊天历史文件,返回可选择的聊天列表 - */ -export function parseExternalChatHistory(jsonContent: string): ParseResult { - try { - const externalData = JSON.parse(jsonContent) - const formatType = detectChatFormat(externalData) - - if (formatType === 'unknown') { - return { - success: false, - formatType: 'unknown', - pages: [], - message: '不支持的文件格式。目前支持DeepSeek和OpenAI导出的聊天历史格式。' - } - } - - const pages: SelectableChatItem[] = [] - - if (formatType === 'deepseek') { - if (Array.isArray(externalData)) { - externalData.forEach((chatData: DeepSeekChat, index) => { - if (chatData.mapping && chatData.title) { - // 计算消息数量 - const messageCount = Object.keys(chatData.mapping).filter( - (key) => key !== 'root' && chatData.mapping[key]?.message - ).length - - pages.push({ - id: `deepseek_${index}_${chatData.id}`, - title: chatData.title, - messageCount, - createTime: new Date(chatData.inserted_at).getTime(), - formatType: 'deepseek', - originalData: chatData - }) - } - }) - } - } else if (formatType === 'openai') { - const chatArray = Array.isArray(externalData) ? externalData : [externalData] - chatArray.forEach((chatData: OpenAIChat, index) => { - if (chatData.mapping && chatData.title) { - // 计算有效消息数量 - const messageCount = Object.values(chatData.mapping).filter( - (node) => - node.message && - node.message.author.role !== 'system' && - node.message.content.parts.join('').trim() !== '' - ).length - - pages.push({ - id: `openai_${index}_${chatData.conversation_id}`, - title: chatData.title, - messageCount, - createTime: chatData.create_time * 1000, - formatType: 'openai', - originalData: chatData - }) - } - }) - } - - return { - success: true, - formatType, - pages, - message: `找到 ${pages.length} 个可导入的聊天记录` - } - } catch (error) { - console.error('解析外部数据失败:', error) - return { - success: false, - formatType: 'unknown', - pages: [], - message: '解析失败,请检查文件格式是否正确' - } - } -} - -/** - * 导入选中的聊天记录 - */ -export function importSelectedChats( - selectedItems: SelectableChatItem[], - folderName?: string -): ImportResult { - if (selectedItems.length === 0) { - return { - success: false, - pages: [], - successCount: 0, - errorCount: 0, - message: '未选择任何聊天记录' - } - } - - const pages: Page[] = [] - let successCount = 0 - let errorCount = 0 - - const folderId = uuidv4() - const finalFolderName = folderName || `外部导入-${new Date().toISOString().split('T')[0]}` - - selectedItems.forEach((item) => { - try { - let messages: ChatMessage[] = [] - - if (item.formatType === 'deepseek') { - const chatData = item.originalData as DeepSeekChat - messages = convertDeepSeekMessages(chatData.mapping) - } else if (item.formatType === 'openai') { - const chatData = item.originalData as OpenAIChat - messages = convertOpenAIMessages(chatData.mapping) - } - - if (messages.length > 0) { - // 使用MessageTree来生成正确的currentPath - const messageTree = new MessageTree(messages) - const currentPath = messageTree.getCurrentPath() - - const chat: RegularChat = { - id: uuidv4(), // 生成新的ID避免冲突 - title: item.title, - type: 'regular', - messages, - currentPath, - folderId, - createdAt: item.createTime, - updatedAt: item.createTime - } - - pages.push(chat) - successCount++ - } - } catch (error) { - console.error('处理单个聊天时出错:', error) - errorCount++ - } - }) - - if (pages.length > 0) { - let message = `导入成功!共导入 ${successCount} 个聊天记录到文件夹"${finalFolderName}"` - if (errorCount > 0) { - message += `,失败 ${errorCount} 个` - } - - return { - success: true, - pages, - folder: { id: folderId, name: finalFolderName }, - successCount, - errorCount, - message - } - } else { - return { - success: false, - pages: [], - successCount: 0, - errorCount: errorCount || 1, - message: '未找到有效的聊天记录' - } - } -} - -/** - * 导入外部聊天历史的主要函数 (保持向后兼容) - */ -export function importExternalChatHistory(jsonContent: string): ImportResult { - try { - const parseResult = parseExternalChatHistory(jsonContent) - - if (!parseResult.success) { - return { - success: false, - pages: [], - successCount: 0, - errorCount: 1, - message: parseResult.message - } - } - - // 限制为50条记录 - const limitedChats = parseResult.pages.slice(0, MAX_IMPORT_LIMIT) - const skippedCount = Math.max(0, parseResult.pages.length - MAX_IMPORT_LIMIT) - - // 生成文件夹名称 - const folderName = generateFolderName( - parseResult.formatType, - parseResult.pages[0]?.originalData - ) - - const result = importSelectedChats(limitedChats, folderName) - - if (result.success && skippedCount > 0) { - result.message += `(跳过 ${skippedCount} 个,已达到50个限制)` - } - - return result - } catch (error) { - console.error('导入外部数据失败:', error) - return { - success: false, - pages: [], - successCount: 0, - errorCount: 1, - message: '导入失败,请检查文件格式是否正确' - } - } -} diff --git a/src/renderer/src/utils/externalChatImporter/converters.ts b/src/renderer/src/utils/externalChatImporter/converters.ts new file mode 100644 index 0000000..95076c0 --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/converters.ts @@ -0,0 +1,130 @@ +import { ChatMessage } from '../../types/type' +import { OpenAIMessage, DeepSeekMessage } from './types' + +/** + * 将OpenAI格式的消息转换为应用内部格式 + */ +export function convertOpenAIMessages(mapping: { [key: string]: OpenAIMessage }): ChatMessage[] { + const messages: ChatMessage[] = [] + const processedIds = new Set() + + // 递归处理消息节点 + const processMessage = (nodeId: string, parentId?: string): void => { + if (processedIds.has(nodeId) || !mapping[nodeId]) return + + const node = mapping[nodeId] + if (!node.message || !node.message.content || !node.message.content.parts) return + + processedIds.add(nodeId) + + // 跳过系统消息和空消息 + if ( + node.message.author.role === 'system' || + node.message.content.parts.join('').trim() === '' + ) { + // 处理子消息 + node.children.forEach((childId) => { + processMessage(childId, parentId) + }) + return + } + + const message: ChatMessage = { + id: node.id, + role: node.message.author.role === 'assistant' ? 'assistant' : 'user', + content: node.message.content.parts.join('\n'), + timestamp: node.message.create_time ? node.message.create_time * 1000 : Date.now(), + parentId: parentId, + children: node.children.length > 0 ? node.children : undefined + } + + messages.push(message) + + // 处理子消息 + node.children.forEach((childId) => { + processMessage(childId, node.id) + }) + } + + // 找到根节点(parent为null的节点) + const rootNodes = Object.values(mapping).filter((node) => node.parent === null) + + rootNodes.forEach((rootNode) => { + rootNode.children.forEach((childId) => { + processMessage(childId) + }) + }) + + return messages +} + +/** + * 将DeepSeek格式的消息树转换为应用内部的消息数组 + */ +export function convertDeepSeekMessages(mapping: { + [key: string]: DeepSeekMessage +}): ChatMessage[] { + const messages: ChatMessage[] = [] + const processedIds = new Set() + + // 递归处理消息节点 + const processMessage = (nodeId: string, parentId?: string): void => { + if (processedIds.has(nodeId) || nodeId === 'root') return + + const node = mapping[nodeId] + if (!node || !node.message) return + + processedIds.add(nodeId) + + // 根据消息内容和模型信息推断角色 + let role: 'user' | 'assistant' = 'user' + + // 如果有模型信息是助手回复 + if (node.message.model) { + role = 'assistant' + } + + // 如果有父消息,根据层级关系判断角色 + if (parentId) { + const parentMessage = messages.find((m) => m.id === parentId) + if (parentMessage) { + role = parentMessage.role === 'user' ? 'assistant' : 'user' + } + } + + // 如果是根节点的直接子节点,通常是用户消息 + const rootNode = mapping['root'] + if (rootNode && rootNode.children.includes(nodeId)) { + role = 'user' + } + + // 转换为应用内部的消息格式 + const message: ChatMessage = { + id: nodeId, + role, + content: node.message.content, + timestamp: new Date(node.message.inserted_at).getTime(), + parentId: parentId, + children: node.children.length > 0 ? node.children : undefined, + modelId: node.message.model, + reasoning_content: node.message.reasoning_content || undefined + } + + messages.push(message) + + // 处理子消息 + node.children.forEach((childId) => { + processMessage(childId, nodeId) + }) + } + + // 从根节点开始处理 + const rootNode = mapping['root'] + if (rootNode && rootNode.children) { + rootNode.children.forEach((childId) => { + processMessage(childId) + }) + } + + return messages +} diff --git a/src/renderer/src/utils/externalChatImporter/formatDetector.ts b/src/renderer/src/utils/externalChatImporter/formatDetector.ts new file mode 100644 index 0000000..76225bc --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/formatDetector.ts @@ -0,0 +1,29 @@ +import { ChatFormat } from './types' + +/** + * 检测聊天历史的格式类型 + */ +export function detectChatFormat(data: any): ChatFormat { + if (Array.isArray(data)) { + const firstItem = data[0] + if (firstItem && firstItem.mapping && firstItem.title && firstItem.inserted_at) { + return 'deepseek' + } + // 检查OpenAI数组格式 + if ( + firstItem && + firstItem.mapping && + firstItem.title && + firstItem.create_time && + firstItem.conversation_id + ) { + return 'openai' + } + } + + if (data.title && data.mapping && data.create_time) { + return 'openai' + } + + return 'unknown' +} diff --git a/src/renderer/src/utils/externalChatImporter/importer.ts b/src/renderer/src/utils/externalChatImporter/importer.ts new file mode 100644 index 0000000..d804b80 --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/importer.ts @@ -0,0 +1,226 @@ +import { v4 as uuidv4 } from 'uuid' +import { Page, ChatMessage, RegularChat } from '../../types/type' +import { MessageTree } from '../../components/pages/chat/messageTree' +import { + MAX_IMPORT_LIMIT, + SelectableChatItem, + ParseResult, + ImportResult, + DeepSeekChat, + OpenAIChat +} from './types' +import { detectChatFormat } from './formatDetector' +import { convertDeepSeekMessages, convertOpenAIMessages } from './converters' +import { generateFolderName } from './utils' + +/** + * 解析外部聊天历史文件,返回可选择的聊天列表 + */ +export function parseExternalChatHistory(jsonContent: string): ParseResult { + try { + const externalData = JSON.parse(jsonContent) + const formatType = detectChatFormat(externalData) + + if (formatType === 'unknown') { + return { + success: false, + formatType: 'unknown', + pages: [], + message: '不支持的文件格式。目前支持DeepSeek和OpenAI导出的聊天历史格式。' + } + } + + const pages: SelectableChatItem[] = [] + + if (formatType === 'deepseek') { + if (Array.isArray(externalData)) { + externalData.forEach((chatData: DeepSeekChat, index) => { + if (chatData.mapping && chatData.title) { + // 计算消息数量 + const messageCount = Object.keys(chatData.mapping).filter( + (key) => key !== 'root' && chatData.mapping[key]?.message + ).length + + pages.push({ + id: `deepseek_${index}_${chatData.id}`, + title: chatData.title, + messageCount, + createTime: new Date(chatData.inserted_at).getTime(), + formatType: 'deepseek', + originalData: chatData + }) + } + }) + } + } else if (formatType === 'openai') { + const chatArray = Array.isArray(externalData) ? externalData : [externalData] + chatArray.forEach((chatData: OpenAIChat, index) => { + if (chatData.mapping && chatData.title) { + // 计算有效消息数量 + const messageCount = Object.values(chatData.mapping).filter( + (node) => + node.message && + node.message.author.role !== 'system' && + node.message.content.parts.join('').trim() !== '' + ).length + + pages.push({ + id: `openai_${index}_${chatData.conversation_id}`, + title: chatData.title, + messageCount, + createTime: chatData.create_time * 1000, + formatType: 'openai', + originalData: chatData + }) + } + }) + } + + return { + success: true, + formatType, + pages, + message: `找到 ${pages.length} 个可导入的聊天记录` + } + } catch (error) { + console.error('解析外部数据失败:', error) + return { + success: false, + formatType: 'unknown', + pages: [], + message: '解析失败,请检查文件格式是否正确' + } + } +} + +/** + * 导入选中的聊天记录 + */ +export function importSelectedChats( + selectedItems: SelectableChatItem[], + folderName?: string +): ImportResult { + if (selectedItems.length === 0) { + return { + success: false, + pages: [], + successCount: 0, + errorCount: 0, + message: '未选择任何聊天记录' + } + } + + const pages: Page[] = [] + let successCount = 0 + let errorCount = 0 + + const folderId = uuidv4() + const finalFolderName = folderName || `外部导入-${new Date().toISOString().split('T')[0]}` + + selectedItems.forEach((item) => { + try { + let messages: ChatMessage[] = [] + + if (item.formatType === 'deepseek') { + const chatData = item.originalData as DeepSeekChat + messages = convertDeepSeekMessages(chatData.mapping) + } else if (item.formatType === 'openai') { + const chatData = item.originalData as OpenAIChat + messages = convertOpenAIMessages(chatData.mapping) + } + + if (messages.length > 0) { + // 使用MessageTree来生成正确的currentPath + const messageTree = new MessageTree(messages) + const currentPath = messageTree.getCurrentPath() + + const chat: RegularChat = { + id: uuidv4(), // 生成新的ID避免冲突 + title: item.title, + type: 'regular', + messages, + currentPath, + folderId, + createdAt: item.createTime, + updatedAt: item.createTime + } + + pages.push(chat) + successCount++ + } + } catch (error) { + console.error('处理单个聊天时出错:', error) + errorCount++ + } + }) + + if (pages.length > 0) { + let message = `导入成功!共导入 ${successCount} 个聊天记录到文件夹"${finalFolderName}"` + if (errorCount > 0) { + message += `,失败 ${errorCount} 个` + } + + return { + success: true, + pages, + folder: { id: folderId, name: finalFolderName }, + successCount, + errorCount, + message + } + } else { + return { + success: false, + pages: [], + successCount: 0, + errorCount: errorCount || 1, + message: '未找到有效的聊天记录' + } + } +} + +/** + * 导入外部聊天历史的主要函数 (保持向后兼容) + */ +export function importExternalChatHistory(jsonContent: string): ImportResult { + try { + const parseResult = parseExternalChatHistory(jsonContent) + + if (!parseResult.success) { + return { + success: false, + pages: [], + successCount: 0, + errorCount: 1, + message: parseResult.message + } + } + + // 限制为50条记录 + const limitedChats = parseResult.pages.slice(0, MAX_IMPORT_LIMIT) + const skippedCount = Math.max(0, parseResult.pages.length - MAX_IMPORT_LIMIT) + + // 生成文件夹名称 + const folderName = generateFolderName( + parseResult.formatType, + parseResult.pages[0]?.originalData + ) + + const result = importSelectedChats(limitedChats, folderName) + + if (result.success && skippedCount > 0) { + result.message += `(跳过 ${skippedCount} 个,已达到50个限制)` + } + + return result + } catch (error) { + console.error('导入外部数据失败:', error) + return { + success: false, + pages: [], + successCount: 0, + errorCount: 1, + message: '导入失败,请检查文件格式是否正确' + } + } +} diff --git a/src/renderer/src/utils/externalChatImporter/index.ts b/src/renderer/src/utils/externalChatImporter/index.ts new file mode 100644 index 0000000..29b3f2f --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/index.ts @@ -0,0 +1,29 @@ +// 导出类型定义 +export type { + DeepSeekMessage, + DeepSeekChat, + OpenAIMessage, + OpenAIChat, + ChatFormat, + ImportResult, + SelectableChatItem, + ParseResult +} from './types' + +export { MAX_IMPORT_LIMIT } from './types' + +// 导出格式检测 +export { detectChatFormat } from './formatDetector' + +// 导出消息转换器 +export { convertOpenAIMessages, convertDeepSeekMessages } from './converters' + +// 导出工具函数 +export { generateFolderName } from './utils' + +// 导出核心导入功能 +export { + parseExternalChatHistory, + importSelectedChats, + importExternalChatHistory +} from './importer' diff --git a/src/renderer/src/utils/externalChatImporter/types.ts b/src/renderer/src/utils/externalChatImporter/types.ts new file mode 100644 index 0000000..e62bda8 --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/types.ts @@ -0,0 +1,112 @@ +import { Page } from '../../types/type' + +// 导入限制常量 +export const MAX_IMPORT_LIMIT = 50 + +// DeepSeek导出格式的类型定义 +export interface DeepSeekMessage { + id: string + parent: string | null + children: string[] + message: { + files: any[] + search_results: any + model: string + reasoning_content: string | null + content: string + inserted_at: string + } | null +} + +export interface DeepSeekChat { + id: string + title: string + inserted_at: string + updated_at: string + mapping: { [key: string]: DeepSeekMessage } +} + +// OpenAI导出格式的类型定义 +export interface OpenAIMessage { + id: string + message: { + id: string + author: { + role: 'user' | 'assistant' | 'system' + name?: string | null + metadata?: any + } + create_time: number | null + update_time: number | null + content: { + content_type: string + parts: string[] + } + status: string + end_turn?: boolean | null + weight: number + metadata?: any + recipient?: string + channel?: string | null + } | null + parent: string | null + children: string[] +} + +export interface OpenAIChat { + title: string + create_time: number + update_time: number + mapping: { [key: string]: OpenAIMessage } + moderation_results?: any[] + current_node?: string + plugin_ids?: string[] | null + conversation_id: string + conversation_template_id?: string | null + gizmo_id?: string | null + gizmo_type?: string | null + is_archived?: boolean + is_starred?: boolean | null + safe_urls?: string[] + blocked_urls?: string[] + default_model_slug?: string + conversation_origin?: string | null + voice?: string | null + async_status?: string | null + disabled_tool_ids?: string[] + is_do_not_remember?: boolean | null + memory_scope?: string + sugar_item_id?: string | null + id: string +} + +// 支持的聊天格式类型 +export type ChatFormat = 'deepseek' | 'openai' | 'unknown' + +// 导入结果 +export interface ImportResult { + success: boolean + pages: Page[] + folder?: { id: string; name: string } + successCount: number + errorCount: number + message: string +} + +// 可选择的聊天项 +export interface SelectableChatItem { + id: string + title: string + messageCount: number + createTime: number + formatType: ChatFormat + originalData: DeepSeekChat | OpenAIChat +} + +// 解析结果 +export interface ParseResult { + success: boolean + formatType: ChatFormat + pages: SelectableChatItem[] + message: string +} diff --git a/src/renderer/src/utils/externalChatImporter/utils.ts b/src/renderer/src/utils/externalChatImporter/utils.ts new file mode 100644 index 0000000..689184b --- /dev/null +++ b/src/renderer/src/utils/externalChatImporter/utils.ts @@ -0,0 +1,32 @@ +import { ChatFormat } from './types' + +/** + * 生成导入文件夹名称 + */ +export function generateFolderName(formatType: ChatFormat, data: any): string { + const today = new Date().toISOString().split('T')[0] // YYYY-MM-DD格式 + + if (formatType === 'deepseek') { + // 尝试从DeepSeek数据中获取日期 + if (Array.isArray(data) && data.length > 0) { + const firstChat = data[0] + if (firstChat.inserted_at) { + const chatDate = new Date(firstChat.inserted_at).toISOString().split('T')[0] + return `DeepSeek导入-${chatDate}` + } + } + return `DeepSeek导入-${today}` + } else if (formatType === 'openai') { + // 尝试从OpenAI数据中获取日期 + if (Array.isArray(data) && data.length > 0 && data[0].create_time) { + const chatDate = new Date(data[0].create_time * 1000).toISOString().split('T')[0] + return `OpenAI导入-${chatDate}` + } else if (data.create_time) { + const chatDate = new Date(data.create_time * 1000).toISOString().split('T')[0] + return `OpenAI导入-${chatDate}` + } + return `OpenAI导入-${today}` + } else { + return `外部导入-${today}` + } +}