feat(types): 错误体系重构 — ErrorKind + error code + Serialize
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

Rust (crates/zclaw-types/src/error.rs):
- 新增 ErrorKind enum (17 种) + Serde Serialize/Deserialize
- 新增 error_codes 模块 (稳定错误码 E4040-E5110)
- ZclawError 新增 kind() / code() 方法
- 新增 ErrorDetail struct + Serialize impl
- 保留所有现有变体和构造器 (零破坏性)
- 新增 12 个测试: kind 映射 + code 稳定性 + JSON 序列化

TypeScript (desktop/src/lib/error-types.ts):
- 新增 RustErrorKind / RustErrorDetail 类型定义
- 新增 tryParseRustError() 结构化错误解析
- 新增 classifyRustError() 按 ErrorKind 分类
- classifyError() 优先解析结构化错误,fallback 字符串匹配
- 17 种 ErrorKind → 中文标题映射

验证: cargo check ✓ | tsc ✓ | 62 zclaw-types tests ✓
This commit is contained in:
iven
2026-04-17 19:38:19 +08:00
parent 0754ea19c2
commit f9290ea683
2 changed files with 416 additions and 3 deletions

View File

@@ -45,6 +45,193 @@ export interface RecoveryStep {
label?: string;
}
// === Structured Error from Rust Backend ===
/**
* Error kinds matching Rust `zclaw_types::ErrorKind`.
* When the backend returns structured error JSON, use these for classification.
*/
export type RustErrorKind =
| 'not_found'
| 'permission'
| 'auth'
| 'llm'
| 'tool'
| 'storage'
| 'config'
| 'http'
| 'timeout'
| 'validation'
| 'loop_detected'
| 'rate_limit'
| 'mcp'
| 'security'
| 'hand'
| 'export'
| 'internal';
/**
* Structured error response from Rust backend.
* Matches `zclaw_types::ErrorDetail` serialization.
*/
export interface RustErrorDetail {
kind: RustErrorKind;
code: string; // e.g., "E4040", "E5001"
message: string;
}
/**
* Map Rust ErrorKind to frontend ErrorCategory.
*/
const ERROR_KIND_TO_CATEGORY: Record<RustErrorKind, ErrorCategory> = {
not_found: 'client',
permission: 'permission',
auth: 'auth',
llm: 'server',
tool: 'system',
storage: 'system',
config: 'config',
http: 'network',
timeout: 'timeout',
validation: 'validation',
loop_detected: 'system',
rate_limit: 'client',
mcp: 'system',
security: 'permission',
hand: 'system',
export: 'system',
internal: 'system',
};
/**
* Map Rust ErrorKind to error severity.
*/
const ERROR_KIND_TO_SEVERITY: Record<RustErrorKind, ErrorSeverity> = {
not_found: 'low',
permission: 'medium',
auth: 'high',
llm: 'high',
tool: 'medium',
storage: 'high',
config: 'medium',
http: 'medium',
timeout: 'medium',
validation: 'low',
loop_detected: 'medium',
rate_limit: 'medium',
mcp: 'medium',
security: 'high',
hand: 'medium',
export: 'low',
internal: 'high',
};
/**
* Map Rust ErrorKind to user-friendly title (Chinese).
*/
const ERROR_KIND_TO_TITLE: Record<RustErrorKind, string> = {
not_found: '资源未找到',
permission: '权限不足',
auth: '认证失败',
llm: '模型服务错误',
tool: '工具执行失败',
storage: '存储错误',
config: '配置错误',
http: '网络请求错误',
timeout: '请求超时',
validation: '输入无效',
loop_detected: '检测到循环调用',
rate_limit: '请求过于频繁',
mcp: 'MCP 协议错误',
security: '安全检查未通过',
hand: '自主能力执行失败',
export: '导出失败',
internal: '内部错误',
};
/**
* Try to parse a Tauri command error as a structured Rust error.
* Returns null if the error is not in structured format.
*/
export function tryParseRustError(error: unknown): RustErrorDetail | null {
if (typeof error === 'string') {
try {
const parsed = JSON.parse(error);
if (parsed && typeof parsed.kind === 'string' && typeof parsed.code === 'string') {
return parsed as RustErrorDetail;
}
} catch {
// Not JSON — fall through to string matching
}
}
if (typeof error === 'object' && error !== null) {
const obj = error as Record<string, unknown>;
if (typeof obj.kind === 'string' && typeof obj.code === 'string') {
return error as RustErrorDetail;
}
}
return null;
}
/**
* Classify a structured Rust error into an AppError.
*/
export function classifyRustError(rustError: RustErrorDetail, originalError?: unknown): AppError {
const kind = rustError.kind as RustErrorKind;
return {
id: `err_${Date.now()}_${generateRandomString(6)}`,
category: ERROR_KIND_TO_CATEGORY[kind] ?? 'system',
severity: ERROR_KIND_TO_SEVERITY[kind] ?? 'medium',
title: ERROR_KIND_TO_TITLE[kind] ?? '未知错误',
message: rustError.message,
technicalDetails: `[${rustError.code}] ${rustError.kind}`,
recoverable: !['auth', 'security', 'internal'].includes(kind),
recoverySteps: getRecoveryStepsForKind(kind),
timestamp: new Date(),
originalError: import.meta.env.DEV ? originalError : undefined,
};
}
function getRecoveryStepsForKind(kind: RustErrorKind): RecoveryStep[] {
switch (kind) {
case 'auth':
return [
{ description: '重新登录或重新连接' },
{ description: '检查 API Key 是否有效' },
];
case 'timeout':
return [
{ description: '稍后重试' },
{ description: '尝试简化请求内容' },
];
case 'rate_limit':
return [
{ description: '等待片刻后重试' },
{ description: '减少请求频率' },
];
case 'permission':
return [
{ description: '联系管理员获取权限' },
{ description: '检查当前角色是否有操作权限' },
];
case 'storage':
return [
{ description: '检查磁盘空间' },
{ description: '重启应用后重试' },
];
case 'llm':
return [
{ description: '检查模型配置是否正确' },
{ description: '切换到其他模型重试' },
];
default:
return [
{ description: '重试操作' },
{ description: '刷新页面后重试' },
];
}
}
// === Error Detection Patterns ===
interface ErrorPattern {
@@ -345,6 +532,13 @@ function matchPattern(error: unknown): { pattern: ErrorPattern; match: string }
* Classify an error and create an AppError with recovery suggestions.
*/
export function classifyError(error: unknown): AppError {
// Priority 1: structured Rust error (when backend returns ErrorDetail JSON)
const rustError = tryParseRustError(error);
if (rustError) {
return classifyRustError(rustError, error);
}
// Priority 2: string pattern matching (existing behavior)
const matched = matchPattern(error);
if (matched) {