用自动化与 AI 和 221(g) 焦虑对抗:NVC Case 邮件订阅网站实践记
线上地址:https://eazyctl.com
等待美签结果最折磨人的部分,是你明明什么都做不了,却又不敢错过任何微小的进展。这篇文章想分享我如何把 221(g) 行政处理期间的焦虑拆解成一个个可执行的问题,从最初的命令行脚本一路搭建成如今的 NVC Case 邮件订阅网站,并在过程中引入 AI、自动化、可视化、监控等手段,让“等待”变得可量化、可追踪、可分享。
背景:从 221(g) 焦虑到动手做工具
2025 年底,我在面谈后收到了 221(g) 补件要求。那段时间每天都在刷 CEAC,盯着那行 Last Updated 字样,不知道下一次更新时间会是什么时候,却依旧没法把“变化”体系化。。最初的几周,我用 curl 去抓页面,把日期输出到一个 txt 文件里;后来受不了这种等待,就让脚本顺便发一封邮件,这样至少不用半夜起来刷新。随着日志积累,我开始把每一次更新时间串成时间线,第一次发现“原来 12 月那几天还发生了这么多变化”。
这种“把不确定拆成任务”的感觉很微妙:即便流程没有加快,但我知道自己做了点什么。我在第三周给脚本加了 SQLite,把历史写进去;第五周用 React + shadcn 搭起了一个味道很“工程师 UI”的小面板;第七周试着用 Ollama + qwen3-vl 去识别验证码,让夜间巡检不再依赖我;第十周把它分享给同样在等待的朋友;现在干脆公开地址,让更多人用得上。
产品定位与核心功能
网站对用户的承诺很简单:只要你告诉我案件号、必要的信息和通知邮箱,我就替你定期访问 CEAC。一旦状态或 Last Update 日期有所变化,我会把差异和简短的时间线摘要通过邮件发给你。用户第一次使用通常会从 MyNvcCase 页面开始,那里既能发起一次即时查询,也能看到每一步日志,包括“初始化会话”“下载验证码”“AI 识别结果”“提交表单”等。这些日志都以流式方式返回,如果AI识别的验证码不正确,会重试最多15次,一般来说,重试不会超过5次就会成功。
订阅成功后,后台的定时任务会把该 case 编入轮询队列。系统会把每一次查询得到的状态写入历史表,如果发现了任何变化,系统会把变化的内容标注出来,把最新的结果发送到订阅者的邮箱。
整体流程如下
- 录入 Case:在
MyNvcCase页面填写 Case Number、护照号前缀、姓氏、签证类型(IV/NIV)等。 - 即时 Lookup:前端发起一次流式请求,实时看到日志(初始化、验证码下载/识别、提交 CEAC、解析结果)。
- 订阅:若希望后台自动轮询,可输入一个或多个邮箱(逗号分隔),系统会缓存这些邮箱并写入队列。
- 等待通知:后台脚本按既定频率查询。一旦 status/lastUpdate 变更,通过邮件把差异和时间线摘要发给用户。

示例:日志流片段
1 | [1]正在向 NVC 验证案件是否存在... |
查询完成后,用户可以通过分享卡片的形式分享自己的Case状态:
系统架构一览
整体架构仍然是一个“前端 + API + 定时任务 + 数据库”的经典组合,但我在其中加入了流式日志、AI 验证码服务、自建 SMTP、以及用于健康检测的本地 Analytics。
flowchart LR
subgraph Client
A[ReactWeb]
end
subgraph Server
B[Express API]
C[Node-cron Scheduler]
D[Ollama Runtime]
E[SMTP Sender]
F[(SQLite)]
end
A -->|REST / SSE| B
B --> F
C -->|enqueue| B
C -->|captcha| D
B -->|mail payload| E
E -->|status email| User
C -->|HTTP| CEAC[CEAC Site]
前端使用 React + Vite 搭配 shadcn/ui 和 Tailwind,Timeline、Tooltip、状态徽章等全部在浏览器渲染。后端是一个 Express 应用,负责接收订阅请求、触发即时查询、提供时间线数据、管理退订等,SQLite 则承担所有持久化工作。定时任务通过 node-cron 每三十分钟唤醒一次,遍历仍有邮箱订阅的案件并逐个查询;每个 case 之间会暂停五秒,避免触发对方的限流策略。验证码服务跑在同一台机器上的 Ollama 里,需要时即开,空闲时自动休眠。邮件发送使用自建 SMTP,加上 Cloudflare Tunnel 让端口只对 API 节点开放。
- 前端栈:React + Vite + shadcn/ui + Tailwind + lucide 图标;Axios + fetch 混用,根据场景(SSE/流式日志使用原生 fetch)。
- 后端栈:Node.js + Express;SQLite 作为本地嵌入式 DB;node-cron 定时;Nodemailer + 自建 SMTP。
- 重要模块:
monitorService:每 30 分钟扫描 still subscribed 的 cases,串行执行checkCaseStatus。checkCaseStatus:封装 CEAC 请求、验证码处理、状态解析、历史写入、通知发送。MyNvcCaseAPI:/api/my-case/subscribe,/refresh,/unsubscribe,/status等。
Subscribe → Notification 流程
sequenceDiagram
participant User
participant Frontend
participant API
participant DB
participant Scheduler
participant Mailer
User->>Frontend: 输入 Case + Email
Frontend->>API: POST /api/my-case/subscribe (stream)
API->>DB: upsert case + emails
API-->>Frontend: 日志 & 状态 (SSE)
Scheduler->>API: checkCaseStatus(id)
API->>CEAC: 带验证码请求
API->>DB: 更新 status / history
API->>Mailer: 发送差异邮件
Mailer->>User: 状态提醒
AI 在验证码识别与流程守护中的实践
流程细化
sequenceDiagram
participant Cron
participant CaptchaSvc
participant Ollama
participant CacheDB
Cron->>CaptchaSvc: 下载 captcha.jpg
CaptchaSvc->>CacheDB: 查询 MD5 是否存在
alt 命中缓存
CacheDB-->>Cron: 返回 code
else 未命中
CaptchaSvc->>Ollama: POST /api/generate
Ollama-->>CaptchaSvc: 流式输出 code
CaptchaSvc->>CacheDB: INSERT (md5, code)
end
Cron->>CEAC: 带 code 请求
- 模型选择:qwen3-vl:30b 在识别拉丁字符混合验证码时准确率较好;同时本地部署便于控制调用频率和隐私。
- 流式解析:Ollama 输出是多段 JSON 流,后台边读边拼接,直到
response完整为止;最长超时 120s。 - AI 以外的兜底:
- 如果 CEAC 返回“验证码错误”字符串,会立即换图重试;
- 若网站整体不可达,任务会退回队列并记录失败时间,防止无效重试。
伪代码
1 | async function solveCaptcha(buffer) { |
AI 在验证码识别与流程守护中的实践
CEAC 的验证码非常不友好:字母和数字混在一起,背景上还有噪点,而且每次请求都必须重新输入。最初我都是手动输入,夜里经常被任务失败的通知叫醒。后来决定把这一环交给本地的开源视觉AI模型 qwen3-vl:任务在下载验证码后,会根据图片内容生成 MD5,如果缓存里已经出现过同一张图,就直接使用之前的答案;否则把图片以 base64 形式发送给 Ollama,请求流式输出。模型通常会在二十秒左右给出结果,我会把它保存到 captchas 表里,这样下次遇到同图就能命中缓存。全部流程都能再前端日志中清楚看到。
这一套机制整个case状态查询流程实现了自动化,不需要人工干预即可定期查询。
实现细节、踩坑与优化
项目核心都是围绕“让自动化尽量可靠”展开。比如 CEAC 会频繁返回 302 并携带多个 set-cookie,如果不逐项合并,很容易在下一次请求里丢失 session。我因此写了一个 updateCookies 函数,把所有 cookie 都放进 map 里统一维护,再拼回请求头。又比如夜间很多 case 会同时触发任务,早期我曾经直接并发查询,结果不到一天 IP 就被临时封禁。现在的策略是由 cron 统一唤醒,再按照 case id 顺序串行处理,中间穿插随机延迟,必要时对特定案件设置“冷却时间”。
状态解析也是一堆坑:IV 页面和 NIV 页面 HTML 完全不同,某些领馆还会返回额外字段。我最后写了多个解析器,根据签证类型和页面特征自动切换;一旦解析失败就把原始 HTML 保存到日志,方便回放。邮件模板也从一长段纯文本,升级成包含“新状态 vs 旧状态”对比、时间线摘要、FAQ 链接的结构化内容,方便用户快速理解差异。
成本、部署与监控
资源与费用
| 项目 | 当前方案 | 月成本 (USD) |
|---|---|---|
| 计算 | 1 vCPU / 1GB RAM VPS (旧金山) + 本地主机提供算力支持 | ~12 |
| 存储 | SQLite(本地磁盘定期备份至对象存储) | <1 |
| 域名 & SSL | Cloudflare 免费版 + Let’s Encrypt自动续期 + 域名 | ~2 |
| 邮件 | 第三方 SMTP | 0 |
| AI 推理 | 本地显卡 (RTX 4070) + Ollama,7x24 运行,夜间批量工作 | 折合电费 ~2 |
部署策略
- Docker Compose:将 API、Scheduler、Ollama 进程封装成服务;一键启动/重启。
- PM2:在非 Docker 环境下使用 PM2 守护脚本,自动拉起 crash 进程。
- 滚动发布:新版镜像先在备机验证,确保 AI、cron、API 正常后再切换主机。
监控与告警
- PM2 日志,观察 CPU/内存/错误日志。
- 自制健康探针:如果 30 分钟内没有任何 case 被成功查询,会给自己发 邮件提醒。
- 验证码失败率、CEAC 502/503 次数超过阈值会自动暂停轮询 10 分钟,防止触发封禁。
隐私与安全策略
虽然系统只需要很少的个人信息,但我仍然遵循“最小化收集”和“默认加密”的原则。
- 最小化收集:只保存 Case Number、部分护照号/姓氏、邮箱;所有敏感字段仅用于 CEAC 查询,邮件会掩码显示。
- 静态加密:数据库位于服务器本地磁盘,外部无法访问;备份文件使用对称加密后才上传对象存储。
- 传输安全:全站 HTTPS;内部调用(如 Ollama)走本地 loopback,不经过公网。
- 权限控制:后台无多用户登录,只在本地通过密码控制;密码哈希存储(SHA-256)。
测试与质量保障
虽然项目规模不大,但我还是保持了最基本的测试节奏。关键的解析、掩码、缓存函数用 Jest 写了简单的单元测试;test_nvc.js 和 test_nvc_ollama.js 可以在本地或 CI 中模拟一个完整的订阅流程,包括验证码识别、状态写入、邮件发送。每次发版前我都会手动跑一次“冒烟测试”——选一个真实 case,依次执行 Lookup、Subscribe、Unsubscribe,并核对日志与邮件内容是否符合预期。所有 CEAC 错误和验证码失败都会被记录原始上下文,必要时可以在本地复现。
个人成长与心态转折
- 焦虑 → 行动:从“被动刷新”转向“主动记录”,每天至少投入 1 小时改善产品,即便进度再慢也能看到量化成果。
- 分享 → 共鸣:将工具开放给朋友后,收到很多感谢邮件,也收到新的需求(如 USCIS 状态提醒),让我更坚定要把它做成长期项目。
- 应对不确定性:这段经历让我意识到,只要能把复杂问题拆成可执行的任务,就能把无力感转化为行动力。
当你无法决定流程快慢时,至少可以做点事,让等待变得可量化、可回顾。
Roadmap:打造多场景的签证状态自动提醒
| 时间段 | Feature | 说明 |
|---|---|---|
| 近期 | USCIS I‑130 / I‑140 状态轮询 | 对接 USCIS 官方 Case Status API,支持 Receipt Number,状态变更时推送。 |
| 中期 | 多国家签证节点 | 征集英国、加拿大、申根、澳洲等匿名 Case 数据,逐一实现自动查询。 |
| 中期 | 多语言 & 多时区 | 自动识别用户所在地,选择合适的发送时间与语言(中/英)。 |
| 中期 | 通知升级 | 引入“状态超时预警”“面谈倒计时”“准备材料清单”功能。 |
| 长期 | 开放插件 / API | 如果社区有需求,考虑提供受控的查询接口或插件式部署方式。 |
若你正在等待其他国家的签证,欢迎分享匿名 Case(仅需 Case Number/Receipt,不包含个人身份信息),帮助我更快打通新场景。
号召 & 联系方式
- 产品体验入口:https://eazyctl.com
- 反馈 / 合作 / 提供 case 信息:
- Email:
chewenkaich@gmail.com
- Email:
- 如果这份工具或文章对你有帮助,欢迎分享给仍在等待签证的朋友,一起用技术对抗漫长的无力感。
“When you can’t speed up the process, at least build something that makes the waiting feel under control.”