mirror of
https://github.com/Mabbs/mabbs.github.io
synced 2026-11-01 07:25:48 +08:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2abc8e7269 | |||
| 0fa7816fa8 | |||
| 7f6baa0c8d | |||
| 1052b66270 | |||
| 8cb782d1ff | |||
| 7659d3e9d5 | |||
| a4cc075205 | |||
| 44d8461ab1 | |||
| 303f5df01c | |||
| 932e310f9d | |||
| 2036ee2c4f | |||
| a89e95869e | |||
| c217a52c70 | |||
| 4b94d48330 | |||
| 2ec9345113 | |||
| 987c604dea | |||
| 8e0b6e11b3 | |||
| 317f5bdce7 | |||
| 89a0af5632 | |||
| 798f65eb97 | |||
| 05998cdc3f | |||
| 5d87b2c9d4 | |||
| b8b29f86d5 | |||
| 0e4e4d2fb7 | |||
| 7943cc7d6a | |||
| 772e3ed0b7 | |||
| ee4c9acfde | |||
| 2acc1f6fd7 | |||
| 5f2a3a99ee | |||
| 36cf19d4aa | |||
| 9c769e4ab2 | |||
| fd5f1703dd | |||
| 2a512a487d | |||
| 556e27c9ad |
+5
-2
@@ -12,5 +12,8 @@ build:
|
||||
- mkdir Mabbs
|
||||
- curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
||||
- bundle exec jekyll build --destination $READTHEDOCS_OUTPUT/html
|
||||
- tar czvf MayxBlog.tgz -C $READTHEDOCS_OUTPUT html
|
||||
- mv MayxBlog.tgz $READTHEDOCS_OUTPUT/html
|
||||
- python3 _tools/blogquine.py --src-dir blog --quine-dir blog $READTHEDOCS_OUTPUT/html MayxBlog.7z
|
||||
- curl -L -O https://github.com/ip7z/7zip/releases/download/26.03/7z2603-linux-x64.tar.xz
|
||||
- tar xvf 7z2603-linux-x64.tar.xz
|
||||
- ./7zzs a -mx9 -bd -bb0 MayxBlog-new.7z MayxBlog.7z
|
||||
- mv MayxBlog-new.7z $READTHEDOCS_OUTPUT/html/MayxBlog.7z
|
||||
+1
-1
@@ -77,7 +77,7 @@
|
||||
y="0"
|
||||
class="header"
|
||||
data-testid="header"
|
||||
>Mayx's Blog Article</text>
|
||||
>Mayx's Blog Posts</text>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
||||
+5
-1
@@ -178,5 +178,9 @@
|
||||
"/2026/03/01/llm3.html": "这篇文章介绍了作者近期在LLM部署和应用方面的经历,主要包括以下几个方面:\n\n首先,作者升级硬件,从单张RTX4090 48GiB升级到双路RTX4090 48GiB,并购买了TRX40+TR 3960X的主板套装,用于运行GPT-OSS模型。随后,作者尝试使用vLLM框架替换Ollama,并成功配置了GPT-OSS模型,达到了接近190Tps的性能。\n\n其次,作者体验了DeepSeek 1M上下文模型,发现其在处理长上下文任务时表现出色,能够展现摘要无法捕捉的细节,并成功生成简历、分析人格等。\n\n此外,作者还尝试使用DeepSeek重构Mabbs,并发现DeepSeek能够识别作者的博客信息,这表明训练样本中包含了作者的信息。\n\n最后,作者在8GiB内存的MacBook Pro上运行了LFM2.5-1.2B-Thinking模型,并使用了Apollo软件,体验了其快速的推理速度和良好的思考能力。作者总结认为,AI的发展令人惊叹,软件优化使其在有限硬件环境下也能运行。",
|
||||
"/2026/04/14/ai-agent.html": "这篇文章介绍了“AI个人助理”Agent的发展现状和各种尝试。作者体验了OpenClaw、QClaw、WorkBuddy、Cline、LuckClaw和ApkClaw等不同的Agent项目,发现它们在功能、易用性和性能上各有优劣。OpenClaw安装和使用存在困难,国内大厂的QClaw和WorkBuddy则更易于上手,但免费额度有限。作者认为,开发任务更适合在编辑器集成AI中进行,如GitHub Copilot。LuckClaw在微型开发板上运行表现出色,ApkClaw则利用手机的优势操作移动应用。尽管Agent技术仍存在诸多问题,但其应用场景不断拓展,有望吸引更多人参与其中,推动AI应用化进程。",
|
||||
"/2026/05/01/virtual-net.html": "这篇文章介绍了作者在尝试异地组网搭建虚拟局域网时,对多种组网工具的探索和体验。作者首先尝试了n2n,但由于其项目停止更新且在NAT后的机器间存在掉线问题,最终选择了WireGuard作为主要方案。文章详细描述了WireGuard的配置过程,包括在Linux、OpenWrt和openEuler系统上的安装和配置,并遇到了各种各样的挑战,例如在红米AX3000路由器上找不到内核模块,以及在openEuler上缺少WireGuard相关包。此外,文章还提到了Netmaker和Headscale等WireGuard的控制平面,以及VNT和EasyTier等其他组网工具,并总结了最终选择WireGuard的原因:简单、够用、可靠性不错,且已经投入了大量配置。",
|
||||
"/2026/06/01/dedupe.html": "这篇文章介绍了如何通过优化游戏资源文件来节约游戏占用的硬盘空间。作者由于Mac只有256GiB的硬盘存储空间,在下载了几十部游戏后空间不足,于是寻找解决方法。文章主要介绍了以下几点:\n\n1. 使用jdupes工具去除重复素材:作者发现许多游戏使用相同的引擎和素材,导致硬盘空间浪费。通过使用jdupes工具进行硬链接,可以有效去除重复文件,节约空间。\n\n2. 不同引擎的处理方式:\n - 对于RPG制作大师MV/MZ,作者使用RPG Maker Decrypter工具解密资源文件,并将图片转换为WebP格式以压缩空间。\n - 对于RPG制作大师XP/VX/VA,作者使用mkxp-z工具跨平台运行游戏,并通过RPG Maker Decrypter解包资源文件,与RTP素材合并后进行去重。\n - 对于Ren'Py游戏,作者使用unrpa工具解包rpa文件,但由于公共资源不多,仅在系列游戏情况下进行解包。\n\n3. 最终效果:通过以上优化,作者成功将游戏文件夹大小从47G降至33G,节约了大量硬盘空间。"
|
||||
"/2026/06/01/dedupe.html": "这篇文章介绍了如何通过优化游戏资源文件来节约游戏占用的硬盘空间。作者由于Mac只有256GiB的硬盘存储空间,在下载了几十部游戏后空间不足,于是寻找解决方法。文章主要介绍了以下几点:\n\n1. 使用jdupes工具去除重复素材:作者发现许多游戏使用相同的引擎和素材,导致硬盘空间浪费。通过使用jdupes工具进行硬链接,可以有效去除重复文件,节约空间。\n\n2. 不同引擎的处理方式:\n - 对于RPG制作大师MV/MZ,作者使用RPG Maker Decrypter工具解密资源文件,并将图片转换为WebP格式以压缩空间。\n - 对于RPG制作大师XP/VX/VA,作者使用mkxp-z工具跨平台运行游戏,并通过RPG Maker Decrypter解包资源文件,与RTP素材合并后进行去重。\n - 对于Ren'Py游戏,作者使用unrpa工具解包rpa文件,但由于公共资源不多,仅在系列游戏情况下进行解包。\n\n3. 最终效果:通过以上优化,作者成功将游戏文件夹大小从47G降至33G,节约了大量硬盘空间。",
|
||||
"/2026/07/01/vibe-coding.html": "这篇文章介绍了作者使用AI Agent进行Vibe Coding的体验。作者通过AI Agent生成代码,实现了博客的PJAX功能和一个立直麻将Wordle小游戏。在使用AI Agent的过程中,作者发现它能够理解整个项目的代码,并且能够根据需求进行修改和优化。然而,作者也遇到了一些问题,例如生成的代码质量不高,需要手动优化和调试。\n\n作者首先使用AI Agent为博客添加了PJAX功能,实现了无刷新加载页面。虽然生成的代码质量不高,但作者还是成功地实现了该功能。接着,作者使用AI Agent生成了一个立直麻将Wordle小游戏,并对其进行了优化和修改。作者还遇到了一些技术问题,例如字体兼容性和役种计算问题,但最终都通过AI Agent解决了。\n\n通过这次体验,作者认为Vibe Coding的体验相当不错,但也存在一些问题。完全由AI生成的代码可能会出现问题,需要手动优化和调试。然而,如果能够正确地使用AI Agent,也可以实现高质量的代码。作者还提到,有时候自己调查和研究比问AI更有效,可以避免浪费时间和精力。",
|
||||
"/2026/08/01/terminal.html": "这篇文章介绍了作者利用AI助手制作终端版博客的经历。起初,作者想在博客上添加一个终端功能,允许用户在浏览器中使用真正的Linux访问博客。作者选择了基于WASM Linux的方案,并让AI助手开发了一个可以在浏览器中执行JS的设备文件,以实现与外界的通信。在此基础上,作者实现了列出文章、查看文章、打开文章和查看评论等功能。然而,该系统存在一些限制,如不能使用管道符和异步操作。作者还尝试了另一种方案,即基于JSNix的版本,虽然不是真正的Linux,但模拟得非常到位,且具有更好的兼容性和加载速度。最终,作者成功制作了两个版本的终端版博客,并表示对AI助手的强大功能感到满意。",
|
||||
"/2026/09/01/vibe-coding2.html": "这篇文章介绍了作者使用AI进行Vibe Coding的体验。两个月前,作者首次体验了Vibe Coding,但当时对AI的能力持怀疑态度。然而,在使用AI进行终端版博客开发后,作者意识到AI的能力可能超过了自己的想象。作者开始使用国内的免费AI模型腾讯混元Hy3进行各种项目开发,并取得了令人惊讶的成果。\n\n作者使用AI开发了 REPL 终端、WebMCP 项目、博客 API,并将 WebMCP 应用于 AI,让 AI 调用函数。作者还使用 AI 重放了一个即将关服的游戏的离散快照,让游戏在浏览器中运行。AI 分析了游戏的 WebSocket+Protobuf 协议,并用 Python 和 JavaScript 实现了一个服务端,让游戏在浏览器中运行。\n\n此外,作者还使用 AI 制作了一个完整的麻将对局引擎,包括人机对战功能。作者让 AI 录制了几把实际对局的包,并理解了数据包的时序,最终 AI 成功地制作出了完整的对局引擎。\n\n作者还探索了其他功能,如挂载本地文件夹、基于 WebRTC 的局域网内组网和使用 VLESS 协议的网关。",
|
||||
"/2026/10/01/quine.html": "这篇文章介绍了作者利用AI制作一个“完整”的博客压缩包的过程。起因是作者之前尝试使用ZIP Quine制作包含自身压缩包,但由于技术限制未能成功。作者此次使用AI尝试基于LZMA2算法制作7z格式的博客压缩包,最终成功生成代码并制作完成。文章中还讨论了作者对AI创新能力的看法,认为AI不仅可以重组现有信息,还可以完成人类以前认为具有挑战性的任务。"
|
||||
}
|
||||
+8
-3
@@ -2,7 +2,7 @@ title,link,feed_url,description
|
||||
花火学园,https://www.sayhanabi.net/,,和谐融洽的ACG交流以及资源聚集地
|
||||
资源统筹局,https://gkdworld.com/,,统筹保管用户分享的资源
|
||||
贫困的蚊子,https://mozz.ie/,https://mozz.ie/index.xml,
|
||||
极客兔兔,https://geektutu.com/,https://geektutu.com/atom.xml,致力于分享有趣的技术实践
|
||||
极客兔兔,https://geektutu.com/,https://geektutu.com/feed.xml,致力于分享有趣的技术实践
|
||||
维基萌,https://www.wikimoe.com/,https://www.wikimoe.com/rss,萌即是正义!一名热爱acg的前端设计师的小站!
|
||||
7gugu's blog,https://www.7gugu.com/,https://7gugu.com/index.php/feed/,"一个用来存放我爱好的地方,编程,摄影之类的空间"
|
||||
Kingfish404,https://blog.kingfish404.cn/,https://blog.kingfish404.cn/index.xml,"Stay curious,stay naive. WUT. Jin Yu's Blog"
|
||||
@@ -18,7 +18,12 @@ Lanke's blog,https://blog.blueke.top/,https://blog.blueke.top/rss.xml,请为一
|
||||
寒士杰克,https://www.hansjack.com/,https://www.hansjack.com/feed/,喜欢捣鼓,不断进步!
|
||||
Pinpe 的云端,https://pinpe.top/,https://pinpe.top/rss.xml,一个属于自己的云朵。
|
||||
Chise Hachiroku,https://chise.hachiroku.com/zh/,https://chise.hachiroku.com/zh/feed/,向明日的辉迹,干杯!
|
||||
映屿,https://www.glowisle.me/,https://www.glowisle.me/atom.xml,关于互联网、书籍、生活琐事以及那些一闪而过的念头
|
||||
映屿,https://blog.verdant.ee/,https://blog.verdant.ee/atom.xml,关于互联网、书籍、生活琐事以及那些一闪而过的念头
|
||||
Restent's Notebook,https://blog.gxres.net/,https://blog.gxres.net/atom.xml,不前沿技术分享
|
||||
Coseroom,https://coseroom.com,,
|
||||
RavelloH's Blog,https://ravelloh.com,https://ravelloh.com/feed.xml,Beginning of meditation.
|
||||
初然忆,https://www.imcry.vip,https://www.imcry.vip/index.xml,初心如初见,然忆似流年。
|
||||
姓王者,https://xingwangzhe.fun/,https://xingwangzhe.fun/rss.xml,探索、学习、进步、创造无限可能!
|
||||
EXYONE@BLOG:~$,https://exyon.ee,https://exyon.ee/feed.xml,一隅藏天地,流水遇知音。
|
||||
小景,https://jingyuan-zheng.github.io/zh/,https://jingyuan-zheng.github.io/zh/index.xml,随心分享有趣的技术、开源项目与生活记录。
|
||||
热汤茶馆,https://blog.hotsouprealm.top/,https://blog.hotsouprealm.top/atom.xml,茶凉了可以续,话断了可以接,这里是hahahotsoup的茶馆。
|
||||
FGHRSH 日记簿,https://www.fghrsh.net/,https://www.fghrsh.net/feed.php,把喜欢的东西都记下来···
|
||||
|
||||
|
+96
-418
@@ -2,519 +2,197 @@ repo_url
|
||||
http://gdatura24gtdy23lxd7ht3xzx6mi7mdlkabpvuefhrjn4t5jduviw5ad.onion/Mayx/mayx
|
||||
http://git.dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion/mayx/blog
|
||||
http://giteabolfdejtdzblkooalqei6jr67imiugmhtsh6ocw4hlj5a4q.b32.i2p/mayx/blog
|
||||
https://lavaforge.org/mayx/blog
|
||||
https://sij.ai/mayx/blog
|
||||
https://repobase.net/mayx/blog
|
||||
https://gitlab.lain.la/mayx/mayx.pages.lain.la
|
||||
https://gitplac.si/mayx/mayx.gitpage.si
|
||||
https://git.32bit.cafe/mayx/blog
|
||||
https://gitnet.fr/mayx/blog
|
||||
https://opencommit.eu/mayx/blog
|
||||
https://forge.fedoraproject.org/mabbs/blog
|
||||
https://tilde.club/~mayx/git/blog.git/
|
||||
https://git.envs.net/Mayx/mayx
|
||||
https://tildegit.org/Mayx/mayx
|
||||
https://tildegit.org/Mayx/blog
|
||||
https://git.tilde.town/mayx/blog
|
||||
https://gitlab.haskell.org/mayx/mayx
|
||||
https://lavaforge.org/mayx/blog
|
||||
https://repo2.serv00.com/git/pub/Mayx/mayx/
|
||||
https://git.pixie.town/mayx/mayx
|
||||
https://codefloe.com/mayx/blog
|
||||
https://sij.ai/mayx/blog
|
||||
https://git.projectnightcrawler.dev/mayx/blog
|
||||
https://git.pixie.town/mayx/blog
|
||||
https://git.minetest.land/Mayx/blog
|
||||
http://1.6.141.109:3000/mayx/blog
|
||||
http://52.28.156.42/mayx/blog
|
||||
https://code.dsconce.space/mayx/blog
|
||||
http://101.66.229.132:61088/mayx/blog
|
||||
https://git.bp-web.app/mayx/blog
|
||||
https://mygit.iexercice.com/mayx/blog
|
||||
http://162.215.134.149:4000/mayx/blog
|
||||
http://durfee.mycrestron.com:3000/mayx/blog
|
||||
https://gitea.nongnghiepso.com/mayx/blog
|
||||
https://codes.tools.asitavsen.com/mayx/blog
|
||||
https://git.zeroplay.io/mayx/blog
|
||||
http://45.33.87.87/mayx/blog
|
||||
http://168.126.28.36:3000/mayx/blog
|
||||
http://69.62.77.234:8888/mayx/blog
|
||||
http://112.219.147.186:7000/mayx/blog
|
||||
https://git.rootfinlay.co.uk/mayx/blog
|
||||
https://gitea.belanjaparts.com/mayx/blog
|
||||
http://185.208.225.190:3000/mayx/blog
|
||||
http://116.204.34.35:12000/mayx/blog
|
||||
https://git.Apture.io/mayx/blog
|
||||
https://gitea.jasonstolle.com/mayx/blog
|
||||
http://8.134.61.107:3000/mayx/blog
|
||||
http://47.107.88.161:3000/mayx/blog
|
||||
http://142.171.47.170:3000/mayx/blog
|
||||
http://111.231.146.230:8418/mayx/blog
|
||||
https://git.pwaapp.cc/mayx/blog
|
||||
https://an360.top/mayx/blog
|
||||
http://111.119.244.185:3000/mayx/blog
|
||||
https://git.influxfin.com/mayx/blog
|
||||
http://219.157.255.213:25311/mayx/blog
|
||||
https://git.vekus.ru/mayx/blog
|
||||
http://116.204.75.78:6180/mayx/blog
|
||||
http://109.228.48.121:8000/mayx/blog
|
||||
http://27.124.12.222:3000/mayx/blog
|
||||
https://gitea.hintsight.com/mayx/blog
|
||||
http://blackobelisk.xyz:3000/mayx/blog
|
||||
https://git.the-kn.com/mayx/blog
|
||||
https://gitea.shirom.me/mayx/blog
|
||||
https://git.wangxinlei.cn/mayx/blog
|
||||
http://123.56.193.182:3000/mayx/blog
|
||||
http://167.172.7.198:8081/mayx/blog
|
||||
https://gitea.meetgu.ru/mayx/blog
|
||||
http://116.236.50.103:8789/mayx/blog
|
||||
https://git.paulll.cc/mayx/blog
|
||||
http://94.224.160.69:7990/mayx/blog
|
||||
https://gitea.ekjeong.synology.me/mayx/blog
|
||||
https://git.daoyoucloud.com/mayx/blog
|
||||
https://code.bitahub.com/mayx/blog
|
||||
https://repo.gusdya.net/mayx/blog
|
||||
https://gitea.synapsetec.cn/mayx/blog
|
||||
http://gitea.yunshanghub.com:8081/mayx/blog
|
||||
http://113.177.27.200:2033/mayx/blog
|
||||
http://152.69.204.151:3000/mayx/blog
|
||||
http://207.180.229.193:3001/mayx/blog
|
||||
http://34.81.52.16/mayx/blog
|
||||
http://123.57.16.111:3000/mayx/blog
|
||||
https://lius.familyds.org:3000/mayx/blog
|
||||
http://git.chaojing-film.com:3000/mayx/blog
|
||||
https://git.nusaerp.com/mayx/blog
|
||||
http://35.207.205.18:3000/mayx/blog
|
||||
http://git.zjsciot.com:3000/mayx/blog
|
||||
https://gitea.ontoast.uk/mayx/blog
|
||||
http://112.124.40.88:5510/mayx/blog
|
||||
http://121.36.37.70:15501/mayx/blog
|
||||
http://187.216.152.151:9999/mayx/blog
|
||||
http://116.63.173.179:8001/mayx/blog
|
||||
http://114.116.79.196/mayx/blog
|
||||
http://121.40.40.177:3000/mayx/blog
|
||||
http://178.254.35.219:3000/mayx/blog
|
||||
http://2.59.132.109:3001/mayx/blog
|
||||
http://81.71.148.57:8080/mayx/blog
|
||||
https://git.lmskaran.com/mayx/blog
|
||||
http://154.86.0.30:3000/mayx/blog
|
||||
http://52.23.128.62:3000/mayx/blog
|
||||
http://120.46.222.128:10021/mayx/blog
|
||||
http://120.26.108.239:9188/mayx/blog
|
||||
http://47.111.17.177:3000/mayx/blog
|
||||
https://k0ki-dev.com/mayx/blog
|
||||
http://110.42.45.89:2052/mayx/blog
|
||||
https://git.furcom.org/mayx/blog
|
||||
http://34.102.70.200:3000/mayx/blog
|
||||
https://gitea.oio.cat/mayx/blog
|
||||
https://git.karma-riuk.com/mayx/blog
|
||||
https://git.7o9o.net/mayx/blog
|
||||
https://git.gupaoedu.cn/mayx/blog
|
||||
https://git.7milch.com/mayx/blog
|
||||
https://git.sitenevis.com/mayx/blog
|
||||
https://dreamplacesai.de/mayx/blog
|
||||
http://1.94.13.224:9080/mayx/blog
|
||||
http://gitea.coderpath.com/mayx/blog
|
||||
http://43.136.169.169:3000/mayx/blog
|
||||
http://121.196.213.68:3000/mayx/blog
|
||||
https://git.micahmoore.io/mayx/blog
|
||||
https://git.anatid.net/mayx/blog
|
||||
https://git.lekai.info/mayx/blog
|
||||
http://135.235.225.198:3000/mayx/blog
|
||||
http://117.72.114.197:3000/mayx/blog
|
||||
https://git.ashcloud.com/mayx/blog
|
||||
https://git.novaa.xyz/mayx/blog
|
||||
http://101.46.208.93:3000/mayx/blog
|
||||
http://23.94.57.60:3000/mayx/blog
|
||||
https://gitea.sciotech.cn/mayx/blog
|
||||
http://8.140.250.85:3000/mayx/blog
|
||||
http://202.65.194.19:3000/mayx/blog
|
||||
http://101.201.34.43:3000/mayx/blog
|
||||
https://git.gloje-rinchen-dorjee-rinpoche-buddhist-monastery.org/mayx/blog
|
||||
http://101.35.227.2:3000/mayx/blog
|
||||
http://175.126.123.163:3000/mayx/blog
|
||||
http://209.141.47.52:3000/mayx/blog
|
||||
http://60.204.158.188:3000/mayx/blog
|
||||
http://60.204.156.211:3000/mayx/blog
|
||||
http://nas.bi1kbu.com:8418/mayx/blog
|
||||
http://1.94.178.207:3000/mayx/blog
|
||||
https://gitea.tsaida.synology.me/mayx/blog
|
||||
https://git.teygaming.com/mayx/blog
|
||||
http://osztromok.com:3164/mayx/blog
|
||||
http://175.209.53.134:3030/mayx/blog
|
||||
http://39.107.226.169:3000/mayx/blog
|
||||
http://39.96.211.118:3000/mayx/blog
|
||||
http://121.37.47.20:3000/mayx/blog
|
||||
https://git.antropix.dev/mayx/blog
|
||||
http://15.237.198.144/mayx/blog
|
||||
http://183.6.120.101:30000/mayx/blog
|
||||
http://57.129.94.190:3000/mayx/blog
|
||||
https://gitea.micro-stack.org/mayx/blog
|
||||
https://gitea.temp.brentgruber.com/mayx/blog
|
||||
http://47.102.147.170:3050/mayx/blog
|
||||
http://172.236.250.154:3000/mayx/blog
|
||||
http://54.198.134.152:3000/mayx/blog
|
||||
http://121.41.35.226:11011/mayx/blog
|
||||
http://47.111.1.12:3000/mayx/blog
|
||||
https://nelgit.nelpi.co.uk/mayx/blog
|
||||
http://8.141.91.86:3000/mayx/blog
|
||||
http://117.72.15.187:3000/mayx/blog
|
||||
http://81.70.30.91:3000/mayx/blog
|
||||
http://106.54.211.95:3000/mayx/blog
|
||||
http://210.245.20.73:3333/mayx/blog
|
||||
http://43.248.97.109:3000/mayx/blog
|
||||
http://113.207.86.104:3080/mayx/blog
|
||||
http://39.96.195.72:10082/mayx/blog
|
||||
https://221.219.181.35:30000/mayx/blog
|
||||
http://172.172.102.93:3000/mayx/blog
|
||||
http://115.120.245.164:3000/mayx/blog
|
||||
http://35.194.179.90:3000/mayx/blog
|
||||
http://66.179.208.56:3001/mayx/blog
|
||||
http://121.43.60.72:9015/mayx/blog
|
||||
http://171.80.13.66:9112/mayx/blog
|
||||
http://223.108.157.174:3000/mayx/blog
|
||||
http://120.24.249.56/mayx/blog
|
||||
http://54.179.203.2:3000/mayx/blog
|
||||
http://14.103.246.124:16000/mayx/blog
|
||||
http://152.42.207.183:3000/mayx/blog
|
||||
http://47.99.60.81:10082/mayx/blog
|
||||
http://120.211.66.170:8418/mayx/blog
|
||||
https://gitea.vilcap.com/mayx/blog
|
||||
http://121.196.245.62/mayx/blog
|
||||
http://8.138.142.17:3000/mayx/blog
|
||||
http://www.koelndom.cn:13030/mayx/blog
|
||||
http://209.127.59.74:3000/mayx/blog
|
||||
https://git.dotb.cloud/mayx/blog
|
||||
https://gitea.augeo.dev/mayx/blog
|
||||
http://120.46.23.215:3000/mayx/blog
|
||||
http://121.41.2.71:3000/mayx/blog
|
||||
http://110.41.177.80:5000/mayx/blog
|
||||
https://gitea.dsmaster.myds.me/mayx/blog
|
||||
http://www.scserverddns.top:13000/mayx/blog
|
||||
https://git.lucas-michel.fr/mayx/blog
|
||||
https://git.imvictor.tech:2/mayx/blog
|
||||
http://47.112.137.193:3000/mayx/blog
|
||||
http://58.38.123.148:3176/mayx/blog
|
||||
http://docker.clhero.fun:3000/mayx/blog
|
||||
https://bdgit.educoder.net/mayx/blog
|
||||
http://e19510c831.iok.la/mayx/blog
|
||||
http://119.45.49.212:3000/mayx/blog
|
||||
https://code.antopie.org/mayx/blog
|
||||
https://git.serenetia.com/mayx/blog
|
||||
https://vcs.cozydsp.space/mayx/blog
|
||||
http://115.190.107.87:3000/mayx/blog
|
||||
http://106.12.50.144:8081/mayx/blog
|
||||
https://code.wxk8.com/mayx/blog
|
||||
http://8.155.58.218:9000/mayx/blog
|
||||
http://fanlibo.i234.me:8418/mayx/blog
|
||||
https://git.temporaryname.org/mayx/blog
|
||||
https://Repo.gusdya.net/mayx/blog
|
||||
https://git.m.ctf.arrobe.fr/mayx/blog
|
||||
http://69.62.64.52:3333/mayx/blog
|
||||
https://git.flymiracle.com/mayx/blog
|
||||
https://gitea.manekenbrand.com/mayx/blog
|
||||
http://106.14.138.181:3000/mayx/blog
|
||||
https://git.yyuu.xyz/mayx/blog
|
||||
http://39.107.70.124:3000/mayx/blog
|
||||
https://git.stormrain.cn/mayx/blog
|
||||
https://git.deuxfleurs.fr/mayx/blog
|
||||
https://www.simpra.org:3000/mayx/blog
|
||||
http://8.148.31.14:3000/mayx/blog
|
||||
http://47.94.246.1:3000/mayx/blog
|
||||
https://www.arcbyte.dev/mayx/blog
|
||||
https://scm.bcorex.e3labs.net/mayx/blog
|
||||
http://58.17.14.95:8001/mayx/blog
|
||||
https://hero-cloud-stg-code.cnbita.com/mayx/blog
|
||||
http://101.37.69.204:3000/mayx/blog
|
||||
https://gitea.tecamino.com/mayx/blog
|
||||
http://39.99.175.172:8000/mayx/blog
|
||||
http://8.138.13.251:3000/mayx/blog
|
||||
http://47.115.223.229:8888/mayx/blog
|
||||
http://104.254.131.244:3000/mayx/blog
|
||||
https://gitea.alacloud.de/mayx/blog
|
||||
https://git.tobiasweise.dev/mayx/blog
|
||||
https://code.cif.su/mayx/blog
|
||||
https://git.cynic.moe/mayx/blog
|
||||
https://git.muellers-software.org/mayx/blog
|
||||
http://8.130.128.130:3000/mayx/blog
|
||||
https://repos.fbpx.io/mayx/blog
|
||||
https://git.cavemanon.xyz/mayx/blog
|
||||
http://svn.rivastudio.cn/mayx/blog
|
||||
https://kamtk.ru:4000/mayx/blog
|
||||
http://39.98.126.115:8080/mayx/blog
|
||||
http://46.202.189.66:3000/mayx/blog
|
||||
http://82.26.157.11:3001/mayx/blog
|
||||
http://123.60.146.54:3000/mayx/blog
|
||||
http://8.140.248.67:3000/mayx/blog
|
||||
http://118.178.172.49:3000/mayx/blog
|
||||
https://gitea.lasallesaintdenis.com/mayx/blog
|
||||
http://123.57.225.51:3000/mayx/blog
|
||||
http://8.130.135.159:3000/mayx/blog
|
||||
https://git.suo0.com/mayx/blog
|
||||
http://103.228.160.127:3100/mayx/blog
|
||||
https://git.dshkabatur.ru/mayx/blog
|
||||
https://gitea.hoba.dedyn.io/mayx/blog
|
||||
https://git.wegoo.ltd/mayx/blog
|
||||
http://114.203.209.83:3000/mayx/blog
|
||||
http://47.100.111.106:3000/mayx/blog
|
||||
http://47.103.78.70:3000/mayx/blog
|
||||
http://47.100.208.160:51300/mayx/blog
|
||||
http://113.44.218.8:3000/mayx/blog
|
||||
https://git.cenoq.com/mayx/blog
|
||||
http://8.152.205.35:3000/mayx/blog
|
||||
http://120.210.80.160:3000/mayx/blog
|
||||
http://1.95.192.200:59300/mayx/blog
|
||||
http://194.5.152.156:3000/mayx/blog
|
||||
http://8.155.172.147:3001/mayx/blog
|
||||
https://git.erfmann.dev/mayx/blog
|
||||
https://git.weavefun.com:5443/mayx/blog
|
||||
http://58.65.162.118:3000/mayx/blog
|
||||
https://git.arkon.solutions/mayx/blog
|
||||
http://8.131.93.145:54082/mayx/blog
|
||||
http://111.9.31.174:10007/mayx/blog
|
||||
https://forgejo.bridgetownrb.com/mayx/blog
|
||||
http://54.199.96.217:3000/mayx/blog
|
||||
http://20.219.0.85:3000/mayx/blog
|
||||
https://gitea.doinlab.com/mayx/blog
|
||||
https://git.7af.ru/mayx/blog
|
||||
https://gitea.yimoyuyan.cn/mayx/blog
|
||||
https://git.apextoaster.com/mayx/blog
|
||||
https://hub.open-verse.ai/mayx/blog
|
||||
http://221.203.14.217:3000/mayx/blog
|
||||
https://git.sophiagwen.au/mayx/blog
|
||||
http://139.196.96.28:13000/mayx/blog
|
||||
https://dev.kiramtech.com/mayx/blog
|
||||
https://git.ihatemen.uk/mayx/blog
|
||||
https://git.123doit.com/mayx/blog
|
||||
http://62.43.207.91:8889/mayx/blog
|
||||
https://git.johntsai.online/mayx/blog
|
||||
https://gitea.css-sistemas.com.br/mayx/blog
|
||||
https://git.f4e.lol/mayx/blog
|
||||
http://47.113.145.232:3000/mayx/blog
|
||||
http://47.115.212.237:3000/mayx/blog
|
||||
http://72.61.229.93:4000/mayx/blog
|
||||
https://lishan148.synology.me:3014/mayx/blog_cn
|
||||
http://1.95.221.174:3000/mayx/blog
|
||||
https://git.huwhy.cn/mayx/blog_cn
|
||||
http://119.91.35.154:3000/mayx/blog_cn
|
||||
http://110.42.101.39:13000/mayx/blog_cn
|
||||
http://39.101.74.135:5000/mayx/blog_cn
|
||||
http://124.236.46.74:9103/mayx/blog_cn
|
||||
http://123.57.130.140:3000/mayx/blog_cn
|
||||
http://112.124.49.128:3000/mayx/blog_cn
|
||||
http://118.24.46.223:3000/mayx/blog_cn
|
||||
http://s3.v100.vip:31057/mayx/blog_cn
|
||||
https://www.syq.im:2025/mayx/blog_cn
|
||||
http://101.33.225.95:3000/mayx/blog_cn
|
||||
http://45.55.138.82:3000/mayx/blog_cn
|
||||
http://82.156.121.2:3000/mayx/blog_cn
|
||||
http://118.24.129.148:3000/mayx/blog_cn
|
||||
http://58.241.155.106:10140/mayx/blog_cn
|
||||
http://120.48.141.82:3000/mayx/blog_cn
|
||||
http://61.178.84.89:8998/mayx/blog_cn
|
||||
http://82.156.111.58:3000/mayx/blog_cn
|
||||
https://git.saike.fun:9755/mayx/blog_cn
|
||||
https://git.cool2645.com/mayx/blog_cn
|
||||
http://1.95.173.44:3000/mayx/blog_cn
|
||||
http://58.221.157.122:3000/mayx/blog_cn
|
||||
https://gitlab.liruwei.cn/mayx/blog_cn
|
||||
http://61.190.74.90:9900/mayx/blog_cn
|
||||
http://49.234.27.222:3000/mayx/blog_cn
|
||||
http://8.153.70.81:30001/mayx/blog_cn
|
||||
http://175.27.229.211:3000/mayx/blog_cn
|
||||
http://106.55.174.214:3000/mayx/blog_cn
|
||||
https://git.yidaimingjvn.xyz/mayx/blog_cn
|
||||
http://39.105.67.143:3000/mayx/blog_cn
|
||||
http://8.140.232.131:8100/mayx/blog_cn
|
||||
http://82.156.89.21:3000/mayx/blog_cn
|
||||
https://git.sskuaixiu.com/mayx/blog_cn
|
||||
http://xujiesoft.vicp.net:3000/mayx/blog_cn
|
||||
http://113.44.36.103:23000/mayx/blog_cn
|
||||
https://git.mingliqiye.com/mayx/blog_cn
|
||||
http://119.29.194.155:8894/mayx/blog_cn
|
||||
http://58.213.60.6:19000/mayx/blog_cn
|
||||
http://36.153.162.171:3000/mayx/blog_cn
|
||||
http://47.92.113.131:3000/mayx/blog
|
||||
http://gyc.myds.me:4000/mayx/blog
|
||||
https://git.tablet.sh/mayx/blog
|
||||
https://git.gnyra.com/mayx/blog
|
||||
https://git.graveyard.sh/mayx/blog
|
||||
https://git.nathanspackman.com/mayx/blog
|
||||
https://git.rmarl.in/mayx/blog
|
||||
http://git.mynas71.myds.me/mayx/blog
|
||||
https://git.4lsa.com/mayx/blog
|
||||
https://rlgit.pro/mayx/blog
|
||||
https://git.veraskolivna.net/mayx/blog
|
||||
https://git.lifetop.net/mayx/blog
|
||||
https://gitea.jnyuxia.com/mayx/blog
|
||||
http://git.liuhung.com/mayx/blog
|
||||
https://gitea.adber.tech/mayx/blog
|
||||
https://gitea.hello.faith/mayx/blog
|
||||
https://tea.neuron.my/mayx/blog
|
||||
https://git.violka-it.net/mayx/blog
|
||||
https://git.adityagupta.dev/mayx/blog
|
||||
https://git.danpeak.co.uk/mayx/blog
|
||||
https://gitea.mynas-lechner.de/mayx/blog
|
||||
https://git.automathon.org/mayx/blog
|
||||
https://git.esen.gay/mayx/blog
|
||||
https://git.webtims.ru/mayx/blog
|
||||
https://gitea.personalsoftware.space/mayx/blog
|
||||
http://gitea.yiban.com.tw:3030/mayx/blog
|
||||
https://gitlab.iplusus.com/mayx/blog
|
||||
https://gitea.zachl.tech/mayx/blog
|
||||
https://git.miasma-os.com/mayx/blog
|
||||
https://gitea.nacsity.cn/mayx/blog
|
||||
https://gitea.diputadosalta.gob.ar/mayx/blog
|
||||
https://ttym.space/mayx/blog
|
||||
https://getskill.work/mayx/blog
|
||||
https://suprasage.com/mayx/blog
|
||||
https://git.zakum.cn/mayx/blog
|
||||
https://gitea.amazingcoders.com/mayx/blog
|
||||
https://gitea.kamilklecha.dev/mayx/blog
|
||||
http://47.109.103.110:9000/mayx/blog_cn
|
||||
http://47.105.124.101:3000/mayx/blog_cn
|
||||
http://49.232.183.190:3000/mayx/blog_cn
|
||||
http://git.zxkedu.com:33769/mayx/blog_cn
|
||||
https://code.wemediacn.com/mayx/blog_cn
|
||||
http://51.159.198.233:3000/mayx/blog
|
||||
https://gitea.adriangonzalezbarbosa.eu/mayx/blog
|
||||
https://git.legatus.ru/mayx/blog
|
||||
https://git.kayashov.keenetic.pro/mayx/blog
|
||||
http://47.104.241.192:19999/mayx/blog_cn
|
||||
http://47.98.148.146:1026/mayx/blog_cn
|
||||
http://119.96.62.56:3000/mayx/blog_cn
|
||||
http://101.42.28.156:3000/mayx/blog_cn
|
||||
http://106.55.0.66:31807/mayx/blog_cn
|
||||
https://gitea.gentronhealth.com/mayx/blog_cn
|
||||
http://43.143.209.246:6300/mayx/blog_cn
|
||||
http://152.136.158.133:36512/mayx/blog_cn
|
||||
http://yidaima.cn:6008/mayx/blog_cn
|
||||
http://47.113.101.80:3000/mayx/blog_cn
|
||||
http://82.156.98.34:3000/mayx/blog_cn
|
||||
http://zgd.synology.me:8418/mayx/blog_cn
|
||||
https://git.zguiy.com/mayx/blog_cn
|
||||
http://58.221.216.202:3000/mayx/blog_cn
|
||||
http://39.100.183.95:88/mayx/blog_cn
|
||||
http://www.xshideserver.com:3000/mayx/blog_cn
|
||||
http://124.223.54.62:28/mayx/blog_cn
|
||||
http://159.75.131.235:3001/mayx/blog_cn
|
||||
http://1.117.67.95:3000/mayx/blog_cn
|
||||
http://103.236.53.208:3000/mayx/blog_cn
|
||||
http://118.195.135.194:3000/mayx/blog_cn
|
||||
http://81.70.102.154/mayx/blog_cn
|
||||
http://154.8.164.149:3000/mayx/blog_cn
|
||||
http://111.228.34.40:3000/mayx/blog_cn
|
||||
http://82.156.249.211:3000/mayx/blog_cn
|
||||
http://36.133.248.69:3088/mayx/blog_cn
|
||||
http://220.205.16.27:18081/mayx/blog_cn
|
||||
http://81.69.221.216:3000/mayx/blog_cn
|
||||
http://115.159.194.75:4000/mayx/blog_cn
|
||||
http://183.204.60.122:10081/mayx/blog_cn
|
||||
http://43.139.240.37:17000/mayx/blog_cn
|
||||
http://159.75.27.114:3000/mayx/blog_cn
|
||||
http://arcreya.top/mayx/blog_cn
|
||||
https://code.tczkiot.com/mayx/blog_cn
|
||||
http://111.170.153.123:3000/mayx/blog_cn
|
||||
http://xeroworld.team/mayx/blog_cn
|
||||
https://gitea.wuyuank.com/mayx/blog_cn
|
||||
http://47.117.245.9:17701/mayx/blog_cn
|
||||
https://git.sunlix.one/mayx/blog_cn
|
||||
http://47.106.222.181:20511/mayx/blog_cn
|
||||
http://101.43.95.130:3001/mayx/blog_cn
|
||||
https://gitea.myat4.com/mayx/blog_cn
|
||||
http://118.24.161.24:3000/mayx/blog_cn
|
||||
http://githanea.s.odn.cc/mayx/blog_cn
|
||||
https://git.dev.chuweizi.com/mayx/blog
|
||||
http://35.175.189.8/mayx/blog
|
||||
http://meowug.com:8418/mayx/blog_cn
|
||||
http://36.138.125.206:3000/mayx/blog_cn
|
||||
https://git.4lcap.com/mayx/blog_cn
|
||||
http://101.43.238.71:3000/mayx/blog_cn
|
||||
http://59.110.175.62:4322/mayx/blog_cn
|
||||
https://www.luluvip.cn:8419/mayx/blog_cn
|
||||
http://xianyuhome.cn:11013/mayx/blog_cn
|
||||
https://git.chatumbrella.online/mayx/blog
|
||||
https://silica.codes/mayx/blog
|
||||
https://git.crystalyx.net/mayx/blog
|
||||
https://gittea.dev/mayx/blog
|
||||
https://git.newnaturalphilosophy.org/mayx/blog
|
||||
https://code.infininov.com/mayx/blog
|
||||
https://git.apt142.ru/mayx/blog
|
||||
http://gitea.mcelwain.net/mayx/blog
|
||||
https://chunkks.com/mayx/blog
|
||||
https://gitea.earthmanrpg.me/mayx/blog
|
||||
https://ndiuky.site/mayx/blog
|
||||
https://src.enterpriselinux.social/mayx/blog
|
||||
https://gitea.grxe.io/mayx/blog
|
||||
https://git.mhworld.net/mayx/blog
|
||||
https://blvckout.foo/mayx/blog
|
||||
https://gitea.accept.dev.dbf.nl/mayx/blog
|
||||
https://git.ke5.de/mayx/blog
|
||||
https://git.jhossain.online/mayx/blog
|
||||
http://gitea.xsec.fun/mayx/blog
|
||||
https://barhoum-lab.fr/mayx/blog
|
||||
https://git.fedaya.net/mayx/blog
|
||||
http://gitea.nakile.fr/mayx/blog
|
||||
https://redev.lol/mayx/blog
|
||||
http://git.zfxfzb.com/mayx/blog
|
||||
https://papi.tkpups.com/mayx/blog
|
||||
https://git.beyond-a-i.org/mayx/blog
|
||||
http://huanghomenas2.myqnapcloud.com:4000/mayx/blog_cn
|
||||
http://43.142.166.108:10082/mayx/blog_cn
|
||||
https://tm-jikayo.com/mayx/blog_cn
|
||||
http://zzdgitea.stnav.com/mayx/blog_cn
|
||||
http://1.117.66.197:3000/mayx/blog_cn
|
||||
http://git.zhmight.com/mayx/blog_cn
|
||||
https://intl-dev.gaia888.com/mayx/blog_cn
|
||||
http://gitea.snailtrack.cn/mayx/blog_cn
|
||||
http://ydds.cloud:3000/mayx/blog_cn
|
||||
http://120.24.50.145:3000/mayx/blog_cn
|
||||
https://code.draussenfunker.de/mayx/blog_cn
|
||||
https://git.dinsor.co.th/mayx/blog
|
||||
https://ofibohost.com/mayx/blog
|
||||
https://lab.iishka.net/mayx/blog
|
||||
http://www.arkproject.top/mayx/blog
|
||||
http://www.bkandssp.cn:30/mayx/blog
|
||||
https://gitea.spitaki.cloud/mayx/blog
|
||||
https://git.codle.ru/mayx/blog
|
||||
https://codeop.ru/mayx/blog
|
||||
https://git.mirocom.org/mayx/blog
|
||||
http://gitea.ydxtool.com/mayx/blog
|
||||
http://18.167.251.121:10003/mayx/blog
|
||||
http://39.98.171.121:53000/mayx/blog
|
||||
https://gitea.malxte.de/mayx/blog
|
||||
https://git.nizart.me/mayx/blog
|
||||
https://git.ddns.net/mayx/blog
|
||||
http://222.85.214.245:9776/mayx/blog
|
||||
https://git.kraevsky.ru/mayx/blog
|
||||
https://ruyiscx.cloud:3000/mayx/blog
|
||||
https://git.0xee.eu/mayx/blog
|
||||
https://gitea.deitglobal.com/mayx/blog
|
||||
https://git.crwlr.ir/mayx/blog
|
||||
https://git.nozora.top/mayx/blog
|
||||
https://git.sortug.com/mayx/blog
|
||||
https://aivyx-gitea.cloud/mayx/blog
|
||||
https://git.edenit.co.kr/mayx/blog
|
||||
https://git.catgirlsneed.homes/mayx/blog
|
||||
https://git.eldev.netcraze.pro/mayx/blog
|
||||
http://110.41.184.238:3000/mayx/blog
|
||||
http://47.108.255.216:3000/mayx/blog_cn
|
||||
https://gitea.molietech.com/mayx/blog_cn
|
||||
http://58.87.88.234:3000/mayx/blog_cn
|
||||
http://210.75.240.13:3000/mayx/blog_cn
|
||||
https://git.xz-i.com:30443/mayx/blog_cn
|
||||
https://git.fynn.vip/mayx/blog_cn
|
||||
http://119.91.212.17:3000/mayx/blog_cn
|
||||
http://git.hbg99.com:8080/mayx/blog_cn
|
||||
https://git.eplg.services/mayx/blog
|
||||
http://89.167.38.168:3001/mayx/blog
|
||||
http://namonba.asuscomm.com:3001/mayx/blog
|
||||
http://109.199.98.226:3001/mayx/blog
|
||||
https://git.extra.eiffel.com/mayx/blog
|
||||
https://gitea.digitanie.org/mayx/blog
|
||||
https://git.xleed.com/mayx/blog
|
||||
https://qlcodegitserver.online/mayx/blog
|
||||
https://gitea.vvzvlad.xyz/mayx/blog
|
||||
https://git.supernets.org/mayx/blog
|
||||
https://git.digitaltelepresence.com/mayx/blog
|
||||
https://git.hrfee.pw/mayx/blog
|
||||
https://git.libregaming.org/mayx/blog
|
||||
https://git.kaki87.net/mayx/blog
|
||||
https://forgejo.vanten-s.com/mayx/blog
|
||||
https://git.heartnn.com/mayx/blog
|
||||
http://124.71.169.183:3000/mayx/blog
|
||||
http://140.120.108.238:49308/mayx/blog
|
||||
http://git.chilidoginteractive.com:3000/mayx/blog_cn
|
||||
https://git.panda-number.one/mayx/blog
|
||||
https://datacortex.pl/mayx/blog
|
||||
https://gitea.keymantek.com:777/mayx/blog
|
||||
https://git.tirtapakuan.co.id/mayx/blog
|
||||
http://tian-you.top:7020/mayx/blog
|
||||
http://34.17.182.140/mayx-gogs/blog
|
||||
https://mcgit.place/mayx/blog
|
||||
http://git.instal.org.cn/mayx/blog
|
||||
https://git.chiasson.cloud/mayx/blog
|
||||
https://repo.qruize.com/mayx/blog
|
||||
https://git.xneon.org/mayx/blog
|
||||
https://lab.dutt.ch/mayx/blog
|
||||
https://gitea.dabit.synology.me/mayx/blog
|
||||
https://git.joinplu.me/mayx/blog
|
||||
https://git.research.dezeeuw.ca/mayx/blog
|
||||
http://149.104.29.239:8081/mayx/blog
|
||||
http://188.66.0.25:3000/mayx/blog
|
||||
https://gitea.deliverables.io/mayx/blog
|
||||
http://23.182.16.150:3000/mayx/blog
|
||||
https://git.ritonquilol.fr/mayx/blog
|
||||
https://bis127.vse.cz/mayx/blog
|
||||
https://git.jhammah.com/mayx/blog
|
||||
https://gitea.kolesarhome.com/mayx/blog
|
||||
https://git.wikeo.ovh/mayx/blog
|
||||
https://devcraft-dot.tech/mayx/blog
|
||||
https://a-t-g.ru/mayx/blog
|
||||
http://wangbeibei.xyz:6002/mayx/blog
|
||||
http://8.141.82.163:20000/mayx/blog_cn
|
||||
http://yidaima.cn:6008/mayx/blog_cn
|
||||
http://8.148.31.14:3000/mayx/blog_cn
|
||||
http://58.17.14.95:8001/mayx/blog_cn
|
||||
http://39.99.175.172:8000/mayx/blog_cn
|
||||
http://175.178.193.35:3000/mayx/blog_cn
|
||||
http://gitea.jb1000.com:88/mayx/blog_cn
|
||||
http://www.22ai.vip/mayx/blog_cn
|
||||
http://219.151.177.172:3000/mayx/blog_cn
|
||||
http://62.234.20.54:3000/mayx/blog_cn
|
||||
https://gitea.4gunn.cn:52443/mayx/blog_cn
|
||||
http://jdunlap.com/mayx/blog
|
||||
https://git.silica.codes/mayx/blog
|
||||
https://gitruhub.ru/mayx/blog
|
||||
https://mjjtop.com/mayx/blog
|
||||
https://forgejo.wanderingmonster.dev/mayx/blog
|
||||
https://gitea.ns5001k.sigma2.no/mayx/blog
|
||||
https://git.hemangvyas.com/mayx/blog
|
||||
https://gitav.ru/mayx/blog
|
||||
https://git.aptcloud.ru/mayx/blog
|
||||
https://gitlab.dev.genai-team.ru/mayx/blog
|
||||
https://git.bitpak.ru/mayx/blog
|
||||
https://git.kunstglass.de/mayx/blog
|
||||
https://git.kry008.xyz/mayx/blog
|
||||
http://36.213.200.127:23000/mayx/blog_cn
|
||||
https://gitea.yfandxt.asia/mayx/blog_cn
|
||||
http://81.69.57.215:3000/mayx/blog_cn
|
||||
https://git.dglyoo.com/mayx/blog_cn
|
||||
http://c8202.top:3000/mayx/blog_cn
|
||||
http://nojava.fun/mayx/blog_cn
|
||||
https://git.vycsucre.gob.ve/mayx/blog
|
||||
https://git.uj-s.com/mayx/blog
|
||||
https://git.albiobola.nl/mayx/blog
|
||||
https://git.83channel.net/mayx/blog
|
||||
https://repo.paperless.rs/mayx/blog
|
||||
https://git.checkerwars.com/mayx/blog
|
||||
https://git.broderlands.com/mayx/blog
|
||||
https://gitea.schwegmann.tech/mayx/blog
|
||||
https://gitea.kcm-nixi.cn:32443/mayx/blog
|
||||
https://git.riskreduction.net/mayx/blog
|
||||
https://git.twitchmagic.pro/mayx/blog
|
||||
https://gitea.marcin00.pl/mayx/blog
|
||||
https://git.imagitech.mx/mayx/blog
|
||||
https://github.jogjacloud.com/mayx/blog
|
||||
https://git.stormofgalaxy.com/mayx/blog
|
||||
https://git.jibai.dev/mayx/blog
|
||||
https://dev.rasadyar.com/mayx/blog
|
||||
https://git.dew.vn/mayx/blog
|
||||
https://git.recstar.club/mayx/blog
|
||||
https://git.ric-rac.org/mayx/blog
|
||||
https://git.yan.best/mayx/blog
|
||||
https://forgejo.cgr-international.com/mayx/blog
|
||||
https://yang-gitea.umersoft.com/mayx/blog
|
||||
https://synthesist.d4d.host/mayx/blog
|
||||
https://git.takits.me/mayx/blog
|
||||
https://gitea.zazlab.freeddns.org/mayx/blog
|
||||
https://gitweb.cn/mayx/blog
|
||||
https://forge.harusato.com/mayx/blog
|
||||
https://forgejo.siriusdevops.com/mayx/blog
|
||||
https://git.tessdev.fr/mayx/blog
|
||||
https://gitlab.navy.mi.th/mayx/blog
|
||||
https://forgejo.quantum-forge.io/mayx/blog
|
||||
|
||||
|
+2
-7
@@ -1,6 +1,5 @@
|
||||
proxies:
|
||||
- https://blog.mayx.workers.dev/
|
||||
- https://mayx.deno.dev/
|
||||
- https://mayx.val.run/
|
||||
- https://mayx.azion.app/
|
||||
- https://yuki.gear.host/
|
||||
@@ -16,16 +15,12 @@ mirrors:
|
||||
- https://mayx.gitpage.si/
|
||||
- https://mayx.serv00.net/
|
||||
- https://mayx.vercel.app/
|
||||
- https://mayx-blog.pgs.sh/
|
||||
- https://mayx.netlify.app/
|
||||
- https://mayx.gitnet.page/
|
||||
- https://mayx.stormkit.dev/
|
||||
- https://mayx.grebedoc.dev/
|
||||
- https://mabbs.kinsta.page/
|
||||
- https://mayx.codeberg.page/
|
||||
- https://mayx.codefloe.page/
|
||||
- https://mayx.tildepages.org/
|
||||
- https://mayx.pandastack.app/
|
||||
- https://mayx.pages.lain.la/
|
||||
- https://mayx.4everland.app/
|
||||
- https://mayx.readthedocs.io/
|
||||
- https://mayx.sourceforge.io/
|
||||
@@ -42,7 +37,6 @@ repos:
|
||||
- https://framagit.org/mayx/mayx.frama.io
|
||||
- https://salsa.debian.org/mayx/mayx.pages.debian.net
|
||||
- https://codeberg.org/mayx/blog
|
||||
- https://pagure.io/mayx
|
||||
- https://git.gay/mayx/mayx
|
||||
- https://repo.or.cz/mayx.git
|
||||
- https://gitea.com/mayx/mayx
|
||||
@@ -50,6 +44,7 @@ repos:
|
||||
- https://git.sr.ht/~mayx/mayx
|
||||
- https://worktree.ca/mayx/blog
|
||||
- https://git.launchpad.net/mayx
|
||||
- https://codefloe.com/mayx/blog
|
||||
- https://gin.g-node.org/mayx/blog
|
||||
- https://tildeforge.dev/mayx/blog
|
||||
- https://git.disroot.org/mayx/mayx
|
||||
|
||||
@@ -98,7 +98,8 @@ layout: xslt_container
|
||||
<li><a href="{{ site.github.tar_url }}">Download <strong>TAR Ball</strong></a></li>
|
||||
<li><a href="{{ site.github.repository_url }}">View On <strong>GitHub</strong></a></li>
|
||||
{% else %}
|
||||
<li style="width: 270px; border-right: none;"><a href="/MayxBlog.tgz">Download <strong>TGZ File</strong></a></li>
|
||||
<li style="width: 134px;"><a href="/MayxBlog.7z">Download <strong>7-Zip File</strong></a></li>
|
||||
<li style="width: 134px; border-right: none;"><a href="/offline.html">Manage <strong>Offline Mode</strong></a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</header>
|
||||
@@ -117,6 +118,7 @@ layout: xslt_container
|
||||
<script src="{{ "/assets/js/scale.fix.js" | relative_url }}"></script>
|
||||
<!--[if !IE]> -->
|
||||
<script src="/assets/js/main_new.js"></script>
|
||||
<script src="/assets/js/blog-console.js"></script>
|
||||
<script src="/Live2dHistoire/live2d/js/live2d.js"></script>
|
||||
<script src="/Live2dHistoire/live2d/js/message.js"></script>
|
||||
<script src="/assets/js/jquery.pjax.min.js"></script>
|
||||
|
||||
@@ -20,7 +20,7 @@ tags: [Mayx,MOS]
|
||||
MOS是一个用来解释我不能解释的东西的一个代替品,就如同UFO和人们定义的神一样,因为未知,而又需要一种解释的方法,于是出现了MOS。
|
||||
***
|
||||
## 正文
|
||||

|
||||

|
||||
```
|
||||
MOS Log System has been Load.
|
||||
MOS Analysis System is Ready.
|
||||
|
||||
@@ -139,7 +139,7 @@ Tel +34 612 520330
|
||||
|
||||
然后还非常用心的配了一张快递单:
|
||||
|
||||

|
||||

|
||||
|
||||
注:由于隐私原因,涉及隐私的地方已经被删除 ~~(尽管大家都知道我的手机号)~~
|
||||
这个邮件当时也没细看,然后就给Lori Robinson说了我已经给他们发了我的信息,过了几天,她给我发:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
layout: post
|
||||
title: Vibe Coding新体验
|
||||
tags: [Vibe Coding, AI, PJAX, 麻将]
|
||||
---
|
||||
|
||||
All in AI!<!--more-->
|
||||
|
||||
# 起因
|
||||
前段时间,我写了一篇关于[AI Agent](/2026/04/14/ai-agent.html)的文章,虽然当时我说这并没有什么创新之处,但在那之后用着用着感觉还是挺好用的,至少相比以前[在云端Vibe Coding](/2025/04/08/feed.html)来说又快又免费,所以我打算分享一下最近我用AI Agent的经历。
|
||||
不过大多数情况下我写代码都可以直接让各路AI在对话中给我生成,那到底有什么样的场景才需要用AI Agent呢?一般应该就是需要边改边测的东西吧。
|
||||
|
||||
# 关于PJAX的改造
|
||||
虽然说AI生成的东西可靠性并不是很高,但至少在网上已经有很多例子的代码应该不至于乱生成吧……其实我一直都想给我的博客增加全站无刷新的功能,但是每次看到PJAX都要自己重新调用当前页面的各种函数就懒得动手了😂。不过其实这也不是什么复杂的事情,主要是一个一个分析函数比较麻烦,所以既然有AI了,不如直接让AI帮我做吧。于是我让它克隆了我的仓库,分析并写出PJAX的代码。
|
||||
用AI Agent最大的好处就是能让它了解整个项目的所有代码,如果是直接在对话里让AI修改的话就很难做到这一点。总之我告诉它之后等了一会它真的写出来了,我试了一下确实能用,虽然刚开始有不少问题,主要是因为我用了[Live2D](/Live2dHistoire/),不少问题也是这个功能引起的。不过只要我告诉它问题在哪里,它总是能给我改好,先不论它实现的代码怎么样,但至少从效果来说还是相当不错的。
|
||||
最终生成的代码我看了一下,代码质量非常糟糕,把我在别处的代码到处复制粘贴到PJAX用的代码里了,但是体验效果不错……所以没办法,虽然代码很糟糕,但先用着吧。不过我觉得优化应该也能交给AI Agent,只是它有时候改来改去还会把正常代码改坏,所以优化看起来还是只能自己做……
|
||||
最终的代码就是:[pjax.js](/assets/js/pjax.js),虽然我对这个代码质量不是很满意,但是毕竟能用,效果也不错,就先这样用吧~
|
||||
|
||||
# 生成一个小游戏
|
||||
前段时间,我在Bilibili上看视频的时候发现了一个有趣的微信小游戏,叫做“雀谜”,它把立直麻将和Wordle两个玩法结合到一起,看起来挺有意思。不过它作为微信上的小游戏有些问题,一是看提示要看广告,二是换题也要看广告,而且看广告就算能换题也有次数限制,一天也就几次机会,让我有点不爽。
|
||||
不过从功能上来说看起来并不复杂,感觉用AI直接Vibe Coding完全不是问题,不过AI Agent从零开始写比较考验模型的水平,所以我打算先让各路网页上的AI生成初版的Demo,之后再考虑怎么改。于是我对它们说:
|
||||
> 帮我制作一款立直麻将Wordle网页小游戏,规则为:
|
||||
> 1. 系统随机生成一个有役能和的14张牌,必须按照万、筒、索、字的顺序排列,同花色内数字从小到大,但是第14张用来和的牌可以不遵守排列规则
|
||||
> 2. 让玩家猜6次来猜到这个牌型,每次猜的牌也必须是满足有役和牌的条件
|
||||
> 3. 位置和内容正确的牌用绿色标注、只有内容正确但是位置不正确的用黄色标注,不在生成范围内的牌用灰色标注
|
||||
> 4. 界面中所有的牌使用Unicode中的麻将字符,并且提供一组输入按钮用于输入这些麻将字符
|
||||
> 5. 输入按钮在游玩过程中也要标注颜色
|
||||
> 6. 随机生成的牌需要标注场风、自风和是否自摸,如果是荣和需要额外的役,需要考虑平和之类的役种
|
||||
> 7. 加一个提示键,可以查看答案牌型的役种
|
||||
|
||||
直接生成确实很考验水平,有很多AI直接生成出来的完全用不了,像DeepSeek生成出来的按钮按上去都没有反应😅,最终只有Gemini生成出来的感觉还不错,无论是界面还是输入的感觉都很好,于是接下来的修改我就打算以Gemini生成出来的代码为主。
|
||||
刚生成出来的代码虽然能玩,但是每个AI直接生成都不想做完整的役种检测,说什么一个对话不够😥,明明各个都是几百k上下文的AI,有什么不够的……总之想一口气就完美做出来显然是不现实的,不过其实最开始我也不打算把所有役都做了,毕竟条件是“有役和牌”,而且没有副露,所以好多可以叠加的役其实都不需要做,尤其是役满那些,大多都可以用其他役的条件满足。
|
||||
在这期间,“平和”算是实现起来最麻烦的,主要是这个役需要根据和牌判定,必须是顺子的两面听牌才可以……虽然AI理解这些知识,但是写的时候经常出问题,像边张之类的经常误判……但是不实现又不行,毕竟这是可以nomi的牌型。
|
||||
最终写的差不多了,感觉还不错,于是在GitHub Pages上[部署了一份](https://mabbs.github.io/riichi-mahjong-wordle/),起名叫[立直麻将 Wordle](https://github.com/Mabbs/riichi-mahjong-wordle)。
|
||||
最开始我以为这个小游戏的玩法是他们原创的,于是把生成的代码又丢给AI,让它评价一下创新性,结果AI说已经有开源了😂,有个叫做[Mahjong Handle](https://github.com/xnuk/mahjong-handle)的才是最早做这个玩法的……还好这代码不是我写的,要是我写了半天结果发现有现成的那就很无语了😆。
|
||||
当然我让AI写的版本也有一些特色,Mahjong Handle那个和原版Wordle一样,每天只有一道题,而且这个题是从题库里抽的,我让AI Agent看了一下它的代码,似乎是用[phoenix-logs](https://github.com/MahjongRepository/phoenix-logs)这个脚本从天凤上下载的牌谱数据解析得到的题库,所以它的题目数量是有限的,而我的代码是完全随机生成的,所以结果是无限的。但AI似乎认为作为Wordle来说一天一题才是它的特色,无限就显得没意思了……既然如此,那干脆让它给我把两个游戏合并了,让它参考Mahjong Handle把一天一题的功能也给我加上,另外Mahjong Handle还有一个MPSZ快速输入法,也让AI给我合并进我的游戏里了。
|
||||
这时候就体现了AI Agent的优势,如果直接用线上的AI把Mahjong Handle中的功能移植到我的代码,我还得完整描述要实现的功能,但是AI Agent能自己看人家仓库的代码,就知道要改什么东西了。
|
||||
最终实现的效果也很不错,虽然我的代码没有题库,但是使用了当天日期作为随机种子,用mulberry32算法搞出了一天一题的功能,还挺有意思的。
|
||||
另外在Mahjong Handle以及原版的Wordle中,还有一个困难模式,就是每次猜测必须使用之前的猜测条件,不过我觉得如果原模原样把这个功能抄过来似乎有点无聊,而且我发现玩这个东西一般直接一个国士+两个一气就基本上把要猜的牌找完了,有点简单,所以我想了一下,要不然限制一下提交的条件,提交的牌型必须包含目标的役种之类的……不过要是做这个功能就不能像之前那样只实现部分役种了,得把所有能用到的都实现一遍,还好这种事情肯定也不是我来做,肯定还得是让AI来😋。
|
||||
实现的时候还不能让AI直接一口气全加上,还得一步一步的提示,而且每个牌型还得手动测一遍……最终虽然做的看起来似乎已经很完善了,不过逻辑的根本还是有点问题,AI设计役种计算的时候是按拆解后役种数量最多的情况来算,但事实上立直麻将算役种是按照高点法,要最终点数最大才行。虽然按照目前的方法绝大多数情况下都能正常工作,但偶尔也会遇到问题,比如像这副牌:
|
||||
- 🀈🀈🀈🀉🀉🀉🀊🀊🀊🀞🀟🀗🀗 🀝
|
||||
|
||||
按照目前的方法计算的结果是:
|
||||
- 断幺·平和·一杯口 = 3番30符
|
||||
|
||||
然而正确答案是:
|
||||
- 断幺·三暗刻 = 3番50符
|
||||
|
||||
但是符数的计算又很麻烦……不过反正提示和验牌都是同一套程序,题倒是也能做出来……所以就懒得修了🤣。当然说到底还是因为AI没有用现有的计算库,像Mahjong Handle用的是现成的[Riichi](https://github.com/takayama-lily/riichi)库,制作起来比我这个就简单多了。
|
||||
|
||||
# 为游戏添加字体
|
||||
在我制作“立直麻将 Wordle”这个小游戏的时候,我让AI使用的是Unicode中的麻将字符,毕竟有现成的字符总比去找牌的图片要方便,而且实现起来感觉更简单一些。不过用字符确实也会遇到一些问题,我不知道为什么在macOS和Android上其他牌都是黑白的,唯有🀄️是彩色的😅,Windows上使用的Segoe UI Emoji倒都是彩色的,但是牌很丑,像是小学生设计的……虽然不影响玩,但是看着很难受,至少牌的颜色统一一下也好吧?虽然也可以搞出黑白的🀄︎,但如果用这个,在Windows上又成了只有🀄️是黑白的😅。虽然Mahjong Handle是直接用了SVG格式的牌图片,但我觉得应该有现成的麻将字体,像这种问题应该有现成的解决方法才对吧?
|
||||
于是我直接问AI有没有这样的字体,AI搜了一下看起来还真有,有个叫做[Mahjong Colored](https://github.com/kfarwell/Mahjong-Colored)就是基于那些SVG做的OpenType-SVG字体,我本来以为问题就会这样解决,然而当我应用了字体之后发现牌的位置变成了白色的方块,貌似只有Firefox才支持这种字体……然而现在谁还用Firefox啊?
|
||||
那遇到这种问题怎么办?当然还是让AI解决,它说要在Safari和Chrome上使用彩色字体,用COLR或者SBIX格式比较好,COLR格式的兼容性最好,三个浏览器都能用,而SBIX的话Firefox没法用。不过Firefox既然基本上没人用了,我倒是不太在乎它的兼容性,但我肯定还是更希望能用兼容性更好的方案,于是我把OpenType-SVG字体转换为COLR的工作交给了AI Agent。
|
||||
结果它整了半天也没整出来,貌似是因为SVG的特效太多,没办法转换为COLR格式,于是它选择了SBIX方案,不过COLR存储的是矢量图,而SBIX存储的是位图,大小要大一些,而且放大还会有锯齿。只是目前实在是没办法了,只能用这种格式。
|
||||
最终我把它转换后的字体存了一份到[仓库](https://github.com/Mabbs/Mahjong-Colored-SBIX),但是看着它1MiB多的大小以及不太好的浏览器兼容性,再考虑到它还是位图,实在是不太想在我的游戏里应用它……
|
||||
结果后来我又搜了一下,原来是有现成的麻将字体啊,有个叫做[Mahjong Font](https://github.com/rutopio/mahjong-font)的项目用的就是COLR格式,兼容性非常好,而且大小只有80KiB,算是最完美的解决方案了。垃圾AI给我推荐的Mahjong Colored真的是……浪费了不少时间,看来有的时候自己调查要比问AI好一些吧。
|
||||
|
||||
# 感想
|
||||
总的来看,目前Vibe Coding的体验确实是相当不错,只是问题也非常明显,完全让AI生成的代码要么实现的不优雅,要么就会出BUG,如果完全不懂要写的东西,纯粹由AI生成,那出问题的概率就非常高了。不过要是看不出来错误,完全被AI忽悠过去了,也许这时候的Vibe Coding在另一方面算是完美吧😂?至少对用的人来说也算是解决了问题呢。
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
layout: post
|
||||
title: 一次制作终端版博客的经历
|
||||
tags: [WASM, 终端, Linux, 博客]
|
||||
---
|
||||
|
||||
在浏览器上用真的Linux访问博客会是什么样的感觉?<!--more-->
|
||||
|
||||
# 起因
|
||||
在去年的时候我为了想在博客上加个终端,写过一篇关于[在浏览器中运行Linux](/2025/12/01/linux.html)的文章。不过那时候只是调查了有哪些可能的方法,并没有真的做出来,主要是自己做感觉太麻烦了😂。
|
||||
但从上次进行了[Vibe Coding](/2026/07/01/vibe-coding.html)之后,我意识到这可能不再是什么麻烦事,也许只要和AI聊聊天就能轻松做出来了,于是接着之前的方案,我开始着手完成我的想法。
|
||||
|
||||
# 终端版博客的不同形式
|
||||
## 基于WASM Linux的版本
|
||||
在之前的文章里,我其实更倾向于WASM Linux的方案,尤其是[Thomas Stokes](https://github.com/tombl)制作的[WASM Linux](https://github.com/tombl/linux),因为它相比于虚拟机更轻量,效率更高,而且还能完美地在Safari中运行(毕竟我用的是macOS😂),因此接下来我计划用它为基础制作我的终端版博客。
|
||||
不过之前我也说过,目前这个WASM Linux还不能和外界通信,不能和外界通信就不能读取我的文章。于是我打算先让AI解决这个问题。但如果我直接给它说想要和外界通信也许它会给我整一堆奇怪的实现,所以我得自己先想一个实现起来最简单的方案。之前我记得Fabrice Bellard大神写的JSLinux中有一个剪贴板设备文件可以和外部通信,那么干脆让AI写一个能在浏览器中执行JS的设备文件吧,这样做出来只需要写简单的Shell脚本就可以和浏览器交互了,也不需要考虑如何编译WASM程序,于是就这样我开始让它实现这个功能。
|
||||
最终经过几次调试和修改之后AI真的把这个设备文件给做出来了,不过刚做出来的时候有些问题。因为在这个Linux中的进程都是在Web Worker中运行,执行的JS也会在Web Worker中运行。但是Web Worker的局限性很大,并不能和当前网页进行交互,不过这种问题也都是小事,发现了之后只要给AI一反馈也是轻轻松松就改好了……最终实现的效果我感觉相当不错,既然如此,那我就把AI做的这个驱动给上游[PR](https://github.com/tombl/linux/pull/24)一下吧,也算做个贡献。不过最终原作者不太喜欢这种实现方式,他好像更喜欢标准化的方式,说是已经做了一个[VsockDevice](https://github.com/tombl/linux/blob/b36416c7f266a465361ab7dde7622eb37239cd7e/tools/wasm/src/virtio.ts#L354),可以直接使用Linux自带的驱动和外界通信,所以把我的PR拒了(看来AI Slop还是入不了大佬的法眼😂)。 ~~不过我问了一下AI,它说用这个设备的话可能需要`ioctl()`系统调用,就没办法直接用Shell操作了,只能用别的语言写的程序才能用它通信,所以我只好自己用这种不标准的方式了。~~ 虽然VsockDevice确实是不太好操作,但是大佬随后又新增了一个ethernetDevice可以用来上网,考虑到维护自己的分支实在是太麻烦了,所以我让AI写了一个虚拟的HTTP服务,使其能够执行JS,顺便还增加了连接互联网的功能。这样我就可以直接使用上游的代码,不需要再维护之前写的驱动了(其实以太网比Vsock复杂多了,只是因为可以用wget之类的客户端请求所以才简单😋)。
|
||||
在做好与外界通信的通道后我就开始让AI写能和我博客交互的脚本,不过在这个过程中也遇到了不少问题,最开始的时候发现这个Shell的管道有些问题,执行的时候会忽略管道前的命令,后面的命令就没法执行了……感觉有点奇怪,所以我要求它在实现交互脚本时不要使用管道符。除此之外我还发现反引号和`$()`这些能把执行结果插入字符串的语法也不能用……总之就是局限相当多,但它还是克服了这些困难最终把脚本给写出来了,最终实现了列出文章、查看文章、打开文章和查看评论等功能,效果还是相当不错。 ~~唯一的缺陷就是它不能进行异步操作,只能同步请求,所以在进行请求的时候会把浏览器卡住,体验稍有打折。~~ 后来用了网络版的JS执行服务后就可以异步了,避免出现把浏览器卡住的情况。
|
||||
不过这个系统如果管道符不能用的话效果会差一大截,这可是Shell的灵魂啊,所以我提交了一个[Issue](https://github.com/tombl/linux/issues/25),希望作者能修一下。结果作者真的强得离谱,提出问题之后第二天就修复了,实在是太强了😨。但修好这个问题之后作者又整了一堆别的花活,把内存限制给取消了,结果我在Safari上一运行就报OOM的错误……于是我把这个问题又[反馈](https://github.com/tombl/linux/pull/26#issuecomment-5018155963)了一下,顺便让AI看看这个问题怎么解决,结果AI也不辜负我的期待,轻轻松松就解决了🤣。最终我除了会提出问题之外什么也不会做,只能看着大佬和AI完成我的想法了😝。
|
||||
后来增加的以太网网关也挺折腾,原理其实并不复杂,但直接给AI说想用这个接口上网的时候它会自己想办法搓一套漏洞百出的TCP/IP协议栈,所以后来我在GitHub找了个[tcpip.js](https://github.com/chipmk/tcpip.js)库让它集成,BUG就比它自己手搓的少了不少。后来我又想加一个TLS终止功能,因为在浏览器上没办法直接让系统里的程序发送原始的TLS数据包去请求网站,但我又希望它能够请求带有https协议的网址,所以要提前对TLS进行解密再转换成`fetch()`,这时候我就没让AI自己手搓了,在网上找了个[node-forge](https://github.com/digitalbazaar/forge)库让AI对接,就可以避免出现一堆奇怪的BUG了。
|
||||
做好网关之后我又突发奇想,想做一个展示终端内状态的页面,但是目前JS并不能主动和里面的系统直接通信,所以又让AI搓了一个通过Service Worker访问系统内网络服务的功能,然后用busybox的httpd执行sh写的cgi程序,就可以被前端读取到了。随后我还给原作者发了个[Issue](https://github.com/tombl/linux/issues/34)展示这个Service Worker的效果,结果人家已经做了这个功能,我还是太Naive了😂。
|
||||
最终做好的网站可以在[这里](https://mabbs.github.io/linux/)查看,效果真的不是一般的好,相比于其他模仿终端形式的博客简直是降维打击,毕竟这是真的Linux啊~虽然目前依然有些BUG,但是原作者修BUG的速度还是挺快的,相信等他发布正式版之后,体验会更进一步。
|
||||
|
||||
## 基于JSNix的版本
|
||||
虽然我觉得追求真实感很重要,但是我又回去看了一下为我提供想法的[JSNix](https://github.com/Erzbir/jsnix)。虽然这个项目并不是真的Linux,但是我让AI看了一遍之后它对这个项目的评价也很高,说是作者对Linux的了解非常深入,很多细节模拟的也非常到位。而且为WASM Linux制作的那些脚本,在这个JSNix上其实完全可以实现,还更简单,于是我直接让AI参考给WASM Linux写的脚本把那些功能全部移植到这个JSNix上。也许是因为AI更擅长编写网页,很快它就完成了这些功能,而且效果非常不错,基本上和WASM Linux上面的效果一样,表现也相当不错,最终生成的结果可以在[这里](https://mabbs.github.io/jsnix/)查看。
|
||||
不过假的毕竟还是假的,和真的Linux差异还是有不少的,像正经的Shell可以在终端中直接用if、for和while之类的语法写一些东西,它这个就必须要编写成脚本才可以,而且解析也有相当多的问题。各种命令的运行结果很多都是硬编码的,只是编了一段对应命令的输出,参数做得也不全。另外著名的Linux文本三剑客grep、sed和awk在这个系统里实现得也非常差,只做了一些非常基础的功能,基本上就是完全不可用。所以说这个系统还是不够真实,只能做最基础的体验,不能够深究。
|
||||
但并不是说它没有优点,它作为纯粹的JS不像WASM Linux那样容易出现兼容性问题,比如很多手机浏览器不支持跨源隔离,导致SharedArrayBuffer之类的API没法用,所以手机上只能体验这个JSNix的版本;另外WASM Linux的体积虽然说并不算很大,但也有好几MiB,不像这个JSNix连1MiB都不到;还有这个系统没有启动过程,相比于WASM Linux来说加载更快;而且在编写和修改功能的时候比WASM Linux简单不少,像给WASM Linux做的终端状态展示在JSNix上根本不需要折腾什么Service Worker,以及请求网络之类的也非常简单,直接`fetch()`就可以(也正是如此,JSNix的版本AI没一会儿就做完了)……总之就是只要不涉及具体功能方面JSNix都比WASM Linux要强不少,不过这样说有点“抛开事实不谈”的感觉了😂。
|
||||
|
||||
# 感想
|
||||
最终做出来的这两个版本的终端版博客基本上所有的代码都不是我写的,真的做到了仅仅是和AI聊聊天就把东西做出来了……只能说AI现在真的不是一般的强啊~当然方案还是得自己考虑,因为AI一旦走错了方向就很容易往一个方向不停地钻,结果就是花了很长时间可能效果会很差,尤其是现在的AI还特别喜欢自己造轮子,所以这时候还是自己找找看已经有的方案然后告诉它会比较好。
|
||||
从效果来说这次写的东西也让我非常满意,我想我这次做的这个博客界面估计是前所未有的,毕竟这可是在浏览器上用**真的**Linux访问博客啊,其他人做的都是假的。至于虚拟机那些方案可能也是因为太重,所以基本上没人用,而WASM Linux只有Thomas Stokes做的效果最好,但是又没有受到应有的关注……总之从我个人的角度来看这次做的东西还挺Cool,就是不知道会不会吸引其他人来看呢……可能还是太小众了吧😂。
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
layout: post
|
||||
title: Vibe Coding 新体验(2)
|
||||
tags: [Vibe Coding, AI, Agent]
|
||||
---
|
||||
|
||||
AI is Everything!<!--more-->
|
||||
|
||||
# 起因
|
||||
在两个月前,我[体验了现在的Vibe Coding](/2026/07/01/vibe-coding.html),不过那时候我其实还是没有完全信任AI,认为它的能力可能还不够强,大多数内容依然需要人来做。
|
||||
但是自从上次做了[终端版博客](/2026/08/01/terminal.html)之后,我意识到,它的能力也许比我想象的更强,可能真的能做到超越99%的开发者了,因此在最近一段时间,我一直使用AI做我想做的各种东西,结果就是一次又一次地让我感到难以置信,现在的AI真的实在是太强了😨。而且我做的这一切并不是用最先进的那批AI,仅仅是国内被大家不怎么看好的免费模型——[腾讯混元Hy3](https://huggingface.co/tencent/Hy3),那么我很难想象,如果我能用像GPT 5.6 Sol或者Fable 5那样当下最先进的AI,我做的这些东西得有多完美啊……
|
||||
|
||||
# 最近Vibe Coding出来的各种项目
|
||||
## Blog API + WebMCP
|
||||
在上次做完[终端版博客](/2026/08/01/terminal.html)之后,我就在想,用Linux终端来呈现博客的形式真的是个好选择吗?毕竟Linux在浏览器上并不是原生的,尽管用了[WASM Linux](https://github.com/tombl/linux)可以做到WASM原生运行Linux,但这还是额外添加的东西。于是我就想,JavaScript应该也能作为终端吧?就像Node.js一样,REPL也可以作为终端使用,正好那段时间DeepSeek-V4-Flash正式版出了,就让它直接写了[一个Demo](https://github.com/Mabbs/jsnix/blob/main/repl.html)出来试试看,可以点开[这里](https://mabbs.github.io/jsnix/repl.html)体验。
|
||||
写出来效果还挺不错的,和之前做的终端版博客的主要区别就是我写的是函数,而不是命令,不管怎么说也算是“真的”终端吧,唯一体验不太好的可能就是函数需要带个括号才能运行,稍微有点麻烦。
|
||||
但后来我想了想,明明开发者工具中就有个控制台,根本没必要搞什么REPL终端啊,直接把要用的功能提出来作为函数,在控制台中运行效果比那个终端的效果要好得多啊,而且这样就可以直接集成到我的博客中,不需要单独搞个页面,感觉体验更好一些。
|
||||
既然有了函数,我觉得只给人类用就太浪费了,前段时间看到谷歌搞了个[WebMCP](https://webmachinelearning.github.io/webmcp/),那我完全可以把这些函数做成WebMCP让AI来调用啊,相比于其他乱七八糟给AI的协议,至少WebMCP是有谷歌和微软撑腰的,所以加一个也不错。
|
||||
最终我把WebMCP的文档以及之前实现的REPL代码直接发给AI,让它帮我实现对应的函数以及应用WebMCP API,没几分钟它就把所有功能实现好了,最终代码就是:[blog-console.js](/assets/js/blog-console.js)。在开发的过程中,AI严格按照文档中的要求实现了,结果我在实际测试的时候,发现Chrome在实现`registerTool`方法的时候和文档的实现有差异,文档说这个方法会返回Promise,但Chrome实际并没有返回,所以AI调用`.then()`时直接报错了😓……没办法就只能手动给它套一层Promise,才算是能正常工作。毕竟这还是一个没有正式上线的API,出点问题倒也能理解吧。
|
||||
虽然东西已经实现了,不过到底怎么才能让其他人知道我用了WebMCP的技术呢?我发现网上有一个WebMCP Directory专门收录使用了WebMCP技术的网站,于是我就随手提交了一下,现在在[这里](https://webmcp.com/sites/mabbs.github.io)就能看到我博客的WebMCP信息了。
|
||||
## 某个即将关服的游戏的离线快照
|
||||
最近一段时间我在玩的一个使用Unity3D引擎制作的立直麻将游戏发了篇公告,说是要关服了,还给全服所有玩家送了一堆抽卡资源。我听到这个消息之后感觉还是挺遗憾的,那个游戏总体来说做得还算不错吧,而且画风也符合我的口味,角色模型有点米哈游的那种感觉。我曾经遇到过几次游戏关服的事件,但是每一次我都无能为力,只能看着游戏消失然后逐渐被遗忘。然而现在我有了AI,也许一切会变得不一样吧?这次我要使用AI,让即将消失的游戏不再消失。
|
||||
### 重放大厅数据
|
||||
最开始我其实想得很简单,游戏登录之后其实没有和服务器有太多的通信,像账号中有的角色以及各种材料等信息在登录的时候就已经发送了,直到进入对局之前客户端都没有和服务器有什么有效的沟通,而我主要需要的也就是能登进去看看角色,听听语音就好了。那么接下来我只需要让AI把整个游戏资源下载下来,然后抓个包,让它给客户端重放就好了,也不需要什么逆向操作。
|
||||
AI分析了一下之后发现它用的是WebSocket+Protobuf协议,正常来说如果要使用Protobuf协议得要有.proto接口定义文件才可以,不过既然我的要求只是重放,它就没去考虑逆向了,只是简单分析了一下编码规则,就用Python把服务端给写出来了。
|
||||
不过如果这个快照必须本地启动一个服务器,玩起来就有点麻烦了,这样搞得我估计就没心思玩它了,但是自从我做了终端版博客之后,我觉得浏览器可能没有做不到的事情,既然Python能运行服务器,那JavaScript一样可以,于是我就要求AI让游戏和服务端一起运行在浏览器中。
|
||||
结果它还真折腾出来了,在JavaScript中似乎一切都可以被覆盖,于是AI就替换了浏览器的WebSocket对象,让客户端以为它连接到了真实的服务器,然后再按照之前录制的包发送就好了,折腾了一段时间之后游戏真的能正常启动了,真的是太离谱了😨。
|
||||
### 制作对局引擎
|
||||
其实这基本上已经满足我的要求了,但我还是有点好奇,这个客户端里真的没有.proto接口定义文件吗?正常来说既然客户端知道怎么和服务端通信,那么这个文件应该是必须要有的吧?反正目的已经达到了,接下来就让AI瞎折腾看看。
|
||||
在我要求完之后它又折腾了好长时间,结果.proto也被它给搞出来了😨,也许对于会逆向Unity3D游戏的人来说这不算什么,但这已经让我很震惊了……既然有了这些,也许完成一个完整的服务端将不再是难事。
|
||||
于是我让它录制了几把我实际对局的包,并理解整个对局中数据包的时序,开始考虑如何做出完整的麻将对局。首先作为离线运行的游戏来说,和人对局肯定是做不出来了,那就得想办法搓个人机出来。我最先想到的是著名的麻将AI [Mortal](https://github.com/Equim-chan/Mortal),之前用它在雀魂上打了几把之后结果号没了😂,不过我确实觉得和人打麻将没什么意思,感觉麻将如果全按最优解打的话结果就全是靠运气……不过后来我想了一下还是不用Mortal了,首先这个模型不小,整个模型要好几百MiB,而且三麻和四麻的模型还不一样,还有三麻的模型还是收费的……另外真和Mortal打的话感觉难度也确实有点高,除了运气因素外,至少人机犯错的概率比人要低不少,那打起来估计会很难受。所以我就对AI说,做一个降低向听数为主的全攻型人机,其他细节让它自己考虑,另外我想到上次做的那个麻将Wordle它自己写的役种分析实在是有点烂,所以这次就告诉它必须要使用现成的[Riichi](https://github.com/takayama-lily/riichi)库,免得出乱七八糟的问题。
|
||||
在连续跑了好几个小时之后AI成功把完整的[对局引擎](https://github.com/Mabbs/mahjong-dream-snapshot/blob/main/mock/riichi.js)给搓出来了,真的是相当了不起,要知道这个东西正常来说人开发怎么说也得好几周吧?它就几个小时开发出来了,还理解了数据包的时序,用之前分析的.proto文件成功接入到了游戏中,实在是太强了……当然因为我没有用大量真实牌谱去测试,有BUG也是正常的,比如目前的引擎还存在立直之后完全没法开杠这样的边缘问题,就只能慢慢修了,不过这都是小问题,它能做到目前的水平已经不是一般人能做到的了。
|
||||
最终做出来的项目可以在[这里](https://github.com/Mabbs/mahjong-dream-snapshot)看到,还可以通过[GitHub Pages](https://mabbs.github.io/mahjong-dream-snapshot/)上的部署直接游玩。
|
||||
### 隐藏彩蛋
|
||||
在AI做完整个服务端之后,我闲得没事瞎改对战人机的角色和皮肤,因为游戏中的皮肤ID有一定的规律,所以可以根据角色ID来猜皮肤ID。在这个时候,我发现其中有一个角色在游戏中没有对应的泳装皮肤,但是我把人机的角色按照规律配置推测的泳装皮肤ID是能显示出来的……那也就是说这个游戏可能在配置中隐藏了这个皮肤,但是并没有删掉美术资源,所以接下来我就想着能不能让AI把这个隐藏的皮肤显示出来,这样就能在游戏中直接使用了。
|
||||
所以我根据自己推测的信息,告诉AI:在这个游戏中有一个皮肤编号3708002,头像编号1018的资源被隐藏了,其中关于这些资源的配置应该在commonconfigs中,帮我解包并分析,考虑如何将这个皮肤和头像恢复显示,然后重新打包资源,使其可以正常加载。就这么个需求,它从Bundle的解包,到分析FlatBuffers,然后又处理XAsset的校验……最终花了3个小时就给我做出来了,而且我试了一下,非常完美,简直就像这个皮肤本来就存在一样,不过原本这就是被官方隐藏的嘛,解除隐藏自然就和真的存在一样啊😂。
|
||||
## 能在WASM Linux中运行的SSG
|
||||
之前我做完终端版博客之后就在想,让这个完整的Linux只用来展示博客似乎有点浪费,感觉它还能做更多事,但对我来说编译在这个系统中能运行的二进制程序还是有一点困难,虽然我有用AI,但是我的Agent是运行在macOS上的,所以编译Linux二进制程序有点困难……不过单是BusyBox其实就能做相当多的事情了,毕竟我当年可是在手机上用ash做出了论坛系统[Mabbs](https://github.com/Mabbs/Mabbs.project),那它能做的事情应该还有很多。
|
||||
最近我在和群友聊天的时候发现有人用C#做了个静态网站生成器,也是用AI做的,看起来挺有意思。其实SSG的原理还是挺简单的,说到底就是些文字转换罢了。于是我就想,要不然我就让AI搓个能在WASM Linux中运行的SSG出来吧?
|
||||
当然直接搓我还是会有一点怀疑AI的能力,所以又找了点素材,之前在[体验Tilde社区](/2025/08/10/tilde.html)的时候发现了[bashblog](https://github.com/cfenollosa/bashblog),其实这就已经是一个现成的SSG了,只不过它用的是Bash语法,得让AI转换成POSIX风格的语法。另外它还依赖使用Perl语言编写的[Markdown库](https://daringfireball.net/projects/markdown/),我这个系统里可没有Perl啊,也得想办法找个可以替代的东西。
|
||||
我想了一下,在BusyBox中最适合处理文本的大概就是awk了,所以我首先应该找找有没有用awk编写的Markdown转换程序。结果搜了一下还真被我找到了,有一个叫做[d.awk](https://github.com/wernsey/d.awk)的库,里面包含一个完全用awk写的Markdown转换程序,那么接下来我只需要把这两个项目丢给AI,让它以bashblog为基础,以d.awk为引擎制作一个兼容性极强的POSIX SSG吧。
|
||||
这种问题果然还是太简单了,它只用了一两个小时的时间,就真的做出来了,最终它把这个项目起名叫做[possg](https://gist.github.com/Mabbs/3db745dc403d752a1d59d3e06938e310),并且我把这个程序粘贴到了我的终端版博客中运行了一下,效果也是非常完美……如果哪一天我抛弃了Jekyll,也许就会用这个possg吧😂。
|
||||
## 终端版博客的各种新功能
|
||||
最近一段时间我在我的[终端版博客](https://github.com/Mabbs/linux)又有一些新的想法,于是就对着AI反复许愿,结果AI还真的能让愿望成真。以下就是从上次做完之后又新增的一些功能:
|
||||
### Markdown高亮
|
||||
最初博客在终端中显示文章时是直接打印的Markdown原文,感觉不太好看,所以我就想着如果能加个高亮功能体验应该会更好吧?最开始我要求AI做这个功能的时候它选择使用Shell+sed来实现,那时候我还没有做possg,没考虑用awk来做,所以性能非常差。因为Shell在处理这种任务的时候要不停地调用sed,这个系统开进程的开销又很大,所以处理一篇文章得好几秒,后来才考虑用awk来做,结果性能确实好了不少,最终的代码可以在[这里](https://github.com/Mabbs/linux/blob/initramfs/bin/mdhl)查看。
|
||||
### 挂载本地文件夹
|
||||
前段时间我看到作者合并了一个关于[virtiofs的PR](https://github.com/tombl/linux/pull/40),当时我看到这个PR的时候就想,记得Chrome浏览器有一个[showDirectoryPicker API](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/showDirectoryPicker),可以允许网页对指定的文件夹进行读写。现在有了这个驱动的话,让AI做个将本地文件夹挂载到Linux的功能应该是个很简单的事情,于是我直接就闭着眼睛让AI做了。
|
||||
做完之后虽然能用,但是我又回想起上次让AI搓的通过Service Worker访问系统内网络服务的功能,结果人家原作者已经做了一样的功能,我还给作者发了[Issue](https://github.com/tombl/linux/issues/34)推荐😅,所以这次我得吸取教训看看作者是不是已经做了这个功能,毕竟作者实现virtiofs的时候估计就有这个打算吧?结果看了一眼还真[做了](https://github.com/tombl/distro/blob/main/packages/linux-guest/src/browser.ts)😆,我闭着眼睛让这个垃圾AI去做看来确实有点着急了,虽然我不知道原来我的AI做得到底怎么样,但是我对它还是没什么信心,也许作者这个代码也是AI写的,但很明显他的AI比我的AI水平强得多,所以让它参考作者做的这个版本重新写了一版。总之最终都能用,写出来的代码就是:[hostfs.js](https://github.com/Mabbs/linux/blob/main/hostfs.js),再配合上[mount_host](https://github.com/Mabbs/linux/blob/initramfs/bin/mount_host)脚本就可以实现挂载本地文件夹的功能了。
|
||||
### 基于WebRTC的局域网内组网
|
||||
我在制作网络模块的时候一直在想,有没有办法能在不依靠服务器的情况下让两个这样的Linux系统互联呢?正常来说要想让两个浏览器内的服务互相访问就必须有一个中转服务器,因为浏览器肯定是不允许网页监听端口,而且就算能监听,我的这个网页还是放在GitHub Pages上,使用了HTTPS,也没办法访问HTTP资源,搞证书也是个麻烦事。
|
||||
但其实浏览器是有P2P功能的,那就是[WebRTC](https://developer.mozilla.org/zh-CN/docs/Web/API/WebRTC_API),只是我不太确定这个技术能不能做到无服务器,于是我问了问AI,这个功能能不能不要服务器,结果AI说可以,只要两个机器在同一个局域网,就可以通过手动交换SDP信令来实现P2P连接,而且考虑到我基本上完全没有锥形NAT网络可以用,所以就不搞STUN服务器了,反正也连不上。那既然理论上可以的话,就让AI实现吧。
|
||||
最终花了一点时间AI就写出来了,使用[webrtc-p2p.js](https://github.com/Mabbs/linux/blob/main/webrtc-p2p.js)配合[connector](https://github.com/Mabbs/linux/blob/initramfs/bin/connector)就可以实现同一个局域网内两台电脑上的浏览器中的Linux互联了。
|
||||
这个功能做出来之后直接就有了无限的想象空间,比如想用A电脑的浏览器控制B电脑浏览器中的Linux,就可以在B电脑中执行`tcpsvd -v 0.0.0.0 4444 /usr/bin/script -qc 'stty -echo; /bin/sh -i' /dev/null`,然后在A电脑用`nc 10.0.2.x 4444`去连接就可以了,效果就像SSH一样,而且配合之前做的挂载本地文件夹功能,甚至可以直接操作B电脑中的文件;另外之前做的Service Worker访问系统内网络服务也能用得上,假如B电脑有一个网页想要分享给A电脑,可以在B电脑上用`mount_host`把本地文件夹挂载到Linux的`/mnt/host`目录中,然后在那个目录执行`httpd -p 8080`,A电脑可以执行`tcpsvd -v 0.0.0.0 8080 /usr/bin/nc 10.0.2.x 8080`转发,接下来打开`http/8080/`就可以访问到B电脑上的网页了。
|
||||
总之只要有了网络,这个系统的功能就能被无限扩大了,只是……说是这么说,其实还是依赖浏览器的功能,所以这些功能就算用JavaScript直接做也完全没问题,但毕竟是作为Linux终端,有了这些能力之后就显得更完整了。
|
||||
最终我仔细确认了原作者确实没有做这个功能,所以就发了个[Issue](https://github.com/tombl/distro/issues/156)看看他有没有兴趣把这个功能加到他的库中,如果被接受了也算是做出贡献了吧。
|
||||
### 使用VLESS协议的网关
|
||||
虽然之前已经加了基于浏览器Fetch API的网关,但这个网关的局限性还是挺大的,基本上也就只能请求网站了。那其他能在浏览器上运行系统的项目是怎么做的呢?看了一下像v86用的是一个基于L2 Over WS的公共网关relay.widgetry.org,而WebVM用的则是Tailscale。这两种我都不喜欢,主要是这些东西基本上专门服务于这类在浏览器中运行的系统,普通人并不会特意去搭建,而我既不想用某个特定的服务,也不想自己搭……那么有什么样的网络服务既随处可见,又能在浏览器中使用呢?这时候我就想到了VLESS协议,它天生就适合运行在WebSocket中,而且搭建起来极其简单,比如有人用Cloudflare Workers做了一个叫做[edgetunnel](https://github.com/zizifn/edgetunnel)的脚本,可以轻松将Cloudflare作为代理使用,因此我决定接下来让AI实现一个基于VLESS+WSS的网关,这样除了请求网站之外还能做点别的事情,比如用`telnet towel.blinkenlights.nl`命令看ASCII版的《星球大战》😆。
|
||||
只不过相比于其他项目用的那些L2的服务来说,VLESS承载的是L4的协议,所以我还得让AI实现TCP/IP协议栈……其实我不太想这样做的,因为AI做这种事情容易出错,但是它看了一下我之前用的[tcpip.js](https://github.com/chipmk/tcpip.js)库说是里面的API不满足要求……那没办法,AI既然自己想搓就让它搓吧。不过VLESS协议这个我觉得还是得给它个[文档](https://github.com/XTLS/Xray-docs-next/blob/main/docs/en/development/protocols/vless.md),免得它开始瞎编。
|
||||
不管怎么说它还是搓出来了,虽然质量我不是特别满意……但毕竟手头用的AI不是最先进的AI,就先这样吧~最终的代码就是:[vless-gateway.js](https://github.com/Mabbs/linux/blob/main/vless-gateway.js),用法也很简单,首先要有一个**证书合法**的VLESS+WSS节点,然后在我的博客终端中按F12打开控制台,输入:`switchToVless("wss://domain:port/path", "UUID")`就可以将原来基于Fetch的网络切换到VLESS节点的网络了,考虑到避免被滥用的情况,我就不内置节点了,如果有人有兴趣可以用自己的节点进行连接。
|
||||
|
||||
# 感想
|
||||
做到这里,可以感受到最近的AI所做的一切真的是令人难以置信,它不仅知道几乎所有知识,还有不知疲倦的思考能力,以及足够的动手能力,而这也仅仅建立在一个很一般的免费模型上。再回想起前段时间用的Agent,做的什么[订阅源预览](/2025/04/08/feed.html),简直就太简单了😂。目前据我所知其他人使用更先进的AI已经可以解决人类没能解决的数学难题、自主设计处理器指令集架构在上面运行Doom、[从零开发一个Classic Mac OS](https://github.com/Kelsidavis/System7)、制作直接在UEFI中运行的软件、以及完全逆向Unity3D游戏,把几款游戏混合起来做成一个新游戏……(至于网上总传的什么DeepSeek的所谓的灰测模型搞出来的[各种Demo](https://github.com/6WENHAO/DS-Games)就算了,根本比不了)过去各种难以想象的东西都被轻松做出来了,而这一切就发生在最近几个月,我已经很难想象未来AI能做到什么样的地步了,也许AI真的会带领人类走向无法想象的高度吧~
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,572 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""LZMA2 7z quine 生成器,仅依赖 Python 标准库。
|
||||
|
||||
用法:python blogquine.py 源目录 输出.7z [--quine-name 名称]
|
||||
[--src-dir 前缀目录] [--quine-dir 自身所在目录]
|
||||
解压得到源文件及输出归档自身;内容原样存储,辅助信息位于归档内部。
|
||||
"""
|
||||
import argparse
|
||||
import binascii
|
||||
from dataclasses import dataclass
|
||||
import lzma
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import time
|
||||
|
||||
SIG = b"7z\xbc\xaf'\x1c"
|
||||
VER = b"\x00\x04"
|
||||
CHUNK = 1 << 16 # LZMA2 未压缩 chunk 上限
|
||||
MAX_CHUNK_OUTPUT = 1 << 21 # LZMA2 压缩 chunk 解压上限
|
||||
MAX_MATCH = 273
|
||||
PROPS_BYTE = 0x5D # lc=3, lp=0, pb=2
|
||||
FILETIME_EPOCH_OFFSET = 116444736000000000
|
||||
PROB_INIT = 1024
|
||||
|
||||
|
||||
class RangeEncoder:
|
||||
"""LZMA 区间编码器;所有整数截断与 SDK 的 UInt32 运算一致。"""
|
||||
|
||||
def __init__(self):
|
||||
self.low = 0
|
||||
self.range = 0xFFFFFFFF
|
||||
self.cache = 0
|
||||
self.cache_size = 1
|
||||
self.buf = bytearray()
|
||||
|
||||
def _shift_low(self):
|
||||
carry = self.low >> 32
|
||||
if (self.low & 0xFFFFFFFF) < 0xFF000000 or carry:
|
||||
temp = self.cache
|
||||
while self.cache_size:
|
||||
self.buf.append((temp + carry) & 0xFF)
|
||||
temp = 0xFF
|
||||
self.cache_size -= 1
|
||||
self.cache = (self.low >> 24) & 0xFF
|
||||
self.cache_size += 1
|
||||
self.low = (self.low << 8) & 0xFFFFFFFF
|
||||
|
||||
def _normalize(self):
|
||||
while self.range < 1 << 24:
|
||||
self.range = (self.range << 8) & 0xFFFFFFFF
|
||||
self._shift_low()
|
||||
|
||||
def encode_bit(self, probs, index, bit):
|
||||
probability = probs[index]
|
||||
bound = (self.range >> 11) * probability
|
||||
if bit:
|
||||
self.low += bound
|
||||
self.range -= bound
|
||||
probs[index] -= probability >> 5
|
||||
else:
|
||||
self.range = bound
|
||||
probs[index] += (2048 - probability) >> 5
|
||||
self._normalize()
|
||||
|
||||
def encode_direct_bits(self, value, bits):
|
||||
for shift in range(bits - 1, -1, -1):
|
||||
self.range >>= 1
|
||||
if value >> shift & 1:
|
||||
self.low += self.range
|
||||
self._normalize()
|
||||
|
||||
def bit_tree(self, probs, offset, bits, symbol, reverse=False):
|
||||
node = 1
|
||||
shifts = range(bits) if reverse else range(bits - 1, -1, -1)
|
||||
for shift in shifts:
|
||||
bit = symbol >> shift & 1
|
||||
self.encode_bit(probs, offset + node, bit)
|
||||
node = (node << 1) | bit
|
||||
|
||||
def finish(self):
|
||||
for _ in range(5):
|
||||
self._shift_low()
|
||||
return bytes(self.buf)
|
||||
|
||||
|
||||
class LengthEncoder:
|
||||
"""普通 match 和 rep0 使用独立概率,但共享长度编码逻辑。"""
|
||||
|
||||
def __init__(self):
|
||||
self.choice = [PROB_INIT] * 2
|
||||
self.low = [PROB_INIT] * 32
|
||||
self.mid = [PROB_INIT] * 32
|
||||
self.high = [PROB_INIT] * 256
|
||||
|
||||
def encode(self, rc, pos_state, length):
|
||||
value = length - 2
|
||||
rc.encode_bit(self.choice, 0, value >= 8)
|
||||
if value < 8:
|
||||
rc.bit_tree(self.low, pos_state << 3, 3, value)
|
||||
else:
|
||||
rc.encode_bit(self.choice, 1, value >= 16)
|
||||
if value < 16:
|
||||
rc.bit_tree(self.mid, pos_state << 3, 3, value - 8)
|
||||
else:
|
||||
rc.bit_tree(self.high, 0, 8, value - 16)
|
||||
|
||||
|
||||
class LzmaEncoder:
|
||||
"""只编码 match/rep0,编码结果不依赖历史字节,供布局后填充数据。"""
|
||||
|
||||
def __init__(self, out_pos=0):
|
||||
self.rc = RangeEncoder()
|
||||
self.pos = out_pos
|
||||
self.state = 0
|
||||
self.is_match = [PROB_INIT] * (12 << 4)
|
||||
self.is_rep = [PROB_INIT] * 12
|
||||
self.is_rep_g0 = [PROB_INIT] * 12
|
||||
self.rep0_long = [PROB_INIT] * (12 << 4)
|
||||
self.pos_slot = [PROB_INIT] * (4 << 6)
|
||||
self.spec_pos = [PROB_INIT] * 114
|
||||
self.align = [PROB_INIT] * 16
|
||||
self.match_len = LengthEncoder()
|
||||
self.rep_len = LengthEncoder()
|
||||
|
||||
def _write_dist(self, dist, length):
|
||||
if not 1 <= dist <= 0xFFFFFFFF:
|
||||
raise ValueError("LZMA distance must be 1..2^32-1")
|
||||
value = dist - 1
|
||||
bits = max(0, value.bit_length() - 2)
|
||||
slot = value if value < 4 else 2 * (bits + 1) + (value >> bits & 1)
|
||||
self.rc.bit_tree(self.pos_slot, min(length - 2, 3) << 6, 6, slot)
|
||||
if slot >= 4:
|
||||
base = (2 | (slot & 1)) << bits
|
||||
extra = value - base
|
||||
if slot < 14:
|
||||
self.rc.bit_tree(self.spec_pos, base - slot - 1, bits, extra, True)
|
||||
else:
|
||||
self.rc.encode_direct_bits(extra >> 4, bits - 4)
|
||||
self.rc.bit_tree(self.align, 0, 4, extra & 15, True)
|
||||
|
||||
def match(self, dist, length):
|
||||
if not 2 <= length <= MAX_MATCH:
|
||||
raise ValueError("LZMA match length must be 2..273")
|
||||
self.rc.encode_bit(self.is_match, (self.state << 4) + (self.pos & 3), 1)
|
||||
self.rc.encode_bit(self.is_rep, self.state, 0)
|
||||
self.match_len.encode(self.rc, self.pos & 3, length)
|
||||
self._write_dist(dist, length)
|
||||
self.state = 7 if self.state < 7 else 10
|
||||
self.pos += length
|
||||
|
||||
def rep_match(self, length):
|
||||
if not 2 <= length <= MAX_MATCH:
|
||||
raise ValueError("LZMA rep0 length must be 2..273")
|
||||
index = (self.state << 4) + (self.pos & 3)
|
||||
self.rc.encode_bit(self.is_match, index, 1)
|
||||
self.rc.encode_bit(self.is_rep, self.state, 1)
|
||||
self.rc.encode_bit(self.is_rep_g0, self.state, 0)
|
||||
self.rc.encode_bit(self.rep0_long, index, 1)
|
||||
self.rep_len.encode(self.rc, self.pos & 3, length)
|
||||
self.state = 8 if self.state < 7 else 11
|
||||
self.pos += length
|
||||
|
||||
def finish(self):
|
||||
return self.rc.finish()
|
||||
|
||||
|
||||
def crc32(data, value=0):
|
||||
return binascii.crc32(data, value) & 0xFFFFFFFF
|
||||
|
||||
|
||||
def varint(value):
|
||||
"""7z UINT64:首字节给出后续小端字节数,最多九字节。"""
|
||||
if not 0 <= value < 1 << 64:
|
||||
raise ValueError("7z UINT64 out of range")
|
||||
for size in range(8):
|
||||
if value < 1 << (7 * (size + 1)):
|
||||
first = ((0xFF << (8 - size)) & 0xFF) | (value >> (8 * size))
|
||||
return bytes([first]) + (value & ((1 << (8 * size)) - 1)).to_bytes(size, "little")
|
||||
return b"\xff" + value.to_bytes(8, "little")
|
||||
|
||||
|
||||
def store_hdr(payload_len, first=False):
|
||||
if not 1 <= payload_len <= CHUNK:
|
||||
raise ValueError("LZMA2 stored chunk must contain 1..65536 bytes")
|
||||
return bytes([1 if first else 2]) + (payload_len - 1).to_bytes(2, "big")
|
||||
|
||||
|
||||
def matches_for(dist, total):
|
||||
"""固定距离的首个 match 加 rep0;分段时不留下单字节。"""
|
||||
if total < 2:
|
||||
raise ValueError("match sequence must output at least two bytes")
|
||||
tokens = []
|
||||
while total:
|
||||
length = min(MAX_MATCH, total)
|
||||
if total - length == 1:
|
||||
length -= 1
|
||||
tokens.append(("r0", length) if tokens else ("m", dist, length))
|
||||
total -= length
|
||||
return tokens
|
||||
|
||||
|
||||
def dict_prop_for(maxdist):
|
||||
for prop in range(41):
|
||||
size = 0xFFFFFFFF if prop == 40 else (2 | (prop & 1)) << (prop // 2 + 11)
|
||||
if size >= maxdist:
|
||||
return prop, size
|
||||
raise ValueError("LZMA2 dictionary distance exceeds 2^32-1")
|
||||
|
||||
|
||||
def lzma_chunk(tokens, out_pos):
|
||||
"""重置状态和概率,保留字典;21-bit 解压长度,不写结束标记。"""
|
||||
enc = LzmaEncoder(out_pos)
|
||||
for token in tokens:
|
||||
if token[0] == "m":
|
||||
enc.match(token[1], token[2])
|
||||
elif token[0] == "r0":
|
||||
enc.rep_match(token[1])
|
||||
else:
|
||||
raise ValueError("unsupported LZMA token: %r" % (token[0],))
|
||||
data = enc.finish()
|
||||
total = enc.pos - out_pos
|
||||
if not 1 <= total <= MAX_CHUNK_OUTPUT or not 1 <= len(data) <= CHUNK:
|
||||
raise ValueError("LZMA2 compressed chunk size out of range")
|
||||
unpacked, packed = total - 1, len(data) - 1
|
||||
header = (bytes([0xC0 | (unpacked >> 16)])
|
||||
+ (unpacked & 0xFFFF).to_bytes(2, "big")
|
||||
+ packed.to_bytes(2, "big") + bytes([PROPS_BYTE]))
|
||||
return header + data, total
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Entry:
|
||||
name: str
|
||||
is_dir: bool
|
||||
data: bytes = b""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Layout:
|
||||
prefix_size: int
|
||||
chunks: tuple
|
||||
samples: bytes
|
||||
padding: int
|
||||
matches: bytes
|
||||
header: bytes
|
||||
crc_offset: int
|
||||
dict_size: int
|
||||
|
||||
@property
|
||||
def packed_size(self):
|
||||
return sum(self.chunks) + 3 * len(self.chunks) + len(self.matches) + 1
|
||||
|
||||
|
||||
class LayoutError(RuntimeError):
|
||||
"""当前填充量没有满足布局或 CRC 定点,换一个填充量重试。"""
|
||||
|
||||
|
||||
def split_sizes(size, limit):
|
||||
"""每片至少两字节,避免 match 无法表达的一字节尾片。"""
|
||||
if size < 2:
|
||||
raise ValueError("分片总长度必须至少为 2")
|
||||
sizes = [limit] * (size // limit)
|
||||
tail = size % limit
|
||||
if tail == 1:
|
||||
sizes[-1] -= 1
|
||||
tail = 2
|
||||
if tail:
|
||||
sizes.append(tail)
|
||||
return tuple(sizes)
|
||||
|
||||
|
||||
def bitfield(flags):
|
||||
data = bytearray((len(flags) + 7) // 8)
|
||||
for i, flag in enumerate(flags):
|
||||
if flag:
|
||||
data[i // 8] |= 1 << (7 - i % 8)
|
||||
return bytes(data)
|
||||
|
||||
|
||||
def archive_path(value, allow_empty=False):
|
||||
value = value.replace("\\", "/")
|
||||
if not value and allow_empty:
|
||||
return ""
|
||||
parts = value.split("/")
|
||||
if any(part in ("", ".", "..") for part in parts) or any(c in value for c in ("\0", ":")):
|
||||
raise ValueError("归档内路径必须是相对路径,不能包含空段、. 或 ..:%r" % value)
|
||||
return "/".join(parts)
|
||||
|
||||
|
||||
def encode_segments(segments, out_pos):
|
||||
"""把 (历史起点, 长度) 转成 match 流;跨 chunk 时保留字典。"""
|
||||
result, tokens = bytearray(), []
|
||||
used, max_distance = 0, 0
|
||||
for source, length in segments:
|
||||
for size in split_sizes(length, MAX_CHUNK_OUTPUT):
|
||||
if used + size > MAX_CHUNK_OUTPUT:
|
||||
block, _ = lzma_chunk(tokens, out_pos)
|
||||
result += block
|
||||
out_pos += used
|
||||
tokens, used = [], 0
|
||||
distance = out_pos + used - source
|
||||
if distance <= 0:
|
||||
raise LayoutError("回引源尚未输出")
|
||||
max_distance = max(max_distance, distance)
|
||||
tokens.extend(matches_for(distance, size))
|
||||
used += size
|
||||
source += size
|
||||
if tokens:
|
||||
block, _ = lzma_chunk(tokens, out_pos)
|
||||
result += block
|
||||
return bytes(result), max_distance
|
||||
|
||||
|
||||
def solve_fixed_crc(data, groups, targets):
|
||||
"""CRC 是 GF(2) 仿射函数;同步所有镜像,解三个 CRC 的 96-bit 定点。"""
|
||||
def vector(values):
|
||||
return sum(value << (32 * i) for i, value in enumerate(values))
|
||||
|
||||
base = vector(targets())
|
||||
columns = []
|
||||
for copies in groups:
|
||||
for bit in range(32):
|
||||
positions = [copy[bit // 8] for copy in copies]
|
||||
for pos in positions:
|
||||
data[pos] ^= 1 << (bit % 8)
|
||||
columns.append(vector(targets()) ^ base)
|
||||
for pos in positions:
|
||||
data[pos] ^= 1 << (bit % 8)
|
||||
|
||||
rows = []
|
||||
for bit in range(96):
|
||||
coeff = 1 << bit
|
||||
for j, column in enumerate(columns):
|
||||
coeff ^= ((column >> bit) & 1) << j
|
||||
rows.append([coeff, (base >> bit) & 1])
|
||||
pivots = []
|
||||
for column in range(96):
|
||||
rank = len(pivots)
|
||||
pivot = next((i for i in range(rank, 96) if rows[i][0] >> column & 1), None)
|
||||
if pivot is None:
|
||||
continue
|
||||
rows[rank], rows[pivot] = rows[pivot], rows[rank]
|
||||
for i in range(96):
|
||||
if i != rank and rows[i][0] >> column & 1:
|
||||
rows[i][0] ^= rows[rank][0]
|
||||
rows[i][1] ^= rows[rank][1]
|
||||
pivots.append(column)
|
||||
if any(not coeff and rhs for coeff, rhs in rows):
|
||||
raise LayoutError("CRC 定点方程无解")
|
||||
solution = sum(rows[i][1] << column for i, column in enumerate(pivots))
|
||||
values = [(solution >> (32 * i)) & 0xFFFFFFFF for i in range(3)]
|
||||
for copies, value in zip(groups, values):
|
||||
packed = struct.pack("<I", value)
|
||||
for copy in copies:
|
||||
for i, pos in enumerate(copy):
|
||||
data[pos] = packed[i]
|
||||
if tuple(values) != targets():
|
||||
raise RuntimeError("CRC 定点校验失败")
|
||||
|
||||
|
||||
class BlogQuine:
|
||||
"""单层 LZMA2:解码(P) = content || Q,无独立的辅助文件条目。
|
||||
|
||||
Q = V || P || H,V = S32 || H副本 || chunk头样本 || pad || M副本 || 00。
|
||||
P 先 store(content || V),再由 M 回引已输出的字节,复现 P || H。
|
||||
内容按原样存储;M 的编码不依赖历史字节,可先求布局,再填头部与 CRC。
|
||||
"""
|
||||
|
||||
def __init__(self, root, quine_name="MayxBlog.7z", src_dir="", exclude=None):
|
||||
root = Path(root).resolve()
|
||||
if not root.is_dir():
|
||||
raise ValueError("源目录不存在:%s" % root)
|
||||
self.quine_name = archive_path(quine_name)
|
||||
prefix = archive_path(src_dir.strip("/\\"), allow_empty=True)
|
||||
excluded = Path(exclude).resolve() if exclude is not None else None
|
||||
self.entries, self.files = [], []
|
||||
names = {}
|
||||
|
||||
def add(name, is_dir, data=b""):
|
||||
key = name.casefold()
|
||||
if key in names:
|
||||
if is_dir and names[key].is_dir and names[key].name == name:
|
||||
return
|
||||
raise ValueError("归档内路径冲突:%s" % name)
|
||||
entry = Entry(name, is_dir, data)
|
||||
names[key] = entry
|
||||
self.entries.append(entry)
|
||||
if not is_dir:
|
||||
self.files.append(entry)
|
||||
|
||||
def add_parents(name):
|
||||
parts = name.split("/")
|
||||
for i in range(1, len(parts)):
|
||||
add("/".join(parts[:i]), True)
|
||||
|
||||
if prefix:
|
||||
add_parents(prefix)
|
||||
add(prefix, True)
|
||||
|
||||
def walk_error(error):
|
||||
raise error
|
||||
|
||||
for directory, dirs, files in os.walk(root, onerror=walk_error):
|
||||
dirs.sort()
|
||||
children = [(name, True) for name in dirs]
|
||||
children += [(name, False) for name in sorted(files)]
|
||||
for name, is_dir in children:
|
||||
path = Path(directory) / name
|
||||
if not is_dir and path.resolve() == excluded:
|
||||
continue
|
||||
rel = path.relative_to(root).as_posix()
|
||||
name = archive_path(prefix + "/" + rel if prefix else rel)
|
||||
add_parents(name)
|
||||
add(name, is_dir, b"" if is_dir else path.read_bytes())
|
||||
|
||||
add_parents(self.quine_name)
|
||||
if self.quine_name.casefold() in names:
|
||||
raise ValueError("quine 文件名与源条目冲突:%s" % self.quine_name)
|
||||
self.entries.append(Entry(self.quine_name, False))
|
||||
self.content = b"".join(entry.data for entry in self.files)
|
||||
self.stream_files = [entry for entry in self.files if entry.data]
|
||||
self.stream_sizes = b"".join(varint(len(entry.data)) for entry in self.stream_files)
|
||||
self.stream_crcs = b"".join(struct.pack("<I", crc32(entry.data)) for entry in self.stream_files)
|
||||
self.mtime = time.time_ns() // 100 + FILETIME_EPOCH_OFFSET
|
||||
|
||||
def build_header(self, prefix_size, packed_size, archive_size, dict_prop):
|
||||
h = bytearray(b"\x01\x04\x06" + varint(prefix_size - 32)
|
||||
+ b"\x01\x09" + varint(packed_size) + b"\x00\x07\x0b\x01\x00")
|
||||
h += b"\x01\x21\x21\x01" + bytes([dict_prop]) # 一个 LZMA2 coder
|
||||
h += b"\x0c" + varint(len(self.content) + archive_size) + b"\x00"
|
||||
h += b"\x08\x0d" + varint(len(self.stream_files) + 1) + b"\x09" + self.stream_sizes
|
||||
h += b"\x0a\x01" + self.stream_crcs
|
||||
crc_offset = len(h)
|
||||
h += b"\x00" * 4 + b"\x00\x00\x05" + varint(len(self.entries))
|
||||
|
||||
def prop(kind, data):
|
||||
return bytes([kind]) + varint(len(data)) + data
|
||||
|
||||
empty = [entry.is_dir or (not entry.data and i != len(self.entries) - 1)
|
||||
for i, entry in enumerate(self.entries)]
|
||||
h += prop(0x0E, bitfield(empty))
|
||||
if any(flag and not entry.is_dir for flag, entry in zip(empty, self.entries)):
|
||||
empty_files = [not entry.is_dir for flag, entry in zip(empty, self.entries) if flag]
|
||||
h += prop(0x0F, bitfield(empty_files))
|
||||
h += prop(0x14, b"\x01\x00" + struct.pack("<Q", self.mtime) * len(self.entries))
|
||||
names = b"".join(entry.name.encode("utf-16-le") + b"\x00\x00" for entry in self.entries)
|
||||
attrs = b"".join(struct.pack("<I", 0x10 if entry.is_dir else 0x20) for entry in self.entries)
|
||||
h += prop(0x11, b"\x00" + names)
|
||||
h += prop(0x15, b"\x01\x00" + attrs)
|
||||
return bytes(h + b"\x00\x00"), crc_offset
|
||||
|
||||
def layout(self, padding):
|
||||
header_size, match_size = 256, 256
|
||||
seen = set()
|
||||
dc = len(self.content)
|
||||
for _ in range(40):
|
||||
prefix_size = 32 + header_size + 12 + padding + match_size + 1
|
||||
output_size = dc + prefix_size
|
||||
chunks = split_sizes(output_size, CHUNK)
|
||||
samples = (store_hdr(chunks[0], True) + store_hdr(CHUNK)
|
||||
+ store_hdr(CHUNK - 1) + store_hdr(chunks[-1]))
|
||||
segments, offset = [], 0
|
||||
for i, size in enumerate(chunks):
|
||||
sample = 0 if i == 0 else 1 if size == CHUNK else 2 if size == CHUNK - 1 else 3
|
||||
segments += [(dc + 32 + header_size + 3 * sample, 3), (offset, size)]
|
||||
offset += size
|
||||
segments += [(dc + 32 + header_size + 12 + padding, match_size + 1),
|
||||
(dc + 32, header_size)]
|
||||
matches, max_distance = encode_segments(segments, output_size)
|
||||
dict_prop, dict_size = dict_prop_for(max_distance)
|
||||
packed_size = output_size + 3 * len(chunks) + len(matches) + 1
|
||||
header, crc_offset = self.build_header(
|
||||
prefix_size, packed_size, prefix_size + packed_size + header_size, dict_prop)
|
||||
key = len(header), len(matches)
|
||||
if key == (header_size, match_size):
|
||||
return Layout(prefix_size, chunks, samples, padding, matches,
|
||||
header, crc_offset, dict_size)
|
||||
if key in seen:
|
||||
break
|
||||
seen.add(key)
|
||||
header_size, match_size = key
|
||||
raise LayoutError("布局没有收敛")
|
||||
|
||||
def assemble(self, layout):
|
||||
start = struct.pack("<QQI", layout.prefix_size - 32 + layout.packed_size,
|
||||
len(layout.header), 0)
|
||||
signature = SIG + VER + b"\x00" * 4 + start
|
||||
prefix = (signature + layout.header + layout.samples + b"\x00" * layout.padding
|
||||
+ layout.matches + b"\x00")
|
||||
initial = self.content + prefix
|
||||
data, offset = bytearray(prefix), 0
|
||||
for i, size in enumerate(layout.chunks):
|
||||
data += store_hdr(size, first=i == 0) + initial[offset:offset + size]
|
||||
offset += size
|
||||
data += layout.matches + b"\x00" + layout.header
|
||||
if len(prefix) != layout.prefix_size or offset != len(initial):
|
||||
raise RuntimeError("装配布局不一致")
|
||||
|
||||
def stored_positions(output_offset, size):
|
||||
result, consumed, physical = [], 0, layout.prefix_size
|
||||
for length in layout.chunks:
|
||||
begin = max(output_offset, consumed)
|
||||
end = min(output_offset + size, consumed + length)
|
||||
result.extend(range(physical + 3 + begin - consumed, physical + 3 + end - consumed))
|
||||
consumed += length
|
||||
physical += 3 + length
|
||||
if len(result) != size:
|
||||
raise RuntimeError("无法定位 store 载荷")
|
||||
return result
|
||||
|
||||
slot = layout.crc_offset
|
||||
hoff = layout.prefix_size + layout.packed_size
|
||||
groups = [[list(range(32 + slot, 36 + slot)),
|
||||
stored_positions(len(self.content) + 32 + slot, 4),
|
||||
list(range(hoff + slot, hoff + slot + 4))]]
|
||||
for slot in (28, 8):
|
||||
groups.append([list(range(slot, slot + 4)), stored_positions(len(self.content) + slot, 4)])
|
||||
view = memoryview(data)
|
||||
solve_fixed_crc(data, groups, lambda: (crc32(view), crc32(view[hoff:]), crc32(view[12:32])))
|
||||
return bytes(data)
|
||||
|
||||
def verify(self, data, layout):
|
||||
dec = lzma.LZMADecompressor(format=lzma.FORMAT_RAW,
|
||||
filters=[{"id": lzma.FILTER_LZMA2, "dict_size": layout.dict_size}])
|
||||
expected = len(self.content) + len(data)
|
||||
packed = data[layout.prefix_size:layout.prefix_size + layout.packed_size]
|
||||
decoded = dec.decompress(packed, max_length=expected + 1)
|
||||
view = memoryview(decoded)
|
||||
if (not dec.eof or dec.unused_data or len(decoded) != expected
|
||||
or view[:len(self.content)] != self.content or view[len(self.content):] != data):
|
||||
raise RuntimeError("自解码校验失败")
|
||||
|
||||
def build(self):
|
||||
last_error = None
|
||||
for padding in range(256):
|
||||
try:
|
||||
layout = self.layout(padding)
|
||||
data = self.assemble(layout)
|
||||
self.verify(data, layout)
|
||||
return data
|
||||
except LayoutError as error:
|
||||
last_error = error
|
||||
raise RuntimeError("无法找到布局/CRC 定点:%s" % last_error)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="LZMA2 7z quine 生成器")
|
||||
parser.add_argument("srcdir", help="要打包的源目录")
|
||||
parser.add_argument("output", help="输出的 7z 文件")
|
||||
parser.add_argument("--quine-name", help="归档内自身文件名,默认使用输出文件名")
|
||||
parser.add_argument("--src-dir", default="", help="源文件在归档内的前缀目录")
|
||||
parser.add_argument("--quine-dir", default="", help="归档内自身文件所在目录")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
output = Path(args.output)
|
||||
name = args.quine_name or output.name
|
||||
if args.quine_dir:
|
||||
name = archive_path(args.quine_dir.strip("/\\")) + "/" + name
|
||||
quine = BlogQuine(args.srcdir, name, args.src_dir, exclude=output)
|
||||
data = quine.build() # 验证成功后才写出,失败时保留已有输出。
|
||||
output.write_bytes(data)
|
||||
except (OSError, ValueError, RuntimeError, lzma.LZMAError) as error:
|
||||
parser.exit(1, "blogquine: %s\n" % error)
|
||||
print("[blogquine] %d 个文件,%d 个目录,内容 %d 字节" %
|
||||
(len(quine.files), sum(entry.is_dir for entry in quine.entries), len(quine.content)))
|
||||
print("[blogquine] 已写入 %s:%d 字节;自解码通过" % (output, len(data)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/bin/bash
|
||||
cd /home/mayx/
|
||||
rm -rf public_html
|
||||
rm -rf public_html blog/*
|
||||
git --work-tree=/home/mayx/blog --git-dir=/home/mayx/blog.git checkout -f
|
||||
cd blog
|
||||
mkdir Mabbs
|
||||
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
||||
bundle exec jekyll build -d ../public_html
|
||||
rsync --delete -rv ../public_html/ mayx@pgs.sh:/blog
|
||||
tar czvf MayxBlog.tgz --exclude-vcs ../public_html/
|
||||
mv MayxBlog.tgz ../public_html/
|
||||
python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../public_html/ MayxBlog.7z
|
||||
7z a -mx9 -bd -bb0 MayxBlog-new.7z MayxBlog.7z
|
||||
mv MayxBlog-new.7z ../public_html/MayxBlog.7z
|
||||
cd ../public_html/
|
||||
unset GIT_DIR
|
||||
git init
|
||||
@@ -17,8 +17,8 @@ git add .
|
||||
git commit -m "update"
|
||||
git branch pages
|
||||
git remote add codeberg ssh://git@codeberg.org/mayx/pages.git
|
||||
git remote add bitbucket ssh://git@bitbucket.org/unmayx/unmayx.bitbucket.io.git
|
||||
git remote add bitbucket ssh://git@ssh.bitbucket.org/unmayx/unmayx.bitbucket.io.git
|
||||
git push -f codeberg main
|
||||
git push -f codeberg pages
|
||||
git push -f codeberg pages
|
||||
git push -f bitbucket main
|
||||
/home/mayx/blog-env/node_modules/surge/bin/surge /home/mayx/public_html/ mayx.surge.sh
|
||||
@@ -1,23 +1,27 @@
|
||||
#!/bin/bash
|
||||
cd /home/Mayx/
|
||||
rm -rf domains/mayx.serv00.net/public_html/
|
||||
rm -rf domains/mayx.serv00.net/public_html/ blog/*
|
||||
git --work-tree=/home/Mayx/blog --git-dir=/home/Mayx/repo/git/pub/mayx checkout -f
|
||||
cd blog
|
||||
mkdir Mabbs
|
||||
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
||||
bundle exec jekyll build -d ../domains/mayx.serv00.net/public_html/
|
||||
tar czvf MayxBlog.tgz --exclude-vcs -C ../domains/mayx.serv00.net public_html/
|
||||
mv MayxBlog.tgz ../domains/mayx.serv00.net/public_html/
|
||||
python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../domains/mayx.serv00.net/public_html/ MayxBlog.7z
|
||||
7zz a -mx9 -bd -bb0 MayxBlog-new.7z MayxBlog.7z
|
||||
mv MayxBlog-new.7z ../domains/mayx.serv00.net/public_html/MayxBlog.7z
|
||||
rsync -avz --delete ../domains/mayx.serv00.net/public_html/ mayx@web.sourceforge.net:/home/project-web/mayx/htdocs/
|
||||
cd ../domains/mayx.serv00.net/public_html/
|
||||
unset GIT_DIR
|
||||
git init
|
||||
git lfs install
|
||||
git lfs track "*.png"
|
||||
git lfs track "*.webp"
|
||||
git lfs track "*.moc"
|
||||
git lfs track "*.tgz"
|
||||
git lfs track "*.jpg"
|
||||
git lfs track "*.zip"
|
||||
git lfs track "*.7z"
|
||||
git lfs track "*.wasm"
|
||||
git branch -m main
|
||||
echo "---
|
||||
title: Mayx
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
+5
-4
File diff suppressed because one or more lines are too long
@@ -17,6 +17,25 @@ function initVisitors() {
|
||||
}
|
||||
}
|
||||
|
||||
function highlightKeyword() {
|
||||
var match = location.search.match(/[?&]kw=([^&]+)/);
|
||||
var kw = match ? $.trim(decodeURIComponent(match[1].replace(/\+/g, ' '))) : '';
|
||||
if (!kw) return;
|
||||
|
||||
var reg = new RegExp('(' + kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
|
||||
var escapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
||||
|
||||
$('section, section *').not('script, style, textarea').contents().filter(function () {
|
||||
return this.nodeType === 3;
|
||||
}).each(function () {
|
||||
var escapedText = this.nodeValue.replace(/[&<>"']/g, function (m) { return escapeMap[m]; });
|
||||
var highlighted = escapedText.replace(reg, '<mark>$1</mark>');
|
||||
if (escapedText !== highlighted) {
|
||||
$(this).replaceWith(highlighted);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(function () {
|
||||
(function () {
|
||||
var $backToTopTxt = "返回顶部", $backToTopEle = $('<div class="backToTop"></div>').appendTo($("body"))
|
||||
@@ -42,6 +61,7 @@ $(function () {
|
||||
});
|
||||
console.warn("Mayx may already be Dead");
|
||||
}
|
||||
highlightKeyword();
|
||||
});
|
||||
|
||||
function getSearchJSON(callback) {
|
||||
|
||||
@@ -1,52 +1,3 @@
|
||||
/**
|
||||
* 根据 URL ?kw= 参数高亮页面内匹配的关键词。
|
||||
* 提取为全局函数,供 pjax.js 在页面切换后复用,避免重复实现。
|
||||
*/
|
||||
function highlightKeyword() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const keyword = urlParams.get('kw')?.trim();
|
||||
|
||||
if (!keyword) return;
|
||||
|
||||
// 转义正则表达式特殊字符,避免安全问题
|
||||
const escapedKeyword = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
// 创建不区分大小写的正则表达式(全局匹配)
|
||||
const regex = new RegExp(`(${escapedKeyword})`, 'gi');
|
||||
|
||||
// 递归遍历并高亮文本节点
|
||||
const escapeHTML = str => str.replace(/[&<>"']/g,
|
||||
tag => ({
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[tag] || tag));
|
||||
function highlightTextNodes(element) {
|
||||
$(element).contents().each(function () {
|
||||
if (this.nodeType === Node.TEXT_NODE) {
|
||||
const $this = $(this);
|
||||
const text = escapeHTML($this.text());
|
||||
|
||||
// 使用正则替换并保留原始大小写
|
||||
if (regex.test(text)) {
|
||||
const replaced = text.replace(regex, '<mark>$1</mark>');
|
||||
$this.replaceWith(replaced);
|
||||
}
|
||||
} else if (
|
||||
this.nodeType === Node.ELEMENT_NODE &&
|
||||
!$(this).is('script, style, noscript, textarea')
|
||||
) {
|
||||
highlightTextNodes(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$('section').each(function () {
|
||||
highlightTextNodes(this);
|
||||
});
|
||||
};
|
||||
|
||||
function initCopyButtons() {
|
||||
$('.copy').remove();
|
||||
$('div.highlight').each(function () {
|
||||
@@ -63,6 +14,5 @@ function initCopyButtons() {
|
||||
}
|
||||
|
||||
$(function () {
|
||||
highlightKeyword();
|
||||
initCopyButtons();
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Mayx的博客 · /offline.html 控制页控制器(classic script,PJAX 兼容)
|
||||
*
|
||||
* 本体是 IIFE,不含模块语法:PJAX 换页时 jQuery 会重新执行本文件,
|
||||
* 每次执行都重新查询元素、重新绑定事件,天然幂等。真正的解包逻辑在
|
||||
* ES module /assets/js/offline-unpack.js 中,通过动态 import() 加载
|
||||
* (浏览器模块缓存保证 PJAX 反复进入本页也不会重复加载)。
|
||||
* 注意:本文件经 Liquid 原样输出,不能使用模板语法字符。
|
||||
*/
|
||||
|
||||
(function () {
|
||||
var MODULE_URL = '/assets/js/offline-unpack.js';
|
||||
// 压缩包大致量级(当前约 27MB),仅用于进度条比例,不影响正确性
|
||||
var EXPECTED_BYTES = 27 * 1024 * 1024;
|
||||
var busy = false;
|
||||
|
||||
function el(id) { return document.getElementById(id); }
|
||||
function fmtMB(bytes) { return (bytes / 1048576).toFixed(1) + ' MB'; }
|
||||
|
||||
function setMessage(text) {
|
||||
el('offline-message').textContent = text || '';
|
||||
}
|
||||
|
||||
function showProgress(show) {
|
||||
el('offline-progress-box').hidden = !show;
|
||||
}
|
||||
|
||||
function render(meta) {
|
||||
var online = !meta;
|
||||
el('offline-state').textContent = online
|
||||
? '在线模式(离线未启用)'
|
||||
: '离线模式(优先从本地缓存加载)';
|
||||
el('offline-version').textContent = online ? '-' : fmtMB(meta.payloadSize) + '(压缩包大小即版本号)';
|
||||
el('offline-files').textContent = online ? '-' : meta.files + ' 个文件 / ' + fmtMB(meta.bytes);
|
||||
el('offline-time').textContent = online ? '-' : new Date(meta.unpackedAt).toLocaleString();
|
||||
el('offline-persist').textContent = online ? '-' : (meta.persisted ? '已授权(缓存不会被自动回收)' : '未授权(浏览器空间紧张时可能回收缓存)');
|
||||
el('offline-enable').disabled = busy || !online;
|
||||
el('offline-disable').disabled = busy || online;
|
||||
}
|
||||
|
||||
function onProgress(info) {
|
||||
if (info.done) return;
|
||||
el('offline-progress').value = Math.min(100, (info.bytes / EXPECTED_BYTES) * 100);
|
||||
el('offline-progress-text').textContent =
|
||||
'已解包 ' + info.files + ' 个文件(' + fmtMB(info.bytes) + ')…';
|
||||
}
|
||||
|
||||
function withModule(fn) {
|
||||
return import(MODULE_URL).then(fn);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!('serviceWorker' in navigator) || !('caches' in window)) {
|
||||
el('offline-state').textContent = '当前浏览器不支持离线模式(需要 Service Worker)';
|
||||
el('offline-enable').disabled = true;
|
||||
el('offline-disable').disabled = true;
|
||||
return;
|
||||
}
|
||||
withModule(function (mod) { return mod.ensureFresh(); }).then(function (fresh) {
|
||||
render(fresh.ready ? fresh.meta : null);
|
||||
if (fresh.stale) {
|
||||
setMessage('检测到站点压缩包已更新,离线缓存已自动失效,当前处于在线模式。可重新启用离线模式。');
|
||||
}
|
||||
}).catch(function (e) {
|
||||
setMessage('状态检测失败:' + (e && e.message ? e.message : e));
|
||||
});
|
||||
}
|
||||
|
||||
function onEnable() {
|
||||
busy = true;
|
||||
render(null);
|
||||
showProgress(true);
|
||||
setMessage('正在下载并解包站点压缩包,请保持网络连接…');
|
||||
withModule(function (mod) { return mod.activateOffline({ onProgress: onProgress }); })
|
||||
.then(function (meta) {
|
||||
el('offline-progress').value = 100;
|
||||
el('offline-progress-text').textContent = '解包完成';
|
||||
setMessage('离线模式已启用。之后访问本博客将优先使用本地缓存,断网也能阅读。');
|
||||
busy = false;
|
||||
showProgress(false);
|
||||
render(meta); // 立即展示离线状态,不必等后台刷新
|
||||
})
|
||||
.catch(function (e) {
|
||||
setMessage('启用失败:' + (e && e.message ? e.message : e));
|
||||
busy = false;
|
||||
showProgress(false);
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
function onDisable() {
|
||||
busy = true;
|
||||
setMessage('');
|
||||
withModule(function (mod) { return mod.deactivateOffline(); })
|
||||
.then(function () {
|
||||
setMessage('已切回在线模式,之后页面将通过网络加载。');
|
||||
busy = false;
|
||||
render(null);
|
||||
if (navigator.onLine) {
|
||||
setTimeout(function () { location.reload(); }, 800);
|
||||
}
|
||||
})
|
||||
.catch(function (e) {
|
||||
setMessage('操作失败:' + (e && e.message ? e.message : e));
|
||||
busy = false;
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
function bind() {
|
||||
var enable = el('offline-enable');
|
||||
if (!enable) return; // 不在控制页(PJAX 容错)
|
||||
enable.addEventListener('click', onEnable);
|
||||
el('offline-disable').addEventListener('click', onDisable);
|
||||
refresh();
|
||||
}
|
||||
|
||||
bind();
|
||||
})();
|
||||
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Mayx的博客 · 离线模式核心逻辑(/offline.html 控制页使用,ES module)
|
||||
*
|
||||
* - activateOffline():注册 SW,下载站点压缩包 /MayxBlog.7z,经 libarchive(WASM)
|
||||
* 流式解包,把每个文件按站点路径写入 Cache Storage,最后写入就绪标记
|
||||
* (清单 + payloadSize 版本号),自此进入离线模式。
|
||||
* - ensureFresh():HEAD 探测线上压缩包大小,与标记中的 payloadSize 不一致
|
||||
* 说明站点已更新 → 清空缓存,回退在线模式。
|
||||
* - deactivateOffline():清空缓存,手动切回在线模式。
|
||||
* - readStatus():读取当前离线状态,供控制页展示。
|
||||
*
|
||||
*/
|
||||
|
||||
const ARCHIVE_URL = '/MayxBlog.7z';
|
||||
const ARCHIVE_ROOT = 'blog/';
|
||||
const CACHE_NAME = 'mayxblog-offline-v1';
|
||||
const READY_KEY = '/__offline_manifest__';
|
||||
const WORKER_URL = '/vendor/libarchive/worker-bundle.js';
|
||||
|
||||
const MIME = {
|
||||
html: 'text/html; charset=utf-8',
|
||||
htm: 'text/html; charset=utf-8',
|
||||
css: 'text/css; charset=utf-8',
|
||||
js: 'application/javascript; charset=utf-8',
|
||||
mjs: 'application/javascript; charset=utf-8',
|
||||
json: 'application/json; charset=utf-8',
|
||||
xml: 'application/xml; charset=utf-8',
|
||||
xsl: 'text/xsl; charset=utf-8',
|
||||
opml: 'text/x-opml; charset=utf-8',
|
||||
txt: 'text/plain; charset=utf-8',
|
||||
md: 'text/markdown; charset=utf-8',
|
||||
csv: 'text/csv; charset=utf-8',
|
||||
svg: 'image/svg+xml',
|
||||
png: 'image/png',
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
avif: 'image/avif',
|
||||
ico: 'image/x-icon',
|
||||
bmp: 'image/bmp',
|
||||
woff: 'font/woff',
|
||||
woff2: 'font/woff2',
|
||||
ttf: 'font/ttf',
|
||||
otf: 'font/otf',
|
||||
eot: 'application/vnd.ms-fontobject',
|
||||
mp3: 'audio/mpeg',
|
||||
mp4: 'video/mp4',
|
||||
webm: 'video/webm',
|
||||
'7z': 'application/x-7z-compressed',
|
||||
zip: 'application/zip',
|
||||
wasm: 'application/wasm',
|
||||
mtn: 'application/octet-stream',
|
||||
moc: 'application/octet-stream',
|
||||
pdf: 'application/pdf',
|
||||
};
|
||||
|
||||
function mimeFor(pathname) {
|
||||
const dot = pathname.lastIndexOf('.');
|
||||
const ext = dot === -1 ? '' : pathname.slice(dot + 1).toLowerCase();
|
||||
return MIME[ext] || 'application/octet-stream';
|
||||
}
|
||||
|
||||
function sitePathFor(entryPath) {
|
||||
const normalized = entryPath.replace(/\\/g, '/');
|
||||
if (!normalized.startsWith(ARCHIVE_ROOT)) return null;
|
||||
const relative = normalized.slice(ARCHIVE_ROOT.length);
|
||||
if (!relative) return null;
|
||||
|
||||
const segments = [];
|
||||
for (const segment of relative.split('/')) {
|
||||
if (!segment || segment === '.') continue;
|
||||
if (segment === '..') return null;
|
||||
segments.push(segment);
|
||||
}
|
||||
if (!segments.length) return null;
|
||||
return '/' + segments.join('/');
|
||||
}
|
||||
|
||||
/*
|
||||
* 在独立 worker 中打开一层压缩包并流式吐出条目;
|
||||
* 返回其中根级嵌套压缩包(下一层包装)的原始字节。
|
||||
*/
|
||||
function extractLayer(file, onEntry) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker(WORKER_URL);
|
||||
const nested = [];
|
||||
let settled = false;
|
||||
|
||||
const finish = (error) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
worker.terminate();
|
||||
if (error) reject(error);
|
||||
else resolve(nested);
|
||||
};
|
||||
|
||||
worker.addEventListener('error', (event) => {
|
||||
finish(new Error(event.message || 'libarchive worker failed'));
|
||||
});
|
||||
|
||||
worker.addEventListener('message', ({ data }) => {
|
||||
if (data.type === 'ERROR') {
|
||||
finish(new Error(data.error && data.error.message ? data.error.message : 'libarchive failed'));
|
||||
return;
|
||||
}
|
||||
if (data.type === 'READY') {
|
||||
worker.postMessage({ type: 'OPEN', file });
|
||||
return;
|
||||
}
|
||||
if (data.type === 'OPENED') {
|
||||
worker.postMessage({ type: 'EXTRACT_FILES' });
|
||||
return;
|
||||
}
|
||||
if (data.type === 'END') {
|
||||
finish(null);
|
||||
return;
|
||||
}
|
||||
if (data.type !== 'ENTRY') return;
|
||||
|
||||
const entry = data.entry;
|
||||
if (entry.type !== 'FILE' || !entry.fileData) return;
|
||||
|
||||
// 根级压缩包是下一层包装,不是站点内容
|
||||
if (entry.path.replace(/\\/g, '/') === 'MayxBlog.7z') {
|
||||
nested.push(entry.fileData);
|
||||
return;
|
||||
}
|
||||
onEntry(entry);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* 几十 MB 的缓存是浏览器常见的回收对象,而重新解包代价高,尽量申请持久存储。 */
|
||||
async function requestPersistence() {
|
||||
if (!navigator.storage || !navigator.storage.persist) return false;
|
||||
try {
|
||||
if (await navigator.storage.persisted()) return true;
|
||||
return await navigator.storage.persist();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* 读取就绪标记(= 离线模式状态);损坏或缺清单的标记视为不存在。 */
|
||||
export async function readStatus() {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const hit = await cache.match(READY_KEY);
|
||||
if (!hit) return null;
|
||||
const meta = await hit.json().catch(() => null);
|
||||
if (!meta || !Array.isArray(meta.keys) || !meta.keys.length) return null;
|
||||
return meta;
|
||||
}
|
||||
|
||||
/*
|
||||
* 线上压缩包的大小即版本号。HEAD 不被 SW 拦截,拿到的一定是线上真实值;
|
||||
* 网络失败返回 null,调用方选择信任现有缓存而不是误伤。
|
||||
*/
|
||||
async function payloadSize() {
|
||||
try {
|
||||
const response = await fetch(ARCHIVE_URL, { method: 'HEAD', cache: 'no-store' });
|
||||
if (!response.ok) return null;
|
||||
const size = Number(response.headers.get('content-length'));
|
||||
return Number.isFinite(size) && size > 0 ? size : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 控制页打开时调用:未启用 → {ready:false};已启用且版本一致 → {ready:true, meta};
|
||||
* 版本不一致 → 清空缓存并返回 {ready:false, stale:true}(已回退在线模式)。
|
||||
*/
|
||||
export async function ensureFresh() {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const size = await payloadSize();
|
||||
const meta = await readStatus();
|
||||
|
||||
if (!meta) return { ready: false, size, stale: false };
|
||||
if (size && meta.payloadSize !== size) {
|
||||
await caches.delete(CACHE_NAME);
|
||||
return { ready: false, size, stale: true };
|
||||
}
|
||||
return { ready: true, size, stale: false, meta };
|
||||
}
|
||||
|
||||
/*
|
||||
* 启用离线模式:注册 SW → 清空旧缓存(同时清掉上次中断的残留,保证随后
|
||||
* 的压缩包下载不会被旧缓存喂旧的 quine 副本)→ 下载并逐层解包 → 写标记。
|
||||
*/
|
||||
export async function activateOffline({ onProgress } = {}) {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
throw new Error('当前浏览器不支持 Service Worker,无法启用离线模式。');
|
||||
}
|
||||
await navigator.serviceWorker.register('/sw.js', { scope: '/' });
|
||||
await navigator.serviceWorker.ready;
|
||||
await caches.delete(CACHE_NAME);
|
||||
|
||||
const persisted = await requestPersistence();
|
||||
|
||||
// 写操作串行链,保证按到达顺序落盘且不产生无界并发
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
let writes = Promise.resolve();
|
||||
const keys = new Set();
|
||||
let files = 0;
|
||||
let bytes = 0;
|
||||
let skipped = 0;
|
||||
|
||||
const response = await fetch(ARCHIVE_URL, { cache: 'no-store' });
|
||||
if (!response.ok) throw new Error(`无法下载站点压缩包:HTTP ${response.status}`);
|
||||
const outer = await response.arrayBuffer();
|
||||
const downloadedBytes = outer.byteLength;
|
||||
|
||||
let layer = [outer];
|
||||
while (layer.length) {
|
||||
const bytesThisLayer = layer.shift();
|
||||
const file = new File([bytesThisLayer], 'MayxBlog.7z');
|
||||
|
||||
layer = await extractLayer(file, (entry) => {
|
||||
const key = sitePathFor(entry.path);
|
||||
if (!key) {
|
||||
skipped += 1;
|
||||
return;
|
||||
}
|
||||
const body = entry.fileData;
|
||||
files += 1;
|
||||
bytes += body.byteLength;
|
||||
|
||||
writes = writes.then(async () => {
|
||||
await cache.put(key, new Response(body, {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': mimeFor(key) },
|
||||
}));
|
||||
keys.add(key);
|
||||
});
|
||||
|
||||
if (onProgress) onProgress({ done: false, files, bytes, skipped });
|
||||
});
|
||||
}
|
||||
|
||||
await writes;
|
||||
|
||||
const meta = {
|
||||
files,
|
||||
bytes,
|
||||
skipped,
|
||||
persisted,
|
||||
payloadSize: downloadedBytes,
|
||||
failed: files - keys.size,
|
||||
keys: [...keys],
|
||||
unpackedAt: new Date().toISOString(),
|
||||
};
|
||||
await cache.put(
|
||||
READY_KEY,
|
||||
new Response(JSON.stringify(meta), {
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
||||
})
|
||||
);
|
||||
|
||||
if (onProgress) onProgress({ done: true, cached: false, ...meta });
|
||||
return meta;
|
||||
}
|
||||
|
||||
/* 切回在线模式:删掉缓存(含标记),SW 随即恢复纯转发。返回之前是否处于离线模式。 */
|
||||
export async function deactivateOffline() {
|
||||
const had = await readStatus();
|
||||
await caches.delete(CACHE_NAME);
|
||||
return Boolean(had);
|
||||
}
|
||||
+1
-1
@@ -116,7 +116,7 @@
|
||||
// 排除列表:外链、锚点、静态资源、Live2D 目录
|
||||
var exclude = ':not([target="_blank"]):not([href^="http"]):not([href^="//"])' +
|
||||
':not([href^="mailto"]):not([href^="#"])' +
|
||||
':not([href$=".xml"]):not([href$=".json"]):not([href$=".tgz"]):not([href$=".zip"])' +
|
||||
':not([href$=".xml"]):not([href$=".json"]):not([href$=".7z"]):not([href$=".zip"])' +
|
||||
':not([href^="/Live2dHistoire"])';
|
||||
$(document).pjax('a' + exclude, PJAX_OPTS.container, PJAX_OPTS);
|
||||
$(document).on('submit', 'form#search-input-all', function (e) {
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
24f0040157c07cc424b28b7b52ea734d
|
||||
@@ -2,5 +2,8 @@
|
||||
mkdir Mabbs
|
||||
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
||||
bundle exec jekyll build -d public
|
||||
tar czvf MayxBlog.tgz public/
|
||||
mv MayxBlog.tgz public/
|
||||
python3 _tools/blogquine.py --src-dir blog --quine-dir blog public MayxBlog.7z
|
||||
curl -L -O https://github.com/ip7z/7zip/releases/download/26.03/7z2603-linux-x64.tar.xz
|
||||
tar xvf 7z2603-linux-x64.tar.xz
|
||||
./7zzs a -mx9 -bd -bb0 MayxBlog-new.7z MayxBlog.7z
|
||||
mv MayxBlog-new.7z public/MayxBlog.7z
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
layout: default
|
||||
title: Mayx的游戏列表
|
||||
---
|
||||
|
||||
# Mayx的游戏列表
|
||||
- 凯露&危险生存( [CHS]({% unless site.github %}https://mabbs.github.io{% endunless %}/karyl-yabaival/?cn) \| [JA]({% unless site.github %}https://mabbs.github.io{% endunless %}/karyl-yabaival/) \| [KO]({% unless site.github %}https://mabbs.github.io{% endunless %}/karyl-yabaival/?kr) )
|
||||
- [立直麻将 Wordle]({% unless site.github %}https://mabbs.github.io{% endunless %}/riichi-mahjong-wordle/)
|
||||
- [Brainfuck可视化演示]({% unless site.github %}https://mabbs.github.io{% endunless %}/brainfuck-visualizer/)
|
||||
- [《激情麻将》关服前快照]({% unless site.github %}https://mabbs.github.io{% endunless %}/mahjong-dream-snapshot/) ([国际服]({% unless site.github %}https://mabbs.github.io{% endunless %}/mahjong-dream-snapshot/?intl))
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 748 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
+4
-4
@@ -12,7 +12,7 @@ image: https://screenshot.mayx.eu.org/
|
||||
<!-- 遍历分页后的文章 -->
|
||||
<table class="entry-content h-feed">
|
||||
{% for post in paginator.posts %}
|
||||
<tr><td class="h-entry" data-url="{{ post.url }}">
|
||||
<tr><td class="h-entry post-row" data-url="{{ post.url }}">
|
||||
<h2 class="p-name"><a class="post-link u-url" href="{{ post.url }}">{{ post.title }}{% if post.layout == "encrypt" %} [加密] {% endif %}</a></h2>
|
||||
<p>
|
||||
<time class="date dt-published" datetime="{{ post.date | date_to_xmlschema }}">{{ post.date | date: "%-d %B %Y" }}</time>
|
||||
@@ -66,8 +66,8 @@ image: https://screenshot.mayx.eu.org/
|
||||
<h2>其他页面</h2>
|
||||
<p>
|
||||
<a href="/service.html">Mayx的公开服务</a><br />
|
||||
凯露&危险生存( <a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/karyl-yabaival/?cn">CHS</a> | <a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/karyl-yabaival/">JA</a> | <a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/karyl-yabaival/?kr">KO</a> )<br />
|
||||
<a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/brainfuck-visualizer/">Brainfuck可视化演示</a><br />
|
||||
<a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/linux/">Mayx's Blog Terminal</a> (<a href="{% unless site.github %}https://mabbs.github.io{% endunless %}/jsnix/">Fake Version</a>)<br />
|
||||
<a href="/games.html">Mayx的游戏列表</a><br />
|
||||
<a href="/message.html">留言板</a><br />
|
||||
<a href="/links.html">Links</a><br />
|
||||
<a href="/proxylist.html">代理列表</a><br />
|
||||
@@ -79,7 +79,7 @@ image: https://screenshot.mayx.eu.org/
|
||||
<a href="https://xn--sr8hvo.ws/next">→</a><br /><a href="https://icp.gov.moe/?keyword=20218888" target="_blank">萌ICP备 20218888号</a></small>
|
||||
<input name="live2dBGM" value="https://music.163.com/song/media/outer/url?id=523658881.mp3" type="hidden" />
|
||||
<script>
|
||||
$(document).on('click', '.h-entry', function(e) {
|
||||
$(document).on('click', '.post-row', function(e) {
|
||||
if ($(e.target).closest('.p-category').length === 0) {
|
||||
go($(this).attr('data-url'));
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
32691808d04b4e838d0c0610ec848ca2
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
---
|
||||
layout: default
|
||||
title: 离线模式
|
||||
---
|
||||
|
||||
# 离线模式
|
||||
|
||||
本页用于管理本博客的离线阅读功能:
|
||||
|
||||
- **启用离线模式**:浏览器会下载整站压缩包(`MayxBlog.7z`)并解包到本地缓存,之后即使没有网络也能正常浏览博客;
|
||||
- **自动回退**:当站点压缩包更新(博客重新部署)时,离线缓存会自动失效并回退到在线模式,下次重新启用即可获取最新内容;
|
||||
- **手动切换**:离线模式下可以随时在本页切回在线模式,切回后页面将重新通过网络加载。
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
<tr><th>当前状态</th><td id="offline-state">检测中…</td></tr>
|
||||
<tr><th>缓存版本</th><td id="offline-version">-</td></tr>
|
||||
<tr><th>文件统计</th><td id="offline-files">-</td></tr>
|
||||
<tr><th>解包时间</th><td id="offline-time">-</td></tr>
|
||||
<tr><th>持久存储</th><td id="offline-persist">-</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p id="offline-progress-box" hidden="hidden">
|
||||
<progress id="offline-progress" max="100" value="0"></progress>
|
||||
|
||||
<small id="offline-progress-text">准备中…</small>
|
||||
</p>
|
||||
|
||||
<p><strong id="offline-message"></strong></p>
|
||||
|
||||
<p>
|
||||
<button type="button" id="offline-enable">启用离线模式</button>
|
||||
<button type="button" id="offline-disable">切换回在线模式</button>
|
||||
</p>
|
||||
|
||||
<script src="/assets/js/offline-page.js"></script>
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Mayx的博客 · 离线模式 Service Worker
|
||||
*
|
||||
* 两种模式(由 Cache Storage 中是否存在就绪标记决定):
|
||||
* - 在线模式(默认,无标记):所有请求原样转发网络,SW 形同虚设。
|
||||
* - 离线模式(/offline.html 启用后写入标记):同源 GET 请求缓存优先,
|
||||
* 缓存未命中且清单里没有的路径才回源网络,实现断网阅读。
|
||||
*
|
||||
* 版本控制:压缩包 /MayxBlog.7z 的 content-length 即版本号(每次重新部署
|
||||
* 都会变化)。SW 激活时、以及导航请求按 RECHECK_INTERVAL 节流复核;
|
||||
* 一旦发现线上压缩包与已缓存版本不一致,立即清空缓存 → 自动回退在线模式。
|
||||
* 网络不可用时复核失败,保留缓存(离线阅读本来就是离线模式的目的)。
|
||||
*/
|
||||
|
||||
const CACHE_NAME = 'mayxblog-offline-v1';
|
||||
const READY_KEY = '/__offline_manifest__';
|
||||
const ARCHIVE_URL = '/MayxBlog.7z';
|
||||
const RECHECK_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
let lastRecheck = 0;
|
||||
|
||||
self.addEventListener('install', () => {
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((keys) =>
|
||||
Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k)))
|
||||
)
|
||||
.then(verifyPayloadVersion)
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
* HEAD 请求不被下面的 fetch 处理器拦截,这里读到的一定是线上真实压缩包,
|
||||
* 而不是缓存里的 quine 副本。网络失败返回 null,调用方选择信任现有缓存。
|
||||
*/
|
||||
async function payloadSize() {
|
||||
try {
|
||||
const response = await fetch(ARCHIVE_URL, { method: 'HEAD', cache: 'no-store' });
|
||||
if (!response.ok) return null;
|
||||
const size = Number(response.headers.get('content-length'));
|
||||
return Number.isFinite(size) && size > 0 ? size : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 就绪标记同时是清单:记录了解包写入的所有路径。没有它就无法区分
|
||||
* “站点本来就没有这个页面”和“缓存条目被浏览器回收了”。
|
||||
*/
|
||||
async function readManifest(cache) {
|
||||
const hit = await cache.match(READY_KEY);
|
||||
if (!hit) return null;
|
||||
const meta = await hit.json().catch(() => null);
|
||||
if (!meta || !Array.isArray(meta.keys) || !meta.keys.length) return null;
|
||||
return meta;
|
||||
}
|
||||
|
||||
/* 压缩包更新 → 清空缓存(即回退在线模式);返回是否发生了失效。 */
|
||||
async function verifyPayloadVersion() {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const meta = await readManifest(cache);
|
||||
if (!meta) return false;
|
||||
const size = await payloadSize();
|
||||
if (size && meta.payloadSize !== size) {
|
||||
await caches.delete(CACHE_NAME);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* 服务器对目录索引的处理与 Jekyll 产物一致,这里补齐候选路径。 */
|
||||
function candidatesFor(pathname) {
|
||||
const keys = [pathname];
|
||||
if (pathname.endsWith('/')) {
|
||||
keys.push(pathname + 'index.html');
|
||||
} else {
|
||||
keys.push(pathname + '/index.html');
|
||||
keys.push(pathname + '.html');
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const request = event.request;
|
||||
if (request.method !== 'GET') return; // HEAD 等直通网络(版本探测依赖这一点)
|
||||
|
||||
const url = new URL(request.url);
|
||||
if (url.origin !== self.location.origin) return; // 跨域请求不干预
|
||||
|
||||
// 显式 no-store 的请求(重新下载压缩包等)绕过缓存,避免吃到旧的 quine 副本
|
||||
if (request.cache === 'no-store') return;
|
||||
|
||||
event.respondWith(respond(request, url, event));
|
||||
});
|
||||
|
||||
async function respond(request, url, event) {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
|
||||
// 导航请求顺带做节流版本复核;失效发生在后台,本次响应不受影响,
|
||||
// 下一次请求自然落入在线模式。
|
||||
if (request.mode === 'navigate' && Date.now() - lastRecheck > RECHECK_INTERVAL) {
|
||||
lastRecheck = Date.now();
|
||||
event.waitUntil(verifyPayloadVersion());
|
||||
}
|
||||
|
||||
const candidates = candidatesFor(url.pathname);
|
||||
for (const key of candidates) {
|
||||
const hit = await cache.match(key);
|
||||
if (hit) return hit;
|
||||
}
|
||||
|
||||
const meta = await readManifest(cache);
|
||||
if (!meta) return fetch(request); // 在线模式:纯转发
|
||||
|
||||
// 清单承诺过这个路径但缓存里没有 → 条目被浏览器回收,缓存已不可信,
|
||||
// 整体作废并回退在线模式。
|
||||
if (candidates.some((key) => meta.keys.includes(key))) {
|
||||
await caches.delete(CACHE_NAME);
|
||||
return fetch(request);
|
||||
}
|
||||
|
||||
// 清单之外的路径回源网络;断网时的导航请求用站点 404 页兜底。
|
||||
try {
|
||||
return await fetch(request);
|
||||
} catch (error) {
|
||||
if (request.mode === 'navigate') {
|
||||
const notFound = await cache.match('/404.html');
|
||||
if (notFound) {
|
||||
return new Response(notFound.body, { status: 404, headers: notFound.headers });
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 ნიკა
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+15
File diff suppressed because one or more lines are too long
BIN
Binary file not shown.
Vendored
+1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user