Files
terminal-lab/terminal/docs/xterm-standalone-lab-plan-2026-03-01.md
douboer@gmail.com 3b7c1d558a first commit
2026-03-03 13:23:14 +08:00

52 KiB
Raw Blame History

xterm 移植跨平台终端核心架构方案2026-03-01

1. 目标与范围

本方案构建一个生产可用的跨平台终端核心库,将 xterm.js 的 VT 仿真能力移植为独立的、无 DOM 依赖的 TypeScript 包(packages/terminal-core),供 Web 端、iOS 端Capacitor WKWebView、微信小程序端三个平台共用各端仅需实现 Transport 与 Renderer 两个平台边界。

规范基线:

  1. 渲染与输入语义以 xterm.js@5.3.0 源码为准。
  2. 文档中的换行、回显、光标、粘贴规则均以该版本源码行为校正。
  3. 若未来升级 xterm 版本,需先做"规范差异审计"再修改本方案。

目标:

  1. 核心包独立packages/terminal-core 无 DOM 依赖,可在任意 JS Runtime浏览器/WKWebView/微信小程序逻辑层)中运行。
  2. 三端生产可用WebVue SPA、iOSCapacitor WKWebView 加载同一 Web 构建产物)、微信小程序(原生 WXML 组件 + 逻辑层复用 terminal-core
  3. 平台边界最薄:各端只需实现 TerminalTransport(网络层)和 RendererAdapter渲染层两个接口Session/VT 仿真/输入映射全部共用。
  4. SSH 协议外置SSH 握手与流转发由 apps/gatewayWebSocket或 iOS 原生插件负责,terminal-core 只处理 PTY stdout/stderr 字节流。
  5. 构建/运行可复用:命令约定统一为 dev/build/test/lint/typecheck

2. 非目标

  1. 不在本方案中实现 SSH 协议本身(由 apps/gateway 或 iOS 原生 Swift 插件负责)。
  2. 不修改 apps/gateway 的业务协议和转发逻辑。
  3. 不改 packages/shared 的现有模型定义。
  4. 不实现图片/Sixel/Kitty 图像协议渲染。

2.1 明确不实现的 xterm 能力

以下能力在本方案中确认跳过,遇到相关需求时应主动忽略,不可因"部分实现"引入不稳定行为:

  1. Kitty 键盘协议(CSI =u / >u / <u / ?uKittyKeyboardFlags)。
  2. Win32InputModeWindows 伪终端特有输入模式)。
  3. 鼠标跟踪模式(?1000 / ?1002 / ?1003onBinary 二进制鼠标上报通道)。
  4. screenReaderMode 及 ARIA 无障碍支持。
  5. Markers / DecorationsregisterMarker / registerDecoration)。
  6. Link ProviderregisterLinkProvider)。
  7. CharacterJoinerregisterCharacterJoiner / deregisterCharacterJoiner,仅 WebGL 渲染器使用)。
  8. LigaturesAddon / ImageAddonSixel/Kitty 图片协议)。
  9. SerializeAddon终端状态序列化OutputBuffer 承担类似职责)。
  10. Unicode Grapheme Clustersaddon-unicode-graphemesemoji 字形簇宽度计算)。
  11. DECSCA字符保护CSI "q)与 DECRQM模式查询CSI $p / ?$p)。
  12. macOptionIsMetaaltClickMovesCursorrightClickSelectsWord(桌面端特有选项)。
  13. OSC 52剪贴板写入、OSC 133Shell 集成/语义标记)及其它非标 OSC 扩展。
  14. attachCustomKeyEventHandler / attachCustomWheelEventHandler 公开 API通过 eventOwnershipRouter 统一管理,不暴露此 API

3. 独立性与复用规则

3.1 必须满足

  1. packages/terminal-core 不得 import apps/* 的业务模块;apps/* 可单向依赖 terminal-core
  2. 禁止运行时依赖 @xterm/*xterm 仅作为源码语义参考,不作为可执行依赖。
  3. terminal-core 构建产物在零 DOM 环境(微信小程序逻辑层)中可直接运行。
  4. 各平台的 Transport / Renderer / InputBridge 实现隔离在各自 apps/ 或平台目录内,不得混用。

3.2 允许复用

  1. 依赖版本策略可复用(例如与根工程保持同主版本)。
  2. 工程工具可复用TypeScript、Vite、ESLint、Vitest 配置风格)。
  3. 运行方式可复用(统一命令命名,不要求命令实现完全一致)。
  4. 主题 token 可复用现有 Web/小程序的成熟定义(颜色变量、语义色分层、主题切换策略),但实现代码保留在各端目录内。

3.3 核心包 DOM 隔离约束(强制)

  1. packages/terminal-core/src/ 内所有文件禁止直接引用 document / window / HTMLElement / Event / Canvas / CanvasRenderingContext2D 等任何 DOM/BOM API。
  2. 平台相关的测量(字符宽高、容器尺寸)以 IMeasureAdapter 接口注入;事件来源(键盘/IME/触摸)以 IInputSource 接口注入;terminal-core 只定义接口,不实现。
  3. sizeCalculator 在 core 包内只保留纯算法(给定 px 宽高 → cols/rowsDOM 测量(ResizeObserver)和 wx 测量(wx.createSelectorQuery)各自在 apps/ 中实现并注入。
  4. imeController 在 core 包内只保留 IME 状态机逻辑idle/composing/commit_pendingcompositionstart/end 等 DOM 事件由 Web 层订阅后调用 core 接口;小程序通过 bindinput + isComposing 等效调用同一接口。
  5. CI 中增加以下 typecheck 任务保证零 DOM 泄漏:
    tsc -p packages/terminal-core/tsconfig.json --lib esnext --noEmit
    

4. 目录规划(实施级)

packages/terminal-core/          ← 核心包,零 DOM三端共用
  package.json                   ← name: "@remoteconn/terminal-core"
  tsconfig.json                  ← lib: ["esnext"],无 dom lib
  src/
    session/
      sessionMachine.ts          ← 会话状态机(纯状态逻辑)
      sessionTypes.ts
    transport/
      terminalTransport.ts       ← TerminalTransport 接口定义
    renderer/
      terminalCore.ts            ← VT 仿真内核(无 DOM
      rendererAdapter.ts         ← RendererAdapter 接口定义
      outputBuffer.ts
    input/
      inputBridge.ts             ← 键序列映射纯逻辑
      imeController.ts           ← IME 状态机(无 DOM 事件)
      IInputSource.ts            ← 平台输入源接口
    layout/
      sizeCalculator.ts          ← 纯算法px → cols/rows
      cursorMath.ts
      IMeasureAdapter.ts         ← 平台测量接口
    sanitize/
      terminalSanitizer.ts

packages/shared/                 ← 现有,不改

apps/web/                        ← Vue SPAWeb + iOS Capacitor WKWebView
  src/
    transport/
      gatewayTransport.ts        ← WebSocket → TerminalTransport
      iosNativeTransport.ts      ← Capacitor Bridge → TerminalTransport
    renderer/
      compatRenderer.ts          ← Canvas DOM 渲染
      textareaRenderer.ts        ← textarea DOM 渲染
    input/
      domInputBridge.ts          ← DOM 事件 → IInputSource
      domImeController.ts        ← compositionstart/end → ImeController
    layout/
      domMeasureAdapter.ts       ← ResizeObserver → IMeasureAdapter
    pages/TerminalPage.vue
    components/
      TerminalToolbar.vue
      TerminalViewport.vue
      TerminalInputBar.vue
      TerminalTouchTools.vue
    stores/useTerminalStore.ts
    styles/terminal.css

apps/miniprogram/                ← 微信小程序(原生组件)
  utils/
    wxTransport.js               ← wx.connectSocket → TerminalTransport
    wxInputBridge.js             ← bindinput/bindconfirm → IInputSource
    wxMeasureAdapter.js          ← wx.createSelectorQuery → IMeasureAdapter
  components/
    terminal-core-view/          ← WXML 渲染,消费 TerminalCore.snapshot()
      index.js  index.wxml  index.wxss

ios/plugin/RemoteConnSSHPlugin/  ← Swift SSH 插件(补全骨架后供 Capacitor 调用)

4.1 微信小程序消费 terminal-core 构建产物(工程约束)

由于小程序逻辑层不支持 TypeScript 直接运行,需要以下构建策略:

  1. packages/terminal-core/package.json 增加 "miniprogram": "dist-miniprogram/index.js" 字段,提供预编译纯 JS CommonJS 产物。
  2. 对应构建命令:tsc -p packages/terminal-core/tsconfig.json --module commonjs --outDir packages/terminal-core/dist-miniprogram,产物为零 DOM 纯 JS可直接被小程序 require()
  3. apps/miniprogram/utils/wxTransport.js 等文件通过相对路径引用:require('../../../packages/terminal-core/dist-miniprogram/index')
  4. CI 中小程序相关构建步骤之前,必须先完成 terminal-core 构建(根 package.json scripts 中保证 workspaces 构建顺序,或通过 prebuild 钩子)。
  5. 源码变更后必须重建 dist-miniprogram禁止小程序逻辑层直接 require TypeScript 源文件。

5. 总体架构与数据流

┌────────────── packages/terminal-core零 DOM三端共用──────────────┐
│  SessionMachine → TerminalTransport(接口) ←→ Gateway/iOS/wx         │
│  stdout/stderr → TerminalSanitizer → OutputBuffer → TerminalCore    │
│  TerminalCore.snapshot() → RendererAdapter(接口) → 平台渲染层        │
│  IInputSource(接口) → InputBridge(键序列映射) → Transport.send()     │
│  IMeasureAdapter(接口) → sizeCalculator(px→cols/rows) → resize       │
└───────────────────────────────────────────────────────────────────────┘
         ↓ 接口注入                          ↓ 接口注入
┌── Web / iOS ────────────────┐   ┌── 微信小程序 ────────────────────┐
│ GatewayTransport(WebSocket) │   │ WxTransport(wx.connectSocket)   │
│ IosNativeTransport(Capacitor│   │ terminal-core-view(WXML组件)    │
│ compatRenderer(Canvas DOM)  │   │ wxInputBridge(bindinput)        │
│ textareaRenderer(textarea)  │   │ wxMeasureAdapter(selectorQuery) │
│ domInputBridge              │   └─────────────────────────────────┘
│ domMeasureAdapter(ResizeObs)│
└─────────────────────────────┘

关键原则:

  1. 传输层不感知渲染器类型;渲染器不感知传输层。
  2. 输入处理统一走 InputBridge(键序列映射逻辑),事件来源由各平台的 IInputSource 实现注入。
  3. 渲染器通过统一 RendererAdapter 接口实现,支持 compat/textarea 切换和跨端适配。
  4. 输入映射遵循 xterm 规则:Enter -> CR,粘贴文本行结束归一到 CR
  5. 输出渲染遵循 xterm 控制字符纪律:CRLF 分离语义,convertEol 默认 false
  6. 原生响应优先软键盘、选区、滚动由原生通道Web textarea / 小程序 input)主导,不得被 terminal-core 接管。
  7. 事件主权清晰:键盘/触摸事件先由应用层做区域过滤与路由,再决定是否进入终端输入链路。

6. 状态机规划

6.1 会话状态机(连接生命周期)

状态集合(与现有模型对齐):

  1. idle
  2. connecting
  3. auth_pending
  4. connected
  5. reconnecting
  6. disconnected
  7. error

合法迁移(必须在 sessionMachine.ts 中强校验):

  1. idle -> connecting | disconnected
  2. connecting -> auth_pending | error | disconnected
  3. auth_pending -> connected | error | disconnected
  4. connected -> reconnecting | disconnected | error
  5. reconnecting -> connected | error | disconnected
  6. disconnected -> connecting | idle
  7. error -> connecting | disconnected

触发事件:

  1. connect_request
  2. socket_open
  3. connected_frame
  4. stdout_first_packet
  5. disconnect_frame/ws_close
  6. error_frame/ws_error
  7. manual_disconnect
  8. auto_reconnect_timeout

6.2 输入状态机IME/普通输入)

状态:

  1. input_idle
  2. input_composing
  3. input_commit_pending

规则:

  1. compositionstart -> input_composing
  2. compositionend -> 进入 input_commit_pending,提交非 ASCII 候选
  3. beforeinput/input 非组合态可触发 ASCII 兜底发送
  4. 超时守卫(如 1.8s)未收到 compositionend 时自动恢复到 input_idle

6.3 触摸焦点状态机(移动端)

动作集合:

  1. BLUR_ONLY
  2. FOCUS_KEYBOARD
  3. PASS_NATIVE
  4. PASS_SCROLL

决策优先级:

  1. hasSelectionStart || hasSelectionEnd -> PASS_NATIVE
  2. scrollLike -> PASS_SCROLL
  3. moved -> PASS_NATIVE
  4. !inBand -> BLUR_ONLY
  5. inBand -> FOCUS_KEYBOARD

硬约束(新增):

  1. 只有 FOCUS_KEYBOARD 动作允许触发原生键盘弹出。
  2. FOCUS_KEYBOARD 必须满足“光标附近行”条件(inBand=true),禁止在非光标邻近区域弹键盘。
  3. PASS_NATIVE/PASS_SCROLL 禁止 preventDefault,确保原生选区和滚动不被破坏。
  4. 禁止在终端根容器上全局拦截 touchstart/touchmove/click;仅允许在明确手势分支下做最小拦截。
  5. 移动端 compat 仅承担回显渲染,输入焦点必须由原生 textarea 锚点持有。
  6. 禁止在非用户手势路径下调用 terminal.focus() 触发软键盘。

6.4 渲染模式状态机

状态:

  1. renderer_compat
  2. renderer_textarea

切换事件:

  1. switch_to_compat
  2. switch_to_textarea

切换过程必须原子化:

  1. oldRenderer.dispose()
  2. newRenderer.mount(container)
  3. replay buffer
  4. apply size/theme
  5. focus restore(仅桌面自动聚焦)

6.5 换行模式状态xterm 对齐)

状态:

  1. convertEol=false(默认)
  2. convertEol=true(由 CSI 20 h 打开)

规则:

  1. LF/VT/FF 进入 lineFeed():始终 y+1,并在触底时滚屏。
  2. convertEol=true 时,lineFeed() 额外执行 x=0false 时保留列位置。
  3. CR 仅执行 x=0,不改变 y
  4. NELESC E / C1.NEL)等价 x=0 + index()(下移一行并在必要时滚屏)。
  5. 右边界写入是否自动折行由 DECAWM(wraparound) 控制。
  6. 自动折行触发条件按 xterm 口径:字符写入将越过末列时触发换行;换行行需标记 isWrapped=true
  7. 显式 LF 后目标行应清除 isWrapped,避免把硬换行误判为软换行。

6.6 屏幕缓冲状态(主屏/备用屏,强制)

状态:

  1. buffer_normal(主屏)
  2. buffer_alternate(备用屏)

切换规则xterm 对齐):

  1. CSI ? 47 h / CSI ? 1047 h:切换到备用屏。
  2. CSI ? 47 l / CSI ? 1047 l:返回主屏。
  3. CSI ? 1049 h:保存光标状态并切换到备用屏。
  4. CSI ? 1049 l:返回主屏并恢复保存光标状态。
  5. 对齐 xterm.js@5.3.0 现实行为:1049 不强制清空备用屏历史(按源码注释口径处理)。

实现约束:

  1. TerminalCore 必须同时维护 normalBufferalternateBuffer,仅一个为 activeBuffer
  2. 两个缓冲区的 cursorscroll regionisWrapped 状态独立维护,禁止互相污染。
  3. 切屏仅切换活动缓冲区引用,不得重建对象导致历史丢失。
  4. 1049 路径必须包含 saveCursor/restoreCursor,并保证退出全屏后提示符位置正确。
  5. 该能力只影响输出缓冲与光标状态,不改变“原生输入主通道”与事件归属策略。

7. 组件职责与接口

7.1 TerminalPage.vue

职责:

  1. 页面编排、模式切换、连接/断开、状态展示。
  2. 组装 store + renderer + inputBridge

7.2 TerminalViewport.vue

职责:

  1. 只负责承载渲染容器(不直连 transport
  2. 提供挂载点给 RendererAdapter

7.3 TerminalInputBar.vue

职责:

  1. 文本输入框、发送按钮、粘贴、快捷键入口。
  2. 通过 InputBridge.send() 发送,不直接访问 transport。

7.4 TerminalToolbar.vue

职责:

  1. 展示连接状态、延迟、渲染模式。
  2. 触发 connect/disconnect/clear/switchRenderer

7.5 TerminalTouchTools.vue

职责:

  1. 方向键、Enter、Ctrl+C、Tab、Paste。
  2. 输出标准控制序列(如 ESC[A)。

8. 传输与协议实现要点

8.1 四个核心平台接口TypeScript 签名)

以下接口定义于 packages/terminal-core/src/ 内,各端必须严格实现,不得扩展签名

// ── 会话状态§6.1 状态集对应) ───────────────────────────────────────────
type SessionState =
  | 'idle' | 'connecting' | 'auth_pending' | 'connected'
  | 'reconnecting' | 'disconnected' | 'error';

// ── 连接参数 ──────────────────────────────────────────────────────────────
interface ConnectParams {
  host: string;
  port: number;
  username: string;
  password?: string;
  privateKey?: string;       // PEM 格式私钥字符串
  passphrase?: string;
}

// ── 帧元数据(可选,用于去重/溯源) ──────────────────────────────────────
interface FrameMeta {
  source?: 'keyboard' | 'assist' | 'paste';
  txnId?: string;
}

// ── Transport 事件(出站帧,见 §8.2 ─────────────────────────────────────
type TransportEvent =
  | { type: 'stdout';  data: string }
  | { type: 'stderr';  data: string }
  | { type: 'control'; action: 'connected' | 'disconnect' | 'pong'; data?: string }
  | { type: 'error';   code: string; message: string };

type TransportEventListener = (event: TransportEvent) => void;

// ── TerminalTransport 接口 ────────────────────────────────────────────────
interface TerminalTransport {
  connect(params: ConnectParams): Promise<void>;
  send(data: string, meta?: FrameMeta): void;
  resize(cols: number, rows: number): void;
  disconnect(reason?: string): void;
  on(listener: TransportEventListener): () => void;  // 返回取消订阅函数
  getState(): SessionState;
}

// ── RendererAdapter 接口 ──────────────────────────────────────────────────
interface RendererAdapter {
  mount(container: unknown): void;  // Web: HTMLElement小程序: WXML 组件自挂载时传 null
  write(data: string): void;        // 增量写入 VT 字节流
  resize(cols: number, rows: number): void;
  applySnapshot(snapshot: TerminalSnapshot): void;  // 全量重放(切换渲染器时调用)
  dispose(): void;
}

// ── IMeasureAdapter 接口 ──────────────────────────────────────────────────
interface IMeasureAdapter {
  measureChar(): { widthPx: number; heightPx: number };      // 单字符像素尺寸
  measureContainer(): { widthPx: number; heightPx: number }; // 终端容器内部尺寸
  onResize(cb: () => void): () => void;                      // 容器尺寸变化订阅
}

// ── IInputSource 接口 ─────────────────────────────────────────────────────
interface KeyPayload {
  key: string; code: string;
  ctrlKey: boolean; altKey: boolean; shiftKey: boolean; metaKey: boolean;
}
interface InputPayload   { data: string; isComposing: boolean; }
interface PastePayload   { text: string; }
interface CompositionPayload { data: string; }

interface IInputSource {
  on(event: 'key',          cb: (p: KeyPayload) => void):         () => void;
  on(event: 'input',        cb: (p: InputPayload) => void):       () => void;
  on(event: 'paste',        cb: (p: PastePayload) => void):       () => void;
  on(event: 'compositionstart', cb: (p: CompositionPayload) => void): () => void;
  on(event: 'compositionend',   cb: (p: CompositionPayload) => void): () => void;
}

8.2 网关帧协议(三端统一,不分叉)

帧格式JSON 序列化后经 WebSocket / Capacitor / wx.connectSocket 传输。

// ── 入站帧(客户端 → 网关) ──────────────────────────────────────────────
type InboundFrame =
  | { type: 'init';    sessionId: string; params: ConnectParams }
  | { type: 'stdin';   data: string;   meta?: FrameMeta }
  | { type: 'resize';  cols: number;   rows: number }
  | { type: 'control'; action: 'ping' | 'pong' | 'disconnect'; reason?: string };

// ── 出站帧(网关 → 客户端) ──────────────────────────────────────────────
type OutboundFrame =
  | { type: 'stdout';  data: string }                         // PTY 输出UTF-8 字符串)
  | { type: 'stderr';  data: string }                         // PTY 错误输出
  | { type: 'control'; action: 'connected' | 'disconnect' | 'pong'; data?: string }
  | { type: 'error';   code: ErrorCode; message: string };

type ErrorCode =
  | 'AUTH_FAILED'       // SSH 认证失败
  | 'HOST_UNREACHABLE'  // 目标主机不可达
  | 'TIMEOUT'           // 连接超时
  | 'SESSION_NOT_FOUND' // sessionId 不存在
  | 'INTERNAL_ERROR';   // 服务端内部错误

约束:

  1. data 字段统一为 UTF-8 字符串;二进制内容须 Base64 编码后传输。
  2. txnId 为可选幂等键,网关对相同 txnIdstdin 帧去重。
  3. 协议版本通过 init 帧扩展字段(如 version: '1')传递,当前默认省略。

8.3 三端 Transport 实现

实现 位置 底层 API 适用场景
GatewayTransport apps/web/src/transport/gatewayTransport.ts WebSocket Web 浏览器、微信小程序后备
IosNativeTransport apps/web/src/transport/iosNativeTransport.ts window.Capacitor.Plugins.RemoteConnSSH iOS Capacitor WKWebView
WxTransport apps/miniprogram/utils/wxTransport.js wx.connectSocket 微信小程序原生

约束:

  1. 三端帧协议完全一致(8.2 定义Transport 实现只替换网络层 API不改协议语义。
  2. IosNativeTransport 通过 Capacitor Bridge 调用 Swift 插件(connect/send/resize/disconnect 四方法 + addListener 事件推送Swift 层须完成真实 SSH 接入(当前为骨架)。
  3. WxTransportwx.connectSocket 在小程序逻辑层运行,帧收发逻辑与 GatewayTransport 对称实现。

9. 渲染管线设计

9.1 输出处理链

  1. 收到 stdout/stderr
  2. 执行 sanitizeTerminalOutput(过滤同步更新模式等控制序列噪音)。
  3. 追加到 OutputBuffer(条目上限 + 字节上限双阈值)。
  4. outputRevision++
  5. 渲染器增量写入;必要时重放。

9.2 缓冲策略(必须)

  1. maxEntries 下限保护(建议 >= 200
  2. maxBytes 下限保护(建议 >= 64KB
  3. 先按条目裁剪,再按字节裁剪。
  4. 至少保留最新一条,避免全清空闪烁。

9.3 兼容渲染规则(自研)

  1. 使用 write() 增量写入,不用 writeln() 改写换行语义。
  2. convertEol 默认值必须为 false(与 xterm 默认一致)。
  3. 显式 LF 不等于 CRLFCRLF 分别处理,禁止业务层自行合并。
  4. fit 后同步 resize(cols, rows)

9.4 textarea 渲染规则

  1. 采用“只读显示区 + 输入区”或“单 textarea 双模式”方案(二选一,建议前者)。
  2. 输出区按文本流追加,必要时使用 requestAnimationFrame 批量刷新。
  3. 默认自动滚到底;用户手动上滚时暂停自动跟随。
  4. 不允许使用“仅按字符串估算”的粗略光标算法作为最终实现。
  5. 必须模拟 xterm 的 C0/C1 换行语义:CRLFVTFFNEL
  6. 必须显式维护软换行标记(等价 isWrapped),区分“自动折行”与“显式换行”。

9.5 精准光标内核(强制)

为满足“回显后光标位置必须精准计算”,实验项目必须引入统一终端状态内核(TerminalCore),并将其作为两种渲染模式的唯一真相源。

实现要求:

  1. stdout/stderr 必须先写入 TerminalCore,由内核解析控制序列并更新光标、屏幕缓冲、滚动区域状态。
  2. compat 模式与 textarea 模式都必须消费同一份 TerminalCore 状态快照。
  3. 禁止直接通过 selectionStart + 文本长度 推断“终端光标”,该方法仅可用于“输入框本地插入点”,不能代表远端终端光标。
  4. TerminalCore 至少要正确处理本项目目标程序涉及的序列族,按优先级分层:
    • C0/C1 控制字符CR / LF / VT / FF / NEL / BS / HT / BELBEL 须抛事件,不得静默丢弃)。
    • 光标移动CSI A/B/C/D(上下左右)、CSI E/F(行首上下)、CSI G(列绝对)、CSI H/f(行列绝对)、CSI I(前进 TabCSI Z(后退 Tab
    • 擦除CSI J / ?J(擦除显示)、CSI K / ?K(擦除行)、CSI X(擦除字符)。
    • 行/字符插删TUI 全屏必需)CSI LinsertLinesCSI MdeleteLinesCSI @insertCharsCSI PdeleteCharsCSI SscrollUpCSI TscrollDown
    • 滚动区域TUI 全屏必需)CSI rDECSTBM设置滚动上下边界DECOM(?6h/l) 联动。
    • 光标保存/恢复CSI s / uANSI save/restore cursorESC 7 / ESC 8DECSC/DECRC
    • 全终端重置ESC cRIS全量重置到初始状态CSI !pDECSTR 软重置)。
    • SGR 属性CSI m(见 §9.6)。
    • 模式控制SM/RM (Ps=4 insertMode, Ps=20 convertEol)DECSET/DECRST (含 DECAWM ?7, DECTCEM ?25, DECCKM ?1, 备用屏 ?47/?1047/?1049, bracketed paste ?2004)
    • OSCOSC 0/2(标题,见 §9.9);其余 OSC 须安全解析并丢弃,不得透传到显示区。
  5. TerminalCore 必须维护以下状态:cursorX/cursorYbaseYviewportYscrollTop/scrollBottomconvertEolwraparoundline.isWrapped
  6. 每次输出处理后,必须产出 cursor: { x, y, globalRow }viewport 快照,供触摸激活带与滚动定位复用。

9.6 颜色渲染规范xterm 对齐,强制)

目标:

  1. 颜色与文本属性语义必须对齐 xterm.js@5.3.0SGR(CSI ... m) 行为。
  2. compat 模式与 textarea 模式必须共享同一套属性状态机,避免模式切换后颜色跳变。

实现边界:

  1. 必须支持基础 SGR 属性:0/1/2/3/4/7/8/9/21/22/23/24/25/27/28/29/53/55
  2. 必须支持前景/背景标准色:30-3740-47
  3. 必须支持高亮色:90-97100-107
  4. 必须支持扩展色:
    • 前景 38
    • 背景 48
    • 下划线颜色 58
  5. 扩展色子模式必须支持:
    • ;5;INDEX256 色索引)
    • ;2;R;G;BTrueColor
  6. 必须支持默认色复位:
    • 39 复位前景
    • 49 复位背景
    • 59 复位下划线颜色

颜色状态模型(建议):

  1. fg: { mode: default | p16 | p256 | rgb, value }
  2. bg: { mode: default | p16 | p256 | rgb, value }
  3. underlineColor: { mode: default | p256 | rgb, value }
  4. flags: bold/dim/italic/underline/inverse/invisible/strikethrough/overline
  5. underlineStyle: none/single/double/curly/dotted/dashed

重置纪律:

  1. SGR 0 必须重置前景、背景、下划线样式与颜色到默认(等价 xterm _processSGR0)。
  2. 22 仅取消 bolddim,不影响颜色。
  3. 24 仅取消下划线与下划线样式,不清空其它属性。
  4. 27 仅取消 inverse
  5. 28 仅取消 invisible
  6. 29 仅取消删除线。

渲染映射规则:

  1. inverse 采用“渲染时交换 fg/bg”策略不直接改写底层存储值。
  2. invisible 保留背景渲染,前景以透明或背景同色处理(与终端“隐藏文字”语义一致)。
  3. bold 只作为属性位,不应强行映射为高亮颜色(除非显式启用“粗体映射亮色”策略)。
  4. blink 在 xterm 5.3 源码中为已记录属性但渲染支持有限,实验项目默认可不做动画实现,但要保留属性位兼容。

主题与调色板约束:

  1. 16 色与 256 色索引色应来自统一调色板定义,避免 xtermtextarea 调色板不一致。
  2. TrueColor (rgb) 必须按原值渲染,不经过主题二次量化。
  3. 主题切换只影响“默认色与索引色映射”,不应篡改已存在的 TrueColor 单元格。

9.7 主题复用与轻量化策略(强制)

目标:

  1. 主题实现参考并复用现有 Web/小程序成熟方案,保持视觉一致与维护成本可控。
  2. xterm 移植以“核心稳定 + 轻量高效”为优先级,禁止默认堆叠非必要能力。

主题复用规则:

  1. 统一采用语义 tokenterminalFg/terminalBg/cursor/selection/ansi16),禁止在组件内硬编码颜色。
  2. 默认前景/背景与 ansi16 从现有主题配置映射生成,避免与现有产品主题出现明显偏差。
  3. 256 色表采用固定映射(标准 xterm256 表),不随业务主题动态重算。
  4. 各端只维护“主题映射层”,不复制跨端整套业务主题逻辑。

轻量化规则:

  1. 不引入 @xterm/* 运行时依赖;实现以 TerminalCore + 自研渲染器 为主。
  2. 兼容能力按需实现并按模块拆分首屏仅加载连接、输入、回显、resize 必需逻辑。
  3. 默认使用原生 DOM/Canvas 路径Web 端)或 WXML 组件(小程序端),禁止引入重型图形加速依赖作为前置条件。
  4. 非核心能力(复杂链接检测、额外动画)默认关闭,确保输入回显路径最短。

性能预算(建议):

  1. Web 端 JS 增量目标gzip <= 150KB超限需说明来源与必要性
  2. 终端首屏可输入目标:页面进入后 1 秒内可完成输入与回显(普通开发机基线)。
  3. 持续输出下避免长卡顿:连续主线程阻塞 >100ms 视为性能问题并需定位。

降级开关(必须):

  1. compatLiteMode关闭可选增强仅保留连接、输入、回显、resize。
  2. rendererFallback=textarea:低端设备或异常场景可一键降级。
  3. 主题映射失败时自动回退默认主题,禁止阻塞会话连接与输入流程。

9.8 主屏/备用屏落地实现(强制)

最小实现步骤:

  1. TerminalCore 新增 bufferSet = { normal, alternate, active }savedCursor
  2. 解析 DECSET/DECRST 时接入 47/1047/1049 分支,不允许只做日志忽略。
  3. switchToAlternate():切换 active=alternate,同步 viewport/baseY/cursor
  4. switchToNormal():切换 active=normal,恢复主屏可视与滚动位置。
  5. 1049hsaveCursor() 再切换;1049l 先切回主屏再 restoreCursor()
  6. 渲染器读取统一 activeBuffer 快照,禁止自行缓存“上一屏”造成穿透渲染。

兼容边界:

  1. 若目标程序未使用备用屏序列,行为与当前方案完全一致。
  2. 若收到未知私有模式,记录日志并安全忽略,不得破坏当前缓冲状态。
  3. 对于不在首期范围的高级 TUI 能力,可延后,但 47/1047/1049 不可缺失。

9.9 OSC / BEL 处理规范(强制)

BEL\x07

  1. TerminalCore 必须注册 BEL 执行处理器,检测到后向外抛 onBell 事件,不得静默丢弃。
  2. Web 层(TerminalPage)可选响应:振动(navigator.vibrate)、提示音或弹层,默认静音(但回调链路必须存在)。
  3. 小程序层可通过 wx.vibrateShort() 响应 onBelliOS 层可通过 AudioServicesPlaySystemSound 响应。
  4. BEL 不得在显示区产生任何可见字符。

OSC 0 / OSC 2终端标题

  1. TerminalCore 必须解析 OSC 0 ; text BELOSC 2 ; text BEL,提取 text 后向外抛 onTitleChange(title: string) 事件。
  2. Web 层默认将标题更新到 TerminalToolbar 展示区;不强制写入 document.title
  3. OSC 1icon name与 OSC 0 处理逻辑相同,text 同步抛出,忽略图标语义。

OSC 4 / OSC 10 / OSC 11 / OSC 12调色盘/默认色动态修改)

  1. 首期:安全解析序列,忽略颜色写入。
  2. 不得将原始 OSC 序列透传到显示区造成乱码。
  3. 可记录调试日志,供后期审计。

OSC 8超链接

  1. 首期:安全解析并忽略,不渲染为链接。
  2. 不得透传到显示区。

通用 OSC 兜底规则

  1. 未命中任何已注册 OSC handler 的序列,必须在解析完成后安全丢弃。
  2. 禁止将 OSC 原始字节写入可视 buffer解析失败时记录错误日志但不崩溃。
  3. OSC 以 BEL(\x07)ST(\x9C / ESC \\) 结束,两者均须支持。

9.10 TerminalSnapshot 数据结构与 TerminalCore 公开 API强制

所有渲染器compat、textarea、小程序 WXML setData)均通过 TerminalCore.snapshot() 获取渲染数据,以下是完整类型定义:

// ── 颜色值 ────────────────────────────────────────────────────────────────
interface ColorValue {
  mode: 'default' | 'p16' | 'p256' | 'rgb';
  value: number;  // p16: 0-15p256: 0-255rgb: 0xRRGGBB
}

// ── 单元格 ────────────────────────────────────────────────────────────────
interface TerminalCell {
  char: string;         // 显示字符(空单元格为 ' '
  width: 1 | 2;         // 东亚宽字符占 2 列
  fg: ColorValue;
  bg: ColorValue;
  flags: number;        // SGR flags bitmaskbold=1,dim=2,italic=4,underline=8,
                        // inverse=16,invisible=32,strikethrough=64,overline=128
  underlineStyle: 'none' | 'single' | 'double' | 'curly' | 'dotted' | 'dashed';
  underlineColor: ColorValue;
}

// ── 行 ───────────────────────────────────────────────────────────────────
interface TerminalLine {
  cells: TerminalCell[];  // 长度固定为 cols
  isWrapped: boolean;     // 此行为前一行的自动折行续行
}

// ── 光标状态 ──────────────────────────────────────────────────────────────
interface CursorState {
  x: number;          // 列0-based
  y: number;          // 行(相对 baseY0-based
  globalRow: number;  // baseY + y全局行号供触摸激活带复用
  visible: boolean;   // DECTCEMCSI ?25h/l控制
}

// ── 快照(渲染器唯一数据源) ─────────────────────────────────────────────
interface TerminalSnapshot {
  cols: number;
  rows: number;
  cursor: CursorState;
  lines: TerminalLine[];   // 长度 = rows仅含当前可视区
  title: string;           // 最近 OSC 0/2 设置的标题
  revision: number;        // 每次 write() 处理后递增,供渲染器脏检测
  isAlternateBuffer: boolean; // 当前是否为备用屏
}

TerminalCore 公开 API必须实现不可对外暴露细节方法

type TerminalCoreEvent = 'bell' | 'titleChange' | 'resize';

interface TerminalCoreEventMap {
  bell: void;
  titleChange: string;                   // 新标题字符串
  resize: { cols: number; rows: number }; // 内部触发的 resize 通知
}

class TerminalCore {
  // 写入 VT 字节流内部完成序列解析、状态更新、revision 自增
  write(data: string): void;

  // 全量重置到初始状态(等价 ESC c / RIS
  reset(): void;

  // 产出当前可视区快照;调用成本 O(rows*cols),建议仅在 RAF 回调中调用
  snapshot(): TerminalSnapshot;

  // 更新终端尺寸(通常由 sizeCalculator 计算后注入)
  resize(cols: number, rows: number): void;

  // 事件订阅;返回取消订阅函数(调用后立即停止回调)
  on<K extends TerminalCoreEvent>(
    event: K,
    cb: (payload: TerminalCoreEventMap[K]) => void
  ): () => void;
}

渲染器实现约束:

  1. 渲染器只读 snapshot禁止回写 TerminalCore 内部状态。
  2. 可通过 revision 做脏检测,跳过无变化帧的重渲染。
  3. 小程序 terminal-core-viewonUpdate 中调用 snapshot() 后执行 this.setData({ lines, cursor }),最小化 setData 数据量。

10. 页面渲染与布局规则

10.1 基础布局

  1. 顶部状态栏state、latency、mode
  2. 中部终端视口xterm 或 textarea 输出)。
  3. 底部:输入栏与触摸工具。

10.2 尺寸与 PTY 计算规则(必须统一)

变量:

  1. containerInnerWidthPx
  2. containerInnerHeightPx
  3. charWidthPx
  4. lineHeightPx

公式:

  1. cols = max(20, floor(containerInnerWidthPx / charWidthPx))
  2. rows = max(8, floor(containerInnerHeightPx / lineHeightPx))

约束:

  1. 变化阈值去抖:|cols-lastCols| + |rows-lastRows| >= 1 才发 resize
  2. 初次挂载后至少重试 fit/measure 3~10 次(应对路由切换延迟布局)。

11. 光标与坐标计算规则(重点)

说明:本节区分两类光标,避免语义混淆。

  1. 终端光标:远端 PTY 回显语义对应的光标(用于激活带判定)。
  2. 输入光标:本地输入框 selectionStart/selectionEnd(用于编辑行为)。

11.1 行高计算

优先级:

  1. 优先取真实渲染行高DOM 测量)。
  2. 无法测量时回退:lineHeightPx = fontSize * lineHeight

11.2 触点行号计算

  1. localRow = floor((clientY - viewportTop) / rowHeightPx)
  2. compat 模式:touchRow = viewportY + localRow
  3. textarea 模式:touchRow = viewportTopRow + localRow
  4. clientY 不在视口边界内,判定为无效触点。

11.3 compat 光标行号

  1. cursorLocalRow = core.cursorY
  2. cursorBaseRow = core.baseY
  3. cursorRow = cursorBaseRow + cursorLocalRow(全局行号)
  4. 激活带判断:abs(touchRow - cursorRow) <= activationRadius(建议 2 行)。

11.4 textarea 光标位置计算

输入(终端光标,来自 TerminalCore

  1. coreCursorRow
  2. coreCursorCol
  3. viewportTopRow
  4. rows

规则:

  1. cursorRow = clamp(coreCursorRow, 0, +inf)
  2. cursorVisualRow = cursorRow - viewportTopRow
  3. 仅当 0 <= cursorVisualRow < rows 时认为光标在当前可视窗口内。
  4. 激活带判断统一使用 cursorRow(全局行号),禁止改用输入框 selectionStart
  5. coreCursorRow 语义需与 xterm 对齐:globalRow = baseY + cursorY

输入(本地输入光标,仅编辑用途):

  1. selectionStart(光标字符索引)
  2. text(当前显示文本)
  3. cols(当前列数)

算法(必须实现成纯函数):

  1. text[0..selectionStart) 顺序扫描。
  2. 碰到 \nrow += 1; col = 0
  3. 普通字符:col += charDisplayWidth(ch)
  4. col >= cols:按自动换行规则折行:row += floor(col / cols); col = col % cols

charDisplayWidth(ch) 规则:

  1. ASCII宽度 1。
  2. CJK/全角:宽度 2可用简化 east-asian-width 判定)。
  3. 控制字符:宽度 0。

11.5 坐标到光标索引(可选增强,仅输入框)

若需要“点按定位光标”:

  1. 先由 (x,y) 推导目标 (row,col)
  2. 再线性扫描文本映射回最近字符索引。
  3. 时间复杂度 O(n),长文本需缓存行起始索引优化。

11.6 原生键盘弹出门控(强制)

必须按以下判定顺序执行:

  1. sessionState === connected
  2. 当前手势动作被状态机判定为 FOCUS_KEYBOARD
  3. inBand === true(即 abs(touchRow - cursorRow) <= activationRadius)。
  4. 不存在活动原生选区(否则走 PASS_NATIVE)。

执行动作:

  1. readOnly=false
  2. blur -> focus 顺序触发输入锚点激活。
  3. 进入短保护窗口,防止 iOS 瞬时 blur 立刻收回键盘。

禁止动作:

  1. inBand 区域触摸时强制 focus()
  2. PASS_NATIVE/PASS_SCROLL 分支触发键盘弹出。

12. 输入处理规则(必须)

12.1 统一发送入口

  1. 所有输入只走 InputBridge.sendRaw()
  2. 键盘 Enter 必须映射为 CR (\r)(与 xterm Keyboard.ts 一致)。
  3. 普通文本输入keypress/input按字符原样发送不做全局换行重写。

12.2 meta 标记

  1. 常规按键:meta.source = "keyboard"
  2. 语音/候选提交:meta.source = "assist",可携带 txnId 去重。

12.3 粘贴规则

  1. 粘贴前执行 xterm 对齐的行结束归一:/\r?\n/g -> '\r'
  2. 若开启 bracketed pasteESC[200~ + text + ESC[201~ 包裹发送。
  3. 粘贴文本直接写入 stdin不走逐键模拟。

12.4 快捷键规则

  1. Ctrl/Cmd + C:有选中则复制;无选中则发 \u0003
  2. Ctrl/Cmd + V:读剪贴板后发送。
  3. 方向键默认发送:ESC[A/ESC[B/ESC[C/ESC[D;应用光标模式下发送:ESCOA/ESCOB/ESCOC/ESCOD

12.5 反劫持策略(强制)

  1. 输入主通道固定为原生 textarea,禁止把系统软键盘直接绑定到 xterm 隐藏输入节点。
  2. 兼容层键盘监听仅用于桌面对照模式;移动端默认禁用兼容层直连 stdin 的按键采集。
  3. 触摸事件默认透传,只有 FOCUS_KEYBOARD 分支可执行最小化 preventDefault
  4. 原生输入法事件(compositionstart/update/endbeforeinput)必须完整透传到输入状态机,不得被 xterm 监听提前吞掉。
  5. 发生冲突时遵循优先级:系统行为 > 原生输入通道 > xterm 视图行为。

12.6 区域过滤与事件归属(强制)

区域定义(建议使用 data-zone 标记):

  1. terminal-output-zone:终端可视回显区(仅显示与文本选区)。
  2. native-input-zone:原生输入锚点区(软键盘与 IME 主入口)。
  3. app-control-zone:应用工具栏、按钮、菜单区。
  4. app-overlay-zone:应用弹层、右键菜单、浮层工具区。

归属规则:

  1. 键盘事件默认归 native-input-zone 与应用层快捷键系统xterm 仅消费“已路由到终端”的标准控制输入。
  2. 鼠标点击在 app-control-zone/app-overlay-zone 必须 100% 透传应用层,禁止被 xterm 截获。
  3. wheel/touchmove 默认交给最近可滚动容器;仅终端主视口滚动时才进入终端滚动链路。
  4. 文本选择相关事件优先给系统原生选区xterm 不得覆盖浏览器/系统的选区手柄行为。

实现约束:

  1. 在捕获阶段先执行 eventOwnershipRouter,返回 APP | NATIVE | XTERM,再分发事件。
  2. 未命中白名单事件时默认 APP,禁止“默认给 xterm”。
  3. 仅以下事件允许进入 xtermstdin 字符输入、终端方向键控制序列、明确授权的复制粘贴桥接。
  4. 右键菜单、双击词选、三击行选等交互由应用策略统一控制,不由 xterm 内建策略隐式决定。

13. 关键风险与强约束

  1. 风险textarea 模式无法完整支持 TUI 全屏程序。
    约束:文档中明确为已知限制,不作为首期阻塞项。
  2. 风险IME 双发或丢字。
    约束:保留 composition + beforeinput/input + fallback 三层机制和去重窗口。
  3. 风险:移动端误弹键盘。
    约束:触摸状态机按优先级表执行,禁止随意新增 preventDefault 分支。
  4. 风险:模式切换导致历史丢失。
    约束:统一 OutputBuffer,切换只换渲染器,不换数据源。
  5. 风险xterm 事件劫持导致软键盘、选区、滚动异常。
    约束:移动端输入以原生 textarea 为唯一焦点源xterm 不持有输入焦点。

14. 分阶段实施计划(可直接执行)

阶段 A核心包骨架与接口

  1. 建立 packages/terminal-core 目录与模块骨架。
  2. 定义并导出 TerminalTransportRendererAdapterIMeasureAdapterIInputSource 四个核心接口。
  3. 完成 sessionMachine + terminalSanitizer + outputBuffer 骨架。
  4. tsconfig.json 配置 lib: ["esnext"]CI 零 DOM typecheck 通过。

阶段 BWeb 基线TerminalCore + compatRenderer

  1. 实现 TerminalCoreVT 仿真内核,含 §9.5 全部序列族)。
  2. apps/web 接入 terminal-core,实现 GatewayTransport + compatRenderer + domInputBridge + domMeasureAdapter
  3. 跑通 SSH 会话输入、回显、resize、快捷键、粘贴。
  4. 完成基线测试用例Enter/CR、convertEol、DECAWM、isWrapped、备用屏、颜色 SGR

阶段 Ctextarea 渲染 + iOS 接入

  1. 实现 textareaRenderer,接入 TerminalCore.snapshot(),完成 compat ↔ textarea 切换。
  2. 实现 iosNativeTransportCapacitor Bridgeapps/web 同一构建产物在 Capacitor 壳中加载。
  3. 补全 ios/plugin/RemoteConnSSHPlugin Swift 真实 SSH 接入(替换骨架占位)。
  4. iOS 设备冒烟vim、htop 基础操作可用。

阶段 D微信小程序原生接入

  1. 实现 WxTransportwx.connectSocket)、wxInputBridgewxMeasureAdapter
  2. 实现 terminal-core-view WXML 渲染组件,消费 TerminalCore.snapshot()
  3. 小程序开发者工具 + 真机冒烟vim、nano 基础操作可用。

阶段 E全端验收

  1. 三端Web / iOS / 小程序)同时跑 vim / htop 冒烟测试DECSTBM、insertLines/deleteLines、RIS、OSC 标题)。
  2. 性能验收:各端高频输出不明显卡顿。
  3. 删除演练:可单独删除旧版 apps/web 终端实现,terminal-core 与小程序不受影响;主工程 typecheck/lint/test/build 通过。

15. 测试与验收清单

功能验收:

  1. 连接状态迁移严格符合状态机。
  2. stdout/stderr 能稳定显示,无额外换行污染。
  3. Enter 语义对齐 xterm按键 Enter 发送 CR,不发送裸 LF
  4. 中文输入(含候选提交)无明显丢字/双发。
  5. 模式切换后输出可重放,连接不中断。
  6. 光标精准性:同一份输出流在 compattextarea 模式下,终端光标 (row,col) 一致。
  7. 光标附近弹键盘:仅在光标邻近行轻触触发原生键盘;非邻近行不弹键盘。
  8. 长按选区场景下,键盘不误弹,原生选区与手柄保持可用。
  9. convertEol=false 时,LF 仅下移不回列 0convertEol=trueLF 同时回列 0。
  10. CR 只回列 0NEL 执行“回列 0 + 下移一行”。
  11. 粘贴文本行结束归一为 CR,并在 bracketed paste 模式下正确包裹。
  12. 自动折行与显式换行可区分:软换行行具备 isWrapped=true 语义。
  13. 颜色回显一致性:同一输出流在 compattextarea 模式下,SGR 属性(粗体/下划线/反显/隐藏/删除线)渲染结果一致。
  14. 颜色指令完整性:30-37/40-47/90-97/100-107/38/48/5839/49/59/0/22/24/27/28/29 复位行为符合预期。
  15. 扩展色准确性:38;5;INDEX48;5;INDEX38;2;R;G;B48;2;R;G;B58;2;R;G;B 能精准渲染且模式切换后不漂移。
  16. 主题切换约束:切换主题后默认色与索引色生效,历史 TrueColor 单元格颜色值保持不变。
  17. 主题复用一致性:实验页主题映射与现有 Web/小程序基线一致,同主题下无明显色偏。
  18. 轻量化约束:运行时不依赖 @xterm/*,未启用增强能力不进入首屏加载链路。
  19. 降级策略有效:compatLiteModerendererFallback=textarea 能生效且核心会话稳定。
  20. 原生响应优先:软键盘弹出/收起、长按选区、滚动手势在移动端与普通 textarea 行为一致,无明显被劫持现象。
  21. 焦点纪律正确:移动端会话中 textarea 始终为输入焦点主体xterm 不直接持有键盘焦点。
  22. 区域归属正确:app-control-zone/app-overlay-zone 的键鼠事件不被 xterm 捕获,应用可独立演进交互逻辑。
  23. 路由兜底正确:未声明事件默认归应用层处理,不出现“事件默认落到 xterm”导致的不可控行为。
  24. 备用屏切换正确:?1049h 进入全屏后不污染主屏历史,?1049l 退出后恢复原提示符与光标位置。
  25. 47/1047 兼容正确:切换主/备用屏时两屏内容与滚动状态互不污染。
  26. TUI 滚动区域正确性:CSI r 设定滚动边界后vim/nano 状态栏固定不随内容区滚动,内容区滚动不越界。
  27. RIS/软重置正确性:ESC c 后终端恢复干净初始状态,不残留 TUI 程序的绘图内容;CSI !p 后属性/模式复位但缓冲内容保留。
  28. BEL/OSC 无乱码:\x07 不在显示区产生可见字符;OSC 0;title\a 抛出标题事件且显示区无乱码;未知 OSC 序列安全丢弃。
  29. iOS 一致性iOS Capacitor 壳加载同一 apps/web 构建产物,渲染与输入行为与桌面浏览器一致。
  30. 小程序原生可用性:微信小程序端通过 terminal-core-view 组件运行vim / nano 基础操作可用DECSTBM + insertLines/deleteLines 验证),无 <web-view> 嵌套。
  31. DOM 隔离验证:packages/terminal-core--lib esnext(无 dom lib的 TypeScript 编译下零错误。

性能验收:

  1. 高频输出不明显卡顿(基线:普通开发机可持续滚动)。
  2. 缓冲裁剪后不出现白屏或大面积闪烁。

回归验收:

  1. 仅新增小程序入口改动;主页面行为一致。
  2. 删除实验目录后,主项目 typecheck/lint/test/build 通过。

16. xterm 源码对照

本方案关键规则对应 xterm.js@5.3.0 源码:

  1. Enter -> CRsrc/common/input/Keyboard.ts
  2. 粘贴换行归一(/\r?\n/g -> \rsrc/browser/Clipboard.ts
  3. LF/VT/FF -> lineFeedCR -> carriageReturnNEL -> nextLinesrc/common/InputHandler.ts
  4. convertEol 默认 false,且受 CSI 20 h/l 控制:src/common/services/OptionsService.ts + InputHandler.ts
  5. 自动折行DECAWMisWrappedsrc/common/InputHandler.ts#print
  6. 光标全局行号语义(cursorY 相对 baseYtypings/xterm.d.tsIBuffer 定义。
  7. SGR 属性解析与颜色扩展(含 38/48/5839/49/590/22/24/27/28/29src/common/InputHandler.ts#charAttributes
  8. 主屏/备用屏切换与 1049 光标保存恢复:src/common/InputHandler.ts#setModePrivate + resetModePrivate
  9. 缓冲区实现与切屏基础结构:src/common/buffer/BufferSet.ts
  10. CSI L/MinsertLines/deleteLinessrc/common/InputHandler.ts#insertLines + deleteLines
  11. CSI @/P/X/S/TinsertChars/deleteChars/eraseChars/scrollUp/scrollDownsrc/common/InputHandler.ts 对应同名方法。
  12. CSI rDECSTBM 滚动区域):src/common/InputHandler.ts#setScrollRegion
  13. ESC cRIS 全量重置)、CSI !pDECSTR 软重置):src/common/InputHandler.ts execute 注册 + softReset
  14. BEL → bell()OSC 0/1/2 → onTitleChangesrc/common/InputHandler.ts execute/OSC handler 注册。

17. 完成定义DoD

  1. 本文档中的状态机、组件职责、算法规则均已对应到代码模块;packages/terminal-core DOM 隔离 CI 检查通过。
  2. 三端Web / iOS Capacitor / 微信小程序)均可独立开发与验证,且不互相耦合。
  3. 微信小程序端通过原生 WXML 组件接入,不依赖 <web-view> 嵌套。
  4. 删除演练通过:可单独删除旧版 apps/web 终端实现,terminal-core、小程序端、iOS 端均不受影响;主工程 typecheck/lint/test/build 通过。
  5. 换行、光标、TUI 必需 CSI 序列(含 DECSTBM/insertLines/deleteLines/RIS及 OSC/BEL 处理已与 xterm.js@5.3.0 源码逐条对齐,并有对应验收用例。
  6. 开发者仅依赖本方案即可按阶段落地实现。
  7. 主屏/备用屏与 1049 光标恢复能力已落地并通过验收用例。