i-have-adhd TypeScript扩展架构深度解析:从loadRules到syncContext的完整调用链
【免费下载链接】i-have-adhdA skill to stop your coding agent from burying the answer. ADHD-friendly output.项目地址: https://gitcode.com/GitHub_Trending/ih/i-have-adhd
i-have-adhd 是一个让 AI 编码助手输出 ADHD 友好内容的技能插件:答案先行、步骤编号、零客套。本文以它面向 Pi / OMP 的 TypeScript 扩展为例,完整拆解从loadRules读取规则文件、到syncContext把规则注入对话的调用链,带你弄清这个扩展如何在整个会话期间让规则"常驻"在 Agent 上下文中。
1️⃣ i-have-adhd 是什么:一个 ADHD 友好输出技能
普通编码 Agent 回答"认证怎么修",可能先铺垫三句再给答案。i-have-adhd 做的事情很简单:别让 Agent 把答案埋在话里。
它的规则集共 10 条,全部定义在 skills/i-have-adhd/SKILL.md 中:
| 规则 | 一句话说明 |
|---|---|
| 1. 答案先行 | 第一行就是能执行的命令或路径 |
| 2. 步骤编号 | 多步任务用编号列表,一步一个动作 |
| 3. 以单一动作收尾 | 结尾只留一个两分钟内能做的下一步 |
| 4. 抑制跑题 | 当前问题没解决完,不谈第二个问题 |
| 5. 每轮复述状态 | "第 3 步/共 5 步完成",不依赖读者记忆 |
| 6. 具体耗时估计 | 说"约 15 分钟",不说"一点工作量" |
| 7. 让进展可见 | 明确说出"现在能跑通了" |
| 8. 平静陈述错误 | 位置、原因、修复,不用"Oh no" |
| 9. 列表不超过 5 项 | 超出就拆成"现在做"和"以后做" |
| 10. 无开场、无总结、无客套 | 从答案开始,到答案结束 |
规则本体是"单一事实来源"(single source of truth),扩展代码只负责把这份规则文件送进模型上下文,两者职责分离——这正是理解其 TypeScript 架构的钥匙。
核心文件一览:
| 文件 | 职责 |
|---|---|
| extensions/i-have-adhd.ts | 扩展入口:加载规则、管理状态、订阅事件 |
| extensions/context-compat.ts | 运行时兼容层:从 Pi / OMP 读取上下文消息 |
| skills/i-have-adhd/SKILL.md | 10 条规则的权威定义 |
| package.json | 在pi/omp字段中声明扩展入口 |
| scripts/check_context_compat.ts | 兼容性自检脚本 |
2️⃣ 启动第一步:loadRules 读取规则文件
Pi 或 OMP 启动时会加载 extensions/i-have-adhd.ts 导出的工厂函数iHaveAdhdExtension(pi),它做的第一件事就是调用loadRules()(见 extensions/i-have-adhd.ts):
- 定位文件:模块顶层用自己的文件位置(
fileURLToPath(import.meta.url))拼出SKILL.md的绝对路径,不依赖工作目录。 - 读取全文:用
readFileSync读文件;读不到就抛一个带路径的错误,让问题在启动时暴露。 - 剥离 frontmatter:
stripFrontmatter()用正则删掉文件头部的 YAML 元数据块(name、description等),只保留规则正文。 - 空内容即失败:规则文件是空的会直接抛错,避免"静默无规则"的会话。
💡 关键设计:
loadRules()每个扩展生命周期只执行一次,结果存入闭包变量rules。之后无论注入多少次上下文,都不再重复读盘。
3️⃣ 三种打开方式:启动标志、斜杠命令、自然语言
扩展注册了三类用户入口(extensions/i-have-adhd.ts):
- 启动标志
pi --adhd:通过pi.registerFlag("adhd", ...)注册,让新会话默认开启; - 斜杠命令
/i-have-adhd [on|off]:会话内切换,不带参数则翻转当前状态; - 自然语言:
input事件监听器拦截stop adhd mode和normal mode两个短语,随时关闭。
另外还有一个细节:/skill:i-have-adhd这个内置技能命令会被扩展接管(extensions/i-have-adhd.ts),直接等价于开启模式并返回handled,防止 Pi 把同一份规则再展开第二遍。
状态变更统一走setEnabled():它把开关写入一条会话条目(类型为i-have-adhd-state),刷新底部状态栏的● ADHD ON标记,再调用syncContext()同步上下文。
4️⃣ 会话开始:restoreState 决定开关初值
session_start和session_tree事件都会触发restoreState(ctx)(extensions/i-have-adhd.ts),它的决策逻辑是:
- 查历史:
getSavedState()扫描当前会话分支的所有条目,取最近一条i-have-adhd-state——用户在上一段会话里说过"stop adhd mode",恢复后仍保持关闭; - 定默认值:
pi.getFlag("adhd") === true(即pi --adhd启动)或~/.pi/agent/.i-have-adhd-always标志文件存在,则默认开启; - 合并:
enabled = savedState ?? enabledByDefault——用户的显式选择永远压过默认值; - 落地:先
updateStatus()刷新界面状态,再syncContext()把规则同步进对话。
5️⃣ syncContext 核心:规则只注入一次,而非每次重写
syncContext()(extensions/i-have-adhd.ts)是整个调用链的心脏,决策表非常克制:
| 模式 enabled | 规则已在上下文中 | 动作 |
|---|---|---|
| ✅ | ❌ | 注入规则消息(隐藏) |
| ✅ | ✅ | 什么都不做 |
| ❌ | ✅ | 注入"已禁用"通知 |
| ❌ | ❌ | 什么都不做 |
注入用的是pi.sendMessage({ customType: "i-have-adhd-rules", display: false }, { triggerTurn: false })——一条不显示、不触发模型回合的隐藏消息,安静地留在对话里,模型在之后的每个请求中都能"看到"它。
为什么"只注入一次"?代码注释里写得很直白:这与 Claude Code 的 SessionStart 钩子行为保持一致——注入一次规则集,而不是在每个请求前重写系统提示。省 token,也避免规则反复刷新干扰对话节奏。
关闭时也不是删消息,而是追加一条i-have-adhd-disabled通知:"忽略之前注入的 ADHD 规则,恢复默认风格"——用最新的标记覆盖旧规则,后文细讲。
6️⃣ context-compat 兼容层:一份代码跑两种运行时
"规则还在不在上下文里?"需要读取会话消息列表,但 Pi 和 OMP 的 sessionManager API 并不相同。兼容层 extensions/context-compat.ts 解决了这个问题:
- OMP的 API 是
buildSessionContext(),返回{ messages }; - Pi的 API 是
buildContextEntries(),直接返回条目数组。
contextMessages()(extensions/context-compat.ts)按顺序尝试两个 API,并且是故障放行(fail open)设计:sessionManager 缺失、API 不存在、甚至调用抛异常,一律返回空数组。后果是调用方认为"规则不在上下文",于是重新注入一次——重复注入无害(见下节),而让会话启动崩溃才是真事故。
真正裁决规则是否"生效"的是latestMarkerIsActive()(extensions/context-compat.ts):它按时间顺序扫描消息,只看最后一个标记:
- 先注入规则、后追加"禁用"通知 → 规则失效;
- 先禁用、后重新开启(再注入规则)→ 规则生效;
- 普通消息(非 custom 标记)一律忽略,不会误激活规则。
这套逻辑有专门的自检脚本 scripts/check_context_compat.ts 验证,用bun scripts/check_context_compat.ts运行即可。
7️⃣ session_compact:压缩之后规则"复活"
Agent 会话过长时会触发压缩(compaction),被总结掉的旧消息会从上下文里物理移除——之前注入的规则消息也可能就此消失。
扩展对session_compact事件的响应只有一行:再跑一次syncContext(ctx)。结合第 6 节的判断逻辑:规则没了就重新注入,还在就什么都不做。配合latestMarkerIsActive只看最新标记的特性,整条链对压缩、分支、会话恢复都是安全的。
8️⃣ 完整调用链总览
把上面的环节串起来,整条链是:
Pi / OMP 启动 └─► iHaveAdhdExtension(pi) ├─► loadRules() ← 读取 SKILL.md,剥离 frontmatter(一次性) └─► 注册 adhd 标志、/i-have-adhd 命令、input 事件监听 会话开始(session_start / session_tree) └─► restoreState(ctx) ├─► getSavedState() 从会话条目还原用户上次的开关选择 ├─► updateStatus() 底部状态栏显示 ● ADHD ON └─► syncContext(ctx) └─► rulesAreInContext(ctx) ├─► contextMessages() OMP: buildSessionContext / Pi: buildContextEntries(fail open) └─► latestMarkerIsActive() 只看最新标记裁决生效状态 └─► 未注入 → sendMessage(规则, 隐藏, 不触发回合) 用户切换(命令 / 自然语言) └─► setEnabled() → appendEntry(持久化) → updateStatus() → syncContext() 会话压缩(session_compact) └─► syncContext() 规则被压缩掉了?重新注入三个值得借鉴的架构要点:
- 规则与代码分离——改规则只需编辑 skills/i-have-adhd/SKILL.md,扩展代码零改动;
- 一次性注入 + 标记裁决——省 token 且对压缩、分支天然安全;
- 故障放行兼容层——运行时 API 变了也不崩,最多重复注入一次。
📚 延伸阅读:各运行时安装与激活方式见 INSTALL.md,仓库整体地图见 AGENTS.md,always-on 钩子声明在 hooks/hooks.json。
【免费下载链接】i-have-adhdA skill to stop your coding agent from burying the answer. ADHD-friendly output.项目地址: https://gitcode.com/GitHub_Trending/ih/i-have-adhd
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考