API 参考
Annulo 给项目的全部平台原语:本机函数和 ctx、业务表、定时任务、任务、插件、页面接口和命令行。写模板、写插件,或者想知道助手能做什么,从这里查。
概览
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 | ✓ |
fetch | await fetch(url, { method, headers, body }) | 全局 fetch,不能访问本机和内网,见 联网 | ✓ |
ctx.fetchAll | await ctx.fetchAll([url | { url, method, headers, body }]) | 最多 4 个并发,顺序和输入一致 | ✗ |
ctx.html | ctx.html(text) → .find(css) / .text() / .markdown() | 解析 HTML | ✗ |
ctx.browser | 见 浏览器 | 用本机 Chrome 和你的登录状态操作网页 | ✗ |
ctx.exec | await ctx.exec(cmd, args, { cwd?, input?, timeout?, env? }) | 跑命令行工具,见 命令行 | ✗ |
ctx.secrets.get | (name) → string | null | 读 设置 → 密钥 里的值,或环境变量 | ✗ |
ctx.oauth | await ctx.oauth('google', { account? }) → access_token | 设置 → 连接 里授权的账号;ctx.oauth.accounts('google') 列出账号 | ✗ |
ctx.mcp | await ctx.mcp(server, tool, args?) | 调接好的 MCP 工具,JSON 结果已解析;ctx.mcp.servers() 看连接状态 | 只有 creght |
ctx.llm | await ctx.llm(prompt) 或 ctx.llm({ system, prompt }) → string | 用当前模型答一次,不带工具、不进对话 | ✗ |
ctx.llm.providers | () → [{ id, name, kind, models, … }] | 设置 → 模型 里的服务商,不含 key | ✗ |
ctx.llm.fetch | await 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.sleep | await 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_id | string | 助手在对话里跑时是那段对话的 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(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 分钟没数据才断开,不限总时长。
一次发多个请求,最多 4 个并发,结果顺序和输入一致。失败的那一项是 { ok: false, status: 0, url, error },不会让整批失败。
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、模型
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.get(name) 读 设置 → 密钥 里存的值(没有就读同名环境变量),没有返回 null。密钥只存在本机,助手读不到。
await ctx.oauth('google', { account? }) 拿 设置 → 连接 里授权的 access token(Search Console、GA4 只读)。连了多个账号时用 account 指定,ctx.oauth.accounts('google') 列出账号,最早连的在前。
await ctx.mcp(server, tool, args?) 调 设置 → MCP 里接好的工具,结果是 JSON 文本时已解析;工具报错时 reject。ctx.mcp.servers() 返回 [{ name, status }],status 是 connected / needs_auth / connecting / failed / disabled。
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 = [...]:转到你的电脑上跑,要电脑开着。本机能用的都能用(浏览器、密钥、命令行)。
你在地铁上,用手机打开后台的「社媒」页:
- 页面先显示近 30 天的点赞趋势。 这只是读表、算一下,在 creght 上就能做,家里电脑关着也没关系 →
cloud; - 你觉得数据旧了,点「立即采集」。 采集要打开小红书主页、用你登录过的账号去读,这些只有你的电脑上有 →
remote:creght 把这次调用转给家里开着的 Annulo,它在电脑上开浏览器读数、写进表,进度一路推回你的手机; - 没人点的时候,定时任务每 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', …) 已经包好了这一层,同一行代码在两边都能用。
- 只有项目成员能调;
- 能用:
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。
- 电脑上 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;把一段话交给助手(新开一段对话) |
every | 30m / 6h / 1d / 7d,最短 5 分钟 |
at | HH: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,有就优先用它。取数、存表、格式这些规则留在任务文件里。
skills/<名字>/SKILL.md写给助手的做事说明,frontmatter 有 name(小写字母、数字、-,和目录名一致)和 description。
ANNULO.md:项目的总说明,每段新对话都放进助手的系统提示,最多 8000 字;INSTRUCTIONS.md:用户在 设置 → 项目 里写的「给助手的说明」,和项目说明冲突时以它为准。
项目文件和 user/
{
"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/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/upload | multipart 字段 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 |
页面发 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 |
| 32 | ctx.chat_id |
| 31 | 任务 frontmatter 的 thinking |
| 30 | ctx.mcp.servers()、ctx.agent.current()、服务商的 kind |
| 29 | ctx.exec |
| 28 | 插件 |
| 27 | annulo.json 的 assistant |
| 26 | Annulo 新名字(旧名字都还认) |
| 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 |
| 21 | export const remote |
| 20 | export const cloud |
| 19 | ctx.db 的 filter 写法、offset、total |
| 18 | 页面在本机渲染、react-router |
| 17 | ctx.browser.profile |
| 16 | 表、定时任务改成一个一个文件 |
| 15 | workspace.machine |
| 14 | where.id;运行日志、annulo logs |
| 13 | browser.open 的 keep_open |
| 12 | local/files 和 'local:<name>' |
| 11 | 浏览器失败现场、b.snapshot |
| 10 | b.upload 传网址时流式下载 |
| 9 | Google 连接加 GA4 |
| 8 | prompts/ 和 user/prompts/;local/ask |
| 7 | 任务 tasks/<id>.md;定时任务可以跑任务 |
| 6 | ctx.locale |
| 5 | 页面能调 local/upload、local/secrets |
| 4 | 定时任务可以是 prompt;open-chat 消息 |
| 3 | query 的 filter / order_by / cursor;ctx.db.aggregate |
| 2 | ctx.browser、ctx.oauth、定时任务 |
| 1 | 本机函数和 ctx.db、fetch、html、secrets、llm、mcp;业务表;run / push;模板升级 |