Skip to content

docs:产品需求调研 - #3

Closed
HuXiaohui424 wants to merge 0 commit into
1024XEngineer:mainfrom
HuXiaohui424:main
Closed

docs:产品需求调研#3
HuXiaohui424 wants to merge 0 commit into
1024XEngineer:mainfrom
HuXiaohui424:main

Conversation

@HuXiaohui424

@HuXiaohui424 HuXiaohui424 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

产品需求调研文档内容

Related to: #10
Related to: #11
Related to: #15

@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 summary:

I found a couple of documentation issues that make the new research note harder to render and validate as project input. No runtime code was changed in this PR.

Comment thread MS1文档/调研.md Outdated
- 优先级

定义事件的优先级(用户手动定义,或可考虑通过 agent 分析人物画像进行定义事件优先级)
1. 重要且紧急

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This nested priority list is not indented under - 优先级, so Markdown renders it as a new top-level ordered list. That makes the following 4. GOOGLE Calendar ambiguous with priority item 4 instead of product item 4. Please indent these four priority lines under the 优先级 bullet or convert the product names to headings so the rendered structure preserves the product hierarchy.

Comment thread MS1文档/调研.md Outdated
- 数据统计

事项、完成度、每个事项占用一天、一周、一年的多少。
[图片]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The document relies on [图片] placeholders for UI references, and the same placeholder appears throughout the file. Since the PR is meant to capture product research, those missing screenshots or links leave the UI observations unverifiable after merge. Please replace the placeholders with actual image assets, stable links, or remove the UI-only entries until the evidence is available.

@Lyshen Lyshen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

2026.07.08 当前时刻,你看完市场之后,观点是什么?

@HuXiaohui424 HuXiaohui424 changed the title 产品需求调研 docs:产品需求调研 Jul 14, 2026
JunLang-7 referenced this pull request in JunLang-7/XE6-15 Jul 16, 2026
回应"模块划分基于流程而非领域内聚"的反馈,做两处结构性调整:

1. Prioritization 并入 Planning:排序(Ranker,纯计算)与重排
   (Replanner,改计划)是同一领域内的两个职责,不拆成对等模块。
   rank 从模块接口变为 Ranker 类方法;"排序只算不改 / 重排才改"
   成为 Planning 模块内部约束。原第四+五节合并为第四节,后续节
   顺延编号(五~八)。

2. GitHub Connection → Connection,端口去 GitHub 前缀
   (AuthorizationProvider / RepositoryCatalog / MilestoneCatalog /
    TaskReader 等),GitHub 只作为 Infra 适配器名出现
   (GitHubOAuthAdapter / GitHubTaskReaderAdapter 等)。为未来更换
   Provider 保留空间,符合"领域不把 GitHub 内化进模型"。
   GitHubAccount → Account;GitHubTaskDelta → SourceDelta;
   事件 GitHubConnected/Disconnected → Connected/Disconnected。

模块按领域内聚划分(各自守护自己的数据与不变量),不按产品交互
流程划分。各节加领域句(English 一句话)+ 类清单。运行时流水线
不变(Connection → Task Pool → Planning 的 Ranker→Replanner)。

同步更新:第一节 mermaid 模块图(3 领域)、第五节 Coordinator
流程与差异化表、第六节不变量(#3 改为 Planning 内部约束、新增 1024XEngineer#16
去 Provider 化)、第七节 App Shell façade、第八节 Infra 端口映射。

Refs: 1024XEngineer#39
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#21024XEngineer#3。

主机测试 10/10 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit to ZhaoXingPeng/XE6-15 that referenced this pull request Aug 6, 2026
Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

这是 fennoai 审查 1024XEngineer#106 标记的阻塞项 1024XEngineer#21024XEngineer#3。

主机测试 10/10 通过。

Refs 1024XEngineer#106
ZhaoXingPeng added a commit that referenced this pull request Aug 6, 2026
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架

建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。

* 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次

* ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证

Linx 服务端可能在重连 hello 中返回不同的下行采样率或帧时长;如果继续沿用旧的播放配置,会把协议已变更的音频静默送进错误的 AudioOutput。物理链路在 hello 失败但尚未完成清理时也不能被重复 Connect。

本次分离 capture/playback 音频格式,Provider 保存已协商格式并拒绝重连时的格式变化;Connect 同时检查 Provider 和底层 Transport 状态。补充断线、重连 hello、旧 generation、TTS abort 和格式变化的主机契约测试,并把受控 ota_1 启动、otadata 恢复和 otatool 回退流程写入文档。

验证:./scripts/run_checks.sh(主机 9/9、架构、Profile、Python 4/4);ESP-IDF 6.0.2 / ESP32-S3 构建通过,voicelife.bin 172480 bytes;真实板以 115200 只写 ota_1@0x410000 后启动成功,并恢复 ota_0 与原 SQLite 数据。

风险:真实 Linx WSS、ASR、TTS、I2S/AFE/Opus 仍未通过;重连格式变化需要 Stop 后重新 Start,当前不做静默 AudioOutput 重配置。

Upstream: 78/xiaozhi-esp32@dd99da0

Refs #107

Refs #91

* 🐛 fix(test): 显式初始化语音事件测试夹具

GitHub GCC 13 在 -Wmissing-field-initializers -Werror 下拒绝 VoiceEvent 聚合初始化,导致主机测试 job 无法编译。

为 TTS start/stop、断线和重连 fixture 显式补齐 text 与 aborted 字段,保持测试意图不变。

验证:voice_session_contract_test 通过;等待远端完整 CI 复跑。

Refs #107

Refs #91

* ✨ feat(voice): 固化有界音频队列与迁移边界

把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91

* ✨ feat(voice): 接入会话采集音频回调契约

参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。

Refs #107
Refs #105
Refs #91

* 📝 docs(voice): 刷新官方接入与跨板能力边界

记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91

* 🐛 fix(docs): 移除失效的 Zephyr 音频来源

跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91

* 👷 ci(quality): 统一 Linx ESP 传输格式

补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。

* ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试

- 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径
- 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界
- 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径

* 📝 docs(process): 将研究资料迁移到 Issue 归档

* 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表

sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB
sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB
新增 partitions_voicelife_16mb.csv:
  nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB

实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时
spi_flash 警告并浪费 14MB 空间。

Refs #106

* ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动

将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为
新 VoiceSession + SpeechProviderRegistry 架构:

- 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider
  实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter)
- Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry
- Start() 从 Registry 创建 Provider 并构造 VoiceSession
- 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold

这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径
上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。
现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷

Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

这是 fennoai 审查 #106 标记的阻塞项 #2#3。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限

能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。
之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。

帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。
AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload,
防止畸形帧或攻击性 payload 压爆嵌入式内存。

这是 fennoai 审查 #106 标记的阻塞项 #7#8#16#22。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷

BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值,
回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。

Linx OnText: 解码后校验 session_id 与当前会话匹配,
拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。

EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出,
不再把 PCM 专用字段硬编码进 Opus hello。

Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一
输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。

这是 fennoai 审查 #106 标记的阻塞项 #5#14#17#20。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长

Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。

Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式,
避免协商结果被错误信任。

play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50,
格式变化时自动缩放缓冲时长。

  Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。

这是 fennoai 审查 #106 标记的阻塞项 #12#15#21。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界

UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。
支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。
解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。

FindField: 匹配字段名前先验证前驱字符为 { 或 ,,
避免把值里的同名字符串误判为对象键。

Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。

这是 fennoai 审查 #106 标记的阻塞项 #11#18#19(部分缓解)。

主机测试 10/10 通过。

Refs #106

* ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析

third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。

LinxJsonCodec 全面重写:
  Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted
  DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过
    cJSON_GetObjectItem 按类型读取
  删除: ReadJsonString, SkipSpace, FindField, ReadStringField,
    ReadUnsignedField, ReadBoolField, ReadObjectField, Quote
    (~300行手动解析逻辑)
  保留: CodecName, ModeName

主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson
ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c

修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持
  cJSON_False|cJSON_True 组合查询。

这是 fennoai 审查 #106 标记的阻塞项 #18#19。

主机测试 10/10 通过。
ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。

Refs #106

* 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验

Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少
channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。

主机测试 17/17 通过。

Refs #106

* 🔧 build(style): clang-format 修复 CI 格式检查

格式化 voice_ports.h, voice_provider_registry.cc,
runtime.cc, voice_session.cc, linx_json_codec.cc,
linx_speech_provider.cc 以通过 CI clang-format 门禁。

Refs #108

* 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释

AudioInputPort, AudioOutputPort, VoiceTransportPort,
CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter,
SpeechProviderAdapter, SpeechProviderRegistry 全部公开
类型和函数补上 /// Doxygen 注释。

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释

AudioInputPort, AudioOutputPort, VoiceTransportPort,
SpeechProviderPort, CodecStrategy, ASRAdapter, TTSAdapter,
RealtimeAdapter 所有公开析构函数和方法补上 /// 注释。

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen

* 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式

全部 33 个公开头文件通过 check_public_api_docs.py 校验。

Refs #108

* 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists

修复与 main 合并后丢失的注释:
- voice_types.h, voice_session.h, audio_frame_queue.h 补中文 Doxygen
- linx_types.h, websocket_fragment_assembler.h 恢复 Doxygen 版本
- CapabilityProfile::Has 改为 std::find, 避免公共 API 检查器误报

tests/host/CMakeLists.txt 清理重复的 linx_esp 定义。

主机测试 25/25 通过, Doxygen 33 头文件 PASS。

Refs #108

* 🔧 build(style): clang-format 修复 voice_session_contract_test.cc

Refs #108
ZhaoXingPeng added a commit that referenced this pull request Aug 6, 2026
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架

建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。

* 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次

* ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证

Linx 服务端可能在重连 hello 中返回不同的下行采样率或帧时长;如果继续沿用旧的播放配置,会把协议已变更的音频静默送进错误的 AudioOutput。物理链路在 hello 失败但尚未完成清理时也不能被重复 Connect。

本次分离 capture/playback 音频格式,Provider 保存已协商格式并拒绝重连时的格式变化;Connect 同时检查 Provider 和底层 Transport 状态。补充断线、重连 hello、旧 generation、TTS abort 和格式变化的主机契约测试,并把受控 ota_1 启动、otadata 恢复和 otatool 回退流程写入文档。

验证:./scripts/run_checks.sh(主机 9/9、架构、Profile、Python 4/4);ESP-IDF 6.0.2 / ESP32-S3 构建通过,voicelife.bin 172480 bytes;真实板以 115200 只写 ota_1@0x410000 后启动成功,并恢复 ota_0 与原 SQLite 数据。

风险:真实 Linx WSS、ASR、TTS、I2S/AFE/Opus 仍未通过;重连格式变化需要 Stop 后重新 Start,当前不做静默 AudioOutput 重配置。

Upstream: 78/xiaozhi-esp32@dd99da0

Refs #107

Refs #91

* 🐛 fix(test): 显式初始化语音事件测试夹具

GitHub GCC 13 在 -Wmissing-field-initializers -Werror 下拒绝 VoiceEvent 聚合初始化,导致主机测试 job 无法编译。

为 TTS start/stop、断线和重连 fixture 显式补齐 text 与 aborted 字段,保持测试意图不变。

验证:voice_session_contract_test 通过;等待远端完整 CI 复跑。

Refs #107

Refs #91

* ✨ feat(voice): 固化有界音频队列与迁移边界

把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91

* ✨ feat(voice): 接入会话采集音频回调契约

参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。

Refs #107
Refs #105
Refs #91

* 📝 docs(voice): 刷新官方接入与跨板能力边界

记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91

* 🐛 fix(docs): 移除失效的 Zephyr 音频来源

跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91

* 👷 ci(quality): 统一 Linx ESP 传输格式

补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。

* ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试

- 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径
- 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界
- 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径

* 📝 docs(process): 将研究资料迁移到 Issue 归档

* 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表

sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB
sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB
新增 partitions_voicelife_16mb.csv:
  nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB

实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时
spi_flash 警告并浪费 14MB 空间。

Refs #106

* ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动

将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为
新 VoiceSession + SpeechProviderRegistry 架构:

- 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider
  实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter)
- Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry
- Start() 从 Registry 创建 Provider 并构造 VoiceSession
- 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold

这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径
上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。
现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷

Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

这是 fennoai 审查 #106 标记的阻塞项 #2#3。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限

能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。
之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。

帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。
AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload,
防止畸形帧或攻击性 payload 压爆嵌入式内存。

这是 fennoai 审查 #106 标记的阻塞项 #7#8#16#22。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷

BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值,
回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。

Linx OnText: 解码后校验 session_id 与当前会话匹配,
拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。

EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出,
不再把 PCM 专用字段硬编码进 Opus hello。

Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一
输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。

这是 fennoai 审查 #106 标记的阻塞项 #5#14#17#20。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长

Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。

Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式,
避免协商结果被错误信任。

play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50,
格式变化时自动缩放缓冲时长。

  Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。

这是 fennoai 审查 #106 标记的阻塞项 #12#15#21。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界

UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。
支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。
解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。

FindField: 匹配字段名前先验证前驱字符为 { 或 ,,
避免把值里的同名字符串误判为对象键。

Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。

这是 fennoai 审查 #106 标记的阻塞项 #11#18#19(部分缓解)。

主机测试 10/10 通过。

Refs #106

* ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析

third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。

LinxJsonCodec 全面重写:
  Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted
  DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过
    cJSON_GetObjectItem 按类型读取
  删除: ReadJsonString, SkipSpace, FindField, ReadStringField,
    ReadUnsignedField, ReadBoolField, ReadObjectField, Quote
    (~300行手动解析逻辑)
  保留: CodecName, ModeName

主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson
ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c

修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持
  cJSON_False|cJSON_True 组合查询。

这是 fennoai 审查 #106 标记的阻塞项 #18#19。

主机测试 10/10 通过。
ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。

Refs #106

* 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验

Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少
channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。

主机测试 17/17 通过。

Refs #106

* 🔧 build(style): clang-format 修复 CI 格式检查

格式化 voice_ports.h, voice_provider_registry.cc,
runtime.cc, voice_session.cc, linx_json_codec.cc,
linx_speech_provider.cc 以通过 CI clang-format 门禁。

Refs #108

* 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释

AudioInputPort, AudioOutputPort, VoiceTransportPort,
CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter,
SpeechProviderAdapter, SpeechProviderRegistry 全部公开
类型和函数补上 /// Doxygen 注释。

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释

AudioInputPort, AudioOutputPort, VoiceTransportPort,
SpeechProviderPort, CodecStrategy, ASRAdapter, TTSAdapter,
RealtimeAdapter 所有公开析构函数和方法补上 /// 注释。

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen

* 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式

全部 33 个公开头文件通过 check_public_api_docs.py 校验。

Refs #108

* 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists

修复与 main 合并后丢失的注释:
- voice_types.h, voice_session.h, audio_frame_queue.h 补中文 Doxygen
- linx_types.h, websocket_fragment_assembler.h 恢复 Doxygen 版本
- CapabilityProfile::Has 改为 std::find, 避免公共 API 检查器误报

tests/host/CMakeLists.txt 清理重复的 linx_esp 定义。

主机测试 25/25 通过, Doxygen 33 头文件 PASS。

Refs #108

* ✨ feat(voice): 建立 ESP32-S3 PCM/I2S 板级探针

将板级 GPIO、I2C 地址、设备 PCM 格式和 DMA 预算收敛到独立 Profile,并把探针编排放回 Runtime 组装根。探针只验证 I2C ACK、I2S 通道生命周期和有限静音读写,不把 Codec 录放或云端闭环伪装成已完成。

主机契约测试 11/11、Python 测试 4/4、架构检查和 ESP-IDF 6.0.2 的 ESP32-S3 构建均通过。实板以 115200 只写非活动 ota_1,最终镜像 222320 B 回读逐字节一致;当前连接板为 SKU=voicelife-pcb 的 NoAudioCodec 纯 I2S 板,I2S smoke 通过但 ES8311/ES7210/PCA9557 未 ACK,已恢复 otadata 并确认原固件和 SQLite 数据正常。后续必须为纯 I2S 板与 Lichuang Codec 板分别建立 Profile。

Refs #109

* 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释

audio_board_profile.h, esp32s3_audio_probe.h 公开 API
补齐 /** @brief @PARAM @return */ 注释。

Doxygen 35 头文件 PASS。

Refs #109

* 🏗️ refactor(voice): 接入 voicelife-pcb 纯 I2S PCM Profile

当前实板原固件报告 SKU=voicelife-pcb/NoAudioCodec,不能复用 Lichuang Codec Profile。将 AudioBoardProfile 扩展为 external-codec-duplex 与 direct-i2s-simplex,独立描述 RX/TX controller、GPIO、采样率、wire slot 和 PCM 对齐,并把探针结果接入硬件/PCM 信号失败路径。

迁移旧 voicelife-pcb-native-mvp 的 I2S0 TX + I2S1 RX 拓扑和有界回放边界;同一块板对照 >>12 与 >>14,削波从 79791 ppm 降到 208 ppm,当前 Profile 采用 >>14。主机 11/11、Python 测试、Profile validate、架构检查和 ESP-IDF 6.0.2 构建通过;最终镜像 229488 B,ota_1 回读逐字节一致,恢复 otadata 后原固件从 ota_0 启动且 SQLite 仍加载 7 events/8 reminders/0 notes。

仍未宣称 Codec、AFE、AEC、WakeNet、Opus、WSS、ASR、TTS 或声学播放闭环。

Upstream: 78/xiaozhi-esp32@dd99da0

Refs #111

* 📝 docs(audio): 补 esp32s3_audio_probe.h Impl Doxygen

Refs #111

* 🐛 fix(audio): 同步 audio_esp 源文件与新版头文件字段

audio_board_profile.cc, esp32s3_audio_probe.cc 使用 capture_i2s/
playback_i2s 字段; audio_board_profile.h, esp32s3_audio_probe.h
补齐中文 Doxygen 注释。

主机测试 26/26 通过, Doxygen PASS。

Refs #109

* 🔧 build(style): clang-format 修复 audio_esp 与测试文件

runtime.cc, audio_board_profile_contract_test.cc,
voice_session_contract_test.cc 等通过 clang-format-18。

Refs #109
ZhaoXingPeng added a commit that referenced this pull request Aug 6, 2026
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架

建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。

* 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次

* ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证

Linx 服务端可能在重连 hello 中返回不同的下行采样率或帧时长;如果继续沿用旧的播放配置,会把协议已变更的音频静默送进错误的 AudioOutput。物理链路在 hello 失败但尚未完成清理时也不能被重复 Connect。

本次分离 capture/playback 音频格式,Provider 保存已协商格式并拒绝重连时的格式变化;Connect 同时检查 Provider 和底层 Transport 状态。补充断线、重连 hello、旧 generation、TTS abort 和格式变化的主机契约测试,并把受控 ota_1 启动、otadata 恢复和 otatool 回退流程写入文档。

验证:./scripts/run_checks.sh(主机 9/9、架构、Profile、Python 4/4);ESP-IDF 6.0.2 / ESP32-S3 构建通过,voicelife.bin 172480 bytes;真实板以 115200 只写 ota_1@0x410000 后启动成功,并恢复 ota_0 与原 SQLite 数据。

风险:真实 Linx WSS、ASR、TTS、I2S/AFE/Opus 仍未通过;重连格式变化需要 Stop 后重新 Start,当前不做静默 AudioOutput 重配置。

Upstream: 78/xiaozhi-esp32@dd99da0

Refs #107

Refs #91

* 🐛 fix(test): 显式初始化语音事件测试夹具

GitHub GCC 13 在 -Wmissing-field-initializers -Werror 下拒绝 VoiceEvent 聚合初始化,导致主机测试 job 无法编译。

为 TTS start/stop、断线和重连 fixture 显式补齐 text 与 aborted 字段,保持测试意图不变。

验证:voice_session_contract_test 通过;等待远端完整 CI 复跑。

Refs #107

Refs #91

* ✨ feat(voice): 固化有界音频队列与迁移边界

把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91

* ✨ feat(voice): 接入会话采集音频回调契约

参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。

Refs #107
Refs #105
Refs #91

* 📝 docs(voice): 刷新官方接入与跨板能力边界

记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91

* 🐛 fix(docs): 移除失效的 Zephyr 音频来源

跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91

* 👷 ci(quality): 统一 Linx ESP 传输格式

补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。

* ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试

- 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径
- 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界
- 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径

* 📝 docs(process): 将研究资料迁移到 Issue 归档

* 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表

sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB
sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB
新增 partitions_voicelife_16mb.csv:
  nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB

实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时
spi_flash 警告并浪费 14MB 空间。

Refs #106

* ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动

将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为
新 VoiceSession + SpeechProviderRegistry 架构:

- 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider
  实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter)
- Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry
- Start() 从 Registry 创建 Provider 并构造 VoiceSession
- 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold

这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径
上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。
现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷

Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

这是 fennoai 审查 #106 标记的阻塞项 #2#3。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限

能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。
之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。

帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。
AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload,
防止畸形帧或攻击性 payload 压爆嵌入式内存。

这是 fennoai 审查 #106 标记的阻塞项 #7#8#16#22。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷

BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值,
回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。

Linx OnText: 解码后校验 session_id 与当前会话匹配,
拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。

EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出,
不再把 PCM 专用字段硬编码进 Opus hello。

Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一
输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。

这是 fennoai 审查 #106 标记的阻塞项 #5#14#17#20。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长

Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。

Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式,
避免协商结果被错误信任。

play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50,
格式变化时自动缩放缓冲时长。

  Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。

这是 fennoai 审查 #106 标记的阻塞项 #12#15#21。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界

UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。
支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。
解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。

FindField: 匹配字段名前先验证前驱字符为 { 或 ,,
避免把值里的同名字符串误判为对象键。

Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。

这是 fennoai 审查 #106 标记的阻塞项 #11#18#19(部分缓解)。

主机测试 10/10 通过。

Refs #106

* ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析

third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。

LinxJsonCodec 全面重写:
  Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted
  DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过
    cJSON_GetObjectItem 按类型读取
  删除: ReadJsonString, SkipSpace, FindField, ReadStringField,
    ReadUnsignedField, ReadBoolField, ReadObjectField, Quote
    (~300行手动解析逻辑)
  保留: CodecName, ModeName

主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson
ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c

修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持
  cJSON_False|cJSON_True 组合查询。

这是 fennoai 审查 #106 标记的阻塞项 #18#19。

主机测试 10/10 通过。
ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。

Refs #106

* 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验

Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少
channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。

主机测试 17/17 通过。

Refs #106

* 🔧 build(style): clang-format 修复 CI 格式检查

格式化 voice_ports.h, voice_provider_registry.cc,
runtime.cc, voice_session.cc, linx_json_codec.cc,
linx_speech_provider.cc 以通过 CI clang-format 门禁。

Refs #108

* 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释

AudioInputPort, AudioOutputPort, VoiceTransportPort,
CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter,
SpeechProviderAdapter, SpeechProviderRegistry 全部公开
类型和函数补上 /// Doxygen 注释。

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释

AudioInputPort, AudioOutputPort, VoiceTransportPort,
SpeechProviderPort, CodecStrategy, ASRAdapter, TTSAdapter,
RealtimeAdapter 所有公开析构函数和方法补上 /// 注释。

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen

* 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式

全部 33 个公开头文件通过 check_public_api_docs.py 校验。

Refs #108

* 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists

修复与 main 合并后丢失的注释:
- voice_types.h, voice_session.h, audio_frame_queue.h 补中文 Doxygen
- linx_types.h, websocket_fragment_assembler.h 恢复 Doxygen 版本
- CapabilityProfile::Has 改为 std::find, 避免公共 API 检查器误报

tests/host/CMakeLists.txt 清理重复的 linx_esp 定义。

主机测试 25/25 通过, Doxygen 33 头文件 PASS。

Refs #108

* ✨ feat(voice): 建立 ESP32-S3 PCM/I2S 板级探针

将板级 GPIO、I2C 地址、设备 PCM 格式和 DMA 预算收敛到独立 Profile,并把探针编排放回 Runtime 组装根。探针只验证 I2C ACK、I2S 通道生命周期和有限静音读写,不把 Codec 录放或云端闭环伪装成已完成。

主机契约测试 11/11、Python 测试 4/4、架构检查和 ESP-IDF 6.0.2 的 ESP32-S3 构建均通过。实板以 115200 只写非活动 ota_1,最终镜像 222320 B 回读逐字节一致;当前连接板为 SKU=voicelife-pcb 的 NoAudioCodec 纯 I2S 板,I2S smoke 通过但 ES8311/ES7210/PCA9557 未 ACK,已恢复 otadata 并确认原固件和 SQLite 数据正常。后续必须为纯 I2S 板与 Lichuang Codec 板分别建立 Profile。

Refs #109

* 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释

audio_board_profile.h, esp32s3_audio_probe.h 公开 API
补齐 /** @brief @PARAM @return */ 注释。

Doxygen 35 头文件 PASS。

Refs #109

* 🏗️ refactor(voice): 接入 voicelife-pcb 纯 I2S PCM Profile

当前实板原固件报告 SKU=voicelife-pcb/NoAudioCodec,不能复用 Lichuang Codec Profile。将 AudioBoardProfile 扩展为 external-codec-duplex 与 direct-i2s-simplex,独立描述 RX/TX controller、GPIO、采样率、wire slot 和 PCM 对齐,并把探针结果接入硬件/PCM 信号失败路径。

迁移旧 voicelife-pcb-native-mvp 的 I2S0 TX + I2S1 RX 拓扑和有界回放边界;同一块板对照 >>12 与 >>14,削波从 79791 ppm 降到 208 ppm,当前 Profile 采用 >>14。主机 11/11、Python 测试、Profile validate、架构检查和 ESP-IDF 6.0.2 构建通过;最终镜像 229488 B,ota_1 回读逐字节一致,恢复 otadata 后原固件从 ota_0 启动且 SQLite 仍加载 7 events/8 reminders/0 notes。

仍未宣称 Codec、AFE、AEC、WakeNet、Opus、WSS、ASR、TTS 或声学播放闭环。

Upstream: 78/xiaozhi-esp32@dd99da0

Refs #111

* 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释

audio_board_profile.h, esp32s3_audio_probe.h 公开 API
补齐 /** @brief @PARAM @return */ 注释与 Impl 说明。

Doxygen 35 头文件 PASS。

Refs #111

* 🔧 build(style): clang-format 修复 audio_esp 与测试文件

Refs #111
ZhaoXingPeng added a commit that referenced this pull request Aug 6, 2026
* ✨ feat(voice): 接入 ESP32-S3 Linx WSS 传输骨架

建立平台无关的 WebSocket 分片重组与 generation 隔离,增加 ESP-IDF 6.0.2 的 WSS/TLS Transport、凭据解析边界、固定事件队列和 Linx hello 超时。同步主机 TDD、架构边界和语音模块文档,明确真实板闭环仍待 Issue #107 验收。

* 🐛 fix(voice): 固化 MVP 音频经验并保护会话代次

* ✨ feat(voice): 完善 Linx 双向协商与 ESP32-S3 回退验证

Linx 服务端可能在重连 hello 中返回不同的下行采样率或帧时长;如果继续沿用旧的播放配置,会把协议已变更的音频静默送进错误的 AudioOutput。物理链路在 hello 失败但尚未完成清理时也不能被重复 Connect。

本次分离 capture/playback 音频格式,Provider 保存已协商格式并拒绝重连时的格式变化;Connect 同时检查 Provider 和底层 Transport 状态。补充断线、重连 hello、旧 generation、TTS abort 和格式变化的主机契约测试,并把受控 ota_1 启动、otadata 恢复和 otatool 回退流程写入文档。

验证:./scripts/run_checks.sh(主机 9/9、架构、Profile、Python 4/4);ESP-IDF 6.0.2 / ESP32-S3 构建通过,voicelife.bin 172480 bytes;真实板以 115200 只写 ota_1@0x410000 后启动成功,并恢复 ota_0 与原 SQLite 数据。

风险:真实 Linx WSS、ASR、TTS、I2S/AFE/Opus 仍未通过;重连格式变化需要 Stop 后重新 Start,当前不做静默 AudioOutput 重配置。

Upstream: 78/xiaozhi-esp32@dd99da0

Refs #107

Refs #91

* 🐛 fix(test): 显式初始化语音事件测试夹具

GitHub GCC 13 在 -Wmissing-field-initializers -Werror 下拒绝 VoiceEvent 聚合初始化,导致主机测试 job 无法编译。

为 TTS start/stop、断线和重连 fixture 显式补齐 text 与 aborted 字段,保持测试意图不变。

验证:voice_session_contract_test 通过;等待远端完整 CI 复跑。

Refs #107

Refs #91

* ✨ feat(voice): 固化有界音频队列与迁移边界

把旧 MVP 中的队列满载策略、generation 清理和水位统计落成平台无关契约,新增主机 TDD 覆盖。同步 Linx、ESP32-S3、小智 AFE 与 SQLite 控制面研究证据,明确其他板卡仅按 Profile 准入。\n\nRefs #105\nRefs #91

* ✨ feat(voice): 接入会话采集音频回调契约

参考旧 voicelife-pcb-native-mvp 的采集任务边界,让 AudioInputPort 只提交格式和负载,由 VoiceSession 统一补齐 generation 与 sequence。补充 TDD 覆盖迟到帧拒绝、回调清理和格式校验,并同步语音架构与研究决策文档。

Refs #107
Refs #105
Refs #91

* 📝 docs(voice): 刷新官方接入与跨板能力边界

记录 Linx MQTT 公开资料缺口、ESP-IDF 6.0.2 I2S/PDM 约束、跨板能力矩阵和立创板迁移输入。\n\n明确当前只验证研究与契约,不把旧 MVP 的 Codec 引脚或芯片能力冒充新工程实板录放通过。\n\nRefs #107\nRefs #105\nRefs #91

* 🐛 fix(docs): 移除失效的 Zephyr 音频来源

跨板矩阵事实仍以 ESP-IDF 6.0.2 I2S 官方文档为主,Zephyr 仅保留已核对的文件系统抽象参考。\n\nRefs #107\nRefs #105\nRefs #91

* 👷 ci(quality): 统一 Linx ESP 传输格式

补齐新增语音端口的 clang-format-18 结果,保持 WebSocket 分片和会话逻辑不变。

* ✅ test(voice): 补充 Linx Codec 与 Provider Registry 错误路径测试

- 覆盖 hello 无效音频、空 abort、非 JSON、未知 tts 状态等拒绝路径
- 覆盖 \u 转义拒绝、opus/wav 音频参数解析边界
- 覆盖 Registry 空 ID、重复注册、未找到与缺能力路径

* 📝 docs(process): 将研究资料迁移到 Issue 归档

* 🔧 build(config): 修正 Flash 配置为 16MB 并启用双 OTA 分区表

sdkconfig: CONFIG_ESPTOOLPY_FLASHSIZE 2MB → 16MB
sdkconfig.defaults: SINGLE_APP_LARGE → CUSTOM + 16MB
新增 partitions_voicelife_16mb.csv:
  nvs 24KB + otadata 8KB + ota_0 4MB + ota_1 4MB + voicelife 2MB

实板验证: ESP32-S3 16MB Flash, 之前配成 2MB 导致启动时
spi_flash 警告并浪费 14MB 空间。

Refs #106

* ♻️ refactor(runtime): 从 Scaffold 硬编码切换为 Provider Registry 驱动

将 Runtime 从旧 VoiceSessionCoordinator + ScaffoldAdapters 重构为
新 VoiceSession + SpeechProviderRegistry 架构:

- 新增 ScaffoldAudioInput/ScaffoldAudioOutput/ScaffoldSpeechProvider
  实现新端口接口 (AudioInputPort/AudioOutputPort/SpeechProviderAdapter)
- Runtime 构造时注册 scaffold Provider 到 SpeechProviderRegistry
- Start() 从 Registry 创建 Provider 并构造 VoiceSession
- 移除旧 AudioDevicePort/SpeechProviderPort/硬编码 scaffold

这是 fennoai 审查 #106 标记的阻塞项 #1: 之前新语音架构在启动路径
上是死代码, Runtime 从未使用 VoiceSession 或 SpeechProviderRegistry。
现在架构主干真正串联, Scaffold Provider 可被真实 Adapter 替换。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Interrupt 和 EndCapture 的状态机缺陷

Interrupt: generation 递增从 Abort/Flush 之后移到之前。
旧顺序下迟到的音频帧在 Abort→Flush 窗口内仍可进入会话;
新顺序先失效旧代次, Provider/Output 拒绝后续的旧帧。

EndCapture: provider_.StopCapture() 失败时不再静默保留
kCapturing 状态(此时本地输入已停止, 状态分裂)。
改为显式转 kFailed 并上报 capture_stop_failed 证据。

这是 fennoai 审查 #106 标记的阻塞项 #2#3。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 去掉虚假 opus 声明,添加上下行帧大小上限

能力声明: DefaultCapabilities 移除未实现的 opus, 只保留 pcm。
之前 Registry 会按虚假能力选到 Linx Provider, 运行时在音频链路炸掉。

帧校验: AudioFrame 新增 kMaxPayloadBytes=16384。
AcceptFrameLocked、HandleInputAudio、HandleAudio 三层都拒绝超大 payload,
防止畸形帧或攻击性 payload 压爆嵌入式内存。

这是 fennoai 审查 #106 标记的阻塞项 #7#8#16#22。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 BeginCapture 回滚、session_id 校验和 JSON 编解码缺陷

BeginCapture: input 启动失败后不再忽略 provider_.StopCapture() 返回值,
回滚失败时 emit evidence 并优先返回回滚错误, 避免云端残留半开采集状态。

Linx OnText: 解码后校验 session_id 与当前会话匹配,
拒绝同 WebSocket 上的旧会话或误路由消息污染当前状态。

EncodeHello: sample_format/frame_size/play_buffer_duration 仅 PCM 时输出,
不再把 PCM 专用字段硬编码进 Opus hello。

Quote: 补全 \b \f 转义, 其余 ASCII 控制字符 (0x00-0x1F) 统一
输出 \u00XX 六位 hex 序列, 避免产出非法 JSON。

这是 fennoai 审查 #106 标记的阻塞项 #5#14#17#20。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): 修复 Registry 上限、hello 缺少 audio_params 和硬编码缓冲时长

Registry: kMaxProviders 8→16, 满表错误含上限值和诊断提示。

Linx hello: 服务端 hello 不含 audio_params 时拒绝而非静默使用默认格式,
避免协商结果被错误信任。

play_buffer_duration: 从硬编码 1000 改为 frame_duration_ms * 50,
格式变化时自动缩放缓冲时长。

  Connect() 已阻塞等待 hello_cv_, hello_timeout_ms 已驱动 wait_for。

这是 fennoai 审查 #106 标记的阻塞项 #12#15#21。

主机测试 10/10 通过。

Refs #106

* 🐛 fix(voice): JSON codec 支持 \uXXXX Unicode 转义并加固字段边界

UnescapeJsonString: 新增 \uXXXX 四字节 hex 解码为 UTF-8。
支持 BMP 范围 (U+0000-U+FFFF), 拒绝代理对 (\uD800-\uDFFF)。
解决了此前中文 ASR/TTS 文本因 \uXXXX 转义导致解析失败的问题。

FindField: 匹配字段名前先验证前驱字符为 { 或 ,,
避免把值里的同名字符串误判为对象键。

Registry: 文档化线程安全约束 -- Register() 必须在调度启动前完成。

这是 fennoai 审查 #106 标记的阻塞项 #11#18#19(部分缓解)。

主机测试 10/10 通过。

Refs #106

* ♻️ refactor(voice): LinxJsonCodec 从字符串扫描替换为 cJSON 结构化解析

third_party/cjson: 引入 cJSON (MIT license) 源码, ESP-IDF 和主机测试共用。

LinxJsonCodec 全面重写:
  Encode*: std::ostringstream → cJSON_CreateObject + cJSON_PrintUnformatted
  DecodeText: 字符串扫描 → cJSON_ParseWithLength, 字段通过
    cJSON_GetObjectItem 按类型读取
  删除: ReadJsonString, SkipSpace, FindField, ReadStringField,
    ReadUnsignedField, ReadBoolField, ReadObjectField, Quote
    (~300行手动解析逻辑)
  保留: CodecName, ModeName

主机测试: CMakeLists 新增 cjson 库 (LANGUAGES C CXX), linx 链接 cjson
ESP-IDF: voicelife_linx 直接编译 third_party/cjson/cJSON.c

修复: GetRequired/GetOptional 类型匹配改为位掩码 (&) 以支持
  cJSON_False|cJSON_True 组合查询。

这是 fennoai 审查 #106 标记的阻塞项 #18#19。

主机测试 10/10 通过。
ESP-IDF 编译 voicelife.bin 181KB, 实板 16MB Flash 正常启动。

Refs #106

* 🐛 fix(voice): cJSON ParseAudioParams 补回 channels=0 等范围校验

Rebase 后远程新增错误路径测试, cJSON 版 ParseAudioParams 缺少
channels/bits/duration 零值拒绝。补回与旧代码等价的范围校验。

主机测试 17/17 通过。

Refs #106

* 🔧 build(style): clang-format 修复 CI 格式检查

格式化 voice_ports.h, voice_provider_registry.cc,
runtime.cc, voice_session.cc, linx_json_codec.cc,
linx_speech_provider.cc 以通过 CI clang-format 门禁。

Refs #108

* 📝 docs(voice): 补齐 voice_ports.h 公开 API Doxygen 文档注释

AudioInputPort, AudioOutputPort, VoiceTransportPort,
CodecStrategy, ASRAdapter, TTSAdapter, RealtimeAdapter,
SpeechProviderAdapter, SpeechProviderRegistry 全部公开
类型和函数补上 /// Doxygen 注释。

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补齐所有公开 API 的 Doxygen 注释

AudioInputPort, AudioOutputPort, VoiceTransportPort,
SpeechProviderPort, CodecStrategy, ASRAdapter, TTSAdapter,
RealtimeAdapter 所有公开析构函数和方法补上 /// 注释。

通过 CI 公共 API 文档门禁。

Refs #108

* 📝 docs(voice): 补 SpeechProviderAdapter 析构和 Entry 结构体 Doxygen

* 📝 docs(voice): 重写 voice_ports.h 为中文 Doxygen 格式

全部 33 个公开头文件通过 check_public_api_docs.py 校验。

Refs #108

* 📝 docs(voice): rebase 后补齐头文件 Doxygen 与清理 CMakeLists

修复与 main 合并后丢失的注释:
- voice_types.h, voice_session.h, audio_frame_queue.h 补中文 Doxygen
- linx_types.h, websocket_fragment_assembler.h 恢复 Doxygen 版本
- CapabilityProfile::Has 改为 std::find, 避免公共 API 检查器误报

tests/host/CMakeLists.txt 清理重复的 linx_esp 定义。

主机测试 25/25 通过, Doxygen 33 头文件 PASS。

Refs #108

* ✨ feat(voice): 建立 ESP32-S3 PCM/I2S 板级探针

将板级 GPIO、I2C 地址、设备 PCM 格式和 DMA 预算收敛到独立 Profile,并把探针编排放回 Runtime 组装根。探针只验证 I2C ACK、I2S 通道生命周期和有限静音读写,不把 Codec 录放或云端闭环伪装成已完成。

主机契约测试 11/11、Python 测试 4/4、架构检查和 ESP-IDF 6.0.2 的 ESP32-S3 构建均通过。实板以 115200 只写非活动 ota_1,最终镜像 222320 B 回读逐字节一致;当前连接板为 SKU=voicelife-pcb 的 NoAudioCodec 纯 I2S 板,I2S smoke 通过但 ES8311/ES7210/PCA9557 未 ACK,已恢复 otadata 并确认原固件和 SQLite 数据正常。后续必须为纯 I2S 板与 Lichuang Codec 板分别建立 Profile。

Refs #109

* 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释

audio_board_profile.h, esp32s3_audio_probe.h 公开 API
补齐 /** @brief @PARAM @return */ 注释。

Doxygen 35 头文件 PASS。

Refs #109

* 🏗️ refactor(voice): 接入 voicelife-pcb 纯 I2S PCM Profile

当前实板原固件报告 SKU=voicelife-pcb/NoAudioCodec,不能复用 Lichuang Codec Profile。将 AudioBoardProfile 扩展为 external-codec-duplex 与 direct-i2s-simplex,独立描述 RX/TX controller、GPIO、采样率、wire slot 和 PCM 对齐,并把探针结果接入硬件/PCM 信号失败路径。

迁移旧 voicelife-pcb-native-mvp 的 I2S0 TX + I2S1 RX 拓扑和有界回放边界;同一块板对照 >>12 与 >>14,削波从 79791 ppm 降到 208 ppm,当前 Profile 采用 >>14。主机 11/11、Python 测试、Profile validate、架构检查和 ESP-IDF 6.0.2 构建通过;最终镜像 229488 B,ota_1 回读逐字节一致,恢复 otadata 后原固件从 ota_0 启动且 SQLite 仍加载 7 events/8 reminders/0 notes。

仍未宣称 Codec、AFE、AEC、WakeNet、Opus、WSS、ASR、TTS 或声学播放闭环。

Upstream: 78/xiaozhi-esp32@dd99da0

Refs #111

* 📝 docs(audio): 补齐 audio_esp 头文件中文 Doxygen 注释

audio_board_profile.h, esp32s3_audio_probe.h 公开 API
补齐 /** @brief @PARAM @return */ 注释与 Impl 说明。

Doxygen 35 头文件 PASS。

Refs #111

* ✨ feat(voice): 接入 ESP32-S3 PCM Audio Port

新增硬件 period 到传输帧的组装器、独立采集/投递/播放任务和有界队列,并将 Profile 接入 Runtime。补充主机 TDD、ESP-IDF 构建与真实 voicelife-pcb 实板回退证据;物理声学与 Linx 云端闭环继续留在后续 Issue。

Refs #113

Refs #91

* 🐛 fix(voice): 收紧 Linx 传输生命周期与会话失败回滚

* 🏗️ refactor(voice): 拆分超大语音源文件以通过规模门禁

- esp32s3_pcm_audio_port.cc 670 行拆分为平台无关入口
  + esp32s3_pcm_i2s_runtime.cc(I2S 通道与任务循环)
- linx_json_codec.cc 拆出 linx_json_reader.{h,cc}
- esp_websocket_transport.cc 拆出 impl 与事件处理
- 同步 host 测试 CMakeLists 补齐新源文件

* 🐛 fix(voice): 恢复 Connect/BeginCapture/Stop 失败回滚并补 Doxygen

基于新 #112 重建 PR #114:
- voice_session.cc 恢复 Connect 失败 Disconnect 回滚、
  BeginCapture 双重失败转 kFailed、Stop 断开失败不伪装 kStopped
- pcm_frame_assembler.h, esp32s3_pcm_audio_port.h 补中文 Doxygen

主机测试 27/27 通过, Doxygen PASS, 代码规模 PASS。

Refs #113

* 🔧 build(style): clang-format 修复 audio_esp 与测试文件

Refs #113

* 🔧 chore(build): 移除误提交的 ESP-IDF 构建产物

* 🔧 fix(timing): 链接合并后的主机服务实现
JunLang-7 added a commit that referenced this pull request Aug 7, 2026
* ✨ feat(im): 建立 ReminderActionCommand 设备契约

新增动作命令契约解析与共享 fixture,语义与 TypeScript
ReminderActionCommand 一致:动作类型限制 acknowledge/snooze、
snooze 必须携带 params.minutes、expiresAt 与 occurredAt 严格 ISO。
先写解析测试(RED)后实现,主机测试与双端 wire 契约校验通过。

Refs #127

* ✨ feat(im): 建立 NotificationSubmission 受理结果契约

设备上报通知意图后,网关以 NotificationSubmission 应答,强提醒
携带 actionStream 窗口(reminderTriggerId + expiresAt),弱提醒
不携带。先写解析测试(RED)后实现,覆盖强/弱/非法变体与双端
wire 一致性。

Refs #127

* ✨ feat(im): 建立设备侧动作通道并接入结果回传

新增 ImActionChannel:强提醒窗口内建立临时 SSE,校验 deviceId、
reminderTriggerId 与命令有效期,过期命令回传 expired 终态;以
operationId 去重保证断线重连后相同命令只执行一次,回传复用
operationId 幂等键,Last-Event-ID 仅作流游标不代替业务 ACK。
结果经 ImReportingChannel.SubmitReminderActionResult 回传到
POST /v1/devices/{deviceId}/reminder-actions/{commandId}/result。
先写主机测试(RED)后实现,覆盖过期窗口、归属校验、重放只执行
一次与结果 round-trip。

Refs #127

* ✨ feat(im): 上报通道透传受理结果响应体并提取动作窗口

SubmitNotification 的 ReportResult 携带网关响应体,供调用方解析
NotificationSubmission 的 actionStream;新增 ExtractActionWindow 将
强提醒受理结果映射为动作窗口、弱提醒返回空,据此决定是否建立 SSE。
双端门禁豁免不带 schemaVersion 的 NotificationSubmission 契约。
先写主机测试(RED)后实现。

Refs #127

* ✨ feat(im): 建立设备侧 SSE 动作流传输与时钟适配

SseDecoder 解析 SSE 帧并抽取 reminder.action 载荷,主机测试覆盖
拆帧、心跳注释、CRLF 与 Reset;EspActionStreamTransport 基于
esp_http_client 流式读取 /v1/devices/{deviceId}/reminder-actions/stream,
携带 Authorization 与 Last-Event-ID,读取超时大于 20 秒心跳;
EspClock 提供 ISO-8601 UTC 时间供动作窗口过期判定。先写主机测试
(RED)后实现。

Refs #127

* 🐛 fix(im): 修复 SSE 跨喂入 CRLF 拆帧与 acknowledge 参数校验

SseDecoder 在 \r 落在上次喂入末尾、\n 落在下次喂入开头时会把两处拼成
虚假的空行边界、导致帧被提前切开,新增 trailing_cr_ 跨喂入折叠 CRLF;
ParseReminderActionCommand 拒绝 acknowledge 携带 params,与 TS
parseReminderActionCommand 的拒绝语义对齐。均先写主机测试(RED)。

Refs #127

* ✅ test(im): 补齐动作结果序列化与契约拒绝分支的覆盖

新增 SerializeReminderActionResult 可选字段(nextTriggerAt/errorCode/
details)序列化测试,经动作通道回传路径覆盖 AppendJsonValue 全部值种类;
为 ReminderActionCommand 与 NotificationSubmission 解析器补齐字段缺失、
参数越界、交付行与动作流非法形状等拒绝 fixture 与断言,与 TS 解析器
拒绝语义逐项对齐,消除补丁覆盖率缺口。

Refs #127

* 🐛 fix(im): 加固动作通道时间判定与流式传输健壮性

响应 fennoai/codex 评审:窗口按含边界过期判定、命令有效期解析带时区
偏移、连接失败按可重连处理、游标按提醒触发分区且仅确认后推进、结果
路径与 Last-Event-ID 百分号编码、非有限数序列化为 null、SSE 单帧超限
溢出中止、Last-Event-ID 改请求头并校验 Content-Type 与 frame.id、
响应体与单帧缓冲封顶;测试按职责拆分并共享夹具。

Refs #127

* 🔧 chore(im): 声明动作通道单线程所有权并拆分传输日志标签

响应 fennoai 评审第 4、28 条:ImActionChannel 类文档补充单线程所有权
契约(Run 串行调用、实例不可并发访问);动作流 SSE 与结果上报 HTTP
传输日志标签拆分,现场日志可区分上报 POST 与动作 SSE。

Refs #127

* 🐛 fix(im): 窗口过期后清理动作结果缓存避免长期堆积

响应 codex/fennoai 评审:executed_ 幂等缓存随窗口截止毫秒记录,Run 入口
清理已过期窗口的缓存条目,同一 reminderTriggerId 的新窗口对相同
operationId 重新执行而非复用旧结果;TDD 先 RED 后 GREEN,跨窗口用例固化
清理语义,避免设备长期运行后缓存无界增长。

Refs #127

* 🐛 fix(im): SSE 流读取区分正常结束与中断,断线按可重连

响应 codex 评审 #2/#5:ImActionCommandStream::Next 改返回带状态结果,
区分 kCommand/kEndOfStream/kNetworkError/kProtocolError;通道对网络中断与
协议错误标记未确认并按可重连返回,不再把已执行命令后的 TCP/TLS 断线误报
为正常结束;坏帧从静默跳过改为协议错误并关闭连接。TDD 先 RED 后 GREEN。

Refs #127

* 🐛 fix(im): 仅网关受理推进确认游标,明确拒绝不重放

响应 codex 评审 #3:Settle 仅在 kSubmitted(服务端受理结果)时推进确认
游标;kRejected(明确 4xx 拒绝)、凭据被拒与可重试均视为未确认,网关
在重连后重放该命令,避免把非业务 ACK 当作已确认而永久丢失结果。TDD 先
RED 后 GREEN,TestServerRejectionDoesNotAdvanceCursor 固化语义。

Refs #127

* 🐛 fix(im): details 对象键 JSON 转义避免生成非法回传

结果 details 的键与值同样需要转义:含引号、反斜杠或控制字符的键此前
原样拼入 JSON,被本地契约校验拒绝后按本地故障处理,导致合法结果无法回传。
统一复用 AppendJsonString 追加键名,保持与值一致的双向可往返性。

Refs #127

* 🐛 fix(im): SSE 传输声明补齐 StreamRead 返回类型修复固件编译

Next() 改为返回 StreamRead 后仅更新了实现与通道消费方,传输头仍声明旧的
optional 返回类型,导致固件在 esp_action_stream_transport.cc 处编译失败
(主机测试不编译该 ESP 实现,未暴露)。同步声明并移除不再使用的 optional
包含,固件全量构建通过。

Refs #127

* 🐛 fix(im): 受理响应超限按未受理处理,避免截断误判成功

ReadResponseBody 此前在响应体达到 64 KiB 上限时静默截断,2xx 仍返回成功,
截断的 NotificationSubmission 会被当作受理结果提取动作窗口。现由
ReadResponseBody 报告是否完整,Post 在超限截断时改为网络失败并携带原因,
调用方按未确认重连重放,避免基于不完整响应推进业务。

Refs #127

* 🐛 fix(im): 响应读取区分 EOF/错误/截断,半截响应按未受理

上一版把提前 EOF 与读取错误误判为成功:已知 Content-Length 未读满、
或 esp_http_client_read 返回负数时仍返回 true,不完整的受理结果会被当作
已确认。现把完整性判定提取为 ImResponseReader 接口 + ReadResponseBody
纯函数:已知长度必须读满、分块流必须读到 EOF、读取错误立即失败、命中
上限严格截断;主机侧补 FakeReader 回归覆盖全部路径。esp_http_transport
通过 EspResponseReader 适配复用,2xx 且响应不完整时按未确认处理。

Refs #127

---------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants