Erlang/OTP 端口(Ports)互操作实战:用 Port 从 Erlang 调用 C 程序完整教程
编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载Port端口是 Erlang/OTP 内置的三大互操作机制之一它以字节流为接口让 Erlang 进程能够与运行在独立 OS 进程中的外部程序通常是 C 程序通信。本文以官方互操作教程 Ports 章节 为主体围绕“在 Erlang 中调用 C 函数”这一经典场景完整讲解端口的创建、消息协议、C 端通信代码的写法与端到端运行流程。读完本文你将掌握open_port/2的用法、{packet,2}长度前缀协议、C 程序在 stdin/stdout 上的读写约定以及端口失败检测与优雅关闭的完整套路。端口机制概览字节导向的外部世界接口从 Erlang 的视角看端口提供了与外部世界通信的基本机制。端口对外部程序呈现**字节导向byte-oriented**的接口创建端口后Erlang 与外部程序之间交换的是字节列表或二进制而不是 Erlang 项terms。因此程序员往往需要自行设计一套编码/解码encoding/decoding方案把 Erlang 项转成字节序列再在另一端还原。这一点在官方 Interoperability Tutorial 概述 中有着明确说明。端口的底层实现依赖平台在 UNIX 上使用管道pipes外部程序默认从标准输入文件描述符 0读取、向标准输出文件描述符 1写入。只要外部程序能够处理这种进程间通信机制它可以用任何语言编写——本文以 C 为例。在 Erlang/OTP 中与端口并列的内建互操作机制还有分布式 Erlang用于 Erlang 节点间通信和NIF把 C 代码直接链接进运行时系统。端口适合“Erlang 程序与外部程序运行在同一台机器”的各种互操作场景编程相对直白而 NIF 由于直接链接进模拟器进程调用开销最小但崩溃风险最高——一个出错的 NIF 会让整个运行时系统泄漏内存、挂起或崩溃因此官方建议在开销可接受时优先使用外部端口。这一取舍在 overview.md 中有详细论述。原文档用一张流程图概括了端口通信的参与方图中的关键点有两个一是端口位于 ERTS 内部作为 Erlang 进程与外部 OS 进程之间的桥梁二是 Erlang 侧必须有一个**connected process连接进程**负责与端口交互——所有进出端口的数据都必须经过它。问题场景为什么需要端口本教程要解决的经典互操作问题是把一段求解复杂问题的 C 代码集成进 Erlang 程序。假设我们有如下两个 C 函数希望从 Erlang 中调用见 example.md 与配套的 complex.c/* complex.c */ int foo(int x) { return x1; } int bar(int y) { return y*2; }这两个函数被刻意保持得尽可能简单以便读者把注意力集中在互操作机制本身。从 Erlang 的角度理想情况是不必关心foo、bar是 C 函数直接调用即可% Erlang code ... Res complex:foo(X), ...这里的诀窍在于C 通信的细节被封装在complex.erl的实现内部。后续章节将展示如何用端口以及教程中的 NIF 等其他机制实现这个模块。本教程将用端口方案实现一个complex1模块来完成任务。Erlang 端创建端口、封装调用创建端口open_port/2与连接进程Erlang 与 C 之间的一切通信都必须从创建端口开始。创建端口使用 BIFopen_port/2第一个参数传{spawn, ExtPrg}——字符串ExtPrg是外部程序的名字可以包含命令行参数第二个参数是选项列表本例中只有一个{packet, 2}。{packet, 2}的含义是在 Erlang 与 C 之间传输的数据前附加一个2 字节的长度指示符以简化双端通信。Erlang 侧会自动加上这个长度指示符而 C 侧必须显式地构造和解析它。创建端口的 Erlang 进程被称为该端口的connected process连接进程。所有到端口的通信和来自端口的数据都必须经过该进程如果连接进程终止端口也随之终止只要外部程序写得正确它也会随之终止。同时进程被设置为trap_exit以便检测外部程序的失败-module(complex1). -export([start/1, init/1]). start(ExtPrg) - spawn(?MODULE, init, [ExtPrg]). init(ExtPrg) - register(complex, self()), process_flag(trap_exit, true), Port open_port({spawn, ExtPrg}, [{packet, 2}]), loop(Port).这里start/1会派生一个独立的init/1进程它把自己的 PID 注册成全局名complex这样其他进程可以通过名字发送消息设置trap_exit然后创建端口并进入主循环。注意连接进程是init/1所在进程而不是调用start/1的进程。从 Ports and Port Drivers 参考手册 可以进一步确认端口 BIF 的细节PortName通常是{spawn, Command}元组Command为外部程序名字符串外部程序运行在 Erlang 工作区之外除非找到了同名端口驱动linked-in driver此时启动的是驱动。PortSettings是端口设置选项列表通常至少包含{packet, N}指定传输数据前附加N 字节长度指示符N 的合法取值为 1、2 或 4。若希望用二进制binary而非字节列表通信则需加上binary选项。端口标识port identifier的使用方式与 PID 类似可以像 PID 一样收发消息也可以用link/1链接或用register/2注册名字。对外 APIfoo/1与bar/1现在实现complex1:foo/1和complex1:bar/1。两者都把请求发给complex进程然后等待回复foo(X) - call_port({foo, X}). bar(Y) - call_port({bar, Y}). call_port(Msg) - complex ! {call, self(), Msg}, receive {complex, Result} - Result end.调用者进程把{call, self(), Msg}消息发给注册名complex然后阻塞等待{complex, Result}回复并返回结果。这样从调用者角度看foo、bar与普通 Erlang 函数无异。主循环编码、发送、等待、解码、回传complex进程的主循环承担五步工作把消息编码成字节序列发送到端口等待回复解码回复把结果回传给调用者。loop(Port) - receive {call, Caller, Msg} - Port ! {self(), {command, encode(Msg)}}, receive {Port, {data, Data}} - Caller ! {complex, decode(Data)} end, loop(Port) end.这里使用的两条端口消息协议是更完整的协议列表见 ports.md{Pid, {command, Data}}— 把Data发送给端口Data必须是 I/O 列表即二进制或 0255 整数组成的可嵌套列表{Port, {data, Data}}— 从外部程序收到的数据发给端口所有者connected process。此外参考手册还列举了{Pid, close}关闭端口端口在缓冲区刷完后回复{Port, closed}和{Pid, {connect, NewPid}}转移端口所有权回复{Port, connected}等消息并提醒发往端口的消息是异步投递的这一行为自 Erlang/OTP 16 起生效更早版本是同步投递。简单的编码/解码方案假设 C 函数的参数和返回值都小于 256教程采用了一套极简编码方案foo用字节 1 表示bar用字节 2 表示参数/结果各用一个字节表示encode({foo, X}) - [1, X]; encode({bar, Y}) - [2, Y]. decode([Int]) - Int.这套方案的代价是显而易见的——它限定了参数与结果必须在 0255 之间实际项目中若数据范围更大要么改用多字节编码要么配合binary选项 term_to_binary/1走 Erlang 外部项格式见 overview.md 中关于 Erl_Interface 与term_to_binary/1、binary_to_term/1的讨论。完整 Erlang 程序停止与失败检测完整的complex1模块仓库中的 complex1.erl还包含停止端口和检测端口失败的逻辑-module(complex1). -export([start/1, stop/0, init/1]). -export([foo/1, bar/1]). start(ExtPrg) - spawn(?MODULE, init, [ExtPrg]). stop() - complex ! stop. foo(X) - call_port({foo, X}). bar(Y) - call_port({bar, Y}). call_port(Msg) - complex ! {call, self(), Msg}, receive {complex, Result} - Result end. init(ExtPrg) - register(complex, self()), process_flag(trap_exit, true), Port open_port({spawn, ExtPrg}, [{packet, 2}]), loop(Port). loop(Port) - receive {call, Caller, Msg} - Port ! {self(), {command, encode(Msg)}}, receive {Port, {data, Data}} - Caller ! {complex, decode(Data)} end, loop(Port); stop - Port ! {self(), close}, receive {Port, closed} - exit(normal) end; {EXIT, Port, Reason} - exit(port_terminated) end. encode({foo, X}) - [1, X]; encode({bar, Y}) - [2, Y]. decode([Int]) - Int.主循环现在处理三种消息{call, Caller, Msg}— 正常调用流程如上所述stop— 向端口发送{self(), close}等待{Port, closed}确认后以exit(normal)正常退出{EXIT, Port, Reason}— 端口异常终止如外部程序崩溃此时进程以exit(port_terminated)退出。后两种分支正是“连接进程 trap 端口退出信号”的实际应用因为init/1进程设置了process_flag(trap_exit, true)端口终止时发送的{EXIT, Port, Reason}会作为普通消息进入信箱而不是直接把进程杀死从而让进程有机会做清理并给出明确的失败语义。C 端在 stdin/stdout 上实现长度前缀协议字节级读写read_exact与write_exact在 C 侧需要编写从 Erlang 接收数据、向 Erlang 发送数据的函数且要处理2 字节长度指示符。默认情况下C 程序从标准输入文件描述符 0读取、向标准输出文件描述符 1写入对应仓库中的 erl_comm.c/* erl_comm.c */ #include stdio.h #include unistd.h typedef unsigned char byte; int read_exact(byte *buf, int len) { int i, got0; do { if ((i read(0, bufgot, len-got)) 0){ return(i); } got i; } while (gotlen); return(len); } int write_exact(byte *buf, int len) { int i, wrote 0; do { if ((i write(1, bufwrote, len-wrote)) 0) return (i); wrote i; } while (wrotelen); return (len); } int read_cmd(byte *buf) { int len; if (read_exact(buf, 2) ! 2) return(-1); len (buf[0] 8) | buf[1]; return read_exact(buf, len); } int write_cmd(byte *buf, int len) { byte li; li (len 8) 0xff; write_exact(li, 1); li len 0xff; write_exact(li, 1); return write_exact(buf, len); }几个值得注意的实现细节read_exact/write_exact用循环确保读满/写完指定字节数。管道上的read/write可能因为信号中断或缓冲区限制而“短读/短写”循环重试正是生产级代码的基本功read_cmd先读 2 字节长度前缀大端序(buf[0] 8) | buf[1]再按该长度读取完整数据write_cmd则先写出 2 字节长度前缀再写正文教程特别提醒stdin/stdout是带缓冲的输入/输出流绝不能用于与 Erlang 的通信。必须直接使用文件描述符 0/1 上的read/write系统调用这也是头文件引入unistd.h的原因否则缓冲机制会破坏协议同步。主程序分发函数调用并回传结果main函数监听来自 Erlang 的消息按照约定的编码方案用第一个字节决定调用哪个函数、第二个字节作为参数然后把函数结果回传给 Erlang对应仓库中的 port.c/* port.c */ typedef unsigned char byte; int main() { int fn, arg, res; byte buf[100]; while (read_cmd(buf) 0) { fn buf[0]; arg buf[1]; if (fn 1) { res foo(arg); } else if (fn 2) { res bar(arg); } buf[0] res; write_cmd(buf, 1); } }注意C 程序运行在一个while循环中并检查read_cmd/1的返回值——这是为了检测端口何时关闭并自行终止。当 Erlang 侧关闭端口或连接进程退出导致端口终止时管道写端关闭read_cmd返回 ≤ 0循环退出、进程自然结束。这正是 Erlang 文档中所说的“外部程序只要写得正确会随端口一起终止”的落地实现。端到端运行三步跑通示例仓库的 tutorial 目录 下提供了全部示例源码complex.c、erl_comm.c、port.c与complex1.erl可以直接照此操作。Step 1. 编译 C 代码$ gcc -o extprg complex.c erl_comm.c port.c这里把三个 C 文件链接成一个可执行文件extprg。编译命令在 UNIX 环境下运行需要系统具备 gcc 工具链。Step 2. 启动 Erlang 并编译 Erlang 代码$ erl Erlang/OTP 26 [erts-14.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns] Eshell V14.2 (press CtrlG to abort, type help(). for help) 1 c(complex1). {ok,complex1}上面的版本 banner 是教程撰写时 Erlang/OTP 26 环境的真实输出实际以你本地安装的 Erlang/OTP 版本为准。Step 3. 运行示例2 complex1:start(./extprg). 0.34.0 3 complex1:foo(3). 4 4 complex1:bar(5). 10 5 complex1:stop(). stop执行结果验证了整条链路foo(3)返回 4C 侧x1bar(5)返回 10C 侧y*2。调用stop()后complex1进程向端口发送close消息等待{Port, closed}确认后正常退出与此同时extprg进程因为读不到新数据而退出while循环并终止——Erlang 进程、端口、外部 OS 进程三者的生命周期就此完整收束。端口协议速查与生命周期要点结合 ports.md 与本文示例可以把端口通信的协议与生命周期归纳如下发送给端口的消息Data须为 I/O 列表消息含义{Pid, {command, Data}}把Data发送给端口{Pid, close}关闭端口端口在缓冲区刷完后回复{Port, closed}{Pid, {connect, NewPid}}把端口所有权转移给NewPid回复{Port, connected}从端口接收的消息消息含义{Port, {data, Data}}从外部程序收到数据{Port, closed}对close的确认{Port, connected}对connect的确认{EXIT, Port, Reason}端口已终止若未 trap 退出信号该消息会直接终止连接进程生命周期关键规则端口由open_port({spawn, ExtPrg}, [{packet, N}])创建N取 1、2 或 4长度前缀自动附加在 Erlang 侧须由 C 侧显式处理创建端口的进程是 connected process端口所有者所有通信必须经过它它终止端口随之终止端口消息异步投递Erlang/OTP 16 之前为同步外部程序通过检测read返回 ≤ 0 来感知端口关闭并自行退出若需在 C 侧使用 Erlang 项而非自定义字节编码可结合binary选项与term_to_binary/1/binary_to_term/1或改用 Erl_Interface 库见 overview.md。延伸阅读Interoperability Tutorial 目录互操作机制的总体介绍与前置知识问题示例本文所解问题的原始定义NIFs 章节同一问题用 NIF 的解法便于对比端口与 NIF 的取舍NIF 更快但崩溃风险更高官方建议优先考虑外部端口C Nodes 章节 与 Erl_Interface 章节把 C 程序包装成分布式节点的另一条路径Ports and Port Drivers 参考手册端口 BIF、消息协议与端口驱动的权威参考配套源码complex1.erl、complex.c、erl_comm.c、port.c均可直接编译运行验证本文全部内容。赞分享编程语言语言运行时标准库编译器并发编程【免费下载链接】otpErlang/OTP项目地址https://gitcode.com/gh_mirrors/ot/otp点击查看免费下载相关推荐Erlang/OTP 互操作实战用 linked-in Port Driver 在 Erlang 中调用 C 代码c_portdriver 教程详解Erlang/OTP 互操作实战用 linked in Port Driver 在 Erlang 中调用 C 代码c_portdriver 教程详解 本指编程语言语言运行时标准库编译器并发编程使用 vmctl 将 Grafana Mimir 历史指标迁移到 VictoriaMetricsremote-read 与对象存储双模式实战指南使用 vmctl 将 Grafana Mimir 历史指标迁移到 VictoriaMetricsremote read 与对象存储双模式实战指南 Grafan编程语言语言运行时标准库编译器并发编程3 步批量解除 PDF 复制打印限制PDF 补丁丁实操指南3 步批量解除 PDF 复制打印限制PDF 补丁丁实操指南 想把 PDF 里的一段话复制出来阅读器却弹出权限不足想打印按钮是灰的。这类锁由文档自身的编程语言语言运行时标准库编译器并发编程上一篇终极fscan网络安全扫描工具从入门到精通完整指南下一篇告别重复造轮子用Edgar for Unity打造无限可能的2D地牢世界创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

xberg C 绑定批量文档抽取实战:xberg_extract_batch 与 per-input config 逐条配置

xberg C 绑定批量文档抽取实战:xberg_extract_batch 与 per-input config 逐条配置

后端AI 应用NLP 【免费下载链接】xberg Polyglot document intelligence with a Rust core: extract text, metadata, images, tables, and structured data from 106 formats across 140 file extensions, plus code intelligence for 371 languages. Fifteen bindings, with …

2026/9/25 2:25:07 阅读更多 →
MBA论文写作必备:9类AI工具全流程实战指南

MBA论文写作必备:9类AI工具全流程实战指南

MBA论文季的朋友圈,基本就是大型崩溃现场:文献综述堆了一屏又一屏,案例分析写得像百度百科,问卷数据导出来根本不知道从哪下手,更别说打完收工时还要面对那行让人心虚的AI痕迹。我当年也是从这套流程里趟过来的&#x…

2026/9/25 2:25:07 阅读更多 →
RisingWave 中的 Keys 体系:Stream Key 与存储主键(Storage Primary Key)深度解析

RisingWave 中的 Keys 体系:Stream Key 与存储主键(Storage Primary Key)深度解析

数据库流处理后端数据工程 【免费下载链接】risingwave Event streaming platform for agentic AI. Continuously ingest, transform, and serve event streams in real time, at scale. 项目地址: https://gitcode.com/gh_mirrors/ri/risingwave 点击查看 免费下载…

2026/9/25 2:25:07 阅读更多 →

最新新闻

深入理解 Sinon 的 `spyCall.firstArg`:读取单次调用首个参数的正确姿势

深入理解 Sinon 的 `spyCall.firstArg`:读取单次调用首个参数的正确姿势

测试开发工具 【免费下载链接】sinon Test spies, stubs and mocks for JavaScript. 项目地址: https://gitcode.com/gh_mirrors/si/sinon 点击查看 免费下载 spyCall.firstArg 是 Sinon 中 spy call 对象的一个核心只读属性,用于获取某一次函数调用传入…

2026/9/25 4:57:52 阅读更多 →
腾讯云WorkBuddy Enterprise企业级AI Agent平台架构与实操指南

腾讯云WorkBuddy Enterprise企业级AI Agent平台架构与实操指南

1. 从零理解 WorkBuddy Enterprise 的定位与核心价值1.1 这个平台到底解决什么问题WorkBuddy Enterprise 是腾讯云推出的一套企业级 AI 平台与 Agent 生态产品。说白了,它要解决的核心问题是:企业想用 AI,但不知道怎么把 AI 能力安全、可控、…

2026/9/25 4:57:52 阅读更多 →
Endnote在Word中消失?COM加载项排查与修复指南

Endnote在Word中消失?COM加载项排查与修复指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:57:52 阅读更多 →
Java图书管理系统SWT实战:从环境搭建到避坑指南

Java图书管理系统SWT实战:从环境搭建到避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:57:52 阅读更多 →
GDS版图从入门到精通:层次结构、生成流程与-uniquifycellnames避坑指南

GDS版图从入门到精通:层次结构、生成流程与-uniquifycellnames避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:57:52 阅读更多 →
Navicat免安装版深度解析:依赖库、配置与MySQL连接排查指南

Navicat免安装版深度解析:依赖库、配置与MySQL连接排查指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/25 4:56:51 阅读更多 →

日新闻

AI元人文:从工具使用到思维重构的深度探索

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:00:41 阅读更多 →
Python+CNN车牌识别实战:从数据预处理到模型训练与部署

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:00:41 阅读更多 →
Vim基础操作全攻略:保存退出、模式切换与高频命令实战

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/25 0:00:41 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/24 14:34:13 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/24 14:33:56 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/24 12:50:34 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/24 14:33:48 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/24 12:49:17 阅读更多 →