躺在沙发上开发 Openclaw 的移动端APP

我没有从零写 OpenClaw 的手机 App 网关层。我只是躺在沙发上,用自己做的对话 App 跟本机的 AI 聊了几句,它就把一份 1593 行的 Swift 源码「翻译」成了 Dart,帮我做出了 Flutter 版的 gateway_connection.dart。这篇把整个过程和关键代码对照摊开给你看。

起因:我要给 OpenClaw 写个手机端

OpenClaw 是个本地跑的 AI agent 网关。我想给它配一个手机 App,第一关就是连上它的 WebSocket 网关------所有命令、消息、事件都走这条连接。

官方 macOS 客户端里有一份 GatewayConnection.swift1593 行,把网关连接这件麻烦事全封装好了:连接管理、鉴权、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. 连接服务器

SwiftGatewayConnection.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 发一条消息)

SwiftsendAgent):

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 MethodrawValue
struct(值类型) class / const 构造
async throws Future<T>

这里最重要的一个原则,也是我回头觉得最值钱的:wire 层的字符串绝对一个字都不能改。

比如那些 JSON-RPC 方法名------agentset-heartbeatssystem-eventchat.sendcron.addsessions.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 它给的代码。

我把它「能躺着做」拆成三个前提:

  1. 它是跑在我自己机器上的。 OpenClaw 在本机跑,我的代码只在我自己的机器/内网里被读取分析,不出内网。我敢把整个开源库源码丢给它读,也正是因为数据不出我这台电脑。
  2. 它看得比人快、比人全。 1593 行、几十个方法、一整套鉴权错误码,人扫一遍容易漏,它一遍给我结构化全貌,我再挑不懂的点追问。
  3. 跨语言它天生是「翻译」。 把 Swift 移植到 Dart,本质是"读懂语义 + 按目标语言重写",正好是它擅长、我懒得干的部分。

成品在这里

完整的 Dart 移植我开源在 Gitee:

对照的 OpenClaw Swift 原版:github.com/openclaw/op...


表面上看,这篇讲的是「我让 AI 读懂了一份 1593 行的 Swift」。但实际上------我只是想看一个自己写的 App,能在沙发上被开发出来。 现在我的手机 App 真能连上本机的 OpenClaw 了,而那份 Swift 文件,我到现在也没完整逐行读完,是它替我读的。

如果你也在用 AI 对付"别人的老代码库",或者对「用 AI 做跨语言移植」有心得,评论区聊聊,挺有意思的。

相关推荐
用户2181697049301 小时前
Flutter(十三)Text Image TextField
前端
程序员黑豆1 小时前
深入解析Java数据类型:基本类型与引用类型的本质区别与实战选择
前端·ai编程·全栈
天空之城--2 小时前
Android Flutter行业动态与学习参考(2026年8月)
android·flutter
东方小月2 小时前
从零开发一个 Coding Agent(七):实现纯文本 Agent Loop
前端·人工智能
大家的林语冰2 小时前
👉 尤雨溪再次成立新公司,同时官宣 Pinia 4 正式发布!
前端·javascript·vue.js
浮生望2 小时前
React useRef 深度解析:从DOM操作到Web Worker多线程协作
前端
浮生望2 小时前
React Router v7 实战全景:路由配置、嵌套路由、鉴权守卫与懒加载完整指南
前端
用户938515635072 小时前
从零在浏览器里跑 DeepSeek-R1:WebGPU + Transformer.js 全链路实战(二)
前端·javascript·typescript
雪隐2 小时前
个人电脑玩AI-16让5060 Ti给你打工——MiniMax H3 提速实录:从"泡杯茶等视频"到"视频等你",一张 5060 Ti 的自我修养
前端·人工智能·后端