[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"list-\u002Farticles\u002F2":3},{"items":4,"total":149},[5,20,32,44,55,66,77,93,101,114,125,137],{"id":6,"type":7,"slug":8,"title":9,"date":10,"category":11,"tags":12,"body_markdown":14,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":19},101,"article","woodworking-year-experience","我当木工这一年","2025-02-12 18:00","life",[13],"木工","\n> 给不了解背景的朋友一个前情提要：我从2024年4月份开始给新房子打柜子，一直到12月，目前已基本完工。\n>\n> 哪种打柜子：自己买整张板材，用推台锯裁板，用封边机封边，然后修边，打螺丝或者其他固定件组装。\n\n![题图](\u002Fassets\u002Flife\u002F2025\u002Fwoodworking-year-experience\u002F01.jpg)\n\n## 开端\n\n在2023年的总结中，我写到了换房子的事情，简单说就是卖了福田区（中心区）的小房子，在坪山区（郊区）买了一套面积更大一点的。年底的时候新房子收房了，然后后就一直在忙活装修的事情。\n\n开发商交付的是精装房，硬装工作量不大，主要是封了个阳台。剩下的就是所谓的“全屋定制”，通俗一点说就是做柜子。\n\n大概是之前小房子带来的储物焦虑太多，我们给新房子设计的柜子非常多。也带着一些初步的想法，跑了好多家做全屋定制的商家，发现价格都不便宜，而且在沟通过程中发现很多商家对空间的使用并没有太多想法，只是在做柜子。\n\n彼时我已经做好了离职的准备，非常希望在离职后做一段与互联网完全不相干的事情，给自己一些放空的时间。于是琢磨着：这个柜子，有没有可能自己来做？\n\n\u003C!-- more -->\n\n## 可行性探讨\n\n和家人说出这个想法的时候其实我心里也没有底，聊得非常小心翼翼，但是很意外的是并没有收到强烈的反对意见。而我当时也已经看了挺多木工打柜子的视频，仔细研究下来觉得也不难，无非是裁板、封边和打螺丝而已。\n\n于是就这么一边学一边和家人讨论着，慢慢好像大家都接受柜子可以自己打这件事了。我也就开始买工具和耗材了。\n\n打柜子最重要的当然是板材，这里面的门道也非常多。我们在用什么板材上纠结了很久，最后还是靠一个朴素的想法：家具板材是一个充分竞争的市场，价格一定程度上可以反映选择的好坏。年前年后跑了不少市场，也看了挺多网店，最后在宝安定了一家，买了柜体用的多层板。\n\n## 新手上路\n\n3月底正式离职，4月份剪了个寸头就开始了打柜子。一开始是非常兴奋的，毕竟很多工具以前只在视频中见过，现在却在自己手上。每天都能对工具更熟悉一点（推台锯、封边机、修边刀、修边机、钉枪、电钻等），或者掌握一些新的工艺如背板开孔、螺丝孔沉孔、二合一等等。\n\n慢慢将一块块大木板变成一个个精致的小柜子，是一件非常开心的事。当柜子组装完成，放到房间里的时候，忍不住会欣赏半天，然后拍些照片和朋友们分享。\n\n![乐器柜](\u002Fassets\u002Flife\u002F2025\u002Fwoodworking-year-experience\u002F02.jpg)\n\n![衣柜](\u002Fassets\u002Flife\u002F2025\u002Fwoodworking-year-experience\u002F03.jpg)\n\n![书桌和吊柜](\u002Fassets\u002Flife\u002F2025\u002Fwoodworking-year-experience\u002F04.jpg)\n\n这一段应该算是全年最平静的日子。大概有两三个月没有开过电脑，没有写过代码，甚至连手机也只是早晚简单看一下。\n\n当时的心思特别简单，每天就只用操心打柜子这一件事情：算算板材的尺寸，琢磨下工艺如何改进，再想想哪个姿势操作工具会更省力一些。\n\n虽然身体是很累的，但是心里非常惬意。随之而来的是睡眠质量也非常好，甚至可以在晚上8点就陪娃一起睡觉。\n\n## 焦灼和焦虑\n\n5月下旬，工具基本已熟练掌握，工艺也都优化得差不多，各种结构的柜子拆板（把柜子变成一块块有具体尺寸的板子）也越来越熟练。这时候打柜子已经没有新鲜感了，每天上午8到12点，下午14到18点，都在不停地做柜子。也就是在这个时候，我们开始把这件事叫做上班。\n\n这时候我们都开始陷入一种焦虑：这个打柜子的进度，远远落后于预期，不知道啥时候才能做得完了！而我们原计划要夏天开始回新疆的长途自驾游！\n\n你看，不到两个月，头发长出来了，上班的种种也回来了。可见天下没有银弹，不上班并不能解决任何问题，心理状态和上班仍然是一模一样的，而且还没有人来发工资。\n\n## 休息和整理\n\n7月底，深圳已经是30好几度的天，而我们干活的客餐厅空调也为了防尘包起来了，因此干木工活非常热。\n\n此时我们的心态已经稍微放平了一些，坦然接受了短时间打不完柜子的事实。我认真地整理了一下剩余的工作量，至少还需要35到40个工作日，也就是差不多两个月，到9月底。\n\n9月底的深圳依然不会很凉，但是新疆却有可能开始下雪了。\n\n在这种情况下我们改变了原有的计划：与其匆匆忙忙赶工，再赶一下下着雪的新疆，还不如先暂停一下，从新疆回来再继续打柜子。\n\n就这样，8月1号，我们正式踏上了自驾回新疆的路。\n\n## 再度开工\n\n10月底，我们自驾结束，回到了深圳。\n\n这时候深圳的天已经转凉了，而我们经过几个月的休整，心态也更加放松。再度开工以后发现手感还在，有前期的经验，做起柜子来也更加得心应手。\n\n这一阶段的我们更像是在为别人家打柜子的熟练工：完全不焦虑，但是手上很熟，每天的进展都很快。\n\n在12月快要结束的时候，我们终于打完了餐边柜。这是我们从一开始就讨论最多的柜子，因为它的位置就在入户的地方，是一个重要的门面，从结构上来说也是最大最复杂的柜子。餐边柜的完工基本标志着我们全屋的柜子基本打完，剩下的就是一些边角料的缝缝补补。\n\n![餐边柜](\u002Fassets\u002Flife\u002F2025\u002Fwoodworking-year-experience\u002F05.jpg)\n\n## 小结\n\n回想起来，能够有接近一年的时间自己去打柜子，这绝对是一段空前绝后的经历，真的需要天时地利人和：有足够的时间，有足够的场地，有家人的支持。在未来不太可能再有这样天时地利人和的机会，更重要的是自己也未必会再有挑战一次的勇气。\n\n从干木工活中，也有非常多的收获。\n\n可能很多人不太能理解，我从中获得的最大收获首先是大脑得到了全面的休息。在这一年里，我有意识地对互联网有所戒断，完全远离了技术圈，也不再对产品、技术、规划、绩效、业界动态而焦虑，这是一种非常难得的放松。\n\n此外，完成一件在外人看来非常艰难的事情，能够获得巨大的成就感。每当我踏进家门看到满屋子自己亲手制作的柜子，想起每一个柜子制作过程中发生的事情，就会觉得非常满足，这在一定程度上也能够治愈自己的焦虑：看，其实我还是个不错的人。\n",null,0,"2026-08-28 04:37:17","published","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Flife\u002F2025\u002Fwoodworking-year-experience\u002F01.jpg",{"id":21,"type":7,"slug":22,"title":23,"date":24,"category":25,"tags":26,"body_markdown":30,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":31},137,"play-with-xiaomi-r1d-router","折腾小米初代路由器R1D","2024-12-01 22:00","tech",[27,28,29],"路由器","小米","OpenWRT","\n随着装修逐步进入尾声，最近时不时就会开始想以后的Home Lab要怎么搭。眼光在家瞟来瞟去的，突然瞟到了已经十来年的小米路由器了。这是一台小米一代路由器（R1D），因为当时负责这款产品的老大还有一些同事都是以前腾讯的老同事，所以很早就注意到并且购买了这台造型独特、自带1T硬盘的路由器。\n\n![小米路由器R1D](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F01.jpg)\n\n后来好多年我家一直使用这台路由器，也是这台路由器让我感觉到原来面积中小的房子只需要一台路由器就能稳定覆盖WiFi信号。同时它自带的硬盘还能存点文件，有时候还能远程下载点影视剧。\n\n几年前IPv6开始普及了，当时这款路由器已经没有固件更新了，老固件没有对IPv6的支持。为了折腾IPv6，就换了一台路由器，这台小米一代路由器就被闲置了。\n\n\u003C!-- more -->\n\n## 刷机修复错误\n\n给路由器插上网线，通电之后，一开始是黄色灯常亮（开机中），后来变成了闪红灯。查了一下指示灯的显示：\n\n- 黄灯常亮：启动中\n- 蓝灯常亮：正常\n- 红灯闪烁：系统故障\n- 黄灯闪烁：升级\u002F刷机安装系统中\n\n那么看起来是系统坏了，进入了恢复模式。直接访问路由器的管理页面，也能看到很明显的提示，系统目前处于恢复模式。\n\n![恢复模式](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F02.png)\n\n网上的教程都说需要用U盘刷机，但此时管理页面是可以访问的，因此也可以直接从官网下载固件，然后在网页上选择刷入即可。因为本来就是想再折腾一下这台路由器，我直接选择了开发版固件。\n\n几分钟后，路由器刷机完成自动重启，终于变成了蓝灯，此时路由器的功能已完全恢复正常，内置硬盘中的文件也可以浏览操作了。\n\n## 打开SSH\n\n接下来试着打开路由器的SSH，官网有说明，按说明从官网下载SSH的工具包，改名为`miwifi_ssh.bin`，放入U盘（FAT文件系统）根目录插入路由器USB口，然后重启路由并用卡针顶住RESET口不放，直到黄灯闪烁，说明正在刷机。\n\n这一次只需要几秒钟即可，随后路由器再次重启，SSH端口成功打开。\n\n接下来兴高采烈地准备SSH连接上去`ssh root@192.168.31.1`，结果碰到一个华丽丽的报错：\n\n```sh\nUnable to negotiate with 192.168.31.1 port 22: no matching key exchange method found. Their offer: diffie-hellman-group1-sha1,diffie-hellman-group14-sha1\n```\n\n这个是什么意思呢？嗯……简单来说就是服务端和客户端的算法对不上，客户端是最新的macOS，而服务端是十年前的路由器系统（魔改OpenWRT），这十年间安全界也在不断淘汰旧算法，采纳新算法，于是一个跨越十年的“代沟”就这么产生了：服务端想用老算法，客户端说这玩意我不知道是啥。\n\n解决方法也不难，就是让客户端兼容一下服务端，勉为其难用一用旧算法：\n\n```sh\nssh -oKexAlgorithms=+diffie-hellman-group1-sha1,diffie-hellman-group14-sha1 root@192.168.31.1\n```\n\n至此终于可以登录到路由器中了。不过关于SSH的坑还没有踩完，这里也一并记录一下。\n\n一般来说登录到服务器后为了方便（也为了安全），会设置一下公钥登录。小米这款路由器的SSH服务端用的并不是OpenSSH，而是beardrop，因此需要将我们的公钥放在`\u002Fetc\u002Fbeardrop\u002Fauthorized_keys`中，而不是常见的`~\u002F.ssh\u002Fauthorized_keys`。\n\n当我将公钥放上去之后，公钥登录却并没有生效，通过给`ssh`命令添加`-v`参数查看调试信息，可以看到另一个错误：\n\n```\ndebug1: Offering public key: id_rsa_toobug RSA SHA256:cY3mrYxBSCKd9yDJYYSkvytZmKl6Btb+bMZG1SCrg\u002FM agent\ndebug1: send_pubkey_test: no mutual signature algorithm\n```\n\n通过报错信息，结合一些资料，可以知道，这也是一个“代沟”：客户端和服务端的签名算法对不上。同样的方法，给客户端指定一下旧的算法（`ssh-rsa`）即可。\n\n我将相关参数直接写在了ssh的配置文件中：\n\n```\nHost mi-r1d\n    HostName 192.168.31.1\n    User root\n    HostKeyAlgorithms +ssh-rsa\n    PubkeyAcceptedAlgorithms +ssh-rsa\n    KexAlgorithms +diffie-hellman-group1-sha1,diffie-hellman-group14-sha1\n```\n\n> 以上关于SSH命令的调试、解决均有ChatGPT的协助。\n\n## OPKG源\n\n解决了SSH的问题，本以为一切顺利了。正常来说就是用包管理软件了，先更新一下源，再安装想要的软件即可。\n\n```sh\nopkg update\n```\n\n结果这里碰到了无数个问题，为了简单起见，直接用一个列表按时间顺序列出步骤、问题和解决方案：\n\n1. 软件源地址失效：`\u002Fetc\u002Fopkg.conf`文件中写的源地址已不存在`http:\u002F\u002Fdownloads.openwrt.org\u002Fattitude_adjustment\u002F12.09\u002Fbrcm4709\u002FR1D\u002Fpackages`\n2. 在上述网站中手工翻找，找到`https:\u002F\u002Fdownloads.openwrt.org\u002Fattitude_adjustment\u002F12.09\u002Fbrcm47xx\u002Fgeneric\u002Fpackages`\n    - 执行`opkg update`时报错`wget: not an http or ftp url: https:\u002F\u002Fdownloads.openwrt.org\u002Fattitude_adjustment\u002F12.09\u002Fbrcm47xx\u002Fgeneric\u002Fpackages\u002FPackages.gz`，也就是说它调用了wget，但是wget不支持HTTPS\n      ![wget报错](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F03.png)\n    - 尝试换用`http`协议，wget直接报错`-1`，应该是网站不允许使用HTTP访问了\n    - 手工翻包列表，才发现原来wget还分wget-nossl和wget-ssl\n    - 手工下载`wget-ssl`的包，尝试通过SCP传到路由器上，报错（具体信息不记得了），同样也是SCP的传输方式已经变了，需要使用`-O`强制使用旧版本的方式传输\n    - `wget-ssl`的包上传到路由器之后并不能安装，而且无论架构是`brcm47xx`还是`armv7`都一样报错：\n      ```\n      opkg install \u002Ftmp\u002Fwget_1.13.4-1_ar7.ipk\n       Unknown package 'wget'.\n       Collected errors:\n        * pkg_hash_fetch_best_installation_candidate: Packages for wget found, but incompatible with the architectures configured\n        * opkg_install_cmd: Cannot install package wget.\n      ```\n    - 尝试使用下文的Entware的源，update可以成功，但所有软件都提示架构不兼容\n      ![使用Entware源报错](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F04.png)\n\n至此，折腾OPKG彻底失败，这台路由器到目前为止没有成功安装上任何软件。\n\n## 安装Entware\n\n在即将陷入僵局的时候，突然发现了有一个专为嵌入式设备服务的软件仓库：Entware（[Github](https:\u002F\u002Fgithub.com\u002FEntware\u002FEntware)）。它的文档还是比较多的，但是组织并不友好，安装时可参与其在某一个平台的文档，例如[在群晖上的安装文档](https:\u002F\u002Fgithub.com\u002FEntware\u002FEntware\u002Fwiki\u002FInstall-on-Synology-NAS)。\n\n其中最重要的是`wget -O - https:\u002F\u002Fbin.entware.net\u002Farmv7sf-k2.6\u002Finstaller\u002Fgeneric.sh | \u002Fbin\u002Fsh`这句脚本。（注意其中`armv7sf-k2.6`，其中2.6是内核，在路由器上通过`uname -a`可看到内核版本是2.6）。很明显，我们的wget访问不了这个HTTPS地址，但我们还有两种方法可以处理：\n\n1. 下载这个安装脚本后再通过SCP传到路由器上\n2. 将命令中的`https`改成`http`（是的，它支持使用HTTP协议访问！）\n\n![安装开始](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F05.png)\n\n![安装成功](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F06.png)\n\n因为Entware安装的软件都会在`\u002Fopt`目录中，我们需要按照提示，需要将`\u002Fopt\u002Fbin`和`\u002Fopt\u002Fsbin`放入`$PATH`变量中，这样才可以直接执行它安装的软件。这里我选择在`\u002Fetc\u002Fprofile`中修改。\n\n```sh\nexport PATH=\u002Fopt\u002Fbin:\u002Fopt\u002Fsbin:.....\n```\n\n此外还有一条命令，建议放到开机命令中去，大致方法是在`\u002Fetc\u002Finit.d`新建一个脚本，然后启用即可，这里我没有去做，可参考其他教程。\n\n在安装完Entware后，就有了另一个`opkg`命令，我们可以用它来安装软件了。你可以用`which opkg`来确认，确保当前运行的是`\u002Fopt`目录下的`opkg`。\n\n首先解决冤大头`wget-ssl`，这样就可以访问HTTPS网站了，但实际上安装完之后依然不能访问HTTPS网站，会显示证书不信任。这同样是因为时间太久了，路由器内置的CA证书与目前公认受信任的CA证书列表已大不一样。此时还需要安装`ca-certificates`，安装完后终于能正常访问HTTPS网站了。\n\n![安装wget-ssl](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F07.png)\n\n![安装ca-certificates](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F08.png)\n\n![成功访问https网站](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F09.png)\n\n后面我用Entware安装了nginx，使用完全正常使用。\n\n![nginx](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F10.png)\n\n## 软链\n\n路由器的`\u002Fopt`位于内置存储中，空间只有100多M，装几个软件就满了，因此我们需要给它们挪个位置，把软件都挪到1T的硬盘中去。\n\n```sh\nmkdir -p \u002Fuserdisk\u002Fdata\u002F.system\nmv \u002Fopt \u002Fuserdisk\u002Fdata\u002F.system\nln -s \u002Fuserdisk\u002Fdata\u002F.system\u002Fopt \u002Fopt\n```\n\n首先在硬盘中新建一个`.system`目录（名字随意），然后将`\u002Fopt`挪过去，最后将`opt`目录软链回原来的路径`\u002Fopt`。这样文件存储是在硬盘中，但访问路径没有任何变化，各个软件仍然可以正常工作。\n\n## 设置防火墙\n\n这个路由器现在是被当作Home Lab来使用，因此不承担路由的功能，它通过有线网连接到主路由下，对它来说有两个网络：\n\n- LAN：即原来的`192.168.31.1`网段（实际上为了避免与主路由冲突，修改了网段，此处为示意仍然使用这个地址）\n- WAN：即主路由所在网段\n\n如果电脑连接主路由，则只能访问到Home Lab的WAN接口，如果要访问LAN接口，则需要连接Home Lab的WiFi或者插入网线才可以。这两者主要区别是：管理界面和文件共享等功能都只允许从LAN访问，也就是说如果连接主路由，那Home Lab的管理界面和文件共享都访问不到。\n\n此时我们需要修改它的防火墙设置，让它允许接受来自WAN的访问。修改`\u002Fetc\u002Fconfig\u002Ffirewall`，找到`wan`，将其中的`input`从`REJECT`修改为`ACCEPT`，然后重启`\u002Fetc\u002Finit.d\u002Ffirewall restart`即可。\n\n![修改防火墙设置](\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F11.png)\n\n## 其他和小结\n\n虽然到目前为止，似乎解决了软件安装的问题和最常用的网络访问问题，但实际上在后续折腾的过程中还是碰到了更多问题。我尝试安装TailScale，安装成功了，但执行`tailscale up`时，报了`Illegal instruction (core dumped)`的错误。后续使用Node.js时也出现了同样现象，但神奇的是`node -v`可以正常执行。\n\n按照ChatGPT的说法，出现这种现象可能是程序使用了CPU不支持的指令，可以尝试使用`gdb`调试。我也试了一下生成Coredump文件，但实际上这已经远远超出了我的知识范围，只能暂时放弃了。\n\n至此，这个路由器的折腾告一段落，目前路由器功能正常，硬盘功能正常，有需要的话还可以开启nginx访问，只能说聊胜于无，先当个小垃圾用着吧。\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2024\u002Fplay-with-xiaomi-r1d-router\u002F01.jpg",{"id":33,"type":7,"slug":34,"title":35,"date":36,"category":37,"tags":38,"body_markdown":42,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":43},193,"use-npm-mirror-on-pnpm","在pnpm中使用NPM镜像","2024-11-23 14:00","web",[39,40,41],"NPM","代理","pnpm","\n在[上一篇文章](\u002Farticle\u002Fweb\u002F2024\u002Fsetup-npm-proxy)中，我为了解决网络阻断的问题，搭建了一个NPM代理服务器。文章发布后，收到了不少反馈，其中有一条是关于是否可以在pnpm中直接使用国内镜像。\n\n首先明确一下问题背景与结论：\n\n- 在使用NPM安装包的时候，如果遇到网络阻断问题，可以通过配置NPM镜像来下载依赖，但是有可能影响CI环境以及与他人的协作\n- 在pnpm中，可以直接配置使用任意NPM镜像，而不会影响CI环境和他人协作\n\n下面我们来详细看看来龙去脉。\n\n\u003C!-- more -->\n\n## npm pnpm和它们的lock文件\n\nnpm在安装包的时候，会生成一个`package-lock.json`文件，里面记录了每个包的版本号和下载地址，除此之外还记录了一个`integrity`字段，这个字段是一个哈希值，用来校验包的完整性。我们摘录`package-lock.json`文件中的一段内容：\n\n```json\n\"node_modules\u002Fis-number\": {\n    \"version\": \"6.0.0\",\n    \"resolved\": \"https:\u002F\u002Fregistry.npmjs.org\u002Fis-number\u002F-\u002Fis-number-6.0.0.tgz\",\n    \"integrity\": \"sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6\u002F538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==\",\n    \"engines\": {\n        \"node\": \">=0.10.0\"\n    }\n},\n```\n\n其中`resolved`字段就是包的下载地址，`integrity`字段是包的哈希值。\n\npnpm也有一个类似的文件，叫做`pnpm-lock.yaml`，它的内容和`package-lock.json`类似，但是有一点不同：pnpm的lock文件中，并不记录包的下载地址，而是只记录了包的哈希值。我们摘录`pnpm-lock.yaml`文件中的一段内容：\n\n```yaml\n\u002Fis-number@6.0.0:\n  resolution: {integrity: sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6\u002F538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==}\n  engines: {node: '>=0.10.0'}\n  dev: false\n```\n\n可以看到，`resolved`字段被省略了，只有`integrity`字段，而且`integrity`字段的值和`package-lock.json`中的一样。\n\n## lock文件的差异导致的registry镜像问题\n\n首先，因为pnpm的lock文件中没有记录包的下载地址，所以更换registry镜像并不会影响lock文件的内容。这意味着，如果我们在pnpm中使用了国内镜像，那么lock文件中的所有内容都和不使用镜像时一致，不会发生改变。\n\n但npm的表现就不一样了。如果我们在NPM中使用了registry镜像，那么lock文件中的`resolved`字段就**可能**被替换成镜像地址。注意这里我说的是**可能**，因为npm客户端记录的lock文件的`resolved`字段除了受配置的registry镜像影响，还会受到`node_modules`目录中已经存在的包以及本机npm缓存的影响。\n\n下面是一个真实的例子：\n\n```json\n\"node_modules\u002Fis-number\": {\n    \"version\": \"6.0.0\",\n    \"resolved\": \"https:\u002F\u002Fregistry.npmjs.org\u002Fis-number\u002F-\u002Fis-number-6.0.0.tgz\",\n    \"integrity\": \"sha512-Wu1VHeILBK8KAWJUAiSZQX94GmOE45Rg6\u002F538fKwiloUu21KncEkYGPqob2oSZ5mUT73vLGrHQjKw3KMPwfDzg==\",\n    \"engines\": {\n    \"node\": \">=0.10.0\"\n    }\n},\n\"node_modules\u002Fis-odd\": {\n    \"version\": \"3.0.1\",\n    \"resolved\": \"https:\u002F\u002Fregistry.npmmirror.com\u002Fis-odd\u002F-\u002Fis-odd-3.0.1.tgz\",\n    \"integrity\": \"sha512-CQpnWPrDwmP1+SMHXZhtLtJv90yiyVfluGsX5iNCVkrhQtU3TQHsUWPG9wkdk9Lgd5yNpAg9jQEo90CBaXgWMA==\",\n    \"dependencies\": {\n    \"is-number\": \"^6.0.0\"\n    },\n    \"engines\": {\n    \"node\": \">=4\"\n    }\n}\n```\n\n虽然我只配置了`registry.npmmirror.com`一个源，但在这个lock文件中同时存在`registry.npmjs.org`和`registry.npmmirror.com`两个地址。也就是说，安装同一个包时，不同的机器可能会有不同的`resolved`字段，无法保证所有机器上生成的lock文件都是一致的。\n\n如果我们将npm的lock文件提交上去，就有可能导致两个问题：\n\n1. CI服务器会尝试访问这个镜像地址，而不是原始的npm registry地址，这可能会导致CI环境无法正常安装依赖（企业中CI机器一般有严格的网络策略，不会允许访问任意registry镜像）。\n2. 因为不同机器上的lock文件可能不一致，所以在多人协作的情况下，可能会导致不同人生成的lock文件不一致，进而导致冲突。\n\n## 一些无用小资料\n\n在翻查相关资料的过程中，还发现一些有意思的小细节。\n\n### npm的隐藏lock文件\n\n首先是npm的lock文件有3个不同的版本，其中v1是比较老的版本使用，而v2和v3的格式则完全一样，那为什么会有一个v3版本呢？\n\n```json\n{\n  \"name\": \"npm-test\",\n  \"version\": \"1.0.0\",\n  \"lockfileVersion\": 3,\n  ...\n}\n```\n\n这实际上是npm v7带来的一个新东西，叫“隐藏lock文件”。在npm v7以上的版本中，如果你使用npm安装依赖，它会生成一个`node_modules\u002F.package-lock.json`文件，注意不是根目录下的`package-lock.json`文件。这个文件的内容和根目录下的lock文件几乎一样，只是位置不同。这样做的目的是为了减少对`node_modules`目录的扫描，以提高性能。当满足以下条件时，npm会直接读取隐藏lock文件中的信息，而不对`node_modules`进行扫描：\n\n- 隐藏lock文件中所有的包对应的目录都存在\n- 所有包目录中的包都在隐藏lock文件中列出\n- 隐藏lock文件的修改时间不早于所有包目录的修改时间\n\n这3个条件的意思也就是“隐藏lock文件”是在最近一次安装\u002F更新包依赖时被更新的。\n\n因此`lockfileVersion: 3`的意思也就是“这里有一个隐藏lock文件”。\n\n### pnpm对“换源”问题的解决\n\n通过前面的介绍，我们可以知道pnpm对于更换registry镜像是可以无感的，因为它的lock文件中并不记录包的下载地址。但实际上可能是出于对完整性的考虑，pnpm在换源后重新安装包的时候会报错：\n\n```shell\nERROR  This modules directory was created using the following registries configuration: {\"default\":\"https:\u002F\u002Fregistry.npmjs.org\u002F\"}. The current configuration is {\"default\":\"https:\u002F\u002Fregistry.npmmirror.com\u002F\"}. To recreate the modules directory using the new settings, run \"pnpm install\".\n```\n\n也就是说换源后需要重新使用`pnpm install`来安装一次依赖。\n\n但是既然pnpm的lock文件并没有记录包的下载地址，它是怎么知道之前这些包是从哪下载的呢？通过查看pnpm的源码，最终找到了答案：pnpm会在`node_modules\u002F.modules.yaml`中记录一些信息：\n\n```yaml\nhoistPattern:\n  - '*'\nhoistedDependencies:\n  \u002Fis-number\u002F6.0.0:\n    is-number: private\nincluded:\n  dependencies: true\n  devDependencies: true\n  optionalDependencies: true\ninjectedDeps: {}\nlayoutVersion: 5\nnodeLinker: isolated\npackageManager: pnpm@8.10.2\npendingBuilds: []\nprunedAt: Sat, 23 Nov 2024 04:56:52 GMT\npublicHoistPattern:\n  - '*eslint*'\n  - '*prettier*'\nregistries:\n  default: https:\u002F\u002Fregistry.npmjs.org\u002F\nskipped: []\nstoreDir: \u002FUsers\u002Ftoobug\u002F.pnpm-global\u002Fstore\u002Fv3\nvirtualStoreDir: .pnpm\n```\n\n这是不是又和npm的隐藏lock文件有点像呢？\n\n## 结语\n\npnpm真香！用pnpm解千愁！以及需要认真对维护npm镜像的同学们表示感谢！\n","",{"id":45,"type":7,"slug":46,"title":47,"date":48,"category":37,"tags":49,"body_markdown":54,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":43},192,"setup-npm-proxy","NPM代理搭建指南","2024-11-16 16:00",[39,40,50,51,52,53],"反向代理","SSL","证书","VPN","\n最近在家里开发项目的时候，时常碰到npm安装超时。想了一下手上有好多服务器，应该能解决这个问题，于是我搭建了一个代理服务器。\n\n## 技术方案概述\n\nnpm registry有两个主要地址：\n\n- https:\u002F\u002Fregistry.npmjs.com\n- https:\u002F\u002Fregistry.npmjs.org\n\n需要让npm客户端访问这两个域名的时候走自己的服务器，并突破封锁。\n\n\u003C!-- more -->\n\n本方案概要：\n\n- 使用Nginx搭建反向代理服务器\n- 自签名SSL证书并设置信任\n- 使用TailScale VPN实现网络互通\n- hosts文件配置域名解析\n\n其中TailScale是一个基于WireGuard的现代VPN解决方案，它是本方案中不可或缺的组件，因为它能够帮助我们：\n\n- 通过零配置组网快速建立安全通道\n- 利用 WireGuard 的高性能加密通信确保数据安全\n- 实现 NAT 穿透，突破网络封锁\n- 支持多平台部署\n\n> 为什么不直接使用梯子：一方面是因为梯子流量有限，安装npm包会消耗大量流量；另一方面是我主要使用浏览器插件来分流，因此梯子没有配置按域名分流，无法针对npm的请求进行分流。\n\n> 为什么不使用npm镜像：因为通过npm镜像安装的话，lock文件也会使用镜像地址，这样在CI\u002FCD环境中可能会出现新的问题。\n\n## SSL证书配置与信任\n\n为了确保npm客户端信任我们自己搭的反向代理，我们需要配置并信任自签名证书。整个过程分为三步：生成根证书（CA）、生成服务器证书、添加证书信任。\n\n### 生成根证书（CA）\n\n这个脚本完成以下工作：\n- 生成 2048 位的 CA 私钥\n- 创建证书签名请求（CSR）\n- 设置基本约束和密钥用途\n- 生成有效期为10年的CA证书\n\n```sh\n#!\u002Fbin\u002Fbash\nset -o errexit\n\n# Generate CA private key\nopenssl genrsa -out ca.key 2048\n\n# Generate CA certificate signing request\nopenssl req -new -key ca.key -out ca.csr -sha256 \\\n    -subj \"\u002FC=CN\u002FST=GuangDong\u002FL=Shenzhen\u002FO=Tinkink\u002FOU=Tinkink\u002FCN=tinkink.net\"\n\n# Create ca.ext file\necho \"basicConstraints=CA:TRUE\nkeyUsage=keyCertSign,cRLSign\" > ca.ext\n\n# Generate CA certificate\nopenssl x509 -req -in ca.csr -signkey ca.key -out ca.crt \\\n    -extfile ca.ext -sha256 -days 3650\n```\n\n### 生成服务器证书\n\n这个脚本完成以下工作：\n\n- 生成服务器私钥\n- 创建服务器证书签名请求\n- 配置证书扩展信息，包括多域名支持\n- 使用之前创建的 CA 证书签发服务器证书\n\n```sh\n#!\u002Fbin\u002Fbash\n\n# Generate private key for npm\nopenssl genrsa -out npm.key 2048\n\n# Generate certificate signing request\nopenssl req -new -key npm.key -out npm.csr -sha256 \\\n    -subj \"\u002FC=CN\u002FST=GuangDong\u002FL=Shenzhen\u002FO=Tinkink\u002FOU=Tinkink\u002FCN=registry.npmjs.org\"\n\n# Create npm.ext file\necho \"authorityKeyIdentifier=keyid,issuer\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment\nsubjectAltName=@alt_names\n\n[alt_names]\nDNS.1=registry.npmjs.org\nDNS.2=registry.npmjs.com\" > npm.ext\n\n# Generate certificate\nopenssl x509 -req -in npm.csr -CA ca.crt -CAkey ca.key \\\n    -CAcreateserial -out npm.crt -extfile npm.ext -sha256 -days 720\n```\n\n### 证书信任配置\n\n生成证书后，需要将 CA 证书（ca.crt）添加到系统的信任存储中：\n\n**Windows**:\n\n1. 双击证书文件\n2. 选择\"安装证书\" -> \"本地计算机\"（需要管理员权限）\n3. 选择\"受信任的根证书颁发机构\"\n4. 也可以使用管理员权限运行 PowerShell 命令：\n\n```powershell\nImport-Certificate -FilePath \"ca.crt\" -CertStoreLocation Cert:\\LocalMachine\\Root\n```\n\n**Mac**:\n\n1. 双击证书文件，添加到钥匙串访问\n2. 在钥匙串访问中找到证书，双击展开\n3. 展开\"信任\"选项，将\"使用此证书时\"设置为\"始终信任\"\n\n**Linux**:\n\n```bash\nsudo cp ca.crt \u002Fusr\u002Flocal\u002Fshare\u002Fca-certificates\u002F\nsudo update-ca-certificates\n```\n\n## Nginx 反向代理配置\n\n在服务器上先配置好TailScale VPN，确保本地和服务器在同一个网络中。然后使用以下 Nginx 配置文件：\n\n```nginx\nserver {\n    listen 80;\n    server_name registry.npmjs.com registry.npmjs.org;\n\n    # Redirect all HTTP requests to HTTPS\n    return 301 https:\u002F\u002F$host$request_uri;\n}\n\nserver {\n    listen 443 ssl;\n    server_name registry.npmjs.com registry.npmjs.org;\n\n    ssl_certificate \u002Fetc\u002Fnginx\u002Fssl\u002Fnpm.crt;\n    ssl_certificate_key \u002Fetc\u002Fnginx\u002Fssl\u002Fnpm.key;\n\n    ssl_protocols TLSv1.2 TLSv1.3;\n\n    # Define the DNS resolver\n    resolver 8.8.8.8 8.8.4.4 valid=30s; # You can use Google's public DNS or any resolver you prefer\n    resolver_timeout 5s;\n\n    # Proxy requests to npm registry\n    location \u002F {\n        proxy_pass https:\u002F\u002F$host;\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n\n        # Proxy settings to handle large requests and timeouts\n        proxy_read_timeout 90;\n        proxy_connect_timeout 90;\n        proxy_redirect off;\n\n        # Optionally, if you need to handle large response or request sizes\n        client_max_body_size 50M;\n    }\n}\n```\n\n这里重点说明两个关键配置：\n\n1. `resolver`配置用来指定当nginx访问npm registry时的DNS解析服务器，这里使用了Google的公共DNS服务器，如果不设置的话会报错\n2. `proxy_pass https:\u002F\u002F$host`：使用 `$host` 变量而不是硬编码域名，这样可以支持多个npm registry域名\n\n## hosts 文件配置\n\n要让本地请求指向代理服务器，需要修改 hosts 文件。推荐使用[SwitchHosts](https:\u002F\u002Fgithub.com\u002Foldj\u002FSwitchHosts)。但需要注意，大部分系统下，修改Hosts都需要管理员权限，有些系统还需要专门添加可写权限才能修改成功。\n\n```plaintext\n# NPM registry proxy\n10.x.x.x    registry.npmjs.com registry.npmjs.org\n```\n\n## 结语\n\n通过以上配置，我们就可以使用自己的服务器来代理npm包的下载，目前我已经使用了一段时间，下载速度明显提升，推荐有这个问题且不方便用梯子的也尝试一下。\n\n> 本文部分内容由Cursor（AI驱动的代码编辑器）协助编写和润色。\n",{"id":56,"type":7,"slug":57,"title":58,"date":59,"category":11,"tags":60,"body_markdown":64,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":65},96,"roadmap-of-the-journey-to-xinjiang","一场新疆自驾游的路书","2024-10-24 16:19",[61,62,63],"自驾游","新疆","旅游","熟悉我的朋友都知道，自从今年3月份离职以后，我就一直在家里打柜子。原计划是柜子打完以后再出发自驾新疆，没想到这个柜子一拖再拖，一直没打完。于是心一横，先出发再说。\n\n这一去就接近3个月，直到前几天才返回深圳。路上遇到的美景、美食以及有趣的人和事实在太多，不可尽数，只能先发一个简单的路书，向家人朋友们报一声平安。后续有时间慢慢整理路上的风景、美食和趣事，再单独发出来。\n\n8月1日，我们正式从深圳出发，途经贺州-桂林-清镇（贵阳）-遵义-重庆-成都-陇南-兰州-玉门-哈密，8月中旬到达新疆昌吉。\n\n这一路有很多印象深刻的事：例如在桂林扎胎以后满大街找轮胎店换轮胎，还有成都到陇南的路上遇到高速上翻车事故被迫停在高速休息等等。\n\n![回新疆](\u002Fassets\u002Flife\u002F2024\u002Froadmap-of-the-journey-to-xinjiang\u002F01.jpeg)\n\n8月中旬到达新疆，随后一个多月在新疆休息，中间出去了几趟，零零碎碎跑了一些地方：\n\n- 克拉玛依-塔城-小白杨哨所（G219国道）-博乐-赛里木湖\n- 乌鲁木齐-S101国防公路-安集海大峡谷-石河子\n\n这一路主要是在填补我对新疆的空白印象，比如虽然去过新疆很多次，但直到这一次才认真逛了一下乌鲁木齐。\n\n因为新疆非常大，随便一出门都是三五百公里起步，我们便没有去到太多的地方，以后还会有很多机会去慢慢探索。\n\n![新疆游](\u002Fassets\u002Flife\u002F2024\u002Froadmap-of-the-journey-to-xinjiang\u002F02.jpeg)\n\n9月中旬，过完中秋以后天开始明显冷起来了，考虑到电车续航的问题，我们也就启程从新疆出发了。\n\n大概路线：昌吉-吐鲁番-哈密-玉门-酒泉（金塔胡杨林）-张掖-武威（冰沟河）-兰州-甘南-九寨沟-陇南-礼县-天水-麻阳\n\n这一程唯一的目的地是10月2日抵达湖南麻阳参加朋友婚礼，因此走得非常悠闲，走了非常多的国道、省道、县道、乡道，体验了很多不走高速才能看得到的奇美风景。\n\n从行程也能看得出，线路上走得非常随意，比如我们已经从武威开出去好远了，又临时决定要去冰沟河和乌梢岭，于是下了高速调头再走；又比如本来要从兰州去天水的，临时加入了甘南的行程，在甘南转了半天之后又临时决定拐去九寨沟。\n\n总而言之，这是我感受最好的一段自驾路线：不赶时间，随遇而安，总在邂逅计划之外的风景。\n\n![回湖北](\u002Fassets\u002Flife\u002F2024\u002Froadmap-of-the-journey-to-xinjiang\u002F03.jpeg)\n\n十一过后从湖北再次出发，经河南 安徽 浙江，随后返回深圳。\n\n这一程去的几个地方都印象比较深刻：襄阳、洛阳、开封、黟县、义乌，各有不同的历史文化背景和城市面貌。\n\n但是因为没有了具体的目标时间和目的地，多少有一些迷失感，总是在纠结接下来应该去哪。因此最后结束得也很匆忙：在义乌逛完小商品市场，连夜奔袭回到深圳。\n\n![回深圳](\u002Fassets\u002Flife\u002F2024\u002Froadmap-of-the-journey-to-xinjiang\u002F04.jpeg)\n\n总而言之，一段历时接近3个月的自驾结束了，全程17000多公里。接下来又将回到打柜子的日常生活。\n\n如果你对这段行程有什么感兴趣的地方，也欢迎留言或者找我私聊，我会在后续分享更多行程中的见闻。\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Flife\u002F2024\u002Froadmap-of-the-journey-to-xinjiang\u002F01.jpeg",{"id":67,"type":7,"slug":68,"title":69,"date":70,"category":25,"tags":71,"body_markdown":76,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":43},138,"synology-nas-data-recovery","记一次群晖NAS数据恢复","2024-07-31 12:08",[72,73,74,75],"NAS","群晖","数据恢复","Linux","\n## 背景：群晖坏了 买个二手\n\n我在2019年购入了群晖DS218+作为家里的主要存储设备，几年来一直正常使用，除了归档的文件之外，还存了家里几个人所有的照片视频，并时常从手机上自动备份新的照片视频。\n\n前几天，我突然发现群晖所有的灯都不亮了，一开始以为是某次关机后没有再次开机，于是手工开机试了一下，才发现没有任何反应。在排除了电源的问题后，我开始怀疑是NAS硬件出了问题。\n\n自己拆开看了一下，发现主板上有一颗芯片有明显的烧毁痕迹，应该是短路导致的。鉴于这已经超出我的能力范围，于是在网上找了一家维修店，后来对方反馈除了我看到的芯片短路之外，EC芯片也有问题，而EC芯片中有加密的程序，导致第三方无法维修。\n\n经过5分钟的快速思考，我决定先买个二手低端机顶一下，能正常读写数据和备份照片即可，至于其他的后续再打算。于是就在闲鱼上淘了一台DS218J，于是踩坑之旅正式开始。\n\n\u003C!-- more -->\n\n## 数据恢复方案\n\n一开始想得很简单：直接把硬盘换到新的机器上，然后就能一切照旧。但在我兴奋地插上两块硬盘之后，发现新的机器上无法看到原来的文件夹，看了下存储管理器，发现原来DS218J不能使用BTRFS文件系统，而我的DS218+上的硬盘正是BTRFS文件系统！\n\n对于这一点，我至今仍然在震惊当中，BTRFS是Linux底层的文件系统，为什么群晖会在不同型号的机器上使用不同的文件系统？\n\n于是开始手忙脚乱地设计数据恢复方案，我所拥有的条件：\n\n1. 一台DS218J，不支持BTRFS文件系统\n2. 两块RAID 1硬盘，BTRFS文件系统\n3. 一台macbook笔记本，可以安装Linux虚拟机\n4. 一个USB 3.0硬盘盒\n5. 一个macbook扩展坞，集充电、HDMI、USB、网络接口等功能于一体\n6. 一个国外的网络存储，有和硬盘中完全一样的数据备份\n\n最终设计的数据恢复方案（省略中间各种决策过程）：\n\n- 将原本作为RAID 1使用的两块硬盘拆开使用，一块放入USB 3.0硬盘盒中，另一块放入DS218J中\n- 将USB 3.0硬盘盒通过扩展坞连接到macbook上，使用Linux虚拟机挂载硬盘\n- 通过虚拟机文件共享机制，先将硬盘上的数据复制到macbook上\n- 逐步将macbook上的数据复制到DS218J中\n- 当所有的数据全部存入DS218J中的硬盘之后，再重新将两块硬盘组成RAID 1\n\n## 操作步骤\n\n参考[群晖官方文档](https:\u002F\u002Fkb.synology.cn\u002Fzh-cn\u002FDSM\u002Ftutorial\u002FHow_can_I_recover_data_from_my_DiskStation_using_a_PC#x_anchor_idenvironment1)\n\n```sh\n# 安装mdadm和lvm2\napt-get update\napt-get install -y mdadm lvm2\n\n# 重组磁盘阵列\nmdadm -AsfR && vgchange -ay\n\n# 获取磁盘阵列的信息\ncat \u002Fproc\u002Fmdstat\nlvs\n\n# 只读挂载目录\nmkdir \u002Fmnt\u002Fds\nmount \u002Fdev\u002Fvg1000\u002Flv \u002Fmnt\u002Fds -o ro\n```\n\n## 踩的一些坑和注意事项\n\n1. 虚拟机必须用Ubuntu 18.04.4以下的版本，否则在读取磁盘阵列的时候会出现错误，具体原因可参见[这篇文章](https:\u002F\u002Fyadom.in\u002Farchives\u002Fmount-synology-hard-drive-on-linux.html)。\n2. 虚拟机软件用的UTM，可能是因为USB 3.0支持有问题，当macOS中显示的扩展坞是USB 3.0 Hub时，虚拟机中无法识别硬盘，最终我找了一根“不那么厉害”的线，插上去之后macOS中显示为USB 2.0 Hub，此时才能在虚拟机中识别硬盘，但USB 2.0速度很慢，这导致从硬盘中读取文件需要很长时间\n3. 由于硬盘是BTRFS文件系统，所以在虚拟机中挂载硬盘的时候，需要先安装BTRFS工具，否则会报错。\n4. 文件传输到DS218J速度极慢，尤其是大量小文件时，速度可能只有20M\u002Fs左右，大约1G\u002F分钟，并且无论是SMB协议还是NFS、FTP协议，速度都差不多，原因不明。\n\n## 一些感想\n\n1. 硬件产品还是挺麻烦的，不出问题的时候啥都好，一旦出问题就得全靠自己\n2. 数据备份非常重要，尽管群晖坏了，但我完全不担心数据会丢失，因为硬盘是好的，且两块硬盘互为备份，而且还有一份在网络存储中\n3. 除了数据不丢之外，数据能被访问到也很重要，比如这次群晖坏掉之后所有的数据都无法访问\n4. 群晖的官方文档还是比较清晰的，只不过我在查了很多第三方资料，踩了很多坑之后才发现官方文档，浪费了不少时间\n",{"id":78,"type":7,"slug":79,"title":80,"date":81,"category":82,"tags":83,"body_markdown":86,"permalink":15,"excerpt_src":15,"media_type":87,"media_title":88,"media_author":89,"media_url":90,"rating":91,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":92},20,"podcast-fraud-murder-mystery-game","电信诈骗一手音频资料的听后感","2024-03-05 00:00:00","bmm",[84,85],"播客","诈骗","\n![反诈宣传](\u002Fassets\u002Fbmm\u002F2024\u002Fpodcast-fraud-murder-mystery-game\u002F01.jpg)\n\n开车上下班的时候听了《故事FM》最近的两期关于电信诈骗的讲述：\n\n- 上集\n  - 苹果播客 https:\u002F\u002Fpodcasts.apple.com\u002Fcn\u002Fpodcast\u002F%E6%95%85%E4%BA%8Bfm\u002Fid1256399960?i=1000647366054\n  - 豆瓣 https:\u002F\u002Fwww.douban.com\u002Fpodcast_episode\u002F183718\n- 下集\n  - 苹果播客 https:\u002F\u002Fpodcasts.apple.com\u002Fcn\u002Fpodcast\u002F%E6%95%85%E4%BA%8Bfm\u002Fid1256399960?i=1000647648796\n  - 豆瓣 https:\u002F\u002Fwww.douban.com\u002Fpodcast_episode\u002F184291\n\n电信诈骗这几年受关注度很高，大家也都接受了非常多的反诈教育，但听完这两期播客之后，仍然非常想推荐给大家听。\n\n这两期节目中，有一手的诈骗电话录音，有被骗者一手的讲述，能更清晰直白地了解到整个电信诈骗过程是如何进行的。\n\n促使我想写点文字的理由却是另一个方面：一个人被电信诈骗，与哪些认知\u002F习惯有关。\n\n\u003C!-- more -->\n\n## 对坏人的态度\n\n家长会尽力地保护自己的小孩不受坏人的影响，而大部分家长保护的方式是隔离。大部分的小孩从来没有接触过坏人，甚至有坏人的影视节目也不能看。\n\n这样的教育自然会为孩子带来好的结果：阳光、乐观、善良，但与此同时也会失去一些让他们面对负面情况的机会。\n\n播客中有一个被骗的女孩，她详细讲述了骗子的种种不合理行为，我一直觉得非常纳闷，为什么非常明显的问题都没有识别出来，直到听到下面这句，突然明白过来：她应该几乎没有面对过任何负面的人际关系。\n\n这个女孩最后反思的时候说：“这是我长这么大第一次感受到被人骗，我还那么相信他们。”\n\n而在节目的最后，她被问到，如果你再见到骗你的那位“警官”，你想和他说点什么，她的回答是“让他请我吃顿饭”。\n\n一个骗了自己几十万、且让自己几个月的时间身心受尽折磨的人，当你有机会再见他时，居然想的是让对方请吃饭。\n\n**坏人就是坏人，我们可以不挑起战争，但不可以没有武器。**\n\n## 面子\n\n被骗的过程中，有一个很关键的节点：对方提出需要他回国处理，她答应之后，对方却开始强调“你是以犯罪嫌疑人的身份回来，你的家长会很难堪”，于是她退缩了，打算瞒着家人独自处理。\n\n这是一个非常典型的“面子”问题：如果果断选择回国处理，就不会有后续的诈骗发生，而阻止她选择回国处理的只是因为父母会没面子。\n\n这类问题在我的生活中也经常出现，但我的选择是直面问题，面子不重要。\n\n**长期坚持实事求是，是一种综合成本更低的生活方式。**\n\n你不再需要花时间揣摩别人的心思，不需要花时间维护自己的面子，也不需要在乎别人的评价。\n\n## 亲密关系\n\n节目中有一个没有被骗的女孩，因为她在挂完电话第一时间就把这件事告诉了室友。而被骗的女孩则老老实实听了骗子的话，几个月的时间一直没有对自己的爸妈、同居的男朋友、公司的领导和同事透露任何信息。\n\n我觉得这也是一个典型的不会认知和处理亲密关系的表现。\n\n我曾经有很长时间都在思考以前看到过的一个问题：如果你发现你的父亲有问题，你会不会举报他？我相信在六七十年代，有非常多的人真实地面对过这个问题，大家也做出了各自不同的选择。\n\n后来我逐步倾向于一个观点：**亲密关系是不应该被外界力量打破的，即使这个外界力量是看起来很正义的。**\n\n人需要亲密关系来承载自己的另一部分，包括感情寄托、安全感、决策支持等等。\n\n我不知道这个女孩有怎样的成长经历，但我觉得如果有人在亲密关系中给了她足够的安全感和依赖感，事情也许会不一样。\n\n## 权威和程序正义\n\n冒充公检法的诈骗之所以成功，很大一部分原因是因为人们对权威的盲目信任和恐惧：一听公安要找自己，立刻就慌了，然后任人摆布。\n\n要破解这个问题，确实需要一些认知上的更新，对于个人来说，我觉得这需要2个方向上的努力：\n\n1. 让自己变成流氓：不怕事，不管你是什么机关也好，你都不能把我怎么样\n2. 让自己变成律师：懂程序，懂权利，你要找我可以，请按程序来\n\n关于第2点，即使是看看电视剧也能知道一些常识：\n\n1. 公安机关是不会电话办案的\n2. 公安机关是不会当着你的面交接给检察院\u002F检察官的\n3. 公安机关是不会让你交保证金的（你又不是保释）\n4. 保密案件的保密义务是针对工作人员的，不是针对当事人的\n5. 你有权找律师\n\n## 信任链\n\n在安全领域，有个信任链的概念，即所有你觉得可信的东西，都可以一层一层往下推导信任的来源，并有一个最终的可信源。举一些例子：\n\n- HTTPS的可信源是操作系统内置的根证书\n- iPhone的可信源是苹果公司\n- 货币的可信源是国家信用\n\n如果你相信对面的人是警察，那是因为他出示了警察证；你相信这个警察证是真的，是因为你相信这个警察证是由公安部门颁发的；你相信公安部门的权威，是因为你相信国家的权威。\n\n而在电信诈骗中，最开始的信任源往往是“来电显示”，因为手机显示这个号码是110，所以你相信对方是警察。但“来电显示”这个信任源是可以伪造的，所以这条信任链从一开始就是错误的。\n\n普通人并不容易识别类似的技术上的问题，但我们可以用一个非常简单的方法来破解这个问题：**主动发起联系。**\n\n简单地说，如果对方自称是警察，可以问清楚具体的工作单位，然后自己找到查号台打电话拨过去再继续聊之前的事。\n\n## 深渊\n\n播客的最后有一段对骗子的采访，我觉得是极为少见的一段资料。逻辑还是只有一句话：当坏人是逼不得已。\n\n他还算清醒，尽管在憧憬日后还能娶妻生子，但自己也知道没有别的路可走。\n\n**一旦滑入深渊，就再也没有回头路了。**\n","podcast","故事FM","寇爱哲","https:\u002F\u002Fwww.douban.com\u002Fpodcast\u002F36722786\u002F",5,"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fbmm\u002F2024\u002Fpodcast-fraud-murder-mystery-game\u002F01.jpg",{"id":94,"type":7,"slug":95,"title":96,"date":97,"category":11,"tags":98,"body_markdown":100,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":43},97,"stages-of-life-a-summary-for-2023","人生有阶段——写给2023年的小结","2024-02-06 17:30",[99],"年度总结","\n2023年已经过去，当我想着要给这一年写一段小结的时候，很自然地冒出来了这样一个标题——人生有阶段。\n\n是的，人生是有不同的阶段的。这是我在2023年最深刻的感悟。\n\n## 一、一段轻松的开始\n\n这一年是从一种轻松的节奏中开始的，这种轻松首先来自于过去三年无比压抑的疫情管控彻底放开。尽管很多人心有余悸，但对我这种早就不耐烦的人来说无异于一种莫大的解脱。\n\n春节在新疆度过，虽然已经在那里过过好几次春节，但这一次特别清静。在家的时候永远是四五个人，家长各忙各的事，我们也有机会自己带着小麦去零下30度的室外玩雪，自己去逛街逛商场，还有步行到亲戚家串门。\n\n和以往一样，春节前我往往是家里最后一个放假休息的人，于是早早让家人先回了新疆，我放假之后再一个人坐飞机回去过春节。节后也是自己先过来深圳，于是我又收获了一段独处的时间。\n\n每到周末的时候我就自己背着包，带上我的电脑，随便去找一个没有去过或者并不熟悉的地方，寻一个书店或者咖啡店去坐一天，在那边写一写代码，发一发文章，或者拍一拍照片，天黑了还能再去酒吧听一晚爵士再回家。\n\n3月份家人都回来了深圳，就换了一种节奏，一到周末就带娃出门，在深圳逛了很多地方，也去了顺德、泉州、赣州、惠州，很累但也很快乐。\n\n\u003C!-- more -->\n\n## 二、一件大事\n\n因为参与投资市场的关系，顺便也有关注到房价。年初回来深圳后和家人简单提过想把房子卖了，但这毕竟是一件大事，提过之后就不了了之。直到4月份，房价已经往下跌很多了，我们也想了很久，于是正式去找中介挂盘卖房了。\n\n我们的房子在深圳的中心区福田区，位置非常好，但是房子很小。从房子的窗户往外看，能看到对面有一个新的楼盘正在建，之前有考虑到那里换一个大一点的房子。但我骨子里一直不认可在房子上花太多钱，如果一套房要花出一千多万的话，心理上很难接受。\n\n你知道一个成年人能拎得动多少现金吗？大概也就一两百万。一千万摆到面前是你得花好几趟才能搬完的一座小山了，以我的收入，也需要几十年才可能攒到这个数，但如果你买房，花掉它只需要1分钟。这是一种很不划算的行动。\n\n但房子面积小也是事实，于是我们决定去深圳的郊区——坪山区。刚好下半年在坪山区看到了一个很满意的户型，而且年底就能交付。于是我们就这么买房了。\n\n到目前为止，旧房子已经成功卖出，新房子也已经交付了。尽管还有很多事项在持续跟进中，但总算是一件尘埃落定的大事了。\n\n> 在卖房的过程中还出现过很多和中介之间的恶心事，很多时候你需要让自己浑身长刺，变成一个拿着刀的恶人，才能把事情办了。尽管我知道这个社会有些角落就是这么运作的，但仍然觉得非常恶心，我希望自己永远不要再与这样的行业这样的人打交代，永远不要变成一个靠发疯去办事的人。\n\n## 三、一段中年人的经历\n\n> 我很想把这段经历写成一篇小说，但确实没有空，就趁年终总结的时候一起来写一写吧。\n\n在[2018年小结](\u002Farticle\u002Flife\u002F2019\u002F2018-summary.html)中，我很顽固地拒绝了三十岁的到来所带来的新阶段，并写下了“心怀正念，保持年轻，事在人为，听天由命”，希望自己能保持住年轻的心态。然而这一段中年人的经历却让我相信，人生的确是有不同的阶段了，我确实是一个中年人了。\n\n中年人，尤其是中年男人的核心特征是油腻。我一直在尽力避免让自己变得油腻。但回过头来看的话，人到中年还有一句常常被挂在嘴边的话——上有老下有小。\n\n### 开端：工程师思维的危机\n\n8月份一个普通的夜晚，主卧的灯坏了。\n\n之后的某一天，家里人刚洗完澡正吹着头发，突然电路跳闸了，我跑去合上了开关，一切恢复正常。第二天同样的故事再次上演，那么很容易得出结论：吹风机有问题了，会导致跳闸。\n\n第二天家里人去买了一个新的吹风机。晚上，大家都发现新的吹风机很好用，于是我也去用了一下。啪！又断电了。\n\n好了，这下故事有意思了，吹风机可能是无辜的，也许是其他的电器或者电路出问题了。到了第三天晚上，必须得找到问题了。\n\n按照找bug的思路，先分析了一下，家里有五条回路，于是就逐个去关掉回路开关，但跳闸并没有呈现出什么和回路相关的规律，反而越来越频繁。于是最终不了了之，在没有空调没有风扇的晚上扛过了一夜（小麦热得坐起来了，他可能不太明白，为什么明明这么困却又睡不着）。\n\n第二天一大早叫来了电工，我正要和他同步我的试验情况，结果人家连电箱都没拆，直接说空气开关坏了，去买一个回来再叫他来安装。我照办，然后这个困扰了几天的问题就这么解决了。\n\n这件事情让我回味了很久：这是一个我不熟悉的领域，那么按照工程师的思维方式来排查是没有错的，但最终并没有解决问题。而一个经验丰富的电工过来，连排查都不需要，只需要一眼就解决了问题。这背后可解读的点实在是太多，也许先成为一个电工更切实际一些。毕竟事情还没完……\n\n还记得一开始说的主卧的灯坏了吗？本来是坏一个灯泡的事，但家人拿着旧灯泡去五金市场，老板们都说这种灯早就淘汰了。于是回来拼多多买了一个灯泡，换上后好了。\n\n然后……次卧的也坏了。我一看，同款，赶紧又下单了一个，但安装的时候却发现两个卧室的灯大小不一样，费了九牛二虎之力才勉强卡上去。\n\n再然后……厨房的灯也坏了，这次换了个思路，没再去买灯泡，而是直接买了一个新的平板灯，好在换起来不难。\n\n还有还有……\n\n- 车钥匙在洗衣机洗了，坏了……\n- 备用车钥匙没电了……\n- Airpods Pro杂音……\n- 洗衣机坏了……\n\n总之那一阵基本上就是这个坏完那个坏，我就每天不停地在买东西换东西修东西……\n\n### 祸不单行\n\n在修好电路的那一刻，我兴奋地打开了电视，点开了一个电影，刚看了一会家人就接到电话：小麦姥姥摔了。\n\n兵分两路，我跑到地下车库去开车，接上人就往医院赶，十来分钟到了医院：肋骨骨折。\n\n至此，3个人带1个小孩，变成了2个人带1个老人一个小孩，而我仍然要上班，就变成了1拖2，这大概比单独带小孩更令人崩溃。\n\n### 一波三折\n\n接下来的日子大概就是一年中最难熬的一段，一边姥姥在卧床休息，另一边奶奶也出状况。\n\n>奶奶年初在家膝盖受伤，然后一直不能正常站立和行走，一边觉得疼，一边又觉得神经麻木。最初在老家镇上医院看，后来到县里市里，折腾了几个月也没个准确结论。大概是会有膝盖和腰椎两处的问题，可能要手术。\n>\n>我们一合计，既然要手术，那还不如来广东，毕竟医疗条件好一些，而且两个儿子都在广东，好照顾一些。\n>\n>于是先去了广州中山三院看了膝盖，然后又转到了神经内科住院。\n>\n>这一段经历简直可以写书：从住院开始就没有见过主治医生，三四天一直在开检查单，从脚到大脑全部检查一遍，并且在没有任何诊断的情况下就开始用药，更神奇的是，我们每天从东莞和深圳一大早跑过去，从来没见过医生查房。\n>\n>在意识到不对后我们去门诊堵到了主治医生，一番争吵以后最终不欢而散。于是马上办理出院手续，把奶奶接到了深圳。\n\n好了，至此家里有了卧床的姥姥和卧床的奶奶，活蹦乱跳的小麦，还有照顾他们的小麦妈妈，以及晚上才会出现在家里的我。\n\n好在奶奶在深圳的检查比较顺利，需要做一个腰椎手术，很快就住院和手术了。\n\n这段时间就是一大早去医院看看情况，然后白天上班，下班后再去医院看一下，待一两个小时再回家。\n\n在奶奶手术完回家之后，姥姥稍微好一些了，但很快又出现新的症状，折腾2天后我们拿出了新冠试纸，测了一下发现姥姥阳了，而且这是她首次阳性，症状很重。于是又是漫长的一周多的时间。\n\n9月底是我生日，到这一天，好像这些事情终于都告一段落了，我们去吃了个海底捞，为这一段煎熬的经历画了一个句号。\n\n> 其实中间还穿插了一段买新房的经历，但过于顺利，并没有带来心理上的煎熬，就不多写。\n\n## 一些自我的回归\n\n### 开发的乐趣\n\n\n我一直有在用一个统计写代码时间的工具，它去年发给我的年终总结中写的平均每天编码时间是40分钟。这个数据一度让我感到惶恐：我已经离写代码好远了。\n\n年初那段悠闲自在的时光里，我打算重新学习一下移动端的开发，于是简单移植了一个很久以前看到过的小游戏：东方格。\n\n>也因为这个原因，在M2芯片的Macbook Air出来不久我就入手了一台。\n\n我已经很久没有发布过任何新的产品了，我记得那是一个早上，我刚下楼要去上班，就看到了我的小游戏审核通过的邮件，于是赶紧发了个朋友圈。\n\n这件小事让我兴奋了好一阵，感觉自己好像重新找回了一些写代码做产品的乐趣。尤其是在推上收到一些朋友的建议时更觉得这是一个奇妙的世界。\n\n除此之外我还花了大半年的业余时间做了一个完整的邮箱系统 TinkMail，包括收信、发信、web界面、SMTP、IMAP等完整功能。\n\n时至今日，邮箱已经是一个完全蓝海的领域，但我仍然想尝试一下，一方面是用来满足自己的好奇心，非常想知道邮箱系统到底是如何运作的，另一方面也想一路做到商业化，不一定有多少营收，但需要感受商业化的产品是怎么回事。\n\n做的过程中也能发现同行的很多问题，比如QQ域名邮箱的TLS证书缺失、QQ邮箱在协议层面不能正确处理邮件回复等等。\n\n\n### AI带来的冲动\n\nAI在这一年大放异彩，上半年几乎每天都能见到好几个新产品，到下半年则不断看到一些在突破能力边界的产品。\n\n这种感受非常像当年互联网产品大爆发的时候，每天一睁眼就有新的东西可以玩。而不像现在，每天都在用十年前的产品。\n\n年初给自己定目标的时候其实写了要学习AI，也有常识去看机器学习入门教程，但终归是没能深入。光是学习AI应用的使用方法都已经不够时间了。\n\n希望在未来花些时间补补课，如果有机会也可以尝试做一些应用，毕竟要持续学习的话，最好的方式是入局。\n\n### 享受一点点运营\n\n我以前做过几个内容站点，最后无一例外都关掉了，原因是经过这么多年的折磨，最终还是觉得自己没有耐心去运营一个网站的内容。\n\n但在2023年我又开了一个内容为主的教程站，并且开始认真运营。第一次认真关注SEO并不断优化，第一次真正在意关键词的排名和流量来源，也是第一次接入在线广告。到目前为止每个月有2-3刀的广告收入，虽然不多，但我应该不会再关掉它了。\n\n这也让我有一些新的想法：也许我并不是没有耐心去运营，而是没有找到及时得到正反馈的方法。也许在以后还可能会把以前关掉的产品再拿出来重新审视一下是否能继续上线运营。\n\n### 重新与世界产生连接\n\n过去很长一段时间内，我在网上几乎是陌生人隔绝的。在某一些阶段，这是一种必要的生活方式，因为减少与世界的连接能让自己更关注手上的事情。\n\n但减少与世界连接的同时，也会面临一些心理上的问题：需要不断靠自己来确认自己的位置和价值，否则就很容易产生无意义感。\n\n在这一年我加大了在推上的活跃度，有了更多与陌生网友交流的机会，也开始重新写公众号，尽量发出来的没有几篇，但与之前相比已经很多了。\n\n很开心的是，不管是公众号、朋友圈还是推特，有不少人反馈觉得这些文字很有意义，希望我能多写一些。我想，与世界连接不仅仅是让自己有所收获，也许自己的某些不经意的想法也能给到他人启发，以后多写写吧。\n\n我在去年底也建了一个关于书影音和播客的群，只是遗憾目前不太活跃，希望后面有空的时候多花些心思。\n\n## 一些小事\n\n我已经将我的手机、电脑都设置为英文界面一两年了，直到2023年才算真的习惯全是英语的世界。在搜索资料的时候也已经习惯使用英文来搜索。\n\n如果要说带来了非常多的改变，那倒确实是没有，但主要从心理上不再有任何畏惧英语的想法，也不再想着什么资料都先看看中文的。相信这将为以后更加国际化的网络冲浪生活带来打下更多基础。\n\n这一年听了很多播客节目，也算是正式入门了（听）播客这一领域。认真听下来也会发现真的有很多优质的节目，可能那些在传统社交网站上逐步闭嘴的人，在播客中还能再说一说实话。希望这个领域的质量能保持得久一些。\n\n最后的最后，又骑了一年车，体验非常不错。\n\n## 小结\n\n如果用一个词来总结这一年的话，那应该是“印象深刻”，有很多经历是之前没有过的，这里面既有被动应对的部分，也有主动选择的部分。在未来的若干年往回望的时候，2023都是一个不可能被忽略的年份。\n\n到2023年结束的时候，很多事情也都告一段落了，从人生经历的角度来说，可能也算是一个阶段的结束。\n\n下一个阶段即将开始，希望能继续有所爱有所得。\n",{"id":102,"type":7,"slug":103,"title":104,"date":105,"category":82,"tags":106,"body_markdown":108,"permalink":15,"excerpt_src":15,"media_type":109,"media_title":110,"media_author":111,"media_url":112,"rating":91,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":113},19,"the-eighth-suspect","电影《第八个嫌疑人》","2023-11-20 00:00:00",[107],"电影","\n![第八个嫌疑人](\u002Fassets\u002Fbmm\u002F2023\u002Fthe-eighth-suspect\u002F01.jpeg)\n\n周末趁娃睡觉的时候刷了刷电影列表，看到了《第八个嫌疑人》，是今年的新片。\n\n说是犯罪、悬疑片，但整体情节很简单，根据90年代真实银行抢劫案改编，并没有什么太出格的剧情或者大反转什么的。\n\n促使我想记录一下的原因还是这部电影的细节。\n\n\u003C!--more-->\n\n首先是人物的造型，用通俗地话说就是大家都能豁得出去，90年代的造型就有90年代的味道，该风光的时候风光，该落魄的时候落魄，完全不会有让人出戏的感觉。\n\n我觉得全片拍得最好的是两位主角在云南第一次见面的戏（有剧透）：警察通过编小时候的故事确认对方的身份是伪造的，然后在饭桌上通过对方和父亲的合影表明自己的身份，并通知对方已经知道他伪造身份的事，然后将父亲说成是老友，告知对方父亲已过世。整场戏的情绪非常到位，两位主角的心理活动有来有回，虽然饭桌上的孩子和妻子什么都没听出来，但两个人已经交换完了所有的信息，并且也告知了对方自己的态度。\n\n除此之外，人物的塑造上也还比较丰满。虽然人物的来历未见很完整，但是在感情的表达上还是着了重墨，无论黑白两道，主要的人物都有内心深处隐藏着的感情和煎熬。\n\n另外一个让我觉得处理得也很好的点，是这个电影并不执着于传统犯罪片要将所有细节全部交代清楚的套路，而是选择通过人物自己的行为让观众推测其前后的事情经过。比如劫的士的镜头反复出现了很多次，却并不交代其前因后果；比如影片的最后，也并不交代最终的处罚结果。反而在整个剧情中把最动人的部分交代得很清楚，例如心理活动导致的挣扎，痛苦导致的崩溃等等。\n\n翻了下豆瓣只有6.1分，但不重要，我愿意给10分。\n","movie","第八个嫌疑人","李子俊","https:\u002F\u002Fmovie.douban.com\u002Fsubject\u002F34436452\u002F","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fbmm\u002F2023\u002Fthe-eighth-suspect\u002F01.jpeg",{"id":115,"type":7,"slug":116,"title":117,"date":118,"category":82,"tags":119,"body_markdown":120,"permalink":15,"excerpt_src":15,"media_type":109,"media_title":121,"media_author":122,"media_url":123,"rating":91,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":124},18,"the-blind-side","电影《弱点》","2023-10-07 00:00:00",[107],"\n![弱点电影封面](\u002Fassets\u002Fbmm\u002F2023\u002Fthe-blind-side\u002F01.jpeg)\n\n周末趁娃睡觉的时间刷了刷电影，偶尔看到了这部《弱点》，豆瓣评分8.7，就点开看了一下。\n\n影片的简介说的是一个橄榄球运动员的成长故事，但看完电影发现完全不是这么回事。\n\n\u003C!-- more -->\n\n从片尾来看，这部电影来自真实故事的改编，剧情其实挺简单的：一家人的儿子在学校上学，后来妈妈发现一个黑人同学无家可归，于是将他带回来，后来帮助他成为一名出色的橄榄球员并进入了大学。\n\n本来以为是一部励志片，但出乎意料地发现它原来是一部温情片。而促使我想写几行文字记录一下的原因，则是源于我在看片的时候产生的不安感。\n\n我看电影一向非常认真，但是在看这部片的时候，我居然几次回过神来，发现自己手放在下巴上，非常紧张。在电影结束后我开始仔细思考，这种紧张不安是从何而来。\n\n大约是已经习惯了影视剧一定要有剧烈冲突的设定，当我看到这个孩子被带到学校、被老师同学善待、被妈妈收留的时候，就总是在担心下一幕会有天大的悲剧从天而降，把前面铺垫下来的美好彻底毁掉。\n\n即使是当电影中发生车祸，也即真的产生了冲突的时候，这部电影仍然没有仔细去渲染，只是用了短短一两分钟，寥寥几个镜头就交代完了整场车祸。\n\n当然，这个镜头也让我想到了一些别的东西：一部电影，把它最剧烈的冲突通过点到即止的方式交代完，但仍然让观众感受到其中的紧张、冲突和爱，说明电影的张力非常强。这大概也是我从一开始就被带入进入，陷入持续紧张的原因。\n\n当下，我们正处在一种非常撕裂的社会氛围中，没有人再将善良当作美德，很多时候善良会意味着软弱和被欺负，而媒体也在不断渲染各种激烈的情绪，加剧这种撕裂。好在，这部电影在这两个方面都给了我极大的安慰。\n\n五星推荐。\n","弱点","约翰·李·汉考克","https:\u002F\u002Fmovie.douban.com\u002Fsubject\u002F3552028\u002F","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fbmm\u002F2023\u002Fthe-blind-side\u002F01.jpeg",{"id":126,"type":7,"slug":127,"title":128,"date":129,"category":25,"tags":130,"body_markdown":135,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":136},132,"blog-upgrade","个人主页5.0升级全记录","2023-09-28 16:00",[131,132,133,134],"博客","个人主页","升级","VitePress","\n我大概是从2007年起开始写博客的，过去这么多年中，也有更新非常频繁的时间，但总体来看最近几年的确是写得越来越少了。随着公众号、短视频的兴起，大家也不再关注博客，而我也一度不怎么更新，甚至想不起来要写一写文字。\n\n但是最近两年，我又突然发现，虽然写内容的平台到处都是，但只有博客是真正属于自己的，那种永远不会背叛你的安全感是其他平台无法比拟的。\n\n再回头看看自己的博客，发现已经很久没有更新过了，而前一版整理的[AMP版本](\u002Farticle\u002Fweb\u002F2017\u002Fmigrate-blog-to-amp.html)，现在回头看也算是掉坑里了。刚好看到勾股写的[博客站迁移至 VitePress 的备忘](https:\u002F\u002Fjiongks.name\u002Fblog\u002Fmigrate-my-blog-to-vitepress)，于是就打算自己也再整理一下，顺便做一个升级，拍个脑袋就叫“个人主页5.0”。\n\n以下为过程中的一些记录。\n\n\u003C!-- more -->\n\n![首页截图](\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F01.png)\n\n## 换程序\n\n之前使用的是Hexo，是一个典型的“静态博客”，即所有内容都在构建时生成，托管时只有静态的HTML和对应的资源文件。这次打算换成[VitePress](https:\u002F\u002Fvitepress.dev\u002F)，也同样是“静态”的，因此核心概念和大致用法上比较类似。按照VitePress的文档，首先安装好依赖，然后将文章（`.md`文件）放到`docs`目录下即可。\n\n由于之前使用Hexo时就在路由中加了一级`\u002Farticle`，本次迁移过程中也需要建立一个对应的子目录，因此将文章目录由`source\u002F_posts`修改为`docs\u002Farticle`即可。\n\n与之对应的，构建脚本也需要做修改，以前用`hexo server`和`hexo generate`，现在换成`vitepress dev docs`和`vitepress build docs`。需要注意的是，VitePress是一个“极为先进”的项目，所以需要在`package.json`中加入`\"type\": \"module\"`，否则会报错。\n\n图片的引用路径同样需要修改，按照VitePress的文档，图片最好放在`docs`目录下的子目录，例如`docs\u002Fassets`，然后在文章中使用相对路径引用即可。但最终我还是将图片放到了`docs\u002Fpublic\u002Fassets`目录下，具体原因见后文。\n\nVitePress的主题是通过`docs\u002F.vitepress\u002Ftheme`目录下的文件来定义的，其中`index.js`是主要的入口文件。我们可以自定义一个新的主题来使用，但如果这样做的话默认主题的一些功能就会丢失，例如暗黑模式、顶部导航、404页面等，于是我选择了直接修改默认主题，即从代码仓库中将整个默认主题拷贝过来，然后针对个别地方进行修改。\n\n基本上经过简单的替换以后，文章详情页就可以正常访问了，但是还有一些页面需要处理，例如首页、文章列表页以及一些固定的页面（如[关于](\u002Fabout.html)）。\n\n## 导航&固定页面\n\n固定页面的实现是最简单的，只需要在`docs`目录下新建一个`.md`文件即可，例如`docs\u002Fabout.md`，就能通过`\u002Fabout.html`访问。\n\nVitePress中顶部导航是通过`docs\u002F.vitepress\u002Fconfig.js`中的`themeConfig.nav`配置来实现的：\n\n```js\nthemeConfig: {\n    logo: '\u002Flogo.png',\n    nav: [\n        { text: '首页', link: '\u002F' },\n        { text: '作品', link: '\u002Fworks.html'},\n        { text: '关于', link: '\u002Fabout.html' },\n    ],\n    ...\n}\n```\n\n## 首页\n\n首页和固定页面的概念是一样的，只需要在`docs`目录下新建一个`index.md`文件即可，就能通过`\u002F`访问。但是如果直接在这个文件中写文字内容的话，它的表现和普通文章是一样的，因此这里需要更多的定制化内容。\n\n到这里终于可以体会到VitePress的高明之处了：所有的`.md`文件都会被转换为`.vue`组件，然后通过Vue的工具链进行编译，因此我们可以在`.md`文件中使用Vue的语法，例如`\u003Cscript>`、`\u003Cstyle>`等，基本上和写一个`.vue`组件没有太大差异。\n\n也正是因为有这样的机制在，使得我们可以在首页中使用一些Vue生态中可用的开源项目，例如为了编写样式，我引入了TailwindCSS，引入方法和在Vue 3项目中几乎没有区别，值得注意的有几个点：\n\n第一，通常Vue项目中我们会在`main.js`或者`app.js`中引入一个公共CSS文件，而VitePress中需要在`docs\u002F.vitepress\u002Ftheme\u002Findex.js`中引入（例如`tailwind.css`），其内容如下：\n\n```css\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n```\n\n第二，`tailwind.config.js`中配置的`content`选项需要修改，因为VitePress中的`.md`文件也需要处理，因此需要在`content`中加入`.md`后续。除此之外，因为`.vitepress`目录以`.`开头，默认会被忽略，需要单独写出来。\n\n为了适配VitePress的暗黑模式，这里可以顺便设置`darkMode: class`，这样就可以使用TailwindCSS中的`dark:`前缀为暗黑模式匹配样式。\n\n```js\nexport default {\n    content: [\n        '.\u002Fdocs\u002F**\u002F*.{vue,ts,js,md}',\n        '.\u002Fdocs\u002F.vitepress\u002F**\u002F*.{vue,ts,js,md}',\n    ],\n    darkMode: 'class',\n}\n```\n\n具体到页面内容，就比较无聊了，使用Grid布局放了几个大块块，然后适当做了一点响应式布局的调整就完成了。\n\n## 文章分类页\n\n![文章分类页](\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F02.png)\n\n有了固定页的经验，按道理分类页也很简单，只需要在`docs`目录下新建对应分类的`.md`文件即可，例如`docs\u002Farticle\u002Fweb.md`，就能通过`\u002Farticle\u002Fweb.html`访问。但这样做有两个问题：\n\n1. 分类是写死的，一旦有新增或者变化就需要手工调整\n2. 无法处理分页的情况\n\n此时就可以使用VitePress的“动态路由”功能，具体而言，是这么做：\n\n1. 建立一个带有占位符的`.md`文件，例如`docs\u002Farticle\u002F[category]-[page].md`，其中`[category]`和`[page]`都是占位符\n2. 建立一个同名的`.paths.js`文件，例如`docs\u002Farticle\u002F[category]-[page].paths.js`，它的作用是输出`[category]`和`[page]`占位符的所有可能值，例如`web-1` `web-2` `tech-1` `tech-2`等等\n\n这样，就可以通过`\u002Farticle\u002Fweb-1.html` `article\u002Fweb-2.html`等访问到对应的分类页了。为了额外提供一个“所有”分类，在处理`[category]`的值时加入一个`all`作为分类，这样就可以通过`\u002Farticle\u002Fall-1.html`访问到所有文章的第一页了。\n\n具体的写法大致如下：\n\n```javascript\n\u002F\u002F [category]-[page].paths.js\n\n\u002F\u002F Recursively get all files in a directory\nfunction getFiles(dir: string): MdFile[] {\n  const files = readdirSync(dir);\n  let fileList: MdFile[] = [];\n\n  files.forEach((file) => {\n    const filePath = join(dir, file);\n    const stats = statSync(filePath);\n\n    if (stats.isDirectory()) {\n      fileList = fileList.concat(getFiles(filePath));\n    } else if (filePath.endsWith('.md') && !filePath.endsWith('\u002Findex.md') && !\u002F\\.\\\u002Fdocs\\\u002F[^\u002F]+\\.md\u002F.test(filePath)) {\n      const fileContent = readFileSync(filePath, 'utf8');\n      const { data } = matter(fileContent);\n      const category = data.category || (Array.isArray(data.categories) && data.categories[0]);\n      fileList.push({ filePath, category });\n    }\n  });\n\n  return fileList;\n}\n\n\u002F\u002F Get all markdown files\nconst data = getFiles('.\u002Fdocs');\n\n\u002F\u002F Group files by category\nconst categories: { [key: string]: MdFile[] } = {};\ndata.forEach((file) => {\n  const category = file.category || 'all';\n  if (!categories[category]) {\n    categories[category] = [];\n  }\n  categories[category].push(file);\n\n  if (category !== 'all') {\n    if (!categories.all) {\n      categories.all = [];\n    }\n    categories.all.push(file);\n  }\n});\n\n\u002F\u002F Calculate page count for each category\nconst pageParams: PageParams[] = [];\nObject.entries(categories).forEach(([category, files]) => {\n  const pageCount = Math.ceil(files.length \u002F PAGE_SIZE);\n  for (let i = 1; i \u003C= pageCount; i++) {\n    pageParams.push({ params: { category, page: i } });\n  }\n});\n\nexport default {\n  paths() {\n    return pageParams;\n  },\n};\n```\n\n> 写本文的时候在想为什么这里没有使用createContentLoader来写，暂时没有想清楚，可能尝试过碰到了问题，记不太清了，回头再试一下。\n\n按照官方文档，在`[category]-[page].md`中，可以使用`createContentLoader`来获取文章数据，这一段逻辑被封装到一个单独的文件`posts.data.js`中：\n\n```javascript\nexport default createContentLoader('article\u002F**\u002F*.md', {\n    excerpt: '\u003C!-- more -->',\n    includeSrc: true,\n    transform(raw): Post[] {\n        return raw\n            .filter(({ url }) => url && !\u002F\\[.+\\]\u002F.test(url))\n            .map(({ src, url, frontmatter, excerpt }) => ({\n                title: frontmatter.title,\n                url,\n                date: formatDate(frontmatter.date),\n                category: frontmatter.category || frontmatter.categories?.[0] || 'all',\n                excerpt,\n                cover: getCover(src),\n            }))\n            .sort((a, b) => b.date.time - a.date.time)\n    }\n});\n```\n\n其中`getCover()`是一个从原文中解析封面图片的方法。\n\n这里有一个困扰了很久的点，就是图片路径的问题。按照官方文档，推荐将图片放在`docs\u002Fassets`中，在`.md`文件中通过相对路径进行引用，如果这样做的话，这个`getCover()`获取到的将是构建前的图片原始地址，而构建之后，图片地址会发生变化，使得封面无法显示。因此最终还是将图片放在了`docs\u002Fpublic\u002Fassets`中。\n\n渲染的时候根据根据`params`中的`category`和`page`来筛选文章，最后将结果渲染出来即可：\n\n```javascript\nimport { data as posts } from '..\u002F.vitepress\u002Ftheme\u002Fposts.data'\n\nconst { params } = useData()\n\nconst PAGE_SIZE = 12\nconst PAGE = +params.value.page\nconst CATEGORY = params.value.category\n\nconst categoryPosts = computed(() => {\n    return posts.filter(post => CATEGORY === 'all' || post.category === CATEGORY);\n});\n\nconst currentPosts = computed(() => {\n    const start = (PAGE - 1) * PAGE_SIZE\n    const end = start + PAGE_SIZE\n    return categoryPosts.value.slice(start, end)\n});\n```\n\n在界面上还要放一些分类选择、页面选择的组件，比较常规，不过多记录。\n\n## 影集页面\n\n![影集详情页](\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F03.png)\n\n影集模块中的详情页主要用来展示照片，这里不想使用文章中把图片顺序铺排下来的方式，而是希望有一个自定义的布局，于是扩展了一个单独的`GalaryLayout`来展示。\n\n首先编写好布局组件，就是非常常规的Vue组件，使用`useData()`方法获取页面对应的数据，然后进行展示即可。为了方便，在`docs\u002F.vitepress\u002Fconfig.js`中的`transformPageData()`hook中加了一个解析页面图片的功能：\n\n```javascript\ntransformPageData(pageData, ctx) {\n    if (pageData.frontmatter.category === 'galary') {\n        \u002F\u002F @ts-ignore\n        pageData.images = getImages(readFileSync(join(process.cwd(), 'docs', pageData.relativePath), 'utf8'));\n    }\n\n    return pageData;\n},\n```\n\n> 这个页面还有待进一步优化，图片的查看方式、整体视觉风格都还不够好。\n\n## 敏感信息擦除\n\n因为有一些文章中有部分文字不适宜对外发布，但作为个人的文字记录，又不想删掉，于是之前就做了一个敏感信息的标记（`[*敏感文字*]`），在渲染的时候会擦除标记中的文字。这次迁移的时候发现VitePress不容易做这样的处理，因为它没有给出在渲染前进行原文修改的hook，只能修改渲染后的HTML代码。\n\n于是修改后的代码如下：\n\n```javascript\nexport function eraseSensitiveText(text?: string): string {\n    if (!text) return '';\n    return text.replace(\u002F\\[\u003Cem>.*?\u003C\\\u002Fem>\\]\u002Fg, '[***]');\n}\n```\n\n在`docs\u002F.vitepress\u002Fconfig.js`中使用`trnasformHtml()`hook来调用：\n\n```javascript\ntransformHtml(code, id, ctx) {\n    return eraseSensitiveText(code);\n},\n```\n\n> 写成`transformHtml: eraseSensitiveText`的形式也可以，但个人习惯保留回调函数的完整参数信息。\n\n## 评论\n\n写作的时候Copilot提示说“评论是博客不可缺少的功能”，想了下也有几分道理，我的确是这么想的。\n\n之前使用的评论服务是Disqus，考虑到它已经被墙，且开始提供越来越多与评论无关的功能，这次打算顺手迁移一下。\n\n因为站点本身是静态的，不具备数据库和后端逻辑，因此评论数据需要托管到一个另外的地方。这里大致有几种路线：\n\n1. 基于Github issue的评论系统\n2. 基于LeanCloud之类的BaaS的评论系统\n3. 基于自托管的评论系统\n\n在方案选择上纠结了很久。首先个人觉得使用issue作为评论数据托管的地方，多多少少有一些滥用的嫌疑，而且Github的API现在也有频率限制，因此放弃这个路线。然后是LeanCloud之类的数据托管，有一些很私人的考虑，不太想再使用国内的服务，而国外的类似产品又不多（也可能是没有去穷尽）。\n\n最后看到了[Artalk](https:\u002F\u002Fartalk.js.org\u002F)这个产品，属于自托管的评论系统。看了一下前端界面还是挺符合需求的，但它的后端是用Go写的。我希望这个服务能部署到Cloudflare workers中去，因为刚好它还有D1（SQLite）服务，这样就可以完美解决后端程序和数据库的问题，既不用花钱，也不用自己运维。\n\n于是就翻了一下Artalk的文档和源码，自己写了一个接口完全一致的后端，部署到了Cloudflare workers中。当然，因为我只有自己使用，有一部分特性用不着，所以只实现了最核心的评论相关的特性，其它部分都未做实现。\n\n> Cloudfalre有一篇官方教程，讲的就是如何为静态站点写一个评论功能，作为入门参考非常合适。\u003Chttps:\u002F\u002Fdevelopers.cloudflare.com\u002Fd1\u002Ftutorials\u002Fbuild-a-comments-api\u002F>\n\n前端部分的使用和文档基本一致，需要注意的是VitePress生成的是一个SPA应用，即点击站内链接时页面不会刷新，这将导致评论内容也不更新。因此需要手工调用`update()`方法传入新的配置并调用`reload()`重新拉取评论。\n\n```javascript\nconst router = useRouter()\n\nrouter.onAfterRouteChanged = async () => {\n    initArtalk();\n}\n\nlet Artalk, artalk;\n\nconst initArtalk = () => {\n\n    const conf = {\n        el: '#artalk-container',\n        pageKey: location.href.replace(\u002F\\?.*\u002F, '').replace(\u002F#.*\u002F, '').replace(\u002Fhttp:\\\u002F\\\u002Flocalhost:\\d+\u002F, 'https:\u002F\u002Fwww.toobug.net'),\n        pageTitle: document.title,\n        server: location.host.includes('localhost') ? 'http:\u002F\u002Flocalhost:8787' : 'https:\u002F\u002Fcomments.toobug.net',\n        site: 'TooBug',\n        nestMax: 2,\n        emoticons: false,\n    }\n\n    if (!artalk) {\n        artalk = Artalk.init(conf);\n    } else {\n        artalk.update(conf);\n        artalk.reload();\n    }\n\n};\n```\n\n在这个过程中还发现了一个Bug，Artalk在调用`update()`方法时会重复为按钮绑定事件，从而导致点击时出现重复评论的情况，目前给官方报了issue（\u003Chttps:\u002F\u002Fgithub.com\u002FArtalkJS\u002FArtalk\u002Fissues\u002F592>），期待解决。而我自己先加了一个初始化的标识放在`dataset`上，使用`pnpm patch`打了一个补丁作为临时解决方案：\n\n```javascript\nfunction submitBtn(editor) {\n  editor.refreshSendBtnText();\n  const $submitBtn = editor.getUI().$submitBtn;\n  const isInit = $submitBtn.dataset.init === \"1\";\n  if (!isInit) {\n    $submitBtn.addEventListener(\"click\", () => editor.submit());\n    $submitBtn.dataset.init = \"1\";\n  }\n}\n```\n\n在评论的通知部分，Artalk原版后端应该是邮件通知的，这部分我没去做实现，只是简单接了一个Telegram机器人，这样当别人添加新的评论的时候我就能收到机器人的推送通知。\n\n## 其他\n\n### RSS\n\nRSS基本上照抄Vue博客的代码，源码位于\u003Chttps:\u002F\u002Fgithub.com\u002Fvuejs\u002Fblog\u002Fblob\u002Fmain\u002F.vitepress\u002FgenFeed.ts>。唯一的改动是我没有输入全部文章，因此加了一个`.slice()`方法来限制输出的文章数量。\n\n### GA & favicon\n\nGA和favicon之类的都很简单，只需要在`\u003Chead>`中添加几行代码即可，对应到VitePress是在`docs\u002F.vitepress\u002Fconfig.js`中添加配置：\n\n```javascript\nhead: [\n    ['link', { rel: 'icon', href: '\u002Flogo.png' }],\n    ['script', { async: '', src: 'https:\u002F\u002Fwww.googletagmanager.com\u002Fgtag\u002Fjs?id=G-XXXXXXXX' }],\n    ['script', {}, `window.dataLayer = window.dataLayer || [];function gtag(){dataLayer.push(arguments);}gtag('js', new Date());gtag('config', 'G-XXXXXXXX');`],\n],\n```\n\n### 首页彩蛋\n\n![首页渐变背景](\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F04.png)\n\n在正式上线之前，临时为首页的模块加了一点点效果：鼠标hover到某个模块上时，会产生一个渐变的背景，并且每一次hover看到的颜色都是随机的。利益于Tailwind的封装，整体代码比较简洁：\n\n```html\n\u003Csection class=\"hover:bg-gradient-to-br\" :class=\"getGradient()\" @mouseenter=\"refreshGradient\">\n...\n\u003C\u002Fsection>\n\n\u003Cscript setup>\nconst gradientList = {\n    light: [\n        ['from-amber-200', 'to-yellow-400'],\n        ['from-amber-500', 'to-pink-500'],\n        ['from-violet-200', 'to-pink-200'],\n        ['from-blue-200', 'to-cyan-200'],\n        ['from-teal-200', 'to-teal-500'],\n    ],\n    dark: [\n        ['from-purple-500', 'to-purple-900'],\n        ['from-blue-800', 'to-indigo-900'],\n        ['from-emerald-500', 'to-emerald-900'],\n        ['from-slate-500', 'to-slate-800'],\n        ['from-violet-600', 'to-indigo-800'],\n    ]\n};\n\nconst isDark = ref(false);\nconst checkDark = () => {\n    isDark.value = document.documentElement.classList.contains('dark');\n}\n\nconst refreshGradient = () => {\n    isDark.value = true;\n    isDark.value = false;\n    checkDark();\n}\n\nconst themeList = computed(() => {\n    const listName = isDark.value ? 'dark' : 'light';\n    return gradientList[listName];\n});\n\nconst getGradient = () => {\n    const index = Math.floor(Math.random() * themeList.value.length);\n    return themeList.value[index].join(' ')\n};\n\nonMounted(checkDark)\n\u003C\u002Fscript>\n```\n\n整体思路是当鼠标hover的时候重置`isDark`的值，然后就会依次更新`themeList`和`getGradient()`的值，从而实现更换渐变背景的效果。\n\n## 小结\n\n翻了一下提交记录，第一次安装`vitepress`依赖的时候是8月28日，不知不觉居然折腾了一个月了，还是花费了不少时间的。从最终结果上来看，基本上达到了我的预期，希望后续可以在这个站点上做更多的记录。\n\n这里也遗留了很多后续需要去处理的问题：\n\n- 整体视觉风格的优化，包括TailwindCSS和VitePress默认主题的颜色不一致的问题处理以及暗黑模式下一些细节的优化\n- 影集模块的功能和视觉优化\n- 首页动画的微调和优化\n- 评论系统的完善，包括邮件通知、回复等功能\n- 移动端一些样式优化和交互优化\n- 构建性能的优化（目前需要3分钟）\n\n希望以后有时间可以慢慢去完善。\n\n### 题外话\n\n通过这次迁移，对VitePress的认知也更深刻了。总体上来看，它的设计非常优雅，能够使用的特性并不太多，但是有足够的灵活性来满足各种各样的需求。而且因为它的最终产物是SPA，使用体验极好，相对Astro之类的框架来说，最终的使用体验是更好的。因此如果VitePress在后续的扩展和迭代中保持目前设计上的优异表现，我相信完全可以产生一个更大的生态圈。\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2023\u002Fblog-upgrade\u002F01.png",{"id":138,"type":7,"slug":139,"title":140,"date":141,"category":25,"tags":142,"body_markdown":147,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"cover":148},136,"run-chatglm2-on-m2-macbook","在M2 Macbook上运行ChatGLM2","2023-07-11 18:30",[143,144,145,146],"AI","ChatGLM2","M2","Macbook","\n## 介绍\n\nChatGLM 是一个“开源双语对话语言模型”（官方介绍），也就是我们所熟知的“大语言模型”，或者简单理解为开源版ChatGPT平替（之一）。ChatGLM有时候也被叫作“清华大学的大语言模型”，但它的作者应该是和清华大学有合作，总之和清华大学有点关系就是了。\n\nChatGLM2 是 ChatGLM 的第二个版本，性能更强大，支持更长的上下文，推理更高效。目前 ChatGLM2 已经开源，能够下载到的模型是 6B 参数训练的，因此也叫 ChatGLM2-6B 。\n\n本文主要记录一下在 M2 Macbook 上运行 ChatGLM2 的过程。\n\n\u003C!-- more -->\n\n## 下载模型\n\nChatGLM-6B 的下载地址 \u003Chttps:\u002F\u002Fhuggingface.co\u002FTHUDM\u002Fchatglm2-6b> ，在 Files and versions 页面中找到 Clone repository，按照指引即可克隆下来：\n\n```sh\ngit lfs install\ngit clone https:\u002F\u002Fhuggingface.co\u002FTHUDM\u002Fchatglm2-6b\n```\n\n整个模型有 13G 左右，需要比较长的时间。\n\n> 注意，自行解决科学上网等问题。\n\n## 下载ChatGLM.cpp\n\n默认的模型在 Mac 系统下推理性能比较差，因此有人基于 ggml 重新实现了一个 ChatGLM.cpp，完全使用 C++ 实现，性能更好。\n\nChatGLM.cpp 的下载地址 \u003Chttps:\u002F\u002Fgithub.com\u002Fli-plus\u002Fchatglm.cpp>，按照指引克隆下来：\n\n```sh\ngit clone --recursive https:\u002F\u002Fgithub.com\u002Fli-plus\u002Fchatglm.cpp.git && cd chatglm.cpp\n```\n\n## 安装依赖\n\n接下来需要安装一些 Python 依赖。\n\n> 如果你没有安装 Python 3 的话，可以通过 Homebrew 安装：`brew install python3`\n\n```sh\npip install protobuf transformers==4.30.2 cpm_kernels torch==2.0 gradio mdtex2html sentencepiece accelerate\n```\n\n## 转换模型\n\n接下来需要将原始的模型转换成 ChatGLM.cpp 可以使用的格式。\n\n```sh\npython convert.py -i ..\u002Fchatglm2-6b\u002F -t q4_0 -o chatglm2-ggml.bin\n```\n\n其中`..\u002Fchatglm2-6b`是原始的模型目录，`chatglm2-ggml.bin`是转换后的模型文件。参数`-t`是量化类型，官方说明如下：\n\n- `q4_0` 4-bit integer quantization with fp16 scales.\n- `q5_0` 5-bit integer quantization with fp16 scales.\n- `q5_1` 5-bit integer quantization with fp16 scales and minimum values.\n- `q8_0` 8-bit integer quantization with fp16 scales.\n- `f16` half precision floating point weights without quantization.\n- `f32` single precision floating point weights without quantization.\n\n总之`q4_0`是对机器性能要求最低的，这里就选它了。\n\n## 编译ChatGLM.cpp\n\nChatGLM.cpp 的编译需要使用`cmake`，如果没有安装的话使用 Homebrew 安装：`brew install cmake`。\n\n```sh\ncmake -B build\ncmake --build build -j\n```\n\n## 运行ChatGLM.cpp\n\n```sh\n.\u002Fbuild\u002Fbin\u002Fmain -m chatglm2-ggml.bin -p 你好\n你好👋！我是人工智能助手 ChatGLM2-6B，很高兴见到你，欢迎问我任何问题。\n```\n\n可以使用`-i`参数进入交互式模式：\n\n```sh\n.\u002Fbuild\u002Fbin\u002Fmain -m chatglm2-ggml.bin -i\n    ________          __  ________    __  ___\n   \u002F ____\u002F \u002F_  ____ _\u002F \u002F_\u002F ____\u002F \u002F   \u002F  |\u002F  \u002F_________  ____\n  \u002F \u002F   \u002F __ \\\u002F __ `\u002F __\u002F \u002F __\u002F \u002F   \u002F \u002F|_\u002F \u002F\u002F ___\u002F __ \\\u002F __ \\\n \u002F \u002F___\u002F \u002F \u002F \u002F \u002F_\u002F \u002F \u002F_\u002F \u002F_\u002F \u002F \u002F___\u002F \u002F  \u002F \u002F\u002F \u002F__\u002F \u002F_\u002F \u002F \u002F_\u002F \u002F\n \\____\u002F_\u002F \u002F_\u002F\\__,_\u002F\\__\u002F\\____\u002F_____\u002F_\u002F  \u002F_(_)___\u002F .___\u002F .___\u002F\n                                              \u002F_\u002F   \u002F_\u002F\n\nWelcome to ChatGLM.cpp! Ask whatever you want. Type 'clear' to clear context. Type 'stop' to exit.\n\nPrompt   > 你好\nChatGLM2 > 你好👋！我是人工智能助手 ChatGLM2-6B，很高兴见到你，欢迎问我任何问题。\nPrompt   >\n```\n\n简单试了一下鸡兔同笼的问题：\n\n![](\u002Fassets\u002Ftech\u002F2023\u002Frun-chatglm2-on-m2-macbook\u002F01.png)\n\n可见 ChatGLM.cpp 的中文理解非常不错，数学成绩也还行，但是后续实测中代码的理解上还有待提高，和 ChatGPT 有差距。但不管怎样，一个本地能运行的大语言模型，也可以干很多事了，突然对这个行业的未来充满了期待。\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Ftech\u002F2023\u002Frun-chatglm2-on-m2-macbook\u002F01.png",190]