Browse Source

动态gif加载(发送未开发

枫林 1 month ago
parent
commit
bb9711eebc

+ 2 - 0
.gitignore

@@ -95,3 +95,5 @@ botQQ_screenshots/GroupEventData.json
 botQQ_screenshots/GroupWorldData.json
 botQQ_screenshots/bilibiliData.json
 botQQ_screenshots/bilibiliSpace.json
+
+/botQQ_Puppeteer

+ 1 - 1
botQQ_screenshots/GroupWorldData.json

@@ -1 +1 @@
-{"638236452":{"worldData":["雪推个小妹","狗群主","gqz","√群主","群主","qz","🐶","🐶柚子","苟柚子"],"createtime":1752389067048,"updatetime":1752404552990,"userData":[]},"":{"worldData":"","createtime":1752388877325,"updatetime":1753081335343}}
+{"638236452":{"worldData":["雪推个小妹","狗群主","gqz","√群主","群主","qz","🐶","🐶柚子","苟柚子"],"createtime":1752389067048,"updatetime":1752404552990,"userData":[]},"":{"worldData":"","createtime":1752388877325,"updatetime":1753324826899}}

BIN
botQQ_screenshots/screenshot_1753259081224.png


BIN
botQQ_screenshots/screenshot_1753275615193.png


+ 4 - 0
package.json

@@ -14,16 +14,20 @@
   "license": "ISC",
   "dependencies": {
     "@renmu/bili-api": "^2.7.0",
+    "@types/sharp": "^0.32.0",
     "art-template": "^4.13.2",
     "axios": "^1.7.8",
+    "fluent-ffmpeg": "^2.1.3",
     "jimp": "^1.6.0",
     "js-yaml": "^4.1.0",
     "minimist": "^1.2.8",
     "node-cron": "^3.0.3",
     "node-napcat-ts": "^0.4.0",
     "puppeteer": "^23.9.0",
+    "puppeteer-screen-recorder": "^3.0.6",
     "rcon-client": "^4.2.5",
     "reflect-metadata": "^0.2.2",
+    "sharp": "^0.34.3",
     "tesseract.js": "^6.0.1",
     "winston": "^3.17.0",
     "winston-daily-rotate-file": "^5.0.0",

+ 404 - 0
pnpm-lock.yaml

@@ -11,12 +11,18 @@ importers:
       '@renmu/bili-api':
         specifier: ^2.7.0
         version: 2.7.0
+      '@types/sharp':
+        specifier: ^0.32.0
+        version: 0.32.0
       art-template:
         specifier: ^4.13.2
         version: 4.13.4
       axios:
         specifier: ^1.7.8
         version: 1.9.0
+      fluent-ffmpeg:
+        specifier: ^2.1.3
+        version: 2.1.3
       jimp:
         specifier: ^1.6.0
         version: 1.6.0
@@ -35,12 +41,18 @@ importers:
       puppeteer:
         specifier: ^23.9.0
         version: 23.11.1(bufferutil@4.0.9)(typescript@4.9.5)(utf-8-validate@5.0.10)
+      puppeteer-screen-recorder:
+        specifier: ^3.0.6
+        version: 3.0.6(puppeteer@23.11.1(bufferutil@4.0.9)(typescript@4.9.5)(utf-8-validate@5.0.10))
       rcon-client:
         specifier: ^4.2.5
         version: 4.2.5
       reflect-metadata:
         specifier: ^0.2.2
         version: 0.2.2
+      sharp:
+        specifier: ^0.34.3
+        version: 0.34.3
       tesseract.js:
         specifier: ^6.0.1
         version: 6.0.1
@@ -106,6 +118,174 @@ packages:
   '@dabh/diagnostics@2.0.3':
     resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
 
+  '@emnapi/runtime@1.4.5':
+    resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
+
+  '@ffmpeg-installer/darwin-arm64@4.1.5':
+    resolution: {integrity: sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@ffmpeg-installer/darwin-x64@4.1.0':
+    resolution: {integrity: sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@ffmpeg-installer/ffmpeg@1.1.0':
+    resolution: {integrity: sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==}
+
+  '@ffmpeg-installer/linux-arm64@4.1.4':
+    resolution: {integrity: sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@ffmpeg-installer/linux-arm@4.1.3':
+    resolution: {integrity: sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==}
+    cpu: [arm]
+    os: [linux]
+
+  '@ffmpeg-installer/linux-ia32@4.1.0':
+    resolution: {integrity: sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==}
+    cpu: [ia32]
+    os: [linux]
+
+  '@ffmpeg-installer/linux-x64@4.1.0':
+    resolution: {integrity: sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==}
+    cpu: [x64]
+    os: [linux]
+
+  '@ffmpeg-installer/win32-ia32@4.1.0':
+    resolution: {integrity: sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==}
+    cpu: [ia32]
+    os: [win32]
+
+  '@ffmpeg-installer/win32-x64@4.1.0':
+    resolution: {integrity: sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==}
+    cpu: [x64]
+    os: [win32]
+
+  '@img/sharp-darwin-arm64@0.34.3':
+    resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@img/sharp-darwin-x64@0.34.3':
+    resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [darwin]
+
+  '@img/sharp-libvips-darwin-arm64@1.2.0':
+    resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@img/sharp-libvips-darwin-x64@1.2.0':
+    resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@img/sharp-libvips-linux-arm64@1.2.0':
+    resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@img/sharp-libvips-linux-arm@1.2.0':
+    resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==}
+    cpu: [arm]
+    os: [linux]
+
+  '@img/sharp-libvips-linux-ppc64@1.2.0':
+    resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@img/sharp-libvips-linux-s390x@1.2.0':
+    resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==}
+    cpu: [s390x]
+    os: [linux]
+
+  '@img/sharp-libvips-linux-x64@1.2.0':
+    resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==}
+    cpu: [x64]
+    os: [linux]
+
+  '@img/sharp-libvips-linuxmusl-arm64@1.2.0':
+    resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@img/sharp-libvips-linuxmusl-x64@1.2.0':
+    resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==}
+    cpu: [x64]
+    os: [linux]
+
+  '@img/sharp-linux-arm64@0.34.3':
+    resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [linux]
+
+  '@img/sharp-linux-arm@0.34.3':
+    resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm]
+    os: [linux]
+
+  '@img/sharp-linux-ppc64@0.34.3':
+    resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@img/sharp-linux-s390x@0.34.3':
+    resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [s390x]
+    os: [linux]
+
+  '@img/sharp-linux-x64@0.34.3':
+    resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [linux]
+
+  '@img/sharp-linuxmusl-arm64@0.34.3':
+    resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [linux]
+
+  '@img/sharp-linuxmusl-x64@0.34.3':
+    resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [linux]
+
+  '@img/sharp-wasm32@0.34.3':
+    resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [wasm32]
+
+  '@img/sharp-win32-arm64@0.34.3':
+    resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [win32]
+
+  '@img/sharp-win32-ia32@0.34.3':
+    resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [ia32]
+    os: [win32]
+
+  '@img/sharp-win32-x64@0.34.3':
+    resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [win32]
+
   '@jimp/core@1.6.0':
     resolution: {integrity: sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w==}
     engines: {node: '>=18'}
@@ -274,6 +454,10 @@ packages:
   '@types/node@16.9.1':
     resolution: {integrity: sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==}
 
+  '@types/sharp@0.32.0':
+    resolution: {integrity: sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==}
+    deprecated: This is a stub types definition. sharp provides its own type definitions, so you do not need this installed.
+
   '@types/triple-beam@1.3.5':
     resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
 
@@ -335,6 +519,9 @@ packages:
     resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==}
     engines: {node: '>=4'}
 
+  async@0.2.10:
+    resolution: {integrity: sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==}
+
   async@3.2.6:
     resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
 
@@ -473,6 +660,10 @@ packages:
   color@3.2.1:
     resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==}
 
+  color@4.2.3:
+    resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
+    engines: {node: '>=12.5.0'}
+
   colorspace@1.1.4:
     resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==}
 
@@ -533,6 +724,10 @@ packages:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
 
+  detect-libc@2.0.4:
+    resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
+    engines: {node: '>=8'}
+
   devtools-protocol@0.0.1367902:
     resolution: {integrity: sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==}
 
@@ -653,6 +848,11 @@ packages:
     resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
     engines: {node: '>=8'}
 
+  fluent-ffmpeg@2.1.3:
+    resolution: {integrity: sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==}
+    engines: {node: '>=18'}
+    deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
+
   fn.name@1.1.0:
     resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==}
 
@@ -805,6 +1005,9 @@ packages:
   is-url@1.2.4:
     resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
 
+  isexe@2.0.0:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
   isomorphic-ws@5.0.0:
     resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==}
     peerDependencies:
@@ -1056,6 +1259,12 @@ packages:
     resolution: {integrity: sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==}
     engines: {node: '>=18'}
 
+  puppeteer-screen-recorder@3.0.6:
+    resolution: {integrity: sha512-yzWlXWGi/FjsAe0fVr/zYlKFKsH1PGc6Pm7t58wlzVbs/jdLimjdO5VaTIqDJIkWuXFfD4WuDFptdU6AG3ls/Q==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      puppeteer: 19.0.0
+
   puppeteer@23.11.1:
     resolution: {integrity: sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==}
     engines: {node: '>=18'}
@@ -1113,6 +1322,10 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
+  sharp@0.34.3:
+    resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+
   simple-swizzle@0.2.2:
     resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
 
@@ -1304,6 +1517,10 @@ packages:
   whatwg-url@5.0.0:
     resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
 
+  which@1.3.1:
+    resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
+    hasBin: true
+
   winston-daily-rotate-file@5.0.0:
     resolution: {integrity: sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==}
     engines: {node: '>=8'}
@@ -1406,6 +1623,133 @@ snapshots:
       enabled: 2.0.0
       kuler: 2.0.0
 
+  '@emnapi/runtime@1.4.5':
+    dependencies:
+      tslib: 2.8.1
+    optional: true
+
+  '@ffmpeg-installer/darwin-arm64@4.1.5':
+    optional: true
+
+  '@ffmpeg-installer/darwin-x64@4.1.0':
+    optional: true
+
+  '@ffmpeg-installer/ffmpeg@1.1.0':
+    optionalDependencies:
+      '@ffmpeg-installer/darwin-arm64': 4.1.5
+      '@ffmpeg-installer/darwin-x64': 4.1.0
+      '@ffmpeg-installer/linux-arm': 4.1.3
+      '@ffmpeg-installer/linux-arm64': 4.1.4
+      '@ffmpeg-installer/linux-ia32': 4.1.0
+      '@ffmpeg-installer/linux-x64': 4.1.0
+      '@ffmpeg-installer/win32-ia32': 4.1.0
+      '@ffmpeg-installer/win32-x64': 4.1.0
+    optional: true
+
+  '@ffmpeg-installer/linux-arm64@4.1.4':
+    optional: true
+
+  '@ffmpeg-installer/linux-arm@4.1.3':
+    optional: true
+
+  '@ffmpeg-installer/linux-ia32@4.1.0':
+    optional: true
+
+  '@ffmpeg-installer/linux-x64@4.1.0':
+    optional: true
+
+  '@ffmpeg-installer/win32-ia32@4.1.0':
+    optional: true
+
+  '@ffmpeg-installer/win32-x64@4.1.0':
+    optional: true
+
+  '@img/sharp-darwin-arm64@0.34.3':
+    optionalDependencies:
+      '@img/sharp-libvips-darwin-arm64': 1.2.0
+    optional: true
+
+  '@img/sharp-darwin-x64@0.34.3':
+    optionalDependencies:
+      '@img/sharp-libvips-darwin-x64': 1.2.0
+    optional: true
+
+  '@img/sharp-libvips-darwin-arm64@1.2.0':
+    optional: true
+
+  '@img/sharp-libvips-darwin-x64@1.2.0':
+    optional: true
+
+  '@img/sharp-libvips-linux-arm64@1.2.0':
+    optional: true
+
+  '@img/sharp-libvips-linux-arm@1.2.0':
+    optional: true
+
+  '@img/sharp-libvips-linux-ppc64@1.2.0':
+    optional: true
+
+  '@img/sharp-libvips-linux-s390x@1.2.0':
+    optional: true
+
+  '@img/sharp-libvips-linux-x64@1.2.0':
+    optional: true
+
+  '@img/sharp-libvips-linuxmusl-arm64@1.2.0':
+    optional: true
+
+  '@img/sharp-libvips-linuxmusl-x64@1.2.0':
+    optional: true
+
+  '@img/sharp-linux-arm64@0.34.3':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-arm64': 1.2.0
+    optional: true
+
+  '@img/sharp-linux-arm@0.34.3':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-arm': 1.2.0
+    optional: true
+
+  '@img/sharp-linux-ppc64@0.34.3':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-ppc64': 1.2.0
+    optional: true
+
+  '@img/sharp-linux-s390x@0.34.3':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-s390x': 1.2.0
+    optional: true
+
+  '@img/sharp-linux-x64@0.34.3':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-x64': 1.2.0
+    optional: true
+
+  '@img/sharp-linuxmusl-arm64@0.34.3':
+    optionalDependencies:
+      '@img/sharp-libvips-linuxmusl-arm64': 1.2.0
+    optional: true
+
+  '@img/sharp-linuxmusl-x64@0.34.3':
+    optionalDependencies:
+      '@img/sharp-libvips-linuxmusl-x64': 1.2.0
+    optional: true
+
+  '@img/sharp-wasm32@0.34.3':
+    dependencies:
+      '@emnapi/runtime': 1.4.5
+    optional: true
+
+  '@img/sharp-win32-arm64@0.34.3':
+    optional: true
+
+  '@img/sharp-win32-ia32@0.34.3':
+    optional: true
+
+  '@img/sharp-win32-x64@0.34.3':
+    optional: true
+
   '@jimp/core@1.6.0':
     dependencies:
       '@jimp/file-ops': 1.6.0
@@ -1659,6 +2003,10 @@ snapshots:
 
   '@types/node@16.9.1': {}
 
+  '@types/sharp@0.32.0':
+    dependencies:
+      sharp: 0.34.3
+
   '@types/triple-beam@1.3.5': {}
 
   '@types/winston@2.4.4':
@@ -1716,6 +2064,8 @@ snapshots:
     dependencies:
       tslib: 2.8.1
 
+  async@0.2.10: {}
+
   async@3.2.6: {}
 
   asynckit@0.4.0: {}
@@ -1862,6 +2212,11 @@ snapshots:
       color-convert: 1.9.3
       color-string: 1.9.1
 
+  color@4.2.3:
+    dependencies:
+      color-convert: 2.0.1
+      color-string: 1.9.1
+
   colorspace@1.1.4:
     dependencies:
       color: 3.2.1
@@ -1910,6 +2265,8 @@ snapshots:
 
   delayed-stream@1.0.0: {}
 
+  detect-libc@2.0.4: {}
+
   devtools-protocol@0.0.1367902: {}
 
   diff@4.0.2: {}
@@ -2022,6 +2379,11 @@ snapshots:
     dependencies:
       to-regex-range: 5.0.1
 
+  fluent-ffmpeg@2.1.3:
+    dependencies:
+      async: 0.2.10
+      which: 1.3.1
+
   fn.name@1.1.0: {}
 
   follow-redirects@1.15.9: {}
@@ -2167,6 +2529,8 @@ snapshots:
 
   is-url@1.2.4: {}
 
+  isexe@2.0.0: {}
+
   isomorphic-ws@5.0.0(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)):
     dependencies:
       ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)
@@ -2444,6 +2808,13 @@ snapshots:
       - supports-color
       - utf-8-validate
 
+  puppeteer-screen-recorder@3.0.6(puppeteer@23.11.1(bufferutil@4.0.9)(typescript@4.9.5)(utf-8-validate@5.0.10)):
+    dependencies:
+      fluent-ffmpeg: 2.1.3
+      puppeteer: 23.11.1(bufferutil@4.0.9)(typescript@4.9.5)(utf-8-validate@5.0.10)
+    optionalDependencies:
+      '@ffmpeg-installer/ffmpeg': 1.1.0
+
   puppeteer@23.11.1(bufferutil@4.0.9)(typescript@4.9.5)(utf-8-validate@5.0.10):
     dependencies:
       '@puppeteer/browsers': 2.6.1
@@ -2503,6 +2874,35 @@ snapshots:
 
   semver@7.7.2: {}
 
+  sharp@0.34.3:
+    dependencies:
+      color: 4.2.3
+      detect-libc: 2.0.4
+      semver: 7.7.2
+    optionalDependencies:
+      '@img/sharp-darwin-arm64': 0.34.3
+      '@img/sharp-darwin-x64': 0.34.3
+      '@img/sharp-libvips-darwin-arm64': 1.2.0
+      '@img/sharp-libvips-darwin-x64': 1.2.0
+      '@img/sharp-libvips-linux-arm': 1.2.0
+      '@img/sharp-libvips-linux-arm64': 1.2.0
+      '@img/sharp-libvips-linux-ppc64': 1.2.0
+      '@img/sharp-libvips-linux-s390x': 1.2.0
+      '@img/sharp-libvips-linux-x64': 1.2.0
+      '@img/sharp-libvips-linuxmusl-arm64': 1.2.0
+      '@img/sharp-libvips-linuxmusl-x64': 1.2.0
+      '@img/sharp-linux-arm': 0.34.3
+      '@img/sharp-linux-arm64': 0.34.3
+      '@img/sharp-linux-ppc64': 0.34.3
+      '@img/sharp-linux-s390x': 0.34.3
+      '@img/sharp-linux-x64': 0.34.3
+      '@img/sharp-linuxmusl-arm64': 0.34.3
+      '@img/sharp-linuxmusl-x64': 0.34.3
+      '@img/sharp-wasm32': 0.34.3
+      '@img/sharp-win32-arm64': 0.34.3
+      '@img/sharp-win32-ia32': 0.34.3
+      '@img/sharp-win32-x64': 0.34.3
+
   simple-swizzle@0.2.2:
     dependencies:
       is-arrayish: 0.3.2
@@ -2709,6 +3109,10 @@ snapshots:
       tr46: 0.0.3
       webidl-conversions: 3.0.1
 
+  which@1.3.1:
+    dependencies:
+      isexe: 2.0.0
+
   winston-daily-rotate-file@5.0.0(winston@3.17.0):
     dependencies:
       file-stream-rotator: 0.6.1

+ 12 - 2
src/lib/Plugins.ts

@@ -14,7 +14,7 @@ import * as fs from 'fs'
 import * as path from 'path'
 // 获取指令前缀
 import { Botconfig as config, economy, load, PermissionConfig, saveConfig } from './config.js'
-import { ImageSegment, Receive, ReplySegment, TextSegment } from "node-napcat-ts/dist/Structs.js";
+import { FileSegment, ImageSegment, Receive, ReplySegment, TextSegment } from "node-napcat-ts/dist/Structs.js";
 import { fileURLToPath } from 'node:url';
 import { qqBot } from "../app.js";
 import { IsPermission } from "./Permission.js";
@@ -40,6 +40,15 @@ function createTextMessage(text: string): TextSegment {
 }
 
 function createImageMessage(base64Data: string): ImageSegment {
+    //动画表情
+    if(base64Data.startsWith('data:image/gif;base64,')){
+        return {
+            type: "image",
+            data: {
+                file: `base64://${base64Data}`,
+            }
+        };
+    }
     return {
         type: "image",
         data: {
@@ -394,6 +403,7 @@ async function handleCommand(context: PrivateFriendMessage | PrivateGroupMessage
                 const templateHtml = result.template.html;
                 const templatePath = result.template.path;
                 const url= result.template.render.url;
+                const isgif = result.template.render?.isgif || false;
                 
                 if (templateHtml) {
                     templateIsPath = false;
@@ -411,6 +421,7 @@ async function handleCommand(context: PrivateFriendMessage | PrivateGroupMessage
                         templateIsPath,
                         data: result,
                         url: url,
+                        isgif: isgif,
                         width: result.template.render?.width || 800,
                         height: result.template.render?.height || 600,
                         type: result.template.render?.type || 'png',
@@ -454,7 +465,6 @@ async function handleCommand(context: PrivateFriendMessage | PrivateGroupMessage
                             });
                         }
                     }
-
                 } finally {
                     await htmlImg.close();
                 }

+ 37 - 8
src/lib/Puppeteer.ts

@@ -2,6 +2,11 @@ import puppeteer, { Browser, PuppeteerLaunchOptions } from 'puppeteer';
 import art from 'art-template';
 import * as fs from 'fs';
 import botlogger from './logger.js';
+import { PuppeteerScreenRecorder } from 'puppeteer-screen-recorder';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { execSync } from 'child_process';
+
 
 export class HtmlImg {
     private browser: Browser | null = null;
@@ -25,6 +30,7 @@ export class HtmlImg {
         template: string;
         templateIsPath?: boolean;
         data: any;
+        isgif?: boolean;
         width?: number;
         height?: number;
         type?: string;
@@ -36,6 +42,7 @@ export class HtmlImg {
             await this.init();
             const {
                 url,
+                isgif,
                 template,
                 templateIsPath = true,
                 data,
@@ -64,7 +71,7 @@ export class HtmlImg {
             if (url) {
                 await page.goto(url);
                 await page.emulate({
-                    viewport: { width: 375, height: 667 },
+                    viewport: { width: 1080, height: 667 },
                     userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1'
                 });
                 await page.waitForNetworkIdle();
@@ -90,14 +97,36 @@ export class HtmlImg {
                 await page.setViewport({ width: bodywidth, height: bodyheight });
                 await page.waitForNetworkIdle();
             }
+            let image ;
+            if (isgif) {
+                //puppeteer
+                const recorder = new PuppeteerScreenRecorder(page,{
+                    followNewTab: true,
+                    fps: 15,
+                });
+                //base64
+                const __dirname = path.dirname(fileURLToPath(import.meta.url));
+                const tempDir = path.resolve(__dirname, '..', '..', 'botQQ_Puppeteer',`${Date.now()}.mp4`);
+                await recorder.start(tempDir);
+                await new Promise(resolve => setTimeout(resolve, 3 * 1000));
+                await recorder.stop();
+                const ffmpegPath = '/Users/fenglin/Desktop/botQQ/ffmpeg/ffmpeg'
+                execSync(`${ffmpegPath} -i ${tempDir} -vf "fps=15,scale=320:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" -y ${tempDir}.gif`);
+                botlogger.info(`转换为gif成功!`)
+                image = fs.readFileSync(`${tempDir}.gif`,{
+                    encoding: 'base64'
+                });
+                fs.unlinkSync(tempDir);
+                fs.unlinkSync(`${tempDir}.gif`);
+            }else{
+                image = await page.screenshot({
+                    type: type as 'png' | 'jpeg',
+                    quality: type === 'jpeg' ? quality : undefined,
+                    fullPage,
+                    omitBackground: !background
+                });
+            }
             // 截图
-            const image = await page.screenshot({
-                type: type as 'png' | 'jpeg',
-                quality: type === 'jpeg' ? quality : undefined,
-                fullPage,
-                omitBackground: !background
-            });
-
             await page.close();
             return image;
 

+ 1 - 0
src/plugins/test.ts

@@ -74,6 +74,7 @@ export class test {
                 path: path.resolve(__dirname, '..', 'resources', 'test', 'param.html'),//模版路径,推荐按规范放置在resources目录下
                 // html: `<div>简约自定义html渲染内容</div>`,//简易渲染,填写html内容
                 render: {//浏览器默认参数设置,用于打开浏览器的设置
+                    isgif: true,
                     width: 600, // 模板宽度
                     height: 1, // 模板高度
                     type: 'png',// 模板类型

+ 223 - 119
src/plugins/wiki.ts

@@ -9,8 +9,7 @@ import { createWorker } from 'tesseract.js';
 import { fileURLToPath } from 'url';
 import { PSM } from 'tesseract.js';
 import { Jimp, loadFont } from 'jimp'; // 假设未引入,添加引入语句
-import { Type } from 'js-yaml';
-
+import sharp from 'sharp';
 
 @plugins({
     easycmd: true,
@@ -73,7 +72,7 @@ export class wiki {
         }
     }
 
-    @runcod(['方舟智能点击','智能点击'], `会自动识别文字,安卓模拟器的操作方舟点击操作,优先使用创建的点击步骤`)
+    @runcod(['方舟智能点击', '智能点击'], `会自动识别文字,安卓模拟器的操作方舟点击操作,优先使用创建的点击步骤`)
     async rig(
         @param("点击文本", 'text') targetText: Receive["text"],
         @param("x偏移", 'text', { type: 'text', data: { text: '' } }, true) x1: Receive["text"],
@@ -97,15 +96,15 @@ export class wiki {
             const readxy = await this.readxy(targetText?.data?.text)
             let x = 0
             let y = 0
-            let img ='' 
-            let txst =''
+            let img = ''
+            let txst = ''
             if (readxy) {
                 x = readxy.x1;
                 y = readxy.y1;
                 txst = '记录结果'
                 img = buffer.toString('base64');
-            }else{
-                const data  = await this.recognizeTextPosition(tempFile, targetText.data.text);
+            } else {
+                const data = await this.recognizeTextPosition(tempFile, targetText.data.text);
                 x = data.x
                 y = data.y
                 txst = data.txst
@@ -134,12 +133,12 @@ export class wiki {
                     data: {
                         text: `识别结果:x:${x},y:${y}(${txst})`
                     },
-                },{
+                }, {
                     type: 'image',
                     data: {
                         file: `base64://${img}`
                     }
-                },{
+                }, {
                     type: 'image',
                     data: {
                         file: `base64://${base64Data}`
@@ -166,13 +165,13 @@ export class wiki {
             }
         }
     }
-    @runcod(['方舟滑动','滑动'], `滑动屏幕`)
+    @runcod(['方舟滑动', '滑动'], `滑动屏幕`)
     async huadong(
         @param("name", 'text') name: Receive["text"],
-        @param("type", 'text', { type: 'text', data: { text: '' } },true) type: Receive["text"],
-        @param("距离", 'text', { type: 'text', data: { text: '' } },true) distance: Receive["text"],
-        @param("X", 'text', { type: 'text', data: { text: '' } },true) x1: Receive["text"],
-        @param("y", 'text', { type: 'text', data: { text: '' } },true) y1: Receive["text"],
+        @param("type", 'text', { type: 'text', data: { text: '' } }, true) type: Receive["text"],
+        @param("距离", 'text', { type: 'text', data: { text: '' } }, true) distance: Receive["text"],
+        @param("X", 'text', { type: 'text', data: { text: '' } }, true) x1: Receive["text"],
+        @param("y", 'text', { type: 'text', data: { text: '' } }, true) y1: Receive["text"],
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
     ) {
         if (!name?.data?.text) { return; }
@@ -279,7 +278,7 @@ export class wiki {
                 data: {
                     file: `base64://${base64Data}`
                 }
-        }])
+            }])
         if (fs.existsSync(tempFile)) {
             fs.unlinkSync(tempFile);
         }
@@ -288,11 +287,11 @@ export class wiki {
         }
         return '操作成功';
     }
-    @runcod(['方舟输入','输入'], `输入文本`)
+    @runcod(['方舟输入', '输入'], `输入文本`)
     async input(
         @param("输入文本", 'text') text: Receive["text"],
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
-    ){
+    ) {
         const __dirname = path.dirname(fileURLToPath(import.meta.url));
         const tempDir = path.resolve(__dirname, '..', '..', 'botQQ_screenshots');
         const tempFile = path.join(tempDir, `screenshot_${Date.now()}.png`);
@@ -306,9 +305,9 @@ export class wiki {
             fs.writeFileSync(tempFile, buffer);
             if (text?.data?.text) {
                 // 使用Base64编码处理中文
-                const encodedText = text.data.text||"";
+                const encodedText = text.data.text || "";
                 const CMD = `${adbPath} -s ${deviceList[0]} shell am broadcast -a ADB_INPUT_TEXT --es msg "${encodedText}"`
-                execSync(CMD);                
+                execSync(CMD);
                 const buffer = await this.getimg(adbPath, deviceList, newtempFile);
                 if (!buffer) {
                     return;
@@ -319,13 +318,13 @@ export class wiki {
                     data: {
                         text: `记录结果:${text.data.text}`
                     },
-                },{
+                }, {
                     type: 'image',
                     data: {
                         file: `base64://${base64Data}`
                     }
                 }
-            ])
+                ])
             }
         }
         catch (error) {
@@ -339,7 +338,7 @@ export class wiki {
             if (fs.existsSync(tempFile)) {
                 fs.unlinkSync(tempFile);
             }
-        }finally  {
+        } finally {
             if (fs.existsSync(tempFile)) {
                 fs.unlinkSync(tempFile);
             }
@@ -349,13 +348,13 @@ export class wiki {
         }
     }
 
-    @runcod(['方舟点击','点击'], `创建一个点击步骤`)
+    @runcod(['方舟点击', '点击'], `创建一个点击步骤`)
     async rig2(
         @param("点击文本", 'text') targetText: Receive["text"],
         @param("x偏移", 'text', { type: 'text', data: { text: '' } }, true) x1: Receive["text"],
         @param("y偏移", 'text', { type: 'text', data: { text: '' } }, true) y1: Receive["text"],
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
-    ){
+    ) {
         const __dirname = path.dirname(fileURLToPath(import.meta.url));
         const tempDir = path.resolve(__dirname, '..', '..', 'botQQ_screenshots');
         const tempFile = path.join(tempDir, `screenshot_${Date.now()}.png`);
@@ -380,13 +379,13 @@ export class wiki {
                     data: {
                         text: `记录结果:x:${x1.data.text},y:${y1.data.text}(${targetText.data.text})\n 图片记录`
                     },
-                },{
+                }, {
                     type: 'image',
                     data: {
                         file: `base64://${base64Data}`
                     }
                 }]);
-                this.savetest(targetText.data.text, 'cilik' , Number(x1.data.text) , Number(y1.data.text));
+                this.savetest(targetText.data.text, 'cilik', Number(x1.data.text), Number(y1.data.text));
             }
             return '操作成功';
         } catch (error) {
@@ -409,10 +408,10 @@ export class wiki {
         }
     }
 
-    @runcod(['步骤','查看步骤'], `查看创建的步骤`)
+    @runcod(['步骤', '查看步骤'], `查看创建的步骤`)
     async list(
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
-    ){
+    ) {
         const __dirname = path.dirname(fileURLToPath(import.meta.url));
         const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'test.json');
         let data: any = {};
@@ -424,30 +423,30 @@ export class wiki {
         for (const key in data) {
             if (data.hasOwnProperty(key)) {
                 const element = data[key];
-                str += `${key}(${element.type?? 'click' }):x:${element.x1},y:${element.y1}\n`;
+                str += `${key}(${element.type ?? 'click'}):x:${element.x1},y:${element.y1}\n`;
             }
         }
         return str;
     }
-    @runcod(['步骤集','查看步骤集'], `查看创建的步骤集`)
-    async lists(){
+    @runcod(['步骤集', '查看步骤集'], `查看创建的步骤集`)
+    async lists() {
         const __dirname = path.dirname(fileURLToPath(import.meta.url));
         const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'tests.json');
-        let str=''
+        let str = ''
         let data: any = {};
         if (fs.existsSync(filePath)) {
             const fileContent = fs.readFileSync(filePath, 'utf-8');
             data = JSON.parse(fileContent);
         }
-        let i=1;
+        let i = 1;
         for (const key in data) {
-           const v = await this.readtests(key)
-           if (v) {
-            str+= `${i++}.${key}:`
-            for (const i in v) {
-                str+= `${v[i].name}(${v[i].type}),`
+            const v = await this.readtests(key)
+            if (v) {
+                str += `${i++}.${key}:`
+                for (const i in v) {
+                    str += `${v[i].name}(${v[i].type}),`
+                }
             }
-           }
         }
         return str;
     }
@@ -456,10 +455,10 @@ export class wiki {
         @param("步骤集名称", 'text') testname: Receive["text"],
         @param("步骤,例子:步骤名称1:类别,步骤名称2:类别", 'text') texts: Receive["text"],
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
-    ){
-        let str=''
+    ) {
+        let str = ''
         const tests = texts?.data?.text?.split(',')
-        const datatest:{
+        const datatest: {
             name: string;
             type: 'input' | 'click' | 'slide';
         }[] = [];
@@ -467,42 +466,42 @@ export class wiki {
             for (const test of tests) {
                 const [name, type] = test.split(':');
                 if (type == 'input') {
-                    datatest.push({type,name});
-                }else if (type == 'click'){
+                    datatest.push({ type, name });
+                } else if (type == 'click') {
                     const xy = await this.readxy(name)
                     if (xy) {
-                        datatest.push({type,name});
-                    }else{
-                        str+= `步骤${name}未找到,请创建\n`
+                        datatest.push({ type, name });
+                    } else {
+                        str += `步骤${name}未找到,请创建\n`
                     }
-                }else{
-                    str+= `步骤${name}未找到,请创建\n`
+                } else {
+                    str += `步骤${name}未找到,请创建\n`
                 }
-                
+
             }
-            this.createtests(testname.data.text,datatest);
-            str+= `步骤集${testname.data.text}创建成功\n`
+            this.createtests(testname.data.text, datatest);
+            str += `步骤集${testname.data.text}创建成功\n`
         }
         return str;
     }
-    @runcod(['执行步骤集','runtests'], `执行步骤集`)
+    @runcod(['执行步骤集', 'runtests'], `执行步骤集`)
     async runtest(
         @param("步骤集名称", 'text') name: Receive["text"],
-        @param("步骤,例子:输入值1:输入值2:输入值3:", 'text',{type:'text',data:{text:''}},true) input: Receive["text"],
+        @param("步骤,例子:输入值1:输入值2:输入值3:", 'text', { type: 'text', data: { text: '' } }, true) input: Receive["text"],
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
-    ){
+    ) {
         if (!name?.data?.text) {
-            return '步骤集名称不能为空';   
+            return '步骤集名称不能为空';
         }
         if (input?.data?.text == '') {
             return '输入值不能为空';
         }
-        let str=''
+        let str = ''
         const inputs = input?.data?.text?.split(':')
         const tests = await this.readtests(name.data.text)
         const { adbPath, deviceList } = this.getadb();
         if (tests) {
-            for (const test of  tests) {
+            for (const test of tests) {
                 if (test.type == 'input') {
                     if (inputs) {
                         const input = inputs.shift()
@@ -513,29 +512,29 @@ export class wiki {
                             str += `步骤${test.name}执行成功 类别:${test.type}输入值:${input}\n`
                         }
                     }
-                }else if (test.type == 'click'){
+                } else if (test.type == 'click') {
                     const readxy = await this.readxy(test.name)
-                    let cmd=`${adbPath} -s ${deviceList[0]} shell input tap ${readxy?.x1?? 0} ${readxy?.y1??0}`
+                    let cmd = `${adbPath} -s ${deviceList[0]} shell input tap ${readxy?.x1 ?? 0} ${readxy?.y1 ?? 0}`
                     execSync(cmd);
-                    str += `步骤${test.name}执行成功 类别:${test.type}(x:${readxy?.x1?? 0},y:${readxy?.y1??0})\n`
+                    str += `步骤${test.name}执行成功 类别:${test.type}(x:${readxy?.x1 ?? 0},y:${readxy?.y1 ?? 0})\n`
                     await new Promise((resolve) => setTimeout(resolve, 3000));
-                }else if (test.type == 'slide'){
+                } else if (test.type == 'slide') {
                     const readxy = await this.readxy(test.name)
-                    let cmd=`${adbPath} -s ${deviceList[0]} shell input swipe ${readxy?.x1?? 0} ${readxy?.y1??0} ${readxy?.x2?? 0} ${readxy?.y2??0}`
+                    let cmd = `${adbPath} -s ${deviceList[0]} shell input swipe ${readxy?.x1 ?? 0} ${readxy?.y1 ?? 0} ${readxy?.x2 ?? 0} ${readxy?.y2 ?? 0}`
                     execSync(cmd);
-                    str += `步骤${test.name}执行成功 类别:${test.type}(x:${readxy?.x1?? 0},y:${readxy?.y1??0},x2:${readxy?.x2?? 0},y2:${readxy?.y2??0})\n`
+                    str += `步骤${test.name}执行成功 类别:${test.type}(x:${readxy?.x1 ?? 0},y:${readxy?.y1 ?? 0},x2:${readxy?.x2 ?? 0},y2:${readxy?.y2 ?? 0})\n`
                     await new Promise((resolve) => setTimeout(resolve, 3000));
                 }
             }
             await this.tu(context);
             return str;
         }
-        return str??'步骤集不存在';
+        return str ?? '步骤集不存在';
     }
-    @runcod(['方舟状态','截图','状态'], `当前方舟运行状态`)
+    @runcod(['方舟状态', '截图', '状态'], `当前方舟运行状态`)
     async tu(
         context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
-    ){
+    ) {
         if (!context) {
             return;
         }
@@ -557,7 +556,7 @@ export class wiki {
                 data: {
                     file: `base64://${base64Data}`
                 }
-            }, 
+            },
             {
                 type: 'image',
                 data: {
@@ -584,6 +583,28 @@ export class wiki {
             }
         }
     }
+    @runcod(['红点识别', '点击'], `根据点击的红点识别参数`)
+    async recognizeText(@param("识别图片", 'image') image: Receive["image"],
+                        context: PrivateFriendMessage | PrivateGroupMessage | GroupMessage
+    ) { 
+        if (!image?.data?.url) {
+            return '图片不能为空';
+        }
+        const __dirname = path.dirname(fileURLToPath(import.meta.url));
+        let tempDir = path.resolve(__dirname, '..', '..', 'botQQ_screenshots');
+        let tempFile = path.join(tempDir, `screenshot_${Date.now()}.png`);
+        await this.downloadFile(image.data.url, tempFile);
+        const redDot = await this.findRedDot(tempFile);
+        if (redDot) {
+            fs.unlinkSync(tempFile);
+            const { adbPath, deviceList } = this.getadb();
+            execSync(`${adbPath} -s ${deviceList[0]} shell input tap ${redDot[0].x} ${redDot[0].y}`);
+            await new Promise((resolve) => setTimeout(resolve, 2000));
+            await this.tu(context);
+            return `识别成功`;
+        }
+        return '识别失败';
+    }
     //获取安卓模拟器设备
     getadb() {
         const adbPath = '/opt/homebrew/bin/adb';
@@ -604,9 +625,9 @@ export class wiki {
     }
     async getimg(adbPath: string, deviceList: string[], tempFile: string) {
         if (deviceList.length === 0) {
-            return   
+            return
         }
-        if (!adbPath){
+        if (!adbPath) {
             return
         }
         execSync(`${adbPath} -s ${deviceList[0]} exec-out screencap -p > ${tempFile}`);
@@ -617,9 +638,9 @@ export class wiki {
         }
         return Buffer.concat(chunks);
     }
-    private async recognizeTextPosition(imagePath: string, text: string): Promise<{ x: number; y: number,img:string ,txst:string}> {
+    private async recognizeTextPosition(imagePath: string, text: string): Promise<{ x: number; y: number, img: string, txst: string }> {
         if (!imagePath || !text) {
-            return { x: 0, y: 0,img:'',txst:'' };
+            return { x: 0, y: 0, img: '', txst: '' };
         }
         try {
             const worker = await createWorker();
@@ -649,7 +670,7 @@ export class wiki {
                 throw new Error('未找到目标文本');
             }
             let words: any = []
-            let txst='';
+            let txst = '';
             target.paragraphs.forEach((paragraph) => {
                 paragraph.lines.forEach((line) => {
                     line.words.forEach((word) => {
@@ -658,41 +679,124 @@ export class wiki {
                             x1: word.bbox.x1,
                             y0: word.bbox.y0,
                             y1: word.bbox.y1,
-                            text:  txst += `文本:${line.text}(${Math.floor(word.bbox.x0 + (word.bbox.x1 - word.bbox.x0) / 2)},${Math.floor(word.bbox.y0 + (word.bbox.y1 - word.bbox.y0) / 2)})\n`
+                            text: txst += `文本:${line.text}(${Math.floor(word.bbox.x0 + (word.bbox.x1 - word.bbox.x0) / 2)},${Math.floor(word.bbox.y0 + (word.bbox.y1 - word.bbox.y0) / 2)})\n`
                         })
                     });
                 });
             })
             const x = Math.floor(words[0].x0 + (words[0].x1 - words[0].x0) / 2)
             const y = Math.floor(words[0].y0 + (words[0].y1 - words[0].y0) / 2)
-            return {x,y, img:await this.drawCircle(imagePath,x,y) ,txst};
+            return { x, y, img: await this.drawCircle(imagePath, x, y), txst };
         } catch (error) {
             throw new Error(`识别失败:${error instanceof Error ? error.message : '请检查OCR依赖安装'}`);
         }
     }
     //绘制圆形标记
-    private async drawCircle(imagePath: string, x: number,y: number): Promise<string> {
+    private async drawCircle(imagePath: string, x: number, y: number): Promise<string> {
         if (!imagePath) {
             return '';
         }
         const image = await Jimp.read(imagePath);
-            const marker = new Jimp({
-                width: 50,
-                height: 50,
-                color: 0x00000000
-            });
-            marker.scan(0, 0, marker.bitmap.width, marker.bitmap.height,  (x, y, idx) => {
-                const dx = x - 25;
-                const dy = y - 25;
-                const distance = Math.sqrt(dx * dx + dy * dy);
-                if (distance <= 25) {
-                    marker.bitmap.data[idx + 3] = 255;
+        const marker = new Jimp({
+            width: 50,
+            height: 50,
+            color: 0x00000000
+        });
+        marker.scan(0, 0, marker.bitmap.width, marker.bitmap.height, (x, y, idx) => {
+            const dx = x - 25;
+            const dy = y - 25;
+            const distance = Math.sqrt(dx * dx + dy * dy);
+            if (distance <= 25) {
+                marker.bitmap.data[idx + 3] = 255;
+            }
+        });
+        marker.circle({ radius: 5, x: 25, y: 25 });
+        image.composite(marker, x - 10, y - 10);
+        const img = await image.getBase64('image/png');
+        return img.split(',')[1];
+    }
+    // 判断圆形标记位置
+    async findRedDot(imagePath: string): Promise<{ x: number, y: number }[]> {
+        // 读取图像数据
+        if (!imagePath) {
+            return [];
+        }
+        const { data, info } = await sharp(imagePath)
+            .raw()
+            .toBuffer({ resolveWithObject: true });
+
+        const width = info.width;
+        const height = info.height;
+        const channels = info.channels;
+        const pixels = new Uint8Array(data.buffer);
+
+        const redDots: { x: number, y: number }[] = [];
+
+        // 定义红色的阈值(可以根据需要调整)
+        const isRed = (r: number, g: number, b: number) => {
+            return r > 200 && g < 50 && b < 50;
+        };
+
+        // 检查5x5区域是否是圆形红点
+        const isRedDot = (x: number, y: number) => {
+            // 检查中心点必须是红色
+            if (!isPixelRed(x, y)) return false;
+
+            // 检查5x5区域
+            let redCount = 0;
+            const radius = 2; // 5x5区域的半径
+
+            for (let dy = -radius; dy <= radius; dy++) {
+                for (let dx = -radius; dx <= radius; dx++) {
+                    // 圆形检查:只考虑在半径内的点
+                    if (dx * dx + dy * dy <= radius * radius) {
+                        if (isPixelRed(x + dx, y + dy)) {
+                            redCount++;
+                        }
+                    }
                 }
-            });
-            marker.circle({ radius: 5, x: 25, y: 25 });
-            image.composite(marker, x - 10, y - 10);
-            const img =await image.getBase64('image/png');
-            return img.split(',')[1];
+            }
+
+            // 如果红色像素足够多,则认为是红点
+            return redCount >= 10; // 5x5圆形区域大约有13个像素
+        };
+
+        // 检查单个像素是否是红色
+        const isPixelRed = (x: number, y: number) => {
+            if (x < 0 || x >= width || y < 0 || y >= height) return false;
+            const idx = (y * width + x) * channels;
+            const r = pixels[idx];
+            const g = pixels[idx + 1];
+            const b = pixels[idx + 2];
+            return isRed(r, g, b);
+        };
+
+        // 扫描图像寻找红点
+        for (let y = 2; y < height - 2; y++) {
+            for (let x = 2; x < width - 2; x++) {
+                if (isRedDot(x, y)) {
+                    // 避免重复检测邻近的点
+                    if (!redDots.some(dot => Math.abs(dot.x - x) < 5 && Math.abs(dot.y - y) < 5)) {
+                        redDots.push({ x, y });
+                    }
+                }
+            }
+        }
+
+        return redDots;
+    }
+    //下载文件
+    private async downloadFile(url: string, path: string) {
+        if (!url) {
+            return;
+        }
+        if (!path) {
+            return;
+        }
+        const response = await fetch(url);
+        const arrayBuffer = await response.arrayBuffer();
+        const buffer = Buffer.from(arrayBuffer);
+        fs.writeFileSync(path, buffer);
     }
     //绘制网格
     private async drawGrid(imagePath: string): Promise<string> {
@@ -718,7 +822,7 @@ export class wiki {
         }
         for (let y = 0; y < height; y += gridSize) {
             // 绘制水平线
-            if (typeof Jimp!== 'undefined' && image instanceof Jimp) {
+            if (typeof Jimp !== 'undefined' && image instanceof Jimp) {
                 image.scan(0, y, width, 1, (xScan, yScan, idx) => {
                     image.bitmap.data[idx + 0] = 0;   // R
                     image.bitmap.data[idx + 1] = 0;   // G
@@ -730,7 +834,7 @@ export class wiki {
         // 线两段标注数字
         for (let x = 0; x < width; x += gridSize) {
             for (let y = 0; y < height; y += gridSize) {
-                if (typeof Jimp!== 'undefined' && image instanceof Jimp) {
+                if (typeof Jimp !== 'undefined' && image instanceof Jimp) {
                     if (font) {
                         image.print({ font, x: x - 10, y: y - 10, text: `${x},${y}` });
                     }
@@ -742,7 +846,7 @@ export class wiki {
         return img.split(',')[1];
     }
     //保存步骤记录
-    private async savetest(test: string,type:'cilik' | 'slide', x1: number, y1: number, x2?:number, y2?:number): Promise<void> {
+    private async savetest(test: string, type: 'cilik' | 'slide', x1: number, y1: number, x2?: number, y2?: number): Promise<void> {
         const __dirname = path.dirname(fileURLToPath(import.meta.url));
         //json
         const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'test.json');
@@ -750,36 +854,36 @@ export class wiki {
         if (fs.existsSync(filePath)) {
             const fileContent = fs.readFileSync(filePath, 'utf-8');
             data = JSON.parse(fileContent);
-        }else{
+        } else {
             fs.writeFileSync(filePath, JSON.stringify(data));
         }
-        if(data[test]){
-            switch(type){
+        if (data[test]) {
+            switch (type) {
                 case 'cilik':
-                    data[test].x1=x1
-                    data[test].y1=y1
+                    data[test].x1 = x1
+                    data[test].y1 = y1
                     break;
                 case 'slide':
-                    data[test].x1=x1
-                    data[test].y1=y1
-                    data[test].x2=x2
-                    data[test].y2=y2
+                    data[test].x1 = x1
+                    data[test].y1 = y1
+                    data[test].x2 = x2
+                    data[test].y2 = y2
             }
-        }else{
-            switch(type){
+        } else {
+            switch (type) {
                 case 'cilik':
-                    data[test]={type,x1,y1}
+                    data[test] = { type, x1, y1 }
                     break;
                 case 'slide':
-                    data[test]={type,x1,y1,x2,y2}
+                    data[test] = { type, x1, y1, x2, y2 }
             }
-            
+
         }
         fs.writeFileSync(filePath, JSON.stringify(data));
         return;
     }
     //读取步骤记录
-    private async readxy(test: string): Promise< { type:'cilik' | 'slide', x1: number, y1: number, x2?:number, y2?:number } | undefined> {
+    private async readxy(test: string): Promise<{ type: 'cilik' | 'slide', x1: number, y1: number, x2?: number, y2?: number } | undefined> {
         const __dirname = path.dirname(fileURLToPath(import.meta.url));
         const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'test.json');
         let data: any = {};
@@ -787,15 +891,15 @@ export class wiki {
             const fileContent = fs.readFileSync(filePath, 'utf-8');
             data = JSON.parse(fileContent);
         }
-        if(data[test]){
-            return data[test] as { type:'cilik' | 'slide', x1: number, y1: number, x2?:number, y2?:number };
+        if (data[test]) {
+            return data[test] as { type: 'cilik' | 'slide', x1: number, y1: number, x2?: number, y2?: number };
         }
     }
     //创建步骤记录
-    private async createtests(name:string , test: {
+    private async createtests(name: string, test: {
         name: string;
         type: 'input' | 'click' | 'slide';
-        
+
     }[]): Promise<void> {
         const __dirname = path.dirname(fileURLToPath(import.meta.url));
         const filePath = path.join(__dirname, '..', '..', 'botQQ_screenshots', 'tests.json');
@@ -804,12 +908,12 @@ export class wiki {
             const fileContent = fs.readFileSync(filePath, 'utf-8');
             data = JSON.parse(fileContent);
         }
-        data[name]=test
+        data[name] = test
         fs.writeFileSync(filePath, JSON.stringify(data));
         return;
     }
     //读取步骤记录
-    private async readtests(name:string): Promise<{
+    private async readtests(name: string): Promise<{
         name: string;
         type: 'input' | 'click' | 'slide';
     }[] | undefined> {
@@ -819,11 +923,11 @@ export class wiki {
         if (fs.existsSync(filePath)) {
             const fileContent = fs.readFileSync(filePath, 'utf-8');
             data = JSON.parse(fileContent);
-            if(data[name]){
-                return data[name] ;
+            if (data[name]) {
+                return data[name];
             }
         }
-        if(data[name]){
+        if (data[name]) {
             return data[name];
         }
     }

+ 28 - 10
src/resources/test/param.html

@@ -1,10 +1,28 @@
-<span>
-    模版生图演示
-    <br>
-    参数1(字符串): {{param1}}
-    <br>
-    参数2(数字): {{param2}}
-    <br>
-    参数3(at): {{param3}}
-    <br>
-</span>
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Document</title>
+</head>
+<body>
+    //动态画个动态的图片
+    <div>
+        <img src="https://n.sinaimg.cn/sinakd20115/160/w480h480/20250127/3d95-gif32719e5a3de39da1d6f90d07667e4e7e.gif" alt="">
+    </div>
+    //其他内容
+    <div>
+        <span>
+            模版生图演示
+            <br>
+                参数1(字符串): {{param1}}
+            <br>
+                参数2(数字): {{param2}}
+            <br>
+                参数3(at): {{param3}}
+            <br>
+        </span>
+    </div>
+    
+</body>
+</html>