import { Model } from '../types/models'; import { authService } from './authService'; // API Base URL - nginx 反代模式下为空字符串(走相对路径 /api/) const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? ''; // API 响应类型定义 export interface ApiResponse { code: number; message: string; data: T; } // 后端API响应类型(对应新表 models_new) export interface BackendModelResponse { id: number; model_code: string; display_name: string | null; img: string | null; tag1: string | null; tag2: string | null; description: string | null; custom_description: string | null; keyword: string | null; keywords: string | null; categories: number[]; category: number; supplier: string; is_featured: boolean; is_api_enabled: boolean; is_show_enabled: boolean; created_at: string; updated_at: string; // 前端兼容字段(后端 model_service 里有映射) title: string; name: string; time: string | null; price_id: number | null; pricing_mode: string | null; input_price: string | null; output_price: string | null; price_unit: string | null; price_currency: string | null; price_tiers: any[]; } // 精选模型列表响应类型 export interface FeaturedModelsResponse { items: BackendModelResponse[]; } export interface ParsedPricingResponse { model_code: string; model_intro?: string | null; model_tags?: string[] | null; model_capabilities?: any | null; model_pricing?: any | null; tool_call_pricing?: any | null; model_limits?: any | null; api_examples?: any | null; is_api_enabled?: boolean; categories?: number[]; } export interface ParsedPricingMappingItem { source_key: string; normalized_key: string; model_code: string; } export interface ParsedPricingMappingResponse { items: ParsedPricingMappingItem[]; } export interface BackendPaginatedResponse { total: number; page: number; page_size: number; items: BackendModelResponse[]; } export interface Pagination { page: number; pageSize: number; total: number; totalPages: number; } export interface ModelListResponse { list: Model[]; pagination: Pagination; } export interface ModelDetailResponse extends Model { apiEndpoint?: string; apiDocumentation?: string; pricing?: { type: string; unit: string; price: number; }; capabilities?: string[]; limitations?: string[]; } // 请求参数接口 export interface GetModelsParams { keyword?: string; page?: number; pageSize?: number; category?: string; supplier?: string; // 供应商筛选(对应后端 supplier 参数) group_name?: string; // 模型分组筛选(对应后端 group_name 参数) filter_keyword?: string; // 关键字筛选(对应后端 filter_keyword 参数) filter_tag?: string; // 标签筛选(对应后端 filter_tag 参数) is_api_enabled?: boolean; // 是否支持API调用筛选 // 以下为前端本地筛选参数 isNew?: boolean; isHot?: boolean; sortBy?: string; order?: 'asc' | 'desc'; } // 模型分类映射到分类字符串(支持多分类,取优先级最高的) const CATEGORY_NAMES: Record = { 0: '语言模型', 1: '多模态模型', 2: 'TTS模型', 3: 'STT模型', 4: '生图模型', 5: '生视频模型', 6: '图像编辑', 7: 'Embedding', 8: 'Rerank' }; const getModelCategory = (categories: number[], category?: number): string => { const cats = categories?.length ? categories : (category !== undefined ? [category] : [0]); const priority = [6, 5, 4, 3, 2, 8, 7, 1, 0]; const main = priority.find(c => cats.includes(c)) ?? cats[0] ?? 0; return CATEGORY_NAMES[main] || '语言模型'; }; // 前端分类名称映射到后端 category 数值 const categoryNameToNumber: Record = { '语言模型': 0, '多模态模型': 1, 'TTS模型': 2, 'STT模型': 3, '生图模型': 4, '生视频模型': 5, '图像编辑': 6, 'Embedding': 7, 'Rerank': 8 }; // 将后端模型数据转换为前端Model格式(字段与 Model 类型对齐) function convertBackendModelToModel(backendModel: BackendModelResponse): Model { const cats = backendModel.categories?.length ? backendModel.categories : [backendModel.category ?? 0]; return { id: backendModel.id, title: backendModel.model_code, img: backendModel.img || '', name: backendModel.display_name || backendModel.model_code, tag1: backendModel.tag1, description: backendModel.custom_description || backendModel.description, keyword: backendModel.keywords || backendModel.keyword, time: null, tag2: backendModel.tag2, category: cats[0], categories: cats, supplier: backendModel.supplier, price_id: null, is_featured: backendModel.is_featured, is_api_enabled: backendModel.is_api_enabled, created_at: backendModel.created_at, updated_at: backendModel.updated_at, pricing_mode: null, input_price: null, output_price: null, price_unit: null, price_currency: null, price_tiers: [], }; } // 真实 API 调用 class ModelApiService { private baseUrl = `${API_BASE_URL}/api/models`; /** * 获取请求头(包含鉴权信息) * @returns 请求头对象 */ private getHeaders(): Record { const headers: Record = { 'Content-Type': 'application/json' }; // 始终添加 Authorization header(如果有token) const authHeader = authService.getAuthHeader(); if (authHeader) { headers['Authorization'] = authHeader; } return headers; } /** * 处理 API 响应,检查鉴权错误 * @param response 响应对象 */ private handleAuthError(response: ApiResponse): void { if (response.code === 401) { // Token 过期或无效,清除本地存储并重定向到登录页 authService.logout(); // 实际项目中可以触发路由跳转到登录页 if (typeof window !== 'undefined') { console.warn('Token 已过期,请重新登录'); // window.location.href = '/login'; } } } /** * 发送HTTP请求 * @param url 请求URL * @param options 请求选项 * @returns Promise */ private async fetchApi(url: string, options: RequestInit = {}): Promise { const response = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...options.headers, }, }); if (!response.ok) { if (response.status === 401) { authService.logout(); throw new Error('未授权:请先登录'); } if (response.status === 404) { throw new Error('资源不存在'); } throw new Error(`HTTP错误: ${response.status}`); } return response; } /** * 获取模型列表 * API: GET /api/models * @param params 查询参数 */ async getModels(params: GetModelsParams = {}): Promise> { try { // 构建查询参数 - 使用后端API支持的参数 const queryParams = new URLSearchParams(); if (params.page) { queryParams.append('page', params.page.toString()); } if (params.pageSize) { queryParams.append('page_size', params.pageSize.toString()); } if (params.keyword) { queryParams.append('keyword', params.keyword); } // 分类筛选 - 直接传给后端处理 if (params.category && params.category !== '全部') { queryParams.append('category', params.category); } // 供应商筛选 - 后端支持精确匹配 if (params.supplier) { queryParams.append('supplier', params.supplier); } // 模型分组筛选 - 后端支持精确匹配 if (params.group_name) { queryParams.append('group_name', params.group_name); } // 关键字筛选 - 后端支持精确匹配 keyword 字段 if (params.filter_keyword) { queryParams.append('filter_keyword', params.filter_keyword); } // 标签筛选 - 后端支持在 tag1 或 tag2 中精确匹配 if (params.filter_tag) { queryParams.append('filter_tag', params.filter_tag); } // API支持筛选 if (params.is_api_enabled !== undefined) { queryParams.append('is_api_enabled', params.is_api_enabled.toString()); } const url = `${this.baseUrl}?${queryParams.toString()}`; const headers = this.getHeaders(); const response = await this.fetchApi(url, { method: 'GET', headers, }); const apiResponse: ApiResponse = await response.json(); // 检查鉴权错误 this.handleAuthError(apiResponse); if (apiResponse.code !== 200) { throw new Error(apiResponse.message || '获取模型列表失败'); } // 转换后端数据格式为前端格式 const convertedModels = apiResponse.data.items.map(convertBackendModelToModel); // 前端本地筛选(后端不支持的筛选条件) let filteredModels = [...convertedModels]; // 最新模型筛选:基于 tag1 / tag2 推断 if (params.isNew !== undefined) { filteredModels = filteredModels.filter(m => params.isNew ? (m.tag1 === 'New' || m.tag2 === 'New') : !(m.tag1 === 'New' || m.tag2 === 'New') ); } // 热门模型筛选:基于 is_featured 字段 if (params.isHot !== undefined) { filteredModels = filteredModels.filter(m => params.isHot ? m.is_featured : !m.is_featured ); } // 排序 const sortBy = params.sortBy || 'createdAt'; const order = params.order || 'desc'; filteredModels.sort((a, b) => { if (sortBy === 'name') { return order === 'asc' ? a.name.localeCompare(b.name) : b.name.localeCompare(a.name); } // 默认按创建时间排序 return order === 'asc' ? new Date(a.created_at).getTime() - new Date(b.created_at).getTime() : new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); }); // 使用后端返回的分页信息 const page = apiResponse.data.page; const pageSize = apiResponse.data.page_size; // 如果有前端本地筛选,需要重新计算 total const hasLocalFilter = params.isNew !== undefined || params.isHot !== undefined; const total = hasLocalFilter ? filteredModels.length : apiResponse.data.total; const totalPages = Math.ceil(total / pageSize); // 如果进行了前端本地筛选,需要重新分页 let paginatedModels = filteredModels; if (hasLocalFilter) { const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; paginatedModels = filteredModels.slice(startIndex, endIndex); } return { code: 200, message: 'success', data: { list: paginatedModels, pagination: { page, pageSize, total, totalPages } } }; } catch (error) { console.error('Failed to fetch models:', error); throw error; } } /** * 获取 parsed_json 中的模型定价信息 * API: GET /api/models/pricing/{model_code} * @param modelCode 模型代码 */ async getParsedPricing(modelCode: string): Promise> { const url = `${this.baseUrl}/pricing/${encodeURIComponent(modelCode)}`; const response = await this.fetchApi(url, { method: 'GET', headers: this.getHeaders() }); const data = await response.json(); this.handleAuthError(data); return data; } /** * 获取模型定价映射关系 * API: GET /api/models/pricing/mapping */ async getParsedPricingMapping(): Promise> { const url = `${this.baseUrl}/pricing/mapping`; const response = await this.fetchApi(url, { method: 'GET', headers: this.getHeaders() }); const data = await response.json(); this.handleAuthError(data); return data; } /** * 获取模型详情 * API: GET /api/models/{modelId} * @param modelId 模型 ID(数字ID) */ async getModelDetail(modelId: number): Promise> { try { const url = `${this.baseUrl}/${modelId}`; const headers = this.getHeaders(); const response = await this.fetchApi(url, { method: 'GET', headers, }); const apiResponse: ApiResponse = await response.json(); // 检查鉴权错误 this.handleAuthError(apiResponse); if (apiResponse.code !== 200) { throw new Error(apiResponse.message || '获取模型详情失败'); } // 转换后端数据格式为前端格式 const model = convertBackendModelToModel(apiResponse.data); const category = getModelCategory(model.category); // 扩展模型详情信息 const detail: ModelDetailResponse = { ...model, apiEndpoint: `${API_BASE_URL}/api/models/${modelId}`, apiDocumentation: `${API_BASE_URL}/docs`, pricing: { type: 'usage', unit: 'token', price: 0.001 }, capabilities: [ `${category}生成`, `${category}理解`, '多语言支持' ], limitations: [ '最大上下文长度:128K tokens', '不支持图像输入' ] }; return { code: 200, message: 'success', data: detail }; } catch (error) { console.error('Failed to fetch model detail:', error); throw error; } } /** * 获取模型分类统计 * 通过获取所有模型并统计分类 */ async getCategoryStatistics(): Promise>> { try { // 获取所有模型(使用较大的page_size) const response = await this.getModels({ page: 1, pageSize: 1000 }); const allModels = response.data.list; const hasCategory = (m: Model, c: number) => { const cats: number[] = (m as any).categories?.length ? (m as any).categories : [m.category]; return cats.includes(c); }; const statistics: Record = { '全部': response.data.pagination.total, '语言模型': allModels.filter(m => hasCategory(m, 0)).length, '多模态模型': allModels.filter(m => hasCategory(m, 1)).length, 'TTS模型': allModels.filter(m => hasCategory(m, 2)).length, 'STT模型': allModels.filter(m => hasCategory(m, 3)).length, '生图模型': allModels.filter(m => hasCategory(m, 4)).length, '生视频模型': allModels.filter(m => hasCategory(m, 5)).length, '图像编辑': allModels.filter(m => hasCategory(m, 6)).length, 'Embedding': allModels.filter(m => hasCategory(m, 7)).length, 'Rerank': allModels.filter(m => hasCategory(m, 8)).length, }; return { code: 200, message: 'success', data: statistics }; } catch (error) { console.error('Failed to fetch category statistics:', error); throw error; } } /** * 获取精选模型列表 * API: GET /api/models/featured/list * @param limit 返回数量,范围1-10,默认3 */ async getFeaturedModels(limit: number = 3): Promise> { try { const queryParams = new URLSearchParams(); queryParams.append('limit', limit.toString()); const url = `${this.baseUrl}/featured/list?${queryParams.toString()}`; const response = await this.fetchApi(url, { method: 'GET', }); const apiResponse: ApiResponse = await response.json(); if (apiResponse.code !== 200) { throw new Error(apiResponse.message || '获取精选模型列表失败'); } // 转换后端数据格式为前端格式 const convertedModels = apiResponse.data.items.map(convertBackendModelToModel); return { code: 200, message: 'success', data: { items: convertedModels } }; } catch (error) { console.error('Failed to fetch featured models:', error); throw error; } } /** * 获取所有模型分组名称(去重排序) * API: GET /api/models/group-names/list */ async getGroupNames(): Promise> { try { const url = `${this.baseUrl}/group-names/list`; const response = await this.fetchApi(url, { method: 'GET', }); const apiResponse: ApiResponse = await response.json(); if (apiResponse.code !== 200) { throw new Error(apiResponse.message || '获取分组列表失败'); } return apiResponse; } catch (error) { console.error('Failed to fetch group names:', error); throw error; } } } // 导出单例 export const modelApi = new ModelApiService(); let pricingMappingCache: Map | null = null; let pricingMappingPromise: Promise> | null = null; export const getPricingMappingLookup = async (): Promise> => { if (pricingMappingCache) { return pricingMappingCache; } if (pricingMappingPromise) { return pricingMappingPromise; } pricingMappingPromise = modelApi.getParsedPricingMapping() .then((response) => { const mapping = new Map(); if (response?.data?.items) { response.data.items.forEach((item) => { if (item.normalized_key && item.model_code) { mapping.set(item.normalized_key, item.model_code); } if (item.source_key && item.model_code) { mapping.set(item.source_key, item.model_code); } }); } pricingMappingCache = mapping; return mapping; }) .catch((error) => { pricingMappingPromise = null; throw error; }); return pricingMappingPromise; };