躺在沙发上开发 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 做跨语言移植」有心得,评论区聊聊,挺有意思的。

相关推荐
开开心心就好3 小时前
批量提取PDF中的图片,直接导出原图
前端·javascript·支持向量机·智能手机·pdf·html·启发式算法
Setsuna_F_Seiei5 小时前
前端转型 Agent 开发 05 之 Agent Hooks 与 Checkpointer(让 Agent 从全自动转变人为可掌控)
前端·agent·ai编程
百万蹄蹄向前冲7 小时前
风扇转了一晚上MVP专家团翻车事故
前端·人工智能
默_笙7 小时前
🏛 给 AI 配一间办公室:Harness Engineering 六大模块与它的实现
前端·javascript
linux_cfan9 小时前
videojs v10 源代码系列解读:14 · 谓词守卫:在运行时安全地调用能力
前端·javascript·音视频
kyriewen9 小时前
我扒了 10,221 条 JD:腾讯技术岗 75% 在要 AI
前端·人工智能·ai编程
郑州光合科技余经理10 小时前
同城外卖小程序开发:下单成功后,后台导出能不能对上用户端状态
开发语言·前端·git·后端·uni-app·php·ai编程
IT_陈寒10 小时前
Vue的响应式让我熬到凌晨三点,原来漏了这个小细节
前端·人工智能·后端
Blanche150011 小时前
利用 RAG 为答疑机器人扩展知识范围
前端
天若有情67311 小时前
【纯前端小工具】公历生日转农历,批量查询每年农历生日对应的公历日期(GitHub Pages在线直接用)
前端·javascript·github pages·农历转换·lunisolar·网页小工具