wechaty-puppet-matrix 0.0.54 → 0.0.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/matrix/messages/message-image.d.ts +8 -0
- package/dist/cjs/src/matrix/messages/message-image.d.ts.map +1 -1
- package/dist/cjs/src/matrix/messages/message-image.js +38 -2
- package/dist/cjs/src/matrix/messages/message-image.spec.d.ts +3 -0
- package/dist/cjs/src/matrix/messages/message-image.spec.d.ts.map +1 -0
- package/dist/cjs/src/matrix/messages/message-image.spec.js +66 -0
- package/dist/cjs/src/matrix/messages/message-video.d.ts +6 -0
- package/dist/cjs/src/matrix/messages/message-video.d.ts.map +1 -1
- package/dist/cjs/src/matrix/messages/message-video.js +20 -0
- package/dist/cjs/src/matrix/messages/message-video.spec.d.ts +3 -0
- package/dist/cjs/src/matrix/messages/message-video.spec.d.ts.map +1 -0
- package/dist/cjs/src/matrix/messages/message-video.spec.js +27 -0
- package/dist/cjs/src/matrix/service/request.d.ts +9 -0
- package/dist/cjs/src/matrix/service/request.d.ts.map +1 -1
- package/dist/cjs/src/matrix/service/request.js +56 -32
- package/dist/cjs/src/matrix/utils/file-type.d.ts +4 -0
- package/dist/cjs/src/matrix/utils/file-type.d.ts.map +1 -0
- package/dist/cjs/src/matrix/utils/file-type.js +55 -0
- package/dist/cjs/src/matrix/utils/file-type.spec.d.ts +3 -0
- package/dist/cjs/src/matrix/utils/file-type.spec.d.ts.map +1 -0
- package/dist/cjs/src/matrix/utils/file-type.spec.js +25 -0
- package/dist/cjs/src/puppet-matrix.d.ts +1 -0
- package/dist/cjs/src/puppet-matrix.d.ts.map +1 -1
- package/dist/cjs/src/puppet-matrix.js +16 -1
- package/dist/esm/src/matrix/messages/message-image.d.ts +8 -0
- package/dist/esm/src/matrix/messages/message-image.d.ts.map +1 -1
- package/dist/esm/src/matrix/messages/message-image.js +37 -2
- package/dist/esm/src/matrix/messages/message-image.spec.d.ts +3 -0
- package/dist/esm/src/matrix/messages/message-image.spec.d.ts.map +1 -0
- package/dist/esm/src/matrix/messages/message-image.spec.js +64 -0
- package/dist/esm/src/matrix/messages/message-video.d.ts +6 -0
- package/dist/esm/src/matrix/messages/message-video.d.ts.map +1 -1
- package/dist/esm/src/matrix/messages/message-video.js +19 -0
- package/dist/esm/src/matrix/messages/message-video.spec.d.ts +3 -0
- package/dist/esm/src/matrix/messages/message-video.spec.d.ts.map +1 -0
- package/dist/esm/src/matrix/messages/message-video.spec.js +25 -0
- package/dist/esm/src/matrix/service/request.d.ts +9 -0
- package/dist/esm/src/matrix/service/request.d.ts.map +1 -1
- package/dist/esm/src/matrix/service/request.js +55 -32
- package/dist/esm/src/matrix/utils/file-type.d.ts +4 -0
- package/dist/esm/src/matrix/utils/file-type.d.ts.map +1 -0
- package/dist/esm/src/matrix/utils/file-type.js +47 -0
- package/dist/esm/src/matrix/utils/file-type.spec.d.ts +3 -0
- package/dist/esm/src/matrix/utils/file-type.spec.d.ts.map +1 -0
- package/dist/esm/src/matrix/utils/file-type.spec.js +23 -0
- package/dist/esm/src/puppet-matrix.d.ts +1 -0
- package/dist/esm/src/puppet-matrix.d.ts.map +1 -1
- package/dist/esm/src/puppet-matrix.js +16 -1
- package/package.json +1 -1
- package/src/matrix/messages/message-image.spec.ts +107 -0
- package/src/matrix/messages/message-image.ts +63 -2
- package/src/matrix/messages/message-video.spec.ts +37 -0
- package/src/matrix/messages/message-video.ts +28 -1
- package/src/matrix/service/request.ts +85 -32
- package/src/matrix/utils/file-type.spec.ts +29 -0
- package/src/matrix/utils/file-type.ts +59 -0
- package/src/puppet-matrix.ts +20 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env -S node -r ts-node/register
|
|
2
|
+
import { test } from 'tstest';
|
|
3
|
+
import { detectImageExt, replaceExt } from './file-type.js';
|
|
4
|
+
const hex = (s) => Buffer.from(s, 'hex');
|
|
5
|
+
test('detectImageExt recognizes common image headers', async (t) => {
|
|
6
|
+
t.equal(detectImageExt(hex('ffd8fffe00104c61766335392e33372e')), 'jpg', 'jpeg');
|
|
7
|
+
t.equal(detectImageExt(hex('89504e470d0a1a0a0000000d49484452')), 'png', 'png');
|
|
8
|
+
t.equal(detectImageExt(Buffer.from('GIF89a......')), 'gif', 'gif');
|
|
9
|
+
t.equal(detectImageExt(Buffer.concat([Buffer.from('RIFF'), hex('00000000'), Buffer.from('WEBPVP8 ')])), 'webp', 'webp');
|
|
10
|
+
t.equal(detectImageExt(hex('424d000000000000')), 'bmp', 'bmp');
|
|
11
|
+
t.equal(detectImageExt(Buffer.concat([hex('00000018'), Buffer.from('ftypheic'), hex('00000000')])), 'heic', 'heic');
|
|
12
|
+
t.equal(detectImageExt(Buffer.concat([hex('0000001c'), Buffer.from('ftypavif'), hex('00000000')])), 'avif', 'avif');
|
|
13
|
+
});
|
|
14
|
+
test('detectImageExt returns undefined for unknown or short data', async (t) => {
|
|
15
|
+
t.equal(detectImageExt(hex('0000001c667479706d703432')), undefined, 'mp4 is not an image');
|
|
16
|
+
t.equal(detectImageExt(Buffer.from('<html>')), undefined, 'html');
|
|
17
|
+
t.equal(detectImageExt(Buffer.alloc(0)), undefined, 'empty');
|
|
18
|
+
});
|
|
19
|
+
test('replaceExt replaces or appends extension', async (t) => {
|
|
20
|
+
t.equal(replaceExt('微信图片_20261105234022.png', 'jpg'), '微信图片_20261105234022.jpg', 'replace');
|
|
21
|
+
t.equal(replaceExt('image', 'gif'), 'image.gif', 'append');
|
|
22
|
+
t.equal(replaceExt('.hidden', 'jpg'), '.hidden.jpg', 'dotfile');
|
|
23
|
+
});
|
|
@@ -126,6 +126,7 @@ declare class PuppetMatrix extends PUPPET.Puppet {
|
|
|
126
126
|
private _stopPuppetHeart;
|
|
127
127
|
private _fileBoxFromUrl;
|
|
128
128
|
private _getMessageImageFileBox;
|
|
129
|
+
private _imageNameByHead;
|
|
129
130
|
private _getMessageAudioFileBox;
|
|
130
131
|
private _getMessageVideoFileBox;
|
|
131
132
|
private _getMessageFileFileBox;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"puppet-matrix.d.ts","sourceRoot":"","sources":["../../../src/puppet-matrix.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,MAAM,sBAAsB,CAAA;AAC9C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AAKhD,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAGxE,OAAO,MAAM,MAAM,6BAA6B,CAAA;
|
|
1
|
+
{"version":3,"file":"puppet-matrix.d.ts","sourceRoot":"","sources":["../../../src/puppet-matrix.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ import { WechatMessageType } from './matrix/types.js';
|
|
|
5
5
|
import Client from './matrix/service/request.js';
|
|
6
6
|
import { getUnixTime } from 'date-fns';
|
|
7
7
|
import { mergeWithFallback } from './matrix/utils/index.js';
|
|
8
|
+
import { detectImageExt, fetchRemoteHead, replaceExt } from './matrix/utils/file-type.js';
|
|
8
9
|
import path from 'path';
|
|
9
10
|
import { CacheManager } from './matrix/cache-manager.js';
|
|
10
11
|
import { isIMContactId, isRoomId } from './matrix/utils/is-type.js';
|
|
@@ -1456,7 +1457,21 @@ class PuppetMatrix extends PUPPET.Puppet {
|
|
|
1456
1457
|
log.error(`Can not get file for message: ${messageId}`);
|
|
1457
1458
|
}
|
|
1458
1459
|
const url = imageInfo.is_ww ? await this._client?.downloadWWMedia(imageInfo, 1) : await this._client?.downloadImage(imageInfo);
|
|
1459
|
-
return this._fileBoxFromUrl(url, messageId);
|
|
1460
|
+
return this._fileBoxFromUrl(url, messageId, url ? await this._imageNameByHead(url) : undefined);
|
|
1461
|
+
}
|
|
1462
|
+
async _imageNameByHead(url) {
|
|
1463
|
+
const head = await fetchRemoteHead(url);
|
|
1464
|
+
const ext = head && detectImageExt(head);
|
|
1465
|
+
if (!ext) {
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
let rawName = 'image';
|
|
1469
|
+
try {
|
|
1470
|
+
rawName = decodeURIComponent(new URL(url).pathname.split('/').pop() || rawName);
|
|
1471
|
+
}
|
|
1472
|
+
catch {
|
|
1473
|
+
}
|
|
1474
|
+
return replaceExt(rawName, ext);
|
|
1460
1475
|
}
|
|
1461
1476
|
async _getMessageAudioFileBox(messageId, messagePayload) {
|
|
1462
1477
|
const audioInfo = await parseAudioMessagePayload(messagePayload);
|
package/package.json
CHANGED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env -S node -r ts-node/register
|
|
2
|
+
import { test } from 'tstest'
|
|
3
|
+
|
|
4
|
+
import type { MessagePayload } from '../../engine-schema.js'
|
|
5
|
+
import { parseImageMessagePayload } from './message-image.js'
|
|
6
|
+
|
|
7
|
+
const buildPayload = (imgAttrs: string): MessagePayload => ({
|
|
8
|
+
msg: `<?xml version="1.0"?>\n<msg>\n\t<img ${imgAttrs}>\n\t</img>\n</msg>\n`,
|
|
9
|
+
} as unknown as MessagePayload)
|
|
10
|
+
|
|
11
|
+
test('parseImageMessagePayload uses hevc_mid_size when downloading mid image', async t => {
|
|
12
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
13
|
+
'aeskey="key" cdnmidimgurl="mid-url" length="157626" md5="md5" hevc_mid_size="53274"',
|
|
14
|
+
))
|
|
15
|
+
|
|
16
|
+
t.equal(payload.file_id, 'mid-url', 'should use mid image url')
|
|
17
|
+
t.equal(payload.file_size, '53274', 'size should match the mid image, not the origin length')
|
|
18
|
+
t.equal(payload.is_ww, false, 'should not be ww image')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('parseImageMessagePayload prefers cdnmidimgurl_size', async t => {
|
|
22
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
23
|
+
'aeskey="key" cdnmidimgurl="mid-url" length="157626" hevc_mid_size="53274" cdnmidimgurl_size="60000"',
|
|
24
|
+
))
|
|
25
|
+
|
|
26
|
+
t.equal(payload.file_size, '60000', 'cdnmidimgurl_size should win')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('parseImageMessagePayload falls back to length for legacy xml', async t => {
|
|
30
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
31
|
+
'aeskey="key" cdnmidimgurl="mid-url" length="12345" hevc_mid_size="0"',
|
|
32
|
+
))
|
|
33
|
+
|
|
34
|
+
t.equal(payload.file_size, '12345', 'length should be used when no mid size is available')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('parseImageMessagePayload falls back to length when mid size attrs are absent', async t => {
|
|
38
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
39
|
+
'aeskey="key" cdnmidimgurl="mid-url" length="12345" md5="md5"',
|
|
40
|
+
))
|
|
41
|
+
|
|
42
|
+
t.equal(payload.file_size, '12345', 'length should be used when hevc_mid_size is missing')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('parseImageMessagePayload uses big image url when mid size is missing', async t => {
|
|
46
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
47
|
+
'aeskey="key" cdnmidimgurl="mid-url" cdnbigimgurl="big-url" length="157626"',
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
t.equal(payload.file_id, 'big-url', 'should switch to origin image url')
|
|
51
|
+
t.equal(payload.file_size, '157626', 'size should be origin length')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('parseImageMessagePayload keeps mid url for legacy xml with hdlength', async t => {
|
|
55
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
56
|
+
'aeskey="key" cdnmidimgurl="mid-url" cdnbigimgurl="big-url" length="12345" hdlength="99999"',
|
|
57
|
+
))
|
|
58
|
+
|
|
59
|
+
t.equal(payload.file_id, 'mid-url', 'legacy xml should still use mid url')
|
|
60
|
+
t.equal(payload.file_size, '12345', 'legacy length is mid image size')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('parseImageMessagePayload keeps length for ww image', async t => {
|
|
64
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
65
|
+
'aeskey="key" tpurl="tp-url" tpauthkey="auth" length="888" hevc_mid_size="111"',
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
t.equal(payload.is_ww, true, 'should be ww image')
|
|
69
|
+
t.equal(payload.file_id, 'tp-url', 'should use tpurl')
|
|
70
|
+
t.equal(payload.file_size, '888', 'ww image should keep length')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('parseImageMessagePayload builds fallback candidates for new xml', async t => {
|
|
74
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
75
|
+
'aeskey="key" cdnmidimgurl="mid-url" cdnbigimgurl="big-url" length="157626" hevc_mid_size="53274"',
|
|
76
|
+
))
|
|
77
|
+
|
|
78
|
+
t.same(payload.candidates, [
|
|
79
|
+
{ file_id: 'mid-url', file_size: '53274' },
|
|
80
|
+
{ file_id: 'big-url', file_size: '157626' },
|
|
81
|
+
{ file_id: 'mid-url', file_size: '157626' },
|
|
82
|
+
], 'should try mid size first, then origin, then legacy combination')
|
|
83
|
+
t.equal(payload.file_id, payload.candidates?.[0]?.file_id, 'file_id should be first candidate')
|
|
84
|
+
t.equal(payload.file_size, payload.candidates?.[0]?.file_size, 'file_size should be first candidate')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test('parseImageMessagePayload builds fallback candidates for legacy xml', async t => {
|
|
88
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
89
|
+
'aeskey="key" cdnmidimgurl="mid-url" cdnbigimgurl="big-url" length="12345" hdlength="99999"',
|
|
90
|
+
))
|
|
91
|
+
|
|
92
|
+
t.same(payload.candidates, [
|
|
93
|
+
{ file_id: 'mid-url', file_size: '12345' },
|
|
94
|
+
{ file_id: 'big-url', file_size: '99999' },
|
|
95
|
+
], 'legacy xml should try mid + length, then big + hdlength')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('parseImageMessagePayload dedupes candidates', async t => {
|
|
99
|
+
const payload = await parseImageMessagePayload(buildPayload(
|
|
100
|
+
'aeskey="key" cdnmidimgurl="mid-url" length="157626" hevc_mid_size="53274" cdnmidimgurl_size="53274"',
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
t.same(payload.candidates, [
|
|
104
|
+
{ file_id: 'mid-url', file_size: '53274' },
|
|
105
|
+
{ file_id: 'mid-url', file_size: '157626' },
|
|
106
|
+
], 'same url + size should only appear once')
|
|
107
|
+
})
|
|
@@ -20,6 +20,9 @@ export interface ImageXmlSchema {
|
|
|
20
20
|
length: string
|
|
21
21
|
cdnbigimgurl: string
|
|
22
22
|
hdlength: string
|
|
23
|
+
// 新版 XML 中 length 是原图大小,中图大小单独给出
|
|
24
|
+
hevc_mid_size?: string
|
|
25
|
+
cdnmidimgurl_size?: string
|
|
23
26
|
md5: string
|
|
24
27
|
tpurl?: string
|
|
25
28
|
tpauthkey?: string
|
|
@@ -28,6 +31,11 @@ export interface ImageXmlSchema {
|
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
export interface ImageFileCandidate {
|
|
35
|
+
file_id: string,
|
|
36
|
+
file_size: string,
|
|
37
|
+
}
|
|
38
|
+
|
|
31
39
|
export interface ImageMessagePayload {
|
|
32
40
|
aes_key: string,
|
|
33
41
|
auth_key: string,
|
|
@@ -35,18 +43,71 @@ export interface ImageMessagePayload {
|
|
|
35
43
|
file_size: string,
|
|
36
44
|
file_name: string,
|
|
37
45
|
is_ww: boolean,
|
|
46
|
+
/**
|
|
47
|
+
* 按优先级排列的 (file_id, file_size) 组合,第一项即 file_id/file_size。
|
|
48
|
+
* 下载失败时按顺序回落到下一项,避免在错误的 size 上反复重试。
|
|
49
|
+
*/
|
|
50
|
+
candidates?: ImageFileCandidate[],
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
export async function parseImageMessagePayload (rawPayload: MessagePayload): Promise<ImageMessagePayload> {
|
|
41
54
|
const imageXml: ImageXmlSchema = await xmlToJson(rawPayload.msg)
|
|
42
55
|
const schema = imageXml.msg.img.$
|
|
43
56
|
|
|
57
|
+
const candidates = buildImageCandidates(schema)
|
|
58
|
+
const primary = candidates[0] || { file_id: schema.cdnmidimgurl || schema.tpurl || '', file_size: schema.length }
|
|
59
|
+
|
|
44
60
|
return {
|
|
45
61
|
is_ww: !!schema.tpurl,
|
|
46
62
|
aes_key: schema.aeskey,
|
|
47
63
|
auth_key: schema.tpauthkey || '',
|
|
48
|
-
file_id:
|
|
49
|
-
file_size:
|
|
64
|
+
file_id: primary.file_id,
|
|
65
|
+
file_size: primary.file_size,
|
|
50
66
|
file_name: '',
|
|
67
|
+
candidates: candidates.length ? candidates : [ primary ],
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isPositiveSize = (size?: string): size is string => !!size && Number(size) > 0
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* file_id 和 file_size 必须指向同一张图,否则下载端按错误的 size 做 Range/断点会失败。
|
|
75
|
+
* 候选优先级:
|
|
76
|
+
* 1. 企业微信 (tpurl):保持原逻辑,只有一个候选,size 用 length
|
|
77
|
+
* 2. cdnmidimgurl + 中图大小 (cdnmidimgurl_size,其次 hevc_mid_size)
|
|
78
|
+
* 3. 旧版 XML (有 hdlength,length 是中图大小):cdnmidimgurl + length,其次 cdnbigimgurl + hdlength
|
|
79
|
+
* 新版 XML (无 hdlength,length 是原图大小):cdnbigimgurl + length,其次 cdnmidimgurl + length
|
|
80
|
+
*/
|
|
81
|
+
export function buildImageCandidates (schema: ImageXmlSchema['msg']['img']['$']): ImageFileCandidate[] {
|
|
82
|
+
if (schema.tpurl) {
|
|
83
|
+
return [ { file_id: schema.cdnmidimgurl || schema.tpurl, file_size: schema.length } ]
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const midUrl = schema.cdnmidimgurl
|
|
87
|
+
const bigUrl = schema.cdnbigimgurl
|
|
88
|
+
const list: Array<[string | undefined, string | undefined]> = [
|
|
89
|
+
[ midUrl, schema.cdnmidimgurl_size ],
|
|
90
|
+
[ midUrl, schema.hevc_mid_size ],
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
if (isPositiveSize(schema.hdlength)) {
|
|
94
|
+
list.push([ midUrl, schema.length ], [ bigUrl, schema.hdlength ])
|
|
95
|
+
} else {
|
|
96
|
+
list.push([ bigUrl, schema.length ], [ midUrl, schema.length ])
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const seen = new Set<string>()
|
|
100
|
+
const candidates: ImageFileCandidate[] = []
|
|
101
|
+
for (const [ fileId, fileSize ] of list) {
|
|
102
|
+
if (!fileId || !isPositiveSize(fileSize)) {
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
const key = `${fileId}|${fileSize}`
|
|
106
|
+
if (seen.has(key)) {
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
seen.add(key)
|
|
110
|
+
candidates.push({ file_id: fileId, file_size: fileSize })
|
|
51
111
|
}
|
|
112
|
+
return candidates
|
|
52
113
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env -S node -r ts-node/register
|
|
2
|
+
import { test } from 'tstest'
|
|
3
|
+
|
|
4
|
+
import type { MessagePayload } from '../../engine-schema.js'
|
|
5
|
+
import { parseVideoMessagePayload } from './message-video.js'
|
|
6
|
+
import { parseSizeMismatch } from '../service/request.js'
|
|
7
|
+
|
|
8
|
+
const buildPayload = (attrs: string): MessagePayload => ({
|
|
9
|
+
msg: `<?xml version="1.0"?>\n<msg>\n\t<videomsg ${attrs} />\n</msg>\n`,
|
|
10
|
+
} as unknown as MessagePayload)
|
|
11
|
+
|
|
12
|
+
test('parseVideoMessagePayload adds raw video as fallback candidate', async t => {
|
|
13
|
+
const payload = await parseVideoMessagePayload(buildPayload(
|
|
14
|
+
'aeskey="key" cdnvideourl="video-url" length="766838" playlength="5" rawlength="769445" cdnrawvideourl="raw-url" cdnrawvideoaeskey="raw-key"',
|
|
15
|
+
))
|
|
16
|
+
|
|
17
|
+
t.equal(payload.file_id, 'video-url', 'primary file_id unchanged')
|
|
18
|
+
t.equal(payload.file_size, '766838', 'primary file_size unchanged')
|
|
19
|
+
t.same(payload.candidates, [
|
|
20
|
+
{ file_id: 'video-url', file_size: '766838' },
|
|
21
|
+
{ file_id: 'raw-url', file_size: '769445', aes_key: 'raw-key' },
|
|
22
|
+
], 'raw video should be second candidate with its own aes key')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('parseVideoMessagePayload skips empty raw video', async t => {
|
|
26
|
+
const payload = await parseVideoMessagePayload(buildPayload(
|
|
27
|
+
'aeskey="key" cdnvideourl="video-url" length="766838" playlength="5" rawlength="0" cdnrawvideourl="" cdnrawvideoaeskey=""',
|
|
28
|
+
))
|
|
29
|
+
|
|
30
|
+
t.same(payload.candidates, [ { file_id: 'video-url', file_size: '766838' } ], 'only cdn video candidate')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('parseSizeMismatch extracts actual size from backend error', async t => {
|
|
34
|
+
t.equal(parseSizeMismatch('download plaintext size differs: expected 766838, got 1228708'), '1228708', 'should return actual size')
|
|
35
|
+
t.equal(parseSizeMismatch('timeout'), undefined, 'other errors return undefined')
|
|
36
|
+
t.equal(parseSizeMismatch(undefined), undefined, 'undefined returns undefined')
|
|
37
|
+
})
|
|
@@ -50,9 +50,17 @@ export interface VideoMessagePayload {
|
|
|
50
50
|
thumb_size: string,
|
|
51
51
|
thumb_width: string,
|
|
52
52
|
thumb_height: string,
|
|
53
|
-
file_name: ''
|
|
53
|
+
file_name: '',
|
|
54
|
+
/**
|
|
55
|
+
* 按优先级排列的下载候选,第一项即 file_id/file_size。
|
|
56
|
+
* 实测 XML 的 length 可能和 CDN 实际大小不一致 (expected 766838, got 1228708),
|
|
57
|
+
* 有原始视频时回落到 cdnrawvideourl + rawlength
|
|
58
|
+
*/
|
|
59
|
+
candidates?: Array<{ file_id: string, file_size: string, aes_key?: string }>,
|
|
54
60
|
}
|
|
55
61
|
|
|
62
|
+
const isPositiveSize = (size?: string): size is string => !!size && Number(size) > 0
|
|
63
|
+
|
|
56
64
|
export async function parseVideoMessagePayload (rawPayload: MessagePayload): Promise<VideoMessagePayload> {
|
|
57
65
|
const videoXml: VideoXmlSchema = await xmlToJson(rawPayload.msg)
|
|
58
66
|
const videoSchema = videoXml.msg.videomsg.$
|
|
@@ -70,5 +78,24 @@ export async function parseVideoMessagePayload (rawPayload: MessagePayload): Pro
|
|
|
70
78
|
thumb_width: videoSchema.cdnthumbwidth || videoSchema.tpthumbwidth || '',
|
|
71
79
|
thumb_height: videoSchema.cdnthumbheight || videoSchema.tpthumbheight || '',
|
|
72
80
|
file_name: '',
|
|
81
|
+
candidates: buildVideoCandidates(videoSchema),
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildVideoCandidates (schema: VideoXmlSchema['msg']['videomsg']['$']): VideoMessagePayload['candidates'] {
|
|
86
|
+
if (schema.tpvideourl) {
|
|
87
|
+
return undefined
|
|
88
|
+
}
|
|
89
|
+
const candidates: NonNullable<VideoMessagePayload['candidates']> = []
|
|
90
|
+
if (schema.cdnvideourl && isPositiveSize(schema.length)) {
|
|
91
|
+
candidates.push({ file_id: schema.cdnvideourl, file_size: schema.length })
|
|
92
|
+
}
|
|
93
|
+
if (schema.cdnrawvideourl && isPositiveSize(schema.rawlength)) {
|
|
94
|
+
candidates.push({
|
|
95
|
+
file_id: schema.cdnrawvideourl,
|
|
96
|
+
file_size: schema.rawlength,
|
|
97
|
+
aes_key: schema.cdnrawvideoaeskey || schema.aeskey,
|
|
98
|
+
})
|
|
73
99
|
}
|
|
100
|
+
return candidates.length ? candidates : undefined
|
|
74
101
|
}
|
|
@@ -285,12 +285,29 @@ export interface BaseEvent {
|
|
|
285
285
|
robotInfo?: ContactPayload;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
export interface CdnDownloadCandidate {
|
|
289
|
+
file_id: string;
|
|
290
|
+
file_size: string | number;
|
|
291
|
+
/** 与默认 aes_key 不同时才需要,例如原始视频 cdnrawvideoaeskey */
|
|
292
|
+
aes_key?: string;
|
|
293
|
+
}
|
|
294
|
+
|
|
288
295
|
export interface ImageMessagePayload {
|
|
289
296
|
aes_key: string;
|
|
290
297
|
auth_key: string;
|
|
291
298
|
file_id: string;
|
|
292
299
|
file_size?: string | number;
|
|
293
300
|
file_name?: string;
|
|
301
|
+
/** 图片下载的候选 (file_id, file_size),按顺序回落 */
|
|
302
|
+
candidates?: CdnDownloadCandidate[];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* 解析后端 "download plaintext size differs: expected X, got Y",返回实际大小 Y
|
|
307
|
+
*/
|
|
308
|
+
export function parseSizeMismatch (errmsg?: string): string | undefined {
|
|
309
|
+
const match = typeof errmsg === 'string' ? errmsg.match(/size differs: expected \d+, got (\d+)/) : null
|
|
310
|
+
return match && Number(match[1]) > 0 ? match[1] : undefined
|
|
294
311
|
}
|
|
295
312
|
|
|
296
313
|
export interface SnsImageMessagePayload {
|
|
@@ -336,6 +353,7 @@ export interface VideoMessagePayload {
|
|
|
336
353
|
thumb_width: string;
|
|
337
354
|
thumb_height: string;
|
|
338
355
|
file_name: '';
|
|
356
|
+
candidates?: CdnDownloadCandidate[];
|
|
339
357
|
}
|
|
340
358
|
|
|
341
359
|
export enum FriendshipType {
|
|
@@ -1917,8 +1935,7 @@ class Client extends EventEmitter {
|
|
|
1917
1935
|
})
|
|
1918
1936
|
}
|
|
1919
1937
|
|
|
1920
|
-
private async retryDownload (fn: () => Promise<any
|
|
1921
|
-
const maxRetries = 5
|
|
1938
|
+
private async retryDownload (fn: () => Promise<any>, maxRetries = 5): Promise<any> {
|
|
1922
1939
|
const delayMs = 5000
|
|
1923
1940
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1924
1941
|
try {
|
|
@@ -1927,6 +1944,10 @@ class Client extends EventEmitter {
|
|
|
1927
1944
|
return res
|
|
1928
1945
|
}
|
|
1929
1946
|
log.warn(PRE, 'download attempt %d/%d failed (no url in response): %s', attempt, maxRetries, JSON.stringify(res))
|
|
1947
|
+
// size 不匹配是确定性错误,重试同一个 size 没有意义,交给调用方换 size
|
|
1948
|
+
if (parseSizeMismatch(res?.errmsg)) {
|
|
1949
|
+
return res
|
|
1950
|
+
}
|
|
1930
1951
|
} catch (e) {
|
|
1931
1952
|
log.warn(PRE, 'download attempt %d/%d error: %s', attempt, maxRetries, e)
|
|
1932
1953
|
}
|
|
@@ -1937,21 +1958,66 @@ class Client extends EventEmitter {
|
|
|
1937
1958
|
return null
|
|
1938
1959
|
}
|
|
1939
1960
|
|
|
1961
|
+
/**
|
|
1962
|
+
* 按候选 (file_id, file_size) 依次下载 CDN 文件。
|
|
1963
|
+
* - 后端返回 "plaintext size differs: expected X, got Y" 时立即用 Y 重试同一个 file_id
|
|
1964
|
+
* - 其它失败按 retriesPerCandidate 重试后回落到下一个候选
|
|
1965
|
+
*/
|
|
1966
|
+
private async cdnDownloadWithFallback (
|
|
1967
|
+
label: string,
|
|
1968
|
+
baseParams: Record<string, any>,
|
|
1969
|
+
candidates: CdnDownloadCandidate[],
|
|
1970
|
+
): Promise<string> {
|
|
1971
|
+
const queue = [ ...candidates ]
|
|
1972
|
+
const tried = new Set<string>()
|
|
1973
|
+
// 只有一个候选时保持原来的 5 次重试;有多个候选时每个只试 2 次就回落
|
|
1974
|
+
const retriesPerCandidate = candidates.length > 1 ? 2 : 5
|
|
1975
|
+
|
|
1976
|
+
while (queue.length) {
|
|
1977
|
+
const candidate = queue.shift()!
|
|
1978
|
+
const key = `${candidate.file_id}|${candidate.file_size}|${candidate.aes_key || ''}`
|
|
1979
|
+
if (!candidate.file_id || tried.has(key)) {
|
|
1980
|
+
continue
|
|
1981
|
+
}
|
|
1982
|
+
tried.add(key)
|
|
1983
|
+
|
|
1984
|
+
const params = {
|
|
1985
|
+
...baseParams,
|
|
1986
|
+
file_id: candidate.file_id,
|
|
1987
|
+
file_size: candidate.file_size,
|
|
1988
|
+
...(candidate.aes_key ? { aes_key: candidate.aes_key } : {}),
|
|
1989
|
+
}
|
|
1990
|
+
const res = await this.retryDownload(() => this.postData({ path: '/cloud/cdn_download', data: params }), retriesPerCandidate)
|
|
1991
|
+
if (res?.url) {
|
|
1992
|
+
if (tried.size > 1) {
|
|
1993
|
+
log.info(PRE, '%s succeeded with fallback, file_size: %s', label, candidate.file_size)
|
|
1994
|
+
}
|
|
1995
|
+
return encodeURI(res.url) || ''
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
const actualSize = parseSizeMismatch(res?.errmsg)
|
|
1999
|
+
if (actualSize) {
|
|
2000
|
+
log.warn(PRE, '%s size mismatch (xml %s, actual %s), retry with actual size', label, candidate.file_size, actualSize)
|
|
2001
|
+
queue.unshift({ ...candidate, file_size: actualSize })
|
|
2002
|
+
continue
|
|
2003
|
+
}
|
|
2004
|
+
log.warn(PRE, '%s candidate failed, params: %s', label, JSON.stringify(params))
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
log.error(PRE, '%s failed for all candidates: %s', label, JSON.stringify(candidates))
|
|
2008
|
+
return ''
|
|
2009
|
+
}
|
|
2010
|
+
|
|
1940
2011
|
public async downloadImage (imageInfo: ImageMessagePayload): Promise<any> {
|
|
1941
|
-
const
|
|
2012
|
+
const candidates = imageInfo.candidates?.length
|
|
2013
|
+
? imageInfo.candidates
|
|
2014
|
+
: [ { file_id: imageInfo.file_id, file_size: imageInfo.file_size || '' } ]
|
|
2015
|
+
return this.cdnDownloadWithFallback('downloadImage', {
|
|
1942
2016
|
log_response: true,
|
|
1943
2017
|
file_type: 2,
|
|
1944
|
-
file_id: imageInfo.file_id,
|
|
1945
2018
|
aes_key: imageInfo.aes_key,
|
|
1946
|
-
file_size: imageInfo.file_size,
|
|
1947
2019
|
file_name: `/微信图片_${format(new Date(), 'yyyyMMddHHmmss')}.png`,
|
|
1948
|
-
}
|
|
1949
|
-
const res = await this.retryDownload(() => this.postData({ path: '/cloud/cdn_download', data: params }))
|
|
1950
|
-
if (res?.url) {
|
|
1951
|
-
return encodeURI(res.url) || ''
|
|
1952
|
-
}
|
|
1953
|
-
log.error(PRE, 'downloadImage failed after %d retries, params: %s', 5, JSON.stringify(params))
|
|
1954
|
-
return ''
|
|
2020
|
+
}, candidates)
|
|
1955
2021
|
}
|
|
1956
2022
|
|
|
1957
2023
|
public async downloadWWMedia (imageInfo: ImageMessagePayload, type: number): Promise<any> {
|
|
@@ -1981,20 +2047,12 @@ class Client extends EventEmitter {
|
|
|
1981
2047
|
* @param imageInfo
|
|
1982
2048
|
*/
|
|
1983
2049
|
public async downloadFile (imageInfo: ImageMessagePayload): Promise<any> {
|
|
1984
|
-
|
|
2050
|
+
return this.cdnDownloadWithFallback('downloadFile', {
|
|
1985
2051
|
log_response: true,
|
|
1986
2052
|
file_type: 5,
|
|
1987
|
-
file_id: imageInfo.file_id.replace('@cdn_', ''),
|
|
1988
2053
|
aes_key: imageInfo.aes_key,
|
|
1989
|
-
file_size: imageInfo.file_size,
|
|
1990
2054
|
file_name: '/' + sanitizeFilename(imageInfo.file_name),
|
|
1991
|
-
}
|
|
1992
|
-
const res = await this.retryDownload(() => this.postData({ path: '/cloud/cdn_download', data: params }))
|
|
1993
|
-
if (res?.url) {
|
|
1994
|
-
return encodeURI(res.url) || ''
|
|
1995
|
-
}
|
|
1996
|
-
log.error(PRE, 'downloadFile failed after %d retries, params: %s', 5, JSON.stringify(params))
|
|
1997
|
-
return ''
|
|
2055
|
+
}, [ { file_id: imageInfo.file_id.replace('@cdn_', ''), file_size: imageInfo.file_size || '' } ])
|
|
1998
2056
|
}
|
|
1999
2057
|
|
|
2000
2058
|
/**
|
|
@@ -2100,19 +2158,14 @@ class Client extends EventEmitter {
|
|
|
2100
2158
|
* @param imageInfo
|
|
2101
2159
|
*/
|
|
2102
2160
|
public async downloadVideo (videoInfo: VideoMessagePayload): Promise<any> {
|
|
2103
|
-
const
|
|
2161
|
+
const candidates = videoInfo.candidates?.length
|
|
2162
|
+
? videoInfo.candidates
|
|
2163
|
+
: [ { file_id: videoInfo.file_id, file_size: videoInfo.file_size } ]
|
|
2164
|
+
return this.cdnDownloadWithFallback('downloadVideo', {
|
|
2104
2165
|
file_type: 4,
|
|
2105
|
-
file_id: videoInfo.file_id,
|
|
2106
2166
|
aes_key: videoInfo.aes_key,
|
|
2107
|
-
file_size: videoInfo.file_size,
|
|
2108
2167
|
file_name: `/微信视频_${format(new Date(), 'yyyyMMddHHmmss')}.mp4`,
|
|
2109
|
-
}
|
|
2110
|
-
const res = await this.retryDownload(() => this.postData({ path: '/cloud/cdn_download', data: params }))
|
|
2111
|
-
if (res?.url) {
|
|
2112
|
-
return encodeURI(res.url) || ''
|
|
2113
|
-
}
|
|
2114
|
-
log.error(PRE, 'downloadVideo failed after %d retries, params: %s', 5, JSON.stringify(params))
|
|
2115
|
-
return ''
|
|
2168
|
+
}, candidates)
|
|
2116
2169
|
}
|
|
2117
2170
|
|
|
2118
2171
|
/**
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env -S node -r ts-node/register
|
|
2
|
+
import { test } from 'tstest'
|
|
3
|
+
|
|
4
|
+
import { detectImageExt, replaceExt } from './file-type.js'
|
|
5
|
+
|
|
6
|
+
const hex = (s: string) => Buffer.from(s, 'hex')
|
|
7
|
+
|
|
8
|
+
test('detectImageExt recognizes common image headers', async t => {
|
|
9
|
+
// 实测微信图片下载结果的文件头
|
|
10
|
+
t.equal(detectImageExt(hex('ffd8fffe00104c61766335392e33372e')), 'jpg', 'jpeg')
|
|
11
|
+
t.equal(detectImageExt(hex('89504e470d0a1a0a0000000d49484452')), 'png', 'png')
|
|
12
|
+
t.equal(detectImageExt(Buffer.from('GIF89a......')), 'gif', 'gif')
|
|
13
|
+
t.equal(detectImageExt(Buffer.concat([ Buffer.from('RIFF'), hex('00000000'), Buffer.from('WEBPVP8 ') ])), 'webp', 'webp')
|
|
14
|
+
t.equal(detectImageExt(hex('424d000000000000')), 'bmp', 'bmp')
|
|
15
|
+
t.equal(detectImageExt(Buffer.concat([ hex('00000018'), Buffer.from('ftypheic'), hex('00000000') ])), 'heic', 'heic')
|
|
16
|
+
t.equal(detectImageExt(Buffer.concat([ hex('0000001c'), Buffer.from('ftypavif'), hex('00000000') ])), 'avif', 'avif')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('detectImageExt returns undefined for unknown or short data', async t => {
|
|
20
|
+
t.equal(detectImageExt(hex('0000001c667479706d703432')), undefined, 'mp4 is not an image')
|
|
21
|
+
t.equal(detectImageExt(Buffer.from('<html>')), undefined, 'html')
|
|
22
|
+
t.equal(detectImageExt(Buffer.alloc(0)), undefined, 'empty')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('replaceExt replaces or appends extension', async t => {
|
|
26
|
+
t.equal(replaceExt('微信图片_20261105234022.png', 'jpg'), '微信图片_20261105234022.jpg', 'replace')
|
|
27
|
+
t.equal(replaceExt('image', 'gif'), 'image.gif', 'append')
|
|
28
|
+
t.equal(replaceExt('.hidden', 'jpg'), '.hidden.jpg', 'dotfile')
|
|
29
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 根据文件头 (magic bytes) 判断图片扩展名,无法识别时返回 undefined
|
|
5
|
+
*/
|
|
6
|
+
export function detectImageExt (head: Buffer): string | undefined {
|
|
7
|
+
if (head.length >= 3 && head[0] === 0xff && head[1] === 0xd8 && head[2] === 0xff) {
|
|
8
|
+
return 'jpg'
|
|
9
|
+
}
|
|
10
|
+
if (head.length >= 8 && head.subarray(0, 8).equals(Buffer.from([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a ]))) {
|
|
11
|
+
return 'png'
|
|
12
|
+
}
|
|
13
|
+
if (head.length >= 6 && [ 'GIF87a', 'GIF89a' ].includes(head.subarray(0, 6).toString('ascii'))) {
|
|
14
|
+
return 'gif'
|
|
15
|
+
}
|
|
16
|
+
if (head.length >= 12 && head.subarray(0, 4).toString('ascii') === 'RIFF' && head.subarray(8, 12).toString('ascii') === 'WEBP') {
|
|
17
|
+
return 'webp'
|
|
18
|
+
}
|
|
19
|
+
if (head.length >= 2 && head[0] === 0x42 && head[1] === 0x4d) {
|
|
20
|
+
return 'bmp'
|
|
21
|
+
}
|
|
22
|
+
if (head.length >= 12 && head.subarray(4, 8).toString('ascii') === 'ftyp') {
|
|
23
|
+
const brand = head.subarray(8, 12).toString('ascii')
|
|
24
|
+
if ([ 'heic', 'heix', 'hevc', 'hevx', 'mif1', 'msf1' ].includes(brand)) {
|
|
25
|
+
return 'heic'
|
|
26
|
+
}
|
|
27
|
+
if ([ 'avif', 'avis' ].includes(brand)) {
|
|
28
|
+
return 'avif'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 用 Range 请求读取远程文件开头若干字节,失败返回 undefined
|
|
36
|
+
*/
|
|
37
|
+
export async function fetchRemoteHead (url: string, bytes = 32, timeoutMs = 5000): Promise<Buffer | undefined> {
|
|
38
|
+
try {
|
|
39
|
+
const res = await axios.get<ArrayBuffer>(url, {
|
|
40
|
+
headers: { Range: `bytes=0-${bytes - 1}` },
|
|
41
|
+
responseType: 'arraybuffer',
|
|
42
|
+
timeout: timeoutMs,
|
|
43
|
+
// 服务端不支持 Range 时会返回整个文件,限制最大读取量
|
|
44
|
+
maxContentLength: 50 * 1024 * 1024,
|
|
45
|
+
})
|
|
46
|
+
return Buffer.from(res.data).subarray(0, bytes)
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 把文件名的扩展名替换成 ext
|
|
54
|
+
*/
|
|
55
|
+
export function replaceExt (fileName: string, ext: string): string {
|
|
56
|
+
const dot = fileName.lastIndexOf('.')
|
|
57
|
+
const base = dot > 0 ? fileName.slice(0, dot) : fileName
|
|
58
|
+
return `${base}.${ext}`
|
|
59
|
+
}
|