一个能实时对话的原型,距离可以放到公网给人使用,中间还隔着准入、配额、密钥、隐私、部署和真实链路验证。这次我把用 DeepSeek Harness 开发的 Voice 整合进 Fobo,并把它发布到了独立测试站。

福宝实时英语对话测试站:邀请码换取设备通行证

上线地址:https://voice.denghy.cn/talk

目前站点采用邀请测试,不公开发布邀请码。

这次上线不是把一个页面复制到另一个项目

Fobo 原来是一个小而简单的英语角:五个主题、六个快捷句型,一次请求得到一次文字和语音回复。它使用 Laravel、Inertia、Vue 3、SQLite 和 Vite,重点是让孩子能快速完成“听见、开口、得到反馈”的练习闭环。

原 Fobo 英语角:首页、主题与快捷句型继续保留

Voice 则已经走到了另一条路:它有实时 WebSocket 对话、字幕、打断、场景、会话、配额、录音和管理端。两边技术栈相近,但产品边界、状态模型和运行依赖并不相同。

实时语音测试页提供六个教材场景,当前选中 U3 寻找玩具

实时入口不是一个空白聊天框。当前测试版提供五感魔法秀、家庭聚会、寻找玩具、我家附近、农场之旅和中秋节六个场景,每个场景带有词汇、句型和难度提示,孩子点击卡片即可开始。

所以第一步不是合并文件,而是 Review。

Review 时分别跑通了两个项目的既有基线:Fobo 有 10 个后端测试、44 个断言;Voice 有 16 个后端测试、76 个断言,以及 30 个前端测试、类型检查和生产构建。

更重要的是,Review 找出了六个不能直接带进生产的问题:

  1. 本地 Relay 依赖 Valet 证书、固定端口和本机环境文件,不能原样部署;
  2. 管理端沿用了不适合公网的简单认证和默认账号;
  3. 配额只在会话结束后累计,并发打开多个会话可以绕过限制;
  4. Token 与费用由浏览器上报,服务端不能把它当成可信事实;
  5. WebSocket 缺少 Origin、连接数、帧大小、队列和硬超时约束;
  6. Voice 的狐狸形象与 Fobo 小熊猫品牌不一致,AudioWorklet 也改变了旧 iPad 的兼容边界。

原 Voice 数据库中已有的访客、会话、字幕和约 86MB 录音全部留在原项目,没有进入 Fobo 代码库或生产站。

最终架构:同一个域名,三条清晰边界

最初评估过把前端放到托管 Site、再把 WebSocket Relay 放到个人服务器。进一步比较运行时、Cookie、跨域和维护成本后,最终选择个人服务器全栈、同域分流:

https://voice.denghy.cn
        │
        ▼
      Nginx
        ├─ /ws      → Node Relay → Qwen Realtime
        ├─ /build   → Vue 静态资源
        └─ 其他请求 → Laravel / Inertia

这个决定减少了跨域和身份传递的复杂度。页面、API 和 WebSocket 都在同一个站点下,Nginx 统一处理 HTTPS,Laravel 管准入和会话,Node Relay 只负责实时转发和供应商用量回传。

浏览器永远拿不到 Qwen 的长期 API Key。Relay 只监听服务器回环地址,公网只能通过 Nginx 的 /ws 进入。

不注册账号,也不能让费用失控

孩子端如果先要求注册、登录、验证邮箱,使用门槛会过高;但完全匿名公开实时语音,又很容易产生不可控费用。

最后采用了四层准入:

邀请码
  ↓ 首次兑换
设备通行证
  ↓ 开始会话
服务端原子预占配额
  ↓
60 秒单次 Relay 票据
  ↓
Qwen 实时会话

邀请码只保存 HMAC 摘要,不保存明文。兑换后,浏览器得到 Secure、HttpOnly、SameSite=Lax 的设备通行证,不需要反复输入邀请码,前端脚本也读不到通行证内容。

开始语音前,Laravel 在事务里同时检查并预占单次、每日、总时长和并发额度。会话失败或没有真正连上 Relay,预占额度会释放;会话正常结束,则按服务端时间结算。系统还有全站每日时长和月度预算熔断,单个邀请码和整个站点都不能无限产生费用。

通过配额门槛后,Laravel 才签发大约 60 秒有效、只能使用一次的 Relay 票据。Relay 校验签名、有效期、Origin、会话和 nonce;重连必须重新申请票据。供应商返回的累计 Token 用量由 Relay 回传,浏览器提交的用量字段不再被信任。

第一版默认关闭录音。前端不会上传音频片段,生产日志也不记录完整票据、音频或 API Key。

发布过程:每一步都设置停止条件

整合开始前,Fobo 先建立可回退的 Git 基线,Voice 来源固定在干净提交 c3553e0。迁入的是实时语音能力和必要的测试,不是原项目的环境文件、数据库、访客数据或录音。

本地实现完成后,发布门包含:

  • 24 个 PHP 测试、141 个断言通过;
  • 30 个前端单元测试通过;
  • TypeScript 检查和 Vite 生产构建通过;
  • Relay 的模拟上游转发和非法 Origin 拒绝通过;
  • 全新 SQLite 完成迁移、全量回滚、再次迁移和场景播种;
  • Git 树中确认没有真实 Qwen Key 和录音。

服务器阶段先做只读盘点。盘点发现,最初识别到的主机并不是 denghy.cn 所在服务器;正确主机又没有宿主机 PHP 和 Node,并且内存有限。于是没有临时安装全局运行时,而是复用经过验证的镜像,把应用拆成两个独立的 Docker 和 systemd 服务:Laravel 只监听 127.0.0.1:9086,Relay 只监听 127.0.0.1:9333

部署使用经过校验的 Git Bundle 和前端制品。上传前后核对 SHA-256,生产 checkout 只做快进更新,数据库、环境文件和服务配置先备份再变更。

TLS 也没有一步到位地打开最终配置。先启用只响应 ACME challenge 的 HTTP 引导站点,确认 DNS 已指向正确服务器、挑战文件返回 200,同时让业务根路径在证书签发前保持 503。证书签发成功后,才切换最终 HTTPS 配置,并补上证书续期后的 Nginx reload 钩子。

生产最终提交为 ab055fc

真正的验收不是页面返回 200

HTTP 200 只能证明页面能打开,不能证明实时语音能用。

这次生产验收继续向上游走了一段完整链路:

  • 未持设备通行证访问语音 API,返回 401;
  • Relay 健康检查成功,且只监听回环地址;
  • 使用真实 Qwen 上游收到 session.created
  • 同一个 Relay 票据再次使用,返回 403;
  • 60 秒测试会话被服务端硬上限自动结束,并准确结算 60 秒;
  • HTTP 正确跳转 HTTPS,证书和自动续期正常;
  • /ws 关闭访问日志,Laravel、Relay 和 Nginx 日志未发现密钥或票据泄露。

U3 寻找玩具真实运行状态:连续字幕、AI 追问、监听状态和剩余额度

这张图来自一次真实运行中的 U3“寻找玩具”练习。页面连续显示孩子的英文字幕和 AI 老师的追问,顶部同时保留监听状态、会话计时和当日剩余额度。画面中仍出现“还没听到你的声音”提示,因此它不是一张刻意整理过的无异常宣传图;这说明实时链路正在工作,也如实保留了当时的麦克风拾音提示。

U3 寻找玩具练习结果:1 分 48 秒、5 次开口、10 句对话和目标词汇

同一次练习结束后,结果页给出四星评价,并回读了 1 分 48 秒练习时长、5 次开口、10 句对话、目标词汇和当日剩余 3 分 12 秒。它把“页面能对话”继续推进到“会话能结束、指标能结算、结果能展示”的完整闭环。

另一次 U3 寻找玩具真实对话:字幕、AI 回复与重连失败提示

另一轮真实练习在会话后段发生了重连失败,截图同样没有把错误裁掉:界面明确提示结束本次练习后重试,也没有在断线后继续伪装成正在收音。

这张异常现场与前面的正常对话、结果页相互独立。前两张证明实时字幕、AI 回复和结算闭环已经工作,这张则证明异常状态能够被用户看见;连接为什么中断,仍需要结合 Relay 和浏览器日志独立诊断。

浏览器验收还发现了一个自动测试没有抓到的问题:实时页面左上角仍残留狐狸头像。修复后再次发布和回读,Fobo 的小熊猫品牌才真正统一。

这件小事很有代表性。代码测试能证明状态机、配额和安全边界,浏览器截图能发现品牌和视觉问题,真实上游会话才能证明 WebSocket 与供应商协议。三类证据不能互相替代。

这次上线留下的四个结论

第一,原型 Review 的价值不是评价代码好坏,而是找出哪些隐含假设只在开发机成立。

第二,实时语音的安全边界应该围绕短时票据和服务端配额建立,而不是围绕隐藏前端参数建立。

第三,隐私和成本控制要进入架构默认值。默认不录音、原子预占、硬超时和预算熔断,比上线后再补开关可靠得多。

第四,完成必须由证据定义。测试、构建、迁移闭环、TLS、真实上游事件、票据重放、结算回读和日志审计一起通过,才算真正上线。

现在,原 Fobo 英语角仍然保留在首页,实时英语对话作为独立入口运行在 /talk。它还处在邀请测试阶段,但已经从“能在开发机说话”的原型,变成了一套可以控制访问、费用、密钥、隐私和故障边界的生产系统。


测试站:https://voice.denghy.cn/talk

资料核对时间:2026-08-27。文中的测试数量、生产提交、TLS、Qwen 事件、票据重放和会话结算均来自本次发布后的实际回读。