Skip to content

plugin-tool-system — 系统工具集 ​

包名: @aalis/plugin-tool-system
源码: packages/plugin-tool-system/src/index.ts

概述 ​

机器交互基础工具集,提供 Shell 命令、文件操作、系统信息和 HTTP 请求等工具。四类工具各有独立的配置开关,但对外只暴露 system 一个工具分组(见下文工具组)。

插件声明 ​

typescript
export default definePlugin({
  name: '@aalis/plugin-tool-system',
  uses: {
    tools: optional(tools),
    logger,
    config,
    commands: optional(commands),
    storage: optional(storage),
    process: optional(processService),
  },
  apply(caps) { /* 见源码 */ },
});

配置 ​

字段类型默认值说明
workingDirectorystring'workspace:/'初始工作目录:进程启动时的初始 cwd(unix 心智模型)。agent 可用 cd 工具在会话内切换,不会写回本配置。shell 以本项初始值为基准、不受 cd 影响;code-runner 使用它自己的 workingDirectory 配置。
shellobject—Shell 工具
shell.enabledbooleantrue启用 Shell 工具
shell.maxOutputSizenumber65536最大输出字节:exec 结果里 stdout、stderr 各自保留的上限
fileobject—文件工具
file.enabledbooleantrue启用文件工具
file.maxReadSizenumber1048576最大读取字节
file.maxSearchBytesnumber1048576单次搜索最大扫描字节
file.maxWriteSizenumber10485760最大写入字节
file.allowedRootsmultiselect["workspace","tmp"]允许访问的存储根:默认仅 agent 工作区(workspace/tmp),不含 data 等系统根(防裸读 data:/users.json 等)。设为 * 放开全部 readable 根;也可显式列出根名。写入/删除仍受各根自身权限限制。
systemobject—系统工具
system.enabledbooleantrue启用系统工具
httpobject—HTTP 工具
http.enabledbooleantrue启用 HTTP 工具
http.defaultTimeoutnumber30000默认超时 (ms):http_request 的默认超时;http_download 不另设超时,等多久由工具执行器的让出时限管
http.maxResponseSizenumber1048576最大响应字节

工作目录、各工具启用开关及 file.allowedRoots 显式值无效时,插件拒绝激活;不丢弃坏根名后继续提供文件工具。显式 allowedRoots: [] 沿用默认的 workspace / tmp 两根。

路径与 cwd 心智模型 ​

所有 file_* 工具的 path 参数遵循 unix shell 风格:

  • 完整 storage URI(aalis:/packages/core、workspace:/notes/a.md)→ 直接定位
  • 相对路径(packages/core、./a.ts、../plugin-tools)→ 基于当前 session 的 cwd 解析
  • 宿主机绝对路径(/Users/...、C:\...、C:/...)→ 一律拒绝(Windows 盘符文法与单字母根名冲突,工具路径输入侧一律按盘符处理,单字母根名在工具里不可达)

cwd 工具返回当前目录 + 所有可用 storage 根的清单(含读/写/删权限),调用一次即可看清"我在哪、能去哪"。cd 工具切换当前 session 的 cwd(仅内存,进程重启回到 workingDirectory 配置值,不写配置文件)。

shell 以本插件 workingDirectory 的初始值为基准,不受 cd 影响;code-runner 使用它自己的 workingDirectory 配置。子进程模型决定了两者只能在文件系统型根下执行。

工具组 ​

本插件只向 tools 服务注册一个工具分组 system(标签「系统工具」),下面四类工具全部落在这一个分组里:

源码模块工具配置开关
shellShell 命令执行shell.enabled
file文件读写操作file.enabled
system系统信息查询system.enabled
httpHTTP 请求http.enabled

shell / file / system / http 是配置开关与源码模块名,不是分组名。 平台档的 enabledToolGroups 只认 system 这一个组名:写 ['file'] 匹配不到本插件的任何工具(静默、无告警;无分组的通用工具不受分组闸影响,仍照常可见);写 ['system'] 则一次放开 exec、file_write、http_request 等全部工具。要按类收窄,用上表的配置开关关掉整类,而不是在平台档里按模块名列。

shell 工具:exec 的停止与转后台 ​

shell 工具只有 exec。exec 声明命令类展示(命令在 command,工作目录在 cwd,退出码在结果的 exitCode,不为 0 时工具卡记为失败、写明退出码),file_edit、file_write、file_append 声明改文件类展示(路径在 path;file_edit 删的一侧是 oldText、加的一侧是 newText,另两个只有加的一侧 content),会话的工具卡与审批卡据此显示命令或 diff,见 api-tools。

exec 声明可转后台(background,让出时限用执行器的默认 30 秒),等多久交给 plugin-tools 的执行器:在前台等满让出时限还没结束,执行器把这次调用登记成后台任务、先回给模型,命令照常跑,正常结束时向起它的会话发结束通知;模型可以用执行器补的 waitSeconds 改前台等多久,用 runInBackground 立即转后台。查看输出、等待与停止用执行器的后台任务工具(background_list、background_output、background_wait、background_stop)。参数里没有超时,没有「到时就杀」;不在对话回合里的调用(编排的工具节点、mcp-server)到让出时限还没结束,由执行器中止并回报已有输出。

命令要按前台写法给出,不要再加 & 或 nohup:命令自己放到后台的子进程在 shell 退出之后不受管理,停不掉,也没有结束通知。长驻进程(开发服务器、文件监视)正常运行时不会结束,也就没有结束通知,是否就绪看它的输出。

exec 只认执行器给这次调用的中止信号:在前台时随回合中止(WebUI 的停止键、禁言、停机等停下这一轮),转到后台之后随停止、工具撤回登记与执行器停用中止。中止时命令按进程组停掉:POSIX 先 SIGTERM、宽限 2000ms 后 SIGKILL,Windows 立即结束整棵进程树。结果只在命令确实是被信号结束时标为中止,因为这条结果随工具调用落库,下一轮模型看到「已中止」可能把已经生效的命令(如 git push)再跑一遍:

情形结果
中止时命令被信号结束aborted: true、message: '命令已中止',带已收集的输出
中止前命令已自行退出(包括孙进程占着管道的收尾窗口),或结束时没有终止信号(Windows 下强制结束可能如此)按实际退出码与输出回报,另加 note: '调用已中止'
起进程前已中止不执行命令,返回 aborted: true、同一条 message 与 exitCode: -1

中止只打原进程组:命令里主动脱离进程组的后代(setsid、以 detached: true 起的子进程等守护化写法)会活下来。

边跑边写输出 ​

exec 在命令运行期间把 stdout 与 stderr 按到达顺序写进执行器给这次调用的输出口(ToolCallContext.output,见 api-tools)。两路各自流式解码,多字节字符被拆在两次读取之间也不出乱码。推给会话客户端的工具卡、转到后台之后留进任务的输出缓冲,都由执行器做(见 plugin-tools)。工具结果照旧在命令结束时一次给出、落库、交给模型,stdout 与 stderr 各自不超过 shell.maxOutputSize 字节。

http 工具:请求正文、响应与下载 ​

http_request 的请求正文有两个参数,只给一个:body 是字符串,原样发出,不补任何请求头(fetch 按字符串给 text/plain;charset=UTF-8);json 是对象,按 JSON.stringify 发出,请求头里没写 Content-Type(不分大小写)时补 application/json;给的不是对象(字符串、数组、null)报错,字符串正文用 body。两个都给的报错。响应正文是不是文本按内容判:按服务端声明的 charset(Content-Type 的 charset 参数,没声明的与认不出的按 UTF-8)解得开、不含 NUL 字节的给出正文(服务端声明的类型不常见也照样给),否则(二进制,或编码与声明的不符)只给「不是 <编码> 文本」与字节数、声明的 Content-Type;声明是 application/json 的正文美化后给出。Content-Length 超过 http.maxResponseSize 的不读,取消响应体,不占着连接;http_download 上游回错误状态时同样取消。

http_download 声明可转后台:下载只认执行器给这次调用的中止信号,参数里没有超时,等多久同样由执行器的让出时限管;中止时下载停住、不写盘。http.defaultTimeout 只给 http_request。

file 工具:exclude/include 与 file_search 行为 ​

file_search 与 file_tree 接受可选参数 exclude、include(数组,glob 字符串)。

默认排除(DEFAULT_EXCLUDE_PATTERNS) ​

**/node_modules/**   **/dist/**     **/build/**      **/out/**
**/.git/**           **/.next/**    **/.nuxt/**      **/.turbo/**
**/.cache/**         **/coverage/** **/.venv/**      **/__pycache__/**

调用方传入 exclude 时替换默认列表(不追加);exclude: [] 关闭全部默认,全量搜索。 include 仅做正向白名单(命中即保留),未指定时所有未被 exclude 的条目都纳入。

Glob 语义 ​

  • * 匹配单段内任意字符(不跨 /)
  • **/ / /** 匹配零或多层目录(修复了之前"至少一层"导致 node_modules/** 不命中根目录下 node_modules/foo.js 的 bug)
  • ? 单字符(不跨 /)

目录级早跳 ​

collectFiles() 在递归进入目录前先用 exclude 测试目录路径,命中则整棵子树短路。 对 node_modules / .git 等大目录是 O(命中即跳),不会逐文件遍历再丢弃。

截断提示 ​

file_search 结果默认 50 条,最多 200 条(maxResults);超出时 advice 字段会建议缩小搜索范围或追加 include。

目录模式下读不出来的文件(权限不足、枚举之后被删)不计入命中,但也不会被静默跳过:返回体带 skippedFiles 计数,advice 里明确提示「不要据此断言找不到」。此前这类跳过完全无痕—— 该文件一行未扫,结果却照常给出 matchCount 且 truncated 仍为 false,模型会把这个 「非截断」的可信信号当成「不存在」。

单行字节上限 ​

按行读取的工具(file_read 的行范围、file_search)对单行另有字节上限,取自 file.maxReadSize / file.maxSearchBytes:超上限的行按字节截断后交付,余下部分丢弃到下一个换行。 无换行的大文件(单行 JSON、压缩产物)因此不会被整行载入内存——否则预算判定发生在整行已成型 之后,上限形同虚设,而超过 V8 单字符串上限时抛出的 RangeError 是从流的 data 事件栈上同步 抛出的,工具侧的 try/catch 接不住,会一路逃到宿主的未捕获异常处理器把进程打掉。

目录模式的续搜协议 ​

目录搜索的预算(maxResults / maxSearchBytes)跨文件累加,耗尽即 truncated: true。此时结果里一定带 nextStartFile(相对所搜目录的文件路径)与 nextStartLine:下一次调用传同一个 path 加这两个值,并把本次的 pattern / isRegex / ignoreCase / exclude / include 原样重传,就从断点原地接着扫,不重复也不遗漏。这几个参数任一不同则断点失效:文件集或匹配规则一变,断点指向的位置就不再是同一个(漏传 isRegex 会静默按字面量少命中,漏传 exclude 则断点文件可能已被排除,直接报 startFile 未找到)。

  • startFile:从这个文件开始扫,遍历序(目录优先、字典序、与 collectFiles 同源)在它之前的文件整体跳过;只在目录搜索生效,不在该目录下时直接报错而非静默从头重扫
  • startLine:文件模式下是文件内起始行;目录模式下须与 startFile 同传,表示在 startFile 内的起始行,其余文件一律整篇扫

断点的取法:

耗尽方式nextStartFilenextStartLine
maxResults 用完停下的那个文件最后一条命中行 + 1
maxSearchBytes 用完停下的那个文件最后扫描行 + 1

断点只有这两种:预算一耗尽,停在哪个文件就从那个文件续,不存在「从下一个文件第 1 行开始」的情形。预算正好在某文件最后一行用完时,nextStartLine 会越过该文件末尾:下次续搜只是多开一次空文件,命中集不受影响。

正则模式体检 ​

isRegex: true 时模式先过体检再编译。体检拦下已知的几类灾难性写法(嵌套量词、含量词的分支重复、无界量词堆叠),不是完备保证——判据是语法形状而非真实回溯代价,放行的模式里仍可能有慢写法。被拒的几类:

  • 嵌套量词:重复量词作用在「内部含无界量词或分支」的分组上((a+)+、((a+)a)+,子分组的标记向父层传播);内部全有界的嵌套((?:[0-9]{1,3}\.){3}[0-9]{1,3}、(\d{2}:){3})重复次数封顶为常数,照常放行
  • 含量词的分支重复((a+|b)*);无内层量词的分支重复((?:foo|bar)+)一并拒掉,分支是否重叠无法便宜判定
  • 无界量词(+ * {n,})多于 5 个(a+a+a+a+a+a+b);有上限的 {n} / {n,m} 不计入这一项
  • 模式超过 500 字符、量词总数多于 20 个(a?a?a?…x 这类堆叠同样能爆)

V8 的正则回溯是同步执行的,超时与 abort 都打不断,(a+)+$ 这类模式每多两个字符耗时翻数倍,足以把整个进程冻住——故取舍是被拒只需换个模式重试,冻死进程则不可恢复。字面量、字符类、单层量词、锚点、未加量词的分组与分支、.*foo.*bar.* / ^\s*//.*$ 这类常态写法照常放行。需要纯文本语义时传 isRegex: false,模式整体按字面量处理,不过体检。

file 工具:改动类操作的串行闸 ​

file_edit / file_append / file_write / file_move / file_delete 按 storage URI 排队执行。前三者是「读—改—写」,而 agent 同一轮的 tool call 并行发起:无闸时两次改同一文件会各读到同一份原文、后写者盖掉前写者,两边都回「成功」。闸只按 URI 串行,不同文件仍并行;file_move 按字典序依次取两端的键,避免交叉移动互等。

file_append 读原文失败时不再当成空文件:只有「文件不存在」(ENOENT)才走创建,读原文的其余错误(不可读的根、瞬时 IO 错)一律原样返回错误——否则整篇写回会把原文静默截断成只剩追加的部分。宁可这次追加失败,也不拿空串当原文。

共享 runtime 工具 ​

storage URI 规范化(toStorageUri / resolveAgainstCwd / parseStorageUri)已抽取到 @aalis/api-storage,SSRF 私网判定(isPrivateHost / isPrivateAddress)已抽取到 @aalis/util-network-guard;本包内部以及 plugin-tool-browser / plugin-tool-code-runner 都直接复用,不再各写一份。api-tools 现为纯契约包,原 utils 已删除。

指令 ​

  • /tools — 列出本插件真正注册了的工具组(shell / file / system / http 里的;开关开着、却因缺 storage 或 process 跳过注册的不列)

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