diff --git a/.eslintignore b/.eslintignore index 8129084d..f874b469 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,4 @@ node_modules dist out .gitignore - +scripts/cloudflare-worker.js diff --git a/scripts/cloudflare-worker.js b/scripts/cloudflare-worker.js new file mode 100644 index 00000000..e912009d --- /dev/null +++ b/scripts/cloudflare-worker.js @@ -0,0 +1,595 @@ +// 配置信息 +const config = { + R2_CUSTOM_DOMAIN: 'cherrystudio.ocool.online', + R2_BUCKET_NAME: 'cherrystudio', + // 缓存键名 + CACHE_KEY: 'cherry-studio-latest-release', + VERSION_DB: 'versions.json', + LOG_FILE: 'logs.json', + MAX_LOGS: 1000 // 最多保存多少条日志 + }; + + // Worker 入口函数 + const worker = { + // 定时器触发配置 + scheduled: { + cron: '*/1 * * * *' // 每分钟执行一次 + }, + + // 定时器执行函数 - 只负责检查和更新 + async scheduled(event, env, ctx) { + try { + await initDataFiles(env); + console.log('开始定时检查新版本...'); + // 注意这里使用新的函数 + 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' } + }); + } + + 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' } + }); + } + } + }; + + 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: [] }; + + if (logFile) { + logs = JSON.parse(await logFile.text()); + } + + 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); + } + + await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(logs, null, 2)); + } catch (error) { + console.error('写入日志失败:', error); + } + } + + /** + * 检查并更新发布版本 + * 由定时器触发,检查新版本并更新 R2 存储 + */ + async function checkAndUpdateRelease(env) { + try { + // 获取版本数据库 + const versionDB = await env.R2_BUCKET.get(config.VERSION_DB); + let versions = { versions: {}, latestVersion: null, lastChecked: null }; + + if (versionDB) { + versions = JSON.parse(await versionDB.text()); + } + + // 获取 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 请求失败'); + } + + const releaseData = await githubResponse.json(); + const version = releaseData.tag_name; + + // 更新最后检查时间 + versions.lastChecked = new Date().toISOString(); + + // 检查是否需要更新 + if (versions.latestVersion !== version) { + 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 + }; + + // 上传文件 + for (const asset of releaseData.assets) { + try { + const existingFile = await env.R2_BUCKET.get(asset.name); + if (existingFile) { + // 更新文件状态 + const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name); + if (fileIndex !== -1) { + versionRecord.files[fileIndex].uploaded = true; + } + continue; + } + + 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', `文件上传成功: ${asset.name}`); + } catch (error) { + await addLog(env, 'ERROR', `文件上传失败: ${asset.name}`, error.message); + } + } + + // 更新版本记录 + 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) + })) + }; + + await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData)); + + // 清理旧版本 + const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a)); + if (versionList.length > 2) { + const oldVersions = versionList.slice(2); + for (const oldVersion of oldVersions) { + const oldFiles = versions.versions[oldVersion].files; + for (const file of oldFiles) { + if (file.uploaded) { + await env.R2_BUCKET.delete(file.name); + await addLog(env, 'INFO', `删除旧文件: ${file.name}`); + } + } + delete versions.versions[oldVersion]; + } + // 保存更新后的版本数据库 + await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2)); + } + + return cacheData; + } else { + // 没有新版本,返回缓存数据 + const cached = await env.R2_BUCKET.get(config.CACHE_KEY); + return cached ? JSON.parse(await cached.text()) : null; + } + } catch (error) { + await addLog(env, 'ERROR', '检查更新失败', error.message); + throw 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 data = await checkAndUpdateRelease(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': '*' + } + }); + } 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': '*' + } + }); + } + } + + // 修改下载处理函数,直接接收 env + async function handleDownload(env, filename) { + try { + const object = await env.R2_BUCKET.get(filename); + + 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}"`); + + return new Response(object.body, { + headers + }); + } catch (error) { + console.error('下载文件时发生错误:', error); + return new Response('获取文件失败', { status: 500 }); + } + } + + /** + * 根据文件扩展名获取对应的 Content-Type + */ + function getContentType(filename) { + 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'; + } + + /** + * 格式化文件大小 + * 将字节转换为人类可读的格式(B, KB, MB, GB) + */ + function formatFileSize(bytes) { + const units = ['B', 'KB', 'MB', 'GB']; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + return `${size.toFixed(2)} ${units[unitIndex]}`; + } + + /** + * 版本号比较函数 + * 用于对版本号进行排序 + */ + function compareVersions(a, b) { + 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); + + if (numA !== numB) { + return numA - numB; + } + } + + 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 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); + } + } + + // 新增:只获取缓存的版本信息 + 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': '*' + } + }); + } + } + // 如果没有任何数据,返回错误 + 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; + } + } + + // 新增:只检查新版本并更新 + async function checkNewRelease(env) { + try { + // 获取 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 请求失败'); + } + + 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() }; + + if (versionDB) { + versions = JSON.parse(await versionDB.text()); + } + + // 如果版本相同,不需要更新 + if (versions.latestVersion === version) { + console.log('当前已是最新版本'); + return; + } + + 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 + }; + + // 上传文件 + for (const asset of releaseData.assets) { + try { + const existingFile = await env.R2_BUCKET.get(asset.name); + if (existingFile) { + // 更新文件状态 + const fileIndex = versionRecord.files.findIndex(f => f.name === asset.name); + if (fileIndex !== -1) { + versionRecord.files[fileIndex].uploaded = true; + } + continue; + } + + 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', `文件上传成功: ${asset.name}`); + } catch (error) { + await addLog(env, 'ERROR', `文件上传失败: ${asset.name}`, error.message); + } + } + + // 更新版本记录 + 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) + })) + }; + + await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData)); + + // 清理旧版本 + const versionList = Object.keys(versions.versions).sort((a, b) => compareVersions(b, a)); + if (versionList.length > 2) { + const oldVersions = versionList.slice(2); + for (const oldVersion of oldVersions) { + const oldFiles = versions.versions[oldVersion].files; + for (const file of oldFiles) { + if (file.uploaded) { + await env.R2_BUCKET.delete(file.name); + await addLog(env, 'INFO', `删除旧文件: ${file.name}`); + } + } + delete versions.versions[oldVersion]; + } + // 保存更新后的版本数据库 + await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(versions, null, 2)); + } + + return cacheData; + } catch (error) { + await addLog(env, 'ERROR', '检查新版本失败', error.message); + throw error; + } + } \ No newline at end of file