|
|
|
|
const API_BASE = '/api/v1/ai-config';
|
|
|
|
|
|
|
|
|
|
let currentConfig = null;
|
|
|
|
|
let selectedProvider = 'openai';
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
loadProviders();
|
|
|
|
|
loadConfig();
|
|
|
|
|
initEventListeners();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function initEventListeners() {
|
|
|
|
|
document.getElementById('api-provider').addEventListener('change', function() {
|
|
|
|
|
selectedProvider = this.value;
|
|
|
|
|
updateProviderModels();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.getElementById('temperature').addEventListener('input', function() {
|
|
|
|
|
document.getElementById('temp-value').textContent = this.value;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadProviders() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/providers`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
if (data.success) {
|
|
|
|
|
renderProviders(data.data);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载提供商失败:', error);
|
|
|
|
|
renderProviders(getDefaultProviders());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getDefaultProviders() {
|
|
|
|
|
return [
|
|
|
|
|
{ id: 'openai', name: 'OpenAI', icon: 'fas fa-brain' },
|
|
|
|
|
{ id: 'anthropic', name: 'Claude', icon: 'fas fa-robot' },
|
|
|
|
|
{ id: 'google', name: 'Gemini', icon: 'fas fa-gem' },
|
|
|
|
|
{ id: 'aliyun', name: '通义千问', icon: 'fas fa-cloud' },
|
|
|
|
|
{ id: 'aliyun_coding', name: '通义灵码', icon: 'fas fa-code' },
|
|
|
|
|
{ id: 'bailian', name: '阿里百炼', icon: 'fas fa-flask' },
|
|
|
|
|
{ id: 'baidu', name: '文心一言', icon: 'fas fa-comments' },
|
|
|
|
|
{ id: 'zhipu', name: '智谱清言', icon: 'fas fa-lightbulb' }
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderProviders(providers) {
|
|
|
|
|
const grid = document.getElementById('provider-grid');
|
|
|
|
|
const iconMap = {
|
|
|
|
|
'openai': 'fas fa-brain',
|
|
|
|
|
'anthropic': 'fas fa-robot',
|
|
|
|
|
'google': 'fas fa-gem',
|
|
|
|
|
'aliyun': 'fas fa-cloud',
|
|
|
|
|
'aliyun_coding': 'fas fa-code',
|
|
|
|
|
'bailian': 'fas fa-flask',
|
|
|
|
|
'baidu': 'fas fa-comments',
|
|
|
|
|
'zhipu': 'fas fa-lightbulb',
|
|
|
|
|
'custom': 'fas fa-cog'
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
grid.innerHTML = providers.map(p => `
|
|
|
|
|
<div class="provider-card ${p.id === selectedProvider ? 'active' : ''}" data-provider="${p.id}">
|
|
|
|
|
<i class="${iconMap[p.id] || 'fas fa-cog'}"></i>
|
|
|
|
|
<div class="provider-name">${p.name}</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
|
|
|
|
|
grid.querySelectorAll('.provider-card').forEach(card => {
|
|
|
|
|
card.addEventListener('click', function() {
|
|
|
|
|
grid.querySelectorAll('.provider-card').forEach(c => c.classList.remove('active'));
|
|
|
|
|
this.classList.add('active');
|
|
|
|
|
selectedProvider = this.dataset.provider;
|
|
|
|
|
document.getElementById('api-provider').value = selectedProvider;
|
|
|
|
|
updateProviderModels();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateProviderModels() {
|
|
|
|
|
const modelSelect = document.getElementById('model-id');
|
|
|
|
|
const modelMap = {
|
|
|
|
|
'openai': ['gpt-4o', 'gpt-4-turbo', 'gpt-3.5-turbo'],
|
|
|
|
|
'anthropic': ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku'],
|
|
|
|
|
'google': ['gemini-pro', 'gemini-pro-vision'],
|
|
|
|
|
'aliyun': ['qwen-max', 'qwen-plus', 'qwen-turbo'],
|
|
|
|
|
'aliyun_coding': ['qwen-coder-plus', 'qwen-coder-turbo'],
|
|
|
|
|
'bailian': ['qwen3.6-plus', 'qwen3.5-plus', 'qwen3-max', 'qwen3-coder-plus', 'MiniMax-M2.5', 'glm-4.7', 'kimi-k2.5', 'custom'],
|
|
|
|
|
'baidu': ['ernie-4.0', 'ernie-3.5', 'ernie-speed'],
|
|
|
|
|
'zhipu': ['glm-4', 'glm-3-turbo'],
|
|
|
|
|
'custom': ['custom-model']
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const apiBaseMap = {
|
|
|
|
|
'openai': 'https://api.openai.com/v1',
|
|
|
|
|
'anthropic': 'https://api.anthropic.com/v1',
|
|
|
|
|
'google': 'https://generativelanguage.googleapis.com/v1beta',
|
|
|
|
|
'aliyun': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
|
|
|
'aliyun_coding': 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
|
|
|
'bailian': 'https://coding.dashscope.aliyuncs.com/v1',
|
|
|
|
|
'baidu': 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop',
|
|
|
|
|
'zhipu': 'https://open.bigmodel.cn/api/paas/v4',
|
|
|
|
|
'custom': ''
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const models = modelMap[selectedProvider] || ['custom-model'];
|
|
|
|
|
modelSelect.innerHTML = models.map(m => `<option value="${m}">${m}</option>`).join('');
|
|
|
|
|
document.getElementById('api-base').value = apiBaseMap[selectedProvider] || '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadConfig() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(API_BASE);
|
|
|
|
|
const result = await response.json();
|
|
|
|
|
if (result.success && result.data) {
|
|
|
|
|
currentConfig = result.data;
|
|
|
|
|
populateForm(currentConfig);
|
|
|
|
|
renderModelsList(currentConfig.models || []);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('加载配置失败:', error);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function populateForm(config) {
|
|
|
|
|
if (config.models && config.models.length > 0) {
|
|
|
|
|
const activeModel = config.models.find(m => m.enabled) || config.models[0];
|
|
|
|
|
document.getElementById('api-provider').value = activeModel.provider || 'openai';
|
|
|
|
|
document.getElementById('api-key').value = activeModel.api_key || '';
|
|
|
|
|
document.getElementById('api-base').value = activeModel.api_base || '';
|
|
|
|
|
document.getElementById('model-id').value = activeModel.model_id || 'gpt-4o';
|
|
|
|
|
document.getElementById('custom-model').value = '';
|
|
|
|
|
document.getElementById('temperature').value = activeModel.temperature || 0.7;
|
|
|
|
|
document.getElementById('temp-value').textContent = activeModel.temperature || 0.7;
|
|
|
|
|
document.getElementById('max-tokens').value = activeModel.max_tokens || 2000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (config.analysis_settings) {
|
|
|
|
|
document.getElementById('enable-technical').checked = config.analysis_settings.enable_technical_analysis !== false;
|
|
|
|
|
document.getElementById('enable-fundamental').checked = config.analysis_settings.enable_fundamental_analysis === true;
|
|
|
|
|
document.getElementById('enable-sentiment').checked = config.analysis_settings.enable_sentiment_analysis === true;
|
|
|
|
|
document.getElementById('risk-tolerance').value = config.analysis_settings.risk_tolerance || 'medium';
|
|
|
|
|
document.getElementById('max-position').value = config.analysis_settings.max_position_pct || 10;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderModelsList(models) {
|
|
|
|
|
const list = document.getElementById('models-list');
|
|
|
|
|
if (!models || models.length === 0) {
|
|
|
|
|
list.innerHTML = '<div class="empty-state">暂无已保存的模型</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list.innerHTML = models.map((model, index) => `
|
|
|
|
|
<div class="model-item">
|
|
|
|
|
<div class="model-info">
|
|
|
|
|
<div class="model-status ${model.enabled ? 'active' : 'inactive'}"></div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="model-name">${model.model_name || model.model_id}</div>
|
|
|
|
|
<div class="model-provider">${getProviderName(model.provider || model.api_base)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="model-actions">
|
|
|
|
|
${!model.enabled ? `<button class="btn-set-active" onclick="setActiveModel(${index})">设为默认</button>` : '<span class="active-badge">默认</span>'}
|
|
|
|
|
<button class="btn-delete" onclick="deleteModel(${index})"><i class="fas fa-trash"></i></button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getProviderName(apiBase) {
|
|
|
|
|
const map = {
|
|
|
|
|
'openai': 'OpenAI',
|
|
|
|
|
'anthropic': 'Anthropic',
|
|
|
|
|
'google': 'Google',
|
|
|
|
|
'aliyun': '阿里云',
|
|
|
|
|
'aliyun_coding': '阿里云通义灵码',
|
|
|
|
|
'bailian': '阿里百炼',
|
|
|
|
|
'baidu': '百度',
|
|
|
|
|
'zhipu': '智谱'
|
|
|
|
|
};
|
|
|
|
|
return map[apiBase] || apiBase;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleApiKeyVisibility() {
|
|
|
|
|
const input = document.getElementById('api-key');
|
|
|
|
|
const icon = document.querySelector('.toggle-visibility i');
|
|
|
|
|
if (input.type === 'password') {
|
|
|
|
|
input.type = 'text';
|
|
|
|
|
icon.className = 'fas fa-eye-slash';
|
|
|
|
|
} else {
|
|
|
|
|
input.type = 'password';
|
|
|
|
|
icon.className = 'fas fa-eye';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function testConnection() {
|
|
|
|
|
const resultEl = document.getElementById('test-result');
|
|
|
|
|
resultEl.textContent = '测试中...';
|
|
|
|
|
resultEl.className = 'test-result';
|
|
|
|
|
|
|
|
|
|
const customModel = document.getElementById('custom-model').value.trim();
|
|
|
|
|
const selectedModel = document.getElementById('model-id').value;
|
|
|
|
|
const modelId = customModel || (selectedModel === 'custom' ? '' : selectedModel);
|
|
|
|
|
|
|
|
|
|
if (!modelId) {
|
|
|
|
|
resultEl.textContent = '✗ 请输入模型ID';
|
|
|
|
|
resultEl.className = 'test-result error';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const config = {
|
|
|
|
|
model_name: modelId,
|
|
|
|
|
api_key: document.getElementById('api-key').value,
|
|
|
|
|
api_base: document.getElementById('api-base').value,
|
|
|
|
|
model_id: modelId,
|
|
|
|
|
temperature: parseFloat(document.getElementById('temperature').value),
|
|
|
|
|
max_tokens: parseInt(document.getElementById('max-tokens').value),
|
|
|
|
|
enabled: true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/test`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(config)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
resultEl.textContent = `✗ 请求失败: ${response.status}`;
|
|
|
|
|
resultEl.className = 'test-result error';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
resultEl.textContent = '✓ 连接成功';
|
|
|
|
|
resultEl.className = 'test-result success';
|
|
|
|
|
} else {
|
|
|
|
|
resultEl.textContent = '✗ ' + (data.message || '未知错误');
|
|
|
|
|
resultEl.className = 'test-result error';
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
resultEl.textContent = '✗ 连接失败: ' + (error.message || '未知错误');
|
|
|
|
|
resultEl.className = 'test-result error';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveConfig() {
|
|
|
|
|
const models = currentConfig?.models || [];
|
|
|
|
|
const existingIndex = models.findIndex(m => m.provider === selectedProvider);
|
|
|
|
|
|
|
|
|
|
const customModel = document.getElementById('custom-model').value.trim();
|
|
|
|
|
const selectedModel = document.getElementById('model-id').value;
|
|
|
|
|
const modelId = customModel || (selectedModel === 'custom' ? '' : selectedModel);
|
|
|
|
|
|
|
|
|
|
const newModel = {
|
|
|
|
|
model_name: modelId,
|
|
|
|
|
provider: selectedProvider,
|
|
|
|
|
api_key: document.getElementById('api-key').value,
|
|
|
|
|
api_base: document.getElementById('api-base').value,
|
|
|
|
|
model_id: modelId,
|
|
|
|
|
temperature: parseFloat(document.getElementById('temperature').value),
|
|
|
|
|
max_tokens: parseInt(document.getElementById('max-tokens').value),
|
|
|
|
|
enabled: true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (existingIndex >= 0) {
|
|
|
|
|
models[existingIndex] = { ...models[existingIndex], ...newModel };
|
|
|
|
|
} else {
|
|
|
|
|
models.push(newModel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const config = {
|
|
|
|
|
models: models,
|
|
|
|
|
active_model: selectedProvider,
|
|
|
|
|
analysis_settings: {
|
|
|
|
|
enable_technical_analysis: document.getElementById('enable-technical').checked,
|
|
|
|
|
enable_fundamental_analysis: document.getElementById('enable-fundamental').checked,
|
|
|
|
|
enable_sentiment_analysis: document.getElementById('enable-sentiment').checked,
|
|
|
|
|
risk_tolerance: document.getElementById('risk-tolerance').value,
|
|
|
|
|
max_position_pct: parseInt(document.getElementById('max-position').value)
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(API_BASE, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(config)
|
|
|
|
|
});
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
alert('配置保存成功!');
|
|
|
|
|
currentConfig = config;
|
|
|
|
|
renderModelsList(models);
|
|
|
|
|
} else {
|
|
|
|
|
alert('保存失败: ' + data.message);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
alert('保存失败: ' + error.message);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setActiveModel(index) {
|
|
|
|
|
if (!currentConfig || !currentConfig.models) return;
|
|
|
|
|
|
|
|
|
|
currentConfig.models.forEach((m, i) => {
|
|
|
|
|
m.enabled = i === index;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
saveConfig();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteModel(index) {
|
|
|
|
|
if (!confirm('确定要删除这个模型吗?')) return;
|
|
|
|
|
|
|
|
|
|
if (!currentConfig || !currentConfig.models) return;
|
|
|
|
|
|
|
|
|
|
currentConfig.models.splice(index, 1);
|
|
|
|
|
saveConfig();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addNewModel() {
|
|
|
|
|
document.getElementById('api-key').value = '';
|
|
|
|
|
document.getElementById('api-key').focus();
|
|
|
|
|
}
|