更新记录

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-v8ax86_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" blockbeamunderlinehollow 仅 Android 原生支持,iOS 会回退为 block
cursor-blink string "off" offonalways;iOS 将 alwayson 处理
blink-interval number 750 Android 光标闪烁间隔,范围 120-2000 毫秒;iOS 由系统终端引擎控制
blink-timeout number 5 预留属性,当前版本不生效
color-scheme string "default" defaultsolarizedDarksolarizedLightdraculagruvboxDark
bold-is-bright boolean true 是否将 ANSI 基础色的粗体显示为亮色
background-color string "#0b1020" 背景色,使用 #RRGGBB
refresh-token number 0 更新预览内容后递增该值,可主动触发视图刷新

主题可用值为 defaultsolarizedDarksolarizedLightdraculagruvboxDark;光标样式可用值为 blockbeamunderlinehollow

组件事件

事件 说明
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 passwordprivateKey
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()termcolsrowsscrollbackLines 当前均为必填字段。shellIntegration 是预留选项,当前版本不会改变 Shell 行为。

sendKey() 支持 Enter、Backspace、Tab、Shift-Tab、Esc、方向键、Home/End、Insert/Delete、PageUp/PageDown、F1-F12、Ctrl 组合键、Alt 组合键和数字键盘键。options 可包含 altctrlshiftapplicationCursorapplicationKeypadaltSendsEscape

iOS 的终端视图以实时终端模式为准,不应用 applicationCursorapplicationKeypadaltSendsEscape 覆盖值;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)
}

坐标参数 xy 使用组件内的原生视图坐标。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
连接 connecteddisconnectedconnectionsDisconnectederror
主机密钥 hostKeyPendinghostKeyAcceptedhostKeyRejectedhostKeyChangedhostKeyTrustedhostKeyResponseknownHostSavedknownHostRemovedknownHostsCleared
Shell shellStartedshellReadyshellDatashellResizedshellExitshellErrorshellClosedshellsClosed
命令 commandStartedcommandFinishedcommandFailed
Shell 集成 workingDirectoryChangedpromptStartpromptEndcommandTexttitleChangedbell
交互 clipboardChangedselectionCopiedpasted
预览 previewCreatedpreviewDatapreviewClosed

HansTerminalEvent 可能包含 connectionIdshellIdmessageinfopathcommandexitCodedurationMs。使用上表中的字符串判断 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 包含 typepublicKeyfingerprintSha256encryptedprivateKeyvalidatePrivateKey() 不返回私钥字段。生成的私钥由业务自行安全保存,插件不会自动写入持久存储。

错误码

异步 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 会话仍然有效。
  • shellIntegrationblink-timeout 当前为预留参数。
  • iOS 不应用 sendKey() 的终端模式覆盖值,也不应用粘贴的 bracketed 覆盖值。
  • 复杂 TUI、Shell 集成脚本和不同 SSH 服务端的行为存在差异,上线前应在目标环境验证。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

Android: android.permission.INTERNET; iOS: outbound SSH network access; clipboard access requires no extra platform permission

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件本身不采集数据;SSH 连接信息由宿主应用传入并由原生层使用;接受的 SSH 主机公钥指纹/公钥会保存在本机 known_hosts 存储中,密码和私钥不会由 known_hosts 保存;剪贴板仅在业务显式调用复制/粘贴 API 时读取或写入

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。