跳转到内容
↑↓导航 ↵打开 esc关闭

Host API

当你需要准确了解 connectHost 的方法、限制和错误码时,使用本页。关于文件夹结构、清单和安装,请从构建扩展开始。

import { connectHost, HostRequestError } from "@openchamber/sdk";
const host = connectHost();

没有 window 时,connectHost 抛出 HOST_UNAVAILABLE。在 OpenChamber 之外,它返回一个客户端,其每次调用都以 HOST_UNAVAILABLE 拒绝。卸载页面时调用 dispose();仍在进行中的调用会以同一错误码拒绝。

OpenChamber 推送给你的内容

onReady 在第一份快照到达时触发,之后每当 OpenChamber 刷新快照时再次触发。

它不是只执行一次的挂载回调。UI 挂载和订阅注册只做一次,之后更新主题和上下文,不要替换输入框或草稿。参见 UI kit 中的防重复挂载示例。onConnection 等监听器会立即提供当前值,并可能在新快照中再次提供相同值。发起新请求前比较相关字段,并忽略已被新请求替代的旧响应。

字段含义
theme.modelight 或 dark
theme.tokens应用的颜色(表面、文本、交互状态、primary、success/warning/error/info),以及 font、mono 和 radius。在挂载 UI 之前把它传给 applyHostReady。
locale应用的语言标签
directory当前项目目录,或 null
session{ id, title, busy } 或 null。没有标题时回退为会话 id。busy 是实时状态。会话有 model 和 agent(OpenCode 的 agent)时也会出现。
surface侧栏为 panel,附加窗口为 dialog,全屏为 page,Work Status 面板中为 status,Files 中的文件编辑器为 file,聊天的 UI 块中为 block
item此界面因何而打开,或 null:用户点击的已附加条目(与你传给 attach 的字段相同,包括 data)、来自你声明的某个操作的消息(kind: "message")或会话(kind: "session")。见操作。
connection你的集成的 { connected, account }
settings你在 integration.settings 中声明的字段的值

访问令牌永远不会出现在这里,也不会出现在 request 的结果中。

onDirectory、onSession、onSessionLifecycle、onConnection、onSettings 和 onItem 在你较晚订阅时会重放最新值,之后随变化持续触发。

theme.tokens 包含 primaryText、successText、warningText、errorText 和 infoText。这些必填字段提供宿主计算的文字颜色,用于中性背景和 UI kit 的浅色控件,而非实心彩色填充。applyHostReady 会在每次快照中应用这些值。CSS 变量见 UI kit。

方法

调用作用
toast({ kind, message })在应用中显示提示消息。kind 为 info、success 或 error。
openUrl(url)在用户的浏览器中打开 URL
openSurface(surfaceId)把应用切换到该界面
writeClipboard(text)复制文本。1 到 32000 个字符。
compose({ text, mode? })把文本放进聊天框但不发送。mode 为 append(默认)或 replace。去除首尾空白后 1 到 16000 个字符。
attach({ ... })在聊天框上放一个标签,和 GitHub、Linear 条目落在同一位置。同一时间只能有一个标签。
startSession({ ... })创建一个附加了该条目的会话。返回 { sessionId, sent }。需要 sessions 能力。
prompt({ text, send? })写入当前会话,或在当前会话中发送。返回 { sent }。发送需要 prompt 能力。
sessionLink({ ... })把条目附加到当前会话,而不创建新会话
close()关闭附加窗口。在侧栏中无效果。
oauthStart()打开提供方的授权页面;对于 host: { provider: "linear" } 集成,打开 Linear 的授权页面
oauthDisconnect()忘记已存储的令牌,或断开 Linear 连接
request({ method, path, query?, body? })调用你的集成的 apiOrigin,并附上用户的令牌
serviceRequest({ method, path, query?, body? })调用你的扩展的本地服务(见 GUEST_SERVICES.md)
serviceStatus()stopped、starting、ready 或 failed
readFile(path)读取文本文件。返回 { content }。
writeFile(path, content)原子地写入文本文件,并创建父文件夹。返回 { written: true }。
listDir(path)列出文件夹。返回 { entries: [{ name, kind }] },kind 为 file、directory 或 other。
stat(path){ kind, size, mtime },kind 为 file、directory、other 或 missing。
generate({ prompt, system?, maxOutputTokens? })来自用户 Small Model 的一次性文本。返回 { text }。需要 model 能力。
onResolve(handler)注册你的斜杠命令处理函数。它收到 { command, args },返回一个要附加的条目或 null。
setBadge(count)在你的侧栏图标上显示一个圆点,并在提示中显示数字(0 到 999),或传 null 清除
openCommit(sha)在当前项目的该提交上打开 Diff 视图。sha 为 7 到 64 个十六进制字符;OpenChamber 自己读取提交。
setHeight(px)设置你的 Work Status 分区的高度。OpenChamber 会把它限制在 24 到 320 像素之间;更高的内容在你的页面内滚动。其他位置会忽略它。
onFileOpen(listener)文件编辑器:获取要编辑的文件,{ path, name, readOnly },外加 encoding: "text" 与 content,或 encoding: "binary" 与 bytes。同时让你页面中的 Cmd/Ctrl+S 能够保存。
onFileSnapshot(handler)文件编辑器:返回 { content, version }(二进制编辑器则返回 { bytes, version }),即编辑后的完整文件。OpenChamber 在保存时以及切换到源码视图或全屏之前调用它。
onFileSaved(listener)文件编辑器:带该 version 的快照已写入磁盘。用 reportFileChange 回应。
reportFileChange({ dirty, edited })文件编辑器:告诉 OpenChamber 是否有未保存的更改,以及文档是否刚刚发生变化
requestFileSave()文件编辑器:立即保存,与 Cmd/Ctrl+S 相同
reportFileUnsupported()文件编辑器:此文件无法在这里打开,改为显示其源码

三者接受相同的条目字段:

await host.attach({
providerId: "acme-hello",
id: "TICKET-1",
title: "Login is broken",
url: "https://example.com/TICKET-1",
});
await host.attach({
providerId: "acme-hello",
id: "!12",
title: "Fix login",
url: "https://example.com/merge_requests/12",
kind: "pull",
author: "ada",
branches: { head: "feature", base: "main" },
text: "Optional notes for the model",
});
await host.startSession({
providerId: "acme-hello",
id: "!12",
title: "Fix login",
url: "https://example.com/merge_requests/12",
kind: "pull",
worktree: true,
text: "Optional first message",
});

providerId 是你的面板 id;无论如何 OpenChamber 都会用你的 id 覆盖它。id 是你自己为条目定义的标识符。kind 为 issue(默认)或 pull。text 是给模型的可选上下文,去除首尾空白后 1 到 16000 个字符。data 是你自定义的可选 JSON(状态、评论,任何内容),序列化后最多 16000 个字符。OpenChamber 会把它和标签一起保存,并在用户点击标签时通过 ctx.item 原样交还;它永远不会到达模型。

startSession 接受 projectId,无需切换当前项目。省略 worktree 表示目标目录,true 创建自动命名的工作树,{ kind: "existing", directory } 选择已知工作树,{ kind: "new", name?, baseBranch? } 指定新工作树名称和基准分支。名称同时用于分支。navigation 默认为 "preserve";"open" 打开新聊天。首条消息的模型、代理和变体在创建开始时确定。

结果包含 sessionId、directory、sent、linked 及可选 worktree。sent 为 sent、no-model、skipped 或 failed;linked: false 表示条目保存失败。准备或会话创建失败后若保留了工作树,返回 sessionId: null 和 failure: "bootstrap-failed" | "session-create-failed"。重试前请检查结果。最多等待180秒;超时不代表已回滚创建。

sessionLink 把条目附加到当前打开的会话。没有项目或没有会话时是 NO_SESSION 错误。

客户端在发送任何内容之前应用的限制:

字段最大字符数
id128
title200
url2000
text16000
data(序列化后)16000
author80
每个分支名200

prompt 与会话生命周期

await host.prompt({ text: "Fix the login" });
await host.prompt({ text: "Fix the login", send: true });
host.onSessionLifecycle((event) => {
document.body.dataset.phase = event.phase;
});

不带 send 时,prompt 会替换聊天框中的文本。带 send: true 时,它会用用户已选择的模型和 agent 发送消息。扩展永远不会自己选择它们。没有打开的会话时是 NO_SESSION。会话忙碌时发送是 SESSION_BUSY;忙碌时写入聊天框仍然可以。结果是 { sent },取值与 startSession 相同。

onSessionLifecycle 告诉你会话正在做什么。phase 在模型工作时为 started,在会话转为空闲时为 completed,出现意外状态时为 failure。较晚注册的监听器会立即收到当前阶段。

项目、实时会话和存储

listProjects()、listWorktrees(projectId)、listSessions(projectId) 使用已有 sessions 权限读取共享状态,不会每次遍历 Git,也不暴露对话内容。项目包含 ID、名称和目录;工作树包含分支和可用状态。会话包含时间戳、父会话、工作树,以及仅属于此扩展的附加条目。包括已知的归档会话。

await onProjects(listener)、await onWorktrees(projectId, listener)、await onSessions(projectId, listener) 返回取消订阅函数。请处理注册失败。先发送初始快照,再发送更新。每个 iframe 最多32个订阅;dispose()、关闭、暂停、移除和切换服务器都会清理订阅。state 为 loading、ready 或 error,会话另有按目录的 coverage。失败保留已有数据,只有 ready 表示完整结果,包括真正的空列表。

activity 为 unknown、idle、running、retrying、waiting-permission 或 waiting-question。outcome 为观测到的 completed、failed 或 null,仅在内存中保留最多2,000个会话,不从历史推断。错误后的 idle 保留失败结果直到下次运行。这不代表任务完成。openSession(sessionId) 显式打开聊天。

host.storage.get(key)、set(key, value)、delete(key)、keys() 无需额外权限即可在连接的服务器保存扩展自己的 JSON。缺少的键返回 undefined,保存的 null 仍为 null。键长1到128字符,每个值最多64 KiB UTF-8,整个空间最多2 MiB及2,000个键。项目专属数据可将项目 ID 加入键名。写入串行且原子化,失败保留已有数据。卸载扩展会删除存储。

request

const user = await host.request({ method: "GET", path: "/api/v2/user" });

path 以 / 开头,不含协议和主机名;OpenChamber 会把它拼接到清单中的 apiOrigin 之后,并添加 Authorization 头。令牌集成会原样发送粘贴的令牌;当清单设置了 token.scheme: "bearer" 时,发送为 Bearer <token>;设置为 "basic" 时,发送为 Basic base64(username:token)。OAuth 和 Linear 集成始终发送 Bearer。结果是 { status, body }。body 是文本,JSON 需要你自己解析。

Linear 集成还可以 GET /api/linear/issues/get,OpenChamber 会用自己的 Linear 路由来响应。

20 秒内没有响应是 HOST_TIMEOUT。

try {
await host.request({ method: "GET", path: "/api/v2/user" });
} catch (error) {
if (error instanceof HostRequestError && error.code === "DISCONNECTED") {
await host.oauthStart();
}
}

serviceRequest

当清单声明了 contributes.service 时,OpenChamber 会启动该进程,你的页面通过同样类型的调用与它交互。页面永远不会自己打开 socket。

const status = await host.serviceStatus();
const result = await host.serviceRequest({ method: "GET", path: "/containers" });

path 遵循与 request 相同的规则。声明本地服务会把 service 加入用户在安装时批准的能力;在批准之前,每次 serviceRequest 都是 NO_SERVICE。完整约定见包内文件 GUEST_SERVICES.md。

会话事件

你的页面只在会话打开时知道它的存在。你的服务可以知道更多。在 contributes.service 旁边声明 contributes.sessionEvents,OpenChamber 就会把你列出的事件从每个项目发送给服务,无论是否打开了窗口。

"service": { "entry": "service/main.js", "runtime": "host" },
"sessionEvents": { "events": ["turn-finished", "question-asked"], "content": true }

事件有 turn-started、session-created、session-archived、session-deleted、turn-finished、turn-failed、permission-asked 和 question-asked。turn-finished 表示智能体已经完成并且不再等待任何东西,没有运行中的子智能体,没有未答复的权限或问题,也没有目标步骤。用户停止的一轮对话不发送任何事件。批准扩展就是订阅,对话框会在服务那一行下方列出这些事件。没有 content 时服务只收到标识符,即会话、它的项目目录和用户附加到它的条目。设为 "content": true 时,清单还会请求一项权限,用户会看到它单独占一行,在后台读取代理所说的内容。此时每个事件都带有智能体的最终回答、问题的字段或权限的详情。

每个事件以一次带服务 bearer 令牌的 POST /session-events 到达。用 readSessionEventRequest 读取并回答 { "ok": true }。

import { SESSION_EVENTS_PATH, readSessionEventRequest } from "@openchamber/sdk";
// inside your service's request handler, after the bearer check
if (url.pathname === SESSION_EVENTS_PATH) {
const request = readSessionEventRequest(body);
if (!request) return json(res, 400, { ok: false, error: "not a session event" });
const { event, content } = request;
if (event.type === "turn-finished" && event.project.directory === myProject) {
await postComment(event.linked, content?.type === "turn-finished" ? content.reply : "");
}
return json(res, 200, { ok: true });
}

每个项目的事件都会发送。把 event.project.directory 与你自己的设置比较,其余的回答 { "ok": true } 即可。投递至多一次、按顺序进行,一次只发一个请求。回答失败会让你的事件被丢弃 30 秒,OpenChamber 或你的服务停机期间的事件不会重放。服务不需要面板来做这件事。示例 session-events-log 把每个事件记录到一个文件中。

从服务调用 OpenChamber

服务可以在没有打开任何窗口时行动。createServiceHost 是它回到 OpenChamber 的途径:

import { createServiceHost } from '@openchamber/sdk';
const host = createServiceHost({ env: process.env });
const { sessionId } = await host.sessions.start({ prompt: 'Summarize the failed deploy', directory });
await host.sessions.send(sessionId, 'Now write the incident note');
await host.setInstructions(directory, 'Deploys run through the release branch.');
await host.emit('deploy-finished', { id: 42 });

sessions.start、sessions.send、sessions.retry、sessions.status 和 sessions.stop 需要 contributes.sessionControl,用户会把它作为单独一行批准。sessions.lastReply 需要 sessionControl: { content: true }。通过 sessions.send 发送的消息会关闭智能体在该会话中等待的表单或页面,效果和在聊天中输入的消息一样。

要把智能体的请求转到别处,比如聊天应用,加上 answers: true。用户会把它作为又一行批准,之后服务可以在任何会话中代用户回答:对 permission-asked 事件调用 sessions.answerPermission(sessionId, requestId, { decision: 'allow' })(或带上智能体会读取的 message 的 'deny'),对 event.question.kind 为 form 的 question-asked 事件调用 sessions.answerForm(sessionId, formId, values)。事件内容列出表单的字段及其 key 和选项。用 null 代替值会直接关闭表单。一次允许只对这一个请求有效。

setInstructions 需要 contributes.instructions,用于补充智能体读取的内容,范围是一个文件夹,或者传 null 时覆盖所有位置。

emit 通过 host.onServiceEvent 到达你打开的框架,因此面板不必再轮询。事件不会重放,所以之后才打开的框架要先读取当前状态。readSettings 把集成的设置交给服务,包括 secret 字段,needsConfiguration 则在扩展的卡片上显示一条消息。

无人请求时运行

服务在收到第一个请求时启动,承担角色的服务在空闲时停止。service.keepRunning: true 让它随 OpenChamber 启动,并在停止后恢复运行。service.schedule 按时间唤醒它:

"service": {
"entry": "service/main.js",
"runtime": "host",
"keepRunning": true,
"schedule": [{ "id": "nightly", "cron": "0 3 * * *", "timezone": "Europe/Kyiv" }]
}

一次运行就是一次 POST /schedule。用 readServiceScheduleRequest 读取并回答 200。OpenChamber 停机期间错过的运行不会补上。需要外部事件的服务要自己保持连接或进行轮询。

@ 菜单中的行

contributes.mentions: { label } 会在聊天框的 @ 菜单中添加一个分区。你的服务要在 2 秒内用 { items: [{ id, title, subtitle?, url? }] } 回答 POST /mentions/search,并针对用户选中的行用 { text } 回答 POST /mentions/resolve。文本随消息发送给智能体,用户会看到一个标签。响应慢的服务不会添加任何行,也不会阻塞文件列表;无法读取的行不会留下标签,并会给出提示。

聊天中的块

contributes.blocks 添加智能体可以写入 UI 块的组件。每个组件都由你自己的页面绘制,使用与面板相同的沙箱和相同的权限。

"blocks": [{ "name": "JiraIssue", "description": "A Jira ticket", "args": ["key"], "entry": "blocks/issue.html" }]

智能体写下 JiraIssue("OPE-1"),你的页面读取这次调用:

host.onBlock(({ args }) => draw(args.key));

从根元素上的 ResizeObserver 调用 host.setHeight(px)。iframe 填满自己的框且没有内边距,所以请自行添加。每个扩展最多 4 个组件,因为每个签名都是每个会话要读取的文本。

块可以回答智能体,就像块里的按钮那样。在点击时调用 host.answerBlock(text):智能体收到文本并开始下一轮。每个块只能回答一次,重新加载后 block.answered 仍保留这个回答,这时请把块绘制为已回答状态。

host.onBlock(({ args, answered }) => draw(args.key, answered));
button.onclick = () => host.answerBlock('Close this ticket');

请在这样做的组件上设置 "answers": true。当组件描述相符时,智能体会被告知用它来提问,而不是用内置选项;等待回答的页面会省略自己的发送按钮。在这样的块下方,OpenChamber 会提供与自身表单相同的备注字段:智能体会连同你的回答一起读取备注,只写备注也算回答。

参数由模型写入,所以网页或工单中的文本可能左右它们。请把它们视为不可信内容,在用户点击之前不要执行任何有副作用的操作。

通知渠道

服务可以在桌面端和手机之外投递用户的通知。声明 provides: ["notifications"],并在 contributes.notifications 中描述这个渠道。

"service": { "entry": "service/main.js", "runtime": "host", "provides": ["notifications"] },
"notifications": {
"label": "Slack",
"instances": { "kind": "fixed", "origin": "https://slack.com" },
"token": { "header": "authorization", "scheme": "bearer", "scopes": ["chat:write"] }
}

OpenChamber 根据这段描述在设置 → 集成中为该渠道绘制一张账户卡片,并保管账户。用户在那里连接账户,令牌保留在 OpenChamber 中,你的服务永远看不到它。账户连接后,OpenChamber 会发给手机的每条通知也会发给你的服务,遵循用户自己的通知设置,并且只在没有窗口可见时发送,除非用户选择了始终接收通知。暂停扩展即可停止。

OpenChamber 把三种调用转发到 POST /notifications。auth.probe 问这个服务器是否是你投递的目标,auth.verify 问这个令牌属于谁,notify 送来一条通知。每种都用 readNotificationsProviderRequest 读取,并且只通过出站助手访问渠道的服务器。助手把你的请求连同调用的句柄一起发给 OpenChamber,OpenChamber 加上令牌,并把请求限制在该账户的服务器上。

import { NOTIFICATIONS_PROVIDER_PATH, createNotificationsOutbound, readNotificationsProviderRequest } from "@openchamber/sdk";
const outbound = createNotificationsOutbound({ env: process.env });
// inside your service's request handler, after the bearer check
if (url.pathname === NOTIFICATIONS_PROVIDER_PATH) {
const request = readNotificationsProviderRequest(body);
if (!request) return json(res, 400, { ok: false, error: { message: "not a notifications call" } });
if (request.method === "notify") {
const { notification } = request.input as { notification: GuestNotification };
const answer = await outbound.request(request.session.handle, {
method: "POST", path: "/api/chat.postMessage",
headers: { "content-type": "application/json" },
body: JSON.stringify({ channel: "#agents", text: `${notification.title}: ${notification.body}` }),
});
return json(res, 200, answer.status === 200 ? { ok: true, data: { delivered: true } } : { ok: false, error: { message: `Slack answered ${answer.status}`, status: answer.status } });
}
// auth.probe → { ok: true, data: { supported: true } }; auth.verify → { ok: true, data: { account: { id, login } } }
}

哪个项目发到哪个房间或频道由你的扩展自己决定。通知携带 directory 和 sessionName。在企业模式下它只携带通用标题,和手机一样。示例 notification-webhook 是一个完整的渠道,并带有一个可以用来试验的接收端。

跟踪器

服务可以成为 Linear 旁边的工作项跟踪器。声明 provides: ["tracker"],并在 contributes.tracker 中描述这个跟踪器。OpenChamber 随后会在跟踪器出现的所有地方把它列在 Linear 旁边,仅凭清单即可,在服务运行之前就已生效。

"service": { "entry": "service/main.js", "runtime": "host", "provides": ["tracker"] },
"tracker": {
"wire": 1,
"label": "YouTrack",
"instances": { "kind": "user", "label": "Server address", "placeholder": "https://youtrack.example.com" },
"auth": { "token": { "header": "authorization", "scheme": "bearer", "createPath": "/users/me?tab=account-security" } },
"capabilities": { "stateUpdate": true, "labels": true, "priority": true, "projectMapping": true, "sessionComments": true },
"stateFilters": ["open", "all", "todo", "in-progress", "done"],
"identifier": "key-number",
"urls": { "item": ["/issue/{id}", "/{segments+}/issue/{id}"] }
}

用户在 扩展账户 下该跟踪器的卡片上连接账户:令牌,或者在你声明了 auth.oauth 时在浏览器中登录(OpenChamber 用用户输入的 client id 完成登录并保管令牌)。你的服务永远看不到凭据。urls.item 是条目地址的模板,{id} 代表标识符(当它出现在查询参数中时也可以写成 ?name={id});OpenChamber 会编译这些模板,让粘贴的链接变成关联条目。identifier 说明键的形式:key-number(PROJ-42)、number 或 short-code。

OpenChamber 把九种调用转发到 POST /tracker:auth.probe 和 auth.verify,读取调用 items.list、items.get、items.liveSummaries、states.list 和 containers.list,以及写入调用 items.updateState 和 comments.create。每种都用 readTrackerProviderRequest 读取,以中性的条目形式应答,并且只通过出站助手访问跟踪器的服务器。助手把你的请求连同调用的句柄一起发给 OpenChamber,OpenChamber 加上凭据,并把请求限制在该账户的服务器上。读取调用的句柄只能发送 GET。

import { TRACKER_PROVIDER_PATH, createTrackerOutbound, readTrackerProviderRequest } from "@openchamber/sdk";
const outbound = createTrackerOutbound({ env: process.env });
// inside your service's request handler, after the bearer check
if (url.pathname === TRACKER_PROVIDER_PATH) {
const request = readTrackerProviderRequest(body);
if (!request) return json(res, 400, { ok: false, error: { message: "not a tracker call" } });
if (request.method === "items.get") {
const answer = await outbound.request(request.session.handle, { method: "GET", path: `/api/issues/${encodeURIComponent(String(request.input.id))}` });
if (answer.status === 404) return json(res, 200, { ok: true, data: { item: null } });
if (answer.status !== 200) return json(res, 200, { ok: false, error: { message: `YouTrack answered ${answer.status}`, status: answer.status } });
return json(res, 200, { ok: true, data: { item: toTrackerItem(JSON.parse(answer.body)) } });
}
}

用户开启会话评论后,OpenChamber 会带着 “session started”、“completed” 或 “failed” 以及会话链接调用 comments.create,各一次;评论不携带其他任何内容。示例 tracker-provider 是一个完整的跟踪器,并带有一个可以用来试验的假服务器。

用量提供商

服务可以在内置提供商旁边报告用量:声明 provides: ["quota"],并在 contributes.quota.providers 中列出它需要凭据的 OpenCode 提供商。OpenChamber 把这个提供商命名为 ext-<panel.id>,用扩展的名称和图标绘制它的卡片,并像询问内置提供商那样询问它。

"service": { "entry": "service/main.js", "runtime": "host", "provides": ["quota"] },
"quota": { "providers": ["openrouter"] }

批准对话框会列出这些提供商,更改列表的版本会再次询问。每次刷新时,OpenChamber 向 POST /quota 发送一次 usage.read 调用,input.credentials 中带有 OpenCode 为每个已列出且持有凭据的提供商保存的凭据(API 密钥,或浏览器登录的令牌),此外不带存储中的任何内容。OpenCode 未保存凭据的已列出提供商显示为未配置,服务不会被调用;允许空列表,用于密钥保存在扩展自身设置中的提供商。

用 readQuotaProviderRequest 读取请求体,并以 { ok: true, data } 回答内置提供商计算出的结果:ok、configured、error、planLabel,以及按窗口名称键控的 usage.windows,每个窗口带有 usedPercent、windowSeconds、resetAt(纪元毫秒)和可选的 valueLabel 或 value,另有按模型划分窗口的 usage.models。剩余份额、重置倒计时和格式化字符串由 OpenChamber 推导,因此卡片与内置提供商的卡片一致。

import { QUOTA_PROVIDER_PATH, readQuotaProviderRequest } from "@openchamber/sdk";
// inside your service's request handler, after the bearer check
if (url.pathname === QUOTA_PROVIDER_PATH) {
const request = readQuotaProviderRequest(body);
if (!request) return json(res, 400, { ok: false, error: { message: "not a usage call" } });
const credential = request.input.credentials.openrouter;
const bearer = credential.type === "api" ? credential.key : credential.access;
const answer = await fetch("https://openrouter.ai/api/v1/key", { headers: { authorization: `Bearer ${bearer}` } });
if (answer.status === 401) return json(res, 200, { ok: true, data: { ok: false, configured: true, error: "OpenRouter refused the key.", usage: null } });
return json(res, 200, { ok: true, data: toUsage(await answer.json()) });
}

服务在 20 秒内回答,十分钟没有调用就会停止;用量页面打开时每三分钟刷新一次。示例 quota-provider 是一个完整的提供商,并带有一个可以用来试验的假用量 API。

文件

相对路径位于打开的项目中,需要 files 能力。以 / 或 ~/ 开头的路径必须匹配已声明的 contributes.filesystem 模式,需要 filesystem 能力。规则见构建扩展。

const { content } = await host.readFile("package.json");
await host.writeFile("notes/today.md", "# Today\n");
const { entries } = await host.listDir("src");
const info = await host.stat("~/.config/opencode/opencode.json");

限制:路径最长 1024 个字符,内容最多 2,000,000 个字符,列表最多 2000 项。内容超出限制为 FILE_TOO_LARGE。

generate

const { text } = await host.generate({ prompt: notes, system: "Summarize in one sentence." });

prompt 在修剪后为 1 到 64,000 个字符,system 最多 8,000 个字符,maxOutputTokens 为 1 到 4,000。内容不会进入会话,也不保留历史记录;扩展永远不能选择模型。调用最多等待 90 秒。没有可用的 Small Model 是 NO_MODEL;模型出错是 MODEL_FAILED。见生成文本。

错误码

失败的调用会抛出 HostRequestError。code 是下列之一;message 提供更多说明。

错误码何时出现
HOST_UNAVAILABLE没有 window、不在 OpenChamber 内,或已执行 dispose()
HOST_TIMEOUT20 秒内没有响应
HOST_REJECTEDOpenChamber 拒绝了请求,或返回了本 SDK 不认识的错误码
DISCONNECTED你的集成没有令牌或 Linear 连接
BAD_PATHpath 格式错误,或试图离开允许的源
NO_INTEGRATION清单中没有 integration
NO_SESSION没有打开的会话时调用了 prompt 或 sessionLink
SESSION_BUSY会话忙碌时调用了 prompt({ send: true })
DISABLED用户在 设置 → 扩展 中暂停了该扩展
NO_SERVICE未声明本地服务、未批准或未运行
NOT_GRANTED用户尚未批准此调用所需的能力
SERVICE_FAILED本地服务崩溃或从未就绪
NO_DIRECTORY在没有打开项目的情况下使用了相对文件路径,或在没有打开项目时调用 openCommit
NOT_FOUND对不存在的文件调用 readFile,或对项目中不存在的提交调用 openCommit
FILE_TOO_LARGE读取或写入时文件内容超出限制
DENIED操作系统拒绝了该文件操作
NO_MODEL没有可用的 Small Model 时调用 generate
MODEL_FAILEDSmall Model 返回了错误
UNSUPPORTED这个 OpenChamber 无法执行该调用,例如在没有 Diff 视图的地方调用 openCommit

相关页面

  • 构建扩展:文件夹、清单、安装和能力
  • UI 套件:按钮、输入框、列表和其他构件