- 添加动态提供商配置组功能,支持通过后缀创建自定义配置组 - 实现前缀匹配机制,使自定义配置组能继承基础类型的配置和模型 - 更新代理、TLS Sidecar、健康检查等系统以支持前缀匹配 - 添加提供商组管理界面,支持创建和显示自定义配置组 - 改进提供商状态API,支持显示所有配置组及其状态 - 升级axios依赖至v1.14.0以获取安全更新
574 lines
20 KiB
JavaScript
574 lines
20 KiB
JavaScript
// 路径路由示例功能模块
|
||
|
||
import { showToast } from './utils.js';
|
||
import { t } from './i18n.js';
|
||
|
||
/**
|
||
* 初始化路径路由示例功能
|
||
*/
|
||
function initRoutingExamples() {
|
||
// 延迟初始化,确保所有DOM都加载完成
|
||
setTimeout(() => {
|
||
initProtocolTabs();
|
||
initCopyButtons();
|
||
initCardInteractions();
|
||
}, 100);
|
||
}
|
||
|
||
/**
|
||
* 初始化协议标签切换功能
|
||
*/
|
||
function initProtocolTabs() {
|
||
// 使用事件委托方式绑定点击事件
|
||
document.addEventListener('click', function(e) {
|
||
// 检查点击的是不是协议标签或者其子元素
|
||
const tab = e.target.classList.contains('protocol-tab') ? e.target : e.target.closest('.protocol-tab');
|
||
|
||
if (tab) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
|
||
const targetProtocol = tab.dataset.protocol;
|
||
const card = tab.closest('.routing-example-card');
|
||
|
||
if (!card) {
|
||
return;
|
||
}
|
||
|
||
// 移除当前卡片中所有标签和内容的活动状态
|
||
const cardTabs = card.querySelectorAll('.protocol-tab');
|
||
const cardContents = card.querySelectorAll('.protocol-content');
|
||
|
||
cardTabs.forEach(t => t.classList.remove('active'));
|
||
cardContents.forEach(c => c.classList.remove('active'));
|
||
|
||
// 为当前标签和对应内容添加活动状态
|
||
tab.classList.add('active');
|
||
|
||
// 使用更精确的选择器来查找对应的内容
|
||
const targetContent = card.querySelector(`.protocol-content[data-protocol="${targetProtocol}"]`);
|
||
if (targetContent) {
|
||
targetContent.classList.add('active');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 初始化复制按钮功能
|
||
*/
|
||
function initCopyButtons() {
|
||
document.addEventListener('click', async function(e) {
|
||
if (e.target.closest('.copy-btn')) {
|
||
e.stopPropagation();
|
||
|
||
const button = e.target.closest('.copy-btn');
|
||
const path = button.dataset.path;
|
||
if (!path) return;
|
||
|
||
try {
|
||
await navigator.clipboard.writeText(path);
|
||
showToast(t('common.success'), `${t('common.success')}: ${path}`, 'success');
|
||
|
||
// 临时更改按钮图标
|
||
const icon = button.querySelector('i');
|
||
if (icon) {
|
||
const originalClass = icon.className;
|
||
icon.className = 'fas fa-check';
|
||
button.style.color = 'var(--success-color)';
|
||
|
||
setTimeout(() => {
|
||
icon.className = originalClass;
|
||
button.style.color = '';
|
||
}, 2000);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('Failed to copy to clipboard:', error);
|
||
showToast(t('common.error'), t('common.error'), 'error');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 初始化卡片交互功能
|
||
*/
|
||
function initCardInteractions() {
|
||
const routingCards = document.querySelectorAll('.routing-example-card');
|
||
|
||
routingCards.forEach(card => {
|
||
// 添加悬停效果
|
||
card.addEventListener('mouseenter', () => {
|
||
card.style.transform = 'translateY(-4px)';
|
||
card.style.boxShadow = 'var(--shadow-lg)';
|
||
});
|
||
|
||
card.addEventListener('mouseleave', () => {
|
||
card.style.transform = '';
|
||
card.style.boxShadow = '';
|
||
});
|
||
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取所有可用的路由端点
|
||
* @returns {Array} 路由端点数组
|
||
*/
|
||
function getAvailableRoutes() {
|
||
return [
|
||
{
|
||
provider: 'forward-api',
|
||
name: 'NewAPI',
|
||
paths: {
|
||
openai: '/forward-api/v1/chat/completions',
|
||
claude: '/forward-api/v1/messages'
|
||
},
|
||
description: t('dashboard.routing.official'),
|
||
badge: t('dashboard.routing.official'),
|
||
badgeClass: 'official'
|
||
},
|
||
{
|
||
provider: 'claude-custom',
|
||
name: 'Claude Custom',
|
||
paths: {
|
||
openai: '/claude-custom/v1/chat/completions',
|
||
claude: '/claude-custom/v1/messages'
|
||
},
|
||
description: t('dashboard.routing.official'),
|
||
badge: t('dashboard.routing.official'),
|
||
badgeClass: 'official'
|
||
},
|
||
{
|
||
provider: 'claude-kiro-oauth',
|
||
name: 'Claude Kiro OAuth',
|
||
paths: {
|
||
openai: '/claude-kiro-oauth/v1/chat/completions',
|
||
claude: '/claude-kiro-oauth/v1/messages'
|
||
},
|
||
description: t('dashboard.routing.free'),
|
||
badge: t('dashboard.routing.free'),
|
||
badgeClass: 'oauth'
|
||
},
|
||
{
|
||
provider: 'openai-custom',
|
||
name: 'OpenAI Custom',
|
||
paths: {
|
||
openai: '/openai-custom/v1/chat/completions',
|
||
claude: '/openai-custom/v1/messages'
|
||
},
|
||
description: t('dashboard.routing.official'),
|
||
badge: t('dashboard.routing.official'),
|
||
badgeClass: 'official'
|
||
},
|
||
{
|
||
provider: 'gemini-cli-oauth',
|
||
name: 'Gemini CLI OAuth',
|
||
paths: {
|
||
openai: '/gemini-cli-oauth/v1/chat/completions',
|
||
claude: '/gemini-cli-oauth/v1/messages'
|
||
},
|
||
description: t('dashboard.routing.oauth'),
|
||
badge: t('dashboard.routing.oauth'),
|
||
badgeClass: 'oauth'
|
||
},
|
||
{
|
||
provider: 'gemini-antigravity',
|
||
name: 'Gemini Antigravity',
|
||
paths: {
|
||
openai: '/gemini-antigravity/v1/chat/completions',
|
||
claude: '/gemini-antigravity/v1/messages'
|
||
},
|
||
description: t('dashboard.routing.experimental') || '实验性',
|
||
badge: t('dashboard.routing.experimental') || '实验性',
|
||
badgeClass: 'oauth'
|
||
},
|
||
{
|
||
provider: 'openai-qwen-oauth',
|
||
name: 'Qwen OAuth',
|
||
paths: {
|
||
openai: '/openai-qwen-oauth/v1/chat/completions',
|
||
claude: '/openai-qwen-oauth/v1/messages'
|
||
},
|
||
description: 'Qwen Code Plus',
|
||
badge: t('dashboard.routing.oauth'),
|
||
badgeClass: 'oauth'
|
||
},
|
||
{
|
||
provider: 'openai-iflow',
|
||
name: 'iFlow OAuth',
|
||
paths: {
|
||
openai: '/openai-iflow/v1/chat/completions',
|
||
claude: '/openai-iflow/v1/messages'
|
||
},
|
||
description: t('dashboard.routing.oauth'),
|
||
badge: t('dashboard.routing.oauth'),
|
||
badgeClass: 'oauth'
|
||
},
|
||
{
|
||
provider: 'openai-codex-oauth',
|
||
name: 'OpenAI Codex OAuth',
|
||
paths: {
|
||
openai: '/openai-codex-oauth/v1/chat/completions',
|
||
claude: '/openai-codex-oauth/v1/messages'
|
||
},
|
||
description: t('dashboard.routing.oauth'),
|
||
badge: t('dashboard.routing.oauth'),
|
||
badgeClass: 'oauth'
|
||
},
|
||
{
|
||
provider: 'openaiResponses-custom',
|
||
name: 'OpenAI Responses',
|
||
paths: {
|
||
openai: '/openaiResponses-custom/v1/responses',
|
||
claude: '/openaiResponses-custom/v1/messages'
|
||
},
|
||
description: '结构化对话API',
|
||
badge: 'Responses',
|
||
badgeClass: 'responses'
|
||
},
|
||
{
|
||
provider: 'grok-custom',
|
||
name: 'Grok Reverse',
|
||
paths: {
|
||
openai: '/grok-custom/v1/chat/completions',
|
||
claude: '/grok-custom/v1/messages'
|
||
},
|
||
description: t('dashboard.routing.free'),
|
||
badge: t('dashboard.routing.free'),
|
||
badgeClass: 'oauth'
|
||
}
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 高亮显示特定提供商路由
|
||
* @param {string} provider - 提供商标识
|
||
*/
|
||
function highlightProviderRoute(provider) {
|
||
const card = document.querySelector(`[data-provider="${provider}"]`);
|
||
if (card) {
|
||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
card.style.borderColor = 'var(--success-color)';
|
||
card.style.boxShadow = '0 0 0 3px rgba(16, 185, 129, 0.1)';
|
||
|
||
setTimeout(() => {
|
||
card.style.borderColor = '';
|
||
card.style.boxShadow = '';
|
||
}, 3000);
|
||
|
||
showToast(t('common.success'), t('common.success') + `: ${provider}`, 'success');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 复制curl命令示例
|
||
* @param {string} provider - 提供商标识
|
||
* @param {Object} options - 选项参数
|
||
*/
|
||
async function copyCurlExample(provider, options = {}) {
|
||
const routes = getAvailableRoutes();
|
||
const route = routes.find(r => r.provider === provider);
|
||
|
||
if (!route) {
|
||
showToast(t('common.error'), t('common.error'), 'error');
|
||
return;
|
||
}
|
||
|
||
const { protocol = 'openai', model = 'default-model', message = 'Hello!' } = options;
|
||
const path = route.paths[protocol];
|
||
|
||
if (!path) {
|
||
showToast(t('common.error'), t('common.error'), 'error');
|
||
return;
|
||
}
|
||
|
||
let curlCommand = '';
|
||
|
||
// 根据不同提供商和协议生成对应的curl命令
|
||
switch (provider) {
|
||
case 'claude-custom':
|
||
case 'claude-kiro-oauth':
|
||
if (protocol === 'openai') {
|
||
curlCommand = `curl http://localhost:3000${path} \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||
-d '{
|
||
"model": "${model}",
|
||
"messages": [{"role": "user", "content": "${message}"}],
|
||
"max_tokens": 1000
|
||
}'`;
|
||
} else {
|
||
curlCommand = `curl http://localhost:3000${path} \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{
|
||
"model": "${model}",
|
||
"max_tokens": 1000,
|
||
"messages": [{"role": "user", "content": "${message}"}]
|
||
}'`;
|
||
}
|
||
break;
|
||
|
||
case 'openai-custom':
|
||
case 'openai-qwen-oauth':
|
||
if (protocol === 'openai') {
|
||
curlCommand = `curl http://localhost:3000${path} \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||
-d '{
|
||
"model": "${model}",
|
||
"messages": [{"role": "user", "content": "${message}"}],
|
||
"max_tokens": 1000
|
||
}'`;
|
||
} else {
|
||
curlCommand = `curl http://localhost:3000${path} \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "X-API-Key: YOUR_API_KEY" \\
|
||
-d '{
|
||
"model": "${model}",
|
||
"max_tokens": 1000,
|
||
"messages": [{"role": "user", "content": "${message}"}]
|
||
}'`;
|
||
}
|
||
break;
|
||
|
||
case 'gemini-cli-oauth':
|
||
if (protocol === 'openai') {
|
||
curlCommand = `curl http://localhost:3000${path} \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{
|
||
"model": "gemini-3.1-pro-preview",
|
||
"messages": [{"role": "user", "content": "${message}"}],
|
||
"max_tokens": 1000
|
||
}'`;
|
||
} else {
|
||
curlCommand = `curl http://localhost:3000${path} \\
|
||
-H "Content-Type: application/json" \\
|
||
-d '{
|
||
"model": "gemini-3.1-pro-preview",
|
||
"max_tokens": 1000,
|
||
"messages": [{"role": "user", "content": "${message}"}]
|
||
}'`;
|
||
}
|
||
break;
|
||
|
||
case 'openaiResponses-custom':
|
||
if (protocol === 'openai') {
|
||
curlCommand = `curl http://localhost:3000${path} \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||
-d '{
|
||
"model": "${model}",
|
||
"input": "${message}",
|
||
"max_output_tokens": 1000
|
||
}'`;
|
||
} else {
|
||
curlCommand = `curl http://localhost:3000${path} \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "X-API-Key: YOUR_API_KEY" \\
|
||
-d '{
|
||
"model": "${model}",
|
||
"max_tokens": 1000,
|
||
"messages": [{"role": "user", "content": "${message}"}]
|
||
}'`;
|
||
}
|
||
break;
|
||
case 'grok-custom':
|
||
if (protocol === 'openai') {
|
||
curlCommand = `curl http://localhost:3000${path} \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||
-d '{
|
||
"model": "grok-3",
|
||
"messages": [{"role": "user", "content": "${message}"}],
|
||
"stream": true
|
||
}'`;
|
||
} else {
|
||
curlCommand = `curl http://localhost:3000${path} \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "X-API-Key: YOUR_API_KEY" \\
|
||
-d '{
|
||
"model": "grok-3",
|
||
"max_tokens": 1000,
|
||
"messages": [{"role": "user", "content": "${message}"}]
|
||
}'`;
|
||
}
|
||
break;
|
||
}
|
||
|
||
try {
|
||
await navigator.clipboard.writeText(curlCommand);
|
||
showToast(t('common.success'), t('oauth.success.msg'), 'success');
|
||
} catch (error) {
|
||
console.error('Failed to copy curl command:', error);
|
||
showToast(t('common.error'), t('common.error'), 'error');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 动态渲染路径路由示例
|
||
* @param {Array} providerConfigs - 提供商配置列表
|
||
*/
|
||
function renderRoutingExamples(providerConfigs) {
|
||
const container = document.querySelector('.routing-examples-grid');
|
||
if (!container) return;
|
||
|
||
container.innerHTML = '';
|
||
|
||
// 获取路由端点基础信息
|
||
const routes = getAvailableRoutes();
|
||
|
||
// 图标映射
|
||
const iconMap = {
|
||
'forward-api': 'fa-share-square',
|
||
'gemini-cli-oauth': 'fa-gem',
|
||
'gemini-antigravity': 'fa-rocket',
|
||
'openai-custom': 'fa-comments',
|
||
'claude-custom': 'fa-brain',
|
||
'claude-kiro-oauth': 'fa-robot',
|
||
'openai-qwen-oauth': 'fa-code',
|
||
'openaiResponses-custom': 'fa-comment-alt',
|
||
'openai-iflow': 'fa-wind',
|
||
'openai-codex-oauth': 'fa-keyboard',
|
||
'grok-custom': 'fa-search'
|
||
};
|
||
|
||
// 默认模型映射 (用于 curl 示例)
|
||
const modelMap = {
|
||
'gemini-cli-oauth': 'gemini-3-flash-preview',
|
||
'gemini-antigravity': 'gemini-3-flash-preview',
|
||
'claude-custom': 'claude-sonnet-4-6',
|
||
'claude-kiro-oauth': 'claude-sonnet-4-6',
|
||
'openai-custom': 'gpt-4o',
|
||
'openai-qwen-oauth': 'qwen3-coder-plus',
|
||
'openai-iflow': 'qwen3-max',
|
||
'openai-codex-oauth': 'gpt-5',
|
||
'grok-custom': 'grok-3',
|
||
'openaiResponses-custom': 'gpt-4o'
|
||
};
|
||
|
||
providerConfigs.forEach(config => {
|
||
if (config.visible === false) return;
|
||
|
||
let routeInfo = routes.find(r => r.provider === config.id);
|
||
|
||
// 如果没找到,则创建一个默认的,并尝试继承基础类型的徽章
|
||
if (!routeInfo) {
|
||
// 尝试查找基础类型的路由信息以获取徽章
|
||
let baseRouteInfo = null;
|
||
for (const r of routes) {
|
||
if (config.id.startsWith(r.provider + '-')) {
|
||
baseRouteInfo = r;
|
||
break;
|
||
}
|
||
}
|
||
|
||
routeInfo = {
|
||
provider: config.id,
|
||
name: config.name,
|
||
paths: {
|
||
openai: `/${config.id}/v1/chat/completions`,
|
||
claude: `/${config.id}/v1/messages`
|
||
},
|
||
description: baseRouteInfo ? baseRouteInfo.description : t('dashboard.routing.oauth'),
|
||
badge: baseRouteInfo ? baseRouteInfo.badge : t('dashboard.routing.oauth'),
|
||
badgeClass: baseRouteInfo ? baseRouteInfo.badgeClass : 'oauth'
|
||
};
|
||
}
|
||
|
||
// 确定图标:尝试精确匹配,然后尝试前缀匹配
|
||
let icon = iconMap[config.id];
|
||
if (!icon) {
|
||
for (const baseId in iconMap) {
|
||
if (config.id.startsWith(baseId + '-')) {
|
||
icon = iconMap[baseId];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
icon = icon || 'fa-route';
|
||
|
||
// 确定默认模型:尝试精确匹配,然后尝试前缀匹配
|
||
let defaultModel = modelMap[config.id];
|
||
if (!defaultModel) {
|
||
for (const baseId in modelMap) {
|
||
if (config.id.startsWith(baseId + '-')) {
|
||
defaultModel = modelMap[baseId];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
defaultModel = defaultModel || 'default-model';
|
||
const hostname = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' ?
|
||
`http://${window.location.host}` :
|
||
`${window.location.protocol}//${window.location.host}`;
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'routing-example-card';
|
||
card.dataset.provider = `${config.id}-card`;
|
||
|
||
card.innerHTML = `
|
||
<div class="routing-card-header">
|
||
<i class="fas ${icon}"></i>
|
||
<h4>${routeInfo.name}</h4>
|
||
<span class="provider-badge ${routeInfo.badgeClass}">${routeInfo.badge}</span>
|
||
</div>
|
||
<div class="routing-card-content">
|
||
<div class="protocol-tabs">
|
||
<button class="protocol-tab ${config.id === 'openai-codex-oauth' ? '' : 'active'}" data-protocol="openai" data-i18n="dashboard.routing.openai">${t('dashboard.routing.openai')}</button>
|
||
<button class="protocol-tab ${config.id === 'openai-codex-oauth' ? 'active' : ''}" data-protocol="claude" data-i18n="dashboard.routing.claude">${t('dashboard.routing.claude')}</button>
|
||
</div>
|
||
|
||
<div class="protocol-content ${config.id === 'openai-codex-oauth' ? '' : 'active'}" data-protocol="openai">
|
||
<div class="endpoint-info">
|
||
<label data-i18n="dashboard.routing.endpoint">${t('dashboard.routing.endpoint')}</label>
|
||
<code class="endpoint-path">${routeInfo.paths.openai}</code>
|
||
</div>
|
||
<div class="usage-example">
|
||
<label data-i18n="dashboard.routing.exampleOpenAI">${t('dashboard.routing.exampleOpenAI')}</label>
|
||
<pre><code>curl ${hostname}${routeInfo.paths.openai} \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "Authorization: Bearer YOUR_API_KEY" \\
|
||
-d '{
|
||
"model": "${defaultModel}",
|
||
"messages": [{"role": "user", "content": "Hello!"}],
|
||
"max_tokens": 1000
|
||
}'</code></pre>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="protocol-content ${config.id === 'openai-codex-oauth' ? 'active' : ''}" data-protocol="claude">
|
||
<div class="endpoint-info">
|
||
<label data-i18n="dashboard.routing.endpoint">${t('dashboard.routing.endpoint')}</label>
|
||
<code class="endpoint-path">${routeInfo.paths.claude}</code>
|
||
</div>
|
||
<div class="usage-example">
|
||
<label data-i18n="dashboard.routing.exampleClaude">${t('dashboard.routing.exampleClaude')}</label>
|
||
<pre><code>curl ${hostname}${routeInfo.paths.claude} \\
|
||
-H "Content-Type: application/json" \\
|
||
-H "X-API-Key: YOUR_API_KEY" \\
|
||
-d '{
|
||
"model": "${defaultModel}",
|
||
"max_tokens": 1000,
|
||
"messages": [{"role": "user", "content": "Hello!"}]
|
||
}'</code></pre>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.appendChild(card);
|
||
});
|
||
|
||
// 重新初始化卡片交互
|
||
initCardInteractions();
|
||
}
|
||
|
||
export {
|
||
initRoutingExamples,
|
||
getAvailableRoutes,
|
||
highlightProviderRoute,
|
||
copyCurlExample,
|
||
renderRoutingExamples
|
||
};
|