From 1923539a8ec1828f2f0a0aef80d8bba07e08a441 Mon Sep 17 00:00:00 2001 From: yaojin Date: Fri, 21 Aug 2026 01:22:11 -0700 Subject: [PATCH] fix(recovery): restore Reset Harness Data action on plugin recovery view The 'Reset Data' option originally added in #81 was lost when the plugin-recovery view replaced the simple dialog in #94. When a third-party plugin cannot be identified as the cause of a startup failure (e.g. a built-in core plugin reports 'failed to import loader entry ...'), the user is left without a self-service recovery path and must manually delete the harness directory. This change re-exposes 'Reset Harness data' as the primary action when no removable plugin is identified. It backs up the current harness directory, recreates a clean working folder, and retries startup automatically. The action is hidden when a removable third-party plugin is offered instead. Closes #69 --- build/plugin-recovery.html | 8 +++++- src/main/index.ts | 44 +++++++++++++++++++++++++++++-- src/main/plugin-recovery-view.ts | 31 ++++++++++++++++------ test/plugin-recovery-view.test.ts | 22 ++++++++++++++-- 4 files changed, 92 insertions(+), 13 deletions(-) diff --git a/build/plugin-recovery.html b/build/plugin-recovery.html index 09692901..9e6f0578 100644 --- a/build/plugin-recovery.html +++ b/build/plugin-recovery.html @@ -464,7 +464,13 @@

primary.disabled = true primary.textContent = model.primaryBusyLabel restart.disabled = true - navigate(model.canUninstall ? 'uninstall' : 'show-log') + if (model.canUninstall) { + navigate('uninstall') + } else if (model.canResetData) { + navigate('reset-data') + } else { + navigate('show-log') + } }) restart.addEventListener('click', () => { restart.disabled = true diff --git a/src/main/index.ts b/src/main/index.ts index ac510712..fc5c109c 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1,6 +1,7 @@ import { spawn } from 'node:child_process' import { join } from 'node:path' import { appendFileSync, existsSync, readFileSync } from 'node:fs' +import { mkdir, rename } from 'node:fs/promises' import { parse } from 'yaml' import { app, @@ -51,13 +52,14 @@ import { import { buildPluginRecoveryViewModel } from './plugin-recovery-view' import { aboutDetail, bundledHarnessVersion } from './version-info' -type PluginRecoveryAction = 'uninstall' | 'show-log' | 'quit' | 'restart' | 'refresh' +type PluginRecoveryAction = 'uninstall' | 'show-log' | 'quit' | 'restart' | 'refresh' | 'reset-data' const PLUGIN_RECOVERY_ACTIONS = new Set([ 'uninstall', 'show-log', 'quit', - 'restart' + 'restart', + 'reset-data' ]) let mainWindow: BrowserWindow | undefined @@ -637,6 +639,28 @@ function showUnexpectedError(error: unknown): void { dialog.showErrorBox('DSH Desktop encountered an error', message) } +async function resetHarnessData(): Promise { + const harnessHome = join(app.getPath('userData'), 'harness') + if (!existsSync(harnessHome)) { + await mkdir(harnessHome, { recursive: true }) + return true + } + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-') + const backupPath = join(app.getPath('userData'), `harness-backup-${timestamp}`) + + try { + await rename(harnessHome, backupPath) + await mkdir(harnessHome, { recursive: true }) + return true + } catch (error) { + console.warn( + `[plugin-recovery] Failed to reset Harness data: ${error instanceof Error ? error.message : String(error)}` + ) + return false + } +} + async function showPluginRecovery(options?: { message?: string logs?: readonly string[] @@ -749,6 +773,22 @@ async function showPluginRecovery(options?: { return } continue + } else if (action === 'reset-data') { + const resetOk = await resetHarnessData() + if (!resetOk) { + notice = isChinese + ? '无法备份或重置 Harness 数据。请打开 Harness 日志查看详情,或手动重试。' + : 'The Harness data could not be backed up or reset. Open the Harness log for details or try again manually.' + continue + } + pluginRecoveryRemovedPlugins.length = 0 + await launchHarness() + if (applyPendingFrontendEvidence()) continue + if (runtime.snapshot().phase === 'ready') { + schedulePluginRecoverySessionReset() + return + } + continue } else if (action === 'show-log') { shell.showItemInFolder(join(app.getPath('logs'), 'harness.log')) continue diff --git a/src/main/plugin-recovery-view.ts b/src/main/plugin-recovery-view.ts index d2b3bdb6..e67ca5b8 100644 --- a/src/main/plugin-recovery-view.ts +++ b/src/main/plugin-recovery-view.ts @@ -27,6 +27,11 @@ export interface PluginRecoveryViewModel { rawError: string quitLabel: string canUninstall: boolean + canResetData: boolean + resetLabel: string + resetBusyLabel: string + resetDetail: string + resetSafetyNote: string } interface FailureDescription { @@ -167,7 +172,7 @@ export function buildPluginRecoveryViewModel(options: { : 'Harness 暂时无法启动', summary: canUninstall ? '' - : '目前还无法定位到具体插件。请打开 Harness 日志查看详细错误。', + : '目前还无法定位到具体插件。可以重置 Harness 数据(自动备份后重建)后再次启动,或打开日志查看详细错误。', reasonTitle: description.title, reasonDetail: description.detail, plugins, @@ -179,8 +184,8 @@ export function buildPluginRecoveryViewModel(options: { safetyNote: '工作区、会话、模型配置和其他插件不会被删除。', primaryLabel: canUninstall ? multiple ? `卸载这 ${plugins.length} 个插件并继续检测` : '卸载此插件并继续检测' - : '打开 Harness 日志', - primaryBusyLabel: canUninstall ? '正在处理并重新检测…' : '正在打开日志…', + : '重置 Harness 数据', + primaryBusyLabel: canUninstall ? '正在处理并重新检测…' : '正在备份并重置…', restartLabel: '重启 Harness', restartBusyLabel: '正在重启…', logLabel: '打开 Harness 日志', @@ -190,7 +195,12 @@ export function buildPluginRecoveryViewModel(options: { launchDirectory: snapshot.launchDirectory, rawError: snapshot.message, quitLabel: '退出 DSH Desktop', - canUninstall + canUninstall, + canResetData: !canUninstall, + resetLabel: '重置 Harness 数据', + resetBusyLabel: '正在备份并重置…', + resetDetail: '备份当前的 Harness 数据后重新创建一个干净的工作目录,再自动尝试启动。', + resetSafetyNote: '会先备份当前数据再清理;工作区和模型配置会被保留。' } } @@ -203,7 +213,7 @@ export function buildPluginRecoveryViewModel(options: { : 'Harness could not start', summary: canUninstall ? '' - : 'No specific plugin could be identified. Open the Harness log to inspect the detailed error.', + : 'No specific plugin could be identified. Reset Harness data to back up the current directory and recreate a clean working folder, or open the log to inspect the detailed error.', reasonTitle: description.title, reasonDetail: description.detail, plugins, @@ -215,8 +225,8 @@ export function buildPluginRecoveryViewModel(options: { safetyNote: 'Your workspaces, sessions, model settings, and other plugins will not be removed.', primaryLabel: canUninstall ? multiple ? `Remove these ${plugins.length} plugins and continue` : 'Remove this plugin and continue' - : 'Open Harness log', - primaryBusyLabel: canUninstall ? 'Removing and checking again…' : 'Opening log…', + : 'Reset Harness data', + primaryBusyLabel: canUninstall ? 'Removing and checking again…' : 'Backing up and resetting…', restartLabel: 'Restart Harness', restartBusyLabel: 'Restarting…', logLabel: 'Open Harness log', @@ -226,6 +236,11 @@ export function buildPluginRecoveryViewModel(options: { launchDirectory: snapshot.launchDirectory, rawError: snapshot.message, quitLabel: 'Quit DSH Desktop', - canUninstall + canUninstall, + canResetData: !canUninstall, + resetLabel: 'Reset Harness data', + resetBusyLabel: 'Backing up and resetting…', + resetDetail: 'Back up the current Harness data and recreate a clean working folder, then retry startup automatically.', + resetSafetyNote: 'Backs up existing data first; your workspaces and model settings will be preserved.' } } diff --git a/test/plugin-recovery-view.test.ts b/test/plugin-recovery-view.test.ts index de272fd1..572af23f 100644 --- a/test/plugin-recovery-view.test.ts +++ b/test/plugin-recovery-view.test.ts @@ -85,7 +85,7 @@ describe('plugin recovery view model', () => { expect(model.canUninstall).toBe(true) }) - it('falls back to the log when no plugin can be identified', () => { + it('falls back to resetting harness data when no plugin can be identified', () => { const model = buildPluginRecoveryViewModel({ snapshot: failedSnapshot(), plugins: [], @@ -93,8 +93,26 @@ describe('plugin recovery view model', () => { locale: 'en' }) expect(model.canUninstall).toBe(false) - expect(model.primaryLabel).toBe('Open Harness log') + expect(model.canResetData).toBe(true) + expect(model.primaryLabel).toBe('Reset Harness data') + expect(model.primaryBusyLabel).toBe('Backing up and resetting…') + expect(model.resetLabel).toBe('Reset Harness data') + expect(model.resetDetail).toContain('Back up') + expect(model.resetSafetyNote).toContain('Backs up') + expect(model.logLabel).toBe('Open Harness log') expect(model.restartLabel).toBe('Restart Harness') expect(model.restartBusyLabel).toBe('Restarting…') }) + + it('hides the reset option when a removable plugin is identified', () => { + const model = buildPluginRecoveryViewModel({ + snapshot: failedSnapshot(), + plugins: ['some-bad-plugin'], + removedPlugins: [], + locale: 'en' + }) + expect(model.canUninstall).toBe(true) + expect(model.canResetData).toBe(false) + expect(model.primaryLabel).toBe('Remove this plugin and continue') + }) })