278 lines
No EOL
9.5 KiB
JavaScript
278 lines
No EOL
9.5 KiB
JavaScript
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);
|
||
}
|
||
});
|
||
});
|
||
} |