Files
terminalX/CLAUDE.md
kid 866df1a323 feat: M3 mosh 挂起→恢复<3s(唤醒脉冲 + 会话状态机三相)
- SessionMachine 加 mosh 三相(moshActive/moshParked/moshResuming)+看门狗兜底,
  mosh 接管即拆 SSH、后台冻结不重建、前台唤醒脉冲等 SSP 续(+5 单测)
- bridge.go: Node.WakeUp()(tsnet InjectEvent+MagicSock.Rebind/ReSTUN) +
  MoshRelay.Rebind()(同端口重开 loopback、泵按 socket 生成容错)
- iosclient.cc: g_mosh_last_heard 探针(导出 mosh_last_heard_ms) + SIGCONT 全屏重绘
- MoshSession: nudge()(SIGCONT) + lastHeardMs();SSHTerminalModel 前台脉冲三连
  + healthy 轮询 + beginBackgroundTask;URL scheme 供无头前台唤醒
- 验证(vohive-vm over tsnet):15s 冻结→前台 wake pulse→1.3s recovered,<3s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-24 17:04:33 +08:00

7.3 KiB
Raw Blame History

CLAUDE.md — terminalX

原生苹果生态终端工具,解决 iPad/iPhone 远程运维 macOS/Linux + vibe coding 痛点。 平台优先级 iPadOS > iOS > macOS(Apple Silicon)。四大核心:libghostty 引擎 · mosh · Tailscale(tsnet 用户态) · tmux control-mode hook。 iOS/iPadOS 只做远程(沙盒禁 fork/execmacOS 支持本地 PTY。

新会话必读:先读 docs/HANDOFF.md(项目全貌 + 里程碑状态 + 如何继续)。本文件只讲操作规范。

⚠️ 铁律:绝不臆想工具结果

调用任何工具后,在真实结果返回前绝不预测/编造/续写其输出(不写假想的 BUILD SUCCEEDED、日志、ls、截图内容)。一次调用→停→读真实返回→再推理。想说预期只在调用之前用将来时。发现自己编造要硬停重新核实。(曾因此误诊,教训见 git 历史 / HANDOFF。

环境约束(关键,决定一切构建方式)

  • 构建机走带认证的 HTTPS 代理(疑似国内网)。规律:github.com release blob 可经 curl 下;go.dev/ghcr.io(brew bottle) 被代理 abortSwiftPM 不支持带认证代理 → 所有 SPM 依赖必须 vendor 本地化(见 vendor/。Go 模块走 goproxy.cn
  • Go 1.26.5 在 ~/.local/go(不在默认 PATH经 aliyun 镜像 curl 装);gomobile/gobind 在 ~/go/bingo env 已固化 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。

构建 & 验证命令

# 纯逻辑单测(无需 Xcode/模拟器)
swift test --package-path packages/TXCore                 # 35 tests

# 重建 SSH 库 xcframeworklibssh2+mbedTLS改 vendor/libssh2|mbedtls 后)
bash vendor/build/build-cssh.sh all                        # 产出 artifacts/CSSHCore.xcframework (library-only!)

# 重建 mosh 库 xcframeworkmosh C++ + protobuf-lite改 vendor/mosh|protobuf 或 build/mosh/* 后)
bash vendor/build/build-mosh.sh all                       # 产出 artifacts/MoshCore.xcframework (library-only!)
# 注:首次需 host protoc 生成 .pb.cc已 checkin无需重跑
#   cmake -S vendor/protobuf -B vendor/protobuf/build-host -G Ninja -Dprotobuf_BUILD_TESTS=OFF && \
#   cmake --build vendor/protobuf/build-host --target protoc && \
#   vendor/protobuf/build-host/protoc --proto_path=vendor/mosh/src/protobufs --cpp_out=vendor/mosh/src/protobufs *.proto

# 重建 tsnet 桥(改 vendor/tsnet-bridge/tsnetbridge/bridge.go 后)
export GOROOT=$HOME/.local/go; export PATH=$GOROOT/bin:$HOME/go/bin:$PATH
cd vendor/tsnet-bridge && gomobile bind -target=ios,iossimulator \
  -o ../../artifacts/TsnetBridge.xcframework ./tsnetbridge

# 构建 app改 project.yml 后先 xcodegen generate
cd apps/TerminalX && xcodegen generate --spec project.yml
xcodebuild -project TerminalX.xcodeproj -scheme TerminalX \
  -destination 'platform=iOS Simulator,id=<UDID>' \
  -clonedSourcePackagesDirPath /tmp/tx-spm build

无头验证(本环境无 GUI用 simctl 截图取证)

iPad Pro 13(M5) 模拟器 UDID2EA9055E-4557-4B20-82AF-56A68AC1D454(变了用 xcrun simctl list devices)。

UDID=2EA9055E-4557-4B20-82AF-56A68AC1D454
APP=$(find ~/Library/Developer/Xcode/DerivedData/TerminalX-*/Build/Products/Debug-iphonesimulator -maxdepth 1 -name TerminalX.app | head -1)
xcrun simctl boot $UDID; xcrun simctl install $UDID "$APP"
xcrun simctl launch $UDID ai.athom.terminalx --args <见下>
sleep N; xcrun simctl io $UDID screenshot /tmp/x.png     # 然后用 Read 工具看 /tmp/x.png

启动参数NSUserDefaults 参数域,自动连接,供无头验证)

  • 直连 SSH-txHost H -txPort 22 -txUser U -txPass PW -txAutoCommand "echo hi"
  • tsnet SSH额外加 -txTsnetKey <authkey>-txHost 填 tailnet IP
  • 纯 tsnet 自检(加入+对端枚举):只给 -txTsnetKey <authkey>(不给 -txHost
  • mosh 直连(LAN)-txHost H -txPort 22 -txUser U -txPass PW -txMosh 1(登录后跑 mosh-server→解析 MOSH CONNECT→mosh_main 直连 UDP
  • mosh over tsnet:额外加 -txTsnetKey <authkey>-txHost 填 tailnet IPrelay=loopback↔tsnet UDP
  • 自定义 mosh-server 命令:-txMoshServerCmd "…"(默认 mosh-server new -s -c 256 -l LANG=en_US.UTF-8 -l LC_ALL=en_US.UTF-8

M3 挂起→恢复无头验证:退后台 simctl openurl $UDID https://apple.com;冻结 kill -STOP $(pgrep -x TerminalX);解冻 kill -CONT;前台 simctl openurl $UDID "terminalx://resume"URL scheme 已注册)。日志 log stream | grep -E 'MOSHDBG|TXM3'wake pulseTXM3 recovered 时间差判 <3s。坑:模拟器看门狗会杀掉被 STOP 冻结过久(>~30s)的 app且模拟器不复现 socket defunct/WG 过期,深挂起需真机锁屏补测。

仓库结构

  • packages/TXCore — 零依赖纯逻辑tmux 解析器/layout/SessionMachine(会话状态机+退避重连)/TmuxControlSequence(DCS 检测)。
  • packages/TXTransportSSHSession(libssh2, 支持外部 fd)/MoshSession(mosh_main 桥)/Transport 协议C shim: CSSH+CSSHCore(libssh2)、CMosh+MoshCore(mosh)。
  • apps/TerminalX/iOSSSHTerminalModel(连接编排+tsnet+tmux 路由+mosh 编排)/ContentView/TmuxController(tmux 网关)/TsnetProbe(M1 自检)。
  • vendor/ — 所有非 Swift 依赖本地化:libghostty-spm(GhosttyKit)/MSDisplayLink/libssh2/mbedtls/tsnet-bridge(Go)/mosh(blinksh/ios 分支)/protobuf(3.21.12)build/mosh/(手写 config.h+CMakeLists)。
  • artifacts/ — 预编译 xcframework有意提交,非 gitignoreCSSHCore/TsnetBridge/MoshCore

约定 & 坑

  • CSSHCore/MoshCore 必须 library-only(无 Headers/modulemapSwift module 由包内 CSSH/CMosh C target 提供。否则与 GhosttyKit 撞 include/module.modulemap
  • mosh 构建:绕过 autotools 用 CMake 直编 src/*.cc(手写 build/mosh/config.hcrypto 用 CommonCrypto(USE_APPLE_COMMON_CRYPTO_AES=1);用 terminaldisplayinit_ios.cc(无 terminfo);已 patch 3 处 vendored 源(::bind 消歧义 / curses include 包 #if !IOS_CONTROLLER / 去 iOS 禁用 system())protobuf 只需 lite runtime.pb.cc 已 checkin。
  • protoc 装不了不是问题:用源码 CMake 编一个 host protoc 生成 .pb.cc 即可(一次性,产物 checkin
  • 一进程一个 Go runtime、只一次 gomobile bind(全在 tsnetbridge 包)。
  • Swift \r\n 是单个 Character扩展字形簇:判行尾用 Character.isNewline,不能 c != "\r"MoshConnectScanner 曾踩)。
  • Swift6 SendNonSendable pass 可能崩溃pthread_create+Unmanaged 上下文+C 入口的组合):改用 Foundation.Thread+线程内 pthread_self() 规避(见 MoshSession)。
  • GhosttyKit 的 InMemoryTerminalSession.receive 在 surface 未 attach 时丢弃写入 → 用 OutputGate 缓冲到 surfaceSize != nil 再 flush。
  • tmux -CC attach 不推 %window-add → 必须主动 list-windows 枚举。
  • SourceKit "No such module" 是索引噪声,swift build/xcodebuild 为准
  • 密钥/密码绝不写进仓库;测试主机凭据由用户即时提供。
  • 提交/推送 git 只在用户明确要求时(当前已 git init,尚无 commit
  • 与用户始终用简体中文交流。