<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/rss/atom-styles.xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>在 23 点</title>
  <subtitle>在 23 点是Monolith 的个人博客，主要用来分享各种奇怪的新内容。</subtitle>
  <link href="https://23h.at//atom.xml" rel="self" type="application/atom+xml"/>
  <link href="https://23h.at/" rel="alternate" type="text/html"/>
  <updated>2026-01-06T09:00:06.751Z</updated>
  <language>zh</language>
  <id>https://23h.at//</id>
  <author>
    <name>Monolith</name>
    <uri>https://23h.at/</uri>
  </author>
  <generator uri="https://github.com/Dnzzk2/Litos" version="5.0">Astro Litos Theme</generator>
  <rights>Copyright © 2026 Monolith</rights>
  
  <entry>
    <title>Astro Fuwari主题添加评论功能</title>
    <link href="https://23h.at//posts/add-comment-for-fuwari" rel="alternate" type="text/html"/>
    <id>https://23h.at//posts/add-comment-for-fuwari</id>
    <updated>2024-12-30T00:00:00.000Z</updated>
    <published>2024-12-30T00:00:00.000Z</published>
    <author>
      <name>Monolith</name>
    </author>
    <summary type="text">为缺失评论功能的 Fuwari 主题添加评论功能，同样也适用于那些使用了 SWUP 的 Astro 主题。</summary>
    <content type="html"><![CDATA[<img src="https://23h.at/_astro/cover.UReElM_7_15Y3DX.webp" alt="Astro Fuwari主题添加评论功能" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>总述</h2>
<p>Astro 作为一个相对 Hexo 和 Hugo 这种框架，整体技术比较新，且上手难度比较大的框架而言，目前能用的优秀主题并不是很多。
而 Fuwari 是目前做得比较好的一个。</p>
<p>而关键的评论功能，虽然作者将其列入了自己的 To-Do 列表，但是可能也是考虑到诸多原因，而没有在其中添加相应代码。
这篇文章则针对这个部分进行添加。</p>
<hr />
<h2>现有问题</h2>
<p>Fuwari 作为一个使用比较多动效的网站而言，也是支持的 View Transition 这个 API。</p>
<p>ViewTransition 这个 API 可以在页面间移动的时候，仅仅只更新 DOM，而无需进行页面的重新加载。
同时，这个 API 还支持制作多个页面间过渡的动画，让 MPA 程序实现类似原生系统的切换效果，仿佛是在使用 SPA 的应用一样。
Astro 这个框架，则原生的支持了这个功能，开发者可以在<strong>无需引入任何第三方库或 JS</strong>的情况下，实现页面切换。
仅仅使用 <code>&lt;ViewTransition/&gt;</code> 这个组件即可实现整个页面的替换。<a href="https://docs.astro.build/en/tutorials/add-view-transitions/#update-scripts" rel="noopener noreferrer" target="_blank">文档</a></p>
<h3>ViewTransition 带来的问题</h3>
<p>这个 View Transition好虽好，但是也有一些别的问题，而其中最严重的一点是：<strong>JS 只能执行一次</strong>。
由于页面并没有真的重新加载，而仅仅是替换，所以切换到新页面的 JS 内容将不会被执行.
而与此同时失效的还有<code>domcontentloaded</code>这个事件。</p>
<p>对此， Astro 提供了一种<a href="https://docs.astro.build/en/tutorials/add-view-transitions/#update-scripts" rel="noopener noreferrer" target="_blank">处理方法</a>，或者说 WorkAround， 即<code>page-load</code>事件：</p>
<pre><code>document.addEventListener('astro:page-load', () =&gt; {
  document.querySelector('.hamburger').addEventListener('click', () =&gt; {
    document.querySelector('.nav-links').classList.toggle('expanded');
  });
});
</code></pre>
<p>这个事件的行为逻辑就是个简单的回调函数，在 Astro 完成 View Transition 后主动进行调取。
而且，这段代码必须在<strong>页面第一次加载</strong>就要被调用，在之后的 Transition 则与其他的 JS 内容一样，无法进行执行。</p>
<h3>评论系统的设计</h3>
<p>现在几乎所有的评论系统都有着一样的行为逻辑。即页面找一个对应元素，然后执行 JS。
比如 Artalk，则需要使用下面的初始化代码：</p>
<pre><code>Artalk.init({
  el:        document.querySelector('#Comments'), // 挂载的 DOM 元素
  pageKey:   '/post/1',                           // 固定链接
  pageTitle: '关于引入 Artalk 的这档子事',           // 页面标题
  server:    'http://artalk.example.com:8080',    // 后端地址
  site:      'Artalk 的博客',                      // 站点名
})
</code></pre>
<p>因为只有文章页面有 ID 为 <code>Comments</code> 的元素，所以这段代码必须在文章页面执行。</p>
<h3>Astro 的方法</h3>
<p>所以，Astro 的方法，其实很简单，就是单纯的在页面添加一个事件监听就解决了。</p>
<h2>Fuwari 不一样。</h2>
<p>但是，Fuwari其并没有使用Astro 自带的 <code>ViewTransition</code> 接口，而是使用了<strong>SWUP</strong>这个库来进行的实现。</p>
<div>
<a href="https://github.com/swup/swup" target="_blank" rel="noopener noreferrer">
  <div>
    <div>
      <div>
        <div></div>
        <div>swup</div>
      </div>
      <div>/</div>
      <div>swup</div>
    </div>
    <div>
      
        
      
    </div>
  </div>
  <div>Loading...</div>
  <div>
    <div>
      
        
      
      <span>0</span>
    </div>
    <div>
      
        
      
      <span>0</span>
    </div>
    <div>
      
        
      
      <span>-</span>
    </div>
  </div>
</a>

              </div>
<p>相比于 Astro 的 View Transition 方法而言，单纯的加监听没有解决任何问题。</p>
<p>SWUP 的官方在其对 Astro 的集成中，提供了一个名为 <code>reloadScript</code> 的参数。
尽管这个参数可能会带来很多别的问题，比如内存泄露这种，但是不管怎么将应该问题算解决了？对吧？</p>
<p><strong>并没有！！！</strong></p>
<p>即便使用这个函数，初始化代码仍然没有被执行 （原因我也没找到）。</p>
<h3>SWUP‘s Workaround</h3>
<p>与 Astro 一样，SWUP 提供了一个绕过方法，Hooks。</p>
<p>与 React 的 Hooks 不同，SWUP 的 Hooks 的本质就是事件监听……</p>
<p>所以我们可以用类似的方法来实现，比如使用以下的方法来搞：</p>
<pre><code> window.swup.hooks.on('page:view', () =&gt; {
            initArtalk()
        });
</code></pre>
<h3>但是 Astro 他……</h3>
<p>如果这是个标准的网站，我觉得这个问题可能已经得到的妥善的解决。
但是这里有 Astro。在页面初始化的时候，Astro 很有可能还并没有将 JS 加载到本地，贸然调用会出现导致 SWUP 根本不存在。
所以，需要按照SWUP 的存在与否来决定是否初始化评论组件。
也就是监听：<code>swup:enable</code> 这个事件。</p>
<pre><code>document.addEventListener("swup:enable", setup)
</code></pre>
<p>与此同时，我们应该同时判断，Comments 元素的存在与否来避免在错误页面初始化评论区，
且应该在 SWUP 没有加载的时候将对应的函数放到 SWUP 的钩子中。</p>
<h2>TL;DR 的大结局</h2>
<p>所以一切的一切，只需要在 Layout.Astro添加下面的内容即可。</p>
<p>（这个文件永远会在页面第一次被加载时执行）</p>
<pre><code>&lt;script&gt;
    import Artalk from "artalk";
    import {siteConfig} from "../config";

    function initArtalk() {
        let artalkDataNode = document.getElementById("artalk-data");
        if (artalkDataNode) {
            Artalk.init({
                el: document.querySelector('#artalk'), // 挂载的 DOM 元素
                pageKey: artalkDataNode.getAttribute("artalk-slug"),   // 固定链接
                pageTitle: artalkDataNode.getAttribute("artalk-title"), // 页面标题
                server: siteConfig.comments.backendURL,   // 后端地址
                site: siteConfig.title,                      // 站点名
            })
        }
    }

    function setup() {
        initArtalk()
        window.swup.hooks.on('page:view', () =&gt; {
            initArtalk()
        });

    }
    if (window.swup) {
        setup()
    } else {
        document.addEventListener("swup:enable", setup)
    }
&lt;/script&gt;
</code></pre>]]></content>
    <category term="Astro" />
    <category term="主题" />
    <category term="Artalk" />
  </entry>
  <entry>
    <title>Astro整合自部署 MeiliSearch</title>
    <link href="https://23h.at//posts/astro-melisearch" rel="alternate" type="text/html"/>
    <id>https://23h.at//posts/astro-melisearch</id>
    <updated>2024-12-30T00:00:00.000Z</updated>
    <published>2024-12-30T00:00:00.000Z</published>
    <author>
      <name>Monolith</name>
    </author>
    <summary type="text">由于 PageFind 的依赖相对比较臃肿且搜索能力比较一般，故将 Astro 的 PageFind 退役，将 MeiliSearch 整合。</summary>
    <content type="html"><![CDATA[<img src="https://23h.at/_astro/cover.BGF0f4yY_ZutSKJ.webp" alt="Astro整合自部署 MeiliSearch" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>序言</h2>
<p>我之前使用的 Fuwari 主题，默认是内置了 PageFind 来作为文章搜索工具的。
尽管 PageFind 并不是一个很差的解决方案，但是作为一个纯客户端搜索，还是带来了一些小问题的：</p>
<ul>
<li>中文的分词做得相对比较差</li>
<li>依赖于一个索引文件，而索引文件会随着文章大小增大而增大。
在这种情况下，我决定使用一款带有服务端的搜素服务来解决问题。</li>
</ul>
<h2>选择搜索服务</h2>
<p>目前主流的搜索服务中，有牛逼但是非常重量级的 ElasticSearch，也有 Algolia 这这种付费 SaaS 选手。</p>
<p>而开源、省资源且可以自己建立的情况下，则只剩下了 TypeSense 和 MeiliSearch 这两位。</p>
<p>TypeSense相对而言对复杂搜索做的比较好，但是对于博客的搜素，TypeSense 更高的硬件要求则显得有些多此一举。</p>
<p>相对于 TypeSense，MeiliSearch尽管缺失了很多高级功能，比如 <code>StartWith</code> 这些，但是其有特殊的分词优化，对于中文博客搜索还是绰绰有余的。</p>
<p>因为，我选择使用 MeiliSearch 来进行搜索服务的实现。</p>
<h2>创建 MeiliSearch 服务</h2>
<h3>安装 MeiliSearch</h3>
<p>MeiliSearch 不支持多节点，自然也不需要像 ElasticSearch 那样复杂的安装，一个简单的 DockerCompose 足够拉起来整个项目。</p>
<pre><code>services:
    meilisearch:
        command: /bin/meilisearch
        environment:
            MEILI_CONFIG_FILE_PATH: /config/config.toml
            MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
        image: getmeili/meilisearch:v1.11.3
        ports:
            -  7700:7700
        restart: always
        volumes:
            - ./config:/config
            - ./data:/meili_data
            - ./dumps:/dumps
            - ./snapshots:/snapshots
</code></pre>
<p>这里唯一需要注意的是，设置一个比较复杂的管理员秘钥，越长越好那种，之后所有的操作都是围绕这个秘钥进行的，这个秘钥也需要保管好。</p>
<p>之后所有的请求，均需要在请求头中添加 <code>Authorization</code> 字段，内容则需要以<code>Bareer &lt;YOUR_KEY&gt;</code>这样的形式书写。</p>
<h3>创建索引</h3>
<p>创建 MeiliSearch 索引的唯一方法就是通过 API。按照官方的文档，使用下面的方法创建即可:</p>
<pre><code>curl \
  -X POST 'http://localhost:7700/indexes' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "uid": "articles",
    "primaryKey": "id"
  }'
</code></pre>
<p>这里一共有两个字段需要设置，一个是<code>uid</code>，这个字段就是索引的名字。另一个字段是<code>primaryKey</code>，是文档的唯一标识符。</p>
<p>而与 ElasticSearch 等服务不同的是，MeiliSearch 的搜索是需要额外<strong>手动提供文档 ID 的</strong>。这一点在之后部署的时候需要注意。</p>
<h3>创建搜索专用 Key</h3>
<p>由于 MasterKey 有着极高的权限。我们有必要为前台搜索提供专门的 Key 并限制具体访问的 API。
MeiliSearch 可以通过一下的 API 来创建 Key：</p>
<pre><code>curl \
  -X POST 'http://localhost:7700/keys' \
  -H 'Authorization: Bearer MASTER_KEY' \
  -H 'Content-Type: application/json' \
  --data-binary '{
    "description": "The API key for search the articles",
    "actions": ["search"],
    "indexes": ["articles"],
    "expiresAt": "2042-04-02T00:42:42Z"
  }'
</code></pre>
<p>这里同样有两个字段需要注意：</p>
<ul>
<li><code>actions</code>：这个字段主要限制 key 的权限范围，具体可用参数可以 <a href="https://www.meilisearch.com/docs/reference/api/keys#create-a-key" rel="noopener noreferrer" target="_blank">参考这里</a>。</li>
<li><code>indexes</code>：这个参数则主要限制可以使用的索引名称，里面是有索引 UID 组成的数组。这里我们仅仅设置我们之前设置的索引 ID 即可。</li>
</ul>
<p>按照官方提供的例子，下面的字段<code>key</code>就是我们创建好的 key</p>
<pre><code>{
  "name": null,
  "description": "Manage documents: Products/Reviews API key",
  "key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
  "uid": "6062abda-a5aa-4414-ac91-ecd7944c0f8d",
  "actions": [
    "documents.add"
  ],
  "indexes": [
    "products"
  ],
  "expiresAt": "2021-11-13T00:00:00Z",
  "createdAt": "2021-11-12T10:00:00Z",
  "updatedAt": "2021-11-12T10:00:00Z"
}

</code></pre>
<h2>录入文档</h2>
<p>由于 Astro 是 SSG 框架，所以索引的录入只能放在编译阶段。
目前其实有两种思路来录入文档，一种是直接读取 Markdown 文件，另外一种是读取编译好的页面。</p>
<p>根据我的实验，建议在页面编译完成后再录入文档，其中主要原因在于：</p>
<ul>
<li>Astro 的页面地址在编译前可能并不是确定的。</li>
<li>部分文档类型如 MDX 可能会引入很多没用的东西。</li>
</ul>
<p>按照逻辑，我们应该可以使用下面的代码来读取并生成页面的元数据信息：</p>
<pre><code>async function getPosts() {
    const postsDirectory = path.join(process.cwd(), 'dist', 'posts')
    const folders = await readdir(postsDirectory)

    const posts = await Promise.all(
        folders.map(async folderName =&gt; {
            const filePath = path.join(postsDirectory, folderName, 'index.html')
            const source = await readFile(filePath, 'utf-8')

            // Parse HTML content using JSDOM
            const dom = new JSDOM(source)
            const document = dom.window.document

            // Get the main content
            const contentElement = document.querySelector('.markdown-content')
            const content = contentElement ? contentElement.textContent.trim() : ''

            // Get metadata from JSON-LD
            const scriptElement = document.querySelector(
                'script[type="application/ld+json"]',
            )
            const metadata = scriptElement
                ? JSON.parse(scriptElement.textContent)
                : {}

            return {
                id: nanoid(),
                content,
                title: metadata.headline || '',
                description: metadata.description || '',
                keywords: metadata.keywords || [],
                author: metadata.author?.name || '',
                datePublished: metadata.datePublished || '',
                language: metadata.inLanguage || 'en',
                url: `${siteUrl}/posts/${folderName}`,
            }
        }),
    )

    return posts
}
</code></pre>
<p>上边是本站正在使用的部分，简单讲解下就是以下的思路：</p>
<ol>
<li>读取生成目录下的所有文章的 HTML。</li>
<li>读取页面中的元数据部分。（部分主题，如这里，可能已经将元数据做成了 json 格式）</li>
<li>去除文章中多余的 HTML 代码部分。</li>
<li><strong>生成 ID</strong>，整理并返回页面的元数据。</li>
</ol>
<blockquote><p>[!info] 提醒
由于截止目前，MeiliSearch 还不支持嵌套值的搜索，所以建议将页面的可搜索字段拉平。</p></blockquote>
<p>在获取到文章后，则可以将所有的文章提交到 MeiliSearch 服务了。</p>
<pre><code>const client = new MeiliSearch({host: MEILISEARCH_HOST, apiKey: YOUR_MASTERKEY})

const posts = await getPosts()
const indexName = 'articles'

// Get the index instance
const index = await client.getIndex(indexName)


await index.deleteAllDocuments()

// Add documents to the index
await index.addDocuments(posts);
</code></pre>
<h2>前端实现搜索功能</h2>
<p>与 ElasticSearch 不同，MeiliSearch 支持将API直接暴露在前端，那么，自然就可以直接与 MeiliSearch 的搜索接口通讯。</p>
<p>可以使用下面的代码与自己的 MeiliSearch 对接：</p>
<pre><code>async function useMeiliSearch (keyword: string) {
    if (!keyword){
        return {
            hits:[]
        }
    }
    return (await fetch(`${import.meta.env.PUBLIC_MEILISEARCH_HOST}/indexes/articles/search`, {
        method: 'POST',
        headers: {
            "content-type": "application/json",
            'authorization': `Bearer ${import.meta.env.PUBLIC_MEILISEARCH_SEARCH_KEY}`,
        },
        body: JSON.stringify({
            'q': keyword,
            "attributesToHighlight":["content","title","description"], //这里填写需要高亮的字段
            "attributesToCrop":["content"], //这里填写需要剪裁的字段。建议将文章剪裁来实现高亮。
            "attributesToRetrieve":["url"],
            "highlightPostTag":"&lt;/mark&gt;", //这两个是高亮使用的 HTML TAG
            "highlightPreTag":"&lt;mark&gt;"
        })
    })).json()
}
</code></pre>
<div><div><div></div><div>提示</div></div><div><p>如果你想在客户端（浏览器）获取到编译时的部分环境变量，可以在变量前加入 <code>PUBLIC_</code>这个前缀来解决。</p></div></div>
<p>至于与主题的集成，则由于每个人的主题不同，有着不同的集成方法。</p>
<p>如本站，则是简单的替换了原主题使用的 PageFind方法实现的。</p>
<p>再次就不一而足了。</p>
<h2>关于安全性</h2>
<p>集成 MeiliSearch 的操作其实本身并不复杂。但是其周边生态其实有不少要考虑的。</p>
<p>比如如果使用第三方 CI 或 Serverless 部署，可能需要专门为其提供秘钥和访问方法来限制普通用户的访问。</p>
<p>对于这种情况，可以使用 <a href="https://developers.cloudflare.com/cloudflare-one/identity/service-tokens/" rel="noopener noreferrer" target="_blank">Cloudflare Access 的 Service Auth</a> 来避免公网直接访问非搜索接口的问题。</p>
<p>而对于搜索接口本身，同样建议反向代理指定 API 并增加速率限制等方式防止被刷。</p>]]></content>
    <category term="Astro" />
    <category term="MeiliSearch" />
  </entry>
  <entry>
    <title>反向代理下 Minio 无法 HeadObject 问题检查</title>
    <link href="https://23h.at//posts/minio-head-obeject" rel="alternate" type="text/html"/>
    <id>https://23h.at//posts/minio-head-obeject</id>
    <updated>2024-12-30T00:00:00.000Z</updated>
    <published>2024-12-30T00:00:00.000Z</published>
    <author>
      <name>Monolith</name>
    </author>
    <summary type="text">反向代理下 Minio 无法 HeadObject 问题检查</summary>
    <content type="html"><![CDATA[<img src="https://23h.at/_astro/cover.CqoVE93__Y6wQo.webp" alt="反向代理下 Minio 无法 HeadObject 问题检查" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>问题描述</h2>
<p>这两天自己部署了一台 MinIO 的服务器，用来提供 S3 的存储相关 API。 （这主要得益于 Alist 的相关性能并不算优秀才用的 Minio。
但是，在于 Directus 进行整合的时候，出现了内部服服务错误。</p>
<p>于此同时，在使用 RClone 进行文件上传的时候，也出现了 Head Object 时，返回 403 的错误。</p>
<h2>Head Object 是什么</h2>
<p>Head Object 是 S3 的 API 之一，用于获取对象的元数据，而不需要实际下载对象的内容。</p>
<p>相比于 Get Object 而言，Head Object 的请求头中不包含对象的内容，仅仅只通过一个 HTTP <code>HEAD</code> 请求，就能拿到部分文件内容和基础数据。</p>
<p>与 Get Object 相比，传输量更少，很多时候会被一些 S3 服务用来先期判断文件是否存在，或者上传之后校验文件使用。</p>
<h2>问题排查</h2>
<p>一开始曾经怀疑是 Minio 的权限配置存在错误。但是一番调查后发现，即便使用最高权限账户 ConsoleAdmin，且 ACL 全部配置为允许，依然会出现 403 的错误。</p>
<p>网上一番搜索过后，所有的内容都在说 Cloudflare 的缓存策略存在问题。</p>
<p>但是，我的服务中并没有使用任何 Cloudflare 对 S3 服务进行代理。（法律上将，很多人都提到，Cloudflare 代理对象存储是违反 TOS 的）。</p>
<p>同时，我又将服务换成了 Garage 来进行处理 S3 内容，但是问题却消失了……</p>
<p>但这并不是最优解，主要原因在于 Garage 的 Bug 也太多了。</p>
<h2>问题解决</h2>
<p>考虑到问题仅仅出现在 HeadObject，而网上并没有针对 Minio 的反馈，那么问题应该就是出现在反向代理上。</p>
<p>在百般折磨后，终于在 1panel 的官方 GitHub 中找到了相关的 Issue。</p>
<div><a href="https://github.com/1Panel-dev/1Panel/issues/3924" target="_blank" rel="noopener noreferrer"><div><div><div><img src="https://github.com/fluidicon.png" alt="" class="h-4 w-4 shrink-0 brightness-75 contrast-110" /><span>github.com</span></div><div>
          [QUESTION]OpenResty反代MinIO的API端口无法进行上传等操作 · Issue #3924 · 1Panel-dev/1Panel
        </div><p>
          请描述您的问题 使用S3 Browser以及Halo的S3服务时，MinIO的API端口反代的域名无法进行上传等操作，但是源站IP+端口的方式一切正常。 1、服务器版本 发行版本 ubuntu-22.04 内核版本 5.15.0-76-generic 系统类型 x86_64 2、环境版本 OpenResty版本 1.21.4.3-0-focal MinIO版本 2023-01-05
        </p><div><span>https://github.com/1Panel-dev/1Panel/issues/3924</span></div></div><div><img src="https://opengraph.githubassets.com/c3c9bcc3c8aac6dab8cce8b4ac9df61906c5e4db15d5b32394831b1677fe7593/1Panel-dev/1Panel/issues/3924" alt="Preview" class="h-full w-full object-cover brightness-75 contrast-110" /></div></div></a></div>
<p>里面提供了一个解决方法：</p>
<pre><code>proxy_cache_convert_head off;
</code></pre>
<p>就这么简单的一行语句即可解决问题。</p>
<h2>了解 proxy_cache_convert_head</h2>
<p>调查后发现，1panel 默认配置的 OpenResty 中，做了于 Cloudflare 一样的事情：</p>
<p><strong>将 HEAD 请求转换为 GET 请求并进行缓存</strong></p>
<p>当然我们不是说这个策略是错误的，只是在 S3 的场景下，这个策略是有问题的。</p>
<p>这与 S3 的签名设计有关。</p>
<p>为了防止非法请求, S3 相关的请求，均需要携带内容签名。</p>
<p>签名的一个大概流程可以参考下图：</p>
<img src="https://23h.at/_astro/sigV4-using-query-params.BrzLe1xA_Z2pGMXx.webp" alt="S3 签名流程" />
<p>从图中可以看到，签名中需要同时包含 HTTP 的请求方法，请求路径，请求头，参数等等。</p>
<p>这就导致一个问题，Nginx 提供的 <code>proxy_cache_convert_head</code> 策略，会将 HEAD 请求转换为 GET 请求，导致签名无法匹配。</p>
<p>最后 Minio 只能返回一个 Access Denied 的错误。</p>]]></content>
    <category term="Minio" />
    <category term="反向代理" />
    <category term="Nginx" />
  </entry>
  <entry>
    <title>QUIC - 记一次 Hetzner 防火墙 与 Cloudflare ZeroTrust 的 Debug 之旅</title>
    <link href="https://23h.at//posts/quic-udp-cloudflare-zt" rel="alternate" type="text/html"/>
    <id>https://23h.at//posts/quic-udp-cloudflare-zt</id>
    <updated>2024-12-01T00:00:00.000Z</updated>
    <published>2024-12-01T00:00:00.000Z</published>
    <author>
      <name>Monolith</name>
    </author>
    <summary type="text">本文记录了由于 QUIC 的原因，导致 Hetzner 防火墙与 Cloudflare ZeroTrust 无法正常通讯的问题。</summary>
    <content type="html"><![CDATA[<img src="https://23h.at/_astro/cover.B1P2BJBx_Z7FGWq.webp" alt="QUIC - 记一次 Hetzner 防火墙 与 Cloudflare ZeroTrust 的 Debug 之旅" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>背景</h2>
<p>我个人有一台 Hetzner 的独立服务器。Hetner 对于独立服务器，提供的有 Stateless 的防火强服务。</p>
<div></div><div>关于无状态（Stateless）防火墙</div><div></div><div><p>简而言之，就是防火墙不会去追踪每个链接的信息，仅仅按照条件过滤包的内容。
Hetzner 的防火墙更是仅仅只有四元组（源 IP、目标 IP、源端口、目标端口）这一种过滤条件。</p></div>
<p>由于懒得处理 Docker 的端口问题，所以很自然而然的打开了这个防火墙服务，并使用了其默认配置的 WebServer 模版。</p>
<p>默认配置如下图：</p>
<img src="https://img.23h.at/i/2024/12/01/111a5fm.webp" alt="默认 webserver 模版的配置" />
<p>与此同时，我使用了 Cloudflare 的 Zero Trust 服务，来访问一些内部服务。</p>
<h2>问题</h2>
<p>在配置好防火墙后的某一天，突然发现，本应经由 Cloudflare ZeroTrust 访问的服务，突然变得无法访问了。</p>
<p>Cloudflare 官方给出的报错非常简单明了，就是 Argo Tunnel 的连接失败。 在查询了 Cloudflare 的后台后，发现这台服务器已经标记为离线状态。</p>
<p>在连接到服务并查询Cloudfalred 的日志，会发现有如下报错信息：</p>
<pre><code>Failed to dial a quic connection error="failed to dial to edge with quic: context canceled" connIndex=0 event=0 ip=198.41.200.193
</code></pre>
<p>那么，我们可以仅因此判断，时与 Cloudflare 的连接出现了问题。</p>
<p>那么问题来了，是什么东西导致的问题的。</p>
<h2>排查与分析</h2>
<p>首先的排查对象肯定是对应 IP 的可联通性，我曾经尝试过使用 <code>Curl</code> 来访问服务，发现可以正常获取到服务器的的错误页面。
那么也就是说，单纯的访问这个页面其实是没问题的。</p>
<p>但是，这里的小字，<code>quic</code> 引起了我的注意。
尽管之前曾听说过 QUIC 的大名，但其实除了仅仅在各大防火墙的配置上，以及知道是使用 UDP 来进行通信外，并没有更多的了解。</p>
<p>知道 QUIC 的原因，也仅仅是因为各大防火墙服务，提供了阻止 QUIC 链接的选项。（此时的我并没有意识到为什么要阻止 QUIC）。</p>
<p>Cloudflare 官方是提供的关于 ZeroTrust 和防火墙的文档。</p>
<div><a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/deploy-tunnels/tunnel-with-firewall/" target="_blank" rel="noopener noreferrer"><div><div><div><img src="https://developers.cloudflare.com/favicon.png" alt="" class="h-4 w-4 shrink-0 brightness-75 contrast-110" /><span>developers.cloudflare.com</span></div><div>
          Tunnel with firewall
        </div><p>
          You can implement a positive security model with Cloudflare Tunnel by blocking all ingress traffic and allowing only egress traffic from cloudflared. Only the services specified in your tunnel configuration will be exposed to the outside world.
        </p><div><span>https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/deploy-tunnels/tunnel-with-firewall/</span></div></div><div><img src="https://developers.cloudflare.com/zt-preview.png" alt="Preview" class="h-full w-full object-cover brightness-75 contrast-110" /></div></div></a></div>
<p>根据这份文档，可以得到的结论是，Cloudflared 使用 QUIC 来进行通讯，因此，需要开放 7844 这个端口的出站。</p>
<p>但是根据 Hetzner 的模版，出站是完全开放的，也就是说不存在任何阻止。</p>
<p>（这期间，我曾经怀疑过是否是 Hetzner 防火墙的问题，使用诸如允许所有链接这样的方法来解决，但是现在看来并没有任何意义。）</p>
<p>即便使用防火墙单独开放 QUIC 的 7844 端口，也依然无法解决问题。</p>
<p>直到最后，我放行了全部的 UDP 访问，QUIC 突然就通了。</p>
<p>最后，所有的定位都定位到了 QUIC 这个协议的问题。</p>
<h2>来自 QUIC 的问题</h2>
<p>QUIC 的具体工作细节，有很多人都比我更加了解。仅就目前我们可以知道的是，QUIC 使用了 UDP 进行通讯。
由于减少了 TCP 一样的握手过程，整个通讯速度非常快（至少官方是这么说的）， 可以做到 0-RTT。</p>
<p>而与我，以及很多人想象的不同，QUIC 并不是要取代 HTTP 应用层传输协议，QUIC 实际上是与 TCP 和 UDP 同属于传输层的协议。</p>
<p>这也就是说，QUIC 实际上能够承载的，不仅仅只有 HTTP 一种。</p>
<p>而 UDP 本身是一种只管发不管理的协议，本身也并不具备任何的可靠性可言。但很明显，QUIC 既然要试图达到 TCP 的效果，自然需要实现某种特定的机制，来解决链接的可靠性问题。</p>
<h3>TCP 与防火墙</h3>
<p>TCP 对于链接的识别，实际上是基于四元组（源 IP、目标 IP、源端口、目标端口）这样的方式来识别的。
而用户或服务端的网络 IP 发生变化 （例如，用户位于移动网络中），那么，用户与服务器的链接实际上是完全不同的链接。</p>
<p>对于大多数防火墙而言，由于接收方均为固定端口，所以只要设置好目标的端口，那么出站策略为目标 IP 和端口就完全 OK 了.</p>
<h3>QUIC 的可靠性</h3>
<p>由于 UDP 并没有类似的操作，本质上是只要发出去就算成功，所以 QUIC 自己实现了一套类似的机制。这个机制的名字叫做 <code>Connection ID</code>。</p>
<p>根据<a href="https://gist.github.com/martinthomson/744d04cbcec9be554f2f8e7bae2715b8" rel="noopener noreferrer" target="_blank">这篇文档</a>,</p>
<p>可以看到 QUIC 的包其实有这个如下结构：</p>
<pre><code> 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|S|Typ|  Next   |              Magic "uic"/"UIC"                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                         Connection ID                         +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                            Version                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Packet Number                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       [Header Extensions]                   ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           Payload                           ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
</code></pre>
<p>也就是说，整个通讯实际都是依靠 <code>Connection ID</code> 来维持整个通讯的唯一性。</p>
<p>于此同时，QUIC 使用了 TLS 来进行加密，这个 <code>Connection ID</code> 并不能被任何中间人所识别和定位。</p>
<h2>根本原因</h2>
<p>这个根本原因，其实比任何人想象的都简单，由于 QUIC 使用了 UDP 进行通讯，他也需要一个端口来进行通讯的接收。</p>
<p>问题就出现在这个问题端口上，<strong>QUIC 中，客户端的端口，是完全随机决定的！</strong></p>
<p>我们简单梳理下，这个通讯是这样一个逻辑：</p>
<ol>
<li>服务端的端口是确定的，在发送一个 QUIC 请求的时候，客户端会先随机选择一个端口发送 UDP的包。</li>
<li>服务端接收到这个包，直接向用户发送 UDP 请求的端口返回。</li>
</ol>
<p>但是，由于与 TCP 不同，UDP 的包本身不包含任何确认信息（如，HETZNER 的防火墙，就针对 TCP 的 ACK包，做了特殊的处理）， 防火墙均无法分辨这个请求是否是用来建立链接的，只能将 UDP 完全 DROP。</p>
<h2>解决方案</h2>
<p>这个问题到此就很晴朗了，理论上将，<strong>只需要开放所有的 UDP 端口，就可以解决这个问题</strong>。</p>
<p>但是，开放全部的 UDP 端口，并不是一个很好的策略。</p>
<p>由于 QUIC 并不是百分百普及，仅对于Cloudflare ZeroTrust 而言，我们完全可以选择其基于 HTTP/2 的通讯方式。</p>
<p>使用方法也仅仅是使用 <code>--protocol http2</code> 这个参数即可。</p>
<p>当然，对于其他的有防火墙的情况，关闭 QUIC 在目前来看，并不失为一种选择。</p>]]></content>
    <category term="QUIC" />
    <category term="Debug" />
    <category term="服务器" />
  </entry>
  <entry>
    <title>使用 Expressive Code 强化 Astro 代码块</title>
    <link href="https://23h.at//posts/expressive-code-astro" rel="alternate" type="text/html"/>
    <id>https://23h.at//posts/expressive-code-astro</id>
    <updated>2024-11-14T00:00:00.000Z</updated>
    <published>2024-11-14T00:00:00.000Z</published>
    <author>
      <name>Monolith</name>
    </author>
    <summary type="text">增强现有的基于 Shiki 的 Astro 代码块。</summary>
    <content type="html"><![CDATA[<img src="https://23h.at/_astro/cover.CcE_H3ER_Z224o5m.webp" alt="使用 Expressive Code 强化 Astro 代码块" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>总述</h2>
<p>Astro 默认使用的是 Shiki 这个库来进行代码高亮操作。</p>
<div>
<a href="https://github.com/shikijs/shiki" target="_blank" rel="noopener noreferrer">
  <div>
    <div>
      <div>
        <div></div>
        <div>shikijs</div>
      </div>
      <div>/</div>
      <div>shiki</div>
    </div>
    <div>
      
        
      
    </div>
  </div>
  <div>Loading...</div>
  <div>
    <div>
      
        
      
      <span>0</span>
    </div>
    <div>
      
        
      
      <span>0</span>
    </div>
    <div>
      
        
      
      <span>-</span>
    </div>
  </div>
</a>

              </div>
<p>相比于 PrismJS 这个常见选择，Astro 官方选择 Shiki 的原因无外乎，语言更新，主题更好看（且原生支持所有 VS Code 的主题），官方曾经的解释被放在了这里：</p>
<div><a href="https://github.com/withastro/astro/issues/1212" target="_blank" rel="noopener noreferrer"><div><div><div><img src="https://github.com/fluidicon.png" alt="" class="h-4 w-4 shrink-0 brightness-75 contrast-110" /><span>github.com</span></div><div>
          💡 RFC: Replace Prism with Shiki · Issue #1212 · withastro/astro
        </div><p>
          Background &amp; Motivation There’s been significant interest in our Discord to replace Prism with Shiki: @tusharsadhwani https://discord.com/channels/830184174198718474/872579324446928896/875712505287…
        </p><div><span>https://github.com/withastro/astro/issues/1212</span></div></div><div><img src="https://opengraph.githubassets.com/f680b373a81563dc413f544974a3149a4b5fa571ae9a2904919a2e81c09a3675/withastro/astro/issues/1212" alt="Preview" class="h-full w-full object-cover brightness-75 contrast-110" /></div></div></a></div>
<p>但是尽管强大，但是能够提供的功能目前也就只有代码高亮一个，而且由于是新的库，Prism 之前就有的比如行号等功能是缺失的。</p>
<p>此时就有了我们的 <code>Expressive Code</code>。 这个库是个拓展，可以提供诸如行号，窗口框，文件名这样的功能。</p>
<h2>安装</h2>
<p>对于 Astro 项目而言，安装 Expressive Code 非常简单，仅仅需要一行命令即可：</p>
<pre><code>npx astro add astro-expressive-code
</code></pre>
<p>但是，需要注意的是，默认的安装实际上是有很多问题的，特别是对于 MDX 用户来讲。</p>
<h2>针对 MDX 的 Fix</h2>
<p>如果正在使用 MDX 来进行 Markdown 的渲染的话，那么需要进行以下几步操作</p>
<h3>调整优先级</h3>
<p>将 Expressive Code 的优先级放在 MDX 上面，防止 MDX 提前将代码块预处理。</p>
<pre><code>export default defineConfig({
integrations: [
    expressiveCode(),
    mdx()
]})
</code></pre>
<h3>调整 MDX 插件位置</h3>
<p>大部分人的 MDX 都是装了各种各样的插件，但是由于 Expressive Code 会默认修改并重组配置文件，所以可能会导致渲染不生效。</p>
<p>查阅了一些文档后，有提到，<strong>需要将 MDX 的插件配置转移到 Markdown 中</strong></p>
<p>也就是说将 <code>rehypePlugins</code> 和 <code>remarkPlugins</code> 这两个字段转移。</p>
<p>由于 MDX 会自动读取 Markdown 中的配置，这样不会造成 MDX 和 Expressive Code 的冲突。</p>
<h2>Expressive Code 的功能</h2>
<h3>主题框架</h3>
<p>如同本站一样，Expressive Code 支持将代码框框起来，变成编辑器的样子，并增加一个 title 的部分：</p>
<p>这里的代码如下：</p>
<pre><code>
```java title=HelloWorld.java
public static void main(string args[]){
    System.out.println("Hello World!")
}
```

</code></pre>
<h3>行高亮</h3>
<p>代码高亮也算是Express Code 的功能之一（主要是默认的 Shiki 没有提供类似的功能），从官方文档投下来的代码如下：</p>
<pre><code>```js {1, 4, 7-8}
// Line 1 - targeted by line number
// Line 2
// Line 3
// Line 4 - targeted by line number
// Line 5
// Line 6
// Line 7 - targeted by range "7-8"
// Line 8 - targeted by range "7-8"
```
</code></pre>
<h3>行号</h3>
<p>行号也算是一个常用组件，与 PrimsJS 一样，需要单独引入一个包：</p>
<pre><code>pnpm i @expressive-code/plugin-line-numbers
</code></pre>]]></content>
    <category term="Shiki" />
    <category term="Expressive Code" />
    <category term="Astro" />
  </entry>
  <entry>
    <title>使用Coder + DevContainers快速构建远程开发环境</title>
    <link href="https://23h.at//posts/coder-dev-env" rel="alternate" type="text/html"/>
    <id>https://23h.at//posts/coder-dev-env</id>
    <updated>2024-11-11T00:00:00.000Z</updated>
    <published>2024-11-11T00:00:00.000Z</published>
    <author>
      <name>Monolith</name>
    </author>
    <summary type="text">本文主要介绍 Coder 的相关安装使用配置，来达到一个自建 GitHub CodeSpace 的功能。同时，会使用 DevContainer 来进行开发环境的配置。</summary>
    <content type="html"><![CDATA[<img src="https://23h.at/_astro/cover.DqpVZs9K_1Vmpdb.webp" alt="使用Coder + DevContainers快速构建远程开发环境" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>序言</h2>
<p>最近几天在努力试用 Github CodeSpace，这个东西基本可以覆盖一些远程开发的需求。
但是既然是云服务，自然而然的有着各种各样的限制，而其中一个绕不开的就是价格。具体的价格可以查看下面的网页：</p>
<div><a href="https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-codespaces/about-billing-for-github-codespaces" target="_blank" rel="noopener noreferrer"><div><div><div><img src="https://docs.github.com/assets/cb-345/images/site/favicon.png" alt="" class="h-4 w-4 shrink-0 brightness-75 contrast-110" /><span>docs-internal.github.com</span></div><div>
          GitHub Codespaces billing - GitHub Docs
        </div><p>
          Learn about the costs for using GitHub Codespaces, and the monthly usage quotas included with GitHub personal accounts.
        </p><div><span>https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-codespaces/about-billing-for-github-codespaces</span></div></div><div><img src="https://docs.github.com/assets/cb-345/images/social-cards/default.png" alt="Preview" class="h-full w-full object-cover brightness-75 contrast-110" /></div></div></a></div>
<p>总的来说，每个月有 120 个计算时长。其中，每个 CPU 每运行一小时算一个计算时长。</p>
<p>对于免费用户来讲，最低配置（2 核 4G 内存)一个月满打满算大约能有 60 个小时的最低配置服务器。</p>
<p>当然，对于大部分人这个目前也算是比较够用的了。</p>
<p>但是 CodeSpace 终归是依赖于云服务的，且是国外的服务，国内访问性能谈不上优秀（特别是还有各种防火墙）。</p>
<p>所以，利用家里的 NAS 或者自己的服务器自建一个类似的开发环境是个很好的选择。</p>
<h2>自建 CodeSpace 的选择</h2>
<p>市面上能用的选择其实比想象中的更少。</p>



































<table><thead><tr><th>特性</th><th>Coder</th><th>GitPod</th><th>DevPod</th></tr></thead><tbody><tr><td>本地部署</td><td>✅</td><td>✅</td><td>✅</td></tr><tr><td>开源</td><td>✅</td><td>✅</td><td>✅</td></tr><tr><td>服务端部署</td><td>✅</td><td>✅</td><td>❌</td></tr><tr><td>支持 Docker</td><td>✅</td><td>❌</td><td>✅</td></tr></tbody></table>
<p>所以，最后 Coder 几乎成了自建 CodeSpace-link 的不二之选。</p>
<p>但是，开源版的 Coder 仍然有以下缺点：</p>
<ul>
<li>仅支持一个 Git External Account</li>
<li>Terraform 相当繁杂且难以理解使用</li>
</ul>
<h2>部署 Coder</h2>
<p>Coder 本身只是个管理程序，具体的业务实际上 Coder 是不管的。
我们常规的部署方法就是 Docker 启动，而无需专门搞一个服务器部署。</p>
<p>启动的配置文件可以参考以下内容：</p>
<pre><code>version: '3.8'

services:
  coder:
    # This MUST be stable for our documentation and
    # other automations.
    image: ghcr.io/coder/coder:${CODER_VERSION:-latest}
    ports:
      - "7080:7080"
    environment:
      CODER_PG_CONNECTION_URL: "postgresql://username:password@host/database?sslmode=disable"
      CODER_HTTP_ADDRESS: "0.0.0.0:7080"
      CODER_ACCESS_URL: "&lt;&gt;"
      CODER_WILDCARD_ACCESS_URL: "*.your.domain"
      CODER_EXTERNAL_AUTH_0_ID: "github"
      CODER_EXTERNAL_AUTH_0_TYPE: github
      CODER_EXTERNAL_AUTH_0_CLIENT_ID: &lt;你的 Github Oauth APP ID&gt;
      CODER_EXTERNAL_AUTH_0_CLIENT_SECRET: &lt;&lt;你的 Github Oauth APP Secret&gt;
    group_add:
      - "988" # docker group on host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
</code></pre>
<p>这里有几个参数需要额外注意下：</p>
<ul>
<li><code>CODER_ACCESS_URL</code>: 这个是 你的 Coder 访问用域名。</li>
<li><code>CODER_WILDCARD_ACCESS_URL</code>: 如果你使用 Cloudflare 进行代理加速，那么需要域名符合 <code>*.example.com</code> 这个模式。使用 <code>*.coder.example.com</code>这个模式会导致 Cloudflare 无法签发 SSL 证书，需要额外交钱。
<ul>
<li>这个域名可以和 <code>CODER_ACCESS_URL</code> 不在一个域名下。</li>
</ul>
</li>
<li><code>group_add</code> 和 <code>volumes</code>： 这两个参数主要用于解决 Coder 与 Docker 通信的问题（如果是有部署Coder 的机子作为容器机器的情况）。</li>
<li><code>CODER_EXTERNAL_AUTH_0_*</code>；这个需要自己去 Github 申请一个 Github APP。具体权限配置要求如下表<sup><a href="#user-content-fn-1">1</a></sup>：</li>
</ul>



































<table><thead><tr><th>Name</th><th>Permission</th><th>Description</th></tr></thead><tbody><tr><td>Contents</td><td>Read &amp; Write</td><td>Grants access to code and commit statuses.</td></tr><tr><td>Pull requests</td><td>Read &amp; Write</td><td>Grants access to create and update pull requests.</td></tr><tr><td>Workflows</td><td>Read &amp; Write</td><td>Grants access to update files in .github/workflows/.</td></tr><tr><td>Metadata</td><td>Read-only</td><td>Grants access to metadata written by GitHub Apps.</td></tr><tr><td>Members</td><td>Read-only</td><td>Grants access to organization members and teams.</td></tr></tbody></table>
<h2>配置 Template</h2>
<p>Coder 主要的构建方法是使用 <a href="https://www.terraform.io/" rel="noopener noreferrer" target="_blank">terraform</a> 来进行具体环境的构建。简单的描述即为，根据用户的声明，来决定如何构建一个完整 Ops 环境。</p>
<p>对于绝大多数开发者而言，可以直接考虑使用 Coder 官方提供的模板，而仅需在自己进行扩展时进行修改 Template。</p>
<p>通常情况下，选择下面的模板启动项目即可：
<img src="https://img.23h.at/i/2024/11/11/ih5l0p.webp" alt="Dev Container选择示例" /></p>
<h3>扩展 Template</h3>
<p>由于 Coder 使用了 Terraform 来进行构建，则我们可以在这个基础上进行一些拓展，比如，我的配置文件就加入了下面的内容来自动化配置 Github Token，和添加了 Jetbrains 系列 IDE 的支持：</p>
<pre><code>module "github-upload-public-key" {
  source   = "registry.coder.com/modules/github-upload-public-key/coder"
  version  = "1.0.15"
  agent_id = coder_agent.main.id
}

module "jetbrains_gateway" {
  source         = "registry.coder.com/modules/jetbrains-gateway/coder"
  version        = "1.0.23"
  agent_id       = coder_agent.main.id
  agent_name     = "main"
  folder         = "/workspaces"
  jetbrains_ides = ["CL", "GO", "IU", "PY", "WS"]
  default        = "WS"
}

module "vscode-web" {
  source         = "registry.coder.com/modules/vscode-web/coder"
  version        = "1.0.22"
  agent_id       = coder_agent.main.id
  accept_license = true
}
</code></pre>
<p>Coder 官方提供了一些官方的扩展，可以参考并按需添加: <a href="https://registry.coder.com/modules" rel="noopener noreferrer" target="_blank">Coder Registry</a></p>
<h2>配置DevContainer</h2>
<p>DevContainer 对程序开发最大的优势即为对单个项目标准化了整个项目开发环境，而不用因个体开发设备的不同而单独配置环境。</p>
<p>Github 的官方文档其实有关于该功能比较详细的叙述：</p>
<div><a href="https://docs.github.com/zh/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers" target="_blank" rel="noopener noreferrer"><div><div><div><img src="https://docs.github.com/assets/cb-345/images/site/favicon.png" alt="" class="h-4 w-4 shrink-0 brightness-75 contrast-110" /><span>docs-internal.github.com</span></div><div>
          开发容器简介 - GitHub 文档
        </div><p>
          在 codespace 中工作时，你工作所处的环境是使用托管在虚拟机上的开发容器创建的。
        </p><div><span>https://docs.github.com/zh/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers</span></div></div><div><img src="https://docs.github.com/assets/cb-345/images/social-cards/default.png" alt="Preview" class="h-full w-full object-cover brightness-75 contrast-110" /></div></div></a></div>
<p>总结下来就是，配置文件生成具体的开发环境。</p>
<p>为了启用 DevContainer，开发者需要在项目的 Repo 中添加 名为<code>.devcontainer/devcontainer.json</code>。一份案例配置可以参考如下内容：</p>
<pre><code>// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
    //环境的名称
  "name": "Neon Develop Env",
  // 如何创建 Docker 镜像。
 //微软官方也提供了一些模版配置:https://containers.dev/templates
  "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
  //按照template 启用一些相关的 feature，如下文则启用了 node js。
  "features": {
    "ghcr.io/devcontainers/features/node:1": {}
  },

  // 容器内部端口通常不可直接访问，需要进行端口转发。下面的配置不是必须的，通常我们也会使用 Coder 自带的配置进行端口转发。
  "forwardPorts": [
    4321
  ],
  "workspaceFolder": "/workspace/neon"
}
</code></pre>
<p>但是与 Github CodeSpace 稍有不同的是，Coder 使用的是自己的一套组件来进行环境构建：</p>
<div>
<a href="https://github.com/coder/envbuilder" target="_blank" rel="noopener noreferrer">
  <div>
    <div>
      <div>
        <div></div>
        <div>coder</div>
      </div>
      <div>/</div>
      <div>envbuilder</div>
    </div>
    <div>
      
        
      
    </div>
  </div>
  <div>Loading...</div>
  <div>
    <div>
      
        
      
      <span>0</span>
    </div>
    <div>
      
        
      
      <span>0</span>
    </div>
    <div>
      
        
      
      <span>-</span>
    </div>
  </div>
</a>

              </div>
<p>但是这点差异，目前看多数个人项目不会构成显著区别。</p>
<h2>启动项目</h2>
<p>由于之前已经完成了项目的配置，启动整个项目只需要点几下按钮即可:</p>
<img src="https://img.23h.at/i/2024/11/11/iwn9yx.webp" alt="选择 DevContainer" />
<img src="https://img.23h.at/i/2024/11/11/iwn7k9.webp" alt="启动的项目环境" />
<p>之后便可以点击 VS Code 通过 VS Code 进行开发，或者直接使用 Code-Server / VS Code Web 进行在线开发。</p>
<p>当然，如果需要 VSCode 安装有额外的插件，也可以通过配置文件进行配置，来达到自动安装插件的目的。</p>
<h2>总结</h2>
<p>Coder 对很多有远程开发需求的开发者其实提供了一个比较好的入口。</p>
<p>整体的使用难度仍在接受范围内，适合很多开发者装在自己的开发机上来远程开发。</p>
<p>当然，免费版本的 Coder 仍然有诸多限制，比如SSO支持，只能使用一个 Git 外部账号，使用其他的 Git 链接就会比较困难等等。</p>
<p>但是目前来看，在有新产品出现前，Coder已经是个人远程开发的一个比较好的选择。</p>
<section><h2>Footnotes</h2>
<ol>
<li>
<p>参考自官方的指南：<a href="https://coder.com/docs/admin/external-auth#github" rel="noopener noreferrer" target="_blank">https://coder.com/docs/admin/external-auth#github</a> <a href="#user-content-fnref-1">↩</a></p>
</li>
</ol>
</section>]]></content>
    <category term="Coder" />
    <category term="Docker" />
    <category term="开发" />
    <category term="DevContainer" />
  </entry>
  <entry>
    <title>HIMEHINA 现地指南 (下）旅行篇</title>
    <link href="https://23h.at//posts/himehina-live-guide-2024-2" rel="alternate" type="text/html"/>
    <id>https://23h.at//posts/himehina-live-guide-2024-2</id>
    <updated>2024-11-10T00:00:00.000Z</updated>
    <published>2024-11-10T00:00:00.000Z</published>
    <author>
      <name>Monolith</name>
    </author>
    <summary type="text">关于如何前往 HIMEHINA 的线下 LIVE。作为下篇的本文来讲，主要帮助从未参加赴日参加 LIVE 的朋友的一个进行旅游指导。</summary>
    <content type="html"><![CDATA[<img src="https://23h.at/_astro/cover.CJSLSFcb_ZOGBTx.webp" alt="HIMEHINA 现地指南 (下）旅行篇" style="width: 100%; height: auto; margin-bottom: 1em;" />
<div><div><div></div><div>注意</div></div><div><p>这篇文章写于HIMEHINA 2024 西日本巡演前，可能有内容因为时效性而发生变化。</p></div></div>
<h2>总述</h2>
<p>HIMEHINA 的第一次巡演马上就要开始了，为了方便想要现地参加的酒姬民，特意写下这篇文章。
所以，这篇文章主要面向还没有赴日或出国旅游经验酒姬民。主要会从签证、机票、日本交通、住宿及 LIVE 参加流程入手。
如果对抽票有疑问的酒姬民，可以先参考我的上一篇文章：</p>
<div><a href="http://23h.at/posts/himehina-live-guide-2024-1" target="_blank" rel="noopener noreferrer"><div><div><div><img src="http://23h.at/apple-touch-icon.png" alt="" class="h-4 w-4 shrink-0 brightness-75 contrast-110" /><span>23h.at</span></div><div>
          HIMEHINA 现地指南 (上）票务篇 | 在 23 点
        </div><p>
          关于如何前往 HIMEHINA 的线下 LIVE。本文主要关注如何获得一个可以在中国使用的日本手机号，和如何进行抽选。
        </p><div><span>http://23h.at/posts/himehina-live-guide-2024-1</span></div></div><div><img src="https://23h.at/og-image.webp" alt="Preview" class="h-full w-full object-cover brightness-75 contrast-110" /></div></div></a></div>
<h2>护照与签证篇</h2>
<h3>🪪 护照办理</h3>
<div><div><div></div><div>注意</div></div><div><p>护照的办理主要针对<strong>中国大陆</strong>的酒姬民，其他地区会略有不同。</p></div></div>
<p>对于多数非敏感地区（如福建、云南等地）的一般居民来讲，护照的办理并不复杂，且多数地区均提供<strong>线上查询办理资料的渠道</strong>。
尽管经过改革后，护照的签发机关从出入境管理局改为了国家移民局，但是多数办理方法仍然趋同。
对于还没有护照的酒姬民，可以直接在高德上搜 <code>出入境管理局</code> 或更简单的 <code>护照</code>，即可返回离你比较近的办事地点。</p>
<p>办理护照通常需要的资料有：</p>
<ol>
<li>户口本</li>
<li>身份证</li>
<li>未满 16 周岁，需要监护人陪同。</li>
<li>照片
<ul>
<li>通常可直接在出入境管理局大厅现场拍照。</li>
</ul>
</li>
<li>100 - 200 元手续费</li>
</ol>
<p>护照通常需要 1 周时间即可获得，且有着 10 年的有效期，且不会因为出行与否而失效，建议尽早办理。</p>
<h3>👮 签证</h3>
<p>中国大陆作为正义联盟的一员，出访发达国家通常需要申请额外的签证。有了签证，才有对应地区的进入许可。</p>
<p>相较于其他地区，诸如美国、欧洲申根等，日本签证属于相当好签那种，几乎没有太多压力。</p>
<p>日本旅行签证通常有三种类型。其办理难度依次递增：</p>
<ul>
<li>单次签证</li>
<li>三年多次签证</li>
<li>五年多次签证</li>
</ul>
<p>申请签证的具体材料可以参考日本大使馆的文章：<a href="https://www.cn.emb-japan.go.jp/itpr_zh/20190101.html" rel="noopener noreferrer" target="_blank">点击这里查看</a></p>
<p>具体需要的材料可能会因为你所在地区、身份不同而不同。</p>
<div><div><div></div><div>注意</div></div><div><p>由于日本人特有的死板特性，日本旅游签证<strong>必须通过旅行社递交</strong>。</p></div></div>
<p>因此，日本旅游签证的最简单提交方法，就是通过第三方平台，诸如淘宝、携程等。</p>
<p>由于每个人个体不同，所需签证材料也有所不同，具体需要问旅行社。</p>
<p>此外，日本签证通常需要花费两周左右，且需要再签证签发内的三个月有一次日本入境记录，建议合理安排办理时间。</p>
<h2>机票与酒店</h2>
<p>机票与酒店与国内旅行并无本质不同，具体的购买方法仍然是通过携程、飞猪等第三方平台购买。</p>
<h3>🛫 机票</h3>
<p>尽管都是在携程购买，但是由于出发地、目的地等不同，可选的航班也有所不同。同时，国际航班由于航司不同，也会带来一些体验上的差异。</p>
<ul>
<li>全日空 （ANA）和日本航空 （JAL）
<ul>
<li>均为日本航司。</li>
<li>全日空通常是最好的那一班，有这不错的服务，可以携带两件 23KG 的行李。</li>
<li>JAL 我没有做过，但是体验也还可以，行李额也是 23KG。</li>
</ul>
</li>
<li>东方航空 和 南方航空
<ul>
<li>经典且常见的国内航司。</li>
<li>行李额同样是 23KG。</li>
</ul>
</li>
<li>春秋航空 和 吉祥航空
<ul>
<li>廉价航空，但价格不算太廉价</li>
<li>有很多红眼航班，可能价格会很便宜。</li>
<li>没有行李额，且对手提行李要求严格。单件行李约数百元。</li>
</ul>
</li>
</ul>
<p>购买机票不需要签证，仅需要护照。
如果你能够确定自己100%能够获得日本签证，可以提前购买机票 （价格会比之后便宜很多）。</p>
<h3>🛏️ 酒店及住宿</h3>
<p>为什么叫酒店及住宿，主要原因在于你可以不住酒店。</p>
<h4>🏨酒店</h4>
<p>日本酒店预定于预定国内酒店十分相仿。都是在一些中介平台上购买。由于每个人的具体需求不一样，需要按照个人徐行定制。</p>
<p>预订酒店需要注意以下内容；</p>
<ul>
<li>姓名需要跟护照上保持一致
<ul>
<li>曾有群友因为此原因导致无法入住。</li>
</ul>
</li>
<li>入住人数需要于填写的一致，部分酒店可能会根据入住人数收费。</li>
</ul>
<h4>其他住宿选择</h4>
<p>下面的部分并不作为推介，这些选择通常有着比较糟心的体验，但是仍然是预算不足的酒姬民的最后之选。
具体的使用方法，请自行努力。</p>
<ul>
<li>网吧
<ul>
<li>日本网吧通常有床</li>
</ul>
</li>
<li>公园
<ul>
<li>体验差点，但胜在一分不要。</li>
</ul>
</li>
</ul>
<h2>在日交通及付款</h2>
<p>日本的公共交通体系以铁路运输为主，公路（如巴士）运输为辅，且通常不支持使用常见的二维码支付。</p>
<h3>💳 支付方式</h3>
<p>日本国内，通常旅游胜地的地方，可用的支付选项非常繁多，包括但不限于信用卡，支付宝等。</p>
<p>但是，如果想要玩的痛快，建议去当地银行兑换一些日元，并申办一张Visa / MasterCard / JCB的信用卡。</p>
<p>HIMEHINA 当日的物贩，也通常会提供信用卡支付的选项 （但不会有支付宝支付）。</p>
<p>然而如果要抽扭蛋，现金则是必备的。通常的价格是 500 日元一次，建议量力而行。</p>
<h3>西瓜卡 （Suica）</h3>
<p><img src="https://img.23h.at/i/2024/11/10/hd79do.webp" alt="西瓜卡样式" />
日本通用的公交卡品牌，类似国内的交通联合，仅需这一张卡，几乎可以刷通日本所有的公共交通。</p>
<p>这张卡的购买分为Android 用户和 iOS 用户。
对于苹果手机用户，可以直接在 <code>钱包</code> 这个APP 开通电子版并使用中国银行卡充值。具体开通方法可以直接参考 Apple 官方的指南：</p>
<div><a href="https://support.apple.com/zh-cn/108772" target="_blank" rel="noopener noreferrer"><div><div><div><img src="https://support.apple.com/favicon.ico" alt="" class="h-4 w-4 shrink-0 brightness-75 contrast-110" /><span>support.apple.com</span></div><div>
          将 Suica、PASMO 或 ICOCA 卡添加到 Apple 钱包中 - 官方 Apple 支持 (中国)
        </div><p>
          在 iPhone 或 Apple Watch 上将 Suica、PASMO 或 ICOCA 卡添加到 Apple 钱包中，以便使用 Apple Pay 乘坐公共交通和购物。
        </p><div><span>https://support.apple.com/zh-cn/108772</span></div></div><div><img src="https://cdsassets.apple.com/live/7WUAS350/images/inline-icons/ios15-add-icon.png" alt="Preview" class="h-full w-full object-cover brightness-75 contrast-110" /></div></div></a></div>
<p>而对于 Android 用户，非日本手机均无法开通。需要自行在现场购买实体卡 （传说已经重新开售）。</p>
<h3>🚃 铁路交通</h3>
<p>与中国铁路全部为中国铁道总公司(CR)大包大揽不同，日本的铁路由多家公司运营，且每家铁路公司的价格、体验乃至线路均有所不同。</p>
<p>因此，相比于国内的同站台换乘，日本多数需要出站换乘。</p>
<p>在日本做铁路系统（包括地铁），更多的参考的是地铁运输模式的火车。</p>
<p>使用手机 APP 查询线路是最主要的，可以选择使用 <code>谷歌地图</code> 来查询地铁的路线。（这款软件需要通常可以在各大应用市场下载）</p>
<p>以下面的图片为例：
<img src="https://img.23h.at/i/2024/11/10/yv4s25.webp" alt="谷歌地图示例" /></p>
<p>可以看到图片上有很多信息，我们来一个个解释：</p>
<ul>
<li><strong>日暮里</strong>： 当前站点
<ul>
<li>一个站台可能有多个不同的铁道公司运行，但是这些公司可能位于不同的地点，请遵照当地的地面指示找到对应的地铁线路。</li>
</ul>
</li>
<li><strong>山手线</strong>： 具体的线路名称，如同国内某某10 号线。
<ul>
<li>不同的线路名称可能代表不同的公司运行。</li>
<li>通常 J 开头的方框均为 JR 所属的列车，否则几乎都是其他铁路公司。</li>
<li><strong>如果你看到纯数字了，那大概率不是铁路而是公交车🚌……</strong>（我被坑过）</li>
</ul>
</li>
<li><strong>上野/东京方面（外环）</strong>： 列车的行进方向</li>
<li><strong>23:11</strong>：下一班车的时间
<ul>
<li>日本的列车整体是准时的，不出意外按照这个就是下一班列车到达的时间。</li>
</ul>
</li>
<li><strong>10 号站台</strong>： 列车即将停靠的站台。
<ul>
<li>结构类似于国内火车站，乘客需要到指定的站台候车，通常情况下按照站台走，比像国内一样按照线路走要容易的多。</li>
</ul>
</li>
<li><strong>各站停车</strong>: 车辆类型
<ul>
<li>日本同个站台可能会有多种车（类似上海 3/4 号线路）。各站停车则意味着每站都停。</li>
<li>各站停车通常比较慢（但是地铁均为各站停车），但是如果你对当地不熟悉，建议遵循谷歌地图的上车时间引导，以避免上错车。</li>
</ul>
</li>
<li><strong>有乐町</strong>： 下车的站点。</li>
</ul>
<div><div><div></div><div>关于车上睡觉</div></div><div><p>看这篇文章可能都是第一次去日本，可能并不会困，但是仍然可能有人因为过于疲倦而睡着。<br />
但仍然建议尽可能保持清醒，以免被诸如 <a href="https://www.bilibili.com/video/BV13T411e79z" rel="noopener noreferrer" target="_blank">阪环离心机</a> 这类列车发配边疆而登上神人榜。</p></div></div>
<h2>🤘 参加 LIVE</h2>
<h3>🥕 物贩</h3>
<p>参加 LIVE 很重要的一个环节就是参加物品贩卖，毕竟这么老远参加 LIVE，<strong>不买点可爱的 HH 周边作为纪念品多少有些良心不安</strong>。
此外，仍然有部分人没有/忘记携带应援榜（诸如胡萝卜）这些物件，而需要在物贩进行购买。</p>
<p>按照 Lara 的习俗，物贩通常会在 LIVE 当天的上午开始，直到演唱会的开场前结束。而具体贩卖内容则会登记在 LIVE 的专页上。</p>
<p>如果要购买物贩，<strong>建议在开始前 30 分钟左右排队会比较好</strong>。尽管通常 LaRa 的备货相对充足，但是人多了必卖光，只会剩下部分商品了。</p>
<p>购买物贩建议使用信用卡（这样买的更多），也不用费心费力的与工作人员研究现金支付的各种问题。</p>
<p>由于 LIVE 通常为夜晚开始，所以购买完可以直接放到酒店。</p>
<h3>💰 扭蛋</h3>
<div><div><div></div><div>抽卡是这样的</div></div><div><p>HIMEHINA 的抽卡难度，并不比大部分的手游高到哪里去。<strong>放平心态</strong>，建议就当给 HH 送钱了。</p></div></div>
<p>扭蛋这个东西通常可遇不可求，每次 LIVE 也不一定能有。
一般的扭蛋奖品有以下几类：</p>
<ol>
<li>特等奖：签名挂画</li>
<li>算不上一等奖的一等奖：贴纸</li>
<li>反正怎么都会中的：各种各样的徽章（吧唧）</li>
</ol>
<p>抽奖为 500 日元一次，如果没有硬币需要在工作人员处兑换。</p>
<p>抽奖为<strong>现金 Only</strong>的，<strong>所以务必携带现金</strong>。</p>
<p>每次扭蛋可以扭蛋十次，但不必完全消耗完所有硬币。</p>
<p>这个建议量力而行。</p>
<div></div><div>乐子一则</div><div></div><div><p>HIMEHINA 的 2024 演唱会的扭蛋机，是透明的。<br />
因此，你可以在投币时就知道自己要沉船了。
不是说你你这个必抽不可的。</p></div>
<h3>🎟️ 入场</h3>
<p>对于入场而言，抽票时使用的罗森 APP 为HIMEHINA LIVE 中不得不品鉴的一环。
此时，对于不同的票来源，则有着不同的入场方式。</p>
<p>全站的 LIVE 通常是叫号，所以会按照你票上写的数字来决定入场先后。
也就是说，需要在入场前根据工作人员指引进行排队入场。</p>
<div><div><div></div><div>进场需要付款 600 日元</div></div><div><p>部分演唱会场地是以酒吧名义注册的，所以需要现场付一笔酒水费，约 600 日元。<br />
之后进场时可以随便拿一瓶饮料或者啤酒。<br />
这里请提前准备现金（好像可以刷西瓜，我不确定）。<br /></p></div></div>
<h4>📱 自行抽票</h4>
<p>这种情况下，需要在<strong>落地日本</strong>后在手机上安装<code>ローチケ</code>这款 APP。</p>
<p>于此同时，请换上<strong>抽票时使用的日本手机卡</strong>。（此时，如果你是双卡手机，则建议取下中国手机卡来完成手机验证。）</p>
<p>而在打开 APP 后，会见到这个页面：</p>
<img src="https://img.23h.at/i/2024/11/10/10k1z2f.webp" alt="罗森 APP 登录示意" />
<p>与国内常用的验证方法不同，这款软件并不是你收验证码，而是你发送短信验证码给罗森。</p>
<p>当你点击<code>認証して利用をする</code>这个按钮后，会要求你发送短信。</p>
<p>请不要恐慌，并直接点击发送按钮即可。等待后即可完成登录，并自动完成下票手续。</p>
<p>之后便能在下面的页面核对你拥有的票，建议及时核对，以免</p>
<img src="https://img.23h.at/i/2024/11/10/10nm0zj.webp" alt="罗森 APP 票界面示意" />
<div><div><div></div><div>注意</div></div><div><p>通常到这里，建议停下你的操作并直接关闭 APP，且演唱会结束前不要插拔手机卡。<br />
<strong>如果你这里误操作很有可能会丢掉你的票！</strong> <br />
想要直到为什么可以看下面的抽象部分。<br /></p></div></div>
<h4>🥂 同时进场</h4>
<p>如果你并没有日本手机号，而是在其他手中购买的票（且并未进行转票操作）。则请直接跟着卖票的人一块进场即可。</p>
<h4>⚠️ 抽象部分 ⚠️</h4>
<p>使用罗森 APP 的 LIVE，不知为何并不是二维码检票，此外这个票也仅仅跟你的手机号绑定。</p>
<p>一旦你在手机上打开这个 APP，且在开演 14 天内，这个票就会自动下载到你的手机上。</p>
<p>但是请注意：此时该票将同时与你的 SIM 卡和手机绑定。更换手机重新登录 APP 或更换 SIM 卡均会导致找不到这张票。</p>
<p>由于下票是一次性的，之后并没有联系客服来挽回票这一途径。</p>
<p>此外，这个 APP 的检票方式为：</p>
<blockquote><p><strong>工作人员拿着你的手机，同时按住下面的图像上部中的的两个圆圈滑动。</strong></p></blockquote>
<img src="https://img.23h.at/i/2024/11/10/10x63wt.webp" alt="罗森检票示意" />
<p>也就是说，一旦你误操作了，票直接视为已使用，并没有任何其他的验证手段。</p>
<p><strong>一定、一定不要手贱去碰那两个圆圈。</strong></p>
<h2>🥕 享受 LIVE 吧 🥕</h2>
<p>HIMEHINA 的 LIVE 并没有太多的仪式和流程。但曾有朋友云：日式 LIVE 是演出者与观众共同成就的。</p>
<p>如果你想更加享受这场 LIVE，可以试着进行一些互动。</p>
<h3>打 Call</h3>
<p>应援棒建议选择官方的胡萝卜，如果实在没有可以在物贩购买或者找同行的群友借。</p>
<p>如果你有携带胡萝卜等应援棒，可以按照周边的人的颜色来选择打 CALL 的颜色，尽可能的与周围保持统一会比较好。</p>
<p>目前除了 <strong>「愛包ダンスホール」</strong> 这首歌以外，并没有具体的打 CALL 流程，而大概率大家的日语都不太行，不跟着唱也是 OK 的。</p>
<p>HIMEHINA 的打 CALL 没有特殊手法要求，一般慢歌一个手法，快歌一个手法就是全部了。可以参考周围人的动作。</p>
<p>（酒姬民人都很好的，就算你不会也不要担心犯错。）</p>
<h2>总结</h2>
<p>对于大部分人而言，卖出第一步通常是最难的。一旦有过一次参加 LIVE 的经验，我认为大部分人还是想要参加第二次的。</p>
<p>这篇文章可能写的并不是很全，也可能有很多有疑问的地方。</p>
<p>如果有哪些不足或建议，欢迎在评论区提出。</p>]]></content>
    <category term="HIMEHINA" />
    <category term="LIVE" />
    <category term="旅游" />
  </entry>
  <entry>
    <title>HIMEHINA 现地指南 (上）票务篇</title>
    <link href="https://23h.at//posts/himehina-live-guide-2024-1" rel="alternate" type="text/html"/>
    <id>https://23h.at//posts/himehina-live-guide-2024-1</id>
    <updated>2024-10-21T00:00:00.000Z</updated>
    <published>2024-10-21T00:00:00.000Z</published>
    <author>
      <name>Monolith</name>
    </author>
    <summary type="text">关于如何前往 HIMEHINA 的线下 LIVE。本文主要关注如何获得一个可以在中国使用的日本手机号，和如何进行抽选。</summary>
    <content type="html"><![CDATA[<img src="https://23h.at/_astro/cover.CJSLSFcb_ZOGBTx.webp" alt="HIMEHINA 现地指南 (上）票务篇" style="width: 100%; height: auto; margin-bottom: 1em;" />
<div><div><div></div><div>关于 2025 年 3 月演唱会</div></div><div><p>25 年 3 月的演唱会于巡演的抽选方式相同。本文仍旧可以提供参考。</p></div></div>
<h2>总述</h2>
<p>其实 2024年的巡演马上就要开始了。所以这篇文章的意义其实没有想象那么大了。</p>
<p>不过，估计再有几个月，也会有 2025 的 LIVE （也就一年不到的样子）或者未来的武道馆，所以这篇文章说是写给明年的 LIVE 也不是不行。但是当前还是针对 2024 「涙の薫りがする」这场巡演来完成的。</p>
<h2>准备手机号</h2>
<blockquote><p><em>迈出第一步要比想象中困难。</em></p></blockquote>
<p>日本因为反黄牛的原因，所以演唱会的抽票均需要手机号进行验证。HIMEHINA 所使用的验证方式，目前分别为抽票时验证，以及票下发时进行验证。</p>
<p>尽管上次 LIVE 没有要求抽票时验证手机号，但是这次巡演，对于抽票本身，同样需要手机号进行验证。</p>
<p>日本手机号是_严格实名制_，通常来讲，如果你没有一张可以在日本的在留卡（即长期居留许可）或者其他允许长期在日本停留的身份（如日本护照）， 那么对于外国人而言一张可以在日本外可以接收短信或者拨打电话的手机基本是不存在的。</p>
<p>此外，日本手机卡无一例外，均需要在日本激活。也因此，如果你需要在抽票时接收验证码（如 2024 年巡演），但手机卡并未激活，则可能因此无法参加票务抽选。</p>
<p>目前唯一能够进行票务抽选的选择是中国移动于日本推出的 CMLink-JP 手机卡。</p>
<h3>申请 CMLink 手机号</h3>
<div><div><div></div><div>关于激活</div></div><div><p>这张手机卡，同样需要在日本进行激活。如果未有最近的旅日计划，则这个方案也不适合你。</p></div></div>
<p>CMLink 实际上是中国移动面向华人在外旅游，留学，工作等推出的手机卡。这张卡也会与申请者在中国的身份进行绑定。</p>
<p>按照<a href="https://www.cmlink.com/jp/zh/faq/?ref=23h.at" rel="noopener noreferrer" target="_blank">官方说明</a>，这张卡同样支持非中国大陆户籍的人员进行办理，但是非中国大陆的申请者，建议先行联系 CMLink 以了解申请所需文件。</p>
<p>具体的申请步骤很简单，只需打开下面的网页：</p>
<p>并在此处选择合适的套餐：</p>
<img src="https://img.23h.at/i/2024/11/09/j54mxf.png" alt="upload in progress, 0" />
<p>这里通常选择<strong>单月套餐</strong>即可。但是对于需要长期保号的通讯，特惠套餐可能会更实惠。</p>
<p>选择手机卡时需要注意的是，请选择_语音数据卡_，选择纯数据卡是无法通过任何验证的。</p>
<p>对于申请页面中的增值服务【<strong>10分钟日本本地语音放题</strong>】建议不要勾选（因为通常用不上）。</p>
<p>之后按照说明提交申请并交钱即可。</p>
<p>关于实名认证部分，如果你是中国大陆居民，通常提交身份证和户口本/暂住证即33可。其他地区居民可以考虑水电单据这种。</p>
<p>手机卡通常会在数日后送达填写的地址上。</p>
<h3>CMLink激活</h3>
<p>日本手机卡_均需要在日本进行激活_， 所以这张卡必须落地日本方可使用。</p>
<p>具体的激活方法会在卡片随附的说明书，或此处<a href="https://www.cmlink.com/jp/zh/faq/" rel="noopener noreferrer" target="_blank">进行</a>查看。</p>
<h2>抽票</h2>
<p>与中国内通常采用的抢票方式有所不同，日本通常会使用抽选的方式进行票，而且通常还是分为多阶段抽取。抽票阶段分别为：</p>
<ul>
<li>FC 先行抽选
<ul>
<li>FC 先行抽选通常会要求加入对应艺人的 FanClub。如 HIMEHINA 的 JOJI Club（不是 Youtube 或者 Bilibili ）。加入 FC会收取月费，550日元/月。</li>
<li>加入 HIMEHINA 的FC 需要国际信用卡。</li>
<li>FC 是最优先抽选项，可能会含有其他抽选不会含有的特殊票种类。如优先入场的 S 票，巡演三场的通票等。</li>
</ul>
</li>
<li>抽选卷抽选，如购买BD。</li>
<li>票务平台及票务平台 VIP 抽选</li>
<li>一般购买
<ul>
<li>人气稍微火一点就没机会了。</li>
</ul>
</li>
</ul>
<p>本篇文章只考虑使用罗森进行抽选的情况。</p>
<p>抽选的入口，统一在演唱会宣传中，这次的巡演页面在这里：</p>
<div><a href="https://himehina.jp/pages/special?t=live2024_namidanokaori_tour" target="_blank" rel="noopener noreferrer"><div><div><div><img src="https://himehina.jp/resource/1068/559cccc8-3480-4e82-bed3-ef0101e74a17.png?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6XC9cL2hpbWVoaW5hLmpwXC9yZXNvdXJjZVwvMTA2OFwvNTU5Y2NjYzgtMzQ4MC00ZTgyLWJlZDMtZWYwMTAxZTc0YTE3LnBuZyIsIkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTc2NzcwNDI3MH19fV19&amp;Signature=euUvBX72p7zHEwzDXkh-s0FxlLq~qRO3kH~ErK1hZ4r2DXuE7JzGcqTa3e7G6CRNKkrOTsykwO31GVu4KDeM3GMC0zsVGQ0OU~qoKQJuH9ll1oy-YUKu6jkVxA7EOuNPiEjzGuMAmkegbTzB9h6Wee~-cAOPNRW-7sGLZ69DEH2xWr-LtXfLZqRcjeln1tbjbYItEQALMq1jzq2WOG70YGqiW34RbGG9lvLNW5pb7VjYI7lBKdi0IsDADwBR4F5dMnV7KeclfPkVDNBFMKrtI2fg9Dmx5GvnNnJJP3KjDuBSqeC8Guym3QRyHw17nX6Fagk28JJuCmHNGv0fEBYACg__&amp;Key-Pair-Id=K1WUY38TKRJ192" alt="" class="h-4 w-4 shrink-0 brightness-75 contrast-110" /><span>himehina.jp</span></div><div>
          HIMEHINA 全国ツアー2024(西)『涙の薫りがする』Sponsored by LIVE DAM AiR | ヒメヒナ公式WEB＆FCジョジ倶楽部
        </div><p>
          当サイトは、田中ヒメ鈴木ヒナの公式サイト、ファンクラブになります。ファンクラブならではのオリジナルコンテンツを会員様だけに随時配信中！会員受付中です！
        </p><div><span>https://himehina.jp/pages/special?t=live2024_namidanokaori_tour</span></div></div><div><img src="https://himehina.jp/images/aurora_bg.png" alt="Preview" class="h-full w-full object-cover brightness-75 contrast-110" /></div></div></a></div>
<p>参加抽选按钮的日语为：「お申し込みはこちら」，如下图：</p>
<img src="https://img.23h.at/i/2024/11/09/lmfq8g.png" alt="お申し込みはこちら" />
<p>通常来讲，一段时间能参与的抽选有且只有一个，需要合理安排。</p>
<h2>准备购物车</h2>
<blockquote><p>[!danger] 关于网络
日本的抽票，尤其是罗森的，是一个非常抽象的抽票系统。抽票全程请全程 <strong>挂好日本 VPN</strong>。</p></blockquote>
<div><div><div></div><div>WARNING</div></div><div><p>由于罗森系统的 BUG，所以参加抽选请使用智能手机，或使用浏览器的手机模式。</p></div></div>
<p>这次抽选以罗森抽选而_非 FC 抽选_为 Demo 展示，但是流程基本一样。</p>
<p>之后选择想要抽取的场次或票的类型：</p>
<img src="https://img.23h.at/i/2024/11/09/j552ee.png" alt="" />
<p>日本抽票有点像是国内的高考填志愿，你可以_同时_选择多种票种，然后选择按照第一第二志愿的顺序排序。</p>
<p>一路下一步后，即可进入添加志愿的部分：</p>
<img src="https://img.23h.at/i/2024/11/09/j5555t.png" alt="" />
<p>这里可以选择要抽取多少张票，这里的抽票的数量和中选概率没有什么关系。</p>
<img src="https://img.23h.at/i/2024/11/09/j54zge.png" alt="" />
<p>之后可以点击<code>转到申请</code>进入申请页面，或者返回购物车，添加第二种票。</p>
<p>如下图就是成功了两个志愿的结果：</p>
<img src="https://img.23h.at/i/2024/11/09/j54usq.png" alt="" />
<p>在添加好所有的票之后，点击转到申请，会要求你来登录 （ローチケ）Lawson 票务的账号。</p>
<h2>账号注册</h2>
<p>这里请选择「ローシンWEB会員登録」（Lawson Web 会员登录）进行账号注册。</p>
<p>账号的注册并不复杂，下面是一些常见的翻译和填写方法：</p>
<ul>
<li><strong>マールアドレス:</strong> 电子邮箱</li>
<li><strong>パスコード:</strong> 密码</li>
<li><strong>パスコード確認：</strong> 确认密码</li>
<li><strong>文字認証:</strong> 验证码</li>
</ul>
<p>提交后，请前往你确认的邮箱确认邮箱的，即点击邮箱里的链接即可。</p>
<p>之后会要求你再次输入一遍密码。</p>
<p>之后进入填写个人信息的阶段。这里建议不要瞎填写或者伪造身份。否则一旦出现问题，很难维权。下面仍然是一些个人信息的填写方法：</p>
<ul>
<li><strong>氏名（漢字）</strong>：繁体的你的名字</li>
<li><strong>氏名（カナ）</strong>: 片假名形式的名字
<ul>
<li>对于不知道自己名字日语写法的可以使用 <a href="https://namehenkan.com/?ref=23h.at" rel="noopener noreferrer" target="_blank">Name變換君</a></li>
</ul>
</li>
<li><strong>邮编和住址</strong>：实在不知道怎么搞就去网上搜一个日本转运用他们的地址就好。</li>
</ul>
<p>之后提交即可。</p>
<h2>继续提交抽票申请</h2>
<p>如果遇到了这种验证，自行翻译红框里的东西点选即可。</p>
<p>之后系统会让你提交支付信息。作为没有旅日身份的同学，唯一的方法就是信用卡（クレジットカード）：</p>
<img src="https://img.23h.at/i/2024/11/09/j54n8t.png" alt="" />
<p>下面请填写可以在日本使用的日本手机号。</p>
<div><div><div></div><div>注意</div></div><div><p>请务必确保可以携带插入有这张手机号的手机卡的手机进入会场！！</p></div></div>
<p>之后则会要求你填写你的信用卡信息。</p>
<p>信用卡你可以选择使用招商银行的 JCB 信用卡，减少扣款失败概率（曾有倒霉蛋因为信用卡被拒付导致明明中票却落选）</p>
<img src="https://img.23h.at/i/2024/11/09/j54rqh.png" alt="" />
<p>这里分别是：</p>
<ul>
<li><strong>カード番号:</strong> 信用卡号码</li>
<li><strong>有效期限:</strong> 信用卡的有效期</li>
<li><strong>セキュリティーコード:</strong> 安全码
<ul>
<li>也就是卡片后面的三位或四位数字。</li>
</ul>
</li>
</ul>
<p>之后点击下一步并完成信用卡验证（如有）即可。</p>
<h2>总结</h2>
<p>抽票本身其实并不难。主要难点还是集中在手机号、信用卡和抽票相关软件的使用。</p>
<p>如果这些部分对你造成了困难，可以考虑从国内其他酒姬民中搞二手票。</p>]]></content>
    <category term="HIMEHINA" />
    <category term="LIVE" />
    <category term="旅游" />
  </entry>
  <entry>
    <title>Prism.JS 实现黑暗模式切换</title>
    <link href="https://23h.at//posts/primsa-dark-mode" rel="alternate" type="text/html"/>
    <id>https://23h.at//posts/primsa-dark-mode</id>
    <updated>2024-10-18T00:00:00.000Z</updated>
    <published>2024-10-18T00:00:00.000Z</published>
    <author>
      <name>Monolith</name>
    </author>
    <summary type="text">在 Ghost 中仅使用 CSS 来实现 黑暗模式的切换</summary>
    <content type="html"><![CDATA[<img src="https://23h.at/_astro/cover.B0fbW1JR_Z24iezG.webp" alt="Prism.JS 实现黑暗模式切换" style="width: 100%; height: auto; margin-bottom: 1em;" />
<h2>说明</h2>
<p>Prism 的主题部分和做代码分析的部分，实际上是分开。</p>
<p>从文件上看，能简单理解为:</p>
<ul>
<li>JS 部分会负责整理识别各语言，整理并将内部的代码进行分类，打上对应的标签。</li>
<li>CSS部分负责染色</li>
</ul>
<p>既然染色部分是独立的，那么自然也可以实现黑暗模式的颜色切换了。</p>
<h2>⚠️使用 AI 得到的糟糕方案</h2>
<p>由于并没有修改 Prism 的经验，所以在询问 GPT 后得到了一个令人_匪夷所思_的答案:</p>
<pre><code>&lt;link rel="stylesheet" id="prism-theme" href="path/to/prism-light-theme.css"&gt;

&lt;script&gt;
  const darkTheme = 'path/to/prism-dark-theme.css';
  const lightTheme = 'path/to/prism-light-theme.css';

  if (window.matchMedia &amp;&amp; window.matchMedia('(prefers-color-scheme: dark)').matches) {
    // 用户偏好 Dark Mode，使用 Dark 主题
    document.getElementById('prism-theme').setAttribute('href', darkTheme);
  }

  // 监听系统主题切换事件
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event =&gt; {
    const theme = event.matches ? darkTheme : lightTheme;
    document.getElementById('prism-theme').setAttribute('href', theme);
  });
&lt;/script&gt;
</code></pre>
<p>之所以说的上是匪夷所思，是因为这个方案的核心思想，使用不同的 schema，然后监听对应的时间切换，来实现。这个方案看起来实现倒是简单，但是小问题非常多：</p>
<ul>
<li>每次进行切换的时候，由于时间的延后，以及需要重新加载 CSS，所以代码框的绘制永远是延后的。</li>
<li>这个方案默认是监听<code>prefers-color-schema</code>这个系统事件的，对于一些通过修改 html 属性的手动切换，代码的侵入性实在是过大。</li>
</ul>
<p>这个方案，在最后不得不废弃。</p>
<h2>重写 CSS</h2>
<p>尽管这个说起来十分简单，但是不知道为什么，在网上并没有找到现成可以用的相关代码（可能大家都没兴趣？）</p>
<p>在多方查阅后，某个 Codepen 提供的代码改动给了我一些启发。并完成了如下的内容：</p>
<pre><code>/* PrismJS - used for code highlighting */

[class*="language-"] ::selection {
  color: #000;
  text-shadow: none;
}

.token.important,
.token.bold {
  font-weight: bold;
}
.token.italic {
  font-style: italic;
}
.token.entity {
  cursor: help;
}
.namespace {
  opacity: 0.7;
}

/* Default colors (Light mode) */
:not(pre) &gt; code[class*="language-"],
pre[class*="language-"] {
  --color: #33a;
  --textShadow: #fff;
  --comment: #6e6e6e;
  --punctuation: #4e4e4e;
  --property: #905;
  --operator: #70b;
  --selector: #487b00;
  --url: #8d6640;
  --urlBg: hsla(0, 0%, 100%, .5);
  --boolean: #905;
  --atrule: #0075a8;
  --keyword: #0075a8;
  --function: #c93654;
  --regex: #860;
  --boxShadow: hsla(0,0%,0%,.3);
  --prism-background: #f5f2f0;

}


/* If the OS has dark mode set then... */
/* Change dark to light to test */

@media (prefers-color-scheme: dark) {
  html:not([data-theme="light"]) code[class*="language-"]:not([contenteditable]),
  html:not([data-theme="light"]) pre[class*="language-"] {
    --color: #6ae;
    --textShadow: #000;
    --comment: #9ab;
    --punctuation: #999;
    --property: #e70;
    --operator: #d7f;
    --selector: #8b2;
    --url: #cde;
    --urlBg: rgba(0,0,0,.5);
    --boolean: #a8f;
    --atrule: #f00;
    --keyword: #f00;
    --function: #f55;
    --regex: #f91;

    --boxShadow: #000;
    --prism-background: #f5f2f0;
  }
}


/* Manual switch mode - where implemented */
html[data-theme="dark"] code[class*="language-"]:not([contenteditable]),
html[data-theme="dark"] pre[class*="language-"] {

  /* Exactly the same as prefers-color-scheme: dark */
  --color: #6ae;
  --textShadow: #000;
  --comment: #9ab;
  --punctuation: #999;
  --property: #e70;
  --operator: #d7f;
  --selector: #8b2;
  --url: #cde;
  --urlBg: rgba(0,0,0,.5);
  --boolean: #8af;
  --atrule: #ffb;
  --keyword: #fe6;
  --function: #f55;
  --regex: #f91;
  --prism-background: #2a2a2a;
  --boxShadow: #000;

}

.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
  color: var(--comment);
}
.token.punctuation {
  color: var(--punctuation);
}
.token.property,
.token.symbol,
.token.tag,
.token.constant,
.token.deleted {
  color: var(--property);
}
.token.boolean,
.token.number {
  color: var(--boolean);
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
  color: var(--selector);
}
.token.operator {
  color: var(--operator);
}
.token.url,
.token.entity,
.language-css .token.string,
.style .token.string {
  color: var(--url);
  background-color: var(--urlBg);
}
.token.atrule,
.token.attr-value {
  color: var(--atrule);
}
.token.keyword {
  color: var(--keyword);
}
.token.function {
  color: var(--function);
}
.token.regex,
.token.important,
.token.variable {
  color: var(--regex);
}
:not(pre) &gt; code {
  background: #f5f2f0;
  padding: 2px 0;
}
html[data-theme="dark"] :not(pre) &gt; code {
  background-color: #2a2a2a;
}

:not(pre) &gt; code[class*="language-"],
pre[class*="language-"] {
  background: var(--prism-background);
}


@media print {
  code[class*="language-"],
  pre[class*="language-"] {
    text-shadow: none;

  }
}

</code></pre>
<p>这段代码主要还是通过声明具体的 class 的颜色为CSS变量来实现的。通过监听html 标签中<code>data-theme</code>属性是否为<code>dark</code>来判断当前使用的主题颜色。</p>
<p>这个方案是本站目前正在使用的方案，相比重新加载 CSS 会有更好的性能和显示效果。</p>]]></content>
    <category term="PrismJS" />
    <category term="Ghost" />
  </entry>
</feed>