This commit is contained in:
Irene-Maxine
2025-09-30 21:27:23 +08:00
58 changed files with 455 additions and 60 deletions

View File

@@ -36,7 +36,7 @@ except Exception as e:
'''IMU-Sensor''' '''IMU-Sensor'''
try : try :
import qmi8658 import qmi8658
onboard_imu = qmi8658.QMI8658(onboard_i2c, 0x6A) onboard_imu = qmi8658.QMI8658(onboard_i2c)
except Exception as e: except Exception as e:
print("Warning: Failed to communicate with QMI8658 (IMU) or",e) print("Warning: Failed to communicate with QMI8658 (IMU) or",e)

View File

@@ -171,7 +171,6 @@ export const turtle_setxy = {
}; };
export const turtle_pos_shape = { export const turtle_pos_shape = {
init: function () { init: function () {
this.setColour(TURTLE_HUE); this.setColour(TURTLE_HUE);
var pos_shape = var pos_shape =
@@ -200,6 +199,18 @@ export const turtle_pos_shape = {
} }
}; };
export const turtle_stamp = {
init: function () {
this.setColour(TURTLE_HUE);
this.appendValueInput('TUR')
.setCheck('Turtle')
this.appendDummyInput()
.appendField(Blockly.Msg.TURTLE_STAMP);
this.setInputsInline(true);
this.setPreviousStatement(true);
this.setNextStatement(true);
}
};
export const turtle_clear = { export const turtle_clear = {
init: function () { init: function () {
@@ -211,8 +222,6 @@ export const turtle_clear = {
this.setColour(TURTLE_HUE); this.setColour(TURTLE_HUE);
this.appendDummyInput("") this.appendDummyInput("")
.appendField(new Blockly.FieldDropdown(clear_reset), 'DIR') .appendField(new Blockly.FieldDropdown(clear_reset), 'DIR')
this.setInputsInline(true); this.setInputsInline(true);
this.setPreviousStatement(true); this.setPreviousStatement(true);
this.setNextStatement(true); this.setNextStatement(true);

View File

@@ -75,6 +75,13 @@ export const turtle_pos_shape = function (_, generator) {
return [code, generator.ORDER_ATOMIC]; return [code, generator.ORDER_ATOMIC];
} }
export const turtle_stamp = function (_, generator) {
generator.definitions_.import_turtle = "import turtle";
var varName = generator.valueToCode(this, 'TUR', generator.ORDER_ASSIGNMENT) || '0';
var code = varName + '.stamp()\n';
return code;
}
export const turtle_clear = function (_, generator) { export const turtle_clear = function (_, generator) {
generator.definitions_.import_turtle = "import turtle"; generator.definitions_.import_turtle = "import turtle";
var varName = generator.valueToCode(this, 'TUR', generator.ORDER_ASSIGNMENT) || '0'; var varName = generator.valueToCode(this, 'TUR', generator.ORDER_ASSIGNMENT) || '0';

View File

@@ -1814,6 +1814,13 @@
</shadow> </shadow>
</value> </value>
</block> </block>
<block type="turtle_stamp">
<value name="TUR">
<shadow type="variables_get">
<field name="VAR">tina</field>
</shadow>
</value>
</block>
<block type="turtle_clear"> <block type="turtle_clear">
<value name="TUR"> <value name="TUR">
<shadow type="variables_get"> <shadow type="variables_get">

View File

@@ -1777,6 +1777,13 @@
</shadow> </shadow>
</value> </value>
</block> </block>
<block type="turtle_stamp">
<value name="TUR">
<shadow type="variables_get">
<field name="VAR">tina</field>
</shadow>
</value>
</block>
<block type="turtle_clear"> <block type="turtle_clear">
<value name="TUR"> <value name="TUR">
<shadow type="variables_get"> <shadow type="variables_get">

View File

@@ -1445,6 +1445,13 @@
</shadow> </shadow>
</value> </value>
</block> </block>
<block type="turtle_stamp">
<value name="TUR">
<shadow type="variables_get">
<field name="VAR">tina</field>
</shadow>
</value>
</block>
<block type="turtle_clear"> <block type="turtle_clear">
<value name="TUR"> <value name="TUR">
<shadow type="variables_get"> <shadow type="variables_get">

View File

@@ -1776,6 +1776,7 @@ En.TURTLE_POS_SHAPE = "Get the turtle currently";
En.TURTLE_POS = "Get the current position of the turtle"; En.TURTLE_POS = "Get the current position of the turtle";
En.TURTLE_SHAPE = "shape"; En.TURTLE_SHAPE = "shape";
En.TURTLE_HEADING = "Heading"; En.TURTLE_HEADING = "Heading";
En.TURTLE_STAMP = "Stamp";
En.MIXLY_TOOLTIP_TURTEL_POS = 'Get the turtle\'s current position and return a tuple containing the current position x and y coordinates'; En.MIXLY_TOOLTIP_TURTEL_POS = 'Get the turtle\'s current position and return a tuple containing the current position x and y coordinates';
En.MIXLY_TOOLTIP_TURTEL_SHAPE = 'Get the turtle\'s current shape and return a string'; En.MIXLY_TOOLTIP_TURTEL_SHAPE = 'Get the turtle\'s current shape and return a string';
En.MIXLY_TOOLTIP_TURTEL_GET_SPEED = "get the turtles current speed and return an integer;"; En.MIXLY_TOOLTIP_TURTEL_GET_SPEED = "get the turtles current speed and return an integer;";

View File

@@ -1789,6 +1789,7 @@ ZhHans.TURTLE_POS_SHAPE = "获取海龟当前";
ZhHans.TURTLE_POS = "位置"; ZhHans.TURTLE_POS = "位置";
ZhHans.TURTLE_SHAPE = "形状"; ZhHans.TURTLE_SHAPE = "形状";
ZhHans.TURTLE_HEADING = "朝向"; ZhHans.TURTLE_HEADING = "朝向";
ZhHans.TURTLE_STAMP = "留下印记";
ZhHans.MIXLY_TOOLTIP_TURTEL_POS = '获取海龟当前位置返回一个包含当前位置x坐标和y坐标的元组'; ZhHans.MIXLY_TOOLTIP_TURTEL_POS = '获取海龟当前位置返回一个包含当前位置x坐标和y坐标的元组';
ZhHans.MIXLY_TOOLTIP_TURTEL_SHAPE = '获取海龟当前形状,返回一个字符串'; ZhHans.MIXLY_TOOLTIP_TURTEL_SHAPE = '获取海龟当前形状,返回一个字符串';
ZhHans.MIXLY_TOOLTIP_TURTEL_GET_SPEED = '获取海龟当前速度,返回一个整数'; ZhHans.MIXLY_TOOLTIP_TURTEL_GET_SPEED = '获取海龟当前速度,返回一个整数';

View File

@@ -1789,6 +1789,7 @@ ZhHant.TURTLE_POS_SHAPE = "獲取海龜當前";
ZhHant.TURTLE_POS = "位置"; ZhHant.TURTLE_POS = "位置";
ZhHant.TURTLE_SHAPE = "形狀"; ZhHant.TURTLE_SHAPE = "形狀";
ZhHant.TURTLE_HEADING = "朝向"; ZhHant.TURTLE_HEADING = "朝向";
ZhHant.TURTLE_STAMP = "留下印記";
ZhHant.MIXLY_TOOLTIP_TURTEL_POS = '獲取海龜當前位置返回一個包含當前位置x座標和y座標的元組'; ZhHant.MIXLY_TOOLTIP_TURTEL_POS = '獲取海龜當前位置返回一個包含當前位置x座標和y座標的元組';
ZhHant.MIXLY_TOOLTIP_TURTEL_SHAPE = '獲取海龜當前形狀,返回一個字符串'; ZhHant.MIXLY_TOOLTIP_TURTEL_SHAPE = '獲取海龜當前形狀,返回一個字符串';
ZhHant.MIXLY_TOOLTIP_TURTEL_GET_SPEED = '獲取海龜當前速度,返回一個整數'; ZhHant.MIXLY_TOOLTIP_TURTEL_GET_SPEED = '獲取海龜當前速度,返回一個整數';

View File

@@ -8,6 +8,7 @@
<script type="text/javascript" src="./common/modules/web-modules/lazyload.js"></script> <script type="text/javascript" src="./common/modules/web-modules/lazyload.js"></script>
<script type="text/javascript" src="./common/blockly-core/base.js"></script> <script type="text/javascript" src="./common/blockly-core/base.js"></script>
<script type="text/javascript" src="./common/main.js?view=home"></script> <script type="text/javascript" src="./common/main.js?view=home"></script>
<link rel="stylesheet" type="text/css" href="./common/ui/fomantic-ui/semantic.min.css" />
<link rel="stylesheet" href="./files/bootstrap.min.css"> <link rel="stylesheet" href="./files/bootstrap.min.css">
<link rel="stylesheet" href="./files/owl.carousel.min.css"> <link rel="stylesheet" href="./files/owl.carousel.min.css">
<link rel="stylesheet" href="./files/slicknav.min.css"> <link rel="stylesheet" href="./files/slicknav.min.css">

View File

@@ -31,7 +31,7 @@ Config.init = () => {
const boardPageConfig = Url.getConfig(); const boardPageConfig = Url.getConfig();
Config.BOARD_PAGE = boardPageConfig ?? {}; Config.BOARD_PAGE = boardPageConfig ?? {};
console.log('Config.BOARD_PAGE:', Config.BOARD_PAGE); console.log('Config.BOARD_PAGE:', Config.BOARD_PAGE);
document.title = Config.SOFTWARE.version ?? 'Mixly 2.0'; document.title = Config.SOFTWARE.version ?? 'Mixly 3.0';
Config.USER = { Config.USER = {
...Config.USER, ...Config.USER,

View File

@@ -5,6 +5,7 @@ goog.require('ace.ExtLanguageTools');
goog.require('layui'); goog.require('layui');
goog.require('store'); goog.require('store');
goog.require('$.select2'); goog.require('$.select2');
goog.require('$.fomanticUI');
goog.require('Mixly.XML'); goog.require('Mixly.XML');
goog.require('Mixly.LayerExt'); goog.require('Mixly.LayerExt');
goog.require('Mixly.Msg'); goog.require('Mixly.Msg');
@@ -167,13 +168,14 @@ Setting.onclick = () => {
Setting.addOnchangeOptionListener = () => { Setting.addOnchangeOptionListener = () => {
element.on('tab(setting-menu-filter)', function(data) { element.on('tab(setting-menu-filter)', function(data) {
const { index } = data; const { index } = data;
if (index === 1) { const type = $(data.elem.prevObject).data('type');
if (type === 'import-board') {
if (data.index !== Setting.nowIndex) { if (data.index !== Setting.nowIndex) {
goog.isElectron && BoardManager.onclickImportBoards(); goog.isElectron && BoardManager.onclickImportBoards();
} else { } else {
layui.table.resize('cloud-boards-table'); layui.table.resize('cloud-boards-table');
} }
} else if (index === 2) { } else if (type === 'ws-update') {
if (data.index !== Setting.nowIndex) { if (data.index !== Setting.nowIndex) {
$('#setting-menu-update').loading({ $('#setting-menu-update').loading({
background: USER.theme === 'dark' ? '#807b7b' : '#fff', background: USER.theme === 'dark' ? '#807b7b' : '#fff',
@@ -189,6 +191,26 @@ Setting.addOnchangeOptionListener = () => {
args: [ SOFTWARE.configUrl ] args: [ SOFTWARE.configUrl ]
}); });
} }
} else if (type === 'nw-update') {
fetch('/api/check-update', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then((response) => response.json())
.then((result) => {
const {
localVersion,
cloudVersion,
needsUpdate,
cloudFile
} = result;
Setting.refreshUpdateMenuStatus(localVersion, cloudVersion, needsUpdate, cloudFile);
})
.catch((error) => {
console.log(error)
});
} }
Setting.nowIndex = index; Setting.nowIndex = index;
}); });
@@ -229,58 +251,65 @@ Setting.configMenuGetValue = (obj) => {
return config; return config;
} }
Setting.refreshUpdateMenuStatus = (config) => { Setting.refreshUpdateMenuStatus = (localVersion, cloudVersion, needsUpdate, url) => {
console.log(config); const $serverDiv = $('#setting-menu-update-server');
const { const $btnDiv = $('#setting-menu-update > div:nth-child(2)');
serverVersion const $button = $btnDiv.children('button');
} = config; $button.removeClass('layui-btn-disabled');
let $serverDiv = $('#setting-menu-update-server'); $button.addClass('self-adaption-btn');
let $btnDiv = $('#setting-menu-update > div:nth-child(2)'); const $mixlyProgress = $serverDiv.find('.mixly-progress');
$serverDiv.find('span').css('display', 'none'); $serverDiv.find('span').css('display', 'none');
let needUpdateServer = false; $mixlyProgress.hide();
if (serverVersion && serverVersion !== SOFTWARE.serverVersion) { $mixlyProgress.find('.progress').show();
$serverDiv.find('span[value="obsolete"]').css('display', 'inline-block'); $mixlyProgress.removeClass('swinging indeterminate');
needUpdateServer = true; if (needsUpdate) {
$serverDiv.find('text').text(`${SOFTWARE.serverVersion}${serverVersion}`); $serverDiv.find('span.obsolete').css('display', 'inline-block');
$serverDiv.find('text').text(`${localVersion}${cloudVersion}`);
} else { } else {
$serverDiv.find('span[value="latest"]').css('display', 'inline-block'); $serverDiv.find('span.latest').css('display', 'inline-block');
$serverDiv.find('text').text(SOFTWARE.serverVersion); $serverDiv.find('text').text(localVersion);
} }
if (needUpdateServer) { if (needsUpdate) {
$btnDiv.css('display', 'flex'); $btnDiv.css('display', 'flex');
$btnDiv.children('button').off().click((event) => { $button.off().one('click', (event) => {
LayerExt.open({ $button.addClass('layui-btn-disabled');
title: Msg.getLang('PROGRESS'), $button.removeClass('self-adaption-btn');
id: 'setting-menu-update-layer', const eventSource = new EventSource(`/api/download?url=${encodeURIComponent(url)}&cloudVersion=${cloudVersion}`);
shade: LayerExt.SHADE_ALL, $mixlyProgress.show();
area: ['40%', '60%'], eventSource.onmessage = function(event) {
max: ['800px', '300px'], const data = JSON.parse(event.data);
min: ['500px', '100px'], if (data.type === 'progress') {
success: (layero, index) => { $mixlyProgress.progress({
$('#setting-menu-update-layer').css('overflow', 'hidden'); percent: data.progress
layero.find('.layui-layer-setwin').css('display', 'none');
Setting.ace = Setting.createAceEditor('setting-menu-update-layer');
Setting.ace.resize();
const { Socket } = Mixly.WebSocket;
Socket.sendCommand({
obj: 'Socket',
func: 'updateSW',
args: []
}); });
}, } else if (data.type === 'unzip') {
resizing: (layero) => { $mixlyProgress.addClass('swinging indeterminate');
Setting.ace.resize(); $mixlyProgress.progress({
}, percent: 100
end: () => { });
$mixlyProgress.find('.progress').hide();
layer.msg('解压中...', { time: 1000 });
} else if (data.type === 'complete') {
$mixlyProgress.removeClass('swinging indeterminate');
layer.msg('更新完成5秒后自动刷新...', { time: 1000 });
eventSource.close();
setTimeout(function(){
window.location.reload();
}, 5000);
} }
}); };
eventSource.onerror = function(error) {
layer.msg('下载失败5秒后自动刷新...', { time: 1000 });
setTimeout(function(){
window.location.reload();
}, 5000);
eventSource.close();
};
}); });
} else { } else {
$btnDiv.css('display', 'none'); $btnDiv.css('display', 'none');
} }
setTimeout(() => {
$('#setting-menu-update').loading('destroy');
}, 500);
} }
Setting.showUpdateMessage = (data) => { Setting.showUpdateMessage = (data) => {

View File

@@ -21,6 +21,9 @@ if (Env.hasSocketServer) {
if (env === 'electron' && !goog.isElectron) { if (env === 'electron' && !goog.isElectron) {
env = 'web'; env = 'web';
} }
if (typeof nw === 'object') {
env = 'nw';
}
XML.TEMPLATE_CONFIG = [ XML.TEMPLATE_CONFIG = [
{ {

View File

@@ -89,6 +89,7 @@
"layui", "layui",
"store", "store",
"$.select2", "$.select2",
"$.fomanticUI",
"Mixly.XML", "Mixly.XML",
"Mixly.LayerExt", "Mixly.LayerExt",
"Mixly.Msg", "Mixly.Msg",

View File

@@ -167,22 +167,52 @@
#setting-menu-update .layui-panel { #setting-menu-update .layui-panel {
margin: 5px; margin: 5px;
} }
#setting-menu-update .latest,
#setting-menu-update .obsolete {
display: none;
}
#setting-menu-update-server .mixly-progress {
display: none;
margin-bottom: 0px;
}
#setting-menu-update-server .bar {
height: 20px;
}
#setting-menu-update-server .bar > .progress {
/*display: none;*/
height: 20px;
margin-bottom: 10px;
overflow: hidden;
border-radius: 1px;
-webkit-box-shadow: unset;
box-shadow: unset;
background-color: unset;
}
</style> </style>
<div id="setting-menu" class="layui-layer-wrap"> <div id="setting-menu" class="layui-layer-wrap">
<div id="setting-menu-top"> <div id="setting-menu-top">
<div id="setting-menu-left"> <div id="setting-menu-left">
<ul id="setting-menu-options" class="layui-nav layui-nav-tree layui-bg-cyan layui-inline" <ul id="setting-menu-options" class="layui-nav layui-nav-tree layui-bg-cyan layui-inline"
lay-filter="setting-menu-filter"> lay-filter="setting-menu-filter">
<li class="layui-nav-item layui-this" lay-id="0"> <li class="layui-nav-item layui-this" lay-id="0" data-type="config">
<a m-id="0">{{ d.personalise }}</a> <a m-id="0">{{ d.personalise }}</a>
</li> </li>
{{# if(d.env === 'electron'){ }} {{# if(d.env === 'electron'){ }}
<li class="layui-nav-item" lay-id="1"> <li class="layui-nav-item" lay-id="1" data-type="import-board">
<a m-id="1" href="javascript:;">{{ d.importBoard }}</a> <a m-id="1" href="javascript:;">{{ d.importBoard }}</a>
</li> </li>
{{# } }} {{# } }}
{{# if(d.env === 'web-socket'){ }} {{# if(d.env === 'web-socket'){ }}
<li class="layui-nav-item" lay-id="2"> <li class="layui-nav-item" lay-id="2" data-type="ws-update">
<a m-id="2">{{ d.checkForUpdates }}</a>
</li>
{{# } }}
{{# if(d.env === 'nw'){ }}
<li class="layui-nav-item" lay-id="3" data-type="nw-update">
<a m-id="2">{{ d.checkForUpdates }}</a> <a m-id="2">{{ d.checkForUpdates }}</a>
</li> </li>
{{# } }} {{# } }}
@@ -341,19 +371,24 @@
<div class="layui-hide" id="import-board-page" lay-filter="import-board-page-filter"></div> <div class="layui-hide" id="import-board-page" lay-filter="import-board-page-filter"></div>
</div> </div>
{{# } }} {{# } }}
{{# if(d.env === 'web-socket'){ }} {{# if(d.env === 'web-socket' || d.env === 'nw'){ }}
<div class="menu-body"> <div class="menu-body">
<div id="setting-menu-update" class="setting-menu-info"> <div id="setting-menu-update" class="setting-menu-info">
<div> <div>
<div id="setting-menu-update-server" class="layui-card layui-panel"> <div id="setting-menu-update-server" class="layui-card layui-panel">
<div class="layui-card-header"> <div class="layui-card-header">
<label>{{ d.server }}</label> <label>{{ d.server }}</label>
<span class="layui-badge layui-bg-green" value="latest">{{ d.latest }}</span> <span class="latest layui-badge layui-bg-green" value="latest">{{ d.latest }}</span>
<span class="layui-badge" value="obsolete">{{ d.obsolete }}</span> <span class="obsolete layui-badge" value="obsolete">{{ d.obsolete }}</span>
</div> </div>
<div class="layui-card-body" class=""> <div class="layui-card-body" class="">
<label>{{ d.version }}: </label> <label>{{ d.version }}: </label>
<text>1.0.0</text> <text></text>
<div class="mixly-progress ui teal progress">
<div class="bar">
<div class="progress"></div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -20,6 +20,8 @@
"author": "Mixly", "author": "Mixly",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"adm-zip": "^0.5.16",
"axios": "^0.27.2",
"commander": "^11.1.0", "commander": "^11.1.0",
"copy-webpack-plugin": "^11.0.0", "copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1", "css-loader": "^6.8.1",

View File

@@ -36,6 +36,8 @@
"nwjs-builder-phoenix": "^1.15.0" "nwjs-builder-phoenix": "^1.15.0"
}, },
"dependencies": { "dependencies": {
"adm-zip": "^0.5.16",
"axios": "^0.27.2",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"http": "^0.0.1-security", "http": "^0.0.1-security",
"path": "^0.12.7", "path": "^0.12.7",

View File

@@ -23,11 +23,15 @@ if (options.type === 'special') {
options.type = path.basename(process.cwd()); options.type = path.basename(process.cwd());
} }
let targetIndex = 0;
const generateHash = (inputString) => { const generateHash = (inputString) => {
const hash = crypto.createHash('sha256'); // const hash = crypto.createHash('sha256');
hash.update(inputString); // hash.update(inputString);
return hash.digest('hex'); // return hash.digest('hex');
} targetIndex += 1;
return String(targetIndex);
};
const getExamples = (dirPath, convertExample = false) => { const getExamples = (dirPath, convertExample = false) => {
let examples = {}; let examples = {};

276
static-server/api.js Normal file
View File

@@ -0,0 +1,276 @@
const fs = require('fs');
const path = require('path');
const express = require('express');
const axios = require('axios');
const AdmZip = require('adm-zip');
class AsyncAdmZip {
constructor(zipPath) {
this.zipPath = zipPath;
this.zip = new AdmZip(zipPath);
}
// 异步解压到目录
async extractAllTo(targetPath, overwrite = true) {
return new Promise((resolve, reject) => {
try {
// 确保目标目录存在
fs.promises.mkdir(targetPath, { recursive: true })
.then(() => {
this.zip.extractAllTo(targetPath, overwrite);
resolve({
success: true,
targetPath,
fileCount: this.zip.getEntries().length
});
})
.catch(reject);
} catch (error) {
reject(error);
}
});
}
// 异步解压单个文件
async extractEntry(entryName, targetPath, overwrite = true) {
return new Promise((resolve, reject) => {
try {
const entry = this.zip.getEntry(entryName);
if (!entry) {
reject(new Error(`条目不存在: ${entryName}`));
return;
}
if (entry.isDirectory) {
reject(new Error(`条目是目录: ${entryName}`));
return;
}
// 确保目标目录存在
const targetDir = path.dirname(targetPath);
fs.promises.mkdir(targetDir, { recursive: true })
.then(() => {
this.zip.extractEntryTo(entry, targetDir, false, overwrite);
resolve({
success: true,
entryName,
targetPath
});
})
.catch(reject);
} catch (error) {
reject(error);
}
});
}
// 异步获取zip文件信息
async getZipInfo() {
return new Promise((resolve) => {
const entries = this.zip.getEntries();
const info = {
fileCount: entries.length,
totalSize: entries.reduce((sum, entry) => sum + entry.header.size, 0),
entries: entries.map(entry => ({
name: entry.entryName,
size: entry.header.size,
isDirectory: entry.isDirectory,
compressedSize: entry.header.compressedSize
}))
};
resolve(info);
});
}
}
const router = express.Router();
const TEMP_FOLDER_PATH = path.resolve(__dirname, '../temp');;
const VERSION_FILE = path.resolve(__dirname, '../version.json');
function getLocalVersion() {
try {
if (fs.existsSync(VERSION_FILE)) {
const data = fs.readFileSync(VERSION_FILE, 'utf8');
return data;
}
} catch (error) {
console.error('读取版本文件失败:', error);
}
return '2025.09.06';
}
function saveVersionInfo(version) {
fs.writeFileSync(VERSION_FILE, version);
}
async function getCloudVersion() {
try {
const response = await axios.get('http://update.mixly.cn/index.php');
return response.data.mixly.all;
} catch (error) {
console.error('获取云端版本信息失败:', error);
return {
file: 'mixly.zip',
version: '2025.09.06'
};
}
}
async function checkUpdate() {
try {
const localVersion = getLocalVersion();
const cloudVersions = await getCloudVersion();
const cloudVersion = cloudVersions['version'];
const cloudFile = 'http://update.mixly.cn/download.php?file=' + cloudVersions['file']
return {
needsUpdate: localVersion !== cloudVersion,
localVersion: localVersion,
cloudVersion: cloudVersion,
cloudFile: cloudFile,
error: ''
};
} catch (error) {
return {
needsUpdate: false,
localVersion: '',
cloudVersion: '',
cloudFile: '',
error: error.message
}
}
}
// 检查更新接口
router.post('/check-update', async (req, res) => {
const updateInfo = await checkUpdate();
res.json(updateInfo);
});
// 下载进度返回
function deleteFolderRecursive(dirPath) {
if (fs.existsSync(dirPath)) {
fs.readdirSync(dirPath).forEach(file => {
const curPath = path.join(dirPath, file);
if (fs.lstatSync(curPath).isDirectory()) {
deleteFolderRecursive(curPath);
} else {
fs.unlinkSync(curPath);
}
});
fs.rmdirSync(dirPath);
}
}
router.get('/download', async (req, res) => {
try {
const { url, cloudVersion } = req.query;
// 清理临时文件夹
if (fs.existsSync(TEMP_FOLDER_PATH)) {
deleteFolderRecursive(TEMP_FOLDER_PATH);
}
fs.mkdirSync(TEMP_FOLDER_PATH, { recursive: true });
const filePath = path.resolve(TEMP_FOLDER_PATH, 'mixly.zip');
const fileStream = fs.createWriteStream(filePath);
// 设置 SSE 响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
// 发起下载请求 - 添加 NW.js 特定配置
const response = await axios({
method: 'GET',
url: url,
responseType: 'stream',
timeout: 60000,
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
},
adapter: require('axios/lib/adapters/http')
});
const totalSize = parseInt(response.headers['content-length'], 10) || 0;
let downloadedSize = 0;
let lastProgress = 0;
// 发送进度信息
const sendProgress = (progress) => {
if (progress !== lastProgress) {
const data = JSON.stringify({ type: 'progress', progress });
res.write(`data: ${data}\n\n`);
lastProgress = progress;
}
};
// 管道流处理
response.data.pipe(fileStream);
// 进度监控
response.data.on('data', (chunk) => {
downloadedSize += chunk.length;
if (totalSize > 0) {
const progress = Math.round((downloadedSize / totalSize) * 100);
sendProgress(progress);
}
});
// 文件流完成
fileStream.on('finish', async () => {
console.log('文件下载完成,开始解压');
// 发送解压信息
res.write(`data: ${JSON.stringify({ type: 'unzip' })}\n\n`);
try {
const asyncZip = new AsyncAdmZip(filePath);
await asyncZip.extractAllTo(path.resolve(__dirname, '../'));
// 保存版本信息
saveVersionInfo(cloudVersion);
// 发送完成信息
res.write(`data: ${JSON.stringify({ type: 'complete', version: cloudVersion })}\n\n`);
// 清理临时文件
if (fs.existsSync(TEMP_FOLDER_PATH)) {
deleteFolderRecursive(TEMP_FOLDER_PATH);
}
res.end();
} catch (error) {
console.error('解压失败:', error);
res.write(`data: ${JSON.stringify({ type: 'error', message: '解压失败' })}\n\n`);
res.end();
}
});
// 错误处理
response.data.on('error', (error) => {
console.error('下载流错误:', error);
res.write(`data: ${JSON.stringify({ type: 'error', message: '下载流错误' })}\n\n`);
res.end();
});
fileStream.on('error', (error) => {
console.error('文件流错误:', error);
res.write(`data: ${JSON.stringify({ type: 'error', message: '文件保存错误' })}\n\n`);
res.end();
});
} catch (error) {
console.error('下载过程错误:', error);
res.write(`data: ${JSON.stringify({ type: 'error', message: '下载失败: ' + error.message })}\n\n`);
res.end();
}
});
module.exports = router;

View File

@@ -1,12 +1,14 @@
const http = require('http'); const http = require('http');
const express = require('express'); const express = require('express');
const path = require('path'); const path = require('path');
const apiRoutes = require('./api.js');
const StaticServer = {}; const StaticServer = {};
StaticServer.run = (port) => { StaticServer.run = (port) => {
const app = express(); const app = express();
app.use(express.static(path.resolve(__dirname, '../'))); app.use(express.static(path.resolve(__dirname, '../')));
app.use('/api/', apiRoutes);
const httpServer = http.createServer(app); const httpServer = http.createServer(app);
httpServer.listen(port); httpServer.listen(port);
console.log('Static服务器正在运行 [端口 - ' + port + ', http]...'); console.log('Static服务器正在运行 [端口 - ' + port + ', http]...');