mirror of
https://mirror.skon.top/github.com/ILoveBingLu/CipherTalk
synced 2026-04-30 13:51:50 +08:00
feat: 支持 GitHub 主源与自定义策略更新
This commit is contained in:
46
README.md
46
README.md
@@ -206,6 +206,52 @@ npm run mcp
|
||||
|
||||
安装版会附带 `ciphertalk-mcp.cmd` 伴随启动器,放在安装目录根部,可直接作为宿主的 `command` 使用。
|
||||
|
||||
### 强制更新清单
|
||||
|
||||
当前更新架构:
|
||||
|
||||
- **主更新源**:GitHub Release(安装包、`latest.yml`)
|
||||
- **策略补充源**:`https://miyuapp.aiqji.com`
|
||||
- **策略优先级**:GitHub 优先,自定义源仅在 GitHub 策略不可用时作为回退
|
||||
|
||||
应用启动时会按以下顺序请求 `force-update.json`,用于判定:
|
||||
|
||||
1. `https://github.com/ILoveBingLu/CipherTalk/releases/latest/download/force-update.json`
|
||||
2. `https://miyuapp.aiqji.com/force-update.json`
|
||||
|
||||
策略字段含义:
|
||||
|
||||
- 最低安全版本 `minimumSupportedVersion`
|
||||
- 被封禁版本列表 `blockedVersions`
|
||||
- 强制更新提示文案 `title` / `message`
|
||||
|
||||
可用以下命令在 `release/force-update.json` 生成清单:
|
||||
|
||||
```bash
|
||||
FORCE_UPDATE_MIN_VERSION=2.2.15 npm run build:force-update-manifest
|
||||
```
|
||||
|
||||
示例结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"latestVersion": "2.2.15",
|
||||
"minimumSupportedVersion": "2.2.14",
|
||||
"blockedVersions": ["2.2.13"],
|
||||
"title": "必须更新到最新版本",
|
||||
"message": "当前版本存在安全风险,请立即更新。",
|
||||
"releaseNotes": "修复关键安全问题",
|
||||
"publishedAt": "2026-04-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
发布要求:
|
||||
|
||||
- **GitHub Release 必须上传**:安装包、`latest.yml`、`force-update.json`
|
||||
- **自定义源可上传**:`force-update.json`
|
||||
- 自定义源不再承担安装包和 `latest.yml` 分发
|
||||
|
||||
### v1 工具
|
||||
|
||||
- `health_check`
|
||||
|
||||
@@ -3,6 +3,7 @@ import { join } from 'path'
|
||||
import { readFileSync, existsSync, mkdirSync } from 'fs'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { DatabaseService } from './services/database'
|
||||
import { appUpdateService } from './services/appUpdateService'
|
||||
|
||||
import { wechatDecryptService } from './services/decryptService'
|
||||
import { ConfigService } from './services/config'
|
||||
@@ -61,29 +62,6 @@ autoUpdater.autoDownload = false
|
||||
autoUpdater.autoInstallOnAppQuit = true
|
||||
autoUpdater.disableDifferentialDownload = true // 禁用差分更新,强制全量下载
|
||||
|
||||
/**
|
||||
* 比较两个语义化版本号
|
||||
* @param version1 版本1
|
||||
* @param version2 版本2
|
||||
* @returns version1 > version2 返回 true
|
||||
*/
|
||||
function isNewerVersion(version1: string, version2: string): boolean {
|
||||
const v1Parts = version1.split('.').map(Number)
|
||||
const v2Parts = version2.split('.').map(Number)
|
||||
|
||||
// 补齐版本号位数
|
||||
const maxLength = Math.max(v1Parts.length, v2Parts.length)
|
||||
while (v1Parts.length < maxLength) v1Parts.push(0)
|
||||
while (v2Parts.length < maxLength) v2Parts.push(0)
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
if (v1Parts[i] > v2Parts[i]) return true
|
||||
if (v1Parts[i] < v2Parts[i]) return false
|
||||
}
|
||||
|
||||
return false // 版本相同
|
||||
}
|
||||
|
||||
// 单例服务
|
||||
let dbService: DatabaseService | null = null
|
||||
|
||||
@@ -237,6 +215,12 @@ function createWindow() {
|
||||
|
||||
// 监听窗口关闭事件
|
||||
win.on('close', (event) => {
|
||||
const updateInfo = appUpdateService.getCachedUpdateInfo()
|
||||
if (updateInfo?.forceUpdate) {
|
||||
app.isQuitting = true
|
||||
return
|
||||
}
|
||||
|
||||
// 如果是真正退出应用,不阻止
|
||||
if (app.isQuitting) {
|
||||
return
|
||||
@@ -1297,25 +1281,20 @@ function registerIpcHandlers() {
|
||||
})
|
||||
|
||||
ipcMain.handle('app:checkForUpdates', async () => {
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates()
|
||||
if (result && result.updateInfo) {
|
||||
const currentVersion = app.getVersion()
|
||||
const latestVersion = result.updateInfo.version
|
||||
return appUpdateService.checkForUpdates()
|
||||
})
|
||||
|
||||
// 使用语义化版本比较
|
||||
if (isNewerVersion(latestVersion, currentVersion)) {
|
||||
return {
|
||||
hasUpdate: true,
|
||||
version: latestVersion,
|
||||
releaseNotes: result.updateInfo.releaseNotes as string || ''
|
||||
}
|
||||
}
|
||||
}
|
||||
return { hasUpdate: false }
|
||||
} catch (error) {
|
||||
console.error('检查更新失败:', error)
|
||||
return { hasUpdate: false }
|
||||
ipcMain.handle('app:getUpdateState', async () => {
|
||||
return appUpdateService.getCachedUpdateInfo()
|
||||
})
|
||||
|
||||
ipcMain.handle('app:getUpdateSourceInfo', async () => {
|
||||
return {
|
||||
primaryUpdateSource: 'github' as const,
|
||||
githubRepository: appUpdateService.getGithubRepository(),
|
||||
policySources: ['github', 'custom'] as const,
|
||||
policyPrecedence: 'github' as const,
|
||||
forceUpdatePolicyFallbackUrl: appUpdateService.getForceUpdatePolicyFallbackUrl()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -3708,19 +3687,9 @@ function checkForUpdatesOnStartup() {
|
||||
// 延迟3秒检测,等待窗口完全加载
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates()
|
||||
if (result && result.updateInfo) {
|
||||
const currentVersion = app.getVersion()
|
||||
const latestVersion = result.updateInfo.version
|
||||
|
||||
// 使用语义化版本比较
|
||||
if (isNewerVersion(latestVersion, currentVersion) && mainWindow) {
|
||||
// 通知渲染进程有新版本
|
||||
mainWindow.webContents.send('app:updateAvailable', {
|
||||
version: latestVersion,
|
||||
releaseNotes: result.updateInfo.releaseNotes || ''
|
||||
})
|
||||
}
|
||||
const result = await appUpdateService.checkForUpdates()
|
||||
if (result.hasUpdate && mainWindow) {
|
||||
mainWindow.webContents.send('app:updateAvailable', result)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动时检查更新失败:', error)
|
||||
|
||||
@@ -71,6 +71,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
getDownloadsPath: () => ipcRenderer.invoke('app:getDownloadsPath'),
|
||||
getVersion: () => ipcRenderer.invoke('app:getVersion'),
|
||||
getMcpLaunchConfig: () => getMcpLaunchConfigSafe(),
|
||||
getUpdateState: () => ipcRenderer.invoke('app:getUpdateState'),
|
||||
getUpdateSourceInfo: () => ipcRenderer.invoke('app:getUpdateSourceInfo'),
|
||||
checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'),
|
||||
downloadAndInstall: () => ipcRenderer.invoke('app:downloadAndInstall'),
|
||||
getStartupDbConnected: () => ipcRenderer.invoke('app:getStartupDbConnected'),
|
||||
@@ -79,7 +81,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
ipcRenderer.on('app:downloadProgress', (_, progress) => callback(progress))
|
||||
return () => ipcRenderer.removeAllListeners('app:downloadProgress')
|
||||
},
|
||||
onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => {
|
||||
onUpdateAvailable: (callback: (info: {
|
||||
hasUpdate: boolean
|
||||
forceUpdate: boolean
|
||||
currentVersion: string
|
||||
version?: string
|
||||
releaseNotes?: string
|
||||
title?: string
|
||||
message?: string
|
||||
minimumSupportedVersion?: string
|
||||
reason?: 'minimum-version' | 'blocked-version'
|
||||
checkedAt: number
|
||||
updateSource: 'github' | 'custom' | 'none'
|
||||
policySource: 'github' | 'custom' | 'none'
|
||||
}) => void) => {
|
||||
ipcRenderer.on('app:updateAvailable', (_, info) => callback(info))
|
||||
return () => ipcRenderer.removeAllListeners('app:updateAvailable')
|
||||
}
|
||||
|
||||
198
electron/services/appUpdateService.ts
Normal file
198
electron/services/appUpdateService.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import { app } from 'electron'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
|
||||
const GITHUB_OWNER = 'ILoveBingLu'
|
||||
const GITHUB_REPO = 'CipherTalk'
|
||||
const GITHUB_FORCE_UPDATE_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download/force-update.json`
|
||||
const FORCE_UPDATE_POLICY_FALLBACK_URL = 'https://miyuapp.aiqji.com'
|
||||
|
||||
export type ForceUpdateReason = 'minimum-version' | 'blocked-version'
|
||||
export type AppUpdateSource = 'github' | 'custom' | 'none'
|
||||
|
||||
export interface ForceUpdateManifest {
|
||||
schemaVersion: number
|
||||
latestVersion?: string
|
||||
minimumSupportedVersion?: string
|
||||
blockedVersions?: string[]
|
||||
title?: string
|
||||
message?: string
|
||||
releaseNotes?: string
|
||||
publishedAt?: string
|
||||
}
|
||||
|
||||
export interface AppUpdateInfo {
|
||||
hasUpdate: boolean
|
||||
forceUpdate: boolean
|
||||
currentVersion: string
|
||||
version?: string
|
||||
releaseNotes?: string
|
||||
title?: string
|
||||
message?: string
|
||||
minimumSupportedVersion?: string
|
||||
reason?: ForceUpdateReason
|
||||
checkedAt: number
|
||||
updateSource: AppUpdateSource
|
||||
policySource: AppUpdateSource
|
||||
}
|
||||
|
||||
type ManifestLookupResult = {
|
||||
manifest: ForceUpdateManifest | null
|
||||
source: AppUpdateSource
|
||||
}
|
||||
|
||||
function isNewerVersion(version1: string, version2: string): boolean {
|
||||
const v1Parts = version1.split('.').map(Number)
|
||||
const v2Parts = version2.split('.').map(Number)
|
||||
const maxLength = Math.max(v1Parts.length, v2Parts.length)
|
||||
while (v1Parts.length < maxLength) v1Parts.push(0)
|
||||
while (v2Parts.length < maxLength) v2Parts.push(0)
|
||||
|
||||
for (let i = 0; i < maxLength; i++) {
|
||||
if (v1Parts[i] > v2Parts[i]) return true
|
||||
if (v1Parts[i] < v2Parts[i]) return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function isVersionEqual(version1: string, version2: string): boolean {
|
||||
return !isNewerVersion(version1, version2) && !isNewerVersion(version2, version1)
|
||||
}
|
||||
|
||||
function normalizeReleaseNotes(value: unknown): string {
|
||||
if (!value) return ''
|
||||
if (typeof value === 'string') return value
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => {
|
||||
if (typeof item === 'string') return item
|
||||
if (item && typeof item === 'object' && 'note' in item) {
|
||||
return String((item as { note?: unknown }).note || '')
|
||||
}
|
||||
return String(item)
|
||||
}).filter(Boolean).join('\n\n')
|
||||
}
|
||||
if (value && typeof value === 'object' && 'note' in value) {
|
||||
return String((value as { note?: unknown }).note || '')
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
||||
async function fetchManifestFromUrl(url: string): Promise<ForceUpdateManifest | null> {
|
||||
try {
|
||||
const response = await fetch(`${url}${url.includes('?') ? '&' : '?'}t=${Date.now()}`, {
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
}
|
||||
})
|
||||
if (!response.ok) return null
|
||||
|
||||
const data = await response.json() as ForceUpdateManifest
|
||||
if (!data || typeof data !== 'object') return null
|
||||
if (Number(data.schemaVersion || 0) < 1) return null
|
||||
return data
|
||||
} catch (error) {
|
||||
console.warn('[AppUpdate] 获取策略文件失败:', url, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function resolveForceUpdateManifest(): Promise<ManifestLookupResult> {
|
||||
const githubManifest = await fetchManifestFromUrl(GITHUB_FORCE_UPDATE_URL)
|
||||
if (githubManifest) {
|
||||
return { manifest: githubManifest, source: 'github' }
|
||||
}
|
||||
|
||||
const fallbackUrl = `${FORCE_UPDATE_POLICY_FALLBACK_URL.replace(/\/+$/, '')}/force-update.json`
|
||||
const customManifest = await fetchManifestFromUrl(fallbackUrl)
|
||||
if (customManifest) {
|
||||
return { manifest: customManifest, source: 'custom' }
|
||||
}
|
||||
|
||||
return { manifest: null, source: 'none' }
|
||||
}
|
||||
|
||||
class AppUpdateService {
|
||||
private lastInfo: AppUpdateInfo | null = null
|
||||
|
||||
getCachedUpdateInfo(): AppUpdateInfo | null {
|
||||
return this.lastInfo
|
||||
}
|
||||
|
||||
getForceUpdatePolicyFallbackUrl(): string {
|
||||
return FORCE_UPDATE_POLICY_FALLBACK_URL
|
||||
}
|
||||
|
||||
getGithubRepository(): { owner: string; repo: string } {
|
||||
return {
|
||||
owner: GITHUB_OWNER,
|
||||
repo: GITHUB_REPO
|
||||
}
|
||||
}
|
||||
|
||||
private buildInfo(payload: Partial<AppUpdateInfo>): AppUpdateInfo {
|
||||
return {
|
||||
hasUpdate: false,
|
||||
forceUpdate: false,
|
||||
currentVersion: app.getVersion(),
|
||||
checkedAt: Date.now(),
|
||||
updateSource: 'none',
|
||||
policySource: 'none',
|
||||
...payload
|
||||
}
|
||||
}
|
||||
|
||||
async checkForUpdates(): Promise<AppUpdateInfo> {
|
||||
const currentVersion = app.getVersion()
|
||||
let latestVersion: string | undefined
|
||||
let releaseNotes = ''
|
||||
let hasUpdate = false
|
||||
let updateSource: AppUpdateSource = 'none'
|
||||
|
||||
try {
|
||||
const result = await autoUpdater.checkForUpdates()
|
||||
if (result?.updateInfo?.version) {
|
||||
latestVersion = result.updateInfo.version
|
||||
releaseNotes = normalizeReleaseNotes(result.updateInfo.releaseNotes)
|
||||
hasUpdate = isNewerVersion(latestVersion, currentVersion)
|
||||
updateSource = hasUpdate ? 'github' : 'none'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[AppUpdate] 检查 GitHub 更新失败:', error)
|
||||
}
|
||||
|
||||
const { manifest, source: policySource } = await resolveForceUpdateManifest()
|
||||
let forceUpdate = false
|
||||
let reason: ForceUpdateReason | undefined
|
||||
|
||||
if (manifest?.minimumSupportedVersion && isNewerVersion(manifest.minimumSupportedVersion, currentVersion)) {
|
||||
forceUpdate = true
|
||||
reason = 'minimum-version'
|
||||
} else if (manifest?.blockedVersions?.some((version) => isVersionEqual(currentVersion, version))) {
|
||||
forceUpdate = true
|
||||
reason = 'blocked-version'
|
||||
}
|
||||
|
||||
const finalVersion = latestVersion || manifest?.latestVersion
|
||||
const finalReleaseNotes = releaseNotes || manifest?.releaseNotes || ''
|
||||
|
||||
const info = this.buildInfo({
|
||||
hasUpdate: hasUpdate || forceUpdate,
|
||||
forceUpdate,
|
||||
currentVersion,
|
||||
version: finalVersion,
|
||||
releaseNotes: finalReleaseNotes,
|
||||
title: manifest?.title || (forceUpdate ? '必须更新到最新版本' : undefined),
|
||||
message: manifest?.message,
|
||||
minimumSupportedVersion: manifest?.minimumSupportedVersion,
|
||||
reason,
|
||||
updateSource,
|
||||
policySource
|
||||
})
|
||||
|
||||
this.lastInfo = info
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
export const appUpdateService = new AppUpdateService()
|
||||
@@ -12,6 +12,7 @@
|
||||
"build": "tsc && vite build && electron-builder && node scripts/add-size-to-yml.js",
|
||||
"build:mcp": "tsc && vite build",
|
||||
"build:pro": "node scripts/build-full.js",
|
||||
"build:force-update-manifest": "node scripts/generate-force-update-manifest.js",
|
||||
"mcp": "node scripts/mcp-runner.js",
|
||||
"mcp:probe": "node scripts/mcp-probe.js",
|
||||
"preview": "vite preview",
|
||||
@@ -88,8 +89,9 @@
|
||||
"output": "release"
|
||||
},
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": "https://miyuapp.aiqji.com"
|
||||
"provider": "github",
|
||||
"owner": "ILoveBingLu",
|
||||
"repo": "CipherTalk"
|
||||
},
|
||||
"win": {
|
||||
"icon": "public/xinnian.ico",
|
||||
|
||||
33
scripts/generate-force-update-manifest.js
Normal file
33
scripts/generate-force-update-manifest.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..')
|
||||
const releaseDir = path.join(rootDir, 'release')
|
||||
const pkg = require(path.join(rootDir, 'package.json'))
|
||||
|
||||
const parseList = (value) => {
|
||||
if (!value) return []
|
||||
return String(value)
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
latestVersion: pkg.version,
|
||||
minimumSupportedVersion: process.env.FORCE_UPDATE_MIN_VERSION || undefined,
|
||||
blockedVersions: parseList(process.env.FORCE_UPDATE_BLOCKED_VERSIONS),
|
||||
title: process.env.FORCE_UPDATE_TITLE || '',
|
||||
message: process.env.FORCE_UPDATE_MESSAGE || '',
|
||||
releaseNotes: process.env.FORCE_UPDATE_RELEASE_NOTES || '',
|
||||
publishedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
if (!fs.existsSync(releaseDir)) {
|
||||
fs.mkdirSync(releaseDir, { recursive: true })
|
||||
}
|
||||
|
||||
const outputPath = path.join(releaseDir, 'force-update.json')
|
||||
fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
||||
console.log(`✅ force-update.json 已生成: ${outputPath}`)
|
||||
114
src/App.scss
114
src/App.scss
@@ -88,6 +88,120 @@
|
||||
}
|
||||
}
|
||||
|
||||
.force-update-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 5000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background: rgba(6, 10, 16, 0.82);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.force-update-card {
|
||||
width: min(720px, 100%);
|
||||
max-height: 85vh;
|
||||
overflow: auto;
|
||||
padding: 28px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: var(--bg-secondary);
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
|
||||
|
||||
h2 {
|
||||
margin: 16px 0 10px;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.force-update-desc {
|
||||
margin: 0 0 18px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.force-update-meta {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 18px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.force-update-notes {
|
||||
margin-bottom: 18px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
background: var(--bg-primary);
|
||||
|
||||
.force-update-notes-title {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: var(--text-secondary);
|
||||
font-family: inherit;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.force-update-progress {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.force-update-progress-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.force-update-progress-bar {
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.force-update-progress-fill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #ff7849 0%, #ff4747 100%);
|
||||
}
|
||||
|
||||
.force-update-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.force-update-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 92, 92, 0.12);
|
||||
color: #ff5c5c;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
// 独立聊天窗口容器
|
||||
.chat-window-container {
|
||||
height: 100vh;
|
||||
|
||||
81
src/App.tsx
81
src/App.tsx
@@ -38,6 +38,21 @@ import { useAuthStore } from './stores/authStore'
|
||||
import { X, Shield, Loader2 } from 'lucide-react'
|
||||
import './App.scss'
|
||||
|
||||
type AppUpdateInfo = {
|
||||
hasUpdate: boolean
|
||||
forceUpdate: boolean
|
||||
currentVersion: string
|
||||
version?: string
|
||||
releaseNotes?: string
|
||||
title?: string
|
||||
message?: string
|
||||
minimumSupportedVersion?: string
|
||||
reason?: 'minimum-version' | 'blocked-version'
|
||||
checkedAt: number
|
||||
updateSource: 'github' | 'custom' | 'none'
|
||||
policySource: 'github' | 'custom' | 'none'
|
||||
}
|
||||
|
||||
function App() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
@@ -55,7 +70,7 @@ function App() {
|
||||
const [showActivation, setShowActivation] = useState(false)
|
||||
|
||||
// 更新提示状态
|
||||
const [updateInfo, setUpdateInfo] = useState<{ version: string; releaseNotes: string } | null>(null)
|
||||
const [updateInfo, setUpdateInfo] = useState<AppUpdateInfo | null>(null)
|
||||
const [downloadProgress, setDownloadProgress] = useState<number | null>(null)
|
||||
|
||||
// 加载主题配置
|
||||
@@ -136,6 +151,15 @@ function App() {
|
||||
|
||||
// 监听启动时的更新通知
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
window.electronAPI.app.getUpdateState?.().then((info) => {
|
||||
if (mounted && info?.hasUpdate) {
|
||||
setUpdateInfo(info)
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('获取更新状态失败:', error)
|
||||
})
|
||||
|
||||
const removeUpdateListener = window.electronAPI.app.onUpdateAvailable?.((info) => {
|
||||
setUpdateInfo(info)
|
||||
})
|
||||
@@ -162,6 +186,7 @@ function App() {
|
||||
})
|
||||
|
||||
return () => {
|
||||
mounted = false
|
||||
removeUpdateListener?.()
|
||||
removeSessionsListener?.()
|
||||
removeUpdateAvailableListener?.()
|
||||
@@ -179,6 +204,7 @@ function App() {
|
||||
}, [])
|
||||
|
||||
const dismissUpdate = () => {
|
||||
if (updateInfo?.forceUpdate) return
|
||||
setUpdateInfo(null)
|
||||
}
|
||||
|
||||
@@ -466,12 +492,13 @@ function App() {
|
||||
return (
|
||||
<div className="app-container">
|
||||
<TitleBar />
|
||||
{updateInfo && (
|
||||
{updateInfo && !updateInfo.forceUpdate && (
|
||||
<div className="update-toast">
|
||||
<div className="update-toast-icon">🎉</div>
|
||||
<div className="update-toast-content">
|
||||
<div className="update-toast-title">发现新版本</div>
|
||||
<div className="update-toast-version">v{updateInfo.version} 已发布</div>
|
||||
<div className="update-toast-version">更新源:{updateInfo.updateSource === 'github' ? 'GitHub Release' : '未知'}</div>
|
||||
</div>
|
||||
<button className="update-toast-btn" onClick={() => {
|
||||
window.electronAPI.app.downloadAndInstall()
|
||||
@@ -484,6 +511,56 @@ function App() {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{updateInfo?.forceUpdate && (
|
||||
<div className="force-update-overlay">
|
||||
<div className="force-update-card">
|
||||
<div className="force-update-badge">
|
||||
<Shield size={18} />
|
||||
<span>强制更新</span>
|
||||
</div>
|
||||
<h2>{updateInfo.title || '必须更新后才能继续使用'}</h2>
|
||||
<p className="force-update-desc">
|
||||
{updateInfo.message || '当前版本已被标记为需要立即升级,应用将限制继续使用,直到安装最新版本。'}
|
||||
</p>
|
||||
|
||||
<div className="force-update-meta">
|
||||
<div>当前版本:v{updateInfo.currentVersion}</div>
|
||||
{updateInfo.version && <div>目标版本:v{updateInfo.version}</div>}
|
||||
{updateInfo.minimumSupportedVersion && <div>最低安全版本:v{updateInfo.minimumSupportedVersion}</div>}
|
||||
<div>更新来源:{updateInfo.updateSource === 'github' ? 'GitHub Release' : '未检测到普通更新源'}</div>
|
||||
<div>策略来源:{updateInfo.policySource === 'github' ? 'GitHub 策略源' : updateInfo.policySource === 'custom' ? '自定义策略源' : '无'}</div>
|
||||
</div>
|
||||
|
||||
{updateInfo.releaseNotes && (
|
||||
<div className="force-update-notes">
|
||||
<div className="force-update-notes-title">更新说明</div>
|
||||
<pre>{updateInfo.releaseNotes}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{downloadProgress !== null && (
|
||||
<div className="force-update-progress">
|
||||
<div className="force-update-progress-label">
|
||||
<Loader2 size={16} className="spin" />
|
||||
<span>正在下载更新... {downloadProgress.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="force-update-progress-bar">
|
||||
<div className="force-update-progress-fill" style={{ width: `${downloadProgress}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="force-update-actions">
|
||||
<button className="btn btn-primary" onClick={() => window.electronAPI.app.downloadAndInstall()}>
|
||||
立即更新
|
||||
</button>
|
||||
<button className="btn btn-secondary" onClick={() => window.electronAPI.window.close()}>
|
||||
退出应用
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -98,7 +98,30 @@ function SettingsPage() {
|
||||
const [isDownloading, setIsDownloading] = useState(false)
|
||||
const [downloadProgress, setDownloadProgress] = useState(0)
|
||||
const [appVersion, setAppVersion] = useState('')
|
||||
const [updateInfo, setUpdateInfo] = useState<{ hasUpdate: boolean; version?: string; releaseNotes?: string } | null>(null)
|
||||
const [updateInfo, setUpdateInfo] = useState<{
|
||||
hasUpdate: boolean
|
||||
forceUpdate: boolean
|
||||
currentVersion: string
|
||||
version?: string
|
||||
releaseNotes?: string
|
||||
title?: string
|
||||
message?: string
|
||||
minimumSupportedVersion?: string
|
||||
reason?: 'minimum-version' | 'blocked-version'
|
||||
checkedAt: number
|
||||
updateSource: 'github' | 'custom' | 'none'
|
||||
policySource: 'github' | 'custom' | 'none'
|
||||
} | null>(null)
|
||||
const [updateSourceInfo, setUpdateSourceInfo] = useState<{
|
||||
primaryUpdateSource: 'github'
|
||||
githubRepository: {
|
||||
owner: string
|
||||
repo: string
|
||||
}
|
||||
policySources: Array<'github' | 'custom'>
|
||||
policyPrecedence: 'github'
|
||||
forceUpdatePolicyFallbackUrl: string
|
||||
} | null>(null)
|
||||
const [keyStatus, setKeyStatus] = useState('')
|
||||
const [message, setMessage] = useState<{ text: string; success: boolean } | null>(null)
|
||||
const [showDecryptKey, setShowDecryptKey] = useState(false)
|
||||
@@ -466,7 +489,7 @@ function SettingsPage() {
|
||||
const result = await window.electronAPI.app.checkForUpdates()
|
||||
if (result.hasUpdate) {
|
||||
setUpdateInfo(result)
|
||||
showMessage(`发现新版本 ${result.version}`, true)
|
||||
showMessage(result.forceUpdate ? `检测到强制更新 ${result.version}` : `发现新版本 ${result.version}`, true)
|
||||
} else {
|
||||
showMessage('当前已是最新版本', true)
|
||||
}
|
||||
@@ -2646,6 +2669,14 @@ function SettingsPage() {
|
||||
}
|
||||
}, [location.state])
|
||||
|
||||
useEffect(() => {
|
||||
window.electronAPI.app.getUpdateSourceInfo?.().then((info) => {
|
||||
setUpdateSourceInfo(info)
|
||||
}).catch((error) => {
|
||||
console.error('获取更新源信息失败:', error)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const renderAboutTab = () => (
|
||||
<div className="tab-content about-tab">
|
||||
<div className="about-card">
|
||||
@@ -2657,9 +2688,24 @@ function SettingsPage() {
|
||||
<p className="about-version">v{appVersion || '...'}</p>
|
||||
|
||||
<div className="about-update">
|
||||
{updateSourceInfo && (
|
||||
<div className="update-hint" style={{ marginBottom: '10px' }}>
|
||||
主更新源:GitHub Release ({updateSourceInfo.githubRepository.owner}/{updateSourceInfo.githubRepository.repo})<br />
|
||||
策略补充源:{updateSourceInfo.forceUpdatePolicyFallbackUrl}
|
||||
</div>
|
||||
)}
|
||||
{updateInfo?.hasUpdate ? (
|
||||
<>
|
||||
<p className="update-hint">新版本 v{updateInfo.version} 可用</p>
|
||||
<p className="update-hint">
|
||||
{updateInfo.forceUpdate ? '检测到强制更新' : `新版本 v${updateInfo.version} 可用`}
|
||||
</p>
|
||||
<p className="update-hint">
|
||||
更新来源:{updateInfo.updateSource === 'github' ? 'GitHub Release' : '未知'} / 策略来源:
|
||||
{updateInfo.policySource === 'github' ? 'GitHub' : updateInfo.policySource === 'custom' ? '自定义源' : '无'}
|
||||
</p>
|
||||
{updateInfo.forceUpdate && updateInfo.minimumSupportedVersion && (
|
||||
<p className="update-hint">最低安全版本:v{updateInfo.minimumSupportedVersion}</p>
|
||||
)}
|
||||
{isDownloading ? (
|
||||
<div className="download-progress">
|
||||
<div className="progress-bar">
|
||||
|
||||
54
src/types/electron.d.ts
vendored
54
src/types/electron.d.ts
vendored
@@ -81,12 +81,62 @@ export interface ElectronAPI {
|
||||
cwd: string
|
||||
mode: 'dev' | 'packaged'
|
||||
} | null>
|
||||
checkForUpdates: () => Promise<{ hasUpdate: boolean; version?: string; releaseNotes?: string }>
|
||||
getUpdateState: () => Promise<{
|
||||
hasUpdate: boolean
|
||||
forceUpdate: boolean
|
||||
currentVersion: string
|
||||
version?: string
|
||||
releaseNotes?: string
|
||||
title?: string
|
||||
message?: string
|
||||
minimumSupportedVersion?: string
|
||||
reason?: 'minimum-version' | 'blocked-version'
|
||||
checkedAt: number
|
||||
updateSource: 'github' | 'custom' | 'none'
|
||||
policySource: 'github' | 'custom' | 'none'
|
||||
} | null>
|
||||
getUpdateSourceInfo: () => Promise<{
|
||||
primaryUpdateSource: 'github'
|
||||
githubRepository: {
|
||||
owner: string
|
||||
repo: string
|
||||
}
|
||||
policySources: Array<'github' | 'custom'>
|
||||
policyPrecedence: 'github'
|
||||
forceUpdatePolicyFallbackUrl: string
|
||||
}>
|
||||
checkForUpdates: () => Promise<{
|
||||
hasUpdate: boolean
|
||||
forceUpdate: boolean
|
||||
currentVersion: string
|
||||
version?: string
|
||||
releaseNotes?: string
|
||||
title?: string
|
||||
message?: string
|
||||
minimumSupportedVersion?: string
|
||||
reason?: 'minimum-version' | 'blocked-version'
|
||||
checkedAt: number
|
||||
updateSource: 'github' | 'custom' | 'none'
|
||||
policySource: 'github' | 'custom' | 'none'
|
||||
}>
|
||||
downloadAndInstall: () => Promise<void>
|
||||
getStartupDbConnected?: () => Promise<boolean>
|
||||
setAppIcon: (iconName: string) => Promise<void>
|
||||
onDownloadProgress: (callback: (progress: number) => void) => () => void
|
||||
onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => () => void
|
||||
onUpdateAvailable: (callback: (info: {
|
||||
hasUpdate: boolean
|
||||
forceUpdate: boolean
|
||||
currentVersion: string
|
||||
version?: string
|
||||
releaseNotes?: string
|
||||
title?: string
|
||||
message?: string
|
||||
minimumSupportedVersion?: string
|
||||
reason?: 'minimum-version' | 'blocked-version'
|
||||
checkedAt: number
|
||||
updateSource: 'github' | 'custom' | 'none'
|
||||
policySource: 'github' | 'custom' | 'none'
|
||||
}) => void) => () => void
|
||||
}
|
||||
httpApi: {
|
||||
getStatus: () => Promise<{
|
||||
|
||||
Reference in New Issue
Block a user