Node 内置模块使用建议
适用版本:当前
dev分支 配套接口:@aalis/api-storage/@aalis/api-process配套 lint:biome.json中style/noRestrictedImports规则 + 显式 overrides 例外清单
0. 这份文档的定位
这份文档是建议而非铁律。 除了下面 § 1 明确写出的「@aalis/core 不 import 任何 node:*」这一条之外,其它所有规则都是:
- lint 默认值:通过
biome.json把一些node:*加入noRestrictedImports,目的是当你绕过 storage / process 服务时给出一个带解释的报错,而不是默默引入难以审计的耦合。 - 社区约定:让生态里的插件优先用同一套抽象(storage / process / fetch),便于用户切换后端、便于重构、便于给某些场景做沙箱。
如果你写的是第三方插件,并且:
- 评估后认为某个
node:*用法在你的场景里是合理的; - 接受随之而来的「与 Aalis 抽象层耦合度变低」的代价;
完全可以在你自己插件的 biome.json 或 lint 配置里 noRestrictedImports: off。没有人会强制阻止——这是一份建议清单,不是审批流程。作为发布到 npm 的第三方插件,发布即可被 aalis-plugin 关键词检索到、装上即用,能不能跑起来与任何评审无关;只有当你主动把插件合入主仓库(成为一方插件)时,code review 才会评估这些 node:* 用法是否合理。
下面所有「建议 / 推荐 / 优先」字样都按这个语境理解。
1. 总原则
@aalis/core 是环境无关的内存运行时(不 import 任何 node:*、不读 process.env、不调 process.cwd()),所有副作用都被推到插件层。这一条是核心架构红线,让 core 理论上可以在 Workers / Deno / 浏览器 host 等环境里运行。
基于这条红线,对插件层给出以下默认建议:
业务插件(agent、memory、tools、persona、scheduler、websearch、image-recognition、…)建议优先通过以下服务访问宿主能力,而不是直接 import
node:*:- 文件系统 →
@aalis/api-storage(uses里声明storage后调caps.storage.use;命名根如 workspace、data、pluginData、tmp、cache、logs、self) - 子进程 / 外部文件 / 临时目录 →
@aalis/api-process(uses里声明processService后调caps.process.use;方法含spawn/execFile/makeTempDir/readExternalFile) - 出站网络 →
useNetwork()/ fetch - 入站网络 → 服务端类插件(webui-server / mcp-server)
- 系统信息(CPU/内存/用户名)→
plugin-tool-system的system_info工具
这样做的好处:用户切换 storage 后端(local / S3 / 内存)时插件不用改,重构时影响面可控,集成测试也更便于用 mock 提供者替换。
- 文件系统 →
基础设施插件(提供上述服务的实现,或本身就是宿主服务,例如 HTTP 入站)通常需要直接使用对应的
node:*,这是它们的本职。每个例外在biome.json的overrides例外清单中显式列出,并在本文件登记原因——新增例外时建议同步更新两处,方便审阅者理解。几乎总是可以直接用的
node:*模块(详见 § 1.5 边界说明):node:buffer—— Web 标准Uint8Array不能完全覆盖二进制 API(Buffer.from(x,'base64')/Buffer.concat/Buffer.toString('hex')一行即可),且StorageService.writeFile签名string | Buffer等 API 直接要求 Buffer。node:path—— 纯函数路径运算(join/resolve/dirname/extname/posix),无 I/O,无 Web 等价替代。node:url——fileURLToPath/pathToFileURL推荐使用;URL本身建议用全局URL,避免import { URL } from 'node:url'这种冗余写法。
建议优先用 Web 标准替代的
node:*模块:node:crypto—— 推荐用全局 Web Crypto(crypto.subtle.digest/crypto.randomUUID/crypto.getRandomValues)。Web Crypto 已能覆盖绝大多数场景;只有遇到 Web Crypto 确实没有的高级需求(如 X.509、传统 hash 算法)才需要回到node:crypto,那种情况请走 § 4 流程加例外。
1.5 Buffer / URL / Crypto 边界
- Buffer:新代码默认用
Uint8Array;以下场景显式import { Buffer } from 'node:buffer'更合适:- 调用
StorageService.writeFile等强制要求 Buffer 签名的 API; - 需要
base64/hex编解码一行完成(Buffer.from(s,'base64')/.toString('base64')); - 需要
Buffer.concat([...])拼接多个二进制片段。
- 调用
- URL:
new URL(...)直接用全局;只有把file://URI 换成本地路径或反向操作时才import { fileURLToPath, pathToFileURL } from 'node:url'。 - Crypto:
crypto.subtle.digest('SHA-256', bytes)返回ArrayBuffer;hex 化要手动Array.from(new Uint8Array(ab)).map(b=>b.toString(16).padStart(2,'0')).join('')。crypto.randomUUID()与crypto.getRandomValues()直接调全局即可。
2. 模块 → 提供者映射
| node 模块 | 业务插件建议做法 | 当前直接持有者 | 持有原因 |
|---|---|---|---|
node:fs / node:fs/promises | 走 @aalis/api-storage | plugin-storage-local | StorageService 的当前实现 |
plugin-process-local | readExternalFile():用于 OneBot 实现端动作应答里交回的 /tmp/... 等不在任何 storage 根内的绝对路径 | ||
plugin-webui-server | existsSync 探测前端 dist 目录(位于工作区外、由 webui-client 提供) | ||
create-aalis-plugin | 脚手架 CLI,物理生成新插件目录 | ||
plugin-tool-browser | 探测 puppeteer 包内 cli.js 与 Chrome 是否已下载(包资产管理) | ||
plugin-memory-sqlite/src/index.ts | statSync 取数据库文件的设备号与 inode,判断两个实例是否打开了同一个库 | ||
plugin-session-server/src/approvals.ts | appendFile / mkdir 追加审批审计:只追加的记录,存储服务没有追加写 | ||
plugin-usage-stats/src/index.ts | appendFile / mkdir / createReadStream 追加与按行读回用量账本:只追加的记录,存储服务没有追加写与按行读 | ||
plugin-reach | 宿主路径的真实路径(安装集、用户主目录、守护进程的入口、链接装进来的包、清单里各规则的根)、按 .git 找仓库的 git 目录、本会话临时区(0700、清扫)、沙箱用的 npm 配置副本:这些路径不在存储根内,临时区要按 0700 建、按 lstat 不跟随链接清扫,存储服务没有这些能力;存储单元之后存储服务的宿主路径根要经触及凭据,而凭据正是本插件签发的 | ||
node:child_process | 走 @aalis/api-process(spawn/execFile) | plugin-process-local | ProcessService 的当前实现 |
node:http / node:https | 出站建议用 fetch | plugin-webui-server | HTTP 入站服务 + WebSocket 升级 |
plugin-mcp-server | HTTP/SSE 入站端点 | ||
node:net | 出站建议用 fetch(受用户或 LLM 影响的 URL 走 safeFetch) | plugin-tool-browser | 浏览器网络闸:在 127.0.0.1 起 SOCKS5 服务接收浏览器的全部 TCP 连接,判定通过后由插件进程连接目标 |
node:os | 走 system_info 工具聚合返回 | plugin-tool-system/src/tools/system.ts | 该工具的本意就是暴露系统信息 |
plugin-reach | homedir / userInfo:拒读清单与落点按用户主目录生成,子进程环境取 owner 的 login shell | ||
node:sqlite | 消息与元数据走 @aalis/api-memory(memory 服务) | plugin-memory-sqlite/src/index.ts | MemoryService 的 SQLite 实现 |
node:crypto | 推荐 Web Crypto(crypto.subtle.digest / crypto.randomUUID / crypto.getRandomValues) | plugin-tool-browser | createHash 取截图 PNG 内容的 sha256 作文件名 |
plugin-storage-local | randomBytes 生成原子写临时文件名里的随机段 | ||
runtime/src/providers.ts | randomBytes 生成配置文件原子写临时文件名里的随机段 | ||
plugin-session-server/src/approvals.ts | createHash 同步算审批的内容哈希 | ||
plugin-session-server/src/files.ts | createHmac / timingSafeEqual 同步签发与核验文件句柄 | ||
plugin-mcp-server | createHash / timingSafeEqual 定时比较访问令牌 | ||
node:buffer / node:path | 直接使用 | 全部 | 纯函数 / Web 标准不覆盖,无 I/O |
node:url | fileURLToPath / pathToFileURL 推荐使用;URL 用全局 | 全部 | 仅用作路径互转时引入 |
3. 直接持有 node:* 的插件清单(白名单)
每行格式:插件 — 引入的 node:* — 用途。biome.json overrides[1].includes 与此清单保持一致即可让 lint 通过(plugin-memory-sqlite 除外,见其条目)。
基础设施(服务实现)
plugin-storage-local—node:fs(createReadStream/watch)+node:fs/promises(CRUD)+node:crypto(randomBytes) 本地文件系统后端,实现StorageService接口。其它插件落盘走caps.storage.use即可,无需绕过它。randomBytes为原子写的临时文件名生成随机段,避免同进程同一毫秒并发写同一路径时临时文件重名。plugin-process-local—node:child_process(spawn)+node:fs/promises(readFile)ProcessService的当前实现:负责spawn/execFile/makeTempDir/readExternalFile。readExternalFile用于 OneBot 实现端动作的应答这类场景,源端给的是 OS 直读路径(如/tmp/xxx.jpg),不属于任何 storage 根,所以刻意绕过 storage 沙箱直接fs.readFile;该方法不受沙箱约束,调用方自行保证路径安全。
入站网络服务
plugin-webui-server—node:http(createServer)+node:fs(existsSync) Express + ws 实现的 WebUI 后台。existsSync用于探测webui-client的 dist 目录是否存在;服务启动后浏览器拉取的用户文件都走storage.createReadStream,不直接fs.readFile工作区内容。autoOpen打开浏览器走ProcessService.spawn。plugin-mcp-server—node:http(createServer)+node:crypto(createHash/timingSafeEqual) HTTP/SSE 形式的 MCP 服务端,对外暴露工具/资源/提示词。每个请求的访问令牌两边先取 SHA-256 摘要再定时比较:Web Crypto 没有定时比较。
系统信息工具
plugin-tool-system/src/tools/system.ts—node:os实现system_info工具(hostname/cpu/uptime/loadavg/memory/userInfo)。整个 plugin-tool-system 包只有这一个文件被列入例外;同包其它工具(shell/file 等)走 ProcessService / StorageService。
脚手架
create-aalis-plugin—node:fs(existsSync)+node:fs/promises(mkdir/writeFile)npm create aalis-plugin的 CLI,在工作区外生成新插件目录,无法、也不应通过 storage 服务。create-aalis—node:fs(existsSync/mkdirSync/writeFileSync/readdirSync/realpathSync)+node:child_process(spawn,用于npm install)+node:url(fileURLToPath)npm create aalis的 CLI,在工作区外脚手架一个可运行的独立项目(生成目录、aalis.config.yaml、安装依赖),同样无法、也不应通过 storage 服务。
浏览器自动化
plugin-tool-browser/src/index.ts—node:fs(existsSync)+node:path+node:url(fileURLToPath)+node:crypto(createHash)+node:net(createServer/connect/isIP) 动态import('puppeteer')后需要对 puppeteer 包做包内文件探测(找到内置cli.js、检测 Chrome 是否已下载),属于 puppeteer 包的本地资产管理需求,storage 服务覆盖不到。Chrome 安装本身(原先的execFileSync)已迁移到ProcessService.execFile。node:net用于浏览器网络闸:在127.0.0.1起 SOCKS5 服务接收浏览器的全部 TCP 连接,判定通过后由插件进程连接目标。node:crypto的createHash取截图 PNG 内容的 sha256 作文件名。
记忆后端
plugin-memory-sqlite/src/index.ts—node:fs(statSync)+node:sqlite(DatabaseSync) 数据库用 Node 自带的node:sqlite,经process.getBuiltinModule取得:插件源码也在 vitest 里加载,而 vitest 解析不了静态的import 'node:sqlite'。node:sqlite不在受限清单里,不需要 lint 例外。它直接打开resolveLocalPath给出的本地文件,取这个文件的身份与开库属于同一层。开库后用statSync取设备号与 inode,按文件身份判断两个实例是否打开了同一个库:大小写不敏感的卷上只差大小写的两个路径、指向同一文件的硬链接,按路径字符串比较都认不出来。没有为此给StorageService增加文件身份字段,因为目前只有这一个消费者。这个文件不在overrides[1]里整条关掉noRestrictedImports:biome.json为它单独写了一段 override,node:fs用allowImportNames只放行statSync,其余受限的node:*照常报错。biome 的 override 会整体替换规则选项,这段 override 因此重抄了overrides[0]的全部路径,改overrides[0]时要一并改它。
会话服务
plugin-session-server/src/approvals.ts—node:fs/promises(appendFile/mkdir)+node:crypto(createHash) 审批的审计是只追加的 JSON 行文件,落在 host-paths 给的数据目录state/audit/approvals.jsonl;StorageService没有追加写,整份重写会让每条记录的代价随文件变大。内容哈希要同步算:请求在发起的同一拍里定下去留与排队次序,Web Crypto 的异步摘要会让并行的请求按完成先后乱序。两处都只在这个文件里,用行内的biome-ignore写明理由,不整包放开。plugin-session-server/src/files.ts—node:crypto(createHmac/timingSafeEqual) 条目里的文件句柄是 HMAC 签名,条目在拼出的同一拍里就要带上它:实时推送按seq排序,历史按页同步归并,Web Crypto 的 HMAC 只有异步接口。核验用定长比较。密钥取自全局的crypto.getRandomValues。同样用行内的biome-ignore写明理由。
触及
plugin-reach—node:fs/promises(realpath/lstat/stat/readdir/readFile/writeFile/rename/mkdir/rm/access)+node:os(homedir/userInfo) 它读写的路径不在存储根内(安装集与链接装进来的包、用户主目录、仓库的.git),本会话临时区要按 0700 建、按 lstat 不跟随链接清扫,存储服务没有这些能力;存储单元之后,存储服务读写宿主路径时要经触及凭据判,而凭据正是本插件签发的。各处 import 用行内的biome-ignore写明理由。三张受保护清单按真实路径比较:安装集、用户主目录与经链接装进来的包先取真实路径;仓库的 git 目录从绑定目录往上读.git;本会话临时区按 0700 建、随会话删、按修改时间清扫;沙箱里 npm 的用户配置是用户主目录.npmrc去掉 npm 认证键的副本,以随机名独占创建再改名到位,不跟随沙箱换上的符号链接。owner 的 login shell 取自SHELL,没有时取操作系统账户登记的(userInfo);拉起它走ProcessService.execFile。
宿主运行时
@aalis/runtime—node:fs/node:fs/promises(文件日志、node_modules插件加载、配置读写与监听)+node:child_process(spawn)+node:module(createRequire)+node:crypto(randomBytes)+node:path/node:url独立部署宿主:从node_modules加载插件、读 YAML 配置、注入存储 / 进程等环境专有件。它本身就是宿主,直接使用node:*是本职——core 的环境无关红线正是靠它把副作用兜在外层。randomBytes(providers.ts)为配置文件原子写的临时文件名生成随机段。
历史遗留迁移已完成:
plugin-file-reader的 sha256 hashId 改 Web Crypto;plugin-asr-openai/plugin-llm-ollama的file://读取改走ProcessService.readExternalFile;plugin-tool-browser的 Chrome 安装改走ProcessService.execFile。新代码不要再加入直接动态 importnode:fs|crypto|child_process——如有需要,请按 § 4 流程申报新的例外。
4. 增加新例外的流程(仅当你想合入社区 / 让 lint 通过)
- 先评估:能否扩展
StorageService/ProcessService接口(如为ProcessService增补readExternalFile这样的方法)使你的需求落到既有服务里?能就走服务——通常对集成测试和后端切换更友好。 - 若确实需要新插件直接持有某个
node:*:- 在本文件 § 3 增加条目,写清「引入的模块 + 必要理由」。
- 在
biome.jsonoverrides[1].includes增加对应路径。 - 必要时同步更新
overrides[0]中该模块的提示文字(保持错误信息与白名单一致)。
- CI 通过 = 三处自洽。审阅者据此判断是否真的「必要」。
第三方插件如果不打算合入主仓库,完全可以在自己的 lint 配置里把 noRestrictedImports 关掉,跳过这套流程。
5. 与其它文档的关系
- 架构总览:
docs/architecture.md§ "环境无关 core"。 - StorageService 接口与沙箱说明:
packages/api-storage/src/index.ts顶部 JSDoc。 - ProcessService 接口与方法:
packages/api-process/src/index.ts。 - 插件作者指南:
docs/plugin-author-guide.md—— 新作者首先建议看的就是本文件。