2025-10-15 00:00:00
最近我的 GitHub 页面右上角一直有个小蓝点,就像这样:
这是有未读通知的指示,但点进去却什么也看不到。
这种「幽灵通知」已经干扰了我的正常使用体验。这天实在忍无可忍,正打算给 GitHub 提交一个工单时,在官方开设的讨论区里发现了一个讨论贴,我在里面找到了有效的临时解决办法,特此记录下来以备后用。
讨论贴链接:https://github.com/orgs/community/discussions/6874
我使用的是讨论里提到的 利用浏览器开发者工具的解决方案。
在浏览器打开通知列表页面,点击左侧标记有数字的 Filters 或者 Repositories,比如我上文贴的图里的 Participating
、Mentioned
,或者 outcaster552/gitcoinpromosender
、gitcionoda/org
、yycombinator/-co
、paradigm-ventures/paradigm
等等。
打开浏览器的开发者工具(F12),切换到 Console 标签页,粘贴以下代码并回车执行:
document.querySelector('.js-notifications-mark-all-actions').removeAttribute('hidden');
document.querySelector('.js-notifications-mark-all-actions form[action="/notifications/beta/archive"] button').removeAttribute('disabled');
这时页面上会出现一个 Done
按钮,点击它即可清除对应的通知。
如果以上办法没有解决问题,还可以试一下贴子里 被标记为答案的回复 是一个借助 curl 命令的解决方案。
curl -X PUT
-H "Accept: application/vnd.github.v3+json" \
-H "Authorization: token $TOKEN" \
https://api.github.com/notifications \
-d '{"last_read_at":"2025-10-15T10:00:00Z"}'
其中 $TOKEN
需要替换成你自己的 GitHub 个人访问令牌(Personal Access Token),可以在 https://github.com/settings/tokens/new 创建,注意 Select scopes 里需要勾选 notifications
。命令里的 last_read_at
字段的值可以按需修改为当前时间。
这种幽灵通知,看情况应该来自于某些居心不良的开发者,在他们的仓库里恶意 at 大量的用户来引流,然后这些用户就会收到通知。但如果这些仓库后来因为违规被删除,那么这些通知就会变成幽灵通知,无法被正常清除。
看讨论的时间线,这个问题已经存在至少四年了,GitHub 官方似乎并没有打算修复它。希望本文能帮到和我有同样困扰的朋友。
2025-08-30 00:00:00
项目里的页面一多,重复的页面布局就不可避免地冒了出来,作为程序员,消除重复,义不容辞。那么,今天就来聊聊如何在 FreeMarker 中复用页面 layout,让代码更优雅、更易维护。
FreeMarker 提供了 include 指令,可以把一些公共页面元素单独提取出来,然后在需要的地方通过 include 引入,例如:
<#-- includes/header.ftl -->
<p>我来组成头部</p>
<#-- includes/footer.ftl -->
<p>我来组成底部</p>
<#-- somepage.ftl -->
<#include "./includes/header.ftl">
<p>我是页面内容</p>
<#include "./includes/footer.ftl">
<script>
// 这里是一些 JavaScript 代码
</script>
但是所有类似的页面都要手写这个结构也挺麻烦的,更糟糕的是,一旦这些页面的结构发生变化,得在 N 个页面里反复修改,想想都头大。
很多博客引擎(比如 Jekyll)都支持 layout 功能,允许我们定义统一的页面布局,具体页面只需专注于内容。
FreeMarker 虽然没有内置 layout,但我们可以用 macro 来实现类似的效果。
比如,抽象出一个 layout/page.ftl 文件,作为布局模板:
<#-- layout/page.ftl -->
<#macro layout body js="">
<#include "../includes/header.ftl" />
${body}
<#include "../includes/footer.ftl" />
${js}
</#macro>
然后在需要的页面这样用:
<#import "./layout/page.ftl" as base>
<#assign body>
<p>我是页面内容</p>
<p>当前时间:<span id="current-time">${.now?string("yyyy-MM-dd HH:mm:ss")}</span></p>
</#assign>
<#assign js>
<script>
// 每隔一秒刷新当前时间
setInterval(function() {
document.getElementById("current-time").innerHTML = new Date().toLocaleString();
}, 1000);
</script>
</#assign>
<@base.layout body=body js=js />
页面效果如下:
虽然布局复用问题解决了,但每次新建页面还得手写一遍结构,还是不够优雅。程序员的信条是:能自动化的绝不手动!
这时就轮到编辑器/IDE 的 code snippets 功能登场了。把上面的结构定义成代码片段,新建页面时只需输入一个触发词,基本结构就自动生成。
以 VSCode 为例,可以在项目的 .vscode
目录下新建 layout.code-snippets
文件,内容如下:
{
"page_layout": {
"scope": "ftl",
"prefix": "layout:page",
"body": [
"<#import \"./layout/page.ftl\" as base>",
"",
"<#assign body>",
"",
"",
"",
"</#assign>",
"",
"<#assign js>",
"",
"<script>",
"",
"</script>",
"",
"</#assign>",
"",
"<@base.layout body=body js=js />"
],
"description": "Page layout template for FTL files"
}
}
这样新建 .ftl 文件后,输入 layout:page
,页面布局结构就自动生成了。
如图所示:
IntelliJ IDEA 也可以用 Live Templates 实现同样的效果。
本文相关代码和示例已上传至 GitHub,见 https://github.com/mzlogin/learn-spring 的 freemarker-test 目录。
2025-06-05 00:00:00
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。
如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LAUNCHPAD,按照说明在 Mac 上也可以使用。
而我想着后期做一些定制,所以还是需要在 Mac 上搭建 ESP-IDF 开发环境,自己编译和烧录固件。而这个在 小智 AI 聊天机器人百科全书 中没有详细提及,所以我就记录一下搭建过程,供有需要的朋友参考。
先上一个跑起来后的效果:
这一步参考乐鑫官方的 Linux 和 macOS 平台工具链的标准设置 完成,我这里指定了使用 ESP-IDF v5.4.1 版本,编译目标是 ESP32-S3。
brew install cmake ninja dfu-util ccache python3
mkdir ~/github
cd ~/github
git clone -b v5.4.1 --recursive https://github.com/espressif/esp-idf.git
ESP-IDF 将下载至 ~/github/esp-idf
目录。
cd ~/github/esp-idf
./install.sh esp32s3
在 ~/.zshrc 中添加以下内容:
alias get_idf='. $HOME/github/esp-idf/export.sh'
然后 source ~/.zshrc
使其生效。
这样在需要用到 ESP-IDF 环境的时候,只需要在终端中执行 get_idf
即可。
在执行以上步骤时,如果遇到问题,可以到 乐鑫官方文档 里看看有没有解决方案。
cd ~/github
git clone -b v1.6.2 [email protected]:78/xiaozhi-esp32.git
cd xiaozhi-esp32
然后接入 ESP32-S3 开发板,执行以下命令:
get_idf
idf.py set-target esp32s3
idf.py build
idf.py flash monitor
一切顺利的话,会向 ESP32-S3 开发板烧录小智 AI 固件,并且进入监控模式。
至此,就初步能跑起来了。按照提示进行 WiFi 配置和小智 AI 平台的设备绑定,即可开始使用。
如果后续需要定制固件,可以基于 ~/github/xiaozhi-esp32
目录进行修改和编译。若习惯使用 VSCode 进行开发,可以安装 适用于 VSCode 的 ESP-IDF 扩展,这样可以更方便地进行开发和调试。
2025-05-08 00:00:00
用 Mac mini 外接第三方键盘时,音量调节可能会让人感到痛苦。比如,Fn + F11 和 Fn + F12 这对音量调节快捷键可能没法用,而基于它们的微调组合键(Fn + Option + Shift + F11 和 Fn + Option + Shift + F12)更是想都别想。
我的工作键盘是 IKBC C87,虽然有个 Fn 键,但它和 Mac 键盘的 Fn 完全不是一回事。键盘自带的音量调节组合键只能一格一格地调节,听歌时还好,但在 Coding 时,这“一格”的音量有时就显得过于喧闹,让人无法沉浸式思考。微调音量成了刚需。
最终,Karabiner-Elements 拯救了我。
只需将右 Ctrl 键映射为 Fn 键,就能像用苹果妙控键盘一样,正常使用各种基于 Fn 的快捷键,包括音量微调。
Karabiner-Elements 是一款强大的键盘映射工具,功能远不止映射单键这么简单。它还能:
此外,官网还提供了丰富的规则库,按需导入即可:https://ke-complex-modifications.pqrs.org/。
现在,我终于可以在 Coding 时享受“刚刚好”的背景音乐,而不用被“一格音量”的霸道支配。
希望能帮到和我一样有此困扰的你。
2025-04-23 00:00:00
在小数据量场景下,如何优化模糊搜索体验?本文分享一个简单实用的方案,虽然有点“土”,但效果还不错。
假设有一张表 t_course
,数据量在三到四位数,字段 name
需要支持模糊搜索。用普通的 LIKE
语句,比如:
SELECT id, name FROM t_course WHERE name LIKE '%2025数学高一下%';
结果却查不到 2025年高一数学下学期
。这就很尴尬了,用户体验直接拉胯。
首先想到 MySQL 的全文索引,但要支持中文分词得改 ngram_token_size
配置,还得重启数据库。为了不动生产环境配置,果断放弃。
接着想到 Elasticsearch,但对这么简单的场景来说,未免有点“杀鸡用牛刀”。于是继续寻找更轻量的方案。
最后想到一个“土办法”:先对用户输入进行分词,再用 MySQL 的 INSTR
函数匹配。简单粗暴,但很实用。
一开始用了 jcseg
分词库,写了个工具类:
public class JcSegUtils {
private static final SegmenterConfig CONFIG = new SegmenterConfig(true);
private static final ADictionary DIC = DictionaryFactory.createSingletonDictionary(CONFIG);
public static List<String> segment(String text) throws IOException {
ISegment seg = ISegment.NLP.factory.create(CONFIG, DIC);
seg.reset(new StringReader(text));
IWord word;
List<String> result = new ArrayList<>();
while ((word = seg.next()) != null) {
String wordText = word.getValue();
if (StringUtils.isNotBlank(wordText)) {
result.add(wordText);
}
}
return result;
}
}
本地测试一切正常,但部署到测试环境后,分词结果却变了!比如:
[2025, 数学, 高一, 下]
[2025, 数, 学, 高, 1, 下]
原因是 jcseg
在 jar 包中加载默认配置和词库时出问题了。网上的解决方案大多是外置词库,但我懒得折腾,决定自己撸个简易分词工具。
最终实现如下:
public class WordSegmentationUtils {
private static final List<String> DICT;
private static final String COURSE_SEARCH_KEYWORD_LIST = "数学,物理,化学,生物,地理,历史,政治,英语,语文,高中,高一,高二,高三";
static {
DICT = new ArrayList<>();
for (int i = 2018; i <= 2099; i++) {
DICT.add(String.valueOf(i));
}
DICT.addAll(Arrays.asList(COURSE_SEARCH_KEYWORD_LIST.split(",")));
}
public static List<String> segment(String text) {
if (StringUtils.isBlank(text)) {
return new ArrayList<>();
}
List<String> segments = new ArrayList<>();
segments.add(text);
for (String word : DICT) {
segments = segment(segments, word);
}
return segments;
}
private static List<String> segment(List<String> segments, String word) {
List<String> newSegments = new ArrayList<>();
for (String segment : segments) {
if (segment.contains(word)) {
newSegments.add(word);
String[] split = segment.split(word);
for (String s : split) {
if (StringUtils.isNotBlank(s)) {
newSegments.add(s.trim());
}
}
} else {
newSegments.add(segment);
}
}
return newSegments;
}
}
这个工具基于一个简单的词典 DICT
,按词典中的词对输入文本进行分割。比如:
2025数学高一下
[2025, 数学, 高一, 下]
现在,无论用户输入以下哪种形式,都能成功匹配到 2025年高一数学下学期
:
2025高一数学下
2025 高一 数学
数学高一2025
这个方案虽然简单,但在小数据量场景下,性能和体验都能满足需求,且实现成本低。如果遇到特殊情况,可以通过动态更新词典来解决。
当然,这种“土办法”并不适合复杂场景。如果需求升级,可以再考虑 MySQL 全文索引或 Elasticsearch。
最后,自己一个人负责开发和运维就是任性!如果有团队一起评审,这方案可能早就被否了吧……额,达摩克利斯之剑高悬。
2025-03-21 00:00:00
有没有这样一种奇妙体验:家里老人对你的忠告嗤之以鼻,却对网络医疗广告深信不疑?仿佛那些”三天速效”“纯天然”“祖传秘方”字样自带某种魔力,能让他们心甘情愿掏空钱包?
我爸就是这样。医院检查出肠道息肉后,他不愿接受正规治疗,却在网上找了个不知名的乡镇医院,买了一堆贵得离谱的中药。当我劝他去本市三甲医院时,我们吵了一架,不欢而散。
我百思不得其解:为什么长辈会信任网上随机广告,胜过亲生儿女的劝告?是年代差异让他们天然信任媒体?是搜索引擎大品牌的背书效应?还是他们骨子里相信”酒香也怕巷子深,神医总在小诊所”?
曾经,我尝试给他安装丁香医生,希望提供专业的医疗参考。结果他嫌上面评论太少,不够可信。转头却给我展示抖音上的”体外无痛胃肠检查”广告——零评论,明显广告标识。我内心:这双标技术堪称奥运冠军水平啊!
绝望之际,我发现了转机。
一天晚上,我爸向我展示他用”豆包”AI制作的视频。灵光乍现!我让他用豆包查询那个”神奇”的体外检查技术。豆包给出了客观分析,而他竟然接受了这个意见!
第二天他主动用豆包查询其他健康问题,我立刻顺水推舟:”以后不用上百度了,直接问豆包就好。”他居然欣然接受!
所以,我所谓的方法其实很简单:用 AI 助手替代浏览器和搜索引擎。无论是豆包、DeepSeek 还是其他 AI 产品,它们提供简洁明了的单一答案,不会展示五花八门的广告链接,也(暂时)不会被商业利益左右。
感谢技术进步,我终于不用在”爸,这是骗人的”和”儿子,你懂什么”之间无限循环了。
类似的救赎,tk 教主的经历显然更加硬核,但我可能来不及去建立那样的信任了: