- TXTransport: SSHSigner 协议 + Authentication.publicKeyCallback;SSHSession 用 @convention(c) sshSignCallback(abstract 桥 SignerBox + malloc 出参)接 libssh2_userauth_publickey - app SigningKeyProvider: SE 优先(kSecAttrTokenIDSecureEnclave),未签名/无 SE 回退软件 P-256 (导出落文件持久化),同一 SSHSigner 路径;SecKeyCreateSignature(ecdsa)→ECDSAConv.derToSSHSignature - 公钥 blob=ecdsa-sha2-nistp256||nistp256||X963;-txPubkeyAuth 日志打 authorized_keys 行 - 验证(192.168.9.199):不给密码,公钥加 authorized_keys 后免密登录成功(whoami=kid); 模拟器 SE 生成 hw=true 可达,真机 SE 硬件签名+keychain 持久化待补 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
89 lines
8.3 KiB
Markdown
89 lines
8.3 KiB
Markdown
# 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/exec);macOS 支持本地 PTY。
|
||
|
||
> **新会话必读**:先读 `docs/HANDOFF.md`(项目全貌 + 里程碑状态 + 如何继续)。本文件只讲操作规范。
|
||
|
||
## ⚠️ 铁律:绝不臆想工具结果
|
||
调用任何工具后,在**真实结果返回前**绝不预测/编造/续写其输出(不写假想的 `BUILD SUCCEEDED`、日志、`ls`、截图内容)。一次调用→停→读真实返回→再推理。想说预期只在调用**之前**用将来时。发现自己编造要**硬停**重新核实。(曾因此误诊,教训见 git 历史 / HANDOFF。)
|
||
|
||
## 环境约束(关键,决定一切构建方式)
|
||
- 构建机走**带认证的 HTTPS 代理**(疑似国内网)。规律:`github.com` release blob 可经 `curl` 下;**`go.dev`/`ghcr.io`(brew bottle) 被代理 abort**;**SwiftPM 不支持带认证代理** → 所有 SPM 依赖必须 **vendor 本地化**(见 `vendor/`)。Go 模块走 `goproxy.cn`。
|
||
- **Go 1.26.5 在 `~/.local/go`**(不在默认 PATH!经 aliyun 镜像 curl 装);**gomobile/gobind 在 `~/go/bin`**。`go 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。
|
||
|
||
## 构建 & 验证命令
|
||
|
||
```sh
|
||
# 纯逻辑单测(无需 Xcode/模拟器)
|
||
swift test --package-path packages/TXCore # 35 tests
|
||
|
||
# 重建 SSH 库 xcframework(libssh2+mbedTLS,改 vendor/libssh2|mbedtls 后)
|
||
bash vendor/build/build-cssh.sh all # 产出 artifacts/CSSHCore.xcframework (library-only!)
|
||
|
||
# 重建 mosh 库 xcframework(mosh 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) 模拟器 UDID:`2EA9055E-4557-4B20-82AF-56A68AC1D454`(变了用 `xcrun simctl list devices`)。
|
||
```sh
|
||
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 IP;relay=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`)
|
||
- **M4 publickey 认证**:`-txPubkeyAuth 1`(不给 -txPass;SigningKeyProvider 造 P-256 key,日志打 authorized_keys 行,加到服务器后重连免密登录)
|
||
- **M4 host key mismatch 测试**:`-txHostKeyPinOverrideBase64 <b64>`(播种假 pin → 真 host key 不符 → 弹告警)
|
||
|
||
**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 pulse`→`TXM3 recovered` 时间差判 <3s。**坑:模拟器看门狗会杀掉被 STOP 冻结过久(>~30s)的 app;且模拟器不复现 socket defunct/WG 过期,深挂起需真机锁屏补测。**
|
||
|
||
## 仓库结构
|
||
- `packages/TXCore` — 零依赖纯逻辑:tmux 解析器/layout/`SessionMachine`(会话状态机+退避重连)/`TmuxControlSequence`(DCS 检测)。
|
||
- `packages/TXTransport` — `SSHSession`(libssh2, 支持外部 fd)/`MoshSession`(mosh_main 桥)/`Transport` 协议;C shim: `CSSH`+`CSSHCore`(libssh2)、`CMosh`+`MoshCore`(mosh)。
|
||
- `apps/TerminalX/iOS` — `SSHTerminalModel`(连接编排+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(**有意提交**,非 gitignore):`CSSHCore`/`TsnetBridge`/`MoshCore`。
|
||
|
||
## 约定 & 坑
|
||
- **CSSHCore/MoshCore 必须 library-only**(无 Headers/modulemap);Swift module 由包内 `CSSH`/`CMosh` C target 提供。否则与 GhosttyKit 撞 `include/module.modulemap`。
|
||
- **mosh 构建**:绕过 autotools 用 CMake 直编 `src/*.cc`(手写 `build/mosh/config.h`);crypto 用 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` 枚举。
|
||
- **tmux 多 pane 尺寸走"路线 b"**:app 按全屏格子发 `refresh-client -C WxH` 让 tmux 布局,pane surface 尺寸严格取 layout rect(%output 按 tmux pane 宽高排版,二者不一致必换行/清屏错乱);pane surface 的 resize 回调**只断言不反向驱动 tmux**(防反馈环)。diff 用 full layout、渲染用 visible layout(zoom 免费);SwiftUI identity 只用 paneID(布局变不重建 surface)。新 pane 在 `capture-pane` %end 前丢弃其 %output(tmux 单线程串行保证快照不缺不重)。
|
||
- **tmux 网关字节必须单消费者保序**:多个 `Task{@MainActor}` 到主线程顺序无保证,多 pane 高吞吐会打碎 % 协议 → 用 `TmuxByteChannel`(AsyncStream) 单消费者。
|
||
- SourceKit "No such module" 是索引噪声,**以 `swift build`/`xcodebuild` 为准**。
|
||
- 密钥/密码**绝不写进仓库**;测试主机凭据由用户即时提供。
|
||
- 提交/推送 git 只在用户明确要求时(当前已 `git init`,尚无 commit)。
|
||
- 与用户始终用**简体中文**交流。
|
||
</content>
|