<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Vinking]]></title><description><![CDATA[愿星光永远偏爱你的梦境]]></description><link>https://www.vinking.top</link><image><url>https://cdn.vinking.top/avatar.webp</url><title>Vinking</title><link>https://www.vinking.top</link></image><generator>Yohaku (https://github.com/Innei/Yohaku)</generator><lastBuildDate>Sat, 14 Nov 2026 14:42:15 GMT</lastBuildDate><atom:link href="https://www.vinking.top/feed" rel="self" type="application/rss+xml"/><pubDate>Sat, 14 Nov 2026 14:42:15 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[IFT：或许是未来 Web 中文字体的最优方案之一]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn.vinking.top/TJ8hiXqvXr8u4OE.webp"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://www.vinking.top/posts/codes/ift-future-web-chinese-font-solution">https://www.vinking.top/posts/codes/ift-future-web-chinese-font-solution</a></blockquote><div><p>有时候，我们需要给网页引入网络字体来增强页面的阅读体验。</p><p>纯英文页面通常只需要一套包含大小写字母、数字以及符号的字体，在经过 WOFF2 压缩之后，这类字体的体积往往非常小。但是对于中文为主的网页来说，一套包含完整中文的字体即使经过压缩后文件体积也可能达到 MB 级别。如果直接引用未经过优化的字体文件会给页面加载带来产生非常明显的网络开销。</p><p>目前字体方面已经有一系列非常成熟的优化方案，例如对字体进行预加载、子集化、<code>unicode-range</code> 切片、<strong><code>font-display: swap</code></strong> 等等。在引入这些方案后会有非常明显的优化效果，通常情况下，能将一套包含中文字体文件体积控制在一两百 KB 甚至更小而且不会对页面体验产生明显影响，这样的优化已经能满足绝大部分的场景。</p><h2 id="--dlc-">直接高效的本体 + DLC 方案</h2><p>最近开发的过程中碰到了一个比较有意思的情况：页面有一组固定不变的字符以及另一组会不定时改变且经常复用的带生僻字的动态字符。原来的解决方法类似于游戏本体 + DLC，通过脚本 + 工作流的方式定时从后端获取所有动态字符进行子集化，生成不变字符字体和动态字符字体。这种方法非常直接高效，原本 5.7 MB 的字体文件压缩到了 110KB 的不变字符字体 + 8KB 的动态字符字体。</p><p>但是前段时间调整数据库架构之后，如果继续使用之前的解决方法去聚合所有动态字符，会把私有用户的某些信息暴露出去。所以需要一种不预先收集所有文本，也能按需加载字体中缺失字符的方案。</p><h2 id="-unicode-range-">看起来很合理的 <code>unicode-range</code> 方案</h2><p>当使用 <code>@font-face</code> 引入字体的时候，CSS 提供了一个 <code>unicode-range</code> 描述符，允许传入 Unicode 码位来实现根据页面实际使用的字符，加载对应的字体文件，从而避免直接传入庞大的字体文件。</p><pre class="language-css lang-css"><code class="language-css lang-css">@font-face {
  font-family: &#x27;Noto Sans SC&#x27;;
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/notosanssc/v41/k3kCo84MPvpLmixcA63oeAL7Iqp5IZJF9bmaG9_FnYlNbPzT7HEL7j12XCOHJKg4RgZw3nFTvwZ8atTsBvwlvRUk7mYP2g.4.woff2) format(&#x27;woff2&#x27;);
  unicode-range: U+1f1e9-1f1f5, U+1f1f7-1f1ff, U+1f21a, U+1f232, U+1f234-1f237, U+1f250-1f251, U+1f300, U+1f302-1f308, U+1f30a-1f311, U+1f315, U+1f319-1f320, U+1f324, U+1f327, U+1f32a, U+1f32c-1f32d, U+1f330-1f357, U+1f359-1f37e;
}
</code></pre>
<p>根据目前的需求可以将不变字符组单独切分一片，其余字符根据 <a href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC">Google Fonts 中文字体的 unicode-range 切片方式</a>，剔除掉不变字符组后再进行切片。</p><p>实际上 Google Fonts 等中文字体切片方案并不是简单地将一套字体按照码位进行平均切片，其会在字频、切片后的字体文件体积、每个页面平均的请求数量以及字符覆盖率之间做平衡。</p><p><img alt="来源：https://fontsconverter.com/guides/font-subsetting-by-language" src="https://cdn.vinking.top/TJ8hiXqvXr8u4OE.webp"/></p><p>这种方案在普通网站中效果非常优秀，但是上文提到，这个页面的动态字符里带有生僻字（而且还不少😭。实际测试下来大量生僻字被分散到各种大块 Unicode 切片里，仅仅一个生僻字 “翳” 就得触发一整个几十 KB 切片的下载。即使限制了单个切片的最大体积，最夸张的一个页面也还是会触发 50 个切片总共超过 400 KB 的字体下载。为了减轻字体压力的 <code>unicode-range</code> 反而产生了大量不必要的开销。</p><table><thead><tr><th>  </th><th> 极端页面数据量 </th><th> 极端页面请求次数 </th><th> 平均页面数据量 </th><th> 平均页面请求次数 </th><th> 总切片数 </th></tr></thead><tbody><tr><td> Google Fonts 切片方式 </td><td> 705.6 KB </td><td> 31 </td><td> 363.5 KB </td><td> 13.9 </td><td> 98 </td></tr><tr><td> Google Fonts 切片方式（单个最大 8 KB） </td><td> 425.7 KB </td><td> 50 </td><td> 239.2 KB </td><td> 18.8 </td><td> 335 </td></tr><tr><td> 8 码位切片 </td><td> 253.3 KB </td><td> 77 </td><td> 174.1 KB </td><td> 23.7 </td><td> 2854 </td></tr><tr><td> 32 码位切片 </td><td> 431.6 KB </td><td> 75 </td><td> 218.1 KB </td><td> 23 </td><td> 765 </td></tr><tr><td> 64 码位切片 </td><td> 643.9 KB </td><td> 66 </td><td> 293.7 KB </td><td> 22.4 </td><td> 406 </td></tr></tbody></table><h2 id="-ift">未来可期的 IFT</h2><blockquote class="markdown-alert-warning"><header>WARNING</header><p>截止文章撰写时依然没有浏览器原生支持 IFT，下文的测试均使用官方 DEMO (https://garretrieger.github.io/ift-demo) 的 WASM 实现</p></blockquote>
<p>目前正在 W3C 候选推荐草案阶段的 <a href="https://www.w3.org/TR/IFT/">增量字体传输 IFT</a> 提供了一个新的选择。</p><h3 id="ift-">IFT 又是什么</h3><p>与 Unicode 切片是一个个完整的字体文件不同，IFT 实际上是由一个初始字体文件 + 许多后续加载的二进制字体补丁组成。初始字体文件内自带一份补丁映射表，浏览器在加载完初始字体文件之后会先检查其中的字体子集是否已经包含页面所有的字形，再根据补丁映射表下载对应的字形补丁，最后合并到初始字体文件中。</p><p>一个 TTF 的字体文件里主要包含字形数据和字体表两类数据，字形数据用来说明一个字符的形状以及应该怎么绘制出这个字符，而字体表提供怎么去找对应的字符以及怎么去排版等的信息。由于 IFT （<strong>增量</strong>字体传输）允许字体文件通过打补丁的方式来拓展字体集，所以 IFT 也主要分为字形补丁（glyph keyed patch）和表补丁（table keyed patch）两种机制，字形补丁用来<strong>增量</strong>补上字符字的字形数据，表补丁用来<strong>增量</strong>补充或者更新字体表。</p><p>IFT 对于字形补丁和表补丁两种补丁机制有可能会出现两种不同的处理方式：</p><ol start="1"><li>字形补丁描述的是单个字符如何进行处理，一般不会因为其中某一个字形补丁而导致另一个字形补丁失效。</li><li>而表补丁除了可以修改字体表以外，还可能更新补丁映射表。上文说过浏览器在加载完初始字体文件会根据补丁映射表下载对应的字形补丁，一旦触发了更新补丁映射表，其余的表补丁以及字形补丁就需要等待补丁映射表更新完成后重新去判断是否还需要该补丁，所以一般情况下处理表补丁会比处理字形补丁更复杂。</li></ol><p style="padding:6px 12px;border-left:2px solid #C56473;background:#C5647350;font-style:italic;font-weight:500">Not support render this content in RSS render</p><h3 id="">效果</h3><p>要使用 IFT 的相关功能需要使用 <a href="https://github.com/w3c/ift-encoder">w3c/ift-encoder</a> 库对字体文件进行 IFT 编码，这里我们关注几个主要的参数：</p><p>由于页面是中文为主，可以使用 <code>--auto_config_primary_script=Script_chinese-simplified</code> 来指定主要文字系统为简体中文。但是与上文的 Google Fonts 切片方式类似，对于大量生僻字的页面来说这种编码方式效果依然不佳，好在 IFT 允许通过 <code>FrequencyDataConfig</code> 指向根据我们自己字频数据编码的 Riegeli 文件来贴合页面的实际效果，后面测试中会使用页面以前的动态字符数据构建自己的 Riegeli 文件。</p><p>上文提到表补丁可能相互产生影响，所以在对字体进行编码的时候，编码器需要使用补丁图去处理不同补丁之间的关系。我们可以使用<code>max_depth</code> 参数去限制表补丁图的最大深度，否则会导致补丁规模爆炸。</p><p>原始 TTF 字体可能包含几万个字符，在编码的时候需要对里面的字符组合打包成若干份补丁，编辑器想要找到最优的打包方案的话需要对每个候选组合进行非常精确同时也非常慢的压缩（主合并），如果直接进行主合并会导致编码过程慢到不可用的状态，所以在主合并前需要进行粗略的预处理合并。预处理合并的工作流程主要受 <strong><code>preprocess_merging_group_size</code>  和 </strong><code>preprocess_merging_probability_threshold</code>** 这两个参数控制。具体如下：</p><ol start="1"><li>将 Riegeli 文件按字频数据从高到低进行排序</li><li>概率低于 <strong><code>preprocess_merging_probability_threshold</code></strong> 的字归类为低频字，从尾部开始每 <strong><code>preprocess_merging_group_size</code></strong> 个字符打包成一份补丁</li><li>概率高于 <strong><code>preprocess_merging_probability_threshold</code></strong> 的字归类为高频字，交由主合并进行后续打包</li></ol><p>这样就能在保证效果的情况下，极大减轻编码流程的时间以及工作量。</p><p>对字体进行 IFT 编码后，页面相关测试结果如下：</p><table><thead><tr><th> 方案 </th><th> 极端页面数据量 </th><th> 极端页面请求次数 </th><th> 平均页面数据量 </th><th> 平均页面请求次数 </th><th> 总分片数 </th></tr></thead><tbody><tr><td> IFT（ Script_chinese-simplified） </td><td> 455.9 KB </td><td> 428 </td><td> 436.3 KB </td><td> 403 </td><td> 9845 </td></tr><tr><td> 8 码位切片 unicode-range 方案 </td><td> 253.3 KB </td><td> 77 </td><td> 174.1 KB </td><td> 23.7 </td><td> 2854 </td></tr><tr><td> IFT（后续全部使用自定义字频数据） </td><td> 226.2 KB </td><td> 49 </td><td> 182 KB </td><td> 10.8 </td><td> 13026 </td></tr><tr><td> IFT （深度=3 + 预处理合并=12） </td><td> 247.9 KB </td><td> 49 </td><td> 169.3 KB </td><td> 10.8 </td><td> 1944 </td></tr><tr><td> IFT （深度=3 + 预处理合并=8） </td><td> 235.4 KB </td><td> 49 </td><td> 169.9 KB </td><td> 10.8 </td><td> 2648 </td></tr><tr><td> IFT （深度=3 + 预处理合并=6） </td><td> 228.8 KB </td><td> 49 </td><td> 170.8 KB </td><td> 10.8 </td><td> 3354 </td></tr><tr><td> IFT （深度=3） </td><td> 224.2 KB </td><td> 49 </td><td> 172.5 KB </td><td> 10.8 </td><td> 4760 </td></tr><tr><td> IFT （深度=4） </td><td> 225.6 KB </td><td> 49 </td><td> 172 KB </td><td> 10.8 </td><td> 5362 </td></tr><tr><td> IFT （深度=5） </td><td> 223.7 KB </td><td> 49 </td><td> 171.6 KiB </td><td> 10.8 </td><td> 6762 </td></tr></tbody></table><h2 id="-ift-">所以 IFT 方案目前推荐使用吗？</h2><p>在「不预先收集所有文本，也要按需加载字体中缺失的字符」的限制下，算上后续尝试的现用现切的动态子集方案，IFT 方案依然是我目前试过最满意的方案。</p><p>极端页面下，对比 <code>unicode-range</code> 方案里最优的 8 码位切片，IFT 的请求数减少了三分之一以上，传输数据量也更低。而得益于 IFT 的增量传输机制，能够实现一直围绕同一份字体文件，相对于 <code>unicode-range</code> 切分成许多不同的分片字体，IFT 有不容易破坏字距、连字排版规则等原生优势。</p><p>但是，其目前仍有两个明显的障碍：一是目前需要捆绑上一个 WASM 来实现 IFT 功能，会白白增加两百多 KB 的开销，二是没法充分利用浏览器原生的字体预加载等优化机制，所以现阶段我并不推荐使用这套方案。</p><p>等到未来哪天浏览器原生支持 <code>tech(incremental)</code>，彻底甩掉 WASM 之后，IFT 大概就会从「未来可期」变成真正的最优选择之一吧。</p></div><p style="text-align:right"><a href="https://www.vinking.top/posts/codes/ift-future-web-chinese-font-solution#comments">览毕，何不一言？</a></p></div>]]></description><link>https://www.vinking.top/posts/codes/ift-future-web-chinese-font-solution</link><guid isPermaLink="true">https://www.vinking.top/posts/codes/ift-future-web-chinese-font-solution</guid><dc:creator><![CDATA[Vinking]]></dc:creator><pubDate>Mon, 12 Oct 2026 11:28:38 GMT</pubDate></item><item><title><![CDATA[半小时没想好一个标题不如去睡大觉]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn.vinking.top/HnkjBs8ulfglaHF.webp"/><link rel="preload" as="image" href="https://cdn.vinking.top/yXPZIVP6zAQjnA5.webp"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://www.vinking.top/notes/24">https://www.vinking.top/notes/24</a></blockquote><div><p>最近发现服务器系统的 EOL 快到了，加上目前的 2C2G 配置跑几个项目的话内存占用就已经常驻 70%+，于是趁着续费服务器就直接换了台 2C4G 的服务器（才不是因为新买一台比续费便宜。</p><p>因为两台服务器的系统不同，无法通过自定义镜像去快速迁移数据。但是得益于目前 AI 的空前发达，服务器创建了个快照之后给 AI 配置了两条隧道，尝试让它帮忙做迁移。原本以为要花一下午才弄好，没想到半个小时不到就搞定了 <del>AI真是太好用了你知道吗</del></p><p>还顺便在腾讯云领了国补（其实是退的钱😋</p><p><img src="https://cdn.vinking.top/HnkjBs8ulfglaHF.webp"/></p><hr/><p>另外最近玩的二游又多又杂，每个二游都有自己的社区签到给蚊子腿奖励，每天起床一睁眼就跟上班一样去各个社区签到。参考 GitHub 上面不同社区的签到脚本缝了个带推送的整合版脚本，给身边玩二游的朋友们用了反响似乎还挺不错。</p><p><img src="https://cdn.vinking.top/yXPZIVP6zAQjnA5.webp"/></p></div><p style="text-align:right"><a href="https://www.vinking.top/notes/24#comments">览毕，何不一言？</a></p></div>]]></description><link>https://www.vinking.top/notes/24</link><guid isPermaLink="true">https://www.vinking.top/notes/24</guid><dc:creator><![CDATA[Vinking]]></dc:creator><pubDate>Sat, 08 Aug 2026 19:32:44 GMT</pubDate></item><item><title><![CDATA[从快捷指令到 App：一次在免费开发者账号下实现 HealthKit 授权的踩坑记录]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn.vinking.top/o9nk09MfoW1fcOK.webp"/><link rel="preload" as="image" href="https://cdn.vinking.top/XEyqibYJ2iLluLc.webp"/><link rel="preload" as="image" href="https://cdn.vinking.top/ERMTumG3I5dbB75.webp"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://www.vinking.top/posts/codes/healthkit-authorization-free-apple-developer-account">https://www.vinking.top/posts/codes/healthkit-authorization-free-apple-developer-account</a></blockquote><div><p>以前写过一篇文章，使用快捷指令实现将健身记录同步到博客的个人状态中。</p><p><a href="https://www.vinking.top/posts/codes/sync-fitness-records-to-blog-status">https://www.vinking.top/posts/codes/sync-fitness-records-to-blog-status</a></p><p>但是实际上用下来感觉非常的难受，一方面由于快捷指令太长了，一旦需要对其中部分的信息进行调整，操作会非常非常的麻烦；另一方面，苹果的自动化并没有固定时间自动执行的选项，为了覆盖一整天的定时同步，需要创建五六个自动化任务。整体显得非常笨重且繁琐。所以在过年的这段时间写了一个应用，使用 HKObserverQuery + 小组件定时任务来实现数据的后台自动推送。</p><p>::: masonry
<img height="2556" src="https://cdn.vinking.top/o9nk09MfoW1fcOK.webp" width="1179"/>
<img height="2556" src="https://cdn.vinking.top/XEyqibYJ2iLluLc.webp" width="1179"/>
<img height="2556" src="https://cdn.vinking.top/ERMTumG3I5dbB75.webp" width="1179"/>
:::</p><p>但由于我使用的是免费的苹果开发者账户，只能将应用手动打包为 ipa 文件，然后通过 SideStore 进行安装。导致即使声明了 HealthKit entitlements，安装后也不会弹出健康数据授权页面，也就无法访问任何健康数据。网上找了一圈也没什么好的解决方案，一度以为只能每周连接 Xcode 重新构建到手机来给应用续命了。</p><p>后面无意中翻到 Reddit 的这个 <a href="https://www.reddit.com/r/AltStore/comments/1mnnvtd/issue_with_app_ids_not_expiring">帖子</a> ，评论区里有人提到一个思路：通过修改应用的 Bundle ID，让它使用另一个已经存在 App ID 来通过替身的方式绕开 App ID 未过期导致无法安装新应用的问题。</p><p>经过尝试，这个方法也适用于解决通过 SideStore 安装的应用无法弹出健康数据授权的问题。具体做法是：通过将 Xcode 项目里的 Bundle ID 改成与最初通过 SideStore 安装该应用时相同的 App ID，然后在 Xcode 里连接真机运行一次，让 Xcode 给这个 App ID 注册 HealthKit capability ，这时候打开应用就可以正常弹出健康数据授权页面并且完成数据的授权。</p><p>最后将应用重新构建为 ipa，再用 SideStore 安装后，就能在已授权状态下访问到 HealthKit 数据。这种方法可以在一定程度上绕开苹果免费开发者账户侧载应用不弹授权页面的限制。</p><p><del>当然通过这种方式完成的授权能否长期保持还有待进一步观察。如果后续有新的结果，我会再更新。</del> 该方法确认可以长期使用。</p></div><p style="text-align:right"><a href="https://www.vinking.top/posts/codes/healthkit-authorization-free-apple-developer-account#comments">览毕，何不一言？</a></p></div>]]></description><link>https://www.vinking.top/posts/codes/healthkit-authorization-free-apple-developer-account</link><guid isPermaLink="true">https://www.vinking.top/posts/codes/healthkit-authorization-free-apple-developer-account</guid><dc:creator><![CDATA[Vinking]]></dc:creator><pubDate>Mon, 02 Mar 2026 07:54:59 GMT</pubDate></item><item><title><![CDATA[获取《明日方舟：终末地》的抽卡记录]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn.vinking.top/R1secGzm8IL0qt7.webp"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://www.vinking.top/posts/codes/how-to-obtain-gacha-records-in-arknights-endfield">https://www.vinking.top/posts/codes/how-to-obtain-gacha-records-in-arknights-endfield</a></blockquote><div><div><blockquote class="markdown-alert-note"><header>NOTE</header><p>2026-10-06 更新：本文最初写于开服时，而目前官方 API 返回的数据结构、相关数据的获取和处理都经过了较大变化。现补上赠礼机制、重构池等信息。</p></blockquote>
<p>今天《明日方舟：终末地》公测了。体验了一段时间之后，想着将抽卡数据导入<a href="https://astrionyx.vinking.top/">抽卡分析应用</a>中。但是可能是游戏刚开服的缘故，找了一轮都没能找到导出抽卡数据的工具，最终只能自己动手实现这个功能。下面分享主要思路，如果你需要实现这个功能希望对你有所帮助。URL 里都有什么</p><p>首先，和其他游戏一样，我们可以通过抓包软件抓取或者一些更方便的方法<a href="#1"><sup>1</sup></a>获取到这样的抽卡记录链接：</p><pre class="language-text lang-text"><code class="language-text lang-text">https://ef-webview.hypergryph.com/api/record/char?lang=zh-cn&amp;seq_id={seqId}&amp;pool_type={poolType}&amp;token={token}&amp;server_id={serverId}
</code></pre>
<p><img alt="抽卡记录链接" src="https://cdn.vinking.top/R1secGzm8IL0qt7.webp"/></p><hr/><h3 id="">角色池</h3><p>针对角色池，我们主要关注 <code>pool_type</code> 和 <code>seq_id</code> 这两个参数：</p><p><code>pool_type</code> 是卡池枚举值，可以通过下面 API 获取抽卡记录页面提供的角色池：</p><pre class="language-text lang-text"><code class="language-text lang-text">https://ef-webview.hypergryph.com/api/record/char/meta?lang=zh-cn&amp;token={token}&amp;server_id={serverId}
</code></pre>
<p>返回数据：</p><pre class="language-json lang-json"><code class="language-json lang-json">{
    &quot;code&quot;: 0,
    &quot;data&quot;: {
        &quot;tabs&quot;: [{
                &quot;key&quot;: &quot;special&quot;,
                &quot;label&quot;: &quot;&quot;,
                &quot;poolType&quot;: &quot;E_CharacterGachaPoolType_Special&quot;
            },
            {
                &quot;key&quot;: &quot;rerun&quot;,
                &quot;label&quot;: &quot;&quot;,
                &quot;poolType&quot;: &quot;E_CharacterGachaPoolType_Rerun&quot;
            },
            {
                &quot;key&quot;: &quot;normal&quot;,
                &quot;label&quot;: &quot;&quot;,
                &quot;poolType&quot;: &quot;E_CharacterGachaPoolType_Standard&quot;
            }
        ],
        &quot;beginnerPullCount&quot;: 40
    },
    &quot;msg&quot;: &quot;&quot;
}
</code></pre>
<p>目前仅能取如下卡池之一：</p><ul><li>启程寻访（新手池）：<code>E_CharacterGachaPoolType_Beginner</code></li><li>基础寻访（常驻池）：<code>E_CharacterGachaPoolType_Standard</code></li><li>特许寻访（限定池）：<code>E_CharacterGachaPoolType_Special</code></li><li>重构寻访（复刻池）：<code>E_CharacterGachaPoolType_Rerun</code></li><li>特殊寻访（辉光庆典）：<code>E_CharacterGachaPoolType_Joint</code></li></ul><p>而与原神，鸣潮等游戏不同，终末地的抽卡记录链接通常带有 <code>seq_id</code> 参数。</p><pre class="language-json lang-json"><code class="language-json lang-json">// seq_id=26&amp;pool_type=E_CharacterGachaPoolType_Standard
{
  &quot;code&quot;: 0,
  &quot;data&quot;: {
    &quot;list&quot;: [
      //...
      {
        &quot;poolId&quot;: &quot;standard&quot;,
        &quot;poolName&quot;: &quot;基础寻访&quot;,
        &quot;charId&quot;: &quot;chr_0019_karin&quot;,
        &quot;charName&quot;: &quot;秋栗&quot;,
        &quot;rarity&quot;: 4,
        &quot;isFree&quot;: false,
        &quot;isNew&quot;: false,
        &quot;gachaTs&quot;: &quot;1769073164712&quot;,
        &quot;seqId&quot;: &quot;21&quot;
      }
    ],
    &quot;hasMore&quot;: false
  },
  &quot;msg&quot;: &quot;&quot;
}

// pool_type=E_CharacterGachaPoolType_Beginner
{
  &quot;code&quot;: 0,
  &quot;data&quot;: {
    &quot;list&quot;: [
      {
        &quot;poolId&quot;: &quot;beginner&quot;,
        &quot;poolName&quot;: &quot;启程寻访&quot;,
        &quot;charId&quot;: &quot;chr_0022_bounda&quot;,
        &quot;charName&quot;: &quot;萤石&quot;,
        &quot;rarity&quot;: 4,
        &quot;isFree&quot;: false,
        &quot;isNew&quot;: false,
        &quot;gachaTs&quot;: &quot;1769072324787&quot;,
        &quot;seqId&quot;: &quot;20&quot; //该卡池最新的抽卡记录
      },
      // ...
    ],
    &quot;hasMore&quot;: true
  },
  &quot;msg&quot;: &quot;&quot;
}
</code></pre>
<p>通过观察上面两个不同卡池的记录，我们可以发现 <code>seq_id</code> 参数的一些特征：</p><ol start="1"><li><code>seq_id (seqId)</code> 是一个 <strong>账号级的</strong>、<strong>跨子卡池的</strong>、<strong>不同类型卡池互相独立的</strong> 递增序号，所有角色子卡池的抽卡记录共享同一条 <code>seq_id (seqId)</code> 序列，而武器子卡池的抽卡记录共享另一条 <code>seq_id (seqId)</code> 序列。在同一卡池类型（所有角色池或者所有武器池）下，即使我们更换子卡池的类型也不会让 <code>seq_id (seqId)</code> 重新开始计数。</li><li><code>seq_id (seqId)</code> 数字越大，时间越晚、数据越新。</li><li>当请求的链接不带 <code>seq_id</code> 参数的时候会返回该卡池最新的五条记录（即 <code>seqId</code> 最大的五条记录）。</li></ol><p>根据这些特征我们可以知道：<strong>在游戏里的抽卡记录展示页中，我们从第一页开始往后翻（从新记录到旧记录）的过程就是抽卡记录链接 <code>seq_id</code> 参数递减的过程。</strong></p><p>在链接返回的抽卡记录中，还会带有一个 <code>hasMore</code> 参数。其代表的是当前返回结果的最小 <code>seqId</code> 之下是否还有数据，反映到抽卡页面就是 是否还能翻下一页。</p><p>此外，终末地的卡池有赠礼机制。当期特许寻访累计达到 60 次，会额外获得 1 本寻访情报书，接口也会添加一条 <code>kind: &quot;gift_intel_book&quot;</code> 的奖励记录。在处理抽卡数据的时候需要对其进行过滤，否则会出现接口返回的记录条数与抽卡次数不一致。</p><hr/><h3 id="">武器池</h3><p>与角色池相似，武器池同样可以通过两个 API 获取记录：</p><h4 id="">获取武器池列表</h4><p>先通过下面的链接获取用户存在抽卡记录的武器池列表：</p><pre class="language-text lang-text"><code class="language-text lang-text">https://ef-webview.hypergryph.com/api/record/weapon/pool?lang=zh-cn&amp;token={token}&amp;server_id={serverId}
</code></pre>
<p>返回的数据结构：</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;code&quot;: 0,
  &quot;data&quot;: [
    { &quot;poolId&quot;: &quot;weponbox_1_0_1&quot;, &quot;poolName&quot;: &quot;熔铸申领&quot; },
    { &quot;poolId&quot;: &quot;weaponbox_constant_2&quot;, &quot;poolName&quot;: &quot;星声申领&quot; }
    // ...
  ],
  &quot;msg&quot;: &quot;&quot;
}
</code></pre>
<p>这里可以看到武器池分为几种类型：</p><ul><li><strong>武器限定池</strong>：<code>poolId</code> 格式为 <code>weponbox_*</code>（如 <code>weponbox_1_0_1</code>），对应当期 UP 武器</li><li><strong>武器常驻池</strong>：<code>poolId</code> 格式为 <code>weaponbox_constant_*</code>（如 <code>weaponbox_constant_2</code>）</li><li><strong>武器重构池</strong>：<code>poolType === &quot;rerun&quot;</code></li></ul><blockquote class="markdown-alert-note"><header>NOTE</header><p>这里有意思的是，武器限定池的 
weapon
 前缀拼写为了 
wepon</p></blockquote>
<h4 id="">获取武器抽卡记录</h4><p>获取具体武器池的抽卡记录链接与角色池抽卡记录链接格式有所不同，武器池的抽卡记录链接将 <code>pool_id</code> 参数代替了 <code>pool_type</code> 参数：</p><pre class="language-text lang-text"><code class="language-text lang-text">https://ef-webview.hypergryph.com/api/record/weapon?lang=zh-cn&amp;seq_id={seqId}&amp;pool_id={poolId}&amp;token={token}&amp;server_id={serverId}
</code></pre>
<p>其返回的数据结构为：</p><pre class="language-json lang-json"><code class="language-json lang-json">{
  &quot;code&quot;: 0,
  &quot;data&quot;: {
    &quot;list&quot;: [
      {
        &quot;poolId&quot;: &quot;weponbox_1_0_1&quot;,
        &quot;poolName&quot;: &quot;熔铸申领&quot;,
        &quot;weaponId&quot;: &quot;wpn_funnel_0010&quot;,
        &quot;weaponName&quot;: &quot;骑士精神&quot;,
        &quot;weaponType&quot;: &quot;E_WeaponType_Wand&quot;,
        &quot;rarity&quot;: 6,
        &quot;isNew&quot;: true,
        &quot;gachaTs&quot;: &quot;1769238381938&quot;,
        &quot;seqId&quot;: &quot;47&quot;
      },
      // ...
    ],
    &quot;hasMore&quot;: true
  },
  &quot;msg&quot;: &quot;&quot;
}
</code></pre>
<p>武器记录的字段与角色记录略有不同：</p><ul><li><code>weaponId</code>：武器 ID</li><li><code>weaponName</code>：武器名称</li><li><code>weaponType</code>：武器类型（<code>E_WeaponType_Sword</code>、<code>E_WeaponType_Wand</code> 等）</li></ul><p>武器池的 <code>seq_id</code> 特性与角色池相同，所有武器子卡池共享同一条 <code>seqId</code> 序列。</p><hr/><p>至此，我们已经基本搞清楚了《明日方舟：终末地》抽卡记录链接的逻辑。</p><h2 id="">主要思路</h2><p>根据上面的介绍，我们能构想出通过一个有效的终末地抽卡记录链接来获取当前可以查询的所有记录的方法：</p><h3 id="">角色池</h3><ol start="1"><li>先通过 API 获取账号的有效角色池，使用返回的 <code>poolType</code> 不带 <code>seq_id</code> 获取该子池最新的抽卡记录。</li><li>后续使用返回数据中最小 <code>seqId</code> 作为游标，遍历该卡池的所有记录，直至后续抽卡数据被服务器清理或着到了该卡池的第一次抽卡 <code>hasMore === false</code> 停止。</li><li>切换为另一个卡池，继续重复第一步的操作即可。</li></ol><h3 id="">武器池</h3><ol start="1"><li>首先调用武器池列表 API 获取用户有数据的武器池。</li><li>遍历每个武器池，使用 <code>pool_id</code> 参数获取记录。</li><li>将 <code>poolType === &quot;rerun&quot;</code> 判定为武器重构池；其他卡池根据 <code>poolId</code> 判断卡池类型：包含 <code>constant</code> 的为常驻池，否则为限定池。</li></ol><h3 id="">代码实现</h3><pre class="language-typescript lang-typescript"><code class="language-typescript lang-typescript">// 获取角色池记录
async function fetchCharacterRecords(token: string, serverId: string) {
  const metaUrl = `https://ef-webview.hypergryph.com/api/record/char/meta?lang=zh-cn&amp;token=${encodeURIComponent(token)}&amp;server_id=${encodeURIComponent(serverId)}`;
  const metaResponse = await fetch(metaUrl);
  if (!metaResponse.ok) throw new Error(&quot;获取角色池元数据失败&quot;);
  const { code, data: meta } = await metaResponse.json();
  if (code !== 0 || !Array.isArray(meta?.tabs)) throw new Error(&quot;无效的角色池元数据&quot;);
  const allRecords = [];

  for (const pool of meta.tabs) {
    if (typeof pool?.poolType !== &quot;string&quot; || !pool.poolType ||
        (pool.poolId != null &amp;&amp; typeof pool.poolId !== &quot;string&quot;)) {
      throw new Error(&quot;无效的角色子池&quot;);
    }
    let cursor: number | null = null;
    let hasMore = true;

    while (hasMore) {
      const seqIdParam = cursor !== null ? `&amp;seq_id=${cursor}` : &quot;&quot;;
      const poolIdParam = pool.poolId ? `&amp;pool_id=${encodeURIComponent(pool.poolId)}` : &quot;&quot;;
      const url = `https://ef-webview.hypergryph.com/api/record/char?lang=zh-cn${seqIdParam}&amp;pool_type=${encodeURIComponent(pool.poolType)}${poolIdParam}&amp;token=${encodeURIComponent(token)}&amp;server_id=${encodeURIComponent(serverId)}`;

      const response = await fetch(url);
      if (!response.ok) throw new Error(&quot;获取记录失败&quot;);
      const { code, data } = await response.json();
      if (code !== 0 || !Array.isArray(data?.list) ||
          typeof data.hasMore !== &quot;boolean&quot; || (!data.list.length &amp;&amp; data.hasMore)) {
        throw new Error(&quot;官方接口返回错误或无效分页&quot;);
      }

      if (data.list.length &gt; 0) {
        allRecords.push(...data.list);
        hasMore = data.hasMore;

        if (hasMore) {
          const seqIds = data.list.map((item: { seqId: string }) =&gt; Number(item.seqId));
          if (seqIds.some((id: number) =&gt; !Number.isSafeInteger(id) || id &lt;= 0)) {
            throw new Error(&quot;序号超出安全整数范围或格式无效&quot;);
          }
          const nextCursor = Math.min(...seqIds);
          if (cursor !== null &amp;&amp; nextCursor &gt;= cursor) throw new Error(&quot;分页游标未前进&quot;);
          cursor = nextCursor;
          await new Promise(resolve =&gt; setTimeout(resolve, 300));
        }
      } else {
        hasMore = false;
      }
    }
  }

  return allRecords;
}

// 获取武器池记录
async function fetchWeaponRecords(token: string, serverId: string) {
  const allRecords = [];
  const poolListUrl = `https://ef-webview.hypergryph.com/api/record/weapon/pool?lang=zh-cn&amp;token=${encodeURIComponent(token)}&amp;server_id=${encodeURIComponent(serverId)}`;
  const poolListResponse = await fetch(poolListUrl);
  if (!poolListResponse.ok) throw new Error(&quot;获取武器池列表失败&quot;);
  const { code, data: weaponPools } = await poolListResponse.json();
  if (code !== 0 || !Array.isArray(weaponPools)) throw new Error(&quot;无效的武器池列表&quot;);

  for (const pool of weaponPools) {
    if (typeof pool.poolId !== &quot;string&quot; || !pool.poolId) throw new Error(&quot;武器池缺少标识&quot;);
    let cursor: number | null = null;
    let hasMore = true;

    while (hasMore) {
      const seqIdParam = cursor !== null ? `&amp;seq_id=${cursor}` : &quot;&quot;;
      const url = `https://ef-webview.hypergryph.com/api/record/weapon?lang=zh-cn${seqIdParam}&amp;pool_id=${encodeURIComponent(pool.poolId)}&amp;token=${encodeURIComponent(token)}&amp;server_id=${encodeURIComponent(serverId)}`;

      const response = await fetch(url);
      if (!response.ok) throw new Error(&quot;获取记录失败&quot;);
      const { code, data } = await response.json();
      if (code !== 0 || !Array.isArray(data?.list) ||
          typeof data.hasMore !== &quot;boolean&quot; || (!data.list.length &amp;&amp; data.hasMore)) {
        throw new Error(&quot;官方接口返回错误或无效分页&quot;);
      }

      if (data.list.length &gt; 0) {
        allRecords.push(...data.list);
        hasMore = data.hasMore;

        if (hasMore) {
          const seqIds = data.list.map((item: { seqId: string }) =&gt; Number(item.seqId));
          if (seqIds.some((id: number) =&gt; !Number.isSafeInteger(id) || id &lt;= 0)) {
            throw new Error(&quot;序号超出安全整数范围或格式无效&quot;);
          }
          const nextCursor = Math.min(...seqIds);
          if (cursor !== null &amp;&amp; nextCursor &gt;= cursor) throw new Error(&quot;分页游标未前进&quot;);
          cursor = nextCursor;
          await new Promise(resolve =&gt; setTimeout(resolve, 300));
        }
      } else {
        hasMore = false;
      }
    }
  }

  return allRecords;
}
</code></pre>
<p>这样我们就实现了获取《明日方舟：终末地》的全部卡池的可查询记录。</p></div><footer><div id="1">1<!-- -->: 自从《明日方舟：终末地》 1.1 版本后，HGWebview.log 日志文件中不再记录寻访记录链接，导致目前社区相关脚本均已失效。但是依然可以在鹰角网络通行证等页面根据登录后的 loginToken -&gt; oauthToken -&gt; uid -&gt; u8Token 通过 js 来一键拼出可用的寻访记录链接，具体方法这里不做展开。</div></footer></div><p style="text-align:right"><a href="https://www.vinking.top/posts/codes/how-to-obtain-gacha-records-in-arknights-endfield#comments">览毕，何不一言？</a></p></div>]]></description><link>https://www.vinking.top/posts/codes/how-to-obtain-gacha-records-in-arknights-endfield</link><guid isPermaLink="true">https://www.vinking.top/posts/codes/how-to-obtain-gacha-records-in-arknights-endfield</guid><dc:creator><![CDATA[Vinking]]></dc:creator><pubDate>Thu, 22 Jan 2026 21:13:15 GMT</pubDate></item><item><title><![CDATA[悲报：要被榨干了！]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn.vinking.top/xlGDHu44ucvTNz2.webp"/><link rel="preload" as="image" href="https://cdn.vinking.top/44SdOfkPkkaF20M.webp"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://www.vinking.top/notes/23">https://www.vinking.top/notes/23</a></blockquote><div><p><strong>【 战斗速报！】</strong> 各位早上好 中午好 晚上好，从前天开始持续遭遇多个脚本的恶意攻击。今天早上到达了小高峰，几小时就受到了 20w+ 的恶意请求，已经刷掉近 10GB 的流量。目前已经将这些 IP 段拉入黑名单：</p><pre class=""><code class="">182.34.4.0/24
180.119.118.0/23
140.75.192.0/24
121.233.200.0/24
61.147.92.0/24
120.238.245.0/24
58.222.45.0/24
218.90.199.0/24
</code></pre>
<p>由于这些请求基本上是每 5 分钟消耗 100 MB 左右的流量，所以并没有触发 CDN 设置的 5 分钟 200MB 封顶限制，加上周末早上在睡大觉，导致本月的 CDN 免费额度已经被消耗了一半以上</p><p>如果后续继续被攻击到 CDN 免费额度耗尽，会将境内的流量全部解析到<a href="https://www.vink.ing/">境外的线路</a>躲一阵子，届时会出现不限于网站打开时间大幅拉长，部分地区无法访问等情况，请各位谅解。</p><p>最后请脚本大佬手下留情</p><p><img alt="已经一滴都没有了" height="256" src="https://cdn.vinking.top/xlGDHu44ucvTNz2.webp" width="256"/></p><h2 id="">后续</h2><p>鉴于恶意攻击仍在零星持续，目前已经着手编写并且启用了自动检测并更新 CDN 黑名单的脚本，这次大概能睡一个好觉了。</p><p><img src="https://cdn.vinking.top/44SdOfkPkkaF20M.webp"/></p></div><p style="text-align:right"><a href="https://www.vinking.top/notes/23#comments">览毕，何不一言？</a></p></div>]]></description><link>https://www.vinking.top/notes/23</link><guid isPermaLink="true">https://www.vinking.top/notes/23</guid><dc:creator><![CDATA[Vinking]]></dc:creator><pubDate>Sat, 05 Jul 2025 08:36:46 GMT</pubDate></item><item><title><![CDATA[Astrionyx - 鸣潮抽卡分析工具及其细节]]></title><description><![CDATA[<p>当前内容无法于 RSS 阅读器中正确渲染，请往：<a href="https://www.vinking.top/posts/codes/astrionyx-wuwa-gacha-analysis">https://www.vinking.top/posts/codes/astrionyx-wuwa-gacha-analysis</a></p>]]></description><link>https://www.vinking.top/posts/codes/astrionyx-wuwa-gacha-analysis</link><guid isPermaLink="true">https://www.vinking.top/posts/codes/astrionyx-wuwa-gacha-analysis</guid><dc:creator><![CDATA[Vinking]]></dc:creator><pubDate>Fri, 30 May 2025 02:44:21 GMT</pubDate></item><item><title><![CDATA[句点]]></title><description><![CDATA[<div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://www.vinking.top/notes/22">https://www.vinking.top/notes/22</a></blockquote><div><p>最近压力太大了。</p><p>趁着 Steam 特卖节看了好多游戏，买了《灾厄堡垒》和《超灵秘法社》，都没能沉下心来好好玩。 也似乎只能玩得下 Gal （大概是不用动脑吧</p><p>草稿箱里有好几篇文章都是只写了开头，有时候半夜突然坐起来打开电脑，光标在空白处闪烁三分钟，又默默合上笔记本。</p><p>或许该接受生活长出点青苔，就像接受有些故事线不必强行推进。</p></div><p style="text-align:right"><a href="https://www.vinking.top/notes/22#comments">览毕，何不一言？</a></p></div>]]></description><link>https://www.vinking.top/notes/22</link><guid isPermaLink="true">https://www.vinking.top/notes/22</guid><dc:creator><![CDATA[Vinking]]></dc:creator><pubDate>Fri, 21 Mar 2025 13:31:25 GMT</pubDate></item><item><title><![CDATA[将苹果的健身记录同步到博客展示]]></title><description><![CDATA[<link rel="preload" as="image" href="https://cdn.vinking.top/g2EvuIOL5NGvcJo.webp"/><link rel="preload" as="image" href="https://cdn.vinking.top/ih67w63XjrzZA8z.webp"/><link rel="preload" as="image" href="https://cdn.vinking.top/eB9pK0g0E8HlDjp.webp"/><link rel="preload" as="image" href="https://cdn.vinking.top/p0hU3X6MFbPc0ql.webp"/><link rel="preload" as="image" href="https://cdn.vinking.top/FKZ376cb9Dn1d9l.webp"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://www.vinking.top/posts/codes/sync-fitness-records-to-blog-status">https://www.vinking.top/posts/codes/sync-fitness-records-to-blog-status</a></blockquote><div><h3 id="">前言</h3><p>在 Shiro 主题中有一个 <a href="https://mx-space.js.org/docs/themes/shiro/extra#%E4%B8%AA%E4%BA%BA%E7%8A%B6%E6%80%81%E5%B1%95%E7%A4%BA">「个人状态展示」</a> 功能，可以展示一些自定义的状态。</p><p>为了充分利用这个功能，想将手机的健身圆环的数据同步展示在个人状态里，效果如下：</p><p><img alt="效果图" height="164" src="https://cdn.vinking.top/g2EvuIOL5NGvcJo.webp" width="379"/></p><p>最初计划通过编写 JavaScript 脚本并借助 Scriptable 应用来实现这个想法。然而经过尝试后发现，Scriptable 并不支持 HealthKit，从而无法获取到健康数据。在苹果设备上，应用需要通过 HealthKit 的 API 才能访问健康数据，而 <a href="https://x.com/scriptableapp/status/1317876804472999936">Scriptable 曾尝试集成 HealthKit但是被苹果商店拒绝</a>。所以只能退而求其次，使用苹果自带的快捷指令来实现这一功能。</p><blockquote><p>2026.3.1 更新，由于快捷指令的限制太多，目前已经更换为自己编写的 App + SideStore自签来实现自动推送</p></blockquote>
<h3 id="">状态展示的设置</h3><p>通过手动设置个人状态展示，可以在控制台中看到页面通过 <code>POST</code> 的方式向 <code>https://{后端服务器}/api/v2/serverless/shiro/status</code> 发送了一个请求：</p><p><img alt="个人状态展示请求" height="412" src="https://cdn.vinking.top/ih67w63XjrzZA8z.webp" width="1331"/></p><p>请求的请求体包含下面这样格式的 JSON：</p><pre class="language-json lang-json"><code class="language-json lang-json">{
    &quot;emoji&quot;: &quot;😴&quot;, // 头像右下角的 Emoji
    &quot;desc&quot;: &quot;消耗0千卡能量 锻炼0分钟 站立0小时&quot;, // 展开后的描述
    &quot;ttl&quot;: 60 // 状态的持续时间，单位为秒
}
</code></pre>
<p>为了完成更新个人状态的操作，需要先获取访问令牌并且构建请求头 <code>Authorization: bearer {token}</code> 用于验证用户权限。</p><p>访问令牌有两种获得方法，第一种是每次设置状态前先发起一次登录请求，在登录请求返回的 JSON 里就能获得一个 <code>xxxxx.xxxxxxxx</code> 格式的访问令牌。</p><p>另一种是通过在后台 <strong>设定</strong> -&gt; <strong>安全</strong> -&gt; <strong>API Token</strong> 里新建一个 <code>xxxxxxxxxxxxxx</code> 格式的访问令牌，这里推荐这种方式。</p><p>拿到访问令牌后，控制台里验证一下：</p><pre class="language-js lang-js"><code class="language-js lang-js">const token = &quot;xxxxxxxxx&quot;;
fetch(&quot;https://server.vinking.top/api/v2/serverless/shiro/status&quot;, {
  method: &quot;POST&quot;,
  headers: {
    &quot;Content-Type&quot;: &quot;application/json&quot;,
    Authorization: token,
  },
  body: JSON.stringify({
    emoji: &quot;✌️&quot;,
    desc: &quot;测试&quot;,
    ttl: 60,
  }),
})
  .then((response) =&gt; {
    if (response.ok) {
      return response.text().then(() =&gt; {
        console.log(`用户状态设置成功，状态码: ${response.status}`);
      });
    }
  })
  .catch((error) =&gt; console.error(&quot;用户状态设置错误:&quot;, error));
</code></pre>
<p>成功设置状态。</p><p><img alt="设置成功" height="132" src="https://cdn.vinking.top/eB9pK0g0E8HlDjp.webp" width="222"/></p><h3 id="">设计快捷指令</h3><p>要实现自动同步健康数据到个人状态展示，需要设计一个能获取健康数据、构建请求并调用 API 的快捷指令。在将流程图转换为快捷指令的过程中有几个需要注意的坑：</p><ol start="1"><li>如果某类数据当天没有记录（如未锻炼），快捷指令会<strong>返回空值而不是 0</strong>。需要提前设置一个初始值 0 以避免后续拼接描述文本出现问题。</li><li>如果想要数据与苹果健康的健身记录面板数据一致，需特别注意「站立 x 小时」（Stand Hours）的统计方式。它反映的是<strong>达标次数</strong>，而非实际站立时长。其计算逻辑为：每个小时内，只要累计站立时间达到或超过 1 分钟，即算作 1 小时达标。例如，08:00 - 08:01 站立，09:30 - 09:31 站立，会被记录为 2 小时达标。相比之下，快捷指令获取的「站立分钟数」（Stand Time）是每次站立的实际分钟数。因此，若要统计「站立 x 小时」，需通过<strong>计数</strong>的方式统计每小时是否达标，而非直接累加站立分钟数。使用指令表达如下：</li></ol><p><img alt="统计达标次数" height="600" src="https://cdn.vinking.top/p0hU3X6MFbPc0ql.webp" width="310"/></p><ol start="3"><li>当接口成功设置状态时，仅返回 <code>204</code> 状态码且不返回任何内容，加上快捷指令本身不支持直接对 HTTP 状态码进行详细判断，因此只能通过判断返回文件的大小是否为 0 来确认设置是否成功。</li></ol><p>由于快捷指令太长了，所以放<a href="https://www.icloud.com/shortcuts/8ea3b57527394aa4aa5ef046c1a2ecf0">快捷指令</a>。</p><p>快捷指令完整流程如下（超级长图警告）：</p><p><img src="https://cdn.vinking.top/FKZ376cb9Dn1d9l.webp"/></p></div><p style="text-align:right"><a href="https://www.vinking.top/posts/codes/sync-fitness-records-to-blog-status#comments">览毕，何不一言？</a></p></div>]]></description><link>https://www.vinking.top/posts/codes/sync-fitness-records-to-blog-status</link><guid isPermaLink="true">https://www.vinking.top/posts/codes/sync-fitness-records-to-blog-status</guid><dc:creator><![CDATA[Vinking]]></dc:creator><pubDate>Mon, 24 Feb 2025 19:13:03 GMT</pubDate></item><item><title><![CDATA[2025 没有新年游戏环节]]></title><description><![CDATA[<link rel="preload" as="image" href="https://image.vinking.top/fJVbVtd3ZPIg5Ps.webp"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://www.vinking.top/posts/confetti/2025-no-new-year-game">https://www.vinking.top/posts/confetti/2025-no-new-year-game</a></blockquote><div><div><p>首先很遗憾的是，<strong>今年没有新年游戏环节</strong>。</p><p>博客上次更新还在两个月前，期间断断续续写了很多草稿，都因为忙于工作上的事情而被迫搁置，其中当然也包括 25 年的新年游戏。目前游戏处于一个半成品中的半成品状态（甚至并没有确定完整的玩法😭），所以在这里公开今年新年游戏里一个比较有意思的想法。</p><h2 id="">游戏灵感💡</h2><p>克里普托斯（Kryptos）是一座位于美国弗吉尼亚州中央情报局总部广场上的雕塑，由 James Sanborn 设计建造，于 1990 年 11 月 3 日完工。该雕塑的名字来源于希腊语“隐藏”，主题为“情报收集”。雕塑上刻有 865 个字符，这些字符被分为四段密文，每段采用不同的加密方法。</p><ul><li>第一段：已被破译，内容为“Between subtle shading and the absence of light lies the nuance of iqlusion”。其中“iqlusion”是故意拼错的“illusion”。</li><li>第二段：已被破译，得到经纬度 38°57′6.5″N 77°8′44″W。</li><li>第三段：已被破译，描述 Howard Carter 打开埃及法老图坦卡蒙陵墓时的情景。</li><li>第四段：至今仍未被完全破译。<a href="#1"><sup>1</sup></a></li></ul><p>其中雕塑第二段使用的是维吉尼亚密码（Vigenère cipher）进行加密的。<a href="#2"><sup>2</sup></a></p><h2 id="">游戏想法</h2><p>本次新年游戏会分为两个阶段</p><ul><li>游戏第一阶段的谜题尚没有想好，玩家解谜完成后将会得到一串字符串并且可以领取第一阶段的红包奖励🧧。</li><li>游戏第二阶段会给出一个维吉尼亚密码表（当然，并不会说明这是一个维吉尼亚密码表）以及一段密文。玩家需要将第一阶段得到的字符串作为的明文倒推出密钥，这个密钥就是游戏的最终结果同时可以领取第二阶段的红包奖励🧧。</li></ul><p>另外附上一个自己写的<a href="https://kryptos-six.vercel.app/">校验小工具</a>，希望有助于你的理解。</p><p>如果你有更好的想法或者建议，请跟我细说细说✌️</p><p>最后：</p><p style="color:#ff57a0" class="text-3xl">恭贺新禧，新年快乐</p><p style="color:#ff57a0;font-weight:bold">愿你的未来纯净明朗，像你此刻的可爱目光。</p><p><img height="100" src="https://image.vinking.top/fJVbVtd3ZPIg5Ps.webp" width="100"/></p><h3 id="">推荐观看</h3><p><a href="https://www.lemmi.no/p/the-unbreakable-kryptos-code">[Video]The Unbreakable Kryptos Code</a></p><h3 id="">参考文献</h3></div><footer><div id="1">1<!-- -->: &quot;Kryptos&quot; <em>Wikipedia</em>. Wikimedia Foundation, January 28, 2025. <a href="https://en.wikipedia.org/wiki/Kryptos">https://en.wikipedia.org/wiki/Kryptos</a>.</div><div id="2">2<!-- -->: &quot;The Unbreakable Kryptos Code.&quot; <em>Lemmi.no</em>. January 28, 2025. <a href="https://www.lemmi.no/p/the-unbreakable-kryptos-code">https://www.lemmi.no/p/the-unbreakable-kryptos-code</a>.</div></footer></div><p style="text-align:right"><a href="https://www.vinking.top/posts/confetti/2025-no-new-year-game#comments">览毕，何不一言？</a></p></div>]]></description><link>https://www.vinking.top/posts/confetti/2025-no-new-year-game</link><guid isPermaLink="true">https://www.vinking.top/posts/confetti/2025-no-new-year-game</guid><dc:creator><![CDATA[Vinking]]></dc:creator><pubDate>Mon, 27 Jan 2025 20:19:45 GMT</pubDate></item><item><title><![CDATA[将 SSL 证书同步到多吉云 CDN]]></title><description><![CDATA[<link rel="preload" as="image" href="https://image.vinking.top/UOO6W0N4.webp"/><link rel="preload" as="image" href="https://cdn.vinking.top/XhDtQ0MKSoUbdaW.webp"/><link rel="preload" as="image" href="https://cdn.vinking.top/lLvnFK30YCWTBd4.webp?imageMogr2/thumbnail/!80p"/><link rel="preload" as="image" href="https://cdn.vinking.top/dZKph0WwJNn3Rws.webp?imageMogr2/thumbnail/!80p"/><div><blockquote>此渲染由 Yohaku API 生成，或存排版之虞，最佳体验请往：<a href="https://www.vinking.top/posts/codes/auto-sync-ssl-certificates-to-dogecloud-cdn">https://www.vinking.top/posts/codes/auto-sync-ssl-certificates-to-dogecloud-cdn</a></blockquote><div><p>前两个月由于腾讯云 CDN 不支持欠费立刻暂停服务换到了多吉云 CDN。</p><p> <img alt="但是！" height="120" src="https://image.vinking.top/UOO6W0N4.webp" width="120"/></p><p>目前 宝塔/1Panel 的免费 SSL 证书都只有 90 天的有效期，虽然可以自动续签，但是续签后还是需要频繁手动更新 CDN 的证书，非常麻烦。</p><p>好在多吉云提供了对应的 API，使得我们可以通过脚本的方式自动实现同步 SSL 证书到多吉云 CDN。</p><p> <img height="272" src="https://cdn.vinking.top/XhDtQ0MKSoUbdaW.webp" width="533"/></p><h2 id="">功能</h2><ul><li>🔑 动态生成多吉云 API 访问令牌</li><li>📤 一键式证书上传与管理</li><li>🌐 多域名智能证书绑定</li><li>🗑️ 旧证书清理功能（可选）</li><li>⏰ 无缝衔接 Let&#x27;s Encrypt 自动续期</li><li>✅ 双平台支持（宝塔/1Panel）</li></ul><p><a href="https://github.com/DearVinking/Cert2DogeCloud">https://github.com/DearVinking/Cert2DogeCloud</a></p><h2 id="">快速开始</h2><ol start="1"><li>获取多吉云 API 密钥：
<ul><li>登录多吉云控制台</li><li>进入「用户中心」→「密钥管理」</li><li>创建新密钥对</li></ul></li><li>确认证书路径：
<ul><li>宝塔面板：<code>/www/server/panel/vhost/ssl/域名目录/</code></li><li>1Panel 可跳过</li></ul></li></ol><h2 id="">配置指南</h2><p>编辑脚本中的以下参数：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash"># 多吉云 AccessKey 和 SecretK
ACCESS_KEY=&quot;your_access_key_here&quot;   # 替换为你的AccessKey
SECRET_KEY=&quot;your_secret_key_here&quot;  # 替换为你的SecretKey

# 证书路径配置
FULLCHAIN_PATH=&quot;/path/to/fullchain.pem&quot;  # 全链证书路径
PRIVKEY_PATH=&quot;/path/to/privkey.pem&quot;      # 私钥路径

# 域名配置
DOMAINS=(&quot;example.com&quot; &quot;cdn.example.com&quot; &quot;www.example.com&quot;)  # 需要绑定的域名列表

# 旧证书处理策略
DELETE_OLD_CERT=false  # true=自动删除旧证书 | false=保留历史证书
</code></pre>
<h2 id="">部署指南</h2><h3 id="1panel">1Panel</h3><ol start="1"><li>进入证书管理界面</li><li><p>创建/编辑证书：</p><ul><li>启用「自动续签」</li><li>启用「推送证书到本地目录」</li><li>选择目录</li><li>启用「申请后执行脚本」</li><li>粘贴本脚本内容</li><li>修改证书路径为：</li></ul><pre class="language-sh lang-sh"><code class="language-sh lang-sh"># 证书路径
FULLCHAIN_PATH=&quot;./fullchain.pem&quot;
PRIVKEY_PATH=&quot;./privkey.pem&quot;
</code></pre>
</li></ol><p><img alt="1Panel 证书设置" height="511" src="https://cdn.vinking.top/lLvnFK30YCWTBd4.webp?imageMogr2/thumbnail/!80p" width="759"/></p><h3 id="">宝塔</h3><ol start="1"><li>创建定时任务：
<ul><li>任务类型：Shell 脚本</li><li>任务名称：随意</li><li>执行周期：每月 1 号 01:30 执行一次</li><li>执行用户：root</li><li>脚本内容：粘贴本脚本内容</li></ul></li></ol><p><img alt="宝塔定时任务" height="473" src="https://cdn.vinking.top/dZKph0WwJNn3Rws.webp?imageMogr2/thumbnail/!80p" width="788"/></p><ol start="2"><li>配合自动续签 Let’s Encrypt 证书定时任务 <code>/www/server/panel/pyenv/bin/python /www/server/panel/class/acme_v2.py –renew=1</code> 理论上可以实现放养多吉云 CDN 的证书。</li></ol><p>保存后执行一次任务，如果显示下面信息即已经完成证书的同步：</p><pre class=""><code class="">证书上传成功！
证书ID：12345
证书已成功绑定到 www.vinking.top
证书已成功绑定到 vinking.top
证书ID 12344 删除成功。
----------------------------------------------------------------------------
★[2024-12-26 16:01:29] Successful
----------------------------------------------------------------------------
</code></pre>
<h2 id="">代码思路</h2><h3 id="---accesstoken">获取密钥 &amp; 生成 <code>AccessToken</code></h3><p>多吉云的 API 具有验证机制，使用前需要在控制台的 <a href="https://console.dogecloud.com/user/keys">密钥管理</a> 获取 <code>AccessKey</code> 以及 <code>SecretKey</code>，然后根据 <code>AccessKey</code> 和 <code>SecretKey</code>生成 <code>AccessToken</code> 。</p><p> <code>AccessToken</code> 的生成过程是将请求地址和请求内容拼接后，使用 <code>SecretKey</code> 进行 <code>HMAC-SHA1</code> 加密，然后将得到的加密值与 <code>AccessKey</code> 用冒号连接起来。具体的生成算法可以参考文档 <a href="https://docs.dogecloud.com/cdn/api-access-token?id=%e7%ae%97%e6%b3%95">验证机制</a> 。</p><pre class="language-sh lang-sh"><code class="language-sh lang-sh"># 多吉云 AccessKey 和 SecretKey
ACCESS_KEY=&quot;xxxx&quot;
SECRET_KEY=&quot;xxxxxx&quot;

function generateAccessToken() {
    local apiPath=&quot;$1&quot;
    local body=&quot;$2&quot;
    local signStr=$(echo -e &quot;${apiPath}\n${body}&quot;)
    local sign=$(echo -n &quot;$signStr&quot; | openssl dgst -sha1 -hmac &quot;$SECRET_KEY&quot; | awk &#x27;{print $NF}&#x27;)
    local accessToken=&quot;$ACCESS_KEY:$sign&quot;

    echo &quot;$accessToken&quot;
}
 </code></pre>
<p>生成了 <code>AccessToken</code> 后，只需要在请求头中加上 <code>Authorization: TOKEN &lt;AccessToken&gt;</code> 即可通过验证。</p><h3 id="--">找到宝塔的域名证书 &amp; 上传证书</h3><p>在 <code>/www/server/panel/vhost/ssl/</code> 目录下，可以找到宝塔所有以域名命名的文件夹，文件夹内包含这个域名对应的<strong>完整证书链</strong> <code>fullchain.pem</code> 以及<strong>私钥</strong> <code>privkey.pem</code>。</p><p>这里推荐给域名及其所有子域名使用同一个泛域名证书，可以实现一次性将域名及其所有子域名完成证书同步。</p><p>以域名 <code>vinking.top</code> 为例，完整证书链文件和私钥文件的目录如下：</p><pre class=""><code class="">FULLCHAIN_PATH=&quot;/www/server/panel/vhost/ssl/vinking.top/fullchain.pem&quot;
PRIVKEY_PATH=&quot;/www/server/panel/vhost/ssl/vinking.top/privkey.pem&quot;
</code></pre>
<p>获取到域名证书后，需要通过 POST 的方式向 <code>https://api.dogecloud.com/cdn/cert/upload.json</code> 提交证书内容。上传成功后需要获取证书 ID，以便将刚才上传的证书绑定到域名。</p><p>参考文档 <a href="https://docs.dogecloud.com/cdn/api-cert-upload">上传证书</a> 、 <a href="https://docs.dogecloud.com/cdn/api-cert-bind">绑定证书</a> 。</p><pre class="language-sh lang-sh"><code class="language-sh lang-sh"># 宝塔面板 Let&#x27;s Encrypt 证书路径
FULLCHAIN_PATH=&quot;/www/server/panel/vhost/ssl/vinking.top/fullchain.pem&quot;
PRIVKEY_PATH=&quot;/www/server/panel/vhost/ssl/vinking.top/privkey.pem&quot;

# 证书备注名
CURRENT_DATE=$(date +&quot;%y/%m/%d&quot;)
NOTE=&quot;Certificate $CURRENT_DATE&quot;

# 需要绑定的域名列表
DOMAINS=(&quot;xxxxx.com&quot; &quot;cdn.xxxxx.com&quot; &quot;www.xxxxx.com&quot;)

# 上传证书到多吉云
function uploadCert() {
    local note=&quot;$1&quot;
    local certFile=&quot;$2&quot;
    local privateKeyFile=&quot;$3&quot;
    local certContent=$(&lt;&quot;$certFile&quot;)
    local privateKeyContent=$(&lt;&quot;$privateKeyFile&quot;)
    local encodedCert=$(echo &quot;$certContent&quot; | jq -sRr @uri)
    local encodedPrivateKey=$(echo &quot;$privateKeyContent&quot; | jq -sRr @uri)
    local body=&quot;note=$note&amp;cert=$encodedCert&amp;private=$encodedPrivateKey&quot;
    local accessToken=$(generateAccessToken &quot;/cdn/cert/upload.json&quot; &quot;$body&quot;)
    local response=$(curl -s -X POST &quot;https://api.dogecloud.com/cdn/cert/upload.json&quot;  \
         -H &quot;Authorization: TOKEN $accessToken&quot; \
         -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \
         --data &quot;$body&quot;)

    local code=$(echo &quot;$response&quot; | jq -r &#x27;.code&#x27;)

    if [ &quot;$code&quot; -eq 200 ]; then
        echo &quot;证书上传成功！&quot;
        local certId=$(echo &quot;$response&quot; | jq -r &#x27;.data.id&#x27;)
        echo &quot;证书 ID：$certId&quot;
        bindCert &quot;$certId&quot;
    else
        local errMsg=$(echo &quot;$response&quot; | jq -r &#x27;.msg&#x27;)
        echo &quot;证书上传失败，错误代码：$code，错误信息：$errMsg&quot;
    fi
}

# 绑定证书
function bindCert() {
    local certId=&quot;$1&quot;
    local responses=()

    for domain in &quot;${DOMAINS[@]}&quot;; do
        (
            local body=&quot;id=$certId&amp;domain=$domain&quot;
            local accessToken=$(generateAccessToken &quot;/cdn/cert/bind.json&quot; &quot;$body&quot;)
            local response=$(curl -s -X POST &quot;https://api.dogecloud.com/cdn/cert/bind.json&quot;  \
                 -H &quot;Authorization: TOKEN $accessToken&quot; \
                 -H &quot;Content-Type: application/x-www-form-urlencoded&quot; \
                 --data &quot;$body&quot;)
            local code=$(echo &quot;$response&quot; | jq -r &#x27;.code&#x27;)

            if [ &quot;$code&quot; -eq 200 ]; then
                echo &quot;证书已成功绑定到 $domain&quot;
            else
                local errMsg=$(echo &quot;$response&quot; | jq -r &#x27;.msg&#x27;)
                echo &quot;绑定证书到 $domain 失败，错误代码：$code，错误信息：$errMsg&quot;
            fi
        ) &amp;
    done

    wait
}
</code></pre></div><p style="text-align:right"><a href="https://www.vinking.top/posts/codes/auto-sync-ssl-certificates-to-dogecloud-cdn#comments">览毕，何不一言？</a></p></div>]]></description><link>https://www.vinking.top/posts/codes/auto-sync-ssl-certificates-to-dogecloud-cdn</link><guid isPermaLink="true">https://www.vinking.top/posts/codes/auto-sync-ssl-certificates-to-dogecloud-cdn</guid><dc:creator><![CDATA[Vinking]]></dc:creator><pubDate>Tue, 19 Nov 2024 10:32:56 GMT</pubDate></item></channel></rss>