Initial commit: OmniAI Web Frontend
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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;
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user