mirror of
https://github.com/Mabbs/mabbs.github.io
synced 2026-11-03 11:52:29 -07:00
Compare commits
7 Commits
932e310f9d
...
7f6baa0c8d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f6baa0c8d | ||
|
|
1052b66270 | ||
|
|
8cb782d1ff | ||
|
|
7659d3e9d5 | ||
|
|
a4cc075205 | ||
|
|
44d8461ab1 | ||
|
|
303f5df01c |
@ -13,4 +13,7 @@ build:
|
||||
- curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
||||
- bundle exec jekyll build --destination $READTHEDOCS_OUTPUT/html
|
||||
- python3 _tools/blogquine.py --src-dir blog --quine-dir blog $READTHEDOCS_OUTPUT/html MayxBlog.7z
|
||||
- mv MayxBlog.7z $READTHEDOCS_OUTPUT/html
|
||||
- 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
|
||||
@ -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不仅可以重组现有信息,还可以完成人类以前认为具有挑战性的任务。"
|
||||
}
|
||||
@ -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,把喜欢的东西都记下来···
|
||||
|
||||
|
@ -4,6 +4,7 @@ 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
|
||||
@ -31,16 +32,13 @@ 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
|
||||
@ -64,11 +62,9 @@ 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
|
||||
@ -80,20 +76,16 @@ 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
|
||||
@ -106,12 +98,9 @@ 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://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
|
||||
@ -129,7 +118,6 @@ 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
|
||||
https://git.panda-number.one/mayx/blog
|
||||
@ -138,7 +126,6 @@ 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
|
||||
@ -149,7 +136,6 @@ 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
|
||||
@ -170,12 +156,10 @@ 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://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://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
|
||||
@ -185,19 +169,12 @@ 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.techworkshop42.ru/mayx/blog
|
||||
http://speedyfox.app/mayx/blog
|
||||
https://mjjtop.com/mayx/blog
|
||||
https://forgejo.wanderingmonster.dev/mayx/blog
|
||||
https://git.edavmig.ru/mayx/blog
|
||||
https://gitea.ns5001k.sigma2.no/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://git.umervtilte.lol/mayx/blog
|
||||
@ -212,17 +189,23 @@ 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
|
||||
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://repo.paperless.rs/mayx/blog
|
||||
https://urbal.dev/mayx/blog
|
||||
https://git.pwarde.nl/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://veretium.com/mayx/blog
|
||||
https://gitea.kcm-nixi.cn:32443/mayx/blog
|
||||
https://git.riskreduction.net/mayx/blog
|
||||
https://git.twitchmagic.pro/mayx/blog
|
||||
https://gitea.ctmp.ir/mayx/blog
|
||||
https://gitea.marcin00.pl/mayx/blog
|
||||
https://git.imagitech.mx/mayx/blog
|
||||
https://git.hidosi.ru/mayx/blog
|
||||
|
||||
|
@ -17,9 +17,7 @@ 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.tildepages.org/
|
||||
- https://mayx.4everland.app/
|
||||
|
||||
@ -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.7z">Download <strong>7-Zip 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>
|
||||
|
||||
@ -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说了我已经给他们发了我的信息,过了几天,她给我发:
|
||||
|
||||
1914
_posts/2026-10-01-quine.md
Normal file
1914
_posts/2026-10-01-quine.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -12,8 +12,8 @@ import time
|
||||
# ============================================================
|
||||
# Part 1: 最小 LZMA1 range coder(只做编码)
|
||||
#
|
||||
# 设计目标:只实现 quine 构造需要的 token:literal / match / rep0-match /
|
||||
# end-marker,编码字节与历史内容无关(不维护输出历史),
|
||||
# 设计目标:只实现 quine 构造需要的 token:match / rep0-match,
|
||||
# 编码字节与历史内容无关(不维护输出历史),
|
||||
# 这是"先算结构、后填数据"两阶段装配的基础。
|
||||
# 参考:Igor Pavlov 的 LZMA SDK (LzmaEnc.c / LzmaDec.c)。
|
||||
# ============================================================
|
||||
@ -126,27 +126,21 @@ def _pos_slot_and_bits(d):
|
||||
|
||||
|
||||
class LzmaEncoder:
|
||||
def __init__(self, lc=3, lp=0, pb=2):
|
||||
def __init__(self, pb=2):
|
||||
"""
|
||||
不维护输出假历史:match 的编码字节只取决于 (dist,len,pos),与历史内容无关,
|
||||
因此 quine 构造(先算结构、后填数据)不需要逐字节复制假历史。
|
||||
"""
|
||||
self.lc, self.lp, self.pb = lc, lp, pb
|
||||
self.pb = pb
|
||||
self.pos_mask = (1 << pb) - 1
|
||||
self.lp_mask = (1 << lp) - 1
|
||||
self.rc = RangeEncoder()
|
||||
self.pos = 0 # 已输出字节数
|
||||
self.state = 0
|
||||
self.prev_byte = 0
|
||||
self.reps = [0, 0, 0, 0] # rep0..rep3,存的是 0 基距离
|
||||
self.out = bytearray()
|
||||
# 概率模型
|
||||
n = PROB_INIT
|
||||
self.p_is_match = [n] * (kNumStates << kNumPosBitsMax)
|
||||
self.p_is_rep = [n] * kNumStates
|
||||
self.p_is_rep_g0 = [n] * kNumStates
|
||||
self.p_is_rep_g1 = [n] * kNumStates
|
||||
self.p_is_rep_g2 = [n] * kNumStates
|
||||
self.p_rep0_long = [n] * (kNumStates << kNumPosBitsMax)
|
||||
self.p_pos_slot = [n] * (kNumLenToPosStates << kNumPosSlotBits)
|
||||
self.p_spec_pos = [n] * (kNumFullDistances - kEndPosModelIndex) # 114
|
||||
@ -159,7 +153,6 @@ class LzmaEncoder:
|
||||
self.p_rep_len_low = [n] * (16 * kNumLowLenSymbols)
|
||||
self.p_rep_len_mid = [n] * (16 * kNumMidLenSymbols)
|
||||
self.p_rep_len_high = [n] * (1 << kNumHighLenBits)
|
||||
self.p_lit = [n] * (0x300 << (lc + lp))
|
||||
|
||||
# ---------- 内部 ----------
|
||||
@property
|
||||
@ -184,25 +177,6 @@ class LzmaEncoder:
|
||||
self.rc.bittree_encode(high, 0, kNumHighLenBits, l - kNumMidLenSymbols)
|
||||
|
||||
# ---------- 对外 token ----------
|
||||
def literal(self, b):
|
||||
if self.state >= kNumLitStates:
|
||||
raise NotImplementedError("matched-literal 未实现(quine 构造不需要)")
|
||||
# 每个符号先编 isMatch 位:0 表示这是 literal
|
||||
ps = self.pos_state
|
||||
self.rc.encode_bit(self.p_is_match, (self.state << kNumPosBitsMax) + ps, 0)
|
||||
lit_state = ((self.pos & self.lp_mask) << self.lc) + (self.prev_byte >> (8 - self.lc))
|
||||
self.rc.bittree_encode(self.p_lit, lit_state * 0x300, 8, b)
|
||||
self.out.append(b)
|
||||
self.prev_byte = b
|
||||
self.pos += 1
|
||||
# UpdateState_Literal
|
||||
if self.state <= 3:
|
||||
self.state = 0
|
||||
elif self.state <= 9:
|
||||
self.state -= 3
|
||||
else:
|
||||
self.state -= 6
|
||||
|
||||
def _write_dist(self, dist, lts):
|
||||
d = dist - 1
|
||||
slot, n, low_bits = _pos_slot_and_bits(d)
|
||||
@ -217,7 +191,6 @@ class LzmaEncoder:
|
||||
self.rc.encode_direct_bits(low_bits >> kNumAlignBits, n - kNumAlignBits)
|
||||
self.rc.bittree_reverse_encode(self.p_align, 0, kNumAlignBits,
|
||||
low_bits & ((1 << kNumAlignBits) - 1))
|
||||
return d
|
||||
|
||||
def match(self, dist, length):
|
||||
assert kMatchMinLen <= length <= 273, length
|
||||
@ -227,11 +200,9 @@ class LzmaEncoder:
|
||||
lts = min(length - kMatchMinLen, kNumLenToPosStates - 1)
|
||||
self._encode_len(self.p_len_choice,
|
||||
self.p_len_low, self.p_len_mid, self.p_len_high, length)
|
||||
d = self._write_dist(dist, lts)
|
||||
# 状态与 rep 链更新
|
||||
self._write_dist(dist, lts)
|
||||
# 状态更新
|
||||
self.state = 7 if self.state < kNumLitStates else 10
|
||||
self.reps[3], self.reps[2], self.reps[1], self.reps[0] = \
|
||||
self.reps[2], self.reps[1], self.reps[0], d
|
||||
self.pos += length
|
||||
|
||||
def rep_match(self, length):
|
||||
@ -247,16 +218,6 @@ class LzmaEncoder:
|
||||
self.state = 8 if self.state < kNumLitStates else 11
|
||||
self.pos += length
|
||||
|
||||
def end(self):
|
||||
"""LZMA 结束标记:编码一个 0 基距离为 0xFFFFFFFF 的 match。"""
|
||||
ps = self.pos_state
|
||||
self.rc.encode_bit(self.p_is_match, (self.state << kNumPosBitsMax) + ps, 1)
|
||||
self.rc.encode_bit(self.p_is_rep, self.state, 0)
|
||||
self._encode_len(self.p_len_choice,
|
||||
self.p_len_low, self.p_len_mid, self.p_len_high, kMatchMinLen)
|
||||
self._write_dist(0x100000000, 0)
|
||||
self.state = 7 if self.state < kNumLitStates else 10
|
||||
|
||||
def finish(self):
|
||||
return self.rc.finish()
|
||||
|
||||
@ -374,7 +335,7 @@ class BlogQuine:
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
dirnames.sort()
|
||||
for dn in dirnames:
|
||||
rel = os.path.relpath(os.path.join(dirpath, dn), root)
|
||||
rel = os.path.relpath(os.path.join(dirpath, dn), root).replace(os.sep, "/")
|
||||
self.walk_entries.append((self._prefixed(rel), True))
|
||||
for fn in sorted(filenames):
|
||||
p = os.path.join(dirpath, fn)
|
||||
|
||||
@ -7,7 +7,8 @@ mkdir Mabbs
|
||||
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
||||
bundle exec jekyll build -d ../public_html
|
||||
python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../public_html/ MayxBlog.7z
|
||||
mv MayxBlog.7z ../public_html/
|
||||
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
|
||||
@ -18,6 +19,6 @@ 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 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,18 +7,21 @@ 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/
|
||||
python3 _tools/blogquine.py --src-dir blog --quine-dir blog ../domains/mayx.serv00.net/public_html/ MayxBlog.7z
|
||||
mv MayxBlog.7z ../domains/mayx.serv00.net/public_html/
|
||||
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
assets/js/offline-page.js
Normal file
119
assets/js/offline-page.js
Normal 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
assets/js/offline-unpack.js
Normal file
269
assets/js/offline-unpack.js
Normal 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);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -3,4 +3,7 @@ mkdir Mabbs
|
||||
curl -L -o Mabbs/README.md https://github.com/Mabbs/Mabbs/raw/main/README.md
|
||||
bundle exec jekyll build -d public
|
||||
python3 _tools/blogquine.py --src-dir blog --quine-dir blog public MayxBlog.7z
|
||||
mv MayxBlog.7z public/
|
||||
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 |
BIN
images/GEDSC AIRWAY BILL TO CHINA.webp
Normal file
BIN
images/GEDSC AIRWAY BILL TO CHINA.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 331 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 748 KiB |
BIN
images/MOS_Demo.webp
Normal file
BIN
images/MOS_Demo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 209 KiB |
37
offline.md
Normal file
37
offline.md
Normal 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
sw.js
Normal file
140
sw.js
Normal 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
vendor/libarchive/LICENSE
vendored
Normal file
21
vendor/libarchive/LICENSE
vendored
Normal 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.
|
||||
15
vendor/libarchive/wasm-gen/libarchive.js
vendored
Normal file
15
vendor/libarchive/wasm-gen/libarchive.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
vendor/libarchive/wasm-gen/libarchive.wasm
vendored
Normal file
BIN
vendor/libarchive/wasm-gen/libarchive.wasm
vendored
Normal file
Binary file not shown.
1
vendor/libarchive/worker-bundle.js
vendored
Normal file
1
vendor/libarchive/worker-bundle.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user