AIClient-2-API/src/ui-modules/event-broadcast.js

278 lines
No EOL
9.5 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 { existsSync, readFileSync } from 'fs';
import { promises as fs } from 'fs';
import path from 'path';
import multer from 'multer';
import logger from '../utils/logger.js';
// Token存储到本地文件中
const TOKEN_STORE_FILE = path.join(process.cwd(), 'configs', 'token-store.json');
// 用量缓存文件路径
const USAGE_CACHE_FILE = path.join(process.cwd(), 'configs', 'usage-cache.json');
/**
* Helper function to broadcast events to UI clients
* @param {string} eventType - The type of event
* @param {any} data - The data to broadcast
*/
export function broadcastEvent(eventType, data) {
if (global.eventClients && global.eventClients.length > 0) {
const payload = typeof data === 'string' ? data : JSON.stringify(data);
global.eventClients.forEach(client => {
client.write(`event: ${eventType}\n`);
client.write(`data: ${payload}\n\n`);
});
}
}
/**
* Server-Sent Events for real-time updates
*/
export async function handleEvents(req, res) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
try {
res.write('\n');
} catch (err) {
logger.error('[Event Broadcast] Failed to write initial data:', err.message);
return true;
}
// Store the response object for broadcasting
if (!global.eventClients) {
global.eventClients = [];
}
global.eventClients.push(res);
// Keep connection alive
const keepAlive = setInterval(() => {
if (!res.writableEnded && !res.destroyed) {
try {
res.write(':\n\n');
} catch (err) {
logger.error('[Event Broadcast] Failed to write keepalive:', err.message);
clearInterval(keepAlive);
global.eventClients = global.eventClients.filter(r => r !== res);
}
} else {
clearInterval(keepAlive);
global.eventClients = global.eventClients.filter(r => r !== res);
}
}, 30000);
req.on('close', () => {
clearInterval(keepAlive);
global.eventClients = global.eventClients.filter(r => r !== res);
});
return true;
}
/**
* Initialize UI management features
*/
export function initializeUIManagement() {
// Initialize log broadcasting for UI
if (!global.eventClients) {
global.eventClients = [];
}
if (!global.logBuffer) {
global.logBuffer = [];
}
// Override console.log to broadcast logs
const originalLog = console.log;
console.log = function(...args) {
originalLog.apply(console, args);
const logEntry = {
timestamp: new Date().toISOString(),
level: 'info',
message: args.map(arg => {
if (typeof arg === 'string') return arg;
try {
return JSON.stringify(arg);
} catch (e) {
return String(arg);
}
}).join(' ')
};
global.logBuffer.push(logEntry);
if (global.logBuffer.length > 100) {
global.logBuffer.shift();
}
broadcastEvent('log', logEntry);
};
// Override console.error to broadcast errors
const originalError = console.error;
console.error = function(...args) {
originalError.apply(console, args);
const logEntry = {
timestamp: new Date().toISOString(),
level: 'error',
message: args.map(arg => {
if (typeof arg === 'string') return arg;
try {
return JSON.stringify(arg);
} catch (e) {
return String(arg);
}
}).join(' ')
};
global.logBuffer.push(logEntry);
if (global.logBuffer.length > 100) {
global.logBuffer.shift();
}
broadcastEvent('log', logEntry);
};
}
// 配置multer中间件
const storage = multer.diskStorage({
destination: async (req, file, cb) => {
try {
// multer在destination回调时req.body还未解析先使用默认路径
// 实际的provider会在文件上传完成后从req.body中获取
const uploadPath = path.join(process.cwd(), 'configs', 'temp');
await fs.mkdir(uploadPath, { recursive: true });
cb(null, uploadPath);
} catch (error) {
cb(error);
}
},
filename: (req, file, cb) => {
const timestamp = Date.now();
const sanitizedName = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_');
cb(null, `${timestamp}_${sanitizedName}`);
}
});
const fileFilter = (req, file, cb) => {
const allowedTypes = ['.json', '.txt', '.key', '.pem', '.p12', '.pfx'];
const ext = path.extname(file.originalname).toLowerCase();
if (allowedTypes.includes(ext)) {
cb(null, true);
} else {
cb(new Error('Unsupported file type'), false);
}
};
export const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB限制
}
});
/**
* 处理 OAuth 凭据文件上传
* @param {http.IncomingMessage} req - HTTP 请求对象
* @param {http.ServerResponse} res - HTTP 响应对象
* @param {Object} options - 可选配置
* @param {Object} options.providerMap - 提供商类型映射表
* @param {string} options.logPrefix - 日志前缀
* @param {string} options.userInfo - 用户信息(用于日志)
* @param {Object} options.customUpload - 自定义 multer 实例
* @returns {Promise<boolean>} 始终返回 true 表示请求已处理
*/
export function handleUploadOAuthCredentials(req, res, options = {}) {
const {
providerMap = {},
logPrefix = '[UI API]',
userInfo = '',
customUpload = null
} = options;
const uploadMiddleware = customUpload ? customUpload.single('file') : upload.single('file');
return new Promise((resolve) => {
uploadMiddleware(req, res, async (err) => {
if (err) {
logger.error(`${logPrefix} File upload error:`, err.message);
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
message: err.message || 'File upload failed'
}
}));
resolve(true);
return;
}
try {
if (!req.file) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
message: 'No file was uploaded'
}
}));
resolve(true);
return;
}
// multer执行完成后表单字段已解析到req.body中
const providerType = req.body.provider || 'common';
// 应用提供商映射(如果有)
const provider = providerMap[providerType] || providerType;
const tempFilePath = req.file.path;
// 根据实际的provider移动文件到正确的目录
let targetDir = path.join(process.cwd(), 'configs', provider);
// 如果是kiro类型的凭证需要再包裹一层文件夹
if (provider === 'kiro') {
// 使用时间戳作为子文件夹名称,确保每个上传的文件都有独立的目录
const timestamp = Date.now();
const originalNameWithoutExt = path.parse(req.file.originalname).name;
const subFolder = `${timestamp}_${originalNameWithoutExt}`;
targetDir = path.join(targetDir, subFolder);
}
await fs.mkdir(targetDir, { recursive: true });
const targetFilePath = path.join(targetDir, req.file.filename);
await fs.rename(tempFilePath, targetFilePath);
const relativePath = path.relative(process.cwd(), targetFilePath);
// 广播更新事件
broadcastEvent('config_update', {
action: 'add',
filePath: relativePath,
provider: provider,
timestamp: new Date().toISOString()
});
const userInfoStr = userInfo ? `, ${userInfo}` : '';
logger.info(`${logPrefix} OAuth credentials file uploaded: ${targetFilePath} (provider: ${provider}${userInfoStr})`);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: 'File uploaded successfully',
filePath: relativePath,
originalName: req.file.originalname,
provider: provider
}));
resolve(true);
} catch (error) {
logger.error(`${logPrefix} File upload processing error:`, error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
message: 'File upload processing failed: ' + error.message
}
}));
resolve(true);
}
});
});
}