2025 年 11 月记事板

12 日

晚上课上来写一点吧,有必要稍微给近期做的一些事情做一个总结。

体测

前阵子去体测了一下,总体来说成绩是三年来最好的,除了 1km。

测了测身高,看来我是超不过一米八了。这个身高就很尴尬,别人问的时候不好意思讲一米八,实话实说后别人又会说没一米八吗。背也好酸。

体重可喜可贺减小了一点。不过带来的连锁效应是 BMI 差点拿不了满分了。

除了 1km 外的成绩全都是三次体测以来最好的。1km 我是真没法子了,叹息。不过饶是如此,成绩也是比大一的要好。

但这次体测我也验证了一件事情,确实计时偏少,而且其实都不能叫「偏」,因为照我的计时起码少了十秒,跟我一年前的计算是一个量级的(很难想象我要用「量级」形容)。不太可能是因为从最后一个出发的人开始计时导致的,因为我是从人群中间出发的,而上一次人比较少在比较前面。但我也懒得计较这么多了,毕竟体测成绩能高一点。

引体在体测前两周吧开始复健,拉了十个后就没力气了。而后几天手臂都非常非常酸痛,一直到体测那周初,于是我只好把一阶段体测从原定的周三改到了周六,而周日就是二阶段(两个跑步)。

问了问顾问,这个是「延迟性肌肉酸痛」,属于是长时不锻炼的正常现象。了解后感觉我后面跑步前都要拉几下引体了,总不能每次都是提前一两周复健吧。然后果然逐渐好了,于是体测前几天基本上就是每天来拉 15 个。

体测的时候拉了 25 个。跟之前想的一样,想尽可能多拉几个看看能不能多加分。然后到 24 后已经基本没力了,但也是为「凑整」凑了个 25,这个是真拼尽全力的一击。

然后我想了解一下能加多少分,总不能是无限度的吧?就去查了一下,也是我体测以来第一次查询计分。震惊地了解到,居然多拉一个加 1 分,上限居然有 10 分之多?!顿时感觉自己亏了好多分。

虽说我现在拼尽全力也就 25 个,但我不觉得 30 个是什么难事,只不过需要更早一点的复健罢了。这真是亏炸了,我要之前知道能加这么多,那肯定每次提前一个月开始练,爽爽加满。

上面还说到改期,因为周日是二阶段跑步,而周内又要上课,就选定了周六。结果周六那天体测后我再看一遍,发现报成鼓楼的了……不仅是鼓楼的,还是二阶段跑步……

也管不了这么多了,测都测完了。而且跟去年一样,我直接拿身份证去刷了,应该问题不大吧?应该……

现在每天都会看看体测成绩,二阶段跑步很快就出了,但现在一阶段还是没有,有点担心。

按键

最近一直在写 Focust,因为待会要写到,想从按键角度了解一下,于是看了看 showKeyboard 数据。

有点意外地发现,10.2 的时候 J 以 8097 的次数拿下了第一,打破了此前 AppsKey 保持了半年多的数据,也成为第一个破 8k 的键。

真没想到,一年前的时候还在「惆怅」谁能破 6k,一年后 J 就破了 8k 了,真没有上限的吗?那我倒要看看是谁先能将 1w 这个标靶射落。

J 也算是彻彻底底雪耻了,一年前被 DownCtrl 羞辱,半年前被 AppsKey 羞辱,而现在它终于重夺第一的桂冠,也成为了 8k 里程碑的开拓者,成就了 6k, 7k 滑铁卢后的凯旋门。

也是一年多没聊聊按键世界了,现在一看才发现 J 家族已经这么强大了吗,现在的总额是 73w,牢牢坐稳第一家族的位置。

而之前提及的第一家族 Space 已经没落了,甚至不如 L

当初同一个层级的 CtrlShift 家族现在也依旧是相爱相杀,分别是 56w 和 55w,位居第二三。还有一个 50w 家族便是 J 的秦晋之好——K 了,以 50w 的资产堪堪进入第二集团。

第三集团就差远了,而且硬要算的话应该就只要 1 的 39w 和 BackSpace 的 37w。再往后就是 I 的 30w 与 L 的 29w,加上 Space 的 27w 与 Enter 的 25w。这些就是 25w+ 的了(U 有 24.7w,D 更是有 24.9w,非常接近了)。

其他的就没啥好说的了,没有非常突出的、能破之前纪录的成绩。像是应用什么的就不提了,因为 Scoop 的原因,也没法看总额。

另外挺意外 10.2 助力 J 登顶的,于是看了看那天我做了些什么:原来是在写编译原理实验,那天交了有几十次吧。不过还是有点夸张啊。

另外对比一下鼠标这块:右键 20w,左键 176w(其实说 176.999w),下滑滚轮 583w,上滑滚轮 177w。

Focust

一个晚上没能写完啊……因为写到一半之前遇到的一个窗口冻结问题又来了,结果去看日志发现日志丢了,没能找详细日志排查,又去折腾了点别的修复,心累……

13 日

Focust

那今天继续来写吧,虽然摆了大半个晚上时间已经所剩无几了。

从程序员日的 10.24 我写下下面两段话以来,已经过去差不多三周了。而这三周以来,除了刚开始在探索其他方向(试了试其他 GUI 框架),后面回归 Tauri 方案并不断迭代完善与代码审查没有提交,以及刚开始那周周末摆了两天啥事没做外,每天都有 commit,而且均摊下来有日均 10+ commits。

  • 然后最后想到又是月底了,高级请求才用了 30% 左右吧,于是 Focust 走起!
  • 现在效果真不错了,还是用 Tauri 方案,VC 了一晚上,到现在可以正常打开设置界面了,而且挺美观的,还得是 Web,同时还加了不少测试,都能通过了,感觉可以继续推进 Focust 了。但这周不能继续弄了,必须要正事搞完才能继续。

这三周以来也真的是像是找到目标一样非常投入,几乎无时不刻不开着 VS Code 在写代码,即便是到这周也是如此。在写 Focust 前我基本上又回归了开摆的状况,每天就是混日子般度过。

洗完澡回来困了,一直在摆了,看来今晚又写不了多少了。

沉迷 coding 到连着三周的课上都一直在写了,一点课没听。本来 Focust 刚重启的时候我差不多把近期的事情做完了,因此倒也比较闲,有机会全身心投入 Focust。结果到现在已经倒欠不少事情了,尤其是期中已过,临近期末,按理来说我应该准备 Anki 了。

虽然本来这学期也没咋听课,但课上好歹会跟着进度搞笔记、审查一下内容的,结果现在也是没怎么做。

但不得不说,这也许是我大二以来最为投入的一个项目了,没有其他的约束,完全是在按照自己的设想、期望去做一件事情,不是功利、被迫的。这的的确确让我感到非常充实与愉悦。

像是 AI 写作业什么的我虽然身体已经屈服了,但心理上仍有点抗拒。而去实现自己的想法则完全不会有这样的顾虑,有的只是自己的想法在逐渐变成现实的欣喜与激动。

这也是跟目的有关。像作业什么的目的并不是单纯地完成一份作业而已,而是从中学到点什么,不一定是知识,也可能是期末考点。

而一个我自己的想法、灵感,目的就是将其变成一个真正的产出。不管是什么方式,好用的就够了。

而这个灵感其实并不久远,就在暑假。暑假刚开始确实是摆了,但我也雄起过几天。打算好好改一改习惯,健康生活一点。

于是我意外了解到了 Stretchly 这个软件。简而言之就是一个定期让你从屏幕中脱离出来休息的软件。虽然说我后面没怎么用,但那几天还是有稍微遵循一下子的,只是后面就萎了。

本来也没什么想法,但是我感觉这个 Stretchly 有一些不好用的地方。

例如说我任务管理器打开,发现居然即便在后台,Stretchly 都要占据 100M+ 的内存。于是去看了一眼,果然是 Electron。

此外还有一些功能,是只有贡献者才能进行设置,这我也有点不爽。但后面好像搜源码得知了在哪里,然后可以轻松地通过 Ctrl + Shift + I 打开控制台修改元素显示出来。不过其实我也没有试用那些选项。

此外还有后面我还摸索出一个作弊机制,即便是暑假后期还一直开着这个软件,即便开了严格模式,依旧一弹出休息我就能秒关掉(有 Focus 那味了)。这个方式就是 Win + L 进入锁屏,然后再快速输入 PIN 回来。刚刚试了一下 Focust,不会。不过这个行为似乎可以通过 Stretchly 设置修改?但我也没试过。

然后还有就是计划了,我感觉 Stretchly 没有计划这个,也是一个缺陷。即便我个人用不到,但我想到了这应该是一个挺重要的思路。

同时我又想到,休息提醒是动态的,但我可能也需要一个静态时间的提醒。而这也就孵化成了 Focust 的 Attention 概念。

于是在我亲自体会到了这些缺陷后,突然便灵光一闪——我要自己写一个这样的软件,同时框架就定为我草草听说过的 Tauri——而这,也便是 Focust 的最初灵感由来。

14 日

Focust

灵感迸发的时候的感觉,就跟当初 AntsVsSomeBees 想到用 Web GUI 一样,是豁然开朗、柳暗花明。

翻了一下,正式开始写代码与构思是在 7.10,正好是 Git 那篇博文后一天,无缝衔接一件颇有意义的事情。

当然我在上个月最后的记事中也提过,一开始还没怎么用助理。我都是先写个 prompt 给顾问让它进行技术选型与提供代码架构。然后翻了一下最早的 prompt:

……我想用 Rust 重写并增强一下 Stretchly 这个休息时间提醒应用程序作为一个个人实践的练习品……

……目前的技术选型是 Tauri 框架 + TypeScript + Vue。前端工具我用的是 Bun……

……具体而言,我首先希望能够复刻 Stretchly 的全部功能,然后对一些功能进行改进。例如说 Stretchly 仅支持几种主题色,我想支持更自定义的(更自由的主题色选取、可以使用图片或者图片文件夹,甚至可以使用图源如必应等);还比如说 Stretchly 不支持设置多种「计划」,例如某个时段开启,这样就不用手动关闭了,而我想支持多种计划,可以在不同时间段配置不同的一些设置如休息时间等……

我是一个比较喜欢使用新技术的人,因此在很多个人甚至我主导的团队作业中都会用比较新的工具。

看上面的 prompt,除了图源没有实现外,其他基本上都是完美实现了。图源这个要实现应该问题也不大,只不过我翻了一下最早的数据结构提交,那会虽然标注了图源,但实际数据结构中并没有涵盖。我想可能也是想先实现比较基础、简单的,这个可以作为后续扩展。当然现在我有点累了,短时间内不想加新功能了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum BackgroundSource {
Solid(String),
ImagePath(String),
ImageFolder(String),
}

#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ThemeSettings {
pub background: BackgroundSource, // Background source type, e.g., Solid, ImagePath, ImageFolder, ImageProvider
pub text_color: HexColor, // Hex color code for text, e.g., "#FFFFFF"
pub blur_radius: u8, // Radius in pixels for background blur
pub opacity: f32, // Opacity level from 0.0 to 1.0
pub font_size: u8, // Font size in pixels
pub font_family: FontFamily, // Font family name, e.g., "Arial", "Roboto"
}

这里就不讨论技术细节了,我有时间的话会另开一篇博文,记录一下 Focust 迭代过程中的一些事情。我觉得还是有必要记录一下的,纸上谈兵与实操真的是完全不同,只有实际投入去完成一个项目,才能体会到教程、顾问参考中不会涉及到的一些方面。

虽然说像找到了方向一般,但其实并没有这么顺利。否则的话,那不应该是暑假就搞出来了吗?为何等三个多月后的十月底,进度才突飞猛进?

一个原因当然是因为当时太摆了,在「看票」。但主要原因其实是我推进不下去了。要不然的话也不会 7.16 完成了暑假的最后一次提交了。

刚开始肯定是先实现一下后端,而后端的调度器也是重中之重。我在有了构思后的第一想法就是构造一个调度器,动态地计算出下一次事件的时间,然后等待到那时候再触发。而不是一个轮询检查,像 Stretchly 那样。

因此我一开始也主要投入在调度器上,咨询了顾问很多细节,最终搓出来一个非常初期、简陋的调度器。

现在看来的话这个调度器其实设计得非常的粗糙、简陋、耦合。它将大小休息、提醒与系统通知这四个事件在一个调度器中进行了调度,然而休息与提醒本来是两个差异很大的事情:休息是动态计算的时间,可能会被推迟、因暂停而重置、跳过等等,但不管如何,提醒都是一个确定的时间。这注定了它们在调度逻辑上的差异。因此在一开始将它们视为等同并一同进行调度,其实是一个挺差劲的设计。

但我也觉得不应该苛责,在当时我是真沉浸在「这真是一个绝妙的设计」的泡泡当中,感觉对比一个 setInterval 不要太叼了。真正意识到这种耦合的设计的缺陷的时候,是在我不断完善调度器、尝试添加新功能的时候注意到了。而在这种情况下我意识到了这个问题,并当机立断进行了重构,这也便够了。

过早优化是万恶之源。虽然说这其实是设计上的缺陷,但如果一直在不停地尝试完善设计,而原地踏步,那其实反而局限了视野。我受到的历练确实不够多,因此一开始没能觉察出设计上的缺陷,同时身处信息茧房当中,顾问也不会提及相关的事情。这个是没办法在起点就发现的,我必须真正将其应用,才能意识到问题。

写到这里,我想到最初的 prompt 还有这么一句:

但是毕竟我没有相关经验,我有设计不足恐惧症——总想在设计阶段尽善尽美,唯恐后面需要大规模重构——需要你给予我详细的开发指引,告诉我该如何一步一步完成,包括但不限于前期设计规划以及实际开发过程。最好是有具体的安排表,同时每个步骤都清晰、详尽地说明。

四个月后再看到这一句着实是有点感叹。这三周已经见证了我好几次大规模重构。确实,这让我身心俱疲,加上基础功能已经完善了,也打算逐渐从中抽身了。但也不得不说,我也好几次感叹新设计的精妙之处。

顾问是怎么回复的呢?

……但解决它的最好方法不是在开始前就设计出完美的系统(这几乎不可能),而是设计一个有良好扩展性的核心,然后通过迭代去完善它。

完成比完美更重要,尤其是对于个人项目,一个拥有 80% 功能的、已完成的应用,远比一个停留在设计阶段的、100% 完美的「空中楼阁」更有价值。

根本原因在于,你正在尝试在脑海中一次性构建并连接整个复杂的系统。你看到了调度器、状态、配置、UI 之间的所有依赖关系,这让你感觉无从下手,因为任何一点的改动都可能影响全局。解决方案不是做一个更庞大、更复杂的计划,恰恰相反,是将问题分解到极致,并采用「积木式」的开发策略。

另外 7.12 的时候我也对整个项目的架构感到有一些恐惧,认为有很多地方都困扰着我,「总感觉实现正在看的这个要先额外实现其他的」。从而向顾问发问「如何在实际写代码之前进行详细规划设计呢?」。而下面是顾问的回答:

这种「感觉要实现 A,就得先完成 B 和 C」的困境,通常被称为「依赖地狱」或「分析瘫痪」(Analysis Paralysis)。

根本原因在于,你正在尝试在脑海中一次性构建并连接整个复杂的系统。你看到了调度器、状态、配置、UI 之间的所有依赖关系,这让你感觉无从下手,因为任何一点的改动都可能影响全局。

解决方案不是做一个更庞大、更复杂的计划,恰恰相反,是将问题分解到极致,并采用「积木式」的开发策略。

现在看来确实是至理箴言,但我当时虽然看进去了,却不太能理解。最初我不断打补丁、完善的调度器核心设计,在现在的我看来简直跟垃圾没什么两样,这想必是当时的我难以预见的。

但我要是一直死死地抱着那个垃圾不放,只是一直在上面细致地进行雕琢,也不会有现在的 Focust 了。

这不是说现在的 Focust 就是一块璞玉,恰恰相反,在未来的我看来这可能还是跟垃圾没什么两样。但是它跟最初的垃圾比起来,那就是它是一个完成品,是一个真正能解决我需求的垃圾,一个垃圾的完成品总比一个垃圾的设计好。

我最初的思维困境其实源自一个错误的认识,那就是一个好的成品必须源自一个好的设计。当然,这一定程度上没错。但是谁也没有预知未来的能力,我也经验尚浅,无法预知未来可能的拓展与修改,而这些都是需要在实践中积累的。因此好设计就够了,它用不着足够好、非常好,它只需要好到能启动就够了。

实际上一个好的成品源自一个垃圾的成品,一个垃圾的成品源自一个垃圾的设计,而停留在思路与稿纸的设计都是垃圾。一个好的设计应当是好的产品反过来印证的。

应该说多亏了助理。如果不是助理的帮助的话,我应该还在不断给调度器修修补补。这个垃圾的框架、能够让我在上面进行迭代的垃圾,就是由助理搭建的。

那么来看看我在完成调度器的基本实现后,进行了怎样的修修补补吧。不过最初的修修补补其实大多发生在提交之前,因为我想提交一个「完美的最终版本」,所以下面只能记录提交之后的了:

  1. 给命令加了个 Deref trait 实现,就不用 .0 了。
  2. 自这个开始,下面都是这学期开始后的了:修复了 Clippy 警告。
  3. 将事件解耦了,这是隐约感知到了调度器的耦合,但没完全意识到,想着是将事件的来源拆分出来,但还是共用一个调度器。
  4. 加测试。

当然,这期间其实还涉及了别的一些地方的小打小闹。不过比较令我惊讶的可能是一开始我其实就支持了 User Idle,也就是空闲时间检测。也许是因为在设计之初我就觉得有点棘手了,因此在最开始我就了解了一下解决方案,并写了一个 spawn_idle_monitor_task,作为一个异步任务检查。当然这个我没啥好的解决方法,只能轮询了,毕竟也没事件,只是将轮询间隔设置为了 10s,而不是像 Stretchly 用的 1s。因为我感觉这个有点误差没啥区别,平均也就多个 5s。而你空闲五分钟(默认设置)跟空闲五分五秒还是十秒,其实没啥差别。而这个随便写的 monitor 也在后续我支持 DND 与应用排除后,变成了 Monitor 模块。

感觉要是不细谈代码细节方面的事情的话,也没啥好说的了。那就这样吧,有时间回顾一下。

闲人

感觉我这个大学上的真对不起学费,明明学费是其他专业的两倍,结果给自己越学越闲了。

没错,这学期再创新纪录了,峰值只有 23 节课时一周(还只会持续一周),而现在正是课时最少的时候,并一直会持续到期末,只有 17 课时一周。

这固然跟我提前修了《机器学习》有关,但即便算上去,那还是少之又少。因此我感觉跟其他同学,甚至是同专业(不同校)的其他同学相比,我真可谓是一个闲人了。

另外说起来,上周机器学习课的时候,我没去上课,结果辅导员签到,让我首次登上了未签到人员名单中。看起来是直接拿着学院名册点名的,说起来去年上课的时候我也是跟着他们一起签到的。为了避免出什么意外,私戳辅导员解释了一下。结果第二天辅导员气势汹汹地私聊问我怎么没去上课,要做出解释什么的,吓我一大跳。一点进去,撤回了……

不过刚开学的时候倒不是这样的,那会峰值是有 29 节课时的,这是我退了两节课后的成果。

一开始不太懂啊,专业选修就全点上了呗。结果上了一周统计了一下期末,没把我吓死,居然有这么多期末(但好像没大一的时候多?那时候真超人啊)。而且算了算学分,发现似乎超学分了,加上还有项目压力什么的,我就果不其然怂了。

然后详细介绍了一下情况,跟顾问商量,最终退了一门课——《移动互联网应用开发》。退的理由倒也很朴素,有期末 + 项目,而且是鸿蒙应用开发。

说实话我一开始选这个课一个原因确实是想学习一点移动应用开发的知识,不过鸿蒙开发这个确实是让我有点望而却步了。

首先我肯定是支持进行鸿蒙开发的,需要开辟一个新的、自主的应用生态。但我是一个懒鬼啊,我畏难,尤其是这种文档、社区、经验、基础设施等什么的都不齐全的,确实是让我退缩了。

加上其实我没有非常大的动力进行,你像 Tauri 我也是硬着上了,这是因为 Focust 是我所欲。但我没啥移动应用开发的需求,我手机基本上是基础设施,已经够用了,并不需要额外的扩展什么的,同时我也没用鸿蒙,那就更没有什么动力去开发了。

让我比较意外的是,我把这个跟顾问讲后它没有顺着我,而是坚定的让我退掉。

与此同时,有一些顾问还建议我退掉第二门课——《人机交互系统》。当时一口气问了好几个顾问,基本上是异口同声说退掉《移动互联网应用开发》,但同时有一些顾问还建议退掉《人机交互设计》。

这也是一个项目 + 期末的。与前者不同是的,这个我半毛钱兴趣都没有,但按老师的说法,可以复用之前的项目。因此第一轮退课的时候我只退了前者,还是留着这个。

但最终还是退掉了。因为我对软工二的作品非常不满意,可以说完全是我随便糊弄的,即便花了不少时间精力在上面,但确实是没怎么用心。当然是因为我对这个项目不感兴趣,真的是一点爱都见不到。

因此我后面思考了一下,想直接复用应该是做不到的,很可能需要大规模重构。那还是算了吧,相当于把已经埋了的大便再刨出来塞嘴里再消化一遍,然后再拉一坨消化得更完全的大便。于是就退了。

这也就是为啥我这学期这么闲的原因了。

当然,那专业选修学分呢?根据我的计算,退了这两门后还差 4 分(不算那个 6,只算 18 那部分。6 那个已经拿 3 了,要拿也只有下个学期)。说实话,我后悔这个暑假没去上暑期了,要上了首先暑假不会那么摆,其次那就只差 3 学分了,只需要一门课就够了。而现在这个情况,想要两个半学期的课难度是比较高的,那最少也是一个完整的课再加一门别的了,保险一点的话下学期完成,那就是多了一门。所以现在就是非常后悔,没有计算一下。

看自己这般计算,全然没了大一时的随然,也是心中不免有些酸涩。但这又何尝不算是一种成长呢?

日本

这部分是在写开发记录的时候,去洗澡的时候偶然想到的。

最近日本很跳呐。真的感觉历史大事在逐渐大踏步而来了。

小时候的我似乎在日记本上提到过「安倍」。不过当时的我显然是不知道什么更多的内容,最多知道他是个日本首相。甚至当时其实我似乎是读成了 péi,甚至好像还写错了?(安培哭死)。

现在想来时间节点大概是 2013 年,钓鱼岛争议吧?我比较主动去了解世界政治,大概不是高中那也得是初中了。但是一个 8 岁左右的小孩也能对安倍这个人留下印象并记录到日记当中,着实让我有些吃惊。尤其是对比当时,U 型锁、砸日产车,而现在却是攻守之势异也,十三年河西都算不上。

又回想起 2022,安倍遇刺的时候。那时我应该是高二,记得似乎还在十几楼的宿舍收拾东西吧,似乎是准备回家还是怎的?然后同学突然就说,「安倍遇刺了」。当时我记得是小小惊讶了一下,但还是非常平静,停下收拾的行动,掏出手机开始看新闻。如果没记错的话,当时同学间似乎还笑着说「没有生还的风险吧」,后面果然就死了。到现在也三年多了,安倍都两岁大了。

不是怜悯日本,不是惋惜安倍,只是感叹时间飞逝,不管是我个人的、还是国家的。

27 日

昨天的经历跌宕起伏,虽然我已经在多处记录了一些相关内容,但还是没能完整地梳理一下经过与各种细节。因此就趁现在已经知道了更全面的信息之时,详尽地回顾一下自己的经历细节以及真实的心路历程吧。

昨天早上打开 GitHub 的时候,映入眼帘的是一连串 star 的消息,然后我定睛一看,我的 Focust 怎么有 123 个 star 了?

那一刹那是有点幸福的,毕竟有这么多 star,所有仓库加起来都没这个的零头,说不开心那是假的。

但是天上不会掉馅饼,事出反常必有妖。在此之前我并没有在其他地方对 Focust 进行推广,总不能是某个大佬随便就点进了一个仓库,感觉不错于是进行了宣传吧?当然也不是没想过

加上 star 的时间高度重合,这难免不让人起疑心。于是我点开了几个 star 用户的主页,想要寻找一些端倪。

但很让我意外的是,这些仓库乍一看没看出有什么不对劲的点:既有空荡荡的贡献图表,又有辛勤耕耘开垦的绿瓷砖,也有各种钻研的学生。仓库也看起来非常正常,star 数目从零星到数十数百都有。如果不是他们给了我 star,我不会感觉到任何异常。

但点开几个后,我就察觉到了反常之处——那就是他们的 star 数目都大概在七八百以上,大约是八百上下,多者可能上千。

于是我就点开其中一个的 stars 页面,发现一整页都是都是 Hrishikesh332 (Hrishikesh Yadav) 的仓库,不管是普通的仓库,还是一点没改甚至落后原仓库进度的 fork,都 star 了。

多点开几个,如出一辙。看到这里我就笑了,看来是这个人买了 star,但是为了防止给看出来,拿我当了掩护。

而这些账户,大概也确实是正常的,只不过可能是 GitHub Token 泄露了之类的,被刷 star 的黑产利用了罢了。

然后由于早上要做实践,我也就没来得及继续深入调查。当时也准备了一下去 GitHub Support 开工单(这学期初的 Billing 工单到现在还没回复我呢……),但似乎交互逻辑又变了一下,我折腾了一会没开成,时间又快到了就先去做实践了。

上面的解释似乎就已经足够了,足以说明整起事件了。但是「为什么是我」这个问题仍然萦绕在我的心头。做实践的时候我还是有点好奇,于是在边走路的时候,边用手机 GitHub,打开了一个人的 stars 记录,一直往下翻,打算看看这八百个 star 有什么特征。

现在我已经查证不到当时翻的是哪个用户的 stars 历史了,现在边写,就挑 RyanKim17920 (Ryan Kim) / Starred 吧,希望能符合当时的观察要是不符合我就换一个

翻了几页后终于不是那位仁兄的仓库了,变成 arpitbbhayani (Arpit Bhayani) 的了,特征也是类似的,不管是普通仓库还是 fork 的仓库都 star 了。

而再往早,则是 linus (Linus G Thiel)torvalds (Linus Torvalds)。由于这两个 Linus 我还是眼熟的,当时就并没有归类为「买 star」中。

到这里后,下面的仓库就比较「杂乱」了,出自不同的所有者。

一开始我还没注意到这些仓库有什么共性,毕竟不管是所有者、仓库主要语言都各不相同。但翻多一点后就觉察出了一些不对劲的点,怎么感觉仓库主要语言 Rust 占比这么高啊?除此以外,不是 TypeScript 那也是 Vue 或 Svelte 了。

于是我就有了一个预感——这些,该不会都是 Tauri 项目吧?

这个预感很快得到了证实,我刻意没有选择 Rust 项目,而是选择了 TypeScript, Vue, Svelte 为主要语言的项目,抽样检测的几个,无一例外,都是 Tauri 项目。

尤其是在继续往下翻后,看到了 Tauri 字样,如 Tauri 的一些插件、模板也出现后,彻底得到了印证。

「为什么是我」得到了一个似乎还算满意的答案:似乎针对的是 Tauri 项目?当然,这其实还不是终点。

在翻到了我自己的项目后,很快我就看到了 blink-eye 这个项目。其他 Tauri 项目我不认识,但这个我是有印象的。因为我在将项目提交到 Awesome Tauri 中时,就搜了一下,发现了这个项目跟我的项目的一些相似性。

于是我就有了一个更大胆的猜测,「为什么是我」这个问题的真正答案,不仅仅是 Tauri 应用这一点,可能其实是针对的是 Awesome Tauri。

不过当时因为是在手机,也不方便验证这个想法。就在上面边写边翻的时候,继续抽检了几个项目,不管开源闭源(闭源的单纯放个占位符),均列在 Awesome Tauri 列表中。

因此这个猜想在我目前看来,仍然非常真。Awesome Tauri 没能成宣传机,反而成了「死亡名单」。

我当时翻到这里继续往下翻,后面的就脱离 Tauri 生态系统了,依稀就记得个 FastAPI。我现在再尝试看看有没有什么新特征,不过到这里就可能有点困难了,毕竟受害者可能本身就有一些 star。

按上面的用户的 star 列表,最后一个 Tauri 项目是 Kaas,而第一个不再是 Tauri 的项目是 piccolo admin。这个项目主要语言是 Python 和 Vue,因此我搜了一下 Awesome PythonAwesome Vue,不过一无所获。

但我很快灵机一动,查了一下 README,果然用了 FastAPI,于是再搜了一下 Awesome FastAPI,不出所料,果然在里面。

不过后面又出现了新的,最后一个满足的是 FastAPI with Observability,新的则是 Azure AI-900 Exam Notes / Preparation Repository。这个一开始没找到什么特点,还以为可能是本来就有的,不过额外加了第二个发现同样有。

这个项目找不到什么特征了,直接复制名称 + Awesome 在 GitHub 搜索,搜不到。去掉 Awesome 还是找不到什么可能的「名单」。因此后面就算了,我来看看一直到第一个「可疑」项目是什么。不过比较遗憾的是,这可能比较困难,因为 star 顺序并非是完全一致的。

再看了看,这个项目的特征可能是所有者是 anxkhn (Anas Khan),类似刚开始那两个用户,还包含了这个用户的其他少量仓库。

嗯……该说是不是巧合呢,我看了看,这三个用户似乎都是印度人?第一个用户看了看 X,看到过「孟买」字样,这个用户的所在地看了看,似乎也在孟买?当然这其实不好说,毕竟孟买是大城市。

还有一个仓库,是 ninadnaik10/ninadnaik10,这个似乎也是印度人,X 上显示城市似乎也在孟买?

然后是 Linux 内核仓库,后面就没有重复了,似乎这就是起点了?

好,停,star 就翻到这里了。我在早上的实践中也就翻到了 FastAPI,后面的无法一眼看出特征,翻到底也就这样了。接下来是后面的发展。

似乎到这其实也已经有了完美的解释了,那就是莫名其妙的黑产利用了泄露的 GitHub Token 操纵了一百多个受害者的 GitHub 账户,给 Linux 内核仓库、Awesome FastAPI、Awesome Tauri 和几个印度人的仓库刷了一百多个 star。

不过在中午的时候,看了看具体时间是 1:36,又有新的用户开始 star 我了,眨眼间又涨了几个 star。吓得我以为第二波开始了,不过现在看来第二波挺小的,也就四个用户:

但就是这新出现的四个 star,让我有机会一窥真相。

四个 star 出现后,我照例抽样检测看了一下账号的主页。这次我意外地发现其中一个账号首页仓库非常特殊,仓库的描述都是:F**K Guillermo, F**K VERCEL --multi

"F**k" 啥意思,总不能是 Fork 吧。一开始因为全大写我没注意到,后面的是 Vercel,给别人提醒了一下才注意到,而 Guillermo 是 Vercel CEO。

起初我只是觉得这用户可能对这俩深恶痛绝,于是把仓库的描述换成这个。但我很快觉得不对劲,因为我在网页与手机的 GitHub 都看到了这个描述,而这两处我打开的是不同的用户。即有两个用户都出现了这个现象。

于是在下午的时候,我打开了其中一个人的首页中带有这个描述的仓库,注意到了 README 非常诡异:

1
2
# Free AI at api.airforce
https://discord.gg/AJDsM7jtbq

相同的信息还出现在凌晨 star 的一些用户的仓库描述中。这时候我就以为可能是这个 AI 提供商的「广告」。不过因为当时在教室,我不认识这个,也不敢直接打开或是搜索,要是黄色网站那就社死了,因此也就没进行深入研究。

然后我再去看 commit history,这个广告信息是被塞进去的,多个用户、多个仓库都是类似的现象。

当然,目前已经有一些被 force push 掩盖了,比较幽默的是,用的是 Linux 的名义发出的 commit。这证明了 GPG 签名的重要性。

当然,GitHub Activity 这时候就派上用场了,force push 历史清晰可见:

而后面我就去搜索了一下 api.airforce,看起来似乎就是一个普通的 AI 提供商,没看出来什么异常。然后我试着将上面那条「广告信息」搜索一下,在 X 上找到了一条推文:

点开看 Wiz 的这篇博文 Sha1-Hulud 2.0 Supply Chain Attack: 25K+ Repos Exposed | Wiz Blog,才知道事情的真相。

简而言之就跟博文标题说法一致,这是一次「供应链攻击」。具体技术细节我就不细谈了,上面的博文中有详细讲解,我就不班门弄斧了。

大概就是靠钓鱼窃取了一些 NPM 包作者的令牌,然后推送恶意更新,利用 preinstall 钩子进一步扩散,其他用户执行安装命令的时候也被感染,从而窃取到一些隐私信息,而如果是包作者的话可以进一步感染。

这次的叫 Sha1-Hulud 2.0,也就是沙虫 2.0,而 1.0 的时候就在前几个月的九月,只不过因为我当时没有被牵扯入其中,也就没多加关注了。

最近这供应链攻击真是频发啊,而不到两周后的 12.9 NPM 就要推行撤销经典令牌的行动这已经是延期过了的,原定 11.19),这是在九月的 1.0 攻击后发出的。因此有专家写道:

Aikido

The timing is notable, given npm」s recent announcement that it will revoke classic tokens on December 9 after the wave of supply-chain attacks. With many users still not migrated to trusted publishing, the attacker seized the moment for one more hit before npm」s deadline.

鉴于 npm 近期宣布将于 12 月 9 日撤销经典代币,此次攻击的时机颇为耐人寻味。此前,npm 遭遇了一系列供应链攻击。由于许多用户尚未迁移到可信发布平台,攻击者趁此机会在 npm 的最后期限前发动了最后一次攻击。

看了一下报告后我大惊失色,毕竟我 Focust 有项目也是用到了 NPM 生态的包,于是赶紧自查了一下高危的包,好在都没有查到,本地与 GitHub 上均未发现任何异常,不管是奇怪的文件、多余的 Action 或是诡异的 commit 等,都没有发现,应该是幸免于难了,万幸。

然后晚上就是再了解了点这个供应链攻击的信息,我几个平台都搜了一下,没看到什么人谈论啊,这件事不应该影响范围很大吗,而且 11.24 其实就开始了。

不过就在今天中午的时候,我再次打开 Wiz 的那篇博文再翻了一下,注意到了一个新的补充:

Exploitation of compromised credentials was further observed around 1:00 UTC 11/26, by an attacker using repository descriptions to advertise: # Free AI at api.airforce https://discord.gg/AJDsM7jtbq

The initial push impacted roughly 3,200 repositories, and appears to have also publicized previously private repositories. (h/t @porobertdev) Some of these accounts also appear to have been used to "boost" the star counts of projects in the Tauri ecosystem. (h/t pilgrimlyieu) Some repository descriptions have been updated to F**K Guillermo, F**K VERCEL --multi

大约在世界协调时 11 月 26 日 1:00 左右,我们进一步观察到攻击者利用泄露的凭证进行攻击,攻击者使用存储库描述来发布以下信息:# Free AI at api.airforce https://discord.gg/AJDsM7jtbq

最初的推送影响了大约 3200 个代码库,并且似乎还公开了一些之前私有的代码库。(感谢 @porobertdev 提供信息)其中一些账户似乎也被用来「提升」Tauri 生态系统中项目的 star 数。(感谢 pilgrimlyieu 提供信息)部分代码库的描述已更新为 F**K Guillermo, F**K VERCEL --multi

加粗部分是我后面看到的补充内容,也是提到了我的内容。真没想到,我还能在其中掺上一脚啊。

除了在 GitHub Support 开了一个工单外,我昨天其实还在 GitHub Community 开了一个 Discussion Suspicious Star Spikes on Specific Project, Likely Linked to Malicious Bot Activity · community · Discussion #180436,想必这就是 Wiz 安全专家们得到信息的源头。

唔,写到这似乎已经没啥好写的了,毕竟我知道的信息就是这些了。不过我应该还是会关注一下后续的,毕竟这是我真正给「波及」到——虽然是以一种诡异的方式——我会看看是否有可能调查出来「为什么」,为什么在窃取信息外,针对我上面所说的几类仓库进行了刷 star 的操作。

至于这件事情算不算是一件好事呢?或许吧,我最初看到这么多 star 还是挺开心的,毕竟我所有项目的 star 凑一起可能都没这刷的零头。

但是我又再想了一下,如果我看到其他人的项目凭空多出来这么多 star,而且 star 的人记录非常可疑,我会有什么想法呢?我思考了一下,我可能会降低对这个项目乃至于对作者的评价与看法,哪怕他的项目写得非常好。由于不是我的项目,我可能不会刨根问底,去探寻究竟他的项目是被黑产当作是掩护的,还是他就是黑产的最终目标。

例如说在不知道沙虫的时候,我看到那两个印度老兄的这么多仓库都给刷 star 的时候,想的是他们大概就是刷 star 真正的金主吧,我才是连带的受害者。只是现在看来,他们也许也跟我一样都是受害者,而我未必每一次都有机会了解到更深的信息。

换位思考一下,我不希望我的项目被人这样看待。因此如果可能的话,我还是希望这些 star 能够被清除。现在应该已经逐渐有一些受害者开始恢复了,我已经掉了可能有 10 个 star 左右了。

28 日

昨天下午写完上面的记事后,回去路上,我想的是尾声一句「似乎已经没啥好写的了」。是啊,似乎是这样的,毕竟我已经利用我所知道的信息了解了更多内容了,而我又没学过网络安全领域,也没有更多资源去进一步探索了,这些就只能留给安全领域的专家们去调查了,并期望他们能得到一个我需要的结果。

但很快就有一个声音习惯性地反驳我,不,其实还有信息。在翻阅受害者的 stars 列表时我就想过,这一个个翻也太累了,因此我只能抽样调查。同时 Awesome 清单的猜测也依旧属于是一厢情愿的想法,我没法真正确定——有多少 Awesome 项目被攻击了,到底只是一个巧合还是真的是目标?而趁现在受害者们还没有完全恢复过来,完全是有可能进行系统性的统计与调查的。

当时听到这个时,我也只是一如既往地想着:啊,这也太麻烦了吧,我也没怎么用过 GitHub API,折腾一下也太费劲了吧,我已经做了很多了,够了……

不,还不够。我确实好奇我直觉的准确性。因此吃晚饭的时候我就开始准备着要进行调查,同时一吃完晚饭就立刻赶回去,开始了研究。

AI 时代一个之前想都不敢想的事情就是,当你有了一个想法的时候,立刻就可以开始去做了。

如果是之前的话,我想要进行这样调查,首先可能需要查找 GitHub API 相关文档,还可能需要查找一些问答;而我并不熟悉 Python 数据处理与图表相关的知识,我可能还要翻一下像是 Polars, Plotly 这些库的文档,可能要翻上好一阵子;最后开始写代码,可能一行要切三次窗口检查;即使到最后可能还跑不起来,还得进行复查。

这一套加起来,两三天算是比较保守的估计了,这恐怕热情一下子就耗尽了。但是在现在,我一个晚上三个多小时,就弄出来了。

然后提一点数据部分。

首先是我的直觉是非常正确的,两个 Awesome 列表被攻击的覆盖率非常惊人。Awesome 列表是我手动复制下来,然后用 Vim 进行清理的,因此可能有多余的链接。考虑到这个情况,覆盖率可能比下面的数据更高:

列表 攻击总数(次) 被攻击的仓库(个) 仓库总数(个) 覆盖率
Awesome FastAPI 12,346 127 134 94.78%
Awesome Tauri 24,638 244 283 86.22%

另外被攻击的仓库所有者也呈现长尾分布,两个最开始看到的印度老哥占据第一、第二:

所有者 被攻击的 star 总数
arpitbbhayani 18,240
Hrishikesh332 13,973
linus 3,840
andrew 3,267
Welding-Torch 2,548
anxkhn 2,198
hanzalahwaheed 2,063
Ratheshan03 1,888
ninadnaik10 1,712

翻了一下这些被攻击次数上千的 9 个用户的特征:

  1. arpitbbhayani: 印度人,GitHub 与 X 上地址印度班加罗尔
  2. Hrishikesh332: 印度人?X 地址德国柏林,最近的推文表明他对孟买「了如指掌」
  3. linus: 瑞典人?GitHub 地址瑞典斯德哥尔摩
  4. andrew: 英国人?GitHub 地址英国布里斯托尔,有一个 ultimate-awesome 仓库
  5. Welding-Torch: 印度人,GitHub 简介显示印度孟买
  6. anxkhn: 印度人,GitHub 地址印度孟买
  7. hanzalahwaheed: 印度人?博客显示在阿联酋迪拜或印度班加罗尔工作
  8. Ratheshan03: 斯里兰卡人,GitHub 地址斯里兰卡科伦坡
  9. ninadnaik10: 印度人,GitHub 地址印度孟买

被攻击 101 次(即受害者总数)的仓库有 371 个,100 次及以上的有 387 个,而下面是明显的分界点:

序号 目标仓库 攻击次数
775 Hrishikesh332/XRay_Efficient 88
776 torvalds/libgit2 80
783 torvalds/1590A 80
784 devout-coder/shelly 35
1094 ninadnaik10/FireSense 26
1095 Hiral25p/bit-n-build-hackathon 19

783-784 这中间剧烈的断层,也符合我观察到的大多数 star 在七八百量级的现象。

被攻击的 127 个 Awesome FastAPI 仓库中,112 个被攻击了 101 次,占比 88%。下面是攻击不足 101 次的数据:

仓库 攻击次数
MagicStack/asyncpg 100
koldakov/futuramaapi 100
piccolo-orm/piccolo 100
koxudaxi/fastapi-code-generator 100
aminalaee/sqladmin 100
tiangolo/pydantic-sqlalchemy 100
openapi-generators/openapi-python-client 99
fastapi/full-stack-fastapi-template 99
fastapi/fastapi 98
sindresorhus/awesome 93
vimalloc/flask-jwt-extended 9
fly-apps/hello-fastapi 9
iwpnd/fastapi-aws-lambda-example 9
benavlabs/fastcrud 9
maxcountryman/flask-login 9

被攻击的 244 个 Awesome Tauri 仓库中,全部被攻击 99 次及以上,其中 239 个被攻击 101 次,占比 98%。下面是攻击不足 101 次的数据:

仓库 攻击次数
kkoomen/pointless 100
tw93/Pake 100
fastrepl/hyprnote 100
btpf/Alexandria 100
gethopp/hopp 99