11 Commits

Author SHA1 Message Date
mayx 2abc8e7269 Update 6 files
- /_posts/2026-11-01-compress.md
- /_tools/blogquine.py
- /_tools/serv00_post-receive
- /_tools/envs_post-receive
- /assets/quine.rar
- /_data/other_repo_list.csv
2026-10-31 16:07:12 +00:00
mayx 0fa7816fa8 Update 3 files
- /_data/proxylist.yml
- /_data/other_repo_list.csv
- /_tools/envs_post-receive
2026-10-19 10:16:31 +00:00
mayx 7f6baa0c8d Update 2 files
- /_tools/serv00_post-receive
- /_posts/2026-10-01-quine.md
2026-10-07 06:16:54 +00:00
mayx 1052b66270 Update file default.html 2026-10-03 08:14:38 +00:00
mayx 8cb782d1ff Update 12 files
- /vendor/libarchive/LICENSE
- /vendor/libarchive/worker-bundle.js
- /vendor/libarchive/wasm-gen/libarchive.js
- /vendor/libarchive/wasm-gen/libarchive.wasm
- /sw.js
- /offline.md
- /_layouts/default.html
- /_tools/serv00_post-receive
- /_tools/envs_post-receive
- /_data/proxylist.yml
- /assets/js/offline-page.js
- /assets/js/offline-unpack.js
2026-10-03 07:57:00 +00:00
mayx 7659d3e9d5 Update 7 files
- /images/GEDSC AIRWAY BILL TO CHINA.jpg
- /images/MOS_Demo.png
- /images/MOS_Demo.webp
- /images/GEDSC AIRWAY BILL TO CHINA.webp
- /images/2020.04.06.zip
- /_posts/2018-10-28-mos-reporter.md
- /_posts/2019-06-10-cheat.md
2026-10-02 16:38:55 +00:00
mayx a4cc075205 Update 2 files
- /deploy.sh
- /.readthedocs.yaml
2026-10-01 15:06:06 +00:00
mayx 44d8461ab1 Update 3 files
- /_data/ai-cache.json
- /_data/proxylist.yml
- /deploy.sh
2026-10-01 14:49:18 +00:00
mayx 303f5df01c Update 5 files
- /_data/other_repo_list.csv
- /_data/links.csv
- /_posts/2026-10-01-quine.md
- /_tools/blogquine.py
- /assets/js/pjax.js
2026-09-30 16:01:13 +00:00
mayx 932e310f9d Update 6 files
- /_tools/blogquine.py
- /_tools/serv00_post-receive
- /_tools/envs_post-receive
- /_data/other_repo_list.csv
- /deploy.sh
- /.readthedocs.yaml
2026-09-11 10:30:53 +00:00
mayx 2036ee2c4f Update 5 files
- /_tools/blogquine.py
- /_data/proxylist.yml
- /_layouts/default.html
- /deploy.sh
- /.readthedocs.yaml
2026-09-07 09:23:41 +00:00
29 changed files with 4577 additions and 95 deletions
+5 -2
View File
@@ -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
+2 -1
View File
@@ -181,5 +181,6 @@
"/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/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不仅可以重组现有信息,还可以完成人类以前认为具有挑战性的任务。"
}
+1
View File
@@ -26,3 +26,4 @@ RavelloH's Blog,https://ravelloh.com,https://ravelloh.com/feed.xml,Beginning of
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,把喜欢的东西都记下来···
1 title link feed_url description
26 EXYONE@BLOG:~$ https://exyon.ee https://exyon.ee/feed.xml 一隅藏天地,流水遇知音。
27 小景 https://jingyuan-zheng.github.io/zh/ https://jingyuan-zheng.github.io/zh/index.xml 随心分享有趣的技术、开源项目与生活记录。
28 热汤茶馆 https://blog.hotsouprealm.top/ https://blog.hotsouprealm.top/atom.xml 茶凉了可以续,话断了可以接,这里是hahahotsoup的茶馆。
29 FGHRSH 日记簿 https://www.fghrsh.net/ https://www.fghrsh.net/feed.php 把喜欢的东西都记下来···
+28 -75
View File
@@ -4,47 +4,40 @@ http://git.dkforestseeaaq2dqz2uflmlsybvnq2irzn4ygyvu53oazyorednviid.onion/mayx/b
http://giteabolfdejtdzblkooalqei6jr67imiugmhtsh6ocw4hlj5a4q.b32.i2p/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://sij.ai/mayx/blog
https://git.projectnightcrawler.dev/mayx/blog
https://codefloe.com/mayx/blog
https://git.pixie.town/mayx/blog
https://git.minetest.land/Mayx/blog
http://1.6.141.109:3000/mayx/blog
https://mygit.iexercice.com/mayx/blog
http://162.215.134.149:4000/mayx/blog
http://45.33.87.87/mayx/blog
http://185.208.225.190:3000/mayx/blog
https://git.Apture.io/mayx/blog
http://8.134.61.107:3000/mayx/blog
http://219.157.255.213:25311/mayx/blog
http://116.236.50.103:8789/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://187.216.152.151:9999/mayx/blog
http://116.63.173.179:8001/mayx/blog
https://git.furcom.org/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://gitea.sciotech.cn/mayx/blog
https://gitea.micro-stack.org/mayx/blog
http://106.54.211.95:3000/mayx/blog
http://172.172.102.93:3000/mayx/blog
http://120.24.249.56/mayx/blog
http://120.211.66.170:8418/mayx/blog
http://209.127.59.74:3000/mayx/blog
http://121.41.2.71:3000/mayx/blog
@@ -65,40 +58,31 @@ http://8.130.135.159:3000/mayx/blog
https://git.dshkabatur.ru/mayx/blog
http://47.103.78.70:3000/mayx/blog
http://194.5.152.156:3000/mayx/blog
http://58.65.162.118:3000/mayx/blog
https://git.arkon.solutions/mayx/blog
https://gitea.yimoyuyan.cn/mayx/blog
http://221.203.14.217:3000/mayx/blog
https://dev.kiramtech.com/mayx/blog
https://git.f4e.lol/mayx/blog
http://72.61.229.93:4000/mayx/blog
http://39.101.74.135:5000/mayx/blog_cn
http://123.57.130.140:3000/mayx/blog_cn
http://118.24.46.223:3000/mayx/blog_cn
http://s3.v100.vip:31057/mayx/blog_cn
http://101.33.225.95:3000/mayx/blog_cn
http://45.55.138.82:3000/mayx/blog_cn
http://118.24.129.148:3000/mayx/blog_cn
http://120.48.141.82:3000/mayx/blog_cn
http://175.27.229.211:3000/mayx/blog_cn
https://git.yidaimingjvn.xyz/mayx/blog_cn
http://39.105.67.143:3000/mayx/blog_cn
http://82.156.89.21:3000/mayx/blog_cn
http://xujiesoft.vicp.net:3000/mayx/blog_cn
https://git.mingliqiye.com/mayx/blog_cn
http://58.213.60.6:19000/mayx/blog_cn
http://47.92.113.131:3000/mayx/blog
http://gyc.myds.me:4000/mayx/blog
https://git.graveyard.sh/mayx/blog
https://gitea.jnyuxia.com/mayx/blog
https://gitea.mynas-lechner.de/mayx/blog
https://gitlab.iplusus.com/mayx/blog
https://git.miasma-os.com/mayx/blog
https://git.zakum.cn/mayx/blog
http://47.109.103.110:9000/mayx/blog_cn
http://47.104.241.192:19999/mayx/blog_cn
http://47.98.148.146:1026/mayx/blog_cn
http://43.143.209.246:6300/mayx/blog_cn
http://39.100.183.95:88/mayx/blog_cn
http://81.70.102.154/mayx/blog_cn
http://36.133.248.69:3088/mayx/blog_cn
@@ -107,139 +91,108 @@ http://47.106.222.181:20511/mayx/blog_cn
http://118.24.161.24:3000/mayx/blog_cn
http://36.138.125.206:3000/mayx/blog_cn
https://git.crystalyx.net/mayx/blog
https://gitea.grxe.io/mayx/blog
https://gitea.accept.dev.dbf.nl/mayx/blog
https://git.jhossain.online/mayx/blog
https://barhoum-lab.fr/mayx/blog
http://gitea.snailtrack.cn/mayx/blog_cn
https://code.draussenfunker.de/mayx/blog_cn
https://git.dinsor.co.th/mayx/blog
http://www.bkandssp.cn:30/mayx/blog
http://gitea.ydxtool.com/mayx/blog
https://gitea.malxte.de/mayx/blog
https://git.nozora.top/mayx/blog
https://git.edenit.co.kr/mayx/blog
https://git.catgirlsneed.homes/mayx/blog
http://47.108.255.216:3000/mayx/blog_cn
http://210.75.240.13:3000/mayx/blog_cn
http://git.hbg99.com:8080/mayx/blog_cn
https://git.eplg.services/mayx/blog
https://git.xleed.com/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
http://124.71.169.183:3000/mayx/blog
https://git.msoucy.me/mayx/blog
http://140.120.108.238:49308/mayx/blog
http://git.chilidoginteractive.com:3000/mayx/blog_cn
http://git.uhfdemo.com/mayx/blog_cn
http://43.143.142.38:7001/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
http://gitea.mikarsoft.com/mayx/blog
https://mcgit.place/mayx/blog
https://forgejo.rekallservices.com/mayx/blog
http://git.instal.org.cn/mayx/blog
https://git.smart-family.net/mayx/blog
https://git.chiasson.cloud/mayx/blog
https://git.everestlinux.org/mayx/blog
https://git.netzbyte.com/mayx/blog
https://ataymakhzan.com/mayx/blog
https://repo.qruize.com/mayx/blog
https://git.xneon.org/mayx/blog
https://git.lamby.gay/mayx/blog
https://lab.dutt.ch/mayx/blog
https://gitea.dabit.synology.me/mayx/blog
https://git.joinplu.me/mayx/blog
http://otter.top/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://app.gitpasha.com/mayx/blog
https://shaman2025.site/mayx/blog
https://git.jhammah.com/mayx/blog
https://git.kemomimi.site/mayx/blog
https://gitea.kolesarhome.com/mayx/blog
https://gitbucket.aint-no.info/mayx/blog
https://git.wikeo.ovh/mayx/blog
https://devcraft-dot.tech/mayx/blog
https://a-t-g.ru/mayx/blog
http://qiubei-git.cn/mayx/blog_cn
http://wangbeibei.xyz:6002/mayx/blog
http://8.141.82.163:20000/mayx/blog_cn
http://81.70.30.91:3000/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
https://git.newnaturalphilosophy.org/mayx/blog_cn
https://gitea.detr.top/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
https://git.extra.eiffel.com/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://www.vitelgrid.app/mayx/blog
https://gitruhub.ru/mayx/blog
https://git.linuxposting.xyz/mayx/blog
https://git.techworkshop42.ru/mayx/blog
http://speedyfox.app/mayx/blog
https://mjjtop.com/mayx/blog
https://forgejo.wanderingmonster.dev/mayx/blog
http://v.udvip.com/mayx/blog
https://git.edavmig.ru/mayx/blog
https://gitea.ns5001k.sigma2.no/mayx/blog
https://fzsimo.com/mayx/blog
https://git.hemangvyas.com/mayx/blog
https://gitav.ru/mayx/blog
https://git.sakuzyo.net/mayx/blog
http://www.365rise.top/mayx/blog
https://git.ifuntanhub.dev/mayx/blog
https://git.aptcloud.ru/mayx/blog
https://gitlab.dev.genai-team.ru/mayx/blog
https://d.roxyipt.com/mayx/blog
https://git.umervtilte.lol/mayx/blog
https://git.bitpak.ru/mayx/blog
https://git.amamedis.de/mayx/blog
https://git.kunstglass.de/mayx/blog
https://git.kry008.xyz/mayx/blog
https://gitea.onfing.ir/mayx/blog
https://gitea.augeo.dev/mayx/blog_cn
http://36.213.200.127:23000/mayx/blog_cn
http://20.219.0.85:3000/mayx/blog_cn
https://git.niisse.net/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://gitimn.com/mayx/blog
http://git.zapthisblog.com/mayx/blog
https://git.resacachile.cl/mayx/blog
https://git.vycsucre.gob.ve/mayx/blog
https://newtrafficworksgit.online/mayx/blog
https://git.uj-s.com/mayx/blog
https://git.albiobola.nl/mayx/blog
http://gitea.amm.ar/mayx/blog
https://git.83channel.net/mayx/blog
https://git.zeppone.com/mayx/blog
https://git.telecom.quest/mayx/blog
https://repo.paperless.rs/mayx/blog
https://urbal.dev/mayx/blog
https://git.pwarde.nl/mayx/blog
https://presko.info/mayx/blog
https://bolha.dev/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
1 repo_url
4 http://giteabolfdejtdzblkooalqei6jr67imiugmhtsh6ocw4hlj5a4q.b32.i2p/mayx/blog
5 https://gitlab.lain.la/mayx/mayx.pages.lain.la
6 https://gitplac.si/mayx/mayx.gitpage.si
7 https://git.32bit.cafe/mayx/blog
8 https://gitnet.fr/mayx/blog
9 https://opencommit.eu/mayx/blog
10 https://forge.fedoraproject.org/mabbs/blog
11 https://tilde.club/~mayx/git/blog.git/
12 https://git.envs.net/Mayx/mayx
13 https://tildegit.org/Mayx/mayx https://tildegit.org/Mayx/blog
14 https://git.tilde.town/mayx/blog
15 https://gitlab.haskell.org/mayx/mayx
16 https://lavaforge.org/mayx/blog
17 https://repo2.serv00.com/git/pub/Mayx/mayx/
18 https://sij.ai/mayx/blog
19 https://git.projectnightcrawler.dev/mayx/blog
https://codefloe.com/mayx/blog
20 https://git.pixie.town/mayx/blog
21 https://git.minetest.land/Mayx/blog
22 http://1.6.141.109:3000/mayx/blog
23 https://mygit.iexercice.com/mayx/blog
24 http://162.215.134.149:4000/mayx/blog
http://45.33.87.87/mayx/blog
25 http://185.208.225.190:3000/mayx/blog
26 https://git.Apture.io/mayx/blog
http://8.134.61.107:3000/mayx/blog
27 http://219.157.255.213:25311/mayx/blog
28 http://116.236.50.103:8789/mayx/blog
29 https://repo.gusdya.net/mayx/blog
https://gitea.synapsetec.cn/mayx/blog
30 http://gitea.yunshanghub.com:8081/mayx/blog
http://113.177.27.200:2033/mayx/blog
31 http://152.69.204.151:3000/mayx/blog
32 http://187.216.152.151:9999/mayx/blog
33 http://116.63.173.179:8001/mayx/blog
34 https://git.furcom.org/mayx/blog
35 https://git.karma-riuk.com/mayx/blog
https://git.7o9o.net/mayx/blog
36 https://git.gupaoedu.cn/mayx/blog
37 https://git.7milch.com/mayx/blog
https://gitea.sciotech.cn/mayx/blog
38 https://gitea.micro-stack.org/mayx/blog
39 http://106.54.211.95:3000/mayx/blog
40 http://172.172.102.93:3000/mayx/blog
http://120.24.249.56/mayx/blog
41 http://120.211.66.170:8418/mayx/blog
42 http://209.127.59.74:3000/mayx/blog
43 http://121.41.2.71:3000/mayx/blog
58 https://git.dshkabatur.ru/mayx/blog
59 http://47.103.78.70:3000/mayx/blog
60 http://194.5.152.156:3000/mayx/blog
http://58.65.162.118:3000/mayx/blog
61 https://git.arkon.solutions/mayx/blog
62 https://gitea.yimoyuyan.cn/mayx/blog
63 http://221.203.14.217:3000/mayx/blog
https://dev.kiramtech.com/mayx/blog
64 https://git.f4e.lol/mayx/blog
65 http://72.61.229.93:4000/mayx/blog
66 http://39.101.74.135:5000/mayx/blog_cn
67 http://123.57.130.140:3000/mayx/blog_cn
68 http://118.24.46.223:3000/mayx/blog_cn
69 http://s3.v100.vip:31057/mayx/blog_cn
http://101.33.225.95:3000/mayx/blog_cn
70 http://45.55.138.82:3000/mayx/blog_cn
71 http://118.24.129.148:3000/mayx/blog_cn
72 http://120.48.141.82:3000/mayx/blog_cn
73 http://175.27.229.211:3000/mayx/blog_cn
https://git.yidaimingjvn.xyz/mayx/blog_cn
74 http://39.105.67.143:3000/mayx/blog_cn
75 http://82.156.89.21:3000/mayx/blog_cn
76 http://xujiesoft.vicp.net:3000/mayx/blog_cn
https://git.mingliqiye.com/mayx/blog_cn
77 http://58.213.60.6:19000/mayx/blog_cn
http://47.92.113.131:3000/mayx/blog
78 http://gyc.myds.me:4000/mayx/blog
79 https://git.graveyard.sh/mayx/blog
80 https://gitea.jnyuxia.com/mayx/blog
https://gitea.mynas-lechner.de/mayx/blog
81 https://gitlab.iplusus.com/mayx/blog
82 https://git.miasma-os.com/mayx/blog
https://git.zakum.cn/mayx/blog
83 http://47.109.103.110:9000/mayx/blog_cn
84 http://47.104.241.192:19999/mayx/blog_cn
85 http://47.98.148.146:1026/mayx/blog_cn
http://43.143.209.246:6300/mayx/blog_cn
86 http://39.100.183.95:88/mayx/blog_cn
87 http://81.70.102.154/mayx/blog_cn
88 http://36.133.248.69:3088/mayx/blog_cn
91 http://118.24.161.24:3000/mayx/blog_cn
92 http://36.138.125.206:3000/mayx/blog_cn
93 https://git.crystalyx.net/mayx/blog
https://gitea.grxe.io/mayx/blog
https://gitea.accept.dev.dbf.nl/mayx/blog
https://git.jhossain.online/mayx/blog
94 https://barhoum-lab.fr/mayx/blog
95 http://gitea.snailtrack.cn/mayx/blog_cn
https://code.draussenfunker.de/mayx/blog_cn
https://git.dinsor.co.th/mayx/blog
96 http://www.bkandssp.cn:30/mayx/blog
97 http://gitea.ydxtool.com/mayx/blog
98 https://gitea.malxte.de/mayx/blog
https://git.nozora.top/mayx/blog
99 https://git.edenit.co.kr/mayx/blog
100 https://git.catgirlsneed.homes/mayx/blog
http://47.108.255.216:3000/mayx/blog_cn
101 http://210.75.240.13:3000/mayx/blog_cn
102 http://git.hbg99.com:8080/mayx/blog_cn
103 https://git.eplg.services/mayx/blog
104 https://git.xleed.com/mayx/blog
105 https://git.supernets.org/mayx/blog
https://git.digitaltelepresence.com/mayx/blog
https://git.hrfee.pw/mayx/blog
106 https://git.libregaming.org/mayx/blog
107 https://git.kaki87.net/mayx/blog
108 https://forgejo.vanten-s.com/mayx/blog
109 http://124.71.169.183:3000/mayx/blog
https://git.msoucy.me/mayx/blog
110 http://140.120.108.238:49308/mayx/blog
111 http://git.chilidoginteractive.com:3000/mayx/blog_cn
http://git.uhfdemo.com/mayx/blog_cn
http://43.143.142.38:7001/mayx/blog_cn
112 https://git.panda-number.one/mayx/blog
113 https://datacortex.pl/mayx/blog
114 https://gitea.keymantek.com:777/mayx/blog
115 https://git.tirtapakuan.co.id/mayx/blog
116 http://tian-you.top:7020/mayx/blog
117 http://34.17.182.140/mayx-gogs/blog
http://gitea.mikarsoft.com/mayx/blog
118 https://mcgit.place/mayx/blog
https://forgejo.rekallservices.com/mayx/blog
119 http://git.instal.org.cn/mayx/blog
https://git.smart-family.net/mayx/blog
120 https://git.chiasson.cloud/mayx/blog
https://git.everestlinux.org/mayx/blog
https://git.netzbyte.com/mayx/blog
https://ataymakhzan.com/mayx/blog
121 https://repo.qruize.com/mayx/blog
122 https://git.xneon.org/mayx/blog
https://git.lamby.gay/mayx/blog
123 https://lab.dutt.ch/mayx/blog
124 https://gitea.dabit.synology.me/mayx/blog
125 https://git.joinplu.me/mayx/blog
http://otter.top/mayx/blog
126 http://188.66.0.25:3000/mayx/blog
127 https://gitea.deliverables.io/mayx/blog
128 http://23.182.16.150:3000/mayx/blog
129 https://git.ritonquilol.fr/mayx/blog
130 https://bis127.vse.cz/mayx/blog
https://app.gitpasha.com/mayx/blog
https://shaman2025.site/mayx/blog
131 https://git.jhammah.com/mayx/blog
https://git.kemomimi.site/mayx/blog
132 https://gitea.kolesarhome.com/mayx/blog
https://gitbucket.aint-no.info/mayx/blog
133 https://git.wikeo.ovh/mayx/blog
134 https://devcraft-dot.tech/mayx/blog
135 https://a-t-g.ru/mayx/blog
http://qiubei-git.cn/mayx/blog_cn
136 http://wangbeibei.xyz:6002/mayx/blog
137 http://8.141.82.163:20000/mayx/blog_cn
http://81.70.30.91:3000/mayx/blog_cn
138 http://yidaima.cn:6008/mayx/blog_cn
139 http://8.148.31.14:3000/mayx/blog_cn
140 http://58.17.14.95:8001/mayx/blog_cn
141 http://39.99.175.172:8000/mayx/blog_cn
https://git.newnaturalphilosophy.org/mayx/blog_cn
https://gitea.detr.top/mayx/blog_cn
142 http://175.178.193.35:3000/mayx/blog_cn
143 http://gitea.jb1000.com:88/mayx/blog_cn
144 http://www.22ai.vip/mayx/blog_cn
https://git.extra.eiffel.com/mayx/blog_cn
145 http://219.151.177.172:3000/mayx/blog_cn
146 http://62.234.20.54:3000/mayx/blog_cn
147 https://gitea.4gunn.cn:52443/mayx/blog_cn
148 http://jdunlap.com/mayx/blog
149 https://git.silica.codes/mayx/blog
https://www.vitelgrid.app/mayx/blog
150 https://gitruhub.ru/mayx/blog
https://git.linuxposting.xyz/mayx/blog
https://git.techworkshop42.ru/mayx/blog
http://speedyfox.app/mayx/blog
151 https://mjjtop.com/mayx/blog
152 https://forgejo.wanderingmonster.dev/mayx/blog
http://v.udvip.com/mayx/blog
https://git.edavmig.ru/mayx/blog
153 https://gitea.ns5001k.sigma2.no/mayx/blog
https://fzsimo.com/mayx/blog
154 https://git.hemangvyas.com/mayx/blog
155 https://gitav.ru/mayx/blog
https://git.sakuzyo.net/mayx/blog
http://www.365rise.top/mayx/blog
https://git.ifuntanhub.dev/mayx/blog
156 https://git.aptcloud.ru/mayx/blog
157 https://gitlab.dev.genai-team.ru/mayx/blog
https://d.roxyipt.com/mayx/blog
https://git.umervtilte.lol/mayx/blog
158 https://git.bitpak.ru/mayx/blog
https://git.amamedis.de/mayx/blog
159 https://git.kunstglass.de/mayx/blog
160 https://git.kry008.xyz/mayx/blog
https://gitea.onfing.ir/mayx/blog
https://gitea.augeo.dev/mayx/blog_cn
161 http://36.213.200.127:23000/mayx/blog_cn
http://20.219.0.85:3000/mayx/blog_cn
https://git.niisse.net/mayx/blog_cn
162 https://gitea.yfandxt.asia/mayx/blog_cn
163 http://81.69.57.215:3000/mayx/blog_cn
164 https://git.dglyoo.com/mayx/blog_cn
165 http://c8202.top:3000/mayx/blog_cn
166 http://nojava.fun/mayx/blog_cn
https://gitimn.com/mayx/blog
http://git.zapthisblog.com/mayx/blog
https://git.resacachile.cl/mayx/blog
167 https://git.vycsucre.gob.ve/mayx/blog
https://newtrafficworksgit.online/mayx/blog
168 https://git.uj-s.com/mayx/blog
169 https://git.albiobola.nl/mayx/blog
http://gitea.amm.ar/mayx/blog
170 https://git.83channel.net/mayx/blog
https://git.zeppone.com/mayx/blog
https://git.telecom.quest/mayx/blog
171 https://repo.paperless.rs/mayx/blog
https://urbal.dev/mayx/blog
https://git.pwarde.nl/mayx/blog
https://presko.info/mayx/blog
https://bolha.dev/mayx/blog
172 https://git.checkerwars.com/mayx/blog
173 https://git.broderlands.com/mayx/blog
174 https://gitea.schwegmann.tech/mayx/blog
175 https://gitea.kcm-nixi.cn:32443/mayx/blog
176 https://git.riskreduction.net/mayx/blog
177 https://git.twitchmagic.pro/mayx/blog
178 https://gitea.marcin00.pl/mayx/blog
179 https://git.imagitech.mx/mayx/blog
180 https://github.jogjacloud.com/mayx/blog
181 https://git.stormofgalaxy.com/mayx/blog
182 https://git.jibai.dev/mayx/blog
183 https://dev.rasadyar.com/mayx/blog
184 https://git.dew.vn/mayx/blog
185 https://git.recstar.club/mayx/blog
186 https://git.ric-rac.org/mayx/blog
187 https://git.yan.best/mayx/blog
188 https://forgejo.cgr-international.com/mayx/blog
189 https://yang-gitea.umersoft.com/mayx/blog
190 https://synthesist.d4d.host/mayx/blog
191 https://git.takits.me/mayx/blog
192 https://gitea.zazlab.freeddns.org/mayx/blog
193 https://gitweb.cn/mayx/blog
194 https://forge.harusato.com/mayx/blog
195 https://forgejo.siriusdevops.com/mayx/blog
196 https://git.tessdev.fr/mayx/blog
197 https://gitlab.navy.mi.th/mayx/blog
198 https://forgejo.quantum-forge.io/mayx/blog
+2 -3
View File
@@ -17,12 +17,10 @@ mirrors:
- https://mayx.vercel.app/
- 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.pages.lain.la/
- https://mayx.4everland.app/
- https://mayx.readthedocs.io/
- https://mayx.sourceforge.io/
@@ -46,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
+2 -1
View File
@@ -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>
+1 -1
View File
@@ -20,7 +20,7 @@ tags: [Mayx,MOS]
MOS是一个用来解释我不能解释的东西的一个代替品,就如同UFO和人们定义的神一样,因为未知,而又需要一种解释的方法,于是出现了MOS。
***
## 正文
![MOS_DEMO](/images/MOS_Demo.png "MOS的大概效果图")
![MOS_DEMO](/images/MOS_Demo.webp "MOS的大概效果图")
```
MOS Log System has been Load.
MOS Analysis System is Ready.
+1 -1
View File
@@ -139,7 +139,7 @@ Tel +34 612 520330
然后还非常用心的配了一张快递单:
![GEDSC AIRWAY BILL TO CHINA](/images/GEDSC%20AIRWAY%20BILL%20TO%20CHINA.jpg)
![GEDSC AIRWAY BILL TO CHINA](/images/GEDSC%20AIRWAY%20BILL%20TO%20CHINA.webp)
注:由于隐私原因,涉及隐私的地方已经被删除 ~~(尽管大家都知道我的手机号)~~
这个邮件当时也没细看,然后就给Lori Robinson说了我已经给他们发了我的信息,过了几天,她给我发:
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+572
View File
@@ -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()
+6 -5
View File
@@ -1,13 +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
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
@@ -16,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
+7 -3
View File
@@ -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
+119
View File
@@ -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();
})();
+269
View File
@@ -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
View File
@@ -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) {
BIN
View File
Binary file not shown.
+5 -2
View File
@@ -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
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

+37
View File
@@ -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>
+140
View File
@@ -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;
}
}
+21
View File
@@ -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.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long