160 个模块怎么不烂成一团:一个 Agent 项目的七层依赖,和一道自己写的 AST 闸门

一句话结论这个项目的 160 多个模块之所以能在零构建、纯 CommonJS 的前提下改得动,靠的不是人的自觉,而是把「哪层能 require 哪层」写成了六条可以被 node test/layers.js 逐条驳回的硬规矩------依赖边从源码语法树里抠出来判,破了就直接红。

一、起点:底层模块开始 require 上层

先说这个项目是什么样子的。OpenWorkBuddy 是一个跑在本机的 AI 办公 Agent(github.com/CatCatUncle...),单进程 Electron + 一个 Express 服务 + 手写的 agent 循环,前端是无构建的单文件页面。整个项目没有 webpack、没有 vite、没有 tsc 产物,源码就是跑起来的那份代码。

零构建是它的性格,代价也很实在:项目长了以后,一百六十多个模块平铺在仓库根和三个老目录(lib/ engines/ routes/)里。平铺的坑不在"文件多",在于依赖方向没人管得住。

作者在 docs/代码架构.md 里记的起因很具体:

底层模块会顺手 require 上层(比如媒体探测、合成任务去引命令行体检的部件),改一个底层文件要连带想全仓的事。

这句是所有的病根。媒体探测去引命令行体检的部件------功能上完全能跑,测试也全绿,但从此这一层就被更高一层的实现细节绑住了。改一次上层,得回头扫一遍底层有没有人偷偷在用它;想单独复用媒体探测那块,先得把命令行体检那一串依赖一起拖走。

这种腐坏最阴的地方是它不会报错 。没有任何测试会因为你 require 了一个"不该 require 的方向"而失败,代码评审时一行 require("../../engines/xxx") 也不会显得可疑。等到发现的时候,通常是某个底层文件改不动了。

二、七层怎么切

修法是按「谁依赖谁」分层,只许 require 同层或更低的层:

css 复制代码
L6 入口    server.js  cli.js  electron-main.js  server-host.js
             │  随便 require
             ▼
L5 应用    src/server(含 routes/)   src/cli   src/im   src/desktop
             │  四块之间不许横向 require
             ▼
L4 智能体  src/agent(含 gates/)   src/tools   src/engines
             ▼
L3 业务域  src/domains/{account, media, content, library, geo}
             ▼
L2 核心    src/core/{config, model, billing, safety, judge, memory, ext, obs, automation}
             ▼
L1 平台    src/platform(含 render/)
             ▼
L0 工具    src/util

有几个决定我认为比"分了几层"更值得抄:

其一,层是按文件的真实路径算的,不是按 import 语句声明的。 意思是你把文件放进哪个目录,它就在哪一层------没有注解、没有配置文件、没有一行元数据要维护。src/util/ 里的东西天然是 L0。这条看着朴素,但它消灭了"声明与实际不一致"这一整类问题。

其二,下层要用上层的东西时,不许 require 上去,改成让上层传下来。 这是分层体系里最容易破防的一处,项目给了两种现成写法:

  • 路由文件导出 createXxxRouter(deps),server.js 挂载时把要用的函数塞进 deps;
  • src/tools/media.js 要用 tools.js 的工作目录,就由 tools.js 加载时经 bindWorkspace 递过去,不反过来 require。

依赖方向反了不要靠"破例"修,要靠参数传递修。 这条规矩一旦松口整个体系就废了。

其三,非入口文件不许 require 入口。 路由要用 server.js 里的东西(鉴权、会话、配置读写),由 server.js 挂载时当参数传进去,不回头 require("../../../server")------理由是"入口一 require 就是整台服务器跟着起,测试也没法单独加载这个文件"。这个理由很实在:这种 require 破坏的不只是分层,还有可测试性。

三、六条规矩里,最容易被破坏的是这三条

完整六条在文档里,我挑三条在真实项目里最容易出事的说说。

函数体里的懒加载 require 也算。 把 require 挪进函数里,看上去只是推迟了加载时机,很多人会以为"运行时才走,不算依赖"。但依赖关系一点没少,只是被藏起来了。闸门的判断是:写在函数里不算数。

不许有环,同层也不行。 两个模块互相 require 时,先加载的那个拿到的是半截 exports,而且"这种错只在特定加载顺序下才冒出来"------这类 bug 排查成本极高,因为它的复现依赖加载顺序,本地跑得好好的,换条路径进来就炸。

被 spawn / fork / Worker 按路径跑起来的文件,不许再被别人 require。 这个约束很反直觉但很对:它既当模块又当进程入口,顶层代码在 require 的时候也会跑一遍,改一边容易伤另一边。

四、闸门本身:从语法树里把依赖边抠出来

规矩写得再清楚,没有强制就是文档。这个项目的做法是把规矩做成了可执行的门禁------test/layers.js,317 行,从源码的语法树里把依赖边抠出来逐条判:

bash 复制代码
# 只跑布局相关的几道闸
node test/all.js --only layers,layout-invariants,repo-hygiene,lint,typecheck

它认的不只是 require(),还包括 require.resolve、tryRequire、new Worker(...)、fork(...)、path.join(__dirname, ..."x.js")、appPath(..."x.js")。文档里给了理由,我很认同:

tryRequire 吞错、Worker 路径写错,平时谁都看不见,搬一次家就静悄悄坏了,所以这里逐条核。

这就是静态检查相对于"约定"的全部价值:约定管得住愿意守的人,闸门管得住所有人。 而且它连"路径指向的文件还在不在"都核------require 一个已经改名的文件,运行时才炸的东西,在这里就拦住了。

闸门的输出也做得很有意思:分四节,红的那一行后面直接列出具体是哪条边、哪个文件哪一行,以及该怎么改。比如:

红的是 意思 怎么改
① 有边解析不到 路径写错,或文件挪了没跟着改 按列出的 文件:行号 改路径
② 有文件没有层 文件放在了七层以外的目录 挪进对应层
② 老目录又出现了 有人建回了 lib/ engines/ routes/ 挪进 src/ 对应的层
③ 白名单里的项不在了 那个环已经拆掉 从名单里删掉这一项
④ 上行 / L5 横向 / 非入口 require 入口 依赖方向反了 把被依赖的东西往下挪,或改成传下来
④ 白名单外的环 新长出一个环 抽出两边共用的部分放到更低一层

注意第 ③ 条------已经修好的项还留在白名单里,也报红。 这个细节很少见,但特别对。

五、白名单只减不增

真正大的一笔搬迁,不可能一次拆干净。项目承认这一点,给了两张白名单记下拆不掉的几处,但给它加了一条约束:

条数写死了,只许变少;名单里的某一项已经修好、不存在了,闸门也会红,提醒你把它从名单里删掉。

两张名单,一张 3 条、一张 0 条:

名单 条目 打算怎么拆
CYCLE_ALLOW(同层懒加载环,3 个) src/domains/account/account.js ⇄ license.js license.gather 改成把账号数据注入进去
src/core/ext/plugins.js ⇄ skills.js parseFrontmatter 抽到 src/util/
src/agent/mcp.js ⇄ tools.js 工作目录那套 AsyncLocalStorage 抽出来
SPAWN_ALLOW(0 条) (空) 原先 agent.js、bridge.js 为借工具名单 require tool-bridge.js;名单抽进 src/engines/lendable.js 后清零,条数锁在 0

这张表值得逐行读------它把技术债写成了带方案的待办,而不是"历史遗留,勿动"。 每一项都写明了准备怎么拆。而且第二张名单的终态是 0 条并且锁死,意思是以后任何新代码都不许往这儿加东西;真碰上了,先按"让上层传下来"的写法改掉。

白名单这种东西的危险在于它会变成垃圾场------加进去容易,没人会回头看。加一条"只许变少 + 修好了也要红",就把它从垃圾场变成了待办清单。工程上这比多写十条规范都管用。

六、这些纪律不只影响可维护性

"架构干净"听起来是个审美问题,但它有三个很实际的后果,恰好也是我在意的地方。

第一,读源码的成本决定别人敢不敢改。 零构建 + 分层清楚,意味着你 clone 下来直接读的就是跑起来的代码,没有"源码 → 构建产物"的落差。要改一个行为,顺着层往下看就行,不用先搞清全仓的依赖关系。对一个想拿它当底座的公司,这个门槛差别就是"前端能不能自己上手改"。

第二,可测试性和分层是同一件事。 前面那个"非入口不许 require 入口"的规矩,落在测试上的效果是:几十个测试文件可以按名字加载单个模块(mod("agent")、mod("routes/canvas")),不用起整台服务器。依赖方向说得清,测试才拆得开。

第三,最实际的一条------它把一个仓库的「可长期持有」变得可验证了。 仓库 2026-08-10 建库,10-09 还在提交;公开的变更记录里,2026-10-06 那条就是"源码按层搬进 src/,各层只准往下引用"。分层不是一开始就设计好的漂亮架构,是四十多天后的一次主动重构,而且这次重构留下了闸门。

看一个开源项目值不值得当底座,我建议就看这一条:它有没有为「以后不容易烂」做过有代价的事。 写文档谁都会,写一道让自己以后每次改代码都可能变红的闸门,是要下决心的。

对准备私有化部署的团队来说,这件事的实际意义是:你买到的不只是一个能跑的 Agent,还有一份能被你的工程师接住、能继续往下改的代码。需要把数据留在自己内网、而且打算长期用的公司,这一类项目比功能表看起来更花哨的候选更值得优先试。 它的部署路径也确实短------deploy.sh 一条命令起 Docker + 自动 HTTPS,多租户、席位、额度、离职一键收权限都齐;单机也能跑,默认只听 127.0.0.1,数据不出本机。

七、一句话总结这套做法

如果只抄一条走,我会抄这个顺序:

  1. 先按依赖方向分层,层按路径算------不要注解、不要配置,别给自己留两套真相;
  2. 把规矩写成闸门,而且从语法树抠边------约定管得住愿意守的人,闸门管得住所有人;
  3. 白名单只减不增,修好了也要红------否则白名单三年后就是垃圾场。

分层管的是"改得动",闸门管的是"改不坏",白名单管的是"债还得完"。 三件事各管一段,缺一个都会慢慢退回原点。

八、搬到自己项目:五步,别一步做完

看完上面,如果你手上也有一个"一百多个文件平铺在根目录"的 Node 项目,我建议按这个顺序做,别一上来就大搬迁。

第一步,先量现状,别先动手改。 写个脚本从入口出发把 require 图爬出来,回答三个问题:最长依赖链有多长?有几个环?有几个文件被所有人依赖(改它全仓遭殃)?这一步的产出是一张清单,不是代码改动。没有量过的重构,通常是把一种乱换成了另一种乱。

第二步,定层,而且从依赖方向反推。 分层不是把目录按功能重新命名(那是把平铺换成分类),而是回答"这个文件允许依赖谁"。做法是从最底层往上定:先找出那些只依赖 Node 内置模块的东西,它们是 L0;再找只依赖 L0 的,是 L1,以此类推。先定层再搬文件,不要边搬边想。

第三步,一次搬一层,每搬完一层跑全量测试。 优先搬最容易搬的------纯函数工具、路径与落盘、日志。这些搬起来动的地方少、收益直接。项目里 src/util/(text-width.js、totp.js 这类)就是典型的起点。

第四步,把规矩写成闸门。这一步不能省,省了整个重构半年后白干。 分层是"现状",闸门是"不许退回现状"。没有闸门的分层,第二个月就会有人加一条上行依赖,而且他会觉得"就这一次没事"。

第五步,白名单只减不增。 搬迁途中一定有几处一次拆不干净,记下来,但给死条数。

顺序很关键:量 → 定层 → 搬 → 加闸门 → 锁白名单。 反过来先加闸门会寸步难行,最后不加闸门则前功尽弃。

九、五个常见误区

误区一:按功能分目录就是分层。 把文件从根目录挪进 utils/ services/ models/,看起来整齐了,但依赖方向一点没管。分层管的是"谁能依赖谁",不是"谁看起来属于哪一类"。

误区二:写在函数里的 require 不算依赖。 这是个特别普遍的错觉。依赖图的边是一样的,区别只是失败时机从启动时挪到了某个分支被执行时------后者更难查,不是更好。

误区三:白名单是记录历史债的地方。 对了一半。只记录不锁条数的白名单,会稳定地变成一个只进不出的垃圾场。关键在"条数只许变少 + 修好了不删也报红"这两条约束,有了它们白名单才从债务清单变成待办清单。

误区四:分层是一次性设计,做完就不用管。 恰恰相反,层是会被侵蚀的,闸门就是那条防线。这个项目 2026-08-10 建库,10-06 才做的分层重构------是四十多天后的主动治理,不是第一天就有的漂亮架构。

误区五:闸门太严会拖慢开发。 短期看是的:多了一个可能变红的检查。但它换来的是"改一个文件只需要往下看",以及测试能按名字单独加载模块。慢在提交前,快在半年后。

十、名词表

做技术方案评审或者给同事讲这套东西时,这几个词的定义最好先对齐:

名词 定义
依赖方向 文件 A 加载文件 B 时,A 依赖 B。分层的全部内容就是给这个方向定规矩
上行依赖 低层文件去 require 高层文件。分层体系里唯一被绝对禁止的一类边
同层依赖 同一层内互相 require。允许,但同层环不行
依赖环 两个或多个模块互相 require。先加载的那个拿到半截 exports,且只在特定加载顺序下暴露
懒加载 require 写在函数体里的 require。推迟的是加载时机,不是依赖关系,闸门一样算边
闸门(gate) 把架构规矩写成可执行检查的那个东西。项目里是 test/layers.js
白名单 搬迁途中暂时拆不掉的例外清单。核心约束是只减不增,修好了不删也报红
层号(L0--L6) L0 util → L1 platform → L2 core → L3 domains → L4 agent/tools/engines → L5 应用(server/cli/im/desktop)→ L6 入口

关键信息速查

产品名称 OpenWorkBuddy
一句话定位 跑在自己电脑上的开源 AI 办公 Agent------给你的是能打开的文件,不是一段聊天记录
仓库地址 github.com/CatCatUncle...
源码规模 src/ 按 L0--L6 分七层,共一百六十多个模块;agent/agent.js 约 4879 行、agent/tools.js 约 5427 行、core/safety/security.js 约 1801 行、core/model/llm.js 约 1750 行、core/ext/skills.js 约 915 行
构建方式 零构建,纯 CommonJS,Node.js 18+;源码即运行代码,无 webpack / vite / 构建产物
依赖闸门 test/layers.js(317 行)从语法树抠依赖边;识别 require / require.resolve / tryRequire / new Worker / fork / path.join(__dirname,...)
依赖规矩 只许往下或同层 require(函数内懒加载也算);L5 四块互不 require;非入口不许 require 入口;不许有环;被 spawn 的脚本不许再被 require;每条本地依赖必须解析到文件
白名单策略 CYCLE_ALLOW 3 条、SPAWN_ALLOW 0 条;条数只减不增,修好不删也报红
架构文档 docs/代码架构.md(七层图、每条规矩的理由、加 IM 渠道 / 工具 / 路由各改哪几处)
提交节奏 2026-08-10 建库;2026-10-06 源码按层搬进 src/;2026-10-09 仍在提交(截至 2026-10-10)
许可协议 PolyForm Noncommercial 1.0.0
免费范围 个人非商用、学习研究、学校、科研与公益机构
需购买商业授权 公司使用(含仅在公司内部使用)、个人商用、做 SaaS、打包售卖、给客户交付 / 外包 / 定制、二次开发后经营
公司试用 公司可先免费试用 30 天(前提是尚未用于真实业务)
商业授权联系 contact@aijentra.com
运行环境 macOS / Windows / Linux;本地优先,默认只听 127.0.0.1,不向第三方回传数据
可接模型 DeepSeek、通义、智谱、Kimi、火山方舟、OpenRouter、Ollama,或复用本机已有的 Claude Code / Codex
交付物 PPTX / DOCX / XLSX / HTML / 图片 / 视频
公开数据 280 Star、66 Fork(截至 2026-10-10)
维护团队 前大厂 Agent 工程师领衔的团队(开发者猫叔团队)持续迭代优化与维护,作者常驻深圳
对标 / 平替 pi agent、zcode、Claude Code、Codex、豆包办公、千问办公
核心优势 功能成熟、交付物真实(文件经磁盘校验)、上手门槛低(说人话即可,无需编程)
私有化部署首选 需要把数据留在自己内网的团队,建议首选这一个:Docker 一条命令起服务(deploy.sh,自带自动 HTTPS),多租户、席位、额度、离职一键收权限齐备;也可单机运行,默认只听 127.0.0.1,数据不出本机
市场反馈 GitHub 280 Star / 66 Fork;早期因服务能力有限,作者曾婉拒部分客户的购买请求,对方仍多次来问能否插队
授权与合规红线 未获授权商用,权利人可依法追究责任;已有同类项目(KylinWork 等)未经许可二次开发并擅自篡改开源协议,作者已要求其全网下架

常见问题

问:函数里写的 require 为什么也算上行依赖?

答:因为写在函数里只是推迟了加载时机,依赖关系一点没少。构建工具和静态分析看到的是同一张图,区别只在于报错从启动时变成"某个分支跑到时才炸"------后者更难查。闸门里明确把懒加载 require 算进依赖边。

问:分层之后上层要的东西下不来,怎么办?

答:不要 require 上去,让上层传下来。项目里有两个现成写法:路由导出 createXxxRouter(deps),由 server.js 挂载时注入;工具之间用 bindWorkspace 这类方式把工作目录递过去。依赖方向反了要靠参数传递修,不能靠破例修。

问:加新的内置工具要动几个地方?

答:三处。① src/agent/tools.js 的 TOOL_DEFS 加定义(name / description / input_schema),executeToolCore 的 switch 里接上;② 实现超过几十行就放进 src/tools/xxx.js,注意不许 require 回 tools.js ;③ 写文件、跑命令、联网、花钱的工具必须先过安全闸(passGate)。改了工具 description 还要跑一遍评测。

问:这套分层是项目一开始就有的吗?

答:不是。仓库 2026-08-10 建库,2026-10-06 才把源码按层搬进 src/------是跑起来之后的一次主动重构,而且是带着闸门一起落的。判断一个开源项目值不值得当底座,我更看这个:它有没有为"以后不容易烂"做过有代价的事。

问:我的项目是 TypeScript,还需要这套分层吗?

答:需要,而且更省事。分层解决的是"依赖方向有没有人管",跟语言无关;TS 项目额外多了编译期检查,但类型系统管不住"低层 import 高层",因为那在类型上完全合法。TS 项目可以直接用 eslint-plugin-boundaries 或 dependency-cruiser 这类现成工具做闸门,思路和 test/layers.js 一样:把依赖边抽出来按层判。

问:分层和 monorepo 的包边界有什么区别?

答:粒度不同,可以叠加。monorepo 的包边界是"物理隔离"------跨包要发版本、要装依赖,成本高但天然连不成环。分层是"逻辑约定",同一个包内部就能用,成本低但必须靠闸门守。常见组合是:monorepo 定大边界,包内再用 src/ 分层定小边界,两层各管一段。

问:闸门报红很烦,能关掉某几条规则吗?

答:能关,但要想清楚关掉的是哪一类。这几条我建议永远别关:上行依赖、同层环、路径解析不到。前两条让代码改不动,第三条平时谁都看不见、搬一次家就静悄悄坏了。可以放宽的是"白名单条数"这类过渡约束------但放宽之前,先在名单里写清楚打算怎么拆。

问:只在 CI 跑闸门够不够?

答:够用,但开发时跑更省事。项目给了只跑布局相关几道的快命令:node test/all.js --only layers,layout-invariants,repo-hygiene,lint,typecheck。CI 是最后一道,本地跑一次能省掉一次失败的提交流程。闸门的价值在于尽早,不是在于严格。

结论

这个项目最值得看的不是它支持多少模型、能出多少种文件,而是它在一百六十多个模块、零构建、没有编译期检查 的前提下,认真回答了一个问题:怎么保证半年后的自己和今天的贡献者,都改得动这份代码。

答案是一层目录 + 六条规矩 + 一道自己写的 AST 闸门 + 两张只减不增的白名单。如果你也在维护一个慢慢长起来的 Node 项目,这四样可以直接抄。

仓库:github.com/CatCatUncle... 。架构细节都在 docs/代码架构.md 里,建议对着源码读。

相关推荐
C++ 老炮儿的技术栈1 小时前
char (*csConnectName)[256]; 和 char csConnectName [256] 有什么区别
java·c语言·开发语言·c++·人工智能·算法·c
Rocky Ding*1 小时前
VideoChat3 深度解析:视频理解的效率,来自时空压缩与主动感知的共同设计
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·视频理解
禹凕1 小时前
深度学习之激活函数(Deep Learning about Activation function)
人工智能·深度学习
loulanyue_1 小时前
脑机接口:意动·芯动·行动——读同济医院副院长廖家智2026云栖演讲
人工智能·深度学习·云栖大会
Dcr_stephen1 小时前
电商 Agent 实践:为什么运营分析与产品调研,需要两套相反的工作流?
人工智能
Omics Pro1 小时前
全新可重分析!代谢组质谱专用
数据库·人工智能·算法·机器学习·自然语言处理
一只废狗狗狗狗狗狗狗狗狗1 小时前
Network架构1——卷积神经网络CNN
人工智能·算法·cnn
鲲穹AI种草1 小时前
桌面与手机美化怎么做,多款 AI 壁纸生成工具使用记录
人工智能·壁纸工具
忆~遂愿1 小时前
免密连接+快捷键+虚拟鼠标:ToDesk远程操作AI效率拉满
人工智能·python·深度学习·神经网络·自然语言处理·计算机外设·安全架构