<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>zty012</title><description>No description</description><link>https://2y.nz/</link><language>zh_CN</language><item><title>Linux 蓝牙音频突然断流的解决方案</title><link>https://2y.nz/p/linux-bluetooth-audio-dropouts/</link><guid isPermaLink="true">https://2y.nz/p/linux-bluetooth-audio-dropouts/</guid><description>彻底解决 Linux 使用蓝牙音频设备时出现的断流问题</description><pubDate>Sat, 17 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;ERTM (Enhanced Re-Transmission Mode) 本意是为了在丢包时通过重传保证可靠性。但在 Linux 环境下，由于驱动实现不佳或环境干扰（如 USB 3.0 噪声），协议栈会陷入重传循环，导致实时性要求极高的音频流（A2DP）被阻塞。表现出来就是：连接没断，但没声音了。&lt;/p&gt;
&lt;h2&gt;解决方法&lt;/h2&gt;
&lt;p&gt;只需要设置一个内核模块参数，禁用 ERTM 即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;options bluetooth disable_ertm=1
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>给 MicroPython 的模块添加类型提示，提升开发体验</title><link>https://2y.nz/p/mpy-types/</link><guid isPermaLink="true">https://2y.nz/p/mpy-types/</guid><description>本文介绍如何为 MicroPython 的模块添加类型提示，以提升代码的可读性和开发体验，涵盖类型提示的基本概念、具体实现方法以及示例代码。</description><pubDate>Sat, 08 Nov 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://micropython-stubs.readthedocs.io/en/main/index.html&quot;&gt;MicroPython-Stubs&lt;/a&gt; 是一个比较新的、活跃的项目，旨在为 MicroPython 的模块添加 pyi 存根文件，从而为 MicroPython 提供类型提示支持。&lt;/p&gt;
&lt;h2&gt;安装&lt;/h2&gt;
&lt;p&gt;这里我使用 uv 来管理依赖，当然 venv 也是可以的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uv init
uv add micropython-esp32-stubs
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置编辑器&lt;/h2&gt;
&lt;h3&gt;Zed&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;lsp&quot;: {
    &quot;pyright&quot;: {
      &quot;settings&quot;: {
        &quot;python.analysis&quot;: {
          &quot;extraPaths&quot;: [
            &quot;./.venv/Lib/site-packages&quot;,
            &quot;./.venv/lib/python3.12/site-packages&quot;,
            &quot;./.venv/lib/python3.13/site-packages&quot;,
            &quot;./.venv/lib/python3.14/site-packages&quot;
          ],
          &quot;typeCheckingMode&quot;: &quot;basic&quot;,
          &quot;diagnosticSeverityOverrides&quot;: {
            &quot;reportMissingModuleSource&quot;: false
          }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;VSCode&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;python.analysis.extraPaths&quot;: [
    &quot;./.venv/Lib/site-packages&quot;,
    &quot;./.venv/lib/python3.12/site-packages&quot;,
    &quot;./.venv/lib/python3.13/site-packages&quot;,
    &quot;./.venv/lib/python3.14/site-packages&quot;
  ],
  &quot;python.analysis.typeCheckingMode&quot;: &quot;basic&quot;,
  &quot;python.analysis.diagnosticSeverityOverrides&quot;: {
    &quot;reportMissingModuleSource&quot;: &quot;none&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;跋&lt;/h2&gt;
&lt;p&gt;经过如上配置后，编辑器就能识别 MicroPython 模块的类型提示了，从而提升代码的可读性和开发体验。希望本文对你有所帮助，祝你在 MicroPython 的开发旅程中一切顺利！&lt;/p&gt;
</content:encoded></item><item><title>使用 Docker Compose 部署 Discourse</title><link>https://2y.nz/p/discourse-docker-compose/</link><guid isPermaLink="true">https://2y.nz/p/discourse-docker-compose/</guid><description>本文介绍如何使用 Docker Compose 来部署 Discourse 论坛软件，涵盖环境准备、配置文件编写以及启动服务的步骤。</description><pubDate>Sun, 26 Oct 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Discourse 是很好用的论坛框架，但是它不能用 Docker Compose 来管理，本文介绍一种使用 Docker Compose 来部署它的方法。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt; 文件&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;version: &quot;3.8&quot;
services:
  redis:
    image: redis:7-alpine
    restart: always
  postgres:
    # 原版 postgres 镜像不支持 pgvector 扩展，所以需要用这个镜像
    image: pgvector/pgvector:0.8.1-pg18-trixie
    restart: always
    environment:
      POSTGRES_DB: discourse
      POSTGRES_USER: discourse
      POSTGRES_PASSWORD: ${DB_PASS}
    volumes:
      - postgres-data:/var/lib/postgresql/data
  discourse:
    # VMWare 的 Bitnami 镜像已经转收费了，这里用 public.ecr.aws 上的免费版
    image: public.ecr.aws/bitnami/discourse:latest
    restart: always
    depends_on:
      - postgres
      - redis
    environment:
      DISCOURSE_HOSTNAME: ${DOMAIN}
      DISCOURSE_DATABASE_NAME: discourse
      DISCOURSE_DATABASE_USER: discourse
      DISCOURSE_DATABASE_PASSWORD: ${DB_PASS}
      DISCOURSE_DATABASE_HOST: postgres
      DISCOURSE_REDIS_HOST: redis
      DISCOURSE_SMTP_HOST: ${SMTP_HOST}
      DISCOURSE_SMTP_PORT: ${SMTP_PORT}
      DISCOURSE_SMTP_USER: ${SMTP_USER}
      DISCOURSE_SMTP_PASSWORD: ${SMTP_PASS}
      DISCOURSE_SMTP_PROTOCOL: tls
      DISCOURSE_EMAIL: ${ADMIN_EMAIL}
      DISCOURSE_POSTGRESQL_EXTENSIONS: unaccent,pg_trgm,btree_gist
      DISCOURSE_SKIP_BOOTSTRAP: &quot;yes&quot; # 稍后再进行初始化
      BITNAMI_DEBUG: true
    volumes:
      - discourse-data:/bitnami/discourse
    # 暴露端口
    # ports:
    #  - &quot;3000:3000&quot;
    # 或者用 Traefik
    # labels:
    #  - &quot;traefik.enable=true&quot;
    #  - ...
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置环境变量&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;DOMAIN=forum.example.com
DB_PASS=your_db_password
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your_smtp_user
SMTP_PASS=your_smtp_password
ADMIN_EMAIL=admin@example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;启动服务&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;创建管理员账号&lt;/h2&gt;
&lt;p&gt;进入容器，运行以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd /opt/bitnami/discourse
RAILS_ENV=production bundle exec rake admin:create
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完成&lt;/h2&gt;
&lt;p&gt;至此，Discourse 已经成功部署完成，可以通过浏览器访问 &lt;code&gt;http://forum.example.com&lt;/code&gt; 来使用论坛了。&lt;/p&gt;
&lt;h2&gt;配置 Github OAuth&lt;/h2&gt;
&lt;p&gt;跟着&lt;a href=&quot;https://meta.discourse.org/t/configure-github-login-for-discourse/13745&quot;&gt;文档&lt;/a&gt;操作就行，但是注意 &lt;code&gt;Redirect URI&lt;/code&gt; 不能用 &lt;code&gt;https://&lt;/code&gt;，得写成类似 &lt;code&gt;http://forum.example.com/auth/github/callback&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>编译 Python 0.9.1 版本</title><link>https://2y.nz/p/compile-python-091/</link><guid isPermaLink="true">https://2y.nz/p/compile-python-091/</guid><pubDate>Fri, 26 Sep 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;首先下载源码: &lt;a href=&quot;https://www.python.org/ftp/python/src/Python-0.9.1.tar.gz&quot;&gt;Python-0.9.1.tar.gz&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;修改 Makefile&lt;/h2&gt;
&lt;p&gt;将 56 行左右的 &lt;code&gt;For BSD&lt;/code&gt; 注释掉，将 &lt;code&gt;For System V&lt;/code&gt; 取消注释&lt;/p&gt;
&lt;h2&gt;编译&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;make clean
make CFLAGS=&quot;-ansi -Wno-implicit-int -Wno-implicit-function-declaration&quot;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>向 IANA 申请新的 Media Type (MIME Type) 全流程</title><link>https://2y.nz/p/register-media-type/</link><guid isPermaLink="true">https://2y.nz/p/register-media-type/</guid><description>向 IANA 申请一个 Media Type，这样就可以让别的软件识别你的文件格式</description><pubDate>Fri, 29 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文以 &lt;code&gt;application/vnd.project-graph&lt;/code&gt; 为例，这是一个基于 ZIP 和 MessagePack 的文件格式&lt;/p&gt;
&lt;p&gt;建议使用 &lt;code&gt;application/vnd.&lt;/code&gt; 前缀，其他（比如 &lt;code&gt;prs.&lt;/code&gt;）可能会被拒绝，&lt;code&gt;x-&lt;/code&gt; 前缀已经被废弃&lt;/p&gt;
&lt;h2&gt;前期准备&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;一个邮箱：用于与 IANA 进行沟通&lt;/li&gt;
&lt;li&gt;标准文档：必须用英文撰写，描述你的文件格式的细节 (&lt;a href=&quot;https://project-graph.top/docs/spec/prg&quot;&gt;example&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;风险评估：说明你的文件格式的安全性和潜在风险&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;填写表单&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]
如果提交失败，你输入的东西不会丢失&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;打开 &lt;a href=&quot;https://www.iana.org/form/media-types&quot;&gt;IANA Media Types Registration Form&lt;/a&gt;，填写以下信息：&lt;/p&gt;
&lt;h3&gt;Name&lt;/h3&gt;
&lt;p&gt;最好填写真实的姓名，提高社区对你的信任度&lt;/p&gt;
&lt;h3&gt;Email&lt;/h3&gt;
&lt;p&gt;填写你的邮箱地址，最好写一个长期可用的邮箱&lt;/p&gt;
&lt;h3&gt;Type Name&lt;/h3&gt;
&lt;p&gt;即斜杠前面的部分，比如 &lt;code&gt;application&lt;/code&gt;、&lt;code&gt;text&lt;/code&gt;、&lt;code&gt;image&lt;/code&gt; 等&lt;/p&gt;
&lt;h3&gt;Subtype Name&lt;/h3&gt;
&lt;p&gt;下拉框中选择 &lt;code&gt;vnd.&lt;/code&gt;，输入框中填写你的子类型名称，比如 &lt;code&gt;project-graph&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Required Parameters 和 Optional Parameters&lt;/h3&gt;
&lt;p&gt;比如有一个类型 &lt;code&gt;text/html; charset=UTF-8&lt;/code&gt;，其中 &lt;code&gt;charset&lt;/code&gt; 就是一个 Required Parameter&lt;/p&gt;
&lt;p&gt;如果不需要填写 &lt;code&gt;N/A&lt;/code&gt; 即可&lt;/p&gt;
&lt;h3&gt;Encoding Considerations&lt;/h3&gt;
&lt;p&gt;文件使用的编码方式&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;选项&lt;/th&gt;
&lt;th&gt;是否二进制&lt;/th&gt;
&lt;th&gt;字符范围&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;7bit&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;ASCII&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8bit&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;UTF-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;binary&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;frame&lt;/code&gt; 选项不用管，因为我也不知道是干嘛的&lt;/p&gt;
&lt;h3&gt;Security Considerations&lt;/h3&gt;
&lt;p&gt;描述你的文件格式的安全性和潜在风险，至少需要涵盖以下几点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;说明媒体类型是否包含活动或可执行内容。如果媒体类型确实包含可执行内容，请说明已采取哪些措施来确保它可以安全执行，例如沙箱、安全作集、签名内容等。&lt;/li&gt;
&lt;li&gt;说明媒体类型中包含的信息是否需要隐私或完整性服务。&lt;/li&gt;
&lt;li&gt;如果对 （2） 的答案是肯定的，请详细说明媒体类型本身提供的任何隐私或完整性服务。如果它不提供此类服务，请解释应如何在外部提供这些服务，例如通过使用 SSL/TLS。&lt;/li&gt;
&lt;li&gt;如果媒体类型使用现有格式（e.g. XML 或 JSON），则必须引用该格式的安全注意事项，并且必须描述与该格式的使用相关的任何问题，例如 XML 可扩展性。
a. 如果介质类型采用压缩，则必须涵盖与该用法相关的安全考虑。
b. 如果媒体类型采用容器格式，例如 ZIP，则需要描述与该使用相关的任何问题。&lt;/li&gt;
&lt;li&gt;如果媒体类型包含必须引用的链接才能正确解释该类型，则应注意这一点。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Compression Bombs: A PRG file may contain a small ZIP that decompresses to an extremely large amount of data, causing denial-of-service. Implementations MUST impose reasonable limits on the number of extracted files and the total uncompressed size.
Path Traversal: Maliciously crafted ZIP entries could have names like ../../../some_important_file. Implementations MUST NOT extract files to filesystem, instead, read them directly from the ZIP stream to memory or a controlled environment.
Attachment Risks: The attachments directory can contain any file type. The application processing the PRG file is responsible for handling each attachment in a secure manner (e.g., run script files in sandbox, detect malware in attachments).
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Interoperability Considerations&lt;/h3&gt;
&lt;p&gt;描述你的文件格式的互操作性（即别的软件操作你的文件）考虑&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This media type defines a format for representing complex node graphs, such as project dependency graphs.
The content is a ZIP-archived container that holds one `stage.msgpack` file serialized using MessagePack, and may have other file in any format. This combination provides efficient storage and fast parsing.
The structure of the MessagePack-serialized data within the archive MUST conform to the schema defined in [https://project-graph.top/docs/spec/prg]
Key considerations for implementers include:
- Handling of required and optional properties gracefully (e.g., ignoring unknown optional properties rather than failing).
- Being aware that the graph may contain cycles and must be processed accordingly to avoid infinite loops.
- The possibility of very large graphs, requiring streaming or chunked processing strategies.
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Published specification&lt;/h3&gt;
&lt;p&gt;填写可公开访问的标准文档链接&lt;/p&gt;
&lt;h3&gt;Application Usage&lt;/h3&gt;
&lt;p&gt;哪个软件用了这个格式，一般填写你自己的软件即可&lt;/p&gt;
&lt;h3&gt;Fragment Identifier Considerations&lt;/h3&gt;
&lt;p&gt;描述 Anchor (&lt;code&gt;#&lt;/code&gt;) 符号后面的内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Fragment identifier considerations: UUIDs separated by `;`
See section 6.5 in the specification
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Provisional Registrations&lt;/h3&gt;
&lt;p&gt;选 &lt;code&gt;No&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Additional Information&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Deprecated alias names for this type: &lt;code&gt;N/A&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Magic number(s): 填写文件的魔数，比如 ZIP 的 &lt;code&gt;PK\x03\x04&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;File extension(s): 文件的扩展名，比如 &lt;code&gt;.prg&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Macintosh file type code(s): &lt;code&gt;N/A&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Object Identifier(s) or OID(s): &lt;code&gt;N/A&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Intended Usage&lt;/h3&gt;
&lt;p&gt;选 &lt;code&gt;COMMON&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;Other Information &amp;amp; Comments&lt;/h3&gt;
&lt;p&gt;不用管&lt;/p&gt;
&lt;h3&gt;Contact Person 和 Author/Change Controller&lt;/h3&gt;
&lt;p&gt;这个 Media Type 的联系人，邮箱一定要长期可用！&lt;/p&gt;
&lt;h3&gt;提交表单&lt;/h3&gt;
&lt;p&gt;检查无误后，点 &lt;code&gt;Lodge request&lt;/code&gt; 提交表单&lt;/p&gt;
&lt;h2&gt;收到邮件&lt;/h2&gt;
&lt;p&gt;不出意外，凌晨 1 点左右你会收到来自 &lt;code&gt;iana-mime@iana.org&lt;/code&gt; 的邮件，内容大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Dear &amp;lt;x&amp;gt;,

Thank you for contacting us.

When we send media type requests to the IESG-designated expert for review, we typically copy a publicly-viewable mailing list. Is that OK for this request, or does this need to be handled privately?

If we do copy the list, we&apos;ll wait to send you any feedback until the expert determines what (if anything) should be passed along.

Best regards,

&amp;lt;y&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接回复 OK&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hi &amp;lt;y&amp;gt;,

That’s fine--please proceed with copying the public list.

Thanks for checking.

Best regards,
&amp;lt;x&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;专家审核&lt;/h2&gt;
&lt;p&gt;接下来会收到另一封来自 &lt;code&gt;iana-mime@iana.org&lt;/code&gt; 的邮件，内容大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hi &amp;lt;x&amp;gt;,

I&apos;ve sent this on to the expert (a volunteer designated by the IESG) and the mailing list. Thanks!

Best regards,
&amp;lt;y&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后打开 &lt;a href=&quot;https://mailarchive.ietf.org/arch/&quot;&gt;Mailing List&lt;/a&gt;，搜索 &lt;code&gt;media-types&lt;/code&gt;，不出意外会看到一封关于你的 Media Type 的邮件，内容大致如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hi &amp;lt;z&amp;gt;,

Would you be able to review this new request by &amp;lt;date&amp;gt;?

thanks,
&amp;lt;y&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;专家会在 &lt;code&gt;&amp;lt;date&amp;gt;&lt;/code&gt; 这一天之前处理你的请求&lt;/p&gt;
&lt;h2&gt;to be continued&lt;/h2&gt;
</content:encoded></item><item><title>将 Docker 数据迁移到 /data 数据盘</title><link>https://2y.nz/p/migrate-docker-data/</link><guid isPermaLink="true">https://2y.nz/p/migrate-docker-data/</guid><description>本文介绍如何将 Docker 的数据目录从默认位置迁移到新的数据盘（如 /data），以便更好地管理存储空间。</description><pubDate>Thu, 28 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在使用 Docker 的过程中，默认情况下，Docker 会将其数据存储在 &lt;code&gt;/var/lib/docker&lt;/code&gt; 目录下。如果你的系统盘空间有限，或者你希望将 Docker 数据存储在一个更大的数据盘上，可以通过以下步骤将 Docker 数据迁移到新的位置（例如 &lt;code&gt;/data/docker&lt;/code&gt;）&lt;/p&gt;
&lt;p&gt;本文以 &lt;code&gt;/data/docker&lt;/code&gt; 作为新的数据目录为例&lt;/p&gt;
&lt;h2&gt;停止 Docker 服务&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl stop docker.socket docker containerd
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;迁移数据&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo rsync -aqxP /var/lib/docker/ /data/docker/
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;配置 Docker 使用新目录&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;data-root&quot;: &quot;/data/docker&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除或重命名旧数据目录（可选）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo mv /var/lib/docker /var/lib/docker.bak
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;启动 Docker 服务&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;sudo systemctl start containerd docker.socket docker
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用 pyftsubset 压缩字体</title><link>https://2y.nz/p/pyftsubset/</link><guid isPermaLink="true">https://2y.nz/p/pyftsubset/</guid><description>使用 pyftsubset 工具来压缩字体文件，减少文件大小并提高加载速度。</description><pubDate>Thu, 21 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;首先需要安装 &lt;code&gt;fonttools&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;只保留数字和大写字母：&lt;code&gt;pyftsubset ./DINPro-Bold_13934.ttf --unicodes=&quot;U+0030-0039,U+0041-005A,U+002E&quot; --flavor=woff2 --with-zopfli&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>为何按钮应该使用默认光标样式</title><link>https://2y.nz/p/buttons-should-use-default-cursor/</link><guid isPermaLink="true">https://2y.nz/p/buttons-should-use-default-cursor/</guid><description>从历史的角度探讨为何按钮不应该使用手形光标，而是应该使用默认的光标样式</description><pubDate>Mon, 11 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;一些开发者和用户认为，当用户的光标放在按钮上时，应该使用手形光标来表示按钮是可点击的&lt;/p&gt;
&lt;p&gt;其实不然&lt;/p&gt;
&lt;h2&gt;历史&lt;/h2&gt;
&lt;p&gt;「按钮」在 GUI 诞生之初就具有边框或阴影效果，和现实世界中按钮相似，表明它们可以和现实中的按钮一样可以被点击&lt;/p&gt;
&lt;p&gt;早在 Web 时代之前，「超链接」就已经诞生了，使用下划线和不同颜色来表示，但是发出的「可点击」信号还是比较弱，就有了手形光标作为提示&lt;/p&gt;
&lt;p&gt;所以得出结论：「按钮」就应该用默认光标，「链接」才要用手形光标&lt;/p&gt;
&lt;h2&gt;shadcn/ui&lt;/h2&gt;
&lt;p&gt;在 shadcn/ui 的 Tailwind v4 版本中，将按钮的光标样式改为了默认&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/shadcn-ui/ui/issues/6843&quot;&gt;#6743&lt;/a&gt; 中至今还在讨论该决定&lt;/p&gt;
</content:encoded></item><item><title>GitHub 秒过学生认证！</title><link>https://2y.nz/p/github-student/</link><guid isPermaLink="true">https://2y.nz/p/github-student/</guid><description>GitHub 教育包可以获得免费 Copilot Pro，以及各种优惠</description><pubDate>Fri, 08 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;修改 Billing information&lt;/h2&gt;
&lt;p&gt;打开 &lt;a href=&quot;https://github.com/settings/billing/payment_information&quot;&gt;Settings -&amp;gt; Billing and licensing -&amp;gt; Payment information&lt;/a&gt; 页面，修改 &lt;code&gt;Billing information&lt;/code&gt; 中的 &lt;code&gt;First name&lt;/code&gt; 和 &lt;code&gt;Last name&lt;/code&gt; 为你的姓名&lt;/p&gt;
&lt;p&gt;:::important
需要重新登录 GitHub！
:::&lt;/p&gt;
&lt;h2&gt;学生证和翻译版的文本&lt;/h2&gt;
&lt;p&gt;创建一个文本文档，写入学生证翻译成英文的内容&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Student ID

School: xxxx
Name: xxx
Admission Date: xxx
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;需要有关键词 &lt;code&gt;Student ID&lt;/code&gt;, &lt;code&gt;School&lt;/code&gt;, &lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;Admission Date&lt;/code&gt;，&lt;code&gt;Name&lt;/code&gt; 必须与刚才 &lt;code&gt;Billing information&lt;/code&gt; 中填写的姓名一致&lt;/p&gt;
&lt;h2&gt;开始申请&lt;/h2&gt;
&lt;p&gt;打开 &lt;a href=&quot;https://github.com/settings/education/benefits&quot;&gt;Settings -&amp;gt; Billing and licensing -&amp;gt; Education benefits&lt;/a&gt;，点击 &lt;code&gt;Start an application&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;学校名称必须与刚才翻译的学生证中的 &lt;code&gt;School&lt;/code&gt; 一致，其他信息按需填写&lt;/p&gt;
&lt;p&gt;证明选择第一个，然后拍照的时候把学生证和电脑上翻译版的文本一起拍&lt;/p&gt;
&lt;h2&gt;未通过&lt;/h2&gt;
&lt;p&gt;可以在翻译的学生证中加一些其他的字段，比如 &lt;code&gt;Expires at&lt;/code&gt; 等等，然后重新申请&lt;/p&gt;
</content:encoded></item><item><title>在 Next.js 中使用 Primer UI</title><link>https://2y.nz/p/nextjs-primer/</link><guid isPermaLink="true">https://2y.nz/p/nextjs-primer/</guid><description>以 Brand UI 为例，介绍如何在 Next.js 项目中集成 Primer UI 组件库，并且无缝兼容 next-themes 主题系统</description><pubDate>Fri, 08 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;安装 Primer&lt;/h2&gt;
&lt;p&gt;首先需要安装 Product UI 或者 Brand UI，详见 Primer 文档，本文以 Brand UI 为例&lt;/p&gt;
&lt;h2&gt;创建 &lt;code&gt;ThemeProvider&lt;/code&gt; 组件&lt;/h2&gt;
&lt;p&gt;由于 Primer UI 的 &lt;code&gt;ThemeProvider&lt;/code&gt; 是客户端组件，所以不能直接加入到 &lt;code&gt;layout.tsx&lt;/code&gt; 中，得自己写一个&lt;/p&gt;
&lt;p&gt;:::note
也可以直接使用 Primer 提供的 &lt;code&gt;ThemeProvider&lt;/code&gt; 组件，只要能提供 &lt;code&gt;data-color-mode&lt;/code&gt; 属性即可
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;use client&quot;;

import { useTheme } from &quot;next-themes&quot;;
import { ReactNode, useEffect } from &quot;react&quot;;

export default function ThemeProvider({ children }: { children: ReactNode }) {
  const { theme } = useTheme();

  useEffect(() =&amp;gt; {
    document.body.dataset.colorMode = theme;
  }, [theme]);

  return children;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用 &lt;code&gt;ThemeProvider&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;然后在 &lt;code&gt;layout.tsx&lt;/code&gt; 中使用这个 Provider 就可以了&lt;/p&gt;
</content:encoded></item><item><title>使用 Gemini CLI 全自动生成文档</title><link>https://2y.nz/p/docs-by-gemini/</link><guid isPermaLink="true">https://2y.nz/p/docs-by-gemini/</guid><description>借助 Gemini 方便的 CLI 工具，编写 Shell 脚本，自动为项目内所有文件生成文档</description><pubDate>Thu, 07 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;首先需要安装 Gemini CLI，不多赘述&lt;/p&gt;
&lt;p&gt;然后编写一个 Bash 脚本:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
set -e

model=&quot;gemini-2.5-flash&quot;
out=&quot;./docs/services&quot;
mkdir -p &quot;$out&quot;

for file in $(grep -rl &quot;^@service&quot; app/src/core); do
  relative=${file#app/src/core/}
  relative=&quot;app/src/core/$relative&quot;
  service=$(grep -oP &apos;@service\(&quot;\K[^&quot;]+(?=&quot;\))&apos; &quot;$file&quot;)
  service=$(echo &quot;$service&quot; | sed -E &apos;s/([a-z0-9])([A-Z])/\1-\L\2/g&apos; | tr &apos;[:upper:]&apos; &apos;[:lower:]&apos;)

  doc=&quot;$out/$service.zh-CN.mdx&quot;
  [[ -e $doc ]] &amp;amp;&amp;amp; { echo &quot;$(tput setaf 3)文件 $doc 已存在，跳过&quot;; continue; }

  prompt=$(cat &amp;lt;&amp;lt;EOF
下面是一段 TypeScript 源码。请完成三件事，用三行“---”作为分隔符输出：

1. 用中文概述这个服务的用途，要求同以前（不要称“类”，分段，善用标题，不要一级标题，API 方法也要标题）。
2. 给这个服务起一个 lucide 的大驼峰图标名。
3. 把服务原始大驼峰名翻译成中文，格式：“[原文的大驼峰形式][空格][译文]”。

不要输出多余的解释或空白。

example:
用于管理用户会话，跟踪登录状态与权限……

## API

### \`login(userId: string): Promise&amp;lt;void&amp;gt;\`

……

### \`logout(): Promise&amp;lt;void&amp;gt;\`

……
---
UserCog
---
UserService 用户服务
end example.

---
$(cat &quot;$file&quot;)
EOF
  )

  tput setaf 4
  echo &quot;正在生成 $service.zh-CN.mdx, $relative&quot;
  tput setaf 8

  # 一次请求拿到三段
  raw=$(gemini -p &quot;$prompt&quot; -m &quot;$model&quot;)

  # 用 awk 按 &quot;---&quot; 拆分
  doc_body=$(echo &quot;$raw&quot; | awk -v RS=&apos;---&apos; &apos;NR==1{print; exit}&apos;)
  icon=$(echo      &quot;$raw&quot; | awk -v RS=&apos;---&apos; &apos;NR==2{gsub(/[[:space:]]/, &quot;&quot;); print}&apos;)
  title=$(echo     &quot;$raw&quot; | awk -v RS=&apos;---&apos; &apos;NR==3{gsub(/^[[:space:]]+|[[:space:]]+$/, &quot;&quot;); print}&apos;)

  tput setaf 2
  echo &quot;标题: $title&quot;
  echo &quot;图标: $icon&quot;

  cat &amp;gt; &quot;$doc&quot; &amp;lt;&amp;lt;EOF
---
title: $title
icon: $icon
relatedFile: $relative
---

$doc_body
EOF
done
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;脚本会找到项目内所有使用 &lt;code&gt;@service&lt;/code&gt; 装饰器的 TypeScript 文件，并获取服务名和文件路径&lt;/li&gt;
&lt;li&gt;调用 Gemini CLI，生成文档内容、标题、图标&lt;/li&gt;
&lt;li&gt;保存到 &lt;code&gt;.md&lt;/code&gt; 文件，并添加 &lt;code&gt;relatedFile&lt;/code&gt; frontmatter，指向源文件的相对路径&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;See Also&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/graphif/project-graph/blob/31f067a9cc67e6c0ada05dad257bbda5f591eacf/utils/generate-service-docs.sh&quot;&gt;源文件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;/p/tput&quot;&gt;tput 颜色速查表&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>tput 颜色速查表</title><link>https://2y.nz/p/tput/</link><guid isPermaLink="true">https://2y.nz/p/tput/</guid><description>这下忘不了 tput setaf 的数字对应颜色了</description><pubDate>Thu, 07 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;重置: &lt;code&gt;tput sgr0&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;:::warning
颜色会随终端配色方案变化，可能不准确，以上图片为 tty 中的颜色
:::&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;在终端中快速查看颜色表&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;colors() {
  color(){
    for c; do
        printf &apos;\e[48;5;%dm%03d&apos; $c $c
    done
    printf &apos;\e[0m \n&apos;
  }

  IFS=$&apos; \t\n&apos;
  color {0..15}
  for ((i=0;i&amp;lt;6;i++)); do
      color $(seq $((i*36+16)) $((i*36+51)))
  done
  color {232..255}
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>@graphif/serializer 引用机制的原理</title><link>https://2y.nz/p/serializer-ref/</link><guid isPermaLink="true">https://2y.nz/p/serializer-ref/</guid><description>利用对象引用机制，压缩对象序列化后的数据，用时间换空间</description><pubDate>Wed, 06 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
算法有待优化，不建议学习
:::&lt;/p&gt;
&lt;h2&gt;概念&lt;/h2&gt;
&lt;p&gt;在一个对象中，经常会有「两个键指向相同的对象」的情况，这种情况下，可以将第二个键的值设为一个特殊的对象以节省空间，即「引用」。&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;@graphif/serializer&lt;/code&gt; 以前的版本中，没有考虑到相同的对象可以被优化。&lt;/p&gt;
&lt;p&gt;现在，既然我想到了，就把它做出来。&lt;/p&gt;
&lt;h2&gt;哪些对象可以被优化？&lt;/h2&gt;
&lt;p&gt;并不能粗暴地将所有相同的对象都优化，只有那些「比较复杂」的对象才有可能被优化。&lt;/p&gt;
&lt;p&gt;比如，我这里通过「字符串值总长度」和「键值对数量」两个指标来判断一个对象是否比较复杂。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function isComplex(obj: Record&amp;lt;string, any&amp;gt;): boolean {
  const stringLengthTotal = Object.values(obj)
    .filter((v) =&amp;gt; typeof v === &quot;string&quot;)
    .reduce((acc, v) =&amp;gt; acc + v.length, 0);
  return Object.keys(obj).length &amp;gt; 5 || stringLengthTotal &amp;gt;= 50;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;「引用」如何表示？&lt;/h2&gt;
&lt;p&gt;在 &lt;code&gt;@graphif/serializer&lt;/code&gt; 中，用 &lt;code&gt;$&lt;/code&gt; 作为引用的标志。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[{ &quot;hello&quot;: &quot;world&quot; }, { &quot;$&quot;: &quot;/0&quot; }]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的例子中，第二个对象引用了第一个对象。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;$&lt;/code&gt; 的值是一个路径，类似 XPath，但是全部使用 &lt;code&gt;/&lt;/code&gt; 分隔，数组也是。&lt;/p&gt;
&lt;h2&gt;怎么知道对象对应的路径？&lt;/h2&gt;
&lt;p&gt;首先生成一个「对象 ID」，可以用任意算法生成，只要保证唯一性即可。&lt;/p&gt;
&lt;p&gt;例如，我使用 &lt;code&gt;md5&lt;/code&gt; 哈希算法和 &lt;code&gt;MsgPack&lt;/code&gt; 编码生成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const okey = md5(encode(result));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::note
&lt;code&gt;okey&lt;/code&gt; == &lt;code&gt;Object Key&lt;/code&gt;
:::&lt;/p&gt;
&lt;p&gt;然后，将「对象 ID」和「路径」对应起来即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const obj2path = new Map&amp;lt;string, string&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;跋&lt;/h2&gt;
&lt;p&gt;第一次做这种东西，尝试了几个小时才成功，效果还行。&lt;/p&gt;
&lt;p&gt;不管了，以后有时间再重新搞一遍吧。&lt;/p&gt;
</content:encoded></item><item><title>Linux 将程序放在后台运行的几种方法</title><link>https://2y.nz/p/linux-run-bg/</link><guid isPermaLink="true">https://2y.nz/p/linux-run-bg/</guid><description>&amp;, %, &amp;!, nohup, disown</description><pubDate>Tue, 05 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;放在后台运行，和 shell 同生共死&lt;/h2&gt;
&lt;p&gt;在命令末尾加上 &lt;code&gt;&amp;amp;&lt;/code&gt; 就能放在后台运行，会提示一个 job 编号&lt;/p&gt;
&lt;p&gt;使用命令 &lt;code&gt;%&lt;/code&gt; 或 &lt;code&gt;fg&lt;/code&gt; 加上 job 编号可以把程序拉回前台&lt;/p&gt;
&lt;p&gt;如果要重新放到后台，可以按 &lt;code&gt;Ctrl-Z&lt;/code&gt; 挂起，再用 &lt;code&gt;bg&lt;/code&gt; 命令放回后台&lt;/p&gt;
&lt;h2&gt;放在后台运行，不管 shell 进程了&lt;/h2&gt;
&lt;h3&gt;方法 1&lt;/h3&gt;
&lt;p&gt;在命令末尾加上 &lt;code&gt;&amp;amp;!&lt;/code&gt; 即可&lt;/p&gt;
&lt;h3&gt;方法 2&lt;/h3&gt;
&lt;p&gt;在命令前加上 &lt;code&gt;nohup&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;方法 3&lt;/h3&gt;
&lt;p&gt;先用 &lt;code&gt;&amp;amp;&lt;/code&gt; 放到后台，然后用 &lt;code&gt;disown&lt;/code&gt; 命令把它从当前 shell 进程中分离&lt;/p&gt;
</content:encoded></item><item><title>Array.reduce() 会修改原数组！</title><link>https://2y.nz/p/js-reduce-clone/</link><guid isPermaLink="true">https://2y.nz/p/js-reduce-clone/</guid><description>神秘 Bug 居然是 Array.reduce() 导致的</description><pubDate>Tue, 05 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;get(index: number) {
  // 先获取从0到index（包含index）的所有patch
  const patches = this.patches.slice(0, index + 1);
  // 从initialStage开始应用patch，得到在index时刻的舞台序列化数据
  const data = patches.reduce((acc, patch) =&amp;gt; {
    return applyPatch(acc, patch).newDocument;
  }, this.initialStage);
  // 反序列化得到舞台对象
  const stage = deserialize(data, this.project);
  console.log(&quot;get %d = %o (patches %o)&quot;, index, stage, patches);
  return stage;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码用来获取在 &lt;code&gt;index&lt;/code&gt; 时刻的舞台数据&lt;/p&gt;
&lt;p&gt;看似没问题对吧，运行一下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[INFO] get 0 = [ {} ] (patches: [
  { op: &apos;add&apos;, path: &apos;/0&apos;, value: [ {} ] },
])
[INFO] get 0 = [ {}, {} ] (patches: [
  { op: &apos;add&apos;, path: &apos;/0&apos;, value: [ {} ] },
])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;诶，结果应该是一样的，为什么第二次运行结果多了一个 &lt;code&gt;{}&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;参考 &lt;a href=&quot;https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce#%E6%9C%89%E5%88%9D%E5%A7%8B%E5%80%BC%E6%97%B6_reduce_%E5%A6%82%E4%BD%95%E8%BF%90%E8%A1%8C&quot;&gt;MDN「&lt;code&gt;Array.prototype.reduce()&lt;/code&gt; # 有初始值时 reduce() 如何运行」&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果给 &lt;code&gt;reduce()&lt;/code&gt; 提供了初始值引用，那么第一个参数提供的函数的返回值将会修改初始值&lt;/p&gt;
&lt;p&gt;所以得拷贝一份 &lt;code&gt;initialStage&lt;/code&gt; 作为初始值，以 lodash 为例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;get(index: number) {
  // 先获取从0到index（包含index）的所有patch
  const patches = this.patches.slice(0, index + 1);
  // 从initialStage开始应用patch，得到在index时刻的舞台序列化数据
  const data = patches.reduce((acc, patch) =&amp;gt; {
    return applyPatch(acc, patch).newDocument;
  }, _.cloneDeep(this.initialStage));
  // 反序列化得到舞台对象
  const stage = deserialize(data, this.project);
  console.log(&quot;get %d = %o (patches %o)&quot;, index, stage, patches);
  return stage;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就不会修改原数组了，问题解决&lt;/p&gt;
</content:encoded></item><item><title>WebKitGTK 与 GPU 加速</title><link>https://2y.nz/p/webkitgtk-gpu/</link><guid isPermaLink="true">https://2y.nz/p/webkitgtk-gpu/</guid><description>让 WebKitGTK 强制使用 GPU 加速，以提升渲染性能</description><pubDate>Mon, 04 Aug 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;添加环境变量即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;WEBKIT_FORCE_COMPOSITING_MODE=1
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>更换头像</title><link>https://2y.nz/p/change-avatar/</link><guid isPermaLink="true">https://2y.nz/p/change-avatar/</guid><description>我的头像终于换了</description><pubDate>Thu, 31 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;原来的头像是 QQ 内置的动态头像&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/img/avatar-old.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;看腻了，于是打算自己搞一个新的头像，新头像就是页面左边那个&lt;/p&gt;
&lt;p&gt;其实这不是渐变，而是我随便用笔刷画的&lt;/p&gt;
</content:encoded></item><item><title>脑子一热，写了个睡眠记录应用</title><link>https://2y.nz/p/sleep/</link><guid isPermaLink="true">https://2y.nz/p/sleep/</guid><description>市面上所有睡眠记录应用都不支持 NFC 记录，于是自己动手写了一个</description><pubDate>Sun, 20 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;使用 Jetpack Compose 编写 UI&lt;/p&gt;
&lt;h2&gt;原理&lt;/h2&gt;
&lt;p&gt;NFC 的原理和支付宝碰一下相同，都是通过 &lt;a href=&quot;https://developer.android.com/develop/connectivity/nfc/nfc#aar&quot;&gt;Android 应用记录 (AAR)&lt;/a&gt; 实现的&lt;/p&gt;
&lt;p&gt;每一条记录有 &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;bedTime&lt;/code&gt;, &lt;code&gt;wakeUpTime&lt;/code&gt; 三个字段，其中 &lt;code&gt;bedTime&lt;/code&gt; 是 nullable 的&lt;/p&gt;
&lt;p&gt;记录开始的时候，添加一条 &lt;code&gt;bedTime == null&lt;/code&gt; 记录，记录结束的时候，更新 &lt;code&gt;bedTime&lt;/code&gt; 字段为当前时间&lt;/p&gt;
&lt;h2&gt;甘特图&lt;/h2&gt;
&lt;p&gt;甘特图是这个软件的特色功能，可以直观地看到睡眠记录的趋势&lt;/p&gt;
&lt;p&gt;:::tip
图中的短线表示 0:00 时间点
:::&lt;/p&gt;
&lt;h2&gt;为什么不用系统自带的？&lt;/h2&gt;
&lt;p&gt;因为算法算出来的肯定不准确&lt;/p&gt;
&lt;h2&gt;为什么不用手环？&lt;/h2&gt;
&lt;p&gt;为了这一个功能而买不值得&lt;/p&gt;
</content:encoded></item><item><title>使用 Coolify 部署前端应用</title><link>https://2y.nz/p/coolify/</link><guid isPermaLink="true">https://2y.nz/p/coolify/</guid><description>放弃有诸多限制 Cloudflare Pages，转移到自部署的 Coolify 平台</description><pubDate>Thu, 17 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;无论是使用 Vercel 还是 Cloudflare Pages 部署，都存在一些限制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;请求数量限制&lt;/li&gt;
&lt;li&gt;应用大小限制&lt;/li&gt;
&lt;li&gt;无法部署 Github 组织中的仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;于是我找到了 Coolify，这是一个 Docker 容器管理器（类似 1Panel），并且得益于 Nixpacks 强大的自动检测框架能力，可以轻松地部署任意类型的应用&lt;/p&gt;
&lt;h2&gt;安装 Coolify&lt;/h2&gt;
&lt;p&gt;只需运行一条命令即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后打开网页，创建一个账号后，就可以开始部署应用了&lt;/p&gt;
&lt;h2&gt;配置 Github App&lt;/h2&gt;
&lt;p&gt;虽然不配置也可以部署，但是配置后可以获得更多的功能，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 Push 代码时自动部署&lt;/li&gt;
&lt;li&gt;支持部署 Private 仓库&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;参考官方文档: &lt;a href=&quot;https://coolify.io/docs/knowledge-base/git/github/integration#with-github-app-recommended&quot;&gt;Github Integration # With GitHub App&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;创建项目&lt;/h2&gt;
&lt;p&gt;选择左侧导航栏中的 Projects，然后点击 Add 按钮，输入项目名称，然后创建&lt;/p&gt;
&lt;h2&gt;创建资源&lt;/h2&gt;
&lt;p&gt;在 Resources 页面，点击 New 按钮&lt;/p&gt;
&lt;p&gt;如果创建了 Github App，选择 &lt;code&gt;Private Repository (with GitHub App)&lt;/code&gt;，否则选择 &lt;code&gt;Public Repository&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;输入项目的仓库地址，模板选择 &lt;code&gt;Nixpacks&lt;/code&gt;，然后点击创建&lt;/p&gt;
&lt;p&gt;创建完成把资源页面中的 &lt;code&gt;Ports Exposes&lt;/code&gt; 改为应用监听的端口，如 &lt;code&gt;3000&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;配置域名&lt;/h2&gt;
&lt;p&gt;给域名添加一条 A 记录，指向服务器，Cloudflare SSL 需要改为灵活模式&lt;/p&gt;
&lt;p&gt;将资源页面中的 &lt;code&gt;Domains&lt;/code&gt; 选项改为 FQDN (Fully Qualified Domain Name)，格式如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;scheme&amp;gt;://&amp;lt;host&amp;gt;:&amp;lt;port&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;scheme&lt;/code&gt;: 如果使用 Cloudflare 等自带 SSL 的 CDN，则为 &lt;code&gt;http&lt;/code&gt;；否则为 &lt;code&gt;https&lt;/code&gt;，Coolify 会自动生成 SSL 证书；如果不需要 SSL，则为 &lt;code&gt;http&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;host&lt;/code&gt;: 域名，如 &lt;code&gt;example.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;port&lt;/code&gt;: &lt;strong&gt;应用监听&lt;/strong&gt;的端口，如 &lt;code&gt;3000&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;部署应用&lt;/h2&gt;
&lt;p&gt;直接点击资源页面右上角的 Deploy，等待部署完成即可&lt;/p&gt;
</content:encoded></item><item><title>脑子一热，写了个英语词典</title><link>https://2y.nz/p/dic/</link><guid isPermaLink="true">https://2y.nz/p/dic/</guid><description>整个界面只有一个搜索框。支持一词多义、例句、输入补全</description><pubDate>Wed, 09 Jul 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;使用 Jetpack Compose 编写 UI，数据来自 ████，使用 Ksoup 解析 HTML&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;fleeksoft/ksoup&quot;}&lt;/p&gt;
</content:encoded></item><item><title>Vinsa T605 Linux 驱动</title><link>https://2y.nz/p/vinsa-t605/</link><guid isPermaLink="true">https://2y.nz/p/vinsa-t605/</guid><description>通过 udev 规则自动配置数位板为电脑模式</description><pubDate>Sun, 01 Jun 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;:::warning
只适用于 16:9 比例的屏幕
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SUBSYSTEM==&quot;usb&quot;, ATTR{idVendor}==&quot;08f2&quot;, ATTR{idProduct}==&quot;6811&quot;, MODE=&quot;0666&quot;, RUN+=&quot;/usr/local/bin/tablet_setup.py&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env python3
import usb1
import time
import sys
import subprocess

VENDOR_ID = 0x08F2
PRODUCT_ID = 0x6811

# 通过逆向 Android 设置工具得到
NORMAL_MODE_REPORTS = [
    [0x08, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
    [0x08, 0x01, 0x00, 0xFF, 0xF0, 0x00, 0xFF, 0xF0]
]

def reset_usb():
    try:
        subprocess.run(
            [&quot;usbreset&quot;, f&quot;{VENDOR_ID:04x}:{PRODUCT_ID:04x}&quot;],
            capture_output=True, text=True
        )
        time.sleep(2)
    except:
        pass

def main():
    reset_usb()
    with usb1.USBContext() as ctx:
        d = ctx.openByVendorIDAndProductID(VENDOR_ID, PRODUCT_ID, skip_on_error=True)
        if not d:
            return 1
        i = 2
        try:
            if d.kernelDriverActive(i):
                try: d.detachKernelDriver(i)
                except: pass
            d.claimInterface(i)
            for r in NORMAL_MODE_REPORTS:
                try:
                    d.controlWrite(0x21, 0x09, 0x0300 | r[0], i, bytes(r), 1000)
                except: pass
                time.sleep(0.3)
            d.controlWrite(0x21, 0x09, 0x0308, i, bytes([0x08, 0x01, 0, 0, 0, 0, 0, 0]), 1000)
        except:
            return 1
        finally:
            try: d.releaseInterface(i)
            except: pass
            try: d.attachKernelDriver(i)
            except: pass
            reset_usb()
    return 0

if __name__ == &quot;__main__&quot;:
    sys.exit(main())
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>某个软件的验证码</title><link>https://2y.nz/p/exam-code-2024/</link><guid isPermaLink="true">https://2y.nz/p/exam-code-2024/</guid><description>？？？</description><pubDate>Sun, 22 Dec 2024 00:00:00 GMT</pubDate><content:encoded>&lt;ol&gt;
&lt;li&gt;0210&lt;/li&gt;
&lt;li&gt;6811&lt;/li&gt;
&lt;li&gt;2453&lt;/li&gt;
&lt;li&gt;0329&lt;/li&gt;
&lt;li&gt;0917&lt;/li&gt;
&lt;li&gt;1211&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>利用正则删除所有 console.log</title><link>https://2y.nz/p/remove-console-log/</link><guid isPermaLink="true">https://2y.nz/p/remove-console-log/</guid><description>利用正则表达式删除所有 console.log，以提高应用性能</description><pubDate>Sat, 23 Nov 2024 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;(// )?console\.log\((.|\n)*?\);?
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>在 Android 应用中隐藏状态栏和导航栏</title><link>https://2y.nz/p/android-fullscreen/</link><guid isPermaLink="true">https://2y.nz/p/android-fullscreen/</guid><description>使用 Kotlin 实现全屏显示 Activity</description><pubDate>Sun, 29 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;修改 theme 文件&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;resources&amp;gt;
  &amp;lt;style name=&quot;AppTheme.FullScreen&quot; parent=&quot;Theme.AppCompat.DayNight.NoActionBar&quot;&amp;gt;
    &amp;lt;item name=&quot;android:windowFullscreen&quot;&amp;gt;true&amp;lt;/item&amp;gt;
    &amp;lt;item name=&quot;android:windowLayoutInDisplayCutoutMode&quot;&amp;gt;shortEdges&amp;lt;/item&amp;gt;
    &amp;lt;item name=&quot;android:windowActionBar&quot;&amp;gt;false&amp;lt;/item&amp;gt;
    &amp;lt;item name=&quot;android:windowNoTitle&quot;&amp;gt;true&amp;lt;/item&amp;gt;
    &amp;lt;item name=&quot;android:windowTranslucentNavigation&quot;&amp;gt;true&amp;lt;/item&amp;gt;
    &amp;lt;item name=&quot;android:navigationBarColor&quot;&amp;gt;@android:color/transparent&amp;lt;/item&amp;gt;
  &amp;lt;/style&amp;gt;
&amp;lt;/resources&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;修改 MainActivity.kt&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;import android.os.Bundle
import android.view.WindowInsets
import android.view.WindowInsetsController
import android.view.WindowManager
import androidx.core.view.WindowCompat

class MainActivity {
  override fun onCreate(savedInstanceState: Bundle?) {
    // ...

    WindowCompat.setDecorFitsSystemWindows(window, false)
    window.decorView.windowInsetsController?.let { controller -&amp;gt;
      controller.hide(WindowInsets.Type.statusBars() or WindowInsets.Type.navigationBars())
      controller.systemBarsBehavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
    }
    window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
  }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>使用 FFmpeg 录制应用的声音</title><link>https://2y.nz/p/record-app-sound/</link><guid isPermaLink="true">https://2y.nz/p/record-app-sound/</guid><description>使用 FFmpeg 和 PulseAudio 录制应用的声音，无需第三方软件</description><pubDate>Thu, 26 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;$ ffmpeg -f pulse -i default output.wav

ffmpeg version n7.0.2 Copyright (c) 2000-2024 the FFmpeg developers
Press [q] to stop, [?] for help
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后就可以在应用里播放声音了，按 q 结束录制&lt;/p&gt;
</content:encoded></item><item><title>Android Platform Tools 相关环境变量</title><link>https://2y.nz/p/android-sdk-env/</link><guid isPermaLink="true">https://2y.nz/p/android-sdk-env/</guid><description>Android SDK 和 NDK 相关的环境变量</description><pubDate>Tue, 24 Sep 2024 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;ANDROID_HOME=/home/&amp;lt;user&amp;gt;/Android/Sdk
ANDROID_NDK=/opt/android-ndk
NDK_HOME=/opt/android-ndk
ANDROID_NDK_HOME=/opt/android-ndk
ANDROID_NDK_ROOT=/opt/android-ndk
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>GSI 资源库记录</title><link>https://2y.nz/p/gsi-download/</link><guid isPermaLink="true">https://2y.nz/p/gsi-download/</guid><description>下载 Android 通用系统镜像</description><pubDate>Fri, 02 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;LineageOS: https://sourceforge.net/projects/andyyan-gsi/files&lt;/p&gt;
&lt;p&gt;其他系统: https://sourceforge.net/projects/misterztr-gsi/files&lt;/p&gt;
</content:encoded></item><item><title>在 Git 历史记录中移除一个文件</title><link>https://2y.nz/p/git-remove-file/</link><guid isPermaLink="true">https://2y.nz/p/git-remove-file/</guid><description>不小心把一个大文件提交到了 Git 仓库</description><pubDate>Sun, 28 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;首先安装 &lt;a href=&quot;https://github.com/newren/git-filter-repo/blob/main/INSTALL.md&quot;&gt;git-filter-repo&lt;/a&gt; 插件&lt;/p&gt;
&lt;p&gt;然后需要把仓库重新 clone 一份到本地&lt;/p&gt;
&lt;p&gt;运行以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git filter-repo --invert-paths --path &amp;lt;文件的路径&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后强制推送远端&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git push -f
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>curl 配置代理</title><link>https://2y.nz/p/curl-proxy/</link><guid isPermaLink="true">https://2y.nz/p/curl-proxy/</guid><description>在不使用 TUN 模式的情况下让 curl 使用代理服务器</description><pubDate>Fri, 05 Jul 2024 00:00:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;proxy = host:port
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>记录</title><link>https://2y.nz/p/notes/</link><guid isPermaLink="true">https://2y.nz/p/notes/</guid><description>一些小东西</description><pubDate>Fri, 01 Jan 1999 00:00:00 GMT</pubDate><content:encoded>&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://jkchao.github.io/typescript-book-chinese/&quot;&gt;《深入理解 TypeScript》&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.gradle.org/current/userguide/compatibility.html&quot;&gt;Gradle 对应 Java 版本&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://learn.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11#sample-code-for-detecting-arm-or-x86&quot;&gt;从 UA 获取系统架构&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://graphemica.com/&quot;&gt;Unicode 字符&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://tailwind-color-finder.vercel.app/&quot;&gt;16 进制转 Tailwind 颜色&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;无损压缩视频: &lt;code&gt;ffmpeg -i input.mp4 -c:v libx264 -preset slower -crf 23 -c:a copy output.mp4&lt;/code&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>