English

为了预约一次西班牙录指纹,我先在西班牙搭了一台服务器

为了帮家人预约西班牙 TIE 录指纹,我从 AWS、SOCKS5 一路折腾到 Chrome Native Messaging,最后还是卡在了 Cl@ve。

我最近一直在帮家人办理西班牙非盈利居留。几经周折,签证终于办下来了,人也顺利入境西班牙。接下来要做的,是预约去警察局录指纹,然后申请实体的 TIE 居留卡。

我原本以为,这一步无非就是打开网站、填几项资料、选一个时间。

结果第一个要解决的难题不是准备材料,而是先想办法打开这个移民局网站

我在西班牙以外怎么也打不开这个网站

我分别从中国和瑞典试过。进入预约流程以后,页面一直转圈,最后只告诉我网页无法访问。

它也不解释原因,只是沉默地卡在那里,让你怀疑是浏览器坏了、网络坏了,还是网站今天又挂了。

但我的直觉是,这个网站可能只接受来自西班牙的访问。为了进一步确认,我用 GlobalPing 从西班牙节点发了一次请求,页面可以正常响应。

于是我又买了几个提供西班牙节点的商业 VPN。VPN 本身都能用,分配给我的 IP 看起来也确实在西班牙,但这个预约网站还是打不开。

我的猜测是,它不只是限制访问地区,还拦了不少商业 VPN 常用的西班牙 IP。

既然常见 VPN 不行,那我就自己做一个吧。

干回老本行,在西班牙搭一台服务器

我打开 AWS 控制台,切到西班牙区域,创建了一台 EC2。

没必要把它配置成一套完整的 VPN。一个 SSH dynamic port forwarding 就够了:在本地开一个 SOCKS5 端口,再让浏览器通过这台西班牙 EC2 访问网站。

ssh -i <SSH_KEY> \
  -N \
  -D 127.0.0.1:1080 \
  -o ExitOnForwardFailure=yes \
  ubuntu@<EC2_IP>

隧道建立以后,我通过这个 SOCKS5 代理访问 IP 检测服务。返回的公网 IP 和 EC2 一致,地理位置也确实是 Spain、Aragón、Zaragoza。

所以 SSH Tunnel 没问题。

但隧道存在,不代表浏览器会自动走这条隧道。我也不想修改整个 macOS 的系统代理,毕竟我只需要用西班牙网络打开这一个网站。

最后的方案是,创建一个完全独立的 Chrome profile,只让这个 Chrome 进程使用 SOCKS5。

一开始我用 mktemp 创建了一个临时 profile。网站终于能打开了,但临时 profile 不会继承平时 Chrome 里的 Cookie、扩展和登录状态,每次重新运行启动命令还会再创建一个。

所以我后来把它改成了一个持久 profile:

~/Library/Application Support/Google/Chrome-Spain-Visa

最终的启动命令是:

open -na "Google Chrome" --args \
  --user-data-dir="$HOME/Library/Application Support/Google/Chrome-Spain-Visa" \
  --proxy-server="socks5://127.0.0.1:1080" \
  --host-resolver-rules="MAP * ~NOTFOUND, EXCLUDE 127.0.0.1" \
  --no-first-run \
  --no-default-browser-check \
  --new-window \
  "https://checkip.amazonaws.com/"

这个 Chrome 显示的是西班牙 EC2 的公网 IP,预约网站也终于可以正常打开。

到这里,我以为最麻烦的部分已经结束了。

结果还早。

网站打开了,Codex 却控制错了 Chrome

网站能打开还不够。因为我知道,预约不是进去填一次表就结束了。大部分时候根本没有位置,只能隔一段时间再回来,从头走一遍,一次一次地刷。

所以从一开始,我就没准备每次都自己手动操作。我想让 Codex 控制浏览器,把这套流程跑通并记录下来,为以后自动重复查询做准备。

这也意味着,我不只需要一个能打开网站的浏览器,还需要这个浏览器能被 Codex 稳定控制。

我平时默认用 Brave,但 ChatGPT Chrome Extension 和 Codex 的浏览器控制目前都是用 Chrome 验证的,所以这次我直接用了 Chrome。

我先在普通 Chrome profile 里安装了 ChatGPT Chrome Extension。Codex 可以连接,也确实可以控制它。

但从这个被控制的 Chrome 里检查公网 IP,返回的还是原来的 IP,不是西班牙 IP。

也就是说,Codex 虽然已经“能控制 Chrome”,控制的却是错误的 Chrome profile。

与此同时,真正走西班牙 EC2 的那个 Chrome 能打开预约网站,却没有安装扩展,Codex 控制不了它。

问题变成了:

普通 Chrome
  → Codex 可以控制
  → 但没有走西班牙代理

西班牙 Chrome
  → 可以打开预约网站
  → 但 Codex 控制不了

我要做的,是把这两件事合并到同一个 Chrome profile 里。

我在持久的 Chrome-Spain-Visa profile 里安装并启用了 ChatGPT Chrome Extension,但扩展一直提示:

Install the app to use ChatGPT in Chrome

问题是,Codex App 明明已经安装了。

我试过退出所有 Chrome、重新启用扩展、重新启动专用 profile,还是不行。最后才在错误信息里看到:

NATIVE_DISCONNECT Specified native messaging host not found.

这才把问题缩小到 Chrome Native Messaging。

真正的问题藏在 Native Messaging

ChatGPT Chrome Extension 不是直接连接 Codex App。它需要通过 Chrome 的 Native Messaging 机制,连接 Codex App 安装在本地的 native host。

我和 Codex 把 manifest、runtime 和 native host 一层层查了一遍。该在的文件都在,协议诊断也能正常运行。Codex App、本地 runtime 和 native host 本身其实都没有坏。

真正的问题是:这个 Chrome 根本找不到 manifest。

至少在这次使用自定义 user-data-dir 的情况下,Chrome 会去这个用户目录下的 NativeMessagingHosts 查找 manifest。

正常安装时,Codex App 把 manifest 放在 Chrome 默认用户目录下面。但我启动西班牙浏览器时指定了一个自定义目录:

--user-data-dir=".../Google/Chrome-Spain-Visa"

对于这个 Chrome 进程来说,Chrome-Spain-Visa 才是它的用户数据根目录。它不会再去普通 Chrome 的目录里寻找 native messaging manifest。

所以扩展才会报:

Specified native messaging host not found.

最终,我把 Codex App 已经安装的 manifest 放到了这个 profile 对应的 NativeMessagingHosts 目录。目标位置类似:

~/Library/Application Support/Google/Chrome-Spain-Visa/
  NativeMessagingHosts/
    com.openai.codexextension.json

重新启动 Chrome 以后,扩展终于成功连接。

这次从 Codex 实际控制的 Chrome 里检查公网 IP,返回的是西班牙 EC2 的 IP。两件事终于同时成立:

  • 这个 Chrome 确实走西班牙网络;
  • Codex 控制的也确实是这个 Chrome。

这里其实有两条不同的路线。

Codex 控制浏览器:

Codex App
  ↔ native host
  ↔ ChatGPT Chrome Extension
  ↔ Chrome-Spain-Visa profile

浏览器访问网站:

Chrome-Spain-Visa profile
  → SOCKS5 127.0.0.1:1080
  → SSH Tunnel
  → AWS Spain EC2
  → 西班牙预约网站

为了在西班牙录个指纹,我终于在西班牙搭了一台服务器,还顺手把 Chrome Native Messaging 排查了一遍。

终于进入真正的预约流程

既然以后还要反复刷位置,这一遍就不能只是点完算了。我让 Codex 按照我的指令操作浏览器,并把每一页需要点什么、填写什么、如何判断结果,完整记录成一份 runbook。

Codex 从西班牙政府电子政务网站的 Cita previa de extranjería 预约页面开始,选择 Barcelona,保持任意办公室,选择 TIE 录指纹业务,然后进入无 Cl@ve 流程,填写 NIE、姓名和国籍。

折腾了这么久,总算到了查询名额的最后一步。

页面显示:

En este momento no hay citas disponibles
para la reserva sin cl@ve.

不用 Cl@ve,当前没有任何空位。

紧接着,页面又用粗体告诉我:

SÍ TIENEN A SU DISPOSICIÓN MEDIANTE EL USO DE CL@VE,
CITAS DISPONIBLES PARA SU RESERVA.

但如果使用 Cl@ve,就有位置。

Cl@ve 是西班牙政府的电子身份认证系统。问题是,对于一个刚拿到签证、刚到西班牙、第一次申请 TIE 的人来说,没有 Cl@ve 本来就很正常。

同一个业务、同一个省份、同一个时间点,有没有位置,居然还取决于你有没有西班牙电子身份。

整个设计多少有点循环依赖的味道。

走完一遍以后,我又让 Codex 从检查浏览器的公网 IP 开始,完整重放了一次。第二次从政府网站的预约页面一直到“无 Cl@ve 没位置”的结果页,全部由 Codex 自动完成。

这说明我们保存下来的不只是一段聊天记录,而是一套确实可以复现的查询流程。

这不只是让 AI 帮我点几个按钮

这件事让我觉得有意思的地方,不是 Codex 替我点击了几个按钮。

真正有用的是,我们把一次充满试错的操作,慢慢变成了一套可以重复运行的流程:浏览器确实通过西班牙 EC2 访问网站,使用的是独立的环境,Codex 控制的也是正确的 profile,每一步都有明确的定位,失败时也知道应该停在哪里。

以后我可以直接告诉 Codex:

再帮我检查一次有没有位置。

它就可以按照 runbook 重新跑一遍。

对我个人来说,Codex 加一份 runbook 已经足够用了。但做到这里,我也开始想:这是不是不只我一个人遇到的问题?

这能不能变成一个小产品

如果很多刚到西班牙的人都卡在同样的地方,它也许可以变成一个很小的 browser-use 工具。

申请人的资料只保存在本地,工具负责检查网络、重放流程,在发现可用名额时通知用户。遇到安全提示、CAPTCHA 或最终确认,就停下来交给用户,而不是自己继续往下点。

现在还远没到真正做产品的时候。网站允不允许定时检查、多久查一次比较安全、SSH Tunnel 怎么保活、浏览器断线以后怎么恢复,这些问题都还没有答案。

更现实的是,我甚至还没有成功约到一次。

但这个问题已经从“一家人的签证麻烦”,慢慢变成了一个我想继续观察的产品问题。

在想清楚这些之前,我还得先回答眼下最现实的问题:

西班牙到底什么时候,才肯给一个刚到西班牙、还没有 Cl@ve 的人放出预约名额?