mirror of
https://github.com/samanhappy/mcphub.git
synced 2025-12-24 02:39:19 -05:00
feat:support DXT file server installation (#200)
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -24,3 +24,5 @@ yarn-error.log*
|
|||||||
.vscode/
|
.vscode/
|
||||||
*.log
|
*.log
|
||||||
coverage/
|
coverage/
|
||||||
|
|
||||||
|
data/
|
||||||
@@ -57,7 +57,7 @@ Create a `mcp_settings.json` file to customize your server settings:
|
|||||||
**Recommended**: Mount your custom config:
|
**Recommended**: Mount your custom config:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 3000:3000 -v $(pwd)/mcp_settings.json:/app/mcp_settings.json samanhappy/mcphub
|
docker run -p 3000:3000 -v ./mcp_settings.json:/app/mcp_settings.json -v ./data:/app/data samanhappy/mcphub
|
||||||
```
|
```
|
||||||
|
|
||||||
or run with default settings:
|
or run with default settings:
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ MCPHub 通过将多个 MCP(Model Context Protocol)服务器组织为灵活
|
|||||||
**推荐**:挂载自定义配置:
|
**推荐**:挂载自定义配置:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -p 3000:3000 -v $(pwd)/mcp_settings.json:/app/mcp_settings.json samanhappy/mcphub
|
docker run -p 3000:3000 -v ./mcp_settings.json:/app/mcp_settings.json -v ./data:/app/data samanhappy/mcphub
|
||||||
```
|
```
|
||||||
|
|
||||||
或使用默认配置运行:
|
或使用默认配置运行:
|
||||||
|
|||||||
413
frontend/src/components/DxtUploadForm.tsx
Normal file
413
frontend/src/components/DxtUploadForm.tsx
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { getApiUrl } from '@/utils/runtime';
|
||||||
|
import ConfirmDialog from '@/components/ui/ConfirmDialog';
|
||||||
|
|
||||||
|
interface DxtUploadFormProps {
|
||||||
|
onSuccess: (serverConfig: any) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DxtUploadResponse {
|
||||||
|
success: boolean;
|
||||||
|
data?: {
|
||||||
|
manifest: any;
|
||||||
|
extractDir: string;
|
||||||
|
};
|
||||||
|
message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DxtUploadForm: React.FC<DxtUploadFormProps> = ({ onSuccess, onCancel }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||||
|
const [showServerForm, setShowServerForm] = useState(false);
|
||||||
|
const [manifestData, setManifestData] = useState<any>(null);
|
||||||
|
const [extractDir, setExtractDir] = useState<string>('');
|
||||||
|
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||||
|
const [pendingServerName, setPendingServerName] = useState<string>('');
|
||||||
|
|
||||||
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragLeave = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragging(false);
|
||||||
|
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length > 0) {
|
||||||
|
const file = files[0];
|
||||||
|
if (file.name.endsWith('.dxt')) {
|
||||||
|
setSelectedFile(file);
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
setError(t('dxt.invalidFileType'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFileSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const files = e.target.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
const file = files[0];
|
||||||
|
if (file.name.endsWith('.dxt')) {
|
||||||
|
setSelectedFile(file);
|
||||||
|
setError(null);
|
||||||
|
} else {
|
||||||
|
setError(t('dxt.invalidFileType'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpload = async () => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
setError(t('dxt.noFileSelected'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsUploading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('dxtFile', selectedFile);
|
||||||
|
|
||||||
|
const token = localStorage.getItem('mcphub_token');
|
||||||
|
const response = await fetch(getApiUrl('/dxt/upload'), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'x-auth-token': token || '',
|
||||||
|
},
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: DxtUploadResponse = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || `HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
setManifestData(result.data.manifest);
|
||||||
|
setExtractDir(result.data.extractDir);
|
||||||
|
setShowServerForm(true);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || t('dxt.uploadFailed'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('DXT upload error:', err);
|
||||||
|
setError(err instanceof Error ? err.message : t('dxt.uploadFailed'));
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstallServer = async (serverName: string, forceOverride: boolean = false) => {
|
||||||
|
setIsUploading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert DXT manifest to MCPHub stdio server configuration
|
||||||
|
const serverConfig = convertDxtToMcpConfig(manifestData, extractDir, serverName);
|
||||||
|
|
||||||
|
const token = localStorage.getItem('mcphub_token');
|
||||||
|
|
||||||
|
// First, check if server exists
|
||||||
|
if (!forceOverride) {
|
||||||
|
const checkResponse = await fetch(getApiUrl('/servers'), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'x-auth-token': token || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkResponse.ok) {
|
||||||
|
const checkResult = await checkResponse.json();
|
||||||
|
const existingServer = checkResult.data?.find((server: any) => server.name === serverName);
|
||||||
|
|
||||||
|
if (existingServer) {
|
||||||
|
// Server exists, show confirmation dialog
|
||||||
|
setPendingServerName(serverName);
|
||||||
|
setShowConfirmDialog(true);
|
||||||
|
setIsUploading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install or override the server
|
||||||
|
const method = forceOverride ? 'PUT' : 'POST';
|
||||||
|
const url = forceOverride ? getApiUrl(`/servers/${encodeURIComponent(serverName)}`) : getApiUrl('/servers');
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-auth-token': token || '',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: serverName,
|
||||||
|
config: serverConfig,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(result.message || `HTTP error! Status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
onSuccess(serverConfig);
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || t('dxt.installFailed'));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('DXT install error:', err);
|
||||||
|
setError(err instanceof Error ? err.message : t('dxt.installFailed'));
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirmOverride = () => {
|
||||||
|
setShowConfirmDialog(false);
|
||||||
|
if (pendingServerName) {
|
||||||
|
handleInstallServer(pendingServerName, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelOverride = () => {
|
||||||
|
setShowConfirmDialog(false);
|
||||||
|
setPendingServerName('');
|
||||||
|
setIsUploading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDxtToMcpConfig = (manifest: any, extractPath: string, _serverName: string) => {
|
||||||
|
const mcpConfig = manifest.server?.mcp_config || {};
|
||||||
|
|
||||||
|
// Convert DXT manifest to MCPHub stdio configuration
|
||||||
|
const config: any = {
|
||||||
|
type: 'stdio',
|
||||||
|
command: mcpConfig.command || 'node',
|
||||||
|
args: (mcpConfig.args || []).map((arg: string) =>
|
||||||
|
arg.replace('${__dirname}', extractPath)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add environment variables if they exist
|
||||||
|
if (mcpConfig.env && Object.keys(mcpConfig.env).length > 0) {
|
||||||
|
config.env = { ...mcpConfig.env };
|
||||||
|
|
||||||
|
// Replace ${__dirname} in environment variables
|
||||||
|
Object.keys(config.env).forEach(key => {
|
||||||
|
if (typeof config.env[key] === 'string') {
|
||||||
|
config.env[key] = config.env[key].replace('${__dirname}', extractPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (showServerForm && manifestData) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={showConfirmDialog}
|
||||||
|
onClose={handleCancelOverride}
|
||||||
|
onConfirm={handleConfirmOverride}
|
||||||
|
title={t('dxt.serverExistsTitle')}
|
||||||
|
message={t('dxt.serverExistsConfirm', { serverName: pendingServerName })}
|
||||||
|
confirmText={t('dxt.override')}
|
||||||
|
cancelText={t('common.cancel')}
|
||||||
|
variant="warning"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={`fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4 ${showConfirmDialog ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||||
|
<div className="bg-white shadow rounded-lg p-6 w-full max-w-2xl max-h-screen overflow-y-auto">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">{t('dxt.installServer')}</h2>
|
||||||
|
<button onClick={onCancel} className="text-gray-500 hover:text-gray-700">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 bg-red-50 border-l-4 border-red-500 p-4 rounded">
|
||||||
|
<p className="text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Extension Info */}
|
||||||
|
<div className="bg-gray-50 p-4 rounded-lg">
|
||||||
|
<h3 className="font-medium text-gray-900 mb-2">{t('dxt.extensionInfo')}</h3>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div><strong>{t('dxt.name')}:</strong> {manifestData.display_name || manifestData.name}</div>
|
||||||
|
<div><strong>{t('dxt.version')}:</strong> {manifestData.version}</div>
|
||||||
|
<div><strong>{t('dxt.description')}:</strong> {manifestData.description}</div>
|
||||||
|
{manifestData.author && (
|
||||||
|
<div><strong>{t('dxt.author')}:</strong> {manifestData.author.name}</div>
|
||||||
|
)}
|
||||||
|
{manifestData.tools && manifestData.tools.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<strong>{t('dxt.tools')}:</strong>
|
||||||
|
<ul className="list-disc list-inside ml-4">
|
||||||
|
{manifestData.tools.map((tool: any, index: number) => (
|
||||||
|
<li key={index}>{tool.name} - {tool.description}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Server Configuration */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
{t('dxt.serverName')}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="serverName"
|
||||||
|
defaultValue={manifestData.name}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder={t('dxt.serverNamePlaceholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Action Buttons */}
|
||||||
|
<div className="flex justify-end space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={isUploading}
|
||||||
|
className="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const nameInput = document.getElementById('serverName') as HTMLInputElement;
|
||||||
|
const serverName = nameInput?.value.trim() || manifestData.name;
|
||||||
|
handleInstallServer(serverName);
|
||||||
|
}}
|
||||||
|
disabled={isUploading}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center"
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
{t('dxt.installing')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t('dxt.install')
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="bg-white shadow rounded-lg p-6 w-full max-w-lg">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">{t('dxt.uploadTitle')}</h2>
|
||||||
|
<button onClick={onCancel} className="text-gray-500 hover:text-gray-700">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="mb-4 bg-red-50 border-l-4 border-red-500 p-4 rounded">
|
||||||
|
<p className="text-red-700">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* File Drop Zone */}
|
||||||
|
<div
|
||||||
|
className={`relative border-2 border-dashed rounded-lg p-8 text-center transition-colors ${isDragging
|
||||||
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: selectedFile
|
||||||
|
? 'border-green-500 bg-green-50'
|
||||||
|
: 'border-gray-300 hover:border-gray-400'
|
||||||
|
}`}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
>
|
||||||
|
{selectedFile ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<svg className="mx-auto h-12 w-12 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
<p className="text-sm text-gray-900 font-medium">{selectedFile.name}</p>
|
||||||
|
<p className="text-xs text-gray-500">{(selectedFile.size / 1024 / 1024).toFixed(2)} MB</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-900">{t('dxt.dropFileHere')}</p>
|
||||||
|
<p className="text-xs text-gray-500">{t('dxt.orClickToSelect')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".dxt"
|
||||||
|
onChange={handleFileSelect}
|
||||||
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex justify-end space-x-4">
|
||||||
|
<button
|
||||||
|
onClick={onCancel}
|
||||||
|
disabled={isUploading}
|
||||||
|
className="px-4 py-2 text-gray-700 bg-gray-200 rounded hover:bg-gray-300 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{t('common.cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleUpload}
|
||||||
|
disabled={!selectedFile || isUploading}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 flex items-center"
|
||||||
|
>
|
||||||
|
{isUploading ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin h-4 w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
{t('dxt.uploading')}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
t('dxt.upload')
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DxtUploadForm;
|
||||||
142
frontend/src/components/ui/ConfirmDialog.tsx
Normal file
142
frontend/src/components/ui/ConfirmDialog.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
interface ConfirmDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
variant?: 'danger' | 'warning' | 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmText,
|
||||||
|
cancelText,
|
||||||
|
variant = 'warning'
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const getVariantStyles = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'danger':
|
||||||
|
return {
|
||||||
|
icon: (
|
||||||
|
<svg className="w-6 h-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
confirmClass: 'bg-red-600 hover:bg-red-700 text-white',
|
||||||
|
};
|
||||||
|
case 'warning':
|
||||||
|
return {
|
||||||
|
icon: (
|
||||||
|
<svg className="w-6 h-6 text-yellow-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
confirmClass: 'bg-yellow-600 hover:bg-yellow-700 text-white',
|
||||||
|
};
|
||||||
|
case 'info':
|
||||||
|
return {
|
||||||
|
icon: (
|
||||||
|
<svg className="w-6 h-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
confirmClass: 'bg-blue-600 hover:bg-blue-700 text-white',
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
icon: null,
|
||||||
|
confirmClass: 'bg-blue-600 hover:bg-blue-700 text-white',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const { icon, confirmClass } = getVariantStyles();
|
||||||
|
|
||||||
|
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
onClose();
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
onConfirm();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-black/50 z-[100] flex items-center justify-center p-4"
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-white rounded-lg shadow-xl max-w-md w-full transform transition-all duration-200 ease-out"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="confirm-dialog-title"
|
||||||
|
aria-describedby="confirm-dialog-message"
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
{icon && (
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1">
|
||||||
|
{title && (
|
||||||
|
<h3
|
||||||
|
id="confirm-dialog-title"
|
||||||
|
className="text-lg font-medium text-gray-900 mb-2"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
)}
|
||||||
|
<p
|
||||||
|
id="confirm-dialog-message"
|
||||||
|
className="text-gray-600 leading-relaxed"
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3 mt-6">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 text-gray-600 hover:text-gray-800 hover:bg-gray-100 rounded-md transition-colors duration-150"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
{cancelText || t('common.cancel')}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={onConfirm}
|
||||||
|
className={`px-4 py-2 rounded-md transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-offset-2 ${confirmClass}`}
|
||||||
|
>
|
||||||
|
{confirmText || t('common.confirm')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmDialog;
|
||||||
@@ -174,7 +174,8 @@
|
|||||||
"copy": "Copy",
|
"copy": "Copy",
|
||||||
"copySuccess": "Copied to clipboard",
|
"copySuccess": "Copied to clipboard",
|
||||||
"copyFailed": "Copy failed",
|
"copyFailed": "Copy failed",
|
||||||
"close": "Close"
|
"close": "Close",
|
||||||
|
"confirm": "Confirm"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
@@ -366,5 +367,30 @@
|
|||||||
"smartRoutingConfigUpdated": "Smart routing configuration updated successfully",
|
"smartRoutingConfigUpdated": "Smart routing configuration updated successfully",
|
||||||
"smartRoutingRequiredFields": "Database URL and OpenAI API Key are required to enable smart routing",
|
"smartRoutingRequiredFields": "Database URL and OpenAI API Key are required to enable smart routing",
|
||||||
"smartRoutingValidationError": "Please fill in the required fields before enabling Smart Routing: {{fields}}"
|
"smartRoutingValidationError": "Please fill in the required fields before enabling Smart Routing: {{fields}}"
|
||||||
|
},
|
||||||
|
"dxt": {
|
||||||
|
"upload": "Upload",
|
||||||
|
"uploadTitle": "Upload DXT Extension",
|
||||||
|
"dropFileHere": "Drop your .dxt file here",
|
||||||
|
"orClickToSelect": "or click to select from your computer",
|
||||||
|
"invalidFileType": "Please select a valid .dxt file",
|
||||||
|
"noFileSelected": "Please select a .dxt file to upload",
|
||||||
|
"uploading": "Uploading...",
|
||||||
|
"uploadFailed": "Failed to upload DXT file",
|
||||||
|
"installServer": "Install MCP Server from DXT",
|
||||||
|
"extensionInfo": "Extension Information",
|
||||||
|
"name": "Name",
|
||||||
|
"version": "Version",
|
||||||
|
"description": "Description",
|
||||||
|
"author": "Author",
|
||||||
|
"tools": "Tools",
|
||||||
|
"serverName": "Server Name",
|
||||||
|
"serverNamePlaceholder": "Enter a name for this server",
|
||||||
|
"install": "Install",
|
||||||
|
"installing": "Installing...",
|
||||||
|
"installFailed": "Failed to install server from DXT",
|
||||||
|
"serverExistsTitle": "Server Already Exists",
|
||||||
|
"serverExistsConfirm": "Server '{{serverName}}' already exists. Do you want to override it with the new version?",
|
||||||
|
"override": "Override"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +175,8 @@
|
|||||||
"copy": "复制",
|
"copy": "复制",
|
||||||
"copySuccess": "已复制到剪贴板",
|
"copySuccess": "已复制到剪贴板",
|
||||||
"copyFailed": "复制失败",
|
"copyFailed": "复制失败",
|
||||||
"close": "关闭"
|
"close": "关闭",
|
||||||
|
"confirm": "确认"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "仪表盘",
|
"dashboard": "仪表盘",
|
||||||
@@ -368,5 +369,30 @@
|
|||||||
"smartRoutingConfigUpdated": "智能路由配置更新成功",
|
"smartRoutingConfigUpdated": "智能路由配置更新成功",
|
||||||
"smartRoutingRequiredFields": "启用智能路由需要填写数据库连接地址和 OpenAI API 密钥",
|
"smartRoutingRequiredFields": "启用智能路由需要填写数据库连接地址和 OpenAI API 密钥",
|
||||||
"smartRoutingValidationError": "启用智能路由前请先填写必要字段:{{fields}}"
|
"smartRoutingValidationError": "启用智能路由前请先填写必要字段:{{fields}}"
|
||||||
|
},
|
||||||
|
"dxt": {
|
||||||
|
"upload": "上传",
|
||||||
|
"uploadTitle": "上传 DXT 扩展",
|
||||||
|
"dropFileHere": "将 .dxt 文件拖拽到此处",
|
||||||
|
"orClickToSelect": "或点击从计算机选择",
|
||||||
|
"invalidFileType": "请选择有效的 .dxt 文件",
|
||||||
|
"noFileSelected": "请选择要上传的 .dxt 文件",
|
||||||
|
"uploading": "上传中...",
|
||||||
|
"uploadFailed": "上传 DXT 文件失败",
|
||||||
|
"installServer": "从 DXT 安装 MCP 服务器",
|
||||||
|
"extensionInfo": "扩展信息",
|
||||||
|
"name": "名称",
|
||||||
|
"version": "版本",
|
||||||
|
"description": "描述",
|
||||||
|
"author": "作者",
|
||||||
|
"tools": "工具",
|
||||||
|
"serverName": "服务器名称",
|
||||||
|
"serverNamePlaceholder": "为此服务器输入名称",
|
||||||
|
"install": "安装",
|
||||||
|
"installing": "安装中...",
|
||||||
|
"installFailed": "从 DXT 安装服务器失败",
|
||||||
|
"serverExistsTitle": "服务器已存在",
|
||||||
|
"serverExistsConfirm": "服务器 '{{serverName}}' 已存在。是否要用新版本覆盖它?",
|
||||||
|
"override": "覆盖"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import ServerCard from '@/components/ServerCard';
|
|||||||
import AddServerForm from '@/components/AddServerForm';
|
import AddServerForm from '@/components/AddServerForm';
|
||||||
import EditServerForm from '@/components/EditServerForm';
|
import EditServerForm from '@/components/EditServerForm';
|
||||||
import { useServerData } from '@/hooks/useServerData';
|
import { useServerData } from '@/hooks/useServerData';
|
||||||
|
import DxtUploadForm from '@/components/DxtUploadForm';
|
||||||
|
|
||||||
const ServersPage: React.FC = () => {
|
const ServersPage: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -23,6 +24,7 @@ const ServersPage: React.FC = () => {
|
|||||||
} = useServerData();
|
} = useServerData();
|
||||||
const [editingServer, setEditingServer] = useState<Server | null>(null);
|
const [editingServer, setEditingServer] = useState<Server | null>(null);
|
||||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
|
const [showDxtUpload, setShowDxtUpload] = useState(false);
|
||||||
|
|
||||||
const handleEditClick = async (server: Server) => {
|
const handleEditClick = async (server: Server) => {
|
||||||
const fullServerData = await handleServerEdit(server);
|
const fullServerData = await handleServerEdit(server);
|
||||||
@@ -47,6 +49,12 @@ const ServersPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDxtUploadSuccess = (_serverConfig: any) => {
|
||||||
|
// Close upload dialog and refresh servers
|
||||||
|
setShowDxtUpload(false);
|
||||||
|
triggerRefresh();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className="flex justify-between items-center mb-8">
|
||||||
@@ -62,6 +70,15 @@ const ServersPage: React.FC = () => {
|
|||||||
{t('nav.market')}
|
{t('nav.market')}
|
||||||
</button>
|
</button>
|
||||||
<AddServerForm onAdd={handleServerAdd} />
|
<AddServerForm onAdd={handleServerAdd} />
|
||||||
|
<button
|
||||||
|
onClick={() => setShowDxtUpload(true)}
|
||||||
|
className="px-4 py-2 bg-blue-100 text-blue-800 rounded hover:bg-blue-200 flex items-center"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M5.5 13a3.5 3.5 0 01-.369-6.98 4 4 0 117.753-1.977A4.5 4.5 0 1113.5 13H11V9.413l1.293 1.293a1 1 0 001.414-1.414l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.413V13H5.5z" />
|
||||||
|
</svg>
|
||||||
|
{t('dxt.upload')}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleRefresh}
|
onClick={handleRefresh}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
@@ -138,6 +155,13 @@ const ServersPage: React.FC = () => {
|
|||||||
onCancel={() => setEditingServer(null)}
|
onCancel={() => setEditingServer(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showDxtUpload && (
|
||||||
|
<DxtUploadForm
|
||||||
|
onSuccess={handleDxtUploadSuccess}
|
||||||
|
onCancel={() => setShowDxtUpload(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,7 +47,10 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/swagger-parser": "^11.0.1",
|
"@apidevtools/swagger-parser": "^11.0.1",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
|
"@types/adm-zip": "^0.5.7",
|
||||||
|
"@types/multer": "^1.4.13",
|
||||||
"@types/pg": "^8.15.2",
|
"@types/pg": "^8.15.2",
|
||||||
|
"adm-zip": "^0.5.16",
|
||||||
"axios": "^1.10.0",
|
"axios": "^1.10.0",
|
||||||
"bcryptjs": "^3.0.2",
|
"bcryptjs": "^3.0.2",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
@@ -55,6 +58,7 @@
|
|||||||
"express": "^4.21.2",
|
"express": "^4.21.2",
|
||||||
"express-validator": "^7.2.1",
|
"express-validator": "^7.2.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"multer": "^2.0.1",
|
||||||
"openai": "^4.103.0",
|
"openai": "^4.103.0",
|
||||||
"openapi-types": "^12.1.3",
|
"openapi-types": "^12.1.3",
|
||||||
"pg": "^8.16.0",
|
"pg": "^8.16.0",
|
||||||
|
|||||||
111
pnpm-lock.yaml
generated
111
pnpm-lock.yaml
generated
@@ -14,9 +14,18 @@ importers:
|
|||||||
'@modelcontextprotocol/sdk':
|
'@modelcontextprotocol/sdk':
|
||||||
specifier: ^1.12.1
|
specifier: ^1.12.1
|
||||||
version: 1.12.1
|
version: 1.12.1
|
||||||
|
'@types/adm-zip':
|
||||||
|
specifier: ^0.5.7
|
||||||
|
version: 0.5.7
|
||||||
|
'@types/multer':
|
||||||
|
specifier: ^1.4.13
|
||||||
|
version: 1.4.13
|
||||||
'@types/pg':
|
'@types/pg':
|
||||||
specifier: ^8.15.2
|
specifier: ^8.15.2
|
||||||
version: 8.15.4
|
version: 8.15.4
|
||||||
|
adm-zip:
|
||||||
|
specifier: ^0.5.16
|
||||||
|
version: 0.5.16
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.10.0
|
specifier: ^1.10.0
|
||||||
version: 1.10.0
|
version: 1.10.0
|
||||||
@@ -38,6 +47,9 @@ importers:
|
|||||||
jsonwebtoken:
|
jsonwebtoken:
|
||||||
specifier: ^9.0.2
|
specifier: ^9.0.2
|
||||||
version: 9.0.2
|
version: 9.0.2
|
||||||
|
multer:
|
||||||
|
specifier: ^2.0.1
|
||||||
|
version: 2.0.1
|
||||||
openai:
|
openai:
|
||||||
specifier: ^4.103.0
|
specifier: ^4.103.0
|
||||||
version: 4.104.0(zod@3.25.48)
|
version: 4.104.0(zod@3.25.48)
|
||||||
@@ -616,85 +628,72 @@ packages:
|
|||||||
resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
|
resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm@1.1.0':
|
'@img/sharp-libvips-linux-arm@1.1.0':
|
||||||
resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
|
resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-ppc64@1.1.0':
|
'@img/sharp-libvips-linux-ppc64@1.1.0':
|
||||||
resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
|
resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-s390x@1.1.0':
|
'@img/sharp-libvips-linux-s390x@1.1.0':
|
||||||
resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
|
resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.1.0':
|
'@img/sharp-libvips-linux-x64@1.1.0':
|
||||||
resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
|
resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
|
'@img/sharp-libvips-linuxmusl-arm64@1.1.0':
|
||||||
resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==}
|
resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
|
'@img/sharp-libvips-linuxmusl-x64@1.1.0':
|
||||||
resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==}
|
resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-linux-arm64@0.34.2':
|
'@img/sharp-linux-arm64@0.34.2':
|
||||||
resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==}
|
resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linux-arm@0.34.2':
|
'@img/sharp-linux-arm@0.34.2':
|
||||||
resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==}
|
resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linux-s390x@0.34.2':
|
'@img/sharp-linux-s390x@0.34.2':
|
||||||
resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==}
|
resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.34.2':
|
'@img/sharp-linux-x64@0.34.2':
|
||||||
resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==}
|
resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-arm64@0.34.2':
|
'@img/sharp-linuxmusl-arm64@0.34.2':
|
||||||
resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==}
|
resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-x64@0.34.2':
|
'@img/sharp-linuxmusl-x64@0.34.2':
|
||||||
resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==}
|
resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@img/sharp-wasm32@0.34.2':
|
'@img/sharp-wasm32@0.34.2':
|
||||||
resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==}
|
resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==}
|
||||||
@@ -870,28 +869,24 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@15.3.3':
|
'@next/swc-linux-arm64-musl@15.3.3':
|
||||||
resolution: {integrity: sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==}
|
resolution: {integrity: sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@15.3.3':
|
'@next/swc-linux-x64-gnu@15.3.3':
|
||||||
resolution: {integrity: sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==}
|
resolution: {integrity: sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@15.3.3':
|
'@next/swc-linux-x64-musl@15.3.3':
|
||||||
resolution: {integrity: sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==}
|
resolution: {integrity: sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@15.3.3':
|
'@next/swc-win32-arm64-msvc@15.3.3':
|
||||||
resolution: {integrity: sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==}
|
resolution: {integrity: sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==}
|
||||||
@@ -1105,67 +1100,56 @@ packages:
|
|||||||
resolution: {integrity: sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==}
|
resolution: {integrity: sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.40.1':
|
'@rollup/rollup-linux-arm-musleabihf@4.40.1':
|
||||||
resolution: {integrity: sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==}
|
resolution: {integrity: sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.40.1':
|
'@rollup/rollup-linux-arm64-gnu@4.40.1':
|
||||||
resolution: {integrity: sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==}
|
resolution: {integrity: sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.40.1':
|
'@rollup/rollup-linux-arm64-musl@4.40.1':
|
||||||
resolution: {integrity: sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==}
|
resolution: {integrity: sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-loongarch64-gnu@4.40.1':
|
'@rollup/rollup-linux-loongarch64-gnu@4.40.1':
|
||||||
resolution: {integrity: sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==}
|
resolution: {integrity: sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-powerpc64le-gnu@4.40.1':
|
'@rollup/rollup-linux-powerpc64le-gnu@4.40.1':
|
||||||
resolution: {integrity: sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==}
|
resolution: {integrity: sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.40.1':
|
'@rollup/rollup-linux-riscv64-gnu@4.40.1':
|
||||||
resolution: {integrity: sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==}
|
resolution: {integrity: sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.40.1':
|
'@rollup/rollup-linux-riscv64-musl@4.40.1':
|
||||||
resolution: {integrity: sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==}
|
resolution: {integrity: sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.40.1':
|
'@rollup/rollup-linux-s390x-gnu@4.40.1':
|
||||||
resolution: {integrity: sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==}
|
resolution: {integrity: sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.40.1':
|
'@rollup/rollup-linux-x64-gnu@4.40.1':
|
||||||
resolution: {integrity: sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==}
|
resolution: {integrity: sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.40.1':
|
'@rollup/rollup-linux-x64-musl@4.40.1':
|
||||||
resolution: {integrity: sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==}
|
resolution: {integrity: sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@rollup/rollup-win32-arm64-msvc@4.40.1':
|
'@rollup/rollup-win32-arm64-msvc@4.40.1':
|
||||||
resolution: {integrity: sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==}
|
resolution: {integrity: sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg==}
|
||||||
@@ -1248,28 +1232,24 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.8':
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.8':
|
||||||
resolution: {integrity: sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==}
|
resolution: {integrity: sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.8':
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.8':
|
||||||
resolution: {integrity: sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==}
|
resolution: {integrity: sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.1.8':
|
'@tailwindcss/oxide-linux-x64-musl@4.1.8':
|
||||||
resolution: {integrity: sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==}
|
resolution: {integrity: sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.1.8':
|
'@tailwindcss/oxide-wasm32-wasi@4.1.8':
|
||||||
resolution: {integrity: sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==}
|
resolution: {integrity: sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==}
|
||||||
@@ -1319,6 +1299,9 @@ packages:
|
|||||||
'@tsconfig/node16@1.0.4':
|
'@tsconfig/node16@1.0.4':
|
||||||
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
|
||||||
|
|
||||||
|
'@types/adm-zip@0.5.7':
|
||||||
|
resolution: {integrity: sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==}
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||||
|
|
||||||
@@ -1386,6 +1369,9 @@ packages:
|
|||||||
'@types/ms@2.1.0':
|
'@types/ms@2.1.0':
|
||||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
|
'@types/multer@1.4.13':
|
||||||
|
resolution: {integrity: sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==}
|
||||||
|
|
||||||
'@types/node-fetch@2.6.12':
|
'@types/node-fetch@2.6.12':
|
||||||
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
||||||
|
|
||||||
@@ -1538,6 +1524,10 @@ packages:
|
|||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
adm-zip@0.5.16:
|
||||||
|
resolution: {integrity: sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==}
|
||||||
|
engines: {node: '>=12.0'}
|
||||||
|
|
||||||
agentkeepalive@4.6.0:
|
agentkeepalive@4.6.0:
|
||||||
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==}
|
||||||
engines: {node: '>= 8.0.0'}
|
engines: {node: '>= 8.0.0'}
|
||||||
@@ -1592,6 +1582,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==}
|
resolution: {integrity: sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==}
|
||||||
engines: {node: '>= 6.0.0'}
|
engines: {node: '>= 6.0.0'}
|
||||||
|
|
||||||
|
append-field@1.0.0:
|
||||||
|
resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==}
|
||||||
|
|
||||||
arg@4.1.3:
|
arg@4.1.3:
|
||||||
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
|
||||||
|
|
||||||
@@ -1842,6 +1835,10 @@ packages:
|
|||||||
concat-map@0.0.1:
|
concat-map@0.0.1:
|
||||||
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
|
||||||
|
|
||||||
|
concat-stream@2.0.0:
|
||||||
|
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
|
||||||
|
engines: {'0': node >= 6.0}
|
||||||
|
|
||||||
concurrently@9.1.2:
|
concurrently@9.1.2:
|
||||||
resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==}
|
resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2820,28 +2817,24 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.30.1:
|
lightningcss-linux-arm64-musl@1.30.1:
|
||||||
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.30.1:
|
lightningcss-linux-x64-gnu@1.30.1:
|
||||||
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.30.1:
|
lightningcss-linux-x64-musl@1.30.1:
|
||||||
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.30.1:
|
lightningcss-win32-arm64-msvc@1.30.1:
|
||||||
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
|
||||||
@@ -3022,6 +3015,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
|
resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
mkdirp@0.5.6:
|
||||||
|
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
mkdirp@1.0.4:
|
mkdirp@1.0.4:
|
||||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3038,6 +3035,10 @@ packages:
|
|||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
multer@2.0.1:
|
||||||
|
resolution: {integrity: sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==}
|
||||||
|
engines: {node: '>= 10.16.0'}
|
||||||
|
|
||||||
nanoid@3.3.11:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@@ -3877,6 +3878,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
typedarray@0.0.6:
|
||||||
|
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||||
|
|
||||||
typeorm@0.3.24:
|
typeorm@0.3.24:
|
||||||
resolution: {integrity: sha512-4IrHG7A0tY8l5gEGXfW56VOMfUVWEkWlH/h5wmcyZ+V8oCiLj7iTPp0lEjMEZVrxEkGSdP9ErgTKHKXQApl/oA==}
|
resolution: {integrity: sha512-4IrHG7A0tY8l5gEGXfW56VOMfUVWEkWlH/h5wmcyZ+V8oCiLj7iTPp0lEjMEZVrxEkGSdP9ErgTKHKXQApl/oA==}
|
||||||
engines: {node: '>=16.13.0'}
|
engines: {node: '>=16.13.0'}
|
||||||
@@ -5156,6 +5160,10 @@ snapshots:
|
|||||||
|
|
||||||
'@tsconfig/node16@1.0.4': {}
|
'@tsconfig/node16@1.0.4': {}
|
||||||
|
|
||||||
|
'@types/adm-zip@0.5.7':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 22.15.29
|
||||||
|
|
||||||
'@types/babel__core@7.20.5':
|
'@types/babel__core@7.20.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.27.4
|
'@babel/parser': 7.27.4
|
||||||
@@ -5242,6 +5250,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
|
'@types/multer@1.4.13':
|
||||||
|
dependencies:
|
||||||
|
'@types/express': 4.17.22
|
||||||
|
|
||||||
'@types/node-fetch@2.6.12':
|
'@types/node-fetch@2.6.12':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.15.29
|
'@types/node': 22.15.29
|
||||||
@@ -5436,6 +5448,8 @@ snapshots:
|
|||||||
|
|
||||||
acorn@8.14.1: {}
|
acorn@8.14.1: {}
|
||||||
|
|
||||||
|
adm-zip@0.5.16: {}
|
||||||
|
|
||||||
agentkeepalive@4.6.0:
|
agentkeepalive@4.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
humanize-ms: 1.2.1
|
humanize-ms: 1.2.1
|
||||||
@@ -5483,6 +5497,8 @@ snapshots:
|
|||||||
|
|
||||||
app-root-path@3.1.0: {}
|
app-root-path@3.1.0: {}
|
||||||
|
|
||||||
|
append-field@1.0.0: {}
|
||||||
|
|
||||||
arg@4.1.3: {}
|
arg@4.1.3: {}
|
||||||
|
|
||||||
argparse@1.0.10:
|
argparse@1.0.10:
|
||||||
@@ -5774,6 +5790,13 @@ snapshots:
|
|||||||
|
|
||||||
concat-map@0.0.1: {}
|
concat-map@0.0.1: {}
|
||||||
|
|
||||||
|
concat-stream@2.0.0:
|
||||||
|
dependencies:
|
||||||
|
buffer-from: 1.1.2
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
typedarray: 0.0.6
|
||||||
|
|
||||||
concurrently@9.1.2:
|
concurrently@9.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
@@ -7176,6 +7199,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minipass: 7.1.2
|
minipass: 7.1.2
|
||||||
|
|
||||||
|
mkdirp@0.5.6:
|
||||||
|
dependencies:
|
||||||
|
minimist: 1.2.8
|
||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
mkdirp@3.0.1: {}
|
mkdirp@3.0.1: {}
|
||||||
@@ -7184,6 +7211,16 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
multer@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
append-field: 1.0.0
|
||||||
|
busboy: 1.6.0
|
||||||
|
concat-stream: 2.0.0
|
||||||
|
mkdirp: 0.5.6
|
||||||
|
object-assign: 4.1.1
|
||||||
|
type-is: 1.6.18
|
||||||
|
xtend: 4.0.2
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
natural-compare@1.4.0: {}
|
natural-compare@1.4.0: {}
|
||||||
@@ -8030,6 +8067,8 @@ snapshots:
|
|||||||
media-typer: 1.1.0
|
media-typer: 1.1.0
|
||||||
mime-types: 3.0.1
|
mime-types: 3.0.1
|
||||||
|
|
||||||
|
typedarray@0.0.6: {}
|
||||||
|
|
||||||
typeorm@0.3.24(pg@8.16.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)):
|
typeorm@0.3.24(pg@8.16.0)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sqltools/formatter': 1.2.5
|
'@sqltools/formatter': 1.2.5
|
||||||
|
|||||||
156
src/controllers/dxtController.ts
Normal file
156
src/controllers/dxtController.ts
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import multer from 'multer';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import AdmZip from 'adm-zip';
|
||||||
|
import { ApiResponse } from '../types/index.js';
|
||||||
|
|
||||||
|
// Get the directory name in ESM
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
// Configure multer for file uploads
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: (req, file, cb) => {
|
||||||
|
const uploadDir = path.join(__dirname, '../../data/uploads/dxt');
|
||||||
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
cb(null, uploadDir);
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const originalName = path.parse(file.originalname).name;
|
||||||
|
cb(null, `${originalName}-${timestamp}.dxt`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage,
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (file.originalname.endsWith('.dxt')) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Only .dxt files are allowed'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limits: {
|
||||||
|
fileSize: 100 * 1024 * 1024, // 100MB limit
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const uploadMiddleware = upload.single('dxtFile');
|
||||||
|
|
||||||
|
// Clean up old DXT server files when installing a new version
|
||||||
|
const cleanupOldDxtServer = (serverName: string): void => {
|
||||||
|
try {
|
||||||
|
const uploadDir = path.join(__dirname, '../../data/uploads/dxt');
|
||||||
|
const serverPattern = `server-${serverName}`;
|
||||||
|
|
||||||
|
if (fs.existsSync(uploadDir)) {
|
||||||
|
const files = fs.readdirSync(uploadDir);
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (file.startsWith(serverPattern)) {
|
||||||
|
const filePath = path.join(uploadDir, file);
|
||||||
|
if (fs.statSync(filePath).isDirectory()) {
|
||||||
|
fs.rmSync(filePath, { recursive: true, force: true });
|
||||||
|
console.log(`Cleaned up old DXT server directory: ${filePath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to cleanup old DXT server files:', error);
|
||||||
|
// Don't fail the installation if cleanup fails
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadDxtFile = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'No DXT file uploaded',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dxtFilePath = req.file.path;
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const tempExtractDir = path.join(path.dirname(dxtFilePath), `temp-extracted-${timestamp}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract the DXT file (which is a ZIP archive) to a temporary directory first
|
||||||
|
const zip = new AdmZip(dxtFilePath);
|
||||||
|
zip.extractAllTo(tempExtractDir, true);
|
||||||
|
|
||||||
|
// Read and validate the manifest.json
|
||||||
|
const manifestPath = path.join(tempExtractDir, 'manifest.json');
|
||||||
|
if (!fs.existsSync(manifestPath)) {
|
||||||
|
throw new Error('manifest.json not found in DXT file');
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifestContent = fs.readFileSync(manifestPath, 'utf-8');
|
||||||
|
const manifest = JSON.parse(manifestContent);
|
||||||
|
|
||||||
|
// Validate required fields in manifest
|
||||||
|
if (!manifest.dxt_version) {
|
||||||
|
throw new Error('Invalid manifest: missing dxt_version');
|
||||||
|
}
|
||||||
|
if (!manifest.name) {
|
||||||
|
throw new Error('Invalid manifest: missing name');
|
||||||
|
}
|
||||||
|
if (!manifest.version) {
|
||||||
|
throw new Error('Invalid manifest: missing version');
|
||||||
|
}
|
||||||
|
if (!manifest.server) {
|
||||||
|
throw new Error('Invalid manifest: missing server configuration');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use server name as the final extract directory for automatic version management
|
||||||
|
const finalExtractDir = path.join(path.dirname(dxtFilePath), `server-${manifest.name}`);
|
||||||
|
|
||||||
|
// Clean up any existing version of this server
|
||||||
|
cleanupOldDxtServer(manifest.name);
|
||||||
|
|
||||||
|
// Move the temporary directory to the final location
|
||||||
|
fs.renameSync(tempExtractDir, finalExtractDir);
|
||||||
|
console.log(`DXT server extracted to: ${finalExtractDir}`);
|
||||||
|
|
||||||
|
// Clean up the uploaded DXT file
|
||||||
|
fs.unlinkSync(dxtFilePath);
|
||||||
|
|
||||||
|
const response: ApiResponse = {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
manifest,
|
||||||
|
extractDir: finalExtractDir,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
res.json(response);
|
||||||
|
} catch (extractError) {
|
||||||
|
// Clean up files on error
|
||||||
|
if (fs.existsSync(dxtFilePath)) {
|
||||||
|
fs.unlinkSync(dxtFilePath);
|
||||||
|
}
|
||||||
|
if (fs.existsSync(tempExtractDir)) {
|
||||||
|
fs.rmSync(tempExtractDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
throw extractError;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('DXT upload error:', error);
|
||||||
|
|
||||||
|
let message = 'Failed to process DXT file';
|
||||||
|
if (error instanceof Error) {
|
||||||
|
message = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -3,8 +3,8 @@ import { ApiResponse, AddServerRequest } from '../types/index.js';
|
|||||||
import {
|
import {
|
||||||
getServersInfo,
|
getServersInfo,
|
||||||
addServer,
|
addServer,
|
||||||
|
addOrUpdateServer,
|
||||||
removeServer,
|
removeServer,
|
||||||
updateMcpServer,
|
|
||||||
notifyToolChanged,
|
notifyToolChanged,
|
||||||
syncToolEmbedding,
|
syncToolEmbedding,
|
||||||
toggleServerStatus,
|
toggleServerStatus,
|
||||||
@@ -264,7 +264,7 @@ export const updateServer = async (req: Request, res: Response): Promise<void> =
|
|||||||
config.keepAliveInterval = 60000; // Default 60 seconds for SSE servers
|
config.keepAliveInterval = 60000; // Default 60 seconds for SSE servers
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await updateMcpServer(name, config);
|
const result = await addOrUpdateServer(name, config, true); // Allow override for updates
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
notifyToolChanged();
|
notifyToolChanged();
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { login, register, getCurrentUser, changePassword } from '../controllers/
|
|||||||
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
|
import { getAllLogs, clearLogs, streamLogs } from '../controllers/logController.js';
|
||||||
import { getRuntimeConfig, getPublicConfig } from '../controllers/configController.js';
|
import { getRuntimeConfig, getPublicConfig } from '../controllers/configController.js';
|
||||||
import { callTool } from '../controllers/toolController.js';
|
import { callTool } from '../controllers/toolController.js';
|
||||||
|
import { uploadDxtFile, uploadMiddleware } from '../controllers/dxtController.js';
|
||||||
import { auth } from '../middlewares/auth.js';
|
import { auth } from '../middlewares/auth.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@@ -67,6 +68,9 @@ export const initRoutes = (app: express.Application): void => {
|
|||||||
// Tool management routes
|
// Tool management routes
|
||||||
router.post('/tools/call/:server', callTool);
|
router.post('/tools/call/:server', callTool);
|
||||||
|
|
||||||
|
// DXT upload routes
|
||||||
|
router.post('/dxt/upload', uploadMiddleware, uploadDxtFile);
|
||||||
|
|
||||||
// Market routes
|
// Market routes
|
||||||
router.get('/market/servers', getAllMarketServers);
|
router.get('/market/servers', getAllMarketServers);
|
||||||
router.get('/market/servers/search', searchMarketServersByQuery);
|
router.get('/market/servers/search', searchMarketServersByQuery);
|
||||||
|
|||||||
@@ -513,6 +513,42 @@ export const updateMcpServer = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Add or update server (supports overriding existing servers for DXT)
|
||||||
|
export const addOrUpdateServer = async (
|
||||||
|
name: string,
|
||||||
|
config: ServerConfig,
|
||||||
|
allowOverride: boolean = false,
|
||||||
|
): Promise<{ success: boolean; message?: string }> => {
|
||||||
|
try {
|
||||||
|
const settings = loadSettings();
|
||||||
|
const exists = !!settings.mcpServers[name];
|
||||||
|
|
||||||
|
if (exists && !allowOverride) {
|
||||||
|
return { success: false, message: 'Server name already exists' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If overriding and this is a DXT server (stdio type with file paths),
|
||||||
|
// we might want to clean up old files in the future
|
||||||
|
if (exists && config.type === 'stdio') {
|
||||||
|
// Close existing server connections
|
||||||
|
closeServer(name);
|
||||||
|
// Remove from server infos
|
||||||
|
serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.mcpServers[name] = config;
|
||||||
|
if (!saveSettings(settings)) {
|
||||||
|
return { success: false, message: 'Failed to save settings' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = exists ? 'updated' : 'added';
|
||||||
|
return { success: true, message: `Server ${action} successfully` };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to add/update server: ${name}`, error);
|
||||||
|
return { success: false, message: 'Failed to add/update server' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Close server client and transport
|
// Close server client and transport
|
||||||
function closeServer(name: string) {
|
function closeServer(name: string) {
|
||||||
const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
|
const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
|
||||||
|
|||||||
Reference in New Issue
Block a user