Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0d83299
✨ feat(e2e): 增加分层 Host 与 HIL 门禁
JunLang-7 Aug 21, 2026
6539833
🔒️ fix(e2e): 收紧分层门禁的失败证据与设备输入
JunLang-7 Aug 21, 2026
559b1fd
👷 ci(e2e): 暂停 HIL 自动调度保留手工入口
JunLang-7 Aug 21, 2026
f7c71b7
🔒️ fix(e2e): 防止恢复详情写入公开产物
JunLang-7 Aug 21, 2026
5c87b44
✨ feat(e2e): 增加 SparkBot 语音 HIL 旅程
JunLang-7 Aug 21, 2026
8da6fe8
✨ feat(e2e): 支持 PCB 语音 HIL
JunLang-7 Aug 21, 2026
b07fb8c
✅ test(voice): 固化百炼 SparkBot 实板测试模板
ZhaoXingPeng Aug 22, 2026
3e2f929
🐛 fix(linx): 保持 listen 会话模式与协议一致
ZhaoXingPeng Aug 22, 2026
7ae4f5a
🐛 fix(test): 提升百炼 SparkBot 唤醒注入重跑稳定性
ZhaoXingPeng Aug 22, 2026
156d4e7
🐛 fix(linx): 完善 SparkBot 传输边界与重连诊断
ZhaoXingPeng Aug 22, 2026
04c3d8d
🐛 fix(voice): 增强 SparkBot 唤醒恢复与 Linx 诊断
ZhaoXingPeng Aug 23, 2026
5bdcc85
🐛 fix(test): 补齐 Linx 音频配置测试夹具
ZhaoXingPeng Aug 23, 2026
e89a133
Merge remote-tracking branch 'origin/main' into fix/sparkbot-wake-rec…
ZhaoXingPeng Aug 23, 2026
015f88f
🐛 fix(mcp): 补齐 Linx 工具列表分页游标
ZhaoXingPeng Aug 23, 2026
81faa3c
🐛 fix(voice): 恢复 SparkBot 唤醒确认音
ZhaoXingPeng Aug 23, 2026
363655d
🐛 fix(voice): 对齐 SparkBot auto 监听模式
ZhaoXingPeng Aug 23, 2026
8e20852
🐛 fix(voice): 修复串口注入背压并补齐 Linx 工具类型
ZhaoXingPeng Aug 23, 2026
498fa4a
🐛 fix(test): 放宽串口回合结束门禁等待
ZhaoXingPeng Aug 23, 2026
fa3af12
🐛 fix(test): 按输入长度延长回合门禁
ZhaoXingPeng Aug 23, 2026
d8832e5
🐛 fix(test): 接受本地VAD回合结束证据
ZhaoXingPeng Aug 23, 2026
545b8e4
🐛 fix(voice): 扩大 SparkBot 音频有界突发缓冲
ZhaoXingPeng Aug 23, 2026
c98a5af
🐛 fix(test): 接受 auto 模式远端 STT 回合结束
ZhaoXingPeng Aug 23, 2026
5f90e15
🐛 fix(test): 修正 auto 多轮状态门禁边界
ZhaoXingPeng Aug 23, 2026
74c2a25
🐛 fix(mcp): 对齐 PR338 单页工具发现协议
ZhaoXingPeng Aug 23, 2026
bfa80ee
Merge PR 349 voice wake recovery baseline
JunLang-7 Aug 23, 2026
9516dcf
✨ feat(e2e): 完整接入语音 HIL TTS 与唤醒门禁
JunLang-7 Aug 23, 2026
ee5db45
🐛 fix(voice): PCB唤醒注入纳入HIL门禁
JunLang-7 Aug 24, 2026
78cafaa
Merge remote-tracking branch 'upstream/main' into dev/288-host-hil-gates
JunLang-7 Aug 24, 2026
0f38983
🐛 fix(mcp): 对齐最新工具 Schema 断言
JunLang-7 Aug 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
self-hosted-runner:
labels:
- voicelife-hil
- sparkbot
- pcb
28 changes: 26 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ jobs:
--journey im-gateway-strong-reminder \
--profile host \
--artifact-dir artifacts/im-gateway-e2e \
--timeout 60
--timeout 60 \
--retries 0
- name: 运行 Host 快速恢复 E2E
run: |
E2E_RECOVERY_SUITE=quick python3 scripts/run_e2e.py \
Expand All @@ -101,13 +102,36 @@ jobs:
--artifact-dir artifacts/im-gateway-e2e \
--timeout 120 \
--retries 0
- name: 上传 Host E2E 脱敏证据
- name: 验证故意失败 evidence
if: always()
shell: bash
run: |
set +e
VOICELIFE_E2E_CONTRACT_FAILURE=1 python3 scripts/run_e2e.py \
--layer host \
--journey lifecycle-example \
--profile host \
--artifact-dir artifacts/im-gateway-e2e/contract-failure \
--timeout 60 \
--retries 0
failure_status=$?
set -e
test "$failure_status" -eq 20
- name: 校验 Host E2E 脱敏 evidence
id: validate-host-evidence
if: always()
run: python3 scripts/check_e2e_artifacts.py artifacts/im-gateway-e2e
- name: 写入 Host E2E job summary
if: always()
run: python3 scripts/render_e2e_summary.py artifacts/im-gateway-e2e >> "$GITHUB_STEP_SUMMARY"
- name: 上传 Host E2E 脱敏证据
if: always() && steps.validate-host-evidence.outcome == 'success'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: im-gateway-host-e2e-evidence
path: artifacts/im-gateway-e2e
if-no-files-found: ignore
retention-days: 14

coverage:
name: 覆盖率(Codecov)
Expand Down
154 changes: 154 additions & 0 deletions .github/workflows/hil-nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
name: HIL Manual

on:
workflow_dispatch:
inputs:
profile:
description: Device profile to run (the default runs both profiles)
required: false
type: choice
default: all
options:
- all
- sparkbot
- pcb
journey:
description: HIL journey
required: false
type: choice
default: im-pairing
options:
- im-pairing
- voice
tts_provider:
description: Host TTS fixture provider used by the voice journey
required: false
type: choice
default: dashscope
options:
- dashscope
- aliyun-nls
device:
description: Optional descriptor name; requires a single profile selection
required: false
type: string
default: ""

permissions:
contents: read

concurrency:
group: hil-nightly-${{ github.ref }}-${{ inputs.profile || 'all' }}
cancel-in-progress: false

jobs:
hil:
name: HIL / ${{ matrix.profile }}
runs-on: [self-hosted, voicelife-hil, "${{ matrix.profile }}"]
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
profile: ${{ fromJSON(inputs.profile == 'sparkbot' && '["sparkbot"]' || inputs.profile == 'pcb' && '["pcb"]' || '["sparkbot", "pcb"]') }}
env:
HIL_DEVICE_ROOT: ${{ vars.VOICELIFE_HIL_DEVICE_ROOT || '/opt/voicelife/hil/devices' }}
HIL_LEASE_ROOT: ${{ vars.VOICELIFE_HIL_LEASE_ROOT || '/opt/voicelife/hil/leases' }}
VOICELIFE_HIL_SERVER: ${{ secrets.VOICELIFE_HIL_SERVER }}
VOICELIFE_HIL_SERVER_DIR: ${{ secrets.VOICELIFE_HIL_SERVER_DIR }}
VOICELIFE_HIL_GATEWAY_ORIGIN: ${{ secrets.VOICELIFE_HIL_GATEWAY_ORIGIN }}
VOICELIFE_HIL_USER_ID: ${{ secrets.VOICELIFE_HIL_USER_ID }}
DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
ALIYUN_NLS_APPKEY: ${{ secrets.ALIYUN_NLS_APPKEY }}
ALIYUN_NLS_TOKEN: ${{ secrets.ALIYUN_NLS_TOKEN }}
ALIYUN_NLS_URL: ${{ vars.ALIYUN_NLS_URL }}
HIL_DEVICE_NAME: ${{ inputs.device || '' }}
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: 检查受控 Runner 配置
run: |
set -euo pipefail
device_name="$HIL_DEVICE_NAME"
if [[ -n "$device_name" ]]; then
[[ "${{ inputs.profile || 'all' }}" != "all" ]]
[[ "$device_name" =~ ^[a-z0-9][a-z0-9_-]{0,63}$ ]]
descriptor="$HIL_DEVICE_ROOT/$device_name.json"
else
descriptor="$HIL_DEVICE_ROOT/${{ matrix.profile }}.json"
fi
test -r "$descriptor"
test -n "${VOICELIFE_HIL_SERVER:-}"
test -n "${VOICELIFE_HIL_SERVER_DIR:-}"
test -n "${VOICELIFE_HIL_GATEWAY_ORIGIN:-}"
test -n "${VOICELIFE_HIL_USER_ID:-}"
python3 -c 'import serial, esptool' >/dev/null
if [[ "${{ inputs.journey || 'im-pairing' }}" == "voice" ]]; then
if [[ "${{ inputs.tts_provider || 'dashscope' }}" == "dashscope" ]]; then
test -n "${DASHSCOPE_API_KEY:-}"
python3 -c 'import dashscope' >/dev/null
else
test -n "${ALIYUN_NLS_APPKEY:-}"
test -n "${ALIYUN_NLS_TOKEN:-}"
python3 -c 'import nls' >/dev/null
fi
command -v ffmpeg >/dev/null
fi
- name: 运行 HIL journey
id: journey
run: |
set +e
mkdir -p "artifacts/hil-${{ matrix.profile }}"
device_name="$HIL_DEVICE_NAME"
if [[ -n "$device_name" ]]; then
device_path="$HIL_DEVICE_ROOT/$device_name.json"
else
device_path="$HIL_DEVICE_ROOT/${{ matrix.profile }}.json"
fi
run_args=(
python3 scripts/run_e2e.py \
--layer hil \
--journey "${{ inputs.journey || 'im-pairing' }}" \
--profile "${{ matrix.profile }}" \
--artifact-dir "artifacts/hil-${{ matrix.profile }}" \
--timeout 900 \
--retries 0 \
--device "$device_path" \
--lease-dir "$HIL_LEASE_ROOT" \
--server "$VOICELIFE_HIL_SERVER" \
--server-dir "$VOICELIFE_HIL_SERVER_DIR" \
--gateway-origin "$VOICELIFE_HIL_GATEWAY_ORIGIN" \
--user-id "$VOICELIFE_HIL_USER_ID"
)
if [[ "${{ inputs.journey || 'im-pairing' }}" == "voice" ]]; then
run_args+=(
--input-tts "${{ inputs.tts_provider || 'dashscope' }}"
--tts-model "${VOICELIFE_TTS_MODEL:-qwen-audio-3.0-tts-flash}"
--voice "${VOICELIFE_TTS_VOICE:-longanlingxi}"
--text '你好牛牛,请介绍一下你自己。'
--text '把刚才的回答再简短一点。'
--text '请用一句话总结我们刚才的对话。'
--expect-terminal
)
fi
"${run_args[@]}"
run_status=$?
exit "$run_status"
- name: 校验 HIL 脱敏 evidence
id: validate-hil-evidence
if: always()
run: python3 scripts/check_e2e_artifacts.py "artifacts/hil-${{ matrix.profile }}"
- name: 写入 HIL job summary
if: always()
run: python3 scripts/render_e2e_summary.py "artifacts/hil-${{ matrix.profile }}" >> "$GITHUB_STEP_SUMMARY"
- name: 上传 HIL 脱敏 evidence
if: always() && steps.validate-hil-evidence.outcome == 'success'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: hil-${{ matrix.profile }}-evidence
path: artifacts/hil-${{ matrix.profile }}
if-no-files-found: error
retention-days: 14
10 changes: 9 additions & 1 deletion .github/workflows/im-recovery-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,18 @@ jobs:
--artifact-dir artifacts/im-gateway-recovery \
--timeout 300 \
--retries 0
- name: 上传恢复矩阵脱敏证据
- name: 校验恢复矩阵脱敏 evidence
id: validate-recovery-evidence
if: always()
run: python3 scripts/check_e2e_artifacts.py artifacts/im-gateway-recovery

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] Do not scan raw recovery details as public artifacts

run-recovery-e2e.mjs writes recovery-<run_id>.json into this directory, and each snapshot row always contains the action_token field (even when its value is null). check_e2e_artifacts.py scans every JSON file, treats that key as sensitive, and therefore exits 1 after an otherwise successful recovery run; the subsequent upload is skipped and the nightly job is reported failed. The same directory-level validation was added to .github/workflows/ci.yml, so the PR Host recovery gate is affected too. Validate only the generated evidence-*.json files, or sanitize/allowlist the detailed diagnostic file before running this check.

- name: 写入恢复矩阵 job summary
if: always()
run: python3 scripts/render_e2e_summary.py artifacts/im-gateway-recovery >> "$GITHUB_STEP_SUMMARY"
- name: 上传恢复矩阵脱敏证据
if: always() && steps.validate-recovery-evidence.outcome == 'success'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: im-gateway-recovery-nightly-evidence
path: artifacts/im-gateway-recovery
if-no-files-found: ignore
retention-days: 14
3 changes: 0 additions & 3 deletions components/voicelife_mcp/test/mcp_server_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -463,9 +463,6 @@ void TestToolListing() {
Check(document != nullptr, "tools/list 应序列化为合法 JSON");
yyjson_val* tools = yyjson_obj_get(yyjson_doc_get_root(document), "tools");
Check(yyjson_is_arr(tools) && yyjson_arr_size(tools) == 3, "tools/list JSON 应包含全部工具");
Check(yyjson_is_null(yyjson_obj_get(yyjson_doc_get_root(document), "nextCursor")),
"tools/list JSON 应包含空的 nextCursor 分页字段");

yyjson_val* configure = yyjson_arr_get(tools, 0);
yyjson_val* configure_schema = yyjson_obj_get(configure, "inputSchema");
yyjson_val* properties = yyjson_obj_get(configure_schema, "properties");
Expand Down
62 changes: 62 additions & 0 deletions config/profiles/esp32s3-voicelife-pcb-serial-voice.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"schemaVersion": 1,
"id": "esp32s3-voicelife-pcb-serial-voice",
"target": "esp32s3",
"adapters": {
"audio": {
"driver": "esp32s3-pcm-port",
"capabilities": [
"pcm-port",
"bounded-input",
"bounded-output",
"direct-i2s-simplex",
"transport-frame-assembly"
],
"configRef": "env://VOICELIFE_AUDIO_PROFILE"
},
"speech": {
"driver": "xrobot-websocket",
"capabilities": ["streaming-asr", "tts", "cancel-generation", "pcm"],
"configRef": "nvs://linx/websocket_url"
},
"storage": {
"driver": "fatfs-sqlite",
"capabilities": ["persistent-sqlite"]
},
"im": {
"driver": "voicelife-gateway",
"capabilities": ["https", "secure-credentials"],
"configRef": "nvs://im"
}
},
"sdkconfig": [
"CONFIG_LOG_DEFAULT_LEVEL_INFO=y",
"CONFIG_NVS_ENCRYPTION=y",
"CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC=y",
"CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID=0",
"CONFIG_VOICELIFE_IM_GATEWAY=y",
"CONFIG_LWIP_DHCP_GET_NTP_SRV=y",
"CONFIG_LWIP_SNTP_MAX_SERVERS=2",
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_CROSS_SIGNED_VERIFY=y",
"CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG=y",
"CONFIG_ESP_CONSOLE_SECONDARY_NONE=y",
"CONFIG_ESP_WIFI_NVS_ENABLED=n",
"CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384",
"CONFIG_FREERTOS_CHECK_STACKOVERFLOW=2",
"CONFIG_PARTITION_TABLE_CUSTOM_FILENAME=\"config/partitions/voicelife-pcb.csv\"",
"CONFIG_SPIRAM=y",
"CONFIG_SPIRAM_MODE_OCT=y",
"CONFIG_SPIRAM_SPEED_80M=y",
"CONFIG_SPIRAM_USE_MALLOC=y",
"CONFIG_FREERTOS_TASK_CREATE_ALLOW_EXT_MEM=y",
"CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=2048",
"CONFIG_SPIRAM_MALLOC_RESERVE_INTERNAL=98304",
"CONFIG_SPIRAM_MEMTEST=n",
"CONFIG_SR_MN_CN_MULTINET7_QUANT=y",
"CONFIG_FATFS_SECTOR_4096=y",
"CONFIG_WL_SECTOR_SIZE_4096=y",
"CONFIG_VOICELIFE_STORAGE_FATFS=y",
"CONFIG_VOICELIFE_STORAGE_SQLITE=y",
"CONFIG_VOICELIFE_SERIAL_VOICE_TEST=y"
]
}
2 changes: 1 addition & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
| 语音日程联调 | [语音日程联调测试矩阵](engineering/schedule-voice-integration-test-matrix.md)、[语音日程边界调研与本期取舍](engineering/schedule-voice-industry-boundaries.md) |
| 架构维护者 | [架构与适配器设计](architecture/design-guidelines.md)、[语音子架构](architecture/voice-subarchitecture.md)、[SQLite 存储子架构](architecture/storage-subarchitecture.md)、[ADR](adr/) |
| 组件与服务维护者 | [SparkBot 显示组件](components/sparkbot-display.md)、[IM Gateway](services/im-gateway.md) |
| 协作与交付 | [协同开发](engineering/collaboration.md)、[质量门禁](engineering/ci-quality-gates.md)、[契约版本](engineering/contract-versioning.md)、[提交规范](engineering/commit-convention.md) |
| 协作与交付 | [协同开发](engineering/collaboration.md)、[质量门禁](engineering/ci-quality-gates.md)、[E2E 分层门禁与真机 HIL](engineering/e2e-layered-gates.md)、[发布清单](engineering/release-checklist.md)、[契约版本](engineering/contract-versioning.md)、[提交规范](engineering/commit-convention.md) |
| 文档归档判断 | [文档放置规则](engineering/document-placement.md)、[历史归档](archive/README.md) |

Issue [#264](https://github.com/1024XEngineer/VoiceLife/issues/264) 将旧小智能力迁移计划和旧 ESP32-S3 验证记录移入[历史归档](archive/README.md),并删除没有独立长期价值的 SparkBot 阶段执行草稿。语音原始研究材料继续归档在 Issue [#150](https://github.com/1024XEngineer/VoiceLife/issues/150)。
42 changes: 42 additions & 0 deletions docs/engineering/e2e-journey-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# E2E Journey 模板

新增 journey 前复制本页到对应 Issue/PR,并在代码中复用 `scripts/run_e2e.py` 的统一生命周期。

## 标识

- Journey:`<lowercase-name>`
- 层级:`host` / `hil`
- Profile:`host` / `sparkbot` / `pcb`
- 负责人和依赖:`<owner>`, `<service/device>`

## 生命周期

| 阶段 | 资源与动作 | 超时 | 清理 | 失败分类 |
| --- | --- | --- | --- | --- |
| prepare | 租约、临时目录、进程或设备检查 | `<seconds>` | `<action>` | configuration/lease/device/infrastructure |
| run | 真实请求或串口旅程 | `<seconds>` | `<action>` | external/product/device |
| assert | 有序状态和终态断言 | `<seconds>` | `<action>` | product |
| collect | 只采集 allowlist 字段 | `<seconds>` | `<action>` | infrastructure |
| cleanup | 撤销凭据、复位设备、释放租约 | `<seconds>` | `<action>` | cleanup |

## Evidence

- 公开字段:`<ids/timestamps/metrics/markers>`
- 明确禁止:原始串口、token、密码、SSID、个人数据、完整 URL 或命令输出
- 故意失败测试:`<test name>`
- 脱敏校验:`python3 scripts/check_e2e_artifacts.py <artifact-dir>`

## 门禁入口

- 本地:`<exact command>`
- PR / nightly / manual:`<workflow and job>`
- artifact retention:`<days>`
- required check 计划:先观察,达到稳定门槛后再提议

## 验收

- [ ] Host 和 HIL(如适用)使用同一 runner/evidence schema
- [ ] 硬超时、默认 retries=0、并发隔离已测试
- [ ] 无设备、外部服务和产品断言能分别分类
- [ ] 失败与清理 evidence 可上传且无敏感字段
- [ ] 本地命令、workflow 命令和文档一致
Loading
Loading