更新记录
1.0.0(2026-08-16)
- 首次发布。
- 支持 Android、iOS 原生 SSH 连接、交互式 Shell 与终端渲染。
- 支持密码和私钥认证、主机密钥校验及 known_hosts 管理。
- 支持终端按键、尺寸调整、文本选择、剪贴板和主题配置。
平台兼容性
uni-app x(5.24)
| Chrome | Safari | Android | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|
| - | - | √ | √ | - | - |
原生 SSH 终端渲染组件
hans-terminal 是集 SSH 连接、终端仿真与原生渲染于一体的 uni-app x Android、iOS 组件,提供命令执行、交互式 Shell、会话管理、known_hosts 和剪贴板能力。
平台兼容
| 平台 | 支持情况 | 最低版本/架构 |
|---|---|---|
| Android | 支持 | Android 8.0(API 26);arm64-v8a、x86_64 |
| iOS | 支持 | iOS 16.4 |
| HarmonyOS | 不支持 | 调用 API 会返回不支持错误 |
| Web、小程序、uni-app | 不支持 | 仅支持 uni-app x App |
HBuilderX 最低版本为 4.31。
Android 调试必须使用包含本插件原生库的自定义基座,标准基座可以编译,但运行时无法加载插件的 .so 文件。正式打包时也应确认目标 ABI 在上表范围内。
安装
将插件目录放到项目的 uni_modules/hans-terminal。组件会被 easycom 自动识别;API 和类型统一从插件入口导入:
import type { HansTerminalConnectResult } from "@/uni_modules/hans-terminal"
import {
connect,
startShell
} from "@/uni_modules/hans-terminal"
不要直接导入 utssdk 下的公共接口或平台实现文件。
基本用法
下面的示例覆盖 SSH 连接、首次主机密钥确认、启动交互式 Shell 和组件绑定。生产环境中应通过可信渠道核对 fingerprintSha256,不要自动接受未知或已变更的主机密钥。
<template>
<view class="page">
<hans-terminal
class="terminal"
:shell-id="shellId"
color-scheme="default"
cursor-blink="on"
@terminal-event="handleTerminalEvent"
@init-error="handleInitError"
/>
<view v-if="pendingHostKeyId.length > 0">
<text>{{ pendingFingerprint }}</text>
<button @click="acceptHostKey">信任并连接</button>
<button @click="rejectHostKey">拒绝</button>
</view>
<button @click="openTerminal">连接 SSH</button>
<button @click="closeTerminal">断开连接</button>
</view>
</template>
<script setup lang="uts">
import type {
HansTerminalConnectResult,
HansTerminalEvent,
HansTerminalShellResult
} from "@/uni_modules/hans-terminal"
import {
closeShell,
connect,
disconnect,
respondToHostKey,
startShell
} from "@/uni_modules/hans-terminal"
type HansTerminalComponentErrorEvent = {
message?: string | null
}
const connectionId = ref<string>("")
const shellId = ref<string>("")
const pendingHostKeyId = ref<string>("")
const pendingFingerprint = ref<string>("")
function handleApiError(error: any): void {
console.error(error)
}
function openTerminal(): void {
connect({
host: "192.168.1.20",
port: 22,
username: "demo",
auth: {
type: "password",
password: "your-password"
},
timeoutMs: 15000,
knownHosts: true
}).then((connection: HansTerminalConnectResult): void => {
connectionId.value = connection.connectionId
startShell(connection.connectionId, {
term: "xterm-256color",
cols: 80,
rows: 24,
scrollbackLines: 10000
}).then((shell: HansTerminalShellResult): void => {
shellId.value = shell.shellId
}).catch((error: any): void => {
handleApiError(error)
})
}).catch((error: any): void => {
handleApiError(error)
})
}
function handleTerminalEvent(event: HansTerminalEvent): void {
if (
event.type == "hostKeyPending" ||
event.type == "hostKeyChanged"
) {
pendingHostKeyId.value = event.connectionId ?? ""
pendingFingerprint.value = event.info?.fingerprintSha256 ?? ""
}
}
function acceptHostKey(): void {
if (pendingHostKeyId.value.length == 0) return
respondToHostKey(pendingHostKeyId.value, true)
pendingHostKeyId.value = ""
}
function rejectHostKey(): void {
if (pendingHostKeyId.value.length == 0) return
respondToHostKey(pendingHostKeyId.value, false)
pendingHostKeyId.value = ""
}
function closeTerminal(): void {
const currentShellId: string = shellId.value
const currentConnectionId: string = connectionId.value
shellId.value = ""
connectionId.value = ""
if (currentShellId.length > 0) {
closeShell(currentShellId)
}
if (currentConnectionId.length > 0) {
disconnect(currentConnectionId)
}
}
function handleInitError(event: HansTerminalComponentErrorEvent): void {
console.error(event.message ?? "terminal init failed")
}
onUnmounted((): void => {
closeTerminal()
})
</script>
<style>
.page {
flex: 1;
}
.terminal {
width: 100%;
height: 360px;
}
</style>
connect() 在首次连接或主机密钥变化时会等待 respondToHostKey(),因此应在调用 connect() 前确保事件监听已经生效。挂载 <hans-terminal> 时,组件会自动监听并轮询运行时事件。页面状态使用 ref<T>(),API 成功回调和组件事件也建议显式标注插件导出的类型;Promise 的拒绝值在 UTS 中没有静态泛型参数,因此 catch 参数只能先写为 any,再在统一错误处理函数中收窄。
组件属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
shell-id |
string |
"" |
要显示的交互式 Shell ID 或预览 ID |
font-size |
number |
14 |
字号;Android 限制为 10-28,iOS 限制为 4-64 |
padding |
number |
8 |
终端内容内边距,范围 0-32 |
cursor-style |
string |
"block" |
block、beam、underline;hollow 仅 Android 原生支持,iOS 会回退为 block |
cursor-blink |
string |
"off" |
off、on、always;iOS 将 always 按 on 处理 |
blink-interval |
number |
750 |
Android 光标闪烁间隔,范围 120-2000 毫秒;iOS 由系统终端引擎控制 |
blink-timeout |
number |
5 |
预留属性,当前版本不生效 |
color-scheme |
string |
"default" |
default、solarizedDark、solarizedLight、dracula、gruvboxDark |
bold-is-bright |
boolean |
true |
是否将 ANSI 基础色的粗体显示为亮色 |
background-color |
string |
"#0b1020" |
背景色,使用 #RRGGBB |
refresh-token |
number |
0 |
更新预览内容后递增该值,可主动触发视图刷新 |
主题可用值为 default、solarizedDark、solarizedLight、dracula、gruvboxDark;光标样式可用值为 block、beam、underline、hollow。
组件事件
| 事件 | 说明 |
|---|---|
ready |
原生视图初始化完成,或绑定的 Shell 发出 shellReady |
init-error |
原生视图初始化失败,或绑定的 Shell 发出 shellError |
terminal-event |
转发当前 shell-id 相关的运行时事件;没有 shellId 的连接级事件也会转发 |
title-change |
收到终端标题变化事件 |
bell |
收到终端 BEL 事件 |
selection-change |
copySelection() 成功后触发 |
组件事件使用 kebab-case 监听,例如 @terminal-event、@title-change。
SSH 连接
连接参数
connect(options) 接收以下参数:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
host |
string |
是 | SSH 主机名或 IP |
port |
number |
是 | 端口,范围 1-65535 |
username |
string |
是 | SSH 用户名 |
auth.type |
string |
是 | password 或 privateKey |
auth.password |
string |
密码认证时 | 密码 |
auth.privateKey |
string |
私钥认证时 | OpenSSH 格式私钥 |
auth.passphrase |
string |
否 | 加密私钥的口令 |
timeoutMs |
number |
否 | 超时时间,默认 15000 毫秒,有效范围 1000-120000 |
knownHosts |
boolean |
否 | 是否读取并保存本机 known_hosts,默认 true |
私钥认证示例:
import type { HansTerminalConnectResult } from "@/uni_modules/hans-terminal"
import { connect } from "@/uni_modules/hans-terminal"
const privateKeyPem: string = "-----BEGIN OPENSSH PRIVATE KEY-----\n..."
const passphrase: string = ""
connect({
host: "example.com",
port: 22,
username: "demo",
auth: {
type: "privateKey",
privateKey: privateKeyPem,
passphrase: passphrase
}
}).then((connection: HansTerminalConnectResult): void => {
console.log(connection.connectionId)
})
主机密钥校验
- 第一次连接会发出
hostKeyPending,由业务核对指纹后调用respondToHostKey(pendingId, true|false)。 - 接受后,主机公钥按
host + port保存;以后匹配时发出hostKeyTrusted并自动连接。 - 已保存的密钥与服务器不一致时会先发出
hostKeyChanged,再发出hostKeyPending,不会自动连接。 knownHosts: false仍会要求本次确认,但接受后不会保存。getKnownHosts()、removeKnownHost(host, port)、clearKnownHosts()用于管理已保存记录。
交互式 Shell
import type { HansTerminalShellResult } from "@/uni_modules/hans-terminal"
import {
closeShell,
resize,
sendData,
sendKey,
startShell
} from "@/uni_modules/hans-terminal"
async function openShell(connectionId: string): Promise<void> {
const shell: HansTerminalShellResult = await startShell(connectionId, {
term: "xterm-256color",
cols: 80,
rows: 24,
scrollbackLines: 10000
})
const enterData: ArrayBuffer = new Uint8Array([13]).buffer
await sendData(shell.shellId, enterData)
await sendKey(shell.shellId, "arrowUp")
await sendKey(shell.shellId, "c", { ctrl: true })
await resize(shell.shellId, 100, 30)
await closeShell(shell.shellId)
}
startShell() 的 term、cols、rows 和 scrollbackLines 当前均为必填字段。shellIntegration 是预留选项,当前版本不会改变 Shell 行为。
sendKey() 支持 Enter、Backspace、Tab、Shift-Tab、Esc、方向键、Home/End、Insert/Delete、PageUp/PageDown、F1-F12、Ctrl 组合键、Alt 组合键和数字键盘键。options 可包含 alt、ctrl、shift、applicationCursor、applicationKeypad、altSendsEscape。
iOS 的终端视图以实时终端模式为准,不应用 applicationCursor、applicationKeypad 和 altSendsEscape 覆盖值;Android 支持这些覆盖值。
命令与会话管理
| API | 返回值 | 说明 |
|---|---|---|
connect(options) |
Promise<{ connectionId }> |
校验凭据和主机密钥并创建连接配置 |
disconnect(connectionId) |
Promise<void> |
断开连接并关闭该连接关联的 Shell |
runCommand(connectionId, command) |
Promise<{ stdout, stderr, exitCode }> |
执行一次命令;每次调用会重新建立 exec 连接 |
startShell(connectionId, options) |
Promise<{ shellId }> |
启动长连接 PTY Shell |
closeShell(shellId) |
Promise<void> |
关闭一个 Shell |
resize(shellId, cols, rows) |
Promise<void> |
修改 PTY 尺寸 |
sendData(shellId, data) |
Promise<void> |
向 Shell 发送原始 ArrayBuffer 字节 |
sendKey(shellId, key, options?) |
Promise<void> |
发送终端按键 |
getConnections() |
HansTerminalConnectionInfo[] |
获取连接摘要,不包含密码或私钥 |
getShells() |
HansTerminalShellInfo[] |
获取 Shell 摘要和尺寸 |
isConnectionAlive(connectionId) |
boolean |
查询连接是否存在 |
isShellAlive(shellId) |
boolean |
查询 Shell 是否存活 |
closeAllShells() |
number |
关闭全部 Shell,返回实际关闭数量 |
disconnectAll() |
number |
关闭 Shell 并断开全部连接,返回实际断开数量 |
getRuntimeInfo() 可用于排查运行环境,返回当前平台、原生运行时是否可用、预览会话数量和最近一次错误。setTerminalDebugLogging(true) 可临时开启插件日志,问题定位完成后应及时关闭。
滚动、选择与剪贴板
以下 API 需要目标 shellId 已绑定到一个挂载中的 <hans-terminal>:
async function useTerminalSelection(shellId: string): Promise<void> {
await scroll(shellId, 96)
selectionStart(shellId, 24, 64, "line")
selectionUpdate(shellId, 24, 320)
const selected: string | null = selectionText(shellId)
selectionAll(shellId)
const copied: string = await copySelection(shellId)
const link: string | null = hyperlinkAt(shellId, 24, 350)
console.log(selected, copied, link)
await pasteText(shellId, "echo hello\n")
await pasteClipboard(shellId)
selectionClear(shellId)
}
坐标参数 x、y 使用组件内的原生视图坐标。hyperlinkAt() 返回对应位置的 OSC 8 链接,没有链接时返回 null。
剪贴板 API:
| API | 说明 |
|---|---|
setClipboardText(text) |
写入系统剪贴板 |
getClipboardText() |
读取系统剪贴板 |
copySelection(shellId) |
将当前选择写入剪贴板并返回文本 |
pasteText(shellId, text, options?) |
通过终端输入路径粘贴文本 |
pasteClipboard(shellId, options?) |
读取剪贴板并粘贴 |
Android 支持 options.bracketed 强制开启或关闭 bracketed paste。iOS 忽略该覆盖值,使用终端当前协商的 bracketed-paste 状态。
运行时事件
不使用组件时,可以直接订阅事件:
import type {
HansTerminalEvent,
HansTerminalUnsubscribe
} from "@/uni_modules/hans-terminal"
import {
onTerminalEvent,
pollTerminalEvents
} from "@/uni_modules/hans-terminal"
const unsubscribe: HansTerminalUnsubscribe = onTerminalEvent((event: HansTerminalEvent): void => {
console.log(event.type, event.connectionId, event.shellId)
})
// 仅在未挂载 <hans-terminal> 时按业务需要定时调用。
const events: Array<HansTerminalEvent> = pollTerminalEvents()
// 页面卸载时取消订阅。
unsubscribe()
挂载 <hans-terminal> 后组件会自动调用 pollTerminalEvents(),业务无需重复高频轮询。
事件按用途分为:
| 类别 | event.type |
|---|---|
| 连接 | connected、disconnected、connectionsDisconnected、error |
| 主机密钥 | hostKeyPending、hostKeyAccepted、hostKeyRejected、hostKeyChanged、hostKeyTrusted、hostKeyResponse、knownHostSaved、knownHostRemoved、knownHostsCleared |
| Shell | shellStarted、shellReady、shellData、shellResized、shellExit、shellError、shellClosed、shellsClosed |
| 命令 | commandStarted、commandFinished、commandFailed |
| Shell 集成 | workingDirectoryChanged、promptStart、promptEnd、commandText、titleChanged、bell |
| 交互 | clipboardChanged、selectionCopied、pasted |
| 预览 | previewCreated、previewData、previewClosed |
HansTerminalEvent 可能包含 connectionId、shellId、message、info、path、command、exitCode、durationMs。使用上表中的字符串判断 event.type。
预览模式
预览模式无需 SSH,可用于展示静态或增量终端内容:
import {
appendPreviewTerm,
closePreviewTerm,
createPreviewTerm
} from "@/uni_modules/hans-terminal"
const previewId: string = "preview-1"
const refreshToken = ref<number>(0)
createPreviewTerm(previewId, "$ echo ready\nready")
appendPreviewTerm(previewId, "\n$ echo next\nnext")
refreshToken.value = refreshToken.value + 1
// <hans-terminal :shell-id="previewId" :refresh-token="refreshToken" />
closePreviewTerm(previewId)
getPreviewText(previewId) 可读取纯文本,getPreviewSnapshotJson(previewId) 可读取终端快照 JSON。
密钥工具
当前仅支持 Ed25519:
type HansTerminalGeneratedKeyInfo = {
type: string
publicKey: string
fingerprintSha256: string
encrypted: boolean
privateKey: string
}
type HansTerminalValidatedKeyInfo = {
type: string
publicKey: string
fingerprintSha256: string
encrypted: boolean
}
import {
generateKeyPair,
validatePrivateKey
} from "@/uni_modules/hans-terminal"
const generated: string = generateKeyPair("ed25519")
const generatedInfo: HansTerminalGeneratedKeyInfo = JSON.parse<HansTerminalGeneratedKeyInfo>(generated)
const validated: string = validatePrivateKey(generatedInfo.privateKey)
const validatedInfo: HansTerminalValidatedKeyInfo = JSON.parse<HansTerminalValidatedKeyInfo>(validated)
generateKeyPair() 返回的 JSON 包含 type、publicKey、fingerprintSha256、encrypted、privateKey;validatePrivateKey() 不返回私钥字段。生成的私钥由业务自行安全保存,插件不会自动写入持久存储。
错误码
异步 API 失败时返回 HansTerminalFail:
| 错误码 | 含义 |
|---|---|
9020001 |
当前平台不支持 |
9020002 |
API 尚未实现 |
9020003 |
参数无效 |
9020004 |
状态无效或目标不存在 |
9020005 |
原生运行时不可用 |
9020006 |
SSH 认证失败 |
9020007 |
SSH 主机密钥被拒绝 |
9020008 |
SSH 主机密钥已变化 |
9020009 |
操作超时 |
9020010 |
Shell 已关闭或不存在 |
9020011 |
剪贴板不可用 |
9020099 |
未分类错误 |
权限与数据
- Android 使用
android.permission.INTERNET;iOS 使用应用的出站网络能力。 - SSH 密码和私钥仅由原生层在当前连接生命周期内使用,不写入 known_hosts;
disconnect()/disconnectAll()后移除对应连接配置。 - known_hosts 仅持久化已接受的主机、端口、公钥算法、指纹和公钥内容。
- 只有业务显式调用复制、读取或粘贴 API 时,插件才访问系统剪贴板。
getConnections()和getShells()不返回密码或私钥。- 调试日志默认关闭,可通过
setTerminalDebugLogging(true)临时启用;生产环境建议保持关闭。
已知限制
- HarmonyOS、Web、小程序和 uni-app 项目不受支持。
- Android 标准基座不能加载插件原生库,调试需使用自定义基座。
runCommand()每次调用都会新建一次 exec 连接,不复用交互式 Shell。- 滚动、选择、复制和链接查询依赖已挂载且绑定相同
shellId的原生终端视图。 - iOS 的
sendKey()、pasteText()和pasteClipboard()也依赖已挂载的终端视图;Android 只要求对应 Shell 会话仍然有效。 shellIntegration和blink-timeout当前为预留参数。- iOS 不应用
sendKey()的终端模式覆盖值,也不应用粘贴的bracketed覆盖值。 - 复杂 TUI、Shell 集成脚本和不同 SSH 服务端的行为存在差异,上线前应在目标环境验证。

收藏人数:
购买源码授权版(
试用
使用 HBuilderX 导入示例项目
赞赏(0)
下载 387
赞赏 0
下载 12510232
赞赏 1943
赞赏
京公网安备:11010802035340号