开发和提交 DBX 插件
DBX 插件是独立安装的 .dbxp 包,可以提供连接类型、工作台、文件系统和原生 Sidecar。插件源码、DBX 插件框架和官方商店目录由不同仓库负责。
推荐路径: 使用 dbx-plugin create 创建项目,在自己的仓库开发并发布未签名候选包;dbx-store 会为已注册的插件自动创建或更新候选 PR,维护者审核并签名后才会进入官方目录。
插件上架 Pull Request 提交到 t8y2/dbx-store,不是 t8y2/dbx。 插件自己的功能代码通常保留在开发者自己的源码仓库;只有修改 DBX Host、SDK、CLI、协议、Schema、文档或官方示例时,才向 t8y2/dbx 提交 PR。
插件快捷入口
在 插件中心 → 设置 → 全局配置 中管理快捷入口。默认开启并显示在右侧顶部;可选择左侧或右侧的顶部/底部优先、连接树底部,或更新按钮左侧的浮动栏。浮动栏默认显示 3 个图标,可设置 0–10 个,空间不足的入口放入下拉菜单,以图标和名称逐行显示。
连接树底部区域的「插件」标题栏右上角默认提供「插件设置」入口,其他显示位置则将该入口固定在末尾,点击直达插件中心的设置页,不参与拖动排序。浮动栏溢出时,该入口位于下拉菜单末尾。在「全局配置」中关闭「显示“插件设置”快捷入口」可隐藏它;仍可从插件中心的「设置」页重新开启。没有可显示的插件快捷入口时,整个快捷区域(包括设置入口)隐藏,不占用布局空间。
每个功能有独立图标,可以拖动排序;显示开关按插件生效,关闭后隐藏该插件的所有入口。所有位置共用保存的顺序。隐藏或卸载不会删除已有排序记录,重新出现的相同入口恢复原位置,未记录的入口排列在已保存入口之后。排序标识由插件 ID、入口类型和功能 ID 组成,修改这些 ID 会被视为新入口。
拖动预览跟随光标,靠近窗口边缘时自动调整方向和位置;右侧停靠优先显示在光标左侧,长名称会根据可用宽度省略显示。
选择「插件中心右侧(下拉)」可通过插件中心按钮旁的箭头展开全部快捷入口,每行显示图标和名称,支持拖动排序;该位置不受浮动栏显示数量限制。
连接树底部采用左侧图标、右侧名称的单列列表,行样式和字号与连接树一致。默认按入口数量显示所需行数,最多五行,超出后竖向滚动。拖动分隔线可保存自定义高度,双击恢复自动高度。拖动图标时按 Esc、切换窗口或在区域外释放会取消排序;保存失败会提示并回滚。
快捷入口保留 appToolbar 声明的可见条件及命令执行条件。无连接类型的插件还可从快捷栏打开已有工作台;有对应命令时优先执行命令,没有命令体系的旧式插件也可显示文件浏览入口。连接型插件只显示明确声明的全局命令,不自动生成缺少连接上下文的工作台入口。不兼容插件不展示。
显示位置只决定图标位置,内容继续按原有 panel/tab 语义打开。隐藏快捷栏、切换位置或隐藏插件均不会关闭已打开的会话。面板快捷入口仅切换当前选中且匹配的命令面板,恢复时保留该实例。无需修改插件 Manifest 或后端协议;旧 left/right 设置迁移为对应侧顶部优先。
应该提交到哪个仓库
| 你要修改的内容 | 提交位置 | 是否向 DBX 官方提 PR |
|---|---|---|
| 你的插件前端、Rust/Go 后端、测试和发布脚本 | 插件自己的源码仓库 | 不向 dbx 提交;在自己的仓库开发和发布 |
| 新插件上架、版本更新、商店图标和介绍 | t8y2/dbx-store | 是,目标分支为 main |
| DBX 插件 Host、Manifest/Marketplace Schema、SDK、CLI、Packager | t8y2/dbx | 是,目标分支为 main |
| DBX 官方示例或插件开发文档 | t8y2/dbx | 是 |
| 某个第三方插件自身的 Bug | 该插件的源码仓库 | 不要提交到 dbx-store |
| 自定义或企业私有仓库 | 你的私有仓库 | 不需要向官方商店提交 |
| 数据库厂商 JDBC Driver JAR | 不提交到 Git 仓库 | 用户在本机或服务器中导入 |
DBX 官方维护但需要独立发布节奏的插件,通常也应使用独立源码仓库。只有与 Host 强耦合的最小示例、SDK 验证项目或维护者明确同意的组件才放进 t8y2/dbx。
插件由哪些部分组成
my-plugin/
├── manifest.json
├── dbx-plugin.toml
├── assets/
├── ui/
├── backend/ # frontend 模板没有此目录
├── .github/workflows/
│ └── plugin-release.yml
└── dist/ # 构建输出,不提交到 Gitmanifest.json声明插件身份、版本、权限、入口和 Contributions。ui/在沙箱工作台中运行,通过 Host API 与 DBX 交互。backend/是可选的原生 Sidecar,可以使用 Rust、Go 或其他能够实现 DBX JSON-RPC 协议的语言。.dbxp是安装包,不是源码仓库。- 前端插件可以发布一个
universal包;包含原生 Sidecar 的插件需要按操作系统和 CPU 架构分别构建。
Manifest 完整说明
manifest.json 是运行时契约,文件必须位于插件包根目录。Manifest v1 不接受未声明字段,建议在编辑器中引用仓库内的 plugins/manifest.schema.json。最小的纯前端 Manifest 如下:
{
"$schema": "https://raw.githubusercontent.com/t8y2/dbx/main/plugins/manifest.schema.json",
"manifest_version": 1,
"id": "com.example.files",
"name": "Example Files",
"version": "0.1.0",
"publisher": "example",
"description": "Browse files from an example service.",
"icon": "assets/plugin.svg",
"source": "https://github.com/example/dbx-plugin-files",
"homepage": "https://github.com/example/dbx-plugin-files",
"engines": { "host_api": "1" },
"permissions": [],
"entrypoints": { "ui": { "root": "ui", "entry": "ui/index.html" } },
"contributions": [
{ "type": "workbench", "id": "com.example.files.main", "label": "Files", "icon": "assets/plugin.svg" }
]
}Manifest 中的路径相对于包根目录,不能以 / 开头、不能包含 ..、反斜杠或重复斜杠。ui.entry 必须位于 ui.root 内:当 root 为 ui 时,入口写 ui/index.html,不是 index.html。signingKeyId 属于商店 artifact metadata,不是 Manifest 字段。
| 字段 | 必需 | 说明 |
|---|---|---|
$schema | 否 | 编辑器使用的 JSON Schema 地址,不参与身份校验。 |
manifest_version | 是 | 当前必须为数字 1。 |
id | 是 | 全局稳定 ID,只能使用小写字母、数字、., _, -,例如 io.github.example.files;发布后不要更改。 |
name | 是 | 默认显示名称。 |
icon | 否 | 插件默认图标的包内相对路径;推荐 SVG 或 PNG。 |
version | 是 | major.minor.patch 语义化版本,可带 prerelease/build 后缀。相同 ID 和版本的正式包不能覆盖。 |
publisher | 是 | 发布者标识;首次上架后应保持不变。 |
description | 否 | 插件说明。 |
source | 否 | 源码仓库 HTTPS 地址,安装后可在插件信息中查看。 |
homepage | 否 | 项目主页、文档或支持地址。 |
engines | 是 | 运行时兼容范围,至少包含 host_api。 |
permissions | 否 | 最小权限集合,见下文。 |
entrypoints | 否 | UI 和原生 Sidecar 入口;贡献点存在时按需声明。 |
contributions | 否 | 连接、工作台、文件系统、连接右键菜单、结果视图、命令/菜单和 MCP 工具面声明。 |
localizations | 否 | 按 locale 覆盖名称、说明、贡献点、字段和动作文案。 |
engines、权限和入口
{
"engines": { "dbx": ">=0.6.0", "host_api": "1" },
"permissions": [
"host.workbench",
"host.events",
"host.filesystem",
"host.binary",
"host.plans:read",
"host.schema:read",
"host.storage",
"host.clipboard:read",
"host.data:read",
"host.network:https://s3.example.com:443"
],
"entrypoints": {
"ui": { "root": "ui", "entry": "ui/index.html" },
"backend": {
"protocol_versions": [1],
"transport": "stdio-jsonl",
"executable": "bin/linux-x64/backend"
}
}
}engines.dbx是可选的 DBX 版本范围;engines.host_api是必需的 Host API 版本声明。host.workbench允许打开另一个工作台,host.events接收宿主事件,host.binary使用二进制通道,host.filesystem使用文件系统入口。host.plans:read允许针对 DBX 自己的连接读取估算执行计划(见 估算执行计划)。它是只读权限:不授予普通 SQL 执行、写操作、DDL 或真实执行计划,且这是唯一存在的计划类权限。host.schema:read允许通过host.getTableMetadata读取已打开连接的窄化 table schema metadata;它不会授予任意 SQL、写入或隐式重连(见 Table Schema Metadata)。host.storage为工作台 UI 提供本插件独享的持久化键值存储(见 持久化 UI 状态);数据只落在插件自己的插件数据目录里。host.ai允许插件接入 DBX 全局 AI 面板:可以打开 Ask 快照对话、选择性打开使用实时插件工具的 Agent 对话,以及注册当前工作台的推荐问题。旧版 DBX 会在安装时因未知权限而失败,需要兼容旧宿主的插件应把host.ai作为可选能力,并通过capabilities.ai/capabilities.aiRecommendations判断运行时支持。host.clipboard:read允许工作台 UI 读取系统剪贴板(见 系统剪贴板)。写剪贴板不需要权限;读取会把用户数据(密码、令牌)交给插件代码,因此需要门控。host.data:read允许window.dbxPlugin.queryData在用户授权给插件的连接上执行一条只读 SQL(见 只读数据查询)。每个连接都需要用户同意;它不会授予写入、DDL 或重连。host.network:https://host[:port]声明浏览器可访问的 HTTPS origin,DBX 将它加入 CSP 的connect-src;最多 8 个,不允许路径、通配符或 Token。仍受目标服务 CORS 规则约束。该权限不是原生 Sidecar 的网络防火墙。backend.transport默认是stdio-jsonl;需要二进制帧时使用stdio-framed,并同时声明host.binary。backend.executable、ui.root和ui.entry必须指向包内文件;带原生后端的包必须为每个目标平台提供对应可执行文件。
Connection Provider
连接 Provider 负责声明连接表单,DBX 负责渲染表单和保存生命周期;插件只处理自己的连接协议。binding 决定字段语义:config 写入外部配置,secret 写入 Secret Store,name、host、port、username、password、database 映射到标准连接字段。
{
"type": "connection-provider",
"id": "com.example.files.connection",
"label": "Example Service",
"icon": "assets/connection.svg",
"database_type": "example-files",
"description": "Connect to an Example Service.",
"fields": [
{ "key": "display_name", "label": "Name", "type": "text", "binding": "name", "required": true },
{ "key": "endpoint", "label": "Endpoint", "type": "text", "binding": "host", "required": true },
{ "key": "port", "label": "Port", "type": "number", "binding": "port", "default": 443 },
{ "key": "region", "label": "Region", "type": "radio", "options": [{ "label": "US", "value": "us" }, { "label": "EU", "value": "eu" }], "binding": "config" },
{ "key": "token", "label": "Access token", "type": "password", "binding": "secret", "required": true }
],
"workbench": "com.example.files.main",
"capabilities": ["test", "connect", "disconnect"],
"actions": [
{ "id": "refresh", "label": "Refresh metadata", "variant": "outline", "when": "edit", "requires_valid_form": true, "timeout_ms": 30000 }
]
}字段类型为 text、password、number、boolean、select、radio 或 textarea;select/radio 必须提供非空 options,每项包含 label 和字符串 value。binding: "port" 必须使用 number。不要把密码、Token 或私钥放进 config、Workbench context、事件或日志。
每个字段必须声明 key、label、type,可选 description、placeholder、required、default、options 和 binding;默认值必须符合字段类型。省略 binding 的 password 字段默认按 secret 处理。database_type 是插件自定义类型标识,不会给 DBX 内置数据库枚举添加成员。
字段还支持条件显隐/条件必填。visible_when 与 required_when 接受单条子句 { "field": "mode", "one_of": ["custom"] }(引用字段取到列表中的值时命中,按字符串比较,因此 [false] 与 ["false"] 都能匹配布尔 false),并用 all_of / any_of / not 组合:
{
"key": "sudo_command",
"label": "Sudo command",
"type": "text",
"visible_when": {
"all_of": [
{ "field": "sudo_source", "one_of": ["custom"] },
{ "field": "read_only", "one_of": [false] }
]
}
}条件会级联:子句引用的字段自身处于隐藏状态时该子句不参与判定,因此隐藏容器的默认值不会把孙级字段点亮;未填写或空值永不命中;组合树最多 8 层、64 个节点。DBX 在对话框与保存/测试/连接校验中使用同一套求值,不会出现「表单放行、宿主拒绝」的分歧。
text、password、textarea 字段可以用 picker 声明本机文件选择(例如 SSH 私钥、keystore):
{
"key": "private_key_path",
"label": "Private key path",
"type": "text",
"binding": "config",
"picker": { "kind": "file", "accept": [".pem", ".key"], "content_field": "private_key" }
}桌面端打开系统文件选择器,把选中的绝对路径写回该字段(插件后端同机运行,可自行读取文件);网页端拿不到客户端路径,同一操作变成上传:DBX 读取文件内容写入声明的 content_field 兄弟字段,并清空路径字段。两者互相清除——选了路径就丢弃已上传内容,上传内容就清空路径,避免同时存在两份互相矛盾的来源。kind 还支持 directory(仅桌面端),accept 最多 16 条扩展名或 MIME 过滤,上传上限 1 MiB。
插件在连接过程中需要用户输入时(堡垒机登录期 keyboard-interactive 验证码、主机密钥确认、账号选择等),可以调用 Host API 1.1 新增的 host/requestUserInput,通过 DBX 同一个阻塞对话框拿到用户输入,连接测试 与真实连接走同一条通道:
{
"jsonrpc": "2.0",
"id": "prompt-1",
"method": "host/requestUserInput",
"params": { "prompt": "Verification code (6 digits)", "title": "JumpServer login", "echo": false, "timeoutSecs": 300 }
}返回值为 { "action": "submit", "value": "123456" }、{ "action": "cancel" } 或 { "action": "timeout" };只有 submit 带值,其余必须按「没有答案」处理并失败关闭。插件发起的请求使用字符串 id,DBX 自己的请求与响应继续使用数字 id;提示期间 DBX 会暂停等待该回答的请求超时计时,用户输入验证码不会被误判成连接超时。错误码 -32001 表示没有可用的用户界面(headless/MCP),-32602 参数非法,-32601 宿主不支持该方法,三种情况都要优雅降级。调用前请确认 plugin/initialize 通告的 hostApiVersion 为 1.1.0(或 host.features 含 host.requestUserInput);Rust SDK 对应 dbx_plugin_sdk::host_client() 与 HostClient::supports("host/requestUserInput")、HostClient::request_user_input(&UserInputPrompt::secret("Verification code"))。
连接生命周期使用固定的 connection/test、connection/connect、connection/disconnect,自定义表单动作使用 connection/action,参数包含 provider、connection、runtime;动作额外包含 action: { id }。后端使用 connection.id 保存会话,并使用 runtime.host/runtime.port 连接经过 DBX 隧道或代理转换后的端点。仅在后端生命周期请求中接收补齐 Secret 的连接;前端通过 connectionId 使用已经建立的会话。
多端点协议(Kafka advertised.listeners、集群发现等)在贡献点上声明 proxy_route。配置了传输层后,DBX 会在 payload 中下发 SOCKS5 路由而非静态转发:runtime.host/runtime.port 保留逻辑端点,插件通过 runtime.proxy({ "type": "socks5", "host": "...", "port": 1080, "username": "...", "password": "..." })拨号每个广播端点——SSH 末层暴露其动态 SOCKS5 端点,SOCKS5 代理层直接使用;代理凭据与连接 Secret 走同一加密通道,插件不得记录日志。未声明该旗标时传输层保持静态转发行为,要求标准 host/port 字段给出单一远端端点;对会隧道到空端点的插件连接,DBX 直接给出可行动的报错而非静默超时。
动作的 when 可为 always、create、edit;variant 可为 default、outline、secondary、destructive、ghost;timeout_ms 为 1–120000。requires_valid_form 决定是否要求完整表单,close_on_success 决定成功后是否关闭。返回 { "success": true, "message": "...", "fieldValues": { "port": 443 } } 可以更新已声明的字段;返回 success: false 表示失败。
其他 Contributions
| 类型 | 必需字段 | 用途 |
|---|---|---|
workbench | type, id, label | 注册可从侧边栏或插件入口打开的工作台。 |
filesystem-provider | type, id, label, schemes | 注册虚拟文件系统;root_uri 例如 s3://bucket,能力可选 read、write、delete、rename、mkdir。 |
context-menu | type、id、label、menu: "connection" 或 "table"、可选 action | 向 Sidebar Tree 的连接或 table 右键菜单增加后端动作,或打开已声明的 Workbench。 |
result-view | type, id, label | 注册查询或任务结果视图。 |
menu: "table" contribution 当前只在用户对 Sidebar Tree 中的具体 table 节点打开右键菜单时生效。未声明 action 的旧式 contribution 会通过必需的 backend entrypoint 调用 contextMenu/<contribution-id>,并传递 table identity:
{
"table": {
"connectionId": "connection-id",
"database": "example",
"schema": "public",
"table": "users"
}
}database 和 schema 是可选字段;不可用时会省略,不会强制传递空字符串。该 context 只包含对象 identity,不包含 credential、connection string 或原始连接配置。Object Browser 后续可以复用相同 identity contract,但本次版本没有把它作为 table 菜单入口。
context-menu 也可以声明由 Host 处理的 Workbench action:
{
"type": "context-menu",
"id": "vendor.example.generate",
"label": "生成测试数据",
"menu": "table",
"action": { "type": "open-workbench", "workbench": "vendor.example.main" }
}action.workbench 必须引用同一插件 manifest 中声明的 workbench。Host 会直接打开该 Workbench,不调用 sidecar,并将当前 invocation context 作为 Workbench context 传入。menu: "table" 传入上面的 TableContext 对象本身(不是 backend 的 { "table": ... } envelope);menu: "connection" 传入现有的非敏感 connection summary(id、dbType、name、database)。Host 还可能提供用于 tab 关联的标准 connectionId。两者都不会暴露 host、port、username、password、connection string 或原始连接配置。再次打开同一个 Workbench 时会用最新 invocation context 刷新其 context。
只有未声明 action 的旧式 context-menu 才要求 backend entrypoint;其返回的 { "message": "..." } 仍会显示为 toast。
同一插件内通过 id 互相引用:Connection Provider 的 workbench 必须指向一个已声明的工作台,filesystem_provider 必须指向一个已声明的文件系统 Provider。图标优先使用 Contribution 的 icon,缺失时回退到插件级 icon。
自定义文件浏览器应使用 workbench:列表、虚拟滚动、预览、右键菜单和分栏由插件前端实现。只有希望使用 DBX 通用文件管理器时才选择 filesystem_provider;同时声明两者时默认打开工作台。未声明 action 的旧式 context-menu 调用后端 contextMenu/<contribution-id>;声明式 open-workbench action 则由 Host 直接打开同插件 Workbench。result-view 在工作台 context 中接收结果快照。SDK 会在初始化后通过 dbxPlugin.contributionId 暴露 Host 选中的 contribution id,因此单个 UI 入口可以为多个工作台和结果视图服务;具体负载见完整贡献点协议。
文件系统协议
使用 DBX 通用文件管理器时,后端实现以下方法;自定义工作台也可以自行通过 RPC 使用这些方法,但不能把声明 Contribution 当成已经实现业务逻辑。
| 方法 | 参数与返回 |
|---|---|
filesystem/list | providerId、可选 connectionId、uri、可选 cursor、limit;返回 { entries, nextCursor? }。 |
filesystem/read | 同上身份字段、uri、maxBytes;返回 { dataBase64, contentType?, truncated, etag? }。 |
filesystem/write | uri、dataBase64、create、overwrite、可选 etag;需声明 write。 |
filesystem/createDirectory | uri;需声明 mkdir。 |
filesystem/delete | uri、recursive;需声明 delete。 |
filesystem/rename | sourceUri、targetUri、overwrite;需声明 rename。 |
所有方法都携带 providerId 和可选 connectionId。目录项包含单个文件名 name、完整 uri、kind(file、directory、symlink、other)以及可选 size、modifiedAt、contentType。name 不能包含 /、\\ 或是 ./..;完整路径放在 uri。最后一页省略 nextCursor,不要返回已经使用的 cursor。图片预览应返回实际 MIME 和 base64 字节,不能把二进制当作 UTF-8 文本。
写操作返回 { success, message?, entry? };内联读写限制为 4 MiB。大文件传输应采用分块与进度/取消协议,而不是一个巨大的 JSON/base64 值。
国际化
localizations 的 key 使用 zh-CN、en 等 locale。可覆盖插件名称、说明以及每个 Contribution 的 label、description、字段和动作文案;它不会翻译插件 UI 自己的文本。
{
"localizations": {
"zh-CN": {
"name": "示例文件",
"contributions": {
"com.example.files.connection": {
"label": "示例服务",
"fields": { "token": { "label": "访问令牌" } }
}
}
}
}
}Workbench 上下文契约
Workbench 上下文跨越 DBX 与插件边界时是 JSON 数据快照。宿主会递归移除 Vue 响应式包装并发送独立副本,因此插件不能依赖 Vue ref、Proxy、DOM 节点、函数、组件实例或凭据。
上下文可以包含 null、布尔值、有限数字、字符串、数组和普通对象。对象中的 undefined 字段会省略,数组中的 undefined 会转换为 null。Date、Map、Set、Symbol、BigInt、非有限数字、循环引用和自定义 class 实例会被拒绝并返回错误。上下文 UTF-8 编码后的大小上限为 2 MiB。
连接 Provider 使用 binding: "config" 的字段会保存到 external_config;使用 binding: "secret" 的字段会保存到 Secret Store。插件连接的 external_config 会在新建、编辑、保存和重新连接流程中保留。
宿主还会为每个 workbench 上下文写入三个保留字段:workbenchId(宿主生成的稳定实例 id)、restored 和 surface。surface 表示当前 UI 跑在哪种容器里——"tab"(主多标签工作台)、"dock"(底部面板)或 "window"(桌面浮动小组件窗口,见浮动窗口)。同一个插件的同一个 contribution 可以同时开在多个 surface 上,各自拥有独立的上下文和生命周期;遇到未知取值应退化成 "tab" 的行为。
前端 Host API
UI 在隔离的 iframe 中运行,DBX 通过 window.dbxPlugin 提供桥接对象。不要导入 DBX 的 Vue/Tauri 模块,也不要假设浏览器页面能直接访问 Node.js、文件系统或网络。
多个 Workbench 或 result-view contribution 共用同一个 UI 入口时,可通过 Host 选中的 identity 路由页面:
await window.dbxPlugin.ready;
const contributionId = window.dbxPlugin.contributionId;contributionId 与 context 相互独立;例如从 Plugin Center 打开的 Workbench,其 context 可以是空对象 {}。SDK 会在 resolve ready 前记录 identity,因此 await dbxPlugin.ready 后可确定性地读取。为兼容旧插件,dbx-plugin-init 仍会携带完整 init payload 并作为通知派发;但该事件只派发一次,不应成为读取权威状态的唯一来源。
以下自定义 objects/list 和 objects/changed 需要由插件后端实现,资源示例假设包内存在 ui/assets/empty-state.svg:
await window.dbxPlugin.ready;
const contributionId = window.dbxPlugin.contributionId;
const context = window.dbxPlugin.context;
const result = await window.dbxPlugin.invoke("objects/list", { prefix: "docs/" }, { timeoutMs: 30000 });
const off = window.dbxPlugin.onContext((nextContext) => render(nextContext));
await window.dbxPlugin.notify("objects/changed", { count: result.items.length });
const assetUrl = await window.dbxPlugin.readAssetUrl("assets/empty-state.svg");| API | 作用 |
|---|---|
ready | 等待 Host 完成初始化;应用启动逻辑放在 await dbxPlugin.ready 之后。 |
contributionId | Host 打开的 contribution 的只读 identity;await ready 后可读取,且与 context 独立。 |
context / onContext(fn) | 读取或监听当前工作台上下文。 |
locale | 读取当前语言;插件 UI 需自行完成翻译。 |
theme | 读取 appearance 和 DBX 设计 tokens,适配明暗主题。 |
request(method, params) | 调用 Host API;常用内部方法包括 host.getContext、ui.readAsset。 |
invoke(method, params, { timeoutMs }) | 向自己的 Sidecar 发送有响应的 RPC。 |
notify(method, params) | 向 Sidecar 发送无需返回业务结果的通知。 |
sendBinary(channel, data) / onBinary(fn) | 发送或接收二进制数据;需要 stdio-framed 和 host.binary。 |
readAsset(path) / readAssetUrl(path) | 读取包内资源;路径不能越出插件资源根目录。 |
openWorkbench(id, context) | 打开本插件的另一个工作台;需要 host.workbench。 |
closeWorkbench() | 关闭承载当前 UI 的表面——工作台标签、dock 面板条目,或本插件的浮动窗口。与 Ctrl/Cmd+W 快捷键走同一条路径,因此插件可以提供自己的关闭或最小化按钮。 |
executeCommand(commandId, context?) | 执行本插件自己声明的命令,与菜单执行完全同径——重新检查 enablement、按单例复用,并遵循声明的 presentation(panel 停靠底部面板,tab 打开工作台标签页)。context 合并到命令上下文之上,并解析 instance_key 的 {{path}} 占位符(如 instance_key: "logs:{{connectionId}}" 即每个连接一个面板实例)。预期结果通过 { error } 返回;需要 host.workbench。 |
openFilesystem(id, context) | 打开本插件的文件系统入口;需要 host.filesystem。 |
getPlanCapabilities(connectionId) | 读取宿主与该连接能提供什么计划能力;需要 host.plans:read。 |
explainPlan({ connectionId, database?, schema?, sql, mode, timeoutMs? }) | 返回 sql 的估算执行计划;需要 host.plans:read。 |
getTableMetadata({ connectionId, database?, schema?, table }) | 读取已打开连接的窄化 table schema metadata;需要 host.schema:read。 |
queryData({ connectionId, database?, schema?, sql, maxRows?, timeoutMs? }) | 在用户授权给插件的连接上执行一条只读 SQL;需要 host.data:read。 |
capabilities | 来自 init 消息的 { downloadFile, planApi, schemaMetadataApi, dataApi, storage, ai, aiRecommendations, clipboardWrite, clipboardRead, floating }。字段缺失或为 false 表示当前宿主不支持该组 Host API,应据此关闭功能而不是用真实请求试探。 |
ai.openConversation({ title, prompt, context, send?, mode? }) | 在内置 AI 面板新建插件对话;send 默认 false,mode 默认 ask。mode: "agent" 会在 context 包含已打开插件连接时使用实时插件工具;需要 host.ai,并以 capabilities.ai 广播。 |
ai.setRecommendations({ context, items }) | 覆盖当前工作台的默认推荐问题并立即刷新全局 AI 面板;每次最多 5 条,支持 {{path.to.value}} 占位符;需要 host.ai 和 capabilities.aiRecommendations。 |
ai.clearRecommendations() | 清空当前工作台的推荐问题;需要 host.ai 和 capabilities.aiRecommendations。 |
onInit(fn) / onEvent(fn) | 监听初始化、环境变化或后端事件;转发后端事件需要 host.events。 |
storage | 本插件独享的持久化键值状态;需要 host.storage;见下文。 |
fileTransfer | 本地文件流式传输、下载与(桌面端)系统拖放;见下文。 |
clipboard | 沙箱内的系统剪贴板访问;见下文。 |
floating | 打开并控制桌面浮动小组件窗口(仅桌面宿主);见浮动窗口。 |
调用失败会以 Promise rejection 返回,处理时应展示可理解的错误并允许重试。通过 request 调用的宿主方法、参数和返回值以当前 Host API 版本为准,不要调用未在协议中声明的 DBX 内部函数。
readAsset 的路径相对于 ui.root,所以这里的 assets/empty-state.svg 对应包内 ui/assets/empty-state.svg。资源不用时调用 URL.revokeObjectURL(assetUrl);组件销毁时调用 onContext/onEvent 等返回的取消订阅函数,避免重复监听。
估算执行计划
插件可以在不自带驱动、连接池和凭据的前提下读取一条查询的估算执行计划。插件只传自己的原始 SQL 和连接引用;DBX 用自己已有的方言规则生成 EXPLAIN 语句、应用与自身计划视图完全相同的只读安全校验,并在宿主连接上执行。插件只拿到原始计划,由自己完成解析、归一化和可视化。
{
"permissions": ["host.plans:read"]
}if (window.dbxPlugin.capabilities.planApi) {
const capabilities = await window.dbxPlugin.getPlanCapabilities(connectionId);
if (capabilities.supports.estimatedPlan) {
const plan = await window.dbxPlugin.explainPlan({ connectionId, database, sql, mode: "estimated", timeoutMs: 15000 });
render(plan.rawPlan, plan.format);
}
}getPlanCapabilities(connectionId) 返回 { dbType, dbVersion?, supports: { estimatedPlan }, limits: { maxTimeoutMs, maxPlanBytes } }:
supports.estimatedPlan为false表示该连接的方言在 DBX 中没有估算计划路径;应直接禁用功能,而不是调用explainPlan。limits.maxTimeoutMs已经考虑该连接自身的查询超时,请求里的timeoutMs会被夹到该上限;limits.maxPlanBytes限制计划负载大小。dbVersion只在 DBX 已经知道该连接产品版本时才存在;宿主不会为了插件去探测服务器。- 该调用只读取已保存的连接配置,不会建立连接;但连接必须已经打开:已保存但当前已断开的连接会被拒绝,而不是替插件重新建连。
explainPlan(request) 返回 { dbType, dbVersion?, format, rawPlan, truncated, warnings }:
format为json、xml(SQL ServerShowPlanXML)或text。format为json时rawPlan是已解析的 JSON 文档,其余情况是计划文本。warnings可能包含:plan_not_json(服务端返回的内容不是 JSON,负载会如实标记为text,而不是假装可解析)、plan_truncated(宿主为遵守maxPlanBytes截断了计划)、plan_rows_truncated(驱动达到计划行数上限)。只要发生过任一截断,truncated都是true。
计划 API 的边界:
- 只做估算计划。
mode必须是"estimated",其它取值一律拒绝。真实执行计划(EXPLAIN ANALYZE、SET STATISTICS XML)会真正执行语句,不属于本 API。 - 插件不能自己提供要执行的 SQL。 只接受原始
sql,EXPLAIN语句完全由宿主生成,因此插件无法传入EXPLAIN文本、驱动命令或执行模式。 - 只允许只读目标。 DBX 自身计划视图使用的同一道校验会拒绝多语句、DDL、DML 和危险关键字。Oracle 是唯一例外:DBX 也会为它规划 DML,因为
EXPLAIN PLAN FOR并不会执行该语句。 - 不涉及凭据,也不返回结果集。 返回体只有计划和元信息,密码、凭据、连接串、驱动内部结构都不会越过这条边界。
- 连接必须已经打开。 DBX 不会替插件建立连接,也不会为插件创建连接。
getPlanCapabilities与explainPlan对「已保存但已断开」的连接都会以Connection is not open拒绝;只有 DBX 当前已经打开的连接才能规划。
判断能力时请读 dbxPlugin.capabilities.planApi(旧宿主不会带这个字段),再用 getPlanCapabilities 确认这条连接是否支持,最后才调用 explainPlan。自行拼 EXPLAIN 文本既没必要,也不会被接受。
计划 API 属于 Host API 1.2。无法离开它工作的插件应在 manifest 里声明下限:"engines": { "host_api": "^1.2" }。manifest 里的版本范围是兼容下限,capabilities.planApi 是运行期探测,两者都保留。
Table Schema Metadata(表结构元数据)
插件可以读取当前 DBX 已打开连接中某个 table 的窄化 schema metadata,而不需要拥有驱动、连接池、凭据或自己拼 SQL。请求复用现有的 PluginTableContext,只允许 table identity:
{
"permissions": ["host.schema:read"]
}if (window.dbxPlugin.capabilities.schemaMetadataApi) {
const metadata = await window.dbxPlugin.getTableMetadata({
connectionId,
database,
schema,
table: "users"
});
for (const column of metadata.columns) {
renderColumn(column.name, column.dataType, column.nullable);
}
}getTableMetadata({ connectionId, database?, schema?, table }) 返回:
{
"columns": [
{
"name": "id",
"dataType": "integer",
"nullable": false,
"precision": 32,
"default": "nextval('users_id_seq'::regclass)"
}
],
"fieldCapabilities": {
"length": "supported",
"precision": "supported",
"scale": "supported",
"default": "supported"
}
}columns只包含name、dataType、nullable、可选的length、precision、scale和default。不会返回 comment、key/index、credential、连接串、驱动内部对象或任意 SQL 结果。- 可选值在没有值时保持
null或直接省略,绝不会被伪造为0或空字符串。fieldCapabilities用supported、unsupported、unknown区分提供方支持、明确不支持和宿主没有可靠 provenance 的情况;unknown不应被插件当成支持。 database和schema可选;不可用时省略,不要把空字符串当作 public identity。connectionId和table必须是非空且不超过 256 个字符的 identity 值。- 这是只读 API。DBX 只复用已打开的 Host connection/session,禁止插件隐式重连、创建连接池或执行任意 SQL。已保存但当前断开的连接会以
Connection is not open拒绝;请求的 database 没有对应已打开 session 时也会拒绝,而不是切换到别的连接。
该 API 属于 Host API 1.3。无法离开它工作的插件应声明:"engines": { "host_api": "^1.3" };同时仍应读取 capabilities.schemaMetadataApi,因为旧宿主不会提供该字段。没有 host.schema:read 时,宿主会在请求到达 backend 前拒绝调用。
只读数据查询
插件可以在用户授权给它的 DBX 连接上执行一条只读 SQL,而不需要驱动、连接池、凭据或连接串。声明权限,并按能力位决定是否调用:
{
"engines": { "host_api": "^1.4" },
"permissions": ["host.data:read"]
}if (window.dbxPlugin.capabilities.dataApi) {
const result = await window.dbxPlugin.queryData({
connectionId,
database,
sql: "SELECT status, count(*) AS total FROM orders GROUP BY status",
maxRows: 200
});
renderChart(result.columns, result.rows);
}queryData({ connectionId, database?, schema?, sql, maxRows?, timeoutMs? }) 返回:
{
"dbType": "postgres",
"columns": [{ "name": "status", "dataType": "text" }, { "name": "total", "dataType": "int8" }],
"rows": [["paid", 1204], ["refunded", 37]],
"truncated": false,
"elapsedMs": 12
}- 按连接征求同意。 插件第一次查询某个连接时,宿主会弹窗写明插件名和连接名。用户允许后授权会持久保存,并显示在「插件中心 → 已安装」该插件下,用户可随时撤销;拒绝会在本次工作台会话内被记住。插件无法自行授权。
- 只读且单条语句。 宿主只接受一条被 DBX SQL 风险分类器判定为只读的语句——与 MCP 只读模式和 AI Agent 使用同一道闸门。写入、DDL、
SELECT … FOR UPDATE、多条语句以及USE这类切换会话数据库的语句都会被拒绝;请改用database参数。 - 只用已打开的连接。 与执行计划和表结构元数据 API 一样,DBX 不会替插件发起连接:已保存但未打开的连接会以
Connection is not open拒绝。Redis、MongoDB、搜索引擎等非 SQL 连接不提供该能力。 - 有上限。
maxRows默认 500、最多 5000;序列化后的行数据上限 8 MiB;timeoutMs会被限制在连接自身超时和 60 秒上限之内。只要有行被截断,truncated就为true。 - 不含凭据。 响应只包含列和行。授权被撤销后,调用会返回以
PLUGIN_DATA_ACCESS_NOT_GRANTED开头的错误,下一次查询会重新征求用户同意。
该 API 属于 Host API 1.4。请同时保留 engines.host_api 下限和 capabilities.dataApi 运行时检查;无法弹出确认框的 Web 宿主会拒绝请求,而不会静默授权。
系统剪贴板
插件沙箱是 opaque origin 且没有剪贴板权限,navigator.clipboard 在里面不可用。宿主代为桥接系统剪贴板:
window.dbxPlugin.copy(text)/window.dbxPlugin.clipboard.writeText(text)写系统剪贴板。两者走同一条无权限门控的桥接方法:写操作的输入就是用户数据本身,是低风险的一半。window.dbxPlugin.clipboard.readText()读系统剪贴板。它需要host.clipboard:read权限——读取会把用户数据(密码、令牌)交给插件代码,且没有后续用户交互兜底——属于 Host API 1.3:离开它无法工作的插件应声明"engines": { "host_api": "^1.3" }。
读剪贴板请看 init 能力而不是试探:capabilities.clipboardRead(写对应 capabilities.clipboardWrite)在旧宿主上缺失即为 falsy。读被拒绝(缺权限、Web 宿主没有原生剪贴板访问)时 Promise 会 reject——请优雅降级,例如回落到键盘粘贴路径,而不是让交互直接失败。
await window.dbxPlugin.ready;
const canRead = !!window.dbxPlugin.capabilities.clipboardRead;
const canWrite = !!window.dbxPlugin.capabilities.clipboardWrite;
async function pasteFromClipboard() {
if (!canRead) return null;
try {
return await window.dbxPlugin.clipboard.readText();
} catch {
// 缺权限或读取失败:保留键盘粘贴路径。
return null;
}
}文件传输与系统拖放
桌面端宿主通过 window.dbxPlugin.fileTransfer 把本地文件流式接入插件沙箱。句柄只在用户明确同意后打开:原生打开/保存对话框(pick / beginSave),或从系统拖放到本插件工作台区域内的文件(onDrop)。传输按分块进行,多 GB 文件也不会整体载入内存。
const fileTransfer = window.dbxPlugin.fileTransfer;
if (!fileTransfer) {
// Web 宿主:回退到 <input type="file"> 和 sidecar 落盘。
}
// 上传:用户在原生对话框选择文件,插件把分块推给自己的 sidecar。
const { files } = await fileTransfer.pick({ multiple: true });
for (const file of files) {
let offset = 0;
for (;;) {
const chunk = await fileTransfer.read(file.handleId, offset, 256 * 1024);
await uploadChunk(file.name, offset, chunk.dataBase64); // 你自己的 RPC
if (chunk.eof) break;
offset += chunk.length;
}
await fileTransfer.cancel(file.handleId);
}
// 下载:打开保存目标,分块写入,最后 finish 落盘。
const target = await fileTransfer.beginSave({ name: "export.csv", size });
await fileTransfer.write(target.handleId, 0, bytes);
await fileTransfer.finish(target.handleId);
// 系统拖放(仅桌面端):被拖入的文件以已打开的句柄送达。
const offDrop = fileTransfer.onDrop((files) => uploadAll(files));
const offDrag = fileTransfer.onDragState((active) => showDropOverlay(active));| API | 用途 |
|---|---|
pick({ multiple }) | 原生打开对话框;返回已打开的读句柄 { handleId, name, size, contentType }。 |
read(handleId, offset, length?) | 从读句柄流式读取一个分块 { dataBase64, length, eof }。 |
beginSave({ name, contentType?, size? }) | 原生保存对话框 + 写句柄;返回 { handleId, chunkBytes },用户取消时为 null。 |
write(handleId, offset, data) | 写入一个分块(二进制 transfer 或 base64);返回 { written, nextOffset }。 |
finish(handleId) / cancel(handleId) | 冲洗并关闭写句柄 / 丢弃任意句柄。 |
onDrop(fn) | 拖入本工作台的文件以读句柄送达;拖入文件夹会被展开为其中包含的文件(.git 等点开头文件与目录会被跳过),每个文件额外携带 relativePath(以 / 分隔、以拖入文件夹自身名字开头的路径,如 folder/nested/a.csv),可用于还原拖入的目录结构,也能区分不同拖入根目录下的同名文件。监听器签名为 (files, { dropId, truncated }):dropId 标识本次拖入的整组条目(取消与进度记账的分组键),truncated 为 true 表示展开因 2000 文件或 8 层深度上限被截断。 |
onDragState(fn) | 是否有系统拖拽悬在本工作台上方;用它驱动拖放遮罩。 |
桌面端与网页端的行为差异:请以 init 消息里 additive 的 capabilities.fileTransfer 标志位(pick、beginSave、read、drop、folderExpansion)做能力探测,不要靠试调用。桌面端 pick / beginSave 走原生对话框、onDrop 接收系统拖放(句柄由宿主惰性打开,读时分块流式,因此超大文件夹与大文件都不会占满句柄;选取文件数超过共享句柄池时自动降级为惰性条目,不会静默丢弃)。网页端 pick 回退到浏览器文件选择器、beginSave 回退为内存缓冲加浏览器下载、onDrop / onDragState 不会触发(drop/folderExpansion 为 false)、句柄也没有 relativePath。onDrop 只在拖放落点位于本插件工作台区域内时触发;落点在宿主其他区域时保持宿主自身的行为。
持久化 UI 状态
沙箱是 opaque origin,localStorage 在里面会直接抛错。需要跨重载或跨应用重启保留状态的工作台(最近访问的目录、面板尺寸、草稿等)改用 window.dbxPlugin.storage。它需要 host.storage 权限,并以 init 消息里的 capabilities.storage 广播;使用前请同时探测这两者。
if (window.dbxPlugin.capabilities.storage) {
const storage = window.dbxPlugin.storage;
await storage.set("layout", { sidebar: "collapsed", sort: "name" });
const layout = await storage.get("layout"); // { sidebar: "collapsed", sort: "name" },未设置时为 null
await storage.delete("layout");
}| API | 用途 |
|---|---|
get(key) | 返回存储的 JSON 值;键未设置时返回 null。 |
set(key, value) | 持久化任意 JSON 值(undefined 归一化为 null)。 |
delete(key) | 删除一个键;删除不存在的键同样成功。 |
键为不超过 256 字符的字符串;单个值序列化后上限 256 KiB,整库上限 1 MiB——这是 UI 状态存储,不是数据落盘通道。数据保存在插件自己的 plugin-data/<id> 目录里,按插件隔离,版本升级和卸载后都保留。大批量数据、缓存以及 Sidecar 产生的任何文件请放进 Sidecar 的数据目录(见 原生 Sidecar 和 RPC)。Web 宿主上同一 API 由浏览器本地存储支撑,两端代码完全一致。
在 DBX AI 中分析插件数据
在 manifest 中声明 "permissions": ["host.ai"]。插件可以直接打开全局 AI 面板,也可以为每个 Workbench 注册快捷推荐问题。两种能力都沿用 DBX 的模型选择、历史、追问、停止和导出功能。
声明 Workbench 的默认推荐问题
在 workbench contribution 下增加可选的 ai.recommendations。label 显示在 DBX 全局 AI 面板,prompt 是点击后发送给 AI 的文本;order 越小越靠前。
{
"type": "workbench",
"id": "com.example.files.main",
"label": "Files",
"ai": {
"recommendations": [
{
"id": "health",
"label": "检查 {{resource.name}} 健康状态",
"prompt": "分析 {{resource.kind}}/{{resource.name}} 的健康状态和风险",
"order": 10
}
]
}
}占位符从当前 Workbench context 读取,支持对象属性和数组下标,例如 {{resource.name}}、{{items.0.status}}。无法解析的占位符会使该条推荐被隐藏;__proto__、prototype 和 constructor 等路径会被拒绝。推荐问题最多显示 5 条。
根据资源变化更新推荐问题
插件切换页面或资源时,可以提交运行时推荐问题。运行时 items 会覆盖 manifest 默认值;传入空数组表示清空当前推荐。运行时 context 通常只需要提交资源字段,Host 会保留当前 Workbench 的 connectionId、数据库和实例标识,确保点击推荐问题后仍绑定到正确的插件连接。
function updateRecommendations(resource) {
if (!dbxPlugin.capabilities.aiRecommendations) return;
return dbxPlugin.ai.setRecommendations({
context: { resource },
items: [
{
id: "health",
label: `检查 ${resource.name} 健康状态`,
prompt: "分析 {{resource.kind}}/{{resource.name}} 的健康状态和风险",
order: 10,
},
{
id: "events",
label: "查看最近事件",
prompt: "列出这个资源最近的重要事件并给出处理建议",
order: 20,
},
],
});
}
await dbxPlugin.ai.clearRecommendations();推荐问题显示在 DBX 全局 AI 对话窗口中,而不是插件页面内新增一个 AI 入口。用户点击后,DBX 会使用当前推荐文本创建新的 Agent 对话并立即发送;Agent 会通过当前已打开的插件连接调用插件工具获取实时数据。推荐列表本身只属于当前 Workbench 上下文,不会写入对话历史;用户点击后发送的实际问题会正常保存。
直接打开 Ask 或 Agent 对话
ai.openConversation 仍适合插件按钮或其他明确动作。默认 mode 为 ask,宿主会复制插件提供的 JSON 快照;快照会随对话保存,后续资源刷新不会改变已经创建的对话。需要实时工具时显式使用 mode: "agent",并在 context 中提供当前已打开插件连接的 connectionId:
await dbxPlugin.ai.openConversation({
title: "Watchlist analysis",
prompt: "Compare the current quotes and explain risks and missing data.",
context: {
connectionId,
resource: { kind: "watchlist", name: "primary" },
dataTimestamp,
},
mode: "agent",
send: true,
});title 最多 200 字符,prompt 最多 32000 字符,context 必须是纯 JSON 对象,上限 2 MiB。Agent 只能使用宿主为该插件连接公开的工具;插件不会收到模型回复或模型配置,也不能借此获得数据库执行能力。旧插件不声明推荐问题时仍可正常使用原有 AI API;旧宿主缺少新增 capability 字段时,插件应关闭对应入口并继续提供其他功能。
浮动窗口
浮动窗口把插件的某个 workbench 放进一个独立的无边框、置顶桌面窗口,而不是塞在 DBX 外壳里——这正是小型常驻小组件需要的表面(播放器胶囊、会话状态徽章、快捷选择器)。它会盖在其他应用之上,可以被拖到桌面任意位置。在屏幕中间松手就停在落点;落到或越过工作区边缘(不管怼得多用力)都会齐着那条边吸附并停靠。停靠不会在落下的那一刻就把小组件藏起来:它先保持整颗可读的胶囊,光标离开约 2.5 秒后才滑到屏外只留一条细边,光标碰到细边就滑回来。重新打开一个上次吸在边上的小组件也是这套时序(先胶囊、后收起);刚唤出的小组件即使默认落点就贴着边,也只停在胶囊态。使用前先判断 capabilities.floating:Web 宿主没有窗口,桌面宿主也可能不支持窗口管理。
if (window.dbxPlugin.capabilities.floating) {
// 在标签或 dock 工作台里:打开小组件。
// 一个(插件, workbench contribution)只会有一个窗口,重复调用只是把已有窗口带到前台。
const { windowId } = await window.dbxPlugin.floating.open({
contributionId: "capsule",
title: "Now playing",
width: 260,
height: 60,
context: { queueId: currentQueueId },
});
}
// 在小组件内部(context.surface === "window"):控制自己所在的窗口。
grip.addEventListener("pointerdown", (event) => {
grip.setPointerCapture(event.pointerId);
dragging = false;
});
grip.addEventListener("pointermove", async (event) => {
if (dragging || Math.hypot(event.clientX - downX, event.clientY - downY) < 4) return;
dragging = true;
// 从这一刻起窗口归宿主移动:它自己读原生光标,不需要插件逐帧上报。
await window.dbxPlugin.floating.beginDrag();
});
grip.addEventListener("pointerup", async () => {
if (!dragging) return;
dragging = false;
await window.dbxPlugin.floating.endDrag(); // 吸附并记住位置
});| API | 作用 |
|---|---|
floating.open(options) | 为本插件的某个 workbench contribution 打开(或前置)浮动窗口。options:contributionId(必填)、title、逻辑像素的 width/height、物理屏幕像素的 x/y、alwaysOnTop(默认 true)、skipTaskbar(默认 true)、resizable(默认 false)、context。返回 { windowId, reused };需要 host.workbench。 |
floating.close(contributionId?) | 关闭本插件的浮动窗口——指定某个 contribution,或省略 id 关闭本插件的全部浮动窗口。在小组件内部不带参数调用即关闭自己。 |
floating.beginDrag() | 把窗口交给宿主的拖动循环。在 pointermove 里、自己的拖动阈值越过之后调用(此时按键还按住)。之后由宿主自己读原生光标,插件没有逐帧调用要做。 |
floating.endDrag({ snap? }) | 结束拖动。snap(默认 true)时宿主把窗口齐着它到达或越过的工作区边缘吸附并停靠在那(先保持整颗胶囊,光标离开一会儿才收起到屏外、只留一条悬停细边),落点在屏幕中间则完整夹回屏内;位置与停靠边都会记住,供下次打开使用。返回物理像素的最终矩形。实践中可不调:宿主在光标静止后也会自己收尾,收不到 pointerup 的小组件不会把窗口一直粘在光标上。 |
floating.setSize(width, height) | 以逻辑像素调整所在窗口大小;宿主会夹到自己的上下限内。 |
小组件决定拖动何时开始(哪个元素是拖拽把手、指针要先走多远);窗口由宿主移动,吸附与位置记忆也归宿主。刻意没有逐帧的桥调用:宿主轮询原生光标,webview 忙不过来时小组件也照样贴着指针走,光标静止即收尾,pointerup 丢了也不会一直拖下去。几何类调用(beginDrag/endDrag/setSize)只在浮动窗口内部有效,在其他表面调用会被拒绝,因此标签页永远无法移动它不在其中的窗口。
几条规则让这个表面的行为可预期:
- 一个 contribution 只有一个窗口。 窗口 id 由插件 id 和 contribution id 推导,所以
floating.open是幂等的:第二次调用会把已开的窗口带到前台,而不是叠出更多小组件。 - 窗口是不透明的。 宿主自己不画任何外壳,用户看到的就是小组件本身:给页面一个背景加圆角,配合宿主按内容收好的窗口尺寸,读起来就是浮在桌面上的一张卡片。刻意不提供逐像素透明(带镂空角的塑形小组件)——透明区域还收不收鼠标事件各平台不一致,而一个要给用户拖的小组件必须处处点得动。
- 外壳导航会被转发。 浮动窗口没有标签栏、dock 和面板,所以从它发起的
openWorkbench和openFilesystem会在 DBX 主窗口里打开,主窗口会自行前置。executeCommand在这个表面完全不提供:每个命令动作都要打开一个外壳表面,调用会成功返回但什么都看不见。 - 位置跨重启保留,按插件和 contribution 分别记录;当记录的位置已经不在任何已连接的显示器上时会被丢弃(拔掉显示器后退化为工作区默认位置,而不是开到屏幕外)。
- 小组件就是一个完整工作台。 它拿到和标签页相同的桥、权限、上下文契约和 sidecar RPC;
context.surface为"window",context.workbenchId为窗口 id。它启动的是你 UI 的另一份实例,所以共享状态必须走 sidecar(或storage),不能指望模块内存。
主题、布局和静态资源
DBX 将主题 tokens 注入文档根元素,并更新 data-dbx-theme。自定义组件库(包括 Svelte、shadcn-svelte)应使用这些变量,不要依赖父页面 CSS 自动穿透 iframe:
body {
margin: 0;
background: var(--color-background, #fff);
color: var(--color-foreground, #18181b);
}
button {
background: var(--color-primary, #2563eb);
color: var(--color-primary-foreground, #fff);
cursor: pointer;
}使用 window.addEventListener("dbx-plugin-env", handler) 响应语言或主题变化。先读取 dbxPlugin.locale/theme 初始化,再在事件中刷新自己的状态。DBX 提供轻量 dbx-btn、dbx-input 等样式,但开发宿主不模拟完整组件 kit,自定义 token 样式在两者中更一致。
主题负载携带什么
env 消息的 theme 结构为 { appearance, tokens, editor? }。appearance 是 "light" | "dark";tokens 是按前缀扫取的根节点设计变量(--color-*、--radius-*、--font-*);editor 是无 CSS 令牌载体的 SQL 编辑器设置结构化快照。投递矩阵:
| DBX 设置 | 投递形式 |
|---|---|
| 浅色/深色/跟随系统 | theme.appearance |
| 调色板 + 自定义配色 | --color-* tokens |
| 圆角风格 | --radius-* tokens |
| UI 字体 | --font-sans token |
| 编辑器(等宽)字体 | --font-mono token + theme.editor.fontFamily |
| 编辑器字号 | theme.editor.fontSize |
| SQL 编辑器语法主题 | theme.editor.theme |
| UI 缩放(宿主窗口级) | 不投递——插件自管缩放 |
| 数据网格字体/类型配色、编辑器背景图 | 不投递——DB 域(--dbx-* 不在扫取范围) |
字体"字号"类关注点归插件所有:宿主不规定你的字号;如适合你的 UI,可把 theme.editor.fontSize 当默认值消费。所有负载变化都经 env 通道实时推送——字体族变更走主题修订号,字号/语法主题走显式监听——无需刷新。
UI 必须构建为包内静态资源,不要把 Vite 开发服务器或 CDN 的脚本地址写入发行包。宿主把入口及资源转换成沙箱可加载内容;CSP 报错时检查构建产物是否存在、路径是否落在 ui.root 内,并使用匹配版本的 CLI 复现。用插件自己的对话框组件处理删除/重命名,不要依赖沙箱中的原生 alert/confirm/prompt。
懒加载与代码分割
宿主会把入口脚本内联进沙箱文档,ui/ 下的其余资源在运行时通过 dbx-plugin 协议保持可寻址:dbx-plugin://localhost/<插件id>/<ui.root内相对路径>(WebView2 上映射为 http://dbx-plugin.localhost/<插件id>/…)。宿主会把平台对应的形式注入为沙箱文档的 <base>,并放行资源类 CSP,因此打包后的插件可以正常动态 import() 代码分割的 chunk——前提是构建产物使用相对资源地址:Vite 设 base: "./"、webpack 设 publicPath: "./",让 chunk 和 CSS 资源地址基于注入的 base 解析而不是宿主 origin。绝对路径 /assets/… 携带不了插件 id,会 404。该协议只读文件、限定在每个插件的 ui.root 内,且插件更新后不会沿用旧缓存。
内联后的样式表
宿主会读取所有指向 ui.root 内部的 <link rel="stylesheet">,替换成带同样属性的 <style> 元素,因此插件运行时仍然能找到自己的样式表(document.querySelectorAll("style[data-skin-css]"))并切换它们。有一处差异需要注意:disabled 在 <style> 元素上不会像 <link> 那样从内容属性反映到 IDL,宿主重新序列化时也无法用 IDL 属性把它带过去。所以自带 disabled 样式表的插件必须在加载后自己重新设置 sheet.disabled = true;其余属性(id、data-* 标记、media)都会原样保留。
全屏
沙箱里的 requestFullscreen() 取决于框架的 Permissions-Policy,宿主已经做了委派(allow="clipboard-write; fullscreen")。插件无法自己授予该策略,因此应当用 document.fullscreenEnabled 做特性检测,为 false 时退化成页面内的最大化布局——否则在不委派该策略的旧宿主上,每次全屏请求都会被拒绝。
原生 Sidecar 和 RPC
只有浏览器沙箱无法完成的能力才需要原生后端,例如 S3/SSH 协议、系统凭据、长连接或高性能计算。Sidecar 不是操作系统沙箱,会以当前用户权限运行。
Rust 和 Go SDK 都实现 Sidecar Protocol v1:
{"jsonrpc":"2.0","id":1,"method":"plugin/initialize","params":{"host":{"protocolVersions":[1]}}}
{"jsonrpc":"2.0","id":2,"method":"objects/list","params":{"prefix":"docs/"}}响应必须使用相同的 id,成功返回 result,失败返回 JSON-RPC error。初始化响应至少包含 protocolVersion、capabilities 和 plugin: { id, version };ID 和版本必须与 Manifest 一致,协议必须在双方支持范围内,否则会出现 Sidecar identity or protocol does not match manifest。初始化 capabilities 描述后端能力,不等于 Manifest 的 Host 权限列表。
use dbx_plugin_sdk::{PluginEmitter, PluginError, PluginHandler, PluginMetadata, PluginServer, RequestContext};
use serde_json::{json, Value};
struct Plugin;
impl PluginHandler for Plugin {
fn handle(&self, _context: RequestContext, method: &str, _params: Value, _emitter: &PluginEmitter) -> Result<Value, PluginError> {
match method {
"example/ping" => Ok(json!({ "ok": true })),
_ => Err(PluginError::method_not_found(method)),
}
}
}
fn main() -> std::io::Result<()> {
let metadata = PluginMetadata::new("com.example.files", env!("CARGO_PKG_VERSION"));
PluginServer::new(metadata, Plugin).serve()
}Sidecar 约束:stdout 只能输出协议消息,日志写 stderr;请求 ID 必须支持并发关联;连接和断开应幂等;长任务应设计超时、取消和分块确认;不要在事件、context 或错误消息中泄露 Secret。
每个 Sidecar 启动时都会带上 DBX_PLUGIN_DATA_DIR 环境变量,指向为本插件保留的持久化目录(plugin-data/<id>,与安装包目录平级)。它在版本升级和卸载后都保留,插件的所有 Sidecar 运行共享;缓存、Token、后端管理的数据库等应放在这里——首次写入前需自行创建目录,宿主不会预创建。Go SDK 提供了 dbxpluginsdk.DataDir() / EnsureDataDir() 封装。工作台 UI 需要小体量持久化时走同一目录里的 host.storage(见 持久化 UI 状态),不要直接读写这些文件。
上例可替换 Rust 模板的 backend/src/main.rs,同时让 Cargo 包版本与 Manifest 一致。Go 使用模板生成的 dbxpluginsdk.NewServer(metadata, handler).Serve();当前 Go SDK 支持 JSONL,Rust SDK 还支持 framed transport。不要仅修改 Manifest transport 就认为 Go 已具备二进制帧处理能力。完整接口见 Rust SDK 和 Go SDK。
为 DBX AI 助手提供工具
带原生后端的插件可以暴露工具,供 DBX 内置 AI 助手在 Agent 模式下调用。这样「订单接口为什么变慢了?」这类问题就能把数据库工具和插件提供的指标、日志或 Shell 能力结合起来。Sidecar 需要实现两个方法:
mcp/tools以 MCP 工具格式返回{ "tools": [{ "name", "description", "inputSchema", "annotations"? }] }。DBX 会传入正在绑定的{ "connectionId" },插件可以据此在只读连接上隐藏写工具。mcp/call接收{ "tool", "arguments", "lifecycle" },返回 MCPCallToolResult({ "content": [{ "type": "text", "text": "..." }], "isError": false })。lifecycle与已打开连接的connection/connect载荷相同——凭据由宿主解析,运行端点已经过 SSH 或代理层——因此工具参数里永远不需要密钥。
宿主在此之上负责:
- 显式加入、可撤销。 插件只有在用户于「插件中心 → 已安装 → 内置 AI 工具」中开启后才会贡献工具——仅安装不会暴露任何东西。同一面板会列出工具并标出哪些需要确认,开关可把某个插件永久关闭。manifest 中的
mcp贡献声明ai_tools: false可让插件即使被用户开启也不进入该面。 - 只用已打开的连接。 只为用户当前已打开的插件连接提供工具。连接由 DBX 绑定:模型看到的 schema 中会去掉
connectionId/connectionName;如果插件 schema 声明了connectionId,宿主会在转发参数中注入绑定连接的 id。打开了多个连接时,模型通过额外的dbx_connection参数选择其一。 - 默认需要确认。 只有声明了
"annotations": { "readOnlyHint": true }的工具才会直接执行。其他调用都会暂停运行,并向用户展示将要转发的完整参数,由用户选择允许一次或拒绝;五分钟未响应视为拒绝。请为真正只读的工具加上标注,同时保留插件自身的防护(只读连接标志、两阶段确认等)——宿主确认拦截的是模型的误操作,不能替代插件自己的安全规则。 - 可移植的 schema。 工具以
<前缀>__<工具名>暴露(例如ssh__ssh_exec),schema 会被收敛到所有支持的模型供应商都接受的子集:type、description、properties、required、items、字符串enum以及数值、长度和元素数量边界。参数名请只使用字母、数字和下划线。 - 结果有上限。 单次调用 120 秒超时,结果在交给模型前会被压缩。长任务请作为后台任务启动并返回句柄,不要阻塞。
内置 Agent 会把工具输出当作不可信数据处理。结果里请返回事实而不是指令,也不要回显任何密钥。
同样的工具也会出现在外部 dbx MCP 服务器(Claude Code、Cursor 等)上,名称为 dbx_<前缀>__<工具名>,并受全局 MCP 设置(工具白名单 + 连接白名单)约束、绑定到插件已保存的连接。该面按作者声明加入:manifest 中的 mcp 贡献声明 external_tools: true 才会暴露;连接选择参数为 dbx_connection。
创建插件
直接安装预编译 CLI,不需要 Rust,也不需要克隆 DBX 源码:
npm install --global @dbx-app/plugin-cli
dbx-plugin --help也可以直接使用 npx:
npx @dbx-app/plugin-cli create my-ui-plugin --template frontendnpm 包会自动选择当前平台的预编译二进制,并携带匹配版本的 Rust/Go 插件 SDK。纯前端插件不需要 Rust 或 Go;原生编译环境只用于构建插件自己的 Sidecar。
创建项目:
# 纯前端、跨平台
dbx-plugin create my-ui-plugin --template frontend
# Svelte + Vite、跨平台
dbx-plugin create my-svelte-plugin --template svelte
# Rust Sidecar + 前端
dbx-plugin create my-rust-plugin --template rust
# Go Sidecar + 前端
dbx-plugin create my-go-plugin --template go四种模板的选择:frontend 适合原生 HTML 或自行接入 Vue/React 的静态 UI;svelte 自带 Svelte 5 + Vite;rust 和 go 额外生成原生 Sidecar。--backend none|svelte|rust|go 可以作为模板别名,--language rust|go 是原生项目兼容别名。脚本或 CI 中可以用 --yes 跳过交互向导:
dbx-plugin create my-plugin \
--template svelte \
--id com.example.my-plugin \
--name "My Plugin" \
--publisher example \
--description "A DBX plugin." \
--version 0.1.0 \
--yesSvelte 模板构建脚本会把 Vite 输出到 ui/,并在成功构建后输出 DBX_UI_BUILD_SUCCESS:
cd my-svelte-plugin
npm install
npm run build
dbx-plugin dev --port 5190生成项目后,先在自己的插件源码仓库开发、测试和维护版本记录。不要为了发布一个普通插件而把整个源码目录复制进 t8y2/dbx。
构建候选包
在插件项目根目录执行:
dbx-plugin package .该命令只生成未签名审核候选包:
dist/<plugin-id>-<version>-<target>.dbxp
dist/<plugin-id>-<version>-<target>.artifact.json前端-only 项目默认使用 universal target;Rust/Go 项目使用当前平台 target。--output-dir 可以改变输出目录,--artifact-url 可以写入发布后的 HTTPS 地址:
dbx-plugin package . --target universal --output-dir dist --artifact-url https://downloads.example.com/my-plugin.dbxpCLI 会只打包 dbx-plugin.toml 的 [package].include 声明目录,并拒绝符号链接、越界路径、.dbx-dev、过大的文件和不安全的输出位置。不要把 dist/ 或开发数据再次作为输入目录。
官方插件作者不创建或持有 DBX Store 私钥。生成的 GitHub Release 工作流会按目标平台构建候选包,并合并产生 release-candidates.json。
本地测试未签名包时,需要在插件中心显式开启“允许安装未签名开发包”。该选项不会放宽官方商店安装校验。
本地调试和验证
需要实时观察前端时,在插件项目根目录运行独立开发宿主;它不会启动 DBX 桌面端:
dbx-plugin dev --path . --port 5190打开终端输出的 http://127.0.0.1:5190/。--port 0 或端口被占用时会选择可用端口,--data-dir 可以把开发数据移出项目目录:
dbx-plugin dev --path . --data-dir /tmp/my-plugin-devdbx-plugin dev 不会自动安装插件依赖;前端构建由 dbx-plugin.toml 的 [dev] 命令决定:
[dev]
ui_build = ["npm", "run", "build"]
ui_watch = ["npm", "run", "build:watch"]配置 ui_watch 时,命令只有在一次完整成功构建后输出独立一行 DBX_UI_BUILD_SUCCESS,开发宿主才会重载 UI。不要在失败路径或无条件的退出钩子中输出这个标记。
调试页可以切换语言、主题、自动重载和日志面板。日志只保留内存中的最近 500 条,密码、Token 和 Manifest 中的 Secret 字段会脱敏,二进制和过大的值会省略或截断;开发目录 .dbx-dev/ 可能包含明文凭据,必须加入 .gitignore,不要提交或共享。
需要从脚本读取脱敏日志时:
curl -sS 'http://127.0.0.1:5190/api/diagnostics?after=0&limit=100&level=error'后续请求使用响应里的 nextAfter 和 instanceId。开发宿主只模拟受支持的 Host API 子集;安装、签名、真实 Secret Store、桌面端生命周期和生产权限必须再用 DBX 验证。
本地验收清单
- 关闭未使用的权限,确认 Manifest 中的权限与实际调用一致。
- 用空配置、错误凭据、超时、断网和 Sidecar 重启分别测试失败路径。
- 测试 DBX 浅色/深色主题和
zh-CN/en;不要依赖宿主替你翻译插件 UI。 - 测试重新连接、关闭工作台、重复打开同一工作台以及上下文为空的情况。
- 检查包内所有资源路径、可执行文件权限和最终包是否包含不应发布的文件。
提交官方商店的完整流程
第一步:发布源码和候选 Release
在插件自己的源码仓库创建版本 Tag 和 GitHub Release。Release 中应包含:
- 每个平台的未签名
.dbxp候选包; - 每个候选包对应的
.artifact.json; - 汇总后的
release-candidates.json; - Release Notes 和对应源码 Tag。
候选包可以放在插件源码仓库的 GitHub Releases、CDN 或对象存储中,但不要把 .dbxp 提交到 Git 历史。
第二步:让 dbx-store 自动创建候选 PR
dbx-store 的同步 Workflow 会定期检查已在 automation/plugin-sources.json 注册且 autoUpdate: true 的公开插件仓库。它读取最新 Release 的 release-candidates.json,创建或更新唯一的:
candidates/<plugin-id>.json因此,已启用自动更新的插件作者不需要每个版本手工创建 Issue 或 PR,也不需要在插件仓库配置商店签名 Secret。插件仓库的 Release、候选 metadata 和可访问的源码 Tag 是同步所需的主要信息。首次上架或需要修改商店展示字段时,才按 dbx-store 的贡献指南 提供发布者和候选信息。
.dbx-store.json 是可选的商店元数据文件:首次登记或主动修改商店名称、标签、许可证、主页等信息时使用;仅发布新版本时不需要重复修改它。它不是插件运行时 Manifest,也不能放签名私钥。
例如:
{
"name": "Example Files",
"description": "Browse files from an example service.",
"icon": "assets/plugin.svg",
"tags": ["files", "storage"],
"permissions": ["host.workbench"],
"source": "https://github.com/example/dbx-plugin-files",
"homepage": "https://github.com/example/dbx-plugin-files",
"license": "Apache-2.0",
"releaseNotes": "Initial release."
}允许的商店字段是 name、description、icon、tags、permissions、source、homepage、license、releaseNotes 和 localizations;icon 可以是包内相对路径,自动同步时会转换成源码 Tag 下的 HTTPS 地址。autoUpdate 是 dbx-store/automation/plugin-sources.json 的注册字段,不是这个文件的字段。
自动同步只创建或更新候选 PR,不会自动批准、签名或合并。候选 PR 的目标仓库是 t8y2/dbx-store,目标分支是 main,而不是 t8y2/dbx。
如果插件还没有登记到自动同步列表,首次上架可以手动 Fork dbx-store,向 main 提交一个 PR,包含发布者首次需要的 publishers/<publisher-id>.json 和 candidates/<plugin-id>.json。不需要先创建单独的 Issue;CI 会在签名前有意保持 open candidate(s) awaiting DBX Store signing,签名 Workflow 回写正式目录后才会变绿。插件作者可以在 dbx-store 提交登记自动同步源的 PR,或请维护者登记;无需在插件仓库增加 App 私钥。
候选文件的关键结构如下。它只引用插件仓库自己的未签名 Release 资产,不能填写官方 signingKeyId:
{
"schemaVersion": 1,
"id": "com.example.files",
"publisher": "example",
"version": "0.1.0",
"name": "Example Files",
"source": "https://github.com/example/dbx-plugin-files/tree/v0.1.0",
"license": "Apache-2.0",
"targets": [{
"target": "universal",
"url": "https://github.com/example/dbx-plugin-files/releases/download/v0.1.0/com.example.files-0.1.0-universal.dbxp",
"sha256": "<64 位十六进制 SHA-256>",
"size": 123456
}]
}新版本仅提交新增版本;生成的 plugins/<plugin-id>.json 和 catalog/index.json 不要手工编辑。
第三步:DBX Store 审核并签名
维护者会检查源码、Manifest、权限、候选 SHA-256、大小和原生行为。审核通过后,受保护的 DBX Store 工作流会:
- 按已审核 SHA-256 和大小下载候选包;
- 确认候选包没有已有签名,并核对插件 ID 和版本;
- 使用 DBX Store 仓库密钥添加 Ed25519 签名;
- 发布最终
.dbxp、最终 artifact metadata 和 signing receipt。
开发者不会接触官方仓库私钥。
第四步:合并候选 PR
维护者运行受保护的签名 Workflow 后,Workflow 会把最终的 plugins/<plugin-id>.json 和生成的 catalog/index.json 回写到同一个候选 PR,并将签名包、artifact metadata 和 signing receipt 发布到 dbx-store Release。校验通过后,维护者再审核并合并这个 PR。
上架或更新 PR 中不要包含:
.dbxp二进制;- 插件完整源码副本;
- Ed25519 私钥、Token 或下载凭据;
- 未经 DBX Store 签名的候选包 URL;
- 自行伪造的
verified: true。
更新已有插件
每次更新都发布新语义版本,不允许覆盖旧版本的 Release 资产:
- 修改插件源码,并递增
manifest.json的version;不要复用旧版本号。 - 发布新的源码 Tag 和候选 Release;旧 Release 资产不可覆盖。
- 等待同步 Workflow 创建或更新
automation/plugin-release/<plugin-id>/<version>候选 PR。 - 维护者审核、签名并合并该 PR;目录随后包含新版本和对应平台包。
如果仓库未启用自动同步,才需要按照 dbx-store 贡献指南手动提交 candidates/<plugin-id>.json。新版本只提交新版本候选信息,不要手工重写已生成的 plugins/ 或 catalog/ 文件。
插件代码问题继续在插件源码仓库修复;只有目录、审核状态、最终下载地址、哈希、大小或商店文案变更才进入 dbx-store。
签名和发布者身份
DBX 当前使用单一仓库签名:
publisher表示作者归属和商店记录;signingKeyId表示发布最终安装包的仓库密钥;- 官方插件由 DBX Store 审核后统一签名;
- 自定义或私有仓库由仓库运营方维护自己的密钥;
- 开发者签名或双签名不是当前上架要求。
人工审核决定插件能否展示,Ed25519 仓库签名保证审核后的安装包没有被替换。签名不能替代源码审查,也不是原生 Sidecar 的操作系统沙箱。
官方商店插件作者不要运行 keygen,也不要自行填写 signingKeyId。如果你运营自己的私有插件仓库,才使用 CLI 的高级签名工具:
dbx-plugin keygen company.plugins.release
source .dbx-repository-signing-key.env该命令生成 32 字节 Ed25519 seed 的受保护环境文件(Unix 权限为 0600),并输出 Key ID 和 Base64 公钥;私钥文件绝不能提交 Git。自定义仓库还必须把公钥加入自己的 DBX 信任配置。官方商店的私钥只存在 dbx-store 受保护的 Environment Secret 中,公钥由 DBX 客户端随官方信任列表发布。
artifact.json 中的 target、url、sha256 和 size 绑定确切字节;官方签名后才会增加 signingKeyId。已发布资产不可覆盖,任何字节变化都必须递增插件版本并重新审核。