import type { WebNotification, WebNotificationType, WebViewKey } from "../types"; import { isOptionalApiRouteMissing } from "./apiErrorUtils"; import { isRecord, isServerRequestError, serverRequest, writeStoredSession } from "./serverConnection"; interface CreateNotificationInput { type: WebNotificationType; title: string; description?: string; targetType?: string; targetId?: string; metadata?: Record; } const NOTIFICATION_VIEW_BY_TARGET: Record = { task: "workbench", generation_task: "workbench", community_case: "login", asset: "assets", project: "canvas", draft: "workbench", }; let notificationsRouteMissing = false; let notificationsUnauthorized = false; function toStringValue(value: unknown, fallback = ""): string { if (typeof value === "string") return value.trim() || fallback; if (typeof value === "number" && Number.isFinite(value)) return String(value); return fallback; } function normalizeType(value: unknown): WebNotificationType { const type = toStringValue(value); if ( type === "task_completed" || type === "task_failed" || type === "review_pending" || type === "review_passed" || type === "review_rejected" || type === "credits_low" || type === "session_expired" ) { return type; } return "info"; } function normalizeNotification(raw: unknown): WebNotification { const item = isRecord(raw) ? raw : {}; const targetType = toStringValue(item.targetType ?? item.target_type) || null; const targetId = toStringValue(item.targetId ?? item.target_id) || undefined; const readAt = toStringValue(item.readAt ?? item.read_at) || null; return { id: toStringValue(item.id, `notice-${Date.now()}`), type: normalizeType(item.type), title: toStringValue(item.title, "通知"), description: toStringValue(item.description), createdAt: toStringValue(item.createdAt ?? item.created_at, new Date().toISOString()), isRead: Boolean(item.isRead ?? item.is_read ?? readAt), targetType, targetId, targetView: targetType ? NOTIFICATION_VIEW_BY_TARGET[targetType] : undefined, readAt, metadata: isRecord(item.metadata) ? item.metadata : {}, }; } function extractNotifications(payload: unknown): WebNotification[] { if (Array.isArray(payload)) return payload.map(normalizeNotification); if (!isRecord(payload)) return []; const rows = payload.notifications ?? payload.items; return Array.isArray(rows) ? rows.map(normalizeNotification) : []; } function isUnauthorized(error: unknown): boolean { return isServerRequestError(error) && (error.status === 401 || error.status === 403); } function handleUnauthorizedNotifications(): void { notificationsUnauthorized = true; writeStoredSession(null); } export const notificationClient = { async list(): Promise { if (notificationsRouteMissing || notificationsUnauthorized) return []; try { return extractNotifications(await serverRequest("notifications")); } catch (error) { if (isOptionalApiRouteMissing(error)) { notificationsRouteMissing = true; return []; } if (isUnauthorized(error)) { handleUnauthorizedNotifications(); return []; } throw error; } }, async create(input: CreateNotificationInput): Promise { if (notificationsRouteMissing || notificationsUnauthorized) { return normalizeNotification({ ...input, id: `local-notice-${Date.now()}`, createdAt: new Date().toISOString(), }); } try { const payload = await serverRequest<{ notification: unknown }>("notifications", { method: "POST", body: input, }); return normalizeNotification(payload.notification ?? payload); } catch (error) { if (isOptionalApiRouteMissing(error)) { notificationsRouteMissing = true; return normalizeNotification({ ...input, id: `local-notice-${Date.now()}`, createdAt: new Date().toISOString(), }); } if (isUnauthorized(error)) { handleUnauthorizedNotifications(); return normalizeNotification({ ...input, id: `local-notice-${Date.now()}`, createdAt: new Date().toISOString(), }); } throw error; } }, async markRead(id: string, isRead = true): Promise { if (notificationsRouteMissing || notificationsUnauthorized) return; try { await serverRequest(`notifications/${id}/read`, { method: "PATCH", body: { isRead }, }); } catch (error) { if (isOptionalApiRouteMissing(error)) { notificationsRouteMissing = true; return; } if (isUnauthorized(error)) { handleUnauthorizedNotifications(); return; } throw error; } }, async markAllRead(): Promise { if (notificationsRouteMissing || notificationsUnauthorized) return; try { await serverRequest("notifications/read-all", { method: "POST" }); } catch (error) { if (isOptionalApiRouteMissing(error)) { notificationsRouteMissing = true; return; } if (isUnauthorized(error)) { handleUnauthorizedNotifications(); return; } throw error; } }, };