Appearance
素材管理(Markdown 图片)
概述
素材管理是官方提供的一套图片预上传接口:把图片先行上传到平台,拿到 qqbot-media:// 引用链接,之后在 Markdown 消息里反复引用。
相比直接外链,素材管理的优势:
- 不依赖公网图床,图片由平台托管
- 同一张图在多条消息中重复使用,不必每次转存
- 发送时平台自动转存,客户端加载更稳定
本地图片 ──prepare──> upload_id + 预签名 URL
──PUT────> COS 直传(返回 ETag = 文件 MD5)
──complete──> image_id + qqbot-media:// 引用链接快速上手
上传并引用
一次性完成全流程(计算大小 / MD5 / SHA1 → 申请上传 → 直传 → 完成):
java
import cn.org.bukkit.bngeuit.Bngeuit;
import cn.org.bukkit.bngeuit.dto.message.media.MediaImageCompleteResponse;
import java.io.File;
MediaImageCompleteResponse img = Bngeuit.uploadMediaImage(new File("kline.png"));
if (img != null) {
// 宽高可选,省略则按原图比例
String md = " + ")";
Bngeuit.sendGroupMarkdown(groupOpenid, "# 行情\n" + md);
}img.url() 形如 qqbot-media://image/v1/<token>,直接写进 Markdown 图片语法即可,框架不再提供额外的标签拼接方法。
分步调用
需要自行控制每一步时(例如已有 MD5/SHA1 缓存、或需要打印中间结果):
java
import cn.org.bukkit.bngeuit.Bngeuit;
import cn.org.bukkit.bngeuit.dto.message.media.*;
var api = Bngeuit.api();
File file = new File("kline.png");
// 1. 申请上传
MediaImagePrepareResponse prep = api.prepareMediaImageUpload(
file.getName(), file.length(), MediaImageFormat.fromFile(file), md5, sha1);
// 2. 直传二进制(Content-Type 必须与申请时一致)
String etag = api.uploadMediaImageBinary(prep.presignedUrl(), MediaImageFormat.PNG, file);
// 3. 完成上传(md5 传上一步返回的 ETag)
MediaImageCompleteResponse done = api.completeMediaImageUpload(prep.uploadId(), etag);
System.out.println(done.url());md5 必须用 ETag
第 3 步的 md5 必须是第 2 步 PUT 返回的 ETag(服务端会与 COS 对象比对),而不是本地计算的 MD5,否则 complete 会失败。uploadMediaImage(File) 组合方法内部已按此顺序处理,ETag 缺失时才回退为本地 MD5。
查询素材列表
java
import cn.org.bukkit.bngeuit.dto.message.media.MediaImage;
import cn.org.bukkit.bngeuit.dto.message.media.MediaImageListResponse;
MediaImageListResponse page = Bngeuit.getMediaImages(); // 按上传时间倒序
for (MediaImage img : page.images()) {
System.out.println(img.fileName() + " 过期于 " + img.expiresAt().getValue());
}
System.out.println("当前有效图片数:" + page.total());
// 翻页
String cursor = page.nextCursor();
if (cursor != null && !cursor.isEmpty()) {
page = Bngeuit.getMediaImages(cursor);
}删除素材
java
Bngeuit.deleteMediaImage(imageId); // 释放数量配额删除后引用链接立即失效,但已发送的消息不受影响(发送时平台已转存副本)。
方法一览
一步步调用
| 方法 | 返回类型 | 说明 |
|---|---|---|
prepareMediaImageUpload(fileName, fileSize, format, md5, sha1) | MediaImagePrepareResponse | 申请上传 |
uploadMediaImageBinary(presignedUrl, format, data) | String | 直传字节数组,返回 ETag |
uploadMediaImageBinary(presignedUrl, format, file) | String | 直传本地文件,返回 ETag |
completeMediaImageUpload(uploadId, md5) | MediaImageCompleteResponse | 完成上传 |
getMediaImages(cursor) | MediaImageListResponse | 查询列表 |
deleteMediaImage(imageId) | EmptyResponse | 删除 |
uploadMediaImage(File) | MediaImageCompleteResponse | 组合方法(以上 1-3 步) |
Bngeuit 门面提供 uploadMediaImage / getMediaImages / deleteMediaImage 的静态版本,多步流程走 Bngeuit.api()。
限制
| 项目 | 限制 | 错误码 |
|---|---|---|
| 支持格式 | png / jpeg(jpg) / gif / webp,扩展名必须与 Content-Type 一致 | 40035001 |
| 单张大小 | ≤ 10MB | 40035002 |
| 单机器人数量 | 100 张 | 40035007 |
| 上传任务有效期 | 4 小时 | 40035003 |
| 引用链接有效期 | 滑动 90 天,每次被消息引用后顺延 | 40035009 |
40035003 的框架释义是「上传任务不存在、已过期(超过 4 小时)或不属于当前机器人」。遇到该错误时,除了确认上传任务是否已超时,也要确认传给 complete 的 md5 是否为 PUT 返回的 ETag。
直传 403 的常见原因
PUT 时的 Content-Type 必须与 prepare 阶段声明的格式完全一致,例如申请时用 image/png,直传也必须发 image/png。不一致时 COS 直接返回 403,框架会在日志中提示该原因。
数据结构
均位于 cn.org.bukkit.bngeuit.dto.message.media。
MediaImageFormat
图片格式枚举,getValue() 即请求参数 content_type:
| 枚举 | MIME | 允许的扩展名 |
|---|---|---|
PNG | image/png | .png |
JPEG | image/jpeg | .jpeg / .jpg |
GIF | image/gif | .gif |
WEBP | image/webp | .webp |
静态方法:
| 方法 | 说明 |
|---|---|
fromValue(mimeType) | MIME → 枚举(未知类型抛 IllegalArgumentException) |
fromFileName(fileName) | 按扩展名推断(无法识别返回 null) |
fromFile(file) | 按文件扩展名推断(无法识别返回 null) |
实例方法:
| 方法 | 说明 |
|---|---|
getValue() | 该格式的 MIME 字符串(请求参数 content_type) |
getExtensions() | 允许的扩展名数组 |
matchesExtension(ext) | 扩展名是否属于该格式(大小写不敏感,可带前导点) |
MediaImagePrepareResponse
| 字段 | 类型 | 说明 |
|---|---|---|
uploadId() | String | 上传任务 ID(4 小时内有效) |
presignedUrl() | String | 预签名上传 URL |
expiresIn() | long | 预签名 URL 有效期(秒) |
MediaImageCompleteResponse
| 字段 | 类型 | 说明 |
|---|---|---|
imageId() | String | 图片 ID,用于查询与删除 |
url() | String | 引用链接 qqbot-media://image/v1/<token> |
ttl() | long | 滑动有效期窗口(秒) |
previewUrl() | String | 短时效外网预览链接,仅供预览,勿持久化 |
MediaImage
| 字段 | 类型 | 说明 |
|---|---|---|
imageId() | String | 图片 ID |
url() | String | 引用链接 |
fileName() | String | 上传时的文件名 |
previewUrl() | String | 短时效预览链接 |
createdAt() | Timestamp | 上传时间 |
expiresAt() | Timestamp | 过期时间(被引用后顺延) |
MediaImageListResponse
| 字段 | 类型 | 说明 |
|---|---|---|
images() | List<MediaImage> | 图片列表 |
nextCursor() | String | 下一页游标(空串 = 末页) |
total() | long | 当前有效图片总数 |
事件
素材管理有 3 对前置 / 后置事件:
| 操作 | 前置(可取消) | 后置(成功后) |
|---|---|---|
| 申请上传 | MediaImagePrepareEvent | MediaImagePrepareNotifyEvent |
| 完成上传 | MediaImageCompleteEvent | MediaImageCompleteNotifyEvent |
| 删除素材 | MediaImageDeleteEvent | MediaImageDeleteNotifyEvent |
前置事件字段:
| 事件 | 字段 |
|---|---|
MediaImagePrepareEvent | getFileName() / getFileSize() / getFormat() / getMd5() / getSha1() |
MediaImageCompleteEvent | getUploadId() / getMd5() |
MediaImageDeleteEvent | getImageId() |
查询列表(getMediaImages)不触发事件;PUT 直传单独调用时也不触发事件——直传属于上传流程内部步骤,事件挂在 prepare 与 complete 两端。
java
@SubscribeEvent
public void onPrepare(MediaImagePrepareEvent e) {
if (e.getFileSize() > 5 * 1024 * 1024) {
e.setCancelled(true); // 超过 5MB 的素材一律拒绝
}
}