完全指南:记录、容器、生产者组件与地面解码)
F´ 数据产品Data Products完全指南记录、容器、生产者组件与地面解码【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime数据产品Data Products是 F´F Prime飞行软件框架中用于机上产生、机内存储、按优先级下传的结构化任务数据的完整机制。本文以 docs/user-manual/framework/data-products.md 为核心骨架结合 Fw/Dp 的端口与容器实现、Svc/DpManager、Svc/DpWriter、Svc/DpCatalog 等标准组件源码系统讲解记录Record、容器Container、数据产品字典Dictionary的基本概念生产者组件从 FPP 建模到自生成 C 代码、再到单元测试支持与拓扑接入的完整实践路径。读完本文你将能够在自己的 F´ 项目中定义一个数据产品生产者组件实现申请容器 → 序列化记录 → 发送容器 → 地面解码的完整闭环并理解其底层数据包格式与内存管理原理。快速上手请参阅 docs/how-to/develop/data-products.mdHow To: Generate Data Products本文是该指南的技术深化版。1. 引言什么是数据产品数据产品Data Product指由嵌入式系统产生、在机上存储、并按优先级顺序传输到地面的任意数据。与实时遥测不同数据产品强调生成后暂存、择机批量下传典型场景包括科学采样数据science samples图像瓦片或处理后结果批量传感器数据周期性分析输出存储的系统健康数据。F´ 为数据产品的生成generation、存储storage与下传downlink提供了一整套管理特性覆盖从组件建模、内存分配、落盘存储到地面解码的完整链路。2. 基本概念2.1. 记录Records、容器Containers与数据产品字典DictionaryF´ 数据产品建立在两个核心抽象之上记录Record数据的基本单位。例如一个结构体、一个类型已知且大小静态确定的数组、或一个大小静态未知的字节数组。容器Container拥有一个标识符identifier和一个优先级priority用于存放记录。在 C 中容器表示为一个类对象其成员字段1保存头部数据header data2保存一个指向记录存储内存的Fw::Buffer对象。所有容器规格container specifications的集合构成数据产品字典Data Product Dictionary。F´ 对字典的管理方式与命令commands、遥测telemetry、事件events、参数parameters一致遵循三步规则每个组件C声明自己的记录与容器容器 ID 是组件局部local的通常取 0、1、2、…C的每个实例I为C中定义的每个容器c向字典贡献一个容器I.c其全局标识符 实例I的基地址base identifier 容器c的局部标识符。例如基地址为 0x1000 时全局 ID 可能为 0x1000、0x1001、0x1002、…对于任意拓扑TT中所有组件实例的全局标识符I.c共同构成T的数据产品字典。这一局部声明 实例基地址换算全局 ID的设计使每个组件可以独立、可复用地进行建模最终由拓扑把所有实例拼接成一份完整的全局字典。2.2. F´ 数据产品系统的标准组件典型的数据产品系统中除了任务特定的生产者组件外F´ 提供以下标准管理组件组件职责源码/文档数据产品管理者Svc::DpManager为空容器分配内存并将已填充容器转发给数据产品写入者Svc/DpManager/docs/sdd.md数据产品写入者Svc::DpWriter接收生产者发来的已填充容器将内容写入非易失存储磁盘Svc/DpWriter/docs/sdd.md数据产品目录Svc::DpCatalog维护可用数据产品的数据库通过命令下传与删除数据产品Svc/DpCatalog/docs/sdd.md数据产品处理器Svc::DpCompressProc等接收写入流程中的已填充容器尝试处理其内容并返回修改后的容器典型例子是无损压缩Svc::DpZLibCompressorSvc/DpCompressProc、Svc/DpZLibCompressor[!IMPORTANT] 使用数据产品时你只需要开发生产者组件其余组件全部由 F´ 框架提供。3. 生产者组件Producer Components3.1. 典型活动循环一个生产者组件通常会按需反复执行以下三步活动请求容器向数据管理者组件请求一个容器填充容器收到容器后将记录序列化进容器即用数据填充容器发送容器容器写满后将容器发送给数据产品管理者由其转发给数据产品写入者。FPP 模型与自生成的 C 代码为这三步提供了大量专用支持下面逐一说明。3.2. FPP 建模3.2.1. 专用端口PortsFPP 为数据产品管理提供了四类专用端口1. Product Get 端口类型Fw::DpGet 用于同步向缓冲管理者请求内存的输出端口。请求在调用该端口的线程上被服务并会在该线程上施加互斥锁。声明语法product get port productGetOut2. Product Request 端口类型Fw::DpRequest 用于异步向数据产品管理者请求内存的输出端口。请求在数据产品管理者的线程上被服务。这种方式引入独立线程的开销但请求线程无需加锁。声明语法product request port productRequestOut3. Product Receive 端口类型Fw::DpResponse 用于接收异步请求返回的空容器的输入端口。声明语法async product recv port productRecvIn4. Product Send 端口类型Fw::DpSend 用于将已填充容器发送给数据产品写入者的输出端口。声明语法product send port productSendOut每个数据产品生产者组件必须具备以下端口product get端口与product request端口两者之一可同时具备一个product send端口拥有product request端口的组件必须同时拥有product recv端口。上述端口的具体定义可查看 Fw/Dp/Dp.fppDpGet携带容器 ID$id、请求数据大小dataSize与输出缓冲ref buffer并返回Fw.SuccessDpRequest只发送$id与dataSizeDpResponse回传$id、buffer与statusDpSend携带$id与buffer。3.2.2. 记录Records声明记录是数据单位。定义生产者组件时可以声明一个或多个记录。记录规格 名称 类型说明符 可选标识符。类型说明符有两种形式FPP 类型T记录保存单个T类型的值。T可以是任意 FPP 类型包括结构体或数组类型FPP 类型T后跟关键字array记录是大小静态未知的T类型值的数组数组大小存储在记录中。无论哪种形式T都允许是任意 FPP 类型含 struct、array。示例 A struct with a fixed-size member array struct FixedSizeData { data: [1024] F32 } A record containing fixed-size data product record FixedSizeDataRecord: FixedSizeData id 0x00 A record containing a variable-size array product record F32ArrayRecord: F32 array id 0x01[!NOTE] 记录没有隐式时间。如果容器自带的时间标签不足以满足需求必须在记录类型中加入时间字段参见 docs/how-to/develop/data-products.md 中SinusoidDataType内含timeTag: Fw.TimeValue的示例。记录 ID 会被序列化进容器地面软件据此正确解释记录类型。3.2.3. 容器Containers声明容器是存放记录的数据结构。每个容器可存放组件中声明的任意记录。容器规格 名称 可选标识符 可选默认优先级默认优先级在未为容器指定其他优先级时使用。示例product container C1 product container C2 id 0x01 default priority 10[!TIP] 优先级只是默认值可在运行时被覆盖DpContainer::setPriority。3.3. 自生成的 C 代码Autocoded C生产者组件C的自生成 C 基类提供以下 API 元素1. 枚举与常量定义可用的容器 ID、容器优先级与记录 ID 的枚举。2. 成员类_C_::DpContainer该类派生自Fw::DpContainer是针对C中定义的数据产品特化的容器。_C_::DpContainer的每个实例都是Fw::BufferB的包装B指向已分配内存。该类为C中定义的每个记录R提供一个序列化操作_C_::DpContainer::serialize_ _R_用于把记录序列化进B指向的内存。序列化格式见 Fw/Dp/docs/sdd.md。[!CAUTION] 务必使用自编码生成的局部DpContainer类型而不是直接使用Fw::DpContainer——前者才带有组件专属记录的序列化方法。3.dpGet_c成员函数需要product get端口为每个容器c生成一个dpGet_c函数接收数据大小与数据产品容器引用D。它调用productGetOut通常连接到数据产品管理者名义情况下返回足以存放指定数据大小 数据产品包开销的Fw::BufferB随后用 ID 与B初始化D并返回表示缓冲分配是否成功的状态值。4.dpRequest_c成员函数需要product request端口接收数据大小在productRequestOut通常连接到数据产品管理者上发出缓冲请求请求大小同样足以容纳指定数据大小的数据产品包。5.dpRecv_c_handler纯虚函数需要product recv端口当响应dpRequest调用的新容器到达时自生成代码根据容器 ID 选择并调用对应的dpRecv处理器。C的实现必须覆写每个处理器以提供任务特定的填充行为。处理器的参数提供1容器引用实现可填充之与2表示容器是否有效的状态值——缓冲分配失败会产生无效容器。6.dpSend成员函数用于发送已填充的数据产品容器接收容器引用与可选时间标签。执行过程若未提供时间标签调用timeGetOut获取系统时间并设置为时间标签将时间标签存入容器c在productSendOut上发送c。7. 记录大小的常量表达式单值记录RSIZE_OF_R_RECORD求值为该记录的大小数组记录RSIZE_OF_R_RECORD(size)求值为含size个元素的数组记录R的大小。这些表达式用于计算请求数据产品缓冲时的数据大小。例如组件声明了记录Image则组件实现中10 * SIZE_OF_Image_RECORD表示容纳 10 条Image记录的存储大小。dpGet调用会自动加上容器头部等开销见 docs/how-to/develop/data-products.md 的containerSize RECORD_COUNT * (SIZE_OF_SineRecord_RECORD SIZE_OF_CosineRecord_RECORD);示例。完整的实现流程示例同步分配# Producer.fpp active component Producer { product get port productGetOut product send port productSendOut product record SineRecord: SinusoidDataType id 0 product record CosineRecord: SinusoidDataType id 1 product container SinusoidContainer id 0 default priority 10 }// Producer.cpp // 1. 计算并申请容器 Fw::Success status this-dpGet_SinusoidContainer(containerSize, this-m_container); // 2. 序列化记录 SinusoidDataType sineRecord, cosineRecord; sineRecord.timeTag ...; sineRecord.value ...; cosineRecord.timeTag ...; cosineRecord.value ...; this-m_container.serializeRecord_SineRecord(sineRecord); this-m_container.serializeRecord_CosineRecord(cosineRecord); // 3. 发送容器发送后容器归框架所有不得再修改 this-dpSend(this-m_container);[!WARNING] 分配可能因内存压力失败如缓冲池耗尽生产者必须优雅处理失败路径——跳过本次记录生成、下个周期重试。发送后的容器由框架接管必须重新分配新容器严禁再次使用或修改已发送容器。3.4. 单元测试支持F´ 中每个组件C都自动生成_C_ TesterBase与_C_ GTestBase两类测试基类GTestBase派生自TesterBase提供基于 Google Test 框架的测试宏。编写单元测试时构造类_C_ Tester通常派生自GTestBase若平台不支持 Google Test则_C_ Tester可派生自TesterBase。3.4.1. TesterBase 类历史数据结构Histories端口历史名元素类型字段product get端口productGetHistoryDpGet容器 ID、sizeproduct request端口productRequestHistoryDpRequest容器 ID、size总是存在productSendHistoryDpSend容器 ID、缓冲的浅拷贝历史管理函数函数说明pushProductGetEntry(id, size)构造DpGet历史对象并压入productGetHistory通常由productGet_handler调用productGet_handler(id, size, buffer)测试组件收到被测组件 product get 端口数据时被调用。默认调用pushProductGetEntry并返回FAILURE表示未分配内存、B未更新。virtual可覆写为压入历史、分配缓冲写入B、返回SUCCESSpushProductRequestEntry(id, size)压入DpRequest历史对象productRequest_handler(id, size)默认调用pushProductRequestEntryvirtual可覆写pushProductSendEntry(id, buffer)压入DpSend历史对象productSend_handler(id, buffer)默认调用pushProductSendEntryvirtual可覆写3.4.2. GTestBase 类——验证宏Product Get 宏组件定义数据产品且具备product get端口时提供ASSERT_PRODUCT_GET_SIZE(size)检查productGetHistory的条目数ASSERT_PRODUCT_GET(index, id, size)检查指定历史索引处的容器 ID 与 size。Product Request 宏具备product request端口时提供ASSERT_PRODUCT_REQUEST_SIZE(size)检查productRequestHistory条目数ASSERT_PRODUCT_REQUEST(index, id, size)检查指定索引处的 ID 与 size。Product Send 宏组件定义数据产品时提供ASSERT_PRODUCT_SEND_SIZE(size)检查productSendHistory条目数ASSERT_PRODUCT_SEND(index, id, priority, timeTag, procType, userData, dataSize, buffer)所有参数均为只读输入唯一例外是buffer按引用输出须为Fw::Buffer变量。该宏执行以下检查校验entry.id与指定 ID 匹配反序列化entry.buffer中的数据产品头部校验头部中的容器 ID、优先级、时间标签、处理器类型、用户数据与数据大小与指定值匹配将entry.buffer赋给buffer——宏运行后buffer的反序列化指针指向entry.buffer数据负载的起点可继续编写代码反序列化并检查数据负载。容器 ID 换算被测组件发出的容器 ID 是全局 ID。因此构造期望 ID 时必须把测试组件中指定的 ID 基地址ID_BASE每个 Tester 实现中的标准常量在构造函数中传入 Tester 基类与组件中的局部 ID 相加ID_BASE Component::ContainerId::CONTAINER模拟分配失败可覆写分配处理器例如通过成员变量驱动失败Fw::Success::T ProducerTester::productGet_handler(FwDpIdType id, FwSizeType dataSize, Fw::Buffer buffer) { EXPECT_EQ(dataSize, sizeof(this-m_buffer)); buffer.set(this-m_buffer, dataSize); this-pushProductGetEntry(id, dataSize); return (this-m_allocation_failure) ? Fw::Success::FAILURE : Fw::Success::SUCCESS; }4. 底层实现Fw::DpContainer与序列化格式4.1. 类结构与内存布局Fw/Dp/DpContainer.hpp 定义了基础容器类Fw::DpContainer。其头结构Header通过常量定义了各字段的字节偏移PACKET_DESCRIPTOR_OFFSET0→ID_OFFSET→PRIORITY_OFFSET→TIME_TAG_OFFSET→PROC_TYPES_OFFSET→USER_DATA_OFFSET→DP_STATE_OFFSET→DATA_SIZE_OFFSET。同时定义HEADER_HASH_OFFSET Header::SIZE; // 头部之后 DATA_OFFSET HEADER_HASH_OFFSET HASH_DIGEST_LENGTH; // 数据起点 MIN_PACKET_SIZE Header::SIZE 2 * HASH_DIGEST_LENGTH; // 零数据包大小setBuffer会断言缓冲不小于MIN_PACKET_SIZE并把数据子缓冲m_dataBuffer指向DATA_OFFSET处getPacketSizeForDataSize(dataSize)返回Header::SIZE dataSize 2 * HASH_DIGEST_LENGTH即头部 数据 头部哈希 数据哈希的总包长。shrinkBufferSize可将Fw::Buffer收缩为与实际数据大小一致的包长仅允许收缩不允许增长这正是 DpWriter 在压缩处理后重新解析头部并收缩包长的底层依据。4.2. 序列化包格式数据产品容器序列化后由四部分组成头部Header→ 头部哈希Header Hash→ 数据Data→ 数据哈希Data Hash。完整字段见 Fw/Dp/docs/sdd.md头部Header字段数据类型序列化大小说明PacketDescriptorFwPacketDescriptorTypesizeof(FwPacketDescriptorType)F´ 包描述符FW_PACKET_DPIdFwDpIdTypesizeof(FwDpIdType)容器 ID系统全局 ID 组件局部 ID 组件基地址PriorityFwDpPriorityTypesizeof(FwDpPriorityType)容器优先级TimeTagFw::TimeFw::Time::SERIALIZED_SIZE容器时间标签ProcTypesFw::DpCfg::ProcType::SerialTypesizeof(...SerialType)处理类型位掩码UserDataHeader::UserDataDpCfg::CONTAINER_USER_DATA_SIZE用户可配置数据默认 32 字节DpStateDpStateDpState::SERIALIZED_SIZE数据产品状态DataSizeFwSizeTypesizeof(FwSizeStoreType)数据负载字节数头部哈希 / 数据哈希均为HASH_DIGEST_LENGTH长度见 Utils/Hash/README.md。数据记录序列化单值记录product recordname:typeIdFwDpIdTypeData类型T的序列化数据数组记录product recordname:typearrayIdSizeFwSizeType元素个数nDatan个T元素。DpContainer::serializeHeader/deserializeHeader与哈希校验checkHeaderHash、checkDataHash在 Fw/Dp/DpContainer.cpp 中实现反序列化头部时还校验PacketDescriptor FW_PACKET_DP。DpState枚举在 Fw/Dp/Dp.fpp 中定义UNTRANSMITTED未传输、PARTIAL部分传输、TRANSMITTED已传输。4.3. 配置项数据产品相关配置位于 default/config/DpCfg.fppFw::DpCfg::CONTAINER_USER_DATA_SIZE默认32容器包头中用户可配置数据的大小Fw::DpCfg::ProcType枚举定义选择处理类型的位掩码写入磁盘前对容器应用。取值PROC_TYPE_NONE 0x00、PROC_TYPE_ZLIB_DEFLATE 0x01、PROC_TYPE_ONE 0x02、PROC_TYPE_TWO 0x04。DpWriter按位掩码顺序调用procBufferSendOut处理端口位2^n选择端口索引n。5. 标准组件的职责与协作5.1.Svc::DpManager内存分配与转发Svc/DpManager/docs/sdd.md 定义DpManager是活动组件负责1接收数据产品缓冲请求——同步请求时向缓冲管理者申请Fw::Buffer并直接返回给客户端异步请求时申请缓冲后经productResponseOut回送客户端2接收客户端填好的缓冲并经productSendOut类型Fw.BufferSend转发给下游如Svc::BufferAccumulator或Svc::DpWriter处理最终归还缓冲管理者回收。核心辅助函数getBuffer调用bufferGetOut_out(portNum, size)取缓冲有效则原子递增numSuccessfulAllocations并返回SUCCESS无效则递增numFailedAllocations并发出BufferAllocationFailedwarning high事件。遥测项包括NumSuccessfulAllocations、NumFailedAllocations、NumDataProds、NumBytes。端口数组数量由编译期常量DpManagerNumPortsdefault/config/AcConstants.fpp决定默认与DpWriterNumPorts对齐。若该值大于 1可用一个DpManager实例的多个端口支持多组连接。5.2.Svc::DpWriter处理与落盘Svc/DpWriter/docs/sdd.md 定义DpWriter是活动组件接收包含已填充容器头的缓冲通常来自DpManager也可能经过BufferAccumulator对每个缓冲校验缓冲有效、足够容纳数据产品包、头部可反序列化、头部哈希有效、数据大小不超过缓冲读取头部ProcType位掩码按序调用procBufferSendOut中被置位的端口处理缓冲就地修改内存例如压缩重新解析头部并按新数据大小收缩缓冲对应DpContainer::shrinkBufferSize使用timeGetOut提供的时间戳按文件格式写入磁盘若dpWrittenOut[N]已连接发送文件名、优先级与文件大小通知Svc::DpCatalog可接收该通知更新目录在deallocBufferSendOut[N]上归还有效缓冲必须连接未连接会断言dpWrittenOut[N]可留空。配置要点DpWriterNumPorts默认 5输入/通知/归还三条路由路径的匹配数组数所有路径与schedIn、异步CLEAR_EVENT_THROTTLE命令共享一个消息队列队列容量需按最大缓冲突发 挂起调度滴答 命令数估算DpWriterNumProcPorts处理端口数量DP_FILENAME_FORMATdefault/config/DpCfg.hpp文件名格式串依次含%s文件名前缀通常为目录路径前缀、%PRI_FwDpIdType容器 ID、%PRI_u32时间秒、%PRI_u32时间微秒运行时configure(prefix)可设置 DP 文件名前缀不调用则默认为空字符串。遥测NumBuffersReceived、NumBytesWritten、NumSuccessfulWrites、NumFailedWrites、NumErrors。5.3.Svc::DpCatalog编目与下传Svc/DpCatalog/docs/sdd.md 定义DpCatalog是活动组件管理已生成数据产品的下传。收到命令后扫描指定目录、读取数据产品文件头部元数据并建立目录然后按下传优先级排序列表并经Svc::FileDownlink逐个下传文件。支持在传目录构建后插入新产品addToCat端口接收Svc.DpWritten通知、停止传输、按命令删除/重新调整优先级。排序算法compareEntries依次比较优先级数值小者优先→ 生成时间旧者优先→ 产品 ID小者优先底层使用Fw::RedBlackTreeSet自平衡树begin()恒取最高优先级条目下传状态写入stateFile以便重启后保持。命令序列BUILD_CATALOG扫描目录建目录→START_XMIT_CATALOG按优先级下传→STOP_XMIT_CATALOG停止当前文件传完后完成状态失同步时CLEAR_CATALOG后重建。注意该组件在当前仓库中标注为早期原型只能整文件下传、不能分片中断需整体重传。5.4. 拓扑集成示例在项目拓扑 FPP 中生产者通过DataProducts子拓扑与DpManager相连目录组件经文件下传链路与FileHandling子拓扑相连topology ... { instance FileHandling.Subtopology # For file downlink instance DataProducts.Subtopology instance producer # Connect producers to DpManager connections DataProducers { producer.productGetOut - DataProducts.Subtopology.productGetIn producer.productSendOut - DataProducts.Subtopology.productSendIn } # For connecting the catalog to the file downlink connections FileHandling_DataProducts { DataProducts.Subtopology.dpCatFileOut - FileHandling.Subtopology.fileDownlinkSendFile FileHandling.Subtopology.fileDownlinkFileComplete - DataProducts.Subtopology.dpCatFileDone } }时序上同步请求数据产品缓冲的链路为producer→dpManagerproductGetIn→bufferManagerbufferGetOut→ 返回B→dpManager存B给 producer → 返回SUCCESS异步链路则经productRequestIn→ 申请缓冲 →productResponseOut回送发送链路为producer经productSendIn交dpManager再经productSendOut交dpWriter落盘最后由dpWriter向bufferManager归还缓冲。6. 典型用例与地面解码请求与发送数据产品见 Svc/DpManager/docs/sdd.md 中的示例文中producer即数据产品生产者。写入非易失存储见 Svc/DpWriter/docs/sdd.md。编目与下传数据产品见 Svc/DpCatalog/docs/sdd.md。机上无损压缩见 Svc/DpCompressProc 与 Svc/DpZLibCompressor。地面解码数据产品二进制文件通常带.fdp扩展名可在地面用fprime-dp命令解码——该命令是 F´ GDS 附带的 CLI 工具。基本用法fprime-dp decode --bin-file data_product_file --dictionary path_to_dictionary --output output.jsonfprime-dp decode -h可查看全部选项。解码后的 JSON 可交由任意下游工具进一步分析。仓库中还提供了集成测试示例可参考 TestDeploymentsProject/Ref/DpDemo/test/int/dp_demo_integration_test.py 了解端到端流程。7. 设计考量与最佳实践同步 vs 异步分配同步product get更简单但请求线程会阻塞并加互斥锁异步product requestproduct recv以线程开销换取响应性请求线程无需加锁。应根据任务实时性需求选择。容器尺寸策略可按数量count、时间time或大小size决定何时发送容器。大容器降低分配与发送开销但占用更多 RAM、意外重启时数据损失更大小容器减少重启丢数据量但增加开销。分配失败处理内存压力下分配必然可能失败DpManager 会递增numFailedAllocations并发 warning 事件生产者必须记录容器状态、跳过本周期并优雅重试。容器状态跟踪典型内部流程为Allocate → Fill → Send → Reset建议在组件头文件中将局部DpContainer作为成员变量跟踪状态// In Component.hpp DpContainer m_container; //! Tracked container state测试覆盖验证仅按需分配容器记录跨多次调用累积恰在期望时机发送发送后状态正确复位分配失败被优雅处理等路径对应宏ASSERT_PRODUCT_GET_SIZE/ASSERT_PRODUCT_GET、ASSERT_PRODUCT_SEND_SIZE/ASSERT_PRODUCT_SEND。8. 总结数据产品为 F´ 提供了结构化、可扩展的任务数据生成与管理方式。生产者组件只需在 FPP 中声明记录、容器与专用端口自生成代码即提供dpGet/dpRequest/dpRecv/dpSend与serialize_系列 API框架侧的DpManager分配、DpWriter落盘、DpCatalog编目下传与可选的DpCompressProc/DpZLibCompressor压缩构成完整的存储转发流水线Fw::DpContainer的哈希防护包格式保证了数据完整性fprime-dp命令完成地面解码闭环。深入源码可继续阅读 Fw/Dp、Svc/DpManager、Svc/DpWriter、Svc/DpCatalog 各目录及其单元测试如 Svc/DpCatalog/test/ut。【免费下载链接】fprimeF´ - A flight software and embedded systems framework项目地址: https://gitcode.com/GitHub_Trending/fpr/fprime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考