跳转至
DRAFT

LinkFrameBuilder · ports[] 双格式解析

AI 生成 · 待 owner review

本文档由 DocAgent S2 模式自动生成(基于 git commit 3c0c109 的真实 diff)。status: draft,owner 工程师 review 通过后请把 status 改为 published,并删除 frontmatter 中的 generated_by 字段。

1. 概述

LinkFrameBuilder.cs 在构建模块链路帧时,需要解析项目 JSON 中每个模块的 ports[] 数组,提取每个端口的运行时配置(channels / sampleRate / blockSize / dataType / isComplex)。

3c0c109 提交在该解析路径上引入双格式自动检测: - 新格式(优先):ModulePortDef 嵌套结构 — 端口 metadata 在 portDefVal.{id, direction},运行时值在 portDefVal.portdefRuntime.{numPortChannels, numPortSampleRate, numPortBlockSize, signalType, isComplex} - 旧格式(回退):PortInfo 平铺结构 — 所有字段直接写在端口对象顶层({id, direction, channels, sampleRate, blockSize, dataType, isComplex})

检测依据:端口对象上是否存在 portDefVal 属性。这保证旧版本项目文件继续可用,同时新版本的 ModulePortDef 嵌套格式可以无缝接入。

2. 代码改动锚点

改动类型 文件 行号 说明
修改 Services/LinkFrameBuilder.cs 108-156 foreach (var p in portsEl.EnumerateArray()) 内部解析逻辑改写为双分支(新/旧格式)

完整 diff 见 git show 3c0c109 -- AlgoDepartment/04_development/backend_csharp/Services/LinkFrameBuilder.cs

3. 接口签名变化

foreach (var p in portsEl.EnumerateArray())
{
    var pid  = p.TryGetProperty("id",         out var pidEl) ? pidEl.GetString() ?? "" : "";
    var dir  = p.TryGetProperty("direction",  out var dirEl) ? dirEl.GetString() ?? "input" : "input";
    int pch  = p.TryGetProperty("channels",   out var chEl)  ? (chEl.ValueKind  == JsonValueKind.Number ? chEl.GetInt32()  : 0) : 0;
    int psr  = p.TryGetProperty("sampleRate", out var srEl)  ? (srEl.ValueKind  == JsonValueKind.Number ? srEl.GetInt32()  : sampleRate) : sampleRate;
    int pblk = p.TryGetProperty("blockSize",  out var bsEl)  ? (bsEl.ValueKind  == JsonValueKind.Number ? bsEl.GetInt32()  : blockSize)  : blockSize;
    var dt   = p.TryGetProperty("dataType",   out var dtEl)  ? dtEl.GetString() ?? "float32" : "float32";
    bool cplx = p.TryGetProperty("isComplex", out var cxEl)
        && (cxEl.ValueKind == JsonValueKind.True
            || (cxEl.ValueKind == JsonValueKind.String && cxEl.GetString() == "1")
            || (cxEl.ValueKind == JsonValueKind.Number && cxEl.GetDouble() != 0));
    // ... 0 值兜底 + 写入 portInfos
}
foreach (var p in portsEl.EnumerateArray())
{
    string pid, dir, dt;
    int pch, psr, pblk;
    bool cplx;

    // 检测是否为新格式(ModulePortDef)还是旧格式(PortInfo)
    if (p.TryGetProperty("portDefVal", out var portDefVal))
    {
        // 新格式:ModulePortDef
        pid = portDefVal.TryGetProperty("id",        out var pidEl) ? pidEl.GetString() ?? "" : "";
        dir = portDefVal.TryGetProperty("direction", out var dirEl) ? dirEl.GetString() ?? "input" : "input";

        if (portDefVal.TryGetProperty("portdefRuntime", out var rtEl))
        {
            pch  = rtEl.TryGetProperty("numPortChannels",   out var cEl) ? ... : 0;
            psr  = rtEl.TryGetProperty("numPortSampleRate", out var sEl) ? ... : sampleRate;
            pblk = rtEl.TryGetProperty("numPortBlockSize",  out var bEl) ? ... : blockSize;
            dt   = rtEl.TryGetProperty("signalType", out var dtEl) ? dtEl.GetString() ?? "float32" : "float32";
            cplx = rtEl.TryGetProperty("isComplex",  out var cxEl) && ...;
        }
        else { /* 全部回退到全局值 */ }
    }
    else
    {
        // 旧格式(PortInfo):直接读顶层字段(同 Before)
        ...
    }

    if (pch  == 0) pch  = channels;   // 0 = 动态/继承,使用全局值
    if (pblk == 0) pblk = blockSize;
    portInfos.Add(new PortInfoEntry(pid, dir, pch, psr, pblk, dt, cplx));
}

4. 双格式 schema 对照

4.1 新格式(ModulePortDef · 推荐)

{
  "ports": [
    {
      "portDefVal": {
        "id": "in0",
        "direction": "input",
        "portdefRuntime": {
          "numPortChannels":   2,
          "numPortSampleRate": 48000,
          "numPortBlockSize":  128,
          "signalType":        "float32",
          "isComplex":         false
        }
      }
    }
  ]
}

4.2 旧格式(PortInfo · 回退)

{
  "ports": [
    {
      "id":         "in0",
      "direction":  "input",
      "channels":   2,
      "sampleRate": 48000,
      "blockSize":  128,
      "dataType":   "float32",
      "isComplex":  false
    }
  ]
}

4.3 字段映射表

PortInfoEntry 字段 新格式路径 旧格式路径 缺省值
pid portDefVal.id id ""
dir portDefVal.direction direction "input"
pch (channels) portDefVal.portdefRuntime.numPortChannels channels 0 → 兜底为全局 channels
psr (sampleRate) portDefVal.portdefRuntime.numPortSampleRate sampleRate 全局 sampleRate
pblk (blockSize) portDefVal.portdefRuntime.numPortBlockSize blockSize 0 → 兜底为全局 blockSize
dt (dataType) portDefVal.portdefRuntime.signalType dataType "float32"
cplx (isComplex) portDefVal.portdefRuntime.isComplex isComplex false

5. 解析流程图

flowchart TD
    A[ports[] 数组项 p]:::xyL3 --> B{p 含 portDefVal?}:::xyL2
    B -->|是 · 新格式| C[读 portDefVal.id / direction]:::xyL3
    C --> D{含 portdefRuntime?}:::xyL2
    D -->|是| E[读 numPortChannels<br/>numPortSampleRate<br/>numPortBlockSize<br/>signalType / isComplex]:::xyL3
    D -->|否| F[全部回退到全局值]:::xyL5
    B -->|否 · 旧格式| G[读顶层 id / direction<br/>channels / sampleRate<br/>blockSize / dataType / isComplex]:::xyL3
    E --> H[0 值兜底:<br/>pch=0 → channels<br/>pblk=0 → blockSize]:::xyL3
    F --> H
    G --> H
    H --> I[portInfos.Add PortInfoEntry]:::xyL0

    classDef xyL0 fill:#2E8D7E,stroke:#2E8D7E,color:#fff
    classDef xyL2 fill:#E8C9A0,stroke:#E8C9A0,color:#000
    classDef xyL3 fill:#D4A574,stroke:#D4A574,color:#fff
    classDef xyL5 fill:#9D4EDD,stroke:#9D4EDD,color:#fff

6. 影响面

  • 调用方:LinkFrameBuilder.Build* 内部消费 portsEl,外部无 API 签名变化(仅内部解析逻辑双格式化)
  • 兼容性:Non-breaking
    • 旧项目文件(无 portDefVal)→ 走旧分支,行为与 3c0c109 之前完全一致
    • 新项目文件(含 portDefVal)→ 走新分支,自动读取 portdefRuntime 嵌套字段
  • 迁移指引:无需迁移。新版前端/项目生成器输出 ModulePortDef 嵌套格式即可,后端自动接住

7. 测试验证

测试类型 路径 说明
Unit AlgoDepartment/04_development/backend_csharp/TuningTool.Backend.Tests/ 建议新增 LinkFrameBuilderTests.ParsesPorts_NewFormat / ParsesPorts_OldFormat 两个用例,分别构造 §4.1 / §4.2 JSON 并断言 PortInfoEntry 字段值
Integration AlgoDepartment/04_development/backend_csharp/test/integrationtest/ 用一个真实新格式 link.json 跑端到端,确认 DSP 链路加载成功

8. 关键决策

为什么用"端口对象上是否含 portDefVal"作为格式探测信号?

候选方案有 3 个: (1) 项目根 JSON 加 schemaVersion 字段; (2) 端口对象上看是否含 portDefVal(本次采用); (3) 启动时由前端显式声明格式。 选 (2) 是因为它是完全本地的、O(1) 的、零 schema 升级成本的检测 —— 不需要前端配合,也不需要在每个文件加版本号。代价是新旧格式的字段名不能冲突,但实际上 portDefVal 是新格式独有的命名空间,天然无冲突风险。

9. Changelog

版本 日期 改动
0.1.0 2026-05-13 首版(DocAgent 基于 git 3c0c109 生成)