mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33: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.
58 lines
1.8 KiB
TypeScript
58 lines
1.8 KiB
TypeScript
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
|
import { DoctorService } from '../services/doctorService.js'
|
|
|
|
export async function handleDoctorApi(
|
|
req: Request,
|
|
url: URL,
|
|
segments: string[],
|
|
): Promise<Response> {
|
|
try {
|
|
const sub = segments[2]
|
|
|
|
if ((req.method === 'GET' && !sub) || (req.method === 'GET' && sub === 'report')) {
|
|
const cwd = url.searchParams.get('cwd') || undefined
|
|
const service = new DoctorService({ projectRoot: cwd })
|
|
return Response.json({ report: await service.getReport() })
|
|
}
|
|
|
|
if (req.method === 'POST' && sub === 'repair') {
|
|
const body = await parseJsonBody(req)
|
|
const cwd = typeof body.cwd === 'string' ? body.cwd : url.searchParams.get('cwd') || undefined
|
|
const targetIds = Array.isArray(body.targetIds)
|
|
? body.targetIds.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
: undefined
|
|
const service = new DoctorService({ projectRoot: cwd })
|
|
return Response.json({ result: await service.repair(targetIds) })
|
|
}
|
|
|
|
if (!sub) {
|
|
throw methodNotAllowed(req.method, '/api/doctor')
|
|
}
|
|
|
|
throw ApiError.notFound(`Unknown doctor endpoint: ${sub}`)
|
|
} catch (error) {
|
|
return errorResponse(error)
|
|
}
|
|
}
|
|
|
|
async function parseJsonBody(req: Request): Promise<Record<string, unknown>> {
|
|
if (
|
|
!req.headers.get('content-length') &&
|
|
!req.headers.get('transfer-encoding') &&
|
|
!req.headers.get('content-type')
|
|
) {
|
|
return {}
|
|
}
|
|
|
|
try {
|
|
const body = await req.json()
|
|
return body && typeof body === 'object' ? body as Record<string, unknown> : {}
|
|
} catch {
|
|
throw ApiError.badRequest('Invalid JSON body')
|
|
}
|
|
}
|
|
|
|
function methodNotAllowed(method: string, route: string): ApiError {
|
|
return new ApiError(405, `Method ${method} not allowed on ${route}`, 'METHOD_NOT_ALLOWED')
|
|
}
|