- M0: libghostty SSH 终端(渲染/输入/连接)+ 白屏修复(OutputGate) + 会话状态机/自动重连 - M1: tsnet 用户态组网 + SSH-over-tsnet(fd 桥),shell 级真机验证;R5(Go+gvisor+C+++Swift 同进程) retire - M1.5: tmux -CC 原生 tab(MVP) - 结构: packages/(TXCore·TXTransport), apps/TerminalX, vendor/(libghostty-spm/libssh2/mbedtls/tsnet-bridge), artifacts/ - 文档: CLAUDE.md + docs/HANDOFF.md(新会话入口) - 环境: 认证代理→依赖 vendor 本地化;Go 在 ~/.local/go;仅模拟器/未签名 - 待续: M2 mosh, tmux 多 pane, M4 安全(host key/SE) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.7 KiB
8.7 KiB
terminalX 交接文档(HANDOFF)
目的:让没有上下文的新会话读完即可掌握项目全貌并继续工作。配合
../CLAUDE.md(操作规范/命令)与项目记忆(~/.claude/projects/-Users-kid-...-terminalX/memory/,本项目会话会自动注入)一起看。 最后更新:2026-07-24。
0. 一句话现状
一个原生 iPad SSH 终端已可用并真机验证:libghostty 渲染 + 键盘输入 + SSH 直连 + SSH-over-tsnet(Tailscale 用户态) + tmux -CC 原生 tab + 断线自动重连。四大核心需求(libghostty/tailscale/tmux 已验证,mosh 未做)。仅在 iOS 模拟器验证(未上真机/未签名/未提交 git)。
1. 项目愿景与硬约束
- 目标:iPad/iPhone 远程轻量运维 macOS/Linux + vibe coding。平台优先级 iPadOS > iOS > macOS(Apple Silicon)。
- iOS/iPadOS 只远程(沙盒禁 fork/exec,不能跑本地 shell);macOS 才支持本地 PTY。
- 分发目标 App Store(M5,已暂缓)。
- 四大核心:libghostty(终端引擎) · mosh · Tailscale tsnet(用户态,非 NetworkExtension) · tmux control-mode(-CC) hook。
2. 关键架构决策(已定,勿推翻)
- 终端引擎用 libghostty,经社区 GhosttyKit(
Lakr233/libghostty-spm,已 vendor 到vendor/libghostty-spm)。TerminalViewState是ObservableObject;喂字节入口InMemoryTerminalSession.receive(Data);SwiftUI 视图TerminalSurfaceView(context:)。 - Tailscale 走 tsnet 用户态(Go,gomobile bind →
TsnetBridge.xcframework),不用 NetworkExtension(避开 VPN entitlement + 50MB 内存墙)。 - SSH 用 libssh2 + mbedTLS(
CSSHCore.xcframework),支持传入外部 fd。 - SSH-over-tsnet = fd 桥:
tsnet.Dial()得 net.Conn(无真实 fd)→socketpair(AF_UNIX)+ 双向 io.Copy 泵 → 把另一端 fd 交给 libssh2。见vendor/tsnet-bridge/tsnetbridge/bridge.go: DialTCPFD。 - 一进程只一个 Go runtime、只一次 gomobile bind;C/C++ 库符号隐藏只导出 C API。
- 多账号(未实现):多 tsnet 节点并存;每 host 绑 egress profile(Direct/tailnet);禁止多 tailnet 并发盲探(重叠 100.x/同名会误连)。
3. 环境(决定构建方式,极重要)
- 带认证的 HTTPS 代理(
HTTPS_PROXY=...@...:38128)。github.comrelease blob 能 curl;go.dev / ghcr.io 被代理 abort;SwiftPM 不支持带认证代理 → 所有 SPM 依赖 vendor 本地化。Go 模块用goproxy.cn。 - Go 1.26.5 在
~/.local/go(不在默认 PATH;经 aliyun 镜像 curl 装),gomobile/gobind 在~/go/bin,go env -w GOPROXY=https://goproxy.cn GOSUMDB=off已固化。 - 工具链:Xcode 26.6 / Swift 6.3 / xcodegen 2.46 / zig 0.15.2 / cmake 4.4 / ninja 1.13 / git。
4. 里程碑状态
| 里程碑 | 状态 | 真机实证 |
|---|---|---|
| M0 SSH 直连终端 | ✅ 完成 | libghostty 渲染(SGR/CJK)、键盘输入、连接;白屏 bug 修复(OutputGate);会话状态机+自动重连(31→含后加共 35 单测) |
| M1 tsnet + SSH-over-tsnet | ✅ 完成 | app 内 tsnet 加入 tailnet(IP 100.74.60.108);对端枚举;SSH-over-tsnet 登录 Ubuntu shell(服务端 Last login from 100.x 证明走 tailnet) |
| M1.5 tmux -CC 原生 tab | ✅ MVP | 直连跑 tmux -CC attach → 渲染 fish/tmux 两原生 tab + 活动窗口内容 |
| dial 重试打磨 | ✅ | TsnetManager.dialFD 6 次退避重试,解决 tsnet 首连路径预热超时 |
| R5 Go+gvisor+C+++Swift 同进程 | ✅ retire | 全程真机无崩溃 |
| M2 mosh | ◻️ 未开始 | libmoshios(C++) + 本地 UDP relay over tsnet |
| M3 生命周期深化 | ◻️ 部分 | 前后台 FSM 已做;mosh 快速恢复 + tmux 兜底 待做 |
| M4 安全 | ◻️ 未做 | 当前 SSH 接受任意 host key(见 SSHSession TODO);known_hosts 固定 + Secure Enclave 密钥 待做 |
| M5 合规提审 | ⏸️ 暂缓 | 用户指示暂缓 |
5. 代码地图与数据流
packages/TXCore/Sources/TXCore/Tmux/TmuxControlParser.swiftTmuxEvent.swiftTmuxIDs.swiftTmuxOutputDecoder.swiftTmuxLayout.swiftTmuxControlSequence.swift(DCS 检测)—— 纯解析,35 单测在Tests/。Session/SessionMachine.swift——reduce(event)->[Effect]会话状态机(指数退避重连/后台冻结/前台恢复)。
packages/TXTransport/Sources/TXTransport/Transport.swift(协议+SSHConfig+SSHError)、SSHSession.swift(libssh2;preconnectedFD支持外部 fd;事件循环 poll+非阻塞)。CSSH/(C target:libssh2 头 +include/module.modulemap,供import CSSH)、CSSHCore(binaryTarget,library-only)。
apps/TerminalX/iOS/SSHTerminalModel.swift—— 核心编排:OutputGate(surface-ready 缓冲)、TransportHolder、TsnetManager(tsnet 节点 up/dial+重试)、TsnetError、TmuxRouter(raw/gateway 字节分流)、状态机驱动 startSession(直连/tsnet 分支)、autoConnectIfConfigured(启动参数)。ContentView.swift—— 分支:tmuxController != nil→TmuxTabbedView(tab 条+活动窗口);否则RawTerminalView;TsnetProbeView(纯自检);ConnectionForm。TmuxController.swift—— tmux 网关:消费解析事件、window↔TmuxWindow(各自终端会话+OutputGate)、list-windows/list-panes/capture-pane握手、send-keys/select-window/new-window。TsnetProbe.swift—— M1 自检视图(加入 tailnet + 对端列表)。TerminalXApp.swift—— @main。
- 数据流(直连):transport.onBytes →
TmuxRouter.feed→ (raw)OutputGate.deliver→session.receive→libghostty;(检测到 DCS) →TmuxController.feed→解析→各 window。输入:libghostty→session write 闭包→transport.send(tmux 模式→send-keys)。 - 数据流(tsnet):
TsnetManager.ensureUp(auth key)→dialFD(socketpair fd)→SSHSession(preconnectedFD:)。
6. 已知问题 / TODO(继续工作的入口)
- M4 安全:
SSHSession.connect()有TODO(M4):不校验 host key(接受任意)。需接 known_hosts 三元组 pin(egress,host,port) + Secure Enclave 私钥(libssh2_userauth_publickey_frommemory+SecKeyCreateSignature)。 - tmux MVP 简化:每 window 只显示活动 pane(多 pane 分屏未做);历史靠
capture-pane抓当前屏(非完整 scrollback);命令应答 FIFO 简化匹配;resize 未回传(默认 80x24,应发refresh-client -C)。 - tsnet 首连:已加 dial 重试;更优是等 peer online 再 dial(读 PeersJSON online 状态)。
- 签名/真机:
CODE_SIGNING_ALLOWED=NO,仅模拟器;上真机/TestFlight 需配置签名 + entitlements(tsnet 用户态无需 NE entitlement,利好审核)。 - git:已
git init,尚无 commit;artifacts/*.xcframework已 gitignore(需make/脚本重建或从 Release 拉)。 - 多平台:iOS target 已跑;macOS target(本地 PTY,
TXPTY)未建。
7. 下一步建议(按价值)
- M2 mosh over tsnet:接
blinksh/mosh(libmoshios C++,依赖 protobuf) 交叉编译成 xcframework(参考build-cssh.sh模式) → 本地 UDP relay(mosh 连 127.0.0.1:N,Go 侧tsnetUDPListenPacket/Dial转发;bridge 加 UDP relay 方法,类比DialTCPFD)。SSH 先起mosh-server拿 key+port。需用户提供装了 mosh-server 的 tailnet 主机做端到端。 - tmux 多 pane 分屏:
TmuxLayout已能解析布局树 → 把 window 内多 pane 渲染成 SwiftUI 分屏(每 pane 一 surface)。 - M4 安全:known_hosts 固定 + SE 密钥。
- 产品化:主机列表持久化(Keychain)、多标签(非 tmux)、软键盘运维工具栏(GhosttyKit 内建,接线即可)。
8. 测试资源(用户提供,凭据勿写入仓库/勿硬编码,每次向用户索取)
- 一台局域网 macOS(有 tmux)、一台 tailnet 内 online 的 Ubuntu VM(vohive-vm)、一个 tailnet auth key(ephemeral,可能过期,用户会给新的)。用于直连 SSH / SSH-over-tsnet / tmux / M2 mosh 端到端验证。
- tailnet 是
tail3b5057.ts.net,已见节点:vohive-vm(linux)、iphone-air、ipad-pro、macbook-air-m1、macbook-air。
9. 工作纪律(务必遵守)
- 绝不臆想工具结果:调用后等真实返回再推理,绝不编造 BUILD/日志/截图。曾因连续编造工具输出(含假 BUILD SUCCEEDED、错误归因 GhosttyKit)而误诊——真相全靠老实
Read完整 log。此规则已写入全局~/.claude/CLAUDE.md。 - 每步用真实证据验证:
swift test看真实计数、xcodebuild看真实 BUILD 结果、模拟器screenshot+Read看真实画面。 - 与用户用简体中文;commit/push 只在明确要求时。