diff --git a/doc/prd/workbench.md b/doc/prd/workbench.md index caf0b850..1e59e1ba 100644 --- a/doc/prd/workbench.md +++ b/doc/prd/workbench.md @@ -322,7 +322,7 @@ - `WB-COMPOSER-012-AC2` 用户主动点击后才开始下载,下载中显示进度且失败后可重试。 - `WB-COMPOSER-012-AC3` 下载完成后可立即重启或稍后处理,稍后处理时入口保留为“重启更新”。 - `WB-COMPOSER-012-AC4` 当前应用检测到本机 SQLite schema version 更高时,不初始化 Workbench 和其他业务服务,只显示独立升级恢复页,并明确展示检测版本与当前支持版本。 - - `WB-COMPOSER-012-AC5` 升级恢复页自动检查更新,按状态提供检查、下载、进度、失败重试和重启安装;始终保留官网中文下载页入口与退出入口,更新失败或不可用时不能绕过进入业务界面。页面不展示底层更新错误,统一使用可理解的中文失败提示。 + - `WB-COMPOSER-012-AC5` 升级恢复页自动检查更新,按状态提供检查、下载、进度、失败重试和重启安装;始终保留官网中文下载页入口与退出入口。点击官网入口必须通过系统默认浏览器打开(恢复模式也要注册 `openExternal`,不能只渲染链接)。更新失败或不可用时不能绕过进入业务界面。页面不展示底层更新错误,统一使用可理解的中文失败提示。恢复页顶部保留 44px 窗口拖动条(Windows 另有独立 36px 原生标题栏,业务内容从其下方开始),按钮和链接使用 `no-drag`。 #### WB-COMPOSER-008 上下文用量提示 diff --git a/doc/test-plan/workbench-test.md b/doc/test-plan/workbench-test.md index 76b2f39f..de4a7b55 100644 --- a/doc/test-plan/workbench-test.md +++ b/doc/test-plan/workbench-test.md @@ -140,8 +140,8 @@ tests/case/workbench/ - 测试类型:`e2e` - What:数据库版本高于当前应用支持版本时的仅更新恢复模式。 - Why:旧应用误开新 schema 数据库时,必须先升级应用,不能初始化业务层继续读写。 -- How:先启动应用创建正式 schema,再关闭应用,在同一隔离 `userData/app.sqlite` 的合法迁移前缀后写入更高版本记录并重启,通过真实主进程版本检查进入恢复页;继续用更新 fixture 验证自动检查、下载、失败和重启安装。 -- Oracle:只显示恢复页及本机数据库版本和当前应用支持版本,Workbench 不存在;恢复页提供下载、重启安装、中文失败提示、官网下载页和退出入口,底层英文错误不直接展示,失败后仍不能进入业务界面。对齐 `WB-COMPOSER-012-AC4`、`WB-COMPOSER-012-AC5`。 +- How:先启动应用创建正式 schema,再关闭应用,在同一隔离 `userData/app.sqlite` 的合法迁移前缀后写入更高版本记录并重启,通过真实主进程版本检查进入恢复页;继续用更新 fixture 验证自动检查、下载、失败和重启安装。点击「前往官网下载」后除记录 renderer 调用外,还等待 `openExternalResult` 并断言 IPC 返回成功。 +- Oracle:只显示恢复页及本机数据库版本和当前应用支持版本,Workbench 不存在;恢复页提供下载、重启安装、中文失败提示、官网下载页和退出入口,点击官网入口时 `open-external` 必须真正成功;底层英文错误不直接展示,失败后仍不能进入业务界面。对齐 `WB-COMPOSER-012-AC4`、`WB-COMPOSER-012-AC5`。 - Not:启动前数据库 fixture 只制造“未来 schema version”,不绕过或替换生产版本检查;更新 fixture 不证明真实安装包下载、签名、安装成功或安装后数据库兼容。 #### `context-ring-usage` diff --git a/src/main/index.ts b/src/main/index.ts index 869e9b40..50efb72a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -3,7 +3,7 @@ import { join } from 'path' import { fileURLToPath } from 'node:url' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' -import { registerHandlers } from './ipc/handlers' +import { registerHandlers, registerOpenExternalHandler } from './ipc/handlers' import { IngestionService } from './services/ingestion/loader' import { DEFAULT_EMBEDDING_MODEL_NAME, EmbeddingService } from './services/vector/EmbeddingService' import { EmbeddingRuntimeClient } from './services/vector/runtime/EmbeddingRuntimeClient' @@ -288,6 +288,7 @@ if (!singleInstanceLock) { error, context: { event: 'database-version-too-new' } }) + registerOpenExternalHandler() createWindow() appUpdateService.checkForUpdatesOnLaunch() app.on('activate', () => { diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index 9d5ccc4e..8ed5cf18 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -195,6 +195,21 @@ const normalizeReferenceKindFilters = ( return normalized.length > 0 ? new Set(normalized) : null } +export function registerOpenExternalHandler(): void { + ipcMain.handle('open-external', async (_event, url: string) => { + try { + const parsed = new URL(String(url)) + if (!['http:', 'https:'].includes(parsed.protocol)) { + return { success: false, error: '仅支持 http/https URL' } + } + await shell.openExternal(parsed.toString()) + return { success: true } + } catch (error) { + return { success: false, error: getErrorMessage(error) } + } + }) +} + export function registerHandlers(services: { ingestionService: IngestionService embeddingService: EmbeddingService @@ -1495,18 +1510,7 @@ export function registerHandlers(services: { } ) - ipcMain.handle('open-external', async (_event, url: string) => { - try { - const parsed = new URL(String(url)) - if (!['http:', 'https:'].includes(parsed.protocol)) { - return { success: false, error: '仅支持 http/https URL' } - } - await shell.openExternal(parsed.toString()) - return { success: true } - } catch (error) { - return { success: false, error: getErrorMessage(error) } - } - }) + registerOpenExternalHandler() ipcMain.handle( 'file-watch-start', diff --git a/src/preload/index.ts b/src/preload/index.ts index cfe73817..6c7cd1e6 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -109,9 +109,23 @@ const api: DesktopApi = { ipcRenderer.invoke('ingest-web', payload), ingestWebs: (payload): Promise<{ success: boolean; created?: number; error?: string }> => ipcRenderer.invoke('ingest-webs', payload), - openExternal: (url: string): Promise<{ success: boolean; error?: string }> => { + openExternal: async (url: string): Promise<{ success: boolean; error?: string }> => { emitE2EIpcCall('openExternal', String(url || '').trim()) - return ipcRenderer.invoke('open-external', url) + try { + const response = (await ipcRenderer.invoke('open-external', url)) as { + success: boolean + error?: string + } + emitE2EIpcCall('openExternalResult', response) + return response + } catch (error) { + const response = { + success: false, + error: error instanceof Error ? error.message : String(error) + } + emitE2EIpcCall('openExternalResult', response) + return response + } }, openPath: (filePath: string): Promise<{ success: boolean; error?: string }> => { const safePath = String(filePath || '').trim() diff --git a/src/renderer/src/assets/base.css b/src/renderer/src/assets/base.css index fdca8d5a..e231d019 100644 --- a/src/renderer/src/assets/base.css +++ b/src/renderer/src/assets/base.css @@ -911,7 +911,8 @@ textarea, position: relative; } -.nc-settings-main-titlebar-drag { +.nc-settings-main-titlebar-drag, +.nc-update-required-titlebar-drag { position: absolute; top: 0; right: 0; @@ -922,6 +923,11 @@ textarea, -webkit-app-region: drag; } +.nc-update-required-page button, +.nc-update-required-page a { + -webkit-app-region: no-drag; +} + .nc-settings-header { position: relative; z-index: 4; diff --git a/src/renderer/src/pages/UpdateRequiredPage.tsx b/src/renderer/src/pages/UpdateRequiredPage.tsx index aaf6e047..c476fc4b 100644 --- a/src/renderer/src/pages/UpdateRequiredPage.tsx +++ b/src/renderer/src/pages/UpdateRequiredPage.tsx @@ -43,9 +43,14 @@ const UpdateRequiredPage: React.FC<{ startupStatus: UpdateRequiredStatus }> = ({ return (
+