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/config.ts b/src/common/config.ts index 39269cb..d8336e2 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -6,7 +6,7 @@ import path from "node:path"; import {selfInfo} from "./data"; import {DATA_DIR} from "./utils"; -export const HOOK_LOG = false; +export const HOOK_LOG = true; export const ALLOW_SEND_TEMP_MSG = false; 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/db.ts b/src/common/db.ts index 8dbc859..5038c46 100644 --- a/src/common/db.ts +++ b/src/common/db.ts @@ -222,14 +222,14 @@ class DBUtil { return this.currentShortId; } - async addFileCache(fileName: string, data: FileCache) { - const key = this.DB_KEY_PREFIX_FILE + fileName; + async addFileCache(fileNameOrUuid: string, data: FileCache) { + const key = this.DB_KEY_PREFIX_FILE + fileNameOrUuid; if (this.cache[key]) { return } let cacheDBData = {...data} delete cacheDBData['downloadFunc'] - this.cache[fileName] = data; + this.cache[fileNameOrUuid] = data; try { await this.db.put(key, JSON.stringify(cacheDBData)); } catch (e) { @@ -237,8 +237,8 @@ class DBUtil { } } - async getFileCache(fileName: string): Promise { - const key = this.DB_KEY_PREFIX_FILE + fileName; + async getFileCache(fileNameOrUuid: string): Promise { + const key = this.DB_KEY_PREFIX_FILE + (fileNameOrUuid); if (this.cache[key]) { return this.cache[key] as FileCache } 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..d73af45 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 } @@ -36,6 +38,8 @@ export interface FileCache { fileName: string filePath: string fileSize: string + fileUuid?: string url?: string + msgId?: string downloadFunc?: () => Promise } diff --git a/src/common/utils/file.ts b/src/common/utils/file.ts index f473aef..530cc43 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"; + 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); } }); } @@ -182,61 +191,7 @@ export async function encodeSilk(filePath: string) { } } -export async function getVideoInfo(filePath: string) { - const size = fs.statSync(filePath).size; - return new Promise<{ - width: number, - height: number, - time: number, - format: string, - size: number, - filePath: string - }>((resolve, reject) => { - ffmpeg(filePath).ffprobe((err, metadata) => { - if (err) { - reject(err); - } else { - const videoStream = metadata.streams.find(s => s.codec_type === 'video'); - if (videoStream) { - console.log(`视频尺寸: ${videoStream.width}x${videoStream.height}`); - } else { - console.log('未找到视频流信息。'); - } - resolve({ - width: videoStream.width, height: videoStream.height, - time: parseInt(videoStream.duration), - format: metadata.format.format_name, - size, - filePath - }); - } - }); - }) -} -export async function encodeMp4(filePath: string) { - let videoInfo = await getVideoInfo(filePath); - log("视频信息", videoInfo) - if (videoInfo.format.indexOf("mp4") === -1) { - log("视频需要转换为MP4格式", filePath) - // 转成mp4 - const newPath: string = await new Promise((resolve, reject) => { - const newPath = filePath + ".mp4" - ffmpeg(filePath) - .toFormat('mp4') - .on('error', (err) => { - reject(`转换视频格式失败: ${err.message}`); - }) - .on('end', () => { - log('视频转换为MP4格式完成'); - resolve(newPath); // 返回转换后的文件路径 - }) - .save(newPath); - }); - return await getVideoInfo(newPath) - } - return videoInfo -} export function calculateFileMD5(filePath: string): Promise { return new Promise((resolve, reject) => { @@ -260,4 +215,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..5898e52 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -1,10 +1,16 @@ +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); +} +export {getVideoInfo} from "./video"; \ 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/common/utils/video.ts b/src/common/utils/video.ts new file mode 100644 index 0000000..83a5f20 --- /dev/null +++ b/src/common/utils/video.ts @@ -0,0 +1,63 @@ +import {log} from "./log"; +import ffmpeg from "fluent-ffmpeg"; +import fs from "fs"; + +const defaultVideoThumbB64 = "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" + +export const defaultVideoThumb = Buffer.from(defaultVideoThumbB64, 'base64') + +export async function getVideoInfo(filePath: string) { + const size = fs.statSync(filePath).size; + return new Promise<{ + width: number, + height: number, + time: number, + format: string, + size: number, + filePath: string + }>((resolve, reject) => { + ffmpeg(filePath).ffprobe((err, metadata) => { + if (err) { + reject(err); + } else { + const videoStream = metadata.streams.find(s => s.codec_type === 'video'); + if (videoStream) { + console.log(`视频尺寸: ${videoStream.width}x${videoStream.height}`); + } else { + console.log('未找到视频流信息。'); + } + resolve({ + width: videoStream.width, height: videoStream.height, + time: parseInt(videoStream.duration), + format: metadata.format.format_name, + size, + filePath + }); + } + }); + }) +} + +export async function encodeMp4(filePath: string) { + let videoInfo = await getVideoInfo(filePath); + log("视频信息", videoInfo) + if (videoInfo.format.indexOf("mp4") === -1) { + log("视频需要转换为MP4格式", filePath) + // 转成mp4 + const newPath: string = await new Promise((resolve, reject) => { + const newPath = filePath + ".mp4" + ffmpeg(filePath) + .toFormat('mp4') + .on('error', (err) => { + reject(`转换视频格式失败: ${err.message}`); + }) + .on('end', () => { + log('视频转换为MP4格式完成'); + resolve(newPath); // 返回转换后的文件路径 + }) + .save(newPath); + }); + return await getVideoInfo(newPath) + } + return videoInfo +} \ No newline at end of file diff --git a/src/main/main.ts b/src/main/main.ts index 15da2d2..d16b8c1 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/file.ts b/src/ntqqapi/api/file.ts index cdd6335..76016ef 100644 --- a/src/ntqqapi/api/file.ts +++ b/src/ntqqapi/api/file.ts @@ -4,7 +4,8 @@ import { CacheFileListItem, CacheFileType, CacheScanResult, - ChatCacheList, ChatCacheListItemBasic, + ChatCacheList, + ChatCacheListItemBasic, ChatType, ElementType } from "../types"; @@ -13,12 +14,13 @@ import fs from "fs"; import {ReceiveCmdS} from "../hook"; import {log} from "../../common/utils/log"; -export class NTQQFileApi{ +export class NTQQFileApi { static async getFileType(filePath: string) { return await callNTQQApi<{ ext: string }>({ className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_TYPE, args: [filePath] }) } + static async getFileMd5(filePath: string) { return await callNTQQApi({ className: NTQQApiClass.FS_API, @@ -26,6 +28,7 @@ export class NTQQFileApi{ args: [filePath] }) } + static async copyFile(filePath: string, destPath: string) { return await callNTQQApi({ className: NTQQApiClass.FS_API, @@ -36,11 +39,13 @@ export class NTQQFileApi{ }] }) } + static async getFileSize(filePath: string) { return await callNTQQApi({ className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.FILE_SIZE, args: [filePath] }) } + // 上传文件到QQ的文件夹 static async uploadFile(filePath: string, elementType: ElementType = ElementType.PIC) { const md5 = await NTQQFileApi.getFileMd5(filePath); @@ -79,14 +84,18 @@ export class NTQQFileApi{ fileSize } } - static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string) { + + static async downloadMedia(msgId: string, chatType: ChatType, peerUid: string, elementId: string, thumbPath: string, sourcePath: string, isFile: boolean = false) { // 用于下载收到的消息中的图片等 - if (fs.existsSync(sourcePath)) { + if (sourcePath && fs.existsSync(sourcePath)) { return sourcePath } const apiParams = [ { getReq: { + fileModelId: "0", + downloadSourceType: 0, + triggerType: 1, msgId: msgId, chatType: chatType, peerUid: peerUid, @@ -96,20 +105,21 @@ export class NTQQFileApi{ filePath: thumbPath, }, }, - undefined, + null, ] // log("需要下载media", sourcePath); await callNTQQApi({ methodName: NTQQApiMethod.DOWNLOAD_MEDIA, args: apiParams, cbCmd: ReceiveCmdS.MEDIA_DOWNLOAD_COMPLETE, - cmdCB: (payload: { notifyInfo: { filePath: string } }) => { - // log("media 下载完成判断", payload.notifyInfo.filePath, sourcePath); - return payload.notifyInfo.filePath == sourcePath; + cmdCB: (payload: { notifyInfo: { filePath: string, msgId: string } }) => { + log("media 下载完成判断", payload.notifyInfo.msgId, msgId); + return payload.notifyInfo.msgId == msgId; } }) return sourcePath } + static async getImageSize(filePath: string) { return await callNTQQApi<{ width: number, height: number }>({ className: NTQQApiClass.FS_API, methodName: NTQQApiMethod.IMAGE_SIZE, args: [filePath] @@ -118,7 +128,7 @@ export class NTQQFileApi{ } -export class NTQQFileCacheApi{ +export class NTQQFileCacheApi { static async setCacheSilentScan(isSilent: boolean = true) { return await callNTQQApi({ methodName: NTQQApiMethod.CACHE_SET_SILENCE, @@ -127,6 +137,7 @@ export class NTQQFileCacheApi{ }, null] }); } + static getCacheSessionPathList() { return callNTQQApi<{ key: string, @@ -136,6 +147,7 @@ export class NTQQFileCacheApi{ methodName: NTQQApiMethod.CACHE_PATH_SESSION, }); } + static clearCache(cacheKeys: Array = ['tmp', 'hotUpdate']) { return callNTQQApi({ // TODO: 目前还不知道真正的返回值是什么 methodName: NTQQApiMethod.CACHE_CLEAR, @@ -144,6 +156,7 @@ export class NTQQFileCacheApi{ }, null] }); } + static addCacheScannedPaths(pathMap: object = {}) { return callNTQQApi({ methodName: NTQQApiMethod.CACHE_ADD_SCANNED_PATH, @@ -152,6 +165,7 @@ export class NTQQFileCacheApi{ }, null] }); } + static scanCache() { callNTQQApi({ methodName: ReceiveCmdS.CACHE_SCAN_FINISH, @@ -163,6 +177,7 @@ export class NTQQFileCacheApi{ timeoutSecond: 300, }); } + static getHotUpdateCachePath() { return callNTQQApi({ className: NTQQApiClass.HOTUPDATE_API, @@ -176,6 +191,7 @@ export class NTQQFileCacheApi{ methodName: NTQQApiMethod.CACHE_PATH_DESKTOP_TEMP }); } + static getChatCacheList(type: ChatType, pageSize: number = 1000, pageIndex: number = 0) { return new Promise((res, rej) => { callNTQQApi({ @@ -190,6 +206,7 @@ export class NTQQFileCacheApi{ .catch(e => rej(e)); }); } + static getFileCacheInfo(fileType: CacheFileType, pageSize: number = 1000, lastRecord?: CacheFileListItem) { const _lastRecord = lastRecord ? lastRecord : {fileType: fileType}; @@ -204,6 +221,7 @@ export class NTQQFileCacheApi{ }, null] }) } + static async clearChatCache(chats: ChatCacheListItemBasic[] = [], fileKeys: string[] = []) { return await callNTQQApi({ methodName: NTQQApiMethod.CACHE_CHAT_CLEAR, 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/constructor.ts b/src/ntqqapi/constructor.ts index 679f173..57d4c23 100644 --- a/src/ntqqapi/constructor.ts +++ b/src/ntqqapi/constructor.ts @@ -14,9 +14,9 @@ import { import {promises as fs} from "node:fs"; import ffmpeg from "fluent-ffmpeg" import {NTQQFileApi} from "./api/file"; -import {calculateFileMD5, encodeSilk, getVideoInfo, isGIF} from "../common/utils/file"; +import {calculateFileMD5, encodeSilk, isGIF} from "../common/utils/file"; import {log} from "../common/utils/log"; -import {sleep} from "../common/utils/helper"; +import {defaultVideoThumb, getVideoInfo} from "../common/utils/video"; export class SendMsgElementConstructor { @@ -108,29 +108,45 @@ export class SendMsgElementConstructor { return element; } - static async video(filePath: string, fileName: string = ""): Promise { + static async video(filePath: string, fileName: string = "", diyThumbPath: string = ""): Promise { let {fileName: _fileName, path, fileSize, md5} = await NTQQFileApi.uploadFile(filePath, ElementType.VIDEO); if (fileSize === 0) { throw "文件异常,大小为0"; } - // const videoInfo = await encodeMp4(path); - // path = videoInfo.filePath - // md5 = videoInfo.md5; - // fileSize = videoInfo.size; - // log("上传视频", md5, path, fileSize, fileName || _fileName) const pathLib = require("path"); let thumb = path.replace(`${pathLib.sep}Ori${pathLib.sep}`, `${pathLib.sep}Thumb${pathLib.sep}`) thumb = pathLib.dirname(thumb) // log("thumb 目录", thumb) - const videoInfo = await getVideoInfo(path); - log("视频信息", videoInfo) + let videoInfo ={ + width: 1920, height: 1080, + time: 15, + format: "mp4", + size: fileSize, + filePath + }; + try { + videoInfo = await getVideoInfo(path); + log("视频信息", videoInfo) + }catch (e) { + log("获取视频信息失败", e) + } const createThumb = new Promise((resolve, reject) => { const thumbFileName = `${md5}_0.png` + const thumbPath = pathLib.join(thumb, thumbFileName) ffmpeg(filePath) .on("end", () => { }) .on("error", (err) => { - reject(err); + log("获取视频封面失败,使用默认封面", err) + if (diyThumbPath) { + fs.copyFile(diyThumbPath, thumbPath).then(() => { + resolve(thumbPath); + }).catch(reject) + } else { + fs.writeFile(thumbPath, defaultVideoThumb).then(() => { + resolve(thumbPath); + }).catch(reject) + } }) .screenshots({ timestamps: [0], @@ -138,7 +154,7 @@ export class SendMsgElementConstructor { folder: thumb, size: videoInfo.width + "x" + videoInfo.height }).on("end", () => { - resolve(pathLib.join(thumb, thumbFileName)); + resolve(thumbPath); }); }) let thumbPath = new Map() @@ -225,7 +241,11 @@ export class SendMsgElementConstructor { return { elementType: ElementType.ARK, elementId: "", - arkElement: data + arkElement: { + bytesData: data, + linkInfo: null, + subElementType: null + } } } } \ No newline at end of file diff --git a/src/ntqqapi/hook.ts b/src/ntqqapi/hook.ts index 8d403b5..d9139db 100644 --- a/src/ntqqapi/hook.ts +++ b/src/ntqqapi/hook.ts @@ -125,6 +125,27 @@ export function hookNTQQApiCall(window: BrowserWindow) { } else { webContents._events["-ipc-message"] = proxyIpcMsg; } + + const ipc_invoke_proxy = webContents._events["-ipc-invoke"]?.[0] || webContents._events["-ipc-invoke"]; + const proxyIpcInvoke = new Proxy(ipc_invoke_proxy, { + apply(target, thisArg, args) { + // console.log(args); + HOOK_LOG && log("call NTQQ invoke api", thisArg, args) + args[0]["_replyChannel"]["sendReply"] = new Proxy(args[0]["_replyChannel"]["sendReply"], { + apply(sendtarget, sendthisArg, sendargs) { + sendtarget.apply(sendthisArg, sendargs); + } + }); + let ret = target.apply(thisArg, args); + HOOK_LOG && log("call NTQQ invoke api return", ret) + return ret; + } + }); + if (webContents._events["-ipc-invoke"]?.[0]) { + webContents._events["-ipc-invoke"][0] = proxyIpcInvoke; + } else { + webContents._events["-ipc-invoke"] = proxyIpcInvoke; + } } export function registerReceiveHook(method: ReceiveCmd | ReceiveCmd[], hookFunc: (payload: PayloadType) => void): string { 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..dee07f5 100644 --- a/src/onebot11/action/GetFile.ts +++ b/src/onebot11/action/GetFile.ts @@ -2,9 +2,12 @@ import BaseAction from "./BaseAction"; import fs from "fs/promises"; import {dbUtil} from "../../common/db"; import {getConfigUtil} from "../../common/config"; +import {log, sleep, uri2local} from "../../common/utils"; +import {NTQQFileApi} from "../../ntqqapi/api/file"; +import {ActionName} from "./types"; export interface GetFilePayload { - file: string // 文件名 + file: string // 文件名或者fileUuid } export interface GetFileResponse { @@ -26,6 +29,42 @@ 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) + if (cache.url){ + 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) + } + } + else{ + // 没有url的可能是私聊文件或者群文件,需要自己下载 + log("需要调用 NTQQ 下载文件api") + if (cache.msgId) { + let msg = await dbUtil.getMsgByLongId(cache.msgId) + if (msg){ + log("找到了文件 msg", msg) + const element = msg.elements.find(e=>e.fileElement) + log("找到了文件 element", element); + // 构建下载函数 + await NTQQFileApi.downloadMedia(msg.msgId, msg.chatType, msg.peerUid, + element.elementId, "", "", true) + await sleep(1000); + msg = await dbUtil.getMsgByLongId(cache.msgId) + log("下载完成后的msg", msg) + cache.filePath = msg?.elements.find(e=>e.fileElement)?.fileElement?.filePath + dbUtil.addFileCache(payload.file, cache).then() + } + + } + } + + } let res: GetFileResponse = { file: cache.filePath, url: cache.url, @@ -34,14 +73,30 @@ export class GetFileBase extends BaseAction { } if (enableLocalFile2Url) { if (!cache.url) { - res.base64 = await fs.readFile(cache.filePath, 'base64') + try{ + res.base64 = await fs.readFile(cache.filePath, 'base64') + }catch (e) { + throw new Error("文件下载失败. " + e) + } } } - if (autoDeleteFile) { - setTimeout(() => { - fs.unlink(cache.filePath) - }, autoDeleteFileSecond * 1000) - } + // if (autoDeleteFile) { + // setTimeout(() => { + // fs.unlink(cache.filePath) + // }, autoDeleteFileSecond * 1000) + // } return res } +} + +export default class GetFile extends GetFileBase { + actionName = ActionName.GetFile + + protected async _handle(payload: {file_id: string, file: string}): Promise { + if (!payload.file_id) { + throw new Error('file_id 不能为空') + } + payload.file = payload.file_id + return super._handle(payload); + } } \ No newline at end of file diff --git a/src/onebot11/action/SendMsg.ts b/src/onebot11/action/SendMsg.ts index b7eb2fa..b941ce1 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 { @@ -430,7 +430,14 @@ export class SendMsg extends BaseAction { sendElements.push(await SendMsgElementConstructor.file(path, payloadFileName || fileName)); } else if (sendMsg.type === OB11MessageDataType.video) { log("发送视频", path, payloadFileName || fileName) - sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName)); + let thumb = sendMsg.data?.thumb; + if (thumb){ + let uri2LocalRes = await uri2local(thumb) + if (uri2LocalRes.success){ + thumb = uri2LocalRes.path; + } + } + sendElements.push(await SendMsgElementConstructor.video(path, payloadFileName || fileName, thumb)); } else if (sendMsg.type === OB11MessageDataType.voice) { sendElements.push(await SendMsgElementConstructor.ptt(path)); }else if (sendMsg.type === OB11MessageDataType.image) { @@ -438,8 +445,10 @@ export class SendMsg extends BaseAction { } } } - } - break; + } break; + case OB11MessageDataType.json: { + sendElements.push(SendMsgElementConstructor.ark(sendMsg.data.data)) + }break } } 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..dff68b4 100644 --- a/src/onebot11/action/index.ts +++ b/src/onebot11/action/index.ts @@ -36,8 +36,12 @@ 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"; +import GetFile from "./GetFile"; export const actionHandlers = [ + new GetFile(), new Debug(), new GetConfigAction(), new SetConfigAction(), @@ -72,9 +76,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..5985b1b 100644 --- a/src/onebot11/action/types.ts +++ b/src/onebot11/action/types.ts @@ -14,11 +14,14 @@ export interface InvalidCheckResult { } export enum ActionName { + // llonebot GetGroupIgnoreAddRequest = "get_group_ignore_add_request", SetQQAvatar = "set_qq_avatar", GetConfig = "get_config", SetConfig = "set_config", Debug = "llonebot_debug", + GetFile = "get_file", + // onebot 11 SendLike = "send_like", GetLoginInfo = "get_login_info", GetFriendList = "get_friend_list", @@ -54,4 +57,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..fa223c1 100644 --- a/src/onebot11/constructor.ts +++ b/src/onebot11/constructor.ts @@ -16,7 +16,8 @@ import { GroupMember, IMAGE_HTTP_HOST, RawMessage, - SelfInfo, Sex, + SelfInfo, + Sex, TipGroupElementType, User } from '../ntqqapi/types'; @@ -174,10 +175,12 @@ export class OB11Constructor { message_data["type"] = OB11MessageDataType.file; message_data["data"]["file"] = element.fileElement.fileName // message_data["data"]["path"] = element.fileElement.filePath - // message_data["data"]["file_id"] = element.fileElement.fileUuid + message_data["data"]["file_id"] = element.fileElement.fileUuid message_data["data"]["file_size"] = element.fileElement.fileSize - dbUtil.addFileCache(element.fileElement.fileName, { + dbUtil.addFileCache(element.fileElement.fileUuid, { + msgId: msg.msgId, fileName: element.fileElement.fileName, + fileUuid: element.fileElement.fileUuid, filePath: element.fileElement.filePath, fileSize: element.fileElement.fileSize, downloadFunc: async () => { @@ -251,18 +254,16 @@ export class OB11Constructor { // log("构造群增加事件", event) return event; } - } - else if (groupElement.type === TipGroupElementType.ban) { + } else if (groupElement.type === TipGroupElementType.ban) { log("收到群群员禁言提示", groupElement) const memberUid = groupElement.shutUp.member.uid const adminUid = groupElement.shutUp.admin.uid let memberUin: string = "" let duration = parseInt(groupElement.shutUp.duration) let sub_type: "ban" | "lift_ban" = duration > 0 ? "ban" : "lift_ban" - if (memberUid){ + if (memberUid) { memberUin = (await getGroupMember(msg.peerUid, memberUid))?.uin || (await NTQQUserApi.getUserDetailInfo(memberUid))?.uin - } - else { + } else { memberUin = "0"; // 0表示全员禁言 if (duration > 0) { duration = -1 @@ -273,16 +274,19 @@ export class OB11Constructor { return new OB11GroupBanEvent(parseInt(msg.peerUid), parseInt(memberUin), parseInt(adminUin), duration, sub_type); } } - } - else if (element.fileElement){ - return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), {id: element.fileElement.fileUuid, name: element.fileElement.fileName, size: parseInt(element.fileElement.fileSize)}) + } else if (element.fileElement) { + return new OB11GroupUploadNoticeEvent(parseInt(msg.peerUid), parseInt(msg.senderUin), { + id: element.fileElement.fileUuid, + name: element.fileElement.fileName, + size: parseInt(element.fileElement.fileSize) + }) } if (grayTipElement) { - if (grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER){ + if (grayTipElement.subElementType == GrayTipElementSubType.INVITE_NEW_MEMBER) { log("收到新人被邀请进群消息", grayTipElement) const xmlElement = grayTipElement.xmlElement - if (xmlElement?.content){ + if (xmlElement?.content) { const regex = /jp="(\d+)"/g; let matches = []; @@ -291,7 +295,7 @@ export class OB11Constructor { while ((match = regex.exec(xmlElement.content)) !== null) { matches.push(match[1]); } - if (matches.length === 2){ + if (matches.length === 2) { const [inviter, invitee] = matches; return new OB11GroupIncreaseEvent(parseInt(msg.peerUid), parseInt(invitee), parseInt(inviter), "invite"); } @@ -305,9 +309,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 { @@ -329,7 +334,7 @@ export class OB11Constructor { }[role] } - static sex(sex: Sex): OB11UserSex{ + static sex(sex: Sex): OB11UserSex { const sexMap = { [Sex.male]: OB11UserSex.male, [Sex.female]: OB11UserSex.female, @@ -337,6 +342,7 @@ export class OB11Constructor { } return sexMap[sex] || OB11UserSex.unknown } + static groupMember(group_id: string, member: GroupMember): OB11GroupMember { return { group_id: parseInt(group_id), diff --git a/src/onebot11/types.ts b/src/onebot11/types.ts index b8db420..db7bdf5 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 { @@ -115,6 +117,7 @@ export interface OB11MessageText { interface OB11MessageFileBase { data: { + thumb?: string; name?: string; file: string, url?: string; @@ -185,12 +188,17 @@ export interface OB11MessageCustomMusic{ } } +export interface OB11MessageJson { + type: OB11MessageDataType.json + data: {config: {token: string}} & any +} + export type OB11MessageData = OB11MessageText | OB11MessageFace | OB11MessageAt | OB11MessageReply | OB11MessageImage | OB11MessageRecord | OB11MessageFile | OB11MessageVideo | - OB11MessageNode | OB11MessageCustomMusic + OB11MessageNode | OB11MessageCustomMusic | OB11MessageJson export interface OB11PostSendMsg { message_type?: "private" | "group" 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/components/select.ts b/src/renderer/components/select.ts index 0c33332..ef2403a 100644 --- a/src/renderer/components/select.ts +++ b/src/renderer/components/select.ts @@ -1,11 +1,77 @@ import { SettingOption } from "./option"; +interface MouseEventExtend extends MouseEvent { + target: HTMLElement, +} + +// +const SelectTemplate = document.createElement('template'); +SelectTemplate.innerHTML = ` +
+
+ + + + +
+ +
`; + +window.customElements.define('ob-setting-select', class extends HTMLElement { + readonly _button: HTMLDivElement; + readonly _text: HTMLInputElement; + readonly _context: HTMLUListElement; + + constructor() { + super(); + + this.attachShadow({ mode: 'open' }); + this.shadowRoot.append(SelectTemplate.content.cloneNode(true)); + + this._button = this.shadowRoot.querySelector('div[part="button"]'); + this._text = this.shadowRoot.querySelector('input[part="current-text"]'); + this._context = this.shadowRoot.querySelector('ul[part="option-list"]'); + + const buttonClick = () => { + const isHidden = this._context.classList.toggle('hidden'); + window[`${isHidden ? 'remove': 'add'}EventListener`]('pointerdown', windowPointerDown); + }; + + const windowPointerDown = ({ target }) => { + if (!this.contains(target)) buttonClick(); + }; + + this._button.addEventListener('click', buttonClick); + this._context.addEventListener('click', ({ target }: MouseEventExtend) => { + if (target.tagName !== 'SETTING-OPTION') return; + buttonClick(); + + if (target.hasAttribute('is-selected')) return; + + this.querySelectorAll('setting-option[is-selected]').forEach(dom => dom.toggleAttribute('is-selected')); + target.toggleAttribute('is-selected'); + + this._text.value = target.textContent; + this.dispatchEvent(new CustomEvent('selected', { + bubbles: true, + composed: true, + detail: { + name: target.textContent, + value: target.dataset.value, + }, + })); + }); + + this._text.value = this.querySelector('setting-option[is-selected]').textContent; + } +}); + export const SettingSelect = (items: Array<{ text: string, value: string }>, configKey?: string, configValue?: any) => { - return ` -
- ${items.map((e, i) => { - return SettingOption(e.text, e.value, (configKey && configValue ? configValue === e.value : i === 0)); - }).join('')} -
-
`; + return ` + ${items.map((e, i) => { + return SettingOption(e.text, e.value, (configKey && configValue ? configValue === e.value : i === 0)); + }).join('')} +`; } \ No newline at end of file diff --git a/src/renderer/index.ts b/src/renderer/index.ts index c16f12b..29ad76b 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, SettingSelect} from './components'; import StyleRaw from './style.css?raw'; // 打开设置界面时触发 @@ -14,7 +9,7 @@ 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('.'); @@ -25,7 +20,7 @@ async function onSettingWindowCreated(view: Element) { 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 +30,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 +64,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,11 +90,11 @@ async function onSettingWindowCreated(view: Element) { `
`, ), SettingItem( - '消息上报格式类型', + '启用CQ码上报格式,不启用则为消息段格式', '如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 OneBot v11 文档', SettingSelect([ - { text: '消息段', value: 'array' }, - { text: 'CQ码', value: 'string' }, + {text: '消息段', value: 'array'}, + {text: 'CQ码', value: 'string'}, ], 'ob11.messagePostFormat', config.ob11.messagePostFormat), ), SettingItem( @@ -122,7 +130,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 +174,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 +200,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 +232,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 +256,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() @@ -283,7 +303,7 @@ async function onSettingWindowCreated(view: Element) { }); // 下拉框 - doc.querySelectorAll('setting-select').forEach((dom: HTMLElement) => { + doc.querySelectorAll('ob-setting-select[data-config-key]').forEach((dom: HTMLElement) => { dom.addEventListener('selected', (e: CustomEvent) => { const configKey = dom.dataset.configKey; const configValue = e.detail.value; @@ -297,27 +317,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 028aa96..842b702 100644 --- a/src/renderer/style.css +++ b/src/renderer/style.css @@ -61,4 +61,105 @@ setting-item a:hover { setting-item a:active, setting-item a:visited { color: var(--text-link); -} \ No newline at end of file +} + +ob-setting-select { + width: 100px; +} + +ob-setting-select, +ob-setting-select::part(parent), +ob-setting-select::part(button) { + display: block; + position: relative; + height: 24px; + font-size: 12px; + line-height: 24px; + box-sizing: border-box; +} + +ob-setting-select::part(button) { + display: flex; + padding: 0px 8px; + background-color: transparent; + border-radius: 4px; + border: 1px solid var(--border_dark); + z-index: 5; + cursor: default; + align-items: center; + flex-direction: row; + flex-wrap: nowrap; +} + +ob-setting-select::part(current-text) { + display: block; + margin-right: 8px; + padding: 0px; + background: none; + background-color: transparent; + font-size: 12px; + color: var(--text_primary); + text-overflow: ellipsis; + border-radius: 0px; + border: none; + outline: none; + overflow: hidden; + appearance: none; + box-sizing: border-box; + cursor: default; + flex: 1; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; + -webkit-pointer-events: none; + -moz-pointer-events: none; + -ms-pointer-events: none; + -o-pointer-events: none; + pointer-events: none; +} + +ob-setting-select::part(button-arrow) { + position: relative; + display: block; + width: 16px; + height: 16px; + color: var(--icon_primary); +} + +ob-setting-select::part(option-list) { + display: flex; + position: absolute; + top: 100%; + padding: 4px; + margin: 5px 0px; + width: 100%; + max-height: var(--q-contextmenu-max-height); + background-color: var(--blur_middle_standard); + background-clip: padding-box; + backdrop-filter: blur(8px); + font-size: 12px; + box-shadow: var(--shadow_bg_middle_secondary); + border: 1px solid var(--border_secondary); + border-radius: 4px; + box-sizing: border-box; + app-region: no-drag; + overflow-x: hidden; + overflow-y: auto; + list-style: none; + z-index: 999; + flex-direction: column; + align-items: stretch; + flex-wrap: nowrap; + justify-content: flex-start; + gap: 4px; +} + +#llonebot-error { + padding-top: 10px; + padding-bottom: 10px; + overflow: visible; + display: flex; + align-items: center; +} diff --git a/src/version.ts b/src/version.ts index 42fc3ff..dce4ea1 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.17.0" \ No newline at end of file