二次开发萌化VisitorLoggerPro插件其实之前我一直在用VisitorLoggerPro这个插件，但是由于之前的版本不够完善，也没想着进一步提升改造。前几天偶然翻了翻这个项目的GitHub仓库，发现基本上更新完善了，上一次更改已经是三个月之前，遂下了最新版回来尝试一下，发现功能非常满意。但是，美中不足的是，插件从jsdelivr.net下载相关的静态文件容易卡半天加载不出来。虽然插件写有智能回退使用网站的服务器进行加载，但是我发现它缺失了文件且返回路径不对，导致如果jsdelivr.net无法访问，页面会加载异常。于是，就萌生了修复这个插件的问题并且改造进行个性化的想法。打开了后台页面观摩了一下，并用浏览器的开发者工具随手调了一下CSS样式，最终明确了个性化的路径：设置一个全局背景图看板卡片进行半透明化于是说干就干，我先Fork了原项目，想了想要不就取名VisitorLoggerPro-Enhanced吧！然后开始了我的折腾和修改之旅。Github项目地址：VisitorLoggerPro-Enhanced可以去直接下载使用。修复静态文件回退到本地失败这肯定是最先需要解决的问题。如果不解决这个问题，该插件在我这里就处于不可用状态。补回缺失的本地文件这一步对于有网站搭建经验的人很简单，F12打开浏览器开发者工具，然后刷新页面观察网页都发送了那些外链请求，一下子就抓出来了：本地缺失的js：https://cdn.jsdelivr.net/npm/flatpickr本地缺失的css：https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css其中，flatpickr抓取回来是没有后缀的，得加上个.js后缀变成flatpickr.js。然后对号入座，flatpickr.js放到js/目录，新建一个css/目录放置flatpickr.min.css。补回文件后的插件目录文件树应该是这样的：tree . ├── action │ └── visitor-stats.php ├── Action.php ├── adapter.php ├── css │ └── flatpickr.min.css ├── getTrendData.php ├── getVisitStatistic.php ├── ip2region │ └── src │ ├── ip2region.xdb │ └── XdbSearcher.php ├── ipdata │ └── src │ ├── ipdbv6.func.php │ ├── IpLocation.php │ ├── ipv6wry-country.db │ ├── ipv6wrys.db │ ├── qqwry.dat │ ├── qqwrye.dat │ └── zxipv6wry.db ├── js │ ├── echarts.min.js │ ├── flatpickr.js │ └── README.md ├── panel.php ├── Plugin.php ├── README.md ├── trend.php └── visitor-stats.php 8 directories, 23 files修复完善静态文件获取逻辑原本插件本地静态文件的相对链接写法是：localScript.src = './js/echarts.min.js';这会导致获取文件时发生404错误，因为在插件页面上下文中，该相对路径会被解析为https://www.nekopara.uk/admin/js/echarts.min.js，那包404的。正确的URL连接位置应该是https://www.nekopara.uk/usr/plugins/VisitorLoggerPro/js/echarts.min.js，所以，这个相对链接应该改成../usr/plugins/VisitorLoggerPro/js/echarts.min.js。而且，原插件仅实现了echarts.min.js的本地回退逻辑，未处理另外两个静态文件（flatpickr.js和flatpickr.min.css），因此即使修复了路径，还是会卡加载。还有一个致命的缺陷是，原本的回退本地源的逻辑是等待失败，但是一等就是大几十秒，非常影响体验。于是我就设置了超时时间，一旦第三方的静态文件加载时间超过2秒，立即回退到本地，避免长时间等待。以下是完善后的逻辑代码：对于panel.php内嵌js部分的修改，在原文件64行的地方开始修改： 对于visitor-stats.php内嵌js部分的修改，在原文件90行的地方开始修改： 对于trend.php内嵌js部分的修改，在原文件12行的地方开始修改： 对实现显示图表的trend.php，panel.php和visitor-stats.php的修复主要解决以下核心问题：加载超时问题：原插件等待第三方CDN加载超时（大几十秒），现添加2秒超时机制，一旦CDN加载超时立即回退到本地，极大提升加载体验。加载逻辑完善：原逻辑仅处理echarts.min.js，现完整支持echarts.min.js、flatpickr.js和flatpickr.min.css的智能加载与回源。错误处理增强：添加加载成功/失败日志，以及加载失败时的用户提示，提升用户体验与问题排查效率。以上修复确保在jsdelivr.net不可用时，插件仍能快速加载并正常运行，同时保持原有功能完整性。修复我已经为GitHub上的原项目提交了的Pull Requests，能不能被合并就不知道了（接下来开始搞我的个性化了）萌化VisitorLoggerPro首先，最简单粗暴的萌化方式就是加上二次元背景图，这是最直接，最有效果的方式。我的修改思路是，先在浏览器开发者工具里面直接修改网页代码查看效果，然后再针对性去修改插件里面对应的代码。为插件添加背景图在插件界面，右键点击页面边缘的空白处，选择检查。然后在HTML代码区一路上划找到页面最底层的 盒子，发现是 ，然后针对性去找CSS样式表，真给我找到了： .main { padding: 20px; background-color: #f5f7fa; min-height: 100vh; }这时可以先在浏览器里面修改CSS样式表看看效果：.main { padding: 20px; min-height: 100vh; background-image: url('https://teachermate.oss-cn-qingdao.aliyuncs.com/irGnf-1725727799259-neko4_n33b.png'); background-repeat: no-repeat; background-position: center center; background-attachment: fixed; background-size: cover; }瞬间就对味了一大截：然后就需要修改规则了，在全部文件中检索.main这个CSS类选择器，然后进行代码修改。好了，那么现在问题来了，我虽然是可以直接找到对应的代码写死URL在上面，但是如果别人想换个图片，就没这么方便了。于是我就在思考，能不能把这个参数作为应该可以给用户设置的插件参数？得益于Typecho插件开发的便利性，答案是肯定的，只需要在插件设置界面给出设置插件的参数的地方，在后面通过Helper::options()获取这个参数即可。找Plugin.php添加多一个设置选项：插件背景设置 /* 插件背景设置 */ $backgroundUrl = new Typecho_Widget_Helper_Form_Element_Text( 'backgroundUrl', null, 'https://pic.nekopara.uk/?format=webp', // 默认值 _t('插件背景设置'), _t('可填写图片API的URL（如随机图片API）或具体图片的URL直链。默认使用猫娘乐园图片API') ); $form->addInput($backgroundUrl);我这里定义参数名称为backgroundUrl，然后在插件代码里面进行获取的方式就是：$backgroundUrl = Helper::options()->plugin('VisitorLoggerPro')->backgroundUrl ?: 'https://pic.nekopara.uk/?format=webp';在嵌入到PHP的CSS样式表部分，我只需要这样子修改： .main { padding: 20px; min-height: 100vh; background-image: url('<?php echo $backgroundUrl; ?>'); background-repeat: no-repeat; background-attachment: fixed; background-position: center center; background-size: cover; }就可以根据设置的URL返回特定的样式表了，让用户可以自定义背景图的问题也就迎刃而解了。插件默认的设置图片来源是我的猫娘乐园CG图API，可以自行更换为其他图片直链或者图片API。为卡片设置半透明颜色但是，只是修改一个背景图，卡片是不透明的把好看的图片都基本上档完了，多没意思？于是，我就想在保证可读性的情况下，让卡片半透明化，让背景图能被看到。继续F12大法，找到每个卡片对应的CSS规则：访客日志页面部分，对应panel.php文件： .page-header { background: #fff; border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; } .action-form { background: #fff; padding: 20px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; display: flex; flex-direction: column; } .logs-section { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); /* height: calc(100vh - 200px); */ overflow: auto; min-width: 0; border: 1px solid #e2e8f0; } .stats-section { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); /* height: calc(100vh - 200px); */ display: flex; flex-direction: column; gap: 24px; border: 1px solid #e2e8f0; } .chart-container { flex: 1; min-height: 260px; background: linear-gradient(135deg, #fff 0%, #f8fafc 100%); border-radius: 12px; padding: 8px; border: 1px solid #e2e8f0; position: relative; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); transition: all 0.3s ease; overflow: hidden; }访客日志趋势分析部分，对应trend.php文件： .page-header { background: #fff; border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; } .controls-section { background: #fff; border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; } .trend-section { background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; } .stats-summary { display: flex; justify-content: center; gap: 20px; margin-bottom: 20px; padding: 16px; background: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0; flex-wrap: wrap; } .metrics-explanation { background: #fff; border-radius: 12px; padding: 24px; margin-top: 24px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; } .metric-card { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 20px; transition: all 0.3s ease; } .technical-notes { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 10px; padding: 20px; border-left: 4px solid #3498db; }只需要将每个卡片的background: #fff;定义颜色部分设置成半透明即可。虽然最简单的方法就是保持原来的颜色，增加透明度，但是我考虑到这一片卡片后面如果是靠修改代码调整，调整起来也很花时间，不如像图片背景那样给用户输入一个颜色自己配。同样的方法，在Plugin.php中增加配置选项： /* 插件卡片颜色设置 */ $backgroundColour = new Typecho_Widget_Helper_Form_Element_Text( 'backgroundColour', null, '#ffffffc4', // 默认值 _t('插件展示内容卡片颜色设置，采用HTML颜色代码') ); $form->addInput($backgroundColour);然后在panel.php和trend.php加上：// 获取配置的卡片背景色（带默认值） $backgroundColour = Helper::options()->plugin('VisitorLoggerPro')->backgroundColour ?: '#ffffffc4';并针对性的设置需要设置的颜色CSS代码位置，例如： .page-header { background: <?php echo $backgroundColour; ?>; border-radius: 12px; padding: 20px; margin-bottom: 24px; box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05); border: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; }找到上面说的CSS代码，将背景颜色代码部分替换为<?php echo $backgroundColour; ?>就完事了。这次的自定义修改，让我对Typecho的插件和PHP代码逻辑有了更进一步的认识，虽然仍是面向LLM编程，但是至少有实现是思路和方法，不会具体的语法和代码，倒是可以让LLM来写。效果展示最后展示一下萌化后的效果：这下暴露出小破站访问量了，果然是没什么人看的，被打的次数倒是不少（番外：使用方法进入你的Typecho部署目录，然后进入插件文件夹：cd /data/typecho/ cd usr/plugins/随后，克隆项目并重命名文件夹：git clone https://github.com/Chocola-X/VisitorLoggerPro-Enhanced mv VisitorLoggerPro-Enhanced/ VisitorLoggerPro/删除无用文件（可选）：cd VisitorLoggerPro/ rm -rf .git/ rm -rf .gitignore rm README.md 然后进入Typecho后台启用插件即可。当然，如果你凑巧服务器在国内无法正常访问GitHub的话，我这里有直链可以给你下载，进入插件目录下载并解压即可：wget https://teachermate.oss-cn-qingdao.aliyuncs.com/air2Q-1762344686455-VisitorLoggerPro.zip unzip https://teachermate.oss-cn-qingdao.aliyuncs.com/air2Q-1762344686455-VisitorLoggerPro.zip rm https://teachermate.oss-cn-qingdao.aliyuncs.com/air2Q-1762344686455-VisitorLoggerPro.zip
基于 E-mote 模型的动态猫娘展示与部署教程看门见山，先给一个展示地址看看效果：猫娘乐园角色E-mote图鉴Github Pages部署的纯静态版本：猫娘乐园角色E-mote图鉴这篇文章或许是最符合网站域名的文章之一？项目缘起很久之前，在我切换到Linux系统后，发现我之前最喜欢的猫娘乐园动态壁纸没办法在Linux上使用，然后根据那个第三方壁纸的相关信息，刨根问底发现了FreeMote这个开源项目，并且看到了他的Web段实现了Demo，就萌生了这个项目的想法了。但是碍于那时候技术能力和能分配的时间有限，于是这个想法就一直搁置了。最近不知道哪根经搭错了，突然就想把这个多年前的想法去实现，重新翻阅了相关的开源项目，借助AI，然后发现当时不能成功是疏漏了一些东西，于是打算再次去尝试实现。一开始的那几天天天碰壁，试了很多不同的方法都不行，但是，偶然的一次机会，我观察到提取的模型和解包的模型有相似之处，于是我就尝试从中间进行合成，没想到，真成功了！接下来的工作就顺风顺水了，但是就是很累人，要组织和转换每一个模型，八个猫娘，主要的六个猫娘还有好几套衣服，那个工作量是真的巨大啊！但是，如你所见，经过不懈努力，这个项目做出来了！接下来，请让我为你介绍——「NekoWebShow」开发技术路线简单来说，这个项目最核心的部分就是FreeMote-SDK，没有这个开源的项目，本项目也无法实现。本项目是在弄清楚FreeMote这套框架逻辑后，将从UlyssesWu制作出来的的猫娘乐园角色动态壁纸解包出来的模型转换为可以让FreeMote-SDK的WebGL驱动的pure.psb格式，然后将各个模型和对应的声音组织好，写一个用于切换不同角色的前端框架，这就是我所做的事情。前期的一部分开发工作记录，我已经写了一篇文章：试玩FreeMote，把二次元老婆搬上浏览器，感兴趣的可以去看看，这篇文章关注点是如何使用和部署。因为前端需要实现的功能比较简单，所以我是直接面向LLM编程了，没有使用其他框架。本来想用MDUI的，但是后面想想还是算了。在这里，要特别感谢嘉然小姐对本项目的支持，她帮助我解决了驱动库无法加载超过32MB的pure.psb文件问题。没有她的支持，部分角色衣服的模型就没办法正常展示出来了。项目分支说明项目有两个分支，一个是php版本，另一个是html版本。之前是只打算做php版本的，毕竟可以代码复用，减少文件数量和代码量，但是后面考虑到如果是本地部署起来做动态壁纸，还要再跑一个php-fpm有点太地狱了，于是就搞了html的版本，纯静态实现，本地只需要部署一个web服务器即可。不过按理来说应该是可以直接打开本地文件而不需要服务器的，但是不知道为什么firefox打开就是报错，显示被阻止，所以也只好设置一个服务器了。不过，后面我试了一下，使用KDE桌面的HTML Wallpaper插件，直接指定本地文件就像，不会有报错，也可以正常显示，估计只是firefox的限制（所以，要是这样就非常好了，本地设置成壁纸web服务器都省了。部署方法一：PHP环境部署首先，确保你有一台服务器，部署上web服务器和php环境，我这里选择nginx和php，采用的操作系统为Alpine Linux 3.20。apk add php php-fpm openssl nginx rc-update add php-fpm83 default rc-update add nginx default service php-fpm82 start service nginx start这样一通操作下来，我们的基本环境就有了。接下来就是配置一下默认页面的规则，当然这是可选项，你可以直接一股脑用默认直接http部署，但是我并不推荐，我认为还是设置个域名配合SSL证书比较好。设置默认匹配页面我们先来生成一个默认的SSL自签名证书用于没有匹配到域名时错误提示页面使用。mkdir -p /etc/nginx/ssl/ openssl genpkey -algorithm RSA -out /etc/nginx/ssl/selfsigned.key -pkeyopt rsa_keygen_bits:2048 openssl req -new -x509 -key /etc/nginx/ssl/selfsigned.key -out /etc/nginx/ssl/selfsigned.crt -days 365 -subj "/"这样子就生成了自签名证书，证书和密钥位置分别在/etc/nginx/ssl/selfsigned.crt和/etc/nginx/ssl/selfsigned.key然后，删除/etc/nginx/http.d/default.conf，再重新创建：rm /etc/nginx/http.d/default.conf nano /etc/nginx/http.d/default.conf内容替换为如下内容：server { listen 80 default_server; #listen [::]:443 ssl default_server; server_name _; # 这是一个通配符，表示任何未匹配的主机名 #return 444; # 关闭连接，不发送任何响应 root /var/www/; # 静态文件的位置 index index.html; # 默认主页 location / { try_files $uri $uri/ =404; # 尝试提供请求的文件或目录，如果不存在则返回404 } # 处理错误页面 error_page 404 /404.html; location = /404.html { internal; } } server { listen 443 ssl default_server; #listen [::]:443 ssl default_server; server_name _; # 这是一个通配符，表示任何未匹配的主机名 ssl_certificate /etc/nginx/ssl/selfsigned.crt; ssl_certificate_key /etc/nginx/ssl/selfsigned.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES256-GCM-SHA384'; root /var/www/; # 静态文件的位置 index index.html; # 默认主页 location / { try_files $uri $uri/ =404; # 尝试提供请求的文件或目录，如果不存在则返回404 } # 处理错误页面 error_page 404 /404.html; location = /404.html { internal; } } server { listen 80; server_name *.nekopara.uk; # 302 Redirect all traffic to the corresponding HTTPS URL location / { return 302 https://$host$request_uri; } }记得在/var/www/创建一个错误提示页面index.html:nano /var/www/index.html内容我（宝塔 x)已经帮你想好了： 没有找到站点 没有找到站点 您的请求在Web服务器中没有找到对应的站点！ 可能原因： 您没有将此域名或IP绑定到对应站点! 配置文件未生效! 如何解决： 检查是否已经绑定到对应站点，若确认已绑定，请尝试重载Web服务； 检查端口是否正确； 若您使用了CDN产品，请尝试清除CDN缓存； 普通网站访客，请联系网站管理员； 默认页面设置好了，接下来就是设置对应的站点了。部署NekoWebShow我个人使用习惯是在根目录创建/data目录用于存放各种东西，先让我们克隆项目：mkdir /data cd /data/ git clone --depth 1 https://github.com/Chocola-X/NekoWebShow然后就是申请证书并且解析域名到你的服务器，这里不再赘述，有acme.sh可以用，当然你也可以用我的网页在线生成证书：猫娘☆免费SSL证书申请申请完成的证书，放到/data/certs/目录下。包括证书和私钥，例如_nekopara_uk.pem和_nekopara_uk.key。然后是配置nginx服务器：nano /etc/nginx/http.d/nekowebshow.conf配置文件内容如下：server { listen 443 ssl; server_name chocola.nekopara.uk vanilla.nekopara.uk azuki.nekopara.uk coconut.nekopara.uk fraise.nekopara.uk maple.nekopara.uk milk.nekopara.uk cinnamon.nekopara.uk show.nekopara.uk; # SSL certificate and key paths ssl_certificate /data/certs/_nekopara_uk.pem; ssl_certificate_key /data/certs/_nekopara_uk.key; # SSL settings (optional but recommended for security) ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # Root directory of the website root /data/nekowebshow; index index.html index.php; # Logging #access_log /var/log/nginx/nekopara.uk.access.log; #error_log /var/log/nginx/nekopara.uk.error.log; # Serve static files directly location / { try_files $uri $uri/ /index.html /index.php?$args; } # PHP processing location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } # Deny access to hidden files location ~ /\.ht { deny all; } }注意将域名替换为你实际的域名，以及你实际的证书路径，证书建议使用通配符证书。因为我的域名nekopara.uk很符合主题，所以我给每一个角色都添加了子域名，例如chocola.nekopara.uk就很有趣了。对于其他域名，请根据需要自行选择。这时候，打开对应的域名，应该是可以正常工作了，但是你会发现打开新的角色图鉴，就会跳转到我的网站。不要急不要哭，打开index.php让我们来改几个参数即可，这就是为什么我选择php构建的原因。打开index.php找到开头这里：$domain_chocola = 'https://chocola.nekopara.uk'; $domain_vanilla = 'https://vanilla.nekopara.uk'; $domain_azuki = 'https://azuki.nekopara.uk'; $domain_coconut = 'https://coconut.nekopara.uk'; $domain_maple = 'https://maple.nekopara.uk'; $domain_cinnamon = 'https://cinnamon.nekopara.uk'; $domain_milk = 'https://milk.nekopara.uk'; $domain_fraise = 'https://fraise.nekopara.uk'; $background_url = './img/bg.png';替换为对应的域名即可，例如第一条替换为：$domain_chocola = 'https://chocola.example.com';然后背景也是可以修改的，替换掉bg.png或者改为别的背景图URL即可，但是我个人还是认为游戏原本的背景比较搭配。。。修改完成后，也就部署好了。你可以之作为一个普通的角色图鉴使用，当然，我认为更好的做法是把这个做成动态壁纸，使用 KDE 的 HTML Wallpaper 设置对应的URL即可。因为本来我开发这个的初衷，就是想要一个好看的动态壁纸。部署好后Be Like:部署方法二：静态HTML部署相比于方法一，这个就不需要设置php环境了，能够减小系统性能开销。缺点就是，如果你想修改一些网页参数，会比较麻烦。更合适的做法是用在本地桌面背景，而不是展示了。web服务器部署当然，你愿意折腾的话，也不是不行，参照方法一配置nginx环境和证书即可，php就不需要安装了，配置方法这里就不再赘述，就是配置文件nekowebshow.conf要稍微改变一下：server { listen 443 ssl; server_name chocola.nekopara.uk vanilla.nekopara.uk azuki.nekopara.uk coconut.nekopara.uk fraise.nekopara.uk maple.nekopara.uk milk.nekopara.uk cinnamon.nekopara.uk show.nekopara.uk; # SSL certificate and key paths ssl_certificate /data/certs/_nekopara_uk.pem; ssl_certificate_key /data/certs/_nekopara_uk.key; # SSL settings (optional but recommended for security) ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; # Root directory of the website root /data/nekowebshow; index index.html index.php; # Logging #access_log /var/log/nginx/nekopara.uk.access.log; #error_log /var/log/nginx/nekopara.uk.error.log; location / { try_files $uri $uri/ =404; # 尝试提供请求的文件或目录，如果不存在则返回404 } # 处理错误页面 error_page 404 /404.html; location = /404.html { internal; } }要注意，克隆的时候，请克隆html_version分支：git clone --depth 1 --branch html_version https://github.com/Chocola-X/NekoWebShow当然，纯html的版本我已经在Github Pages部署了一份了，如果只是想要设置个动态壁纸不想部署的话，直接设置这个URL就好了。但是缺点是断网就寄。直接用Github Pages链接部署成桌面背景Be Like:本地文件部署如果你设置动态壁纸的软件支持直接读取本地文件且不报错的话，推荐采用这种方式。当然，本地打开html文件，firefox已经拦截跨文件夹请求了（找一个你适合存放这个壁纸的文件夹，壁纸体积大概2.9G，然后和上面一样克隆项目：git clone --depth 1 --branch html_version https://github.com/Chocola-X/NekoWebShow然后在HTML Wallpaper设置壁纸路径即可享用。虽然但是有点吃性能就是了（
浅尝开源项目：FreeMote这是一个比较冷门的开源项目，可以看作是E-Mote的开源实现。之前我还是用Windows做主力机使用的Nekopara动态壁纸就是这个项目实现的，而现在我已经切换到Linux作为主力系统使用一坤年了，是时候想想办法把我的巧克力弄到我的桌面上了。Linux想搞动态壁纸，主要就是两种：视频和网页。而FreeMote-SDK就有网页的WebGL实现。很早之前我就尝试过一次，但是那时候技术力不足，而且没学会查看文档，失败了。这次，既然我都能写文章发出来了，那肯定是取得了阶段性的进展了。相比于Live2D，E-Mote的应用就比较少了，主要是几个Galgame在用，其中就包括NEKOPARA。而绝大部分网站的动态二次元看板娘都是Live2D实现的，E-mote几乎看不见。所以文档少，案例少，基本上是摸著石头过河。我主要是研究网页端的FreeMote，下面，就开始探索吧。Demo分析这是项目Project-AZUSA给的网页Demo project-azusa.github.io 基本上可以在网页实现E-mote的功能。不过看了一下FreeMote-SDK，文档约等于没有，只能自己根据网页Demo进行推断了。看描述，说是需要Pure(unencrypted) PSB文件，当然，此PSB不是Photoshop的文件，而是这一套工具的文件格式。为了得到这个文件，则需要FreeMote的这些工具进行转换。接下来就是那个Demo网页用到的文件，我已经爬取下来了：. ├── data │ └── emote_test2.pure.emtbytes ├── driver │ ├── emoteplayer.js │ └── FreeMoteDriver.js ├── img │ └── bg.jpg ├── index.html └── main.js 4 directories, 6 files从文件命名就可以看出，emote_test2.pure.emtbytes是模型文件。但是其格式却不是psb？但是也有可能只是换了一下后缀名。搞清楚了大概的情况，关键点就在于模型文件的获取，以及尝试去替换另一个模型。虽然FreeMote-SDK没有什么文档，但是FreeMote项目的文档还是相对比较完善的，对于生成特定的模型文件具有比较高的参考价值。Demo模型转换测试开始之前，只需要下载FreeMote的工具集即可，在Release里面就有下载了。首先是先要分析现有的文件，按照文档的说法，我决定先尝试解包实例Demo的模型文件：[chocola@chocola-sa5212m4 data]$ wine ../../Ulysses-FreeMoteToolkit-v4.0.3/PsbDecompile.exe ./emote_test2.pure.emtbytes 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Decompiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Decompiling: emote_test2.pure Done. [chocola@chocola-sa5212m4 data]$ ls emote_test2.pure emote_test2.pure.emtbytes emote_test2.pure.json emote_test2.pure.resx.json [chocola@chocola-sa5212m4 data]$ tree . ├── emote_test2.pure │ └── tex-texture.png ├── emote_test2.pure.emtbytes ├── emote_test2.pure.json └── emote_test2.pure.resx.json 2 directories, 4 files [chocola@chocola-sa5212m4 data]$ 可以看到解包后有tex-texture.png纹理图片和emote_test2.pure.json设置文件，其中纹理图片和Live2D的纹理图片看起来很像。然后，再尝试合成回psb文件：[chocola@chocola-sa5212m4 data]$ wine ../../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe ./emote_test2.pure.json 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Compiling emote_test2.pure ... Compile emote_test2.pure done. Done. [chocola@chocola-sa5212m4 data]$ ls emote_test2.pure emote_test2.pure.emtbytes emote_test2.pure.json emote_test2.pure.pure.psb emote_test2.pure.resx.json可以看到，得到了emote_test2.pure.pure.psb文件，尝试去替换掉原本的emote_test2.pure.emtbytes文件，看看能不能正常工作。这里需要修改main.js文件，找到文件加载路径，将emote_test2.pure.emtbytes替换为emote_test2.pure.pure.psb就可以了： // load data then, register mouse event player.promiseLoadDataFromURL("./data/emote_test2.pure.pure.psb")然后我测试了一下，一切正常，说明整个工具链是可以用的，WebGL端确实是可以使用工具生成的pure.psb文件。尝试替换模型因为项目文档里面说有psb测试文件可以供使用，存放的仓库是FreeMote.Samples，于是我就去下了一个模型：[FlattenArray][PSBv4][win]dx_ふかみ_駅員服.psb，来进行尝试。因为这个模型文件名太长了不方便，我直接重命名为test.psb了，下面就是操作：[chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/PsbDecompile.exe test.psb 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Decompiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Decompiling: test Done. [chocola@chocola-sa5212m4 Ext_Asset]$ tree . ├── test │ └── tex-texture.png ├── test.json ├── test.psb └── test.resx.json 2 directories, 4 files [chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe test.json 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Compiling test ... Compile test done. Done. [chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/EmtConvert.exe test.pure.psb.lz4 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Converter by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Shell type: LZ4 Done. [chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/FreeMoteViewer.exe test.pure.psb.decompressed.psb 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote Viewer by Ulysses, wdwxy12345@gmail.com 最后一个命令，是查看生成的模型test.pure.psb.decompressed.psb是否正常，界面大概长这样：确定无误后，我满怀信心的将模型替换到WebGL端，结果。。。居然报错了？不过细看红色框出来的地方：motion file 'template' has not adaptive spec [win] of motion file. export psb again.似乎是因为模型属性不兼容，针对win这个关键字，我检索了两个模型的json文件。好兄弟，看出来区别了吗？正常运行的那个模型spec和Platform对应的值是ems而不能运行的对应的是win，所以。。。改一下？把win全换成ems。然后再重新转换一下：[chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe test.json 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Compiling test ... Compile test done. Done. [chocola@chocola-sa5212m4 Ext_Asset]$ wine ../Ulysses-FreeMoteToolkit-v4.0.3/EmtConvert.exe test.pure.psb.lz4 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Converter by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Shell type: LZ4 Done.不过，似乎改完之后颜色变奇怪了？然后发现原来的那个模型文件颜色也是奇怪的，那没事了，直接开始尝试！虽然动作有点奇怪，但是至少成功了！获取猫娘模型我先不打算解包NEKOPARA，怕东西太多了，打算先拿这个项目的作者自己搞得猫娘动态壁纸开刀。对，就是我之前提到的在Windows上面用的动态壁纸。看了一下那个动态壁纸的文件，应该是Unity3D做的，那只需要解包工具就好了，使用AssetStudio就可以完美解包了。资源的文件在NekoWallpaper_Data/data.unity3d。用AssetStudioGUI打开这个文件，在Asset List选项卡中，点一下Container标题，就可以按照存放的目录排序了，这样子比较好找模型。我们就拿牛奶来开刀吧，需要的文件是milk-winter和milk-winter-b_tex000.png，把这两个文件提取出来。其中没有后缀名称这个，个人推断是psb文件。然后对没有后缀名的文件进行转换：[chocola@chocola-sa5212m4 winter]$ wine ../../../Ulysses-FreeMoteToolkit-v4.0.3/PsbDecompile.exe milk-winter 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Decompiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Decompiling: milk-winter Done. [chocola@chocola-sa5212m4 winter]$ tree . ├── milk-winter ├── milk-winter-b_tex000.png ├── milk-winter.json ├── milk-winter-resources │ └── tex-texture.png └── milk-winter.resx.json 2 directories, 5 files可以看到多了一个纹理图片，但是我查看过后发现实际上是空图片，所以，我将提取出来的图片替换掉了解包出来的空图片。然后，合成psb:[chocola@chocola-sa5212m4 winter]$ wine ../../../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe milk-winter.json 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Compiling milk-winter ... Compile milk-winter done. Done.但是，有一部分的模型，解包出来是没有纹理的：[chocola@chocola-sa5212m4 pajama]$ tree . ├── vanilla-pajama ├── vanilla-pajama.json └── vanilla-pajama.resx.json 1 directory, 3 files这时候就需要自己添加纹理了，先将提取出来的纹理放到同一个目录下：[chocola@chocola-sa5212m4 pajama]$ tree . ├── vanilla-pajama ├── vanilla-pajama-a-ex2-type2_tex000.png ├── vanilla-pajama.json └── vanilla-pajama.resx.json 1 directory, 4 files在生成psb文件后关联纹理：[chocola@chocola-sa5212m4 pajama]$ wine ../../../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe vanilla-pajama.json 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Compiling vanilla-pajama ... Compile vanilla-pajama done. Done. [chocola@chocola-sa5212m4 pajama]$ wine ../../../Ulysses-FreeMoteToolkit-v4.0.3/PsBuild.exe link vanilla-pajama.pure.psb vanilla-pajama-a-ex2-type2_tex000.png 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:mscoree:parse_supported_runtime sku=L".NETFramework,Version=v4.8" not implemented 0024:fixme:ntdll:NtQuerySystemInformation info_class SYSTEM_PERFORMANCE_INFORMATION FreeMote PSB Compiler by Ulysses, wdwxy12345@gmail.com 18 Plugins Loaded. Link vanilla-pajama.pure done. Done. [chocola@chocola-sa5212m4 pajama]$ tree . ├── vanilla-pajama ├── vanilla-pajama-a-ex2-type2_tex000.png ├── vanilla-pajama.json ├── vanilla-pajama.pure.linked.psb ├── vanilla-pajama.pure.psb └── vanilla-pajama.resx.json 1 directory, 6 files其中，vanilla-pajama.pure.linked.psb就是最终文件了。处理完成后，可以看到，效果非常好：接下来，如前所述，改一下属性值，尝试放到网页上。真不错，下一步工作就是继续完善了，这个阶段性成果我已经很满意了！
二次开发 Typecho 后台服务器状态查看插件
发现Typecho后台服务器状态插件在翻Typecho插件的时候，我偶然发现了两双筷子写的Typecho后台服务器状态查看插件，于是就尝试了一下下。这个插件在桌面端网页没问题，感觉还挺不错的。但是一旦变成移动端，后台的布局就会出现问题，逼死强迫症。而且，CPU利用率有时候还会变成0.9999999999%这种离谱的数据，我寻思着就不能取两位小数好看一点吗？于是，我决定，自己修改代码，把服务器信息移动到页面底部的位置，并且修改显示数据的逻辑，让信息看着更加舒适一点。（其实也不完全是我写，是面向LLM编程 X）修改代码的过程首先，需要把注册方法的地方改为页面底部Typecho_Plugin::factory('admin/footer.php')->begin = array('LinuxInfo_Plugin', 'render');然后就是修改一下插入的样式：echo <<<STR $info_str STR;因为我觉的系统信息用鼠标悬浮预览不明显，于是修改为直接独立一行把信息全部展示：return <<<SYS_INFO 服务器信息: 持续运行：{$uptime}，Web服务器：{$_SERVER['SERVER_SOFTWARE']}，PHP 版本：{$php['version']}，SAPI 接口：{$php['sapi']}，PHP内存限制：{$php['mem_max']}，上传文件限制：{$php['up_max']} SYS_INFO;以及让LLM帮忙完善了CPU信息获取的逻辑：private static function cpu_info(){ function GetCoreInformation() { $data = file('/proc/stat'); $cores = []; foreach( $data as $line ) { if( preg_match('/^cpu[0-9]/', $line) ) { $info = explode(' ', $line); $cores[] = [ 'user' => $info[1], 'nice' => $info[2], 'sys' => $info[3], 'idle' => $info[4], 'iowait' => $info[5], 'irq' => $info[6], 'softirq' => $info[7] ]; } } return $cores; } function GetCpuPercentages($stat1, $stat2) { if(count($stat1) !== count($stat2)){ return; } $cpus = []; for( $i = 0, $l = count($stat1); $i < $l; $i++) { $dif = []; $dif['user'] = $stat2[$i]['user'] - $stat1[$i]['user']; $dif['nice'] = $stat2[$i]['nice'] - $stat1[$i]['nice']; $dif['sys'] = $stat2[$i]['sys'] - $stat1[$i]['sys']; $dif['idle'] = $stat2[$i]['idle'] - $stat1[$i]['idle']; $dif['iowait'] = $stat2[$i]['iowait'] - $stat1[$i]['iowait']; $dif['irq'] = $stat2[$i]['irq'] - $stat1[$i]['irq']; $dif['softirq'] = $stat2[$i]['softirq'] - $stat1[$i]['softirq']; $total = array_sum($dif); $cpu = []; foreach($dif as $x=>$y) $cpu[$x] = round($y / $total * 100, 2); $cpus['CPU' . $i] = $cpu; } return $cpus; } $stat1 = GetCoreInformation(); sleep(1); $stat2 = GetCoreInformation(); $data = GetCpuPercentages($stat1, $stat2); $overall_cpu_usage = 0; $core_details = []; foreach ($data as $core => $usage) { $core_usage = 100 - $usage['idle']; $overall_cpu_usage += $core_usage; $core_details[] = "{$core}: " . number_format($core_usage, 2) . "%"; } $overall_cpu_usage = round($overall_cpu_usage / count($data), 2); $core_details_str = implode(", ", $core_details); $sys_load = sys_getloadavg(); $sys_load_1min = number_format($sys_load[0], 3); $sys_load_5min = number_format($sys_load[1], 3); $sys_load_15min = number_format($sys_load[2], 3); return <<<CPU_INFO CPU: {$overall_cpu_usage} % CPU_INFO; }原来的逻辑只是判断单个CPU的占有率，当CPU数量为多个的时候就会不准确，这里我优化成了整体利用率，并且悬停时会显示每个CPU的占用率信息。改造效果虽然不是彻底重构，但是我对改造后的插件还是比较满意的。手机的网页端也舒服了。修改后的插件分享修改后的插件已经放到了GitHub：Typecho-LinuxInfo反正源码也不多，我这里也可以直接放出来：Plugin.php:<?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; /** * 查看系统信息，仅限 Linux 系统，基于两双筷子的插件进行二次开发，使用帮助 * * @package LinuxInfo * @author GTX690战术核显卡导弹 * @version 1.1.0 * @link https://www.nekopara.uk */ class LinuxInfo_Plugin implements Typecho_Plugin_Interface { // 插件名称 protected static $plugin_name = 'LinuxInfo'; /** * 激活插件方法,如果激活失败,直接抛出异常 * * @access public * @return void * @throws Typecho_Plugin_Exception */ public static function activate() { // 如果当前系统不是 linux 则抛出错误 if(PHP_OS != 'Linux') { throw new Typecho_Plugin_Exception(_t("此插件仅支持 Linux 系统")); } // 在后端页面底部 注册方法 Typecho_Plugin::factory('admin/footer.php')->begin = array('LinuxInfo_Plugin', 'render'); } /** * 禁用插件方法,如果禁用失败,直接抛出异常 * * @static * @access public * @return void * @throws Typecho_Plugin_Exception */ public static function deactivate(){} /** * 获取插件配置面板 * * @access public * @param Typecho_Widget_Helper_Form $form 配置面板 * @return void */ public static function config(Typecho_Widget_Helper_Form $form) { /** 分类名称 */ $show_item = array( 'cpu_info' => _t('CPU使用情况') . '（ CPU 使用率、系统负载）', 'mem_info' => _t('内存使用情况') . '（内存使用率、SWAP交换分区）', 'disk_info' => _t('磁盘使用情况') . '（磁盘使用率、磁盘已用空间、磁盘总空间）', 'sys_info' => _t('服务器信息') . ' （系统运行时间、服务器软件、PHP 版本、SAPI 接口、PHP 内存限制、上传文件限制）', ); // 设置默认选中 $show_option_value = ['cpu_info','mem_info','disk_info']; $ShowInfo = new Typecho_Widget_Helper_Form_Element_Checkbox('show_items',$show_item, $show_option_value, _t('选择显示系统信息')); $form->addInput($ShowInfo->multiMode()); $separator = new Typecho_Widget_Helper_Form_Element_Text('separator', NULL, ' ★ ', _t('分隔符'), '设置数据项分割字符'); $form->addInput($separator); } /** * 个人用户的配置面板 * * @access public * @param Typecho_Widget_Helper_Form $form * @return void */ public static function personalConfig(Typecho_Widget_Helper_Form $form){} /** * 插件实现方法 * * @access public * @return void */ public static function render() { $show_items = Typecho_Widget::widget('Widget_Options')->plugin(self::$plugin_name)->show_items; $separator = Typecho_Widget::widget('Widget_Options')->plugin(self::$plugin_name)->separator; // 获取 linux 系统消息 $info_str = ''; $show_items_max = count($show_items) - 1; foreach ($show_items as $show_item_k => $show_item) { $method = $show_item; if (method_exists(__CLASS__, $method)) { $info_str .= self::$method(); } if ($show_items_max != $show_item_k) { $info_str .= $separator; } } echo <<<STR $info_str STR; } // 获取CPU信息 private static function cpu_info(){ function GetCoreInformation() { $data = file('/proc/stat'); $cores = []; foreach( $data as $line ) { if( preg_match('/^cpu[0-9]/', $line) ) { $info = explode(' ', $line); $cores[] = [ 'user' => $info[1], 'nice' => $info[2], 'sys' => $info[3], 'idle' => $info[4], 'iowait' => $info[5], 'irq' => $info[6], 'softirq' => $info[7] ]; } } return $cores; } function GetCpuPercentages($stat1, $stat2) { if(count($stat1) !== count($stat2)){ return; } $cpus = []; for( $i = 0, $l = count($stat1); $i < $l; $i++) { $dif = []; $dif['user'] = $stat2[$i]['user'] - $stat1[$i]['user']; $dif['nice'] = $stat2[$i]['nice'] - $stat1[$i]['nice']; $dif['sys'] = $stat2[$i]['sys'] - $stat1[$i]['sys']; $dif['idle'] = $stat2[$i]['idle'] - $stat1[$i]['idle']; $dif['iowait'] = $stat2[$i]['iowait'] - $stat1[$i]['iowait']; $dif['irq'] = $stat2[$i]['irq'] - $stat1[$i]['irq']; $dif['softirq'] = $stat2[$i]['softirq'] - $stat1[$i]['softirq']; $total = array_sum($dif); $cpu = []; foreach($dif as $x=>$y) $cpu[$x] = round($y / $total * 100, 2); $cpus['CPU' . $i] = $cpu; } return $cpus; } $stat1 = GetCoreInformation(); sleep(1); $stat2 = GetCoreInformation(); $data = GetCpuPercentages($stat1, $stat2); $overall_cpu_usage = 0; $core_details = []; foreach ($data as $core => $usage) { $core_usage = 100 - $usage['idle']; $overall_cpu_usage += $core_usage; $core_details[] = "{$core}: " . number_format($core_usage, 2) . "%"; } $overall_cpu_usage = round($overall_cpu_usage / count($data), 2); $core_details_str = implode(", ", $core_details); $sys_load = sys_getloadavg(); $sys_load_1min = number_format($sys_load[0], 3); $sys_load_5min = number_format($sys_load[1], 3); $sys_load_15min = number_format($sys_load[2], 3); return <<<CPU_INFO CPU: {$overall_cpu_usage} % CPU_INFO; } // 获取内存信息 private static function mem_info(){ if (false === ($str = @file("/proc/meminfo"))) return false; $str = implode("", $str); preg_match_all("/MemTotal\s{0,}\:+\s{0,}([\d\.]+).+?MemFree\s{0,}\:+\s{0,}([\d\.]+).+?Cached\s{0,}\:+\s{0,}([\d\.]+).+?SwapTotal\s{0,}\:+\s{0,}([\d\.]+).+?SwapFree\s{0,}\:+\s{0,}([\d\.]+)/s", $str, $buf); preg_match_all("/Buffers\s{0,}\:+\s{0,}([\d\.]+)/s", $str, $buffers); $mem_info['memTotal'] = round($buf[1][0]/1024, 2); $mem_info['memFree'] = round($buf[2][0]/1024, 2); $mem_info['memBuffers'] = round($buffers[1][0]/1024, 2); $mem_info['memCached'] = round($buf[3][0]/1024, 2); $mem_info['memUsed'] = $mem_info['memTotal']-$mem_info['memFree']; $mem_info['memPercent'] = (floatval($mem_info['memTotal'])!=0)?round($mem_info['memUsed']/$mem_info['memTotal']*100,2):0; $mem_info['memRealUsed'] = $mem_info['memTotal'] - $mem_info['memFree'] - $mem_info['memCached'] - $mem_info['memBuffers']; //真实内存使用 $mem_info['memRealFree'] = $mem_info['memTotal'] - $mem_info['memRealUsed']; //真实空闲 $mem_info['memRealPercent'] = (floatval($mem_info['memTotal'])!=0)?round($mem_info['memRealUsed']/$mem_info['memTotal']*100,2):0; //真实内存使用率 $mem_info['memCachedPercent'] = (floatval($mem_info['memCached'])!=0)?round($mem_info['memCached']/$mem_info['memTotal']*100,2):0; //Cached内存使用率 $mem_info['swapTotal'] = round($buf[4][0]/1024, 2); $mem_info['swapFree'] = round($buf[5][0]/1024, 2); $mem_info['swapUsed'] = round($mem_info['swapTotal']-$mem_info['swapFree'], 2); $mem_info['swapPercent'] = (floatval($mem_info['swapTotal'])!=0)?round($mem_info['swapUsed']/$mem_info['swapTotal']*100,2):0; $mem_per = round($mem_info['memRealUsed'] / $mem_info['memTotal'] * 100, 2); return <<<MEM_INFO 内存: {$mem_per} % MEM_INFO; } // 获取磁盘信息 private static function disk_info(){ $dt = round(@disk_total_space(".")/(1024*1024*1024), 2); //总 $df = round(@disk_free_space(".")/(1024*1024*1024), 2); //可用 $du = $dt-$df; //已用 $disk_use = round($du / $dt * 100, 2); return <<<DISK_INFO 硬盘: {$disk_use} % DISK_INFO; } // 获取 系统信息 private static function sys_info() { if (false === ($str = @file("/proc/uptime"))) return false; $str = explode(" ", implode("", $str)); $str = trim($str[0]); $min = $str / 60; $hours = $min / 60; $days = floor($hours / 24); $hours = floor($hours - ($days * 24)); $min = floor($min - ($days * 60 * 24) - ($hours * 60)); if ($days !== 0) $uptime = $days."天"; if ($hours !== 0) $uptime .= $hours."小时"; $uptime .= $min."分钟"; $php['version'] = PHP_VERSION; $php['sapi'] = php_sapi_name(); $php['mem_max'] = get_cfg_var('memory_limit'); $php['up_max'] = get_cfg_var('upload_max_filesize'); return <<<SYS_INFO 服务器信息: 持续运行：{$uptime}，Web服务器：{$_SERVER['SERVER_SOFTWARE']}，PHP 版本：{$php['version']}，SAPI 接口：{$php['sapi']}，PHP内存限制：{$php['mem_max']}，上传文件限制：{$php['up_max']} SYS_INFO; } }优先使用Github上面的吧，万一那天我修复一些问题，可能只会修改Github上面的仓库的内容而忘记修改这篇文章。当然，我希望以后不会出现问题。使用方法将GitHub上面的Plugin.php或者这个页面的Plugin.php内容保存下载，放到Typecho的插件目录usr/plugins/下的LinuxInfo文件夹（需要自己创建），例如：typecho/usr/plugins/LinuxInfo/Plugin.php，进入Typecho后台启用插件即可。默认不显示系统详细信息，需要自己勾选显示才会展示。不过，需要注意的是，这个插件仅可用于Linux系统，Windows系统不适用。谁用Windows做服务器啊？！！不会吧不会吧？最后还是要感谢一下两双筷子写出来这个有意思的插件。
浪潮SA5212M4风扇调速Python脚本写脚本的原因之前每次开机都要进BMC把风扇转速调低，不然吵得要死。但是又不能调得太低，因为如果调的太低怕一不留神温度就飙上去了，导致硬件出问题。正好前几天看到了一个别人用Python写的简易风扇一键调节脚本，但是只能设置固定转速，没办法根据温度动态调节。于是我重构了这个脚本，做到了根据温度动态调节风扇转速。环境要求系统要求：Linux（Windows不可用，因为没有sensors软件包）软件环境：Python3环境，sensors软件包（用于获取CPU温度）Python源代码import subprocess import re import http.client import time # 全局变量用于存储会话连接和令牌 conn = None session_cookie = None def login(IP, User, Password): global conn, session_cookie if conn is not None: conn.close() # 关闭旧连接 conn = http.client.HTTPConnection(IP) payload = "WEBVAR_USERNAME=" + User + "&WEBVAR_PASSWORD=" + Password headers = { 'Content-Type': "application/x-www-form-urlencoded", 'Origin': "http://{}".format(IP), 'Referer': "http://{}/index.html".format(IP), 'User-Agent': "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0", } conn.request("POST", "/rpc/WEBSES/create.asp", payload, headers) response = conn.getresponse().read().decode("utf-8") match = re.search(r"'SESSION_COOKIE' : '([^']*)'", response) if match: session_cookie = match.group(1) else: raise ValueError("SESSION_COOKIE not found in the string") def set_fan_speed(IP, fan_speeds): global conn, session_cookie # 设置风扇为手动模式 payload1 = "MODE=1" headers1 = { 'Content-Type': "application/x-www-form-urlencoded", 'Cookie': "BMC_IP_ADDR={}; SessionCookie=".format(IP) + session_cookie, } conn.request("POST", "/rpc/setfanmode.asp", payload1, headers1) conn.getresponse() # 调整风扇转速 headers2 = { 'Content-Type': "application/x-www-form-urlencoded", 'Cookie': "SessionCookie=" + session_cookie + "; BMC_IP_ADDR={}".format(IP), } for fan_ID, Speed_x in fan_speeds.items(): payload2 = "ID=" + fan_ID + "&PERCENT=" + Speed_x conn.request("POST", "/rpc/setfanspeed.asp", payload2, headers2) conn.getresponse() print("调整风扇{}转速{}%".format(fan_ID, Speed_x)) def get_package_temperatures(): output = subprocess.check_output(['sensors']).decode('utf-8') pattern = re.compile( r'^Package id (\d+):\s+\+([\d.]+)\.?\s*°C', re.MULTILINE ) matches = pattern.findall(output) result = {f"Package id {package_id}": int(float(temp_str)) for package_id, temp_str in matches} return result def compute_fan_speed(temp): if temp <= 45: return 10 elif temp >= 70: return 100 else: return int(10 + (temp - 45) * (90 / (70 - 45))) last_temp = None if __name__ == "__main__": IP = "192.168.0.6" #服务器BMC内网IP地址 User = "admin" #服务器BMC管理员账号 Password = "admin" #服务器BMC管理员密码 # 首次登录 login(IP, User, Password) while True: try: temperatures = get_package_temperatures() max_temp = max(temperatures.values()) if last_temp is None or abs(max_temp - last_temp) >= 2: fan_speed = str(compute_fan_speed(max_temp)) set_fan_speed(IP, {"0": fan_speed, "2": str(compute_fan_speed(max_temp)+3), "4": fan_speed, "6": fan_speed}) last_temp = max_temp print(temperatures, max_temp) else: continue #print(f"温度变化不足2度 ({last_temp}℃ -> {max_temp}℃)，跳过风扇转速调整.") except Exception as e: print(f"遇到错误: {e}") # 发生错误时重新登录 login(IP, User, Password) time.sleep(2)代码说明风扇转速的策略是，45度以下风扇转速10%，超过45度开始线性增加，70度时风扇转速100%。可以根据需要修改compute_fan_speed函数的逻辑。02号风扇默认比其他风扇转速高3%，因为这个风扇对应的是CPU1，为了安装显卡换了散热器，并且阻挡较多温度较高，所以风扇转速设置偏高一点平衡两边温度。如果不需要安装显卡可以修改set_fan_speed(IP, {"0": fan_speed, "2": str(compute_fan_speed(max_temp)+3), "4": fan_speed, "6": fan_speed})这部分，风扇速度传入的是一个字典，键值对是风扇ID:转速百分比。如果你的服务器是其他有八个风扇的型号（例如SA5112M4），那只需要吧传入的字典部分改成八个对应的键值对即可。希望这个风扇调节脚本可以帮到你。