docs(claude): restructure documentation management and add feedback system
- Restructure §8 from "文档沉淀规则" to "文档管理规则" with 4 subsections - Add docs/ structure with features/ and knowledge-base/ directories - Add feature documentation template with 7 sections (概述/设计初衷/技术设计/预期作用/实际效果/演化路线/头脑风暴) - Add feature update trigger matrix (新增/修改/完成/问题/反馈) - Add documentation quality checklist - Add §16
This commit is contained in:
373
desktop/src/lib/error-handling.ts
Normal file
373
desktop/src/lib/error-handling.ts
Normal file
@@ -0,0 +1,373 @@
|
||||
/**
|
||||
* ZCLAW Error Handling Utilities
|
||||
*
|
||||
* Centralized error reporting, notification, and tracking system.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import {
|
||||
AppError,
|
||||
classifyError,
|
||||
ErrorCategory,
|
||||
ErrorSeverity,
|
||||
} from './error-types';
|
||||
|
||||
// === Error Store ===
|
||||
|
||||
interface StoredError extends AppError {
|
||||
dismissed: boolean;
|
||||
reported: boolean;
|
||||
}
|
||||
|
||||
interface ErrorStore {
|
||||
errors: StoredError[];
|
||||
addError: (error: AppError) => void;
|
||||
dismissError: (id: string) => void;
|
||||
dismissAll: () => void;
|
||||
markReported: (id: string) => void;
|
||||
getUndismissedErrors: () => StoredError[];
|
||||
getErrorCount: () => number;
|
||||
getErrorsByCategory: (category: ErrorCategory) => StoredError[];
|
||||
getErrorsBySeverity: (severity: ErrorSeverity) => StoredError[];
|
||||
}
|
||||
|
||||
// === Global Error Store ===
|
||||
|
||||
let errorStore: ErrorStore = {
|
||||
errors: [],
|
||||
addError: () => {},
|
||||
dismissError: () => {},
|
||||
dismissAll: () => {},
|
||||
markReported: () => {},
|
||||
getUndismissedErrors: () => [],
|
||||
getErrorCount: () => 0,
|
||||
getErrorsByCategory: () => [],
|
||||
getErrorsBySeverity: () => [],
|
||||
};
|
||||
|
||||
// === Initialize Store ===
|
||||
|
||||
function initErrorStore(): void {
|
||||
errorStore = {
|
||||
errors: [],
|
||||
|
||||
addError: (error: AppError) => {
|
||||
errorStore.errors = [error, ...errorStore.errors];
|
||||
// Notify listeners
|
||||
notifyErrorListeners(error);
|
||||
},
|
||||
|
||||
dismissError: (id: string) => void {
|
||||
const error = errorStore.errors.find(e => e.id === id);
|
||||
if (error) {
|
||||
errorStore.errors = errorStore.errors.map(e =>
|
||||
e.id === id ? { ...e, dismissed: true } : e
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
dismissAll: () => void {
|
||||
errorStore.errors = errorStore.errors.map(e => ({ ...e, dismissed: true }));
|
||||
},
|
||||
|
||||
markReported: (id: string) => void {
|
||||
const error = errorStore.errors.find(e => e.id === id);
|
||||
if (error) {
|
||||
errorStore.errors = errorStore.errors.map(e =>
|
||||
e.id === id ? { ...e, reported: true } : e
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
getUndismissedErrors: () => StoredError[] => {
|
||||
return errorStore.errors.filter(e => !e.dismissed);
|
||||
},
|
||||
|
||||
getErrorCount: () => number => {
|
||||
return errorStore.errors.filter(e => !e.dismissed).length;
|
||||
},
|
||||
|
||||
getErrorsByCategory: (category: ErrorCategory) => StoredError[] => {
|
||||
return errorStore.errors.filter(e => e.category === category && !e.dismissed);
|
||||
},
|
||||
|
||||
getErrorsBySeverity: (severity: ErrorSeverity) => StoredError[] => {
|
||||
return errorStore.errors.filter(e => e.severity === severity && !e.dismissed);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// === Error Listeners ===
|
||||
|
||||
type ErrorListener = (error: AppError) => void;
|
||||
const errorListeners: Set<ErrorListener> = new Set();
|
||||
|
||||
function addErrorListener(listener: ErrorListener): () => void {
|
||||
errorListeners.add(listener);
|
||||
return () => errorListeners.delete(listener);
|
||||
}
|
||||
|
||||
function notifyErrorListeners(error: AppError): void {
|
||||
errorListeners.forEach(listener => {
|
||||
try {
|
||||
listener(error);
|
||||
} catch (e) {
|
||||
console.error('[ErrorHandling] Listener error:', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize on first import
|
||||
initErrorStore();
|
||||
|
||||
// === Public API ===
|
||||
|
||||
/**
|
||||
* Report an error to the centralized error handling system.
|
||||
*/
|
||||
export function reportError(
|
||||
error: unknown,
|
||||
context?: {
|
||||
componentStack?: string;
|
||||
errorName?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
): AppError {
|
||||
const appError = classifyError(error);
|
||||
|
||||
// Add context information if provided
|
||||
if (context) {
|
||||
const technicalDetails = [
|
||||
context.componentStack && `Component Stack:\n${context.componentStack}`,
|
||||
context.errorName && `Error Name: ${context.errorName}`,
|
||||
context.errorMessage && `Error Message: ${context.errorMessage}`,
|
||||
].filter(Boolean).join('\n\n');
|
||||
|
||||
if (technicalDetails) {
|
||||
(appError as { technicalDetails?: string }).technicalDetails = technicalDetails;
|
||||
}
|
||||
}
|
||||
|
||||
errorStore.addError(appError);
|
||||
|
||||
// Log to console in development
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[ErrorHandling] Error reported:', {
|
||||
id: appError.id,
|
||||
category: appError.category,
|
||||
severity: appError.severity,
|
||||
title: appError.title,
|
||||
message: appError.message,
|
||||
});
|
||||
}
|
||||
|
||||
return appError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report an error from an API response.
|
||||
*/
|
||||
export function reportApiError(
|
||||
response: Response,
|
||||
endpoint: string,
|
||||
method: string = 'GET'
|
||||
): AppError {
|
||||
const status = response.status;
|
||||
let category: ErrorCategory = 'server';
|
||||
let severity: ErrorSeverity = 'medium';
|
||||
let title = 'API Error';
|
||||
let message = `Request to ${endpoint} failed with status ${status}`;
|
||||
let recoverySteps: { description: string }[] = [];
|
||||
|
||||
if (status === 401) {
|
||||
category = 'auth';
|
||||
severity = 'high';
|
||||
title = 'Authentication Required';
|
||||
message = 'Your session has expired. Please authenticate again.';
|
||||
recoverySteps = [
|
||||
{ description: 'Click "Reconnect" to authenticate' },
|
||||
{ description: 'Check your API key in settings' },
|
||||
];
|
||||
} else if (status === 403) {
|
||||
category = 'permission';
|
||||
severity = 'medium';
|
||||
title = 'Permission Denied';
|
||||
message = 'You do not have permission to perform this action.';
|
||||
recoverySteps = [
|
||||
{ description: 'Contact your administrator for access' },
|
||||
{ description: 'Check your RBAC configuration' },
|
||||
];
|
||||
} else if (status === 404) {
|
||||
category = 'client';
|
||||
severity = 'low';
|
||||
title = 'Not Found';
|
||||
message = `The requested resource was not found: ${endpoint}`;
|
||||
recoverySteps = [
|
||||
{ description: 'Verify the resource exists' },
|
||||
{ description: 'Check the URL is correct' },
|
||||
];
|
||||
} else if (status === 422) {
|
||||
category = 'validation';
|
||||
severity = 'low';
|
||||
title = 'Validation Error';
|
||||
message = 'The request data is invalid.';
|
||||
recoverySteps = [
|
||||
{ description: 'Check your input data format' },
|
||||
{ description: 'Verify required fields are provided' },
|
||||
];
|
||||
} else if (status === 429) {
|
||||
category = 'client';
|
||||
severity = 'medium';
|
||||
title = 'Rate Limited';
|
||||
message = 'Too many requests. Please wait before trying again.';
|
||||
recoverySteps = [
|
||||
{ description: 'Wait a moment before retrying' },
|
||||
{ description: 'Reduce request frequency' },
|
||||
];
|
||||
} else if (status >= 500) {
|
||||
category = 'server';
|
||||
severity = 'high';
|
||||
title = 'Server Error';
|
||||
message = 'The server encountered an error processing your request.';
|
||||
recoverySteps = [
|
||||
{ description: 'Try again in a few moments' },
|
||||
{ description: 'Contact support if the problem persists' },
|
||||
];
|
||||
}
|
||||
|
||||
const appError: AppError = {
|
||||
id: uuidv4(),
|
||||
category,
|
||||
severity,
|
||||
title,
|
||||
message,
|
||||
technicalDetails: `${method} ${endpoint}\nStatus: ${status}\nResponse: ${response.statusText}`,
|
||||
recoverable: status !== 500 || status < 400,
|
||||
recoverySteps,
|
||||
timestamp: new Date(),
|
||||
originalError: response,
|
||||
};
|
||||
|
||||
errorStore.addError(appError);
|
||||
return appError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a network error.
|
||||
*/
|
||||
export function reportNetworkError(
|
||||
error: Error,
|
||||
url?: string
|
||||
): AppError {
|
||||
return reportError(error, {
|
||||
errorMessage: url ? `URL: ${url}\n${error.message}` : error.message,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Report a WebSocket error.
|
||||
*/
|
||||
export function reportWebSocketError(
|
||||
event: CloseEvent | ErrorEvent,
|
||||
url: string
|
||||
): AppError {
|
||||
const code = 'code' in event ? event.code : 0;
|
||||
const reason = 'reason' in event ? event.reason : 'Unknown';
|
||||
|
||||
return reportError(
|
||||
new Error(`WebSocket error: ${reason} (code: ${code})`),
|
||||
{
|
||||
errorMessage: `WebSocket URL: ${url}\nCode: ${code}\nReason: ${reason}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss an error by ID.
|
||||
*/
|
||||
export function dismissError(id: string): void {
|
||||
errorStore.dismissError(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss all active errors.
|
||||
*/
|
||||
export function dismissAllErrors(): void {
|
||||
errorStore.dismissAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark an error as reported.
|
||||
*/
|
||||
export function markErrorReported(id: string): void {
|
||||
errorStore.markReported(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active (non-dismissed) errors.
|
||||
*/
|
||||
export function getActiveErrors(): StoredError[] {
|
||||
return errorStore.getUndismissedErrors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of active errors.
|
||||
*/
|
||||
export function getActiveErrorCount(): number {
|
||||
return errorStore.getErrorCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get errors filtered by category.
|
||||
*/
|
||||
export function getErrorsByCategory(category: ErrorCategory): StoredError[] {
|
||||
return errorStore.getErrorsByCategory(category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get errors filtered by severity.
|
||||
*/
|
||||
export function getErrorsBySeverity(severity: ErrorSeverity): StoredError[] {
|
||||
return errorStore.getErrorsBySeverity(severity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to error events.
|
||||
*/
|
||||
export function subscribeToErrors(listener: ErrorListener): () => void {
|
||||
return addErrorListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any critical errors.
|
||||
*/
|
||||
export function hasCriticalErrors(): boolean {
|
||||
return errorStore.getErrorsBySeverity('critical').length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are any high severity errors.
|
||||
*/
|
||||
export function hasHighSeverityErrors(): boolean {
|
||||
const highSeverity = ['high', 'critical'];
|
||||
return errorStore.errors.some(e => highSeverity.includes(e.severity) && !e.dismissed);
|
||||
}
|
||||
|
||||
// === Types ===
|
||||
|
||||
interface CloseEvent {
|
||||
code?: number;
|
||||
reason?: string;
|
||||
wasClean?: boolean;
|
||||
}
|
||||
|
||||
interface ErrorEvent {
|
||||
code?: number;
|
||||
reason?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface StoredError extends AppError {
|
||||
dismissed: boolean;
|
||||
reported: boolean;
|
||||
}
|
||||
Reference in New Issue
Block a user