diff --git a/electron-builder.yml b/electron-builder.yml index 9b9a239160..05fdc8b2f6 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -125,6 +125,7 @@ afterSign: scripts/notarize.js artifactBuildCompleted: scripts/artifact-build-completed.js releaseInfo: releaseNotes: | + 🚀 New Features: - Refactored AI core engine for more efficient and stable content generation - Added support for multiple AI model providers: CherryIN, AiOnly @@ -151,4 +152,32 @@ releaseInfo: - Improved scrollbar component with horizontal scrolling support - Fixed multiple translation issues: paste handling, file processing, state management - Various UI optimizations and bug fixes + + 🚀 新功能: + - 重构 AI 核心引擎,提供更高效稳定的内容生成 + - 新增多个 AI 模型提供商支持:CherryIN、AiOnly + - 新增 API 服务器功能,支持外部应用集成 + - 新增 PaddleOCR 文档识别,增强文档处理能力 + - 新增 Anthropic OAuth 认证支持 + - 新增数据存储空间限制提醒 + - 新增字体设置,支持全局字体和代码字体自定义 + - 新增翻译完成后自动复制功能 + - 新增键盘快捷键:重命名主题、编辑最后一条消息等 + - 新增文本附件预览,可查看消息中的文件内容 + - 新增自定义窗口控制按钮(最小化、最大化、关闭) + - 支持通义千问长文本(qwen-long)和文档分析(qwen-doc)模型,原生文件上传 + - 支持通义千问图像识别模型(Qwen-Image) + - 新增 iFlow CLI 支持 + - 知识库和网页搜索转换为工具调用方式,提升灵活性 + + 🎨 界面改进与问题修复: + - 集成 HeroUI 和 Tailwind CSS 框架 + - 优化消息通知样式,统一 toast 组件 + - 免费模型移至底部固定位置,便于访问 + - 重构快捷面板和输入栏工具,操作更流畅 + - 优化导航栏和侧边栏响应式设计 + - 改进滚动条组件,支持水平滚动 + - 修复多个翻译问题:粘贴处理、文件处理、状态管理 + - 各种界面优化和问题修复 + diff --git a/src/main/services/AppUpdater.ts b/src/main/services/AppUpdater.ts index 3cb1558b0e..66b88bce84 100644 --- a/src/main/services/AppUpdater.ts +++ b/src/main/services/AppUpdater.ts @@ -17,6 +17,13 @@ import { windowService } from './WindowService' const logger = loggerService.withContext('AppUpdater') +// Language markers constants for multi-language release notes +const LANG_MARKERS = { + EN_START: '', + ZH_CN_START: '', + END: '' +} as const + export default class AppUpdater { autoUpdater: _AppUpdater = autoUpdater private releaseInfo: UpdateInfo | undefined @@ -41,7 +48,8 @@ export default class AppUpdater { autoUpdater.on('update-available', (releaseInfo: UpdateInfo) => { logger.info('update available', releaseInfo) - windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateAvailable, releaseInfo) + const processedReleaseInfo = this.processReleaseInfo(releaseInfo) + windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateAvailable, processedReleaseInfo) }) // 检测到不需要更新时 @@ -56,9 +64,10 @@ export default class AppUpdater { // 当需要更新的内容下载完成后 autoUpdater.on('update-downloaded', (releaseInfo: UpdateInfo) => { - windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, releaseInfo) - this.releaseInfo = releaseInfo - logger.info('update downloaded', releaseInfo) + const processedReleaseInfo = this.processReleaseInfo(releaseInfo) + windowService.getMainWindow()?.webContents.send(IpcChannel.UpdateDownloaded, processedReleaseInfo) + this.releaseInfo = processedReleaseInfo + logger.info('update downloaded', processedReleaseInfo) }) if (isWin) { @@ -271,16 +280,99 @@ export default class AppUpdater { }) } + /** + * Check if release notes contain multi-language markers + */ + private hasMultiLanguageMarkers(releaseNotes: string): boolean { + return releaseNotes.includes(LANG_MARKERS.EN_START) + } + + /** + * Parse multi-language release notes and return the appropriate language version + * @param releaseNotes - Release notes string with language markers + * @returns Parsed release notes for the user's language + * + * Expected format: + * English contentChinese content + */ + private parseMultiLangReleaseNotes(releaseNotes: string): string { + try { + const language = configManager.getLanguage() + const isChineseUser = language === 'zh-CN' || language === 'zh-TW' + + // Create regex patterns using constants + const enPattern = new RegExp( + `${LANG_MARKERS.EN_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)${LANG_MARKERS.ZH_CN_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}` + ) + const zhPattern = new RegExp( + `${LANG_MARKERS.ZH_CN_START.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([\\s\\S]*?)${LANG_MARKERS.END.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}` + ) + + // Extract language sections + const enMatch = releaseNotes.match(enPattern) + const zhMatch = releaseNotes.match(zhPattern) + + // Return appropriate language version with proper fallback + if (isChineseUser && zhMatch) { + return zhMatch[1].trim() + } else if (enMatch) { + return enMatch[1].trim() + } else { + // Clean fallback: remove all language markers + logger.warn('Failed to extract language-specific release notes, using cleaned fallback') + return releaseNotes + .replace(new RegExp(`${LANG_MARKERS.EN_START}|${LANG_MARKERS.ZH_CN_START}|${LANG_MARKERS.END}`, 'g'), '') + .trim() + } + } catch (error) { + logger.error('Failed to parse multi-language release notes', error as Error) + // Return original notes as safe fallback + return releaseNotes + } + } + + /** + * Process release info to handle multi-language release notes + * @param releaseInfo - Original release info from updater + * @returns Processed release info with localized release notes + */ + private processReleaseInfo(releaseInfo: UpdateInfo): UpdateInfo { + const processedInfo = { ...releaseInfo } + + // Handle multi-language release notes in string format + if (releaseInfo.releaseNotes && typeof releaseInfo.releaseNotes === 'string') { + // Check if it contains multi-language markers + if (this.hasMultiLanguageMarkers(releaseInfo.releaseNotes)) { + processedInfo.releaseNotes = this.parseMultiLangReleaseNotes(releaseInfo.releaseNotes) + } + } + + return processedInfo + } + + /** + * Format release notes for display + * @param releaseNotes - Release notes in various formats + * @returns Formatted string for display + */ private formatReleaseNotes(releaseNotes: string | ReleaseNoteInfo[] | null | undefined): string { if (!releaseNotes) { return '' } if (typeof releaseNotes === 'string') { + // Check if it contains multi-language markers + if (this.hasMultiLanguageMarkers(releaseNotes)) { + return this.parseMultiLangReleaseNotes(releaseNotes) + } return releaseNotes } - return releaseNotes.map((note) => note.note).join('\n') + if (Array.isArray(releaseNotes)) { + return releaseNotes.map((note) => note.note).join('\n') + } + + return '' } } interface GithubReleaseInfo { diff --git a/src/main/services/__tests__/AppUpdater.test.ts b/src/main/services/__tests__/AppUpdater.test.ts new file mode 100644 index 0000000000..bb6a7827cb --- /dev/null +++ b/src/main/services/__tests__/AppUpdater.test.ts @@ -0,0 +1,319 @@ +import { UpdateInfo } from 'builder-util-runtime' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +// Mock dependencies +vi.mock('@logger', () => ({ + loggerService: { + withContext: () => ({ + info: vi.fn(), + error: vi.fn(), + warn: vi.fn() + }) + } +})) + +vi.mock('../ConfigManager', () => ({ + configManager: { + getLanguage: vi.fn(), + getAutoUpdate: vi.fn(() => false), + getTestPlan: vi.fn(() => false), + getTestChannel: vi.fn(), + getClientId: vi.fn(() => 'test-client-id') + } +})) + +vi.mock('../WindowService', () => ({ + windowService: { + getMainWindow: vi.fn() + } +})) + +vi.mock('@main/constant', () => ({ + isWin: false +})) + +vi.mock('@main/utils/ipService', () => ({ + getIpCountry: vi.fn(() => 'US') +})) + +vi.mock('@main/utils/locales', () => ({ + locales: { + en: { translation: { update: {} } }, + 'zh-CN': { translation: { update: {} } } + } +})) + +vi.mock('@main/utils/systemInfo', () => ({ + generateUserAgent: vi.fn(() => 'test-user-agent') +})) + +vi.mock('electron', () => ({ + app: { + isPackaged: true, + getVersion: vi.fn(() => '1.0.0'), + getPath: vi.fn(() => '/test/path') + }, + dialog: { + showMessageBox: vi.fn() + }, + BrowserWindow: vi.fn(), + net: { + fetch: vi.fn() + } +})) + +vi.mock('electron-updater', () => ({ + autoUpdater: { + logger: null, + forceDevUpdateConfig: false, + autoDownload: false, + autoInstallOnAppQuit: false, + requestHeaders: {}, + on: vi.fn(), + setFeedURL: vi.fn(), + checkForUpdates: vi.fn(), + downloadUpdate: vi.fn(), + quitAndInstall: vi.fn(), + channel: '', + allowDowngrade: false, + disableDifferentialDownload: false, + currentVersion: '1.0.0' + }, + Logger: vi.fn(), + NsisUpdater: vi.fn(), + AppUpdater: vi.fn() +})) + +// Import after mocks +import AppUpdater from '../AppUpdater' +import { configManager } from '../ConfigManager' + +describe('AppUpdater', () => { + let appUpdater: AppUpdater + + beforeEach(() => { + vi.clearAllMocks() + appUpdater = new AppUpdater() + }) + + describe('parseMultiLangReleaseNotes', () => { + const sampleReleaseNotes = ` +🚀 New Features: +- Feature A +- Feature B + +🎨 UI Improvements: +- Improvement A + +🚀 新功能: +- 功能 A +- 功能 B + +🎨 界面改进: +- 改进 A +` + + it('should return Chinese notes for zh-CN users', () => { + vi.mocked(configManager.getLanguage).mockReturnValue('zh-CN') + + const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes) + + expect(result).toContain('新功能') + expect(result).toContain('功能 A') + expect(result).not.toContain('New Features') + }) + + it('should return Chinese notes for zh-TW users', () => { + vi.mocked(configManager.getLanguage).mockReturnValue('zh-TW') + + const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes) + + expect(result).toContain('新功能') + expect(result).toContain('功能 A') + expect(result).not.toContain('New Features') + }) + + it('should return English notes for non-Chinese users', () => { + vi.mocked(configManager.getLanguage).mockReturnValue('en-US') + + const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes) + + expect(result).toContain('New Features') + expect(result).toContain('Feature A') + expect(result).not.toContain('新功能') + }) + + it('should return English notes for other language users', () => { + vi.mocked(configManager.getLanguage).mockReturnValue('ru-RU') + + const result = (appUpdater as any).parseMultiLangReleaseNotes(sampleReleaseNotes) + + expect(result).toContain('New Features') + expect(result).not.toContain('新功能') + }) + + it('should handle missing language sections gracefully', () => { + const malformedNotes = 'Simple release notes without markers' + + const result = (appUpdater as any).parseMultiLangReleaseNotes(malformedNotes) + + expect(result).toBe('Simple release notes without markers') + }) + + it('should handle malformed markers', () => { + const malformedNotes = `English only` + vi.mocked(configManager.getLanguage).mockReturnValue('zh-CN') + + const result = (appUpdater as any).parseMultiLangReleaseNotes(malformedNotes) + + // Should clean up markers and return cleaned content + expect(result).toContain('English only') + expect(result).not.toContain('Test' + + const result = (appUpdater as any).hasMultiLanguageMarkers(notes) + + expect(result).toBe(true) + }) + + it('should return false when no markers are present', () => { + const notes = 'Simple text without markers' + + const result = (appUpdater as any).hasMultiLanguageMarkers(notes) + + expect(result).toBe(false) + }) + }) + + describe('processReleaseInfo', () => { + it('should process multi-language release notes in string format', () => { + vi.mocked(configManager.getLanguage).mockReturnValue('zh-CN') + + const releaseInfo = { + version: '1.0.0', + files: [], + path: '', + sha512: '', + releaseDate: new Date().toISOString(), + releaseNotes: `English notes中文说明` + } as UpdateInfo + + const result = (appUpdater as any).processReleaseInfo(releaseInfo) + + expect(result.releaseNotes).toBe('中文说明') + }) + + it('should not process release notes without markers', () => { + const releaseInfo = { + version: '1.0.0', + files: [], + path: '', + sha512: '', + releaseDate: new Date().toISOString(), + releaseNotes: 'Simple release notes' + } as UpdateInfo + + const result = (appUpdater as any).processReleaseInfo(releaseInfo) + + expect(result.releaseNotes).toBe('Simple release notes') + }) + + it('should handle array format release notes', () => { + const releaseInfo = { + version: '1.0.0', + files: [], + path: '', + sha512: '', + releaseDate: new Date().toISOString(), + releaseNotes: [ + { version: '1.0.0', note: 'Note 1' }, + { version: '1.0.1', note: 'Note 2' } + ] + } as UpdateInfo + + const result = (appUpdater as any).processReleaseInfo(releaseInfo) + + expect(result.releaseNotes).toEqual(releaseInfo.releaseNotes) + }) + + it('should handle null release notes', () => { + const releaseInfo = { + version: '1.0.0', + files: [], + path: '', + sha512: '', + releaseDate: new Date().toISOString(), + releaseNotes: null + } as UpdateInfo + + const result = (appUpdater as any).processReleaseInfo(releaseInfo) + + expect(result.releaseNotes).toBeNull() + }) + }) + + describe('formatReleaseNotes', () => { + it('should format string release notes with markers', () => { + vi.mocked(configManager.getLanguage).mockReturnValue('en-US') + const notes = `English中文` + + const result = (appUpdater as any).formatReleaseNotes(notes) + + expect(result).toBe('English') + }) + + it('should format string release notes without markers', () => { + const notes = 'Simple notes' + + const result = (appUpdater as any).formatReleaseNotes(notes) + + expect(result).toBe('Simple notes') + }) + + it('should format array release notes', () => { + const notes = [ + { version: '1.0.0', note: 'Note 1' }, + { version: '1.0.1', note: 'Note 2' } + ] + + const result = (appUpdater as any).formatReleaseNotes(notes) + + expect(result).toBe('Note 1\nNote 2') + }) + + it('should handle null release notes', () => { + const result = (appUpdater as any).formatReleaseNotes(null) + + expect(result).toBe('') + }) + + it('should handle undefined release notes', () => { + const result = (appUpdater as any).formatReleaseNotes(undefined) + + expect(result).toBe('') + }) + }) +})