import axios from "axios"; import { SPARK_BACKEND_BASE_URL } from "@/global/storeConfig"; import type { AppReview, AuthSession, DownloadedAppList, DownloadedAppRecord, FavoriteFolder, FavoriteItem, RatingSummary, ReviewTags, SparkUser, SyncedAppList, SyncedAppListItem, } from "@/global/typedefinition"; const backend = axios.create({ baseURL: SPARK_BACKEND_BASE_URL, timeout: 10000, }); type ApiRecord = Record; const asApiRecord = (value: unknown): ApiRecord => { if (value && typeof value === "object" && !Array.isArray(value)) { return value as ApiRecord; } return {}; }; const asApiRecordArray = (value: unknown): ApiRecord[] => { if (!Array.isArray(value)) return []; return value.map(asApiRecord); }; export const parseForumGroups = (raw: unknown): string[] => { if (Array.isArray(raw)) { return raw.filter((item): item is string => typeof item === "string"); } if (typeof raw !== "string" || raw.length === 0) return []; try { const parsed: unknown = JSON.parse(raw); return Array.isArray(parsed) ? parsed.filter((item): item is string => typeof item === "string") : []; } catch { return []; } }; const toUser = (raw: ApiRecord): SparkUser => ({ id: Number(raw.id), flarumUserId: String(raw.flarum_user_id || ""), username: String(raw.username || ""), displayName: String(raw.display_name || raw.username || ""), avatarUrl: String(raw.avatar_url || ""), forumLevel: String(raw.forum_level || "论坛用户"), forumGroups: parseForumGroups(raw.forum_groups), }); const toReview = (raw: ApiRecord): AppReview => ({ id: Number(raw.id), rating: Number(raw.rating), content: String(raw.content || ""), version: String(raw.version || "unknown"), packageArch: String(raw.package_arch || "unknown"), clientArch: String(raw.client_arch || "unknown"), distro: String(raw.distro || "unknown"), origin: raw.origin === "spark" ? "spark" : "apm", category: String(raw.category || ""), createdAt: String(raw.created_at || ""), updatedAt: String(raw.updated_at || ""), userDisplayName: String(raw.user_display_name || ""), userAvatarUrl: String(raw.user_avatar_url || ""), }); const toFavoriteFolder = (raw: ApiRecord): FavoriteFolder => ({ id: Number(raw.id), name: String(raw.name || ""), itemCount: Number(raw.item_count || 0), createdAt: String(raw.created_at || ""), updatedAt: String(raw.updated_at || ""), }); const toFavoriteItem = (raw: ApiRecord): FavoriteItem => ({ id: Number(raw.id), appKey: String(raw.app_key || ""), pkgname: String(raw.pkgname || ""), name: String(raw.name || ""), category: String(raw.category || ""), iconUrl: String(raw.icon_url || ""), createdAt: String(raw.created_at || ""), }); const toDownloadedApp = (raw: ApiRecord): DownloadedAppRecord => ({ id: Number(raw.id), appKey: String(raw.app_key || ""), pkgname: String(raw.pkgname || ""), name: String(raw.name || ""), category: String(raw.category || ""), selectedOrigin: raw.selected_origin === "spark" ? "spark" : "apm", version: String(raw.version || ""), packageArch: String(raw.package_arch || "unknown"), downloadedAt: String(raw.downloaded_at || ""), }); const toSyncedAppListItem = (raw: ApiRecord): SyncedAppListItem => ({ id: raw.id === undefined ? undefined : Number(raw.id), pkgname: String(raw.pkgname || ""), origin: raw.origin === "spark" ? "spark" : "apm", category: String(raw.category || ""), version: String(raw.version || ""), packageArch: String(raw.package_arch || "unknown"), appName: String(raw.app_name || ""), iconUrl: String(raw.icon_url || ""), }); const toSyncedAppList = ( raw: ApiRecord, fallback?: { clientArch: string; distro: string; items: SyncedAppListItem[] }, ): SyncedAppList => ({ snapshotName: String(raw.snapshot_name || "默认列表"), clientArch: String(raw.client_arch || fallback?.clientArch || "unknown"), distro: String(raw.distro || fallback?.distro || "unknown"), updatedAt: String(raw.updated_at || ""), items: raw.items ? asApiRecordArray(raw.items).map(toSyncedAppListItem) : fallback?.items || [], }); export const setBackendToken = (token: string | null): void => { const backendWithOptionalDefaults = backend as typeof backend & { defaults?: { headers?: { common?: Record } }; }; const commonHeaders = backendWithOptionalDefaults.defaults?.headers?.common; if (!commonHeaders) return; if (token) commonHeaders.Authorization = `Bearer ${token}`; else delete commonHeaders.Authorization; }; export const exchangeFlarumToken = async (payload: { flarumUserId: string; flarumToken: string; }): Promise => { const response = await backend.post("/auth/flarum", { flarum_user_id: payload.flarumUserId, flarum_token: payload.flarumToken, }); const data = asApiRecord(response.data); return { accessToken: String(data.access_token || ""), tokenType: "bearer", user: toUser(asApiRecord(data.user)), }; }; export const fetchMe = async (): Promise => { const response = await backend.get("/me"); return toUser(asApiRecord(response.data)); }; export const fetchRatingSummary = async ( appKey: string, ): Promise => { const response = await backend.get( `/apps/${encodeURIComponent(appKey)}/rating-summary`, ); const data = asApiRecord(response.data); return { averageRating: Number(data.average_rating || 0), reviewCount: Number(data.review_count || 0), starCounts: Object.fromEntries( Object.entries(asApiRecord(data.star_counts)).map(([key, value]) => [ Number(key), Number(value), ]), ), }; }; export const fetchReviews = async (appKey: string): Promise => { const response = await backend.get( `/apps/${encodeURIComponent(appKey)}/reviews`, ); return asApiRecordArray(response.data).map(toReview); }; export const submitReview = async ( appKey: string, payload: { rating: number; content: string; tags: ReviewTags }, ): Promise => { const response = await backend.post( `/apps/${encodeURIComponent(appKey)}/reviews`, { rating: payload.rating, content: payload.content, tags: { origin: payload.tags.origin, category: payload.tags.category, pkgname: payload.tags.pkgname, version: payload.tags.version, package_arch: payload.tags.packageArch, client_arch: payload.tags.clientArch, distro: payload.tags.distro, }, }, ); return toReview(asApiRecord(response.data)); }; export const listFavoriteFolders = async (): Promise => { const response = await backend.get("/me/favorite-folders"); return asApiRecordArray(response.data).map(toFavoriteFolder); }; export const createFavoriteFolder = async ( name: string, ): Promise => { const response = await backend.post("/me/favorite-folders", { name }); return toFavoriteFolder(asApiRecord(response.data)); }; export const renameFavoriteFolder = async ( folderId: number, name: string, ): Promise => { const response = await backend.patch(`/me/favorite-folders/${folderId}`, { name, }); return toFavoriteFolder(asApiRecord(response.data)); }; export const deleteFavoriteFolder = async (folderId: number): Promise => { await backend.delete(`/me/favorite-folders/${folderId}`); }; export const listFavoriteItems = async ( folderId: number, ): Promise => { const response = await backend.get(`/me/favorite-folders/${folderId}/items`); return asApiRecordArray(response.data).map(toFavoriteItem); }; export const addFavoriteItem = async ( folderId: number | "default", item: Omit, ): Promise => { const response = await backend.post( `/me/favorite-folders/${folderId}/items`, { app_key: item.appKey, pkgname: item.pkgname, name: item.name, category: item.category, icon_url: item.iconUrl, }, ); return toFavoriteItem(asApiRecord(response.data)); }; export const deleteFavoriteItem = async ( folderId: number, itemId: number, ): Promise => { await backend.delete(`/me/favorite-folders/${folderId}/items/${itemId}`); }; export const bulkDeleteFavoriteItems = async ( folderId: number, itemIds: number[], ): Promise => { const response = await backend.post( `/me/favorite-folders/${folderId}/items/bulk-delete`, { item_ids: itemIds }, ); return Number(asApiRecord(response.data).deleted_count || 0); }; export const listDownloadedApps = async ( page = 1, pageSize = 20, ): Promise => { const response = await backend.get("/me/downloaded-apps", { params: { page, page_size: pageSize }, }); const data = asApiRecord(response.data); return { items: asApiRecordArray(data.items).map(toDownloadedApp), total: Number(data.total || 0), page: Number(data.page || page), pageSize: Number(data.page_size || pageSize), }; }; export const recordDownloadedApp = async ( item: Omit, ): Promise => { const response = await backend.post("/me/downloaded-apps", { app_key: item.appKey, pkgname: item.pkgname, name: item.name, category: item.category, selected_origin: item.selectedOrigin, version: item.version, package_arch: item.packageArch, }); return toDownloadedApp(asApiRecord(response.data)); }; export const fetchSyncedAppList = async (): Promise => { const response = await backend.get("/me/app-list"); if (!response.data) return null; return toSyncedAppList(asApiRecord(response.data)); }; export const uploadSyncedAppList = async (payload: { clientArch: string; distro: string; items: SyncedAppListItem[]; }): Promise => { const response = await backend.put("/me/app-list", { client_arch: payload.clientArch, distro: payload.distro, items: payload.items.map((item) => ({ pkgname: item.pkgname, origin: item.origin, category: item.category, version: item.version, package_arch: item.packageArch, app_name: item.appName, icon_url: item.iconUrl, })), }); return toSyncedAppList(asApiRecord(response.data), payload); };