mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
Diagnostics exports previously preserved only compact summaries, which made user-uploaded issue bundles hard to debug. This records richer sanitized details from server, CLI, SDK, browser, React, and desktop API failure paths while keeping request bodies and secrets out of client-side reports. Constraint: Diagnostic bundles must be useful for GitHub issues without including chat contents, file contents, full environments, or API keys. Rejected: Only expand the Settings UI summary | the exported bundle would still miss hidden runtime failures. Confidence: high Scope-risk: moderate Directive: Keep diagnostics write paths best-effort and non-blocking; do not add request-body capture without a redaction review. Tested: bun test src/server/__tests__/diagnostics-service.test.ts src/server/__tests__/conversation-service.test.ts Tested: cd desktop && bun run test src/api/client.test.ts src/__tests__/diagnosticsSettings.test.tsx --run Tested: bun run check:server Tested: bun run check:desktop Tested: agent-browser E2E on ports 37652/41752 captured client_unhandled_rejection and exported a redacted tar.gz bundle
85 lines
2.9 KiB
TypeScript
85 lines
2.9 KiB
TypeScript
/**
|
|
* Diagnostics REST API
|
|
*
|
|
* GET /api/diagnostics/status — log directory, retention and counters
|
|
* GET /api/diagnostics/events — recent sanitized diagnostic events
|
|
* POST /api/diagnostics/events — append a sanitized client diagnostic event
|
|
* POST /api/diagnostics/export — write a sanitized tar.gz bundle
|
|
* POST /api/diagnostics/open-log-dir — open the diagnostics directory
|
|
* DELETE /api/diagnostics — clear diagnostics files
|
|
*/
|
|
|
|
import { diagnosticsService } from '../services/diagnosticsService.js'
|
|
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
|
|
|
export async function handleDiagnosticsApi(
|
|
req: Request,
|
|
url: URL,
|
|
segments: string[],
|
|
): Promise<Response> {
|
|
try {
|
|
const action = segments[2]
|
|
|
|
if (!action && req.method === 'DELETE') {
|
|
await diagnosticsService.clear()
|
|
return Response.json({ ok: true })
|
|
}
|
|
|
|
if (action === 'status' && req.method === 'GET') {
|
|
return Response.json(await diagnosticsService.getStatus())
|
|
}
|
|
|
|
if (action === 'events' && req.method === 'GET') {
|
|
const limit = Number.parseInt(url.searchParams.get('limit') || '100', 10)
|
|
const events = await diagnosticsService.readRecentEvents(Number.isFinite(limit) ? limit : 100)
|
|
return Response.json({ events })
|
|
}
|
|
|
|
if (action === 'events' && req.method === 'POST') {
|
|
const body = await parseJsonBody(req)
|
|
const type = typeof body.type === 'string' && body.type.trim()
|
|
? body.type.trim().slice(0, 128)
|
|
: 'client_diagnostic_event'
|
|
const severity = isDiagnosticSeverity(body.severity) ? body.severity : 'error'
|
|
const summary = typeof body.summary === 'string' && body.summary.trim()
|
|
? body.summary
|
|
: type
|
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId : undefined
|
|
await diagnosticsService.recordEvent({
|
|
type,
|
|
severity,
|
|
summary,
|
|
sessionId,
|
|
details: body.details,
|
|
})
|
|
return Response.json({ ok: true })
|
|
}
|
|
|
|
if (action === 'export' && req.method === 'POST') {
|
|
return Response.json({ bundle: await diagnosticsService.exportBundle() })
|
|
}
|
|
|
|
if (action === 'open-log-dir' && req.method === 'POST') {
|
|
await diagnosticsService.openLogDir()
|
|
return Response.json({ ok: true })
|
|
}
|
|
|
|
throw new ApiError(404, `Unknown diagnostics endpoint: ${action ?? '(root)'}`, 'NOT_FOUND')
|
|
} catch (error) {
|
|
return errorResponse(error)
|
|
}
|
|
}
|
|
|
|
async function parseJsonBody(req: Request): Promise<Record<string, unknown>> {
|
|
try {
|
|
const body = await req.json()
|
|
return body && typeof body === 'object' ? body as Record<string, unknown> : {}
|
|
} catch {
|
|
throw ApiError.badRequest('Invalid JSON body')
|
|
}
|
|
}
|
|
|
|
function isDiagnosticSeverity(value: unknown): value is 'debug' | 'info' | 'warn' | 'error' {
|
|
return value === 'debug' || value === 'info' || value === 'warn' || value === 'error'
|
|
}
|