| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603 |
- 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<T> {
- 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<number, string> = {
- 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<string, number> = {
- '语言模型': 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<string, string> {
- const headers: Record<string, string> = {
- '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<any>): 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<Response>
- */
- private async fetchApi(url: string, options: RequestInit = {}): Promise<Response> {
- 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<ApiResponse<ModelListResponse>> {
- 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<BackendPaginatedResponse> = 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<ApiResponse<ParsedPricingResponse>> {
- 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<ApiResponse<ParsedPricingMappingResponse>> {
- 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<ApiResponse<ModelDetailResponse>> {
- try {
- const url = `${this.baseUrl}/${modelId}`;
- const headers = this.getHeaders();
-
- const response = await this.fetchApi(url, {
- method: 'GET',
- headers,
- });
- const apiResponse: ApiResponse<BackendModelResponse> = 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<ApiResponse<Record<string, number>>> {
- 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<string, number> = {
- '全部': 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<ApiResponse<{ items: Model[] }>> {
- 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<FeaturedModelsResponse> = 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<ApiResponse<string[]>> {
- try {
- const url = `${this.baseUrl}/group-names/list`;
-
- const response = await this.fetchApi(url, {
- method: 'GET',
- });
- const apiResponse: ApiResponse<string[]> = 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<string, string> | null = null;
- let pricingMappingPromise: Promise<Map<string, string>> | null = null;
- export const getPricingMappingLookup = async (): Promise<Map<string, string>> => {
- if (pricingMappingCache) {
- return pricingMappingCache;
- }
- if (pricingMappingPromise) {
- return pricingMappingPromise;
- }
- pricingMappingPromise = modelApi.getParsedPricingMapping()
- .then((response) => {
- const mapping = new Map<string, string>();
- 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;
- };
|