文档 · 插件
编写插件
插件负责登录某个服务、替它发请求。magpie 把插件变成一个供应商,所有 agent 都能用,多账号、故障切换、用量这些也都有。magpie 运行的是 OpenCode 的供应商插件,为 OpenCode 写的插件不用改就能在这里用;注册了供应商的 pi 扩展包也能直接用(见 pi 扩展包)。本页讲插件作者需要知道的:magpie 调用哪些钩子、拿到返回值后做什么,以及怎么测试、发布和上架插件。
config声明供应商和它的默认模型。auth给出登录方式,以及一个loader,由它的fetch发出每个请求。provider.models列出某个账号有哪些模型。auth.usage说明套餐用了多少。这是 magpie 自己的钩子,OpenCode 会忽略它。auth.refresh在账号的 token 过期前续期,同一时间只续一次。它也是 magpie 自己的钩子。
fetch,再把回答流式传回去。五分钟写一个插件
这个例子登录 Acme,一个虚构的编程套餐。Acme 用 API key 登录,接口兼容 OpenAI,并能报告每个账号的模型和用量。真实的插件主要区别在于怎么登录、怎么给请求签名。整个例子已在 magpie v0.1.676 和本地模拟的 Acme API 上跑通。
{
"name": "opencode-acme-auth",
"version": "0.1.0",
"description": "OpenCode provider plugin: Acme Coding Plan",
"type": "module",
"main": "./index.mjs",
"files": ["index.mjs", "README.md"],
"keywords": ["opencode", "opencode-plugin", "acme"],
"license": "MIT",
"magpie": { "maxConcurrency": 4 }
}
const PROVIDER = "acme" const BASE = "https://api.acme.example/v1" export const AcmeAuthPlugin = async ({ client }) => ({ // 供应商,以及还没人登录时它有的模型 config: async (cfg) => { cfg.provider ??= {} cfg.provider[PROVIDER] ??= { name: "Acme", npm: "@ai-sdk/openai-compatible", // chat completions api: BASE, models: { "acme-coder": { name: "Acme Coder", limit: { context: 200000, output: 32000 }, tool_call: true }, }, } }, auth: { provider: PROVIDER, methods: [{ type: "api", label: "Acme API key", placeholder: "acme-…" }], // 这个账号的每个请求带上什么 async loader(getAuth) { const auth = await getAuth() if (auth?.type !== "api") return {} return { baseURL: BASE, async fetch(input, init) { const headers = new Headers(init?.headers) headers.set("authorization", `Bearer ${auth.key}`) headers.set("x-acme-client", "opencode-acme-auth/0.1.0") return fetch(input, { ...init, headers }) }, } }, // magpie 自己的钩子:套餐用了多少 async usage(getAuth) { const auth = await getAuth() const res = await fetch(`${BASE}/usage`, { headers: { authorization: `Bearer ${auth.key}` } }) if (!res.ok) return { error: `Acme answered ${res.status}`, windows: [] } const u = await res.json() return { plan: u.plan, windows: [{ name: "5 hours", used: u.percent, resetsAt: u.resets_at, span: 5 * 3600 }], } }, }, // 向 Acme 查询这个账号有哪些模型 provider: { id: PROVIDER, async models(provider, { auth } = {}) { if (auth?.type !== "api") return provider.models const res = await fetch(`${BASE}/models`, { headers: { authorization: `Bearer ${auth.key}` } }) if (!res.ok) return provider.models // magpie 保留原来的列表 const { data } = await res.json() const base = provider.models["acme-coder"] return Object.fromEntries(data.map((m) => [m.id, { ...base, id: m.id, name: m.name ?? m.id }])) }, }, })
把文件夹加到 magpie,登录,然后试一下:
magpie plugin add ./opencode-acme-auth # 文件夹就地加载,不安装任何东西 magpie plugin login acme # 询问 key magpie plugin # acme (Acme, 2 models) signed in magpie provider test acme # ✓ chat 7 ms · acme-coder magpie quota # acme · Pro · 5h 37% ↻59m
现在 agent 可以用 acme/acme-coder 访问它的模型,和其他供应商的模型一样。在应用里,打开插件,点添加插件,填入文件夹路径,再在这个供应商那一行登录。
HOME,做法见测试。magpie 如何运行插件
- Bun。第一次添加插件时,magpie 会下载 Bun,所有插件都跑在它上面。Bun 能直接运行 TypeScript,所以入口可以是
index.ts。 - 一个宿主进程。所有开着的插件都加载进同一个 Bun 进程,第一次用到时启动。magpie 和宿主通过 stdin、stdout 通信,每行一条 JSON 消息。插件打印的任何内容,包括
console.log和process.stdout,都会被转到 stderr,magpie 以plugin: …记进日志。 - 安装。
- npm 包用
bun add安装,不运行安装脚本,和 OpenCode 安装插件的方式一样。依赖postinstall构建的包无法使用。 - git 仓库由 Bun 拉取:
github:owner/repo[#ref]、git+https://…或owner/repo。 - 文件夹或文件的路径就地加载。
- npm 包用
- 重启。添加、移除、更新、开关插件都会重启宿主。还在通过旧宿主流式返回的回答会先完成。终端里每条
magpie命令都启动自己的宿主,所以文件夹插件总是按当前内容加载。在应用里,把插件关掉再打开即可加载你的修改。 - 代理。插件为某个供应商或账号发出的每个 fetch,都走用户给这个供应商或账号设置的代理;没有设置就走 magpie 自己的代理,除非设成直连。回环地址从不走代理。自己传了
proxy选项的 fetch 不会被改动。 - 更新。
- 社区的包(
@magpie-community/*)在 magpie 启动后不久自动更新,之后每六小时一次。 - 其他包有新版本时,插件上会出现一个小圆点,用户点一下或运行
magpie plugin update更新。 - 固定了版本的写法(
name@1.2.3)会停在那个版本。
- 社区的包(
magpie 在它的配置目录(~/.config/magpie 或 $XDG_CONFIG_HOME/magpie)里保存这些:
| 文件 | 内容 |
|---|---|
plugins.json | 已添加的插件:{"plugins": [{"spec", "off"?, "options"?}], "config"?}。options 是插件的第二个参数,和 OpenCode 的 ["name", {…}] 一样。config 是交给插件的 OpenCode 配置。 |
plugins/ | 已安装的 npm 和 git 包,是一个独立的 Bun 项目。 |
plugin-auth.json | 登录信息(权限 600),格式和 OpenCode 的 auth.json 相同,所以登录可以在两者之间迁移。 |
plugin-providers.json | 插件上次列出的供应商和模型,magpie 启动时不必再问插件。 |
包的结构
magpie 从哪里加载
magpie 查找插件服务端入口的位置和 OpenCode 一样,加载第一个导出了插件的文件:
package.json里的exports["./server"],然后main,然后exports["."](exports是字符串时就用它)。- 否则是
index.ts、index.tsx、index.js、index.mjs或index.cjs。
路径指向单个文件时,就加载那个文件。
模块导出什么
export default { id, server }(OpenCode v1):server就是插件。- 否则,模块导出的每个函数都会被当作插件调用,导出的
{ server }对象也一样。
所以不要从入口文件导出辅助函数。测试需要的东西放在一个对象上,社区插件的做法是 export const _internal = { … }。
插件函数
async (input, options) => hooks // input: { client, project, directory, worktree, serverUrl, $, experimental_workspace } // options: plugins.json 中这一项的 "options",没有则为 undefined // hooks: { config?, auth?, provider?, "chat.headers"? }
导出多个函数,一个插件就能提供多个供应商,每个函数的钩子互不相干。两个插件为同一个供应商提供了 auth 钩子时,后加载的生效,和 OpenCode 一样。
package.json 里 magpie 的字段
OpenCode 会忽略 magpie 对象,所以给 OpenCode 插件加上它是安全的。
| 字段 | 含义 |
|---|---|
magpie.icon | 供应商的图标:https:// 地址或 data:image/… URI,最大 1 MB,SVG、PNG 等都行。 |
magpie.maxConcurrency | 每个账号同时接多少个请求,1 到 1000 的整数。用户在供应商上设置的上限优先。 |
同样的字段也可以写在 auth 钩子上(auth.icon、auth.maxConcurrency),优先于 package.json。
供应商与模型
声明供应商
供应商的 id 就是 auth.provider。如果 models.dev 收录了这个 id,magpie 会以那里的条目为起点,和 OpenCode 一样。否则就在 config 钩子里声明供应商。用 ??=,以免覆盖用户在 plugins.json 里配置的供应商:
config: async (cfg) => {
cfg.provider ??= {}
cfg.provider["acme"] ??= {
name: "Acme",
npm: "@ai-sdk/anthropic", // 模型使用的 API(见下表)
api: "https://api.acme.example/v1", // 请求发往哪里,除非 loader 另有指定
models: {
"acme-large": {
name: "Acme Large",
id: "acme-large-2026-09", // API 接受的 id,和键不同时填
limit: { context: 400000, output: 64000 },
reasoning: true,
tool_call: true,
modalities: { input: ["text", "image"] },
cost: { input: 3, output: 15, cache_read: 0.3 }, // 每百万 token 的美元价格;套餐可不填
variants: { low: {}, medium: {}, high: {} }, // 推理档位
},
},
}
}
模型还可以写 provider: { npm, api },使用和供应商不同的 API 或地址;也可以写 headers、options,以及用 disabled: true 隐藏它。
模型使用的 API
模型或供应商上 npm 指定的 AI SDK 包决定了 API,也就决定了你的 fetch 收到的请求格式。无论 agent 发来的是什么,magpie 的网关都已经转换好了:
| npm | API | 基地址后的路径 |
|---|---|---|
@ai-sdk/anthropic、@ai-sdk/google-vertex/anthropic | Anthropic Messages | /messages |
@ai-sdk/openai、@ai-sdk/azure | OpenAI Responses | /responses |
@ai-sdk/google | Gemini | /models/<id>:streamGenerateContent?alt=sse |
@ai-sdk/github-copilot | GPT-5 及之后(gpt-5-mini 除外)用 Responses,其余用 chat completions | /responses 或 /chat/completions |
其他(@ai-sdk/openai-compatible、xai、groq、deepseek 等) | Chat completions | /chat/completions |
基地址按这个顺序选:
- loader 的
baseURL; - 模型的
provider.api; - 供应商的
api; - AI SDK 包自己的默认地址(
https://api.anthropic.com/v1、https://api.openai.com/v1等)。
请求体里的 model 是模型的 API id:有 id 就用它,否则用键名。
每个账号的模型
模型因账号而异时,加一个 provider 钩子:
provider: {
id: "acme",
async models(provider, { auth }) {
// provider.models:models.dev 和 config 钩子给出的模型,以 id 为键
// auth:这个账号的登录信息,和 plugin-auth.json 中保存的一样
return { "acme-large": { ...provider.models["acme-large"], name: "Acme Large" } }
},
}
- magpie 为每个账号调用一次,在这个账号的作用域里、经过它的代理。每个账号只承接为它列出的模型,所以低档套餐的账号不会收到它没有的模型。
- 模型是 OpenCode 的
Model:id、name;api: {id, url, npm};limit: {context, input?, output};capabilities: {reasoning, toolcall, attachment, input: {text, image, …}};cost: {input, output, cache: {read, write}};variants、release_date、status。
provider.models里的模型,再改几个字段。status为"deprecated"的模型不会显示。 - magpie 还会读取模型上的这些字段,OpenCode 会忽略它们:
free: true:套餐免费提供,不占额度。rate:一次请求消耗的点数倍率,可以是数字(0.5),也可以照厂商的写法("x0.5")。rateWas:当前优惠之前的倍率。
- 模型支持推理时,
variants的键会成为它的推理档位。
连不上厂商时
钩子出现以下任一情况,magpie 都会保留原有的列表,和内置订阅拉取列表失败时一样:
- 请求失败后原样返回
provider.models; - 抛出异常;
- 返回自己的列表,并设置
list[Symbol.for("magpie.fellBack")] = true。
如果失败的原因是登录失效,就抛出带 signIn: "expired" 的错误,这个账号会被标记为需要重新登录(见登录状态)。
登录
auth 钩子的形式是 { provider, methods, loader, usage?, refresh?, refreshLead?, icon?, maxConcurrency? }。每个 method 是用户可选的一种登录方式。在应用里它们是按钮;在终端里,magpie plugin login <id> [<method>] 会询问选哪个。
API key
{ type: "api", label: "Acme API key", placeholder: "acme-…" }
label是 key 输入框的标题。只写“API key”的会显示成“Acme API key”。placeholder是 magpie 自己的字段,显示在输入框里作提示。- 没有
authorize函数时,key 原样保存:{type: "api", key, metadata?}。prompts的回答放在metadata里。 - 有
authorize(inputs)时,由插件自己校验或换取 key,返回{type: "success", key, metadata?}或{type: "failed", error?}。
浏览器或设备码登录
{
type: "oauth",
label: "Sign in with Acme",
async authorize(inputs) {
const d = await startDeviceCode()
return {
url: d.verification_uri, // magpie 打开它
instructions: `Enter the code ${d.user_code}`, // 并显示这段说明
method: "auto", // "auto":callback() 自己完成
async callback() { // "code":callback(code),用户粘贴的码
const t = await pollForToken(d)
if (!t) return { type: "failed", error: "The code expired" }
return { type: "success", refresh: t.refresh_token, access: t.access_token,
expires: Date.now() + t.expires_in * 1000, accountId: t.email }
},
}
},
}
- 带
refresh的成功结果保存为{type: "oauth", refresh, access, expires, …},你返回的其他字段(accountId、enterpriseUrl、你自己的字段)都一并保存。 - 带
key的成功结果保存为 API key。 - 结果里写
provider: "other-id",登录就保存到插件负责的另一个供应商下。 - 失败结果里的
error是 magpie 自己的字段:用户会看到原因(最多 500 字符)。不写的话,用户只能看到登录失败。 - 只有带
prompts的方式才会收到inputs。没有 prompts 的方式收到undefined,和 OpenCode 的 TUI 一样。OpenCode 的 CLI 传的是空对象,有些插件因此会在终端里自己提问。
登录前的提问
method 的 prompts 在 authorize 之前依次提问,每一项只在条件成立时出现:
prompts: [
{ type: "select", key: "region", message: "Region",
options: [{ label: "Global", value: "global" }, { label: "China", value: "cn", hint: "acme.cn" }] },
{ type: "text", key: "team", message: "Team id", placeholder: "t-…",
when: { key: "region", op: "eq", value: "global" }, // 或 condition: (inputs) => boolean
validate: (v) => (/^t-/.test(v) ? undefined : "Starts with t-") },
]
账号
- 保存位置。一个供应商可以登录多次。第一个账号保存在供应商 id 下,和 OpenCode 一样;其他账号保存在
id#xxxxxx下。 - 用户怎么区分它们。magpie 依次用
accountId、metadata.email、email给账号命名。至少保存其中一个,能拿到邮箱就用邮箱。 - 重新登录。新的登录和已有账号
accountId相同时,会替换那个账号。如果两边都有uid(厂商自己的 id,用于名字可能重复的厂商),它也必须相同。没有 id 时,比较密钥是否相同。都不匹配就成为新账号。 - 作用域。magpie 为某个账号发起的每次调用,都在这个账号的作用域里运行:
getAuth()返回这个账号。client.auth.set({ path: { id: "acme" }, body })保存到这个账号。
- 刷新。提供
auth.refresh,magpie 就会替你续期(见续期登录)。否则 token 过期时,在fetch里刷新,再用client.auth.set保存完整对象。你给的内容会替换原有内容,不做合并。
续期登录
从 magpie 0.1.684 起,插件可以用 auth.refresh 把 token 续期交给 magpie。这是 magpie 自己的钩子(OpenCode 会忽略它):
auth: {
provider: "acme",
methods: [ … ],
// 可选:在 expires 之前多久续期(毫秒),默认 5 分钟
refreshLead: 5 * 60 * 1000,
async refresh(auth, provider) { // auth 是这个账号保存的登录信息
const t = await exchange(auth.refresh)
if (t.error === "invalid_grant")
throw Object.assign(new Error("Acme 拒绝了登录"), { signIn: "expired" })
return { access: t.access_token, refresh: t.refresh_token ?? auth.refresh,
expires: Date.now() + t.expires_in * 1000 }
},
loader: async (getAuth) => ({ … }), // getAuth() 拿到的已经是续期后的 token
}
- 什么时候运行。OAuth 账号的
expires距现在不到refreshLead时,在这个账号的 loader、provider.models、auth.usage或请求之前运行。expires为 0 或没有的账号不会续期。 - 同一时间只续一次。发现 token 快过期的请求会一起等同一次续期,所以只允许 refresh token 用一次的厂商不会收到两次。最多等 15 秒;超时的续期在完成时照样保存。
- 保存什么。你返回的字段会覆盖账号原有的同名字段(会合并,这点和
client.auth.set不同),账号的标记也会清除。没什么可续的就什么都不返回。 - 失败时。账号保持原样,请求带着旧 token 继续发,由厂商的回答来说明结果。带
signIn: "expired"的错误会标记账号(见登录状态),重新登录之前 magpie 不再尝试。其他错误 30 秒后再试。 - OpenCode 和旧版 magpie。它们会忽略这个钩子。如果插件也要在那里运行,
fetch里的检查请保留:在 magpie 下它会发现 token 是新的,什么也不做。
请求
loader(getAuth, provider) 每个账号运行一次,账号登录信息变化后会重新运行。它返回 OpenCode 会交给 AI SDK 的内容:
| 字段 | magpie 怎么用 |
|---|---|
baseURL | 请求发往的地址,优先于模型和供应商的 api。 |
apiKey | 按 AI SDK 包的方式发送:Anthropic 用 x-api-key,Google 用 x-goog-api-key,其他用 Authorization: Bearer。API key 登录时默认就是保存的 key。 |
headers | 加到每个请求上。 |
fetch | 发出请求。不提供时,magpie 的宿主用标准 fetch。 |
你的 fetch(input, init) 收到的请求:
input是完整 URL:基地址加上模型 API 对应的路径。init.method通常是POST。init.headers按下面的顺序设置,后面的覆盖前面的:apiKey对应的 key 头;- loader 的
headers; - 网关请求的头;
chat.headers钩子加的头。
init.body是这种 API 格式的 JSON 字符串(不是 UTF-8 文本时为字节)。其中的model是 API id,stream照 agent 的要求。init.signal在 agent 放弃请求时中止。
返回一个 Response。它的状态码、头和正文原样传回,正文逐块流式传输。如果你改写了流式正文,要去掉头里的 content-length 和 content-encoding。
失败与故障切换
- 非 2xx 的响应就是厂商的回答,网关会像对待其他供应商一样处理:限流或服务端错误会把请求转到下一个账号或模型,401 会标记账号(见登录状态)。
- 抛出异常会让请求失败,然后故障切换继续。
- 尽量用该 API 自己的错误格式回答,这样 agent 会显示厂商的原话。
chat.headers
OpenCode 的 chat.headers 钩子对每个请求都会运行。它的 input.sessionID 是 magpie 的会话 id,在同一段对话的各轮之间保持不变。可以用它填厂商的会话头或缓存头:
"chat.headers": async (input, output) => { if (input.model.providerID === "acme") output.headers["x-acme-session"] = input.sessionID }
登录状态
厂商拒绝了某个账号的登录时,magpie 会给它打上标记,告诉用户,并且在它恢复之前不再往它发请求。默认情况下,401 打标记,成功清除标记。插件可以用 X-Magpie-Sign-In 响应头更准确地说明一个回答的含义,magpie 会在转发前去掉这个头:
| 值 | 含义 |
|---|---|
expired | 登录被拒绝,无论状态码是什么(有些厂商为此返回 403 或 502)。 |
kept | 账号保持原样(这个 401 说的是别的问题)。 |
renewed | 登录已续期:清除标记,无论请求接下来遇到什么。 |
在 fetch 或 provider.models 里,抛出带 signIn: "expired" 的错误效果相同:
throw Object.assign(new Error("Acme refused the sign-in; sign in again"), { signIn: "expired" })
用量
auth.usage(getAuth, provider) 是 magpie 自己的钩子,说明一个账号的额度用了多少。它在账号的作用域里运行,所以它刷新的 token 会保存到这个账号。magpie 把结果显示在供应商里的账号上、magpie quota 里,以及网关的 GET /v1/magpie/quotas。所有字段都可选:
{
plan: "Pro",
user: "ada@example.com", // 厂商那边的账号名
until: "2026-11-01T00:00:00Z", // 套餐到期时间
renew: "auto", // 或 "off"
balance: "$12.40",
error: "", // 显示在额度窗口的位置
windows: [{
name: "5 hours",
used: 37, // 百分比;超用时大于 100
resetsAt: "2026-10-03T18:00:00Z", // 或 resetSecs: 3600
span: 18000, // 窗口长度,单位秒
display: "370 / 1000 credits",
model: "opus", // 只统计 id 含这个词的模型
models: [], notModels: [], // 或恰好这些 id,或除这些外的全部
aside: false, // true:用完也不影响账号使用
}],
resets: { count: 2, until: "…", byWindow: false, fiveHour: 1, weekly: 1 }, // 账号可用的重置次数
signIn: "kept", // 同 X-Magpie-Sign-In
}
时间可以是 ISO 字符串,也可以是以秒或毫秒为单位的时间戳。没有 signIn 时,提示要重新登录的 error 会标记账号,一次正常读取会清除标记。
插件能拿到什么
magpie 交给插件的是 OpenCode PluginInput 里的内容,只要它在没有 OpenCode 服务端时还有意义:
| 字段 | 在 magpie 里 |
|---|---|
client.auth.set / .remove | 保存或删除当前作用域账号的登录信息。 |
client.app.log | 写入 magpie 的日志,格式为 plugin [level]: [service] message。 |
client.tui.showToast | 写入 magpie 的日志。 |
client.config.get | 所有插件的 config 钩子运行之后的配置。 |
其他 client.* | 返回 { data: undefined }。 |
directory、worktree | magpie 的配置目录,可以在这里存文件。 |
project | { id: "magpie", worktree }。 |
$ | Bun 的 shell。 |
serverUrl | 占位值,后面没有 OpenCode 服务端。 |
magpie 调用的钩子是 config、auth(它的 methods、loader 和 usage)、provider.models 和 chat.headers。插件的其他钩子(tool、event、chat.params 等)不会被调用,工具由 agent 自己运行。
供应商 id
- agent 用
<id>/<model>指代插件的模型,id 就是插件的auth.provider。 - 这个 id 在 magpie 里已被占用时,比如内置订阅、预设(
google、openai、anthropic等)或magpie本身,插件的供应商会叫<id>-plugin。请选一个只属于你的 id。 - 社区为 magpie 已弃用的内置订阅做的插件是例外。它们用内置订阅的 id(
cursor、zed等),用户迁移到插件后就接管这个 id,agent 无需任何改动。
测试
在沙箱里运行 magpie,这样你自己的 magpie、agent 和登录信息都不会被碰到:
sb=$(mktemp -d)
m() { env HOME=$sb XDG_CONFIG_HOME=$sb/.config XDG_CACHE_HOME=$sb/.cache MAGPIE_ADDR=127.0.0.1:3499 magpie "$@"; }
m plugin add ./opencode-acme-auth </dev/null
m plugin --json # 插件列出的内容:方式、账号、模型、加载错误
m plugin login acme # 每种方式、每个提问
m provider test acme # 每种 API 发一个很小的请求
m provider test acme acme-coder # 或只测指定的模型
m quota # usage 钩子
m plugin logout acme
- 加载失败的插件,在
magpie plugin里会说明原因。 - 钩子的错误和插件打印的内容都在输出里,以
plugin: …开头。 - 单元测试用
bun test,测试你放在_internal上的函数。 - 社区仓库里有几个脚本:
bun scripts/check.mjs像 magpie 一样加载每个包,并检查它的钩子。scripts/try.sh <name>在上面那样的沙箱里运行它。
发布与上架
- npm。照常发布。用户可以按名字添加(
magpie plugin add opencode-acme-auth),按版本(…@0.2.0),从 git,或从文件夹。 - 搜索。插件里的搜索会向 npm 查询
<关键词> opencode,显示名字或 keywords 里同时含有opencode和auth、plugin、provider之一的包。keywords 写成["opencode", "opencode-plugin", "<厂商>"]就可以。它还会查询keywords:pi-package <关键词>,显示名字或 keywords 里提到auth或provider的 pi 扩展包。 - README。插件在 magpie 里的详情页显示它在 npm 上的 README。写清楚它登录什么、怎么登录、登录信息存在哪里、有哪些模型。
- 推荐列表。插件页推荐的是社区仓库
registry.json里的插件。magpie 最多每六小时拉取一次,并内置一份副本,拉不到时使用。想被收录,就提一个 PR 加上一项:registry.json{ "package": "opencode-acme-auth", "name": "Acme", "icon": "https://acme.example/icon.svg", "providers": ["acme"], "summary": { "en": "Your Acme Coding Plan.", "zh": "使用你的 Acme Coding Plan。" } }MAGPIE_PLUGIN_MARKET=<url>让 magpie 改用另一份列表,可以用来检查你的条目;设为off则只用内置副本。 - 社区包。想把插件贡献到 magpie-community/plugins:
- 放在
packages/<name>下,包名为@magpie-community/opencode-<name>-auth,除非确实需要,不要有运行时依赖。 - 把版本号的改动推到
main就会发布。 - 这些包在每个 magpie 里都会自动更新。
- 放在
pi 扩展包
magpie 也运行 pi 的扩展包。用 pi.registerProvider 注册了供应商的 pi 扩展,在 magpie 里和在 pi 里一样工作,不用改任何东西,添加方式和其他插件相同(magpie plugin add pi-antigravity)。
- 怎样算 pi 的包。
package.json里有pi清单("pi": { "extensions": [...] })、有pi-package关键词,或者dependencies、peerDependencies里有@earendil-works/pi-coding-agent(或旧的@mariozechner/pi-coding-agent)。单个文件导入了 pi 也算。 - 加载。magpie 在包旁边装好 pi,用 pi 自己的加载器在同一个插件宿主里加载它。它注册的供应商成为 magpie 的供应商,模型就是 pi 为它们列出的那些(pi 能拉取账号自己的列表时用那份)。它的命令、工具、渲染器和事件处理器都不会用到。
- 登录。OAuth 登录按 pi 的方式进行:它的提问(
onPrompt)在 magpie 的登录流程里问,它的页面(onAuth)在浏览器里打开,refreshToken负责续期。扩展包用ctx.ui弹出的对话框(select、confirm、input、editor)和交给ctx.ui.custom的终端组件,也都变成 magpie 的提问。要 API key 的供应商会有一个填 key 的输入框。每次登录就是一个账号,所以 pi 的供应商也能有多个账号。 - pi 的文件。pi 的设置、它的
auth.json(按 pi 的方式保存每个供应商的第一个账号)以及扩展包自己写的文件,都放在 magpie 配置目录下的pi/里,不会动用户的~/.pi,除非设置了PI_CODING_AGENT_DIR。账号本身和其他插件一样存在plugin-auth.json里。 - 请求。pi 供应商的模型接收 Anthropic Messages:网关把 agent 发来的请求翻译过去,magpie 通过 pi 流式发送,回复里的思考、签名和工具调用都会保留。厂商的错误会带着状态码传回,所以故障切换和其他插件一样有效。
- id。pi 供应商的 id 和其他插件规则相同,见供应商 id。
宿主协议
写插件用不到这一节,它是给开发 magpie 本身或调试宿主的人看的。magpie 向宿主的 stdin 写入 {id, method, params},每行一条。宿主回复 {id, result} 或 {id, error: {message}}。
| 方法 | 作用 |
|---|---|
init | 加载插件,运行它们的 config 钩子。其他调用都会等它完成。 |
providers | 每个供应商、它的登录方式、账号,以及每个账号的模型。 |
prompt、validate | 登录方式的下一个提问;校验一个回答。 |
authorize、callback、apiKey | 登录。 |
load | 某个账号的 loader 选项。 |
fetch | 一个请求。先流式发出 {id, event: "head", status, headers},然后是 {id, event: "chunk", data}(base64),最后 {id, result: null}。{method: "abort", params: {id}} 取消它。 |
usage、check | 账号的用量;像真实请求一样检查一个账号。 |
import、take、signOut、reload | 账号的迁入迁出、删除,以及重新运行它们的 loader。 |
宿主也会主动发送事件:log、toast、auth(登录信息已保存或刷新)和 signIn(账号的登录状态,同上)。源码见 internal/plugin/host.js。
答疑
我的 OpenCode 插件不改就能在 magpie 里用吗?
可以,只要它通过 auth、config、provider 和 chat.headers 来登录和发请求。magpie 增加的部分(auth.usage、auth.refresh、magpie.icon、maxConcurrency、free、rate、X-Magpie-Sign-In)OpenCode 都会忽略,所以一个包能同时服务两边。
我的 pi 扩展包能在 magpie 里用吗?
能,只要它通过 pi.registerProvider 登录并提供模型。magpie 只使用它注册的供应商,见 pi 扩展包。给它加上 pi-package 关键词,搜索就能找到它。
为什么我的供应商叫 …-plugin?
它的 id 在 magpie 里已经有了。见供应商 id。
我的一些辅助函数被当成插件运行了。
入口模块导出的每个函数都会被当作插件调用。只导出插件函数,或者用 export default { id, server }。
可以用 npm 依赖吗?
可以。它们会随插件一起安装,但安装脚本不会运行。Bun 的 fetch、crypto、fs 和 $ 能满足大部分需要。
插件可以调用厂商的 CLI 吗?
可以,用 Bun 的 $ 或 child_process。好几个社区插件就是通过厂商自己的 CLI 登录的。要考虑到有些机器上没装它,在错误信息里说明怎么安装。
插件能看到用户的哪些内容?
它发出的请求里的一切:对话、工具及其结果。只把它发到用户要求的地方,也不要记进日志。
怎么让 magpie 看到我的修改?
终端里的 magpie 命令每次都按文件夹的当前内容加载。在应用里,到插件把它关掉再打开。
有问题,或者有插件想分享?来 Discord 问,或在 magpie-community/plugins 开 issue。