diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5aa4d57f..ad60467f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,11 +2,6 @@ name: Release on: workflow_dispatch: - inputs: - version: - description: 'Version (e.g. v1.2.3)' - required: true - type: string push: tags: - v*.*.* @@ -20,8 +15,7 @@ jobs: strategy: matrix: - # windows-latest, ubuntu-latest - os: [macos-13, macos-latest] + os: [macos-13, macos-latest, windows-latest, ubuntu-latest] arch: [x64, arm64] exclude: - os: windows-latest @@ -41,6 +35,9 @@ jobs: node-version: 20 arch: ${{ matrix.arch }} + - name: Install corepack + run: corepack enable && corepack prepare yarn@4.3.1 --activate + - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT @@ -55,9 +52,6 @@ jobs: restore-keys: | ${{ runner.os }}-yarn- - - name: Install corepack - run: corepack enable && corepack prepare yarn@4.3.1 --activate - - name: Install Dependencies run: yarn install @@ -70,7 +64,7 @@ jobs: - name: Build Mac if: matrix.os == 'macos-13' || matrix.os == 'macos-latest' - run: yarn build:mac + run: yarn build:mac && mv dist/latest-mac.yml dist/latest-mac-${{ matrix.arch }}.yml env: CSC_LINK: ${{ secrets.CSC_LINK }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} diff --git a/electron-builder.yml b/electron-builder.yml index 37a7460c..5970be70 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -48,47 +48,22 @@ mac: - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. notarize: false - target: - - target: dmg - arch: - - arm64 - - x64 - - target: zip - arch: - - arm64 - - x64 dmg: artifactName: ${productName}-${version}-${arch}.${ext} linux: target: - target: AppImage - arch: - - arm64 - - x64 - # - snap - # - deb maintainer: electronjs.org category: Utility appImage: artifactName: ${productName}-${version}-${arch}.${ext} -npmRebuild: false publish: provider: generic url: https://cherrystudio.ocool.online electronDownload: mirror: https://npmmirror.com/mirrors/electron/ afterPack: scripts/removeLocales.js -# afterSign: scripts/notarize.js +afterSign: scripts/notarize.js releaseInfo: releaseNotes: | - 增加小程序快捷入口 - 增加ThinkAny、纳米搜索小程序 - 优化 Markdown 列表显示 - 可以多次点击上传文件按钮上传文件 - 大屏幕默认使用更大的输入框 - 设置中增加显示设置模块 - 支持 SVG 预览 - Mermaid 图表支持复制源码 - 增加复制最后一条消息快捷键 - o1模型默认开启流式输出 - 长文本粘贴为文件支持修改阈值 + 增加知识库功能 diff --git a/package.json b/package.json index 156c56e4..58ce2b5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "CherryStudio", - "version": "0.8.27", + "version": "0.9.0", "private": true, "description": "A powerful AI assistant for producer.", "main": "./out/main/index.js", diff --git a/scripts/cloudflare-worker.js b/scripts/cloudflare-worker.js index 283c0383..27cd86ec 100644 --- a/scripts/cloudflare-worker.js +++ b/scripts/cloudflare-worker.js @@ -6,510 +6,558 @@ const config = { CACHE_KEY: 'cherry-studio-latest-release', VERSION_DB: 'versions.json', LOG_FILE: 'logs.json', - MAX_LOGS: 1000 // 最多保存多少条日志 -}; + MAX_LOGS: 1000 // 最多保存多少条日志 +} // Worker 入口函数 const worker = { // 定时器触发配置 scheduled: { - cron: '*/1 * * * *' // 每分钟执行一次 + cron: '*/1 * * * *' // 每分钟执行一次 }, // 定时器执行函数 - 只负责检查和更新 async scheduled(event, env, ctx) { - try { - await initDataFiles(env); - console.log('开始定时检查新版本...'); - // 使用新的 checkNewRelease 函数 - await checkNewRelease(env); - } catch (error) { - console.error('定时任务执行失败:', error); - } + try { + await initDataFiles(env) + console.log('开始定时检查新版本...') + // 使用新的 checkNewRelease 函数 + await checkNewRelease(env) + } catch (error) { + console.error('定时任务执行失败:', error) + } }, // HTTP 请求处理函数 - 只负责返回数据 async fetch(request, env, ctx) { - if (!env || !env.R2_BUCKET) { - return new Response(JSON.stringify({ - error: 'R2 存储桶未正确配置' - }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); + if (!env || !env.R2_BUCKET) { + return new Response( + JSON.stringify({ + error: 'R2 存储桶未正确配置' + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' } + } + ) + } + + const url = new URL(request.url) + const filename = url.pathname.slice(1) + + try { + // 处理文件下载请求 + if (filename) { + return await handleDownload(env, filename) } - const url = new URL(request.url); - const filename = url.pathname.slice(1); - - try { - // 处理文件下载请求 - if (filename) { - return await handleDownload(env, filename); - } - - // 只返回缓存的版本信息 - return await getCachedRelease(env); - } catch (error) { - return new Response(JSON.stringify({ - error: error.message, - stack: error.stack - }), { - status: 500, - headers: { 'Content-Type': 'application/json' } - }); - } + // 只返回缓存的版本信息 + return await getCachedRelease(env) + } catch (error) { + return new Response( + JSON.stringify({ + error: error.message, + stack: error.stack + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' } + } + ) + } } -}; +} -export default worker; +export default worker /** -* 添加日志记录函数 -*/ + * 添加日志记录函数 + */ async function addLog(env, type, event, details = null) { try { - const logFile = await env.R2_BUCKET.get(config.LOG_FILE); - let logs = { logs: [] }; + const logFile = await env.R2_BUCKET.get(config.LOG_FILE) + let logs = { logs: [] } - if (logFile) { - logs = JSON.parse(await logFile.text()); - } + if (logFile) { + logs = JSON.parse(await logFile.text()) + } - logs.logs.unshift({ - timestamp: new Date().toISOString(), - type, - event, - details - }); + logs.logs.unshift({ + timestamp: new Date().toISOString(), + type, + event, + details + }) - // 保持日志数量在限制内 - if (logs.logs.length > config.MAX_LOGS) { - logs.logs = logs.logs.slice(0, config.MAX_LOGS); - } + // 保持日志数量在限制内 + if (logs.logs.length > config.MAX_LOGS) { + logs.logs = logs.logs.slice(0, config.MAX_LOGS) + } - await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(logs, null, 2)); + await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(logs, null, 2)) } catch (error) { - console.error('写入日志失败:', error); + console.error('写入日志失败:', error) } } /** -* 获取最新版本信息 -*/ + * 获取最新版本信息 + */ async function getLatestRelease(env) { try { - const cached = await env.R2_BUCKET.get(config.CACHE_KEY); - if (!cached) { - // 如果缓存不存在,先检查版本数据库 - const versionDB = await env.R2_BUCKET.get(config.VERSION_DB); - if (versionDB) { - const versions = JSON.parse(await versionDB.text()); - if (versions.latestVersion) { - // 从版本数据库重建缓存 - const latestVersion = versions.versions[versions.latestVersion]; - const cacheData = { - version: latestVersion.version, - publishedAt: latestVersion.publishedAt, - changelog: latestVersion.changelog, - downloads: latestVersion.files - .filter(file => file.uploaded) - .map(file => ({ - name: file.name, - url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`, - size: formatFileSize(file.size) - })) - }; - // 更新缓存 - await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData)); - return new Response(JSON.stringify(cacheData), { - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - } - }); - } + const cached = await env.R2_BUCKET.get(config.CACHE_KEY) + if (!cached) { + // 如果缓存不存在,先检查版本数据库 + const versionDB = await env.R2_BUCKET.get(config.VERSION_DB) + if (versionDB) { + const versions = JSON.parse(await versionDB.text()) + if (versions.latestVersion) { + // 从版本数据库重建缓存 + const latestVersion = versions.versions[versions.latestVersion] + const cacheData = { + version: latestVersion.version, + publishedAt: latestVersion.publishedAt, + changelog: latestVersion.changelog, + downloads: latestVersion.files + .filter((file) => file.uploaded) + .map((file) => ({ + name: file.name, + url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`, + size: formatFileSize(file.size) + })) } - // 如果版本数据库也没有数据,才执行检查更新 - const data = await checkNewRelease(env); - return new Response(JSON.stringify(data), { - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - } - }); + // 更新缓存 + await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData)) + return new Response(JSON.stringify(cacheData), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + } + }) + } } + // 如果版本数据库也没有数据,才执行检查更新 + const data = await checkNewRelease(env) + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + } + }) + } - const data = await cached.text(); - return new Response(data, { - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - } - }); + const data = await cached.text() + return new Response(data, { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + } + }) } catch (error) { - await addLog(env, 'ERROR', '获取版本信息失败', error.message); - return new Response(JSON.stringify({ - error: '获取版本信息失败: ' + error.message, - detail: '请稍���再试' - }), { - status: 500, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - } - }); + await addLog(env, 'ERROR', '获取版本信息失败', error.message) + return new Response( + JSON.stringify({ + error: '获取版本信息失败: ' + error.message, + detail: '请稍后再试' + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + } + } + ) } } // 修改下载处理函数,直接接收 env async function handleDownload(env, filename) { try { - const object = await env.R2_BUCKET.get(filename); + const object = await env.R2_BUCKET.get(filename) - if (!object) { - return new Response('文件未找到', { status: 404 }); - } + if (!object) { + return new Response('文件未找到', { status: 404 }) + } - // 设置响应头 - const headers = new Headers(); - object.writeHttpMetadata(headers); - headers.set('etag', object.httpEtag); - headers.set('Content-Disposition', `attachment; filename="${filename}"`); + // 设置响应头 + const headers = new Headers() + object.writeHttpMetadata(headers) + headers.set('etag', object.httpEtag) + headers.set('Content-Disposition', `attachment; filename="${filename}"`) - return new Response(object.body, { - headers - }); + return new Response(object.body, { + headers + }) } catch (error) { - console.error('下载文件时发生错误:', error); - return new Response('获取文件失败', { status: 500 }); + console.error('下载文件时发生错误:', error) + return new Response('获取文件失败', { status: 500 }) } } /** -* 根据文件扩展名获取对应的 Content-Type -*/ + * 根据文件扩展名获取对应的 Content-Type + */ function getContentType(filename) { - const ext = filename.split('.').pop().toLowerCase(); + const ext = filename.split('.').pop().toLowerCase() const types = { - 'exe': 'application/x-msdownload', // Windows 可执行文件 - 'dmg': 'application/x-apple-diskimage', // macOS 安装包 - 'zip': 'application/zip', // 压缩包 - 'AppImage': 'application/x-executable', // Linux 可执行文件 - 'blockmap': 'application/octet-stream' // 更新文件 - }; - return types[ext] || 'application/octet-stream'; + exe: 'application/x-msdownload', // Windows 可执行文件 + dmg: 'application/x-apple-diskimage', // macOS 安装包 + zip: 'application/zip', // 压缩包 + AppImage: 'application/x-executable', // Linux 可执行文件 + blockmap: 'application/octet-stream' // 更新文件 + } + return types[ext] || 'application/octet-stream' } /** -* 格式化文件大小 -* 将字节转换为人类可读的格式(B, KB, MB, GB) -*/ + * 格式化文件大小 + * 将字节转换为人类可读的格式(B, KB, MB, GB) + */ function formatFileSize(bytes) { - const units = ['B', 'KB', 'MB', 'GB']; - let size = bytes; - let unitIndex = 0; + const units = ['B', 'KB', 'MB', 'GB'] + let size = bytes + let unitIndex = 0 while (size >= 1024 && unitIndex < units.length - 1) { - size /= 1024; - unitIndex++; + size /= 1024 + unitIndex++ } - return `${size.toFixed(2)} ${units[unitIndex]}`; + return `${size.toFixed(2)} ${units[unitIndex]}` } /** -* 版本号比较函数 -* 用于对版本号进行排序 -*/ + * 版本号比较函数 + * 用于对版本号进行排序 + */ function compareVersions(a, b) { - const partsA = a.replace('v', '').split('.'); - const partsB = b.replace('v', '').split('.'); + const partsA = a.replace('v', '').split('.') + const partsB = b.replace('v', '').split('.') for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { - const numA = parseInt(partsA[i] || 0); - const numB = parseInt(partsB[i] || 0); + const numA = parseInt(partsA[i] || 0) + const numB = parseInt(partsB[i] || 0) - if (numA !== numB) { - return numA - numB; - } + if (numA !== numB) { + return numA - numB + } } - return 0; + return 0 } /** -* 初始化数据文件 -*/ + * 初始化数据文件 + */ async function initDataFiles(env) { try { - // 检查并初始化版本数据库 - const versionDB = await env.R2_BUCKET.get(config.VERSION_DB); - if (!versionDB) { - const initialVersions = { - versions: {}, - latestVersion: null, - lastChecked: new Date().toISOString() - }; - await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(initialVersions, null, 2)); - await addLog(env, 'INFO', 'versions.json 初始化成功'); + // 检查并初始化版本数据库 + const versionDB = await env.R2_BUCKET.get(config.VERSION_DB) + if (!versionDB) { + const initialVersions = { + versions: {}, + latestVersion: null, + lastChecked: new Date().toISOString() } + await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(initialVersions, null, 2)) + await addLog(env, 'INFO', 'versions.json 初始化成功') + } - // 检查并初始化日志文件 - const logFile = await env.R2_BUCKET.get(config.LOG_FILE); - if (!logFile) { - const initialLogs = { - logs: [{ - timestamp: new Date().toISOString(), - type: 'INFO', - event: '系统初始化' - }] - }; - await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2)); - console.log('logs.json 初始化成功'); + // 检查并初始化日志文件 + const logFile = await env.R2_BUCKET.get(config.LOG_FILE) + if (!logFile) { + const initialLogs = { + logs: [ + { + timestamp: new Date().toISOString(), + type: 'INFO', + event: '系统初始化' + } + ] } + await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2)) + console.log('logs.json 初始化成功') + } } catch (error) { - console.error('初始化数据文件失败:', error); + console.error('初始化数据文件失败:', error) } } // 新增:只获取缓存的版本信息 async function getCachedRelease(env) { try { - const cached = await env.R2_BUCKET.get(config.CACHE_KEY); - if (!cached) { - // 如果缓存不存在,从版本数据库获取 - const versionDB = await env.R2_BUCKET.get(config.VERSION_DB); - if (versionDB) { - const versions = JSON.parse(await versionDB.text()); - if (versions.latestVersion) { - const latestVersion = versions.versions[versions.latestVersion]; - const cacheData = { - version: latestVersion.version, - publishedAt: latestVersion.publishedAt, - changelog: latestVersion.changelog, - downloads: latestVersion.files - .filter(file => file.uploaded) - .map(file => ({ - name: file.name, - url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`, - size: formatFileSize(file.size) - })) - }; - // 重建缓存 - await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData)); - return new Response(JSON.stringify(cacheData), { - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - } - }); - } + const cached = await env.R2_BUCKET.get(config.CACHE_KEY) + if (!cached) { + // 如果缓存不存在,从版本数据库获取 + const versionDB = await env.R2_BUCKET.get(config.VERSION_DB) + if (versionDB) { + const versions = JSON.parse(await versionDB.text()) + if (versions.latestVersion) { + const latestVersion = versions.versions[versions.latestVersion] + const cacheData = { + version: latestVersion.version, + publishedAt: latestVersion.publishedAt, + changelog: latestVersion.changelog, + downloads: latestVersion.files + .filter((file) => file.uploaded) + .map((file) => ({ + name: file.name, + url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`, + size: formatFileSize(file.size) + })) } - // 如果没有任何数据,返回错误 - return new Response(JSON.stringify({ - error: '没有可用的版本信息' - }), { - status: 404, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*' - } - }); - } - - // 返回缓存数据 - return new Response(await cached.text(), { - headers: { + // 重建缓存 + await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData)) + return new Response(JSON.stringify(cacheData), { + headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' + } + }) + } + } + // 如果没有任何数据,返回错误 + return new Response( + JSON.stringify({ + error: '没有可用的版本信息' + }), + { + status: 404, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' } - }); + } + ) + } + + // 返回缓存数据 + return new Response(await cached.text(), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*' + } + }) } catch (error) { - await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message); - throw error; + await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message) + throw error } } // 新增:只检查新版本并更新 async function checkNewRelease(env) { try { - // 获取 GitHub 最新版本 - const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', { - headers: { 'User-Agent': 'CloudflareWorker' }, - }); + // 获取 GitHub 最新版本 + const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', { + headers: { 'User-Agent': 'CloudflareWorker' } + }) - if (!githubResponse.ok) { - throw new Error('GitHub API 请求失败'); - } + if (!githubResponse.ok) { + throw new Error('GitHub API 请求失败') + } - const releaseData = await githubResponse.json(); - const version = releaseData.tag_name; + const releaseData = await githubResponse.json() + const version = releaseData.tag_name - // 获取版本数据库 - const versionDB = await env.R2_BUCKET.get(config.VERSION_DB); - let versions = { versions: {}, latestVersion: null, lastChecked: new Date().toISOString() }; + // 获取版本数据库 + const versionDB = await env.R2_BUCKET.get(config.VERSION_DB) + let versions = { versions: {}, latestVersion: null, lastChecked: new Date().toISOString() } - if (versionDB) { - versions = JSON.parse(await versionDB.text()); - } + if (versionDB) { + versions = JSON.parse(await versionDB.text()) + } - // 移除版本检查,改为记录是否有文件更新的标志 - let hasUpdates = false; - if (versions.latestVersion !== version) { - await addLog(env, 'INFO', `发现新版本: ${version}`); - hasUpdates = true; - } else { - await addLog(env, 'INFO', `版本 ${version} 文件完整性检查开始`); - } + // 移除版本检查,改为记录是否有文件更新的标志 + let hasUpdates = false + if (versions.latestVersion !== version) { + await addLog(env, 'INFO', `发现新版本: ${version}`) + hasUpdates = true + } else { + await addLog(env, 'INFO', `版本 ${version} 文件完整性检查开始`) + } - // 准备新版本记录 - const versionRecord = { - version, - publishedAt: releaseData.published_at, - uploadedAt: null, - files: releaseData.assets.map(asset => ({ - name: asset.name, - size: asset.size, - uploaded: false - })), - changelog: releaseData.body - }; + // 准备新版本记录 + const versionRecord = { + version, + publishedAt: releaseData.published_at, + uploadedAt: null, + files: releaseData.assets.map((asset) => ({ + name: asset.name, + size: asset.size, + uploaded: false + })), + changelog: releaseData.body + } - // 检查并上传文件 - for (const asset of releaseData.assets) { - try { - const existingFile = await env.R2_BUCKET.get(asset.name); - // 检查文件是否存在且大小是否一致 - if (!existingFile || existingFile.size !== asset.size) { - hasUpdates = true; - const response = await fetch(asset.browser_download_url); - if (!response.ok) { - throw new Error(`下载失败: HTTP ${response.status}`); - } - - const file = await response.arrayBuffer(); - await env.R2_BUCKET.put(asset.name, file, { - httpMetadata: { contentType: getContentType(asset.name) } - }); - - // 更新文件状态 - const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name); - if (fileIndex !== -1) { - versionRecord.files[fileIndex].uploaded = true; - } - - await addLog(env, 'INFO', `文件${existingFile ? '更新' : '上传'}成功: ${asset.name}`); - } else { - // 文件存在且大小相同,标记为已上传 - const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name); - if (fileIndex !== -1) { - versionRecord.files[fileIndex].uploaded = true; - } - await addLog(env, 'INFO', `文件完整性验证通过: ${asset.name}`); - } - } catch (error) { - await addLog(env, 'ERROR', `文件处理失败: ${asset.name}`, error.message); + // 检查并上传文件 + for (const asset of releaseData.assets) { + try { + const existingFile = await env.R2_BUCKET.get(asset.name) + // 检查文件是否存在且大小是否一致 + if (!existingFile || existingFile.size !== asset.size) { + hasUpdates = true + const response = await fetch(asset.browser_download_url) + if (!response.ok) { + throw new Error(`下载失败: HTTP ${response.status}`) } - } - // 只有在有更新或是新版本时才更新数据库和缓存 - if (hasUpdates) { - // 更新版本记录 - versionRecord.uploadedAt = new Date().toISOString(); - versions.versions[version] = versionRecord; - versions.latestVersion = version; + const file = await response.arrayBuffer() + await env.R2_BUCKET.put(asset.name, file, { + httpMetadata: { contentType: getContentType(asset.name) } + }) - // 保存版本数据库 - await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2)); - - // 更新缓存 - const cacheData = { - version, - publishedAt: releaseData.published_at, - changelog: releaseData.body, - downloads: versionRecord.files - .filter(file => file.uploaded) - .map(file => ({ - name: file.name, - url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`, - size: formatFileSize(file.size) - })) - }; - - await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData)); - await addLog(env, 'INFO', hasUpdates ? '更新完成' : '文件完整性检查完成'); - - // 清理旧版本 - const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a)); - if (versionList.length > 2) { - // 获取需要保留的两个最新版本 - const keepVersions = versionList.slice(0, 2); - // 获取所有需要删除的版本 - const oldVersions = versionList.slice(2); - - // 先获取 R2 桶中的所有文件列表 - const allFiles = await listAllFiles(env); - - // 获取需要保留的文件名列表 - const keepFiles = new Set(); - for (const keepVersion of keepVersions) { - const versionFiles = versions.versions[keepVersion].files; - versionFiles.forEach(file => keepFiles.add(file.name)); - } - - // 删除所有旧版本文件 - for (const oldVersion of oldVersions) { - const oldFiles = versions.versions[oldVersion].files; - for (const file of oldFiles) { - try { - if (file.uploaded) { - await env.R2_BUCKET.delete(file.name); - await addLog(env, 'INFO', `删除旧文件: ${file.name}`); - } - } catch (error) { - await addLog(env, 'ERROR', `删除旧文件失败: ${file.name}`, error.message); - } - } - delete versions.versions[oldVersion]; - } - - // 清理可能遗留的旧文件 - for (const file of allFiles) { - if (!keepFiles.has(file.name)) { - try { - await env.R2_BUCKET.delete(file.name); - await addLog(env, 'INFO', `删除遗留文件: ${file.name}`); - } catch (error) { - await addLog(env, 'ERROR', `删除遗留文件失败: ${file.name}`, error.message); - } - } - } - - // 保存更新后的版本数据库 - await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2)); + // 更新文件状态 + const fileIndex = versionRecord.files.findIndex((f) => f.name === asset.name) + if (fileIndex !== -1) { + versionRecord.files[fileIndex].uploaded = true } - } else { - await addLog(env, 'INFO', '所有文件完整性检查通过,无需更新'); + + await addLog(env, 'INFO', `文件${existingFile ? '更新' : '上传'}成功: ${asset.name}`) + } else { + // 文件存在且大小相同,标记为已上传 + const fileIndex = versionRecord.files.findIndex((f) => f.name === asset.name) + if (fileIndex !== -1) { + versionRecord.files[fileIndex].uploaded = true + } + await addLog(env, 'INFO', `文件完整性验证通过: ${asset.name}`) + } + } catch (error) { + await addLog(env, 'ERROR', `文件处理失败: ${asset.name}`, error.message) + } + } + + // 只有在有更新或是新版本时才更新数据库和缓存 + if (hasUpdates) { + // 更新版本记录 + versionRecord.uploadedAt = new Date().toISOString() + versions.versions[version] = versionRecord + versions.latestVersion = version + + // 保存版本数据库 + await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2)) + + // 更新缓存 + const cacheData = { + version, + publishedAt: releaseData.published_at, + changelog: releaseData.body, + downloads: versionRecord.files + .filter((file) => file.uploaded) + .map((file) => ({ + name: file.name, + url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`, + size: formatFileSize(file.size) + })) } - return hasUpdates ? cacheData : null; + await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData)) + await addLog(env, 'INFO', hasUpdates ? '更新完成' : '文件完整性检查完成') + + // 清理旧版本 + const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a)) + if (versionList.length > 2) { + // 获取需要保留的两个最新版本 + const keepVersions = versionList.slice(0, 2) + // 获取所有需要删除的版本 + const oldVersions = versionList.slice(2) + + // 先获取 R2 桶中的所有文件列表 + const allFiles = await listAllFiles(env) + + // 获取需要保留的文件名列表 + const keepFiles = new Set() + for (const keepVersion of keepVersions) { + const versionFiles = versions.versions[keepVersion].files + versionFiles.forEach((file) => keepFiles.add(file.name)) + } + + // 删除所有旧版本文件 + for (const oldVersion of oldVersions) { + const oldFiles = versions.versions[oldVersion].files + for (const file of oldFiles) { + try { + if (file.uploaded) { + await env.R2_BUCKET.delete(file.name) + await addLog(env, 'INFO', `删除旧文件: ${file.name}`) + } + } catch (error) { + await addLog(env, 'ERROR', `删除旧文件失败: ${file.name}`, error.message) + } + } + delete versions.versions[oldVersion] + } + + // 清理可能遗留的旧文件 + for (const file of allFiles) { + if (!keepFiles.has(file.name)) { + try { + await env.R2_BUCKET.delete(file.name) + await addLog(env, 'INFO', `删除遗留文件: ${file.name}`) + } catch (error) { + await addLog(env, 'ERROR', `删除遗留文件失败: ${file.name}`, error.message) + } + } + } + + // 保存更新后的版本数据库 + await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2)) + } + } else { + await addLog(env, 'INFO', '所有文件完整性检查通过,无需更新') + } + + // 合并 Mac yml 文件 + await mergeMacYmlFiles(env) + + return hasUpdates ? cacheData : null } catch (error) { - await addLog(env, 'ERROR', '检查新版本失败', error.message); - throw error; + await addLog(env, 'ERROR', '检查新版本失败', error.message) + throw error } } // 新增:获取 R2 桶中的所有文件列表 async function listAllFiles(env) { - const files = []; - let cursor; + const files = [] + let cursor do { - const listed = await env.R2_BUCKET.list({ cursor, include: ['customMetadata'] }); - files.push(...listed.objects); - cursor = listed.cursor; - } while (cursor); + const listed = await env.R2_BUCKET.list({ cursor, include: ['customMetadata'] }) + files.push(...listed.objects) + cursor = listed.cursor + } while (cursor) - return files; -} + return files +} + +async function mergeMacYmlFiles(env) { + try { + const macX64Yml = await env.R2_BUCKET.get('latest-mac-x64.yml') + const macArm64Yml = await env.R2_BUCKET.get('latest-mac-arm64.yml') + + if (!macX64Yml || !macArm64Yml) { + return + } + + const x64Content = await macX64Yml.text() + const arm64Content = await macArm64Yml.text() + + // 使用正则表达式提取 files 部分 + const filesRegex = /files:\n( - url:[\s\S]+?)(?=\npath:)/ + const x64Files = x64Content.match(filesRegex)[1] + const arm64Files = arm64Content.match(filesRegex)[1] + + // 合并内容 + const mergedContent = arm64Content.replace(filesRegex, `files:\n${arm64Files}\n${x64Files}`) + + // 保存合并后的文件 + await env.R2_BUCKET.put('latest-mac.yml', mergedContent, { + httpMetadata: { contentType: 'application/x-yaml' } + }) + + await addLog(env, 'INFO', 'Mac yml 文件合并成功') + } catch (error) { + await addLog(env, 'ERROR', 'Mac yml 文件合并失败', error.message) + } +} diff --git a/src/renderer/src/hooks/useknowledge.ts b/src/renderer/src/hooks/useknowledge.ts deleted file mode 100644 index 3c030cec..00000000 --- a/src/renderer/src/hooks/useknowledge.ts +++ /dev/null @@ -1,279 +0,0 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import { db } from '@renderer/databases/index' -import KnowledgeQueue from '@renderer/queue/KnowledgeQueue' -import FileManager from '@renderer/services/FileManager' -import { getKnowledgeBaseParams } from '@renderer/services/KnowledgeService' -import { RootState } from '@renderer/store' -import { - addBase, - addFiles as addFilesAction, - addItem, - clearAllProcessing, - clearCompletedProcessing, - deleteBase, - removeItem as removeItemAction, - renameBase, - updateBase, - updateBases, - updateItemProcessingStatus, - updateNotes -} from '@renderer/store/knowledge' -import { FileType, KnowledgeBase, ProcessingStatus } from '@renderer/types' -import { KnowledgeItem } from '@renderer/types' -import { runAsyncFunction } from '@renderer/utils' -import { useEffect, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { v4 as uuidv4 } from 'uuid' - -export const useKnowledge = (baseId: string) => { - const dispatch = useDispatch() - const base = useSelector((state: RootState) => state.knowledge.bases.find((b) => b.id === baseId)) - - // 重命名知识库 - const renameKnowledgeBase = (name: string) => { - dispatch(renameBase({ baseId, name })) - } - - // 更新知识库 - const updateKnowledgeBase = (base: KnowledgeBase) => { - dispatch(updateBase(base)) - } - - // 批量添加文件 - const addFiles = (files: FileType[]) => { - const filesItems: KnowledgeItem[] = files.map((file) => ({ - id: uuidv4(), - type: 'file' as const, - content: file, - created_at: Date.now(), - updated_at: Date.now(), - processingStatus: 'pending', - processingProgress: 0, - processingError: '', - retryCount: 0 - })) - dispatch(addFilesAction({ baseId, items: filesItems })) - setTimeout(() => KnowledgeQueue.checkAllBases(), 0) - } - - // 添加URL - const addUrl = (url: string) => { - const newUrlItem: KnowledgeItem = { - id: uuidv4(), - type: 'url' as const, - content: url, - created_at: Date.now(), - updated_at: Date.now(), - processingStatus: 'pending', - processingProgress: 0, - processingError: '', - retryCount: 0 - } - dispatch(addItem({ baseId, item: newUrlItem })) - setTimeout(() => KnowledgeQueue.checkAllBases(), 0) - } - - // 添加笔记 - const addNote = async (content: string) => { - const noteId = uuidv4() - const note: KnowledgeItem = { - id: noteId, - type: 'note', - content, - created_at: Date.now(), - updated_at: Date.now() - } - - // 存储完整笔记到数据库 - await db.knowledge_notes.add(note) - - // 在 store 中只存储引用 - const noteRef: KnowledgeItem = { - id: noteId, - baseId, - type: 'note', - content: '', // store中不需要存储实际内容 - created_at: Date.now(), - updated_at: Date.now(), - processingStatus: 'pending', - processingProgress: 0, - processingError: '', - retryCount: 0 - } - - dispatch(updateNotes({ baseId, item: noteRef })) - setTimeout(() => KnowledgeQueue.checkAllBases(), 0) - } - - // 更新笔记内容 - const updateNoteContent = async (noteId: string, content: string) => { - const note = await db.knowledge_notes.get(noteId) - if (note) { - const updatedNote = { - ...note, - content, - updated_at: Date.now() - } - await db.knowledge_notes.put(updatedNote) - dispatch(updateNotes({ baseId, item: updatedNote })) - } - setTimeout(() => KnowledgeQueue.checkAllBases(), 0) - } - - // 获取笔记内容 - const getNoteContent = async (noteId: string) => { - return await db.knowledge_notes.get(noteId) - } - - // 移除项目 - const removeItem = async (item: KnowledgeItem) => { - dispatch(removeItemAction({ baseId, item })) - if (base) { - if (item?.uniqueId) { - await window.api.knowledgeBase.remove({ uniqueId: item.uniqueId, base: getKnowledgeBaseParams(base) }) - } - if (item.type === 'file' && typeof item.content === 'object') { - await FileManager.deleteFile(item.content.id) - } - } - } - - // 更新处理状态 - const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => { - dispatch( - updateItemProcessingStatus({ - baseId, - itemId, - status, - progress, - error - }) - ) - } - - // 获取特定项目的处理状态 - const getProcessingStatus = (itemId: string) => { - return base?.items.find((item) => item.id === itemId)?.processingStatus - } - - // 获取特定类型的所有处理项 - const getProcessingItemsByType = (type: 'file' | 'url' | 'note') => { - return base?.items.filter((item) => item.type === type && item.processingStatus !== undefined) || [] - } - - // 清除已完成的项目 - const clearCompleted = () => { - dispatch(clearCompletedProcessing({ baseId })) - } - - // 清除所有处理状态 - const clearAll = () => { - dispatch(clearAllProcessing({ baseId })) - } - - // 添加 Sitemap - const addSitemap = (url: string) => { - const newSitemapItem: KnowledgeItem = { - id: uuidv4(), - type: 'sitemap' as const, - content: url, - created_at: Date.now(), - updated_at: Date.now(), - processingStatus: 'pending', - processingProgress: 0, - processingError: '', - retryCount: 0 - } - dispatch(addItem({ baseId, item: newSitemapItem })) - setTimeout(() => KnowledgeQueue.checkAllBases(), 0) - } - - // Add directory support - const addDirectory = (path: string) => { - const newDirectoryItem: KnowledgeItem = { - id: uuidv4(), - type: 'directory', - content: path, - created_at: Date.now(), - updated_at: Date.now(), - processingStatus: 'pending', - processingProgress: 0, - processingError: '', - retryCount: 0 - } - dispatch(addItem({ baseId, item: newDirectoryItem })) - setTimeout(() => KnowledgeQueue.checkAllBases(), 0) - } - - const fileItems = base?.items.filter((item) => item.type === 'file') || [] - const directoryItems = base?.items.filter((item) => item.type === 'directory') || [] - const urlItems = base?.items.filter((item) => item.type === 'url') || [] - const sitemapItems = base?.items.filter((item) => item.type === 'sitemap') || [] - const [noteItems, setNoteItems] = useState([]) - - useEffect(() => { - const notes = base?.items.filter((item) => item.type === 'note') || [] - runAsyncFunction(async () => { - const newNoteItems = await Promise.all( - notes.map(async (item) => { - const note = await db.knowledge_notes.get(item.id) - return { ...item, content: note?.content || '' } - }) - ) - setNoteItems(newNoteItems.filter((note) => note !== undefined) as KnowledgeItem[]) - }) - }, [base?.items]) - - return { - base, - fileItems, - urlItems, - sitemapItems, - noteItems, - renameKnowledgeBase, - updateKnowledgeBase, - addFiles, - addUrl, - addSitemap, - addNote, - updateNoteContent, - getNoteContent, - updateItemStatus, - getProcessingStatus, - getProcessingItemsByType, - clearCompleted, - clearAll, - removeItem, - directoryItems, - addDirectory - } -} - -export const useKnowledgeBases = () => { - const dispatch = useDispatch() - const bases = useSelector((state: RootState) => state.knowledge.bases) - - const addKnowledgeBase = (base: KnowledgeBase) => { - dispatch(addBase(base)) - } - - const renameKnowledgeBase = (baseId: string, name: string) => { - dispatch(renameBase({ baseId, name })) - } - - const deleteKnowledgeBase = (baseId: string) => { - dispatch(deleteBase({ baseId })) - } - - const updateKnowledgeBases = (bases: KnowledgeBase[]) => { - dispatch(updateBases(bases)) - } - - return { - bases, - addKnowledgeBase, - renameKnowledgeBase, - deleteKnowledgeBase, - updateKnowledgeBases - } -}