Neon 中的 postgres_ffi:用 Rust 精确驾驭 PostgreSQL 磁盘格式与 WAL 的 FFI 封装库
2026/9/13 5:09:44 网站建设 项目流程

Neon 中的 postgres_ffi:用 Rust 精确驾驭 PostgreSQL 磁盘格式与 WAL 的 FFI 封装库

【免费下载链接】neonNeon: Serverless Postgres. We separated storage and compute to offer autoscaling, code-like database branching, and scale to zero.项目地址: https://gitcode.com/GitHub_Trending/ne/neon

本文以 libs/postgres_ffi/README.md 为骨架,结合 libs/postgres_ffi 下的源码、测试与基准,系统讲解 Neon 中这一关键模块:它如何借助 bindgen 从 PostgreSQL 头文件自动生成 Rust 结构体,如何按 PostgreSQL 大版本组织绑定,以及如何用 Rust 读写 pg_control、解析关系文件命名、解码与生成 WAL 记录。读完本文,你将理解 Neon 这类"存算分离"数据库在纯 Rust 代码中处理 PostgreSQL 专有格式的完整工程思路,并掌握在 Neon 仓库中定位、使用与扩展 postgres_ffi 的方法。

一、模块定位:封装一切"PostgreSQL 专有格式知识"

Neon 将 PostgreSQL 的存储与计算分离:计算节点(compute node)仍运行完整的 PostgreSQL,而 pageserver 以纯 Rust 实现持久化存储层,safekeeper 负责 WAL 的接收与复制。这意味着 Rust 侧代码必须能够读懂 PostgreSQL 的数据文件、控制文件与 WAL 记录,这正是postgres_fficrate 的职责。

按 libs/postgres_ffi/README.md 的说明,该模块是"一组用于处理 PostgreSQL 文件格式的工具":它包含一批通过 bindgen 从 PostgreSQL 头文件自动生成的结构体,以及用 Rust 读写、操作这些结构的函数。README 还提出了一个重要的架构原则——WAL 布局和 PostgreSQL 文件格式的"内部知识"应当全部封装在本模块内,代码库其他部分不应深入了解这些格式细节

"The rest of the codebase should not have intimate knowledge of PostgreSQL file formats or WAL layout, that knowledge should be encapsulated in this module."

从 Cargo.toml 可以看到它的依赖画像:bytes(字节缓冲)、crc32c(CRC32-C 校验,用于 pg_control 与 WAL 记录)、regex(文件名解析)、serde(结构体的序列化/反序列化)、postgres_ffi_types(版本无关的共享类型)以及postgres_versioninfo(大版本枚举PgMajorVersion),构建期依赖bindgen

1.1 非跨平台、随版本变化的磁盘格式

README 强调了一个关键约束:PostgreSQL 的磁盘格式既不能跨 CPU 架构与操作系统移植,也会在每个大版本中变化。因此:

  • 结构体布局必须严格对应某个特定版本的 PostgreSQL C 结构;
  • 绑定与依赖它们的代码是版本相关的,不能混用;
  • 模块当前按postgres_ffi::v14postgres_ffi::v15postgres_ffi::v16组织(README 撰写时),而当前仓库代码已进一步扩展出v17子模块(见下文第四节)。

二、bindgen:从 PostgreSQL 头文件自动生成 Rust 结构体

自动生成的入口是 bindgen_deps.h,它作为 bindgen 的输入,集中 include 了所需的 PostgreSQL 头文件:

#include "c.h" #include "catalog/pg_control.h" #include "access/xlog_internal.h" #include "storage/block.h" #include "storage/bufpage.h" #include "storage/off.h" #include "access/multixact.h"

该文件头注释明确说明了工作流:"如果需要向 Rust 代码暴露新的结构体,就在这里添加头文件,并在 build.rs 中白名单该结构体"。这意味着新增绑定是声明式的:改头文件、改白名单、重新构建即可,不需要手写任何 FFI 胶水。

生成的绑定通过 lib.rs 中的postgres_ffi!宏装配到各版本子模块中:

pub mod bindings { include!(concat!(env!("OUT_DIR"), "/bindings_", stringify!($version), ".rs")); include!(concat!("pg_constants_", stringify!($version), ".rs")); }

即构建产物目录OUT_DIR下会为每个版本生成bindings_v14.rsbindings_v15.rs等文件,再与手工维护的版本相关常量文件pg_constants_v14.rs等合并,共同构成该版本的bindings模块。每个版本子模块还从绑定中重新导出最常用的符号,例如CheckPointControlFileDataDBState_DB_SHUTDOWNEDXLogRecord(见 lib.rs)。

2.1 构建产物的典型内容

从 lib.rs 末尾的重新导出可以看出,v14 的绑定中包含了大量"不太可能跨版本变化"的基础类型,被提升到 crate 顶层供全局使用:

类型说明
BlockNumber块号(u32)
CheckPoint/ControlFileData检查点与控制文件结构
MultiXactId/MultiXactOffsetMultiXact 事务 ID 与偏移
OffsetNumber页内元组偏移
Oid/TimeLineID/TransactionId对象 ID、时间线 ID、事务 ID
PageHeaderData页头结构
RepOriginId复制源 ID
XLogRecord/XLogRecPtr/XLogSegNoWAL 记录与指针
uint32/uint64PG 风格的无符号类型别名

同时导出了若干版本无关的常量(lib.rs),其值与 PostgreSQL 默认编译参数对应:

pub const BLCKSZ: u16 = 8192; // 数据块大小 pub const RELSEG_SIZE: u32 = 1024 * 1024 * 1024 / BLCKSZ; // 每个段文件 1GB pub const XLOG_BLCKSZ: usize = 8192; // WAL 页大小 pub const WAL_SEGMENT_SIZE: usize = 16 * 1024 * 1024; // WAL 段大小 16MB

注释指出这些值对应pg_config.h,可用--with-blocksize--with-segsize修改,但 Neon 假定使用默认值。也就是说,绑定与常量都以"标准 PostgreSQL 编译配置"为前提

三、按版本组织的模块结构与运行时版本分派

lib.rs用宏一次性为所有支持的版本生成子模块,并提供一个"枚举所有版本"的宏:

#[macro_export] macro_rules! for_all_postgres_versions { ($macro:tt) => { $macro!(v14); $macro!(v15); $macro!(v16); $macro!(v17); }; } for_all_postgres_versions! { postgres_ffi }

每个postgres_ffi::vN子模块包含:

  • bindings:bindgen 生成的绑定 + 版本相关常量;
  • controlfile_utils:pg_control 读写;
  • nonrelfile_utils:CLOG、MultiXact 等非关系文件的工具;
  • wal_craft_test_export/wal_generator:测试用 WAL 生成;
  • waldecoder_handler:该版本的流式 WAL 解码器实现;
  • xlog_utils:WAL 文件与 LSN 工具。

3.1 运行时按版本分发:dispatch_pgversion!

由于一条 WAL 流或一份 basebackup 可能来自 v14~v17 中的任意一个版本,postgres_ffi 提供了编译期分发宏 dispatch_pgversion!,把"某个PgMajorVersion"映射到"对应的pgv命名空间":

dispatch_pgversion!(my_pgversion, { pgv::constants::XLOG_DBASE_CREATE })

其语义是:若my_pgversion是支持的版本,则在作用域内以pgv别名use对应版本的绑定后执行代码块;若不支持则panic!(也可传入第三个参数提供默认处理,例如返回错误而不是 panic)。同类宏还有 enum_pgversion!,用于生成"跨版本枚举"类型——把各版本的同名结构(如CheckPoint)统一包装成一个枚举,并提供pg_version()方法和从具体版本类型到枚举的Into转换。

这两个宏是 postgres_ffi 处理"多版本共存"的基石:pageserver、safekeeper 在运行时面对的 PostgreSQL 版本不确定,而 Rust 的类型必须在编译期确定,分发宏把"运行时版本"转化为"编译期分支"。

四、pg_constants:手工维护的版本无关常量

README 专门提到 pg_constants.rs 中有一批常量是"从 PostgreSQL 头文件手工复制,而非自动生成",并指出它们"大多也应该自动生成,但这是 TODO"。文件注释补充了保留手工方式的理由:"集中在一处、便于加注释"。

这些常量覆盖了 Neon 需要直接理解 WAL 语义的方方面面,大致可分几类:

  • WAL 记录类型(rmgr 操作码)XLOG_HEAP_INSERT/DELETE/UPDATE/HOT_UPDATE/LOCKXLOG_HEAP2_VISIBLE/MULTI_INSERTXLOG_CHECKPOINT_SHUTDOWN/ONLINEXLOG_PARAMETER_CHANGEXLOG_FPIXLOG_SWITCH等;
  • 资源管理器 ID(rmgrlist)RM_XLOG_ID=0RM_XACT_ID=1RM_SMGR_ID=2RM_CLOG_ID=3……RM_LOGICALMSG_ID=21,以及 Neon 自定义的RM_NEON_ID=134和一批XLOG_NEON_*操作码(对应 pgxn/neon/neon_rmgr 中的 Neon WAL 扩展);
  • XLogRecord 头布局XLR_BLOCK_ID_DATA_SHORT=255XLR_BLOCK_ID_DATA_LONG=254XLR_BLOCK_ID_ORIGIN=253XLR_BLOCK_ID_TOPLEVEL_XID=252BKPBLOCK_*标志位、BKPIMAGE_HAS_HOLE等;
  • 事务与 CLOGFIRST_NORMAL_TRANSACTION_ID=3CLOG_XACTS_PER_BYTE=4TRANSACTION_STATUS_*等;
  • MultiXact、可见性映射(visibilitymap)、SLRU 布局等。

此外还有两个对 Neon 运维很实用的导出:

pub const PGDATA_SPECIAL_FILES: [&str; 3] = ["pg_hba.conf", "pg_ident.conf", "postgresql.auto.conf"]; pub static PG_HBA: &str = include_str!("../samples/pg_hba.conf");

注释解释了原因:basebackup 恢复时不能覆盖postgresql.conf(因为 safekeeper 同步需要先有配置),而这三个文件可以安全地在备份恢复后修改;samples/pg_hba.conf则是随 crate 附带的默认认证配置样例。

4.1 版本相关常量文件

pg_constants.rs相对,每个版本还有自己的pg_constants_v14.rspg_constants_v17.rs,存放随版本变化的常量(例如各版本不同的 WAL 记录布局参数),它们与 bindgen 输出一起被include!进对应版本的bindings模块。

五、文件系统层工具:关系文件与非关系文件

5.1 关系文件命名解析 relfile_utils.rs

PostgreSQL 数据目录中关系文件的命名遵循relpath()_mdfd_segpath()的规则。postgres_ffi 用正则实现了解析函数parse_relfilename,把文件名解析为(relfilenode, forknum, segno)三元组:

<oid> → (oid, 0, 0) <oid>_<fork name> → (oid, forknum, 0) <oid>.<segment number> → (oid, 0, segno) <oid>_<fork name>.<segment number>

正则:^(?P<relnode>\d+)(_(?P<forkname>[a-z]+))?(\.(?P<segno>\d+))?$forkname通过forkname_to_number(来自postgres_ffi_types::forknum)映射为 fork 编号,如main→0、fsm→1、vm→2、init→3。文件中还带有一套完整的单元测试(relfile_utils.rs),验证合法输入(含3147483648这样超出 i32 的 relfilenode)、非法输入(foo1.2.31234_invalid、负数、超长数字)以及边界情况(0被接受,超大的段号也被接受),可作为理解解析规则的第一手资料。

5.2 CLOG 与 MultiXact 工具 nonrelfile_utils.rs

非关系文件方面,该文件实现了与 PostgreSQL C 宏/函数等价的 Rust 版本:

  • transaction_id_set_status/transaction_id_get_status:在 CLOG 页中读写某个 XID 的事务状态(提交/中止/子提交),按CLOG_XACTS_PER_PAGECLOG_XACTS_PER_BYTECLOG_BITS_PER_XACT计算字节偏移与位偏移;
  • clogpage_precedes:判断 CLOG 页序(等价于 clog.c 中的CLOGPagePrecedes,处理 XID 回绕);
  • slru_may_delete_clogsegment:判断某个 SLRU 段是否可删除(对应 slru.c 的SlruMayDeleteSegment());
  • mx_offset_to_*系列:把 MultiXact 成员偏移换算为 flags/member 在页内与段内的位置。

测试 test_multixid_calc 特意声明:这些测试值由一个小 C 程序调用 PostgreSQL 宏生成,用来证明 Rust 实现与 PostgreSQL 的MXOffsetTo*逐位一致——这是"磁盘格式兼容"最有力的验证方式。

六、pg_control 控制文件的读写 controlfile_utils.rs

global/pg_control是 PostgreSQL 启动时最先读取的文件之一:它记录上次是否干净关闭、最新检查点的位置与副本,并包含版本号、块大小、对齐与字节序等"数据目录与二进制是否兼容"的信息。由于磁盘格式不跨平台,这些字段必须被严格解析。文件注释还透露了一个布局细节:有效数据被设计为小于 512 字节以支持原子更新,实际文件为 8192 字节,其余部分填充零

ControlFileData提供了两个方法:

  • decode(buf):先校验长度不小于结构体大小,再用crc32ccrc字段之前的内容计算校验和并与文件中的 CRC 比对,最后用utils::bin_ser::LeSer反序列化;
  • encode():序列化后重新计算 CRC 写回,并填充到完整的PG_CONTROL_FILE_SIZE(8192 字节)。

CRC 位置通过std::mem::offset_of!(ControlFileData, crc)计算,等价于 C 的offsetof,保证无论 bindgen 生成何种布局都能正确定位。可用 PostgreSQL 自带的pg_controldata工具对照查看内容。

6.1 为"按 LSN 启动"生成 pg_control xlog_utils.rs

控制文件工具在 Neon 中最重要的应用场景是 generate_pg_control:pageserver 持久化了 pg_control 与 checkpoint 记录,当 compute 节点需要从某个 LSN 启动 basebackup 时,该函数据此合成一份全新的 pg_control。其关键语义包括:

  • Neon 内部 checkpoint 结构中的redo字段约定与 PostgreSQL 不同(关闭检查点指向 WAL 记录末尾而非开头,在线检查点则置 0);
  • was_shutdown = Lsn(checkpoint.redo) == lsn用于判断"该 LSN 处是否存在关闭检查点",即是否从干净关闭状态启动;
  • 输出结构中redo恒被设为启动 LSN,以提示无需 WAL 重放(还需要 PostgreSQL 侧 Neon 定制代码配合);
  • 即使并非干净关闭,state也写为DBState_DB_SHUTDOWNED,因为 Neon 启动流程本就忽略控制文件中的状态(类似独立 PostgreSQL 的归档恢复),checkPoint指针置 0。

返回值是(pg_control 内容, system_identifier, was_shutdown)三元组。这正是"存算分离"下 basebackup 无需重放 WAL 的关键一环,相关持久化细节见 pageserver 的 walingest.rs。

七、WAL 处理:从文件命名到流式解码

7.1 WAL 段文件工具

xlog_utils.rs 实现了与 PostgreSQL 同名函数对应的 Rust 版本(刻意保留 C 风格命名):

  • XLogFileName(tli, logSegNo, wal_seg_size):把时间线 ID 与段号格式化为000000010000000000000001形式的 24 位十六进制文件名(时间线 8 位 + log 8 位 + seg 8 位);
  • XLogFromFileName/IsXLogFileName/IsPartialXLogFileName:反向解析文件名、判断是否为合法 WAL 段名或.partial段名;
  • XLogSegmentsPerXLogId/XLogSegNoOffsetToRecPtr:段号与 LSN 的换算;
  • normalize_lsn:若 LSN 落在页首则后移一个页头(段首是XLOG_SIZE_OF_XLOG_LONG_PHD,其余是XLOG_SIZE_OF_XLOG_SHORT_PHD),否则 8 字节对齐——这是 WAL 记录起始位置的硬性要求。

Neon 的 PostgreSQL 时间线(PG timeline)固定为 1,与 Neon 自身的 timeline 概念无关(见 lib.rs):

pub const PG_TLI: u32 = 1;

7.2 事务 ID 与页操作

lib.rs 还导出几个纯 Rust 移植的小工具:

  • transaction_id_is_normal/transaction_id_precedes:对应 transam.h/transam.c,处理 32 位 XID 回绕的模 2^32 比较;
  • page_is_new/page_get_lsn/page_set_lsn:通过检查pg_upper==0判断页未初始化(对应 PostgreSQL 的PageIsInit()宏),以及读写页头前 8 字节的 LSN;
  • fsm_logical_to_physical:对应 freespace.c,把 FSM 逻辑地址转为物理块号。

7.3 流式 WAL 解码器 WalStreamDecoder

lib.rs 定义了WalStreamDecoder,一个面向"从 safekeeper 拉取/接收 WAL 字节流"场景的有状态解码器:

  • State枚举表示三态:WaitingForRecord(等待下一条记录)、ReassemblingRecord(一条记录跨多个 chunk,正在拼接)、SkippingEverything(跳过直到某个 LSN);
  • feed_bytes(buf)持续喂入字节,poll_decode()返回Result<Option<(Lsn, Bytes)>>——每次产出"记录结束 LSN + 记录原始字节";
  • 底层通过dispatch_pgversion!分派到对应版本的 waldecoder_handler.rs 实现;
  • WalDecodeError携带出错位置 LSN,便于日志与断点续传。

解码器在仓库中的实际用法可参考 find_end_of_wal:从某个记录边界 LSN 开始,按段遍历(优先尝试.partial,再尝试完整段),逐段喂给解码器并推进"已知最大完整记录末尾"的 LSN,直到段缺失或 EOF——这是判断一个数据目录中 WAL 写到哪里的通用工具。

7.4 生成空的 WAL 段 generate_wal_segment

计算节点启动需要一个从给定 LSN 开始、头两页带正确页头的空 WAL 段(pg_waldump等工具才能识别)。该函数:

  • 若 LSN 在段首页:写长页头(XLogLongPageHeaderData,含 system id、段大小、块大小、magic),并在需要时伪造一条XLP_FIRST_IS_CONTRECORD的"假记录"使xlp_rem_len指向第一个真实记录起始处;
  • 若 LSN 在段中某页:在对应页偏移处写短页头,同样处理 contrecord 语义;
  • 其余部分全部补零,最后返回一个完整 16MB 的段。

7.5 测试与基准用 WAL 生成器 wal_generator.rs

与解码相对,模块还提供生成WAL 的能力,供测试与基准使用:

  • Record:一条记录的载荷(rmid、info、data),encode(prev_lsn)负责加 XLogRecord 头、按数据长度选择XLR_BLOCK_ID_DATA_SHORT/LONG头、并用crc32c_append计算 CRC;
  • RecordGeneratortrait 与WalGenerator:迭代器式地把记录序列写成"页头 + 记录 + 8 字节对齐填充"的完整、良构 WAL,可跨页跨段;
  • 文件注释给出了直观的布局示意(Segment/Page/Record 层级),并提醒 WAL 格式与版本相关,需按目标版本导入(如postgres_ffi::v17::wal_generator::WalGenerator)。

配套的 wal_craft crate 承接更复杂的"手工构造 WAL 写入测试"需求(README 注释里也提到"如果要构造 WAL 并为本模块写测试,请放到 wal_craft crate"),并提供了共享的测试导出模块wal_craft_test_export

八、WAL 记录的解码与人类可读描述 walrecord.rs

walrecord.rs承载了 WAL 记录层的核心解码逻辑,其入口 decode_wal_record 严格遵循 xlogrecord.h 描述的记录布局逐段解析:

XLogRecord 固定头 XLogRecordBlockHeader(可多个) ├─ 若 BKPBLOCK_HAS_IMAGE:XLogRecordBlockImageHeader │ └─ 若 HAS_HOLE 且压缩:XLogRecordBlockCompressHeader ├─ 若未设 BKPBLOCK_SAME_REL:RelFileNode └─ BlockNumber XLogRecordDataHeader[Short|Long] 各 block 数据区 主数据区(main data)

该函数把结果填入调用者复用的DecodedWALRecord结构(注释明确说明这是 WAL 消化热路径,复用结构体以避免分配),产出:记录的xl_xidxl_infoxl_rmid、原始字节、涉及的块列表DecodedBkpBlock(含 relfilenode 三元组、fork、blkno、全页镜像的 hole 偏移/长度/压缩标志等)以及main_data_offset

8.1 各版本 rmgr 数据解码

同文件按版本实现了若干 rmgr 记录的载荷解码,例如:

  • v14::XlHeapInsert/XlHeapDelete/XlHeapUpdate/XlHeapLock/XlHeapMultiInsert/XlParameterChange
  • v15复用 v14 的定义,v16重新定义了字段有变化的XlHeapDelete/XlHeapUpdate/XlHeapLock,并新增rm_neon子模块(PG16 起引入 Neon 自定义 RMGRRM_NEON_ID=134来承载 Neon 风格 WAL,见 pg_constants.rs);
  • v17新增XlEndOfRecovery解码,其余复用 v14/v16。

版本差异是真实的:例如XlClogTruncatepageno在 PG17 之前是 u32,PG17 起是 u64(walrecord.rs);DecodedWALRecord::is_dbase_create_copy也按版本区分 PG14 的XLOG_DBASE_CREATE与 PG15+ 的XLOG_DBASE_CREATE_FILE_COPY

8.2 事务记录解析 XlXactParsedRecord

XlXactParsedRecord::decode对应 PostgreSQL xactdesc.c 中的ParseCommitRecord/ParseAbortRecord,解析 commit/abort 记录中的时间戳、xinfo标志位(XACT_XINFO_HAS_*系列)、db/ts 信息、子事务列表、涉及的 relfilenode 列表等——Neon 需要据此知道提交影响了哪些关系文件。XlRunningXacts则解析XLOG_RUNNING_XACTS记录中的活动事务快照。

8.3 描述函数 describe_postgres_wal_record

describe_postgres_wal_record 把常见 WAL 记录翻译成人类可读的字符串(如"HEAP INSERT""HEAP2 MULTI_INSERT""XLOG FPI"),用于dump_layer_file之类的调试工具。注释坦承这是手写的近似实现,理想方案是复用 PostgreSQL 的 rmgrdesc 基础设施。

九、基准测试与验证手段

benches/README.md 给出了围绕 WAL 解码器的性能测试方式(criterion + pprof):

# 全部基准 cargo bench --package postgres_ffi # 指定基准文件 cargo bench --package postgres_ffi --bench waldecoder # 指定用例,例如 1024 字节完整记录 cargo bench --package postgres_ffi --bench waldecoder complete_record/size=1024 # 列出可用用例 cargo bench --package postgres_ffi --benches -- --list # 生成火焰图(profiling 10 秒,输出 target/criterion/*/profile/flamegraph.svg) cargo bench --package postgres_ffi --bench waldecoder complete_record/size=1024 -- --profile-time 10

图表与统计见target/criterion/report/index.html,基准会自动与上一次运行对比,也支持--baseline/--save-baseline。由于decode_wal_record位于 WAL 消化热路径,这类基准对 pageserver 的写入延迟意义重大。解码器基准实现位于 benches/waldecoder.rs,依赖 wal_craft 生成测试 WAL 数据。

十、在 Neon 仓库中的实际应用

postgres_ffi 是 Neon 中"纯 Rust 读写 PostgreSQL 格式"的唯一知识源,其主要消费方包括:

  • pageserver:消化 WAL(decode_wal_recordWalStreamDecoder)、维护 pg_control 与 checkpoint(generate_pg_control)、解析关系文件(parse_relfilename)、识别 Neon 自定义 RMGR 记录(RM_NEON_ID);
  • safekeeper:接收、存储与转发 WAL 段(XLogFileNameIsPartialXLogFileNameWalStreamDecoder),并生成空 WAL 段供 compute 启动(generate_wal_segment);
  • compute 节点 basebackupfind_end_of_wal定位 WAL 末尾、generate_wal_segment制造可识别的起始段;
  • 测试与基准wal_craftwal_generatorWalStreamDecoder基准。

总结

postgres_ffi用清晰的工程分层解决了"存算分离数据库在 Rust 中兼容 PostgreSQL 磁盘格式"的难题:bindgen 自动生成结构体保证与 C 布局一致,按大版本组织子模块并用dispatch_pgversion!宏做运行时分发来容纳版本差异,pg_constants集中维护语义常量,controlfile_utils/relfile_utils/nonrelfile_utils/xlog_utils/walrecord各司其职地覆盖控制文件、关系文件、CLOG/MultiXact、WAL 段与 WAL 记录的全链路读写。README 中提出的"将格式知识封装在本模块"的原则,至今仍是 Neon 代码库中处理 PostgreSQL 专有格式的一致约定。

【免费下载链接】neonNeon: Serverless Postgres. We separated storage and compute to offer autoscaling, code-like database branching, and scale to zero.项目地址: https://gitcode.com/GitHub_Trending/ne/neon

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询