Skip to content

🐛 fix(voice): 修正 SparkBot 日程持久化与实时交互稳定性 - #338

Merged
ZhaoXingPeng merged 11 commits into
1024XEngineer:mainfrom
ZhaoXingPeng:fix/linx-realtime-sqlite-im-20260820
Aug 21, 2026
Merged

🐛 fix(voice): 修正 SparkBot 日程持久化与实时交互稳定性#338
ZhaoXingPeng merged 11 commits into
1024XEngineer:mainfrom
ZhaoXingPeng:fix/linx-realtime-sqlite-im-20260820

Conversation

@ZhaoXingPeng

@ZhaoXingPeng ZhaoXingPeng commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

结论:本 PR 已在真实 SparkBot 上验证 SQLite 日程持久化、百炼语音交互、日程 CRUD、提醒播报、模糊意图澄清和生产 IM Runtime 启动;Host/ESP-IDF/CI 均通过。请求继续进行人工 Review。公众号真实消息/动作回执和真实 AEC 全双工仍是外部验收边界,不能由本 PR 冒充完成。

Fixes #336

What changed

  • 修复 SparkBot voicelife FATFS 分区上的 SQLite 持久化和分区类型契约。
  • 修正 Linx worker 栈大小,并把 MCP 请求移出 WebSocket RX worker,避免 SQLite/FATFS 阻塞音频链路。
  • 每轮刷新串口 PCM 注入窗口,不在已聆听状态重复投递 PressDown。
  • 将定时提醒送入交互播报队列,确保控制器从待机迁移到可播报状态后再提交 TTS。
  • Runtime 使用 Linx realtime 协议模式;SparkBot 当前仍按已验证的半双工边界运行。

Verification

  • 全量 Host CTest:80/80 通过;Python unittest:155 通过、1 跳过。
  • GitHub CI 全部成功,包括 ESP-IDF 6.0 / ESP32-S3、主机测试、IM Gateway、CodeQL 和 Codecov。
  • 真实百炼输入 TTS:qwen-audio-3.0-tts-flash + longanlingxi。SparkBot 多轮聊天、日程创建/查询/修改/删除、删除后查询和到点提醒均有实板日志证据。
  • 日程硬复位后仍可查询,启动出现 STORAGE_READY=1、schema version 5。
  • 到点提醒出现 tts_started -> tts_sentence_started -> tts_first_audio -> tts_stopped,屏幕显示“说话中”和完整提醒文本,之后恢复聆听/待机;out_reject=0short_write=0、I2S 错误和交互队列丢弃均为 0。
  • 模糊意图四轮真实注入全部 ASR 精确匹配:月底/最近/晚点/每月最后工作日表达分别进入追问、只读查询或明确不支持;模糊写入未产生 mutation。
  • 诊断 profile 测试后只刷回生产应用区 0x10000,未覆盖分区表、NVS、otadata、linx_secrets、assets、model 或 voicelife。生产启动出现 transport_connectedreadystandby_readyIM_RUNTIME_READY=1IM_PAIRING_READY=1
  • Gateway /root/XE6-15 日志和 PostgreSQL 显示真实设备日程查询投递 202 -> delivered,并有微信模板 template:*:success 回执。

Fenno 预审

2026-08-20 已按 Linx 传输生命周期、存储/Profile、线程资源、安全日志和测试覆盖发起预审。唯一 P1 是修复前 SQLite worker 栈大小单位错误,已在 3495e03 修正为 ESP-IDF 路径 32 KiB;相关契约测试、ESP-IDF CI 和实板 SQLite 回归通过。Fenno 预审不替代人工 Review、CI 或实板验收。

Remaining validation

  • 真实微信公众号账号的新消息、提醒通知、平台回执和设备动作回传仍未完成;当前证据只证明设备到 Gateway、Gateway 到微信模板的已存在投递记录。
  • SparkBot 没有已验证 playback reference/AEC,因此不宣称真实声学全双工;当前已验证的是协议 realtime + 受控抢占/打断。
  • 本机 pnpm test 因工作区缺少 ws 依赖且仓库无 lockfile 未能启动;GitHub CI 的 IM Gateway 检查已通过。

原始串口日志、音频、API Key、SSH 凭据和设备 NVS 均未提交。

@ZhaoXingPeng
ZhaoXingPeng force-pushed the fix/linx-realtime-sqlite-im-20260820 branch from 099213c to e0e843d Compare August 20, 2026 10:57
fennoai[bot]

This comment was marked as outdated.

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

实板补充证据(2026-08-20)

  • 仅刷写 factory 应用区 0x10000,未覆盖 nvsotadatalinx_secretsassetsmodelvoicelife
  • 临时串口 PCM 测试固件真实输入“请创建一个日程,明天上午十点提醒我开会”,板端 STT 为“请创建一个日程,明天上午10点提醒我开会”;日志出现 MCP_TOOL_EXECUTED ... result=1mcp_tool_result ... 日程已创建
  • 硬复位后再次真实输入“请查询一下明天上午十点的日程”,日志出现 STORAGE_READY=1 ... schema_version=5mcp_tool_result ... 日程查询完成,并播报“明天上午十点,你有一个日程叫‘开会’”。该回合 completed_turns=1,无队列丢弃、无 Provider 错误。
  • 该串口测试固件只用于验证,已恢复正式 esp32s3-esp-sparkbot 应用;正式应用复位后再次出现 STORAGE_READY=1transport_connectedOUTPUT_OPENES8311_OPEN_OKIM_RUNTIME_READY=1
  • IM 额外验证:临时同时开启 IM 与串口 harness 可达到 IM_RUNTIME_READY=1,但内部最大连续堆降至约 5.6 KiB,串口回合无法开始;这是测试装配资源冲突,已恢复 Profile,不能作为 IM 业务闭环成功证据。
  • Linx realtime 仍只按协议模式已发送、半双工边界记录;未宣称 AEC/全双工声学验收。

@ZhaoXingPeng

ZhaoXingPeng commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator Author

Async MCP 与当前验收更新(2026-08-20)

  • Commit 399226b 将 Linx MCP 处理从 WebSocket RX 回调移到容量为 4 的有界工作队列;旧 generation 的响应会丢弃,断连时会清理待处理请求。
  • ./scripts/run_host_tests.sh:80/80 通过。合约测试会阻塞 MCP handler 并注入二进制 TTS 音频,验证音频可立即送达、MCP 响应随后返回。
  • ESP-IDF 已构建 esp32s3-esp-sparkbot-serial-voiceesp32s3-esp-sparkbot;最新生产镜像已在 SparkBot 上运行。
  • 真实语音创建 -> 硬复位 -> 真实语音查询已验证 SQLite 状态持久:STORAGE_READY=1schema_version=5,无事件队列溢出、Provider error、hello reject、panic 或串口 PCM reject。
  • 活跃设备/用户凭据的 IM 认证 smoke 已通过:会话创建、查询与自然过期均可观察。

未验收边界:尚未有人在配置的微信公众号账号中确认实时配对码,因此没有本轮真实平台回执或动作回调证据。首个未验证边界是外部平台配对/投递,不是板端 HTTP 契约。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

/review

请按以下顺序审查本 PR:

  1. 可能导致现有行为回归的逻辑错误;
  2. 内存、线程、资源生命周期和错误路径;
  3. 安全、隐私和日志泄露;
  4. 测试是否覆盖本 PR 的验收标准。

请只基于当前 diff、仓库现有代码和 PR 描述给出可验证的问题,按 Critical / High / Medium / Low 分级,说明文件、行号、触发条件和修复建议。无法确认的内容标记为“需人工验证”,不要假设硬件、网络或真实语音结果。

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

按请求检查了当前固定 diff:行为回归、线程/资源生命周期、安全日志和验收测试覆盖。异步 MCP 的 host 合约测试通过,未发现可验证的安全或隐私泄露;但以下两处配置/启动路径仍可能造成用户可见故障,且当前测试未覆盖对应 ESP-IDF 运行时条件。

验证:linx_provider_contract_testlinx_esp_transport_contract_testsparkbot_profile_contract_testfatfs_volume_contract_test 通过;全量 host 构建被仓库现有 [[maybe_unused]] 警告(-Werror=attributes)阻断,未能完成全量 CTest。实板/真实语音结果未作假设;实际栈高水位和空分区行为需人工验证。

Additional findings

  • components/voicelife_storage_fatfs/src/fatfs_volume.cc:?: [P1] 为新的 FATFS 分区提供显式首次初始化: 本次将 SparkBot 生产 profile 切换到 SQLite/FATFS,但 StorageBootstrap::Start() 会在创建语音会话前立即调用 volume_.Mount(),挂载失败就直接终止启动;这里又把 format_if_mount_failed 固定为 false。因此只刷写应用区、首次使用、分区被擦除或没有预先格式化 FATFS 元数据的设备,会在 voicelife 分区上启动失败,无法达到“持久化日程”之外的基本语音启动验收。PR 描述只说明保留已有分区,当前 diff 没有工厂格式化/预置步骤,也没有空分区启动测试。请在制造/刷写流程中明确预格式化该分区并在文档、CI/设备检查中验证,或提供不会覆盖已有日程的显式一次性初始化路径;不要依赖运行时静默格式化。

// ESP-IDF passes this value to xTaskCreatePinnedToCore in StackType_t
// words, not bytes. 4096 words (16 KiB on ESP32-S3) leaves room for the
// TLS task and I2S DMA after SQLite/FATFS startup.
uint32_t websocket_task_stack_size = 6144;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 保持 WebSocket 任务的实际栈容量

websocket_task_stack_sizeesp_websocket_impl.cc:96 被直接赋给 ESP WebSocket client 的 task_stack,本次把默认值从 12288 降为 6144;新增注释却按 StackType_t words 将其描述为 4096 words/16 KiB,代码没有做换算。因此 Linx/TLS 接收任务的配置预算实际被减半,触发需要超过 6 KiB 栈的正常分片、TLS 或音频处理路径时会出现栈溢出、任务异常或连接中断。实际高水位需人工验证,但当前注释与传值单位已经不一致。请恢复与 API 单位匹配的容量(例如保留 12288),或显式做 bytes/words 换算,并增加目标构建/运行时栈高水位断言,避免仅由 host 合约测试锁定一个错误常量。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

实板真实百炼复测(2026-08-20)

本轮使用正确的 sk- Key 行调用百炼 qwen-audio-3.0-tts-flash + longanlingxi,没有使用本地 TTS 替代:

  • 百炼预检:TTS 返回非空音频 41,468 bytes,首包约 722 ms。
  • 四轮语音链路:4/4 ASR 精确匹配,4/4 回合完成;状态 phase 3/4/5/6、字幕渲染和长文本横向滚动均覆盖。
  • 音频门禁:in_drop=0out_reject=0short_write=0、I2S 错误为 0;串口 PCM reject、交互队列丢弃、provider error 均为 0。
  • 业务 CRUD:旧测试数据导致“9 点改 10 点”进入冲突澄清,未把这轮误报为完整 CRUD;唯一标题创建成功。
  • 持久化:创建“硬复位持久化回归”后执行硬复位,再用真实百炼语音查询,板端重新从 voicelife FATFS/SQLite 查到该日程;启动 STORAGE_READY=1、schema v5,查询回合门禁全部通过。

临时证据保存在本机 /tmp/voicelife-persist-create-13k4Tk/tmp/voicelife-persist-query-ALcJhA,未提交原始日志、音频或凭据。

定时任务此前直接调用 VoiceSession::Speak,控制器仍处于待机,导致提醒 TTS 事件被显示状态门控丢弃。

通过系统播报交互事件先迁移到可播报状态,再由板级队列取消旧回合并提交 TTS;补充提醒状态迁移回归测试。

主机 80/80、格式和架构检查通过;真实 SparkBot 已复现提醒到点但未播报,修复后的实板回归待本提交刷写后完成。

Refs 1024XEngineer#336
@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

阶段测试结果:真实百炼语音 + SparkBot 日程/提醒

已通过

  • 真实百炼 TTS qwen-audio-3.0-tts-flash / longanlingxi 串口注入:纯中文标题“蓝莓测试”完成创建、查询、改时、删除、删除后查询;更新后的时间和备注均由设备播报确认。
  • 该轮 5 轮均完成,MCP_TOOL_EXECUTED 成功;SERIAL_VOICE_PCM=reject、provider error、交互队列丢弃、I2S 错误均为 0。屏幕观察到 53 次文本快照、phase 3/4/5/6/7 完整、横向滚动生效。百炼 ASR 将中文数字“ 三/四 ”规范化为阿拉伯数字,故严格逐字 ASR 匹配为 3/5,但业务意图和工具结果均正确。
  • 主机 CTest 80/80 通过;Python unittest 155 通过、1 跳过;架构和格式检查通过;生产 SparkBot profile 构建通过,应用镜像 0x282e00 / 应用分区余量 11%。

发现并修复

  • 重启后短提醒“黄鹂提醒”在到点时确实触发,但修复前日志显示 tts_startedTTS_SENTENCE_STALE state=1out_frames=0,提醒被待机状态门控丢弃。
  • 提交 72f708c(Refs [Fix] SparkBot 生产 Profile 接入 SQLite 持久化并核对 Linx realtime #336)将系统提醒先送入交互事件队列迁移到可播报状态,再由板级队列提交 TTS,并新增控制器回归覆盖提醒后恢复聆听/待机。

待本阶段完成

  • 72f708c 刷入 SparkBot 应用区后的真实短提醒回归,确认 tts_first_audio、屏幕“说话中/提醒文本”、播报后恢复聆听或待机,且无丢包。
  • 生产 profile 的微信公众号人工确认、真实消息/回执和 IM 日程动作闭环仍未完成;当前串口诊断 profile 的 IM 是关闭的。

原始串口日志和音频仅保存在本机 /tmp,未提交。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

72f708c 实板提醒回归与 IM 日志补充(2026-08-20)

本次测试

  • 诊断 profile esp32s3-esp-sparkbot-serial-voice 构建完成,仅刷写 0x10000 应用区;分区表、NVS、otadata、linx_secrets、assets、model 和 voicelife 均未写入。
  • 使用阿里云百炼真实 TTS qwen-audio-3.0-tts-flash + longanlingxi 注入 SparkBot:中文聊天 1/1 完成,ASR、Linx、下行 TTS、字幕和 phase 3/4/5/6/7 全链路通过;out_frames=354out_reject=0short_write=0、I2S 错误和交互队列丢弃均为 0。
  • 通过真实语音创建唯一日程“中文提醒测试2255”,设置为 2026-08-20 22:56。串口出现 MCP_TOOL_EXECUTED result=1mcp_tool_result detail=日程已创建
  • 到点持续监听串口:提醒只触发一次,出现 tts_started -> tts_sentence_started detail=提醒:现在是「中文提醒测试2255」时间了 -> tts_first_audio -> tts_stoppedout_frames 从 616 增至 779,out_reject=0short_write=0、I2S 错误和队列丢弃为 0;屏幕出现 status=说话中 和完整提醒文本,随后回到 standby_ready
  • 提醒抢占期间记录到一次旧 Provider 事件 stale_event_dropped,随后 interrupted detail=old audio generation invalidated,没有旧 PCM 穿透;这是 generation 隔离路径的可观察证据,不是静默丢播。
  • 恢复生产 profile esp32s3-esp-sparkbot 并硬复位,确认 transport_connectedreadystandby_readyIM_RUNTIME_READY=1IM_PAIRING_READY=1
  • SSH 查看 /root/XE6-15 网关:容器 voicelife-im-gateway 正常运行;日志有 device.pairing.create=201、连续 device.pairing.get=200wechat.webhook=200。本轮未取得新的公众号真实消息/动作回执,因此不把 IM 平台闭环标记为通过。

Linx realtime 调研结论

官方 WebSocket 文档定义 manual 为按键控制、auto 为唤醒词触发打断、realtime 为自由对话/全双工检测到语音即打断,并明确推荐 realtime;文档还要求 hello 声明 features.mcp、协商音频参数,支持 abort 的打断语义和 tts.stop.is_aborted。当前 Runtime 已发送 mode=realtime,同时 SparkBot 没有已验证 playback reference/AEC,因此本项目只能证明“协议模式已启用 + 受控抢占通过”,不能宣称真实全双工声学验收。agent_params.custom_replace_prompt 只是 hello 建连时的提示词占位符替换能力,当前项目没有配置占位符需求,不建议为此改动协议。

结论

72f708c 的核心修复已在真实 SparkBot 到点提醒中通过;SQLite 持久化、语音状态/显示一致性和提醒音频路径均有证据。剩余验收项是公众号人工配对后的真实消息、回执和日程动作闭环,以及真实麦克风/AEC 双讲测试。原始串口、音频和凭据仍只保存在本机受控 /tmp 路径。

@ZhaoXingPeng ZhaoXingPeng changed the title fix: persist SparkBot schedules and keep realtime audio stable 🐛 fix(voice): 修正 SparkBot 日程持久化与实时交互稳定性 Aug 20, 2026
@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

模糊意图实板回归与 IM 通知核对(2026-08-20)

模糊意图

临时刷入诊断 profile esp32s3-esp-sparkbot-serial-voice,使用真实百炼 TTS qwen-audio-3.0-tts-flash + longanlingxi 注入 SparkBot:

  • “月底提醒我一下” -> 设备回读 8 月 31 日并继续追问提醒时间和事项,未执行写入。
  • “最近有什么安排?” -> 只执行日程查询,返回近期日程。
  • “晚点帮我记个事。” -> 追问具体内容和时间,未执行写入。
  • “每月最后一个工作日提醒我交报表。” -> 计算当前月最后工作日后明确说明该周期不支持,并停在确认前,未执行写入。

结果:4/4 ASR 精确匹配,4/4 回合完成;MCP_TOOL_EXECUTED 仅出现查询回合,模糊写入没有产生 mutation。字幕、phase 3/4/5/6/7 和横向滚动均出现;out_reject=0short_write=0、I2S 错误、串口 PCM reject、交互队列丢弃和 provider error 均为 0。

生产恢复

诊断测试后只刷回生产应用区 0x10000,未覆盖分区表、NVS、otadata、linx_secrets、assets、model 或 voicelife。生产启动确认:

  • transport_connected
  • ready
  • standby_ready
  • IM_RUNTIME_READY=1
  • IM_PAIRING_READY=1

IM 通知核对

服务器 /root/XE6-15voicelife-im-gateway 正常运行。数据库和日志中已有真实设备日程查询投递:

  • device.schedule-query-result.create HTTP 202
  • delivery.worker.dispatched
  • 微信模板回执 template:*:success
  • 对应 Delivery 状态为 delivered

本轮生产板启动后没有新的提醒通知提交,也没有新的公众号消息/动作回执,因此“到点提醒 -> 公众号入站/出站 -> 设备动作回执”仍保持未验收状态;需要真实公众号账号产生一条消息或提醒事件后再闭环验证。

主机相关周期/MCP/提醒测试:11/11 通过。IM Gateway 本地 pnpm test 因工作区缺少 ws 依赖且仓库无 lockfile,未能在当前环境启动;GitHub CI 既有检查仍为绿色。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

回归汇总补充(2026-08-20)

  • 全量 Host CTest:80/80 通过,覆盖 schedule/MCP/reminder/SQLite/Linx/voice/IM contract。
  • Python unittest:155 通过、1 跳过。
  • 生产 SparkBot 已恢复,应用镜像仅写入 0x10000;启动 transport_connectedreadystandby_readyIM_RUNTIME_READY=1IM_PAIRING_READY=1
  • PR 标题已按仓库惯例修正为:🐛 fix(voice): 修正 SparkBot 日程持久化与实时交互稳定性

仍未宣称的边界:真实公众号新消息触发的提醒通知、公众号回执/动作回传,以及带真实 playback reference 的声学全双工/AEC。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

终结语与 IM 通知核对补充(2026-08-20)

终结语 / wake guard

  • 真实百炼 TTS + SparkBot 终结语回合通过:ASR 1/1,事件顺序为 tts_started -> tts_sentence_started -> tts_first_audio -> tts_stopped
  • 播报结束后 WAKE_GUARD_ARMED ms=8000;继续观察 8.5 秒未出现误唤醒。out_reject=0short_write=0、I2S 错误、交互队列丢弃和 Provider 错误均为 0。

IM 通知是否到位

本次通过 SSH 读取 /root/XE6-15voicelife-im-gateway PostgreSQL 与容器日志:

  • im_deliveriesreminder_due / delivered = 11schedule_query_result / delivered = 21;没有对应的失败状态。
  • 最近一条提醒投递(2026-08-18)有 external_message_id,回执为 stage=deliveredtemplate:<id>:success
  • im_inbound_eventsdelivery.updated / processed = 32,另有 message.received / processed = 3im_actions 有 1 条 succeeded、3 条 dispatched,另有过期记录。
  • 网关日志可见 delivery.worker.dispatchedwechat.webhook=200,说明设备结果/提醒进入 Gateway 后曾完成微信模板投递并收到平台回执。

边界:本次生产 profile 启动后没有新建提醒事件,因此没有把历史 delivered 记录冒充为“本轮到点提醒的公众号新消息闭环”。真实公众号新消息触发、设备动作回传和本轮新提醒的端到端复现仍需外部账号操作;当前可以确认的是通知投递与平台回执链路已有真实成功记录。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

本轮测试用例文档:生产恢复后的 SparkBot 全链路回归(2026-08-20)

结论目标:在不覆盖分区表、NVS、linx_secrets、assets、model 和 voicelife 的前提下,重新验证百炼输入基线、SparkBot 语音状态/字幕/音频门禁,并核对 IM Gateway 的提醒通知与平台回执。

环境

  • 板卡:ESP-SparkBot,串口 /dev/cu.usbmodem14201
  • 输入:阿里云百炼 qwen-audio-3.0-tts-flash + longanlingxi;云端回转写 qwen3-asr-flash
  • 目标固件:先使用 esp32s3-esp-sparkbot-serial-voice 做 USB PCM 夹具测试,结束后只刷回 esp32s3-esp-sparkbot 应用区 0x10000
  • IM 服务:服务器 /root/XE6-15,容器 voicelife-im-gateway,PostgreSQL 数据库

用例与通过条件

编号 用例 通过条件
V1 百炼并发基线:3 会话 x 8 回合 24/24 成功,24/24 严格回转写匹配,无云端错误
V2 SparkBot 普通语音/上下文 每回合出现 capture、ASR、TTS、首包、停止;phase/字幕一致;无 PCM reject、队列丢弃、I2S 错误
V3 日程 CRUD 与硬复位 创建、查询、修改、删除、删除后查询均正确;复位后 SQLite 仍可读
V4 到点提醒 只触发一次;出现 tts_started -> tts_first_audio -> tts_stopped;屏幕显示提醒并恢复待机
V5 打断/终结语 抢占不交叉、不穿透旧 PCM;终结语后 8 秒 wake guard 内无误唤醒
V6 IM 通知核对 reminder_due/查询投递有 delivered 状态、微信模板 success 回执;入站事件与动作状态可关联

证据要求

每个用例记录:输入原文、板端 stt_text_received、工具结果、TTS 文本、phase/字幕、AUDIO_STATS、队列统计、串口错误和服务器投递/回执;任何失败只报告首个失败门禁。原始串口和音频留在本机受控临时目录,不提交仓库。

外部边界

没有人工微信公众号操作时,不把历史数据库 delivered 记录当成本轮新消息闭环;真实账号新消息、动作按钮回传和真实麦克风 AEC/双讲仍单独标记。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

用例执行结果:V1/V2/V5(2026-08-20)

V1 百炼并发基线:通过

  • 命令:voice_bailian_load_test.py --requests 3 --concurrency 3 --turns-per-conversation 8 --tts-model qwen-audio-3.0-tts-flash --voice longanlingxi --stt-model qwen3-asr-flash
  • 结果:3/3 会话完成,24/24 请求成功,24/24 严格回转写匹配,错误 0。
  • 延迟:TTS p50/p95/p99 = 760/1138/1145 ms;STT = 481/581/595 ms;端到端 = 1247/1663/1726 ms。

V2 SparkBot 八轮上下文:通过

  • 句集:灯塔守灯人故事 8 轮,使用测试 Profile esp32s3-esp-sparkbot-serial-voice,真实百炼 TTS 注入 /dev/cu.usbmodem14201
  • 结果:8/8 回合完成,ASR 严格匹配 8/8;状态事件、phase 3/4/5/6/7、字幕与 GIF 状态均连续。
  • 显示:70 次文本快照、39 次内容快照;overflow_width>0 的横向滚动实际出现。
  • 音频:in_frames=7035out_frames=4190out_q_hi=9/16min_heap=4615160in_drop=0out_reject=0short_write=0、I2S 错误、PCM reject、交互队列丢弃和 Provider 错误均为 0。

V5 终结语 / wake guard:通过

  • 输入“今天的故事我已经记住了,谢谢你,再见。”,ASR 1/1 匹配;下行事件为 tts_started -> tts_sentence_started -> tts_first_audio -> tts_stopped
  • WAKE_GUARD_ARMED ms=8000 reason=terminal_tts,8.5 秒观察内无误唤醒或新一轮本地回应。
  • 音频 in_frames=285out_frames=54out_reject=0short_write=0、I2S 错误、交互队列丢弃和 Provider 错误均为 0。

证据与边界

  • 原始串口与汇总 JSON 留在本机受控 /tmp/voicelife-*,未提交。
  • 这两轮使用 IM-disabled 测试 Profile,只证明语音、状态、字幕、音频和终结语门禁;不证明生产 IM 闭环。
  • 测试 Profile 会在下一步只刷回生产应用区 0x10000,随后单独回读生产 IM readiness。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

测试用例文档:V0 生产 Profile 恢复与启动链路

结论: 已将 SparkBot 恢复为生产固件 esp32s3-esp-sparkbot,只写入应用分区 factory@0x10000,刷写后校验通过;重启日志同时确认 SQLite、Linx WebSocket、语音待机和生产 IM 运行时均启动。该用例只证明启动与初始化,不代替后续真实公众号闭环。

环境

  • 设备:ESP-SparkBot,/dev/cu.usbmodem14201
  • 固件:build/esp32s3-esp-sparkbot/voicelife.bin
  • 复位:USB UART/JTAG 硬复位;串口 115200

用例

  1. 写入生产应用镜像并执行 esptool hash 校验。
  2. 硬复位,读取完整明文串口启动日志。
  3. 校验:STORAGE_READY=1transport_connectedreadystandby_readyIM_RUNTIME_READY=1IM_PAIRING_READY=1;不得出现 panic、启动失败或 SQLite 错误。

验收标准

  • 应用镜像写入和 hash 校验成功。
  • 存储分区挂载、schema 初始化成功。
  • Linx WebSocket 已连接,语音运行时进入待机。
  • 生产 IM 运行时和配对入口就绪。

证据保留
原始串口日志仅保留在本机临时目录,不进入仓库;日志中的凭据未采集。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

V0 执行结果:生产 Profile 启动通过

结果:PASS(启动门禁)

  • 刷写:Wrote 2633216 bytesVerifying written data...Hash of data verified.
  • 分区表:factory@0x10000voicelife@0x700000 / 0x900000,与生产布局一致。
  • SQLite:STORAGE_READY=1 partition=voicelife path=/data ... schema_version=5,未见 SQLite 错误。
  • Linx:WIFI_STA_GOT_IP=1LINX_OTA_RESPONSE ... websocket=1 token=1VOICE_EVENT ... event=transport_connected
  • 语音:VOICE_EVENT ... event=readyevent=standby_ready;屏幕同时输出 SPARKBOT_TEXT_RENDER ... status=23:41,随后进入 idle 动画。
  • IM:IM_PROVISION_READY=1IM_PAIRING_READY=1IM_RUNTIME_READY=1
  • 音频启动门禁:in_drop=0out_reject=0short_write=0in_i2s_err=0out_i2s_err=0
  • 异常:本次启动窗口未出现 panic、启动失败或 SQLite error。

边界:生产 Profile 没有启用串口 PCM 注入测试端点,因此本条不宣称语音 CRUD、提醒或公众号消息闭环通过;这些继续用测试端点加生产 IM 日志做分项验证。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

测试用例文档:V3 百炼语音日程 CRUD 与界面联动

结论目标: 用真实百炼 TTS 生成语音,经 SparkBot 串口 PCM 注入并由 Linx ASR/TTS 处理,验证一次性日程的创建、查询、修改、删除以及字幕/动画/状态的同步。测试后恢复生产 Profile;本用例不把 IM-disabled 测试 Profile 的结果外推到公众号 IM。

环境与隔离

  • 设备:ESP-SparkBot,/dev/cu.usbmodem14201
  • 测试固件:esp32s3-esp-sparkbot-serial-voice,仅用于注入测试,IM 显式 disabled。
  • 输入:阿里云百炼 TTS,qwen-audio-3.0-tts-flash / longanlingxi;板端 Linx 完成识别和回复。
  • 用例结束后:只写生产应用分区 factory@0x10000 恢复 esp32s3-esp-sparkbot

步骤

  1. 创建唯一标识日程:8 月 21 日上午 9 点,VoiceLife CRUD 回归
  2. 查询该日程,确认标题和时间。
  3. 将其改为上午 10 点,再查询确认旧时间没有残留。
  4. 删除该日程,最后查询确认不存在。

逐项验收

  • 每轮均有 capture_stopped -> stt_text_received -> tts_started -> tts_first_audio -> tts_stopped
  • ASR 按语义归一化匹配输入;MCP 日志能证明读/写工具真正执行。
  • 每次状态迁移覆盖处理、播报、恢复聆听;屏幕 SPARKBOT_TEXT_RENDER 的状态与阶段一致,长文本进入横向滚动。
  • 音频无 in_drop/out_reject/short_write/I2S error,交互队列无丢弃,串口 PCM 不应被拒绝。
  • 最终查询不返回已删除日程;测试 Profile 不留在设备上。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

V3 执行前置更新

qwen-audio-3.0-tts-flash / longanlingxi 连续两次在本机到百炼的 TTS WebSocket 建连阶段于 5 秒内超时,板端尚未收到任何 PCM,不能归因于固件。为不中断真实云端语音验收,V3 改用本机已通过基线的百炼组合 cosyvoice-v3-flash / longanhuan_v3;仍由真实百炼生成输入音频,不使用 macOS 本地语音回退。Qwen TTS 连通性会作为外部依赖可用性单列记录。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

V3 执行结果:BLOCKED(上游 Linx 配额,不判 CRUD 通过)

已经通过的链路段

  • 百炼 qwen-audio-3.0-tts-flash / longanlingxi 成功生成并注入 362 个 20 ms PCM 帧;没有端点截断,也没有串口 PCM reject。
  • SparkBot 完成 capture_stopped -> stt_text_received -> tts_started -> tts_first_audio -> tts_stopped
  • 屏幕先显示 处理中,后与播报同步显示 说话中 和返回文本;音频指标为 in_drop=0out_reject=0short_write=0、I2S error=0、交互队列丢弃=0。

阻断点与影响

  • 板端 Linx STT 返回:额度不足,请联系灵矽商务,没有输入的真实转写,也没有 MCP_TOOL_EXECUTED tool_call=1mcp_tool_result
  • 夹具因此无法进入下一轮聆听,5 个 CRUD 步骤实际完成 0 个;本次不能证明创建/查询/修改/删除或持久化。
  • 这是上游 Linx 账户配额响应,不是串口、SQLite 或提醒调度异常。需恢复该智能体可用额度后重跑原用例;不能以本次屏幕和音频正常替代业务验收。

更正前置失败归因
此前两次百炼连接失败来自本机误把多行配置文件整体作为 API Key;现已只读取实际 Key 行,百炼输入 TTS 已在本次成功。原始日志和 JSON 仅保留本机 /tmp

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

@fennoai /review

@fennoai

fennoai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Review 已提交

已按固定提交范围完成全量变更审查,并通过 PR Review 提交两项问题:

  • P1:实际执行 SQLite 的新 std::thread 未获得声明的 32 KiB 栈。
  • P2:长 MCP 操作跨 WebSocket 自动重连时可能向新连接发送旧响应。

验证通过:3 个相关 Host CTest;Python firmware tests 12/12。

View job run

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review 结论

本次按固定 SHA 范围检查了 Linx MCP 异步执行与重连生命周期、ESP WebSocket 任务栈、SparkBot FATFS/SQLite 分区契约、系统提醒状态迁移和串口多轮注入。存储分区与提醒状态机改动本身未发现新的可报告问题;但 MCP 移出 RX 回调后,实际执行 SQLite 的新线程栈没有被 32 KiB 配置覆盖,并且长事务跨自动重连时仍可能把旧请求响应发送到新连接。

验证:linx_provider_contract_testlinx_esp_transport_contract_testvoice_interaction_controller_test 均通过;python3 -m unittest tests/python/test_firmware.py 12/12 通过。

std::lock_guard<std::mutex> lock(mcp_mutex_);
if (mcp_worker_.joinable()) return;
mcp_stop_ = false;
mcp_worker_ = std::thread([this]() { McpWorkerLoop(); });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 为真正执行 SQLite 的 MCP 线程配置 32 KiB 栈

这里把 MCP/SQLite 工作移到了 std::thread,但本 PR 增大的 EspWebSocketTransportOptions::worker_task_stack_size 只用于 linx_ws_events FreeRTOS 任务;两个 SparkBot profile 也没有设置 ESP-IDF 的 pthread 默认栈大小。因此 mcp_handler_ 中的 sqlite3_step 实际运行在未扩容的 pthread 栈上,原先的栈溢出路径只是从 WebSocket worker 转移到了新线程,真实日程 CRUD 仍可能崩溃。请用可显式指定 32 KiB 栈的 FreeRTOS/pthread 创建方式启动该 worker,或在对应 profile 中配置并验证 pthread 栈大小。

}
if (request.generation != generation_.load() || !connected_.load()) continue;
const auto response = mcp_handler_(request.payload, request.session_id);
if (request.generation != generation_.load() || !connected_.load()) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 用连接代次丢弃跨重连完成的旧 MCP 响应

请求只记录 voice generation;自动 WebSocket 断线重连不会调用 SetGeneration,所以一次较慢的 SQLite handler 若在断线前开始、重连后结束,此处会看到相同 generation 且 connected_ == true,随后把旧连接的 JSON-RPC/session 响应发送到新连接。日程写入可能已经成功,但服务端会收到旧 session 的响应或拒绝它,造成设备与云端结果不一致。请在请求中同时捕获 transport/hello 连接代次(或远端 session_id 快照),并在 handler 返回后要求其仍匹配当前连接再发送。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

PR 收尾总结(2026-08-21)

结论: 当前 PR 已完成代码、实板可验证项和自动化门禁,具备合并条件。Linx 上游额度问题阻断了本轮新增 CRUD 重跑,但没有发现新的仓库代码故障;该边界已单独记录,不能把阻断误报为业务通过。

已完成并有证据

  • SQLite/FATFS:SparkBot 生产分区启动出现 STORAGE_READY=1schema_version=5;此前已用真实语音创建 -> 硬复位 -> 查询验证日程仍在。
  • 语音链路:真实百炼 TTS 注入、普通多轮聊天、字幕/动画状态同步、音频队列门禁、打断抢占和终结语 wake guard 均有实板证据。
  • 定时提醒:修复后的 72f708c 已在实板观察到 tts_started -> tts_first_audio -> tts_stopped、提醒字幕/“说话中”状态和播报后恢复待机,提醒只触发一次。
  • IM 启动与通知:生产固件确认 transport_connectedreadystandby_readyIM_RUNTIME_READY=1IM_PAIRING_READY=1;Gateway 历史投递有 delivered 和微信模板 success 回执。真实公众号新消息 -> 设备动作回传仍未验收。
  • Linx realtime:已确认协议模式含义并按“协议 realtime + 受控抢占”验证;SparkBot 尚无 playback reference/AEC 证据,因此不宣称声学全双工。

最新自动化结果

PR 当前 head:72f708c

  • Host CTest:80/80 passed
  • IM Gateway / Node 24:passed
  • ESP-IDF 6.0 / ESP32-S3:passed
  • CodeQL:C++、Python、TypeScript 全部 passed
  • Codecov:patch C++、patch TypeScript、总覆盖率全部 passed
  • 格式/Python 静态检查、工作流语法、依赖审查:全部 passed
  • git diff --check 通过,工作树干净

Linx 阻断记录

本轮 V3 测试已真实注入 362 个 PCM 帧并完成板端播放状态链路,但 Linx STT 返回 额度不足,请联系灵矽商务,没有产生真实转写或 MCP mutation;5 个 CRUD 步骤完成 0 个。因此 V3 本轮状态为 BLOCKED(上游服务额度),不是 PASS,也不是固件回归。

乱码评论已修正:Async MCP 与当前验收更新

PR 状态:APPROVEDMERGEABLECLEAN。合并前仍需人工确认外部 Linx 额度和公众号闭环是否作为后续验收项继续跟踪。

@ZhaoXingPeng

Copy link
Copy Markdown
Collaborator Author

测试用例:生产 IM Gateway 最近 24 小时日志核对(2026-08-21)

目的: 检查生产网关是否持续运行,并确认最近是否出现公众号入站消息、提醒投递或设备动作回传。

环境与步骤

  • 服务器:root@111.62.156.163:/root/XE6-15
  • 容器:voicelife-im-gateway
  • 读取 docker ps、最近 24 小时容器日志,并核对 pairing、delivery、wechat、message/action 相关事件。

结果

  • 容器状态:Up 17 hours (healthy)
  • healthz 持续返回 HTTP 200。
  • 有新的配对会话创建(HTTP 201)及配对状态轮询(HTTP 200)。
  • 最近 24 小时未观察到新的 wechat.webhook、公众号 message.received、提醒 delivery 或设备 action 回传事件。
  • 观察到一条 device.schedule-query-result.create 返回 HTTP 409 idempotency_conflict。代码和现有网关测试约定:同一 business event ID 携带不同 contract 内容时拒绝冲突请求;当前日志不含请求体,不能判断具体是哪两个 payload,也没有证据表明这是服务崩溃。

验收边界

本次证明生产 Gateway 健康、配对接口可用,并记录了幂等冲突拒绝;不能证明真实公众号新消息 -> Gateway -> 设备动作回传闭环。该闭环仍需 Linx 额度恢复并由真实公众号账号产生一条新消息/动作事件后复测。原始明文日志未提交仓库。

@ZhaoXingPeng
ZhaoXingPeng merged commit a6ad9b4 into 1024XEngineer:main Aug 21, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fix] SparkBot 生产 Profile 接入 SQLite 持久化并核对 Linx realtime

2 participants