feat: add WeChat media analysis adapters
This commit is contained in:
@@ -9,12 +9,19 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const DEFAULT_WECHAT_MEDIA_URL = 'https://api.weixin.qq.com/cgi-bin/media/get';
|
||||
const DEFAULT_MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
||||
const DEFAULT_MAX_ATTACHMENT_BYTES = 30 * 1024 * 1024;
|
||||
const ALLOWED_IMAGE_MIME_TYPES = new Map([
|
||||
['image/jpeg', 'jpg'],
|
||||
['image/png', 'png'],
|
||||
['image/webp', 'webp'],
|
||||
['image/gif', 'gif'],
|
||||
]);
|
||||
const ALLOWED_ATTACHMENT_EXTENSIONS = new Map([
|
||||
['.doc', 'application/msword'],
|
||||
['.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
||||
['.xls', 'application/vnd.ms-excel'],
|
||||
['.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
||||
]);
|
||||
|
||||
function resolveImageExtension(contentType = '', fallbackUrl = '') {
|
||||
const normalized = String(contentType ?? '')
|
||||
@@ -44,6 +51,32 @@ function ensureImageWithinLimit(buffer, maxBytes) {
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeAttachmentFilename(filename = '') {
|
||||
const basename = path.basename(String(filename ?? '').trim()).replace(/[\u0000-\u001f\u007f]/g, '');
|
||||
if (!basename || basename === '.' || basename === '..') {
|
||||
throw new Error('微信文件缺少有效文件名');
|
||||
}
|
||||
const extension = path.extname(basename).toLowerCase();
|
||||
const mimeType = ALLOWED_ATTACHMENT_EXTENSIONS.get(extension);
|
||||
if (!mimeType) {
|
||||
throw new Error('当前服务号文件仅支持 Word(doc/docx)和 Excel(xls/xlsx)');
|
||||
}
|
||||
return {
|
||||
filename: basename.slice(0, 160),
|
||||
extension,
|
||||
mimeType,
|
||||
};
|
||||
}
|
||||
|
||||
function ensureAttachmentWithinLimit(buffer, maxBytes) {
|
||||
if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
|
||||
throw new Error('微信文件内容为空');
|
||||
}
|
||||
if (buffer.length > maxBytes) {
|
||||
throw new Error(`文件超过大小限制(${maxBytes} bytes)`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadTemporaryMedia(accessToken, mediaId, { wechatFetch = undiciFetch } = {}) {
|
||||
if (!accessToken) throw new Error('缺少微信 access_token');
|
||||
if (!mediaId) throw new Error('缺少微信 mediaId');
|
||||
@@ -154,3 +187,58 @@ export async function persistWechatImage(
|
||||
source,
|
||||
};
|
||||
}
|
||||
|
||||
export async function persistWechatAttachment(
|
||||
{
|
||||
userId,
|
||||
appId,
|
||||
openid,
|
||||
msgId,
|
||||
mediaId,
|
||||
filename,
|
||||
publicBaseUrl,
|
||||
maxFileBytes = DEFAULT_MAX_ATTACHMENT_BYTES,
|
||||
},
|
||||
{
|
||||
wechatFetch = undiciFetch,
|
||||
accessToken,
|
||||
h5Root = __dirname,
|
||||
} = {},
|
||||
) {
|
||||
if (!userId) throw new Error('缺少 userId');
|
||||
const resolved = sanitizeAttachmentFilename(filename);
|
||||
const downloaded = await downloadTemporaryMedia(accessToken, mediaId, { wechatFetch });
|
||||
ensureAttachmentWithinLimit(downloaded.buffer, maxFileBytes);
|
||||
|
||||
const publishDir = path.join(h5Root, PUBLISH_ROOT_DIR, String(userId), PUBLIC_ZONE_DIR, 'wechat-mp');
|
||||
fs.mkdirSync(publishDir, { recursive: true });
|
||||
|
||||
const timestamp = Date.now();
|
||||
const hash = crypto.createHash('sha1').update(downloaded.buffer).digest('hex').slice(0, 12);
|
||||
const originalStem = path.basename(resolved.filename, resolved.extension)
|
||||
.replace(/[^\p{L}\p{N}._-]+/gu, '_')
|
||||
.replace(/^_+|_+$/g, '')
|
||||
.slice(0, 80) || 'attachment';
|
||||
const identity = [appId || 'wx', openid || 'openid', msgId || timestamp, mediaId || hash]
|
||||
.filter(Boolean)
|
||||
.join('-')
|
||||
.replace(/[^a-zA-Z0-9._-]+/g, '_')
|
||||
.slice(0, 100);
|
||||
const publicFilename = `${originalStem}-${identity}-${hash}${resolved.extension}`;
|
||||
const absolutePath = path.join(publishDir, publicFilename);
|
||||
fs.writeFileSync(absolutePath, downloaded.buffer);
|
||||
|
||||
return {
|
||||
absolutePath,
|
||||
bytes: downloaded.buffer.length,
|
||||
contentType: resolved.mimeType,
|
||||
filename: resolved.filename,
|
||||
publicFilename,
|
||||
publicUrl: buildWechatImagePublicUrl({
|
||||
publicBaseUrl,
|
||||
publishKey: String(userId),
|
||||
filename: publicFilename,
|
||||
}),
|
||||
source: 'wechat_media',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user