English | 简体中文
将 Grandstream HT802 和一台模拟电话变成当前本机 Codex 任务的语音终端:摘机说话、挂机提交;Codex 完成后电话最多响两声,接听后播报回复,并继续等待下一轮语音。
本仓库只包含 HT802 电话方案。它不包含 ESP32 固件、串口状态灯、第二个 Codex 会话或非公开 Codex App Server/MCP 接口。
Setup 必须按下面的顺序完成。网络和 ATA 是后续步骤的基础;Codex Hook 在安装或改路径后需要重新加载。每一步末尾的“完成标志”用于判断是否可以继续。
- 将模拟电话接到 HT802 PHONE 1。
- 将 HT802 的 Ethernet 口接到 Mac 的独立 USB Ethernet 接口并通电。
- 在“系统设置 → 网络”中把该接口设为手动 IPv4:
- 地址:
192.168.82.1 - 子网掩码:
255.255.255.0 - 路由器:留空或
0.0.0.0
- 地址:
- 不要在该接口启用 Internet Sharing;继续使用 Wi-Fi 上网。
完成标志: Mac 的专用接口显示已连接,地址为 192.168.82.1/24。
如果不知道 HT802 地址,摘机拨 ***,听到菜单后按 02。用浏览器进入设备管理页,先把设备设为静态地址 192.168.82.100/24,然后按 HT802 配置填写 PHONE 1 的 SIP、自动拨号、DTMF 和响铃参数。
保存并重启 HT802,然后检查网络:
ping 192.168.82.100完成标志: ping 可以访问 192.168.82.100,管理页显示 PHONE 1 已保存预期参数。
git clone https://github.com/prikevs/codex-redline.git
cd codex-redline
export HT802_PHONE_ROOT="$PWD"
export HT802_PASSWORD='设备管理员密码'
python3 ht802-configure.py configure
python3 ht802-configure.py inspect
npm ci
ffmpeg -version
sh native/build-bridge.sh
sh native/build-audio.sh如果 ffmpeg -version 失败,请先使用本机包管理器安装 ffmpeg。首次使用时,在“系统设置 → 隐私与安全性 → 辅助功能”中允许 REDLINE Bridge.app。
完成标志: inspect 返回预期 P 值,并且以下检查都成功:
'.runtime/REDLINE Bridge.app/Contents/MacOS/RedlineBridge' --check
'.runtime/phone-audio' --devicesexport OPENAI_API_KEY='...'
python3 install-phone-hooks.py
ln -sfn "$PWD/skills/redline-phone-bind" "$HOME/.codex/skills/redline-phone-bind"安装或移动 Hook 后,完全退出并重新打开 Codex,再在 Codex /hooks 中审查并信任新命令。只关闭当前任务页面可能不会重新加载已经缓存的 Hook 路径。
完成标志: ~/.codex/hooks.json 中的 UserPromptSubmit 和 Stop 指向当前 clone 的 phone_hook.py,绑定 Skill 指向当前 clone。
从保存了 OPENAI_API_KEY 的环境启动服务:
node src/phone-control.mjs start
node src/phone-control.mjs status打开要绑定的 Codex 任务,并让 Codex 执行“将电话绑定到当前任务”。Skill 会在该任务环境中运行 bind,使用真实的 CODEX_THREAD_ID。不要从普通 Terminal 手工填写任务 UUID。
绑定成功后,PHONE 1 最多响两声。接听后应听到提示音;说一句测试内容并挂机,Codex 输入框应只提交一次。Codex 完成后电话再次最多响两声,接听一秒后开始播放回复。
完成标志: status 显示正确的 binding.projectPath、voiceSubmission.enabled: true,并完成一次“电话提交 → Codex 回复 → 电话播报”的闭环。
- 在目标 Codex 任务中执行
bind。 - Companion 将任务 UUID 和执行
bind时的 project 根目录保存在本机。 - 绑定成功后 PHONE 1 最多响两声。
- 如果接听绑定回铃,电话播放 660 Hz 就绪音并开始监听;说话后挂机即转写并提交。
- 如果没有接听,绑定仍然有效;以后直接摘机也会在就绪音后开始监听。
- Codex 完成后,官方 Hook 把最终回复交给 Companion。
- Companion 清理不适合朗读的 Markdown,使用 OpenAI TTS 生成语音,转换成电话音频并先归档到对应 project。
- PHONE 1 最多响两声。接听后先保留 1 秒静音,再播放回复。
- 回复播完后播放 660 Hz 提示音并直接进入下一轮监听;说话并挂机再次提交。
- 播报尚未结束时挂机会停止并跳过该回复;下一次摘机开始新一轮输入。
| 项目 | 规格 |
|---|---|
| 模拟口 | HT802 PHONE 1;PHONE 2 不参与 |
| SIP | Companion 监听 UDP 192.168.82.1:5090 |
| RTP | Companion 监听 UDP 192.168.82.1:15004 |
| 编解码 | G.711 μ-law / PCMU,8 kHz,20 ms RTP 帧 |
| DTMF | RFC 2833 / RFC 4733,动态 payload type,配置值 101 |
| 摘机目标 | 内部用户 263,HT802 自动补齐 SIP host |
| 回铃上限 | c=2000/4000; 下 0–2 秒、6–8 秒两个响铃段;8.2 秒取消 |
| 接听延迟 | 回复开头前 1 秒 PCMU 静音 |
| 就绪提示 | 660 Hz,300 ms;提示音结束后才接收用户语音 |
| 错误提示 | 220 Hz,600 ms;快速失败时延迟 350 ms,避免接通瞬间被截断 |
| 输入提交 | 仅正常物理挂机提交;空白、异常断线、焦点变化均不提交 |
| 回复队列 | 最多 8 条;服务重启时未读队列丢失 |
| 单条回复 | 最多 20,000 字符;TTS 每 1,000 字符按句末分段 |
| 播报时长 | 最多 10 分钟 |
flowchart LR
P[模拟电话<br/>PHONE 1] <-->|RJ11| H[Grandstream HT802]
H <-->|SIP + RTP/PCMU<br/>直连以太网| D[本机 Companion]
D -->|24 kHz PCM<br/>WebSocket| STT[OpenAI Realtime STT]
D -->|macOS Accessibility| C[当前 Codex 输入框]
C -->|UserPromptSubmit / Stop Hook| D
D -->|回复文字| TTS[OpenAI Speech API]
TTS -->|WAV| D
D -->|8 kHz WAV| A[project/.redline/phone-replies]
Companion 不创建或恢复另一个 Codex 会话。任务选择由 CODEX_THREAD_ID 完成;提交依赖当前前台 Codex 窗口中唯一的空白 Do anything 输入框。辅助功能桥会自动聚焦该输入框,但无法独立证明屏幕上展示的任务 UUID,因此绑定任务必须保持打开。
- macOS,Apple Silicon。
- Grandstream HT802 V1.5A,固件
1.0.3.2。 - 老式模拟电话接 PHONE 1。
- HT802 通过 USB Ethernet 直接连接 Mac。
- Node.js 20 或更高版本。
ffmpeg在PATH中。- Xcode Command Line Tools,用于编译 Swift 辅助程序。
- Codex 桌面应用和可用的
OPENAI_API_KEY。
HT802 V2 和新固件的页面布局、字段编号或默认值可能不同。Grandstream 的官方 HT80x 管理手册说明了静态地址、FXS/SIP、Off Hook Auto Dial、DTMF 和 System Ring Cadence;官方用户手册说明了 IVR、网页配置和 PHONE 端口操作。本仓库脚本的 P 值以已验证的 V1 固件为准。
默认使用一个不经过路由器的隔离子网:
| 设备 | 地址 | 用途 |
|---|---|---|
| Mac USB Ethernet | 192.168.82.1/24 |
SIP 与 RTP 服务端 |
| HT802 | 192.168.82.100/24 |
ATA |
| HT802 SIP 端口 | 5060/UDP |
Companion 发起回铃 |
| Companion SIP 端口 | 5090/UDP |
HT802 摘机自动呼叫 |
| Companion RTP 端口 | 15004/UDP |
双向 PCMU |
在“系统设置 → 网络”中将连接 HT802 的 USB Ethernet 服务设为手动 IPv4:
- IP 地址:
192.168.82.1 - 子网掩码:
255.255.255.0 - 路由器:留空或
0.0.0.0
不要在这个接口上启用 Internet Sharing。Wi-Fi 可以继续承担互联网访问。
如需自定义地址和端口,在启动服务及执行配置脚本前设置:
export HT802_LOCAL_ADDRESS=192.168.82.1
export HT802_ADDRESS=192.168.82.100
export HT802_SIP_PORT=5090
export HT802_DEVICE_SIP_PORT=5060
export HT802_RTP_PORT=15004自动配置脚本固定使用 /24 掩码;如果需要其他网段长度,应先修改并测试脚本。
- 把模拟电话接到 PHONE 1,把 HT802 Ethernet 接到 Mac 的专用网口并通电。
- 如不知道地址,摘机拨
***,听到菜单后按02;设备会播报当前 IPv4 地址。 - 浏览器打开 HT802 地址,以管理员身份登录。
- 在 Basic Settings 中选择 Static IP,设置
192.168.82.100/24。 - 在 FXS Port 1 中配置下表,保存后重启。
| 网页字段 | 值 | 作用 |
|---|---|---|
| Primary SIP Server | 192.168.82.1:5090 |
将 PHONE 1 的 SIP 请求发给 Companion |
| SIP User ID | redline |
本地 SIP 标识,不需要公网账号 |
| SIP Registration | No |
直连方案不使用 SIP 注册服务器 |
| Off Hook Auto Dial | 263 |
摘机立即呼叫 Companion |
| Off Hook Auto Dial Delay | 0 |
不等待拨号 |
| DTMF Payload Type | 101 |
与 Companion 的 RFC 4733 协商一致 |
| DTMF 方式 | RFC 2833 | 发送带外 DTMF 事件 |
| System Ring Cadence | c=2000/4000; |
每次响 2 秒、停 4 秒 |
已验证脚本实际提交的 V1 P 值如下。不同固件应以 Grandstream 对应版本的 configuration template 为准,不要只凭 P 编号跨版本复制。
| 页面 | P 值 | 值 |
|---|---|---|
config_a1 |
P47 |
192.168.82.1:5090 |
config_a1 |
P35 |
redline |
config_a1 |
P31 |
0 |
config_a1 |
P71 |
263 |
config_a1 |
P4045 |
0 |
config_a1 |
P850 |
101 |
config_a1 |
P870 |
0 |
config_a1 |
P4010 |
c=2000/4000; |
config2 |
P8 |
1,静态 IPv4 |
config2 |
P9–P12 |
192.168.82.100 |
config2 |
P13–P16 |
255.255.255.0 |
脚本直连设备网页接口,禁用代理,保存第一次修改前的原值,回读验证后才要求重启。默认管理员密码为 admin;生产设备应通过环境变量提供已经修改过的密码。
export HT802_PASSWORD='设备管理员密码'
export HT802_EXPECTED_MAC='设备标签上的 MAC 地址'
python3 ht802-configure.py inspect
python3 ht802-configure.py configureHT802_EXPECTED_MAC 可选;设置后脚本会在修改前核对目标设备。第一次 configure 的原值保存在 .runtime/ht802-original.json,后续配置不会覆盖基线。
回滚 ATA:
python3 ht802-configure.py restore恢复前必须仍能访问 HT802 当前地址。恢复 ATA 后,再把 Mac USB Ethernet 改回 DHCP 或原配置。
克隆后在仓库根目录保存其绝对路径,后续从其他 project 调用绑定命令时会用到:
git clone https://github.com/prikevs/codex-redline.git
cd codex-redline
export HT802_PHONE_ROOT="$PWD"npm ci
ffmpeg -versionsh native/build-bridge.sh
sh native/build-audio.shREDLINE Bridge.app 负责识别、聚焦和提交 Codex 输入框。首次使用时,在“系统设置 → 隐私与安全性 → 辅助功能”中允许它。其 bundle identifier 为 local.redline.phone-bridge,为兼容已有授权而保留。
phone-audio 是可选的 BlackHole/Dictate 诊断工具;正常 OpenAI STT + Accessibility 路径不需要 BlackHole。
验证辅助功能:
'.runtime/REDLINE Bridge.app/Contents/MacOS/RedlineBridge' --check
'.runtime/REDLINE Bridge.app/Contents/MacOS/RedlineBridge' --validate--validate 只验证并聚焦唯一空白输入框,不输入内容。
export OPENAI_API_KEY='...'仓库提供不含凭据的 .env.example 作为变量清单。程序不会自动读取 .env;可由 shell、密码管理器或服务管理器注入这些变量。
- 用户语音:OpenAI Realtime
gpt-live-transcribe,电话 PCMU 在内存中转换为 24 kHz PCM。 - 回复语音:OpenAI
gpt-4o-mini-tts,cedar,速度1.15。 - 可覆盖:
HT802_TTS_MODEL、HT802_TTS_VOICE、HT802_TTS_SPEED、HT802_TTS_INSTRUCTIONS。 - 旧的
REDLINE_TTS_*变量仍作为兼容别名接受,HT802_TTS_*优先。
python3 install-phone-hooks.py脚本只合并 UserPromptSubmit 和 Stop command hooks,保留其他 hook,并在 .runtime/hooks-before-phone.json 保存一次原配置备份。安装后在 Codex /hooks 中审查并信任。Hook 只转发任务 ID、turn ID、事件类型和最终回复,不读取 transcript 文件、不批准权限、不提交提示词。
安装器只去重当前仓库生成的精确命令。若从其他目录迁移,请在 ~/.codex/hooks.json 中删除旧仓库的 phone_hook.py command,避免同一事件被转发两次;其他 command hook 不受影响。
Codex Hooks 的行为以官方 Hooks 文档为准。
将仓库中的 skill 链接到个人 skills 目录:
ln -sfn "$PWD/skills/redline-phone-bind" "$HOME/.codex/skills/redline-phone-bind"Skill 的职责只有 bind、rebind 和 unbind。它从调用任务的 CODEX_THREAD_ID 取得真实任务 UUID,不允许手工传入另一个 UUID。
启动服务时必须保留 OPENAI_API_KEY 和网络环境变量:
node src/phone-control.mjs start
node src/phone-control.mjs statusstart 是幂等操作。它把守护进程输出写到 .runtime/phone.log,控制 socket 为 .runtime/phone.sock。
在需要绑定的 Codex project 根目录执行:
node "$HT802_PHONE_ROOT/src/phone-control.mjs" bindbind 保存当前 CODEX_THREAD_ID、本机 host 标识、标题和当前绝对工作目录。它同时创建 <project>/.redline/phone-replies/、启用语音输入并触发最多两声确认回铃。
其他命令:
node src/phone-control.mjs enable-voice
node src/phone-control.mjs disable-voice
node src/phone-control.mjs test-transcription
node src/phone-control.mjs test-dictate-audio
node src/phone-control.mjs restore-microphone
node src/phone-control.mjs unbind
node src/phone-control.mjs stopdisable-voice只关闭用户语音输入,回复播报仍工作。- 服务重启后语音输入默认关闭;执行
enable-voice或重新bind。 unbind清除绑定、停止通话、清空待播回复并关闭语音输入,不响铃。stop只停止服务并保留绑定。
每条通过 Hook 收到且成功合成的回复,必须先归档才允许响铃:
<bound-project>/.redline/phone-replies/
.gitignore
2026-09-08T12-34-56-789Z__<turn-id>__<uuid>.wav
WAV 是 8 kHz、单声道、16-bit PCM,内容与电话实际播报信号一致,不包含接听前 1 秒静音。文件以 owner-only 权限原子写入;目录内 .gitignore 使用 *,防止私人语音进入 Git。文件不会自动清理。
.redline/phone-replies、SIP 用户 redline、redline-phone-bind skill 和 local.redline.phone-bridge bundle identifier 是从原项目迁移时保留的兼容名称;它们不表示仍依赖原 REDLINE 仓库。
数据边界:
- 用户电话原始音频不落盘,实时发送给 OpenAI 转写。
- 用户转写文字不落盘;成功状态只保留字符数。
- Codex 最终回复文字发送给 OpenAI TTS。
- 生成的回复电话音频按用户要求长期保存在绑定 project。
- 绑定记录、设备备份、hook 备份、日志和 Unix socket 位于本仓库
.runtime/,不进入 Git。
status 的关键字段:
| 字段 | 含义 |
|---|---|
binding |
当前任务、project 路径和绑定时间 |
handset |
on_hook / off_hook |
lineBusy |
当前是否有 SIP 通话 |
queuedReplies |
待播报回复数量 |
replyReady |
队首回复是否已完成归档并可播放 |
replyError |
TTS、归档或队列错误 |
lastReplyArchive |
最近一条归档 WAV 的绝对路径 |
voiceSubmission |
语音输入是否启用、进行中及最近提交结果 |
confirmation |
绑定回铃状态 |
lastEvent |
最近运行事件 |
常见问题:
| 现象 | 检查 |
|---|---|
| 摘机没有提示音 | Codex 是否在前台、绑定任务是否打开、输入框是否为空;查看 voiceSubmission.result |
| 低音错误提示 | Accessibility、API key、输入框草稿或 Realtime 初始化失败;查看 .runtime/phone.log |
| HT802 没有呼叫 | 检查 Off Hook Auto Dial=263、Primary SIP Server、Mac 192.168.82.1 和 UDP 5090 |
| 能接通但无声 | 检查 RTP 15004、防火墙、PCMU、HT802 SDP 地址和 PHONE 1 |
| Codex 完成不回铃 | 检查 hooks 信任、任务 UUID、服务 socket、replyError 与 project 写权限 |
| 回复没有归档 | 重新从目标 project 根目录执行 bind,检查 binding.projectPath |
| 开头漏听 | 确认代码仍使用 1 秒静音前缀;过早挂机会跳过回复 |
| 只响一次或不满两次 | 两声是上限;接听、线路忙或 SIP 取消都会提前终止 |
连通性检查:
ping 192.168.82.100
nc -vz -u 192.168.82.100 5060
python3 ht802-configure.py inspect
node src/phone-control.mjs status
tail -f .runtime/phone.lognpm test自动测试覆盖 μ-law、RTP、SIP、双响铃截止、绑定持久化、转写生命周期、Accessibility 提交边界、TTS 分段、回复归档、播报中挂机、回复后继续监听,以及接听绑定回铃后开始语音。
硬件验收顺序:
inspect能读到预期 HT802。status显示线路空闲且绑定含正确projectPath。- 直接摘机听到高音,说固定测试句并挂机。
- Codex 输入框只提交一次,内容与语音一致。
- Codex 完成后电话最多响两声。
- 接听后 1 秒再开始播放,开头完整。
- WAV 出现在绑定 project,能够正常播放。
- 回复结束后听到高音,再说一句并挂机,进入下一 round。
- 播报中挂机立即停止,下一次摘机开始新 round。
unbind后摘机不再提交,旧任务完成也不回铃。
- SIP/RTP 没有 TLS、认证或媒体加密,只应运行在直连或可信隔离网络。
- Unix socket 权限为
0600,运行目录与归档目录为 owner-only。 - Companion 不处理 Codex 权限批准,不读取完整对话历史。
- Accessibility 提交不重试不确定结果,避免重复消息。
- 当前只支持一个 HT802、PHONE 1 和一个绑定任务。
- 服务没有自动启动项;Mac 重启后需重新
start并enable-voice。 - HT802 管理密码和 OpenAI key 只通过环境变量提供,不写入仓库。
发布公开仓库前,确认 .runtime/、.env 和各 project 的 .redline/phone-replies/ 没有被强制加入 Git。提交历史不应包含 API key、设备管理密码、绑定任务 UUID、归档音频或个人绝对路径。