Files
omniai-web/src/api/notificationClient.ts
T

174 lines
5.2 KiB
TypeScript
Raw Normal View History

2026-06-02 12:38:01 +08:00
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<string, unknown>;
}
const NOTIFICATION_VIEW_BY_TARGET: Record<string, WebViewKey> = {
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<WebNotification[]> {
if (notificationsRouteMissing || notificationsUnauthorized) return [];
try {
return extractNotifications(await serverRequest<unknown>("notifications"));
} catch (error) {
if (isOptionalApiRouteMissing(error)) {
notificationsRouteMissing = true;
return [];
}
if (isUnauthorized(error)) {
handleUnauthorizedNotifications();
return [];
}
throw error;
}
},
async create(input: CreateNotificationInput): Promise<WebNotification> {
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<void> {
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<void> {
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;
}
},
};