时间好快，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信息并更新数据库了。
解决PyTorch CUDA报错：no kernel image is available for execution on the device
修复GTX 10系显卡no kernel image报错今天想试试看P104-100玩AI画图怎么样，结果遇到了PyTorch相关的报错，解决后感觉也挺有代表性的，写篇文章简单记录一下。问题复现尝试使用P104-100跑ComfyUI玩，结果收到了报错：torch.AcceleratorError: CUDA error: no kernel image is available for execution on the device Search for `cudaErrorNoKernelImageForDevice' in https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__TYPES.html for more information. CUDA kernel errors might be asynchronously reported at some other API call, so the stacktrace below might be incorrect. For debugging consider passing CUDA_LAUNCH_BLOCKING=1 Compile with `TORCH_USE_CUDA_DSA` to enable device-side assertions.我就觉得纳闷，为什么同样的驱动版本，都是580版本的驱动，V100不报错，但是P104-100报错了。然后我运行了检查GPU compute capability的代码，得到结果如下：print(torch.cuda.get_device_properties(0)) /home/rin/.local/lib/python3.12/site-packages/torch/cuda/__init__.py:283: UserWarning: Found GPU0 NVIDIA P104-100 which is of cuda capability 6.1. Minimum and Maximum cuda capability supported by this version of PyTorch is (7.0) - (12.0) warnings.warn( /home/rin/.local/lib/python3.12/site-packages/torch/cuda/__init__.py:304: UserWarning: Please install PyTorch with a following CUDA configurations: 12.6 following instructions at https://pytorch.org/get-started/locally/ warnings.warn(matched_cuda_warn.format(matched_arches)) /home/rin/.local/lib/python3.12/site-packages/torch/cuda/__init__.py:283: UserWarning: Found GPU1 NVIDIA P104-100 which is of cuda capability 6.1. Minimum and Maximum cuda capability supported by this version of PyTorch is (7.0) - (12.0) warnings.warn( /home/rin/.local/lib/python3.12/site-packages/torch/cuda/__init__.py:326: UserWarning: NVIDIA P104-100 with CUDA capability sm_61 is not compatible with the current PyTorch installation. The current PyTorch install supports CUDA capabilities sm_70 sm_75 sm_80 sm_86 sm_90 sm_100 sm_120. If you want to use the NVIDIA P104-100 GPU with PyTorch, please check the instructions at https://pytorch.org/get-started/locally/ warnings.warn( _CudaDeviceProperties(name='NVIDIA P104-100', major=6, minor=1, total_memory=8109MB, multi_processor_count=15, uuid=eb87a003-8cda-0e4c-b06d-768185c4dc13, pci_bus_id=1, pci_device_id=0, pci_domain_id=0, L2_cache_size=2MB)这下就清楚了，V100不报错是因为其cuda capability为7.0，正好赶上末班车，而10系显卡的Pascal架构cuda capability为6.1，刚好被抛弃了。解决方法也很简单了，那就是去找出来最后支持cuda capability为6.1的PyTorch版本。修复报错查找了资料后，我得知PyTorch的2.7.1版本是最后支持Pascal架构的版本，我们只需要卸载当前的PyTorch，降级为2.7.1即可。pip3.12 uninstall torch torchvision torchaudio pip3.12 install torch==2.7.1 torchvision==0.22.1 torchaudio==2.7.1然后在尝试运行print(torch.cuda.get_device_properties(0))验证一下，应该就不报错了：print(torch.cuda.get_device_properties(0)) _CudaDeviceProperties(name='NVIDIA P104-100', major=6, minor=1, total_memory=8109MB, multi_processor_count=15, uuid=eb87a003-8cda-0e4c-b06d-768185c4dc13, L2_cache_size=2MB)如果得到这样的结果，comfyUI应该也可以正常运行了。最后我补充说明一下我的软件版本环境：Python 3.12ComfyUI 0.3.76
V100 GPU 初体验：AlmaLinux 8.10 快速部署 ComfyUI
在 AlmaLinux 8.10 上部署 ComfyUI最近买了一张 V100，为了爽玩 AI 绘图，我单独组装了一台 X99 平台主机来运行 ComfyUI。操作系统方面，我选择了 AlmaLinux 8.10，看中的是其稳定性以及对多版本 Python 的良好支持。硬件介绍显卡本体Tesla V100 显卡展示（和RTX2080Ti同框）：安装到转接板上：转接板是200买的，虽然比最便宜的99元转接板贵了一倍，但用的是原装座子，原装座子的转接板就算是10X10CM的小板子都170+了。我加多30，买个带温控的，尺寸大一点的板，还自带螺栓孔位固定，完美安装，他不香吗？装上散热器后的样子：我为了尽可能降低温度，延长使用寿命，买了最大的散热器，到手发现确实巨大无比，比想象中的要大得多。但是其实现在看，买小一号的9CM高度的可能更合适，因为更好安装8CM风扇，不会浪费散热面积。我最后还是选择了8CM的风扇来散热，放弃了之前使用12CM风扇的想法——因为没办法彻底吹透散热器，温度还高。装上风扇Be Like:因为两边会漏风，导致无法彻底吹透散热器，所以搞了一张纸包了一下，迫使气流贯穿散热器。配套硬件平台开头我先说一下我踩到的坑，首先平台选择就先栽了一头：一开始我想用我闲置的H61平台来带这个玩意，而且也确认看过那个主板有Above 4G Decoding选项，也打开了，但是就是死活没办法驱动这个显卡，系统识别到了这个设备，但是NVIDIA驱动无法初始化设备，导致无法使用。这个问题折磨了我很久，检查日志具体表现是：[root@Neko-P104 ~]# dmesg | tail -100 | grep -i -E 'nvidia|nvrm|gpu|bar' [ 3.121900] NVRM: This PCI I/O region assigned to your NVIDIA device is invalid: NVRM: BAR4 is 0M @ 0x0 (PCI:0000:01:00.0) [ 3.121902] NVRM: This PCI I/O region assigned to your NVIDIA device is invalid: NVRM: BAR5 is 0M @ 0x0 (PCI:0000:01:00.0) [ 3.322080] NVRM: loading NVIDIA UNIX x86_64 Kernel Module 580.105.08 Tue Apr 8 12:41:17 UTC 2025 [ 3.369648] nvidia-modeset: Loading NVIDIA Kernel Mode Setting Driver for UNIX platforms 580.105.08 Tue Apr 8 12:09:34 UTC 2025 [ 3.376189] [drm] [nvidia-drm] [GPU ID 0x00000100] Loading driver [ 3.376192] [drm] Initialized nvidia-drm 0.0.0 20160202 for 0000:01:00.0 on minor 0 [ 112.149308] caller _nv041720rm+0x3a/0xb0 [nvidia] mapping multiple BARs [ 112.163383] NVRM: GPU 0000:01:00.0: RmInitAdapter failed! (0x24:0x72:1568) [ 112.163415] NVRM: GPU 0000:01:00.0: rm_init_adapter failed, device minor number 0 [ 112.285687] nvidia-uvm: Loaded the UVM driver, major device number 237. [ 379.801748] caller _nv041720rm+0x3a/0xb0 [nvidia] mapping multiple BARs [ 379.815873] NVRM: GPU 0000:01:00.0: RmInitAdapter failed! (0x24:0x72:1568) [ 379.815912] NVRM: GPU 0000:01:00.0: rm_init_adapter failed, device minor number 0这表明：BAR 分配失败：GPU 的某些地址空间没有正确映射到系统内存中。RmInitAdapter 失败：驱动尝试初始化 GPU 时遇到了错误，无法完成初始化。所以这是平台限制，必须换平台了。后面我在我主力机（X99双路服务器）上面强行安装上显卡，结果成功驱动了。作死照片：于是，我就从我家NAS拆了这套X99，然后NAS换回H61，因为NAS用X99本来就性能过剩。之前手痒痒给家里云升级了X99，现在又被拆走，回归最开始的H61，这算是回归本质了（）以下是配置：主板：山寨X99主板，H81芯片组魔改。CPU：E5-2630 v4内存：SK REG ECC DDR4 2133p 2Rx4 16G 两条硬盘：500G机械硬盘GPU：Tesla V100装好起来就长这样了：如果你仔细观察的话，你会发现带显卡的是独立的服务器电源，也就是这个电脑用了两个电源。问就是靠继电器触发服务器电源给显卡提供额外的12V供电，继电器的电源是主电源的12V供电，这样就可以同步上电。这样就可以不用花这么多钱买大电源了，而且服务器拆机电源用料还好，垃圾佬基本操作（）搁这装炸弹呢（bushi硬件介绍完了，接下来就开始折腾吧！驱动安装因为默认安装的AlmaLinux 8.10并没有自带NVIDIA驱动，所以，我我们需要手动安装驱动。具体的操作我博客之前的文章有介绍过，可以搜索看一下，这里我就简要过一遍：yum install epel-release dnf config-manager --add-repo http://developer.download.nvidia.com/compute/cuda/repos/rhel8/$(uname -i)/cuda-rhel8.repo dnf install kernel-headers-$(uname -r) kernel-devel-$(uname -r) tar bzip2 make automake gcc gcc-c++ pciutils elfutils-libelf-devel libglvnd-opengl libglvnd-glx libglvnd-devel acpid pkgconfig dkms dnf module install nvidia-driver:580-dkms安装完成后，重启，执行nvidia-smi能看到显卡信息，就说明成功了：[rin@Neko-P104 ~]$ nvidia-smi Fri Dec 5 20:50:54 2025 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 580.105.08 Driver Version: 580.105.08 CUDA Version: 13.0 | +-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 Tesla V100-SXM2-16GB Off | 00000000:03:00.0 Off | 0 | | N/A 45C P0 153W / 300W | 2733MiB / 16384MiB | 99% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | 0 N/A N/A 319443 C ...2a49dd2e4e7a546c540f/rend.exe 2730MiB | +-----------------------------------------------------------------------------------------+部署 ComfyUI截至我写这篇文章的时候，ComfyUI的版本为0.3.76，本文写的教材都是在这个版本下进行操作的，进行参考时请注意。由于0.3.76版本的ComfyUI推荐Python版本是3.12，我们需要安装Python3.12。得益于AlmaLinux8.10支持多版本Python共存安装，事情变得很简单，不需要Anaconda这种东西。而且这个机器因为是单一的服务器用途，大概率相关的Python项目只会部署一个ComfyUI，所以直接在系统的Python环境部署即可。安装Python3.12：yum install python3.12 python3.12-pip下载并解压ComfyUI：wget https://github.com/comfyanonymous/ComfyUI/archive/refs/tags/v0.3.76.zip unzip v0.3.76.zip rm v0.3.76.zippip换源：cd ~ mkdir .pip nano .pip/pip.conf将以下内容写入配置文件：[global] index-url = https://pypi.mirrors.ustc.edu.cn/simple/ trusted-host = pypi.mirrors.ustc.edu.cn安装依赖环境：cd ComfyUI-0.3.76/ pip3.12 install -r requirements.txt如果你之前用过Stable Diffusion WebUI的话，可以照着extra_model_paths.yaml.example创建一个extra_model_paths.yaml，导入之前的模型。取消注释对应的部分，填上路径就可以了：a111: base_path: /home/rin/stable-diffusion-webui/ checkpoints: models/Stable-diffusion configs: models/Stable-diffusion vae: models/VAE loras: | models/Lora models/LyCORIS upscale_models: | models/ESRGAN models/RealESRGAN models/SwinIR embeddings: embeddings hypernetworks: models/hypernetworks controlnet: models/ControlNet一切设置妥当后，就可以ComfyUI启动了：python3.12 main.py --listen 0.0.0.0不出意外的话，访问服务器的IP地址+8188端口（例如192.168.6.6:8188）就可以看到界面了。尝试跑几张图，没问题就部署完成了。缺失模型下载技巧如果你的服务器位于国内，因为众所周知的原因，是没办法根据它给出的地址直接用wget拉取的，但是我发现了一个小技巧，不需要给服务器魔法上网环境，也可以高速拉取模型。例如，使用这个模板的时候缺失了模型：点击复制连接，然后找一台国外的VPS使用curl来获取下载连接：NyankoHost [~]# curl https://huggingface.co/lightx2v/Qwen-Image-Lightning/resolve/main/Qwen-Image-Lightning-8steps-V1.0.safetensors Found. Redirecting to https://cas-bridge.xethub.hf.co/xet-bridge-us/689761ce345c5cafa2ebc6a6/dbc67890a57bd825922462d911e0d32c7ee7706ee7985f8ddf105b615a1e8e32?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=cas%2F20251205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251205T131704Z&X-Amz-Expires=3600&X-Amz-Signature=84def4e6cfc1dcd8ba5d646142f7d19680a030829568a0f67b4a887d35ecdb4b&X-Amz-SignedHeaders=host&X-Xet-Cas-Uid=public&response-content-disposition=inline%3B+filename*%3DUTF-8%27%27Qwen-Image-Lightning-8steps-V1.0.safetensors%3B+filename%3D%22Qwen-Image-Lightning-8steps-V1.0.safetensors%22%3B&x-id=GetObject&Expires=1764944224&Policy=[binary]&Signature=coo9prmThYpuZYwNqFpZafSP8LHUXhb4DYd6PZbIn4EnSgC%7EDQP6gE3EneVg4I-1-QJcoGBj1lihIWOn8qkrtH5xJfekUCYSSzA1m8jQl89MmrxUxwEZEfYBR3ZturhNeXY5FOdqiYElE98gqr7LHocp0ue5EJJUCbYmlwlJGr4HnwqpM7VkvN1MAATGsIwUbztXOvsvPD6WyjxFGPfeHUPidQVWfCkZiMm-yDHFWRyMT3TYeh6YH8f0ZsVPO2TMvBUFcKRjaGoyKUOHA0EuCz0yKH%7EfrLzkqAVsc2V9MpWEVE5gcCseZufIFFLPDg7garAB%7E2wJddHK4WPw5H-K9g__&Key-Pair-Id=K2L8F4GPSG1IFC NyankoHost [~]# 获取到了下载连接后，我们在服务器上进入对应的目录位置进行拉取：cd ComfyUI-0.3.76/models/loras/ wget -O Qwen-Image-Lightning-8steps-V1.0.safetensors 'https://cas-bridge.xethub.hf.co/xet-bridge-us/689761ce345c5cafa2ebc6a6/dbc67890a57bd825922462d911e0d32c7ee7706ee7985f8ddf105b615a1e8e32?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=cas%2F20251205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251205T131704Z&X-Amz-Expires=3600&X-Amz-Signature=84def4e6cfc1dcd8ba5d646142f7d19680a030829568a0f67b4a887d35ecdb4b&X-Amz-SignedHeaders=host&X-Xet-Cas-Uid=public&response-content-disposition=inline%3B+filename*%3DUTF-8%27%27Qwen-Image-Lightning-8steps-V1.0.safetensors%3B+filename%3D%22Qwen-Image-Lightning-8steps-V1.0.safetensors%22%3B&x-id=GetObject&Expires=1764944224&Policy=[binary]&Signature=coo9prmThYpuZYwNqFpZafSP8LHUXhb4DYd6PZbIn4EnSgC%7EDQP6gE3EneVg4I-1-QJcoGBj1lihIWOn8qkrtH5xJfekUCYSSzA1m8jQl89MmrxUxwEZEfYBR3ZturhNeXY5FOdqiYElE98gqr7LHocp0ue5EJJUCbYmlwlJGr4HnwqpM7VkvN1MAATGsIwUbztXOvsvPD6WyjxFGPfeHUPidQVWfCkZiMm-yDHFWRyMT3TYeh6YH8f0ZsVPO2TMvBUFcKRjaGoyKUOHA0EuCz0yKH%7EfrLzkqAVsc2V9MpWEVE5gcCseZufIFFLPDg7garAB%7E2wJddHK4WPw5H-K9g__&Key-Pair-Id=K2L8F4GPSG1IFC'然后你就能获得起飞一般的下载速度：[rin@Neko-P104 loras]$ wget -O Qwen-Image-Lightning-8steps-V1.0.safetensors 'https://cas-bridge.xethub.hf.co/xet-bridge-us/689761ce345c5cafa2ebc6a6/dbc67890a57bd825922462d911e0d32c7ee7706ee7985f8ddf105b615a1e8e32?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=cas%2F20251205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251205T131704Z&X-Amz-Expires=3600&X-Amz-Signature=84def4e6cfc1dcd8ba5d646142f7d19680a030829568a0f67b4a887d35ecdb4b&X-Amz-SignedHeaders=host&X-Xet-Cas-Uid=public&response-content-disposition=inline%3B+filename*%3DUTF-8%27%27Qwen-Image-Lightning-8steps-V1.0.safetensors%3B+filename%3D%22Qwen-Image-Lightning-8steps-V1.0.safetensors%22%3B&x-id=GetObject&Expires=1764944224&Policy=[binary]&Signature=coo9prmThYpuZYwNqFpZafSP8LHUXhb4DYd6PZbIn4EnSgC%7EDQP6gE3EneVg4I-1-QJcoGBj1lihIWOn8qkrtH5xJfekUCYSSzA1m8jQl89MmrxUxwEZEfYBR3ZturhNeXY5FOdqiYElE98gqr7LHocp0ue5EJJUCbYmlwlJGr4HnwqpM7VkvN1MAATGsIwUbztXOvsvPD6WyjxFGPfeHUPidQVWfCkZiMm-yDHFWRyMT3TYeh6YH8f0ZsVPO2TMvBUFcKRjaGoyKUOHA0EuCz0yKH%7EfrLzkqAVsc2V9MpWEVE5gcCseZufIFFLPDg7garAB%7E2wJddHK4WPw5H-K9g__&Key-Pair-Id=K2L8F4GPSG1IFC' --2025-12-05 21:21:03-- https://cas-bridge.xethub.hf.co/xet-bridge-us/689761ce345c5cafa2ebc6a6/dbc67890a57bd825922462d911e0d32c7ee7706ee7985f8ddf105b615a1e8e32?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=cas%2F20251205%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20251205T131704Z&X-Amz-Expires=3600&X-Amz-Signature=84def4e6cfc1dcd8ba5d646142f7d19680a030829568a0f67b4a887d35ecdb4b&X-Amz-SignedHeaders=host&X-Xet-Cas-Uid=public&response-content-disposition=inline%3B+filename*%3DUTF-8%27%27Qwen-Image-Lightning-8steps-V1.0.safetensors%3B+filename%3D%22Qwen-Image-Lightning-8steps-V1.0.safetensors%22%3B&x-id=GetObject&Expires=1764944224&Policy=[binary]&Signature=coo9prmThYpuZYwNqFpZafSP8LHUXhb4DYd6PZbIn4EnSgC%7EDQP6gE3EneVg4I-1-QJcoGBj1lihIWOn8qkrtH5xJfekUCYSSzA1m8jQl89MmrxUxwEZEfYBR3ZturhNeXY5FOdqiYElE98gqr7LHocp0ue5EJJUCbYmlwlJGr4HnwqpM7VkvN1MAATGsIwUbztXOvsvPD6WyjxFGPfeHUPidQVWfCkZiMm-yDHFWRyMT3TYeh6YH8f0ZsVPO2TMvBUFcKRjaGoyKUOHA0EuCz0yKH%7EfrLzkqAVsc2V9MpWEVE5gcCseZufIFFLPDg7garAB%7E2wJddHK4WPw5H-K9g__&Key-Pair-Id=K2L8F4GPSG1IFC Resolving cas-bridge.xethub.hf.co (cas-bridge.xethub.hf.co)... 13.33.183.18, 13.33.183.37, 13.33.183.46, ... Connecting to cas-bridge.xethub.hf.co (cas-bridge.xethub.hf.co)|13.33.183.18|:443... connected. HTTP request sent, awaiting response... 200 OK Length: 1698951104 (1.6G) Saving to: ‘Qwen-Image-Lightning-8steps-V1.0.safetensors’ Qwen-Image-Lightning-8steps-V1.0.safetens 100%[==================================================================================>] 1.58G 28.8MB/s in 1m 3s 2025-12-05 21:23:08 (13.1 MB/s) - ‘Qwen-Image-Lightning-8steps-V1.0.safetensors’ saved [1698951104/1698951104] [rin@Neko-P104 loras]$ 能这么操作的原因是，huggingface.co的域名被阻断了，而真正用于分发下载的cas-bridge.xethub.hf.co域名并没有被阻断，而且因为是亚太的amazon节点，速度还很快。番外：V100 的 SheepIt 战力我用顺带跑了一下SheepIt渲染农场的渲染速度，测试了几次，基本上就是大概116%的算力，还是可以的。