当前位置: 首页 > news >正文

electron实现软件(热)更新(附带示例源码)

热更新指的是:electron 程序已经开启,在不关闭的情况下执行更新,需要我们把远程的app.asar文件下载到本地执行替换,然而 在electron应用程序开启状态是无法直接下载app.asar文件的,下载会检查出app.asar文件被占用,所以我们需要在本地将app.asar文件反编译,编译出一个app文件夹,里面有项目所需的所有源码文件,这时通过vue或react打包的渲染进程代码是经过压缩的,但是主进程代码会直接暴露,所以刚好,我们可以将主进程代码做压缩混淆,然后生成一个app.zip压缩包,上传至服务器,然后下载这个压缩包,解压,将其编译成app.asar文件替换到resources目录中,从而实现electron软件的热更新。

  1. 主进程(nodejs)侧代码:
const {app, BrowserWindow, ipcMain} = require('electron')
const path = require('path')
const fs = require("fs");
const http = require("http");
const asar = require('asar');
const AdmZip = require('adm-zip');
const fsExtra = require('fs-extra');const mainData = require("./mainData");
const txtConsole = require("./txtConsole");//当前环境
const production = mainData?.production;//Electron 安装根目录
const rootPath = production === 'dev' ? path.resolve('./public') : path.dirname(app.getPath('exe'));
mainData.rootPath = rootPath;// 软件更新配置信息(目前需要手动修改~~~)
const winUpdateConfig = {currentVersion: null, //当前版本updateVersionFilePath: 'http://103.117.121.53:8002/latest', //远程版本信息路径updateFilePath: 'http://103.117.121.53:8002/app.zip', //远程包路径localUpdateVersionFilePath: production === 'dev' ? `${rootPath}/latest` : `${rootPath}/resources/latest`, //本地版本信息路径localUpdateFilePath: production === 'dev' ? rootPath : `${rootPath}/resources`, //本地包路径updateSteps: [{id: 1, desc: '开始下载并解压更新文件,请勿重启!', active: 'active'},{id: 2, desc: '下载并解压完成, 开始覆盖安装!', active: 'wait'},{id: 3, desc: '更新完毕, 即将重启,请稍候!(第3步完成后也可以手动重启)', active: 'wait'},], //更新步骤  active:正在进行  wait:等待   success:执行成功  error: 执行失败
};let versionInfo = ''; //获取最新版本信息
let locallatest = ''; //本地版本号function appInit() {txtConsole.log('初始化');try {locallatest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');locallatest = JSON.parse(locallatest);//设置当前版本信息winUpdateConfig.currentVersion = locallatest?.version;txtConsole.log('已设置当前版本信息', locallatest?.version);//删除日志txtConsole.clearLog();} catch (err) {txtConsole.log(err);}
}//创建主窗口
function createWindow() {const mainWindow = new BrowserWindow({width: 800,height: 600,webPreferences: {// preload: path.join(__dirname, 'preload.js'),nodeIntegration: true,contextIsolation: false,webSecurity: false, //禁用同源策略}});mainWindow.loadFile('index.html').then();// 打开开发者工具 控制台if (mainData.winControl === 'dev') {mainWindow.webContents.openDevTools();}// 检查更新ipcMain.on('window-version', function (event) {try {txtConsole.log('检查更新', versionInfo);if (!versionInfo) {!event?.sender?.isDestroyed() &&event?.sender?.send('window-version-err-msg', '更新文件读取失败');return;}const v = {...(versionInfo || {})};//最新版本号let firNewVersion = versionInfo?.version?.split('.')?.[0]; //第一位let secNewVersion = versionInfo?.version?.split('.')?.[1]; //第二位let thiNewVersion = versionInfo?.version?.split('.')?.[2]; //第三位//当前版本号let firOldVersion = versionInfo?.currentVersion?.split('.')?.[0]; //第一位let secOldVersion = versionInfo?.currentVersion?.split('.')?.[1]; //第二位let thiOldVersion = versionInfo?.currentVersion?.split('.')?.[2]; //第三位//按位比较是否需要更新if (Number(firNewVersion || 10000) > Number(firOldVersion || 10000)) {v['versionVisible'] = true;}else if (Number(secNewVersion || 10000) > Number(secOldVersion || 10000)) {v['versionVisible'] = true;}else if (Number(thiNewVersion || 10000) > Number(thiOldVersion || 10000)) {v['versionVisible'] = true;}else {v['versionVisible'] = false;v['currentVersion'] = versionInfo?.version;}if (!v['versionVisible']) {let latest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');latest = JSON.parse(latest);latest.version = versionInfo?.version || latest?.version;latest.currentVersion = versionInfo?.version || latest?.version;fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath, JSON.stringify(latest));txtConsole.log('hot: ', latest.version);}txtConsole.log('versionVisible=> ', v['versionVisible']);!event?.sender?.isDestroyed() && event?.sender?.send('window-version-msg', v);} catch (err) {!event?.sender?.isDestroyed() &&event?.sender?.send('window-version-err-msg', '更新文件读取失败');txtConsole.log('检查更新err:', err);}});// 下载更新文件ipcMain.on('window-download-newfile', function (event) {txtConsole.log('开始下载并解压更新文件  热更新');event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);const file = fs.createWriteStream(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'),);let downloadedBytes = 0;let totalBytes = 0;http.get(winUpdateConfig.updateFilePath, (response) => {totalBytes = parseInt(response?.headers['content-length'], 10);let prevTimestamp = Date.now();response?.on('data', (chunk) => {downloadedBytes += chunk.length;const timestamp = Date.now();const timeDiff = timestamp - prevTimestamp;// 每1.5秒钟更新一次进度if (timeDiff >= 1500) {const progress = ((downloadedBytes / totalBytes) * 100).toFixed(2);txtConsole.log(`下载进度:${progress}% `, totalBytes);prevTimestamp = timestamp;event?.sender?.send('window-download-progress-msg', Math.min(Number(progress), 80));}});response?.pipe(file);}).on('error', (err) => {txtConsole.log(`下载错误: ${err.message}`);event?.sender?.send('window-download-newfile-err-msg', '更新文件下载失败');});file?.on('finish', function () {event.sender.send('window-download-progress-msg', 90);winUpdateConfig.updateSteps[0]['active'] = 'success';winUpdateConfig.updateSteps[1]['active'] = 'active';event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);// 文件已经完全写入磁盘,开始解压try {const zip = new AdmZip(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'), void 0);zip.extractAllTo(winUpdateConfig.localUpdateFilePath, true, void 0, void 0);} catch (err) {txtConsole.log('解压异常 error: ', err);!event?.sender?.isDestroyed() &&event?.sender?.send('window-download-newfile-err-msg', '解压异常');return;}winUpdateConfig.updateSteps[1]['active'] = 'success';winUpdateConfig.updateSteps[2]['active'] = 'active';event?.sender?.send('window-download-newfile-msg', winUpdateConfig.updateSteps);event?.sender?.send('window-download-progress-msg', 95);const sourceDir = path.join(winUpdateConfig.localUpdateFilePath, 'apps');const destPath = path.join(winUpdateConfig.localUpdateFilePath, 'app.asar');asar.createPackage(sourceDir, destPath).then(() => {if (fs.existsSync(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'))) {fs.unlinkSync(path.resolve(winUpdateConfig.localUpdateFilePath, 'app.zip'));}txtConsole.log('更新完毕');event.sender.send('window-download-progress-msg', 100);winUpdateConfig.updateSteps[2]['active'] = 'success';event?.sender?.send('window-download-newfile-msg',winUpdateConfig.updateSteps,'success',);//设置当前版本信息try {let latest = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath, 'utf-8');latest = JSON.parse(latest);latest.version = versionInfo.version;fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath, JSON.stringify(latest));txtConsole.log('更新后已设置当前版本信息', latest?.version);//删除apps文件夹  防止执行文件夹内的代码deleteFolderRecursive(sourceDir);} catch (err) {txtConsole.log(err);}}).catch((err) => {txtConsole.log('创建asar文件失败: ', err);event.sender.send('window-download-newfile-err-msg', 'asar文件创建失败');});});file?.on('error', function (err) {txtConsole.log('更新asar=>Error: ', err);event.sender.send('window-download-newfile-err-msg', err);});});
}//检查更新
function checkUpdate(callback) {txtConsole.log('检查更新');http.get(winUpdateConfig.updateVersionFilePath, (res) => {res.on('data', (chunk) => {versionInfo += chunk;});res.on('end', () => {try {if (versionInfo && versionInfo?.indexOf('404 Not Found') < 0) {versionInfo = JSON.parse(versionInfo);winUpdateConfig.updateFilePath = versionInfo.updateFilePath;//热更最新信息let asarVersionInfo = {newVersionDesc: versionInfo.newVersionDesc,currentVersion: winUpdateConfig.currentVersion,};versionInfo.currentVersion = winUpdateConfig.currentVersion;let writeNewVersonInfo;//不存在则创建latest文件if (!fs.existsSync(winUpdateConfig.localUpdateVersionFilePath)) {writeNewVersonInfo = versionInfo;txtConsole.log('latest文件重新创建成功');}else {let currentVersion = fs.readFileSync(winUpdateConfig.localUpdateVersionFilePath,'utf8',);currentVersion = JSON.parse(currentVersion);currentVersion['updateFilePath'] = '';//只覆盖热更版本信息writeNewVersonInfo = {...currentVersion, ...asarVersionInfo};}//将整理好的配置文件信息写入fs.writeFileSync(winUpdateConfig.localUpdateVersionFilePath,JSON.stringify(writeNewVersonInfo),);// txtConsole.log('已将新的更新配置文件信息写入:', JSON.stringify(writeNewVersonInfo));txtConsole.log(`更新检查完毕:最新版本:${versionInfo.version}, 当前版本:${asarVersionInfo.currentVersion}`,);txtConsole.log('-------------------------------------------------------');callback?.(null, versionInfo);}else {txtConsole.log('更新配置文件读取失败');callback?.('更新配置文件读取失败');}} catch (err) {txtConsole.log('更新配置文件覆写失败');callback?.('更新配置文件覆写失败');}});}).on('error', (error) => {txtConsole.log(`更新配置文件下载失败: ${error.message}`);callback?.('更新配置文件下载失败');});
}//删除更新文件
function deleteFolderRecursive(folderPath) {if (fs.existsSync(folderPath)) {fs.readdirSync(folderPath).forEach((file) => {const curPath = path.join(folderPath, file);if (fs.lstatSync(curPath).isDirectory()) {// 递归删除子文件夹deleteFolderRecursive(curPath);}else {// 删除文件fs.unlinkSync(curPath);}});// 删除子文件夹后删除文件夹本身fs.rmdirSync(folderPath);}
}app.whenReady().then(() => {//初始化appInit();//检查更新checkUpdate(async (check, versionInfo = {}) => {if (check) {txtConsole.log('检查更新执行失败');}else {txtConsole.log('检查更新执行成功');}createWindow();app.on('activate', function () {if (BrowserWindow.getAllWindows().length === 0) createWindow()})});
})app.on('window-all-closed', function () {if (process.platform !== 'darwin') app.quit()
})
  1. 渲染进程侧代码(以原生为例):
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"/><meta http-equiv="X-UA-Compatible" content="IE=edge"/><meta name="viewport" content="width=device-width,initial-scale=1.0"/><title>Hello World!</title>
</head>
<body>
<h1>Hello World!</h1>
<button onclick="onCheckUpdate()">检查更新</button>
<button onclick="onUpdateVersion()">测试更新</button><div class="updateInfo"></div>
<div class="descInfo"></div><script>const {ipcRenderer} = require("electron");const onVersion = {updateDsec: [],process: 0,};//检查更新function onCheckUpdate() {ipcRenderer?.send('window-version');ipcRenderer?.once('window-version-msg', (_, bool) => {document.querySelector('.updateInfo').innerHTML = JSON.stringify(bool);});ipcRenderer?.once('window-version-err-msg', (_, err) => {document.querySelector('.updateInfo').innerHTML = err;});}//测试更新function onUpdateVersion() {onVersion.updateDsec = [];ipcRenderer?.send('window-download-newfile');//监听下载版本信息ipcRenderer?.on('window-download-newfile-msg', (event, updateDsec, status) => {if (!event.handel) {event.handel = true;onVersion.isStartUpdate = true;onVersion.updateDsec = updateDsec;document.querySelector('.descInfo').innerHTML = updateDsec.map(item => `<span style="color:${item.active === 'success' ? 'green' : 'orangered'}">${item.desc}</span>`).join('</br>');if (status === 'success') {ipcRenderer?.send('window-restart-app');}onVersion.visible = true;}});//监听更新包下载进度ipcRenderer?.on('window-download-progress-msg', (event, process) => {if (!event.handel) {event.handel = true;document.querySelector('.updateInfo').innerHTML = String("完成进度:" + process + '%');}});//监听下载版本错误信息ipcRenderer?.once('window-download-newfile-err-msg', (event, res) => {console.log(res)});}
</script>
</body>
</html>
  1. 轮子(实现压缩混淆反编译):
//生成 反编译app.asar  并生成压缩包
const asar = require('asar');
const path = require('path');
const fs = require('fs');
const fsExtra = require('fs-extra');
const zlib = require('zlib');
const archiver = require('archiver');
const uglify = require('uglify-js');
const moment = require('moment');
const {exec} = require('child_process');
const JavaScriptObfuscator = require('javascript-obfuscator');const mainData = require('./mainData');const startTime = moment().unix(); //秒级时间戳const rootPath = path.resolve(__dirname); // 获取项目根路径
const asarPath = './build/win-ia32-unpacked/resources/app.asar'; // 获取 app.asar 文件路径
const sourceDir = './apps'; // 要压缩的文件夹路径
const asarAppPath = './apps/apps'; // asar反编译文件的存放路径
const buildPath = './build'; //electron 打包后的build文件夹
const destFile = './app.zip'; // 压缩后的文件路径
const publicLogPath = './public/log.txt';
const publicAsarPath = './public/app.asar';// 配置环境路径为项目根路径
const env = Object.assign({}, process.env, {PATH: rootPath + ';' + process.env.PATH,npm_config_prefix: 'C:\\Program Files\\nodejs\\npm', // 这里是你的 npm 安装路径
});const Console = {log(p1 = '', p2 = '', p3 = '', p4 = '', p5 = '') {console.log(`${moment().format('HH:mm:ss')}  |  ${p1}${p2}${p3}${p4}${p5}`);},
};//压缩主进程 main.js 相关代码
function zipMainJS() {try {const dir = path.resolve(asarAppPath, 'main.js');const dirJs = fs.readFileSync(dir, 'utf8');//压缩代码 mangle: true,const result = uglify.minify(dirJs, {mangle: {toplevel: true,},});// 混淆代码const obfuscationResult = JavaScriptObfuscator.obfuscate(result.code, {compact: true,controlFlowFlattening: true,controlFlowFlatteningThreshold: 0.75,numbersToExpressions: true,simplify: true,shuffleStringArray: true,splitStrings: true,stringArrayThreshold: 0.75,});fs.writeFileSync(dir, obfuscationResult.getObfuscatedCode());return true;} catch (err) {Console.log(err);return false;}
}//添加开始执行 app.asar反编译逻辑
async function init() {//执行app.asar 反编译、压缩、混淆Console.log('正在执行app.asar 反编译、压缩、混淆');// 将 app.asar 解压缩到指定文件夹中asar.extractAll(asarPath, asarAppPath);Console.log('正在压缩app文件夹到项目根目录');//压缩main.js相关代码let zipRes = zipMainJS();if (!zipRes) {Console.log('!!!压缩main.js主进程代码失败!');return;}Console.log('主进程相关代码压缩完毕');//再次生成 app.asarasar.createPackage(asarAppPath, asarPath).then(() => {Console.log('已再次生成 app.asar 文件(代码压缩后的asar文件)');onAppZip();});
}function onAppZip() {// 创建一个可写流,将压缩后的文件写入到目标文件中const destStream = fs.createWriteStream(destFile);// 创建一个 archiver 实例const archive = archiver('zip', {zlib: {level: zlib.constants.Z_BEST_COMPRESSION},});// 将可写流传递给 archiver 实例archive.pipe(destStream);// 将要压缩的文件夹添加到 archiver 实例中archive.directory(sourceDir, false, null);// 完成压缩并关闭可写流archive.finalize();// 监听可写流的 'close' 事件,表示压缩完成destStream.on('close', () => {Console.log(`压缩完毕,压缩包路径:【${path.resolve(__dirname, destFile)}`);Console.log('共用时:' + (moment().unix() - startTime) + '秒');});
}try {if (mainData?.production === 'dev') {throw "请将环境切换为生产环境 mainData.js => 【const production = 'pro';】";}if (mainData?.winControl === 'dev') {throw '请关闭主窗口调试控制台!' + 'winControl';}fsExtra.removeSync(buildPath);Console.log('已删除build文件夹内容');fsExtra.removeSync(publicLogPath);Console.log('已删除public/log.txt');fsExtra.removeSync(publicAsarPath);Console.log('已删除public/app.asar');fsExtra.removeSync(asarAppPath);Console.log('已删除apps');//执行 打包命令Console.log('正在执行【npm run packager32】命令');exec('npm run packager32', env, (error, stdout, stderr) => {if (error) {Console.log(`执行出错: ${error}`);return;}stderr && Console.log('【npm run packager32】 stderr=>', stderr);//生成app.init();});} catch (err) {Console.log(err);
}

示例Demo: https://github.com/qglovehy/electron-updater.git

相关文章:

electron实现软件(热)更新(附带示例源码)

热更新指的是&#xff1a;electron 程序已经开启&#xff0c;在不关闭的情况下执行更新&#xff0c;需要我们把远程的app.asar文件下载到本地执行替换&#xff0c;然而 在electron应用程序开启状态是无法直接下载app.asar文件的&#xff0c;下载会检查出app.asar文件被占用&…...

飞天使-k8s知识点12-kubernetes散装知识点1-架构有状态资源对象分类

文章目录 k8s架构图有状态和无状态服务 资源和对象对象规约和状态 资源的对象-资源的分类元数据型与集群型资源命名空间 k8s架构图 有状态和无状态服务 区分有状态和无状态服务有利于维护yaml文件 因为配置不同资源和对象 命令行yaml来定义对象对象规约和状态 规约 spec 描述…...

mhz_c1f

信息收集 探测到存活主机的IP地址为 192.168.101.32 # nmap -sT --min-rate 10000 -p- 192.168.101.32 -oN port.nmap Starting Nmap 7.94 ( https://nmap.org ) at 2024-02-03 13:41 CST Nmap scan report for 192.168.101.32 Host is up (0.0020s latency). Not shown: 6553…...

Excel——高级筛选匹配条件提取数据

一、筛选多条件 Q&#xff1a;筛选多个条件&#xff0c;并将筛选出的内容复制到其他区域 点击任意一个单元格 点击【数据】——【筛选】——【高级筛选】 选择【将筛选结果复制到其他位置】——在【列表区域】 鼠标选择对应的区域位置&#xff0c;条件区域一定要单独写出来&a…...

Python初学者学习记录——python基础综合案例:数据可视化——动态柱状图

一、案例效果 通过pyecharts可以实现数据的动态显示&#xff0c;直观的感受1960~2019年世界各国GDP的变化趋势 二、通过Bar构建基础柱状图 反转x轴和y轴 标签数值在右侧 from pyecharts.charts import Bar from pyecharts.options import LabelOpts# 构建柱状图对象 bar Bar()…...

1.27马尔科夫链,抽样蒙特卡洛模拟(逆转化方法,接受拒绝矩阵),马尔科夫链蒙特卡洛MCMC,隐马尔科夫(HMM(V算法剪枝优化),NLP)

马尔科夫链 蒙特卡洛法模拟 抽样&#xff0c;逆转换方法 就是说由系统自带的随机函数RANDOM&#xff0c;通过下面这个方法&#xff0c;可以变为对应的随机模拟函数 就是说要实现蒙特卡洛模拟&#xff0c;是要先有一个概率表达式&#xff0c;然后基于这个概率表达式&#xff0…...

MC34063异常发热分析

问题描述&#xff1a; 工程现场反馈若干电源转换模块损坏&#xff0c;没有输出。拿到问题模块后&#xff0c;查看有一个MC34063周围的PCB有比较明显的高温痕迹&#xff0c;配套的电感也有明显的高温过热痕迹。 问题调查&#xff1a; MC34063的电路非常经典&#xff08;虽然自…...

获取真实 IP 地址(一):判断是否使用 CDN(附链接)

一、介绍 CDN&#xff0c;全称为内容分发网络&#xff08;Content Delivery Network&#xff09;&#xff0c;是一种网络架构&#xff0c;旨在提高用户对于网络上内容的访问速度和性能。CDN通过在全球各地部署分布式服务器节点来存储和分发静态和动态内容&#xff0c;从而减少…...

跨越财务困境,聚道云软件连接器如何助力企业轻松实现数字化转型?

客户介绍 某家庭服务科技有限公司是一家专注于提供高品质家庭服务的综合性企业。公司以“让家庭生活更美好”为使命&#xff0c;致力于为每一位客户提供专业、细致、周到的家庭服务。作为一家具有社会责任感的企业&#xff0c;该公司积极履行企业公民义务&#xff0c;关注家庭…...

Python接口自动化测试框架运行原理及流程

这篇文章主要介绍了Python接口自动化测试框架运行原理及流程,文中通过示例代码介绍的非常详细&#xff0c;对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 本文总结分享介绍接口测试框架开发&#xff0c;环境使用python3selenium3unittestddtrequests测试框…...

strtok的使用

Strtok&#xff1a;原型char *strtok(char s[], const char *delim); s为要分解的字符&#xff0c;delim为分隔符字符&#xff08;如果传入字符串&#xff0c;则传入的字符串中每个字符均为分割符&#xff09;。首次调用时&#xff0c;s指向要分解的字符串&#xff0c;之后再次…...

0206作业

TCP&#xff08;传输控制协议&#xff09;和 UDP&#xff08;用户数据报协议&#xff09;是两种常用的网络传输协议。它们之间的主要区别在于&#xff1a; 可靠性&#xff1a;TCP 是一种可靠的传输协议&#xff0c;它提供了数据传输的确认、重传和排序功能。如果数据在传输过程…...

数据结构-栈

1.容器 容器用于容纳元素集合&#xff0c;并对元素集合进行管理和维护&#xff0e; 传统意义上的管理和维护就是&#xff1a;增&#xff0c;删&#xff0c;改&#xff0c;查&#xff0e; 我们分析每种类型容器时&#xff0c;主要分析其增&#xff0c;删&#xff0c;改&#xff…...

CentOS7搭建k8s-v1.28.6集群详情

文章目录 1.灌装集群节点操作系统1.1 设置hosts1.2 设置nameserver1.3 关闭防火墙1.4 关闭Selinux1.5 关闭Swap分区1.6 时间同步1.7 调整内核参数1.8 系统内核升级 2.安装Docker2.1 卸载旧Docker2.2 配置Docker软件源2.3 安装Docker 3.部署Kubernets集群3.1 设置 K8s 软件源3.2…...

Android实现底部导航栏方法(Navigation篇)

Navigation实现底部导航栏 前言导入和基本使用导入基础使用创建nav文件编辑Nav文件添加页面&#xff08;代码版&#xff09;添加页面&#xff08;图解版&#xff09; 创建导航动作 action创建action&#xff08;代码版&#xff09;创建action&#xff08;图解版&#xff09; 编…...

python 爬虫篇(1)---->re正则的详细讲解(附带演示代码)

re正则的详细讲解 文章目录 re正则的详细讲解前言4.re正则表达式(1)e正则的匹配模式(2) re.search 的使用(3)re.findall()的使用(4)re.sub()的使用结语前言 大家好,今天我将开始更新python爬虫篇,陆续更新几种解析数据的方法,例如 re正则表达式beautifulsoup xpath lxml 等等,…...

(超详细)10-YOLOV5改进-替换CIou为Wise-IoU

yolov5中box_iou其默认用的是CIoU&#xff0c;其中代码还带有GIoU&#xff0c;DIoU&#xff0c;文件路径&#xff1a;utils/metrics.py&#xff0c;函数名为&#xff1a;bbox_iou 将下面代码放到metrics.py文件里面&#xff0c;原来的bbox_iou函数删掉 class WIoU_Scale: mon…...

Java-并发高频面试题-2

接着之前的Java-并发高频面试题 7. synchronized的实现原理是怎么样的&#xff1f; 首先我们要知道synchronized它是解决线程安全问题的一种方式&#xff0c;而具体是怎么解决的呢&#xff1f;主要是通过加锁的方式来解决 在底层实现上来看 是通过 monitorenter、monitorexit…...

Windows安装Redis

安装Redis是一个比较简单的过程&#xff0c;以下是在Windows上安装Redis的基本步骤&#xff1a; 下载Redis&#xff1a;首先&#xff0c;你需要从Redis官方网站&#xff08;https://redis.io/download&#xff09;下载适合Windows的Redis安装包。你可以选择稳定版本或者开发版本…...

Nicn的刷题日常之 有序序列判断

目录 1.题目描述 描述 输入描述&#xff1a; 输出描述&#xff1a; 示例1 示例2 示例3 2.解题 1.题目描述 描述 输入一个整数序列&#xff0c;判断是否是有序序列&#xff0c;有序&#xff0c;指序列中的整数从小到大排序或者从大到小排序(相同元素也视为有序)。 数据…...

微信小程序之bind和catch

这两个呢&#xff0c;都是绑定事件用的&#xff0c;具体使用有些小区别。 官方文档&#xff1a; 事件冒泡处理不同 bind&#xff1a;绑定的事件会向上冒泡&#xff0c;即触发当前组件的事件后&#xff0c;还会继续触发父组件的相同事件。例如&#xff0c;有一个子视图绑定了b…...

vscode(仍待补充)

写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh&#xff1f; debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...

对WWDC 2025 Keynote 内容的预测

借助我们以往对苹果公司发展路径的深入研究经验&#xff0c;以及大语言模型的分析能力&#xff0c;我们系统梳理了多年来苹果 WWDC 主题演讲的规律。在 WWDC 2025 即将揭幕之际&#xff0c;我们让 ChatGPT 对今年的 Keynote 内容进行了一个初步预测&#xff0c;聊作存档。等到明…...

Spring AI 入门:Java 开发者的生成式 AI 实践之路

一、Spring AI 简介 在人工智能技术快速迭代的今天&#xff0c;Spring AI 作为 Spring 生态系统的新生力量&#xff0c;正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务&#xff08;如 OpenAI、Anthropic&#xff09;的无缝对接&…...

OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别

OpenPrompt 和直接对提示词的嵌入向量进行训练有什么区别 直接训练提示词嵌入向量的核心区别 您提到的代码: prompt_embedding = initial_embedding.clone().requires_grad_(True) optimizer = torch.optim.Adam([prompt_embedding...

AI,如何重构理解、匹配与决策?

AI 时代&#xff0c;我们如何理解消费&#xff1f; 作者&#xff5c;王彬 封面&#xff5c;Unplash 人们通过信息理解世界。 曾几何时&#xff0c;PC 与移动互联网重塑了人们的购物路径&#xff1a;信息变得唾手可得&#xff0c;商品决策变得高度依赖内容。 但 AI 时代的来…...

视觉slam十四讲实践部分记录——ch2、ch3

ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...

协议转换利器,profinet转ethercat网关的两大派系,各有千秋

随着工业以太网的发展&#xff0c;其高效、便捷、协议开放、易于冗余等诸多优点&#xff0c;被越来越多的工业现场所采用。西门子SIMATIC S7-1200/1500系列PLC集成有Profinet接口&#xff0c;具有实时性、开放性&#xff0c;使用TCP/IP和IT标准&#xff0c;符合基于工业以太网的…...

mac:大模型系列测试

0 MAC 前几天经过学生优惠以及国补17K入手了mac studio,然后这两天亲自测试其模型行运用能力如何&#xff0c;是否支持微调、推理速度等能力。下面进入正文。 1 mac 与 unsloth 按照下面的进行安装以及测试&#xff0c;是可以跑通文章里面的代码。训练速度也是很快的。 注意…...

智能职业发展系统:AI驱动的职业规划平台技术解析

智能职业发展系统&#xff1a;AI驱动的职业规划平台技术解析 引言&#xff1a;数字时代的职业革命 在当今瞬息万变的就业市场中&#xff0c;传统的职业规划方法已无法满足个人和企业的需求。据统计&#xff0c;全球每年有超过2亿人面临职业转型困境&#xff0c;而企业也因此遭…...