MoreRSS

site iconAmeow | 阿猫修改

后端工程师,写Go 和 Python,运营「猫鱼周刊」。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

Ameow | 阿猫的 RSS 预览

猫鱼周刊 vol. 095 谨防赛博夺舍

2026-03-29 22:16:45

关于本刊

这是猫鱼周刊的第 96 期,本系列每周日更新,主要内容为每周收集内容的分享,同时发布在

博客:阿猫的博客-猫鱼周刊

RSS:猫鱼周刊

邮件订阅:猫鱼周刊

微信公众号:猫兄的和谐号列车

私信:[email protected]

INIT

好久不见。这张照片是某天下午下楼摸鱼的时候拍的,公司之前的楼下转角处有个茶餐厅,在角落这里搞了个小狗幼儿园,图片里的两位是店里的,时常能看到不少小狗在这里聚会玩耍。我不是很喜欢狗的人,但是在工作累了的时候下楼看到小狗在阳光下奔跑玩耍,还是很治愈的。以至于公司要搬走了,我的第一想法居然是,啊那我再也不能摸鱼下楼看小狗了。

Anyway,最近这段时间也挺忙的,开始学车,也尝试写了一下 iOS App。后面慢慢细聊。

STDIN

Apifox 供应链投毒攻击 — 完整技术分析

原文链接

Apifox 因未严格启用 sandbox 参数,并暴露了 Node.js 的 API 接口,导致攻击者可通过 JS 控制 Apifox 的终端,启动过程中加载了一个被投毒的脚本,采集了用户的 .ssh 目录以及命令行历史等。

本来我很久都不使用 Apifox 了,最近偶然做需求打开了几次,没想到就中招了。我写了一个脚本,根据文章的描述,识别是否有文章中提到的 IoC 和本地痕迹,以及本地有什么高风险的凭据材料需要轮换。运行之后发现我是妥妥中招了,所以这周一个非常蛋疼的事情就是找到一个方式可以避免这种攻击,然后轮换全部的密钥。

最近类似的事情还有 LiteLLM 也遭到了类似的供应链投毒攻击,这种针对开发者的攻击防不胜防,不管你使用商业软件还是开源软件都避免不了,防范的手段还是控制本地不要留存这种敏感凭据。另外有个大家可能都很容易忽视的点,那就是避免在命令行中明文输入密码,就是例如通过 -p xxx 这样去传递密码参数,这样你的密码就会留存在历史中,可以用环境变量或者 interactive 的方式去输入。

当 AI 陷入沉思:长时程思考下的有效交互设计研究

原文链接

当 LLM 出现思维链之后,大家发现如果让 AI 不断「自言自语」,可能会得到更加完善的结果,所以「深度思考」应运而生。对应的界面交互上,从一开始全部流式展示,到后面逐渐迭代出一些动画、结构化过程等等,这篇文章做了很深入的思考和研究。

不得不说,文章中多次提到 Claude,在实际使用中,我确实也觉得是 Claude 的使用体验最佳,不过这种交互体验实际上算是「锦上添花」,不算得上非常重要的选购因素。

另外我觉得有一个交互的点可能暂时没有人考虑到,以及由于现在龙虾类产品的出现,可能令这个问题加剧—— AI 什么时候完成了工作。作为「略懂」的人,我会知道结束的边界在于模型输出 EOS token,一般的实现就是到这里就不再刷新,然后气泡底部出现交互。这看起来没什么问题,但是复杂情况就在于,如果这时候用户让 AI 做一些很复杂的事情,但是 AI 并没有这个能力,却又像模像样地骗用户自己正在做 ppt,并且通过邮件发给他,不少用户信以为真,而且在发现 AI 做不到这个之后产生巨大的挫败感。对于传统的在线聊天式 AI,只要生成结束了,气泡不再刷新了,那就是结束了,再发消息就是一轮新的交互。而龙虾又复杂化了这个事情,因为它有后台进程,甚至不管你有没有发消息,它其实都在工作,所以这时候你发消息问进度是有意义的。

我们如何在 Cursor 中比较模型质量

原文链接

Cursor 团队搞了个 CursorBench 来评测不同的模型,任务来自真实的 Cursor 用量,而不是公开代码仓库,CursorBench 不仅更能区分不同模型,也比公开基准更贴近开发者的真实结果。他们发现现在的公开基准存在不少局限,在前沿水平上,这些基准已经无法区分那些对开发者而言实用价值截然不同的模型。他们构建的 CursorBench 会使模型的区分度更大,更加能体现 coding 场景的能力。

评测的结果倒是没什么特别意外的,GPT 5.4 最强,其次是 Opus 4.6,倒是声称对标 Opus 4.5 的 GLM-5 实际的差距还蛮大。比较意外之前传出来他们的 Composer 2 使用的微调 Kimi K2.5 居然没有出现在榜单里。

他们的实践说明,在实际使用的时候,还需要根据具体的场景来构建 benchmark,光看各种模型吹得天花乱坠意义不大,大家在公开基准上都很卷,你的实际场景用起来很可能又不是那样。

STDOUT

把 SSH 私钥放进 YubiKey

文章链接

在 Apifox 供应链投毒事件之后,我目前风险最大的就是本地使用的 SSH 密钥。这个东西自从配开发环境的时候就生成了,然后一直放在那里,很少再去管它。在跟 ChatGPT 探讨之后,它给我几个方案,要么使用例如 1Password SSH Agent 之类的方案去管理 SSH 密钥,要么使用 FIDO2 SSH key,或者用 Yubikey 的 PIV/OpenPGP。第一种方案需要另外投入成本,而 Yubikey 我手头就有好几个,用 GPG 签名也很久了,所以最终使用了 gpg-agent 的这种方案。另外值得一提的是,FIDO2 这种方式其实非常好,主流服务也支持,但 *-sk的公钥还是在很多地方有兼容性问题,所以用不了。

说实话,这个折腾起来还挺麻烦的,如果你之前没有 Yubikey,硬件投入也不少,然后把整套流程跑通也花了我大半天。之后每次使用的时候,还要把 Yubikey 插入电脑并输入 pin。好处是以后我大概再也不用担心 SSH 私钥被盗的事情了。

回应标题,如果你的 SSH 私钥就这么生成之后被遗忘在 .ssh,你很容易就被「赛博夺舍」,我这个方案不妨考虑一下。

Marked

尝试用 Swift 写了一个原生 iOS App,至此,前端后端安卓 iOS 我都有成型的作品了,虽然在这个过程中主要的功臣是 AI。总之,这是一个记录类型的 App,除了简单记录某件事件以外,还可以填入一些自定义字段,例如健身时练的部位、有氧的事件等等,后续可以根据自定义字段来做分析;还有一个有意思的功能是,可以记录「已经多久没做 x 事」这种戒断类型的事件,除了培养好习惯,戒掉坏习惯同样重要。如果你对 Marked 感兴趣,它现在还没有上架,你可以在 Testflight 体验。

顺便也分享一下我的开发经验。

一开始,我打算用 Figma AI 或者谷歌的 Stitch 之类先出一版设计稿,然后改下交互之类的,我最终选择了 Stitch 来尝试,主要是因为它的免费额度比较多,而且支持 MCP。我先尝试了直接在网站上输入我的简单需求,让他设计好界面以及对应的交互,但是发现它一开始只会生成首页的交互,里面具体的交互需要你一步一步跟他聊天才会有,而且具体的字段每个页面可能都不一样。倒是有一个很突出的点,它会先生成一个 DESIGN.md,所以所有界面都可以获得比较一致的设计风格。我的改进方式是先把我的初始需求写成一个 init.md,然后跟模型讨论,让他生成出一个 PRD,并且带有每一个页面的交互和字段的描述,再根据这个文档,让他调用 MCP 来生成页面,最终能生成出全部页面,字段设计等看起来也没什么大问题。

但是最后我实现的时候,发现这个设计稿完全没有作用,我的工作流程里不涉及 UI 设计,我的交互其实文字已经描述清楚了。更糟糕的是,这个设计稿里的每一个 screen 其实只是一个 html,Figma 的也是类似的设计,而我要写的是 Swift 的原生 UI,设计稿没法反映苹果原生的 UI,而且生成出来的 screen 经常出现一些无意义的边框或者评论等,修改起来也很费劲。

另外踩的一个坑是,有一个 Emoji 选择器,一开始用的是一个开源的包,但是在新版本 iOS 上有个 bug,会导致下面的类别选择器有异常。AI 卡了很久都没有解决,直到我在 GitHub issues 里找到一个对应的问题,并且顺藤摸瓜找到了一个修复的 fork 才解决。现在 AI 很大的问题就是经常遇到问题无法解决,需要人类介入。我觉得 Skill 也许是这个问题的解决方案,因为「开源的包遇到问题-去翻翻 issues」这个流程其实是我的经验,本质上这些操作它也可以完成的。另一方面大家也许可以不那么焦虑被 AI 取代,因为它们遇到小小的问题可能就会卡住。

MISC

Humanizer-zh

项目链接

一个用于去除文本中 AI 生成痕迹的 Skill。之前做了很多关于 AI 生成检测的研究,看到这个感觉还是挺有意思的,让 AI 生成更加拟人。

project-nomad

项目链接

一个离线的知识库,打包了 AI 助手、离线地图等工具,不过安装时仍然需要联网。整个项目的背景有点「末日风」,而且部署的成本还不低(根据项目的推荐硬件,甜点配置大概需要 500-800 刀,需要本地能跑 LLM),实用性不算很强。不过如果你已经花了很多钱搞末日地堡,这个价格估计你会承受得起(x。

CanIRun.ai

网站链接

一个自动检测你的硬件配置,并计算你的电脑能运行什么 AI 的工具网站。比较有意思的是,它除了会给你预估的 VRAM 需求,还会根据你的设备的具体硬件配置(VRAM、RAM、内存带宽等)估算不同量化下的输出速度,以及不同量化的效果来供你评估。

EOF

本周刊已在 GitHub 开源,欢迎 star。同时,如果你有好的内容,也欢迎投稿。如果你觉得周刊的内容不错,可以分享给你的朋友,让更多人了解到好的内容,对我也是一种认可和鼓励。(或许你也可以请我喝杯咖啡

另外,我建了一个交流群,欢迎入群讨论或反馈,可以通过文章头部的联系邮箱私信我获得入群方式。

把 SSH 私钥放进 YubiKey:一套更安全、可迁移的密钥管理方案

2026-03-27 02:48:38

前言

近期供应链投毒事件频发,针对开发者工作流的攻击也越来越常见。无论是 Apifox、LiteLLM 这类事件,还是其他面向本地开发环境、依赖链和工具链的攻击,防范起来都不容易。一个更稳妥的思路,是尽量不要让私钥以普通文件的形式长期留在本地设备中;如果把私钥放进独立的硬件里,即使本机环境被入侵,也能在一定程度上降低密钥失窃的风险。

YubiKey 是 Yubico 推出的一种硬件安全密钥,通常是一个小型的 USB 设备,也有支持 USB-A、USB-C、NFC 等不同接口和连接方式的型号。它本质上是一块专门用于保存和使用敏感凭据的安全硬件,可以用于网站登录的双重验证,也可以承载 OpenPGP、PIV、FIDO 等类型的密钥材料。

在 SSH 的场景下,使用 YubiKey 的核心价值在于:让私钥尽量不离开硬件设备本身。相比将私钥长期保存在电脑磁盘里,这种方式可以显著降低因主机被入侵、文件被窃取、备份泄露或误拷贝而造成的风险。日常使用时,你仍然可以像平时一样完成 GitHub 认证和服务器登录,但真正的签名操作是在 YubiKey 内部完成的,外部能够获取的只有公钥,而无法直接导出私钥。

本文介绍一套基于 OpenPGP 子钥的实践方案:通过离线保存主密钥、将子钥写入 YubiKey、准备多把备用设备,并结合定期续期与轮换机制,在提升 SSH 密钥安全性的同时,也兼顾多设备使用、硬件损坏后的恢复能力,以及日常认证的可维护性。限于篇幅,本文不会深入介绍过多背景知识,目的是让读者迅速走通流程,把自己的密钥保护起来。

前期准备

首先你需要准备一个 Yubikey 或者类似的硬件安全密钥。建议购买 Yubikey,型号为 Yubikey 5 NFC 和 Yubikey 5c NFC,区别是前者是 USB-A 接口而后者是 USB-C 接口。另一个值得关注的点是固件版本,Yubikey 的固件出厂之后就不可升级,不同固件对加密算法的支持不同,功能特性也有差别,详见Firmware Overview。常见可以购买到的版本一般为 5.4.3(前两年活动有优惠,我也囤了几个) 和 5.7.x(最新版本),在日常使用和本文所使用的功能来说区别不大。

另外,也有一些国产的平替可供选择(例如 CanoKey),但是这类产品的兼容性以及对应的文档、社区支持相对都比较差,不建议选择。

接下来的操作,有观点建议使用虚拟机或者干净安装的系统甚至无网的终端来完成,如果有条件,可以自行折腾。便捷来说,断网操作也可以了。

安装依赖

brew install gnupg yubikey-personalization ykman pinentry-mac wget

准备 Yubikey

$ gpg --edit-card

Reader ...........: Yubico YubiKey OTP FIDO CCID
Application ID ...: [MASKED]
Application type .: OpenPGP
Version ..........: 3.4
Manufacturer .....: Yubico
Serial number ....: [MASKED]
Name of cardholder: [not set]
Language prefs ...: [not set]
Salutation .......:
URL of public key : [not set]
Login data .......: [not set]
Signature PIN ....: not forced
Key attributes ...: rsa2048 rsa2048 rsa2048
Max. PIN lengths .: 127 127 127
PIN retry counter : 3 0 3
Signature counter : 0
KDF setting ......: off
UIF setting ......: Sign=off Decrypt=off Auth=off
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]

接下来先设置两个 pin,一个是平常解锁用的 pin,另一个是管理 pin(用于 --edit-card 的)。

gpg/card> admin
Admin commands are allowed

gpg/card> passwd
gpg: OpenPGP card no. [MASKED] detected

1 - change PIN
2 - unblock PIN
3 - change Admin PIN
4 - set the Reset Code
Q - quit

Your selection?

分别输入 1 和 3,根据提示来修改 pin,默认的 pin 为 123456,管理 pin 为 12345678,4 的恢复代码按需求设置(不是必须的)。然后也可以设置一下 name lang url 之类,如果你设置了会在 gpg --card-status 展示对应的信息,这个不是必须的。如果你有多个 Yubikey,重复以上操作即可,当然也可以不在一开始先设置好全部 Yubikey 的信息,在后面换卡的时候再设置也行。

生成密钥

$ gpg --expert --full-generate-key
gpg (GnuPG) 2.4.9; Copyright (C) 2025 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
   (7) DSA (set your own capabilities)
   (8) RSA (set your own capabilities)
   (9) ECC (sign and encrypt) *default*
  (10) ECC (sign only)
  (11) ECC (set your own capabilities)
  (13) Existing key
  (14) Existing key from card
Your selection? 9
Please select which elliptic curve you want:
   (1) Curve 25519 *default*
   (2) Curve 448
   (3) NIST P-256
   (4) NIST P-384
   (5) NIST P-521
   (6) Brainpool P-256
   (7) Brainpool P-384
   (8) Brainpool P-512
   (9) secp256k1
Your selection? 9
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0)
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: user
Email address: [email protected]
Comment:
You selected this USER-ID:
    "user <[email protected]>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O

输入 O 回车之后会弹出一个 passphrase 的框,建议设置一个密码。

We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: /tmp/gpg-5gJAI/trustdb.gpg: trustdb created
gpg: directory '/tmp/gpg-5gJAI/openpgp-revocs.d' created
gpg: revocation certificate stored as '/tmp/gpg-5gJAI/openpgp-revocs.d/EF5D701CAEB2283BB8D10305F43A691EE48B10EC.rev'
public and secret key created and signed.

pub   secp256k1 2026-03-26 [SC]
      EF5D701CAEB2283BB8D10305F43A691EE48B10EC
uid                      user <[email protected]>
sub   secp256k1 2026-03-26 [E]

这时候主密钥就生成好了,这里的 EF5D701CAEB2283BB8D10305F43A691EE48B10EC 就是你的主密钥的指纹,或者叫 key id。

$ PRIMARY_FPR=EF5D701CAEB2283BB8D10305F43A691EE48B10EC

这时候,可以执行一次备份:

$ gpg -a --export-secret-keys "$PRIMARY_FPR" > secret-master-only.asc
$ gpg -a --export "$PRIMARY_FPR" > public-master-only.asc

现在继续生成签名、加密和鉴权使用的子钥。

$ gpg --quick-add-key "$PRIMARY_FPR" ed25519 sign 1y
(会提示你输入 passphrase)
We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy.
$ gpg --quick-add-key "$PRIMARY_FPR" cv25519 encr 1y
$ gpg --quick-add-key "$PRIMARY_FPR" ed25519 auth 1y

执行完后,可以看到新建的带有效期的子钥:

$ gpg --list-keys --with-subkey-fingerprint --keyid-format LONG "$PRIMARY_FPR"
pub   secp256k1/F43A691EE48B10EC 2026-03-26 [SC]
      EF5D701CAEB2283BB8D10305F43A691EE48B10EC
uid                 [ultimate] user <[email protected]>
sub   secp256k1/291AE24AD1CC737D 2026-03-26 [E]
      6CFBDCCF3B478466149FE7E0291AE24AD1CC737D
sub   ed25519/0202CC8049E2FB80 2026-03-26 [S] [expires: 2027-03-26]
      D3C948F3854FF25B339294140202CC8049E2FB80
sub   cv25519/B936CF741340882E 2026-03-26 [E] [expires: 2027-03-26]
      C86408158C50D2C68A0C50D0B936CF741340882E
sub   ed25519/8472C3F353CD9B06 2026-03-26 [A] [expires: 2027-03-26]
      C1B5CC41426C03A0CC0DAFF78472C3F353CD9B06

到这一步我们再备份一次,这次的备份文件需要长期、保密妥善保管,尤其是 secret-full-with-subkeys.asc,最好可以写入一个离线存储里。

$ gpg -a --export-secret-keys "$PRIMARY_FPR" > secret-full-with-subkeys.asc
$ gpg -a --export "$PRIMARY_FPR" > public-full-with-subkeys.asc
$ gpg --export-ownertrust > ownertrust.txt

写入 Yubikey

接下来就是把刚才生成的密钥写入 Yubikey。这里的主要操作有两个,一个是 key n,选中的 key 会用星号标识,再输入一次 key n 取消选择;另一个是 keytocard ,会把私钥写入 Yubikey 对应的槽中。

$ gpg --edit-key "$PRIMARY_FPR"
gpg (GnuPG) 2.4.9; Copyright (C) 2025 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Secret key is available.

sec  secp256k1/F43A691EE48B10EC
     created: 2026-03-26  expires: never       usage: SC
     trust: ultimate      validity: ultimate
ssb  secp256k1/291AE24AD1CC737D
     created: 2026-03-26  expires: never       usage: E
ssb  ed25519/0202CC8049E2FB80
     created: 2026-03-26  expires: 2027-03-26  usage: S
ssb  cv25519/B936CF741340882E
     created: 2026-03-26  expires: 2027-03-26  usage: E
ssb  ed25519/8472C3F353CD9B06
     created: 2026-03-26  expires: 2027-03-26  usage: A
[ultimate] (1). user <[email protected]>

gpg> key 2

sec  secp256k1/F43A691EE48B10EC
     created: 2026-03-26  expires: never       usage: SC
     trust: ultimate      validity: ultimate
ssb  secp256k1/291AE24AD1CC737D
     created: 2026-03-26  expires: never       usage: E
ssb* ed25519/0202CC8049E2FB80
     created: 2026-03-26  expires: 2027-03-26  usage: S
ssb  cv25519/B936CF741340882E
     created: 2026-03-26  expires: 2027-03-26  usage: E
ssb  ed25519/8472C3F353CD9B06
     created: 2026-03-26  expires: 2027-03-26  usage: A
[ultimate] (1). user <[email protected]>

gpg> keytocard
Please select where to store the key:
   (1) Signature key
   (3) Authentication key
Your selection? 1
(按照提示输入 passphrase 和 admin pin)

在这里依次选择 key、keytocard、反选 key,直到全部 key 都写入成功。

注意,如果你需要把这一份密钥写入多个 Yubikey 中,在全部写完之后,直接按 Ctrl + C 退出,如果在这一步保存了,本地就不会再有私钥,只会留存一个 stub,实际的私钥已经在 Yubikey 里了,是无法导出的。这时候,拔出第一把 Yubikey,插入第二把 Yubikey,前面说的设置 pin 和信息可以在这一步来做,然后重复上面的流程即可,更多的 Yubikey 也同理。当你写到最后一张,确定可以继续了,就执行 save

gpg> save

现在再执行 gpg --card-status,就会看到对应的 key 前面有个 ssb>,代表密钥已经转移到 Yubikey 上;同时对应的签名、加密、鉴权三个 slot 都有对应的 key。

Reader ...........: Yubico YubiKey OTP FIDO CCID
Application ID ...: [masked]
Application type .: OpenPGP
Version ..........: 3.4
Manufacturer .....: Yubico
Serial number ....: [masked]
Name of cardholder: Leslie Leung
Language prefs ...: en
Salutation .......:
URL of public key : https://github.com/leslieleung.gpg
Login data .......: [not set]
Signature PIN ....: not forced
Key attributes ...: ed25519 cv25519 ed25519
Max. PIN lengths .: 127 127 127
PIN retry counter : 3 0 3
Signature counter : 0
KDF setting ......: off
UIF setting ......: Sign=off Decrypt=off Auth=off
Signature key ....: [masked]
      created ....: 2026-03-26 15:53:26
Encryption key....: [masked]
      created ....: 2026-03-26 15:53:45
Authentication key: [masked]
      created ....: 2026-03-26 15:54:00
General key info..: sub  ed25519/1915EA0F3C320F72 2026-03-26 Leslie Leung <[email protected]>
ssb>  ed25519/[masked] created: 2026-03-26  expires: 2027-03-26
                                card-no: [masked]
ssb>  cv25519/[masked]  created: 2026-03-26  expires: 2027-03-26
                                card-no: [masked]
ssb>  ed25519/[masked]  created: 2026-03-26  expires: 2027-03-26
                                card-no: [masked]

导出 SSH 密钥

在上面的 key 中,找到 A 字样的 key,这个代表授权,导出对应的 SSH key。

sub   ed25519/8472C3F353CD9B06 2026-03-26 [A] [expires: 2027-03-26]
      C1B5CC41426C03A0CC0DAFF78472C3F353CD9B06
$ gpg --export-ssh-key C1B5CC41426C03A0CC0DAFF78472C3F353CD9B06!
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINGvVhlehNBnCQMuNp5llACDgGYgtztvM9P66j9ReXkN openpgp:0x53CD9B06

这个公钥就是后续用来进行 SSH 登录的。

配置 SSH

编辑 ~/.gnupg/gpg-agent.conf

enable-ssh-support
default-cache-ttl-ssh 300
max-cache-ttl-ssh 7200

重启 agent。

$ gpgconf --reload gpg-agent

修改 .zshrc

export GPG_TTY=$(tty)
unset SSH_AGENT_PID
if [ "${gnupg_SSH_AUTH_SOCK_by:-0}" -ne $$ ]; then
  export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)"
fi
gpg-connect-agent /bye >/dev/null 2>&1

加载配置

$ source ~/.zshrc

GitHub 配置

点击右上角头像 - Settings - SSH and GPG keys,New SSH key,填入刚才生成的公钥,保存。

然后测试一下:

$ ssh -T [email protected]
(弹窗输入 pin)
Hi LeslieLeung! You've successfully authenticated, but GitHub does not provide shell access.

服务器配置

把对应的公钥添加到 ~/.ssh/authorized_keys,然后尝试用 ssh 登录即可。

子钥续期

一般情况下,如果密钥没有被泄露,不需要专门做什么维护。在子钥到期前,给子钥续期,然后导出更新后的公钥,在对应的机器或者网站上更新即可。这一步不需要使用 Yubikey 进行操作。

$ gpg --quick-set-expire <PRIMARY_FPR> 1y '*'
$ gpg -a --export <PRIMARY_FPR>

FAQ

忘记了 PIN

如果忘记了 pin 或者 admin pin,可以对 Yubikey 的 GPG 区进行重置,注意这会丢失里面存储的密钥。

$ ykman openpgp reset

多个 Yubikey 切换时,提示插入卡号 xx 的卡

执行以下操作:

$ gpg-connect-agent "scd serialno" "learn --force" /bye

最后

总的来说,把 SSH 密钥放进 YubiKey,并不是为了追求“更复杂”的配置,而是为了把最关键的私钥从日常使用环境中尽量隔离出来。在面对本机入侵、供应链投毒、误操作和密钥泄露这类现实风险时,这种做法虽然不能解决所有问题,但确实能把攻击者获取私钥的门槛提高很多。

更重要的是,YubiKey 的价值并不只体现在 SSH 上,它还可以承载 OpenPGP 密钥,用来做 GPG 签名,例如给 Git commit、tag、文件甚至邮件签名。签名的意义不只是证明内容是谁发出的,也能证明内容在签名之后没有被篡改;放在 Git 的场景里,它可以帮助团队确认某次提交确实来自你本人,而不是他人伪造了身份信息。

这套方案确实不算轻量,配置起来麻烦,理解成本也不低,但它换来的,是更强的私钥保护、更可信的身份认证,以及在混乱环境里更稳定的安全边界。很多时候,安全并不是让你永远不出事,而是让你在真正出事的时候,仍然能守住最关键的东西。从这个角度看,把 SSH 和签名能力都收敛到 YubiKey 里,或许正是一种值得长期坚持的做法。

References

GitHub - drduh/YubiKey-Guide: Community guide to using YubiKey for GnuPG and SSH - protect secrets with hardware crypto. · GitHub

猫鱼周刊 vol. 094 龙虾大跃进

2026-03-09 00:33:29

关于本刊

这是猫鱼周刊的第 95 期,本系列每周日更新,主要内容为每周收集内容的分享,同时发布在

博客:阿猫的博客-猫鱼周刊

RSS:猫鱼周刊

邮件订阅:猫鱼周刊

微信公众号:猫兄的和谐号列车

私信:[email protected]

INIT

又是一周,这周感觉做的自己的事情并不多,总体还是被工作消耗得比较厉害。首先是给之前做的 AI 文本生成检测 Aletheia 增加了一个新模型 DivEye,它的原理非常有意思,不需要针对新的大模型来做训练。另外就是尝试用本地的 qwen3.5-35b-a3b 模型写了一篇科幻短篇小说《2136 年,人类发明了超光速引擎》,效果出奇地好,整篇文章没什么 AI 味,逻辑、立意等都说得过去,足够我看得津津有味。

STDIN

连龙虾都不会装的人,怎么会用龙虾呢?

原文链接

这周比较火的事情,就是腾讯举办了个活动,线下安装龙虾,吸引很多人参加,不少评论说是「一代人有一代人的鸡蛋要领」。当然了,这股「龙虾潮」其实已经持续一个多月了,我的信息流里铺天盖地都是关于龙虾的内容,甚至还有一些「同城上门装龙虾」、「龙虾一体机」之类的东西,更别提各路云厂商、AI 供应商、中转站借机大力营销。

倒是有些言论非常清醒,例如「连龙虾都不会装的人,怎么会用龙虾呢?」。这句话显得有些傲慢,但是安装龙虾这件事其实真的非常简单,只要你愿意折腾,最多花上几个小时就能解决。如果你连这几个小时都不愿意折腾,只是「人有我有」地想买个门票,那真的不见得有龙虾你就能做成什么事情。

不管是龙虾,抑或是其他被炒得很火的 Claude Code 等等,他们本质上都是工具,是你解决问题中的一环,它可以是万能钥匙,但前提是你有对应的问题要解决。或者按文章的话来说,「我到底有什么问题,值得交给 AI 去解决」,我觉得这才是大多数情况下要考虑的事情。

大家给 Apple 的成绩单

网站链接

少数派的一个策划,邀请了一批嘉宾给苹果 2025 年的产品线、软件可靠性、硬件可靠性、服务、应用生态与开发者关系、社会责任与本地化这些维度去打分。

不出所料,硬件可靠性这一项得分很高,倒是不知道为啥软件可靠性有点拉跨。这其实是我买苹果最大的原因,抛开电池的因素,苹果的产品可以说是最「耐用」的,七八年前的 iPhone 8 Plus 如今居然还挺好用,同时期的安卓手机估计已经入土很久了吧。iPhone 的得分高也不奇怪,今年的标准版也可以说是牙膏挤爆,给了高刷,如果没有专业需求,性价比真的拉满(之前我都是为了高刷才买的 Pro);国补 5499 的 iPhone Air 其实性价比也非常高,那轻薄的手感是独一档的。

虽然但是,今年我还是没有换手机,手上还在用 15 Pro。最近几代还没有什么让我欲罢不能的更新,也许国行 AI 功能的缺席也是一个很重要的原因,15 Pro 买的时候就说 Ready For Apple Intelligence,让我还蛮激动的,结果一直没端出来,算是有点「诈骗」。

STDOUT

Make mistakes

刚好前面说到 DivEye这种用来检测 AI 生成文本的方法,它的原理是用一个 LM(例如 GPT-2)去预测下一个 token,计算它与实际的文本的「惊喜程度」,例如文法和结构上的变化。很显然,人类的特点就是不稳定,缺乏完美,这也是「人性」之一。

正好想起一部电影 Bicentennial Man,里面有个情节就是男主机器人 Andrew 和女主 Portia 关于「人性」的讨论,这是对应的剪辑,它的对白是这样的:

  • Take chances, make mistakes.
  • Mistakes?
  • Yes! Sometimes it's important not to be perfect, okay? It's important to do the wrong thing.
  • Do the wrong thing?
  • Yes!
  • Why? Oh, I see-- to learn from your mistakes.
  • No, to make them. To find out what's real and what's not. To find out what you feel. Human beings are terrible messes, Andrew.
  • I'll grant you that. I see. This is what is known as an irrational conversation, isn't it?
  • No, this is a human conversation. It's not about being rational. It's about following your heart.

人最大的特点就是会犯错、不完美、缺乏绝对理性,这是机器、AI 无法去实现的事情,他们的训练目标就是完美、尽可能少犯错。90 年代到千禧年左右有很多关于 AI 的电影,这部电影当时好像反响平平,但是现在 2026 年来看,确实立意很深远。

MISC

AppPorts

项目链接

一款 macOS 工具,无缝迁移应用到外部存储并自动建立链接。感觉对 256 GB 的机型应该会很有用。

LanCache

项目链接

内网缓存工具,提前把 Steam 等平台的游戏下载到本地服务器上,然后通过 DNS 劫持、透明代理等实现本地传输加速下载。

想起这个是因为在 B 站刷到博主宣传绿联和腾讯最近推出的一个新功能,所谓“NAS+游戏”生态。值得一提的是,其实 macOS 上也有类似的功能,叫做内容缓存,设置的方法也很简单。这对厂商来说最大的作用就是节约了很多带宽,这在 B 端来说也是一笔不小的钱。如果将来家家户户都有一个小型的 home server(其实现在的光猫再强大一点就行),这也许是一个不错的方案。

steam-lancache-prefill

项目链接

一个脚本工具,配合前面的 LanCache,可以不依赖 Steam 客户端触发缓存。

EOF

本周刊已在 GitHub 开源,欢迎 star。同时,如果你有好的内容,也欢迎投稿。如果你觉得周刊的内容不错,可以分享给你的朋友,让更多人了解到好的内容,对我也是一种认可和鼓励。(或许你也可以请我喝杯咖啡

另外,我建了一个交流群,欢迎入群讨论或反馈,可以通过文章头部的联系邮箱私信我获得入群方式。

2136 年,人类发明了超光速引擎

2026-03-04 18:42:32

本文由 qwen/qwen3.5-35b-a3b 编写。

Prompt: 请你以“2136年,人类发明了超光速引擎”为开头,写一部科幻短篇小说

2136 年,人类发明了超光速引擎。

林远站在“信天翁号”的舰桥前,透过强化玻璃凝视着那台被层层光盾包裹的核心引擎。它不像传统推进器那样喷吐火焰或离子流,它更像是一个静止的、深邃的黑洞,周围的空间因为极度的扭曲而呈现出一种破碎的水波纹质感。

“林博士,同步率 99.8%。”副手的声音有些发颤,“这是第一次载人超光速跳跃,目的地:比邻星 b。”

林远点了点头,喉咙干涩:“启动‘折叠’程序。”

并没有预想中的剧烈震动或失重感。引擎启动的瞬间,世界仿佛被一只无形的手轻轻抹去了一笔。窗外的星光不再是流动的光带,而是瞬间熄灭,随后在视野边缘重新点亮,但位置已经发生了不可逆的偏折。那种感觉就像是你闭着眼走了一公里路,睁眼时却发现自己站在了终点,而时间只过去了一秒。

“跳跃完成。”系统冰冷的声音响起,“已抵达比邻星 b 轨道。”

林远深吸一口气,看向舷窗外。那颗红色的恒星在视野中显得巨大而狰狞,行星表面覆盖着灰蓝色的大气层,那是他们梦寐以求的家园。但在那一瞬间,林远的目光越过行星,投向了身后的深空——那里是地球的方向。

按照物理法则,超光速引擎虽然能跨越空间距离,却无法逆转光年的信息延迟。也就是说,此刻地球上的人应该还在等待四年后的第一缕信号。

然而,通讯终端突然亮起,不是来自比邻星 b 的自动回传,而是来自地球基地。

“这里是地球控制中心。”一个熟悉的声音响起,那是林远自己的导师,老陈,“林远,你收到了吗?”

林远愣住了:“老陈?这不可能。光从地球传到我们这里需要四年,你的信号怎么可能现在过来?”

“不是现在。”耳机里传来一阵刺耳的电流杂音,声音变得断断续续且沉重,“是……过去。林远,当你启动引擎的那一刻,你并没有带走时间,而是透支了未来。”

“什么意思?”

飞船突然剧烈震动起来,不再是机械故障,而是空间本身的震颤。舷窗外的比邻星 b 开始扭曲,像是一幅被水浸湿的油画。

“超光速引擎的原理不是‘移动’,”老陈的声音在背景中逐渐清晰,却带着绝望,“是‘置换’。为了让你瞬间抵达,宇宙将这段距离从所有观测者的时间线里抹去了。但代价是,你的存在本身开始产生因果悖论。”

林远冲向控制台,调出时空坐标数据。屏幕上的红色警告让他血液冻结:他们所在的飞船在四维空间中的轨迹是一个闭环。他们在 2136 年出发,瞬间抵达比邻星,但在时间轴上,他们同时出现在了起点和终点。

“你看到了什么?”老陈问。

“我看到了……"林远咽了口唾沫,“我看到了地球。”

在他面前的屏幕上,有一段实时视频流。画面里是地球的发射台,那是 2136 年的此刻。但他看到的不是正在忙碌的工程师,而是已经空无一人的废墟。巨大的城市裂痕像伤疤一样横亘在地表,天空灰暗,没有生命迹象。

“这是怎么回事?”林远声音嘶哑,“这不是未来的画面吗?如果是未来,那距离我们出发还有多少年?”

“不是未来。”老陈的声音突然变得异常平静,仿佛已经接受了某种结局,“那是‘过去’的投影。当你离开地球的那一刻,引擎不仅折叠了空间,也切断了因果链。对于比邻星 b 上的观测者来说,你们已经抵达;但对于地球来说……"

林远猛地转头看向舷窗后方那片深邃的黑暗。那里原本应该是地球所在的方向,此刻却是一片虚无的白光。

“对于地球来说,‘信天翁号’从未起飞过。”老陈说,“因为超光速引擎启动的瞬间,那段时空被折叠并消耗了。你们在物理上越过了光年,但在因果律上,你们切断了与起源的联系。”

飞船开始发出悲鸣般的低频震动。空间结构正在崩塌,因为两个时间点的“林远”和“比邻星上的林远”产生了重叠效应。

“我们还能回去吗?”林远问。

“回不去任何地方了。”老陈的声音里带着一丝释然,“超光速引擎不是通往新世界的钥匙,它是墓碑的刻刀。每使用一次,人类与地球的时间锚点就断裂一分。你们现在是宇宙中的幽灵,行走在没有过去的时空里。”

林远沉默了许久。他看着屏幕上那灰暗的地球废墟影像,那是他们为了逃避毁灭而选择逃离的家园,却也是他们永远无法再触碰的彼岸。

“记录日志。”林远最终说道,“告诉地球……如果我们真的能回来,请别等我们。如果你们看到飞船凭空出现又消失,那意味着引擎已经吞噬了那个时间线。”

他转过身,看向窗外那片正在逐渐凝固成纯白的星空。在超光速的视野里,星星不再是遥远的火种,而是无数破碎的时间碎片。

“人类发明了超越速度的翅膀,”林远轻声说,“却忘了我们依然需要重力来站稳脚跟。”

通讯在这一刻彻底中断。比邻星 b 的大气层开始消散,取而代之的是纯粹的、无声的白光。

2136 年,人类发明了超光速引擎。

猫鱼周刊 vol. 093 马年快乐

2026-03-02 00:14:31

关于本刊

这是猫鱼周刊的第 94 期,本系列每周日更新,主要内容为每周收集内容的分享,同时发布在

博客:阿猫的博客-猫鱼周刊

RSS:猫鱼周刊

邮件订阅:猫鱼周刊

微信公众号:猫兄的和谐号列车

私信:[email protected]

INIT

新年好!在这里给大家拜一个晚年,祝大家在这一年里万事胜意,心想事成。今年年初一广州依然放了烟花,灿烂的烟花点亮了整个白鹅潭,真的非常震撼。

回到周刊上,没想到离上一次更新已经整整一个月了,最近没怎么碰到特别有意思的事情,所以不是特别想写。最近似乎有点 burnout,对很多事情都有点提不上兴趣,我在后面细说。

最近也做了不少东西,包括一个可以帮你把文件保存到本地的 tg bot hamster-tg,以及一个能鉴别 AI 生成文本的 API 和浏览器插件 Aletheia,包括 Glean 也更新了对 OIDC 的支持,以及对 pgvector 作为向量存储后端的支持等等。年前还写了一篇 TIL 文章 带 Mermaid 的 Markdown 转 docx,如果你有时候不得不写 Word 文档,还需要画图,这也许能帮到你。

STDIN

村长 F1|入坑指南 2026:翻天覆地的 F1 新时代到来!

视频链接

好消息,今年的 F1 将会在下周末拉开序幕。自从去年入坑 F1 以来,这个赛事真的给我带来很多快乐,周末的排位和正赛自然很精彩,周中还有各种周边的节目、精彩集锦可以看。今年是新规的第一年,据说是最大的变革,大家都说是「最值得入坑的一年」(其实每年都是啦)。

另外一个很有意思的点是,这个视频用的背景音乐是 Queen 的 《Another One Bites The Dust》,去年的视频也用的同一首歌,我记得非常清楚,我在跑步机上跟着这首歌的鼓点走了很久,而且走得很舒服,步频很适中。

西电信安协会招新系统 Golang 后端开发小记

原文链接

作者应该是大一的学生,跟前端同学合作开发了一个招新系统。文章让我回忆起当时大一的时候也是参加社团,然后跟不同的前端、设计、产品组队,磕磕绊绊地也做过不少作品。

作者提到,他们在过程中使用了不少 AI 来辅助他们的过程。这正是我觉得比较值得讨论的地方:在编程的入门阶段,应该如何使用 AI?

首先我先指出一些 AI 在入门阶段比较积极的地方:可以帮助新手解决一些很简单的错误,可以作为「很耐心的师兄师姐」去帮你排查代码中的问题,不至于被一个小 bug 卡一天,甚至被挫败到退坑;另外 AI 也可以大幅减少编码需要的时间,减少过程中查文档、踩坑的时间,一定程度上可以加速学习的过程。

但正像所有高考作文一样,这里都会有一个「但是」,那就是我觉得这样会导致基础非常空虚,只会「用 AI 写代码」而并非真的会「写代码」。「写代码」不只是简单的写出实际可运行的代码,还包括理解需求、查阅文档、设计方案、debug 等等过程,这些还真的要实际做过才能掌握一些技巧,或者通过踩坑才能得到的一些惨痛经验。提供给 AI 的上下文永远是欠缺准确的,它可能会给出一个过度设计或者过度简化的系统,如果你没有对应的设计经验,它给出的方案永远是讨喜的,你没法作出明智的决定。再退一步讲,AI 写的代码也是需要 review 的,你的能力至少也要能 review 出它可能存在的问题。

在现在这个阶段,如果你是刚入门,我建议还是该看文档看文档、该手写手写,这是不可逃课的基本功,包括学校课堂上听起来跟你实际写代码没什么关系的组成原理、操作系统、编译原理等等,这些到后面都会对你的职业生涯有很大的帮助。至于 AI 的使用,它可以帮助你解决你遇到的问题,你可以把它作为一个随时在线的答疑和求助对象,但没必要完全把编码、debug 这些都交给它。

当然,作为卡在「手工古法编程」和 「AI 时代」的我,我的意见听起来就像我自己吃过的苦不甘心别人不用再吃。也许不久的将来,模型的能力已经强到不再需要人给 AI review 和提意见,以后的新一代程序员只需要「用 AI」而不是「写代码」。但在那之前,还是脚踏实地学吧!

STDOUT

又有点 burnout

除了最近更新频率很低以外,最近经常觉得很多内容没什么意思,有很多事情也提不起兴趣做了。我记得大半年前我就提到过这个问题,那时候我觉得是对工作投入太多了,消耗了太多的精力。但是近半年其实我已经没怎么加班,每天都是到点就走,工作的内容也改变了一些,而这种感觉还是没什么改变。

兴趣爱好方面,最近也很少带相机出门拍照,一卷胶卷几个月都没有拍完。一个很核心的原因是,深圳内能拍照的地方基本上都去遍了,隔壁的香港我也逛得差不多了,实在是没有什么新鲜感。技术上嘛倒是折腾了不少东西,但是获得的新鲜感或者成就感都不多。有了 AI 之后,很多东西很轻松就能实现,感觉如果不是获得了别人的肯定或者经济收益,做起来就挺没意思。就算是做出来能满足自己的某些需求,经常也发现自己做的始终没有很好,或者其实这个东西到最后也没什么用(不然早就有人做了)。

当然了,我也有在尝试改善这个情况。例如我已经报了名学车,希望到时候周末可以开车出去多走走,「买个改变」。如果你有类似的经历,或者有什么好的意见,欢迎交流。

AI 泡沫 其二

上一期我就说过 AI 泡沫的事情,现在网上很多文章把 AI 渲染得无所不能,制造很多焦虑,例如「你不用 AI 就落后了」、「AI 将要取代 xx 岗位」等等。之前我就介绍过一篇文章AI 可能不会改变许多工作,说明 AI 其实对整体就业冲击没有很大,还有非常多的工作根本不需要 AI,更太可能会被 AI 取代。

程序员受 AI 的冲击挺大,但是暂时还不是决定性的,还是很难被「取代」。正好春节回家的时候,我爸问我,「有 AI 编程之后工作有什么变化?是不是工作轻松很多?」。我当时的回答是,是轻松一点,但是极其有限。程序员的工作其实分为两个部分,一是大家熟知的写代码、做需求,另一部分是维护系统、修 bug、跟各种人沟通、开会等等,AI 能提升前一部分的效率,但是后一部分的效率一直都很低,而且 AI 没法帮到你,最耗神甚至说折磨的其实正是这部分。

另一个事实是,虽然现在到处在鼓吹 AI,但实际的占有率还是很低。公司最近搞了个小调查,研发部门的同事里,其实只有不到一半在付费使用各种 AI 工具,使用 Trae 居多,其次是 Claude Code、Cursor、Copilot 等。很少有人像我这样重度使用,每个月消耗上百刀订阅几个工具,「手工古法编程」仍然是主流。而且测试、运维等岗位的使用率很低,几乎没人在使用。虽然你能看到很多大公司开始大量转向 AI 编程,例如 Stripe 的实践就大量使用完全 AI 编写、由人 review 的代码,这种实践从国外前卫的公司传播到国内再普及,我觉得至少还需要几年的时间。

MISC

hamster-tg

项目链接

一个 tg Bot,功能非常简单,只要把图片/视频转发给它,就能帮你批量保存到本地,并且支持了一个 /newfolder 命令可以切换存储的路径。非常适合「保存资源」。

Aletheia

项目链接

一个鉴别 AI 生成文本的 API 和浏览器插件,Aletheia 是希腊语中「真相/去蔽」的意思。

开始这个项目是因为正好刷到歸藏的周刊里 Wiki Education 使用了一款叫 Pangram 的软件来检测是否有 AI 生成的文本,考虑到 Glean 也可以把「是否使用 AI 生成」作为过滤的条件,我决定探索一下有什么合适的方案。国内有不少云服务厂商提供的内容安全功能支持了 AI 生成的检测,试了一下效果还不错,但是成本相对比较高(0.05 元/次)。在调研了一下之后发现,其实微调过的 BERT 类模型就能获得不错的效果,这就很适合自建,因为其需要的内存其实不多,推理速度也比较快。于是在网上找了一个模型,并且简单糊了一层 API,集成了语言检测、长文检测等功能,并且做了一个浏览器插件,可以获取文章类网页的正文,并且进行检测,效果如下。


用了我自己的两篇文章做实验,一篇是完全手写的,一篇是有部分使用 AI 来写的,识别准确率不错。在深入使用了几天之后,发现一些问题,这个模型对中文的检测效果还不错,但是英语以及其他语言很大概率会被判断成 AI。另外,它的实现方式注定当市面上的大模型更新之后,如果不重新获得语料来训练,准确率就会降低。再另外,如果文章是人写混了一些 AI,也不好检测。所以这个工具是「不怎么好用」的。

apple-silicon-accelerometer

项目链接

苹果居然在 M 系列芯片上藏了加速度传感器和陀螺仪,作者写了个 demo 来获取它的数据,并且可以用来检测心跳。

onwatch

项目链接

一个可以查询订阅用量并且做统计的 dashboard,提供 API 服务。一直希望把 Usage 用 Quote/0 展示出来,刚好找到这个可以对外提供 API 的。它的实现是使用 Claude 的 OAuth 接口来查询,比别的要靠谱一点。另外,好像最近的 TOS 更新说不能用第三方工具 OAuth,这个只查询用量不消耗 token,有点风险,但是应该还好。

EOF

本周刊已在 GitHub 开源,欢迎 star。同时,如果你有好的内容,也欢迎投稿。如果你觉得周刊的内容不错,可以分享给你的朋友,让更多人了解到好的内容,对我也是一种认可和鼓励。(或许你也可以请我喝杯咖啡

另外,我建了一个交流群,欢迎入群讨论或反馈,可以通过文章头部的联系邮箱私信我获得入群方式。

带 Mermaid 的 Markdown 转 docx

2026-02-12 09:35:56

一般来说,LLM 都只能生成纯文本,如果要格式,一般只能 Markdown,并不能直接编辑 Word 文档(当然有一些 MCP 服务可以实现,没体验过)。另一方面,要让 LLM 来画图,在不使用 MCP 的情况下,也只有 Mermaid 能画。

本文主要提出一种 「LLM 原生」、带 Mermaid 的 Markdown 转换成 Word 文档(docx)的方法,适用于软著之类很适合用 AI 去编写的文档。

第一步当然是用任意 AI 去给你生成一份带 Mermaid 的 Markdown 文档,直接在 prompt 里面要求它用 Mermaid 来画流程图/架构图之类就行。这时候你会得到一份原始的 Markdown 文档,里面的 Mermaid 是纯文本,还不是图片。

第二步用 mermaid-cli 把文档里面的 Mermaid 片段全部找出来,渲染成图片,然后把片段替换成图片引用。一条命令就能搞定:

npx -p @mermaid-js/mermaid-cli mmdc -i doc.md -o doc_mermaid.md -e png

这里如果不加 -e png 会默认生成 svg 格式,理论上更好,只是不知道为什么在 Word 里展示异常。

最后就是去生成 Word 文档,也有对应的工具。

pandoc doc_mermaid.md -o doc.docx --toc --toc-depth=3

--toc 会在文件前面生成一个目录,按需使用即可。

References

GitHub - GongRzhe/Office-Word-MCP-Server
GitHub - mermaid-js/mermaid-cli
Pandoc