diff --git a/package.json b/package.json index eb9d61a4..5e7b8ed8 100644 --- a/package.json +++ b/package.json @@ -32,11 +32,14 @@ "dependencies": { "@electron-toolkit/preload": "^3.0.0", "@electron-toolkit/utils": "^3.0.0", + "archiver": "^7.0.1", "electron-log": "^5.1.5", "electron-store": "^8.2.0", "electron-updater": "^6.1.7", "electron-window-state": "^5.0.3", - "html2canvas": "^1.4.1" + "fs-extra": "^11.2.0", + "html2canvas": "^1.4.1", + "unzipper": "^0.12.3" }, "devDependencies": { "@anthropic-ai/sdk": "^0.24.3", @@ -47,11 +50,13 @@ "@hello-pangea/dnd": "^16.6.0", "@kangfenmao/keyv-storage": "^0.1.0", "@reduxjs/toolkit": "^2.2.5", + "@types/fs-extra": "^11", "@types/lodash": "^4.17.5", "@types/node": "^18.19.9", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", "@types/tinycolor2": "^1", + "@types/unzipper": "^0", "@vitejs/plugin-react": "^4.2.1", "antd": "^5.18.3", "axios": "^1.7.3", diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 13a06bd9..8f330339 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -1,13 +1,13 @@ -import { FileType } from '@types' -import { BrowserWindow, ipcMain, OpenDialogOptions, session, shell } from 'electron' +import { BrowserWindow, ipcMain, session, shell } from 'electron' import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config' import AppUpdater from './services/AppUpdater' +import BackupManager from './services/BackupManager' import FileManager from './services/FileManager' -import { compress, decompress } from './utils/zip' import { createMinappWindow } from './window' const fileManager = new FileManager() +const backupManager = new BackupManager() export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { const { autoUpdater } = new AppUpdater(mainWindow) @@ -29,24 +29,22 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { ipcMain.handle('reload', () => mainWindow.reload()) - ipcMain.handle('zip:compress', (_, text: string) => compress(text)) - ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text)) + ipcMain.handle('backup:save', backupManager.backup) + ipcMain.handle('backup:restore', backupManager.restore) ipcMain.handle('file:open', fileManager.open) ipcMain.handle('file:save', fileManager.save) + ipcMain.handle('file:select', fileManager.selectFile) + ipcMain.handle('file:upload', fileManager.uploadFile) + ipcMain.handle('file:clear', fileManager.clear) + ipcMain.handle('file:read', fileManager.readFile) + ipcMain.handle('file:delete', fileManager.deleteFile) + ipcMain.handle('file:get', fileManager.getFile) + ipcMain.handle('file:selectFolder', fileManager.selectFolder) + ipcMain.handle('file:create', fileManager.createTempFile) + ipcMain.handle('file:write', fileManager.writeFile) ipcMain.handle('file:saveImage', fileManager.saveImage) - ipcMain.handle('file:base64Image', async (_, id) => await fileManager.base64Image(id)) - ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => await fileManager.selectFile(options)) - ipcMain.handle('file:upload', async (_, file: FileType) => await fileManager.uploadFile(file)) - ipcMain.handle('file:clear', async () => await fileManager.clear()) - ipcMain.handle('file:read', async (_, id: string) => await fileManager.readFile(id)) - ipcMain.handle('file:delete', async (_, id: string) => await fileManager.deleteFile(id)) - ipcMain.handle('file:get', async (_, filePath: string) => await fileManager.getFile(filePath)) - ipcMain.handle('file:create', async (_, fileName: string) => await fileManager.createTempFile(fileName)) - ipcMain.handle( - 'file:write', - async (_, filePath: string, data: Uint8Array | string) => await fileManager.writeFile(filePath, data) - ) + ipcMain.handle('file:base64Image', fileManager.base64Image) ipcMain.handle('minapp', (_, args) => { createMinappWindow({ diff --git a/src/main/services/BackupManager.ts b/src/main/services/BackupManager.ts new file mode 100644 index 00000000..513289c6 --- /dev/null +++ b/src/main/services/BackupManager.ts @@ -0,0 +1,82 @@ +import archiver from 'archiver' +import { app } from 'electron' +import Logger from 'electron-log' +import * as fs from 'fs-extra' +import * as path from 'path' +import * as unzipper from 'unzipper' + +class BackupManager { + private tempDir: string + + constructor() { + this.tempDir = path.join(app.getPath('temp'), 'CherryStudio', 'backup') + this.backup = this.backup.bind(this) + this.restore = this.restore.bind(this) + } + + async backup(_: Electron.IpcMainInvokeEvent, data: string, fileName: string, destinationPath: string): Promise { + try { + // 创建临时目录 + await fs.ensureDir(this.tempDir) + + // 将 data 写入临时文件 + const tempDataPath = path.join(this.tempDir, 'data.json') + await fs.writeFile(tempDataPath, data) + + // 复制 Data 目录到临时目录 + const sourcePath = path.join(app.getPath('userData'), 'Data') + const tempDataDir = path.join(this.tempDir, 'Data') + await fs.copy(sourcePath, tempDataDir) + + // 创建 zip 文件 + const output = fs.createWriteStream(path.join(destinationPath, `${fileName}.zip`)) + const archive = archiver('zip', { zlib: { level: 9 } }) + + archive.pipe(output) + archive.directory(this.tempDir, false) + await archive.finalize() + + // 清理临时目录 + await fs.remove(this.tempDir) + + Logger.log('Backup completed successfully') + } catch (error) { + Logger.error('Backup failed:', error) + throw error + } + } + + async restore(_: Electron.IpcMainInvokeEvent, backupPath: string): Promise<{ data: string; success: boolean }> { + try { + // 创建临时目录 + await fs.ensureDir(this.tempDir) + + // 解压备份文件到临时目录 + await fs + .createReadStream(backupPath) + .pipe(unzipper.Extract({ path: this.tempDir })) + .promise() + + // 读取 data.json + const dataPath = path.join(this.tempDir, 'data.json') + const data = await fs.readFile(dataPath, 'utf-8') + + // 恢复 Data 目录 + const sourcePath = path.join(this.tempDir, 'Data') + const destPath = path.join(app.getPath('userData'), 'Data') + await fs.remove(destPath) + await fs.copy(sourcePath, destPath) + + // 清理临时目录 + await fs.remove(this.tempDir) + + Logger.log('Restore completed successfully') + return { data, success: true } + } catch (error) { + Logger.error('Restore failed:', error) + return { data: '', success: false } + } + } +} + +export default BackupManager diff --git a/src/main/services/FileManager.ts b/src/main/services/FileManager.ts index e031c548..eeca5d29 100644 --- a/src/main/services/FileManager.ts +++ b/src/main/services/FileManager.ts @@ -17,20 +17,19 @@ import * as path from 'path' import { v4 as uuidv4 } from 'uuid' class FileManager { - private storageDir: string + private storageDir = path.join(app.getPath('userData'), 'Data', 'Files') constructor() { - this.storageDir = path.join(app.getPath('userData'), 'Data', 'Files') this.initStorageDir() } - private initStorageDir(): void { + private initStorageDir = (): void => { if (!fs.existsSync(this.storageDir)) { fs.mkdirSync(this.storageDir, { recursive: true }) } } - private async getFileHash(filePath: string): Promise { + private getFileHash = async (filePath: string): Promise => { return new Promise((resolve, reject) => { const hash = crypto.createHash('md5') const stream = fs.createReadStream(filePath) @@ -40,7 +39,7 @@ class FileManager { }) } - async findDuplicateFile(filePath: string): Promise { + findDuplicateFile = async (filePath: string): Promise => { const stats = fs.statSync(filePath) const fileSize = stats.size @@ -76,7 +75,10 @@ class FileManager { return null } - async selectFile(options?: OpenDialogOptions): Promise { + public selectFile = async ( + _: Electron.IpcMainInvokeEvent, + options?: OpenDialogOptions + ): Promise => { const defaultOptions: OpenDialogOptions = { properties: ['openFile'] } @@ -110,7 +112,7 @@ class FileManager { return Promise.all(fileMetadataPromises) } - async uploadFile(file: FileType): Promise { + public uploadFile = async (_: Electron.IpcMainInvokeEvent, file: FileType): Promise => { const duplicateFile = await this.findDuplicateFile(file.path) if (duplicateFile) { @@ -141,7 +143,7 @@ class FileManager { return fileMetadata } - async getFile(filePath: string): Promise { + public getFile = async (_: Electron.IpcMainInvokeEvent, filePath: string): Promise => { if (!fs.existsSync(filePath)) { return null } @@ -165,16 +167,16 @@ class FileManager { return fileInfo } - async deleteFile(id: string): Promise { + public deleteFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise => { await fs.promises.unlink(path.join(this.storageDir, id)) } - async readFile(id: string): Promise { + public readFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise => { const filePath = path.join(this.storageDir, id) return fs.readFileSync(filePath, 'utf8') } - async createTempFile(fileName: string): Promise { + public createTempFile = async (_: Electron.IpcMainInvokeEvent, fileName: string): Promise => { const tempDir = path.join(app.getPath('temp'), 'CherryStudio') if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }) @@ -183,11 +185,18 @@ class FileManager { return tempFilePath } - async writeFile(filePath: string, data: Uint8Array | string): Promise { + public writeFile = async ( + _: Electron.IpcMainInvokeEvent, + filePath: string, + data: Uint8Array | string + ): Promise => { await fs.promises.writeFile(filePath, data) } - async base64Image(id: string): Promise<{ mime: string; base64: string; data: string }> { + public base64Image = async ( + _: Electron.IpcMainInvokeEvent, + id: string + ): Promise<{ mime: string; base64: string; data: string }> => { const filePath = path.join(this.storageDir, id) const data = await fs.promises.readFile(filePath) const base64 = data.toString('base64') @@ -199,15 +208,15 @@ class FileManager { } } - async clear(): Promise { + public clear = async (): Promise => { await fs.promises.rmdir(this.storageDir, { recursive: true }) await this.initStorageDir() } - async open( + public open = async ( _: Electron.IpcMainInvokeEvent, options: OpenDialogOptions - ): Promise<{ fileName: string; content: Buffer } | null> { + ): Promise<{ fileName: string; filePath: string; content: Buffer } | null> => { try { const result: OpenDialogReturnValue = await dialog.showOpenDialog({ title: '打开文件', @@ -220,7 +229,7 @@ class FileManager { const filePath = result.filePaths[0] const fileName = filePath.split('/').pop() || '' const content = await readFile(filePath) - return { fileName, content } + return { fileName, filePath, content } } return null @@ -230,12 +239,12 @@ class FileManager { } } - async save( + public save = async ( _: Electron.IpcMainInvokeEvent, fileName: string, content: string, options?: SaveDialogOptions - ): Promise { + ): Promise => { try { const result: SaveDialogReturnValue = await dialog.showSaveDialog({ title: '保存文件', @@ -251,7 +260,7 @@ class FileManager { } } - async saveImage(_: Electron.IpcMainInvokeEvent, name: string, data: string): Promise { + public saveImage = async (_: Electron.IpcMainInvokeEvent, name: string, data: string): Promise => { try { const filePath = dialog.showSaveDialogSync({ defaultPath: `${name}.png`, @@ -266,6 +275,25 @@ class FileManager { logger.error('[IPC - Error]', 'An error occurred saving the image:', error) } } + + public selectFolder = async (_: Electron.IpcMainInvokeEvent, options: OpenDialogOptions): Promise => { + try { + const result: OpenDialogReturnValue = await dialog.showOpenDialog({ + title: '选择文件夹', + properties: ['openDirectory'], + ...options + }) + + if (!result.canceled && result.filePaths.length > 0) { + return result.filePaths[0] + } + + return null + } catch (err) { + logger.error('[IPC - Error]', 'An error occurred selecting the folder:', err) + return null + } + } } export default FileManager diff --git a/src/main/utils/zip.ts b/src/main/utils/zip.ts deleted file mode 100644 index 7b456973..00000000 --- a/src/main/utils/zip.ts +++ /dev/null @@ -1,39 +0,0 @@ -import util from 'node:util' -import zlib from 'node:zlib' - -import logger from 'electron-log' - -// 将 zlib 的 gzip 和 gunzip 方法转换为 Promise 版本 -const gzipPromise = util.promisify(zlib.gzip) -const gunzipPromise = util.promisify(zlib.gunzip) - -/** - * 压缩字符串 - * @param {string} string - 要压缩的 JSON 字符串 - * @returns {Promise} 压缩后的 Buffer - */ -export async function compress(str) { - try { - const buffer = Buffer.from(str, 'utf-8') - const compressedBuffer = await gzipPromise(buffer) - return compressedBuffer - } catch (error) { - logger.error('Compression failed:', error) - throw error - } -} - -/** - * 解压缩 Buffer 到 JSON 字符串 - * @param {Buffer} compressedBuffer - 压缩的 Buffer - * @returns {Promise} 解压缩后的 JSON 字符串 - */ -export async function decompress(compressedBuffer) { - try { - const buffer = await gunzipPromise(compressedBuffer) - return buffer.toString('utf-8') - } catch (error) { - logger.error('Decompression failed:', error) - throw error - } -} diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 4a210e25..e769a6c7 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -19,19 +19,24 @@ declare global { reload: () => void compress: (text: string) => Promise decompress: (text: Buffer) => Promise + backup: { + save: (data: string, fileName: string, destinationPath: string) => Promise + restore: (backupPath: string) => Promise<{ data: string; success: boolean }> + } file: { select: (options?: OpenDialogOptions) => Promise upload: (file: FileType) => Promise delete: (fileId: string) => Promise read: (fileId: string) => Promise - base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }> clear: () => Promise get: (filePath: string) => Promise + selectFolder: () => Promise create: (fileName: string) => Promise write: (filePath: string, data: Uint8Array | string) => Promise - open: (options?: OpenDialogOptions) => Promise<{ fileName: string; content: Buffer } | null> + open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null> save: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void saveImage: (name: string, data: string) => void + base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }> } } } diff --git a/src/preload/index.ts b/src/preload/index.ts index ba38277e..c6db5a1b 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -10,23 +10,27 @@ const api = { setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme), minApp: (url: string) => ipcRenderer.invoke('minapp', url), reload: () => ipcRenderer.invoke('reload'), - compress: (text: string) => ipcRenderer.invoke('zip:compress', text), - decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text), + backup: { + save: (data: string, fileName: string, destinationPath: string) => { + ipcRenderer.invoke('backup:save', data, fileName, destinationPath) + }, + restore: (backupPath: string) => ipcRenderer.invoke('backup:restore', backupPath) + }, file: { select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options), upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath), delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId), read: (fileId: string) => ipcRenderer.invoke('file:read', fileId), - base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId), clear: () => ipcRenderer.invoke('file:clear'), get: (filePath: string) => ipcRenderer.invoke('file:get', filePath), create: (fileName: string) => ipcRenderer.invoke('file:create', fileName), write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data), open: (options?: { decompress: boolean }) => ipcRenderer.invoke('file:open', options), - save: (path: string, content: string, options?: { compress: boolean }) => { - return ipcRenderer.invoke('file:save', path, content, options) - }, - saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data) + save: (path: string, content: string, options?: { compress: boolean }) => + ipcRenderer.invoke('file:save', path, content, options), + selectFolder: () => ipcRenderer.invoke('file:selectFolder'), + saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data), + base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId) } } diff --git a/src/renderer/src/pages/files/FilesPage.tsx b/src/renderer/src/pages/files/FilesPage.tsx index f268b117..f6094cc7 100644 --- a/src/renderer/src/pages/files/FilesPage.tsx +++ b/src/renderer/src/pages/files/FilesPage.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components' const FilesPage: FC = () => { const { t } = useTranslation() - const files = useLiveQuery(() => db.files.orderBy('created_at').reverse().toArray()) + const files = useLiveQuery(() => db.files.orderBy('ext').reverse().toArray()) const dataSource = files?.map((file) => { const isImage = file.type === FileTypes.IMAGE @@ -65,8 +65,14 @@ const FilesPage: FC = () => { {t('files.title')} - - + +
diff --git a/src/renderer/src/providers/AnthropicProvider.ts b/src/renderer/src/providers/AnthropicProvider.ts index 8b48a686..fdd1e5ae 100644 --- a/src/renderer/src/providers/AnthropicProvider.ts +++ b/src/renderer/src/providers/AnthropicProvider.ts @@ -58,7 +58,7 @@ export default class AnthropicProvider extends BaseProvider { const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant) const userMessagesParams: MessageParam[] = [] - const _messages = filterContextMessages(takeRight(messages, contextCount + 2)) + const _messages = filterContextMessages(takeRight(messages, contextCount + 1)) onFilterMessages(_messages) diff --git a/src/renderer/src/providers/GeminiProvider.ts b/src/renderer/src/providers/GeminiProvider.ts index a98f67da..c25e4d9c 100644 --- a/src/renderer/src/providers/GeminiProvider.ts +++ b/src/renderer/src/providers/GeminiProvider.ts @@ -59,7 +59,7 @@ export default class GeminiProvider extends BaseProvider { const model = assistant.model || defaultModel const { contextCount, maxTokens, streamOutput } = getAssistantSettings(assistant) - const userMessages = filterContextMessages(takeRight(messages, contextCount + 2)) + const userMessages = filterContextMessages(takeRight(messages, contextCount + 1)) onFilterMessages(userMessages) if (first(userMessages)?.role === 'assistant') { diff --git a/src/renderer/src/providers/OpenAIProvider.ts b/src/renderer/src/providers/OpenAIProvider.ts index 8bfd947c..dd79f8f1 100644 --- a/src/renderer/src/providers/OpenAIProvider.ts +++ b/src/renderer/src/providers/OpenAIProvider.ts @@ -117,7 +117,7 @@ export default class OpenAIProvider extends BaseProvider { const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined const userMessages: ChatCompletionMessageParam[] = [] - const _messages = filterContextMessages(takeRight(messages, contextCount + 1)) + const _messages = filterContextMessages(takeRight(messages, contextCount)) onFilterMessages(_messages) for (const message of _messages) { diff --git a/src/renderer/src/services/backup.ts b/src/renderer/src/services/backup.ts index 691633a0..b77ddc84 100644 --- a/src/renderer/src/services/backup.ts +++ b/src/renderer/src/services/backup.ts @@ -4,7 +4,7 @@ import dayjs from 'dayjs' import localforage from 'localforage' export async function backup() { - const version = 2 + const version = 3 const time = new Date().getTime() const data = { @@ -14,22 +14,31 @@ export async function backup() { indexedDB: await backupDatabase() } - const filename = `cherry-studio.${dayjs().format('YYYYMMDD')}.bak` + const filename = `cherry-studio.${dayjs().format('YYYYMMDDHHmm')}` const fileContnet = JSON.stringify(data) - const file = await window.api.compress(fileContnet) - await window.api.file.save(filename, file) + const selectFolder = await window.api.file.selectFolder() - window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' }) + if (selectFolder) { + await window.api.backup.save(fileContnet, filename, selectFolder) + window.message.success({ content: i18n.t('message.backup.success'), key: 'backup' }) + } } export async function restore() { - const file = await window.api.file.open() + const file = await window.api.file.open({ filters: [{ name: '备份文件', extensions: ['bak', 'zip'] }] }) if (file) { try { - const content = await window.api.decompress(file.content) - const data = JSON.parse(content) + let data: Record = {} + + // zip backup file + if (file?.fileName.endsWith('.zip')) { + const restoreData = await window.api.backup.restore(file.filePath) + data = JSON.parse(restoreData.data) + } else { + data = JSON.parse(await window.api.decompress(file.content)) + } if (data.version === 1) { await clearDatabase() @@ -49,7 +58,7 @@ export async function restore() { return } - if (data.version === 2) { + if (data.version >= 2) { localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio']) await restoreDatabase(data.indexedDB) window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' }) diff --git a/yarn.lock b/yarn.lock index af20a1b9..eda06878 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1793,6 +1793,16 @@ __metadata: languageName: node linkType: hard +"@types/fs-extra@npm:^11": + version: 11.0.4 + resolution: "@types/fs-extra@npm:11.0.4" + dependencies: + "@types/jsonfile": "npm:*" + "@types/node": "npm:*" + checksum: 10c0/9e34f9b24ea464f3c0b18c3f8a82aefc36dc524cc720fc2b886e5465abc66486ff4e439ea3fb2c0acebf91f6d3f74e514f9983b1f02d4243706bdbb7511796ad + languageName: node + linkType: hard + "@types/glob@npm:^7.1.0": version: 7.2.0 resolution: "@types/glob@npm:7.2.0" @@ -1845,6 +1855,15 @@ __metadata: languageName: node linkType: hard +"@types/jsonfile@npm:*": + version: 6.1.4 + resolution: "@types/jsonfile@npm:6.1.4" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/b12d068b021e4078f6ac4441353965769be87acf15326173e2aea9f3bf8ead41bd0ad29421df5bbeb0123ec3fc02eb0a734481d52903704a1454a1845896b9eb + languageName: node + linkType: hard + "@types/katex@npm:^0.16.0": version: 0.16.7 resolution: "@types/katex@npm:0.16.7" @@ -2013,6 +2032,15 @@ __metadata: languageName: node linkType: hard +"@types/unzipper@npm:^0": + version: 0.10.10 + resolution: "@types/unzipper@npm:0.10.10" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/10e9da33791be1087adb25adc2fe4d5ab267dae51fbcf7b1f10d0aca3130a13ef5fed994d7be45af8c465ff3946bc360a53eff6e5aab4eb9ac9489477535342f + languageName: node + linkType: hard + "@types/use-sync-external-store@npm:^0.0.3": version: 0.0.3 resolution: "@types/use-sync-external-store@npm:0.0.3" @@ -2202,13 +2230,16 @@ __metadata: "@hello-pangea/dnd": "npm:^16.6.0" "@kangfenmao/keyv-storage": "npm:^0.1.0" "@reduxjs/toolkit": "npm:^2.2.5" + "@types/fs-extra": "npm:^11" "@types/lodash": "npm:^4.17.5" "@types/node": "npm:^18.19.9" "@types/react": "npm:^18.2.48" "@types/react-dom": "npm:^18.2.18" "@types/tinycolor2": "npm:^1" + "@types/unzipper": "npm:^0" "@vitejs/plugin-react": "npm:^4.2.1" antd: "npm:^5.18.3" + archiver: "npm:^7.0.1" axios: "npm:^1.7.3" browser-image-compression: "npm:^2.0.2" dayjs: "npm:^1.11.11" @@ -2231,6 +2262,7 @@ __metadata: eslint-plugin-react-hooks: "npm:^4.6.2" eslint-plugin-simple-import-sort: "npm:^12.1.1" eslint-plugin-unused-imports: "npm:^4.0.0" + fs-extra: "npm:^11.2.0" gpt-tokens: "npm:^1.3.10" html2canvas: "npm:^1.4.1" i18next: "npm:^23.11.5" @@ -2257,6 +2289,7 @@ __metadata: styled-components: "npm:^6.1.11" tinycolor2: "npm:^1.6.0" typescript: "npm:^5.6.2" + unzipper: "npm:^0.12.3" uuid: "npm:^10.0.0" vite: "npm:^5.0.12" peerDependencies: @@ -2558,6 +2591,36 @@ __metadata: languageName: node linkType: hard +"archiver-utils@npm:^5.0.0, archiver-utils@npm:^5.0.2": + version: 5.0.2 + resolution: "archiver-utils@npm:5.0.2" + dependencies: + glob: "npm:^10.0.0" + graceful-fs: "npm:^4.2.0" + is-stream: "npm:^2.0.1" + lazystream: "npm:^1.0.0" + lodash: "npm:^4.17.15" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^4.0.0" + checksum: 10c0/3782c5fa9922186aa1a8e41ed0c2867569faa5f15c8e5e6418ea4c1b730b476e21bd68270b3ea457daf459ae23aaea070b2b9f90cf90a59def8dc79b9e4ef538 + languageName: node + linkType: hard + +"archiver@npm:^7.0.1": + version: 7.0.1 + resolution: "archiver@npm:7.0.1" + dependencies: + archiver-utils: "npm:^5.0.2" + async: "npm:^3.2.4" + buffer-crc32: "npm:^1.0.0" + readable-stream: "npm:^4.0.0" + readdir-glob: "npm:^1.1.2" + tar-stream: "npm:^3.0.0" + zip-stream: "npm:^6.0.1" + checksum: 10c0/02afd87ca16f6184f752db8e26884e6eff911c476812a0e7f7b26c4beb09f06119807f388a8e26ed2558aa8ba9db28646ebd147a4f99e46813b8b43158e1438e + languageName: node + linkType: hard + "argparse@npm:^2.0.1": version: 2.0.1 resolution: "argparse@npm:2.0.1" @@ -2712,7 +2775,7 @@ __metadata: languageName: node linkType: hard -"async@npm:^3.2.3": +"async@npm:^3.2.3, async@npm:^3.2.4": version: 3.2.6 resolution: "async@npm:3.2.6" checksum: 10c0/36484bb15ceddf07078688d95e27076379cc2f87b10c03b6dd8a83e89475a3c8df5848859dd06a4c95af1e4c16fc973de0171a77f18ea00be899aca2a4f85e70 @@ -2774,6 +2837,13 @@ __metadata: languageName: node linkType: hard +"b4a@npm:^1.6.4": + version: 1.6.7 + resolution: "b4a@npm:1.6.7" + checksum: 10c0/ec2f004d1daae04be8c5a1f8aeb7fea213c34025e279db4958eb0b82c1729ee25f7c6e89f92a5f65c8a9cf2d017ce27e3dda912403341d1781bd74528a4849d4 + languageName: node + linkType: hard + "bail@npm:^2.0.0": version: 2.0.2 resolution: "bail@npm:2.0.2" @@ -2788,6 +2858,13 @@ __metadata: languageName: node linkType: hard +"bare-events@npm:^2.2.0": + version: 2.5.0 + resolution: "bare-events@npm:2.5.0" + checksum: 10c0/afbeec4e8be4d93fb4a3be65c3b4a891a2205aae30b5a38fafd42976cc76cf30dad348963fe330a0d70186e15dc507c11af42c89af5dddab2a54e5aff02e2896 + languageName: node + linkType: hard + "base64-arraybuffer@npm:^1.0.2": version: 1.0.2 resolution: "base64-arraybuffer@npm:1.0.2" @@ -2827,7 +2904,7 @@ __metadata: languageName: node linkType: hard -"bluebird@npm:^3.5.5": +"bluebird@npm:^3.5.5, bluebird@npm:~3.7.2": version: 3.7.2 resolution: "bluebird@npm:3.7.2" checksum: 10c0/680de03adc54ff925eaa6c7bb9a47a0690e8b5de60f4792604aae8ed618c65e6b63a7893b57ca924beaf53eee69c5af4f8314148c08124c550fe1df1add897d2 @@ -2899,6 +2976,13 @@ __metadata: languageName: node linkType: hard +"buffer-crc32@npm:^1.0.0": + version: 1.0.0 + resolution: "buffer-crc32@npm:1.0.0" + checksum: 10c0/8b86e161cee4bb48d5fa622cbae4c18f25e4857e5203b89e23de59e627ab26beb82d9d7999f2b8de02580165f61f83f997beaf02980cdf06affd175b651921ab + languageName: node + linkType: hard + "buffer-crc32@npm:~0.2.3": version: 0.2.13 resolution: "buffer-crc32@npm:0.2.13" @@ -2937,6 +3021,16 @@ __metadata: languageName: node linkType: hard +"buffer@npm:^6.0.3": + version: 6.0.3 + resolution: "buffer@npm:6.0.3" + dependencies: + base64-js: "npm:^1.3.1" + ieee754: "npm:^1.2.1" + checksum: 10c0/2a905fbbcde73cc5d8bd18d1caa23715d5f83a5935867c2329f0ac06104204ba7947be098fe1317fbd8830e26090ff8e764f08cd14fefc977bb248c3487bcbd0 + languageName: node + linkType: hard + "builder-util-runtime@npm:9.2.4": version: 9.2.4 resolution: "builder-util-runtime@npm:9.2.4" @@ -3356,6 +3450,19 @@ __metadata: languageName: node linkType: hard +"compress-commons@npm:^6.0.2": + version: 6.0.2 + resolution: "compress-commons@npm:6.0.2" + dependencies: + crc-32: "npm:^1.2.0" + crc32-stream: "npm:^6.0.0" + is-stream: "npm:^2.0.1" + normalize-path: "npm:^3.0.0" + readable-stream: "npm:^4.0.0" + checksum: 10c0/2347031b7c92c8ed5011b07b93ec53b298fa2cd1800897532ac4d4d1aeae06567883f481b6e35f13b65fc31b190c751df6635434d525562f0203fde76f1f0814 + languageName: node + linkType: hard + "compute-scroll-into-view@npm:^3.0.2": version: 3.1.0 resolution: "compute-scroll-into-view@npm:3.1.0" @@ -3440,6 +3547,25 @@ __metadata: languageName: node linkType: hard +"crc-32@npm:^1.2.0": + version: 1.2.2 + resolution: "crc-32@npm:1.2.2" + bin: + crc32: bin/crc32.njs + checksum: 10c0/11dcf4a2e77ee793835d49f2c028838eae58b44f50d1ff08394a610bfd817523f105d6ae4d9b5bef0aad45510f633eb23c903e9902e4409bed1ce70cb82b9bf0 + languageName: node + linkType: hard + +"crc32-stream@npm:^6.0.0": + version: 6.0.0 + resolution: "crc32-stream@npm:6.0.0" + dependencies: + crc-32: "npm:^1.2.0" + readable-stream: "npm:^4.0.0" + checksum: 10c0/bf9c84571ede2d119c2b4f3a9ef5eeb9ff94b588493c0d3862259af86d3679dcce1c8569dd2b0a6eff2f35f5e2081cc1263b846d2538d4054da78cf34f262a3d + languageName: node + linkType: hard + "crc@npm:^3.8.0": version: 3.8.0 resolution: "crc@npm:3.8.0" @@ -3845,6 +3971,15 @@ __metadata: languageName: node linkType: hard +"duplexer2@npm:~0.1.4": + version: 0.1.4 + resolution: "duplexer2@npm:0.1.4" + dependencies: + readable-stream: "npm:^2.0.2" + checksum: 10c0/0765a4cc6fe6d9615d43cc6dbccff6f8412811d89a6f6aa44828ca9422a0a469625ce023bf81cee68f52930dbedf9c5716056ff264ac886612702d134b5e39b4 + languageName: node + linkType: hard + "eastasianwidth@npm:^0.2.0": version: 0.2.0 resolution: "eastasianwidth@npm:0.2.0" @@ -4562,6 +4697,13 @@ __metadata: languageName: node linkType: hard +"events@npm:^3.3.0": + version: 3.3.0 + resolution: "events@npm:3.3.0" + checksum: 10c0/d6b6f2adbccbcda74ddbab52ed07db727ef52e31a61ed26db9feb7dc62af7fc8e060defa65e5f8af9449b86b52cc1a1f6a79f2eafcf4e62add2b7a1fa4a432f6 + languageName: node + linkType: hard + "exif-parser@npm:^0.1.12": version: 0.1.12 resolution: "exif-parser@npm:0.1.12" @@ -4642,6 +4784,13 @@ __metadata: languageName: node linkType: hard +"fast-fifo@npm:^1.2.0, fast-fifo@npm:^1.3.2": + version: 1.3.2 + resolution: "fast-fifo@npm:1.3.2" + checksum: 10c0/d53f6f786875e8b0529f784b59b4b05d4b5c31c651710496440006a398389a579c8dbcd2081311478b5bf77f4b0b21de69109c5a4eabea9d8e8783d1eb864e4c + languageName: node + linkType: hard + "fast-glob@npm:^3.2.9": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" @@ -4899,6 +5048,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.2.0": + version: 11.2.0 + resolution: "fs-extra@npm:11.2.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10c0/d77a9a9efe60532d2e790e938c81a02c1b24904ef7a3efb3990b835514465ba720e99a6ea56fd5e2db53b4695319b644d76d5a0e9988a2beef80aa7b1da63398 + languageName: node + linkType: hard + "fs-extra@npm:^8.1.0": version: 8.1.0 resolution: "fs-extra@npm:8.1.0" @@ -5083,7 +5243,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^10.2.2, glob@npm:^10.3.10": +"glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.3.10": version: 10.4.5 resolution: "glob@npm:10.4.5" dependencies: @@ -5216,7 +5376,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.1.9, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 @@ -6083,6 +6243,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^2.0.1": + version: 2.0.1 + resolution: "is-stream@npm:2.0.1" + checksum: 10c0/7c284241313fc6efc329b8d7f08e16c0efeb6baab1b4cd0ba579eb78e5af1aa5da11e68559896a2067cd6c526bd29241dda4eb1225e627d5aa1a89a76d4635a5 + languageName: node + linkType: hard + "is-string@npm:^1.0.5, is-string@npm:^1.0.7": version: 1.0.7 resolution: "is-string@npm:1.0.7" @@ -6495,6 +6662,15 @@ __metadata: languageName: node linkType: hard +"lazystream@npm:^1.0.0": + version: 1.0.1 + resolution: "lazystream@npm:1.0.1" + dependencies: + readable-stream: "npm:^2.0.5" + checksum: 10c0/ea4e509a5226ecfcc303ba6782cc269be8867d372b9bcbd625c88955df1987ea1a20da4643bf9270336415a398d33531ebf0d5f0d393b9283dc7c98bfcbd7b69 + languageName: node + linkType: hard + "lcid@npm:^1.0.0": version: 1.0.0 resolution: "lcid@npm:1.0.0" @@ -7419,7 +7595,7 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^5.0.1, minimatch@npm:^5.1.1": +"minimatch@npm:^5.0.1, minimatch@npm:^5.1.0, minimatch@npm:^5.1.1": version: 5.1.6 resolution: "minimatch@npm:5.1.6" dependencies: @@ -7642,6 +7818,13 @@ __metadata: languageName: node linkType: hard +"node-int64@npm:^0.4.0": + version: 0.4.0 + resolution: "node-int64@npm:0.4.0" + checksum: 10c0/a6a4d8369e2f2720e9c645255ffde909c0fbd41c92ea92a5607fc17055955daac99c1ff589d421eee12a0d24e99f7bfc2aabfeb1a4c14742f6c099a51863f31a + languageName: node + linkType: hard + "node-releases@npm:^2.0.18": version: 2.0.18 resolution: "node-releases@npm:2.0.18" @@ -8427,6 +8610,13 @@ __metadata: languageName: node linkType: hard +"queue-tick@npm:^1.0.1": + version: 1.0.1 + resolution: "queue-tick@npm:1.0.1" + checksum: 10c0/0db998e2c9b15215317dbcf801e9b23e6bcde4044e115155dae34f8e7454b9a783f737c9a725528d677b7a66c775eb7a955cf144fe0b87f62b575ce5bfd515a9 + languageName: node + linkType: hard + "quick-lru@npm:^5.1.1": version: 5.1.1 resolution: "quick-lru@npm:5.1.1" @@ -9191,7 +9381,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.2.2, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.2.2, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -9217,6 +9407,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.0.0": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: "npm:^3.0.0" + buffer: "npm:^6.0.3" + events: "npm:^3.3.0" + process: "npm:^0.11.10" + string_decoder: "npm:^1.3.0" + checksum: 10c0/a2c80e0e53aabd91d7df0330929e32d0a73219f9477dbbb18472f6fdd6a11a699fc5d172a1beff98d50eae4f1496c950ffa85b7cc2c4c196963f289a5f39275d + languageName: node + linkType: hard + "readable-web-to-node-stream@npm:^3.0.0": version: 3.0.2 resolution: "readable-web-to-node-stream@npm:3.0.2" @@ -9226,6 +9429,15 @@ __metadata: languageName: node linkType: hard +"readdir-glob@npm:^1.1.2": + version: 1.1.3 + resolution: "readdir-glob@npm:1.1.3" + dependencies: + minimatch: "npm:^5.1.0" + checksum: 10c0/a37e0716726650845d761f1041387acd93aa91b28dd5381950733f994b6c349ddc1e21e266ec7cc1f9b92e205a7a972232f9b89d5424d07361c2c3753d5dbace + languageName: node + linkType: hard + "readdirp@npm:~3.6.0": version: 3.6.0 resolution: "readdirp@npm:3.6.0" @@ -10043,6 +10255,21 @@ __metadata: languageName: node linkType: hard +"streamx@npm:^2.15.0": + version: 2.20.1 + resolution: "streamx@npm:2.20.1" + dependencies: + bare-events: "npm:^2.2.0" + fast-fifo: "npm:^1.3.2" + queue-tick: "npm:^1.0.1" + text-decoder: "npm:^1.1.0" + dependenciesMeta: + bare-events: + optional: true + checksum: 10c0/34ffa2ee9465d70e18c7e2ba70189720c166d150ab83eb7700304620fa23ff42a69cb37d712ea4b5fc6234d8e74346a88bb4baceb873c6b05e52ac420f8abb4d + languageName: node + linkType: hard + "string-convert@npm:^0.2.0": version: 0.2.1 resolution: "string-convert@npm:0.2.1" @@ -10147,7 +10374,7 @@ __metadata: languageName: node linkType: hard -"string_decoder@npm:^1.1.1": +"string_decoder@npm:^1.1.1, string_decoder@npm:^1.3.0": version: 1.3.0 resolution: "string_decoder@npm:1.3.0" dependencies: @@ -10329,6 +10556,17 @@ __metadata: languageName: node linkType: hard +"tar-stream@npm:^3.0.0": + version: 3.1.7 + resolution: "tar-stream@npm:3.1.7" + dependencies: + b4a: "npm:^1.6.4" + fast-fifo: "npm:^1.2.0" + streamx: "npm:^2.15.0" + checksum: 10c0/a09199d21f8714bd729993ac49b6c8efcb808b544b89f23378ad6ffff6d1cb540878614ba9d4cfec11a64ef39e1a6f009a5398371491eb1fda606ffc7f70f718 + languageName: node + linkType: hard + "tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.2.1": version: 6.2.1 resolution: "tar@npm:6.2.1" @@ -10353,6 +10591,15 @@ __metadata: languageName: node linkType: hard +"text-decoder@npm:^1.1.0": + version: 1.2.0 + resolution: "text-decoder@npm:1.2.0" + dependencies: + b4a: "npm:^1.6.4" + checksum: 10c0/398171bef376e06864cd6ba24e0787cc626bebc84a1bbda758d06a6e9b729cc8613f7923dd0d294abd88e8bb5cd7261aad5fda7911fb87253fe71b2b5ac6e507 + languageName: node + linkType: hard + "text-segmentation@npm:^1.0.3": version: 1.0.3 resolution: "text-segmentation@npm:1.0.3" @@ -10800,6 +11047,19 @@ __metadata: languageName: node linkType: hard +"unzipper@npm:^0.12.3": + version: 0.12.3 + resolution: "unzipper@npm:0.12.3" + dependencies: + bluebird: "npm:~3.7.2" + duplexer2: "npm:~0.1.4" + fs-extra: "npm:^11.2.0" + graceful-fs: "npm:^4.2.2" + node-int64: "npm:^0.4.0" + checksum: 10c0/4cae2ad23bfd47011d5f8a6d61fb1dc0e4b5008bc3896e6f3d5ab946a64e9482714992a988128bce541440aa646e16e5e5c9bf35e49097edbaf833e7f814d36d + languageName: node + linkType: hard + "update-browserslist-db@npm:^1.1.0": version: 1.1.0 resolution: "update-browserslist-db@npm:1.1.0" @@ -11355,6 +11615,17 @@ __metadata: languageName: node linkType: hard +"zip-stream@npm:^6.0.1": + version: 6.0.1 + resolution: "zip-stream@npm:6.0.1" + dependencies: + archiver-utils: "npm:^5.0.0" + compress-commons: "npm:^6.0.2" + readable-stream: "npm:^4.0.0" + checksum: 10c0/50f2fb30327fb9d09879abf7ae2493705313adf403e794b030151aaae00009162419d60d0519e807673ec04d442e140c8879ca14314df0a0192de3b233e8f28b + languageName: node + linkType: hard + "zwitch@npm:^2.0.0": version: 2.0.4 resolution: "zwitch@npm:2.0.4"