一个目录塞 63 个脚本是什么体验

用 AI Agent 自动化日常工作流听起来很美好:健康数据追踪、财务记账、情报监控、内容发布、记忆管理……每个功能背后一两个脚本,全塞进一个 scripts/ 文件夹。

10 个脚本的时候还好,肉眼能扫完。长到 60 多个的时候,打开目录就是一堵墙。哪个脚本属于哪个功能?不知道。哪些脚本之间有依赖?不知道。想单独迁移"健康追踪"到另一台机器?做梦。

更致命的是配置散落——18 个 Notion 数据库 ID、54 个 Discord 频道 ID,硬编码在 30 多个脚本里。定时任务一跑,有的脚本还在往旧数据库写数据。那种感觉,就像你以为关了所有窗户,暴雨天发现阳台那扇忘了。

所以重构的动机很朴素:不是看了什么架构文章受到启发,纯粹是再这样下去,迟早因为漏改一个 ID 把线上搞崩。

核心难题:给飞行中的飞机换引擎

重构目标谁都能想清楚:按功能拆模块、配置集中管理、依赖显式化、能独立测试和迁移。

难的是——系统在线上跑着,几十个定时任务每小时都在触发,怎么重构不中断服务?

一条铁律贯穿始终:任何时刻,现有功能不能断。哪怕重构到一半,系统也必须正常工作。 这条约束直接决定了后面所有设计决策。

五步走,每一步都能回滚

整个重构拆成五个阶段,不是一口气干完,而是每个阶段独立交付,出问题随时回滚。

Phase 1:先建基础设施

在动任何脚本之前,先写好 config-loader——一个统一的配置读取模块。每个模块目录里放一个 config.json,脚本启动时调用 loadModuleConfig(import.meta.url),自动根据当前脚本路径找到所属模块的配置文件。

为什么用 import.meta.url?因为脚本不需要知道自己在哪个目录。你把它从 A 模块移到 B 模块,它自动读 B 的配置,零改动。这个设计是整个重构的基石,后面所有"把硬编码换成配置读取"的工作都建立在它上面。

Phase 2:建目录,移脚本,留 symlink

这是最关键也最容易翻车的一步。

创建 13 个模块目录,按功能职责归类。每个模块统一结构:MODULE.md(说明文档)+ config.json(配置)+ scripts/(脚本)。

但脚本一移动,原来的路径就失效了。几十个定时任务还指着旧路径。怎么办?

symlink 过渡。 每个脚本移到新位置后,在原来的 scripts/ 目录创建一个 symlink 指向新位置。所有定时任务和外部引用都不受影响——它们访问的路径没变,只是背后的文件位置变了。

这招看起来简单,但它解决了一个核心问题:把"移动文件"和"更新引用"这两件事解耦了。不需要一次性改完所有引用,今天改 10 个定时任务的路径,明天改 10 个,每改完一批就删除对应的 symlink。任何时刻,只要 symlink 还在,旧路径就能用。这就是安全网。

Phase 3:替换硬编码

一个模块一个模块来。改完一个模块,跑一遍这个模块的所有脚本,确认没问题再改下一个。

最终 24 个脚本完成配置迁移。18 个 Notion 数据库 ID、54 个 Discord 频道 ID,全部收敛到各模块的 config.json 里。现在要换一个数据库 ID?打开对应模块的 config.json,改一行,完事。

Phase 4:更新定时任务路径

大约 50 个定时任务需要从旧路径切换到新路径。最枯燥的一步,也是最不能出错的一步。策略很直接:改一个,测一个,不批量改。

Phase 5:清理收尾

删除所有 symlink,更新文档,创建模块索引。到这一步系统已经完全运行在新架构上,删除 symlink 只是把安全网撤掉——因为已经不需要了。

三个值得深聊的设计决策

比起"怎么移文件",这三个决策更有参考价值。

模块边界怎么划?

最初想过按"数据源"划分——跟 Notion 交互的放一起,跟 Google 交互的放一起。很快否决了。一个健康追踪脚本可能同时读 Google Fit 数据、写 Notion 数据库、发 Discord 通知,按数据源划分它算哪个模块?

最终选了按业务职责划分。不管调用什么 API,只要服务于"健康追踪"这个业务目标,就归到健康模块。好处是直觉性强——想找跟健康相关的所有逻辑?打开健康模块就行。

13 个模块:基础设施、健康追踪、财务管理、记忆系统、内容创作、生活管理、情报监控、运维管理、图书管理、英语学习、安全审计、深度反思、每日简报。最大的 14 个脚本(健康追踪),最小的 1 个(每日简报)。大小不均匀完全没关系,模块边界是为了职责清晰,不是为了代码量均分。

跨模块依赖怎么处理?

设一个 shared 基础设施模块,所有公共依赖放在这里(比如 Google Workspace API 封装、Notion 通用操作)。其他模块通过显式引用 shared 来使用公共能力。

关键规则:只有 shared 可以被所有模块引用,模块之间不能互相引用。 如果 A 模块需要调用 B 模块的功能,要么把这个功能抽到 shared,要么说明这两个模块的边界划错了。不允许模块间形成网状依赖。

这条规则看起来严格,但它避免了最头疼的问题:你想迁移一个模块,结果发现它依赖另外三个模块,而那三个模块又互相依赖……说白了就是把"牵一发动全身"变成"拔一个盒子走人"。

渐进迁移还是一步到位?

一步到位的诱惑很大——反正都要改,不如一次全改完。但改一个脚本,测试这一个就行;一次改 60 个脚本,你得测试所有脚本的所有交互,出了 bug 还不知道是哪个改动引入的。全量改动的测试成本是指数级的。

渐进迁移的代价是过渡期会有"新旧混合"状态——一些脚本已经用 config-loader,一些还在硬编码。但这个代价远小于一次性改崩的风险。

重构前后的体感差异

  • 改配置:之前 grep 全目录 → 逐个文件修改 → 祈祷没漏掉。之后打开一个 config.json → 改一行 → 结束。
  • 新增功能:之前往 scripts/ 里扔一个脚本,过三个月忘了它干嘛的。之后在对应模块加脚本,读 MODULE.md 就知道上下文,配置从 config.json 读取。
  • 排查问题:之前健康数据没同步?在 60 多个脚本里翻。之后直接进健康模块,所有相关脚本都在。
  • 迁移部署:之前不可能,脚本之间隐式依赖,拔出萝卜带出泥。之后把一个模块目录整个拷走,带上 shared 模块,就能独立运行。

硬编码从 30 多个脚本里的 72 个 ID,收敛到各模块 config.json 的集中管理。残留硬编码:1 个,非关键的。

什么时候该动手

不是所有系统都需要模块化。如果你的 Agent 系统只有 5 个脚本,搞这套纯属过度工程。

但如果你遇到了这些信号,就是时候了:

  • 改一个配置要搜好几个文件
  • 不敢删任何一个脚本,因为不知道有没有别的在引用它
  • 三个月后的你自己打开项目,完全看不懂结构
  • 想把某个功能单独部署,发现根本拆不出来

模块化的终态大家都能想明白,真正难的是从现状过渡到终态的路径。symlink 过渡是这次最大的收获——新路径准备好 → 旧路径 symlink 过去 → 逐步切换引用 → 全部切完删除 symlink。这个思路不只适用于文件迁移,任何需要"在不中断服务的前提下切换底层实现"的场景都能借鉴。

重构完成后记得让 AI 做一次巡检,检查路径解析在移动后有没有偏移。实际验证效果:重构完成后新增一个模块,从讨论方案到脚本上线,10 分钟。

模块化不是让系统变复杂,恰恰相反——它是在系统已经复杂到失控的时候,把复杂度装进盒子里。每个盒子内部可以复杂,但盒子之间的关系必须简单。你不需要理解整个系统,只需要打开你关心的那个盒子。