AIClient-2-API/static/app/event-handlers.js
hex2077 ce7d78f7d0 feat(plugins): 新增 AI 监控插件并优化日志管理
- 新增 AI 监控插件 (ai-monitor),支持全链路协议转换监控
  - 捕获 AI 接口请求参数(转换前后)
  - 监控流式和非流式响应(转换前后)
  - 支持内部请求转换监控
- 新增日志清空功能,支持前端和服务器端同时清空当日日志
- 默认禁用 api-potluck 和 ai-monitor 插件
- 更新多语言文档和配置示例
- 优化提供商适配器开发指南
2026-01-25 19:40:04 +08:00

565 lines
No EOL
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 事件监听器模块
import { elements, autoScroll, setAutoScroll, clearLogs } from './constants.js';
import { showToast } from './utils.js';
import { fileUploadHandler } from './file-upload.js';
import { t } from './i18n.js';
import { checkUpdate, performUpdate } from './provider-manager.js';
/**
* 初始化所有事件监听器
*/
function initEventListeners() {
// 重启按钮
if (elements.restartBtn) {
elements.restartBtn.addEventListener('click', handleRestart);
}
// 清空日志
if (elements.clearLogsBtn) {
elements.clearLogsBtn.addEventListener('click', async () => {
// 显示确认对话框,明确提示会清空本地日志文件
const confirmed = confirm(t('logs.clear.confirm.msg'));
if (!confirmed) {
return;
}
try {
const token = window.authManager.getToken();
if (!token) {
showToast(t('common.error'), '请先登录', 'error');
return;
}
// 调用后端 API 清空日志文件
const response = await fetch(`${window.location.origin}/api/system/clear-log`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.status === 401) {
showToast(t('common.error'), '认证失败,请重新登录', 'error');
window.authManager.clearToken();
window.location.href = '/login.html';
return;
}
const result = await response.json();
if (result.success) {
// 清空前端日志显示
clearLogs();
if (elements.logsContainer) {
elements.logsContainer.innerHTML = '';
}
// 显示成功提示,明确说明已清空本地日志文件
showToast(
t('logs.clear.success.title'),
t('logs.clear.success.msg'),
'success',
5000 // 显示 5 秒
);
} else {
showToast(t('common.error'), t('logs.clear.failed'), 'error');
}
} catch (error) {
console.error('清空日志失败:', error);
showToast(t('common.error'), t('logs.clear.failed') + ': ' + error.message, 'error');
}
});
}
// 下载日志
if (elements.downloadLogsBtn) {
elements.downloadLogsBtn.addEventListener('click', async () => {
try {
const token = window.authManager.getToken();
if (!token) {
showToast(t('common.error'), '请先登录', 'error');
return;
}
// 使用带认证的方式下载文件
const url = `${window.location.origin}/api/system/download-log`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.status === 401) {
showToast(t('common.error'), '认证失败,请重新登录', 'error');
window.authManager.clearToken();
window.location.href = '/login.html';
return;
}
if (!response.ok) {
const errorData = await response.json();
showToast(t('common.error'), errorData.error?.message || '下载失败', 'error');
return;
}
// 获取文件名
const contentDisposition = response.headers.get('Content-Disposition');
let filename = 'app.log';
if (contentDisposition) {
const matches = /filename="?([^"]+)"?/.exec(contentDisposition);
if (matches && matches[1]) {
filename = matches[1];
}
}
// 下载文件
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(downloadUrl);
showToast(t('common.success'), '日志下载成功', 'success');
} catch (error) {
console.error('下载日志失败:', error);
showToast(t('common.error'), '下载失败: ' + error.message, 'error');
}
});
}
// 自动滚动切换
if (elements.toggleAutoScrollBtn) {
elements.toggleAutoScrollBtn.addEventListener('click', () => {
const newAutoScroll = !autoScroll;
setAutoScroll(newAutoScroll);
elements.toggleAutoScrollBtn.dataset.enabled = newAutoScroll;
const statusText = newAutoScroll ? t('logs.autoScroll.on') : t('logs.autoScroll.off');
elements.toggleAutoScrollBtn.innerHTML = `
<i class="fas fa-arrow-down"></i>
<span data-i18n="${newAutoScroll ? 'logs.autoScroll.on' : 'logs.autoScroll.off'}">${statusText}</span>
`;
});
}
// 保存配置
if (elements.saveConfigBtn) {
elements.saveConfigBtn.addEventListener('click', saveConfiguration);
}
// 重置配置
if (elements.resetConfigBtn) {
elements.resetConfigBtn.addEventListener('click', loadInitialData);
}
// 模型提供商切换
if (elements.modelProvider) {
elements.modelProvider.addEventListener('change', handleProviderChange);
}
// Gemini凭据类型切换
document.querySelectorAll('input[name="geminiCredsType"]').forEach(radio => {
radio.addEventListener('change', handleGeminiCredsTypeChange);
});
// Kiro凭据类型切换
document.querySelectorAll('input[name="kiroCredsType"]').forEach(radio => {
radio.addEventListener('change', handleKiroCredsTypeChange);
});
// 密码显示/隐藏切换
document.querySelectorAll('.password-toggle').forEach(button => {
button.addEventListener('click', handlePasswordToggle);
});
// 生成凭据按钮监听
document.querySelectorAll('.generate-creds-btn').forEach(button => {
button.addEventListener('click', handleGenerateCreds);
});
// 提供商池配置监听
// const providerPoolsInput = document.getElementById('providerPoolsFilePath');
// if (providerPoolsInput) {
// providerPoolsInput.addEventListener('input', handleProviderPoolsConfigChange);
// }
// 检查更新按钮
const checkUpdateBtn = document.getElementById('checkUpdateBtn');
if (checkUpdateBtn) {
checkUpdateBtn.addEventListener('click', () => checkUpdate(false));
}
// 执行更新按钮
const performUpdateBtn = document.getElementById('performUpdateBtn');
if (performUpdateBtn) {
performUpdateBtn.addEventListener('click', performUpdate);
}
// 日志容器滚动
if (elements.logsContainer) {
elements.logsContainer.addEventListener('scroll', () => {
if (autoScroll) {
const isAtBottom = elements.logsContainer.scrollTop + elements.logsContainer.clientHeight
>= elements.logsContainer.scrollHeight - 5;
if (!isAtBottom) {
setAutoScroll(false);
elements.toggleAutoScrollBtn.dataset.enabled = false;
elements.toggleAutoScrollBtn.innerHTML = `
<i class="fas fa-arrow-down"></i>
<span data-i18n="logs.autoScroll.off">${t('logs.autoScroll.off')}</span>
`;
}
}
});
}
}
/**
* 提供商配置切换处理
*/
function handleProviderChange() {
const selectedProvider = elements.modelProvider?.value;
if (!selectedProvider) return;
const allProviderConfigs = document.querySelectorAll('.provider-config');
// 隐藏所有提供商配置
allProviderConfigs.forEach(config => {
config.style.display = 'none';
});
// 显示当前选中的提供商配置
const targetConfig = document.querySelector(`[data-provider="${selectedProvider}"]`);
if (targetConfig) {
targetConfig.style.display = 'block';
}
}
/**
* Gemini凭据类型切换
* @param {Event} event - 事件对象
*/
function handleGeminiCredsTypeChange(event) {
const selectedType = event.target.value;
const base64Group = document.getElementById('geminiCredsBase64Group');
const fileGroup = document.getElementById('geminiCredsFileGroup');
if (selectedType === 'base64') {
if (base64Group) base64Group.style.display = 'block';
if (fileGroup) fileGroup.style.display = 'none';
} else {
if (base64Group) base64Group.style.display = 'none';
if (fileGroup) fileGroup.style.display = 'block';
}
}
/**
* Kiro凭据类型切换
* @param {Event} event - 事件对象
*/
function handleKiroCredsTypeChange(event) {
const selectedType = event.target.value;
const base64Group = document.getElementById('kiroCredsBase64Group');
const fileGroup = document.getElementById('kiroCredsFileGroup');
if (selectedType === 'base64') {
if (base64Group) base64Group.style.display = 'block';
if (fileGroup) fileGroup.style.display = 'none';
} else {
if (base64Group) base64Group.style.display = 'none';
if (fileGroup) fileGroup.style.display = 'block';
}
}
/**
* 密码显示/隐藏切换处理
* @param {Event} event - 事件对象
*/
function handlePasswordToggle(event) {
const button = event.target.closest('.password-toggle');
if (!button) return;
const targetId = button.getAttribute('data-target');
const input = document.getElementById(targetId);
const icon = button.querySelector('i');
if (!input || !icon) return;
if (input.type === 'password') {
input.type = 'text';
icon.className = 'fas fa-eye-slash';
} else {
input.type = 'password';
icon.className = 'fas fa-eye';
}
}
/**
* 处理生成凭据逻辑
* @param {Event} event - 事件对象
*/
async function handleGenerateCreds(event) {
const button = event.target.closest('.generate-creds-btn');
if (!button) return;
const providerType = button.getAttribute('data-provider');
const targetInputId = button.getAttribute('data-target');
try {
// 如果是 Kiro OAuth先显示认证方式选择对话框
if (providerType === 'claude-kiro-oauth') {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.style.display = 'flex';
modal.innerHTML = `
<div class="modal-content" style="max-width: 500px;">
<div class="modal-header">
<h3><i class="fas fa-key"></i> <span data-i18n="oauth.kiro.selectMethod">${t('oauth.kiro.selectMethod')}</span></h3>
<button class="modal-close">&times;</button>
</div>
<div class="modal-body">
<div class="auth-method-options" style="display: flex; flex-direction: column; gap: 12px;">
<!--<button class="auth-method-btn" data-method="google" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-google" style="font-size: 24px; color: #4285f4;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.google">${t('oauth.kiro.google')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.googleDesc">${t('oauth.kiro.googleDesc')}</div>
</div>
</button>
<button class="auth-method-btn" data-method="github" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-github" style="font-size: 24px; color: #333;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.github">${t('oauth.kiro.github')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.githubDesc">${t('oauth.kiro.githubDesc')}</div>
</div>
</button> -->
<button class="auth-method-btn" data-method="builder-id" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
<i class="fab fa-aws" style="font-size: 24px; color: #ff9900;"></i>
<div style="text-align: left;">
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.awsBuilder">${t('oauth.kiro.awsBuilder')}</div>
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.awsBuilderDesc">${t('oauth.kiro.awsBuilderDesc')}</div>
</div>
</button>
</div>
</div>
<div class="modal-footer">
<button class="modal-cancel" data-i18n="modal.provider.cancel">${t('modal.provider.cancel')}</button>
</div>
</div>
`;
document.body.appendChild(modal);
const closeModal = () => modal.remove();
modal.querySelector('.modal-close').onclick = closeModal;
modal.querySelector('.modal-cancel').onclick = closeModal;
modal.querySelectorAll('.auth-method-btn').forEach(btn => {
btn.onclick = async () => {
const method = btn.dataset.method;
closeModal();
await proceedWithAuth(providerType, targetInputId, { method });
};
});
return;
}
await proceedWithAuth(providerType, targetInputId, {});
} catch (error) {
console.error('生成凭据失败:', error);
showToast(t('common.error'), t('modal.provider.auth.failed') + `: ${error.message}`, 'error');
}
}
/**
* 实际执行授权逻辑
*/
async function proceedWithAuth(providerType, targetInputId, extraOptions = {}) {
if (window.executeGenerateAuthUrl) {
await window.executeGenerateAuthUrl(providerType, {
targetInputId,
...extraOptions
});
} else {
console.error('executeGenerateAuthUrl not found');
}
}
/**
* 提供商池配置变化处理
* @param {Event} event - 事件对象
*/
function handleProviderPoolsConfigChange(event) {
const filePath = event.target.value.trim();
const providersMenuItem = document.querySelector('.nav-item[data-section="providers"]');
if (filePath) {
// 显示提供商池菜单
if (providersMenuItem) providersMenuItem.style.display = 'flex';
} else {
// 隐藏提供商池菜单
if (providersMenuItem) providersMenuItem.style.display = 'none';
// 如果当前在提供商池页面,切换到仪表盘
if (providersMenuItem && providersMenuItem.classList.contains('active')) {
const dashboardItem = document.querySelector('.nav-item[data-section="dashboard"]');
const dashboardSection = document.getElementById('dashboard');
// 更新导航状态
document.querySelectorAll('.nav-item').forEach(nav => nav.classList.remove('active'));
document.querySelectorAll('.section').forEach(section => section.classList.remove('active'));
if (dashboardItem) dashboardItem.classList.add('active');
if (dashboardSection) dashboardSection.classList.add('active');
}
}
}
/**
* 密码显示/隐藏切换处理(用于模态框中的密码输入框)
* @param {HTMLElement} button - 按钮元素
*/
function handleProviderPasswordToggle(button) {
const targetKey = button.getAttribute('data-target');
const input = button.parentNode.querySelector(`input[data-config-key="${targetKey}"]`);
const icon = button.querySelector('i');
if (!input || !icon) return;
if (input.type === 'password') {
input.type = 'text';
icon.className = 'fas fa-eye-slash';
} else {
input.type = 'password';
icon.className = 'fas fa-eye';
}
}
// 数据加载函数(需要从主模块导入)
let loadInitialData;
let saveConfiguration;
let reloadConfig;
// 当前服务模式(由 provider-manager.js 设置)
let currentServiceMode = 'worker';
/**
* 设置当前服务模式
* @param {string} mode - 服务模式 ('worker' 或 'standalone')
*/
export function setServiceMode(mode) {
currentServiceMode = mode;
}
/**
* 获取当前服务模式
* @returns {string} 当前服务模式
*/
export function getServiceMode() {
return currentServiceMode;
}
// 重启/重载服务处理函数
async function handleRestart() {
try {
// 根据服务模式执行不同操作
if (currentServiceMode === 'standalone') {
// 独立模式:执行重载配置
await handleReloadConfig();
} else {
// 子进程模式:执行重启服务
await handleRestartService();
}
} catch (error) {
console.error('Operation failed:', error);
const errorKey = currentServiceMode === 'standalone' ? 'header.reload.failed' : 'header.restart.failed';
showToast(t('common.error'), t(errorKey) + ': ' + error.message, 'error');
}
}
/**
* 重载配置(独立模式)
*/
async function handleReloadConfig() {
// 确认重载操作
if (!confirm(t('header.reload.confirm'))) {
return;
}
showToast(t('common.info'), t('header.reload.requesting'), 'info');
// 先刷新基础数据
if (loadInitialData) {
loadInitialData();
}
// 如果reloadConfig函数可用则也刷新配置
if (reloadConfig) {
await reloadConfig();
}
}
/**
* 重启服务(子进程模式)
*/
async function handleRestartService() {
// 确认重启操作
if (!confirm(t('header.restart.confirm'))) {
return;
}
showToast(t('common.info'), t('header.restart.requesting'), 'info');
const result = await window.apiClient.post('/restart-service');
if (result.success) {
showToast(t('common.success'), result.message || t('header.restart.success'), 'success');
// 如果是 worker 模式,服务会自动重启,等待几秒后刷新页面
if (result.mode === 'worker') {
setTimeout(() => {
showToast(t('common.info'), t('header.restart.reconnecting'), 'info');
// 等待服务重启后刷新页面
setTimeout(() => {
window.location.reload();
}, 3000);
}, 2000);
}
} else {
// 显示错误信息
const errorMsg = result.message || result.error?.message || t('header.restart.failed');
showToast(t('common.error'), errorMsg, 'error');
// 如果是独立模式,显示提示
if (result.mode === 'standalone') {
showToast(t('common.info'), result.hint, 'warning');
}
}
}
export function setDataLoaders(dataLoader, configSaver) {
loadInitialData = dataLoader;
saveConfiguration = configSaver;
}
export function setReloadConfig(configReloader) {
reloadConfig = configReloader;
}
export {
initEventListeners,
handleProviderChange,
handleGeminiCredsTypeChange,
handleKiroCredsTypeChange,
handlePasswordToggle,
handleProviderPoolsConfigChange,
handleProviderPasswordToggle
};