CrazyAirhead

疯狂的傻瓜,傻瓜也疯狂——傻方能执著,疯狂才专注!

0%

把一份脚本植入自己大脑的记录

2025 年底,我在得到上看了李笑来和脱不花在《长谈》里的对谈 —— 在某次听课中知道有这么一个对谈,然后翻出来看的。话题是:如何不靠意志力改掉坏习惯。李笑来讲了自己戒烟的方法:一个 40 年烟龄的人,用 self-talk(自我对话)无痛戒了烟。方法不复杂,就是一个句式:

我从不抽烟

我听完的反应是:既然如此,拿自己试一次。

选什么?没纠结。选那个每次想起来都让我最烦自己的事:

走路看手机。

这个选择不是凭空来的。

我自己受过伤。一次是回家路上,刚下公交车就掏出手机看,结果脚崴了,肿了好几天。又一次是在家里,端着手机往厨房走,一头撞在玻璃移门上,额头疼了很久。还有一次是旁观:亲眼看到外卖小哥骑车看手机,把人给撞了。

按说我是个会有意识控制手机使用的人:软件通知全关了(除了短信和电话),娱乐手机和工作手机分开,微信取消了小红点。但上下班的路上,下车走路的那一小段,我还是会掏出手机,点亮,把几个 IM 都打开看一遍;没有消息,就从订阅的公众号里挑一篇文章来读 —— 似乎连这点时间也不能浪费。受过伤,也看过别人受伤,走路的时候照旧掏手机。为这件事,我苦恼了很久。

所以从第一天起,我就开始用 self-talk。一个人走路的时候,把那句脚本读出声:

“我是一个走路不看手机的人。”

“我是走路不看手机的人。”

这不算我第一次用语言给自己下咒。之前听罗胖的罗辑思维,讲探险家斯坦利在非洲丛林里每天坚持刮胡子的故事,我深受触动,当时就用”不刮胡子就不是文明人”这样的话吓自己,至今几乎每天刮胡子(原来都是胡子长了才刮),除非临时出差。那次是吓出来的,这一次,我给咒语加上了理由:走路看手机会受伤;走路看手机,和行尸走肉有什么区别。

真正的考验,在手伸进口袋的那一刻。手会自己伸进去,指尖碰到手机的瞬间,有一种极细微的踏实感,说不清为什么,就是”它在,就好”。头几天,我还是会掏出来看。于是我补了一条规则:要看,就停下来,站在原地,把几个 IM 的消息确认完,再开始走。剩下的路程交给脚本 —— 碰到手机就念,边走边念,念完再念。

大约一周之后,变化来了:不碰手机,心里也不再有那种空落落的感觉;即便手机抓在手上,也可以做到不看。

就这样过了半年多,我以为自己已经完全做到了走路不看手机。

然后,前两周,老家出了些事,好些天上班时间我都待在医院里。手机通知我一直关着,那几天却开始发慌——“万一漏掉什么呢?””耽误同事的进度怎么办?””项目进度本来就紧,我已经在请假了。”于是走路时,我又开始掏手机。每次掏出来,往往什么都没有。但下一次,照掏。

更让我不安的是,从老家回来之后,这个行为并没有自己消失。爬楼梯的时候,我又会把手机掏出来,一直看到进家门。

半年攒下的咒语效果,两周就退了回去。

好在作息恢复正常之后,我也警醒过来:这不是我想要的行为。我又开始念动咒语——

“我是走路不看手机的人。”

一遍,两遍,十遍。只要是我自己一个人走路、不用接送小孩,我就一直念,从下车到工位的路上念,从下车到家的路上也念。同时,把那条规则重新立了起来:

只要在路上想起任何需要处理的事 —— 停下来,处理完,再走。

咒语管身份,规则管动作。

也是因为这次反复,我把《半秒之间》完整地看了一遍。

这些天,我处的状态是:大部分时候正常,偶尔还是会掏手机。但和从前不一样——

想看的时候,知道自己在看,并且手里握着选择权。 我可以停下来看一会,我可以就这么抓着手机,我可以把手机放回口袋。

所以这不是一篇成功的记录,此刻的我依然会掏手机,改变远没有完成,只是方向对了。

如果你身上也有这样一件”每次想起来都烦自己”的事,方法都在上面了,拢共四步。

先找一个真实的理由。最好带着具体的画面和疼痛,别用别人的道理。

再写出你的脚本。套那个格式:”因为我是 X,当 Y 发生时,我会做 Z。”一句话,越短越好。

然后在触发场景里,用自己的声音念出来。出声,重复,允许它枯燥;带感情更好,但别演。

最后,准备迎接反复。反复不是失败,它只是过程的一部分。每次复发之后,你只需要再次念动咒语。

植入一个思想,不需要三层梦境,只需要重复和半秒。

《盗梦空间》里有一句台词:“一个念头,一旦生根,就几乎不可能被拔掉。”

电影中,柯布团队耗时数周,潜入一层又一层梦境,冒着永远迷失在潜意识边缘的风险,才在费舍脑中植入了一个想法。而植入的最后一步最关键:他们把“毁掉父亲的产业”包装成“父亲希望你去过自己的生活”。

费舍醒来,看着风车流泪,做出了改变一生的决定。

他坚信不疑:这是我自己的选择。

这才是“思想植入”最可怕的地方——最高级的植入,是让你以为那是你的本能。

你可能会说,电影是科幻,现实里哪有这种事。

现实里不仅有,而且每天都在你身上发生。它不需要造梦机,不需要化学药剂师 —— 它只需要重复半秒

01 现实里的盗梦者,不需要进入你的梦

电影的植入之所以难,是因为要对抗人的本能防御:人抗拒一切别人塞给自己的想法。

所以柯布团队的破解方案,是让植入的想法“感觉像自己长出来的”。

而现实中,这个难题早已被绕过。因为有一套机制,植入的想法从来不会以“别人的想法”的面目出现——它直接伪装成你的第一反应。

你小时候被说了八千次“你怎么这么笨”,“我不行”就被写进了系统。成年后每次遇到挑战,半秒之内你就自动退缩了——你以为那叫“没自信”,其实是自动播放。

你从小看了几十万条广告,每一条都在说“拥有它你才完整”。“消费即幸福”成了你的默认反应,月底看到账单,你都不知道钱是怎么花出去的。

你每天刷短视频,算法不断投喂“三秒必爆”的刺激。你的大脑被重新训练:长文看不下去,深度思考坐不住,半秒之内,手已经划走了。

这些都不是你的选择。这是别人趁你不注意,写进你大脑的代码。

思想就像病毒。一旦被植入,它就开始自我复制,而你甚至不知道自己是宿主。

而最可怕的是:你把这段代码,当成了“自我”的一部分。

02 为什么是半秒?因为你的意志力根本来不及

这背后的机制,比电影精密得多。

诺贝尔奖得主卡尼曼在《思考,快与慢》中指出,人脑有两套系统:

系统 1:本能、快速、不耗能的自动反应,在 0.5 秒内完成判断。
系统 2:慢、耗能、需要刻意启动——这才是你以为的“你自己”。

而意志力,属于系统 2。

也就是说,当你终于反应过来“不该抽这根烟”时,烟已经点上了;当你想起“不该发脾气”时,话已经吼出去了。

你不是败给了欲望。你是败给了速度

那么,谁在控制这半秒?

李笑来在《The Half Second: How First Reactions Get Installed, and How to Edit Them》中给出了答案:大脑里有一个类似“频率计数器”的机制 —— 像一台最原始的投票器。

它的核心特征只有一条:只数次数,不辨真伪

认知心理学的研究反复验证了这一点。1977 年,Hasher、Goldstein 和 Toppino 的“虚幻真实效应”实验发现:被重复过的陈述,无论真假,都会获得更高的真实度评分。2015 年,Fazio 的实验更狠 —— 受试者明明知道苏格兰短裙叫 kilt,但在反复听到“苏格兰短裙叫 sari 的错误说法之后,对错误信息的“真实感”评分依然上升。

你明确知道一件事是假的,仍然挡不住重复带来的“真实感入侵”。

这就是现实版“盗梦”的底层原理:

谁重复得多,谁就赢。

03 反制的第一步:在旧代码和行动之间,开一道缝

听起来很绝望:大脑里装满了别人植入的程序,而且改都来不及改。

别急。知道程序是怎么装进去的,就是卸载重装的第一步。

而反制分两步。第一步,不是改 —— 是看见。

你无法阻止第一反应的出现,它在意识到达之前就生成了。但你可以不让它直接变成行动。

当焦虑、愤怒、自我怀疑涌起时,在心里做一个标记:

“这不是我。这是旧代码在运行。”

这个标记只需要零点几秒,但它能在旧反应和行动之间,撕开一道缝隙。电影里,柯布靠旋转的陀螺分辨梦境与现实;现实里,你需要的不是陀螺,是一句“觉察咒语”:

“等一下,这是谁的脚本?”

“我选择不马上反应。”

不需要复杂。半秒的觉察,就能打破半秒的劫持。

先别急着重写:问一个更根本的问题——“我是谁?”

写新脚本之前,有一个问题必须先处理。

有人会问:你说“我是一个不抽烟的人”要重复到变成第一反应——这不也是植入吗? 用一个假身份骗自己,跟被广告骗,有什么区别?

问得好。这个问题,正好把讨论推向了最深处。

在回答它之前,先搞清楚一件事:植入的机制是中性的。别人用它来写你的代码,你也可以用它来写自己的代码。区别不在“重复”这个动作本身,而在重复开始之前——有没有经过你的审问。

批判性思维里有一个基本功:审视一个信念的来源,而不是只看它的内容。 面对脑子里任何一个“我是______”的句子,问四个问题:

1. 这个说法,最早是谁告诉我的?(来源)

2. 我是自己验证过它,还是只是听得次数多了?(证据 vs 重复)

3. 如果我相信它,谁会受益?(利益)

4. 它帮我活得更好,还是更差?(后果)

拿“我不行”来过一遍:来源是童年的一句评价;你从没验证过,只是听了几千次;如果你信它,你的父母获得了一个“听话谦虚”的孩子、你的老板获得了不给你加薪的理由、你的恐惧获得了继续管着你的权力;后果是你一次次退缩。

四个问题问完,你会得到一个让人脊背发凉的结论——

“我不行”从来没有被证明过。它只是被重复过。

而“重复”不构成证据。频率计数器把它当成真的,只因为它数不清“事实”和“复读”。

哲学家大卫·休谟在两百多年前就发现了这件事。他说:我往内看,试图找到那个“自我”,找到的从来只是一串具体的知觉 —— 一个念头、一阵情绪、一段记忆——“自我”这个东西本身,从来没被找到过。

现代心理学接过了这个结论:所谓“我是谁”,不是一块被发现的矿石,而是一个被讲述的故事。 而只要是故事,就有作者。问题只在于 —— 现在执笔的,是不是你。

这就是新旧身份的本质区别,也是对开头那个问题的回答:

被广告植入的身份和自我对话的身份,装法确实一样 —— 都是重复。区别不在安装过程,而在安装之前有没有经过你自己的审问。 一个通过了四个问题的身份,不是自我欺骗,而是自我选择。你没骗自己说“我天生不抽烟”,你只是决定:从今天起,这句话由我来写。

所以,在写新脚本之前,先做一件更根本的事:把你脑子里所有的“我是______”列出来,逐条过那四个问题。

但注意:写这份清单的时候,脑子里想的是“我身上已经装了什么”,而不是“我应该是什么”。前者是调查,后者是目标。先把调查做完,再定目标 —— 顺序不能反。

这份清单可能会像这样:

  • “母亲说我悲观,父亲说我实际,同学说我严肃”
  • “老师说我聪明但不努力”
  • “大家都说我是好人”
  • “我不适合当众讲话”

一条一条审问。然后你会分成三堆:

  • 有证据、对我有用的 —— 留下。这是你的。
  • 只是重复、对我不利的 —— 标记。这是别人的代码。
  • 只是重复、但对我有用的 —— 也留下。来源不重要,你审问过它,它就是你的了。

比如:“我不行”——划掉。
“我是学习者”——留下。
“我是乐观的人”——哪怕它来源不明(可能是你母亲经常这么说),但你发现它确实在帮助你面对困难,那就保留。

注意,批判性思维的目的不是拆毁一切,而是把无意识接受,变成有意识选择。

而这个整理过程本身,就是“我是谁”的答案——

你不是你脑中的那些代码。你是那个能把代码拉出来逐条审问的东西。

代码会恐惧、会退缩、会上瘾,但审问本身不恐惧、不退缩、不上瘾。你平时感觉不到它,因为它不吵不闹——就像你看得见屏幕上的画面,却注意不到投影仪的光。

它是观察者。是审问者。是那个在半秒缝隙里能说“等一下”的东西。

前面的觉察咒语为什么有效?“这不是我,这是旧代码在运行” —— 现在你知道这句话不是修辞了。它是一个身份声明:代码在跑,而我在看。

想清楚这一点,自我对话就不再有“骗自己”的心理负担 —— 你不是一个容器在被灌输,而是一个作者在改稿。

那支笔,现在在你手上。下一节,我们写新脚本。

05 第二步:用你的脚本,覆盖别人的程序

我们问完了“我是谁”,现在开始写新的脚本。

方法叫 self-talk(自我对话)。但它不是对着镜子说“我要变好”——它的核心是改身份,而不是对抗行为。

还是以抽烟为例。一个第一反应由三样东西构成:

  • 身份:我是抽烟的人
  • 情境:饭后、焦虑时、朋友递烟时
  • 动作:点一根

如果你只对抗动作,就要在每一个情境里分别作战:饭后不能抽、焦虑不能抽、别人递烟不能接……情境是无穷的,你挡不住。

但如果你改身份 —— “我是一个不抽烟的人” —— 这一条一旦植入,会自动作用于所有情境。

身份是枢纽。改一个枢纽,胜过打一百场遭遇战

为什么身份改得动?还记得频率计数器吗?它不辨真伪,只数次数。旧身份是被重复几千次植入进去的;那么新的身份声明,只要重复的次数足够多,同样植入得进去。

你是在用植入你的机制,反过来自己编码自己

具体怎么写这个新脚本?李笑来给出了 ARISE 框架:

  • A(Action,动作) :身体能执行、半秒内能启动的具体动作
  • R(Reason,理由) :做这个动作的原因
  • I(Identity,身份) :你想成为什么样的人
  • S(Situation,情境) :触发旧习惯的具体场景
  • E(Emotion,情绪) :行动时的感受

脚本的核心是动作。不要给大脑下“状态命令”,要给身体下动作命令:

  • “我很冷静” → 不如 “我深呼吸三次再回答”
  • “我不焦虑” → 不如 “账单来了,我 60 秒内打开它”
  • “我不冲动交易” → 不如 “价格波动时,我 24 小时不操作”
  • “我要更爱孩子” → 不如 “孩子说话时,我看着他的眼睛再回答”

完整的脚本长这样:

因为我是 ______ (你想成为的身份),
______ (触发旧习惯的场景)发生时,
我会做 ______ (一个具体的身体动作),
因为 ______ (做这个动作的理由)。

然后,用自己的声音,在触发场景里反复说给自己听

研究发现,一个新习惯稳定下来的中位时间约为 66 天,范围从 18 天到 254 天不等。你的旧脚本被重复了几千次,新脚本不可能几天就取代它。但只要你的重复次数追上旧脚本,频率计数器就会调转枪口——

这次,为你而战。

06 陀螺倒下之前

把电影和现实放在一起看:

《盗梦空间》 你的现实
谁在植入 专业盗梦团队 广告、算法、童年、环境
植入方式 进入梦境 利用频率计数器,重复再重复
植入结果 费舍以为想法是自己的 你以为第一反应是“直觉”
谁来破解 柯布团队 只有你自己

电影结尾,柯布回到家中,旋转陀螺,镜头在将倒未倒时切黑。诺兰问的是:你确定自己不在梦中?

《半秒之间》问的是另一个:你确定你的每一个第一反应,真的是“你”的?

答案多半是:不是。它们是被植入的。被父母、被广告、被算法、被重复了无数次的某句话。

但好消息藏在坏消息里——既然能被植入,就能被重写

你无法阻止半秒之内的反应,它快过意识。但你可以决定那之后的一切。你脑中的代码不是你;你是那个在半秒缝隙里能说“等一下”的观察者

从今天起,在每一个微小的瞬间里,给自己留出半秒的觉察,然后问一句:

“这是我的选择,还是别人植入的程序?”

能回答这个问题的人,才算真正从梦里醒来。

陀螺终会倒下——而旧代码失效的那一天,你会发现:这次醒来的感觉,和植入的梦,完全不同。

附:两份工具,请收好

工具一:四问清单(先审后写)

把你脑中的“我是______”逐条列出——写的时候想的是“我已经被植入了什么”,不是“我应该是什么”。然后过四个问题:

  1. 这个说法,最早是谁告诉我的?
  2. 我是自己验证过它,还是只是听得次数多了?
  3. 如果我相信它,谁会受益?
  4. 它帮我活得更好,还是更差?

工具二:ARISE 脚本填空模板(写下来,每天说一遍)

因为我是 ______ (身份),
______ (情境)发生时,
我会做 ______ (具体动作),
因为 ______ (理由)。

代码写完,只算把事情做了一半;文档建起来,这件事才算完整。

个人开源项目的宿命,大多是这样的:代码写完,发一个 README,心里默念「文档以后再补」—— 而「以后」永远不会来。不是不想补,是完整性太贵。一个像样的文档站,意味着选型、配置、部署、CI、内容规范、校验脚本……每一项都得查资料、踩坑、返工。于是索性砍掉,美其名曰「聚焦核心」。

前面介绍过 aifei-go —— Java 版 Aifei 的 Go 移植。它的处境比一般项目更尴尬:这是一个宣称「为 AI Coding 而生」的框架,Java 版有官方文档 aifei.cn/doc 摆在那里当基准,Go 版要是只有一个 README,多少有点打脸。而且这里还藏着一个顺理成章的推论 —— 既然是为 AI Coding 而生的框架,它的文档,本来就该由 AI 来写。于是花了两天,把文档站建了起来:https://crazy-airhead.github.io/aifei-go/

完整性的坑,AI 都记得

技术上没什么新鲜事:VitePress + pnpm + GitHub Actions,push 到 master 自动构建、发布到 gh-pages。新鲜的不是技术,是细节有人替你想着。随手摘两段:

1
2
3
4
5
6
7
8
9
10
11
- name: Checkout(完整历史,供 lastUpdated 读取时间)
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Deploy to gh-pages
uses: peaceiris/actions-gh-pages@v4
with:
publish_dir: docs/.vitepress/dist
# dist 是纯生成产物:单提交孤儿分支,保证删除的页面同步消失
force_orphan: true

配置里的这些注释,本身就说明创建一个文档站不是一件容易的事情。没写进注释里的还有一堆:base 路径要配 /aifei-go/、head 里的 favicon 不会自动加前缀得写全路径、paths 过滤让只有 docs 变更才触发构建、concurrency 把排队的旧部署取消掉、sitemap 要配 hostname……但这些坑,每一个都是前人踩过的,AI 都记得。以前要花一个下午翻 issue 才能凑齐的事,现在是一轮对话。

先写「怎么写」,再写「写什么」

真正值得记的不是部署,是内容的生产方式。开工第一步,不是让 AI 写文档,而是先写「文档怎么写」—— 一份 docs/guide/_STYLE.md 写作规范。统一模板大纲(背景 → 架构 → 关键 API → 核心机制 → 配置集成 → 模块结构 → 总结)、风格规则(多用表格和代码块、交叉链接、信息密度要高),再加几条质量红线:

  • 内容来源必须实际读取源码,不得凭记忆编造
  • 代码示例的类型名 / 方法签名 / 配置键必须与源码一致,逐项核实
  • 篇幅 300~500 行,完成后用 wc -lgrep 自查行数与标题结构。

规范开头有一句:「本规范人与 AI 均适用」。人做决策,AI 生成 —— 落到这件事上,就是人定标准、立标杆(风格范例是那篇五百行的 data-isolate 文档),AI 读源码、照模板写。最后落地的,是二十五篇模块文档。这套招数其实是框架自己的招数:Just Service 用命名约定让 AI 稳定生成代码,_STYLE.md 用模板和红线让 AI 稳定生成文档,一回事。

抽卡之后,要有验收

规范把写作经验沉淀了下来,让下一篇的起点更高。但光有规范还不够 —— AI 的输出是基于概率的,「抽卡」式的不确定性不会因为换了任务就消失。git 历史里躺着证据:首次部署之后,紧跟一串提交 —— 更新 Logo、更新文档图、把 Actions 升级到 Node 24 运行时、修 Markdown 语法错误、修 index.md 语法错误。所以要有验收闭环:构建、校验、人工过目,一轮下来,概率性的输出才算变成确定性的成品。

最让我意外的是 scripts/check-mermaid.mjs。VitePress 构建时并不校验 mermaid 图表的语法,错了要到浏览器渲染时才暴露。这件事我没有让 AI 做,是它自己想到的:文档里有图、图会坏、坏了要在上线前发现 —— 于是有了这个脚本:jsdom 搭环境,调 mermaid.parse 把 docs 下所有 mermaid 代码块逐个校验,内部文件自动跳过。这种「想到你没让它想的事」,是完整性的另一个来源:人容易在「能用」的地方停下来;AI 不会累,也就没有「差不多得了」。

过程留痕,成品干净

仓库里有个 docs/issues/ 目录,编号归档了移植过程中发现的十九个缺陷:enjoy 的算术精度降级、for 循环迭代不了 map、内置指令缺失、db 缺方言……每一条都是留了案的复盘。这些记录通过 srcExclude 排除在发布站点之外 —— 对外的成品要干净,对内的过程要留痕。完整,不是把所有东西都端出去,而是该在的都在。

完整是长出来的

有了 AI 的帮助,让自己做事情更完整 —— 改变的到底是什么?不是 AI 会写文档了,文档它一直会写;是完整性的成本变了。以前文档、CI、校验、sitemap、孤儿分支,这些收尾活最劝退;现在它们的边际成本趋近于零,「能用」和「完整」之间的那段距离,走着走着就走完了。

你得知道「完整」长什么样—— 当然也不必一开始就知道。完整是在深入的过程中长出来的:每一步追问一句「还差什么」,追问多了,「完整」的认知自然成形。这两天下来,我对「完整」的理解,就比开工时具体得多。

最后是个自举:一个为 AI Coding 而生的框架,它自己的文档站,也是 AI Coding 做出来的。文档站的地址挂在那里,往后每一次 push,它都会自己生长。

人负责想要什么,AI 负责让它完整。

类比,是从已知通往未知的桥梁。

四年多前,我做过一款小程序「类比宝库」—— 一个用来收集类比的本子。

四年多后,我把它重做了一版,改名为「翻翻类比」。

为什么还做这件事

上帝说要有光,于是便有了光。笑来老师说,每个精彩的类比都是一笔财富,于是便有了这个小程序。

当年设计 Logo >≈< 时的想法:类比是约等于,不是等于号 —— 用得恰当,它就是大于号;用得不恰当,它就是小于号。

如果参加过笑来老师的写作课,应该知道第五课有个作业 —— 准备一个本子,从今天开始,遇到任何类比都收集起来。

最新的 AI 课程里,他又用了一个类比:Git 是平行时光机。你看,好类比永远管用。

「翻翻类比」,就是这个本子。

从「宝库」到「翻翻」

「类比宝库」走的是众人拾柴的路线 —— 大家一起攒。后来停更了,很现实的原因:微信的认证费和云服务开销,对一个个人小程序来说,扛不动。

这中间,我用墨问小程序更新、收集过一段时间的类比,但拿它收集和分享类比,总觉得隔了一层纱,不够直接。后来想明白了,我自己的需求其实很简单:收集类比,是为了有一天能用上 —— 随时翻到,翻得顺手,翻得舒服。

所以这次,思路被成本逼出来了:与其做一个「在线宝库」,不如先做一个「离线卡片集」。

  • 类比库随小程序一起发布、随版本更新。如今一共 185 句:其中一部分从当年历史数据库里迁出来的 —— 众人拾过的柴,还在这座炉子里烧着;剩下的,是我陆陆续续攒的。
  • 无网络、无登录、无云开发,不采任何信息。运维成本?题目直接删掉了。

翻起来什么样

指尖轻舞,流光溢彩。「翻翻类比」邀您步入一场全屏的思维漫游。左右滑动,在绚烂渐变中拾取智慧的隐喻;长按定格,将瞬间的顿悟化作海报珍藏。类比,是已知通往未知的桥梁 —— 一句妙语,洞见更广阔的世界。

一句话概括:全屏,左滑,无尽头。打开就是一句类比,居中衬在渐变色板上。左滑下一句,右滑上一句,卡片跟着手指回弹,翻页轻轻一震。

几点设计:

  • 20 套渐变色板:深蓝、米白、荧光绿……相邻两张不重样,翻起来像一叠老明信片。
  • 一屏一句:字号自适应,短句舒展,长句也容得下。
  • 长按生成海报:当前句子和配色渲染成一张卡片,右下角带小程序码,可存可分享。
  • 搜索:多关键词过滤,命中高亮,找到后直接进入卡片模式,不影响首页进度。

看不见的讲究:

  • 洗牌队列:不是随机抽一句,而是用 Fisher-Yates 把整个类比库洗成一轮 —— 一轮之内句句不重、句句都会出现,翻完自动重洗,衔接处还做了去重。
  • 记住进度:退出再进,从上次翻到的地方接着翻。

还是那个场景

等车时,排队时,想刷点什么的时候 —— 打开它。

现在刷的是短视频同款手势。刷十条短视频,笑完就忘了;翻十句类比,总有一句会留下来,变成你的财富。

一些说明,也是求助

  • 类比库随小程序发版更新。想分享的,直接发我微信(Crazy_Airhead),精选后收录。至于怎么让大家不经我手就能往里添柴 —— 还在想,有好点子随时喊我。
  • 小程序预留了广告位,目前没有开启;开了的话,多包涵。
  • 四年前,自己主要做后端开发,小程序开发是个新手。四年后有了 AI 的帮助,这个新手做出来的小程序,交互体验好了不少 —— 当然肯定还有不尽如人意的地方,请继续多多包涵,继续提建议。

微信里搜「翻翻类比」,翻翻看。

最早听笑来老师的课,知道他在写一款叫 VMark 的 Markdown 编辑器。当时看了看,觉得还有不少问题,就继续用着 MarkText。最近又看到笑来老师发的《我为什么制作了一个 Markdown 编辑器 VMark?》 ,正好自己这段时间一直在用 IDEA 的 cc-gui 插件配合 Claude Code 做 Vibe Coding。我发现,自己平时更多是在编辑 Markdown、在 AI 会话里聊天,可 IDEA 侧重的毕竟是源码编辑,一旦切到分栏或预览模式,渲染效果就很差,体验不好;而 cc-gui 虽然提供了多标签,方便多任务,却经常卡死,逼得我不得不重启 IDEA。于是,自己动手写一款 Markdown 编辑器的念头,就慢慢成形了。我已经写了自己的 SSH 客户端管理工具 vshell,那么再定制一款自己的 Markdown 编辑器,似乎也不是不可以。

前面说过,我平时最常用的 Markdown 编辑器是开源的 MarkText(虽然也买了 Typora)。又因为我是程序员,深知 Git 的重要性,而自己最常用的编程工具 IDEA,它的 Git 集成做得相当好。把这些凑到一起,就有了 GMark —— 一个 AI 驱动的创作编辑器。它的核心理念是:

人用 Markdown 下达指令(工作区),AI 生成内容(制品区),人审核确认,Git 全程记录。

简单说,GMark = Git + Markdown + AI,这也是它叫 GMark 的由来 —— 名字也参考了 VMark。

GMark 的另一个核心理念,是工作区与制品区的分离:用两个目录(两个 Git 仓库)。一个是工作区目录,里面是各种 Markdown 文件,还包含 AI 引擎相关的文件,比如 Claude Code 的 .claudeCLAUDE.md 等;另一个是制品区目录,用来放生成的代码、图片、文件等等。这样做最大的好处,是不用再纠结哪些文件该进 Git、哪些不该进 Git —— 比如苹果官方 App 误打 CLAUDE.md 上新闻那种事,就不会发生。

GMark 支持多个 AI 引擎,一个是 Claude Code,一个是 SolonCode。其实 AI 给我规划方案的时候,还顺带配了 Codex 的适配,但我想着自己完全没用过 Codex,适配反而给自己增加难度,就取消了。如果你听过笑来老师的课,他把「人审核」这一步也交给 AI 了 —— 这或许会是 GMark 下一步的方向。笑来老师用的是自己写的 cc-suite 插件,而我得想办法让两个 AI 引擎能彼此沟通起来。

GMark 和 vshell 用的是同一套技术栈:Go + Wails3 + Vue3 + Naive UI;Markdown 编辑器核心用了 MarkText 的编辑器 Muya,源码编辑器用的是 Monaco,其他一些辅助选型是 AI 帮我挑的。GMark 目前没有开源计划:一来它本身是个很个性化的需求;二来它还有不少问题,只适合自己用。更重要的是 —— 如果你有心,我也已经把技术栈交代清楚了,你应该会想自己造一个,而不是用我的版本。随着 AI 让软件创建越来越容易,定制化的需求也会越来越多。但 AI 的本质是基于概率的,存在「抽卡」式的不确定性,所以需要沉淀 —— 而 GMark,只是我沉淀出来的一个版本。

VMark 是一个高度固执己见(Highly Opinionated)的东西 —— 事实上,我猜,以后所有 Vibe Coded Software/Services 都是高度固执己见的…… 这其实是没办法的事儿,因为一切 Vibe Coding 的过程自然而然地都是「不需要与人开会」的「生产过程」—— 只有我自己和另外一个绝不争辩的执行者。

我只是一个 Producer(制作人)。

另外,还有个「高度固执己见」自动带来的后果:VMark 就算开源了,也不能指望「社群贡献」。首先,这完全是为了让自己顺手才写的东西,很多功能对别人来说并无太大价值。最为关键的是,Markdown 编辑器不是什么科技前沿的东西,是个无数人实现过无数次的编辑器中的一个简单分子,所以,AI 可以帮助我们解决关于它的任何问题。

本来计划用 GMark 来改进 GMark,吃自己的狗粮,让 GMark 实现自举、自我迭代。无奈问题还是太多,而且自举的时候需要重启自己,要么就得维护多个版本来回切换,徒增麻烦,于是放弃了自我迭代。

最近看到两样东西。一个是小木老师的 TokUI —— 号称全球首个「For AI & 零依赖」的流式 UI 描述与渲染框架:后端用极简 DSL 描述组件,经 SSE 或 WebSocket 流式推送,前端增量解析,首个 Token 就开始渲染,让 AI 用极少的 Token 输出更灵活、更有表现力的 UI。GMark 的 AI 会话记录,就是用 TokUI 渲染的,用下来感觉不错。另一个是 Martin Fowler 的一篇文章,《DSLs Enable Reliable Use of LLMs》。如果 DSL 确实更可靠,那 TokUI 应该是个正确的发展方向;再加上之前也看过「HTML 比 Markdown 更好」的说法,所以 —— 为什么不用 DSL/UI 来当练手项目呢,也能继续吃自己的狗粮,迭代 GMark。

于是在 cnb.cool 上建了两个仓库,一个工作区,一个制品区。目前两个仓库都是公开的,有兴趣的同学可以拿去参考。不过,如果你想拿 TurboUI 用于生产,请谨慎:一来我还在实验阶段,内容变动会比较大;二来它没有经过验证,使用有风险。真要上生产,我还是推荐你用 TokUI,能获得更多支持。

1
2
3
4
5
# 工作区仓库,设计
https://cnb.cool/goldsyear/onestep/TurboUI-Design

# 制品区仓库,作品
https://cnb.cool/goldsyear/onestep/TurboUI

TurboUI 第一个版本的需求,其实非常粗暴。我当时应该是在动车上,用手机的 DeepSeek 网页端写的。别问我为什么用 Turbo、Stimulus —— 这只是个人喜好,我喜欢 37signals(Basecamp),而 Hotwire 的技术也确实先进。就像笑来老师新 AI 课里讲的,不过是搜索空间的不同。

当然,新学的招得用上 —— 多模型互搏,于是我把需求也发给了 ChatGLM。

经过它们几轮「互殴」,我选了 TurboDSL/TurboUI 这套名字,但整体方案用了 GLM 的。最终形成的文档,在 TurboUI-Design 的 design.md 里。然后 AI 照着文档一顿设计:architecture.mdsdk-architecture.md,还生成了 sdk-spec —— 也就是 TurboDSL 的契约。接着它自己把剩下的事也包圆了,顺手给我整了个小 demo,我一看,像那么回事。

不过,笑来老师不是说要 Grill yourself 么?于是我在让它 Grill 拷问自己的时候,它给我生成了 why-dsl-grill.md

核心论点(贯穿全文):把「TurboUI 值得做」和「必须是一门方括号 DSL」拆开看。前者成立;后者的每一条理由都可用「受约束 HTML profile + server-resolve 桥 + 词表校验」等价达成,且保留 HTML 的语料红利。所以「为什么是方括号 DSL」这道题,design.md 目前答不及格。

它推荐用 HTML 的子集,而不是用方括号。后来它自己做了对比测试,只能说是不相上下,于是我就继续保留了方括号的形式,以保持和 TokUI 一致。

不过在逐步讨论的过程中,其实也补强了我的一些主张。比如,click 是一个 token,clk 看起来更短,却不一定是一个 token —— 人觉得更短,AI 分词的时候未必这么认为。不能一味用简写,要尽量用常见的单词,一方面能确定 token,另一方面能减少误解。

后面陆续让 AI 处理、生成 TODO,逐步核查和修改。碰到最多的问题,其实笑来老师的课里也讲过 —— 就是只处理一半问题,或者在处理过程中说「这个是旧问题,不是这次改动引入的」。没办法,还是没办法,得好好看看笑来老师提供的 Skills 了。

实现的过程中,还得顺手修 GMark 的各种问题:工作区和制品区的限制问题、AI 提供的审核问题、Git 扫描 node_modules 导致 CPU 飙升卡顿、文件引擎的问题、AI 引擎切换时参数没完整切换导致接口异常、发布后路径无法识别、Claude Code 被判离线、Terminal 没识别到环境变量,等等。光是自己在使用时就登记下来的问题,就有 80 多个。总而言之,这狗粮,是真难吃。

不过看着 TurboUI 相对干净的工作区和制品区,感觉还是挺好的。

最后,我让 AI 看 TokUI 的演示站点,给我重新生成一个更完整的 Demo,效果也还不错 —— 它直观地展示了 DSL 相比 HTML 节省的 Token 数,效果满意。

1
2
3
# 获取代码后执行
pnmp install
pnpm demo

Aifei-Go:为 AI Coding 而生的 Go 服务端框架

Aifei-Go 是 Aifei(Java 版)的 Go 语言移植版。它继承了 Aifei 的核心设计理念 —— Just Service 扁平架构、HIO 自主 IO 模型、Enjoy 自研模板引擎、Db + Row 数据库模式,并在 Go 的语言特性与生态上重新落地:用接口与 Handler 包装链替代动态代理,用 net/http 替代 Undertow,用 Radix 树路由替代注解扫描,同时又参考了 Solon,引入 Dami 提供进程内事件总线,引入 Nami 作为 HTTP RPC 客户端框架,用于支撑微服务。

注意事项:Aifei-Go 由 GLM5.2 生成,不喜勿入。Aifei 是基准,AI 翻译。人做决策,AI 生成。

Java 版文档:https://aifei.cn/doc

1. 什么是 Aifei-Go

Aifei 原本是”一款用于 AI Coding 的 Java 服务端框架”,其核心设计目标是 极小化 Token 消耗、极大化 Attention 浓度,让 AI 稳定生成高品质代码。要做到这一点,最有效的手段就是消除冗余分层与样板代码 —— 这也是 Aifei 开创 Just Service 范式 的根本动因。

近十年间,前后端分离成为主流,页面路由、交互编排与渲染职责大量转移到前端(react-router、vue-router 已接管了过去由服务端 Controller 承担的职责)。路由既然在前端,服务端就不再需要 Controller 这一层。Just Service 范式之下,开发者无需编写 Controller、Render、Repository、Mapper 这类冗余代码,直接写业务即可。

Aifei-Go 把这套理念原汁原味地带到了 Go 语言:

  • 只写 Service,不写 Controller —— 方法名即路由,按命名约定自动映射为 RESTful 端点。
  • 核心零外部依赖 —— 核心库与独立框架(aifei / enjoy / db / json / log / nami / dami)仅用 Go 标准库;插件按需引入第三方库。
  • 模块化、按需组合 —— 每个模块可独立 go get,不拉入多余依赖。
  • AI 友好 —— 代码量少、结构扁平、约定明确,AI 生成时上下文负担小、命中率高。

1.1 为什么从 Java 移植到 Go

Aifei Java 版内核仅 3333 行(内核核心仅 260 行),零第三方依赖,已经是极致精简。移植到 Go 带来三点额外收益:

维度 Java Aifei Go Aifei-Go
部署形态 JVM + 打包 单一静态二进制,毫秒级启动
并发模型 线程池 + 同步阻塞 goroutine 天然并发
依赖管理 Maven 多模块 Go workspace 多模块,按需 import
动态能力 CGLIB/Javassist 动态代理 接口 + Handler 包装链(无运行时反射代理)
HTTP 服务器 Undertow(去 Servlet) net/http(标准库,零依赖)
路由 注解 @Path + 包扫描 Radix 树 + 代码注册(编译期确定)

Java Aifei 中被废弃的三个模块(aifei-proxyaifei-undertowaifei-all)在 Go 里不再需要:Go 没有动态代理机制,AOP 由 Handler 包装链 + Interceptor 接口实现;Go 有标准库 net/http;Go 的 import 机制天然按需引入。保留下来的,是 Aifei 真正有价值的内核:HIO、Just Service、Enjoy、Db + Row。

2. 核心理念:Just Service

Just Service 的本质是:方法名即路由。一个 Go struct 的导出方法,按命名约定自动映射成一条 HTTP 路由,无需任何注解、配置文件或注册代码(注册由生成器在 init() 中自动完成)。

1
2
3
4
5
6
7
8
type UserService struct{}

func (s *UserService) List(in aifei.Input) aifei.Output { /* GET /api/user/list */ }
func (s *UserService) Paginate(in aifei.Input) aifei.Output { /* GET /api/user */ }
func (s *UserService) Create(in aifei.Input) aifei.Output { /* POST /api/user */ }
func (s *UserService) GetById(in aifei.Input) aifei.Output { /* GET /api/user/:id */ }
func (s *UserService) UpdateById(in aifei.Input) aifei.Output{ /* PUT /api/user/:id */ }
func (s *UserService) DeleteById(in aifei.Input) aifei.Output{ /* DELETE /api/user/:id */ }

server.Register() 通过两条规则把方法名翻译成 HTTP 方法 + URL:

  1. 默认动作(精确匹配) —— 直接挂在 service 前缀上:PaginateGET /prefixCreatePOST /prefixListGET /prefix/list
  2. 动词前缀 —— 方法名以(且长于)Get/Post/Put/Delete/Update 开头,动词决定 HTTP 方法,剩余部分 camelCase→kebab-case 作为路径后缀。例如 GetProfileGET /prefix/profile

两个特殊约定:

  • ById 后缀自动转为 :id 路径参数:GetByIdGET /prefix/:id
  • 既非默认动作、也不符合动词前缀的方法不会被路由 —— 天然成为私有 helper,无需额外的可见性控制。

对比 Java 版的 @Path 注解 + 包扫描:Go 没有注解,Aifei-Go 用「命名约定 + 代码注册」达成同样的效果,且路由在编译期就已确定,没有运行时反射扫描开销。

3. HIO 架构:Input / Output / Handler

Java Aifei 采用 HIO 自主架构(Handler + Input + Output),让用户自主掌控处理流程与数据结构。Aifei-Go 完整保留了这一设计,并用 Go 接口重新表达:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 处理流程的单元:Input 进、Output 出
type HandlerFunc func(in Input) Output

// Input = Param(读参数)+ Meta(请求元信息)
type Input interface {
Param // Has / GetStr / GetInt / GetBean / GetMap / PathPara ...
Meta // Context / Header / Path / Body
}

// Output 由业务构建,IoHandler 决定如何渲染
type Output interface {
Code() int
Msg() string
Data() interface{}
}

几个关键点:

  • Input 与 HTTP 解耦Input 只承载「任何调用源都能满足」的契约——参数读取与请求元信息。HTTP 专属的概念(method 动词、remote 地址、cookie)不在这个接口上,它们留在 HTTP 适配层。这意味着同一个 Service 方法既可被 HTTP 请求驱动,也可被测试桩、内部调用驱动,业务代码不绑死 HTTP。

  • Output 是意图,不是渲染。业务代码构建一个 Outserver.Ok() / server.Of(data) / server.Fail(msg)),它累积 code/msg/data 以及渲染意图(JSON、Enjoy HTML 视图、文件下载、原始字节、重定向)。IoHandler 读这些意图来决定 如何 写响应 —— 业务代码从不直接碰 net/http,文件下载、响应头都通过闭包/构建器表达。

  • 两条包装链。Aifei-Go 区分两类横切逻辑:Handler 级Input → Output,作用于业务处理,如 LoggerRecoverTxInterceptor)与 HTTP 级(作用于 http.Handler,如 CORSBasicAuthRequestID)。前者对应 Java Aifei 的全局拦截器,后者处理纯传输层关切。

Java 用 CGLIB/Javassist 动态代理实现 AOP,Go 没有这套机制 —— Aifei-Go 用 Handler 包装链 + Interceptor 接口 替代:ChainHandlers() 把一组 Handler 包装器组合成一条链,Interceptor 提供方法级 AOP(@Before / @Clear 的等价物)。

4. 模块结构

Aifei-Go 是一个 Go workspace 多模块项目,按角色分层。每个模块独立版本化、可独立 go get,互不拉入多余依赖:

模块 职责 依赖
核心框架 aifei-go/aifei Input/Output、Router、Handler wrapper、Interceptor
核心库 aifei-go/enjoy 模板/SQL 引擎(自研)
核心库 aifei-go/db 数据库访问(Row/Dao/Dialect/Enjoy SQL) enjoy
核心库 aifei-go/json JSON 工具
核心库 aifei-go/log 日志接口
核心库 aifei-go/config 分层配置(yml + 环境变量 + 命令行 + 云配置) yaml.v3
运行时 aifei-go/http net/http 适配器 aifei
运行时 aifei-go/server 启动引导、内置 Handler、OutRegister aifei, http, db, enjoy, log
独立框架 aifei-go/nami HTTP RPC 客户端框架
独立框架 aifei-go/dami 进程内事件总线(send/call/stream/lpc)
代码生成 aifei-go/tools/generator Schema → 类型安全 CRUD 代码 db, enjoy
代码生成 aifei-go/tools/damigen dami 相关代码生成 enjoy
插件 plugins/cache 两级缓存(本地 + Redis) jetcache-go, go-redis
插件 plugins/kafka Kafka 生产/消费 franz-go
插件 plugins/nacos 服务注册、配置中心、发现 nacos-sdk-go
插件 plugins/storage 文件存储(本地 + S3 兼容) minio-go
插件 plugins/swagger OpenAPI 文档(knife4j-vue3) swaggo/swag
插件 plugins/dataisolate 租户 + 行/列数据隔离 GoSQLX

「核心库」层的模块(enjoy / db / json / log)刻意保持零外部依赖、可脱离框架独立使用——你完全可以只用 enjoy 做模板渲染,或只用 db 做数据库访问,而不引入整个 web 框架。插件层则把第三方库的集成集中隔离,不污染核心。

5. 快速开始

1
go get github.com/crazy-airhead/aifei-go/aifei

一个最小的 HTTP 服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
"github.com/crazy-airhead/aifei-go/aifei"
"github.com/crazy-airhead/aifei-go/server"
)

func main() {
app := aifei.New()

// 全局 Handler 包装链
app.Use(server.Logger(), server.Recover())

// HandlerFunc: func(in aifei.Input) aifei.Output
app.GET("/", func(in aifei.Input) aifei.Output {
return server.Of("Hello, Aifei!")
})

app.GET("/hello/:name", func(in aifei.Input) aifei.Output {
return server.Ok("Hello, " + in.GetStr("name"))
})

// 启动(支持 CORS、BasicAuth 等 HTTP 级包装器)
server.Run(app, ":8080", server.WithCORS("*"))
}

而一个真实的多 Service 应用,骨架甚至更短——所有表对应的 Service 通过生成器在各自的 init() 里自注册,主程序只需一行 server.AutoRegisterServices(app)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
db.Init("sqlite", "./demo.db")
// ...建表...

app := aifei.New()
app.Use(server.Logger(), server.Recover())

// 每个 per-table 包在 init() 中注册自己的 Table 元数据与 Service 路由
_ "github.com/crazy-airhead/aifei-go/_test/demo/internal/user"
_ "github.com/crazy-airhead/aifei-go/_test/demo/internal/loginlog"

server.AutoRegisterServices(app) // 一行注册全部 Service
server.Run(app, ":8081", server.WithCORS("*"))
}

完整可运行示例见 _test/demogo run ./_test/demo)。

6. 核心特性

6.1 Enjoy 模板引擎

Enjoy 是 Aifei 的招牌特性——一套自研的模板语言(~2800 行),自带词法分析器(DKFF 算法)、递归下降语法分析器(DLRD)和完整的表达式引擎。它不仅是页面渲染引擎,也是 SQL 模板引擎(见 6.3)。

1
2
3
engine := enjoy.NewEngine("myEngine")
tpl := engine.GetTemplateByString("Hello, #(name)! Age: #(age)")
out := tpl.RenderToString(map[string]interface{}{"name": "james", "age": 18})

支持的语法:#() 表达式输出、#if/#else/#elseif#for#set/#setLocal/#setGlobal#define/#call#include#switch/#case/#default#break/#continue/#return;表达式层支持算术/比较/逻辑/三元、空安全(???.)、方法调用、map/数组字面量、静态访问(::)。

6.2 Db + Row + Dao

数据库访问沿用 Java Aifei 的 Db + Row 模式(与 JFinal 的 Db + Record 几乎一致),核心是链式 API 与 Active Record 变更追踪。db 模块本身不含驱动,用户自行引入所需驱动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import (
"github.com/crazy-airhead/aifei-go/db"
_ "modernc.org/sqlite" // 或 _ "github.com/go-sql-driver/mysql"
)

func main() {
db.Init("sqlite", "./app.db")

// Active Record —— 插入
row := db.NewRow("user").Set("name", "james").Set("age", 18)
result, _ := db.Insert(row)

// 主键查询
found, _ := db.FindByID("user", result.GetID())

// 分页查询
page, _ := db.RawSql("SELECT * FROM user ORDER BY id DESC").Paginate(1, 10)
_ = found
_ = page
}

内置 MySQL / PostgreSQL / SQLite 三种方言,支持事务、批量操作、类型转换。Row.Set() 追踪变更(用于 UPDATE),Put() 不追踪——精确控制更新范围。

6.3 Enjoy SQL:模板化的动态 SQL

db.Sql(...) 接受 Enjoy 模板作为 SQL,提供 #where / #and / #orderBy / #para 等指令,支持 18 种操作符,条件为空时自动省略——这是处理「动态查询条件」最优雅的方式,告别手写字符串拼接:

1
2
3
4
5
data := map[string]interface{}{"minAge": 18, "status": 1}
list, _ := db.Sql(
"SELECT * FROM user #where() #and(age > #para(minAge)) #and(status = #para(status))",
data,
).Find()

#orderBy 指令接收一个字段白名单,实际排序字段从入参读取并校验——防 SQL 注入的同时支持多字段排序与字段名映射。

6.4 代码生成器

Aifei 没有 Model 概念——Model 的支持完全由生成器实现。tools/generator 扫描数据库 Schema,每张表生成一个独立包base.go / model.go / dao.go / service.go),提供编译期类型安全的 CRUD API:

1
2
3
4
5
6
gen := generator.New(pool, dialect, "./myapp/db", "myapp/db")
gen.Generate() // 一次扫描所有表:user/、loginlog/ …

// 使用生成的类型安全 API
u, _ := user.FindById(123)
u.SetName("new name").Update()

每表一包的策略让每张表的字段都有具名 getter/setter,AI 生成业务代码时不必猜测字段名拼法,命中率显著提升。

6.5 分层配置

config 模块提供分层加载(L1–L5):app.yml + app-{env}.yml → 扩展配置 → 环境变量 + 命令行参数 → 编程式 LoadInto() → 云配置(如 Nacos)。线程安全,支持运行时热更新。提供 Get/GetStr/GetBool/GetInt 访问器、Sub(prefix) 作用域切片、Bind(v) YAML 往返到自定义结构体。所有插件统一从 config.Props 读取自身配置(storage.*cache.*kafka.* …),约定一致。

6.6 插件生态

插件实现 aifei.PluginStart()/Stop() 生命周期),从 config.Props 读自身配置并装配一个包级默认实例,让顶层便捷函数开箱即用:

  • plugins/cache —— 两级缓存(本地 FreeCache/TinyLFU + Redis),GetOrStore 自带 singleflight 与缓存穿透防护,实例级 key 前缀隔离。
  • plugins/storage —— 统一本地文件系统与 S3 兼容后端(AWS S3 / Minio / OSS / COS),按 bucket 路由。
  • plugins/kafka —— 基于 franz-go 的生产/消费,多集群,Subscribe 至少一次投递(失败记录不提交、下次重投)。
  • plugins/nacos —— 服务注册与发现、配置中心,自动桥接到 nami RPC 客户端;init() 自动注册云配置加载器。
  • plugins/swagger —— 内嵌 knife4j-vue3 UI 的 OpenAPI 文档插件。
  • plugins/dataisolate —— 租户 + 行 + 列三正交维度的数据隔离,AST 改写 SQL,应用代码零隔离感知(详见 data-isolate-intro.md)。

6.7 独立框架:Nami 与 Dami

两个不依赖 aifei 的兄弟框架,可独立使用:

  • nami —— 轻量 HTTP RPC 客户端框架(移植自 Java Solon Nami)。Channel 传输(channel/http)、编解码(coder/json)、Filter 链、Upstream/Discovery 服务发现、流式 Builder/ClientFactory。它是 aifei 服务端的对偶:aifei 暴露服务,nami 消费服务。
  • dami —— 进程内事件总线(send/call/stream/lpc),发布订阅 + 同步调用 + 流式返回,用于解耦模块间通信。

7. Java Aifei → Go Aifei-Go 设计决策

移植并非逐行翻译,而是在保持 API 风格一致的前提下,用 Go 惯用模式替代 Java 机制:

决策点 Java Aifei Go Aifei-Go 理由
请求上下文 Input + Output 接口 Input / Output 接口(保持分离) 与 Java API 一致,职责清晰
AOP / 拦截 CGLIB/Javassist 动态代理 + Interceptor Handler 包装链 + Interceptor 接口 Go 无运行时动态代理
路由注册 @Path 注解 + 反射包扫描 命名约定 + Register() 代码注册 Go 无注解,编译期确定
泛型 Java 泛型 Handler<I, O> Go 接口 简化接口
配置 AifeiConfig 接口 + 多个 config() Functional Options 模式 Go 惯用配置模式
HTTP 服务器 Undertow(去 Servlet) net/http(http 适配 + server 启动) 标准库,零依赖
包扫描 ClassLoader + JAR 扫描 不需要(Go 静态编译) 编译时确定所有代码
依赖注入 @Inject + 反射 构造函数注入 Go 惯例
路由匹配 HashMap + ActionGroup Radix 树 高性能路由标准做法
错误处理 throws Throwable error 返回值 + panic/recover Go 惯例
聚合模块 aifei-all import 按需引入 不再需要

四条贯穿全局的重写原则:保持 API 风格一致Go 惯用优先核心最小依赖AI 友好

8. 继承与差异

继承自 Java Aifei 的(最有价值的设计):Just Service 范式、HIO 自主架构、Enjoy 模板引擎、Db + Row 模式与链式 API、sql(sql, para).find() 式查询入口、生成器生成的 Model、集中式配置中心思想、拦截器(@Before/@Clear 的等价物)。

Go 版新增/调整的

  • 路由约定化 —— 用动词前缀 + 默认动作的命名约定替代 @Path,方法名直接表达 HTTP 语义。
  • 代码生成深化 —— 每表一包 + typed Dao,编译期类型安全;Service 在 init() 中自注册,主程序零路由样板。
  • 插件生态扩展 —— 在 Java 插件机制之上,新增 cache / kafka / storage / swagger / dataisolate 等开箱即用插件,全部配置驱动。
  • 兄弟框架 —— nami(RPC 客户端)与 dami(事件总线)作为独立模块,与 aifei 服务端组合成完整微服务工具链。
  • 数据隔离 —— 通过 plugins/dataisolate 提供声明式、AST 改写式的租户/行/列隔离,这是 Java 版未单独成插件的能力。

9. 小结

Aifei-Go 的定位很明确:把 Aifei「为 AI Coding 而生」的设计哲学,用 Go 的方式重新实现一遍

它不追求大而全。核心框架零外部依赖,只依赖标准库;它不引入 Controller/Service/DAO 的冗余分层,一个 struct 方法就是一条路由;它把模板引擎、SQL 模板、ORM、代码生成、配置、缓存、消息、存储、数据隔离做成正交的模块,按需取用。

如果你用过 JFinal 或 Java Aifei,你会对 Db + Row、Enjoy SQL 指令、集中式配置感到熟悉;如果你是 Go 开发者,你会发现它的接口设计、错误处理、并发模型都是地道的 Go 风格。而无论哪种背景,Just Service 让你把注意力放在业务上 —— 这正是 Aifei 一开始就想做到的事。

延伸阅读

今天早上,系统又提示磁盘空间不足。我查看了一下操作系统的存储管理,发现可用空间已不足 5GB,于是打开 Tencent Lemon Lite 做了一次深度扫描。

首先发现 QQ、微信、企业微信、钉钉、Mixin 等聊天软件占用了大量数据。这些软件自带了存储管理功能,把图片、视频和文件清理之后,一下子腾出了约 20G 空间。

接着,我注意到 Ollama 的模型也占用了不少空间。考虑到最近主要使用云服务,基本没用本地模型,便把 Ollama 和 deepseek 和 qwen 的几个模型一起删除掉,又释放了约 15G。

在存储管理中,看到 SystemData 占用了大约 200GB,因为有上一次清理硬盘的经历,我直接去查看 ~/Library/Application Support~/Library/Caches 目录。这些数据大多是应用运行时产生的,虽然可以删除,也不影响应用的使用。话虽这么说,真要删的时候还是会犹豫,不知道该删哪些好。

清理完这些数据后。系统又多了 100 G 的空余空间,又能撑上一阵子。

最近看 Anthropic 的产品经理说的,如果一件事情重复三次就要考虑自动化。那么为什么不定制个磁盘清理工具,给 AI 提了清理 System Data、清理软件删除残留、清理聊天工具缓存的需求。

系统的整体硬盘情况

img

开发工具产出的缓存数据

img

清理应用残留

img

清理聊天工具缓存

img

开发工具缓存扫描和清理就是一个非常个性化的需求。随着 AI 让软件创建变得越来越容易,定制化需求也会越来越多。但 AI 的本质是基于概率的,存在“抽卡”式的不确定性,所以软件可能会日更,却不可能日抛。软件需要不断积累,需要不停的维护。

说明

2026-06-06 Aifei 框架正式发布了。波总在五一的时候已经放出了 aifei 的正式版和 vip的订阅,作为第一批用户,用上了 aifei-vip-arch 和 aifei-vip,趁着五一学习了一波,顺便让还让AI 生成了Aifei-dev 的 skills,感兴趣的同学可以试一试,https://gitee.com/CrazyAirhead/aifei-dev。当然为了验证这个 skills 我也是做了个示例的,简单的截两张图,看看效果:

img

img

因为 Aifei 刚发布,还有很多生态没有起来,而我日常使用的都是 Solon,而且 Solon 生态已经比较完善,又因为非常喜欢 Aifei,于是先把 aifei-enjoy 和 aifei-db 集成到 Solon 生态里面。Aifei 的完全使用就边用边学吧。

集成 aifei-enjoy

由我贡献的 solon-view-aifei-enjoy 插件已经被合并到 Solon 主版本,将会在Solon 的 4.x 版本发布,https://solon.noear.org/article/1456。

因为 Solon 原来就集成了 enjoy,enjoy 又是波总比较新的作品,此次 aifei-enjoy,主要是调整包名,没有太大的变动。所以集成 aifei-enjoy,主要是在 solon-view-enjoy 插件的基础上修复包名。

有一点需要注意的就是enjoy 不支持 Directive 工厂,Solon 的作者自己做一个 enjoy 的扩展,也就是 solon-view-enjoy 不是使用原版的 enjoy 的 jar 包。在 aifei-enjoy 中,提供了原生的支持,所以可以优先使用 Solon 的容器对象。

1
2
3
4
5
6
7
8
9
10
Engine.setDirectiveFactory(
(cls) -> {
Directive directive = Objects.requireNonNull(Solon.context()).getBean(cls);
if (directive != null) {
return directive;
}

// 兜底
return ClassUtil.newInstance(cls);
});

集成 aifei-db

aifei-db-solon-plugin 的插件,我还在开发中,主体功能是基于 activerecord-solon-plugin 改造的,因为 aifei-db 相对于 activerecord 有较大的变动,我要多验证下,暂时还没有签入和提交 PR。

img

相对于 ActiveRecord,aifei-db 中的 AifeiRow 做了比较多的增强,因此注册AifeiRow被简化。

1
2
3
4
5
6
7
8
9
10
11
/**
* @author airhead
*/
public class ModelManager {
@Getter static Set<Class<? extends AifeiRow<?>>> modelSet = new LinkedHashSet<>();

@SuppressWarnings("unchecked")
public static void addModel(Table table, AifeiRow<?> model) {
modelSet.add((Class<? extends AifeiRow<?>>) model.getClass());
}
}

另外需要注意的是 aifei-core 不包含 Slf4jLogFactory,因此需要自己扩展一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package top.godenyear.ray.framework.aifei.db;

import cn.aifei.log.Log;
import cn.aifei.log.LogFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Aifei LogFactory 的 Slf4j 桥接实现。 解决 aifei-core 不含 Slf4jLogFactory 的问题。
*
* @author airhead
*/
public class Slf4jLogFactory implements LogFactory {

@Override
public Log getLog() {
return getLog(LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME));
}

@Override
public Log getLog(Class<?> clz) {
return getLog(LoggerFactory.getLogger(clz));
}

@Override
public Log getLog(String name) {
return getLog(LoggerFactory.getLogger(name));
}

private Log getLog(Logger slf4jLogger) {
return new Log() {
@Override
public String getName() {
return slf4jLogger.getName();
}

@Override
public boolean isTraceEnabled() {
return slf4jLogger.isTraceEnabled();
}

@Override
public void trace(String msg, Throwable t) {
slf4jLogger.trace(msg, t);
}

@Override
public void trace(String msg) {
slf4jLogger.trace(msg);
}

@Override
public void trace(String format, Object arg) {
slf4jLogger.trace(format, arg);
}

@Override
public void trace(String format, Object arg1, Object arg2) {
slf4jLogger.trace(format, arg1, arg2);
}

@Override
public void trace(String format, Object... args) {
slf4jLogger.trace(format, args);
}

@Override
public void trace(java.util.function.Supplier<String> supplier) {
if (isTraceEnabled()) {
slf4jLogger.trace(supplier.get());
}
}

@Override
public boolean isDebugEnabled() {
return slf4jLogger.isDebugEnabled();
}

@Override
public void debug(String msg, Throwable t) {
slf4jLogger.debug(msg, t);
}

@Override
public void debug(String msg) {
slf4jLogger.debug(msg);
}

@Override
public void debug(String format, Object arg) {
slf4jLogger.debug(format, arg);
}

@Override
public void debug(String format, Object arg1, Object arg2) {
slf4jLogger.debug(format, arg1, arg2);
}

@Override
public void debug(String format, Object... args) {
slf4jLogger.debug(format, args);
}

@Override
public void debug(java.util.function.Supplier<String> supplier) {
if (isDebugEnabled()) slf4jLogger.debug(supplier.get());
}

@Override
public boolean isInfoEnabled() {
return slf4jLogger.isInfoEnabled();
}

@Override
public void info(String msg, Throwable t) {
slf4jLogger.info(msg, t);
}

@Override
public void info(String msg) {
slf4jLogger.info(msg);
}

@Override
public void info(String format, Object arg) {
slf4jLogger.info(format, arg);
}

@Override
public void info(String format, Object arg1, Object arg2) {
slf4jLogger.info(format, arg1, arg2);
}

@Override
public void info(String format, Object... args) {
slf4jLogger.info(format, args);
}

@Override
public void info(java.util.function.Supplier<String> supplier) {
if (isInfoEnabled()) slf4jLogger.info(supplier.get());
}

@Override
public boolean isWarnEnabled() {
return slf4jLogger.isWarnEnabled();
}

@Override
public void warn(String msg, Throwable t) {
slf4jLogger.warn(msg, t);
}

@Override
public void warn(String msg) {
slf4jLogger.warn(msg);
}

@Override
public void warn(String format, Object arg) {
slf4jLogger.warn(format, arg);
}

@Override
public void warn(String format, Object arg1, Object arg2) {
slf4jLogger.warn(format, arg1, arg2);
}

@Override
public void warn(String format, Object... args) {
slf4jLogger.warn(format, args);
}

@Override
public void warn(java.util.function.Supplier<String> supplier) {
if (isWarnEnabled()) {
slf4jLogger.warn(supplier.get());
}
}

@Override
public boolean isErrorEnabled() {
return slf4jLogger.isErrorEnabled();
}

@Override
public void error(String msg, Throwable t) {
slf4jLogger.error(msg, t);
}

@Override
public void error(String msg) {
slf4jLogger.error(msg);
}

@Override
public void error(String format, Object arg) {
slf4jLogger.error(format, arg);
}

@Override
public void error(String format, Object arg1, Object arg2) {
slf4jLogger.error(format, arg1, arg2);
}

@Override
public void error(String format, Object... args) {
slf4jLogger.error(format, args);
}

@Override
public void error(java.util.function.Supplier<String> supplier) {
if (isErrorEnabled()) {
slf4jLogger.error(supplier.get());
}
}
};
}
}

处理序列化问题

不管是 ActiveRecord 中 Model,还是Aifei 中的 AifeiRow,AifeiModel,他的底层数据结构是 Map,通过Bean 方法包装后进行 Bean 的操作,而 AifeiModel 更进一步使用了链式的 setter。

在 Solon 文档 https://solon.noear.org/article/18 中说到,Model 转 Json 输入的方法,不过在我的使用过程中体验不是很好。

1
2
3
4
5
6
7
8
9
10
11
//应用加载完成事件
@Componentpublic
class AppPluginLoadEndEventListener implements EventListener<AppPluginLoadEndEvent> {
@Overridepublic void onEvent(AppPluginLoadEndEvent e) throws Throwable {
//定制 json 序列化输出(使用新的处理接管 "@json" 指令)
e.app().renders().register("@json", (data, ctx) -> {
String json = JFinalJson.getJson().toJson(data);
ctx.outputAsJson(json);
});
}
}

这次在进一步整合 Aifei-db 的过程中,算是找到了一些比较好的集成方式,可以参看Solon 的 issue,重点涉及 EntityConverter 和 PropsConverterExt。

https://gitee.com/opensolon/solon/issues/IJSD9V https://gitee.com/opensolon/solon/issues/IJSCEX

主体的思路是通过完整的替换序列化插件,从而减少扩展链路和调整的地方。

Snack4StringSerializer

mime 为空时也使用JSON序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 是否匹配
*
* @param ctx 请求上下文
* @param mime 内容类型
*/
@Override
public boolean matched(Context ctx, String mime) {
if (mime == null || mime.isEmpty()) {
return true;
} else {
return mime.contains(label) || mime.startsWith(MimeType.APPLICATION_X_NDJSON_VALUE);
}
}

Snack4EntityConverter

changeEntityDo 时使用JSON 进行转换,而不是交给 ClassUtil 处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Snack 实体转换器
*
* @author noear
* @since 3.6
*/
public class Snack4EntityConverter extends AbstractStringEntityConverter<Snack4StringSerializer> {
@Override
protected Object changeEntityDo(Context ctx, ParamWrap p, String name, Class<?> type)
throws Exception {
Props props = new Props().addAll(ctx.paramMap());
Options options = serializer.getDeserializeConfig().getOptions();
return ONode.ofBean(props, options).toBean(type);
}
}

针对 AifeiRow 提供相应的 encoder

ModelEncoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* @author airhead
*/
public class ModelEncoder<T extends AifeiRow<?>> implements ObjectPatternEncoder<T> {
@Override
public ONode encode(EncodeContext ctx, T value, ONode node) {
if (value == null) {
return node;
}

return encode(node, value.data());
}

@Override
public boolean canEncode(Object value) {
return value instanceof AifeiRow;
}

protected ONode encode(ONode node, Map<String, Object> map) {
if (CollUtil.isEmpty(map)) {
return node;
}

node.fill(map);

return node;
}
}

配置 Snack4StringSerializer

为AifeiRow 指定 ModelEncoder

1
2
3
4
5
6
7
8
context.getBeanAsync(
Snack4StringSerializer.class,
serializer -> {
serializer.getDeserializeConfig().addFeatures(Feature.Decode_AllowUseSetter);
serializer.getSerializeConfig().addFeatures(Feature.Write_UseSmlCamelStyle);

serializer.addEncoder(AifeiRow.class, new ModelEncoder<>());
});

小结

经过以上的处理,现在应该能在 Solon 里面愉快的玩耍 aifei-enjoy 和 aifei-db了,enjoy 模版,SQL 模板,原来想要的都回来了。虽然 Solon 比 Aifei 的概念会多一些,但整体还是非常克制的,因此即便多了一些 Controller的逻辑,对于AI 的消耗也不会增加太多。

补充

关于 aifei 的一些尝试,可以仓库代码 https://github.com/crazy-airhead/aifei

  1. 扩展 aifei-json-snack4,替代fastjson2,使用 snack4 进行处理 JSON。
  2. 扩展 aifei-feathttp, 替代undertow,使用 feathttp(原来的smarthttp)作为底层服务器。

过去遇到不顺手的软件,我们要么将就,要么苦等别人更新。哪怕自己是程序员,也很少动念头给自己写一个 —— 毕竟打通设计、交互和编码全链路实在太耗时耗力了(编程大佬除外)。如今,AI 颠覆了这一切,代码生成速度快得惊人:过去程序员要写上几天甚至一个月的代码量,现在一天甚至几小时就能搞定。于是,那些曾经“能忍则忍”的将就,都变成了必须满足的需求,因为技术让定制变得“理所应当”且“触手可及”。软件定制化的时代开启了。

阅读全文 »

之前看到京东微前端框架 MicroApp 的时候,就觉得这个东西挺有意思的,可以和后端的微服务,或者插件系统对接起来用。

MicroApp 官网是这么介绍微前端的(https://jd-opensource.github.io/micro-app/docs.html#/)

微前端的概念是由 ThoughtWorks 在2016年提出的,它借鉴了微服务的架构理念,核心在于将一个庞大的前端应用拆分成多个独立灵活的小型应用,每个应用都可以独立开发、独立运行、独立部署,再将这些小型应用融合为一个完整的应用,或者将原本运行已久、没有关联的几个应用融合为一个应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
它主要解决了两个问题:
1、随着项目迭代应用越来越庞大,难以维护。
2、跨团队或跨部门协作开发项目导致效率低下的问题。

虽然也尝试过几次,但效果都不理想,毕竟自己不会前端,很多布局类的东西就弄不出来,于是就没再搞了。这次让 AI 重新设计的时候,他是这么回答的,我感觉有戏了。

![image-20260404221920910](/Users/airhead/Library/Application Support/typora-user-images/image-20260404221920910.png)

于是让他不停的改,在耗光一周的 Token 限额的时候,总算有想要的效果了。主应用和子应用都是基于 Soybean Admin 进行修改的。

  • 主应用

![image-20260404222447210](/Users/airhead/Library/Application Support/typora-user-images/image-20260404222447210.png)

  • 子应用模板

![image-20260404222544000](/Users/airhead/Library/Application Support/typora-user-images/image-20260404222544000.png)

  • 系统管理

![image-20260404222624817](/Users/airhead/Library/Application Support/typora-user-images/image-20260404222624817.png)