跳转到内容

插件开发指南

适用于 OxideTerm v1.6.2+(Plugin API v3 — 2026-03-15 更新)

OxideTerm 插件系统遵循以下设计原则:

  • 运行时动态加载:插件以 ESM 包的形式在运行时通过 Blob URL + dynamic import() 加载,不需要重新编译宿主应用
  • 膜式隔离 (Membrane Pattern):插件通过 Object.freeze() 冻结的 PluginContext 与宿主通信,所有 API 对象均为不可变的
  • 声明式 Manifest:插件的能力(tabs、sidebar、terminal hooks 等)必须在 plugin.json 中预先声明,运行时强制校验
  • 失败安全 (Fail-Open):Terminal hooks 中的异常不会阻塞终端 I/O,而是回退到原始数据
  • 自动清理:基于 Disposable 模式的自动资源回收,插件卸载时所有注册自动清除
┌──────────────────────────────────────────────────────────────────┐
│ OxideTerm 宿主应用 │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────────┐ │
│ │ Rust Backend │ │ Tauri IPC │ │ React Frontend │ │
│ │ │ │ Control │ │ │ │
│ │ plugin.rs │←→│ Plane │←→│ ┌───────────────────┐ │ │
│ │ - list │ │ │ │ │ pluginStore │ │ │
│ │ - read_file │ │ │ │ │ (Zustand) │ │ │
│ │ - config │ │ │ │ └───────┬───────────┘ │ │
│ └─────────────┘ └──────────────┘ │ │ │ │
│ │ ┌───────▼───────────┐ │ │
│ │ │ pluginLoader │ │ │
│ │ │ - discover │ │ │
│ │ │ - validate │ │ │
│ │ │ - load / unload │ │ │
│ │ └───────┬───────────┘ │ │
│ │ │ │ │
│ │ ┌───────▼───────────┐ │ │
│ │ │ Context Factory │ │ │
│ │ │ (buildPluginCtx) │ │ │
│ │ │ → Object.freeze │ │ │
│ │ └───────┬───────────┘ │ │
│ │ │ │ │
│ └──────────┼──────────────┘ │
│ │ │
│ ┌──────────────────────────────────▼────────────┐ │
│ │ Plugin (ESM) │ │
│ │ │ │
│ │ activate(ctx) ←── PluginContext (frozen) │ │
│ │ ctx.connections ctx.events ctx.ui │ │
│ │ ctx.terminal ctx.settings ctx.i18n │ │
│ │ ctx.storage ctx.api ctx.assets │ │
│ │ ctx.sftp ctx.forward │ │
│ │ ctx.sessions ctx.transfers ctx.profiler │ │
│ │ ctx.eventLog ctx.ide ctx.ai ctx.app │ │
│ │ │ │
│ │ window.__OXIDE__ │ │
│ │ React · ReactDOM · zustand · lucideIcons │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘

关键点

  1. 插件与宿主运行在同一个 JS 上下文中(非 iframe/WebWorker)
  2. 通过 window.__OXIDE__ 共享 React 实例,确保 hooks 兼容
  3. Rust 后端负责文件 I/O(带路径遍历保护),前端负责生命周期管理
  4. Event Bridge 将 appStore 的连接状态变更桥接为插件事件
层级机制说明
膜式隔离Object.freeze()所有 API 对象不可修改、不可扩展
Manifest 声明运行时校验未声明的 tab/panel/hook/command 注册时抛异常
路径保护Rust validate_plugin_id() + validate_relative_path() + canonicalize防止路径遍历攻击
API 白名单contributes.apiCommands限制插件可调用的 Tauri 命令(Advisory
断路器10 次错误 / 60 秒 → 自动禁用防止故障插件拖垮系统
时间预算Terminal hooks 5ms 预算超时计入断路器

  • 开发 OxideTerm 插件不需要额外的构建工具
  • 插件是纯 ESM JavaScript 文件,直接被 OxideTerm 动态导入
  • 如需 TypeScript,可自行编译为 ESM;项目提供了独立类型定义文件 plugin-api.d.ts(见 20. 类型参考
  • 如需打包(多文件→单文件),可使用 esbuild / rollup(format: esm

方式一:通过 Plugin Manager 创建(推荐)

Section titled “方式一:通过 Plugin Manager 创建(推荐)”
  1. 在 OxideTerm 中打开 Plugin Manager(侧边栏 🧩 图标 → Plugin Manager)
  2. 点击右上角的 新建插件 按钮(+ 图标)
  3. 输入插件 ID(小写字母、数字和连字符,如 my-first-plugin)和显示名称
  4. 点击 创建
  5. OxideTerm 会自动在 ~/.oxideterm/plugins/ 下生成完整的插件骨架:
    • plugin.json — 预填好的清单文件
    • main.js — 带有 activate()/deactivate() 的 Hello World 模板
  6. 创建完成后插件自动注册到 Plugin Manager,点击 Reload 即可加载

步骤 1:创建插件目录

Terminal window
mkdir -p ~/.oxideterm/plugins/my-first-plugin
cd ~/.oxideterm/plugins/my-first-plugin

插件目录名不需要与 plugin.json 中的 id 一致,但建议保持相同以便管理。

步骤 2:编写 plugin.json

{
"id": "my-first-plugin",
"name": "My First Plugin",
"version": "0.1.0",
"description": "A minimal OxideTerm plugin",
"author": "Your Name",
"main": "./main.js",
"engines": {
"oxideterm": ">=1.6.0"
},
"contributes": {
"tabs": [
{
"id": "hello",
"title": "Hello World",
"icon": "Smile"
}
]
}
}

步骤 3:编写 main.js

// 从宿主获取 React(必须使用宿主的 React 实例!)
const { React } = window.__OXIDE__;
const { createElement: h, useState } = React;
// Tab 组件
function HelloTab({ tabId, pluginId }) {
const [count, setCount] = useState(0);
return h('div', { className: 'p-6' },
h('h1', { className: 'text-xl font-bold text-foreground mb-4' },
'Hello from Plugin! 🧩'
),
h('p', { className: 'text-muted-foreground mb-4' },
`Plugin: ${pluginId} | Tab: ${tabId}`
),
h('button', {
onClick: () => setCount(c => c + 1),
className: 'px-4 py-2 rounded bg-primary text-primary-foreground hover:bg-primary/90',
}, `Clicked ${count} times`),
);
}
// 激活入口
export function activate(ctx) {
console.log(`[MyPlugin] Activating (id: ${ctx.pluginId})`);
ctx.ui.registerTabView('hello', HelloTab);
ctx.ui.showToast({ title: 'My Plugin Activated!', variant: 'success' });
}
// 停用入口(可选)
export function deactivate() {
console.log('[MyPlugin] Deactivating');
}

方式一:手动安装(开发模式)

  1. 确保插件文件放在 ~/.oxideterm/plugins/my-first-plugin/
  2. 在 OxideTerm 中打开 Plugin Manager(侧边栏 🧩 图标 → Plugin Manager)
  3. 点击 Refresh 按钮扫描新插件
  4. 插件将自动加载并显示在列表中
  5. 在侧边栏中可以看到插件的 Tab 图标,点击打开 Tab

方式二:从注册表安装(推荐)

  1. 在 Plugin Manager 中切换到 浏览 标签页
  2. 搜索或浏览可用插件
  3. 点击 安装 按钮
  4. 插件将自动下载、验证并安装
  5. 安装完成后插件自动激活

方式三:更新已安装插件

  1. 浏览 标签页中,已安装插件如有更新会显示 更新 按钮
  2. 点击 更新 按钮
  3. 旧版本将被卸载,新版本自动安装并激活

卸载插件

  1. 已安装 标签页中找到要卸载的插件
  2. 点击插件行右侧的 🗑️ 按钮
  3. 插件将被停用并从磁盘删除

调试提示:

  • 打开 DevTools(Cmd+Shift+I / Ctrl+Shift+I)查看 console.log 输出
  • 插件加载失败会在 Plugin Manager 中显示红色错误状态,并附带可操作的错误提示(如 “activate() must resolve within 5s”、“ensure your main.js exports an activate() function” 等)
  • 每个插件在 Plugin Manager 列表中都有 日志查看器(📜 图标),可实时查看插件的激活、卸载、错误等生命周期日志,无需打开 DevTools
  • 修改代码后,在 Plugin Manager 中点击插件的 Reload 按钮热重载

v1 单文件 Bundle(默认)

~/.oxideterm/plugins/
└── your-plugin-id/
├── plugin.json # 必需:插件清单
├── main.js # 必需:ESM 入口(由 manifest.main 指定)
├── locales/ # 可选:i18n 翻译文件
│ ├── en.json
│ ├── zh-CN.json
│ ├── ja.json
│ └── ...
└── assets/ # 可选:其他资源文件
└── ...

v2 多文件 Packageformat: "package"):

~/.oxideterm/plugins/
└── your-plugin-id/
├── plugin.json # 必需:manifestVersion: 2, format: "package"
├── src/
│ ├── main.js # ESM 入口(支持模块间相对 import)
│ ├── components/
│ │ ├── Dashboard.js
│ │ └── Charts.js
│ └── utils/
│ └── helpers.js
├── styles/
│ ├── main.css # 声明在 manifest.styles 中自动加载
│ └── charts.css
├── assets/
│ ├── logo.png # 通过 ctx.assets.getAssetUrl() 访问
│ └── config.json
└── locales/
├── en.json
└── zh-CN.json

v2 多文件包通过内置的本地 HTTP 文件服务器(127.0.0.1,OS 分配端口)加载,支持文件间的标准 ES Module import 语法。

路径约束

  • 所有文件路径相对于插件根目录
  • 禁止 .. 路径遍历
  • 禁止 绝对路径
  • 插件 ID 中禁止 /\.. 和控制字符
  • Rust 后端会对解析后的路径做 canonicalize() 检查,确保不逃逸出插件目录

这是插件的核心描述文件。OxideTerm 通过扫描 ~/.oxideterm/plugins/*/plugin.json 发现插件。

{
"id": "your-plugin-id",
"name": "Human Readable Name",
"version": "1.0.0",
"description": "What this plugin does",
"author": "Your Name",
"main": "./main.js",
"engines": {
"oxideterm": ">=1.6.0"
},
"locales": "./locales",
"contributes": {
"tabs": [...],
"sidebarPanels": [...],
"settings": [...],
"terminalHooks": {...},
"connectionHooks": [...],
"apiCommands": [...]
}
}

入口文件必须是有效的 ES Module,并 export 以下函数:

/**
* 必需。插件激活时被调用。
* @param {PluginContext} ctx - 冻结的 API 上下文对象
*/
export function activate(ctx) {
// 注册 UI、hooks、事件监听等
}
/**
* 可选。插件卸载时被调用。
* 用于清理全局状态(window 上挂载的东西等)。
* 注意:Disposable 注册的内容会自动清理,无需在此手动清除。
*/
export function deactivate() {
// 清理全局引用
}

两个函数均支持返回 Promise(异步激活/停用),但有 5 秒超时限制

加载机制(双策略)

v1 单文件 Bundle(默认 / format: "bundled"

Rust read_plugin_file(id, "main.js")
→ 字节数组传递到前端
→ new Blob([bytes], { type: 'application/javascript' })
→ URL.createObjectURL(blob)
→ import(blobUrl)
→ module.activate(frozenContext)

使用 Blob URL 加载时,插件内部不能使用相对路径 import。请使用打包工具(esbuild/rollup)合并为单文件 ESM bundle。

v2 多文件 Package(format: "package"

前端调用 api.pluginStartServer()
→ Rust 启动本地 HTTP Server (127.0.0.1:0)
→ 返回 OS 分配的端口号
import(`http://127.0.0.1:{port}/plugins/{id}/src/main.js`)
→ 浏览器标准 ES Module 加载
→ main.js 中的 import './components/Dashboard.js' 自动解析
→ module.activate(frozenContext)

v2 包支持文件间的相对路径 import,浏览器会自动通过 HTTP Server 解析。服务器首次使用时自动启动,支持优雅停机。

v2 多文件入口示例

// src/main.js — import 同包的其他模块
import { Dashboard } from './components/Dashboard.js';
import { formatBytes } from './utils/helpers.js';
export async function activate(ctx) {
// 动态加载额外 CSS
const cssDisposable = await ctx.assets.loadCSS('./styles/extra.css');
// 获取资源文件的 blob URL(用于 <img> src 等)
const logoUrl = await ctx.assets.getAssetUrl('./assets/logo.png');
ctx.ui.registerTabView('dashboard', (props) => {
const { React } = window.__OXIDE__;
return React.createElement(Dashboard, { ...props, logoUrl });
});
}
export function deactivate() {
// Disposable 会自动清理 CSS 和 blob URL
}

字段类型必需说明
idstring插件唯一标识符。只能包含字母、数字、连字符、点号。不允许 /\..、控制字符。
namestring人类可读的插件名称
versionstring语义化版本号 (如 "1.0.0")
descriptionstring插件描述
authorstring作者
mainstringESM 入口文件的相对路径 (如 "./main.js""./src/main.js")
enginesobject版本兼容性要求
engines.oxidetermstring所需最低 OxideTerm 版本 (如 ">=1.6.0")。支持 >=x.y.z 格式。
contributesobject插件贡献的能力声明
localesstringi18n 翻译文件目录的相对路径 (如 "./locales")

v2 Package 扩展字段

字段类型必需说明
manifestVersion1 | 2清单版本,默认 1
format'bundled' | 'package'bundled(默认)= 单文件 Blob URL 加载;package = 本地 HTTP Server 加载(支持相对 import)
assetsstring资源目录相对路径(如 "./assets"),配合 ctx.assets API 使用
stylesstring[]CSS 文件列表(如 ["./styles/main.css"]),加载时自动注入 <style><head>
sharedDependenciesRecord<string, string>声明从宿主共享的依赖版本。当前支持:reactreact-domzustandlucide-react
repositorystring源码仓库 URL
checksumstringSHA-256 校验和(用于完整性验证)

v2 manifest 示例

{
"id": "com.example.multi-file-plugin",
"name": "Multi-File Plugin",
"version": "2.0.0",
"main": "./src/main.js",
"engines": { "oxideterm": ">=1.6.2" },
"manifestVersion": 2,
"format": "package",
"styles": ["./styles/main.css"],
"sharedDependencies": {
"react": "^18.0.0",
"lucide-react": "^0.300.0"
},
"contributes": {
"tabs": [{ "id": "dashboard", "title": "Dashboard", "icon": "LayoutDashboard" }]
},
"locales": "./locales"
}

声明插件提供的 Tab 视图。

"tabs": [
{
"id": "dashboard",
"title": "Plugin Dashboard",
"icon": "LayoutDashboard"
}
]
字段类型说明
idstringTab 标识符,在插件内唯一
titlestringTab 标题(显示在标签栏中)
iconstringLucide React 图标名称

声明后需在 activate() 中调用 ctx.ui.registerTabView(id, Component) 注册组件。

icon 字段直接用于标签栏(Tab Bar)的图标渲染。使用 PascalCase 的 Lucide 图标名,例如 "LayoutDashboard""Server""Activity"。如果名称无效或缺失,默认显示 Puzzle 图标。

完整图标列表见: https://lucide.dev/icons/

声明插件提供的侧边栏面板。

"sidebarPanels": [
{
"id": "quick-info",
"title": "Quick Info",
"icon": "Info",
"position": "bottom"
}
]
字段类型说明
idstringPanel 标识符,在插件内唯一
titlestring面板标题
iconstringLucide React 图标名称
position"top" | "bottom"在侧边栏中的位置。默认 "bottom"

icon 字段直接用于侧边栏活动栏(Activity Bar)的图标渲染。使用 PascalCase 的 Lucide 图标名,例如 "Info""Database""BarChart"。如果名称无效或缺失,默认显示 Puzzle 图标。

当插件面板较多时,活动栏中部区域会自动支持滚动,底部的固定按钮(本地终端、文件管理、设置、插件管理)始终可见。

声明插件的可配置项。用户可在 Plugin Manager 中查看和修改。

"settings": [
{
"id": "greeting",
"type": "string",
"default": "Hello!",
"title": "Greeting Message",
"description": "The greeting shown in the dashboard"
},
{
"id": "enableFeature",
"type": "boolean",
"default": false,
"title": "Enable Feature",
"description": "Toggle this feature on or off"
},
{
"id": "theme",
"type": "select",
"default": "dark",
"title": "Theme",
"description": "Choose a color theme",
"options": [
{ "label": "Dark", "value": "dark" },
{ "label": "Light", "value": "light" },
{ "label": "System", "value": "system" }
]
},
{
"id": "maxItems",
"type": "number",
"default": 50,
"title": "Max Items",
"description": "Maximum number of items to display"
}
]
字段类型说明
idstring设置标识符
type"string" | "number" | "boolean" | "select"值类型
defaultany默认值
titlestring显示标题
descriptionstring?描述说明
optionsArray<{ label, value }>?type: "select" 时使用

声明终端 I/O 拦截能力。

"terminalHooks": {
"inputInterceptor": true,
"outputProcessor": true,
"shortcuts": [
{ "key": "ctrl+shift+d", "command": "openDashboard" },
{ "key": "ctrl+shift+s", "command": "saveBuffer" }
]
}
字段类型说明
inputInterceptorboolean?是否注册输入拦截器
outputProcessorboolean?是否注册输出处理器
shortcutsArray<{ key, command }>?终端内快捷键声明
shortcuts[].keystring快捷键组合,如 "ctrl+shift+d"
shortcuts[].commandstring命令名称(用于 registerShortcut() 匹配)

快捷键格式

  • 修饰键:ctrl(macOS 上 Ctrl/Cmd 都算)、shiftalt
  • 字母键:小写,如 ds
  • + 连接:ctrl+shift+d
  • 内部会对修饰键排序归一化

声明插件关注的连接生命周期事件。

"connectionHooks": ["onConnect", "onDisconnect", "onReconnect", "onLinkDown"]

可选值:"onConnect" | "onDisconnect" | "onReconnect" | "onLinkDown"

注意:这个字段当前仅作为文档声明,实际事件订阅通过 ctx.events.onConnect() 等方法完成。

声明插件需要调用的 Tauri 后端命令白名单。

"apiCommands": ["list_sessions", "get_session_info"]

只有声明在此列表中的命令才能通过 ctx.api.invoke() 调用。未声明的命令会在调用时抛出异常并在 console 输出警告。

类别命令说明
连接list_connections列出所有活跃连接
get_connection_health获取连接健康指标
quick_health_check快速连接检查
SFTPnode_sftp_init初始化 SFTP 通道
node_sftp_list_dir列出远程目录
node_sftp_stat获取文件/目录信息
node_sftp_preview预览文件内容
node_sftp_write写入文件
node_sftp_mkdir创建目录
node_sftp_delete删除文件
node_sftp_delete_recursive递归删除目录
node_sftp_rename重命名/移动文件
node_sftp_download下载文件
node_sftp_upload上传文件
node_sftp_download_dir递归下载目录
node_sftp_upload_dir递归上传目录
node_sftp_tar_probe探测远端 tar 支持
node_sftp_tar_uploadtar 流式上传
node_sftp_tar_downloadtar 流式下载
端口转发list_port_forwards列出会话端口转发
create_port_forward创建端口转发
stop_port_forward停止端口转发
delete_port_forward删除端口转发规则
restart_port_forward重启端口转发
update_port_forward更新转发参数
get_port_forward_stats获取转发流量统计
stop_all_forwards停止所有转发
传输队列sftp_cancel_transfer取消传输
sftp_pause_transfer暂停传输
sftp_resume_transfer恢复传输
sftp_transfer_stats传输队列统计
系统get_app_version获取 OxideTerm 版本
get_system_info获取系统信息

指向 i18n 翻译文件目录的相对路径。

"locales": "./locales"

详见 11. 国际化 (i18n) 章节。


OxideTerm 启动时(或用户在 Plugin Manager 中点击 Refresh 时),Rust 后端扫描 ~/.oxideterm/plugins/ 目录:

list_plugins()
→ 遍历 plugins/ 下的每个子目录
→ 查找 plugin.json
→ serde 解析为 PluginManifest
→ 验证必需字段 (id, name, main 非空)
→ 返回 Vec<PluginManifest>

不包含 plugin.json 或解析失败的目录会被跳过(日志警告)。

前端 loadPlugin() 收到 manifest 后进行二次验证:

  1. 必需字段检查idnameversionmain 必须为非空 string
  2. 版本兼容检查:如果声明了 engines.oxideterm,与当前 OxideTerm 版本做简单 semver >= 比较
  3. 验证失败 → 设置 state: 'error' 并记录错误信息
loadPlugin(manifest)
1. setPluginState('loading')
2. api.pluginReadFile(id, mainPath) // Rust 读取文件字节
3. new Blob([bytes]) → blobUrl // 创建 Blob URL
4. import(blobUrl) // 动态 ESM 导入
5. URL.revokeObjectURL(blobUrl) // 回收 Blob URL
6. 验证 module.activate 是 function
7. setPluginModule(id, module)
8. loadPluginLocales(id, ...) // 加载 i18n(如声明)
9. buildPluginContext(manifest) // 构建冻结上下文
10. module.activate(ctx) // 调用 activate(5s 超时)
11. setPluginState('active')

失败处理:加载过程中任何步骤失败会:

  • 调用 store.cleanupPlugin(id) 清理部分状态
  • 调用 removePluginI18n(id) 清理 i18n 资源
  • 设置 state: 'error' 并记录错误消息

activate(ctx) 是插件的主入口,应在此完成所有注册:

export function activate(ctx) {
// 1. 注册 UI 组件
ctx.ui.registerTabView('myTab', MyTabComponent);
ctx.ui.registerSidebarPanel('myPanel', MyPanelComponent);
// 2. 注册终端 hooks
ctx.terminal.registerInputInterceptor(myInterceptor);
ctx.terminal.registerOutputProcessor(myProcessor);
ctx.terminal.registerShortcut('myCommand', myHandler);
// 3. 订阅事件
ctx.events.onConnect(handleConnect);
ctx.events.onDisconnect(handleDisconnect);
// 4. 读取设置
const value = ctx.settings.get('myKey');
// 5. 读取存储
const data = ctx.storage.get('myData');
}

超时activate() 如返回 Promise,必须在 5000ms 内 resolve,否则将被视为加载失败。

激活后,插件进入运行状态:

  • 注册的 Tab/Sidebar 组件随 React 渲染
  • Terminal hooks 在每次终端 I/O 时同步调用
  • 事件处理器在连接状态变化时异步触发(queueMicrotask()
  • 设置/存储的读写即时生效

用户在 Plugin Manager 中禁用或重载插件时触发:

export function deactivate() {
// 清理全局状态
delete window.__MY_PLUGIN_STATE__;
}

超时:如返回 Promise,必须在 5000ms 内 resolve。

注意:通过 Disposable 注册的内容(事件监听、UI 组件、terminal hooks 等)无需在 deactivate() 中手动清理,系统会自动处理。

unloadPlugin(pluginId)
1. 调用 module.deactivate() // 5s 超时
2. cleanupPlugin(pluginId) // 销毁所有 Disposable
3. removePluginI18n(pluginId) // 清除 i18n 资源
4. 关闭该插件的所有 Tab
5. 清除错误跟踪器
6. setPluginState('inactive')
┌──────────┐
│ inactive │ ←── 初始状态 / 卸载后
└────┬─────┘
│ loadPlugin()
┌────▼─────┐
│ loading │
└────┬─────┘
成功 / │ \ 失败
┌────▼──┐ ┌──▼───┐
│ active │ │ error│
└────┬───┘ └──┬───┘
│ │ 可重试
unload / │ ▼
disable │ ┌──────────┐
│ │ disabled │ ←── 用户手动禁用 / 断路器自动禁用
│ └──────────┘
┌──────────┐
│ inactive │
└──────────┘

PluginState 枚举值:

状态含义
'inactive'未加载 / 已卸载
'loading'正在加载中
'active'已激活,正常运行
'error'加载或运行时出错
'disabled'被用户或断路器禁用

PluginContext 是传递给 activate(ctx) 的唯一参数。它是一个深度冻结的对象,包含 19 个命名空间(pluginId + 18 个子 API)。v3 新增了 7 个只读命名空间。

type PluginContext = Readonly<{
pluginId: string;
connections: PluginConnectionsAPI;
events: PluginEventsAPI;
ui: PluginUIAPI;
terminal: PluginTerminalAPI;
settings: PluginSettingsAPI;
i18n: PluginI18nAPI;
storage: PluginStorageAPI;
api: PluginBackendAPI;
assets: PluginAssetsAPI;
sftp: PluginSftpAPI;
forward: PluginForwardAPI;
// v3 新增命名空间
sessions: PluginSessionsAPI; // 会话树(只读)
transfers: PluginTransfersAPI; // SFTP 传输监控
profiler: PluginProfilerAPI; // 资源监控
eventLog: PluginEventLogAPI; // 事件日志
ide: PluginIdeAPI; // IDE 模式(只读)
ai: PluginAiAPI; // AI 对话(只读)
app: PluginAppAPI; // 应用信息
}>;
ctx.pluginId: string

当前插件的唯一标识符,与 plugin.json 中的 id 字段一致。


只读连接状态查询 API。

connections.getAll(): ReadonlyArray<ConnectionSnapshot>

返回所有 SSH 连接的不可变快照数组。

const conns = ctx.connections.getAll();
conns.forEach(c => {
console.log(`${c.username}@${c.host}:${c.port} [${c.state}]`);
});
connections.get(connectionId: string): ConnectionSnapshot | null

根据连接 ID 获取单个连接快照。不存在时返回 null

connections.getState(connectionId: string): SshConnectionState | null

快速获取连接当前状态。不存在时返回 null

可能的状态值:'idle' | 'connecting' | 'active' | 'disconnecting' | 'disconnected' | 'reconnecting' | 'link_down' | { error: string }


事件订阅与发布 API。所有 on* 方法返回 Disposable。事件处理器通过 queueMicrotask() 异步调用,不会阻塞状态更新。

events.onConnect(handler: (snapshot: ConnectionSnapshot) => void): Disposable

当连接变为 'active' 状态时触发(新建连接或从非活跃状态恢复)。

events.onDisconnect(handler: (snapshot: ConnectionSnapshot) => void): Disposable

当连接进入 'disconnected''disconnecting' 状态时触发,以及连接被移除时触发。

events.onLinkDown(handler: (snapshot: ConnectionSnapshot) => void): Disposable

当连接进入 'reconnecting''link_down'error 状态时触发。

events.onReconnect(handler: (snapshot: ConnectionSnapshot) => void): Disposable

当连接从 'reconnecting'/'link_down'/error 状态恢复到 'active' 时触发。

events.onSessionCreated(handler: (info: { sessionId: string; connectionId: string }) => void): Disposable

当一个新的终端会话(terminal session)在某个连接上创建时触发。

events.onSessionClosed(handler: (info: { sessionId: string }) => void): Disposable

当终端会话关闭时触发。

events.on(name: string, handler: (data: unknown) => void): Disposable

监听自定义(插件间)事件。事件名会自动加上命名空间前缀 plugin:{pluginId}:{name}

注意:你只能监听自己插件命名空间下的事件。如需跨插件通信,接收方需监听发送方的命名空间(例如直接使用 pluginEventBridge)。

emit(name, data) — 发射自定义事件

Section titled “emit(name, data) — 发射自定义事件”
events.emit(name: string, data: unknown): void

发射自定义事件。事件名同样自动加命名空间前缀。

// 发射
ctx.events.emit('data-ready', { rows: 100 });
// 同一插件内监听
ctx.events.on('data-ready', (data) => {
console.log('Received:', data);
});

UI 注册与交互 API。

ui.registerTabView(tabId: string, component: React.ComponentType<PluginTabProps>): Disposable

注册 Tab 视图组件。tabId 必须在 contributes.tabs 中预先声明。

PluginTabProps

type PluginTabProps = {
tabId: string; // Tab ID
pluginId: string; // 插件 ID
};
function MyTab({ tabId, pluginId }) {
return h('div', null, `Hello from ${pluginId}!`);
}
ctx.ui.registerTabView('myTab', MyTab);
ui.registerSidebarPanel(panelId: string, component: React.ComponentType): Disposable

注册侧边栏面板组件。panelId 必须在 contributes.sidebarPanels 中预先声明。

面板组件不接收 props(与 Tab 不同)。

function MyPanel() {
return h('div', { className: 'p-2' }, 'Sidebar content');
}
ctx.ui.registerSidebarPanel('myPanel', MyPanel);

注册一条命令到全局命令面板(⌘K / Ctrl+K)。

const disposable = ctx.ui.registerCommand('my-command', {
label: 'My Plugin Action',
icon: 'Zap', // Lucide icon name (optional)
shortcut: '⌘⇧P', // Display shortcut hint (optional)
section: 'tools', // Custom section label (optional)
}, () => {
console.log('Command executed!');
});
// Unregister when no longer needed
disposable.dispose();

命令在插件卸载时自动清理(通过 Disposable 机制)。

ui.openTab(tabId: string): void

以编程方式打开一个 Tab。如果已打开则切换到该 Tab,否则创建新 Tab。

ctx.ui.openTab('dashboard');
ui.showToast(opts: {
title: string;
description?: string;
variant?: 'default' | 'success' | 'error' | 'warning';
}): void

显示 Toast 通知。

ctx.ui.showToast({
title: 'File Saved',
description: 'config.json has been updated',
variant: 'success',
});
ui.showConfirm(opts: {
title: string;
description: string;
}): Promise<boolean>

显示确认对话框,返回用户选择。通过 PluginConfirmDialog(Radix Dialog)实现,样式与宿主应用一致。

const ok = await ctx.ui.showConfirm({
title: 'Delete Item?',
description: 'This action cannot be undone.',
});
if (ok) {
// 执行删除
}
ui.registerContextMenu(target: ContextMenuTarget, items: ContextMenuItem[]): Disposable

为指定目标区域注册右键菜单项。target 可以是 'terminal''sftp''tab''sidebar'

ctx.ui.registerContextMenu('terminal', [
{
label: 'Run Analysis',
icon: 'BarChart',
handler: () => console.log('Analyzing...'),
},
{
label: 'Copy as Markdown',
handler: () => { /* ... */ },
when: () => ctx.terminal.getNodeSelection(currentNodeId) !== null,
},
]);
ui.registerStatusBarItem(options: StatusBarItemOptions): StatusBarHandle

注册状态栏项,返回可更新/释放的句柄。

type StatusBarItemOptions = {
text: string;
icon?: string; // Lucide icon 名称
tooltip?: string;
alignment: 'left' | 'right';
priority?: number; // 数字越小越靠前
onClick?: () => void;
};
type StatusBarHandle = {
update(options: Partial<StatusBarItemOptions>): void;
dispose(): void;
};
const status = ctx.ui.registerStatusBarItem({
text: '✔ Connected',
icon: 'Wifi',
alignment: 'right',
priority: 100,
onClick: () => ctx.ui.openTab('dashboard'),
});
// 动态更新
status.update({ text: '⚠ Reconnecting...', icon: 'WifiOff' });
// 移除
status.dispose();

registerKeybinding(keybinding, handler) v3

Section titled “registerKeybinding(keybinding, handler) v3”
ui.registerKeybinding(keybinding: string, handler: () => void): Disposable

注册全局键盘快捷键(与 Terminal Hooks 的 registerShortcut 不同,这里不需要在 manifest 中声明)。

ctx.ui.registerKeybinding('ctrl+shift+p', () => {
console.log('Plugin action triggered!');
});
ui.showNotification(opts: {
title: string;
body?: string;
severity?: 'info' | 'warning' | 'error';
}): void

显示通知消息(内部映射到 toast 系统)。与 showToast 类似,但提供更语义化的 severity 参数。

ctx.ui.showNotification({
title: 'Transfer Complete',
body: '5 files uploaded successfully',
severity: 'info',
});
ui.showProgress(title: string): ProgressReporter

显示进度指示器,返回可更新和关闭的 ProgressReporter

type ProgressReporter = {
report(value: number, total: number, message?: string): void;
};
const progress = ctx.ui.showProgress('Deploying...');
progress.report(3, 10, 'Uploading files...');
progress.report(7, 10, 'Running scripts...');
progress.report(10, 10, 'Done!');
ui.getLayout(): Readonly<{
sidebarCollapsed: boolean;
activeTabId: string | null;
tabCount: number;
}>

获取当前布局状态的只读快照。

ui.onLayoutChange(handler: (layout: Readonly<{
sidebarCollapsed: boolean;
activeTabId: string | null;
tabCount: number;
}>) => void): Disposable

订阅布局变化事件。

ctx.ui.onLayoutChange((layout) => {
console.log(`Sidebar: ${layout.sidebarCollapsed ? 'collapsed' : 'expanded'}`);
console.log(`Active tab: ${layout.activeTabId}`);
});

终端 hooks 和工具 API。

terminal.registerInputInterceptor(handler: InputInterceptor): Disposable

注册输入拦截器。必须在 manifest 中声明 contributes.terminalHooks.inputInterceptor: true

type InputInterceptor = (
data: string, // 用户输入的原始字符串
context: { sessionId: string }, // 终端会话上下文
) => string | null; // 返回修改后的字符串,或 null 抑制输入

拦截器在终端 I/O 热路径上同步执行,有 5ms 时间预算

ctx.terminal.registerInputInterceptor((data, { sessionId }) => {
// 将所有输入转大写(仅示例!)
return data.toUpperCase();
});
// 返回 null 可以完全抑制输入
ctx.terminal.registerInputInterceptor((data, ctx) => {
if (data.includes('dangerous-command')) {
return null; // 阻止发送
}
return data;
});
terminal.registerOutputProcessor(handler: OutputProcessor): Disposable

注册输出处理器。必须在 manifest 中声明 contributes.terminalHooks.outputProcessor: true

type OutputProcessor = (
data: Uint8Array, // 原始终端输出字节
context: { sessionId: string },
) => Uint8Array; // 返回处理后的字节

同样在热路径上同步执行,有 5ms 时间预算。

ctx.terminal.registerOutputProcessor((data, { sessionId }) => {
// 简单的字节统计
totalBytes += data.length;
return data; // 透传不修改
});
terminal.registerShortcut(command: string, handler: () => void): Disposable

注册终端内快捷键。command 必须在 manifest contributes.terminalHooks.shortcuts 中有对应声明。

// manifest: { "key": "ctrl+shift+d", "command": "openDashboard" }
ctx.terminal.registerShortcut('openDashboard', () => {
ctx.ui.openTab('dashboard');
});
terminal.writeToTerminal(sessionId: string, text: string): void

向指定会话的终端写入文本数据。通过 terminalRegistry 查找对应的 writer 回调,直接写入终端的数据通道(SSH WebSocket 或本地 PTY)。

// 向终端发送命令
ctx.terminal.writeToTerminal(sessionId, 'ls -la\n');
// 发送特殊控制字符(如 Ctrl+C)
ctx.terminal.writeToTerminal(sessionId, '\x03');

如果找不到 sessionId 对应的终端或 writer 未注册,会输出 console.warn 但不会抛异常。

terminal.getBuffer(sessionId: string): string | null

返回指定会话的终端缓冲区文本内容。

const buffer = ctx.terminal.getBuffer(sessionId);
if (buffer) {
const lastLine = buffer.split('\n').pop();
console.log('Last line:', lastLine);
}
terminal.getSelection(sessionId: string): string | null

返回用户在指定会话终端中选中的文本。

terminal.search(nodeId: string, query: string, options?: {
caseSensitive?: boolean;
regex?: boolean;
wholeWord?: boolean;
}): Promise<Readonly<{ matches: ReadonlyArray<unknown>; total_matches: number }>>

在终端缓冲区中搜索文本。通过后端 Rust 命令执行,支持正则和大小写敏感选项。

const result = await ctx.terminal.search(nodeId, 'error', {
caseSensitive: false,
regex: false,
});
console.log(`Found ${result.total_matches} matches`);

getScrollBuffer(nodeId, startLine, count) v3

Section titled “getScrollBuffer(nodeId, startLine, count) v3”
terminal.getScrollBuffer(nodeId: string, startLine: number, count: number):
Promise<ReadonlyArray<Readonly<{ text: string; lineNumber: number }>>>

获取回滚缓冲区内容。返回指定范围的行数据。

const lines = await ctx.terminal.getScrollBuffer(nodeId, 0, 100);
lines.forEach(l => console.log(`[${l.lineNumber}] ${l.text}`));
terminal.getBufferSize(nodeId: string):
Promise<Readonly<{ currentLines: number; totalLines: number; maxLines: number }>>

获取缓冲区大小信息。

const stats = await ctx.terminal.getBufferSize(nodeId);
console.log(`Buffer: ${stats.currentLines}/${stats.maxLines} lines`);
terminal.clearBuffer(nodeId: string): Promise<void>

清空指定会话的终端缓冲区。

await ctx.terminal.clearBuffer(nodeId);

插件作用域的设置 API,持久化到 localStorage

settings.get<T>(key: string): T

获取设置值。如果没有用户设置过的值,返回 manifest 中声明的 default

const greeting = ctx.settings.get('greeting'); // "Hello!"
const max = ctx.settings.get('maxItems'); // 50
settings.set<T>(key: string, value: T): void

设置值。会触发通过 onChange() 注册的监听器。

settings.onChange(key: string, handler: (newValue: unknown) => void): Disposable

监听设置变更。

ctx.settings.onChange('greeting', (newVal) => {
console.log('Greeting changed to:', newVal);
});

存储键格式oxide-plugin-{pluginId}-setting-{settingId}


插件作用域的国际化 API。

i18n.t(key: string, params?: Record<string, string | number>): string

翻译指定 key。key 会自动加上 plugin.{pluginId}. 前缀。

const msg = ctx.i18n.t('greeting');
const hello = ctx.i18n.t('hello_user', { name: 'Alice' });

对应翻译文件 locales/en.json

{
"greeting": "Welcome!",
"hello_user": "Hello, {{name}}!"
}
i18n.getLanguage(): string

获取当前语言代码。如 "en""zh-CN"

i18n.onLanguageChange(handler: (lang: string) => void): Disposable

监听语言切换。


插件作用域的持久化 KV 存储,基于 localStorage

storage.get<T>(key: string): T | null

获取值。不存在或解析失败返回 null。值自动 JSON 反序列化。

storage.set<T>(key: string, value: T): void

存储值。自动 JSON 序列化。

storage.remove(key: string): void

删除指定 key。

// 使用示例:计录启动次数
const count = (ctx.storage.get('launchCount') || 0) + 1;
ctx.storage.set('launchCount', count);

存储键格式oxide-plugin-{pluginId}-{key}


受限的 Tauri 后端命令调用 API。

api.invoke<T>(command: string, args?: Record<string, unknown>): Promise<T>

调用 Tauri 后端命令。命令必须在 contributes.apiCommands 中预先声明。

// manifest: "apiCommands": ["list_sessions"]
const sessions = await ctx.api.invoke('list_sessions');

未声明的命令

  • 调用时 console 输出警告
  • 抛出 Error: Command "xxx" not whitelisted in manifest contributes.apiCommands

插件资源文件访问 API。用于加载 CSS 样式、获取图片/字体/数据文件的 URL。

assets.loadCSS(relativePath: string): Promise<Disposable>

读取插件目录中的 CSS 文件,注入 <style data-plugin="{pluginId}"> 标签到 <head>。返回的 Disposable 调用 dispose() 后会移除该 <style> 标签。

// 动态加载额外样式
const cssDisposable = await ctx.assets.loadCSS('./styles/extra.css');
// 不再需要时手动移除(也可在卸载时自动清理)
cssDisposable.dispose();

注意:manifest.styles 中声明的 CSS 文件会在插件加载时自动注入,无需手动调用 loadCSS()loadCSS() 适用于按需加载的额外样式。

assets.getAssetUrl(relativePath: string): Promise<string>

读取插件目录中的任意文件,返回 blob URL(可用于 <img src>new Image() 等)。

const logoUrl = await ctx.assets.getAssetUrl('./assets/logo.png');
// 在 React 组件中使用
return h('img', { src: logoUrl, alt: 'Logo' });

MIME 类型自动检测

扩展名MIME
pngimage/png
jpg/jpegimage/jpeg
gifimage/gif
svgimage/svg+xml
webpimage/webp
woff/woff2font/woff / font/woff2
ttf/otffont/ttf / font/otf
jsonapplication/json
csstext/css
jsapplication/javascript
其他application/octet-stream
assets.revokeAssetUrl(url: string): void

手动释放通过 getAssetUrl() 创建的 blob URL,释放内存。

const url = await ctx.assets.getAssetUrl('./assets/large-image.png');
// 使用完毕后
ctx.assets.revokeAssetUrl(url);

卸载插件时,所有未手动释放的 blob URL 和注入的 <style> 标签会自动清理


远程文件系统操作 API。通过 SFTP 协议操作远端文件,无需在 contributes.apiCommands 中声明。

所有方法使用 nodeId(稳定标识符),在重连后仍然有效。后端会自动初始化 SFTP 通道。

sftp.listDir(nodeId: string, path: string): Promise<ReadonlyArray<PluginFileInfo>>

列出远程目录内容。返回 frozen 的文件信息数组。

const files = await ctx.sftp.listDir(nodeId, '/home/user');
for (const f of files) {
console.log(`${f.file_type} ${f.name} (${f.size} bytes)`);
}
sftp.stat(nodeId: string, path: string): Promise<PluginFileInfo>

获取远程文件或目录的元数据。

sftp.readFile(nodeId: string, path: string): Promise<string>

读取远程文本文件内容(最大 10 MB)。自动检测编码并返回 UTF-8 字符串。非文本文件或超过大小限制时抛出异常。

const content = await ctx.sftp.readFile(nodeId, '/etc/hostname');
sftp.writeFile(nodeId: string, path: string, content: string): Promise<void>

将文本内容写入远程文件(使用原子写入以防止损坏)。

sftp.mkdir(nodeId: string, path: string): Promise<void>

在远程主机上创建目录。

sftp.delete(nodeId: string, path: string): Promise<void>

删除远程文件。要递归删除目录,请使用 ctx.api.invoke('node_sftp_delete_recursive', { nodeId, path })

sftp.rename(nodeId: string, oldPath: string, newPath: string): Promise<void>

重命名或移动远程文件/目录。

type PluginFileInfo = Readonly<{
name: string;
path: string;
file_type: 'file' | 'directory' | 'symlink' | 'unknown';
size: number;
modified: number | null; // Unix timestamp (seconds)
permissions: string | null; // e.g. "rwxr-xr-x"
}>;

端口转发管理 API。可用于创建、查询和管理 SSH 端口转发,无需在 contributes.apiCommands 中声明。

注意:端口转发使用 sessionId(而非 nodeId),因为转发绑定到 SSH 会话生命周期。可通过 ctx.connections.getByNode(nodeId)?.id 获取 sessionId。

forward.list(sessionId: string): Promise<ReadonlyArray<PluginForwardRule>>

列出某个会话的所有活跃端口转发。

const conn = ctx.connections.getByNode(nodeId);
if (conn) {
const forwards = await ctx.forward.list(conn.id);
forwards.forEach(f => console.log(`${f.forward_type} ${f.bind_address}:${f.bind_port}${f.target_host}:${f.target_port}`));
}
forward.create(request: PluginForwardRequest): Promise<{
success: boolean;
forward?: PluginForwardRule;
error?: string;
}>

创建新的端口转发。支持 local、remote 和 dynamic (SOCKS5) 三种类型。

const result = await ctx.forward.create({
sessionId: conn.id,
forwardType: 'local',
bindAddress: '127.0.0.1',
bindPort: 8080,
targetHost: 'localhost',
targetPort: 80,
description: 'My plugin forward',
});
if (result.success) {
console.log('Forward created:', result.forward?.id);
}
forward.stop(sessionId: string, forwardId: string): Promise<void>

停止一个端口转发。

forward.stopAll(sessionId: string): Promise<void>

停止某个会话的所有端口转发。

forward.getStats(sessionId: string, forwardId: string): Promise<{
connectionCount: number;
activeConnections: number;
bytesSent: number;
bytesReceived: number;
} | null>

获取端口转发的流量统计信息。

type PluginForwardRequest = {
sessionId: string;
forwardType: 'local' | 'remote' | 'dynamic';
bindAddress: string;
bindPort: number;
targetHost: string;
targetPort: number;
description?: string;
};
type PluginForwardRule = Readonly<{
id: string;
forward_type: 'local' | 'remote' | 'dynamic';
bind_address: string;
bind_port: number;
target_host: string;
target_port: number;
status: string;
description?: string;
}>;

完整示例

export async function activate(ctx) {
// 1. 自动加载 manifest.styles 中的 CSS(无需代码)
// 2. 按需加载额外 CSS
const highlightCSS = await ctx.assets.loadCSS('./styles/highlight.css');
// 3. 获取图片 URL
const iconUrl = await ctx.assets.getAssetUrl('./assets/icon.svg');
// 4. 获取 JSON 配置
const configUrl = await ctx.assets.getAssetUrl('./assets/defaults.json');
const configResp = await fetch(configUrl);
const defaults = await configResp.json();
ctx.assets.revokeAssetUrl(configUrl); // JSON 已读取,释放 blob URL
ctx.ui.registerTabView('my-tab', (props) => {
const { React } = window.__OXIDE__;
return React.createElement('div', null,
React.createElement('img', { src: iconUrl, width: 32 }),
React.createElement('pre', null, JSON.stringify(defaults, null, 2)),
);
});
}

会话树只读访问 API。所有数据以冻结快照形式提供。

sessions.getTree(): ReadonlyArray<SessionTreeNodeSnapshot>

获取整个会话树的冻结快照。

type SessionTreeNodeSnapshot = Readonly<{
id: string;
label: string;
host?: string;
port?: number;
username?: string;
parentId: string | null;
childIds: readonly string[];
connectionState: string; // 'idle' | 'connecting' | 'active' | ...
connectionId: string | null;
terminalIds: readonly string[];
sftpSessionId: string | null;
errorMessage?: string;
}>;
const tree = ctx.sessions.getTree();
tree.forEach(node => {
console.log(`${node.label} (${node.connectionState})`);
if (node.host) console.log(`${node.username}@${node.host}:${node.port}`);
});
sessions.getActiveNodes(): ReadonlyArray<Readonly<{
nodeId: string;
sessionId: string | null;
connectionState: string;
}>>

获取所有活跃(已连接)节点列表。

sessions.getNodeState(nodeId: string): string | null

获取单个节点的连接状态。返回 null 表示节点不存在。

sessions.onTreeChange(handler: (tree: ReadonlyArray<SessionTreeNodeSnapshot>) => void): Disposable

订阅会话树结构变化。节点增删或连接状态变化时触发。

ctx.sessions.onTreeChange((tree) => {
const activeCount = tree.filter(n => n.connectionState === 'active').length;
status.update({ text: `${activeCount} active` });
});
sessions.onNodeStateChange(nodeId: string, handler: (state: string) => void): Disposable

订阅特定节点的状态变化。


SFTP 传输监控 API。只读访问,进度事件以 500ms 间隔节流。

transfers.getAll(): ReadonlyArray<TransferSnapshot>

获取所有当前传输任务。

type TransferSnapshot = Readonly<{
id: string;
nodeId: string;
name: string;
localPath: string;
remotePath: string;
direction: 'upload' | 'download';
size: number;
transferred: number;
state: 'pending' | 'active' | 'paused' | 'completed' | 'cancelled' | 'error';
error?: string;
startTime: number;
endTime?: number;
}>;
const transfers = ctx.transfers.getAll();
const active = transfers.filter(t => t.state === 'active');
console.log(`${active.length} active transfers`);
transfers.getByNode(nodeId: string): ReadonlyArray<TransferSnapshot>

获取特定节点的传输任务。

transfers.onProgress(handler: (transfer: TransferSnapshot) => void): Disposable

订阅传输进度更新。以 500ms 间隔节流,避免高频回调影响性能。

ctx.transfers.onProgress((t) => {
const pct = Math.round((t.transferred / t.size) * 100);
console.log(`${t.name}: ${pct}%`);
});
transfers.onComplete(handler: (transfer: TransferSnapshot) => void): Disposable
transfers.onError(handler: (transfer: TransferSnapshot) => void): Disposable

订阅传输完成/错误事件。

ctx.transfers.onComplete((t) => {
ctx.ui.showToast({ title: `${t.name} uploaded`, variant: 'success' });
});
ctx.transfers.onError((t) => {
ctx.ui.showToast({ title: `${t.name} failed: ${t.error}`, variant: 'error' });
});

资源监控 API。提供 CPU、内存、网络等系统指标的只读访问。指标以 1s 间隔节流推送。

profiler.getMetrics(nodeId: string): ProfilerMetricsSnapshot | null

获取节点的最新指标快照。

type ProfilerMetricsSnapshot = Readonly<{
timestampMs: number;
cpuPercent: number | null;
memoryUsed: number | null;
memoryTotal: number | null;
memoryPercent: number | null;
loadAvg1: number | null;
loadAvg5: number | null;
loadAvg15: number | null;
cpuCores: number | null;
netRxBytesPerSec: number | null;
netTxBytesPerSec: number | null;
sshRttMs: number | null;
}>;
const metrics = ctx.profiler.getMetrics(nodeId);
if (metrics) {
console.log(`CPU: ${metrics.cpuPercent}%, Mem: ${metrics.memoryPercent}%`);
}
profiler.getHistory(nodeId: string, maxPoints?: number): ReadonlyArray<ProfilerMetricsSnapshot>

获取历史指标数据。maxPoints 限制返回的数据点数量(从最新开始)。

profiler.isRunning(nodeId: string): boolean

检查指定节点的性能监控是否正在运行。

profiler.onMetrics(nodeId: string, handler: (metrics: ProfilerMetricsSnapshot) => void): Disposable

订阅实时指标推送。以 1 秒间隔节流。

ctx.profiler.onMetrics(nodeId, (m) => {
status.update({ text: `CPU ${m.cpuPercent?.toFixed(1)}%` });
});

连接事件日志只读访问 API。

eventLog.getEntries(filter?: {
severity?: 'info' | 'warn' | 'error';
category?: 'connection' | 'reconnect' | 'node';
}): ReadonlyArray<EventLogEntrySnapshot>

获取事件日志条目,支持按 severity/category 过滤。

type EventLogEntrySnapshot = Readonly<{
id: number;
timestamp: number;
severity: 'info' | 'warn' | 'error';
category: 'connection' | 'reconnect' | 'node';
nodeId?: string;
connectionId?: string;
title: string;
detail?: string;
source: string;
}>;
const errors = ctx.eventLog.getEntries({ severity: 'error' });
console.log(`${errors.length} errors in log`);
errors.forEach(e => {
console.log(`[${new Date(e.timestamp).toISOString()}] ${e.title}`);
});
eventLog.onEntry(handler: (entry: EventLogEntrySnapshot) => void): Disposable

订阅新的日志条目。

ctx.eventLog.onEntry((entry) => {
if (entry.severity === 'error') {
ctx.ui.showNotification({
title: entry.title,
body: entry.detail,
severity: 'error',
});
}
});

IDE 模式只读访问 API。当 OxideTerm 的内置代码编辑器(基于 CodeMirror)激活时,可读取项目和文件信息。

ide.isOpen(): boolean

检查 IDE 模式是否激活。

ide.getProject(): IdeProjectSnapshot | null

获取当前项目信息。

type IdeProjectSnapshot = Readonly<{
nodeId: string;
rootPath: string;
name: string;
isGitRepo: boolean;
gitBranch?: string;
}>;
const project = ctx.ide.getProject();
if (project) {
console.log(`Project: ${project.name} @ ${project.rootPath}`);
if (project.isGitRepo) console.log(`Branch: ${project.gitBranch}`);
}
ide.getOpenFiles(): ReadonlyArray<IdeFileSnapshot>

获取所有打开的文件列表。

type IdeFileSnapshot = Readonly<{
path: string;
name: string;
language: string;
isDirty: boolean;
isActive: boolean;
isPinned: boolean;
}>;
ide.getActiveFile(): IdeFileSnapshot | null

获取当前活跃的文件。

onFileOpen(handler) / onFileClose(handler)

Section titled “onFileOpen(handler) / onFileClose(handler)”
ide.onFileOpen(handler: (file: IdeFileSnapshot) => void): Disposable
ide.onFileClose(handler: (path: string) => void): Disposable

订阅文件打开/关闭事件。

ide.onActiveFileChange(handler: (file: IdeFileSnapshot | null) => void): Disposable

订阅活跃文件切换事件。

ctx.ide.onActiveFileChange((file) => {
if (file) {
console.log(`Now editing: ${file.name} (${file.language})`);
}
});

AI 对话只读访问 API。可读取对话列表和消息,但不能发起对话或发送消息。

ai.getConversations(): ReadonlyArray<AiConversationSnapshot>

获取所有对话摘要。

type AiConversationSnapshot = Readonly<{
id: string;
title: string;
messageCount: number;
createdAt: number;
updatedAt: number;
}>;
ai.getMessages(conversationId: string): ReadonlyArray<AiMessageSnapshot>

获取指定对话的所有消息。

type AiMessageSnapshot = Readonly<{
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: number;
}>;
const convs = ctx.ai.getConversations();
if (convs.length > 0) {
const messages = ctx.ai.getMessages(convs[0].id);
console.log(`Latest conversation: ${convs[0].title} (${messages.length} messages)`);
}

getActiveProvider() / getAvailableModels()

Section titled “getActiveProvider() / getAvailableModels()”
ai.getActiveProvider(): Readonly<{ type: string; displayName: string }> | null
ai.getAvailableModels(): ReadonlyArray<string>

获取当前 AI 提供商信息和可用模型列表。

const provider = ctx.ai.getActiveProvider();
if (provider) {
console.log(`AI Provider: ${provider.displayName} (${provider.type})`);
const models = ctx.ai.getAvailableModels();
console.log(`Available models: ${models.join(', ')}`);
}
ai.onMessage(handler: (info: Readonly<{
conversationId: string;
messageId: string;
role: string;
}>) => void): Disposable

订阅新消息事件(不包含消息内容,需通过 getMessages() 获取)。


应用级只读信息 API。提供主题、设置、平台、版本等全局信息。

app.getTheme(): ThemeSnapshot

获取当前主题信息。

type ThemeSnapshot = Readonly<{
name: string;
isDark: boolean;
}>;
const theme = ctx.app.getTheme();
console.log(`Theme: ${theme.name} (${theme.isDark ? 'dark' : 'light'})`);
app.getSettings(category: 'terminal' | 'appearance' | 'general' | 'buffer' | 'sftp' | 'reconnect'):
Readonly<Record<string, unknown>>

获取指定类别的应用设置快照(只读)。

const terminalSettings = ctx.app.getSettings('terminal');
console.log('Font size:', terminalSettings.fontSize);

getVersion() / getPlatform() / getLocale()

Section titled “getVersion() / getPlatform() / getLocale()”
app.getVersion(): string // e.g. '1.6.2'
app.getPlatform(): 'macos' | 'windows' | 'linux'
app.getLocale(): string // e.g. 'zh-CN', 'en'
console.log(`OxideTerm ${ctx.app.getVersion()} on ${ctx.app.getPlatform()}`);
console.log(`Locale: ${ctx.app.getLocale()}`);
app.onThemeChange(handler: (theme: ThemeSnapshot) => void): Disposable

订阅主题切换事件。

ctx.app.onThemeChange((theme) => {
console.log(`Theme changed to ${theme.name}`);
// 插件可以据此调整自己的 UI
});
app.onSettingsChange(category: string, handler: (settings: Readonly<Record<string, unknown>>) => void): Disposable

订阅指定类别的设置变化。

app.getPoolStats(): Promise<PoolStatsSnapshot>

获取 SSH 连接池统计信息。

type PoolStatsSnapshot = Readonly<{
activeConnections: number;
totalSessions: number;
}>;
const stats = await ctx.app.getPoolStats();
console.log(`Pool: ${stats.activeConnections} connections, ${stats.totalSessions} sessions`);

插件必须使用宿主提供的共享模块,而不是自己打包 React 等库。这确保了 React hooks 的兼容性和避免多实例问题。

window.__OXIDE__ = {
React: typeof import('react');
ReactDOM: { createRoot: typeof import('react-dom/client').createRoot };
zustand: { create: typeof import('zustand').create };
lucideIcons: Record<string, React.FC>; // Lucide 图标名 → 组件映射
clsx: typeof import('clsx').clsx; // 轻量 className 构建器
cn: (...inputs: ClassValue[]) => string; // Tailwind-merge + clsx
useTranslation: typeof import('react-i18next').useTranslation; // i18n hook
ui: PluginUIKit; // 插件 UI 组件库
};
const { React } = window.__OXIDE__;
const { createElement: h, useState, useEffect, useCallback, useRef, useMemo } = React;
// 使用 createElement 代替 JSX
function MyComponent({ name }) {
const [count, setCount] = useState(0);
return h('div', null,
h('h1', null, `Hello ${name}!`),
h('button', { onClick: () => setCount(c => c + 1) }, `Count: ${count}`),
);
}

所有 React Hooks 均可使用,包括但不限于:

  • useState / useReducer — 状态管理
  • useEffect / useLayoutEffect — 副作用
  • useCallback / useMemo — 性能优化
  • useRef — 引用
  • useContext — 上下文(需自行创建 Context)

插件可以使用宿主的 Zustand 创建自己的状态 store:

const { zustand } = window.__OXIDE__;
const useMyStore = zustand.create((set) => ({
items: [],
addItem: (item) => set((s) => ({ items: [...s.items, item] })),
clearItems: () => set({ items: [] }),
}));
// 在组件中使用
function ItemList() {
const { items, clearItems } = useMyStore();
return h('div', null,
h('ul', null, items.map((item, i) => h('li', { key: i }, item))),
h('button', { onClick: clearItems }, 'Clear'),
);
}
const { lucideIcons } = window.__OXIDE__;
// lucideIcons 是一个 { 名称: 组件 } 映射对象
const Activity = lucideIcons['Activity'];
const Terminal = lucideIcons['Terminal'];
function MyIcon() {
return h(Activity, { className: 'h-4 w-4 text-primary' });
}

完整图标列表见: https://lucide.dev/icons/

Manifest 图标解析plugin.jsoncontributes.tabs[].iconcontributes.sidebarPanels[].icon 字段使用图标名称字符串(如 "LayoutDashboard"),系统会通过 resolvePluginIcon() 自动将其解析为对应的 Lucide React 组件,用于标签栏和侧边栏活动栏的图标渲染。插件组件内部通过 lucideIcons['IconName'] 获取图标组件。

OxideTerm 提供了一套轻量级 UI 组件库 window.__OXIDE__.ui,封装了 OxideTerm 的主题系统。强烈建议使用 UI Kit 代替手写 Tailwind CSS 类名,这样可以:

  • 🎨 自动适配所有主题(暗色/亮色/自定义)
  • 🔒 避免类名拼写错误
  • 📝 大幅减少样板代码
  • 🔄 主题系统升级时无需修改插件
const { React, lucideIcons, ui } = window.__OXIDE__;
const { createElement: h, useState } = React;
const Activity = lucideIcons['Activity'];
const Settings = lucideIcons['Settings'];
const Terminal = lucideIcons['Terminal'];

组件一览

组件用途示例
ui.ScrollView全高滚动容器(Tab 根容器)h(ui.ScrollView, null, children)
ui.Stack弹性布局(水平/垂直)h(ui.Stack, { direction: 'horizontal', gap: 2 }, ...)
ui.Grid网格布局h(ui.Grid, { cols: 3, gap: 4 }, ...)
ui.Card带标题/图标的卡片h(ui.Card, { icon: Activity, title: '统计' }, ...)
ui.Stat数值统计卡h(ui.Stat, { icon: Hash, label: '输入', value: 42 })
ui.Button按钮h(ui.Button, { variant: 'primary', onClick }, '点击')
ui.Input文本输入框h(ui.Input, { value, onChange, placeholder: '...' })
ui.Checkbox复选框h(ui.Checkbox, { checked, onChange, label: '启用' })
ui.Select下拉选择h(ui.Select, { value, options, onChange })
ui.Toggle开关控件h(ui.Toggle, { checked, onChange, label: '自动刷新' })
ui.Text语义化文本h(ui.Text, { variant: 'heading' }, '标题')
ui.Badge状态徽章h(ui.Badge, { variant: 'success' }, '在线')
ui.Separator分隔线h(ui.Separator)
ui.IconText图标+文本行h(ui.IconText, { icon: Terminal }, '终端')
ui.KV键值对显示行h(ui.KV, { label: '主机' }, '192.168.1.1')
ui.EmptyState空状态占位h(ui.EmptyState, { icon: Inbox, title: '暂无数据' })
ui.ListItem可点击列表项h(ui.ListItem, { icon: Server, title: 'prod-01', onClick })
ui.Progress进度条h(ui.Progress, { value: 75, variant: 'success' })
ui.Alert提示/警告框h(ui.Alert, { variant: 'warning', title: '注意' }, '...')
ui.Spinner加载指示器h(ui.Spinner, { label: '加载中...' })
ui.Table数据表格h(ui.Table, { columns, data, onRowClick })
ui.CodeBlock代码/终端输出h(ui.CodeBlock, null, 'ssh root@...')
ui.Tabs选项卡切换h(ui.Tabs, { tabs, activeTab, onTabChange }, content)
ui.Header页面级标题栏h(ui.Header, { icon: Layout, title: '仪表板' })

快速示例 — Tab 组件

function MyTab({ tabId, pluginId }) {
const [count, setCount] = useState(0);
return h(ui.ScrollView, null,
h(ui.Header, {
icon: Activity,
title: 'My Plugin',
subtitle: `v1.0.0`,
}),
h(ui.Grid, { cols: 3, gap: 3 },
h(ui.Stat, { icon: Terminal, label: '会话', value: 5 }),
h(ui.Stat, { icon: Activity, label: '流量', value: '12 KB' }),
h(ui.Stat, { icon: Clock, label: '运行时间', value: '2h' }),
),
h(ui.Card, { icon: Settings, title: '控制面板' },
h(ui.Stack, { gap: 2 },
h(ui.Text, { variant: 'muted' }, '点击按钮增加计数'),
h(ui.Stack, { direction: 'horizontal', gap: 2 },
h(ui.Button, { variant: 'primary', onClick: () => setCount(c => c + 1) }, `Count: ${count}`),
h(ui.Button, { variant: 'ghost', onClick: () => setCount(0) }, 'Reset'),
),
),
),
);
}

快速示例 — Sidebar 面板

function MySidebar() {
return h(ui.Stack, { gap: 2, className: 'p-2' },
h(ui.Text, { variant: 'label' }, 'My Plugin'),
h(ui.KV, { label: '状态', mono: true }, 'active'),
h(ui.KV, { label: '连接数', mono: true }, '3'),
h(ui.Button, {
variant: 'outline',
size: 'sm',
className: 'w-full',
onClick: () => ctx.ui.openTab('myTab'),
}, '打开详情'),
);
}

Tab 组件接收 PluginTabProps

// 推荐:使用 UI Kit
function MyTabView({ tabId, pluginId }) {
return h(ui.ScrollView, null,
h(ui.Header, { icon: LayoutDashboard, title: 'My Plugin Tab' }),
h(ui.Card, { title: '内容区' },
h(ui.Text, { variant: 'body' }, '这是一个插件 Tab。'),
),
);
}

纯 createElement 写法(不推荐,但也可以使用):

function MyTabView({ tabId, pluginId }) {
return h('div', { className: 'h-full overflow-auto p-6' },
h('div', { className: 'max-w-4xl mx-auto' },
h('h1', { className: 'text-xl font-bold text-theme-text' }, 'My Plugin Tab'),
),
);
}

注册(在 activate 中)

ctx.ui.registerTabView('myTab', MyTabView);

打开 Tab

ctx.ui.openTab('myTab');

建议的 Tab 组件结构

// 推荐:使用 UI Kit 组件
function MyTab({ tabId, pluginId }) {
return h(ui.ScrollView, null, // 全高 + 滚动 + 居中
h(ui.Header, { // 标题栏
icon: SomeIcon,
title: 'Title',
subtitle: 'Description',
}),
h(ui.Grid, { cols: 3, gap: 3 }, // 统计行
h(ui.Stat, { icon: Icon1, label: 'Metric', value: 42 }),
),
h(ui.Card, { icon: SomeIcon, title: 'Section' }, // 内容卡片
h(ui.Stack, { gap: 2 }, /* children */),
),
);
}

Sidebar 面板组件是无 props 的函数组件:

// 推荐:使用 UI Kit
function MyPanel() {
return h(ui.Stack, { gap: 2, className: 'p-2' },
h(ui.Text, { variant: 'label', className: 'px-1' }, 'My Panel'),
h(ui.KV, { label: '状态', mono: true }, 'active'),
h(ui.KV, { label: '连接数', mono: true }, '3'),
h(ui.Button, {
variant: 'outline', size: 'sm', className: 'w-full mt-1',
onClick: () => ctx.ui.openTab('myTab'),
}, 'Open in Tab'),
);
}

纯 createElement 写法

function MyPanel() {
return h('div', { className: 'p-2 space-y-2' },
h('div', { className: 'text-xs font-semibold text-theme-text-muted uppercase tracking-wider px-1 mb-1' },
'My Panel'
),
);
}

Sidebar 面板空间有限,建议:

  • 使用小字体 (text-xs)
  • 保持布局紧凑 (p-2, space-y-1)
  • 提供 “Open in Tab” 按钮链接到详细视图

以下是所有 window.__OXIDE__.ui 组件的完整 API 参考。

ScrollView — Tab 的标准根容器

Prop类型默认值说明
maxWidthstring'4xl'最大宽度 Tailwind 类后缀
paddingstring'6'内边距 Tailwind 类后缀
classNamestring追加自定义类名
h(ui.ScrollView, null, /* 所有 Tab 内容 */);
h(ui.ScrollView, { maxWidth: '6xl', padding: '4' }, children);

Stack — 弹性布局

Prop类型默认值说明
direction'vertical' | 'horizontal''vertical'方向
gapnumber2间距(Tailwind gap 值)
align'start' | 'center' | 'end' | 'stretch' | 'baseline'交叉轴对齐
justify'start' | 'center' | 'end' | 'between' | 'around'主轴对齐
wrapbooleanfalse是否换行
h(ui.Stack, { direction: 'horizontal', gap: 2, align: 'center' },
h(ui.Button, null, 'A'),
h(ui.Button, null, 'B'),
);

Grid — 网格布局

Prop类型默认值说明
colsnumber2列数
gapnumber4间距
h(ui.Grid, { cols: 3, gap: 3 },
h(ui.Stat, { label: 'A', value: 1 }),
h(ui.Stat, { label: 'B', value: 2 }),
h(ui.Stat, { label: 'C', value: 3 }),
);

Card — 主题化卡片

Prop类型默认值说明
titlestring卡片标题
iconReact.ComponentType标题前图标(Lucide 组件)
headerRightReact.ReactNode标题右侧自定义内容
h(ui.Card, {
icon: Settings,
title: '设置',
headerRight: h(ui.Badge, { variant: 'info' }, 'v2'),
},
h(ui.Text, { variant: 'muted' }, '卡片内容'),
);

Stat — 数值统计卡

Prop类型说明
labelstring描述文本
valuestring | number显示的数值
iconReact.ComponentType可选图标
h(ui.Stat, { icon: Activity, label: '流量', value: '12.5 KB' })

Button — 按钮

Prop类型默认值说明
variant'primary' | 'secondary' | 'destructive' | 'ghost' | 'outline''secondary'样式变体
size'sm' | 'md' | 'lg' | 'icon''md'尺寸
disabledbooleanfalse禁用状态
onClickfunction点击回调
h(ui.Button, { variant: 'primary', onClick: handler }, '保存');
h(ui.Button, { variant: 'destructive', size: 'sm' }, '删除');
h(ui.Button, { variant: 'ghost', size: 'icon' }, h(Trash2, { className: 'h-4 w-4' }));

Input — 文本输入

Prop类型默认值说明
value / defaultValuestring受控/非受控值
placeholderstring占位文本
typestring'text'HTML input type
size'sm' | 'md''md'尺寸
onChangefunction变更回调
onKeyDownfunction键盘事件回调
h(ui.Input, {
value: text,
onChange: (e) => setText(e.target.value),
placeholder: '输入搜索关键词...',
size: 'sm',
});

Checkbox — 复选框

Prop类型说明
checkedboolean选中状态
onChange(checked: boolean) => void变更回调(直接返回 boolean)
labelstring可选标签
disabledboolean禁用状态
h(ui.Checkbox, { checked: enabled, onChange: setEnabled, label: '启用特性' })

Select — 下拉选择

Prop类型说明
valuestring | number当前值
options{ label: string, value: string | number }[]选项列表
onChange(value: string) => void变更回调
placeholderstring占位提示
size'sm' | 'md'尺寸
h(ui.Select, {
value: theme,
options: [
{ label: '暗色', value: 'dark' },
{ label: '亮色', value: 'light' },
],
onChange: setTheme,
});

Text — 语义化文本

variant样式典型用途
'heading'大号粗体页面标题
'subheading'小号粗体区域标题
'body'正常文本段落内容
'muted'灰色小字描述/提示
'mono'等宽字体IP 地址/代码
'label'大写灰色区域标签
'tiny'超小灰字次要信息

可通过 as prop 改变渲染标签:h(ui.Text, { variant: 'heading', as: 'h2' }, '...')

Badge — 状态徽章

variant颜色用途
'default'灰色中性状态
'success'绿色成功/在线
'warning'黄色警告
'error'红色错误/离线
'info'蓝色信息/版本
h(ui.Badge, { variant: 'success' }, 'Active')

KV — 键值对行

h(ui.KV, { label: '主机', mono: true }, '192.168.1.1')

设置 mono: true 使值以等宽字体显示。

IconText — 图标 + 文本

h(ui.IconText, { icon: Terminal }, '活跃会话')

Separator — 分隔线

h(ui.Separator)

EmptyState — 空状态占位

h(ui.EmptyState, {
icon: Inbox,
title: '暂无数据',
description: '添加一个新项目以开始。',
action: h(ui.Button, { variant: 'primary' }, '添加'),
})

ListItem — 列表项

h(ui.ListItem, {
icon: Server,
title: 'production-01',
subtitle: '[email protected]',
right: h(ui.Badge, { variant: 'success' }, 'Active'),
active: isSelected,
onClick: () => select(item),
})

Header — 页面标题栏

h(ui.Header, {
icon: LayoutDashboard,
title: 'Dashboard',
subtitle: 'v1.0.0',
action: h(ui.Button, { size: 'sm' }, 'Refresh'),
})

Tabs — 选项卡切换

const [tab, setTab] = useState('overview');
h(ui.Tabs, {
tabs: [
{ id: 'overview', label: '概览', icon: Activity },
{ id: 'logs', label: '日志', icon: FileText },
],
activeTab: tab,
onTabChange: setTab,
},
tab === 'overview' ? h(OverviewPanel) : h(LogsPanel),
)
Prop类型说明
tabs{ id: string, label: string, icon?: Component }[]Tab 定义数组
activeTabstring当前激活的 tab id
onTabChange(id: string) => voidTab 切换回调

Table — 数据表格

h(ui.Table, {
columns: [
{ key: 'host', header: '主机' },
{ key: 'port', header: '端口', align: 'right', width: '80px' },
{ key: 'status', header: '状态', render: (v) => h(ui.Badge, { variant: v === 'active' ? 'success' : 'error' }, v) },
],
data: connections,
striped: true,
onRowClick: (row) => select(row.id),
})
Prop类型默认值说明
columns{ key, header, width?, align?, render? }[]列定义
dataRecord<string, unknown>[]数据行
compactbooleanfalse紧凑行高
stripedbooleanfalse斑马条纹
emptyTextstring'No data'空数据提示
onRowClick(row, index) => void行点击回调

Progress — 进度条

h(ui.Progress, { value: 75, max: 100, variant: 'success', showLabel: true })
variant颜色
'default'主题强调色
'success'绿色
'warning'黄色
'error'红色

Toggle — 开关控件

h(ui.Toggle, { checked: autoRefresh, onChange: setAutoRefresh, label: '自动刷新' })

与 Checkbox 的区别:Toggle 是滑动开关样式,更适合”开/关”场景。

Alert — 提示/警告框

h(ui.Alert, { variant: 'warning', icon: AlertTriangle, title: '注意' },
'此操作无法撤销。',
)
variant颜色用途
'info'蓝色提示信息
'success'绿色成功提示
'warning'黄色警告提示
'error'红色错误提示

Spinner — 加载指示器

h(ui.Spinner, { size: 'sm', label: '加载中...' })

size 可选值:'sm'(16px)、'md'(24px)、'lg'(32px)

CodeBlock — 代码/终端输出

h(ui.CodeBlock, { maxHeight: '200px', wrap: true },
'ssh [email protected]\nPassword: ****\nWelcome to Ubuntu 22.04',
)
Prop类型默认值说明
maxHeightstring'300px'最大高度(溢出滚动)
wrapbooleanfalse是否自动换行

如果需要超出 UI Kit 范围的自定义样式,可以直接使用 OxideTerm 的语义化 CSS 类:

文本颜色

类名用途
text-theme-text主要文本
text-theme-text-muted次要/灰色文本
text-theme-accent强调色文本

背景颜色

类名用途
bg-theme-bg页面背景
bg-theme-bg-panel卡片/面板背景
bg-theme-bg-hover悬停高亮背景
bg-theme-accent强调色背景

边框

类名用途
border-theme-border标准边框

由于 Tab 和 Sidebar 组件分别渲染,它们之间不能直接通过 React props 通信。推荐方案:

方案 1:Zustand Store(推荐)

const { zustand } = window.__OXIDE__;
// 在模块顶层创建共享 store
const useMyStore = zustand.create((set) => ({
data: [],
setData: (data) => set({ data }),
}));
// Tab 组件
function MyTab() {
const { data } = useMyStore();
return h('div', null, `Items: ${data.length}`);
}
// Sidebar 组件
function MyPanel() {
const { data } = useMyStore();
return h('div', null, `Count: ${data.length}`);
}

方案 2:全局变量 + ctx 引用

// activate 中
window.__MY_PLUGIN_CTX__ = ctx;
// 组件中
function MyTab() {
const ctx = window.__MY_PLUGIN_CTX__;
const conns = ctx?.connections.getAll() ?? [];
// ...
}
// deactivate 中清理
export function deactivate() {
delete window.__MY_PLUGIN_CTX__;
}

输入拦截器在用户每次向终端发送数据时同步调用。位于终端 I/O 的热路径上。

调用链

用户输入 → term.onData(data)
→ runInputPipeline(data, sessionId)
→ 遍历所有 interceptors
→ interceptor(data, { sessionId })
→ 返回修改后的 data 或 null
→ 如果结果非 null → WebSocket 发送到后端

使用场景

  • 输入过滤/审计
  • 自动补全前缀
  • 命令拦截/防误操作
  • 输入统计
// 示例:根据设置添加输入前缀
ctx.terminal.registerInputInterceptor((data, { sessionId }) => {
const prefix = ctx.settings.get('inputPrefix');
if (prefix) return prefix + data;
return data;
});

重要注意事项

  1. 拦截器是同步的,不支持 async
  2. 返回 null 会完全抑制输入(数据不会发送到服务器)
  3. 多个插件的拦截器按注册顺序串联执行,前一个的输出是后一个的输入
  4. 异常被静默捕获,数据透传(fail-open)
  5. 5ms 时间预算,详见 9.4

输出处理器在每次从远程服务器接收到终端数据时同步调用。

调用链

WebSocket 接收 MSG_TYPE_DATA
→ runOutputPipeline(data, sessionId)
→ 遍历所有 processors
→ processor(data, { sessionId })
→ 返回处理后的 Uint8Array
→ 写入 xterm.js 渲染

使用场景

  • 输出统计/审计
  • 敏感信息遮蔽
  • 输出日志记录
ctx.terminal.registerOutputProcessor((data, { sessionId }) => {
// 统计字节数
totalBytes += data.length;
// 透传原始数据
return data;
});

注意

  1. 输入参数是 Uint8Array(原始字节),不是字符串
  2. 返回类型也必须是 Uint8Array
  3. 同 Input Interceptor,有 5ms 时间预算
  4. 异常 fail-open:处理器出错时使用上一步的数据

注册终端聚焦时的键盘快捷键。

注册

// manifest:
// "shortcuts": [{ "key": "ctrl+shift+d", "command": "openDashboard" }]
ctx.terminal.registerShortcut('openDashboard', () => {
ctx.ui.openTab('dashboard');
});

快捷键匹配流程

终端 keydown 事件
→ matchPluginShortcut(event)
→ 构建归一化 key: parts.sort().join('+')
例: Ctrl+Shift+D → "ctrl+d+shift"
→ 在 shortcuts Map 中查找
→ 找到 → 调用 handler 并阻止默认行为

修饰键映射

  • event.ctrlKey || event.metaKey"ctrl" (macOS 上 Cmd 也算 Ctrl)
  • event.shiftKey"shift"
  • event.altKey"alt"

Terminal hooks 运行在终端 I/O 热路径上,每次按键或数据接收都会同步调用。因此有严格的性能限制:

时间预算:每个 hook 调用 ≤ 5ms (HOOK_BUDGET_MS)

  • 超时会输出 console.warn
  • 超时计入断路器错误计数

断路器10 次错误 / 60 秒 → 自动禁用插件

  • 计数器会在 60 秒窗口过期后重置
  • 触发断路器后,插件被立即卸载
  • 禁用状态持久化到 plugin-config.json(跨重启生效)

最佳实践

// ✅ 好的做法:轻量同步操作
ctx.terminal.registerInputInterceptor((data) => {
counter++;
return data;
});
// ❌ 坏的做法:重操作
ctx.terminal.registerInputInterceptor((data) => {
// 不要在这里做正则匹配大文本、DOM 操作等
const result = someExpensiveRegex.test(data);
return data;
});
// ✅ 好的做法:将重操作推迟到微任务
ctx.terminal.registerOutputProcessor((data) => {
queueMicrotask(() => {
// 重操作放这里
processDataAsync(data);
});
return data; // 立即返回原始数据
});

OxideTerm 的 Event Bridge 将 appStore 中的连接状态变更桥接为插件可订阅的事件。

事件触发条件

事件触发条件
connection:connect新连接出现且状态为 active;或非活跃状态(非 reconnecting/link_down/error)→ active
connection:reconnectreconnecting/link_down/erroractive
connection:link_down进入 reconnecting/link_down/error 状态
connection:idleactiveidle(SSH 连接存活但无终端)
connection:disconnect进入 disconnected/disconnecting;或连接从列表中被移除

使用示例

const disposable1 = ctx.events.onConnect((snapshot) => {
console.log(`Connected: ${snapshot.username}@${snapshot.host}`);
console.log(`State: ${snapshot.state}, Terminals: ${snapshot.terminalIds.length}`);
});
const disposable2 = ctx.events.onDisconnect((snapshot) => {
console.log(`Disconnected: ${snapshot.id}`);
});
const disposable3 = ctx.events.onLinkDown((snapshot) => {
ctx.ui.showToast({
title: 'Connection Lost',
description: `${snapshot.host} link down`,
variant: 'warning',
});
});
const disposable4 = ctx.events.onReconnect((snapshot) => {
ctx.ui.showToast({
title: 'Reconnected',
description: `${snapshot.host} is back`,
variant: 'success',
});
});
ctx.events.onSessionCreated(({ sessionId, connectionId }) => {
console.log(`New terminal session ${sessionId} on connection ${connectionId}`);
});
ctx.events.onSessionClosed(({ sessionId }) => {
console.log(`Session ${sessionId} closed`);
});

会话事件通过 diff terminalIds 数组检测。

// 插件 A:发射事件
ctx.events.emit('data-ready', { items: [...] });
// 插件 A:监听自己的事件
ctx.events.on('data-ready', (data) => {
console.log('Received:', data.items.length);
});

命名空间规则

  • ctx.events.emit('foo', data) 实际发射 plugin:{pluginId}:foo
  • ctx.events.on('foo', handler) 实际监听 plugin:{pluginId}:foo
  • 同一插件内的 emit/on 自动匹配

🔬 跨插件通信:当前 API 设计中,每个插件的 on/emit 都自动加上了自己的命名空间前缀。因此默认情况下只能监听自己的事件,跨插件通信需要通过其他机制(如共享 store 或约定好的事件名直接使用底层 bridge)。

所有连接事件的 handler 都收到一个不可变的 ConnectionSnapshot 对象:

type ConnectionSnapshot = Readonly<{
id: string; // 连接唯一 ID
host: string; // SSH 主机地址
port: number; // SSH 端口
username: string; // SSH 用户名
state: SshConnectionState; // 当前连接状态
refCount: number; // 引用计数
keepAlive: boolean; // 是否保持活跃
createdAt: string; // 创建时间
lastActive: string; // 最后活跃时间
terminalIds: readonly string[]; // 关联的终端会话 ID 列表
parentConnectionId?: string; // 父连接 ID(跳板机场景)
}>;

SshConnectionState 可能的值:

type SshConnectionState =
| 'idle'
| 'connecting'
| 'active'
| 'disconnecting'
| 'disconnected'
| 'reconnecting'
| 'link_down'
| { error: string }; // 注意:error 状态是一个对象

v3 新增 SFTP 传输相关事件,通过 ctx.transfers API 订阅:

事件方法触发条件
transfers.onProgress(handler)传输进度更新(500ms 节流)
transfers.onComplete(handler)传输完成
transfers.onError(handler)传输出错

所有 handler 收到 TransferSnapshot 对象(参见 6.14)。

// 监控所有传输
ctx.transfers.onProgress((t) => {
const pct = ((t.transferred / t.size) * 100).toFixed(1);
console.log(`[${t.direction}] ${t.name}: ${pct}%`);
});
ctx.transfers.onComplete((t) => {
const duration = ((t.endTime - t.startTime) / 1000).toFixed(1);
console.log(`Done: ${t.name} in ${duration}s`);
});
ctx.transfers.onError((t) => {
console.error(`Failed: ${t.name}${t.error}`);
});

OxideTerm 使用 i18next 作为 i18n 框架。插件的翻译资源通过 loadPluginI18n() 加载到主 i18next 实例中,命名空间为 plugin.{pluginId}.*

your-plugin/
├── plugin.json ← "locales": "./locales"
└── locales/
├── en.json ← 英语(建议必须提供)
├── zh-CN.json ← 简体中文
├── zh-TW.json ← 繁体中文
├── ja.json ← 日语
├── ko.json ← 韩语
├── de.json ← 德语
├── es-ES.json ← 西班牙语
├── fr-FR.json ← 法语
├── it.json ← 意大利语
├── pt-BR.json ← 葡萄牙语(巴西)
└── vi.json ← 越南语

翻译文件格式(扁平 KV):

{
"dashboard_title": "Plugin Dashboard",
"greeting": "Hello, {{name}}!",
"item_count": "{{count}} items",
"settings_saved": "Settings saved successfully"
}
// 在 activate() 中或组件中
const title = ctx.i18n.t('dashboard_title'); // "Plugin Dashboard"
const greeting = ctx.i18n.t('greeting', { name: 'Alice' }); // "Hello, Alice!"
// 监听语言变化
ctx.i18n.onLanguageChange((lang) => {
console.log('Language changed to:', lang);
// 触发 UI 更新
});

OxideTerm 尝试按以下顺序加载语言文件(文件不存在则跳过):

语言代码语言
enEnglish
zh-CN简体中文
zh-TW繁體中文
ja日本語
ko한국어
deDeutsch
es-ESEspañol
fr-FRFrançais
itItaliano
pt-BRPortuguês (Brasil)
viTiếng Việt

基于 localStorage 的简单 KV 存储,自动 JSON 序列化/反序列化。

// 存
ctx.storage.set('myData', { items: [1, 2, 3], updated: Date.now() });
// 取
const data = ctx.storage.get('myData');
// { items: [1, 2, 3], updated: 1719000000000 }
// 删
ctx.storage.remove('myData');

存储键格式oxide-plugin-{pluginId}-{key}

限制

  • localStorage 容量限制(通常 5-10 MB per origin)
  • 失败时静默处理(不抛异常)
  • 所有值序列化为 JSON(不支持 undefinedfunctionSymbol 等)

ctx.storage 类似但有额外特性:

  • 在 manifest 中声明的设置有 default
  • 支持 onChange 监听
  • 存储键格式:oxide-plugin-{pluginId}-setting-{settingId}

每个插件的存储完全隔离:

localStorage key 格式:
oxide-plugin-{pluginId}-{key} ← storage
oxide-plugin-{pluginId}-setting-{settingId} ← settings

插件卸载时,存储不会自动清除(数据保留以便重新安装)。如需完全清除,可调用内部 clearPluginStorage(pluginId)(目前不通过 ctx 暴露)。


插件只能调用在 contributes.apiCommands 中声明的 Tauri 命令。

plugin.json
{
"contributes": {
"apiCommands": ["list_sessions", "get_session_info"]
}
}
// 在 activate() 中
try {
const sessions = await ctx.api.invoke('list_sessions');
console.log('Active sessions:', sessions);
} catch (err) {
console.error('Failed to list sessions:', err);
}

OxideTerm 的插件系统内置断路器(Circuit Breaker),防止故障插件拖垮整个应用:

参数说明
MAX_ERRORS10触发阈值
ERROR_WINDOW_MS60,000 ms (1 分钟)滑动窗口
HOOK_BUDGET_MS5 msTerminal hook 时间预算

计入断路器的错误

  1. Terminal hook(inputInterceptor / outputProcessor)抛出异常
  2. Terminal hook 执行时间超过 5ms
  3. 其他运行时错误(通过 trackPluginError() 追踪)

触发流程

插件错误
→ trackPluginError(pluginId)
→ 在 60s 窗口内累计错误次数
→ 达到 10 次
→ persistAutoDisable(pluginId)
→ plugin-config.json: { enabled: false }
→ store.setPluginState('disabled')
→ unloadPlugin(pluginId)
// ✅ 在 Terminal hooks 中做好防御
ctx.terminal.registerInputInterceptor((data, { sessionId }) => {
try {
// 你的逻辑
return processInput(data);
} catch (err) {
console.warn('[MyPlugin] Input interceptor error:', err);
return data; // 出错时透传原始数据
}
});
// ✅ 事件处理器中包裹 try-catch
ctx.events.onConnect((snapshot) => {
try {
handleConnection(snapshot);
} catch (err) {
console.error('[MyPlugin] onConnect error:', err);
}
});
// ✅ API 调用使用 try-catch
try {
const result = await ctx.api.invoke('some_command');
} catch (err) {
ctx.ui.showToast({
title: 'API Error',
description: String(err),
variant: 'error',
});
}

当断路器触发时:

  1. 读取 plugin-config.json
  2. 设置 plugins[pluginId].enabled = false
  3. 写回 plugin-config.json
  4. 设置 store 状态为 'disabled'

这意味着重启 OxideTerm 后插件仍然是禁用状态。用户需要在 Plugin Manager 中手动重新启用。


所有 register*on* 方法都返回一个 Disposable 对象:

type Disposable = {
dispose(): void; // 调用一次后变为 no-op
};

如果需要在运行时动态取消注册(例如根据设置切换 hook):

let interceptorDisposable = null;
function enableInterceptor() {
interceptorDisposable = ctx.terminal.registerInputInterceptor(myHandler);
}
function disableInterceptor() {
interceptorDisposable?.dispose();
interceptorDisposable = null;
}
// 根据设置动态启用/禁用
ctx.settings.onChange('enableFilter', (enabled) => {
if (enabled) enableInterceptor();
else disableInterceptor();
});

你不需要在 deactivate() 中手动清理通过 ctx 注册的内容。系统在卸载时会:

  1. 遍历该插件的所有 tracked Disposable
  2. 逐个调用 dispose()
  3. 清除 tabViews、sidebarPanels、inputInterceptors、outputProcessors、shortcuts
  4. 清除 disposables 跟踪列表

deactivate() 适合清理不在 Disposable 管理范围内的内容,例如 window 上的全局引用。


OxideTerm 内置了一个完整的 Demo Plugin 作为参考实现。

~/.oxideterm/plugins/oxide-demo-plugin/
├── plugin.json
└── main.js
{
"id": "oxide-demo-plugin",
"name": "OxideTerm Demo Plugin",
"version": "1.0.0",
"description": "A comprehensive demo plugin that exercises all plugin system APIs",
"author": "OxideTerm Team",
"main": "./main.js",
"engines": {
"oxideterm": ">=1.6.0"
},
"contributes": {
"tabs": [
{ "id": "dashboard", "title": "Plugin Dashboard", "icon": "LayoutDashboard" }
],
"sidebarPanels": [
{ "id": "quick-info", "title": "Quick Info", "icon": "Info", "position": "bottom" }
],
"settings": [
{
"id": "greeting", "type": "string", "default": "Hello from Plugin!",
"title": "Greeting Message", "description": "The greeting shown in the dashboard"
},
{
"id": "inputPrefix", "type": "string", "default": "",
"title": "Input Prefix", "description": "If set, prefix all terminal input"
},
{
"id": "logOutput", "type": "boolean", "default": false,
"title": "Log Output", "description": "Log terminal output byte counts to console"
}
],
"terminalHooks": {
"inputInterceptor": true,
"outputProcessor": true,
"shortcuts": [
{ "key": "ctrl+shift+d", "command": "openDashboard" }
]
},
"connectionHooks": ["onConnect", "onDisconnect"]
}
}

Demo Plugin 的 main.js 展示了所有 API 的使用方式:

1. 获取共享模块(含 UI Kit)

const { React, ReactDOM, zustand, lucideReact, ui } = window.__OXIDE__;
const { createElement: h, useState, useEffect, useCallback, useRef } = React;
const { Activity, Wifi, Terminal, Settings /* ... */ } = lucideReact;

2. 创建共享状态 Store

const useDemoStore = zustand.create((set) => ({
eventLog: [],
inputCount: 0,
outputBytes: 0,
connectionCount: 0,
addEvent: (msg) => set((s) => ({
eventLog: [...s.eventLog.slice(-49), { time: new Date().toLocaleTimeString(), msg }],
})),
incInput: () => set((s) => ({ inputCount: s.inputCount + 1 })),
addOutputBytes: (n) => set((s) => ({ outputBytes: s.outputBytes + n })),
setConnectionCount: (n) => set({ connectionCount: n }),
}));

3. Tab 组件 — 使用 ui.* 组件构建界面,通过 ctx 引用(window 全局)读取 connections、settings、storage

4. activate() 中的完整注册

export function activate(ctx) {
window.__DEMO_PLUGIN_CTX__ = ctx; // 暴露给组件
// UI 注册
ctx.ui.registerTabView('dashboard', DashboardTab);
ctx.ui.registerSidebarPanel('quick-info', QuickInfoPanel);
// Terminal Hooks
ctx.terminal.registerInputInterceptor((data, { sessionId }) => { /* ... */ });
ctx.terminal.registerOutputProcessor((data, { sessionId }) => { /* ... */ });
ctx.terminal.registerShortcut('openDashboard', () => ctx.ui.openTab('dashboard'));
// Events
ctx.events.onConnect((snapshot) => { /* ... */ });
ctx.events.onDisconnect((data) => { /* ... */ });
ctx.events.on('demo-ping', (data) => { /* ... */ });
// Settings Watch
ctx.settings.onChange('greeting', (newVal) => { /* ... */ });
// Storage
const count = (ctx.storage.get('launchCount') || 0) + 1;
ctx.storage.set('launchCount', count);
// Toast
ctx.ui.showToast({ title: 'Demo Plugin Activated', variant: 'success' });
}

5. deactivate() 清理

export function deactivate() {
delete window.__DEMO_PLUGIN_CTX__;
}

  1. 始终使用 window.__OXIDE__ 的共享模块

    • ❌ 不要在插件中打包自己的 React
    • ✅ 使用 const { React } = window.__OXIDE__
  2. 遵守 Manifest 声明

    • 所有 tab、panel、hook、shortcut、api command 必须先在 plugin.json 中声明
    • 运行时注册未声明的内容会抛异常
  3. 保持 activate() 轻量

    • 不要在 activate 中做重计算或长时间网络请求
    • 5 秒超时限制
  4. Terminal Hooks 要极其高效

    • 每次按键都会触发,必须在 5ms 内完成
    • 重操作推迟到 queueMicrotask()setTimeout()
    • 做好 try-catch 防御
  5. 使用语义化 CSS 类

    • 使用 Tailwind 的语义化类名:text-foregroundbg-cardborder-border
    • 不要硬编码颜色值
  6. 清理全局状态

    • deactivate()delete window.__MY_GLOBAL__
    • Disposable 管理的注册无需手动清理
  1. Event Log 限制大小:保留最近 N 条,避免内存泄漏

    eventLog: [...s.eventLog.slice(-49), newEntry] // 最多 50 条
  2. 避免在 output processor 中做字符串解码

    // ❌
    const text = new TextDecoder().decode(data);
    const processed = text.replace(/pattern/, 'replacement');
    return new TextEncoder().encode(processed);
    // ✅
    totalBytes += data.length;
    return data;
  3. 延迟初始化:组件中使用 useEffect 延迟加载数据

  1. 只声明需要的 apiCommands
  2. 不要在 window 上暴露敏感信息
  3. 不要直接导入 @tauri-apps/api/core(虽然技术上可行)
  4. 不要存储密码/密钥到 ctx.storage(localStorage 不加密)
  1. 快照不可变性:所有 v3 快照(TransferSnapshotProfilerMetricsSnapshot 等)通过 Object.freeze() 冻结。不要尝试修改它们——如需变换数据,创建新对象。

  2. 节流事件注意性能transfers.onProgress(500ms)和 profiler.onMetrics(1s)已做节流,但 handler 内仍应保持轻量——避免 DOM 操作或复杂计算。

  3. 按需使用命名空间:v3 的 19 个命名空间按需注入。如果你只需要 uiterminal,不必关心 profilerai

  4. Disposable 生命周期:v3 事件订阅(onTreeChangeonProgressonMetrics 等)返回 Disposable。务必在 deactivate() 中清理,或使用 ctx.events.on 系列 API 由框架自动管理。

  5. AI 数据敏感性ctx.ai.getMessages() 可能包含终端缓冲区内容,视为敏感数据——不要记录到日志或发送到外部服务。


Plugin Manager 为每个插件内置了日志查看面板。当插件有日志记录时,插件行会显示 📜 图标按钮,点击即可展开日志面板。

日志自动记录以下事件:

  • info:插件激活成功、卸载完成
  • error:加载失败(附带具体原因和修复建议)、断路器触发

每个插件最多保留 200 条日志记录。可通过日志面板右上角的「清除」按钮清空。

常见错误提示及含义

错误提示含义修复方法
activate() must resolve within 5s激活函数超时将耗时操作移到 setTimeoutqueueMicrotask
ensure your main.js exports an activate() function入口文件缺少导出检查 export function activate(ctx) 是否存在
check that main.js is a valid ES module bundleJS 语法/导入错误检查文件语法,确保是有效的 ESM 格式

插件的所有 console.log/warn/error 都会出现在 DevTools 中。系统内部日志使用 [PluginLoader][PluginEventBridge][PluginTerminalHooks] 前缀。

有用的调试命令

// 在 DevTools Console 中
// 查看所有已加载插件
JSON.stringify([...window.__ZUSTAND_PLUGIN_STORE__?.getState?.()?.plugins?.entries?.()] ?? 'store not found');
// 查看插件 store 状态(如果你的 store 是全局的)
useDemoStore.getState()
// 手动触发 toast
window.__DEMO_PLUGIN_CTX__?.ui.showToast({ title: 'Test', variant: 'success' });
// 查看当前连接
window.__DEMO_PLUGIN_CTX__?.connections.getAll();
  • Status Badge:显示 active/error/disabled 状态
  • Error Message:错误状态时显示详细错误信息
  • Reload:热重载插件(先 unload 再 load)
  • Refresh:重新扫描磁盘,发现新插件/移除已删除插件
现象可能原因
加载失败:module must export "activate"入口文件没有 export function activate
加载失败:timed out after 5000msactivate() 中有未 resolve 的 Promise
Tab 不显示忘记在 activate() 中调用 ctx.ui.registerTabView()
hooks 不工作Manifest 中未声明 terminalHooks.inputInterceptor: true
Toast 不显示确认 variant 拼写正确(default/success/error/warning
快捷键无效确认终端窗口处于聚焦状态
读取设置返回 undefined确认设置 key 与 manifest 中的 settings[].id 一致
插件被自动禁用断路器触发。检查 Plugin Manager 日志查看器或 DevTools 中的错误/超时警告
样式不对/和主题不协调使用了硬编码颜色而非语义化类名

可以。OxideTerm 提供了独立的类型定义文件 plugin-api.d.ts,无需安装 OxideTerm 源码即可获得完整的 IntelliSense 支持。

步骤 1:获取类型定义

从 OxideTerm 仓库根目录复制 plugin-api.d.ts 到你的插件项目中。

步骤 2:配置 tsconfig.json

{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": ".",
"strict": true
},
"include": ["plugin-api.d.ts", "src/**/*.ts"]
}

步骤 3:编写带类型的插件

src/main.ts
import type { PluginContext } from '../plugin-api';
export function activate(ctx: PluginContext) {
ctx.ui.showToast({ title: 'Hello!', variant: 'success' });
ctx.events.onConnect((snapshot) => {
console.log(`Connected to ${snapshot.host}`);
});
}

步骤 4:编译为 ESM

Terminal window
# 使用 esbuild(推荐)
npx esbuild src/main.ts --bundle --format=esm --outfile=main.js --external:react
# 或 tsc
npx tsc

注意:不要打包 React,使用 window.__OXIDE__ 获取。

  • v1 单文件插件format: "single"):使用 Blob URL 加载,内部 import 不生效。需用打包工具(esbuild/rollup)合并为单文件。
  • v2 包插件format: "package"):支持多文件结构,通过本地 HTTP 服务器加载,可使用 import map。

对 v1 插件的解决方案:

  1. 推荐:使用打包工具(esbuild/rollup)合并为单文件
  2. 备选:将所有代码写在 main.js 一个文件中
Terminal window
# esbuild 打包示例
npx esbuild src/index.ts \
--bundle \
--format=esm \
--outfile=main.js \
--external:react \
--external:react-dom

不能直接访问。插件只能:

  • 通过 ctx.api.invoke() 调用已声明的 Tauri 后端命令
  • 通过 ctx.storage 使用 localStorage

可以使用浏览器原生的 fetch() API。但注意 Tauri 的 CSP 策略可能限制某些域名。

插件默认是纯 JS,需使用 React.createElement。如需 JSX:

  1. 使用 esbuild:--jsx=automatic --jsx-import-source=react
  2. 使用 Babel:@babel/plugin-transform-react-jsx
  3. 在打包时将 React 标记为 external,运行时从 window.__OXIDE__ 获取

当前设计中,ctx.events.on/emit 有命名空间隔离。跨插件通信选项:

  1. 共享全局变量:双方约定 window.__SHARED_DATA__
  2. 底层 Event Bridge:直接使用 pluginEventBridge(需理解内部 API,不推荐)
  3. 未来计划:可能添加跨插件事件通道
  1. 在 Plugin Manager 中点击插件的 📜 图标查看日志,定位具体错误原因和修复建议
  2. 也可查看 DevTools console 中的错误/超时警告
  3. 修复代码中的性能问题或异常
  4. 在 Plugin Manager 中重新启用插件
  5. 或手动编辑 ~/.oxideterm/plugin-config.json
{
"plugins": {
"your-plugin-id": {
"enabled": true
}
}
}

Q: 插件可以修改 OxideTerm 的界面吗?

Section titled “Q: 插件可以修改 OxideTerm 的界面吗?”

通过声明式 API 可以:

  • 添加 Tab 视图
  • 添加 Sidebar 面板
  • 显示 Toast/Confirm
  • v3 新增:注册上下文菜单项(ctx.ui.registerContextMenu
  • v3 新增:注册状态栏项(ctx.ui.registerStatusBarItem
  • v3 新增:注册快捷键(ctx.ui.registerKeybinding
  • v3 新增:显示通知(ctx.ui.showNotification
  • v3 新增:显示进度指示器(ctx.ui.showProgress

不能:

  • 修改现有 UI 组件
  • 修改菜单/工具栏

注意:插件可通过 ctx.assets.loadCSS() 或 manifest styles 字段注入自定义 CSS。

文件/位置说明
~/.oxideterm/plugins/{id}/plugin.json插件清单
~/.oxideterm/plugins/{id}/main.js插件代码
~/.oxideterm/plugin-config.json全局插件启用/禁用配置
localStorage: oxide-plugin-{id}-*插件存储数据
localStorage: oxide-plugin-{id}-setting-*插件设置

Q: 如何发布插件到官方注册表?

Section titled “Q: 如何发布插件到官方注册表?”
  1. 打包插件:将插件目录打包为 ZIP 文件

    Terminal window
    cd ~/.oxideterm/plugins/my-plugin
    zip -r my-plugin-1.0.0.zip .
  2. 计算校验和

    Terminal window
    shasum -a 256 my-plugin-1.0.0.zip
    # 输出: abc123... my-plugin-1.0.0.zip
  3. 托管 ZIP 文件:上传到可公开访问的 URL(GitHub Releases、CDN 等)

  4. 提交到注册表

    • 官方注册表:向 OxideTerm 仓库提交 PR,添加你的插件条目
    • 自建注册表:在你的 registry.json 中添加条目

注册表条目格式

{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "Plugin description",
"author": "Your Name",
"downloadUrl": "https://example.com/my-plugin-1.0.0.zip",
"checksum": "sha256:abc123...",
"size": 12345,
"tags": ["utility"],
"homepage": "https://github.com/you/my-plugin"
}

Q: 如何使用自定义插件注册表?

Section titled “Q: 如何使用自定义插件注册表?”

编辑 ~/.oxideterm/plugin-config.json

{
"registryUrl": "https://your-server.com/registry.json",
"plugins": {}
}

注册表 JSON 格式:

{
"version": 1,
"plugins": [
{ "id": "...", "name": "...", ... }
]
}

推荐:直接使用仓库根目录的 plugin-api.d.ts 文件——它是独立的、零依赖的完整类型定义,复制到你的插件项目即可获得 IntelliSense。详见 FAQ: 插件可以使用 TypeScript 吗?

以下是完整的 TypeScript 类型定义供参考:

oxideterm-plugin.d.ts
// OxideTerm Plugin System Type Definitions
// ── Disposable ──────────────────────────────────────────────
export type Disposable = {
dispose(): void;
};
// ── Plugin States ───────────────────────────────────────────
export type PluginState = 'inactive' | 'loading' | 'active' | 'error' | 'disabled';
export type InstallState = 'downloading' | 'extracting' | 'installing' | 'done' | 'error';
export type SshConnectionState =
| 'idle'
| 'connecting'
| 'active'
| 'disconnecting'
| 'disconnected'
| 'reconnecting'
| 'link_down'
| { error: string };
// ── Connection Snapshot ─────────────────────────────────────
export type ConnectionSnapshot = Readonly<{
id: string;
host: string;
port: number;
username: string;
state: SshConnectionState;
refCount: number;
keepAlive: boolean;
createdAt: string;
lastActive: string;
terminalIds: readonly string[];
parentConnectionId?: string;
}>;
// ── Terminal Hook Types ─────────────────────────────────────
export type TerminalHookContext = {
/** @deprecated Use nodeId instead. Will be removed in next major version. */
sessionId: string;
/** Stable node identifier, survives reconnect. */
nodeId: string;
};
export type InputInterceptor = (
data: string,
context: TerminalHookContext,
) => string | null;
export type OutputProcessor = (
data: Uint8Array,
context: TerminalHookContext,
) => Uint8Array;
// ── Registry Types (Remote Installation) ────────────────────
export type RegistryEntry = {
id: string;
name: string;
description?: string;
author?: string;
version: string;
minOxidetermVersion?: string;
downloadUrl: string;
checksum?: string;
size?: number;
tags?: string[];
homepage?: string;
updatedAt?: string;
};
export type RegistryIndex = {
version: number;
plugins: RegistryEntry[];
};
// ── Plugin Tab Props ────────────────────────────────────────
export type PluginTabProps = {
tabId: string;
pluginId: string;
};
// ── API Interfaces ──────────────────────────────────────────
export type PluginConnectionsAPI = {
getAll(): ReadonlyArray<ConnectionSnapshot>;
get(connectionId: string): ConnectionSnapshot | null;
getState(connectionId: string): SshConnectionState | null;
/** Phase 4.5: resolve node to connection snapshot */
getByNode(nodeId: string): ConnectionSnapshot | null;
};
export type PluginEventsAPI = {
onConnect(handler: (snapshot: ConnectionSnapshot) => void): Disposable;
onDisconnect(handler: (snapshot: ConnectionSnapshot) => void): Disposable;
onLinkDown(handler: (snapshot: ConnectionSnapshot) => void): Disposable;
onReconnect(handler: (snapshot: ConnectionSnapshot) => void): Disposable;
onIdle(handler: (snapshot: ConnectionSnapshot) => void): Disposable;
/** Phase 4.5: Node becomes ready (connected + capabilities available) */
onNodeReady(handler: (info: { nodeId: string; connectionId: string }) => void): Disposable;
/** Phase 4.5: Node disconnected */
onNodeDisconnected(handler: (info: { nodeId: string }) => void): Disposable;
on(name: string, handler: (data: unknown) => void): Disposable;
emit(name: string, data: unknown): void;
};
export type PluginUIAPI = {
registerTabView(tabId: string, component: React.ComponentType<PluginTabProps>): Disposable;
registerSidebarPanel(panelId: string, component: React.ComponentType): Disposable;
registerCommand(id: string, opts: { label: string; icon?: string; shortcut?: string; section?: string }, handler: () => void): Disposable;
openTab(tabId: string): void;
showToast(opts: {
title: string;
description?: string;
variant?: 'default' | 'success' | 'error' | 'warning';
}): void;
showConfirm(opts: { title: string; description: string }): Promise<boolean>;
/** v3 additions */
registerContextMenu(target: ContextMenuTarget, items: ContextMenuItem[]): Disposable;
registerStatusBarItem(options: StatusBarItemOptions): StatusBarHandle;
registerKeybinding(keybinding: string, handler: () => void): Disposable;
showNotification(opts: { title: string; body?: string; severity?: 'info' | 'warning' | 'error' }): void;
showProgress(title: string): ProgressReporter;
getLayout(): Readonly<{ sidebarCollapsed: boolean; activeTabId: string | null; tabCount: number }>;
onLayoutChange(handler: (layout: Readonly<{ sidebarCollapsed: boolean; activeTabId: string | null; tabCount: number }>) => void): Disposable;
};
export type ContextMenuTarget = 'terminal' | 'sftp' | 'tab' | 'sidebar';
export type ContextMenuItem = {
label: string;
icon?: string;
handler: () => void;
when?: () => boolean;
};
export type StatusBarItemOptions = {
text: string;
icon?: string;
tooltip?: string;
alignment: 'left' | 'right';
priority?: number;
onClick?: () => void;
};
export type StatusBarHandle = {
update(options: Partial<StatusBarItemOptions>): void;
dispose(): void;
};
export type ProgressReporter = {
report(value: number, total: number, message?: string): void;
};
export type PluginTerminalAPI = {
registerInputInterceptor(handler: InputInterceptor): Disposable;
registerOutputProcessor(handler: OutputProcessor): Disposable;
registerShortcut(command: string, handler: () => void): Disposable;
/** Write to terminal by nodeId (stable across reconnects) */
writeToNode(nodeId: string, text: string): void;
/** Get terminal buffer by nodeId */
getNodeBuffer(nodeId: string): string | null;
/** Get terminal selection by nodeId */
getNodeSelection(nodeId: string): string | null;
/** v3: Search terminal buffer */
search(nodeId: string, query: string, options?: { caseSensitive?: boolean; regex?: boolean; wholeWord?: boolean }): Promise<Readonly<{ matches: ReadonlyArray<unknown>; total_matches: number }>>;
/** v3: Get scrollback buffer content */
getScrollBuffer(nodeId: string, startLine: number, count: number): Promise<ReadonlyArray<Readonly<{ text: string; lineNumber: number }>>>;
/** v3: Get buffer size info */
getBufferSize(nodeId: string): Promise<Readonly<{ currentLines: number; totalLines: number; maxLines: number }>>;
/** v3: Clear terminal buffer */
clearBuffer(nodeId: string): Promise<void>;
};
export type PluginSettingsAPI = {
get<T>(key: string): T;
set<T>(key: string, value: T): void;
onChange(key: string, handler: (newValue: unknown) => void): Disposable;
};
export type PluginI18nAPI = {
t(key: string, params?: Record<string, string | number>): string;
getLanguage(): string;
onLanguageChange(handler: (lang: string) => void): Disposable;
};
export type PluginStorageAPI = {
get<T>(key: string): T | null;
set<T>(key: string, value: T): void;
remove(key: string): void;
};
export type PluginBackendAPI = {
invoke<T>(command: string, args?: Record<string, unknown>): Promise<T>;
};
export type PluginAssetsAPI = {
loadCSS(relativePath: string): Promise<Disposable>;
getAssetUrl(relativePath: string): Promise<string>;
revokeAssetUrl(url: string): void;
};
// ── v3 Snapshot Types ───────────────────────────────────────
export type SessionTreeNodeSnapshot = Readonly<{
id: string;
label: string;
host?: string;
port?: number;
username?: string;
parentId: string | null;
childIds: readonly string[];
connectionState: string;
connectionId: string | null;
terminalIds: readonly string[];
sftpSessionId: string | null;
errorMessage?: string;
}>;
export type TransferSnapshot = Readonly<{
id: string;
nodeId: string;
name: string;
localPath: string;
remotePath: string;
direction: 'upload' | 'download';
size: number;
transferred: number;
state: 'pending' | 'active' | 'paused' | 'completed' | 'cancelled' | 'error';
error?: string;
startTime: number;
endTime?: number;
}>;
export type ProfilerMetricsSnapshot = Readonly<{
timestampMs: number;
cpuPercent: number | null;
memoryUsed: number | null;
memoryTotal: number | null;
memoryPercent: number | null;
loadAvg1: number | null;
loadAvg5: number | null;
loadAvg15: number | null;
cpuCores: number | null;
netRxBytesPerSec: number | null;
netTxBytesPerSec: number | null;
sshRttMs: number | null;
}>;
export type EventLogEntrySnapshot = Readonly<{
id: number;
timestamp: number;
severity: 'info' | 'warn' | 'error';
category: 'connection' | 'reconnect' | 'node';
nodeId?: string;
connectionId?: string;
title: string;
detail?: string;
source: string;
}>;
export type IdeFileSnapshot = Readonly<{
path: string;
name: string;
language: string;
isDirty: boolean;
isActive: boolean;
isPinned: boolean;
}>;
export type IdeProjectSnapshot = Readonly<{
nodeId: string;
rootPath: string;
name: string;
isGitRepo: boolean;
gitBranch?: string;
}>;
export type AiConversationSnapshot = Readonly<{
id: string;
title: string;
messageCount: number;
createdAt: number;
updatedAt: number;
}>;
export type AiMessageSnapshot = Readonly<{
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
timestamp: number;
}>;
export type ThemeSnapshot = Readonly<{
name: string;
isDark: boolean;
}>;
export type PoolStatsSnapshot = Readonly<{
activeConnections: number;
totalSessions: number;
}>;
// ── v3 Namespace Interfaces ─────────────────────────────────
export type PluginSessionsAPI = {
getTree(): ReadonlyArray<SessionTreeNodeSnapshot>;
getActiveNodes(): ReadonlyArray<Readonly<{ nodeId: string; sessionId: string | null; connectionState: string }>>;
getNodeState(nodeId: string): string | null;
onTreeChange(handler: (tree: ReadonlyArray<SessionTreeNodeSnapshot>) => void): Disposable;
onNodeStateChange(nodeId: string, handler: (state: string) => void): Disposable;
};
export type PluginTransfersAPI = {
getAll(): ReadonlyArray<TransferSnapshot>;
getByNode(nodeId: string): ReadonlyArray<TransferSnapshot>;
onProgress(handler: (transfer: TransferSnapshot) => void): Disposable;
onComplete(handler: (transfer: TransferSnapshot) => void): Disposable;
onError(handler: (transfer: TransferSnapshot) => void): Disposable;
};
export type PluginProfilerAPI = {
getMetrics(nodeId: string): ProfilerMetricsSnapshot | null;
getHistory(nodeId: string, maxPoints?: number): ReadonlyArray<ProfilerMetricsSnapshot>;
isRunning(nodeId: string): boolean;
onMetrics(nodeId: string, handler: (metrics: ProfilerMetricsSnapshot) => void): Disposable;
};
export type PluginEventLogAPI = {
getEntries(filter?: { severity?: 'info' | 'warn' | 'error'; category?: 'connection' | 'reconnect' | 'node' }): ReadonlyArray<EventLogEntrySnapshot>;
onEntry(handler: (entry: EventLogEntrySnapshot) => void): Disposable;
};
export type PluginIdeAPI = {
isOpen(): boolean;
getProject(): IdeProjectSnapshot | null;
getOpenFiles(): ReadonlyArray<IdeFileSnapshot>;
getActiveFile(): IdeFileSnapshot | null;
onFileOpen(handler: (file: IdeFileSnapshot) => void): Disposable;
onFileClose(handler: (path: string) => void): Disposable;
onActiveFileChange(handler: (file: IdeFileSnapshot | null) => void): Disposable;
};
export type PluginAiAPI = {
getConversations(): ReadonlyArray<AiConversationSnapshot>;
getMessages(conversationId: string): ReadonlyArray<AiMessageSnapshot>;
getActiveProvider(): Readonly<{ type: string; displayName: string }> | null;
getAvailableModels(): ReadonlyArray<string>;
onMessage(handler: (info: Readonly<{ conversationId: string; messageId: string; role: string }>) => void): Disposable;
};
export type PluginAppAPI = {
getTheme(): ThemeSnapshot;
getSettings(category: 'terminal' | 'appearance' | 'general' | 'buffer' | 'sftp' | 'reconnect'): Readonly<Record<string, unknown>>;
getVersion(): string;
getPlatform(): 'macos' | 'windows' | 'linux';
getLocale(): string;
onThemeChange(handler: (theme: ThemeSnapshot) => void): Disposable;
onSettingsChange(category: string, handler: (settings: Readonly<Record<string, unknown>>) => void): Disposable;
getPoolStats(): Promise<PoolStatsSnapshot>;
};
// ── Plugin Context ──────────────────────────────────────────
export type PluginContext = Readonly<{
pluginId: string;
connections: PluginConnectionsAPI;
events: PluginEventsAPI;
ui: PluginUIAPI;
terminal: PluginTerminalAPI;
settings: PluginSettingsAPI;
i18n: PluginI18nAPI;
storage: PluginStorageAPI;
api: PluginBackendAPI;
assets: PluginAssetsAPI;
/** v3 namespaces */
sessions: PluginSessionsAPI;
transfers: PluginTransfersAPI;
profiler: PluginProfilerAPI;
eventLog: PluginEventLogAPI;
ide: PluginIdeAPI;
ai: PluginAiAPI;
app: PluginAppAPI;
}>;
// ── Plugin Manifest (v2) ────────────────────────────────────
export type PluginManifest = {
id: string;
name: string;
version: string;
description?: string;
author?: string;
main: string;
engines?: { oxideterm?: string };
// v2 Package fields
manifestVersion?: 1 | 2;
format?: 'bundled' | 'package';
assets?: string;
styles?: string[];
sharedDependencies?: Record<string, string>;
repository?: string;
checksum?: string;
contributes?: { /* ... */ };
locales?: string;
};
// ── Plugin Module ───────────────────────────────────────────
export type PluginModule = {
activate: (ctx: PluginContext) => void | Promise<void>;
deactivate?: () => void | Promise<void>;
};
// ── Shared Modules (window.__OXIDE__) ───────────────────────
declare global {
interface Window {
__OXIDE__?: {
React: typeof import('react');
ReactDOM: { createRoot: typeof import('react-dom/client').createRoot };
zustand: { create: typeof import('zustand').create };
lucideIcons: Record<string, React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>>;
/** @deprecated Use lucideIcons instead. Kept for backward compatibility. */
lucideReact: typeof import('lucide-react');
ui: PluginUIKit; // 24 个预置 UI 组件
version: string; // OxideTerm 版本号
pluginApiVersion: number; // 插件 API 版本号 (3 = current)
};
}
}

{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["id", "name", "version", "main"],
"properties": {
"id": {
"type": "string",
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9_-]*$",
"description": "Unique plugin identifier"
},
"name": { "type": "string", "description": "Human-readable plugin name" },
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+", "description": "Semver version" },
"description": { "type": "string" },
"author": { "type": "string" },
"main": { "type": "string", "description": "Relative path to ESM entry file" },
"manifestVersion": {
"type": "integer", "enum": [1, 2], "default": 1,
"description": "Manifest schema version; set to 2 for v2 Package format"
},
"format": {
"type": "string", "enum": ["bundled", "package"], "default": "bundled",
"description": "bundled = single-file Blob URL; package = multi-file HTTP Server"
},
"assets": {
"type": "string",
"description": "Relative path to assets directory (v2 Package only)"
},
"styles": {
"type": "array", "items": { "type": "string" },
"description": "CSS files to auto-load on activation (v2 Package only)"
},
"sharedDependencies": {
"type": "object",
"additionalProperties": { "type": "string" },
"description": "Dependencies provided by host via window.__OXIDE__"
},
"repository": {
"type": "string",
"description": "Repository URL for source code"
},
"checksum": {
"type": "string",
"description": "SHA-256 hash of the main entry file for integrity verification"
},
"engines": {
"type": "object",
"properties": {
"oxideterm": { "type": "string", "pattern": "^>=?\\d+\\.\\d+\\.\\d+" }
}
},
"locales": { "type": "string", "description": "Relative path to locales directory" },
"contributes": {
"type": "object",
"properties": {
"tabs": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "title", "icon"],
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"icon": { "type": "string", "description": "Lucide React icon name" }
}
}
},
"sidebarPanels": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "title", "icon"],
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"icon": { "type": "string" },
"position": { "type": "string", "enum": ["top", "bottom"], "default": "bottom" }
}
}
},
"settings": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "type", "default", "title"],
"properties": {
"id": { "type": "string" },
"type": { "type": "string", "enum": ["string", "number", "boolean", "select"] },
"default": {},
"title": { "type": "string" },
"description": { "type": "string" },
"options": {
"type": "array",
"items": {
"type": "object",
"required": ["label", "value"],
"properties": {
"label": { "type": "string" },
"value": {}
}
}
}
}
}
},
"terminalHooks": {
"type": "object",
"properties": {
"inputInterceptor": { "type": "boolean" },
"outputProcessor": { "type": "boolean" },
"shortcuts": {
"type": "array",
"items": {
"type": "object",
"required": ["key", "command"],
"properties": {
"key": { "type": "string" },
"command": { "type": "string" }
}
}
}
}
},
"connectionHooks": {
"type": "array",
"items": { "type": "string", "enum": ["onConnect", "onDisconnect", "onReconnect", "onLinkDown"] }
},
"apiCommands": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
}

文件职责
src/types/plugin.ts所有插件类型定义
src/store/pluginStore.tsZustand 插件状态管理
src/lib/plugin/pluginLoader.ts生命周期管理(发现/加载/卸载/断路器)
src/lib/plugin/pluginContextFactory.ts构建冻结的 PluginContext 膜
src/lib/plugin/pluginEventBridge.ts事件桥接(appStore → plugin events)
src/lib/plugin/pluginTerminalHooks.ts终端 I/O hook 管线
src/lib/plugin/pluginStorage.tslocalStorage KV 存储封装
src/lib/plugin/pluginSettingsManager.ts设置管理(声明+持久化+change 通知)
src/lib/plugin/pluginI18nManager.ts插件 i18n 封装(i18next 集成)
src/lib/plugin/pluginUtils.ts共享工具函数(路径验证、安全检查)
src/lib/plugin/pluginUIKit.tsx24 个预置 UI 组件(UIKit)
src-tauri/src/commands/plugin.rsRust 后端(文件 I/O + 路径安全)
src-tauri/src/commands/plugin_server.rsPlugin File Server(多文件 HTTP 访问)
src-tauri/src/commands/plugin_registry.rs插件仓库注册/搜索
src/components/plugin/PluginManagerView.tsxPlugin Manager UI
src/components/plugin/PluginTabRenderer.tsx插件 Tab 渲染器
src/components/plugin/PluginSidebarRenderer.tsx插件 Sidebar 渲染器
src/components/plugin/PluginConfirmDialog.tsx主题化确认对话框(Radix UI)
src/lib/plugin/pluginSnapshots.tsv3 快照生成工厂(冻结 + 深拷贝)
src/lib/plugin/pluginThrottledEvents.tsv3 节流事件桥接(transfers 500ms / profiler 1s)

本文档基于 OxideTerm v1.6.2(Plugin API v3)插件系统源码更新。最后更新:2026-03-15。如有疑问,请参考上述源码文件或提交 Issue。