• Added `.eslintrc.cjs` - Enables `header` + `react-hooks` plugins - Inserts standardized AGPL-3.0 license banner for © 2025 QuantumNous - JS/JSX parsing & JSX support configured • Installed dev-deps: `eslint`, `eslint-plugin-header`, `eslint-plugin-react-hooks` • Updated `web/package.json` scripts - `eslint` → lint with cache - `eslint:fix` → auto-insert/repair license headers • Executed `eslint --fix` to prepend license banner to all JS/JSX files • Ignored runtime cache - Added `.eslintcache` to `.gitignore` & `.dockerignore` Result: consistent AGPL-3.0 license headers, reproducible linting across local dev & CI.
273 lines
7.3 KiB
JavaScript
273 lines
7.3 KiB
JavaScript
/*
|
|
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 <https://www.gnu.org/licenses/>.
|
|
|
|
For commercial licensing, please contact support@quantumnous.com
|
|
*/
|
|
|
|
import { getUserIdFromLocalStorage, showError, formatMessageForAPI, isValidMessage } from './utils';
|
|
import axios from 'axios';
|
|
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,
|
|
group: inputs.group,
|
|
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]) => {
|
|
if (parameterEnabled[key] && inputs[param] !== undefined && inputs[param] !== null) {
|
|
payload[param] = inputs[param];
|
|
}
|
|
});
|
|
|
|
return payload;
|
|
};
|
|
|
|
// 处理API错误响应
|
|
export const handleApiError = (error, response = null) => {
|
|
const errorInfo = {
|
|
error: error.message || '未知错误',
|
|
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 = '服务器返回了错误状态码';
|
|
} else if (error.message.includes('Failed to fetch')) {
|
|
errorInfo.details = '网络连接失败或服务器无响应';
|
|
}
|
|
|
|
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: '用户分组',
|
|
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 '';
|
|
}
|
|
}
|
|
|
|
export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
|
|
const state = await getOAuthState();
|
|
if (!state) return;
|
|
const redirect_uri = `${window.location.origin}/oauth/oidc`;
|
|
const response_type = 'code';
|
|
const scope = 'openid profile email';
|
|
const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
|
if (openInNewTab) {
|
|
window.open(url);
|
|
} else {
|
|
window.location.href = url;
|
|
}
|
|
}
|
|
|
|
export async function onGitHubOAuthClicked(github_client_id) {
|
|
const state = await getOAuthState();
|
|
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) {
|
|
const state = await getOAuthState();
|
|
if (!state) return;
|
|
window.open(
|
|
`https://connect.linux.do/oauth2/authorize?response_type=code&client_id=${linuxdo_client_id}&state=${state}`,
|
|
);
|
|
}
|
|
|
|
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 [];
|
|
}
|