export type ProjectSettingsIssue = { code?: string; field?: string; message?: string; }; export type ProjectSettingsErrorRecovery = | 'refresh_and_freeze' | 'reload_revision' | 'retry_after_switch' | 'show_message'; export class ProjectSettingsApiError extends Error { readonly status: number; readonly code: string; readonly issues: ProjectSettingsIssue[]; constructor(input: { status: number; code: string; message: string; issues?: ProjectSettingsIssue[]; }) { super(input.message); this.name = 'ProjectSettingsApiError'; this.status = input.status; this.code = input.code; this.issues = input.issues ?? []; } } export function parseProjectSettingsErrorBody(text: string): { message: string; code: string; issues: ProjectSettingsIssue[]; } { try { const parsed = JSON.parse(text) as { error?: string; message?: string; issues?: ProjectSettingsIssue[]; }; const issues = Array.isArray(parsed.issues) ? parsed.issues : []; const issueText = issues .map((issue) => issue.message) .filter((msg): msg is string => Boolean(msg)) .join(';'); return { code: String(parsed.error || ''), message: issueText || parsed.message || parsed.error || text || '请求失败', issues, }; } catch { return { code: '', message: text || '请求失败', issues: [] }; } } export function projectSettingsErrorFromResponse( status: number, text: string, fallback: string, ): ProjectSettingsApiError { const parsed = parseProjectSettingsErrorBody(text); return new ProjectSettingsApiError({ status, code: parsed.code, message: parsed.message || fallback, issues: parsed.issues, }); } export function recoverFromProjectSettingsError( err: Pick, ): ProjectSettingsErrorRecovery { if (err.code === 'PLAN_FROZEN') return 'refresh_and_freeze'; if (err.code === 'PLAN_REVISION_CONFLICT') return 'reload_revision'; if (err.code === 'ERR_CONCURRENT_SWITCH') return 'retry_after_switch'; return 'show_message'; } export function retainDraftOnFrozenConflict(draft: T): T { return draft; }