import type { AdminAnalyticsResponse, AdminAiImageProviderTestResponse, AdminAiReindexResponse, AdminAiProviderTestResponse, AdminImageUploadResponse, AdminMediaDeleteResponse, AdminMediaListResponse, AdminPostCoverImageRequest, AdminPostCoverImageResponse, AdminDashboardResponse, AdminPostMetadataResponse, AdminPostPolishResponse, AdminReviewPolishRequest, AdminReviewPolishResponse, AdminR2ConnectivityResponse, AdminSessionResponse, AdminSiteSettingsResponse, CommentListQuery, CommentRecord, CreatePostPayload, CreateReviewPayload, FriendLinkListQuery, FriendLinkPayload, FriendLinkRecord, MarkdownDeleteResponse, MarkdownDocumentResponse, MarkdownImportResponse, PostListQuery, PostRecord, ReviewRecord, SiteSettingsPayload, UpdateCommentPayload, UpdatePostPayload, UpdateReviewPayload, } from '@/lib/types' const API_BASE = import.meta.env.VITE_API_BASE?.trim() || '' export class ApiError extends Error { status: number constructor(message: string, status: number) { super(message) this.name = 'ApiError' this.status = status } } async function readErrorMessage(response: Response) { const text = await response.text().catch(() => '') if (!text) { return `请求失败,状态码 ${response.status}。` } try { const parsed = JSON.parse(text) as { description?: string; error?: string; message?: string } return parsed.description || parsed.error || parsed.message || text } catch { return text } } function appendQueryParams(path: string, params?: Record) { if (!params) { return path } const searchParams = new URLSearchParams() Object.entries(params).forEach(([key, value]) => { if (value === undefined || value === null || value === '') { return } if (typeof value === 'boolean') { searchParams.set(key, String(value)) return } searchParams.set(key, String(value)) }) const queryString = searchParams.toString() return queryString ? `${path}?${queryString}` : path } async function request(path: string, init?: RequestInit): Promise { const headers = new Headers(init?.headers) if (init?.body && !(init.body instanceof FormData) && !headers.has('Content-Type')) { headers.set('Content-Type', 'application/json') } const response = await fetch(`${API_BASE}${path}`, { ...init, headers, }) if (!response.ok) { throw new ApiError(await readErrorMessage(response), response.status) } if (response.status === 204) { return undefined as T } const contentType = response.headers.get('content-type') || '' if (contentType.includes('application/json')) { return (await response.json()) as T } return (await response.text()) as T } export const adminApi = { sessionStatus: () => request('/api/admin/session'), login: (payload: { username: string; password: string }) => request('/api/admin/session/login', { method: 'POST', body: JSON.stringify(payload), }), logout: () => request('/api/admin/session', { method: 'DELETE', }), dashboard: () => request('/api/admin/dashboard'), analytics: () => request('/api/admin/analytics'), getSiteSettings: () => request('/api/admin/site-settings'), updateSiteSettings: (payload: SiteSettingsPayload) => request('/api/admin/site-settings', { method: 'PATCH', body: JSON.stringify(payload), }), reindexAi: () => request('/api/admin/ai/reindex', { method: 'POST', }), testAiProvider: (provider: { id: string name: string provider: string api_base: string | null api_key: string | null chat_model: string | null }) => request('/api/admin/ai/test-provider', { method: 'POST', body: JSON.stringify({ provider }), }), testAiImageProvider: (provider: { provider: string api_base: string | null api_key: string | null image_model: string | null }) => request('/api/admin/ai/test-image-provider', { method: 'POST', body: JSON.stringify({ provider: provider.provider, api_base: provider.api_base, api_key: provider.api_key, image_model: provider.image_model, }), }), uploadReviewCoverImage: (file: File) => { const formData = new FormData() formData.append('file', file, file.name) return request('/api/admin/storage/review-cover', { method: 'POST', body: formData, }) }, testR2Storage: () => request('/api/admin/storage/r2/test', { method: 'POST', }), listMediaObjects: (query?: { prefix?: string; limit?: number }) => request( appendQueryParams('/api/admin/storage/media', { prefix: query?.prefix, limit: query?.limit, }), ), deleteMediaObject: (key: string) => request( `/api/admin/storage/media?key=${encodeURIComponent(key)}`, { method: 'DELETE', }, ), generatePostMetadata: (markdown: string) => request('/api/admin/ai/post-metadata', { method: 'POST', body: JSON.stringify({ markdown }), }), polishPostMarkdown: (markdown: string) => request('/api/admin/ai/polish-post', { method: 'POST', body: JSON.stringify({ markdown }), }), polishReviewDescription: (payload: AdminReviewPolishRequest) => request('/api/admin/ai/polish-review', { method: 'POST', body: JSON.stringify({ title: payload.title, review_type: payload.reviewType, rating: payload.rating, review_date: payload.reviewDate, status: payload.status, tags: payload.tags, description: payload.description, }), }), generatePostCoverImage: (payload: AdminPostCoverImageRequest) => request('/api/admin/ai/post-cover', { method: 'POST', body: JSON.stringify({ title: payload.title, description: payload.description, category: payload.category, tags: payload.tags, post_type: payload.postType, slug: payload.slug, markdown: payload.markdown, }), }), listPosts: (query?: PostListQuery) => request( appendQueryParams('/api/posts', { slug: query?.slug, category: query?.category, tag: query?.tag, search: query?.search, type: query?.postType, pinned: query?.pinned, }), ), getPostBySlug: (slug: string) => request(`/api/posts/slug/${encodeURIComponent(slug)}`), createPost: (payload: CreatePostPayload) => request('/api/posts/markdown', { method: 'POST', body: JSON.stringify({ title: payload.title, slug: payload.slug, description: payload.description, content: payload.content, category: payload.category, tags: payload.tags, post_type: payload.postType, image: payload.image, images: payload.images, pinned: payload.pinned, published: payload.published, }), }), updatePost: (id: number, payload: UpdatePostPayload) => request(`/api/posts/${id}`, { method: 'PATCH', body: JSON.stringify({ title: payload.title, slug: payload.slug, description: payload.description, content: payload.content, category: payload.category, tags: payload.tags, post_type: payload.postType, image: payload.image, images: payload.images, pinned: payload.pinned, }), }), getPostMarkdown: (slug: string) => request(`/api/posts/slug/${encodeURIComponent(slug)}/markdown`), importPosts: async (files: File[]) => { const formData = new FormData() files.forEach((file) => { formData.append('files', file, file.webkitRelativePath || file.name) }) return request('/api/posts/markdown/import', { method: 'POST', body: formData, }) }, updatePostMarkdown: (slug: string, markdown: string) => request(`/api/posts/slug/${encodeURIComponent(slug)}/markdown`, { method: 'PATCH', body: JSON.stringify({ markdown }), }), deletePost: (slug: string) => request(`/api/posts/slug/${encodeURIComponent(slug)}/markdown`, { method: 'DELETE', }), listComments: (query?: CommentListQuery) => request( appendQueryParams('/api/comments', { post_id: query?.postId, post_slug: query?.postSlug, scope: query?.scope, paragraph_key: query?.paragraphKey, approved: query?.approved, }), ), updateComment: (id: number, payload: UpdateCommentPayload) => request(`/api/comments/${id}`, { method: 'PATCH', body: JSON.stringify(payload), }), deleteComment: (id: number) => request(`/api/comments/${id}`, { method: 'DELETE', }), listFriendLinks: (query?: FriendLinkListQuery) => request( appendQueryParams('/api/friend_links', { status: query?.status, category: query?.category, }), ), createFriendLink: (payload: FriendLinkPayload) => request('/api/friend_links', { method: 'POST', body: JSON.stringify({ siteName: payload.siteName, siteUrl: payload.siteUrl, avatarUrl: payload.avatarUrl, description: payload.description, category: payload.category, status: payload.status, }), }), updateFriendLink: (id: number, payload: FriendLinkPayload) => request(`/api/friend_links/${id}`, { method: 'PATCH', body: JSON.stringify({ site_name: payload.siteName, site_url: payload.siteUrl, avatar_url: payload.avatarUrl, description: payload.description, category: payload.category, status: payload.status, }), }), deleteFriendLink: (id: number) => request(`/api/friend_links/${id}`, { method: 'DELETE', }), listReviews: () => request('/api/reviews'), createReview: (payload: CreateReviewPayload) => request('/api/reviews', { method: 'POST', body: JSON.stringify(payload), }), updateReview: (id: number, payload: UpdateReviewPayload) => request(`/api/reviews/${id}`, { method: 'PUT', body: JSON.stringify(payload), }), deleteReview: (id: number) => request(`/api/reviews/${id}`, { method: 'DELETE', }), }