Skip to content

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-storageplugin-storage-localStorageService 的当前实现
plugin-process-localreadExternalFile():用于 OneBot 实现端动作应答里交回的 /tmp/... 等不在任何 storage 根内的绝对路径
plugin-webui-serverexistsSync 探测前端 dist 目录(位于工作区外、由 webui-client 提供)
create-aalis-plugin脚手架 CLI,物理生成新插件目录
plugin-tool-browser探测 puppeteer 包内 cli.js 与 Chrome 是否已下载(包资产管理)
plugin-memory-sqlite/src/index.tsstatSync 取数据库文件的设备号与 inode,判断两个实例是否打开了同一个库
plugin-session-server/src/approvals.tsappendFile / mkdir 追加审批审计:只追加的记录,存储服务没有追加写
plugin-usage-stats/src/index.tsappendFile / mkdir / createReadStream 追加与按行读回用量账本:只追加的记录,存储服务没有追加写与按行读
plugin-reach宿主路径的真实路径(安装集、用户主目录、守护进程的入口、链接装进来的包、清单里各规则的根)、按 .git 找仓库的 git 目录、本会话临时区(0700、清扫)、沙箱用的 npm 配置副本:这些路径不在存储根内,临时区要按 0700 建、按 lstat 不跟随链接清扫,存储服务没有这些能力;存储单元之后存储服务的宿主路径根要经触及凭据,而凭据正是本插件签发的
node:child_process走 @aalis/api-process(spawn/execFile)plugin-process-localProcessService 的当前实现
node:http / node:https出站建议用 fetchplugin-webui-serverHTTP 入站服务 + WebSocket 升级
plugin-mcp-serverHTTP/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-reachhomedir / userInfo:拒读清单与落点按用户主目录生成,子进程环境取 owner 的 login shell
node:sqlite消息与元数据走 @aalis/api-memory(memory 服务)plugin-memory-sqlite/src/index.tsMemoryService 的 SQLite 实现
node:crypto推荐 Web Crypto(crypto.subtle.digest / crypto.randomUUID / crypto.getRandomValues)plugin-tool-browsercreateHash 取截图 PNG 内容的 sha256 作文件名
plugin-storage-localrandomBytes 生成原子写临时文件名里的随机段
runtime/src/providers.tsrandomBytes 生成配置文件原子写临时文件名里的随机段
plugin-session-server/src/approvals.tscreateHash 同步算审批的内容哈希
plugin-session-server/src/files.tscreateHmac / timingSafeEqual 同步签发与核验文件句柄
plugin-mcp-servercreateHash / timingSafeEqual 定时比较访问令牌
node:buffer / node:path直接使用全部纯函数 / Web 标准不覆盖,无 I/O
node:urlfileURLToPath / 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。新代码不要再加入直接动态 import node:fs|crypto|child_process——如有需要,请按 § 4 流程申报新的例外。

4. 增加新例外的流程(仅当你想合入社区 / 让 lint 通过) ​

  1. 先评估:能否扩展 StorageService / ProcessService 接口(如为 ProcessService 增补 readExternalFile 这样的方法)使你的需求落到既有服务里?能就走服务——通常对集成测试和后端切换更友好。
  2. 若确实需要新插件直接持有某个 node:*:
    • 在本文件 § 3 增加条目,写清「引入的模块 + 必要理由」。
    • 在 biome.json overrides[1].includes 增加对应路径。
    • 必要时同步更新 overrides[0] 中该模块的提示文字(保持错误信息与白名单一致)。
  3. 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 —— 新作者首先建议看的就是本文件。

文档对应 dev@c4e046e41,core 0.21.0,构建于 2026-11-02
基于 MIT 许可发布