跳转至
ACCEPTED

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 主仓真值核查报告(AudioDeviceService C# 已就位 / 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 + DataAvailableMeterDataFrame
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)

关注点 路径 现状
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)
关注点 路径 现状
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.vuelinkStore.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 边界铁律(强制约束)

  1. 不动 ADR-12 §3 7 类 MeasurementNode 业务行为契约(已通过 e2e · 3a8d376 Phase 4 真值)· 本 ADR 仅扩展 § Storage 子能力对接 capture
  2. 不动 contract-v1.0(已 frozen · realtime 走 v2 命名空间)
  3. builtin link frozen:realtime-loopback.json 用户不可改 · UI 不显示 edit 按钮 · 修改需新起 ADR
  4. realtime session 与 xilink engine 互斥:同时只能 1 个 running(用户拍板 Q3.1=A · 简化 audio device 抢占)
  5. 测试项目目录与 xilink project 完全独立:data/realtime_test_projects/ vs data/projects/(用户拍板 Q4.1=A4)
  6. ADR-12 §2.11 11 项铁律完全继承:含响应式横竖屏 + design-token 主题切换 · 本 ADR 所有 fork prompt 必含 · 严禁硬编码 hex
  7. 议题 5 节点 tap 能力 + 议题 6 完整 APX sequence 本期禁止实施:仅留 Schema 占位 · 必须新起 ADR-AIOS-14 推动
  8. 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# WasapiCaptureInvalidOperationException 顶栏红警 + "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)
})

① 输入/输出契约

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.4IndexedDB 架构保留作历史参考
> 迁移提交: `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

  • 5ea9806 ADR-12 §5.3 #11 P0.U-bottom-dock-storage-engine(SmartStorageEngine v1 · 本 ADR fork 6 升级 v2)
  • b4a8ea2 ADR-12 §5.3 #10 P0.U-engine-session-snapshots(LeftDock § Engine · 本 ADR fork 5 扩展)
  • 48cf0ba ADR-12 #8 P5.U-meter-tap-multi-tool(7 toolKind 路由 · 本 ADR fork 3 复用)
  • 153a109 ADR-12 #9 P7.U-analyze-extensions(sidecar 5 端点 · 本 ADR 完全复用 · 0 改动)
  • 8379de2 ADR-12 #5 P0.U-measurement-rms-fft-phase(3 widget 真业务 · 本 ADR fork 7 加 capture overlay)
  • 3a8d376 ADR-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 的关键性。