feat: add trusted mirror update source
Version checks and downloads previously depended on GitHub Release. About & Updates now offers GitHub or a validated mirror source, keeps the selector beneath the startup-check switch, and disables it when startup checks are off. The website resolves platform downloads from a bounded OSS release index with a GitHub fallback. Tagged releases publish and verify immutable OSS assets through OIDC before switching the latest-version index; deployment requires the configured Alibaba Cloud environment variables and role. Release note: “关于与更新”新增 GitHub 与镜像节点选择,启动检查、手动检查和下载页使用同一可信来源;官网下载也可按系统、架构和安装包类型直接选择。
This commit is contained in:
@@ -1082,6 +1082,7 @@ describe('App', () => {
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github' as const,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
@@ -1173,12 +1174,14 @@ describe('App', () => {
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
updateSource: 'github' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
updateSource: 'github' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
@@ -1271,12 +1274,14 @@ describe('App', () => {
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
updateSource: 'github' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
updateSource: 'github' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
@@ -7062,12 +7067,14 @@ describe('App', () => {
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github' as const,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
@@ -7107,12 +7114,14 @@ describe('App', () => {
|
||||
api.updates = {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github' as const,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github' as const,
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
@@ -7144,6 +7153,7 @@ describe('App', () => {
|
||||
it('keeps platform-feature switches in Settings without navigating', async () => {
|
||||
let applicationSettings: ApplicationSettings = {
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
|
||||
@@ -168,6 +168,7 @@ const onAnalysisEvent = vi.fn<
|
||||
})
|
||||
const getApplicationSettings = vi.fn<() => Promise<ApplicationSettings>>(async () => ({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
@@ -178,6 +179,7 @@ beforeEach(() => {
|
||||
analysisEventListener = undefined
|
||||
getApplicationSettings.mockResolvedValue({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
@@ -677,6 +679,7 @@ describe('MagicNotesWorkspace', () => {
|
||||
it('reuses the AI comments pane for selected todos', async () => {
|
||||
getApplicationSettings.mockResolvedValue({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'after-save-manual',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
@@ -707,6 +710,7 @@ describe('MagicNotesWorkspace', () => {
|
||||
it('streams with snapshotted sidebar options while later changes stay local', async () => {
|
||||
getApplicationSettings.mockResolvedValue({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'after-save-manual',
|
||||
magicNoteCommentFormat: 'narrative'
|
||||
@@ -820,6 +824,7 @@ describe('MagicNotesWorkspace', () => {
|
||||
it('automatically comments on a newly saved record in auto mode', async () => {
|
||||
getApplicationSettings.mockResolvedValue({
|
||||
checkUpdatesOnStartup: false,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'after-save-auto',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
|
||||
@@ -400,6 +400,7 @@ const diagnoseEmbedding = vi.fn(
|
||||
)
|
||||
let applicationSettings: ApplicationSettings = {
|
||||
checkUpdatesOnStartup: true,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
@@ -565,6 +566,7 @@ describe('SettingsPanel runtime files', () => {
|
||||
await changeUiLocale('zh-CN')
|
||||
applicationSettings = {
|
||||
checkUpdatesOnStartup: true,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: false,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
waitFor
|
||||
} from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { ApplicationSettings } from '../../shared/application-settings-contracts'
|
||||
import type { DesktopApi } from '../../shared/contracts'
|
||||
import { UpdateSettingsSection } from './UpdateSettingsSection'
|
||||
|
||||
@@ -16,17 +17,22 @@ afterEach(() => {
|
||||
|
||||
describe('UpdateSettingsSection', () => {
|
||||
it('checks the official release manifest and updates the startup preference', async () => {
|
||||
let applicationSettings: ApplicationSettings = {
|
||||
checkUpdatesOnStartup: true,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate' as const,
|
||||
magicNoteCommentFormat: 'combined' as const
|
||||
}
|
||||
const updateSettings = vi.fn<
|
||||
NonNullable<DesktopApi['updates']>['updateSettings']
|
||||
>(async (input) => ({
|
||||
checkUpdatesOnStartup:
|
||||
input.checkUpdatesOnStartup ?? true,
|
||||
magicNotesEnabled: input.magicNotesEnabled ?? true,
|
||||
magicNoteCommentMode:
|
||||
input.magicNoteCommentMode ?? 'immediate',
|
||||
magicNoteCommentFormat:
|
||||
input.magicNoteCommentFormat ?? 'combined'
|
||||
}))
|
||||
>(async (input) => {
|
||||
applicationSettings = {
|
||||
...applicationSettings,
|
||||
...input
|
||||
}
|
||||
return applicationSettings
|
||||
})
|
||||
const check = vi.fn<
|
||||
NonNullable<DesktopApi['updates']>['check']
|
||||
>(async () => ({
|
||||
@@ -62,10 +68,7 @@ describe('UpdateSettingsSection', () => {
|
||||
},
|
||||
updates: {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
...applicationSettings
|
||||
})),
|
||||
updateSettings,
|
||||
check,
|
||||
@@ -80,12 +83,36 @@ describe('UpdateSettingsSection', () => {
|
||||
name: '启动时检查新版本'
|
||||
})
|
||||
expect(startup).toBeChecked()
|
||||
const source = screen.getByRole('combobox', {
|
||||
name: '检查更新源'
|
||||
})
|
||||
const startupRow = startup.closest('label')
|
||||
const sourceRow = source.closest('label')
|
||||
expect(source).toHaveValue('github')
|
||||
expect(sourceRow).toHaveClass('update-settings__source')
|
||||
expect(
|
||||
startupRow!.compareDocumentPosition(sourceRow!) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
).toBeTruthy()
|
||||
expect(source).toBeEnabled()
|
||||
fireEvent.change(source, { target: { value: 'mirror' } })
|
||||
await waitFor(() =>
|
||||
expect(updateSettings).toHaveBeenCalledWith({
|
||||
updateSource: 'mirror'
|
||||
})
|
||||
)
|
||||
expect(source).toHaveValue('mirror')
|
||||
expect(
|
||||
screen.getByRole('option', { name: '镜像节点' })
|
||||
).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(startup)
|
||||
await waitFor(() =>
|
||||
expect(updateSettings).toHaveBeenCalledWith({
|
||||
checkUpdatesOnStartup: false
|
||||
})
|
||||
)
|
||||
expect(source).toBeDisabled()
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '立即检查更新' })
|
||||
@@ -113,12 +140,14 @@ describe('UpdateSettingsSection', () => {
|
||||
updates: {
|
||||
getSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
})),
|
||||
updateSettings: vi.fn(async () => ({
|
||||
checkUpdatesOnStartup: true,
|
||||
updateSource: 'github',
|
||||
magicNotesEnabled: true,
|
||||
magicNoteCommentMode: 'immediate',
|
||||
magicNoteCommentFormat: 'combined'
|
||||
@@ -141,7 +170,7 @@ describe('UpdateSettingsSection', () => {
|
||||
|
||||
const alert = await screen.findByRole('alert')
|
||||
expect(alert).toHaveTextContent(
|
||||
'版本检查失败:无法连接 GoodBuddy 官方 GitHub Release,请检查网络或代理后重试'
|
||||
'版本检查失败:无法连接更新源“GitHub”,请检查网络或代理后重试'
|
||||
)
|
||||
expect(alert).not.toHaveTextContent('Error invoking remote method')
|
||||
})
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type {
|
||||
ApplicationSettings,
|
||||
UpdateSource,
|
||||
VersionCheckResult
|
||||
} from '../../shared/application-settings-contracts'
|
||||
import type { AppInfo } from '../../shared/contracts'
|
||||
@@ -110,6 +111,34 @@ export function UpdateSettingsSection(): React.JSX.Element {
|
||||
}
|
||||
}
|
||||
|
||||
const changeUpdateSource = async (
|
||||
updateSource: UpdateSource
|
||||
): Promise<void> => {
|
||||
const updates = window.goodbuddy.updates
|
||||
if (!updates || !settings) {
|
||||
return
|
||||
}
|
||||
setSaving(true)
|
||||
setError(undefined)
|
||||
try {
|
||||
setSettings(await updates.updateSettings({ updateSource }))
|
||||
setResult(undefined)
|
||||
} catch (reason) {
|
||||
const fallback = t('updates.errors.saveSourceFailed')
|
||||
setError(
|
||||
updateErrorMessage(
|
||||
reason,
|
||||
fallback,
|
||||
t('updates.errors.network', {
|
||||
fallback
|
||||
})
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const check = async (): Promise<void> => {
|
||||
const updates = window.goodbuddy.updates
|
||||
if (!updates) {
|
||||
@@ -125,7 +154,12 @@ export function UpdateSettingsSection(): React.JSX.Element {
|
||||
updateErrorMessage(
|
||||
reason,
|
||||
fallback,
|
||||
t('updates.errors.network', { fallback })
|
||||
t('updates.errors.sourceNetwork', {
|
||||
fallback,
|
||||
source: t(
|
||||
`updates.source.names.${settings?.updateSource ?? 'github'}`
|
||||
)
|
||||
})
|
||||
)
|
||||
)
|
||||
} finally {
|
||||
@@ -171,6 +205,33 @@ export function UpdateSettingsSection(): React.JSX.Element {
|
||||
<span>{t('updates.checkOnStartup')}</span>
|
||||
</label>
|
||||
|
||||
<label className="field update-settings__source">
|
||||
<span>{t('updates.source.label')}</span>
|
||||
<select
|
||||
aria-label={t('updates.source.label')}
|
||||
disabled={
|
||||
!settings ||
|
||||
!settings.checkUpdatesOnStartup ||
|
||||
saving ||
|
||||
checking
|
||||
}
|
||||
onChange={(event) =>
|
||||
void changeUpdateSource(
|
||||
event.target.value as UpdateSource
|
||||
)
|
||||
}
|
||||
value={settings?.updateSource ?? 'github'}
|
||||
>
|
||||
<option value="github">
|
||||
{t('updates.source.options.github')}
|
||||
</option>
|
||||
<option value="mirror">
|
||||
{t('updates.source.options.mirror')}
|
||||
</option>
|
||||
</select>
|
||||
<small>{t('updates.source.description')}</small>
|
||||
</label>
|
||||
|
||||
<div className="update-settings__actions">
|
||||
<button
|
||||
className="secondary-button"
|
||||
|
||||
@@ -84,7 +84,7 @@ export const settings = {
|
||||
label: 'About and updates',
|
||||
navigationDescription: 'Version checks and downloads',
|
||||
description:
|
||||
'Checks only the official GoodBuddy GitHub Release and never installs automatically'
|
||||
'Checks the selected official update source and never installs automatically'
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
||||
@@ -285,16 +285,31 @@ export const settingsSections = {
|
||||
'Version checks are not available in this version',
|
||||
readSettingsFailed: 'Could not load application settings',
|
||||
saveSettingsFailed: 'Could not save update settings',
|
||||
saveSourceFailed: 'Could not save the update source',
|
||||
checkFailed: 'Version check failed',
|
||||
network:
|
||||
'{{fallback}}: Could not connect to the official GoodBuddy GitHub Release. Check your network or proxy and try again.'
|
||||
network: '{{fallback}}. Check the system status and try again.',
|
||||
sourceNetwork:
|
||||
'{{fallback}}: Could not connect to update source "{{source}}". Check your network or proxy and try again.'
|
||||
},
|
||||
loadingAppInfo: 'Loading application information…',
|
||||
source: {
|
||||
label: 'Update source',
|
||||
description:
|
||||
'Used for manual checks, startup checks, and the download page.',
|
||||
options: {
|
||||
github: 'GitHub (default)',
|
||||
mirror: 'Mirror node'
|
||||
},
|
||||
names: {
|
||||
github: 'GitHub',
|
||||
mirror: 'Mirror node'
|
||||
}
|
||||
},
|
||||
checkOnStartup: 'Check for updates at startup',
|
||||
actions: {
|
||||
checking: 'Checking…',
|
||||
checkNow: 'Check for updates now',
|
||||
openDownloadPage: 'Open official download page'
|
||||
openDownloadPage: 'Open download page'
|
||||
},
|
||||
result: {
|
||||
available: 'New version {{version}} is available',
|
||||
|
||||
@@ -71,7 +71,7 @@ export const settings = {
|
||||
about: {
|
||||
label: '关于与更新',
|
||||
navigationDescription: '版本检查与下载页',
|
||||
description: '只检查 GoodBuddy 官方 GitHub Release,不自动下载安装'
|
||||
description: '检查所选官方更新源,不自动下载安装'
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
|
||||
@@ -266,16 +266,30 @@ export const settingsSections = {
|
||||
serviceUnavailable: '当前版本未提供版本检查服务',
|
||||
readSettingsFailed: '读取应用设置失败',
|
||||
saveSettingsFailed: '保存更新设置失败',
|
||||
saveSourceFailed: '保存检查更新源失败',
|
||||
checkFailed: '版本检查失败',
|
||||
network:
|
||||
'{{fallback}}:无法连接 GoodBuddy 官方 GitHub Release,请检查网络或代理后重试'
|
||||
network: '{{fallback}}:请检查系统状态后重试',
|
||||
sourceNetwork:
|
||||
'{{fallback}}:无法连接更新源“{{source}}”,请检查网络或代理后重试'
|
||||
},
|
||||
loadingAppInfo: '正在读取应用信息…',
|
||||
source: {
|
||||
label: '检查更新源',
|
||||
description: '用于手动检查、启动时检查和打开下载页。',
|
||||
options: {
|
||||
github: 'GitHub(默认)',
|
||||
mirror: '镜像节点'
|
||||
},
|
||||
names: {
|
||||
github: 'GitHub',
|
||||
mirror: '镜像节点'
|
||||
}
|
||||
},
|
||||
checkOnStartup: '启动时检查新版本',
|
||||
actions: {
|
||||
checking: '正在检查…',
|
||||
checkNow: '立即检查更新',
|
||||
openDownloadPage: '打开官方下载页'
|
||||
openDownloadPage: '打开下载页'
|
||||
},
|
||||
result: {
|
||||
available: '发现新版本 {{version}}',
|
||||
|
||||
@@ -6297,6 +6297,39 @@ details.settings-section > :not(summary) + :not(summary) {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.field.update-settings__source {
|
||||
display: grid;
|
||||
min-height: 36px;
|
||||
align-items: center;
|
||||
grid-template-columns: max-content minmax(160px, 220px) minmax(0, 1fr);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.update-settings__source > span {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-caption);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.update-settings__source > select {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.update-settings__source > select:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.update-settings__source:has(select:disabled) > span,
|
||||
.update-settings__source:has(select:disabled) > small {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.update-settings__source > small {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.update-settings__actions button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -6359,6 +6392,12 @@ details.settings-section > :not(summary) + :not(summary) {
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.field.update-settings__source {
|
||||
align-items: stretch;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.speech-model-settings .settings-section__title--actions {
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user