# Notochord — 全文 # 下载 Notochord **Notochord 目前还在开发中,暂时没有可下载的安装包。** 应用做完以后,将以 **MIT 协议开源发布**,同时提供 macOS、Windows 和 Linux 的安装包,届时会在这一页给出下载地址。 ## 现在能看什么 - [首页](/zh/index.md):Notochord 是什么、为什么这样设计 - [使用文档](/zh/guide/index.md):应用的使用方式,包括项目、对话、模型、gem 和组织 - [开发者文档](/zh/dev/index.md):怎么给 Notochord 开发 gem——清单、权限、数据与存储、事件与触发 文档描述的是开发中版本的设计,细节可能随开发调整。 # Notochord,你的私人AI团队 > Notochord 正在开发中,应用发布后将以 MIT 协议开源。这里先展示它的设计和文档。 [下载](/zh/download.md) · [五分钟上手](/zh/guide/quickstart.md) · [开发者文档](/zh/dev/index.md) ## 安装跟普通软件一样简单,但一点也不含糊 不需要装 Docker、开虚拟机、折腾虚拟化——下载装上就能用。但"简单"不等于"不设防": - **脚本执行有沙盒**:AI 写的脚本在 Deno 沙盒里跑,碰不到沙盒外的系统。 - **联网有规矩**:gem 只能连它申请过的网址;你的电脑和家里、公司的内网,AI 和 gem 默认一个都连不上,只有你列出的地址才放行;外网还可以按白名单或黑名单管——就算某个依赖库被投毒,也翻不出这道墙。 - **密钥 AI 摸不到**:Notochord 有一套专门设计的凭证系统,AI 只拿到「用某个连接发请求」的许可,密钥本身它看不到、也传不出去。 - **数据只留在你的电脑上**:所有对话、文档、数据都加密存在本机,不会上传到任何云端服务器。 ## AI供应商你说了算 OpenAI、Anthropic、Gemini 这些云端模型能接,本地部署或内网私有模型也能接,随时切换,不绑定任何一家供应商。 ## 把重复的工作流程化(SOP) 把一套常做的流程写成 SOP(标准作业流程),以后一键跑完,不用每次重新教它该怎么做。复杂的活儿还能拆给多个 AI 智能体一起协作、在后台异步执行,不用你一直盯着。 ## 自己动手,或者让内置 AI 帮你做 Notochord 提供了丰富的底层接口——工具、技能、SOP、智能体都可以自己开发。没写过代码也没关系,内置的 AI 助手可以陪你一起完成开发。 ## 用得越久,越懂你 内置个人知识库,你的使用习惯和偏好会不断积累下来,AI 会越用越顺手。 ## Gem 商店 任何人都可以开发并发布自己的工具、技能、智能体,分享给其他人用。未来还会支持付费与分成,个人开发者也能把经验和知识变现。 ## 开源,个人和商业都能免费用 Notochord 将以 MIT 协议开源,代码接受公众审核。个人使用、商业使用都不受限制。 ## 为什么叫 Notochord Notochord 是「脊索」——无脊椎动物(比如小龙虾)演化出脊索之后,才有了更复杂的神经系统和更精细的动作能力,一路演化成脊椎动物。这也是这个名字想表达的:希望做的是 AI 智能体从简单反射走向复杂协作、自主行动的那根「脊索」。 # 开发者文档 一个 Gem 就是一个文件夹:一份清单(manifest),加上可选的工具、agent(每个一份带头部的提示词文件)、SOP、页面、一份给 AI 看的 skill、一份给人看的介绍文档(`docs/`,将来是商店里的介绍页)、数据声明和事件订阅。装好以后,gem 的页面上列出它能做什么;需要用户配置的(账号、邮箱、知识库)在用户打开之前都是关着的。 ## 从这里开始 1. [Gem 是什么](/zh/dev/gem-overview.md) — 结构、生命周期、与会话的关系 2. [清单参考](/zh/dev/manifest.md) — manifest 的每个字段 3. [权限模型](/zh/dev/permissions.md) — 网络、模型、知识库、邮件、账号如何申请与校验;存储、文件、文档抽取不用申请 4. [数据与存储](/zh/dev/storage.md) — 用 `ctx.data` 存 gem 自己的数据:集合、Log、文档、Blob,以及迁移、加密与隔离 5. [知识库 API](/zh/dev/knowledge.md) — 通过 `ctx.knowledge` 读写用户的事实与文档 6. [工具 API](/zh/dev/tools.md) — 内置工具与自定义工具 7. [事件与触发](/zh/dev/events.md) — 手动、定时、平台事件三种触发方式,各自的输入和限制 8. [本地开发与调试](/zh/dev/local-dev.md) — Gem Studio 与草稿流程 9. [打包与发布](/zh/dev/publishing.md) — 安装包的组成、安装时的校验、签名与信任级别 ## 机器可读的接口 - [`config.notochord.ai/v1/index.json`](https://config.notochord.ai/v1/index.json) — 客户端从 CDN 拉取的全部静态配置清单 - [`config.notochord.ai/v1/config.json`](https://config.notochord.ai/v1/config.json) — LLM 厂商、服务连接模板、邮箱预设、本地模型、站点链接 - [`config.notochord.ai/v1/gem-templates.json`](https://config.notochord.ai/v1/gem-templates.json) — Gem Studio 模板 - [`download.notochord.ai/gems/index.json`](https://download.notochord.ai/gems/index.json) — Gem 商店的列表(应用的商店页读它) - [`/llms.txt`](/llms.txt) — 整站的 AI 索引 # 使用文档 按顺序读完前两篇就能开始干活,其余按需查阅。 1. [五分钟上手](/zh/guide/quickstart.md) — 首次设置,建第一个项目,跑通第一次对话 2. [用项目组织工作](/zh/guide/projects.md) — 表格、画布、文件和对话放在一起;发布成 gem 3. [对话与会话](/zh/guide/chat.md) — 会话、附件、工具调用记录 4. [配置模型](/zh/guide/models.md) — 云端厂商与本地 GGUF,角色 5. [Gem:安装与使用](/zh/guide/gems.md) — 从商店安装、审批权限、卸载 6. [凭据与连接](/zh/guide/credentials.md) — API Key 怎么存、Gem 怎么用 7. [组织](/zh/guide/organizations.md) — 把工作和个人数据彻底分开 8. [设置项说明](/zh/guide/settings.md) 9. [常见问题](/zh/guide/faq.md) > 视频教程以 YouTube 外链形式放在对应章节,不占用站点体积。 # Gem 是什么 一个 gem 就是一个文件夹:一份清单(`notochord.json`),加上可选的工具(`tools/`)、agent(`agents/`,每个一份带头部的提示词文件)、SOP(`workflows/`)、页面(`pages/`)、一份给 AI 看的 skill(`skill/`)、一份给人看的介绍文档(`docs/`)、数据声明和事件订阅。 - **安装**:从商店或 Gem Studio 安装,先经过签名、完整性和声明的检查(见[打包与发布](/zh/dev/publishing.md)),通过才会装上。装好后,gem 的页面上列出它能做什么。 - **默认什么都碰不到**:需要用户配置的(账号、邮箱、知识库)在用户打开之前都是关着的(见[权限模型](/zh/dev/permissions.md))。 - **隔离**:每个 gem 有自己的数据文件夹和自己的运行进程,别的 gem 读不到。 - **使用**:用户在聊天里让 AI 用它,或者它自己的 SOP 按触发规则自动运行(见[事件与触发](/zh/dev/events.md))。 ## AI 怎么用到你的 gem 用户聊天时面对的是 Chat agent。它默认带着两个工具:`read_skill` 和 `tool_search`。用户可以在「设置 › AI 设置 › Agent」里去掉它们,也可以给别的 agent 加上。 - **AI 知道用户装了哪些 gem,以及每个 gem 对外提供什么。** 工具里有 `read_skill` 的 agent,系统提示里列着每个已安装的 gem,一个占一行:名字、id 和清单里的 `description`(写法见[清单参考](/zh/dev/manifest.md#描述));下面每个对外提供的项(标了 `expose: true` 的工具、SOP、agent)各占一行,写它的描述,SOP 标 (SOP),agent 标 (agent)。gem 没有对外提供的工具,AI 不知道它们存在。 - **要用某个 gem,就用 `read_skill` 读它的 id。** 结果里有:它的 skill(`skill/SKILL.md` 的全文、`skill/references/` 下每个文件的名字),以及这个 gem 对外提供的项。从下一步起,这些项都在 AI 的工具列表上,直到这次对话结束。没有 skill 的 gem 也这样打开,结果里只是没有 skill。 - **对外提供的 agent 就是一个工具。** AI 调用 `gem..({ task })`,把任务用一段话交给它;它在自己的会话里、用自己声明的工具、用用户给它选的模型完成,最后一条回复就是工具的结果。它只看到这段任务。怎么决定对外提供工具还是 agent,见[清单参考](/zh/dev/manifest.md#对外提供expose)。 - **参考资料按需再读。** AI 用 `read_skill` 加结果里列出的名字(`/skill/references/…`)读一个文件。读的同时这个 gem 的工具也会出现。写法见[清单参考](/zh/dev/manifest.md#skill)。 - **也可以按关键词找。** `tool_search` 在已安装的 gem(名字、描述,以及对外提供的项的名字和描述)和 Notochord 自带的工具里找,找到的(最多 5 项)也会出现在工具列表上。 - **读和找都不问用户。** 它们只是让 AI 看到 gem 提供了什么,AI 看过以后不一定会用。 - **第一次调用时问一次。** AI 第一次调用这个 gem 对外提供的某一项时,应用会弹窗请用户同意,问的是整个 gem;同意以后,这次对话里调用它对外提供的任何一项都不再问。同意只对这次对话有效,新对话里要再问一次。用户拒绝时,这次调用不执行,AI 会收到被拒绝的结果。 - **对外提供的项只能整个 gem 一起出现**,不能单独拿到其中的某一个。SOP 对 AI 来说就是一个工具,只是可能要等(等审批、等定时),这时它先返回一个运行编号。 - **没有对外提供的工具只有 gem 自己能调用**:它的 SOP、agent 和页面。别的会话调用它,不管工具列表上有没有,都会被拒绝,也不会问用户。 - 出现在 gem 列表里的是:至少对外提供一项的 gem,以及**什么都不对外提供但有 skill** 的 gem(有 `skill/SKILL.md`)——后者读了只有文字,没有要用户同意的东西。既不对外提供任何东西又没有 skill 的 gem 不会出现(它只靠自己的页面和定时 SOP 工作)。gem 自己的 agent 不受这些影响:它能用哪些工具由它的文件头部决定(见[清单参考](/zh/dev/manifest.md#agents)),也看不到这份 gem 列表。 # 五分钟上手 ## 1. 首次设置 一个组织第一次启动时(包括第一次打开 Notochord),会先出现「设置 Notochord」,一共五步,用 **下一步**、**上一步** 翻页。 1. **语言**:下拉框里是应用已经翻译好的所有语言(English、简体中文)。换了语言,窗口会用这个语言重新打开,接着下一步。以后可以在「设置 › 系统」里改。 2. **要不要加密保存这台电脑上的数据**:见下文。 3. **连接 AI 模型**:和「设置 › AI 设置 › 模型服务商」是同一张表。选一个服务商(比如 OpenAI、Anthropic、Google Gemini、OpenRouter,或者你电脑上的 Ollama),填上它给你的 API key,再添加要用的模型。API key 只存在你的电脑上。这一步可以点 **跳过**,跳过会直接到最后一步。 4. **角色**:给「对话」「识图」「深度思考」「本地」四个角色选模型,和「设置 › AI 设置 › 角色」是同一个设置。角色是什么、为什么建议四个都设,见[角色](/zh/guide/models.md#角色)。各个 agent 不用一个个选:都用「对话」角色;在这一步点 **下一步** 时「本地」已经设了模型的话,「后台任务」(起标题、总结长对话、描述知识库)改用「本地」,这些内容就留在这台电脑上。以后可以在「设置 › AI 设置 › Agent」里改。这一步也可以跳过。 5. **设置好了**:三个链接——本页、[用项目组织工作](/zh/guide/projects.md)、[模型和角色](/zh/guide/models.md)。点 **开始使用**,进入首页。 ### 要不要加密保存这台电脑上的数据 选「加密保存(推荐)」,这些内容会加密存放:对话、AI 做过的操作记录、保存的密码和账号、知识库。 - **能防住的**:电脑中了木马、文件被偷走时,对方打不开这些数据。 - **防不住的**:发给 AI 模型服务商的内容照常发出去,不会因此加密;电脑被黑客完全控制时,数据仍然可能泄露;普通文件不加密——你和项目工作用的文件夹里的文件,都还是原样。 - **风险**:解开数据的钥匙存在这台电脑的系统钥匙串里。钥匙丢了(比如重装系统、换电脑),没有恢复密钥文件就再也打不开这些数据。所以选「加密保存」时,要先点 **保存恢复密钥…** 存好恢复密钥文件,才能继续。把它存在这台电脑以外的地方。 选「不加密」,钥匙和数据放在同一个文件夹里。把这个文件夹复制到另一台电脑,直接就能打开,不需要恢复密钥;反过来,任何拿到这个文件夹的人都能看。 这个选择只在首次设置里做一次。 ### 没有连接模型时 首次设置里跳过了模型的话,设置结束后照样进入首页;以后每次启动,只要还没有模型,应用都先打开「连接一个 AI 模型」:选一个模型服务商,填上 API key,点 **连接**,再选一个模型,点 **开始使用**。 - 你的电脑上已经开着 Ollama 时,最上面会直接提示,点 **使用它** 就行,不需要 key。 - 想用下载到本机的模型文件,点底部的链接去设置里配置。 - 也可以点 **暂时跳过**。没有配好模型之前,侧边栏的「对话」是灰的,点它会回到这一页。以后随时可以在「设置 › AI 设置」里添加或更换。 ## 2. 建第一个项目 一件要持续做一段时间的事(跟踪求职进度、整理一批发票、管一个小活动),放进一个项目。 首页上方是你的项目,下方是 gem。点 **新建项目**,会立刻建一个「未命名项目」并打开,名字可以直接改。在右边的助手面板里告诉助手这件事要做什么,它会建好表格、把页面排好;之后你和助手都可以改表里的内容。详见[用项目组织工作](/zh/guide/projects.md)。 ## 3. 开始第一次对话 不需要项目的事,用对话:侧边栏的「对话」。 - 侧边栏平时收起成一列图标,鼠标移上去会展开。 - 「对话」这一行右边有两个按钮:**新对话**,和**围绕一个文件夹开始对话**(选一个文件夹,助手在这个文件夹里读写文件)。 - 侧边栏展开时,「对话」下面列出你的对话:置顶的在「已置顶」,其余在「最近」。每一行的「…」菜单里可以重命名、置顶、删除。 助手要用一个这个对话还没开放的工具或 gem 时,会先问你;允许后,这个对话剩下的部分里它都可以用。发邮件每一封都要你确认。 ## 4. 装一个 Gem 试试 首页下方是 gem,第一个是「商店」。在商店里点 **安装**,装好后会打开这个 gem 的页面,在那里做设置。见 [Gem:安装与使用](/zh/guide/gems.md)。 # 清单参考 ## 描述 gem 本身、它的每个工具、每个 SOP、每个 agent 都必须写描述(`description`)。描述是给 AI 和用户看的,有字数上限: | 写在哪 | 上限 | 写什么 | | --- | --- | --- | | 清单顶层的 `description` | 200 个字符 | 一两句话:这个 gem 能做什么,主要的工具叫什么(只有 skill 的 gem 写它的 skill 讲什么)。 | | `tools[].description` | 512 个字符 | 这个工具做什么、什么时候用。 | | `workflows[].description`(SOP) | 200 个字符 | 这个 SOP 做什么、什么时候运行。 | | agent 文件头部的 `description` | 不限 | 一两句话:这个 agent 是做什么的,显示在 gem 的页面上。见 [agents](#agents)。 | | 对外提供的工具、SOP、agent 的 `description` | 200 个字符 | 标了 `expose: true` 的项进主聊天的系统提示,所以和 gem 的描述一样限 200。见[对外提供](#对外提供expose)。 | 上限按字符数算,一个汉字算一个字符。没写、写成空的、或者超过上限,清单校验不通过,包装不上(`E_PERM_INVALID`)。agent 的描述没有字数上限,Gem Studio 安装时没写会拒绝。 gem 的描述要写得短,因为它是 AI 认识这个 gem 的唯一依据:用户装的每个 gem 都以一行(名字、id、描述)列在 AI 面前,装了上百个也一起列出来。AI 只凭这一行决定要不要打开这个 gem,打开之后才看得到工具、SOP 和 skill(见 [Gem 是什么](/zh/dev/gem-overview.md#ai-怎么用到你的-gem))。所以 gem 的描述只写做什么和主要工具,用法和细节写进工具、SOP 自己的描述。gem 商店的介绍和搜索将来也用这一段。 ```json "description": "Summarizes and fully translates every new email and keeps both in an encrypted store. Main tools: summarize_email, translate_email, save_record, list_records." ``` Gem Studio 的草稿里,gem 的描述是 `manifest.json` 顶层的 `description`,每个工具、SOP 的描述写在 `exports[]` 各项的 `description` 里;只有一个工具、用顶层简写时,顶层的 `description` 同时是 gem 和这个工具的描述,所以也不能超过 200 个字符。agent 的描述写在它自己文件的头部。 ## tools gem 提供给 AI 和 SOP 调用的工具。 ```json "tools": [ { "name": "summarize_email", "entry": "tools/summarize_email.js", "description": "Distill one email into its key points and any action items. Returns { summary }.", "inputSchema": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] }, "outputSchema": { "type": "object", "properties": { "summary": { "type": "string" } } } } ] ``` | 字段 | 必填 | 说明 | | --- | --- | --- | | `name` | 是 | 小写字母开头,只含小写字母、数字、下划线,最长 64 个字符。SOP 里用 `ctx.call('tool.', …)` 调用它。 | | `entry` | 是 | 工具代码所在的文件。写法见[工具 API](/zh/dev/tools.md)。 | | `description` | 是 | 最多 512 个字符,见上面的「描述」。 | | `inputSchema` | 是 | 输入参数的 JSON Schema。文件和文件夹的路径就是普通字符串,工具用 `workspace:` 前缀在对话文件夹里读写,见[权限模型](/zh/dev/permissions.md#对话文件夹)。 | | `outputSchema` | 否 | 返回值的 JSON Schema,给将来调用这个工具的 gem 和 SOP 参考。现在不做检查。 | | `permissions` | 否 | 这个工具自己用到的权限,见[权限模型](/zh/dev/permissions.md)。gem 自己的存储不用写(见[存储不是权限](/zh/dev/permissions.md#存储不是权限))。 | | `sideEffect` | 否 | `read`(什么都不改:计划模式里只有这一类工具能运行)、`write`(不写时按它处理)或 `destructive`(删除、发送、付款这类难撤回的)。这是你的声明,运行时不检查:只在确实不改任何东西时写 `read`。 | | `expose` | 否 | `true`:对外提供,主聊天的 AI 看得到、能调用。不写就是 gem 自己用的,只有这个 gem 的 SOP、agent 和页面能调用,别的会话调用一律被拒绝。见[对外提供](#对外提供expose)。 | 工具没有单独的时间上限:用户可以随时停止,SOP 里的调用受 SOP 的 `limits.maxDurationMs` 约束。 ## 对外提供(expose) 装好的 gem 有两个方向的作用:靠自己的页面、SOP 和 agent 完成任务,以及作为一个有特长的队友,让主聊天的 AI(和 gem 以外的任何 agent)用上它的本事。后者靠 gem **对外提供**一小组明确的接口:标了 `expose: true` 的工具、SOP 和 agent。其余都是 gem 的内部实现,gem 以外的 agent 看不到也调不到(调用会被 Notochord 拒绝,不管它有什么权限)。 主聊天的 AI 看到的是:每个已安装的 gem 一行(它的 `description`),下面每个对外提供的项一行(它的 `description`)。AI 用 `read_skill` 打开 gem 以后,工具列表上出现的正是这些项;第一次调用时问用户一次,同意的是整个 gem。所以对外提供的项和它们的描述,就是这个 gem 在团队眼里的全部——用队友听得懂的话写(它替人做什么、什么时候用),不要写实现步骤。 提供什么: | 形态 | 什么时候 | 怎么写 | | --- | --- | --- | | 工具 | 一次调用就能做完、结果是一个值:查一个东西、转换、总结一封邮件、记一笔。不多花一次模型调用,输入格式让调用准确。 | `tools[]` 里那一项加 `"expose": true` | | agent | 要串好几个 gem 自己的工具、需要 gem 的判断(报税:收集、分类、计算、填表)。主聊天把任务用一段话交给它(`gem..<名字>({ task })`),它在自己的会话里、用自己的 `tools` 和 `platform_tools`、用用户给它选的模型完成,回复就是结果。它只看到这段任务,看不到对话的其余部分,所以任务要把需要的都带上。gem 的一堆内部工具留在里面。 | agent 文件头部加 `expose: true` | | SOP | 队友可以启动的固定流程(可能等审批、等定时,这时调用方先拿到运行编号)。 | `workflows[]` 里那一项加 `"expose": true` | | 什么都不提供 | 只给团队读知识的 gem(有 skill:主聊天照样能读 `skill/SKILL.md`),或者只靠自己的页面和定时 SOP 工作的 gem。 | 都不写 | 安装时和 Gem Studio 里都检查:每个 gem 最多 5 项对外提供;对外提供的项描述最多 200 个字符(它进系统提示)。为自己的页面和 SOP 生成了很多工具的 gem,对外提供其中一两个,或者一个 agent,而不是整张列表。对外提供的名字在 gem 的工具、SOP、agent 之间不能重复(它们共用 `gem..<名字>` 这一个名字空间,所以工具、SOP、agent 的名字本来就不能相同)。 ## skill skill 是写给 AI 看的说明:这个 gem 的事情怎么做——什么情况下用哪个工具、按什么顺序、要注意什么。它只是文字,不会被执行,也不需要权限。**一个 gem 最多一个 skill**,格式和 [Agent Skills](https://agentskills.io/specification) 一样:一份 `SKILL.md`,加上它提到的参考资料。 ``` skill/ SKILL.md references/ regions.md rules/ 2024.csv ``` - **`skill/SKILL.md`**:skill 本身。AI 一打开这个 gem 就会完整读它,所以要短(最多 64 KB,越短越好):这个 gem 能做什么、主要的做法,以及**什么时候读哪份参考资料**——写出从 gem 根目录开始的完整路径,例如 `skill/references/regions.md`。不需要头部(front matter),AI 选 gem 看的是清单顶层的 `description`。 - **`skill/references/`**:`SKILL.md` 提到的细节,子文件夹随意:步骤、例子、表格、数据。**只能是文本文件**:`.md`、`.txt`、`.json`、`.jsonl`、`.csv`、`.tsv`、`.yaml`、`.yml`、`.xml`,每个最多 64 KB。不能放可以运行的文件(代码、脚本);需要代码示例,写在 Markdown 文件里。 - `skill/` 里不能有别的文件,没有 `SKILL.md` 也不能有 `references/`。不满足的包装不上(`E_PERM_INVALID`)。 **清单里不用声明。** 包里有 `skill/SKILL.md`,这个 gem 就有 skill。 **`SKILL.md` 有没有写对参考资料的路径,Notochord 不检查。** 没被提到的参考资料,AI 不会去读;路径写错的,AI 读不到。写完自己核对一遍:`SKILL.md` 里提到的每个路径都在 `skill/references/` 下,每份参考资料都在 `SKILL.md` 里写明了什么时候读。在 Gem Studio 里可以请助手帮你检查。 **gem 可以只有 skill**:没有工具、SOP、agent,只有 `skill/`——比如一份公司的写作规范、一套业务流程说明。它照样出现在 AI 的 gem 列表里,AI 用 `read_skill` 读它就能看到这些文件。 **AI 怎么读到**:AI 用 `read_skill` 读你的 gem id,一次拿到: - `SKILL.md` 的**全文**; - `skill/references/` 下**每个文件的名字**(`/skill/references/…`); - 你的 gem 的工具(含 SOP),从下一步起出现在 AI 的工具列表上。 之后 AI 需要哪份参考资料,就用 `read_skill` 加这个名字读全文。读 skill 不问用户。 **gem 自己的 agent 也这样读**:只要 gem 有 skill,它的每个 agent 都会拿到 `read_skill`,系统提示的末尾附上 skill 的全部文件名。agent 用 `read_skill` 读的只是文字,它能用的工具仍然只是它自己文件头部写的那些。 ### 怎么写 skill - **`SKILL.md` 放说明和索引**,细节放进 `references/`:`SKILL.md` 每次打开 gem 都会整份读进对话。 - **每次都要遵守的规则**写在 agent 自己的提示词里(`agents/.md` 的正文)——那是 gem 的 agent 每一轮都看得到的。Chat agent 只有在打开你的 gem 时才会读到 skill。 - **参考资料别太多、别太碎**:每个文件都会以一行出现在列表里,几百个小文件会让列表本身就占掉不少上下文,小模型也更难选对。 ## permissions.knowledge 申请访问用户知识库。完整说明见[知识库 API](/zh/dev/knowledge.md)。 ```json "permissions": { "knowledge": { "permissions": ["facts.read:self:location.home.*", "collections.write:diary"], "predicates": [{ "predicate": "pref.food.cuisine", "cardinality": "many", "description": "喜欢的菜系", "aliases": ["菜系"] }], "reason": "显示给用户的一句话理由(可选)" } } ``` | 字段 | 必填 | 说明 | | --- | --- | --- | | `permissions` | 是 | 权限字符串列表,格式和取值见[权限模型](/zh/dev/permissions.md)。安装时逐条校验,格式错误的清单无法安装。 | | `predicates` | 否 | 这个 gem 会写入的谓词。`predicate` 必须是「固定第一级 + 至少一级」,例如 `pref.food.cuisine`;`cardinality` 为 `one` 或 `many`。安装时写进全局词表,已存在的不覆盖。 | | `reason` | 否 | 显示在安装界面上。 | 需要 `ctx.knowledge` 的工具,在工具自己的 `permissions` 里写 `"knowledge": true`。它能访问什么由上面 gem 级的声明和用户的批准决定,工具级只是开关。Gem Studio 草稿里在 `manifest.json` 顶层写 `"knowledge": { ... }`,每个工具默认都能用;某个工具只要其中几项时,在它的那一项里写 `"permissions": ["network", "knowledge"]`(或在 Gem Studio 的工具页勾选),`side_effect` 也写在这里。 ## data gem 的数据要存成什么样:有哪些集合,每个集合有哪些字段、索引和版本。平台按这份声明存储、校验每一次写入,工具用 `ctx.data` 读写,见[数据与存储](/zh/dev/storage.md)。 ```json "data": { "collections": { "notes": { "kind": "table", "description": "用户记下的待办", "fields": { "title": { "type": "string", "required": true }, "status": { "type": "string", "enum": ["open", "done"] }, "due": { "type": "timestamp" }, "project": { "type": "ref", "to": "projects" }, "tags": { "type": "array", "items": "string" } }, "indexes": [["status", "due"], ["project"]] }, "projects": { "kind": "table", "fields": { "name": { "type": "string", "required": true, "unique": true } } }, "readings": { "kind": "log", "fields": { "value": { "type": "number" } }, "retention": { "days": 365 } } } } ``` | 字段 | 必填 | 说明 | | --- | --- | --- | | `collections` | 否 | 集合,键是集合名,最多 64 个。 | | `eventRetentionDays` | 否 | 修改记录(每次写入留下的记录:谁改的、改了什么)保留多少天,1–3650,默认 180。Log 的条目不按它清理,按各自的 `retention`。 | 集合名和字段名:小写字母开头,只含小写字母、数字、下划线,最长 64 个字符。 ### 集合 每个集合是一个表(`table`:可以修改、可以查询的记录)或一个 Log(`log`:只追加、不修改的条目)。 | 字段 | 表 | Log | 说明 | | --- | --- | --- | --- | | `kind` | 必填 | 必填 | `table` 或 `log`。 | | `fields` | 必填 | 必填 | 1–128 个字段,见下面的「字段」。 | | `description` | 可选 | 可选 | 最多 500 个字符。 | | `version` | 可选 | 不能写 | 整数,默认 1,只能升不能降,见下面的「改字段的规则」。 | | `indexes` | 可选 | 不能写 | 最多 16 个,每个是 1–8 个字段名组成的数组,不能重复;只能用标量字段(见下表)。查询需要什么索引见[索引规则](/zh/dev/storage.md#索引规则)。 | | `migrations` | `version` 大于 1 时必填 | 不能写 | 迁移模块在包里的路径:`.js`、`.mjs` 或 `.ts`,不能有 `..`,包里必须有这个文件。它为每一步导出一个函数(`v1_to_v2(data, { id })`、`v2_to_v3`……),见[版本与迁移](/zh/dev/storage.md#版本与迁移)。 | | `retention` | 不能写 | 可选 | 旧条目保留多少:`{ "days": 1–36500, "maxEntries": 1–10000000 }`,至少写一项,由平台清理。不写就一直保留。 | ### 字段 | 字段 | 说明 | | --- | --- | | `type` | 必填。标量:`string`、`number`、`integer`、`boolean`、`timestamp`(整数毫秒)、`ref`(另一条记录的 id)、`blob`(Blob 的哈希),能查询、排序、建索引。另外三种:`array`(只能用 `array-contains` 查)、`object`、`any`(这两种不能查询)。 | | `required` | `true` 时每条记录都必须有这个字段。 | | `unique` | `true` 时这个字段的值在集合里不能重复。只能用在表的标量字段上。 | | `enum` | 允许的值,1–256 个,不能重复;只能用在 `string`、`number`、`integer` 上,值的类型要和字段一致。 | | `to` | `ref` 字段(或 `items` 为 `ref` 的数组)必填:它指向哪个集合,必须是同一份清单里的一个表。 | | `items` | 只能用在 `array` 上:元素的类型,七种标量之一。不写就不检查元素,也不能用 `array-contains`。 | | `description` | 最多 500 个字符。 | ### 改字段的规则 每次激活(安装、升级、Gem Studio 测试)时,Notochord 把清单里的声明和已经存下的数据对比: - **同一个 `version` 里只能放宽**:加可选字段、去掉 `required` 或 `unique`、扩大或去掉 `enum`、加删索引、改说明。加 `unique` 也可以,但已有的值有重复时激活失败。 - **下面这些要升 `version` 并写迁移**:删字段;改 `type`、`to` 或 `items`;给已有字段加 `required`;缩小 `enum`,或给原来没有 `enum` 的字段加上;新加必填字段。 - `version` 不能比已经存下的数据低。 - 集合的 `kind` 不能改。 - Log 没有版本,字段可以随时改,旧条目保持写入时的样子。 - 从清单里删掉的集合,数据还在(接口访问不到),卸载时删除。 违反这些规则,激活失败,报错是 `E_DATA_SCHEMA: …`(新加的 `unique` 遇到重复值是 `E_DATA_UNIQUE: …`);升级和同版本重装时外面包着 `E_ACTIVATION_FAILED`,升级回滚到旧版本(见[打包与发布](/zh/dev/publishing.md#安装时检查什么))。`data` 段本身写得不对,在 Gem Studio 测试、打包和安装时就报 `E_PERM_INVALID`。打包和安装时,形状上的错误(不认识的键、`type` 写错、说明超长)先被清单的 JSON Schema 拦下,消息是 `notochord.json failed schema validation: …`;其余规则(名字、数量上限、`to`、`version` 与 `migrations`、迁移文件不在包里)的消息以 `data:` 开头。Gem Studio 测试时都以 `data:` 开头。 Gem Studio 草稿里,`data` 同样写在 `manifest.json` 顶层,字段相同,直接编辑 `manifest.json`。测试数据怎么保留、迁移怎么测,见[在 Gem Studio 里测试](/zh/dev/storage.md#在-gem-studio-里测试)。 ## agents gem 自带的 agent。**每个 agent 是 `agents/` 下的一个文件**,`agents/<名字>.md`:文件名就是 agent 的名字,开头的 YAML 头部(front matter)写它是做什么的、能用哪些工具,后面的正文是它的系统提示词。 ```markdown --- description: 汇总并翻译新邮件,然后存档。 tools: [summarize_email, translate_email, save_record] platform_tools: [http.request] expose: true --- You are a calm, meticulous email assistant. … ``` **清单里不写 agent。** 打包时 Notochord 读 `agents/` 下的每个文件,生成安装包清单里的 `agents[]`;你自己写的 `notochord.json` 里有 `agents`,打包会报错。Gem Studio 的草稿也一样:`manifest.json` 里没有 agent,新建、改名、删除 agent 就是新建、改名、删除它的文件。 **名字**:文件名去掉 `.md`。小写字母开头,只含小写字母、数字、下划线,最长 64 个字符(`agents/mail_helper.md` 是 `mail_helper`)。文件必须直接放在 `agents/` 里,不能放在子文件夹。SOP 里用 `ctx.call('agent.<名字>', …)` 调用它,页面的助手气泡用 `data-agent="<名字>"` 指定它。 头部只能有下面五个键,写了别的打包会报错: | 键 | 必填 | 说明 | | --- | --- | --- | | `description` | 是 | 这个 agent 是做什么的,一两句话,显示在 gem 的页面上。里面有 `: ` 或 `#` 时整句加引号。 | | `tools` | 否 | 它可以调用的、这个 gem 自己的工具,写工具名。 | | `platform_tools` | 否 | 它可以调用的平台工具,写全名(`fs.cat`、`http.request`、`browser.navigate` 等)。这是一项权限申请,列在 gem 页面的「它能做什么」里。不能写 `tool_search`、`read_skill`、`agent.spawn_child`、`search_gem_catalog`,也不能写别的 gem 的工具(`gem.*`)。 | | `models` | 否 | 它的代码用到的模型,键是名字,`main` 是它对话时用的模型(不写也有)。每一项只有一个可选字段 `requires`:这个模型除了文字以外还必须会什么,见下面的[模型能力](#模型能力)。**用哪个模型由用户选**——在 gem 的页面上或「设置 › AI 设置 › Agent」里,选一个具体的模型,或者选用户的一个角色(内置的是 `chat`、`vision`、`deep`、`local`,`local` 给用户电脑上运行的模型用,界面上叫「本地」);没选时用用户的 `chat` 角色;这里不能指定。想建议用户用什么样的模型(比如处理邮件、个人资料的 agent 建议选「本地」),写在 gem 的介绍文档里(见 [docs](#docs))。 | | `expose` | 否 | `true`:对外提供,主聊天的 AI 把任务交给它(见[对外提供](#对外提供expose))。这时 `description` 限 200 个字符,并计入 gem 最多 5 项的上限。不写,只有这个 gem 的 SOP 和页面能用到它。 | 生成的 `agents[]` 每项是 `name`、`entry`(`agents/<名字>.md`)、`description`、`tools`、`platformTools`、`models`、`expose`。装好以后 Notochord 只按 `agents[]` 决定 agent 能用什么,文件的头部不再读,正文是系统提示词。 agent 只能调用 `tools` 和 `platform_tools` 列出的工具,别的一律用不了。唯一的例外是 `read_skill`:gem 有 skill 时,Notochord 自动给每个 agent 加上它,只用来读 skill(见 [skill](#skill)),不需要也不能写进 `platform_tools`。 ### 模型能力 agent 要模型看图片、听音频这类文字以外的事,在 `models` 的那一项里用 `requires` 写明: ```yaml models: { main: { requires: [image_input] } } ``` 只能写下面六个值,写别的,包**装不上**(报 `E_PERM_INVALID`): | 值 | 模型要能 | | --- | --- | | `image_input` | 看懂图片 | | `audio_input` | 听懂音频,包括语音转文字 | | `video_input` | 看懂视频 | | `image_output` | 生成图片 | | `audio_output` | 生成音频,包括语音合成 | | `video_output` | 生成视频 | 用户在 gem 的页面上给每个 agent 选模型(没选时用用户的 `chat` 角色),「设置 › AI 设置 › Agent」里也能换。Notochord 拿选中的模型对照一份常见模型的能力列表: - 列表说这个模型有你写的全部能力:不显示任何东西。 - 列表说它没有其中某项,或者列表里没有这个模型(比如用户自己起的模型名),或者这个角色还没有配模型:显示一行提示,由用户自己确认。 - 提示不会阻止安装,运行时也不检查。模型真的不支持时,出错的是你的 agent 对模型的那次调用,和没有写 `requires` 一样。 所以 `requires` 只写 agent 离了它就做不成事的能力。只是偶尔用得到的,不要写,免得用户每次都看到提示。 **清单里没有按 agent 设置的运行上限。** 一个 agent 一次运行最多走多少步、用多少 token,由用户在「设置 › 系统」里的「最大循环步数」和「每次运行的 token 上限」 决定,对所有 agent 相同。`maxTurns`、`maxCostUsd` 这两个字段已经删除:打包后的清单里写了它们,安装时会因为清单校验失败被拒绝。 ## docs `docs/` 是写给人看的:这个 gem 能做什么、怎么用。**它会成为 gem 在商店里的介绍页**。使用 gem 时不会用到这个文件夹里的任何内容——gem 的代码、它的 agent、AI 都不读它,所以不要把给 AI 的说明写在这里(那是 [skill](#skill))。 ``` docs/ README.en.md README.zh-CN.md prompts/ first-steps.en.md first-steps.zh-CN.md examples/ weekly-report.en.md ``` - **只能是 Markdown**(`.md`),子文件夹随意。别的文件 Gem Studio 打包时会去掉;自己打的包里有别的文件,打包和安装都会报错(`E_PERM_INVALID`)。图片放不进来。 - **每个文件按语言命名**,用标准的语言代码(BCP 47:`en`、`zh-CN`、`ja`、`de`……): - `README.<语言>.md`:介绍页的正文——这个 gem 做什么、给谁用、怎么设置、怎么用。**写了哪几种语言,商店介绍页的语言菜单里就有哪几种**。至少写一份(`README.en.md`)。 - `prompts/<名字>.<语言>.md`:可选,可以直接试的提示词,每条说明它做什么。商店介绍页上单独一个「提示词」标签页。 - `examples/<名字>.<语言>.md`:可选,使用这个 gem 的完整例子。商店介绍页上单独一个「例子」标签页。 - 商店按普通的 Markdown 渲染这些文件,每个标签页显示用户选的语言。同一条提示词或例子的各个语言版本用同一个 `<名字>`(`prompts/first-steps.en.md`、`prompts/first-steps.zh-CN.md`)。 清单里不用声明 `docs/`。在 Gem Studio 里,结构栏的「介绍文档」列出这个文件夹里的文件,可以在那里新建和编辑。 ## resources 太大、不适合放进安装包的文件(模型、词典、数据集)。在清单里写明地址、大小和校验值,由 Notochord 下载;工具用 [`ctx.resources`](/zh/dev/tools.md#ctxresources) 取得本地路径。 ```json "resources": [ { "id": "ocr-model", "url": "https://cdn.example.com/models/ocr-v3.onnx", "mirrors": ["https://mirror.example.org/models/ocr-v3.onnx"], "sha256": "9f2c…", "size": 412000000, "required": true, "description": "OCR 模型" } ] ``` | 字段 | 必填 | 说明 | | --- | --- | --- | | `id` | 是 | 工具里取这个文件时用的名字。同一份清单里不能重复,重复的清单无法安装。 | | `url` | 是 | 下载地址,必须以 `https://` 开头。 | | `mirrors` | 否 | 备用地址,同样必须是 `https://`。`url` 失败后按顺序逐个尝试。 | | `sha256` | 是 | 文件内容的 SHA-256,64 位小写十六进制。 | | `size` | 是 | 文件大小,单位是字节。 | | `required` | 否 | `true`:安装完成后就在后台开始下载;以后每次启动应用时,如果文件还不在,会再试一次。默认 `false`:工具第一次用到时才下载。 | | `description` | 否 | 显示在 gem 的页面上。不写就显示 `id`。 | 下载是这样进行的: - **由 Notochord 下载,不是 gem 的代码。** 下载地址不用写进 `network`,也不需要申请任何权限。gem 的页面上会列出每个文件的说明、大小、来自哪个主机、装完就下载还是用到时才下载。 - **只接受 `https`,不能指向本机或内网地址。** 重定向最多跟 5 次,每一跳都按同样的规则检查。 - **受用户的网络策略约束。** 用户在「设置 › 系统」的网络策略(白名单 / 黑名单模式)里没有放行的主机下载不了,镜像和重定向经过的主机也算在内。 - **大小和 SHA-256 都要与清单一致。** 服务器多发、少发、或者内容对不上的文件会被丢弃,然后换下一个地址。磁盘剩余空间不够时不会开始下载。 - **下载失败不影响安装。** `required` 的文件也一样:安装不会等它,也不会因为它失败。gem 照常可用,只有用到这个文件的那次调用会报错;下一次调用、或下次启动应用时会自动重试。 - **升级时不重复下载。** 新版本里 `sha256` 没变的文件直接沿用;新版本不再声明的文件,在升级成功后删除。卸载 gem 时全部删除。 - 不支持断点续传,失败后从头下载。目前没有下载进度的界面。 Gem Studio 的草稿里,`resources` 同样写在 `manifest.json` 顶层,字段相同;也可以在 Gem Studio 概览的「资源」里填,填好地址后点「自动填大小和 SHA-256」,Notochord 按下载资源的同一套规则把文件下载一遍、算出这两项(不保留文件)。草稿测试时 `ctx.resources` 和装好以后一样可用。 # 用项目组织工作 一个项目放一件持续在做的事:它的表格、一个页面(画布)、一个文件夹,以及你和助手之间的若干个对话。你和助手在同一份表格上干活,事情做到哪一步,记在项目里,而不是某一个对话里。 一次性的问题用侧边栏的「对话」就够了。要做几天、几周,要记下一批条目、跟踪进度的事,用项目。 ## 新建项目 首页上方是你的项目。点 **新建项目**,会立刻建一个「未命名项目」并打开,名字可以直接改;以后点项目顶栏上的名字也能改。 也可以从一个对话开始:在对话里让助手把这个对话变成一个项目。这个对话会成为新项目的第一个对话,对话里产生的文件一起搬进项目的文件夹;如果这个对话用的是你选的文件夹,那个文件夹就成为项目的文件夹,文件不搬。 ## 工作视图 打开项目看到的是工作视图。顶栏上有项目名、「工作 / 构建」切换、**Agent**、**文件**、**数据** 三个面板按钮,和右边的齿轮菜单。右边是助手面板。 离开项目页(回到首页、去设置)不会打断正在进行的对话和运行。 ### 画布 页面的大部分是画布:助手排好的一块块内容,比如说明文字、表格、统计数字、图表、看板、表单。每一块显示的都是某张表的实时数据,表里的数据一变,画布跟着变。 - 排版由助手来做:告诉它想看什么,它来加、改、删这些块。 - 你可以把一块从页面上移除,它的数据仍在表里。 - 在看板上把卡片拖到另一栏、在表单里新增一行,改的就是表里的数据。 ### Agent、文件、数据三个面板 点顶栏上的按钮,对应的面板浮在画布上方展开,一次开一个,再点一下收起。 **数据**:项目的表格。 - 像电子表格一样直接改,每改一格就保存,没有保存按钮。 - 每张表的第一列是行号 `id`,自动编号,谁都不能改。你和助手说「第 7 行」,指的是同一行。 - 标签后面的 **+** 新建一张表;表头最后的 **+** 加一列(填列名,选类型:文字、数字、是 / 否、日期、JSON);在表格下面点「加一行」。双击表名或列名可以改名。 - 列头的「⋯」菜单可以改这一列的类型,或者隐藏这一列;表也可以隐藏。隐藏不删除数据,在「已隐藏」里点一下就显示回来。隐藏整张表只有你能做,助手不能。 - 改类型时,只要有一个值换不过去,整个改动都不做,并告诉你是哪几行。先改好那几行再来。 **文件**:项目的文件夹。 - 把文件或文件夹拖到面板上(或者拖到顶栏的「文件」按钮上)就复制进来。同名的不覆盖,新的那个名字后面加「 (2)」。 - 项目的所有对话共用这个文件夹:你拖进对话的附件放在这里,助手写的文件也在这里,下一个对话还能用。 **Agent**:现在正在为这个项目工作的东西——项目助手交出去的子 agent,以及项目的对话启动的 gem 的 agent 和 SOP,每个标着它现在的状态(比如「工作中」「等待你的批准」)。agent 旁边的下拉框可以换它用的角色或模型,见[自己给子 agent 换角色](/zh/guide/models.md#自己给子-agent-换角色)。 ### 助手和对话 右边的助手面板里,一个项目可以有好几个对话:在面板顶上切换、新建或删除。面板可以拖宽,也可以折叠,折叠后窗口右边留一条窄栏,点它展开。 - 每个对话看不到以前的对话内容。能接着用的是表格和文件夹,所以要留下来的东西,让助手记进表里。 - 助手每次回复前会知道:项目名、项目可用账号的名字(看不到密钥)、有哪些表,以及上次回复以来表里哪些行被改过——包括你在表格里改的、别的对话改的。 - 项目里的助手用哪个模型,在「设置 › AI 设置 › Agent › 项目」里选,见[配置模型](/zh/guide/models.md#给各个-agent-选模型)。 ### 齿轮菜单 - **账号**:在这个项目里工作的 gem 能用你的哪些账号(包括邮箱)。默认一个都不勾;没勾的账号,gem 为这个项目工作时用不了。助手看得到勾选的账号的名字,看不到密钥。 - **工具**:项目助手能用哪些工具。没改过时和「对话」的助手相同;表格工具始终开启。 - **代码的权限**:见下文[项目的代码](#项目的代码)。 - **打开文件夹**、**选择文件夹…**:项目默认用自己的文件夹,也可以换成电脑上已有的一个文件夹,省得把文件再导一遍。不能选用户主目录、系统目录、应用自己的数据目录和隐藏的文件夹。换文件夹时文件不搬,两边的文件都留在原处;点「改用项目自己的文件夹」换回来。 - **删除项目**:删掉它的对话和表格,项目自己的文件夹进回收站;你为它选的文件夹不动。 账号和工具改完点 **保存** 才生效。 ## 项目的代码 助手可以为项目写工具和 SOP(自动流程),让项目自己去取数据、一行一行处理,或者按时间自动运行。这些代码用的是项目自己的表格和文件夹。 代码要做下面这些事时,先要你允许: - 访问互联网上的某个网站; - 访问你在「设置 › 系统 › 网络访问」里列出的本机和内网地址; - 使用 AI 模型; - 按时间自动运行。 项目页上方会列出它请求的每一项,你点 **允许** 才生效。只改了代码、或者要的比以前少,不用再问。助手不能替你允许。 齿轮 ›「代码的权限」里能看到已经允许的项,也可以 **全部收回**:收回后定时运行停止,代码不再运行,直到你再次允许。 ## 构建视图 顶栏切到「构建」,看到的是项目背后的代码:工具、SOP、改动历史和发布。它的样子和 Gem Studio 的工作台一样:可以看、改代码,试跑工具和 SOP,在运行记录里看每次运行的输入、输出和出错的地方,在历史里回到以前的版本。 右边还是同一个助手、同一个对话。大多数时候不用打开构建视图,让助手写、改代码就行。 ## 发布成 gem 在构建视图的顶栏点 **发布…**。发布把这个项目的格式固定下来,做成一个 gem:谁装上它,都得到同样的表格和代码,从空的开始。 - **带上的**:表格的结构(表、列、类型,包括隐藏的)、工具和 SOP、代码申请的权限。 - **从不带上的**:表里的数据、文件、账号、对话。 - 画布现在还不能放进 gem,发布对话框里会写明有几块不会带上。 - 代码里有项目还不支持发布的内容时,不能发布,对话框会列出原因。 发布有两种: - **安装到本机**:装成这台电脑上的一个 gem,在首页的 gem 里,设置在它自己的页面上(见 [Gem:安装与使用](/zh/guide/gems.md))。再次发布会更新这个 gem。 - **导出 .gem 文件**:得到一个文件,可以给别人,也可以在 Gem Studio 里导入。 构建视图的顶栏显示发布状态:「未发布」「已发布到本机」或「有改动还没发布」。 装好的 gem,表格格式是固定的:在 gem 里不能改它的表结构,只有你回到项目里改好、再发布一次,它才会变。 # 对话与会话 ## 对话列表 对话在侧边栏的「对话」下面。侧边栏平时收起成一列图标,鼠标移上去展开,展开时才列出对话: - 置顶的对话在「已置顶」,其余的在「最近」。 - 每一行的「…」菜单里可以重命名、置顶(或取消置顶)、删除。 - 「对话」这一行右边有两个按钮:**新对话**;**围绕一个文件夹开始对话**——选一个文件夹,助手就在这个文件夹里读写文件。不能选用户主目录、系统目录这类文件夹。 还没有接入模型时,「对话」是灰的。 项目里的对话不在这个列表里,在项目右边的助手面板里,见[用项目组织工作](/zh/guide/projects.md)。 ## 子 agent 助手可以把一部分工作交给子 agent,并按任务给它选一个角色(见[角色](/zh/guide/models.md#角色))。点输入框右边的「专家和工具」按钮: - 「专家」一栏列出这个对话的子 agent。点一个可以看它的工作过程。每个下面有一个「模型」下拉框,可以给它换一个角色或一个具体的模型,改动从它的下一步开始生效。 - 「工具」一栏是助手在这个对话里能用的工具,可以在这里勾选或取消。 # 权限模型 ## 原则 Gem 在清单里列出要的权限。要申请的只有这几样:网络(`network`)、用户的模型(`providers` 里的 `llm`,花的是用户的钱)、知识库(`knowledge`)、邮件(`email`),以及用户的账号(`connections`)。gem 自己的存储、文件、文档抽取(`ctx.extract`)不是权限,每个 gem 的每个工具都有。 安装没有确认步骤(和手机的应用商店一样,用户在商店的介绍页上看到 gem 能做什么,点一下就装);安装时把清单里的权限记录下来,运行时只看这份记录,不再读清单。 **需要用户配置的,没配置就等于没有。** 能读哪些邮箱、能用知识库里的哪些内容、用哪个外部账号,装好时一个都没有,gem 的请求会被拒绝,直到用户在 gem 的页面上打开。升级时保留用户已经打开的,新增的同样默认关闭。运行时不会弹窗确认。 ## 网络 ```json "permissions": { "network": { "hosts": ["api.open-meteo.com"], "reason": "查询天气预报" } } ``` | 字段 | 必填 | 说明 | | --- | --- | --- | | `hosts` | 是 | gem 能访问的外网主机。只写主机名(`api.example.com`),不带协议和路径,不支持通配符;服务不在默认端口上时带上端口(`api.example.com:8443`,端口不同就是不同的主机);国际化域名写成 `xn--` 开头的形式。IP 地址和 `localhost` 只能在 `allowLocalNetwork` 为 `true` 时写。没有列出的主机,请求在发出之前就被拒绝。 | | `allowLocalNetwork` | 否 | 要不要访问用户的本机和内网,默认 `false`,见下文。为 `true` 时 `hosts` 可以为空。 | | `reason` | 是 | 为什么要联网,显示在 gem 的页面上。 | 工具里的 `ctx.http.fetch` 和 `ctx.extract.fromUrl`(下载一个文档再抽取文字)、能用浏览器的 agent,都按这同一份权限检查,每一跳重定向也一样。在这之上还有用户自己的网络规则(「设置 › 系统 › 网络访问」),gem 的权限只会更窄,不会更宽。 ### 本机和内网 Notochord 把地址分成两类,规则不同: - **外网**:网站和在线服务。用户可以选白名单或黑名单模式,默认放开。 - **本机和内网**:用户的电脑(`localhost`、`127.0.0.1`、`[::1]`),家里或公司内部网络的地址(`10.x.x.x`、`172.16–31.x.x`、`192.168.x.x`、`169.254.x.x`、`100.64–127.x.x`,以及 IPv6 的对应地址),还有解析到这些地址的域名(比如公司内网的 `wiki.corp.example.com`)。 为什么分开:本机和内网里是用户自己的设备和内部服务——路由器和 NAS 的管理页、打印机、公司的 OA 和 wiki——它们常常不设密码,因为平时只有内部的人连得到,里面又常有内部资料。AI 读到的网页可能藏着指令,第三方 gem 也可能想打探用户的内网;如果这些地址能随意访问,就等于在互联网和用户的内网之间开了一道门。所以这一类默认一个都不能访问,只有用户亲自列出的地址才放行。 gem 要访问本机和内网: - 在清单里写 `"allowLocalNetwork": true`(Gem Studio 草稿里写 `"allow_local_network": true`,或在概览的权限里勾「本机和内网」)。gem 的页面上会写明它能访问用户列出的本机和内网地址。 - 能访问的只有用户在「网络访问 › 本机和内网」里列出的地址,gem 自己不能指定;本机和内网的地址不用写进 `hosts`。 - 没写 `allowLocalNetwork` 的 gem,用户列出了某个地址,它也访问不了。 - 例如一个帮开发者测试网站的 gem:用户把 `localhost:5173` 列进名单以后,它的 agent 才能用浏览器打开 `http://localhost:5173`。 - [`resources`](/zh/dev/manifest.md#resources) 的下载永远不访问本机和内网。 ## 文件不是权限 gem 的代码能碰到的文件只有两种:**这一次运行自己的文件夹**,和**它被调用的那个对话的文件夹**。对话外的任何东西碰不到,和对话里的 AI 自己一样(它的文件工具也只在对话文件夹里)。没有固定的路径可以申请,清单里不用写什么:每个工具都有 `ctx.files`。读写的接口见[工具 API](/zh/dev/tools.md#ctxfiles)。 ### 运行文件夹 每次运行都有一个自己的文件夹: | 谁在运行 | 运行文件夹在哪 | | --- | --- | | 聊天里调用的 SOP 或工具 | 这个对话的工作目录里,`runs/<名字>-<时间>/` | | 聊天里调用的对外提供的 agent | 同上,`runs/-<时间>/`,也是这个 agent 会话的工作目录 | | 定时、事件触发的 SOP | 这个 gem 自己的数据文件夹里,`runs/<名字>-<时间>/` | | SOP 里调用的工具和 agent | 和这次 SOP 运行共用一个 | - 放在调用它的对话里,所以运行结果对话里的 AI 用自己的文件工具就能读写,用户也能在对话的工作目录里打开。 - 文件夹里的约定(不强制):`tmp/` 放中间文件,`output/` 放结果。生成的报告写成 Markdown 或 HTML。 - 运行成功结束后删掉 `tmp/`;失败、取消时保留——重试是从头重跑,已经成功的步骤不会再写一遍。什么都没留下的运行文件夹会被删掉。 - 留下了文件时,调用方拿到的是 `{ "output": <返回值>, "folder": "runs/…" }`,`folder` 相对于对话的工作目录;没有留下文件时是原样的返回值。对外提供的 agent 返回 `{ "reply": …, "folder": … }`。 - 卸载 gem 时,它数据文件夹里的运行文件夹一起删除;已经放进对话里的保留,那是用户的文件。 ### 对话文件夹 用户拖进对话的文件、「聊一个文件夹」选的文件夹、别的 gem 留下的结果,都在对话文件夹里。gem 用 `workspace:<相对路径>` 读写它(`workspace:inbox/发票.pdf`;`workspace:` 是顶层)。 - **路径就是普通参数。** AI 把对话文件夹里的相对路径当字符串传进来(`{ "folder": "inbox/", "note": "notes.txt" }`),gem 自己加 `workspace:` 前缀。多个文件、整个子文件夹,gem 自己 `list` 自己处理,不用 AI 一个个点名。`inputSchema` 里不需要也没有专门的文件类型。 - **以 `.` 开头的文件和文件夹对 gem 不存在**(`.env`、`.git`):列不出、读不到、写不进。 - **用户的文件不会丢,改动可以回退。** gem 对对话文件夹的每次修改都记进文件历史:覆盖时旧版本先存起来(100 MB 以内,更大的进系统回收站),`delete` 把文件移到用户的回收站并留一份副本,`move` 只记路径;`move` 到已有路径被拒绝。用户在聊天里重试或编辑上一条消息时,那条分支上 gem 改过的文件自动恢复到改前(之后被别处改过的不动)。没有回收站的环境(无界面运行的服务器版)删除是永久的,但历史里的副本照样能回退。 - 没有对话的调用——页面发起的调用,或者没在 gem 页面上选工作文件夹的定时 SOP——用 `workspace:` 报 `E_PERM_DENIED`。 - gem 的 agent 自带的 `fs.*` 平台工具以 `workspace/` **只读**看到对话文件夹;要改文件,走 gem 自己的工具。 ### 定时和事件触发的 SOP 没有对话。它的 `workspace:` 是用户在 gem 页面「设置 › 工作文件夹」里给**这个 gem** 选的一个文件夹(只在 gem 有定时或事件 SOP 时显示);没选就没有 `workspace:`。不能选的文件夹——文件系统的根、个人文件夹本身以及包含它的、Notochord 的数据目录、系统和其他应用的文件夹、隐藏文件夹——和「聊一个文件夹」一样会被拒绝。 ### 用户看到什么 文件不在 gem 页面的「它能做什么」里,升级也不因为文件重新批准:gem 在文件上能做的事由对话决定,不由清单决定。用户看到文件夹的地方只有两处:对话的工作目录,和 gem 页面上为定时 SOP 选的工作文件夹。装了一个声明了网络的 gem,就等于信任它可以把用到它的那些对话里的文件发到它声明的网站。 ## 存储不是权限 每个 gem 都有自己的存储,放运行状态、缓存和它自己整理出来的记录。它不是权限:清单里不用写,用户不用打开,gem 的页面上也不列。读写的接口见[数据与存储](/zh/dev/storage.md)。 - **每个工具都有全部存储**:`ctx.data`(集合、Log、文档、Blob、引用)、`ctx.storage.kv` 和私有的 SQL 数据库 `ctx.storage.sql`。SOP 也可以直接调用 `storage.kv.*`、`storage.sql.*`。清单里没有存储的字段。 - **`sideEffect: "read"` 是你的承诺,平台不检查。** 计划模式会在用户批准方案之前运行声明为 `read` 的工具,平台不看它实际做了什么。所以只有什么都不写的工具才能声明 `read`:不写 `ctx.data` 和 `ctx.storage`,不写知识库,不发 GET 以外的请求。声明不实,就是 gem 的质量问题。 - **每个 gem 一个自己的文件夹,别的 gem 读不到。** 数据库和 Blob 用这个 gem 自己的密钥加密;文档(`.md` 文件)和定时、事件触发的 SOP 的[运行文件夹](#运行文件夹)是明文。卸载 gem 时整个文件夹连同密钥一起删除。细节见[加密与隔离](/zh/dev/storage.md#加密与隔离)。 ## 资源下载不是权限 清单里的 [`resources`](/zh/dev/manifest.md#resources) 不需要申请权限:下载地址固定写在清单里,由 Notochord 下载并核对校验值,gem 的代码既改不了地址,也不经手下载。它和 gem 的 `network` 权限无关——`network` 管的是工具自己发出的请求。 不过用户看得到,也管得住: - gem 的页面上「它能做什么」里有一栏「下载额外的文件」,逐个列出文件的说明、大小、来自哪个主机、什么时候下载。 - 下载和 gem 自己的请求一样,受用户的网络策略(「设置 › 系统」)约束。 ## 知识库权限 权限字符串统一为 `数据类型.动作:范围`: | 权限 | 含义 | | --- | --- | | `facts.read:<主语>:<前缀>` | 读取某类事实。主语为 `self` 或实体 kind(如 `person`);前缀写 `a.b.*` 或完整谓词,至少两级。 | | `facts.write:<主语>:<前缀>` | 写入某类事实,必须被某条 `facts.read` 覆盖。 | | `entities.resolve:` | 用已知的邮箱或名字查实体,不能列出全部。 | | `entities.read:` | 列出该 kind 的全部实体。 | | `entities.write:` | 新建、编辑该 kind 的实体(包含 resolve)。 | | `collections.read:<目标>` / `collections.write:<目标>` | 目标为 `diary`、`keep` 或 `user-selected`(用户装好后在 gem 的页面上勾选)。授权包含该知识库里面的所有子文件夹。 | | `collections.create` | 创建自己的知识库,创建后自动可读可写。建在别的知识库里面时,还需要对那个知识库有写权限。 | | `changes.manage` | 查看、撤销、修改其他写入者的记录。 | | `origin.override` | 写入时自己指定来源(`origin` / `source`)。 | 校验规则: - 安装时:格式错误、前缀不足两级(例如 `self:*`)、`facts.write` 没有被 `facts.read` 覆盖、在清单里直接写知识库 id,都会让安装失败。 - 运行时:每次调用都重新读取用户当前的授权。用户在 gem 页面的「设置 › 知识库」里收回某条权限后,下一次调用立即生效。 - 查询类方法遇到未授权的内容只是不返回;针对单个对象的操作没有权限会抛错。 详见[知识库 API](/zh/dev/knowledge.md)。 # 数据与存储 gem 要长期保存的东西——业务记录、同步进度、生成的报告、图片——放在它自己的数据里。做法是:在清单里**声明**要存什么(集合、字段、索引、版本),工具用 `ctx.data` 读写。平台负责存储,按声明检查每一次写入,为每次修改留下记录(谁改的、改了什么),并要求大集合上的查询有声明的索引可用(见[索引规则](#索引规则))。你不写 SQL,也不用管表结构。 先要知道的几条边界: - **数据只属于这个 gem。** 别的 gem 读不到;卸载时整个删除,重新安装拿不到以前的数据。 - **存储不是权限。** 每个 gem、它的每个工具都有,清单里不用申请,用户也不用打开。 - **要给用户或别的 gem 看的东西,写进知识库**([知识库 API](/zh/dev/knowledge.md))。`ctx.data` 放的是 gem 自己的工作数据。 - **集合、Log、Blob 加密存放,文档是明文文件。** 见[加密与隔离](#加密与隔离)。 - **一个组织一份。** 同一个 gem 装在两个组织里,就是两份互不相通的数据。 `ctx.data` 提供这几样东西,下文逐一说明: | 接口 | 存什么 | | --- | --- | | `ctx.data.collection(name)` | 表:一条条可以修改、可以查询的记录 | | `ctx.data.log(name)` | Log:只追加、不修改的条目,比如每次的读数、每次的操作 | | `ctx.data.docs` | 文档:Markdown,可以按块编辑、按块引用 | | `ctx.data.blobs` | Blob:文件(图片、PDF……),按内容去重 | | `ctx.data.refs` | 引用:上面这些对象之间的链接 | | `ctx.data.transact(fn)` | 事务:几个写入要么全成,要么全不成 | 读写数据的方法都是异步的,要 `await`;`collection()`、`log()`、拼查询的方法和 `ctx.data.newId()` 是同步的。 ## 声明数据 打包后的清单(`notochord.json`)里,集合和 Log 写在顶层的 `data` 段。 ```json { "data": { "collections": { "notes": { "kind": "table", "fields": { "title": { "type": "string", "required": true }, "status": { "type": "string", "enum": ["open", "done"] }, "due": { "type": "timestamp" }, "project": { "type": "ref", "to": "projects" }, "photo": { "type": "blob" }, "tags": { "type": "array", "items": "string" }, "extra": { "type": "object" } }, "indexes": [["status", "due"], ["project"]] }, "projects": { "kind": "table", "fields": { "name": { "type": "string", "required": true, "unique": true }, "open_count": { "type": "integer" } } }, "readings": { "kind": "log", "fields": { "value": { "type": "number" }, "note": { "type": "ref", "to": "notes" } }, "retention": { "days": 365 } } } } } ``` - **`ctx.data` 不用申请**:每个工具都有,装好就能用。只用文档、Blob 和引用、不需要集合时,可以不写 `data` 段。 - **声明 `sideEffect: "read"` 的工具不要写任何存储**:计划模式会在用户批准之前运行这类工具,平台不检查它实际写了什么,见[权限模型](/zh/dev/permissions.md#存储不是权限)。 - Gem Studio 草稿的写法见[在 Gem Studio 里测试](#在-gem-studio-里测试)。 ### 集合 `data.collections` 里每一项是一个集合,键就是 `collection(name)`、`log(name)` 里用的名字。集合有两种:`table`(表)和 `log`。 | 键 | 表 | Log | 说明 | | --- | --- | --- | --- | | `kind` | `"table"` | `"log"` | 必填,以后不能改 | | `fields` | 必填 | 必填 | 1–128 个字段,见下 | | `description` | 可选 | 可选 | 最多 500 个字符 | | `version` | 可选 | 不能写 | 整数,默认 1,只能升不能降,见[版本与迁移](#版本与迁移) | | `indexes` | 可选 | 不能写 | 最多 16 个,每个是 1–8 个字段名的数组,见[索引规则](#索引规则) | | `migrations` | `version` 大于 1 时必填 | 不能写 | 迁移函数所在的文件 | | `retention` | 不能写 | 可选 | `{ "days": 1–36500, "maxEntries": 1–10000000 }`,至少写一个,见 [Log](#log) | 集合名和字段名:小写字母开头,只含小写字母、数字和下划线,最长 64 个字符。一个 gem 最多 64 个集合。 `data` 段里还可以写 `eventRetentionDays`(1–3650,默认 180):平台为每次修改留下的修改记录保留多少天。 ### 字段 | `type` | 值 | 能查询、排序、建索引 | | --- | --- | --- | | `string` | 字符串 | 能 | | `number` | 有限的数 | 能 | | `integer` | 安全整数 | 能 | | `boolean` | `true` 或 `false` | 能 | | `timestamp` | 毫秒时间戳(整数) | 能 | | `ref` | 某个表里一条记录的 id,`to` 写哪个表 | 能 | | `blob` | 已经存进来的 Blob 的哈希 | 能 | | `array` | 数组,`items` 写元素的类型 | 只能用 `array-contains` 查询,而且要写了 `items` | | `object` | 普通对象,内容不检查 | 不能 | | `any` | 任意 JSON | 不能 | 前七种叫标量字段。字段上还可以写: | 键 | 说明 | | --- | --- | | `required` | 必填。 | | `unique` | 值不能重复。只能用在表的标量字段上,Log 不能用。 | | `enum` | 允许的值,1–256 个、不重复,类型要和字段一致。只能用在 `string`、`number`、`integer` 上。 | | `to` | `ref` 字段(和 `items` 为 `ref` 的数组)必填:指向的表的名字,必须是同一个清单里的 `table`。 | | `items` | 只能用在 `array` 上,是标量类型之一。可以不写:不写就不检查元素,但也不能用 `array-contains` 查询。 | | `description` | 最多 500 个字符。 | 写入的数据不合声明就被拒绝(`E_INPUT_INVALID`),平台不会悄悄转换:类型不对、缺必填字段、出现没有声明的字段、`blob` 字段里的哈希还没有存进来,都会报错。`ref` 字段只检查 id 的格式,不检查那条记录是否存在。`object`、`any` 和不写 `items` 的数组最多嵌套 32 层。 只有一个约定:**值为 `null` 等于没有这个字段**。插入时值为 `null` 的字段被丢掉,更新时把字段设成 `null` 就是删除它。 `data` 段写得不对时(名字不合规、`to` 指向不存在的表、`version` 大于 1 却没写 `migrations`……),清单校验不通过,报 `E_PERM_INVALID`。打包、安装和 Gem Studio 测试时都会检查。打包和安装时,形状上的错误(不认识的键、`type` 写错、说明超长)先被清单的 JSON Schema 拦下,消息是 `notochord.json failed schema validation: …`;其余的错误(比如上面举的几种)消息以 `data:` 开头。Gem Studio 测试时都以 `data:` 开头。 ## 集合:写入与查询 ```js export default async function (input, ctx) { const notes = ctx.data.collection('notes') const { id } = await notes.insert({ title: input.title, status: 'open', due: Date.now() }) const note = await notes.get(id) // { id, version, createdAt, updatedAt, data },没有时是 null // 按顶层字段合并,值为 null 的字段被删除;ifVersion:只在没人改过时才改 await notes.update(id, { status: 'done', due: null }, { ifVersion: note.version }) const page = await notes.where('status', '==', 'open').orderBy('due').limit(20).get() // page = { items: [...], nextCursor?: '...' } if (page.nextCursor) { const next = await notes.where('status', '==', 'open').orderBy('due').limit(20).startAfter(page.nextCursor).get() } const open = await notes.where('status', '==', 'open').count() return { open } } ``` | 方法 | 说明 | | --- | --- | | `insert(data, { id? })` | 新建一条记录,返回 `{ id, version, nodeId }`。不给 `id` 时自动生成;给的 `id` 已经存在时报 `E_DATA_CONFLICT`。id 只能含字母、数字、`_` 和 `-`,最长 128 个字符。 | | `get(id)` | `{ id, version, createdAt, updatedAt, data }`,没有这条记录时是 `null`。 | | `update(id, patch, { ifVersion? })` | 按顶层字段合并:`patch` 里的字段替换原值,值为 `null` 的字段被删除,合并后的记录再整体按声明检查一遍。返回 `{ version }`。记录不存在时报 `E_DATA_NOT_FOUND`。 | | `delete(id, { ifVersion? })` | 返回 `{ deleted: true \| false }`,记录本来就不存在时是 `false`,如果带了 `ifVersion` 则报 `E_DATA_CONFLICT`。 | | `where(field, op, value)`、`orderBy(field, 'asc' \| 'desc')`、`limit(n)`、`startAfter(cursor)` | 拼一条查询。每一步返回一个新的查询,调用 `get()` 或 `count()` 时才执行。 | | `get()` | 执行查询,返回 `{ items, nextCursor? }`,`items` 的每一项和 `get(id)` 返回的一样。 | | `count()` | 符合条件的记录数。只看 `where` 条件,排序、`limit` 和 `startAfter` 都不起作用。 | - **`version`** 从 1 开始,每次写入加 1。`update`、`delete` 带上 `ifVersion` 时,如果记录在你读到之后被改过,就报 `E_DATA_CONFLICT`,什么也不改。 - **删除是软删除**:记录从接口里消失,内容还留在数据文件夹里,卸载时才删除,见[加密与隔离](#加密与隔离)。删掉的 id 可以再用来插入。 ### 查询规则 - 运算符:`==`、`!=`、`<`、`<=`、`>`、`>=`、`in`(1–30 个值)、`array-contains`。`where('due', '==', null)` 找没有这个字段的记录,`!= null` 找有的。 - 只有标量字段能过滤和排序,比较的值要和字段的类型一致;`array` 字段只能用 `array-contains`。 - 一条查询最多 16 个条件、8 个排序字段,`in` 最多一个。 - 范围条件(`<`、`<=`、`>`、`>=`、`!=`)只能用在一个字段上;有 `orderBy` 时,第一个排序字段必须是它。 - 所有 `orderBy` 的方向相同。 - 有 `in` 条件的字段如果也参与排序,只能是第一个排序字段。 - 不写 `orderBy` 时:有范围条件就按那个字段排,否则按 id 排。 - **缺少某个字段的记录**,不会出现在按这个字段排序的结果里,也不满足这个字段上的范围条件和 `!=`(`!= null` 除外)。 - 字符串按 Unicode 码位比较,区分大小写。 - `limit` 默认 100,最多 1000。一次结果最多 16 MB,超过报 `E_PAYLOAD_TOO_LARGE`,把 `limit` 调小。 - 还有下一页时,结果里有 `nextCursor`,把它原样传给同一条查询的 `startAfter`。 ## 索引规则 集合里的记录**超过 1000 条**以后,每条查询都必须有一个用得上的已声明索引,否则报 `E_DATA_NEEDS_INDEX`,消息里直接写着要加哪个索引: ``` E_DATA_NEEDS_INDEX: query on 'notes' needs an index: add ["status","due"] to data.collections.notes.indexes ``` 1000 条以内不检查,所以开发时一切正常,等用户的数据多了才出错。**从一开始就把每条查询需要的索引声明好。** 一个索引能用于一条翻页查询(`get()`),当且仅当它的字段**正好**是: 1. 这条查询里 `==` 和 `in` 条件的字段,顺序随意; 2. 接着是它的排序字段,顺序和 `orderBy` 一致。只有范围条件、没写 `orderBy` 时,排序字段就是那个范围字段;被 `==`(`== null` 除外)或 `in` 固定的字段不算排序字段。 多一个字段、少一个字段都不行。`count()` 宽松一些:索引以 `==`、`in` 的字段开头(顺序随意),紧接着是范围字段(如果有)就可以,后面再有别的字段也没关系。 | 查询 | 用得上的索引 | | --- | --- | | `where('status', '==', 'open').orderBy('due').get()` | `["status", "due"]` | | `where('status', '==', 'open').get()` | `["status"]`(`["status", "due"]` 不行) | | `where('due', '<', t).get()` | `["due"]` | | `orderBy('due', 'desc').get()` | `["due"]` | | `where('project', '==', p).where('status', 'in', ['open', 'done']).orderBy('due').get()` | `["project", "status", "due"]` 或 `["status", "project", "due"]` | | `where('status', '==', 'open').count()` | 以 `"status"` 开头的任何索引,比如 `["status", "due"]` | | 没有条件、没有排序(按 id 翻页) | 不需要索引 | - **`array-contains` 用不上索引**(数组不能建索引)。在大集合上,它要和一个有索引的 `==` 或范围条件一起用;单独用会报 `E_DATA_NEEDS_INDEX`。 - **`unique` 只管不重复,查询用不上它。** 要按 `unique` 字段查询,也要在 `indexes` 里声明它。 - 只有标量字段能建索引。增删索引不用升版本,新的索引在 gem 下次启动时(安装、升级后)建好。 - **每次调用都有扫描上限**:一次翻页查询最多读约 64 MB 的记录,一次计数最多数 100 万条,超过也报 `E_DATA_NEEDS_INDEX`。命中的记录太多时,加条件缩小范围。 ## 事务 ```js const noteId = ctx.data.newId() await ctx.data.transact(async (tx) => { const project = await tx.get('projects', input.projectId) // 读:马上执行,记下版本 if (!project) throw new Error('没有这个项目') // 抛错:什么都不提交 tx.update('projects', input.projectId, { open_count: (project.data.open_count ?? 0) + 1 }) // 写:先收集 tx.insert('notes', { title: input.title, project: input.projectId }, { id: noteId }) tx.log('readings').append({ value: 1, note: noteId }) }) // 函数返回后一起提交 ``` - 事务里能用:`get`、`insert`、`update`、`delete`、`log(name).append`、`link`、`unlink`。只有 `get` 是异步的、马上执行;写操作先收集起来,函数返回后一次提交,最多 500 个。 - **函数里读不到它自己还没提交的写入。** - 提交时,平台检查函数读过的记录有没有被别人改过(包括被删除,或者读的时候还不存在、现在有了)。改过就**重新运行整个函数**,默认最多 5 次(`transact(fn, { maxAttempts })` 可以改),还不行就抛 `E_DATA_CONFLICT`。所以函数里不要做有外部副作用的事:发请求、写文件、调模型,都放在事务外面。 - 函数抛错时什么都不提交,错误原样抛出;函数正常返回时,`transact` 返回它的返回值。 - 要在事务里预先知道新记录的 id,用 `ctx.data.newId()`。 - 文档和 Blob 的操作不能放进事务,它们各自是单次原子的。事务外的每一次写入也都是原子的。 ## Log Log 存只追加、不修改的条目,比如每次的读数、每次处理过的邮件。 ```js const readings = ctx.data.log('readings') await readings.append({ value: 4.2, note: noteId }) // { id, nodeId } const { items, nextCursor } = await readings.query({ since: Date.now() - 86400e3, limit: 100 }) // items:[{ id, at, data }],默认新的在前 ``` | 方法 | 说明 | | --- | --- | | `append(entry)` | 追加一条。和表一样按声明的字段检查,最大 1 MiB。返回 `{ id, nodeId }`。 | | `query({ since?, until?, limit?, order?, startAfter? })` | `since`、`until` 是毫秒时间戳,包含两端;`order` 默认 `'desc'`(新的在前);`limit` 默认 100,最多 1000。返回 `{ items: [{ id, at, data }], nextCursor? }`,`at` 是追加的时间。一次结果最多 16 MB。 | - **没有修改、删除单条的接口。** 旧条目由平台按 `retention` 清理:`days` 按时间,`maxEntries` 按条数(保留最新的),两个都写时都生效。不写 `retention` 就一直保留到卸载。清理每天最多一次。 - Log 不能写 `version`、`indexes`、`migrations`,字段也不能 `unique`。它的字段声明只管以后写入的条目,可以随时改,不用升版本;旧条目保持写入时的样子。 - 条目里的 `ref`、`blob` 字段和表里的一样自动维护引用:被条目引用的 Blob 不会被清理,直到条目本身被清理。 - 在事务里追加:`tx.log(name).append(entry)`。 ## 文档 文档是 Markdown,按块(段落、标题、列表、代码块……)编辑,每块有一个稳定的 id。 ```js const { id, blockIds } = await ctx.data.docs.create({ title: '规格书', markdown: '# 规格\n\n油 50%。' }) const doc = await ctx.data.docs.get(id) // { id, title, markdown, blocks: [{ id, type, markdown }], createdAt, updatedAt } await ctx.data.docs.replaceBlock(id, blockIds[1], '油 55%。') await ctx.data.docs.insertAfter(id, blockIds[1], '新的一段\n\n再一段') await ctx.data.docs.deleteBlock(id, blockIds[0]) ``` | 方法 | 说明 | | --- | --- | | `create({ id?, title?, markdown })` | 新建一篇,返回 `{ id, nodeId, blockIds }`。 | | `get(id)` | 返回 `{ id, title, markdown, blocks, createdAt, updatedAt }`,没有时是 `null`。`markdown` 是整篇正文,不带块 id;`blocks` 的每一项是 `{ id, type, markdown }`。 | | `list({ limit?, startAfter? })` | `{ items: [{ id, title, updatedAt }], nextCursor? }`。 | | `update(id, markdown, { title? })` | 整篇替换,内容没变的块保留原来的 id。返回 `{ blockIds }`。 | | `insertAfter(docId, blockId, markdown)` | 在某一块后面插入,`markdown` 可以有好几块;`blockId` 为 `null` 时插到最前。返回新块的 `{ blockIds }`。 | | `replaceBlock(docId, blockId, markdown)` | 替换一块;新内容有好几块时,第一块沿用原来的 id。返回 `{ blockIds }`。 | | `deleteBlock(docId, blockId)` | 删除一块。 | | `delete(id)` | 返回 `{ deleted: true \| false }`。 | - 块 id 形如 `b_…`,编辑后不变,可以被引用(见[引用与节点 id](#引用与节点-id))。块的 `type` 是 `heading`、`paragraph`、`list`、`code`、`quote`、`table`、`thematic_break` 或 `html`。 - 每篇文档是数据文件夹里的一个 `.md` 文件,块之间用空行分开,每块的 id 以 `{#b_…}` 的形式写在这块最后一行的末尾(代码块写在开头的那一行,setext 标题写在文字那一行)。文件在应用外被改了,下次读取时平台会发现,按文件重建块。 - **文档文件不加密。** 敏感内容放进集合,不要放进文档。 - 一篇最大 2 MiB,标题最多 500 个字符。id 的规则和记录一样;只差大小写的两个 id 不能同时存在(后建的报 `E_DATA_CONFLICT`),删掉的文档也算(软删除的文档一直占着它的 id,直到卸载);同一个 id、大小写也相同的可以重新建。`get()` 按 id 精确匹配,大小写不同就找不到。 - 要改的文档或块不存在时报 `E_DATA_NOT_FOUND`。 - 文档的操作不能放进事务,每次调用各自是原子的。 - 删除是软删除:`.md` 文件留在磁盘上,直到卸载。 ## Blob Blob 存文件:图片、PDF、音频…… ```js const { hash } = await ctx.data.blobs.put(base64, { mime: 'image/jpeg', name: 'photo.jpg' }) // { hash, size, nodeId } await ctx.data.collection('notes').update(id, { photo: hash }) const blob = await ctx.data.blobs.get(hash) // { base64, mime, name, size },没有时是 null const info = await ctx.data.blobs.stat(hash) // { mime, name, size, createdAt },没有时是 null ``` - 内容以 base64 传进传出,一个最大 20 MiB。哈希是内容的 SHA-256(64 位小写十六进制),同样的内容只存一份。 - **先 `put`,再把哈希存进 `blob` 字段。** 写进 `blob` 字段的哈希必须已经存进来,否则报 `E_INPUT_INVALID`。 - **没有删除接口。** 没有任何地方引用的 Blob(记录或 Log 条目的 `blob` 字段,或者用 `refs.link` 指向它),在最近一次 `put` 满 7 天后由平台清理。同样的内容再 `put` 一次,7 天重新计算;`stat()` 的 `createdAt` 就是最近一次 `put` 的时间。 - Blob 加密存放,不能放进事务。 ## 引用与节点 id gem 的每个对象都有一个节点 id: ``` gem:/rec// 一条记录 gem:/doc/ 一篇文档 gem:/doc/# 文档里的一块 gem:/blob/ 一个 Blob gem:/log// 一条 Log 条目 ``` `insert`、`docs.create`、`blobs.put` 和 `log(name).append` 都返回 `nodeId`。也可以自己构造: ```js const noteNode = await ctx.data.nodeId({ kind: 'rec', collection: 'notes', id }) const blockNode = await ctx.data.nodeId({ kind: 'doc', docId, blockId }) // 不写 blockId 是整篇文档 // 另外两种:{ kind: 'blob', hash }、{ kind: 'log', name, eventId }(eventId 是 append 返回的 id) await ctx.data.refs.link(noteNode, blockNode, 'cites') await ctx.data.refs.backlinks(blockNode) // 谁引用了它:[{ from, to, kind, detached, createdAt }] await ctx.data.refs.outgoing(noteNode) // 它引用了谁 await ctx.data.refs.unlink(noteNode, blockNode, 'cites') ``` | 方法 | 说明 | | --- | --- | | `link(from, to, kind)` | 建一条引用,返回 `{ linked }`;同样的引用已经存在时是 `false`。 | | `unlink(from, to, kind)` | 删一条引用,返回 `{ unlinked }`。 | | `backlinks(to, { kind?, limit? })` | 指向 `to` 的引用。`limit` 默认 100,最多 1000。 | | `outgoing(from, { kind?, limit? })` | 从 `from` 出发的引用。 | - `kind` 是你自己起的名字,匹配 `^[a-z][a-z0-9_.:-]{0,63}$`。以 `field:` 开头的留给平台:`ref` 和 `blob` 字段会自动维护引用(`kind` 为 `field:<字段名>`),不用自己 `link`。 - 目标被删除时,指向它的引用不会被删,而是标记 `detached: true`;目标重新出现时恢复。引用一个还不存在的对象也可以,同样标记为 `detached`。 - **只能引用本 gem 的对象**,用了别的 gem 的节点 id 报 `E_PERM_DENIED`。 - `link`、`unlink` 是写入:声明为只读的工具不要用;它们可以放进事务。 ## 版本与迁移 表的字段可以改,但已经存下的记录必须仍然合规。所以: - **同一个版本里只能「放宽」**:加可选字段、去掉 `required` 或 `unique`、扩大 `enum` 或去掉 `enum`、增删索引、改说明。加 `unique` 也可以,但已有重复值时 gem 启动失败(`E_DATA_UNIQUE`)。 - **其余改动要升 `version` 并写迁移**:删字段、改 `type`、改 `to` 或 `items`、给已有字段加 `required`、缩小 `enum` 或给原来没有 `enum` 的字段加上、加一个必填的新字段。在同一个版本里做这些改动,gem 启动时报 `E_DATA_SCHEMA`。 - **集合的 `kind` 不能改;`version` 不能降。** - Log 不受这些限制,字段可以随时改。 - 从清单里删掉的集合,数据保留但接口访问不到,卸载时删除;之后再加回来,旧数据还在(同样按上面的规则检查)。 - 升级和同版本重装都保留数据。 ```json "notes": { "kind": "table", "version": 2, "migrations": "migrations/notes.js", "fields": { "title": { "type": "string", "required": true }, "status": { "type": "string", "enum": ["open", "done"] } } } ``` ```js // migrations/notes.js:每一步一个函数,v1_to_v2、v2_to_v3…… export function v1_to_v2(data, { id }) { return { title: data.title, status: data.done ? 'done' : 'open' } } ``` - **迁移在 gem 升级后启动时运行**:平台在沙盒里对每条记录依次运行需要的步骤(从版本 1 升到 3,就先 `v1_to_v2` 再 `v2_to_v3`),结果按新的声明检查后写回。只迁移没有删除的记录。每条记录的迁移都留下一条修改记录。 - **每一步收到一条记录的 `data` 和 `{ id }`,必须返回一个普通对象**,所以迁移不能删除记录。 - **迁移函数必须每次都得到同样的结果**:迁移可能中断后重跑。它没有 `ctx`,调用 `Date.now()`、不带参数的 `new Date()` 或 `Math.random()` 会直接报错。 - `migrations` 是包里的一个 `.js`、`.mjs` 或 `.ts` 文件,路径里不能有 `..`;`version` 大于 1 时必须写,安装时检查它确实在包里。缺少需要的某一步,升级在改动任何数据之前就失败。 - **时限**:每一批(最多 500 条记录)60 秒,整个迁移 10 分钟。 - **任何一步失败**(函数抛错、结果不合新声明、缺函数、超时),**整个升级回滚**:数据恢复原样,旧版本继续运行。用户看到的是 `E_ACTIVATION_FAILED`,原因(`E_DATA_SCHEMA: …`)包在消息里。 - 迁移成功后,数据文件夹里保留一份迁移前的 `data.db` 副本(只留最近的一份,同样加密)。 ## 在 Gem Studio 里测试 ### 草稿里怎么写 Gem Studio 草稿的 `manifest.json` 和打包后的清单写法不同: ```json { "data": { "collections": { "notes": { "kind": "table", "fields": { "title": { "type": "string", "required": true } } } } }, "exports": [ { "name": "save_note", "kind": "tool", "entry": "tools/save_note.js", "...": "..." }, { "name": "list_notes", "kind": "tool", "entry": "tools/list_notes.js", "side_effect": "read", "...": "..." } ] } ``` - 存储不用写:概览的权限里没有存储,工具页上也没有。 - 顶层的 `data`:写法和打包后的清单完全一样,要在 `manifest.json` 里手写。 - 只读的工具写 `"side_effect": "read"`。 - `data` 段写错时,测试和安装一样报 `E_PERM_INVALID`(`data: …`)。 ### 测试数据 - **测试用的是这个草稿自己的测试数据**:单独测试工具、agent 的测试对话、测试 SOP,用的是同一份。它和从 Gem Studio 安装出去的那个 gem 的数据完全分开,互不影响。 - **测试数据一直保留**,跨测试,也跨应用重启。只有 `data` 段或 `db/` 下的文件变了,下一次测试才按新的声明重新打开它。 - **所以迁移在下一次测试时运行**:在草稿里升 `version`、写好迁移文件,然后运行任意一次测试。建议先用旧版本写一些数据,再升版本,看迁移后的数据对不对。 - **升了版本就退不回去**:测试数据迁移到新版本以后,把 `version` 改回去会报 `E_DATA_SCHEMA`。 - **迁移成功以后再改迁移文件,什么也不会发生**——数据已经是新版本了。要修正一个写错的步骤,再升一个版本,把修正写成下一步。 - 迁移失败时,测试数据保持原样,下一次测试会再试一次。 - **只有删除草稿才会清掉它的测试数据**(连同它的密钥)。 - 想像用户那样测一次升级:从 Gem Studio 安装,用装好的 gem 写些数据,然后在草稿里提高集合的 `version`、写好迁移文件,再安装一次。每次从 Gem Studio 安装都会自动得到一个更高的版本号,走的是和商店一样的升级流程:迁移在装好的那份数据上运行,失败就整个回滚。 ## SOP 怎么访问数据 SOP 只有 `ctx.call`,没有 `ctx.data`。 - **集合、Log、文档、Blob:调用你自己的工具**,由工具去读写。 - **`kv` 和 `rawSqlite` 可以直接调用**,不用声明。 ```js export default async function (input, ctx) { // 集合里的数据:交给自己的工具 const { id } = await ctx.call('tool.save_note', { title: input.title }) // kv:直接调用 const previous = await ctx.call('storage.kv.get', { key: 'last_note' }) // 没有时是 null await ctx.call('storage.kv.set', { key: 'last_note', value: id }) return { id, previous } } ``` | 调用目标 | 参数 | 返回 | 访问的存储 | | --- | --- | --- | --- | | `storage.kv.get` | `{ key }` | 值,没有时是 `null` | `kv` | | `storage.kv.set` | `{ key, value }` | `{ ok: true }` | `kv` | | `storage.sql.query` | `{ sql, params? }` | 行的数组 | `rawSqlite` | | `storage.sql.insert` | `{ table, row }` | `{ id }` | `rawSqlite` | - 调用目标和其他 `ctx.call` 一样在安装时扫描出来,见[事件与触发](/zh/dev/events.md#触发之后一次运行)。 - SOP 调用的工具在应用崩溃后可能被重跑一次(见[工具 API](/zh/dev/tools.md#一次调用是怎么运行的))。写数据的工具最好经得起重跑,比如用确定的 id 插入:重跑时得到 `E_DATA_CONFLICT`,而不是多出一条记录。 ## 其他存储种类 `ctx.data` 之外还有两种存储,每个 gem 都有,不用申请: | 种类 | 接口 | 适合 | 代价 | | --- | --- | --- | --- | | `kv` | `ctx.storage.kv.get(key)`、`.set(key, value)` | 小设置、同步游标 | 值只能是字符串,最多 1 MiB | | `rawSqlite` | `ctx.storage.sql.query(sql, params)`、`.insert(table, row)` | `ctx.data` 确实做不到的事 | 自己写 SQL,有时限和 256 MB 上限;没有修改记录、没有引用,平台不检查数据 | ### kv ```js const since = Number((await ctx.storage.kv.get('cursor')) ?? 0) // ……处理 since 之后的内容…… await ctx.storage.kv.set('cursor', String(cursor)) ``` - 存在 `data.db` 里(加密),每次 `set` 和别的写入一样留下修改记录。每个工具都有,不用声明。 - 值只能是字符串,要存对象请先 `JSON.stringify`。键 1–512 个字符,不能含控制字符。 - SOP 可以直接调用 `storage.kv.get`、`storage.kv.set`,见上一节。 ### rawSqlite ```js const { id } = await ctx.storage.sql.insert('notes', { title: input.title, created_at: Date.now() }) const rows = await ctx.storage.sql.query('SELECT id, title FROM notes WHERE created_at > ? LIMIT 50', [since]) ``` - 一个私有的 SQLite 数据库 `raw.db`,用 gem 的密钥加密,在单独的进程里运行。每个 gem 都有,不用声明。`query` 返回行的数组,`insert` 返回 `{ id }`(新行的 rowid)。 - **什么时候打开**:包里有 `db/schema.sql` 或 `db/migrations/` 的 gem,启动时就打开,表结构或迁移出错,gem 就启动不了,安装时就能发现。别的 gem 在第一次查询时才打开;从来不用的 gem,磁盘上没有这个文件。 - **表结构写在包里的 `db/` 文件夹**。在查询里 `CREATE TABLE` 也可以,但建议写进 `db/schema.sql`: - `db/schema.sql` 每次打开数据库都会运行,所以只能写可以重复执行的语句(`CREATE TABLE IF NOT EXISTS …`),改不了已有的表。 - 要改表结构,写 `db/migrations/<序号>_<名字>.sql`(比如 `0002_add_lang.sql`)。每个迁移只运行一次,在 `db/schema.sql` 之后按序号从小到大运行。序号不能重复;发布过的序号不要改、不要重用,修正也写成一个新的、更大的序号。文件名不是「数字_名字.sql」的,安装失败。 - 不能用 `ATTACH`、`DETACH`、`PRAGMA`(包括 `pragma_*` 函数)和 `VACUUM`,含有它们的语句会被拒绝。 - 代价:平台不知道这里面存的是什么——没有修改记录、不知道是谁写的、不能引用,也不检查数据。只在 `ctx.data` 确实做不到时用。时限和大小见[上限](#上限)。 ## 加密与隔离 gem 的数据都在它自己的数据文件夹里(组织数据文件夹下的 `gems/store//`): | 内容 | 存放 | 加密 | | --- | --- | --- | | 集合、Log、`kv`、修改记录 | `data.db` | 加密 | | Blob | `blobs/` | 加密 | | `rawSqlite` | `raw.db` | 加密 | | 迁移前留下的副本 | `data.db.bak-…` | 加密 | | 文档 | `docs/.md` | **不加密** | | 定时、事件触发的 SOP 的[运行文件夹](/zh/dev/permissions.md#运行文件夹) | `runs/` | **不加密** | - **加密用的是每个 gem 自己的密钥**:随机生成的 256 位密钥,存放在应用的凭据库里,gem 的代码拿不到。只拿到这些文件、没有这把密钥,读不出加密部分的内容。 - **别的 gem 读不到。** gem 只能通过 `ctx.data` 和 `ctx.storage` 访问自己的数据,没有读别的 gem 数据的接口,别的 gem 的节点 id 会被拒绝(`E_PERM_DENIED`)。沙盒里的代码也不能直接读写数据文件夹里的文件。 - **每个 gem 的代码在自己的沙盒进程里运行**,不和别的 gem 共用进程。 - **卸载时全部删除**:整个数据文件夹(包括明文的文档和运行文件夹)和这把密钥一起删除。重新安装得到的是一份新的空数据。 - **删除是软删除。** `delete` 之后,记录和文档在接口里消失,但内容仍在数据文件夹里(文档的 `.md` 文件也还在),卸载时才一起删除;修改记录里保存着改动前后的内容,按 `eventRetentionDays` 清理。所以不要向用户承诺「删除就从磁盘上抹掉」。 - Gem Studio 的测试数据是单独的一份(见[在 Gem Studio 里测试](#在-gem-studio-里测试)),删除草稿时连同密钥一起删除。 ### 按组织隔离 用户可以在本机建立多个[组织](/zh/guide/organizations.md),每个组织是一套完全独立的数据。gem 按组织安装:同一个 gem 装在两个组织里,就是两份互不相通的安装。 - **各组织独立的内容**:gem 自己的数据(`ctx.data`、`ctx.storage`)、下载的资源文件(清单里的 [`resources`](/zh/dev/manifest.md#resources))、凭据连接的授权、知识库授权,以及写进知识库的内容。 - **看不到其他组织**:gem 运行时只能访问当前组织的数据,无法访问其他组织,也无法得知其他组织是否存在。`ctx.knowledge` 等接口不接收、也不返回组织 id。 - **切换组织会重启应用**:正在执行的工具调用会中断。未完成的 SOP 运行和到期的定时任务会在这个组织下次打开时继续。工具不要依赖进程常驻,需要保留的中间状态请写进数据里。 - **删除组织**:该组织内的 gem 数据随之永久删除。 ## 上限 ### ctx.data 的上限 | 项 | 上限 | | --- | --- | | 一条记录(JSON) | 1 MiB | | 一条 Log 条目 | 1 MiB | | 一篇文档 | 2 MiB;标题 500 个字符 | | 一个 Blob | 20 MiB | | 一次查询(表或 Log)的结果 | 1000 条、16 MB | | 一次调用扫过的数据 | 翻页查询约 64 MB,计数 100 万条 | | 一条查询的条件 / 排序字段 | 16 个 / 8 个;`in` 最多一个、最多 30 个值 | | 一次事务 | 500 个写操作;冲突时默认最多运行 5 次 | | 集合 | 一个 gem 64 个;每个最多 128 个字段、16 个索引(每个 1–8 个字段) | | `enum` | 256 个值 | | 集合名、字段名 | 64 个字符 | | 记录和文档的 id | 1–128 个字符,只含字母、数字、`_`、`-` | | 字段值的嵌套深度(`object`、`any`、不写 `items` 的数组) | 32 层 | | 引用的 `kind` | 匹配 `^[a-z][a-z0-9_.:-]{0,63}$`,不能以 `field:` 开头 | | 迁移 | 每批 60 秒,总共 10 分钟 | | `kv` | 键 1–512 个字符、不含控制字符;值是字符串,最多 1 MiB | | 整个数据文件夹 | 不设上限。超过 4 GiB 时在应用日志里记一条警告,之后每翻一倍(8 GiB、16 GiB……)再记一次,不会阻止写入 | ### rawSqlite 的上限 - `raw.db` 最多 256 MB。 - 一次查询最多返回 10 万行、16 MB,多了请加 `LIMIT`。 - 每次调用最多 10 秒;打开数据库时,`db/schema.sql` 和每个迁移各最多 30 秒。超时的语句会被停掉,调用报错;`db/schema.sql` 超时,这个 gem 这次就启动不了。所以不要写跑不完的查询(没有终止条件的递归 `WITH`、互相触发的触发器)。 ## 错误码 `ctx.data` 和 `ctx.storage.kv` 的错误消息以错误码开头,工具代码可以据此分支: ```js try { await notes.update(id, patch, { ifVersion: note.version }) } catch (err) { if (!err.message.startsWith('E_DATA_CONFLICT')) throw err // 别人先改了:重新读一遍再决定 } ``` | 错误码 | 含义 | | --- | --- | | `E_INPUT_INVALID` | 参数或记录不合格:类型不对、缺必填字段、没有声明的字段或集合、查询的写法不对 | | `E_DATA_CONFLICT` | `ifVersion` 不符;带 `ifVersion` 删除一条不存在的记录;插入的记录 id 或新建的文档 id 已经存在;事务读过的记录被改了 | | `E_DATA_NOT_FOUND` | 要改的记录、文档或块不存在 | | `E_DATA_UNIQUE` | 违反 `unique` | | `E_DATA_NEEDS_INDEX` | 大集合上没有用得上的索引,或者一次调用扫过的数据太多 | | `E_PAYLOAD_TOO_LARGE` | 超过大小上限:记录、Log 条目、文档、Blob、`kv` 的值、一次查询的结果 | | `E_PERM_DENIED` | 用了别的 gem 的节点 id | | `E_DATA_SCHEMA` | 版本倒退、同一版本里做了需要升版本的改动、改了集合的 `kind`、缺迁移函数或迁移失败 | | `E_DATA_CLOSED` | gem 已停用或卸载 | | `E_PERM_INVALID` | `data` 段写得不对,打包、安装和 Gem Studio 测试时报。消息以 `data:` 开头;打包和安装时,形状上的错误以 `notochord.json failed schema validation:` 开头 | | `E_ACTIVATION_FAILED` | 升级或同版本重装后 gem 启动失败,原因(比如 `E_DATA_SCHEMA`、`E_DATA_UNIQUE`)包在消息里;升级失败时整个回滚到旧版本。第一次安装时报的是原来的错误 | `ctx.storage.sql` 的错误不带这些错误码。 # 配置模型 ## 角色 角色是给一类工作指定的模型。在「设置 › AI 设置 › 角色」里,给每个角色从你接入的模型里选一个。 内置四个角色,不能删除,可以不设模型。每个角色下面有一行浅灰色的英文说明,写着它用来做什么。这行说明是固定的,不翻译:AI 读的就是这一行(见下文)。 | 角色 | 设置里的说明 | 建议选的模型 | | --- | --- | --- | | 对话(`chat`) | Everyday conversation and tasks; what a sub-agent gets when no role fits better. | 你平时用的模型。没有单独选模型的地方都用它 | | 识图(`vision`) | Reading images: screenshots, photos, scanned pages. | 能看图的模型 | | 深度思考(`deep`) | The strongest model, for hard thinking and output that must be good: planning, analysis, generating a whole HTML page or a long report. | 你能用的最强的模型 | | 本地(`local`) | A model on this computer, so nothing leaves it: for sensitive content such as emails, personal data, passwords or private documents. | 在这台电脑上运行的模型,比如 Ollama 里的模型,或下载到本机的 GGUF 模型 | 也可以自己加角色:在列表下面填角色名和一句说明,点 **+**,再给它选模型——选好模型才算建好。说明是必填的:AI 靠这句话判断什么时候用这个角色。说明以后可以直接在列表里改;自己加的角色可以删除。 ### AI 怎么用角色 - 一个 agent 可以把一部分工作交给子 agent。交出去的时候,它看得到每个**已经设了模型**的角色和那行说明,可以按任务选一个角色,子 agent 就用这个角色的模型;不选,子 agent 和交出工作的 agent 用同一个模型。没设模型的角色不会出现在它的选择里。 - 所以建议四个内置角色都设上模型:「本地」选一个本机的模型,「深度思考」选最强的模型,「识图」选一个能看图的模型。 - 交不交、交给哪个角色,是 AI 按说明自己判断的,不是强制规则。而且交出去之前,交出工作的 agent 自己的模型已经读过它手上的内容。想让某类内容只交给本机的模型,最可靠的做法是让处理它的 agent 本身就用「本地」角色(在「设置 › AI 设置 › Agent」里选)。 - 子 agent 用哪个模型,在它被创建时就定下来了。之后在设置里改某个角色的模型,只影响之后创建的子 agent。 ### 自己给子 agent 换角色 - **对话里**:点输入框右边的「专家和工具」按钮,「专家」一栏列出这个对话的子 agent,每个下面有一个「模型」下拉框,可以选一个角色,也可以选一个具体的模型。改动从它的下一步开始生效。 - **项目里**:项目顶栏的「Agent」面板列出现在正在为这个项目工作的东西:项目助手的子 agent,以及项目的对话启动的 gem 的 agent 和 SOP。agent 旁边有一个下拉框。子 agent 的改动从它的下一步开始生效;gem 的 agent 改的是它所有运行都用的设置,和「设置 › AI 设置 › Agent」里是同一个设置。 ### 给各个 agent 选模型 「设置 › AI 设置 › Agent」里可以给每个 agent 单独选一个角色或一个模型,从上到下依次是: - **项目**:项目里的助手。 - **对话**:侧边栏「对话」里的助手。 - **Gem Studio**:Gem Studio 里的助手。 - **后台任务**:给对话起标题、总结长对话、描述知识库里的文件夹。 - 再往下是你装的 gem 的 agent。 没选的,都用「对话」角色。 ## 哪些功能会调用你的模型 你在「设置 › AI 设置」里接入的模型,不只在你发消息时被调用。下面列出应用里所有会向模型发请求的地方:什么时候调用、用哪个模型、发过去什么内容。没有列在这里的功能不调用模型。 文中的「`chat` 角色」,就是「设置 › AI 设置 › 角色」里的「对话」角色。 ### 你发起的 | 功能 | 什么时候调用 | 用哪个模型 | 发给模型什么 | | --- | --- | --- | --- | | 对话 | 每次你发消息,或你设的定时任务触发时。一次回复里,助手每用一次工具就多调用一次 | 「Agent › 对话」里选的模型;没选就是 `chat` 角色 | 系统提示词、可用工具的说明、这个对话的内容,以及当前时间、附件文件名这类背景信息 | | 项目里的对话 | 你在项目的助手面板里发消息时。一次回复里,助手每用一次工具就多调用一次 | 「Agent › 项目」里选的模型;没选就是 `chat` 角色 | 同对话,另加项目的背景信息:项目名、项目可用账号的名字(不含密钥)、有哪些表、上次回复以来表里哪些行有改动 | | 子 agent | 对话或项目里的助手把一部分任务交给子 agent 时 | 交出任务时指定了角色,就是这个角色的模型;没指定,和交出任务的 agent 相同。见[角色](#角色) | 同上,范围是子任务自己的对话 | | Gem 的 agent | 对话里用到某个 gem 的 agent,或 gem 的流程(SOP)运行到 agent 这一步时 | 「Agent」里为这个 agent 选的模型;没选就是 `chat` 角色 | 同对话,系统提示词换成这个 gem 自己的 | | Gem Studio 助手 | 你在 Gem Studio 里和助手对话时 | 「Agent › Gem Studio」里选的模型;没选就是 `chat` 角色 | 同对话,另加它读取的草稿文件 | 一个对话用哪个模型,在它创建时就定下来了。之后改设置只影响新建的对话。 ### 应用在后台自动做的 | 功能 | 什么时候调用 | 用哪个模型 | 发给模型什么 | | --- | --- | --- | --- | | 生成对话标题 | 新对话的第一条消息。6 个字符以内的消息直接当标题,不调用模型 | 「Agent › 后台任务」里选的模型;没选就是 `chat` 角色 | 只有这第一条消息的文字 | | 总结长对话 | 对话的大小超过「设置 › 系统 › 自动压缩对话」时,在助手下一次回复之前。默认 10 万 token,设为 0 关闭 | 「Agent › 后台任务」里选的模型;没选就是 `chat` 角色 | 这个对话里较早的部分:你和助手的消息(太长的只留开头和结尾);工具调用只留工具名、缩短后的参数和结果。最近 10 条消息不在其中 | | 知识库 collection 的一句话描述 | 知识库里某个顶层 collection 的子文件夹或文件名有变化之后。每个 collection 最多每 30 分钟一次 | 「Agent › 后台任务」里选的模型;没选就是 `chat` 角色 | 这个 collection 的目录结构和文件名。不包含文档内容 | | 读出扫描件和图片里的文字 | 往知识库导入扫描版 PDF 或图片时,不会先问你。PDF 里没有文字的页面每页调用一次,每个文件最多读前 500 页(超出的页不读,导入结果里会提示);单独选中或拖进来的图片每张调用一次。导入整个文件夹时,里面的图片不读 | `vision` 角色(界面上叫「识图」);没设就是 `chat` 角色 | 这些页面或图片本身(转成灰度、缩小)。读出的文字存进你的知识库 | | Gem 自己发起的调用 | 你安装的 gem 在它的工具或流程里调用模型时。只有安装时被授予 `llm` 权限的 gem 才能这样做 | `chat` 角色 | 由这个 gem 决定。它能读到什么,取决于你授予它的其他权限 | 这一类调用同样按你的模型服务商的价格计费,用量计入「统计」页。它们用掉的 token 不计入「每次运行的 token 上限」。读扫描件是按页调用的,一份几十页的扫描件,费用会明显高于上表的其他几项。 起标题、总结长对话、描述知识库,这三项读到的是你的消息和文件名,内容会发给「后台任务」所用模型的服务商。在「Agent › 后台任务」里给它选一个在这台电脑上运行的小模型(比如「本地」角色),这些内容就不会离开你的电脑。 读图片里的文字时,模型必须能看图。如果用的模型不能看图(比如只支持文字的模型、本地 GGUF 模型),这份文件会导入失败,导入结果里写着原因——到「设置 › AI 设置 › 角色」给「识图」选一个能看图的模型,再导入一次即可。同一个文件用同一个模型读过之后,再次导入不会重新读;应用更新到新版本后的第一次导入除外。 ### 长对话总结是怎么回事 对话很长时,全部内容会超出模型一次能处理的长度。超过你设置的大小后,应用会请「后台任务」的模型(没选就是 `chat` 角色)把较早的部分写成一份总结。此后发给模型的是「这份总结 + 最近的消息原文」,不再是全部历史。 - 聊天记录不受影响。所有消息都还在;记录里会出现一条「已总结较早的消息」的分隔线,点开可以看到总结的内容。 - 总结之后,助手对较早内容的细节只知道总结里写下的那些。需要它用到某个早先的细节时,直接再告诉它一次。 - 对话继续变长,会再总结一次。新的总结是在上一份的基础上写的。 - 这个大小要设得比你所用模型的上下文长度小。应用目前不知道各个模型的上下文长度,不会替你调整。 - 如果总结没写成(比如模型服务暂时不可用),这次回复照常进行,用的是完整的历史。 ### 不调用你的模型的 - 知识库的检索和向量索引,用的是随应用安装的本地 embedding 模型。它在你的电脑上运行,内容不发给任何服务。 - 导入网页:网页由应用自己的浏览器在后台打开、取出正文,不经过模型。网页里的图片不读。 - 「深度思考」「本地」和你自己加的角色,只在你把某个 agent 指向它们、或者 agent 把子任务交给它们时才会被用到;「识图」除此之外,还用来读扫描件和图片里的文字(见上表)。 ## 给 gem 选模型 gem 装好以后会打开它的页面,在「设置 › AI 模型」里给它的每个 agent 选模型:下拉框里先是你的角色(选角色,就跟着你在「角色」里给这个角色配的模型走),再是你加过的所有模型。没选时用 `chat` 角色。「设置 › AI 设置 › Agent」里改的是同一个设置。还没有添加任何模型时,不显示这一项。 有的 gem 要求模型除了文字以外,还能看图片、听音频、看视频,或者生成图片、音频、视频。你在 gem 的页面上或在「Agent」里给它的 agent 选模型时,Notochord 会拿选中的模型对照一份常见模型的能力列表: - 对得上,什么都不显示。 - 列表说这个模型不会,或者列表里没有这个模型(比如你自己给模型起的名字),或者选的角色还没有配模型,会在下拉框下面显示一行提示。你自己确认:模型其实会就不用管;不会就换一个。 - 提示不会阻止安装,也不会阻止使用。 # 知识库 API 用户的长期知识存放在知识库里:关于用户和他认识的人的**事实**,以及日记、留存等**文档**。Gem 通过 `ctx.knowledge` 访问它,能访问多少完全取决于用户在 gem 的页面上打开了哪些权限——装好时一条都没有。 几条先要知道的边界: - **写进知识库的内容归用户,不归 gem。** 卸载 gem 时,它的私有存储会被彻底删除;它写进知识库的记录默认保留,用户可以在卸载时选择一起删除。 - **你拿不到内部 id。** 所有实体、事实、文档、知识库都以不透明的 `ref` 出现,不同 gem 拿到的 ref 各不相同。 - **邮件不在知识库里。** 读邮件请申请 `email` 能力(`ctx.email`);知识库只保存从邮件中提炼出的背景信息。 - **一个组织一套数据。** 个人和各个组织的数据在物理上分开存放,gem 只能看到它所在的那一份,不存在跨组织访问。 ## 概念 | 概念 | 说明 | | --- | --- | | 实体(entity) | 一个人、组织、地点……以及用户本人(`self`)。实体有 `kind`(`person`、`org`、`place` 等)、显示名和标识(邮箱、电话、域名、网址、社交账号)。 | | 事实(fact) | 「主语 · 谓词 · 值」,例如 `self · pref.food.dislike · 香菜`。值可以是任意 JSON;也可以指向另一个实体(例如同事关系)。事实会过期(被新值替代或被忘记),但不会被物理删除。 | | 谓词(predicate) | 事实的类型,点号分隔,至少两级,例如 `location.home.city`。第一级固定为:`identity`、`contact`、`location`、`relation`、`pref`、`schedule`、`work`、`health`、`finance`、`x`(其他)。往下不固定:应用内置了一批常用谓词(见[核心谓词](#核心谓词)),用户和 gem 都可以在任何一级下面再加。 | | 知识库(collection) | 文档的容器,也是授权的单位。内置 `diary`(日记,每天一篇)和 `keep`(留存);用户和 gem 也可以新建。知识库可以嵌套(文件夹里再建子文件夹,`diary` 除外);一篇文档只属于一个知识库。 | | 文档(document) | 标题 + 正文(Markdown)+ 可选的 `meta`。写入后会自动切块、建立全文和语义索引,并识别其中提到的联系人。 | ## 在清单里申请 在 Gem Studio 的草稿 `manifest.json` 里,知识库权限是 gem 级字段,每个工具默认都能用(某个工具不需要时,在它的 `permissions` 里不列 `knowledge`)。也可以在 Gem Studio 概览的「知识库」一栏里逐条添加,写错的会直接列出来,和安装时的检查一样: ```json { "name": "recipes", "description": "保存用户打过分的菜谱", "kind": "tool", "entry": "tools/rate.js", "knowledge": { "permissions": ["facts.read:self:pref.food.*", "facts.write:self:pref.food.*", "collections.create"], "predicates": [ { "predicate": "pref.food.rated", "cardinality": "many", "description": "你打过高分的菜,用来推荐相似的菜。例如:红烧肉", "aliases": ["高分菜"] } ], "reason": "记住你喜欢的菜,并把菜谱存进你的知识库" } } ``` 打包后的 `notochord.json` 里,同样的内容写在 `permissions.knowledge`,需要 `ctx.knowledge` 的工具在自己的 `permissions` 里写 `"knowledge": true`(Studio 生成清单时会自动完成): ```json { "permissions": { "knowledge": { "permissions": ["facts.read:self:location.home.*"] } }, "tools": [{ "name": "home", "entry": "tools/home.mjs", "permissions": { "knowledge": true }, "...": "..." }] } ``` - `permissions`:权限字符串列表,见下一节。 - `predicates`:[核心谓词](#核心谓词)都不合适时,这个 gem 新加的谓词,安装时写进全局词表。它**不是权限**,只是声明词汇。`cardinality` 为 `one`(同一时间只有一个值,例如居住城市)或 `many`(可以有多个值,例如喜欢的菜)。 - `description` **必填**,最多 200 个字符,是**写给用户看的**:是什么、gem 为什么需要它、举个例子。gem 的页面上会把它显示在用户的值旁边,用户在那里补填或修改。 - `reason`:可选,显示在安装界面上。 ## 权限字符串 格式统一为 `数据类型.动作:范围`。gem 的页面(「设置 › 知识库」)把每一条翻译成一句话列给用户,**全部默认关闭**,用户逐条打开;打开或关掉都在下一次调用时立即生效。没打开的权限,调用会被拒绝。 | 权限 | 安装界面显示为 | 说明 | | --- | --- | --- | | `facts.read:<主语>:<前缀>` | 读取你的居住地 | 主语是 `self` 或实体 kind(如 `person`)。前缀写 `a.b.*`(该前缀下所有谓词)或完整谓词 `a.b.c`。**前缀至少两级**,`self:*`、`self:location.*` 都会被拒绝。 | | `facts.write:<主语>:<前缀>` | 记录你的饮食偏好 | 必须被某条 `facts.read` 覆盖。 | | `entities.resolve:` | 根据邮箱等查询联系人的名字 | 单点查询:拿已知的邮箱或名字换回 ref 和显示名,不能列出全部。 | | `entities.read:` | 查看你的联系人列表 | 列出该 kind 的全部实体及其标识。 | | `entities.write:` | 新增和编辑联系人 | 新建实体、添加标识。包含 `entities.resolve:`。 | | `collections.read:<目标>` / `collections.write:<目标>` | 读取你的日记 | 目标是 `diary`、`keep` 或 `user-selected`(用户装好后在 gem 的页面上勾选具体知识库)。对一个知识库的授权包含它里面的所有子文件夹,包括以后新建的。 | | `collections.create` | 创建自己的知识库 | 新建的知识库自动对创建者可读可写,它里面的子文件夹也是。要建在某个知识库里面(`parent`),还需要对那个知识库有写权限。 | | `changes.manage` | 查看和撤销所有应用写入的知识 | 可以查看、撤销、修改其他写入者的记录(仍限于上面授权的范围)。 | | `origin.override` | 代表其他应用写入数据 | 写入时可以自己指定 `origin` 和 `source`,给「整理类」gem 用。 | 申请尽量窄:只读天气所需的城市,就只申请 `facts.read:self:location.home.*`。 两种失败方式要区分清楚: - **查询类方法**(`profile.get`、`facts.lookup`、`search`、`collections.list`、`changes.list`)遇到没授权的部分**不报错**,只是不返回。你无法据此判断某条数据是否存在。 - **针对单个对象的方法**(写入、按 ref 读取、`entities.*`、`documents.list` 指定的知识库、创建)没有权限时**直接抛错**,错误信息里包含 `lacks knowledge permission <缺少的权限>`。 ## API 参考 所有方法都是异步的。`collection` 参数可以是内置名 `'diary'`、`'keep'`,也可以是 `collections.list()` / `collections.create()` 返回的 ref。 ### 读取事实 ```js await ctx.knowledge.profile.get('location.home') // → [{ predicate: 'location.home.city', value: 'Utrecht' }] ``` 常驻画像:用户本人被置顶(pinned)的事实,再与你的 `facts.read` 权限取交集。前缀可省略。 ```js await ctx.knowledge.facts.lookup('self', 'pref.food') // 或 lookup(entityRef, 'relation') // → [{ ref, predicate, value, object: entityRef | null, validFrom }] ``` 主语当前有效的事实。需要对应主语 kind 的 `facts.read`。**不会返回**来源、写入者、置信度,也不会返回系统推测(inferred)的事实。 ### 写入事实 ```js await ctx.knowledge.facts.put({ subject: 'self', // 默认 'self';也可以是实体 ref predicateHint: 'pref.food.rated',// 最好写完整谓词;也接受别名或一句描述 description: '用户打过高分的菜', // 可选,新谓词时有用 value: '番茄牛腩', object: entityRef, // 可选:值指向另一个实体 sourceRef: 'record/123', // 可选:指回你自己存储里的记录 runId: 'nightly-2026-09-29' // 可选:同一批写入共用,可整批撤销 }) // → { status: 'applied' | 'duplicate' | 'conflict', ref, predicate } ``` - **先解析谓词,再检查权限。** `predicateHint` 会先按「完整谓词 → 别名 → 描述」归一到词表里的谓词,再用归一后的结果检查 `facts.write`。含糊的 hint 可能落到你没有权限的前缀下而被拒绝;完全匹配不上时会落到 `x.`,同样需要权限。所以请直接使用完整谓词名。 - `duplicate`:同一主语、谓词、值已经存在。 - `conflict`:`cardinality: one` 的谓词已经有**用户亲口说的值**,你的值不会写入,原值不变;用户会在「查看改动」里看到这次冲突。 - `cardinality: one` 且原值不是用户说的:旧值自动过期,新值生效。 - `origin`、`source` 默认由系统填写(`gem:<你的 id>` / `extracted`),自己指定需要 `origin.override`。 ```js await ctx.knowledge.facts.retract(factRef) // → { status: 'retracted' | 'already_retracted' } ``` 让事实过期(不物理删除)。只能撤回自己写的;撤回别人写的需要 `changes.manage`。 ### 实体 ```js await ctx.knowledge.entities.resolve({ email: 'zhangsan@acme.nl' }) // kind 默认 'person' await ctx.knowledge.entities.resolve({ name: 'Acme' }, { kind: 'org' }) await ctx.knowledge.entities.resolve({ email, name }, { create: true }) // 找不到就新建,需要 entities.write // → { ref, kind, displayName, created } | null ``` 可用的标识:`email`、`phone`、`domain`、`url`、`handle`(写成 `平台:账号`,例如 `github:zhangsan`),以及 `name`。 ```js await ctx.knowledge.entities.get(ref) // → { ref, kind, displayName, identifiers },需要 entities.read await ctx.knowledge.entities.list('person', { query }) // 需要 entities.read await ctx.knowledge.entities.addIdentifier(ref, 'phone', '+31 6 1234 5678') // → { status: 'added' | 'exists' },需要 entities.write ``` 实体只能由用户(在「知识库」页里)或助手删除、合并,gem 做不到。如果你把实体 ref 存在自己的存储里,要预料到它可能失效: - **被合并**:旧 ref 仍然可用,会自动指向保留下来的那个实体。 - **被删除**:再用这个 ref 调用会抛出「找不到」错误(错误信息含 `no entity`)。这时请丢弃这个 ref,必要时用 `entities.resolve` 重新查找。 ### 检索与文档 ```js await ctx.knowledge.search('番茄牛腩', { collections: ['diary'], timeRange: { from: '2026-09-22', to: '2026-09-22' }, limit: 10 }) // → [{ document: ref, title, collection: ref, text, locator, meta }] ``` 全文 + 语义混合检索,只在你可读的知识库里查。`collections` 里写的知识库连同它里面的所有子文件夹一起查。`timeRange` 接受毫秒或 ISO 日期(只写日期表示整天),对日记按 `meta.date` 过滤。`query` 可以为空,此时按时间倒序列出。`entities: [entityRef]` 只返回提到这些人的段落。 ```js await ctx.knowledge.documents.upsert({ collection: 'diary', key: '2026-09-29', // 传 key:同一个 key 重复写入会覆盖,不会产生重复文档 title: '2026-09-29', body: '# 今天\n\n……', meta: { date: '2026-09-29' } }) // → { status: 'created' | 'updated' | 'unchanged', ref } ``` - 传 `key` 得到确定的文档,适合「每天一篇」「每条记录一篇」这类幂等写入;内容没变时返回 `unchanged`,不会重复建索引。 - 传 `ref` 更新已有文档;两者都不传则新建。 - `text` + `sourceUri` 用于导入的外部内容:只建索引,不保存正文。`sourceUri` 是原文所在的网址,用户可以点开,所以只能是 `http(s)://` 网址,而且主机要在你的 [`permissions.network.hosts`](/zh/dev/permissions.md) 里(没有申请联网的 gem 不能写 `sourceUri`);其他地址(`file:` 等)会被拒绝。 - 覆盖或删除别人写的文档需要 `changes.manage`。 ```js await ctx.knowledge.documents.get(ref, { range: { from: 0, to: 3 } }) // 全文,或按块取片段 await ctx.knowledge.documents.getByKey('diary', '2026-09-29') // → 文档 | null await ctx.knowledge.documents.list('diary', { timeRange: { from: '2026-09-01' } }) await ctx.knowledge.documents.delete(ref) ``` ### 知识库 ```js await ctx.knowledge.collections.list() // → [{ ref, name, builtin: 'diary' | 'keep' | null, kind, canWrite, parent }],只列你能读或写的 await ctx.knowledge.collections.create('菜谱') // → { ref, name, parent: null },需要 collections.create await ctx.knowledge.collections.create('川菜', { parent: ref }) // 建在 ref 这个知识库里面,还需要对它有写权限 ``` - 知识库可以嵌套。`list()` 返回的是平铺的列表,`parent` 是它所在知识库的 ref;顶层的是 `null`。你看不到的上级(只被授权了其中一个子文件夹)也是 `null`。 - `parent` 可以是 `'keep'` 或一个知识库 ref;`'diary'` 里不能建。最多 5 层(顶层算第 1 层),再往下建会报错。 - `documents.list(collection)` 只列直接放在这个知识库里的文档,不包含子文件夹里的;要连子文件夹一起找用 `search`。 - **用户可以移动知识库和文档**(在应用的知识库页面里,gem 不能移动)。你能读写什么跟着位置走:移进你有权限的知识库里的内容,你从此就能读到;移出去的就读不到了。移动不改变 ref。用 `key` 写的文档(日记、你按 key 管理的文档)用户移动不了,所以 `getByKey` 和按 key 写入总能在原来的知识库里找到它。 ### 变更与撤销 ```js await ctx.knowledge.changes.list({ since: '2026-09-28', status: 'conflict' }) // → [{ ref, runId, actor, op, summary, status, proposed, createdAt }] await ctx.knowledge.changes.revert({ runId: 'nightly-2026-09-29' }) // 或 revert(changeRef) ``` 没有 `changes.manage` 时只能看到、撤销自己的变更。每次写入都会记录变更;不传 `runId` 时,写入归到当前这次运行的 `runId`,也可以自己传一个,把一批写入归在一起。 ## 写入规则 - **每个 gem 每天最多 500 次写入**(按本地日期计算),写入的文字每天合计最多 5000 万字符(有 `collections.bulk` 的是 2 亿)。这是系统保护,用户不能调整。超出后写入会抛出含 `quota` 的错误。 - **单次写入的上限**:一篇文档的正文最多 500 万字符,标题最多 1000 字符,`meta` 和一条事实的值序列化成 JSON 后各最多 64 KB。实体的显示名、标识的值和知识库的名称也和标题一样,最多 1000 字符。 - **所有写入都可撤销**,用户可以在「查看改动」里按单条或按批次撤销。 - **不要把知识库当你的数据库。** 运行状态、缓存、草稿放在 gem 自己的数据里([`ctx.data`](/zh/dev/storage.md));只有对用户长期有价值、离开你的 gem 也有意义的内容才写进知识库。 ## 卸载与数据归属 用户卸载 gem 时: 1. 确认对话框显示你贡献了多少事实、文档和知识库,用户选择「保留」(默认)或「一起删除」。 2. 你的私有存储、授权记录被删除。 3. 保留:知识原样留着,来源标记仍指向你。删除:你写入的文档被删除,事实过期,你创建的知识库连同其中的一切一起删除(包括用户后来放进去或移进去的),这一步也可以由用户撤销。 4. 你声明的谓词留在词表里,因为已有事实可能还在引用它们。 重新安装时,你拿不到以前的私有数据;但只要有读权限,就能通过 `ctx.knowledge` 读到之前保留下来的内容。 ## 核心谓词 常用谓词已经内置。请优先使用它们——用户可能已经填过,gem 页面上也会用应用里的名字显示——都不合适时才在清单里声明新的。下面是全部内置谓词: | 谓词 | 基数 | 含义 | | --- | --- | --- | | `identity.name` / `identity.nickname` / `identity.birthday` / `identity.language` | one / many / one / many | 姓名、称呼、生日、会说的语言 | | `contact.email` / `contact.phone` / `contact.address` | many | 联系方式 | | `location.home.city` / `location.home.address` / `location.work.city` / `location.timezone` | one | 居住城市、住址、工作城市、时区 | | `relation.family` / `relation.spouse` / `relation.friend` / `relation.colleague` / `relation.member_of` | many(spouse 为 one) | 人际关系,值通常用 `object` 指向另一个实体 | | `work.company` / `work.title` / `work.project` | one / one / many | 公司、职位、项目 | | `pref.food.like` / `pref.food.dislike` / `pref.food.diet` / `pref.food.cuisine` | many | 饮食偏好 | | `pref.communication.style` / `pref.communication.language` | one | 沟通风格、回复语言 | | `schedule.routine` / `schedule.event` | many | 作息、日程 | | `health.allergy` / `health.condition` | many | 过敏、健康状况 | | `finance.currency` | one | 常用货币 | ## 用户看到什么 gem 的页面「设置 › 知识库」里,每条权限都有一个开关,默认关闭。下面列出你的 `facts.read|write:self:…` 覆盖到的、关于用户本人的事实,以及用户现在的值: - 内置谓词用应用里的名字显示(例如「居住城市」); - 你新加的谓词显示你写的 `description`,标着「这个 gem 新加的」; - 用户可以直接修改、补填、删除。这样写下的值算用户亲口说的:`cardinality: one` 的谓词上,你的写入不会覆盖它(`conflict`)。 关于联系人的事实(`facts.read:person:…`)是一人一份,在 Vault 的联系人页里改,不在这里列出。 ## 示例 只读的天气 gem: ```js // 权限:facts.read:self:location.home.* export default async function (input, ctx) { const [home] = await ctx.knowledge.profile.get('location.home') if (!home) return { error: '不知道你住在哪里' } return { city: home.value } } ``` 读写的菜谱 gem: ```js // 权限:facts.read:self:pref.food.*、facts.write:self:pref.food.*、collections.create export default async function (input, ctx) { const cols = await ctx.knowledge.collections.list() const col = cols.find((c) => c.name === '菜谱') ?? (await ctx.knowledge.collections.create('菜谱')) const fact = await ctx.knowledge.facts.put({ predicateHint: 'pref.food.rated', value: input.dish, sourceRef: `record/${input.id}` }) const doc = await ctx.knowledge.documents.upsert({ collection: col.ref, key: input.id, title: input.dish, body: input.recipe }) return { fact: fact.status, doc: doc.status } } ``` ## 还不支持 - 知识库的变化不会通知 gem(这一版不发布知识库事件,见[事件与触发](/zh/dev/events.md#可用的事件))。需要感知变化请用定时触发去读,`documents.list` 每项带 `updatedAt`。 - 读取其他 gem 的私有存储:不会开放。gem 之间要共享的信息,写进知识库。 # Gem:安装与使用 Gem 是一个装进 Notochord 的小应用:它带着自己的工具、自动化流程(SOP),有的还带一个 AI 助手。装好以后,你可以在聊天里让 AI 用它,它的流程也会按自己的规则自动运行(比如每来一封新邮件运行一次)。 ## 已装的 gem 在哪 打开「首页」,上方是你的项目,下方「Gem」一栏是你装的所有 gem,第一个图标是「商店」。点一个 gem,打开它的页面。 ## 安装 在首页点「商店」,找到想要的 gem,点 **安装** 就装好了,不再弹确认——它是谁做的、能做什么,在商店的介绍里已经写明。装好以后直接打开这个 gem 的页面,在那里做设置(见下一节)。 Notochord 安装前会检查这个包:商店里的包必须有官方签名,内容没有被改动过,声明的权限写法合法。检查不通过就装不上。 **刚装好的 gem 什么都还碰不到。** 它要读的邮箱、要用的知识库内容、要用的外部账号,都要你在它的页面上打开以后才能用;在那之前它的请求会被拒绝。 ## Gem 的页面 点开一个 gem 看到的页面,从上到下: - **还需要完成设置**:还有需要你处理的事时才出现,比如它需要的账号还没连上、还没选它能读的邮箱或能用的知识库,或者给它选的模型可能做不了它要做的事。 - **设置**:装完以后的设置都在这里,随时可以回来改,改了马上生效。 - **AI 模型**:它的 AI 助手用哪个模型。下拉框里先是你的角色(选角色,就跟着你在「设置 › AI 设置 › 角色」里给这个角色配的模型走),再是你加过的所有模型。没选时用 `chat` 角色。还没加任何模型时不显示这一项。 - **账号**:它要用的外部账号(比如某个服务的 API key)。选一个已有的账号,或者新建一个,点 **保存**。没连上之前,它用到这个账号的功能会失败。 - **它能读的邮箱**:它能读哪些邮箱。装好时一个都没有,勾选的才能读;也可以勾「所有账号,包括以后添加的」。它的邮件触发只对你允许的邮箱生效。 - **知识库**:它能用知识库里的哪些内容。每一条都默认关闭,打开的才能用;需要你挑文件夹的,在这里勾选,勾选一个文件夹包括它里面的所有子文件夹。以后你把别的文件夹或文档移进这个文件夹,它也能读到;移出去就读不到了。下面还列出它用到的、关于你的事实和你现在的值(比如居住城市、喜欢的菜),可以直接修改或补填;gem 自己新加的那几项带着作者写的说明。你在这里写的,gem 不能改掉。 - **运行记录**:它的自动化流程每次运行的记录,最新的在最上面。点一条可以看这次运行的每一步做了什么、结果是什么。只有带自动化流程的 gem 有这一节。所有 gem 的运行也可以在首页的「运行记录」里一起看,在那里还能重试失败的运行、取消正在运行的。 - **它能做什么**:它能做什么——会访问哪些网站、会不会读邮件和知识库、包含哪些自动化流程、什么时候运行。下面的 **包含的内容** 列出它包含的工具、AI 助手和技能。 - **问题**:这个 gem 自己的代码出错的次数(只记哪里出错、出了几次,不记内容)。可以复制下来发给它的开发者。 ## 卸载 在 gem 的页面右上角点 **删除**。它的工具、流程、定时任务和它自己存的数据都会删掉,不能恢复。它写进你知识库的内容默认保留,也可以选择一起删掉。 ## 让模型能做 gem 要做的事 有的 gem 要求模型能看图片、听音频等等。给它选模型时,如果这个模型可能做不到,下拉框下面会有一行提示。详见[配置模型](/zh/guide/models.md#给-gem-选模型)。 # 工具 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`,用不了资源文件。 # 事件与触发 触发器回答一个问题:gem 里的一段流程(SOP)**什么时候开始运行**。一共三种方式: | 方式 | `trigger` 的写法 | 什么时候运行 | 输入从哪来 | | --- | --- | --- | --- | | [手动](#手动触发) | `{ "type": "manual" }` | 有人把它当工具调用时 | 调用方传的参数 | | [定时](#定时触发) | `{ "type": "cron", "expr": "0 8 * * *" }` | 到了 cron 表达式指定的时间 | 写在触发器里的固定参数 `args` | | [平台事件](#事件触发) | `{ "type": "event", "source": "email", "name": "onNewMessage" }` | 平台发布了这个事件 | 事件的内容 | **gem 之间不能直接互相触发。** 事件只有平台自己发布的那几种(见[可用的事件](#可用的事件));gem 不能发布事件,也不能订阅另一个 gem 的活动。 ## 在哪里声明 触发器写在 SOP 的声明上,每个 SOP 有且只有一个。 在 Gem Studio 的草稿里,是 `manifest.json` 中 `kind` 为 `sop` 的那一项: ```json { "name": "digest", "kind": "sop", "entry": "workflows/digest.js", "description": "每天早上汇总新邮件。", "input_schema": { "type": "object", "properties": { "limit": { "type": "number" } } }, "trigger": { "type": "cron", "expr": "0 8 * * *", "args": { "limit": 20 } } } ``` 打包后的清单(`notochord.json`)里是 `workflows[]` 的一项,字段含义相同,只是 `input_schema` 写作 `inputSchema`: ```json "workflows": [ { "name": "digest", "entry": "workflows/digest.js", "description": "每天早上汇总新邮件。", "inputSchema": { "type": "object", "properties": { "limit": { "type": "number" } } }, "trigger": { "type": "cron", "expr": "0 8 * * *", "args": { "limit": 20 } } } ] ``` 不写 `trigger` 等于 `{ "type": "manual" }`。 不管声明的是哪一种,SOP 的写法都一样——一个函数,第一个参数就是这次触发带来的输入: ```js export default async function (input, ctx) { // input:手动调用的参数 / 定时触发的 args / 事件的内容 const mail = await ctx.call('tool.list_new_mail', { limit: input.limit ?? 20 }) return { processed: mail.messages.length } } ``` ## 手动触发 每个 SOP 都会被注册成一个工具,名字是 `gem..`,参数就是它的 `input_schema`。`manual` 的意思是「只有这一种启动方式」;声明了定时或事件触发的 SOP 同样可以被手动调用。 - **谁能调用**:聊天里的 agent。这个工具不在会话的工具白名单里时,第一次调用会先问用户是否允许。 - **返回什么**:SOP 的返回值;运行留下了文件时是 `{ "output": <返回值>, "folder": "runs/…" }`,`folder` 是它在对话工作目录里的[运行文件夹](/zh/dev/permissions.md#运行文件夹)。如果流程中途挂起了(等待用户审批,或一次较长的等待),调用方拿到的是 `{ "status": "suspended", "run_id": "…", "folder": "runs/…" }`,流程之后在后台继续,结果仍然写进这个文件夹。失败或被取消时调用方收到错误。 - **文件和文件夹**:路径是普通字符串,工具用 `workspace:` 前缀在这次对话的文件夹里读写,见[权限模型](/zh/dev/permissions.md#对话文件夹)。 - **用谁的权限运行**:SOP 自己的——安装时记录下来的那一份,加上用户在 gem 页面上打开的。调用它的会话有什么权限,和它能做什么无关。 - 一个 SOP 不能调用另一个 SOP,也不能调用别的 gem 的工具。 ## 定时触发 ```json "trigger": { "type": "cron", "expr": "30 7 * * 1-5", "args": { "limit": 20 } } ``` `expr` 是标准的 5 段 cron 表达式,依次是:分、时、日、月、星期。 | 写法 | 含义 | 例子 | | --- | --- | --- | | `*` | 任意值 | `* * * * *` 每分钟 | | 数字 | 这个值 | `0 8 * * *` 每天 8:00 | | `a-b` | 范围 | `0 9-17 * * *` 9 点到 17 点的每个整点 | | `a,b,c` | 列表 | `0 0 1,15 * *` 每月 1 号和 15 号的 0:00 | | `*/n`、`a-b/n` | 步长 | `*/15 * * * *` 每 15 分钟 | 要注意的地方: - **按电脑的本地时间算**,不是 UTC。用户换了时区,触发时刻跟着变。 - **最小粒度是一分钟**。调度器每分钟检查一次,所以实际开始时间可能比表达式晚不到一分钟。 - **星期用 0–6,0 是星期日**。不支持 `7`,不支持 `MON` 这样的名字,也不支持 `@daily` 这类简写。 - **「日」和「星期」同时写了具体值时,两个都要满足**(`0 8 13 * 5` 是「既是 13 号又是星期五」的 8:00)。这一点和有些 cron 实现不同。 - 调度器读不懂的表达式,或者永远不会到点的表达式(比如 2 月 31 日),**在打包和安装时就会报错**。 **输入**:到点时没有人在场提供参数,所以输入是触发器里写死的 `args`(不写就是 `{}`)。`input_schema` 里的必填项必须都能在 `args` 里找到,否则打包和安装时报错。这种 SOP 没有对话:它的工具用 `workspace:` 读写的是用户在 gem 页面上给这个 gem 选的工作文件夹,没选就没有(见[权限模型](/zh/dev/permissions.md#定时和事件触发的-sop))。事件触发的 SOP 也一样。 **应用没开着的时候**不会运行。重新打开后: - 只错过了一次(下一次还没到点)——立刻补跑这一次; - 已经错过了不止一次——不补跑,等下一次到点。 所以「每天 8:00」的任务,电脑 10 点才开机,会在 10 点跑一次;关机三天再开,不会连跑三次,而是等到下一个 8:00。 ## 事件触发 ```json "trigger": { "type": "event", "source": "email", "name": "onNewMessage" } ``` 事件由平台发布,订阅了它的 SOP 各自开始一次运行,事件的内容就是 SOP 的 `input`。 ### 可用的事件 | `source` | `name` | 什么时候发布 | | --- | --- | --- | | `email` | `onNewMessage` | 用户的邮箱收到一封新邮件 | 这一版只有这一个。写了表里没有的事件——包括拼错的名字,和 `"source": "gem:<别的 gem>"` 这种指向另一个 gem 的写法——**打包和安装时会报错**,不会装上以后悄悄不触发。 这一版**不发布**的: - **知识库的变化**(`knowledge.document.changed`)。要处理用户新加的内容,用[定时触发](#定时触发),在运行时读:`ctx.knowledge.documents.list(collection)` 每项带 `updatedAt`,自己记下处理到哪里。见[知识库 API](/zh/dev/knowledge.md)。 - **gem 被卸载**(`gem.uninstalling`)。gem 之间不共享私有数据;要给别的 gem 用的信息,写进知识库。 ### email.onNewMessage 用户添加的邮箱账号每 5 分钟同步一次收件箱;每同步到一封新邮件,在它存好之后发布一次。 ```json { "accountId": "3f6c…", "accountEmail": "me@example.com", "id": "9d2e…", "messageId": "", "fromAddr": "alice@example.com", "toAddrs": ["me@example.com"], "subject": "周五的会议", "date": 1760256000000 } ``` | 字段 | 说明 | | --- | --- | | `accountId` | 收到这封信的账号在应用内部的 id | | `accountEmail` | 收到这封信的账号自己的地址(不是从信的收件人里取的) | | `id` | 这封信在应用里的 id,读正文时和 `accountId` 一起交给 `ctx.email.getMessage` | | `messageId` | 邮件的 Message-ID;没有时是服务器上的 UID | | `fromAddr`、`toAddrs`、`subject` | 发件人、收件人列表、主题 | | `date` | 邮件的日期,毫秒时间戳 | 事件里**没有正文**,只有信封。 - **只有收件箱**(INBOX)里的新邮件会触发。 - **从收到到触发最多隔 5 分钟**(同步间隔)。应用没开着时到的邮件,下次打开同步到时再触发。 - **新添加账号时的历史邮件不触发**。账号第一次同步会回填一段历史,这期间一封也不发布。 - **只收到用户允许的账号**。用户在 gem 页面的「设置 › 它能读的邮箱」里选;没被选中的账号来的邮件不会触发这个 gem。**装好时一个账号都没有**,所以用户选之前这个触发不会发生。用户也可以选「所有账号」(以后新添加的也算)。改了以后,下一封邮件就按新的设置来。 - **要正文,由工具去读**。SOP 本身不能读邮件:给一个工具申请 `email` 权限(gem 级 `"email": true`),SOP 把事件里的 `accountId` 和 `id` 交给这个工具,工具调用 `ctx.email.getMessage({ accountId, id })` 拿到纯文本正文和附件列表。和别处一样,只能读用户在 gem 页面上允许的账号。见[工具的 `ctx.email`](/zh/dev/tools.md#ctxemail)。 一个完整的例子,每来一封新邮件,先由工具读出正文,再交给 gem 自己的 agent 处理: 清单里的相关部分(`agent.assistant` 是同一个 gem 里的一个 agent,这里省略了它的声明): ```json "permissions": { "email": { "read": true } }, "tools": [ { "name": "read_email", "entry": "tools/read_email.js", "description": "Read one email in full from the user's mailbox. Returns { subject, fromAddr, body }.", "inputSchema": { "type": "object", "required": ["accountId", "id"], "properties": { "accountId": { "type": "string" }, "id": { "type": "string", "description": "The message id the email.onNewMessage event gives." } } }, "permissions": { "email": { "read": true } }, "sideEffect": "read" } ], "workflows": [ { "name": "on_new_message", "entry": "workflows/on_new_message.js", "description": "Runs the assistant agent against every newly received email.", "inputSchema": { "type": "object", "required": ["accountId", "id", "messageId"], "properties": { "accountId": { "type": "string" }, "id": { "type": "string" }, "messageId": { "type": "string" }, "subject": { "type": "string" }, "fromAddr": { "type": "string" } } }, "trigger": { "type": "event", "source": "email", "name": "onNewMessage" } } ] ``` 在 Gem Studio 的草稿里,`permissions.email` 写成 `manifest.json` 顶层的 `"email": true`。 `tools/read_email.js`: ```js export default async function (input, ctx) { const message = await ctx.email.getMessage({ accountId: input.accountId, id: input.id }) return { subject: message.subject ?? '', fromAddr: message.fromAddr ?? '', body: message.body } } ``` `workflows/on_new_message.js`: ```js export default async function (input, ctx) { const email = await ctx.call('tool.read_email', { accountId: input.accountId, id: input.id }) await ctx.call('agent.assistant', { messageId: input.messageId, subject: email.subject, fromAddr: email.fromAddr, body: email.body }) return { ok: true, messageId: input.messageId } } ``` ## 触发之后:一次运行 三种方式启动的都是同一种东西——SOP 的一次运行。 - **每次触发是独立的一次运行**,有自己的记录。一封邮件一次、一个定时点一次;同一个事件有多个 gem 订阅时,各跑各的。 - **输入先过 `input_schema`**。不满足时这次运行不会开始:手动调用的调用方收到错误;定时和事件触发则只在日志里留一条记录。给事件触发的 SOP 写 `input_schema` 时,只把真正要用的字段列为必填。 - **有自己的文件夹**。从聊天里调用的,放在那个对话的工作目录里;定时、事件触发的,放在 gem 自己的数据文件夹里。这次运行调用的工具和 agent 都在这里读写,见[运行文件夹](/zh/dev/permissions.md#运行文件夹)。 - **数据通过工具读写**。SOP 没有 `ctx.data`:要读写 gem 的集合、Log、文档、Blob,调用 gem 自己的工具(`ctx.call('tool.save_note', …)`)。`storage.kv.get`、`storage.kv.set`、`storage.sql.query`、`storage.sql.insert` 可以直接调用,不用声明。见 [SOP 怎么访问数据](/zh/dev/storage.md#sop-怎么访问数据)。 - **能做什么在安装时就定了**。SOP 里每个 `ctx.call('…')` 的目标在安装时被扫描出来、记录下来(列在 gem 的页面上);运行时调用不在这份清单里的目标会被拒绝。 - **有上限**。默认一次运行最多活跃 10 分钟、最多 500 次 `ctx.call`,可以用 `limits` 调整:`"limits": { "maxDurationMs": 600000, "maxCalls": 500 }`。挂起等待的时间不算在内。 - **可以挂起**。`ctx.call('wait.approval', …)` 会停下来等用户批准,`ctx.call('wait.sleep', …)` 可以等一段时间;应用重启后挂起的运行会接着跑。 - **同时运行的 SOP 有上限**(目前是 20 个),超出的排队。 - **在哪看**:gem 页面的「运行记录」列出这个 gem 每次运行和它的每一步;应用首页的「运行记录」列出所有 gem 的运行。等待审批的在那里批准或拒绝,运行中的可以取消,失败或取消的可以重试——重试时已经成功的步骤不重跑,从失败的那一步接着做。 ## 送达 - **至少一次,不是恰好一次。** 有两种情况会重复: - 事件发布后先落盘再分发。应用在分发途中崩溃,重启后这个事件会被重新分发,订阅它的 SOP 可能为同一个事件**开始第二次运行**。要防这种重复,用事件里自带的标识自己去重(邮件用 `messageId`)。 - 一次运行中途崩溃,恢复时**正在执行的那一步会重跑**(已经完成的步骤不重跑,直接取用上次的结果)。有外部副作用的工具应该用 `ctx.idempotencyKey` 去重——同一次运行的同一步,重跑时这个值不变。 - **不保证顺序。** 两封几乎同时到的邮件,对应的两次运行谁先谁后没有保证。 - **不会连锁。** gem 之间不能互相触发,gem 的运行也不会引出新的事件。 ## 安装和升级时用户看到什么 - gem 的页面会逐个列出 SOP:它怎么被触发,以及会调用哪些工具、agent 和平台服务。 - 订阅了邮件事件(或有工具申请了 `email` 权限)时,卡片上多一个账号选择框。 - 升级时如果新版本多了定时或事件触发器,卡片会提示这次更新要的权限比已安装的版本多。 - 触发器在安装(或升级)完成时就生效,不需要重启应用。 ## 排查 **装不上,报 `E_WORKFLOW_INVALID`** | 报错里的话 | 原因 | | --- | --- | | `is not an event the platform publishes` | 事件的 `source` / `name` 不在[可用的事件](#可用的事件)里,多半是拼写 | | `a gem cannot be triggered by another gem` | 触发器指向了另一个 gem | | `has a cron trigger that cannot be scheduled` | cron 表达式写法不支持,或永远不会到点 | **报 `E_INPUT_INVALID`**:定时触发器的 `args` 不满足 `input_schema`。 **装上了但没有触发** 1. 在首页的「运行记录」里看有没有这次运行。有而且失败了,点开看是哪一步。 2. 邮件事件:这个账号是不是用户现在允许的账号之一;邮件是不是在收件箱里;是不是刚添加账号、还在回填历史。 3. 定时:按本地时间算;应用当时是否开着。 4. `input_schema` 是否把事件里没有的字段列成了必填。 # 凭据与连接 凭据是你交给 Notochord 保管的密钥:模型服务商的 API key、外部服务的令牌、邮箱的登录信息。它们统一放在「设置 › 账号」里,一个账号就是一个连接。 ## 怎么保存 - 凭据加密存放在这台电脑上,不会上传到任何云端服务器。 - 加密用的钥匙放在系统钥匙串里(首次设置时选了「不加密」的组织则放在组织文件夹里)。见[五分钟上手](/zh/guide/quickstart.md#要不要加密保存这台电脑上的数据)和[组织](/zh/guide/organizations.md)。 - 每个[组织](/zh/guide/organizations.md)有自己的一套凭据,互不相通。 ## AI 和 gem 怎么用它们 **它们都看不到密钥本身。** AI 或 gem 想调用某个外部服务时,拿到的只是「通过这个连接发一次请求」的许可,请求由 Notochord 发出,密钥在发出的那一刻才加到请求里。所以: - AI 的对话记录、工具调用记录里不会出现密钥。 - 就算一个 gem 的代码有问题,它也拿不到密钥,只能请求已经授权给它的连接。 - 连接只能发往它登记过的网站,不能把密钥带到别处。 ## 授权给谁 默认谁都不能用。你在 gem 的页面上选一个账号,这个 gem 才能用它;在项目的设置里勾选,在这个项目里工作的 gem 才能用对应的账号。没有授权的账号,请求会被拒绝。见 [Gem:安装与使用](/zh/guide/gems.md) 和[用项目组织工作](/zh/guide/projects.md)。 ## 删除 在「设置 › 账号」里删掉一个账号,保存的密钥一起删除,用到它的 gem 会失效,直到你给它选别的账号。 # 组织 一个组织就是一套完全独立的数据。第一次打开 Notochord 时会自动创建一个名为 Personal 的个人组织;如果你想把某份工作、某个客户和个人生活分开,可以再建一个组织。 ## 每个组织各自存放什么 下面这些内容每个组织各有一份,互相看不到: - 聊天记录 - 知识库 - 已安装的 gem 及其数据 - 邮件账号 - 保存的凭据(API Key、登录信息) - 设置:界面语言、AI 提供方与模型、agent 配置 - 内置浏览器的登录状态,以及界面本身的浏览器缓存 每个组织的数据放在本机各自的目录里。gem 只能访问它所在组织的数据,不存在跨组织访问。 **唯一的例外是模型文件。** 下载到本机的本地模型(GGUF)和知识库使用的向量模型由本机所有组织共用,免得同一个几 GB 的文件存好几份。模型文件是公开的,不含你的任何数据;「用哪个模型」属于各组织自己的设置。 ## 数据加密 每个组织第一次启动时,首次设置里要选这个组织的数据要不要加密保存(见[五分钟上手](/zh/guide/quickstart.md#要不要加密保存这台电脑上的数据)): - **加密保存**:对话、AI 做过的操作记录、保存的密码和账号、知识库用这个组织自己的钥匙加密,钥匙存在系统钥匙串里。在另一台电脑上或重装之后打开这些数据,需要这个组织的恢复密钥文件。首次设置里会让你先存一份;之后也可以在「设置 › 组织」里点「保存恢复密钥…」再存一份。不要把它和这台电脑放在一起。启动时钥匙串里找不到钥匙,应用会请你选恢复密钥文件;也可以改为新建一个组织,原来的数据原样留着。 - **不加密**:钥匙放在组织的数据文件夹里。把文件夹复制到另一台电脑就能直接打开,不需要恢复密钥;拿到这个文件夹的人也都能看。「设置 › 组织」里同样能保存恢复密钥,里面是同一把钥匙。 这个选择只在首次设置里做一次。 ## 管理组织 在「设置 › 组织」里: - **新建组织**:输入名称即可创建。创建后会询问是否立即切换。新组织第一次启动时,会出现首次设置。 - **切换**:切换到另一个组织。 - **重命名**:修改显示名称。名称只保存在本机,用于区分组织。 - **删除**:删除组织,见下文。 列表中标有 Current 的是当前正在使用的组织。 ## 切换组织会重启应用 同一时间只打开一个组织。切换时 Notochord 会重启,并以新组织启动。 当前组织里正在进行的对话、定时任务和 gem 任务会停止;切回来后,定时任务会补跑错过的时间点。 ## 删除组织 删除会永久清除这个组织在本机的全部数据,并销毁它的加密钥匙。**这一操作无法撤销,数据也无法恢复。** 为了防止误删,需要输入组织名称才能确认。 有两个限制: - 不能删除当前正在使用的组织,需要先切换到别的组织。 - 不能删除最后一个组织,至少要保留一个。 删除本地模型也要留意:在任意一个组织里删除一个已下载的本地模型,其他组织也会失去这个文件,需要时得重新下载。 # 本地开发与调试 Gem Studio 是 Notochord 里开发 gem 的地方:在这里起草、测试,装到自己的机器上用。 ## 草稿 - 在 Gem Studio 里新建一个草稿,可以从模板开始。草稿就是一个 gem 的文件夹:`manifest.json`,加上工具、agent、SOP、页面、skill 和介绍文档。 - 文件可以在 Gem Studio 里编辑,也可以用你自己的 IDE 打开草稿所在的文件夹编辑。 - Gem Studio 里有一个助手,可以帮你写代码、检查 `SKILL.md` 里的路径、解释报错。没写过代码也可以靠它完成。 - 草稿有历史,可以回到以前的版本。 ## 测试 - **工具**:在工具的页面上填入参数直接运行,看返回值和每一步的记录。 - **Agent**:在测试对话里和它聊,走的是和装好以后相同的运行路径。 - **SOP**:手动触发,查看每一步做了什么、结果是什么。 - **页面**:在 Gem Studio 里实时预览。 - 测试时用的权限、数据和装好以后相同:清单里没申请的,测试时也用不了。数据怎么保留、迁移怎么测,见[数据与存储](/zh/dev/storage.md#在-gem-studio-里测试)。 ## 安装到自己的机器上 点「安装」,Notochord 把草稿打成安装包,走和商店里的 gem 相同的安装流程,同样的校验。这个包没有签名,按 Developer mode (unsigned) 处理。详见[打包与发布](/zh/dev/publishing.md)。 ## 排错 - 安装失败时,报错里有错误码,对照[打包与发布](/zh/dev/publishing.md#安装时检查什么)里的表。 - 运行出错时,gem 的页面上有运行记录,点一条能看每一步的输入和结果;Gem Studio 里可以把错误交给助手分析。 # 设置项说明 ## AI 设置 - **模型服务商**:接入的模型服务商和它们的模型。 - **角色**:给「对话」「识图」「深度思考」「本地」和你自己加的角色选模型。每个角色下面那行说明,是 AI 把工作交给子 agent 时选角色的依据,建议四个内置角色都设上模型。见[角色](/zh/guide/models.md#角色)。 - **Agent**:给每个 agent 单独选一个角色或模型,依次是「项目」「对话」「Gem Studio」「后台任务」,再往下是你装的 gem 的 agent;没选的都用「对话」角色。见[给各个 agent 选模型](/zh/guide/models.md#给各个-agent-选模型)。 ## 系统 › 网络访问 AI 和 gem 发出的每一个网络请求、内置浏览器打开的每一个页面,都按这里的规则检查。地址分成两类,各管各的: ### 外网 网站和在线服务。 - **黑名单模式**(默认):名单上的主机不能访问,其余都可以。名单是空的就是全部放开。 - **白名单模式**:只有名单上的主机能访问。 名单里写主机名,例如 `api.example.com`;`*.example.com` 表示它和它下面所有的子域名。 ### 本机和内网 你的电脑,以及家里或公司内部网络里的设备和服务:路由器、NAS、打印机、摄像头,公司的 OA、wiki 这类内部系统。 **默认一个都不能访问。** 只有你在这里列出的地址,AI 和 gem 才能用;AI 和 gem 自己改不了这份名单。 为什么单独管:这些设备和服务常常不设密码——平时只有家里或公司里的人连得到——里面又常有内部资料。AI 打开的网页里可能藏着指令,诱导它去访问这些地址;第三方 gem 也可能想看看你的内网里有什么。能不能碰到它们,由你决定,不交给 AI 和 gem。 怎么写: - **本机上的服务要写端口**,例如 `localhost:5173`。写了 `localhost:5173`,`127.0.0.1:5173` 也一起放开。只写 `localhost` 不行:那会把你电脑上所有程序开的服务都放开。 - **内网里的设备**写 IP 或名字,可以带端口:`192.168.1.20`、`nas.local:5000`。`*.corp.example.com` 表示这个域名下所有的主机。 - 不要带 `http://`,也不要带路径。 - 按你写的名字匹配:列了 `192.168.1.20`,用 `nas.local` 访问同一台设备并不放行,要用哪个名字就列哪个。 常见的用法:开发网站时,让 AI 用浏览器测试你电脑上正在跑的网站,就把 `localhost:端口` 列进来。 另外: - 在「知识库 › 导入网页」里**你自己输入**的地址可以是内网地址(比如公司的 wiki),不用先列进来;那个页面跳转到别的内网地址时,仍按名单检查。 - gem 要访问本机和内网,除了你列出地址,它自己还得在清单里申请过;gem 页面的「它能做什么」会写明。 - 你自己在设置里填的服务地址不受这里限制,比如本地模型(Ollama 的 `localhost:11434`)、公司内网的邮箱服务器:那是你亲自指定的,Notochord 只连你填的那个地址。 # 常见问题 ## 我的数据会上传到云端吗? 不会。对话、文档、表格、凭据都保存在你的电脑上,加密存放,Notochord 没有把它们上传到云端的功能。会离开你电脑的只有你主动发给 AI 模型服务商的内容;想让某些内容留在本机,就给它用本地模型(见[配置模型](/zh/guide/models.md))。 ## 一定要用云端模型吗? 不用。OpenAI、Anthropic、Gemini、OpenRouter 都能接,也可以用你电脑上的 Ollama,或者下载到本机的 GGUF 模型离线运行。可以随时切换,不绑定任何一家。 ## AI 会看到我的 API key 吗? 看不到。AI 只拿到「通过某个连接发请求」的许可,密钥由 Notochord 在发出请求时添加。见[凭据与连接](/zh/guide/credentials.md)。 ## Gem 是什么?装了安全吗? Gem 是装进 Notochord 的小应用,带着自己的工具和自动化流程。装之前,Notochord 会检查包的签名、完整性和权限声明;装好以后它什么都碰不到——邮箱、知识库、账号都要你在它的页面上打开才能用;它只能连它申请过的网址,也连不上你的内网。见 [Gem:安装与使用](/zh/guide/gems.md)。 ## 我的电脑和内网会被 AI 访问吗? 默认不会。本机和内网的地址,只有你在「设置 › 系统 › 网络访问」里列出的,AI 和 gem 才能用。见[设置项说明](/zh/guide/settings.md)。 ## 换电脑或重装系统,数据还在吗? 选了「加密保存」的话,需要首次设置时保存的恢复密钥文件才能在新环境里打开数据,请把它存在这台电脑以外的地方。选了「不加密」的,直接复制组织的文件夹就行。见[组织](/zh/guide/organizations.md)。 ## 要收费吗? Notochord 将以 MIT 协议开源,个人和商业使用都不受限制。你只需要为自己接入的模型服务商付费。 ## 怎么开发自己的 gem? 见[开发者文档](/zh/dev/index.md)。没写过代码也可以:内置的 Gem Studio 助手可以陪你一起做。 # 打包与发布 ## 现在能怎么分发 - **装到自己的机器上**:在 Gem Studio 里点「安装」。Notochord 在本机把草稿打成一个安装包,走和商店里的 gem 相同的安装流程——同样的校验,装完同样到 gem 的页面上设置。只是这个包没有签名,按「Developer mode (unsigned)」处理。 - **分发给别人**:目前还没有把草稿导出成安装包、或者提交到商店的入口。应用里的商店现在列出的是我们自己维护的开源 gem。 ## 安装包里有什么 一个 zip,根目录下是: | 文件 | 说明 | | --- | --- | | `notochord.json` | 清单。除了[清单参考](/zh/dev/manifest.md)里的字段,还带打包时生成的两项:`agents[]`(从 `agents/` 下每个文件的头部生成)和 `integrity`(包里其余每个文件的 SHA-256 和大小)。 | | `SIGNATURE.json` | 对清单的签名。从 Gem Studio 安装的草稿没有这个文件。 | | 其余文件 | 工具(`tools/`)、agent(`agents/<名字>.md`:头部 + 系统提示词)、SOP(`workflows/`)、页面(`pages/`)、集合的迁移模块(`data.collections.<集合>.migrations` 指向的文件)等。`db/` 放 `rawSqlite` 数据库的表结构和迁移(`db/schema.sql`、`db/migrations/`);有它的 gem 启动时就打开这个数据库,表结构或迁移出错,gem 就启动不了。下面两个文件夹不用在清单里声明,放在那里就算:`skill/`(`SKILL.md` 和 `references/` 下的文本文件,见 [skill](/zh/dev/manifest.md#skill)),`docs/`(商店介绍页,只有 Markdown,见 [docs](/zh/dev/manifest.md#docs);Gem Studio 打包时去掉其中不是 `.md` 的文件)。 | ## 安装时检查什么 安装和升级都按下面的顺序检查,任何一项不过,这个包就不会被装上: 1. **解包**。包里不能有绝对路径、`..`、符号链接、只差大小写的重名文件;文件数不能超过 10000;解压出来的内容不能超过清单声明的总大小。 2. **签名**(见下一节)。在读取清单里的任何内容之前进行。 3. **清单格式**。清单里出现不认识的字段会被拒绝,而不是被忽略。gem 的 id 不能以 `draft-test.` 开头,这个前缀留给 Gem Studio 的测试。 4. **版本**。不能比已安装的版本低。版本号相同就是重装:用包里的文件替换已安装的文件,gem 的数据和设置保留。 5. **完整性**。`integrity` 列出的文件和包里实际的文件必须一一对应:多一个、少一个、大小或 SHA-256 对不上,都不行。没有签名的包也必须有 `integrity`(Gem Studio 打包时自动算出)。 6. **声明是否合法**。权限的写法、agent 引用的工具是否存在、`resources` 的 `id` 是否重复、每个 SOP 的调用目标和触发器、`skill/` 和 `docs/` 里的文件类型,以及 [`data`](/zh/dev/manifest.md#data) 段:不合规报 `E_PERM_INVALID`,消息以 `data:` 开头(`data` 段里不认识的键、写错的 `type` 在第 3 步就被拒绝);集合的迁移模块必须是包里的文件。 从商店或 Gem Studio 安装时,这些检查都通过才会装上;没有给用户的确认步骤。装好后打开这个 gem 的页面,需要配置的(账号、邮箱、知识库)都还是关着的,等用户设置。 **升级会迁移数据。** 检查都通过后,新版本被激活。新版本提高了某个集合的 `version` 时,激活时先运行[迁移](/zh/dev/storage.md#版本与迁移)。迁移失败,或者 `data` 的改动不合[改字段的规则](/zh/dev/manifest.md#改字段的规则),整个升级回滚:数据恢复原样,旧版本继续运行,报 `E_ACTIVATION_FAILED`。 常见的报错: | 报错 | 原因 | | --- | --- | | `E_PATH_TRAVERSAL` | 包里有不允许的路径或文件类型,或者文件太多 | | `E_SIG_INVALID` | `SIGNATURE.json` 格式不对,或者签名和清单对不上(清单被改过) | | `E_UNKNOWN_KEY` | 签名用的密钥不是这台机器信任的 | | `E_PUBLISHER_MISMATCH` | `SIGNATURE.json` 的 `publisherId` 和清单的 `author.publisherId` 不一致 | | `E_PERM_INVALID` | 清单格式不对(报错里会指出哪个字段),或者某项声明不合法 | | `E_VERSION_OLDER` | 包的版本号比已安装的低 | | `E_HASH_MISMATCH`、`E_SIZE_MISMATCH` | 包里的文件和 `integrity` 对不上 | | `E_WORKFLOW_INVALID`、`E_INPUT_INVALID` | SOP 或它的触发器有问题,见[事件与触发](/zh/dev/events.md#排查) | | `E_ACTIVATION_FAILED` | 检查都通过了,但升级或同版本重装后这个版本激活失败,多半是 `data`:迁移失败,或者改动不合[改字段的规则](/zh/dev/manifest.md#改字段的规则)。原因(如 `E_DATA_SCHEMA: …`)写在报错里。升级时已经回滚到旧版本。第一次安装时报的是原来的错误。 | ## 签名与信任级别 签名是对 `notochord.json` 做的:Ed25519,签名前先按 RFC 8785 把 JSON 规范化。清单里改动任何一处内容——权限、工具说明、`integrity` 里某个文件的哈希——签名都会失效;包里其余的文件则由 `integrity` 逐个钉住。两者合起来,一个签过名的包,清单和它引用的每个文件都不能在不被发现的情况下改动。 安装时,一个包按下面三级之一处理(应用里目前不显示是哪一级): | 级别 | 含义 | | --- | --- | | Official signature | 签名能用 Notochord 的官方公钥验证。官方公钥内置在应用里,不从磁盘或网络读取。 | | Signed by a trusted developer | 签名来自这台机器信任的某个开发者密钥,并且 `SIGNATURE.json` 的 `publisherId` 和清单的 `author.publisherId` 一致。**应用里目前还没有添加开发者密钥的入口,这一级暂时用不上。** | | Developer mode (unsigned) | 没有验证签名。从 Gem Studio 安装草稿就是这一级。签名以外的检查照常进行。 | - 从商店下载的包必须通过签名验证,通不过就装不上。 - **装好以后也会检查文件。** 激活时、每次调用工具、SOP 启动和恢复、读 skill 文件之前,Notochord 都把这个 gem 目录里的文件和安装时的 `integrity` 比对:多一个、少一个、改了一个(系统生成的 `.DS_Store`、`Thumbs.db`、`desktop.ini` 不算,打包时也不列入清单),这个 gem 就停止运行(报 `E_FILES_CHANGED`),用户在 gem 的页面上点「重新下载」修复,数据和设置都保留。安装后磁盘上的 `notochord.json` 不再被读取,清单以安装时的那份为准。开发时不要直接改已安装 gem 的文件,在 Gem Studio 里改完重新安装。 # Developer docs A gem is a folder: a manifest, plus optional tools, agents (each a prompt file with a header), SOPs, pages, a skill for the AI, an intro document for people (`docs/`, which will become the store's listing page), data declarations, and event subscriptions. Once it is installed, the gem's page lists what it can do; anything the user has to configure (accounts, mailboxes, the knowledge base) stays off until the user turns it on. ## Where to start 1. [What a Gem is](/en/dev/gem-overview.md) — structure, lifecycle, and its relationship to sessions 2. [Manifest reference](/en/dev/manifest.md) — every field in the manifest 3. [Permission model](/en/dev/permissions.md) — how network, model, knowledge-base, email and account access is requested and checked; storage, files and document extraction need no permission 4. [Data & storage](/en/dev/storage.md) — storing a gem's own data with `ctx.data`: collections, logs, documents, blobs, plus migrations, encryption, and isolation 5. [Knowledge API](/en/dev/knowledge.md) — reading and writing the user's facts and documents through `ctx.knowledge` 6. [Tools API](/en/dev/tools.md) — built-in tools and custom tools 7. [Events & triggers](/en/dev/events.md) — the three ways a flow starts (manual call, schedule, platform event), their inputs and limits 8. [Local development & debugging](/en/dev/local-dev.md) — Gem Studio and the draft flow 9. [Packaging & publishing](/en/dev/publishing.md) — what a package contains, the checks at install, signing and trust levels ## Machine-readable interfaces - [`config.notochord.ai/v1/index.json`](https://config.notochord.ai/v1/index.json) — the full index of static config the client pulls from the CDN - [`config.notochord.ai/v1/config.json`](https://config.notochord.ai/v1/config.json) — LLM vendors, service connection templates, email presets, local models, site links - [`config.notochord.ai/v1/gem-templates.json`](https://config.notochord.ai/v1/gem-templates.json) — Gem Studio templates - [`download.notochord.ai/gems/index.json`](https://download.notochord.ai/gems/index.json) — the gem store listing (what the app's Store tab reads) - [`/llms.txt`](/llms.txt) — the site-wide AI index # Download Notochord **Notochord is still under development, so there is no installer to download yet.** When the app is ready it will be released as **open source under the MIT license**, with installers for macOS, Windows, and Linux. Download links will appear on this page. ## What you can read now - [Home](/en/index.md): what Notochord is and why it is designed this way - [User guide](/en/guide/index.md): how the app works — projects, conversations, models, gems, and organizations - [Developer docs](/en/dev/index.md): how to build gems for Notochord — manifest, permissions, data and storage, events and triggers The docs describe the design of the version under development; details may change as development continues. # User guide Read the first two pages in order and you can start working; look up the rest as needed. 1. [Quickstart](/en/guide/quickstart.md) — first-run setup, create your first project, run your first conversation 2. [Organizing work with projects](/en/guide/projects.md) — tables, canvases, files and conversations in one place; publish as a gem 3. [Conversations and sessions](/en/guide/chat.md) — sessions, attachments, tool-call records 4. [Configuring models](/en/guide/models.md) — cloud providers and local GGUF, roles 5. [Gems: installing and using them](/en/guide/gems.md) — install from the store, approve permissions, uninstall 6. [Credentials and connections](/en/guide/credentials.md) — how API keys are stored and how gems use them 7. [Organizations](/en/guide/organizations.md) — keep work and personal data completely separate 8. [Settings reference](/en/guide/settings.md) 9. [FAQ](/en/guide/faq.md) > Video tutorials are linked from the relevant sections as YouTube links, so they take up no space on the site. # Notochord, your private AI team > Notochord is under development and will be released as open source under the MIT license once the app is ready. For now, this site shows its design and documentation. [Download](/en/download.md) · [Quickstart](/en/guide/quickstart.md) · [Developer docs](/en/dev/index.md) ## Install it like any other app — but don't mistake simple for unguarded No Docker, no VMs, no virtualization to wrestle with — download it, open it, done. "Simple" doesn't mean "unprotected": - **Scripts run in a sandbox**: code the AI writes runs inside a Deno sandbox and can't touch anything outside it. - **Network calls are fenced in**: a gem can only reach the hosts it asked for; your computer and your home or office network are off-limits to the AI and to gems until you list an address; the rest of the internet can be allow- or block-listed. Even if a dependency gets poisoned upstream, it can't reach out through this wall. - **The AI never sees your keys**: Notochord's credential system only ever hands the AI a "make this request through this connection" permission — the key itself stays out of its reach and never leaves the machine. - **Your data stays on your machine**: every conversation, document, and table is encrypted and stored locally — nothing is uploaded to any cloud. ## Pick any AI provider OpenAI, Anthropic, Gemini — connect any cloud model, or run something local or on your own private network instead. Switch anytime. You're never locked into one vendor. ## Turn repeat work into a process (SOP) Write down a workflow you do often as an SOP, and run it with one click from then on — no re-explaining it to the AI every time. Bigger jobs can be split across multiple AI agents working together, running in the background, without you having to watch. ## Build it yourself, or build it with the AI's help Notochord exposes a rich set of building blocks — tools, skills, SOPs, and agents are all things you can build yourself. Never written code? The built-in AI assistant can build it with you. ## The longer you use it, the better it knows you A built-in personal knowledge base accumulates how you work and what you prefer, so the AI keeps getting more useful the longer you use it. ## Gem Store Anyone can build and publish their own tools, skills, and agents for others to use. Paid listings and revenue sharing are coming, so independent creators can turn their know-how into income. ## Open source, free for anyone Notochord will be released as open source under the MIT license — the code will be open to public scrutiny, with no limits on personal or commercial use. ## Why "Notochord" A notochord is the rod of cells that let early invertebrates — creatures getting by on little more than a claw and a few reflexes — evolve into something new: a spine, the structure a more complex nervous system and finer motor control could organize around, the first step toward becoming a vertebrate. That's the idea behind the name: building the structure that lets AI agents grow from simple reflexes into coordinated, autonomous work. # What a Gem Is A gem is a folder: a manifest (`notochord.json`), plus optional tools (`tools/`), agents (`agents/`, one prompt file with a header per agent), SOPs (`workflows/`), pages (`pages/`), a skill for the AI (`skill/`), an introduction document for people (`docs/`), data declarations, and event subscriptions. - **Installation**: install from the store or from Gem Studio. The gem first goes through signature, integrity, and declaration checks (see [Packaging and Publishing](/en/dev/publishing.md)) and is installed only if it passes. Once installed, the gem's page lists what it can do. - **It can reach nothing by default**: anything that needs user configuration (accounts, mailboxes, knowledge) stays off until the user turns it on (see [Permission Model](/en/dev/permissions.md)). - **Isolation**: each gem has its own data folder and its own process. Other gems cannot read them. - **Use**: the user asks the AI to use it in chat, or its own SOPs run automatically according to their triggers (see [Events and Triggers](/en/dev/events.md)). ## How the AI uses your gem In chat the user talks to the Chat agent. By default it has two tools: `read_skill` and `tool_search`. The user can remove them in "Settings › AI Settings › Agent", or add them to other agents. - **The AI knows which gems the user has installed and what each one offers.** For an agent that has `read_skill`, the system prompt lists every installed gem, one line each: name, id, and the `description` from the manifest (see [Manifest Reference](/en/dev/manifest.md#descriptions) for how to write it). Below it, each exposed item (a tool, SOP, or agent marked `expose: true`) gets one line with its description; SOPs are marked (SOP) and agents are marked (agent). If a gem exposes no tools, the AI does not know they exist. - **To use a gem, read its id with `read_skill`.** The result contains its skill (the full text of `skill/SKILL.md` and the name of each file under `skill/references/`) and the items the gem exposes. From the next step on, those items are in the AI's tool list until the conversation ends. A gem without a skill is opened the same way; the result just has no skill in it. - **An exposed agent is a tool.** The AI calls `gem..({ task })` and hands it the task in a paragraph. The agent finishes the task in its own session, with the tools it declares and the model the user chose for it, and its last reply is the tool's result. It sees only this task. For how to decide between exposing a tool or an agent, see [Manifest Reference](/en/dev/manifest.md#exposing-items-expose). - **Reference material is read on demand.** The AI uses `read_skill` with a name listed in the result (`/skill/references/…`) to read a file. The gem's tools appear at the same time. For how to write it, see [Manifest Reference](/en/dev/manifest.md#skill). - **It can also search by keyword.** `tool_search` searches installed gems (names, descriptions, and the names and descriptions of exposed items) and Notochord's built-in tools. What it finds (up to 5 items) also appears in the tool list. - **Reading and searching never ask the user.** They only let the AI see what a gem offers; having seen it, the AI may still not use it. - **The first call asks once.** The first time the AI calls an item the gem exposes, the app shows a dialog asking the user to agree. The question is about the whole gem; once the user agrees, calling any exposed item in this conversation no longer asks. The consent applies only to this conversation; a new conversation asks again. If the user refuses, the call is not executed and the AI receives a refused result. - **Exposed items appear only as a whole gem**; you cannot get just one of them. To the AI, a SOP is a tool, except that it may need to wait (for approval or for a schedule), in which case it first returns a run id. - **A tool that is not exposed can be called only by the gem itself**: its SOPs, agents, and pages. If another session calls it, the call is refused whether or not it is in the tool list, and the user is not asked. - **Which gems appear in the gem list**: gems that expose at least one item, and gems that **expose nothing but have a skill** (a `skill/SKILL.md`). Reading the latter gives only text and nothing that needs the user's consent. A gem that exposes nothing and has no skill does not appear (it works only through its own pages and scheduled SOPs). A gem's own agents are not affected by any of this: which tools they can use is determined by the header of their file (see [Manifest Reference](/en/dev/manifest.md#agents)), and they do not see this gem list. # Manifest reference ## Descriptions The gem itself, each of its tools, each SOP, and each agent must have a description (`description`). Descriptions are written for the AI and for users, and they have length limits: | Where | Limit | What to write | | --- | --- | --- | | Top-level `description` in the manifest | 200 characters | One or two sentences: what the gem can do and what its main tools are called (for a gem with only a skill, what the skill covers). | | `tools[].description` | 512 characters | What the tool does and when to use it. | | `workflows[].description` (SOP) | 200 characters | What the SOP does and when it runs. | | `description` in an agent file's header | No limit | One or two sentences: what this agent is for; shown on the gem's page. See [agents](#agents). | | `description` of an exposed tool, SOP, or agent | 200 characters | Items marked `expose: true` go into the main chat's system prompt, so they share the 200-character limit of the gem description. See [Exposing items](#exposing-items-expose). | Limits are counted in characters; one Chinese character counts as one. A missing description, an empty one, or one over the limit fails manifest validation and the package can't be built (`E_PERM_INVALID`). An agent's description has no length limit; Gem Studio refuses to install an agent that has none. Keep the gem's description short, because it is the only thing the AI has to go on to get to know this gem: every gem the user has installed is listed in front of the AI as one line (name, id, description), even when there are hundreds. The AI decides from that one line alone whether to open the gem, and only after opening it can it see the tools, SOPs, and skill (see [What a gem is](/en/dev/gem-overview.md#how-the-ai-uses-your-gem)). So the gem's description covers only what it does and its main tools; usage and details go in the descriptions of the tools and SOPs themselves. The gem store's listing and search will use this same text later. ```json "description": "Summarizes and fully translates every new email and keeps both in an encrypted store. Main tools: summarize_email, translate_email, save_record, list_records." ``` In a Gem Studio draft, the gem's description is the top-level `description` in `manifest.json`, and each tool's or SOP's description is written in the `description` of the corresponding `exports[]` entry. When there is only one tool and the top-level shorthand is used, the top-level `description` is both the gem's and the tool's description, so it also can't exceed 200 characters. An agent's description goes in the header of its own file. ## tools The tools the gem provides for the AI and for SOPs to call. ```json "tools": [ { "name": "summarize_email", "entry": "tools/summarize_email.js", "description": "Distill one email into its key points and any action items. Returns { summary }.", "inputSchema": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] }, "outputSchema": { "type": "object", "properties": { "summary": { "type": "string" } } } } ] ``` | Field | Required | Description | | --- | --- | --- | | `name` | Yes | Starts with a lowercase letter and contains only lowercase letters, digits, and underscores, up to 64 characters. SOPs call it with `ctx.call('tool.', …)`. | | `entry` | Yes | The file that holds the tool's code. For how to write it, see the [Tool API](/en/dev/tools.md). | | `description` | Yes | Up to 512 characters; see "Descriptions" above. | | `inputSchema` | Yes | JSON Schema for the input parameters. File and folder paths are ordinary strings; a tool reads and writes in the conversation folder with the `workspace:` prefix, see [Permission model](/en/dev/permissions.md#conversation-folder). | | `outputSchema` | No | JSON Schema for the return value, for gems and SOPs that call this tool later. It is not checked at present. | | `permissions` | No | The permissions this tool itself uses, see [Permission model](/en/dev/permissions.md). The gem's own storage doesn't need to be listed (see [Storage is not a permission](/en/dev/permissions.md#storage-is-not-a-permission)). | | `sideEffect` | No | `read` (changes nothing; the only kind of tool that can run in plan mode), `write` (treated as this when omitted), or `destructive` (hard to undo: deleting, sending, paying). This is your declaration and is not checked at run time: write `read` only when the tool truly changes nothing. | | `expose` | No | `true`: exposed, so the main chat's AI can see and call it. When omitted the tool is for the gem's own use: only this gem's SOPs, agents, and pages can call it, and calls from any other session are refused. See [Exposing items](#exposing-items-expose). | Tools have no time limit of their own: the user can stop one at any time, and calls inside a SOP are bound by the SOP's `limits.maxDurationMs`. ## Exposing items (expose) An installed gem works in two directions: it gets tasks done through its own pages, SOPs, and agents, and it acts as a teammate with a specialty, letting the main chat's AI (and any agent outside the gem) put its skills to use. The latter works by the gem **exposing** a small, explicit set of interfaces: the tools, SOPs, and agents marked `expose: true`. Everything else is the gem's internal implementation; agents outside the gem can't see it or call it (Notochord refuses the call whatever permissions the caller has). What the main chat's AI sees is: one line per installed gem (its `description`), and under it one line per exposed item (its `description`). After the AI opens the gem with `read_skill`, these are exactly the items that show up in its tool list; on the first call the user is asked once, and the approval covers the whole gem. So the exposed items and their descriptions are everything this gem is in the team's eyes. Write them in words a teammate would understand (what it does for someone, when to use it), not implementation steps. What to expose: | Form | When | How to write it | | --- | --- | --- | | Tool | One call does the whole job and the result is a single value: look something up, convert, summarize one email, record an entry. It costs no extra model call, and the input format keeps calls accurate. | Add `"expose": true` to that entry in `tools[]` | | agent | Several of the gem's own tools must be chained and the gem's judgment is needed (filing taxes: collect, classify, calculate, fill in forms). The main chat hands it the task in a paragraph (`gem..({ task })`), and it completes it in its own session, with its own `tools` and `platform_tools`, using the model the user chose for it; its reply is the result. It sees only this task, not the rest of the conversation, so the task has to carry everything it needs. The gem's many internal tools stay inside. | Add `expose: true` to the agent file's header | | SOP | A fixed procedure a teammate can start (it may wait for approval or for a schedule, in which case the caller first gets a run number). | Add `"expose": true` to that entry in `workflows[]` | | Nothing | A gem that only gives the team knowledge to read (it has a skill: the main chat can read `skill/SKILL.md` anyway), or one that works only through its own pages and scheduled SOPs. | Write none of them | Both at install time and in Gem Studio these are checked: each gem can expose at most 5 items; an exposed item's description is at most 200 characters (it goes into the system prompt). A gem that has generated many tools for its own pages and SOPs should expose one or two of them, or one agent, rather than the whole list. Exposed names can't repeat across the gem's tools, SOPs, and agents (they share the single `gem..` namespace, so a tool, SOP, and agent can never share a name anyway). ## skill A skill is instructions written for the AI: how to get things done with this gem, meaning which tool to use in which situation, in what order, and what to watch out for. It is only text; it is not executed and needs no permissions. **A gem has at most one skill**, in the same format as [Agent Skills](https://agentskills.io/specification): a `SKILL.md`, plus the reference material it mentions. ``` skill/ SKILL.md references/ regions.md rules/ 2024.csv ``` - **`skill/SKILL.md`**: the skill itself. The AI reads it in full as soon as it opens this gem, so keep it short (at most 64 KB, the shorter the better): what the gem can do, the main approach, and **when to read which reference**, written as the full path from the gem root, for example `skill/references/regions.md`. No header (front matter) is needed; the AI picks gems by the top-level `description` in the manifest. - **`skill/references/`**: the details that `SKILL.md` mentions, with any subfolders you like: steps, examples, tables, data. **Text files only**: `.md`, `.txt`, `.json`, `.jsonl`, `.csv`, `.tsv`, `.yaml`, `.yml`, `.xml`, each at most 64 KB. No runnable files (code, scripts); if you need a code example, put it in a Markdown file. - `skill/` can contain nothing else, and `references/` can't exist without a `SKILL.md`. A package that doesn't comply can't be built (`E_PERM_INVALID`). **Nothing to declare in the manifest.** If the package has `skill/SKILL.md`, the gem has a skill. **Notochord doesn't check whether `SKILL.md` has the reference paths right.** A reference that isn't mentioned won't be read by the AI; a wrong path can't be read. Check it yourself after writing: every path mentioned in `SKILL.md` is under `skill/references/`, and every reference says in `SKILL.md` when to read it. In Gem Studio you can ask the assistant to help check. **A gem can have only a skill**: no tools, SOPs, or agents, just `skill/`, for example a company's writing guidelines or a description of a business process. It still appears in the AI's gem list, and the AI can read these files with `read_skill`. **How the AI reads it**: the AI reads your gem id with `read_skill` and gets all of these at once: - The **full text** of `SKILL.md`; - The **name of every file** under `skill/references/` (`/skill/references/…`); - Your gem's tools (including SOPs), which appear in the AI's tool list from the next step on. After that, whenever the AI needs a reference, it reads the full text with `read_skill` and that name. Reading a skill doesn't ask the user. **The gem's own agents read it the same way**: as long as the gem has a skill, each of its agents gets `read_skill`, and the names of all the skill's files are appended to the end of the system prompt. What an agent reads with `read_skill` is only text; the tools it can use are still only those listed in its own file header. ### How to write a skill - **Put instructions and an index in `SKILL.md`** and the details in `references/`: `SKILL.md` is read into the conversation in full every time the gem is opened. - **Rules to follow every time** go in the agent's own prompt (the body of `agents/.md`), which the gem's agent sees on every turn. The Chat agent reads the skill only when it opens your gem. - **Don't have too many or too fragmented references**: each file appears as one line in the list, so hundreds of small files make the list itself eat a lot of context, and small models find it harder to pick correctly. ## permissions.knowledge Request access to the user's knowledge base. For the full description see the [Knowledge API](/en/dev/knowledge.md). ```json "permissions": { "knowledge": { "permissions": ["facts.read:self:location.home.*", "collections.write:diary"], "predicates": [{ "predicate": "pref.food.cuisine", "cardinality": "many", "description": "Cuisines the user likes", "aliases": ["cuisine"] }], "reason": "A one-sentence reason shown to the user (optional)" } } ``` | Field | Required | Description | | --- | --- | --- | | `permissions` | Yes | A list of permission strings; for the format and accepted values see [Permission model](/en/dev/permissions.md). Each one is validated at install time, and a manifest with a malformed entry can't be installed. | | `predicates` | No | The predicates this gem will write. `predicate` must be "a fixed first level plus at least one more level", for example `pref.food.cuisine`; `cardinality` is `one` or `many`. They are written into the global vocabulary at install time, and ones that already exist are not overwritten. | | `reason` | No | Shown on the install screen. | A tool that needs `ctx.knowledge` writes `"knowledge": true` in its own `permissions`. What it can access is decided by the gem-level declaration above and the user's approval; the tool level is only a switch. In a Gem Studio draft, write `"knowledge": { ... }` at the top level of `manifest.json`, and every tool can use it by default. When a tool needs only some of the items, write `"permissions": ["network", "knowledge"]` in its own entry (or check them on the tool's page in Gem Studio); `side_effect` is also written there. ## data What shape the gem's data should be stored in: which collections there are, and each collection's fields, indexes, and version. The platform stores data according to this declaration and validates every write; tools read and write with `ctx.data`, see [Data and storage](/en/dev/storage.md). ```json "data": { "collections": { "notes": { "kind": "table", "description": "To-dos the user has noted down", "fields": { "title": { "type": "string", "required": true }, "status": { "type": "string", "enum": ["open", "done"] }, "due": { "type": "timestamp" }, "project": { "type": "ref", "to": "projects" }, "tags": { "type": "array", "items": "string" } }, "indexes": [["status", "due"], ["project"]] }, "projects": { "kind": "table", "fields": { "name": { "type": "string", "required": true, "unique": true } } }, "readings": { "kind": "log", "fields": { "value": { "type": "number" } }, "retention": { "days": 365 } } } } ``` | Field | Required | Description | | --- | --- | --- | | `collections` | No | The collections; the key is the collection name, at most 64. | | `eventRetentionDays` | No | How many days to keep the change record (the record each write leaves behind: who changed it and what changed), 1–3650, default 180. Log entries aren't cleaned up by this; they follow their own `retention`. | Collection and field names: start with a lowercase letter, contain only lowercase letters, digits, and underscores, up to 64 characters. ### Collections Each collection is either a table (`table`: records that can be modified and queried) or a Log (`log`: entries that are append-only and never modified). | Field | Table | Log | Description | | --- | --- | --- | --- | | `kind` | Required | Required | `table` or `log`. | | `fields` | Required | Required | 1–128 fields, see "Fields" below. | | `description` | Optional | Optional | Up to 500 characters. | | `version` | Optional | Not allowed | An integer, default 1, which can only go up, never down; see "Rules for changing fields" below. | | `indexes` | Optional | Not allowed | Up to 16, each an array of 1–8 field names with no repeats; only scalar fields can be used (see the table below). For what indexes a query needs, see [Index rules](/en/dev/storage.md#index-rules). | | `migrations` | Required when `version` is greater than 1 | Not allowed | The path of the migration module in the package: `.js`, `.mjs`, or `.ts`, with no `..`, and the file must be in the package. It exports one function per step (`v1_to_v2(data, { id })`, `v2_to_v3`, …); see [Versions and migrations](/en/dev/storage.md#versions-and-migrations). | | `retention` | Not allowed | Optional | How much of the old entries to keep: `{ "days": 1–36500, "maxEntries": 1–10000000 }`, with at least one of them, cleaned up by the platform. If omitted, entries are kept forever. | ### Fields | Field | Description | | --- | --- | | `type` | Required. Scalars: `string`, `number`, `integer`, `boolean`, `timestamp` (integer milliseconds), `ref` (the id of another record), `blob` (a Blob's hash); these can be queried, sorted, and indexed. Three others: `array` (can only be queried with `array-contains`), `object`, and `any` (these two can't be queried). | | `required` | When `true`, every record must have this field. | | `unique` | When `true`, this field's value can't repeat within the collection. Only allowed on a table's scalar fields. | | `enum` | The allowed values, 1–256, with no repeats; only allowed on `string`, `number`, and `integer`, and the value type must match the field. | | `to` | Required for a `ref` field (or an array whose `items` is `ref`): which collection it points to, which must be a table in the same manifest. | | `items` | Only allowed on `array`: the element type, one of the seven scalar types. If omitted, elements aren't checked, and `array-contains` can't be used. | | `description` | Up to 500 characters. | ### Rules for changing fields Each time a gem is activated (install, upgrade, Gem Studio test), Notochord compares the declaration in the manifest with the data already stored: - **Within the same `version` you can only loosen**: add an optional field, remove `required` or `unique`, widen or remove an `enum`, add or drop indexes, change descriptions. Adding `unique` is also allowed, but activation fails if existing values contain duplicates. - **These require raising `version` and writing a migration**: removing a field; changing `type`, `to`, or `items`; adding `required` to an existing field; narrowing an `enum`, or adding one to a field that had none; adding a new required field. - `version` can't be lower than the data already stored. - A collection's `kind` can't be changed. - A Log has no version; its fields can be changed at any time, and old entries stay as they were when written. - A collection removed from the manifest keeps its data (the interface can't reach it), and the data is deleted on uninstall. Breaking these rules makes activation fail with `E_DATA_SCHEMA: …` (a newly added `unique` that meets duplicate values gives `E_DATA_UNIQUE: …`); on an upgrade or a reinstall of the same version it is wrapped in `E_ACTIVATION_FAILED`, and the upgrade rolls back to the old version (see [Packaging and publishing](/en/dev/publishing.md#what-is-checked-at-install)). If the `data` section itself is written wrongly, you get `E_PERM_INVALID` in Gem Studio tests, packaging, and installation. When packaging and installing, errors of shape (unknown keys, a wrong `type`, an overlong description) are caught first by the manifest's JSON Schema, with the message `notochord.json failed schema validation: …`; the other rules (names, count limits, `to`, `version` and `migrations`, a migration file missing from the package) have messages that begin with `data:`. In Gem Studio tests they all begin with `data:`. In a Gem Studio draft, `data` is likewise written at the top level of `manifest.json` with the same fields; edit `manifest.json` directly. For how test data is kept and how to test migrations, see [Testing in Gem Studio](/en/dev/storage.md#testing-in-gem-studio). ## agents The agents a gem brings along. **Each agent is one file under `agents/`**, `agents/.md`: the file name is the agent's name, the YAML header (front matter) at the start says what it is for and which tools it can use, and the body after it is its system prompt. ```markdown --- description: Summarize and translate new emails, then file them. tools: [summarize_email, translate_email, save_record] platform_tools: [http.request] expose: true --- You are a calm, meticulous email assistant. … ``` **Agents aren't written in the manifest.** When packaging, Notochord reads every file under `agents/` and generates the `agents[]` in the package's manifest; if the `notochord.json` you wrote has `agents`, packaging reports an error. The same holds for Gem Studio drafts: there are no agents in `manifest.json`, and creating, renaming, or deleting an agent is creating, renaming, or deleting its file. **Name**: the file name without `.md`. Starts with a lowercase letter, contains only lowercase letters, digits, and underscores, up to 64 characters (`agents/mail_helper.md` is `mail_helper`). The file must be directly in `agents/`, not in a subfolder. SOPs call it with `ctx.call('agent.', …)`, and a page's assistant bubble names it with `data-agent=""`. The header can have only the following five keys; writing any other makes packaging report an error: | Key | Required | Description | | --- | --- | --- | | `description` | Yes | What this agent is for, in one or two sentences; shown on the gem's page. If it contains `: ` or `#`, put the whole sentence in quotes. | | `tools` | No | The gem's own tools it can call, written as tool names. | | `platform_tools` | No | The platform tools it can call, written with full names (`fs.cat`, `http.request`, `browser.navigate`, and so on). This is a permission request and is listed under "What it can do" on the gem's page. It can't include `tool_search`, `read_skill`, `agent.spawn_child`, or `search_gem_catalog`, nor another gem's tools (`gem.*`). | | `models` | No | The models its code uses; the key is a name, and `main` is the model it uses when conversing (present even if not written). Each entry has a single optional field, `requires`: what the model must be able to do besides text, see [Model capabilities](#model-capabilities) below. **The user chooses which model is used**, on the gem's page or in "Settings › AI Settings › Agents": a specific model, or one of the user's roles (the built-in ones are `chat`, `vision`, `deep`, and `local`; `local` is for models running on the user's own computer and is called "Local" in the interface). When nothing is chosen, the user's `chat` role is used; it can't be specified here. If you want to suggest what kind of model the user should use (for example, an agent that handles email or personal data should suggest "Local"), write it in the gem's introduction document (see [docs](#docs)). | | `expose` | No | `true`: exposed, so the main chat's AI hands tasks to it (see [Exposing items](#exposing-items-expose)). Then `description` is limited to 200 characters and counts toward the gem's limit of 5 items. If omitted, only this gem's SOPs and pages can use it. | Each item in the generated `agents[]` is `name`, `entry` (`agents/.md`), `description`, `tools`, `platformTools`, `models`, `expose`. After installation, Notochord decides what an agent can use from `agents[]` alone; the file's header is no longer read, and the body is the system prompt. An agent can only call the tools listed in `tools` and `platform_tools`; anything else is unavailable. The one exception is `read_skill`: when the gem has a skill, Notochord automatically adds it to every agent, only for reading the skill (see [skill](#skill)), and it neither needs to nor may be written in `platform_tools`. ### Model capabilities When an agent needs a model to do something beyond text, such as looking at images or listening to audio, say so in that `models` entry with `requires`: ```yaml models: { main: { requires: [image_input] } } ``` Only the six values below are allowed; writing anything else means the package **can't be built** (`E_PERM_INVALID`): | Value | The model must be able to | | --- | --- | | `image_input` | Understand images | | `audio_input` | Understand audio, including speech-to-text | | `video_input` | Understand video | | `image_output` | Generate images | | `audio_output` | Generate audio, including speech synthesis | | `video_output` | Generate video | The user chooses a model for each agent on the gem's page (when nothing is chosen, the user's `chat` role is used) and can also change it in "Settings › AI Settings › Agents". Notochord checks the chosen model against a list of the capabilities of common models: - The list says the model has all the capabilities you wrote: nothing is shown. - The list says it lacks one of them, or the model isn't in the list (for example, a model name the user made up), or the role has no model configured yet: a line of notice is shown for the user to confirm. - The notice doesn't block installation, and nothing is checked at run time. When the model really doesn't support it, what fails is your agent's call to the model, exactly as if you hadn't written `requires`. So write only the capabilities the agent can't do its job without in `requires`. Don't write ones that are only occasionally needed, so users don't see the notice every time. **The manifest has no per-agent run limits.** How many steps an agent may take in one run and how many tokens it may use are decided by the user in "Settings › System" under "Maximum loop steps" and "Token limit per run", and are the same for all agents. The `maxTurns` and `maxCostUsd` fields have been removed: a packaged manifest that contains them is rejected at install time for failing manifest validation. ## docs `docs/` is written for people: what this gem can do and how to use it. **It becomes the gem's introduction page in the store.** Nothing in this folder is used while the gem is in use: the gem's code, its agents, and the AI don't read it, so don't put instructions for the AI here (that is the [skill](#skill)). ``` docs/ README.en.md README.zh-CN.md prompts/ first-steps.en.md first-steps.zh-CN.md examples/ weekly-report.en.md ``` - **Markdown only** (`.md`), with any subfolders. Gem Studio drops other files when packaging; a package you built yourself that contains other files fails both packaging and installation (`E_PERM_INVALID`). Images can't be included. - **Each file is named by language**, using standard language codes (BCP 47: `en`, `zh-CN`, `ja`, `de`, …): - `README..md`: the body of the introduction page: what this gem does, who it is for, how to set it up, how to use it. **The languages you write are exactly the languages in the language menu of the store's introduction page.** Write at least one (`README.en.md`). - `prompts/..md`: optional, prompts that can be tried directly, each with a note on what it does. They get a separate "Prompts" tab on the store's introduction page. - `examples/..md`: optional, complete examples of using this gem. They get a separate "Examples" tab on the store's introduction page. - The store renders these files as ordinary Markdown, and each tab shows the language the user chose. The language versions of the same prompt or example share one `` (`prompts/first-steps.en.md`, `prompts/first-steps.zh-CN.md`). `docs/` doesn't need to be declared in the manifest. In Gem Studio, "Introduction docs" in the structure bar lists the files in this folder, and you can create and edit them there. ## resources Files that are too large to put in the install package (models, dictionaries, datasets). The manifest states their address, size, and checksum, and Notochord downloads them; a tool gets the local path with [`ctx.resources`](/en/dev/tools.md#ctxresources). ```json "resources": [ { "id": "ocr-model", "url": "https://cdn.example.com/models/ocr-v3.onnx", "mirrors": ["https://mirror.example.org/models/ocr-v3.onnx"], "sha256": "9f2c…", "size": 412000000, "required": true, "description": "OCR model" } ] ``` | Field | Required | Description | | --- | --- | --- | | `id` | Yes | The name used to get this file in a tool. It can't repeat within one manifest; a manifest with a repeat can't be installed. | | `url` | Yes | The download address, which must start with `https://`. | | `mirrors` | No | Fallback addresses, which also must be `https://`. They are tried one by one in order after `url` fails. | | `sha256` | Yes | The SHA-256 of the file's contents, 64 lowercase hex digits. | | `size` | Yes | The file size in bytes. | | `required` | No | `true`: the download starts in the background as soon as installation completes, and on every later app start it is tried again if the file isn't there yet. Default `false`: downloaded only when a tool first uses it. | | `description` | No | Shown on the gem's page. If omitted, the `id` is shown. | Downloads work like this: - **Notochord downloads, not the gem's code.** The download address doesn't need to be written into `network`, and no permission needs to be requested. The gem's page lists each file's description, size, which host it comes from, and whether it downloads right after install or only when used. - **Only `https` is accepted, and it can't point to a local or internal-network address.** Redirects are followed at most 5 times, and each hop is checked by the same rules. - **Subject to the user's network policy.** A host the user hasn't allowed in the network policy under "Settings › System" (allowlist / blocklist mode) can't be downloaded from; mirrors and hosts passed through by redirects count too. - **Both the size and the SHA-256 must match the manifest.** A file the server sends too much of, too little of, or with contents that don't match is discarded, and the next address is tried. If the remaining disk space isn't enough, the download doesn't start. - **A failed download doesn't affect installation.** The same goes for `required` files: installation doesn't wait for them and doesn't fail because of them. The gem works as usual, and only the call that uses this file reports an error; the download is retried automatically on the next call or the next app start. - **No repeated download on upgrade.** Files whose `sha256` is unchanged in the new version are reused; files the new version no longer declares are deleted after a successful upgrade. All are deleted when the gem is uninstalled. - Resuming an interrupted download is not supported; after a failure it starts over from the beginning. There is currently no download progress interface. In a Gem Studio draft, `resources` is likewise written at the top level of `manifest.json` with the same fields. You can also fill it in under "Resources" on the Gem Studio overview: after entering the address, click "Auto-fill size and SHA-256", and Notochord downloads the file by the same rules as downloading resources and computes those two values (the file isn't kept). During draft testing, `ctx.resources` works exactly as it does after installation. # Quickstart ## 1. First-run setup The first time an organization starts (including the first time you open Notochord), "Set up Notochord" appears first. It has five steps; use **Next** and **Back** to move between them. 1. **Language**: the dropdown lists all languages the app has been translated into (English, 简体中文). If you change the language, the window reopens in that language and continues to the next step. You can change it later in "Settings › System". 2. **Whether to encrypt the data stored on this computer**: see below. 3. **Connect an AI model**: this is the same table as "Settings › AI Settings › Model providers". Choose a provider (for example OpenAI, Anthropic, Google Gemini, OpenRouter, or Ollama on your computer), enter the API key it gave you, and add the models you want to use. The API key is stored only on your computer. You can click **Skip** on this step, which goes straight to the last step. 4. **Roles**: choose models for the four roles "Chat", "Vision", "Deep thinking", and "Local"; this is the same setting as "Settings › AI Settings › Roles". For what roles are and why it is recommended to set all four, see [Roles](/en/guide/models.md#roles). You don't need to choose for each agent one by one: they all use the "Chat" role. If "Local" already has a model when you click **Next** on this step, "Background tasks" (naming conversations, summarizing long conversations, describing the knowledge base) uses "Local" instead, so that content stays on this computer. You can change this later in "Settings › AI Settings › Agent". This step can also be skipped. 5. **All set**: three links: this page, [Organize Work with Projects](/en/guide/projects.md), and [Models and Roles](/en/guide/models.md). Click **Get started** to enter Home. ### Whether to encrypt the data stored on this computer If you choose "Encrypt (recommended)", these are stored encrypted: conversations, the record of operations the AI has performed, saved passwords and accounts, and the knowledge base. - **What it protects against**: if the computer gets a trojan, or the files are stolen, the other party cannot open this data. - **What it does not protect against**: content sent to the AI model provider is still sent as usual and is not encrypted because of this; if a hacker takes full control of the computer, the data can still leak; ordinary files are not encrypted: files in the folders you and your projects work with stay as they are. - **Risk**: the key that unlocks the data is stored in this computer's system keychain. If the key is lost (for example after reinstalling the system or changing computers), the data can never be opened again without the recovery key file. So when you choose "Encrypt", you must first click **Save recovery key…** and save the recovery key file before you can continue. Store it somewhere other than this computer. If you choose "Don't encrypt", the key and the data are in the same folder. Copy that folder to another computer and it opens directly, with no recovery key needed; conversely, anyone who gets the folder can read it. You make this choice only once, in the first-run setup. ### When no model is connected If you skipped the model in the first-run setup, you still enter Home when setup ends. On every later start, as long as there is still no model, the app first opens "Connect an AI model": choose a model provider, enter the API key, click **Connect**, then choose a model and click **Get started**. - If Ollama is already running on your computer, a notice at the top offers it directly; click **Use it**, no key needed. - To use a model file downloaded to this computer, click the link at the bottom to configure it in Settings. - You can also click **Skip for now**. Until a model is configured, "Chat" in the sidebar is greyed out, and clicking it brings you back to this page. You can add or change models any time in "Settings › AI Settings". ## 2. Create your first project Work that goes on for a while (tracking a job search, sorting a batch of invoices, running a small event) belongs in a project. Your projects are at the top of Home, and gems are below. Click **New project** and an "Untitled project" is created and opened right away; you can edit the name in place. In the assistant panel on the right, tell the assistant what the work is; it will build the tables and lay out the page. After that, both you and the assistant can edit what is in the tables. See [Organize Work with Projects](/en/guide/projects.md) for details. ## 3. Start your first conversation For things that don't need a project, use a conversation: "Chat" in the sidebar. - The sidebar is normally collapsed into a column of icons and expands when you move the mouse over it. - The "Chat" row has two buttons on its right: **New chat**, and **Start a chat around a folder** (choose a folder; the assistant reads and writes files in that folder). - When the sidebar is expanded, your conversations are listed under "Chat": pinned ones under "Pinned", the rest under "Recent". The "…" menu on each row lets you rename, pin, or delete. When the assistant wants to use a tool or gem that this conversation has not yet opened up, it asks you first; once you allow it, it can use it for the rest of that conversation. Every email sent needs your confirmation. ## 4. Try installing a Gem Gems are below on Home, and the first one is "Store". In the Store, click **Install**; when it finishes installing, the gem's page opens, where you do its setup. See [Gems: Install and Use](/en/guide/gems.md). # Organize Work with Projects A project holds one piece of ongoing work: its tables, a page (the canvas), a folder, and any number of conversations between you and the assistant. You and the assistant work on the same tables, and how far the work has got is recorded in the project, not in any single conversation. For one-off questions, the "Chat" entry in the sidebar is enough. For work that runs for days or weeks, where you keep a set of records and track progress, use a project. ## Create a project Your projects are at the top of Home. Click **New project** and an "Untitled project" is created and opened right away. You can edit the name in place; you can also click the name in the project's top bar later to change it. You can also start from a conversation: ask the assistant in that conversation to turn it into a project. The conversation becomes the new project's first conversation, and the files it produced are moved into the project's folder. If the conversation was using a folder you chose, that folder becomes the project's folder and the files are not moved. ## Work view Opening a project shows the Work view. The top bar has the project name, the "Work / Build" switch, three panel buttons (**Agent**, **Files**, **Data**), and the gear menu on the right. The assistant panel is on the right. Leaving the project page (going back to Home, or to Settings) does not interrupt conversations or runs in progress. ### Canvas Most of the page is the canvas: blocks of content arranged by the assistant, such as notes, tables, statistics, charts, boards, and forms. Each block shows live data from a table, and when the data in the table changes, the canvas follows. - The assistant does the layout: tell it what you want to see, and it adds, edits, and removes the blocks. - You can remove a block from the page; its data stays in the table. - Dragging a card to another column on a board, or adding a row in a form, changes the data in the table. ### The Agent, Files, and Data panels Clicking a button in the top bar opens the matching panel over the canvas. One panel is open at a time; click again to close it. **Data**: the project's tables. - Edit directly, like a spreadsheet. Each cell is saved as soon as you change it; there is no Save button. - The first column of every table is the row number `id`, numbered automatically; nobody can edit it. When you tell the assistant "row 7", you both mean the same row. - The **+** after the tabs creates a table; the **+** at the end of the header adds a column (enter a name and choose a type: text, number, yes / no, date, JSON); click "Add row" below the table. Double-click a table name or column name to rename it. - The "⋯" menu on a column header lets you change the column's type or hide the column; tables can be hidden too. Hiding does not delete data; click an item under "Hidden" to show it again. Only you can hide a whole table; the assistant cannot. - When you change a type, if even one value cannot be converted, the whole change is not applied and you are told which rows. Fix those rows first, then try again. **Files**: the project's folder. - Drag files or folders onto the panel (or onto the "Files" button in the top bar) to copy them in. Existing files with the same name are not overwritten; the new one gets " (2)" added to its name. - All of the project's conversations share this folder: attachments you drag into a conversation are stored here, files the assistant writes are here too, and the next conversation can use them. **Agent**: what is working for this project right now: sub-agents handed out by the project assistant, and the agents and SOPs of gems started by the project's conversations, each marked with its current state (for example "Working" or "Waiting for your approval"). The dropdown next to an agent lets you change the role or model it uses; see [Change a sub-agent's role yourself](/en/guide/models.md#changing-a-sub-agents-role-yourself). ### Assistant and conversations In the assistant panel on the right, a project can have several conversations: switch, create, or delete them at the top of the panel. You can drag the panel wider or collapse it; when collapsed, a narrow strip stays at the right edge of the window, and clicking it expands the panel. - Each conversation cannot see the contents of earlier conversations. What carries over is the tables and the folder, so anything you want to keep, ask the assistant to record in a table. - Before each reply, the assistant knows: the project name, the names of the accounts available to the project (it cannot see the secrets), which tables there are, and which rows have changed since its last reply, including changes you made in a table and changes made by other conversations. - Which model the assistant in a project uses is chosen in "Settings › AI Settings › Agent › Project"; see [Choose a model for each agent](/en/guide/models.md#choosing-a-model-for-each-agent). ### Gear menu - **Accounts**: which of your accounts (including email) gems working in this project can use. None are checked by default; an account that is not checked cannot be used when a gem works for this project. The assistant can see the names of checked accounts, not the secrets. - **Tools**: which tools the project assistant can use. If you have not changed it, it is the same as the "Chat" assistant; the table tools are always on. - **Code permissions**: see [Project code](#project-code) below. - **Open folder**, **Choose folder…**: a project uses its own folder by default, but you can switch to an existing folder on your computer so you don't have to import the files again. You cannot choose your home directory, system directories, the app's own data directory, or hidden folders. When you switch folders, files are not moved; the files on both sides stay where they are. Click "Use the project's own folder" to switch back. - **Delete project**: deletes its conversations and tables; the project's own folder goes to the trash; a folder you chose for it is left alone. Changes to accounts and tools take effect only after you click **Save**. ## Project code The assistant can write tools and SOPs (automated procedures) for a project, so the project can fetch data itself, process it row by row, or run automatically on a schedule. This code uses the project's own tables and folder. Before the code can do any of the following, it needs your permission: - access a website on the internet; - access the local and internal-network addresses you listed in "Settings › System › Network Access"; - use an AI model; - run automatically on a schedule. The top of the project page lists each item it requests, and it takes effect only after you click **Allow**. If only the code changed, or it asks for less than before, you are not asked again. The assistant cannot allow on your behalf. Under the gear menu › "Code permissions" you can see the items already allowed, and you can **Revoke all**: after that, scheduled runs stop and the code does not run until you allow it again. ## Build view Switching the top bar to "Build" shows the code behind the project: tools, SOPs, change history, and publishing. It looks like the Gem Studio workbench: you can read and edit code, test-run tools and SOPs, see the input, output, and errors of each run in the run log, and go back to earlier versions in the history. On the right it is still the same assistant and the same conversation. Most of the time you don't need to open the Build view; just have the assistant write and change the code. ## Publish as a gem Click **Publish…** in the top bar of the Build view. Publishing fixes this project's format and makes it a gem: anyone who installs it gets the same tables and code, starting empty. - **Included**: the structure of the tables (tables, columns, types, including hidden ones), tools and SOPs, and the permissions the code requests. - **Never included**: data in the tables, files, accounts, conversations. - The canvas cannot yet be put into a gem; the publish dialog states how many blocks will not be included. - If the code contains something the project cannot yet publish, publishing is blocked and the dialog lists the reasons. There are two kinds of publishing: - **Install on this computer**: installs it as a gem on this computer. It appears among the gems on Home, and its settings are on its own page (see [Gems: Install and Use](/en/guide/gems.md)). Publishing again updates this gem. - **Export a .gem file**: produces a file you can give to others or import in Gem Studio. The top bar of the Build view shows the publish status: "Not published", "Published to this computer", or "Changes not yet published". For an installed gem, the table format is fixed: you cannot change its table structure inside the gem. It changes only when you go back to the project, make the change, and publish again. # Chats and sessions ## The chat list Chats are under "Chats" in the sidebar. The sidebar is normally collapsed to a column of icons and expands when you hover over it; chats are listed only while it is expanded: - Pinned chats are under "Pinned"; the rest are under "Recent". - The "…" menu on each row lets you rename, pin (or unpin), or delete a chat. - Two buttons sit at the right of the "Chats" row: **New chat**, and **Start a chat around a folder**. Pick a folder and the assistant reads and writes files in it. Folders such as your home directory or system directories can't be picked. "Chats" is greyed out until a model is connected. Chats that belong to a project aren't in this list. They are in the assistant panel on the right side of the project; see [Organizing work with projects](/en/guide/projects.md). ## Sub-agents The assistant can hand part of its work to a sub-agent and choose a role for it based on the task (see [Roles](/en/guide/models.md#roles)). Click the "Experts and tools" button at the right of the input box: - The "Experts" column lists the sub-agents in this chat. Click one to see how it is working. Each has a "Model" dropdown where you can switch it to a different role or a specific model; the change takes effect from its next step. - The "Tools" column lists the tools the assistant can use in this chat. You can check or uncheck them here. # Permission Model ## Principles A gem lists the permissions it needs in its manifest. Only these need to be requested: network (`network`), the user's models (`llm` under `providers`, which spends the user's money), the knowledge base (`knowledge`), email (`email`), and the user's accounts (`connections`). A gem's own storage, files, and document extraction (`ctx.extract`) are not permissions; every tool of every gem has them. There is no confirmation step at install time (as in a phone app store, the user sees what the gem can do on its store page and installs it with one click). Install records the permissions listed in the manifest, and at run time only this record is consulted; the manifest is not read again. **Anything that needs configuration by the user is off until configured.** Which mailboxes the gem can read, which parts of the knowledge base it can use, which external account it uses: at install time there are none of these, and the gem's requests are rejected until the user turns them on in the gem's page. On upgrade, what the user already turned on is kept; anything new is likewise off by default. There is no confirmation popup at run time. ## Network ```json "permissions": { "network": { "hosts": ["api.open-meteo.com"], "reason": "Look up weather forecasts" } } ``` | Field | Required | Description | | --- | --- | --- | | `hosts` | Yes | External hosts the gem may reach. Host names only (`api.example.com`), with no scheme or path and no wildcards. If the service is not on the default port, include the port (`api.example.com:8443`; a different port is a different host). Internationalized domain names are written in `xn--` form. IP addresses and `localhost` may be listed only when `allowLocalNetwork` is `true`. A request to a host that is not listed is rejected before it is sent. | | `allowLocalNetwork` | No | Whether the gem may reach the user's own machine and local network. Defaults to `false`; see below. When `true`, `hosts` may be empty. | | `reason` | Yes | Why the gem needs network access. Shown on the gem's page. | `ctx.http.fetch` and `ctx.extract.fromUrl` in tools (download a document, then extract its text), and agents that can use the browser, are all checked against this same permission, including every redirect hop. On top of it sit the user's own network rules ("Settings › System › Network access"); a gem's permission can only narrow these, never widen them. ### Local machine and internal network Notochord sorts addresses into two classes with different rules: - **External network**: websites and online services. The user can choose allowlist or blocklist mode; the default is open. - **Local machine and internal network**: the user's computer (`localhost`, `127.0.0.1`, `[::1]`), addresses on the home or company network (`10.x.x.x`, `172.16–31.x.x`, `192.168.x.x`, `169.254.x.x`, `100.64–127.x.x`, and the IPv6 equivalents), and domain names that resolve to those addresses (for example a company-internal `wiki.corp.example.com`). Why they are separate: the local machine and internal network hold the user's own devices and internal services (router and NAS admin pages, printers, the company's OA system and wiki). These often have no password, because normally only people inside can reach them, and they often hold internal data. A web page the AI reads may hide instructions, and a third-party gem may try to probe the user's internal network; if these addresses were freely reachable, that would open a door between the internet and the user's internal network. So by default none of them can be reached, and only addresses the user has listed personally are allowed. For a gem to reach the local machine and internal network: - Write `"allowLocalNetwork": true` in the manifest (in a Gem Studio draft, write `"allow_local_network": true`, or tick "Local machine and internal network" under Permissions in the overview). The gem's page states that it can reach the local-machine and internal-network addresses the user has listed. - The gem can reach only the addresses the user has listed under "Network access › Local machine and internal network"; the gem cannot specify them itself. Local and internal addresses do not go into `hosts`. - A gem without `allowLocalNetwork` cannot reach an address even if the user has listed it. - Example: a gem that helps a developer test a website. Only after the user adds `localhost:5173` to the list can its agent open `http://localhost:5173` in the browser. - Downloads of [`resources`](/en/dev/manifest.md#resources) never reach the local machine or internal network. ## Files are not a permission A gem's code can touch only two kinds of files: **the folder of this one run**, and **the folder of the conversation it was called from**. Nothing outside the conversation is reachable, just like the AI in the conversation (its file tools also work only in the conversation folder). There are no fixed paths to request, and nothing to write in the manifest: every tool has `ctx.files`. For the read and write interface, see the [Tools API](/en/dev/tools.md#ctxfiles). ### Run folder Every run has its own folder: | Who is running | Where the run folder is | | --- | --- | | A SOP or tool called from a chat | In the conversation's working directory, at `runs/-