版本:v2.2(2026-07-12) 本文合并原
01-requirements.md/02-task-breakdown.md/03-gap-analysis-vs-poc.md, 是当前唯一的计划文档。本文主线按云端 realtime 路线组织;本地三段式(VAD→STT→LLM→TTS) 已作为第四 providerlocal落地(2026-07-10,见文末附录 A),可与云端 provider 切换。 codex 评审全过程留存于.codex_dialog.md;旧版三份文档留存于 git 历史(commit 08754fc)。状态更新(v0.2,2026-07-08):FR-7/9/10/11(桌面 App、批量外呼、通话记录/录音、 通话后摘要)、首启向导、DMG 安装包、P0/P1/P2 主线与 §3 多数 P3 打磨项已回填状态; provider 由 qwen/doubao 扩展为 qwen(默认)/ doubao / openai。
状态更新(0.6.0,2026-07-12):hosted 云控制面(Cloudflare Worker + D1 + Durable Objects + LiveKit,见 #42/#43)已随 0.6.0 签名公证 DMG 发 GitHub Release, 普通用户用配对码即可让手机远程通过模组拨号、无需自建 tunnel。Android 远程拨号 App 的源码 / hosted
/v1协议 / 真机验收已落地(#36/#44),正式签名分发(AAB)待 0.7。 hosted onboarding 指向*-beta.bondings.ai,生产 DNS 切换待异网混沌验收后单独进行。
| 版本 | 目标 | 面向 | 状态 |
|---|---|---|---|
| v0.1 Developer Preview | 源码开源,开发者按 README 手动跑通,收集同型号 EC20 硬件反馈 | 会装 Python/填 Key/跑命令的开发者 | ✅ 已落地(Apache-2.0、去个人化、双语 README、风险声明、仓库清理) |
| v0.2 Mac Beta | 有 .app,仍需装依赖或跑安装脚本 |
技术用户 | ✅ 已落地 |
| v0.3 One-click Mac Beta | .dmg 安装、首次启动向导、内置 runtime、自动 launchd、Developer ID 签名+公证 |
普通用户 | ✅ 已落地(v0.4.0 起 DMG 已签名+公证+staple) |
| v0.6.0 Hosted 桌面 | hosted 云控制面(Cloudflare Worker + D1 + Durable Objects + LiveKit)签名公证 DMG,配对码远程拨号、无需自建 tunnel;Android 远程拨号 App 源码/协议/真机验收已落地、正式分发待 0.7 | 普通用户(桌面 DMG) | ✅ 已发布(2026-07-12;#42/#43 云控制面、#36/#44 Android v0 + hosted /v1) |
| v1.0 | 稳定硬件矩阵、隐私说明、故障恢复、完整文档 | 通用 | 计划 |
v0.1 原则(codex 建议,已采纳):不追求一键安装,追求"陌生开发者 30 分钟能跑通、能提 issue、能贡献";先让 3-5 个同硬件开发者复现成功,再进大众分发。
v0.3 独立 App 状态:packaging/build_installer.sh 已能产出内置 Python runtime、
ffmpeg/libusb、自动 launchd 与首启向导的 CallPilot.dmg;固定安装路径通过 /Applications
推荐路径与 per-user Application Support 数据目录处理。Developer ID 签名与 notarization
已随 v0.4.0 落地;仍缺硬件兼容矩阵完善。
AgentCall:插上 Quectel EC20 4G 模组,来电自动接听并由 AI 对话,支持外呼、短信收发、 AI 工具调用(发短信/挂断/查验证码)。AI 定位为"机主的数字分身"——替接来电、代打外呼。
当前技术栈(realtime):
手机来电 → EC20 模组 ──(AT: RING/ATA/CLCC)── Eg25Modem
│ │ 回调
8kHz PCM CallAgentService / CallSession(单例,双向互斥)
│ │
FfmpegAudioBridge(UAC, macOS) ── VoiceAgent(qwen | doubao | openai)
│ 云端端到端语音 realtime
EventHub → aiohttp 网页仪表盘
- AI 大脑:Qwen Omni / Doubao / OpenAI 云端 realtime(端到端语音,
server_vad云端切句), 不加载本地模型,无预热问题。 - 音频:macOS 上经
uac_ffmpeg模式(ffmpeg 绕过打不开的 PortAudio);nmea/uac(PortAudio) 在本机不可用(见 §6 决策)。 - 单一
CallAgentService+单CallSession处理来电与外呼,_active互斥(同时仅一通)。
已完成(M0 底座):AA 代码迁入并重组为 src/agentcall/ 包 · USB→PTY 桥生命周期管理
(进程锁/重插重连/日志)· 完整离线单测 + Fake 夹具 · 数字分身 prompt 层 · macOS UAC ffmpeg 音频桥 ·
真机来电已能双向通话(qwen)。T0.4 真机连续通话验收待补最后 2-3 通。
| 编号 | 需求 | 状态 |
|---|---|---|
| FR-1 | 来电自动接听(RING/CLCC→ATA,去重) | ✅ 已实现,真机通过 |
| FR-2 | AI 对话(qwen/doubao/openai 可切换,数字分身人设) | ✅ qwen 真机通;openai 已接入;doubao 待补 |
| FR-3 | 外呼(网页/CLI 发起,45s 接通超时,外呼专属 prompt) | ✅ 已实现 |
| FR-4 | 短信收发(UCS2 中文、PDU 解码、+CMTI 上报) | ✅ 迁移自 AA |
| FR-5 | AI 工具调用(发短信/挂断/查验证码,qwen 原生 function calling) | ✅ 已实现 |
| FR-6 | 网页仪表盘(状态/转写/短信/发短信/外呼) | ✅ 已实现 |
| FR-7 | 交付为桌面 App(非浏览器网页) | ✅ 已实现(desktop_app.py + 托盘 tray_app.py + 独立安装包 packaging/build_installer.sh;签名公证已落地) |
| FR-9 | 批量外呼(一次多号码顺序拨打 + 白名单) | ✅ 已实现(dial_queue.py + /api/call/batch_dial) |
| FR-10 | 通话记录 / 录音 / 拨打历史 | ✅ 已实现(call_log.py + /api/history) |
| FR-11 | 通话后结构化总结(谁来过/何事/急否/是否回拨) | ✅ 已实现(summarizer.py,CallSession 调度) |
短信转发到邮箱:应用户要求延后到未来,见附录 B。
-
NFR-1 稳定性:任一通话异常不得使进程崩溃;通话结束必执行 ATH + 关 PCM 通道; USB 桥与 modem 连接支持断连自愈(见 3.1)。
-
NFR-2 非阻塞:音频主循环(10ms tick)内无 >50ms 同步调用。
-
NFR-3 安全:工具调用有 policy——发短信白名单+频控、挂断需显式意图+延迟窗、 查验证码可配开关;工具调用留审计日志;短信内容脱敏可配。
✅ 实现状态(2026-07-08):发短信目标限制已改为「只能回复已联系过号码/当前通话对端」 (
contacts.py,Web 与 AI 工具共用策略);AI 工具与 Web/api/sms/send共用进程内滑动窗口频控SMS_RATE_LIMIT_PER_HOUR(0=不限);TOOL_QUERY_CODE_ENABLED可关闭验证码工具注册;send_sms/hangup_call/query_verification_code已补tool_call审计且不落短信全文/验证码值。 挂断工具延迟窗与send_dtmf审计也已落地。 -
NFR-4 隐私:录音/转写默认本地存储、可一键清除;
.env与各类凭证不入 git。✅ 实现状态:本地存储 ✅、
.env不入 git ✅、RECORDING_RETENTION_DAYS自动过期清理 ✅; 历史页与 REST API 已支持单条删除及一键清除全部,进行中的通话记录会跳过。 -
NFR-5 可移植:平台相关代码(USB 桥、音频后端)隔离在明确边界;不新增 Mac 专属强依赖。
-
NFR-6 可测试:Fake modem/bridge/agent 夹具支撑无硬件单测。
-
NFR-7 生产数字实测:延迟/识别率等以真机实测为准,不引用估计值下结论。
P0-0 下行到电话端可闻 —— ✅ 已验证通过(2026-07-07)
- 真人拨打 AI 号码,
uac_ffmpeg模式下电话端清晰听到 AI、音质佳。下行链路成立。 - 结合此前上行已证实(拨 10000 IVR 被准确转写),双向语音在 uac_ffmpeg 模式下均通。
- 结论:
uac_ffmpeg为确定可用的音频路径(双向);nmea 崩 USB,不用。
P0-1 modem 连接断连自愈(✅ 已完成,commit 3fab59d)
- USB 桥重插后
/tmp/ec20-at指向新/dev/ttysNNN、app 旧 fd 变死句柄;已实现读循环/发送 失败触发重连(指数退避、多线程串行化、初始化期防死锁),真机验证桥重连后自愈。
P0-2 通话记录 / 录音 / 延迟打点 / 拨打历史 —— ✅ 已完成
- 现状:
EventHub只持久化短信(_PERSISTED_TYPES),通话事件仅在内存 deque,刷新即丢。 - 要做:按通话落
data/recordings/<ts>/:events.jsonl(含各环节延迟打点)+ 可选录音 wav; 网页加拨打历史列表;录音开关与保留期可配(隐私见 NFR-4)。 - 价值:排障/审计/复盘 + 为 §4 Spike 攒录音样本。
- 注:短信历史当前已具备(messages.json + 网页),本条只补通话侧。
- 验收:一通电话后产物齐全可回放;关闭开关后不落音频。
P1-1 打包为桌面 App —— ✅ 已完成(FR-7)
- 现状:
app.py起 aiohttp 后webbrowser.open,用户看到浏览器标签页。 - 要做:pywebview(或等价)把现有 aiohttp 网页套进原生窗口——后端全复用,成本低; 后续可 py2app/PyInstaller 打成可双击 .app。
- 验收:双击启动出现独立应用窗口,非浏览器;桥+服务随之拉起。
P1-2 短信转发到邮箱:应用户要求延后,移至附录 B。
P1-3 常驻 / 开机自启 / 崩溃重启 —— ✅ 已完成
- 要做:macOS launchd plist(RunAtLoad/KeepAlive/崩溃重启)+ Linux systemd unit 预留; 桥与 app 两个单元。
- 验收:重启机器后自动待命接电话;kill 掉 app 后自动重启。
P1-4 通话后结构化总结 —— ✅ 已完成(FR-11)
- 现状:通话结束只发
ended事件,无总结。数字分身 prompt 已定位"记要点转告机主",但无落地。 - 要做:通话结束用一次 LLM 请求(复用 realtime 转写全文)生成结构化摘要(对方/事由/紧急度/ 是否需回拨),落 EventHub + 网页展示。与 P0-2 配套。
- 验收:一通来电结束后,网页/记录里出现该通结构化摘要。
P2-1 批量外呼 —— ✅ 已完成(FR-9)
- 现状:
service.dial()仅单号,CallSession单例互斥拒第二通。 - 要做:外呼队列(号码列表 + 每号可带独立 task)+ 顺序调度(一通结束再拨下一通);
外呼白名单(移植 poc
auto_dial_number_is_allowed)+ 频控。与 P1-4 配套产出"每通结果"。 - 验收:填入多号码,逐个拨打并各自记录结果;白名单外号码被拒。
P2-2 配置面板 —— ✅ 已完成
- 要做:网页加配置读写 API 与页面(provider、录音/工具/转发开关、半双工参数等白名单字段), 写回 .env 保留注释;改 provider 提示重启。
- 验收:面板改一项→.env 变更正确→重启生效。
P2-3 realtime 连接预热 / 降低首句延迟 —— ✅ 已完成
- 现状:realtime WS 在通话中才建立,接听→首句约 2-3s。
- 要做:服务空闲时预建/保活连接或至少预热 DNS/TLS,来电时复用(评估空连接成本/超时)。
- 验收:接听→首句延迟实测下降(打点对比)。
P2-4 通话中断 fallback / 重连 —— ✅ 已完成
- 现状:realtime WS 中途掉线则整通结束,无兜底。
- 要做:断线检测 + 有限次重连;重连间隙播预置"稍等"提示音。
- 验收:通话中模拟断网,能恢复或优雅提示,不是突然沉默。
P2-5 provider 凭证启动校验补全 —— ✅ 已完成
- 现状:
app.py只校验 qwen 的DASHSCOPE_API_KEY。 - 要做:按所选 provider 完整校验凭证,缺失时启动即报清晰错误。
P2-6 硬编码参数化 —— ✅ 已完成
HALF_DUPLEX_HANGOVER_SECONDS=0.5、工具挂断延迟Timer(4.5)→ 提为可配; 半双工挂尾值按 §4 S2 真机回声结论标定。
P2-7 AI 下行音频镜像到 Mac 扬声器(本机监听)—— ✅ 已完成
- poc 有:
O2_MONITOR_AI_PLAYBACK("在电脑上听 AI 说话")+O2_HUMAN_OUTPUT(输出设备)MONITOR_AI_GAIN;AgentCall 当前只把 AI 音频写给 modem bridge,无本机监听。
- 要做:
MONITOR_AI_PLAYBACK=true/false+MONITOR_OUTPUT_DEVICE;用后台队列/线程把下行 PCM 同时播到 Mac 指定输出设备,绝不阻塞通话主循环(沿用 P0-2 的非阻塞原则)。 - 双重价值:① 运维时人能实时听到 AI 在说什么;② 诊断 P0-0 的关键工具——把"AI 有没有 出声"(Mac 扬声器能听到)与"电话端有没有收到"(对端能听到)分离,快速锁定下行故障在 TTS 侧还是 EC20 侧。
- 验收:开启后 Mac 扬声器与电话下行同步听到 AI;关闭则静默;通话主循环 tick 不受影响。
- P3-1 DTMF 按键 —— 🚧 信号层已实现、真机端到端待验证:带内合成(默认
DTMF_MODE=inband)/ EC20AT+QVTS回退AT+VTS(打客服菜单导航);真机菜单推进有效性见 #73/#80,待 A/B 实测收口。 - P3-2 barge-in(打断):realtime 原生支持,当前被半双工压制;依赖 §4 S2 回声结论决定放开策略。
- P3-3 音频治理:上行增益/AGC(当前只有
MODEM_TX_GAIN下行)、削波检测、静音填充参数化。 - P3-4 通话中短信播报:通话进行中收到短信时告知正在对话的 AI。
- P3-5 doubao 能力对齐:接数字分身差异化 prompt;工具调用(若官方支持);
实现
say()外呼开场白——当前未能从现有代码/依赖确认豆包 realtime 协议中 与 qwencreate_response等价的文本指令注入消息格式,say()暂为 no-op, 豆包 provider 外呼 AI 不会主动开口(factory 创建时有 warning 提示), 外呼请用 qwen;需查阅火山引擎官方协议文档确认消息格式后补齐。 - P3-6 前端 XSS 加固 —— ✅ 已完成:index.html 已移除 HTML 注入 API,用户可控内容走 DOM 节点+textContent。
- P3-7 launchd plist 模板化 —— ✅ 已完成:install.sh/打包 App 安装时按当前路径生成 launchd 配置,去除硬编码。
- P3-8 web 挂断按钮/API —— ✅ 已完成:已加 POST /api/call/hangup → session.stop()。
- P3-9 外呼收束 —— ✅ 已改为 LLM 收尾裁判(
summarizer.judge_wrap_up,每 ~15s 让 qwen-plus 看转写+目标判「继续/收尾」):治「打转」(原地重复/菜单绕圈→提前收尾)与「太早撤」(对方正在查询→继续等),非关键词枚举;150s 硬时限保留兜底。原「在 prompt 里强化收束」思路已弃(模型不可靠、且属枚举式微调)。 - P3-10 动态场景提示词生成 —— ✅ 已完成(2026-07-09):按(号码+任务+语言)用轻量文本模型(跟随
AGENT_PROVIDER:qwen→qwen-plus、openai→gpt-4o-mini,PROMPT_GEN_MODEL可覆盖)为每通生成一段「场景与开场策略」+ 一句 opening,替代单一通用模板,延续「讲场景不列规则」思路。四条硬约束全部落地:- 不变量固定、不交给生成:立场(机主名下的事、对方是协助方)、安全边界、可用工具、"办完即收尾" 保持模板原文;只生成"这通什么场景、要不要自我介绍、第一句说什么"那段。代码内无任何号码→机构类型映射表,类型判断全交给模型。
- 模板永远作默认/兜底:生成失败/超时/关闭/凭证无效 →
build_instructions行为与改动前逐字节一致(回归测试保证)。 - 延迟藏进拨号窗口:拨号即后台线程生成、接通时限时取用(
PROMPT_GEN_WAIT_SECONDS默认 3s),(号码+任务+语言) 进程内 64 条 LRU 缓存;生成线程异常绝不冒泡到通话主流程。 - 可调试:每通生成的 scenario/opening 记入
events.jsonl的prompt_gen事件 + 日志摘要。
- 关键根因修复:realtime
create_response(instructions=)是叠加在 session 指令上、非替换;原模板 outbound 段硬编码"开头自我介绍",压过动态 opening。改为:scenario 存在时该句条件化为"按场景策略决定要不要自我介绍",scenario 为空保持原句。真机拨 10000 验证:开场第一句变为"正在帮您查询流量使用情况",不再"我是X的数字分身"循环、不再主动报时间戳。 - 隐私 policy(显式接受):任务文本会发给轻量模型、scenario/opening 落
events.jsonl明文——与整通语音本就全程发给同厂商 realtime 模型、逐字稿本就落同一录音文件属同一数据流,不新增泄露面;events.jsonl为本地录音目录文件,不提交、不外传。
- P3-14 语音识别型 IVR 长菜单跑偏(观察项,2026-07-09 真机发现):对"持续播报长选项列表"的语音识别 IVR(如 10000 念"查话费/查套餐/故障报修等,您请说"),realtime 模型会把菜单播报当成对方在提问、顺着每个选项应答("帮您反馈故障报修"),偏离原任务;并伴随立场漂移(说成"帮您查"而非"帮机主查")。与 P3-12 同源(机器 IVR 的轮次兜底行为),提示词层难根治;候选同 P3-12 方案②(手动应答控制)或识别"对方仍在念菜单"时抑制应答。当前不阻塞,先记录。(2026-07-09 稳定性采集补充:8 轮拨 10000 有 1 轮出现完整请求句原文 ×3("您好,麻烦帮我查一下这个号码的流量使用情况…"),即语音识别 IVR 反复催话下的真实复读,发生率 1/8;礼貌短语"您好"多次出现属正常应答已从回归断言中豁免(≤4 字)。)
- P3-15 开场偶发幻觉编造结果(观察项,2026-07-09 真机发现,偶发未稳定复现):一通拨 10000 开场白 AI 直接编造了完整查询结果("当前套餐剩余可用流量为 45GB,本月已使用 12.3GB"),此时对方(IVR)尚未回答任何内容。同批次另两通开场正常、未编造,属偶发。推测与 create_response 的 opening/scenario instructions 下模型放飞脑补有关。对电话助手是可信度隐患(不得编造未从对方获得的信息)。待复现确认后从提示词层强化"未从对方拿到的数据不得编造",或收敛开场 instructions 的自由度。当前偶发、不阻塞。(2026-07-09 下午更新:编造在 qwen「现编提示词」时高发、预置提示词时未见——见 P3-16。已从 common 提示词层加固"不得编造未获得的结果",B2-4 预调教任务库进一步治本。) (2026-07-09 稳定性采集定性:regression_call.py 连拨 10000×8 轮(预设命中),断言"不编造数值"8/8 PASS——加固+预设组合下幻觉 0 复现。转为已缓解,常规回归脚本持续看护,不再单独跟踪。)
- P3-16 预调教任务库 —— ✅ 已完成(B2-4,2026-07-09):对固定 (号码+任务) 用用户预置的精调提示词,替代每次现场生成(P3-10 动态生成)。
number_profiles.py分层匹配 (号码+任务) 精确 > (号码) 通配 > 动态生成兜底;命中跳过轻量模型调用、直接注入 scenario/opening、events 标 source=profile;预设库独立于PROMPT_GEN_ENABLED(可只用预设、关掉现编)。用户可编辑data/number_profiles.json(gitignore),仓库仅占位示例,非枚举/不写死。- 关键实测结论(provider 对比 + 立场问题定性):qwen-omni 在开场立场上不稳——「现编提示词」时间歇性冒充客服("是查流量对吧?我帮您操作给您结果")或冒充机主本人;OpenAI(gpt-realtime-mini) 立场也小漂("我是田野"),但明显优势是会真的调用 send_dtmf 按键(qwen 十几通只说"我按X"从不调工具)。换模型治标、预设库治本:同一 qwen 换成 (10000,咨询流量) 预置提示词后,开场"你好,我想查一下这个号码的流量使用情况"立场稳定、不冒充、对方未给数据时诚实追问而非编造。说明立场漂移主因是「现编不稳」而非模型能力。默认仍 qwen(成本),常打号码靠预设库稳住。
- B2-5 拨号界面下拉:从预调教库选预设(选中即精确命中,避免手输"查信用卡账单"vs"查询信用卡账单"对不齐漏配),保留"自定义"手输走动态兜底;
GET /api/number_profiles供下拉渲染。 - B2-6 完整双语:label/task/scenario/opening 各字段可为字符串或 {zh,en};scenario/opening 跟通话语言(AGENT_LANGUAGE)、label/task 跟界面语言、缺失回退;task 双语匹配(中/英输入都命中同一预设)。本地已配 10 个真实公开热线(运营商 10000/10086/10010、银行 95588/95533/95555/95566、12315、12345)双语预设。独立 review APPROVE、零缺陷;补畸形输入不抛异常回归测试。
- B2-8 任务库管理 —— ✅ 已完成(#8,2026-07-10):新增中英双语「任务库」页面与本地 JSON CRUD API,支持新建/编辑/复制/启停/删除、精确任务与号码通配模式;稳定 profile id 取代易漂移的任务字符串作为拨号选择身份,旧
preset_task与无 id JSON 保持兼容。写入经进程内写锁、字段/冲突校验、fsync+ 原子替换,管理内容仍只落用户数据目录,不引入数据库或新依赖。summarizer.judge_wrap_up收尾裁判把关,要求模型在实质目标未达成前拉回话题/追问结果; 保持非枚举实现,不列客套话词表。
- P3-12 抑制对机器 IVR 的重复应答(结构性,非提示词/非枚举):真机现象——对方是"一直播报"的自动语音时,server VAD 把它切成很多轮,实时模型每轮都被迫回话、没新内容就复读开场白("您好我是X想咨询…"连说 5-6 遍)。提示词层面压不住(属模型对机器 IVR 的轮次兜底行为,非措辞问题)。候选结构性方案:
- 播出前抑制与上一句近乎相同的应答 —— ✅ 方案①已完成(用文本相似度判重,非关键词表;当前作为轻量缓解)。
- 改用手动应答控制 —— ✅ 已完成(默认关,2026-07-09):真机试探确认 qwen-omni 认 OpenAI-style
turn_detection.create_response=false——server VAD 照常断句+ASR,但不自动生成回复,于是走半自动(不必自己做端点检测):MANUAL_RESPONSE_CONTROL=true时注入该字段接管"何时开口",靠静默合并 debounce(纯时序、不看内容、非枚举)——对方一段转写后启动MANUAL_RESPONSE_SILENCE_MS(默认 1000) 定时器,窗口内又来转写则重置(连续播报合并成一段),静默到期才手动create_response();MANUAL_RESPONSE_MAX_WAIT_MS(默认 8000) 防饿死。in_flight由 wire 事件(response.created/done 计数)驱动,AI 回复中不抢答、done 归零补触发,watchdog 防 done 丢失导致永久沉默。真机拨 10000 验证:4000ms 窗口下 IVR 连播 5 段合并成 1 次应答;默认关行为逐字节不变。静默窗口默认值待真机对话节奏精细标定(太短 IVR 间隙抢话、太长真人对话变慢),当前默认关、需用户按体验决定是否默认开及最终窗口值。 - 调 VAD 转向阈值减少对连续播报的过度切分——副作用:拖慢真人对话响应。
- ②已作为长期正解落地(默认关);①作为默认轻量缓解并存。
- P3-13 核实 send_dtmf 真机端到端有效(仍未定论,2026-07-09 凌晨补充证据):
- 证据 A(07-08 20:03 通话):模型确实调用了
send_dtmf(QVTS 返回 OK),但数字菜单照样循环、IVR 最终报"未检测到您的操作"——倾向 QVTS 的音没到对端。 - 证据 B(07-09 02:29 通话):模型只是嘴上说"我按2/按9"、未调用工具(另一问题:模型叙述代替工具调用)。
- 证据 C(07-09 02:32 手动 API 隔离测试):无定论——该通 IVR 处于语音识别模式,无按键场景。
- 新假设(待验证):UAC 模式下上行音频由宿主机喂给模组,
AT+QVTS注入的可能是模组自身音频路径、与 UAC 上行不合流 → 对端听不到按键音。若成立,正解是带内 DTMF:在宿主侧把 DTMF 双音直接合成进上行 PCM(audio_bridge 加 tone 生成器),不依赖 QVTS。 - 复测方法:拨到数字菜单明确报"请按X"时手动 API 发键,观察菜单是否推进;或用带内合成对照测试。
- 进展(07-09 上午):带内 DTMF 已实现并合入(6b5ede4,DTMF_MODE=inband 默认),带内合成独立频谱验证正确、审计事件落 downlink 录音。但真机菜单推进仍未验证成,因为发现更前置的问题——模型今天所有通话说了 14 次"我按X"却 0 次调用 send_dtmf 工具(证据 B 现象常态化):根因是 prompts common 可用工具清单漏列 send_dtmf、引导过弱,已提示词加固(eb70253:补进工具清单 + 明确"必须调用工具真正发送、不是只在话里说按哪个键")。仍待数字按键菜单场景真机确认:① 加固后 AI 是否真调 send_dtmf;② 带内 DTMF 是否让菜单推进。本轮几通 10000 均被路由到语音识别型 IVR("请说XX"),无按键场景,且模型对语音菜单也会误说"我按X"(把语音菜单当按键,措辞混乱,与 P3-14 同源)。需拨一个已知纯数字按键 IVR 才能收口。
- 证据 A(07-08 20:03 通话):模型确实调用了
按优先级的完整清单见 docs/code-review-2026-07.md:P0 正确性 bug 5 项(豆包
provider 外呼静默失效、WS 推送偶发丢事件、hangup 无锁等)→ P1 架构优雅性 5 项
(CallSession 拆分、server.py 样板收敛、配置收口注册表等)→ P2 工程化护栏
(ruff/mypy、CI、.env.example 绑定测试已落地)→ P3 打磨 + 5 个高 ROI 测试缺口。
每项产出报告到 docs/spikes/(目录待建,尚未落地),为上面的需求提供实测依据:
- S1 音频质量:EC20 8k 采集/回放的语音清晰度与 realtime ASR 识别率;采样率/增益调优。
部分已有结论:本机 macOS 仅
uac_ffmpeg可用,NMEA over USB 崩溃、PortAudio UAC 打不开。 - S2 回声与半双工:AI 播放期间上行回采强度,标定半双工挂尾参数,判定 barge-in 可行路线 (裸 VAD / 能量门限 / AEC)。已观察到:真机来电出现"AI 重复自我介绍",疑回声/半双工截断。
- S3 端到端延迟:接听→首句、speech_end→回复各环节打点(realtime 首音频约 291ms 实测), 识别预热/连接优化空间(喂给 P2-3)。
- 测试补全:状态机/音频桥/工具/policy 单测覆盖核心路径;
pytest一键跑。 - 真机验收清单(
docs/acceptance.md,待补):来电自动接听×3、qwen 对话 5 轮×3 通、外呼×1、 中英文短信收发、AI 主动挂断、30 分钟长通话稳定、拔插恢复。
用真实可交互对端做端到端自测,Claude 可自己发起并从转写/事件判定结果:
-
外呼本卡运营商客服 IVR 验证双向语音链路(按 SIM 识别,当前移动卡=10086;电信 10000/联通 10010):客服 IVR 多为语音识别菜单,能与之多轮 对话(如说"查余额/查流量",IVR 按语义推进菜单)就证明——① 下行通(IVR 听到了 AI 说话)、 ② 上行通(AI 听到了 IVR)、③ AI 对话逻辑正常。判定依据:转写里
上行·用户(IVR 提示) 与下行·Agent(AI 应答) 形成连贯推进,且 IVR 响应与 AI 所说一致。 触发:curl -X POST http://127.0.0.1:47100/api/call/dial -d '{"number":"10086"}'(号码按本卡运营商,当前移动=10086,勿跨网拨他家客服)。 -
发短信"查余额"到本卡运营商客服号验证短信收发(当前移动=10086):发出后运营商会回一条余额短信 → 验证
send_sms(发) ++CMTI→on_sms→sms_in(收) + 中文 UCS2 编解码 全链路。 判定依据:EventHub 出现sms_out(status=sent) 且随后收到sms_in(余额内容)。 -
说明:这两条把大量"电话/短信行为"验证从"必须人工在环"降为"Claude 可自证",仅音质主观 好坏、真人对话自然度仍需人耳确认。
-
部署文档(
docs/deploy-macos.md,待补;当前安装见 README 与 docs/faq.md):从零到可接电话(Python/依赖/USB 桥/常驻); key 管理规范;Linux 差异预留。
- D1 以 AA 为基座:AA 的 modem/音频/会话编排真机已跑通,整体迁入重组,未反向改造 poc。
- D2 realtime 优先,三段式为可选 provider:主线走云端 realtime;本地三段式已作为第四 provider
local落地(附录 A),可切换。 - D3 音频模式(双向均已真机验证):
- 已证实:
uac(PortAudio/sounddevice) 在本机打不开 EC20 声卡(AUHAL -66740/-9986, coreaudiod 异常时更甚);nmea(USB 串口 PCM) 在本机会触发 USB 接口崩溃重枚举 (18:01/18:25 多次复现),无法持续通话。 - 已证实:
uac_ffmpeg(ffmpeg 绕过 PortAudio)稳定不崩,且双向语音均通——上行 (拨 10000 IVR 被 qwen 准确转写) 与下行(真人拨 AI 号码清晰听到 AI,2026-07-07 验证) 都通过。 - 现结论:
uac_ffmpeg为 macOS 上确定可用的默认路径;uac(PortAudio)/nmea保留为 诊断/备用(nmea 崩 USB,勿用)。Windows/Linux 原生串口另行验证。
- 已证实:
- D4 UAC 下行时钟:ffmpeg 播放侧按 100ms 实时节奏喂养、空闲补静音,避免 underrun 与输出流迟建(经 codex review)。
- D5 每通电话重启语音通道:挂断会
AT+QPCMV=0,每通接听前须重发initialize_for_voice, 否则第二通起双向无声。 - D6 barge-in 延后:依赖 S2 回声实测,v1 维持半双工抑制。
- D7 工具安全 policy(NFR-3):发短信目标限制+频控、挂断显式意图+延迟窗、查验证码可配、审计日志。 (实现状态见上 NFR-3 审计注:截至 2026-07-08 已落地)
- D8 EC20 USB 掉线风暴根因与缓解(2026-07-07 证据链分析):
- 根因:Mac 系统睡眠(本机
pmset sleep=1)→ 唤醒时 USB 总线重枚举 → EC20 bulk endpoint 进入 stall;旧版桥从不调dev.reset(),只能反复重开-秒挂,产生符号链接 删建风暴(实测峰值 177 次/分钟),且桥与 app 两层自愈互相打架。 - 已落地缓解(v0.1):① launchd plist 用
caffeinate -s包裹两个进程,阻止系统 睡眠(首要诱因);② 桥重连前先dev.reset()清 stall;③ 桥重试指数退避(1→30s), 连续快速失败(存活 <5s)达阈值(EC20_BRIDGE_FAIL_THRESHOLD,默认 6)即 exit(3) 交给 launchd 冷重启;④ 桥单实例文件锁,防双桥抢设备。 - 长期项(物理层:换供电/线材、USB 集线器隔离)挂 P3 观察。
- 新证据(2026-07-08 05:23):caffeinate 生效期间、通话进行中仍发生掉线
(语音 UAC 流 + AT 串口并发时段)——睡眠不是唯一诱因,物理层嫌疑加重
(通话期射频+音频功耗拉高)。建议优先试带独立供电的 USB hub。
连带发现会话僵尸 bug:串口断死后会话不收尾——已修复(CLCC 消失判定 ~4s
- 轮询失联 60s 兜底,commit 5c3ccc8),手动兜底仍保留(界面挂断按钮 / POST /api/call/hangup)。
- 韧性实测(2026-07-08 05:54):通话中杀掉 USB 桥进程,串口 2s 自愈重连, 通话不断、对话持续——语音走 UAC 声卡独立 USB 接口不经桥,AT 通道 短暂丢失可无感穿越。只有 USB 总线级 stall(两通道全死)才会断话。
- 根因:Mac 系统睡眠(本机
状态更新:已作为第四 provider
local落地(agents/local_agent.py),选型改为 sherpa-onnx 全家桶(VAD/STT/TTS,无 torch)+ dashscope 文本脑(默认 qwen-plus), 模型资产python -m agentcall.local_models下载。真机拨 10000 验收 8/8。 原设想的 FunASR torch 栈弃用(依赖数 GB 无法进 DMG)。以下为历史设计记录。
未来若要"本地可控、隐私优先、可离线"的 AI 大脑,再启用本地三段式方案:把
iphone-call-ai-poc 的 Silero VAD → FunASR → DeepSeek → sherpa-onnx 包装成 LocalPipelineAgent,
实现 VoiceAgent 接口作为第三 provider local,与 qwen/doubao 并存可切换。
关键约束(codex 已评审,勿忘):send_audio() 必须非阻塞(后台 worker 跑 STT→LLM→TTS,
utterance 合并语义)、TTS 需 PCM adapter、DeepSeek function calling 需从零实现、需模型资产
安装与预热。届时 STT 预热、模型安装、keepalive 等问题随之回归。
完整设计与任务拆解见 git 历史 docs/01-requirements.md / docs/02-task-breakdown.md
(commit 08754fc)与 .codex_dialog.md 的三轮评审记录。
-
+CMTI→on_sms→EventHub sms_in先维持原短信落盘/界面链路,再非阻塞入有界邮件队列; 只处理实时新短信,不重放messages.json历史。 - 移植并收紧旧 POC 的 OTP 识别:只有验证码语境附近的候选值才进入邮件标题,普通数字不误报。
- 设置面板提供默认关闭的转发开关、单一收件邮箱和每部署自备的 SMTP 参数;跨字段校验原子落盘,
密码按
secret掩码且不进日志/API 响应。 - SMTP 仅允许证书校验的 STARTTLS/SSL,后台发送有限重试;失败不阻塞模组/通话,也不记录正文、 验证码或凭证。公开 DMG/EXE 不内置共享发件密码。
真机发布验收仍需配置私有 SMTP 应用密码后各收一条「含验证码/不含验证码」短信,核对标题与正文。