跳转到内容

下载 Termii · v0.4.5

三分钟,装进你的 Dock

原生构建,包体仅数 MB。应用内自动更新,始终新鲜。

安装说明

macOS:若提示「无法打开」,在终端执行 xattr -dr com.apple.quarantine /Applications/Termii.app。Windows:首次运行可能触发 SmartScreen,选择「仍要运行」即可。

PluginContext API 参考

activate(ctx) 收到的 ctx 是插件与宿主之间的唯一边界。本节给出完整 签名(与宿主 src/lib/plugins/types.ts 对齐,SDK 自动生成同形类型)。

成员 类型 说明
ctx.pluginId readonly string 当前插件 id
ctx.apiVersion readonly number 宿主支持的插件 API 版本(当前 3,= SUPPORTED_API_VERSION)
ctx.ui object 贡献点注册 + toast / modal / navigate
ctx.plugins object 插件服务总线(expose / invoke / isActive)
ctx.terminal object 终端 pane 读写与输出订阅
ctx.sessions object 终端会话创建原语
ctx.process object 本地流式进程(L1 process)
ctx.hosts object 主机连接 / 执行 / 本机执行
ctx.tunnels object 端口转发(隧道)
ctx.dialog object 原生对话框(含目录选择)
ctx.fs object 受限文件写
ctx.vault object 凭证保险库(keyring),批量能力需 L1
ctx.config object 配置快照导出/导入(L1 process)
ctx.paths object 应用路径投影
ctx.sftp object 文件传输(同步形态)
ctx.sidecar object 原生能力桥(L1 sidecar)
ctx.storage object 插件私有持久化 KV
ctx.events object Tauri 事件订阅(白名单)
ctx.i18n object 文案注册 / 语言读取与监听

所有注册方法都返回 Disposer(() => void),插件去激活时宿主统一回放。

ctx.ui.registerView(view: ViewContribution): Disposer;
// ViewContribution {
// id: string; // 必须以 "<pluginId>." 开头
// icon: PluginIcon; // lucide 图标组件
// labelKey: string; // 文案键,默认在 "views" 命名空间解析
// ns?: string; // i18n 命名空间,缺省 "views"
// component: ComponentType;
// }

侧栏条目 + 主区组件。注册后自动获得 ⌘1..9 快捷键与命令面板入口(都从 可见侧栏派生)。

ctx.ui.registerCommand(cmd: CommandContribution): Disposer;
// CommandContribution {
// id: string;
// group: string; // 分组标题(已本地化的展示字符串)
// title: string;
// sub?: string;
// icon: PluginIcon;
// run: () => void | Promise<void>;
// }

⌘K 命令面板条目。

ctx.ui.registerSettingsSection(section: SettingsSectionContribution): Disposer;
// SettingsSectionContribution {
// id: string;
// icon: PluginIcon;
// labelKey: string; // 默认在 "settings" 命名空间解析
// ns?: string; // 缺省 "settings"
// component: ComponentType;
// }

设置页新分类。

ctx.ui.registerShortcut(shortcut: ShortcutContribution): Disposer;
// ShortcutContribution {
// id: string;
// combo: string; // 如 "Mod+Shift+D"
// run: () => void;
// }

combo 语法:Mod = ⌘(macOS) / Ctrl(其他),Cmd / Ctrl 是同义词; Shift / Alt 可选;最后一段为单字符键;大小写不敏感。内置快捷键 (⌘K / T / W / D、⌘0..9)优先命中,未处理的按键才落到插件快捷键表。

ctx.ui.registerTheme(theme: ThemeContribution): Disposer;
// ThemeContribution {
// id: string;
// label: string; // 主题卡片显示名(纯文本,自行本地化)
// dark: boolean; // 暗色主题标记
// previewBg?: string; // 卡片预览色;缺省取 vars 里的 --bg / --fg
// previewFg?: string;
// vars: Record<string, string>; // CSS 自定义属性(-- 开头),经消毒注入
// }

注入 :root[data-theme="<id>"] { …vars }。消毒规则:键必须 -- 开头、 不允许 {} / ;;值不允许 {} / </style;违规声明被静默丢弃。

ctx.ui.registerTrayItem(item: TrayItemContribution): Disposer;
// TrayItemContribution {
// id: string;
// label: string; // 菜单显示文本(纯文本,自行本地化)
// run: () => void;
// }

系统托盘右键菜单固定区条目(「新建本地终端」与「SSH 主机」子菜单之后), 经 tray://plugin-item 事件分发触发。

ctx.ui.toast.success(opts: { title: string; description?: string }): string;
ctx.ui.toast.error(opts: { title: string; description?: string }): string;
ctx.ui.toast.info(opts: { title: string; description?: string }): string;

返回 toast id。

ctx.ui.toast.running(opts: {
title: string;
description?: string;
progress?: { total: number; completed: number; stage: string }; // total 为 0 → indeterminate
}): string; // 持续 toast(不自动消失),结束时用 update 切换
ctx.ui.toast.update(id: string, patch: {
kind?: "success" | "error" | "info" | "running";
title?: string;
description?: string;
duration?: number; // 自动消失时长(ms)
progress?: { total; completed; stage } | null; // null 显式清空进度条
}): void;

典型流程见 界面反馈与文案。

ctx.ui.modal.confirm(opts: {
title: string;
body?: string;
confirmText?: string;
cancelText?: string;
danger?: boolean; // 红色确认按钮
}): Promise<boolean>;
ctx.ui.modal.alert(opts: { title: string; body?: string }): Promise<void>;
ctx.ui.modal.openForm(opts: {
title: ReactNode;
body: ReactNode; // 任意 ReactNode,弹窗栈顶层入口
footer?: ReactNode; // 底部槽位;null → 空 .dlg-footer 容器(ModalFooter portal 渲染按钮)
}): void;
ctx.ui.modal.close(): void; // 关闭当前最顶层弹窗
ctx.ui.modal.setCancelable(cancelable: boolean): void; // Esc / 遮罩关闭守卫(仅 form 弹窗)
ctx.ui.navigate(viewId: string): void;

切换主区视图(核心视图 id 或插件视图 id)。

ctx.plugins.expose(
service: string,
handler: (method: string, params: unknown) => unknown
): Disposer; // 注册本插件的一个服务;去激活自动摘除
ctx.plugins.invoke<T>(
pluginId: string,
service: string,
method: string,
params?: unknown
): Promise<T>; // 只允许调用已启用(active)插件;故障隔离
ctx.plugins.isActive(pluginId: string): boolean; // 同步探测,不触发加载

详见 插件服务总线与依赖。

ctx.terminal.getActivePane(): ActivePaneInfo | null;
// ActivePaneInfo { paneId: string; kind: "local" | "ssh" | "serial"; backendId: string }
// 预览等非终端 pane 返回 null
ctx.terminal.writeActive(text: string): Promise<boolean>; // 写入并聚焦活跃 pane
ctx.terminal.writePane(paneId: string, text: string): Promise<boolean>;
// 写入指定 pane(不在活跃 tab 也能写);pane 不存在 / 写入失败 → false
ctx.terminal.focusActive(): void;
ctx.terminal.onOutput(cb: (chunk: OutputChunk, pane: ActivePaneInfo) => void): Disposer;
// OutputChunk { seq: number; data: string };订阅活跃 pane 输出流,切换自动跟随
ctx.sessions.openLocalTab(): Promise<string | null>; // 新开本地终端 tab,返回 paneId
ctx.sessions.openHostTab(hostId: string): Promise<string | null>; // 新开 SSH tab(自动确保连接在线)
ctx.sessions.focus(paneId: string): void; // 聚焦 pane(切换所在 tab 为活跃)
ctx.process.spawn(
cmd: string,
args: string[],
opts?: { cwd?: string; env?: Record<string, string> }
): Promise<ProcessHandle>;
// 需要 L1 `process` 能力;argv 方式(不做 shell 展开)
interface ProcessHandle {
readonly id: string;
write(data: string): Promise<void>; // 写入 stdin
kill(): Promise<void>; // 终止子进程
onData(cb: (chunk: ProcChunk) => void): Disposer;
// ProcChunk { seq: number; data: string; stream: "stdout" | "stderr" }
onExit(cb: (info: { code: number | null; error?: string }) => void): Disposer;
// code 为 null 表示被终止 / 传输中断,error 给出原因
}

插件去激活时其全部进程被宿主强制回收(孤儿防护)。

ctx.hosts.list(): readonly HostSummary[];
// HostSummary { id, name, host, port, username, group?, tags: string[], favorite }
// 只读投影,不含任何凭证字段
ctx.hosts.connect(hostId: string): Promise<string>; // 建立(或复用)连接,返回 sessionId
ctx.hosts.exec(hostId, command: string, opts?: { timeoutSecs?: number }):
Promise<{ stdout: string; stderr: string; exitCode: number }>;
ctx.hosts.disconnect(hostId: string): Promise<void>;
ctx.hosts.reconnect(hostId: string): Promise<string>; // 重建 transport,返回新 sessionId
ctx.hosts.execLocal(command: string, opts?: { timeoutSecs?: number }):
Promise<{ stdout: string; stderr: string; exitCode: number }>;
// 本机执行(临时 PTY 只收集 stdout,stderr 恒为空串;默认超时 60s)
ctx.hosts.execStream(hostId: string, command: string): Promise<ProcessHandle>;
// SSH exec 通道流式执行;需要 L1 `process`;kill() 关闭通道终止远端进程
ctx.tunnels.list(hostId?: string): Promise<PluginForwardInfo[]>;
// PluginForwardInfo = ForwardInfo & { hostId: string }
// 缺省 hostId → 所有主机的转发;给 hostId → 只返回该主机名下的
ctx.tunnels.start(hostId: string, spec: ForwardSpec): Promise<ForwardInfo>;
// ForwardSpec { kind: "local" | "remote" | "dynamic"; bindHost; bindPort; targetHost; targetPort }
// 自动确保 SSH transport 在线
ctx.tunnels.stop(hostId: string, forwardId: string): Promise<void>;
ctx.tunnels.probeRemote(hostId: string, bindHost: string): Promise<RemoteForwardProbe>;
// 探测远端 sshd 是否会在 -R 转发时尊重 bindHost(启动 -R 前调用)
ctx.tunnels.applyRemoteConfig(hostId: string): Promise<void>;
// 把远端 GatewayPorts 翻成 clientspecified 并 reload sshd(需 NOPASSWD sudo)
ctx.tunnels.onChanged(cb: (hostId: string) => void): Disposer; // 订阅隧道增删变更
ctx.tunnels.saveRule(rule: TunnelRule): Promise<void>;
// 保存(新增或更新)隧道规则,经服务总线转发给 termii-tunnels 的 "tunnels" 服务;
// 只落库不启动;目标插件未启用 → reject
ctx.dialog.pickSavePath(opts?: {
defaultName?: string;
extensions?: string[]; // 文件类型过滤(不含前导点、小写,如 ["tar"])
}): Promise<string | null>; // 取消返回 null;选中的路径在本次会话内可被 fs.writeText 写入
ctx.dialog.pickFile(opts?: { extensions?: string[] }): Promise<string | null>;
ctx.dialog.pickDirectory(): Promise<string | null>; // 取消返回 null
ctx.fs.writeText(path: string, content: string): Promise<void>;
// 仅允许写本次会话内经 ctx.dialog.pickSavePath 返回的路径;未授权 → 抛错
ctx.vault.get(id: string): Promise<string | null>; // 不存在返回 null
ctx.vault.set(id: string, secret: string): Promise<void>;
ctx.vault.delete(id: string): Promise<void>; // 不存在时静默成功
// 批量能力:需要 L1 `process`
ctx.vault.list(): Promise<string[]>;
ctx.vault.export(): Promise<Record<string, string>>;
ctx.vault.import(entries: Record<string, string>): Promise<void>; // 整体替换

按 id 的 get/set/delete 无能力门禁(vault 与 hosts / tunnels / sessions 同属核心服务投影);list/export/import 涉及全量凭据,纳入 L1 process 信任锁。

ctx.config.export(): Promise<string>; // 当前完整配置快照(JSON 字符串)
ctx.config.import(json: string): Promise<void>; // 应用快照并立即落盘

需要 L1 process 能力。导出内容包含 hosts / settings / pluginSettings / layouts;导入会复用启动时的迁移/归一化逻辑并更新当前运行中的 store, 供配置同步类插件使用。

ctx.paths.appDataDir(): Promise<string>; // config.json 与 plugins/ 所在目录
ctx.sftp.upload(
hostId: string, localPath: string, remotePath: string,
onProgress?: (p: { transferred: number; total: number }) => void
): Promise<void>;
ctx.sftp.download(
hostId: string, remotePath: string, localPath: string,
onProgress?: (p: { transferred: number; total: number }) => void
): Promise<void>;

同步形态:Promise 在传输完成 / 失败后才 settle。内部使用主机的文件 SSH 会话(与 hosts.exec 的 terminal 会话分离)。total 在传输开始后 可能为 0(总大小未知)。错误消息透传 sftp 命令的原始错误。

ctx.sidecar.call<T = unknown>(
method: string,
params?: unknown,
timeoutMs?: number // 默认 10000
): Promise<T>;

需要 L1 sidecar(隐含 process)。错误分支见 流式进程与 sidecar。

ctx.storage.get<T>(key: string, fallback: T): T; // 同步读,缺省返回 fallback
ctx.storage.set(key: string, value: unknown): void;

落在 config.json 的 settings.pluginSettings.<pluginId>,按插件隔离。

ctx.events.listen(event: string, cb: (payload: unknown) => void): Disposer;

白名单前缀(白名单外订阅被拒绝并打印错误):

pty:// sshch:// serial:// ssh:// transfer://
fs-progress:// tray:// forward:// proc://
ctx.i18n.addBundle(lang: string, ns: string, resources: Record<string, unknown>): void;
// ns 被强制改写为 "plugin-<pluginId>"(传什么都一样,避免覆盖宿主命名空间);深合并
ctx.i18n.getLanguage(): string; // "en-US" / "zh-CN"
ctx.i18n.onLanguageChanged(cb: (lng: string) => void): Disposer;