CrazyIter_Bin 4 月之前
父节点
当前提交
ba6a7a960d

+ 6 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/IES.ExamClient.njsproj

@@ -28,13 +28,18 @@
     <DebugSymbols>true</DebugSymbols>
   </PropertyGroup>
   <ItemGroup>
-    <Content Include="app.原始备份.js" />
     <Content Include="app.js" />
+    <Content Include="constants.js" />
     <Content Include="header.bmp" />
     <Content Include="logo.ico" />
+    <Content Include="main.js" />
+    <Content Include="menuManager.js" />
+    <Content Include="serverManager.js" />
     <Content Include="sidebar.bmp" />
     <Content Include="package.json" />
     <Content Include="README.md" />
+    <Content Include="updateManager.js" />
+    <Content Include="utils.js" />
   </ItemGroup>
   <Import Project="$(VSToolsPath)\Node.js Tools\Microsoft.NodejsToolsV2.targets" />
 </Project>

+ 246 - 32
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/app.js

@@ -1,43 +1,216 @@
-const { app, BrowserWindow } = require('electron');
+const { app, BrowserWindow, Menu, dialog } = require('electron');
 const { exec } = require('child_process');
 const path = require('path');
 const axios = require('axios');
 const https = require('https');
 const fs = require('fs');
-const AdmZip = require('adm-zip'); // 用于解压 zip 文件
-// 忽略证书的检测
-//app.commandLine.appendSwitch('ignore-certificate-errors'); 原本用于解决SignalR 自签名证书不能使用的问题,将 "@microsoft/signalr": "^8.0.7", 改为  "@microsoft/signalr": "^7.0.14",
+const AdmZip = require('adm-zip'); // 鐢ㄤ簬瑙e帇 zip 鏂囦欢
+// 忽略证书的检测
+//app.commandLine.appendSwitch('ignore-certificate-errors'); 鍘熸湰鐢ㄤ簬瑙e喅SignalR 鑷��鍚嶈瘉涔︿笉鑳戒娇鐢ㄧ殑闂��锛屽皢 "@microsoft/signalr": "^8.0.7", 鏀逛负  "@microsoft/signalr": "^7.0.14",
 const cert = fs.readFileSync('server\\Configs\\cer\\cert.pem');
 const agent = new https.Agent({
     ca: cert,
-    rejectUnauthorized: true, // 启用证书验证
+    rejectUnauthorized: true, // 鍚�敤璇佷功楠岃瘉
 });
-// 定义 Web API 的启动路径和健康检测 URL
+let win = null;
+// 定义 Web API 的启动路径和健康检测 URL
 let serverPath;
 if (app.isPackaged) {
-    // 打包后的路径
+    // 鎵撳寘鍚庣殑璺�緞
     serverPath = path.dirname(app.getPath('exe')) ;
 } else {
-    // 开发环境的路径
+    // 寮€鍙戠幆澧冪殑璺�緞
     serverPath = __dirname;
 }
-console.log('Server path:', serverPath);// 打印路径进行检查
+console.log('Server path:', serverPath);// 打印路径进行检查
 const baseUrl = 'https://exam.habook.local:8888';
+const remoteVersionsUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server/versions.json'; // 浜戠�鐗堟湰淇℃伅 URL
+const remoteZipBaseUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server'; // 浜戠� zip 鏂囦欢鐨勫熀纭€ URL
 let serverProcess;
-// 读取本地 appsettings.json 文件
+// 璇诲彇鏈�湴 appsettings.json 鏂囦欢
 const getLocalVersion = () => {
     const appSettingsPath = path.join(serverPath, 'server', 'appsettings.json');
+    console.log(appSettingsPath)
     try {
-        const appSettings = JSON.parse(fs.readFileSync(appSettingsPath, 'utf-8'));
-        return appSettings.Version; // 假设 version 字段存储版本号
+        if (fs.existsSync(appSettingsPath)) {
+            const appSettings = JSON.parse(fs.readFileSync(appSettingsPath, 'utf-8'));
+            console.log(appSettings)
+            return appSettings.Version || '1.250101.01'; // 默认版本号
+        }
     } catch (error) {
         console.error('Error reading appsettings.json:', error);
+        return '1.250101.01';
+    }
+};
+// 从云端获取历史版本信息
+const getRemoteVersions = async () => {
+    try {
+        const response = await axios.get(remoteVersionsUrl, {
+            validateStatus: (status) => status === 200 || status === 404, // 鍏佽� 404 鐘舵€佺爜
+            timeout: 5000 // 设置超时时间为 5 秒
+        });
+        if (response.status === 404) {
+            console.error('Remote versions file not found.');
+            return null;
+        }
+        return response.data.versions; // 返回版本号数组
+    } catch (error) {
+        if (error.code === 'ECONNABORTED') {
+            console.error('Request to fetch remote versions timed out.');
+        } else {
+            console.error('Error fetching remote versions:', error);
+        }
         return null;
     }
 };
-// 启动 Web API 的函数
 
-// 检查服务器健康状态的函数
+// 版本号格式化(补全当天版本号为两位数)
+const formatVersion = (version) => {
+    const parts = version.split('.');
+    if (parts.length === 3) {
+        const dayVersion = parts[2].padStart(2, '0'); // 补全当天版本号为两位数
+        return `${parts[0]}.${parts[1]}.${dayVersion}`;
+    }
+    return version;
+};
+// 版本号比较(去掉 . 并转换为数字)
+const compareVersions = (localVersion, remoteVersion) => {
+    const localNumber = parseInt(localVersion.replace(/\./g, ''), 10);
+    const remoteNumber = parseInt(remoteVersion.replace(/\./g, ''), 10);
+    return remoteNumber > localNumber;
+};
+// 涓嬭浇鏂囦欢
+const downloadFile = async (url, outputPath) => {
+    try {
+        const writer = fs.createWriteStream(outputPath);
+
+        console.log(`Downloading file from ${url}...`);
+        const response = await axios({
+            url,
+            method: 'GET',
+            responseType: 'stream',
+            timeout: 10000 // 设置超时时间为 10 秒
+        });
+
+        response.data.pipe(writer);
+
+        await new Promise((resolve, reject) => {
+            writer.on('finish', resolve);
+            writer.on('error', reject);
+        });
+
+        console.log('Download completed.');
+        return true;
+    } catch (error) {
+        if (error.code === 'ECONNABORTED') {
+            console.error('Download timed out.');
+        } else {
+            console.error('Error downloading file:', error);
+        }
+        return false;
+    }
+};
+
+// 检查是否需要更新
+const checkForUpdates = async () => {
+    const localVersion = formatVersion(getLocalVersion());
+    console.log('Local version:', localVersion);
+
+    const remoteVersions = await getRemoteVersions();
+    if (!remoteVersions || remoteVersions.length === 0) {
+        console.log('No remote versions found.');
+        return;
+    }
+
+    // 鑾峰彇鏈€鏂扮増鏈�彿
+    const latestRemoteVersion = formatVersion(remoteVersions[remoteVersions.length - 1]);
+    console.log('Latest remote version:', latestRemoteVersion);
+
+    if (compareVersions(localVersion, latestRemoteVersion)) {
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '鐗堟湰鏇存柊',
+            message: `是否更新到(${latestRemoteVersion}) 版本?`,
+            buttons: ['是', '否']
+        });
+
+        if (response === 0) { // 鐢ㄦ埛閫夋嫨 Yes
+            await updateServer(latestRemoteVersion);
+        }
+    } else {
+        console.log('未检测到新版本。');
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '鐗堟湰鏇存柊',
+            message: `鏈��娴嬪埌鏂扮増鏈�€俙,
+            buttons: ['鍏抽棴']
+        });
+        createMenu();
+    }
+};
+// 下载并更新 IES.ExamServer.exe
+const updateServer = async (latestVersion) => {
+    try {
+        const zipUrl = `${remoteZipBaseUrl}/server-${latestVersion}.zip`; // 构造下载 URL
+        const zipPath = path.join(serverPath, 'IES.ExamServer.zip');
+
+        // 1. 涓嬭浇鏈€鏂扮殑 IES.ExamServer.zip
+        const downloadSuccess = await downloadFile(zipUrl, zipPath);
+        if (!downloadSuccess) {
+            throw new Error('Failed to download IES.ExamServer.zip.');
+        }
+
+        try {
+            // 2. 鍏抽棴 IES.ExamServer.exe
+            console.log('Shutting down IES.ExamServer...');
+            const response =  await axios.get(`${baseUrl}/index/shutdown`, {
+                httpsAgent: agent,
+                validateStatus: (status) => status === 200 || status === 404, // 鍏佽� 404 鐘舵€佺爜
+                timeout: 5000 // 设置超时时间为 5 秒
+            });
+            if (response.status === 404) {
+                console.error('API: index/shutdown not found.');
+               
+            }
+            if (response.status === 200) {
+                console.log('IES.ExamServer shutdown completed.');
+            }
+        } catch (error) {
+            console.error('Error Shutting down IES.ExamServer...', error);
+        }
+        await delay(1000);
+        // 3. 瑙e帇 IES.ExamServer.zip
+        console.log('Extracting IES.ExamServer.zip...');
+        const zip = new AdmZip(zipPath);
+        zip.extractAllTo(path.join(serverPath, 'server'), true); // 瑕嗙洊瑙e帇
+        console.log('Extraction completed.');
+        // 4. 鍚�姩鏂扮殑 IES.ExamServer.exe
+        console.log('Starting IES.ExamServer...');
+        // 5. 更新菜单栏的当前版本号
+        createMenu(); // 閲嶆柊鍒涘缓鑿滃崟
+        await startServer();
+        console.log('IES.ExamServer started successfully.');
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '鐗堟湰鏇存柊',
+            message: `鏇存柊鎴愬姛銆俙,
+            buttons: ['鍏抽棴']
+        });
+        win.loadURL(baseUrl, {
+            agent: agent
+        });
+    } catch (error) {
+        console.error('Error updating server:', error);
+    }
+};
+// 启动 Web API 的函数
+function delay(ms) {
+    return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+ 
+
+// 妫€鏌ユ湇鍔″櫒鍋ュ悍鐘舵€佺殑鍑芥暟
 const checkServerHealth = async () => {
     try {
         const response = await axios.get(`${baseUrl}/index/health`, {
@@ -55,25 +228,25 @@ const checkServerHealth = async () => {
 const startServer = () => {
     return new Promise((resolve, reject) => {
         serverProcess = exec(path.join(serverPath, 'server', 'IES.ExamServer.exe'), {
-            cwd: `${serverPath}/server` // 设置工作目录为 server 目录
+            cwd: `${serverPath}/server` // 设置工作目录为 server 目录
             , stdio: 'inherit'
         });
-        // 监听标准输出
+        // 鐩戝惉鏍囧噯杈撳嚭
         serverProcess.stdout.on('data', (data) => {
             console.log(`Server stdout: ${data}`);
         });
 
-        // 监听标准错误输出
+        // 鐩戝惉鏍囧噯閿欒�杈撳嚭
         serverProcess.stderr.on('data', (data) => {
             console.log(`Server stderr: ${data}`);
         });
 
-        // 监听进程退出事件
+        // 监听进程退出事件
         serverProcess.on('close', (data) => {
             console.log(`Server process exited with code ${data}`);
             reject(new Error(`Server process exited with code ${data}`));
         });
-        // 等待 Web API 启动成功
+        // 绛夊緟 Web API 鍚�姩鎴愬姛
         const checkHealth = async () => {
             try {
                 const response = await axios.get(`${baseUrl}/index/health`, {
@@ -85,21 +258,62 @@ const startServer = () => {
                 }
             } catch (error) {
                 console.log('Waiting for server to start...');
-                setTimeout(checkHealth, 1000); // 每隔 1 秒检查一次
+                setTimeout(checkHealth, 1000); // 每隔 1 秒检查一次
             }
         };
         checkHealth();
     });
 };
 
-// 创建 Electron 窗口的函数
+// 鍒涘缓鑿滃崟
+const createMenu = () => {
+    const localVersion = formatVersion(getLocalVersion());
+    console.log('Local version:', localVersion);
+    // 获取 Electron、Node.js 和 Chrome 的版本
+    const electronVersion = process.versions.electron;
+    const nodeVersion = process.versions.node;
+    const chromeVersion = process.versions.chrome;
+    const appVersion = app.getVersion(); // 鑾峰彇搴旂敤绋嬪簭鐗堟湰
+    const template = [
+        {
+            label: '甯�姪',
+            submenu: [
+                {
+                    label: '妫€鏌ユ洿鏂版湇鍔$�',
+                    click: () => {
+                        checkForUpdates();
+                    }
+                },
+                {
+                    label: `服务端版本: ${localVersion}`
+                   
+                }
+                ,{
+                    label: `搴旂敤绋嬪簭鐗堟湰: ${appVersion}` // 鏄剧ず搴旂敤绋嬪簭鐗堟湰
+                },
+                //{
+                //    label: `Electron 鐗堟湰: ${electronVersion}` // 鏄剧ず Electron 鐗堟湰
+                //},
+                //{
+                //    label: `Node.js 鐗堟湰: ${nodeVersion}` // 鏄剧ず Node.js 鐗堟湰
+                //},
+                {
+                    label: `浏览器内核版本: ${chromeVersion}` // 显示 Chrome 版本
+                }
+            ]
+        }
+    ];
+    const menu = Menu.buildFromTemplate(template);
+    Menu.setApplicationMenu(menu);
+};
+// 创建 Electron 窗口的函数
 const createWindow = async () => {
     try {
         const isServerRunning = await checkServerHealth();
         if (!isServerRunning) {
-            await startServer(); // 启动 Web API
+            await startServer(); // 鍚�姩 Web API
         }
-        const win = new BrowserWindow({
+        win = new BrowserWindow({
             width: 800,
             height: 600,
             webPreferences: {
@@ -117,25 +331,25 @@ const createWindow = async () => {
     }
 };
 
-// 当 Electron 应用准备好时创建窗口
+// 当 Electron 应用准备好时创建窗口
 app.whenReady().then(() => {
     createWindow();
-
+    createMenu();
     app.on('activate', () => {
         if (BrowserWindow.getAllWindows().length === 0) {
             createWindow();
         }
     });
-    // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
+    // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
     app.on('before-quit', async (event) => {
-        event.preventDefault(); // 阻止默认的退出行为
+        event.preventDefault(); // 阻止默认的退出行为
         if (serverProcess) {
             console.log('Killing server process...');
             serverProcess.kill();
         }
         try {
             console.log('index/shutdown api ...');
-            // 发起 HTTP 请求来关闭.NET Core Web API
+            // 发起 HTTP 请求来关闭.NET Core Web API
             const response = await axios.get(`${baseUrl}/index/shutdown`, {
                 httpsAgent: agent
             });
@@ -143,15 +357,15 @@ app.whenReady().then(() => {
                 console.log('Server is shutdown!');
                 //resolve();
             }
-           // app.quit(); // 关闭 Electron 应用程序
+           // app.quit(); // 鍏抽棴 Electron 搴旂敤绋嬪簭
         } catch (error) {
-            console.error('关闭.NET Core Web API 时出错:', error);
+            console.error('关闭.NET Core Web API 时出错:', error);
         }
     });
 });
 
-// 当所有窗口关闭时退出应用(macOS 除外)
+// 当所有窗口关闭时退出应用(macOS 除外)
 app.on('window-all-closed', function () {
-    // 无论什么平台,都退出程序
+    // 无论什么平台,都退出程序
     app.quit();
 });

+ 29 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/constants.js

@@ -0,0 +1,29 @@
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+const { app } = require('electron');
+
+const cert = fs.readFileSync('server\\Configs\\cer\\cert.pem');
+const agent = new https.Agent({
+    ca: cert,
+    rejectUnauthorized: true, // 启用证书验证
+});
+
+let serverPath;
+if (app.isPackaged) {
+    serverPath = path.dirname(app.getPath('exe'));
+} else {
+    serverPath = __dirname;
+}
+
+const baseUrl = 'https://exam.habook.local:8888';
+const remoteVersionsUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server/versions.json';
+const remoteZipBaseUrl = 'https://teammodelos.blob.core.chinacloudapi.cn/0-public/exam-server';
+
+module.exports = {
+    serverPath,
+    baseUrl,
+    remoteVersionsUrl,
+    remoteZipBaseUrl,
+    agent
+};

+ 54 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/main.js

@@ -0,0 +1,54 @@
+const { app, BrowserWindow } = require('electron');
+const serverManager = require('./serverManager');
+const menuManager = require('./menuManager');
+const constants = require('./constants');
+
+let win = null;
+
+// 创建 Electron 窗口的函数
+const createWindow = async () => {
+    try {
+        const isServerRunning = await serverManager.checkServerHealth();
+        if (!isServerRunning) {
+            await serverManager.startServer(); // 启动 Web API
+        }
+        win = new BrowserWindow({
+            width: 800,
+            height: 600,
+            webPreferences: {
+                nodeIntegration: true,
+                contextIsolation: false,
+            },
+        });
+        win.maximize();
+        win.loadURL(constants.baseUrl, {
+            agent: constants.agent
+        });
+
+    } catch (error) {
+        console.error('Error starting server or loading window:', error);
+    }
+};
+
+// 当 Electron 应用准备好时创建窗口
+app.whenReady().then(() => {
+    createWindow();
+    menuManager.createMenu();
+    app.on('activate', () => {
+        if (BrowserWindow.getAllWindows().length === 0) {
+            createWindow();
+        }
+    });
+
+    // 监听 before-quit 事件,关闭 IES.ExamServer.exe 进程
+    app.on('before-quit', async (event) => {
+        event.preventDefault(); // 阻止默认的退出行为
+        await serverManager.shutdownServer();
+        //app.quit(); // 关闭 Electron 应用程序
+    });
+});
+
+// 当所有窗口关闭时退出应用(macOS 除外)
+app.on('window-all-closed', function () {
+    app.quit();
+});

+ 60 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/menuManager.js

@@ -0,0 +1,60 @@
+const { Menu, dialog } = require('electron');
+const updateManager = require('./updateManager');
+const constants = require('./constants');
+const utils = require('./utils');
+const path = require('path');
+const fs = require('fs');
+// 创建菜单
+const createMenu = () => {
+    const localVersion = utils.formatVersion(getLocalVersion());
+    console.log('Local version:', localVersion);
+
+    const electronVersion = process.versions.electron;
+    const nodeVersion = process.versions.node;
+    const chromeVersion = process.versions.chrome;
+    const appVersion = require('electron').app.getVersion();
+
+    const template = [
+        {
+            label: '帮助',
+            submenu: [
+                {
+                    label: '检查更新服务端',
+                    click: () => {
+                        updateManager.checkForUpdates();
+                    }
+                },
+                {
+                    label: `服务端版本: ${localVersion}`
+                },
+                {
+                    label: `应用程序版本: ${appVersion}`
+                },
+                {
+                    label: `浏览器内核版本: ${chromeVersion}`
+                }
+            ]
+        }
+    ];
+
+    const menu = Menu.buildFromTemplate(template);
+    Menu.setApplicationMenu(menu);
+};
+
+// 获取本地版本号
+const getLocalVersion = () => {
+    const appSettingsPath = path.join(constants.serverPath, 'server', 'appsettings.json');
+    try {
+        if (fs.existsSync(appSettingsPath)) {
+            const appSettings = JSON.parse(fs.readFileSync(appSettingsPath, 'utf-8'));
+            return appSettings.Version || '1.250101.01'; // 默认版本号
+        }
+    } catch (error) {
+        console.error('Error reading appsettings.json:', error);
+        return '1.250101.01';
+    }
+};
+
+module.exports = {
+    createMenu
+};

+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/package.json

@@ -2,7 +2,7 @@
   "name": "examclient",
   "version": "1.0.0",
   "description": "IES.ExamClient",
-  "main": "app.js",
+  "main": "main.js",
   "scripts": {
     "start": "electron .",
     "test": "echo \"Error: no test specified\" && exit 1",

+ 89 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/serverManager.js

@@ -0,0 +1,89 @@
+const { exec } = require('child_process');
+const axios = require('axios');
+const fs = require('fs');
+const path = require('path');
+const constants = require('./constants');
+const utils = require('./utils');
+
+let serverProcess;
+
+// 启动 Web API 的函数
+const startServer = () => {
+    return new Promise((resolve, reject) => {
+        serverProcess = exec(path.join(constants.serverPath, 'server', 'IES.ExamServer.exe'), {
+            cwd: `${constants.serverPath}/server`, // 设置工作目录为 server 目录
+            stdio: 'inherit'
+        });
+
+        serverProcess.stdout.on('data', (data) => {
+            console.log(`Server stdout: ${data}`);
+        });
+
+        serverProcess.stderr.on('data', (data) => {
+            console.log(`Server stderr: ${data}`);
+        });
+
+        serverProcess.on('close', (data) => {
+            console.log(`Server process exited with code ${data}`);
+            reject(new Error(`Server process exited with code ${data}`));
+        });
+
+        // 等待 Web API 启动成功
+        const checkHealth = async () => {
+            try {
+                const response = await axios.get(`${constants.baseUrl}/index/health`, {
+                    httpsAgent: constants.agent
+                });
+                if (response.status === 200) {
+                    console.log('Server is up and running!');
+                    resolve();
+                }
+            } catch (error) {
+                console.log('Waiting for server to start...');
+                setTimeout(checkHealth, 1000); // 每隔 1 秒检查一次
+            }
+        };
+        checkHealth();
+    });
+};
+
+// 检查服务器健康状态的函数
+const checkServerHealth = async () => {
+    try {
+        const response = await axios.get(`${constants.baseUrl}/index/health`, {
+            httpsAgent: constants.agent
+        });
+        if (response.status === 200) {
+            console.log('Server is up and running!');
+            return true;
+        }
+    } catch (error) {
+        console.log('Server is not running yet.');
+        return false;
+    }
+};
+
+// 关闭服务器的函数
+const shutdownServer = async () => {
+    if (serverProcess) {
+        console.log('Killing server process...');
+        serverProcess.kill();
+    }
+    try {
+        console.log('index/shutdown api ...');
+        const response = await axios.get(`${constants.baseUrl}/index/shutdown`, {
+            httpsAgent: constants.agent
+        });
+        if (response.status === 200) {
+            console.log('Server is shutdown!');
+        }
+    } catch (error) {
+        console.error('关闭.NET Core Web API 时出错:', error);
+    }
+};
+
+module.exports = {
+    startServer,
+    checkServerHealth,
+    shutdownServer
+};

+ 170 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/updateManager.js

@@ -0,0 +1,170 @@
+const axios = require('axios');
+const fs = require('fs');
+const path = require('path');
+const AdmZip = require('adm-zip');
+const { dialog } = require('electron');
+const constants = require('./constants');
+const utils = require('./utils');
+const serverManager = require('./serverManager');
+
+// 获取本地版本号
+const getLocalVersion = () => {
+    const appSettingsPath = path.join(constants.serverPath, 'server', 'appsettings.json');
+    try {
+        if (fs.existsSync(appSettingsPath)) {
+            const appSettings = JSON.parse(fs.readFileSync(appSettingsPath, 'utf-8'));
+            return appSettings.Version || '1.250101.01'; // 默认版本号
+        }
+    } catch (error) {
+        console.error('Error reading appsettings.json:', error);
+        return '1.250101.01';
+    }
+};
+
+// 从云端获取历史版本信息
+const getRemoteVersions = async () => {
+    try {
+        const response = await axios.get(constants.remoteVersionsUrl, {
+            validateStatus: (status) => status === 200 || status === 404, // 允许 404 状态码
+            timeout: 5000 // 设置超时时间为 5 秒
+        });
+        if (response.status === 404) {
+            console.error('Remote versions file not found.');
+            return null;
+        }
+        return response.data.versions; // 返回版本号数组
+    } catch (error) {
+        if (error.code === 'ECONNABORTED') {
+            console.error('Request to fetch remote versions timed out.');
+        } else {
+            console.error('Error fetching remote versions:', error);
+        }
+        return null;
+    }
+};
+
+// 下载文件
+const downloadFile = async (url, outputPath) => {
+    try {
+        const writer = fs.createWriteStream(outputPath);
+
+        console.log(`Downloading file from ${url}...`);
+        const response = await axios({
+            url,
+            method: 'GET',
+            responseType: 'stream',
+            timeout: 10000 // 设置超时时间为 10 秒
+        });
+
+        response.data.pipe(writer);
+
+        await new Promise((resolve, reject) => {
+            writer.on('finish', resolve);
+            writer.on('error', reject);
+        });
+
+        console.log('Download completed.');
+        return true;
+    } catch (error) {
+        if (error.code === 'ECONNABORTED') {
+            console.error('Download timed out.');
+        } else {
+            console.error('Error downloading file:', error);
+        }
+        return false;
+    }
+};
+
+// 检查是否需要更新
+const checkForUpdates = async () => {
+    const localVersion = utils.formatVersion(getLocalVersion());
+    console.log('Local version:', localVersion);
+
+    const remoteVersions = await getRemoteVersions();
+    if (!remoteVersions || remoteVersions.length === 0) {
+        console.log('No remote versions found.');
+        return;
+    }
+
+    // 获取最新版本号
+    const latestRemoteVersion = utils.formatVersion(remoteVersions[remoteVersions.length - 1]);
+    console.log('Latest remote version:', latestRemoteVersion);
+
+    if (utils.compareVersions(localVersion, latestRemoteVersion)) {
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '版本更新',
+            message: `是否更新到(${latestRemoteVersion}) 版本?`,
+            buttons: ['是', '否']
+        });
+
+        if (response === 0) { // 用户选择 Yes
+            await updateServer(latestRemoteVersion);
+        }
+    } else {
+        console.log('未检测到新版本。');
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '版本更新',
+            message: `未检测到新版本。`,
+            buttons: ['关闭']
+        });
+    }
+};
+
+// 下载并更新 IES.ExamServer.exe
+const updateServer = async (latestVersion) => {
+    try {
+        const zipUrl = `${constants.remoteZipBaseUrl}/server-${latestVersion}.zip`; // 构造下载 URL
+        const zipPath = path.join(constants.serverPath, 'IES.ExamServer.zip');
+
+        // 1. 下载最新的 IES.ExamServer.zip
+        const downloadSuccess = await downloadFile(zipUrl, zipPath);
+        if (!downloadSuccess) {
+            throw new Error('Failed to download IES.ExamServer.zip.');
+        }
+
+        try {
+            // 2. 关闭 IES.ExamServer.exe
+            console.log('Shutting down IES.ExamServer...');
+            const response = await axios.get(`${constants.baseUrl}/index/shutdown`, {
+                httpsAgent: constants.agent,
+                validateStatus: (status) => status === 200 || status === 404, // 允许 404 状态码
+                timeout: 5000 // 设置超时时间为 5 秒
+            });
+            if (response.status === 404) {
+                console.error('API: index/shutdown not found.');
+            }
+            if (response.status === 200) {
+                console.log('IES.ExamServer shutdown completed.');
+            }
+        } catch (error) {
+            console.error('Error Shutting down IES.ExamServer...', error);
+        }
+        await utils.delay(1000);
+
+        // 3. 解压 IES.ExamServer.zip
+        console.log('Extracting IES.ExamServer.zip...');
+        const zip = new AdmZip(zipPath);
+        zip.extractAllTo(path.join(constants.serverPath, 'server'), true); // 覆盖解压
+        console.log('Extraction completed.');
+
+        // 4. 启动新的 IES.ExamServer.exe
+        console.log('Starting IES.ExamServer...');
+        await serverManager.startServer();
+        console.log('IES.ExamServer started successfully.');
+
+        const { response } = await dialog.showMessageBox({
+            type: 'info',
+            title: '版本更新',
+            message: `更新成功。`,
+            buttons: ['关闭']
+        });
+    } catch (error) {
+        console.error('Error updating server:', error);
+    }
+};
+
+module.exports = {
+    checkForUpdates
+};

+ 27 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamClient/utils.js

@@ -0,0 +1,27 @@
+// 延迟函数
+const delay = (ms) => {
+    return new Promise(resolve => setTimeout(resolve, ms));
+};
+
+// 版本号格式化(补全当天版本号为两位数)
+const formatVersion = (version) => {
+    const parts = version.split('.');
+    if (parts.length === 3) {
+        const dayVersion = parts[2].padStart(2, '0'); // 补全当天版本号为两位数
+        return `${parts[0]}.${parts[1]}.${dayVersion}`;
+    }
+    return version;
+};
+
+// 版本号比较(去掉 . 并转换为数字)
+const compareVersions = (localVersion, remoteVersion) => {
+    const localNumber = parseInt(localVersion.replace(/\./g, ''), 10);
+    const remoteNumber = parseInt(remoteVersion.replace(/\./g, ''), 10);
+    return remoteNumber > localNumber;
+};
+
+module.exports = {
+    delay,
+    formatVersion,
+    compareVersions
+};

+ 3 - 0
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Configs/versions.json

@@ -0,0 +1,3 @@
+{
+  "versions": [ "1.250205.01", "1.250205.02" ]
+}

+ 1 - 1
TEAMModelOS.Extension/IES.Exam/IES.ExamServer/Controllers/IndexController.cs

@@ -42,7 +42,7 @@ namespace IES.ExamServer.Controllers
         /// </summary>
         /// <returns></returns>
         [HttpGet("shutdown")]
-        public IActionResult Shutdown(int delayMilliseconds = 1000)
+        public IActionResult Shutdown(int delayMilliseconds = 200)
         {
             try
             {