chore(version): 0.9.0

This commit is contained in:
kangfenmao 2024-12-23 14:34:41 +08:00
parent 7506d04c55
commit d558572d97
5 changed files with 446 additions and 708 deletions

View File

@ -2,11 +2,6 @@ name: Release
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
version:
description: 'Version (e.g. v1.2.3)'
required: true
type: string
push: push:
tags: tags:
- v*.*.* - v*.*.*
@ -20,8 +15,7 @@ jobs:
strategy: strategy:
matrix: matrix:
# windows-latest, ubuntu-latest os: [macos-13, macos-latest, windows-latest, ubuntu-latest]
os: [macos-13, macos-latest]
arch: [x64, arm64] arch: [x64, arm64]
exclude: exclude:
- os: windows-latest - os: windows-latest
@ -41,6 +35,9 @@ jobs:
node-version: 20 node-version: 20
arch: ${{ matrix.arch }} arch: ${{ matrix.arch }}
- name: Install corepack
run: corepack enable && corepack prepare yarn@4.3.1 --activate
- name: Get yarn cache directory path - name: Get yarn cache directory path
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
@ -55,9 +52,6 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-yarn- ${{ runner.os }}-yarn-
- name: Install corepack
run: corepack enable && corepack prepare yarn@4.3.1 --activate
- name: Install Dependencies - name: Install Dependencies
run: yarn install run: yarn install
@ -70,7 +64,7 @@ jobs:
- name: Build Mac - name: Build Mac
if: matrix.os == 'macos-13' || matrix.os == 'macos-latest' 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: env:
CSC_LINK: ${{ secrets.CSC_LINK }} CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}

View File

@ -48,47 +48,22 @@ mac:
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
notarize: false notarize: false
target:
- target: dmg
arch:
- arm64
- x64
- target: zip
arch:
- arm64
- x64
dmg: dmg:
artifactName: ${productName}-${version}-${arch}.${ext} artifactName: ${productName}-${version}-${arch}.${ext}
linux: linux:
target: target:
- target: AppImage - target: AppImage
arch:
- arm64
- x64
# - snap
# - deb
maintainer: electronjs.org maintainer: electronjs.org
category: Utility category: Utility
appImage: appImage:
artifactName: ${productName}-${version}-${arch}.${ext} artifactName: ${productName}-${version}-${arch}.${ext}
npmRebuild: false
publish: publish:
provider: generic provider: generic
url: https://cherrystudio.ocool.online url: https://cherrystudio.ocool.online
electronDownload: electronDownload:
mirror: https://npmmirror.com/mirrors/electron/ mirror: https://npmmirror.com/mirrors/electron/
afterPack: scripts/removeLocales.js afterPack: scripts/removeLocales.js
# afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
增加小程序快捷入口 增加知识库功能
增加ThinkAny、纳米搜索小程序
优化 Markdown 列表显示
可以多次点击上传文件按钮上传文件
大屏幕默认使用更大的输入框
设置中增加显示设置模块
支持 SVG 预览
Mermaid 图表支持复制源码
增加复制最后一条消息快捷键
o1模型默认开启流式输出
长文本粘贴为文件支持修改阈值

View File

@ -1,6 +1,6 @@
{ {
"name": "CherryStudio", "name": "CherryStudio",
"version": "0.8.27", "version": "0.9.0",
"private": true, "private": true,
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",

View File

@ -6,510 +6,558 @@ const config = {
CACHE_KEY: 'cherry-studio-latest-release', CACHE_KEY: 'cherry-studio-latest-release',
VERSION_DB: 'versions.json', VERSION_DB: 'versions.json',
LOG_FILE: 'logs.json', LOG_FILE: 'logs.json',
MAX_LOGS: 1000 // 最多保存多少条日志 MAX_LOGS: 1000 // 最多保存多少条日志
}; }
// Worker 入口函数 // Worker 入口函数
const worker = { const worker = {
// 定时器触发配置 // 定时器触发配置
scheduled: { scheduled: {
cron: '*/1 * * * *' // 每分钟执行一次 cron: '*/1 * * * *' // 每分钟执行一次
}, },
// 定时器执行函数 - 只负责检查和更新 // 定时器执行函数 - 只负责检查和更新
async scheduled(event, env, ctx) { async scheduled(event, env, ctx) {
try { try {
await initDataFiles(env); await initDataFiles(env)
console.log('开始定时检查新版本...'); console.log('开始定时检查新版本...')
// 使用新的 checkNewRelease 函数 // 使用新的 checkNewRelease 函数
await checkNewRelease(env); await checkNewRelease(env)
} catch (error) { } catch (error) {
console.error('定时任务执行失败:', error); console.error('定时任务执行失败:', error)
} }
}, },
// HTTP 请求处理函数 - 只负责返回数据 // HTTP 请求处理函数 - 只负责返回数据
async fetch(request, env, ctx) { async fetch(request, env, ctx) {
if (!env || !env.R2_BUCKET) { if (!env || !env.R2_BUCKET) {
return new Response(JSON.stringify({ return new Response(
error: 'R2 存储桶未正确配置' JSON.stringify({
}), { error: 'R2 存储桶未正确配置'
status: 500, }),
headers: { 'Content-Type': 'application/json' } {
}); 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); return await getCachedRelease(env)
} catch (error) {
try { return new Response(
// 处理文件下载请求 JSON.stringify({
if (filename) { error: error.message,
return await handleDownload(env, filename); stack: error.stack
} }),
{
// 只返回缓存的版本信息 status: 500,
return await getCachedRelease(env); headers: { 'Content-Type': 'application/json' }
} 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) { async function addLog(env, type, event, details = null) {
try { try {
const logFile = await env.R2_BUCKET.get(config.LOG_FILE); const logFile = await env.R2_BUCKET.get(config.LOG_FILE)
let logs = { logs: [] }; let logs = { logs: [] }
if (logFile) { if (logFile) {
logs = JSON.parse(await logFile.text()); logs = JSON.parse(await logFile.text())
} }
logs.logs.unshift({ logs.logs.unshift({
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
type, type,
event, event,
details details
}); })
// 保持日志数量在限制内 // 保持日志数量在限制内
if (logs.logs.length > config.MAX_LOGS) { if (logs.logs.length > config.MAX_LOGS) {
logs.logs = logs.logs.slice(0, 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) { } catch (error) {
console.error('写入日志失败:', error); console.error('写入日志失败:', error)
} }
} }
/** /**
* 获取最新版本信息 * 获取最新版本信息
*/ */
async function getLatestRelease(env) { async function getLatestRelease(env) {
try { try {
const cached = await env.R2_BUCKET.get(config.CACHE_KEY); const cached = await env.R2_BUCKET.get(config.CACHE_KEY)
if (!cached) { if (!cached) {
// 如果缓存不存在,先检查版本数据库 // 如果缓存不存在,先检查版本数据库
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB); const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
if (versionDB) { if (versionDB) {
const versions = JSON.parse(await versionDB.text()); const versions = JSON.parse(await versionDB.text())
if (versions.latestVersion) { if (versions.latestVersion) {
// 从版本数据库重建缓存 // 从版本数据库重建缓存
const latestVersion = versions.versions[versions.latestVersion]; const latestVersion = versions.versions[versions.latestVersion]
const cacheData = { const cacheData = {
version: latestVersion.version, version: latestVersion.version,
publishedAt: latestVersion.publishedAt, publishedAt: latestVersion.publishedAt,
changelog: latestVersion.changelog, changelog: latestVersion.changelog,
downloads: latestVersion.files downloads: latestVersion.files
.filter(file => file.uploaded) .filter((file) => file.uploaded)
.map(file => ({ .map((file) => ({
name: file.name, name: file.name,
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`, url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
size: formatFileSize(file.size) 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 checkNewRelease(env); await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
return new Response(JSON.stringify(data), { return new Response(JSON.stringify(cacheData), {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*' '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(); const data = await cached.text()
return new Response(data, { return new Response(data, {
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*' 'Access-Control-Allow-Origin': '*'
} }
}); })
} catch (error) { } catch (error) {
await addLog(env, 'ERROR', '获取版本信息失败', error.message); await addLog(env, 'ERROR', '获取版本信息失败', error.message)
return new Response(JSON.stringify({ return new Response(
error: '获取版本信息失败: ' + error.message, JSON.stringify({
detail: '请稍<E8AFB7><E7A88D><EFBFBD>再试' error: '获取版本信息失败: ' + error.message,
}), { detail: '请稍后再试'
status: 500, }),
headers: { {
'Content-Type': 'application/json', status: 500,
'Access-Control-Allow-Origin': '*' headers: {
} 'Content-Type': 'application/json',
}); 'Access-Control-Allow-Origin': '*'
}
}
)
} }
} }
// 修改下载处理函数,直接接收 env // 修改下载处理函数,直接接收 env
async function handleDownload(env, filename) { async function handleDownload(env, filename) {
try { try {
const object = await env.R2_BUCKET.get(filename); const object = await env.R2_BUCKET.get(filename)
if (!object) { if (!object) {
return new Response('文件未找到', { status: 404 }); return new Response('文件未找到', { status: 404 })
} }
// 设置响应头 // 设置响应头
const headers = new Headers(); const headers = new Headers()
object.writeHttpMetadata(headers); object.writeHttpMetadata(headers)
headers.set('etag', object.httpEtag); headers.set('etag', object.httpEtag)
headers.set('Content-Disposition', `attachment; filename="${filename}"`); headers.set('Content-Disposition', `attachment; filename="${filename}"`)
return new Response(object.body, { return new Response(object.body, {
headers headers
}); })
} catch (error) { } catch (error) {
console.error('下载文件时发生错误:', error); console.error('下载文件时发生错误:', error)
return new Response('获取文件失败', { status: 500 }); return new Response('获取文件失败', { status: 500 })
} }
} }
/** /**
* 根据文件扩展名获取对应的 Content-Type * 根据文件扩展名获取对应的 Content-Type
*/ */
function getContentType(filename) { function getContentType(filename) {
const ext = filename.split('.').pop().toLowerCase(); const ext = filename.split('.').pop().toLowerCase()
const types = { const types = {
'exe': 'application/x-msdownload', // Windows 可执行文件 exe: 'application/x-msdownload', // Windows 可执行文件
'dmg': 'application/x-apple-diskimage', // macOS 安装包 dmg: 'application/x-apple-diskimage', // macOS 安装包
'zip': 'application/zip', // 压缩包 zip: 'application/zip', // 压缩包
'AppImage': 'application/x-executable', // Linux 可执行文件 AppImage: 'application/x-executable', // Linux 可执行文件
'blockmap': 'application/octet-stream' // 更新文件 blockmap: 'application/octet-stream' // 更新文件
}; }
return types[ext] || 'application/octet-stream'; return types[ext] || 'application/octet-stream'
} }
/** /**
* 格式化文件大小 * 格式化文件大小
* 将字节转换为人类可读的格式B, KB, MB, GB * 将字节转换为人类可读的格式B, KB, MB, GB
*/ */
function formatFileSize(bytes) { function formatFileSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB']; const units = ['B', 'KB', 'MB', 'GB']
let size = bytes; let size = bytes
let unitIndex = 0; let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) { while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024; size /= 1024
unitIndex++; unitIndex++
} }
return `${size.toFixed(2)} ${units[unitIndex]}`; return `${size.toFixed(2)} ${units[unitIndex]}`
} }
/** /**
* 版本号比较函数 * 版本号比较函数
* 用于对版本号进行排序 * 用于对版本号进行排序
*/ */
function compareVersions(a, b) { function compareVersions(a, b) {
const partsA = a.replace('v', '').split('.'); const partsA = a.replace('v', '').split('.')
const partsB = b.replace('v', '').split('.'); const partsB = b.replace('v', '').split('.')
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) { for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const numA = parseInt(partsA[i] || 0); const numA = parseInt(partsA[i] || 0)
const numB = parseInt(partsB[i] || 0); const numB = parseInt(partsB[i] || 0)
if (numA !== numB) { if (numA !== numB) {
return numA - numB; return numA - numB
} }
} }
return 0; return 0
} }
/** /**
* 初始化数据文件 * 初始化数据文件
*/ */
async function initDataFiles(env) { async function initDataFiles(env) {
try { try {
// 检查并初始化版本数据库 // 检查并初始化版本数据库
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB); const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
if (!versionDB) { if (!versionDB) {
const initialVersions = { const initialVersions = {
versions: {}, versions: {},
latestVersion: null, latestVersion: null,
lastChecked: new Date().toISOString() lastChecked: new Date().toISOString()
};
await env.R2_BUCKET.put(config.VERSION_DB, JSON.stringify(initialVersions, null, 2));
await addLog(env, 'INFO', 'versions.json 初始化成功');
} }
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); const logFile = await env.R2_BUCKET.get(config.LOG_FILE)
if (!logFile) { if (!logFile) {
const initialLogs = { const initialLogs = {
logs: [{ logs: [
timestamp: new Date().toISOString(), {
type: 'INFO', timestamp: new Date().toISOString(),
event: '系统初始化' type: 'INFO',
}] event: '系统初始化'
}; }
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2)); ]
console.log('logs.json 初始化成功');
} }
await env.R2_BUCKET.put(config.LOG_FILE, JSON.stringify(initialLogs, null, 2))
console.log('logs.json 初始化成功')
}
} catch (error) { } catch (error) {
console.error('初始化数据文件失败:', error); console.error('初始化数据文件失败:', error)
} }
} }
// 新增:只获取缓存的版本信息 // 新增:只获取缓存的版本信息
async function getCachedRelease(env) { async function getCachedRelease(env) {
try { try {
const cached = await env.R2_BUCKET.get(config.CACHE_KEY); const cached = await env.R2_BUCKET.get(config.CACHE_KEY)
if (!cached) { if (!cached) {
// 如果缓存不存在,从版本数据库获取 // 如果缓存不存在,从版本数据库获取
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB); const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
if (versionDB) { if (versionDB) {
const versions = JSON.parse(await versionDB.text()); const versions = JSON.parse(await versionDB.text())
if (versions.latestVersion) { if (versions.latestVersion) {
const latestVersion = versions.versions[versions.latestVersion]; const latestVersion = versions.versions[versions.latestVersion]
const cacheData = { const cacheData = {
version: latestVersion.version, version: latestVersion.version,
publishedAt: latestVersion.publishedAt, publishedAt: latestVersion.publishedAt,
changelog: latestVersion.changelog, changelog: latestVersion.changelog,
downloads: latestVersion.files downloads: latestVersion.files
.filter(file => file.uploaded) .filter((file) => file.uploaded)
.map(file => ({ .map((file) => ({
name: file.name, name: file.name,
url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`, url: `https://${config.R2_CUSTOM_DOMAIN}/${file.name}`,
size: formatFileSize(file.size) 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({ await env.R2_BUCKET.put(config.CACHE_KEY, JSON.stringify(cacheData))
error: '没有可用的版本信息' return new Response(JSON.stringify(cacheData), {
}), { headers: {
status: 404,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
// 返回缓存数据
return new Response(await cached.text(), {
headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*' '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) { } catch (error) {
await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message); await addLog(env, 'ERROR', '获取缓存版本信息失败', error.message)
throw error; throw error
} }
} }
// 新增:只检查新版本并更新 // 新增:只检查新版本并更新
async function checkNewRelease(env) { async function checkNewRelease(env) {
try { try {
// 获取 GitHub 最新版本 // 获取 GitHub 最新版本
const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', { const githubResponse = await fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest', {
headers: { 'User-Agent': 'CloudflareWorker' }, headers: { 'User-Agent': 'CloudflareWorker' }
}); })
if (!githubResponse.ok) { if (!githubResponse.ok) {
throw new Error('GitHub API 请求失败'); throw new Error('GitHub API 请求失败')
} }
const releaseData = await githubResponse.json(); const releaseData = await githubResponse.json()
const version = releaseData.tag_name; const version = releaseData.tag_name
// 获取版本数据库 // 获取版本数据库
const versionDB = await env.R2_BUCKET.get(config.VERSION_DB); const versionDB = await env.R2_BUCKET.get(config.VERSION_DB)
let versions = { versions: {}, latestVersion: null, lastChecked: new Date().toISOString() }; let versions = { versions: {}, latestVersion: null, lastChecked: new Date().toISOString() }
if (versionDB) { if (versionDB) {
versions = JSON.parse(await versionDB.text()); versions = JSON.parse(await versionDB.text())
} }
// 移除版本检查,改为记录是否有文件更新的标志 // 移除版本检查,改为记录是否有文件更新的标志
let hasUpdates = false; let hasUpdates = false
if (versions.latestVersion !== version) { if (versions.latestVersion !== version) {
await addLog(env, 'INFO', `发现新版本: ${version}`); await addLog(env, 'INFO', `发现新版本: ${version}`)
hasUpdates = true; hasUpdates = true
} else { } else {
await addLog(env, 'INFO', `版本 ${version} 文件完整性检查开始`); await addLog(env, 'INFO', `版本 ${version} 文件完整性检查开始`)
} }
// 准备新版本记录 // 准备新版本记录
const versionRecord = { const versionRecord = {
version, version,
publishedAt: releaseData.published_at, publishedAt: releaseData.published_at,
uploadedAt: null, uploadedAt: null,
files: releaseData.assets.map(asset => ({ files: releaseData.assets.map((asset) => ({
name: asset.name, name: asset.name,
size: asset.size, size: asset.size,
uploaded: false uploaded: false
})), })),
changelog: releaseData.body changelog: releaseData.body
}; }
// 检查并上传文件 // 检查并上传文件
for (const asset of releaseData.assets) { for (const asset of releaseData.assets) {
try { try {
const existingFile = await env.R2_BUCKET.get(asset.name); const existingFile = await env.R2_BUCKET.get(asset.name)
// 检查文件是否存在且大小是否一致 // 检查文件是否存在且大小是否一致
if (!existingFile || existingFile.size !== asset.size) { if (!existingFile || existingFile.size !== asset.size) {
hasUpdates = true; hasUpdates = true
const response = await fetch(asset.browser_download_url); const response = await fetch(asset.browser_download_url)
if (!response.ok) { if (!response.ok) {
throw new Error(`下载失败: HTTP ${response.status}`); 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);
} }
}
// 只有在有更新或是新版本时才更新数据库和缓存 const file = await response.arrayBuffer()
if (hasUpdates) { await env.R2_BUCKET.put(asset.name, file, {
// 更新版本记录 httpMetadata: { contentType: getContentType(asset.name) }
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 fileIndex = versionRecord.files.findIndex((f) => f.name === asset.name)
if (fileIndex !== -1) {
// 更新缓存 versionRecord.files[fileIndex].uploaded = true
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));
} }
} 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) { } catch (error) {
await addLog(env, 'ERROR', '检查新版本失败', error.message); await addLog(env, 'ERROR', '检查新版本失败', error.message)
throw error; throw error
} }
} }
// 新增:获取 R2 桶中的所有文件列表 // 新增:获取 R2 桶中的所有文件列表
async function listAllFiles(env) { async function listAllFiles(env) {
const files = []; const files = []
let cursor; let cursor
do { do {
const listed = await env.R2_BUCKET.list({ cursor, include: ['customMetadata'] }); const listed = await env.R2_BUCKET.list({ cursor, include: ['customMetadata'] })
files.push(...listed.objects); files.push(...listed.objects)
cursor = listed.cursor; cursor = listed.cursor
} while (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)
}
} }

View File

@ -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<KnowledgeItem[]>([])
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
}
}