aiclient-2-api / src /ui-modules /upload-config-api.js
Jaasomn
Initial deployment
ceb3821
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;
}
}