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.
Files changed (57) hide show
  1. package/dist/cjs/src/matrix/messages/message-image.d.ts +8 -0
  2. package/dist/cjs/src/matrix/messages/message-image.d.ts.map +1 -1
  3. package/dist/cjs/src/matrix/messages/message-image.js +38 -2
  4. package/dist/cjs/src/matrix/messages/message-image.spec.d.ts +3 -0
  5. package/dist/cjs/src/matrix/messages/message-image.spec.d.ts.map +1 -0
  6. package/dist/cjs/src/matrix/messages/message-image.spec.js +66 -0
  7. package/dist/cjs/src/matrix/messages/message-video.d.ts +6 -0
  8. package/dist/cjs/src/matrix/messages/message-video.d.ts.map +1 -1
  9. package/dist/cjs/src/matrix/messages/message-video.js +20 -0
  10. package/dist/cjs/src/matrix/messages/message-video.spec.d.ts +3 -0
  11. package/dist/cjs/src/matrix/messages/message-video.spec.d.ts.map +1 -0
  12. package/dist/cjs/src/matrix/messages/message-video.spec.js +27 -0
  13. package/dist/cjs/src/matrix/service/request.d.ts +9 -0
  14. package/dist/cjs/src/matrix/service/request.d.ts.map +1 -1
  15. package/dist/cjs/src/matrix/service/request.js +56 -32
  16. package/dist/cjs/src/matrix/utils/file-type.d.ts +4 -0
  17. package/dist/cjs/src/matrix/utils/file-type.d.ts.map +1 -0
  18. package/dist/cjs/src/matrix/utils/file-type.js +55 -0
  19. package/dist/cjs/src/matrix/utils/file-type.spec.d.ts +3 -0
  20. package/dist/cjs/src/matrix/utils/file-type.spec.d.ts.map +1 -0
  21. package/dist/cjs/src/matrix/utils/file-type.spec.js +25 -0
  22. package/dist/cjs/src/puppet-matrix.d.ts +1 -0
  23. package/dist/cjs/src/puppet-matrix.d.ts.map +1 -1
  24. package/dist/cjs/src/puppet-matrix.js +16 -1
  25. package/dist/esm/src/matrix/messages/message-image.d.ts +8 -0
  26. package/dist/esm/src/matrix/messages/message-image.d.ts.map +1 -1
  27. package/dist/esm/src/matrix/messages/message-image.js +37 -2
  28. package/dist/esm/src/matrix/messages/message-image.spec.d.ts +3 -0
  29. package/dist/esm/src/matrix/messages/message-image.spec.d.ts.map +1 -0
  30. package/dist/esm/src/matrix/messages/message-image.spec.js +64 -0
  31. package/dist/esm/src/matrix/messages/message-video.d.ts +6 -0
  32. package/dist/esm/src/matrix/messages/message-video.d.ts.map +1 -1
  33. package/dist/esm/src/matrix/messages/message-video.js +19 -0
  34. package/dist/esm/src/matrix/messages/message-video.spec.d.ts +3 -0
  35. package/dist/esm/src/matrix/messages/message-video.spec.d.ts.map +1 -0
  36. package/dist/esm/src/matrix/messages/message-video.spec.js +25 -0
  37. package/dist/esm/src/matrix/service/request.d.ts +9 -0
  38. package/dist/esm/src/matrix/service/request.d.ts.map +1 -1
  39. package/dist/esm/src/matrix/service/request.js +55 -32
  40. package/dist/esm/src/matrix/utils/file-type.d.ts +4 -0
  41. package/dist/esm/src/matrix/utils/file-type.d.ts.map +1 -0
  42. package/dist/esm/src/matrix/utils/file-type.js +47 -0
  43. package/dist/esm/src/matrix/utils/file-type.spec.d.ts +3 -0
  44. package/dist/esm/src/matrix/utils/file-type.spec.d.ts.map +1 -0
  45. package/dist/esm/src/matrix/utils/file-type.spec.js +23 -0
  46. package/dist/esm/src/puppet-matrix.d.ts +1 -0
  47. package/dist/esm/src/puppet-matrix.d.ts.map +1 -1
  48. package/dist/esm/src/puppet-matrix.js +16 -1
  49. package/package.json +1 -1
  50. package/src/matrix/messages/message-image.spec.ts +107 -0
  51. package/src/matrix/messages/message-image.ts +63 -2
  52. package/src/matrix/messages/message-video.spec.ts +37 -0
  53. package/src/matrix/messages/message-video.ts +28 -1
  54. package/src/matrix/service/request.ts +85 -32
  55. package/src/matrix/utils/file-type.spec.ts +29 -0
  56. package/src/matrix/utils/file-type.ts +59 -0
  57. 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;AA6BhD,QAAA,MAAM,OAAO,QAAiC,CAAA;AAK9C,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,aAAa,GAAG;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,GAAG,CAAA;IACZ,kBAAkB,EAAE,MAAM,CAAA;CAC3B,CAAA;AAED,cAAM,YAAa,SAAQ,MAAM,CAAC,MAAM;IAoBT,OAAO,EAAE,mBAAmB;IAlBzD,OAAO,CAAC,SAAS,CAAC,CAAc;IAChC,OAAO,CAAC,OAAO,CAAC,CAAQ;IACxB,OAAO,CAAC,KAAK,CAAC,CAA4B;IAC1C,OAAO,CAAC,aAAa,CAA2B;IAChD,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,eAAe,CAAC,CAA+B;IACvD,OAAO,CAAC,eAAe,CAAC,CAAuC;IAC/D,OAAO,CAAC,eAAe,CAAC,CAAuC;IAC/D,OAAO,CAAC,sBAAsB,CAAC,CAAuC;IACtE,OAAO,CAAC,eAAe,CAAQ;IAC/B,OAAO,CAAC,aAAa,CAA6B;IAClD,gBAAgC,OAAO,SAAU;gBAOpB,OAAO,GAAE,mBAA+C;IAgBrF,IAAW,MAAM,uBAEhB;IAEc,OAAO,IAAK,OAAO,CAAC,IAAI,CAAC;YAS1B,YAAY;YAuHZ,YAAY;IAoB1B,OAAO,CAAC,eAAe;YAOT,UAAU;IA+DT,eAAe,CAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAS1D,aAAa;YA4Cb,OAAO;IAiCR,KAAK,IAAK,OAAO,CAAC,IAAI,CAAC;IAmBvB,MAAM,IAAK,OAAO,CAAC,IAAI,CAAC;YAIvB,WAAW;IAeV,MAAM,CAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU7C,IAAI,CAAE,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI;IAOpB,aAAa,IAAK,OAAO,CAAC,IAAI,CAAC;IAKjC,iBAAiB,IAAK,OAAO,CAAC,IAAI,CAAC;IAuBjC,eAAe,CAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7C,iBAAiB,IAAK,OAAO,CAAC,MAAM,CAAC;IAKrC,oBAAoB,CAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvD,YAAY,CAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1E,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAChD,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B9D,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAC3D,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAavE,WAAW,IAAK,OAAO,CAAC,MAAM,EAAE,CAAC;IAKjC,wBAAwB,CAAE,SAAS,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,GAAG,IAAI;IAK7E,kBAAkB,CAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAKjE,aAAa,CAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWhD,gBAAgB,CAAE,MAAM,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKxE,mBAAmB,CAAE,MAAM,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAK3E,YAAY,CAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjD,iBAAiB,CAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAWzD,gBAAgB,CAAE,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkBtD,aAAa,CAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,KAAK,CAAC,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC;IAoD5F,qBAAqB,CAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC;IAQ7D,sBAAsB,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC;YAU5D,iBAAiB;YAmBjB,kBAAkB;IAuBjB,cAAc,CAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAKnD,cAAc,CAC3B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;IA6BpB,WAAW,CAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA2C1D,YAAY,CAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAS3D,kBAAkB,CAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;IAe5E,UAAU,CAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;IAsBhE,eAAe,CAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAgBtE,kBAAkB,CAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;YAc/E,wBAAwB;IA2EvB,kBAAkB,CAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC;IA8BhF,eAAe,CAAE,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IA0C3F,sBAAsB,CAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC;IAwB3G,eAAe,CAAE,cAAc,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAoCxG,cAAc,CAAE,cAAc,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,QAAQ,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAsBxG,eAAe,CAAE,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAW1F,aAAa,CAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IASnD,cAAc,CAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAkCrE,OAAO,CAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY1D,UAAU,CAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAWtD,UAAU,CACvB,aAAa,EAAE,MAAM,EAAE,EACvB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC;IAKH,OAAO,CACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IAKD,QAAQ,IAAK,OAAO,CAAC,MAAM,EAAE,CAAC;IAK9B,UAAU,CAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAK5C,QAAQ,CAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIxC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC1C,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvD,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAC7C,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAWzD,cAAc,CAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAMlD,oBAAoB,CAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9D,uBAAuB,CAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;IAKnF,iBAAiB,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAuBnE,uBAAuB,CAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;IAQnF,iBAAiB,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAYhE,oBAAoB,CAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAYpD,oBAAoB,CAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;IAQ7E,cAAc,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAchE,oBAAoB,CAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;YAkBlF,qBAAqB;IAmBpB,0BAA0B,CAAE,UAAU,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;IAQ5F,wBAAwB,CAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAQjE,8BAA8B,CAAE,UAAU,EAAE,GAAG,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC;IAQzF,0BAA0B,CAAE,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;IAQxG,oBAAoB,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC;IAiBtE,WAAW,CAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,GAAG,MAAM,CAAC;IAiEnE,aAAa,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMzC,cAAc,CAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,MAAM,CAAC;IAKnE,kBAAkB,CAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;IAKxF,oBAAoB,CAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC;IAMnF,GAAG,CAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,UAAO,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAY1F,QAAQ;YAWA,kBAAkB;YAoClB,oBAAoB;YAwDpB,mBAAmB;YA+BnB,iBAAiB;IAYlB,WAAW,CAAE,MAAM,EAAE,MAAM;YAgB1B,cAAc;YAkBd,gBAAgB;YAqBhB,cAAc;YAyEd,gBAAgB;YAyBhB,eAAe;YAiCf,iBAAiB;IAa/B,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,eAAe;YAOT,uBAAuB;YASvB,uBAAuB;YASvB,uBAAuB;YAgBvB,sBAAsB;IAwBvB,WAAW,CAAE,IAAI,CAAC,EAAE,OAAO;CAQzC;AAED,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,CAAA;AAEhC,eAAe,YAAY,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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wechaty-puppet-matrix",
3
- "version": "0.0.54",
3
+ "version": "0.0.55",
4
4
  "description": "Puppet matrix for Wechaty",
5
5
  "type": "module",
6
6
  "typings": "./dist/esm/src/mod.d.ts",
@@ -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: schema.cdnmidimgurl || schema.tpurl || '',
49
- file_size: schema.length,
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>): 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 params = {
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
- const params = {
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 params = {
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
+ }