diff --git a/admin/src/lib/api.ts b/admin/src/lib/api.ts index 22a25fe..86c1e72 100644 --- a/admin/src/lib/api.ts +++ b/admin/src/lib/api.ts @@ -1,587 +1,588 @@ -import type { - AdminAnalyticsResponse, - AdminAiImageProviderTestResponse, - AdminAiReindexResponse, - AdminAiProviderTestResponse, - AdminImageUploadResponse, - AdminMediaBatchDeleteResponse, - AdminMediaDeleteResponse, - AdminMediaListResponse, - AdminMediaReplaceResponse, - AdminMediaUploadResponse, - AdminPostCoverImageRequest, - AdminPostCoverImageResponse, - AdminDashboardResponse, - AdminPostMetadataResponse, - AdminPostPolishResponse, - AdminReviewPolishRequest, - AdminReviewPolishResponse, - AdminR2ConnectivityResponse, - AdminSessionResponse, - AdminSiteSettingsResponse, - AuditLogRecord, - CommentListQuery, - CommentBlacklistRecord, - CommentPersonaAnalysisLogRecord, - CommentPersonaAnalysisResponse, - CommentRecord, - CreatePostPayload, - CreateReviewPayload, - FriendLinkListQuery, - FriendLinkPayload, - FriendLinkRecord, - MarkdownDeleteResponse, - MarkdownDocumentResponse, - MarkdownImportResponse, - NotificationDeliveryRecord, - PostListQuery, - PostRevisionDetail, - PostRevisionRecord, - PostRecord, - ReviewRecord, - RestoreRevisionResponse, - SiteSettingsPayload, - SubscriptionDigestResponse, - SubscriptionListResponse, - SubscriptionPayload, - SubscriptionRecord, - SubscriptionUpdatePayload, - UpdateCommentPayload, - UpdatePostPayload, - UpdateReviewPayload, -} from '@/lib/types' -import { getRuntimeAdminBaseUrl, normalizeAdminBaseUrl } from '@/lib/runtime-config' - +import type { + AdminAnalyticsResponse, + AdminAiImageProviderTestResponse, + AdminAiReindexResponse, + AdminAiProviderTestResponse, + AdminImageUploadResponse, + AdminMediaBatchDeleteResponse, + AdminMediaDeleteResponse, + AdminMediaListResponse, + AdminMediaReplaceResponse, + AdminMediaUploadResponse, + AdminPostCoverImageRequest, + AdminPostCoverImageResponse, + AdminDashboardResponse, + AdminPostMetadataResponse, + AdminPostPolishResponse, + AdminReviewPolishRequest, + AdminReviewPolishResponse, + AdminR2ConnectivityResponse, + AdminSessionResponse, + AdminSiteSettingsResponse, + AuditLogRecord, + CommentListQuery, + CommentBlacklistRecord, + CommentPersonaAnalysisLogRecord, + CommentPersonaAnalysisResponse, + CommentRecord, + CreatePostPayload, + CreateReviewPayload, + FriendLinkListQuery, + FriendLinkPayload, + FriendLinkRecord, + MarkdownDeleteResponse, + MarkdownDocumentResponse, + MarkdownImportResponse, + NotificationDeliveryRecord, + PostListQuery, + PostRevisionDetail, + PostRevisionRecord, + PostRecord, + ReviewRecord, + RestoreRevisionResponse, + SiteSettingsPayload, + SubscriptionDigestResponse, + SubscriptionListResponse, + SubscriptionPayload, + SubscriptionRecord, + SubscriptionUpdatePayload, + UpdateCommentPayload, + UpdatePostPayload, + UpdateReviewPayload, +} from '@/lib/types' +import { getRuntimeAdminBaseUrl, normalizeAdminBaseUrl } from '@/lib/runtime-config' + const envApiBase = normalizeAdminBaseUrl(import.meta.env.VITE_API_BASE) -const DEV_API_BASE = 'http://localhost:5150' const PROD_DEFAULT_API_PORT = '5150' +const DEV_DEFAULT_API_HOST = '127.0.0.1' function getApiBase() { const runtimeApiBase = getRuntimeAdminBaseUrl('apiBaseUrl') if (runtimeApiBase) { return runtimeApiBase - } - + } + if (envApiBase) { return envApiBase } if (import.meta.env.DEV) { - return DEV_API_BASE - } + if (typeof window !== 'undefined') { + const { protocol, hostname } = window.location + return `${protocol}//${hostname}:${PROD_DEFAULT_API_PORT}` + } - if (typeof window === 'undefined') { - return DEV_API_BASE + return `http://${DEV_DEFAULT_API_HOST}:${PROD_DEFAULT_API_PORT}` } const { protocol, hostname } = window.location return `${protocol}//${hostname}:${PROD_DEFAULT_API_PORT}` } - -const API_BASE = getApiBase() - -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, - credentials: 'include', - 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', - }), - listAuditLogs: (query?: { action?: string; targetType?: string; limit?: number }) => - request( - appendQueryParams('/api/admin/audit-logs', { - action: query?.action, - target_type: query?.targetType, - limit: query?.limit, - }), - ), - listPostRevisions: (query?: { slug?: string; limit?: number }) => - request( - appendQueryParams('/api/admin/post-revisions', { - slug: query?.slug, - limit: query?.limit, - }), - ), - getPostRevision: (id: number) => request(`/api/admin/post-revisions/${id}`), - restorePostRevision: (id: number, mode: 'full' | 'markdown' | 'metadata' = 'full') => - request(`/api/admin/post-revisions/${id}/restore`, { - method: 'POST', - body: JSON.stringify({ mode }), - }), - listSubscriptions: async () => - (await request('/api/admin/subscriptions')).subscriptions, - createSubscription: (payload: SubscriptionPayload) => - request('/api/admin/subscriptions', { - method: 'POST', - body: JSON.stringify({ - channelType: payload.channelType, - target: payload.target, - displayName: payload.displayName, - status: payload.status, - filters: payload.filters, - metadata: payload.metadata, - secret: payload.secret, - notes: payload.notes, - }), - }), - updateSubscription: (id: number, payload: SubscriptionUpdatePayload) => - request(`/api/admin/subscriptions/${id}`, { - method: 'PATCH', - body: JSON.stringify({ - channelType: payload.channelType, - target: payload.target, - displayName: payload.displayName, - status: payload.status, - filters: payload.filters, - metadata: payload.metadata, - secret: payload.secret, - notes: payload.notes, - }), - }), - deleteSubscription: (id: number) => - request(`/api/admin/subscriptions/${id}`, { - method: 'DELETE', - }), - testSubscription: (id: number) => - request<{ queued: boolean; id: number; delivery_id: number }>(`/api/admin/subscriptions/${id}/test`, { - method: 'POST', - }), - listSubscriptionDeliveries: async (limit = 80) => - (await request<{ deliveries: NotificationDeliveryRecord[] }>( - appendQueryParams('/api/admin/subscriptions/deliveries', { limit }), - )).deliveries, - sendSubscriptionDigest: (period: 'weekly' | 'monthly') => - request('/api/admin/subscriptions/digest', { - method: 'POST', - body: JSON.stringify({ period }), - }), - 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', - }, - ), - uploadMediaObjects: (files: File[], options?: { prefix?: string }) => { - const formData = new FormData() - if (options?.prefix) { - formData.append('prefix', options.prefix) - } - files.forEach((file) => { - formData.append('files', file, file.name) - }) - - return request('/api/admin/storage/media', { - method: 'POST', - body: formData, - }) - }, - batchDeleteMediaObjects: (keys: string[]) => - request('/api/admin/storage/media/batch-delete', { - method: 'POST', - body: JSON.stringify({ keys }), - }), - replaceMediaObject: (key: string, file: File) => { - const formData = new FormData() - formData.append('key', key) - formData.append('file', file, file.name) - - return request('/api/admin/storage/media/replace', { - method: 'POST', - body: formData, - }) - }, - 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, - status: query?.status, - visibility: query?.visibility, - listed_only: query?.listedOnly, - include_private: query?.includePrivate ?? true, - include_redirects: query?.includeRedirects ?? true, - preview: query?.preview ?? true, - }), - ), - getPostBySlug: (slug: string) => - request(`/api/posts/slug/${encodeURIComponent(slug)}?preview=true&include_private=true`), - 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, - status: payload.status, - visibility: payload.visibility, - publish_at: payload.publishAt, - unpublish_at: payload.unpublishAt, - canonical_url: payload.canonicalUrl, - noindex: payload.noindex, - og_image: payload.ogImage, - redirect_from: payload.redirectFrom, - redirect_to: payload.redirectTo, - 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, - status: payload.status, - visibility: payload.visibility, - publish_at: payload.publishAt, - unpublish_at: payload.unpublishAt, - canonical_url: payload.canonicalUrl, - noindex: payload.noindex, - og_image: payload.ogImage, - redirect_from: payload.redirectFrom, - redirect_to: payload.redirectTo, - }), - }), - 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', - }), - listCommentBlacklist: () => - request('/api/admin/comments/blacklist'), - createCommentBlacklist: (payload: { - matcher_type: 'ip' | 'email' | 'user_agent' | string - matcher_value: string - reason?: string | null - active?: boolean - expires_at?: string | null - }) => - request('/api/admin/comments/blacklist', { - method: 'POST', - body: JSON.stringify(payload), - }), - updateCommentBlacklist: ( - id: number, - payload: { - reason?: string | null - active?: boolean - expires_at?: string | null - clear_expires_at?: boolean - }, - ) => - request(`/api/admin/comments/blacklist/${id}`, { - method: 'PATCH', - body: JSON.stringify(payload), - }), - deleteCommentBlacklist: (id: number) => - request<{ deleted: boolean; id: number }>(`/api/admin/comments/blacklist/${id}`, { - method: 'DELETE', - }), - analyzeCommentPersona: (payload: { - matcher_type: 'ip' | 'email' | 'user_agent' | string - matcher_value: string - from?: string | null - to?: string | null - limit?: number - }) => - request('/api/admin/comments/analyze', { - method: 'POST', - body: JSON.stringify(payload), - }), - listCommentPersonaAnalysisLogs: (query?: { - matcher_type?: 'ip' | 'email' | 'user_agent' | string - matcher_value?: string - limit?: number - }) => - request( - appendQueryParams('/api/admin/comments/analyze/logs', { - matcher_type: query?.matcher_type, - matcher_value: query?.matcher_value, - limit: query?.limit, - }), - ), - 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', - }), -} + +const API_BASE = getApiBase() + +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, + credentials: 'include', + 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', + }), + listAuditLogs: (query?: { action?: string; targetType?: string; limit?: number }) => + request( + appendQueryParams('/api/admin/audit-logs', { + action: query?.action, + target_type: query?.targetType, + limit: query?.limit, + }), + ), + listPostRevisions: (query?: { slug?: string; limit?: number }) => + request( + appendQueryParams('/api/admin/post-revisions', { + slug: query?.slug, + limit: query?.limit, + }), + ), + getPostRevision: (id: number) => request(`/api/admin/post-revisions/${id}`), + restorePostRevision: (id: number, mode: 'full' | 'markdown' | 'metadata' = 'full') => + request(`/api/admin/post-revisions/${id}/restore`, { + method: 'POST', + body: JSON.stringify({ mode }), + }), + listSubscriptions: async () => + (await request('/api/admin/subscriptions')).subscriptions, + createSubscription: (payload: SubscriptionPayload) => + request('/api/admin/subscriptions', { + method: 'POST', + body: JSON.stringify({ + channelType: payload.channelType, + target: payload.target, + displayName: payload.displayName, + status: payload.status, + filters: payload.filters, + metadata: payload.metadata, + secret: payload.secret, + notes: payload.notes, + }), + }), + updateSubscription: (id: number, payload: SubscriptionUpdatePayload) => + request(`/api/admin/subscriptions/${id}`, { + method: 'PATCH', + body: JSON.stringify({ + channelType: payload.channelType, + target: payload.target, + displayName: payload.displayName, + status: payload.status, + filters: payload.filters, + metadata: payload.metadata, + secret: payload.secret, + notes: payload.notes, + }), + }), + deleteSubscription: (id: number) => + request(`/api/admin/subscriptions/${id}`, { + method: 'DELETE', + }), + testSubscription: (id: number) => + request<{ queued: boolean; id: number; delivery_id: number }>(`/api/admin/subscriptions/${id}/test`, { + method: 'POST', + }), + listSubscriptionDeliveries: async (limit = 80) => + (await request<{ deliveries: NotificationDeliveryRecord[] }>( + appendQueryParams('/api/admin/subscriptions/deliveries', { limit }), + )).deliveries, + sendSubscriptionDigest: (period: 'weekly' | 'monthly') => + request('/api/admin/subscriptions/digest', { + method: 'POST', + body: JSON.stringify({ period }), + }), + 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', + }, + ), + uploadMediaObjects: (files: File[], options?: { prefix?: string }) => { + const formData = new FormData() + if (options?.prefix) { + formData.append('prefix', options.prefix) + } + files.forEach((file) => { + formData.append('files', file, file.name) + }) + + return request('/api/admin/storage/media', { + method: 'POST', + body: formData, + }) + }, + batchDeleteMediaObjects: (keys: string[]) => + request('/api/admin/storage/media/batch-delete', { + method: 'POST', + body: JSON.stringify({ keys }), + }), + replaceMediaObject: (key: string, file: File) => { + const formData = new FormData() + formData.append('key', key) + formData.append('file', file, file.name) + + return request('/api/admin/storage/media/replace', { + method: 'POST', + body: formData, + }) + }, + 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, + status: query?.status, + visibility: query?.visibility, + listed_only: query?.listedOnly, + include_private: query?.includePrivate ?? true, + include_redirects: query?.includeRedirects ?? true, + preview: query?.preview ?? true, + }), + ), + getPostBySlug: (slug: string) => + request(`/api/posts/slug/${encodeURIComponent(slug)}?preview=true&include_private=true`), + 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, + status: payload.status, + visibility: payload.visibility, + publish_at: payload.publishAt, + unpublish_at: payload.unpublishAt, + canonical_url: payload.canonicalUrl, + noindex: payload.noindex, + og_image: payload.ogImage, + redirect_from: payload.redirectFrom, + redirect_to: payload.redirectTo, + 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, + status: payload.status, + visibility: payload.visibility, + publish_at: payload.publishAt, + unpublish_at: payload.unpublishAt, + canonical_url: payload.canonicalUrl, + noindex: payload.noindex, + og_image: payload.ogImage, + redirect_from: payload.redirectFrom, + redirect_to: payload.redirectTo, + }), + }), + 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', + }), + listCommentBlacklist: () => + request('/api/admin/comments/blacklist'), + createCommentBlacklist: (payload: { + matcher_type: 'ip' | 'email' | 'user_agent' | string + matcher_value: string + reason?: string | null + active?: boolean + expires_at?: string | null + }) => + request('/api/admin/comments/blacklist', { + method: 'POST', + body: JSON.stringify(payload), + }), + updateCommentBlacklist: ( + id: number, + payload: { + reason?: string | null + active?: boolean + expires_at?: string | null + clear_expires_at?: boolean + }, + ) => + request(`/api/admin/comments/blacklist/${id}`, { + method: 'PATCH', + body: JSON.stringify(payload), + }), + deleteCommentBlacklist: (id: number) => + request<{ deleted: boolean; id: number }>(`/api/admin/comments/blacklist/${id}`, { + method: 'DELETE', + }), + analyzeCommentPersona: (payload: { + matcher_type: 'ip' | 'email' | 'user_agent' | string + matcher_value: string + from?: string | null + to?: string | null + limit?: number + }) => + request('/api/admin/comments/analyze', { + method: 'POST', + body: JSON.stringify(payload), + }), + listCommentPersonaAnalysisLogs: (query?: { + matcher_type?: 'ip' | 'email' | 'user_agent' | string + matcher_value?: string + limit?: number + }) => + request( + appendQueryParams('/api/admin/comments/analyze/logs', { + matcher_type: query?.matcher_type, + matcher_value: query?.matcher_value, + limit: query?.limit, + }), + ), + 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', + }), +}