mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
Online upgrades can strand users on stale desktop UI state or malformed local persistence. This adds a deny-by-default Doctor path that resets only regenerable desktop UI state, reports protected local files with redacted metadata, and keeps protected repair as a dry-run no-op until a reviewed backup-first flow exists. Constraint: Chat transcripts, model/provider config, Skills, MCP, IM bindings, adapter sessions, OAuth tokens, plugins, and team/session records are user-owned protected state. Rejected: Automatically rewrite malformed protected JSON | unsafe without schema-specific migrations and backups. Rejected: Continue relying only on startup migrations | users need an explicit recovery action after a white screen. Confidence: high Scope-risk: moderate Directive: Keep Doctor repair deny-by-default; do not mutate protected state without an explicit reviewed backup-first manual repair flow. Tested: bun test src/server/__tests__/doctor-service.test.ts Tested: cd desktop && bun run test src/components/ErrorBoundary.test.tsx src/lib/doctorRepair.test.ts src/__tests__/diagnosticsSettings.test.tsx src/components/layout/StartupErrorView.test.tsx Tested: cd desktop && bun run lint Tested: bun run check:server Tested: bun run verify (failed only existing agent-utils coverage baseline; changed-lines coverage 97.62%) Not-tested: Packaged desktop manual Doctor click path.
83 lines
2.0 KiB
TypeScript
83 lines
2.0 KiB
TypeScript
import { doctorApi, type DoctorReportRepairResponse } from '../api/doctor'
|
|
import { DESKTOP_PERSISTENCE_VERSION_KEY } from './persistenceMigrations'
|
|
|
|
export const SAFE_DOCTOR_STORAGE_KEYS = [
|
|
'cc-haha-open-tabs',
|
|
'cc-haha-session-runtime',
|
|
'cc-haha-theme',
|
|
'cc-haha-locale',
|
|
DESKTOP_PERSISTENCE_VERSION_KEY,
|
|
] as const
|
|
|
|
type DoctorStorage = Pick<Storage, 'getItem' | 'removeItem'>
|
|
|
|
export type LocalDoctorRepairResult = {
|
|
removedKeys: string[]
|
|
missingKeys: string[]
|
|
failedKeys: string[]
|
|
}
|
|
|
|
export type DoctorRepairResult = {
|
|
local: LocalDoctorRepairResult
|
|
server: DoctorReportRepairResponse | null
|
|
serverError: string | null
|
|
}
|
|
|
|
function getDefaultDoctorStorage(): DoctorStorage | null {
|
|
try {
|
|
return globalThis.localStorage ?? null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function runLocalDoctorRepair(storage: DoctorStorage | null = getDefaultDoctorStorage()): LocalDoctorRepairResult {
|
|
if (!storage) {
|
|
return {
|
|
removedKeys: [],
|
|
missingKeys: [...SAFE_DOCTOR_STORAGE_KEYS],
|
|
failedKeys: [],
|
|
}
|
|
}
|
|
|
|
const removedKeys: string[] = []
|
|
const missingKeys: string[] = []
|
|
const failedKeys: string[] = []
|
|
|
|
for (const key of SAFE_DOCTOR_STORAGE_KEYS) {
|
|
try {
|
|
if (storage.getItem(key) === null) {
|
|
missingKeys.push(key)
|
|
continue
|
|
}
|
|
storage.removeItem(key)
|
|
removedKeys.push(key)
|
|
} catch {
|
|
failedKeys.push(key)
|
|
}
|
|
}
|
|
|
|
return { removedKeys, missingKeys, failedKeys }
|
|
}
|
|
|
|
export async function runDoctorRepair(options?: {
|
|
includeServer?: boolean
|
|
storage?: DoctorStorage | null
|
|
}): Promise<DoctorRepairResult> {
|
|
const local = runLocalDoctorRepair(options?.storage)
|
|
if (options?.includeServer === false) {
|
|
return { local, server: null, serverError: null }
|
|
}
|
|
|
|
try {
|
|
const server = await doctorApi.reportAndRepair()
|
|
return { local, server, serverError: null }
|
|
} catch (error) {
|
|
return {
|
|
local,
|
|
server: null,
|
|
serverError: error instanceof Error ? error.message : String(error),
|
|
}
|
|
}
|
|
}
|