我没有从零写 OpenClaw 的手机 App 网关层。我只是躺在沙发上,用自己做的对话 App 跟本机的 AI 聊了几句,它就把一份 1593 行的 Swift 源码「翻译」成了 Dart,帮我做出了 Flutter 版的
gateway_connection.dart。这篇把整个过程和关键代码对照摊开给你看。
起因:我要给 OpenClaw 写个手机端
OpenClaw 是个本地跑的 AI agent 网关。我想给它配一个手机 App,第一关就是连上它的 WebSocket 网关------所有命令、消息、事件都走这条连接。
官方 macOS 客户端里有一份 GatewayConnection.swift,1593 行,把网关连接这件麻烦事全封装好了:连接管理、鉴权、JSON-RPC 方法封装、事件订阅、重连、设备配对......非常完整,但太长。
我不想从零写,更不想逐行啃。我的方案是:让 AI 先读懂,再指导我用 Dart 抄一份。 全程我基本躺着。
我做了什么:让 AI 帮我「翻译」一个类
我用手机上的 ParrotClaw(连到我本机 OpenClaw 的对话 App)跟它聊,过程大概是:
我:
我要用 Flutter/Dart 给 openclaw 写个 App。先把 openclaw 的
apps/macos/Sources/OpenClaw/GatewayConnection.swift读懂,它的职责是封装 WebSocket 网关连接。我想照着移植成 Dart,你先帮我拆结构和给参考实现。
AI:
这层核心是一个单例 actor(
GatewayConnection.shared),所有调用方共享同一条网关 WebSocket 连接。关键部件:
GatewayChannelActor:底层真正持握 WebSocket 的 actor,负责连接/断开/收包EndpointSnapshot / Route / ServerLease:描述"连到哪个网关、用什么鉴权、当前是第几次连接会话"Method枚举:把网关全部 JSON-RPC 方法名抽成枚举(agent、chat.send、cron.*、skills.*、sessions.*...)subscribe():返回AsyncStream<GatewayPush>,App 各处监听网关推送事件- 一系列
request/requestRaw/requestDecoded/requestVoid:按返回类型区分调用
听完我大概有数了:这是一层「WebSocket 连接 + JSON-RPC 封装 + 事件分发」。于是我开始让它逐段翻译。
三个核心功能的 Swift → Dart 对照
下面是我让 AI 翻译、我再核对落地后的三个最核心片段,Swift 和 Dart 放在一起看。
1. 连接服务器
Swift (GatewayConnection.swift 里创建底层连接):
swift
let client = GatewayChannelActor(
url: url,
token: token,
password: password,
pushHandler: { push in await connection.handle(push) },
disconnectHandler: { reason in await connection.handleDisconnect(...) }
)
Dart (ParrotClaw gateway_connection.dart,我照抄了这个思路):
dart
_client = GatewayChannelActor(
url: url,
token: token,
password: password,
pushHandler: (push) => _handle(push),
disconnectHandler: (reason) => _handleDisconnected(reason),
);
_configuredURL = url;
_configuredToken = token;
_configuredPassword = password;
await _client!.connect();
结构几乎一一对应:同样是「一个持握 WebSocket 的包装对象 + 连接参数 + 推送/断开回调 + connect()」。
2. 发送文字(给 agent 发一条消息)
Swift (sendAgent):
swift
func sendAgent(_ invocation: GatewayAgentInvocation) async -> (ok: Bool, error: String?) {
guard !invocation.message.trimmingCharacters(in: .whitespaces).isEmpty else {
return (ok: false, error: "message empty") }
let sessionKey = await canonicalizeSessionKey(invocation.sessionKey)
// 组装 params 后走 requestVoid(.agent, params:)
await self.requestVoid(.agent, params: params)
return (ok: true, error: nil)
}
Dart(我照搬并加了个顺手封装的便捷方法):
dart
Future<({bool ok, String? error})> sendAgent(
GatewayAgentInvocation invocation,
) async {
final trimmed = invocation.message.trim();
if (trimmed.isEmpty) return (ok: false, error: 'message empty');
final sessionKey = canonicalizeSessionKey(invocation.sessionKey);
// ...
await requestVoid(Method.agent, params: params);
return (ok: true, error: null);
}
// 加了个 Swift 里的便捷重载
Future<({bool ok, String? error})> sendAgentMessage({
required String message,
required String sessionKey,
required bool deliver,
String? to,
GatewayAgentChannel channel = GatewayAgentChannel.last,
// ...
}) => sendAgent(GatewayAgentInvocation(message: message, /* ... */));
连「空消息返回 (false, 'message empty')」这种兜底逻辑我都原样保留了。
3. 监听收到的消息(订阅网关推送)
Swift(订阅推送流,返回 AsyncStream):
swift
func subscribe(bufferingNewest: Int = 100) -> AsyncStream<GatewayPush> {
// 用一个 UUID 注册 continuation,推送来了就广播
let id = UUID()
subscribers[id] = continuation
// ...
}
Dart (我用 StreamController 对应,实现同样的广播订阅):
dart
Stream<GatewayPush> subscribe() {
final id = const Uuid().v4();
final controller = StreamController<GatewayPush>.broadcast();
_subscribers[id] = controller;
// 有缓存的 snapshot 就先推一个(对齐 Swift 行为)
final snapshot = _lastSnapshot;
if (snapshot != null) controller.add(GatewayPushSnapshot(snapshot));
controller.onCancel = () => _removeSubscriber(id);
return controller.stream;
}
Swift 用 AsyncStream + [UUID: Continuation] 做订阅/广播,Dart 用 StreamController.broadcast() + [UUID: StreamController],语义一致,只是语言表达不同。
我让它「翻译」的关键窍门
逐行翻译很容易在并发模型上翻车,因为两边的并发思想不一样:
| Swift 概念 | Dart 写法 |
|---|---|
actor(隔离并发状态) |
class + 独立的 GatewayChannelActor |
AsyncStream + [UUID: Continuation] |
StreamController.broadcast() + [UUID: StreamController] |
enum Method: String |
enum Method 带 rawValue |
struct(值类型) |
class / const 构造 |
async throws |
Future<T> |
这里最重要的一个原则,也是我回头觉得最值钱的:wire 层的字符串绝对一个字都不能改。
比如那些 JSON-RPC 方法名------agent、set-heartbeats、system-event、chat.send、cron.add、sessions.preview------这些是网关对外的协议名,Dart 端必须和 Swift 端一字不差,改一个字符就连不上。所以翻译这种「语义 + 协议常量」的组合,AI 是大杀器:结构它帮我翻译,协议常量保证原样。
给翻译结果「留指纹」
所有的 Dart 代码,我都让它在旁边标注了对应的 Swift 出处:
dart
// ─────────────────────────────────────────────
// MARK: - Method enum (≈ Swift GatewayConnection.Method)
// ─────────────────────────────────────────────
enum Method {
agent,
setHeartbeats('set-heartbeats'),
systemEvent('system-event'),
cronAdd('cron.add'),
// ...
}
每一段都带「≈ Swift 的哪个类/函数」。这样做的价值:
- Swift 升级了协议、改了方法,我能顺着注释一分钟定位到 Dart 这边该改哪
- 别人看我的代码,能按注释回查 OpenClaw 原版,维护成本大降
- 万一哪段行为对不上,能快速 diff 出是「翻译错了」还是「协议变了」
为什么我能「躺着」读完
说到头,真正的技术活是 AI 干的,我必须做对的事其实只有两件:告诉它要什么,然后 review 它给的代码。
我把它「能躺着做」拆成三个前提:
- 它是跑在我自己机器上的。 OpenClaw 在本机跑,我的代码只在我自己的机器/内网里被读取分析,不出内网。我敢把整个开源库源码丢给它读,也正是因为数据不出我这台电脑。
- 它看得比人快、比人全。 1593 行、几十个方法、一整套鉴权错误码,人扫一遍容易漏,它一遍给我结构化全貌,我再挑不懂的点追问。
- 跨语言它天生是「翻译」。 把 Swift 移植到 Dart,本质是"读懂语义 + 按目标语言重写",正好是它擅长、我懒得干的部分。
成品在这里
完整的 Dart 移植我开源在 Gitee:
对照的 OpenClaw Swift 原版:github.com/openclaw/op...
表面上看,这篇讲的是「我让 AI 读懂了一份 1593 行的 Swift」。但实际上------我只是想看一个自己写的 App,能在沙发上被开发出来。 现在我的手机 App 真能连上本机的 OpenClaw 了,而那份 Swift 文件,我到现在也没完整逐行读完,是它替我读的。
如果你也在用 AI 对付"别人的老代码库",或者对「用 AI 做跨语言移植」有心得,评论区聊聊,挺有意思的。