问题的起点:TypeScript 生态里缺一块拼图

OpenAlice 是一个全 TypeScript 架构的 AI Agent 项目。选 TypeScript 的理由很直接:主流 Agent SDK 框架——Claude Agent SDK、OpenClaw、Vercel AI SDK——基本都是 TypeScript 生态,甚至 Claude Code 本身也是 TypeScript 写的。对于一个需要前后端一把梭的大型软件来说,TS 几乎是唯一的选择。

但盈透证券(IBKR)的 TWS SDK 只提供 Java、C++ 和 Python 版本。社区虽然有一个 TypeScript 的包,但完成度一般、文档缺失、长期无人维护。这种关键依赖如果自己不能掌控每一行代码,后续的维护就是灾难。

所以只剩一条路:自己用 TypeScript 重写。

为什么不走「桥接」的捷径?

很多人的第一反应是:直接用 Python SDK,起一个 Python 进程做桥接不就行了?

想想这个链路:Alice(TS)→ RESTful → Python 进程 → TWS Socket → 本地 TWS 客户端。中间多了一层完全没必要的进程和端口转换,还会丢类型信息,还需要额外做一遍语义化工作。

更关键的是产品体验问题。对于追求「开箱即用」的产品来说,让用户启动前先配一遍 Python 环境,这是不可接受的。OpenAlice 之前用 OpenBB 做数据源就吃过这个亏——不得不单独起一个 Python 进程,后来被迫把 OpenBB 需要的部分也重构成了 TypeScript。

那 IBKR 官方的 REST API 呢?两个硬伤:一是功能不全,很多 TWS SDK 能做的事 REST API 做不了;二是团队在 Alpaca 上已经被 REST 方案的断连问题坑过——断了之后不到下一次请求你根本不知道,心跳和重连逻辑全得自己做。

一个本地已经在跑的 TWS 实例,用 Socket 直连,比任何 REST 方案都稳定。所以最终方案很明确:用 TypeScript 直接实现 TWS 的 Socket 协议

产品定位:挂机小精灵

这里有一个值得独立开发者思考的产品思维——Alice 并不想取代 TWS。

理想的使用场景是:用户已经在用 IBKR 的 TWS 做专业分析和手动交易,Alice 挂在后台,像游戏里的挂机小精灵一样,帮你盯盘、做策略执行、处理那些不想盯着屏幕做的事情。TWS 是主操作台,Alice 是自动辅助,两者通过本地 Socket 直连,零延迟、零中间层。

这个定位决定了技术选型:不是替代,而是共存。做一个能和任何专业工具共存的 AI 层。

调研先行:选对翻译的「源语言」

决定自己写之后,第一步不是让 AI 开干,而是先把三个官方 SDK 全部拉下来通读——Java、C++、Python。

这里有一个反直觉的发现:社区那个 TypeScript 包是对着 Java 版逐行翻译的,但 Java 版是最早的实现,代码相当陈旧。后来官方用 Python 重写的版本,结构清晰得多,抽象层次也更合理。

所以团队做了一个关键判断:照 Python 版来写,不照 Java 版。社区包的翻译源头就选错了,自然也没法参考。

工程深水区:理解 Peterffy 的协议设计

真正进到工程里才发现,IBKR 的通信协议比表面复杂得多。

它最早用的是一种纯位置编码的消息流。乍一看跟 FIX(金融信息交换协议)有点像——都是用字符串传结构化的金融数据。但有一个根本区别:

  • FIX 协议是自编码的:每个字段前面带着自己的索引号(tag),比如 35=D 表示「第 35 号字段的值是 D」。你不需要知道字段在消息里排第几个。
  • IBKR 的协议把自编码抠掉了:字段含义完全由位置决定。第 1 个字段是什么、第 N 个字段是什么,纯靠数位置。

这个设计在当时可能是为了「简化」——少传几个字节,解析快一点。但后果是极其难以扩展。一旦要在中间加字段,或者某个字段变成可选的,整个解析逻辑就变得极其脆弱。

IBKR 自己也意识到了这个问题,新版本已经在迁移到 Protobuf——本质上就是回到了 FIX 一开始就做对了的自编码设计。Python 版 SDK 里已经用 Proto 文件来做类型化约束了。

这就产生了一个具体的技术问题:怎么把 Proto 定义编译成 TypeScript 代码?Proto 文件的编译配置、TypeScript 代码生成的选型(protobuf-ts?ts-proto?buf?)——这些具体的事情,就是交给 Claude Code 去处理的。

实操中的关键一步:先拆文件,再让 AI 翻译

IBKR 的 Python SDK 很明显是人写的——对于字段多、结构重复的东西,人类工程师倾向于全维护在一个文件里。结果就是动不动一个文件五六千行、七八千行。

这种规模对 AI 来说是灾难性的。AI 在七千行的上下文里会迷路,会丢失关键细节。

所以第一步不是直接让 AI 翻译大文件,而是先把大文件按功能模块拆小。这步看起来不大,但对后续整个 AI 开发进程的加速作用是巨大的。拆完之后,AI 每次只需要理解一个几百行的小模块。

核心方法论:自上而下 + 测试驱动

拆完文件之后,第二个关键问题是:怎么确保 AI 写出来的是「正确的产物」,而不仅仅是「能跑的代码」?

这两者的区别非常大。AI 写代码有一个常见倾向:当它发现缺少字段或类型不匹配时,不会停下来告诉你「这里有问题」,而是会用启发式的方式把问题糊弄过去——

  • 缺一个字段?塞个默认值
  • 类型对不上?加个 as any
  • 某个分支太复杂?直接简化掉

当前上下文里这样改可能看上去无所谓。但换一个新窗口、让另一个 AI 接手时,这种代码就变成地雷:你分不清哪些是「故意这么写的」,哪些是「AI 偷懒糊弄的」。

所以需要一套方法论来约束 AI 的行为:

  1. 测试驱动开发(TDD):先把官方测试案例搬过来,让 AI 知道「正确的输出长什么样」
  2. 自上而下对照翻译:Python 版的实现就是 spec,不让 AI 自由发挥,而是给它明确的参照
  3. 翻译和重构同步进行:在搬运的过程中把大文件拆成小模块,而不是先翻译完再重构

这三步循环下来,一步步把整个重构做完了。

这个方法论的适用范围远不止 IBKR:任何「用 AI 做大型代码翻译/重构」的场景,核心都是三件事——给 AI 明确的参照源、明确的验证标准、小颗粒度的工作单元。

最终效果

整条决策链串起来看:

  • Agent 生态是 TypeScript 的 → 工程只能选 TS
  • 官方不提供 TS SDK → 社区包不可靠 → 自己写
  • Python/Java 桥接多一层无意义的进程 → 不干
  • REST API 功能不全且有断连风险 → 本地 Socket 直连
  • Claude Code 一晚上搞定边界清晰的重构任务

最终 OpenAlice 又向「一键启动、本地运行、全 TypeScript 栈」迈进了一步。Clone 下来,跑一个命令,Alice 就开始工作。不需要 Python 环境,不需要 Java 运行时,不需要额外的端口桥接。

对于独立开发者来说,这个案例最值得带走的不是具体的 IBKR 重构细节,而是那套方法论:当你需要用 AI 完成一个大型重构时,先拆文件降低单次理解负担,再用测试驱动确保输出正确性,最后自上而下对照翻译而非让 AI 自由探索。下次你面对一个「关键依赖没有目标语言实现」的问题时,不妨想想:如果先把源码拆成几百行一个的小模块,Claude Code 能不能一个晚上帮你解决?