feat(wechat): add image generation delivery policy
Memind CI / Test, build, and release guards (pull_request) Successful in 2m45s
Memind CI / Test, build, and release guards (pull_request) Successful in 2m45s
This commit is contained in:
@@ -3,12 +3,15 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { fetch as undiciFetch } from 'undici';
|
||||
import sharp from 'sharp';
|
||||
import { buildPublicUrl, PUBLISH_ROOT_DIR, PUBLIC_ZONE_DIR } from './user-publish.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const DEFAULT_WECHAT_MEDIA_URL = 'https://api.weixin.qq.com/cgi-bin/media/get';
|
||||
const DEFAULT_WECHAT_MEDIA_UPLOAD_URL = 'https://api.weixin.qq.com/cgi-bin/media/upload';
|
||||
const DEFAULT_MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
||||
const DEFAULT_MAX_OUTBOUND_IMAGE_BYTES = 2 * 1024 * 1024;
|
||||
const DEFAULT_MAX_ATTACHMENT_BYTES = 30 * 1024 * 1024;
|
||||
const ALLOWED_IMAGE_MIME_TYPES = new Map([
|
||||
['image/jpeg', 'jpg'],
|
||||
@@ -96,6 +99,93 @@ export async function downloadTemporaryMedia(accessToken, mediaId, { wechatFetch
|
||||
return { buffer, contentType };
|
||||
}
|
||||
|
||||
async function normalizeWechatOutboundImage(buffer, maxBytes = DEFAULT_MAX_OUTBOUND_IMAGE_BYTES) {
|
||||
ensureImageWithinLimit(buffer, DEFAULT_MAX_IMAGE_BYTES * 2);
|
||||
const attempts = [
|
||||
{ width: 2048, quality: 86 },
|
||||
{ width: 1600, quality: 74 },
|
||||
{ width: 1280, quality: 62 },
|
||||
];
|
||||
for (const attempt of attempts) {
|
||||
const normalized = await sharp(buffer, { sequentialRead: true })
|
||||
.rotate()
|
||||
.resize({
|
||||
width: attempt.width,
|
||||
height: attempt.width,
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.flatten({ background: '#ffffff' })
|
||||
.jpeg({ quality: attempt.quality, progressive: true, mozjpeg: true })
|
||||
.toBuffer();
|
||||
if (normalized.length <= maxBytes) {
|
||||
return { buffer: normalized, contentType: 'image/jpeg', filename: 'memind-generated.jpg' };
|
||||
}
|
||||
}
|
||||
throw new Error(`生成图片压缩后仍超过微信图片限制(${maxBytes} bytes)`);
|
||||
}
|
||||
|
||||
export async function uploadWechatGeneratedImage(
|
||||
accessToken,
|
||||
publicUrl,
|
||||
{
|
||||
wechatFetch = undiciFetch,
|
||||
publicBaseUrl = '',
|
||||
uploadUrl = DEFAULT_WECHAT_MEDIA_UPLOAD_URL,
|
||||
maxBytes = DEFAULT_MAX_OUTBOUND_IMAGE_BYTES,
|
||||
} = {},
|
||||
) {
|
||||
if (!accessToken) throw new Error('缺少微信 access_token');
|
||||
if (!publicUrl) throw new Error('缺少生成图片公网地址');
|
||||
const resolvedUrl = new URL(String(publicUrl), publicBaseUrl || undefined).toString();
|
||||
if (publicBaseUrl && new URL(resolvedUrl).origin !== new URL(publicBaseUrl).origin) {
|
||||
throw new Error('生成图片地址不属于当前 MindSpace 公网域名');
|
||||
}
|
||||
const sourceResponse = await wechatFetch(resolvedUrl, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'image/*,*/*;q=0.8' },
|
||||
});
|
||||
if (!sourceResponse.ok) {
|
||||
const text = await sourceResponse.text().catch(() => '');
|
||||
throw new Error(text || `生成图片下载失败 (${sourceResponse.status})`);
|
||||
}
|
||||
const declaredBytes = Number(sourceResponse.headers.get('content-length') ?? 0);
|
||||
if (Number.isFinite(declaredBytes) && declaredBytes > DEFAULT_MAX_IMAGE_BYTES * 2) {
|
||||
throw new Error('生成图片下载体积超过安全限制');
|
||||
}
|
||||
const sourceBuffer = Buffer.from(await sourceResponse.arrayBuffer());
|
||||
const normalized = await normalizeWechatOutboundImage(sourceBuffer, maxBytes);
|
||||
const form = new FormData();
|
||||
form.append(
|
||||
'media',
|
||||
new Blob([normalized.buffer], { type: normalized.contentType }),
|
||||
normalized.filename,
|
||||
);
|
||||
const endpoint = new URL(uploadUrl);
|
||||
endpoint.searchParams.set('access_token', accessToken);
|
||||
endpoint.searchParams.set('type', 'image');
|
||||
const response = await wechatFetch(endpoint.toString(), {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
});
|
||||
const text = await response.text().catch(() => '');
|
||||
let payload = {};
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
payload = {};
|
||||
}
|
||||
if (!response.ok || Number(payload?.errcode ?? 0) !== 0 || !String(payload?.media_id ?? '').trim()) {
|
||||
const detail = String(payload?.errmsg ?? text ?? '').trim() || `HTTP ${response.status}`;
|
||||
throw new Error(`微信生成图片素材上传失败:${detail}`);
|
||||
}
|
||||
return {
|
||||
mediaId: String(payload.media_id),
|
||||
bytes: normalized.buffer.length,
|
||||
contentType: normalized.contentType,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWechatImagePublicUrl({
|
||||
publicBaseUrl,
|
||||
publishKey,
|
||||
|
||||
Reference in New Issue
Block a user