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 生成) |