diff --git a/.github/workflows/dev-daily-fixed.yml b/.github/workflows/dev-daily-fixed.yml new file mode 100644 index 0000000..92419cf --- /dev/null +++ b/.github/workflows/dev-daily-fixed.yml @@ -0,0 +1,208 @@ +name: Dev Daily (Fixed Release) + +on: + schedule: + # GitHub Actions schedule uses UTC. 16:00 UTC = 北京时间次日 00:00 + - cron: "0 16 * * *" + workflow_dispatch: + +permissions: + contents: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + FIXED_DEV_TAG: nightly-dev + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + dev_version: ${{ steps.meta.outputs.dev_version }} + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: "npm" + + - name: Generate daily dev version + id: meta + shell: bash + run: | + set -euo pipefail + BASE_VERSION="$(node -p "require('./package.json').version.split('-')[0]")" + YEAR_2="$(TZ=Asia/Shanghai date +%y)" + MONTH="$(TZ=Asia/Shanghai date +%-m)" + DAY="$(TZ=Asia/Shanghai date +%-d)" + DEV_VERSION="${BASE_VERSION}-dev.${YEAR_2}.${MONTH}.${DAY}" + echo "dev_version=$DEV_VERSION" >> "$GITHUB_OUTPUT" + echo "Dev version: $DEV_VERSION" + + - name: Ensure fixed prerelease exists + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + if gh release view "$FIXED_DEV_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + gh release edit "$FIXED_DEV_TAG" --repo "$GITHUB_REPOSITORY" --title "Daily Dev Build" --prerelease + else + gh release create "$FIXED_DEV_TAG" --repo "$GITHUB_REPOSITORY" --title "Daily Dev Build" --notes "固定开发版发布页(每日覆盖更新)" --prerelease + fi + + dev-mac-arm64: + needs: prepare + runs-on: macos-14 + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: "npm" + + - name: Install Dependencies + run: npm install + + - name: Set dev version + shell: bash + run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version + + - name: Build Frontend & Type Check + run: | + npx tsc + npx vite build + + - name: Package macOS arm64 dev artifacts + run: | + npx electron-builder --mac dmg --arm64 --publish never '--config.publish.channel=dev' '--config.artifactName=${productName}-dev-arm64.${ext}' + + - name: Upload macOS arm64 assets to fixed release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh release upload "$FIXED_DEV_TAG" release/* --repo "$GITHUB_REPOSITORY" --clobber + + dev-linux: + needs: prepare + runs-on: ubuntu-latest + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: "npm" + + - name: Install Dependencies + run: npm install + + - name: Set dev version + shell: bash + run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version + + - name: Build Frontend & Type Check + run: | + npx tsc + npx vite build + + - name: Package Linux dev artifacts + run: | + npx electron-builder --linux --publish never '--config.publish.channel=dev' '--config.artifactName=${productName}-dev-linux.${ext}' + + - name: Upload Linux assets to fixed release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh release upload "$FIXED_DEV_TAG" release/* --repo "$GITHUB_REPOSITORY" --clobber + + dev-win-x64: + needs: prepare + runs-on: windows-latest + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: "npm" + + - name: Install Dependencies + run: npm install + + - name: Set dev version + shell: bash + run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version + + - name: Build Frontend & Type Check + run: | + npx tsc + npx vite build + + - name: Package Windows x64 dev artifacts + run: | + npx electron-builder --win nsis --x64 --publish never '--config.publish.channel=dev' '--config.artifactName=${productName}-dev-x64-Setup.${ext}' + + - name: Upload Windows x64 assets to fixed release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh release upload "$FIXED_DEV_TAG" release/* --repo "$GITHUB_REPOSITORY" --clobber + + dev-win-arm64: + needs: prepare + runs-on: windows-latest + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: "npm" + + - name: Install Dependencies + run: npm install + + - name: Set dev version + shell: bash + run: npm version "${{ needs.prepare.outputs.dev_version }}" --no-git-tag-version --allow-same-version + + - name: Build Frontend & Type Check + run: | + npx tsc + npx vite build + + - name: Package Windows arm64 dev artifacts + run: | + npx electron-builder --win nsis --arm64 --publish never '--config.publish.channel=dev-arm64' '--config.artifactName=${productName}-dev-arm64-Setup.${ext}' + + - name: Upload Windows arm64 assets to fixed release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + gh release upload "$FIXED_DEV_TAG" release/* --repo "$GITHUB_REPOSITORY" --clobber diff --git a/.github/workflows/preview-nightly-main.yml b/.github/workflows/preview-nightly-main.yml new file mode 100644 index 0000000..3f7f1de --- /dev/null +++ b/.github/workflows/preview-nightly-main.yml @@ -0,0 +1,197 @@ +name: Preview Nightly (Main) + +on: + schedule: + # GitHub Actions schedule uses UTC. 16:00 UTC = 北京时间次日 00:00 + - cron: "0 16 * * *" + workflow_dispatch: + +permissions: + contents: write + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + should_build: ${{ steps.meta.outputs.should_build }} + preview_version: ${{ steps.meta.outputs.preview_version }} + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: "npm" + + - name: Decide whether to build and generate preview version + id: meta + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: bash + run: | + set -euo pipefail + + git fetch origin main --depth=1 + COMMITS_24H="$(git rev-list --count --since='24 hours ago' origin/main)" + + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + SHOULD_BUILD=true + elif [ "$COMMITS_24H" -gt 0 ]; then + SHOULD_BUILD=true + else + SHOULD_BUILD=false + fi + + BASE_VERSION="$(node -p "require('./package.json').version.split('-')[0]")" + YEAR_2="$(TZ=Asia/Shanghai date +%y)" + EXISTING_COUNT="$(gh api --paginate "repos/${GITHUB_REPOSITORY}/releases" --jq "[.[].tag_name | select(test(\"^v${BASE_VERSION}-preview\\.${YEAR_2}\\.[0-9]+$\"))] | length")" + NEXT_COUNT=$((EXISTING_COUNT + 1)) + PREVIEW_VERSION="${BASE_VERSION}-preview.${YEAR_2}.${NEXT_COUNT}" + + echo "should_build=$SHOULD_BUILD" >> "$GITHUB_OUTPUT" + echo "preview_version=$PREVIEW_VERSION" >> "$GITHUB_OUTPUT" + echo "Preview version: $PREVIEW_VERSION (commits in last 24h on main: $COMMITS_24H)" + + preview-mac-arm64: + needs: prepare + if: needs.prepare.outputs.should_build == 'true' + runs-on: macos-14 + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: "npm" + + - name: Install Dependencies + run: npm install + + - name: Set preview version + shell: bash + run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version + + - name: Build Frontend & Type Check + run: | + npx tsc + npx vite build + + - name: Package and Publish macOS arm64 preview + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_IDENTITY_AUTO_DISCOVERY: "false" + run: | + npx electron-builder --mac dmg --arm64 --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview' + + preview-linux: + needs: prepare + if: needs.prepare.outputs.should_build == 'true' + runs-on: ubuntu-latest + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: "npm" + + - name: Install Dependencies + run: npm install + + - name: Set preview version + shell: bash + run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version + + - name: Build Frontend & Type Check + run: | + npx tsc + npx vite build + + - name: Package and Publish Linux preview + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npx electron-builder --linux --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview' + + preview-win-x64: + needs: prepare + if: needs.prepare.outputs.should_build == 'true' + runs-on: windows-latest + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: "npm" + + - name: Install Dependencies + run: npm install + + - name: Set preview version + shell: bash + run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version + + - name: Build Frontend & Type Check + run: | + npx tsc + npx vite build + + - name: Package and Publish Windows x64 preview + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npx electron-builder --win nsis --x64 --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview' '--config.artifactName=${productName}-${version}-x64-Setup.${ext}' + + preview-win-arm64: + needs: prepare + if: needs.prepare.outputs.should_build == 'true' + runs-on: windows-latest + steps: + - name: Check out git repository + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Install Node.js + uses: actions/setup-node@v5 + with: + node-version: 24 + cache: "npm" + + - name: Install Dependencies + run: npm install + + - name: Set preview version + shell: bash + run: npm version "${{ needs.prepare.outputs.preview_version }}" --no-git-tag-version --allow-same-version + + - name: Build Frontend & Type Check + run: | + npx tsc + npx vite build + + - name: Package and Publish Windows arm64 preview + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npx electron-builder --win nsis --arm64 --publish always '--config.publish.releaseType=prerelease' '--config.publish.channel=preview-arm64' '--config.artifactName=${productName}-${version}-arm64-Setup.${ext}' diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 82c328c..01f45a0 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -4,6 +4,8 @@ on: schedule: - cron: '0 2 * * *' # 每天 UTC 02:00(北京时间 10:00) workflow_dispatch: # 支持手动触发 + pull_request: # 监控 PR + branches: [ main, dev ] permissions: contents: read diff --git a/electron/main.ts b/electron/main.ts index bf22d19..57be647 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -36,10 +36,41 @@ import { messagePushService } from './services/messagePushService' autoUpdater.autoDownload = false autoUpdater.autoInstallOnAppQuit = true autoUpdater.disableDifferentialDownload = true // 禁用差分更新,强制全量下载 -// Windows x64 与 arm64 使用不同更新通道,避免 latest.yml 互相覆盖导致下错架构安装包。 -if (process.platform === 'win32' && process.arch === 'arm64') { - autoUpdater.channel = 'latest-arm64' +// 更新通道策略: +// - 稳定版(如 4.3.0)默认走 latest +// - 预览版(如 4.3.0-preview.26.1)默认走 preview +// - 开发版(如 4.3.0-dev.26.3.4)默认走 dev +// - 用户可在设置页切换稳定/预览/开发,切换后即时生效 +// 同时区分 Windows x64 / arm64,避免更新清单互相覆盖。 +const appVersion = app.getVersion() +const defaultUpdateTrack: 'stable' | 'preview' | 'dev' = (() => { + if (/-preview\.\d+\.\d+$/i.test(appVersion)) return 'preview' + if (/-dev\.\d+\.\d+\.\d+$/i.test(appVersion)) return 'dev' + if (/(alpha|beta|rc)/i.test(appVersion)) return 'dev' + return 'stable' +})() +const isPrereleaseBuild = defaultUpdateTrack !== 'stable' +let configService: ConfigService | null = null + +const normalizeUpdateTrack = (raw: unknown): 'stable' | 'preview' | 'dev' | null => { + if (raw === 'stable' || raw === 'preview' || raw === 'dev') return raw + return null } + +const applyAutoUpdateChannel = (reason: 'startup' | 'settings' = 'startup') => { + const configuredTrack = normalizeUpdateTrack(configService?.get('updateChannel')) + const track: 'stable' | 'preview' | 'dev' = configuredTrack || defaultUpdateTrack + const baseUpdateChannel = track === 'stable' ? 'latest' : track + autoUpdater.allowPrerelease = track !== 'stable' + autoUpdater.allowDowngrade = isPrereleaseBuild && track === 'stable' + autoUpdater.channel = + process.platform === 'win32' && process.arch === 'arm64' + ? `${baseUpdateChannel}-arm64` + : baseUpdateChannel + console.log(`[Update](${reason}) 当前版本 ${appVersion},渠道偏好: ${track},更新通道: ${autoUpdater.channel}`) +} + +applyAutoUpdateChannel('startup') const AUTO_UPDATE_ENABLED = process.env.AUTO_UPDATE_ENABLED === 'true' || process.env.AUTO_UPDATE_ENABLED === '1' || @@ -87,7 +118,6 @@ function sanitizePathEnv() { sanitizePathEnv() // 单例服务 -let configService: ConfigService | null = null // 协议窗口实例 let agreementWindow: BrowserWindow | null = null @@ -1117,6 +1147,9 @@ function registerIpcHandlers() { ipcMain.handle('config:set', async (_, key: string, value: any) => { const result = configService?.set(key as any, value) + if (key === 'updateChannel') { + applyAutoUpdateChannel('settings') + } void messagePushService.handleConfigChanged(key) return result }) @@ -2741,6 +2774,7 @@ app.whenReady().then(async () => { // 初始化配置服务 updateSplashProgress(5, '正在加载配置...') configService = new ConfigService() + applyAutoUpdateChannel('startup') // 将用户主题配置推送给 Splash 窗口 if (splashWindow && !splashWindow.isDestroyed()) { diff --git a/electron/services/config.ts b/electron/services/config.ts index 5fdd609..3269b0b 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -45,6 +45,7 @@ interface ConfigSchema { // 更新相关 ignoredUpdateVersion: string + updateChannel: 'auto' | 'stable' | 'preview' | 'dev' // 通知 notificationEnabled: boolean @@ -119,6 +120,7 @@ export class ConfigService { authUseHello: false, authHelloSecret: '', ignoredUpdateVersion: '', + updateChannel: 'auto', notificationEnabled: true, notificationPosition: 'top-right', notificationFilterMode: 'all', diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index 5862037..8ebad97 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -1401,6 +1401,220 @@ } } +// 版本更新页面 +.updates-tab { + display: flex; + flex-direction: column; + gap: 16px; +} + +.updates-hero { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 20px 22px; + border-radius: 18px; + border: 1px solid color-mix(in srgb, var(--primary) 18%, var(--border-color)); + background: + linear-gradient(110deg, color-mix(in srgb, var(--bg-primary) 98%, #ffffff 2%), color-mix(in srgb, var(--bg-primary) 97%, var(--primary) 3%)); + box-shadow: 0 8px 22px color-mix(in srgb, var(--primary) 8%, transparent); +} + +.updates-hero-main { + min-width: 0; + + h2 { + margin: 10px 0 6px; + font-size: 30px; + line-height: 1; + letter-spacing: -0.03em; + color: var(--text-primary); + } + + p { + margin: 0; + font-size: 13px; + color: var(--text-secondary); + } +} + +.updates-chip { + display: inline-flex; + align-items: center; + height: 24px; + padding: 0 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + color: var(--primary); + background: color-mix(in srgb, var(--primary) 12%, transparent); +} + +.updates-hero-action { + display: flex; + flex-direction: column; + gap: 8px; + align-items: flex-end; +} + +.updates-card { + padding: 18px; + border-radius: 16px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--bg-primary) 96%, var(--bg-secondary)); +} + +.updates-progress-card { + padding: 16px 18px; + border-radius: 16px; + border: 1px solid color-mix(in srgb, var(--primary) 34%, var(--border-color)); + background: color-mix(in srgb, var(--bg-primary) 95%, var(--primary) 4%); +} + +.updates-progress-header { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 10px; + margin-bottom: 10px; + + h3 { + margin: 0; + font-size: 15px; + color: var(--text-primary); + } + + strong { + color: var(--primary); + font-size: 14px; + font-variant-numeric: tabular-nums; + } + + span { + font-size: 12px; + color: var(--text-secondary); + } +} + +.updates-card-header { + margin-bottom: 12px; + + h3 { + margin: 0 0 4px; + font-size: 16px; + color: var(--text-primary); + } + + span { + font-size: 12px; + color: var(--text-tertiary); + } +} + +.update-channel-grid { + display: grid; + gap: 10px; +} + +.update-channel-card { + width: 100%; + text-align: left; + border: 1px solid var(--border-color); + border-radius: 14px; + padding: 12px 14px; + background: var(--bg-primary); + color: var(--text-primary); + transition: border-color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease; + cursor: pointer; + + &:hover:not(:disabled) { + border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color)); + transform: translateY(-1px); + box-shadow: 0 8px 24px color-mix(in srgb, var(--primary) 12%, transparent); + } + + &:disabled { + cursor: default; + } + + &.active { + border-color: color-mix(in srgb, var(--primary) 60%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 10%, var(--bg-primary)); + } +} + +.update-channel-title-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 6px; + + .title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + } + + svg { + color: var(--primary); + } +} + +.update-channel-card .desc { + display: block; + font-size: 12px; + color: var(--text-secondary); +} + +.updates-progress-track { + height: 10px; + border-radius: 999px; + background: var(--bg-tertiary); + overflow: hidden; +} + +.updates-progress-fill { + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--primary), color-mix(in srgb, var(--primary) 72%, #ffffff)); + transition: width 0.3s ease; + position: relative; + + &::after { + content: ''; + position: absolute; + inset: 0; + background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.26), transparent); + animation: updatesShimmer 1.7s linear infinite; + } +} + +.updates-ignore-btn { + margin-top: 12px; +} + +@media (max-width: 920px) { + .updates-hero { + flex-direction: column; + align-items: flex-start; + } + + .updates-hero-action { + align-items: flex-start; + } +} + +@keyframes updatesShimmer { + from { + transform: translateX(-100%); + } + + to { + transform: translateX(100%); + } +} + // 关于页面 .about-tab { diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 36e1776..55c5779 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -15,7 +15,7 @@ import { import { Avatar } from '../components/Avatar' import './SettingsPage.scss' -type SettingsTab = 'appearance' | 'notification' | 'database' | 'models' | 'cache' | 'api' | 'security' | 'about' | 'analytics' +type SettingsTab = 'appearance' | 'notification' | 'database' | 'models' | 'cache' | 'api' | 'updates' | 'security' | 'about' | 'analytics' const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [ { id: 'appearance', label: '外观', icon: Palette }, @@ -24,9 +24,9 @@ const tabs: { id: SettingsTab; label: string; icon: React.ElementType }[] = [ { id: 'models', label: '模型管理', icon: Mic }, { id: 'cache', label: '缓存', icon: HardDrive }, { id: 'api', label: 'API 服务', icon: Globe }, - { id: 'analytics', label: '分析', icon: BarChart2 }, { id: 'security', label: '安全', icon: ShieldCheck }, + { id: 'updates', label: '版本更新', icon: RefreshCw }, { id: 'about', label: '关于', icon: Info } ] @@ -68,7 +68,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setDownloadProgress, showUpdateDialog, setShowUpdateDialog, - setUpdateError } = useAppStore() const resetChatStore = useChatStore((state) => state.reset) @@ -141,6 +140,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const [notificationFilterList, setNotificationFilterList] = useState([]) const [windowCloseBehavior, setWindowCloseBehavior] = useState('ask') const [quoteLayout, setQuoteLayout] = useState('quote-top') + const [updateChannel, setUpdateChannel] = useState('stable') const [filterSearchKeyword, setFilterSearchKeyword] = useState('') const [filterModeDropdownOpen, setFilterModeDropdownOpen] = useState(false) const [positionDropdownOpen, setPositionDropdownOpen] = useState(false) @@ -339,6 +339,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const savedMessagePushEnabled = await configService.getMessagePushEnabled() const savedWindowCloseBehavior = await configService.getWindowCloseBehavior() const savedQuoteLayout = await configService.getQuoteLayout() + const savedUpdateChannel = await configService.getUpdateChannel() const savedAuthEnabled = await window.electronAPI.auth.verifyEnabled() const savedAuthUseHello = await configService.getAuthUseHello() @@ -387,6 +388,18 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { setMessagePushEnabled(savedMessagePushEnabled) setWindowCloseBehavior(savedWindowCloseBehavior) setQuoteLayout(savedQuoteLayout) + if (savedUpdateChannel) { + setUpdateChannel(savedUpdateChannel) + } else { + const currentVersion = await window.electronAPI.app.getVersion() + if (/-preview\.\d+\.\d+$/i.test(currentVersion)) { + setUpdateChannel('preview') + } else if (/-dev\.\d+\.\d+\.\d+$/i.test(currentVersion) || /(alpha|beta|rc)/i.test(currentVersion)) { + setUpdateChannel('dev') + } else { + setUpdateChannel('stable') + } + } const savedExcludeWords = await configService.getWordCloudExcludeWords() setWordCloudExcludeWords(savedExcludeWords) @@ -512,7 +525,22 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { } } + const handleUpdateChannelChange = async (channel: configService.UpdateChannel) => { + if (channel === updateChannel) return + try { + setUpdateChannel(channel) + await configService.setUpdateChannel(channel) + await configService.setIgnoredUpdateVersion('') + setUpdateInfo(null) + setShowUpdateDialog(false) + const channelLabel = channel === 'stable' ? '稳定版' : channel === 'preview' ? '预览版' : '开发版' + showMessage(`已切换到${channelLabel}更新渠道,正在检查更新`, true) + await handleCheckUpdate() + } catch (e: any) { + showMessage(`切换更新渠道失败: ${e}`, false) + } + } const showMessage = (text: string, success: boolean) => { setMessage({ text, success }) @@ -1474,6 +1502,16 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const renderDatabaseTab = () => (
+
+ + 检测当前数据库配置是否可用 + +
+ +
+
64位十六进制密钥 @@ -2400,35 +2438,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { WeFlow

WeFlow

-

WeFlow

v{appVersion || '...'}

- -
- {updateInfo?.hasUpdate ? ( - <> -

新版 v{updateInfo.version} 可用

- {isDownloading ? ( -
-
-
-
- {(downloadProgress?.percent || 0).toFixed(0)}% -
- ) : ( - - )} - - ) : ( -
- -
- )} -
@@ -2440,7 +2450,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { · { e.preventDefault(); window.electronAPI.window.openAgreementWindow() }}>用户协议
-

© 2025 WeFlow. All rights reserved.

+

© 2026 WeFlow. All rights reserved.

匿名数据收集 @@ -2463,6 +2473,82 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
) + const renderUpdatesTab = () => { + const downloadPercent = Math.max(0, Math.min(100, Number(downloadProgress?.percent || 0))) + const channelCards: { id: configService.UpdateChannel; title: string; desc: string }[] = [ + { id: 'stable', title: '稳定版', desc: '正式发布的版本,适合日常使用' }, + { id: 'preview', title: '预览版', desc: '正式发布前的预览体验版本' }, + { id: 'dev', title: '开发版', desc: '即刻体验我们的屎山代码' } + ] + + return ( +
+
+
+ 当前版本 +

{appVersion || '...'}

+

{updateInfo?.hasUpdate ? `发现新版本 v${updateInfo.version}` : '当前已是最新版本,可手动检查更新'}

+
+
+ {updateInfo?.hasUpdate ? ( + + ) : ( + + )} +
+
+ + {(isDownloading || updateInfo?.hasUpdate) && ( +
+
+

{isDownloading ? `正在下载 v${updateInfo?.version || ''}` : `新版本 v${updateInfo?.version} 已就绪`}

+ {isDownloading ? {downloadPercent.toFixed(0)}% : 可立即安装} +
+
+
+
+ {updateInfo?.hasUpdate && !isDownloading && ( + + )} +
+ )} + +
+
+

更新渠道

+ 切换渠道后会自动重新检查 +
+
+ {channelCards.map((channel) => { + const active = updateChannel === channel.id + return ( + + ) + })} +
+
+
+ ) + } + return (
event.stopPropagation()}> @@ -2510,9 +2596,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {

设置

- {onClose && (