【免费下载链接】maildev
:mailbox: SMTP Server + Web Interface for viewing and testing emails during development.
项目地址:
https://gitcode.com/gh_mirrors/ma/maildev
点击查看 免费下载
导读
MailDev v3 提供了完全基于 TypeScript 与现代 async/await 风格的程序化 API,允许开发者将本地 SMTP 开发服务器、Web 邮件查看界面、REST API 与 WebSocket 推送直接嵌入自己的 Node.js 应用、测试框架或工具链中。本文以官方 docs/api.md 为骨架,结合仓库源码逐层展开:从 MailDev 类的构造与启动、内存/磁盘两种存储模型、邮件对象结构与各类操作(查询、附件、删除、中继),到事件监听、独立子包组合、中间件集成与 v2→v3 迁移,最终你将能够把 MailDev 作为一个可编程组件深度集成进自己的项目中。
一、为什么需要程序化 API
MailDev 在开发环境扮演的角色是"本地邮箱":应用把邮件发到本机 SMTP 端口(默认 1025),开发者通过 Web 界面(默认 1080)查看、搜索、下载与删除这些邮件。CLI 方式(maildev 命令)适合独立使用,但遇到以下场景时,程序化 API 是更优解:
- 自动化测试:为每个并行测试 worker 启动独立实例,通过 smtp: 0 让操作系统分配空闲端口,避免端口冲突;
- 工具链集成:把 SMTP 服务器嵌入自己的 Express/Fastify 应用或 CI 脚本,统一端口与生命周期管理;
- 事件驱动:通过 'new'、'delete' 等事件实时响应邮件,例如自动中继关键邮件到真实邮箱;
- 精细控制:直接操作底层 @maildev/core、@maildev/smtp、@maildev/api 子包,绕过 MailDev 门面类。
从源码结构看,MailDev 类本身是一个门面(facade):构造时将配置合并进默认值,并委托给内部的 Orchestrator 统一创建、启动与停止存储、SMTP、API 三组服务(见 packages/cli/src/index.ts 与 packages/cli/src/server/orchestrator.ts)。
二、快速开始与安装
安装与使用同样简单:
npm install maildev
import { MailDev } from 'maildev'
const maildev = new MailDev({
smtp: 1025,
web: 1080,
})
await maildev.start()
// Access server instances
const servers = maildev.getServers()
// Listen for new emails
servers.smtp.on('new', (email) => {
console.log('Received:', email.subject)
})
// Stop when done
await maildev.stop()
从仓库源码看,start() 内部依次执行:初始化存储(MemoryStorage 或 FileStorage)→ 创建 SMTP 服务器 → 配置中继 → 启动 SMTP → 可选裁剪与恢复磁盘邮件 → 创建并启动 API 服务器(除非 disableWeb),最后把 { storage, smtp, api? } 返回给调用方(见 packages/cli/src/server/orchestrator.ts)。如果服务已经启动,再次调用 start() 会抛出 'Servers are already running' 异常。
三、MailDev 类:构造函数选项详解
MailDev 类提供最简洁的程序化使用方式,构造函数接受一个 PartialMailDevConfig,未提供的字段会与默认配置合并(见 packages/cli/src/config/types.ts)。以下是完整配置选项:
import { MailDev } from 'maildev'
const maildev = new MailDev({
// SMTP Server
smtp: 1025, // SMTP port (default: 1025)
ip: '::', // SMTP bind address (default: '::')
// Web/API Server
web: 1080, // Web UI port (default: 1080)
webIp: '0.0.0.0', // Web bind address (default: '0.0.0.0')
disableWeb: false, // Disable web interface
basePathname: '/', // Base path for web interface
// Storage
mailDirectory: '/tmp/maildev', // Persist emails to disk (optional)
maxEmails: 0, // Keep at most this many emails (0 = unlimited, default)
// Authentication
incomingUser: 'user', // SMTP auth username
incomingPass: 'pass', // SMTP auth password
webUser: 'admin', // Web UI username
webPass: 'admin', // Web UI password
// Relay (outgoing mail)
outgoingHost: 'smtp.example.com',
outgoingPort: 587,
outgoingUser: 'user',
outgoingPass: 'pass',
outgoingSecure: true,
autoRelay: true, // Auto-forward emails
// Logging
verbose: false,
silent: false,
logMailContents: false,
// MCP (Claude integration)
mcp: true, // Enable MCP server at /mcp endpoint
})
下面对几个关键选项做源码级解读:
| smtp | 1025 | SMTP 监听端口。注意 SMTP 服务器源码中使用 ?? 而非 \\|\\| 判断默认值,因此显式传入 smtp: 0 会被当作"由操作系统分配空闲端口"而不是回退默认值(见 packages/smtp/src/server.ts) |
| ip | '::' | SMTP 绑定地址,默认监听所有 IPv6/IPv4 地址 |
| web | 1080 | Web/API 服务器端口 |
| webIp | '0.0.0.0' | Web 服务器绑定地址 |
| disableWeb | false | 为 true 时不创建 API/UI 服务器,适用于纯 SMTP 场景 |
| mailDirectory | 不设置 | 设置后使用 FileStorage 持久化邮件;不设置则用内存存储。见下方"存储模型"小节 |
| maxEmails | 0 | 邮件数量上限,0 表示不限量(历史默认行为)。设置为正数后,超出上限时最旧的邮件连同磁盘 .eml 文件与附件一并删除 |
| incomingUser / incomingPass | 不设置 | SMTP 认证凭据,两者同时设置才启用(见 packages/cli/src/server/orchestrator.ts) |
| webUser / webPass | 不设置 | Web 界面 HTTP Basic Auth 凭据;/api/healthz 健康检查端点始终免认证(见 packages/api/src/server.ts) |
| outgoingHost / outgoingPort | 不设置 / 25 | 出站中继服务器地址。注意 outgoingPort 缺省时在 orchestrator 中回退为 25(见 packages/cli/src/server/orchestrator.ts) |
| outgoingSecure | 不设置 | 中继连接是否使用 TLS/SSL |
| autoRelay | 不设置 | 自动转发所有收件邮件;传入字符串(如 'catch-all@example.com')则把全部邮件转发到该固定收件人 |
| verbose / silent | false | 控制日志输出 |
| mcp | false | 启用 MCP(Model Context Protocol)服务器,挂载于 /mcp 端点,供 Claude 等 Agent 读取与操作邮件 |
构造背后的机制
MailDev 构造函数在源码中做三件事:用默认配置合并用户配置、根据 verbose/silent 创建日志器、创建 Orchestrator(见 packages/cli/src/index.ts)。Orchestrator 的 createStorage() 根据是否设置 mailDirectory 决定实例化 FileStorage 还是 MemoryStorage,两者都接收 maxEmails 作为上限(见 packages/cli/src/server/orchestrator.ts)。
四、核心方法:start / stop / isRunning / getServers
// 启动所有服务器(异步)
const servers = await maildev.start()
// servers.smtp – SMTP server instance
// servers.storage – Storage instance
// servers.api – API server instance (if not disabled)
// 优雅停止所有服务器(异步)
await maildev.stop()
// 检查服务器是否运行
if (maildev.isRunning()) {
console.log('MailDev is running')
}
// 获取服务器实例
const servers = maildev.getServers()
几个值得注意的细节:
- stop() 的关闭顺序:源码中按照"API → SMTP → 存储"逆序停止。API 关闭时会先关闭所有 MCP 会话与 WebSocket 连接再关闭 Fastify;存储则调用 close()(见 packages/cli/src/server/orchestrator.ts)。
- getServers() 的返回时机:只有在 running 且存储、SMTP 都已创建时才返回实例对象,否则返回 null(见 packages/cli/src/server/orchestrator.ts)。
- Servers 结构:{ storage, smtp, api? },api 在 disableWeb 时为可选(见 packages/cli/src/server/orchestrator.ts)。
五、临时端口(Ephemeral Ports):并行测试的关键技巧
并行测试运行器(如 Jest、Vitest 的多 worker)最大的痛点之一是端口冲突。MailDev 支持传 smtp: 0(或 web: 0)让操作系统分配空闲端口,再读回实际绑定的端口——这是官方推荐的在并行测试中"每个 worker 一个 MailDev"的可靠方式,无需手工分配端口:
const maildev = new MailDev({ smtp: 0, disableWeb: true })
const { smtp } = await maildev.start()
const { host, port } = smtp.getAddress()
console.log(`SMTP listening on ${host}:${port}`)
// Point your app's mail transport at `port` — e.g. nodemailer:
// nodemailer.createTransport({ host, port, secure: false })
SMTP 与 API 服务器都暴露了绑定地址访问器:
- smtp.getAddress() → { host, port }(以及 smtp.getPort())
- servers.api?.getAddress() → { host, port } | null(以及 api.getPort())
API 服务器的访问器在开始监听之前返回 null。
const maildev = new MailDev({ smtp: 0, web: 0 })
const { smtp, api } = await maildev.start()
const smtpPort = smtp.getPort() // OS-assigned SMTP port
const webPort = api?.getPort() // OS-assigned web/API port
原理:SMTP 服务器在 start() 成功后通过 this.smtp!.server.address() 读取操作系统实际绑定的端口并写回 this.port(见 packages/smtp/src/server.ts);API 服务器同理,在 listen() 中记录 boundAddress(见 packages/api/src/server.ts)。所以 smtp: 0 报告的是真实端口而非 0。
另外 SMTP 服务器源码对 port: 0 使用 ?? 回退默认值的处理,保证了"0 是有效值"语义正确(见 packages/smtp/src/server.ts)。
六、与邮件交互:事件、查询、分页与持久化
6.1 监听新邮件
MailDev 运行后,可以通过 SMTP 服务器实例访问邮件:
const maildev = new MailDev()
const { smtp } = await maildev.start()
smtp.on('new', (email) => {
console.log('New email received!')
console.log('From:', email.from[0].address)
console.log('To:', email.to.map(t => t.address).join(', '))
console.log('Subject:', email.subject)
console.log('Text:', email.text)
console.log('HTML:', email.html)
})
注意:email.html 经过 sanitizeHtml 净化处理(见 packages/smtp/src/server.ts),避免在 Web 界面中渲染恶意脚本。
6.2 限制保留的邮件数量
MailDev 默认保留所有邮件(maxEmails: 0)。设置正数 maxEmails 后,只保留最新的 N 封,新邮件到达时丢弃最旧的。当邮件持久化到磁盘时,.eml 文件和附件会随邮件一并删除,邮件目录同样保持有界:
const maildev = new MailDev({
mailDirectory: '/var/mail/maildev',
maxEmails: 5000,
})
性能提醒:maxEmails: 0(默认)会无限增长。内存使用与邮件目录都会持续膨胀:按典型邮件大小估算,10000 封邮件约占用 150 MB 堆内存,且磁盘文件永远不会被移除——长时运行或高吞吐场景务必设置正数上限。
实现细节:SMTP 服务器在构造时向存储注册 onEvicted 回调,一旦存储因上限淘汰邮件,就会同步删除磁盘上的 .eml 与附件目录(见 packages/smtp/src/server.ts)。启动时若配置了 maxEmails > 0,orchestrator 还会调用 pruneMailDir 预先清理历史上遗留的超限 .eml 文件(见 packages/cli/src/server/orchestrator.ts)。
6.3 列出大型收件箱:优先使用 storage.list()
smtp.getAllEmails() 会把每封邮件(包括正文)完整物化到内存。列表场景应改用 storage.list()——它返回一页邮件及分页所需的计数,工作量与页大小成正比,而不是与整个存储规模成正比:
const { items, total, unread } = await servers.storage.list({
skip: 0,
limit: 50,
search: 'welcome', // optional: subject, participants and body text
sort: 'desc', // 'desc' (default) is newest first
})
要丢弃邮件正文与头信息(REST /api/email/summary 端点与 Web UI 正是这样做的),把结果页映射到 toSummary:
import { toSummary } from '@maildev/core'
const summaries = items.map(toSummary)
原理:EmailSummary 是刻意设计为"轻量投影"的类型,不包含 html、text、headers 这三个体积大户——每封邮件在列表中只占几百字节,而非几十 KB,这正是 Web UI 在大收件箱下仍保持响应性的关键(见 packages/core/src/types/email.ts)。API 服务端在 /api/email/summary 路由中正是调用 storage.list() 后逐条 toSummary 返回(见 packages/api/src/server.ts)。
6.4 邮件 CRUD 操作
// 获取全部邮件
const emails = await smtp.getAllEmails()
console.log(`Total emails: ${emails.length}`)
// 获取单封邮件
const email = await smtp.getEmail('email-id')
console.log(email.subject)
// 获取原始邮件(EML 格式),可写流落盘
const stream = await smtp.getRawEmail('email-id')
stream.pipe(fs.createWriteStream('email.eml'))
// 删除单封邮件
await smtp.deleteEmail('email-id')
// 删除全部邮件
await smtp.deleteAllEmails()
// 全部标记为已读
const count = await smtp.markAllRead()
console.log(`Marked ${count} emails as read`)
实现细节:
- getRawEmail 直接以流形式返回 mailDir/<id>.eml 文件(见 packages/smtp/src/server.ts);
- deleteEmail 会先删除磁盘文件再删除存储记录,成功时发出 'delete' 事件(见 packages/smtp/src/server.ts);
- deleteAllEmails 会清空整个邮件目录(含附件),并发出 { id: 'all' } 的 'delete' 事件(见 packages/smtp/src/server.ts);
- markAllRead 返回被标记的邮件数量,底层调用存储的 markAllRead()。
6.5 附件操作
const email = await smtp.getEmail('email-id')
for (const attachment of email.attachments) {
console.log(`Attachment: ${attachment.filename}`)
console.log(`Type: ${attachment.contentType}`)
console.log(`Size: ${attachment.size} bytes`)
}
// 获取附件内容
const { contentType, stream } = await smtp.getEmailAttachment('email-id', 'filename.pdf')
stream.pipe(fs.createWriteStream('filename.pdf'))
实现细节:附件以"生成文件名"(generatedFileName,基于 MD5 哈希,见 packages/core/src/types/email.ts)保存为 mailDir/<id>/ 目录下的文件,getEmailAttachment 按 generatedFileName 匹配并返回可读流(见 packages/smtp/src/server.ts)。采用哈希文件名而非原始文件名,是为了避免路径穿越等安全问题。
七、邮件对象结构
Email 对象是 MailDev 的核心数据结构(见 packages/core/src/types/email.ts),定义如下:
interface Email {
id: string
time: Date
read: boolean
subject: string
source: string
size: number
sizeHuman: string
from: Address[]
to: Address[]
cc?: Address[]
bcc?: Address[]
calculatedBcc?: Address[]
date?: Date
html?: string
text?: string
headers: Record<string, string | string[]>
inReplyTo?: string
priority?: 'high' | 'normal' | 'low'
attachments: Attachment[]
envelope: Envelope
relayedAt?: Date // when the email was last successfully relayed
relayedTo?: string[] // recipients delivered to on the last successful relay
}
interface Address {
address: string
name?: string
}
interface Envelope {
from: EnvelopeAddress
to: EnvelopeAddress[]
host?: string
remoteAddress?: string
}
interface EnvelopeAddress extends Address {
args?: boolean | Record<string, unknown>
}
interface Attachment {
filename: string
generatedFileName: string
contentType: string
contentDisposition: 'inline' | 'attachment'
contentId?: string
size?: number
transferred?: boolean
}
字段说明:
- id:8 位字母数字唯一标识,由 makeId() 生成,同时也是磁盘 .eml 文件名;
- source:.eml 文件在磁盘上的绝对路径;
- calculatedBcc:由 SMTP 信封收件人减去头部 To/Cc 推导出的密送收件人(由 calculateBcc 计算,见 packages/core/src/helpers/bcc.ts);
- relayedAt / relayedTo:记录最近一次成功中继的时间与收件人,仅在启用出站中继时存在;
- attachments:仅元数据(文件名、MIME 类型、CID 等),附件二进制内容通过 getEmailAttachment() 以流获取。
八、中继(转发)邮件
MailDev 可以把收到的邮件转发到真实 SMTP 服务器。
8.1 手动中继
const maildev = new MailDev({
outgoingHost: 'smtp.gmail.com',
outgoingPort: 587,
outgoingUser: 'you@gmail.com',
outgoingPass: 'app-password',
outgoingSecure: true,
})
const { smtp } = await maildev.start()
// 手动中继指定邮件
smtp.on('new', async (email) => {
if (email.to.some(t => t.address === 'important@example.com')) {
await smtp.relayEmail(email.id)
console.log('Email relayed!')
}
})
成功中继(手动或自动)后,邮件记录 relayedAt 与 relayedTo,可用于判断消息是否以及在哪里被投递:
const email = await smtp.getEmail('email-id')
if (email.relayedAt) {
console.log(`Relayed at ${email.relayedAt} to ${email.relayedTo?.join(', ')}`)
}
持久化差异:使用 FileStorage 时该状态会持久化到磁盘并在下次启动时恢复;内存存储下它只在进程生命周期内有效。中继成功后 relayEmail() 会把 relayedAt 与 relayedTo 写回存储(见 packages/smtp/src/server.ts)。
8.2 自动中继
const maildev = new MailDev({
outgoingHost: 'smtp.example.com',
outgoingPort: 587,
outgoingUser: 'user',
outgoingPass: 'pass',
autoRelay: true, // 自动中继所有邮件
})
8.3 自动中继到指定地址
const maildev = new MailDev({
outgoingHost: 'smtp.example.com',
outgoingPort: 587,
outgoingUser: 'user',
outgoingPass: 'pass',
autoRelay: 'catch-all@example.com', // 覆盖收件人
})
autoRelay 传入字符串时,orchestrator 会把它作为 defaultRecipient 注入中继配置——即所有邮件改写为发给该固定地址(见 packages/cli/src/server/orchestrator.ts)。
九、事件系统
SMTP 服务器对外发出以下事件(事件类型定义见 packages/smtp/src/types.ts):
'new' — 收到新邮件
smtp.on('new', (email: Email) => {
console.log('New email:', email.subject)
})
'delete' — 邮件被删除
smtp.on('delete', (data: { id: string }) => {
console.log('Deleted email:', data.id)
})
'error' — 服务器错误
smtp.on('error', (error: Error) => {
console.error('SMTP error:', error.message)
})
'close' — 服务器关闭
smtp.on('close', () => {
console.log('SMTP server closed')
})
实现细节:'new' 事件在邮件成功保存到存储后发出;'delete' 在删除成功后发出;'close' 由 stop() 触发(见 packages/smtp/src/server.ts)。WebSocket 层正是通过订阅这两个事件把更新实时推送给 Web UI 的所有标签页(见 packages/api/src/server.ts)。
十、高级用法
10.1 直接使用独立子包
追求更精细控制时,可以绕过 MailDev 门面类,直接组合底层包:
import { MemoryStorage } from '@maildev/core'
import { createSMTPServer } from '@maildev/smtp'
import { createAPIServer } from '@maildev/api'
// 创建存储
const storage = new MemoryStorage()
await storage.initialize()
// 创建 SMTP 服务器
const smtp = createSMTPServer({
port: 1025,
host: '::',
storage,
mailDir: '/tmp/maildev',
})
await smtp.start()
// 创建 API 服务器
const api = createAPIServer({
port: 1080,
storage,
smtp,
mcp: { enabled: true },
})
await api.start()
说明:子包边界清晰——@maildev/core 提供存储与类型(Storage、Email 等),@maildev/smtp 提供 createSMTPServer 与中继能力,@maildev/api 提供基于 Fastify 的 REST API、WebSocket 与 MCP 端点。APIServer 还暴露 registerPlugins() 方法,可先注册插件与路由、再 listen(),便于在注册 UI 前后插入自定义逻辑(见 packages/api/src/server.ts)。
10.2 使用 FileStorage 持久化
import { FileStorage } from '@maildev/core'
const storage = new FileStorage({
mailDirectory: '/var/mail/maildev',
maxEmails: 1000, // optional: cap the store (0/omitted = unlimited)
})
await storage.initialize()
当 maxEmails 被超出时,最旧的邮件被丢弃且其文件被删除。若要清理你自己在邮件旁边写入的其他数据,可注册 evict 回调——save() 会等待它完成,因此回调 resolve 后邮件才算完全消失:
storage.onEvicted(async (email) => {
await removeMyIndexEntry(email.id)
})
实现细节:SMTP 服务器正是通过 onEvicted 挂钩来删除磁盘 .eml 与附件目录的(见 packages/smtp/src/server.ts),这一机制同样可以服务于你的自定义清理逻辑。
10.3 自定义日志器
import { MailDev, createLogger } from 'maildev'
const logger = createLogger({
verbose: true,
silent: false,
})
// The MailDev class uses the logger internally
const maildev = new MailDev({
verbose: true,
})
MailDev 构造时根据 verbose/silent 创建内部 Logger(见 packages/cli/src/index.ts),日志器提供 log / info / warn / error / debug 等级别(见 packages/cli/src/utils/logger.ts)。
10.4 中间件集成(代理 / 子路径挂载)
MailDev 可以挂在反向代理之后,或通过 basePathname 运行在现有 Express/Fastify 应用的子路径下:
const maildev = new MailDev({
basePathname: '/maildev',
web: 3001,
})
await maildev.start()
// MailDev UI now available at http://localhost:3001/maildev
然后把请求代理到 MailDev:
import express from 'express'
import { createProxyMiddleware } from 'http-proxy-middleware'
const app = express()
app.use('/maildev', createProxyMiddleware({
target: 'http://localhost:3001',
ws: true,
}))
app.listen(3000)
// MailDev accessible at http://localhost:3000/maildev
实现细节:orchestrator 会归一化 basePathname——根路径 / 被规范为空字符串、尾部斜杠被去除,再传给 API 服务器(见 packages/cli/src/server/orchestrator.ts)。所有 REST 路由、Socket.io 路径(<basePath>/socket.io)与 MCP 端点(<basePath>/mcp)都会自动加上该前缀(见 packages/api/src/server.ts)。ws: true 确保 WebSocket 也能穿透代理,让多标签页实时刷新正常工作。
十一、TypeScript 支持
MailDev v3 完全用 TypeScript 编写,导出全部类型:
import type {
MailDevConfig,
Email,
EmailSummary,
Address,
Attachment,
Storage,
ListOptions,
ListResult,
} from 'maildev'
import type {
SMTPServer,
SMTPServerOptions,
RelayConfig,
} from '@maildev/smtp'
import type {
APIServer,
APIServerOptions,
} from '@maildev/api'
此外 @maildev/api 还导出了 AuthConfig、CorsConfig、EmailQuery、ConfigResponse、SummaryResponse、DeleteResponse、ErrorResponse 等 API 相关类型,并重导出核心类型(见 packages/api/src/index.ts 与 packages/api/src/types.ts)。例如 SummaryQuery 明确了 /api/email/summary 端点接受的 skip、limit、search、sort、unread 参数(见 packages/api/src/types.ts)。
十二、从 v2 迁移到 v3
关键变化
迁移示例
v2(回调风格):
const MailDev = require('maildev')
const maildev = new MailDev()
maildev.listen(function(err) {
if (err) throw err
console.log('MailDev running')
})
maildev.on('new', function(email) {
console.log('New email:', email.subject)
})
maildev.getAllEmail(function(err, emails) {
console.log('Total:', emails.length)
})
v3(async/await 风格):
import { MailDev } from 'maildev'
const maildev = new MailDev()
const { smtp } = await maildev.start()
console.log('MailDev running')
smtp.on('new', (email) => {
console.log('New email:', email.subject)
})
const emails = await smtp.getAllEmails()
console.log('Total:', emails.length)
注意 v3 中收件接口从 maildev.getAllEmail(cb) 变为 smtp.getAllEmails(),事件监听对象从 maildev 变更为 start() 返回的 smtp 实例——这正是 v3 把"服务器"与"门面"解耦的直接体现。
总结
MailDev v3 的程序化 API 把"本地 SMTP 收件 + Web 查看 + REST/WebSocket + MCP"整合为一个可编程的 TypeScript 组件。本文覆盖了从 MailDev 门面类的全部配置项、生命周期方法,到临时端口、分页查询、附件流式读取、邮件中继、事件驱动、子包级组合与 v2 迁移的完整链路。如果你正在搭建自动化邮件测试、开发调试工具或需要把邮件收件能力嵌入 Node.js 服务,这些 API 即开即用。
如需查看更多细节,可继续阅读仓库内的 docs/rest.md(REST 端点)、docs/mcp.md(MCP 集成)与 docs/https.md(HTTPS 配置),以及各子包的 README(packages/core/README.md、packages/smtp/README.md、packages/api/README.md)。
赞
【免费下载链接】maildev
:mailbox: SMTP Server + Web Interface for viewing and testing emails during development.
项目地址:
https://gitcode.com/gh_mirrors/ma/maildev
点击查看 免费下载
相关推荐
免费音频转换神器fre:ac完整使用指南:从入门到精通
OPC-UA客户端工具:工业自动化数据交互的终极解决方案
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网硕互联帮助中心







评论前必须登录!
注册