✨ feat(im): 实现语音触发的微信公众号绑定用例与 MCP 工具 - #257
Conversation
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
| mcp::PropertyList({mcp::Property("expires_in_minutes", mcp::PropertyType::kInteger, int64_t{10})}), | ||
| [&use_case](const mcp::PropertyList& properties) { | ||
| const int64_t expires = properties.value<int64_t>("expires_in_minutes").value_or(10); | ||
| const im::BindingResult result = use_case.Start(static_cast<int>(expires)); |
There was a problem hiding this comment.
High: This starts a PairingSessionController, but no production path ever calls BindingUseCase::Poll() (the only runtime reference to binding_use_case_ is the Bind() call). Consequently the returned six-digit code is never queried for confirmation or expiry, active() remains true indefinitely, and every later voice request returns already_active until reboot/rebind. Please schedule bounded polling after a successful start, or route this command through the existing pairing task that already drives PairingSessionController::Poll().
| } | ||
|
|
||
| if (im_runtime_.state() == im::ImRuntimeState::kReady) { | ||
| binding_use_case_.Bind(*im_runtime_.pairing_client(), im_pairing_clock_, im_runtime_.user_id()); |
There was a problem hiding this comment.
High: Bind() mutates client_, clock_, user_id_, and controller_ from the IM lifecycle FreeRTOS task, while Start() accesses the same fields from the MCP worker task. There is no synchronization or ownership handoff, so a voice tool call racing IM readiness causes a C++ data race (including a possible concurrent controller_.reset()/dereference). Bind the dependencies before exposing the tool, or protect all BindingUseCase state with a mutex/single-task dispatch.
你的身份你是一名从业12年的四川资深产品经理,深耕硬件配套软件、嵌入式后台、业务系统产品,说话川式直白犀利,绝不留情面,不委婉客套,不照顾情绪,只讲问题、风险、必须改成啥样。 本次输出硬性规则
我会给你输入材料接下来我会粘贴:PRD/需求文档片段、接口文档、业务代码片段。 |
Review 结论直接打回。这个 PR 现在只打通了“创建会话并把六码塞进 MCP output”,没有完成 #235 定义的“显示、播报、轮询确认、终态反馈、断网恢复”闭环。更麻烦的是,现有实现会把会话卡死、存在跨任务数据竞争,还可能在客户端已经超时后继续创建服务端会话。PR 自述的“第一阶段”可以单独立项,但不能拿来关闭 #235。 P0【阻塞:不能上线,必须立刻改,不改直接打回】
P1【严重:可以临时跑,但线上必出事故,本轮迭代必须修复】
P2【优化:功能能跑,但业务逻辑、健壮性、可维护性差,后续版本要整改】
P3【建议:不影响运行,属于经验、规范、可读性层面优化】
材料缺口缺少服务端 Linx/MCP 工具结果消费规则、TTS Prompt、Gateway 配对接口正式 Schema、取消会话接口定义和完整真机日志,无法进一步校验“服务端是否保证播报结构化字段”“服务端残留会话如何清理”“数据库唯一 active binding 是否真实成立”。需要补齐这些材料后才能完成端到端协议审查,不能继续靠设备端自述推断。 验证结果
上线风险总评重灾区是状态闭环、并发安全、超时副作用和需求验收口径。现在上线,最典型结果就是:第一次偶尔生成码但不播报,随后会话永久占用;网络慢时客户端报失败、服务端却偷偷创建成功;IM ready 与语音并发时还有随机崩溃风险。结论只有一个:不能上线,不能关闭 #235,必须先补齐 P0/P1。 |
新增平台无关 BindingUseCase 与 im.binding.start MCP 工具,接通 “语音触发 → 创建配对会话 → 返回六位绑定码 → 服务端 TTS 播报”链路。 - BindingUseCase 封装 PairingSessionController:幂等 Start、手动 Poll、 active/state 查询,复用 1024XEngineer#234 已合入的配对客户端与有限轮询。 - im.binding.start 只返回脱敏 output(status/message/display_code/ expires_at),业务失败映射为可播报文案而非 JSON-RPC error。 - Runtime 在 IM_RUNTIME_READY 后注入配对客户端与 user_id;普通启动 零创建,重复命令返回 already_active 不产生无界会话。 - EspPairingClock 提升为 Runtime 可持有类型。 - 将 voicelife_mcp 工作任务栈从 6144 提升到 32768:配对 HTTPS 在该 worker 内同步完成 TLS 握手与证书链校验,原栈不足会栈溢出崩溃 (LoadProhibited / TLSF heap assertion)。 真机验证:语音绑定码成功生成并回传服务端(如 607824),Gateway 收到 device.pairing.create 201;扩大栈后不再崩溃。 Refs 1024XEngineer#235
im.binding.start 创建会话后无人调用 Poll,active 永久占用导致后续 命令全部返回 already_active;Bind(IM lifecycle 任务)与 Start(MCP worker)并发访问同一对象构成 C++ 数据竞争。本次按 1024XEngineer#257 评审整改: - Runtime 在创建成功后启动有界后台轮询任务,每 3s 推进一次状态机, 轮询到 confirmed/expired/cancelled 等终态后释放会话;任务退出时 上报栈高水位供真机校准。 - BindingUseCase 全部公开方法加同一把互斥锁,Bind/Start/Poll/查询 在三个任务间安全串行。 - already_active 返回当前六位码与到期时间,保证“使用当前绑定码”可执行。 - expires_in_minutes 在 MCP Schema 声明 1~10,越界(含 INT64_MAX 截断 场景)由边界以 kInvalidArgument 拒绝,UseCase 不再静默 clamp。 - 绑定码按 ^[0-9]{6}$ 校验,服务端返回非法码立即 failed 且不进入 active。 - MCP 输出补充稳定 retryable/reason 字段与确定性 speak_text 播报指令。 - StopMcpWorker 等待任务确认退出(上限 5s),未退出时拒绝重建 worker。 主机测试 54/54 通过,ESP-IDF 构建通过。 Refs: 1024XEngineer#257
4741cda to
c3b5864
Compare
binding_use_case_test 的字符串范围循环把 const std::string& 绑定到 临时对象,GCC 13 在 -Werror=range-loop-construct 下报错导致主机测试、 覆盖率与 CodeQL 构建失败;同时对涉及文件执行 clang-format 18 修正 折行(120 列,Google 风格)。 主机测试 54/54 通过;check_format.sh 通过。 Refs: 1024XEngineer#257
结论
本 PR 实现 #235 第一阶段:平台无关的微信公众号绑定用例与
im.binding.startMCP 工具,接通「语音触发 → 创建配对会话 → 返回六位码 → 服务端 TTS 播报」链路。真机已验证绑定码生成、会话创建成功,且六位码经服务端 TTS 字幕在 OLED 显示。评审提出的阻塞问题(会话永不轮询、跨任务数据竞争等)已在c3b5864修复,见「评审修复」。Refs #235
变更
BindingUseCase(voicelife_im):封装PairingSessionController,提供幂等Start、Poll、active/state查询,复用 [IM] 实现设备配对契约、客户端与有限状态轮询 #234 已合入的配对客户端与有限轮询。im.binding.startMCP 工具(voicelife_runtime):只返回脱敏 output(status/reason/retryable/message/display_code/expires_at/speak_text),业务失败映射为可播报文案,而非 JSON-RPC error。IM_RUNTIME_READY后注入配对客户端与user_id;普通启动零创建,重复命令返回携带当前码的already_active,不产生无界会话。EspPairingClock提升为 Runtime 可持有类型。voicelife_mcp工作任务栈 6144 → 32768:配对 HTTPS 在该 worker 内同步完成 TLS 握手与证书链校验,原栈不足会导致栈溢出崩溃(真机验证)。明确未包含:
confirmed后设备主动显示/播报「绑定成功」),属后续 PR。评审修复(c3b5864)
按 #257 评审逐条整改:
voicelife_binding_poll(每 3sPoll至confirmed/expired/cancelled等终态并释放会话),不再永久占用already_active。BindingUseCase全部公开方法加同一把互斥锁,Bind(IM lifecycle 任务)/Start(MCP worker)/Poll(轮询任务)并发安全。expires_in_minutes在 Schema 声明 1~10,越界(含INT64_MAX截断场景)由 MCP 边界以kInvalidArgument拒绝,UseCase 不再clamp。^[0-9]{6}$校验,服务端返回非法码立即failed且不进入active。speak_text(「请在微信公众号发送:绑定 123456」,使用真实码),并补充稳定reason/retryable字段。Refs),本 PR 仅为第一阶段实现。架构与兼容
BindingUseCase是平台无关用例,只依赖ImPairingPort/ImPairingClock/PairingSessionController,不引入微信、Koishi 或 HTTP 类型;公开方法内部以互斥锁串行化。McpServer注册范式(对齐schedule_mcp_tools),参数声明新增整数范围工厂Property::WithIntegerRange。ImPairingClient::Create→PairingSessionController::Begin返回的display_code。验证
./scripts/run_host_tests.sh:54/54 通过(含binding_use_case_test并发压测、im_binding_mcp_tools_test范围/字段契约)clang-format/git diff --check通过idf.py build(ESP-IDF)通过device.pairing.create 201→ 六位码607824生成并回传服务端真机验收记录
ota_1@0x400000)607824在IM_BINDING_DIAG中确认非空并回传tts_sentence_started字幕显示在 OLED(DISPLAY_DRAW text=请在公众号输入607824完成绑定)待办 / 已知问题
confirmed后设备主动显示/播报「绑定成功」属后续 PR;当前后台轮询已实现,终态释放会话。display_code/speak_text的消费。voicelife_binding_poll任务栈(16KB)与 MCP worker 栈(32KB)的 heap 预算需以真机uxTaskGetStackHighWaterMark实测校准(任务退出时已上报高水位)。风险与回退
Review 清单