595 lines
19 KiB
JavaScript
595 lines
19 KiB
JavaScript
// 配置信息
|
||
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;
|
||
}
|
||
}
|