从 tinify-cli 到 imgasset:把博客配图做成一条流水线

从图片压缩工具 tinify-cli 到 AI 生图与压缩一体化工具 imgasset,记录一套博客配图工作流如何从临时脚本沉淀成可复用的 npm CLI。

最近给博客补配图时,我又遇到了一个很熟悉的问题:流程本身并不复杂,但步骤很多,而且每次都差不多。

先根据文章内容写提示词,用图片模型生成原图;原图不能直接进仓库,要放在临时目录里;选中的图要压缩、转成适合网页使用的格式,再放到 public/assets/ 下;最后把 Markdown 里的图片路径补上,跑构建和链接检查。

一两篇文章手工做没什么问题。文章多了之后,这件事就开始变成一种重复劳动。

于是我把这个流程拆成了两个工具:

它们不是一开始就设计好的“产品”。更准确地说,是从真实写博客的流程里,一层一层提炼出来的。

English version: From tinify-cli to imgasset: Turning Blog Images into a Pipeline

AI 生图、压缩和发布目录被组织成一条图片资产流水线

如果要跑完整流程,可以先安装 imgasset

npm install -g @yigemo/imgasset

imgasset 已经把 tinify-cli 作为依赖带上了。只需要单独做图片压缩时,也可以只安装压缩工具:

npm install -g @yigemo/tinify-cli

第一层:先把图片压缩标准化

在写 imgasset 之前,我先写了 tinify-cli

图片压缩这件事看起来很小,但在内容站里非常高频。博客文章、官网配图、产品截图、社交分享图,最后都要面对同一个问题:原图太大,直接放线上不合适。

TinyPNG / Tinify 的压缩效果一直不错,但如果每次都打开网页上传下载,或者在不同项目里复制一段调用 API 的脚本,长期维护会很麻烦。

所以 tinify-cli 的目标很简单:把压缩变成一条稳定命令。

tinify login

登录后压缩一个目录:

tinify temp/article/raw \
  --recursive \
  --out-dir public/assets/article \
  --format jpeg \
  --background white \
  --suffix ""

这里几个参数对博客配图很关键。

--recursive 可以保留目录结构,适合一篇文章有多张图的情况。

--out-dir 可以把压缩后的图片写到发布目录,而不是覆盖原图。

--format jpeg--background white 让生成图可以从 PNG 转成更适合网页展示的 JPEG,同时处理透明背景。

--suffix "" 则是为了让最终文件名保持干净。原图可能在 temp/article/raw/01-context.png,发布图可以直接变成 public/assets/article/01-context.jpg

原图经过压缩和格式转换后进入发布目录

这一步解决的是压缩和格式转换的可重复性。

但很快又出现了第二个问题:原图从哪里来?

第二层:AI 生图也需要工作流

给文章配图时,生图本身只是一个环节。

真正麻烦的是围绕生图的上下文:

  • 每张图的提示词要能保存和复用。
  • 生成出来的原图要有固定位置。
  • 已经生成过的图不要重复生成。
  • API key 不能写进项目仓库。
  • 模型、尺寸、质量、代理、base URL 这些配置应该可以复用。
  • 生成完之后最好能直接进入压缩流程。

如果每次都临时写脚本,这些细节会不断散落在不同项目里。脚本一多,就会出现新的问题:哪个脚本能用,哪个脚本已经过期,哪个脚本里写死了本地路径,哪个脚本里不小心带了敏感配置。

imgasset 解决的就是这部分。

它把生图配置分成三层:

  • 全局 profile:保存 base URL、模型、尺寸、质量、代理等非敏感配置。
  • 全局 secret:保存 API key,不进入项目仓库。
  • 项目配置:保存当前项目的原图目录、发布目录、压缩参数。

初始化配置:

imgasset config init

创建一个 profile:

imgasset profile set default \
  --base-url https://api.example.com/v1 \
  --model gpt-image-2 \
  --size 1536x1024 \
  --quality medium \
  --output-format png \
  --default

保存 API key:

imgasset secret set default

然后在项目里写一个 JSONL 提示词文件:

{"out":"01-context.png","prompt":"Minimal surreal isometric 3D editorial poster..."}
{"out":"02-flow.png","prompt":"Minimal surreal isometric 3D editorial poster..."}

生成原图:

imgasset generate prompts.jsonl \
  --raw-dir temp/imgasset/article/raw \
  --skip-existing

这里的 --skip-existing 很实用。图片生成经常比较慢,也可能遇到网络波动。一批图如果生成到第三张失败,下一次重跑时不应该把前两张再生成一遍。

把两步连起来

单独有 tinify-cliimgasset generate 已经能覆盖大部分场景,但最顺手的还是一条命令跑完整流程。

imgasset run prompts.jsonl \
  --raw-dir temp/imgasset/article/raw \
  --publish-dir public/assets/article \
  --format jpeg \
  --background white \
  --skip-existing

这个命令会先生成原图,再调用内置依赖里的 tinify-cli 做压缩和格式转换。

提示词、原图目录和发布目录被串成连续的图片生产路径

也就是说,一个项目只要安装 imgasset,就同时拥有了生图和压缩能力,不需要再单独维护一套压缩脚本。

实际使用时,我通常会让目录结构保持这样:

temp/
  imgasset/
    my-article/
      raw/
public/
  assets/
    posts/
      2026/
        my-article/
prompts.jsonl

temp/ 放原图和临时文件,进入 .gitignore

public/assets/ 放压缩后的发布图,可以被文章引用。

Markdown 里只引用最终输出:

![内容系统示意图](/assets/posts/2026/my-article/01-context.jpg)

这个边界很重要。原图是生产资料,发布图才是网站资产。

为什么不用一个脚本解决

最开始当然可以用一个脚本解决。

甚至对一个项目来说,一个脚本往往是最省事的。把 API key 从环境变量里读出来,循环请求图片接口,生成后再调用压缩 API,几十行代码就能跑起来。

问题在于,这类脚本很容易变成一次性资产。

当第二个项目也需要类似流程时,就会开始复制脚本。复制之后又会改目录、改模型、改压缩格式、改代理、改错误处理。再过一段时间,就很难判断哪一份才是最新实践。

工具化的价值不在于代码量更少,而在于把边界固定下来:

  • API key 永远不进项目。
  • 原图默认进入临时目录。
  • 提示词用 JSONL 保存。
  • 输出路径由命令或项目配置决定。
  • 压缩能力通过依赖提供,不要求每个项目额外安装。
  • 中断后可以继续跑。

这些约定一旦稳定下来,后续每个项目都能复用同一套工作流。

关于安全和开源

这两个工具都发布到了 npm,也放到了 GitHub 上。

开源前我重点处理了三件事。

第一是密钥。API key 只能存在全局 secret 文件或环境变量里,项目配置、提示词文件、日志和报告都不应该包含密钥。

配置、密钥和项目资产之间保持清晰边界

第二是示例。示例里只能出现 https://api.example.com/v1 这种占位 base URL,不能把任何实际使用的服务地址写进去。

第三是发布流程。两个包都尽量走标准 npm 包形态,imgasset 还配置了 GitHub Actions 和 npm Trusted Publishing。发版时只需要:

pnpm run release

脚本会递增版本号、打 tag,GitHub Actions 再根据 tag 发布到 npm。

这套流程看起来比手动 npm publish 麻烦一点,但长期更可靠。尤其是开源包,发布过程越可追溯越好。

最后还是回到写文章

这套工具做完之后,变化最明显的地方不是少敲了几行命令,而是配图不再打断写作节奏。

以前一想到要给文章补图,脑子里会先冒出一串杂事:提示词放哪,原图放哪,压缩后叫什么名字,路径会不会写错,失败后要从哪里接着跑。每件事都很小,但它们会把注意力从文章里拉出来。

现在流程更接近这样:

  1. 读文章,决定需要几张图。
  2. prompts.jsonl
  3. imgasset run
  4. 把输出图插进 Markdown。
  5. 构建检查。

真正需要判断的部分还在:文章适合什么意象,几张图够不够,图片放在哪里能帮助阅读,哪张图虽然好看但不该用。工具只是把目录、命令、格式转换和失败重试这些事情固定下来。

所以这篇文章想记录的不是“又写了两个 npm 包”,而是一个小流程变稳的过程。

小工具最有价值的状态,大概就是平时感觉不到它的存在,但换一个项目时又能马上带走。tinify-cli 处理压缩这一步,imgasset 把生图、原图保存和发布输出串起来。它们加在一起,解决的不是某一次图片生成,而是下一篇文章、下一个站点、下一个项目里仍然能复用的图片资产流程。