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

1055 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/gateway`WebSocket或 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 / ?u``KittyKeyboardFlags`)。
2. Win32InputModeWindows 伪终端特有输入模式)。
3. 鼠标跟踪模式(`?1000 / ?1002 / ?1003``onBinary` 二进制鼠标上报通道)。
4. `screenReaderMode` 及 ARIA 无障碍支持。
5. Markers / Decorations`registerMarker / registerDecoration`)。
6. Link Provider`registerLinkProvider`)。
7. CharacterJoiner`registerCharacterJoiner / deregisterCharacterJoiner`,仅 WebGL 渲染器使用)。
8. LigaturesAddon / ImageAddonSixel/Kitty 图片协议)。
9. SerializeAddon终端状态序列化`OutputBuffer` 承担类似职责)。
10. Unicode Grapheme Clusters`addon-unicode-graphemes`emoji 字形簇宽度计算)。
11. DECSCA字符保护`CSI "q`)与 DECRQM模式查询`CSI $p / ?$p`)。
12. `macOptionIsMeta``altClickMovesCursor``rightClickSelectsWord`(桌面端特有选项)。
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_pending`compositionstart/end` 等 DOM 事件由 Web 层订阅后调用 core 接口;小程序通过 `bindinput` + `isComposing` 等效调用同一接口。
5. CI 中增加以下 typecheck 任务保证零 DOM 泄漏:
```bash
tsc -p packages/terminal-core/tsconfig.json --lib esnext --noEmit
```
## 4. 目录规划(实施级)
```text
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. 总体架构与数据流
```text
┌────────────── 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 控制字符纪律:`CR` 与 `LF` 分离语义,`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=0``false` 时保留列位置。
3. `CR` 仅执行 `x=0`,不改变 `y`。
4. `NEL``ESC 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` 必须同时维护 `normalBuffer` 与 `alternateBuffer`,仅一个为 `activeBuffer`。
2. 两个缓冲区的 `cursor`、`scroll region`、`isWrapped` 状态独立维护,禁止互相污染。
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/` 内,各端必须严格实现,**不得扩展签名**。
```typescript
// ── 会话状态§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 传输。
```typescript
// ── 入站帧(客户端 → 网关) ──────────────────────────────────────────────
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` 为可选幂等键,网关对相同 `txnId` 的 `stdin` 帧去重。
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. `WxTransport` 的 `wx.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` 不等于 `CRLF``CR` 和 `LF` 分别处理,禁止业务层自行合并。
4. `fit` 后同步 `resize(cols, rows)`。
## 9.4 textarea 渲染规则
1. 采用“只读显示区 + 输入区”或“单 textarea 双模式”方案(二选一,建议前者)。
2. 输出区按文本流追加,必要时使用 `requestAnimationFrame` 批量刷新。
3. 默认自动滚到底;用户手动上滚时暂停自动跟随。
4. 不允许使用“仅按字符串估算”的粗略光标算法作为最终实现。
5. 必须模拟 xterm 的 C0/C1 换行语义:`CR`、`LF`、`VT`、`FF`、`NEL`。
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 / BEL`BEL 须抛事件,不得静默丢弃)。
- **光标移动**`CSI A/B/C/D`(上下左右)、`CSI E/F`(行首上下)、`CSI G`(列绝对)、`CSI H/f`(行列绝对)、`CSI I`(前进 Tab、`CSI Z`(后退 Tab
- **擦除**`CSI J / ?J`(擦除显示)、`CSI K / ?K`(擦除行)、`CSI X`(擦除字符)。
- **行/字符插删TUI 全屏必需)**`CSI L`insertLines、`CSI M`deleteLines、`CSI @`insertChars、`CSI P`deleteChars、`CSI S`scrollUp、`CSI T`scrollDown
- **滚动区域TUI 全屏必需)**`CSI r`DECSTBM设置滚动上下边界与 `DECOM(?6h/l)` 联动。
- **光标保存/恢复**`CSI s / u`ANSI save/restore cursor`ESC 7 / ESC 8`DECSC/DECRC
- **全终端重置**`ESC c`RIS全量重置到初始状态`CSI !p`DECSTR 软重置)。
- **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)`。
- **OSC**`OSC 0/2`(标题,见 §9.9);其余 OSC 须安全解析并丢弃,不得透传到显示区。
5. `TerminalCore` 必须维护以下状态:`cursorX/cursorY`、`baseY`、`viewportY`、`scrollTop/scrollBottom`、`convertEol`、`wraparound`、`line.isWrapped`。
6. 每次输出处理后,必须产出 `cursor: { x, y, globalRow }` 与 `viewport` 快照,供触摸激活带与滚动定位复用。
## 9.6 颜色渲染规范xterm 对齐,强制)
目标:
1. 颜色与文本属性语义必须对齐 `xterm.js@5.3.0` 的 `SGR(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-37`、`40-47`。
3. 必须支持高亮色:`90-97`、`100-107`。
4. 必须支持扩展色:
- 前景 `38`
- 背景 `48`
- 下划线颜色 `58`
5. 扩展色子模式必须支持:
- `;5;INDEX`256 色索引)
- `;2;R;G;B`TrueColor
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` 仅取消 `bold` 与 `dim`,不影响颜色。
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 色索引色应来自统一调色板定义,避免 `xterm` 与 `textarea` 调色板不一致。
2. TrueColor (`rgb`) 必须按原值渲染,不经过主题二次量化。
3. 主题切换只影响“默认色与索引色映射”,不应篡改已存在的 TrueColor 单元格。
## 9.7 主题复用与轻量化策略(强制)
目标:
1. 主题实现参考并复用现有 Web/小程序成熟方案,保持视觉一致与维护成本可控。
2. xterm 移植以“核心稳定 + 轻量高效”为优先级,禁止默认堆叠非必要能力。
主题复用规则:
1. 统一采用语义 token如 `terminalFg/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. `1049h` 先 `saveCursor()` 再切换;`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()` 响应 `onBell`iOS 层可通过 `AudioServicesPlaySystemSound` 响应。
4. BEL 不得在显示区产生任何可见字符。
### OSC 0 / OSC 2终端标题
1. `TerminalCore` 必须解析 `OSC 0 ; text BEL` 与 `OSC 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()` 获取渲染数据,以下是完整类型定义:
```typescript
// ── 颜色值 ────────────────────────────────────────────────────────────────
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必须实现不可对外暴露细节方法
```typescript
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-view` 在 `onUpdate` 中调用 `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. 碰到 `\n``row += 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 paste按 `ESC[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/end`、`beforeinput`)必须完整透传到输入状态机,不得被 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. 仅以下事件允许进入 xterm`stdin` 字符输入、终端方向键控制序列、明确授权的复制粘贴桥接。
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. 定义并导出 `TerminalTransport`、`RendererAdapter`、`IMeasureAdapter`、`IInputSource` 四个核心接口。
3. 完成 `sessionMachine + terminalSanitizer + outputBuffer` 骨架。
4. `tsconfig.json` 配置 `lib: ["esnext"]`CI 零 DOM typecheck 通过。
### 阶段 BWeb 基线TerminalCore + compatRenderer
1. 实现 `TerminalCore`VT 仿真内核,含 §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. 实现 `iosNativeTransport`Capacitor Bridge`apps/web` 同一构建产物在 Capacitor 壳中加载。
3. 补全 `ios/plugin/RemoteConnSSHPlugin` Swift 真实 SSH 接入(替换骨架占位)。
4. iOS 设备冒烟vim、htop 基础操作可用。
### 阶段 D微信小程序原生接入
1. 实现 `WxTransport``wx.connectSocket`)、`wxInputBridge`、`wxMeasureAdapter`。
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. 光标精准性:同一份输出流在 `compat` 与 `textarea` 模式下,终端光标 `(row,col)` 一致。
7. 光标附近弹键盘:仅在光标邻近行轻触触发原生键盘;非邻近行不弹键盘。
8. 长按选区场景下,键盘不误弹,原生选区与手柄保持可用。
9. `convertEol=false` 时,`LF` 仅下移不回列 0`convertEol=true` 时 `LF` 同时回列 0。
10. `CR` 只回列 0`NEL` 执行“回列 0 + 下移一行”。
11. 粘贴文本行结束归一为 `CR`,并在 bracketed paste 模式下正确包裹。
12. 自动折行与显式换行可区分:软换行行具备 `isWrapped=true` 语义。
13. 颜色回显一致性:同一输出流在 `compat` 与 `textarea` 模式下,`SGR` 属性(粗体/下划线/反显/隐藏/删除线)渲染结果一致。
14. 颜色指令完整性:`30-37/40-47/90-97/100-107/38/48/58` 与 `39/49/59/0/22/24/27/28/29` 复位行为符合预期。
15. 扩展色准确性:`38;5;INDEX`、`48;5;INDEX`、`38;2;R;G;B`、`48;2;R;G;B`、`58;2;R;G;B` 能精准渲染且模式切换后不漂移。
16. 主题切换约束:切换主题后默认色与索引色生效,历史 TrueColor 单元格颜色值保持不变。
17. 主题复用一致性:实验页主题映射与现有 Web/小程序基线一致,同主题下无明显色偏。
18. 轻量化约束:运行时不依赖 `@xterm/*`,未启用增强能力不进入首屏加载链路。
19. 降级策略有效:`compatLiteMode` 与 `rendererFallback=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 -> CR``src/common/input/Keyboard.ts`。
2. 粘贴换行归一(`/\r?\n/g -> \r``src/browser/Clipboard.ts`。
3. `LF/VT/FF -> lineFeed`、`CR -> carriageReturn`、`NEL -> nextLine``src/common/InputHandler.ts`。
4. `convertEol` 默认 `false`,且受 `CSI 20 h/l` 控制:`src/common/services/OptionsService.ts` + `InputHandler.ts`。
5. 自动折行DECAWM与 `isWrapped``src/common/InputHandler.ts#print`。
6. 光标全局行号语义(`cursorY` 相对 `baseY``typings/xterm.d.ts` 中 `IBuffer` 定义。
7. `SGR` 属性解析与颜色扩展(含 `38/48/58`、`39/49/59`、`0/22/24/27/28/29``src/common/InputHandler.ts#charAttributes`。
8. 主屏/备用屏切换与 `1049` 光标保存恢复:`src/common/InputHandler.ts#setModePrivate` + `resetModePrivate`。
9. 缓冲区实现与切屏基础结构:`src/common/buffer/BufferSet.ts`。
10. `CSI L/M`insertLines/deleteLines`src/common/InputHandler.ts#insertLines` + `deleteLines`。
11. `CSI @/P/X/S/T`insertChars/deleteChars/eraseChars/scrollUp/scrollDown`src/common/InputHandler.ts` 对应同名方法。
12. `CSI r`DECSTBM 滚动区域):`src/common/InputHandler.ts#setScrollRegion`。
13. `ESC c`RIS 全量重置)、`CSI !p`DECSTR 软重置):`src/common/InputHandler.ts` execute 注册 + `softReset`。
14. `BEL → bell()`、`OSC 0/1/2 → onTitleChange``src/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` 光标恢复能力已落地并通过验收用例。