diff --git a/manifest.json b/manifest.json index c2fd582..c94e72e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "manifest_version": 4, "type": "extension", - "name": "LLOneBot v3.16.1", + "name": "LLOneBot v3.16.0", "slug": "LLOneBot", "description": "LiteLoaderQQNT的OneBotApi,不支持商店在线更新", - "version": "3.16.1", + "version": "3.16.0", "icon": "./icon.jpg", "authors": [ { diff --git a/package-lock.json b/package-lock.json index cda8ff2..dc3d75e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,11 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "compressing": "^1.10.0", "express": "^4.18.2", "file-type": "^19.0.0", "fluent-ffmpeg": "^2.1.2", "level": "^8.0.1", - "node-stream-zip": "^1.15.0", "silk-wasm": "^3.2.3", "utf-8-validate": "^6.0.3", "uuid": "^9.0.1", @@ -551,6 +551,15 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@eggjs/yauzl": { + "version": "2.11.0", + "resolved": "https://registry.npmmirror.com/@eggjs/yauzl/-/yauzl-2.11.0.tgz", + "integrity": "sha512-Jq+k2fCZJ3i3HShb0nxLUiAgq5pwo8JTT1TrH22JoehZQ0Nm2dvByGIja1NYfNyuE4Tx5/Dns5nVsBN/mlC8yg==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer2": "^1.2.0" + } + }, "node_modules/@electron/get": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@electron/get/-/get-2.0.3.tgz", @@ -2151,6 +2160,47 @@ } ] }, + "node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/bl/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/bl/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/bl/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -2281,15 +2331,33 @@ "ieee754": "^1.2.1" } }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, "engines": { "node": "*" } }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://mirrors.cloud.tencent.com/npm/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2515,6 +2583,36 @@ "optional": true, "peer": true }, + "node_modules/compressing": { + "version": "1.10.0", + "resolved": "https://registry.npmmirror.com/compressing/-/compressing-1.10.0.tgz", + "integrity": "sha512-k2vpbZLaJoHe9euyUZjYYE8vOrbR19aU3HcWIYw5EBXiUs34ygfDVnXU+ubI41JXMriHutnoiu0ZFdwCkH6jPA==", + "dependencies": { + "@eggjs/yauzl": "^2.11.0", + "flushwritable": "^1.0.0", + "get-ready": "^1.0.0", + "iconv-lite": "^0.5.0", + "mkdirp": "^0.5.1", + "pump": "^3.0.0", + "streamifier": "^0.1.1", + "tar-stream": "^1.5.2", + "yazl": "^2.4.2" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/compressing/node_modules/iconv-lite": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.5.2.tgz", + "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://mirrors.cloud.tencent.com/npm/concat-map/-/concat-map-0.0.1.tgz", @@ -2559,6 +2657,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://mirrors.cloud.tencent.com/npm/create-require/-/create-require-1.1.1.tgz", @@ -2813,7 +2916,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -3636,6 +3738,14 @@ "pend": "~1.2.0" } }, + "node_modules/fd-slicer2": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/fd-slicer2/-/fd-slicer2-1.2.0.tgz", + "integrity": "sha512-3lBUNUckhMZduCc4g+Pw4Ve16LD9vpX9b8qUkkKq2mgDRLYWzblszZH2luADnJqjJe+cypngjCuKRm/IW12rRw==", + "dependencies": { + "pend": "^1.2.0" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://mirrors.cloud.tencent.com/npm/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -3738,6 +3848,11 @@ "node": ">=0.8.0" } }, + "node_modules/flushwritable": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/flushwritable/-/flushwritable-1.0.0.tgz", + "integrity": "sha512-3VELfuWCLVzt5d2Gblk8qcqFro6nuwvxwMzHaENVDHI7rxcBRtMCwTk/E9FXcgh+82DSpavPNDueA9+RxXJoFg==" + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://mirrors.cloud.tencent.com/npm/for-each/-/for-each-0.3.3.tgz", @@ -3763,6 +3878,11 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -3859,6 +3979,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-ready": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/get-ready/-/get-ready-1.0.0.tgz", + "integrity": "sha512-mFXCZPJIlcYcth+N8267+mghfYN9h3EhsDa6JSnbA3Wrhh/XFpuowviFcsDeYZtKspQyWyJqfs4O6P8CHeTwzw==" + }, "node_modules/get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -4859,11 +4984,21 @@ "version": "1.2.8", "resolved": "https://mirrors.cloud.tencent.com/npm/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/module-error": { "version": "1.0.2", "resolved": "https://mirrors.cloud.tencent.com/npm/module-error/-/module-error-1.0.2.tgz", @@ -4931,18 +5066,6 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, - "node_modules/node-stream-zip": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz", - "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==", - "engines": { - "node": ">=0.12.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/antelle" - } - }, "node_modules/normalize-url": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", @@ -5052,7 +5175,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -5165,8 +5287,7 @@ "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "dev": true + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" }, "node_modules/picocolors": { "version": "1.0.0", @@ -5232,6 +5353,11 @@ "node": ">= 0.8.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -5257,7 +5383,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -5833,6 +5958,14 @@ "node": ">= 0.8" } }, + "node_modules/streamifier": { + "version": "0.1.1", + "resolved": "https://registry.npmmirror.com/streamifier/-/streamifier-0.1.1.tgz", + "integrity": "sha512-zDgl+muIlWzXNsXeyUfOk9dChMjlpkq0DRsxujtYPgyJ676yQ8jEm6zzaaWHFDg5BNcLuif0eD2MTyJdZqXpdg==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -5959,6 +6092,55 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tar-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/tar-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/tar-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/terser": { "version": "5.28.1", "resolved": "https://mirrors.cloud.tencent.com/npm/terser/-/terser-5.28.1.tgz", @@ -5985,6 +6167,11 @@ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, + "node_modules/to-buffer": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/to-buffer/-/to-buffer-1.1.1.tgz", + "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://mirrors.cloud.tencent.com/npm/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -6528,8 +6715,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "8.16.0", @@ -6551,6 +6737,14 @@ } } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -6567,6 +6761,14 @@ "fd-slicer": "~1.1.0" } }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://mirrors.cloud.tencent.com/npm/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index a3392fc..1ef7e71 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,11 @@ "author": "", "license": "ISC", "dependencies": { + "compressing": "^1.10.0", "express": "^4.18.2", "file-type": "^19.0.0", "fluent-ffmpeg": "^2.1.2", "level": "^8.0.1", - "node-stream-zip": "^1.15.0", "silk-wasm": "^3.2.3", "utf-8-validate": "^6.0.3", "uuid": "^9.0.1", diff --git a/src/common/data.ts b/src/common/data.ts index 5bfb876..b006762 100644 --- a/src/common/data.ts +++ b/src/common/data.ts @@ -21,7 +21,9 @@ export let friends: Friend[] = [] export let friendRequests: Map = new Map() export const llonebotError: LLOneBotError = { ffmpegError: '', - otherError: '' + httpServerError: '', + wsServerError: '', + otherError: 'LLOnebot未能正常启动,请检查日志查看错误' } diff --git a/src/common/server/http.ts b/src/common/server/http.ts index 32dd4bb..ccaf49f 100644 --- a/src/common/server/http.ts +++ b/src/common/server/http.ts @@ -1,7 +1,8 @@ -import express, {Express, json, Request, Response} from "express"; +import express, {Express, Request, Response} from "express"; import http from "http"; import {log} from "../utils/log"; import {getConfigUtil} from "../config"; +import {llonebotError} from "../data"; type RegisterHandler = (res: Response, payload: any) => Promise @@ -52,13 +53,20 @@ export abstract class HttpServerBase { }; start(port: number) { - this.expressAPP.get('/', (req: Request, res: Response) => { - res.send(`${this.name}已启动`); - }) - this.listen(port); + try { + this.expressAPP.get('/', (req: Request, res: Response) => { + res.send(`${this.name}已启动`); + }) + this.listen(port); + llonebotError.httpServerError = "" + } catch (e) { + log("HTTP服务启动失败", e.toString()) + llonebotError.httpServerError = "HTTP服务启动失败, " + e.toString() + } } stop() { + llonebotError.httpServerError = "" if (this.server) { this.server.close() this.server = null; diff --git a/src/common/server/websocket.ts b/src/common/server/websocket.ts index 0920b01..a9e4d3d 100644 --- a/src/common/server/websocket.ts +++ b/src/common/server/websocket.ts @@ -3,6 +3,7 @@ import urlParse from "url"; import {IncomingMessage} from "node:http"; import {log} from "../utils/log"; import {getConfigUtil} from "../config"; +import {llonebotError} from "../data"; class WebsocketClientBase { private wsClient: WebSocket @@ -29,7 +30,12 @@ export class WebsocketServerBase { } start(port: number) { - this.ws = new WebSocketServer({port}); + try { + this.ws = new WebSocketServer({port}); + llonebotError.wsServerError = '' + }catch (e) { + llonebotError.wsServerError = "正向ws服务启动失败, " + e.toString() + } this.ws.on("connection", (wsClient, req) => { const url = req.url.split("?").shift() this.authorize(wsClient, req); @@ -41,6 +47,7 @@ export class WebsocketServerBase { } stop() { + llonebotError.wsServerError = '' this.ws.close((err) => { log("ws server close failed!", err) }); diff --git a/src/common/types.ts b/src/common/types.ts index 060a517..aa4d98b 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -28,6 +28,8 @@ export interface Config { } export interface LLOneBotError { + httpServerError?: string + wsServerError?: string ffmpegError?: string otherError?: string } diff --git a/src/common/utils/file.ts b/src/common/utils/file.ts index f473aef..c50b68d 100644 --- a/src/common/utils/file.ts +++ b/src/common/utils/file.ts @@ -1,13 +1,17 @@ import fs from "fs"; +import fsPromise from "fs/promises"; import crypto from "crypto"; import ffmpeg from "fluent-ffmpeg"; import util from "util"; import {encode, getDuration, isWav} from "silk-wasm"; import path from "node:path"; import {v4 as uuidv4} from "uuid"; -import {DATA_DIR} from "./index"; -import {log} from "./log"; +import {DATA_DIR, log, TEMP_DIR} from "./index"; import {getConfigUtil} from "../config"; +import {dbUtil} from "../db"; +import * as fileType from "file-type"; +import {net} from "electron"; +import ClientRequestConstructorOptions = Electron.Main.ClientRequestConstructorOptions; export function isGIF(path: string) { const buffer = Buffer.alloc(4); @@ -64,8 +68,11 @@ export async function file2base64(path: string) { export function checkFfmpeg(newPath: string = null): Promise { return new Promise((resolve, reject) => { + log("开始检查ffmpeg", newPath); if (newPath) { ffmpeg.setFfmpegPath(newPath); + } + try { ffmpeg.getAvailableFormats((err, formats) => { if (err) { log('ffmpeg is not installed or not found in PATH:', err); @@ -75,6 +82,8 @@ export function checkFfmpeg(newPath: string = null): Promise { resolve(true); } }) + } catch (e) { + resolve(false); } }); } @@ -260,4 +269,172 @@ export function calculateFileMD5(filePath: string): Promise { reject(err); }); }); +} + +export interface HttpDownloadOptions { + url: string; + headers?: Record | string; +} +export async function httpDownload(options: string | HttpDownloadOptions): Promise { + let chunks: Buffer[] = []; + let url: string; + let headers: Record = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36" + }; + if (typeof options === "string") { + url = options; + } else { + url = options.url; + if (options.headers) { + if (typeof options.headers === "string") { + headers = JSON.parse(options.headers); + } else { + headers = options.headers; + } + } + } + const fetchRes = await net.fetch(url, headers); + if (!fetchRes.ok) throw new Error(`下载文件失败: ${fetchRes.statusText}`) + + const blob = await fetchRes.blob(); + let buffer = await blob.arrayBuffer(); + return Buffer.from(buffer); +} + +type Uri2LocalRes = { + success: boolean, + errMsg: string, + fileName: string, + ext: string, + path: string, + isLocal: boolean +} + +export async function uri2local(uri: string, fileName: string = null): Promise { + let res = { + success: false, + errMsg: "", + fileName: "", + ext: "", + path: "", + isLocal: false + } + if (!fileName) { + fileName = uuidv4(); + } + let filePath = path.join(TEMP_DIR, fileName) + let url = null; + try { + url = new URL(uri); + } catch (e) { + res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在` + return res + } + + // log("uri protocol", url.protocol, uri); + if (url.protocol == "base64:") { + // base64转成文件 + let base64Data = uri.split("base64://")[1] + try { + const buffer = Buffer.from(base64Data, 'base64'); + fs.writeFileSync(filePath, buffer); + + } catch (e: any) { + res.errMsg = `base64文件下载失败,` + e.toString() + return res + } + } else if (url.protocol == "http:" || url.protocol == "https:") { + // 下载文件 + let buffer: Buffer = null; + try{ + buffer = await httpDownload(uri); + }catch (e) { + res.errMsg = `${url}下载失败,` + e.toString() + return res + } + try { + const pathInfo = path.parse(decodeURIComponent(url.pathname)) + if (pathInfo.name) { + fileName = pathInfo.name + if (pathInfo.ext) { + fileName += pathInfo.ext + // res.ext = pathInfo.ext + } + } + res.fileName = fileName + filePath = path.join(TEMP_DIR, uuidv4() + fileName) + fs.writeFileSync(filePath, buffer); + } catch (e: any) { + res.errMsg = `${url}下载失败,` + e.toString() + return res + } + } else { + let pathname: string; + if (url.protocol === "file:") { + // await fs.copyFile(url.pathname, filePath); + pathname = decodeURIComponent(url.pathname) + if (process.platform === "win32") { + filePath = pathname.slice(1) + } else { + filePath = pathname + } + } else { + const cache = await dbUtil.getFileCache(uri); + if (cache) { + filePath = cache.filePath + } else { + filePath = uri; + } + } + + res.isLocal = true + } + // else{ + // res.errMsg = `不支持的file协议,` + url.protocol + // return res + // } + // if (isGIF(filePath) && !res.isLocal) { + // await fs.rename(filePath, filePath + ".gif"); + // filePath += ".gif"; + // } + if (!res.isLocal && !res.ext) { + try { + let ext: string = (await fileType.fileTypeFromFile(filePath)).ext + if (ext) { + log("获取文件类型", ext, filePath) + fs.renameSync(filePath, filePath + `.${ext}`) + filePath += `.${ext}` + res.fileName += `.${ext}` + res.ext = ext + } + } catch (e) { + // log("获取文件类型失败", filePath,e.stack) + } + } + res.success = true + res.path = filePath + return res +} + +export async function copyFolder(sourcePath: string, destPath: string) { + try { + const entries = await fsPromise.readdir(sourcePath, {withFileTypes: true}); + await fsPromise.mkdir(destPath, {recursive: true}); + for (let entry of entries) { + const srcPath = path.join(sourcePath, entry.name); + const dstPath = path.join(destPath, entry.name); + if (entry.isDirectory()) { + await copyFolder(srcPath, dstPath); + } else { + try { + await fsPromise.copyFile(srcPath, dstPath); + } catch (error) { + console.error(`无法复制文件 '${srcPath}' 到 '${dstPath}': ${error}`); + // 这里可以决定是否要继续复制其他文件 + } + } + } + } catch (error) { + console.error('复制文件夹时出错:', error); + } } \ No newline at end of file diff --git a/src/common/utils/helper.ts b/src/common/utils/helper.ts index 65a999a..48862b8 100644 --- a/src/common/utils/helper.ts +++ b/src/common/utils/helper.ts @@ -43,4 +43,26 @@ export function mergeNewProperties(newObj: any, oldObj: any) { export function isNull(value: any) { return value === undefined || value === null; -} \ No newline at end of file +} + +/** + * 将字符串按最大长度分割并添加换行符 + * @param str 原始字符串 + * @param maxLength 每行的最大字符数 + * @returns 处理后的字符串,超过长度的地方将会换行 + */ +export function wrapText(str: string, maxLength: number): string { + // 初始化一个空字符串用于存放结果 + let result: string = ''; + + // 循环遍历字符串,每次步进maxLength个字符 + for (let i = 0; i < str.length; i += maxLength) { + // 从i开始,截取长度为maxLength的字符串段,并添加到结果字符串 + // 如果不是第一段,先添加一个换行符 + if (i > 0) result += '\n'; + result += str.substring(i, i + maxLength); + } + + return result; +} + diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index af279cf..58c3afa 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -1,10 +1,15 @@ +import path from "node:path"; +import fs from "fs"; + export * from './file' export * from './helper' export * from './log' export * from './qqlevel' export * from './qqpkg' -export * from './update' +export * from './upgrade' export const DATA_DIR = global.LiteLoader.plugins["LLOneBot"].path.data; - - - +export const TEMP_DIR = path.join(DATA_DIR, "temp"); +export const PLUGIN_DIR = global.LiteLoader.plugins["LLOneBot"].path.plugin; +if (!fs.existsSync(TEMP_DIR)) { + fs.mkdirSync(TEMP_DIR); +} \ No newline at end of file diff --git a/src/common/utils/update.ts b/src/common/utils/update.ts deleted file mode 100644 index 8ab6184..0000000 --- a/src/common/utils/update.ts +++ /dev/null @@ -1,62 +0,0 @@ -import {version} from "../../version"; -import https from "node:https"; - -export async function checkVersion() { - const latestVersionText = await getRemoteVersion(); - const latestVersion = latestVersionText.split("."); - const currentVersion = version.split("."); - for (let k in [0, 1, 2]) { - if (latestVersion[k] > currentVersion[k]) { - return { result: false, version: latestVersionText }; - } - } - return { result: true, version: version }; -} -export async function updateLLOneBot() { - let mirrorGithubList = ["https://mirror.ghproxy.com/"]; - const latestVersion = await getRemoteVersion(); - if (latestVersion && latestVersion != "") { - const downloadUrl = "https://github.com/LLOneBot/LLOneBot/releases/download/v" + latestVersion + "/LLOneBot.zip"; - const realUrl = mirrorGithubList[0] + downloadUrl; - } - return false; -} -export async function getRemoteVersion() { - let mirrorGithubList = ["https://521github.com"]; - let Version = ""; - for (let i = 0; i < mirrorGithubList.length; i++) { - let mirrorGithub = mirrorGithubList[i]; - let tVersion = await getRemoteVersionByMirror(mirrorGithub); - if (tVersion && tVersion != "") { - Version = tVersion; - break; - } - } - return Version; -} -export async function getRemoteVersionByMirror(mirrorGithub: string) { - let releasePage = "error"; - let reqPromise = async function (): Promise { - return new Promise((resolve, reject) => { - https.get(mirrorGithub + "/LLOneBot/LLOneBot/releases", res => { - let list = []; - res.on('data', chunk => { - list.push(chunk); - }); - res.on('end', () => { - resolve(Buffer.concat(list).toString()); - }); - }).on('error', err => { - reject(); - }); - }); - } - try { - releasePage = await reqPromise(); - if (releasePage === "error") return ""; - return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0]; - } - catch { } - return ""; - -} \ No newline at end of file diff --git a/src/common/utils/upgrade.ts b/src/common/utils/upgrade.ts new file mode 100644 index 0000000..76ea39b --- /dev/null +++ b/src/common/utils/upgrade.ts @@ -0,0 +1,93 @@ +import {version} from "../../version"; +import * as path from "node:path"; +import * as fs from "node:fs"; +import {copyFolder, httpDownload, log, PLUGIN_DIR, TEMP_DIR} from "."; +import compressing from "compressing"; + + +const downloadMirrorHosts = ["https://mirror.ghproxy.com/"]; +const checkVersionMirrorHosts = ["https://521github.com"]; + +export async function checkVersion() { + const latestVersionText = await getRemoteVersion(); + const latestVersion = latestVersionText.split("."); + log("llonebot last version", latestVersion); + const currentVersion = version.split("."); + for (let k in [0, 1, 2]) { + if (latestVersion[k] > currentVersion[k]) { + return {result: false, version: latestVersionText}; + } + } + return {result: true, version: version}; +} + +export async function upgradeLLOneBot() { + const latestVersion = await getRemoteVersion(); + if (latestVersion && latestVersion != "") { + const downloadUrl = "https://github.com/LLOneBot/LLOneBot/releases/download/v" + latestVersion + "/LLOneBot.zip"; + const filePath = path.join(TEMP_DIR, "./update-" + latestVersion + ".zip"); + let downloadSuccess = false; + // 多镜像下载 + for(const mirrorGithub of downloadMirrorHosts){ + try{ + const buffer = await httpDownload(mirrorGithub + downloadUrl); + fs.writeFileSync(filePath, buffer) + downloadSuccess = true; + break; + }catch (e) { + log("llonebot upgrade error", e); + } + } + if (!downloadSuccess){ + log("llonebot upgrade error", "download failed"); + return false; + } + const temp_ver_dir = path.join(TEMP_DIR, "LLOneBot" + latestVersion); + let uncompressedPromise = async function () { + return new Promise((resolve, reject) => { + compressing.zip.uncompress(filePath, temp_ver_dir).then(() => { + resolve(true); + }).catch((reason: any) => { + log("llonebot upgrade failed, ", reason); + if (reason?.errno == -4082) { + resolve(true); + } + resolve(false); + }); + }); + } + const uncompressedResult = await uncompressedPromise(); + // 复制文件 + await copyFolder(temp_ver_dir, PLUGIN_DIR); + + return uncompressedResult; + } + return false; +} + +export async function getRemoteVersion() { + let Version = ""; + for (let i = 0; i < checkVersionMirrorHosts.length; i++) { + let mirrorGithub = checkVersionMirrorHosts[i]; + let tVersion = await getRemoteVersionByMirror(mirrorGithub); + if (tVersion && tVersion != "") { + Version = tVersion; + break; + } + } + return Version; +} + +export async function getRemoteVersionByMirror(mirrorGithub: string) { + let releasePage = "error"; + + try { + releasePage = (await httpDownload(mirrorGithub + "/LLOneBot/LLOneBot/releases")).toString(); + // log("releasePage", releasePage); + if (releasePage === "error") return ""; + return releasePage.match(new RegExp('(?<=(tag/v)).*?(?=("))'))[0]; + } catch { + } + return ""; + +} \ No newline at end of file diff --git a/src/main/main.ts b/src/main/main.ts index 15da2d2..42257d0 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -13,7 +13,7 @@ import { CHANNEL_UPDATE, } from "../common/channels"; import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; -import {DATA_DIR} from "../common/utils"; +import {DATA_DIR, wrapText} from "../common/utils"; import { friendRequests, getFriend, @@ -41,7 +41,7 @@ import {NTQQUserApi} from "../ntqqapi/api/user"; import {NTQQGroupApi} from "../ntqqapi/api/group"; import {registerPokeHandler} from "../ntqqapi/external/ccpoke"; import {OB11FriendPokeEvent, OB11GroupPokeEvent} from "../onebot11/event/notice/OB11PokeEvent"; -import {checkVersion, updateLLOneBot} from "../common/utils/update"; +import {checkVersion, upgradeLLOneBot} from "../common/utils/upgrade"; import {checkFfmpeg} from "../common/utils/file"; import {log} from "../common/utils/log"; import {getConfigUtil} from "../common/config"; @@ -57,7 +57,7 @@ function onLoad() { return checkVersion(); }); ipcMain.handle(CHANNEL_UPDATE, async (event, arg) => { - return updateLLOneBot(); + return upgradeLLOneBot(); }); ipcMain.handle(CHANNEL_SELECT_FILE, async (event, arg) => { const selectPath = new Promise((resolve, reject) => { @@ -92,8 +92,14 @@ function onLoad() { if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, {recursive: true}); } - ipcMain.handle(CHANNEL_ERROR, (event, arg) => { - return llonebotError; + ipcMain.handle(CHANNEL_ERROR, async (event, arg) => { + const ffmpegOk = await checkFfmpeg(getConfigUtil().getConfig().ffmpeg) + llonebotError.ffmpegError = ffmpegOk ? "" : "没有找到ffmpeg,音频只能发送wav和silk,视频无法发送" + let {httpServerError, wsServerError, otherError, ffmpegError} = llonebotError; + let error = `${otherError}\n${httpServerError}\n${wsServerError}\n${ffmpegError}` + error = error.replace("\n\n", "\n") + error = error.trim(); + return error; }) ipcMain.handle(CHANNEL_GET_CONFIG, async (event, arg) => { const config = getConfigUtil().getConfig() @@ -331,7 +337,7 @@ function onLoad() { async function start() { log("llonebot pid", process.pid) - + llonebotError.otherError = ""; startTime = Date.now(); dbUtil.getReceivedTempUinMap().then(m=>{ for (const [key, value] of Object.entries(m)) { @@ -341,18 +347,8 @@ function onLoad() { startReceiveHook().then(); NTQQGroupApi.getGroups(true).then() const config = getConfigUtil().getConfig() - // 检查ffmpeg - checkFfmpeg(config.ffmpeg).then(exist => { - if (!exist) { - llonebotError.ffmpegError = `没有找到ffmpeg,音频只能发送wav和silk` - } - }) if (config.ob11.enableHttp) { - try { - ob11HTTPServer.start(config.ob11.httpPort) - } catch (e) { - log("http server start failed", e); - } + ob11HTTPServer.start(config.ob11.httpPort) } if (config.ob11.enableWs) { ob11WebsocketServer.start(config.ob11.wsPort); diff --git a/src/main/setConfig.ts b/src/main/setConfig.ts index eb72ede..b82af07 100644 --- a/src/main/setConfig.ts +++ b/src/main/setConfig.ts @@ -3,7 +3,6 @@ import {ob11HTTPServer} from "../onebot11/server/http"; import {ob11WebsocketServer} from "../onebot11/server/ws/WebsocketServer"; import {ob11ReverseWebsockets} from "../onebot11/server/ws/ReverseWebsocket"; import {llonebotError} from "../common/data"; -import {checkFfmpeg} from "../common/utils/file"; import {getConfigUtil} from "../common/config"; export async function setConfig(config: Config) { @@ -21,6 +20,7 @@ export async function setConfig(config: Config) { // 正向ws端口变化,重启服务 if (config.ob11.wsPort != oldConfig.ob11.wsPort) { ob11WebsocketServer.restart(config.ob11.wsPort); + llonebotError.wsServerError = '' } // 判断是否启用或关闭正向ws if (config.ob11.enableWs != oldConfig.ob11.enableWs) { @@ -51,14 +51,4 @@ export async function setConfig(config: Config) { } } } - - // 检查ffmpeg - if (config.ffmpeg) { - checkFfmpeg(config.ffmpeg).then(success => { - if (success) { - llonebotError.ffmpegError = '' - } - }) - } - } \ No newline at end of file diff --git a/src/ntqqapi/api/msg.ts b/src/ntqqapi/api/msg.ts index bfca2be..ec79de3 100644 --- a/src/ntqqapi/api/msg.ts +++ b/src/ntqqapi/api/msg.ts @@ -23,6 +23,17 @@ export class NTQQMsgApi { args: [{peer:{peerUid: groupCode, chatType: ChatType.group}, cnt: 20}, null] }) } + static async getMsgHistory(peer: Peer, msgId: string, count: number) { + return await callNTQQApi({ + methodName: NTQQApiMethod.HISTORY_MSG, + args: [{ + peer, + msgId, + cnt: count, + queryOrder: true, + }, null] + }) + } static async fetchRecentContact(){ await callNTQQApi({ methodName: NTQQApiMethod.RECENT_CONTACT, diff --git a/src/ntqqapi/ntcall.ts b/src/ntqqapi/ntcall.ts index 20c35c6..15b86a2 100644 --- a/src/ntqqapi/ntcall.ts +++ b/src/ntqqapi/ntcall.ts @@ -17,7 +17,7 @@ export enum NTQQApiClass { export enum NTQQApiMethod { RECENT_CONTACT = "nodeIKernelRecentContactService/fetchAndSubscribeABatchOfRecentContact", ADD_ACTIVE_CHAT = "nodeIKernelMsgService/getAioFirstViewLatestMsgsAndAddActiveChat", // 激活群助手内的聊天窗口,这样才能收到消息 - ADD_ACTIVE_CHAT_2 = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat", + HISTORY_MSG = "nodeIKernelMsgService/getMsgsIncludeSelfAndAddActiveChat", LIKE_FRIEND = "nodeIKernelProfileLikeService/setBuddyProfileLike", SELF_INFO = "fetchAuthData", FRIENDS = "nodeIKernelBuddyService/getBuddyList", diff --git a/src/onebot11/action/GetFile.ts b/src/onebot11/action/GetFile.ts index ace5163..0687dfd 100644 --- a/src/onebot11/action/GetFile.ts +++ b/src/onebot11/action/GetFile.ts @@ -2,6 +2,7 @@ import BaseAction from "./BaseAction"; import fs from "fs/promises"; import {dbUtil} from "../../common/db"; import {getConfigUtil} from "../../common/config"; +import {log, uri2local} from "../../common/utils"; export interface GetFilePayload { file: string // 文件名 @@ -26,6 +27,18 @@ export class GetFileBase extends BaseAction { if (cache.downloadFunc) { await cache.downloadFunc() } + try { + await fs.access(cache.filePath, fs.constants.F_OK) + } catch (e) { + log("file not found", e) + const downloadResult = await uri2local(cache.url) + if (downloadResult.success) { + cache.filePath = downloadResult.path + dbUtil.addFileCache(payload.file, cache).then() + } else { + throw new Error("file download failed. " + downloadResult.errMsg) + } + } let res: GetFileResponse = { file: cache.filePath, url: cache.url, @@ -37,11 +50,11 @@ export class GetFileBase extends BaseAction { res.base64 = await fs.readFile(cache.filePath, 'base64') } } - if (autoDeleteFile) { - setTimeout(() => { - fs.unlink(cache.filePath) - }, autoDeleteFileSecond * 1000) - } + // if (autoDeleteFile) { + // setTimeout(() => { + // fs.unlink(cache.filePath) + // }, autoDeleteFileSecond * 1000) + // } return res } } \ No newline at end of file diff --git a/src/onebot11/action/SendMsg.ts b/src/onebot11/action/SendMsg.ts index b7eb2fa..b10976a 100644 --- a/src/onebot11/action/SendMsg.ts +++ b/src/onebot11/action/SendMsg.ts @@ -25,7 +25,6 @@ import { } from '../types'; import {Peer} from "../../ntqqapi/api/msg"; import {SendMsgElementConstructor} from "../../ntqqapi/constructor"; -import {uri2local} from "../utils"; import BaseAction from "./BaseAction"; import {ActionName, BaseCheckResult} from "./types"; import * as fs from "node:fs"; @@ -35,6 +34,7 @@ import {ALLOW_SEND_TEMP_MSG} from "../../common/config"; import {NTQQMsgApi} from "../../ntqqapi/api/msg"; import {log} from "../../common/utils/log"; import {sleep} from "../../common/utils/helper"; +import {uri2local} from "../../common/utils"; function checkSendMessage(sendMsgList: OB11MessageData[]) { function checkUri(uri: string): boolean { diff --git a/src/onebot11/action/go-cqhttp/DownloadFile.ts b/src/onebot11/action/go-cqhttp/DownloadFile.ts new file mode 100644 index 0000000..4ca1204 --- /dev/null +++ b/src/onebot11/action/go-cqhttp/DownloadFile.ts @@ -0,0 +1,73 @@ +import BaseAction from "../BaseAction"; +import {ActionName} from "../types"; +import fs from "fs"; +import {join as joinPath} from "node:path"; +import {calculateFileMD5, httpDownload, TEMP_DIR} from "../../../common/utils"; +import {v4 as uuid4} from "uuid"; + +interface Payload { + thread_count?: number + url?: string + base64?: string + name?: string + headers?: string | string[] +} + +interface FileResponse { + file: string +} + +export default class GoCQHTTPDownloadFile extends BaseAction { + actionName = ActionName.GoCQHTTP_DownloadFile + + protected async _handle(payload: Payload): Promise { + const isRandomName = !payload.name + let name = payload.name || uuid4(); + const filePath = joinPath(TEMP_DIR, name); + + if (payload.base64) { + fs.writeFileSync(filePath, payload.base64, 'base64') + } else if (payload.url) { + const headers = this.getHeaders(payload.headers); + let buffer = await httpDownload({url: payload.url, headers: headers}) + fs.writeFileSync(filePath, Buffer.from(buffer), 'binary'); + } else { + throw new Error("不存在任何文件, 无法下载") + } + if (fs.existsSync(filePath)) { + + if (isRandomName) { + // 默认实现要名称未填写时文件名为文件 md5 + const md5 = await calculateFileMD5(filePath); + const newPath = joinPath(TEMP_DIR, md5); + fs.renameSync(filePath, newPath); + return { file: newPath } + } + return { file: filePath } + } else { + throw new Error("文件写入失败, 检查权限") + } + } + + getHeaders(headersIn?: string | string[]): Record { + const headers = {}; + if (typeof headersIn == 'string') { + headersIn = headersIn.split('[\\r\\n]'); + } + if (Array.isArray(headersIn)) { + for (const headerItem of headersIn) { + const spilt = headerItem.indexOf('='); + if (spilt < 0) { + headers[headerItem] = ""; + } else { + const key = headerItem.substring(0, spilt); + headers[key] = headerItem.substring(0, spilt + 1); + } + } + } + if (!headers['Content-Type']) { + headers['Content-Type'] = 'application/octet-stream'; + } + return headers; + } +} \ No newline at end of file diff --git a/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts b/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts new file mode 100644 index 0000000..05ae6b5 --- /dev/null +++ b/src/onebot11/action/go-cqhttp/GetGroupMsgHistory.ts @@ -0,0 +1,34 @@ +import BaseAction from "../BaseAction"; +import {OB11Message, OB11User} from "../../types"; +import {groups} from "../../../common/data"; +import {ActionName} from "../types"; +import {ChatType} from "../../../ntqqapi/types"; +import {dbUtil} from "../../../common/db"; +import {NTQQMsgApi} from "../../../ntqqapi/api/msg"; +import {OB11Constructor} from "../../constructor"; +import {log} from "../../../common/utils"; + + +interface Payload { + group_id: number + message_seq: number +} + +export default class GoCQHTTPGetGroupMsgHistory extends BaseAction { + actionName = ActionName.GoCQHTTP_GetGroupMsgHistory + + protected async _handle(payload: Payload): Promise { + const group = groups.find(group => group.groupCode === payload.group_id.toString()) + if (!group) { + throw `群${payload.group_id}不存在` + } + const startMsgId = (await dbUtil.getMsgByShortId(payload.message_seq))?.msgId || "0" + // log("startMsgId", startMsgId) + let msgList = (await NTQQMsgApi.getMsgHistory({chatType: ChatType.group, peerUid: group.groupCode}, startMsgId, 20)).msgList + await Promise.all(msgList.map(async msg => { + msg.msgShortId = await dbUtil.addMsg(msg) + })) + const ob11MsgList = await Promise.all(msgList.map(msg=>OB11Constructor.message(msg))) + return ob11MsgList + } +} \ No newline at end of file diff --git a/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts b/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts index a8a21ca..340742f 100644 --- a/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts +++ b/src/onebot11/action/go-cqhttp/GetStrangerInfo.ts @@ -3,23 +3,34 @@ import {OB11User} from "../../types"; import {getFriend, getGroupMember, groups} from "../../../common/data"; import {OB11Constructor} from "../../constructor"; import {ActionName} from "../types"; +import {isNull, log} from "../../../common/utils"; +import {NTQQUserApi} from "../../../ntqqapi/api/user"; +import {Friend, GroupMember} from "../../../ntqqapi/types"; export default class GoCQHTTPGetStrangerInfo extends BaseAction<{ user_id: number }, OB11User> { actionName = ActionName.GoCQHTTP_GetStrangerInfo + private async refreshInfo(user: Friend | GroupMember){ + if (isNull(user.sex)){ + let info = (await NTQQUserApi.getUserDetailInfo(user.uid)) + Object.assign(user, info); + } + } protected async _handle(payload: { user_id: number }): Promise { const user_id = payload.user_id.toString() const friend = await getFriend(user_id) if (friend) { + await this.refreshInfo(friend); return OB11Constructor.friend(friend); } for (const group of groups) { const member = await getGroupMember(group.groupCode, user_id) if (member) { + await this.refreshInfo(member); return OB11Constructor.groupMember(group.groupCode, member) as OB11User } } - throw ("查无此人") + throw new Error("查无此人") } } \ No newline at end of file diff --git a/src/onebot11/action/go-cqhttp/UploadGroupFile.ts b/src/onebot11/action/go-cqhttp/UploadGroupFile.ts index 4eca434..d010090 100644 --- a/src/onebot11/action/go-cqhttp/UploadGroupFile.ts +++ b/src/onebot11/action/go-cqhttp/UploadGroupFile.ts @@ -3,9 +3,9 @@ import {getGroup} from "../../../common/data"; import {ActionName} from "../types"; import {SendMsgElementConstructor} from "../../../ntqqapi/constructor"; import {ChatType, SendFileElement} from "../../../ntqqapi/types"; -import {uri2local} from "../../utils"; import fs from "fs"; import {NTQQMsgApi} from "../../../ntqqapi/api/msg"; +import {uri2local} from "../../../common/utils"; interface Payload{ group_id: number diff --git a/src/onebot11/action/index.ts b/src/onebot11/action/index.ts index af557be..8c65113 100644 --- a/src/onebot11/action/index.ts +++ b/src/onebot11/action/index.ts @@ -36,6 +36,8 @@ import GoCQHTTPUploadGroupFile from "./go-cqhttp/UploadGroupFile"; import {GetConfigAction, SetConfigAction} from "./llonebot/Config"; import GetGroupAddRequest from "./llonebot/GetGroupAddRequest"; import SetQQAvatar from './llonebot/SetQQAvatar' +import GoCQHTTPDownloadFile from "./go-cqhttp/DownloadFile"; +import GoCQHTTPGetGroupMsgHistory from "./go-cqhttp/GetGroupMsgHistory"; export const actionHandlers = [ new Debug(), @@ -72,9 +74,11 @@ export const actionHandlers = [ new GoCQHTTPSendGroupForwardMsg(), new GoCQHTTPSendPrivateForwardMsg(), new GoCQHTTPGetStrangerInfo(), + new GoCQHTTPDownloadFile(), new GetGuildList(), new GoCQHTTPMarkMsgAsRead(), new GoCQHTTPUploadGroupFile(), + new GoCQHTTPGetGroupMsgHistory(), ] diff --git a/src/onebot11/action/llonebot/SetQQAvatar.ts b/src/onebot11/action/llonebot/SetQQAvatar.ts index 7ea4785..04c90f6 100644 --- a/src/onebot11/action/llonebot/SetQQAvatar.ts +++ b/src/onebot11/action/llonebot/SetQQAvatar.ts @@ -1,9 +1,8 @@ import BaseAction from "../BaseAction"; import {ActionName} from "../types"; -import { uri2local } from "../../utils"; import * as fs from "node:fs"; import {NTQQUserApi} from "../../../ntqqapi/api/user"; -import {checkFileReceived} from "../../../common/utils/file"; +import {checkFileReceived, uri2local} from "../../../common/utils/file"; // import { log } from "../../../common/utils"; interface Payload { diff --git a/src/onebot11/action/types.ts b/src/onebot11/action/types.ts index 48db8fd..6ffedc0 100644 --- a/src/onebot11/action/types.ts +++ b/src/onebot11/action/types.ts @@ -54,4 +54,6 @@ export enum ActionName { GetGuildList = "get_guild_list", GoCQHTTP_MarkMsgAsRead = "mark_msg_as_read", GoCQHTTP_UploadGroupFile = "upload_group_file", + GoCQHTTP_DownloadFile = "download_file", + GoCQHTTP_GetGroupMsgHistory = "get_group_msg_history", } \ No newline at end of file diff --git a/src/onebot11/constructor.ts b/src/onebot11/constructor.ts index a9e258f..2d66b9c 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -305,9 +305,10 @@ export class OB11Constructor { return { user_id: parseInt(friend.uin), nickname: friend.nick, - remark: friend.remark + remark: friend.remark, + sex: OB11Constructor.sex(friend.sex), + qq_level: friend.qqLevel && calcQQLevel(friend.qqLevel) || 0 } - } static selfInfo(selfInfo: SelfInfo): OB11User { diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index b8db420..3ebce72 100644 --- a/src/onebot11/types.ts +++ b/src/onebot11/types.ts @@ -4,7 +4,9 @@ import {EventType} from "./event/OB11BaseEvent"; export interface OB11User { user_id: number; nickname: string; - remark?: string + remark?: string; + sex?: OB11UserSex; + qq_level?: number; } export enum OB11UserSex { diff --git a/src/onebot11/utils.ts b/src/onebot11/utils.ts deleted file mode 100644 index d60b258..0000000 --- a/src/onebot11/utils.ts +++ /dev/null @@ -1,130 +0,0 @@ -import {DATA_DIR} from "../common/utils"; -import {v4 as uuidv4} from "uuid"; -import * as path from 'node:path'; -import * as fileType from 'file-type'; -import {dbUtil} from "../common/db"; -import {isGIF} from "../common/utils/file"; -import {log} from "../common/utils/log"; - -const fs = require("fs").promises; - -type Uri2LocalRes = { - success: boolean, - errMsg: string, - fileName: string, - ext: string, - path: string, - isLocal: boolean -} - -export async function uri2local(uri: string, fileName: string = null) : Promise{ - let res = { - success: false, - errMsg: "", - fileName: "", - ext: "", - path: "", - isLocal: false - } - if (!fileName) { - fileName = uuidv4(); - } - let filePath = path.join(DATA_DIR, fileName) - let url = null; - try{ - url = new URL(uri); - }catch (e) { - res.errMsg = `uri ${uri} 解析失败,` + e.toString() + ` 可能${uri}不存在` - return res - } - - // log("uri protocol", url.protocol, uri); - if (url.protocol == "base64:") { - // base64转成文件 - let base64Data = uri.split("base64://")[1] - try { - const buffer = Buffer.from(base64Data, 'base64'); - await fs.writeFile(filePath, buffer); - - } catch (e: any) { - res.errMsg = `base64文件下载失败,` + e.toString() - return res - } - } else if (url.protocol == "http:" || url.protocol == "https:") { - // 下载文件 - let fetchRes: Response; - try{ - fetchRes = await fetch(url) - }catch (e) { - res.errMsg = `${url}下载失败` - return res - } - if (!fetchRes.ok) { - res.errMsg = `${url}下载失败,` + fetchRes.statusText - return res - } - let blob = await fetchRes.blob(); - let buffer = await blob.arrayBuffer(); - try { - const pathInfo = path.parse(decodeURIComponent(url.pathname)) - if (pathInfo.name){ - fileName = pathInfo.name - if (pathInfo.ext){ - fileName += pathInfo.ext - // res.ext = pathInfo.ext - } - } - res.fileName = fileName - filePath = path.join(DATA_DIR, uuidv4() + fileName) - await fs.writeFile(filePath, Buffer.from(buffer)); - } catch (e: any) { - res.errMsg = `${url}下载失败,` + e.toString() - return res - } - } else { - let pathname: string; - if (url.protocol === "file:") { - // await fs.copyFile(url.pathname, filePath); - pathname = decodeURIComponent(url.pathname) - if (process.platform === "win32") { - filePath = pathname.slice(1) - } else { - filePath = pathname - } - } else { - const cache = await dbUtil.getFileCache(uri); - if (cache) { - filePath = cache.filePath - } else { - filePath = uri; - } - } - - res.isLocal = true - } - // else{ - // res.errMsg = `不支持的file协议,` + url.protocol - // return res - // } - // if (isGIF(filePath) && !res.isLocal) { - // await fs.rename(filePath, filePath + ".gif"); - // filePath += ".gif"; - // } - if (!res.isLocal && !res.ext) { - try { - let ext: string = (await fileType.fileTypeFromFile(filePath)).ext - if (ext) { - log("获取文件类型", ext, filePath) - await fs.rename(filePath, filePath + `.${ext}`) - filePath += `.${ext}` - res.fileName += `.${ext}` - res.ext = ext - } - } catch (e) { - // log("获取文件类型失败", filePath,e.stack) - } - } - res.success = true - res.path = filePath - return res -} \ No newline at end of file diff --git a/src/preload.ts b/src/preload.ts index ed83aa0..ebdb027 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -30,7 +30,7 @@ const llonebot = { getConfig: async (): Promise => { return ipcRenderer.invoke(CHANNEL_GET_CONFIG); }, - getError: async (): Promise => { + getError: async (): Promise => { return ipcRenderer.invoke(CHANNEL_ERROR); }, selectFile: (): Promise => { diff --git a/src/renderer/index.ts b/src/renderer/index.ts index ed18b87..ba73a2b 100644 --- a/src/renderer/index.ts +++ b/src/renderer/index.ts @@ -1,11 +1,6 @@ /// -import { - SettingButton, - SettingItem, - SettingList, - SettingSelect, - SettingSwitch -} from './components'; +import { CheckVersion } from '../common/types'; +import {SettingButton, SettingItem, SettingList, SettingSwitch} from './components'; import StyleRaw from './style.css?raw'; // 打开设置界面时触发 @@ -14,18 +9,21 @@ async function onSettingWindowCreated(view: Element) { window.llonebot.log("setting window created"); const isEmpty = (value: any) => value === undefined || value === null || value === ''; let config = await window.llonebot.getConfig(); - let ob11Config = { ...config.ob11 }; + let ob11Config = {...config.ob11}; const setConfig = (key: string, value: any) => { const configKey = key.split('.'); if (key.indexOf('ob11') === 0) { + if (configKey[1] === "messagePostFormat") { + value = value ? "string" : "array" + } if (configKey.length === 2) ob11Config[configKey[1]] = value; else ob11Config[key] = value; } else { if (configKey.length === 2) config[configKey[0]][configKey[1]] = value; else config[key] = value; - if (!['heartInterval', 'token', 'ffmpeg'].includes(key)){ + if (!['heartInterval', 'token', 'ffmpeg'].includes(key)) { window.llonebot.setConfig(config); } } @@ -35,16 +33,29 @@ async function onSettingWindowCreated(view: Element) { const doc = parser.parseFromString([ '
', ``, + ` + + + + 正在检查LLOneBot版本中 + 请稍后 + + + + `, + SettingList([ + '
', + ]), SettingList([ SettingItem('启用 HTTP 服务', null, - SettingSwitch('ob11.enableHttp', config.ob11.enableHttp, { 'control-display-id': 'config-ob11-httpPort' }), + SettingSwitch('ob11.enableHttp', config.ob11.enableHttp, {'control-display-id': 'config-ob11-httpPort'}), ), SettingItem('HTTP 服务监听端口', null, `
`, 'config-ob11-httpPort', config.ob11.enableHttp ), SettingItem('启用 HTTP 事件上报', null, - SettingSwitch('ob11.enableHttpPost', config.ob11.enableHttpPost, { 'control-display-id': 'config-ob11-httpHosts' }), + SettingSwitch('ob11.enableHttpPost', config.ob11.enableHttpPost, {'control-display-id': 'config-ob11-httpHosts'}), ), `
@@ -56,14 +67,14 @@ async function onSettingWindowCreated(view: Element) {
`, SettingItem('启用正向 WebSocket 服务', null, - SettingSwitch('ob11.enableWs', config.ob11.enableWs, { 'control-display-id': 'config-ob11-wsPort' }), + SettingSwitch('ob11.enableWs', config.ob11.enableWs, {'control-display-id': 'config-ob11-wsPort'}), ), SettingItem('正向 WebSocket 服务监听端口', null, `
`, 'config-ob11-wsPort', config.ob11.enableWs ), SettingItem('启用反向 WebSocket 服务', null, - SettingSwitch('ob11.enableWsReverse', config.ob11.enableWsReverse, { 'control-display-id': 'config-ob11-wsHosts' }), + SettingSwitch('ob11.enableWsReverse', config.ob11.enableWsReverse, {'control-display-id': 'config-ob11-wsHosts'}), ), `
@@ -82,12 +93,13 @@ async function onSettingWindowCreated(view: Element) { `
`, ), SettingItem( - '消息上报格式类型', + '启用CQ码上报格式,不启用则为消息段格式', '如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 OneBot v11 文档', - SettingSelect([ - { text: '消息段', value: 'array' }, - { text: 'CQ码', value: 'string' }, - ], 'ob11.messagePostFormat', config.ob11.messagePostFormat), + // SettingSelect([ + // {text: '消息段', value: 'array'}, + // {text: 'CQ码', value: 'string'}, + // ], 'ob11.messagePostFormat', config.ob11.messagePostFormat), + SettingSwitch('ob11.messagePostFormat', config.ob11.messagePostFormat === "string"), ), SettingItem( 'ffmpeg 路径,发送语音、视频需要,同时保证ffprobe和ffmpeg在一起', ` 下载地址 , 路径:${!isEmpty(config.ffmpeg) ? config.ffmpeg : '未指定'}`, @@ -122,7 +134,7 @@ async function onSettingWindowCreated(view: Element) { SettingItem( '自动删除收到的文件', '在收到文件后的指定时间内删除该文件', - SettingSwitch('autoDeleteFile', config.autoDeleteFile, { 'control-display-id': 'config-auto-delete-file-second' }), + SettingSwitch('autoDeleteFile', config.autoDeleteFile, {'control-display-id': 'config-auto-delete-file-second'}), ), SettingItem( '自动删除文件时间', @@ -166,6 +178,18 @@ async function onSettingWindowCreated(view: Element) { '
', ].join(''), "text/html"); + let errorEle = doc.querySelector("#llonebot-error"); + errorEle.style.display = 'none'; + const showError = async () => { + setTimeout(async () => { + let errMessage = await window.llonebot.getError(); + console.log(errMessage) + errMessage = errMessage.replace(/\n/g, '
') + errorEle.innerHTML = errMessage; + errorEle.style.display = errMessage ? 'flex' : 'none'; + }, 1000) + } + showError().then() // 外链按钮 doc.querySelector('#open-github').addEventListener('click', () => { window.LiteLoader.api.openExternal('https://github.com/LLOneBot/LLOneBot') @@ -180,7 +204,7 @@ async function onSettingWindowCreated(view: Element) { window.LiteLoader.api.openExternal('https://llonebot.github.io/') }) // 生成反向地址列表 - const buildHostListItem = (type: string, host: string, index: number, inputAttrs: any={}) => { + const buildHostListItem = (type: string, host: string, index: number, inputAttrs: any = {}) => { const dom = { container: document.createElement('setting-item'), input: document.createElement('input'), @@ -212,23 +236,23 @@ async function onSettingWindowCreated(view: Element) { return dom.container; }; - const buildHostList = (hosts: string[], type: string, inputAttr: any={}) => { + const buildHostList = (hosts: string[], type: string, inputAttr: any = {}) => { const result: HTMLElement[] = []; - + hosts.forEach((host, index) => { result.push(buildHostListItem(type, host, index, inputAttr)); }); - + return result; }; - const addReverseHost = (type: string, doc: Document = document, inputAttr: any={}) => { + const addReverseHost = (type: string, doc: Document = document, inputAttr: any = {}) => { const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`); hostContainerDom.appendChild(buildHostListItem(type, '', ob11Config[type].length, inputAttr)); ob11Config[type].push(''); }; const initReverseHost = (type: string, doc: Document = document) => { const hostContainerDom = doc.body.querySelector(`#config-ob11-${type}-list`); - [ ...hostContainerDom.childNodes ].forEach(dom => dom.remove()); + [...hostContainerDom.childNodes].forEach(dom => dom.remove()); buildHostList(ob11Config[type], type).forEach(dom => { hostContainerDom.appendChild(dom); }); @@ -236,8 +260,8 @@ async function onSettingWindowCreated(view: Element) { initReverseHost('httpHosts', doc); initReverseHost('wsHosts', doc); - doc.querySelector('#config-ob11-httpHosts-add').addEventListener('click', () => addReverseHost('httpHosts', document, {'placeholder': '如:http://127.0.0.1:5140/onebot' })); - doc.querySelector('#config-ob11-wsHosts-add').addEventListener('click', () => addReverseHost('wsHosts', document, {'placeholder': '如:ws://127.0.0.1:5140/onebot' })); + doc.querySelector('#config-ob11-httpHosts-add').addEventListener('click', () => addReverseHost('httpHosts', document, {'placeholder': '如:http://127.0.0.1:5140/onebot'})); + doc.querySelector('#config-ob11-wsHosts-add').addEventListener('click', () => addReverseHost('wsHosts', document, {'placeholder': '如:ws://127.0.0.1:5140/onebot'})); doc.querySelector('#config-ffmpeg-select').addEventListener('click', () => { window.llonebot.selectFile() @@ -297,27 +321,62 @@ async function onSettingWindowCreated(view: Element) { config.ob11 = ob11Config; window.llonebot.setConfig(config); + // window.location.reload(); + showError().then() alert('保存成功'); }); doc.body.childNodes.forEach(node => { view.appendChild(node); }); + // 更新逻辑 + async function checkVersionFunc(ResultVersion: CheckVersion) { + console.log(ResultVersion); + if (ResultVersion.version === "") { + view.querySelector(".llonebot-update-title").innerHTML = "检查更新失败"; + view.querySelector(".llonebot-update-button").innerHTML = "点击重试"; + view.querySelector(".llonebot-update-button").addEventListener("click", async () => { + window.llonebot.checkVersion().then(checkVersionFunc); + }); + return; + } + if (ResultVersion.result) { + view.querySelector(".llonebot-update-title").innerHTML = "当前已是最新版本 V" + ResultVersion.version; + view.querySelector(".llonebot-update-button").innerHTML = "无需更新"; + } else { + view.querySelector(".llonebot-update-title").innerHTML = "已检测到最新版本 V" + ResultVersion.version; + view.querySelector(".llonebot-update-button").innerHTML = "点击更新"; + const update = async () => { + view.querySelector(".llonebot-update-button").innerHTML = "正在更新中..."; + const result = await window.llonebot.updateLLOneBot(); + if (result) { + view.querySelector(".llonebot-update-button").innerHTML = "更新完成请重启"; + view.querySelector(".llonebot-update-button").removeEventListener("click", update); + } else { + view.querySelector(".llonebot-update-button").innerHTML = "更新失败前往仓库下载"; + view.querySelector(".llonebot-update-button").removeEventListener("click", update); + } + } + view.querySelector(".llonebot-update-button").addEventListener("click", update); + } + }; + window.llonebot.checkVersion().then(checkVersionFunc); + } -function init () { - const hash = location.hash - if (hash === '#/blank') { +function init() { + const hash = location.hash + if (hash === '#/blank') { - } + } } if (location.hash === '#/blank') { - (window as any).navigation.addEventListener('navigatesuccess', init, { once: true }) + (window as any).navigation.addEventListener('navigatesuccess', init, {once: true}) } else { - init() + init() } export { - onSettingWindowCreated + onSettingWindowCreated } diff --git a/src/renderer/style.css b/src/renderer/style.css index beca5d6..d09bf4f 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -154,4 +154,10 @@ ob-setting-select::part(option-list) { flex-wrap: nowrap; justify-content: flex-start; gap: 4px; -} \ No newline at end of file + +#llonebot-error{ + color: red; + height: 100px; + overflow: visible; + display: flex; + align-items: center; diff --git a/src/version.ts b/src/version.ts index 42fc3ff..93bafb5 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const version = "3.16.1" \ No newline at end of file +export const version = "3.16.0" \ No newline at end of file