---
title: 工具 API
description: 内置工具清单与自定义工具的定义方式。
section: dev
order: 50
updated: 2026-11-05
---
# 工具 API
## 一次调用是怎么运行的
- **工具在沙盒里运行**(Deno)。工具的代码只能读自己这个 gem 的目录和它下载的资源文件(见下文),不能直接写文件、不能直接联网、读不到环境变量。除此之外的一切——网络、存储、文件、知识库、模型——都要经过 `ctx`。gem 自己的存储(`ctx.data`、`ctx.storage`)、文件(`ctx.files`)和文档抽取(`ctx.extract`)每个工具都有;其余的取决于清单里申请并被用户批准的权限。每个 gem 的代码在它自己的沙盒进程里运行,不和别的 gem 共用;沙盒里不能加载 npm 包(`npm:` 开头的导入都会被拒绝)。
- **不要指望两次调用之间内存里的东西还在。** 沙盒有时会被下一次调用复用,有时不会。要保留的状态请写进 gem 自己的[存储](/zh/dev/storage.md)。
- **一次调用从头跑到结束,不跨应用重启。** 应用退出、或用户切换组织时,正在执行的调用会中断,之后不会接着跑。需要等待、需要用户审批、或者要在重启后继续的事情,请写成 SOP:它的每一步结果都有记录,可以挂起再恢复,见[事件与触发](/zh/dev/events.md#触发之后一次运行)。
- SOP 调用的工具在应用崩溃后可能被重跑一次。有外部副作用的工具请用 `ctx.idempotencyKey` 去重。
- **参数按 `inputSchema` 检查**:调用方给的参数不合你的 `inputSchema`(类型不对、缺必填项、`additionalProperties: false` 时多了字段),工具不会运行,调用方拿到说明哪里不对的错误。`inputSchema` 本身写错、编译不了的,打包和安装时就被拒绝。不支持 `pattern`、`patternProperties` 和 `uniqueItems`——检查它们可能让应用卡住;这类要求请在工具代码里自己检查。可选参数传 `null` 等于没传。
- **返回值序列化成 JSON 后最多约 100 万字符**(1,048,576)。更多的内容请写进文件(`ctx.files`)或[存储](/zh/dev/storage.md),返回一个摘要。
- **`ctx.http.fetch` 没带上凭据时会说原因**:请求指定了 `connection`、但凭据没有注入(主机或路径不在授权里、不是 https 等),返回值里有 `credentialDenied: { reason, host }`,对方通常会接着回 401。
## ctx.files
每个工具都有 `ctx.files`,不用申请:读写这次运行的文件夹,和这次调用所属对话的文件夹(规则见[权限模型](/zh/dev/permissions.md#文件不是权限))。所有方法都是异步的。
```js
export default async function (input, ctx) {
// input.table 是 "sales.csv",input.target 是 "inbox/":AI 传进来的都是对话文件夹里的相对路径
const csv = await ctx.files.read('workspace:' + input.table)
const dir = 'workspace:' + input.target
for (const f of await ctx.files.list(dir)) {
if (f.type === 'file' && f.name.endsWith('.pdf')) {
await ctx.files.move(dir + f.name, dir + 'PDF/' + f.name)
}
}
await ctx.files.write('output/report.md', '# 本月汇总\n…')
return { ok: true }
}
```
| 方法 | 说明 |
| --- | --- |
| `list(path?)` | 文件夹里的内容:`[{ name, type: 'file' \| 'folder', size, modified }]`。不写 `path` 是运行文件夹本身,还没写过东西时返回空数组。 |
| `stat(path)` | 一项的信息,不存在时返回 `null`。 |
| `read(path, { encoding? })` | 文件内容。`encoding` 为 `'utf8'`(默认)或 `'base64'`。 |
| `write(path, content, { encoding? })` | 写文件,上级文件夹不存在时自动建,写入是原子的。覆盖对话文件夹里已有的文件时,旧版本先存进文件历史,用户回退对话时能恢复。 |
| `mkdir(path)` | 建文件夹。 |
| `move(from, to)` | 移动或改名,也可以在运行文件夹和对话文件夹之间移动。目标已经存在时报错,不会覆盖。 |
| `delete(path)` | 移到用户的回收站,并在文件历史里留一份(100 MB 以内),用户回退对话时能恢复。没有回收站的环境(无界面运行的服务器版)是永久删除,历史里的副本照样能回退。 |
- **路径**:普通的相对路径在运行文件夹里;`workspace:<相对路径>` 在这次调用所属对话的文件夹里(`workspace:`、`workspace:inbox/发票.pdf`)。gem 永远看不到绝对路径;用 `..` 或文件夹里的符号链接跳出去会被拒绝。
- **对话文件夹里以 `.` 开头的文件和文件夹对 gem 不存在**:列不出、读不到、写不进。
- **没有对话的调用**(页面发起的调用、没选工作文件夹的定时 SOP)用 `workspace:` 会报 `E_PERM_DENIED`。
- **每次读或写最多 20 MB**,内容要经过沙盒的通道传递。
- 没有生成 PDF 的接口,报告请写成 Markdown 或 HTML,放在 `output/`。
- SOP 自己不读写文件,它把路径交给工具;`.wasm` 工具没有 `ctx`。
## ctx.data 和 ctx.storage
gem 自己的存储。每个工具都有,不用申请(见[权限模型](/zh/dev/permissions.md#存储不是权限)):
| 种类 | 接口 | 用来做什么 |
| --- | --- | --- |
| `data` | `ctx.data` | 首选这个:按清单 [`data`](/zh/dev/manifest.md#data) 段声明的集合,以及 Log、文档、Blob 和引用。 |
| `kv` | `ctx.storage.kv` | 小设置、同步游标这类字符串键值。 |
| `rawSqlite` | `ctx.storage.sql` | 自己写 SQL 的私有数据库,表结构写在包里的 `db/schema.sql`。只在 `ctx.data` 做不到时用。 |
```js
export default async function (input, ctx) {
const notes = ctx.data.collection('notes')
const { id } = await notes.insert({ title: input.title, status: 'open' })
return { id }
}
```
- 接口、查询和索引规则、事务、迁移、上限和错误码见[数据与存储](/zh/dev/storage.md)。
- 声明 `sideEffect: "read"` 的工具不要写任何存储:计划模式会在用户批准之前运行它,平台不检查,见[权限模型](/zh/dev/permissions.md#存储不是权限)。
- SOP 没有 `ctx.data`,要通过 gem 自己的工具读写,见 [SOP 怎么访问数据](/zh/dev/storage.md#sop-怎么访问数据)。
## ctx.email
清单顶层的 `permissions` 和这个工具自己的 `permissions` 里都写了 `"email": { "read": true }` 的工具会拿到 `ctx.email`,读用户的邮件(Gem Studio 草稿里是顶层的 `"email": true`)。**只能读用户在 gem 页面上允许的邮箱**:装好时一个都没有,用户可以选几个账号,或者「所有账号」(以后新添加的也算)。传了没被允许的账号会报错。只能读,不能发信。
```js
export default async function (input, ctx) {
// 上次处理到哪里,存在 gem 自己的存储里
const since = Number((await ctx.storage.kv.get('cursor')) ?? 0)
const { messages, cursor } = await ctx.email.listSince({ since, limit: 20 })
for (const m of messages) {
const full = await ctx.email.getMessage({ accountId: m.accountId, id: m.id })
// 用 full.body 做事……
}
await ctx.storage.kv.set('cursor', String(cursor))
return { processed: messages.length }
}
```
例子用 `ctx.storage.kv` 记下处理到哪里,它是 gem 自己的存储,不用申请。
| 方法 | 说明 |
| --- | --- |
| `listSince({ accountId?, since?, limit? })` | 比 `since`(邮件日期,毫秒时间戳,默认 0)新的邮件,旧的在前,最多 `limit` 封(默认 20,最多 200)。不写 `accountId` 是所有允许的账号。返回 `{ messages, cursor }`:每封是 `{ accountId, id, subject, fromAddr, toAddrs, date }`,**不带正文**;`cursor` 是看到的最新日期,下次作为 `since` 传回来。 |
| `getMessage({ accountId, id })` | 读一封邮件的全文(异步):上面的字段,加上 `body`(正文的纯文本)和 `attachments`(每个附件的 `filename`、`mimeType`、`sizeBytes`,不含内容)。`id` 来自 `listSince` 或 [`email.onNewMessage` 事件](/zh/dev/events.md#emailonnewmessage)。 |
- 新邮件触发的 SOP 拿到的事件里只有信封,没有正文:SOP 把 `accountId` 和 `id` 交给一个这样的工具读正文,见[事件与触发](/zh/dev/events.md#emailonnewmessage)。
## ctx.resources
清单里声明了 [`resources`](/zh/dev/manifest.md#resources) 的 gem,它的每个工具都会拿到 `ctx.resources`。不需要申请权限。
```js
export default async function (input, ctx) {
const modelPath = await ctx.resources.path('ocr-model')
const model = await Deno.readFile(modelPath)
// 用 model 做事……
return { ok: true }
}
```
`ctx.resources.path(id)` 返回这个文件在本机的绝对路径,`id` 是清单里写的那个。
- **文件还没下载时,这次调用会先下载,然后才返回**,大文件可能要等很久。希望装好就能用的文件,在清单里标 `"required": true`,让它在安装后提前开始下载。多个调用同时要同一个文件时,只下载一次。
- **拿到路径后自己读。** 用 Deno 的文件接口(`Deno.readFile`、`Deno.readTextFile`、`Deno.open`)。工具只能读,不能改这些文件;也读不到别的 gem 的资源。
- **每次用的时候都问一次路径,不要自己存下来。** 文件内容变了(新版本换了 `sha256`),路径也会变。文件已经在本机时,这个调用立刻返回。
- **失败时抛出错误**:`id` 没有在清单里声明;或者 `url` 和所有镜像都没能下载到校验通过的文件——错误信息里会逐个写明每个地址失败的原因。gem 不会因此被卸载或停用,下一次调用会重新尝试下载。
- SOP 自己不能读文件,要通过一个工具来用。SOP 调用的工具在等下载时,这段时间计入这次运行的活跃时间(默认上限 10 分钟)。
- `.wasm` 工具没有 `ctx`,用不了资源文件。