Spaces:
Running
Running
| import { existsSync } from 'fs'; | |
| import { promises as fs } from 'fs'; | |
| import path from 'path'; | |
| import AdmZip from 'adm-zip'; | |
| import { broadcastEvent } from './event-broadcast.js'; | |
| import { scanConfigFiles } from './config-scanner.js'; | |
| /** | |
| * 获取上传配置文件列表 | |
| */ | |
| export async function handleGetUploadConfigs(req, res, currentConfig, providerPoolManager) { | |
| try { | |
| const configFiles = await scanConfigFiles(currentConfig, providerPoolManager); | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify(configFiles)); | |
| return true; | |
| } catch (error) { | |
| console.error('[UI API] Failed to scan config files:', error); | |
| res.writeHead(500, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| error: { | |
| message: 'Failed to scan config files: ' + error.message | |
| } | |
| })); | |
| return true; | |
| } | |
| } | |
| /** | |
| * 查看特定配置文件 | |
| */ | |
| export async function handleViewConfigFile(req, res, filePath) { | |
| try { | |
| const fullPath = path.join(process.cwd(), filePath); | |
| // 安全检查:确保文件路径在允许的目录内 | |
| const allowedDirs = ['configs']; | |
| const relativePath = path.relative(process.cwd(), fullPath); | |
| const isAllowed = allowedDirs.some(dir => relativePath.startsWith(dir + path.sep) || relativePath === dir); | |
| if (!isAllowed) { | |
| res.writeHead(403, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| error: { | |
| message: 'Access denied: can only view files in configs directory' | |
| } | |
| })); | |
| return true; | |
| } | |
| if (!existsSync(fullPath)) { | |
| res.writeHead(404, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| error: { | |
| message: 'File does not exist' | |
| } | |
| })); | |
| return true; | |
| } | |
| const content = await fs.readFile(fullPath, 'utf-8'); | |
| const stats = await fs.stat(fullPath); | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| path: relativePath, | |
| content: content, | |
| size: stats.size, | |
| modified: stats.mtime.toISOString(), | |
| name: path.basename(fullPath) | |
| })); | |
| return true; | |
| } catch (error) { | |
| console.error('[UI API] Failed to view config file:', error); | |
| res.writeHead(500, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| error: { | |
| message: 'Failed to view config file: ' + error.message | |
| } | |
| })); | |
| return true; | |
| } | |
| } | |
| /** | |
| * 删除特定配置文件 | |
| */ | |
| export async function handleDeleteConfigFile(req, res, filePath) { | |
| try { | |
| const fullPath = path.join(process.cwd(), filePath); | |
| // 安全检查:确保文件路径在允许的目录内 | |
| const allowedDirs = ['configs']; | |
| const relativePath = path.relative(process.cwd(), fullPath); | |
| const isAllowed = allowedDirs.some(dir => relativePath.startsWith(dir + path.sep) || relativePath === dir); | |
| if (!isAllowed) { | |
| res.writeHead(403, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| error: { | |
| message: 'Access denied: can only delete files in configs directory' | |
| } | |
| })); | |
| return true; | |
| } | |
| if (!existsSync(fullPath)) { | |
| res.writeHead(404, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| error: { | |
| message: 'File does not exist' | |
| } | |
| })); | |
| return true; | |
| } | |
| await fs.unlink(fullPath); | |
| // 广播更新事件 | |
| broadcastEvent('config_update', { | |
| action: 'delete', | |
| filePath: relativePath, | |
| timestamp: new Date().toISOString() | |
| }); | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| success: true, | |
| message: 'File deleted successfully', | |
| filePath: relativePath | |
| })); | |
| return true; | |
| } catch (error) { | |
| console.error('[UI API] Failed to delete config file:', error); | |
| res.writeHead(500, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| error: { | |
| message: 'Failed to delete config file: ' + error.message | |
| } | |
| })); | |
| return true; | |
| } | |
| } | |
| /** | |
| * 下载所有配置为 zip | |
| */ | |
| export async function handleDownloadAllConfigs(req, res) { | |
| try { | |
| const configsPath = path.join(process.cwd(), 'configs'); | |
| if (!existsSync(configsPath)) { | |
| res.writeHead(404, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ error: { message: 'configs directory does not exist' } })); | |
| return true; | |
| } | |
| const zip = new AdmZip(); | |
| // 递归添加目录函数 | |
| const addDirectoryToZip = async (dirPath, zipPath = '') => { | |
| const items = await fs.readdir(dirPath, { withFileTypes: true }); | |
| for (const item of items) { | |
| const fullPath = path.join(dirPath, item.name); | |
| const itemZipPath = zipPath ? path.join(zipPath, item.name) : item.name; | |
| if (item.isFile()) { | |
| const content = await fs.readFile(fullPath); | |
| zip.addFile(itemZipPath.replace(/\\/g, '/'), content); | |
| } else if (item.isDirectory()) { | |
| await addDirectoryToZip(fullPath, itemZipPath); | |
| } | |
| } | |
| }; | |
| await addDirectoryToZip(configsPath); | |
| const zipBuffer = zip.toBuffer(); | |
| const filename = `configs_backup_${new Date().toISOString().replace(/[:.]/g, '-')}.zip`; | |
| res.writeHead(200, { | |
| 'Content-Type': 'application/zip', | |
| 'Content-Disposition': `attachment; filename="${filename}"`, | |
| 'Content-Length': zipBuffer.length | |
| }); | |
| res.end(zipBuffer); | |
| console.log(`[UI API] All configs downloaded as zip: ${filename}`); | |
| return true; | |
| } catch (error) { | |
| console.error('[UI API] Failed to download all configs:', error); | |
| res.writeHead(500, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| error: { | |
| message: 'Failed to download zip: ' + error.message | |
| } | |
| })); | |
| return true; | |
| } | |
| } | |
| /** | |
| * 批量删除未绑定的配置文件 | |
| * 只删除 configs/xxx/ 子目录下的未绑定配置文件 | |
| */ | |
| export async function handleDeleteUnboundConfigs(req, res, currentConfig, providerPoolManager) { | |
| try { | |
| // 首先获取所有配置文件及其绑定状态 | |
| const configFiles = await scanConfigFiles(currentConfig, providerPoolManager); | |
| // 筛选出未绑定的配置文件,并且必须在 configs/xxx/ 子目录下 | |
| // 即路径格式为 configs/子目录名/文件名,而不是直接在 configs/ 根目录下 | |
| const unboundConfigs = configFiles.filter(config => { | |
| if (config.isUsed) return false; | |
| // 检查路径是否在 configs/xxx/ 子目录下 | |
| // 路径格式应该是 configs/子目录/... | |
| const normalizedPath = config.path.replace(/\\/g, '/'); | |
| const pathParts = normalizedPath.split('/'); | |
| // 路径至少需要3部分:configs/子目录/文件名 | |
| // 例如:configs/kiro/xxx.json 或 configs/gemini/xxx.json | |
| if (pathParts.length >= 3 && pathParts[0] === 'configs') { | |
| // 确保第二部分是子目录名(不是文件名) | |
| return true; | |
| } | |
| return false; | |
| }); | |
| if (unboundConfigs.length === 0) { | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| success: true, | |
| message: 'No unbound config files to delete', | |
| deletedCount: 0, | |
| deletedFiles: [] | |
| })); | |
| return true; | |
| } | |
| const deletedFiles = []; | |
| const failedFiles = []; | |
| for (const config of unboundConfigs) { | |
| try { | |
| const fullPath = path.join(process.cwd(), config.path); | |
| // 安全检查:确保文件路径在允许的目录内 | |
| const allowedDirs = ['configs']; | |
| const relativePath = path.relative(process.cwd(), fullPath); | |
| const isAllowed = allowedDirs.some(dir => relativePath.startsWith(dir + path.sep) || relativePath === dir); | |
| if (!isAllowed) { | |
| failedFiles.push({ | |
| path: config.path, | |
| error: 'Access denied: can only delete files in configs directory' | |
| }); | |
| continue; | |
| } | |
| if (!existsSync(fullPath)) { | |
| failedFiles.push({ | |
| path: config.path, | |
| error: 'File does not exist' | |
| }); | |
| continue; | |
| } | |
| await fs.unlink(fullPath); | |
| deletedFiles.push(config.path); | |
| } catch (error) { | |
| failedFiles.push({ | |
| path: config.path, | |
| error: error.message | |
| }); | |
| } | |
| } | |
| // 广播更新事件 | |
| if (deletedFiles.length > 0) { | |
| broadcastEvent('config_update', { | |
| action: 'batch_delete', | |
| deletedFiles: deletedFiles, | |
| timestamp: new Date().toISOString() | |
| }); | |
| } | |
| res.writeHead(200, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| success: true, | |
| message: `Deleted ${deletedFiles.length} unbound config files`, | |
| deletedCount: deletedFiles.length, | |
| deletedFiles: deletedFiles, | |
| failedCount: failedFiles.length, | |
| failedFiles: failedFiles | |
| })); | |
| return true; | |
| } catch (error) { | |
| console.error('[UI API] Failed to delete unbound configs:', error); | |
| res.writeHead(500, { 'Content-Type': 'application/json' }); | |
| res.end(JSON.stringify({ | |
| error: { | |
| message: 'Failed to delete unbound configs: ' + error.message | |
| } | |
| })); | |
| return true; | |
| } | |
| } |