在并夕夕购入猫娘乐园巧克力手办偶然的机会，刷并夕夕的时候，我发现了一个无盒的巧克力手办，原价200多，我看到的时候，只需要90。说是微瑕，但是保证是正版的手办，我当时买的时候大意了，没细看商品图，啪一下就下单了。结果到货后，发现少了头发一边的丝带。。。《微瑕》，嗯，《微瑕》（不过除此之外，好像真没什么问题，整体挺好的，确实是正品的手办，细节没什么大问题。果然，价格腰斩是有原因的（贴几张图吧！
在Manjaro Linux上部署Typecho开发环境因为后面主题的开发就需要实时修改php文件查看效果了，为了方便开发，我决定在自己的电脑上面设置一个基本的Nginx+PHP环境。对于数据库就使用sqlite好了，只是为了搭建一个本地的typecho作为测试平台。在配置期间踩了几个坑，记录一下。软件的安装非常简单，一条命令梭哈：pacman -S nginx php php-fpm php-sqlite php-gd配置软件因为manjaro和其他发行版不太一样，并且我需要这个web服务器以我的用户权限进行运行，方便我更新文件，所以我们需要做出一些更改。php.ini首先是php的配置文件，文件位于/etc/php/php.ini找到下面的部分进修改：上传大小限制：upload_max_filesize = 2M改为upload_max_filesize = 16M然后启用必要的模块，找到以下部分取消注释：extension=curl extension=gd extension=gettext extension=pdo_sqlitewww.conf然后是php的另一个配置文件，让php-fpm以用户身份运行，文件位于/etc/php/php-fpm.d/www.conf找到下面的部分进修改：user = chocola ; <--- 修改这里 group = chocola ; <--- 修改这里 listen.owner = chocola ; <--- 修改这里，确保 Nginx 能和 PHP 通讯 listen.group = chocola ; <--- 修改这里nginx.conf将Nginx的配置文件修改为如下所示： user chocola; worker_processes 1; #error_log logs/error.log; #error_log logs/error.log notice; #error_log logs/error.log info; #pid logs/nginx.pid; # Load all installed modules include modules.d/*.conf; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' # '$status $body_bytes_sent "$http_referer" ' # '"$http_user_agent" "$http_x_forwarded_for"'; #access_log logs/access.log main; sendfile on; #tcp_nopush on; #keepalive_timeout 0; keepalive_timeout 65; gzip on; server { listen 80; server_name localhost; #charset koi8-r; #access_log logs/host.access.log main; root /run/media/chocola/DATA/wwwroot; index index.php index.html index.htm; #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # location ~ .*\.php(\/.*)*$ { # 你的网站根目录 (如果 server 块里已经写了 root，这行可以省略，但加上保险) root /run/media/chocola/DATA/wwwroot; fastcgi_pass unix:/run/php-fpm/php-fpm.sock; fastcgi_index index.php; # 修改点 2：开启路径分割，把 /index.php/action/login 分成两部分 fastcgi_split_path_info ^(.+?.php)(/.*)$; # 修改点 3：重新定义 SCRIPT_FILENAME，使用分割后的变量 fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; # 修改点 4：这一行是 Typecho 后台必须的！告诉 PHP 具体的路径信息 fastcgi_param PATH_INFO $fastcgi_path_info; include fastcgi_params; } # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #} } # another virtual host using mix of IP-, name-, and port-based configuration # #server { # listen 8000; # listen somename:8080; # server_name somename alias another.alias; # location / { # root html; # index index.html index.htm; # } #} # HTTPS server # #server { # listen 443 ssl; # server_name localhost; # ssl_certificate cert.pem; # ssl_certificate_key cert.key; # ssl_session_cache shared:SSL:1m; # ssl_session_timeout 5m; # ssl_ciphers HIGH:!aNULL:!MD5; # ssl_prefer_server_ciphers on; # location / { # root html; # index index.html index.htm; # } #} }主要是让nginx以用户的身份进行运行，并且修改php部分的处理逻辑，以符合Typecho的要求。设置完成后，就可以把Typecho的安装包丢到目标目录进行安装了！
主题开发日志02：前端界面开发基本完成
博客主题前端界面开发工作基本完成经过一个多星期与Gemini 3 Pro的反复拉扯，终于是把主要的三个部分样式做完了：首页+文章列表+独立页面，文章阅读页面，后台管理页面。感觉还是相当不错的！因为还没开发完成，我这里只放截图。文章首页效果对，没错，我将会把动态立绘放在首页！不过因为立绘很吃性能，所以我特地做了优化，当不在首页浏览时，立绘渲染会暂停节省性能。文章列表效果独立页面效果文章页面效果细心的同学应该已经发现了，对话框是贯穿始终的，而且还是nekopara的风格。没错！对话框将会是主题的一大特色，作用相当于网站的导航栏，同时也会在特定的按钮和链接给出相关的提示，让你感觉像是在玩galgame!后台效果后台页面就完全按照nekopara的设置界面风格来制作了。目前主题的前端开发工作做的差不多了，接下来就是怎么缝合到typecho上面了。
给NekoWebShow加载提速为期三个月的实习结束了，准备迎来寒假了，我在想要不要做一些有意思的事情。受到Bhao小姐姐博客的启发，我萌生了想要重新装修博客的想法，主题也很明确——猫娘乐园主题。但是，如果还是像我现在这样，用别人的主题改一改，个性化一下，或许还是太没意思了。于是乎，我就想看看能不能自己从零开始做一个主题。真正驱动我做这件事的，是半年前我搞出来了那个NekoWebShow项目，我希望把巧克力的动态立绘直接放在博客首页，只有首页有这种持续的动画效果，其他页面就是静态展示，这样子的话就不会像现在那样打开我的网站后一直占用性能。我希望可以做到只在首页占用一些性能展示动态立绘，而往下浏览查看其他内容的时候，就没有活动的元素了。一方面可以凸显重点，保持整套主题风格的连贯性，另一方面节省访客电脑的性能。当时还是有点犹豫要不要搞，在Bhao小姐姐的群里问了一下，她坚定地回了一个字——“做！”她随后说其实用框架的话就和搭积木差不多，不是很难，于是我下定决心，开干。而今天完成的工作则算是这个主题正式开工之前的前置工作——优化NekoWebShow加载速度为什么需要优化尽管目前的猫娘展示网页运行正常，但是由于模型文件较大，达到了32MB或者64MB，会导致较长的加载时间，如果网速慢那更是雪上加霜。对于纯粹的猫娘展示网页还是可以接受，但是对于个人博客那就是致命的——访客可能会嫌打开太久离开。所以说，这个是必须要优化的。还有一点是，虽然这些专有格式体积庞大，但是转换前的文件却不算大——3-4MB左右的png纹理文件+3MB左右的json定义文件，很明显应该是有办法可以压缩下来的。优化传输的探索——碰壁asm.js一开始，我是希望逆向JS，顺着模型文件的传输路径去找到对应的处理代码，然后求助于AI解决。但是，可悲的是，我千辛万苦挖了半个小时，结果挖出来一段奇怪的js代码，例如：// EMSCRIPTEN_END_FUNCS var vd=[uB,Be,Ke,Fp,Iz,Pz,fA,uB];var wd=[vB,$e,df,ff,hf,eg,jg,lg];var xd=[wB,re,te,Ee,Fe,Qe,Re,We,Xe,Lf,bg,cg,dg,gg,wv,xg,Ji,Gp,yg,zg,Ne,Ag,Iu,Cg,Ki,Dg,Np,Hg,Qp,Rp,Li,an,bn,un,vn,Ln,Mn,Sn,Tn,bo,co,jo,ko,Co,Do,Xo,Yo,lp,mp,vp,wp,hz,Dp,Ep,Op,Ju,xv,Ew,Fw,Cz,Dz,Ez,Fz,Nz,Xz,Yz,$z,bA,dA,ly,Tz,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB,wB];var yd=[xB,ch];var zd=[yB,we,ye,Ce,Ge,He,Ie,Ve,Ze,bf,uf,Xf,hg,ng,zv,Ip];var Ad=[zB,Xg,vi,zB];var Bd=[AB,ue,ve,vg,xe,ze,De,Se,Te,wg,Ue,Ye,_e,cf,rf,vf,xf,Mf,Of,Qf,ag,fg,ig,og,pg,rg,Hp,gz,gy,Zz,sh,th,uh,Qg,oh,qh,Jh,Ch,Dh,Fh,Eh,xi,ci,zi,yh,xh,Bh,Bi,zh,_h,$h,wh,AB,AB,AB,AB,AB,AB,AB,AB,AB,AB,AB,AB];var Cd=[BB,jh];var Dd=[CB,Yf];var Ed=[DB,hi];var Fd=[EB,qf,_f,$f];var Gd=[FB,gi];var Hd=[GB,Gf];var Id=[HB,mf,of,sf,If,HB,HB,HB];var Jd=[IB,bh];var Kd=[JB,fh,wi,JB];var Ld=[KB,ih];var Md=[LB,Af,Eg,Mi,hy,iy,ny,Ny,Gz,aA,cA,Ng,Rg,Yg,Zg,_g,di,ah,Wh,LB,LB,LB,LB,LB,LB,LB,LB,LB,LB,LB,LB,LB];var Nd=[MB,ui];var Od=[NB,Wg,li,NB];var Pd=[OB,Hz,Oz,eA];var Qd=[PB,Hh,Ih,PB];var Rd=[QB,Je,lf,Vf,Bg,Pp,QB,QB];var Sd=[RB,Kf];var Td=[SB,af,ef,gf,jf,nf,pf,tf,kg,mg,qg,Jf,Gi,Hi,Ig,Sp,Kh,SB,SB,SB,SB,SB,SB,SB,SB,SB,SB,SB,SB,SB,SB,SB];var Ud=[TB,kf];var Vd=[UB,Ci,ji,ni,fi,ki,ti,ri];var Wd=[VB,Cf];var Xd=[WB,gh];var Yd=[XB,yi];var Zd=[YB,pi,oi,ii,mi,YB,YB,YB];var _d=[ZB,yf,zf,Nf,Pf,Rf,Sf,Tf,Wf,sg,yv,Fg,Ni,mh,Og,Pg,nh,Zh,Gh,ei,Oh,Nh,ai,Ai,bi,Yh,Ah,Qh,Ph,kh,Mh,qi,si,qr,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB,ZB];var $d=[_B,Jp,Sg,_B];var ae=[$B,Bf,Ff,Vg,Xh,Vh,$B,$B];var be=[aC,Ef];var ce=[bC,wf,Df,Uf,Zf,tg,Rh,Th,Sh,Uh,lh,Lh,bC,bC,bC,bC];var de=[cC,Ug];var ee=[dC,rh,ph,dC];var fe=[eC,Tg];var ge=[fC,vh,$g,dh];var he=[gC,eh,hh,gC];var ie=[hC,iC,zz,Yq,Sz,hC,hC,hC];var je=[jC,Ae,Le,Hf,Jz,Qz,gA,jC];return{_EmotePlayer_Update:Ci,_EmotePlayer_SetStereovisionVolume:ti,_EmotePlayer_FadeInTimeline:Hh,_EmotePlayer_GetVariableLabelAt:Yh,_bitshift64Lshr:sA,_EmotePlayer_SetColor:gi,_EmotePlayer_GetMainTimelineLabelAt:Oh,_EmotePlayer_SetOuterForce:li,_EmotePlayer_GetState:Rh,_EmotePlayer_StopTimeline:Ai,_EmoteDevice_DeleteEmoteTexture2:oh,_memcpy:tA,_EmotePlayer_SetMeshDivisionRatio:ki,_EmotePlayer_SetVariable:vi,_EmotePlayer_Initialize:Zh,_EmotePlayer_CountPlayingTimelines:zh,___udivmoddi4:xA,_EmotePlayer_Pass:ci,___cxa_can_catch:mA,_EmotePlayer_GetCharaProfile:Lh,_EmotePlayer_SetVariableDiff:wi,_free:hz,_EmoteDevice_Initialize:rh,_main:Di,_EmotePlayer_SetStereovisionRenderScreen:si,_EmotePlayer_GetCharaHeight:Kh,_EmotePlayer_IsCharaProfileAvailable:$h,_EmotePlayer_SetOuterRot:mi,___cxa_is_pointer_type:nA,_llvm_cttz_i32:wA,_EmotePlayer_GetDiffTimelineLabelAt:Nh,_EmotePlayer_IsTimelinePlaying:bi,_EmotePlayer_Step:zi,_EmotePlayer_SetGrayscale:ii,_EmotePlayer_GetVariableFrameLabelAt:Wh,_EmotePlayer_SetTimelineBlendRatio:ui,_EmoteDevice_CreateEmoteTexture2:nh,_EmotePlayer_SetScale:pi,_EmoteDevice_ChangeFrameBufferSize:mh,_EmotePlayer_DrawToTexture:Fh,_EmotePlayer_CountDiffTimelines:xh,_EmotePlayer_PlayTimeline:di,_EmotePlayer_GetVariableDiff:Vh,_EmotePlayer_SetBustScale:fi,_roundf:rA,_EmotePlayer_CountVariableFrameAt:Ah,_EmotePlayer_SetRot:oi,_EmotePlayer_GetTimelineBlendRatio:Sh,_EmotePlayer_IsAnimating:_h,_EmotePlayer_SetStereovisionEnabled:qi,_EmotePlayer_Draw:Dh,_sbrk:uA,_memset:zA,_EmotePlayer_Finish:Jh,_EmotePlayer_FadeOutTimeline:Ih,_EmotePlayer_DrawToScreen:Eh,_i64Subtract:pA,_EmotePlayer_Skip:xi,_EmotePlayer_CountMainTimelines:yh,___muldsi3:BA,_EmotePlayer_GetCharaProfileLabelAt:Mh,_EmotePlayer_GetPlayingTimelineFlagsAt:Ph,_EmotePlayer_SetAsOriginalScale:ei,_malloc:gz,_EmotePlayer_GetPlayingTimelineLabelAt:Qh,_EmotePlayer_StopWind:Bi,_EmoteDevice_SetMaskRegionClipping:th,_EmotePlayer_AttachRenderTexture:vh,_EmotePlayer_DrawToTexture2:Gh,___udivdi3:AA,_EmotePlayer_IsLoopTimeline:ai,_bitshift64Shl:DA,_EmotePlayer_GetVariableFrameValueAt:Xh,_EmotePlayer_SetHairScale:ji,_EmotePlayer_StartWind:yi,_EmotePlayer_SetStereovisionParallaxRatio:ri,___muldi3:CA,_EmotePlayer_CountVariables:Bh,___uremdi3:yA,_EmotePlayer_SetPartsScale:ni,_i64Add:qA,_pthread_self:EA,_EmoteDevice_SetProtectTranslucentTextureColor:uh,___getTypeName:fy,___errno_location:ky,_EmotePlayer_SetCoord:hi,_EmotePlayer_GetVariable:Uh,_memmove:vA,_EmotePlayer_CountCharaProfiles:wh,_EmoteDevice_Finish:ph,_EmoteDevice_GetEmoteTexture2TexId:qh,_EmotePlayer_DetachRenderTexture:Ch,_EmoteDevice_SetMaskMode:sh,_EmotePlayer_GetTimelineTotalFrameCount:Th,__GLOBAL__sub_I_emsbind_cpp:Lg,__GLOBAL__sub_I_bind_cpp:dy,runPostSets:oA,stackAlloc:ke,stackSave:le,stackRestore:me,establishStackSpace:ne,setThrew:oe,setTempRet0:pe,getTempRet0:qe,dynCall_viiiii:FA,dynCall_vid:GA,dynCall_vi:HA,dynCall_iiiidd:IA,dynCall_vii:JA,dynCall_iiiddd:KA,dynCall_ii:LA,dynCall_iiiddddd:MA,dynCall_viidddi:NA,dynCall_iidddd:OA,dynCall_viidd:PA,dynCall_iidddddd:QA,dynCall_viidddd:RA,dynCall_viddd:SA,dynCall_iiiidddi:TA,dynCall_iiiiddd:UA,dynCall_iiiidddd:VA,dynCall_iiii:WA,dynCall_iiidddi:XA,dynCall_iiidddd:YA,dynCall_viiiiii:ZA,dynCall_iiidd:_A,dynCall_viii:$A,dynCall_viddddd:aB,dynCall_di:bB,dynCall_vidddd:cB,dynCall_iid:dB,dynCall_viiddd:eB,dynCall_iiiiiddd:fB,dynCall_iiddddd:gB,dynCall_iiddd:hB,dynCall_iii:iB,dynCall_iiiiii:jB,dynCall_diii:kB,dynCall_viiiddd:lB,dynCall_dii:mB,dynCall_iiidddddd:nB,dynCall_i:oB,dynCall_iiid:pB,dynCall_iiiii:qB,dynCall_diiii:rB,dynCall_v:sB,dynCall_viiii:tB}})我寻思着是谁能写出来这么抽象的js代码，后面经过和LLM近一个多小时的拉扯和了解，得知这个js是用C或者C++通过Emscriptenn编译而来，属于asm.js，或更准确地说，是Emscripten生成的胶水代码。需要修改实现新功能，难度不亚于汇编，因为整个代码高度耦合，是在用js去模拟C的运行环境。并且多次好心劝退我不要尝试去修改这部分的代码，即使这些代码确实都是合规的JS代码。我第一次意识到，有些JS代码虽然语法合法，却因高度混淆和底层模拟，几乎无法安全修改或逆向理解。然后也意识到修改功能实现这个是死胡同了，只能另寻他法。优化传输的探索——轮子套轮子既然已经知道这段抽象的js代码不好惹，那就曲线救国。因为我发现这些模型文件，是经过处理后变大的，而且无论原本的png大小是多少，只要是4096x2048的纹理，处理后就是32MB，只要是4096x4096的纹理，处理后就是64MB。我就意识到这个文件里面肯定包含了很多空数据，应该使用压缩工具可以很好压缩。实验后事实的确如此——压缩后的.zip大概是3-4MB左右的大小，甚至2MB。于是，我的思路就转换过来了：服务器发送压缩后的文件，浏览器前端解压，然后把解压后的文件传给后续的js处理，这样就不需要动那一大堆如同狗屎的js了。为了解决这个问题，我打算引入fflate.js。fflate.js是一个js库，可以支持在前端的浏览器内进行压缩文件解压的操作，这个库也是LLM给我推荐的，后面试了一下确实很不错。首先是把js下载回来，并在html中调用： 接下来是修改代码逻辑： //增加前端解压zip功能，减少数据传输量 try { // 1. Fetch the ZIP file const resp = await fetch(zipUrl); if (!resp.ok) throw new Error(`Failed to load ${zipUrl}`); const zipData = new Uint8Array(await resp.arrayBuffer()); // 2. Decompress ZIP using fflate const files = await new Promise((resolve, reject) => { fflate.unzip(zipData, (err, unzipped) => { if (err) { console.error("ZIP decompression error:", err); reject(err); } else { resolve(unzipped); } }); }); // 3. Find the .psb model file (assume only one .psb file) const binFileName = Object.keys(files).find(name => name.endsWith('.psb')); if (!binFileName) { throw new Error("No .psb file found in ZIP"); } const modelData = files[binFileName]; // 4. Create blob URL for EmotePlayer const blob = new Blob([modelData], { type: 'application/octet-stream' }); const fakeUrl = URL.createObjectURL(blob); // 5. Load model await player.promiseLoadDataFromURL(fakeUrl); // ===== Model loaded successfully ===== document.getElementById('loading').innerHTML = "Done!"; setTimeout(() => { document.getElementById('loading').style.visibility = "hidden"; }, 1000);这部分的代码，改为获取zip压缩文件，解压后放在浏览器的blob上，然后给后续的player.promiseLoadDataFromURL()进行调用。这样一来，就可以极大的降低传输文件的流量消耗和等待时间。不过，需要注意的是，创建压缩文件的时候，需要内部只有一个文件，并且命名符合规范，采用的压缩算法需要是DEFLATE，否则会不支持。批量处理pure.psb文件因为角色模型文件有点多，一个一个手动压缩比较花时间，于是我就用bash脚本批量处理：#!/bin/bash # 遍历当前目录下所有 .pure.psb 文件 while IFS= read -r -d '' file; do # 构造目标 ZIP 文件名 zip_file="${file}.zip" # 如果 ZIP 已存在，跳过（可选：如需强制覆盖，请删除此 if 块） if -e "$zip_file" ; then echo "Skip: $zip_file already exists." continue fi echo "Compressing: $file -> $zip_file" # 使用 zip 最大压缩级别 (-9)，且只存一个文件（-j 可选，但建议保留路径结构） # 注意：这里不加 -j，保留文件名；若你希望 ZIP 内部无路径，可加 -j zip -9 -q "$zip_file" "$file" if $? -eq 0 ; then echo "✅ Success: $zip_file created." else echo "❌ Failed to compress: $file" fi done < <(find . -maxdepth 1 -type f -name "*.pure.psb" -print0) echo "All done!"保存为compress_psb.sh放在模型文件夹内，直接执行就可以全部完成压缩了。目前以上部分的代码改动和优化，已经同步到了NekoWebShow，访问的加载速度应该也可以变快了。番外：增加动画暂停功能接下来这个功能就是为了服务我博客主题构建的了——动画暂停因为我设想是动态的E-Mote立绘只放在首页，而浏览文章目录的时候不会展示，会滑上去看不到，这时如果还是继续运行就有点太浪费性能了。于是，我希望动画可以按需暂停和恢复，而且我想做的页面是一下子就滑掉的，所以只要把控好时机暂停和恢复，对用户观看页面就基本无感。感谢LLM，在把代码给LLM看过后，它说原始代码虽然没有这个功能，但是实现起来不难，只需要在加载播放器完成后执行下面的代码：// monkey-patch 在加载完 EmotePlayer 后执行，实现动画按需暂停，节省性能 // 暂停命令：EmotePlayer.device.pause(); 恢复命令：EmotePlayer.device.resume(); if (EmotePlayer.device) { EmotePlayer.device._originalKick = EmotePlayer.device.kickAnimation; EmotePlayer.device.isManuallyPaused = false; EmotePlayer.device.pause = function() { this.isManuallyPaused = true; if (this.animating) { this.animating = false; cancelAnimationFrame(this.requestId); } }; EmotePlayer.device.resume = function() { this.isManuallyPaused = false; this._originalKick(); // 触发原逻辑 }; // 重写 kickAnimation，使其尊重手动暂停 EmotePlayer.device.kickAnimation = function() { if (this.isManuallyPaused) return; this._originalKick(); }; } // monkey-patch END将这段代码插入到run()函数后面即可实现。这时，在控制台执行EmotePlayer.device.pause();即可实现动画暂停，CPU利用率回到0%需要恢复时，执行EmotePlayer.device.resume();即可恢复动态立绘渲染。
新瓶装旧酒——卖掉内存后电脑硬件重组记录
一次“新瓶装旧酒”的电脑装机在以3960元的高价抛售掉服务器的内存后，我面临了内存短缺的问题。虽然我没有全部卖掉，留了两条16G内存作为应急，但是我已经感觉没有这么大的内存，也没必要再去用我的机架式服务器——因为虽然性能确实强劲，但是绝大部分时间是用不完这部分是性能的。而且我还有另一块X99主板，同时白嫖了一个650W电源和一颗E5-1680v4，所以，我决定尝试一下M-ATX主板的小钢炮主机，兼容更常见和便宜的M-ATX主板和ATX电源，稍微有一点拓展性，牺牲一点便携性，但是相比于我的双路机架式服务器，那确实好很多了。而且还有一个问题，毕业后去读研，不一定有地方可以给我安放服务器。所以，老伙计终究还是要提前退休了。仔细权衡后，我以58包邮的价格在并夕夕上搞到了一个M-ATX小钢炮机箱，然后搭配我之前99卖的X99寨板，两条16G DDR4 RECC内存和E5-1680v4。至于显卡，我认为RTX2080Ti涡轮卡塞这玩意里面应该是毫无压力，而且因为涡轮卡，散热应该会更好——废热直接排出机箱。硬盘方面就比较肉疼了，因为服务器是有12个机械硬盘位，真的是为所欲为，现在要进行配置浓缩了，只能选取了一块3TB硬盘作为数据盘，把常用数据和重要数据复制了一份进去，算是做了一次数据的断舍离。系统盘就直接拆现任的256G nvme 固态硬盘了。虽然是降配，但是实际上对于打游戏反而是提升——因为新的CPU虽然综合性能变差了，但是主频更高了，单核性能更强，对打游戏帮助更大，实际上打游戏帧数确实是更高，更稳定了。现在我的机架式服务器已经成废铁开不起来了，上面还留着一些硬盘和CPU（装机照片先是硬件全家福：装机过程相对来说是比较难一些，毕竟空间狭小。花费时间最多的其实是在想怎么理线上面，毕竟想在这么小的空间整理好布线不容易。好在最后还是解决了，装好后效果不错。哦，对了，整个机箱是无光的，是的，无光，机箱连电源指示灯都没有！12CM的风扇当时是想给V100散热买的，根本就没想过灯光这回事，无光的便宜一点。然后显卡的话，涡轮卡也是没有灯光的。。。一个侧透机箱，硬件全部是无光硬件，有点搞笑了。不过显卡就成主角了——因为其他硬件都是黑色的，线材也是黑色的，然后显卡因为改造过外观，是亮粉色——非常的抢眼。解决更换主板后系统启动问题更换主板后，让我意想不到的是，manjaro居然倒地不起了！开机进行系统引导时报错：grub_is_using_legacy_shim_lock_protocol not found经过初步查阅资料，得知这是安全启动导致的问题，但是在原来的机架式服务器上面我安全启动也是关闭的，我就觉得很奇怪了。而且这个问题选择系统快照无法解决，似乎只能重装了？抱着试一试的心态，想看看按照镜像有没有恢复模式。我用U盘启动了manjaro的安装镜像，然后发现有一个检测本地引导文件的选项，我选择了之后，发现它检测出来了本地的系统，然后居然就成功引导启动了！进入桌面后，通过查阅资料似乎是要重新安装一下GRUB，用root执行了以下命令：grub-install然后就正常引导了。
NetaYume：一个很强的二次元画图模型这段时间没有折腾什么技术方面的内容，自从买了V100，就是天天让V100给我画图，不知不觉花了一大堆图片，有一说一也确实好玩。我想想博客鸽了这么久，不如分享一些不错的画图成果吧。NetaYume模型是Neta Lumina模型微调后的版本，角色理解能力更强。Neta Lumina 的最大优势在于能够理解复杂的自然语言描述。除了使用标签（Tags），Neta Lumina 也能深刻理解自然语言描述（NLP）。模型在训练时学习了多种不同的描述风格，这意味着您可以像与真人画师沟通一样，用完整的句子来描述您想要的画面。以上内容摘自官方文档，所以说，当我初步掌握了如何针对这个模型写提示词的时候，画出来的效果是让我大受震撼的。另外，值得一提的是，我后面还尝试使用它的拆分版本运行，实测H61主板+E3-1220v2+两条8G DDR3内存+P104-100可以跑，但是跑一张1920x1080的图需要跑大概15分钟，而V100只需要5分钟，慢了整整三倍，还是很夸张的。不过也可以理解，一个CUDA核心数1920，另一个5120，差了2.6倍，而且还有显存带宽的差距，所以倒也不奇怪了。好看的巧克力插画其实，我画的最多的角色是猫娘乐园的巧克力，因为好看的同人图太少了。一开始我并没有看文档，想当然地按照常规画图提示词的套路来，虽然结果不够理想，但也抽出来了一些不错的图片：好看是好看，但是感觉像是同人图，没有官方插画那么对味。查阅文档后，我才发现，这个模型有对画师画风进行强化训练，也能比较好地理解角色，所以，我尝试指定了Sayori的画风，得到了出乎意料的惊喜：像！太像了！于是我又尝试去构建了几个不同的场景：真给我画爽了。不过别看这么好看，这是抽卡抽了几百张图，挑出来比较好看的结果，画崩的我就不放出来了。最容易绷的还是手，这个确实没办法，虽然这个模型以及改善了很多，但是还是会不可避免地崩掉一些。尝试指定不同画风另外，因为模型支持指定画师画风，我还尝试了使用别的画师画风去画对应的角色。巧可甜恋的雪村千绘莉，指定为原画师Shiratama画风：初音未来，指定画师TID的画风：看头发和眼睛确实对味了 TID是洛天依官方插画御用画师，所以画出来有点像洛天依（初音未来，指定画师Qys3画风：非常的对味镜音铃，指定画师TID的画风：我还尝试了一下用三种不同画师的画风画了一下巧克力，也就是保持第一张图的随机种子不变，如何分别用三种不同的画师预设画三次：确实就是各自画师的画风风格了，对比还是很明显的。画个香草吧我还是试了一下画香草，但是相对来说没巧克力画的这么完美。因为香草的眼睛上睫毛是平的，虽然多次和AI强调过这个细节，甚至添加了相关的负面提示词，但是总是会画出来有点弧度，导致不太像，结果不理想。其中，图二是最像的，人物的感觉和形象都对了，但是还是差一点感觉。这是抽卡抽了一百多张图才抽出来几张还行的。Z-Image-Tubro试水另外，我发现一些复杂场景描述下，NetaYume没办法很好的驾驭，总是画崩，而Z-Image-Tubro却可以很好的理解画面的描述，进而画出准确的画面。目前在我试过的模型里面，Z-Image-Tubro确实无疑是最强的，只是可惜这个模型是真实画面的通用模型，并没有对二次元插画进行优化，以至于叫它画二次元，画风都是清一色一个样。不过还算可以，至少它在生成图像的时候，对于人造几何体的把控能力，比NetaYume要强得多。我也尝试画了一些图：好像也就只能画对初音未来，巧克力和香草，我用最详细的语言来描述了，还是画不对。不过从构图理解能力，人造几何体绘制能力来看，确实是遥遥领先的。如果这个模型可以微调出来一个二次元专用的版本，那就是无敌。
时间好快，2025年转瞬即逝我感觉上一次写2024年度总结，好像还是前不久的事情，不得不说时间还是太快了。2025年发生了很多很多事情，也算是对我人生影响的关键一年，现在，在这一年即将结束之际，来回顾一下这一年的种种事情。接下来，请看我写流水账（）网站运营方面今年仍然保持了较高强度的更新，虽然我的更新态度是，没事就不更新，更新一般都是事件驱动的，但是因为喜欢各种折腾不知不觉就更新了不少文章。主要的文章还是围绕了解决各种遇到的技术问题，以及分享和记录一些自己感觉有意思的事情。我觉得整个写作过程还是比较有意思的，毕竟博客属于是个人是私人领地，你就是这里的主角。你可以自由的发表你的观点和看法，想骂就骂，想喷就喷，不用在意别人的看法。当然，难免会有人不爽你，不服的话就技术互撕咯，DDoS，CC攻击什么的，一般也都是脚本小子。撕完就又有写文章的素材了，多是一件美事（bushi）发布文章数量在发这篇总结之前，今年一共发了56篇文章（其实是57篇，有一篇因为某些原因隐藏了），算是很高产了平均每个月发4-5篇文章，已经遥遥领先绝大部分友链博客了。这其中，技术类文章的占比仍然是主流，毕竟我当时创立这个博客的想法，就是打算当作一个个人技术笔记来使用，并穿插一部分其他内容。网站被打次数小的攻击，上不了台面的，就算了，这里就统计对我网站造成实际影响的被攻击次数——7次！而且集中在今年下半年9-10月其中，DDoS攻击6次，最高攻击流量达到了惊人的79Gbps！被DDoS UDP流量狠狠灌注的感觉真是太爽了，特别是让对面发现使劲打了还打不死，更乐了。虽然云服务器被黑洞，但是仍然可以保证网站访问，我估计对面已经红温了。具体的配置方法，可以去看看这篇文章：绕过黑洞！利用VPC内网互通低成本实现DDoS攻击下网站高可用另外，在最后一次DDoS攻击之后，有一次大规模CC攻击，通过随机URL请求绕过了我的缓存策略，打在我服务器上拳拳到肉，直接把网站拖垮了。事后我也复盘了，搞了更优的限流防御策略，虽然仍然存在被打垮的可能，但是极大提高了攻击者的攻击代价。想看更详细的信息，可以看一下这篇文章：个人博客低成本CC防护：抵御85%+常见CC攻击就在这次CC后，我网站到目前为止，没有被打垮过了，也没再有过DDoS。在这里要感谢脚本小子的陪练。尝试开设新网站：猫娘答案屋因为之前觉得nekowork.org这个域名挺好的，就盘下来了，之前是作为个人信息聚合页面，感觉还是太浪费了，正巧我有路子可以获得一些网课答案，于是就想拿来搞一个用于分享大学网课答案的网站。但是看了一下搜索引擎的流量表现，好像确实不怎么样，基本上没什么权重，有点可惜。其实三月份的时候就尝试整理了几个网课的答案，然后发现太累太花时间，就没能坚持下来。所以中间断更了半年多，也没再怎么管过这个网站。然后最近手又痒痒了，想尝试一下自动化爬虫抓取答案，当作是爬虫练习去尝试。你别说，配合现在的大语言模型，写一些基础的代码还是相当轻松的，特别是当你有了程序实现逻辑的想法，直接让AI来帮你实现具体的代码就好了。经过几天的测试迭代，我也是成功写出了一套针对那个网课网站的自动化爬虫程序，可以实现自动获取章节作业的标题，随机选择答案，提交答案，保存题目和正确答案的过程，行云流水。但是这只是半自动爬虫，仍需要我手动进入课程，点开作业章节。不过，相对于手动提交答案后复制整理正确答案，速度确实快多了，而且不折磨人。如果网课作业量不算多，作业的章节10个左右的话，基本上一个网课10分钟能整理好，章节里面的题目越多，效率越高。我相信有这套工具加持，我应该是可以持续更新那个答案站的，只要我有时间有精力。白嫖华为云那些事2025年是华为云大撒币的最后一年了，大概10月份的时候，就是最后一次活动了。整个活动持续了大约一年，2024年9月开始有这类白嫖活动，高强度大撒币到2025年9月，后面就调整政策不撒了。有点可惜的是我8月份因为忙保研那些事情，没心思参加，缺席了一次活动，痛失1k代金券。估计后面华为云也不会再搞这类活动了，毕竟一次活动，一千多号人，一人一千这样发，虽然是云资源代金券，但是也是烧钱的。目前我的白嫖战果是：主服务器实例到 2035年6月1日过期 累计白嫖了十余年使用权（2024年9月20日-2035年6月1日）备用服务器实例到 2031年1月18日过期 累计白嫖五年多使用权（2025年9月28日-2031年1月18日）这两个服务器将是我未来网站运行的中流砥柱，关键力量，坚实根基，核心支撑，稳定基石，动力源泉，保障中坚，运转中枢，可靠依托，发展支柱，存续命脉，效能核心，运行保障，长远依托，根基所在，关键支撑，不可或缺，至关重要。它们既是网站平稳运行的固本之基，也是长远发展的赋能之要，更是持续服务的立命之本，为平台建设提供坚实保障，为功能拓展筑牢核心支撑，为效能提升注入不竭动力，为安全运行夯实稳固底盘，为创新发展搭建重要载体，助力网站在数字领域行稳致远、固本强基、持续向好、提质增效、再创佳绩，切实保障了服务供给的稳定性、持续性、安全性、高效性，为长远发展奠定坚实基础、凝聚强劲动能、筑牢安全屏障。(对不起，串味了）个人收藏方面今年又买了一些个人赛博收藏品，主要是一些老的顶级显卡：GTX780Ti （坏的，80包邮，后面朋友帮修好了）GTX TITAN Black 320包邮HD 6990 180包邮基本上没有太大使用价值了，特别是HD 6990 ，已经是纯赛博电阻了。不过，今年确实没再买过手办了（主要是没地方放了）另外，还有一个新增的赛博收藏：nekowork.net 很不错的一个域名，当时注册nekowork.org的时候，这个被人注册了，现在之前用的人不续费了，就到我手里了。一时半会不知道拿来干什么，先留着吧，好域名可遇不可求（）今年买的硬件感觉今年又是买硬件大出血呢！让我看看今年除了刚刚说的三张收藏品显卡，还买了什么东西：P104-100 买了五张，炸一张，一张79包邮，买来跑SheepIt渲染农场赚取渲染积分V100-SXM2 16G 和对应的配件，花费683，其中卡本体385，转接板200，散热器98E5-2667v3 两颗，28一颗，买来装上机架式服务器打游戏，玩对马岛之魂99包邮的杂牌X99主板，买来升级家里云两条16G REG ECC DDR4 2133p内存，海力士的，当时买是70一条包邮，现在涨价到450，太离谱了！四条DDR3 8G 1600MHz内存，买来装渲染机用的，一条24一个500W电脑电源，用于测试收藏的核弹显卡大概就是这么多吧，不过感觉现在内存涨价涨疯了，我有点想把我的服务器的RECC内存全部出掉，8条，一条买个500，4K直接到手！而且也有备用的电脑，备用的内存，等降价后再买更好的，岂不美哉？！这样看，如果真卖了，我白用一坤年机架式服务器，还倒赚500，太离谱了。目前有12条16G REG ECC DDR4 2133p内存，计划出10条，已经3960出掉了，血赚。虽然最后发现E5-2630v4做家里云实在是性能过剩，然后正巧买了V100，而H61配E3-1220v2因为硬件限制无法驱动V100，所以我又把家里云降级回到它原来的配置，用这套X99带V100了。有一说一，V100绝对是今年下半年性价比最高的垃圾佬AI神器，16G的HBM2显存，跑模型速度飞快！美中不足是驱动刚好停更了，但是也正是10月份穿出驱动停更的消息，让淘宝V100价格暴降200！我到手的时候还没停更，但是现在停更了，永远留在581版本的驱动了。不过就玩现在的主流模型还是没什么问题的，现在的模型也很可以了，只是未来可能就玩不了更好的了。相关文章：V100 GPU 初体验：AlmaLinux 8.10 快速部署 ComfyUI其他的小玩具看了一下，今年买的东西确实比前两年更少了，我感觉我现在消费也更理性了，不会轻易乱花钱。当然了，要是真心想要的东西，砸钱也是毫不犹豫。今年买的玩具：两个500W惠普服务器拆机电源，拿来当12V电源玩，现在已经丢在V100的电脑里面当副电源带显卡了，一个25，相当便宜！一个64G U盘，因为另一个U盘落在学校了，想在家装系统没U盘可以用，不见30米一套廉价APU板U套，以及其他用于DIY的配件和物品，拿来装洗洁精主机，还是挺有意思的，大概花费了一百多吧，感兴趣可以看看这篇文章：手搓洗洁精ITX主机教程一个1m的fufu玩偶，丢在宿舍，导致我每天只能抱着她才能睡觉，不然都没位置睡觉了（），花费100黑色星期五撸了两个灵车VPS，都是当玩具的，重要的东西不可能跑在上面。一个2USD一年，无限流量，100M带宽，配置1vCPU 1G内存 50G硬盘，感觉相当划算，CPU性能还凑合，超开不算严重。另一个我感觉有点血亏，9.99刀一年，CPU超开比较严重，也是1vCPU 1G内存 50G硬盘的配置，还限制每个月1TB流量，上下行都算，亏麻了！但是是1G带宽，速度非常快。一张RTX 5090......手办卡，打算送人的，花了118，差不多就是一个手办价格大概就这么多吧，感觉一年买的玩具也不算少了，不过考虑到我平时基本上不吃零食，也不喝奶茶饮料什么的，我觉得每天能省下来的钱，拿来买自己喜欢的玩具，不过分吧？个人学习生活方面这就是我认为这一年过的最紧张刺激，最意义重大的部分了！也算是成功对我在去年的未来展望最看重的部分兑现了，而且是超额完成任务！但是这一年也是我大学里面最焦虑的一年，特别是在暑假这个保研关键期阶段，确实是相当的焦虑了。总体来说整个暑假简直是行尸走肉般的生活，十分难熬，备战感觉无力，玩又玩不开。不过现在回想起来，一切都值得了！寒假生活记录寒假期间我自建了音乐媒体库，并以此为契机写了一篇文章：自建Jellyfin私人媒体服务器，告别屌毛网易云！，不再被网易云卡脖子了，拥有个人媒体库的感觉真好！另外，我在寒假期间狠狠地爽玩了《对马岛之魂》，用RTX2080Ti接家里面的大电视，开4K画质，连上PS4手柄坐在沙发上玩真的爽爆了！这种题材的游戏，我也是第一次玩，大概讲的是蒙古入侵日本的故事，扮演武士进行反抗，还是蛮有意思的。游戏的画面非常好看，走在地图场景里面，导航靠风这个确实很有创意，配上飘落的树叶真的是绝美！不过，要是玩这个游戏，配音建议开日语配音，英语配音感觉有点奇怪（）寒假的绝大部分时间都在古代的对马岛里面“体验武士生活”，当然了，春节期间还是被家里面拽出去走亲访友了。然后在农村亲戚家搞了窑鸡，挺有意思的：然后还看了村BA:逛完农村后又是天天对马岛启动，然后假期接近尾声的时候，把游戏通关完成了。期间再次被家里拽出去瞎逛，去看了一下本地的花卉小镇，离城区挺远的，不过我感觉也没什么好看的，在那里玩兔子：请问你今天要来点兔子吗？春季学期的学习生活寒假结束，开始保研冲刺的最后一个学期。不过好在，这个学期的课程已经非常少了，基本上很闲。课程设计唯一比较重要的课程设计，这次居然需要小组合作了，因为是要设计一个片区改造计划，而且是基于现实立的课题，自然就少不了调研分析了。可惜实际上说要我们进行改造的部分，规划上已经是要全拆了的，有一种理想和现实的冲突感。有一说一，这些老旧社区就像一个一个活生生的时间胶囊，记录了城市发展的点点滴滴，是一代甚至几代人的记忆根基。虽然说确实是“老破小”，但是我感觉直接这样子粗暴拆除，或许并不是最好的做法。调研的时候，搬迁和拆除工作已经进行到了一半，走进空荡荡人去楼空的小区，看着满目疮痍，总有一种偏伤感但是又说不出的感觉。要拆除的老厂房：老社区便利店老板娘养的哈基咪：老破小社区一角：拆除中的居民楼社区：逗猫：课程设计，最后也是如愿以偿拿了一个比较好的成绩，结束了这个最后对保研影响至关重要的过程。新的玩具因为感觉一般的fufu还是太小了，抱着不够爽，于是乎买了一个大的：五一假期因为这个学期的学习任务相对来说比较轻松，所以五一还是可以有闲情逸致和家里面一起出去玩的。然后就和几个老妈认识的朋友一起，直接去广东的电白，湛江去玩了。先去电白泡了温泉：然后又去湛江看了一下一些军事装备，虽然都是老掉牙的东西了，但是第一次进真的飞机驾驶舱还是挺好玩的：校园趣事不知道那个老师不注意，开车把学校里面的一个水井盖撞烂了，也是活久见了：爽玩游戏后面因为课程基本上接近尾声，并且那个学期没有考试，于是在学期后面一个月的空窗期，我直接爽玩赛博朋克2077，狠狠玩了一个月，每天都是吃完早餐直接去教室，开电脑，打游戏，中午和晚上出去吃个饭回来继续玩，高强度打任务，基本上玩到晚上11点才回宿舍。最后也是成功的把游戏的剧情刷了一遍，不得不说这个故事还是挺震撼的。玩游戏的工位Be Like：武汉之行因为要去参加某酒吧舞的学术夏令营，所以要去武汉，于是真来了一场说走就走的旅行，还是坐的绿皮火车！硬座，14小时，还跨夜，差点把我坐死在上面...不过后面顺带去武汉的几个景点看了一下，黄鹤楼，湖北省博物馆这些，顺带去参观了一下臭名昭著的武汉大学。详细的旅行日记，可以看看这篇文章：武汉旅行碎片煎熬的暑假保研准备的煎熬因为并不确定能保哪个学校，所以整个假期都在焦急的准备。之前问了保研成功的学姐的经验，她说我六月份再准备已经有点迟了，搞得我挺懊悔五月份的时候闲的蛋疼疯玩游戏，如果提早一个月会从容很多。不过那时候我还是以自己最快的速度把相关的，可能需要的材料都凑齐了，时间大概花费了一个星期，我感觉这是我大学从事学习方面的事情效率最高的一次，然后就是密切关注各个学校动态了。而这个密切关注，就贯穿了整个暑假，所以非常的煎熬。暑假之前去武汉某酒吧舞，就是其中一次的夏令营，我就当作是练手去的，结果并不理想。这导致我的暑假准备工作更加焦虑了，基本上每天都在反思是哪里出了问题，未来要去的学校考察侧重点在哪里，老师面试可能会问什么问题，我又应该怎么准备......我只能说，保研这条路并不轻松。我就这样煎熬了三个月，期间的过程我愿称之为痛苦，比当年高三还难熬。因为高三是又有外部压力压着你，而且和同学在一起，也算是有人“并肩作战”，尽管都是潜在的竞争对手；而保研就不一样了，这是你自己给自己的压力，比外部压力更难受，并且，你是孤军作战，并没有高中那种“零信息差”的感觉，而是一旦产生信息差，后果极其严重！你不知道对手的动态，也无法参考，唯一可靠的信息就是目标学校历年来发布的夏令营通知或者预推免通知，也就是一版冷冰冰的文字，虽然极具参考价值，但是少了一些人情味，特别很多学校都是一个报名系统，或者发送报名邮件，并没有即时的反馈，更加折磨人。出游散心或许这次，被家里面拽出去，是不错的选择了。因为精神压力实在是太大，家里面说不差这几天，劝说之下，我还是同意了出去散心这个建议。然后就是去了崇左那一带转了一下，看一下自然风光，主要就是明仕田园和德天瀑布：秋季学期：保研冲刺这个学期，我愿意称之为压力和事情两级分化最严重的学期，开学一个月的时间内，绝对是忙疯了，而且压力非常大！因为要准备各种保研材料递交给本校，还要准备面试材料去应对保研目标学校的面试，真的是忙的起飞。但是当保研工作结束后，就进入了闲的蛋疼的时间段了。因为实在是太闲，我甚至为了找点事干，顺带完成学校半硬性的实习要求，去找了一份实习。可以说，在预推免阶段，各个学校的面试节奏相当快，基本上一天就解决了，还要会遇到学校面试时间段撞车的事情。我就去面试了两个酒吧舞，没有像某些海王那样，报了十几个学校，日程直接排满。当时我在面试现场等候的时候，看到一个海王在和她的同学抱怨，说基本上一天一个，跑去面试都不带停的。看她们说，至少报了七八所学校，过于离谱了。好在面试过程还算顺利，甚至超出预期！因为我当时看学校太迟了，只能选了两所酒吧舞，一所稳的，一所冲的，结果，冲的居然上了！而且是压线上！太幸运了。面试完后，直接就顺带去珠海真实群友了，逛了逛石墩市，有网友带，边逛边聊，还是很不错的。华为好，华为美，华为给我增智慧MV拍摄地：拱北口岸：国庆假期出游保研结束后，心情和压力好了不少，于是当家里面说要出去玩的时候，欣然接受了。国庆期间，先去拜访了小三峡景区，不过没想到的是，就这么一个小景区，国庆期间也是人满为患，导致只能进去逛一下，想坐船游是没办法了。不过周边的自然风光确实很好，有一些地方拍照也很能出片。然后，我们又马不停蹄的往北上到贵州，去看了一下小七孔景区，自然风光还是相当可以的。其中，乘坐游船划水的时候，还近距离看到了鸳鸯：然后顺路故地重游了程阳八寨（对我来说），再去了桂林阳朔，参观20元人民币取景地。以及，看到了工具鸟——鸬鹚：万圣节Cosplay变装嗯，女装 Cosplay 这事，其实是当年我做 UP 还火的时候，那帮粉丝们天天喊着想看的东西，现在算是真尝试了，感觉还行。事情的起因是，在群里面口嗨，说你敢买，我就敢穿，结果真买了（）那就，穿上吧！看过番剧的，就知道是小林家的龙女仆里面的托尔了，选择这个角色的原因是，角色身高比较高，我一米八穿上，也不会显得违和。其实带上口罩挡脸再穿Cos，我觉得还算可以了，照片发群里他们居然说好看，是我没想到的。或许是因为我的身材比较标准偏瘦吧，甚至裙子还感觉做大了，松松垮垮的，要用要带的蝴蝶结绑一下才能固定好。设计院实习体验最近几年建筑设计院风评很差，甚至老师推荐的实习工作，还有一个月只给500块钱这种抽象工作，过于离谱。于是我问了一圈同学，找到了一个相对正常一点的，工作日每天给100的设计院，加班工资按劳动法另算，一个月能有个2.5K左右，我觉得还可以接受，就去了。有一说一，设计院强度确实高，加班是常态，甚至说是病态！对于正式工，基本上都要加班，而且工作安排什么的也不太合理，经常就是突然就派任务过来了，而且时间很急。不过，工资这一块，确实还行，虽然辛苦，但是给的确实也多，基本上八九千一个月还是有的，有一些肯干的，一万多一个月都不是问题。问题是，这种工作强度下，身体能不能撑得住呢？我去实习就是去看看实际的工作情况，毕竟后面还要升学读书，给的2K多的工资还想让我干正式工的活是不可能的。所以我就只做他们正式工工作的三分之一到四分之一，多了就不干，或者调到加班时间，额外要加班工资。好在带我的那个姐人挺好的，安排工作相对来说也比较合理，不会的地方也是手把手教，总体来说实习体验还算是不错的。而且其实也不是每时每刻都有活给实习生干，比如现在这篇文章，我就是再办公室里面用公司电脑写的。工作期间，还有一次外出踩盘机会，也就是参观现在的楼盘，当作是竞品考察和分析。我一开始还以为说房地产这块暴雷死透了，然而现实是，还是有新的项目落地，只不过赚钱没有之前这么容易了，开发商更卷了——卷配套，卷环境，卷体验，卷赠送。有一个同事感慨，说2019年那会高位买的房，和现在这个时间点买的房，同样的价格，天差地别！甚至现在买房只需要之前一半的价格，获得了更高的配套品质，之前想都不敢想。以前那是能造出来房子就能卖钱，现在不是了，要造好房子才能卖钱。参观了几个楼盘，和我十多年前陪我老爸去看房，已经是完全不同的体验了：看完之后感慨，房地产确实进化了，更卷了，但是对于建筑设计行业，那也意味着工作量增加了，工资还更低了。亲戚的婚礼这次我也算是有幸作为主角之一，参加了亲戚的婚礼——算是很亲的亲戚了，从小一起长大的。现在想想也十分感慨，这么快，他就结婚了。我是不是准备要被安排了？催婚这个，其实已经有了，只不过我一直懒得找女朋友，更想自己享受生活。这是一个原因，还有另一个原因是，我也不擅长搞这些情感上的东西，并且要和我的兴趣爱好，三观重合，为人比较好，这点还是比较难找的。我感觉如果真的有缘分，那应该是互相之间会越走越近，互相理解，发展共同的兴趣爱好，这才是我想要的女朋友（）想要的生活是，女朋友可以天天穿喜欢的角色的Cosplay，二次元浓度极高，当然，先排除国内二游（例如原神），这种不能接受，我想要的是纯正日系二次元的，然后还可以有计算机技术或者硬件技术方面的兴趣，这样就比较好有共同话题。不过这种想想就好了，几乎不太可能，也有理由推脱催婚了（）附上一张配亲戚去接新娘，到新娘老家的照片：灵山大卢村，算是古建重点保护单位了，大部分都是村民家族的祖公厅，家族客厅。虽然是景点，文物保护单位，但是这些古建筑的所有权，或者说使用权还是归本地村民。所以可以看到在文物保护单位建筑里面进行婚礼仪式，在里面开火做菜，吃席，也算是别具风格了。校园的变化年末校园里面最大的变化，就是图书馆后面的山被挖了，估计又是要新建什么建筑吧。最近几年学校确实在大兴土木搞校园建设，对比我刚入学那会，变化真挺大的。个人兴趣方面Blender动画之前我的计划是，今年进一步学Blneder动画，目前看是有了不少进展。准确来说，算是在实习期间，在公司电脑上摸鱼做的。先是照着教程搭建了一个教堂场景：Blender练习作品：动画风格教堂建模然后，尝试做了一个我之前一直想要做的3D抽象动画，内容是复刻刘华强买瓜的视频。全程高能，有一说一确实抽象，我一边做一边笑，自己都绷不住。丑到搞笑，荒诞到离谱。视频里面，把我的网站网址和一些关系比较好的友链的网址，作为彩蛋出现在了动画里面，主要是充当了街边店铺的招牌。因为过于抽象和丑陋，我就不放在博客毒害大家眼睛了，想看的可以直接去B站搜，很容易找到。AI绘图其实当时继续学Blender的时候我就在想，现在AI已经进化到这个程度了，只要学会给AI描述需求，AI就能生成令人满意的艺术作品，甚至比你苦学好几年的动画，绘画，建模都强，那我们还这样子学艺术的意义是什么呢？正好买了V100，所以可以名正言顺的去体验AI画图了，16G显存可以应对相当一部分本地AI模型的需求了。有一说一，相比于之前第一次玩的Stable Diffusion 1.5，现在新的AI模型画图能力确实强大了非常多。很明显已经没有了那种AI感，各种风格都能轻松驾驭，而且画面出错的情况也是越来越少了。最明显的提升是，手指基本上正常了，很少出现多根手指的情况了，人物躯干也不会画成一坨奇怪的东西了。而且，现在的ComfyUI工作流，可以更好的控制AI生成的功能，我觉得等有时间还是值得深入去学习一下这个工作流的使用。毕竟不可否认，AI就是未来的新质生产力，很有可能会颠覆整个创意行业，甚至世界。非常像计算机的“工业革命”。很巧的是，早些时候我就有过想要学习插画的想法，毕竟能画出自己的二次元老婆，多是一件美事！所以，我希望可以进一步学习这方面的知识，靠AI画出我想要的插画，而不是依赖于画师。这是之前调教了一下画出来的两张图，确实相当哇塞了：打电动今年玩过的游戏有：CS2，对马岛之魂，赛博朋客2077，霍格沃兹之遗其中，CS2单纯就是为了打箱子，打这逼游戏确实太败坏心情了（所以，真正用于体验，接触的新游戏：对马岛之魂：寒假一个月通关赛博朋克2077：春季学期下半学期一个月空窗期通关霍格沃兹之遗：12月Epic送的游戏，据说很好玩，试了一下确实不错，打算是连着寒假一起通关搞开发今年瞎搞的开发活动，有一个成功可喜可贺，那就是——「NekoWebShow」这是一个使用开源的第三方E-mote实现框架FreeMote做的猫娘乐园角色动态立绘展示，终于解决了我Linux桌面没有猫娘动态壁纸可用的问题。另外，还有一个VisitorLoggerPro-Enhanced，对访客插件完成了个性化改造；以及二次开发改造出来了一个Typecho-LinuxInfo插件，可以在Typecho后台看到服务器的基本占用信息。除了这种网站方面的开发，我还自己搞了一个浪潮SA5212M4服务器的风扇动态控制脚本，详情见文章：Python实现浪潮SA5212M4风扇根据温度自动调节最近，我又尝试用Go语言开发了一个校园网自动登录程序，并且能够契合学校上网时段管理规范，详情见文章：断网自动重连！用Go实现广西大学校园网断网自动登录社团活动方面没有参加新的社团，但是在之前参加的社团里面重新变活跃了——因为保研结束了，有一定的空窗时间了。参加了科技协会的电脑义务维修活动帮助科技协会恢复了协会的网站，这算是我对社团最大的贡献了主要是感觉这个社团真玩不起来什么，负责的协会会长天天说什么人才断代了，新生对技术的理解是一代不如一代了。比较让我震惊的是，副会长居然连一些基础的技术概念都不了解。虽然社群还算是又技术氛围，但是那些能讨论技术的基本上都是那几个老登，新人基本上不活跃，聊来聊去都是那几个人，而且大部分在群里面活跃的都毕业了，挺感慨的。明年我也要毕业了，本科生活完结撒花~对生活的看法和感想现在回归一路走过来的历程，我的感受可以用两个字来形容，第一个字是“快”，第二个字是“空”。“快”对于“快”这个字，确实很好理解，四年时光转瞬即逝，光阴似箭，回望高中生活，仿佛就在昨天，初识高中的同学竟然已经是7年前。现在想想是真的吓人，不知不觉就初中毕业了，不知不觉就高中毕业了，不知不觉就要大学毕业了，那未来，是不是不知不觉就研究生了，不知不觉就工作退休了，不知不觉就变老了，不知不觉人就没了。小学初中那会总感觉时间过得很慢，现在在看，太快了，真的太快了。当年的少年，俨然变成了大人的模样，尽管内心还是一个少年。而当年天天哭闹，到处给我爸妈“找麻烦”的弟弟，现在已经上小学三年级了，甚至还向我当年那样，饶有兴致地玩起了GTA......真的是......快！上面这三幅插画手稿，创作的时间点是2017年下半年的秋季学期，也就是我初三的上学期，至今已经过去了8年。不过，这三张插画并不是出自我的手，而是当时初三厚着脸皮让一个女生帮忙画的，现在看感觉有点太无理取闹了，不过或许这就是青春吧。现在在看这些画作，还是能回忆起初三的那个秋天，虽然作画质量一般，肯定是比不过AI出的图片了，但是这或许这种人情味，是AI再怎么精美的画作，也无法替代的吧？之前一直想要扫描这些画作，这次终于是有机会扫描了——再不扫描就越来越烂，越来越褪色了！我放出这几个画作的扫描件，只是想表达一下自己对时间流逝的伤感。年复一年，日复一日，日子就这么过着，也不知道当年的那些同窗，现在怎么样了，不好问，也不敢问——毕竟，已经相隔多年没联系了，各自在忙各自的事情，要聊也不知道聊什么了。顿时印证了鲁迅再《故乡》里面的那句话：我似乎打了一个寒噤；我就知道，我们之间已经隔了一层可悲的厚障壁了。我也说不出话。“空”而对于“空”这个字，可能在外人看来不好理解——在别人看来是本科赢家，绩点卷王，成功上岸酒吧舞，怎么就是“空”了呢？但是这个空，更多的是内心上的空虚——不知道自己真正想要什么，不知道未来要干什么。这里说的想要的和要干的，是更长远的目标，而不是当下一两年时间段的目标，短期的目标，还是有的，没有的话就不可能保研的。这种“空”的感觉，源于我对于人生意义的思考，然后越想越觉得——人生没意义。生老病死是人生常态，“死去元知万事空”，人走了之后，能留下什么吗？你又能带走什么吗？这确实是值得认真思考的问题。或许你思考玩后，也会像我一样，觉得人生没有太大意义，普通人不过是社会运转里面的一个小齿轮，只能按照既定的大框架活动。而你的一生，或许对社会的影响和意义微乎其微——你赚钱了，成家了，离世了，只有和你比较亲近的人才能知道。对于其他人，绝大部分人，毫无感知。或许人生的意义就是安安稳稳，在一个城市，一个固定的圈子过完一生？但是在我看来未免有些太过无聊了，我是希望我的人生更加充实，有趣一点，所以感觉当下的生活真的很“空”。又或许，对于我来说，人生真正有意义的，是过自己想要的生活，而不是被其他人的思想观念左右，干自己真正热爱的事情，认识真正志同道合的朋友，混迹自己真正喜欢的社区，这，才是我想要的生活。可惜现实很骨感，基本上没有太多的机会可以实现这样的生活，只能按照既定的大框架生活，那对我来说，确实就是“空”了用兴趣填补空虚，这算是我目前对抗这种感觉最好的途径了，所以我才会想着搞一个个人网站，学一下建模，玩一些有意思的游戏来体验一下现实中体验不到的世界......对未来的展望展望未来的话，我觉得还是相对比较期待的吧。毕竟准备就要去迎接酒吧舞研究生的生活，能去读酒吧舞，也算是对我当年高考失利最大的弥补了，算是迟到的愿望实现吧。别的就没有太多展望了，只是希望研究生的生活能够顺顺利利完成研究工作，毕业后能够找到薪酬待遇不错的工作，如果足够幸运的话，能够找到一个真正的“二次元”女友，有共同的兴趣爱好，这样的话生活会有趣不少。以及，继续推进我那些兴趣为导向的知识学习：Blender建模，动画制作服务器运维网页前端兴趣向编程和开发，例如学Go和Python年度回顾大概就这么多吧，今天是平安夜，那就祝大家圣诞节快乐！
断网自动重连！用Go实现广西大学校园网断网自动登录
广西大学校园网断网自动重连程序因为广西某211高校校园网有时候会不稳定波动，导致设备掉线。而如果在需要挂机进行一些工作的场合出现这个问题，将会相当蛋疼。再加上在宿舍每天晚上断网后，第二天都要手动重新登录着实有点麻烦，于是乎我就用我入门级中的入门级Go编程能力写了这个程序。程序实现登录方法查找因为学校推荐是使用浏览器打开认真网页进行登录，而经过抓包后，我发现相比于大一的时候，校园网的网页传输数据进行了加密。也就是，如果我要通过逆向网页JavaScript脚本获得登录请求发送逻辑，代价有点大。不过好在你西还是关照到了无GUI Linux服务器使用校园网的问题，在官网挂出了登录方法：linux系统宽带客户端-2024.12.30日后使用这就好办了嘛，只需要构造这个URL请求：http://172.17.0.2:801/eportal/portal/login?callback=dr1003&login_method=1&user_account=账号@cmcc&user_password=密码&wlan_user_ip=终端IP&wlan_user_ipv6=&wlan_user_mac=000000000000&wlan_ac_ip=&wlan_ac_name=&jsVersion=4.2.1&terminal_type=1&lang=zh-cn&v=5574&lang=zh发送给认证服务器就可以登录校园网了。不过我注意到了这个请求URL里面，MAC填写了000000000000，按理来说这部分应该填写设备的MAC，用于保持登录状态和后续无感知登录。这样填写无效的MAC后面大概率会因为校园网波动或者短暂断联掉登录，所以在我的程序逻辑上，这部分应该补上。现在需要发送的校园网认证信息就理清楚了：账号密码线路类型设备IP设备MAC我只需要写一个程序获取到以上信息，然后持续不断判断网络通断状态，当检测到断网后构造认证的URL并发送，就可以实现校园网断网自动重连了。用Go实现校园网自动登录其实之前我就用Python写过一版校园网自动登录程序，但是因为Python的效率和占用确实有点感人了，所以我觉得还是选择编译型语言来进行构建吧。好巧不巧之前学了一些Golang，要不就拿这个试试看？实现思路因为Go是编译型语言，登录的配置就不能像Python那样让用户打开源代码写在对应的变量里面了，得写一个配置文件机制。也就是首次运行创建一个文本的配置文件，然后用户填写了相关的信息，再运行程序，程序从配置文件读取信息并且执行校园网认证和断网自动重连工作。那这样看，实现思路就很清晰了：程序首次运行，输出配置文件模板，提示用户填写用户填写好后，读取配置文件并进行基础的校验，确保参数填写无误实现校园网登录认证的函数，网络通断检测函数，以及上网时段判断函数避免在非上网时段反复尝试重连对于配置文件的部分，我想采用类似ini配置文件的格式，这是模板：# 校园网登录脚本信息设置：（注意请不要改变格式） # 用户名：（填写示例：User=1807210721） User= # 密码：（填写示例：Password=www.nekopara.uk） Password= # 运营商选择，留空选择校园网，如果需要选择运营商，电信填写telecom，联通填写unicom，移动填写cmcc Net_Type= # 是否开启学生上网时段模式？1为开启，0为关闭，开启后周一到周五0:00-6:00将不会尝试重连 Student_Mode=0 # 开启路由器登录模式： # 如果填写以下两个参数（均非空），则使用指定的路由器IP和MAC进行认证。 # 否则使用本机IP和MAC。 # 示例： # Router_IP=172.16.6.6 # Router_MAC=36:88:8A:99:A4:CC Router_IP= Router_MAC=考虑到为了便于小白使用，我输出的是.txt文件，可以直接记事本打开。以及，添加一个命令行启动参数支持，可以让程序作为服务运行和部署，例如：./GXU_Net_AutoLogin -user 1807210721 -passwd mypassword -nettype telecom -studentmode -ip 172.16.6.6 -mac 36:88:8A:99:A4:CC现在技术架构想清楚了，开始编程吧！实现代码package main import ( "bufio" "fmt" "os" "strings" "net/http" "net" "net/url" "time" "io" "flag" ) const configFileName = "config.txt" // Config 结构体保存配置 type Config struct { User string Password string NetType string // 新增字段 StudentMode bool // 路由器模式（当两者都非空时启用） RouterIP string RouterMAC string } // loadConfig 加载或创建配置文件 func loadConfig() (*Config, error) { // 检查文件是否存在 if _, err := os.Stat(configFileName); os.IsNotExist(err) { // 创建默认模板 defaultContent := `# 校园网登陆脚本信息设置：（注意请不要改变格式） # 用户名：（填写示例：User=1807210721） User= # 密码：（填写示例：Password=www.nekopara.uk） Password= # 运营商选择，留空选择校园网，如果需要选择运营商，电信填写telecom，联通填写unicom，移动填写cmcc Net_Type= # 是否开启学生上网时段模式？1为开启，0为关闭，开启后周一到周五0:00-6:00将不会尝试重连 Student_Mode=0 # 开启路由器登陆模式： # 如果填写以下两个参数（均非空），则使用指定的路由器IP和MAC进行认证。 # 否则使用本机IP和MAC。 # 示例： # Router_IP=172.16.6.6 # Router_MAC=36:88:8A:99:A4:CC Router_IP= Router_MAC= ` err = os.WriteFile(configFileName, []byte(defaultContent), 0644) if err != nil { return nil, fmt.Errorf("无法创建配置文件: %v", err) } return nil, fmt.Errorf("未找到配置文件，配置文件 '%s' 已创建，请先填写上网信息后重新运行程序", configFileName) } // 读取并解析 content, err := os.ReadFile(configFileName) if err != nil { return nil, fmt.Errorf("无法读取配置文件: %v", err) } cfg := &Config{} scanner := bufio.NewScanner(strings.NewReader(string(content))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) // 跳过空行和注释 if line == "" || strings.HasPrefix(line, "#") { continue } // 按第一个 '=' 分割（避免密码含等号出错） parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue // 格式错误，跳过 } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) switch key { case "User": cfg.User = value case "Password": cfg.Password = value case "Net_Type": cfg.NetType = value // 新增这一行 case "Student_Mode": cfg.StudentMode = (value == "1") case "Router_IP": cfg.RouterIP = value case "Router_MAC": cfg.RouterMAC = value } } if err := scanner.Err(); err != nil { return nil, err } // 基础校验 if cfg.User == "" || cfg.Password == "" { return nil, fmt.Errorf("请在 '%s' 中填写用户名和密码", configFileName) } // 在 loadConfig 函数中，解析配置后添加： if cfg.NetType != "" { // 检查是否是合法的运营商 valid := false switch strings.ToLower(cfg.NetType) { case "telecom", "unicom", "cmcc": valid = true } if !valid { return nil, fmt.Errorf("错误：运营商类型必须为空、telecom、unicom或cmcc（不区分大小写），当前值: %s", cfg.NetType) } } return cfg, nil } func getLocalIP() (string, error) { conn, err := net.Dial("udp", "8.8.8.8:80") if err != nil { return "", err } defer conn.Close() return conn.LocalAddr().(*net.UDPAddr).IP.String(), nil } func getMACAddress() (string, error) { interfaces, err := net.Interfaces() if err != nil { return "", err } for _, iface := range interfaces { if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { continue } mac := iface.HardwareAddr.String() if mac == "" { continue } addrs, _ := iface.Addrs() for _, addr := range addrs { if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil { return mac, nil } } } return "", fmt.Errorf("no active network interface with MAC found") } func isNetworkOK() bool { client := &http.Client{ Timeout: 1 * time.Second, } resp, err := client.Get("http://connect.rom.miui.com/generate_204") if err != nil { return false // 网络不通 / DNS 故障 / 超时 } defer resp.Body.Close() return resp.StatusCode == 204 } func login(cfg *Config, ip, mac string) { // 格式化 MAC：去掉冒号，转小写（适配你 bash 脚本的行为） cleanMAC := strings.ReplaceAll(strings.ToLower(mac), ":", "") userAccount := cfg.User if cfg.NetType != "" { userAccount = cfg.User + "@" + cfg.NetType } params := url.Values{ "callback": {"dr1003"}, "login_method": {"1"}, "user_account": {userAccount}, "user_password": {cfg.Password}, "wlan_user_ip": {ip}, "wlan_user_mac": {cleanMAC}, "wlan_user_ipv6": {""}, "wlan_ac_ip": {""}, "wlan_ac_name": {""}, "jsVersion": {"4.2.1"}, "terminal_type": {"1"}, "lang": {"zh-cn"}, "v": {"5574"}, } loginURL := "http://172.17.0.2:801/eportal/portal/login?" + params.Encode() resp, err := http.Get(loginURL) if err != nil { fmt.Printf("❌ 登录请求失败: %v\n", err) return } defer resp.Body.Close() // 读取并打印响应体 body, err := io.ReadAll(resp.Body) if err != nil { fmt.Printf("❌ 读取响应体失败: %v\n", err) return } bodyStr := string(body) fmt.Printf("✅ 已发送登录请求（HTTP状态码: %d）\n", resp.StatusCode) fmt.Printf("响应内容: %s\n", bodyStr) } func shouldSkipLogin(cfg *Config) bool { if !cfg.StudentMode { return false } now := time.Now() weekday := now.Weekday() // Sunday = 0, Monday = 1, ..., Friday = 5 hour := now.Hour() // 周一到周五（1~5），且 0:00 ~ 5:59 if weekday >= time.Monday && weekday <= time.Friday && hour >= 0 && hour < 6 { fmt.Println("🌙 学生模式：当前为禁网时段，暂停重连") return true } return false } func getLoginInfo(cfg *Config) (ip, mac string, err error) { // 如果启用了路由器模式（两个字段都非空） if cfg.RouterIP != "" && cfg.RouterMAC != "" { fmt.Println("🌐 使用路由器模式进行认证") return cfg.RouterIP, cfg.RouterMAC, nil } // 否则使用本机信息 fmt.Println("💻 使用本机模式进行认证") ip, err = getLocalIP() if err != nil { return "", "", fmt.Errorf("获取本机IP失败: %w", err) } mac, err = getMACAddress() if err != nil { return "", "", fmt.Errorf("获取本机MAC失败: %w", err) } return ip, mac, nil } func printHelp() { fmt.Println(`广西大学校园网自动登陆程序参数说明： 必须参数： -user 用户名（必须提供） -passwd 密码（必须提供） 可选参数： -nettype 运营商类型（telecom, unicom, cmcc），不加参数则使用校园网 -studentmode 启用学生模式（不带值） -ip 路由器IP（必须与-mac一起使用） -mac 路由器MAC（必须与-ip一起使用） -help 显示此帮助信息 示例（Linux）： ./GXU_Net_AutoLogin -user 1807210721 -passwd mypassword /opt/GXU_Net_AutoLogin/GXU_Net_AutoLogin -user 1807210721 -passwd mypassword -nettype telecom -studentmode ./GXU_Net_AutoLogin -user 1807210721 -passwd mypassword -ip 172.16.6.6 -mac 36:88:8A:99:A4:CC 示例（Windows）： GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword C:\\Program Files\\GXU_Net_AutoLogin\\GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword -nettype telecom -studentmode C:\\Program Files\\GXU_Net_AutoLogin\\GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword -ip 172.16.6.6 -mac 36:88:8A:99:A4:CC `) } func main() { fmt.Printf("🚀广西大学校园网自动登陆程序 By：GTX690战术核显卡导弹（www.nekopara.uk）\n") // 定义命令行参数 var ( user string passwd string nettype string studentMode bool ip string mac string help bool ) flag.StringVar(&user, "user", "", "用户名") flag.StringVar(&passwd, "passwd", "", "密码") flag.StringVar(&nettype, "nettype", "", "运营商类型（telecom, unicom, cmcc）") flag.BoolVar(&studentMode, "studentmode", false, "启用学生模式") flag.StringVar(&ip, "ip", "", "路由器IP（必须与-mac一起使用）") flag.StringVar(&mac, "mac", "", "路由器MAC（必须与-ip一起使用）") flag.BoolVar(&help, "help", false, "显示帮助信息") flag.Parse() // 显示帮助信息 if help { printHelp() os.Exit(0) } // 检查必须参数 if (user == "" && passwd == "") { // 从配置文件加载 cfg, err := loadConfig() if err != nil { fmt.Println("❌ 错误:", err) fmt.Println("💡 请编辑 config.txt 后重新运行本程序。") os.Exit(1) } fmt.Printf("✅ 配置加载成功！\n") fmt.Printf("用户: %s\n", cfg.User) fmt.Printf("密码: %s\n", cfg.Password) fmt.Printf("运营商: %s\n", cfg.NetType) fmt.Printf("学生模式: %t\n", cfg.StudentMode) if cfg.RouterIP != "" && cfg.RouterMAC != "" { fmt.Printf("路由器模式: IP=%s, MAC=%s\n", cfg.RouterIP, cfg.RouterMAC) } // 修复：将配置文件中的值赋给命令行变量 user = cfg.User passwd = cfg.Password nettype = cfg.NetType studentMode = cfg.StudentMode ip = cfg.RouterIP mac = cfg.RouterMAC } else if user != "" && passwd != "" { // 从命令行参数加载 cfg := &Config{ User: user, Password: passwd, NetType: nettype, StudentMode: studentMode, RouterIP: ip, RouterMAC: mac, } // 校验运营商类型 if nettype != "" { valid := false switch strings.ToLower(nettype) { case "telecom", "unicom", "cmcc": valid = true } if !valid { fmt.Printf("❌ 错误：运营商类型必须为telecom, unicom, cmcc（不区分大小写），当前值: %s\n", nettype) os.Exit(1) } } // 校验路由器IP/MAC if (ip != "" && mac == "") || (ip == "" && mac != "") { fmt.Println("❌ 错误：必须同时提供ip和mac参数，两者缺一不可") os.Exit(1) } // 显示配置 fmt.Printf("✅ 命令行参数加载成功！\n") fmt.Printf("用户: %s\n", cfg.User) fmt.Printf("密码: %s\n", cfg.Password) fmt.Printf("运营商: %s\n", cfg.NetType) fmt.Printf("学生模式: %t\n", cfg.StudentMode) if cfg.RouterIP != "" && cfg.RouterMAC != "" { fmt.Printf("路由器模式: IP=%s, MAC=%s\n", cfg.RouterIP, cfg.RouterMAC) } // 使用命令行参数配置 cfg.User = user cfg.Password = passwd cfg.NetType = nettype cfg.StudentMode = studentMode cfg.RouterIP = ip cfg.RouterMAC = mac } else { // 只提供了其中一个参数 fmt.Println("❌ 错误：必须同时提供user和passwd参数，或者都不提供（通过配置文件）") fmt.Println("💡 请使用 -help 查看参数说明") os.Exit(1) } // 获取用于登录的 IP 和 MAC（自动判断模式） ipAddr, macAddr, err := getLoginInfo(&Config{ RouterIP: ip, RouterMAC: mac, }) if err != nil { fmt.Printf("❌ %v\n", err) os.Exit(1) } fmt.Printf("✅ 守护进程启动：认证IP=%s | 认证MAC=%s\n", ipAddr, macAddr) // 主循环 for { if shouldSkipLogin(&Config{ StudentMode: studentMode, }) { time.Sleep(1 * time.Second) continue } if !isNetworkOK() { fmt.Println("⚠️ 检测到断网，正在重新登录...") login(&Config{ User: user, Password: passwd, NetType: nettype, }, ipAddr, macAddr) } time.Sleep(1 * time.Second) } }代码详细解释下面我将详细解释代码中每个函数的作用，帮助你理解这个校园网自动重连程序的工作原理。这些函数都是程序的核心逻辑，我将按照从配置加载到网络检测再到认证的流程进行说明。1. loadConfig()func loadConfig() (*Config, error) { // 检查文件是否存在 if _, err := os.Stat(configFileName); os.IsNotExist(err) { // 创建默认模板 defaultContent := `# 校园网登录脚本信息设置：（注意请不要改变格式） # 用户名：（填写示例：User=1807210721） User= # 密码：（填写示例：Password=www.nekopara.uk） Password= # 运营商选择，留空选择校园网，如果需要选择运营商，电信填写telecom，联通填写unicom，移动填写cmcc Net_Type= # 是否开启学生上网时段模式？1为开启，0为关闭，开启后周一到周五0:00-6:00将不会尝试重连 Student_Mode=0 # 开启路由器登录模式： # 如果填写以下两个参数（均非空），则使用指定的路由器IP和MAC进行认证。 # 否则使用本机IP和MAC。 # 示例： # Router_IP=172.16.6.6 # Router_MAC=36:88:8A:99:A4:CC Router_IP= Router_MAC= ` err = os.WriteFile(configFileName, []byte(defaultContent), 0644) if err != nil { return nil, fmt.Errorf("无法创建配置文件: %v", err) } return nil, fmt.Errorf("未找到配置文件，配置文件 '%s' 已创建，请先填写上网信息后重新运行程序", configFileName) } // 读取并解析 content, err := os.ReadFile(configFileName) if err != nil { return nil, fmt.Errorf("无法读取配置文件: %v", err) } cfg := &Config{} scanner := bufio.NewScanner(strings.NewReader(string(content))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) // 跳过空行和注释 if line == "" || strings.HasPrefix(line, "#") { continue } // 按第一个 '=' 分割（避免密码含等号出错） parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue // 格式错误，跳过 } key := strings.TrimSpace(parts[0]) value := strings.TrimSpace(parts[1]) switch key { case "User": cfg.User = value case "Password": cfg.Password = value case "Net_Type": cfg.NetType = value // 新增这一行 case "Student_Mode": cfg.StudentMode = (value == "1") case "Router_IP": cfg.RouterIP = value case "Router_MAC": cfg.RouterMAC = value } } if err := scanner.Err(); err != nil { return nil, err } // 基础校验 if cfg.User == "" || cfg.Password == "" { return nil, fmt.Errorf("请在 '%s' 中填写用户名和密码", configFileName) } // 在 loadConfig 函数中，解析配置后添加： if cfg.NetType != "" { // 检查是否是合法的运营商 valid := false switch strings.ToLower(cfg.NetType) { case "telecom", "unicom", "cmcc": valid = true } if !valid { return nil, fmt.Errorf("错误：运营商类型必须为空、telecom、unicom或cmcc（不区分大小写），当前值: %s", cfg.NetType) } } return cfg, nil }函数解释：loadConfig() 是程序的配置加载核心函数。它的作用是检查是否存在配置文件，如果不存在则创建一个默认配置文件模板，让用户填写必要信息；如果存在则解析配置文件内容并返回配置结构体。关键点：首次运行时会创建config.txt文件，包含详细的填写说明使用bufio.Scanner逐行解析配置文件，跳过注释和空行对配置项进行基础校验，确保用户名和密码必填对运营商类型进行合法性检查（必须是telecom、unicom或cmcc）如果配置不合法，会返回错误信息提示用户这个函数是程序与用户交互的第一步，也是保证后续程序能正确运行的基础。2. getLocalIP()func getLocalIP() (string, error) { conn, err := net.Dial("udp", "8.8.8.8:80") if err != nil { return "", err } defer conn.Close() return conn.LocalAddr().(*net.UDPAddr).IP.String(), nil }函数解释：getLocalIP() 用于获取当前设备的本地IP地址。这个函数通过与公共DNS服务器（8.8.8.8）建立UDP连接，然后获取连接的本地地址。关键点：使用net.Dial建立UDP连接，连接到Google DNS服务器（8.8.8.8:80）通过conn.LocalAddr()获取连接的本地IP地址这是获取本机IP的常用方法，避免了遍历所有网络接口的复杂性返回的IP地址是字符串格式，例如"192.168.1.100"这个函数在获取本机IP时非常实用，避免了手动遍历网络接口的麻烦。3. getMACAddress()func getMACAddress() (string, error) { interfaces, err := net.Interfaces() if err != nil { return "", err } for _, iface := range interfaces { if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 { continue } mac := iface.HardwareAddr.String() if mac == "" { continue } addrs, _ := iface.Addrs() for _, addr := range addrs { if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() && ipnet.IP.To4() != nil { return mac, nil } } } return "", fmt.Errorf("no active network interface with MAC found") }函数解释：getMACAddress() 用于获取当前设备的MAC地址。它会遍历所有网络接口，找到第一个非回环、启用的接口，并返回其MAC地址。关键点：使用net.Interfaces()获取所有网络接口信息过滤掉回环接口（FlagLoopback）和未启用的接口（FlagUp）从接口的硬件地址（HardwareAddr）获取MAC地址遍历接口的IP地址，确保返回的是有效网络接口的MAC返回的MAC格式如"36:88:8A:99:A4:CC"这个函数在获取本机MAC地址时非常关键，因为校园网认证需要MAC地址来保持登录状态。4. isNetworkOK()func isNetworkOK() bool { client := &http.Client{ Timeout: 1 * time.Second, } resp, err := client.Get("http://connect.rom.miui.com/generate_204") if err != nil { return false // 网络不通 / DNS 故障 / 超时 } defer resp.Body.Close() return resp.StatusCode == 204 }函数解释：isNetworkOK() 用于检测当前网络连接状态。它通过HTTP请求访问一个特殊URL（http://connect.rom.miui.com/generate_204），根据响应状态码判断网络是否通畅。关键点：使用http.Client发送GET请求，设置1秒超时请求的URL是MIUI系统用于检测网络连接的特殊地址如果响应状态码为204，表示网络通畅；否则表示断网这个检测方法非常轻量，不会产生太多网络流量通过defer resp.Body.Close()确保资源正确释放这个函数是程序检测断网的核心，决定了是否需要触发重连逻辑。5. login()func login(cfg *Config, ip, mac string) { // 格式化 MAC：去掉冒号，转小写（适配你 bash 脚本的行为） cleanMAC := strings.ReplaceAll(strings.ToLower(mac), ":", "") userAccount := cfg.User if cfg.NetType != "" { userAccount = cfg.User + "@" + cfg.NetType } params := url.Values{ "callback": {"dr1003"}, "login_method": {"1"}, "user_account": {userAccount}, "user_password": {cfg.Password}, "wlan_user_ip": {ip}, "wlan_user_mac": {cleanMAC}, "wlan_user_ipv6": {""}, "wlan_ac_ip": {""}, "wlan_ac_name": {""}, "jsVersion": {"4.2.1"}, "terminal_type": {"1"}, "lang": {"zh-cn"}, "v": {"5574"}, } loginURL := "http://172.17.0.2:801/eportal/portal/login?" + params.Encode() resp, err := http.Get(loginURL) if err != nil { fmt.Printf("❌ 登录请求失败: %v\n", err) return } defer resp.Body.Close() // 读取并打印响应体 body, err := io.ReadAll(resp.Body) if err != nil { fmt.Printf("❌ 读取响应体失败: %v\n", err) return } bodyStr := string(body) fmt.Printf("✅ 已发送登录请求（HTTP状态码: %d）\n", resp.StatusCode) fmt.Printf("响应内容: %s\n", bodyStr) }函数解释：login() 是校园网认证的核心函数，负责构造认证请求并发送给认证服务器。关键点：对MAC地址进行格式化：去掉冒号，转为小写根据运营商类型（NetType）构造用户名（例如1807210721@cmcc）使用url.Values构建查询参数，确保参数编码正确拼接完整的认证URL（http://172.17.0.2:801/...）发送HTTP GET请求到认证服务器打印请求结果和响应内容，便于调试通过params.Encode()正确编码URL参数这个函数实现了校园网认证的关键逻辑，是整个程序的核心功能。6. shouldSkipLogin()func shouldSkipLogin(cfg *Config) bool { if !cfg.StudentMode { return false } now := time.Now() weekday := now.Weekday() // Sunday = 0, Monday = 1, ..., Friday = 5 hour := now.Hour() // 周一到周五（1~5），且 0:00 ~ 5:59 if weekday >= time.Monday && weekday <= time.Friday && hour >= 0 && hour < 6 { fmt.Println("🌙 学生模式：当前为禁网时段，暂停重连") return true } return false }函数解释：shouldSkipLogin() 用于判断在特定时段是否应该跳过登录尝试。这个函数实现了"学生模式"，即在周一到周五的0:00-6:00期间不尝试重连。关键点：仅在StudentMode为true时生效获取当前时间的星期几和小时判断是否为周一至周五的凌晨时段（0:00-5:59）如果是禁网时段，打印提示并返回true，表示应该跳过登录否则返回false，表示可以尝试登录这个函数是为了解决学生账号在凌晨时段断网后反复尝试自动重连的问题，避免持续不断发起无意义的请求。7. getLoginInfo()func getLoginInfo(cfg *Config) (ip, mac string, err error) { // 如果启用了路由器模式（两个字段都非空） if cfg.RouterIP != "" && cfg.RouterMAC != "" { fmt.Println("🌐 使用路由器模式进行认证") return cfg.RouterIP, cfg.RouterMAC, nil } // 否则使用本机信息 fmt.Println("💻 使用本机模式进行认证") ip, err = getLocalIP() if err != nil { return "", "", fmt.Errorf("获取本机IP失败: %w", err) } mac, err = getMACAddress() if err != nil { return "", "", fmt.Errorf("获取本机MAC失败: %w", err) } return ip, mac, nil }函数解释：getLoginInfo() 用于确定认证时使用的IP和MAC地址。它会根据配置决定是使用路由器的IP/MAC还是本机的IP/MAC。关键点：如果配置了Router_IP和Router_MAC，则使用路由器的IP和MAC否则，使用本机的IP和MAC（通过调用getLocalIP()和getMACAddress()）返回的IP和MAC格式符合校园网认证要求在使用路由器模式时，会打印提示信息这个函数是程序灵活应对不同网络环境和部署需求的关键，既适配在教室本机连接校园网时的认证，又适配在宿舍使用路由器共享上网的场景。8. printHelp()func printHelp() { fmt.Println(`广西大学校园网自动登录程序参数说明： 必须参数： -user 用户名（必须提供） -passwd 密码（必须提供） 可选参数： -nettype 运营商类型（telecom, unicom, cmcc），不加参数则使用校园网 -studentmode 启用学生模式（不带值） -ip 路由器IP（必须与-mac一起使用） -mac 路由器MAC（必须与-ip一起使用） -help 显示此帮助信息 示例（Linux）： ./GXU_Net_AutoLogin -user 1807210721 -passwd mypassword /opt/GXU_Net_AutoLogin/GXU_Net_AutoLogin -user 1807210721 -passwd mypassword -nettype telecom -studentmode ./GXU_Net_AutoLogin -user 1807210721 -passwd mypassword -ip 172.16.6.6 -mac 36:88:8A:99:A4:CC 示例（Windows）： GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword C:\\Program Files\\GXU_Net_AutoLogin\\GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword -nettype telecom -studentmode C:\\Program Files\\GXU_Net_AutoLogin\\GXU_Net_AutoLogin.exe -user 1807210721 -passwd mypassword -ip 172.16.6.6 -mac 36:88:8A:99:A4:CC `) }函数解释：printHelp() 用于打印程序的使用帮助信息，包括参数说明和使用示例。关键点：提供了详细的参数说明，包括必须参数和可选参数列举了Linux和Windows平台的使用示例清晰地说明了各参数的作用便于用户快速了解如何使用程序这个函数在用户输入-help参数时调用，是程序的文档部分，帮助用户正确使用程序。9. main()func main() { fmt.Printf("🚀广西大学校园网自动登陆程序 By：GTX690战术核显卡导弹（www.nekopara.uk）\n") // 定义命令行参数 var ( user string passwd string nettype string studentMode bool ip string mac string help bool ) flag.StringVar(&user, "user", "", "用户名") flag.StringVar(&passwd, "passwd", "", "密码") flag.StringVar(&nettype, "nettype", "", "运营商类型（telecom, unicom, cmcc）") flag.BoolVar(&studentMode, "studentmode", false, "启用学生模式") flag.StringVar(&ip, "ip", "", "路由器IP（必须与-mac一起使用）") flag.StringVar(&mac, "mac", "", "路由器MAC（必须与-ip一起使用）") flag.BoolVar(&help, "help", false, "显示帮助信息") flag.Parse() // 显示帮助信息 if help { printHelp() os.Exit(0) } // 检查必须参数 if (user == "" && passwd == "") { // 从配置文件加载 cfg, err := loadConfig() if err != nil { fmt.Println("❌ 错误:", err) fmt.Println("💡 请编辑 config.txt 后重新运行本程序。") os.Exit(1) } fmt.Printf("✅ 配置加载成功！\n") fmt.Printf("用户: %s\n", cfg.User) fmt.Printf("密码: %s\n", cfg.Password) fmt.Printf("运营商: %s\n", cfg.NetType) fmt.Printf("学生模式: %t\n", cfg.StudentMode) if cfg.RouterIP != "" && cfg.RouterMAC != "" { fmt.Printf("路由器模式: IP=%s, MAC=%s\n", cfg.RouterIP, cfg.RouterMAC) } // 修复：将配置文件中的值赋给命令行变量 user = cfg.User passwd = cfg.Password nettype = cfg.NetType studentMode = cfg.StudentMode ip = cfg.RouterIP mac = cfg.RouterMAC } else if user != "" && passwd != "" { // 从命令行参数加载 cfg := &Config{ User: user, Password: passwd, NetType: nettype, StudentMode: studentMode, RouterIP: ip, RouterMAC: mac, } // 校验运营商类型 if nettype != "" { valid := false switch strings.ToLower(nettype) { case "telecom", "unicom", "cmcc": valid = true } if !valid { fmt.Printf("❌ 错误：运营商类型必须为telecom, unicom, cmcc（不区分大小写），当前值: %s\n", nettype) os.Exit(1) } } // 校验路由器IP/MAC if (ip != "" && mac == "") || (ip == "" && mac != "") { fmt.Println("❌ 错误：必须同时提供ip和mac参数，两者缺一不可") os.Exit(1) } // 显示配置 fmt.Printf("✅ 命令行参数加载成功！\n") fmt.Printf("用户: %s\n", cfg.User) fmt.Printf("密码: %s\n", cfg.Password) fmt.Printf("运营商: %s\n", cfg.NetType) fmt.Printf("学生模式: %t\n", cfg.StudentMode) if cfg.RouterIP != "" && cfg.RouterMAC != "" { fmt.Printf("路由器模式: IP=%s, MAC=%s\n", cfg.RouterIP, cfg.RouterMAC) } // 使用命令行参数配置 cfg.User = user cfg.Password = passwd cfg.NetType = nettype cfg.StudentMode = studentMode cfg.RouterIP = ip cfg.RouterMAC = mac } else { // 只提供了其中一个参数 fmt.Println("❌ 错误：必须同时提供user和passwd参数，或者都不提供（通过配置文件）") fmt.Println("💡 请使用 -help 查看参数说明") os.Exit(1) } // 获取用于登录的 IP 和 MAC（自动判断模式） ipAddr, macAddr, err := getLoginInfo(&Config{ RouterIP: ip, RouterMAC: mac, }) if err != nil { fmt.Printf("❌ %v\n", err) os.Exit(1) } fmt.Printf("✅ 守护进程启动：认证IP=%s | 认证MAC=%s\n", ipAddr, macAddr) // 主循环 for { if shouldSkipLogin(&Config{ StudentMode: studentMode, }) { time.Sleep(1 * time.Second) continue } if !isNetworkOK() { fmt.Println("⚠️ 检测到断网，正在重新登录...") login(&Config{ User: user, Password: passwd, NetType: nettype, }, ipAddr, macAddr) } time.Sleep(1 * time.Second) } }函数解释：main() 是程序的主入口函数，负责处理命令行参数、加载配置、启动守护进程并持续监控网络状态。关键点：使用flag包解析命令行参数支持两种配置方式：通过配置文件或命令行参数对配置进行校验，确保必要参数存在且格式正确根据配置决定使用路由器模式还是本机模式启动主循环，每秒检查网络状态当检测到断网时，调用login()函数进行重连在学生模式下，避免在特定时段重连这个函数是整个程序的"大脑"，协调所有其他函数的工作，实现断网自动重连的核心功能。使用方法你既可以从上面的源代码编译使用，也可以下载我编译好的程序。如果不放心我分发的二进制文件，要自己编译来用的话，把上面那段完整的代码保存为main.go，安装好Go语言环境，然后进行编译：go build -ldflags="-s -w" -o GXU_Net_AutoLogin main.go 或者直接运行也行：go run main.go二进制文件的话，我就直接只编译了win32的程序：GXU_Net_AutoLogin.zip对于Linux的话，我相信使用Linux的你应该知道怎么配置Go环境，源代码放在这了，你就自己编译去吧（其实真不难）。或许可以用wine运行？（bushi补充：其他常见系统架构文件已编译，文件位于Github的Release相关源代码已经放到Github仓库：github.com/Chocola-X/GXU-Net-AutoLogin
阿里云 ESA 回源 IP 段分享，可用于源站保护
利用B站IP查询API更新访客记录IP地理信息偶然的一次机会，在群里面聊天的时候，群友指出纯真数据库的数据并不准确，并且说有一个可以免费调用的IP地理信息库——B站的IP查询API。据说还是用的ipip.net的企业版，这下得狠狠白嫖了。前置任务：修复VisitorLoggerPro插件BUG如果使用过这个插件一段时间，你就应该发现了，在国家统计图表这项，非常的混乱，似乎是只要记录的字符不一致，就会被当作不同的国家处理，例如美国加利福尼亚洛杉矶和美国纽约就被当作不同的国家列在上面。经过深挖代码斗智斗勇，我发现这个问题我无法从根本解决，因为整个代码太奇怪了，而且用的本地IP数据库也都是乱七八糟的，根本没有做好国家的细分地区分类，一股脑返回，导致所有的信息被集中在一起写进了国家字段。于是，我放弃治疗，直接在后面的调用代码中，增加分类逻辑进行分类：这是原来的代码，单独处理和提取了国内省份： foreach ($countryCountsResult as $row) { $countryName = $row['country'] ?: '未知'; $count = $row['count']; if (!isset($countryData[$countryName])) { $countryData[$countryName] = 0; $totalCountries++; } $countryData[$countryName] += $count; if (strpos($countryName, '中国') !== false) { foreach ($provinces as $province) { if (strpos($countryName, $province) !== false) { if (!isset($provinceData[$province])) { $provinceData[$province] = 0; } $provinceData[$province] += $count; break; } } } }我决定在返回国家信息的时候，进行提取合并并返回： foreach ($countryCountsResult as $row) { $rawCountry = $row['country'] ?? ''; $count = (int)($row['count'] ?? 0); if ($rawCountry === '') { $stdCountry = '未知'; } elseif (strpos($rawCountry, '中国') !== false) { // 所有含“中国”的都归为“中国” $stdCountry = '中国'; // 同时尝试提取省份 foreach ($provinces as $province) { if (strpos($rawCountry, $province) !== false) { if (!isset($provinceData[$province])) { $provinceData[$province] = 0; } $provinceData[$province] += $count; break; // 匹配到第一个即停止 } } } else { // 非中国：尝试匹配标准国家名（用于归一化，如“美国加州”→“美国”） $stdCountry = $rawCountry; // 默认保留原值 foreach ($countries as $country) { if (strpos($rawCountry, $country) !== false) { $stdCountry = $country; break; // 匹配到即停止（因列表已按长优先排序） } } } // 累计国家统计 if (!isset($countryData[$stdCountry])) { $countryData[$stdCountry] = 0; $totalCountries++; } $countryData[$stdCountry] += $count; }当然了，需要补上完整的国家地区列表： $countries = [ // 长名称国家（必须放前面） '波斯尼亚和黑塞哥维那', '赤道几内亚', '圣文森特和格林纳丁斯', '特立尼达和多巴哥', '圣多美和普林西比', '阿拉伯联合酋长国', '沙特阿拉伯', '巴布亚新几内亚', '所罗门群岛', '马绍尔群岛', '密克罗尼西亚联邦', '帕劳共和国', '瓦努阿图共和国', '基里巴斯共和国', '瑙鲁共和国', '图瓦卢', '美属萨摩亚', '英属维尔京群岛', '美属维尔京群岛', '北马里亚纳群岛', '福克兰群岛（马尔维纳斯）', '法属圭亚那', '法属波利尼西亚', '法属南部领地', '皮特凯恩群岛', '托克劳', '库克群岛', '纽埃', '新喀里多尼亚', '关岛', '百慕大', '开曼群岛', '安圭拉', '蒙特塞拉特', '荷属圣马丁', '法属圣马丁', '圣皮埃尔和密克隆', '阿森松岛', '特里斯坦-达库尼亚', // 主权国家（按字母拼音排序） '阿富汗', '阿尔巴尼亚', '阿尔及利亚', '安道尔', '安哥拉', '安提瓜和巴布达', '阿根廷', '亚美尼亚', '澳大利亚', '奥地利', '阿塞拜疆', '巴哈马', '巴林', '孟加拉国', '巴巴多斯', '白俄罗斯', '比利时', '伯利兹', '贝宁', '不丹', '玻利维亚', '博茨瓦纳', '巴西', '文莱', '保加利亚', '布基纳法索', '布隆迪', '柬埔寨', '喀麦隆', '加拿大', '佛得角', '中非', '乍得', '智利', '哥伦比亚', '科摩罗', '刚果（布）', '刚果（金）', '哥斯达黎加', '科特迪瓦', '克罗地亚', '古巴', '塞浦路斯', '捷克', '丹麦', '吉布提', '多米尼克', '多米尼加', '厄瓜多尔', '埃及', '萨尔瓦多', '赤道几内亚', '厄立特里亚', '爱沙尼亚', '斯威士兰', '埃塞俄比亚', '斐济', '芬兰', '法国', '加蓬', '冈比亚', '格鲁吉亚', '德国', '加纳', '希腊', '格林纳达', '危地马拉', '几内亚', '几内亚比绍', '圭亚那', '海地', '洪都拉斯', '匈牙利', '冰岛', '印度', '印度尼西亚', '伊朗', '伊拉克', '爱尔兰', '以色列', '意大利', '牙买加', '日本', '约旦', '哈萨克斯坦', '肯尼亚', '基里巴斯', '朝鲜', '韩国', '科威特', '吉尔吉斯斯坦', '老挝', '拉脱维亚', '黎巴嫩', '莱索托', '利比里亚', '利比亚', '列支敦士登', '立陶宛', '卢森堡', '马达加斯加', '马拉维', '马来西亚', '马尔代夫', '马里', '马耳他', '马绍尔群岛', '毛里塔尼亚', '毛里求斯', '墨西哥', '密克罗尼西亚', '摩尔多瓦', '摩纳哥', '蒙古', '黑山', '摩洛哥', '莫桑比克', '缅甸', '纳米比亚', '瑙鲁', '尼泊尔', '荷兰', '新西兰', '尼加拉瓜', '尼日尔', '尼日利亚', '北马其顿', '挪威', '阿曼', '巴基斯坦', '帕劳', '巴拿马', '巴布亚新几内亚', '巴拉圭', '秘鲁', '菲律宾', '波兰', '葡萄牙', '卡塔尔', '罗马尼亚', '俄罗斯', '卢旺达', '圣基茨和尼维斯', '圣卢西亚', '圣马力诺', '圣多美和普林西比', '沙特阿拉伯', '塞内加尔', '塞尔维亚', '塞舌尔', '塞拉利昂', '新加坡', '斯洛伐克', '斯洛文尼亚', '所罗门群岛', '索马里', '南非', '南苏丹', '西班牙', '斯里兰卡', '苏丹', '苏里南', '瑞典', '瑞士', '叙利亚', '塔吉克斯坦', '坦桑尼亚', '泰国', '东帝汶', '多哥', '汤加', '特立尼达和多巴哥', '突尼斯', '土耳其', '土库曼斯坦', '图瓦卢', '乌干达', '乌克兰', '阿联酋', '英国', '美国', '乌拉圭', '乌兹别克斯坦', '瓦努阿图', '梵蒂冈', '委内瑞拉', '越南', '也门', '赞比亚', '津巴布韦' ];虽然我感觉好像把朝鲜放进去并没有什么意义（）修复后的代码已经提交到了我的github仓库：github.com/Chocola-X/VisitorLoggerPro-Enhanced编写更新脚本脚本程序主要的任务就是从数据库里面获取IP，然后从查询API获取到IP的相关信息，再去更新本地记录。考虑到服务器已经是有php环境了，干脆就直接用php来写了。更新已记录IP的IP信息脚本#!/usr/bin/env php <?php // ========== 数据库配置 ========== $DB_HOST = 'localhost'; $DB_PORT = '3306'; $DB_NAME = 'db_name'; $DB_USER = 'db_user'; $DB_PASS = 'www.nekopara.uk'; // =============================== $API_URL_TEMPLATE = 'https://api.live.bilibili.com/ip_service/v1/ip_service/get_ip_addr?ip=%s'; $LOOKBACK_MINUTES = 600; //向前追溯更新的时间，单位分钟 $BATCH_SIZE = 800; //最大更新IP条数 $API_TIMEOUT = 2; $DEBUG = true; // 创建数据库连接 try { $pdo = new PDO( "mysql:host=$DB_HOST;port=$DB_PORT;dbname=$DB_NAME;charset=utf8mb4", $DB_USER, $DB_PASS, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); } catch (PDOException $e) { fwrite(STDERR, "❌ 数据库连接失败: " . $e->getMessage() . "\n"); exit(1); } function queryIpLocation($ip, $apiTemplate, $timeout = 5) { if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) { return null; } $url = sprintf($apiTemplate, urlencode($ip)); $context = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => $timeout, 'header' => "User-Agent: Neko-IP-Geo-Updater/1.0\r\nAccept: application/json\r\n" ] ]); $response = @file_get_contents($url, false, $context); if ($response === false) { return null; } $data = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) { return null; } if (($data['code'] ?? -1) !== 0) { return null; } if (!isset($data['data']) || !is_array($data['data'])) { return null; } $country = trim($data['data']['country'] ?? ''); $province = trim($data['data']['province'] ?? ''); $city = trim($data['data']['city'] ?? ''); $isp = trim($data['data']['isp'] ?? ''); // 构建去重后的部件列表 $parts = []; // 国家总是加入 if (!empty($country) && $country !== 'Unknown' && $country !== '未知') { $parts[] = $country; } // 省份：仅当不等于国家时才加入 if (!empty($province) && $province !== 'Unknown' && $province !== '未知' && $province !== $country) { $parts[] = $province; } // 城市：仅当不等于国家且不等于省份时才加入 if (!empty($city) && $city !== 'Unknown' && $city !== '未知' && $city !== $country && $city !== $province) { $parts[] = $city; } // ISP 总是加入（只要非空） if (!empty($isp) && $isp !== 'Unknown' && $isp !== '未知') { $parts[] = $isp; } // 去除可能的重复（保险起见再做一次 array_unique，保持顺序） $parts = array_values(array_unique($parts)); $location = implode(' ', $parts); return $location ?: 'Unknown'; } // ====================== // 主逻辑 // ====================== $isCli = php_sapi_name() === 'cli'; $log = function($msg) use ($isCli, $DEBUG) { if (!$DEBUG) return; $line = "[" . date('Y-m-d H:i:s') . "] $msg\n"; if ($isCli) { echo $line; } else { echo htmlspecialchars($line); } }; $log("🔍 开始处理最近 {$LOOKBACK_MINUTES} 分钟内的访客记录（将 country 字段替换为完整地理位置）..."); $stmt = $pdo->prepare(" SELECT id, ip, country FROM `typecho_visitor_log` WHERE `time` >= NOW() - INTERVAL ? MINUTE ORDER BY `time` ASC LIMIT ? "); $stmt->bindValue(1, (int)$LOOKBACK_MINUTES, PDO::PARAM_INT); $stmt->bindValue(2, (int)$BATCH_SIZE, PDO::PARAM_INT); $stmt->execute(); $records = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($records)) { $log("✅ 无符合条件的记录。"); exit(0); } $log("📥 找到 " . count($records) . " 条记录，开始查询 API..."); $updateStmt = $pdo->prepare(" UPDATE `typecho_visitor_log` SET `country` = ? WHERE `id` = ? "); $updated = 0; $failed = 0; foreach ($records as $record) { $id = $record['id']; $ip = $record['ip']; $oldCountry = $record['country']; $fullLocation = queryIpLocation($ip, $API_URL_TEMPLATE, $API_TIMEOUT); if ($fullLocation !== null) { $updateStmt->execute([$fullLocation, $id]); $updated++; $log("✅ ID=$id | IP=$ip | 替换为: \"$fullLocation\""); } else { $failed++; $log("❌ ID=$id | IP=$ip | API 查询失败（保留原值: \"$oldCountry\"）"); } } $log("📊 完成：更新 $updated 条，失败 $failed 条");这个脚本用于更新已经存在的IP记录信息，已经设置了相关的参数便于操作，下面是参数解释：$DB_HOST = 'localhost'; 这部分设置数据库地址，例如localhost或192.168.6.6$DB_PORT = '3306'; 这部分是数据库端口号，例如默认端口3306$DB_NAME = 'db_name'; 这部分设置数据库名称，和网站设置的保持一致即可$DB_USER = 'db_user'; 这部分设置数据库用户名，和网站设置的保持一致即可$DB_PASS = 'www.nekopara.uk'; 这部分设置数据库密码，和网站设置的保持一致即可$LOOKBACK_MINUTES = 600; 这部分是往前追溯更新时间设置，也就是设置更新你执行脚本的这个时间之前的多少分钟内的数据$BATCH_SIZE = 800;这部分是设置最大更新IP数量大小，防止IP太多导致长时间占用运行$API_TIMEOUT = 2; 这是API访问超时时间限制，设置2秒即可，如果网络不稳定可以改大一点把以上的脚本修改好对应信息后，保存为一个php文件（例如update_ip_info.php）丢到服务器的一个目录里面（例如/data/ip_info/），然后进入目录执行：php82 update_ip_info.php耐心等待数据更新完毕即可，如果设置数目较大会需要等待较长时间，可以考虑用tumx来挂着运行。自动更新记录脚本话说回来，我们总不能每次都手动上服务器执行这个更新脚本吧？所以，我们需要一个自动化，批量更新多个网站的脚本：<?php // ========== 多站点数据库配置 ========== $sites = [ [ 'name' => 'www.nekopara.uk', 'host' => 'localhost', 'port' => '3306', 'dbname' => 'typecho', 'user' => 'typecho', 'pass' => 'www.nekopara.uk', ], [ 'name' => 'demo.nekopara.uk', 'host' => 'localhost', 'port' => '3306', 'dbname' => 'demo_typecho', 'user' => 'demo_typecho', 'pass' => 'demo.nekopara.uk', ], // 可继续添加更多站点... ]; // ===================================== $API_URL_TEMPLATE = 'https://api.live.bilibili.com/ip_service/v1/ip_service/get_ip_addr?ip=%s'; $LOOKBACK_MINUTES = 10; $BATCH_SIZE = 500; $API_TIMEOUT = 2; $DEBUG = true; $isCli = php_sapi_name() === 'cli'; $log = function($msg) use ($isCli, $DEBUG) { if (!$DEBUG) return; $line = "[" . date('Y-m-d H:i:s') . "] $msg\n"; if ($isCli) { echo $line; } else { echo htmlspecialchars($line); } }; function queryIpLocation($ip, $apiTemplate, $timeout = 5) { if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) { return null; } $url = sprintf($apiTemplate, urlencode($ip)); $context = stream_context_create([ 'http' => [ 'method' => 'GET', 'timeout' => $timeout, 'header' => "User-Agent: Neko-IP-Geo-Updater/1.0\r\nAccept: application/json\r\n" ] ]); $response = @file_get_contents($url, false, $context); if ($response === false) { return null; } $data = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE || !is_array($data)) { return null; } if (($data['code'] ?? -1) !== 0) { return null; } if (!isset($data['data']) || !is_array($data['data'])) { return null; } $country = trim($data['data']['country'] ?? ''); $province = trim($data['data']['province'] ?? ''); $city = trim($data['data']['city'] ?? ''); $isp = trim($data['data']['isp'] ?? ''); // 构建去重后的部件列表 $parts = []; // 国家总是加入 if (!empty($country) && $country !== 'Unknown' && $country !== '未知') { $parts[] = $country; } // 省份：仅当不等于国家时才加入 if (!empty($province) && $province !== 'Unknown' && $province !== '未知' && $province !== $country) { $parts[] = $province; } // 城市：仅当不等于国家且不等于省份时才加入 if (!empty($city) && $city !== 'Unknown' && $city !== '未知' && $city !== $country && $city !== $province) { $parts[] = $city; } // ISP 总是加入（只要非空） if (!empty($isp) && $isp !== 'Unknown' && $isp !== '未知') { $parts[] = $isp; } // 去除可能的重复（保险起见再做一次 array_unique，保持顺序） $parts = array_values(array_unique($parts)); $location = implode(' ', $parts); return $location ?: 'Unknown'; } foreach ($sites as $site) { $log("🌐 开始处理站点: {$site['name']} (数据库: {$site['dbname']})"); try { $pdo = new PDO( "mysql:host={$site['host']};port={$site['port']};dbname={$site['dbname']};charset=utf8mb4", $site['user'], $site['pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); } catch (PDOException $e) { $log("❌ 连接失败 ({$site['name']}): " . $e->getMessage()); continue; // 跳过这个站点，继续下一个 } // 查询近期记录 $stmt = $pdo->prepare(" SELECT id, ip, country FROM `typecho_visitor_log` WHERE `time` >= NOW() - INTERVAL ? MINUTE ORDER BY `time` ASC LIMIT ? "); $stmt->bindValue(1, (int)$LOOKBACK_MINUTES, PDO::PARAM_INT); $stmt->bindValue(2, (int)$BATCH_SIZE, PDO::PARAM_INT); $stmt->execute(); $records = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($records)) { $log("✅ {$site['name']}: 无新记录"); continue; } $updateStmt = $pdo->prepare(" UPDATE `typecho_visitor_log` SET `country` = ? WHERE `id` = ? "); $updated = 0; $failed = 0; foreach ($records as $record) { $id = $record['id']; $ip = $record['ip']; $oldCountry = $record['country']; $fullLocation = queryIpLocation($ip, $API_URL_TEMPLATE, $API_TIMEOUT); if ($fullLocation !== null) { $updateStmt->execute([$fullLocation, $id]); $updated++; $log("✅ {$site['name']} | ID=$id | IP=$ip => \"$fullLocation\""); } else { $failed++; $log("❌ {$site['name']} | ID=$id | IP=$ip | API 失败（保留: \"$oldCountry\"）"); } } $log("📊 {$site['name']} 完成：更新 $updated 条，失败 $failed 条\n"); } $log("🏁 所有站点处理完毕！");这里我设置了往前更新10分钟内的记录，更新IP记录数量限制在500以内，配合cron计划任务，就可以实现定时自动更新访问IP信息：*/10 * * * * /usr/bin/php82 "/data/ip_info/cron_update_ip.php"这样设置后，服务器就会每10分钟自动去获取IP信息并更新数据库了。