API 参考

Annulo 给项目的全部平台原语:本机函数和 ctx、业务表、定时任务、任务、插件、页面接口和命令行。写模板、写插件,或者想知道助手能做什么,从这里查。

能力版本 33源码
开始

概览

Annulo 只提供原语:让助手(或你自己)能在项目里做出业务功能、并让它稳定运行的通用能力。具体的业务(某个平台怎么发帖、某张表存什么)写在项目里,Annulo 不认识任何具体平台。

一个项目是一个目录,原语都通过这几类文件和接口用到:

位置用到的原语
local/*.ts本机函数 和 ctx:表、联网、浏览器、命令行、模型、MCP、密钥
tables/<表>.json业务表
schedules/<id>.json定时任务
tasks/、prompts/、skills/、ANNULO.md任务和说明
annulo.json、user/项目文件
plugins/<id>/插件
pages/页面能调的接口
能力版本

当前能力版本是 33。每加一个原语,版本 +1(历史)。模板用到了某个版本才有的原语,就在 annulo.json 里写 "min_annulo_api": <版本>:Annulo 低于它时,升级卡片提示先更新 Annulo、拒绝合并。

新旧名字

Shuttle 改名 Annulo 后,旧名字都还能用:shuttle.json / SHUTTLE.md / /_shuttle/ / X-Shuttle / shuttle 命令 / SHUTTLE_* 环境变量。

在线和离线

离线项目(默认)的数据在本机 SQLite;在线项目(连了 creght)的数据在 creght 上。两者的 ctx.db 同名同参数。标着 [在线] 的只有在线项目能用。

本机函数

本机函数

确定的、反复跑的逻辑写成本机函数。页面按钮、定时任务、助手都直接调用它,不经过模型。

怎么写
  • 文件放在 local/<文件>.ts,文件名匹配 ^[a-z0-9][a-z0-9_-]*$;以 _ 开头的文件(如 local/_api.ts)放共用代码,用相对路径 import;
  • 导出函数 export function name(input, ctx),可以是 async。调用名是 文件.函数;插件里的是 <插件>/<文件>.<函数>;
  • throw new Error(msg):message 原样显示给用户,stack 进日志;
  • 返回值要能转成 JSON。
// local/leads.ts
export async function stats(input: { days?: number }, ctx) {
  const since = new Date(Date.now() - (input.days ?? 30) * 864e5).toISOString()
  const { list } = ctx.db.aggregate('leads', {
    filter: [{ field: 'created_at', op: 'gte', value: since }],
    group_by: [{ field: 'created_at', trunc: 'day', as: 'day' }],
    metrics: [{ op: 'count' }],
  })
  return list
}
运行环境
  • esbuild 打成 CommonJS(ES2017)后在内嵌的 JS 引擎里跑,不能 require npm 包;talizen 只能做类型导入;
  • 全局有:fetch、console、URL(常用字段和 searchParams.get/has)、btoa / atob、TextEncoder / TextDecoder;
  • 没有 setTimeout,要等待用 await ctx.sleep(ms);
  • 一次运行最长 60 分钟。
怎么调
annulo run                                  # 列出所有函数 / list functions
annulo run leads.stats --input '{"days":7}'
annulo run leads.stats --input @input.json

进度打到 stderr,结果打到 stdout。页面里调见 页面能调的接口。运行日志:annulo logs --fn leads.stats。

本机函数

ctx 一览

函数的第二个参数 ctx。「云端」列是 cloud 函数 里能不能用。

成员签名说明云端
ctx.db见 业务表读写query / aggregate / get / insert / update / delete✓
fetchawait fetch(url, { method, headers, body })全局 fetch,不能访问本机和内网,见 联网✓
ctx.fetchAllawait ctx.fetchAll([url | { url, method, headers, body }])最多 4 个并发,顺序和输入一致✗
ctx.htmlctx.html(text) → .find(css) / .text() / .markdown()解析 HTML✗
ctx.browser见 浏览器用本机 Chrome 和你的登录状态操作网页✗
ctx.execawait ctx.exec(cmd, args, { cwd?, input?, timeout?, env? })跑命令行工具,见 命令行✗
ctx.secrets.get(name) → string | null读 设置 → 密钥 里的值,或环境变量✗
ctx.oauthawait ctx.oauth('google', { account? }) → access_token设置 → 连接 里授权的账号;ctx.oauth.accounts('google') 列出账号✗
ctx.mcpawait ctx.mcp(server, tool, args?)调接好的 MCP 工具,JSON 结果已解析;ctx.mcp.servers() 看连接状态只有 creght
ctx.llmawait ctx.llm(prompt) 或 ctx.llm({ system, prompt }) → string用当前模型答一次,不带工具、不进对话✗
ctx.llm.providers() → [{ id, name, kind, models, … }]设置 → 模型 里的服务商,不含 key✗
ctx.llm.fetchawait ctx.llm.fetch(providerId, path, init) → Response直接调服务商接口,Annulo 补地址和鉴权(可流式)✗
ctx.agent.current() → { id, name, provider, model, ready, error }助手当前用的模型、能不能跑✗
ctx.progress({ done, total, message })推进度到页面和命令行不生效
ctx.log(...args)写运行日志不生效
ctx.sleepawait ctx.sleep(ms)最长 60 秒不生效
ctx.locale'zh' | 'en'界面语言✓
ctx.workspace对象project_id、site_id、offline、machine: { id, name }、logs_dir、annulo_api…undefined
ctx.chat_idstring助手在对话里跑时是那段对话的 id,按钮和定时任务是 ''''

ctx.llm 只用在没人盯着的短判断(比如给每条新询盘打分)。用户点按钮、要 AI 写长内容的,做成 任务。

本机函数

业务表读写

ctx.db 只能读写项目里 声明过的表。读写是同步的。

方法返回
query(table, { where?, filter?, order_by?, limit?, offset?, cursor? }){ total, list, limit, next_cursor };limit 默认 20,1–1000
aggregate(table, { where?, filter?, group_by?, metrics, order_by?, limit?, timezone? }){ list, truncated };最多 10000 组
get(table, id)一行或 null
insert(table, data)新的一行(带 id)
update(table, id, data){ ok: true };顶层浅合并,值写 null 删掉这个字段
delete(table, id)—
const { total, list, next_cursor } = ctx.db.query('articles', {
  where: { status: 'draft' },
  filter: [{ field: 'words', op: 'gte', value: 800 }],
  order_by: 'updated_at desc',
  limit: 50,
})
const row = ctx.db.insert('articles', { title: 'Hello', status: 'draft' })
ctx.db.update('articles', row.id, { status: 'published', draft_note: null })
条件
  • where:字段相等,{ channel_id: 'x' }。标量也匹配数组字段里的一项;where.id 按记录主键读;
  • filter:[{ field, op, value }],或 { match: 'and', conditions: [{ fieldId, operator, value }] },条件之间都是 AND;
  • 运算符:eq(默认)、neq、in、gt、gte、lt、lte、between(value 写 [起, 止],两端都含)。数字按数值比、字符串按字典序,类型不同的不参与。写错直接报错,不会悄悄变成不过滤;
  • order_by:'date desc',多个用逗号分隔;默认新建的在前;
  • cursor:读完全表用。第一页传 '',之后传上一页的 next_cursor,它为空就读完了;不能和 order_by、offset 同时用。
汇总
  • group_by:['field', { field, trunc: 'day' | 'week' | 'month' | 'year', as }],week 从周一算;
  • metrics:[{ op: 'count' | 'sum' | 'avg' | 'min' | 'max' | 'first' | 'last', field, as, order_by }],first / last 是组内按 order_by 排序后的首尾值;
  • 输出列名默认 <op>_<field>,count 是 count;avg 保留 6 位小数;
  • timezone 默认 Asia/Shanghai。

系统字段:id、created_at、updated_at。助手的 db_query / db_aggregate 工具和这里同参数。

本机函数

联网

fetch

全局 fetch(url, { method, headers, body }),返回的 Response 有:

  • ok、status、statusText、url、headers.get(name)、headers.forEach(fn);
  • text()、json()、arrayBuffer();
  • body.getReader():流式读,read() 返回 { done, value: Uint8Array };
  • timing: { ttfb_ms, total_ms }、truncated。

限制:不能访问本机和内网;响应体最多 10MB(超出时 text() 截断、truncated 为 true);等响应头最多 2 分钟,之后连续 1 分钟没数据才断开,不限总时长。

ctx.fetchAll

一次发多个请求,最多 4 个并发,结果顺序和输入一致。失败的那一项是 { ok: false, status: 0, url, error },不会让整批失败。

ctx.html

ctx.html(text) 解析 HTML:.find(css) 返回 [{ tag, text, attrs }],.text() 取纯文本,.markdown() 转成 Markdown。选择器写错直接报错。

本机函数

浏览器

ctx.browser 用这台电脑上的 Chrome 操作网页。每个 profile 是一份独立的登录状态(存在本机),社媒发布、采集就靠它:和你自己操作一样,同一台设备、同一个网络。

const b = await ctx.browser.open({ profile: 'x-main', url: 'https://x.com/home' })
await b.waitFor('[data-testid="tweetTextarea_0"]')
await b.type('[data-testid="tweetTextarea_0"]', input.text)
await b.click({ text: 'Post' })
await b.close()
  • ctx.browser.open({ profile, url?, show?, offscreen?, keep_open? }) → b:keep_open 要配 show: true;
  • ctx.browser.profile(name) → { id } | null:这个 profile 在不在。
方法说明
goto(url, { wait?, timeout? })打开网址
waitFor(sel, { visible?, timeout? })等元素出现
exists(sel)元素在不在
click(sel | { text }, { timeout? })点击
type(sel, text, { clear?, timeout? })输入
press(key)按键
upload(sel, files)上传:网址(流式下载,最多 2GB)、/_annulo/uploaded/…、'local:<name>'、截图的 { file }
eval(fn | string)在页面里执行,返回值要能转成 JSON
text(sel?) / html(sel?) / url()读文字、源码、当前地址
listen(pattern) / responses(pattern, { min?, timeout? })记录并取回匹配的接口响应 [{ url, status, json, text }]
screenshot({ selector?, fullPage? }) → { file }截图
snapshot({ label }) → { dir }主动存一份现场
setContent(html)把一段 HTML 放进页面
close()关闭

单个操作默认最多等 30 秒。goto / waitFor / click / type / upload / responses 失败时自动存一份现场(截图、可操作的元素、源码),报错里带 err.snapshot,助手修脚本时照着它改。

本机函数

命令行、密钥、MCP、模型

ctx.exec

await ctx.exec(cmd, [args…], { cwd?, input?, timeout?, env? }) → { code, stdout, stderr, truncated }

  • 不经过 shell,命令和参数分开传;要管道、重定向就 ctx.exec('sh', ['-c', …]);
  • 命令按登录 shell 的 PATH 找;cwd 相对项目根目录,不能出项目;
  • timeout 默认 30 秒,最多 10 分钟;stdout、stderr 各保留 10MB;
  • 退出码不是 0 不会 reject,自己看 code;找不到命令、超时、cwd 不对才 reject。
ctx.secrets

ctx.secrets.get(name) 读 设置 → 密钥 里存的值(没有就读同名环境变量),没有返回 null。密钥只存在本机,助手读不到。

ctx.oauth

await ctx.oauth('google', { account? }) 拿 设置 → 连接 里授权的 access token(Search Console、GA4 只读)。连了多个账号时用 account 指定,ctx.oauth.accounts('google') 列出账号,最早连的在前。

ctx.mcp

await ctx.mcp(server, tool, args?) 调 设置 → MCP 里接好的工具,结果是 JSON 文本时已解析;工具报错时 reject。ctx.mcp.servers() 返回 [{ name, status }],status 是 connected / needs_auth / connecting / failed / disabled。

ctx.llm
  • await ctx.llm('prompt') 或 ctx.llm({ system, prompt }):用当前模型答一次,返回字符串;
  • ctx.llm.providers():服务商列表 [{ id, name, kind: 'api' | 'cli', api, base_url, enabled, ready, models: [{ id, model, name, web_search }] }],不含 key;
  • ctx.llm.fetch(providerId, path, init):直接调服务商的接口,Annulo 补地址和鉴权,可以流式读;只对 kind === 'api' 的服务商。
云端和手机

云端和远程函数

[在线] 本机函数默认只在 Annulo 里能调。项目连了 creght 以后,手机、别的电脑也能打开后台,这时页面要调的函数在哪跑?由你在函数文件里声明:

  • export const cloud = [...]:在 creght 上跑,电脑关着也能用。只能读写业务表;
  • export const remote = [...]:转到你的电脑上跑,要电脑开着。本机能用的都能用(浏览器、密钥、命令行)。
例子:出门在外,看小红书数据

你在地铁上,用手机打开后台的「社媒」页:

  1. 页面先显示近 30 天的点赞趋势。 这只是读表、算一下,在 creght 上就能做,家里电脑关着也没关系 → cloud;
  2. 你觉得数据旧了,点「立即采集」。 采集要打开小红书主页、用你登录过的账号去读,这些只有你的电脑上有 → remote:creght 把这次调用转给家里开着的 Annulo,它在电脑上开浏览器读数、写进表,进度一路推回你的手机;
  3. 没人点的时候,定时任务每 6 小时在电脑上采一次。

同一个文件,两个函数,各自声明在哪跑:

// local/xhs.ts
export const remote = ['collect']   // 要用浏览器的登录状态:只能在你的电脑上跑
export const cloud = ['summary']    // 只读表:在 creght 上跑,电脑关着也能用

/** 打开账号主页,读每篇笔记的点赞,记一行当天的数 */
export async function collect(input: { channel_id: string }, ctx) {
  const ch = ctx.db.get('social_accounts', input.channel_id)
  ctx.progress({ message: `正在打开「${ch.name}」的主页…` })   // 手机上的按钮也会显示这句

  const b = await ctx.browser.open({ profile: ch.profile, url: ch.home_url })
  const notes = await b.eval(() =>
    [...document.querySelectorAll('section.note-item')].map((el) => ({   // 选择器按平台页面写
      id: el.getAttribute('data-id'),
      likes: Number(el.querySelector('.count')?.textContent ?? 0),
    })),
  )
  await b.close()

  const today = new Date().toISOString().slice(0, 10)
  for (const n of notes) ctx.db.insert('social_post_daily', { channel_id: ch.id, post_id: n.id, date: today, likes: n.likes })
  return { notes: notes.length }
}

/** 近 days 天每天的点赞合计 */
export function summary(input: { channel_id: string; days?: number }, ctx) {
  const since = new Date(Date.now() - (input.days ?? 30) * 864e5).toISOString().slice(0, 10)
  return ctx.db.aggregate('social_post_daily', {
    where: { channel_id: input.channel_id },
    filter: [{ field: 'date', op: 'gte', value: since }],
    group_by: ['date'],
    metrics: [{ op: 'sum', field: 'likes', as: 'likes' }],
  }).list
}
// schedules/xhs-collect.json:没人点的时候,电脑每 6 小时采一次
{ "name": "采集小红书", "fn": "xhs.collect", "every": "6h", "input": { "channel_id": "<账号 id>" } }

写完 annulo push 一次:summary 被打包成 creght 上的站点函数;collect 不用打包,Annulo 连上平台时自己报上去。

在哪运行
谁调的xhs.summary(cloud)xhs.collect(remote)
电脑上,Annulo 里点按钮这台电脑这台电脑
手机上点按钮creght 上,电脑关着也行creght 中转 → 你的电脑(Annulo 要开着)
定时任务—这台电脑
助手在对话里 annulo run这台电脑这台电脑

页面不用管这些:在 Annulo 里调 local/run;在 Annulo 外,cloud 函数调站点函数 local/<文件>,remote 函数经站点函数 shuttle.call 转给电脑。模板的 runLocal('xhs.summary', …) 已经包好了这一层,同一行代码在两边都能用。

cloud 的规则
  • 只有项目成员能调;
  • 能用:ctx.db、ctx.locale、ctx.mcp('creght', …)(只读、只给所有者)、ctx.mcp.servers();progress / log / sleep 不生效,workspace 是 undefined;
  • 用到 secrets、fetchAll、html、oauth、browser、llm、exec、agent 会报错。所以上面的 collect 放不进 cloud。
remote 的规则
  • 电脑上 Annulo 要开着,并且 设置 → 远程访问 里打开了「允许远程调用这台电脑」(默认关);
  • 只有项目所有者能调,电脑只执行声明在 remote 里的函数;
  • 函数照常在电脑上跑,ctx.progress 的进度会推回手机。

同一个函数不能既在 cloud 又在 remote。

项目文件

业务表

一张表一个文件 tables/<表>.json,文件名就是表名(匹配 ^[a-z][a-z0-9_]{1,40}$)。

{
  "name": "询盘",
  "desc": "网站表单和邮件进来的询盘",
  "json_schema": {
    "type": "object",
    "properties": {
      "email":   { "type": "string", "description": "客户邮箱" },
      "score":   { "type": "integer", "description": "意向分 0-100" },
      "tags":    { "type": "array" }
    },
    "required": ["email"]
  }
}
  • 声明了就能读写:离线项目不用建表;在线项目在文件变了时自动补建缺的表;
  • 只有声明过的表能读写:ctx.db、页面的 db/<表>、助手的工具都受这条限制;
  • 插件的表名自动加前缀:plugins/<id>/tables/t.json 在项目里是 <id>_t;
  • 系统字段:id、created_at、updated_at;
  • json_schema 是给人和助手看的说明,Annulo 不按它校验写入。
项目文件

定时任务

一个任务一个文件 schedules/<id>.json,文件名就是 id。

// schedules/collect.json
{ "name": "采集社媒数据", "fn": "social.collect", "every": "6h" }

// schedules/weekly.json
{ "name": "写周报", "task": "weekly-report", "every": "7d", "at": "09:00" }
字段说明
fn / task / prompt三选一:跑本机函数;跑 tasks/<id>.md;把一段话交给助手(新开一段对话)
every30m / 6h / 1d / 7d,最短 5 分钟
atHH:MM,本机时区;和 every 都写时按 at
input传给函数或任务的参数
name显示名
  • 只在 Annulo 开着时跑;错过的那次下次打开时补跑一次;
  • task / prompt 一轮最长 30 分钟,只在当前打开的项目里跑;别的项目只跑 fn;
  • 插件的定时任务 id 是 <插件>/<id>,里面的 fn / task 写全名。
项目文件

任务、写法和说明

任务 tasks/<id>.md

交给助手的活(写文章、出选题、写周报)。frontmatter 只认三个单行字段:name、description、thinking(off / minimal / low / medium / high / xhigh / max),正文是给助手的说明。页面按钮通过 local/tasks/<id>/run 在后台开一段对话去跑。插件的任务 id 是 <插件>/<任务>。

写法 prompts/<id>.md

任务里用户关心的「怎么写」(结构、语气、长度)单独放一个文件:模板给默认的 prompts/<id>.md,用户在页面上改了存到 user/prompts/<id>.md,有就优先用它。取数、存表、格式这些规则留在任务文件里。

skill skills/<名字>/SKILL.md

写给助手的做事说明,frontmatter 有 name(小写字母、数字、-,和目录名一致)和 description。

项目说明
  • ANNULO.md:项目的总说明,每段新对话都放进助手的系统提示,最多 8000 字;
  • INSTRUCTIONS.md:用户在 设置 → 项目 里写的「给助手的说明」,和项目说明冲突时以它为准。
项目文件

项目文件和 user/

annulo.json
{
  "name": { "zh": "自媒体工作台", "en": "Creator studio" },
  "description": { "zh": "…", "en": "…" },
  "min_annulo_api": 33,
  "plugins": { "social": "https://github.com/annulo/plugins#social" },
  "assistant": {
    "intro": { "zh": "…", "en": "…" },
    "suggestions": { "zh": ["…"], "en": ["…"] }
  }
}
字段说明
name / description模板的名字和介绍,字符串或 { zh, en }
min_annulo_api需要的 能力版本,Annulo 低于它时拒绝升级合并
plugins{ "<id>": "<仓库>#<目录>" },新建项目时自动装
assistant右侧助手面板:name、intro、suggestions(每项字符串或 { zh, en })

模板要兼容能力版本低于 26 的 Annulo,还要带一份 shuttle.json。

user/

用户自己的定制:user/prompts/、user/annulo.json(用户装、卸的插件;写 null 表示不要模板带的那个)、user/plugins/<id>/…。模板和插件都不往这里写,所以升级永远不会和用户的定制冲突。要给用户留可定制的地方:模板给默认,用户的存到 user/ 下,有就优先用。

模板版本

模板是 git 仓库里的一个目录(有 annulo.json),版本是 semver tag(v1.2.0,预发布版不提供),tag 的说明就是用户看到的升级说明。升级时 Annulo 在项目的 git 里做三方合并。

项目文件

插件

插件是一组能装进任何项目的功能,放在 plugins/<id>/(id 匹配 ^[a-z][a-z0-9]{1,19}$),和模板分开升级。

文件说明
plugin.json{ name, description, min_annulo_api },name / description 可写 { zh, en }
PLUGIN.md给助手的说明
local/、tables/、tasks/、prompts/、schedules/、skills/、components/和项目里的同名目录一样
  • 不带页面,页面由模板或助手做;
  • 函数调用名 <id>/<文件>.<函数>,表名 <id>_<表>,任务和定时任务 id <id>/<名字>;
  • 用户改过的写法在 user/plugins/<id>/prompts/<任务>.md;
  • 发布:仓库里一个有 plugin.json 的子目录,版本是 semver tag。示例:annulo/plugins。
页面和命令行

页面能调的接口

pages/ 下每个文件是一条路由(react-router 7,只在客户端渲染),Annulo 在本机渲染,保存就刷新。页面通过同源接口用原语:前缀 /_annulo/api/,必须带请求头 `X-Annulo: 1`。响应都带 X-Annulo 头,页面靠它判断自己在不在 Annulo 里。

const res = await fetch('/_annulo/api/local/run', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'X-Annulo': '1' },
  body: JSON.stringify({ fn: 'leads.stats', input: { days: 7 } }),
})
// SSE:data: {"type":"progress",...} … data: {"type":"result","data":{"value":…,"ms":…}}
接口说明
GET local/functions{ list: [{ name, file }] }
POST local/run{ fn, input },返回 SSE:start、progress、log,最后 result { value, ms } 或 error { message }。页面关掉也会跑完
POST local/runs/<id>/abort停止一次运行(id 在响应头 x-annulo-run)
GET local/logs?fn=&id=&limit=运行日志
GET local/tasks、GET local/tasks/<id>任务列表:说明、写法、正在跑的、上次结果
PUT local/tasks/<id>{ prompt } 改写法,{ reset_prompt: true } 恢复默认
POST local/tasks/<id>/run{ input? } → { chat_id },后台开一段对话跑任务;同样参数正在跑时 409
POST local/ask{ text, title? } → { chat_id },后台开一段对话交给助手
GET agent/running正在跑的对话,页面轮询它看交出去的活跑完没有
GET local/schedules、POST local/schedules/<id>/run、…/toggle定时任务:列表、立即运行、暂停 / 恢复
GET db/<表>{ list }:全表,新的在前
POST db/<表>、PATCH db/<表>?id=、DELETE db/<表>?id=新建、合并更新、删除一行
POST local/uploadmultipart 字段 file,单个最多 200MB → { url, … }
POST local/files请求体是文件,头 X-Filename,最多 8GB → { ref: 'local:<name>', url }(存本机,给浏览器上传用)
PUT local/secrets、GET local/secrets?names=A,B写密钥;查哪些设了(不返回值)
POST fetch代发请求 { url, method, headers, body },响应最多 5MB,不能访问内网
GET /_annulo/img?url=图片代理(不带 Referer),单张最多 20MB
和 Annulo 外壳通信

页面发 window.parent.postMessage(msg, location.origin):

  • { type: 'annulo:open-chat', chat_id }:在右侧打开一段对话;
  • { type: 'annulo:navigate', view: 'settings' }:打开 Annulo 的设置;
  • { type: 'annulo:reload-backend' }:整页刷新左侧页面。

外壳会发 shuttle:refresh(助手改了数据,静默重拉)和 shuttle:theme { theme }。

[在线] 页面不在 Annulo 里打开时(手机),改调站点函数:cloud 函数直接在 creght 上跑,remote 函数转到电脑上跑,见 云端和远程函数。

页面和命令行

命令行 annulo

命令调的是本机正在跑的 Annulo,助手在 bash 里也用这些命令。

命令说明
annulo run [<文件.函数>] [--input JSON|@file]跑本机函数;不带参数列出所有函数
annulo logs [--fn name] [--id runId] [--limit N]看运行日志(最多 100 条)
annulo upload <文件…> [--json]把本机文件传进当前项目,打印地址
annulo push [-m msg]提交项目的 git;[在线] 再合并远端、推到 creght 预览、生成云端函数
annulo template看当前项目的模板版本和更新说明
annulo template upgrade [--to N]三方合并升级到最新版(或指定版本)
annulo mcp list已接的 MCP 和状态
annulo mcp add <name> <url>接远程 MCP;-H 'Authorization: Bearer ${KEY}' 加请求头(${密钥名} 会替换)
annulo mcp add <name> -- <cmd> [args…]接本机 MCP;-e K=V 加环境变量,--force 替换同名
annulo mcp remove <name>删掉一个 MCP

老命令 shuttle 也能用。

页面和命令行

助手的工具

Annulo 给助手加的工具很少,每个都是对话里高频、又容易写错参数的事。别的事助手用命令行(annulo run 等)做。用 Claude Code / Codex 当助手时,通过 Annulo 的 MCP 拿到同一套工具。

工具参数说明
db_query{ table, where?, filter?, order_by?, limit?, fields?, cursor? }只读查表,limit 最多 200
db_aggregate{ table, where?, filter?, group_by?, metrics, order_by?, limit?, timezone? }分组汇总,和 ctx.db.aggregate 同参数
page_errors{ reload?, path?, wait_seconds? }看左侧页面有没有报错,改完页面用它验证
request_user_input{ title?, description?, questions: [{ id, type, label, options?, … }] }弹一组问题(1–6 个,单选 / 多选 / 短文本 / 长文本)让用户回答,这一轮结束,回答作为下一条消息

接好的 MCP 工具名是 mcp__<server>__<tool>。

页面和命令行

能力版本历史

版本加了什么
33离线上传地址改成不带端口的 /_annulo/uploaded/…;annulo upload
32ctx.chat_id
31任务 frontmatter 的 thinking
30ctx.mcp.servers()、ctx.agent.current()、服务商的 kind
29ctx.exec
28插件
27annulo.json 的 assistant
26Annulo 新名字(旧名字都还认)
25模型的 web_search 标记
24流式 fetch body、TextEncoder / TextDecoder、arrayBuffer;ctx.llm.providers / ctx.llm.fetch;去掉 ctx.fetch
23多账号 ctx.oauth、ctx.oauth.accounts
22手机上用助手;离线项目、workspace.offline
21export const remote
20export const cloud
19ctx.db 的 filter 写法、offset、total
18页面在本机渲染、react-router
17ctx.browser.profile
16表、定时任务改成一个一个文件
15workspace.machine
14where.id;运行日志、annulo logs
13browser.open 的 keep_open
12local/files 和 'local:<name>'
11浏览器失败现场、b.snapshot
10b.upload 传网址时流式下载
9Google 连接加 GA4
8prompts/ 和 user/prompts/;local/ask
7任务 tasks/<id>.md;定时任务可以跑任务
6ctx.locale
5页面能调 local/upload、local/secrets
4定时任务可以是 prompt;open-chat 消息
3query 的 filter / order_by / cursor;ctx.db.aggregate
2ctx.browser、ctx.oauth、定时任务
1本机函数和 ctx.db、fetch、html、secrets、llm、mcp;业务表;run / push;模板升级

Render diagnostics