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')
+ })
})