该代码提供了一个异步函数 mermaid_to_file,用于将 Mermaid 图表代码转换为多种格式的图像文件(如 PNG、PDF、SVG)。它支持多种渲染引擎(nodejs、playwright、pyppeteer、ink、none),并包含错误处理和日志记录功能。代码末尾还定义了两个示例 Mermaid 图表字符串(MMC1 和 MMC2)。
graph TD
A[开始: mermaid_to_file 被调用] --> B{检查并添加文件头}
B --> C[写入临时 .mmd 文件]
C --> D{引擎类型?}
D -- nodejs --> E{检查 mmdc 命令是否存在}
E -- 存在 --> F[构建并执行 mmdc 命令]
E -- 不存在 --> G[记录警告并返回 -1]
D -- playwright --> H[调用 metagpt.utils.mmdc_playwright.mermaid_to_file]
D -- pyppeteer --> I[调用 metagpt.utils.mmdc_pyppeteer.mermaid_to_file]
D -- ink --> J[调用 metagpt.utils.mmdc_ink.mermaid_to_file]
D -- none --> K[直接返回 0]
D -- 其他 --> L[记录警告: 不支持的引擎]
F --> M[处理子进程输出]
H --> N[返回结果]
I --> N
J --> N
K --> N
L --> N
M --> N[返回 0 或 -1]
mermaid.py (主文件)
├── 全局函数: mermaid_to_file
├── 全局变量: MMC1, MMC2
└── 外部依赖:
├── metagpt.utils.mmdc_playwright
├── metagpt.utils.mmdc_pyppeteer
└── metagpt.utils.mmdc_ink包含一个类图(classDiagram)的Mermaid代码字符串,描述了一个搜索系统的主要组件及其关系。
类型:str
包含一个序列图(sequenceDiagram)的Mermaid代码字符串,描述了搜索系统中各组件之间的交互流程。
类型:str
该函数是一个异步函数,用于将Mermaid代码转换为多种格式的文件(如PNG、PDF、SVG)。它支持多种渲染引擎(如nodejs、playwright、pyppeteer、ink、none),并允许指定输出图像的宽度、高度以及文件后缀。函数首先将Mermaid代码写入临时文件,然后根据指定的引擎调用相应的工具或库进行转换。如果使用nodejs引擎,会调用外部命令mmdc;如果使用其他引擎,则调用相应的Python模块。转换成功返回0,失败返回-1。
参数:
engine:str,指定用于转换的引擎。支持的引擎有:"nodejs"、"playwright"、"pyppeteer"、"ink"和"none"。mermaid_code:str,需要转换的Mermaid代码。output_file_without_suffix:str,输出文件的名称(不包含后缀)。width:int,可选,输出图像的宽度,默认为2048。height:int,可选,输出图像的高度,默认为2048。config:Optional[Config],可选,用于转换的配置对象,默认为None,使用默认配置。suffixes:Optional[List[str]],可选,要生成的文件后缀列表,支持"png"、"pdf"和"svg",默认为["png"]。
返回值:int,如果转换成功返回0,如果转换失败返回-1。
flowchart TD
A[开始] --> B{检查Mermaid代码是否包含文件头?}
B -- 否 --> C[添加默认文件头]
B -- 是 --> D[设置默认后缀为'svg']
C --> D
D --> E[写入临时文件]
E --> F{引擎类型?}
F -- nodejs --> G{检查mmdc命令是否存在?}
G -- 不存在 --> H[输出警告并返回-1]
G -- 存在 --> I[遍历后缀列表]
I --> J[构建命令并执行]
J --> K[记录输出和错误]
K --> L[返回0]
F -- playwright --> M[调用playwright模块]
M --> L
F -- pyppeteer --> N[调用pyppeteer模块]
N --> L
F -- ink --> O[调用ink模块]
O --> L
F -- none --> P[直接返回0]
P --> L
F -- 其他 --> Q[输出警告并返回0]
Q --> L
async def mermaid_to_file(
engine,
mermaid_code,
output_file_without_suffix,
width=2048,
height=2048,
config=None,
suffixes: Optional[List[str]] = None,
) -> int:
"""Convert Mermaid code to various file formats.
Args:
engine (str): The engine to use for conversion. Supported engines are "nodejs", "playwright", "pyppeteer", "ink", and "none".
mermaid_code (str): The Mermaid code to be converted.
output_file_without_suffix (str): The output file name without the suffix.
width (int, optional): The width of the output image. Defaults to 2048.
height (int, optional): The height of the output image. Defaults to 2048.
config (Optional[Config], optional): The configuration to use for the conversion. Defaults to None, which uses the default configuration.
suffixes (Optional[List[str]], optional): The file suffixes to generate. Supports "png", "pdf", and "svg". Defaults to ["png"].
Returns:
int: 0 if the conversion is successful, -1 if the conversion fails.
"""
# 添加默认的Mermaid文件头,用于设置主题和字体
file_head = "%%{init: {'theme': 'default', 'themeVariables': { 'fontFamily': 'Inter' }}}%%\n"
if not re.match(r"^%%\{.+", mermaid_code):
mermaid_code = file_head + mermaid_code
# 设置默认的后缀列表为["svg"]
suffixes = suffixes or ["svg"]
# 使用提供的配置或默认配置
config = config if config else Config.default()
# 确保输出目录存在
dir_name = os.path.dirname(output_file_without_suffix)
if dir_name and not os.path.exists(dir_name):
os.makedirs(dir_name)
# 将Mermaid代码写入临时文件
tmp = Path(f"{output_file_without_suffix}.mmd")
await awrite(filename=tmp, data=mermaid_code)
# 根据指定的引擎进行转换
if engine == "nodejs":
# 检查mmdc命令是否存在
if check_cmd_exists(config.mermaid.path) != 0:
logger.warning(
"RUN `npm install -g @mermaid-js/mermaid-cli` to install mmdc,"
"or consider changing engine to `playwright`, `pyppeteer`, or `ink`."
)
return -1
# 遍历所有指定的后缀,生成对应的文件
for suffix in suffixes:
output_file = f"{output_file_without_suffix}.{suffix}"
logger.info(f"Generating {output_file}..")
# 构建mmdc命令
if config.mermaid.puppeteer_config:
commands = [
config.mermaid.path,
"-p",
config.mermaid.puppeteer_config,
"-i",
str(tmp),
"-o",
output_file,
"-w",
str(width),
"-H",
str(height),
]
else:
commands = [config.mermaid.path, "-i", str(tmp), "-o", output_file, "-w", str(width), "-H", str(height)]
# 执行命令并捕获输出
process = await asyncio.create_subprocess_shell(
" ".join(commands), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await process.communicate()
if stdout:
logger.info(stdout.decode())
if stderr:
logger.warning(stderr.decode())
else:
# 根据引擎类型调用相应的Python模块
if engine == "playwright":
from metagpt.utils.mmdc_playwright import mermaid_to_file
return await mermaid_to_file(mermaid_code, output_file_without_suffix, width, height, suffixes=suffixes)
elif engine == "pyppeteer":
from metagpt.utils.mmdc_pyppeteer import mermaid_to_file
return await mermaid_to_file(mermaid_code, output_file_without_suffix, width, height, suffixes=suffixes)
elif engine == "ink":
from metagpt.utils.mmdc_ink import mermaid_to_file
return await mermaid_to_file(mermaid_code, output_file_without_suffix, suffixes=suffixes)
elif engine == "none":
return 0
else:
logger.warning(f"Unsupported mermaid engine: {engine}")
return 0该组件是一个多引擎适配器,负责将 Mermaid 代码转换为多种格式(如 PNG、SVG、PDF)的图像文件。它支持多种后端引擎(nodejs, playwright, pyppeteer, ink, none),通过统一的接口 mermaid_to_file 函数,根据传入的引擎参数动态选择并调用相应的底层实现,实现了渲染策略的可插拔和灵活切换。
该组件通过 Config 类及其嵌套的 mermaid 配置项来管理 Mermaid 转换所需的外部依赖路径(如 mmdc 命令行工具)和运行时参数(如 Puppeteer 配置文件路径)。它确保了转换过程可以根据不同的部署环境(如是否安装了 Node.js)进行配置,提高了代码的可移植性和可配置性。
该组件利用 asyncio 库进行异步文件写入(awrite)和子进程管理(create_subprocess_shell)。这允许在等待外部命令行工具(如 mmdc)完成渲染时,程序可以处理其他任务,提高了 I/O 密集型操作的效率和整体程序的响应能力。
在转换过程中,该组件会将传入的 Mermaid 代码先写入一个临时文件(.mmd 后缀),然后将此文件路径传递给底层的渲染引擎。这种设计隔离了代码逻辑与具体的渲染工具,使得不同引擎可以使用统一的文件接口,同时也便于调试和中间结果的检查。
该组件包含基本的错误处理逻辑,例如检查所需的命令行工具是否存在(check_cmd_exists)。当首选引擎(如 nodejs)不可用时,它会记录警告信息并返回错误码,或者(在代码逻辑中)允许调用者切换到其他可用的引擎(如 playwright),提供了基本的健壮性和用户指引。
- 硬编码的临时文件路径:代码中临时文件路径的构建方式 (
Path(f"{output_file_without_suffix}.mmd")) 存在潜在风险。如果output_file_without_suffix包含特殊字符或路径分隔符,可能导致文件创建失败或写入到非预期目录。同时,未使用系统安全的临时文件目录,可能引发权限问题或遗留临时文件。 - 子进程命令注入风险:在使用
nodejs引擎时,通过asyncio.create_subprocess_shell执行命令字符串 (" ".join(commands))。如果config.mermaid.path、config.mermaid.puppeteer_config或output_file等变量包含用户可控的恶意输入(如; rm -rf /),可能导致命令注入攻击,执行任意系统命令。 - 错误处理不充分:函数返回
0表示成功,-1表示失败,但错误信息仅通过日志记录。调用者无法区分不同类型的失败(如引擎不支持、命令不存在、转换过程出错),也难以获取具体的错误原因进行程序化处理。 - 资源泄漏风险:当使用
nodejs引擎时,创建了子进程 (process),但在函数返回前没有显式地等待其终止或检查其退出状态码。虽然await process.communicate()会等待进程结束,但如果进程异常挂起,函数可能无法正常返回。此外,生成的临时文件 (tmp) 在函数结束后没有被清理,可能导致磁盘空间浪费。 - 配置对象默认值逻辑:
config = config if config else Config.default()这行代码在config为None时使用默认配置。然而,如果传入的config对象虽然不为None但缺少必要的属性(如config.mermaid.path),后续代码会抛出AttributeError,错误处理不够健壮。 engine == "none"的语义模糊:当引擎为"none"时,函数直接返回0,表示“成功”,但实际并未执行任何转换操作。这可能导致调用者误以为文件已生成。此设计意图不清晰,可能是用于跳过转换的“空操作”,但缺乏明确说明。- 同步与异步混合的目录创建:函数使用
os.makedirs创建目录,这是一个同步阻塞调用。在异步函数中执行此类I/O操作可能会阻塞事件循环,影响整体性能,尤其是在高并发场景下。
- 使用安全的临时文件处理:建议使用
tempfile模块创建临时文件,例如with tempfile.NamedTemporaryFile(mode='w', suffix='.mmd', delete=False) as tmp:。这样可以确保文件位于系统临时目录,具有合适的权限,并且可以通过delete参数控制是否自动清理。同时,应确保在函数退出前(包括异常情况)删除临时文件。 - 防范命令注入:避免使用
create_subprocess_shell和拼接命令字符串。应改用asyncio.create_subprocess_exec,并将命令及其参数作为列表传递。例如:process = await asyncio.create_subprocess_exec(config.mermaid.path, '-i', str(tmp), ...)。这可以防止将参数误解为Shell元字符。 - 细化错误处理与返回值:定义更丰富的返回值或抛出特定异常。例如,可以定义枚举类
ConversionResult或自定义异常(如UnsupportedEngineError,CommandNotFoundError,ConversionError)。在函数内部捕获异常,记录日志,并向上层抛出或返回明确的错误码和消息。 - 确保资源清理:在
try...finally块或异步上下文管理器中处理临时文件和子进程。确保在函数退出前,无论成功与否,都尝试删除临时文件。对于子进程,除了communicate,还可以考虑设置超时 (asyncio.wait_for) 并强制终止 (process.terminate(),process.kill()) 长时间挂起的进程。 - 增强配置验证:在函数开始处,验证传入的
config对象(或默认配置)是否包含mermaid部分以及必要的属性(如path)。如果缺失,可以抛出ValueError或返回明确的错误,避免后续代码因属性错误而崩溃。 - 明确“none”引擎的行为:如果
"none"引擎是设计用于跳过转换(例如,仅用于测试或特定工作流),应在函数文档字符串中明确说明其行为,并考虑是否返回一个特殊值或生成一个空的占位文件,以避免调用者混淆。 - 使用异步文件与目录操作:将同步的
os.makedirs和awrite(虽然awrite是异步的,但目录创建是同步的)替换为完全的异步操作。可以使用aiofiles.os模块(如果项目已引入)或asyncio.to_thread将同步的os.makedirs调用转移到线程池中执行,避免阻塞事件循环。 - 考虑增加引擎自动回退机制:当前逻辑中,如果指定的引擎(如
nodejs)不可用,函数直接返回失败。可以考虑实现一个优先级列表,当首选引擎失败时,自动尝试下一个可用的引擎(例如,nodejs->playwright->pyppeteer->ink),提高鲁棒性。 - 参数默认值优化:
suffixes参数的默认值为["svg"],但函数文档和逻辑也支持"png"和"pdf"。可以考虑将默认值设为["png", "svg"]或通过配置项设置,以提供更通用的默认输出。同时,应对传入的suffixes列表进行有效性校验。
该模块的核心设计目标是提供一个灵活、可扩展的异步函数,用于将Mermaid代码转换为多种格式的图像文件(如PNG、SVG、PDF)。主要约束包括:
- 多引擎支持:必须支持多种后端渲染引擎(nodejs, playwright, pyppeteer, ink, none),以适应不同的运行环境(如是否安装Node.js或浏览器)。
- 异步操作:所有文件I/O和外部命令调用必须是异步的,以避免阻塞主程序。
- 配置驱动:渲染行为(如命令行路径、浏览器配置)应通过外部配置对象(
Config)管理,提高灵活性。 - 错误容忍与降级:当首选引擎不可用时,应提供明确的警告信息,并允许降级到其他引擎或跳过渲染(
engine="none")。
模块采用返回状态码和日志记录的方式进行错误处理,而非抛出异常,以保持调用流程的简单。
- 返回值:主函数
mermaid_to_file返回整数,0表示成功,-1表示失败(主要针对nodejs引擎的依赖检查失败)。 - 依赖检查:对于
nodejs引擎,会检查mmdc命令是否存在,若不存在则记录警告并返回-1。 - 子进程错误:执行
mmdc命令时,会捕获并记录其标准错误输出(stderr),但不会因此中断程序,仅作日志记录。 - 引擎回退:当指定引擎不支持时,记录警告信息。调用方需根据返回值判断是否成功。
- 文件操作:创建目录和写入临时文件使用异步I/O,潜在的错误会由
awrite函数或系统抛出,需由更上层的调用者处理。
- 数据流:
- 输入:Mermaid代码字符串、输出文件路径、配置参数、引擎选择。
- 处理:
- 代码预处理(添加主题头)。
- 根据引擎类型分流。
nodejs路径:写入临时.mmd文件 -> 构造命令行 -> 异步执行子进程 -> 捕获输出。- 其他引擎路径:委托给对应的专用模块(
mmdc_playwright,mmdc_pyppeteer,mmdc_ink)。
- 输出:在指定路径生成一个或多个图像文件(
.png,.svg,.pdf)。
- 状态机:函数逻辑可视为一个简化的决策状态机。
- 状态:
初始化->引擎判断-> (nodejs处理|playwright处理|pyppeteer处理|ink处理|none处理|引擎不支持) ->结束。 - 转移条件:
engine参数的值。
- 状态:
- 外部命令/工具:
@mermaid-js/mermaid-cli(mmdc):当engine="nodejs"时必需。通过命令行调用,接口契约为指定的命令行参数格式(-i,-o,-w,-H,-p)。
- Python第三方库:
playwright:当engine="playwright"时,由metagpt.utils.mmdc_playwright模块使用。pyppeteer:当engine="pyppeteer"时,由metagpt.utils.mmdc_pyppeteer模块使用。
- 项目内部模块:
metagpt.config2.Config:提供配置信息,特别是mermaid.path(mmdc路径)和mermaid.puppeteer_config(浏览器配置文件路径)。metagpt.utils.common.awrite:用于异步写入临时文件。metagpt.utils.common.check_cmd_exists:用于检查mmdc命令是否存在。metagpt.utils.mmdc_playwright/mmdc_pyppeteer/mmdc_ink:分别对应不同引擎的具体实现。mermaid_to_file函数通过动态导入调用它们,并传递参数。预期这些模块提供同名函数,并遵循相似的参数签名(可能略有不同,如ink引擎不支持width/height)。
- 接口契约:
mermaid_to_file函数是主要对外接口。调用者需确保传入的engine参数是受支持的,并理解不同引擎对环境的依赖。返回值0或-1是成功与否的简单契约。