ADR-AIOS-13 · XiTest Realtime 双模式数据链路架构(范围 B)
v1.0 proposed · 2026-06-01 18:50 · Cline-AIOS 调度内核起草
用户拍板范围 B(议题 1+2+3+4 进 ADR-13 · 议题 5 DSP 节点 tap 能力边界 + 议题 6 完整 APX500 sequence 留 ADR-AIOS-14 后续)。基于 3 路 subagent 主仓真值核查报告(
AudioDeviceServiceC# 已就位 / sidecar 0 设备 I/O / xilink engine 单 session / SmartStorageEngine 仅 2 store)+ 用户原话需求 6 议题 · 锁定 4 议题决议 + 5 项业务行为契约必填段。ADR-12 关系:本 ADR 不 supersede ADR-12 · 是 ADR-12 fulfilled 后实施回归 + 业务深度补完。ADR-12 §2.1 整体布局 / §2.7 RightInspector 6 段 / §2.8 BottomDock / §3 7 类 MeasurementNode 业务行为契约 / §2.11 边界铁律 11 项 全部继承不动。本 ADR 在其上补:realtime 模式专属"双数据链路 + 工程态隔离 + capture 类 Smaart"3 大架构层。
1. 上下文(Context)
1.1 触发事件
2026-06-01 13:50 用户起 xitest-realtime-acceptance-2026-06-01.md 验收清单 · 14:30 填 3 条问题 · AIOS 派 P0.UH12 hotfix(prompts/active/)。但 4 hours 后用户实测反馈:
用户原话(2026-06-01 18:08): "目前针对 xitest 的实时模式 · 用了你的提示词也没有修改好 · 最后我尝试了直接给智能体提示词让他修改 bug · 但是实际上问题非常大 · 进度缓慢 · 修改效率十分低 · 所以我还是尝试让你按照之前的任务提示词方式进行修改。我再总结一下需求 · 关于实时模式下的数据接口和 pysidecar 的数据分析流程 [...]"
随后用户给出 6 议题完整需求图谱(原话存档于 xitest-realtime-acceptance-2026-06-01.md v2 段)· AIOS 识别为"架构级新功能定义" · 不应直接派 hotfix · 应起 ADR。用户拍板路径 A(起 ADR-AIOS-13)· 范围 B(议题 1+2+3+4 进本 ADR · 议题 5+6 留 ADR-14)。
1.2 用户原话需求图谱(6 议题)
| 议题 | 用户原话(摘录) | 范围 B 是否进 ADR-13 |
|---|---|---|
| 议题1 真硬件直连 | "input devices 选择实际设备的情况 · 比如 xi 产品 xiprobe xical 设备就是真正的声卡设备接入 · 可以直接抓取硬件设备做数据分析 · 那我理解应该是一套独立的后端数据抓取分析流程 · 不能使用 xilink 的引擎系统了" | ✅ 本 ADR §2.1 |
| 议题2 loopback 内置 link | "input 选择 xiAudio loopback · 可以认为是走 xilink 的引擎系统 · 那此时测试目的可能就是 APX500 中的 THD crossover 等有参考信号的测试 [...] 此时需要在后台虚拟一条 loopback 的链路 · 只有 source_v1+channel_gain+sink · 注意这个 source_v1 module 已经被拆分为各个独立的 source · 但此处原始整合的 sour module 应该可以派上用场" | ✅ 本 ADR §2.2 |
| 议题3 顶栏单按钮 + 工程态隔离 | "顶栏的运行停止按钮就回在两种模式配置下启动不同的数据链路 · 对了启动停止和 xilink 下的不一致 · 需要做成一个按钮 · 根据状态做交互;另外当前的状态要和 xilink 工程隔离 · 因为如果是 loopback 模式 · 需要启动当前默认的 link 而不是 xilink 下的 project" | ✅ 本 ADR §2.4+§2.5 |
| 议题4 capture 类 Smaart | "左侧 dock 中的 workspace 中当前有一个窗口布局的 preset · 还需要针对测试模式进行数据保存 · 此功能类似 smaart 的 capture · 比如当前的频响曲线点击 capture 弹框输入曲线名字 · 然后保存在左侧的 workspace 下的对应测试项目的目录下 · 比如当前 4 个窗口 · 那就再四个目录下存入对应的数据内容 · 在带开工程可以选择显示相应的曲线" | ✅ 本 ADR §2.6+§2.7 |
| 议题5 节点 tap 能力 | "右侧的 rms · 频响相位 dock 的显示的路径我们需要重新讨论 [...] 让各自的 dock 可以选择测量哪个位置的数据 [...] 但是这样是不是就意味着每个 module 的输出数据都要有检测机制可以随意切换 · 是否会增加算法链路的复杂度 · PC 模拟可以做到 · DSP 中是否可以做到?" | ❌ ADR-AIOS-14 后续 |
| 议题6 完整 APX sequence | "realtime 模式下的 loopback 系统 · 他的本意就是 APX500 做的事情 · 输出设备通过测试 sequence 中的指定音频信号比如 1KHz 正弦波 · 播放到目标设备 · 目标设备播放这个信号将输出接入当前的输入设备 · 然后将信号做对比计算得出 THD+N 的测量数值" | ❌ ADR-AIOS-14 后续(本 ADR 仅落 sequence schema 占位) |
1.3 主仓真值核查发现(3 路 subagent · v1.5 铁律)
1.3.1 议题 1 真硬件直连(半有 · C# 后端已就位 · sidecar 0 I/O)
| 关注点 | 路径 | 现状 |
|---|---|---|
| sidecar 设备 I/O | pysidecar/ 全目录 |
❌ 0 命中 pyaudio/sounddevice/wasapi · 完全不做设备 I/O · 仅做 base64 WAV/PCM 数学分析 |
| sidecar 端点 | pysidecar/main.py L68-702 |
❌ 仅 /health /analyze/* /auto_tune/* /signal/generate /report/generate · 无 /devices/* · 无 WS/SSE 推流 |
| C# 设备枚举 | backend_csharp/Services/Meter/AudioDeviceService.cs L25-56 |
✅ NAudio MMDeviceEnumerator + EnumerateAudioEndPoints(DataFlow.Capture) |
| C# 设备采集 | backend_csharp/Services/Meter/AudioDeviceService.cs L58-102 |
✅ WasapiCapture + DataAvailable → MeterDataFrame |
| HTTP API 列设备 | backend_csharp/Routes/MeterDevApiRoutes.cs L19-42 |
✅ GET /dev-api/meter/nodes 返回 sinkPre + physicalInputs[] |
| WS API 设备抓取推流 | backend_csharp/Routes/MeterDevApiRoutes.cs L46-130 |
✅ WS /dev-api/meter/stream · 已能 bypass xilink 直推 |
| XiProbe/XiCal 专用 SDK | 全仓库 grep | ❌ 不存在 · 当作普通 WASAPI 设备处理 |
关键洞察(用户 18:50 当面纠正)::模式 A 不是"走 sidecar 不走 C#" · 而是 C# + sidecar 三层分工:
- L1 I/O = C#(AudioDeviceService + WasapiCapture 已实装)
- L2 数学 = sidecar(/analyze/* 19 端点已实装 · ADR-12 #9 P7.U-analyze-extensions zombie 153a109)
- L3 显示 = 前端 widget(ADR-12 7 类 MeasurementNode zombie)
1.3.2 议题 2 loopback 内置 link(几乎没做)
| 关注点 | 路径 | 现状 |
|---|---|---|
source_v1 module |
dsp_algo/modules/source/ |
⚠️ 已拆分为各独立 source(用户原话)· 原版 source_v1 是否仍保留待 ClaudeB 二审(本 ADR §5 fork 2 必跑) |
| 内置 link 模板系统 | backend_csharp/Services/Link/ |
❌ 不存在 · 全是 user project link · 没有"不可改 builtin link"概念 |
channel_gain module |
dsp_algo/modules/channel_gain/ |
⚠️ 待 fork 2 真值核查 |
| sink module input tap | dsp_algo/modules/sink/ |
⚠️ ADR-12 §3 已要求 sink-pre tap · 实施待核查 |
| 后端独立 link 运行能力 | backend_csharp/Services/AudioEngine/AudioEngineService.cs |
❌ 单 engine instance · 直接读 user project · 没有"运行独立 link 不动 project state"机制 |
| 前端 stage 嵌入 link 预览 | frontend_vue3/src/stages/xitest/ |
❌ 当前 xitest stage 内部无 link 可视化(只有 dashboard widget) |
1.3.3 议题 3.1 顶栏单按钮(死按钮 · 需复用 xilink 范式)
| 关注点 | 路径 | 现状 |
|---|---|---|
| xitest TOOLBAR | frontend_vue3/src/stages/xitest/index.vue L45-49 |
❌ 2 独立按钮 xtest-run + xtest-stop · disabled:true 静态 · eventBus emit 但无监听者 = 死按钮 |
| xilink TOOLBAR(参考范式) | frontend_vue3/src/stages/xilink/index.vue L487 + L491-499 + L336-345 |
✅ 单按钮 id=engine · watchEffect 切 ▶/■ icon + tip + disabled · click handler 调 engineStore.startEngine()/stopEngine() |
| 后端 WS 命令 | frontend_vue3/src/stores/audioEngineStore.ts L24-30 |
✅ engine_start / engine_stop 已实装 |
| 统一 runStateStore | 全仓库 grep | ❌ 不存在 · 运行态散在 audioEngineStore / runtimeStore / realtimeStore 3 store · 需建 realtimeRunStore 统一 |
1.3.4 议题 3.2 工程态隔离(天然解耦 · 改造点小)
| 关注点 | 现状 |
|---|---|
| 前端 xitest 是否读 xilink project | ❌ 不读 · 天然解耦(xitest/index.vue 无 linkStore.loadProject 调用) |
| 后端单 session | ✅ 当前是单 engine instance · 改造为 session ID 区分(xitest-realtime vs xilink user project)即可 |
| realtime 设备/capture 持久化目录 | ❌ 当前没独立目录 · 需新增 data/realtime_test_projects/ |
1.3.5 议题 4 capture 类 Smaart(几乎没做)
| 关注点 | 路径 | 现状 |
|---|---|---|
| SmartStorageEngine | frontend_vue3/src/storage/storage-engine.ts L1-94 |
✅ 5ea9806 已落地 · 单例 · localStorage(<100KB) → IndexedDB(≥100KB) 双层 · IDB v1 仅 2 store(snapshots + workspaces) |
| WorkspaceData schema | frontend_vue3/src/types/storage.ts L3-9 |
⚠️ 极简 5 字段 · 无 capture / testProject 概念 |
| Snapshot 类型 | frontend_vue3/src/types/snapshot.ts L1-11 |
⚠️ SnapshotType = 'fft'|'rms'|'transfer' 等 · 但没有 widget 实施(7 类 widget snapshot 仅 schema) |
| WorkspacePresetPanel | frontend_vue3/src/stages/xitest/drawers/WorkspacePresetPanel.vue |
⚠️ 仅 4 套 Preset 切换 · 无 testProject 目录概念 · 无 capture 列表 |
| 曲线导入导出 | 全仓库 grep | ❌ 0 实施 |
1.4 与 ADR-12 的边界(继承不动)
| ADR-12 章节 | 状态 | 本 ADR 是否动? |
|---|---|---|
| §2.1 整体布局图(Top Toolbar/Left Dock/Realtime Dashboard/Right Inspector/Bottom Dock) | fulfilled | ❌ 不动 · 本 ADR 在其上补 realtime 专属层 |
| §2.7 RightInspector 6 段 | fulfilled | ❌ 不动 · 议题 5 节点 tap 增强留 ADR-14 |
| §2.8 BottomDock | fulfilled | ❌ 不动 |
| §3 7 类 MeasurementNode 业务行为契约 | fulfilled | ❌ 不动 · 本 ADR 仅扩展其 § Storage 子能力对接 capture |
| §2.11 边界铁律 11 项(响应式横竖屏 + design-token 等) | fulfilled | ✅ 完全继承 · 本 ADR 所有 fork prompt 必含 |
| §5 12 fork 实施清单 | fulfilled(13 zombie + 1 候选) | ❌ 不动 · 本 ADR §5 是新增 8 fork |
2. 决议(Decision)
2.1 主决议 · 双数据链路并存(议题 1+2 锁定)
xitest realtime 模式 = 双数据链路并存(根据 input device 选择切换)
┌─ 模式 A · 真硬件直连(bypass xilink · 适用 XiProbe/XiCal/任意 audio input)──────┐
│ │
│ Hardware ──┐ │
│ │ │
│ ▼ │
│ C# AudioDeviceService(WasapiCapture · L1 I/O) │
│ │ │
│ ▼ base64 PCM(N×16-bit/32-bit float) │
│ sidecar /analyze/*(numpy/scipy · L2 数学) │
│ │ │
│ ▼ fft/rms/transfer/thd 计算结果(JSON) │
│ C# WS /ws/realtime/{tool}/stream │
│ │ │
│ ▼ │
│ 前端 widget(ADR-12 7 类 · L3 显示) │
│ │
│ 特点:零 link · 零 xilink project 耦合 · 纯监听 · 适用 Smaart 风格基础测量 │
└──────────────────────────────────────────────────────────────────────────────────┘
┌─ 模式 B · loopback 内置 link(走 xilink 引擎 · 适用 APX500 风格 THD/Crossover)────┐
│ │
│ 前端信号源选择(sine/pink/MLS/multitone) │
│ │ │
│ ▼ │
│ C# Engine 加载 BuiltinLinkRegistry["realtime-loopback"] │
│ (3 module:source_v1(整合源)+ channel_gain + sink) │
│ │ │
│ ▼ │
│ WASAPI 输出(选定 output device) │
│ │ │
│ ▼ │
│ ┌── 外部硬件回路 ──┐ │
│ │ 目标设备播放 │ │
│ │ ↓ │ │
│ │ 麦克风/输入设备 │ │
│ └────────────────────┘ │
│ │ │
│ ▼ │
│ C# WasapiCapture(选定 input device) │
│ │ │
│ ▼ base64 PCM │
│ sidecar /analyze/thd · /analyze/transfer_function 等 │
│ │ │
│ ▼ │
│ 前端 widget · 与参考信号对比 · 计算 THD+N / 传函 / 群延迟 │
│ │
│ 特点:有参考信号 · APX500 范式 · 走 xilink 引擎(builtin link · 不动 user) │
└──────────────────────────────────────────────────────────────────────────────────┘
模式切换规则(realtimeRunStore.activeMode):
- input device = XiAudioLoopback → 自动切模式 B(loopback)
- input device = 任意 WASAPI 物理设备(XiProbe/XiCal/系统声卡) → 自动切模式 A(hardware-direct)
- 模式切换 → 顶栏 ▶ 按钮 tooltip 切显("启动硬件直连测量" vs "启动 loopback 测量")
2.2 子决议 · BuiltinLinkRegistry(议题 2 核心)
新建 backend_csharp/Services/Link/BuiltinLinks/:
backend_csharp/Services/Link/BuiltinLinks/
├── BuiltinLinkRegistry.cs (注册不可改 link 模板 · 单例 service)
├── IBuiltinLinkProvider.cs (接口 · 支持未来扩展更多 builtin)
└── presets/
└── realtime-loopback.json (3 module:source_v1+channel_gain+sink · frozen)
realtime-loopback.json schema(frozen · 不可被 user 修改):
{
"id": "realtime-loopback",
"name": "Realtime Loopback (APX500 Style)",
"type": "builtin",
"frozen": true,
"modules": [
{ "id": "src.0", "type": "source_v1", "config": { "kind": "signal_generator", "default": "sine_1khz_-20dbfs" } },
{ "id": "gain.0", "type": "channel_gain", "config": { "channels": [0, 1], "gainDb": 0 } },
{ "id": "sink.0", "type": "sink", "config": { "device": "$user_selected_output" } }
],
"links": [
{ "from": "src.0:out", "to": "gain.0:in" },
{ "from": "gain.0:out", "to": "sink.0:in" }
]
}
API:
- GET /api/builtin-links → 列出所有 builtin links
- GET /api/builtin-links/{id} → 取 frozen JSON
- POST /api/realtime/start?mode=loopback → 内部加载 builtin link · 不写 user project
2.3 子决议 · source_v1 整合源复用(议题 2 子点)
用户原话:"source_v1 module 已经被拆分为各个独立的 source · 但此处原始整合的 sour module 应该可以派上用场"。
实施方向:
- ClaudeB 在 dsp_algo/modules/source/ 下保留(或恢复)source_v1 整合源(作为 backward-compat)
- 内部支持:signal_generator(sine/pink/MLS/multitone/sweep)+ file_player(WAV) + loopback_input(从硬件输入回路)
- builtin loopback link 仅使用 signal_generator 子模式
- 拆分后的独立 source(source_sine / source_pink 等)在 user project 下继续可用 · 不冲突
2.4 子决议 · RealtimeSessionService(议题 3.2 · 工程态隔离)
新建 backend_csharp/Services/Realtime/RealtimeSessionService.cs:
public class RealtimeSessionService
{
private const string SessionId = "xitest-realtime";
private RealtimeSessionState _state;
public bool IsRunning => _state.Status == "running";
public string ActiveMode => _state.Mode; // "hardware" | "loopback"
// 与 xilink engine 互斥:start 时若 xilink 在 running · 自动 stop xilink
public async Task<StartResult> StartAsync(StartRequest req) { ... }
public async Task StopAsync() { ... }
// WS 推送状态变化
public event Action<RealtimeSessionState> OnStateChanged;
}
API:
- POST /api/realtime/start body:{ mode: "hardware"|"loopback", inputDeviceId, outputDeviceId?, signalConfig? }
- POST /api/realtime/stop
- GET /api/realtime/state
- WS /ws/realtime/state 推 { status, mode, inputDevice, outputDevice, errorEvent? }
与 xilink 互斥规则(用户拍板 Q3.1=A):
- realtime start → 若 audioEngineService.IsRunning → 先调 audioEngineService.StopAsync() → 再 start realtime
- xilink start → 若 realtimeSessionService.IsRunning → 先调 realtimeSessionService.StopAsync() → 再 start xilink
- 同时只能 1 个 active · 避免 audio device 抢占冲突
2.5 子决议 · 顶栏单按钮 + realtimeRunStore(议题 3.1)
参考 xilink engineStore 范式 · 新建 Pinia:
// frontend_vue3/src/stores/realtimeRunStore.ts
export const useRealtimeRunStore = defineStore('realtimeRun', {
state: () => ({
isRunning: false,
activeMode: null as 'hardware' | 'loopback' | null,
inputDeviceId: null as string | null,
outputDeviceId: null as string | null,
signalConfig: null as SignalConfig | null,
error: null as ErrorEvent | null,
}),
actions: {
async start() { /* POST /api/realtime/start */ },
async stop() { /* POST /api/realtime/stop */ },
connectWS() { /* WS /ws/realtime/state · 推送状态 */ },
},
})
xitest stage 顶栏注入(stages/xitest/index.vue):
const TOOLBAR: ToolbarButton[] = [
{ id: 'realtime-run', icon: '▶', tip: '启动 realtime 测量' }, // watchEffect 切 ▶/■
// capture 按钮见 §2.6
]
// onMounted:
watchEffect(() => {
const running = realtimeRunStore.isRunning
shellSlots.setToolbarButtonProps('realtime-run', {
icon: running ? '■' : '▶',
tip: running ? '停止 realtime 测量' : '启动 realtime 测量',
})
})
eventBus.on('toolbar:click', ({ stage, id }) => {
if (stage !== 'xitest' || id !== 'realtime-run') return
realtimeRunStore.isRunning ? realtimeRunStore.stop() : realtimeRunStore.start()
})
模式切换 UI:LeftDock § Engine 段(沿用 ADR-12 §5.3 b4a8ea2 已落)· 新增 input/output device 选择 + mode auto-detect 显示。
2.6 子决议 · 顶栏 capture/recapture(议题 4 · Smaart 范式 · 用户 Q4.1c 修正)
⚠️ 重大修正:用户原话"toolbar 中添加统一的一个 capture · recapture 按钮 对标 smaart" · 不是每 widget 独立按钮。
Smaart 范式 capture:
- 顶栏 1 个 capture 按钮 + 1 个 recapture 按钮
- 点击 capture:弹框 → 默认文件名 <timestamp>__<widgetType>__<userName>.json(用户可改)→ 同时抓 stage 当前所有 widget 的曲线数据 → 按 widget 类型分目录落盘
- 点击 recapture:在已选中的历史 capture 上 · 用当前实时数据替换(同名覆盖)
实施 schema:
const TOOLBAR_REALTIME: ToolbarButton[] = [
{ id: 'realtime-run', icon: '▶', tip: '启动 realtime 测量' },
{ id: 'realtime-capture', icon: '📸', tip: 'Capture · 抓取当前所有曲线' },
{ id: 'realtime-recapture', icon: '🔄', tip: 'Recapture · 用当前数据更新选中历史 capture' },
]
capture 弹框 UI:
┌─ Capture Curves ──────────────────────────────────┐
│ Test Project: [当前 testProject ▼] / [+ New] │
│ Capture Name: [2026-06-01-1830__multi__after-eq] │ ← 默认 · 用户可改
│ Description: [Optional · 自然语言描述] │ ← Ximind 兼容性
│ │
│ 将抓取的 widget(stage 当前所有): │
│ ☑ FFT(SpectrumWidget) │
│ ☑ RMS(RmsMeter) │
│ ☑ Transfer(TransferFn) │
│ ☑ Phase(PhaseMeter) │
│ │
│ [Cancel] [Capture] │
└───────────────────────────────────────────────────┘
点击 Capture 后:
- 创建 4 个 CaptureRecord(各 widget 各 1)· 共享同一个 captureGroupId(便于"一次 capture · 多曲线" 关联)
- 各自落到 <testProject>/captures/{fft,rms,transfer,phase}/<timestamp>__<type>__<name>.json
- 在 LeftDock § Workspace 段对应测试项目目录下显示新增的 4 条 capture
- widget 内部自动加载该 capture 作 overlay 显示(可关闭)
2.7 子决议 · SmartStorageEngine 扩展(议题 4 · captures + testProjects 双 store)
frontend_vue3/src/storage/storage-engine.ts 升级 IDB v1 → v2:
新增 store:
- captures(keyPath:id)+ index byTestProject + byWidgetType + byCaptureGroup
- testProjects(keyPath:id)+ index byCreatedAt
CaptureRecord schema(用户 Q4.1a=A1 物理分目录 + Q4.1b=B1 timestamp+type+name 默认):
export interface CaptureRecord {
id: string // uuid
captureGroupId: string // 同一次 capture 的多曲线共享(eg. 一次 capture 4 widget = 4 record · 同 groupId)
testProjectId: string // 关联 TestProject
widgetType: 'fft'|'rms'|'transfer'|'phase'|'waveform'|'electrical'|'recorder'
measurementNodeRef: string // ADR-12 MeasurementNode id
// 用户可改字段
name: string // 默认 `<timestamp>__<widgetType>__<userName>` · 用户弹框输入
description?: string // 自然语言描述(Ximind 兼容性)
color?: string // 叠加显示颜色 · design-token
// 元数据
capturedAt: number // ms timestamp
modifiedAt: number // recapture 时更新
// 物理路径(议题 4 物理分目录)
physicalPath: string // `<testProject>/captures/<widgetType>/<filename>.json`
// 数据载荷
data: SerializedCurveData // union by widgetType
metadata: {
sampleRate: number
activeMode: 'hardware' | 'loopback'
inputDevice: string
outputDevice?: string
signalConfig?: SignalConfig
}
}
export interface TestProject {
id: string
name: string // 用户输入(eg. "Speaker A · After Tuning")
description?: string
createdAt: number
modifiedAt: number
capturesByWidgetType: Record<WidgetType, string[]> // index of CaptureRecord ids
}
export type SerializedCurveData =
| { kind: 'fft', freqs: number[], magsDb: number[], averagedCount: number }
| { kind: 'rms', timestamps: number[], rmsDb: number[][], peakDb: number[][] }
| { kind: 'transfer', freqs: number[], magnitudeDb: number[], phaseDeg: number[], coherence: number[], delayMs: number }
| { kind: 'phase', freqs: number[], phaseDeg: number[] }
| { kind: 'waveform', samples: number[][], sampleRate: number }
| { kind: 'electrical', metric: 'thd'|'thdN'|'sinad'|'snr', value: number, freq: number }
| { kind: 'recorder', samples: number[][], duration: number, markers: { at: number, label: string }[] }
物理目录结构(议题 4 + 议题 3.2 · 与 xilink project 完全独立):
data/realtime_test_projects/
├── <testProjectId>/
│ ├── meta.json (TestProject 元数据)
│ └── captures/
│ ├── fft/
│ │ └── 2026-06-01-1830__fft__after-eq.json
│ ├── rms/
│ │ └── 2026-06-01-1830__rms__after-eq.json
│ ├── transfer/
│ ├── phase/
│ ├── waveform/
│ ├── electrical/
│ └── recorder/
└── ...
多曲线叠加渲染(widget 端):
- widget 加载时 loadCapturesForWidget(widgetType, testProjectId) → 取该 widget 类型的 capture 列表
- 渲染时 · 每条 capture 一个 trace + 主实时 trace · 用 design-token 区分颜色
- LeftDock § Workspace 段加 capture 列表 + 显隐勾选
2.8 子决议 · LeftDock § Workspace 扩展(议题 4 UI)
frontend_vue3/src/stages/xitest/drawers/WorkspacePresetPanel.vue 扩展为分段 UI:
┌─ § Workspace ──────────────────────────────────┐
│ § Layout Preset(原有) │
│ ○ Tuning ○ Electrical ○ Recording ○ Multi │
│ │
│ § Test Project(新增) │
│ Active: [Speaker A · After Tuning ▼] │
│ [+ New Project] [Rename] [Delete] │
│ │
│ § Captures(新增) │
│ ▼ FFT (3) │
│ ☑ 2026-06-01__fft__after-eq │
│ ☐ 2026-06-01__fft__before-eq │
│ ☑ 2026-06-01__fft__golden │
│ ▶ RMS (2) │
│ ▶ Transfer (1) │
│ ▶ Phase (1) │
└─────────────────────────────────────────────────┘
2.9 三层分工铁律(继承 ADR-12 §2.10 + ADR-07 §1.3.4)
| 层 | 进程 | 本 ADR 新增职责 |
|---|---|---|
| L1 I/O | P5-backend-csharp | ① RealtimeSessionService(独立 session)② BuiltinLinkRegistry + realtime-loopback.json ③ AudioDeviceService 加 bypass-xilink 模式给硬件直连用 ④ WS /ws/realtime/{state,stream} ⑤ TestProjects 持久化(data/realtime_test_projects/) |
| L2 数学 | P7-pysidecar | ❌ 本 ADR 不新增 sidecar 职责 · 完全复用 ADR-12 #9 P7.U-analyze-extensions(153a109 zombie 已落 5 端点)+ 现有 19 端点 |
| L3 显示 | 前端 | ① realtimeRunStore + 顶栏 ▶/■ + 📸 + 🔄 ② input/output device 选择面板(LeftDock § Engine 扩展)③ SmartStorageEngine v2(captures+testProjects store)④ capture 弹框 + 多曲线叠加渲染 ⑤ LeftDock § Workspace 扩展(testProject + captures 列表)· 零数学 |
2.10 边界铁律(强制约束)
- 不动 ADR-12 §3 7 类 MeasurementNode 业务行为契约(已通过 e2e ·
3a8d376Phase 4 真值)· 本 ADR 仅扩展 § Storage 子能力对接 capture - 不动 contract-v1.0(已 frozen · realtime 走 v2 命名空间)
- builtin link frozen:realtime-loopback.json 用户不可改 · UI 不显示 edit 按钮 · 修改需新起 ADR
- realtime session 与 xilink engine 互斥:同时只能 1 个 running(用户拍板 Q3.1=A · 简化 audio device 抢占)
- 测试项目目录与 xilink project 完全独立:
data/realtime_test_projects/vsdata/projects/(用户拍板 Q4.1=A4) - ADR-12 §2.11 11 项铁律完全继承:含响应式横竖屏 + design-token 主题切换 · 本 ADR 所有 fork prompt 必含 · 严禁硬编码 hex
- 议题 5 节点 tap 能力 + 议题 6 完整 APX sequence 本期禁止实施:仅留 Schema 占位 · 必须新起 ADR-AIOS-14 推动
- Ximind 兼容性 5 项必填(详见 §11)· 任何 fork 派发前必含
3. 业务行为契约(Business Behavior Contract · 4 议题 × 5 项契约)
对齐 ADR-12 §3 风格 · 本节是 Cline-AIOS 调度内核继续推进"业务行为契约必填段"标杆。每议题 5 项契约,任何派发缺契约 = 立即拒绝。
3.1 议题 1 · 模式 A 真硬件直连
① 输入/输出契约
// 启动:
POST /api/realtime/start
body: {
mode: "hardware",
inputDeviceId: string, // C# AudioDeviceService 列出的 device id
toolKind: 'fft'|'rms'|'transfer'|'phase'|'waveform'|'electrical'|'recorder', // 同 ADR-12 §3
channels: number[], // 选通道 [0,1,...] 8 通道
sampleRate: 48000|44100|96000,
fftSize?: number,
}
response: { sessionId: "xitest-realtime", status: "running", mode: "hardware" }
// WS 推流(复用 ADR-12 #8 P5.U-meter-tap-multi-tool 48cf0ba 的 7 toolKind 帧 schema):
WS /ws/realtime/stream
frame: ADR-12 §3.X MeterFrame_<toolKind>(原 schema 不动 · 加 source: "realtime-hardware" 字段区分)
② 收敛/成功判据
| 判据 | 阈值 |
|---|---|
| 设备打开成功 | C# WasapiCapture.StartRecording() 不抛异常 · 5s 内首帧到达前端 |
| 帧率达标 | 前端实测 FPS ≥ 25 |
| 通道映射就位 | channels.length === 用户选定 |
③ 失败回退路径
| 失败 | 触发 | UI 表现 | 恢复 |
|---|---|---|---|
| 设备被占用 | C# WasapiCapture 抛 InvalidOperationException |
顶栏红警 + "Audio device busy" + recovery_hints | 用户切其他 device |
| 设备拔出 | MMDevice.OnPropertyValueChanged State=NotPresent |
自动 stop session + WS 推 error_event · widget 灰显 | 重新选 device |
| sidecar 崩溃 | C# 调 /analyze/* 返 502 |
全 widget 灰显 + "分析后端不可用" | C# 自动重启 sidecar + health-check |
④ 用户操作流
Step 1: 进 xitest stage → 选 realtime 模式
Step 2: LeftDock § Engine → 选 Input Device(eg. "XiProbe USB Microphone")
Step 3: 系统自动识别 = 物理设备 → mode auto-detect = "hardware-direct"
Step 4: 顶栏 ▶ 按钮 tip 显示 "启动硬件直连测量" → 点击 → ▶ 切 ■
Step 5: dashboard 4 widget 实时刷新 · 30fps 帧率
Step 6: 顶栏 📸 capture → 弹框命名 → 抓 4 widget 曲线 → 落 testProject 对应目录
⑤ 端到端真值 e2e
test('mode A · XiProbe 注入 1kHz -10dBFS sine · FFT 应在 1kHz 出 -10±1dB peak', async ({ page }) => {
await injectChannel(0, '1kHz sine -10dBFS')
await page.goto('/xitest?mode=realtime')
await selectInputDevice(page, 'XiProbe USB Microphone') // mock
await clickRunButton(page)
await page.waitForFunction(() => __xitestDebug.getFftAveragedCount() >= 8)
expect(await __xitestDebug.getFftPeakBinFreq()).toBeBetween(950, 1050)
expect(await __xitestDebug.getFftPeakDb()).toBeBetween(-11, -9)
})
3.2 议题 2 · 模式 B loopback 内置 link
① 输入/输出契约
POST /api/realtime/start
body: {
mode: "loopback",
inputDeviceId: string, // 麦/输入端
outputDeviceId: string, // 输出端
signalConfig: {
type: 'sine'|'pink'|'mls'|'multitone'|'sweep',
frequency?: number, // sine
amplitude_dbfs: number, // -20 default
duration?: number,
},
toolKind: 'thd'|'transfer'|'electrical'|...
}
② 收敛/成功判据
| 判据 | 阈值 |
|---|---|
| builtin link 加载成功 | C# LinkService.LoadBuiltin("realtime-loopback") 不抛 |
| 输出 + 输入双 device 都打开 | output WasapiOut + input WasapiCapture 双就绪 |
| 信号已稳定 | 输出端 RMS ≈ -20dBFS · 输入端 RMS > noiseFloor + 6dB · 持续 500ms |
| 测量稳定(THD) | THD 跨 8 帧 std < 0.1% |
③ 失败回退路径
| 失败 | UI 表现 |
|---|---|
| 输出 device 与输入 device 是同一个 | 红警 "Loopback requires distinct input/output device" |
| 信号未到达输入 | 黄警 "Signal not detected · check cable" |
| THD 异常高(> 10%) | 黄警 "Possible clipping · reduce signal amplitude" |
④ 用户操作流(APX500 风格)
Step 1: 选 input = XiAudioLoopback 或物理麦 + output = 物理扬声器
Step 2: 系统识别 = loopback mode
Step 3: 选信号源 sine 1kHz -20dBFS
Step 4: ▶ → BuiltinLinkRegistry 加载 realtime-loopback link → C# Engine start
Step 5: 输出端播放 1kHz · 输入端采集 · sidecar 计算 THD = 0.05% 等
Step 6: 📸 capture · 弹框命名 · 落盘
⑤ 端到端真值 e2e
test('mode B · loopback 1kHz sine · THD < 1% (理想线性 device)', async ({ page }) => {
await selectInputDevice(page, 'XiAudioLoopback')
await selectOutputDevice(page, 'TestSpeakerLinear')
await selectSignal(page, { type: 'sine', frequency: 1000, amplitude_dbfs: -20 })
await clickRunButton(page)
await page.waitForFunction(() => __xitestDebug.getThdSettled())
expect(await __xitestDebug.getThdPercent()).toBeLessThan(1)
})
3.3 议题 3 · 顶栏单按钮 + 工程态隔离
① 输入/输出契约
// realtimeRunStore state schema(见 §2.5)
// 与 xilink 互斥的 API 行为:
POST /api/realtime/start
若 audioEngineService.IsRunning === true → 内部先 stop xilink → 再 start realtime
返回 response.body.previousSessionStopped: { type: "xilink", projectId: "..." }
② 收敛/成功判据
| 判据 | 阈值 |
|---|---|
| 顶栏按钮状态切换 | watchEffect 触发后 100ms 内 icon ▶↔■ |
| WS 状态推送 | WS 帧间隔 < 500ms |
| 互斥成功 | xilink stop 后 realtime 才 start · 无并发 audio device 错误 |
③ 失败回退路径
| 失败 | UI 表现 |
|---|---|
| WS 断开 | 顶栏按钮灰 + "已断开 · 重连中" |
| 互斥 stop 失败(xilink stuck) | "无法停止 xilink · 请手动 stop" + 推荐 user 切 stage |
④ 用户操作流
正常路径:
xitest stage → ▶ 启动 realtime → ■ 停止 → 切 xilink stage → ▶ 启动 xilink
互斥路径:
xilink 在 running → 切 xitest stage → ▶ → 提示 "将自动停止 xilink engine" → 用户确认 → 自动 stop xilink + start realtime
⑤ 端到端真值 e2e
test('exclusion · xilink running → start realtime → xilink should stop', async ({ page }) => {
await page.goto('/xilink')
await clickRunButton(page) // xilink running
await page.goto('/xitest?mode=realtime')
await clickRunButton(page) // realtime start
expect(await getEngineStoreIsRunning()).toBe(false) // xilink auto-stopped
expect(await getRealtimeStoreIsRunning()).toBe(true)
})
3.4 议题 4 · capture 类 Smaart
① 输入/输出契约(见 §2.7 CaptureRecord/TestProject schema)
② 收敛/成功判据
| 判据 | 阈值 |
|---|---|
| capture 落盘成功 | IDB v2 captures store 写入完成 · 物理文件存在 |
| 多 widget 同 groupId | 一次 capture 4 widget = 4 record · 共享 captureGroupId |
| 加载叠加显示 | widget 加载 ≤ 200ms · 不卡帧 |
| 重开工程恢复 | F5 后 LeftDock § Workspace 显示同样的 testProject + captures |
③ 失败回退路径
| 失败 | UI 表现 |
|---|---|
| 文件名重复 | 弹框警告 "已存在同名 capture · 是否覆盖?" |
| 磁盘空间不足 | 红警 "Storage quota exceeded" + recovery_hints |
| testProject 不存在 | 弹框引导 "请先创建 Test Project" |
④ 用户操作流
首次 capture:
Step 1: realtime 跑起来 · 4 widget 显示数据
Step 2: LeftDock § Workspace § Test Project → [+ New] → 输入 "Speaker A · After Tuning"
Step 3: 顶栏 📸 → 弹框默认名 `2026-06-01-1830__multi__after-tuning` → 用户改为 "after-eq" → 勾选 4 widget
Step 4: [Capture] → 4 record 落 4 个目录 + IDB index 更新 + LeftDock 立即显示
recapture(同名替换):
Step 1: LeftDock § Captures → 选中 "after-eq" 系列(4 record · 同 groupId)
Step 2: 顶栏 🔄 → 弹框确认 "用当前数据更新 4 条 capture?"
Step 3: [Confirm] → 4 record data 字段更新 · modifiedAt 更新 · 文件覆盖
加载叠加显示:
Step 1: LeftDock § Captures → 勾选 "after-eq" + "before-eq"
Step 2: 各 widget 自动叠加显示 2 条历史曲线 + 主实时曲线 = 3 trace · design-token 区分颜色
⑤ 端到端真值 e2e
test('capture · 4 widget 同时抓 · 4 文件落 4 目录 · F5 后可恢复', async ({ page }) => {
await startRealtimeMode(page)
await createTestProject(page, 'speaker-a')
await clickCaptureButton(page)
await fillCaptureName(page, 'after-eq')
await confirmCapture(page)
// 验证 4 文件落盘
const captures = await page.evaluate(() => storageEngine.listCaptures('speaker-a'))
expect(captures).toHaveLength(4)
expect(captures.map(c => c.widgetType).sort()).toEqual(['fft', 'phase', 'rms', 'transfer'])
// 验证 captureGroupId 共享
const groupIds = new Set(captures.map(c => c.captureGroupId))
expect(groupIds.size).toBe(1)
// F5 重新加载 · 验证 LeftDock 显示
await page.reload()
await expect(page.locator('[data-testid=capture-list-fft]')).toContainText('after-eq')
})
4. 后果(Consequences)
4.1 正面后果
✅ xitest realtime 双模式数据链路定型 · 用户实测可用(对标 Smaart 基础测量 + APX500 loopback 测量) ✅ 三层分工铁律严守 + 复用最大化 · L1 I/O C# 已就位(只补 RealtimeSessionService + BuiltinLinkRegistry · 不改 AudioDeviceService 内核)· L2 数学 sidecar 0 改动 · L3 前端纯增量 ✅ 工程态隔离干净 · realtime 与 xilink user project 完全互斥 · audio device 抢占问题彻底解决 ✅ builtin link 模板系统 · 为后续 ADR-14 完整 APX sequence + 更多 builtin(eg. crossover-test / impedance-meter)铺路 ✅ capture 类 Smaart · 用户工作流贴合行业标杆 · 物理分目录 + 弹框命名让数据可读 · captureGroupId 让"一次 capture 多曲线"自然 ✅ 业务行为契约延续 · ADR-12 §3 风格继续执行 · 4 议题 × 5 项契约 = 20 子段 · 杜绝"壳子框架" ✅ Ximind 兼容性继续覆盖 · 5 项检查清单完整(见 §11)
4.2 负面后果与缓解
| 后果 | 影响 | 缓解 |
|---|---|---|
| ⚠️ realtimeRunStore 与 audioEngineStore 双 store 互斥 · 复杂度 +1 | 前端状态机维护 | 互斥逻辑封装在后端 RealtimeSessionService · 前端只读 WS 推送 |
| ⚠️ source_v1 整合源需 ClaudeB 二审是否仍可用 | fork 2 风险点 | fork 2 Step 1 真值核查 · 若不可用降级方案 = 用拆分后的 source_sine + 多源叠加 |
| ⚠️ IDB v1→v2 迁移 | 现有 user 数据 | 加 LEGACY_SCHEMA_MIGRATION(7 天宽限 · 沿用 ADR-08 §议题② 模式)· 自动迁移 · 失败 fallback 重置 |
| ⚠️ 4 widget 同时 capture 数据量大 | IDB 写入延迟 | 利用 SmartStorageEngine 双层(<100KB localStorage · ≥100KB IDB)· 异步写 · UI 不阻塞 |
| ⚠️ 多曲线叠加渲染性能(每 widget 4-8 trace) | 30fps 卡顿 | Canvas 离屏 + RAF 节流 · 历史 capture trace 静态(只渲染 1 次)· 实时 trace 30fps 重画 |
| ⚠️ ADR-12 fulfilled 后再起 ADR-13 文档复杂度 +1 | 后续维护 | 本 ADR §1.4 明确边界(继承不动)· §10 references 完整 trace · 不 supersede ADR-12 |
4.3 关键非目标(Non-Goals)
- ❌ 不实施议题 5 · 节点 tap 能力(右 dock RMS/频响/相位 选 module 输出)→ ADR-AIOS-14
- ❌ 不实施议题 6 · 完整 APX500 sequence(测试 sequence schema + 执行引擎 + 自动 PASS/FAIL 判据)→ ADR-AIOS-14
- ❌ 不实施 sidecar 设备 I/O(继续维持 sidecar = 纯无状态分析服务)
- ❌ 不实施 XiProbe/XiCal 专用 SDK(当作普通 WASAPI 设备 · 未来 ADR 推 driver/calibration 时再说)
- ❌ 不实施 capture 跨 testProject 复制(同 testProject 内可对比 · 跨 project 留下季度)
- ❌ 不实施 capture 导出 CSV/WAV(本期仅 JSON)
- ❌ 不实施 builtin link 用户克隆为可改 link(用户拍板 Q2=A · 静态 frozen)
- ❌ 不实施多 builtin link(本 ADR 仅 realtime-loopback 1 个 · 未来 ADR-14 加 crossover-test 等)
- ❌ 不修改 contract-v1.0(已 frozen · realtime 走 v2 命名空间)
- ❌ 不联动 ADR-08 子图系统(realtime 用 builtin link · 不嵌 subgraph)
5. 实施清单(Implementation · 8 fork U-thread · 总 6-8d)
5.1 Phase 1 · 后端基础设施(2.3d · 优先级 P1 · 解锁前端)
| # | UID | CPU | 工时 | 范围 | 依赖 |
|---|---|---|---|---|---|
| 1 | P5.UA13-realtime-session-service | ClaudeB | 1.0d | RealtimeSessionService + /api/realtime/{start,stop,state} + WS /ws/realtime/state + 与 xilink engine 互斥逻辑 |
无依赖 · 可立即派 |
| 2 | P5.UA13-builtin-link-registry | ClaudeB | 0.8d | BuiltinLinkRegistry + presets/realtime-loopback.json frozen + /api/builtin-links/* + LinkService 加载 builtin 不动 user project |
无依赖 · 与 fork 1 文件正交 |
| 3 | P5.UA13-audio-device-bypass-mode | ClaudeB | 0.5d | AudioDeviceService 加 "bypass-xilink" 模式 + WS /ws/realtime/stream 直推 PCM 给 widget · 复用 ADR-12 #8 toolKind 路由 schema |
无依赖 · 与 fork 1+2 文件正交 |
5.2 Phase 2 · 前端核心(3.0d · 优先级 P1)
| # | UID | CPU | 工时 | 范围 | 依赖 |
|---|---|---|---|---|---|
| 4 | P0.UA13-realtime-run-store-toolbar | ClaudeA | 0.5d | realtimeRunStore Pinia + xitest 顶栏 ▶/■ 单按钮 watchEffect(替换死按钮) + 顶栏 📸 + 🔄 注册 | fork 1+3 zombie 后派 |
| 5 | P0.UA13-input-output-device-config-panel | ClaudeA | 1.0d | LeftDock § Engine 段扩展 input/output device 选择 + mode auto-detect(hardware/loopback) + signalConfig UI(loopback 模式可见) | fork 1+2 zombie 后派 |
| 6 | P0.UA13-storage-engine-v2-captures | ClaudeA | 1.5d | SmartStorageEngine IDB v1→v2 + captures store + testProjects store + LEGACY 迁移 + CaptureRecord/TestProject types | 无后端依赖 · 与 fork 4+5 文件正交可并行 |
5.3 Phase 3 · 前端 UI 集成(2.0d · 优先级 P1)
| # | UID | CPU | 工时 | 范围 | 依赖 |
|---|---|---|---|---|---|
| 7 | P0.UA13-capture-toolbar-multi-widget | ClaudeA | 1.5d | 顶栏 capture 弹框 UI + recapture 同名替换 + 多 widget 同 groupId 抓取 + 各 widget 多曲线叠加渲染(7 widget 各加 capture overlay 子能力)+ LeftDock § Workspace § Test Project + § Captures 列表 UI | fork 6 zombie 后派 |
| 8 | P_e2e.UA13-truth | ClaudeC | 0.5d | e2e 真值脚本(模式 A 硬件直连 + 模式 B loopback + 互斥切换 + capture 4 widget 同 groupId + F5 恢复)· 横竖屏 + 主题 e2e 复用 ADR-12 §2.11 | fork 4+5+7 zombie 后派 |
5.4 派发顺序(K-thread 占用约束)
Phase 1(三 fork 文件正交并行 · 总 1.0d 关键路径):
fork 1 (P5 RealtimeSessionService) ─┐
fork 2 (P5 BuiltinLinkRegistry) ├─ ClaudeB 内部并行/串行
fork 3 (P5 AudioDevice bypass-xilink)─┘
fork 1+2+3 全 zombie → 解锁 Phase 2
Phase 2(三 fork 文件正交并行 · 总 1.5d 关键路径):
fork 4 (P0 realtimeRunStore + toolbar) ─┐
fork 5 (P0 device config panel) ├─ ClaudeA 内部并行
fork 6 (P0 SmartStorageEngine v2) ─┘
fork 4+5+6 全 zombie → 解锁 Phase 3
Phase 3(串行 · 总 2.0d 关键路径):
fork 7 (P0 capture toolbar + multi-widget overlay) → ClaudeA 1.5d
fork 8 (P_e2e truth) → ClaudeC 0.5d
5.5 隔离类型分配(.clinerules v1.4)
| fork | 隔离 | 理由 |
|---|---|---|
| fork 1 | 🧵 file | 后端 ClaudeB · 新建 Services/Realtime/ 目录 · 与 fork 2+3 路径正交 |
| fork 2 | 🧵 file | 后端 ClaudeB · 新建 Services/Link/BuiltinLinks/ · 与 fork 1+3 路径正交 |
| fork 3 | 🧵 file | 后端 ClaudeB · 仅扩展 Services/Meter/AudioDeviceService.cs 1 文件 · 行号正交 fork 1+2 |
| fork 4 | 🧵 file | 前端 ClaudeA · 新建 stores/realtimeRunStore.ts + 改 stages/xitest/index.vue |
| fork 5 | 🧵 file | 前端 ClaudeA · 改 stages/xitest/drawers/EnginePanel.vue 等 · 与 fork 4 行号正交 |
| fork 6 | 🧵 file | 前端 ClaudeA · 改 storage/storage-engine.ts + types · 与 fork 4+5 路径正交 |
| fork 7 | 🧵 file | 前端 ClaudeA · 改 7 widget + LeftDock + 顶栏弹框 · 串行 fork 6 后 |
| fork 8 | 🧵 file | 测试 ClaudeC · 新建 tests/e2e/realtime-dual-mode.spec.ts 独立文件 |
6. Migration · LEGACY 兼容
6.1 IDB schema 迁移 v1 → v2(fork 6)
// SmartStorageEngine v2:
const IDB_VERSION = 2 // was 1
upgrade(db, oldVersion) {
if (oldVersion < 1) {
db.createObjectStore('snapshots', { keyPath: 'id' })
db.createObjectStore('workspaces', { keyPath: 'id' })
}
if (oldVersion < 2) {
const captures = db.createObjectStore('captures', { keyPath: 'id' })
captures.createIndex('byTestProject', 'testProjectId')
captures.createIndex('byWidgetType', 'widgetType')
captures.createIndex('byCaptureGroup', 'captureGroupId')
> **⚠️ 架构变更 (2026-06-24)**: §2.7 定义的 SmartStorageEngine v2 (IndexedDB) 已迁移到后端 API。
> 存储统一在后端 `data/xitest/realtime/` 目录,前端通过 REST API 操作。
> 详见 ADR-AIOS-22 §9.4。IndexedDB 架构保留作历史参考。
> 迁移提交: `5cce2af`
const testProjects = db.createObjectStore('testProjects', { keyPath: 'id' })
testProjects.createIndex('byCreatedAt', 'createdAt')
}
}
LEGACY 7 天宽限:已存在的 xitest:ws:* localStorage key 自动迁移到 testProject "default" · 显示 banner 提示用户 "已迁移 N 个旧 workspace 到默认测试项目"。
6.2 死按钮 eventBus 清理(fork 4)
stages/xitest/index.vue L70-72 旧 xitest:run-suite / xitest:stop emit 删除 · 改为 realtimeRunStore.start()/stop() 直调。LEGACY_EVENTBUS_MAP 不需要(因为旧按钮无监听者)。
7. Validation · 验收标准
7.1 形式合规
- dotnet build 0 错误 · dotnet test 217+/0 全绿(后端基线 +N 用例)
- npm run typecheck 0 错误 · npm run test:unit 全绿(前端基线 +N 用例)
- sidecar python -m pytest 78/0(不变 · 本 ADR 不动 sidecar)
- 修动文件全在 §5 isolation_files 范围内
- 不动 contract-v1.0 / ADR-12 §3 7 widget / dsp_algo (除 source_v1 二审)
7.2 业务行为契约 e2e(fork 8 实施 · 必跑)
- 议题 1 模式 A:XiProbe 注入 1kHz · FFT widget 显示 1kHz peak ±1dB
- 议题 2 模式 B:loopback sine 1kHz · THD widget 显示 < 1%
- 议题 3.1 顶栏:▶ 切 ■ 100ms 内完成
- 议题 3.2 互斥:xilink running → 切 xitest realtime ▶ → xilink 自动 stop
- 议题 4 capture:1 次 capture 4 widget = 4 record + 同 groupId · 4 个物理目录各 1 文件
- 议题 4 recapture:同名替换 · modifiedAt 更新
- 议题 4 F5 恢复:重新加载页面 LeftDock § Captures 显示同样列表
- 横竖屏 e2e:viewport 1920x1080 ↔ 1080x1920 各 1 次 · 布局正常
- 主题 e2e:浅色 ↔ 深色 · design-token 跟随
7.3 用户验收(必跑)
用户重跑 xitest-realtime-acceptance-2026-06-01.md 4 议题需求 · 全部实测通过 → ADR-13 fulfilled。
8. References
8.1 关联 ADR
- ADR-AIOS-07 XiTune/XiTest 边界:三层分工铁律 §1.3.4 · 本 ADR §2.9 完全继承
- ADR-AIOS-12 XiTest Realtime Widget Workspace 架构 v2.3:fulfilled 🏆 · 本 ADR §1.4 明确边界继承不 supersede
- ADR-AIOS-14(候选 · 本 ADR 触发):议题 5 节点 tap 能力 + 议题 6 完整 APX sequence
- ADR-AIOS-08 XiLink Stage UX:
engineStore范式参考(本 ADR §2.5)
8.2 关联 commit / fork zombie
5ea9806ADR-12 §5.3 #11 P0.U-bottom-dock-storage-engine(SmartStorageEngine v1 · 本 ADR fork 6 升级 v2)b4a8ea2ADR-12 §5.3 #10 P0.U-engine-session-snapshots(LeftDock § Engine · 本 ADR fork 5 扩展)48cf0baADR-12 #8 P5.U-meter-tap-multi-tool(7 toolKind 路由 · 本 ADR fork 3 复用)153a109ADR-12 #9 P7.U-analyze-extensions(sidecar 5 端点 · 本 ADR 完全复用 · 0 改动)8379de2ADR-12 #5 P0.U-measurement-rms-fft-phase(3 widget 真业务 · 本 ADR fork 7 加 capture overlay)3a8d376ADR-12 §5.4 #12 P_e2e.U-phase4-truth(e2e 真值脚本范式 · 本 ADR fork 8 复用)
8.3 对标产品
- Smaart Suite(Rational Acoustics):capture 模式 + 多曲线叠加 + memo 命名 · 本 ADR §2.6 直接对标
- Audio Precision APx500:loopback 测试范式 + 信号源 + THD/SINAD 测量 · 本 ADR §2.1 模式 B 对标
- Ocenaudio:多 trace overlay · 本 ADR fork 7 多曲线渲染参考
8.4 真值核查报告
- 议题 1+2:
subagent #1报告(2026-06-01 18:25)· 详见会话 · AudioDeviceService L25-102 · pysidecar 0 设备 I/O - 议题 3:
subagent #2报告(2026-06-01 18:25)· xitest 死按钮 + xilink engineStore 范式 + 单 session 模型 - 议题 4:
subagent #3报告(2026-06-01 18:25)· SmartStorageEngine 5ea9806 双 store + WorkspacePresetPanel 4 preset
9. 状态流转表
| 时间 | 版本 | 状态 | 动作 |
|---|---|---|---|
| 2026-06-01 13:50 | — | — | 用户起 xitest-realtime-acceptance-2026-06-01.md 验收清单 |
| 2026-06-01 14:30 | — | — | 用户填 3 条问题 · AIOS 派 P0.UH12 hotfix(active/) |
| 2026-06-01 18:08 | — | — | 用户实测 hotfix 不解决问题 · 重新提需求 6 议题 |
| 2026-06-01 18:13 | — | — | 用户拍板路径 A(起 ADR-AIOS-13)+ 范围 B(议题 1+2+3+4) |
| 2026-06-01 18:30 | — | — | AIOS 起 3 路 subagent 并行真值核查议题 1+2+3+4 |
| 2026-06-01 18:46 | — | — | 用户拍板 4 决策点(议题 1A · 议题 2A · Q3.1A · Q4.1a=A1 + Q4.1b=B1+弹框 + Q4.1c=顶栏统一 capture/recapture · 议题 4=A) |
| 2026-06-01 18:50 | v1.0 | proposed | AIOS 起草 ADR-AIOS-13 v1.0 落盘 · 8 fork ready 等用户 accept |
| 2026-06-01 19:21 | v1.0 | accepted | user accept · 8 fork ready 等 start |
10. Appendix · 6 议题 vs ADR-12/ADR-13/ADR-14 矩阵
| 议题 | 范围 | 进入 ADR |
|---|---|---|
| 议题 1 真硬件直连(模式 A · C# + sidecar 三层分工) | 6-8d 方案完整 | ADR-13 §2.1 |
| 议题 2 loopback 内置 link(模式 B · BuiltinLinkRegistry) | 完整 | ADR-13 §2.2+§2.3 |
| 议题 3.1 顶栏单按钮 toggle | 完整 | ADR-13 §2.5 |
| 议题 3.2 工程态隔离(realtime 与 xilink 互斥) | 完整 | ADR-13 §2.4 |
| 议题 4 capture 类 Smaart(顶栏统一按钮 + 物理分目录 + 弹框命名 + 多曲线叠加) | 完整 | ADR-13 §2.6+§2.7 |
| 议题 5 节点 tap 能力(右 dock 选 module 输出 · DSP 边界决策) | 留 ADR-14 | ❌ ADR-14(候选) |
| 议题 6 完整 APX sequence(测试 sequence + 执行引擎 + 自动 PASS/FAIL) | 留 ADR-14 | ❌ ADR-14(候选) |
11. ⭐ Ximind Compatibility(.clinerules v1.3 §ADR 设计基本要求 全局铁律)
11.1 大模型可读状态(structured · self-describing JSON)
| API 端点 | 字段(自描述) | Ximind 用途 |
|---|---|---|
GET /api/realtime/state |
{ status, mode, inputDevice: {id, name, channels}, outputDevice?, signalConfig?, errorEvent? } |
Ximind 知道当前 realtime 在跑啥 |
GET /api/builtin-links |
[{ id, name, type, frozen, modules: [{type, config}], links }] |
Ximind 知道有哪些 builtin 可启动 |
WS /ws/realtime/state |
同 GET state · push 模式 | Ximind 实时跟踪 |
WS /ws/realtime/stream |
ADR-12 MeterFrame_source: "realtime-hardware"|"realtime-loopback" 字段 |
Ximind 解析测量数据 |
前端 realtimeRunStore getter |
同 state | Ximind 通过 devtools 取前端状态 |
前端 storageEngine.listCaptures(testProjectId) |
[CaptureRecord](含 description / metadata) |
Ximind 列出历史 capture |
11.2 大模型可写操作(语义化 API + 自然语言描述)
| API | 操作语义 | recovery_hints |
|---|---|---|
POST /api/realtime/start body含 description?: string |
启动 realtime · 描述本次测量目的(eg. "After EQ tuning · check 1kHz dip") | ["select different input device", "check device permission", "stop xilink first"] |
POST /api/realtime/stop |
停止 realtime | — |
POST /api/realtime/capture body { name, description?, widgetTypes[] } |
同顶栏 📸 按钮 · Ximind 可调 | ["create test project first", "rename to avoid duplicate"] |
POST /api/realtime/recapture/{captureGroupId} |
同 🔄 按钮 | — |
POST /api/test-projects body { name, description? } |
新建测试项目 | — |
DELETE /api/captures/{id} / PATCH /api/captures/{id} |
删除 / 重命名 | — |
11.3 自然语言描述字段
每个 CaptureRecord / TestProject / SignalConfig 必含 description?: string · 大模型可生成 / 可解析:
- CaptureRecord.description = "After applying +6dB PEQ at 1kHz · expecting flat response above 100Hz"
- TestProject.description = "Speaker A · 4-driver crossover tuning session · target curve attached"
- SignalConfig.description = "1kHz sine -20dBFS · 5s sweep up to 20kHz · for THD measurement"
11.4 审计日志路径
| 事件 | 持久化 |
|---|---|
| realtime start/stop | data/realtime_test_projects/_audit.jsonl(append-only) + WS /ws/realtime/state |
| capture create/recapture/delete | <testProject>/_audit.jsonl + IDB captures store(soft delete with deletedAt field) |
| testProject create/rename/delete | data/realtime_test_projects/_audit.jsonl |
| device error | C# ILogger + WS /ws/realtime/state.errorEvent |
11.5 error 结构(structured · 大模型可基于 hints 自主重试)
interface RealtimeError {
code: 'AUDIO_DEVICE_BUSY' | 'AUDIO_DEVICE_NOT_FOUND' | 'XILINK_STOP_FAILED' |
'BUILTIN_LINK_LOAD_FAILED' | 'SIDECAR_UNAVAILABLE' | 'CAPTURE_DUPLICATE_NAME' |
'STORAGE_QUOTA_EXCEEDED' | 'TEST_PROJECT_NOT_FOUND' | ...
message: string // human readable
human_readable_message: string // 中文用户提示
recovery_hints: string[] // Ximind 可基于此重试
context?: Record<string, unknown> // 错误上下文(deviceId / projectId 等)
}
11.6 落地检查清单(每 fork 派发前必填)
- 已识别 N 个"大模型可读状态"(列字段 + 端点)· 见 §11.1 共 6 路
- 已识别 M 个"大模型可写操作"(列端点 + 语义)· 见 §11.2 共 7 端点
- 已设计审计日志路径 · 见 §11.4
- 已定义 error 结构 · 见 §11.5
- 业务流程关键 Step 含 description 字段 · 见 §11.3 三类对象
- 已显式标注哪些 fork 服务 Ximind 兼容性:fork 1(API 端点 + WS 推送)· fork 6(CaptureRecord description 字段)· fork 7(capture 弹框 description 输入)· fork 8(e2e 验证 Ximind 可读字段完整)
11.7 Ximind 落地范例(用户场景)
用户对 Ximind:"帮我看看刚才那次 capture 的 1kHz 处响应 · 比 golden 高多少"
Ximind:
1. 调 GET /api/realtime/state · 知道当前在 testProject "speaker-a-tuning"
2. 调 storageEngine.listCaptures("speaker-a-tuning", { widgetType: 'fft' }) · 拿 capture 列表
3. 找 description 含 "after-eq" 的 capture + golden 标记 capture
4. 解析 SerializedCurveData.fft · 找 freqs 中接近 1000 的 bin · 比较 magsDb
5. 回:"after-eq capture 在 1kHz 处比 golden 高 3.2dB · 建议在 EQ 处再降 3dB"
这个范例说明本 ADR 设计"description 字段 + structured data + structured API"对 Ximind 的关键性。