import type { CoreAssistantMessage, CoreToolMessage, UIMessage, UIMessagePart, } from 'ai'; import { type ClassValue, clsx } from 'clsx'; import { formatISO } from 'date-fns'; import { twMerge } from 'tailwind-merge'; import type { DBMessage, Document } from '@/lib/db/schema'; import { ChatSDKError, type ErrorCode } from './errors'; import { getFriendlyRateLimitMessage, getUserFacingRequestErrorMessage } from './request-error'; import type { ChatMessage, ChatTools, CustomUIDataTypes } from './types'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } export const fetcher = async (url: string) => { const response = await fetch(url); if (!response.ok) { const { code, cause, message } = await response.json(); const error = new ChatSDKError(code as ErrorCode, cause); if (typeof message === "string" && message.trim()) { error.message = message; } throw error; } return response.json(); }; export async function fetchWithErrorHandlers( input: RequestInfo | URL, init?: RequestInit, ) { try { const response = await fetch(input, init); if (!response.ok) { let payload: any = null; let responseText = ""; try { payload = await response.clone().json(); } catch { payload = null; } if (payload == null) { try { responseText = (await response.text()).trim(); } catch { responseText = ""; } } const code = typeof payload?.code === "string" && payload.code.includes(":") ? (payload.code as ErrorCode) : null; const cause = typeof payload?.cause === "string" ? payload.cause : undefined; if (code) { const error = new ChatSDKError(code, cause); if (typeof payload?.message === "string" && payload.message.trim()) { error.message = payload.message; } throw error; } const fallbackMessage = payload?.error === "quota_exceeded" && typeof payload?.message === "string" ? payload.message : getUserFacingRequestErrorMessage( { status: response.status, data: payload, message: responseText, }, `Request failed with status ${response.status}`, { rateLimitMessage: getFriendlyRateLimitMessage(), } ); const requestError = new Error(fallbackMessage) as Error & { status?: number; data?: any; }; requestError.name = "RequestError"; requestError.status = response.status; requestError.data = payload ?? (responseText ? { message: responseText, } : null); throw requestError; } return response; } catch (error: unknown) { if (typeof navigator !== 'undefined' && !navigator.onLine) { throw new ChatSDKError('offline:chat'); } throw error; } } export function getLocalStorage(key: string) { if (typeof window !== 'undefined') { return JSON.parse(localStorage.getItem(key) || '[]'); } return []; } export function generateUUID(): string { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { const r = (Math.random() * 16) | 0; const v = c === 'x' ? r : (r & 0x3) | 0x8; return v.toString(16); }); } type ResponseMessageWithoutId = CoreToolMessage | CoreAssistantMessage; type ResponseMessage = ResponseMessageWithoutId & { id: string }; export function getMostRecentUserMessage(messages: UIMessage[]) { const userMessages = messages.filter((message) => message.role === 'user'); return userMessages.at(-1); } export function getDocumentTimestampByIndex( documents: Document[], index: number, ) { if (!documents) { return new Date(); } if (index > documents.length) { return new Date(); } return documents[index].createdAt; } export function getTrailingMessageId({ messages, }: { messages: ResponseMessage[]; }): string | null { const trailingMessage = messages.at(-1); if (!trailingMessage) { return null; } return trailingMessage.id; } export function sanitizeText(text: string) { return text.replace('', ''); } export function convertToUIMessages(messages: DBMessage[]): ChatMessage[] { return messages.map((message) => ({ id: message.id, role: message.role as 'user' | 'assistant' | 'system', parts: message.parts as UIMessagePart[], metadata: { createdAt: formatISO(message.createdAt), }, })); } export function getTextFromMessage(message: ChatMessage | UIMessage): string { return message.parts .filter((part) => part.type === 'text') .map((part) => (part as { type: 'text'; text: string}).text) .join(''); } /** * 将UTC时间转换为北京时间(UTC+8) * @param date - Date对象或时间字符串 * @returns 北京时间对应的Date对象 */ export function toBeijingTime(date: Date | string): Date { const d = typeof date === "string" ? new Date(date) : date; // 获取UTC时间戳 const utcTime = d.getTime() + (d.getTimezoneOffset() * 60 * 1000); // 转换为北京时间(UTC+8) const beijingTime = new Date(utcTime + (8 * 60 * 60 * 1000)); return beijingTime; } /** * 格式化时间为北京时间字符串 * @param date - Date对象或时间字符串 * @param options - Intl.DateTimeFormatOptions 格式化选项 * @returns 格式化后的北京时间字符串 */ export function formatBeijingTime( date: Date | string, options?: Intl.DateTimeFormatOptions ): string { const beijingTime = toBeijingTime(date); const defaultOptions: Intl.DateTimeFormatOptions = { year: "numeric", month: "numeric", day: "numeric", hour: "2-digit", minute: "2-digit", timeZone: "Asia/Shanghai", ...options, }; return beijingTime.toLocaleString("zh-CN", defaultOptions); }