/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { getUserIdFromLocalStorage, showError, formatMessageForAPI, isValidMessage, } from './utils'; import axios from 'axios'; import i18next from 'i18next'; import { MESSAGE_ROLES } from '../constants/playground.constants'; export let API = axios.create({ baseURL: import.meta.env.VITE_REACT_APP_SERVER_URL ? import.meta.env.VITE_REACT_APP_SERVER_URL : '', headers: { 'New-API-User': getUserIdFromLocalStorage(), 'Cache-Control': 'no-store', }, }); function patchAPIInstance(instance) { const originalGet = instance.get.bind(instance); const inFlightGetRequests = new Map(); const genKey = (url, config = {}) => { const params = config.params ? JSON.stringify(config.params) : '{}'; return `${url}?${params}`; }; instance.get = (url, config = {}) => { if (config?.disableDuplicate) { return originalGet(url, config); } const key = genKey(url, config); if (inFlightGetRequests.has(key)) { return inFlightGetRequests.get(key); } const reqPromise = originalGet(url, config).finally(() => { inFlightGetRequests.delete(key); }); inFlightGetRequests.set(key, reqPromise); return reqPromise; }; } patchAPIInstance(API); export function updateAPI() { API = axios.create({ baseURL: import.meta.env.VITE_REACT_APP_SERVER_URL ? import.meta.env.VITE_REACT_APP_SERVER_URL : '', headers: { 'New-API-User': getUserIdFromLocalStorage(), 'Cache-Control': 'no-store', }, }); patchAPIInstance(API); } API.interceptors.response.use( (response) => response, (error) => { // 如果请求配置中显式要求跳过全局错误处理,则不弹出默认错误提示 if (error.config && error.config.skipErrorHandler) { return Promise.reject(error); } showError(error); return Promise.reject(error); }, ); // playground // 构建API请求负载 export const buildApiPayload = ( messages, systemPrompt, inputs, parameterEnabled, ) => { const processedMessages = messages .filter(isValidMessage) .map(formatMessageForAPI) .filter(Boolean); // 如果有系统提示,插入到消息开头 if (systemPrompt && systemPrompt.trim()) { processedMessages.unshift({ role: MESSAGE_ROLES.SYSTEM, content: systemPrompt.trim(), }); } const payload = { model: inputs.model, group: inputs.group, messages: processedMessages, stream: inputs.stream, }; // 添加启用的参数 const parameterMappings = { temperature: 'temperature', top_p: 'top_p', max_tokens: 'max_tokens', frequency_penalty: 'frequency_penalty', presence_penalty: 'presence_penalty', seed: 'seed', }; Object.entries(parameterMappings).forEach(([key, param]) => { const enabled = parameterEnabled[key]; const value = inputs[param]; const hasValue = value !== undefined && value !== null; if (enabled && hasValue) { payload[param] = value; } }); return payload; }; // 处理API错误响应 export const handleApiError = (error, response = null) => { const errorInfo = { error: error.message || i18next.t('未知错误'), timestamp: new Date().toISOString(), stack: error.stack, }; if (response) { errorInfo.status = response.status; errorInfo.statusText = response.statusText; } if (error.message.includes('HTTP error')) { errorInfo.details = i18next.t('服务器返回了错误状态码'); } else if (error.message.includes('Failed to fetch')) { errorInfo.details = i18next.t('网络连接失败或服务器无响应'); } return errorInfo; }; // 处理模型数据 export const processModelsData = (data, currentModel) => { const modelOptions = data.map((model) => ({ label: model, value: model, })); const hasCurrentModel = modelOptions.some( (option) => option.value === currentModel, ); const selectedModel = hasCurrentModel && modelOptions.length > 0 ? currentModel : modelOptions[0]?.value; return { modelOptions, selectedModel }; }; // 处理分组数据 export const processGroupsData = (data, userGroup) => { let groupOptions = Object.entries(data).map(([group, info]) => ({ label: info.desc.length > 20 ? info.desc.substring(0, 20) + '...' : info.desc, value: group, ratio: info.ratio, fullLabel: info.desc, })); if (groupOptions.length === 0) { groupOptions = [ { label: i18next.t('用户分组'), value: '', ratio: 1, }, ]; } else if (userGroup) { const userGroupIndex = groupOptions.findIndex((g) => g.value === userGroup); if (userGroupIndex > -1) { const userGroupOption = groupOptions.splice(userGroupIndex, 1)[0]; groupOptions.unshift(userGroupOption); } } return groupOptions; }; // 原来components中的utils.js export async function getOAuthState() { let path = '/api/oauth/state'; let affCode = localStorage.getItem('aff'); if (affCode && affCode.length > 0) { path += `?aff=${affCode}`; } const res = await API.get(path); const { success, message, data } = res.data; if (success) { return data; } else { showError(message); return ''; } } async function prepareOAuthState(options = {}) { const { shouldLogout = false } = options; if (shouldLogout) { try { await API.get('/api/user/logout', { skipErrorHandler: true }); } catch (err) {} localStorage.removeItem('user'); updateAPI(); } return await getOAuthState(); } export async function onDiscordOAuthClicked(client_id, options = {}) { const state = await prepareOAuthState(options); if (!state) return; const redirect_uri = `${window.location.origin}/oauth/discord`; const response_type = 'code'; const scope = 'identify+openid'; window.open( `https://discord.com/oauth2/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`, ); } export async function onOIDCClicked( auth_url, client_id, openInNewTab = false, options = {}, ) { const state = await prepareOAuthState(options); if (!state) return; const url = new URL(auth_url); url.searchParams.set('client_id', client_id); url.searchParams.set('redirect_uri', `${window.location.origin}/oauth/oidc`); url.searchParams.set('response_type', 'code'); url.searchParams.set('scope', 'openid profile email'); url.searchParams.set('state', state); if (openInNewTab) { window.open(url.toString(), '_blank'); } else { window.location.href = url.toString(); } } export async function onGitHubOAuthClicked(github_client_id, options = {}) { const state = await prepareOAuthState(options); if (!state) return; window.open( `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`, ); } export async function onLinuxDOOAuthClicked( linuxdo_client_id, options = { shouldLogout: false }, ) { const state = await prepareOAuthState(options); if (!state) return; window.open( `https://connect.linux.do/oauth2/authorize?response_type=code&client_id=${linuxdo_client_id}&state=${state}`, ); } /** * Initiate custom OAuth login * @param {Object} provider - Custom OAuth provider config from status API * @param {string} provider.slug - Provider slug (used for callback URL) * @param {string} provider.client_id - OAuth client ID * @param {string} provider.authorization_endpoint - Authorization URL * @param {string} provider.scopes - OAuth scopes (space-separated) * @param {Object} options - Options * @param {boolean} options.shouldLogout - Whether to logout first */ export async function onCustomOAuthClicked(provider, options = {}) { const state = await prepareOAuthState(options); if (!state) return; try { const redirect_uri = `${window.location.origin}/oauth/${provider.slug}`; // Check if authorization_endpoint is a full URL or relative path let authUrl; if ( provider.authorization_endpoint.startsWith('http://') || provider.authorization_endpoint.startsWith('https://') ) { authUrl = new URL(provider.authorization_endpoint); } else { // Relative path - this is a configuration error, show error message console.error( 'Custom OAuth authorization_endpoint must be a full URL:', provider.authorization_endpoint, ); showError( i18next.t('OAuth 配置错误:授权端点必须是完整的 URL(以 http:// 或 https:// 开头)'), ); return; } authUrl.searchParams.set('client_id', provider.client_id); authUrl.searchParams.set('redirect_uri', redirect_uri); authUrl.searchParams.set('response_type', 'code'); authUrl.searchParams.set( 'scope', provider.scopes || 'openid profile email', ); authUrl.searchParams.set('state', state); window.open(authUrl.toString()); } catch (error) { console.error('Failed to initiate custom OAuth:', error); showError(i18next.t('OAuth 登录失败:') + (error.message || i18next.t('未知错误'))); } } let channelModels = undefined; export async function loadChannelModels() { const res = await API.get('/api/models'); const { success, data } = res.data; if (!success) { return; } channelModels = data; localStorage.setItem('channel_models', JSON.stringify(data)); } export function getChannelModels(type) { if (channelModels !== undefined && type in channelModels) { if (!channelModels[type]) { return []; } return channelModels[type]; } let models = localStorage.getItem('channel_models'); if (!models) { return []; } channelModels = JSON.parse(models); if (type in channelModels) { return channelModels[type]; } return []; } // 渠道定价 API export const channelPricingApi = { // 获取所有渠道定价 getAll: (page = 1, pageSize = 10) => API.get(`/api/channel_pricing/?p=${page}&page_size=${pageSize}`), // 获取渠道定价(带标签) getWithTags: (page = 1, pageSize = 10) => API.get(`/api/channel_pricing/with_tags?p=${page}&page_size=${pageSize}`), // 获取指定模型的渠道定价 getByModel: (modelName) => API.get(`/api/channel_pricing/model/${modelName}`), // 创建/更新渠道定价 create: (data) => API.post('/api/channel_pricing/', data), // 批量创建 batchCreate: (items) => API.post('/api/channel_pricing/batch', { items }), // 删除 delete: (id) => API.delete(`/api/channel_pricing/${id}`), // 复制全局定价 copyGlobal: (channelId, overwrite = false) => API.post(`/api/channel_pricing/copy_global/${channelId}`, { overwrite }), // 设置默认通道 setDefault: (modelName, channelId) => API.post('/api/channel_pricing/set_default', { model_name: modelName, channel_id: channelId }), // 清除默认通道 clearDefault: (modelName) => API.delete(`/api/channel_pricing/default/${encodeURIComponent(modelName)}`), }; // 定价标签 API export const pricingTagApi = { getAll: () => API.get('/api/pricing_tag/'), create: (data) => API.post('/api/pricing_tag/', data), update: (id, data) => API.put(`/api/pricing_tag/${id}`, data), delete: (id) => API.delete(`/api/pricing_tag/${id}`), }; // 用户模型限流 API export const userRateLimitApi = { get: (userId) => API.get(`/api/user/${userId}/rate-limits`), set: (userId, items) => API.put(`/api/user/${userId}/rate-limits`, items), };