Files
cherry-studio/scripts/cloudflare-worker.js
2024-11-22 10:01:54 +08:00

595 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 配置信息
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;
}
}