V0.21.13.250611_alpha:修复了一些后端及图标bug

This commit is contained in:
jinchao 2025-06-11 16:39:04 +08:00
parent 455f4ca783
commit a3dc807854
10 changed files with 444 additions and 836 deletions

Binary file not shown.

View File

@ -23,6 +23,12 @@ class DataMonitor extends HTMLElement {
this.handleTreeItemDblClick = this.handleTreeItemDblClick.bind(this);
this.handlePlot = this.handlePlot.bind(this);
this.handleDelete = this.handleDelete.bind(this);
this.handleCsvInject = this.handleCsvInject.bind(this);
this.handleModalClose = this.handleModalClose.bind(this);
this.handleModalCancel = this.handleModalCancel.bind(this);
this.handleModalConfirm = this.handleModalConfirm.bind(this);
this.handleFileInput = this.handleFileInput.bind(this);
this.validateCsvFile = this.validateCsvFile.bind(this);
// 确保 FloatingChartWindow 组件已注册
if (!customElements.get('floating-chart-window')) {
@ -1115,6 +1121,189 @@ class DataMonitor extends HTMLElement {
background-color: #f5f5f5;
cursor: not-allowed;
}
.csv-inject-button {
padding: 6px 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.3s;
}
.csv-inject-button:hover {
background: #40a9ff;
}
.csv-inject-button:disabled {
background: #d9d9d9;
cursor: not-allowed;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-overlay.active {
display: flex;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 24px;
width: 400px;
max-width: 90%;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.modal-title {
font-size: 18px;
font-weight: 500;
color: #262626;
margin: 0;
}
.modal-close {
background: none;
border: none;
font-size: 20px;
color: #999;
cursor: pointer;
padding: 4px;
line-height: 1;
}
.modal-close:hover {
color: #666;
}
.modal-body {
margin-bottom: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #262626;
font-size: 14px;
}
.form-group input {
width: 100%;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.form-group input:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.modal-button {
padding: 8px 16px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.modal-button.primary {
background: #1890ff;
color: white;
border: none;
}
.modal-button.primary:hover {
background: #40a9ff;
}
.modal-button.secondary {
background: white;
color: #262626;
border: 1px solid #d9d9d9;
}
.modal-button.secondary:hover {
border-color: #1890ff;
color: #1890ff;
}
.file-input-wrapper {
position: relative;
display: inline-block;
width: 100%;
}
.file-input-wrapper input[type="file"] {
position: absolute;
left: 0;
top: 0;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-input-trigger {
display: flex;
align-items: center;
justify-content: center;
padding: 8px;
border: 1px dashed #d9d9d9;
border-radius: 4px;
color: #666;
font-size: 14px;
cursor: pointer;
transition: all 0.3s;
}
.file-input-trigger:hover {
border-color: #1890ff;
color: #1890ff;
}
.file-name {
margin-top: 8px;
font-size: 14px;
color: #666;
word-break: break-all;
}
</style>
<div class="toolbar">
<div class="toolbar-left">
@ -1123,6 +1312,44 @@ class DataMonitor extends HTMLElement {
<span id="statusText">未监控</span>
</div>
</div>
<div class="toolbar-right">
<button class="csv-inject-button" id="csvInjectButton">
<img src="assets/icons/png/file.png" alt="CSV" style="width: 16px; height: 16px;">
CSV文件注入
</button>
</div>
</div>
<div class="modal-overlay" id="csvModal">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">CSV文件注入</h3>
<button class="modal-close" id="modalClose">&times;</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>选择CSV文件</label>
<div class="file-input-wrapper">
<div class="file-input-trigger" id="fileInputTrigger">
点击或拖拽文件到此处
</div>
<input type="file" id="csvFileInput" accept=".csv" />
</div>
<div class="file-name" id="fileName"></div>
</div>
<div class="form-group">
<label>注入频率 (Hz)</label>
<input type="number" id="injectFrequency" min="10" max="10000" step="10" value="100" />
</div>
<div class="form-group">
<label>注入时长 ()</label>
<input type="number" id="injectDuration" min="1" max="3600" step="1" value="60" />
</div>
</div>
<div class="modal-footer">
<button class="modal-button secondary" id="modalCancel">取消</button>
<button class="modal-button primary" id="modalConfirm">确认</button>
</div>
</div>
</div>
<div class="monitor-container">
<div class="tree-container">
@ -1562,6 +1789,61 @@ class DataMonitor extends HTMLElement {
}
}
});
// 添加CSV文件注入相关的事件监听
const csvInjectButton = this.shadowRoot.getElementById('csvInjectButton');
const modal = this.shadowRoot.getElementById('csvModal');
const modalClose = this.shadowRoot.getElementById('modalClose');
const modalCancel = this.shadowRoot.getElementById('modalCancel');
const modalConfirm = this.shadowRoot.getElementById('modalConfirm');
const fileInput = this.shadowRoot.getElementById('csvFileInput');
const fileInputTrigger = this.shadowRoot.getElementById('fileInputTrigger');
if (csvInjectButton) {
csvInjectButton.addEventListener('click', this.handleCsvInject);
}
if (modalClose) {
modalClose.addEventListener('click', this.handleModalClose);
}
if (modalCancel) {
modalCancel.addEventListener('click', this.handleModalCancel);
}
if (modalConfirm) {
modalConfirm.addEventListener('click', this.handleModalConfirm);
}
if (fileInput) {
fileInput.addEventListener('change', this.handleFileInput);
}
if (fileInputTrigger) {
fileInputTrigger.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
fileInputTrigger.style.borderColor = '#1890ff';
});
fileInputTrigger.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
fileInputTrigger.style.borderColor = '#d9d9d9';
});
fileInputTrigger.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
fileInputTrigger.style.borderColor = '#d9d9d9';
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
this.handleFileInput({ target: fileInput });
}
});
}
}
/**
@ -1669,6 +1951,94 @@ class DataMonitor extends HTMLElement {
}
}
/**
* @description 处理CSV文件注入按钮点击事件
*/
handleCsvInject() {
const modal = this.shadowRoot.getElementById('csvModal');
if (modal) {
modal.classList.add('active');
}
}
/**
* @description 处理模态框关闭事件
*/
handleModalClose() {
const modal = this.shadowRoot.getElementById('csvModal');
if (modal) {
modal.classList.remove('active');
}
}
/**
* @description 处理模态框取消按钮点击事件
*/
handleModalCancel() {
this.handleModalClose();
}
/**
* @description 处理模态框确认按钮点击事件
*/
async handleModalConfirm() {
const fileInput = this.shadowRoot.getElementById('csvFileInput');
const frequencyInput = this.shadowRoot.getElementById('injectFrequency');
const durationInput = this.shadowRoot.getElementById('injectDuration');
if (!fileInput.files.length) {
alert('请选择CSV文件');
return;
}
const file = fileInput.files[0];
const frequency = parseInt(frequencyInput.value);
const duration = parseInt(durationInput.value);
if (!this.validateCsvFile(file)) {
return;
}
try {
// TODO: 实现CSV文件注入逻辑
console.log('CSV文件注入:', {
file: file.name,
frequency,
duration
});
this.handleModalClose();
} catch (error) {
console.error('CSV文件注入失败:', error);
alert(`CSV文件注入失败: ${error.message}`);
}
}
/**
* @description 处理文件输入事件
* @param {Event} event - 文件输入事件
*/
handleFileInput(event) {
const file = event.target.files[0];
const fileName = this.shadowRoot.getElementById('fileName');
if (file) {
fileName.textContent = file.name;
} else {
fileName.textContent = '';
}
}
/**
* @description 验证CSV文件
* @param {File} file - CSV文件对象
* @returns {boolean} 是否验证通过
*/
validateCsvFile(file) {
// TODO: 实现CSV文件验证逻辑
return true;
}
disconnectedCallback() {
this.isActive = false;
// 清理所有图表窗口

View File

@ -289,7 +289,7 @@ class RunLog extends HTMLElement {
const logFiles = [];
for (const fileName of files) {
if (fileName.endsWith('.log')) {
const filePath = `${logDirPath}/${fileName}`;
const filePath = fileName; // 直接使用文件名,因为后端已经处理了完整路径
const fileStats = await this.getFileStats(filePath);
// 格式化修改时间
@ -317,7 +317,7 @@ class RunLog extends HTMLElement {
// 读取目录内容
async readLogDirectory(dirPath) {
return new Promise((resolve, reject) => {
fetch(`/api/filesystem/readdir?path=${encodeURIComponent(dirPath)}`)
fetch('/api/filesystem/readdir') // 移除路径参数,因为后端已经处理了
.then(response => {
if (!response.ok) {
throw new Error(`读取目录失败: ${response.status}`);
@ -337,7 +337,7 @@ class RunLog extends HTMLElement {
// 获取文件状态信息
async getFileStats(filePath) {
return new Promise((resolve, reject) => {
fetch(`/api/filesystem/stat?path=${encodeURIComponent(filePath)}`)
fetch(`/api/filesystem/stat?path=${encodeURIComponent(filePath)}`) // 只传递文件名
.then(response => {
if (!response.ok) {
throw new Error(`获取文件信息失败: ${response.status}`);

View File

@ -600,6 +600,19 @@
return;
}
// 处理网络监控标签页
if (title === '网络监控') {
const id = 'network-monitor';
tabsContainer.createTab(id, title, icon, parentText, parentTool);
return;
}
// 处理QTG标签页
if (title === 'QTG') {
const id = 'qtg-monitor';
tabsContainer.createTab(id, title, icon, parentText, parentTool);
return;
}
// 默认情况下使用标题转换为ID
const id = title.toLowerCase().replace(/\s+/g, '-');
tabsContainer.createTab(id, title, icon, parentText, parentTool);
@ -625,7 +638,9 @@
'用户管理': 'users',
'模型开发': 'cube',
'服务开发': 'settings',
'系统日志': 'file-text'
'系统日志': 'file-text',
'网络监控': 'network',
'QTG': 'qtg'
};
return iconMap[title] || parentTool || 'con';

View File

@ -1,37 +1,31 @@
const express = require('express');
const router = express.Router();
const fs = require('fs').promises;
const path = require('path');
const { getActualLogPath } = require('../utils/file-utils');
// 读取目录内容
router.get('/readdir', async (req, res) => {
try {
const dirPath = req.query.path;
if (!dirPath) {
return res.status(400).json({ error: '缺少路径参数' });
}
// 获取实际的文件系统路径
const actualPath = await getActualLogPath(dirPath);
// 获取实际的日志目录路径
const logDirPath = await getActualLogPath();
// 安全检查
if (!actualPath) {
if (!logDirPath) {
return res.status(403).json({ error: '无权访问该目录' });
}
// 检查目录是否存在
try {
const stats = await fs.stat(actualPath);
const stats = await fs.stat(logDirPath);
if (!stats.isDirectory()) {
return res.status(400).json({ error: '指定的路径不是目录' });
}
} catch (statError) {
// 如果目录不存在但是请求的是/log,尝试创建它
if (statError.code === 'ENOENT' && dirPath === '/log') {
// 如果目录不存在,尝试创建它
if (statError.code === 'ENOENT') {
try {
// 确保XNCore下有log目录
await fs.mkdir(actualPath, { recursive: true });
await fs.mkdir(logDirPath, { recursive: true });
} catch (mkdirError) {
console.error('创建日志目录失败:', mkdirError);
return res.status(500).json({ error: '创建日志目录失败' });
@ -42,7 +36,7 @@ router.get('/readdir', async (req, res) => {
}
// 读取目录内容
const files = await fs.readdir(actualPath);
const files = await fs.readdir(logDirPath);
// 返回文件列表
res.json({ files });
@ -64,22 +58,27 @@ router.get('/readdir', async (req, res) => {
// 获取文件状态信息
router.get('/stat', async (req, res) => {
try {
const filePath = req.query.path;
if (!filePath) {
return res.status(400).json({ error: '缺少路径参数' });
const fileName = req.query.path;
if (!fileName) {
return res.status(400).json({ error: '未提供文件名' });
}
// 获取实际的日志目录路径
const logDirPath = await getActualLogPath();
if (!logDirPath) {
return res.status(403).json({ error: '无权访问该文件' });
}
// 构建完整的文件路径
const filePath = path.join(logDirPath, fileName);
// 获取实际的文件系统路径
const actualPath = await getActualLogPath(filePath);
// 安全检查
if (!actualPath) {
// 安全检查:确保文件在日志目录内
if (!filePath.startsWith(logDirPath)) {
return res.status(403).json({ error: '无权访问该文件' });
}
// 获取文件状态
const stats = await fs.stat(actualPath);
const stats = await fs.stat(filePath);
// 返回文件信息
res.json({
@ -109,27 +108,32 @@ router.get('/stat', async (req, res) => {
// 读取文件内容
router.get('/readFile', async (req, res) => {
try {
const filePath = req.query.path;
if (!filePath) {
return res.status(400).json({ error: '缺少路径参数' });
const fileName = req.query.path;
if (!fileName) {
return res.status(400).json({ error: '未提供文件名' });
}
// 获取实际的日志目录路径
const logDirPath = await getActualLogPath();
if (!logDirPath) {
return res.status(403).json({ error: '无权访问该文件' });
}
// 构建完整的文件路径
const filePath = path.join(logDirPath, fileName);
// 获取实际的文件系统路径
const actualPath = await getActualLogPath(filePath);
// 安全检查
if (!actualPath) {
// 安全检查:确保文件在日志目录内
if (!filePath.startsWith(logDirPath)) {
return res.status(403).json({ error: '无权访问该文件' });
}
// 只允许访问.log文件
if (!actualPath.endsWith('.log')) {
if (!fileName.endsWith('.log')) {
return res.status(403).json({ error: '只能访问日志文件' });
}
// 获取文件状态
const stats = await fs.stat(actualPath);
const stats = await fs.stat(filePath);
// 检查文件大小,限制读取过大的文件
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
@ -138,7 +142,7 @@ router.get('/readFile', async (req, res) => {
}
// 读取文件内容
const content = await fs.readFile(actualPath, 'utf-8');
const content = await fs.readFile(filePath, 'utf-8');
// 设置响应头
res.setHeader('Content-Type', 'text/plain; charset=utf-8');

View File

@ -1,189 +0,0 @@
const express = require('express');
const router = express.Router();
const fs = require('fs').promises;
const path = require('path');
// 获取XNCore环境变量
const xnCorePath = process.env.XNCore || '';
// 默认IDL文件保存目录使用XNCore环境变量下的IDL子文件夹
const IDL_BASE_DIR = xnCorePath ? path.join(xnCorePath, 'IDL') : path.join(process.cwd(), 'idl_files');
// 确保IDL文件目录存在
async function ensureIdlDirExists() {
try {
await fs.mkdir(IDL_BASE_DIR, { recursive: true });
} catch (error) {
console.error('创建IDL目录失败:', error);
}
}
// 初始化确保目录存在
ensureIdlDirExists();
// 安全地获取文件路径,避免路径遍历攻击
function getSafePath(filePath) {
// 确保文件路径以.idl结尾
if (!filePath.toLowerCase().endsWith('.idl')) {
filePath += '.idl';
}
// 使用path.join确保路径在IDL_BASE_DIR下
const safePath = path.join(IDL_BASE_DIR, path.basename(filePath));
return safePath;
}
// 列出所有IDL文件
router.get('/list', async (req, res) => {
try {
// 确保目录存在
await ensureIdlDirExists();
// 读取目录
const files = await fs.readdir(IDL_BASE_DIR);
// 过滤出.idl文件
const idlFiles = files.filter(file => file.toLowerCase().endsWith('.idl'));
// 获取文件信息
const fileInfos = await Promise.all(idlFiles.map(async (file) => {
const filePath = path.join(IDL_BASE_DIR, file);
const stats = await fs.stat(filePath);
return {
name: file,
size: stats.size,
modified: stats.mtime
};
}));
res.json({ files: fileInfos });
} catch (error) {
console.error('列出IDL文件失败:', error);
res.status(500).json({ error: '列出IDL文件失败', message: error.message });
}
});
// 读取IDL文件内容
router.get('/read', async (req, res) => {
try {
const filename = req.query.filename;
if (!filename) {
return res.status(400).json({ error: '缺少文件名参数' });
}
const filePath = getSafePath(filename);
// 读取文件内容
const content = await fs.readFile(filePath, 'utf-8');
res.json({ content, filename: path.basename(filePath) });
} catch (error) {
console.error('读取IDL文件失败:', error);
if (error.code === 'ENOENT') {
return res.status(404).json({ error: '文件不存在' });
}
res.status(500).json({ error: '读取IDL文件失败', message: error.message });
}
});
// 保存IDL文件内容
router.post('/save', async (req, res) => {
try {
const { filename, content } = req.body;
if (!filename || content === undefined) {
return res.status(400).json({ error: '缺少必要参数' });
}
// 确保目录存在
await ensureIdlDirExists();
const filePath = getSafePath(filename);
// 写入文件内容
await fs.writeFile(filePath, content, 'utf-8');
res.json({ success: true, filename: path.basename(filePath) });
} catch (error) {
console.error('保存IDL文件失败:', error);
res.status(500).json({ error: '保存IDL文件失败', message: error.message });
}
});
// 新建IDL文件
router.post('/create', async (req, res) => {
try {
const { filename } = req.body;
if (!filename) {
return res.status(400).json({ error: '缺少文件名参数' });
}
// 确保目录存在
await ensureIdlDirExists();
const filePath = getSafePath(filename);
// 检查文件是否已存在
try {
await fs.access(filePath);
return res.status(409).json({ error: '文件已存在' });
} catch (accessError) {
// 文件不存在,可以创建
}
// 创建具有默认内容的IDL文件
const defaultContent = `module XNSim
{
struct YourStruct
{
// 在此添加字段
};
};`;
// 写入文件内容
await fs.writeFile(filePath, defaultContent, 'utf-8');
res.json({
success: true,
filename: path.basename(filePath),
content: defaultContent
});
} catch (error) {
console.error('创建IDL文件失败:', error);
res.status(500).json({ error: '创建IDL文件失败', message: error.message });
}
});
// 删除IDL文件
router.delete('/delete', async (req, res) => {
try {
const filename = req.query.filename;
if (!filename) {
return res.status(400).json({ error: '缺少文件名参数' });
}
const filePath = getSafePath(filename);
// 删除文件
await fs.unlink(filePath);
res.json({ success: true, filename: path.basename(filePath) });
} catch (error) {
console.error('删除IDL文件失败:', error);
if (error.code === 'ENOENT') {
return res.status(404).json({ error: '文件不存在' });
}
res.status(500).json({ error: '删除IDL文件失败', message: error.message });
}
});
module.exports = router;

View File

@ -1,174 +0,0 @@
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs').promises;
const xml2js = require('xml2js');
const { getProjectModelPath, isPathSafe, ensureDirectoryExists } = require('../utils/file-utils');
// 递归读取目录下的所有.xnprj文件
async function readProjectFiles(dir) {
const results = [];
try {
// 确保目录存在
try {
await fs.access(dir);
} catch (error) {
if (error.code === 'ENOENT') {
console.warn(`目录不存在: ${dir}`);
return results;
}
throw error;
}
// 读取目录内容
const entries = await fs.readdir(dir, { withFileTypes: true });
// 处理每个条目
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// 递归读取子目录
const nestedResults = await readProjectFiles(fullPath);
results.push(...nestedResults);
} else if (entry.isFile() && path.extname(entry.name).toLowerCase() === '.xnprj') {
// 读取文件内容
try {
const content = await fs.readFile(fullPath, 'utf-8');
// 解析XML
try {
const parser = new xml2js.Parser({ explicitArray: false });
const result = await parser.parseStringPromise(content);
// 提取关键信息
const modelInfo = {
path: fullPath,
name: (result.Model && result.Model.n) || path.basename(entry.name, '.xnprj'),
description: (result.Model && result.Model.Description) || '',
author: (result.Model && result.Model.Author) || '',
version: (result.Model && result.Model.Version) || '',
createTime: (result.Model && result.Model.CreateTime) || '',
changeTime: (result.Model && result.Model.ChangeTime) || '',
relativePath: path.relative(getProjectModelPath(), fullPath)
};
results.push(modelInfo);
} catch (xmlError) {
console.error(`解析XML文件失败: ${fullPath}`, xmlError);
// 即使解析失败,也返回基本信息
results.push({
path: fullPath,
name: path.basename(entry.name, '.xnprj'),
description: '无法解析的XML文件',
error: true,
relativePath: path.relative(getProjectModelPath(), fullPath)
});
}
} catch (readError) {
console.error(`读取文件失败: ${fullPath}`, readError);
results.push({
path: fullPath,
name: path.basename(entry.name, '.xnprj'),
description: '无法读取的文件',
error: true,
relativePath: path.relative(getProjectModelPath(), fullPath)
});
}
}
}
} catch (error) {
console.error(`扫描目录失败: ${dir}`, error);
}
return results;
}
// 获取所有项目模型信息
router.get('/project-models', async (req, res) => {
try {
const projectModelDir = getProjectModelPath();
// 检查XNCore是否设置
if (!projectModelDir) {
console.error('XNCore环境变量未设置无法获取项目模型目录');
return res.status(500).json({
error: 'XNCore环境变量未设置',
message: '无法获取项目模型目录'
});
}
// 确保目录存在
await ensureDirectoryExists(projectModelDir);
// 读取所有项目模型文件
const modelFiles = await readProjectFiles(projectModelDir);
res.json(modelFiles);
} catch (error) {
console.error('获取项目模型文件列表失败:', error);
res.status(500).json({ error: '获取项目模型文件列表失败', message: error.message });
}
});
// 获取特定项目模型文件内容
router.get('/project-model-content', async (req, res) => {
try {
const filePath = req.query.path;
if (!filePath) {
return res.status(400).json({ error: '缺少路径参数' });
}
// 安全检查 - 确保只能访问Project/Model目录下的文件
const projectModelDir = getProjectModelPath();
if (!isPathSafe(filePath, projectModelDir)) {
return res.status(403).json({ error: '无权访问该文件' });
}
// 检查文件后缀,只允许.xnprj
const ext = path.extname(filePath).toLowerCase();
if (ext !== '.xnprj') {
return res.status(403).json({ error: '只能访问项目模型文件(.xnprj)' });
}
// 检查文件是否存在
try {
await fs.access(filePath);
} catch (error) {
if (error.code === 'ENOENT') {
return res.status(404).json({ error: '文件不存在' });
}
throw error;
}
// 获取文件状态
const stats = await fs.stat(filePath);
// 检查是否为文件
if (!stats.isFile()) {
return res.status(400).json({ error: '请求的路径不是文件' });
}
// 大文件检查
const MAX_FILE_SIZE = 1024 * 1024; // 1MB限制
if (stats.size > MAX_FILE_SIZE) {
return res.status(413).json({ error: '文件过大,无法读取' });
}
// 读取文件内容
const content = await fs.readFile(filePath, 'utf-8');
// 设置响应头
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
// 返回文件内容
res.send(content);
} catch (error) {
console.error('读取项目模型文件内容失败:', error);
res.status(500).json({ error: '读取项目模型文件内容失败', message: error.message });
}
});
module.exports = router;

View File

@ -1,372 +0,0 @@
const express = require('express');
const router = express.Router();
const path = require('path');
const fs = require('fs').promises;
const { getScenarioPath, isPathSafe, ensureDirectoryExists, validateXml } = require('../utils/file-utils');
// 获取场景文件列表
router.get('/scenario-files', async (req, res) => {
try {
const scenarioDir = getScenarioPath();
// 检查XNCore是否设置
if (!scenarioDir) {
console.error('XNCore环境变量未设置无法获取场景目录');
return res.status(500).json({
error: 'XNCore环境变量未设置',
message: '无法获取场景目录'
});
}
//console.log('获取场景文件,目录路径:', scenarioDir);
// 检查并创建目录(如果不存在)
try {
await ensureDirectoryExists(scenarioDir);
} catch (error) {
console.error('场景目录访问失败:', error);
return res.status(500).json({
error: '场景目录不存在且无法创建',
message: error.message
});
}
// 读取目录内容
const files = await fs.readdir(scenarioDir);
//console.log('目录中的文件数量:', files.length);
// 过滤出.sce和.xml文件
const scenarioFiles = [];
for (const file of files) {
const ext = path.extname(file).toLowerCase();
if (ext === '.sce' || ext === '.xml') {
const filePath = path.join(scenarioDir, file);
const stats = await fs.stat(filePath);
if (stats.isFile()) {
scenarioFiles.push({
name: file,
path: filePath,
size: stats.size,
mtime: stats.mtime
});
}
}
}
//console.log('找到的场景文件数量:', scenarioFiles.length);
res.json(scenarioFiles);
} catch (error) {
console.error('获取场景文件列表失败:', error);
res.status(500).json({ error: '获取场景文件列表失败', message: error.message });
}
});
// 获取文件内容
router.get('/file-content', async (req, res) => {
try {
const filePath = req.query.path;
if (!filePath) {
return res.status(400).json({ error: '缺少路径参数' });
}
// 安全检查 - 确保只能访问Scenario目录下的文件
const scenarioDir = getScenarioPath();
if (!isPathSafe(filePath, scenarioDir)) {
return res.status(403).json({ error: '无权访问该文件' });
}
// 检查文件后缀,只允许.sce和.xml
const ext = path.extname(filePath).toLowerCase();
if (ext !== '.sce' && ext !== '.xml') {
return res.status(403).json({ error: '只能访问场景文件(.sce或.xml)' });
}
// 检查文件是否存在
try {
await fs.access(filePath);
} catch (error) {
if (error.code === 'ENOENT') {
// 如果是.sce文件不存在尝试创建空文件
if (ext === '.sce') {
try {
await fs.writeFile(filePath, '', 'utf-8');
// 返回空内容
return res.send('');
} catch (writeError) {
console.error('创建.sce文件失败:', writeError);
return res.status(500).json({ error: '创建文件失败', message: writeError.message });
}
} else {
return res.status(404).json({ error: '文件不存在' });
}
} else {
throw error;
}
}
// 获取文件状态
const stats = await fs.stat(filePath);
// 检查是否为文件
if (!stats.isFile()) {
return res.status(400).json({ error: '请求的路径不是文件' });
}
// 大文件检查
const MAX_FILE_SIZE = 1024 * 1024; // 1MB限制
if (stats.size > MAX_FILE_SIZE) {
return res.status(413).json({ error: '文件过大,无法读取' });
}
// 读取文件内容
const content = await fs.readFile(filePath, 'utf-8');
// 设置响应头
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
// 返回文件内容
res.send(content);
} catch (error) {
console.error('读取文件内容失败:', error);
res.status(500).json({ error: '读取文件内容失败', message: error.message });
}
});
// 保存文件内容
router.post('/save-file', async (req, res) => {
try {
const { path: filePath, content } = req.body;
if (!filePath || content === undefined) {
return res.status(400).json({ error: '缺少必要参数' });
}
// 安全检查 - 确保只能访问Scenario目录下的文件
const scenarioDir = getScenarioPath();
if (!isPathSafe(filePath, scenarioDir)) {
return res.status(403).json({ error: '无权访问该文件' });
}
// 检查文件后缀,只允许.sce和.xml
const ext = path.extname(filePath).toLowerCase();
if (ext !== '.sce' && ext !== '.xml') {
return res.status(403).json({ error: '只能修改场景文件(.sce或.xml)' });
}
// 确保目录存在
await ensureDirectoryExists(path.dirname(filePath));
// 检查XML格式是否有效对于.xml和.sce文件
if (content.trim()) { // 只有当内容不为空时才检查
const validation = await validateXml(content);
if (validation !== true) {
return res.status(400).json(validation);
}
}
// 写入文件内容
await fs.writeFile(filePath, content, 'utf-8');
res.json({ success: true, message: '文件保存成功' });
} catch (error) {
console.error('保存文件内容失败:', error);
res.status(500).json({ error: '保存文件内容失败', message: error.message });
}
});
// 检查文件是否存在
router.get('/file-exists', async (req, res) => {
try {
const filePath = req.query.path;
if (!filePath) {
return res.status(400).json({ error: '缺少路径参数' });
}
// 安全检查 - 确保只能访问Scenario目录下的文件
const scenarioDir = getScenarioPath();
if (!isPathSafe(filePath, scenarioDir)) {
return res.status(403).json({ error: '无权访问该文件' });
}
// 检查文件后缀,只允许.sce和.xml
const ext = path.extname(filePath).toLowerCase();
if (ext !== '.sce' && ext !== '.xml') {
return res.status(403).json({ error: '只能检查场景文件(.sce或.xml)' });
}
// 检查文件是否存在
try {
await fs.access(filePath);
res.json({ exists: true });
} catch (error) {
if (error.code === 'ENOENT') {
res.json({ exists: false });
} else {
throw error;
}
}
} catch (error) {
console.error('检查文件存在性失败:', error);
res.status(500).json({ error: '检查文件存在性失败', message: error.message });
}
});
// 创建新配置文件
router.post('/create-config-file', async (req, res) => {
try {
const { fileName } = req.body;
if (!fileName) {
return res.status(400).json({ error: '缺少文件名参数' });
}
// 获取Scenario目录
const scenarioDir = getScenarioPath();
if (!scenarioDir) {
return res.status(500).json({ error: 'XNCore环境变量未设置' });
}
// 设置文件路径
const filePath = path.join(scenarioDir, fileName);
// 检查文件后缀,只允许.sce和.xml
const ext = path.extname(fileName).toLowerCase();
if (ext !== '.sce' && ext !== '.xml') {
return res.status(403).json({ error: '只能创建场景文件(.sce或.xml)' });
}
// 确保目录存在
await ensureDirectoryExists(scenarioDir);
// 检查文件是否已存在
try {
await fs.access(filePath);
// 文件已存在
return res.status(409).json({ error: '文件已存在' });
} catch (error) {
// 文件不存在,可以继续创建
if (error.code !== 'ENOENT') {
throw error;
}
}
// 创建基本的XML模板
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
<Scenario>
<Environment
OSName="Debian"
Version="11"
RTXVersion="preempt-rt"
CPUAffinity="0,1"
BaseFrequency="120"
WorkPath="/home/jin/Myprj/XNSim/Release/"
ModelsPath="Models/"
ServicesPath="Services/"
DomainID="10"
/>
<ConsoleOutput Debug="1" Info="1" Error="1" Warning="1" />
<Log Debug="0" Info="1" Error="1" Warning="1" />
<ModelGroup Name="新模型组" FreqGroup="0" Priority="99" CPUAff="0">
<!-- 这里添加模型 -->
</ModelGroup>
<ServicesList>
<!-- 这里添加服务 -->
</ServicesList>
</Scenario>`;
// 写入文件内容
await fs.writeFile(filePath, xmlContent, 'utf-8');
res.status(201).json({
success: true,
message: '文件创建成功',
path: filePath,
content: xmlContent
});
} catch (error) {
console.error('创建新配置文件失败:', error);
res.status(500).json({ error: '创建新配置文件失败', message: error.message });
}
});
// 另存为文件
router.post('/save-as-file', async (req, res) => {
try {
const { fileName, content, currentFile, overwrite } = req.body;
if (!fileName || content === undefined) {
return res.status(400).json({ error: '缺少必要参数' });
}
// 获取Scenario目录
const scenarioDir = getScenarioPath();
if (!scenarioDir) {
return res.status(500).json({ error: 'XNCore环境变量未设置' });
}
// 设置目标文件路径
const targetPath = path.join(scenarioDir, fileName);
// 检查文件后缀,只允许.sce和.xml
const ext = path.extname(fileName).toLowerCase();
if (ext !== '.sce' && ext !== '.xml') {
return res.status(403).json({ error: '只能保存场景文件(.sce或.xml)' });
}
// 确保目录存在
await ensureDirectoryExists(path.dirname(targetPath));
// 如果路径与当前文件相同,直接保存
if (currentFile && targetPath === currentFile) {
await fs.writeFile(targetPath, content, 'utf-8');
return res.json({
success: true,
message: '文件保存成功',
path: targetPath
});
}
// 检查文件是否已存在
try {
await fs.access(targetPath);
// 文件已存在,但没有覆盖标记
if (!overwrite) {
return res.status(409).json({
error: '文件已存在',
code: 'FILE_EXISTS'
});
}
} catch (error) {
// 文件不存在,可以直接创建
if (error.code !== 'ENOENT') {
throw error;
}
}
// 检查XML格式是否有效
if (content.trim()) {
const validation = await validateXml(content);
if (validation !== true) {
return res.status(400).json(validation);
}
}
// 写入文件内容
await fs.writeFile(targetPath, content, 'utf-8');
res.json({
success: true,
message: '文件保存成功',
path: targetPath
});
} catch (error) {
console.error('另存为文件失败:', error);
res.status(500).json({ error: '另存为文件失败', message: error.message });
}
});
module.exports = router;

View File

@ -11,10 +11,7 @@ const authRoutes = require('./routes/auth');
const versionRoutes = require('./routes/versions');
const filesystemRoutes = require('./routes/filesystem');
const systemInfoRoutes = require('./routes/system-info');
const scenarioRoutes = require('./routes/scenario-config');
const serviceApiRoutes = require('./routes/service-dev');
const idlRoutes = require('./routes/idl');
const projectModelRoutes = require('./routes/project-model');
const ataChaptersRoutes = require('./routes/model-dev');
const simulationRoutes = require('./routes/run-simulation');
const udpMonitorRoutes = require('./routes/udp-monitor');
@ -83,10 +80,7 @@ app.use('/api', authRoutes);
app.use('/api', versionRoutes);
app.use('/api/filesystem', filesystemRoutes);
app.use('/api', systemInfoRoutes);
app.use('/api', scenarioRoutes);
app.use('/api', serviceApiRoutes);
app.use('/api/idl', idlRoutes);
app.use('/api', projectModelRoutes);
app.use('/api', ataChaptersRoutes);
app.use('/api', simulationRoutes);
app.use('/api/udp-monitor', udpMonitorRoutes);

View File

@ -49,14 +49,7 @@ function getXNCorePath() {
return xnCorePath;
}
// 场景文件目录路径
function getScenarioPath() {
const xnCorePath = getXNCorePath();
if (!xnCorePath) return '';
return path.join(xnCorePath, 'Scenario');
}
// 项目模型文件目录路径
// 模型项目文件目录路径
function getProjectModelPath() {
const xnCorePath = getXNCorePath();
if (!xnCorePath) return '';
@ -70,6 +63,13 @@ function getModelPath() {
return path.join(xnCorePath, 'Models');
}
//服务项目文件目录路径
function getProjectServicePath() {
const xnCorePath = getXNCorePath();
if (!xnCorePath) return '';
return path.join(xnCorePath, 'Project', 'Service');
}
// 服务文件目录路径
function getServicePath() {
const xnCorePath = getXNCorePath();
@ -78,39 +78,14 @@ function getServicePath() {
}
// 日志目录路径处理
async function getActualLogPath(requestedPath) {
const xnCorePath = getXNCorePath();
async function getActualLogPath() {
const currentDir = process.cwd();
const logPath = path.join(currentDir, 'log');
// 如果请求的路径是/log开头
if (requestedPath.startsWith('/log')) {
// 首先尝试在XNCore下查找
const xnCoreLogPath = path.join(xnCorePath, requestedPath);
if (fs.existsSync(xnCoreLogPath)) {
return xnCoreLogPath;
}
// 如果XNCore下不存在则尝试在当前目录下查找
const currentLogPath = path.join(currentDir, requestedPath);
if (fs.existsSync(currentLogPath)) {
return currentLogPath;
}
// 如果都不存在默认使用XNCore下的路径
return xnCoreLogPath;
}
// 如果已经是绝对路径并且在XNCore下
else if (requestedPath.startsWith(xnCorePath)) {
return requestedPath;
}
// 如果已经是绝对路径并且在当前目录下
else if (requestedPath.startsWith(currentDir)) {
return requestedPath;
}
// 默认不允许访问其他路径
else {
return null;
}
// 确保日志目录存在
await ensureDirectoryExists(logPath);
return logPath;
}
// 检查文件路径安全性
@ -147,29 +122,14 @@ async function ensureDirectoryExists(dirPath) {
return false;
}
// XML 验证
async function validateXml(content) {
if (!content || !content.trim()) return true;
try {
const parser = new (require('xml2js')).Parser();
await parser.parseStringPromise(content);
return true;
} catch (xmlError) {
return { error: 'XML格式无效', details: xmlError.message };
}
}
module.exports = {
getXNCorePath,
getScenarioPath,
getModelPath,
getProjectModelPath,
getServicePath,
getActualLogPath,
isPathSafe,
ensureDirectoryExists,
validateXml,
getDBConnection,
closeDBConnection
};