第一章:Go语言蓝牙测试框架的开源背景与核心价值
开源生态的迫切需求
近年来,物联网设备爆发式增长,蓝牙协议栈(BLE 4.2/5.x)在智能穿戴、医疗传感器、工业网关等场景中承担关键通信角色。然而,主流测试工具如nRF Connect、Wireshark仅提供交互式抓包或上位机模拟,缺乏可编程、可集成、跨平台的自动化测试能力。Go语言凭借其静态编译、轻量协程和原生跨平台支持,天然适配嵌入式边缘测试场景——这催生了gobluetooth等开源框架的诞生,填补了CI/CD流水线中蓝牙设备回归测试的空白。
Go语言的独特优势
- 零依赖二进制分发:
go build -o bt-tester ./cmd/tester生成单文件可执行程序,直接部署至树莓派、x86工控机等资源受限环境; - 并发驱动测试流:利用
goroutine + channel实现多设备并行扫描、连接与GATT读写,避免传统Python方案因GIL导致的吞吐瓶颈; - 标准库深度集成:通过
os/exec调用系统BlueZ(Linux)或CoreBluetooth(macOS)CLI工具,无需绑定C库,降低维护复杂度。
核心价值体现
该框架将蓝牙测试从“手动点击”升级为“声明式脚本”,典型用例包括:
// 示例:自动发现并验证心率服务(UUID: 0000180d-0000-1000-8000-00805f9b34fb)
tester := NewTester("hci0")
devices, _ := tester.Scan(5 * time.Second) // 扫描5秒
for _, d := range devices {
if d.Name == "HR-Monitor-01" {
conn, _ := d.Connect() // 建立LE连接
svc, _ := conn.DiscoverService("0000180d-0000-1000-8000-00805f9b34fb")
char, _ := svc.FindCharacteristic("00002a37-0000-1000-8000-00805f9b34fb") // 心率测量
data, _ := char.Read() // 读取原始心率值
fmt.Printf("Heart Rate: %d bpm\n", ParseHeartRate(data))
}
}
| 价值维度 | 传统方案痛点 | Go框架解决方案 |
|---|---|---|
| 可维护性 | Shell脚本混杂CLI参数,难以调试 | 结构化Go代码+单元测试覆盖率>85% |
| 环境一致性 | Python虚拟环境依赖冲突频发 | GOOS=linux GOARCH=arm64 go build 一键交叉编译 |
| 测试可观测性 | 日志分散于GUI日志窗口与终端输出 | 内置JSON格式化报告,直连Prometheus监控 |
第二章:蓝牙协议栈抽象与Go语言建模实践
2.1 BLE GAP/GATT协议状态机的Go结构体建模
BLE设备交互依赖GAP(Generic Access Profile)与GATT(Generic Attribute Profile)双层状态协同。在Go中,需将协议规范中的离散状态、事件驱动转换为可组合、线程安全的结构体模型。
核心状态枚举与角色抽象
type BLERole uint8
const (
Peripheral BLERole = iota // 广播+响应连接
Central // 扫描+主动连接+读写特征值
)
type GAPState uint8
const (
GAPIdle GAPState = iota
GAPAdvertising
GAPScanning
GAPConnected
)
BLERole 区分设备主从角色,影响状态迁移路径;GAPState 映射蓝牙核心规范v5.4中第6章定义的GAP状态机,iota确保无间隙序号,便于位运算与状态校验。
GATT会话生命周期建模
| 字段 | 类型 | 说明 |
|---|---|---|
| ConnHandle | uint16 | 链路层连接句柄,唯一标识 |
| MTU | uint16 | 当前协商的ATT最大传输单元 |
| ServerReady | atomic.Bool | 原子标志,避免GATT服务未就绪即响应读请求 |
状态迁移逻辑(简化版)
graph TD
A[GAPIdle] -->|StartAdv| B[GAPAdvertising]
B -->|ConnectReq| C[GAPConnected]
C -->|Disconnect| A
C -->|GATT_Read| D[GATTBusy]
D -->|ReadComplete| C
组合式状态机结构
type BLEDevice struct {
role BLERole
gapState GAPState
gattSess *GATTSession
mu sync.RWMutex
}
BLEDevice 封装角色、GAP状态与GATT会话,sync.RWMutex 保障并发访问下状态一致性;gattSess 指针支持延迟初始化与热替换,契合BLE服务动态加载场景。
2.2 Peripheral模拟器的事件驱动架构设计与goroutine协程调度
Peripheral模拟器采用事件驱动模型解耦硬件行为与时间推进:每个外设(如UART、I2C)注册事件处理器,由中央事件循环分发带时间戳的Event结构体。
核心事件结构
type Event struct {
Type EventType // e.g., UART_RX_READY, TIMER_EXPIRED
Timestamp int64 // nanoseconds since simulation start
Payload interface{} // device-specific data (e.g., *uart.Frame)
}
Timestamp驱动严格时序调度;Payload实现类型安全泛化,避免反射开销。
goroutine调度策略
- 每个外设实例独占1个长期运行的goroutine
- 事件队列使用
chan Event配合select非阻塞接收 - 高频设备(如PWM)启用批处理模式,合并微秒级事件
事件分发流程
graph TD
A[Hardware Stimulus] --> B{Event Generator}
B --> C[Priority Queue<br/>sorted by Timestamp]
C --> D[Scheduler Loop]
D --> E[Dispatch to Device Goroutine]
| 调度模式 | 延迟上限 | 适用场景 |
|---|---|---|
| 即时唤醒 | 中断响应 | |
| 批量唤醒 | ≤ 1μs | 多帧UART接收 |
| 周期性Tick同步 | 可配置 | RTC/Timer外设 |
2.3 信号强度(RSSI)注入机制:从物理层仿真到应用层API暴露
RSSI注入并非简单赋值,而是跨层协同的闭环流程:物理层仿真生成可信衰减样本 → 链路层校准补偿路径损耗 → 网络层封装为标准事件 → 应用层通过RssiInjector API 实时订阅。
数据同步机制
采用环形缓冲区+时间戳双校验,确保毫秒级注入时序一致性:
class RssiInjector:
def inject(self, rssi_dbm: float, timestamp_ns: int, source_id: str):
# rssi_dbm: 实测/仿真RSSI值(-128 ~ 0 dBm),需满足IEEE 802.15.4规范
# timestamp_ns: 纳秒级采样时刻,用于抵消PHY处理延迟(典型偏移 23μs)
# source_id: 唯一射频源标识符,支持多节点并发注入
self._ring_buffer.push((rssi_dbm, timestamp_ns, source_id))
关键参数映射表
| 层级 | 参数名 | 取值范围 | 作用 |
|---|---|---|---|
| PHY | noise_floor |
-100 ~ -80 dBm | 决定最小可检测RSSI阈值 |
| MAC | path_loss_exp |
2.0 ~ 4.5 | 控制距离衰减非线性度 |
注入流程
graph TD
A[PHY仿真器] -->|高斯白噪声+路径损耗模型| B[MAC校准模块]
B -->|补偿天线增益/信道偏移| C[NetStack事件总线]
C -->|发布RssiEvent| D[Android BluetoothLeScanner]
2.4 可控丢包率仿真:基于时间窗口滑动队列与概率丢弃策略实现
网络协议测试需精准复现链路异常,丢包率可控是关键。本方案采用滑动时间窗口队列记录最近 W 毫秒内所有数据包,并在出队前按动态概率决策是否丢弃。
核心设计要点
- 时间窗口长度
window_ms决定状态记忆深度(默认100ms) - 实时计算当前窗口内已入队包数
N,维持队列时间有序性 - 丢弃概率
p独立配置,支持毫秒级瞬时调整
丢包判定逻辑(Python伪代码)
from collections import deque
import time
import random
class LossSimulator:
def __init__(self, window_ms=100, loss_prob=0.1):
self.window_ms = window_ms
self.loss_prob = loss_prob
self.queue = deque() # 存储 (timestamp, packet_id)
def on_packet_in(self, packet_id):
now = time.time_ns() // 1_000_000 # ms精度
self.queue.append((now, packet_id))
# 清理超窗旧包
cutoff = now - self.window_ms
while self.queue and self.queue[0][0] < cutoff:
self.queue.popleft()
# 概率丢弃:仅对新入队包生效(避免重复判定)
return random.random() > self.loss_prob # True=转发,False=丢弃
逻辑分析:队列按纳秒级时间戳排序,
on_packet_in()先清理过期项再判定;loss_prob直接控制期望丢包率,无需统计归一化——因每个包独立采样,大数定律保障长期稳定性。参数window_ms影响突发丢包的平滑性,值越小响应越快但抖动越大。
性能对比(典型场景)
| 窗口大小 | 吞吐延迟波动 | 内存占用 | 适用场景 |
|---|---|---|---|
| 50ms | ±0.8ms | 低 | 高实时音视频 |
| 200ms | ±2.1ms | 中 | TCP拥塞控制验证 |
| 1000ms | ±6.3ms | 高 | 长周期链路模拟 |
graph TD
A[新包到达] --> B{加入时间队列}
B --> C[裁剪过期包]
C --> D[生成[0,1)随机数]
D --> E{随机数 > loss_prob?}
E -->|是| F[转发]
E -->|否| G[丢弃]
2.5 蓝牙地址、服务UUID、特征值的动态注册与热重载支持
传统蓝牙嵌入式固件中,设备地址、GATT服务UUID及特征值均硬编码于Flash,修改需重新编译烧录。现代低功耗蓝牙(BLE)框架(如Zephyr v3.4+、Nordic nRF Connect SDK v2.7+)已支持运行时动态注册。
动态注册核心接口
// Zephyr BLE GATT 动态服务注册示例
static struct bt_gatt_service *dynamic_svc;
static struct bt_uuid_128 svc_uuid = BT_UUID_INIT_128(
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00);
int err = bt_gatt_service_register(&svc);
// err == 0 表示注册成功,可立即被中央设备发现
bt_gatt_service_register() 将服务结构体挂载至运行时GATT数据库,不依赖静态链接;svc_uuid 支持运行时构造,避免预定义宏污染命名空间。
热重载触发机制
- 修改特征值属性(如
BT_GATT_CHRC_READ→BT_GATT_CHRC_NOTIFY)后调用bt_gatt_service_changed() - 通过
CONFIG_BT_SETTINGS+ Flash NVS 实现重启持久化 - 支持 OTA 下发新UUID字节数组,经校验后调用
bt_uuid_create_128()动态解析
| 阶段 | 触发方式 | 影响范围 |
|---|---|---|
| 地址重载 | bt_id_create() |
广播/连接层 |
| UUID刷新 | bt_gatt_service_unregister() + register |
GATT发现流程 |
| 特征值更新 | bt_gatt_notify() + bt_gatt_attr_set() |
数据通路实时生效 |
graph TD
A[OTA接收新配置] --> B{校验通过?}
B -->|是| C[解析UUID/特征元数据]
B -->|否| D[回滚至上一版本]
C --> E[注销旧服务]
E --> F[注册新服务实例]
F --> G[广播Service Changed]
第三章:框架核心组件的工程化实现
3.1 基于gatt和bluetoothd兼容层的跨平台Peripheral模拟引擎
为统一Linux(bluez)、macOS(CoreBluetooth)与Windows(WinRT Bluetooth)的Peripheral行为,本引擎在用户态构建抽象层,将GATT Server语义映射至各平台原生API。
核心架构设计
// gatt_simulator.h:跨平台GATT服务注册接口
int gatt_periph_register_service(
uint16_t svc_uuid, // 主服务UUID(如 0x180A)
const gatt_char_t* chars, // 特征值数组,含属性、权限、回调
size_t char_count // 特征值数量(最大16)
);
该函数屏蔽了bluez D-Bus GattManager1、CBMutableService及BluetoothLEAdvertisementPublisher的差异,统一调用bluetoothd_compat_dispatch()完成底层适配。
平台适配能力对比
| 平台 | GATT Server支持 | 动态特征添加 | 广播数据合成 |
|---|---|---|---|
| Linux (bluez) | ✅(通过experimental API) | ⚠️需重启service | ✅(advertising-manager) |
| macOS | ✅(CBPeripheralManager) | ✅ | ✅(CBAdvertisementData) |
| Windows | ✅(BluetoothLEServer) | ❌(静态声明) | ✅(BluetoothLEAdvertisement) |
graph TD
A[应用层GATT定义] --> B{兼容层路由}
B --> C[bluez: D-Bus + btgatt-server]
B --> D[macOS: CBPeripheralManager]
B --> E[Windows: BluetoothLEServer]
3.2 RSSI噪声模型与路径损耗算法在Go中的数值稳定性实现
噪声建模:高斯-对数正态混合扰动
RSSI观测值易受多径、阴影及硬件量化误差影响。采用 math/rand.NormFloat64() 生成零均值高斯分量,叠加 lognormal(μ=-0.8, σ=0.3) 模拟衰落非对称性,避免负RSSI溢出。
路径损耗核心计算(Log-Distance + Floor Effect)
func PathLoss(d float64, n float64, d0 float64, pl0 float64, floorDBm float64) float64 {
if d <= d0 {
return math.Max(pl0, floorDBm) // 防止近距离过拟合
}
pl := pl0 + 10*n*math.Log10(d/d0)
return math.Max(pl, floorDBm) // 硬截断保障下界稳定性
}
逻辑说明:
d0=1.0为参考距离(单位:米);n为环境衰减指数(典型值2.0–4.5);pl0是d0处实测基准损耗;floorDBm(如-95.0)防止远距离浮点下溢导致NaN或极端负值,math.Max替代条件分支提升 SIMD 友好性。
数值稳定性关键策略
- ✅ 使用
math.Log10而非math.Log,规避底数转换引入的额外舍入误差 - ✅ 所有输入参数经
math.Abs和math.Max(d, 1e-6)预检,杜绝log(0)或负距离 - ✅ 输出统一通过
float64保持 IEEE-754 双精度动态范围
| 误差源 | Go防护机制 |
|---|---|
| 对数输入为零 | d = math.Max(d, 1e-6) |
| 远距离溢出 | math.Max(pl, floorDBm) |
| NaN传播 | 无未校验 math.NaN() 调用 |
3.3 丢包率控制模块的实时性保障:epoll/kqueue集成与零拷贝缓冲区设计
零拷贝环形缓冲区结构设计
采用 mmap 映射的共享内存环形缓冲区,规避内核/用户态数据拷贝:
struct zc_ring {
uint32_t head __attribute__((aligned(64))); // 生产者位置(cache line对齐)
uint32_t tail __attribute__((aligned(64))); // 消费者位置
uint8_t data[]; // 动态映射区,大小为 2^N
};
head/tail 使用原子操作更新;data[] 直接映射网卡 DMA 区域,实现报文指针零拷贝传递。
epoll/kqueue 事件驱动调度
统一抽象层封装 I/O 多路复用:
| 接口 | Linux (epoll) | BSD/macOS (kqueue) |
|---|---|---|
| 注册fd | epoll_ctl(ADD) |
kevent(EV_ADD) |
| 等待事件 | epoll_wait() |
kevent() |
| 边缘触发 | EPOLLET |
EV_CLEAR + NOTE_TRIGGER |
数据同步机制
使用内存屏障(__atomic_thread_fence(__ATOMIC_ACQ_REL))确保 head/tail 可见性,避免重排序导致的读写竞争。
graph TD
A[网卡DMA写入] --> B[原子更新tail]
B --> C[epoll/kqueue通知就绪]
C --> D[原子读取head/tail计算可用slot]
D --> E[直接指针访问data[]]
第四章:端到端测试场景构建与验证方法论
4.1 构建多连接并发Peripheral集群:连接风暴与资源隔离验证
在BLE Mesh网关场景中,单节点需同时维持32+ Peripheral连接,易触发内核套接字耗尽与HCI缓冲区溢出。
连接风暴模拟脚本
# 模拟50设备并发连接请求(非阻塞)
import asyncio
from bleak import BleakClient
async def storm_connect(addr):
try:
client = BleakClient(addr, timeout=3.0)
await client.connect()
return f"{addr}: OK"
except Exception as e:
return f"{addr}: {type(e).__name__}"
# 并发启动32任务,限制并发度为8防压垮控制器
results = await asyncio.gather(
*[storm_connect(f"AA:BB:CC:DD:EE:{i:02X}") for i in range(32)],
return_exceptions=True
)
逻辑分析:asyncio.gather配合return_exceptions=True捕获连接异常;timeout=3.0防止HCI挂起;并发度未显式限流,依赖事件循环调度——实际部署需嵌入asyncio.Semaphore(8)实现资源隔离。
资源隔离关键参数
| 参数 | 推荐值 | 说明 |
|---|---|---|
hci.max_conn |
64 | 内核模块加载参数,需modprobe btusb max_conn=64 |
bluetoothd --compat |
启用 | 兼容旧版Peripheral的连接握手时序 |
BLE_GAP_ROLE_PERIPH |
静态分配 | 避免角色动态切换引发状态机冲突 |
graph TD
A[Central发起连接] --> B{连接数 < 隔离阈值?}
B -->|是| C[分配独立GATT上下文]
B -->|否| D[拒绝新连接并触发告警]
C --> E[绑定专属HCI通道与ACL缓冲区]
4.2 信号衰减梯度测试:移动轨迹模拟与RSSI时序数据回放
为复现真实移动场景下的信道衰落特性,系统采用参数化轨迹生成器驱动RSSI回放引擎。
数据同步机制
采用时间戳对齐策略,将轨迹点(x, y, t)与实测RSSI序列按毫秒级精度绑定:
# 轨迹-信号联合回放核心逻辑
def replay_trajectory(rssi_series, traj_points, sample_rate=10): # Hz
for i, (x, y, t_ms) in enumerate(traj_points):
idx = int(t_ms * sample_rate // 1000) # 映射至RSSI采样索引
if idx < len(rssi_series):
yield x, y, rssi_series[idx]
sample_rate 决定空间分辨率;t_ms 来自轨迹插值时间轴,确保物理位移与信号衰减严格同步。
回放性能指标
| 指标 | 目标值 | 测量方式 |
|---|---|---|
| 时间对齐误差 | ≤ 15 ms | NTP校准时钟比对 |
| RSSI动态范围 | −110 ~ −30 dBm | 校准接收机实测 |
graph TD
A[轨迹CSV] --> B[时间归一化]
C[RSSI二进制流] --> D[帧率重采样]
B & D --> E[时空对齐缓冲区]
E --> F[实时射频注入]
4.3 高丢包率下GATT事务鲁棒性压测:Write Without Response重试策略验证
在BLE链路丢包率达35%的严苛信道下,Write Without Response(ATT_OP_WRITE_CMD)因无ACK机制易导致数据静默丢失。需构建带状态感知的自适应重试框架。
重试决策逻辑
def should_retry(attempt: int, rssi: int, last_rtt_ms: float) -> bool:
base = attempt <= 3 # 基础重试上限
adaptive = rssi > -85 and last_rtt_ms < 120 # 信道质量达标才续试
return base and adaptive
attempt控制激进度;rssi与last_rtt_ms联合评估链路稳定性,避免在深度衰落时盲目重发加剧拥塞。
重试策略效果对比(丢包率35%)
| 策略 | 成功率 | 平均延迟(ms) | 能耗增量 |
|---|---|---|---|
| 固定3次重试 | 68.2% | 94 | +21% |
| RSSI+RTT自适应 | 91.7% | 76 | +12% |
重试状态机
graph TD
A[发起Write CMD] --> B{ACK超时?}
B -- 是 --> C[记录失败上下文]
C --> D{满足should_retry?}
D -- 是 --> E[指数退避后重发]
D -- 否 --> F[标记失败并回调]
B -- 否 --> G[事务成功]
4.4 与真实Central设备(iOS/Android)的互操作性基准测试流程
测试环境准备
- 搭建 BLE 信标集群(nRF52840 + Zephyr RTOS)
- iOS 设备:iPhone 13(iOS 17.6),启用 CoreBluetooth 后台模式
- Android 设备:Pixel 7(Android 14,启用
BLUETOOTH_CONNECT权限)
数据同步机制
使用 GATT 特征值 00002a19-0000-1000-8000-00805f9b34fb(Battery Level)作为基准读写通道:
// Zephyr 端:注册可写特征值(含响应确认)
static struct bt_gatt_attr attrs[] = {
BT_GATT_PRIMARY_SERVICE(&battery_svc_uuid),
BT_GATT_CHARACTERISTIC(&battery_level_uuid,
BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE_WITHOUT_RESP,
BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
read_battery, write_battery, NULL),
};
// write_battery() 中调用 bt_gatt_notify() 触发 iOS/Android 回调
逻辑分析:WRITE_WITHOUT_RESP 降低 iOS 的 ATT 层重传开销;bt_gatt_notify() 确保 Android onCharacteristicChanged() 可靠触发。参数 NULL 表示无用户数据上下文指针,由服务实例隐式管理。
跨平台时序对齐策略
| 平台 | 连接超时 | 最小连接间隔 (ms) | 通知延迟中位数 |
|---|---|---|---|
| iOS | 30s | 15 | 22 ms |
| Android 14 | 5s | 7.5 | 18 ms |
graph TD
A[Central发起连接] --> B{平台判定}
B -->|iOS| C[启动CBCentralManager delegate]
B -->|Android| D[注册BluetoothGattCallback]
C --> E[等待didConnectPeripheral]
D --> F[等待onConnectionStateChange: CONNECTED]
E & F --> G[批量读Battery Level × 100]
第五章:项目开源现状、社区共建与未来演进方向
开源托管与许可证实践
项目已完整托管于 GitHub 主仓库(org/aiops-core),采用 Apache License 2.0 协议,明确允许商业使用、修改与分发。截至 2024 年 10 月,代码库包含 17 个活跃分支,其中 main 分支通过 CI/CD 流水线实现每日自动化构建与集成测试,覆盖 92.3% 的核心模块单元测试覆盖率(基于 Jest + Pytest 混合测试框架)。关键依赖项如 prometheus-client-python 和 langchain-core 均经 SBOM(软件物料清单)扫描验证,无已知 CVE-8.x 及以上高危漏洞。
社区贡献结构与治理机制
社区采用双轨制协作模型:
- 核心维护组:由 5 名 TSC(技术指导委员会)成员组成,负责版本发布、架构决策与安全响应;
- 领域工作组:包括可观测性、大模型推理适配、边缘部署三个常设小组,每组由 3–7 名认证贡献者轮值主持。
下表展示了近半年社区活跃度关键指标:
| 指标 | Q2 2024 | Q3 2024 | 增长率 |
|---|---|---|---|
| 新增 PR 数量 | 142 | 217 | +52.8% |
| 合并 PR 中外部贡献占比 | 63% | 71% | +12.7% |
| Slack 活跃开发者数 | 386 | 529 | +37.0% |
实战落地案例:某省级政务云平台集成
2024 年 7 月,项目被纳入广东省“粤治云”智能运维平台二期建设,完成以下深度定制:
- 将原生指标采集器适配至国产海光 CPU 架构,通过交叉编译与内核模块白名单校验;
- 集成本地化 LLM(通义千问-Qwen2-7B-Int4)实现日志根因分析,平均响应延迟压降至 840ms(P95);
- 输出《政务场景告警降噪白皮书》作为社区标准文档,已被浙江、四川两地政务云团队复用。
跨生态协同进展
项目已与 CNCF 孵化项目 OpenTelemetry 实现双向协议兼容:
- 支持 OTLP v1.8.0 格式直接接入 Trace 数据;
- 提供
otel-collector-contrib插件包,可将 Prometheus Metrics 自动转换为 OTLP Metrics 并注入 Jaeger 后端。
同时,与 LF AI & Data 基金会合作启动「AI-Native Observability」联合实验室,首批 3 个联合实验项目已进入 PoC 阶段,涵盖时序异常检测模型蒸馏、多模态告警摘要生成等方向。
flowchart LR
A[GitHub Issue] --> B{Triage Bot}
B -->|Bug Report| C[Assign to SIG-Observability]
B -->|Feature Request| D[Vote in Community Forum]
C --> E[PR Review w/ DCO Check]
D --> F[Monthly SIG Sync Meeting]
E --> G[Automated E2E Test on ARM64 Cluster]
F --> G
G --> H[Release Candidate Tag]
未来演进路线图
下一阶段重点推进三项能力落地:
- 轻量化边缘运行时:基于 WebAssembly System Interface(WASI)重构采集代理,目标镜像体积 ≤12MB,内存占用
- 策略即代码(Policy-as-Code)引擎:引入 Rego 语言支持动态告警抑制规则编排,已通过 eBPF 实现内核级规则热加载验证;
- 开源合规自动化流水线:集成 FOSSA 与 ClearlyDefined,对每次 PR 扫描许可证兼容性与版权归属,生成 SPDX 2.3 格式报告。
