普通视图

自然学校12:三小时三百元

2025年12月8日 08:18

现在是06:39分,今天醒来蛮早,途中没有起夜一觉到现在,这是近几个月懒得的睡眠,没有懒床,醒来上了个卫生间,开始今天的新的一天常规工作和生活。

这篇文其实是昨晚记录的,但昨晚记录完后再次出现了怪事,点击发布后页面直接错误,提示无法连接,返回文章列表,查看自动保存,文章居然仅保存了200字左右,两段内容,气死我了,这事之前发生过几次,应该四五次吧。

这事发生后反思,没找到解决办法,解决网站出现这个问题,应该是不太可能。反思思考,决定改变记录方法。typecho的文章是支持MD的,Ob也是Md,为了不再出错,以后记录都现在OB记录,再发布到网站,问题得以解决,OB还多了份备份不错。

回到本文的主题,这次主要记录的是昨天带一组上海客户看房的事,看的事非我们托管的房,因为没房了,看的事村里其他村民家的房,从14点到17点10分的看房经历,有许多事情有许多值得记录和反思的内容,所以记录下。


隐私保护:因涉及隐私内容RSS已做隐藏,请通过原文链接阅读更多
版权声明:如无注明均为原创,未经允许不得任何形式转载
原文链接自然学校12:三小时三百元
  •  

不知道吃什么,那就吃肯德基

2025年12月7日 23:10

不知道吃什么,那就吃肯德基 - 第1张图片

和往月一样,今天加班,中午点外卖吃得挺饱,下午也吃了一点零食,六点多下班时肚子并没有太饿,本想直接回去,又担心晚上会饿,思来想去不知道吃什么合适,最后奔向了公司附近新开的一家肯德基。

不知道吃什么,那就吃肯德基

在我小时候的印象里,汉堡叫做汉堡包,搭配薯条、炸鸡、可乐,这也是我对老家本土品牌“欢乐季”最深远的记忆。每到周五,就会有工作人员在学校门口派发传单,四四方方的纸片上印着各种口味的汉堡、炸鸡、饮品,标注着价格,可以将其剪下,凭此以优惠价格购买。小时候收集了很多,但一次都没有用过。那时候一个汉堡、一份薯条,价格昂贵,家人也没有带我去过,每每路过,都会留下羡慕的眼神,尤其是看到有人生日,一家人在店里戴着生日帽、吹着蜡烛、吃着汉堡,更加羡慕。

长大以后,了解了肯德基、麦当劳,以及现在的塔斯汀,价格虽然没有特别便宜,但这却是上班族较为实惠的选择。人终究会为年少不可得之物困其一生,汉堡、薯条即是如此,遇上疯狂星期四,如果下班早,恰好当天有想吃的餐品,毫不犹豫奔向肯德基;周末在外面,到饭点,麦当劳和肯德基是我认为一人食的最佳选择。吮指原味鸡、热辣香骨鸡、蜜汁全鸡,经典的香辣鸡腿堡、老北京鸡肉卷,只要稍微回忆一下,便能想起它们的味道。

有一份专属工作日的 OK 三件套只需要 19.9 元,任选 1 份黄金 SPA 鸡排堡或者滋滋 YES 烤鸡腿堡,加一份劲爆鸡米花和中可,对于我来说,中午或晚上来上一份,就能吃饱。

陈仓颉在《麦门永存。》中提到肯德基更像是一个卖一些西餐的中餐厅,不仅有豆浆、烧饼、油条、茶叶蛋,今天看菜单,还有炸串、鸡架,越来越贴近中国人的饮食习惯和口味。在使用闲鱼下单肯德基之后,我更加确定了“不知道吃什么,那就吃肯德基”的合理性(ps:肯德基替换成麦当劳或其他品牌也适用)。一份香辣鸡腿堡,一杯中可,在闲鱼上代下单,不到 14 元,十分满意,本想放出截图,怕有广告嫌疑,遂作罢。

不知道吃什么,那就吃肯德基 - 第2张图片

大学时期,学校门口便有一家肯德基,每到疯狂星期四,除了在朋友圈和微信群发一发V我 50 的段子,也会拉上室友参与一番,我点热辣香骨鸡,你点黄金鸡块,如此疯狂,如此惬意。毕业后,工作了,每周依然有疯狂星期四,但我们都不再疯狂,仅仅只为了犒劳工作一天的自己,填饱肚子。

不知道吃什么,那就吃肯德基》最先出现在印记

猫鱼周刊 vol. 087 做一个 RSS 阅读器

2025年12月7日 19:35

关于本刊

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

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

RSS:猫鱼周刊

邮件订阅:猫鱼周刊

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

私信:leslieleung@proton.me

INIT

这周还是没有图。自从进了胶片的坑,出门要么不带数码的,要么带了也不拿出来拍。而一卷有 36 张,经常一次拍不完,又想等囤起来一起洗了,所以很久都没出什么图。

这周主要很多时间精力花在一个新坑上,起因是最近 Folo 团队裁员,裁掉了最核心的两位贡献者,而且发布了所谓「正式版」,加上怎么都关不掉的 AI 功能,实在让我觉得非常难受。RSS 阅读器这块我从一开始的 Reeder(Classic)到 NetNewsWire 再折腾到 Follow(Folo),来回折腾其实都只是不一样的界面,功能上没有什么特别亮眼的,也没有能针对我的信息获取工作流做优化的地方。于是我决定写一个自己的 RSS 阅读器 Glean,中文名叫拾灵,具体的后面细说。

STDIN

AI 吊牌

原文链接

说的是 AI 厂家在输出最后总会加的口癖(例如「要不要我帮你调整 xxx」)。作者举了几个小学生利用 AI 完成作文、作业,以及老师用来发朋友圈文案的例子。

我上周才提到过,如果把 AI 不加处理地用于教育领域,很容易造成「Shit in, shit out」的局面。小朋友的可塑性是非常强的,稍微「权威」的来源会对其认知产生很深远的影响。我记得以前小学的时候,学写信的格式,结尾一定要有「此致」「敬礼」,老师从来没有解释过为什么,我也理解不了为什么为什么要向信的读者敬礼(即使 ta 就是我的朋友),后来才知道其实还可以写「顺颂时祺」之类的套话,但实际上我用得最多的还是漫无章法的「祝好」、「祝万事如意」、「Best Wishes/Regards」。基础教育因为要面对非常多样性的群体,没法做到很细致,如果你拿着这个问题去问小学老师,一般得到的回答就是「你就记着这么用就行了,别问为什么,没有为什么,固定搭配」。

回到这个 AI 的例子,这里面最为讽刺的是,人把自己的知识蒸馏后训练成 AI,结果 AI 再蒸馏一遍知识给下一代的人,但每一步蒸馏都会有不少损失,所以才说对下一代的人来说,是「Shit in, shit^2 out」 。会不会下一代的人,写作风格不仅充满 AI 味,写出来的文章最后还要加上一句「如果你需要根据自己妈妈的特点进行个性化修改,我也可以帮你调整这篇作文。」?

Go 语言的编程哲学

原文链接

一个初学者角度的 Go 语言印象,以及对一些 Go 生态中常见哲学的理解和解读。我觉得 Go 在我心目中算是排名数一数二的语言,虽然我最近用的 Python 比较多。

首先,Go 是偏见非常强的语言,诸如只有 gofmt 一种风格、不允许未使用的变量、强制错误检查等,一定程度上让代码「规范」了很多。这里的反例就是我最讨厌的 Java,八股里面我最讨厌的一个问题「线程有几种创建方式」,在 Go 里只有一种,就是 go 关键字,不需要考虑茴字到底有几种写法。

其次,Go 在语法上非常简单,也没有复杂的语法糖,不管你的本命语言是什么,读 Go 都会觉得很顺畅。作者举的三目表达式就是一个非常好的例子,Go 就鼓励清晰的 if-else 结构。

还有一点,Go 让我写代码的时候逻辑变得非常清晰,或者说形成了一些良好的思考习惯。比如强制的错误检查,会让你在调用某个函数的时候,仔细考虑它会可能会出什么异常,对于业务来说应该怎么处理;又比如非面向对象的设计,会让你更加好地考虑怎么设计数据结构和接口;以及相对比较「裸」的并发写法,让你自己控制通道和锁,比起 Java 那套奇怪的调度方法好理解得多。

最后不得不说的就是跨平台能力和性能。与 Java 的虚拟机机制不同,Go 在所有平台上都是二进制原生运行的,这点就很无敌。性能方面,Go 的线程就是一个很好的例子,内存占用非常小,这在现在以云服务为主,内存寸土寸金的环境来说非常友好。

我的编程入门语言是 Python,甚至是 Python 2,最早是高中的时候自己搞了点书看。然后到大一学了 C/C++,自学了 PHP、Python 3、JavaScript,大二自学了 Java,大三自学了 Kotlin,后来工作之后又自学了 Go。在见识过基本上全部主流的语言之后,我心目中数一数二的就是 Go 和 Python,这俩也是我最为常用的语言。也许后面可以专门写篇文章展开讲讲我对各个语言的一些印象。

STDOUT

Kodak Snapic A1

Kodak Reto (柯达授权经销商)新出的一款胶片相机,25mm f9.5 广角镜头,固定 1/100 快门,有自动卷片、自动回卷和自动闪光,支持双区对焦、双重曝光,使用 7 号电池供电。

我在小红书上刷到的,看了一下国内贩子都在加价,就直接在官网下单买了,​99 + 3.5 运费,算下来比贩子卖得稍微便宜一点,而且 12.2 发货,第二天就收到了。我买的是白色款,包装里自带一个收纳袋和一条挂绳。

带着拍了几天,简单谈谈使用体验。它非常轻便,操作也很简单,上卷、打开电源,按快门即可。拍照的时候会有一个很轻的快门声,然后就是过卷马达的声音,这个比起手动过卷的机器来说没有拨动过片扳手的齿轮声悦耳,跟其他一些傻瓜机差不多。自带的挂绳很有意思,保证你在扫街的时候可以第一时间拿出来拍。(此处应有伍佰)我觉得胸前挂着相机大摇大摆走还是太张扬了,而且给的绳子有点长,在肚子上晃荡,所以我是绳子挂在脖子上,然后相机踹在前胸大口袋里,也很方便。

因为第一卷还没拍完,所以画质这些也许要等后面洗出来再聊。25mm 的镜头对我来说也是一个挑战,因为我平常惯用 50mm 甚至更长的焦段,25mm 这样一个大广角对我来说有点难以掌握,不过我发现这对于拍摄街景、建筑或者壁画这些东西非常适合。另外,固定 1/100 的快门和 f9.5 的光圈也有点蛋疼,官方的说明里推荐晴天/阴天户外使用 ISO 100/200 的卷,阴天、室内使用 ISO 400 的卷。我第一卷装的是全能 400,我也不确定宽容度有多高,但是拍摄的过程涵盖了阴天和大晴天,到时候看看效果吧。小红书上有不少评论说不如买几百块的二手 PS 机,二手 PS 机我也有,但是真的很难买到成色好,更别说全新的机器,而且 PS 机也做不到这样轻的重量。我觉得它的定位是玩具相机和 PS 机的中间,比玩具相机更好的画质、可以重复装卷,结构比 PS 机简单、成本更低。如果你对胶片感兴趣,我觉得这是一台比较值得入手尝试的机器。

Glean 拾灵

项目链接

前面说到,Folo 的变动,让我决定写一个自己的工具,它主要是一个自托管的 RSS 阅读器 + 个人知识管理工具,主要会有以下的功能:

  • RSS 订阅、阅读
  • 网页收藏
  • 智能推荐 & 偏好学习(根据对已读文章的喜欢/不喜欢反馈,以及收藏行为、对 Feed 和作者的亲和性,计算推荐分数,排序时间线上的文章/过滤分数较低的文章)
  • AI 摘要 / 打标(这个就是很多类似工具都会有的功能了)

所以 Glean 在产品形态上大致是 Folo/RSS Reader + Cubox/Karakeep 这样的东西,相当于是把我平时用来阅读和收藏整理信息的工具整合起来了。这是它目前的样子:

它会采用 OSS + SaaS 的商业模式,OSS 版本跟 SaaS 版本的区别就是 BYOK(Bring Your Own Key,使用自己的 AI) 和平台托管,你可以自建 OSS 版本获得基本上全部的功能,如果自建对你来说太麻烦,也可以付出 AI 的成本来获得对应的服务。

做自己的工具对自己来说是非常满足的事情,除了功能,一些交互细节也可以打磨到自己喜欢的样子。例如阅读界面右边的 Outline 功能,就有非常多考虑:

  • 垂直的进度条,来代替滚动条展示阅读进度
  • Outline 在阅读时会模糊,减少注意力分散
  • 进入文章时,Outline 会保持清晰 5 秒;低速滚动时,Outline 会保持模糊;高速滚动时,Outline 会清晰展示;鼠标悬浮时,Outline 会以比较高的速度变清晰。

另外,这次 Vibe Coding 我也在尝试一种比较新的工作流程,在过程中更多地使用 AI 和 文档,大致是:

  • 基于初始需求,反复细化、确定需求,产生 PRD 文档
  • 基于 PRD 文档,确定技术栈,敲定代码风格等,产生架构文档
  • 基于 PRD 文档和架构文档,拆分多个里程碑,决定每个里程碑实现什么内容,产生开发计划文档
  • 基于上面全部文档,生成对应里程碑的开发方案,包括测试和验收方案,修订后,放到项目中让 Claude Code/Cursor 去实现,并且利用浏览器 MCP 实现自动化测试和验收

这套工作流用下来还可以,开发的效率相对比较高,很多时候只要人工介入一下具体的交互细节就可以达到可用的程度。唯一的缺点可能是烧钱太快了,我的 $20 Claude Pro 和 $20 Cursor Pro 订阅一两天就用完了,Cursor 我升级到了 $60 还是不够用,已经上到 $200 了。

MISC

bililive-go

项目链接

直播录制工具,支持多个平台的直播录制。

EOF

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

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

别让你的烟,呛到无辜的人

2025年12月7日 20:08

最近发现公共场所抽烟的人越来越多了,楼道、电梯、路上,经常能碰到。那股烟味非常让人不太舒服。

我的店所在的这个写字楼楼道是一个密闭的空间,虽然物业也贴了楼道禁止吸烟的标语,但是也并没有什么实质性的作用,每当有人抽烟,屋里就会飘来一股烟味,有时候不得不把门关上,但是你又是在做生意,关上门又不合适,真实左右为难。

电梯里抽烟就更让人头疼了。很多次,进入电梯,里面都是一股浓浓的烟味,很多邻居也都在物业群里吐槽,这时候物业就是一只鸵鸟,不吭声,不吭声,逼急了,就说在电梯有禁烟的标语。其实他们也很无奈。还有一些年轻人也是,抽的电子烟,虽然没有那么难闻,但是危害是一样的。

电梯空间小,烟味散得慢。后面坐电梯的人还得闻这个味道。那些对烟味敏感的人,或者带着孩子的人,确实会不舒服。

路上抽烟的人也常见。走在他后面,风一吹,烟味就飘过来。有时候想超过去,他还走的挺快,闭着气赶紧穿过去,心里在骂他的娘。

我琢磨了一下,这些人大概有这么几种心态。第一种是觉得抽烟是自己的权利,想在哪抽就在哪抽。他们忘了自由的前提是不妨碍他人的自由。你的抽烟自由,不能建立在我吸二手烟的不自由上。

第二种是觉得抽几口没事,烟味一会儿就散了。他们也知道二手烟里的有害物质可以在空气中停留很久,但是,关我鸟事。

第三种是看到别人在公共场所抽烟,觉得自己也可以。典型的法不责众心理。问题是,错的事情不会因为做的人多就变成对的。

第四种是知道公共场所抽烟不对,但只要没人管,他们就照抽不误。说白了就是缺乏公德心。

有些人可能觉得,闻点烟味没什么大不了的。但二手烟确实有危害,特别是对孩子、孕妇、老人和有呼吸系统疾病的人。

其实我们国家早就出台了《公共场所控制吸烟条例》,明确规定室内公共场所、公共交通工具、电梯等地方禁止吸烟。问题是,规定是规定,执行是执行。很少看到有人因为公共场所抽烟被处罚。物业不管,保安不管,大家也都抱着"多一事不如少一事"的心态。

这就形成了一个恶性循环:没人管→更多人抽→更没人敢管。

说实话,作为一个普通市民,我们能做的有限。但至少可以从自己做起,如果家里有人抽烟,劝他们去专门的吸烟区,或者至少不要在密闭空间抽。遇到公共场所抽烟的人,可以礼貌地提醒。虽然可能没什么用,但至少表达了态度。

也可以支持公共场所设立明显的禁烟标志,支持物业加强管理。告诉孩子们吸烟的危害,特别是二手烟的危害。

抽烟是个人的选择,这个我理解。但如果在公共场所抽,确实会影响别人。公共场所是大家共用的,不是某个人的吸烟室。你的自由,不应该让别人不舒服。

那些在楼道、电梯、路上抽烟的朋友,可以换个角度想想:如果你的家人每天都要闻别人的烟味,你会怎么想?将心比心,其实大家都明白这个道理。

  •  

石头厝博客2.6版:死磕AI 逼出两个功能

2025年12月6日 23:50

终于被DeepSeek气疯了,实在忍不住对话框里骂了他几句大傻逼,大概有五次吧。这玩意用不好真的不如不用,尤其写代码这事,技术牛叉的压根看不起AI,有那修改完善的时间自己都敲出来了。经过推导重来以及重新整理需求,最终还是实现了我想要的功能。

因现在我干的项目还蛮多,团队就我一人,精力时间创意经验都有限,突发奇想训练出几个AI员工,那很多工作绝对秒杀现实中的员工,省钱还高效,以一当百用呀,原本停下不折腾的,这两天又继续了,再次联系与机器沟通技巧。

这次又完成了两个有意思且超级使用的功能,目前已修复完成同步到网站。


隐私保护:因涉及隐私内容RSS已做隐藏,请通过原文链接阅读更多
版权声明:如无注明均为原创,未经允许不得任何形式转载
原文链接石头厝博客2.6版:死磕AI 逼出两个功能
  •  

让Emlog5.3.1支持到PHP8.4

2025年12月6日 21:34

封面图

前些日子在 破袜子@大致 提到 让WordPress4.9在php8.2下正常运行。 在不知不觉间,PHP已经支持到8.X了。

好快,当初刚接触PHP时还是5.6。

恰好周六,恰好服务器搬家。

索性,在AI的帮助下,尝试让梦幻辰风这个运行在 Emlog5.3.1 版本的部落格,体验到php8的快乐。

通过搜索,在 一年又一年的博客 中找到《Emlog 5.3.1 兼容 PHP 8.0 错误修复》这篇博文,甚至于我还看到今年5月我留下的留言:

感谢大佬分享,我目前还是5.3.1,停留在PHP7.4,想着要不要到8.X的版本。

不过博主没有回我,Ta的更新也停留在2022年2月。

无妨,现在开始,结合 一年又一年的博客 的文章内容和我的操作日志,简单记录一下升级历史。

首先:在2016年或2017年左右,梦幻辰风已经修改版本支持到PHP7.X。

所以,以下所有折腾,都是基于PHP7.0时的版本。而修改Emlog5.3.1支持PHP7.0,我是参考的 星知苑 的博文《PHP7下安装Emlog5.3.1》(注:在本文发布时,Ta的站点已经打不开了。)

(注:以下内容好像是CSDN采集的 星知苑 博文,大致方法如下,如果造成影响,后果自负。)

1.修改include/lib/option.php,大概11行修改为mysqli

//即:默认MySQL连接方式,mysql或mysqli
const DEFAULT_MYSQLCONN = 'mysql';

改为

const DEFAULT_MYSQLCONN = 'mysqli';
//默认链接方式改为mysqli

2.修改include/lib/database.php,大概16行删除default:

case 'mysql':
default ://这边需要删除default:

3.修改include/lib/cache.php,大概195行加大括号

$$row['option_name'] = $row['option_value'];

改为

${$row['option_name']} = $row['option_value'];

4.PHP版本最近更新的比较快,autoload自动加载函数会报错,function.base.php开头的__autoload函数修改如下

spl_autoload_register(function($class) {
$class = strtolower($class);
if (file_exists(EMLOG_ROOT . '/include/model/' . $class . '.php')) {
require_once(EMLOG_ROOT . '/include/model/' . $class . '.php');
} elseif (file_exists(EMLOG_ROOT . '/include/lib/' . $class . '.php')) {
require_once(EMLOG_ROOT . '/include/lib/' . $class . '.php');
} elseif (file_exists(EMLOG_ROOT . '/include/controller/' . $class . '.php')) {
require_once(EMLOG_ROOT . '/include/controller/' . $class . '.php');
} else {
emMsg($class . '加载失败。');
}
});

5.对于部分插件写死了数据库链接方式,需要将 $DB = MySql::getInstance(); 改为 $DB = Database::getInstance(); 等等。

通过上述的修改,Emlog5.3.1(也许)可以支持PHP7.X了。现在,我们将向PHP8进发。

首先,在 init.php 的最开头调用 error_reporting() 设置了错误级别,要排错的话,需要把级别开到 E_ALL

*以下方法来自 一年又一年的博客

1.修改 /include/lib/function.base.php 文件:

  • 修改 __autoload() 函数:
//把 __autoload() 换为匿名函数,然后传给 spl_autoload_register 函数注册
// function __autoload($class) {
spl_autoload_register(function ($class) {
    $class = strtolower($class);
    if (file_exists(EMLOG_ROOT . '/include/model/' . $class . '.php')) {
        require_once(EMLOG_ROOT . '/include/model/' . $class . '.php');
    } elseif (file_exists(EMLOG_ROOT . '/include/lib/' . $class . '.php')) {
        require_once(EMLOG_ROOT . '/include/lib/' . $class . '.php');
    } elseif (file_exists(EMLOG_ROOT . '/include/controller/' . $class . '.php')) {
        require_once(EMLOG_ROOT . '/include/controller/' . $class . '.php');
    } else {
        emMsg($class . '加载失败。');
    }
});
  • 修补 get_magic_quotes_gpc() 函数

在文件开头 function doStripslashes() {} 函数声明前,补充定义纯用于兼容的桩函数 get_magic_quotes_gpc(),恒返回 FALSE。

if (!function_exists('get_magic_quotes_gpc')) {
    function get_magic_quotes_gpc() {
        return false;
    }
}
  • in_array() 前检查 NULL 值
//找到if (!@in_array($actionFunc, $emHooks[$hook])) ,将其修改为:
if (!@$emHooks[$hook] || !@in_array($actionFunc, $emHooks[$hook])) {

2.更新 passwordhash.php 密码函数库

前往 phpass 官网下载最新版本的 passwordhash.php ,替换原本的 /include/lib/passwordhash.php

现在,程序能跑了,但是会有一些小报错,如:

报错提示:Deprecated: Function get_magic_quotes_gpc() is deprecated in /www/include/lib/function.base.php on line 31

需要修改 include/lib/function.base.php 文件,找到 doStripslashes() 函数,修改如下:

function doStripslashes() {
    // 删除对 get_magic_quotes_gpc() 的调用
    // 直接进行 stripslashesDeep 处理
    $_GET = stripslashesDeep($_GET);
    $_POST = stripslashesDeep($_POST);
    $_COOKIE = stripslashesDeep($_COOKIE);
    $_REQUEST = stripslashesDeep($_REQUEST);
}

同时还需要修改 stripslashesDeep() 函数,修改如下:

function stripslashesDeep($value) {
    if (is_array($value)) {
        return array_map('stripslashesDeep', $value);
    } elseif (is_object($value)) {
        $vars = get_object_vars($value);
        foreach ($vars as $key => $data) {
            $value->{$key} = stripslashesDeep($data);
        }
        return $value;
    } else {
        // 添加对 null 值的检查
        return is_string($value) ? stripslashes($value) : $value;
    }
}

同时,元宝AI提示我还有一个完整替代方案:在 init.php最前面添加全局处理。

// 替换 magic_quotes_gpc 功能
if (version_compare(PHP_VERSION, '7.4', '>=')) {
    $process = [&$_GET, &$_POST, &$_COOKIE, &$_REQUEST];
    while (list($key, $val) = each($process)) {
        foreach ($val as $k => $v) {
            unset($process[$key][$k]);
            if (is_array($v)) {
                $process[$key][stripslashes($k)] = $v;
                $process[] = &$process[$key][stripslashes($k)];
            } else {
                $process[$key][stripslashes($k)] = stripslashes($v);
            }
        }
    }
    unset($process);
}

不过我没试,因为已经解决这个报错了。

不过在网站头部出现了:Notice: Undefined variable 。我看了一下代码如下:

<?php if ($logid): ?>

修改为:

<?php if (isset($logid) && $logid): ?>

即可。

随后,新的报错:

Deprecated: Using ${var} in strings is deprecated, use {$var} instead in /www/content/plugins/tpl_options/tpl_options.php on line 987

这个是emlog5.3.1的插件:模板设置

打开 tpl_options.php第 987 行:

// 旧代码(已弃用):
$values[$tag['tagname']] = "${tag['tagname']} (${tag['usenum']})";

// 新代码(推荐):
$values[$tag['tagname']] = "{$tag['tagname']} ({$tag['usenum']})";

完整修改后的函数:

/**
     * @param array $option
     * @return void
     */
    private function renderTag($option) {
        $tags = Cache::getInstance()->readCache('tags');
        $values = array();
        foreach ($tags as $tag) {
            // 修复 PHP 8.2 弃用的字符串插值语法
            $values[$tag['tagname']] = "{$tag['tagname']} ({$tag['usenum']})";
        }
        $option['values'] = $values;
        $this->renderCheckbox($option);
    }

随后,新的报错:

Deprecated: Log_Model::getLogsForHome(): Optional parameter $condition declared before required parameter $perPageNum is implicitly treated as a required parameter in /www/include/model/log_model.php on line 184

修改 log_model.php第 184 行的函数声明:

/**
 * 前台获取文章列表
 *
 * @param int $perPageNum
 * @param string $condition
 * @param int $page
 * @return array
 */
function getLogsForHome($perPageNum, $condition = '', $page = 1) {
    $timezone = Option::get('timezone');
    $start_limit = !empty($page) ? ($page - 1) * $perPageNum : 0;
    $limit = $perPageNum ? "LIMIT $start_limit, $perPageNum" : '';
    $sql = "SELECT * FROM " . DB_PREFIX . "blog WHERE type='blog' and hide='n' and checked='y' $condition $limit";
    $res = $this->db->query($sql);
    $logs = array();
    while ($row = $this->db->fetch_array($res)) {
        $row['date'] += $timezone * 3600;
        $row['log_title'] = htmlspecialchars(trim($row['title']));
        $row['log_url'] = Url::log($row['gid']);
        $row['logid'] = $row['gid'];
        $cookiePassword = isset($_COOKIE['em_logpwd_' . $row['gid']]) ? addslashes(trim($_COOKIE['em_logpwd_' . $row['gid']])) : '';
        if (!empty($row['password']) && $cookiePassword != $row['password']) {
            $row['excerpt'] = '<p>[该文章已设置加密,请点击标题输入密码访问]</p>';
        } else {
            if (!empty($row['excerpt'])) {
                $row['excerpt'] .= '';
                //$row['excerpt'] .= '<p class="readmore"><a href="' . Url::log($row['logid']) . '">阅读全文>></a></p>';
            }
        }
        $row['log_description'] = empty($row['excerpt']) ? breakLog($row['content'], $row['gid']) : $row['excerpt'];
        $row['attachment'] = '';
        $row['tag'] = '';
        $row['tbcount'] = 0;//兼容未删除引用的模板
        $logs[] = $row;
    }
    return $logs;
}

然后需要搜索整个代码库,找到所有调用 getLogsForHome函数的地方,并调整参数顺序。

参照如下:

// 旧调用方式:
$logs = $Log_Model->getLogsForHome('', $page, Option::get('index_lognum'));

// 新调用方式:
$logs = $Log_Model->getLogsForHome(Option::get('index_lognum'), '', $page);

当然,我没这么干,因为太多了。

我选择保持向后兼容,可以修改函数签名但不改变参数顺序:

/**
 * 前台获取文章列表
 *
 * @param string $condition
 * @param int $page
 * @param int|null $perPageNum
 * @return array
 */
function getLogsForHome($condition = '', $page = 1, $perPageNum = null) {
    // 如果 $perPageNum 为 null,使用默认值
    if ($perPageNum === null) {
        $perPageNum = Option::get('index_lognum') ?: 10;
    }

    $timezone = Option::get('timezone');
    $start_limit = !empty($page) ? ($page - 1) * $perPageNum : 0;
    $limit = $perPageNum ? "LIMIT $start_limit, $perPageNum" : '';
    $sql = "SELECT * FROM " . DB_PREFIX . "blog WHERE type='blog' and hide='n' and checked='y' $condition $limit";
    $res = $this->db->query($sql);
    $logs = array();
    while ($row = $this->db->fetch_array($res)) {
        $row['date'] += $timezone * 3600;
        $row['log_title'] = htmlspecialchars(trim($row['title']));
        $row['log_url'] = Url::log($row['gid']);
        $row['logid'] = $row['gid'];
        $cookiePassword = isset($_COOKIE['em_logpwd_' . $row['gid']]) ? addslashes(trim($_COOKIE['em_logpwd_' . $row['gid']])) : '';
        if (!empty($row['password']) && $cookiePassword != $row['password']) {
            $row['excerpt'] = '<p>[该文章已设置加密,请点击标题输入密码访问]</p>';
        } else {
            if (!empty($row['excerpt'])) {
                $row['excerpt'] .= '';
            }
        }
        $row['log_description'] = empty($row['excerpt']) ? breakLog($row['content'], $row['gid']) : $row['excerpt'];
        $row['attachment'] = '';
        $row['tag'] = '';
        $row['tbcount'] = 0;
        $logs[] = $row;
    }
    return $logs;
}

反正没报错了。

然后发现发邮件的插件 kl_sendmail 报错了,而且发送不了邮件。

于是,更新 PHPMailer 的版本,直接更新到7.0.1,将src中所有的文件上传到该插件的目录的class文件中,修改 kl_sendmail.php 文件代码如下:

<?php
/*
Plugin Name: Sendmail
Version: 3.8
Plugin URL: (链接已失效)
Description: 发送博客留言至E-mail。
Author: 作者:KLLER 
Author Email: kller@foxmail.com
Author URL: (链接已失效)
*/

!defined('EMLOG_ROOT') && exit('access deined!');

// 使用命名空间引入PHPMailer类
use PHPMailerPHPMailerPHPMailer;
use PHPMailerPHPMailerSMTP;
use PHPMailerPHPMailerException;

require_once(EMLOG_ROOT.'/content/plugins/kl_sendmail/class/PHPMailer.php');
require_once(EMLOG_ROOT.'/content/plugins/kl_sendmail/class/SMTP.php');
require_once(EMLOG_ROOT.'/content/plugins/kl_sendmail/class/Exception.php');

function UBB($content){
    $content=preg_replace('!【链接:(.*)】!uU',"<br /><a href="/jump.php?url=\1" target="_blank" rel="nofollow">\1</a><br />",$content);
    $content=preg_replace('!【图片链接:(.*)】!uU',"",$content);
    $content=preg_replace('!【图片地址:(.*)】!uU',"",$content);
    $content=preg_replace('!【图片:(.*)】!uU',"",$content);
    $content=preg_replace("!【隐藏评论】([sS]*?)【/隐藏评论】!uU","(小声说)<br /><i>\1</i>",$content);
    $content=preg_replace("!【隐藏内容】([sS]*?)【/隐藏内容】!uU","(小声说)<br /><i>\1</i>",$content);
    $content=preg_replace("!【隐藏内容:([sS]*?)】!uU","(小声说)<br /><i>\1</i>",$content);
    $content=preg_replace("!【隐藏信息:([sS]*?)】!uU","(小声说)<br /><i>\1</i>",$content);
    $content=preg_replace("!【隐藏:([sS]*?)】!uU","(小声说)<br /><i>\1</i>",$content);
    return $content;
}

function kl_sendmail_do($mailserver, $port, $mailuser, $mailpass, $mailto, $subject, $content, $fromname)
{
    try {
        $mail = new PHPMailer(true);

        // 字符设置
        $mail->CharSet = "UTF-8";
        $mail->Encoding = "base64";

        // 服务器设置
        if(KL_MAIL_SENDTYPE == 1) {
            $mail->isSMTP();
        } else {
            $mail->isMail();
        }

        $mail->Host = $mailserver;
        $mail->Port = $port;
        $mail->SMTPAuth = true;
        $mail->Username = $mailuser;
        $mail->Password = $mailpass;

        // SSL/TLS设置
        if($mailserver == 'smtp.gmail.com' || $mailserver == KL_MAIL_SMTP) {
            if($port == 465) {
                $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; // SSL
            } else {
                $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; // TLS
            }
        }

        // 发件人
        $mail->setFrom($mailuser, $fromname);

        // 收件人
        $mail->addAddress($mailto);

        // 内容
        $mail->isHTML(true);
        $mail->Subject = $subject;
        $mail->Body = $content;
        $mail->AltBody = strip_tags($content); // 纯文本版本

        // 发送邮件
        $mail->send();
        return true;

    } catch (Exception $e) {
        error_log("邮件发送失败: " . $mail->ErrorInfo);
        return false;
    }
}

function kl_sendmail_get_comment_mail()
{
    include(EMLOG_ROOT.'/content/plugins/kl_sendmail/kl_sendmail_config.php');
    if(KL_IS_SEND_MAIL == 'Y' || KL_IS_REPLY_MAIL == 'Y')
    {
        $comname = isset($_POST['comname']) ? addslashes(trim($_POST['comname'])) : '';
        $comment = isset($_POST['comment']) ? addslashes(trim($_POST['comment'])) : '';
        $commail = isset($_POST['commail']) ? addslashes(trim($_POST['commail'])) : '';
        $comurl = isset($_POST['comurl']) ? addslashes(trim($_POST['comurl'])) : '';
        $gid = isset($_POST['gid']) ? intval($_POST['gid']) : (isset($_GET['gid']) ? intval($_GET['gid']) : -1);
        $pid = isset($_POST['pid']) ? intval($_POST['pid']) : 0;
        $http_referer = empty($_SERVER['HTTP_REFERER']) ? BLOG_URL : $_SERVER['HTTP_REFERER'];

        $blogname = Option::get('blogname');
        $Log_Model = new Log_Model();
        $logData = $Log_Model->getOneLogForHome($gid);
        $log_title = $logData['log_title'];
        $subject = "文章《{$log_title}》收到了新的评论";

        if(!empty($commail)){$commail = $commail;}else{$commail = '未填写';};
        if(!empty($comurl)){$comurl = $comurl;}else{$comurl = '未填写';};

        if(strpos(KL_MAIL_TOEMAIL, '@139.com') === false)
        {
            $content = '这里懒得弄了~';
        }else{
            $content = $comment;
        }

        if(KL_IS_SEND_MAIL == 'Y')
        {
            if(ROLE == 'visitor') kl_sendmail_do(KL_MAIL_SMTP, KL_MAIL_PORT, KL_MAIL_SENDEMAIL, KL_MAIL_PASSWORD, KL_MAIL_TOEMAIL, $subject, $content, $blogname);
        }

        if(KL_IS_REPLY_MAIL == 'Y')
        {
            if($pid > 0)
            {
                $DB = Option::EMLOG_VERSION >= '5.3.0' ? Database::getInstance() : MySql::getInstance();
                $Comment_Model = new Comment_Model();
                $pinfo = $Comment_Model->getOneComment($pid);
                if(!empty($pinfo['mail']))
                {
                    $subject = '您在梦幻辰风发表的评论收到了Ta的回复';
                    $content = '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><div style="width:99%;">
    <div style="padding:0 15px;color:#111;background-color:#F5FFFA;border:1px solid #d8e3e8;border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;-webkit-border-radius:0 0 6px 6px;-khtml-border-radius:0 0 6px 6px;">
        <p><strong>'.$pinfo['poster'].'</strong>阁下,您之前在《'.$log_title.'》发表的评论:</p>
        <p style="padding:10px;background-color:#F0FFF0;">'.$pinfo['comment'].'</p>
        <p><strong>'.$comname.'</strong> 给您的回复:</p>
        <p style="padding:10px;background-color:#F0F8FF;">'.$comment.'</p>
        <p>您可以直接<a href="'.Url::log($gid).'#'.$pid.'" target="_blank">点击这里查看原文</a>,与'.$comname.'继续交流。</p>
        <p>感谢您关注 梦幻辰风,本通知由自动信箱发出,请勿直接回复本邮件。</p>
    </div>
</div>
<!-- 邮件版面格式来自挨踢路 -->';
                    $content=UBB($content);
                    kl_sendmail_do(KL_MAIL_SMTP, KL_MAIL_PORT, KL_MAIL_SENDEMAIL, KL_MAIL_PASSWORD, $pinfo['mail'], $subject, $content, $blogname);
                }
            }
        }
    }else{
        return;
    }
}
addAction('comment_saved', 'kl_sendmail_get_comment_mail');

/*
* 微语模块 *
*/
function kl_sendmail_get_twitter_mail($r, $name, $date, $tid)
{
    include(EMLOG_ROOT.'/content/plugins/kl_sendmail/kl_sendmail_config.php');
    if(KL_IS_TWITTER_MAIL == 'Y')
    {
        $DB = Option::EMLOG_VERSION >= '5.3.0' ? Database::getInstance() : MySql::getInstance();
        $blogname = Option::get('blogname');
        $sql = "select a.content, b.username from ".DB_PREFIX."twitter a left join ".DB_PREFIX."user b on b.uid=a.author where a.id={$tid}";
        $res = $DB->query($sql);
        $row = $DB->fetch_array($res);
        $author = $row['username'];
        $twitter = $row['content'];
        $subject = "{$author}发布的碎语收到了新的回复";
        if(strpos(KL_MAIL_TOEMAIL, '@139.com') === false)
        {
            $content = "{$author}发布的碎语:{$twitter}<br /><br />{$name}对碎语的回复:{$r}<br /><br /><strong>=> 现在就前往<a href="{$_SERVER['HTTP_REFERER']}" target="_blank">碎语页面</a>进行查看</strong><br />";
            $content=UBB($content);
        }else{
            $content = $r;
        }
        if(ROLE == 'visitor') kl_sendmail_do(KL_MAIL_SMTP, KL_MAIL_PORT, KL_MAIL_SENDEMAIL, KL_MAIL_PASSWORD, KL_MAIL_TOEMAIL, $subject, $content, $blogname);
    }
}
addAction('reply_twitter', 'kl_sendmail_get_twitter_mail');

/*
* 回复评论 *
*/
function kl_sendmail_put_reply_mail($commentId, $reply)
{
    global $userData;
    include(EMLOG_ROOT.'/content/plugins/kl_sendmail/kl_sendmail_config.php');
    if(KL_IS_REPLY_MAIL == 'Y')
    {
        $DB = Option::EMLOG_VERSION >= '5.3.0' ? Database::getInstance() : MySql::getInstance();
        $blogname = Option::get('blogname');
        $Comment_Model = new Comment_Model();
        $commentArray = $Comment_Model->getOneComment($commentId);
        extract($commentArray);
        $subject='您在梦幻辰风发表的评论收到了Ta的回复';
        if(strpos($mail, '@139.com') === false)
        {
            $emBlog = new Log_Model();
            $logData = $emBlog->getOneLogForHome($gid);
            $log_title = $logData['log_title'];
            $content =  '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><div style="width:99%;">
    <div style="padding:0 15px;color:#111;background-color:#F5FFFA;border:1px solid #d8e3e8;border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;-webkit-border-radius:0 0 6px 6px;-khtml-border-radius:0 0 6px 6px;">
        <p><strong>'.$poster.'</strong>阁下,您之前在《'.$log_title.'》发表的评论:</p>
        <p style="padding:10px;background-color:#F0FFF0;">'.$comment.'</p>
        <p><strong>'.$userData['username'].'</strong> 给您的回复:</p>
        <p style="padding:10px;background-color:#F0F8FF;">'.$reply.'</p>
        <p>您可以直接<a href="'.Url::log($gid).'#'.$pid.'" target="_blank">点击这里查看原文</a>,与'.$userData['username'].'继续交流。</p>
        <p>感谢您关注 梦幻辰风,本通知由自动信箱发出,请勿直接回复本邮件。</p>
    </div>
</div>
<!-- 邮件版面格式来自挨踢路 -->';
            $content=UBB($content);
        }else{
            $content = $reply;
            $content=UBB($content);
        }
        if($mail != '')    kl_sendmail_do(KL_MAIL_SMTP, KL_MAIL_PORT, KL_MAIL_SENDEMAIL, KL_MAIL_PASSWORD, $mail, $subject, $content, $blogname);
    }else{
        return;
    }
}
addAction('comment_reply', 'kl_sendmail_put_reply_mail');

function kl_sendmail_menu()
{
    echo '<div class="sidebarsubmenu" id="kl_sendmail"><a href="./plugin.php?plugin=kl_sendmail">邮件设置</a></div>';
}
addAction('adm_sidebar_ext', 'kl_sendmail_menu');
?>

完事。

但是梦幻辰风邮件回复一直失败。

结果发现是今年3月腾讯企业邮箱看我长时间没使用smtp功能,自己给我关了!

……

搞定。

2025年12月7日注:

可能是服务器性能原因,跑PHP8有点卡不愣登的。最后降成PHP7.4,结果反而更快乐了。

欢迎您来到梦幻辰风(www.mhcf.net)来体验更好的阅读!

版权信息

  •  

剖析千行C语言文本编辑器Kilo的技术细节

2025年12月6日 13:26

今天上午学习了一下Kilo的源代码。我很早以前就对文本编辑器的实现方法感兴趣了。

Kilo是一个很简易却不简陋的项目,清晰地展示了如何构建一个终端下的文本编辑器,它的目的不是真正让你学会去开发一个高标准高质量,能投入使用的文本编辑器,而是理解文本编辑器的核心骨架、理解一个看似庞大一团糟的问题的拆解思路。这是一个很好的起点。也过了一把爽玩C语言的瘾(虽然我并没有写几行代码)。

程序分析

整个项目只有一个文件,一千三百行代码。我用了大概一个半小时梳理了程序的执行流程,手画了一个流程图。为了美观,我又用Graphviz绘制了一个电子版:

程序流程图(大意)
程序流程图(大意)

这张图我省略了一些深的函数调用,但也能帮助我大体上掌握这个程序的执行流程。结合这张图与源码,我发现文本编辑器的核心功能——打开、编辑、保存,实现难度并不大,在C语言中容易踩坑的是缓冲区处理、文件读写这种老生常谈的内容。在这个程序中,调用最多、最重磅的部分是initEditor这个函数,以及后续的高亮处理,尤其是前者在窗口尺寸计算、修改后的做法上花费了大功夫。其实和终端环境的交互才是最麻烦的点,它提供的封装和抽象并不多,有很多需要自己手动调试的地方,繁琐是显著特征。

终端信号处理

我发现在终端程序里,需要快捷键的部分都是使用Raw mode和signal相关的函数组合实现的,在理解signal这个函数和它的有关宏的概念时,耗费了比较长的时间。

简单来说,signal用于处理用户在终端发出的信号,比如SIGINT代表由C-c产生的中断信号,SIGIGN代表忽略信号,即接受到这个信号以后什么都不做,关于如何接受信号,就要说起signal()这个函数。定义如下:

void (*signal(int sig, void (*func)(int)))(int);

看起来非常复杂,说人话就是接受两个参数,第一个参数是int类型的sig,是信号编号,比如SIGINT,这是要接收的信号。第二个参数是一个函数指针,接受一个返回void,参数是int类型的信号处理函数,使用第二个函数中的函数对接受到的信号做处理。函数返回原来的信号处理函数(函数指针)。

可以用typedef简化理解:

// 定义信号处理函数的类型
typedef void (*sighandler_t)(int);

// 用 typedef 重写 signal 声明
sighandler_t signal(int sig, sighandler_t func);

在Kilo中,C-c是被忽略的,因为它非常容易导致丢失修改,可以这样实现:

signal(SIGINT, SIGIGN);

不过,在Kilo的实现,是通过调用editorReadKey(),从Raw Mode 中读取一个按键存入数组,用switch匹配按键对应的值再返回给调用方,调用方也通过switch,匹配对应的操作函数。而在C-c的部分,则是直接break掉了。

...

case CTRL_C: /* Ctrl-c */
        /* We ignore ctrl-c, it can't be so simple to lose the changes
         * to the edited file. */
        break;

...

这种实现方法也有一定局限性,不同的终端模拟器可能发送不同的转义字符,硬编码转义字符会出现不适配的情况。并且使用read()阻塞读取输入有性能瓶颈。

精妙的数据结构与算法

这个程序最有趣的地方在于清晰、通用的数据结构的设计,以editorConfig为例:

struct editorConfig {
    int cx, cy;     /* Cursor x and y position in characters */
    int rowoff;     /* Offset of row displayed. */
    int coloff;     /* Offset of column displayed. */
    int screenrows; /* Number of rows that we can show */
    int screencols; /* Number of cols that we can show */
    int numrows;    /* Number of rows */
    int rawmode;    /* Is terminal raw mode enabled? */
    erow *row;      /* Rows */
    int dirty;      /* File modified but not saved. */
    char *filename; /* Currently open filename */
    char statusmsg[80];
    time_t statusmsg_time;
    struct editorSyntax *syntax; /* Current syntax highlight, or NULL. */
};

我们定义了一个editorConfig类型的变量,它全局唯一,维护了程序的基本状态,包括行、列、滚动偏移、终端尺寸。让程序状态的流转非常清楚。这些内容都是一个文本编辑器需要关心的最核心内容:光标位置、视图偏移、数据和文件的状态等信息。

通过这个结构体,能简单地获取程序当前的状态,或者为某项功能对状态作出修改,对一个新手来说还是挺拓宽思路的,至少我想不到怎么设计这些数据结构。

数据与显示的分离

editorConfig中嵌套了一个erow类型的变量,里面的东西也可以展开说说,定义如下:

typedef struct erow {
    int idx;           /* Row index in the file, zero-based. */
    int size;          /* Size of the row, excluding the null term. */
    int rsize;         /* Size of the rendered row. */
    char *chars;       /* Row content. */
    char *render;      /* Row content "rendered" for screen (for TABs). */
    unsigned char *hl; /* Syntax highlight type for each character in render.*/
    int hl_oc;         /* Row had open comment at end in last syntax highlight
                          check. */
} erow;

这里面有一个render字段,在editorUpdateRow()中,有这样的代码:

unsigned int tabs = 0, nonprint = 0;
    int j, idx;

    /* Create a version of the row we can directly print on the screen,
     * respecting tabs, substituting non printable characters with '?'. */
    free(row->render);
    for (j = 0; j < row->size; j++)
        if (row->chars[j] == TAB)
            tabs++;

    unsigned long long allocsize =
        (unsigned long long)row->size + tabs * 8 + nonprint * 9 + 1;
    if (allocsize > UINT32_MAX) {
        printf("Some line of the edited file is too long for kilo\n");
        exit(1);
    }

循环的if中使用的 TAB 定义在KEY_ACTION枚举,值为9,在ASCII码中是\t也就是水平制表符。代码在统计tab的数量。

问题在于,一个\t在内存中占1字节,但在屏幕显示的时候会占据八个字符的宽度,这里就体现出render的作用了,如果一行有两个\t,每个最多展开为八个空格,那么所需要计算的大小就是2 * 8 + chars的大小

(unsigned long long)row->size + tabs * 8 + nonprint * 9 + 1;

那个恒为0的变量nonprint可能是为将来打印不可见字符设计的。结尾的+1'\0'预留。

按照这个公式,给render分配内存:

row->render = malloc(row->size + tabs * 8 + nonprint * 9 + 1);

随后,这些代码在非制表位填充空格:

idx = 0;
    for (j = 0; j < row->size; j++) {
        if (row->chars[j] == TAB) {
            row->render[idx++] = ' ';
            while ((idx + 1) % 8 != 0) // 在非制表位填充空格
                row->render[idx++] = ' ';
        } else { // 正常字符直接赋值
            row->render[idx++] = row->chars[j];
        }
    }

row->rsize = idx; // 在循环结束的时候,idx等于写入字符总数
row-render[idx] = '\0'; //在字符末尾添加结束符

虽然有点绕,但设计还是非常巧妙的!


代码高亮

源码中使用大量篇幅实现了代码高亮,定义了一些关键字:

char *C_HL_extensions[] = {".c", ".h", ".cpp", ".hpp", ".cc", NULL};
char *C_HL_keywords[] = {
    /* C Keywords */
    "auto", "break", "case", "continue", "default", "do", "else", "enum",
    "extern", "for", "goto", "if", "register", "return", "sizeof", "static",
    "struct", "switch", "typedef", "union", "volatile", "while", "NULL",

    /* C++ Keywords */
    "alignas", "alignof", "and", "and_eq", "asm", "bitand", "bitor", "class",
    "compl", "constexpr", "const_cast", "deltype", "delete", "dynamic_cast",
    "explicit", "export", "false", "friend", "inline", "mutable", "namespace",
    "new", "noexcept", "not", "not_eq", "nullptr", "operator", "or", "or_eq",
    "private", "protected", "public", "reinterpret_cast", "static_assert",
    "static_cast", "template", "this", "thread_local", "throw", "true", "try",
    "typeid", "typename", "virtual", "xor", "xor_eq",

    /* C types */
    "int|", "long|", "double|", "float|", "char|", "unsigned|", "signed|",
    "void|", "short|", "auto|", "const|", "bool|", NULL};

然后在具体实现editorUpdateSyntax()中,简单粗暴地遍历字符匹配这些关键字。在一般的教学例子中这样实现是可以的,我认为在具体的工程中应当用词法分析、语法分析和字典树去匹配。更易于维护和拓展,也能适配复杂的嵌套。


上述分析提到的缺点都可以作为优化方向,比如提供更简单操作接口,用词法分析技术或接入LSP服务器,为程序提供Lua接口来扩展插件……不过我相信在古老的纯C应用中,添加这些功能的繁琐程度和开发周期简直是灾难级别的。但是在处理快捷键上,使用termcap库的难度应该小于修改代码高亮部分的难度。

这个项目最值得学习的点是如何将抽象的功能和终端联系起来、如何设计合理的数据结构以及标准库的使用。是阐释「程序 = 数据结构 + 算法」的很好例子。不过我自己是想不到那些函数该什么时候用,没准还会手动实现标准库造好的轮子呢。

学习的过程很好玩,从主函数开始探索整个程序,一段一段地跳转调用,A调用B,B调用C,C调用D,理解了逻辑后再把它们画成图,对感兴趣的部分深入研究,有一种前人用他的智慧抚平我大脑褶皱的感觉……读懂它,几乎就等于一只脚趾踩上了理解Vim / Nano等项目的大门吧。

想自己重新实现一次,然后加入自己的优化,比如联动Lua / Zig甚至是Go来实现上层的功能,好玩好玩真好玩。

头皮好痒,要长脑子了!


  • 在查资料的过程中又发现了 vismicro(它甚至是用Go写的),又有新玩具了!
  •  

酷鸭数据2026盼新活动来了!8核16G高性能配置,香港Colo数据中心,399续费同价

2025年12月6日 14:52

酷鸭数据2026盼新活动来了!8核16G高性能配置,香港Colo数据中心,399续费同价 - 第1张图片

老张博客搬家至酷鸭数据香港VPS》,目前使用已有一段时间了,感觉还是不错的。所以这里推一推自己的AFF。我的推广链接:https://www.kooya.hk/recommend/3gwffST4RbTO

目前酷鸭数据推出2026年盼新活动,8核16G高性能配置,香港Colo数据中心,399续费同价。目前老张博客也是放在这台母鸡上。当我看到这款配置的时候,我和酷鸭交流过,首先问这样高的配置是不是超售,得到答案是绝不会超开。其实像我们这样的博主,根本用不了这样高的配置,我想着能不能把配置降一降比如降到4核4G,带宽从5M往上提一提,这样价格还可以更低。得到的答案是年底了需要向总公司提交年度报告,如果总公司发现容量空那么多就觉得没必要给那么多配置容量给这边市场。所以原来活动配置是8核8G,索性就多送了8G内存。带宽太贵,只能给到5M,对于博客来说,5M的带宽也是足够使用的了。

如有想购买的,用我的推广链接注册呀!首页会弹出活动配置链接。还是那句话,不管是大厂还是小厂,记得天天备份数据哟!

酷鸭数据2026盼新活动来了!8核16G高性能配置,香港Colo数据中心,399续费同价 - 第2张图片

Basic System Information:
---------------------------------
Uptime     : 0 days, 0 hours, 21 minutes
Processor  : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz
CPU cores  : 8 @ 2299.984 MHz
AES-NI     : ✔ Enabled
VM-x/AMD-V : ✔ Enabled
RAM        : 15.4 GiB
Swap       : 0.0 KiB
Disk       : 130.0 GiB
Distro     : Debian GNU/Linux 12 (bookworm)
Kernel     : 4.18.0-358.el8.x86_64
VM Type    : KVM
IPv4/IPv6  : ✔ Online / ❌ Offline

IPv4 Network Information:
---------------------------------
ISP        : Cogent Communications
ASN        : AS401696 cognetcloud INC
Host       : FOJ IP TECHNOLOGY LIMITED
Location   : Mong Kok, Yau Tsim Mong District (KYT)
Country    : Hong Kong

fio Disk Speed Tests (Mixed R/W 50/50) (Partition -):
---------------------------------
Block Size | 4k            (IOPS) | 64k           (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 227.97 MB/s  (56.9k) | 1.44 GB/s    (22.5k)
Write      | 228.57 MB/s  (57.1k) | 1.44 GB/s    (22.6k)
Total      | 456.54 MB/s (114.1k) | 2.88 GB/s    (45.1k)
           |                      |                     
Block Size | 512k          (IOPS) | 1m            (IOPS)
  ------   | ---            ----  | ----           ---- 
Read       | 1.56 GB/s     (3.0k) | 1.61 GB/s     (1.5k)
Write      | 1.64 GB/s     (3.2k) | 1.71 GB/s     (1.6k)
Total      | 3.20 GB/s     (6.2k) | 3.32 GB/s     (3.2k)

Geekbench 5 Benchmark Test:
---------------------------------
Test            | Value                         
                |                               
Single Core     | 739                           
Multi Core      | 4879                          
Full Test       | https://browser.geekbench.com/v5/cpu/23949468

 SysBench CPU 测试 (Fast Mode, 1-Pass @ 5sec)
---------------------------------
 1 线程测试(单核)得分:          884 Scores
 8 线程测试(多核)得分:          6533 Scores
 SysBench 内存测试 (Fast Mode, 1-Pass @ 5sec)
---------------------------------
 单线程读测试:          18155.68 MB/s
 单线程写测试:          14202.38 MB/s

酷鸭数据2026盼新活动来了!8核16G高性能配置,香港Colo数据中心,399续费同价 - 第3张图片
酷鸭数据2026盼新活动来了!8核16G高性能配置,香港Colo数据中心,399续费同价 - 第4张图片
酷鸭数据2026盼新活动来了!8核16G高性能配置,香港Colo数据中心,399续费同价 - 第5张图片

20251205

2025年12月5日 23:40

谢天谢地,今天很早醒来,又正常睡到了八点。起来读《枪炮、病菌与钢铁》。强哥在美国前方发来了好多 App Store Awards Event 的照片。

没有太阳,网球课趣味少了 50%。

今天的目标只有一个,优化完 SnapVinyl 的 Random Pick 功能提交审核,重点是 Random 按钮的动画。做这些细致活很花时间,做到我觉得可以的程度又不知不觉四小时过去了。今远程日,我又开大了音乐。听了一下午德永英明,很喜欢德永英明的声音,似乎这种人声很适合我的音响。顶梁柱躺沙发睡的很香。

image.jpeg

1.9 版本顺利提交,倒上小酒,继续听歌。酒瓶像自己长出来一样,已经有这么多了。

image.jpeg

顶梁柱说想喝热汤,到店里一人来了一碗羊肉汤,大冷天太适合了。

image.jpeg

睡前欣赏贝九。

image.jpeg
  •  

探店(第08期):半月合集 遛娃探店的闲暇时光

2025年12月5日 23:15

探店蛮好玩的,目前来说轻松愉快,没有收店家的钱无推广压力,另外不花啥钱,随性的吃喝玩乐评价也客观真实不用考虑店家。要么是直播福袋,要么是团购抽奖,或者是抖音推的大额卷低价入的,成本接近零元。

与探店博主不一样,有着抖音高等级评价官身份,不收商家推广费,成了独立客观的点评人,这是新型的探店。次次都有不一样的体验和故事,既是吃喝玩乐,又是学习进步,收获颇多。

选些近期图还差不多的,有故事有见解有收获的记录下。


隐私保护:因涉及隐私内容RSS已做隐藏,请通过原文链接阅读更多
版权声明:如无注明均为原创,未经允许不得任何形式转载
原文链接探店(第08期):半月合集 遛娃探店的闲暇时光
  •  

一次对Webmention的探索

2025年12月5日 17:22

Webmention is a simple way to notify any URL when you link to it from your site.

It is an open web standard (W3C Recommendation) for conversations and interactions across the web, a powerful building block used for a growing distributed network of peer-to-peer comments, likes, reposts, and other responses across the web.

Webmention 是一种开放网络标准(W3C推荐标准),用于在链接到某个网址时,自动向该网址发送通知。它是构成分布式网络的基础组件,支持跨网站的点对点评论、点赞、转发等多种互动。

上一篇文章 写完后,我开始寻找更合适的评论方案。Webmention这个看起来古老,但又超前的形式很快引起了我的兴趣。「去中心化」这个词貌似对我有什么魔力,就比如Mastodon,比Twitter更吸引我。

我很想试试用Go手搓一套收发系统,但时间并不充裕,正巧webmention.io提供现成的服务和详细的文档,那就跟着文档配置吧。

验证与接收

教程提供了多种验证方案,这里使用最简便的Github. 只要在网站的首页挂上一个链接,指向用户档案页:

<a href="https://github.com/example" rel="me">github.com/example</a>

如果不想让它可见,也可以设置为一个`:

<link href="https://github.com/example" rel="me">

作为验证,你需要在Github主页中包含你的网站
作为验证,你需要在Github主页中包含你的网站

这里我部署后回退了一下网页就弹出来Github APP 认证请求了,大概需要刷新网页或打开Github?

这样,访问之后页面中提供的链接,就可以看到别人发送的Webmention了。

渲染

在网页中添加link

<link rel="webmention" href="https://webmention.io/你的用户名/webmention" />

我使用webmention.js,根据说明文档,可以复制仓库中的/static/webmention.min.js到博客的/static/js/webmention.min.js.

在评论框下方提供一个容器<div id="webmentions"></div>,并引用脚本

<div id="webmentions"></div>
<script src="/js/webmention.min.js"
    data-id="webmentions" // 与容器id匹配
    data-page-url="https://yourdomain.com/当前文章永久链接" // 建议使用
    data-max-webmentions="50" // 最多显示数量
    data-wordcount="30" // 回复预览最大字数
    data-sort-by="published" // 按照发布时间排序
    data-sort-dir="up" // 时间升序
    async>
</script>

Hugo用户在永久链接上可以使用:

data-page-url="https://yourdomain.com{{ .RelPermalink  }}"

为了本地测试,使用相对链接的变量拼接。脚本会对API发送请求,如果拿到的数据是空的,则会把最终的HTML输出为一个空字符串,所以刚开始看不到输出是正常的。大概写点CSS就可以正常投入使用了。

对Webmention的看法

我认为Webmention是一种很像友情链接的互惠模式,优于在评论区下方留下链接的形式。续上上一篇文章的话题,这样不仅促成高质量的互动,也能丰富自己的写作素材,分量应该重于评论区链接的手段。就像上一篇文章的结尾所说,利人利己,何乐而不为呢?

虽然这种模式的普及程度不高,但我相信去中心化会称为互联网的新趋势。开放、互联才能称为「互联网」。

我通过一些Webmention,一条一条顺藤摸瓜也找到了很多有趣的博客。我准备有时间优化渲染格式,可能也会研究一点桥接的功能,不过还是要保持博客一贯的简洁风格。当然,要先有人回应才有数据拿来渲染,欢迎大家使用Webmention参与互动!

  •  

探店(第07期):金茂凯悦半天深度体验

2025年12月5日 13:55

丽江凯悦酒店目前还没住过,但吃饭咖啡倒是常去,以前是自费去,现在是各种活动、福袋去。现在想想没住过凯悦的原因,应该是几次去吃饭发现和其他酒店比起来毫无特色。

近两年常去是因为孩子,凯悦酒店有了小动物区、丰富的亲子活动。丽江没有动物园,这里成了孩子看些小动物的好去处。近两个月常去一是因为福袋多,有到期时间,不去就错过了可惜;二是酒店里的银杏现在很漂亮。


隐私保护:因涉及隐私内容RSS已做隐藏,请通过原文链接阅读更多
版权声明:如无注明均为原创,未经允许不得任何形式转载
原文链接探店(第07期):金茂凯悦半天深度体验
  •  

那种虚伪的互动和友善,赶快把它撕破吧!

2025年12月4日 16:15

本文使用了一些带有攻击性的词汇,言论风格尖锐,介意者谨慎阅读。

本来想随便写写近期的事,写到网站最近的变化,写到了评论区的问题,越写越多,单独开一篇文章来说说吧。有时候觉得自己像个愤青一样,或者就是个愤青,整天喷天怼地的,应该平和点,不过实在不吐不快。


给评论区做减法

最近我把评论系统换成了Giscus,原因很简单:我累了。

关于评论系统,我想了很长时间。

至少是在我的博客里,如果是议论类文章,基于事实或观点的讨论、延伸,或反驳。偏题跑题、没看完或没看懂文章就急于发表评论的情况并不少见。而且性子比较急的朋友直接看到了评论区,自己的重点也可能会被带跑偏,于是构成恶性循环……

如果是记录类,分享见闻的文章,评论除了附和、赞同之类的评论,也难以找其它类型的评论,而且似乎也不会出现其他类型的评论。

我不是要说教读者该如何写评论,其实是我并不擅长回应这种评论,或者可以说,我没有跟人闲唠嗑说家常和寒暄的能力。一些评论留下的话茬实在让人很难接上,但不回复又显得冷漠,这是一直以来让我困扰的问题。和社交沾边的事,不是热脸贴冷屁股,就是痛并快乐着,又在期待与不安中挣扎。

七八月的时候我特意在评论区上方注了一行小字,具体原文不记得了,但我记得大意是:「我没回复是因为赞同,或者没什么可说的,而不是因为对你有意见」。主题改来改去,换来换去,就忘了重新写上这行字了。这是为我不回复这一行为的辩解,也是为读者可能会受到的「冷落」所做的提前安慰。

另一方面,维护Waline的外部CSS很麻烦,我也不想再修改CSS了。

我在使用Waline的时候尝试给评论功能做减法,我移除了表情回复和正文插入表情的功能、移除了表情包,删除了点赞、头衔,隐藏了头像和IP。力求打造安静的浏览和讨论氛围,甚至有移除网址文本框的想法。出于对博客的理念不同,有些同样在写博客的人就是冲着这个框来的,他们可能就是非常享受与人社交的获得感,把博客作为社交名片。

我觉得移除网址框可以减少为了自我营销而发送的评论,因为自己的博客沦为别人的推广场所的滋味很难受,也实在不知道怎么对待这些评论最为恰当。也想过关闭评论区。如果关闭评论区,可能会显得高冷,甚至是封闭。

折中的方案

转念一想,我怕的是什么?怕的是不知道如何接话。那么提高评论的「门槛」,让真正想要讨论的人评论,也许能解决问题。所以我换成了从前被我鄙视的Giscus。我当时觉得一定需要Github帐号才能评论是在一定程度上打消读者互动的积极性的,但现在才意识到了这是在放过自己。说真的,我非常佩服那些能一直和人聊得来,接得上话的人,也很佩服从一篇干巴巴的日常分享,甚至只有几张图的文章里找出可以评论的点的人,这确实是一种社交能力。

为了照顾没有Github或访问不方便的读者,我也放了一个邮件链接,可以点击发送邮件评论。由于使用场景,它多少会和「正式性」沾边,这也就间接提高了交流的质量。虽然我的博客并没有什么流量,但有效的交流,总比虚伪的敷衍强。我知道那些评论可能出于友善,但是妥善处理它们对我真的很难。

简单地来说,让说话更麻烦,是一种让说出来的话更有分量的方式。它会让说话的人经过深思熟虑,认真想想这些话到底值不值得我费这么大劲去说。

为什么我认为新的做法更好

可能有人要批判我是「精英主义」,把Email和Giscus强行和高质量讨论绑定在一起,或者认为我说以前的讨论都是低质量,我要为自己辩解。

首先,不是会用Github和Email的人都能写高质量的评论,而是这个方法在「倒逼」出正经的评论。

Github和技术身份强关联,它代表作为开源社区的一员的你,对某篇文章的看法。如果说得更「敏感」点,为了维护网络上的形象,至少不应该写出冒犯性的话语或无理取闹的反驳。而邮件在其它场合下的使用就如上文所说,是带有一定正式性的,并且写邮件的「仪式感」也很容易让人认真对待这件事。至少需要安安静静坐下来,组织完整的语言表达看法,留有正式的署名和日期,长期可以留存 —— 这些都在间接促成一个或严谨或私密的正式的讨论环境。

虽然这种做法可能会拦住内向但真诚或没有技术背景的读者,但也能拦住自我营销与应付不来的闲谈,我没有排斥喜爱轻松交流氛围的读者,这是不得已的取舍。而且博客不是论文期刊和政府文件,不是公示真理和规范的地方,这里是自我表达的地方,我在尽力打造理想中的秩序和风格。

此方法也能防止某些幽默的匿名评论。虽然当年百度贴吧和知乎都有很多匿名神贴,匿名才能让他们大胆表达,但总有这么一小波的大胆,给人一种傲慢和轻视的感觉。

林子不大也有各种鸟

比如说《罪恶感》这篇文章写得确实非常矫情,但有人匿名评论:

额…实际上只要是病,能够通过医学手段解决的,有钱人一般能请私人家教授课,可能这就是青春期吧…

这位网友的评论在不在理,确实是在理的。但连留个邮箱的勇气都没有,匿名在个人博客唱反调的行为,高高在上地想轻易解决慢性病,并把情绪都归结给青春期的傲慢的行为,属实让人忍俊不禁。

虽然文章发出来了肯定会有被人喷的风险,但我当时还是忍着自己的痛苦和不安,让人轻飘飘地说得一文不值的愤怒,给出了一个回复:

  1. 文本解读能力的问题

  2. 经历和共情能力的问题

我尊重你这个没啥大事的看法,不强求共鸣。

说得好,我同意了,玩去吧。

现在再仔细想想,这种文章根本不可能有人从搜索引擎来,大概率是某个混博客圈子的人,大概率也写博客。英国女作家伊夫林·比阿特丽斯·霍尔说过:「虽然我不同意你的观点,但我誓死捍卫你说话的权利。」

我今天说,虽然我不同意你的观点,但你唱反调连告诉我你是谁的胆子都没有,那你他妈的还是闭嘴吧!

如果你反对我,那就请你逻辑严密地写出几百字来批判我,即使是匿名我也会输得心服口服,在这种情况里匿名甚至可以促进大胆批判。而傲慢地写下反驳的话语却不敢署名的敢做不敢当的行为,我只能认为你是一个弱者,仗着个人博客对读者隐私的保护与自由的追求,输出轻浮的观点。

当然,匿名评论不是一定没有优势,如上文所说可以促进大胆批评,但任何事情都能从不同角度思考出不同的结果,没有人能做到真正的公平和客观,我只是尽力在维护边界。

在非匿名的评论中,有些人挂着自己的网站链接,写一句「学习了!」、「写得好!」,甚至连编程语言是哪个都没看清,指鹿为马地夸赞着,长期在各个博客的评论区混迹着。

出于人情的默契,这类行为往往被默许。我相信这是中文博客圈的大家都看破不说破的问题。今天我就在这里,以一个和大多数博主比起来确实是小孩的年龄,指出皇帝完全就是光着屁股在大街上乱逛。

我并不反对匿名评论和评论区,而是反对形式化的评论区,反对只是为了自己网站的外链的评论,反对形式化的、缺乏真诚的互动,而非反对社交本身。评论区应当是能围绕文章展开讨论、甚至是反驳的地方,哪怕意见相左,只要是真诚的表达,任何博主都会愿意真诚回应。但当评论的形式大于其本身的作用时,就该好好思考一下它究竟有没有继续存在下去的意义,以及如何存在下去了。

这种你唱我喝,看似牢固的关系,我在其它地方也有感受。

大概在十月份,我退掉了很多在上一次信息节食行动时没有退掉的QQ群。虽然确实有几位群友在问我为什么退群了,但过了一段时间后,仅仅是少了一个人自言自语。该打游戏的还在打游戏,该上班的还在上班,该骂街的还在骂街。这很正常,但也很残酷,少了某个人,大家照样吃饭,中东那几个国家照样战乱不断,地球照样天天转。

这种浅层的链接,只是披着一层友情之皮的无关紧要的感情。当然,这都是我的一家之言,你也可以认为我在社交上相当失败。

我能理解那种单纯对文章表达欣赏,又期待被回访的复杂心理。我个人的做法是开启一个点赞按钮。如果赞同或欣赏文章,也许不发那些简单但带有善意的评论,按一下那个按钮也能表达认同的态度。关于期待回访的心情,如果不是真正读了你的文字,被你的灵魂打动,那种回访就是肤浅且客套的,也许回访者也是出于迫不得已的礼仪压力。

所以无论是Q群聊天还是博客为了外链的「互访」,为了自己和他人考虑,为了让自己不再苦恼于如何应对根本没有留下话茬的评论,为了让别人不再绞尽脑汁填写自己博客的链接,如果你也厌倦了廉价的温情,那就来吧,那种虚伪的互动和友善,赶快把它撕破吧!

  •  
❌